From 89bd555dc1552f757cc85764f296643288d01f51 Mon Sep 17 00:00:00 2001 From: Viktor Miller Date: Sat, 7 Feb 2026 23:46:36 +0900 Subject: [PATCH] feat: modernize wizard UX with smart rows, steppers, transitions, and edit links - Hide montage toggles and dim furniture rows when quantity is 0 - Animate running totals bar with CSS transform instead of display toggle - Add directional slide transitions (forward/backward) between steps - Add +/- stepper buttons around quantity inputs for better affordance - Increase mobile tap targets to 44px and show active step label - Add "Edit" links to summary section headings for quick navigation - Add "Step X of Y" counter below progress bar - Add summaryEdit, stepLabel, stepOf l10n strings to both entry points Co-Authored-By: Claude Opus 4.6 --- assets/css/form.css | 136 ++++++++++++++++++++++++++++--- assets/js/form.js | 76 +++++++++++++---- includes/class-form-renderer.php | 7 +- includes/class-shortcode.php | 3 + templates/form-page.php | 3 + 5 files changed, 193 insertions(+), 32 deletions(-) diff --git a/assets/css/form.css b/assets/css/form.css index ba48d77..97c94d8 100644 --- a/assets/css/form.css +++ b/assets/css/form.css @@ -167,6 +167,14 @@ font-size: 0; } +/* ===== Step Counter ===== */ +.progress-counter { + text-align: center; + font-size: 0.8rem; + color: var(--umzug-text-secondary); + margin-bottom: 16px; +} + /* ===== Running Totals Bar ===== */ .running-totals { position: sticky; @@ -181,11 +189,14 @@ font-size: 0.95rem; font-weight: 500; box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1); - display: none; + transform: translateY(100%); + transition: transform 0.3s; + pointer-events: none; } .running-totals.visible { - display: block; + transform: translateY(0); + pointer-events: auto; } .running-totals-label { @@ -219,6 +230,24 @@ to { opacity: 1; transform: translateY(0); } } +@keyframes slideInForward { + from { opacity: 0; transform: translateX(30px); } + to { opacity: 1; transform: translateX(0); } +} + +@keyframes slideInBackward { + from { opacity: 0; transform: translateX(-30px); } + to { opacity: 1; transform: translateX(0); } +} + +.wizard-step.active.forward { + animation: slideInForward 0.3s ease; +} + +.wizard-step.active.backward { + animation: slideInBackward 0.3s ease; +} + .step-title { font-size: 1.5rem; font-weight: 700; @@ -394,27 +423,78 @@ gap: 12px; padding: 8px 0; border-bottom: 1px solid #f0f0f0; + opacity: 0.55; + transition: opacity 0.2s; +} + +.furniture-item.has-quantity, +.furniture-item:hover, +.furniture-item:focus-within { + opacity: 1; +} + +.furniture-item .montage-toggle { + display: none; +} + +.furniture-item.has-quantity .montage-toggle { + display: flex; } .furniture-item:last-of-type { border-bottom: none; } -.quantity-input { - width: 56px; - height: 36px; - padding: 0 8px; +.quantity-stepper { + display: inline-flex; + align-items: center; border: 1px solid var(--umzug-border); - border-radius: var(--umzug-radius-sm); + border-radius: var(--umzug-radius); + overflow: hidden; + flex-shrink: 0; +} + +.qty-btn { + width: 36px; + height: 36px; + border: none; + background: var(--umzug-bg); + font-size: 1.1rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: background var(--umzug-transition); + color: var(--umzug-text); + padding: 0; + line-height: 1; +} + +.qty-btn:hover { + background: var(--umzug-border); +} + +.qty-btn:active { + background: var(--umzug-primary-light); +} + +.quantity-stepper .quantity-input { + border: none; + border-left: 1px solid var(--umzug-border); + border-right: 1px solid var(--umzug-border); + border-radius: 0; + width: 48px; + height: 36px; + padding: 0 4px; font-size: 0.95rem; text-align: center; - flex-shrink: 0; color: var(--umzug-text); background: var(--umzug-surface); transition: border-color var(--umzug-transition), box-shadow var(--umzug-transition); } -.quantity-input:focus { +.quantity-stepper .quantity-input:focus { outline: none; border-color: var(--umzug-primary); box-shadow: 0 0 0 3px var(--umzug-primary-light); @@ -630,6 +710,19 @@ font-size: 0.8rem; } +.summary-edit { + float: right; + font-size: 0.8rem; + font-weight: 500; + color: var(--umzug-primary); + cursor: pointer; + text-decoration: none; +} + +.summary-edit:hover { + text-decoration: underline; +} + #wizard-summary .summary-grand-total { background: var(--umzug-primary); color: #fff; @@ -730,7 +823,8 @@ flex-basis: calc(100% - 80px); } - .quantity-input { + .quantity-input, + .quantity-stepper { order: 2; } @@ -769,18 +863,34 @@ font-size: 0.95rem; } + .progress-dot { + min-width: 44px; + min-height: 44px; + display: flex; + align-items: center; + justify-content: center; + } + .dot-label { display: none; } + .progress-dot.active .dot-label { + display: block; + font-size: 0.6rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .dot-number { - width: 28px; - height: 28px; + width: 36px; + height: 36px; font-size: 0.7rem; } .progress-track { - top: 14px; + top: 18px; } } diff --git a/assets/js/form.js b/assets/js/form.js index 687990f..2232214 100644 --- a/assets/js/form.js +++ b/assets/js/form.js @@ -48,14 +48,20 @@ function showStep(n) { if (n < 1 || n > TOTAL_STEPS) return; - // Hide all steps + // Determine direction + var direction = n > currentStep ? 'forward' : 'backward'; + + // Hide all steps and remove direction classes qsa('.wizard-step').forEach(function(el) { - el.classList.remove('active'); + el.classList.remove('active', 'forward', 'backward'); }); - // Show target step + // Show target step with direction var target = qs('.wizard-step[data-step="' + n + '"]'); - if (target) target.classList.add('active'); + if (target) { + target.classList.add(direction); + target.classList.add('active'); + } currentStep = n; if (n > highestStep) highestStep = n; @@ -103,6 +109,12 @@ var pct = ((highestStep - 1) / (TOTAL_STEPS - 1)) * 100; fill.style.width = pct + '%'; } + + // Update step counter + var counter = qs('#progress-counter'); + if (counter) { + counter.textContent = (l10n.stepLabel || 'Step') + ' ' + currentStep + ' ' + (l10n.stepOf || 'of') + ' ' + TOTAL_STEPS; + } } function updateNavButtons() { @@ -271,6 +283,11 @@ // ===== Summary Generation ===== + function summaryHeading(text, gotoStep) { + var editLabel = escHtml(l10n.summaryEdit || 'Edit'); + return '

' + escHtml(text) + ' ' + editLabel + '

'; + } + function generateSummary() { var container = qs('#wizard-summary'); if (!container) return; @@ -279,7 +296,7 @@ // Customer info html += '
'; - html += '

' + escHtml(l10n.summaryMovingDate || 'Moving Date') + '

'; + html += summaryHeading(l10n.summaryMovingDate || 'Moving Date', 1); var day = getFieldVal('day'); var month = getFieldVal('month'); var year = getFieldVal('year'); @@ -288,7 +305,7 @@ // Loading address html += '
'; - html += '

' + escHtml(l10n.summaryLoading || 'Loading Address') + '

'; + html += summaryHeading(l10n.summaryLoading || 'Loading Address', 1); html += summaryRow('Name', getFieldVal('bName')); html += summaryRow('Street', getFieldVal('bStrasse')); html += summaryRow('ZIP/City', getFieldVal('bort')); @@ -305,7 +322,7 @@ // Unloading address html += '
'; - html += '

' + escHtml(l10n.summaryUnloading || 'Unloading Address') + '

'; + html += summaryHeading(l10n.summaryUnloading || 'Unloading Address', 1); html += summaryRow('Name', getFieldVal('eName')); html += summaryRow('Street', getFieldVal('eStrasse')); html += summaryRow('ZIP/City', getFieldVal('eort')); @@ -322,13 +339,13 @@ // Room summaries var roomMap = [ - { key: 'wohnzimmer', name: 'Wohnzimmer' }, - { key: 'schlafzimmer', name: 'Schlafzimmer' }, - { key: 'arbeitszimmer', name: 'Arbeitszimmer' }, - { key: 'bad', name: 'Bad' }, - { key: 'kueche_esszimmer', name: 'Kueche_Esszimmer' }, - { key: 'kinderzimmer', name: 'Kinderzimmer' }, - { key: 'keller', name: 'Keller' } + { key: 'wohnzimmer', name: 'Wohnzimmer', step: 2 }, + { key: 'schlafzimmer', name: 'Schlafzimmer', step: 3 }, + { key: 'arbeitszimmer', name: 'Arbeitszimmer', step: 4 }, + { key: 'bad', name: 'Bad', step: 5 }, + { key: 'kueche_esszimmer', name: 'Kueche_Esszimmer', step: 5 }, + { key: 'kinderzimmer', name: 'Kinderzimmer', step: 6 }, + { key: 'keller', name: 'Keller', step: 7 } ]; roomMap.forEach(function(room) { @@ -337,7 +354,7 @@ var total = calculateRoomTotal(room.key); html += '
'; - html += '

' + escHtml(getRoomDisplayName(room.key)) + '

'; + html += summaryHeading(getRoomDisplayName(room.key), room.step); roomItems.forEach(function(item) { html += '
'; html += '' + escHtml(item.name) + ''; @@ -368,7 +385,7 @@ var additionalHtml = getAdditionalWorkSummary(); if (additionalHtml) { html += '
'; - html += '

' + escHtml(l10n.summaryAdditional || 'Additional Work') + '

'; + html += summaryHeading(l10n.summaryAdditional || 'Additional Work', 8); html += additionalHtml; html += '
'; } @@ -377,7 +394,7 @@ var sonstiges = getFieldVal('sonstiges'); if (sonstiges) { html += '
'; - html += '

' + escHtml(l10n.summaryOther || 'Other') + '

'; + html += summaryHeading(l10n.summaryOther || 'Other', 8); html += '

' + escHtml(sonstiges) + '

'; html += '
'; } @@ -526,15 +543,38 @@ document.addEventListener('input', function(e) { if (e.target.classList.contains('quantity-input')) { handleQuantityChange(); + var hasQty = parseGermanDecimal(e.target.value) > 0; // Toggle has-value class - if (parseGermanDecimal(e.target.value) > 0) { + if (hasQty) { e.target.classList.add('has-value'); } else { e.target.classList.remove('has-value'); } + // Toggle has-quantity on parent row for dimming/montage visibility + var row = e.target.closest('.furniture-item'); + if (row) row.classList.toggle('has-quantity', hasQty); } }); + // Stepper button click handlers + document.addEventListener('click', function(e) { + var btn = e.target.closest('.qty-btn'); + if (!btn) return; + var input = btn.parentNode.querySelector('.quantity-input'); + if (!input) return; + var val = parseGermanDecimal(input.value); + if (btn.classList.contains('qty-plus')) val++; + else if (btn.classList.contains('qty-minus') && val > 0) val--; + input.value = val > 0 ? val : ''; + input.dispatchEvent(new Event('input', { bubbles: true })); + }); + + // Summary edit link click handler + document.addEventListener('click', function(e) { + var el = e.target.closest('.summary-edit'); + if (el) showStep(parseInt(el.dataset.goto, 10)); + }); + // Clear field errors on input document.addEventListener('input', function(e) { if (e.target.classList.contains('field-error')) { diff --git a/includes/class-form-renderer.php b/includes/class-form-renderer.php index 2639d93..34505fe 100644 --- a/includes/class-form-renderer.php +++ b/includes/class-form-renderer.php @@ -134,6 +134,7 @@ class Umzugsliste_Form_Renderer {
+
- +
+ + + +
diff --git a/includes/class-shortcode.php b/includes/class-shortcode.php index c51eed1..f4e34b0 100644 --- a/includes/class-shortcode.php +++ b/includes/class-shortcode.php @@ -123,6 +123,9 @@ class Umzugsliste_Shortcode { 'grandTotalLabel' => __( 'Grand Total', 'siegel-umzugsliste' ), 'quantityLabel' => __( 'Qty', 'siegel-umzugsliste' ), 'cbmLabel' => __( 'cbm', 'siegel-umzugsliste' ), + 'summaryEdit' => __( 'Edit', 'siegel-umzugsliste' ), + 'stepLabel' => __( 'Step', 'siegel-umzugsliste' ), + 'stepOf' => __( 'of', 'siegel-umzugsliste' ), ) ); } } diff --git a/templates/form-page.php b/templates/form-page.php index 8624ab8..c4e8832 100644 --- a/templates/form-page.php +++ b/templates/form-page.php @@ -40,6 +40,9 @@ $l10n_data = array( 'grandTotalLabel' => __( 'Grand Total', 'siegel-umzugsliste' ), 'quantityLabel' => __( 'Qty', 'siegel-umzugsliste' ), 'cbmLabel' => __( 'cbm', 'siegel-umzugsliste' ), + 'summaryEdit' => __( 'Edit', 'siegel-umzugsliste' ), + 'stepLabel' => __( 'Step', 'siegel-umzugsliste' ), + 'stepOf' => __( 'of', 'siegel-umzugsliste' ), 'nonce' => wp_create_nonce( 'umzugsliste_submit' ), ); ?>