@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
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>
|
|
@@ -31,8 +31,8 @@ function SidebarPlanCardComponent({ plan, collapsed = false, style, testID }: Si
|
|
|
31
31
|
const geometry = useAnimatedStyle(() => ({
|
|
32
32
|
width: inSidebar ? '100%' : naturalWidth.value > 0 ? naturalWidth.value + (36 - naturalWidth.value) * progress.value : progress.value === 1 ? 36 : '100%',
|
|
33
33
|
height: 56 - 20 * progress.value,
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
paddingInlineStart: 10 + (compactPadding - 10) * progress.value,
|
|
35
|
+
paddingInlineEnd: 12 + (compactPadding - 12) * progress.value,
|
|
36
36
|
}), [progress, inSidebar, naturalWidth, compactPadding]);
|
|
37
37
|
const fill = useAnimatedStyle(() => ({ opacity: 1 - progress.value }), [progress]);
|
|
38
38
|
const avatar = plan.avatar ?? { initials: plan.name.slice(0, 1).toUpperCase(), color: 'blue' as const };
|
|
@@ -87,7 +87,7 @@ function SidebarPlanCardComponent({ plan, collapsed = false, style, testID }: Si
|
|
|
87
87
|
<View style={{ minWidth: 0, flexShrink: 1, flexDirection: 'row', alignItems: 'center' }}>
|
|
88
88
|
{avatarNode}
|
|
89
89
|
<Collapsible collapsed={collapsed}>
|
|
90
|
-
<View style={{
|
|
90
|
+
<View style={{ paddingInlineStart: 8, minWidth: 0, flexShrink: 1, flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'center' }}>
|
|
91
91
|
<Text variant="body-medium" numberOfLines={1} style={{ color: palette.text }}>
|
|
92
92
|
{plan.name}
|
|
93
93
|
</Text>
|
|
@@ -2,6 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import { View } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { Badge } from '../badge';
|
|
5
|
+
import { useDirectionProps } from '../hooks/use-is-rtl';
|
|
5
6
|
import { useSidebarPalette } from './palette';
|
|
6
7
|
import { IS_WEB } from './parts';
|
|
7
8
|
import { SidebarPrimaryAction } from './SidebarPrimaryAction';
|
|
@@ -34,10 +35,14 @@ export function SidebarRail({
|
|
|
34
35
|
secondaryItems = [],
|
|
35
36
|
selected,
|
|
36
37
|
onNavigate,
|
|
38
|
+
accessibilityLabel = 'Sidebar',
|
|
37
39
|
style,
|
|
38
40
|
testID,
|
|
39
41
|
}: SidebarProps) {
|
|
40
42
|
const palette = useSidebarPalette();
|
|
43
|
+
// The badges sit on logical insets; react-native-web resolves those against a
|
|
44
|
+
// `dir` prop, not `<html dir>` (see `useDirectionProps`).
|
|
45
|
+
const dirProps = useDirectionProps();
|
|
41
46
|
|
|
42
47
|
const renderItem = (item: SidebarNavItem) => {
|
|
43
48
|
const isSelected = selected === item.key;
|
|
@@ -68,8 +73,9 @@ export function SidebarRail({
|
|
|
68
73
|
|
|
69
74
|
return (
|
|
70
75
|
<View
|
|
76
|
+
{...dirProps}
|
|
71
77
|
role="complementary"
|
|
72
|
-
accessibilityLabel=
|
|
78
|
+
accessibilityLabel={accessibilityLabel}
|
|
73
79
|
testID={testID}
|
|
74
80
|
style={[
|
|
75
81
|
{
|
|
@@ -26,7 +26,7 @@ const SELECTION_DURATION = 220;
|
|
|
26
26
|
* glyph), background-secondary-hover on hover
|
|
27
27
|
* label 10px, centred, one line; text-primary medium while selected,
|
|
28
28
|
* text-secondary otherwise
|
|
29
|
-
* badge over the indicator's top-right
|
|
29
|
+
* badge over the indicator's top-end corner (top-right in LTR)
|
|
30
30
|
*
|
|
31
31
|
* With `href` the item is a link (a real anchor on web).
|
|
32
32
|
*/
|
|
@@ -111,7 +111,7 @@ const SidebarRailItemComponent: React.FC<SidebarRailItemProps> = ({
|
|
|
111
111
|
<SelectedGlyph width={ICON_SIZE} height={ICON_SIZE} fill={palette.selectedForeground} />
|
|
112
112
|
</Animated.View>
|
|
113
113
|
{badge != null ? (
|
|
114
|
-
<View pointerEvents="none" style={{ position: 'absolute', top: -6,
|
|
114
|
+
<View pointerEvents="none" style={{ position: 'absolute', top: -6, insetInlineStart: INDICATOR_WIDTH - 16 }}>
|
|
115
115
|
{badge}
|
|
116
116
|
</View>
|
|
117
117
|
) : null}
|
|
@@ -10,6 +10,7 @@ import Animated, {
|
|
|
10
10
|
|
|
11
11
|
import { Badge } from '../badge';
|
|
12
12
|
import { useInteractionState } from '../hooks/use-interaction-state';
|
|
13
|
+
import { mirrorAlign, mirrorSide, useIsRtl } from '../hooks/use-is-rtl';
|
|
13
14
|
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
|
|
14
15
|
import { BREAKPOINTS } from '../styles/breakpoints';
|
|
15
16
|
import { borderRadius } from '../styles/tokens';
|
|
@@ -43,7 +44,7 @@ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
|
43
44
|
* tertiary-hover) that turns over while open (200ms)
|
|
44
45
|
* card (collapsed) 36px circle, transparent, just the avatar
|
|
45
46
|
* menu 265 wide, radius 16, 1px border, p10, shadow-dropdown,
|
|
46
|
-
* beside the rail (
|
|
47
|
+
* beside the rail (end side, bottom-aligned), below it under
|
|
47
48
|
* `sm`; header, grouped rows with full-bleed dividers
|
|
48
49
|
* (10px either side), footer with a version chip
|
|
49
50
|
* row p8 radius 10 gap 10, icon 20 secondary, body-medium
|
|
@@ -149,6 +150,7 @@ const SidebarTeamMenuComponent: React.FC<SidebarTeamMenuProps> = ({ team, collap
|
|
|
149
150
|
const narrow = width < BREAKPOINTS.sm;
|
|
150
151
|
const { state: hovered, onIn, onOut } = useInteractionState();
|
|
151
152
|
const reducedMotion = useReducedMotion();
|
|
153
|
+
const rtl = useIsRtl();
|
|
152
154
|
|
|
153
155
|
const turn = useSharedValue(0);
|
|
154
156
|
useEffect(() => {
|
|
@@ -168,7 +170,7 @@ const SidebarTeamMenuComponent: React.FC<SidebarTeamMenuProps> = ({ team, collap
|
|
|
168
170
|
const height = expandedHeight + (36 - expandedHeight) * p;
|
|
169
171
|
return { width: inSidebar ? '100%' : naturalWidth.value > 0 ? naturalWidth.value + (36 - naturalWidth.value) * p : p === 1 ? 36 : '100%',
|
|
170
172
|
height,
|
|
171
|
-
borderRadius: height / 2,
|
|
173
|
+
borderRadius: height / 2, paddingInlineStart: 10 + (compactPadding - 10) * p, paddingInlineEnd: 16 + (compactPadding - 16) * p };
|
|
172
174
|
}, [progress, expandedHeight, inSidebar, naturalWidth, compactPadding]);
|
|
173
175
|
const fill = useAnimatedStyle(() => ({ opacity: 1 - progress.value }), [progress]);
|
|
174
176
|
const cardStyle: WebCssStyle = {
|
|
@@ -209,7 +211,7 @@ const SidebarTeamMenuComponent: React.FC<SidebarTeamMenuProps> = ({ team, collap
|
|
|
209
211
|
<View style={{ flexDirection: 'row', alignItems: 'center', minWidth: 0, flexShrink: 1 }}>
|
|
210
212
|
<SidebarAvatarView avatar={team.avatar} size="md" palette={palette} />
|
|
211
213
|
<Collapsible collapsed={collapsed}>
|
|
212
|
-
<View style={{
|
|
214
|
+
<View style={{ paddingInlineStart: 8, justifyContent: 'center', alignItems: 'flex-start' }}>
|
|
213
215
|
<Text variant="body-medium" numberOfLines={1} style={{ color: palette.text }}>
|
|
214
216
|
{team.name}
|
|
215
217
|
</Text>
|
|
@@ -241,9 +243,11 @@ const SidebarTeamMenuComponent: React.FC<SidebarTeamMenuProps> = ({ team, collap
|
|
|
241
243
|
</AnimatedPressable>
|
|
242
244
|
</PopoverTrigger>
|
|
243
245
|
<PopoverContent
|
|
244
|
-
label={`${team.name} menu`}
|
|
245
|
-
|
|
246
|
-
|
|
246
|
+
label={team.menuLabel ?? `${team.name} menu`}
|
|
247
|
+
// Beside the rail's trailing edge: floating sides are physical, so
|
|
248
|
+
// they are mirrored here rather than by the layout.
|
|
249
|
+
side={mirrorSide(narrow ? 'bottom' : 'right', rtl)}
|
|
250
|
+
align={mirrorAlign(narrow ? 'start' : 'end', narrow ? 'bottom' : 'right', rtl)}
|
|
247
251
|
sideOffset={8}
|
|
248
252
|
maxWidth={width - 32}
|
|
249
253
|
style={SIDEBAR_MENU_PANEL}
|
|
@@ -4,6 +4,7 @@ import Animated, { useAnimatedStyle } from 'react-native-reanimated';
|
|
|
4
4
|
|
|
5
5
|
import { Button } from '../button';
|
|
6
6
|
import { useInteractionState } from '../hooks/use-interaction-state';
|
|
7
|
+
import { mirrorAlign, mirrorSide, useIsRtl } from '../hooks/use-is-rtl';
|
|
7
8
|
import { RiAddFill } from '../icons/remix/RiAddFill';
|
|
8
9
|
import { RiEqualizer3Line } from '../icons/remix/RiEqualizer3Line';
|
|
9
10
|
import { Popover, PopoverContent, PopoverTrigger } from '../popover';
|
|
@@ -36,7 +37,7 @@ const AnimatedPressable = Animated.createAnimatedComponent(Pressable);
|
|
|
36
37
|
* without moving layout. Collapsed: the rail's 36px column, the
|
|
37
38
|
* pill squared to 42×42 (3px sides).
|
|
38
39
|
* menu 265 wide, radius 16, border, p10, shadow-dropdown, beside the
|
|
39
|
-
* rail (right
|
|
40
|
+
* rail (its end side — right in LTR — top-aligned), below it under `sm`: "Users with
|
|
40
41
|
* access" (pt5, gap 6), rows px8 py6 radius 10 (20px avatar +
|
|
41
42
|
* body-medium), a full-bleed divider 14px either side, then two
|
|
42
43
|
* small secondary buttons 12 apart, pb8 so the inset reads 18
|
|
@@ -158,6 +159,7 @@ const SidebarUserMenuComponent: React.FC<SidebarUserMenuProps> = ({
|
|
|
158
159
|
const [open, setOpen] = useState(false);
|
|
159
160
|
const { width } = useWindowDimensions();
|
|
160
161
|
const narrow = width < BREAKPOINTS.sm;
|
|
162
|
+
const rtl = useIsRtl();
|
|
161
163
|
const { state: hovered, onIn, onOut } = useInteractionState();
|
|
162
164
|
|
|
163
165
|
const progress = useSidebarCollapseProgress(collapsed);
|
|
@@ -208,7 +210,7 @@ const SidebarUserMenuComponent: React.FC<SidebarUserMenuProps> = ({
|
|
|
208
210
|
/>
|
|
209
211
|
<SidebarAvatarView avatar={account.avatar} size="md" palette={palette} background={avatarBackground} />
|
|
210
212
|
<Collapsible collapsed={collapsed}>
|
|
211
|
-
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 2,
|
|
213
|
+
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 2, paddingInlineStart: 8 }}>
|
|
212
214
|
<Text variant="body-medium" numberOfLines={1} style={{ color: palette.text }}>
|
|
213
215
|
{account.name}
|
|
214
216
|
</Text>
|
|
@@ -218,9 +220,10 @@ const SidebarUserMenuComponent: React.FC<SidebarUserMenuProps> = ({
|
|
|
218
220
|
</AnimatedPressable>
|
|
219
221
|
</PopoverTrigger>
|
|
220
222
|
<PopoverContent
|
|
221
|
-
label=
|
|
222
|
-
|
|
223
|
-
|
|
223
|
+
label={account.menuLabel ?? 'Account menu'}
|
|
224
|
+
// Beside the rail's trailing edge; floating sides are physical.
|
|
225
|
+
side={mirrorSide(narrow ? 'bottom' : 'right', rtl)}
|
|
226
|
+
align={mirrorAlign('start', narrow ? 'bottom' : 'right', rtl)}
|
|
224
227
|
sideOffset={8}
|
|
225
228
|
maxWidth={width - 32}
|
|
226
229
|
style={SIDEBAR_MENU_PANEL}
|
package/src/sidebar/types.ts
CHANGED
|
@@ -28,7 +28,7 @@ export interface SidebarNavItem {
|
|
|
28
28
|
activeIcon?: SidebarIcon;
|
|
29
29
|
/** Link target. On web the row renders a real anchor. */
|
|
30
30
|
href?: string;
|
|
31
|
-
/** Counter badge
|
|
31
|
+
/** Counter badge at the row's end (the right in LTR); primary-tinted when the row is selected. */
|
|
32
32
|
badge?: string | number;
|
|
33
33
|
/**
|
|
34
34
|
* An ACTION row (Settings → a modal): intercepts the navigation. When set,
|
|
@@ -87,7 +87,7 @@ export interface SidebarRailItemProps {
|
|
|
87
87
|
activeIcon?: SidebarIcon;
|
|
88
88
|
label: string;
|
|
89
89
|
href?: string;
|
|
90
|
-
/** Sits over the indicator's top-right
|
|
90
|
+
/** Sits over the indicator's top-end corner (top-right in LTR). */
|
|
91
91
|
badge?: ReactNode;
|
|
92
92
|
selected?: boolean;
|
|
93
93
|
onPress?: () => void;
|
|
@@ -158,6 +158,8 @@ export interface SidebarTeam {
|
|
|
158
158
|
groups?: SidebarMenuGroup[];
|
|
159
159
|
/** Footer line: product name and an optional version chip. */
|
|
160
160
|
footer?: { label: string; version?: string };
|
|
161
|
+
/** Names the menu the card opens. Default `` `${name} menu` ``. */
|
|
162
|
+
menuLabel?: string;
|
|
161
163
|
}
|
|
162
164
|
|
|
163
165
|
export interface SidebarAccountUser {
|
|
@@ -210,6 +212,8 @@ export interface SidebarAccount {
|
|
|
210
212
|
onManage?: () => void;
|
|
211
213
|
addUserLabel?: string;
|
|
212
214
|
manageLabel?: string;
|
|
215
|
+
/** Names the menu the switcher opens. Default `"Account menu"`. */
|
|
216
|
+
menuLabel?: string;
|
|
213
217
|
}
|
|
214
218
|
|
|
215
219
|
export interface SidebarTeamMenuProps {
|
|
@@ -289,6 +293,15 @@ export interface SidebarProps {
|
|
|
289
293
|
/** Rendered inside a mobile drawer: always expanded, close button instead of collapse. */
|
|
290
294
|
mobile?: boolean;
|
|
291
295
|
onClose?: () => void;
|
|
296
|
+
|
|
297
|
+
/** The landmark's accessible name. Default `"Sidebar"`. */
|
|
298
|
+
accessibilityLabel?: string;
|
|
299
|
+
/** Names the collapse control while expanded. Default `"Collapse sidebar"`. */
|
|
300
|
+
collapseLabel?: string;
|
|
301
|
+
/** Names the same control while collapsed. Default `"Expand sidebar"`. */
|
|
302
|
+
expandLabel?: string;
|
|
303
|
+
/** Names the `mobile` close button. Default `"Close sidebar"`. */
|
|
304
|
+
closeLabel?: string;
|
|
292
305
|
/** Expanded width fills the container instead of the size's own width. */
|
|
293
306
|
fluid?: boolean;
|
|
294
307
|
/** The panel's edge. Defaults to `card`. */
|
|
@@ -307,8 +320,17 @@ export interface SidebarProps {
|
|
|
307
320
|
searchShortcutLabel?: string;
|
|
308
321
|
/** Binds ⌘L / Ctrl+L on web. Defaults to true. */
|
|
309
322
|
searchShortcut?: boolean;
|
|
323
|
+
/** Names the quick-search button. Default `"Quick Search"`. */
|
|
310
324
|
searchLabel?: string;
|
|
325
|
+
/** Names the search button in a flat `mobile` header. Default `"Search"`. */
|
|
326
|
+
searchButtonLabel?: string;
|
|
327
|
+
/** Placeholder. Defaults `"Search navigation…"`, or `"Search..."` in a flat `mobile` header. */
|
|
311
328
|
searchPlaceholder?: string;
|
|
329
|
+
/** Names the search field. Default `"Filter navigation"`. */
|
|
330
|
+
filterLabel?: string;
|
|
331
|
+
/** Names the button that clears and closes the search. Default `"Clear navigation search"`. */
|
|
332
|
+
clearSearchLabel?: string;
|
|
333
|
+
/** Default `"No results"`. */
|
|
312
334
|
noResultsLabel?: string;
|
|
313
335
|
|
|
314
336
|
/**
|
|
@@ -346,7 +368,7 @@ export interface SidebarTreeItem {
|
|
|
346
368
|
onLongPress?: () => void;
|
|
347
369
|
key: string;
|
|
348
370
|
label: string;
|
|
349
|
-
/** A short chip
|
|
371
|
+
/** A short chip at the row's end (the right in LTR), e.g. a relative time (`34m`, `now`). */
|
|
350
372
|
meta?: string;
|
|
351
373
|
/** Link target (a real anchor on web). */
|
|
352
374
|
href?: string;
|
|
@@ -48,6 +48,7 @@ import Animated, {
|
|
|
48
48
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
49
49
|
|
|
50
50
|
import { useHaptics } from '../hooks/use-haptics';
|
|
51
|
+
import { useDirectionProps, useIsRtl } from '../hooks/use-is-rtl';
|
|
51
52
|
import { useClaimBottomEdge } from '../layout/bottom-edge';
|
|
52
53
|
import { windowEdgeGap } from '../layout/edge';
|
|
53
54
|
import type { ProgressiveBlurProps } from '../progressive-blur/types';
|
|
@@ -178,6 +179,14 @@ function TabBarBody({
|
|
|
178
179
|
const isDragging = useSharedValue(false);
|
|
179
180
|
const lastTicked = useSharedValue(-1);
|
|
180
181
|
const { colors } = useTheme();
|
|
182
|
+
// The tabs are a flex row, so they mirror under RTL by themselves; the
|
|
183
|
+
// highlight is a `translateX` from the start edge and the scrub reads a
|
|
184
|
+
// PHYSICAL `event.x`, so both take the direction explicitly.
|
|
185
|
+
const rtl = useIsRtl();
|
|
186
|
+
const dir = rtl ? -1 : 1;
|
|
187
|
+
// The capsule's `insetInlineStart` must resolve on the same side the row
|
|
188
|
+
// mirrors to; react-native-web reads that from a `dir` prop, not `<html>`.
|
|
189
|
+
const dirProps = useDirectionProps();
|
|
181
190
|
const theme = useTabBarTheme(material === 'solid' ? { ...themeOverrides, solidFallback: themeOverrides?.solidFallback ?? colors.backgroundSecondary } : themeOverrides);
|
|
182
191
|
const impact = useHaptics();
|
|
183
192
|
|
|
@@ -310,7 +319,10 @@ function TabBarBody({
|
|
|
310
319
|
// border edge either way.)
|
|
311
320
|
const barWidth = barOuterWidth - sideInset * 2;
|
|
312
321
|
const itemWidth = (barWidth - ROW_PAD_H * 2) / tabCount;
|
|
313
|
-
|
|
322
|
+
// Under RTL the first tab is at the right edge, so distance is measured
|
|
323
|
+
// from there instead.
|
|
324
|
+
const fromStart = rtl ? barWidth - x : x;
|
|
325
|
+
const raw = (fromStart - ROW_PAD_H) / itemWidth - 0.5;
|
|
314
326
|
return Math.min(Math.max(raw, 0), tabCount - 1);
|
|
315
327
|
};
|
|
316
328
|
|
|
@@ -413,6 +425,7 @@ function TabBarBody({
|
|
|
413
425
|
highlightOpacity,
|
|
414
426
|
minimized,
|
|
415
427
|
progress,
|
|
428
|
+
rtl,
|
|
416
429
|
]);
|
|
417
430
|
|
|
418
431
|
// CRITICAL — every shared value and every scalar a mapper READS must appear
|
|
@@ -497,9 +510,9 @@ function TabBarBody({
|
|
|
497
510
|
// out-of-range one is a real place — one item-width to the LEFT of the
|
|
498
511
|
// first tab, i.e. half outside the pill — not an absence.
|
|
499
512
|
opacity: highlightOpacity.value,
|
|
500
|
-
transform: [{ translateX: ROW_PAD_H + itemWidth * slideIndex.value }],
|
|
513
|
+
transform: [{ translateX: dir * (ROW_PAD_H + itemWidth * slideIndex.value) }],
|
|
501
514
|
};
|
|
502
|
-
}, [progress, slideIndex, highlightOpacity, barOuterWidth, tabCount, minimizeInset]);
|
|
515
|
+
}, [progress, slideIndex, highlightOpacity, barOuterWidth, tabCount, minimizeInset, dir]);
|
|
503
516
|
|
|
504
517
|
// Shared with `useTabBarFootprint`, so a consumer accounting for the bar in
|
|
505
518
|
// its own layout can never drift from where the bar actually sits.
|
|
@@ -534,7 +547,7 @@ function TabBarBody({
|
|
|
534
547
|
|
|
535
548
|
const ResolvedSurface = material === 'solid' ? SolidTabBarSurface : Surface;
|
|
536
549
|
return (
|
|
537
|
-
<View {...viewProps} onLayout={(event) => { setContainerWidth(event.nativeEvent.layout.width); onLayout?.(event); }} pointerEvents="box-none" style={[embedded ? { width: '100%' } : styles.root, style]}>
|
|
550
|
+
<View {...viewProps} {...dirProps} onLayout={(event) => { setContainerWidth(event.nativeEvent.layout.width); onLayout?.(event); }} pointerEvents="box-none" style={[embedded ? { width: '100%' } : styles.root, style]}>
|
|
538
551
|
{/* Progressive blur rising from the screen's bottom edge behind the pill.
|
|
539
552
|
Rendered CONDITIONALLY, and as nothing at all when off: the band is
|
|
540
553
|
full-bleed and 114pt tall at a zero bottom inset, so it blurs whatever
|
|
@@ -784,7 +797,8 @@ const styles = StyleSheet.create({
|
|
|
784
797
|
},
|
|
785
798
|
highlight: {
|
|
786
799
|
position: 'absolute',
|
|
787
|
-
|
|
800
|
+
// The START edge: the translate above runs from it, signed by direction.
|
|
801
|
+
insetInlineStart: 0,
|
|
788
802
|
borderCurve: 'continuous',
|
|
789
803
|
},
|
|
790
804
|
itemRow: {
|
|
@@ -9,6 +9,7 @@ import React, {
|
|
|
9
9
|
useState,
|
|
10
10
|
} from 'react';
|
|
11
11
|
import {
|
|
12
|
+
Image,
|
|
12
13
|
Linking,
|
|
13
14
|
Platform,
|
|
14
15
|
Pressable,
|
|
@@ -373,6 +374,7 @@ function SourceMark({
|
|
|
373
374
|
);
|
|
374
375
|
const tx = useSharedValue(0);
|
|
375
376
|
const ty = useSharedValue(0);
|
|
377
|
+
const [faviconFailed, setFaviconFailed] = useState(false);
|
|
376
378
|
|
|
377
379
|
useLayoutEffect(() => {
|
|
378
380
|
const node = ref.current;
|
|
@@ -451,6 +453,13 @@ function SourceMark({
|
|
|
451
453
|
</View>
|
|
452
454
|
) : source.brand ? (
|
|
453
455
|
<BrandMark brand={source.brand} size={12} palette={palette} />
|
|
456
|
+
) : source.faviconUrl && !faviconFailed ? (
|
|
457
|
+
<Image
|
|
458
|
+
source={{ uri: source.faviconUrl }}
|
|
459
|
+
onError={() => setFaviconFailed(true)}
|
|
460
|
+
resizeMode="contain"
|
|
461
|
+
style={{ width: 12, height: 12, borderRadius: 2 }}
|
|
462
|
+
/>
|
|
454
463
|
) : (
|
|
455
464
|
<View style={{ width: 8, height: 8, borderRadius: 4, backgroundColor: palette.markDot }} />
|
|
456
465
|
)}
|
package/src/web-search/types.ts
CHANGED
|
@@ -21,6 +21,12 @@ export interface WebSearchSource {
|
|
|
21
21
|
href?: string;
|
|
22
22
|
/** Draws that site's real mark in its own colour. */
|
|
23
23
|
brand?: WebSearchBrand;
|
|
24
|
+
/**
|
|
25
|
+
* The site's favicon, for a site with no brand mark: an image URL drawn 12×12
|
|
26
|
+
* in the mark. A brand mark wins over it; an image that fails to load falls
|
|
27
|
+
* back to the dot, so a missing icon never leaves a broken one.
|
|
28
|
+
*/
|
|
29
|
+
faviconUrl?: string;
|
|
24
30
|
/**
|
|
25
31
|
* A glyph for anything outside the brand marks — a docs site, a customer's own
|
|
26
32
|
* product. Supplied ready-made and sized by the caller (the slot is 12×12).
|