@oxy.so/bloom 4.15.0 → 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/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/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/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/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/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
|
@@ -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
|
+
}
|
package/src/sidebar/Sidebar.tsx
CHANGED
|
@@ -19,6 +19,7 @@ import { Badge } from '../badge';
|
|
|
19
19
|
import { CloseButton } from '../button';
|
|
20
20
|
import { useControllableState } from '../hooks/use-controllable-state';
|
|
21
21
|
import { useInteractionState } from '../hooks/use-interaction-state';
|
|
22
|
+
import { useDirectionProps, useIsRtl } from '../hooks/use-is-rtl';
|
|
22
23
|
import { RiCloseLine } from '../icons/remix/RiCloseLine';
|
|
23
24
|
import { RiSearchLine } from '../icons/remix/RiSearchLine';
|
|
24
25
|
import { RiSideBarFill } from '../icons/remix/RiSideBarFill';
|
|
@@ -87,6 +88,7 @@ function SearchField({
|
|
|
87
88
|
onDismiss,
|
|
88
89
|
placeholder,
|
|
89
90
|
label,
|
|
91
|
+
clearLabel,
|
|
90
92
|
compact = false,
|
|
91
93
|
}: {
|
|
92
94
|
palette: SidebarPalette;
|
|
@@ -96,6 +98,7 @@ function SearchField({
|
|
|
96
98
|
onDismiss: (restoreFocus: boolean) => void;
|
|
97
99
|
placeholder: string;
|
|
98
100
|
label: string;
|
|
101
|
+
clearLabel: string;
|
|
99
102
|
compact?: boolean;
|
|
100
103
|
}) {
|
|
101
104
|
const inputStyle: TextStyle & WebCssStyle = {
|
|
@@ -130,7 +133,7 @@ function SearchField({
|
|
|
130
133
|
/>
|
|
131
134
|
<CloseButton
|
|
132
135
|
size="xs"
|
|
133
|
-
accessibilityLabel=
|
|
136
|
+
accessibilityLabel={clearLabel}
|
|
134
137
|
onPress={() => onDismiss(true)}
|
|
135
138
|
style={{ backgroundColor: palette.tertiaryHover }}
|
|
136
139
|
/>
|
|
@@ -157,6 +160,10 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
157
160
|
onCollapsedChange,
|
|
158
161
|
mobile = false,
|
|
159
162
|
onClose,
|
|
163
|
+
accessibilityLabel = 'Sidebar',
|
|
164
|
+
collapseLabel = 'Collapse sidebar',
|
|
165
|
+
expandLabel = 'Expand sidebar',
|
|
166
|
+
closeLabel = 'Close sidebar',
|
|
160
167
|
fluid = false,
|
|
161
168
|
surface = 'card',
|
|
162
169
|
size: sizeProp,
|
|
@@ -167,7 +174,10 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
167
174
|
searchQuery,
|
|
168
175
|
onSearchQueryChange,
|
|
169
176
|
searchLabel = 'Quick Search',
|
|
177
|
+
searchButtonLabel = 'Search',
|
|
170
178
|
searchPlaceholder,
|
|
179
|
+
filterLabel = 'Filter navigation',
|
|
180
|
+
clearSearchLabel = 'Clear navigation search',
|
|
171
181
|
noResultsLabel = 'No results',
|
|
172
182
|
logo,
|
|
173
183
|
account,
|
|
@@ -186,6 +196,12 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
186
196
|
const metrics = SIDEBAR_METRICS[size];
|
|
187
197
|
useSidebarWebCss();
|
|
188
198
|
const reducedMotion = useReducedMotion();
|
|
199
|
+
// Insets below are logical and mirror by themselves; the header control's
|
|
200
|
+
// `translateX` and the collapse glyph's `scaleX` are signs, and flip here.
|
|
201
|
+
const dir = useIsRtl() ? -1 : 1;
|
|
202
|
+
// A standalone sidebar has no shell above it to hand react-native-web the
|
|
203
|
+
// document's direction, so it hands it on itself (see `useDirectionProps`).
|
|
204
|
+
const dirProps = useDirectionProps();
|
|
189
205
|
|
|
190
206
|
const [collapsedState, setCollapsed] = useControllableState<boolean>({
|
|
191
207
|
value: collapsedProp,
|
|
@@ -255,8 +271,8 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
255
271
|
return { paddingLeft: padding, paddingRight: padding, marginLeft: (collapsedLane - metrics.row.square) / 2 * progress.value, marginRight: (collapsedLane - metrics.row.square) / 2 * progress.value };
|
|
256
272
|
}, [progress, metrics.row.square, metrics.row.padding, metrics.row.icon, expandedInset, collapsedLane]);
|
|
257
273
|
const sideBarIconStyle = useAnimatedStyle(
|
|
258
|
-
() => ({ transform: [{ scaleX: 2 * progress.value - 1 }] }),
|
|
259
|
-
[progress],
|
|
274
|
+
() => ({ transform: [{ scaleX: dir * (2 * progress.value - 1) }] }),
|
|
275
|
+
[progress, dir],
|
|
260
276
|
);
|
|
261
277
|
|
|
262
278
|
const hasLogo = !!logo;
|
|
@@ -266,17 +282,17 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
266
282
|
}), [progress, hasLogo, hasAccount]);
|
|
267
283
|
const headerIdentityGeometry = useAnimatedStyle(() => ({
|
|
268
284
|
top: hasLogo ? 0 : 30 * progress.value,
|
|
269
|
-
|
|
285
|
+
insetInlineEnd: (hasLogo ? 28 : 20) * (1 - progress.value),
|
|
270
286
|
}), [progress, hasLogo]);
|
|
271
287
|
const headerControlGeometry = useAnimatedStyle(() => {
|
|
272
288
|
const p = progress.value;
|
|
273
289
|
return {
|
|
274
|
-
|
|
290
|
+
insetInlineStart: `${100 * (1 - p)}%` as `${number}%`,
|
|
275
291
|
width: 20 + (collapsedLane - 20) * p,
|
|
276
292
|
top: hasLogo ? 8 + 38 * p : hasAccount ? 6 * (1 - p) : 0,
|
|
277
|
-
transform: [{ translateX: -20 * (1 - p) }],
|
|
293
|
+
transform: [{ translateX: dir * -20 * (1 - p) }],
|
|
278
294
|
};
|
|
279
|
-
}, [progress, hasLogo, hasAccount, collapsedLane]);
|
|
295
|
+
}, [progress, hasLogo, hasAccount, collapsedLane, dir]);
|
|
280
296
|
const themeGeometry = useAnimatedStyle(() => ({ height: 40 - 4 * progress.value }), [progress]);
|
|
281
297
|
const themeExpandedStyle = useAnimatedStyle(() => ({ opacity: 1 - progress.value }), [progress]);
|
|
282
298
|
const themeCollapsedStyle = useAnimatedStyle(() => ({ opacity: progress.value }), [progress]);
|
|
@@ -368,7 +384,8 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
368
384
|
|
|
369
385
|
// ---- chrome ------------------------------------------------------------
|
|
370
386
|
// Three surfaces, one place. `docked` keeps the panel fill and spends its
|
|
371
|
-
// edge on ONE hairline — the side facing the content
|
|
387
|
+
// edge on ONE hairline — the side facing the content, its END edge (the
|
|
388
|
+
// nav is the row's first child, so the right in LTR, the left in RTL) — because a column
|
|
372
389
|
// flush to the window has no other edge to draw: a border all the way round
|
|
373
390
|
// would draw two lines nobody can see and one they can.
|
|
374
391
|
const chrome: WebCssStyle =
|
|
@@ -377,8 +394,8 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
377
394
|
: surface === 'docked'
|
|
378
395
|
? {
|
|
379
396
|
backgroundColor: palette.panel,
|
|
380
|
-
|
|
381
|
-
|
|
397
|
+
borderEndWidth: 1,
|
|
398
|
+
borderEndColor: palette.dockedEdge,
|
|
382
399
|
}
|
|
383
400
|
: {
|
|
384
401
|
borderRadius: 24,
|
|
@@ -411,7 +428,7 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
411
428
|
borderRadius: borderRadius.full,
|
|
412
429
|
backgroundColor: palette.tertiary,
|
|
413
430
|
...(searchActive
|
|
414
|
-
? { flex: 1, gap: 8,
|
|
431
|
+
? { flex: 1, gap: 8, paddingInlineStart: 8, paddingInlineEnd: 10, boxShadow: searchRing }
|
|
415
432
|
: { width: 36, gap: 0, paddingLeft: 8, paddingRight: 8 }),
|
|
416
433
|
}}
|
|
417
434
|
>
|
|
@@ -419,7 +436,7 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
419
436
|
ref={searchTriggerRef}
|
|
420
437
|
{...(IS_WEB ? { dataSet: { bloomSidebar: 'ring' } } : {})}
|
|
421
438
|
role="button"
|
|
422
|
-
accessibilityLabel=
|
|
439
|
+
accessibilityLabel={searchButtonLabel}
|
|
423
440
|
onPress={activateSearch}
|
|
424
441
|
style={{ width: 20, height: 20, alignItems: 'center', justifyContent: 'center', flexShrink: 0 }}
|
|
425
442
|
>
|
|
@@ -434,7 +451,8 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
434
451
|
onChangeText={setQuery}
|
|
435
452
|
onDismiss={deactivateSearch}
|
|
436
453
|
placeholder={placeholder}
|
|
437
|
-
label=
|
|
454
|
+
label={filterLabel}
|
|
455
|
+
clearLabel={clearSearchLabel}
|
|
438
456
|
/>
|
|
439
457
|
) : null}
|
|
440
458
|
</View>
|
|
@@ -442,7 +460,7 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
442
460
|
<Pressable
|
|
443
461
|
{...(IS_WEB ? { dataSet: { bloomSidebar: 'ring' } } : {})}
|
|
444
462
|
role="button"
|
|
445
|
-
accessibilityLabel=
|
|
463
|
+
accessibilityLabel={closeLabel}
|
|
446
464
|
onPress={onClose}
|
|
447
465
|
testID="sidebar-close"
|
|
448
466
|
>
|
|
@@ -452,7 +470,7 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
452
470
|
<Pressable
|
|
453
471
|
{...(IS_WEB ? { dataSet: { bloomSidebar: 'ring' } } : {})}
|
|
454
472
|
role="button"
|
|
455
|
-
accessibilityLabel={collapsed ?
|
|
473
|
+
accessibilityLabel={collapsed ? expandLabel : collapseLabel}
|
|
456
474
|
aria-expanded={!collapsed}
|
|
457
475
|
onPress={toggleCollapse}
|
|
458
476
|
style={{ width: '100%', alignItems: 'center', justifyContent: 'center' }}
|
|
@@ -476,8 +494,8 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
476
494
|
backgroundColor: palette.tertiary,
|
|
477
495
|
paddingTop: 8,
|
|
478
496
|
paddingBottom: 8,
|
|
479
|
-
|
|
480
|
-
|
|
497
|
+
paddingInlineStart: 8,
|
|
498
|
+
paddingInlineEnd: 10,
|
|
481
499
|
boxShadow: searchRing,
|
|
482
500
|
}}
|
|
483
501
|
testID="sidebar-search-field"
|
|
@@ -490,7 +508,8 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
490
508
|
onChangeText={setQuery}
|
|
491
509
|
onDismiss={deactivateSearch}
|
|
492
510
|
placeholder={placeholder}
|
|
493
|
-
label=
|
|
511
|
+
label={filterLabel}
|
|
512
|
+
clearLabel={clearSearchLabel}
|
|
494
513
|
/>
|
|
495
514
|
</View>
|
|
496
515
|
) : (
|
|
@@ -600,7 +619,7 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
600
619
|
) : (
|
|
601
620
|
<>
|
|
602
621
|
<Animated.View testID="sidebar-header" style={[{ width: '100%', position: 'relative' }, headerGeometry]}>
|
|
603
|
-
{logo || account ? <Animated.View style={[{ position: 'absolute',
|
|
622
|
+
{logo || account ? <Animated.View style={[{ position: 'absolute', insetInlineStart: 0, minWidth: 0 }, headerIdentityGeometry]}>
|
|
604
623
|
{logo ? <SidebarLogoView logo={logo} collapsed={collapsed} testID="sidebar-logo" /> : accountMenu}
|
|
605
624
|
</Animated.View> : null}
|
|
606
625
|
<Animated.View testID="sidebar-header-control" style={[{ position: 'absolute', height: 20 }, headerControlGeometry]}>
|
|
@@ -616,8 +635,9 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
616
635
|
<SidebarGeometryProvider value={geometry}>
|
|
617
636
|
<CollapseProvider value={progress}>
|
|
618
637
|
<Animated.View
|
|
638
|
+
{...dirProps}
|
|
619
639
|
role="complementary"
|
|
620
|
-
accessibilityLabel=
|
|
640
|
+
accessibilityLabel={accessibilityLabel}
|
|
621
641
|
onLayout={onPanelLayout}
|
|
622
642
|
testID={testID}
|
|
623
643
|
style={[
|
|
@@ -706,7 +726,7 @@ const SidebarPanel: React.FC<SidebarProps> = ({
|
|
|
706
726
|
<Animated.View testID="sidebar-theme-morph" style={[{ position: 'relative', overflow: 'hidden' }, themeGeometry]}>
|
|
707
727
|
<Animated.View pointerEvents={collapsed ? 'none' : 'auto'} aria-hidden={collapsed} accessibilityElementsHidden={collapsed} importantForAccessibility={collapsed ? 'no-hide-descendants' : 'auto'}
|
|
708
728
|
{...(IS_WEB && collapsed ? { inert: true } : {})}
|
|
709
|
-
style={[{ position: 'absolute',
|
|
729
|
+
style={[{ position: 'absolute', insetInlineStart: 0, top: 0 }, themeExpandedStyle]}>
|
|
710
730
|
<ThemeToggle variant="sidebar-segmented" style={plain ? { backgroundColor: palette.panel } : undefined} />
|
|
711
731
|
</Animated.View>
|
|
712
732
|
<Animated.View pointerEvents={collapsed ? 'auto' : 'none'} aria-hidden={!collapsed} accessibilityElementsHidden={!collapsed} importantForAccessibility={collapsed ? 'auto' : 'no-hide-descendants'}
|
|
@@ -12,6 +12,7 @@ import Svg, { Path } from 'react-native-svg';
|
|
|
12
12
|
|
|
13
13
|
import { useControllableState } from '../hooks/use-controllable-state';
|
|
14
14
|
import { useInteractionState } from '../hooks/use-interaction-state';
|
|
15
|
+
import { useIsRtl } from '../hooks/use-is-rtl';
|
|
15
16
|
import { RiFolderLine } from '../icons/remix/RiFolderLine';
|
|
16
17
|
import { RiFolderOpenLine } from '../icons/remix/RiFolderOpenLine';
|
|
17
18
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
@@ -27,13 +28,15 @@ const FIRST_CENTER = 15;
|
|
|
27
28
|
/**
|
|
28
29
|
* The curved connector: a guide dropping from the folder glyph with a rounded
|
|
29
30
|
* elbow into each row — 1px icon-quaternary, trunk at x 17, each elbow landing 5px
|
|
30
|
-
*
|
|
31
|
+
* toward the end of it and running on to 11.5. The path is drawn left-to-right,
|
|
32
|
+
* so under RTL the box is mirrored as well as anchored from the start edge.
|
|
31
33
|
*/
|
|
32
34
|
function TreeConnector({ count, color }: { count: number; color: string }) {
|
|
35
|
+
const rtl = useIsRtl();
|
|
33
36
|
if (count === 0) return null;
|
|
34
37
|
const height = FIRST_CENTER + ROW_PITCH * (count - 1) + 1;
|
|
35
38
|
return (
|
|
36
|
-
<View pointerEvents="none" style={{ position: 'absolute', top: 0,
|
|
39
|
+
<View pointerEvents="none" style={{ position: 'absolute', top: 0, insetInlineStart: 16.5, width: 12, height, transform: rtl ? [{ scaleX: -1 }] : undefined }}>
|
|
37
40
|
<Svg width={12} height={height} viewBox={`0 0 12 ${height}`} fill="none">
|
|
38
41
|
{Array.from({ length: count }, (_, i) => {
|
|
39
42
|
const y = FIRST_CENTER + ROW_PITCH * i;
|
|
@@ -69,8 +72,8 @@ function TreeRow({
|
|
|
69
72
|
borderRadius: borderRadius.full,
|
|
70
73
|
paddingTop: 5,
|
|
71
74
|
paddingBottom: 5,
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
paddingInlineEnd: 8,
|
|
76
|
+
paddingInlineStart: 36,
|
|
74
77
|
backgroundColor: selected || hovered ? palette.rowHover : 'transparent',
|
|
75
78
|
'--bloom-sidebar-ring': palette.ring,
|
|
76
79
|
};
|
|
@@ -18,7 +18,7 @@ export const SIDEBAR_LOGO_SIZE = 36;
|
|
|
18
18
|
* mark centred in a 36 × 36 box — the collapsed column — so it stays
|
|
19
19
|
* put while the rail morphs; sized by the caller (a 24–28px glyph
|
|
20
20
|
* reads right)
|
|
21
|
-
* wordmark 8
|
|
21
|
+
* wordmark 8 after it (inline-end), in a collapse slot; a string renders as
|
|
22
22
|
* headline-semibold text-primary, anything else as given (an SVG
|
|
23
23
|
* wordmark, an image)
|
|
24
24
|
* link with `href` a real anchor on web, with `onPress` a button; the
|
|
@@ -53,7 +53,7 @@ const SidebarLogoViewComponent: React.FC<SidebarLogoViewProps> = ({
|
|
|
53
53
|
) : null}
|
|
54
54
|
{wordmark != null && showWordmark ? (
|
|
55
55
|
<Collapsible collapsed={collapsed}>
|
|
56
|
-
<View style={{ minHeight: SIDEBAR_LOGO_SIZE,
|
|
56
|
+
<View style={{ minHeight: SIDEBAR_LOGO_SIZE, paddingInlineStart: icon != null ? 8 : 0, justifyContent: 'center' }} testID={testID ? `${testID}-wordmark` : undefined}>
|
|
57
57
|
{typeof wordmark === 'string' ? (
|
|
58
58
|
<Text variant="headline-semibold" numberOfLines={1} style={{ color: palette.text }}>
|
|
59
59
|
{wordmark}
|
|
@@ -118,7 +118,7 @@ function ModeRow({
|
|
|
118
118
|
<Text
|
|
119
119
|
variant="caption-2-regular"
|
|
120
120
|
numberOfLines={1}
|
|
121
|
-
style={{ color: palette.textTertiary,
|
|
121
|
+
style={{ color: palette.textTertiary, paddingInlineEnd: 12, opacity: hovered ? 1 : 0 }}
|
|
122
122
|
>
|
|
123
123
|
{mode.shortcut}
|
|
124
124
|
</Text>
|