@oxy.so/bloom 4.14.1 → 4.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/docs/app-shell.mdx +74 -6
  2. package/docs/bottom-bar.mdx +9 -1
  3. package/docs/hooks.mdx +23 -0
  4. package/docs/sidebar.mdx +15 -2
  5. package/docs/web-search.mdx +1 -1
  6. package/lib/commonjs/app-shell/AppShell.js +3 -1
  7. package/lib/commonjs/app-shell/AppShell.js.map +1 -1
  8. package/lib/commonjs/app-shell/AppShellEngine.js +54 -28
  9. package/lib/commonjs/app-shell/AppShellEngine.js.map +1 -1
  10. package/lib/commonjs/app-shell/AppShellHeader.js +4 -3
  11. package/lib/commonjs/app-shell/AppShellHeader.js.map +1 -1
  12. package/lib/commonjs/app-shell/NotificationBell.js +8 -4
  13. package/lib/commonjs/app-shell/NotificationBell.js.map +1 -1
  14. package/lib/commonjs/app-shell/ProOfferCard.js +19 -11
  15. package/lib/commonjs/app-shell/ProOfferCard.js.map +1 -1
  16. package/lib/commonjs/bottom-bar/BottomBarBase.js +9 -2
  17. package/lib/commonjs/bottom-bar/BottomBarBase.js.map +1 -1
  18. package/lib/commonjs/hooks/index.js +7 -0
  19. package/lib/commonjs/hooks/index.js.map +1 -1
  20. package/lib/commonjs/hooks/use-is-rtl.js +104 -0
  21. package/lib/commonjs/hooks/use-is-rtl.js.map +1 -0
  22. package/lib/commonjs/sidebar/Sidebar.js +41 -22
  23. package/lib/commonjs/sidebar/Sidebar.js.map +1 -1
  24. package/lib/commonjs/sidebar/SidebarFolder.js +11 -5
  25. package/lib/commonjs/sidebar/SidebarFolder.js.map +1 -1
  26. package/lib/commonjs/sidebar/SidebarLogoView.js +2 -2
  27. package/lib/commonjs/sidebar/SidebarLogoView.js.map +1 -1
  28. package/lib/commonjs/sidebar/SidebarModeSwitcher.js +1 -1
  29. package/lib/commonjs/sidebar/SidebarModeSwitcher.js.map +1 -1
  30. package/lib/commonjs/sidebar/SidebarPlanCard.js +3 -3
  31. package/lib/commonjs/sidebar/SidebarPlanCard.js.map +1 -1
  32. package/lib/commonjs/sidebar/SidebarRail.js +7 -1
  33. package/lib/commonjs/sidebar/SidebarRail.js.map +1 -1
  34. package/lib/commonjs/sidebar/SidebarRailItem.js +2 -2
  35. package/lib/commonjs/sidebar/SidebarRailItem.js.map +1 -1
  36. package/lib/commonjs/sidebar/SidebarTeamMenu.js +12 -7
  37. package/lib/commonjs/sidebar/SidebarTeamMenu.js.map +1 -1
  38. package/lib/commonjs/sidebar/SidebarUserMenu.js +9 -5
  39. package/lib/commonjs/sidebar/SidebarUserMenu.js.map +1 -1
  40. package/lib/commonjs/tab-bar/TabBarBase.js +19 -5
  41. package/lib/commonjs/tab-bar/TabBarBase.js.map +1 -1
  42. package/lib/commonjs/web-search/WebSearch.js +12 -0
  43. package/lib/commonjs/web-search/WebSearch.js.map +1 -1
  44. package/lib/module/app-shell/AppShell.js +3 -1
  45. package/lib/module/app-shell/AppShell.js.map +1 -1
  46. package/lib/module/app-shell/AppShellEngine.js +54 -28
  47. package/lib/module/app-shell/AppShellEngine.js.map +1 -1
  48. package/lib/module/app-shell/AppShellHeader.js +4 -3
  49. package/lib/module/app-shell/AppShellHeader.js.map +1 -1
  50. package/lib/module/app-shell/NotificationBell.js +8 -4
  51. package/lib/module/app-shell/NotificationBell.js.map +1 -1
  52. package/lib/module/app-shell/ProOfferCard.js +19 -11
  53. package/lib/module/app-shell/ProOfferCard.js.map +1 -1
  54. package/lib/module/bottom-bar/BottomBarBase.js +9 -2
  55. package/lib/module/bottom-bar/BottomBarBase.js.map +1 -1
  56. package/lib/module/hooks/index.js +1 -0
  57. package/lib/module/hooks/index.js.map +1 -1
  58. package/lib/module/hooks/use-is-rtl.js +97 -0
  59. package/lib/module/hooks/use-is-rtl.js.map +1 -0
  60. package/lib/module/sidebar/Sidebar.js +41 -22
  61. package/lib/module/sidebar/Sidebar.js.map +1 -1
  62. package/lib/module/sidebar/SidebarFolder.js +11 -5
  63. package/lib/module/sidebar/SidebarFolder.js.map +1 -1
  64. package/lib/module/sidebar/SidebarLogoView.js +2 -2
  65. package/lib/module/sidebar/SidebarLogoView.js.map +1 -1
  66. package/lib/module/sidebar/SidebarModeSwitcher.js +1 -1
  67. package/lib/module/sidebar/SidebarModeSwitcher.js.map +1 -1
  68. package/lib/module/sidebar/SidebarPlanCard.js +3 -3
  69. package/lib/module/sidebar/SidebarPlanCard.js.map +1 -1
  70. package/lib/module/sidebar/SidebarRail.js +7 -1
  71. package/lib/module/sidebar/SidebarRail.js.map +1 -1
  72. package/lib/module/sidebar/SidebarRailItem.js +2 -2
  73. package/lib/module/sidebar/SidebarRailItem.js.map +1 -1
  74. package/lib/module/sidebar/SidebarTeamMenu.js +12 -7
  75. package/lib/module/sidebar/SidebarTeamMenu.js.map +1 -1
  76. package/lib/module/sidebar/SidebarUserMenu.js +9 -5
  77. package/lib/module/sidebar/SidebarUserMenu.js.map +1 -1
  78. package/lib/module/tab-bar/TabBarBase.js +19 -5
  79. package/lib/module/tab-bar/TabBarBase.js.map +1 -1
  80. package/lib/module/web-search/WebSearch.js +13 -1
  81. package/lib/module/web-search/WebSearch.js.map +1 -1
  82. package/lib/typescript/commonjs/app-shell/AppShellEngine.d.ts.map +1 -1
  83. package/lib/typescript/commonjs/app-shell/AppShellHeader.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/app-shell/NotificationBell.d.ts.map +1 -1
  85. package/lib/typescript/commonjs/app-shell/ProOfferCard.d.ts.map +1 -1
  86. package/lib/typescript/commonjs/app-shell/types.d.ts +16 -5
  87. package/lib/typescript/commonjs/app-shell/types.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/bottom-bar/BottomBarBase.d.ts.map +1 -1
  89. package/lib/typescript/commonjs/hooks/index.d.ts +1 -0
  90. package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/hooks/use-is-rtl.d.ts +53 -0
  92. package/lib/typescript/commonjs/hooks/use-is-rtl.d.ts.map +1 -0
  93. package/lib/typescript/commonjs/icons/shared.d.ts +10 -10
  94. package/lib/typescript/commonjs/sidebar/Sidebar.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/sidebar/SidebarFolder.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/sidebar/SidebarRail.d.ts +1 -1
  97. package/lib/typescript/commonjs/sidebar/SidebarRail.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/sidebar/SidebarTeamMenu.d.ts.map +1 -1
  99. package/lib/typescript/commonjs/sidebar/SidebarUserMenu.d.ts.map +1 -1
  100. package/lib/typescript/commonjs/sidebar/types.d.ts +24 -3
  101. package/lib/typescript/commonjs/sidebar/types.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/tab-bar/TabBarBase.d.ts.map +1 -1
  103. package/lib/typescript/commonjs/web-search/WebSearch.d.ts.map +1 -1
  104. package/lib/typescript/commonjs/web-search/types.d.ts +6 -0
  105. package/lib/typescript/commonjs/web-search/types.d.ts.map +1 -1
  106. package/lib/typescript/module/app-shell/AppShellEngine.d.ts.map +1 -1
  107. package/lib/typescript/module/app-shell/AppShellHeader.d.ts.map +1 -1
  108. package/lib/typescript/module/app-shell/NotificationBell.d.ts.map +1 -1
  109. package/lib/typescript/module/app-shell/ProOfferCard.d.ts.map +1 -1
  110. package/lib/typescript/module/app-shell/types.d.ts +16 -5
  111. package/lib/typescript/module/app-shell/types.d.ts.map +1 -1
  112. package/lib/typescript/module/bottom-bar/BottomBarBase.d.ts.map +1 -1
  113. package/lib/typescript/module/hooks/index.d.ts +1 -0
  114. package/lib/typescript/module/hooks/index.d.ts.map +1 -1
  115. package/lib/typescript/module/hooks/use-is-rtl.d.ts +53 -0
  116. package/lib/typescript/module/hooks/use-is-rtl.d.ts.map +1 -0
  117. package/lib/typescript/module/icons/shared.d.ts +10 -10
  118. package/lib/typescript/module/sidebar/Sidebar.d.ts.map +1 -1
  119. package/lib/typescript/module/sidebar/SidebarFolder.d.ts.map +1 -1
  120. package/lib/typescript/module/sidebar/SidebarRail.d.ts +1 -1
  121. package/lib/typescript/module/sidebar/SidebarRail.d.ts.map +1 -1
  122. package/lib/typescript/module/sidebar/SidebarTeamMenu.d.ts.map +1 -1
  123. package/lib/typescript/module/sidebar/SidebarUserMenu.d.ts.map +1 -1
  124. package/lib/typescript/module/sidebar/types.d.ts +24 -3
  125. package/lib/typescript/module/sidebar/types.d.ts.map +1 -1
  126. package/lib/typescript/module/tab-bar/TabBarBase.d.ts.map +1 -1
  127. package/lib/typescript/module/web-search/WebSearch.d.ts.map +1 -1
  128. package/lib/typescript/module/web-search/types.d.ts +6 -0
  129. package/lib/typescript/module/web-search/types.d.ts.map +1 -1
  130. package/package.json +1 -1
  131. package/src/app-shell/AppShell.tsx +1 -1
  132. package/src/app-shell/AppShellEngine.tsx +45 -24
  133. package/src/app-shell/AppShellHeader.tsx +4 -3
  134. package/src/app-shell/NotificationBell.tsx +8 -4
  135. package/src/app-shell/ProOfferCard.tsx +18 -10
  136. package/src/app-shell/types.ts +16 -5
  137. package/src/bottom-bar/BottomBarBase.tsx +9 -3
  138. package/src/hooks/index.ts +1 -0
  139. package/src/hooks/use-is-rtl.ts +102 -0
  140. package/src/sidebar/Sidebar.tsx +41 -21
  141. package/src/sidebar/SidebarFolder.tsx +7 -4
  142. package/src/sidebar/SidebarLogoView.tsx +2 -2
  143. package/src/sidebar/SidebarModeSwitcher.tsx +1 -1
  144. package/src/sidebar/SidebarPlanCard.tsx +3 -3
  145. package/src/sidebar/SidebarRail.tsx +7 -1
  146. package/src/sidebar/SidebarRailItem.tsx +2 -2
  147. package/src/sidebar/SidebarTeamMenu.tsx +10 -6
  148. package/src/sidebar/SidebarUserMenu.tsx +8 -5
  149. package/src/sidebar/types.ts +25 -3
  150. package/src/tab-bar/TabBarBase.tsx +19 -5
  151. package/src/web-search/WebSearch.tsx +9 -0
  152. package/src/web-search/types.ts +6 -0
@@ -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="Clear navigation search"
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
- right: (hasLogo ? 28 : 20) * (1 - progress.value),
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
- left: `${100 * (1 - p)}%` as `${number}%`,
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 — because a column
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
- borderRightWidth: 1,
381
- borderRightColor: palette.dockedEdge,
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, paddingLeft: 8, paddingRight: 10, boxShadow: searchRing }
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="Search"
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="Filter navigation"
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="Close sidebar"
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 ? 'Expand sidebar' : 'Collapse sidebar'}
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
- paddingLeft: 8,
480
- paddingRight: 10,
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="Filter navigation"
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', left: 0, minWidth: 0 }, headerIdentityGeometry]}>
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="Sidebar"
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', left: 0, top: 0 }, themeExpandedStyle]}>
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
- * right of it and running on to 11.5.
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, left: 16.5, width: 12, height }}>
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
- paddingRight: 8,
73
- paddingLeft: 36,
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 to the right, in a collapse slot; a string renders as
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, paddingLeft: icon != null ? 8 : 0, justifyContent: 'center' }} testID={testID ? `${testID}-wordmark` : undefined}>
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, paddingRight: 12, opacity: hovered ? 1 : 0 }}
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
- paddingLeft: 10 + (compactPadding - 10) * progress.value,
35
- paddingRight: 12 + (compactPadding - 12) * progress.value,
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={{ paddingLeft: 8, minWidth: 0, flexShrink: 1, flexDirection: 'column', alignItems: 'flex-start', justifyContent: 'center' }}>
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="Sidebar"
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 corner
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, left: INDICATOR_WIDTH - 16 }}>
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 (right, bottom-aligned), below it under
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, paddingLeft: 10 + (compactPadding - 10) * p, paddingRight: 16 + (compactPadding - 16) * p };
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={{ paddingLeft: 8, justifyContent: 'center', alignItems: 'flex-start' }}>
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
- side={narrow ? 'bottom' : 'right'}
246
- align={narrow ? 'start' : 'end'}
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, top-aligned), below it under `sm`: "Users with
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, paddingLeft: 8 }}>
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="Account menu"
222
- side={narrow ? 'bottom' : 'right'}
223
- align="start"
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}
@@ -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 on the right; primary-tinted when the row is selected. */
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 corner. */
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 on the right, e.g. a relative time (`34m`, `now`). */
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
- const raw = (x - ROW_PAD_H) / itemWidth - 0.5;
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
- left: 0,
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
  )}
@@ -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).