@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.
- package/docs/app-shell.mdx +74 -6
- package/docs/bottom-bar.mdx +9 -1
- package/docs/hooks.mdx +23 -0
- package/docs/sidebar.mdx +15 -2
- package/docs/web-search.mdx +1 -1
- package/lib/commonjs/app-shell/AppShell.js +3 -1
- package/lib/commonjs/app-shell/AppShell.js.map +1 -1
- package/lib/commonjs/app-shell/AppShellEngine.js +54 -28
- package/lib/commonjs/app-shell/AppShellEngine.js.map +1 -1
- package/lib/commonjs/app-shell/AppShellHeader.js +4 -3
- package/lib/commonjs/app-shell/AppShellHeader.js.map +1 -1
- package/lib/commonjs/app-shell/NotificationBell.js +8 -4
- package/lib/commonjs/app-shell/NotificationBell.js.map +1 -1
- package/lib/commonjs/app-shell/ProOfferCard.js +19 -11
- package/lib/commonjs/app-shell/ProOfferCard.js.map +1 -1
- package/lib/commonjs/bottom-bar/BottomBarBase.js +9 -2
- package/lib/commonjs/bottom-bar/BottomBarBase.js.map +1 -1
- package/lib/commonjs/hooks/index.js +7 -0
- package/lib/commonjs/hooks/index.js.map +1 -1
- package/lib/commonjs/hooks/use-is-rtl.js +104 -0
- package/lib/commonjs/hooks/use-is-rtl.js.map +1 -0
- package/lib/commonjs/sidebar/Sidebar.js +41 -22
- package/lib/commonjs/sidebar/Sidebar.js.map +1 -1
- package/lib/commonjs/sidebar/SidebarFolder.js +11 -5
- package/lib/commonjs/sidebar/SidebarFolder.js.map +1 -1
- package/lib/commonjs/sidebar/SidebarLogoView.js +2 -2
- package/lib/commonjs/sidebar/SidebarLogoView.js.map +1 -1
- package/lib/commonjs/sidebar/SidebarModeSwitcher.js +1 -1
- package/lib/commonjs/sidebar/SidebarModeSwitcher.js.map +1 -1
- package/lib/commonjs/sidebar/SidebarPlanCard.js +3 -3
- package/lib/commonjs/sidebar/SidebarPlanCard.js.map +1 -1
- package/lib/commonjs/sidebar/SidebarRail.js +7 -1
- package/lib/commonjs/sidebar/SidebarRail.js.map +1 -1
- package/lib/commonjs/sidebar/SidebarRailItem.js +2 -2
- package/lib/commonjs/sidebar/SidebarRailItem.js.map +1 -1
- package/lib/commonjs/sidebar/SidebarTeamMenu.js +12 -7
- package/lib/commonjs/sidebar/SidebarTeamMenu.js.map +1 -1
- package/lib/commonjs/sidebar/SidebarUserMenu.js +9 -5
- package/lib/commonjs/sidebar/SidebarUserMenu.js.map +1 -1
- package/lib/commonjs/tab-bar/TabBarBase.js +19 -5
- package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
- package/lib/commonjs/web-search/WebSearch.js +12 -0
- package/lib/commonjs/web-search/WebSearch.js.map +1 -1
- package/lib/module/app-shell/AppShell.js +3 -1
- package/lib/module/app-shell/AppShell.js.map +1 -1
- package/lib/module/app-shell/AppShellEngine.js +54 -28
- package/lib/module/app-shell/AppShellEngine.js.map +1 -1
- package/lib/module/app-shell/AppShellHeader.js +4 -3
- package/lib/module/app-shell/AppShellHeader.js.map +1 -1
- package/lib/module/app-shell/NotificationBell.js +8 -4
- package/lib/module/app-shell/NotificationBell.js.map +1 -1
- package/lib/module/app-shell/ProOfferCard.js +19 -11
- package/lib/module/app-shell/ProOfferCard.js.map +1 -1
- package/lib/module/bottom-bar/BottomBarBase.js +9 -2
- package/lib/module/bottom-bar/BottomBarBase.js.map +1 -1
- package/lib/module/hooks/index.js +1 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/use-is-rtl.js +97 -0
- package/lib/module/hooks/use-is-rtl.js.map +1 -0
- package/lib/module/sidebar/Sidebar.js +41 -22
- package/lib/module/sidebar/Sidebar.js.map +1 -1
- package/lib/module/sidebar/SidebarFolder.js +11 -5
- package/lib/module/sidebar/SidebarFolder.js.map +1 -1
- package/lib/module/sidebar/SidebarLogoView.js +2 -2
- package/lib/module/sidebar/SidebarLogoView.js.map +1 -1
- package/lib/module/sidebar/SidebarModeSwitcher.js +1 -1
- package/lib/module/sidebar/SidebarModeSwitcher.js.map +1 -1
- package/lib/module/sidebar/SidebarPlanCard.js +3 -3
- package/lib/module/sidebar/SidebarPlanCard.js.map +1 -1
- package/lib/module/sidebar/SidebarRail.js +7 -1
- package/lib/module/sidebar/SidebarRail.js.map +1 -1
- package/lib/module/sidebar/SidebarRailItem.js +2 -2
- package/lib/module/sidebar/SidebarRailItem.js.map +1 -1
- package/lib/module/sidebar/SidebarTeamMenu.js +12 -7
- package/lib/module/sidebar/SidebarTeamMenu.js.map +1 -1
- package/lib/module/sidebar/SidebarUserMenu.js +9 -5
- package/lib/module/sidebar/SidebarUserMenu.js.map +1 -1
- package/lib/module/tab-bar/TabBarBase.js +19 -5
- package/lib/module/tab-bar/TabBarBase.js.map +1 -1
- package/lib/module/web-search/WebSearch.js +13 -1
- package/lib/module/web-search/WebSearch.js.map +1 -1
- package/lib/typescript/commonjs/app-shell/AppShellEngine.d.ts.map +1 -1
- package/lib/typescript/commonjs/app-shell/AppShellHeader.d.ts.map +1 -1
- package/lib/typescript/commonjs/app-shell/NotificationBell.d.ts.map +1 -1
- package/lib/typescript/commonjs/app-shell/ProOfferCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/app-shell/types.d.ts +16 -5
- package/lib/typescript/commonjs/app-shell/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-bar/BottomBarBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/index.d.ts +1 -0
- package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/use-is-rtl.d.ts +53 -0
- package/lib/typescript/commonjs/hooks/use-is-rtl.d.ts.map +1 -0
- package/lib/typescript/commonjs/icons/shared.d.ts +10 -10
- package/lib/typescript/commonjs/sidebar/Sidebar.d.ts.map +1 -1
- package/lib/typescript/commonjs/sidebar/SidebarFolder.d.ts.map +1 -1
- package/lib/typescript/commonjs/sidebar/SidebarRail.d.ts +1 -1
- package/lib/typescript/commonjs/sidebar/SidebarRail.d.ts.map +1 -1
- package/lib/typescript/commonjs/sidebar/SidebarTeamMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/sidebar/SidebarUserMenu.d.ts.map +1 -1
- package/lib/typescript/commonjs/sidebar/types.d.ts +24 -3
- package/lib/typescript/commonjs/sidebar/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/web-search/WebSearch.d.ts.map +1 -1
- package/lib/typescript/commonjs/web-search/types.d.ts +6 -0
- package/lib/typescript/commonjs/web-search/types.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/AppShellEngine.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/AppShellHeader.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/NotificationBell.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/ProOfferCard.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/types.d.ts +16 -5
- package/lib/typescript/module/app-shell/types.d.ts.map +1 -1
- package/lib/typescript/module/bottom-bar/BottomBarBase.d.ts.map +1 -1
- package/lib/typescript/module/hooks/index.d.ts +1 -0
- package/lib/typescript/module/hooks/index.d.ts.map +1 -1
- package/lib/typescript/module/hooks/use-is-rtl.d.ts +53 -0
- package/lib/typescript/module/hooks/use-is-rtl.d.ts.map +1 -0
- package/lib/typescript/module/icons/shared.d.ts +10 -10
- package/lib/typescript/module/sidebar/Sidebar.d.ts.map +1 -1
- package/lib/typescript/module/sidebar/SidebarFolder.d.ts.map +1 -1
- package/lib/typescript/module/sidebar/SidebarRail.d.ts +1 -1
- package/lib/typescript/module/sidebar/SidebarRail.d.ts.map +1 -1
- package/lib/typescript/module/sidebar/SidebarTeamMenu.d.ts.map +1 -1
- package/lib/typescript/module/sidebar/SidebarUserMenu.d.ts.map +1 -1
- package/lib/typescript/module/sidebar/types.d.ts +24 -3
- package/lib/typescript/module/sidebar/types.d.ts.map +1 -1
- package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
- package/lib/typescript/module/web-search/WebSearch.d.ts.map +1 -1
- package/lib/typescript/module/web-search/types.d.ts +6 -0
- package/lib/typescript/module/web-search/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/app-shell/AppShell.tsx +1 -1
- package/src/app-shell/AppShellEngine.tsx +45 -24
- package/src/app-shell/AppShellHeader.tsx +4 -3
- package/src/app-shell/NotificationBell.tsx +8 -4
- package/src/app-shell/ProOfferCard.tsx +18 -10
- package/src/app-shell/types.ts +16 -5
- package/src/bottom-bar/BottomBarBase.tsx +9 -3
- package/src/hooks/index.ts +1 -0
- package/src/hooks/use-is-rtl.ts +102 -0
- package/src/sidebar/Sidebar.tsx +41 -21
- package/src/sidebar/SidebarFolder.tsx +7 -4
- package/src/sidebar/SidebarLogoView.tsx +2 -2
- package/src/sidebar/SidebarModeSwitcher.tsx +1 -1
- package/src/sidebar/SidebarPlanCard.tsx +3 -3
- package/src/sidebar/SidebarRail.tsx +7 -1
- package/src/sidebar/SidebarRailItem.tsx +2 -2
- package/src/sidebar/SidebarTeamMenu.tsx +10 -6
- package/src/sidebar/SidebarUserMenu.tsx +8 -5
- package/src/sidebar/types.ts +25 -3
- package/src/tab-bar/TabBarBase.tsx +19 -5
- package/src/web-search/WebSearch.tsx +9 -0
- 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-
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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:
|
|
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,
|
|
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=
|
|
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 ? {
|
|
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
|
|
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={{
|
|
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=
|
|
771
|
+
accessibilityLabel={drawerCloseLabel}
|
|
758
772
|
/>
|
|
759
773
|
<View
|
|
774
|
+
{...dirProps}
|
|
760
775
|
pointerEvents="box-none"
|
|
761
|
-
style={{ position: 'absolute', top: 0, bottom: 0,
|
|
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
|
-
|
|
778
|
-
|
|
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,
|
|
789
|
-
<Animated.View style={[{ height: '100%', width: 260, transformOrigin:
|
|
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=
|
|
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
|
-
*
|
|
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,
|
|
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=
|
|
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 /
|
|
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
|
|
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
|
-
|
|
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=
|
|
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-
|
|
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-
|
|
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
|
|
36
|
-
*
|
|
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:
|
|
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,
|
|
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
|
-
|
|
164
|
-
...(narrow ? {
|
|
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,
|
|
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>
|
package/src/app-shell/types.ts
CHANGED
|
@@ -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
|
|
14
|
-
*
|
|
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
|
|
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
|
|
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
|
-
|
|
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',
|
|
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>
|
package/src/hooks/index.ts
CHANGED
|
@@ -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
|
+
}
|