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

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,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
- <PriceHero car={car} mode={mode} t={t} alwaysShowUnit />
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
- <PriceTerms
286
- car={car}
287
- mode={mode}
288
- onOpenRateAdjust={onOpenRateAdjust || (() => onOpen(car))}
289
- onShowDetails={onShowFinancingDetails ? () => onShowFinancingDetails(car, mode) : undefined}
290
- t={t}
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
@@ -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
- if (mode === 'buy') return null;
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;
package/constants/app.ts CHANGED
@@ -40,7 +40,17 @@ export const LOCAL_STORAGE_KEY = {
40
40
  i18n_KEY: 'i18nLanguage'
41
41
  };
42
42
 
43
- export const DEFAULT_LANG: string = typeof window !== 'undefined' ? localStorage.getItem(LOCAL_STORAGE_KEY.i18n_KEY) : 'de';
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
- export const DEFAULT_LANG: string = (typeof window !== 'undefined' ? window.localStorage.getItem(LOCAL_STORAGE_KEY.i18n_KEY) : null) ?? 'de';
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
@@ -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
- co2ClassDischarged: `CO2-Klasse ${(cbd.cbd as any)[vehicleOption.wltpDischargedEnergyEfficiencyClass || 'selector_energyEfficiencyClass_g'] || ''} (entladen, 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)`
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.160",
3
+ "version": "1.0.162",
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
@@ -10100,7 +10100,8 @@ line-height: 31px;
10100
10100
  z-index: 71;
10101
10101
  background: var(--md-surface);
10102
10102
  border-radius: var(--md-r-xl) var(--md-r-xl) 0 0;
10103
- max-height: 92vh;
10103
+ /* % of the fixed containing block, not vh/dvh: on iOS vh is the toolbars-collapsed height, and Chrome on iOS does not shrink dvh while its toolbars are shown either, so a 92vh/92dvh bottom sheet grew under the address bar and hid its own header (title, download, close). The fixed box itself always matches the visible area. */
10104
+ max-height: 92%;
10104
10105
  display: flex;
10105
10106
  flex-direction: column;
10106
10107
  box-shadow: 0 -8px 30px rgba(16, 22, 42, 0.22);
@@ -14151,7 +14152,8 @@ color: #005CFF;
14151
14152
  flex-direction: column;
14152
14153
  width: 100%;
14153
14154
  max-width: 560px;
14154
- max-height: calc(100vh - 32px);
14155
+ /* % of the scrim (fixed, inset 0, 16px padding) instead of calc(100vh - 32px): see the iOS note on the mobile .mmodal below */
14156
+ max-height: 100%;
14155
14157
  background: var(--md-sc-lowest, #fff);
14156
14158
  color: var(--md-on-surface);
14157
14159
  border-radius: var(--md-r-lg);
@@ -14297,7 +14299,8 @@ color: #005CFF;
14297
14299
  }
14298
14300
  .md3 .mmodal {
14299
14301
  max-width: 100%;
14300
- max-height: 92vh;
14302
+ /* % of the fixed containing block, not vh/dvh: on iOS vh is the toolbars-collapsed height, and Chrome on iOS does not shrink dvh while its toolbars are shown either, so a 92vh/92dvh bottom sheet grew under the address bar and hid its own header (title, download, close). The fixed box itself always matches the visible area. */
14303
+ max-height: 92%;
14301
14304
  border-radius: var(--md-r-lg) var(--md-r-lg) 0 0;
14302
14305
  border-bottom: 0;
14303
14306
  animation: mmodal-sheet-up 0.25s ease-out;
@@ -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
- const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass || 'selector_energyEfficiencyClass_g';
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 = t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) });
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
- const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass || 'selector_energyEfficiencyClass_g';
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 = t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) });
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
- const dischargedClass = car.environmentEmissions.wltpDischargedEnergyEfficiencyClass || ADV88_DEFAULT_DISCHARGED_CO2_CLASS;
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 = t('vehicleProps:value.co2ClassDischarged', { class: t(`cbd:${dischargedClass}`) });
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