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 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 23:46:36 +09:00
parent b9ae7d707d
commit 89bd555dc1
5 changed files with 193 additions and 32 deletions

View File

@@ -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;
}
}

View File

@@ -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 '<h3>' + escHtml(text) + ' <a class="summary-edit" data-goto="' + gotoStep + '" role="button">' + editLabel + '</a></h3>';
}
function generateSummary() {
var container = qs('#wizard-summary');
if (!container) return;
@@ -279,7 +296,7 @@
// Customer info
html += '<div class="summary-section">';
html += '<h3>' + escHtml(l10n.summaryMovingDate || 'Moving Date') + '</h3>';
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 += '<div class="summary-section">';
html += '<h3>' + escHtml(l10n.summaryLoading || 'Loading Address') + '</h3>';
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 += '<div class="summary-section">';
html += '<h3>' + escHtml(l10n.summaryUnloading || 'Unloading Address') + '</h3>';
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 += '<div class="summary-section">';
html += '<h3>' + escHtml(getRoomDisplayName(room.key)) + '</h3>';
html += summaryHeading(getRoomDisplayName(room.key), room.step);
roomItems.forEach(function(item) {
html += '<div class="summary-item">';
html += '<span class="summary-item-name">' + escHtml(item.name) + '</span>';
@@ -368,7 +385,7 @@
var additionalHtml = getAdditionalWorkSummary();
if (additionalHtml) {
html += '<div class="summary-section">';
html += '<h3>' + escHtml(l10n.summaryAdditional || 'Additional Work') + '</h3>';
html += summaryHeading(l10n.summaryAdditional || 'Additional Work', 8);
html += additionalHtml;
html += '</div>';
}
@@ -377,7 +394,7 @@
var sonstiges = getFieldVal('sonstiges');
if (sonstiges) {
html += '<div class="summary-section">';
html += '<h3>' + escHtml(l10n.summaryOther || 'Other') + '</h3>';
html += summaryHeading(l10n.summaryOther || 'Other', 8);
html += '<p>' + escHtml(sonstiges) + '</p>';
html += '</div>';
}
@@ -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')) {