compote-ui 0.75.0 → 0.75.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/package.json +11 -11
- package/skills/component-usage/SKILL.md +13 -2
- package/skills/component-usage/references/component-export-map.md +20 -6
- package/skills/component-usage/references/files-and-images.md +49 -0
- package/skills/component-usage/references/forms-and-inputs.md +210 -16
- package/skills/component-usage/references/layout-and-display.md +245 -2
- package/skills/component-usage/references/overlays-and-floating-ui.md +198 -5
- package/skills/data-table/SKILL.md +1 -1
- package/skills/data-table/references/column-options.md +76 -2
- package/skills/getting-started/SKILL.md +1 -1
- package/skills/production-checklist/SKILL.md +1 -1
- package/skills/theming/SKILL.md +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "compote-ui",
|
|
3
|
-
"version": "0.75.
|
|
3
|
+
"version": "0.75.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"dev": "vite dev --open",
|
|
@@ -65,25 +65,25 @@
|
|
|
65
65
|
"@sveltejs/package": "^2.5.8",
|
|
66
66
|
"@sveltejs/vite-plugin-svelte": "^7.3.0",
|
|
67
67
|
"@tailwindcss/vite": "^4.3.3",
|
|
68
|
-
"@tanstack/intent": "^0.
|
|
69
|
-
"@tanstack/svelte-table": "9.2.
|
|
68
|
+
"@tanstack/intent": "^0.4.0",
|
|
69
|
+
"@tanstack/svelte-table": "9.2.4",
|
|
70
70
|
"@types/node": "^22.20.0",
|
|
71
|
-
"eslint": "^10.
|
|
71
|
+
"eslint": "^10.10.0",
|
|
72
72
|
"eslint-config-prettier": "^10.1.8",
|
|
73
73
|
"eslint-plugin-svelte": "^3.23.0",
|
|
74
|
-
"globals": "^17.
|
|
74
|
+
"globals": "^17.12.0",
|
|
75
75
|
"prettier": "^3.9.6",
|
|
76
76
|
"prettier-plugin-svelte": "^4.1.1",
|
|
77
77
|
"prettier-plugin-tailwindcss": "^0.8.1",
|
|
78
78
|
"publint": "^0.3.24",
|
|
79
|
-
"svelte": "^5.
|
|
79
|
+
"svelte": "^5.57.0",
|
|
80
80
|
"svelte-check": "^4.7.6",
|
|
81
81
|
"tailwindcss": "^4.3.3",
|
|
82
82
|
"tw-animate-css": "^1.4.0",
|
|
83
83
|
"typescript": "^6.0.3",
|
|
84
|
-
"typescript-eslint": "^8.
|
|
84
|
+
"typescript-eslint": "^8.70.0",
|
|
85
85
|
"unplugin-icons": "^23.0.1",
|
|
86
|
-
"vite": "^8.
|
|
86
|
+
"vite": "^8.3.0",
|
|
87
87
|
"vitest": "^5"
|
|
88
88
|
},
|
|
89
89
|
"keywords": [
|
|
@@ -91,10 +91,10 @@
|
|
|
91
91
|
"tanstack-intent"
|
|
92
92
|
],
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"@ark-ui/svelte": "^5.24.
|
|
94
|
+
"@ark-ui/svelte": "^5.24.1",
|
|
95
95
|
"@fontsource-variable/nunito-sans": "^5.2.7",
|
|
96
|
-
"@internationalized/date": "^3.12.
|
|
97
|
-
"@tanstack/svelte-virtual": "^3.13.
|
|
96
|
+
"@internationalized/date": "^3.12.4",
|
|
97
|
+
"@tanstack/svelte-virtual": "^3.13.37",
|
|
98
98
|
"runed": "^0.37.1",
|
|
99
99
|
"svelte-tel-input": "^4.3.2",
|
|
100
100
|
"tailwind-merge": "^3.6.0",
|
|
@@ -8,7 +8,7 @@ description: >
|
|
|
8
8
|
metadata:
|
|
9
9
|
type: core
|
|
10
10
|
library: compote-ui
|
|
11
|
-
library_version: '0.
|
|
11
|
+
library_version: '0.74.2'
|
|
12
12
|
sources:
|
|
13
13
|
- src/lib/index.ts
|
|
14
14
|
- src/lib/utils/collections.ts
|
|
@@ -19,6 +19,17 @@ sources:
|
|
|
19
19
|
- src/lib/components/file-upload/file-upload.svelte
|
|
20
20
|
- src/lib/components/image-cropper/image-cropper.svelte
|
|
21
21
|
- src/lib/utils/image-processing.ts
|
|
22
|
+
- src/lib/components/app-shell
|
|
23
|
+
- src/lib/components/nav-rail
|
|
24
|
+
- src/lib/components/badge/badge.svelte
|
|
25
|
+
- src/lib/components/table
|
|
26
|
+
- src/lib/components/qr-code/qr-code.svelte
|
|
27
|
+
- src/lib/components/progress
|
|
28
|
+
- src/lib/components/tags-input/tags-input.svelte
|
|
29
|
+
- src/lib/components/menu
|
|
30
|
+
- src/lib/components/popover
|
|
31
|
+
- src/lib/components/hover-card
|
|
32
|
+
- src/lib/components/tooltip
|
|
22
33
|
---
|
|
23
34
|
|
|
24
35
|
# Compote UI — Component Usage
|
|
@@ -189,7 +200,7 @@ Correct:
|
|
|
189
200
|
|
|
190
201
|
Icon-only controls need an accessible name.
|
|
191
202
|
|
|
192
|
-
Source: `
|
|
203
|
+
Source: `references/buttons.md`
|
|
193
204
|
|
|
194
205
|
## References
|
|
195
206
|
|
|
@@ -3,19 +3,23 @@
|
|
|
3
3
|
Root exports:
|
|
4
4
|
|
|
5
5
|
- `Avatar`
|
|
6
|
+
- `Badge`
|
|
6
7
|
- `Button`, `LinkButton`
|
|
7
8
|
- `Checkbox`, `CheckboxGroup`
|
|
8
9
|
- `Combobox`
|
|
9
10
|
- `DateField`, `DateRangeField`, `DateInput`, `DatePicker`
|
|
10
11
|
- `AlertDialog`
|
|
11
12
|
- `FileUploadDropzone`, `FileUpload`
|
|
12
|
-
- `
|
|
13
|
+
- `FormatByte`, `FormatNumber`, `FormatRelativeTime`, `FormatTime`
|
|
14
|
+
- `ImageCropper`, `ImageCropDialog`, `ImageUpload`
|
|
13
15
|
- `JsonTreeView`
|
|
14
16
|
- `NumberInput`, `PasswordInput`, `PhoneInput`
|
|
17
|
+
- `ProgressLinear`, `ProgressCircular`
|
|
15
18
|
- `QrCode`
|
|
16
19
|
- `Select`
|
|
17
20
|
- `Splitter`
|
|
18
21
|
- `Switch`
|
|
22
|
+
- `TagsInput`
|
|
19
23
|
- `Toggle`
|
|
20
24
|
- `TreeView`
|
|
21
25
|
- `LocaleProvider`, `useLocaleContext`
|
|
@@ -25,24 +29,27 @@ Root exports:
|
|
|
25
29
|
|
|
26
30
|
Namespace exports:
|
|
27
31
|
|
|
32
|
+
- `AppShell`
|
|
28
33
|
- `Card`
|
|
29
34
|
- `Collapsible`
|
|
30
35
|
- `HoverCard`
|
|
31
36
|
- `ScrollArea`
|
|
32
37
|
- `Carousel`
|
|
33
38
|
- `Dialog`
|
|
34
|
-
- `DataTable`
|
|
35
|
-
- `VirtualDataTable`
|
|
39
|
+
- `DataTable` (subpath: `compote-ui/data-table`, not re-exported from root)
|
|
40
|
+
- `VirtualDataTable` (subpath: `compote-ui/data-table/virtual`, not re-exported from root)
|
|
36
41
|
- `Drawer`
|
|
42
|
+
- `Field`
|
|
43
|
+
- `Fieldset`
|
|
37
44
|
- `Listbox`
|
|
45
|
+
- `Menu`
|
|
46
|
+
- `NavRail`
|
|
38
47
|
- `Popover`
|
|
48
|
+
- `Table`
|
|
39
49
|
- `Tabs`
|
|
40
50
|
- `Toast`
|
|
41
51
|
- `ToggleGroup`
|
|
42
|
-
- `Menu`
|
|
43
52
|
- `Tooltip`
|
|
44
|
-
- `Field`
|
|
45
|
-
- `Fieldset`
|
|
46
53
|
|
|
47
54
|
Utility exports:
|
|
48
55
|
|
|
@@ -52,3 +59,10 @@ Utility exports:
|
|
|
52
59
|
- `processImage`
|
|
53
60
|
- `createListCollection`
|
|
54
61
|
- `createTreeCollection`
|
|
62
|
+
- `toDateValue`, `dateValueToString`, `dateValueToDate`
|
|
63
|
+
|
|
64
|
+
`DataTable` and `VirtualDataTable` are deliberately **not** root exports — they pull in the
|
|
65
|
+
`@tanstack/svelte-table` (and, for the virtual variant, `@tanstack/svelte-virtual`) peer
|
|
66
|
+
dependency, so importing them from the root package would force that peer on every consumer.
|
|
67
|
+
Import them from the subpath instead: `import * as DataTable from 'compote-ui/data-table'`. See
|
|
68
|
+
the `data-table` skill.
|
|
@@ -1,5 +1,54 @@
|
|
|
1
1
|
# Files And Images
|
|
2
2
|
|
|
3
|
+
## ImageUpload
|
|
4
|
+
|
|
5
|
+
Single-image upload/replace/delete widget — a preview box with a corner `Menu` badge
|
|
6
|
+
(click-triggered, works on touch) opening "Upload/Replace" and "Delete" actions. Selecting a file
|
|
7
|
+
routes through `ImageCropDialog` automatically, so this is the easiest way to wire up an avatar or
|
|
8
|
+
banner uploader — no manual dropzone/crop-dialog wiring needed.
|
|
9
|
+
|
|
10
|
+
```svelte
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
let avatarUrl = $state<string | undefined>();
|
|
13
|
+
|
|
14
|
+
function handleChange(blob: Blob) {
|
|
15
|
+
const form = new FormData();
|
|
16
|
+
form.append('image', blob, 'avatar.webp');
|
|
17
|
+
fetch('/upload', { method: 'POST', body: form });
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<ImageUpload
|
|
22
|
+
bind:value={avatarUrl}
|
|
23
|
+
aspectRatio={1}
|
|
24
|
+
onChange={handleChange}
|
|
25
|
+
onDelete={() => {
|
|
26
|
+
/* clear on server */
|
|
27
|
+
}}
|
|
28
|
+
/>
|
|
29
|
+
|
|
30
|
+
<!-- Square/banner variant: size + shape via class -->
|
|
31
|
+
<ImageUpload
|
|
32
|
+
bind:value={bannerUrl}
|
|
33
|
+
shape="square"
|
|
34
|
+
class="aspect-video w-64"
|
|
35
|
+
aspectRatio={16 / 9}
|
|
36
|
+
uploadLabel="Upload banner"
|
|
37
|
+
/>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Props: `value?: string` (bindable), `shape?: 'circle' | 'square'` (default `'circle'`),
|
|
41
|
+
`class?: ClassValue` (default `'size-24'` — controls preview box sizing), `aspectRatio?: number`
|
|
42
|
+
(passed to the crop dialog), `processOptions?: ProcessImageOptions`,
|
|
43
|
+
`uploadLabel?`/`replaceLabel?`/`deleteLabel?: string`, `onChange: (blob: Blob) => void`,
|
|
44
|
+
`onDelete: () => void`.
|
|
45
|
+
|
|
46
|
+
`value` is updated internally to an object URL after a successful crop; the previous object URL is
|
|
47
|
+
revoked automatically. Delete has **no built-in confirmation** — wrap `onDelete` in your own
|
|
48
|
+
`AlertDialog` if you need one.
|
|
49
|
+
|
|
50
|
+
## ImageCropDialog
|
|
51
|
+
|
|
3
52
|
Use `ImageCropDialog` for the complete upload/crop flow.
|
|
4
53
|
|
|
5
54
|
```svelte
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# Forms And Inputs
|
|
2
2
|
|
|
3
|
+
## Field
|
|
4
|
+
|
|
3
5
|
Use `Field.Root` to propagate `invalid`, `disabled`, `required`, and `readOnly` state.
|
|
4
6
|
|
|
5
7
|
```svelte
|
|
@@ -45,7 +47,23 @@ say), pass `hideMessageLine` to drop the spacer entirely:
|
|
|
45
47
|
</Field.Root>
|
|
46
48
|
```
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
`Field.Textarea` composes the same way as `Field.Input`:
|
|
51
|
+
|
|
52
|
+
```svelte
|
|
53
|
+
<Field.Root required>
|
|
54
|
+
<Field.Label>Bio</Field.Label>
|
|
55
|
+
<Field.Textarea bind:value={bio} rows={4} />
|
|
56
|
+
<Field.HelperText>Max 500 characters.</Field.HelperText>
|
|
57
|
+
</Field.Root>
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Components that wrap their own control** — `PasswordInput`, `NumberInput`, `Select`, the date
|
|
61
|
+
components — read `invalid`/`required`/`disabled`/`readOnly` from `Field.Root`'s context directly.
|
|
62
|
+
Don't pass those props to both the field and the wrapped control.
|
|
63
|
+
|
|
64
|
+
## Select
|
|
65
|
+
|
|
66
|
+
Single-value dropdown using `{ value, label }` items:
|
|
49
67
|
|
|
50
68
|
```svelte
|
|
51
69
|
<script lang="ts">
|
|
@@ -58,48 +76,137 @@ List controls use `{ value, label }` items:
|
|
|
58
76
|
</script>
|
|
59
77
|
|
|
60
78
|
<Select {items} label="Fruit" bind:value placeholder="Select..." />
|
|
61
|
-
<Combobox {items} label="Fruit" bind:value />
|
|
62
79
|
```
|
|
63
80
|
|
|
64
|
-
|
|
81
|
+
### Form submission (hidden select)
|
|
82
|
+
|
|
83
|
+
`Select` renders Ark UI's `Select.HiddenSelect` — a real visually-hidden `<select>`. Pass `name`
|
|
84
|
+
and the value is serialized by `FormData`, so it works with native forms, SvelteKit form actions,
|
|
85
|
+
and remote `form()` functions.
|
|
65
86
|
|
|
66
87
|
```svelte
|
|
67
|
-
<
|
|
68
|
-
|
|
88
|
+
<form {...createPost}>
|
|
89
|
+
<Select {items} label="Fruit" name="fruit" />
|
|
90
|
+
<Select items={amounts} label="Amount" name="n:amount" />
|
|
91
|
+
</form>
|
|
92
|
+
```
|
|
69
93
|
|
|
70
|
-
|
|
71
|
-
|
|
94
|
+
- **Nothing selected omits the key entirely** — the hidden select sits at `selectedIndex: -1`, so
|
|
95
|
+
the field is absent from `FormData` (`undefined`, not `''`). Pair it with `v.optional(...)`.
|
|
96
|
+
- **Numbers need the `n:` name prefix** (e.g. `name="n:amount"`) — values are otherwise strings;
|
|
97
|
+
SvelteKit resolves coercion from the field name, not the element type.
|
|
98
|
+
- **`form.fields.x.value()` is stale on the client** — SvelteKit syncs field state from `input`
|
|
99
|
+
events, and the Ark trigger sets the value programmatically without firing one. Use `bind:value`
|
|
100
|
+
for client-side reads.
|
|
101
|
+
- **No progressive enhancement** — the hidden select is `tabIndex: -1` and the trigger is
|
|
102
|
+
JS-driven.
|
|
103
|
+
|
|
104
|
+
## Combobox
|
|
105
|
+
|
|
106
|
+
Searchable select with optional multi-select and client-side filtering.
|
|
107
|
+
|
|
108
|
+
```svelte
|
|
109
|
+
<Combobox items={langs} label="Language" bind:value={lang} placeholder="Choose..." />
|
|
110
|
+
<Combobox items={langs} label="Languages" bind:value={langs} multiple placeholder="Choose..." />
|
|
111
|
+
<Combobox items={results} label="User" bind:value={userId} loading={fetching} />
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Checkbox / CheckboxGroup
|
|
72
115
|
|
|
73
|
-
|
|
116
|
+
```svelte
|
|
117
|
+
<Checkbox label="I agree to the terms" bind:checked />
|
|
118
|
+
|
|
119
|
+
<!-- Helper text below the label -->
|
|
120
|
+
<Checkbox label="Subscribe to newsletter" bind:checked>Receive weekly digest emails.</Checkbox>
|
|
74
121
|
```
|
|
75
122
|
|
|
76
|
-
|
|
123
|
+
`CheckboxGroup` renders a set of checkboxes from `{ value, label }` items and binds an array:
|
|
77
124
|
|
|
78
125
|
```svelte
|
|
79
126
|
<script lang="ts">
|
|
80
|
-
|
|
127
|
+
const items = [
|
|
128
|
+
{ value: 'svelte', label: 'Svelte' },
|
|
129
|
+
{ value: 'react', label: 'React' }
|
|
130
|
+
];
|
|
131
|
+
let selected = $state(['svelte']);
|
|
132
|
+
</script>
|
|
133
|
+
|
|
134
|
+
<CheckboxGroup {items} bind:value={selected} orientation="vertical" />
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## TagsInput
|
|
138
|
+
|
|
139
|
+
Free-text tag entry — type a value and press `Enter`/`,` to add it, click the `×` to remove it.
|
|
140
|
+
`value` is a plain `string[]`.
|
|
81
141
|
|
|
82
|
-
|
|
142
|
+
```svelte
|
|
143
|
+
<script lang="ts">
|
|
144
|
+
let value = $state(['Svelte', 'React']); // predefined tags
|
|
83
145
|
</script>
|
|
84
146
|
|
|
85
|
-
<
|
|
147
|
+
<TagsInput label="Frameworks" bind:value placeholder="Add a tag" />
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
If selection must be restricted to a fixed set (pick-only, no free text), use `Combobox` with
|
|
151
|
+
`multiple` instead — `TagsInput` accepts arbitrary typed text.
|
|
152
|
+
|
|
153
|
+
## Switch
|
|
154
|
+
|
|
155
|
+
```svelte
|
|
156
|
+
<Switch label="Dark mode" bind:checked={darkMode} />
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`labelSpacer` adds vertical space to align a labelless switch with adjacent labeled fields.
|
|
160
|
+
|
|
161
|
+
## NumberInput
|
|
162
|
+
|
|
163
|
+
```svelte
|
|
164
|
+
<NumberInput label="Quantity" bind:value={qty} min={0} max={100} step={1} />
|
|
165
|
+
<NumberInput
|
|
166
|
+
label="Price"
|
|
167
|
+
bind:value={price}
|
|
168
|
+
formatOptions={{ style: 'currency', currency: 'USD' }}
|
|
169
|
+
/>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Props: `value?: number | null` (bindable), `label?`, `readonly?`, `layout?: 'vertical' | 'horizontal'`,
|
|
173
|
+
plus Ark UI NumberInput props (`min`, `max`, `step`, `formatOptions`, `locale`).
|
|
174
|
+
|
|
175
|
+
## PasswordInput
|
|
176
|
+
|
|
177
|
+
Password field with a built-in show/hide toggle. The underlying `<input>` is a real DOM input —
|
|
178
|
+
pass `name` and it's included in `FormData` directly, no hidden input needed.
|
|
179
|
+
|
|
180
|
+
```svelte
|
|
181
|
+
<form method="post">
|
|
182
|
+
<PasswordInput label="Password" name="password" required />
|
|
183
|
+
<button type="submit">Sign in</button>
|
|
184
|
+
</form>
|
|
86
185
|
```
|
|
87
186
|
|
|
88
|
-
|
|
187
|
+
## PhoneInput
|
|
89
188
|
|
|
90
|
-
`
|
|
189
|
+
International phone input wrapping `svelte-tel-input`. `value` is always E.164
|
|
190
|
+
(e.g. `+12015550123`); `country` is an ISO 3166-1 alpha-2 code.
|
|
91
191
|
|
|
92
192
|
```svelte
|
|
93
193
|
<script lang="ts">
|
|
94
194
|
let phone = $state('');
|
|
95
|
-
let country = $state(
|
|
195
|
+
let country = $state<CountryCode | null>('US');
|
|
96
196
|
let valid = $state(false);
|
|
97
197
|
</script>
|
|
98
198
|
|
|
99
199
|
<PhoneInput label="Phone" bind:value={phone} bind:country bind:valid />
|
|
100
200
|
```
|
|
101
201
|
|
|
102
|
-
`
|
|
202
|
+
`options?: TelInputOptions` (from `svelte-tel-input/types`) configures parsing/validation:
|
|
203
|
+
`autoPlaceholder`, `spaces`, `validateOn`, `allowedCountries`, `lockCountry`. The country picker is
|
|
204
|
+
an Ark UI `Select` (not a native `<select>`) with no search/filter yet.
|
|
205
|
+
|
|
206
|
+
## Listbox
|
|
207
|
+
|
|
208
|
+
`Listbox.Content` exposes filtered `items` and grouped `group` snippet data. `Listbox.Input`
|
|
209
|
+
auto-wires filter state via context — no props needed.
|
|
103
210
|
|
|
104
211
|
```svelte
|
|
105
212
|
<Listbox.Root {items} bind:value={selected}>
|
|
@@ -118,3 +225,90 @@ Use `DateInput` when binding existing `string`, `Date`, or `DateValue` values wi
|
|
|
118
225
|
</Listbox.Content>
|
|
119
226
|
</Listbox.Root>
|
|
120
227
|
```
|
|
228
|
+
|
|
229
|
+
## Date components
|
|
230
|
+
|
|
231
|
+
`DateField`, `DateRangeField`, `DateInput`, and `DatePicker` all accept
|
|
232
|
+
`DateValue | string | Date | null` for `value`/`defaultValue` — bind directly to DB values, no
|
|
233
|
+
`parseDate`/`.toString()` conversion needed. The bound value round-trips in the same shape given
|
|
234
|
+
(string in → string out, `Date` in → `Date` out); if it starts `null`, changes emit as a string.
|
|
235
|
+
`DateRangeField` uses the same notation on separate `start`/`end` props instead of an array `value`.
|
|
236
|
+
|
|
237
|
+
`locale`/`timeZone` default to the nearest `LocaleProvider` / the user's local zone. For UTC values
|
|
238
|
+
from a DB (`timestamptz`), bind the ISO string with its offset and leave `timeZone` unset — the
|
|
239
|
+
component displays local time and emits UTC back.
|
|
240
|
+
|
|
241
|
+
String auto-detection (space separator and bare `+00` offset are normalized):
|
|
242
|
+
|
|
243
|
+
- `"2024-01-15"` → date-only → emits `"2024-01-15"`
|
|
244
|
+
- `"2024-01-15T10:30:00"` → naive datetime → emits the same shape
|
|
245
|
+
- `"2024-01-15T10:30:00Z"` / `"...+02:00"` → zoned, displayed local, emits UTC absolute string
|
|
246
|
+
|
|
247
|
+
A zoned value at time granularity would render a trailing timezone segment (`GMT+2`); all three
|
|
248
|
+
segmented components hide it by default — pass `hideTimeZone={false}` to show it.
|
|
249
|
+
|
|
250
|
+
### Form submission (ISO hidden inputs)
|
|
251
|
+
|
|
252
|
+
Give any date component a `name` and it renders a hidden input carrying a canonical **ISO
|
|
253
|
+
string**, never the locale-formatted display value.
|
|
254
|
+
|
|
255
|
+
```svelte
|
|
256
|
+
<DateField label="Date" bind:value name="start_date" />
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`DateRangeField` is the exception — one field name can't carry two dates, so `name="stay"` is
|
|
260
|
+
shorthand for **two** hidden inputs, `stayStart`/`stayEnd`. Use `startName`/`endName` to name each
|
|
261
|
+
side outright. An unpicked end date submits as `''`, not an absent key.
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
// +page.server.ts
|
|
265
|
+
const start = formData.get('stayStart'); // '2025-06-01'
|
|
266
|
+
const end = formData.get('stayEnd'); // '', if not picked yet
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**With SvelteKit remote functions:** bind strings, not `DateValue` — `query`/`command` args are
|
|
270
|
+
serialized with devalue, which doesn't handle `@internationalized/date` instances. Binding a DB
|
|
271
|
+
string keeps the value serializable end to end. `form.fields.x.value()` also goes stale here (the
|
|
272
|
+
component sets the hidden input's value programmatically without firing `input`) — read from
|
|
273
|
+
`bind:` instead. For full `form` integration (repopulation after a failed submit, `aria-invalid`),
|
|
274
|
+
render the hidden inputs yourself with `.as('hidden', value)` rather than relying on `name`.
|
|
275
|
+
|
|
276
|
+
### DateField ← recommended primary component
|
|
277
|
+
|
|
278
|
+
Segmented date input (separate day/month/year fields) with an optional calendar popup — no format
|
|
279
|
+
ambiguity, keyboard-incrementable segments, proper ARIA labels.
|
|
280
|
+
|
|
281
|
+
```svelte
|
|
282
|
+
<DateField label="Date" bind:value {min} {max} />
|
|
283
|
+
<DateField label="Updated at" granularity="minute" bind:value={row.updated_at} />
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### DateRangeField
|
|
287
|
+
|
|
288
|
+
Two segment groups wired to a shared calendar, bound separately via `bind:start`/`bind:end`.
|
|
289
|
+
|
|
290
|
+
```svelte
|
|
291
|
+
<DateRangeField label="Stay" bind:start bind:end />
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
`end` is ignored while `start` is empty — zag reads the range positionally.
|
|
295
|
+
|
|
296
|
+
### DateInput
|
|
297
|
+
|
|
298
|
+
Segmented input **without** a calendar popup — compact, keyboard-only entry.
|
|
299
|
+
|
|
300
|
+
```svelte
|
|
301
|
+
<DateInput label="Date & time" bind:value={row.updated_at} granularity="minute" />
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
For a nullable string column that starts empty, initialize as `''` (not `null`) so the value stays
|
|
305
|
+
serializable as a string.
|
|
306
|
+
|
|
307
|
+
### DatePicker
|
|
308
|
+
|
|
309
|
+
Free-text input with a calendar popup — simpler, less accessible than `DateField`. Prefer
|
|
310
|
+
`DateField` for most cases.
|
|
311
|
+
|
|
312
|
+
```svelte
|
|
313
|
+
<DatePicker label="Date" bind:value placeholder="MM/DD/YYYY" />
|
|
314
|
+
```
|
|
@@ -1,6 +1,177 @@
|
|
|
1
1
|
# Layout And Display
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## AppShell
|
|
4
|
+
|
|
5
|
+
Page-scaffolding layout with five **optional** regions on a CSS grid. Render only the regions
|
|
6
|
+
you need — an omitted one collapses to zero width/height, no config flag needed.
|
|
7
|
+
|
|
8
|
+
```svelte
|
|
9
|
+
<AppShell.Root>
|
|
10
|
+
<AppShell.Header>Header</AppShell.Header>
|
|
11
|
+
<AppShell.Navbar>Navbar</AppShell.Navbar>
|
|
12
|
+
<AppShell.Main>Main content (scrolls independently)</AppShell.Main>
|
|
13
|
+
<AppShell.Aside>Aside</AppShell.Aside>
|
|
14
|
+
<AppShell.Footer>Footer</AppShell.Footer>
|
|
15
|
+
</AppShell.Root>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
- Sizing is done via the `class` prop (no size props): `<AppShell.Navbar class="w-80">`.
|
|
19
|
+
- Full height by default (`h-dvh`); override with `<AppShell.Root class="h-full">` when nesting.
|
|
20
|
+
- `AppShell.Main` scrolls independently — header/navbar/aside/footer stay put.
|
|
21
|
+
- Use `AppShell.Section grow` inside `Navbar`/`Aside` for a scrolling area between pinned
|
|
22
|
+
top/bottom sections (e.g. logo + scrolling nav links + user menu).
|
|
23
|
+
- Desktop-first, no built-in mobile logic — add your own responsive classes.
|
|
24
|
+
|
|
25
|
+
## NavRail
|
|
26
|
+
|
|
27
|
+
A collapsed icon rail that **expands on hover** (overlay mode), meant to sit inside
|
|
28
|
+
`AppShell.Navbar`. It floats over content and widens on hover/focus/pin — nothing else reflows.
|
|
29
|
+
|
|
30
|
+
```svelte
|
|
31
|
+
<AppShell.Navbar class="w-16">
|
|
32
|
+
<NavRail.Root bind:expanded>
|
|
33
|
+
<AppShell.Section grow class="space-y-1 p-2">
|
|
34
|
+
<NavRail.Item label="Dashboard" active onclick={() => goto('/dashboard')}>
|
|
35
|
+
{#snippet icon()}<PhStar />{/snippet}
|
|
36
|
+
</NavRail.Item>
|
|
37
|
+
</AppShell.Section>
|
|
38
|
+
</NavRail.Root>
|
|
39
|
+
</AppShell.Navbar>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
- `NavRail.Root`: `expanded?: boolean` (bindable) pins it open.
|
|
43
|
+
- `NavRail.Item`: renders `<a>` when `href` is set, else `<button>`. Pass `icon` as a snippet;
|
|
44
|
+
`label` fades in on expand.
|
|
45
|
+
- The `AppShell.Navbar` placeholder must reserve the collapsed width (`class="w-16"`).
|
|
46
|
+
|
|
47
|
+
## Badge
|
|
48
|
+
|
|
49
|
+
Plain presentational `<span>` for status labels, counts, or tags — no Ark UI primitive.
|
|
50
|
+
|
|
51
|
+
```svelte
|
|
52
|
+
<Badge>Draft</Badge>
|
|
53
|
+
<Badge color="success" variant="solid">Verified</Badge>
|
|
54
|
+
<Badge color="danger" variant="outline">Failed</Badge>
|
|
55
|
+
|
|
56
|
+
<Badge color="success" variant="solid">
|
|
57
|
+
{#snippet icon()}<PhCheck />{/snippet}
|
|
58
|
+
Verified
|
|
59
|
+
</Badge>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`Badge` props: `variant?: 'solid' | 'subtle' | 'outline'` (default `'subtle'`),
|
|
63
|
+
`color?: 'neutral' | 'primary' | 'danger' | 'warning' | 'success' | 'info'` (default `'neutral'`),
|
|
64
|
+
`icon?: Snippet`, `class?: ClassValue`.
|
|
65
|
+
|
|
66
|
+
## Card
|
|
67
|
+
|
|
68
|
+
Namespaced presentational component for grouping content.
|
|
69
|
+
|
|
70
|
+
```svelte
|
|
71
|
+
<Card.Root>
|
|
72
|
+
<Card.Header>
|
|
73
|
+
<Card.Title>Card Title</Card.Title>
|
|
74
|
+
<Card.Description>Card Description</Card.Description>
|
|
75
|
+
</Card.Header>
|
|
76
|
+
<Card.Content>
|
|
77
|
+
<p>Card content goes here.</p>
|
|
78
|
+
</Card.Content>
|
|
79
|
+
<Card.Footer>
|
|
80
|
+
<Button variant="outline">Cancel</Button>
|
|
81
|
+
<Button>Confirm</Button>
|
|
82
|
+
</Card.Footer>
|
|
83
|
+
</Card.Root>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
All sub-components (`Root`, `Header`, `Title`, `Description`, `Content`, `Footer`) accept `class`.
|
|
87
|
+
|
|
88
|
+
## Table
|
|
89
|
+
|
|
90
|
+
Plain presentational table primitive — no data model, no column defs, no sorting/filtering.
|
|
91
|
+
|
|
92
|
+
**Table vs DataTable.** Use `DataTable` (see the `data-table` skill) when you need sorting,
|
|
93
|
+
filtering, column visibility/resizing/pinning, row selection, or virtualization. Use `Table` for
|
|
94
|
+
static rows, cells built from arbitrary markup, or tables inside dialogs where selection state
|
|
95
|
+
lives in the consumer. Both share cell metrics and surface tokens, so they stay visually
|
|
96
|
+
consistent.
|
|
97
|
+
|
|
98
|
+
```svelte
|
|
99
|
+
<div class="max-h-64 overflow-auto rounded-lg border border-surface-3">
|
|
100
|
+
<Table.Root class="table-fixed">
|
|
101
|
+
<Table.Header>
|
|
102
|
+
<Table.Row>
|
|
103
|
+
<Table.Head class="w-24">SKU</Table.Head>
|
|
104
|
+
<Table.Head>Item</Table.Head>
|
|
105
|
+
<Table.Head class="text-right">Qty</Table.Head>
|
|
106
|
+
</Table.Row>
|
|
107
|
+
</Table.Header>
|
|
108
|
+
<Table.Body>
|
|
109
|
+
{#each items as item (item.id)}
|
|
110
|
+
<Table.Row selected={selectedIds[item.id]}>
|
|
111
|
+
<Table.Cell>{item.sku || '-'}</Table.Cell>
|
|
112
|
+
<Table.Cell class="truncate">{item.name}</Table.Cell>
|
|
113
|
+
<Table.Cell class="text-right">{item.quantity}</Table.Cell>
|
|
114
|
+
</Table.Row>
|
|
115
|
+
{/each}
|
|
116
|
+
</Table.Body>
|
|
117
|
+
</Table.Root>
|
|
118
|
+
</div>
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`Table.Root` has no border, rounding, background, or scroll container by default — wrap it
|
|
122
|
+
yourself. Add `table-fixed` when setting explicit column widths or using `truncate` on cells.
|
|
123
|
+
Alignment is via utility classes (`text-right`) on `Table.Head`/`Table.Cell`, not props.
|
|
124
|
+
|
|
125
|
+
## QrCode
|
|
126
|
+
|
|
127
|
+
Single-file wrapper rendering an SVG QR code from any string value.
|
|
128
|
+
|
|
129
|
+
```svelte
|
|
130
|
+
<QrCode value="https://example.com" />
|
|
131
|
+
<QrCode value="https://example.com" size="lg" class="text-primary" />
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Props: `value: string`, `size?: 'sm' | 'md' | 'lg' | 'xl'` (default `'md'`),
|
|
135
|
+
`encoding?: QrCodeGenerateOptions`, `class?: string`. Color follows the CSS `color` property — use
|
|
136
|
+
a `text-*` class.
|
|
137
|
+
|
|
138
|
+
## Progress
|
|
139
|
+
|
|
140
|
+
Two wrappers around Ark UI Progress: `ProgressLinear` (bar) and `ProgressCircular` (ring). Both
|
|
141
|
+
are determinate by default; pass `value={null}` for an indeterminate animated state.
|
|
142
|
+
|
|
143
|
+
```svelte
|
|
144
|
+
<ProgressLinear value={60} label="Uploading" showValueText />
|
|
145
|
+
<ProgressCircular value={60} showValueText size="lg" />
|
|
146
|
+
<ProgressLinear value={null} label="Loading…" />
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Shared props: `value?: number | null` (bindable, `null` = indeterminate), `min`/`max` (default
|
|
150
|
+
`0`/`100`), `label?: string`, `showValueText?: boolean`,
|
|
151
|
+
`variant?: 'primary' | 'success' | 'danger' | 'warning' | 'info'`, `formatOptions?: NumberFormatOptions`.
|
|
152
|
+
`ProgressLinear` adds `size?: 'sm' | 'md' | 'lg'`. `ProgressCircular` adds `size?: 'sm' | 'md' | 'lg' | number`
|
|
153
|
+
and `thickness?: number`.
|
|
154
|
+
|
|
155
|
+
## Format
|
|
156
|
+
|
|
157
|
+
Unstyled formatting components re-exported directly from Ark UI (no compote wrapper) — they
|
|
158
|
+
render plain text via `Intl`, so there's nothing to theme. Locale comes from the nearest
|
|
159
|
+
`LocaleProvider` (defaults to browser locale). None of them accept `class` since they render bare
|
|
160
|
+
text nodes — wrap in a `<span>` to style the surrounding text.
|
|
161
|
+
|
|
162
|
+
```svelte
|
|
163
|
+
<FormatRelativeTime value={someDate} />
|
|
164
|
+
<FormatByte value={1536} />
|
|
165
|
+
<FormatNumber value={1234.5} style="currency" currency="EUR" />
|
|
166
|
+
<FormatTime value={someDate} format="24h" />
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- `FormatRelativeTime` — `value: Date` + `Intl.RelativeTimeFormatOptions` (`style`, `numeric`).
|
|
170
|
+
- `FormatByte` — `value: number` (bytes) + `unit?`, `unitDisplay?`, `unitSystem?: 'decimal' | 'binary'`.
|
|
171
|
+
- `FormatNumber` — `value: number` + `Intl.NumberFormatOptions` (`style`, `currency`, etc.).
|
|
172
|
+
- `FormatTime` — `value: string | Date` + `format?: '12h' | '24h'`, `withSeconds?: boolean`.
|
|
173
|
+
|
|
174
|
+
## Tabs
|
|
4
175
|
|
|
5
176
|
```svelte
|
|
6
177
|
<Tabs.Root bind:value={tab} defaultValue="account">
|
|
@@ -14,6 +185,10 @@ Tabs:
|
|
|
14
185
|
</Tabs.Root>
|
|
15
186
|
```
|
|
16
187
|
|
|
188
|
+
`Tabs.Indicator` must be inside `Tabs.List`, after the triggers.
|
|
189
|
+
|
|
190
|
+
## Splitter
|
|
191
|
+
|
|
17
192
|
Splitter fills its container:
|
|
18
193
|
|
|
19
194
|
```svelte
|
|
@@ -34,7 +209,11 @@ Splitter fills its container:
|
|
|
34
209
|
</div>
|
|
35
210
|
```
|
|
36
211
|
|
|
37
|
-
|
|
212
|
+
**Limitation — no nesting.** The `<Splitter>` wrapper doesn't support nesting: Ark UI requires both
|
|
213
|
+
outer and inner `Splitter.Root` to share one `registry` instance, but the wrapper has no way to
|
|
214
|
+
pass it into a `content` snippet. Use Ark UI primitives directly, calling
|
|
215
|
+
`Splitter.createRegistry()` **once** and passing the same `{registry}` to every `Splitter.Root` in
|
|
216
|
+
the tree:
|
|
38
217
|
|
|
39
218
|
```svelte
|
|
40
219
|
<script lang="ts">
|
|
@@ -42,8 +221,43 @@ For nested splitters, use Ark UI directly with one shared registry:
|
|
|
42
221
|
|
|
43
222
|
const registry = Splitter.createRegistry();
|
|
44
223
|
</script>
|
|
224
|
+
|
|
225
|
+
<Splitter.Root {registry} orientation="horizontal" panels={[{ id: 'left' }, { id: 'right' }]}>
|
|
226
|
+
<Splitter.Panel id="left" class="overflow-auto p-2">Left</Splitter.Panel>
|
|
227
|
+
<Splitter.ResizeTrigger id="left:right" aria-label="Resize left and right" />
|
|
228
|
+
<Splitter.Panel id="right" class="overflow-auto">
|
|
229
|
+
<!-- Nested vertical splitter reuses the same registry -->
|
|
230
|
+
<Splitter.Root {registry} orientation="vertical" panels={[{ id: 'top' }, { id: 'bottom' }]}>
|
|
231
|
+
<Splitter.Panel id="top" class="overflow-auto p-2">Top</Splitter.Panel>
|
|
232
|
+
<Splitter.ResizeTrigger id="top:bottom" aria-label="Resize top and bottom" />
|
|
233
|
+
<Splitter.Panel id="bottom" class="overflow-auto p-2">Bottom</Splitter.Panel>
|
|
234
|
+
</Splitter.Root>
|
|
235
|
+
</Splitter.Panel>
|
|
236
|
+
</Splitter.Root>
|
|
45
237
|
```
|
|
46
238
|
|
|
239
|
+
The `ResizeTrigger` `id` must follow the `"panelA:panelB"` format matching adjacent panel ids.
|
|
240
|
+
|
|
241
|
+
## Collapsible
|
|
242
|
+
|
|
243
|
+
```svelte
|
|
244
|
+
<Collapsible.Root>
|
|
245
|
+
<Collapsible.Trigger>
|
|
246
|
+
Section title
|
|
247
|
+
<Collapsible.Indicator />
|
|
248
|
+
</Collapsible.Trigger>
|
|
249
|
+
<Collapsible.Content>
|
|
250
|
+
<p class="px-3 py-2.5">Hidden content here.</p>
|
|
251
|
+
</Collapsible.Content>
|
|
252
|
+
</Collapsible.Root>
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
`Collapsible.Indicator` rotates 90deg on `data-[state=open]` (defaults to a caret-right icon).
|
|
256
|
+
Pass `defaultOpen`, `disabled`, `collapsedHeight` (partial-collapse mode), or `bind:open` to
|
|
257
|
+
`Collapsible.Root`.
|
|
258
|
+
|
|
259
|
+
## ScrollArea
|
|
260
|
+
|
|
47
261
|
ScrollArea needs explicit size:
|
|
48
262
|
|
|
49
263
|
```svelte
|
|
@@ -57,6 +271,8 @@ ScrollArea needs explicit size:
|
|
|
57
271
|
</ScrollArea.Root>
|
|
58
272
|
```
|
|
59
273
|
|
|
274
|
+
## Carousel
|
|
275
|
+
|
|
60
276
|
Vertical Carousel needs explicit height:
|
|
61
277
|
|
|
62
278
|
```svelte
|
|
@@ -70,9 +286,19 @@ Vertical Carousel needs explicit height:
|
|
|
70
286
|
{/each}
|
|
71
287
|
</Carousel.ItemGroup>
|
|
72
288
|
</Carousel.Control>
|
|
289
|
+
<Carousel.IndicatorGroup>
|
|
290
|
+
{#each images as _, index (index)}
|
|
291
|
+
<Carousel.Indicator {index} />
|
|
292
|
+
{/each}
|
|
293
|
+
</Carousel.IndicatorGroup>
|
|
73
294
|
</Carousel.Root>
|
|
74
295
|
```
|
|
75
296
|
|
|
297
|
+
`Carousel.Indicator` renders a dot by default; pass `thumbnail` plus children for a thumbnail
|
|
298
|
+
indicator.
|
|
299
|
+
|
|
300
|
+
## TreeView
|
|
301
|
+
|
|
76
302
|
TreeView fills its container:
|
|
77
303
|
|
|
78
304
|
```svelte
|
|
@@ -80,3 +306,20 @@ TreeView fills its container:
|
|
|
80
306
|
<TreeView {items} label="Files" bind:selectedValue={selected} />
|
|
81
307
|
</div>
|
|
82
308
|
```
|
|
309
|
+
|
|
310
|
+
`selectionMode="multiple"` adds `bind:checkedValue` for checkbox selection. `TreeItem.icon` takes
|
|
311
|
+
an Iconify icon name (e.g. `"ph:folder""`) — requires `@iconify/svelte` in the consumer app.
|
|
312
|
+
|
|
313
|
+
## JsonTreeView
|
|
314
|
+
|
|
315
|
+
JSON/object inspector — renders nested objects, arrays, primitives, Dates, regexes, errors, maps,
|
|
316
|
+
and sets with keyboard navigation.
|
|
317
|
+
|
|
318
|
+
```svelte
|
|
319
|
+
<div class="max-h-96 rounded border p-2">
|
|
320
|
+
<JsonTreeView data={payload} defaultExpandedDepth={2} />
|
|
321
|
+
</div>
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Wrap in a constrained container (`max-h-*`) for large payloads. Pass a `renderValue` snippet for
|
|
325
|
+
custom value rendering (e.g. turning emails into `mailto:` links).
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# Overlays And Floating UI
|
|
2
2
|
|
|
3
3
|
`Dialog`, `Drawer`, `Menu`, `Popover`, `HoverCard`, `Tooltip`, and `Toast` are namespace exports.
|
|
4
|
+
`AlertDialog` is a single component, not a namespace.
|
|
4
5
|
|
|
5
|
-
Dialog
|
|
6
|
+
## Dialog
|
|
6
7
|
|
|
7
8
|
```svelte
|
|
8
9
|
<script lang="ts">
|
|
@@ -20,7 +21,7 @@ Dialog:
|
|
|
20
21
|
</Dialog.Root>
|
|
21
22
|
```
|
|
22
23
|
|
|
23
|
-
Use `onOpenChange`, not `onClose
|
|
24
|
+
Use `onOpenChange`, not `onClose` — `Dialog` has no `onClose` prop:
|
|
24
25
|
|
|
25
26
|
```svelte
|
|
26
27
|
<Dialog.Root
|
|
@@ -33,7 +34,19 @@ Use `onOpenChange`, not `onClose`:
|
|
|
33
34
|
</Dialog.Root>
|
|
34
35
|
```
|
|
35
36
|
|
|
36
|
-
|
|
37
|
+
`Dialog.CloseTrigger` with no children renders an absolute-positioned X icon in the corner
|
|
38
|
+
automatically — no separate placement needed. With children, it renders as an inline button (for
|
|
39
|
+
`Dialog.Footer`). `contentClass` sets dialog width (e.g. `contentClass="max-w-4xl"`).
|
|
40
|
+
|
|
41
|
+
**Stacking:** z-index defaults to `50`. Set the `--layer-index` CSS variable on the dialog's
|
|
42
|
+
container to offset it (e.g. `style="--layer-index: 10"` → z-index 60) — use this when a dialog
|
|
43
|
+
must appear above another fixed element like a drawer.
|
|
44
|
+
|
|
45
|
+
## Drawer
|
|
46
|
+
|
|
47
|
+
Slide-in panel anchored to a viewport edge, with swipe-to-dismiss and snap points.
|
|
48
|
+
`Drawer.Content` renders the backdrop and positioner internally — compose only
|
|
49
|
+
`Header`/`Body`/`Footer`.
|
|
37
50
|
|
|
38
51
|
```svelte
|
|
39
52
|
<Drawer.Root bind:open>
|
|
@@ -51,7 +64,30 @@ Drawer content renders backdrop and positioner internally:
|
|
|
51
64
|
</Drawer.Root>
|
|
52
65
|
```
|
|
53
66
|
|
|
54
|
-
|
|
67
|
+
`swipeDirection?: 'up' | 'down' | 'start' | 'end'` (default `'down'`) controls which edge it slides
|
|
68
|
+
from: `'down'` = bottom sheet (default), `'up'` = top, `'start'` = left (LTR), `'end'` = right
|
|
69
|
+
(LTR). `Drawer.Body` scrolls internally via `ScrollArea` — no fixed height needed. Safe to open a
|
|
70
|
+
`Dialog` from inside a `Drawer` (it portals above it, same z-index, stacked by DOM order).
|
|
71
|
+
|
|
72
|
+
Sizing and styling overrides:
|
|
73
|
+
|
|
74
|
+
```svelte
|
|
75
|
+
<!-- Custom height (bottom/top drawers, default 96svh) -->
|
|
76
|
+
<Drawer.Content maxHeight="50vh">...</Drawer.Content>
|
|
77
|
+
|
|
78
|
+
<!-- Custom width (side drawers, default 25rem) -->
|
|
79
|
+
<Drawer.Content maxWidth="32rem">...</Drawer.Content>
|
|
80
|
+
|
|
81
|
+
<!-- Custom backdrop/positioner classes (power users) -->
|
|
82
|
+
<Drawer.Content backdropClass="bg-black/80" positionerClass="items-center">...</Drawer.Content>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`Drawer.Grabber`/`Drawer.GrabberIndicator` render a drag handle (top edge for bottom/top drawers,
|
|
86
|
+
open edge for side drawers) — repositions automatically per `swipeDirection`.
|
|
87
|
+
|
|
88
|
+
## AlertDialog
|
|
89
|
+
|
|
90
|
+
For confirmations — built-in confirm/cancel buttons, no custom body.
|
|
55
91
|
|
|
56
92
|
```svelte
|
|
57
93
|
<AlertDialog
|
|
@@ -64,10 +100,139 @@ AlertDialog is for confirmations:
|
|
|
64
100
|
/>
|
|
65
101
|
```
|
|
66
102
|
|
|
67
|
-
|
|
103
|
+
Props: `open` (bindable), `title`, `description?: string | string[]`,
|
|
104
|
+
`variant?: 'default' | 'destructive'`, `confirmLabel?`, `cancelLabel?`, `onConfirm?`, `onCancel?`.
|
|
105
|
+
|
|
106
|
+
## Menu
|
|
107
|
+
|
|
108
|
+
Composable dropdown with items, groups, checkboxes, radio items, context trigger, and submenus.
|
|
109
|
+
|
|
110
|
+
```svelte
|
|
111
|
+
<Menu.Root>
|
|
112
|
+
<Menu.Trigger variant="outline">
|
|
113
|
+
Options
|
|
114
|
+
<Menu.Indicator />
|
|
115
|
+
</Menu.Trigger>
|
|
116
|
+
<Menu.Content>
|
|
117
|
+
<Menu.Item value="new">New File</Menu.Item>
|
|
118
|
+
<Menu.Item value="save">Save</Menu.Item>
|
|
119
|
+
<Menu.Separator />
|
|
120
|
+
<Menu.Item value="quit" disabled>Quit</Menu.Item>
|
|
121
|
+
</Menu.Content>
|
|
122
|
+
</Menu.Root>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Groups, checkboxes, and radio items:
|
|
126
|
+
|
|
127
|
+
```svelte
|
|
128
|
+
<Menu.Content>
|
|
129
|
+
<Menu.ItemGroup>
|
|
130
|
+
<Menu.ItemGroupLabel>View</Menu.ItemGroupLabel>
|
|
131
|
+
<Menu.CheckboxItem value="sidebar" checked={sidebar} onCheckedChange={(v) => (sidebar = v)}>
|
|
132
|
+
Show Sidebar
|
|
133
|
+
</Menu.CheckboxItem>
|
|
134
|
+
</Menu.ItemGroup>
|
|
135
|
+
<Menu.RadioItemGroup value={theme} onValueChange={(d) => (theme = d.value)}>
|
|
136
|
+
<Menu.RadioItem value="light">Light</Menu.RadioItem>
|
|
137
|
+
<Menu.RadioItem value="dark">Dark</Menu.RadioItem>
|
|
138
|
+
</Menu.RadioItemGroup>
|
|
139
|
+
</Menu.Content>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Right-click context menu — use `Menu.ContextTrigger` instead of `Menu.Trigger`:
|
|
143
|
+
|
|
144
|
+
```svelte
|
|
145
|
+
<Menu.Root>
|
|
146
|
+
<Menu.ContextTrigger>
|
|
147
|
+
<div class="rounded border p-8 text-center">Right-click here</div>
|
|
148
|
+
</Menu.ContextTrigger>
|
|
149
|
+
<Menu.Content>
|
|
150
|
+
<Menu.Item value="copy">Copy</Menu.Item>
|
|
151
|
+
</Menu.Content>
|
|
152
|
+
</Menu.Root>
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Submenu — nest a `Menu.Root` after a `Menu.TriggerItem`:
|
|
156
|
+
|
|
157
|
+
```svelte
|
|
158
|
+
<Menu.Content>
|
|
159
|
+
<Menu.TriggerItem>Export as...</Menu.TriggerItem>
|
|
160
|
+
<Menu.Root>
|
|
161
|
+
<Menu.Content>
|
|
162
|
+
<Menu.Item value="pdf">PDF</Menu.Item>
|
|
163
|
+
<Menu.Item value="csv">CSV</Menu.Item>
|
|
164
|
+
</Menu.Content>
|
|
165
|
+
</Menu.Root>
|
|
166
|
+
</Menu.Content>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Popover
|
|
170
|
+
|
|
171
|
+
Composable floating panel anchored to a trigger.
|
|
172
|
+
|
|
173
|
+
```svelte
|
|
174
|
+
<Popover.Root>
|
|
175
|
+
<Popover.Trigger>
|
|
176
|
+
<Button>Open Popover</Button>
|
|
177
|
+
</Popover.Trigger>
|
|
178
|
+
<Popover.Content>
|
|
179
|
+
<Popover.Title>Popover Title</Popover.Title>
|
|
180
|
+
<Popover.Description>Content here.</Popover.Description>
|
|
181
|
+
<Popover.CloseTrigger />
|
|
182
|
+
</Popover.Content>
|
|
183
|
+
</Popover.Root>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`Popover.Content` accepts `showArrow?: boolean` (default `true`). Control placement via
|
|
187
|
+
`<Popover.Root positioning={{ placement: 'right' }}>`.
|
|
188
|
+
|
|
189
|
+
## HoverCard
|
|
190
|
+
|
|
191
|
+
Floating card that opens on **hover**, not click — key difference from `Popover`. Use `asChild` on
|
|
192
|
+
the trigger to render any element (e.g. a link) instead of the default button.
|
|
193
|
+
|
|
194
|
+
```svelte
|
|
195
|
+
<HoverCard.Root>
|
|
196
|
+
<p>
|
|
197
|
+
Liked by
|
|
198
|
+
<HoverCard.Trigger>
|
|
199
|
+
{#snippet asChild(props)}
|
|
200
|
+
<a href="/profile" {...props()} class="font-medium text-primary underline"> @sarah_chen </a>
|
|
201
|
+
{/snippet}
|
|
202
|
+
</HoverCard.Trigger>
|
|
203
|
+
</p>
|
|
204
|
+
<HoverCard.Content>
|
|
205
|
+
<p class="text-sm text-ink">Bio text here.</p>
|
|
206
|
+
</HoverCard.Content>
|
|
207
|
+
</HoverCard.Root>
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
`openDelay?: number` (default `600`), `closeDelay?: number` (default `300`) on `HoverCard.Root`.
|
|
211
|
+
|
|
212
|
+
## Tooltip
|
|
213
|
+
|
|
214
|
+
Namespace export wrapping Ark UI Tooltip — short text hint shown on hover/focus.
|
|
215
|
+
|
|
216
|
+
```svelte
|
|
217
|
+
<Tooltip.Root>
|
|
218
|
+
<Tooltip.Trigger>
|
|
219
|
+
<Button variant="ghost" size="icon" aria-label="Info"><PhInfo /></Button>
|
|
220
|
+
</Tooltip.Trigger>
|
|
221
|
+
<Tooltip.Content>More information</Tooltip.Content>
|
|
222
|
+
</Tooltip.Root>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
Prefer `Tooltip` over `HoverCard` for a single line of supplementary text; use `HoverCard` for
|
|
226
|
+
richer content (profile previews, formatted descriptions).
|
|
227
|
+
|
|
228
|
+
## Toast
|
|
229
|
+
|
|
230
|
+
Toaster setup belongs once in the root layout:
|
|
68
231
|
|
|
69
232
|
```svelte
|
|
233
|
+
<!-- +layout.svelte -->
|
|
70
234
|
<Toast.Toaster />
|
|
235
|
+
{@render children()}
|
|
71
236
|
```
|
|
72
237
|
|
|
73
238
|
Then fire toasts anywhere:
|
|
@@ -77,4 +242,32 @@ import { toast } from 'compote-ui';
|
|
|
77
242
|
|
|
78
243
|
toast.success('Saved');
|
|
79
244
|
toast.error('Save failed', { description: 'Please try again.' });
|
|
245
|
+
toast.promise(saveData(), {
|
|
246
|
+
loading: { title: 'Saving…' },
|
|
247
|
+
success: { title: 'Saved' },
|
|
248
|
+
error: { title: 'Save failed' }
|
|
249
|
+
});
|
|
80
250
|
```
|
|
251
|
+
|
|
252
|
+
Shorthand helpers: `toast.info()`, `toast.success()`, `toast.warning()`, `toast.error()`,
|
|
253
|
+
`toast.loading()` — first arg is the title, second an optional options object; each returns the
|
|
254
|
+
toast `id` usable with `toast.update(id, options)` / `toast.dismiss(id)`. `loading` shows a
|
|
255
|
+
spinner and does not auto-dismiss. `toast.create(options)` remains available for full control.
|
|
256
|
+
|
|
257
|
+
Action button (e.g. Undo):
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
toast.create({
|
|
261
|
+
title: 'File deleted',
|
|
262
|
+
type: 'info',
|
|
263
|
+
action: { label: 'Undo', onClick: () => restore() }
|
|
264
|
+
});
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
`toast.create()` options: `title: string`, `description?: string`,
|
|
268
|
+
`type?: 'info' | 'success' | 'error' | 'warning' | 'loading'`, `duration?: number` (ms, overrides
|
|
269
|
+
the toaster default for this toast), `action?: { label: string; onClick: () => void }`.
|
|
270
|
+
|
|
271
|
+
`Toast.Toaster` props: `class?: ClassValue`, `toaster?: CreateToasterReturn` (a custom instance
|
|
272
|
+
from `createToaster()` for a different `placement`/`max`/`duration` — defaults to the shared
|
|
273
|
+
`toast` singleton). Default placement is `bottom-end`; timers pause while the tab is hidden.
|
|
@@ -18,7 +18,7 @@ col.columns([col.accessor('id', { header: 'ID' })]);
|
|
|
18
18
|
Common options:
|
|
19
19
|
|
|
20
20
|
- `header: string`
|
|
21
|
-
- `type?: 'text' | 'number' | 'currency' | 'percent' | 'boolean' | 'select' | 'url' | 'phone' | 'date' | 'time' | 'date-time'`
|
|
21
|
+
- `type?: 'text' | 'number' | 'currency' | 'percent' | 'boolean' | 'select' | 'url' | 'phone' | 'date' | 'time' | 'date-time' | 'action'`
|
|
22
22
|
- `align?: 'left' | 'center' | 'right'`
|
|
23
23
|
- `size?: number`
|
|
24
24
|
- `minSize?: number`
|
|
@@ -34,7 +34,9 @@ Common options:
|
|
|
34
34
|
- `cellComponent?: Component`
|
|
35
35
|
- `cellProps?: (value, row) => Record<string, unknown>`
|
|
36
36
|
- `cellSnippet?: Snippet`
|
|
37
|
-
- `pinned?: 'left' | 'right'`
|
|
37
|
+
- `pinned?: 'left' | 'right'` — sticky during horizontal scroll. Inside a `col.group(...)`, pinning
|
|
38
|
+
a member column also pins the group header fragment above it, so the label stays visible while
|
|
39
|
+
the rest of that group scrolls.
|
|
38
40
|
- `grow?: boolean`
|
|
39
41
|
- `sum?: boolean`
|
|
40
42
|
- `footer?: (values: unknown[]) => string | number | undefined`
|
|
@@ -49,5 +51,77 @@ Type defaults:
|
|
|
49
51
|
- `boolean`: center aligned, size `90`
|
|
50
52
|
- `url`: center aligned, sorting disabled, size `60`
|
|
51
53
|
- `phone`: left aligned, size `160`
|
|
54
|
+
- `action`: center aligned, size `60`, sorting/hiding/column-filter all disabled — layout-only,
|
|
55
|
+
carries no value to format or filter. Supply a `cellComponent`/`cellSnippet` for the row action
|
|
56
|
+
buttons; the defaults keep the actions column out of the column-visibility menu and filter
|
|
57
|
+
toolbar automatically.
|
|
52
58
|
|
|
53
59
|
Use one `grow: true` column to absorb extra horizontal space.
|
|
60
|
+
|
|
61
|
+
## Locale for number/currency/percent
|
|
62
|
+
|
|
63
|
+
Priority: `formatLocale` prop → `useLocaleContext().locale` → browser default. Wrap the app in
|
|
64
|
+
`<LocaleProvider locale="sr-RS">` to set the context locale for every formatted column.
|
|
65
|
+
|
|
66
|
+
## Custom cellComponent
|
|
67
|
+
|
|
68
|
+
```svelte
|
|
69
|
+
<!-- PriceCell.svelte -->
|
|
70
|
+
<script lang="ts">
|
|
71
|
+
let { value, prices }: { value: number; prices: { vendor: string; price: number }[] } = $props();
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<span>{value}</span>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
col.accessorFn((row) => Math.min(...row.prices.map((p) => p.price)), {
|
|
79
|
+
id: 'minPrice',
|
|
80
|
+
header: 'Best Price',
|
|
81
|
+
type: 'currency',
|
|
82
|
+
align: 'right',
|
|
83
|
+
cellComponent: PriceCell,
|
|
84
|
+
cellProps: (value, row) => ({ value, prices: row.prices })
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Row height and cell alignment
|
|
89
|
+
|
|
90
|
+
Body cells are fixed at `h-9` (36px), matching the header, with content vertically centered — any
|
|
91
|
+
control up to 36px tall (every `Button` `icon-xs`/`icon-sm`/`icon`/`icon-lg` size) fits without
|
|
92
|
+
changing row height. Do **not** add negative margins (e.g. `-my-1`) to cell components to
|
|
93
|
+
compensate for row growth — in the virtualized table that fights the virtualizer's 37px row
|
|
94
|
+
estimate. Cells are `truncate` (single-line, `overflow: hidden`), so give action columns a little
|
|
95
|
+
extra `size` if a focus ring on a control near the edge gets clipped.
|
|
96
|
+
|
|
97
|
+
## DataTable.Search
|
|
98
|
+
|
|
99
|
+
Global filter input searching across all text columns, debounced (default 300ms).
|
|
100
|
+
|
|
101
|
+
- `table: DataTableInstance<T>` — required
|
|
102
|
+
- `placeholder?: string` (default `'Search...'`)
|
|
103
|
+
- `class?: ClassValue`
|
|
104
|
+
- `debounceMs?: number` (default `300`)
|
|
105
|
+
|
|
106
|
+
Text/number columns stay searchable even when the first row's value is `null`/`undefined`;
|
|
107
|
+
booleans and object-valued columns are excluded from global search. Also available as
|
|
108
|
+
`VirtualDataTable.Search` for the virtualized variant.
|
|
109
|
+
|
|
110
|
+
## DataTable.Root props
|
|
111
|
+
|
|
112
|
+
`table: DataTableInstance<T>` (required), `caption?: string` (sr-only `<caption>`),
|
|
113
|
+
`emptyMessage?: string` (default `'No rows found'`), `class?: ClassValue`,
|
|
114
|
+
`onRowClick?: (details: { row: T; event: MouseEvent }) => void`,
|
|
115
|
+
`onRowDoubleClick?: (details: { row: T; event: MouseEvent }) => void` — `row` is `row.original`.
|
|
116
|
+
Clicks on the row-selection checkbox and url-cell buttons do **not** trigger either handler.
|
|
117
|
+
`VirtualDataTable.Root` shares the same props.
|
|
118
|
+
|
|
119
|
+
```svelte
|
|
120
|
+
<DataTable.Root
|
|
121
|
+
{table}
|
|
122
|
+
onRowDoubleClick={({ row, event }) => {
|
|
123
|
+
if (event.ctrlKey || event.metaKey) openInNewTab(row.id);
|
|
124
|
+
else navigateTo(row.id);
|
|
125
|
+
}}
|
|
126
|
+
/>
|
|
127
|
+
```
|