@oxy.so/bloom 4.1.0 → 4.2.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 (63) hide show
  1. package/docs/adoption-matrix.mdx +2 -2
  2. package/docs/app-shell.mdx +7 -0
  3. package/docs/bottom-bar.mdx +6 -0
  4. package/docs/settings-modal.mdx +8 -5
  5. package/lib/commonjs/app-shell/AppShellBars.js +8 -2
  6. package/lib/commonjs/app-shell/AppShellBars.js.map +1 -1
  7. package/lib/commonjs/app-shell/AppShellEngine.js +29 -6
  8. package/lib/commonjs/app-shell/AppShellEngine.js.map +1 -1
  9. package/lib/commonjs/bottom-bar/BottomBarBase.js +10 -2
  10. package/lib/commonjs/bottom-bar/BottomBarBase.js.map +1 -1
  11. package/lib/commonjs/layout/bottom-bar-slot.js +10 -0
  12. package/lib/commonjs/layout/bottom-bar-slot.js.map +1 -0
  13. package/lib/commonjs/settings-modal/SettingsModal.js +50 -135
  14. package/lib/commonjs/settings-modal/SettingsModal.js.map +1 -1
  15. package/lib/commonjs/sidebar/Sidebar.js +1 -1
  16. package/lib/commonjs/sidebar/Sidebar.js.map +1 -1
  17. package/lib/module/app-shell/AppShellBars.js +8 -2
  18. package/lib/module/app-shell/AppShellBars.js.map +1 -1
  19. package/lib/module/app-shell/AppShellEngine.js +30 -7
  20. package/lib/module/app-shell/AppShellEngine.js.map +1 -1
  21. package/lib/module/bottom-bar/BottomBarBase.js +11 -3
  22. package/lib/module/bottom-bar/BottomBarBase.js.map +1 -1
  23. package/lib/module/layout/bottom-bar-slot.js +7 -0
  24. package/lib/module/layout/bottom-bar-slot.js.map +1 -0
  25. package/lib/module/settings-modal/SettingsModal.js +43 -128
  26. package/lib/module/settings-modal/SettingsModal.js.map +1 -1
  27. package/lib/module/sidebar/Sidebar.js +1 -1
  28. package/lib/module/sidebar/Sidebar.js.map +1 -1
  29. package/lib/typescript/commonjs/app-shell/AppShellBars.d.ts +1 -0
  30. package/lib/typescript/commonjs/app-shell/AppShellBars.d.ts.map +1 -1
  31. package/lib/typescript/commonjs/app-shell/AppShellEngine.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/bottom-bar/BottomBarBase.d.ts +1 -1
  33. package/lib/typescript/commonjs/bottom-bar/BottomBarBase.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/bottom-bar/types.d.ts +4 -0
  35. package/lib/typescript/commonjs/bottom-bar/types.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/layout/bottom-bar-slot.d.ts +3 -0
  37. package/lib/typescript/commonjs/layout/bottom-bar-slot.d.ts.map +1 -0
  38. package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts +1 -1
  39. package/lib/typescript/commonjs/settings-modal/SettingsModal.d.ts.map +1 -1
  40. package/lib/typescript/commonjs/settings-modal/types.d.ts +4 -2
  41. package/lib/typescript/commonjs/settings-modal/types.d.ts.map +1 -1
  42. package/lib/typescript/module/app-shell/AppShellBars.d.ts +1 -0
  43. package/lib/typescript/module/app-shell/AppShellBars.d.ts.map +1 -1
  44. package/lib/typescript/module/app-shell/AppShellEngine.d.ts.map +1 -1
  45. package/lib/typescript/module/bottom-bar/BottomBarBase.d.ts +1 -1
  46. package/lib/typescript/module/bottom-bar/BottomBarBase.d.ts.map +1 -1
  47. package/lib/typescript/module/bottom-bar/types.d.ts +4 -0
  48. package/lib/typescript/module/bottom-bar/types.d.ts.map +1 -1
  49. package/lib/typescript/module/layout/bottom-bar-slot.d.ts +3 -0
  50. package/lib/typescript/module/layout/bottom-bar-slot.d.ts.map +1 -0
  51. package/lib/typescript/module/settings-modal/SettingsModal.d.ts +1 -1
  52. package/lib/typescript/module/settings-modal/SettingsModal.d.ts.map +1 -1
  53. package/lib/typescript/module/settings-modal/types.d.ts +4 -2
  54. package/lib/typescript/module/settings-modal/types.d.ts.map +1 -1
  55. package/package.json +1 -1
  56. package/src/app-shell/AppShellBars.tsx +5 -3
  57. package/src/app-shell/AppShellEngine.tsx +23 -8
  58. package/src/bottom-bar/BottomBarBase.tsx +7 -4
  59. package/src/bottom-bar/types.ts +4 -0
  60. package/src/layout/bottom-bar-slot.ts +4 -0
  61. package/src/settings-modal/SettingsModal.tsx +48 -136
  62. package/src/settings-modal/types.ts +4 -2
  63. package/src/sidebar/Sidebar.tsx +1 -1
@@ -1,8 +1,9 @@
1
- import { useRef, useState, type ComponentType } from 'react';
1
+ import { useContext, useRef, useState, type ComponentType } from 'react';
2
2
  import { View, ScrollView, Keyboard, Platform } from 'react-native';
3
3
  import { useEffect } from 'react';
4
4
  import Animated, { useAnimatedReaction, useAnimatedStyle, interpolate, runOnJS, useReducedMotion } from 'react-native-reanimated';
5
5
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
6
+ import { BottomBarSlotContext } from '../layout/bottom-bar-slot';
6
7
  import { useClaimBottomEdge } from '../layout/bottom-edge';
7
8
  import { FAB_METRICS } from '../fab/constants';
8
9
  import { windowEdgeGap } from '../layout/edge';
@@ -17,7 +18,7 @@ interface Props extends BottomBarProps {
17
18
  Item: ComponentType<TabBarButtonProps>;
18
19
  Blur: ComponentType<ProgressiveBlurProps>;
19
20
  }
20
- export function BottomBarBase({ Navigation, Item, Blur, items, value, onValueChange, action,
21
+ export function BottomBarBase({ Navigation, Item, Blur, items, value, onValueChange, activeProgress, onValueLongPress, action,
21
22
  actionPlacement = 'auto', actionBehavior = 'hide', material = 'translucent', minimizeProgress, blur = true, maxWidth = 560, style, testID }: Props) {
22
23
  const insets = useSafeAreaInsets();
23
24
  const minimize = useMinimizeState();
@@ -45,14 +46,15 @@ export function BottomBarBase({ Navigation, Item, Blur, items, value, onValueCha
45
46
  node.inert = actionHidden;
46
47
  }, [actionHidden]);
47
48
  const [keyboardVisible, setKeyboardVisible] = useState(false);
48
- const bottom = windowEdgeGap(insets.bottom);
49
+ const suppliedBottomInset = useContext(BottomBarSlotContext);
50
+ const bottom = Math.max(0, windowEdgeGap(insets.bottom) - (suppliedBottomInset ?? 0));
49
51
  useEffect(() => {
50
52
  if (Platform.OS === 'web') return;
51
53
  const show = Keyboard.addListener('keyboardDidShow', () => setKeyboardVisible(true));
52
54
  const hide = Keyboard.addListener('keyboardDidHide', () => setKeyboardVisible(false));
53
55
  return () => { show.remove(); hide.remove(); };
54
56
  }, []);
55
- useClaimBottomEdge(keyboardVisible ? 0 : bottom + footprint);
57
+ useClaimBottomEdge(keyboardVisible || suppliedBottomInset !== undefined ? 0 : bottom + footprint);
56
58
  const rowStyle = useAnimatedStyle(() => ({
57
59
  height: interpolate(reducedMotion ? Number(progress.value > 0.01) : progress.value, [0, 1], [EXPANDED_HEIGHT, MINIMIZED_HEIGHT], 'clamp'),
58
60
  }), [progress, reducedMotion]);
@@ -70,6 +72,7 @@ export function BottomBarBase({ Navigation, Item, Blur, items, value, onValueCha
70
72
  {blur && material === 'translucent' && <Blur direction="bottom" style={{ position: 'absolute', left: 0, right: 0, bottom: 0, height: bottom + footprint + BLUR_BLEED }} />}
71
73
  <Animated.View testID={testID ? `${testID}-row` : undefined} onLayout={event => setRowWidth(event.nativeEvent.layout.width)} pointerEvents="box-none" style={[{ justifyContent: items.length ? 'center' : 'flex-end', alignSelf: 'center', width: '100%', maxWidth, paddingLeft: 12, paddingRight: 12, marginBottom: bottom, flexDirection: 'row', alignItems: 'center' }, rowStyle]}>
72
74
  {items.length > 0 && <View testID={testID ? `${testID}-navigation` : undefined} style={{ width: navigationWidth, minWidth: 0 }}><ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={{ minWidth: '100%' }}><View style={{ width: Math.max(navigationWidth, items.length * 44 + 8) }}><Navigation scrollable={navigationWidth > 0 && navigationWidth < items.length * 44 + 8} embedded blur={false} material={material} minimizeProgress={progress}
75
+ activeProgress={activeProgress} onIndexLongPress={onValueLongPress ? index => { if (items[index]) onValueLongPress(items[index].name); } : undefined}
73
76
  activeIndex={items.findIndex(item => item.name === value)} onIndexChange={index => { if (items[index]) onValueChange(items[index].name); }}>
74
77
  {items.map((item, index) => <Item key={item.name} item={item} index={index} />)}
75
78
  </Navigation></View></ScrollView></View>}
@@ -7,6 +7,10 @@ export interface BottomBarProps {
7
7
  items: TabBarItem[];
8
8
  value: string;
9
9
  onValueChange: (value: string) => void;
10
+ /** Optional continuous selection supplied by a swipe pager, in item-index space. */
11
+ activeProgress?: SharedValue<number>;
12
+ /** Long press on a destination (for example an account switcher). */
13
+ onValueLongPress?: (value: string) => void;
10
14
  /** Static action sibling; outside the navigation scrub detector. */
11
15
  action?: ReactNode;
12
16
  /** Auto places the action above navigation when side-by-side targets would be cramped. */
@@ -0,0 +1,4 @@
1
+ import { createContext } from 'react';
2
+
3
+ /** Bottom safe area already supplied by the measured app-shell slot. */
4
+ export const BottomBarSlotContext = createContext<number | undefined>(undefined);
@@ -26,9 +26,10 @@ import Animated, {
26
26
 
27
27
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
28
28
 
29
- import { RiArrowLeftSLine } from '../icons/remix/RiArrowLeftSLine';
30
29
  import { RiArrowRightSLine } from '../icons/remix/RiArrowRightSLine';
31
30
  import { RiCheckboxCircleFill } from '../icons/remix/RiCheckboxCircleFill';
31
+ import { PageHeader } from '../page-header';
32
+ import { ButtonGroup, ButtonGroupItem } from '../button-group';
32
33
  import { RiCloseLine } from '../icons/remix/RiCloseLine';
33
34
  import { VerticalFade } from './SettingsArt';
34
35
  import { Backdrop, OverlayRoot } from '../overlay';
@@ -58,9 +59,7 @@ import { IS_WEB, useSettingsWebCss } from './web-css';
58
59
  * 20px icon (icon/secondary) + 8 + body-medium label
59
60
  * (selected: background/secondary/hover + text/primary;
60
61
  * hover: background/secondary/hover @ 60%)
61
- * content title row px 32 pt 32 pb 12 (Storage 6), title-3-medium +
62
- * 24px round close (background/tertiary, hover tertiary/hover,
63
- * 16px glyph); the page scrolls under a 40px top fade that eases
62
+ * content shared inline PageHeader and close ButtonGroup island; the page scrolls under a 40px top fade that eases
64
63
  * in (200ms) once it is scrolled; px 32 pb 32
65
64
  * toast "Saved" pill straddling the panel's bottom edge: 1px
66
65
  * border/button, background/primary, py 4 pr 10 pl 6, gap 4,
@@ -133,6 +132,7 @@ export function SettingsModal({
133
132
  page: controlledPage,
134
133
  defaultPage,
135
134
  onPageChange,
135
+ initialView = 'navigation',
136
136
  labels,
137
137
  testID,
138
138
  }: SettingsModalProps) {
@@ -156,7 +156,9 @@ export function SettingsModal({
156
156
  const insets = useSafeAreaInsets();
157
157
 
158
158
  // `compact` layout is two levels: the section list, then a page pushed over it.
159
- const [compactPageOpen, setCompactPageOpen] = useState(false);
159
+ const [compactPageOpen, setCompactPageOpen] = useState(initialView === 'page');
160
+ const initialViewRef = useRef(initialView);
161
+ initialViewRef.current = initialView;
160
162
 
161
163
  const onCloseRef = useRef(onClose);
162
164
  onCloseRef.current = onClose;
@@ -167,7 +169,7 @@ export function SettingsModal({
167
169
 
168
170
  const show = useCallback(() => {
169
171
  setInternalPage(defaultPageRef.current);
170
- setCompactPageOpen(false);
172
+ setCompactPageOpen(initialViewRef.current === 'page');
171
173
  setMounted(true);
172
174
  setOpenCount((count) => count + 1);
173
175
  }, []);
@@ -398,7 +400,7 @@ export function SettingsModal({
398
400
  {layout === 'compact' ? (
399
401
  compactPageOpen && pageConfig ? (
400
402
  <View style={styles.content}>
401
- <CompactHeader
403
+ <SettingsHeader
402
404
  title={pageConfig.title}
403
405
  onBack={() => setCompactPageOpen(false)}
404
406
  backLabel={labels?.back ?? 'Back'}
@@ -419,7 +421,7 @@ export function SettingsModal({
419
421
  </View>
420
422
  ) : (
421
423
  <View style={styles.content}>
422
- <CompactHeader
424
+ <SettingsHeader
423
425
  title={labels?.dialog ?? 'Settings'}
424
426
  closeLabel={closeLabel}
425
427
  onClose={requestClose}
@@ -449,36 +451,18 @@ export function SettingsModal({
449
451
  testID={testID}
450
452
  />
451
453
  <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>
454
+ <SettingsHeader
455
+ title={pageConfig?.title ?? ''}
456
+ closeLabel={closeLabel}
457
+ onClose={requestClose}
458
+ palette={palette}
459
+ testID={testID}
460
+ />
478
461
  <PageScroller
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}
@@ -638,42 +622,8 @@ function RailRow({
638
622
  // Close button, scroller, toast
639
623
  // ---------------------------------------------------------------------------
640
624
 
641
- function SettingsCloseButton({
642
- label,
643
- onPress,
644
- palette,
645
- testID,
646
- }: {
647
- label: string;
648
- onPress: () => void;
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
- }
671
-
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({
625
+ /** Shared header on every layout; the modal already owns safe-area insets. */
626
+ function SettingsHeader({
677
627
  title,
678
628
  onBack,
679
629
  backLabel,
@@ -690,39 +640,34 @@ function CompactHeader({
690
640
  palette: SettingsPalette;
691
641
  testID?: string;
692
642
  }) {
643
+ // Inline chrome does not reveal with scroll. An explicit local offset keeps
644
+ // the page behind the modal from influencing this header's paint or title.
645
+ const scrollY = useSharedValue(0);
693
646
  return (
694
- <View
695
- style={[styles.compactHeader, { borderBottomColor: palette.separator }]}
696
- testID={testID ? `${testID}-title-row` : undefined}
697
- >
698
- {onBack ? (
699
- <Pressable
700
- role="button"
701
- accessibilityLabel={backLabel}
702
- {...webDataSet({ bloomSettingsPress: '' })}
703
- onPress={onBack}
704
- hitSlop={8}
705
- testID={testID ? `${testID}-back` : undefined}
706
- style={styles.compactBack}
707
- >
708
- <RiArrowLeftSLine width={24} height={24} fill={palette.text} />
709
- </Pressable>
710
- ) : null}
711
- <Text
712
- variant="headline-medium"
713
- role="heading"
714
- numberOfLines={1}
715
- style={[styles.compactTitle, { color: palette.text }]}
716
- >
717
- {title}
718
- </Text>
719
- <SettingsCloseButton
720
- label={closeLabel}
721
- onPress={onClose}
722
- palette={palette}
723
- testID={testID ? `${testID}-close` : undefined}
724
- />
725
- </View>
647
+ <PageHeader
648
+ title={title}
649
+ headingLevel={2}
650
+ onBack={onBack}
651
+ backLabel={backLabel}
652
+ safeArea={false}
653
+ sticky={false}
654
+ placement="inline"
655
+ scrim="none"
656
+ scrimColor={palette.full}
657
+ scrollY={scrollY}
658
+ actions={
659
+ <ButtonGroup accessibilityLabel={closeLabel}>
660
+ <ButtonGroupItem
661
+ iconOnly
662
+ leadingIcon={RiCloseLine}
663
+ accessibilityLabel={closeLabel}
664
+ onPress={onClose}
665
+ testID={testID ? `${testID}-close` : undefined}
666
+ />
667
+ </ButtonGroup>
668
+ }
669
+ testID={testID ? `${testID}-header` : undefined}
670
+ />
726
671
  );
727
672
  }
728
673
 
@@ -736,7 +681,7 @@ function PageScroller({
736
681
  children: React.ReactNode;
737
682
  palette: SettingsPalette;
738
683
  inset: number;
739
- /** Top inset — only the compact page, which has no title row above its content. */
684
+ /** Content gap below PageHeader; desktop Storage uses the compact 6px gap. */
740
685
  insetTop?: number;
741
686
  testID?: string;
742
687
  }) {
@@ -865,27 +810,6 @@ const styles = StyleSheet.create({
865
810
  railChevron: {
866
811
  marginLeft: 'auto',
867
812
  },
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
813
  panel: {
890
814
  flexDirection: 'row',
891
815
  borderRadius: 24,
@@ -924,18 +848,6 @@ const styles = StyleSheet.create({
924
848
  flex: 1,
925
849
  minWidth: 0,
926
850
  },
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
851
  scrollHost: {
940
852
  flex: 1,
941
853
  minHeight: 0,
@@ -33,11 +33,11 @@ export interface SettingsNavGroup {
33
33
 
34
34
  /** One page of the content pane. */
35
35
  export interface SettingsModalPage {
36
- /** Heading of the fixed title row. */
36
+ /** Heading of the shared PageHeader. */
37
37
  title: string;
38
38
  content: ReactNode;
39
39
  /**
40
- * Used on the Storage page: a 6px title gap instead of 12px, because the page
40
+ * Used on the Storage page: a 6px gap below PageHeader instead of 12px, because the page
41
41
  * already carries 10px of headroom for the dropzone's progress badge.
42
42
  */
43
43
  compactTitle?: boolean;
@@ -73,6 +73,8 @@ export interface SettingsModalProps {
73
73
  /** Page selected every time the modal opens. Defaults to the first navigable row. */
74
74
  defaultPage?: string;
75
75
  onPageChange?: (page: string) => void;
76
+ /** Compact layout: start at the section list (default) or open the selected page directly. Resets on each open. */
77
+ initialView?: 'navigation' | 'page';
76
78
  labels?: SettingsModalLabels;
77
79
  testID?: string;
78
80
  }
@@ -388,7 +388,7 @@ const SidebarPanel: React.FC<SidebarProps> = ({
388
388
  setSuppressUserHover(expanding);
389
389
  };
390
390
 
391
- const headerControl = flatMobile ? (
391
+ const headerControl = flatMobile && showSearch ? (
392
392
  <View
393
393
  ref={searchFieldRef}
394
394
  style={{