@nqmcreative/ui 0.2.1 → 0.2.3

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 (175) hide show
  1. package/README.md +7 -5
  2. package/dist/core/index.d.ts +1 -1
  3. package/dist/core/index.js +1 -1
  4. package/dist/core/trigger.d.ts +6 -0
  5. package/dist/core/trigger.js +13 -0
  6. package/dist/styles/matte/Button.svelte +4 -4
  7. package/dist/styles/matte/Card.svelte +3 -3
  8. package/dist/styles/matte/Combobox.svelte +2 -2
  9. package/dist/styles/matte/CommandPalette.svelte +32 -7
  10. package/dist/styles/matte/CommandPalette.svelte.d.ts +5 -0
  11. package/dist/styles/matte/DatePicker.svelte +2 -2
  12. package/dist/styles/matte/Drawer.svelte +2 -2
  13. package/dist/styles/matte/Dropzone.svelte +1 -1
  14. package/dist/styles/matte/FeatureCard.svelte +1 -1
  15. package/dist/styles/matte/Input.svelte +4 -4
  16. package/dist/styles/matte/MenuItem.svelte +1 -1
  17. package/dist/styles/matte/Modal.svelte +3 -3
  18. package/dist/styles/matte/NumberInput.svelte +2 -2
  19. package/dist/styles/matte/PasswordInput.svelte +2 -2
  20. package/dist/styles/matte/PricingCard.svelte +3 -3
  21. package/dist/styles/matte/SegmentedControl.svelte +1 -1
  22. package/dist/styles/matte/Select.svelte +1 -1
  23. package/dist/styles/matte/Sidebar.svelte +75 -26
  24. package/dist/styles/matte/Sidebar.svelte.d.ts +15 -1
  25. package/dist/styles/matte/StatsBand.svelte +1 -1
  26. package/dist/styles/matte/Table.svelte +3 -3
  27. package/dist/styles/matte/Testimonial.svelte +2 -2
  28. package/dist/styles/matte/Textarea.svelte +1 -1
  29. package/dist/styles/matte/index.d.ts +1 -1
  30. package/dist/styles/paper/CommandPalette.svelte +25 -2
  31. package/dist/styles/paper/CommandPalette.svelte.d.ts +5 -0
  32. package/dist/styles/paper/Modal.svelte +9 -1
  33. package/dist/styles/paper/Sidebar.svelte +75 -26
  34. package/dist/styles/paper/Sidebar.svelte.d.ts +15 -1
  35. package/dist/styles/paper/fonts.css +34 -7
  36. package/dist/styles/paper/index.d.ts +1 -1
  37. package/dist/styles/paper/theme.css +11 -8
  38. package/dist/styles/sprout/Accordion.svelte +17 -0
  39. package/dist/styles/sprout/Accordion.svelte.d.ts +8 -0
  40. package/dist/styles/sprout/AccordionItem.svelte +46 -0
  41. package/dist/styles/sprout/AccordionItem.svelte.d.ts +14 -0
  42. package/dist/styles/sprout/Alert.svelte +100 -0
  43. package/dist/styles/sprout/Alert.svelte.d.ts +18 -0
  44. package/dist/styles/sprout/Avatar.svelte +60 -0
  45. package/dist/styles/sprout/Avatar.svelte.d.ts +16 -0
  46. package/dist/styles/sprout/AvatarGroup.svelte +17 -0
  47. package/dist/styles/sprout/AvatarGroup.svelte.d.ts +8 -0
  48. package/dist/styles/sprout/Badge.svelte +58 -0
  49. package/dist/styles/sprout/Badge.svelte.d.ts +16 -0
  50. package/dist/styles/sprout/Breadcrumb.svelte +49 -0
  51. package/dist/styles/sprout/Breadcrumb.svelte.d.ts +14 -0
  52. package/dist/styles/sprout/Button.svelte +105 -0
  53. package/dist/styles/sprout/Button.svelte.d.ts +21 -0
  54. package/dist/styles/sprout/CTASection.svelte +77 -0
  55. package/dist/styles/sprout/CTASection.svelte.d.ts +18 -0
  56. package/dist/styles/sprout/Calendar.svelte +202 -0
  57. package/dist/styles/sprout/Calendar.svelte.d.ts +22 -0
  58. package/dist/styles/sprout/Card.svelte +82 -0
  59. package/dist/styles/sprout/Card.svelte.d.ts +21 -0
  60. package/dist/styles/sprout/Checkbox.svelte +76 -0
  61. package/dist/styles/sprout/Checkbox.svelte.d.ts +15 -0
  62. package/dist/styles/sprout/CheckboxGroup.svelte +112 -0
  63. package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +30 -0
  64. package/dist/styles/sprout/Combobox.svelte +267 -0
  65. package/dist/styles/sprout/Combobox.svelte.d.ts +29 -0
  66. package/dist/styles/sprout/CommandPalette.svelte +233 -0
  67. package/dist/styles/sprout/CommandPalette.svelte.d.ts +37 -0
  68. package/dist/styles/sprout/ConfirmDialog.svelte +97 -0
  69. package/dist/styles/sprout/ConfirmDialog.svelte.d.ts +22 -0
  70. package/dist/styles/sprout/ContextMenu.svelte +89 -0
  71. package/dist/styles/sprout/ContextMenu.svelte.d.ts +15 -0
  72. package/dist/styles/sprout/DatePicker.svelte +193 -0
  73. package/dist/styles/sprout/DatePicker.svelte.d.ts +23 -0
  74. package/dist/styles/sprout/Divider.svelte +30 -0
  75. package/dist/styles/sprout/Divider.svelte.d.ts +10 -0
  76. package/dist/styles/sprout/Drawer.svelte +166 -0
  77. package/dist/styles/sprout/Drawer.svelte.d.ts +20 -0
  78. package/dist/styles/sprout/Dropdown.svelte +95 -0
  79. package/dist/styles/sprout/Dropdown.svelte.d.ts +23 -0
  80. package/dist/styles/sprout/Dropzone.svelte +211 -0
  81. package/dist/styles/sprout/Dropzone.svelte.d.ts +29 -0
  82. package/dist/styles/sprout/EmptyState.svelte +71 -0
  83. package/dist/styles/sprout/EmptyState.svelte.d.ts +18 -0
  84. package/dist/styles/sprout/FeatureCard.svelte +61 -0
  85. package/dist/styles/sprout/FeatureCard.svelte.d.ts +16 -0
  86. package/dist/styles/sprout/FeatureGrid.svelte +62 -0
  87. package/dist/styles/sprout/FeatureGrid.svelte.d.ts +16 -0
  88. package/dist/styles/sprout/Field.svelte +42 -0
  89. package/dist/styles/sprout/Field.svelte.d.ts +16 -0
  90. package/dist/styles/sprout/Footer.svelte +91 -0
  91. package/dist/styles/sprout/Footer.svelte.d.ts +25 -0
  92. package/dist/styles/sprout/HeroSection.svelte +86 -0
  93. package/dist/styles/sprout/HeroSection.svelte.d.ts +21 -0
  94. package/dist/styles/sprout/Input.svelte +61 -0
  95. package/dist/styles/sprout/Input.svelte.d.ts +18 -0
  96. package/dist/styles/sprout/InputGroup.svelte +20 -0
  97. package/dist/styles/sprout/InputGroup.svelte.d.ts +11 -0
  98. package/dist/styles/sprout/Kbd.svelte +18 -0
  99. package/dist/styles/sprout/Kbd.svelte.d.ts +8 -0
  100. package/dist/styles/sprout/Label.svelte +16 -0
  101. package/dist/styles/sprout/Label.svelte.d.ts +9 -0
  102. package/dist/styles/sprout/Link.svelte +44 -0
  103. package/dist/styles/sprout/Link.svelte.d.ts +16 -0
  104. package/dist/styles/sprout/LocaleProvider.svelte +24 -0
  105. package/dist/styles/sprout/LocaleProvider.svelte.d.ts +10 -0
  106. package/dist/styles/sprout/LogoCloud.svelte +61 -0
  107. package/dist/styles/sprout/LogoCloud.svelte.d.ts +19 -0
  108. package/dist/styles/sprout/MenuItem.svelte +64 -0
  109. package/dist/styles/sprout/MenuItem.svelte.d.ts +18 -0
  110. package/dist/styles/sprout/MenuSeparator.svelte +22 -0
  111. package/dist/styles/sprout/MenuSeparator.svelte.d.ts +8 -0
  112. package/dist/styles/sprout/Modal.svelte +127 -0
  113. package/dist/styles/sprout/Modal.svelte.d.ts +21 -0
  114. package/dist/styles/sprout/MultiSelect.svelte +288 -0
  115. package/dist/styles/sprout/MultiSelect.svelte.d.ts +31 -0
  116. package/dist/styles/sprout/Navbar.svelte +201 -0
  117. package/dist/styles/sprout/Navbar.svelte.d.ts +33 -0
  118. package/dist/styles/sprout/NumberInput.svelte +97 -0
  119. package/dist/styles/sprout/NumberInput.svelte.d.ts +15 -0
  120. package/dist/styles/sprout/Pagination.svelte +84 -0
  121. package/dist/styles/sprout/Pagination.svelte.d.ts +15 -0
  122. package/dist/styles/sprout/PasswordInput.svelte +101 -0
  123. package/dist/styles/sprout/PasswordInput.svelte.d.ts +12 -0
  124. package/dist/styles/sprout/Popover.svelte +90 -0
  125. package/dist/styles/sprout/Popover.svelte.d.ts +19 -0
  126. package/dist/styles/sprout/PricingCard.svelte +109 -0
  127. package/dist/styles/sprout/PricingCard.svelte.d.ts +25 -0
  128. package/dist/styles/sprout/Progress.svelte +59 -0
  129. package/dist/styles/sprout/Progress.svelte.d.ts +17 -0
  130. package/dist/styles/sprout/Radio.svelte +56 -0
  131. package/dist/styles/sprout/Radio.svelte.d.ts +15 -0
  132. package/dist/styles/sprout/RadioGroup.svelte +96 -0
  133. package/dist/styles/sprout/RadioGroup.svelte.d.ts +27 -0
  134. package/dist/styles/sprout/SegmentedControl.svelte +89 -0
  135. package/dist/styles/sprout/SegmentedControl.svelte.d.ts +25 -0
  136. package/dist/styles/sprout/Select.svelte +62 -0
  137. package/dist/styles/sprout/Select.svelte.d.ts +20 -0
  138. package/dist/styles/sprout/Sidebar.svelte +255 -0
  139. package/dist/styles/sprout/Sidebar.svelte.d.ts +50 -0
  140. package/dist/styles/sprout/Skeleton.svelte +46 -0
  141. package/dist/styles/sprout/Skeleton.svelte.d.ts +13 -0
  142. package/dist/styles/sprout/Slider.svelte +80 -0
  143. package/dist/styles/sprout/Slider.svelte.d.ts +19 -0
  144. package/dist/styles/sprout/Spinner.svelte +34 -0
  145. package/dist/styles/sprout/Spinner.svelte.d.ts +13 -0
  146. package/dist/styles/sprout/Stat.svelte +51 -0
  147. package/dist/styles/sprout/Stat.svelte.d.ts +16 -0
  148. package/dist/styles/sprout/StatsBand.svelte +64 -0
  149. package/dist/styles/sprout/StatsBand.svelte.d.ts +20 -0
  150. package/dist/styles/sprout/Steps.svelte +143 -0
  151. package/dist/styles/sprout/Steps.svelte.d.ts +24 -0
  152. package/dist/styles/sprout/Switch.svelte +65 -0
  153. package/dist/styles/sprout/Switch.svelte.d.ts +14 -0
  154. package/dist/styles/sprout/Table.svelte +218 -0
  155. package/dist/styles/sprout/Table.svelte.d.ts +66 -0
  156. package/dist/styles/sprout/Tabs.svelte +103 -0
  157. package/dist/styles/sprout/Tabs.svelte.d.ts +23 -0
  158. package/dist/styles/sprout/Testimonial.svelte +65 -0
  159. package/dist/styles/sprout/Testimonial.svelte.d.ts +19 -0
  160. package/dist/styles/sprout/Textarea.svelte +55 -0
  161. package/dist/styles/sprout/Textarea.svelte.d.ts +12 -0
  162. package/dist/styles/sprout/ThemeToggle.svelte +131 -0
  163. package/dist/styles/sprout/ThemeToggle.svelte.d.ts +16 -0
  164. package/dist/styles/sprout/Toaster.svelte +133 -0
  165. package/dist/styles/sprout/Toaster.svelte.d.ts +8 -0
  166. package/dist/styles/sprout/Tooltip.svelte +34 -0
  167. package/dist/styles/sprout/Tooltip.svelte.d.ts +12 -0
  168. package/dist/styles/sprout/fonts.css +77 -0
  169. package/dist/styles/sprout/index.d.ts +109 -0
  170. package/dist/styles/sprout/index.js +83 -0
  171. package/dist/styles/sprout/lift.d.ts +40 -0
  172. package/dist/styles/sprout/lift.js +36 -0
  173. package/dist/styles/sprout/theme.css +255 -0
  174. package/package.json +269 -3
  175. package/registry.json +139 -66
@@ -0,0 +1,218 @@
1
+ <script module lang="ts">
2
+ export interface TableColumn {
3
+ key: string;
4
+ label: string;
5
+ align?: 'left' | 'center' | 'right';
6
+ /** Extra classes for this column's cells, e.g. `'w-40 tabular-nums'`. */
7
+ class?: string;
8
+ /** Allow clicking the header to sort by this column. */
9
+ sortable?: boolean;
10
+ }
11
+ </script>
12
+
13
+ <script lang="ts" generics="T extends Record<string, unknown>">
14
+ import type { Snippet } from 'svelte';
15
+ import type { HTMLAttributes } from 'svelte/elements';
16
+ import Checkbox from './Checkbox.svelte';
17
+ import type { Tone } from '../../core/tones.js';
18
+ import { nextSort, sortRows, toggleKey, type TableSort } from '../../core/table.js';
19
+ import { useLocale } from '../../core/locale.svelte.js';
20
+ import { edge } from './lift.js';
21
+
22
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> {
23
+ columns: TableColumn[];
24
+ rows: T[];
25
+ /** Custom cell rendering — falls back to the raw value. */
26
+ cell?: Snippet<[{ row: T; column: TableColumn; value: unknown }]>;
27
+ /** Shown instead of the body when `rows` is empty. */
28
+ empty?: Snippet;
29
+ caption?: string;
30
+ striped?: boolean;
31
+ hoverable?: boolean;
32
+ compact?: boolean;
33
+ /** Header stays put while the body scrolls — pair with a `max-h-*` class. */
34
+ stickyHeader?: boolean;
35
+
36
+ /** Bindable. Set it to sort; leave `sortable` columns to drive it. */
37
+ sort?: TableSort | null;
38
+ /** Sort the rows here rather than refetching them server-side. Default true. */
39
+ sortLocally?: boolean;
40
+ onsort?: (sort: TableSort | null) => void;
41
+
42
+ /** Turns on the checkbox column. Bindable list of selected row keys. */
43
+ selectable?: boolean;
44
+ selected?: string[];
45
+ /** Which field identifies a row. Default `'id'`. */
46
+ rowKey?: string;
47
+ tone?: Tone;
48
+ onselect?: (selected: string[]) => void;
49
+ }
50
+
51
+ let {
52
+ columns,
53
+ rows,
54
+ cell,
55
+ empty,
56
+ caption,
57
+ striped = false,
58
+ hoverable = true,
59
+ compact = false,
60
+ stickyHeader = false,
61
+ sort = $bindable(null),
62
+ sortLocally = true,
63
+ onsort,
64
+ selectable = false,
65
+ selected = $bindable([]),
66
+ rowKey = 'id',
67
+ tone = 'brand',
68
+ onselect,
69
+ class: className = '',
70
+ ...rest
71
+ }: Props = $props();
72
+
73
+ const t = useLocale();
74
+
75
+ const aligns = {
76
+ left: 'text-left',
77
+ center: 'text-center',
78
+ right: 'text-right'
79
+ };
80
+
81
+ const pad = $derived(compact ? 'px-4 py-2' : 'px-4 py-3');
82
+
83
+ const sorted = $derived(sortLocally ? sortRows(rows, sort) : rows);
84
+
85
+ function toggleSort(column: TableColumn) {
86
+ if (!column.sortable) return;
87
+ const next = nextSort(sort, column.key);
88
+ sort = next;
89
+ onsort?.(next);
90
+ }
91
+
92
+ const keyOf = (row: T) => String(row[rowKey] ?? '');
93
+
94
+ const allSelected = $derived(
95
+ sorted.length > 0 && sorted.every((row) => selected.includes(keyOf(row)))
96
+ );
97
+ const someSelected = $derived(
98
+ !allSelected && sorted.some((row) => selected.includes(keyOf(row)))
99
+ );
100
+
101
+ function toggleAll() {
102
+ selected = allSelected ? [] : sorted.map(keyOf);
103
+ onselect?.(selected);
104
+ }
105
+
106
+ function toggleRow(row: T) {
107
+ selected = toggleKey(selected, keyOf(row));
108
+ onselect?.(selected);
109
+ }
110
+ </script>
111
+
112
+ <div
113
+ class="w-full overflow-auto rounded-[20px] border border-hairline bg-bg {edge} {className}"
114
+ {...rest}
115
+ >
116
+ <table class="w-full border-collapse font-sans text-sm">
117
+ {#if caption}
118
+ <caption
119
+ class="border-b border-hairline px-4 py-3 text-left font-sans text-[13px] text-text-muted"
120
+ >
121
+ {caption}
122
+ </caption>
123
+ {/if}
124
+ <thead class={stickyHeader ? 'sticky top-0 z-10' : ''}>
125
+ <tr class="border-b border-hairline bg-bg-alt">
126
+ {#if selectable}
127
+ <th scope="col" class="w-10 {pad}">
128
+ <Checkbox
129
+ checked={allSelected}
130
+ indeterminate={someSelected}
131
+ {tone}
132
+ aria-label={t.current.selectAllRows}
133
+ onchange={toggleAll}
134
+ />
135
+ </th>
136
+ {/if}
137
+ {#each columns as column (column.key)}
138
+ <th
139
+ scope="col"
140
+ aria-sort={sort?.key === column.key
141
+ ? sort.direction === 'asc'
142
+ ? 'ascending'
143
+ : 'descending'
144
+ : undefined}
145
+ class="text-[13px] font-semibold text-text-muted {pad} {aligns[
146
+ column.align ?? 'left'
147
+ ]} {column.class ?? ''}"
148
+ >
149
+ {#if column.sortable}
150
+ <button
151
+ type="button"
152
+ onclick={() => toggleSort(column)}
153
+ class="inline-flex items-center gap-1.5 rounded transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current
154
+ {sort?.key === column.key ? 'text-text' : ''}"
155
+ >
156
+ {column.label}
157
+ <span class="text-[9px] leading-none" aria-hidden="true">
158
+ {#if sort?.key === column.key}
159
+ {sort.direction === 'asc' ? '▲' : '▼'}
160
+ {:else}
161
+
162
+ {/if}
163
+ </span>
164
+ </button>
165
+ {:else}
166
+ {column.label}
167
+ {/if}
168
+ </th>
169
+ {/each}
170
+ </tr>
171
+ </thead>
172
+ <tbody>
173
+ {#if sorted.length === 0}
174
+ <tr>
175
+ <td
176
+ colspan={columns.length + (selectable ? 1 : 0)}
177
+ class="px-4 py-10 text-center text-sm text-text-muted"
178
+ >
179
+ {#if empty}{@render empty()}{:else}{t.current.noData}{/if}
180
+ </td>
181
+ </tr>
182
+ {:else}
183
+ {#each sorted as row, i (selectable ? keyOf(row) : i)}
184
+ {@const isSelected = selectable && selected.includes(keyOf(row))}
185
+ <tr
186
+ aria-selected={selectable ? isSelected : undefined}
187
+ class="border-b border-hairline last:border-b-0
188
+ {isSelected ? 'bg-brand-light' : striped && i % 2 === 1 ? 'bg-bg-alt' : ''}
189
+ {hoverable ? 'transition-colors duration-150 hover:bg-bg-alt' : ''}"
190
+ >
191
+ {#if selectable}
192
+ <td class={pad}>
193
+ <Checkbox
194
+ checked={isSelected}
195
+ {tone}
196
+ aria-label={t.current.selectRow}
197
+ onchange={() => toggleRow(row)}
198
+ />
199
+ </td>
200
+ {/if}
201
+ {#each columns as column (column.key)}
202
+ <td
203
+ class="text-text-secondary {pad} {aligns[column.align ?? 'left']} {column.class ??
204
+ ''}"
205
+ >
206
+ {#if cell}
207
+ {@render cell({ row, column, value: row[column.key] })}
208
+ {:else}
209
+ {row[column.key] ?? ''}
210
+ {/if}
211
+ </td>
212
+ {/each}
213
+ </tr>
214
+ {/each}
215
+ {/if}
216
+ </tbody>
217
+ </table>
218
+ </div>
@@ -0,0 +1,66 @@
1
+ export interface TableColumn {
2
+ key: string;
3
+ label: string;
4
+ align?: 'left' | 'center' | 'right';
5
+ /** Extra classes for this column's cells, e.g. `'w-40 tabular-nums'`. */
6
+ class?: string;
7
+ /** Allow clicking the header to sort by this column. */
8
+ sortable?: boolean;
9
+ }
10
+ import type { Snippet } from 'svelte';
11
+ import type { HTMLAttributes } from 'svelte/elements';
12
+ import type { Tone } from '../../core/tones.js';
13
+ import { type TableSort } from '../../core/table.js';
14
+ declare function $$render<T extends Record<string, unknown>>(): {
15
+ props: Omit<HTMLAttributes<HTMLDivElement>, "onselect"> & {
16
+ columns: TableColumn[];
17
+ rows: T[];
18
+ /** Custom cell rendering — falls back to the raw value. */
19
+ cell?: Snippet<[{
20
+ row: T;
21
+ column: TableColumn;
22
+ value: unknown;
23
+ }]>;
24
+ /** Shown instead of the body when `rows` is empty. */
25
+ empty?: Snippet;
26
+ caption?: string;
27
+ striped?: boolean;
28
+ hoverable?: boolean;
29
+ compact?: boolean;
30
+ /** Header stays put while the body scrolls — pair with a `max-h-*` class. */
31
+ stickyHeader?: boolean;
32
+ /** Bindable. Set it to sort; leave `sortable` columns to drive it. */
33
+ sort?: TableSort | null;
34
+ /** Sort the rows here rather than refetching them server-side. Default true. */
35
+ sortLocally?: boolean;
36
+ onsort?: (sort: TableSort | null) => void;
37
+ /** Turns on the checkbox column. Bindable list of selected row keys. */
38
+ selectable?: boolean;
39
+ selected?: string[];
40
+ /** Which field identifies a row. Default `'id'`. */
41
+ rowKey?: string;
42
+ tone?: Tone;
43
+ onselect?: (selected: string[]) => void;
44
+ };
45
+ exports: {};
46
+ bindings: "sort" | "selected";
47
+ slots: {};
48
+ events: {};
49
+ };
50
+ declare class __sveltets_Render<T extends Record<string, unknown>> {
51
+ props(): ReturnType<typeof $$render<T>>['props'];
52
+ events(): ReturnType<typeof $$render<T>>['events'];
53
+ slots(): ReturnType<typeof $$render<T>>['slots'];
54
+ bindings(): "sort" | "selected";
55
+ exports(): {};
56
+ }
57
+ interface $$IsomorphicComponent {
58
+ new <T extends Record<string, 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']>> & {
59
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
60
+ } & ReturnType<__sveltets_Render<T>['exports']>;
61
+ <T extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
62
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
63
+ }
64
+ declare const Table: $$IsomorphicComponent;
65
+ type Table<T extends Record<string, unknown>> = InstanceType<typeof Table<T>>;
66
+ export default Table;
@@ -0,0 +1,103 @@
1
+ <script lang="ts">
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { edge } from './lift.js';
4
+ import {
5
+ focusRing,
6
+ toneBorder,
7
+ toneRing,
8
+ toneSoft,
9
+ toneText,
10
+ type Tone
11
+ } from '../../core/tones.js';
12
+
13
+ export interface TabItem {
14
+ value: string;
15
+ label: string;
16
+ disabled?: boolean;
17
+ /** Small count/label rendered after the tab title. */
18
+ badge?: string | number;
19
+ }
20
+
21
+ export type TabsVariant = 'underline' | 'pill' | 'segmented';
22
+
23
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange'> {
24
+ items: TabItem[];
25
+ /** Bindable — the `value` of the active tab. */
26
+ value?: string;
27
+ variant?: TabsVariant;
28
+ tone?: Tone;
29
+ /** Spread tabs evenly across the full width. */
30
+ fullWidth?: boolean;
31
+ onchange?: (value: string) => void;
32
+ }
33
+
34
+ let {
35
+ items,
36
+ value = $bindable(items[0]?.value ?? ''),
37
+ variant = 'underline',
38
+ tone = 'brand',
39
+ fullWidth = false,
40
+ onchange,
41
+ class: className = '',
42
+ ...rest
43
+ }: Props = $props();
44
+
45
+ function select(item: TabItem) {
46
+ if (item.disabled) return;
47
+ value = item.value;
48
+ onchange?.(item.value);
49
+ }
50
+
51
+ const listClass = $derived(
52
+ {
53
+ underline: 'gap-5 border-b border-hairline',
54
+ pill: 'gap-1.5',
55
+ segmented: 'gap-1 rounded-2xl border border-hairline bg-bg-inset p-1'
56
+ }[variant]
57
+ );
58
+
59
+ function tabClass(active: boolean) {
60
+ const base = `inline-flex items-center justify-center gap-2 font-sans text-sm font-medium whitespace-nowrap transition-all duration-150 ease-brand-out disabled:opacity-40 disabled:pointer-events-none ${focusRing} ${toneRing[tone]}`;
61
+ if (variant === 'underline') {
62
+ return `${base} -mb-px rounded-t-xl border-b-2 px-1 pb-2.5 ${
63
+ active
64
+ ? `${toneBorder[tone]} ${toneText[tone]}`
65
+ : 'border-transparent text-text-muted hover:text-text'
66
+ }`;
67
+ }
68
+ if (variant === 'pill') {
69
+ return `${base} h-9 rounded-full px-3.5 ${
70
+ active ? toneSoft[tone] : 'text-text-muted hover:bg-bg-inset hover:text-text'
71
+ }`;
72
+ }
73
+ return `${base} h-8 rounded-xl px-3.5 ${
74
+ active ? `bg-bg text-text ${edge}` : 'text-text-muted hover:text-text'
75
+ }`;
76
+ }
77
+ </script>
78
+
79
+ <div
80
+ role="tablist"
81
+ class="flex items-center overflow-x-auto {listClass} {fullWidth ? '*:flex-1' : ''} {className}"
82
+ {...rest}
83
+ >
84
+ {#each items as item (item.value)}
85
+ <button
86
+ type="button"
87
+ role="tab"
88
+ aria-selected={value === item.value}
89
+ disabled={item.disabled}
90
+ onclick={() => select(item)}
91
+ class={tabClass(value === item.value)}
92
+ >
93
+ {item.label}
94
+ {#if item.badge !== undefined}
95
+ <span
96
+ class="rounded-full bg-bg-inset px-1.5 text-[11px] font-normal text-text-muted tabular-nums"
97
+ >
98
+ {item.badge}
99
+ </span>
100
+ {/if}
101
+ </button>
102
+ {/each}
103
+ </div>
@@ -0,0 +1,23 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../../core/tones.js';
3
+ export interface TabItem {
4
+ value: string;
5
+ label: string;
6
+ disabled?: boolean;
7
+ /** Small count/label rendered after the tab title. */
8
+ badge?: string | number;
9
+ }
10
+ export type TabsVariant = 'underline' | 'pill' | 'segmented';
11
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange'> {
12
+ items: TabItem[];
13
+ /** Bindable — the `value` of the active tab. */
14
+ value?: string;
15
+ variant?: TabsVariant;
16
+ tone?: Tone;
17
+ /** Spread tabs evenly across the full width. */
18
+ fullWidth?: boolean;
19
+ onchange?: (value: string) => void;
20
+ }
21
+ declare const Tabs: import("svelte").Component<Props, {}, "value">;
22
+ type Tabs = ReturnType<typeof Tabs>;
23
+ export default Tabs;
@@ -0,0 +1,65 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import Avatar from './Avatar.svelte';
5
+ import { toneBorderLeft, toneSurface, type Tone } from '../../core/tones.js';
6
+ import { edge } from './lift.js';
7
+
8
+ export type TestimonialVariant = 'plain' | 'card' | 'accent';
9
+
10
+ interface Props extends HTMLAttributes<HTMLElement> {
11
+ quote: string;
12
+ author: string;
13
+ role?: string;
14
+ /** Avatar image. Falls back to the author's initials. */
15
+ avatar?: string;
16
+ tone?: Tone;
17
+ variant?: TestimonialVariant;
18
+ /** Bigger type, for a single pull quote. */
19
+ large?: boolean;
20
+ children?: Snippet;
21
+ }
22
+
23
+ let {
24
+ quote,
25
+ author,
26
+ role,
27
+ avatar,
28
+ tone = 'brand',
29
+ variant = 'plain',
30
+ large = false,
31
+ class: className = '',
32
+ children,
33
+ ...rest
34
+ }: Props = $props();
35
+
36
+ const variants = $derived({
37
+ plain: '',
38
+ card: `rounded-[28px] border border-hairline bg-bg p-6 ${edge}`,
39
+ accent: `rounded-[28px] ${toneSurface[tone]} border-l-4 ${toneBorderLeft[tone]} p-6`
40
+ });
41
+ </script>
42
+
43
+ <figure class="flex flex-col gap-5 {variants[variant]} {className}" {...rest}>
44
+ <blockquote
45
+ class="font-sans leading-relaxed text-pretty text-text-secondary {large
46
+ ? 'text-xl leading-relaxed sm:text-2xl'
47
+ : ''}"
48
+ >
49
+ “{quote}”
50
+ </blockquote>
51
+
52
+ {#if children}
53
+ {@render children()}
54
+ {/if}
55
+
56
+ <figcaption class="flex items-center gap-3">
57
+ <Avatar src={avatar} name={author} size="sm" {tone} />
58
+ <div class="flex min-w-0 flex-col">
59
+ <span class="font-sans text-sm font-semibold text-text">{author}</span>
60
+ {#if role}
61
+ <span class="font-sans text-[13px] text-text-muted">{role}</span>
62
+ {/if}
63
+ </div>
64
+ </figcaption>
65
+ </figure>
@@ -0,0 +1,19 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../../core/tones.js';
4
+ export type TestimonialVariant = 'plain' | 'card' | 'accent';
5
+ interface Props extends HTMLAttributes<HTMLElement> {
6
+ quote: string;
7
+ author: string;
8
+ role?: string;
9
+ /** Avatar image. Falls back to the author's initials. */
10
+ avatar?: string;
11
+ tone?: Tone;
12
+ variant?: TestimonialVariant;
13
+ /** Bigger type, for a single pull quote. */
14
+ large?: boolean;
15
+ children?: Snippet;
16
+ }
17
+ declare const Testimonial: import("svelte").Component<Props, {}, "">;
18
+ type Testimonial = ReturnType<typeof Testimonial>;
19
+ export default Testimonial;
@@ -0,0 +1,55 @@
1
+ <script lang="ts">
2
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
3
+ import { toneFocusBorder, type Tone } from '../../core/tones.js';
4
+ import { edge } from './lift.js';
5
+
6
+ interface Props extends HTMLTextareaAttributes {
7
+ value?: string;
8
+ tone?: Tone;
9
+ invalid?: boolean;
10
+ /** Grow with the content instead of showing a scrollbar. */
11
+ autoresize?: boolean;
12
+ }
13
+
14
+ let {
15
+ value = $bindable(''),
16
+ tone = 'brand',
17
+ invalid = false,
18
+ autoresize = false,
19
+ rows = 4,
20
+ class: className = '',
21
+ ...rest
22
+ }: Props = $props();
23
+
24
+ /** Grows the field to fit its content on input, and once on mount. */
25
+ function autosize(node: HTMLTextAreaElement, enabled: boolean) {
26
+ let on = enabled;
27
+ const resize = () => {
28
+ if (!on) return;
29
+ node.style.height = 'auto';
30
+ node.style.height = `${node.scrollHeight}px`;
31
+ };
32
+ resize();
33
+ node.addEventListener('input', resize);
34
+ return {
35
+ update(next: boolean) {
36
+ on = next;
37
+ if (on) resize();
38
+ else node.style.height = '';
39
+ },
40
+ destroy() {
41
+ node.removeEventListener('input', resize);
42
+ }
43
+ };
44
+ }
45
+ </script>
46
+
47
+ <textarea
48
+ use:autosize={autoresize}
49
+ bind:value
50
+ {rows}
51
+ aria-invalid={invalid ? 'true' : undefined}
52
+ class="w-full rounded-xl border bg-bg px-3 py-2 font-sans text-sm text-text {edge} transition-colors duration-150 ease-brand-out placeholder:text-text-muted focus:outline-none disabled:pointer-events-none disabled:opacity-50
53
+ {autoresize ? 'resize-none overflow-hidden' : 'resize-y'}
54
+ {invalid ? 'border-danger' : `border-hairline-strong ${toneFocusBorder[tone]}`} {className}"
55
+ {...rest}></textarea>
@@ -0,0 +1,12 @@
1
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
2
+ import { type Tone } from '../../core/tones.js';
3
+ interface Props extends HTMLTextareaAttributes {
4
+ value?: string;
5
+ tone?: Tone;
6
+ invalid?: boolean;
7
+ /** Grow with the content instead of showing a scrollbar. */
8
+ autoresize?: boolean;
9
+ }
10
+ declare const Textarea: import("svelte").Component<Props, {}, "value">;
11
+ type Textarea = ReturnType<typeof Textarea>;
12
+ export default Textarea;
@@ -0,0 +1,131 @@
1
+ <script lang="ts">
2
+ // Choosing a theme is style-agnostic: every style reads the same two classes
3
+ // off <html>, so `applyTheme`, `storedTheme` and `Theme` live in core and
4
+ // are re-exported by the style barrel — not by this component.
5
+ import { focusRing, toneRing, type Tone } from '../../core/tones.js';
6
+ import {
7
+ applyTheme,
8
+ nextTheme,
9
+ persistTheme,
10
+ storedTheme,
11
+ type Theme
12
+ } from '../../core/theme.svelte.js';
13
+ import { useLocale } from '../../core/locale.svelte.js';
14
+ import { edge } from './lift.js';
15
+
16
+ interface Props {
17
+ /** Bindable. `'system'` follows the OS until the visitor chooses. */
18
+ theme?: Theme;
19
+ /** `'button'` cycles light → dark → system; `'segmented'` shows all three. */
20
+ variant?: 'button' | 'segmented';
21
+ tone?: Tone;
22
+ /** Remember the choice in `localStorage`. Default true. */
23
+ persist?: boolean;
24
+ onchange?: (theme: Theme) => void;
25
+ class?: string;
26
+ }
27
+
28
+ let {
29
+ theme = $bindable(storedTheme()),
30
+ variant = 'button',
31
+ tone = 'neutral',
32
+ persist = true,
33
+ onchange,
34
+ class: className = ''
35
+ }: Props = $props();
36
+
37
+ const t = useLocale();
38
+
39
+ $effect(() => {
40
+ applyTheme(theme);
41
+ if (persist) persistTheme(theme);
42
+ });
43
+
44
+ function set(next: Theme) {
45
+ theme = next;
46
+ onchange?.(next);
47
+ }
48
+
49
+ function cycle() {
50
+ set(nextTheme(theme));
51
+ }
52
+
53
+ const options = $derived([
54
+ { value: 'light' as Theme, label: t.current.lightMode },
55
+ { value: 'dark' as Theme, label: t.current.darkMode },
56
+ { value: 'system' as Theme, label: 'Auto' }
57
+ ]);
58
+ </script>
59
+
60
+ {#snippet glyph(which: Theme)}
61
+ {#if which === 'light'}
62
+ <svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
63
+ <circle cx="10" cy="10" r="3.5" stroke="currentColor" stroke-width="1.5" />
64
+ <path
65
+ d="M10 2v2M10 16v2M2 10h2M16 10h2M4.5 4.5 6 6M14 14l1.5 1.5M15.5 4.5 14 6M6 14l-1.5 1.5"
66
+ stroke="currentColor"
67
+ stroke-width="1.5"
68
+ stroke-linecap="round"
69
+ />
70
+ </svg>
71
+ {:else if which === 'dark'}
72
+ <svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
73
+ <path
74
+ d="M16 12.3A6.8 6.8 0 0 1 7.7 4a6.8 6.8 0 1 0 8.3 8.3Z"
75
+ stroke="currentColor"
76
+ stroke-width="1.5"
77
+ stroke-linejoin="round"
78
+ />
79
+ </svg>
80
+ {:else}
81
+ <svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
82
+ <rect
83
+ x="2.5"
84
+ y="4"
85
+ width="15"
86
+ height="10"
87
+ rx="1.5"
88
+ stroke="currentColor"
89
+ stroke-width="1.5"
90
+ />
91
+ <path d="M7 17h6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
92
+ </svg>
93
+ {/if}
94
+ {/snippet}
95
+
96
+ {#if variant === 'segmented'}
97
+ <div
98
+ role="radiogroup"
99
+ aria-label={t.current.toggleTheme}
100
+ class="inline-flex items-center gap-1 rounded-2xl border border-hairline bg-bg-inset p-1 {className}"
101
+ >
102
+ {#each options as option (option.value)}
103
+ <button
104
+ type="button"
105
+ role="radio"
106
+ aria-checked={theme === option.value}
107
+ aria-label={option.label}
108
+ onclick={() => set(option.value)}
109
+ class="inline-flex h-8 items-center gap-1.5 rounded-xl px-2.5 font-sans text-[13px] font-medium transition-all duration-150 ease-brand-out {focusRing} {toneRing[
110
+ tone
111
+ ]}
112
+ {theme === option.value ? `bg-bg text-text ${edge}` : 'text-text-muted hover:text-text'}"
113
+ >
114
+ {@render glyph(option.value)}
115
+ <span class="hidden sm:inline">{option.label}</span>
116
+ </button>
117
+ {/each}
118
+ </div>
119
+ {:else}
120
+ <button
121
+ type="button"
122
+ onclick={cycle}
123
+ aria-label="{t.current.toggleTheme} ({theme})"
124
+ title={t.current.toggleTheme}
125
+ class="inline-flex size-9 items-center justify-center rounded-xl border border-hairline bg-bg text-text-secondary {edge} transition-colors duration-150 ease-brand-out hover:bg-bg-alt hover:text-text {focusRing} {toneRing[
126
+ tone
127
+ ]} {className}"
128
+ >
129
+ {@render glyph(theme)}
130
+ </button>
131
+ {/if}