@bytebrand/fe-ui-core-autobahn 1.0.63 → 1.0.64
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/CarRow.tsx +3 -1
- package/package.json +1 -1
- package/theme/md3.css +0 -8
- package/ui/CardCarousel.tsx +77 -20
package/cards/CarRow.tsx
CHANGED
|
@@ -86,7 +86,9 @@ const CarRow: React.FunctionComponent<CarRowProps> = ({ car, isFavorite, onToggl
|
|
|
86
86
|
style={{ background: 'var(--md-sc-lowest)' }}
|
|
87
87
|
>
|
|
88
88
|
<div className="listcard-media" style={{ position: 'relative' }} onMouseEnter={onMediaEnter} onTouchStart={onMediaEnter}>
|
|
89
|
-
|
|
89
|
+
{/* 'auto': desktop picks cover/contain per photo (crops only when it wouldn't lose too
|
|
90
|
+
much of the shot); below 601px this is identical to 'contain', so mobile is unchanged. */}
|
|
91
|
+
<CardCarousel images={car.gallery} height="100%" fit="auto" onChange={setPhotoIndex} />
|
|
90
92
|
{/* deal badges — ONE compact line: NEU · −€ · 0 € Anz. · −%, plus the "⚡ Angebot" badge
|
|
91
93
|
(Figma) on its own line below. The price RATING ('Guter Preis') is NOT here — it
|
|
92
94
|
lives in the price column (PriceMeter). */}
|
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.64",
|
|
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
|
@@ -4040,14 +4040,6 @@ line-height: 31px;
|
|
|
4040
4040
|
.md3 .specstrip > span:nth-child(1) { order: -3; }
|
|
4041
4041
|
.md3 .specstrip > span:nth-child(2) { order: -2; }
|
|
4042
4042
|
.md3 .specstrip > span:nth-child(4) { order: -1; }
|
|
4043
|
-
/* mcard (SRP CarRow) photo: fill the media box edge-to-edge on desktop (Figma "mcard") instead
|
|
4044
|
-
of the letterboxed object-fit:contain CardCarousel applies by default (that "contain" +
|
|
4045
|
-
padding is still correct on mobile, untouched — this overrides only the desktop rendering,
|
|
4046
|
-
since CardCarousel's `fit`/`padded` props are one shared JS value for both breakpoints). */
|
|
4047
|
-
.md3 .listcard-media .cardcar img {
|
|
4048
|
-
object-fit: cover !important;
|
|
4049
|
-
padding: 0 !important;
|
|
4050
|
-
}
|
|
4051
4043
|
}
|
|
4052
4044
|
/* filter rail: collapsible sections */
|
|
4053
4045
|
.md3 .frail-group {
|
package/ui/CardCarousel.tsx
CHANGED
|
@@ -10,13 +10,23 @@ export interface CardCarouselProps {
|
|
|
10
10
|
radius?: number | string;
|
|
11
11
|
/** fired with the new active index whenever the slide changes. */
|
|
12
12
|
onChange?: (index: number) => void;
|
|
13
|
-
/**
|
|
14
|
-
|
|
13
|
+
/**
|
|
14
|
+
* object-fit for each image; 'contain' adds inset padding by default. 'auto' picks per IMAGE,
|
|
15
|
+
* comparing that photo's own natural aspect ratio against the box: 'cover' (fills edge-to-edge)
|
|
16
|
+
* when the crop would be modest, 'contain' (letterboxed, no crop) when it would be severe — a
|
|
17
|
+
* wide exterior shot usually survives a horizontal crop fine (cars carry background padding on
|
|
18
|
+
* both sides), a tighter interior/detail shot often doesn't. 'auto' behaves exactly like
|
|
19
|
+
* 'contain' below 601px viewport width (no change to any existing mobile rendering).
|
|
20
|
+
*/
|
|
21
|
+
fit?: 'cover' | 'contain' | 'auto';
|
|
15
22
|
/** set false to drop the 5%/6% inset padding 'contain' normally adds (e.g. the VDP
|
|
16
23
|
* gallery, which wants the image filling the box edge-to-edge). Defaults to true. */
|
|
17
24
|
padded?: boolean;
|
|
18
25
|
}
|
|
19
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
|
+
|
|
20
30
|
/**
|
|
21
31
|
* Image carousel for cards — touch-swipeable, dots + hover arrows (desktop).
|
|
22
32
|
* Stops click bubbling so it can live inside a clickable card.
|
|
@@ -49,10 +59,53 @@ const CardCarousel: React.FunctionComponent<CardCarouselProps> = ({
|
|
|
49
59
|
};
|
|
50
60
|
const go = (d: number, e?: React.SyntheticEvent) => jump((i + d + n) % n, e);
|
|
51
61
|
const trackRef = useRef<HTMLDivElement>(null);
|
|
62
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
52
63
|
const tx = useRef<number | null>(null);
|
|
64
|
+
|
|
65
|
+
// 'auto' mode only diverges from plain 'contain' at desktop widths — below 601px it resolves
|
|
66
|
+
// to 'contain' unconditionally, so mobile's existing rendering is untouched either way.
|
|
67
|
+
const [isDesktop, setIsDesktop] = useState(
|
|
68
|
+
() => fit === 'auto' && typeof window !== 'undefined' && window.matchMedia('(min-width: 601px)').matches,
|
|
69
|
+
);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (fit !== 'auto' || typeof window === 'undefined') return;
|
|
72
|
+
const mq = window.matchMedia('(min-width: 601px)');
|
|
73
|
+
const handler = () => setIsDesktop(mq.matches);
|
|
74
|
+
mq.addEventListener('change', handler);
|
|
75
|
+
return () => mq.removeEventListener('change', handler);
|
|
76
|
+
}, [fit]);
|
|
77
|
+
// per-image measured fit (auto mode, desktop only) — keyed by index so each photo in the
|
|
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]);
|
|
104
|
+
|
|
53
105
|
return (
|
|
54
106
|
<div
|
|
55
107
|
className="cardcar"
|
|
108
|
+
ref={containerRef}
|
|
56
109
|
style={{ position: 'relative', height, overflow: 'hidden', borderRadius: radius }}
|
|
57
110
|
onTouchStart={(e) => {
|
|
58
111
|
tx.current = e.touches[0].clientX;
|
|
@@ -73,24 +126,28 @@ const CardCarousel: React.FunctionComponent<CardCarouselProps> = ({
|
|
|
73
126
|
transition: 'transform var(--dur-3) var(--ease-emphasized)',
|
|
74
127
|
}}
|
|
75
128
|
>
|
|
76
|
-
{images.map((src, k) =>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
129
|
+
{images.map((src, k) => {
|
|
130
|
+
const f = resolvedFit(k);
|
|
131
|
+
return (
|
|
132
|
+
<img
|
|
133
|
+
key={k}
|
|
134
|
+
src={src}
|
|
135
|
+
alt=""
|
|
136
|
+
loading="lazy"
|
|
137
|
+
decoding="async"
|
|
138
|
+
onLoad={(e) => handleLoad(k, e.currentTarget)}
|
|
139
|
+
style={{
|
|
140
|
+
flex: '0 0 100%',
|
|
141
|
+
width: '100%',
|
|
142
|
+
height: '100%',
|
|
143
|
+
objectFit: f,
|
|
144
|
+
padding: f === 'contain' && padded ? '5% 6%' : 0,
|
|
145
|
+
boxSizing: 'border-box',
|
|
146
|
+
background: '#fff',
|
|
147
|
+
}}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
94
151
|
</div>
|
|
95
152
|
{/* arrows + dots only when there is actually something to switch to — with a single
|
|
96
153
|
image (e.g. SRP cards before their gallery is lazy-loaded) they are dead controls. */}
|