@bytebrand/fe-ui-core-autobahn 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/chrome/TopBar.tsx CHANGED
@@ -35,8 +35,10 @@ const R_SAVED_SEARCHES = '/my-searches';
35
35
  const R_MY_VEHICLES = '/my-vehicles/overview';
36
36
  const R_SETTINGS = '/my-settings';
37
37
  const R_MY_ORDERS = '/account/my-orders';
38
- const R_DEALER_DASHBOARD = '/account/dashboard';
38
+ /** DashboardPage branches internally on authStore.isDealer — same route serves regular users too. */
39
+ const R_DASHBOARD = '/account/dashboard';
39
40
  const R_IMAGE_SETTINGS = '/account/image-settings';
41
+ const R_KONTAKT = '/account/kontakt';
40
42
 
41
43
  /** Languages offered in the account menu (legacy supports de/en/ru). Labels are proper nouns
42
44
  * (endonyms) and stay untranslated. */
@@ -215,15 +217,20 @@ const TopBar: React.FunctionComponent<TopBarProps> = ({
215
217
  <div className="md-label-sm" style={{ padding: '6px 14px 8px', color: 'var(--md-on-surface-variant)' }}>
216
218
  {userLabel || t('autobahn:chrome.myAccount')}
217
219
  </div>
220
+ {/* dealer-main users get the same route via the dedicated "Händler-Dashboard" row below */}
221
+ {!isDealerMain &&
222
+ row('autobahn:chrome.dashboard', t('autobahn:chrome.dashboard'), () => goRoute(R_DASHBOARD), { icon: 'layout-grid' })}
218
223
  {row('autobahn:chrome.myVehicles', t('autobahn:chrome.myVehicles'), () => goRoute(R_MY_VEHICLES), { icon: 'car' })}
219
- {row('autobahn:chrome.myOrders', t('autobahn:chrome.myOrders'), () => goRoute(R_MY_ORDERS), { icon: 'package', badge: ordersCount > 0 ? ordersCount : undefined })}
224
+ {ordersCount > 0 &&
225
+ row('autobahn:chrome.myOrders', t('autobahn:chrome.myOrders'), () => goRoute(R_MY_ORDERS), { icon: 'package', badge: ordersCount })}
220
226
  {row('autobahn:chrome.savedSearches', t('autobahn:chrome.savedSearches'), () => goRoute(R_SAVED_SEARCHES), { icon: 'bell' })}
221
227
  {row('autobahn:chrome.favorites', t('autobahn:chrome.favorites'), () => goRoute(R_FAVOURITES), { icon: 'heart', badge: favoritesCount })}
222
228
  {row('autobahn:chrome.settings', t('autobahn:chrome.settings'), () => goRoute(R_SETTINGS), { icon: 'settings' })}
229
+ {row('autobahn:chrome.helpContact', t('autobahn:chrome.helpContact'), () => goRoute(R_KONTAKT), { icon: 'help-circle' })}
223
230
  {isDealerMain && (
224
231
  <>
225
232
  {divider('d-dealer')}
226
- {row('autobahn:chrome.dealerDashboard', t('autobahn:chrome.dealerDashboard'), () => goRoute(R_DEALER_DASHBOARD), { icon: 'store' })}
233
+ {row('autobahn:chrome.dealerDashboard', t('autobahn:chrome.dealerDashboard'), () => goRoute(R_DASHBOARD), { icon: 'store' })}
227
234
  {row('autobahn:chrome.imageSettings', t('autobahn:chrome.imageSettings'), () => goRoute(R_IMAGE_SETTINGS), { icon: 'image' })}
228
235
  </>
229
236
  )}
@@ -56,7 +56,7 @@
56
56
  "termSelection": "Laufzeit",
57
57
  "headerTitle": "Dein Angebot",
58
58
  "adjustRate": "Rate anpassen",
59
- "secureOffer": "Angebot sichern",
59
+ "secureOffer": "Angebot anfragen",
60
60
  "trustLine": "Unverbindlich · kostenlos · 14 Tage Rückgaberecht",
61
61
  "downPaymentLabel": "€ Anzahlung",
62
62
  "monthsLabel": "{{months}} Monat",
@@ -84,7 +84,16 @@
84
84
  "kmPerYear": "{{value}} km/Jahr",
85
85
  "perMonth": "/mtl.",
86
86
  "leasingDetails": "Leasingdetails",
87
- "financingDetails": "Finanzierungsdetails"
87
+ "financingDetails": "Finanzierungsdetails",
88
+ "personalConsultation": "Persönliche Beratung",
89
+ "personalConsultationSub": "Wir sind für Sie da",
90
+ "financingLeasingPossible": "Finanzierung & Leasing möglich",
91
+ "requestRate": "Rate anfragen",
92
+ "callCta": "Anrufen",
93
+ "finalInstallmentToggle": "Mit Schlussrate",
94
+ "addServicesToggle": "Rundum-Sorglos-Paket",
95
+ "addServicesPerMonth": "+{{value}} € / Monat",
96
+ "addServicesOnce": "+{{value}} €"
88
97
  },
89
98
  "specTable": {
90
99
  "title": "Fahrzeugdaten",
@@ -214,6 +223,7 @@
214
223
  "navJobs": "Jobs",
215
224
  "myOrders": "Meine Bestellungen",
216
225
  "myDealers": "Meine Händler",
226
+ "dashboard": "Dashboard",
217
227
  "dealerDashboard": "Händler-Dashboard",
218
228
  "imageSettings": "Bildeinstellungen"
219
229
  },
@@ -602,7 +612,10 @@
602
612
  "totalAmount": "Gesamtbetrag",
603
613
  "totalRepayment": "Gesamtrückzahlungsbetrag",
604
614
  "legalLeasing": "Unverbindliches Leasingbeispiel (Kilometerleasing). Bonität vorausgesetzt. Dieses freibleibende Angebot wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ggf. inkl. MwSt.",
605
- "legalFinancing": "Repräsentatives Berechnungsbeispiel gem. § 17a Abs. 4 PAngV, das 2/3 aller Kunden — Bonität vorausgesetzt — erhalten. Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Dieses freibleibende Angebot der Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach, wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ggf. inkl. MwSt."
615
+ "legalFinancing": "Repräsentatives Berechnungsbeispiel gem. § 17a Abs. 4 PAngV, das 2/3 aller Kunden — Bonität vorausgesetzt — erhalten. Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Dieses freibleibende Angebot der Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach, wird vermittelt durch die auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, die als ungebundener Vermittler nicht beratend tätig ist. Irrtümer vorbehalten. Preise ggf. inkl. MwSt.",
616
+ "sheetTitleFinancing": "Kredit-Rate anpassen",
617
+ "sheetTitleLeasing": "Leasing-Rate anpassen",
618
+ "sheetTitleCash": "Kaufen - Details"
606
619
  },
607
620
  "ratings": {
608
621
  "showLess": "Weniger anzeigen",
@@ -56,7 +56,7 @@
56
56
  "termSelection": "Term",
57
57
  "headerTitle": "Your offer",
58
58
  "adjustRate": "Adjust rate",
59
- "secureOffer": "Secure offer",
59
+ "secureOffer": "Request offer",
60
60
  "trustLine": "Non-binding · free · 14-day return right",
61
61
  "downPaymentLabel": "€ down payment",
62
62
  "monthsLabel": "{{months}} month",
@@ -84,7 +84,16 @@
84
84
  "kmPerYear": "{{value}} km/year",
85
85
  "perMonth": "/mo.",
86
86
  "leasingDetails": "Leasing details",
87
- "financingDetails": "Financing details"
87
+ "financingDetails": "Financing details",
88
+ "personalConsultation": "Personal advice",
89
+ "personalConsultationSub": "We're here for you",
90
+ "financingLeasingPossible": "Financing & leasing available",
91
+ "requestRate": "Request rate",
92
+ "callCta": "Call",
93
+ "finalInstallmentToggle": "With final installment",
94
+ "addServicesToggle": "All-round care package",
95
+ "addServicesPerMonth": "+€{{value}} / month",
96
+ "addServicesOnce": "+€{{value}}"
88
97
  },
89
98
  "specTable": {
90
99
  "title": "Vehicle data",
@@ -214,6 +223,7 @@
214
223
  "navJobs": "Jobs",
215
224
  "myOrders": "My orders",
216
225
  "myDealers": "My dealers",
226
+ "dashboard": "Dashboard",
217
227
  "dealerDashboard": "Dealer dashboard",
218
228
  "imageSettings": "Image settings"
219
229
  },
@@ -602,7 +612,10 @@
602
612
  "totalAmount": "Total amount",
603
613
  "totalRepayment": "Total repayment amount",
604
614
  "legalLeasing": "Non-binding leasing example (mileage leasing). Subject to creditworthiness. This non-binding offer is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, which acts as a non-tied intermediary and does not provide advice. Errors excepted. Prices incl. VAT where applicable.",
605
- "legalFinancing": "Representative calculation example pursuant to § 17a (4) PAngV, obtained by 2/3 of all customers — subject to creditworthiness. For your financing requests we will submit a credit inquiry. This non-binding offer from Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach, is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, which acts as a non-tied intermediary and does not provide advice. Errors excepted. Prices incl. VAT where applicable."
615
+ "legalFinancing": "Representative calculation example pursuant to § 17a (4) PAngV, obtained by 2/3 of all customers — subject to creditworthiness. For your financing requests we will submit a credit inquiry. This non-binding offer from Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach, is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, which acts as a non-tied intermediary and does not provide advice. Errors excepted. Prices incl. VAT where applicable.",
616
+ "sheetTitleFinancing": "Adjust credit rate",
617
+ "sheetTitleLeasing": "Adjust leasing rate",
618
+ "sheetTitleCash": "Purchase - Details"
606
619
  },
607
620
  "ratings": {
608
621
  "showLess": "Show less",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.13",
3
+ "version": "1.0.15",
4
4
  "description": "auto.de md3 (Autobahn) design system — UI primitives, cards, chrome, VDP modules, theme CSS and locales. Ships raw TypeScript source (like fe-ui-core before it); each consumer's bundler compiles it.",
5
5
  "main": "index.ts",
6
6
  "license": "UNLICENSED",
@@ -158,8 +158,11 @@ export interface FinanceCalculatorProps {
158
158
  onSecure?: () => void;
159
159
  /** secondary CTA click (phone). */
160
160
  onCall?: () => void;
161
- /** open the Finanzierungs-/Leasingdetails popup (full breakdown + legal). Omit to hide the link. */
161
+ /** open the Finanzierungs-/Leasingdetails popup (full breakdown + legal, expanded). Omit to hide the link. */
162
162
  onShowDetails?: () => void;
163
+ /** mobile only: open the "Rate anpassen" / "Details anzeigen" sheet (collapsed/default view).
164
+ * Omit to hide the trigger row (falls back to the desktop inline expander). */
165
+ onOpenAdjust?: () => void;
163
166
  /** mode segment changed. */
164
167
  onModeChange: (mode: FinanceMode) => void;
165
168
  /** down-payment slider changed. */
@@ -204,7 +207,7 @@ const DetailsLink: React.FunctionComponent<{ label: string; onClick: () => void
204
207
  );
205
208
 
206
209
  /** "Rate anpassen" / "Details anzeigen" expander toggle button. */
207
- const expanderBtnStyle: React.CSSProperties = {
210
+ export const expanderBtnStyle: React.CSSProperties = {
208
211
  width: '100%',
209
212
  background: 'none',
210
213
  border: 0,
@@ -243,6 +246,7 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
243
246
  onSecure,
244
247
  onCall,
245
248
  onShowDetails,
249
+ onOpenAdjust,
246
250
  onModeChange,
247
251
  onDownChange,
248
252
  onTermChange,
@@ -364,100 +368,157 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
364
368
  </div>
365
369
  </div>
366
370
 
367
- {/* RATE EDIT (Rate anpassen) — between the rate and the request CTA */}
368
- <div className="fin-adjust">
369
- <button onClick={() => setAdjust((a) => !a)} style={expanderBtnStyle}>
370
- <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
371
- <Icon name={isCash ? 'list' : 'sliders-horizontal'} size={16} color="var(--md-primary)" />
372
- {isCash ? t('autobahn:finance.detailsShow') : t('autobahn:finance.adjustRate')}
373
- </span>
374
- <Icon name={adjust ? 'chevron-up' : 'chevron-down'} size={18} color="var(--md-primary)" />
375
- </button>
376
- {adjust &&
377
- (isCash ? (
378
- <div style={{ marginTop: 4 }}>
379
- <CalcRow label={t('autobahn:finance.paymentLabel')}>{t('autobahn:finance.paymentValue')}</CalcRow>
380
- <CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
381
- <CalcRow label={t('autobahn:finance.deliveryLabel')} last>
382
- {t('autobahn:finance.deliveryValue')}
383
- </CalcRow>
384
- </div>
385
- ) : (
386
- <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 8 }}>
387
- <div>
388
- <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
389
- <span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
390
- <span className="num" style={{ fontWeight: 700 }}>
391
- {eur(down)} €
392
- </span>
393
- </div>
394
- <Slider
395
- min={downMin}
396
- max={downMax}
397
- step={downStep}
398
- value={down}
399
- onChange={onDownChange}
400
- fmt={(v) => `${eur(Number(v))} €`}
401
- />
371
+ {/* ============================== DESKTOP (≥601px) — unchanged ============================== */}
372
+ <div className="hide-mobile">
373
+ {/* RATE EDIT (Rate anpassen) — between the rate and the request CTA */}
374
+ <div className="fin-adjust">
375
+ <button onClick={() => setAdjust((a) => !a)} style={expanderBtnStyle}>
376
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
377
+ <Icon name={isCash ? 'list' : 'sliders-horizontal'} size={16} color="var(--md-primary)" />
378
+ {isCash ? t('autobahn:finance.detailsShow') : t('autobahn:finance.adjustRate')}
379
+ </span>
380
+ <Icon name={adjust ? 'chevron-up' : 'chevron-down'} size={18} color="var(--md-primary)" />
381
+ </button>
382
+ {adjust &&
383
+ (isCash ? (
384
+ <div style={{ marginTop: 4 }}>
385
+ <CalcRow label={t('autobahn:finance.paymentLabel')}>{t('autobahn:finance.paymentValue')}</CalcRow>
386
+ <CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
387
+ <CalcRow label={t('autobahn:finance.deliveryLabel')} last>
388
+ {t('autobahn:finance.deliveryValue')}
389
+ </CalcRow>
402
390
  </div>
403
- {annualMileage != null && onAnnualMileageChange && (
391
+ ) : (
392
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginTop: 8 }}>
404
393
  <div>
405
394
  <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
406
- <span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
395
+ <span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
407
396
  <span className="num" style={{ fontWeight: 700 }}>
408
- {eur(annualMileage)} km
397
+ {eur(down)} €
409
398
  </span>
410
399
  </div>
411
400
  <Slider
412
- min={annualMileageMin}
413
- max={annualMileageMax}
414
- step={annualMileageStep}
415
- value={annualMileage}
416
- onChange={onAnnualMileageChange}
417
- fmt={(v) => `${eur(Number(v))} km`}
401
+ min={downMin}
402
+ max={downMax}
403
+ step={downStep}
404
+ value={down}
405
+ onChange={onDownChange}
406
+ fmt={(v) => `${eur(Number(v))} €`}
418
407
  />
419
408
  </div>
420
- )}
421
- <div>
422
- <div className="md-title-sm" style={{ marginBottom: 8 }}>
423
- {t('autobahn:finance.termSelection')}
409
+ {annualMileage != null && onAnnualMileageChange && (
410
+ <div>
411
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
412
+ <span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
413
+ <span className="num" style={{ fontWeight: 700 }}>
414
+ {eur(annualMileage)} km
415
+ </span>
416
+ </div>
417
+ <Slider
418
+ min={annualMileageMin}
419
+ max={annualMileageMax}
420
+ step={annualMileageStep}
421
+ value={annualMileage}
422
+ onChange={onAnnualMileageChange}
423
+ fmt={(v) => `${eur(Number(v))} km`}
424
+ />
425
+ </div>
426
+ )}
427
+ <div>
428
+ <div className="md-title-sm" style={{ marginBottom: 8 }}>
429
+ {t('autobahn:finance.termSelection')}
430
+ </div>
431
+ <TermChips options={terms} value={term} onChange={onTermChange} t={t} />
432
+ </div>
433
+ <div>
434
+ <CalcRow label={t('autobahn:finance.purchasePrice')}>{eur(price)} €</CalcRow>
435
+ <CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
436
+ <CalcRow label={t('autobahn:finance.deliveryLabel')} last>
437
+ {t('autobahn:finance.deliveryValue')}
438
+ </CalcRow>
424
439
  </div>
425
- <TermChips options={terms} value={term} onChange={onTermChange} t={t} />
426
- </div>
427
- <div>
428
- <CalcRow label={t('autobahn:finance.purchasePrice')}>{eur(price)} €</CalcRow>
429
- <CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
430
- <CalcRow label={t('autobahn:finance.deliveryLabel')} last>
431
- {t('autobahn:finance.deliveryValue')}
432
- </CalcRow>
433
440
  </div>
434
- </div>
435
- ))}
436
- </div>
441
+ ))}
442
+ </div>
437
443
 
438
- {/* REQUEST CTA */}
439
- <Button variant="filled" block size="lg" icon="shield-check" style={{ marginTop: 14 }} onClick={onSecure}>
440
- {t('autobahn:finance.secureOffer')}
441
- </Button>
442
- {/* phone CTA — touch only (hidden on desktop via .fin-call) */}
443
- <div className="fin-call">
444
- <Button variant="outlined" block icon="phone" onClick={onCall}>
445
- {phone}
444
+ {/* REQUEST CTA */}
445
+ <Button variant="filled" block size="lg" icon="shield-check" style={{ marginTop: 14 }} onClick={onSecure}>
446
+ {t('autobahn:finance.secureOffer')}
446
447
  </Button>
448
+ {/* phone CTA — touch only (hidden on desktop via .fin-call) */}
449
+ <div className="fin-call">
450
+ <Button variant="outlined" block icon="phone" onClick={onCall}>
451
+ {phone}
452
+ </Button>
453
+ </div>
454
+
455
+ {/* auto.de-ID in front of the Finanzierungs-/Leasingdetails link — one row, under everything */}
456
+ {(autoDeId || (!isCash && onShowDetails)) && (
457
+ <div className="fin-info">
458
+ {autoDeId && <span className="fin-id">auto.de-ID {autoDeId}</span>}
459
+ {!isCash && onShowDetails && (
460
+ <DetailsLink
461
+ label={mode === 'leasing' ? t('autobahn:finance.leasingDetails') : t('autobahn:finance.financingDetails')}
462
+ onClick={onShowDetails}
463
+ />
464
+ )}
465
+ </div>
466
+ )}
447
467
  </div>
448
468
 
449
- {/* auto.de-ID in front of the Finanzierungs-/Leasingdetails link — one row, under everything */}
450
- {(autoDeId || (!isCash && onShowDetails)) && (
469
+ {/* ============================== MOBILE (≤600px) — new layout ============================== */}
470
+ <div className="show-mobile" style={{ flexDirection: 'column' }}>
471
+ {/* side-by-side CTAs */}
472
+ <div style={{ display: 'flex', gap: 10, marginTop: 14 }}>
473
+ <Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
474
+ {t('autobahn:finance.secureOffer')}
475
+ </Button>
476
+ <Button variant="outlined" size="lg" icon="phone" style={{ flex: 1 }} onClick={onCall}>
477
+ {t('autobahn:finance.callCta')}
478
+ </Button>
479
+ </div>
480
+
481
+ {/* Persönliche Beratung (left) · Finanzierungs-/Leasingdetails or hint (right) */}
451
482
  <div className="fin-info">
452
- {autoDeId && <span className="fin-id">auto.de-ID {autoDeId}</span>}
453
- {!isCash && onShowDetails && (
454
- <DetailsLink
455
- label={mode === 'leasing' ? t('autobahn:finance.leasingDetails') : t('autobahn:finance.financingDetails')}
456
- onClick={onShowDetails}
457
- />
483
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
484
+ <Icon name="user" size={18} color="var(--md-on-surface-variant)" style={{ flex: 'none' }} />
485
+ <div style={{ minWidth: 0 }}>
486
+ <div className="md-label-sm" style={{ fontWeight: 700, color: 'var(--md-on-surface)' }}>
487
+ {t('autobahn:finance.personalConsultation')}
488
+ </div>
489
+ <div className="md-label-sm">{t('autobahn:finance.personalConsultationSub')}</div>
490
+ </div>
491
+ </div>
492
+ {isCash ? (
493
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'var(--md-on-surface-variant)' }}>
494
+ <Icon name="info" size={15} />
495
+ <span className="md-label-sm">{t('autobahn:finance.financingLeasingPossible')}</span>
496
+ </div>
497
+ ) : (
498
+ onShowDetails && (
499
+ <DetailsLink
500
+ label={mode === 'leasing' ? t('autobahn:finance.leasingDetails') : t('autobahn:finance.financingDetails')}
501
+ onClick={onShowDetails}
502
+ />
503
+ )
458
504
  )}
459
505
  </div>
460
- )}
506
+
507
+ {/* "Rate anpassen" / "Details anzeigen" — opens the full bottom sheet (not an inline expander) */}
508
+ {onOpenAdjust && (
509
+ <button
510
+ type="button"
511
+ onClick={onOpenAdjust}
512
+ style={{ ...expanderBtnStyle, borderTop: '1px solid var(--md-outline-variant)', marginTop: 4, padding: '14px 0 2px' }}
513
+ >
514
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
515
+ <Icon name={isCash ? 'list' : 'sliders-horizontal'} size={16} color="var(--md-primary)" />
516
+ {isCash ? t('autobahn:finance.detailsShow') : t('autobahn:finance.adjustRate')}
517
+ </span>
518
+ <Icon name="chevron-right" size={18} color="var(--md-primary)" />
519
+ </button>
520
+ )}
521
+ </div>
461
522
  </div>
462
523
  );
463
524
  };
@@ -24,10 +24,10 @@
24
24
  import React, { useState } from 'react';
25
25
  import { TranslationFunction } from '../types';
26
26
 
27
- import { Button, Icon, Spec, StatChip, Stars } from '../ui';
28
- import { CO2_CLASSES, CO2_COLORS } from '../constants';
27
+ import { Button, Icon, Spec, StatChip, Stars, Switch } from '../ui';
28
+ import { CO2_CLASSES, CO2_COLORS, TIER_LABELS, TIER_KEYS, TIER_COLORS } from '../constants';
29
29
  import { defaultVdpSpecs, EquipmentGroups, VehicleDetail, VdpSpec, VdpReview } from './types';
30
- import { Slider, TermChips } from './FinanceCalculator';
30
+ import { CalcRow, expanderBtnStyle, FinanceMode, Slider, TermChips } from './FinanceCalculator';
31
31
 
32
32
  const eur = (n: number): string => n.toLocaleString('de-DE');
33
33
 
@@ -934,6 +934,29 @@ export const InspectionBlock: React.FunctionComponent<InspectionBlockProps> = ({
934
934
  // Finanzierungs-/Leasingdetails popup (desktop modal / mobile bottom-sheet)
935
935
  // ---------------------------------------------------------------------------
936
936
 
937
+ /** full-width label(+optional trailing price) / Switch row — Switch itself is inline-flex
938
+ * (sized to its own content), so the space-between has to live on this wrapper, not on
939
+ * Switch's `label` prop (which renders track-then-label, not a full-width justified row). */
940
+ const ToggleRow: React.FunctionComponent<{
941
+ label: string;
942
+ price?: string;
943
+ checked: boolean;
944
+ disabled?: boolean;
945
+ onChange: (checked: boolean) => void;
946
+ }> = ({ label, price, checked, disabled, onChange }) => (
947
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
948
+ <span className="md-title-sm">{label}</span>
949
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
950
+ {price && (
951
+ <span className="num" style={{ fontWeight: 700, fontSize: 13.5 }}>
952
+ {price}
953
+ </span>
954
+ )}
955
+ <Switch checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} ariaLabel={label} />
956
+ </div>
957
+ </div>
958
+ );
959
+
937
960
  export interface FinanceDetailsModalProps {
938
961
  /** open flag (controlled by the page). */
939
962
  open: boolean;
@@ -971,6 +994,25 @@ export interface FinanceDetailsModalProps {
971
994
  onTermChange?: (term: number) => void;
972
995
  onAnnualMileageChange?: (km: number) => void;
973
996
  onClose: () => void;
997
+ /** mobile only — start with the Finanzierungs-/Leasingdetails breakdown already expanded
998
+ * (opened directly via that link, skipping the "Rate anpassen" collapsed default view). */
999
+ initialShowBreakdown?: boolean;
1000
+ /** mobile only — price-rating label, drives the eval pill/bar (same as the collapsed card). */
1001
+ ratingLabel?: string;
1002
+ /** mobile only — mode tabs inside the sheet (mirrors the collapsed card's tabs). */
1003
+ onModeChange?: (mode: FinanceMode) => void;
1004
+ /** mobile only — primary "Rate anfragen"/"Angebot anfragen" CTA. */
1005
+ onSecure?: () => void;
1006
+ /** mobile only — secondary call CTA. */
1007
+ onCall?: () => void;
1008
+ /** mobile only — "Mit Schlussrate" toggle (financing only; leasing has no balloon concept). */
1009
+ withFinalInstallment?: boolean;
1010
+ onToggleFinalInstallment?: (checked: boolean) => void;
1011
+ /** mobile only — "Rundum-Sorglos-Paket" toggle (all modes; forced-on + disabled for cash). */
1012
+ withAddServices?: boolean;
1013
+ addServicesCost?: number;
1014
+ addServicesDisabled?: boolean;
1015
+ onToggleAddServices?: (checked: boolean) => void;
974
1016
  /** i18n function (drilled from the page). */
975
1017
  t: TranslationFunction;
976
1018
  }
@@ -1008,11 +1050,41 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1008
1050
  onTermChange,
1009
1051
  onAnnualMileageChange,
1010
1052
  onClose,
1053
+ initialShowBreakdown,
1054
+ ratingLabel,
1055
+ onModeChange,
1056
+ onSecure,
1057
+ onCall,
1058
+ withFinalInstallment,
1059
+ onToggleFinalInstallment,
1060
+ withAddServices,
1061
+ addServicesCost,
1062
+ addServicesDisabled,
1063
+ onToggleAddServices,
1011
1064
  t,
1012
1065
  }) => {
1066
+ // mobile-only: the Finanzierungs-/Leasingdetails breakdown starts collapsed (opened via "Rate
1067
+ // anpassen") or expanded (opened directly via the details link) — reset on every open, since
1068
+ // this component stays mounted (the page always renders it, gated by `open`, not unmounted).
1069
+ const [showBreakdown, setShowBreakdown] = React.useState(!!initialShowBreakdown);
1070
+ React.useEffect(() => {
1071
+ if (open) setShowBreakdown(!!initialShowBreakdown);
1072
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1073
+ }, [open, initialShowBreakdown]);
1074
+
1013
1075
  if (!open) return null;
1014
1076
  const isLease = mode === 'leasing';
1077
+ const isCash = mode === 'cash';
1015
1078
  const title = isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails');
1079
+ const mobileTitle = isCash
1080
+ ? t('autobahn:modal.sheetTitleCash')
1081
+ : isLease
1082
+ ? t('autobahn:modal.sheetTitleLeasing')
1083
+ : t('autobahn:modal.sheetTitleFinancing');
1084
+ // price-evaluation tier — identical derivation to the collapsed card (FinanceCalculator).
1085
+ const tierIdx = TIER_LABELS.indexOf(ratingLabel as (typeof TIER_LABELS)[number]);
1086
+ const evalTier = tierIdx >= 0 ? tierIdx : 1;
1087
+ const evalTierLabel = t('autobahn:tier.' + TIER_KEYS[evalTier]);
1016
1088
  // adjustable only for fin/leasing, and only when the page supplies the change handlers + bounds.
1017
1089
  const adjustable = mode !== 'cash' && !!onDownChange && downMin != null && downMax != null && termMin != null && termMax != null;
1018
1090
  // discrete term chip values (supplied periods, else derived from the bounds).
@@ -1041,89 +1113,274 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1041
1113
  [isLease ? t('autobahn:modal.leasingRate') : t('autobahn:modal.monthlyRate'), t('autobahn:modal.ratePerMonth', { amount: eur(monthly) })],
1042
1114
  ...(grossAmount != null && grossAmount > 0 ? ([[isLease ? t('autobahn:modal.totalAmount') : t('autobahn:modal.totalRepayment'), `${eur(grossAmount)} €`]] as Array<[string, string]>) : []),
1043
1115
  ];
1116
+ const breakdownRows = rows.map(([l, v], i) => (
1117
+ <div
1118
+ key={l}
1119
+ style={{
1120
+ display: 'flex',
1121
+ justifyContent: 'space-between',
1122
+ gap: 12,
1123
+ padding: '9px 0',
1124
+ borderBottom: i < rows.length - 1 ? '1px solid var(--md-outline-variant)' : 0,
1125
+ }}
1126
+ >
1127
+ <span className="md-label-sm">{l}</span>
1128
+ <span className="num" style={{ fontSize: 13.5, fontWeight: 600 }}>
1129
+ {v}
1130
+ </span>
1131
+ </div>
1132
+ ));
1133
+
1044
1134
  return (
1045
1135
  <div className="fin-modal-scrim" onClick={onClose}>
1046
1136
  <div className="fin-modal" role="dialog" aria-modal="true" aria-label={title} onClick={(e) => e.stopPropagation()}>
1047
1137
  <div className="fin-modal-head">
1048
- <h2 className="md-headline-sm" style={{ margin: 0 }}>
1138
+ <h2 className="md-headline-sm hide-mobile" style={{ margin: 0 }}>
1049
1139
  {title}
1050
1140
  </h2>
1141
+ <h2 className="md-headline-sm show-mobile" style={{ margin: 0 }}>
1142
+ {mobileTitle}
1143
+ </h2>
1051
1144
  <button className="icon-btn" onClick={onClose} aria-label={t('autobahn:modal.close')}>
1052
1145
  <Icon name="x" size={20} />
1053
1146
  </button>
1054
1147
  </div>
1055
1148
  <div className="fin-modal-body">
1056
- <div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 14 }}>
1057
- <span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
1058
- {eur(monthly)} €
1059
- </span>
1060
- <span className="md-label-sm">{t('autobahn:finance.perMonth')}</span>
1061
- </div>
1062
- {/* adjustment sliders — change a value and every figure below recomputes live (via the page) */}
1063
- {adjustable && (
1064
- <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginBottom: 18 }}>
1065
- <div>
1066
- <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
1067
- <span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
1068
- <span className="num" style={{ fontWeight: 700 }}>
1069
- {eur(down)} €
1070
- </span>
1071
- </div>
1072
- <Slider
1073
- min={downMin as number}
1074
- max={downMax as number}
1075
- step={downStep}
1076
- value={down}
1077
- onChange={onDownChange as (n: number) => void}
1078
- fmt={(v) => `${eur(Number(v))} €`}
1079
- />
1080
- </div>
1081
- {isLease && annualMileage != null && onAnnualMileageChange && (
1149
+ {/* ============================== DESKTOP (≥601px) — unchanged ============================== */}
1150
+ <div className="hide-mobile">
1151
+ <div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 14 }}>
1152
+ <span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
1153
+ {eur(monthly)} €
1154
+ </span>
1155
+ <span className="md-label-sm">{t('autobahn:finance.perMonth')}</span>
1156
+ </div>
1157
+ {/* adjustment sliders — change a value and every figure below recomputes live (via the page) */}
1158
+ {adjustable && (
1159
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginBottom: 18 }}>
1082
1160
  <div>
1083
1161
  <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
1084
- <span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
1162
+ <span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
1085
1163
  <span className="num" style={{ fontWeight: 700 }}>
1086
- {eur(annualMileage)} km
1164
+ {eur(down)} €
1087
1165
  </span>
1088
1166
  </div>
1089
1167
  <Slider
1090
- min={annualMileageMin}
1091
- max={annualMileageMax}
1092
- step={annualMileageStep}
1093
- value={annualMileage}
1094
- onChange={onAnnualMileageChange}
1095
- fmt={(v) => `${eur(Number(v))} km`}
1168
+ min={downMin as number}
1169
+ max={downMax as number}
1170
+ step={downStep}
1171
+ value={down}
1172
+ onChange={onDownChange as (n: number) => void}
1173
+ fmt={(v) => `${eur(Number(v))} €`}
1096
1174
  />
1097
1175
  </div>
1098
- )}
1099
- <div>
1100
- <div className="md-title-sm" style={{ marginBottom: 8 }}>
1101
- {t('autobahn:finance.termSelection')}
1176
+ {isLease && annualMileage != null && onAnnualMileageChange && (
1177
+ <div>
1178
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
1179
+ <span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
1180
+ <span className="num" style={{ fontWeight: 700 }}>
1181
+ {eur(annualMileage)} km
1182
+ </span>
1183
+ </div>
1184
+ <Slider
1185
+ min={annualMileageMin}
1186
+ max={annualMileageMax}
1187
+ step={annualMileageStep}
1188
+ value={annualMileage}
1189
+ onChange={onAnnualMileageChange}
1190
+ fmt={(v) => `${eur(Number(v))} km`}
1191
+ />
1192
+ </div>
1193
+ )}
1194
+ <div>
1195
+ <div className="md-title-sm" style={{ marginBottom: 8 }}>
1196
+ {t('autobahn:finance.termSelection')}
1197
+ </div>
1198
+ <TermChips options={termVals} value={term} onChange={onTermChange as (n: number) => void} t={t} />
1102
1199
  </div>
1103
- <TermChips options={termVals} value={term} onChange={onTermChange as (n: number) => void} t={t} />
1200
+ </div>
1201
+ )}
1202
+ {breakdownRows}
1203
+ <p className="md-label-sm" style={{ marginTop: 16, lineHeight: 1.55 }}>
1204
+ {isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
1205
+ </p>
1206
+ </div>
1207
+
1208
+ {/* ============================== MOBILE (≤600px) — full "Rate anpassen" sheet ============================== */}
1209
+ <div className="show-mobile" style={{ flexDirection: 'column', gap: 18 }}>
1210
+ {onModeChange && (
1211
+ <div className="fin-tabs" role="tablist">
1212
+ {(
1213
+ [
1214
+ ['fin', 'autobahn:finance.mode.financing'],
1215
+ ['leasing', 'autobahn:finance.mode.leasing'],
1216
+ ['cash', 'autobahn:finance.mode.cash'],
1217
+ ] as Array<[FinanceMode, string]>
1218
+ ).map(([k, labelKey]) => (
1219
+ <button
1220
+ key={k}
1221
+ type="button"
1222
+ role="tab"
1223
+ aria-selected={mode === k}
1224
+ className={'fin-tab' + (mode === k ? ' on' : '')}
1225
+ onClick={() => onModeChange(k)}
1226
+ >
1227
+ {t(labelKey)}
1228
+ </button>
1229
+ ))}
1230
+ </div>
1231
+ )}
1232
+
1233
+ <div className="fin-price-grid">
1234
+ <div className="fin-rate-main">
1235
+ <div style={{ display: 'flex', alignItems: 'baseline', gap: 5 }}>
1236
+ <span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
1237
+ {isCash ? eur(price) : eur(Math.round(monthly))}
1238
+ </span>
1239
+ <span className="md-body-sm" style={{ fontWeight: 600 }}>
1240
+ {isCash ? '€' : `€ ${t('autobahn:finance.perMonth')}`}
1241
+ </span>
1242
+ </div>
1243
+ </div>
1244
+ <div>
1245
+ {ratingLabel && (
1246
+ <span
1247
+ className="fin-eval-pill"
1248
+ style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
1249
+ >
1250
+ <Icon
1251
+ name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
1252
+ size={14}
1253
+ stroke={2.6}
1254
+ color={TIER_COLORS[evalTier]}
1255
+ />
1256
+ {evalTierLabel}
1257
+ </span>
1258
+ )}
1259
+ </div>
1260
+ <div />
1261
+ <div>
1262
+ {ratingLabel && (
1263
+ <div className="fin-eval-bar">
1264
+ <div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
1265
+ </div>
1266
+ )}
1104
1267
  </div>
1105
1268
  </div>
1106
- )}
1107
- {rows.map(([l, v], i) => (
1108
- <div
1109
- key={l}
1110
- style={{
1111
- display: 'flex',
1112
- justifyContent: 'space-between',
1113
- gap: 12,
1114
- padding: '9px 0',
1115
- borderBottom: i < rows.length - 1 ? '1px solid var(--md-outline-variant)' : 0,
1116
- }}
1117
- >
1118
- <span className="md-label-sm">{l}</span>
1119
- <span className="num" style={{ fontSize: 13.5, fontWeight: 600 }}>
1120
- {v}
1121
- </span>
1122
- </div>
1123
- ))}
1124
- <p className="md-label-sm" style={{ marginTop: 16, lineHeight: 1.55 }}>
1125
- {isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
1126
- </p>
1269
+
1270
+ {isCash ? (
1271
+ <>
1272
+ <div>
1273
+ <CalcRow label={t('autobahn:finance.paymentLabel')}>{t('autobahn:finance.paymentValue')}</CalcRow>
1274
+ <CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
1275
+ <CalcRow label={t('autobahn:finance.deliveryLabel')} last>
1276
+ {t('autobahn:finance.deliveryValue')}
1277
+ </CalcRow>
1278
+ </div>
1279
+ {addServicesCost != null && (
1280
+ <ToggleRow
1281
+ label={t('autobahn:finance.addServicesToggle')}
1282
+ price={t('autobahn:finance.addServicesOnce', { value: eur(addServicesCost) })}
1283
+ checked={!!withAddServices}
1284
+ disabled={addServicesDisabled}
1285
+ onChange={(checked) => onToggleAddServices && onToggleAddServices(checked)}
1286
+ />
1287
+ )}
1288
+ <div style={{ display: 'flex', gap: 10 }}>
1289
+ <Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
1290
+ {t('autobahn:finance.secureOffer')}
1291
+ </Button>
1292
+ <Button variant="outlined" size="lg" icon="phone" style={{ width: 52, flex: 'none' }} onClick={onCall} />
1293
+ </div>
1294
+ </>
1295
+ ) : (
1296
+ <>
1297
+ {adjustable && (
1298
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
1299
+ <div>
1300
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
1301
+ <span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
1302
+ <span className="num" style={{ fontWeight: 700 }}>
1303
+ {eur(down)} €
1304
+ </span>
1305
+ </div>
1306
+ <Slider
1307
+ min={downMin as number}
1308
+ max={downMax as number}
1309
+ step={downStep}
1310
+ value={down}
1311
+ onChange={onDownChange as (n: number) => void}
1312
+ fmt={(v) => `${eur(Number(v))} €`}
1313
+ />
1314
+ </div>
1315
+ {isLease && annualMileage != null && onAnnualMileageChange && (
1316
+ <div>
1317
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
1318
+ <span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
1319
+ <span className="num" style={{ fontWeight: 700 }}>
1320
+ {eur(annualMileage)} km
1321
+ </span>
1322
+ </div>
1323
+ <Slider
1324
+ min={annualMileageMin}
1325
+ max={annualMileageMax}
1326
+ step={annualMileageStep}
1327
+ value={annualMileage}
1328
+ onChange={onAnnualMileageChange}
1329
+ fmt={(v) => `${eur(Number(v))} km`}
1330
+ />
1331
+ </div>
1332
+ )}
1333
+ <div>
1334
+ <div className="md-title-sm" style={{ marginBottom: 8 }}>
1335
+ {t('autobahn:finance.termSelection')}
1336
+ </div>
1337
+ <TermChips options={termVals} value={term} onChange={onTermChange as (n: number) => void} t={t} />
1338
+ </div>
1339
+ </div>
1340
+ )}
1341
+
1342
+ {!isLease && onToggleFinalInstallment && (
1343
+ <ToggleRow
1344
+ label={t('autobahn:finance.finalInstallmentToggle')}
1345
+ checked={!!withFinalInstallment}
1346
+ onChange={onToggleFinalInstallment}
1347
+ />
1348
+ )}
1349
+ {addServicesCost != null && onToggleAddServices && (
1350
+ <ToggleRow
1351
+ label={t('autobahn:finance.addServicesToggle')}
1352
+ price={t('autobahn:finance.addServicesPerMonth', { value: eur(addServicesCost) })}
1353
+ checked={!!withAddServices}
1354
+ disabled={addServicesDisabled}
1355
+ onChange={onToggleAddServices}
1356
+ />
1357
+ )}
1358
+
1359
+ <div style={{ display: 'flex', gap: 10 }}>
1360
+ <Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
1361
+ {t('autobahn:finance.requestRate')}
1362
+ </Button>
1363
+ <Button variant="outlined" size="lg" icon="phone" style={{ width: 52, flex: 'none' }} onClick={onCall} />
1364
+ </div>
1365
+
1366
+ <button type="button" onClick={() => setShowBreakdown((v) => !v)} style={expanderBtnStyle}>
1367
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
1368
+ <Icon name="info" size={16} color="var(--md-primary)" />
1369
+ {isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails')}
1370
+ </span>
1371
+ <Icon name={showBreakdown ? 'chevron-up' : 'chevron-down'} size={18} color="var(--md-primary)" />
1372
+ </button>
1373
+ {showBreakdown && (
1374
+ <div>
1375
+ {breakdownRows}
1376
+ <p className="md-label-sm" style={{ marginTop: 16, lineHeight: 1.55 }}>
1377
+ {isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
1378
+ </p>
1379
+ </div>
1380
+ )}
1381
+ </>
1382
+ )}
1383
+ </div>
1127
1384
  </div>
1128
1385
  </div>
1129
1386
  </div>