@bytebrand/fe-ui-core-autobahn 1.0.20 → 1.0.22

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.
@@ -14,6 +14,7 @@
14
14
  "leaseTerms": "{{mileage}} km/Jahr · {{months}} Monate",
15
15
  "purchasePrice": "Kaufpreis",
16
16
  "priceDropAmount": "−{{amount}} €",
17
+ "priceDropSave": "{{amount}} € sparen",
17
18
  "savedAmount": "{{amount}} € gespart",
18
19
  "zeroDownCompact": "0 € Anz.",
19
20
  "zeroDown": "0 € Anzahlung",
@@ -414,7 +415,9 @@
414
415
  "back": "zurück",
415
416
  "share": "teilen",
416
417
  "photos": "Alle {{count}} Fotos",
417
- "photos_plural": "Alle {{count}} Fotos"
418
+ "photos_plural": "Alle {{count}} Fotos",
419
+ "photosCount": "{{count}} Fotos",
420
+ "photosCount_plural": "{{count}} Fotos"
418
421
  },
419
422
  "favorite": {
420
423
  "label": "merken"
@@ -14,6 +14,7 @@
14
14
  "leaseTerms": "{{mileage}} km/year · {{months}} months",
15
15
  "purchasePrice": "Purchase price",
16
16
  "priceDropAmount": "−{{amount}} €",
17
+ "priceDropSave": "Save {{amount}} €",
17
18
  "savedAmount": "€{{amount}} saved",
18
19
  "zeroDownCompact": "€0 down",
19
20
  "zeroDown": "€0 down payment",
@@ -414,7 +415,9 @@
414
415
  "back": "Back",
415
416
  "share": "Share",
416
417
  "photos": "All {{count}} photos",
417
- "photos_plural": "All {{count}} photos"
418
+ "photos_plural": "All {{count}} photos",
419
+ "photosCount": "{{count}} photos",
420
+ "photosCount_plural": "{{count}} photos"
418
421
  },
419
422
  "favorite": {
420
423
  "label": "Save"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
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
@@ -155,12 +155,12 @@
155
155
  NOT the 38px tappable filter-chip the base .chip is. */
156
156
  .md3 .listcard-chips .chip { height: 24px; padding: 0 10px; font-size: 11.5px; border-radius: 8px; cursor: default; }
157
157
  .md3 .listcard-chips .chip:hover { background: var(--md-surface); border-color: var(--md-outline-variant); }
158
- /* VDP trust row (Sofort verfügbar/views/favorites/Geprüft) — same compact non-interactive
159
- sizing as the listcard equipment tags, standalone (not nested under .listcard-chips). */
160
- .md3 .vdp-trust-chip { display: inline-flex; align-items: center; gap: 6px; height: 24px; padding: 0 10px;
161
- border-radius: 8px; font-size: 11.5px; font-weight: 500; line-height: 1; cursor: default; white-space: nowrap;
162
- background: var(--md-surface); color: var(--md-on-surface-variant); border: 1px solid var(--md-outline-variant); }
163
- .md3 .vdp-trust-chip .lucide { width: 14px; height: 14px; flex: none; }
158
+ /* VDP trust row (Sofort verfügbar/views/favorites/Geprüft) — mobile-only, so the figma
159
+ push-chip palette is used literally (light blue-gray fill, dark bold label). */
160
+ .md3 .vdp-trust-chip { display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 11px;
161
+ border-radius: 11px; font-size: 11px; font-weight: 700; line-height: 1; cursor: default; white-space: nowrap;
162
+ background: #F5F7FA; color: #1D2A38; border: 1px solid #E5EAF0; }
163
+ .md3 .vdp-trust-chip .lucide { width: 16px; height: 16px; flex: none; }
164
164
 
165
165
  /* status / rating chip (no CTA) */
166
166
  .md3 .statchip { display: inline-flex; align-items: center; gap: 5px; height: 24px; padding: 0 10px;
@@ -174,6 +174,18 @@
174
174
  border-radius: var(--md-r-full); background: rgba(0,0,0,.55); color: #fff; font-size: 12px; font-weight: 500; backdrop-filter: blur(2px); }
175
175
  .md3 .imglabel .lucide { width: 14px; height: 14px; }
176
176
 
177
+ /* mobile-only row under the gallery image (mockup): bordered "X Fotos" pill · slider dots ·
178
+ share/favorite — all in ONE row in normal flow, nothing overlaid on the image. */
179
+ .md3 .gallery-controls-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 2px 0; }
180
+ .md3 .gallery-photos-btn { display: inline-flex; align-items: center; gap: 7px; cursor: pointer; font: inherit;
181
+ font-size: 12px; font-weight: 700; color: var(--md-on-surface); height: 34px; padding: 0 13px; flex: none;
182
+ background: #fff; border: 1px solid #D4DDE7; border-radius: 16px; }
183
+ .md3 .gallery-photos-btn .lucide { width: 17px; height: 17px; }
184
+ /* page-driven slider dots (the carousel's own overlay dots are hidden on the phone VDP) */
185
+ .md3 .gallery-dots { display: flex; align-items: center; gap: 6px; min-width: 0; overflow: hidden; }
186
+ .md3 .gallery-dots .gdot { width: 6px; height: 6px; border-radius: 3px; background: #D4DDE7; flex: none; }
187
+ .md3 .gallery-dots .gdot--on { width: 30px; height: 5px; background: var(--md-on-surface); }
188
+
177
189
  /* ---------- cards ---------- */
178
190
  .md3 .mcard { background: var(--md-sc-lowest); border-radius: var(--md-r-md); border: 1px solid var(--md-outline-variant);
179
191
  overflow: hidden; transition: box-shadow var(--dur-2) var(--ease-standard), border-color var(--dur-2) var(--ease-standard), transform var(--dur-2) var(--ease-standard); }
@@ -1150,7 +1162,7 @@
1150
1162
  .md3 .fin-tabs { display: flex; border-bottom: 1px solid var(--md-outline-variant); margin-bottom: 18px; }
1151
1163
  .md3 .fin-tab { flex: 1; background: none; border: 0; padding: 11px 4px; cursor: pointer; font: inherit;
1152
1164
  font-size: 13.5px; font-weight: 600; color: var(--md-on-surface-variant); border-bottom: 2px solid transparent;
1153
- margin-bottom: -1px; transition: color .15s, border-color .15s; }
1165
+ margin-bottom: -1px; transition: color .15s, border-color .15s; text-align: center; }
1154
1166
  .md3 .fin-tab.on { color: var(--md-primary); border-bottom-color: var(--md-primary); }
1155
1167
  .md3 .fin-tab:hover:not(.on) { color: var(--md-on-surface); }
1156
1168
  /* 2-col grid: left grows, right fixed 148px — pill (row 1) and bar (row 2) share a column edge, never wrap */
@@ -1208,10 +1220,13 @@
1208
1220
  .md3 .enquiry-phone { margin-top: 16px; }
1209
1221
  }
1210
1222
 
1211
- /* sticky mobile CTA bar (VDP) */
1223
+ /* sticky mobile CTA bar (VDP) — hidden (slid off-screen) until the page scrolls past the
1224
+ main offer card, matching the desktop VdpStickyBar's show/scroll-gated behavior. */
1212
1225
  .md3 .vdp-bottom-bar { position: fixed; left: 0; right: 0; bottom: 0; z-index: 60; background: var(--md-sc-lowest);
1213
1226
  border-top: 1px solid var(--md-outline-variant); box-shadow: 0 -2px 14px rgba(0,0,0,.10);
1214
- padding: 10px 16px; align-items: center; gap: 14px; }
1227
+ padding: 10px 16px; align-items: center; gap: 14px;
1228
+ transform: translateY(100%); transition: transform var(--dur-2, .25s); }
1229
+ .md3 .vdp-bottom-bar--on { transform: translateY(0); }
1215
1230
 
1216
1231
  /* bottom-sheet (SRP filters) — sticky header + scrollable body + sticky action bar, slide-up entrance */
1217
1232
  .md3 .sheet-scrim { position: fixed; inset: 0; background: rgba(16,22,42,.5); z-index: 70;
@@ -1453,13 +1468,29 @@
1453
1468
  display: flex; align-items: center; justify-content: center; rotate: 45deg; box-shadow: var(--md-e2); }
1454
1469
  .md3 .dealer-pin .lucide { rotate: -45deg; }
1455
1470
 
1456
- /* VDP: floating back button over the gallery — phone only (breadcrumb is hidden there) */
1471
+ /* VDP: floating back button over the gallery — hidden everywhere now (the phone mockup
1472
+ shows a clean image with only the nav arrows; desktop has the breadcrumb). */
1457
1473
  .md3 .vdp-back { display: none; position: absolute; top: 12px; left: 14px; z-index: 3; }
1458
1474
 
1475
+ /* mobile push-badges row ABOVE the gallery (mockup): flat tinted pills in normal flow —
1476
+ replaces the white overlay chips (DropChip & co) that sat on the image. Figma palette
1477
+ used literally; one non-wrapping swipeable line. */
1478
+ .md3 .vdp-push-badges { display: flex; align-items: center; gap: 8px; flex-wrap: nowrap; overflow-x: auto;
1479
+ scrollbar-width: none; margin-bottom: 10px; }
1480
+ .md3 .vdp-push-badges::-webkit-scrollbar { display: none; }
1481
+ .md3 .push-badge { display: inline-flex; align-items: center; justify-content: center; height: 28px; padding: 0 13px;
1482
+ border-radius: 11px; font-size: 13.5px; font-weight: 700; line-height: 1; white-space: nowrap; flex: none; }
1483
+ .md3 .push-badge--save { background: #E6F7EE; color: #168A45; border: 1px solid rgba(22,138,69,.07); }
1484
+ .md3 .push-badge--discount { background: #E60000; color: #fff; }
1485
+ .md3 .push-badge--down { background: #EAF2FF; color: #005CFF; border: 1px solid rgba(0,92,255,.05); }
1486
+
1459
1487
  @media (max-width: 600px) {
1460
1488
  /* full-bleed gallery: edge-to-edge, app-like (production pattern) */
1461
- .md3 .vdp-back { display: flex; }
1462
- .md3 .vdp-gallerybox { margin: -14px -24px 0; border-radius: 0; border: 0; box-shadow: none; }
1489
+ .md3 .vdp-gallerybox { margin: 0 -24px; border-radius: 0; border: 0; box-shadow: none; }
1490
+ /* the carousel's own overlay dots move into the below-image controls row (page-rendered) */
1491
+ .md3 .vdp-gallerybox .cardcar-dots { display: none; }
1492
+ /* mockup hero rate is a notch bigger on phone (inline style says 48 — hence !important) */
1493
+ .md3 .fin-rate-main .num { font-size: 52px !important; letter-spacing: -1.5px !important; }
1463
1494
  }
1464
1495
 
1465
1496
  /* ---------- save-search bar ---------- */
@@ -99,6 +99,7 @@ const CardCarousel: React.FunctionComponent<CardCarouselProps> = ({
99
99
  <Icon name="chevron-right" size={20} />
100
100
  </button>
101
101
  <div
102
+ className="cardcar-dots"
102
103
  style={{
103
104
  position: 'absolute',
104
105
  bottom: 9,
@@ -312,7 +312,17 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
312
312
  className={'fin-tab' + (mode === k ? ' on' : '')}
313
313
  onClick={() => onModeChange(k)}
314
314
  >
315
- {t(labelKey)}
315
+ {k === 'cash' ? (
316
+ <>
317
+ {/* mockup names the cash tab "Kaufen" on phone; desktop keeps "Barzahlung" */}
318
+ <span className="hide-mobile">{t(labelKey)}</span>
319
+ <span className="show-mobile" style={{ justifyContent: 'center' }}>
320
+ {t('autobahn:card.price.buy')}
321
+ </span>
322
+ </>
323
+ ) : (
324
+ t(labelKey)
325
+ )}
316
326
  </button>
317
327
  ))}
318
328
  </div>
@@ -469,34 +479,30 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
469
479
 
470
480
  {/* ============================== MOBILE (≤600px) — new layout ============================== */}
471
481
  <div className="show-mobile" style={{ flexDirection: 'column' }}>
472
- {/* side-by-side CTAs */}
473
- <div style={{ display: 'flex', gap: 10, marginTop: 14 }}>
474
- <Button variant="filled" size="lg" icon="shield-check" style={{ flex: 1 }} onClick={onSecure}>
482
+ {/* side-by-side CTAs — mockup ratio ~2:1, with the tiny auto.de-ID sitting
483
+ directly under the Anrufen button (not in its own full-width row). */}
484
+ <div style={{ display: 'flex', gap: 10, marginTop: 14, alignItems: 'flex-start' }}>
485
+ <Button variant="filled" size="lg" icon="shield-check" style={{ flex: 2 }} onClick={onSecure}>
475
486
  {t('autobahn:finance.secureOffer')}
476
487
  </Button>
477
- <Button variant="outlined" size="lg" icon="phone" style={{ flex: 1 }} onClick={onCall}>
478
- {t('autobahn:finance.callCta')}
479
- </Button>
480
- </div>
481
-
482
- {/* auto.de-ID (left) · Finanzierungs-/Leasingdetails (right) — same row desktop shows,
483
- restored here per the mockup (round 1 had dropped it in favor of Persönliche Beratung
484
- alone; the mockup wants both rows, not one replacing the other). */}
485
- {(autoDeId || (!isCash && onShowDetails)) && (
486
- <div className="fin-info">
487
- {autoDeId && <span className="fin-id">auto.de-ID {autoDeId}</span>}
488
- {!isCash && onShowDetails && (
489
- <DetailsLink
490
- label={mode === 'leasing' ? t('autobahn:finance.leasingDetails') : t('autobahn:finance.financingDetails')}
491
- onClick={onShowDetails}
492
- />
488
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', gap: 4, minWidth: 0 }}>
489
+ <Button variant="outlined" size="lg" icon="phone" block onClick={onCall}>
490
+ {t('autobahn:finance.callCta')}
491
+ </Button>
492
+ {autoDeId && (
493
+ <span
494
+ className="num"
495
+ style={{ fontSize: 10, fontWeight: 700, color: 'var(--md-primary)', textAlign: 'center', whiteSpace: 'nowrap' }}
496
+ >
497
+ auto.de ID: {autoDeId}
498
+ </span>
493
499
  )}
494
500
  </div>
495
- )}
501
+ </div>
496
502
 
497
- {/* Persönliche Beratung (left) · Finanzierungs-/Leasingdetails or hint (right) */}
503
+ {/* Persönliche Beratung (left) · divider · Finanzierungs-/Leasingdetails or hint (right) */}
498
504
  <div className="fin-info">
499
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0 }}>
505
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, minWidth: 0, flex: 1 }}>
500
506
  <Icon name="user" size={18} color="var(--md-on-surface-variant)" style={{ flex: 'none' }} />
501
507
  <div style={{ minWidth: 0 }}>
502
508
  <div className="md-label-sm" style={{ fontWeight: 700, color: 'var(--md-on-surface)' }}>
@@ -505,18 +511,34 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
505
511
  <div className="md-label-sm">{t('autobahn:finance.personalConsultationSub')}</div>
506
512
  </div>
507
513
  </div>
508
- {isCash ? (
514
+ <span style={{ width: 1, alignSelf: 'stretch', background: 'var(--md-outline-variant)', flex: 'none' }} />
515
+ {!isCash && onShowDetails ? (
516
+ <button
517
+ type="button"
518
+ onClick={onShowDetails}
519
+ style={{
520
+ display: 'flex',
521
+ alignItems: 'center',
522
+ gap: 8,
523
+ background: 'none',
524
+ border: 0,
525
+ padding: 0,
526
+ cursor: 'pointer',
527
+ font: 'inherit',
528
+ color: 'var(--md-on-surface)',
529
+ textAlign: 'left',
530
+ }}
531
+ >
532
+ <Icon name="info" size={18} color="var(--md-on-surface-variant)" style={{ flex: 'none' }} />
533
+ <span className="md-label-sm" style={{ maxWidth: 82, lineHeight: 1.5 }}>
534
+ {mode === 'leasing' ? t('autobahn:finance.leasingDetails') : t('autobahn:finance.financingDetails')}
535
+ </span>
536
+ </button>
537
+ ) : (
509
538
  <div style={{ display: 'flex', alignItems: 'center', gap: 6, color: 'var(--md-on-surface-variant)' }}>
510
539
  <Icon name="info" size={15} />
511
540
  <span className="md-label-sm">{t('autobahn:finance.financingLeasingPossible')}</span>
512
541
  </div>
513
- ) : (
514
- onShowDetails && (
515
- <DetailsLink
516
- label={mode === 'leasing' ? t('autobahn:finance.leasingDetails') : t('autobahn:finance.financingDetails')}
517
- onClick={onShowDetails}
518
- />
519
- )
520
542
  )}
521
543
  </div>
522
544
 
@@ -922,6 +922,13 @@ export const VdpStickyBar: React.FunctionComponent<VdpStickyBarProps> = ({ vehic
922
922
 
923
923
  export interface VdpBottomBarProps {
924
924
  vehicle: VehicleDetail;
925
+ /**
926
+ * Whether the bar is docked/visible — mirrors VdpStickyBar's `show` (same
927
+ * page-owned scroll-position toggle, STICKY_SHOW_AT). Was unconditionally
928
+ * visible before, unlike the desktop VdpStickyBar; the mockup only shows it
929
+ * after scrolling past the main offer card.
930
+ */
931
+ show: boolean;
925
932
  /** primary CTA click. */
926
933
  onSecure?: () => void;
927
934
  /** i18n function (drilled from the page). */
@@ -929,8 +936,8 @@ export interface VdpBottomBarProps {
929
936
  }
930
937
 
931
938
  /** Mobile sticky bottom CTA bar — rate + Kaufpreis + "Angebot sichern". */
932
- export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({ vehicle, onSecure, t }) => (
933
- <div className="vdp-bottom-bar show-mobile">
939
+ export const VdpBottomBar: React.FunctionComponent<VdpBottomBarProps> = ({ vehicle, show, onSecure, t }) => (
940
+ <div className={'vdp-bottom-bar show-mobile' + (show ? ' vdp-bottom-bar--on' : '')}>
934
941
  <div style={{ flex: 1, minWidth: 0 }}>
935
942
  <div style={{ display: 'flex', alignItems: 'baseline', gap: 5, whiteSpace: 'nowrap' }}>
936
943
  <span className="md-label-sm">{t('autobahn:bottom.from')}</span>
@@ -1271,7 +1278,8 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
1271
1278
  [
1272
1279
  ['fin', 'autobahn:finance.mode.financing'],
1273
1280
  ['leasing', 'autobahn:finance.mode.leasing'],
1274
- ['cash', 'autobahn:finance.mode.cash'],
1281
+ // this sheet is mobile-only, so the cash tab is "Kaufen" here (desktop card says "Barzahlung")
1282
+ ['cash', 'autobahn:card.price.buy'],
1275
1283
  ] as Array<[FinanceMode, string]>
1276
1284
  ).map(([k, labelKey]) => (
1277
1285
  <button