@bytebrand/fe-ui-core-autobahn 1.0.64 → 1.0.65

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/ui/CardCarousel.tsx +42 -60
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.64",
3
+ "version": "1.0.65",
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",
@@ -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' 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).
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
- // 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]);
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
- 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
- })}
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. */}