@oxy.so/bloom 4.1.0 → 4.3.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 (93) hide show
  1. package/docs/adoption-matrix.mdx +7 -7
  2. package/docs/ai-chat.mdx +8 -0
  3. package/docs/app-shell.mdx +7 -0
  4. package/docs/bottom-bar.mdx +6 -0
  5. package/docs/settings-modal.mdx +14 -5
  6. package/docs/sidebar.mdx +11 -0
  7. package/lib/commonjs/ai-chat/AiChatContainer.js +8 -8
  8. package/lib/commonjs/ai-chat/AiChatContainer.js.map +1 -1
  9. package/lib/commonjs/app-shell/AppShellBars.js +8 -2
  10. package/lib/commonjs/app-shell/AppShellBars.js.map +1 -1
  11. package/lib/commonjs/app-shell/AppShellEngine.js +29 -6
  12. package/lib/commonjs/app-shell/AppShellEngine.js.map +1 -1
  13. package/lib/commonjs/bottom-bar/BottomBarBase.js +10 -2
  14. package/lib/commonjs/bottom-bar/BottomBarBase.js.map +1 -1
  15. package/lib/commonjs/layout/bottom-bar-slot.js +10 -0
  16. package/lib/commonjs/layout/bottom-bar-slot.js.map +1 -0
  17. package/lib/commonjs/settings-modal/SettingsModal.js +153 -230
  18. package/lib/commonjs/settings-modal/SettingsModal.js.map +1 -1
  19. package/lib/commonjs/sidebar/Sidebar.js +35 -5
  20. package/lib/commonjs/sidebar/Sidebar.js.map +1 -1
  21. package/lib/module/ai-chat/AiChatContainer.js +8 -8
  22. package/lib/module/ai-chat/AiChatContainer.js.map +1 -1
  23. package/lib/module/app-shell/AppShellBars.js +8 -2
  24. package/lib/module/app-shell/AppShellBars.js.map +1 -1
  25. package/lib/module/app-shell/AppShellEngine.js +30 -7
  26. package/lib/module/app-shell/AppShellEngine.js.map +1 -1
  27. package/lib/module/bottom-bar/BottomBarBase.js +11 -3
  28. package/lib/module/bottom-bar/BottomBarBase.js.map +1 -1
  29. package/lib/module/layout/bottom-bar-slot.js +7 -0
  30. package/lib/module/layout/bottom-bar-slot.js.map +1 -0
  31. package/lib/module/settings-modal/SettingsModal.js +150 -227
  32. package/lib/module/settings-modal/SettingsModal.js.map +1 -1
  33. package/lib/module/sidebar/Sidebar.js +35 -5
  34. package/lib/module/sidebar/Sidebar.js.map +1 -1
  35. package/lib/typescript/commonjs/ai-chat/types.d.ts +2 -2
  36. package/lib/typescript/commonjs/ai-chat/types.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/app-shell/AppShellBars.d.ts +1 -0
  38. package/lib/typescript/commonjs/app-shell/AppShellBars.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/app-shell/AppShellEngine.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/bottom-bar/BottomBarBase.d.ts +1 -1
  41. package/lib/typescript/commonjs/bottom-bar/BottomBarBase.d.ts.map +1 -1
  42. package/lib/typescript/commonjs/bottom-bar/types.d.ts +4 -0
  43. package/lib/typescript/commonjs/bottom-bar/types.d.ts.map +1 -1
  44. package/lib/typescript/commonjs/layout/bottom-bar-slot.d.ts +3 -0
  45. package/lib/typescript/commonjs/layout/bottom-bar-slot.d.ts.map +1 -0
  46. package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts +1 -1
  47. package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts.map +1 -1
  48. package/lib/typescript/commonjs/settings-modal/types.d.ts +7 -2
  49. package/lib/typescript/commonjs/settings-modal/types.d.ts.map +1 -1
  50. package/lib/typescript/commonjs/sidebar/Sidebar.d.ts.map +1 -1
  51. package/lib/typescript/commonjs/sidebar/types.d.ts +12 -1
  52. package/lib/typescript/commonjs/sidebar/types.d.ts.map +1 -1
  53. package/lib/typescript/module/ai-chat/types.d.ts +2 -2
  54. package/lib/typescript/module/ai-chat/types.d.ts.map +1 -1
  55. package/lib/typescript/module/app-shell/AppShellBars.d.ts +1 -0
  56. package/lib/typescript/module/app-shell/AppShellBars.d.ts.map +1 -1
  57. package/lib/typescript/module/app-shell/AppShellEngine.d.ts.map +1 -1
  58. package/lib/typescript/module/bottom-bar/BottomBarBase.d.ts +1 -1
  59. package/lib/typescript/module/bottom-bar/BottomBarBase.d.ts.map +1 -1
  60. package/lib/typescript/module/bottom-bar/types.d.ts +4 -0
  61. package/lib/typescript/module/bottom-bar/types.d.ts.map +1 -1
  62. package/lib/typescript/module/layout/bottom-bar-slot.d.ts +3 -0
  63. package/lib/typescript/module/layout/bottom-bar-slot.d.ts.map +1 -0
  64. package/lib/typescript/module/settings-modal/SettingsModal.d.ts +1 -1
  65. package/lib/typescript/module/settings-modal/SettingsModal.d.ts.map +1 -1
  66. package/lib/typescript/module/settings-modal/types.d.ts +7 -2
  67. package/lib/typescript/module/settings-modal/types.d.ts.map +1 -1
  68. package/lib/typescript/module/sidebar/Sidebar.d.ts.map +1 -1
  69. package/lib/typescript/module/sidebar/types.d.ts +12 -1
  70. package/lib/typescript/module/sidebar/types.d.ts.map +1 -1
  71. package/package.json +1 -1
  72. package/src/__tests__/support/adoption-matrix.ts +703 -0
  73. package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
  74. package/src/__tests__/support/commerce-harness.tsx +97 -0
  75. package/src/__tests__/support/composite.ts +68 -0
  76. package/src/__tests__/support/constructed-style-sheets.ts +68 -0
  77. package/src/__tests__/support/press-host.ts +30 -0
  78. package/src/__tests__/support/rendered-style.ts +99 -0
  79. package/src/__tests__/support/unread-hook-fixture.ts +33 -0
  80. package/src/ai-chat/AiChatContainer.tsx +7 -7
  81. package/src/ai-chat/types.ts +2 -2
  82. package/src/app-shell/AppShellBars.tsx +5 -3
  83. package/src/app-shell/AppShellEngine.tsx +23 -8
  84. package/src/bottom-bar/BottomBarBase.tsx +7 -4
  85. package/src/bottom-bar/types.ts +4 -0
  86. package/src/layout/bottom-bar-slot.ts +4 -0
  87. package/src/settings-modal/SettingsModal.tsx +180 -250
  88. package/src/settings-modal/types.ts +7 -2
  89. package/src/sidebar/Sidebar.tsx +26 -5
  90. package/src/sidebar/types.ts +12 -1
  91. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +9346 -0
  92. package/src/theme/__tests__/__fixtures__/tonal-glass-primary-failures.json +69 -0
  93. package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
@@ -1,14 +1,14 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react';
4
- import { BackHandler, Pressable, ScrollView, StyleSheet, View, useWindowDimensions } from 'react-native';
5
- import Animated, { Easing, useAnimatedStyle, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
4
+ import { BackHandler, Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
5
+ import Animated, { Easing, useAnimatedStyle, useAnimatedScrollHandler, useReducedMotion, useSharedValue, withTiming } from 'react-native-reanimated';
6
6
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
7
- import { RiArrowLeftSLine } from "../icons/remix/RiArrowLeftSLine.js";
8
7
  import { RiArrowRightSLine } from "../icons/remix/RiArrowRightSLine.js";
9
8
  import { RiCheckboxCircleFill } from "../icons/remix/RiCheckboxCircleFill.js";
9
+ import { PageHeader } from "../page-header/index.js";
10
+ import { ButtonGroup, ButtonGroupItem } from "../button-group/index.js";
10
11
  import { RiCloseLine } from "../icons/remix/RiCloseLine.js";
11
- import { VerticalFade } from "./SettingsArt.js";
12
12
  import { Backdrop, OverlayRoot } from "../overlay/index.js";
13
13
  import { webDataSet } from "../styles/web-data.js";
14
14
  import { Text } from "../typography/index.js";
@@ -28,10 +28,8 @@ import { IS_WEB, useSettingsWebCss } from "./web-css.js";
28
28
  * 20px icon (icon/secondary) + 8 + body-medium label
29
29
  * (selected: background/secondary/hover + text/primary;
30
30
  * hover: background/secondary/hover @ 60%)
31
- * content title row px 32 pt 32 pb 12 (Storage 6), title-3-medium +
32
- * 24px round close (background/tertiary, hover tertiary/hover,
33
- * 16px glyph); the page scrolls under a 40px top fade that eases
34
- * in (200ms) once it is scrolled; px 32 pb 32
31
+ * content one viewport with a sticky PageHeader and its scroll-linked edge;
32
+ * content insets belong to the page body, never the viewport
35
33
  * toast "Saved" pill straddling the panel's bottom edge: 1px
36
34
  * border/button, background/primary, py 4 pr 10 pl 6, gap 4,
37
35
  * 16px lime check + body-2-medium, shadow-dropdown. Rises 12px in,
@@ -51,7 +49,7 @@ import { IS_WEB, useSettingsWebCss } from "./web-css.js";
51
49
  * rank) and `Backdrop` (the pointer-events contract) — and takes Dialog's
52
50
  * `control` from `useDialogControl()`, so it is driven exactly like one.
53
51
  */
54
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
52
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
55
53
  const PANEL_WIDTH = 871;
56
54
  const PANEL_HEIGHT = 614;
57
55
  const RAIL_WIDTH = 274;
@@ -97,11 +95,13 @@ export function SettingsModal({
97
95
  control,
98
96
  open: controlledOpen,
99
97
  onClose,
98
+ onBeforeLeave,
100
99
  groups,
101
100
  pages,
102
101
  page: controlledPage,
103
102
  defaultPage,
104
103
  onPageChange,
104
+ initialView = 'navigation',
105
105
  labels,
106
106
  testID
107
107
  }) {
@@ -125,7 +125,12 @@ export function SettingsModal({
125
125
  const insets = useSafeAreaInsets();
126
126
 
127
127
  // `compact` layout is two levels: the section list, then a page pushed over it.
128
- const [compactPageOpen, setCompactPageOpen] = useState(false);
128
+ const [compactPageOpen, setCompactPageOpen] = useState(initialView === 'page');
129
+ const initialViewRef = useRef(initialView);
130
+ initialViewRef.current = initialView;
131
+ const beforeLeaveRef = useRef(onBeforeLeave);
132
+ beforeLeaveRef.current = onBeforeLeave;
133
+ const mayLeave = useCallback(reason => beforeLeaveRef.current?.(reason) !== false, []);
129
134
  const onCloseRef = useRef(onClose);
130
135
  onCloseRef.current = onClose;
131
136
  const isControlledRef = useRef(isControlled);
@@ -134,7 +139,7 @@ export function SettingsModal({
134
139
  defaultPageRef.current = defaultPage ?? firstPage(groups);
135
140
  const show = useCallback(() => {
136
141
  setInternalPage(defaultPageRef.current);
137
- setCompactPageOpen(false);
142
+ setCompactPageOpen(initialViewRef.current === 'page');
138
143
  setMounted(true);
139
144
  setOpenCount(count => count + 1);
140
145
  }, []);
@@ -171,20 +176,25 @@ export function SettingsModal({
171
176
  if (controlledOpen) show();else hide();
172
177
  }, [isControlled, controlledOpen, show, hide]);
173
178
  const requestClose = useCallback(() => {
179
+ if (!mayLeave('close')) return false;
174
180
  if (isControlledRef.current) onCloseRef.current?.();else hide();
175
- }, [hide]);
181
+ return true;
182
+ }, [hide, mayLeave]);
176
183
  useImperativeHandle(control?.ref, () => ({
177
184
  open: show,
178
185
  close: cb => {
179
- hide();
180
- if (cb) setTimeout(cb, UNMOUNT_MS);
186
+ if (requestClose() && cb) setTimeout(cb, UNMOUNT_MS);
181
187
  }
182
- }), [show, hide]);
188
+ }), [show, requestClose]);
183
189
  const selectPage = useCallback(next => {
190
+ if (next !== currentPage && !mayLeave('page')) return;
184
191
  if (controlledPage === undefined) setInternalPage(next);
185
192
  setCompactPageOpen(true);
186
193
  onPageChange?.(next);
187
- }, [controlledPage, onPageChange]);
194
+ }, [controlledPage, currentPage, onPageChange, mayLeave]);
195
+ const requestNavigation = useCallback(() => {
196
+ if (mayLeave('navigation')) setCompactPageOpen(false);
197
+ }, [mayLeave]);
188
198
 
189
199
  // Escape closes (web). On `window`, not `document`: a menu or popover opened
190
200
  // inside the modal handles Escape on `document` and stops propagation there,
@@ -209,11 +219,11 @@ export function SettingsModal({
209
219
  if (!mounted || IS_WEB || typeof BackHandler?.addEventListener !== 'function') return;
210
220
  const sub = BackHandler.addEventListener('hardwareBackPress', () => {
211
221
  // On a pushed page (compact), back returns to the section list first.
212
- if (compactPageOpenRef.current) setCompactPageOpen(false);else requestClose();
222
+ if (compactPageOpenRef.current) requestNavigation();else requestClose();
213
223
  return true;
214
224
  });
215
225
  return () => sub.remove();
216
- }, [mounted, requestClose]);
226
+ }, [mounted, requestClose, requestNavigation]);
217
227
 
218
228
  // ----- saved toast -------------------------------------------------------
219
229
  const [savedPhase, setSavedPhase] = useState('hidden');
@@ -323,32 +333,30 @@ export function SettingsModal({
323
333
  backgroundColor: palette.full,
324
334
  boxShadow: palette.shadowXs
325
335
  }, ringVars(palette)],
326
- children: layout === 'compact' ? compactPageOpen && pageConfig ? /*#__PURE__*/_jsxs(View, {
336
+ children: layout === 'compact' ? compactPageOpen && pageConfig ? /*#__PURE__*/_jsx(View, {
327
337
  style: styles.content,
328
- children: [/*#__PURE__*/_jsx(CompactHeader, {
329
- title: pageConfig.title,
330
- onBack: () => setCompactPageOpen(false),
331
- backLabel: labels?.back ?? 'Back',
332
- closeLabel: closeLabel,
333
- onClose: requestClose,
334
- palette: palette,
335
- testID: testID
336
- }), /*#__PURE__*/_jsx(PageScroller, {
338
+ children: /*#__PURE__*/_jsx(PageScroller, {
339
+ header: {
340
+ title: pageConfig.title,
341
+ onBack: requestNavigation,
342
+ backLabel: labels?.back ?? 'Back',
343
+ closeLabel,
344
+ onClose: requestClose
345
+ },
337
346
  palette: palette,
338
347
  inset: CONTENT_INSET.compact,
339
348
  insetTop: CONTENT_INSET.compact,
340
349
  testID: testID,
341
350
  children: pageConfig.content
342
- }, currentPage)]
343
- }) : /*#__PURE__*/_jsxs(View, {
351
+ }, currentPage)
352
+ }) : /*#__PURE__*/_jsx(View, {
344
353
  style: styles.content,
345
- children: [/*#__PURE__*/_jsx(CompactHeader, {
346
- title: labels?.dialog ?? 'Settings',
347
- closeLabel: closeLabel,
348
- onClose: requestClose,
349
- palette: palette,
350
- testID: testID
351
- }), /*#__PURE__*/_jsx(SettingsRail, {
354
+ children: /*#__PURE__*/_jsx(SettingsRail, {
355
+ header: {
356
+ title: labels?.dialog ?? 'Settings',
357
+ closeLabel,
358
+ onClose: requestClose
359
+ },
352
360
  groups: groups,
353
361
  page: currentPage,
354
362
  onSelect: selectPage,
@@ -356,7 +364,7 @@ export function SettingsModal({
356
364
  label: labels?.nav ?? 'Settings sections',
357
365
  layout: "compact",
358
366
  testID: testID
359
- })]
367
+ })
360
368
  }) : /*#__PURE__*/_jsxs(_Fragment, {
361
369
  children: [/*#__PURE__*/_jsx(SettingsRail, {
362
370
  groups: groups,
@@ -366,35 +374,20 @@ export function SettingsModal({
366
374
  label: labels?.nav ?? 'Settings sections',
367
375
  layout: layout,
368
376
  testID: testID
369
- }), /*#__PURE__*/_jsxs(View, {
377
+ }), /*#__PURE__*/_jsx(View, {
370
378
  style: styles.content,
371
- children: [/*#__PURE__*/_jsxs(View, {
372
- style: [styles.titleRow, {
373
- paddingLeft: CONTENT_INSET[layout],
374
- paddingRight: CONTENT_INSET[layout],
375
- paddingTop: CONTENT_INSET[layout],
376
- paddingBottom: pageConfig?.compactTitle ? 6 : 12
377
- }],
378
- testID: testID ? `${testID}-title-row` : undefined,
379
- children: [/*#__PURE__*/_jsx(Text, {
380
- variant: "title-3-medium",
381
- role: "heading",
382
- style: {
383
- color: palette.text
384
- },
385
- children: pageConfig?.title ?? ''
386
- }), /*#__PURE__*/_jsx(SettingsCloseButton, {
387
- label: closeLabel,
388
- onPress: requestClose,
389
- palette: palette,
390
- testID: testID ? `${testID}-close` : undefined
391
- })]
392
- }), /*#__PURE__*/_jsx(PageScroller, {
379
+ children: /*#__PURE__*/_jsx(PageScroller, {
380
+ header: {
381
+ title: pageConfig?.title ?? '',
382
+ closeLabel,
383
+ onClose: requestClose
384
+ },
393
385
  palette: palette,
394
386
  inset: CONTENT_INSET[layout],
387
+ insetTop: pageConfig?.compactTitle ? 6 : 12,
395
388
  testID: testID,
396
389
  children: pageConfig?.content
397
- }, currentPage)]
390
+ }, currentPage)
398
391
  })]
399
392
  })
400
393
  }), /*#__PURE__*/_jsx(SavedToast, {
@@ -423,6 +416,7 @@ function ringVars(palette) {
423
416
  // ---------------------------------------------------------------------------
424
417
 
425
418
  function SettingsRail({
419
+ header,
426
420
  groups,
427
421
  page,
428
422
  onSelect,
@@ -432,7 +426,11 @@ function SettingsRail({
432
426
  testID
433
427
  }) {
434
428
  const compact = layout === 'compact';
435
- return /*#__PURE__*/_jsx(ScrollView, {
429
+ const scrollY = useSharedValue(0);
430
+ const onScroll = useAnimatedScrollHandler(event => {
431
+ scrollY.value = Math.max(0, event.contentOffset.y);
432
+ }, [scrollY]);
433
+ return /*#__PURE__*/_jsxs(Animated.ScrollView, {
436
434
  role: "navigation",
437
435
  "aria-label": label,
438
436
  style: [compact ? styles.railCompact : [styles.rail, {
@@ -441,31 +439,47 @@ function SettingsRail({
441
439
  backgroundColor: palette.secondary,
442
440
  borderRightColor: palette.separator
443
441
  }],
444
- contentContainerStyle: [styles.railContent, compact ? styles.railContentCompact : null],
442
+ contentContainerStyle: header ? undefined : styles.railContent,
443
+ stickyHeaderIndices: header && !IS_WEB ? [0] : undefined,
444
+ onScroll: onScroll,
445
+ scrollEventThrottle: 16,
446
+ contentInsetAdjustmentBehavior: "never",
447
+ automaticallyAdjustContentInsets: false,
445
448
  showsVerticalScrollIndicator: false,
446
449
  testID: testID ? `${testID}-rail` : undefined,
447
- children: groups.map(group => /*#__PURE__*/_jsxs(View, {
448
- style: styles.group,
449
- children: [/*#__PURE__*/_jsx(Text, {
450
- variant: "body-medium",
451
- style: [styles.groupLabel, {
452
- color: palette.textSecondary
453
- }],
454
- children: group.label
455
- }), /*#__PURE__*/_jsx(View, {
456
- style: styles.groupRows,
457
- children: group.items.map(item => /*#__PURE__*/_jsx(RailRow, {
458
- item: item
459
- // A list, not a selection, when the rows ARE the navigation.
460
- ,
461
- selected: !compact && item.page !== undefined && item.page === page,
462
- showChevron: compact && item.page !== undefined,
463
- onSelect: onSelect,
464
- palette: palette,
465
- testID: testID ? `${testID}-nav-${item.key}` : undefined
466
- }, item.key))
467
- })]
468
- }, group.key ?? group.label))
450
+ children: [header ? /*#__PURE__*/_jsx(SettingsHeader, {
451
+ ...header,
452
+ palette: palette,
453
+ scrimColor: palette.secondary,
454
+ scrollY: scrollY,
455
+ testID: testID
456
+ }) : null, /*#__PURE__*/_jsx(View, {
457
+ style: header ? [styles.railContent, styles.railContentCompact] : {
458
+ gap: 20
459
+ },
460
+ children: groups.map(group => /*#__PURE__*/_jsxs(View, {
461
+ style: styles.group,
462
+ children: [/*#__PURE__*/_jsx(Text, {
463
+ variant: "body-medium",
464
+ style: [styles.groupLabel, {
465
+ color: palette.textSecondary
466
+ }],
467
+ children: group.label
468
+ }), /*#__PURE__*/_jsx(View, {
469
+ style: styles.groupRows,
470
+ children: group.items.map(item => /*#__PURE__*/_jsx(RailRow, {
471
+ item: item
472
+ // A list, not a selection, when the rows ARE the navigation.
473
+ ,
474
+ selected: !compact && item.page !== undefined && item.page === page,
475
+ showChevron: compact && item.page !== undefined,
476
+ onSelect: onSelect,
477
+ palette: palette,
478
+ testID: testID ? `${testID}-nav-${item.key}` : undefined
479
+ }, item.key))
480
+ })]
481
+ }, group.key ?? group.label))
482
+ })]
469
483
  });
470
484
  }
471
485
  function RailRow({
@@ -527,124 +541,78 @@ function RailRow({
527
541
  // Close button, scroller, toast
528
542
  // ---------------------------------------------------------------------------
529
543
 
530
- function SettingsCloseButton({
531
- label,
532
- onPress,
533
- palette,
534
- testID
535
- }) {
536
- const [hovered, setHovered] = useState(false);
537
- return /*#__PURE__*/_jsx(Pressable, {
538
- role: "button",
539
- accessibilityLabel: label,
540
- ...webDataSet({
541
- bloomSettingsPress: ''
542
- }),
543
- onPress: onPress,
544
- onHoverIn: () => setHovered(true),
545
- onHoverOut: () => setHovered(false),
546
- testID: testID,
547
- style: [styles.close, {
548
- backgroundColor: hovered ? palette.tertiaryHover : palette.tertiary
549
- }],
550
- children: /*#__PURE__*/_jsx(RiCloseLine, {
551
- width: 16,
552
- height: 16,
553
- fill: palette.iconSecondary
554
- })
555
- });
556
- }
544
+ /** Shared header on every layout; the modal already owns safe-area insets. */
557
545
 
558
- /**
559
- * The `compact` title bar: optional back chevron, the title, the close button —
560
- * 56px, the page's 16px inset, hairline under it.
561
- */
562
- function CompactHeader({
546
+ function SettingsHeader({
563
547
  title,
564
548
  onBack,
565
549
  backLabel,
566
550
  closeLabel,
567
551
  onClose,
568
552
  palette,
553
+ scrollY,
554
+ scrimColor = palette.full,
569
555
  testID
570
556
  }) {
571
- return /*#__PURE__*/_jsxs(View, {
572
- style: [styles.compactHeader, {
573
- borderBottomColor: palette.separator
574
- }],
575
- testID: testID ? `${testID}-title-row` : undefined,
576
- children: [onBack ? /*#__PURE__*/_jsx(Pressable, {
577
- role: "button",
578
- accessibilityLabel: backLabel,
579
- ...webDataSet({
580
- bloomSettingsPress: ''
581
- }),
582
- onPress: onBack,
583
- hitSlop: 8,
584
- testID: testID ? `${testID}-back` : undefined,
585
- style: styles.compactBack,
586
- children: /*#__PURE__*/_jsx(RiArrowLeftSLine, {
587
- width: 24,
588
- height: 24,
589
- fill: palette.text
557
+ return /*#__PURE__*/_jsx(PageHeader, {
558
+ title: title,
559
+ headingLevel: 2,
560
+ onBack: onBack,
561
+ backLabel: backLabel,
562
+ safeArea: false,
563
+ sticky: IS_WEB,
564
+ placement: "inline",
565
+ scrim: "auto",
566
+ scrimColor: scrimColor,
567
+ scrollY: scrollY,
568
+ actions: /*#__PURE__*/_jsx(ButtonGroup, {
569
+ accessibilityLabel: closeLabel,
570
+ children: /*#__PURE__*/_jsx(ButtonGroupItem, {
571
+ iconOnly: true,
572
+ leadingIcon: RiCloseLine,
573
+ accessibilityLabel: closeLabel,
574
+ onPress: onClose,
575
+ testID: testID ? `${testID}-close` : undefined
590
576
  })
591
- }) : null, /*#__PURE__*/_jsx(Text, {
592
- variant: "headline-medium",
593
- role: "heading",
594
- numberOfLines: 1,
595
- style: [styles.compactTitle, {
596
- color: palette.text
597
- }],
598
- children: title
599
- }), /*#__PURE__*/_jsx(SettingsCloseButton, {
600
- label: closeLabel,
601
- onPress: onClose,
602
- palette: palette,
603
- testID: testID ? `${testID}-close` : undefined
604
- })]
577
+ }),
578
+ testID: testID ? `${testID}-header` : undefined
605
579
  });
606
580
  }
581
+
582
+ /** Each page owns one full-height viewport, including its header. */
607
583
  function PageScroller({
608
584
  children,
585
+ header,
609
586
  palette,
610
587
  inset,
611
588
  insetTop = 0,
612
589
  testID
613
590
  }) {
614
- const [scrolled, setScrolled] = useState(false);
615
- const onScroll = useCallback(event => {
616
- const next = event.nativeEvent.contentOffset.y > 0;
617
- setScrolled(prev => prev === next ? prev : next);
618
- }, []);
619
- const fade = {
620
- opacity: scrolled ? 1 : 0,
621
- ...(IS_WEB ? {
622
- transitionProperty: 'opacity',
623
- transitionDuration: '200ms',
624
- transitionTimingFunction: 'ease-out'
625
- } : null)
626
- };
627
- return /*#__PURE__*/_jsxs(View, {
628
- style: styles.scrollHost,
629
- children: [/*#__PURE__*/_jsx(ScrollView, {
630
- style: styles.scroll,
631
- contentContainerStyle: [styles.scrollContent, {
591
+ const scrollY = useSharedValue(0);
592
+ const onScroll = useAnimatedScrollHandler(event => {
593
+ scrollY.value = Math.max(0, event.contentOffset.y);
594
+ }, [scrollY]);
595
+ return /*#__PURE__*/_jsxs(Animated.ScrollView, {
596
+ style: styles.scroll,
597
+ stickyHeaderIndices: IS_WEB ? undefined : [0],
598
+ onScroll: onScroll,
599
+ scrollEventThrottle: 16,
600
+ contentInsetAdjustmentBehavior: "never",
601
+ automaticallyAdjustContentInsets: false,
602
+ testID: testID ? `${testID}-page` : undefined,
603
+ children: [/*#__PURE__*/_jsx(SettingsHeader, {
604
+ ...header,
605
+ palette: palette,
606
+ scrollY: scrollY,
607
+ testID: testID
608
+ }), /*#__PURE__*/_jsx(View, {
609
+ style: {
632
610
  paddingLeft: inset,
633
611
  paddingRight: inset,
634
612
  paddingBottom: inset,
635
613
  paddingTop: insetTop
636
- }],
637
- onScroll: onScroll,
638
- scrollEventThrottle: 16,
639
- testID: testID ? `${testID}-page` : undefined,
614
+ },
640
615
  children: children
641
- }), /*#__PURE__*/_jsx(View, {
642
- pointerEvents: "none",
643
- style: [styles.fade, fade],
644
- testID: testID ? `${testID}-fade` : undefined,
645
- children: /*#__PURE__*/_jsx(VerticalFade, {
646
- color: palette.primary
647
- })
648
616
  })]
649
617
  });
650
618
  }
@@ -741,27 +709,6 @@ const styles = StyleSheet.create({
741
709
  railChevron: {
742
710
  marginLeft: 'auto'
743
711
  },
744
- compactHeader: {
745
- flexDirection: 'row',
746
- alignItems: 'center',
747
- gap: 8,
748
- height: 56,
749
- paddingLeft: 16,
750
- paddingRight: 16,
751
- borderBottomWidth: 1
752
- },
753
- compactBack: {
754
- width: 32,
755
- height: 32,
756
- marginLeft: -8,
757
- alignItems: 'center',
758
- justifyContent: 'center',
759
- borderRadius: 9999
760
- },
761
- compactTitle: {
762
- flex: 1,
763
- minWidth: 0
764
- },
765
712
  panel: {
766
713
  flexDirection: 'row',
767
714
  borderRadius: 24,
@@ -800,33 +747,9 @@ const styles = StyleSheet.create({
800
747
  flex: 1,
801
748
  minWidth: 0
802
749
  },
803
- titleRow: {
804
- flexDirection: 'row',
805
- alignItems: 'center',
806
- justifyContent: 'space-between'
807
- },
808
- close: {
809
- width: 24,
810
- height: 24,
811
- borderRadius: 9999,
812
- alignItems: 'center',
813
- justifyContent: 'center'
814
- },
815
- scrollHost: {
816
- flex: 1,
817
- minHeight: 0,
818
- position: 'relative'
819
- },
820
750
  scroll: {
821
- flex: 1
822
- },
823
- scrollContent: {},
824
- fade: {
825
- position: 'absolute',
826
- left: 0,
827
- right: 0,
828
- top: 0,
829
- height: 40
751
+ flex: 1,
752
+ minHeight: 0
830
753
  },
831
754
  toast: {
832
755
  position: 'absolute',