@noahbaumgartner/svelte-ui 0.0.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 (149) hide show
  1. package/README.md +65 -0
  2. package/dist/components/alert-dialog/AlertDialog.svelte +161 -0
  3. package/dist/components/alert-dialog/AlertDialog.svelte.d.ts +20 -0
  4. package/dist/components/avatar/Avatar.svelte +85 -0
  5. package/dist/components/avatar/Avatar.svelte.d.ts +12 -0
  6. package/dist/components/badge/Badge.svelte +49 -0
  7. package/dist/components/badge/Badge.svelte.d.ts +12 -0
  8. package/dist/components/breadcrumb/Breadcrumb.svelte +37 -0
  9. package/dist/components/breadcrumb/Breadcrumb.svelte.d.ts +9 -0
  10. package/dist/components/breadcrumb/BreadcrumbEllipsis.svelte +32 -0
  11. package/dist/components/breadcrumb/BreadcrumbEllipsis.svelte.d.ts +10 -0
  12. package/dist/components/breadcrumb/BreadcrumbItem.svelte +71 -0
  13. package/dist/components/breadcrumb/BreadcrumbItem.svelte.d.ts +12 -0
  14. package/dist/components/breadcrumb/BreadcrumbSeparator.svelte +18 -0
  15. package/dist/components/breadcrumb/BreadcrumbSeparator.svelte.d.ts +18 -0
  16. package/dist/components/button/Button.svelte +247 -0
  17. package/dist/components/button/Button.svelte.d.ts +24 -0
  18. package/dist/components/button-group/ButtonGroup.svelte +89 -0
  19. package/dist/components/button-group/ButtonGroup.svelte.d.ts +12 -0
  20. package/dist/components/calendar/Calendar.svelte +401 -0
  21. package/dist/components/calendar/Calendar.svelte.d.ts +17 -0
  22. package/dist/components/calendar/dates.d.ts +12 -0
  23. package/dist/components/calendar/dates.js +48 -0
  24. package/dist/components/card/Card.svelte +55 -0
  25. package/dist/components/card/Card.svelte.d.ts +10 -0
  26. package/dist/components/card/CardAction.svelte +24 -0
  27. package/dist/components/card/CardAction.svelte.d.ts +9 -0
  28. package/dist/components/card/CardContent.svelte +20 -0
  29. package/dist/components/card/CardContent.svelte.d.ts +8 -0
  30. package/dist/components/card/CardDescription.svelte +23 -0
  31. package/dist/components/card/CardDescription.svelte.d.ts +8 -0
  32. package/dist/components/card/CardFooter.svelte +28 -0
  33. package/dist/components/card/CardFooter.svelte.d.ts +9 -0
  34. package/dist/components/card/CardHeader.svelte +29 -0
  35. package/dist/components/card/CardHeader.svelte.d.ts +9 -0
  36. package/dist/components/card/CardTitle.svelte +23 -0
  37. package/dist/components/card/CardTitle.svelte.d.ts +8 -0
  38. package/dist/components/checkbox/Checkbox.svelte +93 -0
  39. package/dist/components/checkbox/Checkbox.svelte.d.ts +5 -0
  40. package/dist/components/color-picker/ColorPicker.svelte +233 -0
  41. package/dist/components/color-picker/ColorPicker.svelte.d.ts +10 -0
  42. package/dist/components/context-menu/ContextMenu.svelte +201 -0
  43. package/dist/components/context-menu/ContextMenu.svelte.d.ts +14 -0
  44. package/dist/components/context-menu/ContextMenuItem.svelte +105 -0
  45. package/dist/components/context-menu/ContextMenuItem.svelte.d.ts +14 -0
  46. package/dist/components/context-menu/context.d.ts +14 -0
  47. package/dist/components/context-menu/context.js +2 -0
  48. package/dist/components/dialog/Dialog.svelte +225 -0
  49. package/dist/components/dialog/Dialog.svelte.d.ts +22 -0
  50. package/dist/components/field/Field.svelte +78 -0
  51. package/dist/components/field/Field.svelte.d.ts +11 -0
  52. package/dist/components/field/FieldContent.svelte +25 -0
  53. package/dist/components/field/FieldContent.svelte.d.ts +9 -0
  54. package/dist/components/field/FieldDescription.svelte +34 -0
  55. package/dist/components/field/FieldDescription.svelte.d.ts +9 -0
  56. package/dist/components/field/FieldError.svelte +45 -0
  57. package/dist/components/field/FieldError.svelte.d.ts +11 -0
  58. package/dist/components/field/FieldGroup.svelte +30 -0
  59. package/dist/components/field/FieldGroup.svelte.d.ts +9 -0
  60. package/dist/components/field/FieldLegend.svelte +34 -0
  61. package/dist/components/field/FieldLegend.svelte.d.ts +10 -0
  62. package/dist/components/field/FieldSeparator.svelte +31 -0
  63. package/dist/components/field/FieldSeparator.svelte.d.ts +9 -0
  64. package/dist/components/field/FieldSet.svelte +33 -0
  65. package/dist/components/field/FieldSet.svelte.d.ts +9 -0
  66. package/dist/components/input/Input.svelte +459 -0
  67. package/dist/components/input/Input.svelte.d.ts +10 -0
  68. package/dist/components/input-group/InputGroup.svelte +88 -0
  69. package/dist/components/input-group/InputGroup.svelte.d.ts +9 -0
  70. package/dist/components/input-group/InputGroupAddon.svelte +88 -0
  71. package/dist/components/input-group/InputGroupAddon.svelte.d.ts +11 -0
  72. package/dist/components/kbd/Kbd.svelte +46 -0
  73. package/dist/components/kbd/Kbd.svelte.d.ts +8 -0
  74. package/dist/components/kbd/KbdGroup.svelte +27 -0
  75. package/dist/components/kbd/KbdGroup.svelte.d.ts +8 -0
  76. package/dist/components/label/Label.svelte +48 -0
  77. package/dist/components/label/Label.svelte.d.ts +9 -0
  78. package/dist/components/link/Link.svelte +40 -0
  79. package/dist/components/link/Link.svelte.d.ts +10 -0
  80. package/dist/components/navbar/Navbar.svelte +266 -0
  81. package/dist/components/navbar/Navbar.svelte.d.ts +26 -0
  82. package/dist/components/navigation-menu/NavigationMenu.svelte +95 -0
  83. package/dist/components/navigation-menu/NavigationMenu.svelte.d.ts +12 -0
  84. package/dist/components/navigation-menu/NavigationMenuItem.svelte +257 -0
  85. package/dist/components/navigation-menu/NavigationMenuItem.svelte.d.ts +15 -0
  86. package/dist/components/navigation-menu/NavigationMenuLink.svelte +91 -0
  87. package/dist/components/navigation-menu/NavigationMenuLink.svelte.d.ts +16 -0
  88. package/dist/components/navigation-menu/context.d.ts +15 -0
  89. package/dist/components/navigation-menu/context.js +2 -0
  90. package/dist/components/popover/Popover.svelte +295 -0
  91. package/dist/components/popover/Popover.svelte.d.ts +34 -0
  92. package/dist/components/radio-group/RadioGroup.svelte +86 -0
  93. package/dist/components/radio-group/RadioGroup.svelte.d.ts +16 -0
  94. package/dist/components/radio-group/RadioGroupItem.svelte +95 -0
  95. package/dist/components/radio-group/RadioGroupItem.svelte.d.ts +7 -0
  96. package/dist/components/radio-group/context.d.ts +9 -0
  97. package/dist/components/radio-group/context.js +2 -0
  98. package/dist/components/separator/Separator.svelte +41 -0
  99. package/dist/components/separator/Separator.svelte.d.ts +10 -0
  100. package/dist/components/sidebar/Sidebar.svelte +180 -0
  101. package/dist/components/sidebar/Sidebar.svelte.d.ts +14 -0
  102. package/dist/components/sidebar/SidebarContent.svelte +30 -0
  103. package/dist/components/sidebar/SidebarContent.svelte.d.ts +9 -0
  104. package/dist/components/sidebar/SidebarFooter.svelte +23 -0
  105. package/dist/components/sidebar/SidebarFooter.svelte.d.ts +8 -0
  106. package/dist/components/sidebar/SidebarGroup.svelte +55 -0
  107. package/dist/components/sidebar/SidebarGroup.svelte.d.ts +11 -0
  108. package/dist/components/sidebar/SidebarHeader.svelte +23 -0
  109. package/dist/components/sidebar/SidebarHeader.svelte.d.ts +8 -0
  110. package/dist/components/sidebar/SidebarInset.svelte +25 -0
  111. package/dist/components/sidebar/SidebarInset.svelte.d.ts +9 -0
  112. package/dist/components/sidebar/SidebarMenu.svelte +27 -0
  113. package/dist/components/sidebar/SidebarMenu.svelte.d.ts +9 -0
  114. package/dist/components/sidebar/SidebarMenuItem.svelte +221 -0
  115. package/dist/components/sidebar/SidebarMenuItem.svelte.d.ts +23 -0
  116. package/dist/components/sidebar/SidebarProvider.svelte +62 -0
  117. package/dist/components/sidebar/SidebarProvider.svelte.d.ts +11 -0
  118. package/dist/components/sidebar/SidebarSeparator.svelte +5 -0
  119. package/dist/components/sidebar/SidebarSeparator.svelte.d.ts +18 -0
  120. package/dist/components/sidebar/SidebarTrigger.svelte +24 -0
  121. package/dist/components/sidebar/SidebarTrigger.svelte.d.ts +7 -0
  122. package/dist/components/sidebar/context.d.ts +15 -0
  123. package/dist/components/sidebar/context.js +2 -0
  124. package/dist/components/skeleton/Skeleton.svelte +30 -0
  125. package/dist/components/skeleton/Skeleton.svelte.d.ts +5 -0
  126. package/dist/components/spinner/Spinner.svelte +72 -0
  127. package/dist/components/spinner/Spinner.svelte.d.ts +9 -0
  128. package/dist/components/switch/Switch.svelte +88 -0
  129. package/dist/components/switch/Switch.svelte.d.ts +5 -0
  130. package/dist/components/table/Table.svelte +276 -0
  131. package/dist/components/table/Table.svelte.d.ts +60 -0
  132. package/dist/components/textarea/Textarea.svelte +57 -0
  133. package/dist/components/textarea/Textarea.svelte.d.ts +4 -0
  134. package/dist/components/time-picker/TimePicker.svelte +162 -0
  135. package/dist/components/time-picker/TimePicker.svelte.d.ts +11 -0
  136. package/dist/components/toaster/Toaster.svelte +151 -0
  137. package/dist/components/toaster/Toaster.svelte.d.ts +7 -0
  138. package/dist/components/toaster/toast.svelte.d.ts +28 -0
  139. package/dist/components/toaster/toast.svelte.js +22 -0
  140. package/dist/components/toggle/Toggle.svelte +31 -0
  141. package/dist/components/toggle/Toggle.svelte.d.ts +8 -0
  142. package/dist/components/tooltip/Tooltip.svelte +224 -0
  143. package/dist/components/tooltip/Tooltip.svelte.d.ts +27 -0
  144. package/dist/icons.d.ts +2 -0
  145. package/dist/icons.js +4 -0
  146. package/dist/index.d.ts +73 -0
  147. package/dist/index.js +66 -0
  148. package/dist/styles/tokens.css +50 -0
  149. package/package.json +88 -0
@@ -0,0 +1,276 @@
1
+ <script module lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
4
+ export type TableSortValue = string | number | Date | null | undefined;
5
+
6
+ export type TableSort = {
7
+ /** The column's `id`, falling back to its `key`, then its `header`. */
8
+ column: string;
9
+ direction: 'asc' | 'desc';
10
+ };
11
+
12
+ export type TableColumn<T> = {
13
+ id?: string;
14
+ header: string;
15
+ align?: 'left' | 'center' | 'right';
16
+ /** Any CSS width, e.g. `120px` or `30%`. */
17
+ width?: string;
18
+ } & (
19
+ | {
20
+ /** Renders `row[key]` as plain text. */
21
+ key: keyof T & string;
22
+ cell?: never;
23
+ /** Sorts by `row[key]`. */
24
+ sortable?: boolean;
25
+ }
26
+ | {
27
+ key?: never;
28
+ /** Renders custom content for the current row. */
29
+ cell: Snippet<[T]>;
30
+ /** Returns the value this column sorts by. */
31
+ sortable?: (row: T) => TableSortValue;
32
+ }
33
+ );
34
+ </script>
35
+
36
+ <script lang="ts" generics="T">
37
+ import type { HTMLAttributes } from 'svelte/elements';
38
+ import { ArrowDown, ArrowUp, ChevronsUpDown } from '@lucide/svelte';
39
+ import Checkbox from '../checkbox/Checkbox.svelte';
40
+
41
+ type Props = Omit<HTMLAttributes<HTMLTableElement>, 'children'> & {
42
+ data: T[];
43
+ columns: TableColumn<T>[];
44
+ sort?: TableSort;
45
+ /** Adds a checkbox column. */
46
+ selection?: 'single' | 'multiple';
47
+ selected?: T[];
48
+ /** Identifies rows across data updates; defaults to the row object itself. */
49
+ rowId?: (row: T) => unknown;
50
+ };
51
+
52
+ let {
53
+ data,
54
+ columns,
55
+ sort = $bindable(),
56
+ selection,
57
+ selected = $bindable([]),
58
+ rowId = (row) => row,
59
+ class: className,
60
+ ...rest
61
+ }: Props = $props();
62
+
63
+ const columnId = (column: TableColumn<T>) => column.id ?? column.key ?? column.header;
64
+
65
+ function sortValue(column: TableColumn<T>, row: T): unknown {
66
+ if (typeof column.sortable === 'function') return column.sortable(row);
67
+ return column.key ? row[column.key] : undefined;
68
+ }
69
+
70
+ function compare(a: unknown, b: unknown) {
71
+ if (a instanceof Date && b instanceof Date) return a.getTime() - b.getTime();
72
+ if (typeof a === 'number' && typeof b === 'number') return a - b;
73
+ return String(a).localeCompare(String(b), undefined, { numeric: true });
74
+ }
75
+
76
+ const rows = $derived.by(() => {
77
+ const column = sort && columns.find((c) => c.sortable && columnId(c) === sort!.column);
78
+ if (!sort || !column) return data;
79
+ const direction = sort.direction === 'asc' ? 1 : -1;
80
+ return [...data].sort((rowA, rowB) => {
81
+ const a = sortValue(column, rowA);
82
+ const b = sortValue(column, rowB);
83
+ if (a == null || b == null) return a == null ? (b == null ? 0 : 1) : -1;
84
+ return compare(a, b) * direction;
85
+ });
86
+ });
87
+
88
+ function toggleSort(column: TableColumn<T>) {
89
+ const id = columnId(column);
90
+ if (sort?.column !== id) sort = { column: id, direction: 'asc' };
91
+ else if (sort.direction === 'asc') sort = { column: id, direction: 'desc' };
92
+ else sort = undefined;
93
+ }
94
+
95
+ function ariaSort(column: TableColumn<T>) {
96
+ if (!column.sortable) return undefined;
97
+ if (sort?.column !== columnId(column)) return 'none';
98
+ return sort.direction === 'asc' ? 'ascending' : 'descending';
99
+ }
100
+
101
+ const selectedIds = $derived(new Set(selected.map(rowId)));
102
+ const allSelected = $derived(data.length > 0 && data.every((row) => selectedIds.has(rowId(row))));
103
+ const someSelected = $derived(!allSelected && data.some((row) => selectedIds.has(rowId(row))));
104
+
105
+ function toggleRow(row: T) {
106
+ const id = rowId(row);
107
+ if (selectedIds.has(id)) selected = selected.filter((r) => rowId(r) !== id);
108
+ else selected = selection === 'single' ? [row] : [...selected, row];
109
+ }
110
+
111
+ function toggleAll() {
112
+ selected = allSelected ? [] : [...data];
113
+ }
114
+ </script>
115
+
116
+ <div class="table-container">
117
+ <table {...rest} class={['table', className]}>
118
+ <thead>
119
+ <tr>
120
+ {#if selection}
121
+ <th scope="col" class="table-select">
122
+ {#if selection === 'multiple'}
123
+ <Checkbox
124
+ aria-label="Select all rows"
125
+ checked={allSelected}
126
+ indeterminate={someSelected}
127
+ onchange={toggleAll}
128
+ />
129
+ {/if}
130
+ </th>
131
+ {/if}
132
+ {#each columns as column, i (i)}
133
+ <th
134
+ scope="col"
135
+ class="table-{column.align ?? 'left'}"
136
+ style:width={column.width}
137
+ aria-sort={ariaSort(column)}
138
+ >
139
+ {#if column.sortable}
140
+ {@const active = sort?.column === columnId(column)}
141
+ <button type="button" class="table-sort" onclick={() => toggleSort(column)}>
142
+ {column.header}
143
+ {#if active && sort?.direction === 'asc'}
144
+ <ArrowUp class="table-sort-icon" aria-hidden="true" />
145
+ {:else if active}
146
+ <ArrowDown class="table-sort-icon" aria-hidden="true" />
147
+ {:else}
148
+ <ChevronsUpDown
149
+ class="table-sort-icon table-sort-icon--idle"
150
+ aria-hidden="true"
151
+ />
152
+ {/if}
153
+ </button>
154
+ {:else}
155
+ {column.header}
156
+ {/if}
157
+ </th>
158
+ {/each}
159
+ </tr>
160
+ </thead>
161
+ <tbody>
162
+ {#each rows as row (rowId(row))}
163
+ {@const isSelected = selectedIds.has(rowId(row))}
164
+ <tr data-selected={isSelected || undefined}>
165
+ {#if selection}
166
+ <td class="table-select">
167
+ <Checkbox
168
+ aria-label="Select row"
169
+ checked={isSelected}
170
+ onchange={() => toggleRow(row)}
171
+ />
172
+ </td>
173
+ {/if}
174
+ {#each columns as column, i (i)}
175
+ <td class="table-{column.align ?? 'left'}">
176
+ {#if column.cell}
177
+ {@render column.cell(row)}
178
+ {:else if column.key}
179
+ {row[column.key]}
180
+ {/if}
181
+ </td>
182
+ {/each}
183
+ </tr>
184
+ {/each}
185
+ </tbody>
186
+ </table>
187
+ </div>
188
+
189
+ <style>
190
+ .table-container {
191
+ width: 100%;
192
+ overflow-x: auto;
193
+ }
194
+
195
+ .table {
196
+ width: 100%;
197
+ border-collapse: collapse;
198
+ font-size: 13px;
199
+ color: var(--color-text);
200
+ }
201
+
202
+ th,
203
+ td {
204
+ padding: 10px 12px;
205
+ white-space: nowrap;
206
+ border-bottom: 1px solid var(--color-border);
207
+ }
208
+
209
+ th {
210
+ font-weight: 500;
211
+ color: var(--color-text-muted);
212
+ }
213
+
214
+ tbody tr:last-child td {
215
+ border-bottom: none;
216
+ }
217
+
218
+ tbody tr:hover,
219
+ tbody tr[data-selected] {
220
+ background-color: var(--color-surface);
221
+ }
222
+
223
+ .table-left {
224
+ text-align: left;
225
+ }
226
+
227
+ .table-center {
228
+ text-align: center;
229
+ }
230
+
231
+ .table-right {
232
+ text-align: right;
233
+ }
234
+
235
+ .table-select {
236
+ width: 16px;
237
+ padding-right: 0;
238
+ line-height: 0;
239
+ }
240
+
241
+ .table-sort {
242
+ display: inline-flex;
243
+ align-items: center;
244
+ gap: 4px;
245
+ margin: -2px -4px;
246
+ padding: 2px 4px;
247
+ font: inherit;
248
+ color: inherit;
249
+ background: none;
250
+ border: none;
251
+ border-radius: 6px;
252
+ outline: none;
253
+ cursor: pointer;
254
+ transition: color 200ms ease;
255
+ }
256
+
257
+ .table-sort:hover,
258
+ th[aria-sort='ascending'] .table-sort,
259
+ th[aria-sort='descending'] .table-sort {
260
+ color: var(--color-text);
261
+ }
262
+
263
+ .table-sort:focus-visible {
264
+ outline: 2px solid var(--color-accent);
265
+ }
266
+
267
+ .table-sort :global(.table-sort-icon) {
268
+ width: 14px;
269
+ height: 14px;
270
+ flex-shrink: 0;
271
+ }
272
+
273
+ .table-sort :global(.table-sort-icon--idle) {
274
+ opacity: 0.5;
275
+ }
276
+ </style>
@@ -0,0 +1,60 @@
1
+ import type { Snippet } from 'svelte';
2
+ export type TableSortValue = string | number | Date | null | undefined;
3
+ export type TableSort = {
4
+ /** The column's `id`, falling back to its `key`, then its `header`. */
5
+ column: string;
6
+ direction: 'asc' | 'desc';
7
+ };
8
+ export type TableColumn<T> = {
9
+ id?: string;
10
+ header: string;
11
+ align?: 'left' | 'center' | 'right';
12
+ /** Any CSS width, e.g. `120px` or `30%`. */
13
+ width?: string;
14
+ } & ({
15
+ /** Renders `row[key]` as plain text. */
16
+ key: keyof T & string;
17
+ cell?: never;
18
+ /** Sorts by `row[key]`. */
19
+ sortable?: boolean;
20
+ } | {
21
+ key?: never;
22
+ /** Renders custom content for the current row. */
23
+ cell: Snippet<[T]>;
24
+ /** Returns the value this column sorts by. */
25
+ sortable?: (row: T) => TableSortValue;
26
+ });
27
+ import type { HTMLAttributes } from 'svelte/elements';
28
+ declare function $$render<T>(): {
29
+ props: Omit<HTMLAttributes<HTMLTableElement>, "children"> & {
30
+ data: T[];
31
+ columns: TableColumn<T>[];
32
+ sort?: TableSort;
33
+ /** Adds a checkbox column. */
34
+ selection?: "single" | "multiple";
35
+ selected?: T[];
36
+ /** Identifies rows across data updates; defaults to the row object itself. */
37
+ rowId?: (row: T) => unknown;
38
+ };
39
+ exports: {};
40
+ bindings: "sort" | "selected";
41
+ slots: {};
42
+ events: {};
43
+ };
44
+ declare class __sveltets_Render<T> {
45
+ props(): ReturnType<typeof $$render<T>>['props'];
46
+ events(): ReturnType<typeof $$render<T>>['events'];
47
+ slots(): ReturnType<typeof $$render<T>>['slots'];
48
+ bindings(): "sort" | "selected";
49
+ exports(): {};
50
+ }
51
+ interface $$IsomorphicComponent {
52
+ new <T>(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']>> & {
53
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
54
+ } & ReturnType<__sveltets_Render<T>['exports']>;
55
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
56
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
57
+ }
58
+ declare const Table: $$IsomorphicComponent;
59
+ type Table<T> = InstanceType<typeof Table<T>>;
60
+ export default Table;
@@ -0,0 +1,57 @@
1
+ <script lang="ts">
2
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
3
+
4
+ let { value = $bindable(), class: className, ...rest }: HTMLTextareaAttributes = $props();
5
+ </script>
6
+
7
+ <textarea {...rest} bind:value class={['textarea', className]}></textarea>
8
+
9
+ <style>
10
+ /* Grows with its content from a two-line minimum, like the Input's 32px height */
11
+ .textarea {
12
+ box-sizing: border-box;
13
+ display: block;
14
+ width: 100%;
15
+ min-width: 0;
16
+ min-height: 64px;
17
+ padding: 6px 10px;
18
+ font-family: inherit;
19
+ font-size: 13px;
20
+ line-height: 1.5;
21
+ color: var(--color-text);
22
+ background-color: transparent;
23
+ border: 1px solid var(--color-border-strong);
24
+ border-radius: 10px;
25
+ outline: none;
26
+ resize: vertical;
27
+ field-sizing: content;
28
+ transition: border-color 200ms ease;
29
+ }
30
+
31
+ .textarea::selection {
32
+ color: var(--color-accent-foreground);
33
+ background-color: var(--color-accent);
34
+ }
35
+
36
+ .textarea::placeholder {
37
+ color: var(--color-text-faint);
38
+ }
39
+
40
+ .textarea:focus-visible {
41
+ border-color: var(--color-accent);
42
+ outline: 1px solid var(--color-accent);
43
+ }
44
+
45
+ .textarea[aria-invalid='true'] {
46
+ border-color: var(--color-destructive);
47
+ }
48
+
49
+ .textarea[aria-invalid='true']:focus-visible {
50
+ outline-color: var(--color-destructive);
51
+ }
52
+
53
+ .textarea:disabled {
54
+ cursor: not-allowed;
55
+ opacity: 0.5;
56
+ }
57
+ </style>
@@ -0,0 +1,4 @@
1
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
2
+ declare const Textarea: import("svelte").Component<HTMLTextareaAttributes, {}, "value">;
3
+ type Textarea = ReturnType<typeof Textarea>;
4
+ export default Textarea;
@@ -0,0 +1,162 @@
1
+ <script lang="ts">
2
+ import { tick } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> & {
6
+ /** Bindable, `HH:MM` in 24-hour time like `<input type="time">`. */
7
+ value?: string;
8
+ /** Minutes between the offered values. */
9
+ minuteStep?: number;
10
+ onselect?: (value: string) => void;
11
+ };
12
+
13
+ let {
14
+ value = $bindable(''),
15
+ minuteStep = 1,
16
+ onselect,
17
+ class: className,
18
+ ...rest
19
+ }: Props = $props();
20
+
21
+ const pad = (n: number) => String(n).padStart(2, '0');
22
+ const hours = Array.from({ length: 24 }, (_, i) => i);
23
+ let minutes = $derived(
24
+ Array.from(
25
+ { length: Math.ceil(60 / Math.max(1, minuteStep)) },
26
+ (_, i) => i * Math.max(1, minuteStep)
27
+ )
28
+ );
29
+
30
+ let hour = $derived(value ? Number(value.slice(0, 2)) : undefined);
31
+ let minute = $derived(value ? Number(value.slice(3, 5)) : undefined);
32
+
33
+ let hoursEl: HTMLElement | undefined = $state();
34
+ let minutesEl: HTMLElement | undefined = $state();
35
+
36
+ function set(nextHour: number, nextMinute: number) {
37
+ value = `${pad(nextHour)}:${pad(nextMinute)}`;
38
+ onselect?.(value);
39
+ }
40
+
41
+ function center(column: HTMLElement | undefined) {
42
+ const item = column?.querySelector<HTMLElement>('[tabindex="0"]');
43
+ if (column && item)
44
+ column.scrollTop = item.offsetTop - (column.clientHeight - item.offsetHeight) / 2;
45
+ }
46
+
47
+ $effect(() => {
48
+ void value;
49
+ tick().then(() => {
50
+ center(hoursEl);
51
+ center(minutesEl);
52
+ });
53
+ });
54
+
55
+ async function handleKeydown(event: KeyboardEvent, column: 'hour' | 'minute') {
56
+ const list = column === 'hour' ? hours : minutes;
57
+ const current = list.indexOf((column === 'hour' ? hour : minute) ?? list[0]);
58
+ const next = {
59
+ ArrowUp: current - 1,
60
+ ArrowDown: current + 1,
61
+ Home: 0,
62
+ End: list.length - 1
63
+ }[event.key];
64
+ if (next === undefined) return;
65
+
66
+ event.preventDefault();
67
+ const clamped = list[Math.min(list.length - 1, Math.max(0, next))];
68
+ if (column === 'hour') set(clamped, minute ?? 0);
69
+ else set(hour ?? 0, clamped);
70
+ await tick();
71
+ (column === 'hour' ? hoursEl : minutesEl)
72
+ ?.querySelector<HTMLElement>('[tabindex="0"]')
73
+ ?.focus();
74
+ }
75
+ </script>
76
+
77
+ {#snippet columnItems(list: number[], current: number | undefined, column: 'hour' | 'minute')}
78
+ {#each list as item, i (item)}
79
+ <button
80
+ type="button"
81
+ class={['time-picker-item', { 'time-picker-item--selected': item === current }]}
82
+ tabindex={item === current || (current === undefined && i === 0) ? 0 : -1}
83
+ aria-pressed={item === current}
84
+ onclick={() => (column === 'hour' ? set(item, minute ?? 0) : set(hour ?? 0, item))}
85
+ onkeydown={(event) => handleKeydown(event, column)}
86
+ >
87
+ {pad(item)}
88
+ </button>
89
+ {/each}
90
+ {/snippet}
91
+
92
+ <div {...rest} class={['time-picker', className]}>
93
+ <div class="time-picker-column" role="group" aria-label="Hours" bind:this={hoursEl}>
94
+ {@render columnItems(hours, hour, 'hour')}
95
+ </div>
96
+ <div class="time-picker-column" role="group" aria-label="Minutes" bind:this={minutesEl}>
97
+ {@render columnItems(minutes, minute, 'minute')}
98
+ </div>
99
+ </div>
100
+
101
+ <style>
102
+ .time-picker {
103
+ display: flex;
104
+ gap: 4px;
105
+ height: 232px;
106
+ font-size: 13px;
107
+ color: var(--color-text);
108
+ user-select: none;
109
+ }
110
+
111
+ .time-picker-column {
112
+ display: flex;
113
+ flex-direction: column;
114
+ gap: 2px;
115
+ width: 48px;
116
+ padding: 0 2px;
117
+ overflow-y: auto;
118
+ scrollbar-width: none;
119
+ scroll-snap-type: y proximity;
120
+ }
121
+
122
+ .time-picker-column + .time-picker-column {
123
+ padding-left: 6px;
124
+ border-left: 1px solid var(--color-border);
125
+ }
126
+
127
+ .time-picker-item {
128
+ flex-shrink: 0;
129
+ height: 28px;
130
+ padding: 0;
131
+ font-family: inherit;
132
+ font-size: 13px;
133
+ font-variant-numeric: tabular-nums;
134
+ color: var(--color-text);
135
+ background-color: transparent;
136
+ border: none;
137
+ border-radius: 8px;
138
+ outline: none;
139
+ cursor: pointer;
140
+ scroll-snap-align: center;
141
+ transition: background-color 200ms ease;
142
+ }
143
+
144
+ .time-picker-item:hover {
145
+ background-color: var(--color-surface);
146
+ }
147
+
148
+ .time-picker-item:focus-visible {
149
+ outline: 2px solid var(--color-accent);
150
+ outline-offset: -2px;
151
+ }
152
+
153
+ .time-picker-item--selected,
154
+ .time-picker-item--selected:hover {
155
+ color: var(--color-accent-foreground);
156
+ background-color: var(--color-accent);
157
+ }
158
+
159
+ .time-picker-item--selected:focus-visible {
160
+ outline-color: var(--color-accent-foreground);
161
+ }
162
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ type Props = Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> & {
3
+ /** Bindable, `HH:MM` in 24-hour time like `<input type="time">`. */
4
+ value?: string;
5
+ /** Minutes between the offered values. */
6
+ minuteStep?: number;
7
+ onselect?: (value: string) => void;
8
+ };
9
+ declare const TimePicker: import("svelte").Component<Props, {}, "value">;
10
+ type TimePicker = ReturnType<typeof TimePicker>;
11
+ export default TimePicker;