@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
@@ -8,14 +8,14 @@ var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactNative = require("react-native");
9
9
  var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
10
10
  var _reactNativeSafeAreaContext = require("react-native-safe-area-context");
11
- var _RiArrowLeftSLine = require("../icons/remix/RiArrowLeftSLine.js");
12
11
  var _RiArrowRightSLine = require("../icons/remix/RiArrowRightSLine.js");
13
12
  var _RiCheckboxCircleFill = require("../icons/remix/RiCheckboxCircleFill.js");
13
+ var _index = require("../page-header/index.js");
14
+ var _index2 = require("../button-group/index.js");
14
15
  var _RiCloseLine = require("../icons/remix/RiCloseLine.js");
15
- var _SettingsArt = require("./SettingsArt.js");
16
- var _index = require("../overlay/index.js");
16
+ var _index3 = require("../overlay/index.js");
17
17
  var _webData = require("../styles/web-data.js");
18
- var _index2 = require("../typography/index.js");
18
+ var _index4 = require("../typography/index.js");
19
19
  var _context = require("./context.js");
20
20
  var _modalPortal = require("./modal-portal");
21
21
  var _webCss = require("./web-css.js");
@@ -33,10 +33,8 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
33
33
  * 20px icon (icon/secondary) + 8 + body-medium label
34
34
  * (selected: background/secondary/hover + text/primary;
35
35
  * hover: background/secondary/hover @ 60%)
36
- * content title row px 32 pt 32 pb 12 (Storage 6), title-3-medium +
37
- * 24px round close (background/tertiary, hover tertiary/hover,
38
- * 16px glyph); the page scrolls under a 40px top fade that eases
39
- * in (200ms) once it is scrolled; px 32 pb 32
36
+ * content one viewport with a sticky PageHeader and its scroll-linked edge;
37
+ * content insets belong to the page body, never the viewport
40
38
  * toast "Saved" pill straddling the panel's bottom edge: 1px
41
39
  * border/button, background/primary, py 4 pr 10 pl 6, gap 4,
42
40
  * 16px lime check + body-2-medium, shadow-dropdown. Rises 12px in,
@@ -100,11 +98,13 @@ function SettingsModal({
100
98
  control,
101
99
  open: controlledOpen,
102
100
  onClose,
101
+ onBeforeLeave,
103
102
  groups,
104
103
  pages,
105
104
  page: controlledPage,
106
105
  defaultPage,
107
106
  onPageChange,
107
+ initialView = 'navigation',
108
108
  labels,
109
109
  testID
110
110
  }) {
@@ -128,7 +128,12 @@ function SettingsModal({
128
128
  const insets = (0, _reactNativeSafeAreaContext.useSafeAreaInsets)();
129
129
 
130
130
  // `compact` layout is two levels: the section list, then a page pushed over it.
131
- const [compactPageOpen, setCompactPageOpen] = (0, _react.useState)(false);
131
+ const [compactPageOpen, setCompactPageOpen] = (0, _react.useState)(initialView === 'page');
132
+ const initialViewRef = (0, _react.useRef)(initialView);
133
+ initialViewRef.current = initialView;
134
+ const beforeLeaveRef = (0, _react.useRef)(onBeforeLeave);
135
+ beforeLeaveRef.current = onBeforeLeave;
136
+ const mayLeave = (0, _react.useCallback)(reason => beforeLeaveRef.current?.(reason) !== false, []);
132
137
  const onCloseRef = (0, _react.useRef)(onClose);
133
138
  onCloseRef.current = onClose;
134
139
  const isControlledRef = (0, _react.useRef)(isControlled);
@@ -137,7 +142,7 @@ function SettingsModal({
137
142
  defaultPageRef.current = defaultPage ?? firstPage(groups);
138
143
  const show = (0, _react.useCallback)(() => {
139
144
  setInternalPage(defaultPageRef.current);
140
- setCompactPageOpen(false);
145
+ setCompactPageOpen(initialViewRef.current === 'page');
141
146
  setMounted(true);
142
147
  setOpenCount(count => count + 1);
143
148
  }, []);
@@ -174,20 +179,25 @@ function SettingsModal({
174
179
  if (controlledOpen) show();else hide();
175
180
  }, [isControlled, controlledOpen, show, hide]);
176
181
  const requestClose = (0, _react.useCallback)(() => {
182
+ if (!mayLeave('close')) return false;
177
183
  if (isControlledRef.current) onCloseRef.current?.();else hide();
178
- }, [hide]);
184
+ return true;
185
+ }, [hide, mayLeave]);
179
186
  (0, _react.useImperativeHandle)(control?.ref, () => ({
180
187
  open: show,
181
188
  close: cb => {
182
- hide();
183
- if (cb) setTimeout(cb, UNMOUNT_MS);
189
+ if (requestClose() && cb) setTimeout(cb, UNMOUNT_MS);
184
190
  }
185
- }), [show, hide]);
191
+ }), [show, requestClose]);
186
192
  const selectPage = (0, _react.useCallback)(next => {
193
+ if (next !== currentPage && !mayLeave('page')) return;
187
194
  if (controlledPage === undefined) setInternalPage(next);
188
195
  setCompactPageOpen(true);
189
196
  onPageChange?.(next);
190
- }, [controlledPage, onPageChange]);
197
+ }, [controlledPage, currentPage, onPageChange, mayLeave]);
198
+ const requestNavigation = (0, _react.useCallback)(() => {
199
+ if (mayLeave('navigation')) setCompactPageOpen(false);
200
+ }, [mayLeave]);
191
201
 
192
202
  // Escape closes (web). On `window`, not `document`: a menu or popover opened
193
203
  // inside the modal handles Escape on `document` and stops propagation there,
@@ -212,11 +222,11 @@ function SettingsModal({
212
222
  if (!mounted || _webCss.IS_WEB || typeof _reactNative.BackHandler?.addEventListener !== 'function') return;
213
223
  const sub = _reactNative.BackHandler.addEventListener('hardwareBackPress', () => {
214
224
  // On a pushed page (compact), back returns to the section list first.
215
- if (compactPageOpenRef.current) setCompactPageOpen(false);else requestClose();
225
+ if (compactPageOpenRef.current) requestNavigation();else requestClose();
216
226
  return true;
217
227
  });
218
228
  return () => sub.remove();
219
- }, [mounted, requestClose]);
229
+ }, [mounted, requestClose, requestNavigation]);
220
230
 
221
231
  // ----- saved toast -------------------------------------------------------
222
232
  const [savedPhase, setSavedPhase] = (0, _react.useState)('hidden');
@@ -287,8 +297,8 @@ function SettingsModal({
287
297
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_modalPortal.ModalPortal, {
288
298
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_context.SettingsModalContext.Provider, {
289
299
  value: context,
290
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.OverlayRoot, {
291
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.Backdrop, {
300
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index3.OverlayRoot, {
301
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index3.Backdrop, {
292
302
  onPress: requestClose,
293
303
  progress: progress,
294
304
  blurIntensity: 0,
@@ -326,32 +336,30 @@ function SettingsModal({
326
336
  backgroundColor: palette.full,
327
337
  boxShadow: palette.shadowXs
328
338
  }, ringVars(palette)],
329
- children: layout === 'compact' ? compactPageOpen && pageConfig ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
339
+ children: layout === 'compact' ? compactPageOpen && pageConfig ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
330
340
  style: styles.content,
331
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(CompactHeader, {
332
- title: pageConfig.title,
333
- onBack: () => setCompactPageOpen(false),
334
- backLabel: labels?.back ?? 'Back',
335
- closeLabel: closeLabel,
336
- onClose: requestClose,
337
- palette: palette,
338
- testID: testID
339
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(PageScroller, {
341
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(PageScroller, {
342
+ header: {
343
+ title: pageConfig.title,
344
+ onBack: requestNavigation,
345
+ backLabel: labels?.back ?? 'Back',
346
+ closeLabel,
347
+ onClose: requestClose
348
+ },
340
349
  palette: palette,
341
350
  inset: CONTENT_INSET.compact,
342
351
  insetTop: CONTENT_INSET.compact,
343
352
  testID: testID,
344
353
  children: pageConfig.content
345
- }, currentPage)]
346
- }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
354
+ }, currentPage)
355
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
347
356
  style: styles.content,
348
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(CompactHeader, {
349
- title: labels?.dialog ?? 'Settings',
350
- closeLabel: closeLabel,
351
- onClose: requestClose,
352
- palette: palette,
353
- testID: testID
354
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(SettingsRail, {
357
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(SettingsRail, {
358
+ header: {
359
+ title: labels?.dialog ?? 'Settings',
360
+ closeLabel,
361
+ onClose: requestClose
362
+ },
355
363
  groups: groups,
356
364
  page: currentPage,
357
365
  onSelect: selectPage,
@@ -359,7 +367,7 @@ function SettingsModal({
359
367
  label: labels?.nav ?? 'Settings sections',
360
368
  layout: "compact",
361
369
  testID: testID
362
- })]
370
+ })
363
371
  }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
364
372
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(SettingsRail, {
365
373
  groups: groups,
@@ -369,35 +377,20 @@ function SettingsModal({
369
377
  label: labels?.nav ?? 'Settings sections',
370
378
  layout: layout,
371
379
  testID: testID
372
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
380
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
373
381
  style: styles.content,
374
- children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
375
- style: [styles.titleRow, {
376
- paddingLeft: CONTENT_INSET[layout],
377
- paddingRight: CONTENT_INSET[layout],
378
- paddingTop: CONTENT_INSET[layout],
379
- paddingBottom: pageConfig?.compactTitle ? 6 : 12
380
- }],
381
- testID: testID ? `${testID}-title-row` : undefined,
382
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.Text, {
383
- variant: "title-3-medium",
384
- role: "heading",
385
- style: {
386
- color: palette.text
387
- },
388
- children: pageConfig?.title ?? ''
389
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(SettingsCloseButton, {
390
- label: closeLabel,
391
- onPress: requestClose,
392
- palette: palette,
393
- testID: testID ? `${testID}-close` : undefined
394
- })]
395
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(PageScroller, {
382
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(PageScroller, {
383
+ header: {
384
+ title: pageConfig?.title ?? '',
385
+ closeLabel,
386
+ onClose: requestClose
387
+ },
396
388
  palette: palette,
397
389
  inset: CONTENT_INSET[layout],
390
+ insetTop: pageConfig?.compactTitle ? 6 : 12,
398
391
  testID: testID,
399
392
  children: pageConfig?.content
400
- }, currentPage)]
393
+ }, currentPage)
401
394
  })]
402
395
  })
403
396
  }), /*#__PURE__*/(0, _jsxRuntime.jsx)(SavedToast, {
@@ -426,6 +419,7 @@ function ringVars(palette) {
426
419
  // ---------------------------------------------------------------------------
427
420
 
428
421
  function SettingsRail({
422
+ header,
429
423
  groups,
430
424
  page,
431
425
  onSelect,
@@ -435,7 +429,11 @@ function SettingsRail({
435
429
  testID
436
430
  }) {
437
431
  const compact = layout === 'compact';
438
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
432
+ const scrollY = (0, _reactNativeReanimated.useSharedValue)(0);
433
+ const onScroll = (0, _reactNativeReanimated.useAnimatedScrollHandler)(event => {
434
+ scrollY.value = Math.max(0, event.contentOffset.y);
435
+ }, [scrollY]);
436
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.ScrollView, {
439
437
  role: "navigation",
440
438
  "aria-label": label,
441
439
  style: [compact ? styles.railCompact : [styles.rail, {
@@ -444,31 +442,47 @@ function SettingsRail({
444
442
  backgroundColor: palette.secondary,
445
443
  borderRightColor: palette.separator
446
444
  }],
447
- contentContainerStyle: [styles.railContent, compact ? styles.railContentCompact : null],
445
+ contentContainerStyle: header ? undefined : styles.railContent,
446
+ stickyHeaderIndices: header && !_webCss.IS_WEB ? [0] : undefined,
447
+ onScroll: onScroll,
448
+ scrollEventThrottle: 16,
449
+ contentInsetAdjustmentBehavior: "never",
450
+ automaticallyAdjustContentInsets: false,
448
451
  showsVerticalScrollIndicator: false,
449
452
  testID: testID ? `${testID}-rail` : undefined,
450
- children: groups.map(group => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
451
- style: styles.group,
452
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.Text, {
453
- variant: "body-medium",
454
- style: [styles.groupLabel, {
455
- color: palette.textSecondary
456
- }],
457
- children: group.label
458
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
459
- style: styles.groupRows,
460
- children: group.items.map(item => /*#__PURE__*/(0, _jsxRuntime.jsx)(RailRow, {
461
- item: item
462
- // A list, not a selection, when the rows ARE the navigation.
463
- ,
464
- selected: !compact && item.page !== undefined && item.page === page,
465
- showChevron: compact && item.page !== undefined,
466
- onSelect: onSelect,
467
- palette: palette,
468
- testID: testID ? `${testID}-nav-${item.key}` : undefined
469
- }, item.key))
470
- })]
471
- }, group.key ?? group.label))
453
+ children: [header ? /*#__PURE__*/(0, _jsxRuntime.jsx)(SettingsHeader, {
454
+ ...header,
455
+ palette: palette,
456
+ scrimColor: palette.secondary,
457
+ scrollY: scrollY,
458
+ testID: testID
459
+ }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
460
+ style: header ? [styles.railContent, styles.railContentCompact] : {
461
+ gap: 20
462
+ },
463
+ children: groups.map(group => /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
464
+ style: styles.group,
465
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_index4.Text, {
466
+ variant: "body-medium",
467
+ style: [styles.groupLabel, {
468
+ color: palette.textSecondary
469
+ }],
470
+ children: group.label
471
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
472
+ style: styles.groupRows,
473
+ children: group.items.map(item => /*#__PURE__*/(0, _jsxRuntime.jsx)(RailRow, {
474
+ item: item
475
+ // A list, not a selection, when the rows ARE the navigation.
476
+ ,
477
+ selected: !compact && item.page !== undefined && item.page === page,
478
+ showChevron: compact && item.page !== undefined,
479
+ onSelect: onSelect,
480
+ palette: palette,
481
+ testID: testID ? `${testID}-nav-${item.key}` : undefined
482
+ }, item.key))
483
+ })]
484
+ }, group.key ?? group.label))
485
+ })]
472
486
  });
473
487
  }
474
488
  function RailRow({
@@ -508,7 +522,7 @@ function RailRow({
508
522
  width: 20,
509
523
  height: 20,
510
524
  fill: palette.iconSecondary
511
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.Text, {
525
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_index4.Text, {
512
526
  variant: "body-medium",
513
527
  numberOfLines: 1,
514
528
  style: [styles.railLabel, {
@@ -530,124 +544,78 @@ function RailRow({
530
544
  // Close button, scroller, toast
531
545
  // ---------------------------------------------------------------------------
532
546
 
533
- function SettingsCloseButton({
534
- label,
535
- onPress,
536
- palette,
537
- testID
538
- }) {
539
- const [hovered, setHovered] = (0, _react.useState)(false);
540
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
541
- role: "button",
542
- accessibilityLabel: label,
543
- ...(0, _webData.webDataSet)({
544
- bloomSettingsPress: ''
545
- }),
546
- onPress: onPress,
547
- onHoverIn: () => setHovered(true),
548
- onHoverOut: () => setHovered(false),
549
- testID: testID,
550
- style: [styles.close, {
551
- backgroundColor: hovered ? palette.tertiaryHover : palette.tertiary
552
- }],
553
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_RiCloseLine.RiCloseLine, {
554
- width: 16,
555
- height: 16,
556
- fill: palette.iconSecondary
557
- })
558
- });
559
- }
547
+ /** Shared header on every layout; the modal already owns safe-area insets. */
560
548
 
561
- /**
562
- * The `compact` title bar: optional back chevron, the title, the close button —
563
- * 56px, the page's 16px inset, hairline under it.
564
- */
565
- function CompactHeader({
549
+ function SettingsHeader({
566
550
  title,
567
551
  onBack,
568
552
  backLabel,
569
553
  closeLabel,
570
554
  onClose,
571
555
  palette,
556
+ scrollY,
557
+ scrimColor = palette.full,
572
558
  testID
573
559
  }) {
574
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
575
- style: [styles.compactHeader, {
576
- borderBottomColor: palette.separator
577
- }],
578
- testID: testID ? `${testID}-title-row` : undefined,
579
- children: [onBack ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Pressable, {
580
- role: "button",
581
- accessibilityLabel: backLabel,
582
- ...(0, _webData.webDataSet)({
583
- bloomSettingsPress: ''
584
- }),
585
- onPress: onBack,
586
- hitSlop: 8,
587
- testID: testID ? `${testID}-back` : undefined,
588
- style: styles.compactBack,
589
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_RiArrowLeftSLine.RiArrowLeftSLine, {
590
- width: 24,
591
- height: 24,
592
- fill: palette.text
560
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.PageHeader, {
561
+ title: title,
562
+ headingLevel: 2,
563
+ onBack: onBack,
564
+ backLabel: backLabel,
565
+ safeArea: false,
566
+ sticky: _webCss.IS_WEB,
567
+ placement: "inline",
568
+ scrim: "auto",
569
+ scrimColor: scrimColor,
570
+ scrollY: scrollY,
571
+ actions: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.ButtonGroup, {
572
+ accessibilityLabel: closeLabel,
573
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.ButtonGroupItem, {
574
+ iconOnly: true,
575
+ leadingIcon: _RiCloseLine.RiCloseLine,
576
+ accessibilityLabel: closeLabel,
577
+ onPress: onClose,
578
+ testID: testID ? `${testID}-close` : undefined
593
579
  })
594
- }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.Text, {
595
- variant: "headline-medium",
596
- role: "heading",
597
- numberOfLines: 1,
598
- style: [styles.compactTitle, {
599
- color: palette.text
600
- }],
601
- children: title
602
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(SettingsCloseButton, {
603
- label: closeLabel,
604
- onPress: onClose,
605
- palette: palette,
606
- testID: testID ? `${testID}-close` : undefined
607
- })]
580
+ }),
581
+ testID: testID ? `${testID}-header` : undefined
608
582
  });
609
583
  }
584
+
585
+ /** Each page owns one full-height viewport, including its header. */
610
586
  function PageScroller({
611
587
  children,
588
+ header,
612
589
  palette,
613
590
  inset,
614
591
  insetTop = 0,
615
592
  testID
616
593
  }) {
617
- const [scrolled, setScrolled] = (0, _react.useState)(false);
618
- const onScroll = (0, _react.useCallback)(event => {
619
- const next = event.nativeEvent.contentOffset.y > 0;
620
- setScrolled(prev => prev === next ? prev : next);
621
- }, []);
622
- const fade = {
623
- opacity: scrolled ? 1 : 0,
624
- ...(_webCss.IS_WEB ? {
625
- transitionProperty: 'opacity',
626
- transitionDuration: '200ms',
627
- transitionTimingFunction: 'ease-out'
628
- } : null)
629
- };
630
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
631
- style: styles.scrollHost,
632
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.ScrollView, {
633
- style: styles.scroll,
634
- contentContainerStyle: [styles.scrollContent, {
594
+ const scrollY = (0, _reactNativeReanimated.useSharedValue)(0);
595
+ const onScroll = (0, _reactNativeReanimated.useAnimatedScrollHandler)(event => {
596
+ scrollY.value = Math.max(0, event.contentOffset.y);
597
+ }, [scrollY]);
598
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.ScrollView, {
599
+ style: styles.scroll,
600
+ stickyHeaderIndices: _webCss.IS_WEB ? undefined : [0],
601
+ onScroll: onScroll,
602
+ scrollEventThrottle: 16,
603
+ contentInsetAdjustmentBehavior: "never",
604
+ automaticallyAdjustContentInsets: false,
605
+ testID: testID ? `${testID}-page` : undefined,
606
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(SettingsHeader, {
607
+ ...header,
608
+ palette: palette,
609
+ scrollY: scrollY,
610
+ testID: testID
611
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
612
+ style: {
635
613
  paddingLeft: inset,
636
614
  paddingRight: inset,
637
615
  paddingBottom: inset,
638
616
  paddingTop: insetTop
639
- }],
640
- onScroll: onScroll,
641
- scrollEventThrottle: 16,
642
- testID: testID ? `${testID}-page` : undefined,
617
+ },
643
618
  children: children
644
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
645
- pointerEvents: "none",
646
- style: [styles.fade, fade],
647
- testID: testID ? `${testID}-fade` : undefined,
648
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_SettingsArt.VerticalFade, {
649
- color: palette.primary
650
- })
651
619
  })]
652
620
  });
653
621
  }
@@ -705,7 +673,7 @@ function SavedToast({
705
673
  width: 16,
706
674
  height: 16,
707
675
  fill: palette.success[600]
708
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.Text, {
676
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_index4.Text, {
709
677
  variant: "body-2-medium",
710
678
  numberOfLines: 1,
711
679
  style: {
@@ -744,27 +712,6 @@ const styles = _reactNative.StyleSheet.create({
744
712
  railChevron: {
745
713
  marginLeft: 'auto'
746
714
  },
747
- compactHeader: {
748
- flexDirection: 'row',
749
- alignItems: 'center',
750
- gap: 8,
751
- height: 56,
752
- paddingLeft: 16,
753
- paddingRight: 16,
754
- borderBottomWidth: 1
755
- },
756
- compactBack: {
757
- width: 32,
758
- height: 32,
759
- marginLeft: -8,
760
- alignItems: 'center',
761
- justifyContent: 'center',
762
- borderRadius: 9999
763
- },
764
- compactTitle: {
765
- flex: 1,
766
- minWidth: 0
767
- },
768
715
  panel: {
769
716
  flexDirection: 'row',
770
717
  borderRadius: 24,
@@ -803,33 +750,9 @@ const styles = _reactNative.StyleSheet.create({
803
750
  flex: 1,
804
751
  minWidth: 0
805
752
  },
806
- titleRow: {
807
- flexDirection: 'row',
808
- alignItems: 'center',
809
- justifyContent: 'space-between'
810
- },
811
- close: {
812
- width: 24,
813
- height: 24,
814
- borderRadius: 9999,
815
- alignItems: 'center',
816
- justifyContent: 'center'
817
- },
818
- scrollHost: {
819
- flex: 1,
820
- minHeight: 0,
821
- position: 'relative'
822
- },
823
753
  scroll: {
824
- flex: 1
825
- },
826
- scrollContent: {},
827
- fade: {
828
- position: 'absolute',
829
- left: 0,
830
- right: 0,
831
- top: 0,
832
- height: 40
754
+ flex: 1,
755
+ minHeight: 0
833
756
  },
834
757
  toast: {
835
758
  position: 'absolute',