@lyeve-labs/ui-kit 0.12.1 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/dist/components/Autocomplete.svelte +191 -125
  2. package/dist/components/Autocomplete.svelte.d.ts +29 -8
  3. package/dist/components/Card.svelte +43 -2
  4. package/dist/components/Card.svelte.d.ts +24 -2
  5. package/dist/components/Checkbox.svelte +174 -63
  6. package/dist/components/Checkbox.svelte.d.ts +20 -3
  7. package/dist/components/CheckboxGroup.svelte +162 -0
  8. package/dist/components/CheckboxGroup.svelte.d.ts +51 -0
  9. package/dist/components/Collapsible.svelte +142 -0
  10. package/dist/components/Collapsible.svelte.d.ts +32 -0
  11. package/dist/components/CopyButton.svelte +126 -0
  12. package/dist/components/CopyButton.svelte.d.ts +14 -0
  13. package/dist/components/DatePicker.svelte +48 -6
  14. package/dist/components/DateTimePicker.svelte +337 -0
  15. package/dist/components/DateTimePicker.svelte.d.ts +26 -0
  16. package/dist/components/DescriptionList.svelte +78 -0
  17. package/dist/components/DescriptionList.svelte.d.ts +34 -0
  18. package/dist/components/Field.svelte +104 -0
  19. package/dist/components/Field.svelte.d.ts +46 -0
  20. package/dist/components/FileInput.svelte +5 -2
  21. package/dist/components/FormMessage.svelte +85 -0
  22. package/dist/components/FormMessage.svelte.d.ts +11 -0
  23. package/dist/components/Input.svelte +1 -1
  24. package/dist/components/Label.svelte +7 -1
  25. package/dist/components/Label.svelte.d.ts +6 -0
  26. package/dist/components/MultiSelect.svelte +199 -109
  27. package/dist/components/MultiSelect.svelte.d.ts +22 -9
  28. package/dist/components/NumberInput.svelte +8 -4
  29. package/dist/components/PageHeader.svelte +37 -4
  30. package/dist/components/PageHeader.svelte.d.ts +15 -0
  31. package/dist/components/PageShell.svelte +85 -0
  32. package/dist/components/PageShell.svelte.d.ts +38 -0
  33. package/dist/components/Pagination.svelte +43 -7
  34. package/dist/components/Panel.svelte +101 -0
  35. package/dist/components/Panel.svelte.d.ts +39 -0
  36. package/dist/components/PasswordInput.svelte +139 -0
  37. package/dist/components/PasswordInput.svelte.d.ts +29 -0
  38. package/dist/components/Radio.svelte +152 -35
  39. package/dist/components/Radio.svelte.d.ts +16 -1
  40. package/dist/components/RadioGroup.svelte +118 -71
  41. package/dist/components/RadioGroup.svelte.d.ts +39 -9
  42. package/dist/components/SectionHeading.svelte +39 -0
  43. package/dist/components/SectionHeading.svelte.d.ts +21 -0
  44. package/dist/components/SegmentedControl.svelte +194 -0
  45. package/dist/components/SegmentedControl.svelte.d.ts +55 -0
  46. package/dist/components/Select.svelte +471 -46
  47. package/dist/components/Select.svelte.d.ts +95 -6
  48. package/dist/components/SidebarNav.svelte +264 -0
  49. package/dist/components/SidebarNav.svelte.d.ts +17 -0
  50. package/dist/components/Stat.svelte +53 -2
  51. package/dist/components/Stat.svelte.d.ts +31 -0
  52. package/dist/components/Textarea.svelte +1 -1
  53. package/dist/components/TimePicker.svelte +480 -0
  54. package/dist/components/TimePicker.svelte.d.ts +23 -0
  55. package/dist/components/Toolbar.svelte +39 -0
  56. package/dist/components/Toolbar.svelte.d.ts +26 -0
  57. package/dist/components/TreeView.svelte +339 -0
  58. package/dist/components/TreeView.svelte.d.ts +37 -0
  59. package/dist/index.d.ts +25 -1
  60. package/dist/index.js +20 -1
  61. package/dist/internal/calendar.d.ts +119 -0
  62. package/dist/internal/calendar.js +225 -0
  63. package/dist/internal/choice.d.ts +136 -0
  64. package/dist/internal/choice.js +179 -0
  65. package/dist/internal/field.d.ts +39 -0
  66. package/dist/internal/field.js +48 -0
  67. package/dist/internal/filter.d.ts +80 -0
  68. package/dist/internal/filter.js +80 -0
  69. package/dist/internal/layout.d.ts +119 -0
  70. package/dist/internal/layout.js +132 -0
  71. package/dist/internal/listbox.svelte.d.ts +77 -0
  72. package/dist/internal/listbox.svelte.js +438 -0
  73. package/dist/internal/nav-expansion.svelte.d.ts +36 -0
  74. package/dist/internal/nav-expansion.svelte.js +144 -0
  75. package/dist/internal/nav-tree.d.ts +68 -0
  76. package/dist/internal/nav-tree.js +102 -0
  77. package/dist/internal/panel.d.ts +100 -0
  78. package/dist/internal/panel.js +109 -0
  79. package/dist/internal/rollup.d.ts +52 -0
  80. package/dist/internal/rollup.js +67 -0
  81. package/dist/internal/time.d.ts +103 -0
  82. package/dist/internal/time.js +166 -0
  83. package/dist/internal/tree.d.ts +86 -0
  84. package/dist/internal/tree.js +111 -0
  85. package/dist/styles/theme.css +18 -10
  86. package/package.json +4 -2
  87. package/src/lib/styles/theme.css +18 -10
@@ -0,0 +1,339 @@
1
+ <script lang="ts" generics="T = unknown">
2
+ import { ChevronRight } from '@lucide/svelte';
3
+ import { CHOICE_MARK, choiceBox } from '../internal/choice.js';
4
+ import {
5
+ branchState,
6
+ flattenTree,
7
+ isBranch,
8
+ toggleSubtree,
9
+ type TreeNode,
10
+ type TriState,
11
+ } from '../internal/tree.js';
12
+
13
+ interface Props {
14
+ nodes: TreeNode<T>[];
15
+ /** Required. An unnamed tree announces as just "tree". */
16
+ label: string;
17
+ expanded?: string[];
18
+ selected?: string;
19
+ /** Checked LEAF ids only. A branch's state is always derived, never stored, so the two cannot disagree. */
20
+ checked?: string[];
21
+ checkable?: boolean;
22
+ class?: string;
23
+ onactivate?: (node: TreeNode<T>) => void;
24
+ oncheck?: (checked: string[]) => void;
25
+ }
26
+
27
+ let {
28
+ nodes,
29
+ label,
30
+ expanded = $bindable([]),
31
+ selected = $bindable(undefined),
32
+ checked = $bindable([]),
33
+ checkable = false,
34
+ class: klass = '',
35
+ onactivate,
36
+ oncheck,
37
+ }: Props = $props();
38
+
39
+ let rootEl: HTMLUListElement | undefined = $state();
40
+
41
+ /** The row the roving tabindex sits on. Undefined until the user lands somewhere. */
42
+ let activeId: string | undefined = $state(undefined);
43
+
44
+ const expandedSet = $derived(new Set(expanded));
45
+ const checkedSet = $derived(new Set(checked));
46
+
47
+ /**
48
+ * The visible rows, which is both the paint order and the arrow-key order.
49
+ * A collapsed branch contributes no children here, so ArrowDown cannot land
50
+ * on a row the user cannot see.
51
+ */
52
+ const rows = $derived(flattenTree(nodes, expandedSet));
53
+
54
+ /**
55
+ * Exactly one row is in the tab sequence, and the arrows move it.
56
+ *
57
+ * A tree is one stop, not one stop per row: a hundred-node tree with a
58
+ * tabbable row each is a hundred presses to get past. The fallback chain
59
+ * matters as much as the rule. The active row can be gone after a collapse
60
+ * or a data change, so the tabbable row falls back to the selected row and
61
+ * then to the first, and a tree with no row the keyboard can reach is a tree
62
+ * nobody can enter.
63
+ */
64
+ const tabbableId = $derived(
65
+ rows.find((r) => r.node.id === activeId)?.node.id ??
66
+ rows.find((r) => r.node.id === selected)?.node.id ??
67
+ rows[0]?.node.id,
68
+ );
69
+
70
+ /** Typeahead buffer. Not state: nothing renders it, and it clears on a timer. */
71
+ let typed = '';
72
+ let typedTimer: ReturnType<typeof setTimeout> | undefined;
73
+
74
+ $effect(() => () => clearTimeout(typedTimer));
75
+
76
+ function ariaChecked(state: TriState): 'true' | 'false' | 'mixed' {
77
+ if (state === 'all') return 'true';
78
+ return state === 'some' ? 'mixed' : 'false';
79
+ }
80
+
81
+ function rowPaint(isSelected: boolean, disabled: boolean): string {
82
+ // One utility per property in each branch. A resting `hover:bg-surface-2`
83
+ // that a selected row overrides with `bg-surface-2` resolves by the order
84
+ // Tailwind emits the two, not by the order they are written here.
85
+ if (disabled) return 'cursor-not-allowed opacity-50';
86
+ return isSelected
87
+ ? 'cursor-pointer bg-surface-2 font-medium text-brand'
88
+ : 'cursor-pointer hover:bg-surface-2';
89
+ }
90
+
91
+ function rowElement(id: string): HTMLElement | undefined {
92
+ if (!rootEl) return undefined;
93
+ return [...rootEl.querySelectorAll<HTMLElement>('[role="treeitem"]')].find(
94
+ (el) => el.dataset.treeId === id,
95
+ );
96
+ }
97
+
98
+ function focusRow(id: string | undefined): void {
99
+ if (id === undefined) return;
100
+ activeId = id;
101
+ rowElement(id)?.focus();
102
+ }
103
+
104
+ function setOpen(node: TreeNode<T>, open: boolean): void {
105
+ if (!isBranch(node)) return;
106
+ const next = new Set(expanded);
107
+ if (open) next.add(node.id);
108
+ else next.delete(node.id);
109
+ expanded = [...next];
110
+ }
111
+
112
+ function toggleOpen(node: TreeNode<T>): void {
113
+ setOpen(node, !expandedSet.has(node.id));
114
+ }
115
+
116
+ function activate(node: TreeNode<T>): void {
117
+ if (node.disabled) return;
118
+ selected = node.id;
119
+ onactivate?.(node);
120
+ }
121
+
122
+ function toggleCheck(node: TreeNode<T>): void {
123
+ if (!checkable || node.disabled) return;
124
+ const next = new Set(checked);
125
+ const { add, remove } = toggleSubtree(node, next);
126
+ // A branch with no enabled leaf has nothing to write, and reporting a
127
+ // change that did not happen is how a consumer ends up saving a list it was
128
+ // never given.
129
+ if (add.length === 0 && remove.length === 0) return;
130
+ for (const id of add) next.add(id);
131
+ for (const id of remove) next.delete(id);
132
+ checked = [...next];
133
+ oncheck?.(checked);
134
+ }
135
+
136
+ function typeahead(key: string, fromId: string): void {
137
+ clearTimeout(typedTimer);
138
+ typedTimer = setTimeout(() => {
139
+ typed = '';
140
+ }, 500);
141
+ typed += key.toLowerCase();
142
+
143
+ const start = rows.findIndex((r) => r.node.id === fromId);
144
+ // A single character looks past the current row, so pressing it again walks
145
+ // the matches. A longer buffer includes the current row, or refining a
146
+ // search on the row it already found would jump off it.
147
+ const from = typed.length === 1 ? start + 1 : start;
148
+ for (let step = 0; step < rows.length; step++) {
149
+ const candidate = rows[(from + step + rows.length) % rows.length];
150
+ if (candidate.node.label.toLowerCase().startsWith(typed)) {
151
+ focusRow(candidate.node.id);
152
+ return;
153
+ }
154
+ }
155
+ }
156
+
157
+ function onRowClick(e: MouseEvent, node: TreeNode<T>): void {
158
+ const target = e.target as Element | null;
159
+ // Every row is nested inside its ancestors, so a click on a child bubbles
160
+ // through all of them. A row answers only for a target that is its own.
161
+ if (!target || target.closest('[role="treeitem"]') !== e.currentTarget) return;
162
+
163
+ activeId = node.id;
164
+ if (target.closest('[data-tree-toggle]')) {
165
+ toggleOpen(node);
166
+ return;
167
+ }
168
+ if (checkable && target.closest('[data-tree-check]')) {
169
+ toggleCheck(node);
170
+ return;
171
+ }
172
+ // A branch that does not open when its row is clicked reads as broken, so
173
+ // the row body opens it as well as activating it.
174
+ if (isBranch(node)) toggleOpen(node);
175
+ activate(node);
176
+ }
177
+
178
+ function onRowKeydown(e: KeyboardEvent, node: TreeNode<T>, parentId: string | undefined): void {
179
+ // Keydown bubbles the same way a click does, and only the focused row is
180
+ // the target.
181
+ if (e.target !== e.currentTarget) return;
182
+
183
+ const index = rows.findIndex((r) => r.node.id === node.id);
184
+ const open = expandedSet.has(node.id);
185
+
186
+ switch (e.key) {
187
+ case 'ArrowDown':
188
+ e.preventDefault();
189
+ focusRow(rows[index + 1]?.node.id);
190
+ return;
191
+ case 'ArrowUp':
192
+ e.preventDefault();
193
+ focusRow(rows[index - 1]?.node.id);
194
+ return;
195
+ case 'Home':
196
+ e.preventDefault();
197
+ focusRow(rows[0]?.node.id);
198
+ return;
199
+ case 'End':
200
+ e.preventDefault();
201
+ focusRow(rows[rows.length - 1]?.node.id);
202
+ return;
203
+ case 'ArrowRight':
204
+ e.preventDefault();
205
+ // Opening and descending are two presses. The first press has to leave
206
+ // focus where it is, or a screen reader announces the child before its
207
+ // parent has said it is now open.
208
+ if (!isBranch(node)) return;
209
+ if (!open) setOpen(node, true);
210
+ else focusRow(node.children?.[0]?.id);
211
+ return;
212
+ case 'ArrowLeft':
213
+ e.preventDefault();
214
+ if (isBranch(node) && open) setOpen(node, false);
215
+ else focusRow(parentId);
216
+ return;
217
+ case 'Enter':
218
+ e.preventDefault();
219
+ activate(node);
220
+ return;
221
+ case ' ':
222
+ // Space never reaches the typeahead below. One key that both toggles a
223
+ // check and activates a row leaves the user unable to do either on
224
+ // purpose, so a checkable tree gives Space to the check and keeps
225
+ // Enter for activation.
226
+ e.preventDefault();
227
+ if (checkable) toggleCheck(node);
228
+ else activate(node);
229
+ return;
230
+ default:
231
+ if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
232
+ e.preventDefault();
233
+ typeahead(e.key, node.id);
234
+ }
235
+ }
236
+ }
237
+ </script>
238
+
239
+ {#snippet branch(list: TreeNode<T>[], depth: number, parentId: string | undefined)}
240
+ {#each list as node, i (node.id)}
241
+ {@const open = expandedSet.has(node.id)}
242
+ {@const state = branchState(node, checkedSet)}
243
+ <!--
244
+ The focus ring is stated once, on the element that holds the role and the
245
+ focus, and outside every selected and checked branch. A ring inside the
246
+ selected ternary disappears in the state it was meant to mark, and a
247
+ selected row already carries a brand text colour that a border-only focus
248
+ state would have nothing to add to. Inset, because a tree of any size
249
+ lives in a scrolling container that would crop an outset ring.
250
+ -->
251
+ <li
252
+ role="treeitem"
253
+ data-tree-id={node.id}
254
+ tabindex={node.id === tabbableId ? 0 : -1}
255
+ aria-level={depth + 1}
256
+ aria-setsize={list.length}
257
+ aria-posinset={i + 1}
258
+ aria-expanded={isBranch(node) ? open : undefined}
259
+ aria-selected={selected === node.id}
260
+ aria-checked={checkable ? ariaChecked(state) : undefined}
261
+ aria-disabled={node.disabled ? 'true' : undefined}
262
+ class="rounded-md outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-brand"
263
+ onclick={(e) => onRowClick(e, node)}
264
+ onkeydown={(e) => onRowKeydown(e, node, parentId)}
265
+ onfocus={() => (activeId = node.id)}
266
+ >
267
+ <!--
268
+ Indentation is a style attribute reading the spacing tokens. A class
269
+ built by interpolating the depth matches no candidate in Tailwind's
270
+ scan, so no rule is generated and every row draws flush left.
271
+ -->
272
+ <div
273
+ class="flex w-full items-center gap-2 rounded-md py-1.5 pr-2 text-sm transition-colors duration-150 {rowPaint(
274
+ selected === node.id,
275
+ !!node.disabled,
276
+ )}"
277
+ style="padding-left: calc(var(--spacing-inline) + {depth} * var(--spacing-stack))"
278
+ >
279
+ {#if isBranch(node)}
280
+ <span
281
+ data-tree-toggle
282
+ class="flex h-4 w-4 shrink-0 items-center justify-center text-faint"
283
+ >
284
+ <ChevronRight
285
+ size={14}
286
+ class="transition-transform duration-150 {open ? 'rotate-90' : ''}"
287
+ />
288
+ </span>
289
+ {:else}
290
+ <span class="h-4 w-4 shrink-0"></span>
291
+ {/if}
292
+
293
+ {#if checkable}
294
+ <!--
295
+ A painted box rather than an input: the treeitem itself carries
296
+ aria-checked, and a focusable control inside the row would put a
297
+ second tab stop in a widget whose whole point is that it has one.
298
+ -->
299
+ <span data-tree-check class="flex shrink-0 items-center">
300
+ <span class={choiceBox('checkbox', 'sm', state === 'all', state === 'some')}>
301
+ {#if state !== 'none'}
302
+ <svg width="10" height="8" viewBox="0 0 10 8" fill="none" aria-hidden="true">
303
+ <path
304
+ d={state === 'all' ? CHOICE_MARK.check : CHOICE_MARK.mixed}
305
+ stroke="currentColor"
306
+ stroke-width="1.5"
307
+ stroke-linecap="round"
308
+ stroke-linejoin="round"
309
+ />
310
+ </svg>
311
+ {/if}
312
+ </span>
313
+ </span>
314
+ {/if}
315
+
316
+ {#if node.icon}
317
+ {@const Icon = node.icon}
318
+ <Icon size={14} class="shrink-0 text-faint" />
319
+ {/if}
320
+
321
+ <span class="truncate">{node.label}</span>
322
+
323
+ {#if node.badge !== undefined}
324
+ <span class="ml-auto shrink-0 pl-2 text-xs text-faint">{node.badge}</span>
325
+ {/if}
326
+ </div>
327
+
328
+ {#if isBranch(node) && open}
329
+ <ul role="group">
330
+ {@render branch(node.children ?? [], depth + 1, node.id)}
331
+ </ul>
332
+ {/if}
333
+ </li>
334
+ {/each}
335
+ {/snippet}
336
+
337
+ <ul bind:this={rootEl} role="tree" aria-label={label} class="text-fg select-none {klass}">
338
+ {@render branch(nodes, 0, undefined)}
339
+ </ul>
@@ -0,0 +1,37 @@
1
+ import { type TreeNode } from '../internal/tree.js';
2
+ declare function $$render<T = unknown>(): {
3
+ props: {
4
+ nodes: TreeNode<T>[];
5
+ /** Required. An unnamed tree announces as just "tree". */
6
+ label: string;
7
+ expanded?: string[];
8
+ selected?: string;
9
+ /** Checked LEAF ids only. A branch's state is always derived, never stored, so the two cannot disagree. */
10
+ checked?: string[];
11
+ checkable?: boolean;
12
+ class?: string;
13
+ onactivate?: (node: TreeNode<T>) => void;
14
+ oncheck?: (checked: string[]) => void;
15
+ };
16
+ exports: {};
17
+ bindings: "expanded" | "selected" | "checked";
18
+ slots: {};
19
+ events: {};
20
+ };
21
+ declare class __sveltets_Render<T = unknown> {
22
+ props(): ReturnType<typeof $$render<T>>['props'];
23
+ events(): ReturnType<typeof $$render<T>>['events'];
24
+ slots(): ReturnType<typeof $$render<T>>['slots'];
25
+ bindings(): "expanded" | "selected" | "checked";
26
+ exports(): {};
27
+ }
28
+ interface $$IsomorphicComponent {
29
+ new <T = unknown>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
30
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
31
+ } & ReturnType<__sveltets_Render<T>['exports']>;
32
+ <T = unknown>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
33
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
34
+ }
35
+ declare const TreeView: $$IsomorphicComponent;
36
+ type TreeView<T = unknown> = InstanceType<typeof TreeView<T>>;
37
+ export default TreeView;
package/dist/index.d.ts CHANGED
@@ -7,32 +7,55 @@
7
7
  * is an implementation detail.
8
8
  */
9
9
  export { default as Card } from './components/Card.svelte';
10
+ export { default as Panel } from './components/Panel.svelte';
11
+ export { default as PageShell } from './components/PageShell.svelte';
10
12
  export { default as PageHeader } from './components/PageHeader.svelte';
13
+ export { default as SectionHeading } from './components/SectionHeading.svelte';
11
14
  export { default as Divider } from './components/Divider.svelte';
12
15
  export { default as Accordion } from './components/Accordion.svelte';
13
16
  export { default as AccordionItem } from './components/AccordionItem.svelte';
17
+ export { default as Collapsible } from './components/Collapsible.svelte';
14
18
  export { default as Table } from './components/Table.svelte';
19
+ export { default as DescriptionList } from './components/DescriptionList.svelte';
20
+ export { default as Toolbar } from './components/Toolbar.svelte';
21
+ export { default as TreeView } from './components/TreeView.svelte';
22
+ export type { PageWidth } from './internal/layout.js';
23
+ export type { TreeNode, TriState } from './internal/tree.js';
15
24
  export { default as Button } from './components/Button.svelte';
16
25
  export { default as ButtonGroup } from './components/ButtonGroup.svelte';
17
26
  export { default as Input } from './components/Input.svelte';
27
+ export { default as PasswordInput } from './components/PasswordInput.svelte';
18
28
  export { default as Textarea } from './components/Textarea.svelte';
19
29
  export { default as NumberInput } from './components/NumberInput.svelte';
20
30
  export { default as SearchInput } from './components/SearchInput.svelte';
21
31
  export { default as FileInput } from './components/FileInput.svelte';
22
32
  export { default as Label } from './components/Label.svelte';
33
+ export { default as Field } from './components/Field.svelte';
34
+ export { default as FormMessage } from './components/FormMessage.svelte';
35
+ export { default as SegmentedControl } from './components/SegmentedControl.svelte';
23
36
  export { default as Select } from './components/Select.svelte';
24
37
  export { default as MultiSelect } from './components/MultiSelect.svelte';
25
38
  export { default as Autocomplete } from './components/Autocomplete.svelte';
26
39
  export { default as DatePicker } from './components/DatePicker.svelte';
40
+ export { default as TimePicker } from './components/TimePicker.svelte';
41
+ export { default as DateTimePicker } from './components/DateTimePicker.svelte';
27
42
  export { default as Checkbox } from './components/Checkbox.svelte';
43
+ export { default as CheckboxGroup } from './components/CheckboxGroup.svelte';
28
44
  export { default as Radio } from './components/Radio.svelte';
29
45
  export { default as RadioGroup } from './components/RadioGroup.svelte';
30
46
  export { default as Toggle } from './components/Toggle.svelte';
47
+ export type { ChoiceSize, ChoiceVariant, ChoiceOrientation } from './internal/choice.js';
48
+ export type { ChoiceOption } from './components/CheckboxGroup.svelte';
49
+ export type { SelectOption, SelectChangeEvent } from './components/Select.svelte';
50
+ export type { ListOption } from './components/Autocomplete.svelte';
51
+ export type { FilterFn, FilterInput, FilterContext } from './internal/filter.js';
31
52
  export { default as Breadcrumb } from './components/Breadcrumb.svelte';
32
53
  export { default as Tabs } from './components/Tabs.svelte';
33
54
  export { default as Pagination } from './components/Pagination.svelte';
34
55
  export { default as StepIndicator } from './components/StepIndicator.svelte';
35
56
  export { default as Dropdown } from './components/Dropdown.svelte';
57
+ export { default as SidebarNav } from './components/SidebarNav.svelte';
58
+ export type { NavNode, NavTree } from './internal/nav-tree.js';
36
59
  export { default as Modal } from './components/Modal.svelte';
37
60
  export { default as Drawer } from './components/Drawer.svelte';
38
61
  export { default as Tooltip } from './components/Tooltip.svelte';
@@ -47,6 +70,7 @@ export { default as Skeleton } from './components/Skeleton.svelte';
47
70
  export { default as EmptyState } from './components/EmptyState.svelte';
48
71
  export { default as Stat } from './components/Stat.svelte';
49
72
  export { default as Kbd } from './components/Kbd.svelte';
73
+ export { default as CopyButton } from './components/CopyButton.svelte';
50
74
  export { default as Avatar } from './components/Avatar.svelte';
51
75
  export { default as AvatarGroup } from './components/AvatarGroup.svelte';
52
76
  export { default as ThemeToggle } from './components/ThemeToggle.svelte';
@@ -60,4 +84,4 @@ export { openDialog, closeDialog, dismissDialog, dismissAllDialogs, confirm, set
60
84
  export type { DialogOptions, DialogEntry, DialogSize } from './components/dialog/types.js';
61
85
  export { cn, type ClassValue } from './utils/cn.js';
62
86
  export { getTheme, setTheme, toggleTheme, themeBootScript, type Theme } from './utils/theme.js';
63
- export declare const VERSION = "0.12.1";
87
+ export declare const VERSION = "0.13.1";
package/dist/index.js CHANGED
@@ -8,25 +8,42 @@
8
8
  */
9
9
  // ── Layout & structure ─────────────────────────────────────────────────────
10
10
  export { default as Card } from './components/Card.svelte';
11
+ export { default as Panel } from './components/Panel.svelte';
12
+ export { default as PageShell } from './components/PageShell.svelte';
11
13
  export { default as PageHeader } from './components/PageHeader.svelte';
14
+ export { default as SectionHeading } from './components/SectionHeading.svelte';
12
15
  export { default as Divider } from './components/Divider.svelte';
13
16
  export { default as Accordion } from './components/Accordion.svelte';
14
17
  export { default as AccordionItem } from './components/AccordionItem.svelte';
18
+ export { default as Collapsible } from './components/Collapsible.svelte';
15
19
  export { default as Table } from './components/Table.svelte';
20
+ export { default as DescriptionList } from './components/DescriptionList.svelte';
21
+ export { default as Toolbar } from './components/Toolbar.svelte';
22
+ export { default as TreeView } from './components/TreeView.svelte';
16
23
  // ── Forms & inputs ─────────────────────────────────────────────────────────
17
24
  export { default as Button } from './components/Button.svelte';
18
25
  export { default as ButtonGroup } from './components/ButtonGroup.svelte';
19
26
  export { default as Input } from './components/Input.svelte';
27
+ export { default as PasswordInput } from './components/PasswordInput.svelte';
20
28
  export { default as Textarea } from './components/Textarea.svelte';
21
29
  export { default as NumberInput } from './components/NumberInput.svelte';
22
30
  export { default as SearchInput } from './components/SearchInput.svelte';
23
31
  export { default as FileInput } from './components/FileInput.svelte';
24
32
  export { default as Label } from './components/Label.svelte';
33
+ // The field furniture as a component, for a consumer composing a control the
34
+ // kit does not ship. internal/field.ts stays private, and a page that could
35
+ // not reach it hand-copied the class strings instead.
36
+ export { default as Field } from './components/Field.svelte';
37
+ export { default as FormMessage } from './components/FormMessage.svelte';
38
+ export { default as SegmentedControl } from './components/SegmentedControl.svelte';
25
39
  export { default as Select } from './components/Select.svelte';
26
40
  export { default as MultiSelect } from './components/MultiSelect.svelte';
27
41
  export { default as Autocomplete } from './components/Autocomplete.svelte';
28
42
  export { default as DatePicker } from './components/DatePicker.svelte';
43
+ export { default as TimePicker } from './components/TimePicker.svelte';
44
+ export { default as DateTimePicker } from './components/DateTimePicker.svelte';
29
45
  export { default as Checkbox } from './components/Checkbox.svelte';
46
+ export { default as CheckboxGroup } from './components/CheckboxGroup.svelte';
30
47
  export { default as Radio } from './components/Radio.svelte';
31
48
  export { default as RadioGroup } from './components/RadioGroup.svelte';
32
49
  export { default as Toggle } from './components/Toggle.svelte';
@@ -36,6 +53,7 @@ export { default as Tabs } from './components/Tabs.svelte';
36
53
  export { default as Pagination } from './components/Pagination.svelte';
37
54
  export { default as StepIndicator } from './components/StepIndicator.svelte';
38
55
  export { default as Dropdown } from './components/Dropdown.svelte';
56
+ export { default as SidebarNav } from './components/SidebarNav.svelte';
39
57
  // ── Overlays ───────────────────────────────────────────────────────────────
40
58
  export { default as Modal } from './components/Modal.svelte';
41
59
  export { default as Drawer } from './components/Drawer.svelte';
@@ -52,6 +70,7 @@ export { default as Skeleton } from './components/Skeleton.svelte';
52
70
  export { default as EmptyState } from './components/EmptyState.svelte';
53
71
  export { default as Stat } from './components/Stat.svelte';
54
72
  export { default as Kbd } from './components/Kbd.svelte';
73
+ export { default as CopyButton } from './components/CopyButton.svelte';
55
74
  // ── Media ──────────────────────────────────────────────────────────────────
56
75
  export { default as Avatar } from './components/Avatar.svelte';
57
76
  export { default as AvatarGroup } from './components/AvatarGroup.svelte';
@@ -71,4 +90,4 @@ export { getTheme, setTheme, toggleTheme, themeBootScript } from './utils/theme.
71
90
  // ── Version ────────────────────────────────────────────────────────────────
72
91
  // Generated from package.json by `pnpm version:sync`. Bump package.json, never
73
92
  // this line; the build and the test suite fail when the two disagree.
74
- export const VERSION = '0.12.1';
93
+ export const VERSION = '0.13.1';
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Month arithmetic for the date controls, done without the Date parser.
3
+ *
4
+ * The rule this module exists to enforce: no string is ever handed to the Date
5
+ * parser. `new Date('2026-01-02')` is UTC midnight by specification and
6
+ * `new Date('2026-01-02T00:00')` is local midnight, so one helper applied to a
7
+ * date field and to a datetime field lands in two different zones. Read the
8
+ * first form back through the local getters west of UTC and it is 1 January.
9
+ * That is the off by one day, and it appears only for the part of the world
10
+ * that is not on UTC, which is why it survives a developer's own testing.
11
+ *
12
+ * DatePicker binds `value` to an ISO calendar date spelled `YYYY-MM-DD`: four
13
+ * digit year, two digit zero padded month, two digit zero padded day, with the
14
+ * empty string standing for no date. Its `min` and `max` props take the same
15
+ * spelling and are compared as plain strings, which works only because that
16
+ * spelling sorts lexicographically. DateRangePicker and DateTimePicker will
17
+ * carry the same value and need the same arithmetic, and each of the three is a
18
+ * place the zone bug enters, so the arithmetic is stated once here.
19
+ *
20
+ * The numeric Date constructor is avoided as well. `new Date(24, 0, 1)` is
21
+ * 1924, because the constructor remaps years 0 through 99 into the twentieth
22
+ * century, and the year in a date field comes from a user. Everything below is
23
+ * integer arithmetic on the proleptic Gregorian calendar. The one Date in the
24
+ * file is the no argument one inside todayLocal, which is the only place a real
25
+ * clock is needed.
26
+ *
27
+ * Not exported from the package entry point - this is an implementation detail.
28
+ */
29
+ /** A calendar date with no zone and no time. m is 1-12, the month a human says, not the 0-11 the Date constructor takes. */
30
+ export interface CalendarDate {
31
+ y: number;
32
+ m: number;
33
+ d: number;
34
+ }
35
+ /**
36
+ * Reads the `YYYY-MM-DD` DatePicker binds, or null when the string is not one.
37
+ *
38
+ * Shape checks and round trips. `2024-13-45` returns null rather than silently
39
+ * becoming the 2025-02-14 those three numbers normalize to in a Date: a picker
40
+ * that accepted it would open on a month the user never typed and report a
41
+ * value they never chose. A day is rejected when its month does not have it, so
42
+ * `2024-02-30` and `2023-02-29` are both null while `2024-02-29` is a date.
43
+ * Undefined and the empty string are null too, because an unset field is the
44
+ * ordinary state of a date input and not an error.
45
+ */
46
+ export declare function parseISODate(s: string | undefined | null): CalendarDate | null;
47
+ /**
48
+ * Writes the `YYYY-MM-DD` DatePicker binds.
49
+ *
50
+ * Every part is zero padded, including the year, because the min and max
51
+ * comparison in the control is a string comparison: an unpadded `2024-1-5`
52
+ * sorts after `2024-12-01` and the bound then rejects dates inside it.
53
+ */
54
+ export declare function toISODate(date: CalendarDate): string;
55
+ /**
56
+ * Today in the viewer's own zone, read from the Date's local getters and never
57
+ * from its ISO string.
58
+ *
59
+ * `new Date().toISOString().slice(0, 10)` is the tempting one line version and
60
+ * it is wrong: it converts to UTC first, so a viewer in Jakarta gets yesterday
61
+ * until 7am and a viewer in New York gets tomorrow from the evening on. The
62
+ * date the calendar marks as today has to be the date on the wall behind the
63
+ * viewer.
64
+ */
65
+ export declare function todayLocal(): CalendarDate;
66
+ /**
67
+ * Days in a month, leap years included.
68
+ *
69
+ * A month outside 1 to 12 has no days and returns 0, so a caller bounding a day
70
+ * against it rejects the month rather than accepting a day in a month that does
71
+ * not exist.
72
+ */
73
+ export declare function daysInMonth(y: number, m: number): number;
74
+ /** Weekday of the first of the month, 0 for Sunday. */
75
+ export declare function firstWeekday(y: number, m: number): number;
76
+ /**
77
+ * Adds months, clamping the day rather than rolling into the next month.
78
+ *
79
+ * 31 January plus one month is 28 or 29 February, never 2 or 3 March. A Date
80
+ * rolls, so paging a picker forward from the 31st and back again lands the user
81
+ * on a different day than the one they were looking at, and a range whose start
82
+ * was the 31st can end up after its own end.
83
+ */
84
+ export declare function addMonths(date: CalendarDate, delta: number): CalendarDate;
85
+ /** Adds days across month and year boundaries, leap days included. */
86
+ export declare function addDays(date: CalendarDate, delta: number): CalendarDate;
87
+ /**
88
+ * Orders two dates as -1, 0 or 1.
89
+ *
90
+ * A total order, so a caller can sort and bound without constructing a Date.
91
+ * Returning the field difference would sort just as well and would report 2 for
92
+ * two years apart, which quietly breaks the caller that tests for 1.
93
+ */
94
+ export declare function compareDates(a: CalendarDate, b: CalendarDate): number;
95
+ /**
96
+ * Inclusive on both ends. Either bound may be null, meaning unbounded.
97
+ *
98
+ * Inclusive because a picker given min and max for a single allowed day has to
99
+ * offer that day. DatePicker compares its bounds as strings today, which agrees
100
+ * with this only while every bound is zero padded.
101
+ */
102
+ export declare function withinRange(date: CalendarDate, min: CalendarDate | null, max: CalendarDate | null): boolean;
103
+ /**
104
+ * The six week grid a month renders, always 42 cells so the calendar does not
105
+ * change height between months. Cells outside the viewed month carry
106
+ * `outside: true`.
107
+ *
108
+ * DatePicker pads its lead with empty spans and stops at the last day, so
109
+ * February in a year where it starts on a Sunday draws four rows and a 31 day
110
+ * month starting on a Saturday draws six. The popover resizes as the user pages
111
+ * through the year and the day under the pointer moves out from under it. Fixed
112
+ * at 42 because six weeks is the most any month can touch.
113
+ *
114
+ * `weekStartsOn` is 0 for Sunday and 1 for Monday.
115
+ */
116
+ export declare function monthGrid(y: number, m: number, weekStartsOn?: 0 | 1): {
117
+ date: CalendarDate;
118
+ outside: boolean;
119
+ }[];