@bytebrand/fe-ui-core-autobahn 1.0.14 → 1.0.16

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.
@@ -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",
@@ -603,7 +612,10 @@
603
612
  "totalAmount": "Gesamtbetrag",
604
613
  "totalRepayment": "Gesamtrückzahlungsbetrag",
605
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.",
606
- "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"
607
619
  },
608
620
  "ratings": {
609
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",
@@ -603,7 +612,10 @@
603
612
  "totalAmount": "Total amount",
604
613
  "totalRepayment": "Total repayment amount",
605
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.",
606
- "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"
607
619
  },
608
620
  "ratings": {
609
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.14",
3
+ "version": "1.0.16",
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",
package/theme/md3.css CHANGED
@@ -155,6 +155,12 @@
155
155
  NOT the 38px tappable filter-chip the base .chip is. */
156
156
  .md3 .listcard-chips .chip { height: 24px; padding: 0 10px; font-size: 11.5px; border-radius: 8px; cursor: default; }
157
157
  .md3 .listcard-chips .chip:hover { background: var(--md-surface); border-color: var(--md-outline-variant); }
158
+ /* VDP trust row (Sofort verfügbar/views/favorites/Geprüft) — same compact non-interactive
159
+ sizing as the listcard equipment tags, standalone (not nested under .listcard-chips). */
160
+ .md3 .vdp-trust-chip { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px;
161
+ border-radius: 8px; font-size: 11.5px; font-weight: 500; line-height: 1; cursor: default; white-space: nowrap;
162
+ background: var(--md-surface); color: var(--md-on-surface-variant); border: 1px solid var(--md-outline-variant); }
163
+ .md3 .vdp-trust-chip .lucide { width: 14px; height: 14px; flex: none; }
158
164
 
159
165
  /* status / rating chip (no CTA) */
160
166
  .md3 .statchip { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 10px;
@@ -1283,7 +1289,14 @@
1283
1289
  .md3 .srp-rail { display: none; } /* rail is desktop-only; ≤980 uses the filter sheet */
1284
1290
  .md3 .srp-grid { grid-template-columns: 1fr; }
1285
1291
  .md3 .vdp-grid { display: flex; flex-direction: column; }
1286
- .md3 .vdp-gallery { order: 1; } .md3 .vdp-aside { order: 2; } .md3 .vdp-details { order: 3; }
1292
+ .md3 .vdp-gallery { order: 1; }
1293
+ /* mobile-only "first screen": identity + trust row BEFORE the finance card, highlights/
1294
+ condition tiles moved AFTER it (both split out of the desktop-only VdpHeaderCard,
1295
+ which stays bundled inside .vdp-gallery, hidden on mobile via hide-mobile). */
1296
+ .md3 .vdp-header-summary { order: 2; padding: 0 4px; margin-bottom: 4px; }
1297
+ .md3 .vdp-aside { order: 3; }
1298
+ .md3 .vdp-header-extras { order: 4; margin-top: 16px; }
1299
+ .md3 .vdp-details { order: 5; }
1287
1300
  .md3 .vdp-aside { position: static; }
1288
1301
  .md3 .price-card { position: static; }
1289
1302
  .md3 .fin-call { display: block; } /* tap-to-call restored on touch */
@@ -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
 
@@ -51,14 +51,10 @@ export interface VdpHeaderCardProps {
51
51
  t: TranslationFunction;
52
52
  }
53
53
 
54
- /**
55
- * One container holding the entire vehicle identity: headline + trim/power, the
56
- * dot-separated fact line, the hand-picked highlight chips, and the condition
57
- * (zustand) tiles footer. The gallery itself is composed by the page (W3b) — this
58
- * is just the textual header card that sits beneath it.
59
- */
60
- export const VdpHeaderCard: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
61
- const { condition } = vehicle;
54
+ /** headline + trim/power + rating pill, then the dot-separated fact line — the vehicle's
55
+ * identity block. Split out so the mobile-only layout can show this BEFORE the finance
56
+ * card (see VdpTrustRow) while the highlights/condition tiles move after it. */
57
+ export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
62
58
  const factLine: ReadonlyArray<readonly [string, string]> = [
63
59
  ['gauge', fmtKm(vehicle.km)],
64
60
  ['calendar', vehicle.reg],
@@ -66,15 +62,8 @@ export const VdpHeaderCard: React.FunctionComponent<VdpHeaderCardProps> = ({ veh
66
62
  ['fuel', stripParen(vehicle.fuel)],
67
63
  ['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
68
64
  ];
69
- // condition.hu === 'Neu' compares the RAW backend data value, not a display string — left as-is.
70
- const tiles: ReadonlyArray<readonly [string, string, string]> = [
71
- ['wrench', t('autobahn:header.huLabel'), condition.hu === 'Neu' ? t('autobahn:header.huNew') : t('autobahn:header.huUntil', { date: condition.hu })],
72
- ['shield-check', t('autobahn:header.accidentFree'), condition.unfallfrei ? t('autobahn:header.yes') : t('autobahn:header.no')],
73
- ['clipboard-check', t('autobahn:header.serviceHistory'), condition.scheckheft ? t('autobahn:header.yes') : t('autobahn:header.no')],
74
- ['award', t('autobahn:header.warranty'), condition.garantie],
75
- ];
76
65
  return (
77
- <div className="mcard" style={{ marginTop: 16, padding: '22px 24px 18px' }}>
66
+ <>
78
67
  <div className="vdp-headrow">
79
68
  <div style={{ minWidth: 0 }}>
80
69
  <h1 className="md-headline" style={{ margin: '0 0 4px' }}>
@@ -106,43 +95,112 @@ export const VdpHeaderCard: React.FunctionComponent<VdpHeaderCardProps> = ({ veh
106
95
  </span>
107
96
  ))}
108
97
  </div>
98
+ </>
99
+ );
100
+ };
109
101
 
110
- {/* hand-picked highlights — the single accent row of the header */}
111
- <div
112
- className="vdp-highlights"
113
- style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginTop: 14 }}
114
- >
115
- {vehicle.highlights.slice(0, 6).map((f) => (
116
- <span key={f} className="chip chip--filter chip--selected" style={{ cursor: 'default' }}>
117
- <Icon name="check" size={14} stroke={2.4} />
118
- {f}
119
- </span>
120
- ))}
121
- </div>
102
+ /** hand-picked highlights — the single accent row of the header. */
103
+ export const VdpHeaderHighlights: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle }) => (
104
+ <div className="vdp-highlights" style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8 }}>
105
+ {vehicle.highlights.slice(0, 6).map((f) => (
106
+ <span key={f} className="chip chip--filter chip--selected" style={{ cursor: 'default' }}>
107
+ <Icon name="check" size={14} stroke={2.4} />
108
+ {f}
109
+ </span>
110
+ ))}
111
+ </div>
112
+ );
122
113
 
123
- {/* condition — footer row of the header card, divided by a hairline */}
124
- <div className="zustand-grid">
125
- {tiles.map(([ic, l, v]) => (
126
- <div key={l} className="zustand-tile">
127
- <Icon name={ic} size={19} color="var(--md-primary)" stroke={1.9} style={{ flex: 'none' }} />
128
- <span style={{ minWidth: 0 }}>
129
- <span
130
- className="md-label-sm"
131
- style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
132
- >
133
- {l}
134
- </span>
135
- <span className="md-title-sm" style={{ display: 'block' }}>
136
- {v}
137
- </span>
114
+ /** condition (zustand) tiles — TÜV/Unfallfrei/Scheckheft/Garantie. */
115
+ export const VdpHeaderCondition: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
116
+ const { condition } = vehicle;
117
+ // condition.hu === 'Neu' compares the RAW backend data value, not a display string — left as-is.
118
+ const tiles: ReadonlyArray<readonly [string, string, string]> = [
119
+ ['wrench', t('autobahn:header.huLabel'), condition.hu === 'Neu' ? t('autobahn:header.huNew') : t('autobahn:header.huUntil', { date: condition.hu })],
120
+ ['shield-check', t('autobahn:header.accidentFree'), condition.unfallfrei ? t('autobahn:header.yes') : t('autobahn:header.no')],
121
+ ['clipboard-check', t('autobahn:header.serviceHistory'), condition.scheckheft ? t('autobahn:header.yes') : t('autobahn:header.no')],
122
+ ['award', t('autobahn:header.warranty'), condition.garantie],
123
+ ];
124
+ return (
125
+ <div className="zustand-grid">
126
+ {tiles.map(([ic, l, v]) => (
127
+ <div key={l} className="zustand-tile">
128
+ <Icon name={ic} size={19} color="var(--md-primary)" stroke={1.9} style={{ flex: 'none' }} />
129
+ <span style={{ minWidth: 0 }}>
130
+ <span
131
+ className="md-label-sm"
132
+ style={{ display: 'block', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}
133
+ >
134
+ {l}
138
135
  </span>
139
- </div>
140
- ))}
141
- </div>
136
+ <span className="md-title-sm" style={{ display: 'block' }}>
137
+ {v}
138
+ </span>
139
+ </span>
140
+ </div>
141
+ ))}
142
142
  </div>
143
143
  );
144
144
  };
145
145
 
146
+ /**
147
+ * One container holding the entire vehicle identity: headline + trim/power, the
148
+ * dot-separated fact line, the hand-picked highlight chips, and the condition
149
+ * (zustand) tiles footer. The gallery itself is composed by the page (W3b) — this
150
+ * is just the textual header card that sits beneath it. Desktop-only usage (mobile
151
+ * splits these 3 pieces across the "before/after finance card" layout — see the page).
152
+ */
153
+ export const VdpHeaderCard: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => (
154
+ <div className="mcard" style={{ marginTop: 16, padding: '22px 24px 18px' }}>
155
+ <VdpHeaderSummary vehicle={vehicle} t={t} />
156
+ <div style={{ marginTop: 14 }}>
157
+ <VdpHeaderHighlights vehicle={vehicle} t={t} />
158
+ </div>
159
+ <VdpHeaderCondition vehicle={vehicle} t={t} />
160
+ </div>
161
+ );
162
+
163
+ export interface VdpTrustRowProps {
164
+ /** maps to carStore.isAvailable / state.isOnline — "Sofort verfügbar". */
165
+ available: boolean;
166
+ /** real totalCarImpCount from /service/user-actions/api/v1/stats/viewers — omit while loading. */
167
+ viewCount?: number;
168
+ /** real totalFavCount from the same endpoint — omit while loading. */
169
+ favCount?: number;
170
+ /** i18n function (drilled from the page). */
171
+ t: TranslationFunction;
172
+ }
173
+
174
+ /** mobile-only trust-signal row shown between the identity block and the finance card:
175
+ * availability · view count · favorite count · "Geprüft" (every auto.de listing is
176
+ * 130-point inspected, so this one is always shown, not conditional real data). */
177
+ export const VdpTrustRow: React.FunctionComponent<VdpTrustRowProps> = ({ available, viewCount, favCount, t }) => (
178
+ <div style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 6, marginTop: 10 }}>
179
+ {available && (
180
+ <span className="vdp-trust-chip">
181
+ <Icon name="check" size={13} stroke={2.4} color="var(--md-good)" />
182
+ {t('autobahn:availability.immediately')}
183
+ </span>
184
+ )}
185
+ {viewCount != null && (
186
+ <span className="vdp-trust-chip">
187
+ <Icon name="eye" size={13} />
188
+ {t('autobahn:activity.viewsCount', { count: viewCount })}
189
+ </span>
190
+ )}
191
+ {favCount != null && (
192
+ <span className="vdp-trust-chip">
193
+ <Icon name="heart" size={13} />
194
+ {t('autobahn:activity.favoritesCount', { count: favCount })}
195
+ </span>
196
+ )}
197
+ <span className="vdp-trust-chip">
198
+ <Icon name="shield-check" size={13} />
199
+ {t('autobahn:trust.checked')}
200
+ </span>
201
+ </div>
202
+ );
203
+
146
204
  // ---------------------------------------------------------------------------
147
205
  // Fahrzeugdaten spec table (mobile clamp + "Mehr anzeigen")
148
206
  // ---------------------------------------------------------------------------
@@ -934,6 +992,29 @@ export const InspectionBlock: React.FunctionComponent<InspectionBlockProps> = ({
934
992
  // Finanzierungs-/Leasingdetails popup (desktop modal / mobile bottom-sheet)
935
993
  // ---------------------------------------------------------------------------
936
994
 
995
+ /** full-width label(+optional trailing price) / Switch row — Switch itself is inline-flex
996
+ * (sized to its own content), so the space-between has to live on this wrapper, not on
997
+ * Switch's `label` prop (which renders track-then-label, not a full-width justified row). */
998
+ const ToggleRow: React.FunctionComponent<{
999
+ label: string;
1000
+ price?: string;
1001
+ checked: boolean;
1002
+ disabled?: boolean;
1003
+ onChange: (checked: boolean) => void;
1004
+ }> = ({ label, price, checked, disabled, onChange }) => (
1005
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
1006
+ <span className="md-title-sm">{label}</span>
1007
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
1008
+ {price && (
1009
+ <span className="num" style={{ fontWeight: 700, fontSize: 13.5 }}>
1010
+ {price}
1011
+ </span>
1012
+ )}
1013
+ <Switch checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} ariaLabel={label} />
1014
+ </div>
1015
+ </div>
1016
+ );
1017
+
937
1018
  export interface FinanceDetailsModalProps {
938
1019
  /** open flag (controlled by the page). */
939
1020
  open: boolean;
@@ -971,6 +1052,25 @@ export interface FinanceDetailsModalProps {
971
1052
  onTermChange?: (term: number) => void;
972
1053
  onAnnualMileageChange?: (km: number) => void;
973
1054
  onClose: () => void;
1055
+ /** mobile only — start with the Finanzierungs-/Leasingdetails breakdown already expanded
1056
+ * (opened directly via that link, skipping the "Rate anpassen" collapsed default view). */
1057
+ initialShowBreakdown?: boolean;
1058
+ /** mobile only — price-rating label, drives the eval pill/bar (same as the collapsed card). */
1059
+ ratingLabel?: string;
1060
+ /** mobile only — mode tabs inside the sheet (mirrors the collapsed card's tabs). */
1061
+ onModeChange?: (mode: FinanceMode) => void;
1062
+ /** mobile only — primary "Rate anfragen"/"Angebot anfragen" CTA. */
1063
+ onSecure?: () => void;
1064
+ /** mobile only — secondary call CTA. */
1065
+ onCall?: () => void;
1066
+ /** mobile only — "Mit Schlussrate" toggle (financing only; leasing has no balloon concept). */
1067
+ withFinalInstallment?: boolean;
1068
+ onToggleFinalInstallment?: (checked: boolean) => void;
1069
+ /** mobile only — "Rundum-Sorglos-Paket" toggle (all modes; forced-on + disabled for cash). */
1070
+ withAddServices?: boolean;
1071
+ addServicesCost?: number;
1072
+ addServicesDisabled?: boolean;
1073
+ onToggleAddServices?: (checked: boolean) => void;
974
1074
  /** i18n function (drilled from the page). */
975
1075
  t: TranslationFunction;
976
1076
  }
@@ -1008,11 +1108,41 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1008
1108
  onTermChange,
1009
1109
  onAnnualMileageChange,
1010
1110
  onClose,
1111
+ initialShowBreakdown,
1112
+ ratingLabel,
1113
+ onModeChange,
1114
+ onSecure,
1115
+ onCall,
1116
+ withFinalInstallment,
1117
+ onToggleFinalInstallment,
1118
+ withAddServices,
1119
+ addServicesCost,
1120
+ addServicesDisabled,
1121
+ onToggleAddServices,
1011
1122
  t,
1012
1123
  }) => {
1124
+ // mobile-only: the Finanzierungs-/Leasingdetails breakdown starts collapsed (opened via "Rate
1125
+ // anpassen") or expanded (opened directly via the details link) — reset on every open, since
1126
+ // this component stays mounted (the page always renders it, gated by `open`, not unmounted).
1127
+ const [showBreakdown, setShowBreakdown] = React.useState(!!initialShowBreakdown);
1128
+ React.useEffect(() => {
1129
+ if (open) setShowBreakdown(!!initialShowBreakdown);
1130
+ // eslint-disable-next-line react-hooks/exhaustive-deps
1131
+ }, [open, initialShowBreakdown]);
1132
+
1013
1133
  if (!open) return null;
1014
1134
  const isLease = mode === 'leasing';
1135
+ const isCash = mode === 'cash';
1015
1136
  const title = isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails');
1137
+ const mobileTitle = isCash
1138
+ ? t('autobahn:modal.sheetTitleCash')
1139
+ : isLease
1140
+ ? t('autobahn:modal.sheetTitleLeasing')
1141
+ : t('autobahn:modal.sheetTitleFinancing');
1142
+ // price-evaluation tier — identical derivation to the collapsed card (FinanceCalculator).
1143
+ const tierIdx = TIER_LABELS.indexOf(ratingLabel as (typeof TIER_LABELS)[number]);
1144
+ const evalTier = tierIdx >= 0 ? tierIdx : 1;
1145
+ const evalTierLabel = t('autobahn:tier.' + TIER_KEYS[evalTier]);
1016
1146
  // adjustable only for fin/leasing, and only when the page supplies the change handlers + bounds.
1017
1147
  const adjustable = mode !== 'cash' && !!onDownChange && downMin != null && downMax != null && termMin != null && termMax != null;
1018
1148
  // discrete term chip values (supplied periods, else derived from the bounds).
@@ -1041,89 +1171,274 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1041
1171
  [isLease ? t('autobahn:modal.leasingRate') : t('autobahn:modal.monthlyRate'), t('autobahn:modal.ratePerMonth', { amount: eur(monthly) })],
1042
1172
  ...(grossAmount != null && grossAmount > 0 ? ([[isLease ? t('autobahn:modal.totalAmount') : t('autobahn:modal.totalRepayment'), `${eur(grossAmount)} €`]] as Array<[string, string]>) : []),
1043
1173
  ];
1174
+ const breakdownRows = rows.map(([l, v], i) => (
1175
+ <div
1176
+ key={l}
1177
+ style={{
1178
+ display: 'flex',
1179
+ justifyContent: 'space-between',
1180
+ gap: 12,
1181
+ padding: '9px 0',
1182
+ borderBottom: i < rows.length - 1 ? '1px solid var(--md-outline-variant)' : 0,
1183
+ }}
1184
+ >
1185
+ <span className="md-label-sm">{l}</span>
1186
+ <span className="num" style={{ fontSize: 13.5, fontWeight: 600 }}>
1187
+ {v}
1188
+ </span>
1189
+ </div>
1190
+ ));
1191
+
1044
1192
  return (
1045
1193
  <div className="fin-modal-scrim" onClick={onClose}>
1046
1194
  <div className="fin-modal" role="dialog" aria-modal="true" aria-label={title} onClick={(e) => e.stopPropagation()}>
1047
1195
  <div className="fin-modal-head">
1048
- <h2 className="md-headline-sm" style={{ margin: 0 }}>
1196
+ <h2 className="md-headline-sm hide-mobile" style={{ margin: 0 }}>
1049
1197
  {title}
1050
1198
  </h2>
1199
+ <h2 className="md-headline-sm show-mobile" style={{ margin: 0 }}>
1200
+ {mobileTitle}
1201
+ </h2>
1051
1202
  <button className="icon-btn" onClick={onClose} aria-label={t('autobahn:modal.close')}>
1052
1203
  <Icon name="x" size={20} />
1053
1204
  </button>
1054
1205
  </div>
1055
1206
  <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 && (
1207
+ {/* ============================== DESKTOP (≥601px) — unchanged ============================== */}
1208
+ <div className="hide-mobile">
1209
+ <div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 14 }}>
1210
+ <span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
1211
+ {eur(monthly)} €
1212
+ </span>
1213
+ <span className="md-label-sm">{t('autobahn:finance.perMonth')}</span>
1214
+ </div>
1215
+ {/* adjustment sliders — change a value and every figure below recomputes live (via the page) */}
1216
+ {adjustable && (
1217
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16, marginBottom: 18 }}>
1082
1218
  <div>
1083
1219
  <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
1084
- <span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
1220
+ <span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
1085
1221
  <span className="num" style={{ fontWeight: 700 }}>
1086
- {eur(annualMileage)} km
1222
+ {eur(down)} €
1087
1223
  </span>
1088
1224
  </div>
1089
1225
  <Slider
1090
- min={annualMileageMin}
1091
- max={annualMileageMax}
1092
- step={annualMileageStep}
1093
- value={annualMileage}
1094
- onChange={onAnnualMileageChange}
1095
- fmt={(v) => `${eur(Number(v))} km`}
1226
+ min={downMin as number}
1227
+ max={downMax as number}
1228
+ step={downStep}
1229
+ value={down}
1230
+ onChange={onDownChange as (n: number) => void}
1231
+ fmt={(v) => `${eur(Number(v))} €`}
1096
1232
  />
1097
1233
  </div>
1098
- )}
1099
- <div>
1100
- <div className="md-title-sm" style={{ marginBottom: 8 }}>
1101
- {t('autobahn:finance.termSelection')}
1234
+ {isLease && annualMileage != null && onAnnualMileageChange && (
1235
+ <div>
1236
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
1237
+ <span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
1238
+ <span className="num" style={{ fontWeight: 700 }}>
1239
+ {eur(annualMileage)} km
1240
+ </span>
1241
+ </div>
1242
+ <Slider
1243
+ min={annualMileageMin}
1244
+ max={annualMileageMax}
1245
+ step={annualMileageStep}
1246
+ value={annualMileage}
1247
+ onChange={onAnnualMileageChange}
1248
+ fmt={(v) => `${eur(Number(v))} km`}
1249
+ />
1250
+ </div>
1251
+ )}
1252
+ <div>
1253
+ <div className="md-title-sm" style={{ marginBottom: 8 }}>
1254
+ {t('autobahn:finance.termSelection')}
1255
+ </div>
1256
+ <TermChips options={termVals} value={term} onChange={onTermChange as (n: number) => void} t={t} />
1102
1257
  </div>
1103
- <TermChips options={termVals} value={term} onChange={onTermChange as (n: number) => void} t={t} />
1258
+ </div>
1259
+ )}
1260
+ {breakdownRows}
1261
+ <p className="md-label-sm" style={{ marginTop: 16, lineHeight: 1.55 }}>
1262
+ {isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
1263
+ </p>
1264
+ </div>
1265
+
1266
+ {/* ============================== MOBILE (≤600px) — full "Rate anpassen" sheet ============================== */}
1267
+ <div className="show-mobile" style={{ flexDirection: 'column', gap: 18 }}>
1268
+ {onModeChange && (
1269
+ <div className="fin-tabs" role="tablist">
1270
+ {(
1271
+ [
1272
+ ['fin', 'autobahn:finance.mode.financing'],
1273
+ ['leasing', 'autobahn:finance.mode.leasing'],
1274
+ ['cash', 'autobahn:finance.mode.cash'],
1275
+ ] as Array<[FinanceMode, string]>
1276
+ ).map(([k, labelKey]) => (
1277
+ <button
1278
+ key={k}
1279
+ type="button"
1280
+ role="tab"
1281
+ aria-selected={mode === k}
1282
+ className={'fin-tab' + (mode === k ? ' on' : '')}
1283
+ onClick={() => onModeChange(k)}
1284
+ >
1285
+ {t(labelKey)}
1286
+ </button>
1287
+ ))}
1288
+ </div>
1289
+ )}
1290
+
1291
+ <div className="fin-price-grid">
1292
+ <div className="fin-rate-main">
1293
+ <div style={{ display: 'flex', alignItems: 'baseline', gap: 5 }}>
1294
+ <span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
1295
+ {isCash ? eur(price) : eur(Math.round(monthly))}
1296
+ </span>
1297
+ <span className="md-body-sm" style={{ fontWeight: 600 }}>
1298
+ {isCash ? '€' : `€ ${t('autobahn:finance.perMonth')}`}
1299
+ </span>
1300
+ </div>
1301
+ </div>
1302
+ <div>
1303
+ {ratingLabel && (
1304
+ <span
1305
+ className="fin-eval-pill"
1306
+ style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
1307
+ >
1308
+ <Icon
1309
+ name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
1310
+ size={14}
1311
+ stroke={2.6}
1312
+ color={TIER_COLORS[evalTier]}
1313
+ />
1314
+ {evalTierLabel}
1315
+ </span>
1316
+ )}
1317
+ </div>
1318
+ <div />
1319
+ <div>
1320
+ {ratingLabel && (
1321
+ <div className="fin-eval-bar">
1322
+ <div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
1323
+ </div>
1324
+ )}
1104
1325
  </div>
1105
1326
  </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>
1327
+
1328
+ {isCash ? (
1329
+ <>
1330
+ <div>
1331
+ <CalcRow label={t('autobahn:finance.paymentLabel')}>{t('autobahn:finance.paymentValue')}</CalcRow>
1332
+ <CalcRow label={t('autobahn:finance.warrantyLabel')}>{t('autobahn:finance.warrantyValue')}</CalcRow>
1333
+ <CalcRow label={t('autobahn:finance.deliveryLabel')} last>
1334
+ {t('autobahn:finance.deliveryValue')}
1335
+ </CalcRow>
1336
+ </div>
1337
+ {addServicesCost != null && (
1338
+ <ToggleRow
1339
+ label={t('autobahn:finance.addServicesToggle')}
1340
+ price={t('autobahn:finance.addServicesOnce', { value: eur(addServicesCost) })}
1341
+ checked={!!withAddServices}
1342
+ disabled={addServicesDisabled}
1343
+ onChange={(checked) => onToggleAddServices && onToggleAddServices(checked)}
1344
+ />
1345
+ )}
1346
+ <div style={{ display: 'flex', gap: 10 }}>
1347
+ <Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
1348
+ {t('autobahn:finance.secureOffer')}
1349
+ </Button>
1350
+ <Button variant="outlined" size="lg" icon="phone" style={{ width: 52, flex: 'none' }} onClick={onCall} />
1351
+ </div>
1352
+ </>
1353
+ ) : (
1354
+ <>
1355
+ {adjustable && (
1356
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
1357
+ <div>
1358
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
1359
+ <span className="md-title-sm">{t('autobahn:finance.downPaymentSliderLabel')}</span>
1360
+ <span className="num" style={{ fontWeight: 700 }}>
1361
+ {eur(down)} €
1362
+ </span>
1363
+ </div>
1364
+ <Slider
1365
+ min={downMin as number}
1366
+ max={downMax as number}
1367
+ step={downStep}
1368
+ value={down}
1369
+ onChange={onDownChange as (n: number) => void}
1370
+ fmt={(v) => `${eur(Number(v))} €`}
1371
+ />
1372
+ </div>
1373
+ {isLease && annualMileage != null && onAnnualMileageChange && (
1374
+ <div>
1375
+ <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
1376
+ <span className="md-title-sm">{t('autobahn:finance.annualMileageLabel')}</span>
1377
+ <span className="num" style={{ fontWeight: 700 }}>
1378
+ {eur(annualMileage)} km
1379
+ </span>
1380
+ </div>
1381
+ <Slider
1382
+ min={annualMileageMin}
1383
+ max={annualMileageMax}
1384
+ step={annualMileageStep}
1385
+ value={annualMileage}
1386
+ onChange={onAnnualMileageChange}
1387
+ fmt={(v) => `${eur(Number(v))} km`}
1388
+ />
1389
+ </div>
1390
+ )}
1391
+ <div>
1392
+ <div className="md-title-sm" style={{ marginBottom: 8 }}>
1393
+ {t('autobahn:finance.termSelection')}
1394
+ </div>
1395
+ <TermChips options={termVals} value={term} onChange={onTermChange as (n: number) => void} t={t} />
1396
+ </div>
1397
+ </div>
1398
+ )}
1399
+
1400
+ {!isLease && onToggleFinalInstallment && (
1401
+ <ToggleRow
1402
+ label={t('autobahn:finance.finalInstallmentToggle')}
1403
+ checked={!!withFinalInstallment}
1404
+ onChange={onToggleFinalInstallment}
1405
+ />
1406
+ )}
1407
+ {addServicesCost != null && onToggleAddServices && (
1408
+ <ToggleRow
1409
+ label={t('autobahn:finance.addServicesToggle')}
1410
+ price={t('autobahn:finance.addServicesPerMonth', { value: eur(addServicesCost) })}
1411
+ checked={!!withAddServices}
1412
+ disabled={addServicesDisabled}
1413
+ onChange={onToggleAddServices}
1414
+ />
1415
+ )}
1416
+
1417
+ <div style={{ display: 'flex', gap: 10 }}>
1418
+ <Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
1419
+ {t('autobahn:finance.requestRate')}
1420
+ </Button>
1421
+ <Button variant="outlined" size="lg" icon="phone" style={{ width: 52, flex: 'none' }} onClick={onCall} />
1422
+ </div>
1423
+
1424
+ <button type="button" onClick={() => setShowBreakdown((v) => !v)} style={expanderBtnStyle}>
1425
+ <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
1426
+ <Icon name="info" size={16} color="var(--md-primary)" />
1427
+ {isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails')}
1428
+ </span>
1429
+ <Icon name={showBreakdown ? 'chevron-up' : 'chevron-down'} size={18} color="var(--md-primary)" />
1430
+ </button>
1431
+ {showBreakdown && (
1432
+ <div>
1433
+ {breakdownRows}
1434
+ <p className="md-label-sm" style={{ marginTop: 16, lineHeight: 1.55 }}>
1435
+ {isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
1436
+ </p>
1437
+ </div>
1438
+ )}
1439
+ </>
1440
+ )}
1441
+ </div>
1127
1442
  </div>
1128
1443
  </div>
1129
1444
  </div>
package/vdp/index.ts CHANGED
@@ -23,6 +23,10 @@ export type {
23
23
  // Remaining presentational modules.
24
24
  export {
25
25
  VdpHeaderCard,
26
+ VdpHeaderSummary,
27
+ VdpHeaderHighlights,
28
+ VdpHeaderCondition,
29
+ VdpTrustRow,
26
30
  VdpSpecTable,
27
31
  EquipmentList,
28
32
  Co2Label,
@@ -41,6 +45,7 @@ export {
41
45
  } from './VdpModules';
42
46
  export type {
43
47
  VdpHeaderCardProps,
48
+ VdpTrustRowProps,
44
49
  VdpSpecTableProps,
45
50
  EquipmentListProps,
46
51
  Co2LabelProps,