@bytebrand/fe-ui-core-autobahn 1.0.113 → 1.0.114

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/vdp/VdpViewer.tsx CHANGED
@@ -64,6 +64,27 @@ const MOBILE_BREAKPOINT = 860;
64
64
  /** Landscape thumbnails (Figma node 424:7566 etc.) are 68px, not portrait's 64. */
65
65
  const LS_THUMB_W = 68;
66
66
 
67
+ /** Safe-area helpers — keep overlay controls (especially the close button) clear of the notch /
68
+ * Dynamic Island / status bar (top), home indicator (bottom) and, in landscape, whichever side the
69
+ * notch rotates onto. `env(safe-area-inset-*)` only resolves to anything but 0px when the page's
70
+ * `<meta name="viewport">` declares `viewport-fit=cover` (see services/fe-main/app.html) — the
71
+ * 0px fallback inside every `env()` call here means both of these are complete no-ops (identical
72
+ * to the previous hardcoded numbers) on any browser/device that has no safe area to begin with, so
73
+ * there's no behavioural change for the common case, only where a notch/indicator is actually
74
+ * present. Two shapes, used in different structural contexts below:
75
+ * - `withInset` ADDS the inset to a design offset — for absolute-positioned controls (landscape's
76
+ * floating close/logo/counter/prev-next/zoom/etc.) whose `top`/`right`/… is a gap from the edge
77
+ * of the *safe* content area, which needs to grow by however much the notch/indicator eats into
78
+ * the raw viewport edge.
79
+ * - `atLeastInset` guarantees a MINIMUM gap instead — for padding on a bar that already paints its
80
+ * own edge-to-edge background (portrait's header/bottom-CTA bar, the photosheet), where a small
81
+ * or absent safe area should still leave the existing 12px breathing room, not shrink it.
82
+ */
83
+ const withInset = (px: number, side: 'top' | 'right' | 'bottom' | 'left'): string =>
84
+ `calc(${px}px + env(safe-area-inset-${side}, 0px))`;
85
+ const atLeastInset = (px: number, side: 'top' | 'right' | 'bottom' | 'left'): string =>
86
+ `max(${px}px, env(safe-area-inset-${side}, 0px))`;
87
+
67
88
  type Layout = 'portrait' | 'landscape' | 'desktop';
68
89
  const layoutForWidth = (w: number): Layout => (w <= PORTRAIT_MAX ? 'portrait' : w <= MOBILE_BREAKPOINT ? 'landscape' : 'desktop');
69
90
 
@@ -719,7 +740,10 @@ const VdpViewer: React.FunctionComponent<VdpViewerProps> = ({ images, start = 0,
719
740
  display: 'flex',
720
741
  alignItems: 'flex-start',
721
742
  justifyContent: 'space-between',
722
- padding: '12px 12px 10px',
743
+ // safe-area-aware: this bar sits flush against the top of the fullscreen overlay
744
+ // (the notch/Dynamic Island/status bar's exact territory), and its close button is
745
+ // the primary control the "always tappable" acceptance criterion is about.
746
+ padding: `${atLeastInset(12, 'top')} ${atLeastInset(12, 'right')} 10px ${atLeastInset(12, 'left')}`,
723
747
  background: 'rgba(6,11,18,0.8)',
724
748
  borderBottom: '1px solid rgba(255,255,255,0.12)',
725
749
  }}
@@ -730,7 +754,10 @@ const VdpViewer: React.FunctionComponent<VdpViewerProps> = ({ images, start = 0,
730
754
  ariaLabel={t('autobahn:viewer.closeLabel')}
731
755
  onClick={onClose}
732
756
  />
733
- <div style={{ position: 'absolute', left: '50%', top: 10, transform: 'translateX(-50%)', width: 160, height: 71 }}>
757
+ {/* absolutely positioned (unlike its close/call siblings), so it does NOT
758
+ automatically shift down with the header's safe-area padding above — needs its
759
+ own top offset. */}
760
+ <div style={{ position: 'absolute', left: '50%', top: withInset(10, 'top'), transform: 'translateX(-50%)', width: 160, height: 71 }}>
734
761
  <LogoMark />
735
762
  </div>
736
763
  {onCall ? (
@@ -914,7 +941,23 @@ const VdpViewer: React.FunctionComponent<VdpViewerProps> = ({ images, start = 0,
914
941
  {/* full-width bottom bar — "Jetzt Anfragen" (primary) + "Anrufen" (dark), Figma nodes
915
942
  424:8128-8134. */}
916
943
  {(onEmail || onCall) && (
917
- <div style={{ display: 'flex', gap: THUMB_GAP, padding: 12, height: 82, background: '#797979', flex: 'none' }}>
944
+ // safe-area-aware: last item before the true bottom edge — height is deliberately
945
+ // NOT fixed anymore (was 82px) so the extra bottom padding on a home-indicator device
946
+ // grows the bar instead of squeezing its 56px-tall buttons; `alignItems: 'center'`
947
+ // keeps them centered in that now-variable padding box.
948
+ <div
949
+ style={{
950
+ display: 'flex',
951
+ alignItems: 'center',
952
+ gap: THUMB_GAP,
953
+ paddingTop: 12,
954
+ paddingRight: atLeastInset(12, 'right'),
955
+ paddingBottom: atLeastInset(12, 'bottom'),
956
+ paddingLeft: atLeastInset(12, 'left'),
957
+ background: '#797979',
958
+ flex: 'none',
959
+ }}
960
+ >
918
961
  {onEmail && (
919
962
  <button
920
963
  type="button"
@@ -1035,25 +1078,54 @@ const VdpViewer: React.FunctionComponent<VdpViewerProps> = ({ images, start = 0,
1035
1078
  brand-blue/gradient mark (verified against its raw export), i.e. the SAME svg this
1036
1079
  codebase already ships as `assets/autode-logo.svg` — reused directly, not portrait's
1037
1080
  white LogoMark (landscape has no dark bar behind it to need a white variant). */}
1038
- <div style={{ position: 'absolute', left: 4, top: 10, width: 93, height: 42, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
1081
+ <div
1082
+ style={{
1083
+ position: 'absolute',
1084
+ left: withInset(4, 'left'),
1085
+ top: withInset(10, 'top'),
1086
+ width: 93,
1087
+ height: 42,
1088
+ display: 'flex',
1089
+ alignItems: 'center',
1090
+ justifyContent: 'center',
1091
+ }}
1092
+ >
1039
1093
  <img src={logoUrl} alt="auto.de" style={{ width: 74, height: 21.46, display: 'block' }} />
1040
1094
  </div>
1041
1095
 
1042
1096
  {/* counter pill — Figma node 424:9277 "Counter / BG" */}
1043
- <div style={lsCounterStyle} aria-label={t('autobahn:viewer.counterLabel', { current: i + 1, total: n })}>
1097
+ <div style={{ ...lsCounterStyle, top: withInset(12, 'top') }} aria-label={t('autobahn:viewer.counterLabel', { current: i + 1, total: n })}>
1044
1098
  <span className="num">
1045
1099
  {i + 1} | {n}
1046
1100
  </span>
1047
1101
  </div>
1048
1102
 
1049
- {/* close — Figma nodes 424:7556/7557, no caption text in this layout */}
1050
- <LsCircleButton icon={<IconClose />} ariaLabel={t('autobahn:viewer.closeLabel')} onClick={onClose} style={{ right: 12, top: 12 }} />
1103
+ {/* close — Figma nodes 424:7556/7557, no caption text in this layout. Rotating the
1104
+ phone moves the notch/Dynamic Island to whichever side is now "up" (device-relative
1105
+ left/right, not a fixed screen side) — safe-area-aware on both axes since this sits
1106
+ at the true top-right corner, the corner most likely to overlap it. */}
1107
+ <LsCircleButton
1108
+ icon={<IconClose />}
1109
+ ariaLabel={t('autobahn:viewer.closeLabel')}
1110
+ onClick={onClose}
1111
+ style={{ right: withInset(12, 'right'), top: withInset(12, 'top') }}
1112
+ />
1051
1113
 
1052
1114
  {/* prev/next — Figma nodes 424:7560/7561 and 424:7563/7564 */}
1053
1115
  {n > 1 && (
1054
1116
  <>
1055
- <LsCircleButton icon={<IconChevronLeft />} ariaLabel={t('autobahn:viewer.previousLabel')} onClick={() => go(-1)} style={{ left: 12, top: 126 }} />
1056
- <LsCircleButton icon={<IconChevronRight />} ariaLabel={t('autobahn:viewer.nextLabel')} onClick={() => go(1)} style={{ right: 12, top: 126 }} />
1117
+ <LsCircleButton
1118
+ icon={<IconChevronLeft />}
1119
+ ariaLabel={t('autobahn:viewer.previousLabel')}
1120
+ onClick={() => go(-1)}
1121
+ style={{ left: withInset(12, 'left'), top: 126 }}
1122
+ />
1123
+ <LsCircleButton
1124
+ icon={<IconChevronRight />}
1125
+ ariaLabel={t('autobahn:viewer.nextLabel')}
1126
+ onClick={() => go(1)}
1127
+ style={{ right: withInset(12, 'right'), top: 126 }}
1128
+ />
1057
1129
  </>
1058
1130
  )}
1059
1131
 
@@ -1062,8 +1134,8 @@ const VdpViewer: React.FunctionComponent<VdpViewerProps> = ({ images, start = 0,
1062
1134
  <div
1063
1135
  style={{
1064
1136
  position: 'absolute',
1065
- right: 12,
1066
- bottom: 12,
1137
+ right: withInset(12, 'right'),
1138
+ bottom: withInset(12, 'bottom'),
1067
1139
  width: 48,
1068
1140
  height: 150,
1069
1141
  borderRadius: 'var(--md-r-full)',
@@ -1126,7 +1198,20 @@ const VdpViewer: React.FunctionComponent<VdpViewerProps> = ({ images, start = 0,
1126
1198
  align the way the design does, without hardcoding a container width that would break
1127
1199
  across the 693-860 range. `right: 81` keeps this band clear of the zoom/360 column
1128
1200
  (12 right-margin + 48 wide + the 21px gap Figma leaves before it = 81). */}
1129
- <div style={{ position: 'absolute', left: 12, right: 81, bottom: 12, height: 50, display: 'flex', alignItems: 'center' }}>
1201
+ <div
1202
+ style={{
1203
+ position: 'absolute',
1204
+ left: withInset(12, 'left'),
1205
+ // 81 = 12 right-margin + 48 zoom-column width + 21 Figma gap before it; add the
1206
+ // same right inset the zoom/360 column itself got above so this band still clears
1207
+ // it once that column has shifted in from a notched edge.
1208
+ right: withInset(81, 'right'),
1209
+ bottom: withInset(12, 'bottom'),
1210
+ height: 50,
1211
+ display: 'flex',
1212
+ alignItems: 'center',
1213
+ }}
1214
+ >
1130
1215
  {(() => {
1131
1216
  const overflow = images.length > lsFitCount;
1132
1217
  const visibleCount = overflow ? lsFitCount - 1 : images.length;