@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.
- package/README.md +173 -2
- package/dist/accordion/Accordion.d.ts +65 -0
- package/dist/accordion/Accordion.js +61 -0
- package/dist/accordion/accordion.css +17 -0
- package/dist/action-bar/ActionBar.d.ts +33 -0
- package/dist/action-bar/ActionBar.js +48 -0
- package/dist/action-bar/action-bar.css +22 -0
- package/dist/alert/Alert.d.ts +28 -0
- package/dist/alert/Alert.js +12 -0
- package/dist/alert/alert.css +26 -0
- package/dist/app-shell/AppShell.d.ts +38 -0
- package/dist/app-shell/AppShell.js +92 -0
- package/dist/app-shell/Topbar.d.ts +20 -0
- package/dist/app-shell/Topbar.js +13 -0
- package/dist/app-shell/app-shell.css +41 -0
- package/dist/app-shell/shell.d.ts +9 -0
- package/dist/app-shell/shell.js +3 -0
- package/dist/attachments/Attachments.d.ts +84 -0
- package/dist/attachments/Attachments.js +235 -0
- package/dist/attachments/attachments.css +86 -0
- package/dist/avatar/Avatar.d.ts +31 -0
- package/dist/avatar/Avatar.js +41 -0
- package/dist/avatar/AvatarGroup.d.ts +41 -0
- package/dist/avatar/AvatarGroup.js +17 -0
- package/dist/avatar/avatar.css +73 -0
- package/dist/badge/Badge.d.ts +26 -0
- package/dist/badge/Badge.js +10 -0
- package/dist/badge/badge.css +24 -0
- package/dist/banner/Banner.d.ts +33 -0
- package/dist/banner/Banner.js +14 -0
- package/dist/banner/banner.css +25 -0
- package/dist/breadcrumbs/Breadcrumbs.d.ts +39 -0
- package/dist/breadcrumbs/Breadcrumbs.js +28 -0
- package/dist/breadcrumbs/breadcrumbs.css +18 -0
- package/dist/button/Button.d.ts +40 -0
- package/dist/button/Button.js +19 -0
- package/dist/button/button.css +63 -0
- package/dist/card/Card.d.ts +355 -0
- package/dist/card/Card.js +56 -0
- package/dist/card/card.css +43 -0
- package/dist/chart/AreaChart.d.ts +19 -0
- package/dist/chart/AreaChart.js +61 -0
- package/dist/chart/BarChart.d.ts +3 -0
- package/dist/chart/BarChart.js +60 -0
- package/dist/chart/ChartFrame.d.ts +25 -0
- package/dist/chart/ChartFrame.js +22 -0
- package/dist/chart/DonutChart.d.ts +20 -0
- package/dist/chart/DonutChart.js +41 -0
- package/dist/chart/Sparkline.d.ts +26 -0
- package/dist/chart/Sparkline.js +36 -0
- package/dist/chart/chart.css +51 -0
- package/dist/chart/chart.d.ts +23 -0
- package/dist/chart/chart.js +19 -0
- package/dist/checkbox/Checkbox.d.ts +50 -0
- package/dist/checkbox/Checkbox.js +19 -0
- package/dist/checkbox/CheckboxGroup.d.ts +44 -0
- package/dist/checkbox/CheckboxGroup.js +80 -0
- package/dist/checkbox/checkbox.css +49 -0
- package/dist/collapsible/Collapsible.d.ts +41 -0
- package/dist/collapsible/Collapsible.js +15 -0
- package/dist/collapsible/collapsible.css +20 -0
- package/dist/color-scheme.d.ts +18 -0
- package/dist/color-scheme.js +84 -0
- package/dist/command/Command.d.ts +64 -0
- package/dist/command/Command.js +162 -0
- package/dist/command/command.css +47 -0
- package/dist/confirm/confirm.css +5 -0
- package/dist/confirm/confirm.d.ts +42 -0
- package/dist/confirm/confirm.js +115 -0
- package/dist/copy-button/CopyButton.d.ts +30 -0
- package/dist/copy-button/CopyButton.js +23 -0
- package/dist/copy-button/copy-button.css +12 -0
- package/dist/css.d.ts +2 -0
- package/dist/data-list/DataList.d.ts +55 -0
- package/dist/data-list/DataList.js +43 -0
- package/dist/data-list/data-list.css +19 -0
- package/dist/date-picker/DatePicker.d.ts +95 -0
- package/dist/date-picker/DatePicker.js +237 -0
- package/dist/date-picker/date-picker.css +53 -0
- package/dist/dialog/Dialog.d.ts +98 -0
- package/dist/dialog/Dialog.js +92 -0
- package/dist/dialog/dialog.css +31 -0
- package/dist/divider/Divider.d.ts +27 -0
- package/dist/divider/Divider.js +8 -0
- package/dist/divider/divider.css +12 -0
- package/dist/editable/Editable.d.ts +47 -0
- package/dist/editable/Editable.js +51 -0
- package/dist/editable/editable.css +25 -0
- package/dist/editor/Editor.d.ts +59 -0
- package/dist/editor/Editor.js +220 -0
- package/dist/editor/editor.css +42 -0
- package/dist/editor/image.d.ts +1 -0
- package/dist/editor/image.js +23 -0
- package/dist/empty-state/EmptyState.d.ts +22 -0
- package/dist/empty-state/EmptyState.js +9 -0
- package/dist/empty-state/empty-state.css +14 -0
- package/dist/event-calendar/EventCalendar.d.ts +213 -0
- package/dist/event-calendar/EventCalendar.js +686 -0
- package/dist/event-calendar/dates.d.ts +8 -0
- package/dist/event-calendar/dates.js +21 -0
- package/dist/event-calendar/event-calendar.css +157 -0
- package/dist/field/Field.d.ts +66 -0
- package/dist/field/Field.js +23 -0
- package/dist/field/field.css +26 -0
- package/dist/file-upload/FileUpload.d.ts +60 -0
- package/dist/file-upload/FileUpload.js +102 -0
- package/dist/file-upload/file-upload.css +32 -0
- package/dist/format/FormatNumber.d.ts +42 -0
- package/dist/format/FormatNumber.js +59 -0
- package/dist/format/LocaleProvider.d.ts +33 -0
- package/dist/format/LocaleProvider.js +48 -0
- package/dist/format/format.d.ts +37 -0
- package/dist/format/format.js +74 -0
- package/dist/format/messages.d.ts +211 -0
- package/dist/format/messages.js +202 -0
- package/dist/highlight/Highlight.d.ts +11 -0
- package/dist/highlight/Highlight.js +14 -0
- package/dist/highlight/highlight.css +17 -0
- package/dist/hover-card/HoverCard.d.ts +47 -0
- package/dist/hover-card/HoverCard.js +18 -0
- package/dist/hover-card/hover-card.css +19 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +85 -0
- package/dist/input/Input.d.ts +41 -0
- package/dist/input/Input.js +48 -0
- package/dist/input/input.css +25 -0
- package/dist/kbd/Kbd.d.ts +19 -0
- package/dist/kbd/Kbd.js +20 -0
- package/dist/kbd/kbd.css +8 -0
- package/dist/menu/Menu.d.ts +223 -0
- package/dist/menu/Menu.js +275 -0
- package/dist/menu/menu.css +38 -0
- package/dist/native-select/NativeSelect.d.ts +36 -0
- package/dist/native-select/NativeSelect.js +51 -0
- package/dist/native-select/native-select.css +16 -0
- package/dist/number-input/NumberInput.d.ts +56 -0
- package/dist/number-input/NumberInput.js +67 -0
- package/dist/number-input/number-input.css +21 -0
- package/dist/page/Page.d.ts +30 -0
- package/dist/page/Page.js +9 -0
- package/dist/page/page.css +17 -0
- package/dist/pagination/Pagination.d.ts +31 -0
- package/dist/pagination/Pagination.js +58 -0
- package/dist/pagination/pagination.css +15 -0
- package/dist/password-input/PasswordInput.d.ts +47 -0
- package/dist/password-input/PasswordInput.js +35 -0
- package/dist/password-input/password-input.css +18 -0
- package/dist/pin-input/PinInput.d.ts +45 -0
- package/dist/pin-input/PinInput.js +22 -0
- package/dist/pin-input/pin-input.css +13 -0
- package/dist/popover/Popover.d.ts +65 -0
- package/dist/popover/Popover.js +26 -0
- package/dist/popover/popover.css +16 -0
- package/dist/progress/Progress.d.ts +56 -0
- package/dist/progress/Progress.js +28 -0
- package/dist/progress/progress.css +24 -0
- package/dist/prose/Prose.d.ts +23 -0
- package/dist/prose/Prose.js +10 -0
- package/dist/prose/prose.css +29 -0
- package/dist/radio-group/RadioGroup.d.ts +59 -0
- package/dist/radio-group/RadioGroup.js +37 -0
- package/dist/radio-group/radio-group.css +30 -0
- package/dist/rolling-number/RollingNumber.d.ts +27 -0
- package/dist/rolling-number/RollingNumber.js +18 -0
- package/dist/rolling-number/rolling-number.css +10 -0
- package/dist/segment-group/Segmented.d.ts +68 -0
- package/dist/segment-group/Segmented.js +79 -0
- package/dist/segment-group/segment-group.css +25 -0
- package/dist/select/ListSelect.d.ts +5 -0
- package/dist/select/ListSelect.js +82 -0
- package/dist/select/SearchSelect.d.ts +43 -0
- package/dist/select/SearchSelect.js +219 -0
- package/dist/select/Select.d.ts +147 -0
- package/dist/select/Select.js +32 -0
- package/dist/select/items.d.ts +17 -0
- package/dist/select/items.js +14 -0
- package/dist/select/search-select.css +45 -0
- package/dist/select/select.css +52 -0
- package/dist/sheet/Sheet.d.ts +88 -0
- package/dist/sheet/Sheet.js +66 -0
- package/dist/sheet/sheet.css +44 -0
- package/dist/sidebar/NavGroup.d.ts +29 -0
- package/dist/sidebar/NavGroup.js +78 -0
- package/dist/sidebar/NavItem.d.ts +33 -0
- package/dist/sidebar/NavItem.js +50 -0
- package/dist/sidebar/NavSection.d.ts +20 -0
- package/dist/sidebar/NavSection.js +44 -0
- package/dist/sidebar/Sidebar.d.ts +65 -0
- package/dist/sidebar/Sidebar.js +59 -0
- package/dist/sidebar/SidebarUser.d.ts +31 -0
- package/dist/sidebar/SidebarUser.js +14 -0
- package/dist/sidebar/context.d.ts +13 -0
- package/dist/sidebar/context.js +6 -0
- package/dist/sidebar/sidebar.css +109 -0
- package/dist/skeleton/Skeleton.d.ts +25 -0
- package/dist/skeleton/Skeleton.js +8 -0
- package/dist/skeleton/skeleton.css +13 -0
- package/dist/slider/Slider.d.ts +70 -0
- package/dist/slider/Slider.js +65 -0
- package/dist/slider/slider.css +17 -0
- package/dist/sortable-list/SortableList.d.ts +46 -0
- package/dist/sortable-list/SortableList.js +85 -0
- package/dist/sortable-list/sortable-list.css +25 -0
- package/dist/spinner/Spinner.d.ts +18 -0
- package/dist/spinner/Spinner.js +11 -0
- package/dist/spinner/spinner.css +9 -0
- package/dist/stat/Stat.d.ts +42 -0
- package/dist/stat/Stat.js +21 -0
- package/dist/stat/Stats.d.ts +23 -0
- package/dist/stat/Stats.js +8 -0
- package/dist/stat/stat.css +34 -0
- package/dist/swatch-picker/SwatchPicker.d.ts +45 -0
- package/dist/swatch-picker/SwatchPicker.js +54 -0
- package/dist/swatch-picker/swatch-picker.css +38 -0
- package/dist/switch/Switch.d.ts +39 -0
- package/dist/switch/Switch.js +17 -0
- package/dist/switch/switch.css +13 -0
- package/dist/table/Table.d.ts +90 -0
- package/dist/table/Table.js +145 -0
- package/dist/table/table.css +44 -0
- package/dist/tabs/Tabs.d.ts +87 -0
- package/dist/tabs/Tabs.js +90 -0
- package/dist/tabs/tabs.css +53 -0
- package/dist/tags-input/TagsInput.d.ts +69 -0
- package/dist/tags-input/TagsInput.js +95 -0
- package/dist/tags-input/tags-input.css +37 -0
- package/dist/textarea/Textarea.d.ts +24 -0
- package/dist/textarea/Textarea.js +51 -0
- package/dist/textarea/textarea.css +14 -0
- package/dist/theme-toggle/ThemeToggle.d.ts +49 -0
- package/dist/theme-toggle/ThemeToggle.js +71 -0
- package/dist/theme-toggle/theme-toggle.css +23 -0
- package/dist/theme.css +119 -0
- package/dist/time-input/TimeInput.d.ts +25 -0
- package/dist/time-input/TimeInput.js +20 -0
- package/dist/timeline/Timeline.d.ts +50 -0
- package/dist/timeline/Timeline.js +15 -0
- package/dist/timeline/timeline.css +29 -0
- package/dist/toast/Toaster.d.ts +36 -0
- package/dist/toast/Toaster.js +25 -0
- package/dist/toast/toast.css +33 -0
- package/dist/tooltip/Tooltip.d.ts +55 -0
- package/dist/tooltip/Tooltip.js +48 -0
- package/dist/tooltip/tooltip.css +18 -0
- package/dist/tree-view/TreeView.d.ts +86 -0
- package/dist/tree-view/TreeView.js +74 -0
- package/dist/tree-view/tree-view.css +29 -0
- package/dist/utils.d.ts +19 -0
- package/dist/utils.js +59 -0
- package/package.json +99 -4
package/README.md
CHANGED
|
@@ -1,3 +1,174 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @milktop/react-ui
|
|
2
2
|
|
|
3
|
-
|
|
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';
|