@bytebrand/fe-ui-core-autobahn 1.0.163 → 1.0.165

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.
@@ -797,7 +797,8 @@
797
797
  "serviceHistory": "Scheckheft",
798
798
  "warranty": "Garantie",
799
799
  "yes": "Ja",
800
- "no": "Nein"
800
+ "no": "Nein",
801
+ "notSpecified": "keine Angabe"
801
802
  },
802
803
  "common": {
803
804
  "needsData": "Daten folgt"
@@ -797,7 +797,8 @@
797
797
  "serviceHistory": "Full service history",
798
798
  "warranty": "Warranty",
799
799
  "yes": "Yes",
800
- "no": "No"
800
+ "no": "No",
801
+ "notSpecified": "not specified"
801
802
  },
802
803
  "common": {
803
804
  "needsData": "Data coming soon"
package/package.json CHANGED
@@ -1,30 +1,30 @@
1
- {
2
- "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.163",
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.165",
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/Dropdown.tsx CHANGED
@@ -22,6 +22,7 @@ import * as ReactDOM from 'react-dom';
22
22
 
23
23
  import Icon from './Icon';
24
24
  import { usePortalMenuPosition } from './usePortalMenuPosition';
25
+ import { scrollFieldIntoView } from './scrollFieldIntoView';
25
26
 
26
27
  export interface DropdownOption {
27
28
  value: string;
@@ -189,15 +190,22 @@ const Dropdown: React.FunctionComponent<DropdownProps> = ({
189
190
  if (allowCustomValue) setQuery(value || '');
190
191
  }, [disabled, selectedIndex, allowCustomValue, value]);
191
192
 
192
- // when opened searchable, focus the search input instead of leaving focus on the trigger.
193
- // scrollIntoView first: on mobile the keyboard is about to eat ~40% of the viewport, so the
194
- // trigger (and the popover below it) needs to already be scrolled above the fold before focus
195
- // fires, or the popover can end up hidden behind the keyboard with no way to see it open.
193
+ // Scroll the trigger (searchable or not) to the top of the page on open, THEN — searchable
194
+ // only — focus the search input instead of leaving focus on the trigger. Every Dropdown shares
195
+ // this, not just searchable ones: on mobile a trigger sitting mid-page can open a menu that
196
+ // runs off the bottom of the viewport with no way to see the rest of it, same underlying
197
+ // problem a searchable trigger has with the keyboard eating the space below it. A plain
198
+ // `scrollIntoView({block:'center'})` used to run here (searchable-only) — "center" only moves
199
+ // the trigger when it's already near a viewport edge, so on a trigger that's roughly mid-page
200
+ // already (the common case) it was a near no-op, leaving a few hundred px of dead space above
201
+ // the trigger that the keyboard (searchable) or the menu's own bottom edge (any dropdown) then
202
+ // ran into.
196
203
  React.useEffect(() => {
197
- if (!open || !searchable) return;
198
- if (rootRef.current && rootRef.current.scrollIntoView) {
199
- rootRef.current.scrollIntoView({ block: 'center', behavior: 'smooth' });
200
- }
204
+ if (!open) return;
205
+ if (rootRef.current) scrollFieldIntoView(rootRef.current);
206
+ // searchRef is only ever attached in the searchable+open (editing) render branch, so this
207
+ // naturally no-ops for a non-searchable dropdown — nothing else to focus there, the trigger
208
+ // <button> already has focus from the click/keypress that opened it.
201
209
  if (searchRef.current) {
202
210
  searchRef.current.focus();
203
211
  // pre-filled custom value (see openMenu) starts selected, so typing immediately
package/ui/index.ts CHANGED
@@ -39,6 +39,8 @@ export type { FormFieldProps } from './FormField';
39
39
  export { default as Dropdown } from './Dropdown';
40
40
  export type { DropdownProps, DropdownOption } from './Dropdown';
41
41
 
42
+ export { scrollFieldIntoView } from './scrollFieldIntoView';
43
+
42
44
  export { default as Spec } from './Spec';
43
45
  export type { SpecProps } from './Spec';
44
46
 
@@ -0,0 +1,41 @@
1
+ // Autobahn (auto.de redesign) — shared "scroll a just-opened/just-focused field to the top of the
2
+ // page" behaviour, used by Dropdown.tsx and by fe-main's DualRange/RateSlider (FilterContent.tsx)
3
+ // text fields on /search/detailed. On mobile, opening a dropdown's menu or focusing a text field
4
+ // brings the on-screen keyboard up and/or needs room for a popover below the field — if the field
5
+ // is sitting mid-page, there may not be enough space left below it once the keyboard/menu takes
6
+ // its share. Scrolling the field to the very top of the page first maximizes what's available.
7
+ //
8
+ // Extracted out of Dropdown.tsx so it can be shared verbatim with fe-main's non-Dropdown form
9
+ // fields (DualRange/RateSlider's text inputs) — same problem, same fix, one place to keep them
10
+ // from drifting apart. See Dropdown.tsx's own call site for the original reasoning this was
11
+ // pulled from.
12
+ export function scrollFieldIntoView(field: HTMLElement): void {
13
+ if (typeof window === 'undefined') return;
14
+ // Mobile-only: the on-screen-keyboard/popover space problem this fixes doesn't exist on
15
+ // desktop, where there's no keyboard pushing the viewport around. Same breakpoint + matchMedia
16
+ // pattern as Modal.tsx/CardCarousel.tsx elsewhere in this lib.
17
+ if (window.matchMedia('(min-width: 601px)').matches) return;
18
+ // Anchor on whichever is higher up: the field itself, or a short field label/caption sitting
19
+ // directly above it (own previous sibling, or — the common "label above a row of controls"
20
+ // shape, e.g. "Marke & Modell" above the Make/Model row on /search/detailed — the wrapping
21
+ // row's previous sibling). Checked by height, not by class name, so this works for ANY
22
+ // caller's label without this needing to know that call site's specific markup.
23
+ const MAX_LABEL_HEIGHT = 30;
24
+ const asLabel = (el: Element | null): HTMLElement | null => {
25
+ if (!(el instanceof HTMLElement)) return null;
26
+ const h = el.getBoundingClientRect().height;
27
+ return h > 0 && h <= MAX_LABEL_HEIGHT ? el : null;
28
+ };
29
+ const anchor =
30
+ asLabel(field.previousElementSibling) ||
31
+ asLabel(field.parentElement && field.parentElement.previousElementSibling) ||
32
+ field;
33
+ // No sticky-header reservation here: every current caller (NewMainPage '/', FilterContent on
34
+ // '/search*') sits on a page where TopBar's own autoHideOnScroll is on (App.tsx: isHomePage ||
35
+ // isSearchPage) — the header slides itself out of the way the instant this scroll starts,
36
+ // independently of this call, so reserving space for it here would double-count.
37
+ const GAP = 8;
38
+ const rect = anchor.getBoundingClientRect();
39
+ const targetY = window.scrollY + rect.top - GAP;
40
+ window.scrollTo({ top: Math.max(0, targetY), behavior: 'smooth' });
41
+ }
@@ -79,6 +79,20 @@ const ESTIMATED_MENU_HEIGHT = 300;
79
79
  // a scroll within this window of opening REPOSITIONS instead of closing — see the effect below.
80
80
  const SELF_SCROLL_GRACE_MS = 500;
81
81
 
82
+ // Module-level (not hook-local) so Dropdown.tsx's own searchable-open scroll effect can reuse the
83
+ // exact same measurement — the sticky app header (.topbar, position:sticky/top:0) sits over the
84
+ // top of the viewport on every page that uses it (TopBar's autoHideOnScroll is opt-in, off by
85
+ // default — most pages keep it permanently docked), so anything that scrolls/positions content
86
+ // near the top of the viewport needs to stop at its live bottom edge, not y=0. Reading the
87
+ // header's live bottom edge (0 when absent, e.g. inside a modal, or when autoHideOnScroll has slid
88
+ // it away) gives a correct boundary without hardcoding a pixel height that could drift from the
89
+ // header's own CSS.
90
+ export const getSafeTop = (): number => {
91
+ if (typeof document === 'undefined') return 0;
92
+ const header = document.querySelector('.topbar');
93
+ return header ? Math.max(0, header.getBoundingClientRect().bottom) : 0;
94
+ };
95
+
82
96
  export function usePortalMenuPosition<TMenuEl extends HTMLElement = HTMLDivElement>({
83
97
  enabled,
84
98
  open,
@@ -121,18 +135,6 @@ export function usePortalMenuPosition<TMenuEl extends HTMLElement = HTMLDivEleme
121
135
  };
122
136
  }, [enabled, open, portalRoot]);
123
137
 
124
- // the portal menu's z-index (1400, .mselect-menu--portal) is intentionally higher than every
125
- // other fixed overlay it must paint over (checkout sheets/modals) — which also means it would
126
- // paint over the sticky app header (.topbar, position:sticky/top:0/z-index:50) if left
127
- // unconstrained. Reading the header's live bottom edge (0 when absent, e.g. inside a modal, or
128
- // when TopBar's autoHideOnScroll has slid it away) gives a correct top boundary without
129
- // hardcoding a pixel height that could drift from the header's own CSS.
130
- const getSafeTop = (): number => {
131
- if (typeof document === 'undefined') return 0;
132
- const header = document.querySelector('.topbar');
133
- return header ? Math.max(0, header.getBoundingClientRect().bottom) : 0;
134
- };
135
-
136
138
  const [menuPos, setMenuPos] = React.useState<PortalMenuPos | null>(null);
137
139
 
138
140
  const positionMenu = React.useCallback(() => {
package/utils/index.ts CHANGED
@@ -83,3 +83,5 @@ export type { IValues, IFilters, IQuery } from './query';
83
83
  // Decorated car props (renders React UI; lives in vehiclesProps/ with its
84
84
  // IconContainer + Co2Widget dependencies under compat/).
85
85
  export { default as getDecoratedProps } from '../vehiclesProps/decoratedProps';
86
+ export { setStrictConditionOwners, isStrictConditionOwner, strictAccidentStatus } from '../vehiclesProps/accidentDisclosure';
87
+ export type { AccidentStatus } from '../vehiclesProps/accidentDisclosure';
@@ -92,7 +92,7 @@ export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({
92
92
  ['zap', extractPS(vehicle.power)],
93
93
  ['fuel', stripParen(vehicle.fuel)],
94
94
  ['cog', vehicle.gear],
95
- ['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
95
+ ...(vehicle.owners == null ? [] : ([['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })]] as const)),
96
96
  ];
97
97
  return (
98
98
  <>
@@ -142,7 +142,7 @@ export const VdpFactsGrid: React.FunctionComponent<VdpHeaderCardProps> = ({ vehi
142
142
  ['calendar', vehicle.reg],
143
143
  ['cog', vehicle.gear],
144
144
  ['fuel', stripParen(vehicle.fuel)],
145
- ['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
145
+ ...(vehicle.owners == null ? [] : ([['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })]] as const)),
146
146
  ...(vehicle.body ? ([['car-front', vehicle.body]] as const) : []),
147
147
  ];
148
148
  return (
@@ -216,7 +216,7 @@ export const VdpHeaderCondition: React.FunctionComponent<VdpHeaderCardProps> = (
216
216
  // only the Garantie tile has one, linking out to auto.de's warranty policy page.
217
217
  const tiles: ReadonlyArray<readonly [string, string, string, string?]> = [
218
218
  ['wrench', t('autobahn:header.huLabel'), condition.hu === 'Neu' ? t('autobahn:header.huNew') : t('autobahn:header.huUntil', { date: condition.hu })],
219
- ['shield-check', t('autobahn:header.accidentFree'), condition.unfallfrei ? t('autobahn:header.yes') : t('autobahn:header.no')],
219
+ ['shield-check', t('autobahn:header.accidentFree'), condition.unfallfrei == null ? t('autobahn:header.notSpecified') : condition.unfallfrei ? t('autobahn:header.yes') : t('autobahn:header.no')],
220
220
  ['clipboard-check', t('autobahn:header.serviceHistory'), condition.scheckheft ? t('autobahn:header.yes') : t('autobahn:header.no')],
221
221
  ['award', t('autobahn:header.warranty'), condition.garantie, 'https://www.auto.de/garantie/'],
222
222
  ];
package/vdp/types.ts CHANGED
@@ -111,8 +111,8 @@ export interface Co2CostsSummary {
111
111
  export interface VehicleCondition {
112
112
  /** TÜV/HU validity, e.g. '03/2027' — or 'Neu' for a new HU. */
113
113
  hu: string;
114
- /** accident-free flag (Unfallfrei). */
115
- unfallfrei: boolean;
114
+ /** accident-free flag (Unfallfrei); null = not specified by the source ("keine Angabe"). */
115
+ unfallfrei: boolean | null;
116
116
  /** full service history (Scheckheftgepflegt). */
117
117
  scheckheft: boolean;
118
118
  /** warranty label, e.g. '12 Monate inkl.'. */
@@ -191,8 +191,8 @@ export interface VehicleDetail {
191
191
  fuel: string;
192
192
  /** gearbox label, e.g. 'Automatik' / 'Schaltgetriebe'. */
193
193
  gear: string;
194
- /** number of previous owners (rendered as 'N. Hand'). */
195
- owners: number;
194
+ /** number of previous owners (rendered as 'N. Hand'); null = not known, the fact is not shown. */
195
+ owners: number | null;
196
196
  /** number of seats. */
197
197
  seats: number;
198
198
  /** number of doors. */
@@ -302,7 +302,7 @@ export function defaultVdpSpecs(v: VehicleDetail): VdpSpec[] {
302
302
  [['cog', 'Getriebe', v.gear], true],
303
303
  [['users', 'Sitze', `${v.seats} Sitze`], false],
304
304
  [['door-open', 'Türen', `${v.doors} Türen`], false],
305
- [['user', 'Halter', `${v.owners}. Hand`], true], // 0 Vorbesitzer is valid
305
+ ...(v.owners == null ? [] : [[['user', 'Halter', `${v.owners}. Hand`], true] as const]), // 0 Vorbesitzer is valid
306
306
  [['palette', 'Außenfarbe', v.color], true],
307
307
  [['car-front', 'Karosserie', v.body], true],
308
308
  [['droplet', 'Verbrauch komb.', v.cons], true], // Pkw-EnVKV — exempt from this rule
@@ -0,0 +1,25 @@
1
+ // FINN handover v0.6 (Daniel, 15.10.2026): for the owners set here, accident and owner facts follow
2
+ // only what the source states — mainData.accidentDamaged false → accident-free, true → accident,
3
+ // empty → not specified; an empty owner count is not shown as "0. Hand". Every other dealer keeps
4
+ // the old display (accident-free unless `damaged`). The app sets the owners once at start.
5
+ let strictOwners: ReadonlyArray<string> = [];
6
+
7
+ export function setStrictConditionOwners(ownerIds: ReadonlyArray<string>): void {
8
+ strictOwners = (ownerIds || []).filter(Boolean);
9
+ }
10
+
11
+ export function isStrictConditionOwner(car: any): boolean {
12
+ const ownerId = car?.metaData?.ownerId;
13
+ return !!ownerId && strictOwners.includes(ownerId);
14
+ }
15
+
16
+ export type AccidentStatus = 'accidentFree' | 'accident' | 'notSpecified';
17
+
18
+ // The strict status, or undefined when the car's owner keeps the old display.
19
+ export function strictAccidentStatus(car: any): AccidentStatus | undefined {
20
+ if (!isStrictConditionOwner(car)) return undefined;
21
+ const value = car?.mainData?.accidentDamaged;
22
+ if (value === true) return 'accident';
23
+ if (value === false) return 'accidentFree';
24
+ return 'notSpecified';
25
+ }
@@ -6,6 +6,7 @@ import { formatTimestamp } from '../utils/DateUtils';
6
6
  import { DELIVERY_PERIODS_EXTRA } from '../constants/core';
7
7
  import moment from 'moment';
8
8
  import { IDecoratedProp } from '../types/types';
9
+ import { strictAccidentStatus } from './accidentDisclosure';
9
10
 
10
11
  interface IDecoratedProps {
11
12
  [key: string]: IDecoratedProp;
@@ -519,6 +520,9 @@ const getDecoratedLightProps = (
519
520
  title: t('cbd:mainData_accidentDamaged'),
520
521
  icon: 'wrench',
521
522
  get value() {
523
+ const strict = strictAccidentStatus(car);
524
+ if (strict === 'notSpecified') return t('vehicleProps:value.na');
525
+ if (strict) return strict === 'accident' ? t('vehicleProps:value.damaged') : t('vehicleProps:value.accidentFree');
522
526
  return damaged ? t('vehicleProps:value.damaged') : t('vehicleProps:value.accidentFree');
523
527
  }
524
528
  },
@@ -9,6 +9,7 @@ import { EMISSION_STICKERS_ICONS, WP_EMISSION_STICKERS_ICONS } from '../constant
9
9
  import { DELIVERY_PERIODS_EXTRA } from '../constants';
10
10
  import { IDecoratedProp, ICar, IFinancingUtils } from '../types/types';
11
11
  import { getFormattedPrice } from '../utils/CommonUtils';
12
+ import { strictAccidentStatus } from './accidentDisclosure';
12
13
 
13
14
  const formatDeliveryPeriod = (t: (key: string, options?: object) => string, deliveryPeriod: string) => {
14
15
  const count: number = +DELIVERY_PERIODS_EXTRA.find((period: any) => period.value === deliveryPeriod).label;
@@ -195,6 +196,9 @@ const getDecoratedProps = (
195
196
  title: t('cbd:mainData_accidentDamaged'),
196
197
  icon: 'wrench',
197
198
  get value() {
199
+ const strict = strictAccidentStatus(car);
200
+ if (strict === 'notSpecified') return t('vehicleProps:value.na');
201
+ if (strict) return strict === 'accident' ? t('vehicleProps:value.damaged') : t('vehicleProps:value.accidentFree');
198
202
  return damaged ? t('vehicleProps:value.damaged') : t('vehicleProps:value.accidentFree');
199
203
  }
200
204
  },