@bytebrand/fe-ui-core-autobahn 1.0.145 → 1.0.147
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 +370 -354
- package/package.json +1 -1
- package/theme/md3.css +11359 -11331
- package/vdp/VdpModules.tsx +74 -27
package/vdp/VdpModules.tsx
CHANGED
|
@@ -2225,37 +2225,81 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
2225
2225
|
// via CSS) is the one scrolled to.
|
|
2226
2226
|
const desktopBreakdownRef = React.useRef<HTMLDivElement | null>(null);
|
|
2227
2227
|
const mobileBreakdownRef = React.useRef<HTMLDivElement | null>(null);
|
|
2228
|
-
// mobile (ADV-276):
|
|
2229
|
-
//
|
|
2230
|
-
//
|
|
2231
|
-
//
|
|
2232
|
-
//
|
|
2233
|
-
//
|
|
2228
|
+
// mobile (ADV-276): the sheet opens sized to its rate controls — its bottom edge sits right
|
|
2229
|
+
// under the "Finanzierungs-/Leasing-Details" link (the [data-sheet-fold] element; on Kaufen its
|
|
2230
|
+
// CTA row), with the always-mounted breakdown below the fold, reachable by scrolling by hand.
|
|
2231
|
+
// Tapping the link expands the sheet to (nearly) the full screen (.fin-modal--expanded, 0.35s
|
|
2232
|
+
// height transition in md3.css) and scrolls the body until the "Repräsentatives Beispiel"
|
|
2233
|
+
// headline sits at its top — never so far that the CTA row under the breakdown comes into
|
|
2234
|
+
// view (on tall screens the breakdown is shorter than the sheet; the headline stops lower).
|
|
2235
|
+
// Targets are re-read every frame so the scroll and the growing sheet finish together.
|
|
2234
2236
|
const mobileBottomCtaRef = React.useRef<HTMLDivElement | null>(null);
|
|
2235
2237
|
const breakdownScrollRaf = React.useRef(0);
|
|
2236
|
-
const
|
|
2237
|
-
const
|
|
2238
|
-
|
|
2239
|
-
if (!el || !body) return;
|
|
2240
|
-
const from = body.scrollTop;
|
|
2241
|
-
const bodyTop = body.getBoundingClientRect().top;
|
|
2242
|
-
const headlineTop = el.getBoundingClientRect().top - bodyTop + from;
|
|
2243
|
-
const cta = mobileBottomCtaRef.current;
|
|
2244
|
-
const ctaLimit = cta ? cta.getBoundingClientRect().top - bodyTop + from - body.clientHeight : Infinity;
|
|
2245
|
-
const to = Math.max(0, Math.min(headlineTop - 8, ctaLimit, body.scrollHeight - body.clientHeight));
|
|
2246
|
-
if (to === from) return;
|
|
2238
|
+
const animateSheetScroll = React.useCallback((target: (body: HTMLElement) => number): void => {
|
|
2239
|
+
const body = mobileBreakdownRef.current?.closest('.fin-modal-body') as HTMLElement | null | undefined;
|
|
2240
|
+
if (!body) return;
|
|
2247
2241
|
window.cancelAnimationFrame(breakdownScrollRaf.current);
|
|
2248
|
-
const
|
|
2242
|
+
const from = body.scrollTop;
|
|
2243
|
+
const duration = 450;
|
|
2249
2244
|
let start: number | null = null;
|
|
2250
2245
|
const step = (now: number): void => {
|
|
2251
2246
|
if (start === null) start = now;
|
|
2252
2247
|
const p = Math.min(1, (now - start) / duration);
|
|
2248
|
+
const to = Math.max(0, Math.min(target(body), body.scrollHeight - body.clientHeight));
|
|
2253
2249
|
body.scrollTop = from + (to - from) * (1 - Math.pow(1 - p, 3));
|
|
2254
2250
|
if (p < 1) breakdownScrollRaf.current = window.requestAnimationFrame(step);
|
|
2255
2251
|
};
|
|
2256
2252
|
breakdownScrollRaf.current = window.requestAnimationFrame(step);
|
|
2257
2253
|
}, []);
|
|
2254
|
+
const scrollSheetToBreakdown = React.useCallback((): void => {
|
|
2255
|
+
animateSheetScroll((body) => {
|
|
2256
|
+
const el = mobileBreakdownRef.current;
|
|
2257
|
+
if (!el) return body.scrollTop;
|
|
2258
|
+
const bodyTop = body.getBoundingClientRect().top;
|
|
2259
|
+
const headlineTop = el.getBoundingClientRect().top - bodyTop + body.scrollTop;
|
|
2260
|
+
const cta = mobileBottomCtaRef.current;
|
|
2261
|
+
const ctaLimit = cta ? cta.getBoundingClientRect().top - bodyTop + body.scrollTop - body.clientHeight : Infinity;
|
|
2262
|
+
return Math.min(headlineTop - 8, ctaLimit);
|
|
2263
|
+
});
|
|
2264
|
+
}, [animateSheetScroll]);
|
|
2258
2265
|
React.useEffect(() => () => window.cancelAnimationFrame(breakdownScrollRaf.current), []);
|
|
2266
|
+
// collapsed sheet height = everything down to the [data-sheet-fold] element's bottom edge (its
|
|
2267
|
+
// own bottom padding is the breathing gap — the body's padding only exists at the very end of
|
|
2268
|
+
// the scroll content, so the breakdown would peek in there); re-measured when the content changes size (tab switch, leasing's extra
|
|
2269
|
+
// slider, rate text) or the viewport does. Fed to md3.css as --fin-sheet-fold (mobile only).
|
|
2270
|
+
const sheetRef = React.useRef<HTMLDivElement | null>(null);
|
|
2271
|
+
const [sheetFold, setSheetFold] = React.useState<number | null>(null);
|
|
2272
|
+
const [sheetAnimated, setSheetAnimated] = React.useState(false);
|
|
2273
|
+
React.useLayoutEffect(() => {
|
|
2274
|
+
if (!open) return;
|
|
2275
|
+
const sheet = sheetRef.current;
|
|
2276
|
+
const content = sheet?.querySelector('.fin-modal-body > .show-mobile') as HTMLElement | null | undefined;
|
|
2277
|
+
if (!sheet || !content) return;
|
|
2278
|
+
const measure = (): void => {
|
|
2279
|
+
const body = sheet.querySelector('.fin-modal-body') as HTMLElement | null;
|
|
2280
|
+
const fold = content.querySelector('[data-sheet-fold]') as HTMLElement | null;
|
|
2281
|
+
if (!body || !fold || !content.offsetParent) return;
|
|
2282
|
+
const bodyRect = body.getBoundingClientRect();
|
|
2283
|
+
const foldBottom = fold.getBoundingClientRect().bottom - bodyRect.top + body.scrollTop;
|
|
2284
|
+
const h = bodyRect.top - sheet.getBoundingClientRect().top + foldBottom;
|
|
2285
|
+
setSheetFold(Math.floor(h));
|
|
2286
|
+
};
|
|
2287
|
+
measure();
|
|
2288
|
+
// height transition only from the next frame on — the first measure must not animate the
|
|
2289
|
+
// freshly opened sheet from the 85vh fallback down to its fold
|
|
2290
|
+
const raf = window.requestAnimationFrame(() => setSheetAnimated(true));
|
|
2291
|
+
const ro = typeof ResizeObserver !== 'undefined' ? new ResizeObserver(measure) : null;
|
|
2292
|
+
ro?.observe(content);
|
|
2293
|
+
window.addEventListener('resize', measure);
|
|
2294
|
+
return () => {
|
|
2295
|
+
window.cancelAnimationFrame(raf);
|
|
2296
|
+
ro?.disconnect();
|
|
2297
|
+
window.removeEventListener('resize', measure);
|
|
2298
|
+
};
|
|
2299
|
+
}, [open, mode]);
|
|
2300
|
+
React.useEffect(() => {
|
|
2301
|
+
if (!open) setSheetAnimated(false);
|
|
2302
|
+
}, [open]);
|
|
2259
2303
|
// opened directly via the page's details link: open expanded and scrolled to the breakdown.
|
|
2260
2304
|
React.useEffect(() => {
|
|
2261
2305
|
if (!open) return;
|
|
@@ -2268,19 +2312,19 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
2268
2312
|
return () => window.clearTimeout(timer);
|
|
2269
2313
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
2270
2314
|
}, [open, initialShowBreakdown]);
|
|
2271
|
-
// in-sheet details
|
|
2315
|
+
// in-sheet details link: expanding scrolls down to the breakdown, collapsing back to the top.
|
|
2272
2316
|
const revealBreakdownRef = React.useRef(false);
|
|
2273
2317
|
React.useEffect(() => {
|
|
2274
|
-
if (!
|
|
2318
|
+
if (!revealBreakdownRef.current) return;
|
|
2275
2319
|
revealBreakdownRef.current = false;
|
|
2276
|
-
scrollSheetToBreakdown();
|
|
2277
|
-
|
|
2320
|
+
if (showBreakdown) scrollSheetToBreakdown();
|
|
2321
|
+
else animateSheetScroll(() => 0);
|
|
2322
|
+
}, [showBreakdown, scrollSheetToBreakdown, animateSheetScroll]);
|
|
2278
2323
|
|
|
2279
2324
|
// mobile-only: swipe-down-to-dismiss on the grabber/header (the mockup's drag handle).
|
|
2280
2325
|
// The sheet follows the finger (imperative transform — no re-renders while dragging);
|
|
2281
2326
|
// released past 90px it slides out and closes, otherwise it springs back. The element
|
|
2282
2327
|
// unmounts on close (`open` gate), so the inline transform never leaks into the next open.
|
|
2283
|
-
const sheetRef = React.useRef<HTMLDivElement | null>(null);
|
|
2284
2328
|
const dragStartY = React.useRef<number | null>(null);
|
|
2285
2329
|
const onGrabStart = (e: React.TouchEvent): void => {
|
|
2286
2330
|
// sheet gesture is phone-only — the desktop centered modal must not be draggable
|
|
@@ -2298,7 +2342,8 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
2298
2342
|
const dy = e.changedTouches[0].clientY - dragStartY.current;
|
|
2299
2343
|
dragStartY.current = null;
|
|
2300
2344
|
const el = sheetRef.current;
|
|
2301
|
-
|
|
2345
|
+
// keep .fin-modal's own height transition (expand/collapse) alongside the spring-back
|
|
2346
|
+
el.style.transition = 'transform var(--dur-2, .25s), height 0.35s var(--ease-standard, ease)';
|
|
2302
2347
|
if (dy > 90) {
|
|
2303
2348
|
el.style.transform = 'translateY(100%)';
|
|
2304
2349
|
window.setTimeout(onClose, 200);
|
|
@@ -2509,7 +2554,8 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
2509
2554
|
return (
|
|
2510
2555
|
<div className="fin-modal-scrim" onClick={onClose}>
|
|
2511
2556
|
<div
|
|
2512
|
-
className=
|
|
2557
|
+
className={'fin-modal' + (showBreakdown ? ' fin-modal--expanded' : '') + (sheetAnimated ? ' fin-modal--animated' : '')}
|
|
2558
|
+
style={sheetFold != null ? ({ '--fin-sheet-fold': `${sheetFold}px` } as React.CSSProperties) : undefined}
|
|
2513
2559
|
role="dialog"
|
|
2514
2560
|
aria-modal="true"
|
|
2515
2561
|
aria-label={title}
|
|
@@ -3265,7 +3311,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
3265
3311
|
/>
|
|
3266
3312
|
)}
|
|
3267
3313
|
{/* pushed to the sheet's bottom edge — Figma keeps a big breathing gap above */}
|
|
3268
|
-
<div className="fin-sheet-cta-row fin-sheet-cta-row--cash" style={{ display: 'flex', gap: 7, marginTop: 'auto' }}>
|
|
3314
|
+
<div data-sheet-fold="" className="fin-sheet-cta-row fin-sheet-cta-row--cash" style={{ display: 'flex', gap: 7, marginTop: 'auto' }}>
|
|
3269
3315
|
<Button
|
|
3270
3316
|
variant="filled"
|
|
3271
3317
|
size="lg"
|
|
@@ -3449,8 +3495,9 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
3449
3495
|
{/* centered ⓘ details link (mockup shows no chevron) — scrolls to the breakdown below */}
|
|
3450
3496
|
<button
|
|
3451
3497
|
type="button"
|
|
3498
|
+
data-sheet-fold=""
|
|
3452
3499
|
onClick={() => {
|
|
3453
|
-
revealBreakdownRef.current =
|
|
3500
|
+
revealBreakdownRef.current = true;
|
|
3454
3501
|
setShowBreakdown((v) => !v);
|
|
3455
3502
|
}}
|
|
3456
3503
|
style={{ ...expanderBtnStyle, justifyContent: 'center', padding: '2px 0 20px' }}>
|