@bytebrand/fe-ui-core-autobahn 1.0.64 → 1.0.66
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/cardParts.tsx +6 -1
- package/package.json +1 -1
- package/theme/md3.css +34 -0
- package/ui/CardCarousel.tsx +42 -60
package/cards/cardParts.tsx
CHANGED
|
@@ -861,6 +861,11 @@ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t, c
|
|
|
861
861
|
// same real struck-through reference rate PriceHero shows for the financing tab (oldMonthly) —
|
|
862
862
|
// shown here too now that both rows render at once instead of behind a tab.
|
|
863
863
|
const showOldMonthly = car.oldMonthly != null && car.oldMonthly > car.monthly;
|
|
864
|
+
// same digit-length shrink technique as PriceHero's own rateLenClass, so a long rate (e.g.
|
|
865
|
+
// 1.612 €) doesn't overflow the desktop hero's much larger 65px base size.
|
|
866
|
+
const rateDigits = String(Math.round(car.monthly)).length;
|
|
867
|
+
const rateLenClass =
|
|
868
|
+
rateDigits >= 6 ? ' pp-hero-val--len6' : rateDigits === 5 ? ' pp-hero-val--len5' : rateDigits === 4 ? ' pp-hero-val--len4' : '';
|
|
864
869
|
return (
|
|
865
870
|
<div className={'pricepaths' + (className ? ' ' + className : '')}>
|
|
866
871
|
<div className="pp-hero">
|
|
@@ -871,7 +876,7 @@ export const PricePaths: React.FunctionComponent<PricePathsProps> = ({ car, t, c
|
|
|
871
876
|
{(car.oldMonthly as number).toLocaleString('de-DE')} €<sup>2</sup>
|
|
872
877
|
</span>
|
|
873
878
|
)}
|
|
874
|
-
<b className=
|
|
879
|
+
<b className={'num pp-hero-val' + rateLenClass}>
|
|
875
880
|
{fmtRate(car.monthly)} €<sup>1</sup>
|
|
876
881
|
</b>
|
|
877
882
|
<span className="pp-hero-unit">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/fe-ui-core-autobahn",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.66",
|
|
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
|
@@ -4276,6 +4276,40 @@ line-height: 31px;
|
|
|
4276
4276
|
/* struck-through reference rate/price shown next to the tabbed hero price (Figma card) —
|
|
4277
4277
|
same red strike as .pp-old, sized for the hero context rather than the compact list row. */
|
|
4278
4278
|
.md3 .pp-hero-old { font-size: 15px; color: var(--md-error); text-decoration: line-through; white-space: nowrap; }
|
|
4279
|
+
/* desktop mcard financing hero: same 65px/18px scale already proven (real car data, no overflow)
|
|
4280
|
+
for the grid CarCard's identical .cc-price hero — `.pricepaths` only ever exists inside the
|
|
4281
|
+
SRP card's desktop-only PricePaths output (mobile's PriceHero, used by the tab switcher, has
|
|
4282
|
+
no `.pricepaths` ancestor), so this can't leak into mobile without an extra media query. */
|
|
4283
|
+
.md3 .listcard-price .pricepaths .pp-hero-val {
|
|
4284
|
+
font-size: 65px;
|
|
4285
|
+
line-height: 58px;
|
|
4286
|
+
letter-spacing: -2px;
|
|
4287
|
+
}
|
|
4288
|
+
.md3 .listcard-price .pricepaths .pp-hero-val--len4 { font-size: 50px; line-height: 53px; }
|
|
4289
|
+
.md3 .listcard-price .pricepaths .pp-hero-val--len5 { font-size: 43px; line-height: 45px; }
|
|
4290
|
+
.md3 .listcard-price .pricepaths .pp-hero-val--len6 { font-size: 37px; line-height: 40px; }
|
|
4291
|
+
.md3 .listcard-price .pricepaths .pp-hero-unit { font-size: 18px; font-weight: 700; color: #5b6b7f; }
|
|
4292
|
+
/* diagonal red strike (Figma), replacing the plain text-decoration:line-through above — same
|
|
4293
|
+
generic technique .cc-price/.fin-calc-card already use, content-width-independent (the ±2px
|
|
4294
|
+
left/right offsets stretch the line across whatever the old price's own width ends up being). */
|
|
4295
|
+
.md3 .listcard-price .pricepaths .pp-hero-old {
|
|
4296
|
+
position: relative;
|
|
4297
|
+
display: inline-block;
|
|
4298
|
+
font-size: 17px;
|
|
4299
|
+
font-weight: 400;
|
|
4300
|
+
color: #0e2238;
|
|
4301
|
+
text-decoration: none;
|
|
4302
|
+
}
|
|
4303
|
+
.md3 .listcard-price .pricepaths .pp-hero-old::after {
|
|
4304
|
+
content: '';
|
|
4305
|
+
position: absolute;
|
|
4306
|
+
left: -2px;
|
|
4307
|
+
right: -2px;
|
|
4308
|
+
top: 50%;
|
|
4309
|
+
border-top: 2px solid #e60000;
|
|
4310
|
+
transform: rotate(12.53deg);
|
|
4311
|
+
transform-origin: center;
|
|
4312
|
+
}
|
|
4279
4313
|
.md3 .pp-hero-terms { margin-top: 9px; font-size: 11.5px; color: var(--md-on-surface-variant); }
|
|
4280
4314
|
/* "Details" link at the end of the financing/leasing terms lines (Figma "mcard") — quiet, same
|
|
4281
4315
|
color as the rest of the terms line but underlined so it still reads as tappable. */
|
package/ui/CardCarousel.tsx
CHANGED
|
@@ -11,12 +11,12 @@ export interface CardCarouselProps {
|
|
|
11
11
|
/** fired with the new active index whenever the slide changes. */
|
|
12
12
|
onChange?: (index: number) => void;
|
|
13
13
|
/**
|
|
14
|
-
* object-fit for each image; 'contain' adds inset padding by default. 'auto'
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* 'contain' below 601px viewport width (no change to any
|
|
14
|
+
* object-fit for each image; 'contain' adds inset padding by default. 'auto' always fills the
|
|
15
|
+
* box's full WIDTH (never a left/right gap) and lets height follow the image's own aspect
|
|
16
|
+
* ratio — landscape photos in this taller-than-wide card box typically end up shorter than the
|
|
17
|
+
* box (a top/bottom gap, filled by the studio-white background), while a portrait/near-square
|
|
18
|
+
* photo can end up taller than the box and get cropped top/bottom by its overflow:hidden. 'auto'
|
|
19
|
+
* behaves exactly like 'contain' below 601px viewport width (no change to any mobile rendering).
|
|
20
20
|
*/
|
|
21
21
|
fit?: 'cover' | 'contain' | 'auto';
|
|
22
22
|
/** set false to drop the 5%/6% inset padding 'contain' normally adds (e.g. the VDP
|
|
@@ -24,9 +24,6 @@ export interface CardCarouselProps {
|
|
|
24
24
|
padded?: boolean;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
/** 'auto' mode: object-fit:cover only up to this much of the image being cropped away. */
|
|
28
|
-
const AUTO_FIT_CROP_THRESHOLD = 0.25;
|
|
29
|
-
|
|
30
27
|
/**
|
|
31
28
|
* Image carousel for cards — touch-swipeable, dots + hover arrows (desktop).
|
|
32
29
|
* Stops click bubbling so it can live inside a clickable card.
|
|
@@ -59,7 +56,6 @@ const CardCarousel: React.FunctionComponent<CardCarouselProps> = ({
|
|
|
59
56
|
};
|
|
60
57
|
const go = (d: number, e?: React.SyntheticEvent) => jump((i + d + n) % n, e);
|
|
61
58
|
const trackRef = useRef<HTMLDivElement>(null);
|
|
62
|
-
const containerRef = useRef<HTMLDivElement>(null);
|
|
63
59
|
const tx = useRef<number | null>(null);
|
|
64
60
|
|
|
65
61
|
// 'auto' mode only diverges from plain 'contain' at desktop widths — below 601px it resolves
|
|
@@ -74,38 +70,11 @@ const CardCarousel: React.FunctionComponent<CardCarouselProps> = ({
|
|
|
74
70
|
mq.addEventListener('change', handler);
|
|
75
71
|
return () => mq.removeEventListener('change', handler);
|
|
76
72
|
}, [fit]);
|
|
77
|
-
|
|
78
|
-
// gallery gets its own cover/contain call instead of one verdict for the whole carousel.
|
|
79
|
-
const [autoFits, setAutoFits] = useState<Record<number, 'cover' | 'contain'>>({});
|
|
80
|
-
const handleLoad = (k: number, el: HTMLImageElement | null) => {
|
|
81
|
-
if (fit !== 'auto' || !isDesktop || !el) return;
|
|
82
|
-
const box = containerRef.current;
|
|
83
|
-
if (!box || !el.naturalWidth || !el.naturalHeight) return;
|
|
84
|
-
const boxAspect = box.clientWidth / box.clientHeight;
|
|
85
|
-
const imgAspect = el.naturalWidth / el.naturalHeight;
|
|
86
|
-
const cropped = imgAspect >= boxAspect ? 1 - boxAspect / imgAspect : 1 - imgAspect / boxAspect;
|
|
87
|
-
const resolved: 'cover' | 'contain' = cropped > AUTO_FIT_CROP_THRESHOLD ? 'contain' : 'cover';
|
|
88
|
-
setAutoFits((prev) => (prev[k] === resolved ? prev : { ...prev, [k]: resolved }));
|
|
89
|
-
};
|
|
90
|
-
// resolved per-image fit: literal cover/contain pass straight through; 'auto' defaults to the
|
|
91
|
-
// safe no-crop 'contain' until that image has actually loaded and been measured.
|
|
92
|
-
const resolvedFit = (k: number): 'cover' | 'contain' =>
|
|
93
|
-
fit === 'auto' ? (isDesktop ? (autoFits[k] ?? 'contain') : 'contain') : fit;
|
|
94
|
-
// catches images already complete (browser cache) by the time this mounts — onLoad never
|
|
95
|
-
// fires for those. Runs after the full commit, so containerRef is guaranteed attached (a ref
|
|
96
|
-
// callback on the <img> itself isn't: React attaches child refs before the parent's).
|
|
97
|
-
useEffect(() => {
|
|
98
|
-
if (fit !== 'auto' || !isDesktop) return;
|
|
99
|
-
trackRef.current?.querySelectorAll('img').forEach((el, k) => {
|
|
100
|
-
if (el.complete) handleLoad(k, el);
|
|
101
|
-
});
|
|
102
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
103
|
-
}, [fit, isDesktop, gallerySignature]);
|
|
73
|
+
const autoDesktop = fit === 'auto' && isDesktop;
|
|
104
74
|
|
|
105
75
|
return (
|
|
106
76
|
<div
|
|
107
77
|
className="cardcar"
|
|
108
|
-
ref={containerRef}
|
|
109
78
|
style={{ position: 'relative', height, overflow: 'hidden', borderRadius: radius }}
|
|
110
79
|
onTouchStart={(e) => {
|
|
111
80
|
tx.current = e.touches[0].clientX;
|
|
@@ -126,28 +95,41 @@ const CardCarousel: React.FunctionComponent<CardCarouselProps> = ({
|
|
|
126
95
|
transition: 'transform var(--dur-3) var(--ease-emphasized)',
|
|
127
96
|
}}
|
|
128
97
|
>
|
|
129
|
-
{images.map((src, k) =>
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
98
|
+
{images.map((src, k) => (
|
|
99
|
+
<img
|
|
100
|
+
key={k}
|
|
101
|
+
src={src}
|
|
102
|
+
alt=""
|
|
103
|
+
loading="lazy"
|
|
104
|
+
decoding="async"
|
|
105
|
+
style={
|
|
106
|
+
autoDesktop
|
|
107
|
+
? {
|
|
108
|
+
// width-anchored: always fills the box's full width (no left/right gap,
|
|
109
|
+
// ever); height is 'auto' (NOT a min-height floor — that would force
|
|
110
|
+
// object-fit:fill's default stretch/distortion on a narrower photo), so it
|
|
111
|
+
// follows the photo's own ratio, cropped top/bottom by the box's
|
|
112
|
+
// overflow:hidden when taller than the box, centered so the crop doesn't
|
|
113
|
+
// favor either edge.
|
|
114
|
+
flex: '0 0 100%',
|
|
115
|
+
width: '100%',
|
|
116
|
+
height: 'auto',
|
|
117
|
+
alignSelf: 'center',
|
|
118
|
+
boxSizing: 'border-box',
|
|
119
|
+
background: '#fff',
|
|
120
|
+
}
|
|
121
|
+
: {
|
|
122
|
+
flex: '0 0 100%',
|
|
123
|
+
width: '100%',
|
|
124
|
+
height: '100%',
|
|
125
|
+
objectFit: fit === 'auto' ? 'contain' : fit,
|
|
126
|
+
padding: fit !== 'cover' && padded ? '5% 6%' : 0,
|
|
127
|
+
boxSizing: 'border-box',
|
|
128
|
+
background: '#fff',
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/>
|
|
132
|
+
))}
|
|
151
133
|
</div>
|
|
152
134
|
{/* arrows + dots only when there is actually something to switch to — with a single
|
|
153
135
|
image (e.g. SRP cards before their gallery is lazy-loaded) they are dead controls. */}
|