@bytebrand/fe-ui-core-autobahn 1.0.158 → 1.0.160

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 CHANGED
@@ -157,8 +157,6 @@ 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'];
162
160
  return (
163
161
  <article
164
162
  className="mcard mcard--interactive cc-card"
@@ -279,46 +277,18 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
279
277
  <div className="cc-price" style={{ marginTop: 0, borderTop: '1px solid var(--md-outline-variant)' }}>
280
278
  <PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} />
281
279
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8, marginTop: 10 }}>
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>
280
+ <PriceHero car={car} mode={mode} t={t} alwaysShowUnit />
297
281
  <PriceEvalCol car={car} t={t} />
298
282
  </div>
299
283
  {/* same fallback CardCtaRow already uses below: pages that haven't wired a real
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>
284
+ "Rate anpassen" sheet yet still get a working "Details" link (opens the car). */}
285
+ <PriceTerms
286
+ car={car}
287
+ mode={mode}
288
+ onOpenRateAdjust={onOpenRateAdjust || (() => onOpen(car))}
289
+ onShowDetails={onShowFinancingDetails ? () => onShowFinancingDetails(car, mode) : undefined}
290
+ t={t}
291
+ />
322
292
  </div>
323
293
  {/* Mobile-only row (CardCtaRow itself carries the show-mobile class) — always
324
294
  rendered, with real fallbacks the page can override once it wires its own
@@ -710,12 +710,7 @@ export const PriceTerms: React.FunctionComponent<{
710
710
  onShowDetails?: () => void;
711
711
  t: TranslationFunction;
712
712
  }> = ({ car, mode, onOpenRateAdjust, onShowDetails, t }) => {
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" />;
713
+ if (mode === 'buy') return null;
719
714
  const finFirstInstallment =
720
715
  car.finFirstInstallment != null ? car.finFirstInstallment.toLocaleString('de-DE') : _finFirstInstallment;
721
716
  const finMonths = car.finMonths ?? _finMonths;
package/constants/app.ts CHANGED
@@ -40,17 +40,7 @@ export const LOCAL_STORAGE_KEY = {
40
40
  i18n_KEY: 'i18nLanguage'
41
41
  };
42
42
 
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';
43
+ export const DEFAULT_LANG: string = typeof window !== 'undefined' ? localStorage.getItem(LOCAL_STORAGE_KEY.i18n_KEY) : 'de';
54
44
 
55
45
  // Default support contact phone
56
46
  export const CONTACT_PHONE: string = '+498004030182';
package/constants/core.ts CHANGED
@@ -35,18 +35,7 @@ export const LOCAL_STORAGE_KEY = {
35
35
  i18n_KEY: 'i18nLanguage'
36
36
  };
37
37
 
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';
38
+ export const DEFAULT_LANG: string = (typeof window !== 'undefined' ? window.localStorage.getItem(LOCAL_STORAGE_KEY.i18n_KEY) : null) ?? 'de';
50
39
 
51
40
  export const ROW_STYLES = {
52
41
  marginBottom: 10
@@ -103,8 +103,7 @@ 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
- // 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)`
106
+ co2ClassDischarged: `CO2-Klasse ${(cbd.cbd as any)[vehicleOption.wltpDischargedEnergyEfficiencyClass || 'selector_energyEfficiencyClass_g'] || ''} (entladen, komb)`
108
107
  },
109
108
  title: {
110
109
  'addOfferToMainSlide': 'Zum Schieberegler hinzufügen',
package/package.json CHANGED
@@ -1,30 +1,30 @@
1
- {
2
- "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.158",
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
- "main": "index.ts",
6
- "license": "UNLICENSED",
7
- "peerDependencies": {
8
- "react": "^18.0.0",
9
- "react-dom": "^18.0.0"
10
- },
11
- "dependencies": {
12
- "@bytebrand/car-schema-selectors": "^2.0.7",
13
- "@bytebrand/fe-financing": "^5.14.20",
14
- "classnames": "2.2.6",
15
- "clsx": "^2.1.1",
16
- "d3-format": "1.4.5",
17
- "js-cookie": "^2.2.0",
18
- "lodash": "^4.17.21",
19
- "lucide-react": "^0.288.0",
20
- "mobx-react": "^7.6.0",
21
- "moment": "^2.29.1",
22
- "qs": "^6.6.0",
23
- "react-device-detect": "^2.1.2",
24
- "react-grid-system": "^5.0.2",
25
- "react-lazyload": "^3.2.0",
26
- "react-scroll": "^1.9.3",
27
- "react-slick": "^0.29.0",
28
- "slick-carousel": "1.8.1"
29
- }
30
- }
1
+ {
2
+ "name": "@bytebrand/fe-ui-core-autobahn",
3
+ "version": "1.0.160",
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
+ "main": "index.ts",
6
+ "license": "UNLICENSED",
7
+ "peerDependencies": {
8
+ "react": "^18.0.0",
9
+ "react-dom": "^18.0.0"
10
+ },
11
+ "dependencies": {
12
+ "@bytebrand/car-schema-selectors": "^2.0.7",
13
+ "@bytebrand/fe-financing": "^5.14.20",
14
+ "classnames": "2.2.6",
15
+ "clsx": "^2.1.1",
16
+ "d3-format": "1.4.5",
17
+ "js-cookie": "^2.2.0",
18
+ "lodash": "^4.17.21",
19
+ "lucide-react": "^0.288.0",
20
+ "mobx-react": "^7.6.0",
21
+ "moment": "^2.29.1",
22
+ "qs": "^6.6.0",
23
+ "react-device-detect": "^2.1.2",
24
+ "react-grid-system": "^5.0.2",
25
+ "react-lazyload": "^3.2.0",
26
+ "react-scroll": "^1.9.3",
27
+ "react-slick": "^0.29.0",
28
+ "slick-carousel": "1.8.1"
29
+ }
30
+ }
package/ui/Modal.tsx CHANGED
@@ -136,7 +136,10 @@ const Modal: React.FunctionComponent<ModalProps> = ({
136
136
  const prevOverflow = document.body.style.overflow;
137
137
  document.body.style.overflow = 'hidden';
138
138
 
139
- // focus the dialog surface, not its first control: a header icon would take the :focus-visible ring on open.
139
+ // move focus to the first control in the body/footer (an input, a link, a button) so forms are
140
+ // ready to type into and browser autofill can offer itself; the head row (close + headerAction
141
+ // icons) is skipped so a header icon never takes the :focus-visible ring on open. Falls back to
142
+ // the surface itself when the body has nothing focusable.
140
143
  // { preventScroll: true }: this runs at the very first animation frame of the mobile
141
144
  // sheet's own mmodal-sheet-up entrance (md3.css) — at that instant the surface's actual
142
145
  // computed transform is still the keyframe's `from` state (translateY(100%), pushed
@@ -148,7 +151,12 @@ const Modal: React.FunctionComponent<ModalProps> = ({
148
151
  // skips that native auto-scroll entirely, so the CSS animation alone moves the sheet
149
152
  // (same fix as fe-main's BaseModal.tsx, which hit this same glitch first).
150
153
  const surface = surfaceRef.current;
151
- if (surface) surface.focus({ preventScroll: true });
154
+ if (surface) {
155
+ const first = Array.prototype.slice
156
+ .call(surface.querySelectorAll<HTMLElement>(FOCUSABLE))
157
+ .find((el: HTMLElement) => !el.closest('.mmodal-head'));
158
+ (first || surface).focus({ preventScroll: true });
159
+ }
152
160
 
153
161
  return () => {
154
162
  document.body.style.overflow = prevOverflow;
@@ -165,10 +165,7 @@ const getDecoratedLightProps = (
165
165
  if (hybridPlugin) {
166
166
  if (wltpWeightedPowerCombinedContent && wltpWeightedCombinedContent) {
167
167
  const dischargedCombined = Number.isFinite(wltpConsumptionCombined) ? wltpConsumptionCombined : 19.9;
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;
168
+ const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass || 'selector_energyEfficiencyClass_g';
172
169
  const weightedCombinedLabeled = t('vehicleProps:value.wltpWeightedCombinedLabel', { consumption: wltpWeightedCombined.toLocaleString(language), unit: translatedUnit });
173
170
  const dischargedCombinedLabeled = t('vehicleProps:value.wltpDischargedCombinedLabel', { consumption: dischargedCombined.toLocaleString(language), unit: translatedUnit });
174
171
  const co2WeightedContent = Number.isFinite(wltpCo2)
@@ -177,16 +174,14 @@ const getDecoratedLightProps = (
177
174
  const co2ClassWeighted = isPropDefined(wltpEnergyEfficiencyClass)
178
175
  ? t('vehicleProps:value.co2ClassWeighted', { class: t(`cbd:${wltpEnergyEfficiencyClass}`) })
179
176
  : null;
180
- const co2ClassDischarged = isPropDefined(dischargedClass)
181
- ? t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) })
182
- : null;
177
+ const co2ClassDischarged = t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) });
183
178
  return <React.Fragment>
184
179
  {`${wltpWeightedPowerCombinedContent} + `}
185
180
  {`${weightedCombinedLabeled}, `}
186
181
  {`${dischargedCombinedLabeled}`}
187
182
  {co2WeightedContent && <>, {co2WeightedContent}</>}
188
183
  {co2ClassWeighted && <>, {co2ClassWeighted}</>}
189
- {co2ClassDischarged && <>, {co2ClassDischarged}</>}
184
+ {<>, {co2ClassDischarged}</>}
190
185
  </React.Fragment>
191
186
  }
192
187
 
@@ -321,10 +316,7 @@ const getDecoratedLightProps = (
321
316
  if (hybridPlugin) {
322
317
  if (wltpWeightedPowerCombinedContent && wltpWeightedCombinedContent) {
323
318
  const dischargedCombined = Number.isFinite(wltpConsumptionCombined) ? wltpConsumptionCombined : 19.9;
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;
319
+ const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass || 'selector_energyEfficiencyClass_g';
328
320
  const weightedCombinedLabeled = t('vehicleProps:value.wltpWeightedCombinedLabel', { consumption: wltpWeightedCombined.toLocaleString(language), unit: translatedUnit });
329
321
  const dischargedCombinedLabeled = t('vehicleProps:value.wltpDischargedCombinedLabel', { consumption: dischargedCombined.toLocaleString(language), unit: translatedUnit });
330
322
  const co2WeightedContent = Number.isFinite(wltpCo2)
@@ -333,16 +325,14 @@ const getDecoratedLightProps = (
333
325
  const co2ClassWeighted = isPropDefined(wltpEnergyEfficiencyClass)
334
326
  ? t('vehicleProps:value.co2ClassWeighted', { class: t(`cbd:${wltpEnergyEfficiencyClass}`) })
335
327
  : null;
336
- const co2ClassDischarged = isPropDefined(dischargedClass)
337
- ? t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) })
338
- : null;
328
+ const co2ClassDischarged = t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) });
339
329
  return <React.Fragment>
340
330
  {`${wltpWeightedPowerCombinedContent} + `}
341
331
  {`${weightedCombinedLabeled}, `}
342
332
  {`${dischargedCombinedLabeled}`}
343
333
  {co2WeightedContent && <>, {co2WeightedContent}</>}
344
334
  {co2ClassWeighted && <>, {co2ClassWeighted}</>}
345
- {co2ClassDischarged && <>, {co2ClassDischarged}</>}
335
+ {<>, {co2ClassDischarged}</>}
346
336
  </React.Fragment>
347
337
  }
348
338
 
@@ -880,12 +880,10 @@ 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';
883
884
 
884
885
  const dischargedCombined = Number.isFinite(wltpCombined) ? wltpCombined : ADV88_DEFAULT_DISCHARGED_COMBINED;
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;
886
+ const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass || ADV88_DEFAULT_DISCHARGED_CO2_CLASS;
889
887
 
890
888
  const weightedCombinedLabeled = t('vehicleProps:value.wltpWeightedCombinedLabel', { consumption: wltpWeightedCombined.toLocaleString(language), unit: translatedUnit });
891
889
  const dischargedCombinedLabeled = t('vehicleProps:value.wltpDischargedCombinedLabel', { consumption: dischargedCombined.toLocaleString(language), unit: translatedUnit });
@@ -895,9 +893,7 @@ const getDecoratedProps = (
895
893
  const co2ClassWeighted = isPropDefined(wltpEnergyEfficiencyClass)
896
894
  ? t('vehicleProps:value.co2ClassWeighted', { class: t(`cbd:${wltpEnergyEfficiencyClass}`) })
897
895
  : null;
898
- const co2ClassDischarged = isPropDefined(dischargedClass)
899
- ? t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) })
900
- : null;
896
+ const co2ClassDischarged = t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) });
901
897
 
902
898
  return <React.Fragment>
903
899
  {wrapValue(wltpConsumptionWeightedPowerCombinedContent)}{' + '}
@@ -905,7 +901,7 @@ const getDecoratedProps = (
905
901
  {wrapValue(dischargedCombinedLabeled)}
906
902
  {co2WeightedContent && <>{', '}{wrapValue(co2WeightedContent)}</>}
907
903
  {co2ClassWeighted && <>{', '}{co2ClassWeighted}</>}
908
- {co2ClassDischarged && <>{', '}{co2ClassDischarged}</>}
904
+ {', '}{co2ClassDischarged}
909
905
  </React.Fragment>
910
906
  }
911
907