@oxy.so/bloom 4.14.1 → 4.16.0

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 (152) hide show
  1. package/docs/app-shell.mdx +74 -6
  2. package/docs/bottom-bar.mdx +9 -1
  3. package/docs/hooks.mdx +23 -0
  4. package/docs/sidebar.mdx +15 -2
  5. package/docs/web-search.mdx +1 -1
  6. package/lib/commonjs/app-shell/AppShell.js +3 -1
  7. package/lib/commonjs/app-shell/AppShell.js.map +1 -1
  8. package/lib/commonjs/app-shell/AppShellEngine.js +54 -28
  9. package/lib/commonjs/app-shell/AppShellEngine.js.map +1 -1
  10. package/lib/commonjs/app-shell/AppShellHeader.js +4 -3
  11. package/lib/commonjs/app-shell/AppShellHeader.js.map +1 -1
  12. package/lib/commonjs/app-shell/NotificationBell.js +8 -4
  13. package/lib/commonjs/app-shell/NotificationBell.js.map +1 -1
  14. package/lib/commonjs/app-shell/ProOfferCard.js +19 -11
  15. package/lib/commonjs/app-shell/ProOfferCard.js.map +1 -1
  16. package/lib/commonjs/bottom-bar/BottomBarBase.js +9 -2
  17. package/lib/commonjs/bottom-bar/BottomBarBase.js.map +1 -1
  18. package/lib/commonjs/hooks/index.js +7 -0
  19. package/lib/commonjs/hooks/index.js.map +1 -1
  20. package/lib/commonjs/hooks/use-is-rtl.js +104 -0
  21. package/lib/commonjs/hooks/use-is-rtl.js.map +1 -0
  22. package/lib/commonjs/sidebar/Sidebar.js +41 -22
  23. package/lib/commonjs/sidebar/Sidebar.js.map +1 -1
  24. package/lib/commonjs/sidebar/SidebarFolder.js +11 -5
  25. package/lib/commonjs/sidebar/SidebarFolder.js.map +1 -1
  26. package/lib/commonjs/sidebar/SidebarLogoView.js +2 -2
  27. package/lib/commonjs/sidebar/SidebarLogoView.js.map +1 -1
  28. package/lib/commonjs/sidebar/SidebarModeSwitcher.js +1 -1
  29. package/lib/commonjs/sidebar/SidebarModeSwitcher.js.map +1 -1
  30. package/lib/commonjs/sidebar/SidebarPlanCard.js +3 -3
  31. package/lib/commonjs/sidebar/SidebarPlanCard.js.map +1 -1
  32. package/lib/commonjs/sidebar/SidebarRail.js +7 -1
  33. package/lib/commonjs/sidebar/SidebarRail.js.map +1 -1
  34. package/lib/commonjs/sidebar/SidebarRailItem.js +2 -2
  35. package/lib/commonjs/sidebar/SidebarRailItem.js.map +1 -1
  36. package/lib/commonjs/sidebar/SidebarTeamMenu.js +12 -7
  37. package/lib/commonjs/sidebar/SidebarTeamMenu.js.map +1 -1
  38. package/lib/commonjs/sidebar/SidebarUserMenu.js +9 -5
  39. package/lib/commonjs/sidebar/SidebarUserMenu.js.map +1 -1
  40. package/lib/commonjs/tab-bar/TabBarBase.js +19 -5
  41. package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
  42. package/lib/commonjs/web-search/WebSearch.js +12 -0
  43. package/lib/commonjs/web-search/WebSearch.js.map +1 -1
  44. package/lib/module/app-shell/AppShell.js +3 -1
  45. package/lib/module/app-shell/AppShell.js.map +1 -1
  46. package/lib/module/app-shell/AppShellEngine.js +54 -28
  47. package/lib/module/app-shell/AppShellEngine.js.map +1 -1
  48. package/lib/module/app-shell/AppShellHeader.js +4 -3
  49. package/lib/module/app-shell/AppShellHeader.js.map +1 -1
  50. package/lib/module/app-shell/NotificationBell.js +8 -4
  51. package/lib/module/app-shell/NotificationBell.js.map +1 -1
  52. package/lib/module/app-shell/ProOfferCard.js +19 -11
  53. package/lib/module/app-shell/ProOfferCard.js.map +1 -1
  54. package/lib/module/bottom-bar/BottomBarBase.js +9 -2
  55. package/lib/module/bottom-bar/BottomBarBase.js.map +1 -1
  56. package/lib/module/hooks/index.js +1 -0
  57. package/lib/module/hooks/index.js.map +1 -1
  58. package/lib/module/hooks/use-is-rtl.js +97 -0
  59. package/lib/module/hooks/use-is-rtl.js.map +1 -0
  60. package/lib/module/sidebar/Sidebar.js +41 -22
  61. package/lib/module/sidebar/Sidebar.js.map +1 -1
  62. package/lib/module/sidebar/SidebarFolder.js +11 -5
  63. package/lib/module/sidebar/SidebarFolder.js.map +1 -1
  64. package/lib/module/sidebar/SidebarLogoView.js +2 -2
  65. package/lib/module/sidebar/SidebarLogoView.js.map +1 -1
  66. package/lib/module/sidebar/SidebarModeSwitcher.js +1 -1
  67. package/lib/module/sidebar/SidebarModeSwitcher.js.map +1 -1
  68. package/lib/module/sidebar/SidebarPlanCard.js +3 -3
  69. package/lib/module/sidebar/SidebarPlanCard.js.map +1 -1
  70. package/lib/module/sidebar/SidebarRail.js +7 -1
  71. package/lib/module/sidebar/SidebarRail.js.map +1 -1
  72. package/lib/module/sidebar/SidebarRailItem.js +2 -2
  73. package/lib/module/sidebar/SidebarRailItem.js.map +1 -1
  74. package/lib/module/sidebar/SidebarTeamMenu.js +12 -7
  75. package/lib/module/sidebar/SidebarTeamMenu.js.map +1 -1
  76. package/lib/module/sidebar/SidebarUserMenu.js +9 -5
  77. package/lib/module/sidebar/SidebarUserMenu.js.map +1 -1
  78. package/lib/module/tab-bar/TabBarBase.js +19 -5
  79. package/lib/module/tab-bar/TabBarBase.js.map +1 -1
  80. package/lib/module/web-search/WebSearch.js +13 -1
  81. package/lib/module/web-search/WebSearch.js.map +1 -1
  82. package/lib/typescript/commonjs/app-shell/AppShellEngine.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/app-shell/AppShellHeader.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/app-shell/NotificationBell.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/app-shell/ProOfferCard.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/app-shell/types.d.ts +16 -5
  87. package/lib/typescript/commonjs/app-shell/types.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/bottom-bar/BottomBarBase.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/hooks/index.d.ts +1 -0
  90. package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/hooks/use-is-rtl.d.ts +53 -0
  92. package/lib/typescript/commonjs/hooks/use-is-rtl.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/icons/shared.d.ts +10 -10
  94. package/lib/typescript/commonjs/sidebar/Sidebar.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/sidebar/SidebarFolder.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/sidebar/SidebarRail.d.ts +1 -1
  97. package/lib/typescript/commonjs/sidebar/SidebarRail.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/sidebar/SidebarTeamMenu.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/sidebar/SidebarUserMenu.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/sidebar/types.d.ts +24 -3
  101. package/lib/typescript/commonjs/sidebar/types.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/web-search/WebSearch.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/web-search/types.d.ts +6 -0
  105. package/lib/typescript/commonjs/web-search/types.d.ts.map +1 -1
  106. package/lib/typescript/module/app-shell/AppShellEngine.d.ts.map +1 -1
  107. package/lib/typescript/module/app-shell/AppShellHeader.d.ts.map +1 -1
  108. package/lib/typescript/module/app-shell/NotificationBell.d.ts.map +1 -1
  109. package/lib/typescript/module/app-shell/ProOfferCard.d.ts.map +1 -1
  110. package/lib/typescript/module/app-shell/types.d.ts +16 -5
  111. package/lib/typescript/module/app-shell/types.d.ts.map +1 -1
  112. package/lib/typescript/module/bottom-bar/BottomBarBase.d.ts.map +1 -1
  113. package/lib/typescript/module/hooks/index.d.ts +1 -0
  114. package/lib/typescript/module/hooks/index.d.ts.map +1 -1
  115. package/lib/typescript/module/hooks/use-is-rtl.d.ts +53 -0
  116. package/lib/typescript/module/hooks/use-is-rtl.d.ts.map +1 -0
  117. package/lib/typescript/module/icons/shared.d.ts +10 -10
  118. package/lib/typescript/module/sidebar/Sidebar.d.ts.map +1 -1
  119. package/lib/typescript/module/sidebar/SidebarFolder.d.ts.map +1 -1
  120. package/lib/typescript/module/sidebar/SidebarRail.d.ts +1 -1
  121. package/lib/typescript/module/sidebar/SidebarRail.d.ts.map +1 -1
  122. package/lib/typescript/module/sidebar/SidebarTeamMenu.d.ts.map +1 -1
  123. package/lib/typescript/module/sidebar/SidebarUserMenu.d.ts.map +1 -1
  124. package/lib/typescript/module/sidebar/types.d.ts +24 -3
  125. package/lib/typescript/module/sidebar/types.d.ts.map +1 -1
  126. package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
  127. package/lib/typescript/module/web-search/WebSearch.d.ts.map +1 -1
  128. package/lib/typescript/module/web-search/types.d.ts +6 -0
  129. package/lib/typescript/module/web-search/types.d.ts.map +1 -1
  130. package/package.json +1 -1
  131. package/src/app-shell/AppShell.tsx +1 -1
  132. package/src/app-shell/AppShellEngine.tsx +45 -24
  133. package/src/app-shell/AppShellHeader.tsx +4 -3
  134. package/src/app-shell/NotificationBell.tsx +8 -4
  135. package/src/app-shell/ProOfferCard.tsx +18 -10
  136. package/src/app-shell/types.ts +16 -5
  137. package/src/bottom-bar/BottomBarBase.tsx +9 -3
  138. package/src/hooks/index.ts +1 -0
  139. package/src/hooks/use-is-rtl.ts +102 -0
  140. package/src/sidebar/Sidebar.tsx +41 -21
  141. package/src/sidebar/SidebarFolder.tsx +7 -4
  142. package/src/sidebar/SidebarLogoView.tsx +2 -2
  143. package/src/sidebar/SidebarModeSwitcher.tsx +1 -1
  144. package/src/sidebar/SidebarPlanCard.tsx +3 -3
  145. package/src/sidebar/SidebarRail.tsx +7 -1
  146. package/src/sidebar/SidebarRailItem.tsx +2 -2
  147. package/src/sidebar/SidebarTeamMenu.tsx +10 -6
  148. package/src/sidebar/SidebarUserMenu.tsx +8 -5
  149. package/src/sidebar/types.ts +25 -3
  150. package/src/tab-bar/TabBarBase.tsx +19 -5
  151. package/src/web-search/WebSearch.tsx +9 -0
  152. package/src/web-search/types.ts +6 -0
@@ -13,6 +13,7 @@ import Animated, {
13
13
  import type { ContentPanelProps } from '../content-panel/types';
14
14
  import type { BloomColorScopeProps } from '../theme/color-scope/ColorScope';
15
15
  import { useControllableState } from '../hooks/use-controllable-state';
16
+ import { useDirectionProps, useIsRtl } from '../hooks/use-is-rtl';
16
17
  import { Backdrop, OverlayRoot } from '../overlay';
17
18
  import { Portal } from '../portal';
18
19
  import { Sidebar } from '../sidebar';
@@ -39,11 +40,11 @@ import { useShellWidth } from './use-shell-width';
39
40
  * nav the sidebar in flow from `navFrom` (`lg` for the panel, `sm` for
40
41
  * a rail, `sm` whenever `navExpandedFrom` swaps the two); below it
41
42
  * the same props drive the drawer. `overlay` pads the whole frame
42
- * by `gutter`, `reveal` sits the rail 12 from the top-left
43
+ * by `gutter`, `reveal` sits the rail 12 from the top-start corner
43
44
  * page `dashboard` one fluid column capped at `contentMaxWidth`;
44
45
  * `feed`/`focus` a `contentWidth` reading column centred in what
45
46
  * the nav leaves; `split` a column of header over three panes
46
- * aside optional right column, `asideWidth` (320) wide from `asideFrom`
47
+ * aside optional end-side column (right in LTR), `asideWidth` (320) wide from `asideFrom`
47
48
  * (xl), pinned like the rail; below it stacked after the content
48
49
  * slots `topBar` above the columns, `bottomBar` pinned to the bottom
49
50
  * with the safe-area inset, `floatingAction` above that bar —
@@ -51,7 +52,7 @@ import { useShellWidth } from './use-shell-width';
51
52
  * below nav the header grows a hamburger (also with no title — see
52
53
  * `useAppShell` / `AppShellMenuButton`). `overlay` opens the rail
53
54
  * as a drawer (12 inset) over a 40% black backdrop; `reveal`
54
- * slides the page 272 right and rounds it to 32 while the rail,
55
+ * slides the page 272 toward the end edge and rounds it to 32 while the rail,
55
56
  * flat beneath it, scales up from 0.94 and fades in (325ms,
56
57
  * cubic-bezier(0.42, 0, 0.58, 1)); pressing the page (a 10% black
57
58
  * / 5% white veil) closes it
@@ -148,6 +149,8 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
148
149
  overlay,
149
150
  drawerOpen,
150
151
  onDrawerOpenChange,
152
+ drawerOpenLabel = 'Open navigation',
153
+ drawerCloseLabel = 'Close navigation',
151
154
  scroll = 'document',
152
155
  style,
153
156
  testID,
@@ -204,6 +207,15 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
204
207
  const asideStacked = hasAside && !asideBeside && asideCollapse === 'stack' && !canvas;
205
208
 
206
209
  const reducedMotion = useReducedMotion();
210
+ // Insets here are logical; a `translateX` sign and a `transformOrigin` are
211
+ // not, so the reveal reads the direction once here.
212
+ const rtl = useIsRtl();
213
+ const dir = rtl ? -1 : 1;
214
+ const railOrigin = rtl ? 'right center' : 'left center';
215
+ // react-native-web resolves logical insets against its OWN direction context
216
+ // (a `dir` prop), not `<html dir>` — every root and the portaled drawer hand
217
+ // it on.
218
+ const dirProps = useDirectionProps();
207
219
  const [open, setOpen] = useControllableState<boolean>({
208
220
  value: drawerOpen,
209
221
  defaultValue: false,
@@ -337,6 +349,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
337
349
  actions={actions}
338
350
  menuOpen={isOpen}
339
351
  showMenu={menuInHeader}
352
+ menuLabel={drawerOpenLabel}
340
353
  onMenuPress={menuInHeader ? shell.toggleDrawer : undefined}
341
354
  testID={testID ? `${testID}-header` : undefined}
342
355
  />
@@ -387,8 +400,8 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
387
400
  reveal.value = reducedMotion ? target : withTiming(target, { duration: REVEAL_MS, easing: REVEAL_EASE });
388
401
  }, [isOpen, drawerStyle, reducedMotion, reveal]);
389
402
  const feedPageStyle = useAnimatedStyle(() => ({
390
- transform: reveal.value === 0 ? [] : [{ translateX: (REVEAL_OFFSET + 12) * reveal.value }],
391
- }), [reveal]);
403
+ transform: reveal.value === 0 ? [] : [{ translateX: dir * (REVEAL_OFFSET + 12) * reveal.value }],
404
+ }), [reveal, dir]);
392
405
 
393
406
  const feedChromeTransition: WebCssStyle = Platform.OS === 'web'
394
407
  ? { '--bloom-panel-inset-duration': reducedMotion ? '0ms' : `${REVEAL_MS}ms` } : {};
@@ -407,8 +420,8 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
407
420
  [reveal],
408
421
  );
409
422
  const pageStyle = useAnimatedStyle(
410
- () => ({ transform: reveal.value === 0 ? [] : [{ translateX: REVEAL_OFFSET * reveal.value }], borderRadius: 32 * reveal.value }),
411
- [reveal],
423
+ () => ({ transform: reveal.value === 0 ? [] : [{ translateX: dir * REVEAL_OFFSET * reveal.value }], borderRadius: 32 * reveal.value }),
424
+ [reveal, dir],
412
425
  );
413
426
  const veilStyle = useAnimatedStyle(() => ({ opacity: reveal.value }), [reveal]);
414
427
 
@@ -416,6 +429,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
416
429
  return (
417
430
  <AppShellProvider value={shell}>
418
431
  <View
432
+ {...dirProps}
419
433
  testID={testID}
420
434
  onLayout={onLayout}
421
435
  style={[frame, doc ? { overflow: WEB_OVERFLOW_CLIP } : null, { flexDirection: 'row', backgroundColor: background }, style]}
@@ -425,7 +439,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
425
439
  style={[
426
440
  flowSidebar.surface === 'docked'
427
441
  ? { flexShrink: 0 }
428
- : { paddingTop: 12, paddingBottom: 12, paddingLeft: 12, flexShrink: 0 },
442
+ : { paddingTop: 12, paddingBottom: 12, paddingInlineStart: 12, flexShrink: 0 },
429
443
  doc ? stickyRail(0) : null,
430
444
  ]}
431
445
  >
@@ -438,9 +452,9 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
438
452
  pointerEvents={isOpen ? 'auto' : 'none'}
439
453
  // Fixed on web in document mode: the rail waits beneath the VIEWPORT,
440
454
  // wherever the page is scrolled to.
441
- style={{ position: doc ? WEB_POSITION_FIXED : 'absolute', top: 0, bottom: 0, left: 0, width: 272, paddingTop: 12, paddingBottom: 12, paddingLeft: 6 }}
455
+ style={{ position: doc ? WEB_POSITION_FIXED : 'absolute', top: 0, bottom: 0, insetInlineStart: 0, width: 272, paddingTop: 12, paddingBottom: 12, paddingInlineStart: 6 }}
442
456
  >
443
- <Animated.View style={[{ height: '100%', width: 260, transformOrigin: 'left center' }, railStyle]}>
457
+ <Animated.View style={[{ height: '100%', width: 260, transformOrigin: railOrigin }, railStyle]}>
444
458
  <Sidebar {...drawerSidebar} mobile surface="plain" onClose={() => setOpen(false)} />
445
459
  </Animated.View>
446
460
  </View>
@@ -473,7 +487,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
473
487
  >
474
488
  {column}
475
489
  </Scroller>
476
- {asideColumn({ paddingTop: small ? 24 : 12, paddingBottom: 12, paddingRight: 12 })}
490
+ {asideColumn({ paddingTop: small ? 24 : 12, paddingBottom: 12, paddingInlineEnd: 12 })}
477
491
  {!wide ? (
478
492
  <Animated.View
479
493
  pointerEvents={isOpen ? 'auto' : 'none'}
@@ -481,7 +495,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
481
495
  >
482
496
  <Pressable
483
497
  role="button"
484
- accessibilityLabel="Close navigation"
498
+ accessibilityLabel={drawerCloseLabel}
485
499
  focusable={isOpen}
486
500
  onPress={() => setOpen(false)}
487
501
  style={{
@@ -713,7 +727,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
713
727
  {headerNode}
714
728
  <View style={{ flex: 1, minHeight: 0, minWidth: 0 }}>{children}</View>
715
729
  </View>
716
- {asideColumn(canvas ? { padding: gutter, paddingLeft: 0 } : {})}
730
+ {asideColumn(canvas ? { paddingTop: gutter, paddingBottom: gutter, paddingInlineEnd: gutter } : {})}
717
731
  </>
718
732
  );
719
733
 
@@ -727,7 +741,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
727
741
  : dashboardBody;
728
742
 
729
743
  // A DOCKED nav is flush to the window: it takes the shell's whole height and
730
- // its own hairline is the separator, so the shell gives up its left and
744
+ // its own hairline is the separator, so the shell gives up its start and
731
745
  // vertical padding around that column rather than framing it like a card.
732
746
  const dockedNav = navInFlow && flowSidebar?.surface === 'docked';
733
747
  const navRegion =
@@ -738,7 +752,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
738
752
  </View>
739
753
  ) : canvas && !dockedNav ? (
740
754
  // The canvas row has no padding of its own, so the nav states its.
741
- <View style={{ padding: gutter, paddingRight: 0, flexShrink: 0 }}>
755
+ <View style={{ paddingTop: gutter, paddingBottom: gutter, paddingInlineStart: gutter, flexShrink: 0 }}>
742
756
  <Sidebar {...flowSidebar} />
743
757
  </View>
744
758
  ) : (
@@ -754,11 +768,12 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
754
768
  onPress={() => setOpen(false)}
755
769
  blurIntensity={0}
756
770
  dimOpacity={0.4}
757
- accessibilityLabel="Close navigation"
771
+ accessibilityLabel={drawerCloseLabel}
758
772
  />
759
773
  <View
774
+ {...dirProps}
760
775
  pointerEvents="box-none"
761
- style={{ position: 'absolute', top: 0, bottom: 0, left: 0, flexDirection: 'row', padding: 12 }}
776
+ style={{ position: 'absolute', top: 0, bottom: 0, insetInlineStart: 0, flexDirection: 'row', padding: 12 }}
762
777
  >
763
778
  <View pointerEvents="auto" style={{ height: '100%' }}>
764
779
  <Sidebar {...drawerSidebar} mobile onClose={() => setOpen(false)} />
@@ -774,19 +789,24 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
774
789
  gap: dockedNav || canvas ? 0 : Math.max(0, navigationGap ?? columnGap),
775
790
  // A canvas runs to the window's edge, so the row keeps no padding at all
776
791
  // and each region carries its own (the nav below, the aside in `canvasBody`).
777
- padding: canvas || plainDocumentFrame ? 0 : gutter,
778
- ...(dockedNav ? { paddingLeft: 0, paddingTop: 0, paddingBottom: 0 } : null),
792
+ // A docked nav keeps only the END padding. Spelled in longhands, never the
793
+ // `padding` shorthand plus a logical override: react-native-web expands the
794
+ // shorthand to physical sides, and which of two declarations for one edge
795
+ // wins is then a matter of declaration order rather than intent.
796
+ ...(dockedNav
797
+ ? { paddingTop: 0, paddingBottom: 0, paddingInlineStart: 0, paddingInlineEnd: canvas || plainDocumentFrame ? 0 : gutter }
798
+ : { padding: canvas || plainDocumentFrame ? 0 : gutter }),
779
799
  backgroundColor: background,
780
800
  };
781
801
 
782
802
  if (variant === 'feed' && drawerStyle === 'reveal' && drawerAvailable) {
783
803
  return (
784
804
  <AppShellProvider value={shell}>
785
- <View testID={testID} onLayout={onLayout}
805
+ <View {...dirProps} testID={testID} onLayout={onLayout}
786
806
  style={[frame, { overflow: doc ? WEB_OVERFLOW_CLIP : 'hidden', backgroundColor: background }, style]}>
787
807
  <View aria-hidden={!isOpen} pointerEvents={isOpen ? 'auto' : 'none'}
788
- style={{ position: doc ? WEB_POSITION_FIXED : 'absolute', top: 0, bottom: 0, left: 0, width: 272, paddingTop: 12, paddingBottom: 12, paddingLeft: 6 }}>
789
- <Animated.View style={[{ height: '100%', width: 260, transformOrigin: 'left center' }, railStyle]}>
808
+ style={{ position: doc ? WEB_POSITION_FIXED : 'absolute', top: 0, bottom: 0, insetInlineStart: 0, width: 272, paddingTop: 12, paddingBottom: 12, paddingInlineStart: 6 }}>
809
+ <Animated.View style={[{ height: '100%', width: 260, transformOrigin: railOrigin }, railStyle]}>
790
810
  {drawerSidebar && <Sidebar {...drawerSidebar} mobile surface="plain" onClose={() => setOpen(false)} />}
791
811
  </Animated.View>
792
812
  </View>
@@ -798,7 +818,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
798
818
  </StyledView>
799
819
  <Animated.View pointerEvents={isOpen ? 'auto' : 'none'}
800
820
  style={[{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }, veilStyle]}>
801
- <Pressable role="button" accessibilityLabel="Close navigation" focusable={isOpen}
821
+ <Pressable role="button" accessibilityLabel={drawerCloseLabel} focusable={isOpen}
802
822
  onPress={() => setOpen(false)} testID={testID ? `${testID}-veil` : undefined}
803
823
  style={{ flex: 1 }} />
804
824
  </Animated.View>
@@ -823,6 +843,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
823
843
  return (
824
844
  <AppShellProvider value={shell}>
825
845
  <View
846
+ {...dirProps}
826
847
  testID={testID}
827
848
  onLayout={onLayout}
828
849
  style={[frame, { flexDirection: 'column', backgroundColor: background }, style]}
@@ -842,7 +863,7 @@ const AppShellComponent: React.FC<AppShellEngineProps> = ({
842
863
 
843
864
  return (
844
865
  <AppShellProvider value={shell}>
845
- <View testID={testID} onLayout={onLayout} style={[frame, rowStyle, style]}>
866
+ <View {...dirProps} testID={testID} onLayout={onLayout} style={[frame, rowStyle, style]}>
846
867
  {navRegion}
847
868
  {drawerRegion}
848
869
  {body}
@@ -15,9 +15,9 @@ import type { AppShellHeaderProps } from './types';
15
15
  *
16
16
  * header column, gap 8
17
17
  * title row wraps, items bottom-aligned, space-between, gap 8
18
- * left gap 6: a medium icon button (hamburger, below `lg` only) and the
18
+ * start gap 6: a medium icon button (hamburger, below `lg` only) and the
19
19
  * title — title-2-medium, px4, no wrap, text-primary
20
- * actions wrap, right-aligned, gap 10
20
+ * actions wrap, end-aligned (right in LTR, left in RTL), gap 10
21
21
  */
22
22
  const AppShellHeaderComponent: React.FC<AppShellHeaderProps> = ({
23
23
  title,
@@ -26,6 +26,7 @@ const AppShellHeaderComponent: React.FC<AppShellHeaderProps> = ({
26
26
  onMenuPress,
27
27
  menuOpen = false,
28
28
  showMenu: showMenuProp,
29
+ menuLabel = 'Open navigation',
29
30
  style,
30
31
  testID,
31
32
  }) => {
@@ -52,7 +53,7 @@ const AppShellHeaderComponent: React.FC<AppShellHeaderProps> = ({
52
53
  >
53
54
  <View style={{ minWidth: 0, flexDirection: 'row', alignItems: 'center', gap: 6 }}>
54
55
  {showMenu ? (
55
- <Button size="md" icon={menuOpen ? RiCloseLine : RiMenuLine} accessibilityLabel="Open navigation" aria-expanded={menuOpen} onPress={onMenuPress} testID={testID ? `${testID}-menu` : undefined} appearance="plain" tone="neutral" />
56
+ <Button size="md" icon={menuOpen ? RiCloseLine : RiMenuLine} accessibilityLabel={menuLabel} aria-expanded={menuOpen} onPress={onMenuPress} testID={testID ? `${testID}-menu` : undefined} appearance="plain" tone="neutral" />
56
57
  ) : null}
57
58
  {title != null ? (
58
59
  <Text
@@ -3,6 +3,7 @@ import { View } from 'react-native';
3
3
 
4
4
  import { Button } from '../button';
5
5
  import { useInteractionState } from '../hooks/use-interaction-state';
6
+ import { mirrorAlign, useDirectionProps, useIsRtl } from '../hooks/use-is-rtl';
6
7
  import { RiNotificationLine } from '../icons/remix/RiNotificationLine';
7
8
  import { NotificationCenter } from '../notification-center';
8
9
  import { Popover, PopoverContent, PopoverTrigger } from '../popover';
@@ -16,10 +17,10 @@ import type { NotificationBellProps } from './types';
16
17
  * glyph, and the notification center in a popover under it.
17
18
  *
18
19
  * trigger medium secondary icon button (36), RiNotificationLine 20
19
- * count 16px red-600 disc at top 2 / left 18, 1.5px ring in the button's
20
+ * count 16px red-600 disc at top 2 / inline-start 18, 1.5px ring in the button's
20
21
  * own surface (so it reads punched out), 10/16 bold white; the ring
21
22
  * drops away while the button is hovered or pressed
22
- * popover bottom-end, 8px off, 440 wide, no chrome of its own — the center
23
+ * popover bottom, trailing edge (mirrored under RTL), 8px off, 440 wide, no chrome of its own — the center
23
24
  * (max 430) is the card
24
25
  */
25
26
  const NotificationBellComponent: React.FC<NotificationBellProps> = ({
@@ -33,6 +34,8 @@ const NotificationBellComponent: React.FC<NotificationBellProps> = ({
33
34
  testID,
34
35
  }) => {
35
36
  const theme = useTheme();
37
+ const rtl = useIsRtl();
38
+ const dirProps = useDirectionProps();
36
39
  const [open, setOpen] = useState(false);
37
40
  const { state: hovered, onIn, onOut } = useInteractionState();
38
41
  const unread = unreadCount ?? notifications.filter((item) => item.unread).length;
@@ -48,6 +51,7 @@ const NotificationBellComponent: React.FC<NotificationBellProps> = ({
48
51
  return (
49
52
  <Popover open={open} onOpenChange={setOpen}>
50
53
  <View
54
+ {...dirProps}
51
55
  style={{ position: 'relative', flexDirection: 'row' }}
52
56
  // Hover is read on the wrapper (the `group`).
53
57
  {...({ onPointerEnter: onIn, onPointerLeave: onOut })}
@@ -62,7 +66,7 @@ const NotificationBellComponent: React.FC<NotificationBellProps> = ({
62
66
  style={{
63
67
  position: 'absolute',
64
68
  top: 2,
65
- left: 18,
69
+ insetInlineStart: 18,
66
70
  width: 16,
67
71
  height: 16,
68
72
  borderRadius: borderRadius.full,
@@ -82,7 +86,7 @@ const NotificationBellComponent: React.FC<NotificationBellProps> = ({
82
86
  <PopoverContent
83
87
  label={accessibilityLabel}
84
88
  side="bottom"
85
- align="end"
89
+ align={mirrorAlign('end', 'bottom', rtl)}
86
90
  sideOffset={8}
87
91
  style={{
88
92
  width,
@@ -13,6 +13,7 @@ import Animated, {
13
13
  import Svg, { Defs, LinearGradient, Mask, Rect, Stop } from 'react-native-svg';
14
14
 
15
15
  import { Button, CloseButton } from '../button';
16
+ import { useDirectionProps, useIsRtl } from '../hooks/use-is-rtl';
16
17
  import { BREAKPOINTS } from '../styles/breakpoints';
17
18
  import { WEB_POSITION_FIXED, type WebCssStyle } from '../styles/web-view-style';
18
19
  import { Z_INDEX } from '../styles/z-index';
@@ -22,18 +23,19 @@ import type { ProOfferCardProps } from './types';
22
23
 
23
24
  /**
24
25
  * `ProOfferCard`: the upgrade prompt anchored
25
- * bottom-left of the app.
26
+ * bottom-start of the app (bottom-left in LTR, bottom-right in RTL).
26
27
  *
27
28
  * card 280 wide (full width minus 12 each side under `sm`), 12 from
28
- * the bottom-left, radius 16, 1px border-button-white,
29
+ * the bottom-start corner, radius 16, 1px border-button-white,
29
30
  * background-secondary, p16, gap 12, shadow-waitlist
30
31
  * backdrop a tonal band across the top (card → raised surface →
31
32
  * clear, diagonal, 70%), masked from opaque at 38% to clear
32
33
  * copy mark, then title body-medium / description body-2-regular
33
34
  * secondary, 4 apart
34
35
  * cta full-width button with a white 40% shimmer sweeping across it
35
- * every 2.5s (skewed −12°), hidden under reduced motion
36
- * close xs close button 12 from the top-right
36
+ * every 2.5s (skewed −12°) toward the end edge, hidden under
37
+ * reduced motion
38
+ * close xs close button 12 from the top-end corner
37
39
  * enter fades up 16px over 300ms after a 1.2s delay
38
40
  *
39
41
  * No shader backdrop ships by default; pass your own
@@ -79,6 +81,8 @@ function DefaultBackdrop({ from, via }: { from: string; via: string }) {
79
81
 
80
82
  function Shimmer({ width }: { width: number }) {
81
83
  const reducedMotion = useReducedMotion();
84
+ // The sweep runs start → end, so under RTL both its travel and its lean flip.
85
+ const dir = useIsRtl() ? -1 : 1;
82
86
  const travel = useSharedValue(0);
83
87
  useEffect(() => {
84
88
  if (reducedMotion || width === 0) return;
@@ -90,14 +94,14 @@ function Shimmer({ width }: { width: number }) {
90
94
  }, [reducedMotion, width, travel]);
91
95
  const barWidth = width * 0.45;
92
96
  const style = useAnimatedStyle(
93
- () => ({ transform: [{ translateX: travel.value * barWidth * 3.5 }, { skewX: '-12deg' }] }),
94
- [travel, barWidth],
97
+ () => ({ transform: [{ translateX: dir * travel.value * barWidth * 3.5 }, { skewX: `${dir * -12}deg` }] }),
98
+ [travel, barWidth, dir],
95
99
  );
96
100
  if (reducedMotion || width === 0) return null;
97
101
  return (
98
102
  <Animated.View
99
103
  pointerEvents="none"
100
- style={[{ position: 'absolute', top: 0, bottom: 0, left: -width * 0.55, width: barWidth }, style]}
104
+ style={[{ position: 'absolute', top: 0, bottom: 0, insetInlineStart: -width * 0.55, width: barWidth }, style]}
101
105
  >
102
106
  <Svg width="100%" height="100%">
103
107
  <Defs>
@@ -130,6 +134,9 @@ const ProOfferCardComponent: React.FC<ProOfferCardProps> = ({
130
134
  testID,
131
135
  }) => {
132
136
  const theme = useTheme();
137
+ // Fixed to the viewport, usually outside any shell: it hands react-native-web
138
+ // the document's direction itself so its logical insets resolve against it.
139
+ const dirProps = useDirectionProps();
133
140
  const { width: viewport } = useWindowDimensions();
134
141
  const reducedMotion = useReducedMotion();
135
142
  const [ctaWidth, setCtaWidth] = useState(0);
@@ -160,8 +167,8 @@ const ProOfferCardComponent: React.FC<ProOfferCardProps> = ({
160
167
  ? {
161
168
  position: IS_WEB ? WEB_POSITION_FIXED : 'absolute',
162
169
  bottom: 12,
163
- left: 12,
164
- ...(narrow ? { right: 12 } : { width: 280 }),
170
+ insetInlineStart: 12,
171
+ ...(narrow ? { insetInlineEnd: 12 } : { width: 280 }),
165
172
  zIndex: Z_INDEX.floating,
166
173
  }
167
174
  : { width: 280 };
@@ -183,6 +190,7 @@ const ProOfferCardComponent: React.FC<ProOfferCardProps> = ({
183
190
 
184
191
  return (
185
192
  <Animated.View
193
+ {...dirProps}
186
194
  role="complementary"
187
195
  accessibilityLabel={accessibilityLabel}
188
196
  testID={testID}
@@ -235,7 +243,7 @@ const ProOfferCardComponent: React.FC<ProOfferCardProps> = ({
235
243
  size="xs"
236
244
  accessibilityLabel={dismissLabel}
237
245
  onPress={onDismiss}
238
- style={{ position: 'absolute', top: 12, right: 12, zIndex: Z_INDEX.raised }}
246
+ style={{ position: 'absolute', top: 12, insetInlineEnd: 12, zIndex: Z_INDEX.raised }}
239
247
  testID={testID ? `${testID}-dismiss` : undefined}
240
248
  />
241
249
  </Animated.View>
@@ -10,8 +10,9 @@ import type { SidebarProps } from '../sidebar/types';
10
10
  * How the sidebar reaches a narrow screen (below `lg`):
11
11
  *
12
12
  * - `overlay` a drawer over the page with a 40% black backdrop
13
- * - `reveal` the sidebar waits beneath the page, which slides 272px right and
14
- * rounds its corner to 32 as the rail scales in from 0.94
13
+ * - `reveal` the sidebar waits beneath the page, which slides 272px toward
14
+ * the end edge (right in LTR, left in RTL) and rounds its corner
15
+ * to 32 as the rail scales in from 0.94
15
16
  */
16
17
  export type AppShellDrawer = 'overlay' | 'reveal';
17
18
 
@@ -92,7 +93,7 @@ export interface AppShellEngineProps {
92
93
  title?: string;
93
94
  /** Breadcrumb trail above the heading (a `Breadcrumb`). */
94
95
  breadcrumb?: ReactNode;
95
- /** Header actions on the right (`NotificationBell`, buttons). */
96
+ /** Header actions at the end edge — the right in LTR (`NotificationBell`, buttons). */
96
97
  actions?: ReactNode;
97
98
  /**
98
99
  * Replaces the whole header. Put an `AppShellMenuButton` in it to keep the
@@ -105,7 +106,7 @@ export interface AppShellEngineProps {
105
106
  */
106
107
  header?: ReactNode;
107
108
  /**
108
- * A second column on the right of the page (a detail panel, activity, a
109
+ * A second column at the page's end edge — the right in LTR, the left in RTL (a detail panel, activity, a
109
110
  * chat). In flow from `asideFrom`; below it, stacked under the content or
110
111
  * hidden (`asideCollapse`). Pinned like the sidebar — with document scroll
111
112
  * it stays in document flow and sticks after tall content reaches its lower edge.
@@ -130,6 +131,14 @@ export interface AppShellEngineProps {
130
131
  /** Controlled drawer state (below `lg`). */
131
132
  drawerOpen?: boolean;
132
133
  onDrawerOpenChange?: (open: boolean) => void;
134
+ /** Names the hamburger that opens the drawer. Default `"Open navigation"`. */
135
+ drawerOpenLabel?: string;
136
+ /**
137
+ * Names the controls that close the drawer — the backdrop and the page veil a
138
+ * `reveal` drawer leaves. Default `"Close navigation"`. The sidebar's own close
139
+ * button is `sidebar.closeLabel`.
140
+ */
141
+ drawerCloseLabel?: string;
133
142
  /**
134
143
  * What scrolls the page.
135
144
  *
@@ -285,6 +294,8 @@ export interface AppShellHeaderProps {
285
294
  * header falls back to the window being narrower than `lg`.
286
295
  */
287
296
  showMenu?: boolean;
297
+ /** Names the hamburger. Default `"Open navigation"`. */
298
+ menuLabel?: string;
288
299
  style?: StyleProp<ViewStyle>;
289
300
  testID?: string;
290
301
  }
@@ -318,7 +329,7 @@ export interface ProOfferCardProps {
318
329
  accessibilityLabel?: string;
319
330
  dismissLabel?: string;
320
331
  /**
321
- * `fixed` (default) anchors bottom-left of the viewport (web) / the nearest
332
+ * `fixed` (default) anchors bottom-start (bottom-left in LTR) of the viewport (web) / the nearest
322
333
  * positioned parent (native); `inline` renders in flow.
323
334
  */
324
335
  placement?: 'fixed' | 'inline';
@@ -3,6 +3,7 @@ import { View, ScrollView, Keyboard, Platform } from 'react-native';
3
3
  import { useEffect } from 'react';
4
4
  import Animated, { useAnimatedReaction, useAnimatedStyle, interpolate, runOnJS, useReducedMotion } from 'react-native-reanimated';
5
5
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
+ import { useDirectionProps } from '../hooks/use-is-rtl';
6
7
  import { BottomBarSlotContext } from '../layout/bottom-bar-slot';
7
8
  import { useClaimBottomEdge } from '../layout/bottom-edge';
8
9
  import { FAB_METRICS } from '../fab/constants';
@@ -21,6 +22,8 @@ interface Props extends BottomBarProps {
21
22
  export function BottomBarBase({ Navigation, Item, Blur, items, value, onValueChange, activeProgress, onValueLongPress, action,
22
23
  actionPlacement = 'auto', actionBehavior = 'hide', material = 'translucent', minimizeProgress, blur = true, maxWidth = 560, style, testID }: Props) {
23
24
  const insets = useSafeAreaInsets();
25
+ // Logical insets below; react-native-web needs the direction handed to it.
26
+ const dirProps = useDirectionProps();
24
27
  const minimize = useMinimizeState();
25
28
  const progress = minimizeProgress ?? minimize.progress;
26
29
  const reducedMotion = useReducedMotion();
@@ -62,13 +65,16 @@ export function BottomBarBase({ Navigation, Item, Blur, items, value, onValueCha
62
65
  const amount = hideAction ? Math.min(1, Math.max(0, reducedMotion ? Number(progress.value > 0.01) : progress.value)) : 0;
63
66
  return {
64
67
  ...(actionWidth == null ? {} : { width: actionWidth * (1 - amount) }),
65
- marginLeft: items.length && !above ? 10 * (1 - amount) : 0,
68
+ // Logical, and the CSS name rather than RN's `marginStart`: reanimated's
69
+ // web writer hands a mapper's result to RNW's `createReactDOMStyle`,
70
+ // which never rewrites `marginStart`, so the browser would drop it.
71
+ marginInlineStart: items.length && !above ? 10 * (1 - amount) : 0,
66
72
  opacity: 1 - amount,
67
73
  transform: [{ scale: 1 - amount * 0.15 }],
68
74
  };
69
75
  }, [progress, hideAction, reducedMotion, actionWidth, items.length, above]);
70
76
  if (keyboardVisible || (!items.length && !action)) return null;
71
- return <View testID={testID} pointerEvents="box-none" style={[{ height: bottom + footprint, justifyContent: 'flex-end' }, style]}>
77
+ return <View {...dirProps} testID={testID} pointerEvents="box-none" style={[{ height: bottom + footprint, justifyContent: 'flex-end' }, style]}>
72
78
  {blur && material === 'translucent' && <Blur direction="bottom" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: bottom + footprint + BLUR_BLEED }} />}
73
79
  <Animated.View testID={testID ? `${testID}-row` : undefined} onLayout={event => setRowWidth(event.nativeEvent.layout.width)} pointerEvents="box-none" style={[{ justifyContent: items.length ? 'center' : 'flex-end', alignSelf: 'center', width: '100%', maxWidth, paddingLeft: 12, paddingRight: 12, marginBottom: bottom, flexDirection: 'row', alignItems: 'center' }, rowStyle]}>
74
80
  {items.length > 0 && <View testID={testID ? `${testID}-navigation` : undefined} style={{ width: navigationWidth, minWidth: 0 }}><ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ minWidth: '100%' }}><View style={{ width: Math.max(navigationWidth, items.length * 44 + 8) }}><Navigation scrollable={navigationWidth > 0 && navigationWidth < items.length * 44 + 8} embedded blur={false} material={material} minimizeProgress={progress}
@@ -79,7 +85,7 @@ export function BottomBarBase({ Navigation, Item, Blur, items, value, onValueCha
79
85
  {action && <Animated.View ref={actionRef} pointerEvents={actionHidden ? 'none' : 'auto'}
80
86
  accessibilityElementsHidden={actionHidden} importantForAccessibility={actionHidden ? 'no-hide-descendants' : 'auto'}
81
87
  aria-hidden={actionHidden} testID={testID ? `${testID}-action` : undefined}
82
- style={[{ flexShrink: 0, alignItems: 'flex-start', justifyContent: 'center' }, above ? { position: 'absolute', right: 12, bottom: '100%', marginBottom: 10 } : undefined, actionStyle]}>
88
+ style={[{ flexShrink: 0, alignItems: 'flex-start', justifyContent: 'center' }, above ? { position: 'absolute', insetInlineEnd: 12, bottom: '100%', marginBottom: 10 } : undefined, actionStyle]}>
83
89
  <View testID={testID ? `${testID}-action-content` : undefined} onLayout={event => { setActionWidth(event.nativeEvent.layout.width); setActionHeight(event.nativeEvent.layout.height); }} style={{ flexShrink: 0, ...(actionWidth == null ? {} : { width: actionWidth }) }}>{action}</View>
84
90
  </Animated.View>}
85
91
  </Animated.View>
@@ -14,3 +14,4 @@ export type { Gutter, Gutters } from './use-gutters';
14
14
  export { useImagePreload, preloadImage } from './use-image-preload';
15
15
  export { useContainerWidth } from './use-container-width';
16
16
  export { moveItem, dragTarget, dragShift } from './list-reorder';
17
+ export { useIsRtl } from './use-is-rtl';
@@ -0,0 +1,102 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ import { I18nManager, Platform } from 'react-native';
3
+
4
+ import type { FloatingAlign, FloatingSide } from '../floating/types';
5
+
6
+ /**
7
+ * Whether the layout around the caller is mirrored (right-to-left), read from
8
+ * the SAME place each platform's layout engine reads it, so a sign computed
9
+ * from it agrees with the flex order and the logical insets by construction:
10
+ *
11
+ * web `document.documentElement.dir === 'rtl'`, observed live — an app
12
+ * that switches language sets `dir` on `<html>` and every caller
13
+ * re-renders. Bloom never writes `dir` itself.
14
+ * native `I18nManager.isRTL`. `I18nManager.forceRTL` takes effect on the
15
+ * NEXT launch (a running app cannot mirror itself), so the value is
16
+ * constant for the process and the subscription never fires.
17
+ *
18
+ * It exists for what logical style keys CANNOT express: a `translateX` sign, a
19
+ * `transformOrigin`, a floating `side`. Insets use the `*InlineStart`/
20
+ * `*InlineEnd` keys instead, with `useDirectionProps()` on the root on web.
21
+ *
22
+ * `useSyncExternalStore`, not a read in render: both sources are external
23
+ * mutable state, and the sign has to be settled before the first paint rather
24
+ * than one effect later. The server snapshot is left-to-right — a statically
25
+ * exported shell has no `dir` yet.
26
+ */
27
+ export function useIsRtl(): boolean {
28
+ const web = Platform.OS === 'web';
29
+ return useSyncExternalStore(
30
+ web ? subscribeToDocumentDirection : subscribeToNothing,
31
+ web ? readDocumentDirection : readNativeDirection,
32
+ readServerDirection,
33
+ );
34
+ }
35
+
36
+ function subscribeToNothing(): () => void {
37
+ return () => {};
38
+ }
39
+
40
+ function readNativeDirection(): boolean {
41
+ return I18nManager?.isRTL === true;
42
+ }
43
+
44
+ function readServerDirection(): boolean {
45
+ return false;
46
+ }
47
+
48
+ function subscribeToDocumentDirection(onChange: () => void): () => void {
49
+ // No DOM (static export under Node) or no observer (a bare DOM shim): nothing
50
+ // can change during that render, and the first client render subscribes.
51
+ if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return () => {};
52
+ const observer = new MutationObserver(onChange);
53
+ observer.observe(document.documentElement, { attributes: true, attributeFilter: ['dir'] });
54
+ return () => observer.disconnect();
55
+ }
56
+
57
+ function readDocumentDirection(): boolean {
58
+ if (typeof document === 'undefined') return false;
59
+ return document.documentElement.dir === 'rtl';
60
+ }
61
+
62
+ /**
63
+ * Floating surfaces (`side`, `align`) are PHYSICAL — `'right'` is the right of
64
+ * the viewport, and under a vertical side `'end'` is the right edge of the
65
+ * anchor. A caller that means "beside the trailing edge" mirrors them here.
66
+ */
67
+ export function mirrorSide(side: FloatingSide, rtl: boolean): FloatingSide {
68
+ if (!rtl) return side;
69
+ return side === 'left' ? 'right' : side === 'right' ? 'left' : side;
70
+ }
71
+
72
+ /** The horizontal `align` under a `top`/`bottom` side; a no-op otherwise. */
73
+ export function mirrorAlign(align: FloatingAlign, side: FloatingSide, rtl: boolean): FloatingAlign {
74
+ if (!rtl || (side !== 'top' && side !== 'bottom')) return align;
75
+ return align === 'start' ? 'end' : align === 'end' ? 'start' : align;
76
+ }
77
+
78
+ const RTL_DIR = { dir: 'rtl' } as const;
79
+ const NO_DIR = {} as const;
80
+
81
+ /**
82
+ * Props that hand the document's direction to react-native-web — spread them on
83
+ * the ROOT view of anything using logical insets.
84
+ *
85
+ * react-native-web does NOT resolve `paddingInlineStart`/`insetInlineEnd`/
86
+ * `borderEndWidth` against `<html dir>`. Its inline and atomic style paths both
87
+ * rewrite them to PHYSICAL properties against its own direction context, which
88
+ * only a `dir` (or `lang`) prop on an ancestor `View` sets and which otherwise
89
+ * defaults to left-to-right. Without this the flex rows mirror (CSS `direction`
90
+ * inherits from `<html>`) while every logical inset stays on the left — half a
91
+ * mirror. Measured on react-native-web 0.21.2: `StyleSheet([{ insetInlineStart:
92
+ * 0 }])` resolves to `left: 0px` with no context.
93
+ *
94
+ * Reanimated's web writer bypasses that rewrite and sets the CSS logical
95
+ * property itself, which then follows the DOM `dir` this also sets — so the first
96
+ * paint and every animated frame agree. Web only; native Yoga reads
97
+ * `I18nManager` directly and has no `dir` prop.
98
+ */
99
+ export function useDirectionProps(): { dir?: 'rtl' } {
100
+ const rtl = useIsRtl();
101
+ return Platform.OS === 'web' && rtl ? RTL_DIR : NO_DIR;
102
+ }