@bytebrand/fe-ui-core-autobahn 1.0.42 → 1.0.44
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/locales/de/autobahn.json +19 -10
- package/locales/en/autobahn.json +17 -8
- package/package.json +1 -1
- package/theme/md3.css +38 -4
- package/vdp/FinanceCalculator.tsx +57 -34
- package/vdp/VdpModules.tsx +155 -97
- package/vdp/index.ts +1 -0
package/locales/de/autobahn.json
CHANGED
|
@@ -488,10 +488,10 @@
|
|
|
488
488
|
"immediately": "Sofort verfügbar"
|
|
489
489
|
},
|
|
490
490
|
"tier": {
|
|
491
|
-
"top": "Top
|
|
491
|
+
"top": "Top-Preis",
|
|
492
492
|
"good": "Guter Preis",
|
|
493
|
-
"fair": "Fairer
|
|
494
|
-
"raised": "
|
|
493
|
+
"fair": "Fairer-Preis",
|
|
494
|
+
"raised": "Erhöhter Preis",
|
|
495
495
|
"high": "Hoher Preis"
|
|
496
496
|
},
|
|
497
497
|
"sort": {
|
|
@@ -528,13 +528,13 @@
|
|
|
528
528
|
}
|
|
529
529
|
},
|
|
530
530
|
"eco": {
|
|
531
|
-
"heading": "Umwelt & Verbrauch",
|
|
531
|
+
"heading": "Umwelt & Verbrauch*",
|
|
532
532
|
"stat": {
|
|
533
533
|
"powerConsumptionWeighted": "Stromverbrauch (gew.)",
|
|
534
534
|
"fuelConsumptionWeighted": "Kraftstoff (gew.)",
|
|
535
535
|
"fuelConsumptionDischarged": "Kraftstoff (entladen)",
|
|
536
536
|
"co2WeightedCombined": "CO₂ (gew., komb.)",
|
|
537
|
-
"consumptionCombined": "Verbrauch
|
|
537
|
+
"consumptionCombined": "Verbrauch (WLTP-kombiniert)",
|
|
538
538
|
"co2Emissions": "CO₂-Emissionen (WLTP)",
|
|
539
539
|
"co2Class": "CO₂-Klasse (WLTP)"
|
|
540
540
|
},
|
|
@@ -544,7 +544,8 @@
|
|
|
544
544
|
"energyClassWltpSuffix": "Energieeffizienzklasse nach WLTP · Angaben gem. Pkw-EnVKV*",
|
|
545
545
|
"energyClass": "Energieeffizienzklasse nach WLTP (kombiniert) · Angaben gem. Pkw-EnVKV*",
|
|
546
546
|
"weightedShort": "gew. komb.",
|
|
547
|
-
"dischargedShort": "entladen komb."
|
|
547
|
+
"dischargedShort": "entladen komb.",
|
|
548
|
+
"co2ClassScaleLabel": "CO₂-Klasse (WLTP-kombiniert)"
|
|
548
549
|
},
|
|
549
550
|
"description": {
|
|
550
551
|
"heading": "Fahrzeugbeschreibung",
|
|
@@ -570,7 +571,7 @@
|
|
|
570
571
|
},
|
|
571
572
|
"marketValue": {
|
|
572
573
|
"heading": "Marktwert-Einordnung",
|
|
573
|
-
"belowAverage": "{{rating}} — {{amount}} € unter
|
|
574
|
+
"belowAverage": "{{rating}} — {{amount}} € unter Marktwert"
|
|
574
575
|
},
|
|
575
576
|
"priceMarket": {
|
|
576
577
|
"heading": "Preis & Marktwert",
|
|
@@ -680,11 +681,19 @@
|
|
|
680
681
|
"ratePerMonth": "{{amount}} € /mtl.",
|
|
681
682
|
"totalAmount": "Gesamtbetrag",
|
|
682
683
|
"totalRepayment": "Gesamtrückzahlungsbetrag",
|
|
683
|
-
"legalLeasing": "
|
|
684
|
-
"legalFinancing": "
|
|
684
|
+
"legalLeasing": "Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Bonität vorausgesetzt, ist dies ein repräsentatives Berechnungsbeispiel gem. der Angaben, welches 2/3 aller Kunden, im Sinne des § 17a Abs. 4 PangV, erhalten. 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.",
|
|
685
|
+
"legalFinancing": "Für Ihre Finanzierungswünsche stellen wir eine Bonitätsanfrage. Bonität vorausgesetzt, ist dies ein repräsentatives Berechnungsbeispiel gem. der Angaben, welches 2/3 aller Kunden, im Sinne des § 17a Abs. 4 PangV, erhalten. 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.",
|
|
685
686
|
"sheetTitleFinancing": "Kredit-Rate anpassen",
|
|
686
687
|
"sheetTitleLeasing": "Leasing-Rate anpassen",
|
|
687
|
-
"sheetTitleCash": "Kaufen - Details"
|
|
688
|
+
"sheetTitleCash": "Kaufen - Details",
|
|
689
|
+
"exampleVehiclePrice": "Fahrzeugpreis",
|
|
690
|
+
"examplePriceSuffix": "ggf. inkl. anderer Services",
|
|
691
|
+
"exampleApr": "Effektiver Jahreszins",
|
|
692
|
+
"exampleNominal": "Sollzinssatz p. a. (gebunden für die gesamte Vertragslaufzeit)",
|
|
693
|
+
"exampleNet": "Nettodarlehensbetrag",
|
|
694
|
+
"exampleDown": "Anzahlung",
|
|
695
|
+
"exampleFinal": "Schlussrate",
|
|
696
|
+
"exampleGross": "Gesamtrückzahlungsbetrag"
|
|
688
697
|
},
|
|
689
698
|
"ratings": {
|
|
690
699
|
"showLess": "Weniger anzeigen",
|
package/locales/en/autobahn.json
CHANGED
|
@@ -491,7 +491,7 @@
|
|
|
491
491
|
"top": "Top price",
|
|
492
492
|
"good": "Good price",
|
|
493
493
|
"fair": "Fair price",
|
|
494
|
-
"raised": "Increased",
|
|
494
|
+
"raised": "Increased price",
|
|
495
495
|
"high": "High price"
|
|
496
496
|
},
|
|
497
497
|
"sort": {
|
|
@@ -528,13 +528,13 @@
|
|
|
528
528
|
}
|
|
529
529
|
},
|
|
530
530
|
"eco": {
|
|
531
|
-
"heading": "Environment & consumption",
|
|
531
|
+
"heading": "Environment & consumption*",
|
|
532
532
|
"stat": {
|
|
533
533
|
"powerConsumptionWeighted": "Power consumption (weighted)",
|
|
534
534
|
"fuelConsumptionWeighted": "Fuel (weighted)",
|
|
535
535
|
"fuelConsumptionDischarged": "Fuel (discharged)",
|
|
536
536
|
"co2WeightedCombined": "CO₂ (weighted, combined)",
|
|
537
|
-
"consumptionCombined": "Consumption
|
|
537
|
+
"consumptionCombined": "Consumption (WLTP combined)",
|
|
538
538
|
"co2Emissions": "CO₂ emissions (WLTP)",
|
|
539
539
|
"co2Class": "CO₂ class (WLTP)"
|
|
540
540
|
},
|
|
@@ -544,7 +544,8 @@
|
|
|
544
544
|
"energyClassWltpSuffix": "Energy efficiency class per WLTP · figures pursuant to the German Pkw-EnVKV*",
|
|
545
545
|
"energyClass": "Energy efficiency class per WLTP (combined) · figures pursuant to the German Pkw-EnVKV*",
|
|
546
546
|
"weightedShort": "wtd. comb.",
|
|
547
|
-
"dischargedShort": "depleted comb."
|
|
547
|
+
"dischargedShort": "depleted comb.",
|
|
548
|
+
"co2ClassScaleLabel": "CO₂ class (WLTP combined)"
|
|
548
549
|
},
|
|
549
550
|
"description": {
|
|
550
551
|
"heading": "Vehicle description",
|
|
@@ -570,7 +571,7 @@
|
|
|
570
571
|
},
|
|
571
572
|
"marketValue": {
|
|
572
573
|
"heading": "Market-value positioning",
|
|
573
|
-
"belowAverage": "{{rating}} — {{amount}} € below
|
|
574
|
+
"belowAverage": "{{rating}} — {{amount}} € below market value"
|
|
574
575
|
},
|
|
575
576
|
"priceMarket": {
|
|
576
577
|
"heading": "Price & market value",
|
|
@@ -680,11 +681,19 @@
|
|
|
680
681
|
"ratePerMonth": "{{amount}} € /mo.",
|
|
681
682
|
"totalAmount": "Total amount",
|
|
682
683
|
"totalRepayment": "Total repayment amount",
|
|
683
|
-
"legalLeasing": "
|
|
684
|
-
"legalFinancing": "
|
|
684
|
+
"legalLeasing": "We run a credit check for your financing request. Subject to creditworthiness, this is a representative example per § 17a (4) PangV received by 2/3 of all customers. This non-binding offer of Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, acting as an untied intermediary without advising. Errors excepted. Prices may incl. VAT.",
|
|
685
|
+
"legalFinancing": "We run a credit check for your financing request. Subject to creditworthiness, this is a representative example per § 17a (4) PangV received by 2/3 of all customers. This non-binding offer of Santander Consumer Bank AG, Santander-Platz 1, 41061 Mönchengladbach is brokered by auto.de GmbH, Max-Planck-Str. 19, 06796 Sandersdorf-Brehna, acting as an untied intermediary without advising. Errors excepted. Prices may incl. VAT.",
|
|
685
686
|
"sheetTitleFinancing": "Adjust credit rate",
|
|
686
687
|
"sheetTitleLeasing": "Adjust leasing rate",
|
|
687
|
-
"sheetTitleCash": "Purchase - Details"
|
|
688
|
+
"sheetTitleCash": "Purchase - Details",
|
|
689
|
+
"exampleVehiclePrice": "Vehicle price",
|
|
690
|
+
"examplePriceSuffix": "possibly incl. other services",
|
|
691
|
+
"exampleApr": "Effective annual rate",
|
|
692
|
+
"exampleNominal": "Nominal rate p.a. (fixed for the entire contract term)",
|
|
693
|
+
"exampleNet": "Net loan amount",
|
|
694
|
+
"exampleDown": "Down payment",
|
|
695
|
+
"exampleFinal": "Final installment",
|
|
696
|
+
"exampleGross": "Total repayment amount"
|
|
688
697
|
},
|
|
689
698
|
"ratings": {
|
|
690
699
|
"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.
|
|
3
|
+
"version": "1.0.44",
|
|
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
|
@@ -1195,8 +1195,15 @@
|
|
|
1195
1195
|
border-radius: var(--md-r-full); font-size: 14px; font-weight: 700; line-height: 1; white-space: nowrap; }
|
|
1196
1196
|
.md3 .fin-eval-pill .lucide { flex: none; }
|
|
1197
1197
|
/* gradient spectrum bar + circle thumb — replaces opacity-segment design */
|
|
1198
|
-
|
|
1199
|
-
|
|
1198
|
+
/* segmented evaluation bar (Figma round 12: discrete tier segments, not a gradient) —
|
|
1199
|
+
the gradient background below is dead once .fin-eval-seg children render on top */
|
|
1200
|
+
.md3 .fin-eval-seg { flex: 1; height: 100%; border-radius: 3px; }
|
|
1201
|
+
.md3 .fin-eval-bar { display: flex; gap: 3px; overflow: visible; }
|
|
1202
|
+
.md3 .slider-bubble { position: absolute; top: 0; transform: translateX(-50%); z-index: 1;
|
|
1203
|
+
padding: 4px 10px; border: 1px solid var(--md-outline-variant); border-radius: 8px;
|
|
1204
|
+
background: var(--md-sc-lowest); box-shadow: var(--md-e1); font-size: 13px; font-weight: 700;
|
|
1205
|
+
white-space: nowrap; }
|
|
1206
|
+
.md3 .fin-eval-bar { position: relative; width: 100%; height: 9px; border-radius: 5px; }
|
|
1200
1207
|
.md3 .fin-eval-thumb { position: absolute; top: 50%; transform: translate(-50%, -50%);
|
|
1201
1208
|
width: 17px; height: 17px; border-radius: 50%; background: #fff;
|
|
1202
1209
|
border: 3px solid currentColor; box-shadow: 0 1px 4px rgba(0,0,0,.3); pointer-events: none; }
|
|
@@ -1630,8 +1637,35 @@
|
|
|
1630
1637
|
.md3 .bb-cta-row .mbtn--outlined:not(.bb-adjust) { box-shadow: inset 0 0 0 1.5px var(--md-primary); }
|
|
1631
1638
|
/* mobile page is WHITE like the Figma master — beats the .vdp-tint inline gray */
|
|
1632
1639
|
.md3 .vdp-tint { background: var(--md-sc-lowest) !important; }
|
|
1633
|
-
/*
|
|
1634
|
-
|
|
1640
|
+
/* ---- round 12 (9-screenshot review) ---- */
|
|
1641
|
+
/* Laufzeit chips = one-row segmented control (Figma), unit suffix hidden */
|
|
1642
|
+
.md3 .fin-sheet-terms .term-chips { display: flex; flex-wrap: nowrap; gap: 0;
|
|
1643
|
+
border: 1px solid var(--fig-border); border-radius: 10px; overflow: hidden; }
|
|
1644
|
+
.md3 .fin-sheet-terms .term-chip { flex: 1 1 0; min-width: 0; height: 46px; border: 0;
|
|
1645
|
+
border-left: 1px solid var(--fig-border); border-radius: 0; padding: 0; font-size: 14px; }
|
|
1646
|
+
.md3 .fin-sheet-terms .term-chip:first-child { border-left: 0; }
|
|
1647
|
+
.md3 .fin-sheet-terms .term-chip .tc-unit { display: none; }
|
|
1648
|
+
/* sheet: no divider under the header; one under the rate row instead (Figma) */
|
|
1649
|
+
.md3 .fin-modal-head { border-bottom: 0; padding-bottom: 8px; }
|
|
1650
|
+
.md3 .fin-sheet-adjust { border-top: 1px solid var(--fig-tile-border); padding-top: 16px; }
|
|
1651
|
+
/* sheet cash rows (Zahlung/Garantie/Lieferung) — Figma sizes */
|
|
1652
|
+
.md3 .fin-modal .calcrow { padding: 13px 0; }
|
|
1653
|
+
.md3 .fin-modal .calcrow-label { font-size: 14px; color: var(--fig-gray); }
|
|
1654
|
+
.md3 .fin-modal .calcrow-value { font-size: 15px !important; color: var(--fig-navy); font-weight: 700 !important; }
|
|
1655
|
+
/* condition tiles: Figma value size + roomier gap to the pills above */
|
|
1656
|
+
.md3 .zustand-tile .md-title-sm { font-size: 16.5px; }
|
|
1657
|
+
.md3 .vdp-header-extras .zustand-grid { margin-top: 10px; }
|
|
1658
|
+
/* enquiry: aside without background (Figma), blue-bordered active contact chip,
|
|
1659
|
+
underlined legal links */
|
|
1660
|
+
.md3 .enquiry-aside { background: transparent; border-bottom: 0; }
|
|
1661
|
+
.md3 .enquiry-form .chip--selected { box-shadow: inset 0 0 0 1.5px var(--md-primary);
|
|
1662
|
+
background: var(--fig-chip-bg); color: var(--md-primary); }
|
|
1663
|
+
.md3 .enq-consent a { text-decoration: underline; }
|
|
1664
|
+
/* HowItWorks: text column beside the circle (title above description) */
|
|
1665
|
+
.md3 .hiw-body { display: flex; flex-direction: column; min-width: 0; }
|
|
1666
|
+
/* phone hero rate: 44px — with the € folded into the big span (round 11) 52px pushed the
|
|
1667
|
+
stacked /mtl. unit onto a second line; 44px fits "143 €" + stack beside the rating column */
|
|
1668
|
+
.md3 .fin-rate-main .num { font-size: 44px !important; letter-spacing: -1.2px !important; }
|
|
1635
1669
|
/* the struck old figure must NOT inherit the 52px hero size */
|
|
1636
1670
|
.md3 .fin-rate-main .fin-rate-old { font-size: 15px !important; letter-spacing: 0 !important; }
|
|
1637
1671
|
/* narrower rating column (figma bar is 115px) frees room for the bigger rate; let the
|
|
@@ -30,36 +30,67 @@ export interface SliderProps {
|
|
|
30
30
|
onChange: (value: number) => void;
|
|
31
31
|
/** formats the min/max end labels (and is reused by callers for the value). */
|
|
32
32
|
fmt?: (v: number) => React.ReactNode;
|
|
33
|
+
/** render the current value in a floating bubble above the thumb (Figma sheet sliders). */
|
|
34
|
+
bubble?: boolean;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
/**
|
|
36
38
|
* Range slider with a filled track and min/max end labels. The track fill is a
|
|
37
39
|
* linear-gradient driven by the value's position; the design's `.mslider` class
|
|
38
|
-
* styles the thumb/rail.
|
|
40
|
+
* styles the thumb/rail. With `bubble` the current value floats above the thumb
|
|
41
|
+
* (mockup "Rate anpassen" sliders) instead of the caller printing it elsewhere.
|
|
39
42
|
*/
|
|
40
|
-
export const Slider: React.FunctionComponent<SliderProps> = ({ min, max, step = 1, value, onChange, fmt = (v) => v }) => {
|
|
43
|
+
export const Slider: React.FunctionComponent<SliderProps> = ({ min, max, step = 1, value, onChange, fmt = (v) => v, bubble }) => {
|
|
41
44
|
const span = max - min;
|
|
42
45
|
const pct = span > 0 ? ((value - min) / span) * 100 : 0;
|
|
43
46
|
return (
|
|
44
47
|
<div>
|
|
45
|
-
<
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
<div style={{ position: 'relative', paddingTop: bubble ? 38 : 0 }}>
|
|
49
|
+
{bubble && (
|
|
50
|
+
// thumb is 22px wide: interpolate the bubble anchor between half-thumb insets so it
|
|
51
|
+
// tracks the thumb center (a plain pct% drifts at the rail ends)
|
|
52
|
+
<span className="slider-bubble num" style={{ left: `calc(11px + (100% - 22px) * ${pct / 100})` }}>
|
|
53
|
+
{fmt(value)}
|
|
54
|
+
</span>
|
|
55
|
+
)}
|
|
56
|
+
<input
|
|
57
|
+
type="range"
|
|
58
|
+
className="mslider"
|
|
59
|
+
min={min}
|
|
60
|
+
max={max}
|
|
61
|
+
step={step}
|
|
62
|
+
value={value}
|
|
63
|
+
onChange={(e) => onChange(Number(e.target.value))}
|
|
64
|
+
style={{ background: `linear-gradient(90deg, var(--md-primary) ${pct}%, var(--md-sc-highest) ${pct}%)` }}
|
|
65
|
+
/>
|
|
58
66
|
</div>
|
|
67
|
+
{!bubble && (
|
|
68
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 4 }}>
|
|
69
|
+
<span className="md-label-sm">{fmt(min)}</span>
|
|
70
|
+
<span className="md-label-sm">{fmt(max)}</span>
|
|
71
|
+
</div>
|
|
72
|
+
)}
|
|
59
73
|
</div>
|
|
60
74
|
);
|
|
61
75
|
};
|
|
62
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Segmented price-evaluation bar (Figma: 5 discrete tier segments, NOT a gradient) with a
|
|
79
|
+
* white ring marker. `leftPct` overrides the marker position (the Marktwert gauge positions
|
|
80
|
+
* it continuously); default = centered on the tier segment.
|
|
81
|
+
*/
|
|
82
|
+
export const EvalBar: React.FunctionComponent<{ tier: number; leftPct?: number }> = ({ tier, leftPct }) => (
|
|
83
|
+
<div className="fin-eval-bar">
|
|
84
|
+
{TIER_COLORS.map((c, i) => (
|
|
85
|
+
<span key={i} className="fin-eval-seg" style={{ background: c }} />
|
|
86
|
+
))}
|
|
87
|
+
<div
|
|
88
|
+
className="fin-eval-thumb"
|
|
89
|
+
style={{ left: `${leftPct != null ? leftPct : (tier + 0.5) * 20}%`, borderColor: TIER_COLORS[Math.max(0, Math.min(4, tier))] }}
|
|
90
|
+
/>
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
|
|
63
94
|
export interface TermChipsProps {
|
|
64
95
|
/** the selectable term values (months), e.g. [12,24,36,48,60,72]. */
|
|
65
96
|
options: number[];
|
|
@@ -97,6 +128,7 @@ export interface CalcRowProps {
|
|
|
97
128
|
/** One label/value row inside the "Rate anpassen" / "Details" expander. */
|
|
98
129
|
export const CalcRow: React.FunctionComponent<CalcRowProps> = ({ label, last, children }) => (
|
|
99
130
|
<div
|
|
131
|
+
className="calcrow"
|
|
100
132
|
style={{
|
|
101
133
|
display: 'flex',
|
|
102
134
|
justifyContent: 'space-between',
|
|
@@ -106,8 +138,8 @@ export const CalcRow: React.FunctionComponent<CalcRowProps> = ({ label, last, ch
|
|
|
106
138
|
borderBottom: last ? 0 : '1px solid var(--md-outline-variant)',
|
|
107
139
|
}}
|
|
108
140
|
>
|
|
109
|
-
<span className="md-label-sm">{label}</span>
|
|
110
|
-
<span className="num" style={{ fontSize: 13.5, fontWeight: 600 }}>
|
|
141
|
+
<span className="md-label-sm calcrow-label">{label}</span>
|
|
142
|
+
<span className="num calcrow-value" style={{ fontSize: 13.5, fontWeight: 600 }}>
|
|
111
143
|
{children}
|
|
112
144
|
</span>
|
|
113
145
|
</div>
|
|
@@ -341,9 +373,12 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
341
373
|
className="num"
|
|
342
374
|
style={{ fontSize: 48, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-2px' }}
|
|
343
375
|
>
|
|
344
|
-
{
|
|
376
|
+
{/* cash keeps the long price number-only (a big "13.249 €" overflows the grid cell) */}
|
|
377
|
+
{isCash ? eur(price) : `${eur(Math.round(monthly))} €`}
|
|
345
378
|
</span>
|
|
346
|
-
{
|
|
379
|
+
{isCash ? (
|
|
380
|
+
<span className="md-body-sm" style={{ fontWeight: 600 }}>€</span>
|
|
381
|
+
) : (
|
|
347
382
|
<span className="fin-rate-unit">
|
|
348
383
|
{/* struck old rate (REAL oldMonthlyInstallment; SRP strikes the same source) */}
|
|
349
384
|
{oldMonthly != null && <s className="num fin-rate-old">{eur(Math.round(oldMonthly))} €</s>}
|
|
@@ -358,7 +393,7 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
358
393
|
</div>
|
|
359
394
|
)}
|
|
360
395
|
</div>
|
|
361
|
-
{/* col 2 row 1: price-rating pill */}
|
|
396
|
+
{/* col 2 row 1: price-rating pill (Figma: bold colored label, NO icon) */}
|
|
362
397
|
<div>
|
|
363
398
|
{ratingLabel && (
|
|
364
399
|
<span
|
|
@@ -366,26 +401,14 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
366
401
|
style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
|
|
367
402
|
aria-label={t('autobahn:finance.priceRatingAria', { rating: evalTierLabel })}
|
|
368
403
|
>
|
|
369
|
-
<Icon
|
|
370
|
-
name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
|
|
371
|
-
size={14}
|
|
372
|
-
stroke={2.6}
|
|
373
|
-
color={TIER_COLORS[evalTier]}
|
|
374
|
-
/>
|
|
375
404
|
{evalTierLabel}
|
|
376
405
|
</span>
|
|
377
406
|
)}
|
|
378
407
|
</div>
|
|
379
408
|
{/* col 1 row 2: condition line */}
|
|
380
409
|
<div className="fin-cond">{condLine}</div>
|
|
381
|
-
{/* col 2 row 2:
|
|
382
|
-
<div>
|
|
383
|
-
{ratingLabel && (
|
|
384
|
-
<div className="fin-eval-bar">
|
|
385
|
-
<div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
|
|
386
|
-
</div>
|
|
387
|
-
)}
|
|
388
|
-
</div>
|
|
410
|
+
{/* col 2 row 2: segmented evaluation bar (Figma: discrete tiers, not a gradient) */}
|
|
411
|
+
<div>{ratingLabel && <EvalBar tier={evalTier} />}</div>
|
|
389
412
|
</div>
|
|
390
413
|
|
|
391
414
|
{/* ============================== DESKTOP (≥601px) — unchanged ============================== */}
|
package/vdp/VdpModules.tsx
CHANGED
|
@@ -27,7 +27,7 @@ import { TranslationFunction } from '../types';
|
|
|
27
27
|
import { Button, Icon, Spec, StatChip, Stars, Switch } from '../ui';
|
|
28
28
|
import { CO2_CLASSES, CO2_COLORS, TIER_LABELS, TIER_KEYS, TIER_COLORS } from '../constants';
|
|
29
29
|
import { defaultVdpSpecs, EquipmentGroups, PriceRatingBands, VehicleDetail, VdpSpec, VdpReview } from './types';
|
|
30
|
-
import { CalcRow, expanderBtnStyle, FinanceMode, Slider, TermChips } from './FinanceCalculator';
|
|
30
|
+
import { CalcRow, EvalBar, expanderBtnStyle, FinanceMode, Slider, TermChips } from './FinanceCalculator';
|
|
31
31
|
|
|
32
32
|
const eur = (n: number): string => n.toLocaleString('de-DE');
|
|
33
33
|
|
|
@@ -102,6 +102,30 @@ export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({
|
|
|
102
102
|
);
|
|
103
103
|
};
|
|
104
104
|
|
|
105
|
+
/** the 6-fact icon grid (km/EZ · Getriebe/Kraftstoff · Hand/Karosserie) — reused by the
|
|
106
|
+
* mobile highlights card, which repeats the vehicle identity per the Figma master
|
|
107
|
+
* ("Vehicle Highlights / Card" carries its own title + facts above the pills). */
|
|
108
|
+
export const VdpFactsGrid: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
|
|
109
|
+
const facts: ReadonlyArray<readonly [string, string]> = [
|
|
110
|
+
['gauge', fmtKm(vehicle.km)],
|
|
111
|
+
['calendar', vehicle.reg],
|
|
112
|
+
['cog', vehicle.gear],
|
|
113
|
+
['fuel', stripParen(vehicle.fuel)],
|
|
114
|
+
['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
|
|
115
|
+
...(vehicle.body ? ([['car-front', vehicle.body]] as const) : []),
|
|
116
|
+
];
|
|
117
|
+
return (
|
|
118
|
+
<div className="vdp-meta" style={{ marginTop: 0 }}>
|
|
119
|
+
{facts.map(([ic, v]) => (
|
|
120
|
+
<span key={ic}>
|
|
121
|
+
<Icon name={ic} size={16} color="var(--md-on-surface-variant)" stroke={1.8} />
|
|
122
|
+
{v}
|
|
123
|
+
</span>
|
|
124
|
+
))}
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
105
129
|
/** hand-picked highlights — the accent rows of the header. ALL highlights are shown
|
|
106
130
|
* (review round 9: "all highlights shown", no 6-chip cap); the Figma pill colors live
|
|
107
131
|
* on .vdp-highlights .chip in the theme. */
|
|
@@ -362,9 +386,9 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
|
|
|
362
386
|
[t('autobahn:eco.stat.co2WeightedCombined'), p.co2w],
|
|
363
387
|
]
|
|
364
388
|
: [
|
|
389
|
+
// Figma: two stats only — the class letter lives on the scale, not as a stat row
|
|
365
390
|
[t('autobahn:eco.stat.consumptionCombined'), vehicle.cons],
|
|
366
391
|
[t('autobahn:eco.stat.co2Emissions'), vehicle.co2],
|
|
367
|
-
[t('autobahn:eco.stat.co2Class'), vehicle.co2class],
|
|
368
392
|
];
|
|
369
393
|
return (
|
|
370
394
|
<div className="mcard" style={{ padding: 22 }}>
|
|
@@ -381,6 +405,10 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
|
|
|
381
405
|
</div>
|
|
382
406
|
))}
|
|
383
407
|
</div>
|
|
408
|
+
{/* scale caption (Figma) — replaces the old CO₂-Klasse stat row + EnVKV footer line */}
|
|
409
|
+
<div className="md-label-sm" style={{ margin: '0 0 8px' }}>
|
|
410
|
+
{t('autobahn:eco.co2ClassScaleLabel')}
|
|
411
|
+
</div>
|
|
384
412
|
<div className="co2-scale">
|
|
385
413
|
{CO2_CLASSES.map((c, i) => (
|
|
386
414
|
<div key={c} className="co2-col">
|
|
@@ -407,17 +435,8 @@ export const Co2Label: React.FunctionComponent<Co2LabelProps> = ({ vehicle, t })
|
|
|
407
435
|
</div>
|
|
408
436
|
))}
|
|
409
437
|
</div>
|
|
410
|
-
{
|
|
411
|
-
|
|
412
|
-
{t('autobahn:eco.co2ClassPrefix')} <b style={{ color: 'var(--md-on-surface)' }}>{p.co2classW}</b>{' '}
|
|
413
|
-
{t('autobahn:eco.weightedCombined')} · <b style={{ color: 'var(--md-on-surface)' }}>{p.co2classD}</b>{' '}
|
|
414
|
-
{t('autobahn:eco.dischargedCombined')} · {t('autobahn:eco.energyClassWltpSuffix')}
|
|
415
|
-
</p>
|
|
416
|
-
) : (
|
|
417
|
-
<p className="md-label-sm" style={{ margin: '10px 0 0' }}>
|
|
418
|
-
{t('autobahn:eco.energyClass')}
|
|
419
|
-
</p>
|
|
420
|
-
)}
|
|
438
|
+
{/* no EnVKV footer line — Figma drops it (the * on the heading refers to the page-footer
|
|
439
|
+
disclaimer instead); the scale caption above carries the WLTP-kombiniert wording */}
|
|
421
440
|
</div>
|
|
422
441
|
);
|
|
423
442
|
};
|
|
@@ -617,28 +636,12 @@ export const PriceHistory: React.FunctionComponent<PriceHistoryProps> = ({ vehic
|
|
|
617
636
|
<div className="md-title-sm" style={{ marginBottom: 12 }}>
|
|
618
637
|
{t('autobahn:marketValue.heading')}
|
|
619
638
|
</div>
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}}
|
|
627
|
-
>
|
|
628
|
-
<span
|
|
629
|
-
style={{
|
|
630
|
-
position: 'absolute',
|
|
631
|
-
top: '50%',
|
|
632
|
-
left: `calc(${mpos * 100}% - 9px)`,
|
|
633
|
-
transform: 'translateY(-50%)',
|
|
634
|
-
width: 18,
|
|
635
|
-
height: 18,
|
|
636
|
-
borderRadius: '50%',
|
|
637
|
-
background: '#fff',
|
|
638
|
-
boxShadow: 'var(--md-e2), 0 0 0 2px var(--md-primary)',
|
|
639
|
-
}}
|
|
640
|
-
/>
|
|
641
|
-
</div>
|
|
639
|
+
{/* segmented tier bar (Figma — discrete colors, not a gradient), continuous marker;
|
|
640
|
+
ring color follows the car's actual rating tone */}
|
|
641
|
+
<EvalBar
|
|
642
|
+
tier={{ top: 0, good: 1, fair: 2, raised: 3, high: 4 }[vehicle.rating as 'top' | 'good' | 'fair' | 'raised' | 'high'] ?? 2}
|
|
643
|
+
leftPct={mpos * 100}
|
|
644
|
+
/>
|
|
642
645
|
<div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 8 }}>
|
|
643
646
|
<span className="md-label-sm">{eur(vehicle.marketLow!)} €</span>
|
|
644
647
|
<span className="md-label-sm">{eur(vehicle.marketHigh!)} €</span>
|
|
@@ -650,13 +653,13 @@ export const PriceHistory: React.FunctionComponent<PriceHistoryProps> = ({ vehic
|
|
|
650
653
|
alignItems: 'center',
|
|
651
654
|
gap: 8,
|
|
652
655
|
marginTop: 12,
|
|
653
|
-
padding: '
|
|
656
|
+
padding: '13px 14px',
|
|
654
657
|
borderRadius: 'var(--md-r-sm)',
|
|
655
658
|
background: 'var(--md-good-container)',
|
|
656
659
|
}}
|
|
657
660
|
>
|
|
658
|
-
<Icon name="
|
|
659
|
-
<span style={{ fontSize:
|
|
661
|
+
<Icon name="check-circle" size={18} color="var(--md-good)" />
|
|
662
|
+
<span style={{ fontSize: 14, fontWeight: 700, color: 'var(--md-good)' }}>
|
|
660
663
|
{t('autobahn:marketValue.belowAverage', { rating: t('autobahn:tier.' + vehicle.rating), amount: eur(delta) })}
|
|
661
664
|
</span>
|
|
662
665
|
</div>
|
|
@@ -720,8 +723,41 @@ const RATING_TIERS: ReadonlyArray<readonly [keyof PriceRatingBands, string]> = [
|
|
|
720
723
|
['highPrice', 'var(--md-error)'],
|
|
721
724
|
];
|
|
722
725
|
|
|
726
|
+
/** collapsible sub-card header (blue icon + bold title + chevron) — Figma round 12. */
|
|
727
|
+
const BreakdownCardHead: React.FunctionComponent<{
|
|
728
|
+
icon: string;
|
|
729
|
+
title: string;
|
|
730
|
+
open: boolean;
|
|
731
|
+
onToggle: () => void;
|
|
732
|
+
}> = ({ icon, title, open, onToggle }) => (
|
|
733
|
+
<button
|
|
734
|
+
type="button"
|
|
735
|
+
onClick={onToggle}
|
|
736
|
+
style={{
|
|
737
|
+
display: 'flex',
|
|
738
|
+
alignItems: 'center',
|
|
739
|
+
gap: 10,
|
|
740
|
+
width: '100%',
|
|
741
|
+
background: 'none',
|
|
742
|
+
border: 0,
|
|
743
|
+
padding: 0,
|
|
744
|
+
cursor: 'pointer',
|
|
745
|
+
font: 'inherit',
|
|
746
|
+
textAlign: 'left',
|
|
747
|
+
}}
|
|
748
|
+
>
|
|
749
|
+
<Icon name={icon} size={19} color="var(--md-primary)" stroke={2} style={{ flex: 'none' }} />
|
|
750
|
+
<span className="md-title-sm" style={{ flex: 1, minWidth: 0, fontSize: 15 }}>
|
|
751
|
+
{title}
|
|
752
|
+
</span>
|
|
753
|
+
<Icon name={open ? 'chevron-up' : 'chevron-down'} size={18} color="var(--md-primary)" style={{ flex: 'none' }} />
|
|
754
|
+
</button>
|
|
755
|
+
);
|
|
756
|
+
|
|
723
757
|
const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t: TranslationFunction }> = ({ vehicle, t }) => {
|
|
724
758
|
const [open, setOpen] = useState(false);
|
|
759
|
+
const [criteriaOpen, setCriteriaOpen] = useState(true);
|
|
760
|
+
const [categoriesOpen, setCategoriesOpen] = useState(true);
|
|
725
761
|
const bands = vehicle.priceRatingBands;
|
|
726
762
|
if (!bands) return null;
|
|
727
763
|
|
|
@@ -752,11 +788,15 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
|
|
|
752
788
|
</button>
|
|
753
789
|
{open && (
|
|
754
790
|
<div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 18 }}>
|
|
755
|
-
<div className="mcard mcard--filled" style={{ padding: 16 }}>
|
|
756
|
-
<
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
791
|
+
<div className="mcard mcard--filled" style={{ padding: 16, flexDirection: 'column' }}>
|
|
792
|
+
<BreakdownCardHead
|
|
793
|
+
icon="list"
|
|
794
|
+
title={t('autobahn:priceMarket.criteriaTitle')}
|
|
795
|
+
open={criteriaOpen}
|
|
796
|
+
onToggle={() => setCriteriaOpen((o) => !o)}
|
|
797
|
+
/>
|
|
798
|
+
{criteriaOpen && (
|
|
799
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 14, marginTop: 14 }}>
|
|
760
800
|
{criteria.map(([ic, label, value]) => (
|
|
761
801
|
<div key={label} style={{ display: 'flex', alignItems: 'flex-start', gap: 9, minWidth: '44%', flex: '1 1 44%' }}>
|
|
762
802
|
<Icon name={ic} size={17} color="var(--md-primary)" stroke={1.8} style={{ marginTop: 1, flex: 'none' }} />
|
|
@@ -771,14 +811,18 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
|
|
|
771
811
|
</div>
|
|
772
812
|
))}
|
|
773
813
|
</div>
|
|
814
|
+
)}
|
|
774
815
|
</div>
|
|
775
816
|
|
|
776
|
-
<div className="mcard mcard--filled" style={{ padding: 16 }}>
|
|
777
|
-
<
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
817
|
+
<div className="mcard mcard--filled" style={{ padding: 16, flexDirection: 'column' }}>
|
|
818
|
+
<BreakdownCardHead
|
|
819
|
+
icon="bar-chart-3"
|
|
820
|
+
title={t('autobahn:priceMarket.categoriesTitle')}
|
|
821
|
+
open={categoriesOpen}
|
|
822
|
+
onToggle={() => setCategoriesOpen((o) => !o)}
|
|
823
|
+
/>
|
|
824
|
+
{categoriesOpen && (
|
|
825
|
+
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 8 }}>
|
|
782
826
|
{RATING_TIERS.map(([key, color], i) => (
|
|
783
827
|
<div
|
|
784
828
|
key={key}
|
|
@@ -824,6 +868,7 @@ const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t:
|
|
|
824
868
|
</p>
|
|
825
869
|
</div>
|
|
826
870
|
</div>
|
|
871
|
+
)}
|
|
827
872
|
</div>
|
|
828
873
|
</div>
|
|
829
874
|
)}
|
|
@@ -1198,17 +1243,9 @@ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
|
|
|
1198
1243
|
className="fin-eval-pill"
|
|
1199
1244
|
style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
|
|
1200
1245
|
>
|
|
1201
|
-
<Icon
|
|
1202
|
-
name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
|
|
1203
|
-
size={13}
|
|
1204
|
-
stroke={2.6}
|
|
1205
|
-
color={TIER_COLORS[evalTier]}
|
|
1206
|
-
/>
|
|
1207
1246
|
{t('autobahn:tier.' + TIER_KEYS[evalTier])}
|
|
1208
1247
|
</span>
|
|
1209
|
-
<
|
|
1210
|
-
<div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
|
|
1211
|
-
</div>
|
|
1248
|
+
<EvalBar tier={evalTier} />
|
|
1212
1249
|
</div>
|
|
1213
1250
|
)}
|
|
1214
1251
|
</div>
|
|
@@ -1308,6 +1345,8 @@ export interface FinanceDetailsModalProps {
|
|
|
1308
1345
|
monthly: number;
|
|
1309
1346
|
/** struck old rate (oldMonthlyInstallment, isStrikeShown-gated) — red line-through next to the rate. */
|
|
1310
1347
|
oldMonthly?: number;
|
|
1348
|
+
/** struck list price (common.highestPrice, isStrikeShown-gated) — cash-tab strike (Figma). */
|
|
1349
|
+
listPrice?: number;
|
|
1311
1350
|
apr: number;
|
|
1312
1351
|
down: number;
|
|
1313
1352
|
term: number;
|
|
@@ -1374,6 +1413,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1374
1413
|
price,
|
|
1375
1414
|
monthly,
|
|
1376
1415
|
oldMonthly,
|
|
1416
|
+
listPrice,
|
|
1377
1417
|
apr,
|
|
1378
1418
|
down,
|
|
1379
1419
|
term,
|
|
@@ -1514,6 +1554,20 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1514
1554
|
</div>
|
|
1515
1555
|
));
|
|
1516
1556
|
|
|
1557
|
+
// mobile sheet footnote — the Figma representative example: real figures inline
|
|
1558
|
+
// ("³ Fahrzeugpreis: … €; Effektiver Jahreszins: … %; …") followed by the PangV sentence.
|
|
1559
|
+
// Only REAL config figures are included; absent ones are simply omitted.
|
|
1560
|
+
const exampleParts: string[] = [
|
|
1561
|
+
`${t('autobahn:modal.exampleVehiclePrice')}: ${eur(price)} € ${t('autobahn:modal.examplePriceSuffix')}`,
|
|
1562
|
+
...(apr > 0 ? [`${t('autobahn:modal.exampleApr')}: ${pct(apr)}`] : []),
|
|
1563
|
+
...(nominalRate != null && nominalRate > 0 ? [`${t('autobahn:modal.exampleNominal')}: ${pct(nominalRate)}`] : []),
|
|
1564
|
+
`${t('autobahn:modal.exampleNet')}: ${eur(netto)} €`,
|
|
1565
|
+
`${t('autobahn:modal.exampleDown')}: ${eur(down)} €`,
|
|
1566
|
+
...(finalInstallment != null && finalInstallment > 0 ? [`${t('autobahn:modal.exampleFinal')}: ${eur(finalInstallment)} €`] : []),
|
|
1567
|
+
...(grossAmount != null && grossAmount > 0 ? [`${t('autobahn:modal.exampleGross')}: ${eur(grossAmount)} €`] : []),
|
|
1568
|
+
];
|
|
1569
|
+
const exampleLine = `³ ${exampleParts.join('; ')};`;
|
|
1570
|
+
|
|
1517
1571
|
return (
|
|
1518
1572
|
<div className="fin-modal-scrim" onClick={onClose}>
|
|
1519
1573
|
<div className="fin-modal" role="dialog" aria-modal="true" aria-label={title} ref={sheetRef} onClick={(e) => e.stopPropagation()}>
|
|
@@ -1625,15 +1679,24 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1625
1679
|
|
|
1626
1680
|
<div className="fin-price-grid">
|
|
1627
1681
|
<div className="fin-rate-main">
|
|
1628
|
-
|
|
1682
|
+
{/* Figma Kaufen sheet: struck list price on its own line ABOVE the big figure */}
|
|
1683
|
+
{isCash && listPrice != null && listPrice > price && (
|
|
1684
|
+
<s className="num fin-rate-old" style={{ display: 'block' }}>{eur(listPrice)} €</s>
|
|
1685
|
+
)}
|
|
1686
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
1629
1687
|
<span className="num" style={{ fontSize: 34, fontWeight: 800, color: 'var(--md-primary)', letterSpacing: '-1px' }}>
|
|
1630
1688
|
{isCash ? eur(price) : eur(Math.round(monthly))} €
|
|
1631
1689
|
</span>
|
|
1632
1690
|
{!isCash && (
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1691
|
+
<>
|
|
1692
|
+
<span className="md-label-sm" style={{ fontWeight: 600, alignSelf: 'flex-end', paddingBottom: 5 }}>
|
|
1693
|
+
{t('autobahn:finance.exampleLabel')}
|
|
1694
|
+
</span>
|
|
1695
|
+
<span className="fin-rate-unit">
|
|
1696
|
+
{oldMonthly != null && <s className="num fin-rate-old">{eur(Math.round(oldMonthly))} €</s>}
|
|
1697
|
+
<span>{t('autobahn:finance.perMonth')}</span>
|
|
1698
|
+
</span>
|
|
1699
|
+
</>
|
|
1637
1700
|
)}
|
|
1638
1701
|
</div>
|
|
1639
1702
|
</div>
|
|
@@ -1643,24 +1706,12 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1643
1706
|
className="fin-eval-pill"
|
|
1644
1707
|
style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
|
|
1645
1708
|
>
|
|
1646
|
-
<Icon
|
|
1647
|
-
name={evalTier <= 1 ? 'check' : evalTier === 2 ? 'minus' : 'trending-up'}
|
|
1648
|
-
size={14}
|
|
1649
|
-
stroke={2.6}
|
|
1650
|
-
color={TIER_COLORS[evalTier]}
|
|
1651
|
-
/>
|
|
1652
1709
|
{evalTierLabel}
|
|
1653
1710
|
</span>
|
|
1654
1711
|
)}
|
|
1655
1712
|
</div>
|
|
1656
1713
|
<div />
|
|
1657
|
-
<div>
|
|
1658
|
-
{ratingLabel && (
|
|
1659
|
-
<div className="fin-eval-bar">
|
|
1660
|
-
<div className="fin-eval-thumb" style={{ left: `${(evalTier + 0.5) * 20}%`, borderColor: TIER_COLORS[evalTier] }} />
|
|
1661
|
-
</div>
|
|
1662
|
-
)}
|
|
1663
|
-
</div>
|
|
1714
|
+
<div>{ratingLabel && <EvalBar tier={evalTier} />}</div>
|
|
1664
1715
|
</div>
|
|
1665
1716
|
|
|
1666
1717
|
{isCash ? (
|
|
@@ -1675,14 +1726,14 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1675
1726
|
{addServicesCost != null && (
|
|
1676
1727
|
<ToggleRow
|
|
1677
1728
|
label={t('autobahn:finance.addServicesToggle')}
|
|
1678
|
-
price={t('autobahn:finance.addServicesOnce', { value: eur(addServicesCost) })}
|
|
1729
|
+
price={t('autobahn:finance.addServicesOnce', { value: eur(Math.round(addServicesCost)) })}
|
|
1679
1730
|
checked={!!withAddServices}
|
|
1680
1731
|
disabled={addServicesDisabled}
|
|
1681
1732
|
onChange={(checked) => onToggleAddServices && onToggleAddServices(checked)}
|
|
1682
1733
|
/>
|
|
1683
1734
|
)}
|
|
1684
1735
|
<div style={{ display: 'flex', gap: 10 }}>
|
|
1685
|
-
<Button variant="filled" size="lg" icon="
|
|
1736
|
+
<Button variant="filled" size="lg" icon="file-check" style={{ flex: 1 }} onClick={onSecure}>
|
|
1686
1737
|
{t('autobahn:finance.secureOffer')}
|
|
1687
1738
|
</Button>
|
|
1688
1739
|
{/* padding:0 — mbtn--lg's 30px side padding left no room for the icon at width 52 */}
|
|
@@ -1703,7 +1754,6 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1703
1754
|
{eur(down)} €
|
|
1704
1755
|
</span>
|
|
1705
1756
|
</div>
|
|
1706
|
-
<span className="fin-val-box num">{eur(down)} €</span>
|
|
1707
1757
|
<Slider
|
|
1708
1758
|
min={downMin as number}
|
|
1709
1759
|
max={downMax as number}
|
|
@@ -1711,6 +1761,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1711
1761
|
value={down}
|
|
1712
1762
|
onChange={onDownChange as (n: number) => void}
|
|
1713
1763
|
fmt={(v) => `${eur(Number(v))} €`}
|
|
1764
|
+
bubble
|
|
1714
1765
|
/>
|
|
1715
1766
|
</div>
|
|
1716
1767
|
{isLease && annualMileage != null && onAnnualMileageChange && (
|
|
@@ -1724,7 +1775,6 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1724
1775
|
{eur(annualMileage)} km
|
|
1725
1776
|
</span>
|
|
1726
1777
|
</div>
|
|
1727
|
-
<span className="fin-val-box num">{eur(annualMileage)} km</span>
|
|
1728
1778
|
<Slider
|
|
1729
1779
|
min={annualMileageMin}
|
|
1730
1780
|
max={annualMileageMax}
|
|
@@ -1732,6 +1782,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1732
1782
|
value={annualMileage}
|
|
1733
1783
|
onChange={onAnnualMileageChange}
|
|
1734
1784
|
fmt={(v) => `${eur(Number(v))} km`}
|
|
1785
|
+
bubble
|
|
1735
1786
|
/>
|
|
1736
1787
|
</div>
|
|
1737
1788
|
)}
|
|
@@ -1755,7 +1806,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1755
1806
|
{addServicesCost != null && onToggleAddServices && (
|
|
1756
1807
|
<ToggleRow
|
|
1757
1808
|
label={t('autobahn:finance.addServicesToggle')}
|
|
1758
|
-
price={t('autobahn:finance.addServicesPerMonth', { value: eur(addServicesCost) })}
|
|
1809
|
+
price={t('autobahn:finance.addServicesPerMonth', { value: eur(Math.round(addServicesCost)) })}
|
|
1759
1810
|
checked={!!withAddServices}
|
|
1760
1811
|
disabled={addServicesDisabled}
|
|
1761
1812
|
onChange={onToggleAddServices}
|
|
@@ -1763,7 +1814,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1763
1814
|
)}
|
|
1764
1815
|
|
|
1765
1816
|
<div style={{ display: 'flex', gap: 10 }}>
|
|
1766
|
-
<Button variant="filled" size="lg" icon="
|
|
1817
|
+
<Button variant="filled" size="lg" icon="file-check" style={{ flex: 1 }} onClick={onSecure}>
|
|
1767
1818
|
{t('autobahn:finance.requestRate')}
|
|
1768
1819
|
</Button>
|
|
1769
1820
|
{/* padding:0 — mbtn--lg's 30px side padding left no room for the icon at width 52 */}
|
|
@@ -1777,11 +1828,13 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1777
1828
|
{isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails')}
|
|
1778
1829
|
</span>
|
|
1779
1830
|
</button>
|
|
1780
|
-
{/* mobile mockup shows NO row-by-row breakdown table (round 9 review) —
|
|
1781
|
-
|
|
1831
|
+
{/* mobile mockup shows NO row-by-row breakdown table (round 9 review) — the Figma
|
|
1832
|
+
footnote: real figures inline, then the PangV sentence (round 12). */}
|
|
1782
1833
|
{showBreakdown && (
|
|
1783
1834
|
<div>
|
|
1784
1835
|
<p className="md-label-sm" style={{ margin: 0, lineHeight: 1.55 }}>
|
|
1836
|
+
{exampleLine}
|
|
1837
|
+
<br />
|
|
1785
1838
|
{isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
|
|
1786
1839
|
</p>
|
|
1787
1840
|
</div>
|
|
@@ -1840,11 +1893,13 @@ export const ReviewsTeaser: React.FunctionComponent<ReviewsTeaserProps> = ({ cou
|
|
|
1840
1893
|
}}
|
|
1841
1894
|
>
|
|
1842
1895
|
<span style={{ minWidth: 0 }}>
|
|
1843
|
-
<span style={{ display: 'inline-flex', alignItems: 'center', gap:
|
|
1844
|
-
<Stars value={average} size={
|
|
1845
|
-
<span className="num
|
|
1896
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 9 }}>
|
|
1897
|
+
<Stars value={average} size={19} />
|
|
1898
|
+
<span className="num" style={{ fontWeight: 800, fontSize: 20, color: 'var(--fig-navy)' }}>
|
|
1899
|
+
{average.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })}/5
|
|
1900
|
+
</span>
|
|
1846
1901
|
</span>
|
|
1847
|
-
<span
|
|
1902
|
+
<span style={{ display: 'block', marginTop: 4, fontSize: 14.5, color: 'var(--fig-navy)' }}>
|
|
1848
1903
|
{t('autobahn:reviews.teaserCount', { count })}
|
|
1849
1904
|
</span>
|
|
1850
1905
|
</span>
|
|
@@ -2036,14 +2091,13 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2036
2091
|
const emailOk = /.+@.+\..+/.test(email);
|
|
2037
2092
|
const phoneDigits = (areaCode + phoneNum).replace(/\D/g, '');
|
|
2038
2093
|
const phoneOk = phoneDigits.length >= 6;
|
|
2039
|
-
// Figma round
|
|
2040
|
-
//
|
|
2094
|
+
// Figma round 12: the required-* follows the chosen contact channel — E-Mail* when email is
|
|
2095
|
+
// preferred, Telefonnummer* when phone is; names + consent are always required.
|
|
2041
2096
|
const valid =
|
|
2042
2097
|
firstName.trim().length > 0 &&
|
|
2043
2098
|
lastName.trim().length > 0 &&
|
|
2044
|
-
emailOk &&
|
|
2045
2099
|
consent &&
|
|
2046
|
-
(pref
|
|
2100
|
+
(pref === 'phone' ? phoneOk : emailOk);
|
|
2047
2101
|
|
|
2048
2102
|
const submit = async (): Promise<void> => {
|
|
2049
2103
|
if (!valid || sending) return;
|
|
@@ -2159,7 +2213,8 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2159
2213
|
<input
|
|
2160
2214
|
style={enquiryInput}
|
|
2161
2215
|
type="email"
|
|
2162
|
-
|
|
2216
|
+
// the required-* follows the preferred channel (Figma round 12)
|
|
2217
|
+
placeholder={pref === 'email' ? t('autobahn:enquiry.form.emailPlaceholder') : t('autobahn:enquiry.form.emailPlaceholder').replace(/\*$/, '')}
|
|
2163
2218
|
value={email}
|
|
2164
2219
|
onChange={(e) => setEmail(e.target.value)}
|
|
2165
2220
|
aria-label={t('autobahn:enquiry.contactMethod.email')}
|
|
@@ -2181,7 +2236,7 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2181
2236
|
aria-label={t('autobahn:enquiry.form.areaCodeLabel')}
|
|
2182
2237
|
/>
|
|
2183
2238
|
</FloatField>
|
|
2184
|
-
<FloatField label={t('autobahn:enquiry.form.phoneNumberLabel')}>
|
|
2239
|
+
<FloatField label={t('autobahn:enquiry.form.phoneNumberLabel') + (pref === 'phone' ? '*' : '')}>
|
|
2185
2240
|
<input
|
|
2186
2241
|
type="tel"
|
|
2187
2242
|
value={phoneNum}
|
|
@@ -2202,7 +2257,7 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
|
|
|
2202
2257
|
{/* Nutzungsbedingungen sentence + revocable-consent checkbox (Figma round 9) */}
|
|
2203
2258
|
<p className="md-body-sm" style={{ margin: '14px 0 0' }}>
|
|
2204
2259
|
{t('autobahn:enquiry.form.termsPrefix')}{' '}
|
|
2205
|
-
<a href="/nutzungsbedingungen/" target="_blank" rel="noopener noreferrer" style={{ color: 'inherit' }}>
|
|
2260
|
+
<a href="/nutzungsbedingungen/" target="_blank" rel="noopener noreferrer" style={{ color: 'inherit', textDecoration: 'underline' }}>
|
|
2206
2261
|
{t('autobahn:enquiry.form.termsLink')}
|
|
2207
2262
|
</a>{' '}
|
|
2208
2263
|
{t('autobahn:enquiry.form.termsSuffix')}
|
|
@@ -2286,8 +2341,11 @@ export const HowItWorks: React.FunctionComponent<HowItWorksProps> = ({ steps, t
|
|
|
2286
2341
|
<Icon name={STEP_ICONS[i] || 'star'} size={24} color="#fff" stroke={1.8} />
|
|
2287
2342
|
<span className="hiw-num">{i + 1}</span>
|
|
2288
2343
|
</div>
|
|
2289
|
-
|
|
2290
|
-
<
|
|
2344
|
+
{/* title stacked ABOVE the description (Figma) — one text column beside the circle */}
|
|
2345
|
+
<div className="hiw-body">
|
|
2346
|
+
<div className="hiw-title">{title}</div>
|
|
2347
|
+
<p className="hiw-desc">{desc}</p>
|
|
2348
|
+
</div>
|
|
2291
2349
|
</div>
|
|
2292
2350
|
))}
|
|
2293
2351
|
</div>
|