@bytebrand/fe-ui-core-autobahn 1.0.74 → 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.
- package/cards/CarCard.tsx +37 -4
- package/cards/cardParts.tsx +47 -21
- package/chrome/Footer.tsx +23 -18
- package/constants.ts +31 -31
- package/locales/de/autobahn.json +29 -8
- package/locales/en/autobahn.json +27 -6
- package/package.json +1 -1
- package/theme/md3.css +107 -27
- package/utils/financeFormat.ts +62 -62
- package/utils/priceRating.ts +58 -58
- package/vdp/VdpModules.tsx +398 -109
package/vdp/VdpModules.tsx
CHANGED
|
@@ -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
|
|
1538
|
-
|
|
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
|
-
{
|
|
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:
|
|
1586
|
+
<span className="fin-rate-unit" style={{ fontSize: 12.5, paddingLeft: 15 }}>
|
|
1570
1587
|
{oldMonthly != null && (
|
|
1571
|
-
<
|
|
1572
|
-
{
|
|
1573
|
-
|
|
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:
|
|
1624
|
-
background:
|
|
1657
|
+
color: evalPillColor,
|
|
1658
|
+
background: evalPillBackground,
|
|
1625
1659
|
}}
|
|
1626
1660
|
>
|
|
1627
|
-
{
|
|
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
|
-
|
|
1755
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
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
|
-
|
|
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)}
|
|
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="
|
|
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))}
|
|
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')}
|
|
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
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
<
|
|
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
|
-
{
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
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"
|