@bytebrand/fe-ui-core-autobahn 1.0.82 → 1.0.84

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
@@ -102,6 +102,14 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
102
102
  const [viewerOpen, setViewerOpen] = useState(false);
103
103
  const showSave = car.savings != null && car.savings > 600;
104
104
  const showOfferBadge = !!(car.isNew || car.drop != null || showSave || car.dropPct != null || car.zeroDown);
105
+ // PM call: the badge row only fits 3 pills — when Drop/Save + Percent + ZeroDown are all
106
+ // active that's already 3, so NEU is dropped rather than causing a 4th to overflow.
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;
105
113
  return (
106
114
  <article
107
115
  className="mcard mcard--interactive cc-card"
@@ -148,7 +156,9 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
148
156
  t={t}
149
157
  show={car.zeroDown}
150
158
  style={{
151
- width: 104,
159
+ // "€0 down payment" (en) doesn't fit the German pill's 104px — only German's
160
+ // "0 € Anzahlung" gets the narrow width, every other locale gets more room.
161
+ width: t('autobahn:card.price.zeroDown') === '0 € Anzahlung' ? 104 : 125,
152
162
  height: 30,
153
163
  padding: 0,
154
164
  justifyContent: 'center',
@@ -162,7 +172,7 @@ const CarCard: React.FunctionComponent<CarCardProps> = ({
162
172
  color: '#005CFF',
163
173
  }}
164
174
  />
165
- {car.isNew && <span className="neu-badge">NEU</span>}
175
+ {showNeu && <span className="neu-badge">NEU</span>}
166
176
  </div>
167
177
  <OfferBadge show={showOfferBadge} t={t} />
168
178
  </div>
package/cards/CarRow.tsx CHANGED
@@ -60,13 +60,36 @@ export interface CarRowProps {
60
60
  * Rendered as a quiet button under the CTA; the page decides visibility (JWT claim).
61
61
  */
62
62
  adminAction?: { label: string; onClick: () => void };
63
+ /**
64
+ * takes over the desktop price column's "Details" links (financing/leasing terms rows) — e.g.
65
+ * navigate to this car's own VDP and open its finance modal already expanded, on the tab the
66
+ * link was under. Optional/page-owned like CarCard's identically-named prop; when omitted the
67
+ * links render as plain text (see PricePaths' own fallback).
68
+ */
69
+ onShowFinancingDetails?: (car: AutobahnCar, mode: PaymentMode) => void;
63
70
  }
64
71
 
65
- const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggleFavorite, onOpen, onMediaEnter, t, adminAction }) => {
72
+ const CarRow: React.FunctionComponent<CarRowProps> = ({
73
+ car,
74
+ isFavorite,
75
+ onToggleFavorite,
76
+ onOpen,
77
+ onMediaEnter,
78
+ t,
79
+ adminAction,
80
+ onShowFinancingDetails,
81
+ }) => {
66
82
  const [mode, setMode] = useState<PaymentMode>('financing');
67
83
  const [photoIndex, setPhotoIndex] = useState(0);
68
84
  const showSave = car.savings != null && car.savings > 600;
69
85
  const showOfferBadge = !!(car.isNew || car.drop != null || showSave || car.dropPct != null || car.zeroDown);
86
+ // same overflow rule as CarCard: the row only fits 3 pills, so NEU drops when the other 3 are all active.
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;
70
93
  const facts: Array<[icon: string, value: string]> = [
71
94
  ['gauge', fmtKm(car.km)],
72
95
  ['calendar', t('autobahn:card.specs.firstRegistration', { reg: car.reg })],
@@ -106,7 +129,7 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
106
129
  }}
107
130
  >
108
131
  <div style={{ display: 'flex', flexWrap: 'nowrap', alignItems: 'center', gap: 5 }}>
109
- {car.isNew && <span className="neu-badge">NEU</span>}
132
+ {showNeu && <span className="neu-badge">NEU</span>}
110
133
  {car.drop != null ? <DropChip t={t} amount={car.drop} /> : showSave && <SaveChip t={t} amount={car.savings!} />}
111
134
  <ZeroDownChip t={t} show={car.zeroDown} compact className="show-mobile" />
112
135
  <ZeroDownChip
@@ -259,7 +282,12 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
259
282
  <PpActivity t={t} car={car} />
260
283
  </div>
261
284
 
262
- <PricePaths car={car} t={t} className="hide-mobile" />
285
+ <PricePaths
286
+ car={car}
287
+ t={t}
288
+ className="hide-mobile"
289
+ onShowDetails={onShowFinancingDetails ? (mode) => onShowFinancingDetails(car, mode) : undefined}
290
+ />
263
291
  <div className="pp-purchase hide-mobile">
264
292
  <PriceKauf car={car} t={t} />
265
293
  <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
@@ -386,6 +386,11 @@ export interface PricePathsProps {
386
386
  t: TranslationFunction;
387
387
  /** extra class appended to the root (e.g. the SRP card's desktop-only `hide-mobile` marker). */
388
388
  className?: string;
389
+ /** takes over the financing/leasing terms rows' own "Details" links — e.g. navigate to this
390
+ * car's VDP and open its finance modal already expanded, on the tab the link was under
391
+ * (financing/leasing). Optional/page-owned like CarCard's onShowFinancingDetails; when
392
+ * omitted the links render as plain (non-interactive) text, same as before this prop existed. */
393
+ onShowDetails?: (mode: PaymentMode) => void;
389
394
  }
390
395
 
391
396
  /** Map the AutobahnCar rating tone → meter segment index (0 best … 4 worst). */
@@ -926,7 +931,7 @@ const PpRow: React.FunctionComponent<PpRowProps> = ({ label, value, unit, old, f
926
931
  * is omitted entirely when leasing isn't active. The Kaufen row is rendered separately
927
932
  * (PriceKauf) below a divider so only the financing rate stands out — no wall of blue prices.
928
933
  */
929
- export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t, className }) => {
934
+ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t, className, onShowDetails }) => {
930
935
  const hasLease = car.lease != null;
931
936
  // Terms of the config the displayed rate was actually computed with (per-car:
932
937
  // dealer rate + the user's saved localStorage adjustments) — default config
@@ -938,6 +943,21 @@ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t, c
938
943
  const leasMonths = car.leaseMonths ?? _leasPeriod;
939
944
  const hasLeasTerms = Boolean(leasMileage && leasMonths);
940
945
  const leasMileageStr = leasMileage ? Number(leasMileage).toLocaleString('de-DE') : '';
946
+ const detailsLink = (mode: PaymentMode): React.ReactNode =>
947
+ onShowDetails ? (
948
+ <button
949
+ type="button"
950
+ className="pp-details-link"
951
+ onClick={(e) => {
952
+ e.stopPropagation();
953
+ onShowDetails(mode);
954
+ }}
955
+ >
956
+ {t('autobahn:card.price.details')}
957
+ </button>
958
+ ) : (
959
+ <span className="pp-details-link">{t('autobahn:card.price.details')}</span>
960
+ );
941
961
  return (
942
962
  <div className={'pricepaths' + (className ? ' ' + className : '')}>
943
963
  <div className="pp-eyebrow">{t('autobahn:card.price.financingFrom')}</div>
@@ -952,7 +972,7 @@ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t, c
952
972
  months: finMonths,
953
973
  count: finMonths,
954
974
  })}{' '}
955
- · <span className="pp-details-link">{t('autobahn:card.price.details')}</span>
975
+ · {detailsLink('financing')}
956
976
  </div>
957
977
  {hasLease && (
958
978
  <div className="pp-lease-row">
@@ -967,7 +987,7 @@ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t, c
967
987
  months: leasMonths,
968
988
  mileage: leasMileageStr,
969
989
  })}{' '}
970
- · <span className="pp-details-link">{t('autobahn:card.price.details')}</span>
990
+ · {detailsLink('leasing')}
971
991
  </div>
972
992
  )}
973
993
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.82",
3
+ "version": "1.0.84",
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
@@ -4447,7 +4447,18 @@ line-height: 31px;
4447
4447
  .md3 .listcard-price .pricepaths .pp-hero-terms { font-size: 10.5px; }
4448
4448
  /* "Details" link at the end of the financing/leasing terms lines (Figma "mcard") — quiet, same
4449
4449
  color as the rest of the terms line but underlined so it still reads as tappable. */
4450
- .md3 .pp-details-link { color: var(--md-primary); text-decoration: underline; font-weight: 600; white-space: nowrap; }
4450
+ .md3 .pp-details-link {
4451
+ border: 0;
4452
+ background: none;
4453
+ padding: 0;
4454
+ margin: 0;
4455
+ font: inherit;
4456
+ color: var(--md-primary);
4457
+ text-decoration: underline;
4458
+ font-weight: 600;
4459
+ white-space: nowrap;
4460
+ cursor: pointer;
4461
+ }
4451
4462
  /* z.B./old-rate/unit column beside the CarCard hero figure — mirrors .fin-rate-unit
4452
4463
  (FinanceCalculator.tsx/md3.css) exactly; only used inside .cc-price's PriceHero. */
4453
4464
  .md3 .pp-hero-unit-group {
@@ -1967,8 +1967,10 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1967
1967
  const [uvpTooltipOpen, setUvpTooltipOpen] = React.useState(false);
1968
1968
  // opened directly via the "Finanzierungsdetails" link (initialShowBreakdown) — on mobile the
1969
1969
  // breakdown is still gated behind its own toggle, so scroll the sheet down to reveal it instead
1970
- // of leaving the user at the top. Desktop always shows the breakdown inline now (no toggle), so
1971
- // it's excluded here — scrolling straight past the price/sliders/toggles on open was jarring.
1970
+ // of leaving the user at the top. Desktop always shows the breakdown inline (no toggle), but it
1971
+ // can still sit below the fold inside .fin-modal-body's own scroll area, so it gets the same
1972
+ // treatment — whichever of the two refs is actually visible (show-mobile/hide-mobile via CSS)
1973
+ // is the one scrolled.
1972
1974
  const desktopBreakdownRef = React.useRef<HTMLDivElement | null>(null);
1973
1975
  const mobileBreakdownRef = React.useRef<HTMLDivElement | null>(null);
1974
1976
  React.useEffect(() => {
@@ -1976,8 +1978,8 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1976
1978
  setShowBreakdown(!!initialShowBreakdown);
1977
1979
  if (!initialShowBreakdown) return;
1978
1980
  const timer = window.setTimeout(() => {
1979
- const target = mobileBreakdownRef.current;
1980
- if (target && target.offsetParent !== null) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
1981
+ const target = [mobileBreakdownRef.current, desktopBreakdownRef.current].find((el) => el && el.offsetParent !== null);
1982
+ if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
1981
1983
  }, 50);
1982
1984
  return () => window.clearTimeout(timer);
1983
1985
  // eslint-disable-next-line react-hooks/exhaustive-deps