@duro-app/ui 5.12.0 → 5.13.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 (188) hide show
  1. package/dist/Table-Bms2ftt3.js +1694 -0
  2. package/dist/Table-Bms2ftt3.js.map +1 -0
  3. package/dist/components/AppShell/native.css.d.ts +53 -0
  4. package/dist/components/AppShell/native.css.d.ts.map +1 -0
  5. package/dist/components/Drawer/Drawer.d.ts +7 -35
  6. package/dist/components/Drawer/Drawer.d.ts.map +1 -1
  7. package/dist/components/Drawer/Drawer.meta.d.ts.map +1 -1
  8. package/dist/components/Drawer/DrawerScroll.stories.d.ts.map +1 -1
  9. package/dist/components/Drawer/DrawerSnap.stories.d.ts +20 -0
  10. package/dist/components/Drawer/DrawerSnap.stories.d.ts.map +1 -0
  11. package/dist/components/Drawer/native.css.d.ts +55 -0
  12. package/dist/components/Drawer/native.css.d.ts.map +1 -0
  13. package/dist/components/Drawer/parts.css.d.ts +96 -0
  14. package/dist/components/Drawer/parts.css.d.ts.map +1 -0
  15. package/dist/components/Drawer/parts.d.ts +56 -0
  16. package/dist/components/Drawer/parts.d.ts.map +1 -0
  17. package/dist/components/Drawer/sheetPhysics.d.ts +56 -0
  18. package/dist/components/Drawer/sheetPhysics.d.ts.map +1 -0
  19. package/dist/components/Drawer/styles.css.d.ts +13 -58
  20. package/dist/components/Drawer/styles.css.d.ts.map +1 -1
  21. package/dist/components/Drawer/touchInput.d.ts +35 -0
  22. package/dist/components/Drawer/touchInput.d.ts.map +1 -0
  23. package/dist/components/Drawer/useDrawerRoot.d.ts +33 -0
  24. package/dist/components/Drawer/useDrawerRoot.d.ts.map +1 -0
  25. package/dist/components/Drawer/useSheetHeights.d.ts +7 -0
  26. package/dist/components/Drawer/useSheetHeights.d.ts.map +1 -0
  27. package/dist/components/Drawer/useSwipeDismiss.d.ts +13 -2
  28. package/dist/components/Drawer/useSwipeDismiss.d.ts.map +1 -1
  29. package/dist/components/Menu/Menu.d.ts +7 -1
  30. package/dist/components/Menu/Menu.d.ts.map +1 -1
  31. package/dist/components/Menu/native.css.d.ts +14 -0
  32. package/dist/components/Menu/native.css.d.ts.map +1 -0
  33. package/dist/components/Menu/useMenuOpen.d.ts +16 -0
  34. package/dist/components/Menu/useMenuOpen.d.ts.map +1 -0
  35. package/dist/components/Menu/useMenuRoot.d.ts +3 -6
  36. package/dist/components/Menu/useMenuRoot.d.ts.map +1 -1
  37. package/dist/components/Popover/native.css.d.ts +11 -0
  38. package/dist/components/Popover/native.css.d.ts.map +1 -0
  39. package/dist/components/SideNav/Chevron.d.ts +4 -0
  40. package/dist/components/SideNav/Chevron.d.ts.map +1 -0
  41. package/dist/components/SideNav/SideNav.d.ts.map +1 -1
  42. package/dist/components/SideNav/styles.css.d.ts +6 -0
  43. package/dist/components/SideNav/styles.css.d.ts.map +1 -1
  44. package/dist/components/SideNav/types.d.ts +6 -0
  45. package/dist/components/SideNav/types.d.ts.map +1 -0
  46. package/dist/components/SplitPane/SplitPane.d.ts +38 -0
  47. package/dist/components/SplitPane/SplitPane.d.ts.map +1 -0
  48. package/dist/components/SplitPane/SplitPane.meta.d.ts +3 -0
  49. package/dist/components/SplitPane/SplitPane.meta.d.ts.map +1 -0
  50. package/dist/components/SplitPane/SplitPane.stories.d.ts +17 -0
  51. package/dist/components/SplitPane/SplitPane.stories.d.ts.map +1 -0
  52. package/dist/components/SplitPane/styles.css.d.ts +78 -0
  53. package/dist/components/SplitPane/styles.css.d.ts.map +1 -0
  54. package/dist/components/Tabs/TabLayout.d.ts +7 -0
  55. package/dist/components/Tabs/TabLayout.d.ts.map +1 -0
  56. package/dist/components/Tabs/Tabs.d.ts.map +1 -1
  57. package/dist/components/Tabs/indicator.d.ts +28 -0
  58. package/dist/components/Tabs/indicator.d.ts.map +1 -0
  59. package/dist/components/Tabs/styles.css.d.ts +4 -0
  60. package/dist/components/Tabs/styles.css.d.ts.map +1 -1
  61. package/dist/components/Tabs/useTabIndicator.d.ts +4 -0
  62. package/dist/components/Tabs/useTabIndicator.d.ts.map +1 -0
  63. package/dist/components/Tabs/useTabKeys.d.ts +13 -0
  64. package/dist/components/Tabs/useTabKeys.d.ts.map +1 -0
  65. package/dist/components/Tag/RemoveGlyph.d.ts +4 -0
  66. package/dist/components/Tag/RemoveGlyph.d.ts.map +1 -0
  67. package/dist/components/Tag/Tag.d.ts +2 -24
  68. package/dist/components/Tag/Tag.d.ts.map +1 -1
  69. package/dist/components/Tag/styles.css.d.ts +8 -0
  70. package/dist/components/Tag/styles.css.d.ts.map +1 -1
  71. package/dist/components/Tag/types.d.ts +39 -0
  72. package/dist/components/Tag/types.d.ts.map +1 -0
  73. package/dist/components/Toast/ToastProvider.d.ts +2 -21
  74. package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
  75. package/dist/components/Toast/native.css.d.ts +81 -0
  76. package/dist/components/Toast/native.css.d.ts.map +1 -0
  77. package/dist/components/Toast/store.d.ts +47 -0
  78. package/dist/components/Toast/store.d.ts.map +1 -0
  79. package/dist/components/Tooltip/TooltipHint.d.ts +9 -0
  80. package/dist/components/Tooltip/TooltipHint.d.ts.map +1 -0
  81. package/dist/components/Tree/Tree.d.ts.map +1 -1
  82. package/dist/components/Tree/styles.css.d.ts +6 -0
  83. package/dist/components/Tree/styles.css.d.ts.map +1 -1
  84. package/dist/components/Tree/useTreeKeyboard.d.ts +19 -0
  85. package/dist/components/Tree/useTreeKeyboard.d.ts.map +1 -0
  86. package/dist/components/VirtualList/VirtualList.d.ts +33 -0
  87. package/dist/components/VirtualList/VirtualList.d.ts.map +1 -0
  88. package/dist/components/VirtualList/VirtualList.meta.d.ts +3 -0
  89. package/dist/components/VirtualList/VirtualList.meta.d.ts.map +1 -0
  90. package/dist/components/VirtualList/VirtualList.stories.d.ts +15 -0
  91. package/dist/components/VirtualList/VirtualList.stories.d.ts.map +1 -0
  92. package/dist/components/VirtualList/styles.css.d.ts +28 -0
  93. package/dist/components/VirtualList/styles.css.d.ts.map +1 -0
  94. package/dist/index.css +1 -1
  95. package/dist/index.d.ts +3 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.js +4935 -4260
  98. package/dist/index.js.map +1 -1
  99. package/dist/shared/ButtonHost.d.ts +11 -0
  100. package/dist/shared/ButtonHost.d.ts.map +1 -0
  101. package/dist/shared/pressableChild.d.ts +14 -0
  102. package/dist/shared/pressableChild.d.ts.map +1 -0
  103. package/dist/table.js +1 -1
  104. package/package.json +2 -2
  105. package/src/components/AppShell/AppShell.meta.ts +1 -1
  106. package/src/components/AppShell/AppShell.native.tsx +174 -0
  107. package/src/components/AppShell/native.css.ts +56 -0
  108. package/src/components/Button/Button.tsx +3 -3
  109. package/src/components/Drawer/Drawer.meta.ts +2 -1
  110. package/src/components/Drawer/Drawer.native.tsx +469 -0
  111. package/src/components/Drawer/Drawer.tsx +58 -145
  112. package/src/components/Drawer/DrawerScroll.stories.tsx +4 -54
  113. package/src/components/Drawer/DrawerSnap.stories.tsx +205 -0
  114. package/src/components/Drawer/native.css.ts +58 -0
  115. package/src/components/Drawer/parts.css.ts +125 -0
  116. package/src/components/Drawer/parts.tsx +110 -0
  117. package/src/components/Drawer/sheetPhysics.ts +103 -0
  118. package/src/components/Drawer/styles.css.ts +15 -74
  119. package/src/components/Drawer/touchInput.ts +81 -0
  120. package/src/components/Drawer/useDrawerRoot.ts +90 -0
  121. package/src/components/Drawer/useSheetHeights.ts +42 -0
  122. package/src/components/Drawer/useSwipeDismiss.ts +146 -23
  123. package/src/components/Menu/Menu.meta.ts +1 -1
  124. package/src/components/Menu/Menu.native.tsx +268 -0
  125. package/src/components/Menu/Menu.tsx +11 -2
  126. package/src/components/Menu/native.css.ts +18 -0
  127. package/src/components/Menu/useMenuOpen.ts +33 -0
  128. package/src/components/Menu/useMenuRoot.ts +7 -26
  129. package/src/components/Popover/Popover.meta.ts +1 -1
  130. package/src/components/Popover/Popover.native.tsx +205 -0
  131. package/src/components/Popover/native.css.ts +13 -0
  132. package/src/components/SideNav/Chevron.native.tsx +20 -0
  133. package/src/components/SideNav/Chevron.tsx +20 -0
  134. package/src/components/SideNav/SideNav.meta.ts +1 -1
  135. package/src/components/SideNav/SideNav.tsx +57 -41
  136. package/src/components/SideNav/styles.css.ts +8 -0
  137. package/src/components/SideNav/types.ts +6 -0
  138. package/src/components/SplitPane/SplitPane.meta.ts +40 -0
  139. package/src/components/SplitPane/SplitPane.native.tsx +47 -0
  140. package/src/components/SplitPane/SplitPane.stories.tsx +148 -0
  141. package/src/components/SplitPane/SplitPane.tsx +218 -0
  142. package/src/components/SplitPane/styles.css.ts +81 -0
  143. package/src/components/Tabs/TabLayout.native.tsx +9 -0
  144. package/src/components/Tabs/TabLayout.tsx +6 -0
  145. package/src/components/Tabs/Tabs.meta.ts +1 -1
  146. package/src/components/Tabs/Tabs.tsx +38 -136
  147. package/src/components/Tabs/indicator.ts +36 -0
  148. package/src/components/Tabs/styles.css.ts +7 -0
  149. package/src/components/Tabs/useTabIndicator.native.ts +35 -0
  150. package/src/components/Tabs/useTabIndicator.ts +49 -0
  151. package/src/components/Tabs/useTabKeys.native.ts +2 -0
  152. package/src/components/Tabs/useTabKeys.ts +99 -0
  153. package/src/components/Tag/RemoveGlyph.native.tsx +17 -0
  154. package/src/components/Tag/RemoveGlyph.tsx +10 -0
  155. package/src/components/Tag/Tag.meta.ts +1 -1
  156. package/src/components/Tag/Tag.native.tsx +70 -0
  157. package/src/components/Tag/Tag.tsx +9 -37
  158. package/src/components/Tag/styles.css.ts +9 -0
  159. package/src/components/Tag/types.ts +43 -0
  160. package/src/components/Toast/Toast.meta.ts +1 -1
  161. package/src/components/Toast/ToastProvider.native.tsx +226 -0
  162. package/src/components/Toast/ToastProvider.tsx +22 -84
  163. package/src/components/Toast/native.css.ts +78 -0
  164. package/src/components/Toast/store.ts +96 -0
  165. package/src/components/Toggle/Toggle.tsx +3 -3
  166. package/src/components/Tooltip/Tooltip.meta.ts +1 -1
  167. package/src/components/Tooltip/Tooltip.native.tsx +36 -0
  168. package/src/components/Tooltip/TooltipHint.ts +13 -0
  169. package/src/components/Tree/Tree.meta.ts +1 -1
  170. package/src/components/Tree/Tree.tsx +23 -135
  171. package/src/components/Tree/styles.css.ts +6 -0
  172. package/src/components/Tree/useTreeKeyboard.native.ts +5 -0
  173. package/src/components/Tree/useTreeKeyboard.ts +152 -0
  174. package/src/components/VirtualList/VirtualList.meta.ts +39 -0
  175. package/src/components/VirtualList/VirtualList.stories.tsx +95 -0
  176. package/src/components/VirtualList/VirtualList.tsx +103 -0
  177. package/src/components/VirtualList/styles.css.ts +31 -0
  178. package/src/hooks/useContainerBelow.native.ts +30 -0
  179. package/src/index.native.ts +33 -0
  180. package/src/index.ts +8 -1
  181. package/src/shared/ButtonHost.native.tsx +70 -0
  182. package/src/shared/ButtonHost.tsx +12 -0
  183. package/src/shared/portal/AnchoredOverlay.native.tsx +117 -0
  184. package/src/shared/pressableChild.ts +33 -0
  185. package/src/shared/useKeyboardHeight.native.ts +18 -0
  186. package/src/shared/useScreenReaderEnabled.native.ts +19 -0
  187. package/dist/Table-ColfpgKV.js +0 -1552
  188. 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"}
package/dist/table.js CHANGED
@@ -1,4 +1,4 @@
1
- import { M as F, m as gt, R as it, H as kt, n as pt, o as bt, p as ft, q as mt, b as ht, t as yt, r as St, v as Tt } from "./Table-ColfpgKV.js";
1
+ import { M as F, p as gt, R as it, H as kt, q as pt, t as bt, v as ft, w as mt, b as ht, f as yt, r as St, x as Tt } from "./Table-Bms2ftt3.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.12.0",
3
+ "version": "5.13.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -90,7 +90,7 @@
90
90
  },
91
91
  "dependencies": {
92
92
  "@tanstack/react-virtual": "^3.14.6",
93
- "@duro-app/tokens": "^5.12.0"
93
+ "@duro-app/tokens": "^5.13.0"
94
94
  },
95
95
  "devDependencies": {
96
96
  "@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,7 +1,7 @@
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
7
  import {styles} from './styles.css'
@@ -51,7 +51,7 @@ export function Button({
51
51
  }: ButtonProps) {
52
52
  const grouped = useGroupedControl<HTMLButtonElement>({disabled})
53
53
  return (
54
- <html.button
54
+ <ButtonHost
55
55
  ref={isNative ? ref : mergeRefs(ref, grouped.ref)}
56
56
  tabIndex={grouped.tabIndex}
57
57
  onFocus={grouped.onFocus}
@@ -72,6 +72,6 @@ export function Button({
72
72
  ]}
73
73
  >
74
74
  {children}
75
- </html.button>
75
+ </ButtonHost>
76
76
  )
77
77
  }
@@ -2,10 +2,11 @@ import type {ComponentMeta} from '../component-meta'
2
2
 
3
3
  export const meta: ComponentMeta = {
4
4
  description:
5
- 'Modal sliding panel from a screen edge (right, left, or bottom). Slides in with backdrop overlay. Supports swipe-to-dismiss gestures on mobile, backdrop click dismiss, and Escape key. Swipe yields to an inner Duro ScrollArea: a drag there scrolls it, and dismisses only toward the edge from the scroll start (a native overflow:auto child is not covered — use ScrollArea). Use for secondary workflows, settings panels, or mobile-friendly detail views.',
5
+ 'Modal sliding panel from a screen edge (right, left, or bottom). Slides in with backdrop overlay. Supports swipe-to-dismiss gestures on mobile, backdrop click dismiss, and Escape key. Swipe yields to an inner Duro ScrollArea: a drag there scrolls it, and dismisses only toward the edge from the scroll start (a native overflow:auto child is not covered — use ScrollArea). A bottom drawer with snapPoints (medium and large) is a phone sheet: it rests at its detents, a grabber steps through them on a tap, a drag moves between them and dismisses below the lowest; its Body is the scroll the drag defers to. Header titleAlign="center" centres the title with a text close at the end. Use for secondary workflows, settings panels, or mobile-friendly detail views.',
6
6
  whenToUse: [
7
7
  'Settings or configuration panels that slide in from the side',
8
8
  'Mobile-friendly detail views (bottom sheet pattern)',
9
+ 'Phone pickers and lists that need more room on demand (snapPoints)',
9
10
  'Secondary workflows that need more space than a dialog',
10
11
  'Filters, navigation drawers, or form panels',
11
12
  ],