@bytebrand/fe-ui-core-autobahn 1.0.73 → 1.0.75

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.
@@ -1484,10 +1484,17 @@ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
1484
1484
  onOpenAdjust,
1485
1485
  t,
1486
1486
  }) => {
1487
+ const [uvpTooltipOpen, setUvpTooltipOpen] = useState(false);
1487
1488
  const isCash = mode === 'cash';
1488
1489
  const rate = monthly != null ? monthly : vehicle.monthly;
1489
1490
  const tierIdx = TIER_LABELS.indexOf(ratingLabel as (typeof TIER_LABELS)[number]);
1490
1491
  const evalTier = tierIdx >= 0 ? tierIdx : 1;
1492
+ // "good" collapses into the same "Top-Preis" label as the actual top tier (superscript marks
1493
+ // the collapse) — identical convention to CarCard's PriceEvalCol / FinanceCalculator's pill.
1494
+ const isGoodTier = TIER_KEYS[evalTier] === 'good';
1495
+ const evalTierLabel = isGoodTier ? t('autobahn:tier.top') : t('autobahn:tier.' + TIER_KEYS[evalTier]);
1496
+ const evalPillColor = isGoodTier ? TIER_COLORS[0] : TIER_COLORS[evalTier];
1497
+ const evalPillBackground = isGoodTier ? TIER_BACKGROUNDS[0] : TIER_BACKGROUNDS[evalTier];
1491
1498
  const priceStyle: React.CSSProperties = isCash
1492
1499
  ? {
1493
1500
  fontFamily: 'Arial, Helvetica, sans-serif',
@@ -1534,43 +1541,84 @@ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
1534
1541
  <div className="bb-price-row">
1535
1542
  <div style={{ display: 'flex', flexDirection: 'column', minWidth: 0, flex: 1 }}>
1536
1543
  {isCash && vehicle.listPrice != null && vehicle.listPrice > vehicle.price && (
1537
- <s className="num fin-rate-old fin-rate-old-bb" style={{ alignSelf: 'flex-end', paddingBottom: 0, fontSize: 15 }}>
1538
- {eur(vehicle.listPrice)} €<sup>2</sup>
1544
+ <s
1545
+ className="num fin-rate-old fin-rate-old-bb"
1546
+ style={{ alignSelf: 'flex-end', paddingBottom: 0, fontSize: 15 }}
1547
+ role="button"
1548
+ tabIndex={0}
1549
+ onClick={(e) => {
1550
+ e.stopPropagation();
1551
+ setUvpTooltipOpen((v) => !v);
1552
+ }}
1553
+ >
1554
+ {eur(vehicle.listPrice)} €
1555
+ <span className="pp-eyebrow-badge">2</span>
1556
+ {uvpTooltipOpen && (
1557
+ <div className="pp-uvp-tooltip" onClick={(e) => e.stopPropagation()}>
1558
+ <button
1559
+ type="button"
1560
+ className="pp-uvp-tooltip-close"
1561
+ aria-label={t('autobahn:modal.close')}
1562
+ onClick={(e) => {
1563
+ e.stopPropagation();
1564
+ setUvpTooltipOpen(false);
1565
+ }}
1566
+ >
1567
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
1568
+ <path d="M5 5L15 15M15 5L5 15" stroke="white" strokeWidth={2.4} strokeLinecap="round" />
1569
+ </svg>
1570
+ </button>
1571
+ <div className="pp-uvp-tooltip-body">{t('autobahn:card.price.uvpListTooltipBody')}</div>
1572
+ </div>
1573
+ )}
1539
1574
  </s>
1540
1575
  )}
1541
1576
  <div style={{ display: 'flex', alignItems: 'center', whiteSpace: 'nowrap', minWidth: 0 }}>
1542
1577
  <span className={'num' + priceLenClass} style={priceStyle}>
1543
1578
  {isCash ? eur(vehicle.price) : eur(Math.round(rate))}
1544
1579
  </span>
1545
- {/* Figma sticky footer: tiny "z.B." between the amount and the €, then the struck old
1546
- rate stacked ABOVE "/mtl." */}
1547
- {!isCash && (
1548
- <span
1549
- className="md-label-sm"
1550
- style={{
1551
- fontFamily: 'Arial, Helvetica, sans-serif',
1552
- fontWeight: 400,
1553
- fontSize: 9,
1554
- lineHeight: '10px',
1555
- color: '#5B6B7F',
1556
- alignSelf: 'flex-end',
1557
- paddingBottom: 10,
1558
- }}
1559
- >
1560
- {t('autobahn:finance.exampleLabel')}
1561
- </span>
1562
- )}
1563
- <span className={'num' + priceLenClass} style={priceStyle}>
1580
+ <span className={'num' + priceLenClass} style={{ ...priceStyle, marginLeft: 10 }}>
1564
1581
  €
1565
1582
  </span>
1566
1583
  {isCash && <sup className="bb-price-cash-sup">1</sup>}
1567
1584
  {/* <span className="md-label-sm" style={{ fontWeight: 600 }}>{t('autobahn:finance.fixedPrice')}</span> */}
1568
1585
  {!isCash && (
1569
- <span className="fin-rate-unit" style={{ fontSize: 12.5, paddingLeft: 10 }}>
1586
+ <span className="fin-rate-unit" style={{ fontSize: 12.5, paddingLeft: 15 }}>
1570
1587
  {oldMonthly != null && (
1571
- <s className="num fin-rate-old fin-rate-old-bb">
1572
- {eur(Math.round(oldMonthly))} €<sup>2</sup>
1573
- </s>
1588
+ <span
1589
+ style={{ display: 'contents' }}
1590
+ role="button"
1591
+ tabIndex={0}
1592
+ onClick={() => setUvpTooltipOpen((v) => !v)}
1593
+ >
1594
+ <span className="pp-eyebrow">
1595
+ {t('autobahn:card.price.uvpRateLabel')}
1596
+ <span className="pp-eyebrow-badge">3</span>
1597
+ {uvpTooltipOpen && (
1598
+ <div className="pp-uvp-tooltip" onClick={(e) => e.stopPropagation()}>
1599
+ <button
1600
+ type="button"
1601
+ className="pp-uvp-tooltip-close"
1602
+ aria-label={t('autobahn:modal.close')}
1603
+ onClick={(e) => {
1604
+ e.stopPropagation();
1605
+ setUvpTooltipOpen(false);
1606
+ }}
1607
+ >
1608
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
1609
+ <path d="M5 5L15 15M15 5L5 15" stroke="white" strokeWidth={2.4} strokeLinecap="round" />
1610
+ </svg>
1611
+ </button>
1612
+ <div className="pp-uvp-tooltip-body">
1613
+ {t(mode === 'leasing' ? 'autobahn:card.price.uvpRateTooltipBodyLeasing' : 'autobahn:card.price.uvpRateTooltipBody')}
1614
+ </div>
1615
+ </div>
1616
+ )}
1617
+ </span>
1618
+ <s className="num fin-rate-old fin-rate-old-bb">
1619
+ {eur(Math.round(oldMonthly))} €
1620
+ </s>
1621
+ </span>
1574
1622
  )}
1575
1623
  <span
1576
1624
  style={{
@@ -1582,20 +1630,6 @@ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
1582
1630
  }}
1583
1631
  >
1584
1632
  {t('autobahn:bottom.perMonth').replace(/^\/\s*/, '/ ')}
1585
- <sup
1586
- style={{
1587
- position: 'relative',
1588
- top: -4,
1589
- verticalAlign: 'super',
1590
- fontFamily: 'Arial, Helvetica, sans-serif',
1591
- fontWeight: 400,
1592
- fontSize: 8,
1593
- lineHeight: '9px',
1594
- color: '#5B6B7F',
1595
- }}
1596
- >
1597
- 3
1598
- </sup>
1599
1633
  </span>
1600
1634
  </span>
1601
1635
  )}
@@ -1620,11 +1654,12 @@ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
1620
1654
  lineHeight: '14px',
1621
1655
  textAlign: 'center',
1622
1656
  borderRadius: 11,
1623
- color: TIER_COLORS[evalTier],
1624
- background: TIER_BACKGROUNDS[evalTier],
1657
+ color: evalPillColor,
1658
+ background: evalPillBackground,
1625
1659
  }}
1626
1660
  >
1627
- {t('autobahn:tier.' + TIER_KEYS[evalTier])}
1661
+ {evalTierLabel}
1662
+ {isGoodTier && <sup>4</sup>}
1628
1663
  </span>
1629
1664
  <EvalBar tier={evalTier} />
1630
1665
  </div>
@@ -1751,19 +1786,27 @@ const ToggleRow: React.FunctionComponent<{
1751
1786
  disabled?: boolean;
1752
1787
  onChange: (checked: boolean) => void;
1753
1788
  className?: string;
1754
- }> = ({ label, price, checked, disabled, onChange, className }) => (
1755
- <div className={className} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 10 }}>
1789
+ /** cash mode: the package is always included — shows a static "inklusive" note instead of the switch. */
1790
+ staticNote?: string;
1791
+ }> = ({ label, price, checked, disabled, onChange, className, staticNote }) => (
1792
+ <div className={className} style={{ display: 'flex', alignItems: 'center', justifyContent: staticNote ? undefined : 'space-between', gap: 10 }}>
1756
1793
  <span className="md-title-sm" style={{ display: 'inline-flex', alignItems: 'center', gap: 5 }}>
1757
1794
  {label}
1758
1795
  <Icon name="info" size={13} color="var(--md-on-surface-variant)" />
1759
1796
  </span>
1760
- <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
1797
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginLeft: staticNote ? 'auto' : undefined }}>
1761
1798
  {price && (
1762
- <span className="num" style={{ fontWeight: 700, fontSize: 13.5 }}>
1799
+ <span className="num" style={{ fontWeight: 700, fontSize: 13.5, marginRight: staticNote ? 'auto' : undefined }}>
1763
1800
  {price}
1764
1801
  </span>
1765
1802
  )}
1766
- <Switch checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} ariaLabel={label} />
1803
+ {staticNote ? (
1804
+ <span style={{ fontFamily: 'Arial', fontWeight: 400, fontSize: 15, lineHeight: '17px', color: '#0E2238' }}>
1805
+ {staticNote}
1806
+ </span>
1807
+ ) : (
1808
+ <Switch checked={checked} disabled={disabled} onChange={(e) => onChange(e.target.checked)} ariaLabel={label} />
1809
+ )}
1767
1810
  </div>
1768
1811
  </div>
1769
1812
  );
@@ -1816,6 +1859,9 @@ export interface FinanceDetailsModalProps {
1816
1859
  initialShowBreakdown?: boolean;
1817
1860
  /** mobile only — price-rating label, drives the eval pill/bar (same as the collapsed card). */
1818
1861
  ratingLabel?: string;
1862
+ /** Kaufen tab only — real carStore.isAvailable (same source as VdpTrustRow's "Sofort
1863
+ * verfügbar" badge); drives the Verfügbarkeit row that replaced the static Lieferung row. */
1864
+ available?: boolean;
1819
1865
  /** mobile only — mode tabs inside the sheet (mirrors the collapsed card's tabs). */
1820
1866
  onModeChange?: (mode: FinanceMode) => void;
1821
1867
  /** mobile only — primary "Rate anfragen"/"Angebot anfragen" CTA. */
@@ -1828,6 +1874,10 @@ export interface FinanceDetailsModalProps {
1828
1874
  /** mobile only — "Rundum-Sorglos-Paket" toggle (all modes; forced-on + disabled for cash). */
1829
1875
  withAddServices?: boolean;
1830
1876
  addServicesCost?: number;
1877
+ /** flat one-time package price (getFinancing/getLeasing's addServicesCostRaw) — addServicesCost
1878
+ * above is the MONTHLY slice shown next to the toggle ("+15 €/Monat"); the breakdown's
1879
+ * "Rundum-Sorglos-Paket (einmalig)" row needs the real flat price instead. */
1880
+ addServicesCostRaw?: number;
1831
1881
  addServicesDisabled?: boolean;
1832
1882
  onToggleAddServices?: (checked: boolean) => void;
1833
1883
  /** i18n function (drilled from the page). */
@@ -1872,6 +1922,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1872
1922
  onClose,
1873
1923
  initialShowBreakdown,
1874
1924
  ratingLabel,
1925
+ available,
1875
1926
  onModeChange,
1876
1927
  onSecure,
1877
1928
  onCall,
@@ -1879,6 +1930,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1879
1930
  onToggleFinalInstallment,
1880
1931
  withAddServices,
1881
1932
  addServicesCost,
1933
+ addServicesCostRaw,
1882
1934
  addServicesDisabled,
1883
1935
  onToggleAddServices,
1884
1936
  t,
@@ -1887,8 +1939,25 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1887
1939
  // anpassen") or expanded (opened directly via the details link) — reset on every open, since
1888
1940
  // this component stays mounted (the page always renders it, gated by `open`, not unmounted).
1889
1941
  const [showBreakdown, setShowBreakdown] = React.useState(!!initialShowBreakdown);
1942
+ // same "UVP-Rate"/list-price footnote badge + popover as FinanceCalculator's own hero — this
1943
+ // modal's mobile "Rate anpassen" sheet renders its own separate copy of that hero row, so it
1944
+ // needs its own tooltip-open state (not shared with FinanceCalculator's instance).
1945
+ const [uvpTooltipOpen, setUvpTooltipOpen] = React.useState(false);
1946
+ // opened directly via the "Finanzierungsdetails" link (initialShowBreakdown) — scroll the
1947
+ // modal's own scroll container down to the breakdown table instead of leaving the user at
1948
+ // the top (desktop/mobile render it in separate hide-mobile/show-mobile branches, so try both
1949
+ // refs and scroll whichever one is actually laid out at the current breakpoint).
1950
+ const desktopBreakdownRef = React.useRef<HTMLDivElement | null>(null);
1951
+ const mobileBreakdownRef = React.useRef<HTMLDivElement | null>(null);
1890
1952
  React.useEffect(() => {
1891
- if (open) setShowBreakdown(!!initialShowBreakdown);
1953
+ if (!open) return;
1954
+ setShowBreakdown(!!initialShowBreakdown);
1955
+ if (!initialShowBreakdown) return;
1956
+ const timer = window.setTimeout(() => {
1957
+ const target = [desktopBreakdownRef.current, mobileBreakdownRef.current].find((el) => el && el.offsetParent !== null);
1958
+ target?.scrollIntoView({ behavior: 'smooth', block: 'start' });
1959
+ }, 50);
1960
+ return () => window.clearTimeout(timer);
1892
1961
  // eslint-disable-next-line react-hooks/exhaustive-deps
1893
1962
  }, [open, initialShowBreakdown]);
1894
1963
 
@@ -1963,31 +2032,80 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1963
2032
  // Grand total (Figma "Gesamtbelastung inkl. Anzahlung & Paket") — down payment + the config's
1964
2033
  // real Gesamtbetrag + the package fee. Only shown when Gesamtbetrag is known (never fabricated).
1965
2034
  const totalCost = grossAmount != null && grossAmount > 0 ? down + grossAmount + (hasAddServices ? (addServicesCost as number) : 0) : undefined;
1966
- const rows: Array<[string, string, boolean?]> = [
1967
- [t('autobahn:modal.vehiclePrice'), `${eur(price)} €`],
1968
- [t('autobahn:modal.term'), t('autobahn:modal.months', { months: term, count: term })],
1969
- [t('autobahn:modal.downPayment'), `${eur(down)} €`],
1970
- ...(isLease && annualMileage != null ? ([[t('autobahn:modal.annualMileage'), `${eur(annualMileage)} km`]] as Array<[string, string]>) : []),
1971
- [
1972
- isLease ? t('autobahn:modal.leasingRateCount', { count: term }) : t('autobahn:modal.monthlyRateCount', { count: term }),
1973
- `${eur(monthly)} €`,
1974
- ],
1975
- ...(finalInstallment != null && finalInstallment > 0 ? ([[t('autobahn:modal.finalInstallment'), `${eur(finalInstallment)} €`]] as Array<[string, string]>) : []),
1976
- ...(hasAddServices ? ([[t('autobahn:modal.addServicesRow'), `${eur(addServicesCost as number)} €`]] as Array<[string, string]>) : []),
1977
- ...(nominalRate != null && nominalRate > 0 ? ([[t('autobahn:modal.nominalRate'), pct(nominalRate)]] as Array<[string, string]>) : []),
1978
- [t('autobahn:modal.effectiveRate'), pct(apr)],
1979
- [isLease ? t('autobahn:modal.leasingAmount') : t('autobahn:modal.netLoanAmount'), `${eur(netto)} €`],
1980
- ...(grossAmount != null && grossAmount > 0 ? ([[isLease ? t('autobahn:modal.totalAmount') : t('autobahn:modal.totalRepayment'), `${eur(grossAmount)} €`]] as Array<[string, string]>) : []),
1981
- ...(totalCost != null
1982
- ? ([[hasAddServices ? t('autobahn:modal.totalCostWithPackage') : t('autobahn:modal.totalCost'), `${eur(totalCost)} €`, true]] as Array<
1983
- [string, string, boolean]
1984
- >)
1985
- : []),
1986
- ...(listPrice != null && listPrice > 0 ? ([[t('autobahn:modal.listPrice'), `${eur(listPrice)} €`]] as Array<[string, string]>) : []),
1987
- ];
2035
+ // one-time package figure shown in the breakdown: 0 € while bundled into the rate (its cost is
2036
+ // already carried inside `monthly` instead), the real flat price when switched off.
2037
+ const addServicesRowValue = withAddServices ? 0 : addServicesCostRaw ?? 0;
2038
+ const hasAddServicesRow = addServicesCostRaw != null && addServicesCostRaw > 0;
2039
+
2040
+ let rows: Array<[string, string, boolean?]>;
2041
+ if (isLease) {
2042
+ // Leasing's own "Repräsentatives Beispiel" row set (Figma leasing table — NOT the financing
2043
+ // layout): Anschaffungspreis → Vertragslaufzeit → Laufleistung/Jahr → Sonderzahlung →
2044
+ // "N Monatsraten à" → Schlusszahlung (Restwertleasing has no scheduled final payment, so this
2045
+ // disclosure line is always 0 €) → Restwert (verpflichtend) → Σ Leasingraten (bold) →
2046
+ // Sonderzahlung again → Rundum-Sorglos-Paket → Zwischensumme inkl. Paket (bold) → Restwert
2047
+ // again → Gesamt inkl. Restwert (bold). The residual value (Restwert) is already folded into
2048
+ // `grossAmount` (getLeasing's grossLoanAmount = grossLoanAmountWithoutFinalInstallment +
2049
+ // finalInstallment) — grossAmount minus the residual gives the pure Σ Leasingraten, and the
2050
+ // final grand total is mathematically identical to the financing total's own
2051
+ // down + grossAmount + package formula, just built up in the two visible stages Figma shows.
2052
+ const residual = finalInstallment != null && finalInstallment > 0 ? finalInstallment : 0;
2053
+ const ratesSum = grossAmount != null && grossAmount > 0 ? grossAmount - residual : undefined;
2054
+ const subtotalWithPackage = ratesSum != null ? ratesSum + down + (hasAddServicesRow ? addServicesRowValue : 0) : undefined;
2055
+ const grandTotal = subtotalWithPackage != null ? subtotalWithPackage + residual : undefined;
2056
+ rows = [
2057
+ [t('autobahn:modal.leaseVehiclePrice'), `${eur(price)} €`],
2058
+ [t('autobahn:modal.term'), t('autobahn:modal.months', { months: term, count: term })],
2059
+ ...(annualMileage != null ? ([[t('autobahn:modal.annualMileage'), `${eur(annualMileage)} km`]] as Array<[string, string]>) : []),
2060
+ [t('autobahn:modal.leaseDownPayment'), `${eur(down)} €`],
2061
+ [t('autobahn:modal.leasingRateCount', { count: term }), `${eur(monthly)} €`],
2062
+ [t('autobahn:modal.finalPaymentDue'), `${eur(0)} €`],
2063
+ ...(residual > 0 ? ([[t('autobahn:modal.residualValue'), `${eur(residual)} €`]] as Array<[string, string]>) : []),
2064
+ ...(ratesSum != null
2065
+ ? ([[t('autobahn:modal.rateSumLabel', { term, amount: eur(monthly) }), `${eur(ratesSum)} €`, true]] as Array<[string, string, boolean]>)
2066
+ : []),
2067
+ ...(ratesSum != null ? ([[t('autobahn:modal.leaseDownPayment'), `${eur(down)} €`]] as Array<[string, string]>) : []),
2068
+ ...(ratesSum != null && hasAddServicesRow
2069
+ ? ([[t('autobahn:modal.addServicesRowLease'), `${eur(addServicesRowValue)} €`]] as Array<[string, string]>)
2070
+ : []),
2071
+ ...(subtotalWithPackage != null
2072
+ ? ([[hasAddServicesRow ? t('autobahn:modal.subtotalWithPackage') : t('autobahn:modal.subtotalNoPackage'), `${eur(subtotalWithPackage)} €`, true]] as Array<
2073
+ [string, string, boolean]
2074
+ >)
2075
+ : []),
2076
+ ...(residual > 0 && grandTotal != null ? ([[t('autobahn:modal.residualValue'), `${eur(residual)} €`]] as Array<[string, string]>) : []),
2077
+ ...(grandTotal != null
2078
+ ? ([[t('autobahn:modal.grandTotalWithResidual'), `${eur(grandTotal)} €`, true]] as Array<[string, string, boolean]>)
2079
+ : []),
2080
+ ...(listPrice != null && listPrice > 0 ? ([[t('autobahn:modal.listPriceLease'), `${eur(listPrice)} €`]] as Array<[string, string]>) : []),
2081
+ ];
2082
+ } else {
2083
+ rows = [
2084
+ [t('autobahn:modal.vehiclePrice'), `${eur(price)} €`],
2085
+ [t('autobahn:modal.term'), t('autobahn:modal.months', { months: term, count: term })],
2086
+ [t('autobahn:modal.downPayment'), `${eur(down)} €`],
2087
+ [t('autobahn:modal.monthlyRateCount', { count: term }), `${eur(monthly)} €`],
2088
+ ...(finalInstallment != null && finalInstallment > 0 ? ([[t('autobahn:modal.finalInstallment'), `${eur(finalInstallment)} €`]] as Array<[string, string]>) : []),
2089
+ ...(hasAddServicesRow
2090
+ ? ([[t('autobahn:modal.addServicesRow'), `${eur(addServicesRowValue)} €`]] as Array<[string, string]>)
2091
+ : []),
2092
+ ...(nominalRate != null && nominalRate > 0 ? ([[t('autobahn:modal.nominalRate'), pct(nominalRate)]] as Array<[string, string]>) : []),
2093
+ [t('autobahn:modal.effectiveRate'), pct(apr)],
2094
+ [t('autobahn:modal.netLoanAmount'), `${eur(netto)} €`],
2095
+ ...(grossAmount != null && grossAmount > 0 ? ([[t('autobahn:modal.totalRepayment'), `${eur(grossAmount)} €`]] as Array<[string, string]>) : []),
2096
+ ...(totalCost != null
2097
+ ? ([[hasAddServices ? t('autobahn:modal.totalCostWithPackage') : t('autobahn:modal.totalCost'), `${eur(totalCost)} €`, true]] as Array<
2098
+ [string, string, boolean]
2099
+ >)
2100
+ : []),
2101
+ ...(listPrice != null && listPrice > 0 ? ([[t('autobahn:modal.listPrice'), `${eur(listPrice)} €`]] as Array<[string, string]>) : []),
2102
+ ];
2103
+ }
1988
2104
  const breakdownRows = rows.map(([l, v, bold], i) => (
1989
2105
  <div
1990
- key={l}
2106
+ // leasing repeats "Restwert (verpflichtend)" / "Einmalige Sonderzahlung" (Figma's own
2107
+ // subtotal-buildup layout) — the label text alone is no longer a unique React key.
2108
+ key={`${l}-${i}`}
1991
2109
  style={{
1992
2110
  display: 'flex',
1993
2111
  alignItems: 'baseline',
@@ -2111,7 +2229,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2111
2229
  </div>
2112
2230
  </div>
2113
2231
  )}
2114
- {breakdownRows}
2232
+ <div ref={desktopBreakdownRef}>{breakdownRows}</div>
2115
2233
  <p className="md-label-sm" style={{ marginTop: 16, lineHeight: 1.55 }}>
2116
2234
  {isLease ? t('autobahn:modal.legalLeasing') : t('autobahn:modal.legalFinancing')}
2117
2235
  </p>
@@ -2150,7 +2268,36 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2150
2268
  {isCash && listPrice != null && listPrice > price && (
2151
2269
  <div style={{ display: 'flex', justifyContent: 'flex-end', lineHeight: 1, marginTop: 0 }}>
2152
2270
  <s className="num fin-rate-old fin-rate-old-list">
2153
- {eur(listPrice)} €<sup>2</sup>
2271
+ {eur(listPrice)} €
2272
+ <span
2273
+ className="pp-eyebrow-badge"
2274
+ role="button"
2275
+ tabIndex={0}
2276
+ onClick={(e) => {
2277
+ e.stopPropagation();
2278
+ setUvpTooltipOpen((v) => !v);
2279
+ }}
2280
+ >
2281
+ 2
2282
+ </span>
2283
+ {uvpTooltipOpen && (
2284
+ <div className="pp-uvp-tooltip" onClick={(e) => e.stopPropagation()}>
2285
+ <button
2286
+ type="button"
2287
+ className="pp-uvp-tooltip-close"
2288
+ aria-label={t('autobahn:modal.close')}
2289
+ onClick={(e) => {
2290
+ e.stopPropagation();
2291
+ setUvpTooltipOpen(false);
2292
+ }}
2293
+ >
2294
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2295
+ <path d="M5 5L15 15M15 5L5 15" stroke="white" strokeWidth={2.4} strokeLinecap="round" />
2296
+ </svg>
2297
+ </button>
2298
+ <div className="pp-uvp-tooltip-body">{t('autobahn:card.price.uvpListTooltipBody')}</div>
2299
+ </div>
2300
+ )}
2154
2301
  </s>
2155
2302
  </div>
2156
2303
  )}
@@ -2187,7 +2334,44 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2187
2334
  <span className="fin-rate-unit">
2188
2335
  {oldMonthly != null && (
2189
2336
  <>
2190
- <span className="fin-rate-example-label">{t('autobahn:finance.exampleLabel')}</span>
2337
+ <span className="pp-eyebrow">
2338
+ {t('autobahn:card.price.uvpRateLabel')}
2339
+ <span
2340
+ className="pp-eyebrow-badge"
2341
+ role="button"
2342
+ tabIndex={0}
2343
+ onClick={(e) => {
2344
+ e.stopPropagation();
2345
+ setUvpTooltipOpen((v) => !v);
2346
+ }}
2347
+ >
2348
+ 3
2349
+ </span>
2350
+ {uvpTooltipOpen && (
2351
+ <div className="pp-uvp-tooltip" onClick={(e) => e.stopPropagation()}>
2352
+ <button
2353
+ type="button"
2354
+ className="pp-uvp-tooltip-close"
2355
+ aria-label={t('autobahn:modal.close')}
2356
+ onClick={(e) => {
2357
+ e.stopPropagation();
2358
+ setUvpTooltipOpen(false);
2359
+ }}
2360
+ >
2361
+ <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
2362
+ <path d="M5 5L15 15M15 5L5 15" stroke="white" strokeWidth={2.4} strokeLinecap="round" />
2363
+ </svg>
2364
+ </button>
2365
+ <div className="pp-uvp-tooltip-body">
2366
+ {t(
2367
+ mode === 'leasing'
2368
+ ? 'autobahn:card.price.uvpRateTooltipBodyLeasing'
2369
+ : 'autobahn:card.price.uvpRateTooltipBody',
2370
+ )}
2371
+ </div>
2372
+ </div>
2373
+ )}
2374
+ </span>
2191
2375
  <s
2192
2376
  className={`num fin-rate-old fin-rate-example-price${
2193
2377
  String(Math.round(oldMonthly)).length >= 5
@@ -2197,12 +2381,12 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2197
2381
  : ''
2198
2382
  }`}
2199
2383
  >
2200
- {eur(Math.round(oldMonthly))} €<sup>2</sup>
2384
+ {eur(Math.round(oldMonthly))} €
2201
2385
  </s>
2202
2386
  </>
2203
2387
  )}
2204
2388
  <span className="fin-rate-example-suffix">
2205
- {t('autobahn:finance.perMonth')}<sup>3</sup>
2389
+ {t('autobahn:finance.perMonth')}
2206
2390
  </span>
2207
2391
  </span>
2208
2392
  </>
@@ -2217,6 +2401,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2217
2401
  style={{ color: TIER_COLORS[evalTier], background: `color-mix(in srgb, ${TIER_COLORS[evalTier]} 16%, transparent)` }}
2218
2402
  >
2219
2403
  {evalTierLabel}
2404
+ {TIER_KEYS[evalTier] === 'good' && <sup>4</sup>}
2220
2405
  </span>
2221
2406
  )}
2222
2407
  </div>
@@ -2230,12 +2415,20 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2230
2415
  <CalcRow className="calcrow--payment" label={t('autobahn:finance.paymentLabel')}>
2231
2416
  {t('autobahn:finance.paymentValue')}
2232
2417
  </CalcRow>
2233
- <CalcRow className="calcrow--payment" label={t('autobahn:finance.warrantyLabel')}>
2418
+ <CalcRow className="calcrow--payment" label={t('autobahn:finance.warrantyLabel')} last={!available}>
2234
2419
  {t('autobahn:finance.warrantyValue')}
2235
2420
  </CalcRow>
2421
+ {/* Lieferung row disabled per request, replaced by real availability
2422
+ (carStore.isAvailable) — same "Sofort verfügbar" wording as VdpTrustRow.
2236
2423
  <CalcRow className="calcrow--payment" label={t('autobahn:finance.deliveryLabel')} last>
2237
2424
  {t('autobahn:finance.deliveryValue')}
2238
2425
  </CalcRow>
2426
+ */}
2427
+ {available && (
2428
+ <CalcRow className="calcrow--payment" label={t('autobahn:finance.availabilityLabel')} last>
2429
+ {t('autobahn:finance.availabilityValue')}
2430
+ </CalcRow>
2431
+ )}
2239
2432
  </div>
2240
2433
  {addServicesCost != null && (
2241
2434
  <ToggleRow
@@ -2245,6 +2438,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2245
2438
  checked={!!withAddServices}
2246
2439
  disabled={addServicesDisabled}
2247
2440
  onChange={(checked) => onToggleAddServices && onToggleAddServices(checked)}
2441
+ staticNote={t('autobahn:finance.addServicesIncluded')}
2248
2442
  />
2249
2443
  )}
2250
2444
  {/* pushed to the sheet's bottom edge — Figma keeps a big breathing gap above */}
@@ -2452,7 +2646,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2452
2646
  condensed footnote line. */}
2453
2647
  {showBreakdown && (
2454
2648
  <>
2455
- <div style={{ marginBottom: 5 }}>
2649
+ <div ref={mobileBreakdownRef} style={{ marginBottom: 5 }}>
2456
2650
  <p
2457
2651
  style={{
2458
2652
  fontFamily: 'Arial, Helvetica, sans-serif',
@@ -2499,7 +2693,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2499
2693
  <path d="M4.49961 10.8H22.4996M5.39961 21.6H21.5996M3.59961 24.3H23.3996" stroke="#0E2238" strokeWidth="1.6" strokeLinecap="round" />
2500
2694
  <path d="M7.2002 10.8V21.6M11.7002 10.8V21.6M15.3002 10.8V21.6M19.8002 10.8V21.6" stroke="#0E2238" strokeWidth="1.6" />
2501
2695
  </svg>
2502
- <div>
2696
+ <div style={{ width: '100%' }}>
2503
2697
  <p
2504
2698
  style={{
2505
2699
  fontFamily: 'Arial, Helvetica, sans-serif',
@@ -2511,7 +2705,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2511
2705
  marginBottom: 3,
2512
2706
  }}
2513
2707
  >
2514
- {t('autobahn:modal.lenderLabel')}
2708
+ {isLease ? t('autobahn:modal.leaseLenderLabel') : t('autobahn:modal.lenderLabel')}
2515
2709
  </p>
2516
2710
  <p
2517
2711
  style={{
@@ -2523,10 +2717,78 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2523
2717
  margin: 0,
2524
2718
  }}
2525
2719
  >
2526
- {t('autobahn:modal.lenderName')}
2720
+ {isLease ? t('autobahn:modal.leaseLenderName') : t('autobahn:modal.lenderName')}
2527
2721
  </p>
2722
+ {/* leasing's Darlehensgeber-style card also discloses the contract type
2723
+ (Figma: "Vertragsart: Restwertleasing"), split off by its own divider */}
2724
+ {isLease && (
2725
+ <>
2726
+ <div style={{ height: 1, background: '#D6DEE8', margin: '8px 0' }} />
2727
+ <p
2728
+ style={{
2729
+ fontFamily: 'Arial, Helvetica, sans-serif',
2730
+ fontWeight: 700,
2731
+ fontSize: 14,
2732
+ lineHeight: '13px',
2733
+ color: '#0E2238',
2734
+ margin: 0,
2735
+ marginBottom: 3,
2736
+ }}
2737
+ >
2738
+ {t('autobahn:modal.contractTypeLabel')}
2739
+ </p>
2740
+ <p
2741
+ style={{
2742
+ fontFamily: 'Arial, Helvetica, sans-serif',
2743
+ fontWeight: 400,
2744
+ fontSize: 12,
2745
+ lineHeight: '13px',
2746
+ color: '#0E2238',
2747
+ margin: 0,
2748
+ }}
2749
+ >
2750
+ {t('autobahn:modal.contractTypeValue')}
2751
+ </p>
2752
+ </>
2753
+ )}
2528
2754
  </div>
2529
2755
  </div>
2756
+ {/* leasing-only mandatory-package disclosure (Figma "Hinweisbox") — the
2757
+ Rundum-Sorglos-Paket is compulsory for leasing (unlike financing, where
2758
+ it's an optional add-on), so it gets its own callout instead of a bullet. */}
2759
+ {isLease && hasAddServicesRow && (
2760
+ <div
2761
+ style={{
2762
+ display: 'flex',
2763
+ alignItems: 'flex-start',
2764
+ gap: 10,
2765
+ background: '#F7FAFE',
2766
+ border: '0.5px solid #D6DEE8',
2767
+ borderRadius: 5,
2768
+ padding: '7px 8px',
2769
+ marginBottom: 10,
2770
+ }}
2771
+ >
2772
+ <svg width="12" height="12" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" style={{ flex: 'none', marginTop: 2 }}>
2773
+ <circle cx="12" cy="12" r="10" stroke="#0E2238" strokeWidth="1.6" />
2774
+ <path d="M12 16v-4" stroke="#0E2238" strokeWidth="1.6" strokeLinecap="round" />
2775
+ <path d="M12 8h.01" stroke="#0E2238" strokeWidth="1.6" strokeLinecap="round" />
2776
+ </svg>
2777
+ <p
2778
+ style={{
2779
+ fontFamily: 'Arial, Helvetica, sans-serif',
2780
+ fontWeight: 400,
2781
+ fontSize: 10,
2782
+ lineHeight: '10px',
2783
+ textAlign: 'justify',
2784
+ color: '#0E2238',
2785
+ margin: 0,
2786
+ }}
2787
+ >
2788
+ {t('autobahn:modal.packageMandatoryNotice', { amount: eur(addServicesCostRaw ?? 0) })}
2789
+ </p>
2790
+ </div>
2791
+ )}
2530
2792
  <p
2531
2793
  style={{
2532
2794
  fontFamily: 'Arial, Helvetica, sans-serif',
@@ -2540,42 +2802,69 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2540
2802
  >
2541
2803
  {t('autobahn:modal.legalNoticesTitle')}
2542
2804
  </p>
2543
- <ul style={{ margin: 0, marginBottom: 3, paddingLeft: 16, display: 'flex', flexDirection: 'column', gap: 3 }}>
2544
- {[
2545
- t('autobahn:modal.legalNoticeCreditworthiness'),
2546
- t('autobahn:modal.legalNoticeExample'),
2547
- t('autobahn:modal.legalNoticeTwoThirds'),
2548
- t('autobahn:modal.legalNoticeVat'),
2549
- t('autobahn:modal.legalNoticeAddServices'),
2550
- ].map((notice) => (
2551
- <li
2552
- key={notice}
2805
+ {isLease ? (
2806
+ <>
2807
+ <p style={{ fontFamily: 'Arial, Helvetica, sans-serif', fontWeight: 400, fontSize: 12, lineHeight: '12px', color: '#0E2238', margin: 0, marginBottom: 5 }}>
2808
+ {t('autobahn:modal.legalFootnoteAcquisitionPrice')}
2809
+ </p>
2810
+ <p style={{ fontFamily: 'Arial, Helvetica, sans-serif', fontWeight: 400, fontSize: 12, lineHeight: '12px', color: '#0E2238', margin: 0, marginBottom: 10 }}>
2811
+ {t('autobahn:modal.legalFootnoteListPriceLease')}
2812
+ </p>
2813
+ <p
2553
2814
  style={{
2554
2815
  fontFamily: 'Arial, Helvetica, sans-serif',
2555
2816
  fontWeight: 400,
2556
2817
  fontSize: 12,
2557
2818
  lineHeight: '12px',
2558
2819
  color: '#0E2238',
2820
+ margin: 0,
2821
+ paddingBottom: 10,
2559
2822
  }}
2560
2823
  >
2561
- {notice}
2562
- </li>
2563
- ))}
2564
- </ul>
2565
- <p
2566
- style={{
2567
- fontFamily: 'Arial, Helvetica, sans-serif',
2568
- fontWeight: 400,
2569
- fontSize: 12,
2570
- lineHeight: '12px',
2571
- color: '#0E2238',
2572
- margin: 0,
2573
- paddingBottom: 10,
2574
- borderBottom: '1px solid rgba(212, 221, 231, 0.95)',
2575
- }}
2576
- >
2577
- {t('autobahn:modal.listPriceFootnote')}
2578
- </p>
2824
+ {t('autobahn:modal.closingDisclaimerLine1')}
2825
+ <br />
2826
+ {t('autobahn:modal.closingDisclaimerLine2')}
2827
+ </p>
2828
+ </>
2829
+ ) : (
2830
+ <>
2831
+ <ul style={{ margin: 0, marginBottom: 3, paddingLeft: 16, display: 'flex', flexDirection: 'column', gap: 3 }}>
2832
+ {[
2833
+ t('autobahn:modal.legalNoticeCreditworthiness'),
2834
+ t('autobahn:modal.legalNoticeExample'),
2835
+ t('autobahn:modal.legalNoticeTwoThirds'),
2836
+ t('autobahn:modal.legalNoticeVat'),
2837
+ t('autobahn:modal.legalNoticeAddServices'),
2838
+ ].map((notice) => (
2839
+ <li
2840
+ key={notice}
2841
+ style={{
2842
+ fontFamily: 'Arial, Helvetica, sans-serif',
2843
+ fontWeight: 400,
2844
+ fontSize: 12,
2845
+ lineHeight: '12px',
2846
+ color: '#0E2238',
2847
+ }}
2848
+ >
2849
+ {notice}
2850
+ </li>
2851
+ ))}
2852
+ </ul>
2853
+ <p
2854
+ style={{
2855
+ fontFamily: 'Arial, Helvetica, sans-serif',
2856
+ fontWeight: 400,
2857
+ fontSize: 12,
2858
+ lineHeight: '12px',
2859
+ color: '#0E2238',
2860
+ margin: 0,
2861
+ paddingBottom: 10,
2862
+ }}
2863
+ >
2864
+ {t('autobahn:modal.listPriceFootnote')}
2865
+ </p>
2866
+ </>
2867
+ )}
2579
2868
  <div className="fin-sheet-cta-row" style={{ display: 'flex', gap: 7, paddingBottom: 15 }}>
2580
2869
  <Button
2581
2870
  variant="filled"