@bytebrand/fe-ui-core-autobahn 1.0.87 → 1.0.90
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 +12 -13
- package/cards/CarRow.tsx +15 -17
- package/cards/types.ts +0 -7
- package/compat/CookieModal/Modal.tsx +14 -3
- package/data/toAutobahnCar.ts +1 -21
- package/package.json +1 -1
- package/theme/md3.css +1 -1
- package/vdp/FinanceCalculator.tsx +1 -6
- package/vdp/VdpModules.tsx +0 -3
package/cards/CarCard.tsx
CHANGED
|
@@ -100,17 +100,16 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
100
100
|
const [mode, setMode] = useState<PaymentMode>('financing');
|
|
101
101
|
const [photoIndex, setPhotoIndex] = useState(0);
|
|
102
102
|
const [viewerOpen, setViewerOpen] = useState(false);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const asGoodAsNew = car.asGoodAsNew !== false;
|
|
106
|
-
const showDrop = asGoodAsNew && car.drop != null;
|
|
107
|
-
const showSave = asGoodAsNew && !showDrop && car.savings != null && car.savings > 600;
|
|
108
|
-
const showPct = asGoodAsNew && car.dropPct != null && car.dropPct > 0;
|
|
109
|
-
const showOfferBadge = !!(car.isNew || showDrop || showSave || showPct || car.zeroDown);
|
|
103
|
+
const showSave = car.savings != null && car.savings > 600;
|
|
104
|
+
const showOfferBadge = !!(car.isNew || car.drop != null || showSave || car.dropPct != null || car.zeroDown);
|
|
110
105
|
// PM call: the badge row only fits 3 pills — when Drop/Save + Percent + ZeroDown are all
|
|
111
106
|
// active that's already 3, so NEU is dropped rather than causing a 4th to overflow.
|
|
112
|
-
const activeDealChips = [
|
|
113
|
-
|
|
107
|
+
const activeDealChips = [
|
|
108
|
+
car.drop != null || showSave,
|
|
109
|
+
car.dropPct != null && car.dropPct > 0,
|
|
110
|
+
!!car.zeroDown,
|
|
111
|
+
].filter(Boolean).length;
|
|
112
|
+
const showNeu = car.isNew && activeDealChips < 3;
|
|
114
113
|
return (
|
|
115
114
|
<article
|
|
116
115
|
className="mcard mcard--interactive cc-card"
|
|
@@ -135,7 +134,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
135
134
|
{/* top-left signal stack (heart is top-right): NEU badge + ONE deal signal + the
|
|
136
135
|
"⚡ Angebot" badge (own line, Figma) below it. DropChip takes precedence over
|
|
137
136
|
SaveChip; each only renders when its real data exists. */}
|
|
138
|
-
{(showOfferBadge ||
|
|
137
|
+
{(showOfferBadge || car.dropPct != null) && (
|
|
139
138
|
<div
|
|
140
139
|
className="cc-badges"
|
|
141
140
|
style={{
|
|
@@ -151,8 +150,8 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
151
150
|
}}
|
|
152
151
|
>
|
|
153
152
|
<div style={{ display: 'flex', flexWrap: 'nowrap', gap: 5, alignItems: 'center' }}>
|
|
154
|
-
{
|
|
155
|
-
<PercentChip pct={
|
|
153
|
+
{car.drop != null ? <DropChip t={t} amount={car.drop} /> : showSave && <SaveChip t={t} amount={car.savings!} />}
|
|
154
|
+
<PercentChip pct={car.dropPct} />
|
|
156
155
|
<ZeroDownChip
|
|
157
156
|
t={t}
|
|
158
157
|
show={car.zeroDown}
|
|
@@ -206,7 +205,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
206
205
|
<div className="cc-price" style={{ marginTop: 4, borderTop: '1px solid var(--md-outline-variant)' }}>
|
|
207
206
|
<PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} />
|
|
208
207
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginTop: 10 }}>
|
|
209
|
-
<PriceHero car={car} mode={mode} t={t}
|
|
208
|
+
<PriceHero car={car} mode={mode} t={t} />
|
|
210
209
|
<PriceEvalCol car={car} t={t} />
|
|
211
210
|
</div>
|
|
212
211
|
{/* same fallback CardCtaRow already uses below: pages that haven't wired a real
|
package/cards/CarRow.tsx
CHANGED
|
@@ -81,16 +81,15 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({
|
|
|
81
81
|
}) => {
|
|
82
82
|
const [mode, setMode] = useState<PaymentMode>('financing');
|
|
83
83
|
const [photoIndex, setPhotoIndex] = useState(0);
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const asGoodAsNew = car.asGoodAsNew !== false;
|
|
87
|
-
const showDrop = asGoodAsNew && car.drop != null;
|
|
88
|
-
const showSave = asGoodAsNew && !showDrop && car.savings != null && car.savings > 600;
|
|
89
|
-
const showPct = asGoodAsNew && car.dropPct != null && car.dropPct > 0;
|
|
90
|
-
const showOfferBadge = !!(car.isNew || showDrop || showSave || showPct || car.zeroDown);
|
|
84
|
+
const showSave = car.savings != null && car.savings > 600;
|
|
85
|
+
const showOfferBadge = !!(car.isNew || car.drop != null || showSave || car.dropPct != null || car.zeroDown);
|
|
91
86
|
// same overflow rule as CarCard: the row only fits 3 pills, so NEU drops when the other 3 are all active.
|
|
92
|
-
const activeDealChips = [
|
|
93
|
-
|
|
87
|
+
const activeDealChips = [
|
|
88
|
+
car.drop != null || showSave,
|
|
89
|
+
car.dropPct != null && car.dropPct > 0,
|
|
90
|
+
!!car.zeroDown,
|
|
91
|
+
].filter(Boolean).length;
|
|
92
|
+
const showNeu = car.isNew && activeDealChips < 3;
|
|
94
93
|
const facts: Array<[icon: string, value: string]> = [
|
|
95
94
|
['gauge', fmtKm(car.km)],
|
|
96
95
|
['calendar', t('autobahn:card.specs.firstRegistration', { reg: car.reg })],
|
|
@@ -113,10 +112,9 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({
|
|
|
113
112
|
{/* 'auto': desktop picks cover/contain per photo (crops only when it wouldn't lose too
|
|
114
113
|
much of the shot); below 601px this is identical to 'contain', so mobile is unchanged. */}
|
|
115
114
|
<CardCarousel images={car.gallery} height="100%" fit="auto" onChange={setPhotoIndex} />
|
|
116
|
-
{/* deal badges — ONE compact line
|
|
117
|
-
(
|
|
118
|
-
|
|
119
|
-
NOT here — it lives in the price column (PriceMeter). */}
|
|
115
|
+
{/* deal badges — ONE compact line: NEU · −€ · 0 € Anz. · −%, plus the "⚡ Angebot" badge
|
|
116
|
+
(Figma) on its own line below. The price RATING ('Guter Preis') is NOT here — it
|
|
117
|
+
lives in the price column (PriceMeter). */}
|
|
120
118
|
<div
|
|
121
119
|
style={{
|
|
122
120
|
position: 'absolute',
|
|
@@ -131,8 +129,8 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({
|
|
|
131
129
|
}}
|
|
132
130
|
>
|
|
133
131
|
<div style={{ display: 'flex', flexWrap: 'nowrap', alignItems: 'center', gap: 5 }}>
|
|
134
|
-
{
|
|
135
|
-
<
|
|
132
|
+
{showNeu && <span className="neu-badge">NEU</span>}
|
|
133
|
+
{car.drop != null ? <DropChip t={t} amount={car.drop} /> : showSave && <SaveChip t={t} amount={car.savings!} />}
|
|
136
134
|
<ZeroDownChip t={t} show={car.zeroDown} compact className="show-mobile" />
|
|
137
135
|
<ZeroDownChip
|
|
138
136
|
t={t}
|
|
@@ -149,7 +147,7 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({
|
|
|
149
147
|
fontWeight: 700,
|
|
150
148
|
}}
|
|
151
149
|
/>
|
|
152
|
-
|
|
150
|
+
<PercentChip pct={car.dropPct} />
|
|
153
151
|
</div>
|
|
154
152
|
<OfferBadge show={showOfferBadge} t={t} />
|
|
155
153
|
</div>
|
|
@@ -246,7 +244,7 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({
|
|
|
246
244
|
<PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} className="show-mobile" />
|
|
247
245
|
<div className="pp-purchase show-mobile">
|
|
248
246
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
|
|
249
|
-
<PriceHero car={car} mode={mode} t={t}
|
|
247
|
+
<PriceHero car={car} mode={mode} t={t} />
|
|
250
248
|
{car.rating && (
|
|
251
249
|
<StatChip tone={ratingTone(car.rating)} icon={ratingTone(car.rating) === 'good' ? 'check' : 'minus'}>
|
|
252
250
|
{t('autobahn:tier.' + car.rating)}
|
package/cards/types.ts
CHANGED
|
@@ -118,13 +118,6 @@ export interface AutobahnCar {
|
|
|
118
118
|
dropPct?: number;
|
|
119
119
|
/** 0 € Anzahlung available (offer.showDownPayment) — drives the "0 € Anz." badge. */
|
|
120
120
|
zeroDown?: boolean;
|
|
121
|
-
/**
|
|
122
|
-
* as-good-as-new gate (mileage < 1,000 km AND first registration unknown or < 12 months) —
|
|
123
|
-
* product rule for the drop/percent/NEU deal signals AND the strike-price/UVP-Rate display
|
|
124
|
-
* (see toAutobahnCar.ts's isStrikeShown). Independent of NEU's own showNewLabel (which also
|
|
125
|
-
* requires a recent modificationDate). Does NOT gate zeroDown.
|
|
126
|
-
*/
|
|
127
|
-
asGoodAsNew?: boolean;
|
|
128
121
|
|
|
129
122
|
// --- NEEDS DATA: no real source yet (NEVER fabricate) ---
|
|
130
123
|
/** original list price (for the struck-through reference price). */
|
|
@@ -2,8 +2,18 @@
|
|
|
2
2
|
// BaseModalWrapper is replaced by a plain fixed overlay + centered surface.
|
|
3
3
|
// Deliberately NOT the kit ui/Modal — the cookie consent dialog has no title
|
|
4
4
|
// bar / close button and must not be dismissible without a choice.
|
|
5
|
+
//
|
|
6
|
+
// Rendered via ReactDOM.createPortal into document.body (same technique as
|
|
7
|
+
// ui/Modal.tsx) rather than inline in the page tree: a React hydration-mismatch
|
|
8
|
+
// error ANYWHERE else on the page (a known, separate bug — see the
|
|
9
|
+
// SearchNextPage isMobile SSR/CSR mismatch) can corrupt sibling reconciliation
|
|
10
|
+
// enough that an inline `position: fixed` overlay ends up mounted in the wrong
|
|
11
|
+
// place in the DOM (observed: appearing as plain content below the footer
|
|
12
|
+
// instead of as an overlay). A body-level portal is immune to that — its DOM
|
|
13
|
+
// node never lives inside the page's own React subtree.
|
|
5
14
|
|
|
6
15
|
import * as React from 'react';
|
|
16
|
+
import * as ReactDOM from 'react-dom';
|
|
7
17
|
|
|
8
18
|
import ModalsConfig from './ModalsConfig';
|
|
9
19
|
|
|
@@ -30,13 +40,14 @@ const SURFACE: React.CSSProperties = {
|
|
|
30
40
|
};
|
|
31
41
|
|
|
32
42
|
const BasicModal = ({ isVisible, toggleModal, name, modalProps, handleSentryInit }: IBasicModal) => {
|
|
33
|
-
if (!isVisible) return null;
|
|
34
|
-
return (
|
|
43
|
+
if (!isVisible || typeof document === 'undefined') return null;
|
|
44
|
+
return ReactDOM.createPortal(
|
|
35
45
|
<div style={OVERLAY} role='dialog' aria-modal='true' aria-label='Cookie-Einstellungen'>
|
|
36
46
|
<div style={SURFACE}>
|
|
37
47
|
{ModalsConfig[name] && ModalsConfig[name]({ ...modalProps, toggleModal, handleSentryInit })}
|
|
38
48
|
</div>
|
|
39
|
-
</div
|
|
49
|
+
</div>,
|
|
50
|
+
document.body
|
|
40
51
|
);
|
|
41
52
|
};
|
|
42
53
|
|
package/data/toAutobahnCar.ts
CHANGED
|
@@ -96,22 +96,6 @@ function num(v: any): number | undefined {
|
|
|
96
96
|
return Number.isFinite(n) ? n : undefined;
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
// strike-price ("UVP-Rate"/list-price) eligibility — a separate product rule from the NEU
|
|
100
|
-
// badge's own showNewLabel (which also requires a recent modificationDate; see fe-main's
|
|
101
|
-
// DataTransformers.ts shouldShowNewLabel). Here: as-good-as-new only — mileage under 1,000 km
|
|
102
|
-
// AND first registration either unknown or under 12 months old. Mirrors the identical gate
|
|
103
|
-
// added to VehicleDetailedNextPage.tsx's getFinancing() (VDP "Dein Angebot" card).
|
|
104
|
-
const STRIKE_MAX_MILEAGE_KM = 1000;
|
|
105
|
-
const STRIKE_MAX_AGE_MONTHS = 12;
|
|
106
|
-
const STRIKE_AVG_MS_PER_MONTH = 1000 * 60 * 60 * 24 * 30.44;
|
|
107
|
-
|
|
108
|
-
/** unix-seconds first registration (info.regDate) — unknown can't disqualify what we can't check. */
|
|
109
|
-
function isStrikeAgeEligible(firstRegistration?: number | null): boolean {
|
|
110
|
-
if (firstRegistration == null || !Number.isFinite(firstRegistration) || firstRegistration <= 0) return true;
|
|
111
|
-
const ageMonths = (Date.now() - firstRegistration * 1000) / STRIKE_AVG_MS_PER_MONTH;
|
|
112
|
-
return ageMonths < STRIKE_MAX_AGE_MONTHS;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
99
|
/**
|
|
116
100
|
* Minimal de-DE fallback for the handful of cbd:* selector codes the cards show
|
|
117
101
|
* (fuel + gearbox). Used only when no `t` is injected (e.g. in unit tests).
|
|
@@ -407,10 +391,7 @@ export function toAutobahnCar(storeCar: any, deps: ToAutobahnCarDeps = {}): Auto
|
|
|
407
391
|
// purchase-price strike on the search payload today, so we leave listPrice /
|
|
408
392
|
// savings UNDEFINED unless a real highestPrice strike exists above the sale
|
|
409
393
|
// price. See fieldMapping.ts — flagged for backend confirmation.
|
|
410
|
-
|
|
411
|
-
// same raw timestamp as `reg` above) gate: see isStrikeAgeEligible.
|
|
412
|
-
const asGoodAsNew = Number(num(info.mileage) ?? 0) < STRIKE_MAX_MILEAGE_KM && isStrikeAgeEligible(regTs);
|
|
413
|
-
const isStrikeShown = !!price?.financing?.isStrikeShown && asGoodAsNew;
|
|
394
|
+
const isStrikeShown = !!price?.financing?.isStrikeShown;
|
|
414
395
|
const highest = num(price?.financing?.highestPrice) ?? num(price?.highestPrice);
|
|
415
396
|
let listPrice: number | undefined;
|
|
416
397
|
let savings: number | undefined;
|
|
@@ -511,7 +492,6 @@ export function toAutobahnCar(storeCar: any, deps: ToAutobahnCarDeps = {}): Auto
|
|
|
511
492
|
...(dropPct != null ? { dropPct } : {}),
|
|
512
493
|
...(zeroDown ? { zeroDown } : {}),
|
|
513
494
|
...(isNew ? { isNew } : {}),
|
|
514
|
-
asGoodAsNew,
|
|
515
495
|
...(availability ? { availability } : {}),
|
|
516
496
|
...(viewsCount != null ? { viewsCount } : {}),
|
|
517
497
|
...(favoritesCount != null ? { favoritesCount } : {}),
|
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.90",
|
|
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
|
@@ -4471,7 +4471,7 @@ line-height: 31px;
|
|
|
4471
4471
|
flex: none;
|
|
4472
4472
|
}
|
|
4473
4473
|
.md3 .cc-price .pp-hero-rate {
|
|
4474
|
-
align-items:
|
|
4474
|
+
align-items: center;
|
|
4475
4475
|
flex-wrap: nowrap;
|
|
4476
4476
|
}
|
|
4477
4477
|
/* separator line between the financing block and the leasing row (legacy-design parity) */
|
|
@@ -757,12 +757,7 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
|
|
|
757
757
|
</s>
|
|
758
758
|
</span>
|
|
759
759
|
)}
|
|
760
|
-
|
|
761
|
-
sits level with the blue hero rate instead of centering high in the row. */}
|
|
762
|
-
<span
|
|
763
|
-
className="fin-rate-example-suffix"
|
|
764
|
-
style={oldMonthly == null ? { paddingTop: 20 } : undefined}
|
|
765
|
-
>
|
|
760
|
+
<span className="fin-rate-example-suffix">
|
|
766
761
|
{t('autobahn:finance.perMonth')}
|
|
767
762
|
</span>
|
|
768
763
|
</span>
|
package/vdp/VdpModules.tsx
CHANGED
|
@@ -1655,8 +1655,6 @@ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
|
|
|
1655
1655
|
</s>
|
|
1656
1656
|
</span>
|
|
1657
1657
|
)}
|
|
1658
|
-
{/* no struck old rate above it (strike hidden) — nudge "/ mtl." down so it
|
|
1659
|
-
sits level with the blue hero rate instead of centering high in the row. */}
|
|
1660
1658
|
<span
|
|
1661
1659
|
style={{
|
|
1662
1660
|
fontFamily: 'Arial, Helvetica, sans-serif',
|
|
@@ -1664,7 +1662,6 @@ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({
|
|
|
1664
1662
|
fontSize: 18,
|
|
1665
1663
|
lineHeight: '21px',
|
|
1666
1664
|
color: '#5B6B7F',
|
|
1667
|
-
...(oldMonthly == null ? { paddingTop: 20 } : {}),
|
|
1668
1665
|
}}
|
|
1669
1666
|
>
|
|
1670
1667
|
{t('autobahn:bottom.perMonth').replace(/^\/\s*/, '/ ')}
|