@bytebrand/fe-ui-core-autobahn 1.0.59 → 1.0.60

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/CarRow.tsx CHANGED
@@ -31,6 +31,8 @@ import {
31
31
  OfferBadge,
32
32
  PriceTabs,
33
33
  PriceHero,
34
+ PricePaths,
35
+ PriceKauf,
34
36
  DropChip,
35
37
  PercentChip,
36
38
  ZeroDownChip,
@@ -104,7 +106,8 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
104
106
  <div style={{ display: 'flex', flexWrap: 'nowrap', alignItems: 'center', gap: 5 }}>
105
107
  {car.isNew && <span className="neu-badge">NEU</span>}
106
108
  {car.drop != null ? <DropChip t={t} amount={car.drop} /> : showSave && <SaveChip t={t} amount={car.savings!} />}
107
- <ZeroDownChip t={t} show={car.zeroDown} compact />
109
+ <ZeroDownChip t={t} show={car.zeroDown} compact className="show-mobile" />
110
+ <ZeroDownChip t={t} show={car.zeroDown} className="hide-mobile" />
108
111
  <PercentChip pct={car.dropPct} />
109
112
  </div>
110
113
  <OfferBadge show={showOfferBadge} t={t} />
@@ -121,6 +124,14 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
121
124
  >
122
125
  <IconButton name="heart" filled active={isFavorite} onClick={onToggleFavorite} label={t('autobahn:card.action.save')} />
123
126
  </div>
127
+ {/* desktop: heart sits on the photo itself (Figma "mcard"), not next to the title. */}
128
+ <div
129
+ onClick={(e) => e.stopPropagation()}
130
+ className="hide-mobile"
131
+ style={{ position: 'absolute', top: 10, right: 10, zIndex: 3 }}
132
+ >
133
+ <IconButton name="heart" active={isFavorite} onClick={onToggleFavorite} label={t('autobahn:card.action.save')} />
134
+ </div>
124
135
  {/* activity/trust badges — overlaid on the studio-white floor at the bottom of the photo
125
136
  itself (PM ref), not a separate divided strip below it. Bottom-right, raised above the
126
137
  carousel dots and the photo-count badge (positioning lives in .gallery-badges, md3.css). */}
@@ -128,17 +139,12 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
128
139
  </div>
129
140
 
130
141
  <div className="listcard-info">
131
- <div style={{ display: 'flex', justifyContent: 'space-between', gap: 12 }}>
132
- <div style={{ minWidth: 0 }}>
133
- <div className="md-title" style={{ fontSize: 18, lineHeight: 1.2 }}>
134
- {car.make} {car.model}
135
- </div>
136
- <div className="md-body-sm" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
137
- {car.trim}
138
- </div>
142
+ <div style={{ minWidth: 0 }}>
143
+ <div className="md-title" style={{ fontSize: 18, lineHeight: 1.2 }}>
144
+ {car.make} {car.model}
139
145
  </div>
140
- <div onClick={(e) => e.stopPropagation()} className="hide-mobile">
141
- <IconButton name="heart" active={isFavorite} onClick={onToggleFavorite} label={t('autobahn:card.action.save')} />
146
+ <div className="md-body-sm" style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
147
+ {car.trim}
142
148
  </div>
143
149
  </div>
144
150
  <div className="specstrip">
@@ -160,12 +166,13 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
160
166
  <EcoLine t={t} car={car} />
161
167
  </div>
162
168
 
163
- {/* price column: Finanzierung/Leasing/Kaufen tabs pick ONE hero price (same tabbed
164
- component the grid CarCard uses), then the fixed Preisbewertung meter and the CTA,
165
- anchored to the bottom (margin-top:auto) so the button sits flush at the foot. */}
169
+ {/* price column — mobile keeps the Finanzierung/Leasing/Kaufen tab switcher (ONE hero price
170
+ at a time); desktop shows all three payment paths stacked at once (Figma "mcard"), via
171
+ the previously-unused PricePaths/PriceKauf pair. Both variants share the same
172
+ Preisbewertung meter + CTA, anchored to the bottom (margin-top:auto on .pp-purchase). */}
166
173
  <div className="listcard-price">
167
- <PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} />
168
- <div className="pp-purchase">
174
+ <PriceTabs car={car} mode={mode} onModeChange={setMode} t={t} className="show-mobile" />
175
+ <div className="pp-purchase show-mobile">
169
176
  <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
170
177
  <PriceHero car={car} mode={mode} t={t} />
171
178
  {car.rating && (
@@ -204,6 +211,49 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
204
211
  )}
205
212
  <PpActivity t={t} car={car} />
206
213
  </div>
214
+
215
+ <PricePaths car={car} t={t} className="hide-mobile" />
216
+ <div className="pp-purchase hide-mobile">
217
+ <PriceKauf car={car} t={t} />
218
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
219
+ {car.rating && (
220
+ <StatChip tone={ratingTone(car.rating)} icon={ratingTone(car.rating) === 'good' ? 'check' : 'minus'}>
221
+ {t('autobahn:tier.' + car.rating)}
222
+ </StatChip>
223
+ )}
224
+ <div style={{ flex: 1 }}>
225
+ <PriceMeter t={t} car={car} />
226
+ </div>
227
+ </div>
228
+ <div className="pp-cta">
229
+ <Button
230
+ variant="filled"
231
+ block
232
+ iconRight="arrow-right"
233
+ onClick={(e) => {
234
+ e.stopPropagation();
235
+ onOpen(car);
236
+ }}
237
+ >
238
+ {t('autobahn:card.cta.viewOffer')}
239
+ </Button>
240
+ </div>
241
+ {adminAction && (
242
+ <div className="pp-cta">
243
+ <Button
244
+ variant="outlined"
245
+ block
246
+ onClick={(e) => {
247
+ e.stopPropagation();
248
+ adminAction.onClick();
249
+ }}
250
+ >
251
+ {adminAction.label}
252
+ </Button>
253
+ </div>
254
+ )}
255
+ <PpActivity t={t} car={car} />
256
+ </div>
207
257
  </div>
208
258
  </article>
209
259
  );
@@ -234,14 +234,18 @@ export const PercentChip: React.FunctionComponent<{ pct?: number }> = ({ pct })
234
234
  * there is room (grid cards / wide image) and uses the compact "0 € Anz." on the narrow list-card
235
235
  * image, so the badge row always stays on ONE line on both desktop and mobile.
236
236
  */
237
- export const ZeroDownChip: React.FunctionComponent<{ show?: boolean; compact?: boolean; t: TranslationFunction }> = ({
238
- show,
239
- compact,
240
- t,
241
- }) => {
237
+ export const ZeroDownChip: React.FunctionComponent<{
238
+ show?: boolean;
239
+ compact?: boolean;
240
+ t: TranslationFunction;
241
+ className?: string;
242
+ }> = ({ show, compact, t, className }) => {
242
243
  if (!show) return null;
243
244
  return (
244
- <span style={{ ...dealPill, background: 'var(--md-accent-container)', color: 'var(--md-on-accent-container)' }}>
245
+ <span
246
+ className={className}
247
+ style={{ ...dealPill, background: 'var(--md-accent-container)', color: 'var(--md-on-accent-container)' }}
248
+ >
245
249
  {compact ? t('autobahn:card.price.zeroDownCompact') : t('autobahn:card.price.zeroDown')}
246
250
  </span>
247
251
  );
@@ -341,6 +345,8 @@ export const EcoLine: React.FunctionComponent<EcoLineProps> = ({ car, style, t }
341
345
  export interface PricePathsProps {
342
346
  car: AutobahnCar;
343
347
  t: TranslationFunction;
348
+ /** extra class appended to the root (e.g. the SRP card's desktop-only `hide-mobile` marker). */
349
+ className?: string;
344
350
  }
345
351
 
346
352
  /** Map the AutobahnCar rating tone → meter segment index (0 best … 4 worst). */
@@ -379,6 +385,8 @@ export interface PriceTabsProps {
379
385
  mode: PaymentMode;
380
386
  onModeChange: (mode: PaymentMode) => void;
381
387
  t: TranslationFunction;
388
+ /** extra class appended to the root (e.g. the SRP card's mobile-only `show-mobile` marker). */
389
+ className?: string;
382
390
  }
383
391
 
384
392
  /**
@@ -387,14 +395,14 @@ export interface PriceTabsProps {
387
395
  * identical to the VDP's, not a second tab style. Leasing tab omitted when the car isn't
388
396
  * leasing-eligible (car.lease undefined) — same rule PricePaths already used.
389
397
  */
390
- export const PriceTabs: React.FunctionComponent<PriceTabsProps> = ({ car, mode, onModeChange, t }) => {
398
+ export const PriceTabs: React.FunctionComponent<PriceTabsProps> = ({ car, mode, onModeChange, t, className }) => {
391
399
  const tabs: Array<[PaymentMode, string]> = [
392
400
  ['financing', t('autobahn:finance.mode.financing')],
393
401
  ...(car.lease != null ? ([['leasing', t('autobahn:finance.mode.leasing')]] as Array<[PaymentMode, string]>) : []),
394
402
  ['buy', t('autobahn:card.price.buy')],
395
403
  ];
396
404
  return (
397
- <div className="fin-tabs" role="tablist" onClick={(e) => e.stopPropagation()}>
405
+ <div className={'fin-tabs' + (className ? ' ' + className : '')} role="tablist" onClick={(e) => e.stopPropagation()}>
398
406
  {tabs.map(([m, label]) => (
399
407
  <button
400
408
  key={m}
@@ -589,7 +597,7 @@ const PpRow: React.FunctionComponent<PpRowProps> = ({ label, value, unit, old, f
589
597
  * is omitted entirely when leasing isn't active. The Kaufen row is rendered separately
590
598
  * (PriceKauf) below a divider so only the financing rate stands out — no wall of blue prices.
591
599
  */
592
- export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t }) => {
600
+ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t, className }) => {
593
601
  const hasLease = car.lease != null;
594
602
  // Terms of the config the displayed rate was actually computed with (per-car:
595
603
  // dealer rate + the user's saved localStorage adjustments) — default config
@@ -601,20 +609,34 @@ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t })
601
609
  const leasMonths = car.leaseMonths ?? _leasPeriod;
602
610
  const hasLeasTerms = Boolean(leasMileage && leasMonths);
603
611
  const leasMileageStr = leasMileage ? Number(leasMileage).toLocaleString('de-DE') : '';
612
+ // same real struck-through reference rate PriceHero shows for the financing tab (oldMonthly) —
613
+ // shown here too now that both rows render at once instead of behind a tab.
614
+ const showOldMonthly = car.oldMonthly != null && car.oldMonthly > car.monthly;
604
615
  return (
605
- <div className="pricepaths">
616
+ <div className={'pricepaths' + (className ? ' ' + className : '')}>
606
617
  <div className="pp-hero">
607
618
  <div className="pp-eyebrow">{t('autobahn:card.price.financingFrom')}</div>
608
619
  <div className="pp-hero-rate">
609
- <b className="num pp-hero-val">{fmtRate(car.monthly)} €</b>
610
- <span className="pp-hero-unit">{t('autobahn:card.price.perMonth')}</span>
620
+ {showOldMonthly && (
621
+ <span className="pp-hero-old">
622
+ {(car.oldMonthly as number).toLocaleString('de-DE')} €<sup>2</sup>
623
+ </span>
624
+ )}
625
+ <b className="num pp-hero-val">
626
+ {fmtRate(car.monthly)} €<sup>1</sup>
627
+ </b>
628
+ <span className="pp-hero-unit">
629
+ {t('autobahn:card.price.perMonth')}
630
+ <sup>3</sup>
631
+ </span>
611
632
  </div>
612
633
  <div className="pp-hero-terms">
613
634
  {t('autobahn:card.price.financeTerms', {
614
635
  firstInstallment: finFirstInstallment,
615
636
  months: finMonths,
616
637
  count: finMonths,
617
- })}
638
+ })}{' '}
639
+ · <span className="pp-details-link">{t('autobahn:card.price.details')}</span>
618
640
  </div>
619
641
  </div>
620
642
  {hasLease && (
@@ -622,7 +644,8 @@ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t })
622
644
  <PpRow label={t('autobahn:card.price.leasing')} value={car.lease as number} unit="mtl." />
623
645
  {hasLeasTerms && (
624
646
  <div className="pp-sub-terms">
625
- {t('autobahn:card.price.leaseTerms', { mileage: leasMileageStr, months: leasMonths })}
647
+ {t('autobahn:card.price.leaseTerms', { mileage: leasMileageStr, months: leasMonths })}{' '}
648
+ · <span className="pp-details-link">{t('autobahn:card.price.details')}</span>
626
649
  </div>
627
650
  )}
628
651
  </div>
@@ -22,7 +22,8 @@
22
22
  "perMonth": "/mtl.",
23
23
  "leasing": "Leasing",
24
24
  "leasingFrom": "Leasing ab",
25
- "buy": "Kaufen"
25
+ "buy": "Kaufen",
26
+ "details": "Details"
26
27
  },
27
28
  "eco": {
28
29
  "phevConsumptionLine": "+ {{consFuel}} (gew., komb.) · {{consDis}} (entl.) · CO₂ {{co2w}} · Klasse {{co2classW}} (gew.) / {{co2classD}} (entl.)*",
@@ -22,7 +22,8 @@
22
22
  "perMonth": "/mo.",
23
23
  "leasing": "Leasing",
24
24
  "leasingFrom": "Leasing from",
25
- "buy": "Buy"
25
+ "buy": "Buy",
26
+ "details": "Details"
26
27
  },
27
28
  "eco": {
28
29
  "phevConsumptionLine": "+ {{consFuel}} (weighted, combined) · {{consDis}} (discharged) · CO₂ {{co2w}} · Class {{co2classW}} (weighted) / {{co2classD}} (discharged)*",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.59",
3
+ "version": "1.0.60",
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
@@ -3962,6 +3962,23 @@ line-height: 31px;
3962
3962
  background: var(--md-sc-low);
3963
3963
  border-radius: var(--md-r-md);
3964
3964
  }
3965
+ /* desktop only (mobile keeps the flex-wrap flow above, untouched): a clean 2-column grid so
3966
+ pairs line up (km|EZ, Kraftstoff|Getriebe, Leistung|Halter) instead of reflowing by text
3967
+ length (Figma "mcard"). Facts render in DOM order [km, EZ, Getriebe, Kraftstoff, Leistung,
3968
+ Halter, Verfügbarkeit]; only Getriebe/Kraftstoff swap columns via `order` to land as
3969
+ Kraftstoff|Getriebe per the mockup — the array itself stays untouched (mobile relies on it). */
3970
+ @media (min-width: 601px) {
3971
+ .md3 .specstrip {
3972
+ display: grid;
3973
+ grid-template-columns: 1fr 1fr;
3974
+ }
3975
+ /* km/EZ/Kraftstoff pulled negative (in that order) so they sort ahead of the untouched,
3976
+ default-order Getriebe/Leistung/Halter/Verfügbarkeit — net effect: km, EZ, Kraftstoff,
3977
+ Getriebe, Leistung, Halter, Verfügbarkeit, i.e. only Kraftstoff hops in front of Getriebe. */
3978
+ .md3 .specstrip > span:nth-child(1) { order: -3; }
3979
+ .md3 .specstrip > span:nth-child(2) { order: -2; }
3980
+ .md3 .specstrip > span:nth-child(4) { order: -1; }
3981
+ }
3965
3982
  /* filter rail: collapsible sections */
3966
3983
  .md3 .frail-group {
3967
3984
  border-bottom: 1px solid var(--md-outline-variant);
@@ -4199,6 +4216,9 @@ line-height: 31px;
4199
4216
  .md3 .pp-hero-old { font-size: 15px; color: var(--md-error); text-decoration: line-through; white-space: nowrap; }
4200
4217
  .md3 .pp-hero-val sup, .md3 .pp-hero-old sup, .md3 .pp-hero-unit sup { font-size: .6em; }
4201
4218
  .md3 .pp-hero-terms { margin-top: 9px; font-size: 11.5px; color: var(--md-on-surface-variant); }
4219
+ /* "Details" link at the end of the financing/leasing terms lines (Figma "mcard") — quiet, same
4220
+ color as the rest of the terms line but underlined so it still reads as tappable. */
4221
+ .md3 .pp-details-link { color: var(--md-primary); text-decoration: underline; font-weight: 600; white-space: nowrap; }
4202
4222
  /* separator line between the financing block and the leasing row (legacy-design parity) */
4203
4223
  .md3 .pp-lease-row {
4204
4224
  margin-top: 16px;