@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.
- package/docs/adoption-matrix.mdx +7 -7
- package/docs/ai-chat.mdx +8 -0
- package/docs/app-shell.mdx +7 -0
- package/docs/bottom-bar.mdx +6 -0
- package/docs/settings-modal.mdx +14 -5
- package/docs/sidebar.mdx +11 -0
- package/lib/commonjs/ai-chat/AiChatContainer.js +8 -8
- package/lib/commonjs/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/commonjs/app-shell/AppShellBars.js +8 -2
- package/lib/commonjs/app-shell/AppShellBars.js.map +1 -1
- package/lib/commonjs/app-shell/AppShellEngine.js +29 -6
- package/lib/commonjs/app-shell/AppShellEngine.js.map +1 -1
- package/lib/commonjs/bottom-bar/BottomBarBase.js +10 -2
- package/lib/commonjs/bottom-bar/BottomBarBase.js.map +1 -1
- package/lib/commonjs/layout/bottom-bar-slot.js +10 -0
- package/lib/commonjs/layout/bottom-bar-slot.js.map +1 -0
- package/lib/commonjs/settings-modal/SettingsModal.js +153 -230
- package/lib/commonjs/settings-modal/SettingsModal.js.map +1 -1
- package/lib/commonjs/sidebar/Sidebar.js +35 -5
- package/lib/commonjs/sidebar/Sidebar.js.map +1 -1
- package/lib/module/ai-chat/AiChatContainer.js +8 -8
- package/lib/module/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/module/app-shell/AppShellBars.js +8 -2
- package/lib/module/app-shell/AppShellBars.js.map +1 -1
- package/lib/module/app-shell/AppShellEngine.js +30 -7
- package/lib/module/app-shell/AppShellEngine.js.map +1 -1
- package/lib/module/bottom-bar/BottomBarBase.js +11 -3
- package/lib/module/bottom-bar/BottomBarBase.js.map +1 -1
- package/lib/module/layout/bottom-bar-slot.js +7 -0
- package/lib/module/layout/bottom-bar-slot.js.map +1 -0
- package/lib/module/settings-modal/SettingsModal.js +150 -227
- package/lib/module/settings-modal/SettingsModal.js.map +1 -1
- package/lib/module/sidebar/Sidebar.js +35 -5
- package/lib/module/sidebar/Sidebar.js.map +1 -1
- package/lib/typescript/commonjs/ai-chat/types.d.ts +2 -2
- package/lib/typescript/commonjs/ai-chat/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/app-shell/AppShellBars.d.ts +1 -0
- package/lib/typescript/commonjs/app-shell/AppShellBars.d.ts.map +1 -1
- package/lib/typescript/commonjs/app-shell/AppShellEngine.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-bar/BottomBarBase.d.ts +1 -1
- package/lib/typescript/commonjs/bottom-bar/BottomBarBase.d.ts.map +1 -1
- package/lib/typescript/commonjs/bottom-bar/types.d.ts +4 -0
- package/lib/typescript/commonjs/bottom-bar/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/layout/bottom-bar-slot.d.ts +3 -0
- package/lib/typescript/commonjs/layout/bottom-bar-slot.d.ts.map +1 -0
- package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts +1 -1
- package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/settings-modal/types.d.ts +7 -2
- package/lib/typescript/commonjs/settings-modal/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/sidebar/Sidebar.d.ts.map +1 -1
- package/lib/typescript/commonjs/sidebar/types.d.ts +12 -1
- package/lib/typescript/commonjs/sidebar/types.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/types.d.ts +2 -2
- package/lib/typescript/module/ai-chat/types.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/AppShellBars.d.ts +1 -0
- package/lib/typescript/module/app-shell/AppShellBars.d.ts.map +1 -1
- package/lib/typescript/module/app-shell/AppShellEngine.d.ts.map +1 -1
- package/lib/typescript/module/bottom-bar/BottomBarBase.d.ts +1 -1
- package/lib/typescript/module/bottom-bar/BottomBarBase.d.ts.map +1 -1
- package/lib/typescript/module/bottom-bar/types.d.ts +4 -0
- package/lib/typescript/module/bottom-bar/types.d.ts.map +1 -1
- package/lib/typescript/module/layout/bottom-bar-slot.d.ts +3 -0
- package/lib/typescript/module/layout/bottom-bar-slot.d.ts.map +1 -0
- package/lib/typescript/module/settings-modal/SettingsModal.d.ts +1 -1
- package/lib/typescript/module/settings-modal/SettingsModal.d.ts.map +1 -1
- package/lib/typescript/module/settings-modal/types.d.ts +7 -2
- package/lib/typescript/module/settings-modal/types.d.ts.map +1 -1
- package/lib/typescript/module/sidebar/Sidebar.d.ts.map +1 -1
- package/lib/typescript/module/sidebar/types.d.ts +12 -1
- package/lib/typescript/module/sidebar/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/support/adoption-matrix.ts +703 -0
- package/src/__tests__/support/collision-fixture-barrel.ts +20 -0
- package/src/__tests__/support/commerce-harness.tsx +97 -0
- package/src/__tests__/support/composite.ts +68 -0
- package/src/__tests__/support/constructed-style-sheets.ts +68 -0
- package/src/__tests__/support/press-host.ts +30 -0
- package/src/__tests__/support/rendered-style.ts +99 -0
- package/src/__tests__/support/unread-hook-fixture.ts +33 -0
- package/src/ai-chat/AiChatContainer.tsx +7 -7
- package/src/ai-chat/types.ts +2 -2
- package/src/app-shell/AppShellBars.tsx +5 -3
- package/src/app-shell/AppShellEngine.tsx +23 -8
- package/src/bottom-bar/BottomBarBase.tsx +7 -4
- package/src/bottom-bar/types.ts +4 -0
- package/src/layout/bottom-bar-slot.ts +4 -0
- package/src/settings-modal/SettingsModal.tsx +180 -250
- package/src/settings-modal/types.ts +7 -2
- package/src/sidebar/Sidebar.tsx +26 -5
- package/src/sidebar/types.ts +12 -1
- package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +9346 -0
- package/src/theme/__tests__/__fixtures__/tonal-glass-primary-failures.json +69 -0
- package/src/theme/__tests__/fixtures/color-engine-golden.json +1 -0
|
@@ -6,19 +6,12 @@ import React, {
|
|
|
6
6
|
useRef,
|
|
7
7
|
useState,
|
|
8
8
|
} from 'react';
|
|
9
|
-
import {
|
|
10
|
-
BackHandler,
|
|
11
|
-
Pressable,
|
|
12
|
-
ScrollView,
|
|
13
|
-
StyleSheet,
|
|
14
|
-
View,
|
|
15
|
-
useWindowDimensions,
|
|
16
|
-
type NativeScrollEvent,
|
|
17
|
-
type NativeSyntheticEvent,
|
|
18
|
-
} from 'react-native';
|
|
9
|
+
import { BackHandler, Pressable, StyleSheet, View, useWindowDimensions } from 'react-native';
|
|
19
10
|
import Animated, {
|
|
20
11
|
Easing,
|
|
21
12
|
useAnimatedStyle,
|
|
13
|
+
useAnimatedScrollHandler,
|
|
14
|
+
type SharedValue,
|
|
22
15
|
useReducedMotion,
|
|
23
16
|
useSharedValue,
|
|
24
17
|
withTiming,
|
|
@@ -26,11 +19,11 @@ import Animated, {
|
|
|
26
19
|
|
|
27
20
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
28
21
|
|
|
29
|
-
import { RiArrowLeftSLine } from '../icons/remix/RiArrowLeftSLine';
|
|
30
22
|
import { RiArrowRightSLine } from '../icons/remix/RiArrowRightSLine';
|
|
31
23
|
import { RiCheckboxCircleFill } from '../icons/remix/RiCheckboxCircleFill';
|
|
24
|
+
import { PageHeader } from '../page-header';
|
|
25
|
+
import { ButtonGroup, ButtonGroupItem } from '../button-group';
|
|
32
26
|
import { RiCloseLine } from '../icons/remix/RiCloseLine';
|
|
33
|
-
import { VerticalFade } from './SettingsArt';
|
|
34
27
|
import { Backdrop, OverlayRoot } from '../overlay';
|
|
35
28
|
import type { WebCssStyle } from '../styles/web-view-style';
|
|
36
29
|
import { webDataSet } from '../styles/web-data';
|
|
@@ -58,10 +51,8 @@ import { IS_WEB, useSettingsWebCss } from './web-css';
|
|
|
58
51
|
* 20px icon (icon/secondary) + 8 + body-medium label
|
|
59
52
|
* (selected: background/secondary/hover + text/primary;
|
|
60
53
|
* hover: background/secondary/hover @ 60%)
|
|
61
|
-
* content
|
|
62
|
-
*
|
|
63
|
-
* 16px glyph); the page scrolls under a 40px top fade that eases
|
|
64
|
-
* in (200ms) once it is scrolled; px 32 pb 32
|
|
54
|
+
* content one viewport with a sticky PageHeader and its scroll-linked edge;
|
|
55
|
+
* content insets belong to the page body, never the viewport
|
|
65
56
|
* toast "Saved" pill straddling the panel's bottom edge: 1px
|
|
66
57
|
* border/button, background/primary, py 4 pr 10 pl 6, gap 4,
|
|
67
58
|
* 16px lime check + body-2-medium, shadow-dropdown. Rises 12px in,
|
|
@@ -88,7 +79,11 @@ const RAIL_WIDTH = 274;
|
|
|
88
79
|
/** `medium` layout: the rail narrows so the page keeps a readable measure. */
|
|
89
80
|
const RAIL_WIDTH_MEDIUM = 220;
|
|
90
81
|
/** Content inset per layout — 32 at regular, stepping down with the room. */
|
|
91
|
-
const CONTENT_INSET: Record<SettingsModalLayout, number> = {
|
|
82
|
+
const CONTENT_INSET: Record<SettingsModalLayout, number> = {
|
|
83
|
+
regular: 32,
|
|
84
|
+
medium: 24,
|
|
85
|
+
compact: 16,
|
|
86
|
+
};
|
|
92
87
|
const VIEWPORT_GUTTER = 16;
|
|
93
88
|
const MOTION_MS = 300;
|
|
94
89
|
const UNMOUNT_MS = 320;
|
|
@@ -128,11 +123,13 @@ export function SettingsModal({
|
|
|
128
123
|
control,
|
|
129
124
|
open: controlledOpen,
|
|
130
125
|
onClose,
|
|
126
|
+
onBeforeLeave,
|
|
131
127
|
groups,
|
|
132
128
|
pages,
|
|
133
129
|
page: controlledPage,
|
|
134
130
|
defaultPage,
|
|
135
131
|
onPageChange,
|
|
132
|
+
initialView = 'navigation',
|
|
136
133
|
labels,
|
|
137
134
|
testID,
|
|
138
135
|
}: SettingsModalProps) {
|
|
@@ -156,7 +153,14 @@ export function SettingsModal({
|
|
|
156
153
|
const insets = useSafeAreaInsets();
|
|
157
154
|
|
|
158
155
|
// `compact` layout is two levels: the section list, then a page pushed over it.
|
|
159
|
-
const [compactPageOpen, setCompactPageOpen] = useState(
|
|
156
|
+
const [compactPageOpen, setCompactPageOpen] = useState(initialView === 'page');
|
|
157
|
+
const initialViewRef = useRef(initialView);
|
|
158
|
+
initialViewRef.current = initialView;
|
|
159
|
+
|
|
160
|
+
const beforeLeaveRef = useRef(onBeforeLeave);
|
|
161
|
+
beforeLeaveRef.current = onBeforeLeave;
|
|
162
|
+
const mayLeave = useCallback((reason: 'close' | 'navigation' | 'page') =>
|
|
163
|
+
beforeLeaveRef.current?.(reason) !== false, []);
|
|
160
164
|
|
|
161
165
|
const onCloseRef = useRef(onClose);
|
|
162
166
|
onCloseRef.current = onClose;
|
|
@@ -167,7 +171,7 @@ export function SettingsModal({
|
|
|
167
171
|
|
|
168
172
|
const show = useCallback(() => {
|
|
169
173
|
setInternalPage(defaultPageRef.current);
|
|
170
|
-
setCompactPageOpen(
|
|
174
|
+
setCompactPageOpen(initialViewRef.current === 'page');
|
|
171
175
|
setMounted(true);
|
|
172
176
|
setOpenCount((count) => count + 1);
|
|
173
177
|
}, []);
|
|
@@ -210,31 +214,37 @@ export function SettingsModal({
|
|
|
210
214
|
}, [isControlled, controlledOpen, show, hide]);
|
|
211
215
|
|
|
212
216
|
const requestClose = useCallback(() => {
|
|
217
|
+
if (!mayLeave('close')) return false;
|
|
213
218
|
if (isControlledRef.current) onCloseRef.current?.();
|
|
214
219
|
else hide();
|
|
215
|
-
|
|
220
|
+
return true;
|
|
221
|
+
}, [hide, mayLeave]);
|
|
216
222
|
|
|
217
223
|
useImperativeHandle(
|
|
218
224
|
control?.ref,
|
|
219
225
|
() => ({
|
|
220
226
|
open: show,
|
|
221
227
|
close: (cb?: () => void) => {
|
|
222
|
-
|
|
223
|
-
if (cb) setTimeout(cb, UNMOUNT_MS);
|
|
228
|
+
if (requestClose() && cb) setTimeout(cb, UNMOUNT_MS);
|
|
224
229
|
},
|
|
225
230
|
}),
|
|
226
|
-
[show,
|
|
231
|
+
[show, requestClose],
|
|
227
232
|
);
|
|
228
233
|
|
|
229
234
|
const selectPage = useCallback(
|
|
230
235
|
(next: string) => {
|
|
236
|
+
if (next !== currentPage && !mayLeave('page')) return;
|
|
231
237
|
if (controlledPage === undefined) setInternalPage(next);
|
|
232
238
|
setCompactPageOpen(true);
|
|
233
239
|
onPageChange?.(next);
|
|
234
240
|
},
|
|
235
|
-
[controlledPage, onPageChange],
|
|
241
|
+
[controlledPage, currentPage, onPageChange, mayLeave],
|
|
236
242
|
);
|
|
237
243
|
|
|
244
|
+
const requestNavigation = useCallback(() => {
|
|
245
|
+
if (mayLeave('navigation')) setCompactPageOpen(false);
|
|
246
|
+
}, [mayLeave]);
|
|
247
|
+
|
|
238
248
|
// Escape closes (web). On `window`, not `document`: a menu or popover opened
|
|
239
249
|
// inside the modal handles Escape on `document` and stops propagation there,
|
|
240
250
|
// so the innermost surface closes first and the modal stays open.
|
|
@@ -259,12 +269,12 @@ export function SettingsModal({
|
|
|
259
269
|
if (!mounted || IS_WEB || typeof BackHandler?.addEventListener !== 'function') return;
|
|
260
270
|
const sub = BackHandler.addEventListener('hardwareBackPress', () => {
|
|
261
271
|
// On a pushed page (compact), back returns to the section list first.
|
|
262
|
-
if (compactPageOpenRef.current)
|
|
272
|
+
if (compactPageOpenRef.current) requestNavigation();
|
|
263
273
|
else requestClose();
|
|
264
274
|
return true;
|
|
265
275
|
});
|
|
266
276
|
return () => sub.remove();
|
|
267
|
-
}, [mounted, requestClose]);
|
|
277
|
+
}, [mounted, requestClose, requestNavigation]);
|
|
268
278
|
|
|
269
279
|
// ----- saved toast -------------------------------------------------------
|
|
270
280
|
const [savedPhase, setSavedPhase] = useState<SavedPhase>('hidden');
|
|
@@ -326,9 +336,7 @@ export function SettingsModal({
|
|
|
326
336
|
// Full screen, a .85 scale reads as the page shrinking away; compact rises 16px instead.
|
|
327
337
|
const webPanelMotion: WebCssStyle = {
|
|
328
338
|
opacity: visible ? 1 : 0,
|
|
329
|
-
transform: isCompact
|
|
330
|
-
? [{ translateY: visible ? 0 : 16 }]
|
|
331
|
-
: [{ scale: visible ? 1 : 0.85 }],
|
|
339
|
+
transform: isCompact ? [{ translateY: visible ? 0 : 16 }] : [{ scale: visible ? 1 : 0.85 }],
|
|
332
340
|
filter: visible ? 'blur(0px)' : 'blur(4px)',
|
|
333
341
|
transitionProperty: 'opacity, transform, filter',
|
|
334
342
|
transitionDuration: reducedMotion ? '0ms' : `${MOTION_MS}ms`,
|
|
@@ -398,16 +406,14 @@ export function SettingsModal({
|
|
|
398
406
|
{layout === 'compact' ? (
|
|
399
407
|
compactPageOpen && pageConfig ? (
|
|
400
408
|
<View style={styles.content}>
|
|
401
|
-
<CompactHeader
|
|
402
|
-
title={pageConfig.title}
|
|
403
|
-
onBack={() => setCompactPageOpen(false)}
|
|
404
|
-
backLabel={labels?.back ?? 'Back'}
|
|
405
|
-
closeLabel={closeLabel}
|
|
406
|
-
onClose={requestClose}
|
|
407
|
-
palette={palette}
|
|
408
|
-
testID={testID}
|
|
409
|
-
/>
|
|
410
409
|
<PageScroller
|
|
410
|
+
header={{
|
|
411
|
+
title: pageConfig.title,
|
|
412
|
+
onBack: requestNavigation,
|
|
413
|
+
backLabel: labels?.back ?? 'Back',
|
|
414
|
+
closeLabel,
|
|
415
|
+
onClose: requestClose,
|
|
416
|
+
}}
|
|
411
417
|
key={currentPage}
|
|
412
418
|
palette={palette}
|
|
413
419
|
inset={CONTENT_INSET.compact}
|
|
@@ -419,14 +425,12 @@ export function SettingsModal({
|
|
|
419
425
|
</View>
|
|
420
426
|
) : (
|
|
421
427
|
<View style={styles.content}>
|
|
422
|
-
<CompactHeader
|
|
423
|
-
title={labels?.dialog ?? 'Settings'}
|
|
424
|
-
closeLabel={closeLabel}
|
|
425
|
-
onClose={requestClose}
|
|
426
|
-
palette={palette}
|
|
427
|
-
testID={testID}
|
|
428
|
-
/>
|
|
429
428
|
<SettingsRail
|
|
429
|
+
header={{
|
|
430
|
+
title: labels?.dialog ?? 'Settings',
|
|
431
|
+
closeLabel,
|
|
432
|
+
onClose: requestClose,
|
|
433
|
+
}}
|
|
430
434
|
groups={groups}
|
|
431
435
|
page={currentPage}
|
|
432
436
|
onSelect={selectPage}
|
|
@@ -449,36 +453,16 @@ export function SettingsModal({
|
|
|
449
453
|
testID={testID}
|
|
450
454
|
/>
|
|
451
455
|
<View style={styles.content}>
|
|
452
|
-
<View
|
|
453
|
-
style={[
|
|
454
|
-
styles.titleRow,
|
|
455
|
-
{
|
|
456
|
-
paddingLeft: CONTENT_INSET[layout],
|
|
457
|
-
paddingRight: CONTENT_INSET[layout],
|
|
458
|
-
paddingTop: CONTENT_INSET[layout],
|
|
459
|
-
paddingBottom: pageConfig?.compactTitle ? 6 : 12,
|
|
460
|
-
},
|
|
461
|
-
]}
|
|
462
|
-
testID={testID ? `${testID}-title-row` : undefined}
|
|
463
|
-
>
|
|
464
|
-
<Text
|
|
465
|
-
variant="title-3-medium"
|
|
466
|
-
role="heading"
|
|
467
|
-
style={{ color: palette.text }}
|
|
468
|
-
>
|
|
469
|
-
{pageConfig?.title ?? ''}
|
|
470
|
-
</Text>
|
|
471
|
-
<SettingsCloseButton
|
|
472
|
-
label={closeLabel}
|
|
473
|
-
onPress={requestClose}
|
|
474
|
-
palette={palette}
|
|
475
|
-
testID={testID ? `${testID}-close` : undefined}
|
|
476
|
-
/>
|
|
477
|
-
</View>
|
|
478
456
|
<PageScroller
|
|
457
|
+
header={{
|
|
458
|
+
title: pageConfig?.title ?? '',
|
|
459
|
+
closeLabel,
|
|
460
|
+
onClose: requestClose,
|
|
461
|
+
}}
|
|
479
462
|
key={currentPage}
|
|
480
463
|
palette={palette}
|
|
481
464
|
inset={CONTENT_INSET[layout]}
|
|
465
|
+
insetTop={pageConfig?.compactTitle ? 6 : 12}
|
|
482
466
|
testID={testID}
|
|
483
467
|
>
|
|
484
468
|
{pageConfig?.content}
|
|
@@ -505,7 +489,10 @@ export function SettingsModal({
|
|
|
505
489
|
|
|
506
490
|
function ringVars(palette: SettingsPalette): WebCssStyle | null {
|
|
507
491
|
return IS_WEB
|
|
508
|
-
? {
|
|
492
|
+
? {
|
|
493
|
+
'--bloom-settings-ring': palette.ring,
|
|
494
|
+
'--bloom-settings-ring-offset': palette.full,
|
|
495
|
+
}
|
|
509
496
|
: null;
|
|
510
497
|
}
|
|
511
498
|
|
|
@@ -514,6 +501,7 @@ function ringVars(palette: SettingsPalette): WebCssStyle | null {
|
|
|
514
501
|
// ---------------------------------------------------------------------------
|
|
515
502
|
|
|
516
503
|
function SettingsRail({
|
|
504
|
+
header,
|
|
517
505
|
groups,
|
|
518
506
|
page,
|
|
519
507
|
onSelect,
|
|
@@ -522,6 +510,7 @@ function SettingsRail({
|
|
|
522
510
|
layout,
|
|
523
511
|
testID,
|
|
524
512
|
}: {
|
|
513
|
+
header?: SettingsHeaderProps;
|
|
525
514
|
groups: SettingsNavGroup[];
|
|
526
515
|
page: string | undefined;
|
|
527
516
|
onSelect: (page: string) => void;
|
|
@@ -531,42 +520,68 @@ function SettingsRail({
|
|
|
531
520
|
testID?: string;
|
|
532
521
|
}) {
|
|
533
522
|
const compact = layout === 'compact';
|
|
523
|
+
const scrollY = useSharedValue(0);
|
|
524
|
+
const onScroll = useAnimatedScrollHandler((event) => {
|
|
525
|
+
scrollY.value = Math.max(0, event.contentOffset.y);
|
|
526
|
+
}, [scrollY]);
|
|
534
527
|
return (
|
|
535
|
-
<ScrollView
|
|
528
|
+
<Animated.ScrollView
|
|
536
529
|
role="navigation"
|
|
537
530
|
aria-label={label}
|
|
538
531
|
style={[
|
|
539
532
|
compact
|
|
540
533
|
? styles.railCompact
|
|
541
534
|
: [styles.rail, { width: layout === 'medium' ? RAIL_WIDTH_MEDIUM : RAIL_WIDTH }],
|
|
542
|
-
{
|
|
535
|
+
{
|
|
536
|
+
backgroundColor: palette.secondary,
|
|
537
|
+
borderRightColor: palette.separator,
|
|
538
|
+
},
|
|
543
539
|
]}
|
|
544
|
-
contentContainerStyle={
|
|
540
|
+
contentContainerStyle={header ? undefined : styles.railContent}
|
|
541
|
+
stickyHeaderIndices={header && !IS_WEB ? [0] : undefined}
|
|
542
|
+
onScroll={onScroll}
|
|
543
|
+
scrollEventThrottle={16}
|
|
544
|
+
contentInsetAdjustmentBehavior="never"
|
|
545
|
+
automaticallyAdjustContentInsets={false}
|
|
545
546
|
showsVerticalScrollIndicator={false}
|
|
546
547
|
testID={testID ? `${testID}-rail` : undefined}
|
|
547
548
|
>
|
|
548
|
-
{
|
|
549
|
-
<
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
549
|
+
{header ? (
|
|
550
|
+
<SettingsHeader
|
|
551
|
+
{...header}
|
|
552
|
+
palette={palette}
|
|
553
|
+
scrimColor={palette.secondary}
|
|
554
|
+
scrollY={scrollY}
|
|
555
|
+
testID={testID}
|
|
556
|
+
/>
|
|
557
|
+
) : null}
|
|
558
|
+
<View style={header ? [styles.railContent, styles.railContentCompact] : { gap: 20 }}>
|
|
559
|
+
{groups.map((group) => (
|
|
560
|
+
<View key={group.key ?? group.label} style={styles.group}>
|
|
561
|
+
<Text
|
|
562
|
+
variant="body-medium"
|
|
563
|
+
style={[styles.groupLabel, { color: palette.textSecondary }]}
|
|
564
|
+
>
|
|
565
|
+
{group.label}
|
|
566
|
+
</Text>
|
|
567
|
+
<View style={styles.groupRows}>
|
|
568
|
+
{group.items.map((item) => (
|
|
569
|
+
<RailRow
|
|
570
|
+
key={item.key}
|
|
571
|
+
item={item}
|
|
572
|
+
// A list, not a selection, when the rows ARE the navigation.
|
|
573
|
+
selected={!compact && item.page !== undefined && item.page === page}
|
|
574
|
+
showChevron={compact && item.page !== undefined}
|
|
575
|
+
onSelect={onSelect}
|
|
576
|
+
palette={palette}
|
|
577
|
+
testID={testID ? `${testID}-nav-${item.key}` : undefined}
|
|
578
|
+
/>
|
|
579
|
+
))}
|
|
580
|
+
</View>
|
|
566
581
|
</View>
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
</ScrollView>
|
|
582
|
+
))}
|
|
583
|
+
</View>
|
|
584
|
+
</Animated.ScrollView>
|
|
570
585
|
);
|
|
571
586
|
}
|
|
572
587
|
|
|
@@ -587,9 +602,7 @@ function RailRow({
|
|
|
587
602
|
}) {
|
|
588
603
|
const [hovered, setHovered] = useState(false);
|
|
589
604
|
const Icon = item.icon;
|
|
590
|
-
const onPress = item.page
|
|
591
|
-
? () => onSelect(item.page as string)
|
|
592
|
-
: item.onPress;
|
|
605
|
+
const onPress = item.page ? () => onSelect(item.page as string) : item.onPress;
|
|
593
606
|
return (
|
|
594
607
|
<Pressable
|
|
595
608
|
role="button"
|
|
@@ -620,7 +633,9 @@ function RailRow({
|
|
|
620
633
|
numberOfLines={1}
|
|
621
634
|
style={[
|
|
622
635
|
styles.railLabel,
|
|
623
|
-
{
|
|
636
|
+
{
|
|
637
|
+
color: selected || showChevron ? palette.text : palette.textSecondary,
|
|
638
|
+
},
|
|
624
639
|
]}
|
|
625
640
|
>
|
|
626
641
|
{item.label}
|
|
@@ -638,142 +653,102 @@ function RailRow({
|
|
|
638
653
|
// Close button, scroller, toast
|
|
639
654
|
// ---------------------------------------------------------------------------
|
|
640
655
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
palette: SettingsPalette;
|
|
650
|
-
testID?: string;
|
|
651
|
-
}) {
|
|
652
|
-
const [hovered, setHovered] = useState(false);
|
|
653
|
-
return (
|
|
654
|
-
<Pressable
|
|
655
|
-
role="button"
|
|
656
|
-
accessibilityLabel={label}
|
|
657
|
-
{...webDataSet({ bloomSettingsPress: '' })}
|
|
658
|
-
onPress={onPress}
|
|
659
|
-
onHoverIn={() => setHovered(true)}
|
|
660
|
-
onHoverOut={() => setHovered(false)}
|
|
661
|
-
testID={testID}
|
|
662
|
-
style={[
|
|
663
|
-
styles.close,
|
|
664
|
-
{ backgroundColor: hovered ? palette.tertiaryHover : palette.tertiary },
|
|
665
|
-
]}
|
|
666
|
-
>
|
|
667
|
-
<RiCloseLine width={16} height={16} fill={palette.iconSecondary} />
|
|
668
|
-
</Pressable>
|
|
669
|
-
);
|
|
670
|
-
}
|
|
656
|
+
/** Shared header on every layout; the modal already owns safe-area insets. */
|
|
657
|
+
type SettingsHeaderProps = {
|
|
658
|
+
title: string;
|
|
659
|
+
onBack?: () => void;
|
|
660
|
+
backLabel?: string;
|
|
661
|
+
closeLabel: string;
|
|
662
|
+
onClose: () => void;
|
|
663
|
+
};
|
|
671
664
|
|
|
672
|
-
|
|
673
|
-
* The `compact` title bar: optional back chevron, the title, the close button —
|
|
674
|
-
* 56px, the page's 16px inset, hairline under it.
|
|
675
|
-
*/
|
|
676
|
-
function CompactHeader({
|
|
665
|
+
function SettingsHeader({
|
|
677
666
|
title,
|
|
678
667
|
onBack,
|
|
679
668
|
backLabel,
|
|
680
669
|
closeLabel,
|
|
681
670
|
onClose,
|
|
682
671
|
palette,
|
|
672
|
+
scrollY,
|
|
673
|
+
scrimColor = palette.full,
|
|
683
674
|
testID,
|
|
684
|
-
}: {
|
|
685
|
-
title: string;
|
|
686
|
-
onBack?: () => void;
|
|
687
|
-
backLabel?: string;
|
|
688
|
-
closeLabel: string;
|
|
689
|
-
onClose: () => void;
|
|
675
|
+
}: SettingsHeaderProps & {
|
|
690
676
|
palette: SettingsPalette;
|
|
677
|
+
scrollY: SharedValue<number>;
|
|
678
|
+
scrimColor?: string;
|
|
691
679
|
testID?: string;
|
|
692
680
|
}) {
|
|
693
681
|
return (
|
|
694
|
-
<
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
{
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
</Text>
|
|
719
|
-
<SettingsCloseButton
|
|
720
|
-
label={closeLabel}
|
|
721
|
-
onPress={onClose}
|
|
722
|
-
palette={palette}
|
|
723
|
-
testID={testID ? `${testID}-close` : undefined}
|
|
724
|
-
/>
|
|
725
|
-
</View>
|
|
682
|
+
<PageHeader
|
|
683
|
+
title={title}
|
|
684
|
+
headingLevel={2}
|
|
685
|
+
onBack={onBack}
|
|
686
|
+
backLabel={backLabel}
|
|
687
|
+
safeArea={false}
|
|
688
|
+
sticky={IS_WEB}
|
|
689
|
+
placement="inline"
|
|
690
|
+
scrim="auto"
|
|
691
|
+
scrimColor={scrimColor}
|
|
692
|
+
scrollY={scrollY}
|
|
693
|
+
actions={
|
|
694
|
+
<ButtonGroup accessibilityLabel={closeLabel}>
|
|
695
|
+
<ButtonGroupItem
|
|
696
|
+
iconOnly
|
|
697
|
+
leadingIcon={RiCloseLine}
|
|
698
|
+
accessibilityLabel={closeLabel}
|
|
699
|
+
onPress={onClose}
|
|
700
|
+
testID={testID ? `${testID}-close` : undefined}
|
|
701
|
+
/>
|
|
702
|
+
</ButtonGroup>
|
|
703
|
+
}
|
|
704
|
+
testID={testID ? `${testID}-header` : undefined}
|
|
705
|
+
/>
|
|
726
706
|
);
|
|
727
707
|
}
|
|
728
708
|
|
|
709
|
+
/** Each page owns one full-height viewport, including its header. */
|
|
729
710
|
function PageScroller({
|
|
730
711
|
children,
|
|
712
|
+
header,
|
|
731
713
|
palette,
|
|
732
714
|
inset,
|
|
733
715
|
insetTop = 0,
|
|
734
716
|
testID,
|
|
735
717
|
}: {
|
|
736
718
|
children: React.ReactNode;
|
|
719
|
+
header: SettingsHeaderProps;
|
|
737
720
|
palette: SettingsPalette;
|
|
738
721
|
inset: number;
|
|
739
|
-
/**
|
|
722
|
+
/** Body spacing below the header; compact-title pages keep their smaller gap. */
|
|
740
723
|
insetTop?: number;
|
|
741
724
|
testID?: string;
|
|
742
725
|
}) {
|
|
743
|
-
const
|
|
744
|
-
const onScroll =
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
}, []);
|
|
748
|
-
const fade: WebCssStyle = {
|
|
749
|
-
opacity: scrolled ? 1 : 0,
|
|
750
|
-
...(IS_WEB
|
|
751
|
-
? { transitionProperty: 'opacity', transitionDuration: '200ms', transitionTimingFunction: 'ease-out' }
|
|
752
|
-
: null),
|
|
753
|
-
};
|
|
726
|
+
const scrollY = useSharedValue(0);
|
|
727
|
+
const onScroll = useAnimatedScrollHandler((event) => {
|
|
728
|
+
scrollY.value = Math.max(0, event.contentOffset.y);
|
|
729
|
+
}, [scrollY]);
|
|
754
730
|
return (
|
|
755
|
-
<
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
>
|
|
766
|
-
{children}
|
|
767
|
-
</ScrollView>
|
|
768
|
-
{/* Progressive top fade — `from-background-primary-default`. */}
|
|
731
|
+
<Animated.ScrollView
|
|
732
|
+
style={styles.scroll}
|
|
733
|
+
stickyHeaderIndices={IS_WEB ? undefined : [0]}
|
|
734
|
+
onScroll={onScroll}
|
|
735
|
+
scrollEventThrottle={16}
|
|
736
|
+
contentInsetAdjustmentBehavior="never"
|
|
737
|
+
automaticallyAdjustContentInsets={false}
|
|
738
|
+
testID={testID ? `${testID}-page` : undefined}
|
|
739
|
+
>
|
|
740
|
+
<SettingsHeader {...header} palette={palette} scrollY={scrollY} testID={testID} />
|
|
769
741
|
<View
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
742
|
+
style={{
|
|
743
|
+
paddingLeft: inset,
|
|
744
|
+
paddingRight: inset,
|
|
745
|
+
paddingBottom: inset,
|
|
746
|
+
paddingTop: insetTop,
|
|
747
|
+
}}
|
|
773
748
|
>
|
|
774
|
-
|
|
749
|
+
{children}
|
|
775
750
|
</View>
|
|
776
|
-
</
|
|
751
|
+
</Animated.ScrollView>
|
|
777
752
|
);
|
|
778
753
|
}
|
|
779
754
|
|
|
@@ -865,27 +840,6 @@ const styles = StyleSheet.create({
|
|
|
865
840
|
railChevron: {
|
|
866
841
|
marginLeft: 'auto',
|
|
867
842
|
},
|
|
868
|
-
compactHeader: {
|
|
869
|
-
flexDirection: 'row',
|
|
870
|
-
alignItems: 'center',
|
|
871
|
-
gap: 8,
|
|
872
|
-
height: 56,
|
|
873
|
-
paddingLeft: 16,
|
|
874
|
-
paddingRight: 16,
|
|
875
|
-
borderBottomWidth: 1,
|
|
876
|
-
},
|
|
877
|
-
compactBack: {
|
|
878
|
-
width: 32,
|
|
879
|
-
height: 32,
|
|
880
|
-
marginLeft: -8,
|
|
881
|
-
alignItems: 'center',
|
|
882
|
-
justifyContent: 'center',
|
|
883
|
-
borderRadius: 9999,
|
|
884
|
-
},
|
|
885
|
-
compactTitle: {
|
|
886
|
-
flex: 1,
|
|
887
|
-
minWidth: 0,
|
|
888
|
-
},
|
|
889
843
|
panel: {
|
|
890
844
|
flexDirection: 'row',
|
|
891
845
|
borderRadius: 24,
|
|
@@ -924,33 +878,9 @@ const styles = StyleSheet.create({
|
|
|
924
878
|
flex: 1,
|
|
925
879
|
minWidth: 0,
|
|
926
880
|
},
|
|
927
|
-
titleRow: {
|
|
928
|
-
flexDirection: 'row',
|
|
929
|
-
alignItems: 'center',
|
|
930
|
-
justifyContent: 'space-between',
|
|
931
|
-
},
|
|
932
|
-
close: {
|
|
933
|
-
width: 24,
|
|
934
|
-
height: 24,
|
|
935
|
-
borderRadius: 9999,
|
|
936
|
-
alignItems: 'center',
|
|
937
|
-
justifyContent: 'center',
|
|
938
|
-
},
|
|
939
|
-
scrollHost: {
|
|
940
|
-
flex: 1,
|
|
941
|
-
minHeight: 0,
|
|
942
|
-
position: 'relative',
|
|
943
|
-
},
|
|
944
881
|
scroll: {
|
|
945
882
|
flex: 1,
|
|
946
|
-
|
|
947
|
-
scrollContent: {},
|
|
948
|
-
fade: {
|
|
949
|
-
position: 'absolute',
|
|
950
|
-
left: 0,
|
|
951
|
-
right: 0,
|
|
952
|
-
top: 0,
|
|
953
|
-
height: 40,
|
|
883
|
+
minHeight: 0,
|
|
954
884
|
},
|
|
955
885
|
toast: {
|
|
956
886
|
position: 'absolute',
|