@milktop/react-ui 0.0.0-stage → 0.1.1

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 (250) hide show
  1. package/README.md +173 -2
  2. package/dist/accordion/Accordion.d.ts +65 -0
  3. package/dist/accordion/Accordion.js +61 -0
  4. package/dist/accordion/accordion.css +17 -0
  5. package/dist/action-bar/ActionBar.d.ts +33 -0
  6. package/dist/action-bar/ActionBar.js +48 -0
  7. package/dist/action-bar/action-bar.css +22 -0
  8. package/dist/alert/Alert.d.ts +28 -0
  9. package/dist/alert/Alert.js +12 -0
  10. package/dist/alert/alert.css +26 -0
  11. package/dist/app-shell/AppShell.d.ts +38 -0
  12. package/dist/app-shell/AppShell.js +92 -0
  13. package/dist/app-shell/Topbar.d.ts +20 -0
  14. package/dist/app-shell/Topbar.js +13 -0
  15. package/dist/app-shell/app-shell.css +41 -0
  16. package/dist/app-shell/shell.d.ts +9 -0
  17. package/dist/app-shell/shell.js +3 -0
  18. package/dist/attachments/Attachments.d.ts +84 -0
  19. package/dist/attachments/Attachments.js +235 -0
  20. package/dist/attachments/attachments.css +86 -0
  21. package/dist/avatar/Avatar.d.ts +31 -0
  22. package/dist/avatar/Avatar.js +41 -0
  23. package/dist/avatar/AvatarGroup.d.ts +41 -0
  24. package/dist/avatar/AvatarGroup.js +17 -0
  25. package/dist/avatar/avatar.css +73 -0
  26. package/dist/badge/Badge.d.ts +26 -0
  27. package/dist/badge/Badge.js +10 -0
  28. package/dist/badge/badge.css +24 -0
  29. package/dist/banner/Banner.d.ts +33 -0
  30. package/dist/banner/Banner.js +14 -0
  31. package/dist/banner/banner.css +25 -0
  32. package/dist/breadcrumbs/Breadcrumbs.d.ts +39 -0
  33. package/dist/breadcrumbs/Breadcrumbs.js +28 -0
  34. package/dist/breadcrumbs/breadcrumbs.css +18 -0
  35. package/dist/button/Button.d.ts +40 -0
  36. package/dist/button/Button.js +19 -0
  37. package/dist/button/button.css +63 -0
  38. package/dist/card/Card.d.ts +355 -0
  39. package/dist/card/Card.js +56 -0
  40. package/dist/card/card.css +43 -0
  41. package/dist/chart/AreaChart.d.ts +19 -0
  42. package/dist/chart/AreaChart.js +61 -0
  43. package/dist/chart/BarChart.d.ts +3 -0
  44. package/dist/chart/BarChart.js +60 -0
  45. package/dist/chart/ChartFrame.d.ts +25 -0
  46. package/dist/chart/ChartFrame.js +22 -0
  47. package/dist/chart/DonutChart.d.ts +20 -0
  48. package/dist/chart/DonutChart.js +41 -0
  49. package/dist/chart/Sparkline.d.ts +26 -0
  50. package/dist/chart/Sparkline.js +36 -0
  51. package/dist/chart/chart.css +51 -0
  52. package/dist/chart/chart.d.ts +23 -0
  53. package/dist/chart/chart.js +19 -0
  54. package/dist/checkbox/Checkbox.d.ts +50 -0
  55. package/dist/checkbox/Checkbox.js +19 -0
  56. package/dist/checkbox/CheckboxGroup.d.ts +44 -0
  57. package/dist/checkbox/CheckboxGroup.js +80 -0
  58. package/dist/checkbox/checkbox.css +49 -0
  59. package/dist/collapsible/Collapsible.d.ts +41 -0
  60. package/dist/collapsible/Collapsible.js +15 -0
  61. package/dist/collapsible/collapsible.css +20 -0
  62. package/dist/color-scheme.d.ts +18 -0
  63. package/dist/color-scheme.js +84 -0
  64. package/dist/command/Command.d.ts +64 -0
  65. package/dist/command/Command.js +162 -0
  66. package/dist/command/command.css +47 -0
  67. package/dist/confirm/confirm.css +5 -0
  68. package/dist/confirm/confirm.d.ts +42 -0
  69. package/dist/confirm/confirm.js +115 -0
  70. package/dist/copy-button/CopyButton.d.ts +30 -0
  71. package/dist/copy-button/CopyButton.js +23 -0
  72. package/dist/copy-button/copy-button.css +12 -0
  73. package/dist/css.d.ts +2 -0
  74. package/dist/data-list/DataList.d.ts +55 -0
  75. package/dist/data-list/DataList.js +43 -0
  76. package/dist/data-list/data-list.css +19 -0
  77. package/dist/date-picker/DatePicker.d.ts +95 -0
  78. package/dist/date-picker/DatePicker.js +237 -0
  79. package/dist/date-picker/date-picker.css +53 -0
  80. package/dist/dialog/Dialog.d.ts +98 -0
  81. package/dist/dialog/Dialog.js +92 -0
  82. package/dist/dialog/dialog.css +31 -0
  83. package/dist/divider/Divider.d.ts +27 -0
  84. package/dist/divider/Divider.js +8 -0
  85. package/dist/divider/divider.css +12 -0
  86. package/dist/editable/Editable.d.ts +47 -0
  87. package/dist/editable/Editable.js +51 -0
  88. package/dist/editable/editable.css +25 -0
  89. package/dist/editor/Editor.d.ts +59 -0
  90. package/dist/editor/Editor.js +220 -0
  91. package/dist/editor/editor.css +42 -0
  92. package/dist/editor/image.d.ts +1 -0
  93. package/dist/editor/image.js +23 -0
  94. package/dist/empty-state/EmptyState.d.ts +22 -0
  95. package/dist/empty-state/EmptyState.js +9 -0
  96. package/dist/empty-state/empty-state.css +14 -0
  97. package/dist/event-calendar/EventCalendar.d.ts +213 -0
  98. package/dist/event-calendar/EventCalendar.js +686 -0
  99. package/dist/event-calendar/dates.d.ts +8 -0
  100. package/dist/event-calendar/dates.js +21 -0
  101. package/dist/event-calendar/event-calendar.css +157 -0
  102. package/dist/field/Field.d.ts +66 -0
  103. package/dist/field/Field.js +23 -0
  104. package/dist/field/field.css +26 -0
  105. package/dist/file-upload/FileUpload.d.ts +60 -0
  106. package/dist/file-upload/FileUpload.js +102 -0
  107. package/dist/file-upload/file-upload.css +32 -0
  108. package/dist/format/FormatNumber.d.ts +42 -0
  109. package/dist/format/FormatNumber.js +59 -0
  110. package/dist/format/LocaleProvider.d.ts +33 -0
  111. package/dist/format/LocaleProvider.js +48 -0
  112. package/dist/format/format.d.ts +37 -0
  113. package/dist/format/format.js +74 -0
  114. package/dist/format/messages.d.ts +211 -0
  115. package/dist/format/messages.js +202 -0
  116. package/dist/highlight/Highlight.d.ts +11 -0
  117. package/dist/highlight/Highlight.js +14 -0
  118. package/dist/highlight/highlight.css +17 -0
  119. package/dist/hover-card/HoverCard.d.ts +47 -0
  120. package/dist/hover-card/HoverCard.js +18 -0
  121. package/dist/hover-card/hover-card.css +19 -0
  122. package/dist/index.d.ts +84 -0
  123. package/dist/index.js +85 -0
  124. package/dist/input/Input.d.ts +41 -0
  125. package/dist/input/Input.js +48 -0
  126. package/dist/input/input.css +25 -0
  127. package/dist/kbd/Kbd.d.ts +19 -0
  128. package/dist/kbd/Kbd.js +20 -0
  129. package/dist/kbd/kbd.css +8 -0
  130. package/dist/menu/Menu.d.ts +223 -0
  131. package/dist/menu/Menu.js +275 -0
  132. package/dist/menu/menu.css +38 -0
  133. package/dist/native-select/NativeSelect.d.ts +36 -0
  134. package/dist/native-select/NativeSelect.js +51 -0
  135. package/dist/native-select/native-select.css +16 -0
  136. package/dist/number-input/NumberInput.d.ts +56 -0
  137. package/dist/number-input/NumberInput.js +67 -0
  138. package/dist/number-input/number-input.css +21 -0
  139. package/dist/page/Page.d.ts +30 -0
  140. package/dist/page/Page.js +9 -0
  141. package/dist/page/page.css +17 -0
  142. package/dist/pagination/Pagination.d.ts +31 -0
  143. package/dist/pagination/Pagination.js +58 -0
  144. package/dist/pagination/pagination.css +15 -0
  145. package/dist/password-input/PasswordInput.d.ts +47 -0
  146. package/dist/password-input/PasswordInput.js +35 -0
  147. package/dist/password-input/password-input.css +18 -0
  148. package/dist/pin-input/PinInput.d.ts +45 -0
  149. package/dist/pin-input/PinInput.js +22 -0
  150. package/dist/pin-input/pin-input.css +13 -0
  151. package/dist/popover/Popover.d.ts +65 -0
  152. package/dist/popover/Popover.js +26 -0
  153. package/dist/popover/popover.css +16 -0
  154. package/dist/progress/Progress.d.ts +56 -0
  155. package/dist/progress/Progress.js +28 -0
  156. package/dist/progress/progress.css +24 -0
  157. package/dist/prose/Prose.d.ts +23 -0
  158. package/dist/prose/Prose.js +10 -0
  159. package/dist/prose/prose.css +29 -0
  160. package/dist/radio-group/RadioGroup.d.ts +59 -0
  161. package/dist/radio-group/RadioGroup.js +37 -0
  162. package/dist/radio-group/radio-group.css +30 -0
  163. package/dist/rolling-number/RollingNumber.d.ts +27 -0
  164. package/dist/rolling-number/RollingNumber.js +18 -0
  165. package/dist/rolling-number/rolling-number.css +10 -0
  166. package/dist/segment-group/Segmented.d.ts +68 -0
  167. package/dist/segment-group/Segmented.js +79 -0
  168. package/dist/segment-group/segment-group.css +25 -0
  169. package/dist/select/ListSelect.d.ts +5 -0
  170. package/dist/select/ListSelect.js +82 -0
  171. package/dist/select/SearchSelect.d.ts +43 -0
  172. package/dist/select/SearchSelect.js +219 -0
  173. package/dist/select/Select.d.ts +147 -0
  174. package/dist/select/Select.js +32 -0
  175. package/dist/select/items.d.ts +17 -0
  176. package/dist/select/items.js +14 -0
  177. package/dist/select/search-select.css +45 -0
  178. package/dist/select/select.css +52 -0
  179. package/dist/sheet/Sheet.d.ts +88 -0
  180. package/dist/sheet/Sheet.js +66 -0
  181. package/dist/sheet/sheet.css +44 -0
  182. package/dist/sidebar/NavGroup.d.ts +29 -0
  183. package/dist/sidebar/NavGroup.js +78 -0
  184. package/dist/sidebar/NavItem.d.ts +33 -0
  185. package/dist/sidebar/NavItem.js +50 -0
  186. package/dist/sidebar/NavSection.d.ts +20 -0
  187. package/dist/sidebar/NavSection.js +44 -0
  188. package/dist/sidebar/Sidebar.d.ts +65 -0
  189. package/dist/sidebar/Sidebar.js +59 -0
  190. package/dist/sidebar/SidebarUser.d.ts +31 -0
  191. package/dist/sidebar/SidebarUser.js +14 -0
  192. package/dist/sidebar/context.d.ts +13 -0
  193. package/dist/sidebar/context.js +6 -0
  194. package/dist/sidebar/sidebar.css +109 -0
  195. package/dist/skeleton/Skeleton.d.ts +25 -0
  196. package/dist/skeleton/Skeleton.js +8 -0
  197. package/dist/skeleton/skeleton.css +13 -0
  198. package/dist/slider/Slider.d.ts +70 -0
  199. package/dist/slider/Slider.js +65 -0
  200. package/dist/slider/slider.css +17 -0
  201. package/dist/sortable-list/SortableList.d.ts +46 -0
  202. package/dist/sortable-list/SortableList.js +85 -0
  203. package/dist/sortable-list/sortable-list.css +25 -0
  204. package/dist/spinner/Spinner.d.ts +18 -0
  205. package/dist/spinner/Spinner.js +11 -0
  206. package/dist/spinner/spinner.css +9 -0
  207. package/dist/stat/Stat.d.ts +42 -0
  208. package/dist/stat/Stat.js +21 -0
  209. package/dist/stat/Stats.d.ts +23 -0
  210. package/dist/stat/Stats.js +8 -0
  211. package/dist/stat/stat.css +34 -0
  212. package/dist/swatch-picker/SwatchPicker.d.ts +45 -0
  213. package/dist/swatch-picker/SwatchPicker.js +54 -0
  214. package/dist/swatch-picker/swatch-picker.css +38 -0
  215. package/dist/switch/Switch.d.ts +39 -0
  216. package/dist/switch/Switch.js +17 -0
  217. package/dist/switch/switch.css +13 -0
  218. package/dist/table/Table.d.ts +90 -0
  219. package/dist/table/Table.js +145 -0
  220. package/dist/table/table.css +44 -0
  221. package/dist/tabs/Tabs.d.ts +87 -0
  222. package/dist/tabs/Tabs.js +90 -0
  223. package/dist/tabs/tabs.css +53 -0
  224. package/dist/tags-input/TagsInput.d.ts +69 -0
  225. package/dist/tags-input/TagsInput.js +95 -0
  226. package/dist/tags-input/tags-input.css +37 -0
  227. package/dist/textarea/Textarea.d.ts +24 -0
  228. package/dist/textarea/Textarea.js +51 -0
  229. package/dist/textarea/textarea.css +14 -0
  230. package/dist/theme-toggle/ThemeToggle.d.ts +49 -0
  231. package/dist/theme-toggle/ThemeToggle.js +71 -0
  232. package/dist/theme-toggle/theme-toggle.css +23 -0
  233. package/dist/theme.css +119 -0
  234. package/dist/time-input/TimeInput.d.ts +25 -0
  235. package/dist/time-input/TimeInput.js +20 -0
  236. package/dist/timeline/Timeline.d.ts +50 -0
  237. package/dist/timeline/Timeline.js +15 -0
  238. package/dist/timeline/timeline.css +29 -0
  239. package/dist/toast/Toaster.d.ts +36 -0
  240. package/dist/toast/Toaster.js +25 -0
  241. package/dist/toast/toast.css +33 -0
  242. package/dist/tooltip/Tooltip.d.ts +55 -0
  243. package/dist/tooltip/Tooltip.js +48 -0
  244. package/dist/tooltip/tooltip.css +18 -0
  245. package/dist/tree-view/TreeView.d.ts +86 -0
  246. package/dist/tree-view/TreeView.js +74 -0
  247. package/dist/tree-view/tree-view.css +29 -0
  248. package/dist/utils.d.ts +19 -0
  249. package/dist/utils.js +59 -0
  250. package/package.json +99 -4
package/README.md CHANGED
@@ -1,3 +1,174 @@
1
- # Temporary Holding Version
1
+ # @milktop/react-ui
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ React components on [Ark UI](https://ark-ui.com) (and so on
4
+ [Zag](https://zagjs.com) state machines), with
5
+ [imba-ui](https://github.com/milktop/imba-ui)'s and
6
+ [svelte-ui](https://github.com/milktop/svelte-ui)'s API, tokens and design.
7
+
8
+ ## One component, or its parts
9
+
10
+ Every component is a **shortcut**: one element, props in, done.
11
+
12
+ ```jsx
13
+ import { Field, Select, Checkbox, Button } from '@milktop/react-ui'
14
+
15
+ <Field label="Student" error={errors.student}>
16
+ <Select items={students} labelKey="name" valueKey="id" value={student} onValueChange={setStudent} />
17
+ </Field>
18
+ <Checkbox label="Remind them" checked={remind} onCheckedChange={setRemind} />
19
+ <Button variant="primary" loading={saving}>Save</Button>
20
+ ```
21
+
22
+ The same component carries its **parts**, for the times the shortcut's
23
+ layout isn't what you need. They're Ark's own parts, styled by the same CSS,
24
+ so a hand-built component looks just like the shortcut:
25
+
26
+ ```jsx
27
+ <Checkbox.Root>
28
+ <Checkbox.Label>Terms</Checkbox.Label>
29
+ <Checkbox.Control><Checkbox.Indicator>✓</Checkbox.Indicator></Checkbox.Control>
30
+ <a href="/terms">read them</a>
31
+ <Checkbox.HiddenInput />
32
+ </Checkbox.Root>
33
+ ```
34
+
35
+ No CLI and no copied snippets: every shortcut ships in the package, and
36
+ where one doesn't fit you build from the parts.
37
+
38
+ ## Components
39
+
40
+ So far:
41
+
42
+ - Actions: Button · CopyButton
43
+ - Display: Avatar · AvatarGroup · Badge · Card (props or composed from Card.Header/Body/Footer) ·
44
+ Table (sorting, selection with Shift ranges, loading, custom cells; `sortRows`) ·
45
+ Pagination · DataList · Stat · Stats · Timeline · TreeView · Editable (edit in place) · Attachments (tiles or a list,
46
+ click-to-enlarge images) · Charts (AreaChart, BarChart, DonutChart, Sparkline; d3 for the maths)
47
+ - Text and numbers: FormatNumber · FormatBytes · FormatRelative (and `formatNumber`, `formatBytes`,
48
+ `formatRelative`) · RollingNumber · Divider · Kbd · Prose (rich text as the Editor shows it) · Highlight (search matches)
49
+ - Feedback: Alert · Banner · Progress · Skeleton · Spinner · EmptyState
50
+ - Inputs: Input · Textarea · Editor (rich text on TipTap, with image uploads) · NumberInput · PasswordInput · PinInput · TagsInput ·
51
+ Slider · FileUpload
52
+ - Pickers: TimeInput · NativeSelect · SwatchPicker · Select (one component: searchable, server `load`, `onCreate`,
53
+ multiple, tags; Combobox's parts for building one by hand) · DatePicker
54
+ - Choices: Checkbox · CheckboxGroup · Switch · RadioGroup · Segmented ·
55
+ ThemeToggle (`colorScheme`, `useColorScheme`)
56
+ - Forms: Field · Fields
57
+ - Overlays: Tooltip · Popover · HoverCard · Dialog · Sheet (both: props or
58
+ composed from Header/Body/Footer) · ActionBar · Menu · ContextMenu (Menu.Item,
59
+ Separator, Group, Checkbox, RadioGroup/Radio, Submenu; also as MenuItem…) ·
60
+ Toaster (`toaster.success(…)` from anywhere) · `confirm()` (`await confirm({ … })`
61
+ from anywhere, no state or markup)
62
+ - Scheduling and ordering (React only): EventCalendar (day, n days, work week or
63
+ week by the hour, with resources such as vets or rooms as columns; drag to
64
+ select a time, drag or arrow-key events to move and resize them, all-day
65
+ events, a date jump) · SortableList (drag or keyboard, on dnd-kit)
66
+ - Disclosure: Tabs/Tab (with counts and dots, inline or in the corner) · Accordion · Collapsible
67
+ - Navigation: AppShell (flat or inset; the sidebar collapses to a rail with ⌘B
68
+ and is a drawer on phones) · Topbar · Sidebar (NavSection, NavGroup, NavItem,
69
+ SidebarUser; also Sidebar.Section…) · Page · Breadcrumbs · Command (⌘K)
70
+
71
+ That's all of svelte-ui's components.
72
+
73
+ The playground runs on the library's own AppShell, Sidebar, Topbar, Page,
74
+ Breadcrumbs and Command, with an Appearance panel (accent, font, radius,
75
+ density, layout), Theming and Installation guides, and blocks: whole pages
76
+ (dashboard, students, profile, inbox, settings, sign-in) and sections.
77
+
78
+ ## Locale
79
+
80
+ Wrap the app in `<LocaleProvider locale="fr-FR">` and formatting, DatePicker,
81
+ EventCalendar, Progress, Slider and the charts follow it (each still takes its
82
+ own `locale`). `useLocale()` gives it to your own code.
83
+
84
+ The components' own words (placeholders, empty states, "Showing 1–10 of 95",
85
+ screen-reader labels, Zag's included) follow it too: English and French are
86
+ built in, in `src/format/messages.ts`. Change any of them with `messages`,
87
+ merged over the built-in list:
88
+
89
+ <LocaleProvider locale="fr-FR" messages={{ table: { empty: 'Aucun patient' } }}>
90
+
91
+ For another language, give a whole list (start from the exported `en`). A
92
+ prop given to a component (`placeholder`, `emptyText`, EventCalendar's
93
+ `translations`) still wins. `confirm()`, which renders in its own root, uses
94
+ the words of the app's provider.
95
+
96
+ ## Every component works the same way
97
+
98
+ - One package and namespace: `import { Button, Select } from '@milktop/react-ui'`.
99
+ - Values: `value` (or `checked`, `open`) to control it, `defaultValue` to
100
+ start it, and `onValueChange` (or `onCheckedChange`, `onOpenChange`) called
101
+ with the new value itself, not Ark's details object.
102
+ - `className`: classes for the outer element.
103
+ - `classes`: classes per part, keyed by camelCased part name
104
+ (`classes={{ itemText: 'font-mono' }}`).
105
+ - Slots replace a part's content: a node, or a function given its state
106
+ (`indicator={({ indeterminate }) => …}`, `renderItem={(item) => …}`).
107
+ - `style={{ '--ui-accent': … }}`: tokens for one instance; on `:root` for the whole app.
108
+ - Typed: written in TypeScript, shipped as JS with `.d.ts` types, so TSX apps
109
+ get checked props (Select's value is the type of your items' `valueKey`)
110
+ and JSX apps get autocomplete, with no errors either way.
111
+ - Other props go to the natural element (the button, the input, Ark's root).
112
+
113
+ Parts carry Zag's `data-scope` and `data-part` attributes, which the CSS
114
+ targets, all in `@layer ui`. So your classes and Tailwind's utilities always
115
+ win without `!important`. Part names are Ark's, so they match what you'd
116
+ write by hand (a Field's hint is `helper-text`, its error `error-text`).
117
+
118
+ ## Setup
119
+
120
+ Components import their own CSS. With Tailwind, declare the layer order at
121
+ the top of your app's CSS, so its reset stays below the library and its
122
+ utilities above:
123
+
124
+ ```css
125
+ @layer theme, base, ui, components, utilities;
126
+ @import "tailwindcss";
127
+ ```
128
+
129
+ Theme it by setting tokens (see `src/theme.css`, shared with svelte-ui);
130
+ dark values apply under `html.dark`:
131
+
132
+ ```css
133
+ :root { --ui-accent: #e11d48; --ui-radius: 10px; }
134
+ ```
135
+
136
+ ### React Server Components
137
+
138
+ Not supported yet, and not needed in Vite apps. A server component can't
139
+ read `Checkbox.Root` off a client component; when that's needed, the parts
140
+ will also be exported by name (`CheckboxRoot`…), and the files will get
141
+ `'use client'`.
142
+
143
+ ## Develop
144
+
145
+ ```sh
146
+ npm install
147
+ npm run dev # the playground, http://localhost:5194
148
+ npm run check # types (tsc) and React's hook rules
149
+ npm run build # the playground as a static site, in playground/dist
150
+ npm run build:lib # the package: src/ to dist/ (JS, .d.ts, CSS); runs before npm publish
151
+ ```
152
+
153
+ Releasing (logged in with `npm login`, on a clean main): each runs the checks
154
+ and the build, bumps the version, commits and tags it, pushes with the tag,
155
+ then publishes.
156
+
157
+ ```sh
158
+ npm run release:patch # 0.1.0 → 0.1.1
159
+ npm run release:minor # 0.1.0 → 0.2.0
160
+ npm run release:major # 0.1.0 → 1.0.0
161
+ npm run release:beta # 0.1.0 → 0.1.1-beta.0, published under the beta tag
162
+ ```
163
+
164
+ Each playground page shows its component's doc comment, and its examples are
165
+ the files in `playground/examples/<component>/`, shown with their code. The
166
+ playground compiles the TSX source directly, so it never needs `build:lib`.
167
+
168
+ ## Deploy the playground
169
+
170
+ `npm run build` writes a static, client-side site to `playground/dist`, set
171
+ up for **Cloudflare Pages** just like svelte-ui: framework preset None, build
172
+ command `npm run build`, output directory `playground/dist`, Node 22 from
173
+ `.nvmrc`. The site routes on the URL hash (no SPA fallback needed) and is kept
174
+ out of search results by a `noindex` meta tag and `playground/public/_headers`.
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Sections behind headings that expand them.
3
+ *
4
+ * <Accordion value={open} onValueChange={setOpen}>
5
+ * <Accordion.Item value="pricing" heading="How much are lessons?">…</Accordion.Item>
6
+ * <Accordion.Item value="cancel" heading="Can I cancel?">…</Accordion.Item>
7
+ * </Accordion>
8
+ *
9
+ * <Accordion items={[{ value: 'pricing', heading: 'How much are lessons?', content: '…' }]} />
10
+ *
11
+ * Arrow keys move between headings, Home/End jump to the ends.
12
+ *
13
+ * - `value`, `defaultValue`: the open item's value (or null), or an array of
14
+ * them with `multiple`; `onValueChange`: called with the new one
15
+ * - `multiple`: several open at once
16
+ * - `collapsible`: the open item can be closed again (default true)
17
+ * - `items`: `{ value, heading, content, disabled }`, before any children
18
+ * - Accordion.Item (also AccordionItem): `value`, `heading` (text or
19
+ * markup), `disabled`; its content is what it expands
20
+ *
21
+ * By hand, Ark's parts are here too: Accordion.Root, ItemTrigger,
22
+ * ItemIndicator, ItemContent… (Accordion.Item is ours; Ark's is
23
+ * Accordion.RawItem).
24
+ */
25
+ import { type ReactNode } from 'react';
26
+ import { Accordion as Ark } from '@ark-ui/react/accordion';
27
+ import 'iconify-icon';
28
+ import { type Classes } from '../utils.js';
29
+ import '../theme.css';
30
+ import './accordion.css';
31
+ type Value = string | number;
32
+ export type AccordionEntry = {
33
+ value: Value;
34
+ heading: ReactNode;
35
+ content?: ReactNode;
36
+ disabled?: boolean;
37
+ };
38
+ export type AccordionProps<V extends Value = Value> = Omit<Ark.RootProps, 'value' | 'defaultValue' | 'onValueChange' | 'multiple' | 'collapsible'> & {
39
+ value?: V | V[] | null;
40
+ defaultValue?: V | V[] | null;
41
+ onValueChange?: (value: V | V[] | null) => void;
42
+ multiple?: boolean;
43
+ collapsible?: boolean;
44
+ items?: AccordionEntry[] | null;
45
+ classes?: Classes<'root' | 'item' | 'heading' | 'itemTrigger' | 'headingText' | 'itemIndicator' | 'itemContent' | 'inner'>;
46
+ };
47
+ declare function AccordionShortcut<V extends Value = Value>({ value, defaultValue, onValueChange, multiple, collapsible, items, className, classes, children, ...rest }: AccordionProps<V>): import("react").JSX.Element;
48
+ export type AccordionItemProps = Omit<Ark.ItemProps, 'value'> & {
49
+ value: Value;
50
+ heading?: ReactNode;
51
+ };
52
+ /** One section of an Accordion: a heading button and the content it expands. */
53
+ export declare function AccordionItem({ value, heading, disabled, className, children, ...rest }: AccordionItemProps): import("react").JSX.Element;
54
+ export declare const Accordion: typeof AccordionShortcut & {
55
+ Item: typeof AccordionItem;
56
+ Root: import("react").ForwardRefExoticComponent<Ark.RootProps & import("react").RefAttributes<HTMLDivElement>>;
57
+ RootProvider: import("react").ForwardRefExoticComponent<Ark.RootProviderProps & import("react").RefAttributes<HTMLDivElement>>;
58
+ RawItem: import("react").ForwardRefExoticComponent<Ark.ItemProps & import("react").RefAttributes<HTMLDivElement>>;
59
+ ItemTrigger: import("react").ForwardRefExoticComponent<Ark.ItemTriggerProps & import("react").RefAttributes<HTMLButtonElement>>;
60
+ ItemIndicator: import("react").ForwardRefExoticComponent<Ark.ItemIndicatorProps & import("react").RefAttributes<HTMLDivElement>>;
61
+ ItemContent: import("react").ForwardRefExoticComponent<Ark.ItemContentProps & import("react").RefAttributes<HTMLDivElement>>;
62
+ Context: (props: Ark.ContextProps) => ReactNode;
63
+ ItemContext: (props: Ark.ItemContextProps) => ReactNode;
64
+ };
65
+ export {};
@@ -0,0 +1,61 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * Sections behind headings that expand them.
4
+ *
5
+ * <Accordion value={open} onValueChange={setOpen}>
6
+ * <Accordion.Item value="pricing" heading="How much are lessons?">…</Accordion.Item>
7
+ * <Accordion.Item value="cancel" heading="Can I cancel?">…</Accordion.Item>
8
+ * </Accordion>
9
+ *
10
+ * <Accordion items={[{ value: 'pricing', heading: 'How much are lessons?', content: '…' }]} />
11
+ *
12
+ * Arrow keys move between headings, Home/End jump to the ends.
13
+ *
14
+ * - `value`, `defaultValue`: the open item's value (or null), or an array of
15
+ * them with `multiple`; `onValueChange`: called with the new one
16
+ * - `multiple`: several open at once
17
+ * - `collapsible`: the open item can be closed again (default true)
18
+ * - `items`: `{ value, heading, content, disabled }`, before any children
19
+ * - Accordion.Item (also AccordionItem): `value`, `heading` (text or
20
+ * markup), `disabled`; its content is what it expands
21
+ *
22
+ * By hand, Ark's parts are here too: Accordion.Root, ItemTrigger,
23
+ * ItemIndicator, ItemContent… (Accordion.Item is ours; Ark's is
24
+ * Accordion.RawItem).
25
+ */
26
+ import { createContext, useContext, useRef } from 'react';
27
+ import { Accordion as Ark } from '@ark-ui/react/accordion';
28
+ import 'iconify-icon';
29
+ import { cx, partsOf, useControllable, withParts } from '../utils.js';
30
+ import '../theme.css';
31
+ import './accordion.css';
32
+ // The items register their values (numbers stay numbers) by string key.
33
+ const AccordionContext = createContext(null);
34
+ const keys = (value) => [].concat(value ?? []).map(String);
35
+ function AccordionShortcut({ value, defaultValue = null, onValueChange, multiple = false, collapsible = true, items = null, className, classes, children, ...rest }) {
36
+ const part = partsOf('accordion', classes);
37
+ const values = useRef(new Map());
38
+ const [current, setCurrent] = useControllable(value, defaultValue, onValueChange);
39
+ return (_jsx(AccordionContext, { value: { part, remember: (v) => { values.current.set(String(v), v); } }, children: _jsxs(Ark.Root, { ...rest, ...part('root'), className: cx(className, classes?.root), multiple: multiple, collapsible: multiple || collapsible, value: keys(current), onValueChange: (details) => {
40
+ const picked = details.value.map((key) => (values.current.get(key) ?? key));
41
+ setCurrent(multiple ? picked : (picked[0] ?? null));
42
+ }, children: [(items ?? []).map((item) => (_jsx(AccordionItem, { value: item.value, heading: item.heading, disabled: item.disabled, children: item.content }, item.value))), children] }) }));
43
+ }
44
+ /** One section of an Accordion: a heading button and the content it expands. */
45
+ export function AccordionItem({ value, heading = null, disabled = false, className, children, ...rest }) {
46
+ const accordion = useContext(AccordionContext);
47
+ const part = accordion?.part ?? partsOf('accordion');
48
+ accordion?.remember(value);
49
+ return (_jsxs(Ark.Item, { ...rest, ...part('item'), className: cx(className, part('item').className), value: String(value), disabled: disabled, children: [_jsx("h3", { ...part('heading'), children: _jsxs(Ark.ItemTrigger, { ...part('item-trigger'), children: [_jsx("span", { ...part('heading-text'), children: heading }), _jsx(Ark.ItemIndicator, { ...part('item-indicator'), children: _jsx("iconify-icon", { icon: "lucide:chevron-down" }) })] }) }), _jsx(Ark.ItemContent, { ...part('item-content'), children: _jsx("div", { ...part('inner'), children: children }) })] }));
50
+ }
51
+ export const Accordion = withParts(AccordionShortcut, {
52
+ Item: AccordionItem,
53
+ Root: Ark.Root,
54
+ RootProvider: Ark.RootProvider,
55
+ RawItem: Ark.Item,
56
+ ItemTrigger: Ark.ItemTrigger,
57
+ ItemIndicator: Ark.ItemIndicator,
58
+ ItemContent: Ark.ItemContent,
59
+ Context: Ark.Context,
60
+ ItemContext: Ark.ItemContext,
61
+ });
@@ -0,0 +1,17 @@
1
+ @layer ui {
2
+ [data-scope=accordion][data-part=root] { display: block; width: 100%; color: var(--ui-text); font-family: var(--ui-font); border-top: 1px solid var(--ui-border); }
3
+ [data-scope=accordion][data-part=item] { display: block; border-bottom: 1px solid var(--ui-border); }
4
+ [data-scope=accordion][data-part=heading] { margin: 0; font-size: inherit; font-weight: inherit; }
5
+ [data-scope=accordion][data-part=item-trigger] {
6
+ display: flex; align-items: center; justify-content: space-between; gap: 1rem; width: 100%; padding: 1rem 0;
7
+ border: none; background: transparent; color: inherit; font: inherit; font-size: 14px; font-weight: 500; text-align: left; cursor: pointer;
8
+ &:hover { text-decoration: underline; }
9
+ &:focus-visible { outline: 2px solid var(--ui-ring-soft); outline-offset: 2px; border-radius: 4px; }
10
+ &:disabled { opacity: 0.5; cursor: not-allowed; text-decoration: none; }
11
+ }
12
+ [data-scope=accordion][data-part=item-indicator] { display: inline-flex; color: var(--ui-muted); font-size: 16px; transition: transform 200ms;
13
+ &[data-state=open] { transform: rotate(180deg); } }
14
+ [data-scope=accordion][data-part=item-content] { &[data-state=open] { animation: ui-accordion-in 180ms ease-out; } }
15
+ [data-scope=accordion][data-part=inner] { padding-bottom: 1rem; color: var(--ui-muted); font-size: 14px; }
16
+ @keyframes ui-accordion-in { from { opacity: 0; transform: translateY(-4px); } }
17
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * A small toolbar floating at the bottom of the screen while something is
3
+ * selected, e.g. table rows.
4
+ *
5
+ * <ActionBar open={picked.length > 0} onClose={() => setPicked([])} selection={`${picked.length} selected`}>
6
+ * <Button size="sm">Message</Button>
7
+ * <Button size="sm" variant="danger">Archive</Button>
8
+ * </ActionBar>
9
+ *
10
+ * It isn't modal: the page stays usable (keep ticking rows) and focus isn't
11
+ * moved. It's rendered at the end of <body>, a toolbar for assistive tech,
12
+ * centred over the main area beside an AppShell's sidebar.
13
+ *
14
+ * - `open`: shows it
15
+ * - `selection`: the count or summary on the left
16
+ * - `closable`: false hides the close button; it (and Escape inside the bar)
17
+ * calls `onClose`, where you'd clear the selection
18
+ * - `label`: the toolbar's accessible name
19
+ */
20
+ import { type ComponentProps, type ReactNode } from 'react';
21
+ import 'iconify-icon';
22
+ import { type Classes } from '../utils.js';
23
+ import '../theme.css';
24
+ import './action-bar.css';
25
+ export type ActionBarProps = ComponentProps<'div'> & {
26
+ open?: boolean;
27
+ closable?: boolean;
28
+ label?: string;
29
+ onClose?: (() => void) | null;
30
+ selection?: ReactNode;
31
+ classes?: Classes<'positioner' | 'content' | 'selection' | 'separator' | 'actions' | 'close'>;
32
+ };
33
+ export declare function ActionBar({ open, closable, label: labelProp, onClose, selection, className, classes, children, ...rest }: ActionBarProps): import("react").JSX.Element | null;
@@ -0,0 +1,48 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * A small toolbar floating at the bottom of the screen while something is
4
+ * selected, e.g. table rows.
5
+ *
6
+ * <ActionBar open={picked.length > 0} onClose={() => setPicked([])} selection={`${picked.length} selected`}>
7
+ * <Button size="sm">Message</Button>
8
+ * <Button size="sm" variant="danger">Archive</Button>
9
+ * </ActionBar>
10
+ *
11
+ * It isn't modal: the page stays usable (keep ticking rows) and focus isn't
12
+ * moved. It's rendered at the end of <body>, a toolbar for assistive tech,
13
+ * centred over the main area beside an AppShell's sidebar.
14
+ *
15
+ * - `open`: shows it
16
+ * - `selection`: the count or summary on the left
17
+ * - `closable`: false hides the close button; it (and Escape inside the bar)
18
+ * calls `onClose`, where you'd clear the selection
19
+ * - `label`: the toolbar's accessible name
20
+ */
21
+ import { useEffect, useState } from 'react';
22
+ import { Portal } from '@ark-ui/react/portal';
23
+ import 'iconify-icon';
24
+ import { cx, partsOf } from '../utils.js';
25
+ import { useMessages } from '../format/LocaleProvider.js';
26
+ import '../theme.css';
27
+ import './action-bar.css';
28
+ export function ActionBar({ open = false, closable = true, label: labelProp, onClose = null, selection = null, className, classes, children, ...rest }) {
29
+ const t = useMessages();
30
+ const label = labelProp ?? t.actionBar.label;
31
+ const part = partsOf('action-bar', classes);
32
+ // Kept in the page until its closing animation ends (or 300 ms, if none
33
+ // runs, as with reduced motion).
34
+ const [present, setPresent] = useState(open);
35
+ if (open && !present)
36
+ setPresent(true);
37
+ useEffect(() => {
38
+ if (open || !present)
39
+ return;
40
+ const timer = setTimeout(() => setPresent(false), 300);
41
+ return () => clearTimeout(timer);
42
+ }, [open, present]);
43
+ if (!present)
44
+ return null;
45
+ return (_jsx(Portal, { children: _jsx("div", { ...part('positioner'), children: _jsxs("div", { ...rest, ...part('content'), className: cx(className, classes?.content), role: "toolbar", "aria-label": label, "data-state": open ? 'open' : 'closed', onAnimationEnd: (e) => { if (e.target === e.currentTarget && !open)
46
+ setPresent(false); }, onKeyDown: (e) => { if (e.key === 'Escape')
47
+ onClose?.(); }, children: [selection != null && _jsxs(_Fragment, { children: [_jsx("div", { ...part('selection'), children: selection }), _jsx("span", { ...part('separator'), "aria-hidden": "true" })] }), _jsx("div", { ...part('actions'), children: children }), closable && _jsx("button", { ...part('close'), type: "button", "aria-label": t.common.close, onClick: () => onClose?.(), children: _jsx("iconify-icon", { icon: "lucide:x" }) })] }) }) }));
48
+ }
@@ -0,0 +1,22 @@
1
+ @layer ui {
2
+ /* Centred along the bottom (over the main area, beside AppShell's sidebar); only the bar takes clicks. */
3
+ [data-scope=action-bar][data-part=positioner] { position: fixed; left: var(--ui-main-left, 0px); right: 0; bottom: 1rem; z-index: 90;
4
+ display: flex; justify-content: center; padding: 0 1rem; pointer-events: none; transition: left 200ms ease;
5
+ @media (min-width: 768px) { bottom: 1.5rem; } }
6
+ [data-scope=action-bar][data-part=content] {
7
+ display: flex; align-items: center; gap: 0.5rem; max-width: 100%; min-height: 2.75rem; padding: 0.375rem 0.375rem 0.375rem 1rem; box-sizing: border-box;
8
+ background: var(--ui-surface); color: var(--ui-text); font-family: var(--ui-font); font-size: 14px;
9
+ border: 1px solid var(--ui-border); border-radius: calc(var(--ui-radius) + 6px); box-shadow: var(--ui-shadow); pointer-events: auto;
10
+ &[data-state=open] { animation: ui-action-bar-in 180ms cubic-bezier(0.32, 0.72, 0, 1); }
11
+ &[data-state=closed] { animation: ui-action-bar-out 140ms ease-in forwards; }
12
+ }
13
+ [data-scope=action-bar][data-part=selection] { font-weight: 500; white-space: nowrap; }
14
+ [data-scope=action-bar][data-part=separator] { width: 1px; height: 1.25rem; background: var(--ui-border); flex-shrink: 0; }
15
+ [data-scope=action-bar][data-part=actions] { display: flex; align-items: center; gap: 0.375rem; flex-wrap: wrap; }
16
+ [data-scope=action-bar][data-part=close] { display: grid; place-items: center; width: 2rem; height: 2rem; flex-shrink: 0;
17
+ border: none; border-radius: var(--ui-radius); background: transparent; color: var(--ui-muted); cursor: pointer; font-size: 14px;
18
+ &:hover { background: var(--ui-hover); color: var(--ui-text); }
19
+ &:focus-visible { outline: 2px solid var(--ui-ring-soft); } }
20
+ @keyframes ui-action-bar-in { from { opacity: 0; transform: translateY(12px); } }
21
+ @keyframes ui-action-bar-out { to { opacity: 0; transform: translateY(12px); } }
22
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * A message in the page (not a popup; see Toast for those).
3
+ *
4
+ * <Alert variant="warning" heading="Payment overdue">Ada's invoice is 5 days late.</Alert>
5
+ *
6
+ * - `variant`: 'info' (default), 'success', 'warning' or 'danger'
7
+ * - `heading`: a bold first line; the content is the message
8
+ * - `icon`: an Iconify name in place of the variant's, or false for none
9
+ * - `onDismiss`: shows a close button that calls it (hide the alert in response)
10
+ * - `actions`: buttons under the message
11
+ *
12
+ * Danger and warning alerts are role=alert, so assistive tech announces them
13
+ * when they appear; the others are role=status.
14
+ */
15
+ import type { ComponentProps, ReactNode } from 'react';
16
+ import 'iconify-icon';
17
+ import { type Classes } from '../utils.js';
18
+ import '../theme.css';
19
+ import './alert.css';
20
+ export type AlertProps = ComponentProps<'div'> & {
21
+ variant?: 'info' | 'success' | 'warning' | 'danger';
22
+ heading?: ReactNode;
23
+ icon?: string | boolean;
24
+ onDismiss?: (() => void) | null;
25
+ actions?: ReactNode;
26
+ classes?: Classes<'root' | 'icon' | 'text' | 'heading' | 'description' | 'actions' | 'close'>;
27
+ };
28
+ export declare function Alert({ variant, heading, icon, onDismiss, actions, className, classes, children, ...rest }: AlertProps): import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import 'iconify-icon';
3
+ import { cx, partsOf } from '../utils.js';
4
+ import { useMessages } from '../format/LocaleProvider.js';
5
+ import '../theme.css';
6
+ import './alert.css';
7
+ const icons = { info: 'lucide:info', success: 'lucide:circle-check', warning: 'lucide:triangle-alert', danger: 'lucide:circle-alert' };
8
+ export function Alert({ variant = 'info', heading = null, icon = true, onDismiss = null, actions = null, className, classes, children, ...rest }) {
9
+ const t = useMessages();
10
+ const part = partsOf('alert', classes);
11
+ return (_jsxs("div", { ...rest, ...part('root'), className: cx(className, classes?.root), "data-variant": variant, role: variant === 'danger' || variant === 'warning' ? 'alert' : 'status', children: [icon && _jsx("iconify-icon", { ...part('icon'), icon: icon === true ? icons[variant] ?? icons.info : icon, "aria-hidden": "true" }), _jsxs("div", { ...part('text'), children: [heading && _jsx("div", { ...part('heading'), children: heading }), children != null && _jsx("div", { ...part('description'), children: children }), actions && _jsx("div", { ...part('actions'), children: actions })] }), onDismiss && (_jsx("button", { ...part('close'), type: "button", "aria-label": t.common.dismiss, onClick: onDismiss, children: _jsx("iconify-icon", { icon: "lucide:x" }) }))] }));
12
+ }
@@ -0,0 +1,26 @@
1
+ @layer ui {
2
+ [data-scope=alert][data-part=root] {
3
+ display: flex; align-items: flex-start; gap: 0.75rem; padding: 1rem; box-sizing: border-box;
4
+ border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface);
5
+ color: var(--ui-text); font-family: var(--ui-font); font-size: 14px; line-height: 1.45;
6
+ --tint: var(--ui-text);
7
+ &[data-variant=info] { background: var(--ui-accent-soft); border-color: transparent; --tint: var(--ui-accent-soft-text); }
8
+ &[data-variant=success] { background: #f0fdf4; border-color: #bbf7d0; --tint: #166534; }
9
+ &[data-variant=warning] { background: #fffbeb; border-color: #fde68a; --tint: #92400e; }
10
+ &[data-variant=danger] { background: #fef2f2; border-color: #fecaca; --tint: #991b1b; }
11
+ :is(.dark, [data-theme=dark]) &[data-variant=success] { background: #052e16; border-color: #14532d; --tint: #86efac; }
12
+ :is(.dark, [data-theme=dark]) &[data-variant=warning] { background: #1c1004; border-color: #451a03; --tint: #fcd34d; }
13
+ :is(.dark, [data-theme=dark]) &[data-variant=danger] { background: #1f0505; border-color: #450a0a; --tint: #fca5a5; }
14
+ }
15
+ [data-scope=alert][data-part=icon] { flex-shrink: 0; margin-top: 1px; font-size: 18px; color: var(--tint); }
16
+ [data-scope=alert][data-part=text] { flex: 1; min-width: 0; }
17
+ [data-scope=alert][data-part=heading] { font-weight: 600; margin-bottom: 0.125rem; color: var(--tint); }
18
+ [data-scope=alert][data-part=description] { color: var(--ui-muted); }
19
+ [data-scope=alert][data-part=actions] { display: flex; flex-wrap: wrap; gap: 0.5rem; margin-top: 0.75rem; }
20
+ [data-scope=alert][data-part=close] {
21
+ display: grid; place-items: center; flex-shrink: 0; width: 1.5rem; height: 1.5rem; margin: -0.25rem -0.25rem 0 0;
22
+ border: none; background: transparent; border-radius: 4px; color: var(--ui-muted); cursor: pointer; font-size: 14px;
23
+ &:hover { background: rgba(0, 0, 0, 0.06); color: var(--ui-text); }
24
+ &:focus-visible { outline: 2px solid var(--ui-ring-soft); }
25
+ }
26
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * An app layout: the sidebar down the left, the top bar and page beside it.
3
+ * Put a Sidebar, a Topbar and a Page inside; each takes its place.
4
+ *
5
+ * <AppShell inset persist="app:sidebar">
6
+ * <Sidebar logo="TutorApp">…</Sidebar>
7
+ * <Topbar end={<Button icon="lucide:bell" aria-label="Notifications" />}>…</Topbar>
8
+ * <Page heading="Students">…</Page>
9
+ * </AppShell>
10
+ *
11
+ * - `inset`: the page as a rounded panel on a frame, with a grey sidebar
12
+ * whose hovered and current items go white (the `--ui-sidebar-inset-*` tokens)
13
+ * - `collapsed`, `defaultCollapsed`; `onCollapsedChange`: the sidebar as an
14
+ * icon rail
15
+ * - `persist`: a localStorage key to remember `collapsed`
16
+ * - `breakpoint`: below this width (768px) the sidebar is a drawer, opened
17
+ * from the top bar's menu button
18
+ * - `shortcut`: false to leave ⌘B alone (for a second shell on the page)
19
+ *
20
+ * ⌘B (Ctrl+B) collapses the sidebar, or opens the drawer on phones; Escape
21
+ * closes the drawer. Parts inside read the shell with `useShell()`.
22
+ */
23
+ import { type HTMLAttributes } from 'react';
24
+ import { type Classes } from '../utils.js';
25
+ import '../theme.css';
26
+ import './app-shell.css';
27
+ export type AppShellProps = HTMLAttributes<HTMLDivElement> & {
28
+ inset?: boolean;
29
+ collapsed?: boolean;
30
+ defaultCollapsed?: boolean;
31
+ onCollapsedChange?: (collapsed: boolean) => void;
32
+ persist?: string | null;
33
+ breakpoint?: number;
34
+ shortcut?: boolean;
35
+ classes?: Classes<'root' | 'backdrop'>;
36
+ };
37
+ export declare function AppShell({ inset, collapsed: collapsedProp, defaultCollapsed, onCollapsedChange, persist, breakpoint, shortcut, className, classes, children, ...rest }: AppShellProps): import("react").JSX.Element;
38
+ export { useShell } from './shell.js';
@@ -0,0 +1,92 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * An app layout: the sidebar down the left, the top bar and page beside it.
4
+ * Put a Sidebar, a Topbar and a Page inside; each takes its place.
5
+ *
6
+ * <AppShell inset persist="app:sidebar">
7
+ * <Sidebar logo="TutorApp">…</Sidebar>
8
+ * <Topbar end={<Button icon="lucide:bell" aria-label="Notifications" />}>…</Topbar>
9
+ * <Page heading="Students">…</Page>
10
+ * </AppShell>
11
+ *
12
+ * - `inset`: the page as a rounded panel on a frame, with a grey sidebar
13
+ * whose hovered and current items go white (the `--ui-sidebar-inset-*` tokens)
14
+ * - `collapsed`, `defaultCollapsed`; `onCollapsedChange`: the sidebar as an
15
+ * icon rail
16
+ * - `persist`: a localStorage key to remember `collapsed`
17
+ * - `breakpoint`: below this width (768px) the sidebar is a drawer, opened
18
+ * from the top bar's menu button
19
+ * - `shortcut`: false to leave ⌘B alone (for a second shell on the page)
20
+ *
21
+ * ⌘B (Ctrl+B) collapses the sidebar, or opens the drawer on phones; Escape
22
+ * closes the drawer. Parts inside read the shell with `useShell()`.
23
+ */
24
+ import { useCallback, useEffect, useMemo, useState, useSyncExternalStore } from 'react';
25
+ import { cx, partsOf, useControllable } from '../utils.js';
26
+ import { ShellContext } from './shell.js';
27
+ import '../theme.css';
28
+ import './app-shell.css';
29
+ function saved(key) {
30
+ try {
31
+ const value = key ? localStorage.getItem(key) : null;
32
+ return value === null ? null : value === 'true';
33
+ }
34
+ catch {
35
+ return null;
36
+ }
37
+ }
38
+ // Whether the window is narrower than the breakpoint, kept current.
39
+ function useNarrow(breakpoint) {
40
+ const query = `(max-width: ${breakpoint - 1}px)`;
41
+ const subscribe = useCallback((change) => {
42
+ const list = matchMedia(query);
43
+ list.addEventListener('change', change);
44
+ return () => list.removeEventListener('change', change);
45
+ }, [query]);
46
+ return useSyncExternalStore(subscribe, () => matchMedia(query).matches, () => false);
47
+ }
48
+ export function AppShell({ inset = false, collapsed: collapsedProp, defaultCollapsed = false, onCollapsedChange, persist = null, breakpoint = 768, shortcut = true, className, classes, children, ...rest }) {
49
+ const part = partsOf('app-shell', classes);
50
+ const [initial] = useState(() => saved(persist) ?? defaultCollapsed);
51
+ const [collapsed, setCollapsed] = useControllable(collapsedProp, initial, onCollapsedChange);
52
+ const mobile = useNarrow(breakpoint);
53
+ const [drawer, setDrawer] = useState(false);
54
+ // Widening the window past the breakpoint closes the drawer.
55
+ const drawerOpen = drawer && mobile;
56
+ const rail = collapsed && !mobile;
57
+ const toggle = useCallback(() => {
58
+ if (mobile)
59
+ return setDrawer(!drawerOpen);
60
+ setCollapsed(!collapsed);
61
+ try {
62
+ if (persist)
63
+ localStorage.setItem(persist, String(!collapsed));
64
+ }
65
+ catch { /* private mode */ }
66
+ }, [mobile, drawerOpen, collapsed, setCollapsed, persist]);
67
+ const shell = useMemo(() => ({ rail, mobile, drawerOpen, toggle, close: () => setDrawer(false) }), [rail, mobile, drawerOpen, toggle]);
68
+ // Where the main area starts, for things fixed to the viewport that centre
69
+ // over it (ActionBar).
70
+ useEffect(() => {
71
+ if (!shortcut)
72
+ return;
73
+ const left = mobile ? '0px' : rail ? 'var(--ui-sidebar-rail-width)' : 'var(--ui-sidebar-width)';
74
+ document.documentElement.style.setProperty('--ui-main-left', left);
75
+ return () => { document.documentElement.style.removeProperty('--ui-main-left'); };
76
+ }, [shortcut, mobile, rail]);
77
+ useEffect(() => {
78
+ function keydown(e) {
79
+ // Not in rich text editors, where it means bold.
80
+ if (shortcut && (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'b' && !e.target?.isContentEditable) {
81
+ e.preventDefault();
82
+ toggle();
83
+ }
84
+ else if (e.key === 'Escape' && drawerOpen)
85
+ setDrawer(false);
86
+ }
87
+ addEventListener('keydown', keydown);
88
+ return () => removeEventListener('keydown', keydown);
89
+ }, [shortcut, toggle, drawerOpen]);
90
+ return (_jsx(ShellContext, { value: shell, children: _jsxs("div", { ...rest, ...part('root'), className: cx(className, classes?.root), "data-inset": inset || undefined, "data-rail": rail || undefined, "data-mobile": mobile || undefined, children: [children, drawerOpen && _jsx("div", { ...part('backdrop'), role: "presentation", onClick: () => setDrawer(false) })] }) }));
91
+ }
92
+ export { useShell } from './shell.js';