@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,11 @@
|
|
|
1
|
+
import { ComponentProps } from 'react';
|
|
2
|
+
import { html } from 'react-strict-dom';
|
|
3
|
+
export type ButtonHostProps = ComponentProps<typeof html.button>;
|
|
4
|
+
/**
|
|
5
|
+
* The host element of Button and Toggle. On the web it is the <button>
|
|
6
|
+
* itself, with no component in between (a wrapper changed what a Form's
|
|
7
|
+
* submit focuses); the native variant (ButtonHost.native.tsx) can swap in an
|
|
8
|
+
* RN Pressable to carry a Tooltip's accessibility hint.
|
|
9
|
+
*/
|
|
10
|
+
export declare const ButtonHost: typeof html.button;
|
|
11
|
+
//# sourceMappingURL=ButtonHost.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ButtonHost.d.ts","sourceRoot":"","sources":["../../src/shared/ButtonHost.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,cAAc,EAAC,MAAM,OAAO,CAAA;AACzC,OAAO,EAAC,IAAI,EAAC,MAAM,kBAAkB,CAAA;AAErC,MAAM,MAAM,eAAe,GAAG,cAAc,CAAC,OAAO,IAAI,CAAC,MAAM,CAAC,CAAA;AAEhE;;;;;GAKG;AACH,eAAO,MAAM,UAAU,EAAE,OAAO,IAAI,CAAC,MAAoB,CAAA"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ReactElement, ReactNode, Ref } from 'react';
|
|
2
|
+
interface PressableChildProps {
|
|
3
|
+
onClick?: () => void;
|
|
4
|
+
ref?: Ref<unknown>;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Native overlay triggers and closes: native presses do not bubble to a
|
|
8
|
+
* wrapper as web clicks do, so the one child (a Button) gets the handler
|
|
9
|
+
* itself, after its own. `ref` also receives the child's host, where focus
|
|
10
|
+
* returns when the overlay closes.
|
|
11
|
+
*/
|
|
12
|
+
export declare function withPress(children: ReactNode, onPress: () => void, ref?: Ref<unknown>): ReactElement<PressableChildProps, string | import('react').JSXElementConstructor<any>> | null;
|
|
13
|
+
export {};
|
|
14
|
+
//# sourceMappingURL=pressableChild.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pressableChild.d.ts","sourceRoot":"","sources":["../../src/shared/pressableChild.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,GAAG,EACT,MAAM,OAAO,CAAA;AAGd,UAAU,mBAAmB;IAC3B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAA;IACpB,GAAG,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAA;CACnB;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CAAC,QAAQ,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,IAAI,EAAE,GAAG,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,iGAWrF"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ReactNode } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The text a React node would render, read from its props: a native picker's
|
|
4
|
+
* option label, where there is no DOM `textContent` to read it from.
|
|
5
|
+
*/
|
|
6
|
+
export declare function textOf(node: ReactNode): string;
|
|
7
|
+
//# sourceMappingURL=textOf.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"textOf.d.ts","sourceRoot":"","sources":["../../src/shared/textOf.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiB,KAAK,SAAS,EAAC,MAAM,OAAO,CAAA;AAEpD;;;GAGG;AACH,wBAAgB,MAAM,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAM9C"}
|
package/dist/table.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { M as F,
|
|
1
|
+
import { M as F, q as gt, R as it, H as kt, t as pt, v as bt, w as ft, x as mt, b as ht, g as yt, r as St, y as Tt } from "./Table-DnaoH-h5.js";
|
|
2
2
|
import { jsx as u, jsxs as q } from "react/jsx-runtime";
|
|
3
3
|
import { c as H, m as p, d as b } from "./compiler-runtime-BuugS4D0.js";
|
|
4
4
|
import { flexRender as E, useReactTable as rt, getPaginationRowModel as Ct, getFilteredRowModel as vt, getSortedRowModel as at, getCoreRowModel as st } from "@tanstack/react-table";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.14.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"types": "./dist/table.d.ts"
|
|
40
40
|
},
|
|
41
41
|
"./form": {
|
|
42
|
+
"react-native": "./src/form.native.ts",
|
|
42
43
|
"source": "./src/form.ts",
|
|
43
44
|
"import": "./dist/form.js",
|
|
44
45
|
"types": "./dist/form.d.ts"
|
|
@@ -90,7 +91,7 @@
|
|
|
90
91
|
},
|
|
91
92
|
"dependencies": {
|
|
92
93
|
"@tanstack/react-virtual": "^3.14.6",
|
|
93
|
-
"@duro-app/tokens": "^5.
|
|
94
|
+
"@duro-app/tokens": "^5.14.0"
|
|
94
95
|
},
|
|
95
96
|
"devDependencies": {
|
|
96
97
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
"An application's frame: a navigation rail (a labelled nav, the brand at its top, an optional footer at its foot) beside the main content (main), with an optional Header. Below collapseBelow (sm default, md or lg), measured on the shell's own width, the rail is hidden and one sticky bar (a banner, appBarH tall) holds a Menu button that opens the same navigation in a left Drawer, the brand, and the Header's content when there is one — a shell without a Header still has its navigation on a phone. Above it the Header is a row that scrolls with the page and the rail stays sticky. CSS makes the switch: the server renders one HTML for every width with the brand in both places, and display: none hides the inactive copy, so nothing moves after hydration. A skip link (the first tab stop) focuses Main; an Aside inside Main sticks below the bar. Labels come from props. Compound component — Root is required.",
|
|
5
|
+
"An application's frame: a navigation rail (a labelled nav, the brand at its top, an optional footer at its foot) beside the main content (main), with an optional Header. Below collapseBelow (sm default, md or lg), measured on the shell's own width, the rail is hidden and one sticky bar (a banner, appBarH tall) holds a Menu button that opens the same navigation in a left Drawer, the brand, and the Header's content when there is one — a shell without a Header still has its navigation on a phone. Above it the Header is a row that scrolls with the page and the rail stays sticky. CSS makes the switch: the server renders one HTML for every width with the brand in both places, and display: none hides the inactive copy, so nothing moves after hydration. A skip link (the first tab stop) focuses Main; an Aside inside Main sticks below the bar. Labels come from props. Compound component — Root is required. On React Native it is always the narrow shape: the bar below the status bar, Main under it, and the Rail in a left Drawer; there is no skip link.",
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'The frame of an app with a persistent navigation rail: an admin, a docs site, a dashboard',
|
|
8
8
|
'A layout whose navigation must collapse into a drawer on a phone',
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
createContext,
|
|
4
|
+
isValidElement,
|
|
5
|
+
type ReactNode,
|
|
6
|
+
type Ref,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useMemo,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react'
|
|
12
|
+
import {html} from 'react-strict-dom'
|
|
13
|
+
import {Button} from '../Button/Button'
|
|
14
|
+
import {Drawer} from '../Drawer/Drawer'
|
|
15
|
+
import {Icon} from '../Icon'
|
|
16
|
+
import {useSafeAreaInsets} from '../../shared/safeArea'
|
|
17
|
+
import {nativeStyles} from './native.css'
|
|
18
|
+
|
|
19
|
+
export type AppShellCollapse = 'sm' | 'md' | 'lg'
|
|
20
|
+
|
|
21
|
+
interface AppShellContextValue {
|
|
22
|
+
open: boolean
|
|
23
|
+
setOpen: (open: boolean) => void
|
|
24
|
+
closeLabel: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const AppShellContext = createContext<AppShellContextValue | null>(null)
|
|
28
|
+
|
|
29
|
+
// True only where Root placed a Header itself, as on the web.
|
|
30
|
+
const HeaderSlotContext = createContext(false)
|
|
31
|
+
|
|
32
|
+
function useAppShell(): AppShellContextValue {
|
|
33
|
+
const ctx = useContext(AppShellContext)
|
|
34
|
+
if (!ctx) throw new Error('AppShell compound components must be used within AppShell.Root')
|
|
35
|
+
return ctx
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface RootProps {
|
|
39
|
+
/** The button that opens the navigation ("Menu"), from your text catalog. */
|
|
40
|
+
menuLabel: string
|
|
41
|
+
/** The accessible name of the drawer's close button ("Close navigation"). */
|
|
42
|
+
closeLabel: string
|
|
43
|
+
/** Web only: native has no skip link (no keyboard tab order to skip). */
|
|
44
|
+
skipLabel: string
|
|
45
|
+
/** The app's name or logo, shown in the bar beside the Menu button. */
|
|
46
|
+
brand?: ReactNode
|
|
47
|
+
/** Web only: a phone is below every breakpoint, so native always shows the bar and the drawer. */
|
|
48
|
+
collapseBelow?: AppShellCollapse
|
|
49
|
+
/** `AppShell.Rail`, `AppShell.Main` and an optional `AppShell.Header`, as direct children. */
|
|
50
|
+
children: ReactNode
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Native AppShell (plan N0, Slice B): the narrow shape the web shows below
|
|
55
|
+
* `collapseBelow`, always — the bar on top (Menu button, brand, the Header's
|
|
56
|
+
* content) below the top safe-area inset, Main under it, and the Rail's
|
|
57
|
+
* navigation in a left Drawer. The same parts and props as the web.
|
|
58
|
+
*/
|
|
59
|
+
function Root({menuLabel, closeLabel, brand, children}: RootProps) {
|
|
60
|
+
const [open, setOpen] = useState(false)
|
|
61
|
+
const insets = useSafeAreaInsets()
|
|
62
|
+
|
|
63
|
+
let header: ReactNode = null
|
|
64
|
+
const rest: ReactNode[] = []
|
|
65
|
+
Children.forEach(children, (child) => {
|
|
66
|
+
if (isValidElement<HeaderProps>(child) && child.type === Header) {
|
|
67
|
+
header = child
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
rest.push(child)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const value = useMemo(() => ({open, setOpen, closeLabel}), [open, closeLabel])
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<AppShellContext.Provider value={value}>
|
|
77
|
+
<html.div style={nativeStyles.shell}>
|
|
78
|
+
<html.header style={[nativeStyles.bar, nativeStyles.barTop(insets.top)]}>
|
|
79
|
+
<Button
|
|
80
|
+
variant="secondary"
|
|
81
|
+
size="small"
|
|
82
|
+
aria-expanded={open}
|
|
83
|
+
onClick={() => setOpen(true)}
|
|
84
|
+
>
|
|
85
|
+
<Icon name="menu" size="sm" />
|
|
86
|
+
{menuLabel}
|
|
87
|
+
</Button>
|
|
88
|
+
{brand}
|
|
89
|
+
<HeaderSlotContext.Provider value={true}>{header}</HeaderSlotContext.Provider>
|
|
90
|
+
</html.header>
|
|
91
|
+
{rest}
|
|
92
|
+
</html.div>
|
|
93
|
+
</AppShellContext.Provider>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface AppShellRailState {
|
|
98
|
+
/** Closes the drawer. Call it after any selection in the navigation. */
|
|
99
|
+
close: () => void
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
type RailContent = ReactNode | ((state: AppShellRailState) => ReactNode)
|
|
103
|
+
|
|
104
|
+
interface RailProps {
|
|
105
|
+
/** Names the navigation and the drawer ("Navigation"); required. */
|
|
106
|
+
'aria-label': string
|
|
107
|
+
/** The navigation (a SideNav, a Tree), shown in the drawer; a function receives `close`. */
|
|
108
|
+
children: RailContent
|
|
109
|
+
/** What sits at the end of the drawer; a function receives `close`. */
|
|
110
|
+
footer?: RailContent
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function Rail({'aria-label': ariaLabel, children, footer}: RailProps) {
|
|
114
|
+
const {open, setOpen, closeLabel} = useAppShell()
|
|
115
|
+
const close = useCallback(() => setOpen(false), [setOpen])
|
|
116
|
+
const content = typeof children === 'function' ? children({close}) : children
|
|
117
|
+
const foot = typeof footer === 'function' ? footer({close}) : footer
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Drawer.Root anchor="left" open={open} onOpenChange={setOpen}>
|
|
121
|
+
<Drawer.Portal size="sm">
|
|
122
|
+
<Drawer.Header>
|
|
123
|
+
<Drawer.Title>{ariaLabel}</Drawer.Title>
|
|
124
|
+
<Drawer.Close aria-label={closeLabel} />
|
|
125
|
+
</Drawer.Header>
|
|
126
|
+
<Drawer.Body>
|
|
127
|
+
<html.nav aria-label={ariaLabel}>
|
|
128
|
+
{content}
|
|
129
|
+
{foot != null && <html.div style={nativeStyles.drawerFooter}>{foot}</html.div>}
|
|
130
|
+
</html.nav>
|
|
131
|
+
</Drawer.Body>
|
|
132
|
+
</Drawer.Portal>
|
|
133
|
+
</Drawer.Root>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface HeaderProps {
|
|
138
|
+
/** The header's content: a search field, the account menu. It sits in the bar. */
|
|
139
|
+
children?: ReactNode
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function Header({children}: HeaderProps) {
|
|
143
|
+
useAppShell()
|
|
144
|
+
if (!useContext(HeaderSlotContext)) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
'AppShell.Header must be a direct child of AppShell.Root (not inside a fragment or another component)',
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
return <html.div style={nativeStyles.header}>{children}</html.div>
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface MainProps {
|
|
153
|
+
children: ReactNode
|
|
154
|
+
/** Web only: the skip link's target. */
|
|
155
|
+
id?: string
|
|
156
|
+
/** The main region's host, e.g. to hand `useContainerBelow`. */
|
|
157
|
+
ref?: Ref<HTMLElement>
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function Main({children, ref}: MainProps) {
|
|
161
|
+
useAppShell()
|
|
162
|
+
return (
|
|
163
|
+
<html.main ref={ref} style={nativeStyles.main}>
|
|
164
|
+
{children}
|
|
165
|
+
</html.main>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const AppShell = {
|
|
170
|
+
Root,
|
|
171
|
+
Rail,
|
|
172
|
+
Header,
|
|
173
|
+
Main,
|
|
174
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {sizes} from '@duro-app/tokens/tokens/sizes.css'
|
|
5
|
+
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The native AppShell: a phone is below every `collapseBelow`, so it is
|
|
9
|
+
* always the narrow shape — the bar on top, Main below, the rail in a Drawer.
|
|
10
|
+
*/
|
|
11
|
+
export const nativeStyles = css.create({
|
|
12
|
+
shell: {
|
|
13
|
+
flexGrow: 1,
|
|
14
|
+
display: 'flex',
|
|
15
|
+
flexDirection: 'column',
|
|
16
|
+
color: colors.text,
|
|
17
|
+
backgroundColor: colors.bg,
|
|
18
|
+
},
|
|
19
|
+
bar: {
|
|
20
|
+
display: 'flex',
|
|
21
|
+
flexDirection: 'row',
|
|
22
|
+
alignItems: 'center',
|
|
23
|
+
gap: spacing.sm,
|
|
24
|
+
minHeight: sizes.appBarH,
|
|
25
|
+
paddingLeft: spacing.md,
|
|
26
|
+
paddingRight: spacing.md,
|
|
27
|
+
backgroundColor: colors.bgCard,
|
|
28
|
+
borderBottomWidth: borders.hairline,
|
|
29
|
+
borderBottomStyle: 'solid',
|
|
30
|
+
borderBottomColor: colors.border,
|
|
31
|
+
},
|
|
32
|
+
// The bar sits below the status bar: the top safe-area inset is its padding.
|
|
33
|
+
barTop: (top: number) => ({paddingTop: top}),
|
|
34
|
+
header: {
|
|
35
|
+
display: 'flex',
|
|
36
|
+
flexDirection: 'row',
|
|
37
|
+
alignItems: 'center',
|
|
38
|
+
gap: spacing.sm,
|
|
39
|
+
flexGrow: 1,
|
|
40
|
+
flexShrink: 1,
|
|
41
|
+
},
|
|
42
|
+
main: {
|
|
43
|
+
flexGrow: 1,
|
|
44
|
+
flexShrink: 1,
|
|
45
|
+
paddingTop: spacing.lg,
|
|
46
|
+
paddingLeft: spacing.md,
|
|
47
|
+
paddingRight: spacing.md,
|
|
48
|
+
},
|
|
49
|
+
drawerFooter: {
|
|
50
|
+
marginTop: spacing.md,
|
|
51
|
+
paddingTop: spacing.md,
|
|
52
|
+
borderTopWidth: borders.hairline,
|
|
53
|
+
borderTopStyle: 'solid',
|
|
54
|
+
borderTopColor: colors.border,
|
|
55
|
+
},
|
|
56
|
+
})
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type {ReactNode, Ref} from 'react'
|
|
2
|
-
import {html} from 'react-strict-dom'
|
|
3
2
|
import {isNative} from '../../platform'
|
|
4
3
|
import {mergeRefs} from '../../shared/mergeRefs'
|
|
4
|
+
import {ButtonHost} from '../../shared/ButtonHost'
|
|
5
5
|
import type {ControlSize} from '../../shared/types'
|
|
6
6
|
import {useGroupedControl} from '../ButtonGroup/useGroupedControl'
|
|
7
|
+
import {useFormSubmit} from '../Form/FormSubmitContext'
|
|
7
8
|
import {styles} from './styles.css'
|
|
8
9
|
|
|
9
10
|
export type ButtonVariant = 'primary' | 'secondary' | 'inverseSecondary' | 'link' | 'danger'
|
|
@@ -50,14 +51,24 @@ export function Button({
|
|
|
50
51
|
children,
|
|
51
52
|
}: ButtonProps) {
|
|
52
53
|
const grouped = useGroupedControl<HTMLButtonElement>({disabled})
|
|
54
|
+
// Native Form only (no DOM submit event); the web Form provides none.
|
|
55
|
+
const formSubmit = useFormSubmit()
|
|
56
|
+
const submit = type === 'submit' ? formSubmit : null
|
|
53
57
|
return (
|
|
54
|
-
<
|
|
58
|
+
<ButtonHost
|
|
55
59
|
ref={isNative ? ref : mergeRefs(ref, grouped.ref)}
|
|
56
60
|
tabIndex={grouped.tabIndex}
|
|
57
61
|
onFocus={grouped.onFocus}
|
|
58
62
|
type={type}
|
|
59
63
|
disabled={disabled}
|
|
60
|
-
onClick={
|
|
64
|
+
onClick={
|
|
65
|
+
submit
|
|
66
|
+
? () => {
|
|
67
|
+
onClick?.()
|
|
68
|
+
submit()
|
|
69
|
+
}
|
|
70
|
+
: onClick
|
|
71
|
+
}
|
|
61
72
|
aria-label={ariaLabel}
|
|
62
73
|
aria-expanded={ariaExpanded}
|
|
63
74
|
aria-controls={ariaControls}
|
|
@@ -72,6 +83,6 @@ export function Button({
|
|
|
72
83
|
]}
|
|
73
84
|
>
|
|
74
85
|
{children}
|
|
75
|
-
</
|
|
86
|
+
</ButtonHost>
|
|
76
87
|
)
|
|
77
88
|
}
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
'Color-mode controller + toggle. ColorModeProvider owns the preference (system/light/dark/high-contrast), resolves "system" against the OS scheme, persists it to localStorage, and applies the resolved theme via ThemeProvider. ColorModeToggle is an icon button that cycles the preference; useColorMode() reads it.',
|
|
5
|
+
'Color-mode controller + toggle. ColorModeProvider owns the preference (system/light/dark/high-contrast), resolves "system" against the OS scheme, persists it to localStorage, and applies the resolved theme via ThemeProvider. ColorModeToggle is an icon button that cycles the preference; useColorMode() reads it. On React Native "system" follows the OS appearance (useColorScheme) and nothing is stored: persist the preference yourself through preference and onPreferenceChange.',
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'App-level light/dark (and high-contrast) switching that persists across reloads',
|
|
8
8
|
'Respecting the OS prefers-color-scheme with a user override',
|
|
@@ -12,6 +12,8 @@ import {isNative} from '../../platform'
|
|
|
12
12
|
import {Icon, type IconName, type IconSize} from '../Icon/Icon'
|
|
13
13
|
import {ThemeProvider, type ThemeName} from '../ThemeProvider/ThemeProvider'
|
|
14
14
|
import {styles} from './styles.css'
|
|
15
|
+
import {useSystemScheme} from './useSystemScheme'
|
|
16
|
+
import {readStoredRaw, writeStoredRaw} from './storage'
|
|
15
17
|
|
|
16
18
|
/**
|
|
17
19
|
* The user's *preference*. `'system'` follows the OS `prefers-color-scheme`;
|
|
@@ -62,31 +64,13 @@ export function useColorMode(): ColorModeContextValue {
|
|
|
62
64
|
return ctx
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
const canUseDom =
|
|
66
|
-
!isNative && typeof window !== 'undefined' && typeof window.matchMedia === 'function'
|
|
67
|
-
|
|
68
67
|
function isPreference(value: unknown): value is ColorModePreference {
|
|
69
68
|
return typeof value === 'string' && (PREFERENCES as readonly string[]).includes(value)
|
|
70
69
|
}
|
|
71
70
|
|
|
72
71
|
function readStored(key: string | null): ColorModePreference | null {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const raw = window.localStorage.getItem(key)
|
|
76
|
-
return isPreference(raw) ? raw : null
|
|
77
|
-
} catch {
|
|
78
|
-
// Private mode / blocked storage — degrade to no persistence.
|
|
79
|
-
return null
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function writeStored(key: string | null, value: ColorModePreference): void {
|
|
84
|
-
if (!key || isNative || typeof window === 'undefined') return
|
|
85
|
-
try {
|
|
86
|
-
window.localStorage.setItem(key, value)
|
|
87
|
-
} catch {
|
|
88
|
-
/* ignore */
|
|
89
|
-
}
|
|
72
|
+
const raw = readStoredRaw(key)
|
|
73
|
+
return isPreference(raw) ? raw : null
|
|
90
74
|
}
|
|
91
75
|
|
|
92
76
|
interface ColorModeProviderProps {
|
|
@@ -100,7 +84,8 @@ interface ColorModeProviderProps {
|
|
|
100
84
|
onPreferenceChange?: (preference: ColorModePreference) => void
|
|
101
85
|
/** Cycle order for `cycle()` / `<ColorModeToggle>`. Default `['light', 'dark']`. */
|
|
102
86
|
modes?: readonly ColorModePreference[]
|
|
103
|
-
/** localStorage key for persistence; `null` disables it. Default `'duro-color-mode'`.
|
|
87
|
+
/** localStorage key for persistence; `null` disables it. Default `'duro-color-mode'`.
|
|
88
|
+
* Native has no localStorage: persist through `preference` and `onPreferenceChange`. */
|
|
104
89
|
storageKey?: string | null
|
|
105
90
|
/** Wrap children in the DS `ThemeProvider` with the resolved theme. Default `true`.
|
|
106
91
|
* Set `false` to drive your own ThemeProvider from `useColorMode().theme`. */
|
|
@@ -136,26 +121,15 @@ export function ColorModeProvider({
|
|
|
136
121
|
const setPreference = useCallback(
|
|
137
122
|
(next: ColorModePreference) => {
|
|
138
123
|
if (controlled === undefined) setInternal(next)
|
|
139
|
-
|
|
124
|
+
writeStoredRaw(storageKey, next)
|
|
140
125
|
onPreferenceChange?.(next)
|
|
141
126
|
},
|
|
142
127
|
[controlled, storageKey, onPreferenceChange],
|
|
143
128
|
)
|
|
144
129
|
|
|
145
|
-
// Track the OS scheme so `'system'` stays live
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
// initializer would diverge (server 'dark' vs client's real scheme) and trip
|
|
149
|
-
// a hydration mismatch. The effect reconciles to the real scheme after mount.
|
|
150
|
-
const [systemTheme, setSystemTheme] = useState<'light' | 'dark'>('dark')
|
|
151
|
-
useEffect(() => {
|
|
152
|
-
if (!canUseDom) return
|
|
153
|
-
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
|
154
|
-
const onChange = () => setSystemTheme(mq.matches ? 'dark' : 'light')
|
|
155
|
-
onChange()
|
|
156
|
-
mq.addEventListener('change', onChange)
|
|
157
|
-
return () => mq.removeEventListener('change', onChange)
|
|
158
|
-
}, [])
|
|
130
|
+
// Track the OS scheme so `'system'` stays live (web: prefers-color-scheme,
|
|
131
|
+
// after mount so hydration matches; native: RN Appearance).
|
|
132
|
+
const systemTheme = useSystemScheme()
|
|
159
133
|
|
|
160
134
|
const theme: ThemeName = preference === 'system' ? systemTheme : preference
|
|
161
135
|
|
|
@@ -196,7 +170,12 @@ export function ColorModeToggle({size = 'md', 'aria-label': ariaLabel}: ColorMod
|
|
|
196
170
|
const label = ariaLabel ?? `Color mode: ${LABEL[preference]}. Switch to ${LABEL[next]}.`
|
|
197
171
|
|
|
198
172
|
return (
|
|
199
|
-
<html.button
|
|
173
|
+
<html.button
|
|
174
|
+
type="button"
|
|
175
|
+
aria-label={label}
|
|
176
|
+
onClick={cycle}
|
|
177
|
+
style={[styles.button, isNative && styles.buttonNative]}
|
|
178
|
+
>
|
|
200
179
|
<Icon name={ICON[preference]} size={size} />
|
|
201
180
|
</html.button>
|
|
202
181
|
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Native has no localStorage: an app persists the preference itself, through
|
|
2
|
+
// ColorModeProvider's `preference` and `onPreferenceChange`.
|
|
3
|
+
|
|
4
|
+
export function readStoredRaw(_key: string | null): string | null {
|
|
5
|
+
return null
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function writeStoredRaw(_key: string | null, _value: string): void {}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The stored preference, as a raw string; null without storage or a value. */
|
|
2
|
+
export function readStoredRaw(key: string | null): string | null {
|
|
3
|
+
if (!key || typeof window === 'undefined') return null
|
|
4
|
+
try {
|
|
5
|
+
return window.localStorage.getItem(key)
|
|
6
|
+
} catch {
|
|
7
|
+
// Private mode / blocked storage — degrade to no persistence.
|
|
8
|
+
return null
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function writeStoredRaw(key: string | null, value: string): void {
|
|
13
|
+
if (!key || typeof window === 'undefined') return
|
|
14
|
+
try {
|
|
15
|
+
window.localStorage.setItem(key, value)
|
|
16
|
+
} catch {
|
|
17
|
+
/* ignore */
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -31,4 +31,10 @@ export const styles = css.create({
|
|
|
31
31
|
outlineColor: colors.accent,
|
|
32
32
|
outlineOffset: borders.focusOffset,
|
|
33
33
|
},
|
|
34
|
+
// RSD on native lays out only display: flex (not inline-flex), and a colour
|
|
35
|
+
// transition there ends near-black: keep the layout, drop the colour easing.
|
|
36
|
+
buttonNative: {
|
|
37
|
+
display: 'flex',
|
|
38
|
+
transitionProperty: 'opacity',
|
|
39
|
+
},
|
|
34
40
|
})
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {useEffect, useState} from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The OS colour scheme, live. The first render answers `'dark'`, what the
|
|
5
|
+
* server renders without a DOM, so hydration matches; the effect then reads
|
|
6
|
+
* `prefers-color-scheme` and follows its changes.
|
|
7
|
+
*/
|
|
8
|
+
export function useSystemScheme(): 'light' | 'dark' {
|
|
9
|
+
const [scheme, setScheme] = useState<'light' | 'dark'>('dark')
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') return
|
|
12
|
+
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
|
13
|
+
const onChange = () => setScheme(mq.matches ? 'dark' : 'light')
|
|
14
|
+
onChange()
|
|
15
|
+
mq.addEventListener('change', onChange)
|
|
16
|
+
return () => mq.removeEventListener('change', onChange)
|
|
17
|
+
}, [])
|
|
18
|
+
return scheme
|
|
19
|
+
}
|
|
@@ -2,7 +2,7 @@ import type {ComponentMeta} from '../component-meta'
|
|
|
2
2
|
|
|
3
3
|
export const meta: ComponentMeta = {
|
|
4
4
|
description:
|
|
5
|
-
|
|
5
|
+
"Searchable dropdown for selecting a value from a filterable list. Combines a text input with a popup listbox. Compound component — Root is required. Combobox.Popup takes label (the list's accessible name). On React Native the field opens a bottom sheet at its large detent, above the keyboard, titled by label with a Cancel text button (cancelLabel): a search field is pinned under the header and focused, the list below filters as you type, Combobox.Empty shows when nothing matches, and a press picks an option and closes the sheet. The sheet's search text starts empty each time; Combobox.Trigger renders nothing on native.",
|
|
6
6
|
whenToUse: [
|
|
7
7
|
'Large option lists where users benefit from typing to filter (e.g. country, city, user picker)',
|
|
8
8
|
'Autocomplete or typeahead scenarios',
|