@bytebrand/fe-ui-core-autobahn 1.0.164 → 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.
- package/locales/de/autobahn.json +2 -1
- package/locales/en/autobahn.json +2 -1
- package/package.json +30 -30
- package/ui/scrollFieldIntoView.ts +41 -41
- package/utils/index.ts +2 -0
- package/vdp/VdpModules.tsx +3 -3
- package/vdp/types.ts +5 -5
- package/vehiclesProps/accidentDisclosure.ts +25 -0
- package/vehiclesProps/decoratedLightProps.tsx +4 -0
- package/vehiclesProps/decoratedProps.tsx +4 -0
package/locales/de/autobahn.json
CHANGED
package/locales/en/autobahn.json
CHANGED
package/package.json
CHANGED
|
@@ -1,30 +1,30 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@bytebrand/fe-ui-core-autobahn",
|
|
3
|
-
"version": "1.0.
|
|
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
|
+
}
|
|
@@ -1,41 +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
|
-
}
|
|
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
|
+
}
|
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';
|
package/vdp/VdpModules.tsx
CHANGED
|
@@ -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
|
},
|