@oxyhq/bloom 0.50.0 → 0.50.2

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 (85) hide show
  1. package/lib/commonjs/bottom-sheet/index.web.js +3 -2
  2. package/lib/commonjs/bottom-sheet/index.web.js.map +1 -1
  3. package/lib/commonjs/context-menu/index.web.js +3 -7
  4. package/lib/commonjs/context-menu/index.web.js.map +1 -1
  5. package/lib/commonjs/dialog/Dialog.js +18 -15
  6. package/lib/commonjs/dialog/Dialog.js.map +1 -1
  7. package/lib/commonjs/dialog/Dialog.web.js +17 -23
  8. package/lib/commonjs/dialog/Dialog.web.js.map +1 -1
  9. package/lib/commonjs/dialog/DialogHeader.js +84 -13
  10. package/lib/commonjs/dialog/DialogHeader.js.map +1 -1
  11. package/lib/commonjs/menu/index.web.js +3 -7
  12. package/lib/commonjs/menu/index.web.js.map +1 -1
  13. package/lib/commonjs/popover/index.web.js +5 -7
  14. package/lib/commonjs/popover/index.web.js.map +1 -1
  15. package/lib/commonjs/select/index.web.js +3 -7
  16. package/lib/commonjs/select/index.web.js.map +1 -1
  17. package/lib/commonjs/slider/Slider.js +4 -9
  18. package/lib/commonjs/slider/Slider.js.map +1 -1
  19. package/lib/commonjs/switch/Switch.js +4 -8
  20. package/lib/commonjs/switch/Switch.js.map +1 -1
  21. package/lib/commonjs/tabs/Tabs.js +3 -8
  22. package/lib/commonjs/tabs/Tabs.js.map +1 -1
  23. package/lib/commonjs/toast/Toast.js +3 -8
  24. package/lib/commonjs/toast/Toast.js.map +1 -1
  25. package/lib/module/bottom-sheet/index.web.js +3 -2
  26. package/lib/module/bottom-sheet/index.web.js.map +1 -1
  27. package/lib/module/context-menu/index.web.js +3 -7
  28. package/lib/module/context-menu/index.web.js.map +1 -1
  29. package/lib/module/dialog/Dialog.js +19 -16
  30. package/lib/module/dialog/Dialog.js.map +1 -1
  31. package/lib/module/dialog/Dialog.web.js +18 -24
  32. package/lib/module/dialog/Dialog.web.js.map +1 -1
  33. package/lib/module/dialog/DialogHeader.js +83 -12
  34. package/lib/module/dialog/DialogHeader.js.map +1 -1
  35. package/lib/module/menu/index.web.js +3 -7
  36. package/lib/module/menu/index.web.js.map +1 -1
  37. package/lib/module/popover/index.web.js +5 -7
  38. package/lib/module/popover/index.web.js.map +1 -1
  39. package/lib/module/select/index.web.js +3 -7
  40. package/lib/module/select/index.web.js.map +1 -1
  41. package/lib/module/slider/Slider.js +4 -9
  42. package/lib/module/slider/Slider.js.map +1 -1
  43. package/lib/module/switch/Switch.js +4 -8
  44. package/lib/module/switch/Switch.js.map +1 -1
  45. package/lib/module/tabs/Tabs.js +3 -8
  46. package/lib/module/tabs/Tabs.js.map +1 -1
  47. package/lib/module/toast/Toast.js +3 -8
  48. package/lib/module/toast/Toast.js.map +1 -1
  49. package/lib/typescript/commonjs/context-menu/index.web.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/dialog/Dialog.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/dialog/Dialog.web.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/dialog/DialogHeader.d.ts +13 -0
  53. package/lib/typescript/commonjs/dialog/DialogHeader.d.ts.map +1 -1
  54. package/lib/typescript/commonjs/menu/index.web.d.ts.map +1 -1
  55. package/lib/typescript/commonjs/popover/index.web.d.ts.map +1 -1
  56. package/lib/typescript/commonjs/select/index.web.d.ts.map +1 -1
  57. package/lib/typescript/commonjs/slider/Slider.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/switch/Switch.d.ts.map +1 -1
  59. package/lib/typescript/commonjs/tabs/Tabs.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/toast/Toast.d.ts.map +1 -1
  61. package/lib/typescript/module/context-menu/index.web.d.ts.map +1 -1
  62. package/lib/typescript/module/dialog/Dialog.d.ts.map +1 -1
  63. package/lib/typescript/module/dialog/Dialog.web.d.ts.map +1 -1
  64. package/lib/typescript/module/dialog/DialogHeader.d.ts +13 -0
  65. package/lib/typescript/module/dialog/DialogHeader.d.ts.map +1 -1
  66. package/lib/typescript/module/menu/index.web.d.ts.map +1 -1
  67. package/lib/typescript/module/popover/index.web.d.ts.map +1 -1
  68. package/lib/typescript/module/select/index.web.d.ts.map +1 -1
  69. package/lib/typescript/module/slider/Slider.d.ts.map +1 -1
  70. package/lib/typescript/module/switch/Switch.d.ts.map +1 -1
  71. package/lib/typescript/module/tabs/Tabs.d.ts.map +1 -1
  72. package/lib/typescript/module/toast/Toast.d.ts.map +1 -1
  73. package/package.json +1 -1
  74. package/src/bottom-sheet/index.web.tsx +1 -1
  75. package/src/context-menu/index.web.tsx +3 -4
  76. package/src/dialog/Dialog.tsx +10 -7
  77. package/src/dialog/Dialog.web.tsx +12 -12
  78. package/src/dialog/DialogHeader.tsx +93 -17
  79. package/src/menu/index.web.tsx +3 -4
  80. package/src/popover/index.web.tsx +5 -4
  81. package/src/select/index.web.tsx +3 -4
  82. package/src/slider/Slider.tsx +3 -5
  83. package/src/switch/Switch.tsx +4 -5
  84. package/src/tabs/Tabs.tsx +3 -5
  85. package/src/toast/Toast.tsx +3 -5
@@ -21,6 +21,7 @@ import { RemoveScrollBar } from 'react-remove-scroll-bar';
21
21
 
22
22
  import { Portal } from '../portal/index.web';
23
23
  import { createOverlayZIndex } from '../styles/z-index';
24
+ import { bloomShadowStyle } from '../design-tokens/shadows';
24
25
  import { useTheme } from '../theme/use-theme';
25
26
  import { Context, useDialogContext, useDialogControl } from './context';
26
27
  import { DialogBody } from './DialogContent';
@@ -29,6 +30,7 @@ import {
29
30
  DIALOG_NAV_BAR_HEIGHT,
30
31
  DialogHeaderProvider,
31
32
  DialogLargeTitle,
33
+ DialogNavBarSpacer,
32
34
  DialogNavHeader,
33
35
  useDialogHeaderController,
34
36
  } from './DialogHeader';
@@ -467,10 +469,9 @@ function DialogPanel({
467
469
  backgroundColor: theme.colors.background,
468
470
  borderWidth: 1,
469
471
  borderColor: theme.isDark ? 'rgba(255,255,255,0.1)' : 'rgba(0,0,0,0.1)',
470
- shadowColor: '#000',
471
- shadowOpacity: theme.isDark ? 0.4 : 0.1,
472
- shadowRadius: 30,
473
- shadowOffset: { width: 0, height: 4 },
472
+ // Design-system overlay elevation (`shadow-m`) as a `boxShadow` — RN-Web
473
+ // deprecated the `shadow*` style props.
474
+ ...bloomShadowStyle('m'),
474
475
  zIndex: surfaceZIndex,
475
476
  },
476
477
  isClosing
@@ -497,7 +498,7 @@ function DialogPanel({
497
498
  />
498
499
  {scrollable === false ? (
499
500
  <DialogMorphContent morph={morphState} style={{ flex: 1, minHeight: 0 }}>
500
- <View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />
501
+ <DialogNavBarSpacer controller={headerController} header={header} />
501
502
  <DialogHeaderProvider controller={headerController}>{body}</DialogHeaderProvider>
502
503
  </DialogMorphContent>
503
504
  ) : (
@@ -671,9 +672,8 @@ function SheetSurface({
671
672
 
672
673
  return (
673
674
  <View
674
- style={[sheetStyles.root, { zIndex: backdropZIndex }, containerStyle]}
675
+ style={[sheetStyles.root, { zIndex: backdropZIndex }, containerStyle, { pointerEvents: 'box-none' }]}
675
676
  {...(containerClassName ? ({ className: containerClassName } as Record<string, string>) : {})}
676
- pointerEvents="box-none"
677
677
  >
678
678
  <Pressable
679
679
  testID={testID ? `${testID}-backdrop` : DIALOG_SHEET_BACKDROP_TESTID}
@@ -703,15 +703,16 @@ function SheetSurface({
703
703
  sheetStyles.panel,
704
704
  {
705
705
  backgroundColor: theme.colors.background,
706
- shadowColor: theme.colors.shadow,
707
706
  zIndex: surfaceZIndex,
707
+ pointerEvents: 'auto',
708
+ // Design-system overlay elevation (`shadow-m`) as a `boxShadow`.
709
+ ...bloomShadowStyle('m'),
708
710
  },
709
711
  panelGeometry,
710
712
  panelTransition,
711
713
  panelStyle,
712
714
  style,
713
715
  ]}
714
- pointerEvents="auto"
715
716
  >
716
717
  {header ? (
717
718
  // Nav-header mode on a side drawer: a static titled bar (the drawer
@@ -859,9 +860,8 @@ const sheetStyles = {
859
860
  panel: {
860
861
  position: 'absolute',
861
862
  overflow: 'hidden',
862
- shadowOpacity: 0.18,
863
- shadowRadius: 24,
864
- shadowOffset: { width: 0, height: 8 },
863
+ // Elevation is applied at the usage site via `bloomShadowStyle('m')`
864
+ // (`boxShadow` on web); the deprecated `shadow*` props were removed.
865
865
  } as ViewStyle,
866
866
  };
867
867
 
@@ -106,6 +106,62 @@ interface HeaderStore {
106
106
  subscribe: (listener: () => void) => () => void;
107
107
  }
108
108
 
109
+ type PrimaryAction = NonNullable<DialogHeaderConfig['primaryAction']>;
110
+ type HeaderAction = NonNullable<DialogHeaderConfig['actions']>[number];
111
+ type HeaderSearch = NonNullable<DialogHeaderConfig['search']>;
112
+ type HeaderSegments = NonNullable<DialogHeaderConfig['segments']>;
113
+ type HeaderProgress = NonNullable<DialogHeaderConfig['progress']>;
114
+
115
+ function primaryActionEqual(a: PrimaryAction | undefined, b: PrimaryAction | undefined): boolean {
116
+ if (a === b) return true;
117
+ if (!a || !b) return false;
118
+ // `onPress` is a fresh closure each render — its identity never changes what the
119
+ // button renders, so compare only the render-affecting scalars.
120
+ return a.label === b.label && !!a.disabled === !!b.disabled && !!a.loading === !!b.loading;
121
+ }
122
+
123
+ function actionsEqual(a: HeaderAction[] | undefined, b: HeaderAction[] | undefined): boolean {
124
+ if (a === b) return true;
125
+ if (!a || !b || a.length !== b.length) return false;
126
+ for (let i = 0; i < a.length; i += 1) {
127
+ const x = a[i];
128
+ const y = b[i];
129
+ if (!x || !y) return false;
130
+ // `icon` is a memoized node (compared by identity); `onPress` is ignored.
131
+ if (
132
+ x.accessibilityLabel !== y.accessibilityLabel ||
133
+ !!x.disabled !== !!y.disabled ||
134
+ x.icon !== y.icon
135
+ ) {
136
+ return false;
137
+ }
138
+ }
139
+ return true;
140
+ }
141
+
142
+ function searchEqual(a: HeaderSearch | undefined, b: HeaderSearch | undefined): boolean {
143
+ if (a === b) return true;
144
+ if (!a || !b) return false;
145
+ return a.value === b.value && a.placeholder === b.placeholder;
146
+ }
147
+
148
+ function segmentsEqual(a: HeaderSegments | undefined, b: HeaderSegments | undefined): boolean {
149
+ if (a === b) return true;
150
+ if (!a || !b || a.value !== b.value || a.items.length !== b.items.length) return false;
151
+ for (let i = 0; i < a.items.length; i += 1) {
152
+ const ai = a.items[i];
153
+ const bi = b.items[i];
154
+ if (!ai || !bi || ai.key !== bi.key || ai.label !== bi.label) return false;
155
+ }
156
+ return true;
157
+ }
158
+
159
+ function progressEqual(a: HeaderProgress | undefined, b: HeaderProgress | undefined): boolean {
160
+ if (a === b) return true;
161
+ if (!a || !b) return false;
162
+ return a.step === b.step && a.total === b.total;
163
+ }
164
+
109
165
  function configsEqual(a: DialogHeaderConfig | null, b: DialogHeaderConfig | null): boolean {
110
166
  if (a === b) return true;
111
167
  if (!a || !b) return false;
@@ -116,16 +172,20 @@ function configsEqual(a: DialogHeaderConfig | null, b: DialogHeaderConfig | null
116
172
  a.largeTitle === b.largeTitle &&
117
173
  a.left === b.left &&
118
174
  a.right === b.right &&
119
- a.onBack === b.onBack &&
175
+ // `onBack` legitimately gets a fresh closure each render; its identity never
176
+ // changes what the header renders, so compare by PRESENCE, never identity —
177
+ // treating a new ref as "changed" is exactly what drives the update loop.
178
+ !!a.onBack === !!b.onBack &&
120
179
  a.showClose === b.showClose &&
121
- // Rich fields — object fields compared by identity (callers memoize them);
122
- // `tone` is a plain value.
123
- a.primaryAction === b.primaryAction &&
124
- a.actions === b.actions &&
125
- a.search === b.search &&
126
- a.segments === b.segments &&
180
+ // Rich fields — compared by VALUE (render-affecting scalars only). A caller
181
+ // passing an inline `primaryAction`/`actions`/… object (a fresh reference
182
+ // every render) must NOT force a store update: that is React error #185.
127
183
  a.tone === b.tone &&
128
- a.progress === b.progress
184
+ primaryActionEqual(a.primaryAction, b.primaryAction) &&
185
+ actionsEqual(a.actions, b.actions) &&
186
+ searchEqual(a.search, b.search) &&
187
+ segmentsEqual(a.segments, b.segments) &&
188
+ progressEqual(a.progress, b.progress)
129
189
  );
130
190
  }
131
191
 
@@ -474,28 +534,23 @@ export const DialogNavHeader = memo(function DialogNavHeader({
474
534
  ) : null);
475
535
 
476
536
  return (
477
- <View
478
- pointerEvents="box-none"
479
- style={[styles.overlay, style]}
480
- >
537
+ <View style={[styles.overlay, style, { pointerEvents: 'box-none' }]}>
481
538
  {/* Opaque (surface bg) at the top → transparent at the bottom, so scrolled
482
539
  content fades out under the bar. `onImage` swaps to a dark scrim so the
483
540
  chrome reads over media. Pure NativeWind — no SVG dependency. */}
484
541
  <View
485
- pointerEvents="none"
486
542
  className={
487
543
  onImage
488
544
  ? 'bg-gradient-to-b from-black/60 to-transparent'
489
545
  : 'bg-gradient-to-b from-bg to-transparent'
490
546
  }
491
- style={[StyleSheet.absoluteFill, { height: DIALOG_HEADER_OVERLAY_HEIGHT }]}
547
+ style={[StyleSheet.absoluteFill, { height: DIALOG_HEADER_OVERLAY_HEIGHT, pointerEvents: 'none' }]}
492
548
  />
493
- <View pointerEvents="box-none" style={styles.navRow}>
549
+ <View style={[styles.navRow, { pointerEvents: 'box-none' }]}>
494
550
  <View style={styles.side}>{left}</View>
495
551
  <Animated.View
496
- pointerEvents="none"
497
552
  testID="dialog-nav-title"
498
- style={[styles.centerTitle, titleStyle]}
553
+ style={[styles.centerTitle, titleStyle, { pointerEvents: 'none' }]}
499
554
  >
500
555
  {config.titleContent ?? null}
501
556
  {!config.titleContent && config.title ? (
@@ -624,6 +679,27 @@ export const DialogLargeTitle = memo(function DialogLargeTitle({
624
679
  );
625
680
  });
626
681
 
682
+ /**
683
+ * The nav-bar-height content inset for a header-mode surface whose content owns
684
+ * its own scroll (`scrollable: false`) — the account dialog or an own-scroller
685
+ * like the media picker. Under `tone: 'onImage'` it COLLAPSES to nothing so the
686
+ * media (photo grid / banner) slides UP under the floating translucent nav bar,
687
+ * edge-to-edge (Apple/Material immersive photo grid); otherwise it insets the
688
+ * content below the bar. Reads the merged (base + runtime-override) tone, since
689
+ * `tone` is contributed by the mounted screen through the override store.
690
+ */
691
+ export const DialogNavBarSpacer = memo(function DialogNavBarSpacer({
692
+ controller,
693
+ header,
694
+ }: {
695
+ controller: DialogHeaderController;
696
+ header: DialogHeaderConfig;
697
+ }) {
698
+ const config = useMergedHeaderConfig(header, controller.store);
699
+ if (config.tone === 'onImage') return null;
700
+ return <View style={{ height: DIALOG_NAV_BAR_HEIGHT }} />;
701
+ });
702
+
627
703
  const styles = StyleSheet.create({
628
704
  overlay: {
629
705
  position: 'absolute',
@@ -17,6 +17,7 @@ import { Text } from '../typography';
17
17
  import type { DialogControlProps } from '../dialog/types';
18
18
  import { Portal } from '../portal/index.web';
19
19
  import { createDropdownZIndex } from '../styles/z-index';
20
+ import { bloomShadowStyle } from '../design-tokens/shadows';
20
21
  import {
21
22
  MenuContext,
22
23
  ItemContext,
@@ -255,7 +256,7 @@ export function MenuContent({
255
256
  ? theme.colors.backgroundSecondary
256
257
  : theme.colors.background,
257
258
  borderColor: theme.colors.borderLight,
258
- shadowColor: theme.colors.shadow,
259
+ ...bloomShadowStyle('m'),
259
260
  },
260
261
  style,
261
262
  styles.portaledDropdown,
@@ -393,9 +394,7 @@ const styles = StyleSheet.create({
393
394
  borderWidth: 1,
394
395
  overflow: 'hidden',
395
396
  zIndex: menuZIndex.surface,
396
- shadowOpacity: 0.15,
397
- shadowRadius: 12,
398
- shadowOffset: { width: 0, height: 4 },
397
+ // Overlay elevation applied at the usage site via `bloomShadowStyle('m')`.
399
398
  minWidth: 180,
400
399
  },
401
400
  portaledDropdown: {
@@ -12,6 +12,7 @@ import { Pressable, StyleSheet, View } from 'react-native';
12
12
  import { useTheme } from '../theme/use-theme';
13
13
  import { Portal } from '../portal/index.web';
14
14
  import { createOverlayZIndex } from '../styles/z-index';
15
+ import { bloomShadowStyle } from '../design-tokens/shadows';
15
16
  import { PopoverContext, usePopoverContext } from './context';
16
17
  import type {
17
18
  PopoverContentProps,
@@ -288,7 +289,9 @@ export function PopoverContent({
288
289
  ? theme.colors.backgroundSecondary
289
290
  : theme.colors.background,
290
291
  borderColor: theme.colors.borderLight,
291
- shadowColor: theme.colors.shadow,
292
+ // Design-system overlay elevation (`shadow-m`) as a `boxShadow` —
293
+ // RN-Web deprecated the `shadow*` style props.
294
+ ...bloomShadowStyle('m'),
292
295
  // Hide until measured to avoid a one-frame jump.
293
296
  opacity: panelSize ? 1 : 0,
294
297
  },
@@ -320,9 +323,7 @@ const styles = StyleSheet.create({
320
323
  borderWidth: 1,
321
324
  paddingVertical: 4,
322
325
  overflow: 'hidden',
323
- shadowOpacity: 0.15,
324
- shadowRadius: 12,
325
- shadowOffset: { width: 0, height: 4 },
326
+ // Elevation applied at the usage site via `bloomShadowStyle('m')` (boxShadow).
326
327
  pointerEvents: 'auto',
327
328
  },
328
329
  });
@@ -5,6 +5,7 @@ import { useTheme } from '../theme/use-theme';
5
5
  import { Text } from '../typography';
6
6
  import { Portal } from '../portal/index.web';
7
7
  import { createOverlayZIndex } from '../styles/z-index';
8
+ import { bloomShadowStyle } from '../design-tokens/shadows';
8
9
  import { RadioIndicator } from '../radio-indicator';
9
10
  import { useInteractionState } from '../hooks/useInteractionState';
10
11
  import {
@@ -201,7 +202,7 @@ export function SelectContent<T>({
201
202
  ? theme.colors.backgroundSecondary
202
203
  : theme.colors.background,
203
204
  borderColor: theme.colors.borderLight,
204
- shadowColor: theme.colors.shadow,
205
+ ...bloomShadowStyle('m'),
205
206
  },
206
207
  ]}
207
208
  >
@@ -348,9 +349,7 @@ const styles = StyleSheet.create({
348
349
  overflow: 'hidden',
349
350
  padding: 4,
350
351
  minWidth: 180,
351
- shadowOpacity: 0.15,
352
- shadowRadius: 12,
353
- shadowOffset: { width: 0, height: 4 },
352
+ // Overlay elevation applied at the usage site via `bloomShadowStyle('m')`.
354
353
  // Opt back in from the Portal root's `pointer-events: none`.
355
354
  pointerEvents: 'auto',
356
355
  },
@@ -12,6 +12,7 @@ import {
12
12
 
13
13
  import { useTheme } from '../theme/use-theme';
14
14
  import { animation } from '../styles/tokens';
15
+ import { bloomShadowStyle } from '../design-tokens/shadows';
15
16
  import type { SliderProps } from './types';
16
17
 
17
18
  function clamp(v: number, min: number, max: number): number {
@@ -236,7 +237,6 @@ const SliderComponent = function Slider({
236
237
  backgroundColor: thumbColor,
237
238
  left: thumbLeft,
238
239
  transform: [{ scale: thumbScale }],
239
- shadowColor: theme.colors.shadow,
240
240
  },
241
241
  ]}
242
242
  />
@@ -261,10 +261,8 @@ const styles = StyleSheet.create({
261
261
  thumb: {
262
262
  position: 'absolute',
263
263
  top: '50%',
264
- shadowOffset: { width: 0, height: 1 },
265
- shadowOpacity: 0.2,
266
- shadowRadius: 2,
267
- elevation: 2,
264
+ // Subtle raise (`shadow-s`) — `boxShadow` on web, RN shadow/elevation on native.
265
+ ...bloomShadowStyle('s'),
268
266
  },
269
267
  disabled: {
270
268
  opacity: 0.5,
@@ -3,6 +3,7 @@ import { Pressable, Animated, StyleSheet } from 'react-native';
3
3
 
4
4
  import { useTheme } from '../theme/use-theme';
5
5
  import { animation } from '../styles/tokens';
6
+ import { bloomShadowStyle } from '../design-tokens/shadows';
6
7
  import type { SwitchProps } from './types';
7
8
 
8
9
  const TRACK = { default: { w: 44, h: 26 }, sm: { w: 36, h: 22 } } as const;
@@ -128,10 +129,8 @@ const styles = StyleSheet.create({
128
129
  },
129
130
  thumb: {
130
131
  backgroundColor: '#fff',
131
- shadowColor: '#000',
132
- shadowOffset: { width: 0, height: 2 },
133
- shadowOpacity: 0.15,
134
- shadowRadius: 3,
135
- elevation: 3,
132
+ // Subtle raise (`shadow-s`) — `boxShadow` on web (RN-Web deprecated `shadow*`),
133
+ // RN elevation/shadow on native.
134
+ ...bloomShadowStyle('s'),
136
135
  },
137
136
  });
package/src/tabs/Tabs.tsx CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  import { useTheme } from '../theme/use-theme';
23
23
  import { usePressAnimation } from '../hooks/usePressAnimation';
24
24
  import { borderRadius, space } from '../styles/tokens';
25
+ import { bloomShadowStyle } from '../design-tokens/shadows';
25
26
  import type { TabsProps, TabsTriggerProps, TabsContentProps, TabsVariant } from './types';
26
27
 
27
28
  type TriggerLayout = Pick<LayoutRectangle, 'x' | 'width'>;
@@ -226,11 +227,8 @@ const TabComponent: React.FC<TabsTriggerProps> = ({
226
227
  if (isSelected) {
227
228
  base.backgroundColor = theme.colors.card;
228
229
  base.borderRadius = borderRadius.xs + 2;
229
- base.shadowColor = theme.colors.shadow;
230
- base.shadowOffset = { width: 0, height: 1 };
231
- base.shadowOpacity = 0.1;
232
- base.shadowRadius = 2;
233
- base.elevation = 1;
230
+ // Subtle raise (`shadow-s`) — `boxShadow` on web, RN shadow/elevation on native.
231
+ Object.assign(base, bloomShadowStyle('s'));
234
232
  }
235
233
  break;
236
234
  case 'outlined':
@@ -9,6 +9,7 @@ import {
9
9
 
10
10
  import { useTheme } from '../theme/use-theme';
11
11
  import { useInteractionState } from '../hooks/useInteractionState';
12
+ import { bloomShadowStyle } from '../design-tokens/shadows';
12
13
  import type { ThemeColors } from '../theme/types';
13
14
  import { dismiss } from './sonner';
14
15
  import type { ToastType } from './types';
@@ -242,11 +243,8 @@ const styles = StyleSheet.create({
242
243
  flexDirection: 'row',
243
244
  gap: 8,
244
245
  paddingVertical: 14,
245
- shadowColor: '#000',
246
- shadowOffset: { width: 0, height: 1 },
247
- shadowOpacity: 0.05,
248
- shadowRadius: 2,
249
- elevation: 2,
246
+ // Subtle raise (`shadow-s`) — `boxShadow` on web, RN shadow/elevation on native.
247
+ ...bloomShadowStyle('s'),
250
248
  },
251
249
  textContainer: {
252
250
  flex: 1,