@bytebrand/fe-ui-core-autobahn 1.0.157 → 1.0.158
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 +39 -9
- package/cards/cardParts.tsx +6 -1
- package/compat/CookieModal/CookieModal.tsx +1 -5
- package/compat/Loader/Loader.tsx +1 -7
- package/constants/app.ts +11 -1
- package/constants/core.ts +12 -1
- package/iconsvg/IconSVG.tsx +1 -5
- package/locales/legacy/data.ts +2 -1
- package/package.json +1 -1
- package/theme/md3.css +140 -18
- package/ui/Modal.tsx +6 -5
- package/vehiclesProps/decoratedLightProps.tsx +16 -6
- package/vehiclesProps/decoratedProps.tsx +8 -4
package/cards/CarCard.tsx
CHANGED
|
@@ -157,6 +157,8 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
157
157
|
// active that's already 3, so NEU is dropped rather than causing a 4th to overflow.
|
|
158
158
|
const activeDealChips = [showDrop || showSave, showPct, !!car.zeroDown].filter(Boolean).length;
|
|
159
159
|
const showNeu = asGoodAsNew && car.isNew && activeDealChips < 3;
|
|
160
|
+
// tabs the price block can switch to (PriceTabs shows Leasing only when the car has a lease rate)
|
|
161
|
+
const paymentModes: PaymentMode[] = car.lease != null ? ['financing', 'leasing', 'buy'] : ['financing', 'buy'];
|
|
160
162
|
return (
|
|
161
163
|
<article
|
|
162
164
|
className="mcard mcard--interactive cc-card"
|
|
@@ -277,18 +279,46 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
|
|
|
277
279
|
<div className="cc-price" style={{ marginTop: 0, borderTop: '1px solid var(--md-outline-variant)' }}>
|
|
278
280
|
<PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} />
|
|
279
281
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginTop: 10 }}>
|
|
280
|
-
|
|
282
|
+
{/* every tab's hero stacked in one grid cell (.cc-hero-stack), only the active one
|
|
283
|
+
visible, bottom-aligned — Kaufen's struck list price sits on its own line above
|
|
284
|
+
the price, so without this the row (and the card) grew/shrank by that line on
|
|
285
|
+
every tab switch. Same trick as .cc-terms-stack below. */}
|
|
286
|
+
<div className="cc-hero-stack">
|
|
287
|
+
{paymentModes.map((m) => (
|
|
288
|
+
<div
|
|
289
|
+
key={m}
|
|
290
|
+
className={'cc-hero-slot' + (m === mode ? '' : ' cc-hero-slot--hidden')}
|
|
291
|
+
aria-hidden={m === mode ? undefined : true}
|
|
292
|
+
>
|
|
293
|
+
<PriceHero car={car} mode={m} t={t} alwaysShowUnit />
|
|
294
|
+
</div>
|
|
295
|
+
))}
|
|
296
|
+
</div>
|
|
281
297
|
<PriceEvalCol car={car} t={t} />
|
|
282
298
|
</div>
|
|
283
299
|
{/* same fallback CardCtaRow already uses below: pages that haven't wired a real
|
|
284
|
-
"Rate anpassen" sheet yet still get a working "Details" link (opens the car).
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
300
|
+
"Rate anpassen" sheet yet still get a working "Details" link (opens the car).
|
|
301
|
+
Every tab's terms line is rendered into the same grid cell (.cc-terms-stack) and
|
|
302
|
+
only the active one is visible — the cell is as tall as the tallest line, so the
|
|
303
|
+
card keeps one height across Finanzierung/Leasing/Kaufen (Leasing's longer line
|
|
304
|
+
wraps to 2 lines on narrow phones). No change to the visible line itself. */}
|
|
305
|
+
<div className="cc-terms-stack">
|
|
306
|
+
{paymentModes.map((m) => (
|
|
307
|
+
<div
|
|
308
|
+
key={m}
|
|
309
|
+
className={'cc-terms-slot' + (m === mode ? '' : ' cc-terms-slot--hidden')}
|
|
310
|
+
aria-hidden={m === mode ? undefined : true}
|
|
311
|
+
>
|
|
312
|
+
<PriceTerms
|
|
313
|
+
car={car}
|
|
314
|
+
mode={m}
|
|
315
|
+
onOpenRateAdjust={onOpenRateAdjust || (() => onOpen(car))}
|
|
316
|
+
onShowDetails={onShowFinancingDetails ? () => onShowFinancingDetails(car, m) : undefined}
|
|
317
|
+
t={t}
|
|
318
|
+
/>
|
|
319
|
+
</div>
|
|
320
|
+
))}
|
|
321
|
+
</div>
|
|
292
322
|
</div>
|
|
293
323
|
{/* Mobile-only row (CardCtaRow itself carries the show-mobile class) — always
|
|
294
324
|
rendered, with real fallbacks the page can override once it wires its own
|
package/cards/cardParts.tsx
CHANGED
|
@@ -710,7 +710,12 @@ export const PriceTerms: React.FunctionComponent<{
|
|
|
710
710
|
onShowDetails?: () => void;
|
|
711
711
|
t: TranslationFunction;
|
|
712
712
|
}> = ({ car, mode, onOpenRateAdjust, onShowDetails, t }) => {
|
|
713
|
-
|
|
713
|
+
// Kaufen (buy): a cash purchase has no financing terms/details link — used to `return null`
|
|
714
|
+
// here, which collapsed .cc-terms to 0 height on that tab while Financing/Leasing rendered a
|
|
715
|
+
// real line, jumping the price row (and everything below it: rating pill, CTA row) up/down on
|
|
716
|
+
// every tab switch. Render an empty, still-present .cc-terms div instead — same fix as the VDP
|
|
717
|
+
// FinanceCalculator's .fin-cond (md3.css), which reserves its line's height the same way.
|
|
718
|
+
if (mode === 'buy') return <div className="cc-terms" />;
|
|
714
719
|
const finFirstInstallment =
|
|
715
720
|
car.finFirstInstallment != null ? car.finFirstInstallment.toLocaleString('de-DE') : _finFirstInstallment;
|
|
716
721
|
const finMonths = car.finMonths ?? _finMonths;
|
|
@@ -7,7 +7,7 @@ export interface ICookieModal {
|
|
|
7
7
|
handleSentryInit?: () => void;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
const CookieModal = ({ handleSentryInit }: ICookieModal) => {
|
|
10
|
+
const CookieModal = ({ handleSentryInit = () => {} }: ICookieModal) => {
|
|
11
11
|
const { isVisible, toggleModal } = useModal();
|
|
12
12
|
const [modal, setModal] = useState('');
|
|
13
13
|
|
|
@@ -52,8 +52,4 @@ const CookieModal = ({ handleSentryInit }: ICookieModal) => {
|
|
|
52
52
|
: <></>;
|
|
53
53
|
};
|
|
54
54
|
|
|
55
|
-
CookieModal.defaultProps = {
|
|
56
|
-
handleSentryInit: () => {}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
55
|
export default CookieModal;
|
package/compat/Loader/Loader.tsx
CHANGED
|
@@ -10,7 +10,7 @@ export interface ILoaderProps {
|
|
|
10
10
|
fluid?: boolean;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
const Loader: React.FC<ILoaderProps> = ({ className, size, fluid }) => {
|
|
13
|
+
const Loader: React.FC<ILoaderProps> = ({ className = '', size = 'medium', fluid = false }) => {
|
|
14
14
|
const classNames = classnames(
|
|
15
15
|
styles.loader,
|
|
16
16
|
{ [styles[size]]: !fluid },
|
|
@@ -23,10 +23,4 @@ const Loader: React.FC<ILoaderProps> = ({ className, size, fluid }) => {
|
|
|
23
23
|
);
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
Loader.defaultProps = {
|
|
27
|
-
className: '',
|
|
28
|
-
size: 'medium',
|
|
29
|
-
fluid: false
|
|
30
|
-
};
|
|
31
|
-
|
|
32
26
|
export default Loader;
|
package/constants/app.ts
CHANGED
|
@@ -40,7 +40,17 @@ export const LOCAL_STORAGE_KEY = {
|
|
|
40
40
|
i18n_KEY: 'i18nLanguage'
|
|
41
41
|
};
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
// ADV-284: evaluated at import time — with site storage blocked, reading localStorage throws a
|
|
44
|
+
// SecurityError that took down the whole bundle. Blocked storage behaves like "nothing saved".
|
|
45
|
+
const readStoredLang = (): string | null => {
|
|
46
|
+
try {
|
|
47
|
+
return localStorage.getItem(LOCAL_STORAGE_KEY.i18n_KEY);
|
|
48
|
+
} catch (e) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const DEFAULT_LANG: string = typeof window !== 'undefined' ? readStoredLang() : 'de';
|
|
44
54
|
|
|
45
55
|
// Default support contact phone
|
|
46
56
|
export const CONTACT_PHONE: string = '+498004030182';
|
package/constants/core.ts
CHANGED
|
@@ -35,7 +35,18 @@ export const LOCAL_STORAGE_KEY = {
|
|
|
35
35
|
i18n_KEY: 'i18nLanguage'
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
// ADV-284: evaluated at import time — with site storage blocked, merely reading
|
|
39
|
+
// window.localStorage throws a SecurityError, which took down the whole bundle (landing
|
|
40
|
+
// page never rendered). Treat blocked storage like "no language saved".
|
|
41
|
+
const readStoredLang = (): string | null => {
|
|
42
|
+
try {
|
|
43
|
+
return typeof window !== 'undefined' ? window.localStorage.getItem(LOCAL_STORAGE_KEY.i18n_KEY) : null;
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const DEFAULT_LANG: string = readStoredLang() ?? 'de';
|
|
39
50
|
|
|
40
51
|
export const ROW_STYLES = {
|
|
41
52
|
marginBottom: 10
|
package/iconsvg/IconSVG.tsx
CHANGED
|
@@ -7,7 +7,7 @@ type TIconSVG = {
|
|
|
7
7
|
customDimensions?: boolean;
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
const IconSVG: React.FC<TIconSVG> = ({ name, isWarrantyActive, customDimensions, ...props }: TIconSVG) => {
|
|
10
|
+
const IconSVG: React.FC<TIconSVG> = ({ name, isWarrantyActive, customDimensions = false, ...props }: TIconSVG) => {
|
|
11
11
|
if (!name) return null;
|
|
12
12
|
|
|
13
13
|
const Component: any = components[name.toString()];
|
|
@@ -23,8 +23,4 @@ const IconSVG: React.FC<TIconSVG> = ({ name, isWarrantyActive, customDimensions,
|
|
|
23
23
|
);
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
IconSVG.defaultProps = {
|
|
27
|
-
customDimensions: false
|
|
28
|
-
};
|
|
29
|
-
|
|
30
26
|
export default IconSVG;
|
package/locales/legacy/data.ts
CHANGED
|
@@ -103,7 +103,8 @@ export const vehicleProps = (car?: any, isOfferAvailable: boolean = true) => {
|
|
|
103
103
|
wltpDischargedCombinedLabel: `${getFormattedNumber(vehicleOption.wltpCombined != null ? vehicleOption.wltpCombined : 19.9)}l/100km (entladen, komb)`,
|
|
104
104
|
wltpCo2WtdCombinedShort: `${getFormattedNumber(vehicleOption.wltpCo2)}g CO2/km (gew., komb)*`,
|
|
105
105
|
co2ClassWeighted: `CO2-Klasse ${(cbd.cbd as any)[vehicleOption.wltpEnergyEfficiencyClass] || vehicleOption.wltpEnergyEfficiencyClass || ''} (gew., komb)`,
|
|
106
|
-
|
|
106
|
+
// ADV-187: no hard-coded fallback class — callers only render this when the class is defined.
|
|
107
|
+
co2ClassDischarged: `CO2-Klasse ${(cbd.cbd as any)[vehicleOption.wltpDischargedEnergyEfficiencyClass] || vehicleOption.wltpDischargedEnergyEfficiencyClass || ''} (entladen, komb)`
|
|
107
108
|
},
|
|
108
109
|
title: {
|
|
109
110
|
'addOfferToMainSlide': 'Zum Schieberegler hinzufügen',
|
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.158",
|
|
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
|
@@ -2991,6 +2991,12 @@ line-height: 31px;
|
|
|
2991
2991
|
.md3 .co-address-grid .mfield-error-text {
|
|
2992
2992
|
display: none;
|
|
2993
2993
|
}
|
|
2994
|
+
/* co-birthdate opts back in: future-date/under-18/empty all show the same red border, so
|
|
2995
|
+
unlike the rest of .co-personal-grid the reason actually needs to be spelled out. */
|
|
2996
|
+
.md3 .co-personal-grid .co-birthdate .mfield-error-text {
|
|
2997
|
+
display: block;
|
|
2998
|
+
margin-top: 4px;
|
|
2999
|
+
}
|
|
2994
3000
|
/* bug: clearing a valid field and tabbing away flashed the base .mfield:focus-within blue
|
|
2995
3001
|
ring (#005CFF) for a moment before the error class/red border landed — green -> blue ->
|
|
2996
3002
|
red. Dropping the blue ring here removes that flash; valid/error still show via their own
|
|
@@ -3152,6 +3158,20 @@ line-height: 31px;
|
|
|
3152
3158
|
/* Vorwahl/Telefonnummer values (the "Land" dropdown has no <input>, so this only hits these two). */
|
|
3153
3159
|
.md3 .co-phone .mfield input {
|
|
3154
3160
|
font-size: 15px;
|
|
3161
|
+
padding: 0;
|
|
3162
|
+
}
|
|
3163
|
+
/* valid check floats in the corner, not in the flex row — the digits need the column's width */
|
|
3164
|
+
.md3 .co-phone .mfield .mfield-ok {
|
|
3165
|
+
position: absolute;
|
|
3166
|
+
top: 50%;
|
|
3167
|
+
right: 10px;
|
|
3168
|
+
transform: translateY(-50%);
|
|
3169
|
+
}
|
|
3170
|
+
.md3 .co-phone .mfield {
|
|
3171
|
+
padding: 0 8px;
|
|
3172
|
+
}
|
|
3173
|
+
.md3 .co-phone .mfield.mfield--floating.mfield--valid {
|
|
3174
|
+
padding: 0 25px 0 8px;
|
|
3155
3175
|
}
|
|
3156
3176
|
/* "Land" country-code select — same floating-notched-label look as RegistrationForm.tsx's
|
|
3157
3177
|
.countryCode (OfferRequestModals/modals.styl + mixins.styl's floatingField(), Stylus/CSS
|
|
@@ -4812,8 +4832,38 @@ line-height: 31px;
|
|
|
4812
4832
|
gap: 12px;
|
|
4813
4833
|
}
|
|
4814
4834
|
.md3 .oo-ls-cell {
|
|
4815
|
-
padding: 0
|
|
4816
|
-
|
|
4835
|
+
padding: 0;
|
|
4836
|
+
}
|
|
4837
|
+
.md3 .oo-ls-cell:not(:first-child) {
|
|
4838
|
+
border-left: 0;
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
/* ≤600px Figma mobile variant (63571:28382): rules between rows with 12px above and below,
|
|
4842
|
+
replacing the 800px stack's gap. */
|
|
4843
|
+
@media (max-width: 600px) {
|
|
4844
|
+
.md3 .oo-ls-row {
|
|
4845
|
+
gap: 0;
|
|
4846
|
+
}
|
|
4847
|
+
.md3 .oo-ls-cell {
|
|
4848
|
+
padding: 0 0 12px;
|
|
4849
|
+
}
|
|
4850
|
+
.md3 .oo-ls-cell:not(:first-child) {
|
|
4851
|
+
padding-top: 12px;
|
|
4852
|
+
border-top: 1px solid var(--md-outline-variant);
|
|
4853
|
+
}
|
|
4854
|
+
.md3 .oo-ls-cell:last-child {
|
|
4855
|
+
padding-bottom: 0;
|
|
4856
|
+
}
|
|
4857
|
+
.md3 .oo-ls-title-row .oo-ls-title,
|
|
4858
|
+
.md3 .oo-ls-title-row ~ .oo-ls-sub,
|
|
4859
|
+
.md3 .oo-ls-terms-link {
|
|
4860
|
+
font-family: Arimo, Arial, sans-serif;
|
|
4861
|
+
}
|
|
4862
|
+
.md3 .oo-ls-terms-link {
|
|
4863
|
+
margin-top: 0;
|
|
4864
|
+
}
|
|
4865
|
+
.md3 .oo-ls-title-row .lucide {
|
|
4866
|
+
stroke: var(--md-on-surface-variant);
|
|
4817
4867
|
}
|
|
4818
4868
|
}
|
|
4819
4869
|
/* "2 Jahre Garantie" "i" popover (OrderOverviewNext.tsx) — same AddServicesTooltipBody/
|
|
@@ -5440,6 +5490,12 @@ line-height: 31px;
|
|
|
5440
5490
|
min-width: 0;
|
|
5441
5491
|
flex: 1 0 0;
|
|
5442
5492
|
}
|
|
5493
|
+
.md3 .oo-vs-text {
|
|
5494
|
+
display: flex;
|
|
5495
|
+
flex-direction: column;
|
|
5496
|
+
gap: 3px;
|
|
5497
|
+
min-width: 0;
|
|
5498
|
+
}
|
|
5443
5499
|
.md3 .oo-vs-name {
|
|
5444
5500
|
font-family: Arial;
|
|
5445
5501
|
font-weight: 700;
|
|
@@ -5486,6 +5542,40 @@ line-height: 31px;
|
|
|
5486
5542
|
.md3 .oo-vs-seller-icon {
|
|
5487
5543
|
flex: none;
|
|
5488
5544
|
}
|
|
5545
|
+
/* mobile only (Figma 63571:28325): photo 110×70 and the vehicle text share row 1, the text
|
|
5546
|
+
centred on the photo; the seller row spans the full width below, 12px under the row */
|
|
5547
|
+
@media (max-width: 600px) {
|
|
5548
|
+
.md3 .oo-vs-row {
|
|
5549
|
+
display: grid;
|
|
5550
|
+
grid-template-columns: 110px minmax(0, 1fr);
|
|
5551
|
+
grid-template-rows: minmax(70px, auto) auto;
|
|
5552
|
+
column-gap: 14px;
|
|
5553
|
+
row-gap: 12px;
|
|
5554
|
+
}
|
|
5555
|
+
.md3 .oo-vs-info {
|
|
5556
|
+
display: contents;
|
|
5557
|
+
}
|
|
5558
|
+
.md3 .oo-vs-photo {
|
|
5559
|
+
grid-column: 1;
|
|
5560
|
+
grid-row: 1;
|
|
5561
|
+
width: 110px;
|
|
5562
|
+
height: 70px;
|
|
5563
|
+
border-radius: 0;
|
|
5564
|
+
}
|
|
5565
|
+
.md3 .oo-vs-text {
|
|
5566
|
+
grid-column: 2;
|
|
5567
|
+
grid-row: 1;
|
|
5568
|
+
align-self: center;
|
|
5569
|
+
}
|
|
5570
|
+
.md3 .oo-vs-name {
|
|
5571
|
+
font-size: 16px;
|
|
5572
|
+
}
|
|
5573
|
+
.md3 .oo-vs-seller {
|
|
5574
|
+
grid-column: 1 / -1;
|
|
5575
|
+
grid-row: 2;
|
|
5576
|
+
justify-self: start;
|
|
5577
|
+
}
|
|
5578
|
+
}
|
|
5489
5579
|
.md3 .oo-row {
|
|
5490
5580
|
padding: 12px 0;
|
|
5491
5581
|
border-bottom: 1px solid var(--md-outline-variant);
|
|
@@ -5573,10 +5663,18 @@ line-height: 31px;
|
|
|
5573
5663
|
gap (14px), same as every other direct child in this step. */
|
|
5574
5664
|
.md3 .oo-legal-notice {
|
|
5575
5665
|
margin: 0;
|
|
5576
|
-
font-family:
|
|
5666
|
+
font-family: 'Arimo';
|
|
5667
|
+
font-weight: 400;
|
|
5577
5668
|
font-size: 13px;
|
|
5578
5669
|
line-height: 1.5;
|
|
5670
|
+
letter-spacing: 0;
|
|
5579
5671
|
color: var(--md-on-surface-variant);
|
|
5672
|
+
text-align: left;
|
|
5673
|
+
}
|
|
5674
|
+
@media (max-width: 600px) {
|
|
5675
|
+
.md3 .oo-legal-notice {
|
|
5676
|
+
text-align: justify;
|
|
5677
|
+
}
|
|
5580
5678
|
}
|
|
5581
5679
|
.md3 .oo-legal-link {
|
|
5582
5680
|
text-decoration: underline;
|
|
@@ -5864,6 +5962,17 @@ line-height: 31px;
|
|
|
5864
5962
|
line instead of sitting visibly lower (more obvious once the label wraps to 2 lines). */
|
|
5865
5963
|
margin-top: 7px;
|
|
5866
5964
|
}
|
|
5965
|
+
@media (max-width: 600px) {
|
|
5966
|
+
.md3 .oo-terms-block .mcheck {
|
|
5967
|
+
padding: 0;
|
|
5968
|
+
}
|
|
5969
|
+
.md3 .oo-terms-block .mcheck-box {
|
|
5970
|
+
margin-top: 0;
|
|
5971
|
+
}
|
|
5972
|
+
.md3 .oo-terms-block .mcheck-label {
|
|
5973
|
+
padding-top: 0;
|
|
5974
|
+
}
|
|
5975
|
+
}
|
|
5867
5976
|
/* Error state (Figma 64670:106293, "Jetzt kaufen" clicked without confirming both agreements) —
|
|
5868
5977
|
unconfirmed checkbox gets a thicker red box, plus an icon+message row underneath, indented
|
|
5869
5978
|
32px (20px box + 12px gap) to sit under the label rather than the box. errorIcon (FormCheckbox
|
|
@@ -6026,6 +6135,10 @@ line-height: 31px;
|
|
|
6026
6135
|
background: var(--md-outline-variant);
|
|
6027
6136
|
}
|
|
6028
6137
|
}
|
|
6138
|
+
/* legal-document dialogs: title clears .mmodal-head-action too (24 + 40 + 4 + 40) */
|
|
6139
|
+
.md3 .oo-terms-modal--doc .mmodal-head {
|
|
6140
|
+
padding-right: 108px;
|
|
6141
|
+
}
|
|
6029
6142
|
/* "8. Rabatt-Code hinzufügen" card (Figma 63570:28464) — its own full-width .co-card at the
|
|
6030
6143
|
bottom of the page. Unlike "6."/"7." (where only the small chevron button toggles), the whole
|
|
6031
6144
|
header row is the click target here — the .h3.co-card-title wraps a full-width
|
|
@@ -6366,7 +6479,7 @@ line-height: 31px;
|
|
|
6366
6479
|
/* every narrowed checkout field (Vorname/Nachname/Straße/Nr./PLZ/Stadt/…) has the same
|
|
6367
6480
|
.mfield flex gap:10px reserving room for the trailing .mfield-ok checkmark — fine at
|
|
6368
6481
|
full width, but at mobile widths it was squeezing the input text out of view (same
|
|
6369
|
-
root cause the .co-phone
|
|
6482
|
+
root cause the .co-phone rules above already address for Vorwahl/Telefonnummer;
|
|
6370
6483
|
this covers the rest of .co-form). Taking the icon out of flow removes that reserved
|
|
6371
6484
|
space; the field's own right padding is bumped so text still clears the icon. */
|
|
6372
6485
|
.md3 .co-form .mfield-ok {
|
|
@@ -6399,20 +6512,6 @@ line-height: 31px;
|
|
|
6399
6512
|
.md3 .co-phone .co-birthdate {
|
|
6400
6513
|
grid-column: 1 / -1;
|
|
6401
6514
|
}
|
|
6402
|
-
/* Vorwahl/Telefonnummer at 78px/1fr are too narrow for the flex gap:10px .mfield base rule
|
|
6403
|
-
to reserve room for .mfield-ok inline — it was squeezing the input itself. Taking the
|
|
6404
|
-
checkmark out of flow (.mfield already has position:relative, base rule above) removes
|
|
6405
|
-
that reserved space; the input's own right padding (from .mfield--floating.mfield--valid,
|
|
6406
|
-
10px) is bumped so its text still clears the icon. */
|
|
6407
|
-
.md3 .co-phone .mfield-ok {
|
|
6408
|
-
position: absolute;
|
|
6409
|
-
top: 50%;
|
|
6410
|
-
right: 10px;
|
|
6411
|
-
transform: translateY(-50%);
|
|
6412
|
-
}
|
|
6413
|
-
.md3 .co-phone .mfield--floating.mfield--valid {
|
|
6414
|
-
padding-right: 34px;
|
|
6415
|
-
}
|
|
6416
6515
|
.md3 .co-address-row {
|
|
6417
6516
|
display: flex;
|
|
6418
6517
|
flex-direction: column;
|
|
@@ -14072,6 +14171,10 @@ color: #005CFF;
|
|
|
14072
14171
|
max-width: 800px;
|
|
14073
14172
|
}
|
|
14074
14173
|
|
|
14174
|
+
/* the dialog itself takes focus on open (Modal.tsx): keep its shadow, not the generic focus ring */
|
|
14175
|
+
.md3 .mmodal:focus-visible {
|
|
14176
|
+
box-shadow: var(--md-e3) !important;
|
|
14177
|
+
}
|
|
14075
14178
|
.md3 .mmodal-head {
|
|
14076
14179
|
flex: none;
|
|
14077
14180
|
display: flex;
|
|
@@ -14115,6 +14218,25 @@ color: #005CFF;
|
|
|
14115
14218
|
.md3 .mmodal-close:hover {
|
|
14116
14219
|
background: var(--md-sc-high);
|
|
14117
14220
|
}
|
|
14221
|
+
/* Modal.tsx headerAction: absolute like .mmodal-close, 4px to its left (right = 24 + 40 + 4). */
|
|
14222
|
+
.md3 .mmodal-head-action {
|
|
14223
|
+
position: absolute;
|
|
14224
|
+
top: 20px;
|
|
14225
|
+
right: 68px;
|
|
14226
|
+
width: 40px;
|
|
14227
|
+
height: 40px;
|
|
14228
|
+
padding: 0;
|
|
14229
|
+
border: 0;
|
|
14230
|
+
border-radius: var(--md-r-full);
|
|
14231
|
+
background: transparent;
|
|
14232
|
+
cursor: pointer;
|
|
14233
|
+
display: inline-flex;
|
|
14234
|
+
align-items: center;
|
|
14235
|
+
justify-content: center;
|
|
14236
|
+
}
|
|
14237
|
+
.md3 .mmodal-head-action:hover {
|
|
14238
|
+
background: var(--md-sc-high);
|
|
14239
|
+
}
|
|
14118
14240
|
|
|
14119
14241
|
.md3 .mmodal-body {
|
|
14120
14242
|
flex: 1 1 auto;
|
package/ui/Modal.tsx
CHANGED
|
@@ -51,6 +51,8 @@ export interface ModalProps {
|
|
|
51
51
|
* md3.css), so callers with a specific close-icon spec (exact asset, absolute position
|
|
52
52
|
* independent of title height) aren't stuck fighting the default's flex layout. */
|
|
53
53
|
closeIcon?: React.ReactNode;
|
|
54
|
+
/** optional extra header button rendered before the close button (e.g. a download action). */
|
|
55
|
+
headerAction?: React.ReactNode;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
58
|
// focusable descendants used by the basic focus trap.
|
|
@@ -67,6 +69,7 @@ const Modal: React.FunctionComponent<ModalProps> = ({
|
|
|
67
69
|
dismissOnScrim = true,
|
|
68
70
|
className,
|
|
69
71
|
closeIcon,
|
|
72
|
+
headerAction,
|
|
70
73
|
}) => {
|
|
71
74
|
const surfaceRef = React.useRef<HTMLDivElement | null>(null);
|
|
72
75
|
// the element focused before the dialog opened, so focus can be restored on close.
|
|
@@ -133,7 +136,7 @@ const Modal: React.FunctionComponent<ModalProps> = ({
|
|
|
133
136
|
const prevOverflow = document.body.style.overflow;
|
|
134
137
|
document.body.style.overflow = 'hidden';
|
|
135
138
|
|
|
136
|
-
//
|
|
139
|
+
// focus the dialog surface, not its first control: a header icon would take the :focus-visible ring on open.
|
|
137
140
|
// { preventScroll: true }: this runs at the very first animation frame of the mobile
|
|
138
141
|
// sheet's own mmodal-sheet-up entrance (md3.css) — at that instant the surface's actual
|
|
139
142
|
// computed transform is still the keyframe's `from` state (translateY(100%), pushed
|
|
@@ -145,10 +148,7 @@ const Modal: React.FunctionComponent<ModalProps> = ({
|
|
|
145
148
|
// skips that native auto-scroll entirely, so the CSS animation alone moves the sheet
|
|
146
149
|
// (same fix as fe-main's BaseModal.tsx, which hit this same glitch first).
|
|
147
150
|
const surface = surfaceRef.current;
|
|
148
|
-
if (surface) {
|
|
149
|
-
const first = surface.querySelector<HTMLElement>(FOCUSABLE);
|
|
150
|
-
(first || surface).focus({ preventScroll: true });
|
|
151
|
-
}
|
|
151
|
+
if (surface) surface.focus({ preventScroll: true });
|
|
152
152
|
|
|
153
153
|
return () => {
|
|
154
154
|
document.body.style.overflow = prevOverflow;
|
|
@@ -237,6 +237,7 @@ const Modal: React.FunctionComponent<ModalProps> = ({
|
|
|
237
237
|
) : (
|
|
238
238
|
<span className="mmodal-title" aria-hidden="true" />
|
|
239
239
|
)}
|
|
240
|
+
{headerAction}
|
|
240
241
|
{closeIcon ? (
|
|
241
242
|
<button type="button" className="mmodal-close" aria-label="Schließen" onClick={onClose}>
|
|
242
243
|
{closeIcon}
|
|
@@ -165,7 +165,10 @@ const getDecoratedLightProps = (
|
|
|
165
165
|
if (hybridPlugin) {
|
|
166
166
|
if (wltpWeightedPowerCombinedContent && wltpWeightedCombinedContent) {
|
|
167
167
|
const dischargedCombined = Number.isFinite(wltpConsumptionCombined) ? wltpConsumptionCombined : 19.9;
|
|
168
|
-
|
|
168
|
+
// ADV-187: the discharged class is derived server-side from wltpCo2Discharged; when the
|
|
169
|
+
// backend could not derive it the field is selector_unknown — hide the segment, never
|
|
170
|
+
// fall back to a hard-coded class here.
|
|
171
|
+
const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass;
|
|
169
172
|
const weightedCombinedLabeled = t('vehicleProps:value.wltpWeightedCombinedLabel', { consumption: wltpWeightedCombined.toLocaleString(language), unit: translatedUnit });
|
|
170
173
|
const dischargedCombinedLabeled = t('vehicleProps:value.wltpDischargedCombinedLabel', { consumption: dischargedCombined.toLocaleString(language), unit: translatedUnit });
|
|
171
174
|
const co2WeightedContent = Number.isFinite(wltpCo2)
|
|
@@ -174,14 +177,16 @@ const getDecoratedLightProps = (
|
|
|
174
177
|
const co2ClassWeighted = isPropDefined(wltpEnergyEfficiencyClass)
|
|
175
178
|
? t('vehicleProps:value.co2ClassWeighted', { class: t(`cbd:${wltpEnergyEfficiencyClass}`) })
|
|
176
179
|
: null;
|
|
177
|
-
const co2ClassDischarged =
|
|
180
|
+
const co2ClassDischarged = isPropDefined(dischargedClass)
|
|
181
|
+
? t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) })
|
|
182
|
+
: null;
|
|
178
183
|
return <React.Fragment>
|
|
179
184
|
{`${wltpWeightedPowerCombinedContent} + `}
|
|
180
185
|
{`${weightedCombinedLabeled}, `}
|
|
181
186
|
{`${dischargedCombinedLabeled}`}
|
|
182
187
|
{co2WeightedContent && <>, {co2WeightedContent}</>}
|
|
183
188
|
{co2ClassWeighted && <>, {co2ClassWeighted}</>}
|
|
184
|
-
{<>, {co2ClassDischarged}</>}
|
|
189
|
+
{co2ClassDischarged && <>, {co2ClassDischarged}</>}
|
|
185
190
|
</React.Fragment>
|
|
186
191
|
}
|
|
187
192
|
|
|
@@ -316,7 +321,10 @@ const getDecoratedLightProps = (
|
|
|
316
321
|
if (hybridPlugin) {
|
|
317
322
|
if (wltpWeightedPowerCombinedContent && wltpWeightedCombinedContent) {
|
|
318
323
|
const dischargedCombined = Number.isFinite(wltpConsumptionCombined) ? wltpConsumptionCombined : 19.9;
|
|
319
|
-
|
|
324
|
+
// ADV-187: the discharged class is derived server-side from wltpCo2Discharged; when the
|
|
325
|
+
// backend could not derive it the field is selector_unknown — hide the segment, never
|
|
326
|
+
// fall back to a hard-coded class here.
|
|
327
|
+
const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass;
|
|
320
328
|
const weightedCombinedLabeled = t('vehicleProps:value.wltpWeightedCombinedLabel', { consumption: wltpWeightedCombined.toLocaleString(language), unit: translatedUnit });
|
|
321
329
|
const dischargedCombinedLabeled = t('vehicleProps:value.wltpDischargedCombinedLabel', { consumption: dischargedCombined.toLocaleString(language), unit: translatedUnit });
|
|
322
330
|
const co2WeightedContent = Number.isFinite(wltpCo2)
|
|
@@ -325,14 +333,16 @@ const getDecoratedLightProps = (
|
|
|
325
333
|
const co2ClassWeighted = isPropDefined(wltpEnergyEfficiencyClass)
|
|
326
334
|
? t('vehicleProps:value.co2ClassWeighted', { class: t(`cbd:${wltpEnergyEfficiencyClass}`) })
|
|
327
335
|
: null;
|
|
328
|
-
const co2ClassDischarged =
|
|
336
|
+
const co2ClassDischarged = isPropDefined(dischargedClass)
|
|
337
|
+
? t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) })
|
|
338
|
+
: null;
|
|
329
339
|
return <React.Fragment>
|
|
330
340
|
{`${wltpWeightedPowerCombinedContent} + `}
|
|
331
341
|
{`${weightedCombinedLabeled}, `}
|
|
332
342
|
{`${dischargedCombinedLabeled}`}
|
|
333
343
|
{co2WeightedContent && <>, {co2WeightedContent}</>}
|
|
334
344
|
{co2ClassWeighted && <>, {co2ClassWeighted}</>}
|
|
335
|
-
{<>, {co2ClassDischarged}</>}
|
|
345
|
+
{co2ClassDischarged && <>, {co2ClassDischarged}</>}
|
|
336
346
|
</React.Fragment>
|
|
337
347
|
}
|
|
338
348
|
|
|
@@ -880,10 +880,12 @@ const getDecoratedProps = (
|
|
|
880
880
|
// (entladen, komb) fuel consumption + a discharged CO2 class.
|
|
881
881
|
if (hybridPlugin && wltpConsumptionWeightedPowerCombinedContent && wltpConsumptionWeightedCombinedContent) {
|
|
882
882
|
const ADV88_DEFAULT_DISCHARGED_COMBINED = 19.9;
|
|
883
|
-
const ADV88_DEFAULT_DISCHARGED_CO2_CLASS = 'selector_energyEfficiencyClass_g';
|
|
884
883
|
|
|
885
884
|
const dischargedCombined = Number.isFinite(wltpCombined) ? wltpCombined : ADV88_DEFAULT_DISCHARGED_COMBINED;
|
|
886
|
-
|
|
885
|
+
// ADV-187: the discharged class is derived server-side from wltpCo2Discharged; when the
|
|
886
|
+
// backend could not derive it the field is selector_unknown — hide the segment, never
|
|
887
|
+
// fall back to a hard-coded class here.
|
|
888
|
+
const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass;
|
|
887
889
|
|
|
888
890
|
const weightedCombinedLabeled = t('vehicleProps:value.wltpWeightedCombinedLabel', { consumption: wltpWeightedCombined.toLocaleString(language), unit: translatedUnit });
|
|
889
891
|
const dischargedCombinedLabeled = t('vehicleProps:value.wltpDischargedCombinedLabel', { consumption: dischargedCombined.toLocaleString(language), unit: translatedUnit });
|
|
@@ -893,7 +895,9 @@ const getDecoratedProps = (
|
|
|
893
895
|
const co2ClassWeighted = isPropDefined(wltpEnergyEfficiencyClass)
|
|
894
896
|
? t('vehicleProps:value.co2ClassWeighted', { class: t(`cbd:${wltpEnergyEfficiencyClass}`) })
|
|
895
897
|
: null;
|
|
896
|
-
const co2ClassDischarged =
|
|
898
|
+
const co2ClassDischarged = isPropDefined(dischargedClass)
|
|
899
|
+
? t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) })
|
|
900
|
+
: null;
|
|
897
901
|
|
|
898
902
|
return <React.Fragment>
|
|
899
903
|
{wrapValue(wltpConsumptionWeightedPowerCombinedContent)}{' + '}
|
|
@@ -901,7 +905,7 @@ const getDecoratedProps = (
|
|
|
901
905
|
{wrapValue(dischargedCombinedLabeled)}
|
|
902
906
|
{co2WeightedContent && <>{', '}{wrapValue(co2WeightedContent)}</>}
|
|
903
907
|
{co2ClassWeighted && <>{', '}{co2ClassWeighted}</>}
|
|
904
|
-
{', '}{co2ClassDischarged}
|
|
908
|
+
{co2ClassDischarged && <>{', '}{co2ClassDischarged}</>}
|
|
905
909
|
</React.Fragment>
|
|
906
910
|
}
|
|
907
911
|
|