Changeset 3707052
- Timestamp:
- 09/22/2026 10:26:57 AM (4 weeks ago)
- Location:
- funnel-elite-lite
- Files:
-
- 58 edited
-
tags/1.0.2/admin/css/admin-bundle.css (modified) (11 diffs)
-
tags/1.0.2/admin/css/admin-style.css (modified) (2 diffs)
-
tags/1.0.2/admin/css/admin-tab-dashboard.css (modified) (4 diffs)
-
tags/1.0.2/admin/css/admin-tab-design.css (modified) (5 diffs)
-
tags/1.0.2/admin/includes/class-funnel-builder-manager.php (modified) (2 diffs)
-
tags/1.0.2/admin/includes/class-funnel-settings-manager.php (modified) (2 diffs)
-
tags/1.0.2/admin/js/admin-builder.js (modified) (1 diff)
-
tags/1.0.2/admin/js/admin-visual-nodes-trees.js (modified) (18 diffs)
-
tags/1.0.2/admin/traits/trait-admin-enqueue.php (modified) (4 diffs)
-
tags/1.0.2/admin/views/tabs/design/section-summary.php (modified) (8 diffs)
-
tags/1.0.2/includes/class-funnel-css-generator.php (modified) (7 diffs)
-
tags/1.0.2/includes/class-funnel-data.php (modified) (2 diffs)
-
tags/1.0.2/includes/class-funnel-i18n.php (modified) (7 diffs)
-
tags/1.0.2/includes/class-funnel-main.php (modified) (1 diff)
-
tags/1.0.2/includes/class-funnel-settings-registry.php (modified) (7 diffs)
-
tags/1.0.2/includes/emails/trait-email-formatter.php (modified) (1 diff)
-
tags/1.0.2/includes/emails/trait-email-preview.php (modified) (1 diff)
-
tags/1.0.2/includes/emails/trait-email-template.php (modified) (2 diffs)
-
tags/1.0.2/public/class-funnel-public.php (modified) (12 diffs)
-
tags/1.0.2/public/css/funnel-button-attention.css (modified) (2 diffs)
-
tags/1.0.2/public/css/funnel-button-effects.css (modified) (3 diffs)
-
tags/1.0.2/public/css/funnel-cards.css (modified) (9 diffs)
-
tags/1.0.2/public/css/funnel-core.css (modified) (10 diffs)
-
tags/1.0.2/public/css/funnel-core.min.css (modified) (1 diff)
-
tags/1.0.2/public/css/funnel-modal.css (modified) (6 diffs)
-
tags/1.0.2/public/css/funnel-utilities.css (modified) (2 diffs)
-
tags/1.0.2/public/js/funnel-motion.js (modified) (1 diff)
-
tags/1.0.2/public/js/modules/funnel-engine.js (modified) (9 diffs)
-
tags/1.0.2/public/js/modules/funnel-renderer.js (modified) (6 diffs)
-
trunk/admin/css/admin-bundle.css (modified) (11 diffs)
-
trunk/admin/css/admin-style.css (modified) (2 diffs)
-
trunk/admin/css/admin-tab-dashboard.css (modified) (4 diffs)
-
trunk/admin/css/admin-tab-design.css (modified) (5 diffs)
-
trunk/admin/includes/class-funnel-builder-manager.php (modified) (2 diffs)
-
trunk/admin/includes/class-funnel-settings-manager.php (modified) (2 diffs)
-
trunk/admin/js/admin-builder.js (modified) (1 diff)
-
trunk/admin/js/admin-visual-nodes-trees.js (modified) (18 diffs)
-
trunk/admin/traits/trait-admin-enqueue.php (modified) (4 diffs)
-
trunk/admin/views/tabs/design/section-summary.php (modified) (8 diffs)
-
trunk/includes/class-funnel-css-generator.php (modified) (7 diffs)
-
trunk/includes/class-funnel-data.php (modified) (2 diffs)
-
trunk/includes/class-funnel-i18n.php (modified) (7 diffs)
-
trunk/includes/class-funnel-main.php (modified) (1 diff)
-
trunk/includes/class-funnel-settings-registry.php (modified) (7 diffs)
-
trunk/includes/emails/trait-email-formatter.php (modified) (1 diff)
-
trunk/includes/emails/trait-email-preview.php (modified) (1 diff)
-
trunk/includes/emails/trait-email-template.php (modified) (2 diffs)
-
trunk/public/class-funnel-public.php (modified) (12 diffs)
-
trunk/public/css/funnel-button-attention.css (modified) (2 diffs)
-
trunk/public/css/funnel-button-effects.css (modified) (3 diffs)
-
trunk/public/css/funnel-cards.css (modified) (9 diffs)
-
trunk/public/css/funnel-core.css (modified) (10 diffs)
-
trunk/public/css/funnel-core.min.css (modified) (1 diff)
-
trunk/public/css/funnel-modal.css (modified) (6 diffs)
-
trunk/public/css/funnel-utilities.css (modified) (2 diffs)
-
trunk/public/js/funnel-motion.js (modified) (1 diff)
-
trunk/public/js/modules/funnel-engine.js (modified) (9 diffs)
-
trunk/public/js/modules/funnel-renderer.js (modified) (6 diffs)
Legend:
- Unmodified
- Added
- Removed
-
funnel-elite-lite/tags/1.0.2/admin/css/admin-bundle.css
r3706934 r3707052 43 43 select:focus, 44 44 textarea:focus { 45 /* Farbe aus dem Admin-Farbschema des Users, damit der Ring auch in 46 "Modern", "Midnight" usw. passt. outline bleibt vorhanden (transparent), 47 damit im Windows-Kontrastmodus ueberhaupt ein Ring gezeichnet wird. */ 45 /* Transparent outline keeps a visible ring in forced-colors mode. */ 48 46 border-color: var(--wp-admin-theme-color, #2271b1); 49 47 box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1); … … 241 239 } 242 240 243 /* Die Zentrierung rechnet mit der eingestellten Spaltenzahl. Ab 1400px 244 abwaerts schaltet das Raster auf auto-fill um, dort wuerde sie die 245 letzten Karten schmal quetschen oder in eine eigene Zeile schieben. */ 241 /* Below 1401px the grid uses auto-fill, where this centering breaks. */ 246 242 @media (min-width: 1401px) { 247 243 /* Center last card(s) if alone in incomplete row — only in cards view */ … … 1492 1488 } 1493 1489 1494 /* Parent muss position: relative haben*/1490 /* Parent needs position: relative */ 1495 1491 .funnel-group-box { 1496 1492 position: relative; … … 1524 1520 } 1525 1521 1526 /* Puls ieren effizienter: nur scale statt Opacity + optional*/1522 /* Pulse with scale only (cheaper than opacity) */ 1527 1523 @keyframes pulse-badge { 1528 1524 0%, … … 1537 1533 } 1538 1534 1539 /* Flash Animation beim Scroll */1535 /* Flash animation on scroll */ 1540 1536 .scroll-highlight { 1541 1537 animation: scroll-flash-blue 0.8s ease-in-out 5; … … 1674 1670 } 1675 1671 1676 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on Ne uerFunnel) */1672 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on New Funnel) */ 1677 1673 .actions-group .button .dashicons { 1678 1674 font-size: 18px; … … 2030 2026 font-weight: 600; 2031 2027 display: flex; 2032 /* Titel linksbündig neben dem Icon halten. 2033 Vorher stand hier justify-content: space-between, was mit den erwarteten 2034 zwei Flex-Items (Text + Pfeil) auch links ausrichtete. WordPress ersetzt 2035 im Admin aber Emoji durch <img class="emoji"> (wp-emoji-release.min.js) — 2036 damit werden aus dem einen Textknoten ZWEI Items, in Summe drei, und 2037 space-between schob das Icon nach links, den Titel in die Mitte und den 2038 Pfeil nach rechts. flex-start + margin-left:auto auf dem Pfeil ist 2039 unabhängig von der Anzahl der Items. */ 2028 /* Not space-between: WordPress turns emoji into <img>, adding flex items. */ 2040 2029 justify-content: flex-start; 2041 2030 gap: 8px; … … 2044 2033 } 2045 2034 2046 /* Der aufklapp-Pfeil (▼) ist immer das letzte Kind und bleibt rechts. 2047 Das inline style="float:right" im Markup ist auf einem Flex-Item wirkungslos. */ 2035 /* The arrow is always the last child. */ 2048 2036 .section-toggle > span:last-child { 2049 2037 margin-left: auto; … … 2088 2076 border-top: 1px solid #dcdcde; 2089 2077 } 2090 /* Funnel Design Tab: Spalten-Layout für Typography, Dimensions & Images */2078 /* Funnel Design Tab: column layout for Typography, Dimensions & Images */ 2091 2079 .funnel-dt-typo-grid--columns { 2092 2080 display: grid; … … 2108 2096 .funnel-design-sticky-save .submit { 2109 2097 margin: 0; 2110 /* Ohne padding:0 behaelt der Kern seine 10px/1.5em und die Leiste wird2111 121px hoch - auf einem Handy im Querformat fast die halbe Ansicht. */2112 2098 padding: 0; 2113 2099 text-align: center; … … 2115 2101 2116 2102 .funnel-design-sticky-save .button-hero { 2117 /* schlaegt das globale .button-hero{margin-top:20px!important} */2103 /* Overrides core .button-hero { margin-top: 20px !important; } */ 2118 2104 margin-top: 0 !important; 2119 2105 } -
funnel-elite-lite/tags/1.0.2/admin/css/admin-style.css
r3706934 r3707052 42 42 select:focus, 43 43 textarea:focus { 44 /* Farbe aus dem Admin-Farbschema des Users, damit der Ring auch in 45 "Modern", "Midnight" usw. passt. outline bleibt vorhanden (transparent), 46 damit im Windows-Kontrastmodus ueberhaupt ein Ring gezeichnet wird. */ 44 /* Transparent outline keeps a visible ring in forced-colors mode. */ 47 45 border-color: var(--wp-admin-theme-color, #2271b1); 48 46 box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1); … … 240 238 } 241 239 242 /* Die Zentrierung rechnet mit der eingestellten Spaltenzahl. Ab 1400px 243 abwaerts schaltet das Raster auf auto-fill um, dort wuerde sie die 244 letzten Karten schmal quetschen oder in eine eigene Zeile schieben. */ 240 /* Below 1401px the grid uses auto-fill, where this centering breaks. */ 245 241 @media (min-width: 1401px) { 246 242 /* Center last card(s) if alone in incomplete row — only in cards view */ -
funnel-elite-lite/tags/1.0.2/admin/css/admin-tab-dashboard.css
r3589214 r3707052 443 443 } 444 444 445 /* Parent muss position: relative haben*/445 /* Parent needs position: relative */ 446 446 .funnel-group-box { 447 447 position: relative; … … 475 475 } 476 476 477 /* Puls ieren effizienter: nur scale statt Opacity + optional*/477 /* Pulse with scale only (cheaper than opacity) */ 478 478 @keyframes pulse-badge { 479 479 0%, … … 488 488 } 489 489 490 /* Flash Animation beim Scroll */490 /* Flash animation on scroll */ 491 491 .scroll-highlight { 492 492 animation: scroll-flash-blue 0.8s ease-in-out 5; … … 625 625 } 626 626 627 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on Ne uerFunnel) */627 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on New Funnel) */ 628 628 .actions-group .button .dashicons { 629 629 font-size: 18px; -
funnel-elite-lite/tags/1.0.2/admin/css/admin-tab-design.css
r3706934 r3707052 17 17 font-weight: 600; 18 18 display: flex; 19 /* Titel linksbündig neben dem Icon halten. 20 Vorher stand hier justify-content: space-between, was mit den erwarteten 21 zwei Flex-Items (Text + Pfeil) auch links ausrichtete. WordPress ersetzt 22 im Admin aber Emoji durch <img class="emoji"> (wp-emoji-release.min.js) — 23 damit werden aus dem einen Textknoten ZWEI Items, in Summe drei, und 24 space-between schob das Icon nach links, den Titel in die Mitte und den 25 Pfeil nach rechts. flex-start + margin-left:auto auf dem Pfeil ist 26 unabhängig von der Anzahl der Items. */ 19 /* Not space-between: WordPress turns emoji into <img>, adding flex items. */ 27 20 justify-content: flex-start; 28 21 gap: 8px; … … 31 24 } 32 25 33 /* Der aufklapp-Pfeil (▼) ist immer das letzte Kind und bleibt rechts. 34 Das inline style="float:right" im Markup ist auf einem Flex-Item wirkungslos. */ 26 /* The arrow is always the last child. */ 35 27 .section-toggle > span:last-child { 36 28 margin-left: auto; … … 75 67 border-top: 1px solid #dcdcde; 76 68 } 77 /* Funnel Design Tab: Spalten-Layout für Typography, Dimensions & Images */69 /* Funnel Design Tab: column layout for Typography, Dimensions & Images */ 78 70 .funnel-dt-typo-grid--columns { 79 71 display: grid; … … 95 87 .funnel-design-sticky-save .submit { 96 88 margin: 0; 97 /* Ohne padding:0 behaelt der Kern seine 10px/1.5em und die Leiste wird98 121px hoch - auf einem Handy im Querformat fast die halbe Ansicht. */99 89 padding: 0; 100 90 text-align: center; … … 102 92 103 93 .funnel-design-sticky-save .button-hero { 104 /* schlaegt das globale .button-hero{margin-top:20px!important} */94 /* Overrides core .button-hero { margin-top: 20px !important; } */ 105 95 margin-top: 0 !important; 106 96 } -
funnel-elite-lite/tags/1.0.2/admin/includes/class-funnel-builder-manager.php
r3706934 r3707052 309 309 ]; 310 310 311 // Unbekannte Meta-Keys des Bestands erhalten. 312 // 313 // Das Array oben wird bei jedem Speichern vollstaendig aus $_POST neu 314 // gebaut. Alles, was diese Version nicht kennt, waere damit still weg, 315 // sobald jemand die Karte einmal oeffnet und speichert — in Lite z. B. die 316 // komplette Konfiguration der Pro-Kartentypen (Slider, Mengen-Items, 317 // Distance Map, Consent-Texte, Pricing-Table-Spalten), die ueber einen 318 // Import aus Pro in der Datenbank liegt. 319 // 320 // Bekannte Keys gewinnen, weil sie oben immer gesetzt werden; unbekannte 321 // bleiben unveraendert stehen. 311 // Keep stored meta keys this version does not know (e.g. imported from Pro). 322 312 if ( ! empty( $_POST['id'] ) ) { 323 313 $existing_raw = $this->db->get_var( $this->db->prepare( "SELECT meta_data FROM {$this->table_cards} WHERE id = %d", intval( wp_unslash( $_POST['id'] ) ) ) ); … … 368 358 369 359 /** 370 * Kartenpreis aus dem Karten-Modal lesen. 360 * Parse a typed price. With both separators the last one is the decimal 361 * mark; several of the same kind are thousands separators. 371 362 * 372 * Das Feld ist ein Textfeld (ohne die Pfeile eines Zahlenfelds), es kommt 373 * also an, was getippt wurde. Frueher genuegte str_replace(',', '.') - das 374 * Zahlenfeld liess "1.200,00" gar nicht zu. Jetzt wuerde daraus 1.2. 375 * 376 * - Komma UND Punkt: das hintere Zeichen trennt die Dezimalen, das andere 377 * die Tausender ("1.200,00", "1,200.00"). 378 * - Nur Kommas: eines ist das Dezimalzeichen ("1200,00", "1,2" fuer ×1,2), 379 * mehrere trennen Tausender. 380 * - Nur Punkte: einer ist das Dezimalzeichen ("1200.50"), mehrere trennen 381 * Tausender ("1.200.000"). "1.200" bleibt 1.2 wie bisher - nicht eindeutig. 382 * - Leerzeichen und Waehrungszeichen fallen weg. 383 * 384 * @param string $raw Eingabe aus dem Preisfeld. 363 * @param string $raw Raw price field value. 385 364 * @return float 386 365 */ -
funnel-elite-lite/tags/1.0.2/admin/includes/class-funnel-settings-manager.php
r3706934 r3707052 31 31 32 32 /** 33 * Keep an email template only if it was actually edited. 34 * 35 * The editor in the Email tab is pre-filled with the default template, so 36 * ANY save on that tab — a new SMTP password, a changed colour — writes the 37 * whole default into the project settings, even when nobody touched the 38 * HTML. From that moment the email is that frozen HTML: it keeps the 39 * language it happened to have at the time and never follows the site 40 * again. That is how a German site ends up sending a half-English email, 41 * and no plugin update can repair it, because the database wins. 42 * 43 * An empty field means "use the default", and the default is built fresh on 44 * every send, so it speaks the language of the site. 45 * 46 * Both sides are compared the way they are stored: the POST value has gone 47 * through wp_kses_post(), which strips <html>/<body> out of the template, 48 * and a textarea submits CRLF where the source has LF. Without normalising 49 * both, nothing would ever match and the guard would be dead code. 33 * Store the template only if it differs from the default, so an untouched 34 * one keeps following the site language. 50 35 * 51 36 * @param string $posted The already sanitized POST value. … … 62 47 63 48 /** 64 * The default admin template, or an empty string if the mail class is not 65 * loaded — then nothing can match and the value is kept as posted. 49 * Default admin template, or '' if the mail class is not loaded. 66 50 */ 67 51 private static function default_admin_template() { -
funnel-elite-lite/tags/1.0.2/admin/js/admin-builder.js
r3706934 r3707052 380 380 form.find('input[name="answer"]').val(answer); 381 381 form.find('input[name="image_url"]').val(img); 382 // Ohne Preis leer lassen, damit der Platzhalter ("z. B. 1200,00") zu sehen ist.382 // Empty instead of 0, so the placeholder shows. 383 383 form.find('input[name="price"]').val(parseFloat(price) ? price : ""); 384 384 form.find('select[name="price_operator"]').val(operator); -
funnel-elite-lite/tags/1.0.2/admin/js/admin-visual-nodes-trees.js
r3706934 r3707052 26 26 let lastRenderedSource = ""; // Performance cache 27 27 28 // Welche Karte gerade steht (Routen-Filter) und ob der Admin den Zoom selbst 29 // gewaehlt hat oder die Karte nur eingepasst ist. Siehe "ANSICHT MERKEN". 28 // zoomChosen: zoom set by the user (false = fitted to the container). 30 29 let currentFilter = "all"; 31 30 let zoomChosen = false; … … 92 91 93 92 // =================================== 94 // ANSICHT MERKEN (Zoom + Scrollposition) 95 // =================================== 96 97 // Nach dem Speichern laedt die Seite neu, und die Karte wurde dabei jedes Mal 98 // wieder eingepasst - wer hineingezoomt hatte, musste die Stelle neu suchen. 99 // Gemerkt wird im Browser, je Funnel und je Routen-Filter: jeder Filter 100 // zeichnet eine andere Karte, deren Stelle nicht auf die naechste passt. 93 // REMEMBER VIEW (per funnel and route filter) 94 // =================================== 101 95 102 96 function mapProjectId() { … … 125 119 localStorage.setItem(mapViewKey(), JSON.stringify(views)); 126 120 } catch (e) { 127 // Privates Fenster oder voller Speicher: dann eben ohne Gedaechtnis.121 // Storage unavailable (private window, quota). 128 122 } 129 123 } … … 131 125 function saveMapView() { 132 126 const scroller = document.getElementById("visual-map-scroll-container"); 133 // Solange die gemerkte Ansicht nicht angewendet ist, wuerde der Zwischenstand 134 // sie ueberschreiben. Und versteckt (anderer Tab, eingeklappte Karte) liest 135 // sich jede Scrollposition als 0. 127 // Hidden containers report 0; never overwrite a view not yet applied. 136 128 if (!viewApplied || !scroller || !$(scroller).is(":visible")) return; 137 129 if (!DOM.$graphContainer.find("svg").length) return; … … 139 131 const views = readMapViews(); 140 132 views[currentFilter] = { 141 // null = nur eingepasst. Waechst die Karte, passt sie sich dann neu ein, 142 // statt auf einem veralteten Einpass-Wert stehen zu bleiben. 133 // null = fitted, so a grown map is fitted again. 143 134 zoom: zoomChosen ? currentZoom : null, 144 135 left: scroller.scrollLeft, … … 154 145 } 155 146 156 // Der Routen-Filter selbst sprang nach dem Neuladen auf "Alle" zurueck - die 157 // fuer einen Filter gemerkte Stelle kam so nie zum Zug. Ebenfalls je Funnel. 158 // "Alle" ist der Normalfall und wird nicht gespeichert. 147 // "all" is the default and is not stored. 159 148 160 149 function mapFilterKey() { … … 170 159 } 171 160 } catch (e) { 172 // Privates Fenster oder voller Speicher: dann eben ohne Gedaechtnis.161 // Storage unavailable (private window, quota). 173 162 } 174 163 } 175 164 176 165 /** 177 * Den gemerkten Filter zurueck in die Auswahl schreiben, bevor zum ersten 178 * Mal gezeichnet wird. Gibt es die Route nicht mehr (Karte umbenannt oder 179 * geloescht), bleibt "Alle" stehen und der alte Wert wird vergessen. 166 * Put the stored filter back into the dropdown; forget it if the route is gone. 180 167 */ 181 168 function restoreMapFilter() { … … 201 188 202 189 /** 203 * Karte einpassen und die gemerkte Ansicht wiederherstellen, falls es eine 204 * gibt. Nur bei sichtbarem Container aufrufen - versteckt nimmt der Browser 205 * keine Scrollposition an. 190 * Fit the map and restore the stored view. Call only on a visible container. 206 191 */ 207 192 function showMapView() { … … 350 335 if (!sourceCode) return; 351 336 352 // Ohne Angabe gilt, was in der Auswahl steht. Vorher zeichnete z. B. der353 // Wechsel zurueck auf den Builder-Tab "Alle", waehrend die Auswahl noch354 // einen Filter zeigte.355 337 if (filterRoute === undefined) { 356 338 filterRoute = $("#pages-cards-route-filter").val() || "all"; … … 363 345 // PERFORMANCE: Skip expensive render if source is unchanged 364 346 if (sourceCode === lastRenderedSource) { 365 // Same map shown again (e.g. back on the Builder tab): skip DOM 366 // generation, but put the view back where it was. 347 // Unchanged source: skip rendering, restore the view. 367 348 showMapView(); 368 349 return; … … 421 402 }); 422 403 423 // Fit map after render. Shown first: the stored scroll position only 424 // sticks on a visible container. 404 // Show first: scroll positions only stick on a visible container. 425 405 setTimeout(() => { 426 406 const $collapsible = $("#visual-pages-cards-collapsible"); … … 475 455 }); 476 456 477 // Zuruecksetzen heisst auch vergessen: nach dem Neuladen wird wieder478 // eingepasst.479 457 $(".zoom-map-reset").on("click", (e) => { 480 458 e.preventDefault(); … … 483 461 }); 484 462 485 // Die Scrollposition laufend mitschreiben. "pagehide" faengt den letzten 486 // Stand ab, wenn direkt nach dem Scrollen gespeichert und neu geladen wird. 463 // pagehide catches a scroll right before a reload. 487 464 $("#visual-map-scroll-container").on("scroll", debounce(saveMapView, 150)); 488 465 window.addEventListener("pagehide", saveMapView); … … 697 674 698 675 /** 699 * Apply the stored collapsed state to the map panel. 700 * 701 * Runs without Mermaid so the panel is in its final state even when the 702 * library is never loaded. 676 * Apply the stored collapsed state (works without Mermaid). 703 677 * 704 678 * @return {boolean} true when the map is collapsed. … … 720 694 721 695 function initMermaidAndRender() { 722 // loadMermaidIfNeeded() resolves even when the local file fails to load 723 // (fail-silently, WP.org Guideline #8 — no CDN fallback). Guard so a missing 724 // library cannot throw here. 696 // loadMermaidIfNeeded() also resolves when the file failed to load. 725 697 if (typeof mermaid === "undefined") { 726 698 applyCollapsedState(); … … 733 705 securityLevel: "loose", 734 706 htmlLabels: true, 735 // NO size limit on the map. 736 // 737 // Mermaid stops at 50 000 characters by default and REPLACES the diagram 738 // with "Maximum text size in diagram exceeded" (it does not throw: 739 // text.length > (config.maxTextSize ?? 5e4) && (text = errorGraph) 740 // ). That guard is meant for UNTRUSTED input; here the definition is 741 // generated from the admin's own funnel, so the default only hides the 742 // map once a funnel grows - after roughly ten steps, because the length 743 // comes from the labels rather than from the structure. 744 // 745 // MAX_SAFE_INTEGER rather than Infinity: the comparison is numeric either 746 // way, but a plain number survives any clone of the config object, while 747 // Infinity can come back as null - and `?? 5e4` would then quietly put 748 // the default back. 707 // No size limit (the source is built by the plugin). Not Infinity: it can 708 // become null when the config is cloned, which restores the default. 749 709 maxTextSize: Number.MAX_SAFE_INTEGER, 750 710 maxEdges: Number.MAX_SAFE_INTEGER, … … 791 751 // Global Initialization Boundary 792 752 try { 793 // The stored route filter goes back into the dropdown before anything is794 // drawn, so the first render already uses it.795 753 restoreMapFilter(); 796 754 797 // Put the panel into its stored state first — this must happen whether or798 // not Mermaid ever loads.799 755 const startsCollapsed = applyCollapsedState(); 800 756 … … 806 762 loadMermaidIfNeeded().then(initMermaidAndRender); 807 763 } else { 808 // Nothing to draw yet. Mermaid is ~650 KB, so it stays unloaded until the 809 // map is actually going to be shown: either the Builder tab is opened 810 // (with the map expanded) or the user expands the collapsed map. 764 // Mermaid (~650 KB) loads only once the map is shown. 811 765 $(document).on("click.funnelMermaidLazy", '.nav-tab[href="#tab-builder"]', function () { 812 766 if (localStorage.getItem("funnel_pages_cards_collapsed") === "true") return; -
funnel-elite-lite/tags/1.0.2/admin/traits/trait-admin-enqueue.php
r3706934 r3707052 30 30 31 31 /** 32 * Is the current admin screen one of the plugin's own? 33 * 34 * WordPress passes the screen's hook suffix to admin_enqueue_scripts 35 * ('toplevel_page_funnel-elite-lite'); $_GET['page'] is kept as a fallback 36 * for the AJAX-rendered tabs, which reach this code without a suffix. 32 * Is the current admin screen the plugin's own page? 37 33 * 38 34 * @param string $hook_suffix Hook suffix handed over by WordPress. … … 51 47 52 48 public function enqueue_styles( $hook_suffix = '' ) { 53 // Nur auf der eigenen Plugin-Seite laden. Das Bundle gestaltet Labels,54 // Eingabefelder und Buttons global um; auf fremden Seiten hat es55 // Kern-Formulare, Listentabellen und den Block-Editor veraendert.56 // Das Plugin zeichnet ausserhalb seiner Seite nichts (kein57 // admin_notices, kein admin_bar_menu, kein Dashboard-Widget).58 49 if ( ! $this->is_plugin_admin_page( $hook_suffix ) ) { 59 50 return; … … 83 74 . '.slider:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background-color:#fff;-webkit-transition:.4s;transition:.4s}' 84 75 . 'input:checked+.slider{background-color:#d63384}' 85 // Tastatur-Fokus: der echte Haken ist 0x0 und unsichtbar, ohne86 // diesen Ring sieht man beim Tabben nicht, welcher Schalter dran ist.87 76 . 'input:focus-visible+.slider{outline:2px solid var(--wp-admin-theme-color,#2271b1);outline-offset:2px}' 88 77 . 'input:checked+.slider:before{-webkit-transform:translateX(26px);-ms-transform:translateX(26px);transform:translateX(26px)}' … … 163 152 $mermaid_rel = 'js/vendor/mermaid.min.js'; 164 153 $mermaid_abs = FUNNEL_ELITE_PATH . 'admin/' . $mermaid_rel; 165 // Mit Dateiversion: Mermaid wird per JS nachgeladen und bekommt deshalb 166 // keine ?ver von wp_enqueue_script. Ohne sie nimmt der Browser nach einem 167 // Update die alte Datei aus dem Cache - z. B. mit der alten Kartenreihenfolge. 154 // Lazy-loaded by JS, so it needs its own cache-busting version. 168 155 $mermaid_url = file_exists( $mermaid_abs ) 169 156 ? FUNNEL_ELITE_URL . 'admin/' . $mermaid_rel . '?ver=' . rawurlencode( $this->file_version( $mermaid_abs ) ) -
funnel-elite-lite/tags/1.0.2/admin/views/tabs/design/section-summary.php
r3706934 r3707052 82 82 <div class="funnel-dt-section-title"><?php echo esc_html( Funnel_System_I18n::get_admin('dt_typo_spacing_title') ); ?></div> 83 83 <div class="funnel-dt-details-body"> 84 <!-- 💻 DESKTOP -->85 84 <h4 style="margin:0 0 10px; color:#475569; font-size:12px; text-transform:uppercase; letter-spacing:0.5px;">💻 <?php echo esc_html( Funnel_System_I18n::get_admin('dt_desktop') ); ?></h4> 86 85 <div class="funnel-dt-typo-grid"> … … 129 128 </div> 130 129 <div class="funnel-dt-typo-divider"></div> 131 <!-- 📱 MOBILE -->132 130 <?php 133 // Ein leeres Mobil-Feld erbt den Desktop-Wert. Damit der 134 // Platzhalter nicht etwas anderes verspricht, zeigt er 135 // genau diesen Wert an. 136 $dt_wert = static function ( $key, $fallback ) use ( $project_settings ) { 131 // Placeholder shows the desktop value an empty mobile field inherits. 132 $dt_value = static function ( $key, $fallback ) use ( $project_settings ) { 137 133 return ( isset( $project_settings[ $key ] ) && '' !== $project_settings[ $key ] ) 138 134 ? $project_settings[ $key ] … … 145 141 <label for="mobile_summary_title_size"><?php echo esc_html( Funnel_System_I18n::get_admin('title_font_size_label') ); ?></label> 146 142 <div class="funnel-dt-input-wrap"> 147 <input name="mobile_summary_title_size" type="number" id="mobile_summary_title_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_title_size']) ? $project_settings['mobile_summary_title_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_ wert( 'summary_title_size', 32 ) ); ?>" class="small-text" min="10" max="60">143 <input name="mobile_summary_title_size" type="number" id="mobile_summary_title_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_title_size']) ? $project_settings['mobile_summary_title_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_title_size', 32 ) ); ?>" class="small-text" min="10" max="60"> 148 144 <span class="funnel-dt-unit">px</span> 149 145 </div> … … 152 148 <label for="mobile_summary_text_size"><?php echo esc_html( Funnel_System_I18n::get_admin('text_font_size_label') ); ?></label> 153 149 <div class="funnel-dt-input-wrap"> 154 <input name="mobile_summary_text_size" type="number" id="mobile_summary_text_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_text_size']) ? $project_settings['mobile_summary_text_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_ wert( 'summary_text_size', 16 ) ); ?>" class="small-text" min="10" max="60">150 <input name="mobile_summary_text_size" type="number" id="mobile_summary_text_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_text_size']) ? $project_settings['mobile_summary_text_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_text_size', 16 ) ); ?>" class="small-text" min="10" max="60"> 155 151 <span class="funnel-dt-unit">px</span> 156 152 </div> … … 159 155 <label for="mobile_summary_label_font_size"><?php echo esc_html( Funnel_System_I18n::get_admin('price_label_font_size_label') ); ?></label> 160 156 <div class="funnel-dt-input-wrap"> 161 <input name="mobile_summary_label_font_size" type="number" id="mobile_summary_label_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_label_font_size']) ? $project_settings['mobile_summary_label_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_ wert( 'summary_label_font_size', 14 ) ); ?>" class="small-text" min="10" max="60">157 <input name="mobile_summary_label_font_size" type="number" id="mobile_summary_label_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_label_font_size']) ? $project_settings['mobile_summary_label_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_label_font_size', 14 ) ); ?>" class="small-text" min="10" max="60"> 162 158 <span class="funnel-dt-unit">px</span> 163 159 </div> … … 166 162 <label for="mobile_price_font_size"><?php echo esc_html( Funnel_System_I18n::get_admin('price_font_size_label') ); ?></label> 167 163 <div class="funnel-dt-input-wrap"> 168 <input name="mobile_price_font_size" type="number" id="mobile_price_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_price_font_size']) ? $project_settings['mobile_price_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_ wert( 'price_font_size', 32 ) ); ?>" class="small-text" min="10" max="80">164 <input name="mobile_price_font_size" type="number" id="mobile_price_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_price_font_size']) ? $project_settings['mobile_price_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'price_font_size', 32 ) ); ?>" class="small-text" min="10" max="80"> 169 165 <span class="funnel-dt-unit">px</span> 170 166 </div> … … 173 169 <label for="mobile_summary_hint_size"><?php echo esc_html( Funnel_System_I18n::get_admin('hint_font_size_label') ); ?></label> 174 170 <div class="funnel-dt-input-wrap"> 175 <input name="mobile_summary_hint_size" type="number" id="mobile_summary_hint_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_hint_size']) ? $project_settings['mobile_summary_hint_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_ wert( 'summary_hint_size', 14 ) ); ?>" class="small-text" min="10" max="60">171 <input name="mobile_summary_hint_size" type="number" id="mobile_summary_hint_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_hint_size']) ? $project_settings['mobile_summary_hint_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_hint_size', 14 ) ); ?>" class="small-text" min="10" max="60"> 176 172 <span class="funnel-dt-unit">px</span> 177 173 </div> … … 228 224 </table> 229 225 230 <!-- ── Ergebnisseite: Weiter-Button (optional, leer = global)── -->226 <!-- ── Continue Button ─────────────────────────────── --> 231 227 <div class="funnel-dt-details funnel-dt-details--static" style="margin-top:12px;"> 232 228 <div class="funnel-dt-section-title"><?php echo esc_html( Funnel_System_I18n::get_admin('dt_summary_btn_sizes_title') ); ?></div> -
funnel-elite-lite/tags/1.0.2/includes/class-funnel-css-generator.php
r3706934 r3707052 248 248 $summary_hint_size = ! empty( $settings['summary_hint_size'] ) ? $settings['summary_hint_size'] : 14; 249 249 250 // Ergebnisseite auf dem Telefon. Ein leeres Feld erbt bewusst den 251 // Desktop-Wert - so aendert sich an bestehenden Funnels nichts, solange 252 // niemand ein Mobil-Feld ausfuellt. Der Platzhalter im Design-Tab zeigt 253 // deshalb genau diesen Desktop-Wert an. 250 // Empty mobile fields inherit the desktop value. 254 251 $mobile_summary_title_size = ! empty( $settings['mobile_summary_title_size'] ) ? $settings['mobile_summary_title_size'] : $summary_title_size; 255 252 $mobile_summary_text_size = ! empty( $settings['mobile_summary_text_size'] ) ? $settings['mobile_summary_text_size'] : $summary_text_size; … … 270 267 $back_btn_color = $settings['back_btn_color']; 271 268 $modal_bg_color = $settings['modal_bg_color']; 272 // Als rgba ausgeben: das Video-Overlay braucht die 0.96 Deckkraft,273 // ein reiner Hex-Wert wuerde es undurchsichtig machen.274 269 $modal_overlay_rgba = self::hexToRgba( $modal_bg_color, 0.96 ); 275 270 $max_width = $settings['max_content_width']; … … 365 360 $card_bg_3 = self::hexToRgba($settings['card_bg_color_3'], $card_bg_intensity); 366 361 367 /* 368 * Summary button overrides (Ergebnisseite). 369 * 370 * Every field is optional and means "inherit the global button style" when 371 * empty, so only the properties the admin actually filled in are emitted. 372 * An untouched funnel therefore produces no rule at all and looks exactly 373 * as it did before these settings existed. 374 * 375 * The selector is #funnelbody #btn-summary-next: two IDs beat the global 376 * "#funnelbody .btn-primary" rule, and !important is repeated because that 377 * rule carries it too (see the card hover note — an important declaration 378 * only loses to another important one). 379 */ 362 // Summary button overrides: only filled fields are emitted, empty ones 363 // inherit the global button style (which is !important, hence here too). 380 364 $summary_btn_decls = array(); 381 365 … … 545 529 } 546 530 547 /* Hier standen die beiden Regeln zur Einstellung Layout Format548 (Views). Kein PHP und kein JS hat die zugehoerigen Klassen je an549 #funnelbody gehaengt, die Auswahl war also wirkungslos;550 ausserdem hat der Inline-Minifier die Leerzeichen um das Plus in551 ihrer calc()-Rechnung entfernt und sie damit ungueltig gemacht.552 Einstellung und Regeln sind entfernt.553 Fuer eine echte Vollbreite gibt es die Einstellung Vollbreite erzwingen. */554 555 531 #funnelbody h1, #funnelbody h2 { 556 532 color: var(--funnel-headline); … … 590 566 } 591 567 592 /* Hover/focus background MUST repeat the !important. 593 funnel-cards.css sets --funnel-card-hover-bg on :hover without it, 594 and an !important declaration always wins over a normal one — 595 regardless of specificity or order. The Hover Color setting 596 therefore had no visible effect at all until this rule existed. 597 Do not drop the !important here without also dropping it above. */ 568 /* Needs !important to beat the card background rule. */ 598 569 .st-funnel-card .card:hover, 599 570 .st-funnel-card:focus-visible .card { … … 706 677 #funnelbody .st-funnel-card img { height: {$mobile_img_height}px !important; } 707 678 708 /* Ergebnisseite. Die Variablen oben decken auch die Inline-Styles709 ab, die der Renderer setzt; diese Regeln sind fuer alles, was710 ueber das Stylesheet kommt. */711 679 #funnelbody .summary-title { font-size: {$mobile_summary_title_size}px !important; } 712 680 #funnelbody .summary-text { font-size: {$mobile_summary_text_size}px !important; } … … 752 720 margin-right: 0 !important; 753 721 } 754 /* Offsetbasiert statt 100vw/-50vw: der alte Trick stimmt nur, wenn 755 die Inhaltsspalte mittig im Fenster steht. In einem Layout mit 756 Seitenleiste schob er den Funnel aus dem Bild (gemessen bei 1440px: 757 240px links abgeschnitten). Die beiden Werte setzt das JS aus der 758 tatsaechlichen Position; ohne JS bleibt alles in der Spalte. */ 722 /* --funnel-bleed-l/-r are measured by JS. */ 759 723 body:not(.funnel-active-page) #funnelbody { 760 724 width: auto !important; -
funnel-elite-lite/tags/1.0.2/includes/class-funnel-data.php
r3706934 r3707052 5 5 6 6 /** 7 * Card meta keys that must never reach the frontend. 8 * 9 * funnelDataRaw is printed into the page source and the whole meta_data array 10 * is merged into it, so every key in there is public by default. 11 * 12 * Lite has no UI for these — they can only arrive through an import of a funnel 13 * that was built in Pro, where the export carries meta_data 1:1. The data is 14 * kept (so a Pro -> Lite -> Pro round trip stays lossless) but never published. 7 * Card meta keys (imported from Pro) that are kept but never printed to the page. 15 8 */ 16 9 const ADMIN_ONLY_META_KEYS = array( 'admin_note' ); … … 121 114 ], $meta); 122 115 123 // Internal Pro-only fields must not be published — see the constant.124 116 foreach ( self::ADMIN_ONLY_META_KEYS as $admin_only_key ) { 125 117 unset( $relation_entry[ $admin_only_key ] ); -
funnel-elite-lite/tags/1.0.2/includes/class-funnel-i18n.php
r3706934 r3707052 41 41 42 42 /** 43 * Cache key for the string tables. 44 * 45 * Keyed by locale so switch_to_locale() still returns the right table. 46 * Calls that happen before `init` can still be untranslated (WordPress loads 47 * plugin translations just-in-time), so those get their own slot and never 48 * poison the translated table. 43 * Cache key per locale; calls before `init` get their own untranslated slot. 49 44 */ 50 45 private static function string_cache_key() { … … 64 59 /** 65 60 * Get strings for the Admin Backend (built once per locale and request). 66 *67 * PERFORMANCE: the table below holds ~515 __() lookups. It used to be rebuilt68 * on every single get_admin() call — with ~530 call sites in the admin views69 * that added up to ~275,000 gettext lookups per admin page render (measured:70 * 107ms of pure PHP, 0.2ms with this cache). Never drop this cache.71 61 */ 72 62 public static function get_admin_strings() { … … 232 222 233 223 'card_icon_label' => __( 'Icon (Lucide)', 'funnel-elite-lite' ), 234 // UI Labels aus Design-Vorlage224 // UI labels from the design template 235 225 'icon_size_label' => __( 'Icon Size', 'funnel-elite-lite' ), 236 226 'icon_color_label' => __( 'Icon Color', 'funnel-elite-lite' ), … … 238 228 239 229 'card_price_label' => __( 'Card Price', 'funnel-elite-lite' ), 240 // Platzhalter im Preisfeld - je Sprache mit deren Dezimalzeichen uebersetzen.230 // translators: example price, use the decimal separator of your language. 241 231 'card_price_placeholder' => __( 'e.g. 1200.00', 'funnel-elite-lite' ), 242 232 'card_price_help_text' => __( 'Operator + price set how this card changes the running total: + adds and − subtracts the price; × and ÷ multiply or divide the total (e.g. ×1.2 = +20%).', 'funnel-elite-lite' ), … … 584 574 'sort_order' => __( 'Sort Order', 'funnel-elite-lite' ), 585 575 'video_url_optional' => __( 'Video URL (Optional)', 'funnel-elite-lite' ), 586 // Knopf im Seiten-Modal - vorher stand dort "Save Card". Muss zum Titel587 // edit_step ("Edit Page") passen.588 576 'save_step' => __( 'Save Page', 'funnel-elite-lite' ), 589 577 'edit_card' => __( 'Edit Card', 'funnel-elite-lite' ), … … 675 663 'dt_price_box_content_title' => __( 'Price Box Content', 'funnel-elite-lite' ), 676 664 'dt_summary_colors_title' => __( 'Summary Colors', 'funnel-elite-lite' ), 677 // Ergebnisseite: Weiter-Button (optionale Overrides)678 665 'dt_summary_btn_colors_title' => __( 'Continue Button (Result Page)', 'funnel-elite-lite' ), 679 666 'dt_summary_btn_sizes_title' => __( 'Continue Button Dimensions (Result Page)', 'funnel-elite-lite' ), … … 693 680 /** 694 681 * Get strings for the Public Frontend (built once per locale and request). 695 *696 * Same caching rationale as get_admin_strings().697 682 */ 698 683 public static function get_public_strings() { -
funnel-elite-lite/tags/1.0.2/includes/class-funnel-main.php
r3706934 r3707052 60 60 $this->loader->add_action( 'wp_ajax_funnel_partial_lead', $plugin_public, 'handle_partial_lead' ); 61 61 $this->loader->add_action( 'wp_ajax_nopriv_funnel_partial_lead', $plugin_public, 'handle_partial_lead' ); 62 // Frische Sicherheitsmarke, wenn ein Seiten-Cache eine abgelaufene ausliefert.63 62 $this->loader->add_action( 'wp_ajax_funnel_fresh_nonce', $plugin_public, 'handle_fresh_nonce' ); 64 63 $this->loader->add_action( 'wp_ajax_nopriv_funnel_fresh_nonce', $plugin_public, 'handle_fresh_nonce' ); -
funnel-elite-lite/tags/1.0.2/includes/class-funnel-settings-registry.php
r3706934 r3707052 204 204 205 205 /** 206 * Sanitize an optional pixel value for the summary button overrides. 207 * 208 * These fields mean *inherit the global button style* when left empty, so the 209 * empty string has to survive sanitizing. intval('') would be 0 — and 0 is a 210 * legitimate value here (sharp corners, no border, no padding), so the two 211 * cases must stay distinguishable. 206 * Optional pixel value: '' means inherit, 0 is a valid value. 212 207 * 213 208 * @param mixed $value Raw value. … … 228 223 229 224 /** 230 * Sanitize an optional colour for the summary button overrides. 231 * 232 * Same idea as sanitize_optional_px(): empty means inherit. 225 * Optional colour: '' means inherit. 233 226 * 234 227 * @param mixed $value Raw value. … … 325 318 [ 'id' => 'card_bg_intensity', 'sanitize' => 'intval', 'default' => 100 ], 326 319 [ 'id' => 'card_text_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#212529', 'css_var' => '--funnel-card-text' ], 327 // #007bff liess die Kartenbeschreibung beim Ueberfahren und bei328 // Tastatur-Auswahl auf 3.10:1 fallen; #e9ecef bringt sie auf 7.2:1.329 320 [ 'id' => 'card_hover_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#e9ecef', 'css_var' => '--funnel-card-hover-bg' ], 330 321 [ 'id' => 'card_width', 'sanitize' => 'intval', 'default' => 300, 'unit' => 'px' ], … … 400 391 401 392 // ── Buttons ────────────────────────────────────────── 402 // Weiss auf dem Standard-Sandton #c3b696 sind 2.0:1 - unlesbar.403 // #1d2327 (WordPress-Schwarz) ergibt 7.9:1.404 393 [ 'id' => 'btn_text_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#1d2327', 'css_var' => '--funnel-btn-text' ], 405 394 [ 'id' => 'btn_border_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#c3b696', 'css_var' => '--funnel-btn-border-color' ], … … 423 412 424 413 // ── Summary & Price ────────────────────────────────── 425 // Die fuenf mobile_*-Schriftgroessen der Ergebnisseite sind optional: 426 // leer bedeutet, dass der Desktop-Wert auch auf dem Telefon gilt. 427 // Ein fester Standard wuerde bestehende Funnels beim Update 428 // ungefragt umstellen. 414 // mobile_* sizes are optional: empty = desktop value. 429 415 [ 'id' => 'price_font_size', 'sanitize' => 'intval', 'default' => 32, 'css_var' => '--funnel-price-font-size', 'unit' => 'px' ], 430 416 [ 'id' => 'mobile_price_font_size', 'sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ], … … 433 419 [ 'id' => 'summary_label_font_size','sanitize' => 'intval', 'default' => 14, 'css_var' => '--funnel-label-font-size', 'unit' => 'px' ], 434 420 [ 'id' => 'mobile_summary_label_font_size','sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ], 435 // #8c857b plus der Deckkraft 0.8 ergaben 2.68:1 auf Weiss.436 421 [ 'id' => 'summary_label_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#6b645a','css_var' => '--funnel-label-color' ], 437 422 [ 'id' => 'summary_label_text', 'sanitize' => 'sanitize_text_field', 'default' => '' ], … … 451 436 [ 'id' => 'summary_hint_size', 'sanitize' => 'intval', 'default' => 14, 'css_var' => '--funnel-summary-hint-size', 'unit' => 'px' ], 452 437 [ 'id' => 'mobile_summary_hint_size','sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ], 453 // ── Ergebnisseite: Weiter-Button (alle optional, leer = global erben) ── 454 // Die globalen Button-Einstellungen gelten weiter, solange hier nichts 455 // gesetzt ist. Der CSS-Generator gibt nur die Eigenschaften aus, die 456 // wirklich gefüllt sind — bestehende Funnels sehen unverändert aus. 438 // ── Summary Button (optional, empty = global button style) ── 457 439 [ 'id' => 'summary_btn_bg', 'sanitize' => array( __CLASS__, 'sanitize_optional_hex_color' ), 'default' => '' ], 458 440 [ 'id' => 'summary_btn_text_color', 'sanitize' => array( __CLASS__, 'sanitize_optional_hex_color' ), 'default' => '' ], -
funnel-elite-lite/tags/1.0.2/includes/emails/trait-email-formatter.php
r3706934 r3707052 5 5 6 6 /** 7 * Te xt einer Mail-Vorlage in der Sprache der Website.7 * Template text in the site language, English fallback. 8 8 * 9 * Die Vorlage hatte ihre Ueberschrift fest auf Englisch im Code. Den 10 * passenden Schluessel gibt es laengst und er ist in allen mitgelieferten 11 * Sprachen uebersetzt - er wurde hier nur nie benutzt. 12 * 13 * Faellt auf den englischen Text zurueck, falls die i18n-Klasse (noch) nicht 14 * geladen ist: eine Mail ohne Ueberschrift waere schlimmer als eine 15 * englische. 16 * 17 * @param string $key Schluessel aus der oeffentlichen i18n-Tabelle. 18 * @param string $fallback Englischer Text, falls nichts gefunden wird. 9 * @param string $key Key of the public i18n table. 10 * @param string $fallback Text used when the key is not available. 19 11 * @return string 20 12 */ -
funnel-elite-lite/tags/1.0.2/includes/emails/trait-email-preview.php
r3706934 r3707052 13 13 */ 14 14 public static function get_default_admin_template( $settings = array() ) { 15 // No <html>/<body>: wp_kses_post() removes both when the template is 16 // saved - together with the font and background set on them. They sit 17 // on the outer table instead, so a saved template looks like the editor. 18 // No fixed 600px container: phones shrink a fixed width, text included, 19 // to about two thirds. A div with max-width grows with the screen 20 // (Chrome and iOS Mail ignore max-width on a table). 15 // No <html>/<body>: wp_kses_post() strips them on save. max-width sits on 16 // a div because some mail clients ignore it on tables. 21 17 $t = '<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7f9; padding: 32px 0; font-family: Segoe UI, Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #2d3748;">' . "\n"; 22 18 $t .= '<tr><td align="center" style="padding: 0 12px;">' . "\n"; -
funnel-elite-lite/tags/1.0.2/includes/emails/trait-email-template.php
r3706934 r3707052 75 75 $message .= '<table width="100%" cellpadding="0" cellspacing="0" style="background-color: ' . $bg_color . '; padding: 40px 0;"><tr><td align="center" style="padding: 0 12px;">'; 76 76 77 // Main Container. Grows with the screen instead of a fixed 600px: 78 // phones shrink a fixed width, text included, to about two thirds, and 79 // Chrome and iOS Mail ignore max-width on a table - hence the div. 77 // Main Container (max-width on a div, some mail clients ignore it on tables) 80 78 $message .= '<div style="max-width: 600px; margin: 0 auto;"><table width="100%" cellpadding="0" cellspacing="0" style="width: 100%; background-color: ' . $container_bg . '; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); overflow: hidden;">'; 81 79 … … 153 151 154 152 // Header 155 //156 // This is the mail the RESULT PAGE sends whenever the template editor is157 // empty - and it never had a logo, not even a place for one, although158 // the admin template does. Same markup as build_default_template(), so159 // both look alike at the top.160 153 $message .= '<tr><td style="padding: 30px 40px; text-align: center; border-bottom: 3px solid ' . $primary_color . ';">'; 161 154 if ( $logo_url ) { -
funnel-elite-lite/tags/1.0.2/public/class-funnel-public.php
r3706934 r3707052 111 111 } 112 112 113 114 /** 115 * Soll auf dieser Seite ueberhaupt etwas vom Funnel geladen werden? 116 * 117 * Ohne is_singular() greift die Pruefung auch auf Such- und Archivseiten: 118 * dort zeigt das globale $post auf den ersten Treffer. Steht darin ein 119 * Funnel, luden alle Stylesheets und das komplette JS mit, obwohl die 120 * Auszuege den Shortcode entfernen und gar kein #funnelbody entsteht. 121 * 122 * Wer den Funnel bewusst ausserhalb einer Einzelansicht rendert (Page 123 * Builder, Widget, Archiv mit vollem Inhalt), schaltet die Dateien wie 124 * bisher ueber den Filter frei: 125 * 126 * add_filter( 'funnel_system_load_assets', '__return_true' ); 113 /** 114 * Load assets only on singular views that contain the shortcode. 127 115 * 128 116 * @return bool … … 250 238 wp_enqueue_script( $this->plugin_name . '-main', $main['url'], array( 'jquery', $this->plugin_name . '-engine' ), $main['version'], true ); 251 239 252 // Drei der angebotenen Knopf-Effekte brauchen JS: "Scroll Trigger"253 // setzt .is-inview, "Click Feedback" setzt .is-pressed, "Ripple Click"254 // legt das .play-ripple-Element an. In Lite fehlte diese Datei, die255 // Effekte standen also im Admin zur Wahl und taten nichts - "Scroll256 // Trigger" liess den Knopf sogar dauerhaft blass.257 240 $effect_settings = $project_id ? $this->get_project_settings( $project_id ) : null; 258 241 if ( $effect_settings ) { … … 287 270 wp_add_inline_script( 288 271 $this->plugin_name . '-model', 289 // Nur wenn der Funnel wirklich gerendert wurde: auf Such- 290 // und Archivseiten strippen Auszuege den Shortcode, die 291 // Klasse haette dort das Scrollen der ganzen Seite gesperrt. 272 // Excerpts strip the shortcode, so check that the funnel was rendered. 292 273 'if(document.getElementById("funnelbody")){document.documentElement.classList.add("funnel-active-page");document.body.classList.add("funnel-active-page");}', 293 274 'before' … … 405 386 if ($bg_type === 'image') $layout_class = 'has-bg-image'; 406 387 if ($bg_type === 'pattern') $layout_class = 'has-bg-pattern'; 407 // "Vollbreite erzwingen": Marker fuer das JS, das die beiden Abstaende408 // zum Fensterrand misst (--funnel-bleed-l / -r). Ohne JS bleibt der409 // Funnel einfach in seiner Spalte.410 388 if ( ! empty( $settings['enable_fullscreen_override'] ) ) { 411 389 $layout_class = trim( $layout_class . ' funnel-bleed' ); … … 644 622 645 623 /** 646 * Returns a fresh public nonce for the funnel's own AJAX request. 647 * 648 * The nonce (funnel_public_nonce) is printed into the page. A page cache can 649 * serve a copy that is older than the nonce lifetime (12-24 hours); then 650 * handle_partial_lead() rejects the request with 403 and the selection 651 * e-mail is silently lost. After such a rejection the frontend calls this 652 * endpoint once and repeats the request with the new nonce. 653 * 654 * Intentionally no nonce check here: a visitor holding an expired nonce must 655 * be able to obtain a new one. The endpoint reads no input, writes nothing 656 * and returns exactly what every page load already prints; it sends 657 * no-cache headers. admin-ajax only lets the site's own origins read the 658 * response (get_allowed_http_origins()). The nonce it returns only unlocks 659 * funnel_partial_lead, which verifies it and is open to visitors anyway. 624 * Returns a fresh public nonce, so a cached page with an expired one can retry. 625 * 626 * No nonce check by design: it reads no input, writes nothing and returns 627 * what every page load prints anyway. 660 628 * 661 629 * @return void … … 742 710 } 743 711 744 // The logo has to travel in the meta: build_partial_lead_email() only745 // sees what it is handed, and without this the result mail never had746 // one - neither the logo nor anywhere to put it.747 712 $email_meta = array( 748 713 'subject' => $subject, … … 760 725 } 761 726 762 // Sender name from the Email tab - resolved before SMTP is configured,763 // because configure_phpmailer() sets it last and would otherwise win.764 727 $from_name = self::resolve_from_name( $project_settings ); 765 728 … … 877 840 878 841 /** 879 * The sender name as the admin typed it into "Sender Name". 880 * 881 * The result mail used to send as get_bloginfo('name') and never read this 882 * field. Only the "Send test email" button used it - so the test mail 883 * showed the configured name and the real mails did not, which is exactly 884 * why it went unnoticed. 885 * 886 * Order follows the rest of the mail code (see the logo in 887 * prepare_placeholder_data): project value, then the global option the Email 888 * tab writes, then the site name. 842 * Sender name: project setting, then the global option, then the site name. 889 843 * 890 844 * @param array $project_settings Decoded project settings. … … 900 854 } 901 855 902 // The site name is stored HTML-escaped ("A & B"); a mail header 903 // wants the plain text. Line breaks would allow header injection, and 904 // angle brackets break wp_mail's "Name <address>" parsing. 856 // Strip line breaks (header injection) and angle brackets ("Name <address>"). 905 857 $name = wp_specialchars_decode( $name, ENT_QUOTES ); 906 858 … … 909 861 910 862 /** 911 * The brand logo from the Email tab, same order as resolve_from_name().863 * Brand logo: project setting, then the global option. 912 864 * 913 865 * @param array $project_settings Decoded project settings. … … 925 877 * Apply the plugin's custom SMTP configuration to PHPMailer (if set). 926 878 * 927 * @param string $from_name Display name for the From header. With SMTP 928 * authentication the address has to be the SMTP 929 * user, but the NAME is free - and this is the 930 * last place it is set before sending, so a 931 * correct header alone would be overwritten here. 879 * @param string $from_name Display name for the From header. 932 880 */ 933 881 private function configure_phpmailer( $from_name = '' ) { … … 971 919 $phpmailer->Username = $user; 972 920 $phpmailer->Password = $pass; 973 // The ADDRESS has to be the SMTP user (servers reject a From974 // they do not own). The NAME stays the configured one - forcing975 // it to the site name here silently undid the From header.976 921 $phpmailer->From = $user; 977 922 $phpmailer->FromName = $from_name; -
funnel-elite-lite/tags/1.0.2/public/css/funnel-button-attention.css
r3706934 r3707052 69 69 content: ""; 70 70 position: absolute; 71 /* Langform statt `inset`: aeltere mobile Browser (Samsung Internet < 14, 72 Chrome < 87, iOS < 14.5) kennen die Kurzform nicht und verwerfen die 73 Deklaration komplett - der Ring faellt dann auf wenige Pixel zusammen. */ 71 /* Longhand instead of inset for older mobile browsers. */ 74 72 top: -6px; 75 73 right: -6px; … … 187 185 transition: none !important; 188 186 transform: none !important; 189 /* Kein `opacity: 1 !important` mehr: es hat auch den ausgegrauten190 "Weiter"-Knopf wieder voll sichtbar gemacht, obwohl er nicht191 klickbar ist - und liess den Ring am Video-Knopf dauerhaft stehen. */192 187 box-shadow: none !important; 193 188 } -
funnel-elite-lite/tags/1.0.2/public/css/funnel-button-effects.css
r3706934 r3707052 103 103 104 104 /* ---------- Play Button Effects ---------- */ 105 /* `:active` ist hier kein Beiwerk: Ein Telefon kennt kein Hover, also waeren 106 diese vier Effekte auf jedem Mobilgeraet wirkungslos - genau das war die 107 Beobachtung "ich merke da nichts". Mit `:active` reagieren sie auf die 108 Beruehrung, solange der Finger liegt. Am Rechner aendert sich nichts: wer 109 klickt, fuhr vorher schon mit dem Zeiger darueber. */ 105 /* :active makes these effects work on touch screens. */ 110 106 .funnel-container .play-effect--soft-pulse:hover, 111 107 .funnel-container .play-effect--soft-pulse:focus-visible, … … 117 113 content: ""; 118 114 position: absolute; 119 /* L angform statt `inset` - siehe funnel-button-attention.css. */115 /* Longhand instead of inset for older mobile browsers. */ 120 116 top: -4px; 121 117 right: -4px; … … 189 185 } 190 186 191 /* Mobil/Desktop (17.09.2026): Der Ring des Effekts "Glow Ring" lag ausserhalb 192 des Knopfes und wurde von dessen overflow: hidden abgeschnitten - der Effekt 193 war unsichtbar. !important, weil funnel-button-attention.css spaeter geladen 194 wird und dieselbe Spezifitaet hat. */ 187 /* The ring sits outside the button. !important: funnel-button-attention.css loads later. */ 195 188 .funnel-container .play-btn.play-effect--glow-ring { 196 189 overflow: visible !important; -
funnel-elite-lite/tags/1.0.2/public/css/funnel-cards.css
r3706934 r3707052 42 42 box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08); 43 43 border-color: rgba(0, 0, 0, 0.1); 44 /* Eigener Rahmen statt nur der Hover-Farbe: bei Verlauf-, Glanz- oder 45 Mesh-Hintergruenden deckt das Hintergrundbild die Hover-Farbe zu, dann 46 war die Tastatur-Auswahl nur noch am 8px-Anheben zu erkennen. */ 44 /* Background images can cover the hover colour, so add an outline. */ 47 45 outline: 2px solid var(--funnel-headline, #1d2327); 48 46 outline-offset: 3px; … … 111 109 font-family: var(--funnel-font-family, sans-serif); 112 110 font-size: var(--funnel-card-title-size, 1.4rem); 113 /* War fest auf #2c2825. Auf dunklen Karten (Karte #1d2327, Text weiss)114 blieb der Titel fast schwarz - gemessen 1.09:1. */115 111 color: var(--funnel-card-text, #2c2825); 116 112 margin: 0; … … 118 114 word-wrap: break-word; 119 115 overflow-wrap: break-word; 120 /* Keine Silbentrennung: `hyphens: auto` zerlegte Antworten mitten im Wort 121 ("Fotoshooting & Brautabho-lung & Saalbegleitung"). Ein Umbruch ZWISCHEN 122 Woertern ist in Ordnung, einer INNERHALB nicht. 123 `manual` statt weglassen, weil hyphens vererbt wird - ein Theme mit 124 `hyphens: auto` auf body wuerde es sonst wieder einschleppen. 125 `overflow-wrap: break-word` bleibt als Notnagel: es greift nur, wenn ein 126 einzelnes Wort allein nicht auf eine Zeile passt. */ 116 /* manual, not unset: hyphens is inherited and themes may set auto. */ 127 117 hyphens: manual; 128 118 -webkit-hyphens: manual; … … 142 132 word-wrap: break-word; 143 133 overflow-wrap: break-word; 144 /* Gleiche Regel wie beim Titel - siehe dort. */145 134 hyphens: manual; 146 135 -webkit-hyphens: manual; … … 341 330 342 331 /* ---- WEITER / SUMMARY BUTTON: globale Layout- & Stil-Basis ---- */ 343 /* Stil (Farbe, Padding, Font) wird über .btn-primary im CSS-Generator aus den Admin-Einstellungen gesetzt*/332 /* Colour, padding and font come from .btn-primary in the CSS generator */ 344 333 .funnel-container .funnel-card-next { 345 334 display: block; … … 372 361 } 373 362 374 /* Der Haken schiebt das Wort aus der Mitte: die Gruppe aus Haken und Wort wird 375 zentriert, also sitzt das Wort um die halbe Hakenbreite plus den halben 376 Abstand daneben - gemessen 10,9 px bei 320 px Knopfbreite. Auffallen tut aber 377 das Wort, nicht die Gruppe. 378 Deshalb steht auf der Gegenseite ein unsichtbarer Zwilling desselben 379 Zeichens. Er ist exakt so breit wie der Haken - auch wenn eine andere 380 Schrift einspringt, was bei U+2713 in einer Serifenschrift die Regel ist - 381 und traegt denselben `gap`. Damit liegt das Wort auf der Knopfmitte. 382 `visibility: hidden` statt `display: none`: der Platz muss erhalten bleiben. */ 363 /* Invisible twin of the tick keeps the label centred. */ 383 364 #btn-summary-next .summary-next-balance { 384 365 visibility: hidden; … … 435 416 } 436 417 437 /* ── Mobil (17.09.2026) ──────────────────────────────────────────────────── 438 Streifen-Layout: Auf schmalen Telefonen ragten Preis-Abzeichen und Bild aus 439 der Karte heraus, sobald ein Titel ein langes Wort hatte (gemessen bei 320 440 und 375px: Abzeichen endete 37px hinter der Karte, Bild links angeschnitten). 441 Die flexiblen Spalten geben ohne min-width: 0 nicht nach. Unter 480px wird 442 das Bild kleiner, sonst bleibt fuer den Titel nur ein schmaler Streifen. */ 418 /* Stripe layout: flex columns need min-width: 0 to shrink. */ 443 419 #funnelbody .funnel-layout-stripe .card-body, 444 420 #funnelbody .funnel-layout-stripe .card-text-wrapper { … … 463 439 } 464 440 465 /* Das Abzeichen nimmt sonst die halbe Kartenbreite, und der Titel bricht466 mitten im Wort. Groesser als 13px wird es hier nicht, kleiner eingestellte467 Werte bleiben erhalten. */468 441 #funnelbody .funnel-layout-stripe .card-price-badge, 469 442 #funnelbody .funnel-layout-stripe .card-adjust-badge, … … 474 447 } 475 448 476 /* Wer im Betriebssystem "Bewegung reduzieren" eingestellt hat, bekam trotzdem477 das volle Programm: 8px-Anheben bei Hover und Tastatur-Auswahl, dazu die478 0.4s-Ueberblendung auf jeder Karte. */479 449 @media (prefers-reduced-motion: reduce) { 480 450 .st-funnel-card .card { -
funnel-elite-lite/tags/1.0.2/public/css/funnel-core.css
r3706934 r3707052 3 3 4 4 /* GLOBAL PAGE OVERRIDE - FULLSCREEN MODE 5 Die Hoehe kommt aus top/bottom, NICHT aus 100vh: iOS Safari rechnet vh mit 6 eingeklappten Leisten. Der Kasten ragte dadurch 80-135px unter die untere 7 Safari-Leiste, und weil html/body gesperrt sind, klappt Safari sie nie ein - 8 das Ende der Liste war nie erreichbar (iPhone 13, 17.09.2026). Mit 9 bottom: 0 sitzt die Unterkante immer auf der sichtbaren Kante. 10 overscroll-behavior-y haelt die Wischgeste im Funnel, statt sie an die 11 gesperrte Seite zu geben (nur y: x wuerde das Zurueckwischen in Chrome 12 abschalten). */ 5 Height from top/bottom, not 100vh: iOS Safari measures vh without its bars. */ 13 6 body.funnel-active-page #funnelbody { 14 7 position: fixed !important; … … 98 91 } 99 92 100 /* Gescopt: .active-step ist ein gaengiger Klassenname in Steppern und101 Kassen-Strecken; mit !important hob die Regel fremde Elemente an. */102 93 #funnelbody .active-step { 103 94 position: relative !important; … … 142 133 gap: 8px; 143 134 background: transparent !important; 144 /* Liest jetzt die Einstellung "Zurueck-Knopf-Farbe"; 0.85 statt 0.7 hebt145 den Kontrast auf weissem Grund von 3.57:1 auf 5.11:1 (AA). */146 135 color: var(--funnel-back-btn-color, #4a5568) !important; 147 136 border: none !important; … … 159 148 160 149 .funnel-back-btn:hover { 161 /* Die feste Hover-Farbe ist raus: sie hat jede eingestellte Farbe beim162 Ueberfahren wieder verworfen. Die volle Deckkraft reicht als Rueckmeldung. */163 150 opacity: 1; 164 151 transform: translateX(-3px); … … 175 162 body.funnel-active-page #funnelbody { 176 163 font-size: var(--funnel-mobile-font-size, 14px); 177 /* Im Vollbild steht ueber der Ueberschrift nichts mehr - die 60px vom178 Desktop sind auf einem Telefon verschenkte Hoehe, also 15px weniger.179 !important, weil die Grundregel padding als Kurzform mit !important180 setzt und sonst gewinnt. */181 164 padding-top: 45px !important; 182 165 padding-bottom: 60px !important; 183 /* Zweite Angabe fuer Geraete mit Hausbalken/Notch (nur mit184 viewport-fit=cover im Theme wirksam, sonst 0). */185 166 padding-bottom: calc(60px + env(safe-area-inset-bottom, 0px)) !important; 186 167 } … … 276 257 } 277 258 278 /* ── Mobil (17.09.2026) ────────────────────────────────────────────────────279 Ueberschriften: lange deutsche Woerter liefen aus dem Bild und wurden280 abgeschnitten (gemessen bei 320px). Umgebrochen wird nur, was sonst nicht281 passt; hyphens: manual haelt die bewusste Entscheidung gegen automatische282 Silbentrennung und ueberstimmt ein Theme mit hyphens: auto. */283 259 #funnelbody h1, 284 260 #funnelbody h2 { … … 288 264 } 289 265 290 /* Zurueck-Knopf: nur 15-18px hoch, im Querformat 12px - zu klein zum 291 Antippen. 44px ist die gaengige Mindestgroesse fuer Finger. Die zweite 292 font-size gilt nur, wo max() bekannt ist; aeltere Engines behalten die 293 erste Zeile, statt die Groesse an das Theme zu verlieren. */ 266 /* 44px touch target. The first font-size is the fallback without max(). */ 294 267 .funnel-back-btn { 295 268 min-height: 44px; … … 300 273 } 301 274 302 /* ── Drucken (17.09.2026) ─────────────────────────────────────────────────303 Im Vollbild blieb der Funnel beim Drucken ein fester, scrollender Kasten:304 das Blatt zeigte nur den ersten Bildschirm, der Rest des Schritts oder der305 Ergebnisseite fehlte. Beim Druck wird der Funnel wieder normaler Inhalt. */306 275 @media print { 307 276 html.funnel-active-page, … … 326 295 } 327 296 328 /* Eigener Stapelkontext im Section-Modus: die Schritte (z-index: 100) bleiben 329 im Funnel, statt sich mit festen Kopfzeilen des Themes zu messen. */ 297 /* Keeps the steps' z-index inside the funnel. */ 330 298 body:not(.funnel-active-page) #funnelbody { 331 299 isolation: isolate; 332 300 } 333 301 334 /* Ladeanzeige der Ergebnisseite bei "Bewegung reduzieren" anhalten. Die 335 Effekte stehen inline am Element, deshalb braucht es hier !important. 336 Der Pulsring wuchs sonst dauerhaft auf das 2,1-fache. */ 302 /* Loader effects are inline styles, hence !important. */ 337 303 @media (prefers-reduced-motion: reduce) { 338 304 .funnel-summary-loader, … … 346 312 } 347 313 348 /* Sprachen von rechts nach links: der Pfeil des Zurueck-Knopfes muss in die349 andere Richtung zeigen und sich auch dorthin bewegen. */350 314 [dir="rtl"] .funnel-back-btn::before { 351 content: " 92";315 content: "\2192"; 352 316 } 353 317 -
funnel-elite-lite/tags/1.0.2/public/css/funnel-core.min.css
r3706934 r3707052 2 2 --funnel-font-family, 3 3 sans-serif 4 )!important;font-size:2.8em;font-weight:400;margin-bottom:1.5rem;color:var(--funnel-headline,#2c2825);line-height:1.2}#funnelbody .funnel-description,.funnel-description{font-family:var(--funnel-font-family, sans-serif)!important;font-size:var(--funnel-group-desc-size, 1.1rem);color:var(--funnel-text,#6d665e);font-weight:300;max-width:600px;margin:0 auto 30px}.funnel-back-btn{display:inline-flex!important;align-items:center;gap:8px;background:0 0!important;color:var(--funnel-back-btn-color,#4a5568)!important;border:none!important;font-family:var(--funnel-font-family, sans-serif);font-weight:400;text-transform:uppercase;letter-spacing:1.5px;cursor:pointer;transition:opacity .3s,color .3s,transform .3s;opacity:.85}.funnel-back-btn:hover{opacity:1;transform:translateX(-3px)}.funnel-back-btn::before{content:"\2190";font-size:1.1rem;line-height:1}#funnel-progress-wrapper{width:100%;max-width:var(--funnel-progress-width,600px);margin:0 auto;position:relative;height:var(--funnel-progress-size,6px);background:rgba(0,0,0,.05);border-radius:calc(var(--funnel-progress-size,6px)/ 2);overflow:hidden;z-index:10}#funnel-progress-fill{height:100%;background-color:var(--funnel-progress-color,#0ba360);transition:width .5s cubic-bezier(.22, 1, .36, 1);border-radius:calc(var(--funnel-progress-size,6px)/ 2)}.funnel-pb-shape-segmented{background:repeating-linear-gradient(90deg,rgba(0,0,0,.05),rgba(0,0,0,.05) 10px,transparent 10px,transparent 15px)!important;border-radius:0!important}.funnel-pb-shape-segmented #funnel-progress-fill{background-image:repeating-linear-gradient(90deg,transparent,transparent 10px,rgba(255,255,255,.3) 10px,rgba(255,255,255,.3) 15px)!important;border-radius:0!important}.funnel-pb-shape-dots{background:rgba(0,0,0,.1)!important;border-radius:0!important;mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));-webkit-mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%;-webkit-mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%}.funnel-pb-shape-dots #funnel-progress-fill{border-radius:0!important}@media (max-width:768px){body.funnel-active-page #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-top:45px!important;padding-bottom:60px!important;padding-bottom:calc(60px + env(safe-area-inset-bottom,0px))!important}body:not(.funnel-active-page) #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-bottom:60px!important}.st-funnel-step h2{font-size:2.2em}.funnel-description{font-size:var(--funnel-mobile-group-desc-size, 14px)}#funnel-progress-wrapper{margin:0 auto 20px;width:calc(100% - 30px)}}#funnelbody h1,#funnelbody h2{overflow-wrap:break-word;hyphens:manual;-webkit-hyphens:manual}.funnel-back-btn{min-height:44px;padding:0 12px 0 0!important;margin-bottom:8px;font-size:.85rem!important;font-size:max(13px, .85rem)!important}@media print{body.funnel-active-page,html.funnel-active-page{overflow:visible!important;height:auto!important}body.funnel-active-page #funnelbody,body:not(.funnel-active-page) #funnelbody{position:static!important;height:auto!important;max-height:none!important;overflow:visible!important}.funnel-loader,.funnel-notice-overlay,.funnel-video-modal{display:none!important}}@media (prefers-reduced-motion:reduce){.funnel-loader .spinner,.funnel-summary-loader,.funnel-summary-loader *{animation:none!important}}[dir=rtl] .funnel-back-btn::before{content:" 92"}[dir=rtl] .funnel-back-btn:hover{transform:translateX(3px)}4 )!important;font-size:2.8em;font-weight:400;margin-bottom:1.5rem;color:var(--funnel-headline,#2c2825);line-height:1.2}#funnelbody .funnel-description,.funnel-description{font-family:var(--funnel-font-family, sans-serif)!important;font-size:var(--funnel-group-desc-size, 1.1rem);color:var(--funnel-text,#6d665e);font-weight:300;max-width:600px;margin:0 auto 30px}.funnel-back-btn{display:inline-flex!important;align-items:center;gap:8px;background:0 0!important;color:var(--funnel-back-btn-color,#4a5568)!important;border:none!important;font-family:var(--funnel-font-family, sans-serif);font-weight:400;text-transform:uppercase;letter-spacing:1.5px;cursor:pointer;transition:opacity .3s,color .3s,transform .3s;opacity:.85}.funnel-back-btn:hover{opacity:1;transform:translateX(-3px)}.funnel-back-btn::before{content:"\2190";font-size:1.1rem;line-height:1}#funnel-progress-wrapper{width:100%;max-width:var(--funnel-progress-width,600px);margin:0 auto;position:relative;height:var(--funnel-progress-size,6px);background:rgba(0,0,0,.05);border-radius:calc(var(--funnel-progress-size,6px)/ 2);overflow:hidden;z-index:10}#funnel-progress-fill{height:100%;background-color:var(--funnel-progress-color,#0ba360);transition:width .5s cubic-bezier(.22, 1, .36, 1);border-radius:calc(var(--funnel-progress-size,6px)/ 2)}.funnel-pb-shape-segmented{background:repeating-linear-gradient(90deg,rgba(0,0,0,.05),rgba(0,0,0,.05) 10px,transparent 10px,transparent 15px)!important;border-radius:0!important}.funnel-pb-shape-segmented #funnel-progress-fill{background-image:repeating-linear-gradient(90deg,transparent,transparent 10px,rgba(255,255,255,.3) 10px,rgba(255,255,255,.3) 15px)!important;border-radius:0!important}.funnel-pb-shape-dots{background:rgba(0,0,0,.1)!important;border-radius:0!important;mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));-webkit-mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%;-webkit-mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%}.funnel-pb-shape-dots #funnel-progress-fill{border-radius:0!important}@media (max-width:768px){body.funnel-active-page #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-top:45px!important;padding-bottom:60px!important;padding-bottom:calc(60px + env(safe-area-inset-bottom,0px))!important}body:not(.funnel-active-page) #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-bottom:60px!important}.st-funnel-step h2{font-size:2.2em}.funnel-description{font-size:var(--funnel-mobile-group-desc-size, 14px)}#funnel-progress-wrapper{margin:0 auto 20px;width:calc(100% - 30px)}}#funnelbody h1,#funnelbody h2{overflow-wrap:break-word;hyphens:manual;-webkit-hyphens:manual}.funnel-back-btn{min-height:44px;padding:0 12px 0 0!important;margin-bottom:8px;font-size:.85rem!important;font-size:max(13px, .85rem)!important}@media print{body.funnel-active-page,html.funnel-active-page{overflow:visible!important;height:auto!important}body.funnel-active-page #funnelbody,body:not(.funnel-active-page) #funnelbody{position:static!important;height:auto!important;max-height:none!important;overflow:visible!important}.funnel-loader,.funnel-notice-overlay,.funnel-video-modal{display:none!important}}@media (prefers-reduced-motion:reduce){.funnel-loader .spinner,.funnel-summary-loader,.funnel-summary-loader *{animation:none!important}}[dir=rtl] .funnel-back-btn::before{content:"\2192"}[dir=rtl] .funnel-back-btn:hover{transform:translateX(3px)} -
funnel-elite-lite/tags/1.0.2/public/css/funnel-modal.css
r3706934 r3707052 8 8 width: 100%; 9 9 height: 100%; 10 /* Liest die Einstellung "Modal Overlay Color"; der Rueckfallwert ist11 genau der bisherige Ton, bestehende Seiten sehen unveraendert aus. */12 10 background-color: var(--funnel-modal-overlay, rgba(44, 40, 37, 0.96)); 13 11 backdrop-filter: blur(10px); … … 31 29 color: #fff !important; 32 30 font-family: var(--funnel-font-family, sans-serif); 33 /* 0.3rem waren 4.8px - ein unlesbarer Fleck neben dem Kreuz. */34 31 font-size: 0.75rem; 35 32 text-transform: uppercase; … … 52 49 } 53 50 54 /* An das Popup gebunden: Themes nutzen .video-container fuer ihre eigenen55 responsiven Einbettungen, die Regel hat sie auf 16:9 gezwungen. */56 51 .funnel-video-modal .video-container { 57 52 position: relative; … … 87 82 } 88 83 89 /* ── Mobil (17.09.2026) ──────────────────────────────────────────────────── 90 Video-Popup im Querformat: Unterkante des Videos (Steuerleiste) lag 91 ausserhalb des Bildschirms, der Schliessen-Knopf teilweise daneben. Die 92 Breite richtet sich jetzt nach der verfuegbaren Hoehe (16:9), oben bleiben 93 mindestens 60px fuer den Knopf. Am Desktop aendert sich nichts: dort 94 begrenzt weiter max-width: 1000px. */ 84 /* 16:9 width from the available height, at least 60px above for the close button. */ 95 85 .funnel-video-modal .modal-content { 96 86 width: min(90%, calc((100vh - 140px) * 16 / 9)); … … 104 94 } 105 95 106 /* Feste Groesse statt rem: im Querformat schrumpfte die Themeschrift, und107 "Schliessen" stand mit ~4px da. */108 96 .funnel-video-modal .close { 109 97 font-size: 12px; … … 112 100 } 113 101 114 /* Bei starker Zoomstufe (200 %) ragte der Inhalt des Video-Popups unten aus115 dem Bild und war nicht erreichbar - das Overlay konnte nicht scrollen. */116 102 .funnel-video-modal { 117 103 overflow-y: auto; 118 104 } 119 105 120 /* Der Schliesser ist ein <span> mit role="button"; ohne diese Regel sieht121 man beim Tabben nicht, dass er ausgewaehlt ist. */122 106 .funnel-video-modal .close:focus-visible { 123 107 outline: 2px solid #fff; -
funnel-elite-lite/tags/1.0.2/public/css/funnel-utilities.css
r3706934 r3707052 1 /* Alle Klassennamen hier sind an .funnel-container gebunden (die Klasse 2 sitzt auf #funnelbody). Ungescopt faerbten .row, .btn-primary und Co. 3 auf Theme-Markup derselben Seite ab. 17.09.2026 */ 1 /* Scoped to .funnel-container (on #funnelbody) so theme markup is untouched. */ 4 2 /* ---- UTILITY CLASSES (Lightweight Bootstrap Replacement) ---- */ 5 3 /* Only the exact utility classes used by the funnel frontend */ … … 28 26 29 27 :where(.funnel-container) .text-left { 30 /* start statt left: in Arabisch/Hebraeisch soll der Text rechts beginnen.31 In Sprachen von links nach rechts ist das identisch. */32 28 text-align: start; 33 29 } -
funnel-elite-lite/tags/1.0.2/public/js/funnel-motion.js
r3706934 r3707052 28 28 29 29 /** 30 * Hat der Besucher im Betriebssystem "Bewegung reduzieren" eingeschaltet? 31 * 32 * Wird bei jedem Aufruf frisch gelesen, damit eine Umstellung waehrend des 33 * Besuchs sofort greift. 30 * Is "reduce motion" enabled? Read on every call. 34 31 */ 35 32 function prefersReducedMotion() { -
funnel-elite-lite/tags/1.0.2/public/js/modules/funnel-engine.js
r3706934 r3707052 12 12 let isUIBackClick = false; 13 13 14 // Oeffentliche Anfragen mit Sicherheitsmarke (Nonce). Die Marke steht in der 15 // Seite; liefert ein Seiten-Cache eine Kopie aus, die aelter als 12-24 16 // Stunden ist, antwortet WordPress mit 403 "-1" - die Auswahl-Mail blieb 17 // dann still aus. Jetzt: einmal eine frische Marke holen 18 // (funnel_fresh_nonce) und dieselbe Anfrage wiederholen. 14 // A cached page can carry an expired nonce: fetch a fresh one and retry once. 19 15 let nonceRefresh = null; 20 16 const freshNonce = () => { 21 // Gleichzeitige Anfragen teilen sich EINE Erneuerung.17 // Concurrent requests share one refresh. 22 18 if (nonceRefresh) return nonceRefresh; 23 19 const d = $.Deferred(); 24 20 const p = d.promise(); 25 21 nonceRefresh = p; 26 const fertig= () => {22 const done = () => { 27 23 if (nonceRefresh === p) nonceRefresh = null; 28 24 }; … … 31 27 { action: "funnel_fresh_nonce" }, 32 28 (res) => { 33 fertig();29 done(); 34 30 if (res && res.success && res.data && res.data.nonce) { 35 31 funnelDataRaw.nonce = res.data.nonce; … … 42 38 if (req && typeof req.fail === "function") { 43 39 req.fail(() => { 44 fertig();40 done(); 45 41 d.reject(); 46 42 }); … … 56 52 ); 57 53 }; 58 // Wie $.post(url, daten, erfolg) - liefert ein Promise mit .fail/.always.54 // Like $.post(url, data, success); returns a promise. 59 55 const publicPost = (data, success) => { 60 // Die Daten beim Aufruf einfrieren. Die Aufrufer uebergeben historyStack 61 // selbst; eine Wiederholung nach der Erneuerung haette sonst mitgenommen, 62 // was spaetere Klicks darin geaendert haben (neue Auswahl, alter Preis). 56 // Snapshot, so a retry does not pick up later changes. 63 57 const payload = $.extend(true, {}, data); 64 58 const result = $.Deferred(); 65 59 const send = (retried) => { 66 60 const sentNonce = funnelDataRaw.nonce; 67 // Abgelehnt: einmal mit frischer Marke wiederholen. Hat eine andere 68 // Anfrage inzwischen schon eine geholt, gleich mit der. 61 // Reuse a nonce another request already refreshed. 69 62 const retry = (fail) => { 70 63 if (retried) return false; … … 200 193 } 201 194 202 // Kein Inline-z-index mehr: die 50 schlugen die 100000 aus dem203 // Stylesheet, dadurch lagen feste Theme-Kopfzeilen (Kadence, Astra,204 // Divi, Chat-Widgets) ueber dem Vollbild-Funnel, und im Section-Modus205 // lag der Funnel umgekehrt ueber Menues des Themes. 17.09.2026206 207 195 // Consolidated Video Modal [v5 - Responsive] 208 196 if ($("#funnel-video-modal").length === 0) { … … 273 261 274 262 $modal.fadeIn(300, function () { 275 // Fokus ins Popup, sonst steht die Tastatur weiter hinter dem276 // Overlay im Schritt und Escape traefe niemanden.277 263 $modal.find(".close").trigger("focus"); 278 264 }); … … 303 289 ); 304 290 305 // Mit der Tastatur: Enter/Leertaste auf dem Schliesser, Escape ueberall.306 // Ohne das kam man aus dem bildschirmfuellenden Video nicht mehr heraus.307 291 $(document).on("keydown", "#funnel-video-modal .close", function (e) { 308 292 if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") { … … 707 691 const safeText = (text || "").toString().replace(/\n/g, "<br>"); 708 692 709 // Die Klasse ist der Anker fuer "Bewegung reduzieren" (funnel-core.css); 710 // die Effekte selbst haengen an inline gesetzten animation-Werten. 693 // The class is the reduced-motion hook in funnel-core.css. 711 694 const commonWrapperStart = `<div class="funnel-summary-loader" style="display:flex; align-items:center; justify-content:center; min-height:60vh; width:100%; padding:20px; box-sizing:border-box; font-family:var(--funnel-font-family, sans-serif);"> 712 695 <div style="width:100%; max-width:540px; text-align:center; padding:30px 20px; color:var(--funnel-headline, #2c2825);">`; … … 721 704 } 722 705 723 // Abstaende und Positionen bewusst ohne `gap` und ohne `inset`: beide 724 // kennen aeltere mobile Browser (Samsung Internet < 14, Chrome < 87, 725 // iOS < 14.5) nicht, und eine unbekannte Deklaration wird komplett 726 // verworfen. Beim Pulsring fiel der Ring dadurch von 27px auf 4px 727 // zusammen - der Effekt war unsichtbar, waehrend die anderen drei 728 // liefen. Nachgemessen am 15.09.2026. 706 // No gap or inset, older mobile browsers drop them. 729 707 if (effect === "dots") { 730 708 return `${commonWrapperStart} -
funnel-elite-lite/tags/1.0.2/public/js/modules/funnel-renderer.js
r3706934 r3707052 59 59 funnelDataRaw.section_display_mode === "dynamic" 60 60 ) { 61 // Messen statt schaetzen: der Kasten ist border-box, outerHeight()62 // enthaelt also Innenabstaende und die Fortschrittsleiste. Die alte63 // Rechnung "Schritt + 80" lag daneben, und weil der Section-Kasten64 // overflow: hidden hat, schnitt die zu kleine Hoehe Karten ab.65 61 const $body = $(parent); 66 62 $body.css("height", "auto"); … … 70 66 71 67 /** 72 * "Vollbreite erzwingen": misst, wie weit die Inhaltsspalte vom linken und 73 * rechten Fensterrand entfernt ist, und gibt beides als CSS-Variablen 74 * weiter. Der frueher benutzte Trick width:100vw; left:50%; margin-left:-50vw 75 * stimmt nur, wenn die Spalte mittig im Fenster sitzt - in einem Layout mit 76 * Seitenleiste schnitt er den Funnel links ab (gemessen bei 1440px: 240px). 77 * 78 * clientWidth statt innerWidth: sonst zaehlt auf Windows die klassische 79 * Scrollleiste (~17px) mit und der Funnel wird zu breit. 68 * "Force Full Screen Width": distance of the column to both viewport edges. 69 * clientWidth excludes the scrollbar. 80 70 */ 81 71 this.updateBleed = () => { … … 92 82 }; 93 83 94 // Beim Drehen, Zoomen oder Fenster-Aendern neu messen: vorher blieb die95 // feste Hoehe des letzten Schritts stehen (gemessen: 199px fehlten nach96 // dem Drehen auf 1024x768). 17.09.202697 84 let heightTimer = null; 98 85 if (typeof window !== "undefined" && window.addEventListener) { … … 248 235 249 236 // --- BREATHING ROOM DELAY (skip if old step was interrupted) --- 250 // Bei "Bewegung reduzieren" faellt die Pause weg: sonst bleibt der251 // Bildschirm bei der Standardgeschwindigkeit 0.75s lang leer, obwohl252 // gar nichts mehr animiert wird.253 237 let reducedMotion = false; 254 238 try { … … 436 420 }); 437 421 438 // iOS wendet `:active` auf ein <button> nur an, wenn das Element selbst 439 // einen Touch-Listener traegt - ein bekannter Eigensinn von Safari. 440 // Ohne diese leere Zeile bleiben die Beruehrungs-Effekte auf iPhones 441 // aus, waehrend sie auf Android laufen. `passive`, damit das Scrollen 442 // nicht ausgebremst wird. 422 // iOS applies :active only to elements with a touch listener. 443 423 $btn[0].addEventListener("touchstart", function () {}, { passive: true }); 444 424 … … 473 453 $row.append($headline); 474 454 475 // Das Feld im Backend ist ein <textarea>: wer dort Enter drueckt, meint476 // einen Umbruch. HTML frisst ihn, also wird er zu <br>. Ergebnistext,477 // Hinweistext, Formularbeschreibungen und die Limit-Meldung machen das478 // laengst - die Seitenbeschreibung war die Ausnahme.479 455 const $desc = $("<p />", { 480 456 class: "funnel-description text-center col-12", -
funnel-elite-lite/trunk/admin/css/admin-bundle.css
r3706931 r3707052 43 43 select:focus, 44 44 textarea:focus { 45 /* Farbe aus dem Admin-Farbschema des Users, damit der Ring auch in 46 "Modern", "Midnight" usw. passt. outline bleibt vorhanden (transparent), 47 damit im Windows-Kontrastmodus ueberhaupt ein Ring gezeichnet wird. */ 45 /* Transparent outline keeps a visible ring in forced-colors mode. */ 48 46 border-color: var(--wp-admin-theme-color, #2271b1); 49 47 box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1); … … 241 239 } 242 240 243 /* Die Zentrierung rechnet mit der eingestellten Spaltenzahl. Ab 1400px 244 abwaerts schaltet das Raster auf auto-fill um, dort wuerde sie die 245 letzten Karten schmal quetschen oder in eine eigene Zeile schieben. */ 241 /* Below 1401px the grid uses auto-fill, where this centering breaks. */ 246 242 @media (min-width: 1401px) { 247 243 /* Center last card(s) if alone in incomplete row — only in cards view */ … … 1492 1488 } 1493 1489 1494 /* Parent muss position: relative haben*/1490 /* Parent needs position: relative */ 1495 1491 .funnel-group-box { 1496 1492 position: relative; … … 1524 1520 } 1525 1521 1526 /* Puls ieren effizienter: nur scale statt Opacity + optional*/1522 /* Pulse with scale only (cheaper than opacity) */ 1527 1523 @keyframes pulse-badge { 1528 1524 0%, … … 1537 1533 } 1538 1534 1539 /* Flash Animation beim Scroll */1535 /* Flash animation on scroll */ 1540 1536 .scroll-highlight { 1541 1537 animation: scroll-flash-blue 0.8s ease-in-out 5; … … 1674 1670 } 1675 1671 1676 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on Ne uerFunnel) */1672 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on New Funnel) */ 1677 1673 .actions-group .button .dashicons { 1678 1674 font-size: 18px; … … 2030 2026 font-weight: 600; 2031 2027 display: flex; 2032 /* Titel linksbündig neben dem Icon halten. 2033 Vorher stand hier justify-content: space-between, was mit den erwarteten 2034 zwei Flex-Items (Text + Pfeil) auch links ausrichtete. WordPress ersetzt 2035 im Admin aber Emoji durch <img class="emoji"> (wp-emoji-release.min.js) — 2036 damit werden aus dem einen Textknoten ZWEI Items, in Summe drei, und 2037 space-between schob das Icon nach links, den Titel in die Mitte und den 2038 Pfeil nach rechts. flex-start + margin-left:auto auf dem Pfeil ist 2039 unabhängig von der Anzahl der Items. */ 2028 /* Not space-between: WordPress turns emoji into <img>, adding flex items. */ 2040 2029 justify-content: flex-start; 2041 2030 gap: 8px; … … 2044 2033 } 2045 2034 2046 /* Der aufklapp-Pfeil (▼) ist immer das letzte Kind und bleibt rechts. 2047 Das inline style="float:right" im Markup ist auf einem Flex-Item wirkungslos. */ 2035 /* The arrow is always the last child. */ 2048 2036 .section-toggle > span:last-child { 2049 2037 margin-left: auto; … … 2088 2076 border-top: 1px solid #dcdcde; 2089 2077 } 2090 /* Funnel Design Tab: Spalten-Layout für Typography, Dimensions & Images */2078 /* Funnel Design Tab: column layout for Typography, Dimensions & Images */ 2091 2079 .funnel-dt-typo-grid--columns { 2092 2080 display: grid; … … 2108 2096 .funnel-design-sticky-save .submit { 2109 2097 margin: 0; 2110 /* Ohne padding:0 behaelt der Kern seine 10px/1.5em und die Leiste wird2111 121px hoch - auf einem Handy im Querformat fast die halbe Ansicht. */2112 2098 padding: 0; 2113 2099 text-align: center; … … 2115 2101 2116 2102 .funnel-design-sticky-save .button-hero { 2117 /* schlaegt das globale .button-hero{margin-top:20px!important} */2103 /* Overrides core .button-hero { margin-top: 20px !important; } */ 2118 2104 margin-top: 0 !important; 2119 2105 } -
funnel-elite-lite/trunk/admin/css/admin-style.css
r3706931 r3707052 42 42 select:focus, 43 43 textarea:focus { 44 /* Farbe aus dem Admin-Farbschema des Users, damit der Ring auch in 45 "Modern", "Midnight" usw. passt. outline bleibt vorhanden (transparent), 46 damit im Windows-Kontrastmodus ueberhaupt ein Ring gezeichnet wird. */ 44 /* Transparent outline keeps a visible ring in forced-colors mode. */ 47 45 border-color: var(--wp-admin-theme-color, #2271b1); 48 46 box-shadow: 0 0 0 1px var(--wp-admin-theme-color, #2271b1); … … 240 238 } 241 239 242 /* Die Zentrierung rechnet mit der eingestellten Spaltenzahl. Ab 1400px 243 abwaerts schaltet das Raster auf auto-fill um, dort wuerde sie die 244 letzten Karten schmal quetschen oder in eine eigene Zeile schieben. */ 240 /* Below 1401px the grid uses auto-fill, where this centering breaks. */ 245 241 @media (min-width: 1401px) { 246 242 /* Center last card(s) if alone in incomplete row — only in cards view */ -
funnel-elite-lite/trunk/admin/css/admin-tab-dashboard.css
r3589214 r3707052 443 443 } 444 444 445 /* Parent muss position: relative haben*/445 /* Parent needs position: relative */ 446 446 .funnel-group-box { 447 447 position: relative; … … 475 475 } 476 476 477 /* Puls ieren effizienter: nur scale statt Opacity + optional*/477 /* Pulse with scale only (cheaper than opacity) */ 478 478 @keyframes pulse-badge { 479 479 0%, … … 488 488 } 489 489 490 /* Flash Animation beim Scroll */490 /* Flash animation on scroll */ 491 491 .scroll-highlight { 492 492 animation: scroll-flash-blue 0.8s ease-in-out 5; … … 625 625 } 626 626 627 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on Ne uerFunnel) */627 /* Normalize every dashicon inside toolbar buttons (incl. the "+" on New Funnel) */ 628 628 .actions-group .button .dashicons { 629 629 font-size: 18px; -
funnel-elite-lite/trunk/admin/css/admin-tab-design.css
r3706931 r3707052 17 17 font-weight: 600; 18 18 display: flex; 19 /* Titel linksbündig neben dem Icon halten. 20 Vorher stand hier justify-content: space-between, was mit den erwarteten 21 zwei Flex-Items (Text + Pfeil) auch links ausrichtete. WordPress ersetzt 22 im Admin aber Emoji durch <img class="emoji"> (wp-emoji-release.min.js) — 23 damit werden aus dem einen Textknoten ZWEI Items, in Summe drei, und 24 space-between schob das Icon nach links, den Titel in die Mitte und den 25 Pfeil nach rechts. flex-start + margin-left:auto auf dem Pfeil ist 26 unabhängig von der Anzahl der Items. */ 19 /* Not space-between: WordPress turns emoji into <img>, adding flex items. */ 27 20 justify-content: flex-start; 28 21 gap: 8px; … … 31 24 } 32 25 33 /* Der aufklapp-Pfeil (▼) ist immer das letzte Kind und bleibt rechts. 34 Das inline style="float:right" im Markup ist auf einem Flex-Item wirkungslos. */ 26 /* The arrow is always the last child. */ 35 27 .section-toggle > span:last-child { 36 28 margin-left: auto; … … 75 67 border-top: 1px solid #dcdcde; 76 68 } 77 /* Funnel Design Tab: Spalten-Layout für Typography, Dimensions & Images */69 /* Funnel Design Tab: column layout for Typography, Dimensions & Images */ 78 70 .funnel-dt-typo-grid--columns { 79 71 display: grid; … … 95 87 .funnel-design-sticky-save .submit { 96 88 margin: 0; 97 /* Ohne padding:0 behaelt der Kern seine 10px/1.5em und die Leiste wird98 121px hoch - auf einem Handy im Querformat fast die halbe Ansicht. */99 89 padding: 0; 100 90 text-align: center; … … 102 92 103 93 .funnel-design-sticky-save .button-hero { 104 /* schlaegt das globale .button-hero{margin-top:20px!important} */94 /* Overrides core .button-hero { margin-top: 20px !important; } */ 105 95 margin-top: 0 !important; 106 96 } -
funnel-elite-lite/trunk/admin/includes/class-funnel-builder-manager.php
r3706931 r3707052 309 309 ]; 310 310 311 // Unbekannte Meta-Keys des Bestands erhalten. 312 // 313 // Das Array oben wird bei jedem Speichern vollstaendig aus $_POST neu 314 // gebaut. Alles, was diese Version nicht kennt, waere damit still weg, 315 // sobald jemand die Karte einmal oeffnet und speichert — in Lite z. B. die 316 // komplette Konfiguration der Pro-Kartentypen (Slider, Mengen-Items, 317 // Distance Map, Consent-Texte, Pricing-Table-Spalten), die ueber einen 318 // Import aus Pro in der Datenbank liegt. 319 // 320 // Bekannte Keys gewinnen, weil sie oben immer gesetzt werden; unbekannte 321 // bleiben unveraendert stehen. 311 // Keep stored meta keys this version does not know (e.g. imported from Pro). 322 312 if ( ! empty( $_POST['id'] ) ) { 323 313 $existing_raw = $this->db->get_var( $this->db->prepare( "SELECT meta_data FROM {$this->table_cards} WHERE id = %d", intval( wp_unslash( $_POST['id'] ) ) ) ); … … 368 358 369 359 /** 370 * Kartenpreis aus dem Karten-Modal lesen. 360 * Parse a typed price. With both separators the last one is the decimal 361 * mark; several of the same kind are thousands separators. 371 362 * 372 * Das Feld ist ein Textfeld (ohne die Pfeile eines Zahlenfelds), es kommt 373 * also an, was getippt wurde. Frueher genuegte str_replace(',', '.') - das 374 * Zahlenfeld liess "1.200,00" gar nicht zu. Jetzt wuerde daraus 1.2. 375 * 376 * - Komma UND Punkt: das hintere Zeichen trennt die Dezimalen, das andere 377 * die Tausender ("1.200,00", "1,200.00"). 378 * - Nur Kommas: eines ist das Dezimalzeichen ("1200,00", "1,2" fuer ×1,2), 379 * mehrere trennen Tausender. 380 * - Nur Punkte: einer ist das Dezimalzeichen ("1200.50"), mehrere trennen 381 * Tausender ("1.200.000"). "1.200" bleibt 1.2 wie bisher - nicht eindeutig. 382 * - Leerzeichen und Waehrungszeichen fallen weg. 383 * 384 * @param string $raw Eingabe aus dem Preisfeld. 363 * @param string $raw Raw price field value. 385 364 * @return float 386 365 */ -
funnel-elite-lite/trunk/admin/includes/class-funnel-settings-manager.php
r3706931 r3707052 31 31 32 32 /** 33 * Keep an email template only if it was actually edited. 34 * 35 * The editor in the Email tab is pre-filled with the default template, so 36 * ANY save on that tab — a new SMTP password, a changed colour — writes the 37 * whole default into the project settings, even when nobody touched the 38 * HTML. From that moment the email is that frozen HTML: it keeps the 39 * language it happened to have at the time and never follows the site 40 * again. That is how a German site ends up sending a half-English email, 41 * and no plugin update can repair it, because the database wins. 42 * 43 * An empty field means "use the default", and the default is built fresh on 44 * every send, so it speaks the language of the site. 45 * 46 * Both sides are compared the way they are stored: the POST value has gone 47 * through wp_kses_post(), which strips <html>/<body> out of the template, 48 * and a textarea submits CRLF where the source has LF. Without normalising 49 * both, nothing would ever match and the guard would be dead code. 33 * Store the template only if it differs from the default, so an untouched 34 * one keeps following the site language. 50 35 * 51 36 * @param string $posted The already sanitized POST value. … … 62 47 63 48 /** 64 * The default admin template, or an empty string if the mail class is not 65 * loaded — then nothing can match and the value is kept as posted. 49 * Default admin template, or '' if the mail class is not loaded. 66 50 */ 67 51 private static function default_admin_template() { -
funnel-elite-lite/trunk/admin/js/admin-builder.js
r3706931 r3707052 380 380 form.find('input[name="answer"]').val(answer); 381 381 form.find('input[name="image_url"]').val(img); 382 // Ohne Preis leer lassen, damit der Platzhalter ("z. B. 1200,00") zu sehen ist.382 // Empty instead of 0, so the placeholder shows. 383 383 form.find('input[name="price"]').val(parseFloat(price) ? price : ""); 384 384 form.find('select[name="price_operator"]').val(operator); -
funnel-elite-lite/trunk/admin/js/admin-visual-nodes-trees.js
r3706931 r3707052 26 26 let lastRenderedSource = ""; // Performance cache 27 27 28 // Welche Karte gerade steht (Routen-Filter) und ob der Admin den Zoom selbst 29 // gewaehlt hat oder die Karte nur eingepasst ist. Siehe "ANSICHT MERKEN". 28 // zoomChosen: zoom set by the user (false = fitted to the container). 30 29 let currentFilter = "all"; 31 30 let zoomChosen = false; … … 92 91 93 92 // =================================== 94 // ANSICHT MERKEN (Zoom + Scrollposition) 95 // =================================== 96 97 // Nach dem Speichern laedt die Seite neu, und die Karte wurde dabei jedes Mal 98 // wieder eingepasst - wer hineingezoomt hatte, musste die Stelle neu suchen. 99 // Gemerkt wird im Browser, je Funnel und je Routen-Filter: jeder Filter 100 // zeichnet eine andere Karte, deren Stelle nicht auf die naechste passt. 93 // REMEMBER VIEW (per funnel and route filter) 94 // =================================== 101 95 102 96 function mapProjectId() { … … 125 119 localStorage.setItem(mapViewKey(), JSON.stringify(views)); 126 120 } catch (e) { 127 // Privates Fenster oder voller Speicher: dann eben ohne Gedaechtnis.121 // Storage unavailable (private window, quota). 128 122 } 129 123 } … … 131 125 function saveMapView() { 132 126 const scroller = document.getElementById("visual-map-scroll-container"); 133 // Solange die gemerkte Ansicht nicht angewendet ist, wuerde der Zwischenstand 134 // sie ueberschreiben. Und versteckt (anderer Tab, eingeklappte Karte) liest 135 // sich jede Scrollposition als 0. 127 // Hidden containers report 0; never overwrite a view not yet applied. 136 128 if (!viewApplied || !scroller || !$(scroller).is(":visible")) return; 137 129 if (!DOM.$graphContainer.find("svg").length) return; … … 139 131 const views = readMapViews(); 140 132 views[currentFilter] = { 141 // null = nur eingepasst. Waechst die Karte, passt sie sich dann neu ein, 142 // statt auf einem veralteten Einpass-Wert stehen zu bleiben. 133 // null = fitted, so a grown map is fitted again. 143 134 zoom: zoomChosen ? currentZoom : null, 144 135 left: scroller.scrollLeft, … … 154 145 } 155 146 156 // Der Routen-Filter selbst sprang nach dem Neuladen auf "Alle" zurueck - die 157 // fuer einen Filter gemerkte Stelle kam so nie zum Zug. Ebenfalls je Funnel. 158 // "Alle" ist der Normalfall und wird nicht gespeichert. 147 // "all" is the default and is not stored. 159 148 160 149 function mapFilterKey() { … … 170 159 } 171 160 } catch (e) { 172 // Privates Fenster oder voller Speicher: dann eben ohne Gedaechtnis.161 // Storage unavailable (private window, quota). 173 162 } 174 163 } 175 164 176 165 /** 177 * Den gemerkten Filter zurueck in die Auswahl schreiben, bevor zum ersten 178 * Mal gezeichnet wird. Gibt es die Route nicht mehr (Karte umbenannt oder 179 * geloescht), bleibt "Alle" stehen und der alte Wert wird vergessen. 166 * Put the stored filter back into the dropdown; forget it if the route is gone. 180 167 */ 181 168 function restoreMapFilter() { … … 201 188 202 189 /** 203 * Karte einpassen und die gemerkte Ansicht wiederherstellen, falls es eine 204 * gibt. Nur bei sichtbarem Container aufrufen - versteckt nimmt der Browser 205 * keine Scrollposition an. 190 * Fit the map and restore the stored view. Call only on a visible container. 206 191 */ 207 192 function showMapView() { … … 350 335 if (!sourceCode) return; 351 336 352 // Ohne Angabe gilt, was in der Auswahl steht. Vorher zeichnete z. B. der353 // Wechsel zurueck auf den Builder-Tab "Alle", waehrend die Auswahl noch354 // einen Filter zeigte.355 337 if (filterRoute === undefined) { 356 338 filterRoute = $("#pages-cards-route-filter").val() || "all"; … … 363 345 // PERFORMANCE: Skip expensive render if source is unchanged 364 346 if (sourceCode === lastRenderedSource) { 365 // Same map shown again (e.g. back on the Builder tab): skip DOM 366 // generation, but put the view back where it was. 347 // Unchanged source: skip rendering, restore the view. 367 348 showMapView(); 368 349 return; … … 421 402 }); 422 403 423 // Fit map after render. Shown first: the stored scroll position only 424 // sticks on a visible container. 404 // Show first: scroll positions only stick on a visible container. 425 405 setTimeout(() => { 426 406 const $collapsible = $("#visual-pages-cards-collapsible"); … … 475 455 }); 476 456 477 // Zuruecksetzen heisst auch vergessen: nach dem Neuladen wird wieder478 // eingepasst.479 457 $(".zoom-map-reset").on("click", (e) => { 480 458 e.preventDefault(); … … 483 461 }); 484 462 485 // Die Scrollposition laufend mitschreiben. "pagehide" faengt den letzten 486 // Stand ab, wenn direkt nach dem Scrollen gespeichert und neu geladen wird. 463 // pagehide catches a scroll right before a reload. 487 464 $("#visual-map-scroll-container").on("scroll", debounce(saveMapView, 150)); 488 465 window.addEventListener("pagehide", saveMapView); … … 697 674 698 675 /** 699 * Apply the stored collapsed state to the map panel. 700 * 701 * Runs without Mermaid so the panel is in its final state even when the 702 * library is never loaded. 676 * Apply the stored collapsed state (works without Mermaid). 703 677 * 704 678 * @return {boolean} true when the map is collapsed. … … 720 694 721 695 function initMermaidAndRender() { 722 // loadMermaidIfNeeded() resolves even when the local file fails to load 723 // (fail-silently, WP.org Guideline #8 — no CDN fallback). Guard so a missing 724 // library cannot throw here. 696 // loadMermaidIfNeeded() also resolves when the file failed to load. 725 697 if (typeof mermaid === "undefined") { 726 698 applyCollapsedState(); … … 733 705 securityLevel: "loose", 734 706 htmlLabels: true, 735 // NO size limit on the map. 736 // 737 // Mermaid stops at 50 000 characters by default and REPLACES the diagram 738 // with "Maximum text size in diagram exceeded" (it does not throw: 739 // text.length > (config.maxTextSize ?? 5e4) && (text = errorGraph) 740 // ). That guard is meant for UNTRUSTED input; here the definition is 741 // generated from the admin's own funnel, so the default only hides the 742 // map once a funnel grows - after roughly ten steps, because the length 743 // comes from the labels rather than from the structure. 744 // 745 // MAX_SAFE_INTEGER rather than Infinity: the comparison is numeric either 746 // way, but a plain number survives any clone of the config object, while 747 // Infinity can come back as null - and `?? 5e4` would then quietly put 748 // the default back. 707 // No size limit (the source is built by the plugin). Not Infinity: it can 708 // become null when the config is cloned, which restores the default. 749 709 maxTextSize: Number.MAX_SAFE_INTEGER, 750 710 maxEdges: Number.MAX_SAFE_INTEGER, … … 791 751 // Global Initialization Boundary 792 752 try { 793 // The stored route filter goes back into the dropdown before anything is794 // drawn, so the first render already uses it.795 753 restoreMapFilter(); 796 754 797 // Put the panel into its stored state first — this must happen whether or798 // not Mermaid ever loads.799 755 const startsCollapsed = applyCollapsedState(); 800 756 … … 806 762 loadMermaidIfNeeded().then(initMermaidAndRender); 807 763 } else { 808 // Nothing to draw yet. Mermaid is ~650 KB, so it stays unloaded until the 809 // map is actually going to be shown: either the Builder tab is opened 810 // (with the map expanded) or the user expands the collapsed map. 764 // Mermaid (~650 KB) loads only once the map is shown. 811 765 $(document).on("click.funnelMermaidLazy", '.nav-tab[href="#tab-builder"]', function () { 812 766 if (localStorage.getItem("funnel_pages_cards_collapsed") === "true") return; -
funnel-elite-lite/trunk/admin/traits/trait-admin-enqueue.php
r3706931 r3707052 30 30 31 31 /** 32 * Is the current admin screen one of the plugin's own? 33 * 34 * WordPress passes the screen's hook suffix to admin_enqueue_scripts 35 * ('toplevel_page_funnel-elite-lite'); $_GET['page'] is kept as a fallback 36 * for the AJAX-rendered tabs, which reach this code without a suffix. 32 * Is the current admin screen the plugin's own page? 37 33 * 38 34 * @param string $hook_suffix Hook suffix handed over by WordPress. … … 51 47 52 48 public function enqueue_styles( $hook_suffix = '' ) { 53 // Nur auf der eigenen Plugin-Seite laden. Das Bundle gestaltet Labels,54 // Eingabefelder und Buttons global um; auf fremden Seiten hat es55 // Kern-Formulare, Listentabellen und den Block-Editor veraendert.56 // Das Plugin zeichnet ausserhalb seiner Seite nichts (kein57 // admin_notices, kein admin_bar_menu, kein Dashboard-Widget).58 49 if ( ! $this->is_plugin_admin_page( $hook_suffix ) ) { 59 50 return; … … 83 74 . '.slider:before{position:absolute;content:"";height:18px;width:18px;left:3px;bottom:3px;background-color:#fff;-webkit-transition:.4s;transition:.4s}' 84 75 . 'input:checked+.slider{background-color:#d63384}' 85 // Tastatur-Fokus: der echte Haken ist 0x0 und unsichtbar, ohne86 // diesen Ring sieht man beim Tabben nicht, welcher Schalter dran ist.87 76 . 'input:focus-visible+.slider{outline:2px solid var(--wp-admin-theme-color,#2271b1);outline-offset:2px}' 88 77 . 'input:checked+.slider:before{-webkit-transform:translateX(26px);-ms-transform:translateX(26px);transform:translateX(26px)}' … … 163 152 $mermaid_rel = 'js/vendor/mermaid.min.js'; 164 153 $mermaid_abs = FUNNEL_ELITE_PATH . 'admin/' . $mermaid_rel; 165 // Mit Dateiversion: Mermaid wird per JS nachgeladen und bekommt deshalb 166 // keine ?ver von wp_enqueue_script. Ohne sie nimmt der Browser nach einem 167 // Update die alte Datei aus dem Cache - z. B. mit der alten Kartenreihenfolge. 154 // Lazy-loaded by JS, so it needs its own cache-busting version. 168 155 $mermaid_url = file_exists( $mermaid_abs ) 169 156 ? FUNNEL_ELITE_URL . 'admin/' . $mermaid_rel . '?ver=' . rawurlencode( $this->file_version( $mermaid_abs ) ) -
funnel-elite-lite/trunk/admin/views/tabs/design/section-summary.php
r3706931 r3707052 82 82 <div class="funnel-dt-section-title"><?php echo esc_html( Funnel_System_I18n::get_admin('dt_typo_spacing_title') ); ?></div> 83 83 <div class="funnel-dt-details-body"> 84 <!-- 💻 DESKTOP -->85 84 <h4 style="margin:0 0 10px; color:#475569; font-size:12px; text-transform:uppercase; letter-spacing:0.5px;">💻 <?php echo esc_html( Funnel_System_I18n::get_admin('dt_desktop') ); ?></h4> 86 85 <div class="funnel-dt-typo-grid"> … … 129 128 </div> 130 129 <div class="funnel-dt-typo-divider"></div> 131 <!-- 📱 MOBILE -->132 130 <?php 133 // Ein leeres Mobil-Feld erbt den Desktop-Wert. Damit der 134 // Platzhalter nicht etwas anderes verspricht, zeigt er 135 // genau diesen Wert an. 136 $dt_wert = static function ( $key, $fallback ) use ( $project_settings ) { 131 // Placeholder shows the desktop value an empty mobile field inherits. 132 $dt_value = static function ( $key, $fallback ) use ( $project_settings ) { 137 133 return ( isset( $project_settings[ $key ] ) && '' !== $project_settings[ $key ] ) 138 134 ? $project_settings[ $key ] … … 145 141 <label for="mobile_summary_title_size"><?php echo esc_html( Funnel_System_I18n::get_admin('title_font_size_label') ); ?></label> 146 142 <div class="funnel-dt-input-wrap"> 147 <input name="mobile_summary_title_size" type="number" id="mobile_summary_title_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_title_size']) ? $project_settings['mobile_summary_title_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_ wert( 'summary_title_size', 32 ) ); ?>" class="small-text" min="10" max="60">143 <input name="mobile_summary_title_size" type="number" id="mobile_summary_title_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_title_size']) ? $project_settings['mobile_summary_title_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_title_size', 32 ) ); ?>" class="small-text" min="10" max="60"> 148 144 <span class="funnel-dt-unit">px</span> 149 145 </div> … … 152 148 <label for="mobile_summary_text_size"><?php echo esc_html( Funnel_System_I18n::get_admin('text_font_size_label') ); ?></label> 153 149 <div class="funnel-dt-input-wrap"> 154 <input name="mobile_summary_text_size" type="number" id="mobile_summary_text_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_text_size']) ? $project_settings['mobile_summary_text_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_ wert( 'summary_text_size', 16 ) ); ?>" class="small-text" min="10" max="60">150 <input name="mobile_summary_text_size" type="number" id="mobile_summary_text_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_text_size']) ? $project_settings['mobile_summary_text_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_text_size', 16 ) ); ?>" class="small-text" min="10" max="60"> 155 151 <span class="funnel-dt-unit">px</span> 156 152 </div> … … 159 155 <label for="mobile_summary_label_font_size"><?php echo esc_html( Funnel_System_I18n::get_admin('price_label_font_size_label') ); ?></label> 160 156 <div class="funnel-dt-input-wrap"> 161 <input name="mobile_summary_label_font_size" type="number" id="mobile_summary_label_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_label_font_size']) ? $project_settings['mobile_summary_label_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_ wert( 'summary_label_font_size', 14 ) ); ?>" class="small-text" min="10" max="60">157 <input name="mobile_summary_label_font_size" type="number" id="mobile_summary_label_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_label_font_size']) ? $project_settings['mobile_summary_label_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_label_font_size', 14 ) ); ?>" class="small-text" min="10" max="60"> 162 158 <span class="funnel-dt-unit">px</span> 163 159 </div> … … 166 162 <label for="mobile_price_font_size"><?php echo esc_html( Funnel_System_I18n::get_admin('price_font_size_label') ); ?></label> 167 163 <div class="funnel-dt-input-wrap"> 168 <input name="mobile_price_font_size" type="number" id="mobile_price_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_price_font_size']) ? $project_settings['mobile_price_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_ wert( 'price_font_size', 32 ) ); ?>" class="small-text" min="10" max="80">164 <input name="mobile_price_font_size" type="number" id="mobile_price_font_size" value="<?php echo esc_attr(isset($project_settings['mobile_price_font_size']) ? $project_settings['mobile_price_font_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'price_font_size', 32 ) ); ?>" class="small-text" min="10" max="80"> 169 165 <span class="funnel-dt-unit">px</span> 170 166 </div> … … 173 169 <label for="mobile_summary_hint_size"><?php echo esc_html( Funnel_System_I18n::get_admin('hint_font_size_label') ); ?></label> 174 170 <div class="funnel-dt-input-wrap"> 175 <input name="mobile_summary_hint_size" type="number" id="mobile_summary_hint_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_hint_size']) ? $project_settings['mobile_summary_hint_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_ wert( 'summary_hint_size', 14 ) ); ?>" class="small-text" min="10" max="60">171 <input name="mobile_summary_hint_size" type="number" id="mobile_summary_hint_size" value="<?php echo esc_attr(isset($project_settings['mobile_summary_hint_size']) ? $project_settings['mobile_summary_hint_size'] : ''); ?>" placeholder="<?php echo esc_attr( $dt_value( 'summary_hint_size', 14 ) ); ?>" class="small-text" min="10" max="60"> 176 172 <span class="funnel-dt-unit">px</span> 177 173 </div> … … 228 224 </table> 229 225 230 <!-- ── Ergebnisseite: Weiter-Button (optional, leer = global)── -->226 <!-- ── Continue Button ─────────────────────────────── --> 231 227 <div class="funnel-dt-details funnel-dt-details--static" style="margin-top:12px;"> 232 228 <div class="funnel-dt-section-title"><?php echo esc_html( Funnel_System_I18n::get_admin('dt_summary_btn_sizes_title') ); ?></div> -
funnel-elite-lite/trunk/includes/class-funnel-css-generator.php
r3706931 r3707052 248 248 $summary_hint_size = ! empty( $settings['summary_hint_size'] ) ? $settings['summary_hint_size'] : 14; 249 249 250 // Ergebnisseite auf dem Telefon. Ein leeres Feld erbt bewusst den 251 // Desktop-Wert - so aendert sich an bestehenden Funnels nichts, solange 252 // niemand ein Mobil-Feld ausfuellt. Der Platzhalter im Design-Tab zeigt 253 // deshalb genau diesen Desktop-Wert an. 250 // Empty mobile fields inherit the desktop value. 254 251 $mobile_summary_title_size = ! empty( $settings['mobile_summary_title_size'] ) ? $settings['mobile_summary_title_size'] : $summary_title_size; 255 252 $mobile_summary_text_size = ! empty( $settings['mobile_summary_text_size'] ) ? $settings['mobile_summary_text_size'] : $summary_text_size; … … 270 267 $back_btn_color = $settings['back_btn_color']; 271 268 $modal_bg_color = $settings['modal_bg_color']; 272 // Als rgba ausgeben: das Video-Overlay braucht die 0.96 Deckkraft,273 // ein reiner Hex-Wert wuerde es undurchsichtig machen.274 269 $modal_overlay_rgba = self::hexToRgba( $modal_bg_color, 0.96 ); 275 270 $max_width = $settings['max_content_width']; … … 365 360 $card_bg_3 = self::hexToRgba($settings['card_bg_color_3'], $card_bg_intensity); 366 361 367 /* 368 * Summary button overrides (Ergebnisseite). 369 * 370 * Every field is optional and means "inherit the global button style" when 371 * empty, so only the properties the admin actually filled in are emitted. 372 * An untouched funnel therefore produces no rule at all and looks exactly 373 * as it did before these settings existed. 374 * 375 * The selector is #funnelbody #btn-summary-next: two IDs beat the global 376 * "#funnelbody .btn-primary" rule, and !important is repeated because that 377 * rule carries it too (see the card hover note — an important declaration 378 * only loses to another important one). 379 */ 362 // Summary button overrides: only filled fields are emitted, empty ones 363 // inherit the global button style (which is !important, hence here too). 380 364 $summary_btn_decls = array(); 381 365 … … 545 529 } 546 530 547 /* Hier standen die beiden Regeln zur Einstellung Layout Format548 (Views). Kein PHP und kein JS hat die zugehoerigen Klassen je an549 #funnelbody gehaengt, die Auswahl war also wirkungslos;550 ausserdem hat der Inline-Minifier die Leerzeichen um das Plus in551 ihrer calc()-Rechnung entfernt und sie damit ungueltig gemacht.552 Einstellung und Regeln sind entfernt.553 Fuer eine echte Vollbreite gibt es die Einstellung Vollbreite erzwingen. */554 555 531 #funnelbody h1, #funnelbody h2 { 556 532 color: var(--funnel-headline); … … 590 566 } 591 567 592 /* Hover/focus background MUST repeat the !important. 593 funnel-cards.css sets --funnel-card-hover-bg on :hover without it, 594 and an !important declaration always wins over a normal one — 595 regardless of specificity or order. The Hover Color setting 596 therefore had no visible effect at all until this rule existed. 597 Do not drop the !important here without also dropping it above. */ 568 /* Needs !important to beat the card background rule. */ 598 569 .st-funnel-card .card:hover, 599 570 .st-funnel-card:focus-visible .card { … … 706 677 #funnelbody .st-funnel-card img { height: {$mobile_img_height}px !important; } 707 678 708 /* Ergebnisseite. Die Variablen oben decken auch die Inline-Styles709 ab, die der Renderer setzt; diese Regeln sind fuer alles, was710 ueber das Stylesheet kommt. */711 679 #funnelbody .summary-title { font-size: {$mobile_summary_title_size}px !important; } 712 680 #funnelbody .summary-text { font-size: {$mobile_summary_text_size}px !important; } … … 752 720 margin-right: 0 !important; 753 721 } 754 /* Offsetbasiert statt 100vw/-50vw: der alte Trick stimmt nur, wenn 755 die Inhaltsspalte mittig im Fenster steht. In einem Layout mit 756 Seitenleiste schob er den Funnel aus dem Bild (gemessen bei 1440px: 757 240px links abgeschnitten). Die beiden Werte setzt das JS aus der 758 tatsaechlichen Position; ohne JS bleibt alles in der Spalte. */ 722 /* --funnel-bleed-l/-r are measured by JS. */ 759 723 body:not(.funnel-active-page) #funnelbody { 760 724 width: auto !important; -
funnel-elite-lite/trunk/includes/class-funnel-data.php
r3706931 r3707052 5 5 6 6 /** 7 * Card meta keys that must never reach the frontend. 8 * 9 * funnelDataRaw is printed into the page source and the whole meta_data array 10 * is merged into it, so every key in there is public by default. 11 * 12 * Lite has no UI for these — they can only arrive through an import of a funnel 13 * that was built in Pro, where the export carries meta_data 1:1. The data is 14 * kept (so a Pro -> Lite -> Pro round trip stays lossless) but never published. 7 * Card meta keys (imported from Pro) that are kept but never printed to the page. 15 8 */ 16 9 const ADMIN_ONLY_META_KEYS = array( 'admin_note' ); … … 121 114 ], $meta); 122 115 123 // Internal Pro-only fields must not be published — see the constant.124 116 foreach ( self::ADMIN_ONLY_META_KEYS as $admin_only_key ) { 125 117 unset( $relation_entry[ $admin_only_key ] ); -
funnel-elite-lite/trunk/includes/class-funnel-i18n.php
r3706931 r3707052 41 41 42 42 /** 43 * Cache key for the string tables. 44 * 45 * Keyed by locale so switch_to_locale() still returns the right table. 46 * Calls that happen before `init` can still be untranslated (WordPress loads 47 * plugin translations just-in-time), so those get their own slot and never 48 * poison the translated table. 43 * Cache key per locale; calls before `init` get their own untranslated slot. 49 44 */ 50 45 private static function string_cache_key() { … … 64 59 /** 65 60 * Get strings for the Admin Backend (built once per locale and request). 66 *67 * PERFORMANCE: the table below holds ~515 __() lookups. It used to be rebuilt68 * on every single get_admin() call — with ~530 call sites in the admin views69 * that added up to ~275,000 gettext lookups per admin page render (measured:70 * 107ms of pure PHP, 0.2ms with this cache). Never drop this cache.71 61 */ 72 62 public static function get_admin_strings() { … … 232 222 233 223 'card_icon_label' => __( 'Icon (Lucide)', 'funnel-elite-lite' ), 234 // UI Labels aus Design-Vorlage224 // UI labels from the design template 235 225 'icon_size_label' => __( 'Icon Size', 'funnel-elite-lite' ), 236 226 'icon_color_label' => __( 'Icon Color', 'funnel-elite-lite' ), … … 238 228 239 229 'card_price_label' => __( 'Card Price', 'funnel-elite-lite' ), 240 // Platzhalter im Preisfeld - je Sprache mit deren Dezimalzeichen uebersetzen.230 // translators: example price, use the decimal separator of your language. 241 231 'card_price_placeholder' => __( 'e.g. 1200.00', 'funnel-elite-lite' ), 242 232 'card_price_help_text' => __( 'Operator + price set how this card changes the running total: + adds and − subtracts the price; × and ÷ multiply or divide the total (e.g. ×1.2 = +20%).', 'funnel-elite-lite' ), … … 584 574 'sort_order' => __( 'Sort Order', 'funnel-elite-lite' ), 585 575 'video_url_optional' => __( 'Video URL (Optional)', 'funnel-elite-lite' ), 586 // Knopf im Seiten-Modal - vorher stand dort "Save Card". Muss zum Titel587 // edit_step ("Edit Page") passen.588 576 'save_step' => __( 'Save Page', 'funnel-elite-lite' ), 589 577 'edit_card' => __( 'Edit Card', 'funnel-elite-lite' ), … … 675 663 'dt_price_box_content_title' => __( 'Price Box Content', 'funnel-elite-lite' ), 676 664 'dt_summary_colors_title' => __( 'Summary Colors', 'funnel-elite-lite' ), 677 // Ergebnisseite: Weiter-Button (optionale Overrides)678 665 'dt_summary_btn_colors_title' => __( 'Continue Button (Result Page)', 'funnel-elite-lite' ), 679 666 'dt_summary_btn_sizes_title' => __( 'Continue Button Dimensions (Result Page)', 'funnel-elite-lite' ), … … 693 680 /** 694 681 * Get strings for the Public Frontend (built once per locale and request). 695 *696 * Same caching rationale as get_admin_strings().697 682 */ 698 683 public static function get_public_strings() { -
funnel-elite-lite/trunk/includes/class-funnel-main.php
r3706931 r3707052 60 60 $this->loader->add_action( 'wp_ajax_funnel_partial_lead', $plugin_public, 'handle_partial_lead' ); 61 61 $this->loader->add_action( 'wp_ajax_nopriv_funnel_partial_lead', $plugin_public, 'handle_partial_lead' ); 62 // Frische Sicherheitsmarke, wenn ein Seiten-Cache eine abgelaufene ausliefert.63 62 $this->loader->add_action( 'wp_ajax_funnel_fresh_nonce', $plugin_public, 'handle_fresh_nonce' ); 64 63 $this->loader->add_action( 'wp_ajax_nopriv_funnel_fresh_nonce', $plugin_public, 'handle_fresh_nonce' ); -
funnel-elite-lite/trunk/includes/class-funnel-settings-registry.php
r3706931 r3707052 204 204 205 205 /** 206 * Sanitize an optional pixel value for the summary button overrides. 207 * 208 * These fields mean *inherit the global button style* when left empty, so the 209 * empty string has to survive sanitizing. intval('') would be 0 — and 0 is a 210 * legitimate value here (sharp corners, no border, no padding), so the two 211 * cases must stay distinguishable. 206 * Optional pixel value: '' means inherit, 0 is a valid value. 212 207 * 213 208 * @param mixed $value Raw value. … … 228 223 229 224 /** 230 * Sanitize an optional colour for the summary button overrides. 231 * 232 * Same idea as sanitize_optional_px(): empty means inherit. 225 * Optional colour: '' means inherit. 233 226 * 234 227 * @param mixed $value Raw value. … … 325 318 [ 'id' => 'card_bg_intensity', 'sanitize' => 'intval', 'default' => 100 ], 326 319 [ 'id' => 'card_text_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#212529', 'css_var' => '--funnel-card-text' ], 327 // #007bff liess die Kartenbeschreibung beim Ueberfahren und bei328 // Tastatur-Auswahl auf 3.10:1 fallen; #e9ecef bringt sie auf 7.2:1.329 320 [ 'id' => 'card_hover_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#e9ecef', 'css_var' => '--funnel-card-hover-bg' ], 330 321 [ 'id' => 'card_width', 'sanitize' => 'intval', 'default' => 300, 'unit' => 'px' ], … … 400 391 401 392 // ── Buttons ────────────────────────────────────────── 402 // Weiss auf dem Standard-Sandton #c3b696 sind 2.0:1 - unlesbar.403 // #1d2327 (WordPress-Schwarz) ergibt 7.9:1.404 393 [ 'id' => 'btn_text_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#1d2327', 'css_var' => '--funnel-btn-text' ], 405 394 [ 'id' => 'btn_border_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#c3b696', 'css_var' => '--funnel-btn-border-color' ], … … 423 412 424 413 // ── Summary & Price ────────────────────────────────── 425 // Die fuenf mobile_*-Schriftgroessen der Ergebnisseite sind optional: 426 // leer bedeutet, dass der Desktop-Wert auch auf dem Telefon gilt. 427 // Ein fester Standard wuerde bestehende Funnels beim Update 428 // ungefragt umstellen. 414 // mobile_* sizes are optional: empty = desktop value. 429 415 [ 'id' => 'price_font_size', 'sanitize' => 'intval', 'default' => 32, 'css_var' => '--funnel-price-font-size', 'unit' => 'px' ], 430 416 [ 'id' => 'mobile_price_font_size', 'sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ], … … 433 419 [ 'id' => 'summary_label_font_size','sanitize' => 'intval', 'default' => 14, 'css_var' => '--funnel-label-font-size', 'unit' => 'px' ], 434 420 [ 'id' => 'mobile_summary_label_font_size','sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ], 435 // #8c857b plus der Deckkraft 0.8 ergaben 2.68:1 auf Weiss.436 421 [ 'id' => 'summary_label_color', 'sanitize' => 'sanitize_hex_color', 'default' => '#6b645a','css_var' => '--funnel-label-color' ], 437 422 [ 'id' => 'summary_label_text', 'sanitize' => 'sanitize_text_field', 'default' => '' ], … … 451 436 [ 'id' => 'summary_hint_size', 'sanitize' => 'intval', 'default' => 14, 'css_var' => '--funnel-summary-hint-size', 'unit' => 'px' ], 452 437 [ 'id' => 'mobile_summary_hint_size','sanitize' => array( __CLASS__, 'sanitize_optional_px' ), 'default' => '' ], 453 // ── Ergebnisseite: Weiter-Button (alle optional, leer = global erben) ── 454 // Die globalen Button-Einstellungen gelten weiter, solange hier nichts 455 // gesetzt ist. Der CSS-Generator gibt nur die Eigenschaften aus, die 456 // wirklich gefüllt sind — bestehende Funnels sehen unverändert aus. 438 // ── Summary Button (optional, empty = global button style) ── 457 439 [ 'id' => 'summary_btn_bg', 'sanitize' => array( __CLASS__, 'sanitize_optional_hex_color' ), 'default' => '' ], 458 440 [ 'id' => 'summary_btn_text_color', 'sanitize' => array( __CLASS__, 'sanitize_optional_hex_color' ), 'default' => '' ], -
funnel-elite-lite/trunk/includes/emails/trait-email-formatter.php
r3706931 r3707052 5 5 6 6 /** 7 * Te xt einer Mail-Vorlage in der Sprache der Website.7 * Template text in the site language, English fallback. 8 8 * 9 * Die Vorlage hatte ihre Ueberschrift fest auf Englisch im Code. Den 10 * passenden Schluessel gibt es laengst und er ist in allen mitgelieferten 11 * Sprachen uebersetzt - er wurde hier nur nie benutzt. 12 * 13 * Faellt auf den englischen Text zurueck, falls die i18n-Klasse (noch) nicht 14 * geladen ist: eine Mail ohne Ueberschrift waere schlimmer als eine 15 * englische. 16 * 17 * @param string $key Schluessel aus der oeffentlichen i18n-Tabelle. 18 * @param string $fallback Englischer Text, falls nichts gefunden wird. 9 * @param string $key Key of the public i18n table. 10 * @param string $fallback Text used when the key is not available. 19 11 * @return string 20 12 */ -
funnel-elite-lite/trunk/includes/emails/trait-email-preview.php
r3706931 r3707052 13 13 */ 14 14 public static function get_default_admin_template( $settings = array() ) { 15 // No <html>/<body>: wp_kses_post() removes both when the template is 16 // saved - together with the font and background set on them. They sit 17 // on the outer table instead, so a saved template looks like the editor. 18 // No fixed 600px container: phones shrink a fixed width, text included, 19 // to about two thirds. A div with max-width grows with the screen 20 // (Chrome and iOS Mail ignore max-width on a table). 15 // No <html>/<body>: wp_kses_post() strips them on save. max-width sits on 16 // a div because some mail clients ignore it on tables. 21 17 $t = '<table width="100%" cellpadding="0" cellspacing="0" style="background-color: #f4f7f9; padding: 32px 0; font-family: Segoe UI, Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #2d3748;">' . "\n"; 22 18 $t .= '<tr><td align="center" style="padding: 0 12px;">' . "\n"; -
funnel-elite-lite/trunk/includes/emails/trait-email-template.php
r3706931 r3707052 75 75 $message .= '<table width="100%" cellpadding="0" cellspacing="0" style="background-color: ' . $bg_color . '; padding: 40px 0;"><tr><td align="center" style="padding: 0 12px;">'; 76 76 77 // Main Container. Grows with the screen instead of a fixed 600px: 78 // phones shrink a fixed width, text included, to about two thirds, and 79 // Chrome and iOS Mail ignore max-width on a table - hence the div. 77 // Main Container (max-width on a div, some mail clients ignore it on tables) 80 78 $message .= '<div style="max-width: 600px; margin: 0 auto;"><table width="100%" cellpadding="0" cellspacing="0" style="width: 100%; background-color: ' . $container_bg . '; border-radius: 12px; box-shadow: 0 4px 6px rgba(0,0,0,0.05); overflow: hidden;">'; 81 79 … … 153 151 154 152 // Header 155 //156 // This is the mail the RESULT PAGE sends whenever the template editor is157 // empty - and it never had a logo, not even a place for one, although158 // the admin template does. Same markup as build_default_template(), so159 // both look alike at the top.160 153 $message .= '<tr><td style="padding: 30px 40px; text-align: center; border-bottom: 3px solid ' . $primary_color . ';">'; 161 154 if ( $logo_url ) { -
funnel-elite-lite/trunk/public/class-funnel-public.php
r3706931 r3707052 111 111 } 112 112 113 114 /** 115 * Soll auf dieser Seite ueberhaupt etwas vom Funnel geladen werden? 116 * 117 * Ohne is_singular() greift die Pruefung auch auf Such- und Archivseiten: 118 * dort zeigt das globale $post auf den ersten Treffer. Steht darin ein 119 * Funnel, luden alle Stylesheets und das komplette JS mit, obwohl die 120 * Auszuege den Shortcode entfernen und gar kein #funnelbody entsteht. 121 * 122 * Wer den Funnel bewusst ausserhalb einer Einzelansicht rendert (Page 123 * Builder, Widget, Archiv mit vollem Inhalt), schaltet die Dateien wie 124 * bisher ueber den Filter frei: 125 * 126 * add_filter( 'funnel_system_load_assets', '__return_true' ); 113 /** 114 * Load assets only on singular views that contain the shortcode. 127 115 * 128 116 * @return bool … … 250 238 wp_enqueue_script( $this->plugin_name . '-main', $main['url'], array( 'jquery', $this->plugin_name . '-engine' ), $main['version'], true ); 251 239 252 // Drei der angebotenen Knopf-Effekte brauchen JS: "Scroll Trigger"253 // setzt .is-inview, "Click Feedback" setzt .is-pressed, "Ripple Click"254 // legt das .play-ripple-Element an. In Lite fehlte diese Datei, die255 // Effekte standen also im Admin zur Wahl und taten nichts - "Scroll256 // Trigger" liess den Knopf sogar dauerhaft blass.257 240 $effect_settings = $project_id ? $this->get_project_settings( $project_id ) : null; 258 241 if ( $effect_settings ) { … … 287 270 wp_add_inline_script( 288 271 $this->plugin_name . '-model', 289 // Nur wenn der Funnel wirklich gerendert wurde: auf Such- 290 // und Archivseiten strippen Auszuege den Shortcode, die 291 // Klasse haette dort das Scrollen der ganzen Seite gesperrt. 272 // Excerpts strip the shortcode, so check that the funnel was rendered. 292 273 'if(document.getElementById("funnelbody")){document.documentElement.classList.add("funnel-active-page");document.body.classList.add("funnel-active-page");}', 293 274 'before' … … 405 386 if ($bg_type === 'image') $layout_class = 'has-bg-image'; 406 387 if ($bg_type === 'pattern') $layout_class = 'has-bg-pattern'; 407 // "Vollbreite erzwingen": Marker fuer das JS, das die beiden Abstaende408 // zum Fensterrand misst (--funnel-bleed-l / -r). Ohne JS bleibt der409 // Funnel einfach in seiner Spalte.410 388 if ( ! empty( $settings['enable_fullscreen_override'] ) ) { 411 389 $layout_class = trim( $layout_class . ' funnel-bleed' ); … … 644 622 645 623 /** 646 * Returns a fresh public nonce for the funnel's own AJAX request. 647 * 648 * The nonce (funnel_public_nonce) is printed into the page. A page cache can 649 * serve a copy that is older than the nonce lifetime (12-24 hours); then 650 * handle_partial_lead() rejects the request with 403 and the selection 651 * e-mail is silently lost. After such a rejection the frontend calls this 652 * endpoint once and repeats the request with the new nonce. 653 * 654 * Intentionally no nonce check here: a visitor holding an expired nonce must 655 * be able to obtain a new one. The endpoint reads no input, writes nothing 656 * and returns exactly what every page load already prints; it sends 657 * no-cache headers. admin-ajax only lets the site's own origins read the 658 * response (get_allowed_http_origins()). The nonce it returns only unlocks 659 * funnel_partial_lead, which verifies it and is open to visitors anyway. 624 * Returns a fresh public nonce, so a cached page with an expired one can retry. 625 * 626 * No nonce check by design: it reads no input, writes nothing and returns 627 * what every page load prints anyway. 660 628 * 661 629 * @return void … … 742 710 } 743 711 744 // The logo has to travel in the meta: build_partial_lead_email() only745 // sees what it is handed, and without this the result mail never had746 // one - neither the logo nor anywhere to put it.747 712 $email_meta = array( 748 713 'subject' => $subject, … … 760 725 } 761 726 762 // Sender name from the Email tab - resolved before SMTP is configured,763 // because configure_phpmailer() sets it last and would otherwise win.764 727 $from_name = self::resolve_from_name( $project_settings ); 765 728 … … 877 840 878 841 /** 879 * The sender name as the admin typed it into "Sender Name". 880 * 881 * The result mail used to send as get_bloginfo('name') and never read this 882 * field. Only the "Send test email" button used it - so the test mail 883 * showed the configured name and the real mails did not, which is exactly 884 * why it went unnoticed. 885 * 886 * Order follows the rest of the mail code (see the logo in 887 * prepare_placeholder_data): project value, then the global option the Email 888 * tab writes, then the site name. 842 * Sender name: project setting, then the global option, then the site name. 889 843 * 890 844 * @param array $project_settings Decoded project settings. … … 900 854 } 901 855 902 // The site name is stored HTML-escaped ("A & B"); a mail header 903 // wants the plain text. Line breaks would allow header injection, and 904 // angle brackets break wp_mail's "Name <address>" parsing. 856 // Strip line breaks (header injection) and angle brackets ("Name <address>"). 905 857 $name = wp_specialchars_decode( $name, ENT_QUOTES ); 906 858 … … 909 861 910 862 /** 911 * The brand logo from the Email tab, same order as resolve_from_name().863 * Brand logo: project setting, then the global option. 912 864 * 913 865 * @param array $project_settings Decoded project settings. … … 925 877 * Apply the plugin's custom SMTP configuration to PHPMailer (if set). 926 878 * 927 * @param string $from_name Display name for the From header. With SMTP 928 * authentication the address has to be the SMTP 929 * user, but the NAME is free - and this is the 930 * last place it is set before sending, so a 931 * correct header alone would be overwritten here. 879 * @param string $from_name Display name for the From header. 932 880 */ 933 881 private function configure_phpmailer( $from_name = '' ) { … … 971 919 $phpmailer->Username = $user; 972 920 $phpmailer->Password = $pass; 973 // The ADDRESS has to be the SMTP user (servers reject a From974 // they do not own). The NAME stays the configured one - forcing975 // it to the site name here silently undid the From header.976 921 $phpmailer->From = $user; 977 922 $phpmailer->FromName = $from_name; -
funnel-elite-lite/trunk/public/css/funnel-button-attention.css
r3706931 r3707052 69 69 content: ""; 70 70 position: absolute; 71 /* Langform statt `inset`: aeltere mobile Browser (Samsung Internet < 14, 72 Chrome < 87, iOS < 14.5) kennen die Kurzform nicht und verwerfen die 73 Deklaration komplett - der Ring faellt dann auf wenige Pixel zusammen. */ 71 /* Longhand instead of inset for older mobile browsers. */ 74 72 top: -6px; 75 73 right: -6px; … … 187 185 transition: none !important; 188 186 transform: none !important; 189 /* Kein `opacity: 1 !important` mehr: es hat auch den ausgegrauten190 "Weiter"-Knopf wieder voll sichtbar gemacht, obwohl er nicht191 klickbar ist - und liess den Ring am Video-Knopf dauerhaft stehen. */192 187 box-shadow: none !important; 193 188 } -
funnel-elite-lite/trunk/public/css/funnel-button-effects.css
r3706931 r3707052 103 103 104 104 /* ---------- Play Button Effects ---------- */ 105 /* `:active` ist hier kein Beiwerk: Ein Telefon kennt kein Hover, also waeren 106 diese vier Effekte auf jedem Mobilgeraet wirkungslos - genau das war die 107 Beobachtung "ich merke da nichts". Mit `:active` reagieren sie auf die 108 Beruehrung, solange der Finger liegt. Am Rechner aendert sich nichts: wer 109 klickt, fuhr vorher schon mit dem Zeiger darueber. */ 105 /* :active makes these effects work on touch screens. */ 110 106 .funnel-container .play-effect--soft-pulse:hover, 111 107 .funnel-container .play-effect--soft-pulse:focus-visible, … … 117 113 content: ""; 118 114 position: absolute; 119 /* L angform statt `inset` - siehe funnel-button-attention.css. */115 /* Longhand instead of inset for older mobile browsers. */ 120 116 top: -4px; 121 117 right: -4px; … … 189 185 } 190 186 191 /* Mobil/Desktop (17.09.2026): Der Ring des Effekts "Glow Ring" lag ausserhalb 192 des Knopfes und wurde von dessen overflow: hidden abgeschnitten - der Effekt 193 war unsichtbar. !important, weil funnel-button-attention.css spaeter geladen 194 wird und dieselbe Spezifitaet hat. */ 187 /* The ring sits outside the button. !important: funnel-button-attention.css loads later. */ 195 188 .funnel-container .play-btn.play-effect--glow-ring { 196 189 overflow: visible !important; -
funnel-elite-lite/trunk/public/css/funnel-cards.css
r3706931 r3707052 42 42 box-shadow: 0 12px 30px rgba(0, 0, 0, 0.08); 43 43 border-color: rgba(0, 0, 0, 0.1); 44 /* Eigener Rahmen statt nur der Hover-Farbe: bei Verlauf-, Glanz- oder 45 Mesh-Hintergruenden deckt das Hintergrundbild die Hover-Farbe zu, dann 46 war die Tastatur-Auswahl nur noch am 8px-Anheben zu erkennen. */ 44 /* Background images can cover the hover colour, so add an outline. */ 47 45 outline: 2px solid var(--funnel-headline, #1d2327); 48 46 outline-offset: 3px; … … 111 109 font-family: var(--funnel-font-family, sans-serif); 112 110 font-size: var(--funnel-card-title-size, 1.4rem); 113 /* War fest auf #2c2825. Auf dunklen Karten (Karte #1d2327, Text weiss)114 blieb der Titel fast schwarz - gemessen 1.09:1. */115 111 color: var(--funnel-card-text, #2c2825); 116 112 margin: 0; … … 118 114 word-wrap: break-word; 119 115 overflow-wrap: break-word; 120 /* Keine Silbentrennung: `hyphens: auto` zerlegte Antworten mitten im Wort 121 ("Fotoshooting & Brautabho-lung & Saalbegleitung"). Ein Umbruch ZWISCHEN 122 Woertern ist in Ordnung, einer INNERHALB nicht. 123 `manual` statt weglassen, weil hyphens vererbt wird - ein Theme mit 124 `hyphens: auto` auf body wuerde es sonst wieder einschleppen. 125 `overflow-wrap: break-word` bleibt als Notnagel: es greift nur, wenn ein 126 einzelnes Wort allein nicht auf eine Zeile passt. */ 116 /* manual, not unset: hyphens is inherited and themes may set auto. */ 127 117 hyphens: manual; 128 118 -webkit-hyphens: manual; … … 142 132 word-wrap: break-word; 143 133 overflow-wrap: break-word; 144 /* Gleiche Regel wie beim Titel - siehe dort. */145 134 hyphens: manual; 146 135 -webkit-hyphens: manual; … … 341 330 342 331 /* ---- WEITER / SUMMARY BUTTON: globale Layout- & Stil-Basis ---- */ 343 /* Stil (Farbe, Padding, Font) wird über .btn-primary im CSS-Generator aus den Admin-Einstellungen gesetzt*/332 /* Colour, padding and font come from .btn-primary in the CSS generator */ 344 333 .funnel-container .funnel-card-next { 345 334 display: block; … … 372 361 } 373 362 374 /* Der Haken schiebt das Wort aus der Mitte: die Gruppe aus Haken und Wort wird 375 zentriert, also sitzt das Wort um die halbe Hakenbreite plus den halben 376 Abstand daneben - gemessen 10,9 px bei 320 px Knopfbreite. Auffallen tut aber 377 das Wort, nicht die Gruppe. 378 Deshalb steht auf der Gegenseite ein unsichtbarer Zwilling desselben 379 Zeichens. Er ist exakt so breit wie der Haken - auch wenn eine andere 380 Schrift einspringt, was bei U+2713 in einer Serifenschrift die Regel ist - 381 und traegt denselben `gap`. Damit liegt das Wort auf der Knopfmitte. 382 `visibility: hidden` statt `display: none`: der Platz muss erhalten bleiben. */ 363 /* Invisible twin of the tick keeps the label centred. */ 383 364 #btn-summary-next .summary-next-balance { 384 365 visibility: hidden; … … 435 416 } 436 417 437 /* ── Mobil (17.09.2026) ──────────────────────────────────────────────────── 438 Streifen-Layout: Auf schmalen Telefonen ragten Preis-Abzeichen und Bild aus 439 der Karte heraus, sobald ein Titel ein langes Wort hatte (gemessen bei 320 440 und 375px: Abzeichen endete 37px hinter der Karte, Bild links angeschnitten). 441 Die flexiblen Spalten geben ohne min-width: 0 nicht nach. Unter 480px wird 442 das Bild kleiner, sonst bleibt fuer den Titel nur ein schmaler Streifen. */ 418 /* Stripe layout: flex columns need min-width: 0 to shrink. */ 443 419 #funnelbody .funnel-layout-stripe .card-body, 444 420 #funnelbody .funnel-layout-stripe .card-text-wrapper { … … 463 439 } 464 440 465 /* Das Abzeichen nimmt sonst die halbe Kartenbreite, und der Titel bricht466 mitten im Wort. Groesser als 13px wird es hier nicht, kleiner eingestellte467 Werte bleiben erhalten. */468 441 #funnelbody .funnel-layout-stripe .card-price-badge, 469 442 #funnelbody .funnel-layout-stripe .card-adjust-badge, … … 474 447 } 475 448 476 /* Wer im Betriebssystem "Bewegung reduzieren" eingestellt hat, bekam trotzdem477 das volle Programm: 8px-Anheben bei Hover und Tastatur-Auswahl, dazu die478 0.4s-Ueberblendung auf jeder Karte. */479 449 @media (prefers-reduced-motion: reduce) { 480 450 .st-funnel-card .card { -
funnel-elite-lite/trunk/public/css/funnel-core.css
r3706931 r3707052 3 3 4 4 /* GLOBAL PAGE OVERRIDE - FULLSCREEN MODE 5 Die Hoehe kommt aus top/bottom, NICHT aus 100vh: iOS Safari rechnet vh mit 6 eingeklappten Leisten. Der Kasten ragte dadurch 80-135px unter die untere 7 Safari-Leiste, und weil html/body gesperrt sind, klappt Safari sie nie ein - 8 das Ende der Liste war nie erreichbar (iPhone 13, 17.09.2026). Mit 9 bottom: 0 sitzt die Unterkante immer auf der sichtbaren Kante. 10 overscroll-behavior-y haelt die Wischgeste im Funnel, statt sie an die 11 gesperrte Seite zu geben (nur y: x wuerde das Zurueckwischen in Chrome 12 abschalten). */ 5 Height from top/bottom, not 100vh: iOS Safari measures vh without its bars. */ 13 6 body.funnel-active-page #funnelbody { 14 7 position: fixed !important; … … 98 91 } 99 92 100 /* Gescopt: .active-step ist ein gaengiger Klassenname in Steppern und101 Kassen-Strecken; mit !important hob die Regel fremde Elemente an. */102 93 #funnelbody .active-step { 103 94 position: relative !important; … … 142 133 gap: 8px; 143 134 background: transparent !important; 144 /* Liest jetzt die Einstellung "Zurueck-Knopf-Farbe"; 0.85 statt 0.7 hebt145 den Kontrast auf weissem Grund von 3.57:1 auf 5.11:1 (AA). */146 135 color: var(--funnel-back-btn-color, #4a5568) !important; 147 136 border: none !important; … … 159 148 160 149 .funnel-back-btn:hover { 161 /* Die feste Hover-Farbe ist raus: sie hat jede eingestellte Farbe beim162 Ueberfahren wieder verworfen. Die volle Deckkraft reicht als Rueckmeldung. */163 150 opacity: 1; 164 151 transform: translateX(-3px); … … 175 162 body.funnel-active-page #funnelbody { 176 163 font-size: var(--funnel-mobile-font-size, 14px); 177 /* Im Vollbild steht ueber der Ueberschrift nichts mehr - die 60px vom178 Desktop sind auf einem Telefon verschenkte Hoehe, also 15px weniger.179 !important, weil die Grundregel padding als Kurzform mit !important180 setzt und sonst gewinnt. */181 164 padding-top: 45px !important; 182 165 padding-bottom: 60px !important; 183 /* Zweite Angabe fuer Geraete mit Hausbalken/Notch (nur mit184 viewport-fit=cover im Theme wirksam, sonst 0). */185 166 padding-bottom: calc(60px + env(safe-area-inset-bottom, 0px)) !important; 186 167 } … … 276 257 } 277 258 278 /* ── Mobil (17.09.2026) ────────────────────────────────────────────────────279 Ueberschriften: lange deutsche Woerter liefen aus dem Bild und wurden280 abgeschnitten (gemessen bei 320px). Umgebrochen wird nur, was sonst nicht281 passt; hyphens: manual haelt die bewusste Entscheidung gegen automatische282 Silbentrennung und ueberstimmt ein Theme mit hyphens: auto. */283 259 #funnelbody h1, 284 260 #funnelbody h2 { … … 288 264 } 289 265 290 /* Zurueck-Knopf: nur 15-18px hoch, im Querformat 12px - zu klein zum 291 Antippen. 44px ist die gaengige Mindestgroesse fuer Finger. Die zweite 292 font-size gilt nur, wo max() bekannt ist; aeltere Engines behalten die 293 erste Zeile, statt die Groesse an das Theme zu verlieren. */ 266 /* 44px touch target. The first font-size is the fallback without max(). */ 294 267 .funnel-back-btn { 295 268 min-height: 44px; … … 300 273 } 301 274 302 /* ── Drucken (17.09.2026) ─────────────────────────────────────────────────303 Im Vollbild blieb der Funnel beim Drucken ein fester, scrollender Kasten:304 das Blatt zeigte nur den ersten Bildschirm, der Rest des Schritts oder der305 Ergebnisseite fehlte. Beim Druck wird der Funnel wieder normaler Inhalt. */306 275 @media print { 307 276 html.funnel-active-page, … … 326 295 } 327 296 328 /* Eigener Stapelkontext im Section-Modus: die Schritte (z-index: 100) bleiben 329 im Funnel, statt sich mit festen Kopfzeilen des Themes zu messen. */ 297 /* Keeps the steps' z-index inside the funnel. */ 330 298 body:not(.funnel-active-page) #funnelbody { 331 299 isolation: isolate; 332 300 } 333 301 334 /* Ladeanzeige der Ergebnisseite bei "Bewegung reduzieren" anhalten. Die 335 Effekte stehen inline am Element, deshalb braucht es hier !important. 336 Der Pulsring wuchs sonst dauerhaft auf das 2,1-fache. */ 302 /* Loader effects are inline styles, hence !important. */ 337 303 @media (prefers-reduced-motion: reduce) { 338 304 .funnel-summary-loader, … … 346 312 } 347 313 348 /* Sprachen von rechts nach links: der Pfeil des Zurueck-Knopfes muss in die349 andere Richtung zeigen und sich auch dorthin bewegen. */350 314 [dir="rtl"] .funnel-back-btn::before { 351 content: " 92";315 content: "\2192"; 352 316 } 353 317 -
funnel-elite-lite/trunk/public/css/funnel-core.min.css
r3706931 r3707052 2 2 --funnel-font-family, 3 3 sans-serif 4 )!important;font-size:2.8em;font-weight:400;margin-bottom:1.5rem;color:var(--funnel-headline,#2c2825);line-height:1.2}#funnelbody .funnel-description,.funnel-description{font-family:var(--funnel-font-family, sans-serif)!important;font-size:var(--funnel-group-desc-size, 1.1rem);color:var(--funnel-text,#6d665e);font-weight:300;max-width:600px;margin:0 auto 30px}.funnel-back-btn{display:inline-flex!important;align-items:center;gap:8px;background:0 0!important;color:var(--funnel-back-btn-color,#4a5568)!important;border:none!important;font-family:var(--funnel-font-family, sans-serif);font-weight:400;text-transform:uppercase;letter-spacing:1.5px;cursor:pointer;transition:opacity .3s,color .3s,transform .3s;opacity:.85}.funnel-back-btn:hover{opacity:1;transform:translateX(-3px)}.funnel-back-btn::before{content:"\2190";font-size:1.1rem;line-height:1}#funnel-progress-wrapper{width:100%;max-width:var(--funnel-progress-width,600px);margin:0 auto;position:relative;height:var(--funnel-progress-size,6px);background:rgba(0,0,0,.05);border-radius:calc(var(--funnel-progress-size,6px)/ 2);overflow:hidden;z-index:10}#funnel-progress-fill{height:100%;background-color:var(--funnel-progress-color,#0ba360);transition:width .5s cubic-bezier(.22, 1, .36, 1);border-radius:calc(var(--funnel-progress-size,6px)/ 2)}.funnel-pb-shape-segmented{background:repeating-linear-gradient(90deg,rgba(0,0,0,.05),rgba(0,0,0,.05) 10px,transparent 10px,transparent 15px)!important;border-radius:0!important}.funnel-pb-shape-segmented #funnel-progress-fill{background-image:repeating-linear-gradient(90deg,transparent,transparent 10px,rgba(255,255,255,.3) 10px,rgba(255,255,255,.3) 15px)!important;border-radius:0!important}.funnel-pb-shape-dots{background:rgba(0,0,0,.1)!important;border-radius:0!important;mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));-webkit-mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%;-webkit-mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%}.funnel-pb-shape-dots #funnel-progress-fill{border-radius:0!important}@media (max-width:768px){body.funnel-active-page #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-top:45px!important;padding-bottom:60px!important;padding-bottom:calc(60px + env(safe-area-inset-bottom,0px))!important}body:not(.funnel-active-page) #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-bottom:60px!important}.st-funnel-step h2{font-size:2.2em}.funnel-description{font-size:var(--funnel-mobile-group-desc-size, 14px)}#funnel-progress-wrapper{margin:0 auto 20px;width:calc(100% - 30px)}}#funnelbody h1,#funnelbody h2{overflow-wrap:break-word;hyphens:manual;-webkit-hyphens:manual}.funnel-back-btn{min-height:44px;padding:0 12px 0 0!important;margin-bottom:8px;font-size:.85rem!important;font-size:max(13px, .85rem)!important}@media print{body.funnel-active-page,html.funnel-active-page{overflow:visible!important;height:auto!important}body.funnel-active-page #funnelbody,body:not(.funnel-active-page) #funnelbody{position:static!important;height:auto!important;max-height:none!important;overflow:visible!important}.funnel-loader,.funnel-notice-overlay,.funnel-video-modal{display:none!important}}@media (prefers-reduced-motion:reduce){.funnel-loader .spinner,.funnel-summary-loader,.funnel-summary-loader *{animation:none!important}}[dir=rtl] .funnel-back-btn::before{content:" 92"}[dir=rtl] .funnel-back-btn:hover{transform:translateX(3px)}4 )!important;font-size:2.8em;font-weight:400;margin-bottom:1.5rem;color:var(--funnel-headline,#2c2825);line-height:1.2}#funnelbody .funnel-description,.funnel-description{font-family:var(--funnel-font-family, sans-serif)!important;font-size:var(--funnel-group-desc-size, 1.1rem);color:var(--funnel-text,#6d665e);font-weight:300;max-width:600px;margin:0 auto 30px}.funnel-back-btn{display:inline-flex!important;align-items:center;gap:8px;background:0 0!important;color:var(--funnel-back-btn-color,#4a5568)!important;border:none!important;font-family:var(--funnel-font-family, sans-serif);font-weight:400;text-transform:uppercase;letter-spacing:1.5px;cursor:pointer;transition:opacity .3s,color .3s,transform .3s;opacity:.85}.funnel-back-btn:hover{opacity:1;transform:translateX(-3px)}.funnel-back-btn::before{content:"\2190";font-size:1.1rem;line-height:1}#funnel-progress-wrapper{width:100%;max-width:var(--funnel-progress-width,600px);margin:0 auto;position:relative;height:var(--funnel-progress-size,6px);background:rgba(0,0,0,.05);border-radius:calc(var(--funnel-progress-size,6px)/ 2);overflow:hidden;z-index:10}#funnel-progress-fill{height:100%;background-color:var(--funnel-progress-color,#0ba360);transition:width .5s cubic-bezier(.22, 1, .36, 1);border-radius:calc(var(--funnel-progress-size,6px)/ 2)}.funnel-pb-shape-segmented{background:repeating-linear-gradient(90deg,rgba(0,0,0,.05),rgba(0,0,0,.05) 10px,transparent 10px,transparent 15px)!important;border-radius:0!important}.funnel-pb-shape-segmented #funnel-progress-fill{background-image:repeating-linear-gradient(90deg,transparent,transparent 10px,rgba(255,255,255,.3) 10px,rgba(255,255,255,.3) 15px)!important;border-radius:0!important}.funnel-pb-shape-dots{background:rgba(0,0,0,.1)!important;border-radius:0!important;mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));-webkit-mask-image:radial-gradient(circle,black calc(var(--funnel-progress-size,6px)/ 2),transparent calc(var(--funnel-progress-size,6px)/ 2 + 0.5px));mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%;-webkit-mask-size:calc(var(--funnel-progress-size,6px) * 2.5) 100%}.funnel-pb-shape-dots #funnel-progress-fill{border-radius:0!important}@media (max-width:768px){body.funnel-active-page #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-top:45px!important;padding-bottom:60px!important;padding-bottom:calc(60px + env(safe-area-inset-bottom,0px))!important}body:not(.funnel-active-page) #funnelbody{font-size:var(--funnel-mobile-font-size, 14px);padding-bottom:60px!important}.st-funnel-step h2{font-size:2.2em}.funnel-description{font-size:var(--funnel-mobile-group-desc-size, 14px)}#funnel-progress-wrapper{margin:0 auto 20px;width:calc(100% - 30px)}}#funnelbody h1,#funnelbody h2{overflow-wrap:break-word;hyphens:manual;-webkit-hyphens:manual}.funnel-back-btn{min-height:44px;padding:0 12px 0 0!important;margin-bottom:8px;font-size:.85rem!important;font-size:max(13px, .85rem)!important}@media print{body.funnel-active-page,html.funnel-active-page{overflow:visible!important;height:auto!important}body.funnel-active-page #funnelbody,body:not(.funnel-active-page) #funnelbody{position:static!important;height:auto!important;max-height:none!important;overflow:visible!important}.funnel-loader,.funnel-notice-overlay,.funnel-video-modal{display:none!important}}@media (prefers-reduced-motion:reduce){.funnel-loader .spinner,.funnel-summary-loader,.funnel-summary-loader *{animation:none!important}}[dir=rtl] .funnel-back-btn::before{content:"\2192"}[dir=rtl] .funnel-back-btn:hover{transform:translateX(3px)} -
funnel-elite-lite/trunk/public/css/funnel-modal.css
r3706931 r3707052 8 8 width: 100%; 9 9 height: 100%; 10 /* Liest die Einstellung "Modal Overlay Color"; der Rueckfallwert ist11 genau der bisherige Ton, bestehende Seiten sehen unveraendert aus. */12 10 background-color: var(--funnel-modal-overlay, rgba(44, 40, 37, 0.96)); 13 11 backdrop-filter: blur(10px); … … 31 29 color: #fff !important; 32 30 font-family: var(--funnel-font-family, sans-serif); 33 /* 0.3rem waren 4.8px - ein unlesbarer Fleck neben dem Kreuz. */34 31 font-size: 0.75rem; 35 32 text-transform: uppercase; … … 52 49 } 53 50 54 /* An das Popup gebunden: Themes nutzen .video-container fuer ihre eigenen55 responsiven Einbettungen, die Regel hat sie auf 16:9 gezwungen. */56 51 .funnel-video-modal .video-container { 57 52 position: relative; … … 87 82 } 88 83 89 /* ── Mobil (17.09.2026) ──────────────────────────────────────────────────── 90 Video-Popup im Querformat: Unterkante des Videos (Steuerleiste) lag 91 ausserhalb des Bildschirms, der Schliessen-Knopf teilweise daneben. Die 92 Breite richtet sich jetzt nach der verfuegbaren Hoehe (16:9), oben bleiben 93 mindestens 60px fuer den Knopf. Am Desktop aendert sich nichts: dort 94 begrenzt weiter max-width: 1000px. */ 84 /* 16:9 width from the available height, at least 60px above for the close button. */ 95 85 .funnel-video-modal .modal-content { 96 86 width: min(90%, calc((100vh - 140px) * 16 / 9)); … … 104 94 } 105 95 106 /* Feste Groesse statt rem: im Querformat schrumpfte die Themeschrift, und107 "Schliessen" stand mit ~4px da. */108 96 .funnel-video-modal .close { 109 97 font-size: 12px; … … 112 100 } 113 101 114 /* Bei starker Zoomstufe (200 %) ragte der Inhalt des Video-Popups unten aus115 dem Bild und war nicht erreichbar - das Overlay konnte nicht scrollen. */116 102 .funnel-video-modal { 117 103 overflow-y: auto; 118 104 } 119 105 120 /* Der Schliesser ist ein <span> mit role="button"; ohne diese Regel sieht121 man beim Tabben nicht, dass er ausgewaehlt ist. */122 106 .funnel-video-modal .close:focus-visible { 123 107 outline: 2px solid #fff; -
funnel-elite-lite/trunk/public/css/funnel-utilities.css
r3706931 r3707052 1 /* Alle Klassennamen hier sind an .funnel-container gebunden (die Klasse 2 sitzt auf #funnelbody). Ungescopt faerbten .row, .btn-primary und Co. 3 auf Theme-Markup derselben Seite ab. 17.09.2026 */ 1 /* Scoped to .funnel-container (on #funnelbody) so theme markup is untouched. */ 4 2 /* ---- UTILITY CLASSES (Lightweight Bootstrap Replacement) ---- */ 5 3 /* Only the exact utility classes used by the funnel frontend */ … … 28 26 29 27 :where(.funnel-container) .text-left { 30 /* start statt left: in Arabisch/Hebraeisch soll der Text rechts beginnen.31 In Sprachen von links nach rechts ist das identisch. */32 28 text-align: start; 33 29 } -
funnel-elite-lite/trunk/public/js/funnel-motion.js
r3706931 r3707052 28 28 29 29 /** 30 * Hat der Besucher im Betriebssystem "Bewegung reduzieren" eingeschaltet? 31 * 32 * Wird bei jedem Aufruf frisch gelesen, damit eine Umstellung waehrend des 33 * Besuchs sofort greift. 30 * Is "reduce motion" enabled? Read on every call. 34 31 */ 35 32 function prefersReducedMotion() { -
funnel-elite-lite/trunk/public/js/modules/funnel-engine.js
r3706931 r3707052 12 12 let isUIBackClick = false; 13 13 14 // Oeffentliche Anfragen mit Sicherheitsmarke (Nonce). Die Marke steht in der 15 // Seite; liefert ein Seiten-Cache eine Kopie aus, die aelter als 12-24 16 // Stunden ist, antwortet WordPress mit 403 "-1" - die Auswahl-Mail blieb 17 // dann still aus. Jetzt: einmal eine frische Marke holen 18 // (funnel_fresh_nonce) und dieselbe Anfrage wiederholen. 14 // A cached page can carry an expired nonce: fetch a fresh one and retry once. 19 15 let nonceRefresh = null; 20 16 const freshNonce = () => { 21 // Gleichzeitige Anfragen teilen sich EINE Erneuerung.17 // Concurrent requests share one refresh. 22 18 if (nonceRefresh) return nonceRefresh; 23 19 const d = $.Deferred(); 24 20 const p = d.promise(); 25 21 nonceRefresh = p; 26 const fertig= () => {22 const done = () => { 27 23 if (nonceRefresh === p) nonceRefresh = null; 28 24 }; … … 31 27 { action: "funnel_fresh_nonce" }, 32 28 (res) => { 33 fertig();29 done(); 34 30 if (res && res.success && res.data && res.data.nonce) { 35 31 funnelDataRaw.nonce = res.data.nonce; … … 42 38 if (req && typeof req.fail === "function") { 43 39 req.fail(() => { 44 fertig();40 done(); 45 41 d.reject(); 46 42 }); … … 56 52 ); 57 53 }; 58 // Wie $.post(url, daten, erfolg) - liefert ein Promise mit .fail/.always.54 // Like $.post(url, data, success); returns a promise. 59 55 const publicPost = (data, success) => { 60 // Die Daten beim Aufruf einfrieren. Die Aufrufer uebergeben historyStack 61 // selbst; eine Wiederholung nach der Erneuerung haette sonst mitgenommen, 62 // was spaetere Klicks darin geaendert haben (neue Auswahl, alter Preis). 56 // Snapshot, so a retry does not pick up later changes. 63 57 const payload = $.extend(true, {}, data); 64 58 const result = $.Deferred(); 65 59 const send = (retried) => { 66 60 const sentNonce = funnelDataRaw.nonce; 67 // Abgelehnt: einmal mit frischer Marke wiederholen. Hat eine andere 68 // Anfrage inzwischen schon eine geholt, gleich mit der. 61 // Reuse a nonce another request already refreshed. 69 62 const retry = (fail) => { 70 63 if (retried) return false; … … 200 193 } 201 194 202 // Kein Inline-z-index mehr: die 50 schlugen die 100000 aus dem203 // Stylesheet, dadurch lagen feste Theme-Kopfzeilen (Kadence, Astra,204 // Divi, Chat-Widgets) ueber dem Vollbild-Funnel, und im Section-Modus205 // lag der Funnel umgekehrt ueber Menues des Themes. 17.09.2026206 207 195 // Consolidated Video Modal [v5 - Responsive] 208 196 if ($("#funnel-video-modal").length === 0) { … … 273 261 274 262 $modal.fadeIn(300, function () { 275 // Fokus ins Popup, sonst steht die Tastatur weiter hinter dem276 // Overlay im Schritt und Escape traefe niemanden.277 263 $modal.find(".close").trigger("focus"); 278 264 }); … … 303 289 ); 304 290 305 // Mit der Tastatur: Enter/Leertaste auf dem Schliesser, Escape ueberall.306 // Ohne das kam man aus dem bildschirmfuellenden Video nicht mehr heraus.307 291 $(document).on("keydown", "#funnel-video-modal .close", function (e) { 308 292 if (e.key === "Enter" || e.key === " " || e.key === "Spacebar") { … … 707 691 const safeText = (text || "").toString().replace(/\n/g, "<br>"); 708 692 709 // Die Klasse ist der Anker fuer "Bewegung reduzieren" (funnel-core.css); 710 // die Effekte selbst haengen an inline gesetzten animation-Werten. 693 // The class is the reduced-motion hook in funnel-core.css. 711 694 const commonWrapperStart = `<div class="funnel-summary-loader" style="display:flex; align-items:center; justify-content:center; min-height:60vh; width:100%; padding:20px; box-sizing:border-box; font-family:var(--funnel-font-family, sans-serif);"> 712 695 <div style="width:100%; max-width:540px; text-align:center; padding:30px 20px; color:var(--funnel-headline, #2c2825);">`; … … 721 704 } 722 705 723 // Abstaende und Positionen bewusst ohne `gap` und ohne `inset`: beide 724 // kennen aeltere mobile Browser (Samsung Internet < 14, Chrome < 87, 725 // iOS < 14.5) nicht, und eine unbekannte Deklaration wird komplett 726 // verworfen. Beim Pulsring fiel der Ring dadurch von 27px auf 4px 727 // zusammen - der Effekt war unsichtbar, waehrend die anderen drei 728 // liefen. Nachgemessen am 15.09.2026. 706 // No gap or inset, older mobile browsers drop them. 729 707 if (effect === "dots") { 730 708 return `${commonWrapperStart} -
funnel-elite-lite/trunk/public/js/modules/funnel-renderer.js
r3706931 r3707052 59 59 funnelDataRaw.section_display_mode === "dynamic" 60 60 ) { 61 // Messen statt schaetzen: der Kasten ist border-box, outerHeight()62 // enthaelt also Innenabstaende und die Fortschrittsleiste. Die alte63 // Rechnung "Schritt + 80" lag daneben, und weil der Section-Kasten64 // overflow: hidden hat, schnitt die zu kleine Hoehe Karten ab.65 61 const $body = $(parent); 66 62 $body.css("height", "auto"); … … 70 66 71 67 /** 72 * "Vollbreite erzwingen": misst, wie weit die Inhaltsspalte vom linken und 73 * rechten Fensterrand entfernt ist, und gibt beides als CSS-Variablen 74 * weiter. Der frueher benutzte Trick width:100vw; left:50%; margin-left:-50vw 75 * stimmt nur, wenn die Spalte mittig im Fenster sitzt - in einem Layout mit 76 * Seitenleiste schnitt er den Funnel links ab (gemessen bei 1440px: 240px). 77 * 78 * clientWidth statt innerWidth: sonst zaehlt auf Windows die klassische 79 * Scrollleiste (~17px) mit und der Funnel wird zu breit. 68 * "Force Full Screen Width": distance of the column to both viewport edges. 69 * clientWidth excludes the scrollbar. 80 70 */ 81 71 this.updateBleed = () => { … … 92 82 }; 93 83 94 // Beim Drehen, Zoomen oder Fenster-Aendern neu messen: vorher blieb die95 // feste Hoehe des letzten Schritts stehen (gemessen: 199px fehlten nach96 // dem Drehen auf 1024x768). 17.09.202697 84 let heightTimer = null; 98 85 if (typeof window !== "undefined" && window.addEventListener) { … … 248 235 249 236 // --- BREATHING ROOM DELAY (skip if old step was interrupted) --- 250 // Bei "Bewegung reduzieren" faellt die Pause weg: sonst bleibt der251 // Bildschirm bei der Standardgeschwindigkeit 0.75s lang leer, obwohl252 // gar nichts mehr animiert wird.253 237 let reducedMotion = false; 254 238 try { … … 436 420 }); 437 421 438 // iOS wendet `:active` auf ein <button> nur an, wenn das Element selbst 439 // einen Touch-Listener traegt - ein bekannter Eigensinn von Safari. 440 // Ohne diese leere Zeile bleiben die Beruehrungs-Effekte auf iPhones 441 // aus, waehrend sie auf Android laufen. `passive`, damit das Scrollen 442 // nicht ausgebremst wird. 422 // iOS applies :active only to elements with a touch listener. 443 423 $btn[0].addEventListener("touchstart", function () {}, { passive: true }); 444 424 … … 473 453 $row.append($headline); 474 454 475 // Das Feld im Backend ist ein <textarea>: wer dort Enter drueckt, meint476 // einen Umbruch. HTML frisst ihn, also wird er zu <br>. Ergebnistext,477 // Hinweistext, Formularbeschreibungen und die Limit-Meldung machen das478 // laengst - die Seitenbeschreibung war die Ausnahme.479 455 const $desc = $("<p />", { 480 456 class: "funnel-description text-center col-12",
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)