@duro-app/ui 5.12.0 → 5.14.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/README.md +49 -0
- package/dist/Table-DnaoH-h5.js +1806 -0
- package/dist/Table-DnaoH-h5.js.map +1 -0
- package/dist/components/AppShell/native.css.d.ts +53 -0
- package/dist/components/AppShell/native.css.d.ts.map +1 -0
- package/dist/components/Button/Button.d.ts.map +1 -1
- package/dist/components/ColorMode/ColorMode.d.ts +2 -1
- package/dist/components/ColorMode/ColorMode.d.ts.map +1 -1
- package/dist/components/ColorMode/storage.d.ts +4 -0
- package/dist/components/ColorMode/storage.d.ts.map +1 -0
- package/dist/components/ColorMode/styles.css.d.ts +4 -0
- package/dist/components/ColorMode/styles.css.d.ts.map +1 -1
- package/dist/components/ColorMode/useSystemScheme.d.ts +7 -0
- package/dist/components/ColorMode/useSystemScheme.d.ts.map +1 -0
- package/dist/components/Combobox/Combobox.d.ts +7 -2
- package/dist/components/Combobox/Combobox.d.ts.map +1 -1
- package/dist/components/Combobox/comboboxState.d.ts +29 -0
- package/dist/components/Combobox/comboboxState.d.ts.map +1 -0
- package/dist/components/Combobox/native.css.d.ts +27 -0
- package/dist/components/Combobox/native.css.d.ts.map +1 -0
- package/dist/components/Combobox/useComboboxRoot.d.ts +6 -10
- package/dist/components/Combobox/useComboboxRoot.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.d.ts +12 -37
- package/dist/components/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
- package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -1
- package/dist/components/Drawer/DrawerSnap.stories.d.ts +22 -0
- package/dist/components/Drawer/DrawerSnap.stories.d.ts.map +1 -0
- package/dist/components/Drawer/native.css.d.ts +59 -0
- package/dist/components/Drawer/native.css.d.ts.map +1 -0
- package/dist/components/Drawer/parts.css.d.ts +96 -0
- package/dist/components/Drawer/parts.css.d.ts.map +1 -0
- package/dist/components/Drawer/parts.d.ts +56 -0
- package/dist/components/Drawer/parts.d.ts.map +1 -0
- package/dist/components/Drawer/sheetPhysics.d.ts +56 -0
- package/dist/components/Drawer/sheetPhysics.d.ts.map +1 -0
- package/dist/components/Drawer/styles.css.d.ts +16 -57
- package/dist/components/Drawer/styles.css.d.ts.map +1 -1
- package/dist/components/Drawer/touchInput.d.ts +35 -0
- package/dist/components/Drawer/touchInput.d.ts.map +1 -0
- package/dist/components/Drawer/useDrawerRoot.d.ts +33 -0
- package/dist/components/Drawer/useDrawerRoot.d.ts.map +1 -0
- package/dist/components/Drawer/useSheetHeights.d.ts +7 -0
- package/dist/components/Drawer/useSheetHeights.d.ts.map +1 -0
- package/dist/components/Drawer/useSwipeDismiss.d.ts +13 -2
- package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
- package/dist/components/Form/FormSubmitContext.d.ts +8 -0
- package/dist/components/Form/FormSubmitContext.d.ts.map +1 -0
- package/dist/components/Listbox/native.css.d.ts +84 -0
- package/dist/components/Listbox/native.css.d.ts.map +1 -0
- package/dist/components/Menu/Menu.d.ts +7 -1
- package/dist/components/Menu/Menu.d.ts.map +1 -1
- package/dist/components/Menu/native.css.d.ts +14 -0
- package/dist/components/Menu/native.css.d.ts.map +1 -0
- package/dist/components/Menu/useMenuOpen.d.ts +16 -0
- package/dist/components/Menu/useMenuOpen.d.ts.map +1 -0
- package/dist/components/Menu/useMenuRoot.d.ts +3 -6
- package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
- package/dist/components/Popover/native.css.d.ts +11 -0
- package/dist/components/Popover/native.css.d.ts.map +1 -0
- package/dist/components/Select/Select.d.ts +9 -2
- package/dist/components/Select/Select.d.ts.map +1 -1
- package/dist/components/Select/Select.stories.d.ts +11 -0
- package/dist/components/Select/Select.stories.d.ts.map +1 -1
- package/dist/components/Select/native.css.d.ts +11 -0
- package/dist/components/Select/native.css.d.ts.map +1 -0
- package/dist/components/Select/selectState.d.ts +21 -0
- package/dist/components/Select/selectState.d.ts.map +1 -0
- package/dist/components/Select/styles.css.d.ts +10 -0
- package/dist/components/Select/styles.css.d.ts.map +1 -1
- package/dist/components/Select/useSelectRoot.d.ts +6 -8
- package/dist/components/Select/useSelectRoot.d.ts.map +1 -1
- package/dist/components/SideNav/Chevron.d.ts +4 -0
- package/dist/components/SideNav/Chevron.d.ts.map +1 -0
- package/dist/components/SideNav/SideNav.d.ts.map +1 -1
- package/dist/components/SideNav/styles.css.d.ts +6 -0
- package/dist/components/SideNav/styles.css.d.ts.map +1 -1
- package/dist/components/SideNav/types.d.ts +6 -0
- package/dist/components/SideNav/types.d.ts.map +1 -0
- package/dist/components/SplitPane/SplitPane.d.ts +38 -0
- package/dist/components/SplitPane/SplitPane.d.ts.map +1 -0
- package/dist/components/SplitPane/SplitPane.meta.d.ts +3 -0
- package/dist/components/SplitPane/SplitPane.meta.d.ts.map +1 -0
- package/dist/components/SplitPane/SplitPane.stories.d.ts +17 -0
- package/dist/components/SplitPane/SplitPane.stories.d.ts.map +1 -0
- package/dist/components/SplitPane/styles.css.d.ts +78 -0
- package/dist/components/SplitPane/styles.css.d.ts.map +1 -0
- package/dist/components/Tabs/TabLayout.d.ts +7 -0
- package/dist/components/Tabs/TabLayout.d.ts.map +1 -0
- package/dist/components/Tabs/Tabs.d.ts.map +1 -1
- package/dist/components/Tabs/indicator.d.ts +28 -0
- package/dist/components/Tabs/indicator.d.ts.map +1 -0
- package/dist/components/Tabs/styles.css.d.ts +4 -0
- package/dist/components/Tabs/styles.css.d.ts.map +1 -1
- package/dist/components/Tabs/useTabIndicator.d.ts +4 -0
- package/dist/components/Tabs/useTabIndicator.d.ts.map +1 -0
- package/dist/components/Tabs/useTabKeys.d.ts +13 -0
- package/dist/components/Tabs/useTabKeys.d.ts.map +1 -0
- package/dist/components/Tag/RemoveGlyph.d.ts +4 -0
- package/dist/components/Tag/RemoveGlyph.d.ts.map +1 -0
- package/dist/components/Tag/Tag.d.ts +2 -24
- package/dist/components/Tag/Tag.d.ts.map +1 -1
- package/dist/components/Tag/styles.css.d.ts +8 -0
- package/dist/components/Tag/styles.css.d.ts.map +1 -1
- package/dist/components/Tag/types.d.ts +39 -0
- package/dist/components/Tag/types.d.ts.map +1 -0
- package/dist/components/ThemeProvider/ThemeOverrides.stories.d.ts +13 -0
- package/dist/components/ThemeProvider/ThemeOverrides.stories.d.ts.map +1 -0
- package/dist/components/ThemeProvider/ThemeProvider.d.ts +9 -2
- package/dist/components/ThemeProvider/ThemeProvider.d.ts.map +1 -1
- package/dist/components/ThemeProvider/overrideRule.d.ts +7 -0
- package/dist/components/ThemeProvider/overrideRule.d.ts.map +1 -0
- package/dist/components/ThemeProvider/overrides.d.ts +16 -0
- package/dist/components/ThemeProvider/overrides.d.ts.map +1 -0
- package/dist/components/Toast/ToastProvider.d.ts +2 -21
- package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
- package/dist/components/Toast/native.css.d.ts +81 -0
- package/dist/components/Toast/native.css.d.ts.map +1 -0
- package/dist/components/Toast/store.d.ts +47 -0
- package/dist/components/Toast/store.d.ts.map +1 -0
- package/dist/components/Tooltip/TooltipHint.d.ts +9 -0
- package/dist/components/Tooltip/TooltipHint.d.ts.map +1 -0
- package/dist/components/Tree/Tree.d.ts.map +1 -1
- package/dist/components/Tree/styles.css.d.ts +6 -0
- package/dist/components/Tree/styles.css.d.ts.map +1 -1
- package/dist/components/Tree/useTreeKeyboard.d.ts +19 -0
- package/dist/components/Tree/useTreeKeyboard.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.d.ts +33 -0
- package/dist/components/VirtualList/VirtualList.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.meta.d.ts +3 -0
- package/dist/components/VirtualList/VirtualList.meta.d.ts.map +1 -0
- package/dist/components/VirtualList/VirtualList.stories.d.ts +15 -0
- package/dist/components/VirtualList/VirtualList.stories.d.ts.map +1 -0
- package/dist/components/VirtualList/styles.css.d.ts +28 -0
- package/dist/components/VirtualList/styles.css.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5090 -4381
- package/dist/index.js.map +1 -1
- package/dist/shared/ButtonHost.d.ts +11 -0
- package/dist/shared/ButtonHost.d.ts.map +1 -0
- package/dist/shared/pressableChild.d.ts +14 -0
- package/dist/shared/pressableChild.d.ts.map +1 -0
- package/dist/shared/textOf.d.ts +7 -0
- package/dist/shared/textOf.d.ts.map +1 -0
- package/dist/table.js +1 -1
- package/package.json +3 -2
- package/src/components/AppShell/AppShell.meta.ts +1 -1
- package/src/components/AppShell/AppShell.native.tsx +174 -0
- package/src/components/AppShell/native.css.ts +56 -0
- package/src/components/Button/Button.tsx +15 -4
- package/src/components/ColorMode/ColorMode.meta.ts +1 -1
- package/src/components/ColorMode/ColorMode.tsx +16 -37
- package/src/components/ColorMode/storage.native.ts +8 -0
- package/src/components/ColorMode/storage.ts +19 -0
- package/src/components/ColorMode/styles.css.ts +6 -0
- package/src/components/ColorMode/useSystemScheme.native.ts +6 -0
- package/src/components/ColorMode/useSystemScheme.ts +19 -0
- package/src/components/Combobox/Combobox.meta.ts +1 -1
- package/src/components/Combobox/Combobox.native.tsx +295 -0
- package/src/components/Combobox/Combobox.tsx +14 -8
- package/src/components/Combobox/comboboxState.ts +107 -0
- package/src/components/Combobox/native.css.ts +33 -0
- package/src/components/Combobox/useComboboxRoot.ts +17 -91
- package/src/components/Drawer/Drawer.meta.ts +2 -1
- package/src/components/Drawer/Drawer.native.tsx +491 -0
- package/src/components/Drawer/Drawer.tsx +65 -143
- package/src/components/Drawer/DrawerScroll.stories.tsx +4 -54
- package/src/components/Drawer/DrawerSnap.stories.tsx +225 -0
- package/src/components/Drawer/native.css.ts +62 -0
- package/src/components/Drawer/parts.css.ts +125 -0
- package/src/components/Drawer/parts.tsx +110 -0
- package/src/components/Drawer/sheetPhysics.ts +103 -0
- package/src/components/Drawer/styles.css.ts +18 -73
- package/src/components/Drawer/touchInput.ts +81 -0
- package/src/components/Drawer/useDrawerRoot.ts +90 -0
- package/src/components/Drawer/useSheetHeights.ts +42 -0
- package/src/components/Drawer/useSwipeDismiss.ts +146 -23
- package/src/components/Form/Form.meta.ts +1 -1
- package/src/components/Form/Form.native.tsx +120 -0
- package/src/components/Form/FormSubmitContext.ts +12 -0
- package/src/components/Listbox/Listbox.meta.ts +1 -1
- package/src/components/Listbox/Listbox.native.tsx +153 -0
- package/src/components/Listbox/native.css.ts +91 -0
- package/src/components/Menu/Menu.meta.ts +1 -1
- package/src/components/Menu/Menu.native.tsx +268 -0
- package/src/components/Menu/Menu.tsx +11 -2
- package/src/components/Menu/native.css.ts +18 -0
- package/src/components/Menu/useMenuOpen.ts +33 -0
- package/src/components/Menu/useMenuRoot.ts +7 -26
- package/src/components/Popover/Popover.meta.ts +1 -1
- package/src/components/Popover/Popover.native.tsx +205 -0
- package/src/components/Popover/native.css.ts +13 -0
- package/src/components/ScrollArea/ScrollArea.meta.ts +1 -1
- package/src/components/ScrollArea/ScrollArea.native.tsx +61 -0
- package/src/components/Select/Select.meta.ts +1 -1
- package/src/components/Select/Select.native.tsx +224 -0
- package/src/components/Select/Select.stories.tsx +49 -1
- package/src/components/Select/Select.tsx +20 -3
- package/src/components/Select/native.css.ts +13 -0
- package/src/components/Select/selectState.ts +36 -0
- package/src/components/Select/styles.css.ts +11 -0
- package/src/components/Select/useSelectRoot.ts +9 -30
- package/src/components/SideNav/Chevron.native.tsx +20 -0
- package/src/components/SideNav/Chevron.tsx +20 -0
- package/src/components/SideNav/SideNav.meta.ts +1 -1
- package/src/components/SideNav/SideNav.tsx +57 -41
- package/src/components/SideNav/styles.css.ts +8 -0
- package/src/components/SideNav/types.ts +6 -0
- package/src/components/SplitPane/SplitPane.meta.ts +40 -0
- package/src/components/SplitPane/SplitPane.native.tsx +47 -0
- package/src/components/SplitPane/SplitPane.stories.tsx +148 -0
- package/src/components/SplitPane/SplitPane.tsx +218 -0
- package/src/components/SplitPane/styles.css.ts +81 -0
- package/src/components/Tabs/TabLayout.native.tsx +9 -0
- package/src/components/Tabs/TabLayout.tsx +6 -0
- package/src/components/Tabs/Tabs.meta.ts +1 -1
- package/src/components/Tabs/Tabs.tsx +38 -136
- package/src/components/Tabs/indicator.ts +36 -0
- package/src/components/Tabs/styles.css.ts +7 -0
- package/src/components/Tabs/useTabIndicator.native.ts +35 -0
- package/src/components/Tabs/useTabIndicator.ts +49 -0
- package/src/components/Tabs/useTabKeys.native.ts +2 -0
- package/src/components/Tabs/useTabKeys.ts +99 -0
- package/src/components/Tag/RemoveGlyph.native.tsx +17 -0
- package/src/components/Tag/RemoveGlyph.tsx +10 -0
- package/src/components/Tag/Tag.meta.ts +1 -1
- package/src/components/Tag/Tag.native.tsx +70 -0
- package/src/components/Tag/Tag.tsx +9 -37
- package/src/components/Tag/styles.css.ts +9 -0
- package/src/components/Tag/types.ts +43 -0
- package/src/components/ThemeProvider/ThemeOverrides.stories.tsx +113 -0
- package/src/components/ThemeProvider/ThemeProvider.meta.ts +1 -1
- package/src/components/ThemeProvider/ThemeProvider.native.tsx +36 -7
- package/src/components/ThemeProvider/ThemeProvider.tsx +42 -6
- package/src/components/ThemeProvider/overrideRule.ts +39 -0
- package/src/components/ThemeProvider/overrides.ts +27 -0
- package/src/components/Toast/Toast.meta.ts +1 -1
- package/src/components/Toast/ToastProvider.native.tsx +226 -0
- package/src/components/Toast/ToastProvider.tsx +22 -84
- package/src/components/Toast/native.css.ts +78 -0
- package/src/components/Toast/store.ts +96 -0
- package/src/components/Toggle/Toggle.tsx +3 -3
- package/src/components/Tooltip/Tooltip.meta.ts +1 -1
- package/src/components/Tooltip/Tooltip.native.tsx +36 -0
- package/src/components/Tooltip/TooltipHint.ts +13 -0
- package/src/components/Tree/Tree.meta.ts +1 -1
- package/src/components/Tree/Tree.tsx +23 -135
- package/src/components/Tree/styles.css.ts +6 -0
- package/src/components/Tree/useTreeKeyboard.native.ts +5 -0
- package/src/components/Tree/useTreeKeyboard.ts +152 -0
- package/src/components/VirtualList/VirtualList.meta.ts +39 -0
- package/src/components/VirtualList/VirtualList.stories.tsx +95 -0
- package/src/components/VirtualList/VirtualList.tsx +103 -0
- package/src/components/VirtualList/styles.css.ts +31 -0
- package/src/form.native.ts +7 -0
- package/src/hooks/useContainerBelow.native.ts +30 -0
- package/src/index.native.ts +48 -0
- package/src/index.ts +9 -1
- package/src/shared/ButtonHost.native.tsx +70 -0
- package/src/shared/ButtonHost.tsx +12 -0
- package/src/shared/focusInput.native.ts +18 -0
- package/src/shared/portal/AnchoredOverlay.native.tsx +117 -0
- package/src/shared/pressableChild.ts +33 -0
- package/src/shared/textOf.ts +13 -0
- package/src/shared/useKeyboardHeight.native.ts +18 -0
- package/src/shared/useScreenReaderEnabled.native.ts +19 -0
- package/dist/Table-ColfpgKV.js +0 -1552
- package/dist/Table-ColfpgKV.js.map +0 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
import {duration} from '@duro-app/tokens/tokens/motion.css'
|
|
6
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
7
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
8
|
+
|
|
9
|
+
/** Styles of the Drawer parts both platforms draw (no animations: native has none). */
|
|
10
|
+
export const partStyles = css.create({
|
|
11
|
+
// --- Grabber (snap sheets) ---
|
|
12
|
+
grabberHit: {
|
|
13
|
+
display: 'flex',
|
|
14
|
+
alignItems: 'center',
|
|
15
|
+
justifyContent: 'center',
|
|
16
|
+
alignSelf: 'stretch',
|
|
17
|
+
flexShrink: 0,
|
|
18
|
+
paddingTop: spacing.sm,
|
|
19
|
+
paddingBottom: spacing.sm,
|
|
20
|
+
borderWidth: 0,
|
|
21
|
+
backgroundColor: 'transparent',
|
|
22
|
+
},
|
|
23
|
+
grabber: {
|
|
24
|
+
display: 'block',
|
|
25
|
+
width: sizes.sheetGrabberW,
|
|
26
|
+
height: sizes.sheetGrabberH,
|
|
27
|
+
borderRadius: radii.full,
|
|
28
|
+
backgroundColor: colors.border,
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// --- Header ---
|
|
32
|
+
header: {
|
|
33
|
+
// Header, Description and Footer keep their size; only the Body yields.
|
|
34
|
+
flexShrink: 0,
|
|
35
|
+
display: 'flex',
|
|
36
|
+
alignItems: 'flex-start',
|
|
37
|
+
justifyContent: 'space-between',
|
|
38
|
+
paddingTop: spacing.lg,
|
|
39
|
+
paddingLeft: spacing.lg,
|
|
40
|
+
paddingRight: spacing.lg,
|
|
41
|
+
paddingBottom: spacing.md,
|
|
42
|
+
gap: spacing.md,
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// Under a snap sheet's grabber the header needs no top padding of its own.
|
|
46
|
+
headerUnderGrabber: {
|
|
47
|
+
paddingTop: 0,
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
// Title centred, the other children at the end (Header titleAlign="center").
|
|
51
|
+
headerCentered: {
|
|
52
|
+
alignItems: 'center',
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
headerSide: {
|
|
56
|
+
display: 'flex',
|
|
57
|
+
flexGrow: 1,
|
|
58
|
+
flexBasis: 0,
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
headerEnd: {
|
|
62
|
+
justifyContent: 'flex-end',
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
// --- Title ---
|
|
66
|
+
title: {
|
|
67
|
+
fontSize: typography.fontSizeLg,
|
|
68
|
+
fontWeight: typography.fontWeightSemibold,
|
|
69
|
+
lineHeight: typography.lineHeight,
|
|
70
|
+
color: colors.text,
|
|
71
|
+
margin: 0,
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
// --- Description ---
|
|
75
|
+
description: {
|
|
76
|
+
fontSize: typography.fontSizeSm,
|
|
77
|
+
lineHeight: typography.lineHeight,
|
|
78
|
+
color: colors.textMuted,
|
|
79
|
+
margin: 0,
|
|
80
|
+
paddingLeft: spacing.lg,
|
|
81
|
+
paddingRight: spacing.lg,
|
|
82
|
+
paddingBottom: spacing.md,
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
// --- Footer ---
|
|
86
|
+
footer: {
|
|
87
|
+
display: 'flex',
|
|
88
|
+
alignItems: 'center',
|
|
89
|
+
justifyContent: 'flex-end',
|
|
90
|
+
gap: spacing.sm,
|
|
91
|
+
paddingTop: spacing.md,
|
|
92
|
+
paddingLeft: spacing.lg,
|
|
93
|
+
paddingRight: spacing.lg,
|
|
94
|
+
paddingBottom: spacing.lg,
|
|
95
|
+
borderTopWidth: borders.hairline,
|
|
96
|
+
borderTopStyle: 'solid',
|
|
97
|
+
borderTopColor: colors.border,
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// --- Close button ---
|
|
101
|
+
closeButton: {
|
|
102
|
+
display: 'flex',
|
|
103
|
+
alignItems: 'center',
|
|
104
|
+
justifyContent: 'center',
|
|
105
|
+
width: sizes.iconButton,
|
|
106
|
+
height: sizes.iconButton,
|
|
107
|
+
borderRadius: radii.sm,
|
|
108
|
+
borderWidth: 0,
|
|
109
|
+
backgroundColor: {
|
|
110
|
+
default: 'transparent',
|
|
111
|
+
':hover': colors.bgCardHover,
|
|
112
|
+
},
|
|
113
|
+
color: colors.textMuted,
|
|
114
|
+
cursor: 'pointer',
|
|
115
|
+
padding: 0,
|
|
116
|
+
flexShrink: 0,
|
|
117
|
+
transitionProperty: 'background-color',
|
|
118
|
+
transitionDuration: duration.fast,
|
|
119
|
+
},
|
|
120
|
+
|
|
121
|
+
// --- Inline wrapper ---
|
|
122
|
+
inlineWrapper: {
|
|
123
|
+
display: 'inline-flex',
|
|
124
|
+
},
|
|
125
|
+
})
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
createContext,
|
|
4
|
+
isValidElement,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
type RefObject,
|
|
7
|
+
useContext,
|
|
8
|
+
} from 'react'
|
|
9
|
+
import {html} from 'react-strict-dom'
|
|
10
|
+
import type {DurationToken} from '@duro-app/tokens/keys'
|
|
11
|
+
import {partStyles} from './parts.css'
|
|
12
|
+
import type {SnapPoint} from './sheetPhysics'
|
|
13
|
+
|
|
14
|
+
export type DrawerAnchor = 'right' | 'left' | 'bottom'
|
|
15
|
+
export type DrawerSize = 'sm' | 'md' | 'lg'
|
|
16
|
+
export type {SnapPoint}
|
|
17
|
+
|
|
18
|
+
export interface DrawerContextValue {
|
|
19
|
+
/** Rendered: open, or playing the exit animation. */
|
|
20
|
+
open: boolean
|
|
21
|
+
closing: boolean
|
|
22
|
+
closeDuration: DurationToken
|
|
23
|
+
anchor: DrawerAnchor
|
|
24
|
+
dismissable: boolean
|
|
25
|
+
swipeDismiss: boolean
|
|
26
|
+
requestOpen: () => void
|
|
27
|
+
requestClose: () => void
|
|
28
|
+
/** Close immediately without the exit animation (a swipe has played its own) */
|
|
29
|
+
requestCloseImmediate: () => void
|
|
30
|
+
titleId: string
|
|
31
|
+
descriptionId: string
|
|
32
|
+
/** The panel's host: a DOM element on the web, a view on native */
|
|
33
|
+
panelRef: RefObject<unknown>
|
|
34
|
+
/** A bottom sheet's detents, ascending; empty when the drawer has none */
|
|
35
|
+
snapPoints: readonly SnapPoint[]
|
|
36
|
+
/** Index into snapPoints the sheet rests at */
|
|
37
|
+
snapIndex: number
|
|
38
|
+
setSnapIndex: (index: number) => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const DrawerContext = createContext<DrawerContextValue | null>(null)
|
|
42
|
+
|
|
43
|
+
export function useDrawer() {
|
|
44
|
+
const ctx = useContext(DrawerContext)
|
|
45
|
+
if (!ctx) throw new Error('Drawer compound components must be used within Drawer.Root')
|
|
46
|
+
return ctx
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface GrabberLabels {
|
|
50
|
+
/** The grabber's name when a press grows the sheet */
|
|
51
|
+
expand: string
|
|
52
|
+
/** The grabber's name when a press shrinks it */
|
|
53
|
+
collapse: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const DEFAULT_GRABBER_LABELS: GrabberLabels = {
|
|
57
|
+
expand: 'Expand sheet',
|
|
58
|
+
collapse: 'Shrink sheet',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// The parts below are pure react-strict-dom, drawn the same on both platforms.
|
|
62
|
+
|
|
63
|
+
interface HeaderProps {
|
|
64
|
+
children: ReactNode
|
|
65
|
+
/**
|
|
66
|
+
* 'center' puts the Title in the middle and the other children at the end,
|
|
67
|
+
* as a phone sheet does (a text close button on the right). Default: 'start'
|
|
68
|
+
*/
|
|
69
|
+
titleAlign?: 'start' | 'center'
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function Header({children, titleAlign = 'start'}: HeaderProps) {
|
|
73
|
+
const underGrabber = useDrawer().snapPoints.length > 0 && partStyles.headerUnderGrabber
|
|
74
|
+
if (titleAlign === 'start') {
|
|
75
|
+
return <html.div style={[partStyles.header, underGrabber]}>{children}</html.div>
|
|
76
|
+
}
|
|
77
|
+
const items = Children.toArray(children)
|
|
78
|
+
const isTitle = (c: ReactNode) => isValidElement(c) && c.type === Title
|
|
79
|
+
return (
|
|
80
|
+
<html.div style={[partStyles.header, underGrabber, partStyles.headerCentered]}>
|
|
81
|
+
<html.div style={partStyles.headerSide} />
|
|
82
|
+
{items.filter(isTitle)}
|
|
83
|
+
<html.div style={[partStyles.headerSide, partStyles.headerEnd]}>
|
|
84
|
+
{items.filter((c) => !isTitle(c))}
|
|
85
|
+
</html.div>
|
|
86
|
+
</html.div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function Title({children}: {children: ReactNode}) {
|
|
91
|
+
const {titleId} = useDrawer()
|
|
92
|
+
return (
|
|
93
|
+
<html.h2 id={titleId} style={partStyles.title}>
|
|
94
|
+
{children}
|
|
95
|
+
</html.h2>
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function Description({children}: {children: ReactNode}) {
|
|
100
|
+
const {descriptionId} = useDrawer()
|
|
101
|
+
return (
|
|
102
|
+
<html.p id={descriptionId} style={partStyles.description}>
|
|
103
|
+
{children}
|
|
104
|
+
</html.p>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export function Footer({children}: {children: ReactNode}) {
|
|
109
|
+
return <html.div style={partStyles.footer}>{children}</html.div>
|
|
110
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drawer's drag rules, shared by the web (pointer events) and native
|
|
3
|
+
* (Gesture Handler on the UI thread) so both settle a drag the same way.
|
|
4
|
+
* Pure numbers: no DOM, no React Native. The functions the native drag
|
|
5
|
+
* calls on the UI thread are worklets ('worklet'); on the web the directive
|
|
6
|
+
* is an inert string.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {DURATION_MS, SPACING_PX} from '@duro-app/tokens/keys'
|
|
10
|
+
|
|
11
|
+
/** A bottom-sheet detent (plan N0, Decision 5). */
|
|
12
|
+
export type SnapPoint = 'medium' | 'large'
|
|
13
|
+
|
|
14
|
+
/** Fraction of the panel (or of the lowest detent) a drag must cover to dismiss */
|
|
15
|
+
export const DISMISS_THRESHOLD = 0.3
|
|
16
|
+
|
|
17
|
+
/** Release speed (px/ms) that dismisses, or moves one detent, whatever the distance */
|
|
18
|
+
export const VELOCITY_THRESHOLD = 0.5
|
|
19
|
+
|
|
20
|
+
/** Duration of the slide-out once a drag dismisses (ms) */
|
|
21
|
+
export const DISMISS_DURATION = DURATION_MS.brisk
|
|
22
|
+
|
|
23
|
+
/** Duration of the settle onto a detent, or the snap back (ms) */
|
|
24
|
+
export const SNAP_DURATION = DURATION_MS.base
|
|
25
|
+
|
|
26
|
+
/** Movement (px) before a press becomes a drag; under it a tap stays a tap */
|
|
27
|
+
export const DRAG_SLOP = 8
|
|
28
|
+
|
|
29
|
+
/** Movement (px) across the drag's axis that hands the gesture to a scroll instead */
|
|
30
|
+
export const CROSS_SLOP = 16
|
|
31
|
+
|
|
32
|
+
/** 'medium' is half the window */
|
|
33
|
+
export const MEDIUM_FRACTION = 0.5
|
|
34
|
+
|
|
35
|
+
/** How far (px) a snap sheet may be pulled past its largest detent: it resists, never further */
|
|
36
|
+
export const SNAP_MAX_OVERSHOOT = SPACING_PX.lg
|
|
37
|
+
|
|
38
|
+
/** Height of one detent: 'medium' half the window, 'large' the window less its top inset. */
|
|
39
|
+
export function detentHeight(point: SnapPoint, windowHeight: number, topInset: number): number {
|
|
40
|
+
'worklet'
|
|
41
|
+
return point === 'medium' ? windowHeight * MEDIUM_FRACTION : windowHeight - topInset
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The detents in ascending height, each once. */
|
|
45
|
+
export function sortSnapPoints(points: readonly SnapPoint[]): SnapPoint[] {
|
|
46
|
+
return (['medium', 'large'] as const).filter((p) => points.includes(p))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Where a released drag settles: a detent index into `heights` (ascending),
|
|
51
|
+
* or 'dismiss'.
|
|
52
|
+
*
|
|
53
|
+
* - A fling (|velocity| over VELOCITY_THRESHOLD) moves one detent in its
|
|
54
|
+
* direction from the detent it started at; down from the lowest dismisses.
|
|
55
|
+
* - Otherwise the nearest detent wins, unless the sheet is below the lowest
|
|
56
|
+
* by more than DISMISS_THRESHOLD of it, which dismisses.
|
|
57
|
+
*
|
|
58
|
+
* `velocity` is px/ms, positive downward (closing).
|
|
59
|
+
*/
|
|
60
|
+
export function settleDetent({
|
|
61
|
+
heights,
|
|
62
|
+
from,
|
|
63
|
+
height,
|
|
64
|
+
velocity,
|
|
65
|
+
}: {
|
|
66
|
+
heights: readonly number[]
|
|
67
|
+
from: number
|
|
68
|
+
height: number
|
|
69
|
+
velocity: number
|
|
70
|
+
}): number | 'dismiss' {
|
|
71
|
+
'worklet'
|
|
72
|
+
const last = heights.length - 1
|
|
73
|
+
if (velocity > VELOCITY_THRESHOLD) return from === 0 ? 'dismiss' : from - 1
|
|
74
|
+
if (velocity < -VELOCITY_THRESHOLD) return Math.min(from + 1, last)
|
|
75
|
+
if (height < heights[0] * (1 - DISMISS_THRESHOLD)) return 'dismiss'
|
|
76
|
+
let nearest = 0
|
|
77
|
+
for (let i = 1; i <= last; i++) {
|
|
78
|
+
if (Math.abs(heights[i] - height) < Math.abs(heights[nearest] - height)) nearest = i
|
|
79
|
+
}
|
|
80
|
+
return nearest
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The grabber's tap: the next detent up, wrapping from the largest to the smallest. */
|
|
84
|
+
export function cycleDetent(index: number, count: number): number {
|
|
85
|
+
'worklet'
|
|
86
|
+
return count === 0 ? 0 : (index + 1) % count
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Past the largest detent the sheet resists: the overshoot grows ever more
|
|
91
|
+
* slowly and never exceeds `max`.
|
|
92
|
+
*/
|
|
93
|
+
export function rubberBand(overshoot: number, max: number): number {
|
|
94
|
+
'worklet'
|
|
95
|
+
if (overshoot <= 0) return 0
|
|
96
|
+
return (overshoot / (overshoot + max * 4)) * max
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Whether a side drawer's release dismisses it. `movement` is toward its edge. */
|
|
100
|
+
export function shouldDismissEdge(movement: number, size: number, velocity: number): boolean {
|
|
101
|
+
'worklet'
|
|
102
|
+
return movement / size > DISMISS_THRESHOLD || velocity > VELOCITY_THRESHOLD
|
|
103
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {css} from 'react-strict-dom'
|
|
2
2
|
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
3
|
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
-
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
4
|
import {shadows} from '@duro-app/tokens/tokens/shadows.css'
|
|
6
5
|
import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
|
|
7
6
|
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
@@ -97,6 +96,21 @@ export const styles = css.create({
|
|
|
97
96
|
borderTopRightRadius: radii.lg,
|
|
98
97
|
maxHeight: '85vh',
|
|
99
98
|
},
|
|
99
|
+
// A snap sheet: as tall as its detent, which replaces the 85vh cap.
|
|
100
|
+
panelSnap: {
|
|
101
|
+
maxHeight: 'none',
|
|
102
|
+
transitionProperty: 'height',
|
|
103
|
+
transitionDuration: duration.base,
|
|
104
|
+
transitionTimingFunction: easing.easeOut,
|
|
105
|
+
},
|
|
106
|
+
sheetHeight: (height: number) => ({height}),
|
|
107
|
+
|
|
108
|
+
// The grabber's web-only additions: a grab cursor, and the browser leaves the drag to us.
|
|
109
|
+
grabberHitWeb: {
|
|
110
|
+
cursor: 'grab',
|
|
111
|
+
touchAction: 'none',
|
|
112
|
+
},
|
|
113
|
+
|
|
100
114
|
panelLeftBorder: {
|
|
101
115
|
borderLeftWidth: 0,
|
|
102
116
|
borderRightWidth: borders.hairline,
|
|
@@ -180,38 +194,6 @@ export const styles = css.create({
|
|
|
180
194
|
closeDurationBase: {animationDuration: duration.base},
|
|
181
195
|
closeDurationSlow: {animationDuration: duration.slow},
|
|
182
196
|
|
|
183
|
-
// --- Header ---
|
|
184
|
-
header: {
|
|
185
|
-
display: 'flex',
|
|
186
|
-
alignItems: 'flex-start',
|
|
187
|
-
justifyContent: 'space-between',
|
|
188
|
-
paddingTop: spacing.lg,
|
|
189
|
-
paddingLeft: spacing.lg,
|
|
190
|
-
paddingRight: spacing.lg,
|
|
191
|
-
paddingBottom: spacing.md,
|
|
192
|
-
gap: spacing.md,
|
|
193
|
-
},
|
|
194
|
-
|
|
195
|
-
// --- Title ---
|
|
196
|
-
title: {
|
|
197
|
-
fontSize: typography.fontSizeLg,
|
|
198
|
-
fontWeight: typography.fontWeightSemibold,
|
|
199
|
-
lineHeight: typography.lineHeight,
|
|
200
|
-
color: colors.text,
|
|
201
|
-
margin: 0,
|
|
202
|
-
},
|
|
203
|
-
|
|
204
|
-
// --- Description ---
|
|
205
|
-
description: {
|
|
206
|
-
fontSize: typography.fontSizeSm,
|
|
207
|
-
lineHeight: typography.lineHeight,
|
|
208
|
-
color: colors.textMuted,
|
|
209
|
-
margin: 0,
|
|
210
|
-
paddingLeft: spacing.lg,
|
|
211
|
-
paddingRight: spacing.lg,
|
|
212
|
-
paddingBottom: spacing.md,
|
|
213
|
-
},
|
|
214
|
-
|
|
215
197
|
// --- Body ---
|
|
216
198
|
body: {
|
|
217
199
|
paddingLeft: spacing.lg,
|
|
@@ -220,45 +202,8 @@ export const styles = css.create({
|
|
|
220
202
|
overflowY: 'auto',
|
|
221
203
|
flex: 1,
|
|
222
204
|
},
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
display: 'flex',
|
|
227
|
-
alignItems: 'center',
|
|
228
|
-
justifyContent: 'flex-end',
|
|
229
|
-
gap: spacing.sm,
|
|
230
|
-
paddingTop: spacing.md,
|
|
231
|
-
paddingLeft: spacing.lg,
|
|
232
|
-
paddingRight: spacing.lg,
|
|
233
|
-
paddingBottom: spacing.lg,
|
|
234
|
-
borderTopWidth: borders.hairline,
|
|
235
|
-
borderTopStyle: 'solid',
|
|
236
|
-
borderTopColor: colors.border,
|
|
237
|
-
},
|
|
238
|
-
|
|
239
|
-
// --- Close button ---
|
|
240
|
-
closeButton: {
|
|
241
|
-
display: 'flex',
|
|
242
|
-
alignItems: 'center',
|
|
243
|
-
justifyContent: 'center',
|
|
244
|
-
width: sizes.iconButton,
|
|
245
|
-
height: sizes.iconButton,
|
|
246
|
-
borderRadius: radii.sm,
|
|
247
|
-
borderWidth: 0,
|
|
248
|
-
backgroundColor: {
|
|
249
|
-
default: 'transparent',
|
|
250
|
-
':hover': colors.bgCardHover,
|
|
251
|
-
},
|
|
252
|
-
color: colors.textMuted,
|
|
253
|
-
cursor: 'pointer',
|
|
254
|
-
padding: 0,
|
|
255
|
-
flexShrink: 0,
|
|
256
|
-
transitionProperty: 'background-color',
|
|
257
|
-
transitionDuration: duration.fast,
|
|
258
|
-
},
|
|
259
|
-
|
|
260
|
-
// --- Inline wrapper ---
|
|
261
|
-
inlineWrapper: {
|
|
262
|
-
display: 'inline-flex',
|
|
205
|
+
bodyFlush: {
|
|
206
|
+
paddingLeft: 0,
|
|
207
|
+
paddingRight: 0,
|
|
263
208
|
},
|
|
264
209
|
})
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real touch input and viewport control for Storybook interaction tests
|
|
3
|
+
* (the Drawer's, SplitPane's), through
|
|
4
|
+
* Chrome DevTools Protocol: synthetic pointer events neither scroll nor go
|
|
5
|
+
* through touch adjustment the way a finger does.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export type Point = {x: number; y: number}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A CDP command. `vitest/browser` loads only under the test runner, so it is
|
|
12
|
+
* imported here, at call time: a static import breaks the built Storybook.
|
|
13
|
+
* `CDPSession` is empty until the provider's typings load; `send` exists at runtime.
|
|
14
|
+
*/
|
|
15
|
+
export async function cdpSend(method: string, params: object) {
|
|
16
|
+
const {cdp} = await import('vitest/browser')
|
|
17
|
+
const session = cdp() as unknown as {send: (m: string, p: object) => Promise<unknown>}
|
|
18
|
+
return session.send(method, params)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Set the page's viewport (test runner only, like `cdpSend`). */
|
|
22
|
+
export async function setViewport(width: number, height: number) {
|
|
23
|
+
const {page} = await import('vitest/browser')
|
|
24
|
+
await page.viewport(width, height)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const pause = (ms: number) => new Promise((r) => setTimeout(r, ms))
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* CDP input lands in top-level page coordinates; the story runs in an iframe
|
|
31
|
+
* the test runner offsets and may scale to fit. Map a point up every frame.
|
|
32
|
+
*/
|
|
33
|
+
export function toPage(p: Point): Point {
|
|
34
|
+
let {x, y} = p
|
|
35
|
+
let win: Window = window
|
|
36
|
+
while (win.frameElement && win.parent !== win) {
|
|
37
|
+
const rect = win.frameElement.getBoundingClientRect()
|
|
38
|
+
const scale = rect.width / win.innerWidth
|
|
39
|
+
x = rect.left + x * scale
|
|
40
|
+
y = rect.top + y * scale
|
|
41
|
+
win = win.parent
|
|
42
|
+
}
|
|
43
|
+
return {x, y}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* A page-coordinate touch point with a 1px radius: Chrome's touch adjustment
|
|
48
|
+
* snaps a fat default touch onto the nearest control, which would turn every
|
|
49
|
+
* drag in the list into a drag on a toggle.
|
|
50
|
+
*/
|
|
51
|
+
const touchAt = (p: Point) => ({...toPage(p), radiusX: 1, radiusY: 1})
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* A finger drag from one point to another, in `steps` moves 16 ms apart.
|
|
55
|
+
* Each move is a round trip to the browser; few of them keep a test well
|
|
56
|
+
* inside its timeout on a loaded machine.
|
|
57
|
+
*/
|
|
58
|
+
export async function touchDrag(from: Point, to: Point, steps = 6) {
|
|
59
|
+
const send = (type: string, touchPoints: Point[]) =>
|
|
60
|
+
cdpSend('Input.dispatchTouchEvent', {type, touchPoints: touchPoints.map(touchAt)})
|
|
61
|
+
await send('touchStart', [from])
|
|
62
|
+
for (let i = 1; i <= steps; i++) {
|
|
63
|
+
await send('touchMove', [
|
|
64
|
+
{x: from.x + ((to.x - from.x) * i) / steps, y: from.y + ((to.y - from.y) * i) / steps},
|
|
65
|
+
])
|
|
66
|
+
await pause(16)
|
|
67
|
+
}
|
|
68
|
+
await send('touchEnd', [])
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** A finger tap. */
|
|
72
|
+
export async function touchTap(at: Point) {
|
|
73
|
+
await cdpSend('Input.dispatchTouchEvent', {type: 'touchStart', touchPoints: [touchAt(at)]})
|
|
74
|
+
await cdpSend('Input.dispatchTouchEvent', {type: 'touchEnd', touchPoints: []})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** The centre of an element, in its own document's coordinates. */
|
|
78
|
+
export function centre(el: Element): Point {
|
|
79
|
+
const r = el.getBoundingClientRect()
|
|
80
|
+
return {x: r.left + r.width / 2, y: r.top + r.height / 2}
|
|
81
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {type ReactNode, useEffect, useId, useMemo, useRef, useState} from 'react'
|
|
2
|
+
import type {DurationToken} from '@duro-app/tokens/keys'
|
|
3
|
+
import {useOverlayOpenState} from '../../shared/useOverlayOpenState'
|
|
4
|
+
import type {DrawerAnchor, DrawerContextValue} from './parts'
|
|
5
|
+
import {sortSnapPoints, type SnapPoint} from './sheetPhysics'
|
|
6
|
+
|
|
7
|
+
export interface DrawerRootProps {
|
|
8
|
+
children: ReactNode
|
|
9
|
+
/** Controlled open state */
|
|
10
|
+
open?: boolean
|
|
11
|
+
/** Default open state (uncontrolled) */
|
|
12
|
+
defaultOpen?: boolean
|
|
13
|
+
/** Called when drawer open state changes */
|
|
14
|
+
onOpenChange?: (open: boolean) => void
|
|
15
|
+
/** Which edge the drawer slides from. Default: 'right' */
|
|
16
|
+
anchor?: DrawerAnchor
|
|
17
|
+
/** Whether clicking backdrop or pressing Escape (Android's back button on native) closes the drawer. Default: true */
|
|
18
|
+
dismissable?: boolean
|
|
19
|
+
/** Whether swipe gesture dismisses the drawer. Default: true */
|
|
20
|
+
swipeDismiss?: boolean
|
|
21
|
+
/** Motion token for the close animation. Default: 'fast' (150ms) */
|
|
22
|
+
closeAnimationDuration?: DurationToken
|
|
23
|
+
/**
|
|
24
|
+
* Bottom anchor only: the heights the sheet rests at. 'medium' is half the
|
|
25
|
+
* window, 'large' the window less its top safe-area inset. A drag moves
|
|
26
|
+
* between them and dismisses below the lowest; the grabber steps through
|
|
27
|
+
* them on a tap. Without it, a bottom drawer is as tall as its content.
|
|
28
|
+
*/
|
|
29
|
+
snapPoints?: readonly SnapPoint[]
|
|
30
|
+
/** The detent the sheet opens at. Default: the lowest of snapPoints */
|
|
31
|
+
defaultSnapPoint?: SnapPoint
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The state both platforms' Drawer.Root share: open and closing, ids, and the detent. */
|
|
35
|
+
export function useDrawerRoot({
|
|
36
|
+
open: controlledOpen,
|
|
37
|
+
defaultOpen = false,
|
|
38
|
+
onOpenChange,
|
|
39
|
+
anchor = 'right',
|
|
40
|
+
dismissable = true,
|
|
41
|
+
swipeDismiss = true,
|
|
42
|
+
closeAnimationDuration = 'fast',
|
|
43
|
+
snapPoints: snapPointsProp,
|
|
44
|
+
defaultSnapPoint,
|
|
45
|
+
}: Omit<DrawerRootProps, 'children'>): DrawerContextValue {
|
|
46
|
+
const {
|
|
47
|
+
present,
|
|
48
|
+
closing,
|
|
49
|
+
requestOpen,
|
|
50
|
+
requestClose,
|
|
51
|
+
closeNow: requestCloseImmediate,
|
|
52
|
+
} = useOverlayOpenState({
|
|
53
|
+
open: controlledOpen,
|
|
54
|
+
defaultOpen,
|
|
55
|
+
onOpenChange,
|
|
56
|
+
exitDuration: closeAnimationDuration,
|
|
57
|
+
closeNotice: 'after-exit',
|
|
58
|
+
})
|
|
59
|
+
const panelRef = useRef<unknown>(null)
|
|
60
|
+
const titleId = useId()
|
|
61
|
+
const descriptionId = useId()
|
|
62
|
+
|
|
63
|
+
// Keyed by content: a new array literal each render is the same detents.
|
|
64
|
+
const snapKey = anchor === 'bottom' && snapPointsProp ? sortSnapPoints(snapPointsProp).join() : ''
|
|
65
|
+
const snapPoints = useMemo(() => (snapKey ? (snapKey.split(',') as SnapPoint[]) : []), [snapKey])
|
|
66
|
+
const openIndex = Math.max(0, defaultSnapPoint ? snapPoints.indexOf(defaultSnapPoint) : 0)
|
|
67
|
+
const [snapIndex, setSnapIndex] = useState(openIndex)
|
|
68
|
+
// Each opening starts at the default detent.
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!present) setSnapIndex(openIndex)
|
|
71
|
+
}, [present, openIndex])
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
open: present,
|
|
75
|
+
closing,
|
|
76
|
+
closeDuration: closeAnimationDuration,
|
|
77
|
+
anchor,
|
|
78
|
+
dismissable,
|
|
79
|
+
swipeDismiss,
|
|
80
|
+
requestOpen,
|
|
81
|
+
requestClose,
|
|
82
|
+
requestCloseImmediate,
|
|
83
|
+
titleId,
|
|
84
|
+
descriptionId,
|
|
85
|
+
panelRef,
|
|
86
|
+
snapPoints,
|
|
87
|
+
snapIndex,
|
|
88
|
+
setSnapIndex,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import {useEffect, useMemo, useState} from 'react'
|
|
2
|
+
import {detentHeight, type SnapPoint} from './sheetPhysics'
|
|
3
|
+
|
|
4
|
+
interface Viewport {
|
|
5
|
+
height: number
|
|
6
|
+
topInset: number
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** The top safe-area inset, read through a probe: CSS knows it, JavaScript does not. */
|
|
10
|
+
function readTopInset(): number {
|
|
11
|
+
const probe = document.createElement('div')
|
|
12
|
+
probe.style.cssText = 'position:fixed;visibility:hidden;padding-top:env(safe-area-inset-top,0px)'
|
|
13
|
+
document.body.appendChild(probe)
|
|
14
|
+
const inset = parseFloat(getComputedStyle(probe).paddingTop) || 0
|
|
15
|
+
probe.remove()
|
|
16
|
+
return inset
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function readViewport(): Viewport {
|
|
20
|
+
return {height: window.innerHeight, topInset: readTopInset()}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Web: each detent's height in px, ascending, following the window as it
|
|
25
|
+
* resizes (a rotated phone, an opened on-screen keyboard).
|
|
26
|
+
*/
|
|
27
|
+
export function useSheetHeights(points: readonly SnapPoint[], active: boolean): number[] {
|
|
28
|
+
const [viewport, setViewport] = useState<Viewport | null>(null)
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!active) return
|
|
32
|
+
setViewport(readViewport())
|
|
33
|
+
const onResize = () => setViewport(readViewport())
|
|
34
|
+
window.addEventListener('resize', onResize)
|
|
35
|
+
return () => window.removeEventListener('resize', onResize)
|
|
36
|
+
}, [active])
|
|
37
|
+
|
|
38
|
+
return useMemo(
|
|
39
|
+
() => (viewport ? points.map((p) => detentHeight(p, viewport.height, viewport.topInset)) : []),
|
|
40
|
+
[points, viewport],
|
|
41
|
+
)
|
|
42
|
+
}
|