compote-ui 0.74.2 → 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/dist/index.d.ts CHANGED
@@ -77,6 +77,8 @@ export { getLocalTimeZone, parseAbsolute, parseDateTime } from '@internationaliz
77
77
  export type { DateValue } from '@internationalized/date';
78
78
  export { Portal } from '@ark-ui/svelte/portal';
79
79
  export type { PortalProps } from '@ark-ui/svelte/portal';
80
+ export { FormatByte, FormatNumber, FormatRelativeTime, FormatTime } from '@ark-ui/svelte/format';
81
+ export type { FormatByteProps, FormatNumberProps, FormatRelativeTimeProps, FormatTimeProps } from '@ark-ui/svelte/format';
80
82
  export { createListCollection, createTreeCollection } from './utils/collections';
81
83
  export type { ListItem, TreeItem } from './utils/collections';
82
84
  export type { ListCollection, TreeCollection } from '@ark-ui/svelte/collection';
package/dist/index.js CHANGED
@@ -59,6 +59,7 @@ export { default as TreeView } from './components/tree-view/tree-view.svelte';
59
59
  export { LocaleProvider, useLocaleContext } from '@ark-ui/svelte/locale';
60
60
  export { getLocalTimeZone, parseAbsolute, parseDateTime } from '@internationalized/date';
61
61
  export { Portal } from '@ark-ui/svelte/portal';
62
+ export { FormatByte, FormatNumber, FormatRelativeTime, FormatTime } from '@ark-ui/svelte/format';
62
63
  export { createListCollection, createTreeCollection } from './utils/collections';
63
64
  export { PersistedState, Debounced } from 'runed';
64
65
  export { cn } from 'tailwind-variants';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "compote-ui",
3
- "version": "0.74.2",
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.3.7",
69
- "@tanstack/svelte-table": "9.2.3",
68
+ "@tanstack/intent": "^0.4.0",
69
+ "@tanstack/svelte-table": "9.2.4",
70
70
  "@types/node": "^22.20.0",
71
- "eslint": "^10.9.1",
71
+ "eslint": "^10.10.0",
72
72
  "eslint-config-prettier": "^10.1.8",
73
73
  "eslint-plugin-svelte": "^3.23.0",
74
- "globals": "^17.11.0",
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.56.10",
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.68.0",
84
+ "typescript-eslint": "^8.70.0",
85
85
  "unplugin-icons": "^23.0.1",
86
- "vite": "^8.2.2",
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.0",
94
+ "@ark-ui/svelte": "^5.24.1",
95
95
  "@fontsource-variable/nunito-sans": "^5.2.7",
96
- "@internationalized/date": "^3.12.3",
97
- "@tanstack/svelte-virtual": "^3.13.36",
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.62.1'
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: `C:/Users/tihom/.claude/skills/compote-ui/references/button.md`
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
- - `ImageCropper`, `ImageCropDialog`
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
- List controls use `{ value, label }` items:
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
- Use `DateField` as the default accessible date input:
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
- <script lang="ts">
68
- import type { DateValue } from 'compote-ui';
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
- let value = $state<DateValue | null>(null);
71
- </script>
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
- <DateField label="Date" bind:value />
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
- Use `DateRangeField` for ranges:
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
- import type { DateValue } from 'compote-ui';
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
- let range = $state<DateValue[]>([]);
142
+ ```svelte
143
+ <script lang="ts">
144
+ let value = $state(['Svelte', 'React']); // predefined tags
83
145
  </script>
84
146
 
85
- <DateRangeField label="Range" bind:value={range} />
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
- Use `DateInput` when binding existing `string`, `Date`, or `DateValue` values without a calendar popup.
187
+ ## PhoneInput
89
188
 
90
- `PhoneInput` stores E.164 values:
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(null);
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
- `Listbox.Content` exposes filtered `items` and grouped `group` snippet data:
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
- Tabs:
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
- For nested splitters, use Ark UI directly with one shared registry:
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
- Drawer content renders backdrop and positioner internally:
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
- AlertDialog is for confirmations:
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
- Toast setup belongs in layout:
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.
@@ -8,7 +8,7 @@ description: >
8
8
  metadata:
9
9
  type: composition
10
10
  library: compote-ui
11
- library_version: '0.62.1'
11
+ library_version: '0.74.2'
12
12
  requires:
13
13
  - component-usage
14
14
  - theming
@@ -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
+ ```
@@ -7,7 +7,7 @@ description: >
7
7
  metadata:
8
8
  type: lifecycle
9
9
  library: compote-ui
10
- library_version: '0.62.1'
10
+ library_version: '0.74.2'
11
11
  sources:
12
12
  - package.json
13
13
  - README.md
@@ -8,7 +8,7 @@ description: >
8
8
  metadata:
9
9
  type: lifecycle
10
10
  library: compote-ui
11
- library_version: '0.62.1'
11
+ library_version: '0.74.2'
12
12
  requires:
13
13
  - getting-started
14
14
  - component-usage
@@ -7,7 +7,7 @@ description: >
7
7
  metadata:
8
8
  type: core
9
9
  library: compote-ui
10
- library_version: '0.62.1'
10
+ library_version: '0.74.2'
11
11
  sources:
12
12
  - src/lib/theme.css
13
13
  - src/routes/layout.css