@nqmcreative/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +507 -0
- package/cli/index.mjs +410 -0
- package/dist/actions/anchor.d.ts +29 -0
- package/dist/actions/anchor.js +122 -0
- package/dist/actions/dismissable.d.ts +44 -0
- package/dist/actions/dismissable.js +147 -0
- package/dist/components/Accordion.svelte +14 -0
- package/dist/components/Accordion.svelte.d.ts +8 -0
- package/dist/components/AccordionItem.svelte +46 -0
- package/dist/components/AccordionItem.svelte.d.ts +14 -0
- package/dist/components/Alert.svelte +88 -0
- package/dist/components/Alert.svelte.d.ts +18 -0
- package/dist/components/Avatar.svelte +58 -0
- package/dist/components/Avatar.svelte.d.ts +16 -0
- package/dist/components/AvatarGroup.svelte +17 -0
- package/dist/components/AvatarGroup.svelte.d.ts +8 -0
- package/dist/components/Badge.svelte +53 -0
- package/dist/components/Badge.svelte.d.ts +18 -0
- package/dist/components/Breadcrumb.svelte +46 -0
- package/dist/components/Breadcrumb.svelte.d.ts +14 -0
- package/dist/components/Button.svelte +94 -0
- package/dist/components/Button.svelte.d.ts +21 -0
- package/dist/components/Calendar.svelte +206 -0
- package/dist/components/Calendar.svelte.d.ts +22 -0
- package/dist/components/Card.svelte +76 -0
- package/dist/components/Card.svelte.d.ts +21 -0
- package/dist/components/Checkbox.svelte +69 -0
- package/dist/components/Checkbox.svelte.d.ts +15 -0
- package/dist/components/CheckboxGroup.svelte +109 -0
- package/dist/components/CheckboxGroup.svelte.d.ts +30 -0
- package/dist/components/Combobox.svelte +268 -0
- package/dist/components/Combobox.svelte.d.ts +29 -0
- package/dist/components/CommandPalette.svelte +226 -0
- package/dist/components/CommandPalette.svelte.d.ts +32 -0
- package/dist/components/ConfirmDialog.svelte +97 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +22 -0
- package/dist/components/ContextMenu.svelte +90 -0
- package/dist/components/ContextMenu.svelte.d.ts +15 -0
- package/dist/components/DatePicker.svelte +186 -0
- package/dist/components/DatePicker.svelte.d.ts +23 -0
- package/dist/components/Divider.svelte +30 -0
- package/dist/components/Divider.svelte.d.ts +10 -0
- package/dist/components/Drawer.svelte +161 -0
- package/dist/components/Drawer.svelte.d.ts +20 -0
- package/dist/components/Dropdown.svelte +112 -0
- package/dist/components/Dropdown.svelte.d.ts +23 -0
- package/dist/components/Dropzone.svelte +226 -0
- package/dist/components/Dropzone.svelte.d.ts +32 -0
- package/dist/components/EmptyState.svelte +57 -0
- package/dist/components/EmptyState.svelte.d.ts +18 -0
- package/dist/components/Field.svelte +42 -0
- package/dist/components/Field.svelte.d.ts +16 -0
- package/dist/components/Footer.svelte +93 -0
- package/dist/components/Footer.svelte.d.ts +25 -0
- package/dist/components/Input.svelte +60 -0
- package/dist/components/Input.svelte.d.ts +18 -0
- package/dist/components/InputGroup.svelte +19 -0
- package/dist/components/InputGroup.svelte.d.ts +10 -0
- package/dist/components/Kbd.svelte +17 -0
- package/dist/components/Kbd.svelte.d.ts +8 -0
- package/dist/components/Label.svelte +16 -0
- package/dist/components/Label.svelte.d.ts +9 -0
- package/dist/components/Link.svelte +44 -0
- package/dist/components/Link.svelte.d.ts +16 -0
- package/dist/components/LocaleProvider.svelte +24 -0
- package/dist/components/LocaleProvider.svelte.d.ts +10 -0
- package/dist/components/MenuItem.svelte +58 -0
- package/dist/components/MenuItem.svelte.d.ts +18 -0
- package/dist/components/MenuSeparator.svelte +22 -0
- package/dist/components/MenuSeparator.svelte.d.ts +8 -0
- package/dist/components/Modal.svelte +114 -0
- package/dist/components/Modal.svelte.d.ts +21 -0
- package/dist/components/MultiSelect.svelte +292 -0
- package/dist/components/MultiSelect.svelte.d.ts +31 -0
- package/dist/components/Navbar.svelte +190 -0
- package/dist/components/Navbar.svelte.d.ts +33 -0
- package/dist/components/NumberInput.svelte +100 -0
- package/dist/components/NumberInput.svelte.d.ts +15 -0
- package/dist/components/Pagination.svelte +90 -0
- package/dist/components/Pagination.svelte.d.ts +15 -0
- package/dist/components/PasswordInput.svelte +104 -0
- package/dist/components/PasswordInput.svelte.d.ts +12 -0
- package/dist/components/Popover.svelte +114 -0
- package/dist/components/Popover.svelte.d.ts +19 -0
- package/dist/components/Progress.svelte +56 -0
- package/dist/components/Progress.svelte.d.ts +17 -0
- package/dist/components/Radio.svelte +55 -0
- package/dist/components/Radio.svelte.d.ts +15 -0
- package/dist/components/RadioGroup.svelte +93 -0
- package/dist/components/RadioGroup.svelte.d.ts +27 -0
- package/dist/components/SegmentedControl.svelte +88 -0
- package/dist/components/SegmentedControl.svelte.d.ts +25 -0
- package/dist/components/Select.svelte +61 -0
- package/dist/components/Select.svelte.d.ts +20 -0
- package/dist/components/Sidebar.svelte +206 -0
- package/dist/components/Sidebar.svelte.d.ts +36 -0
- package/dist/components/Skeleton.svelte +46 -0
- package/dist/components/Skeleton.svelte.d.ts +13 -0
- package/dist/components/Slider.svelte +79 -0
- package/dist/components/Slider.svelte.d.ts +19 -0
- package/dist/components/Spinner.svelte +34 -0
- package/dist/components/Spinner.svelte.d.ts +13 -0
- package/dist/components/Stat.svelte +49 -0
- package/dist/components/Stat.svelte.d.ts +16 -0
- package/dist/components/Steps.svelte +136 -0
- package/dist/components/Steps.svelte.d.ts +24 -0
- package/dist/components/Switch.svelte +64 -0
- package/dist/components/Switch.svelte.d.ts +14 -0
- package/dist/components/Table.svelte +236 -0
- package/dist/components/Table.svelte.d.ts +70 -0
- package/dist/components/Tabs.svelte +91 -0
- package/dist/components/Tabs.svelte.d.ts +23 -0
- package/dist/components/Textarea.svelte +54 -0
- package/dist/components/Textarea.svelte.d.ts +12 -0
- package/dist/components/ThemeToggle.svelte +133 -0
- package/dist/components/ThemeToggle.svelte.d.ts +20 -0
- package/dist/components/Toaster.svelte +110 -0
- package/dist/components/Toaster.svelte.d.ts +8 -0
- package/dist/components/Tooltip.svelte +33 -0
- package/dist/components/Tooltip.svelte.d.ts +12 -0
- package/dist/date.d.ts +35 -0
- package/dist/date.js +108 -0
- package/dist/fonts.css +193 -0
- package/dist/index.d.ts +105 -0
- package/dist/index.js +77 -0
- package/dist/locale.svelte.d.ts +93 -0
- package/dist/locale.svelte.js +153 -0
- package/dist/theme.css +210 -0
- package/dist/toast.svelte.d.ts +47 -0
- package/dist/toast.svelte.js +60 -0
- package/dist/tones.d.ts +48 -0
- package/dist/tones.js +209 -0
- package/package.json +349 -0
- package/registry.json +669 -0
|
@@ -0,0 +1,236 @@
|
|
|
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 font-mono'`. */
|
|
7
|
+
class?: string;
|
|
8
|
+
/** Allow clicking the header to sort by this column. */
|
|
9
|
+
sortable?: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type SortDirection = 'asc' | 'desc';
|
|
13
|
+
|
|
14
|
+
export interface TableSort {
|
|
15
|
+
key: string;
|
|
16
|
+
direction: SortDirection;
|
|
17
|
+
}
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<script lang="ts" generics="T extends Record<string, unknown>">
|
|
21
|
+
import type { Snippet } from 'svelte';
|
|
22
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
23
|
+
import Checkbox from './Checkbox.svelte';
|
|
24
|
+
import type { Tone } from '../tones.js';
|
|
25
|
+
import { useLocale } from '../locale.svelte.js';
|
|
26
|
+
|
|
27
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> {
|
|
28
|
+
columns: TableColumn[];
|
|
29
|
+
rows: T[];
|
|
30
|
+
/** Custom cell rendering — falls back to the raw value. */
|
|
31
|
+
cell?: Snippet<[{ row: T; column: TableColumn; value: unknown }]>;
|
|
32
|
+
/** Shown instead of the body when `rows` is empty. */
|
|
33
|
+
empty?: Snippet;
|
|
34
|
+
caption?: string;
|
|
35
|
+
striped?: boolean;
|
|
36
|
+
hoverable?: boolean;
|
|
37
|
+
compact?: boolean;
|
|
38
|
+
/** Header stays put while the body scrolls — pair with a `max-h-*` class. */
|
|
39
|
+
stickyHeader?: boolean;
|
|
40
|
+
|
|
41
|
+
/** Bindable. Set it to sort; leave `sortable` columns to drive it. */
|
|
42
|
+
sort?: TableSort | null;
|
|
43
|
+
/** Sort the rows here rather than refetching them server-side. Default true. */
|
|
44
|
+
sortLocally?: boolean;
|
|
45
|
+
onsort?: (sort: TableSort | null) => void;
|
|
46
|
+
|
|
47
|
+
/** Turns on the checkbox column. Bindable list of selected row keys. */
|
|
48
|
+
selectable?: boolean;
|
|
49
|
+
selected?: string[];
|
|
50
|
+
/** Which field identifies a row. Default `'id'`. */
|
|
51
|
+
rowKey?: string;
|
|
52
|
+
tone?: Tone;
|
|
53
|
+
onselect?: (selected: string[]) => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let {
|
|
57
|
+
columns,
|
|
58
|
+
rows,
|
|
59
|
+
cell,
|
|
60
|
+
empty,
|
|
61
|
+
caption,
|
|
62
|
+
striped = false,
|
|
63
|
+
hoverable = true,
|
|
64
|
+
compact = false,
|
|
65
|
+
stickyHeader = false,
|
|
66
|
+
sort = $bindable(null),
|
|
67
|
+
sortLocally = true,
|
|
68
|
+
onsort,
|
|
69
|
+
selectable = false,
|
|
70
|
+
selected = $bindable([]),
|
|
71
|
+
rowKey = 'id',
|
|
72
|
+
tone = 'brand',
|
|
73
|
+
onselect,
|
|
74
|
+
class: className = '',
|
|
75
|
+
...rest
|
|
76
|
+
}: Props = $props();
|
|
77
|
+
|
|
78
|
+
const t = useLocale();
|
|
79
|
+
|
|
80
|
+
const aligns = {
|
|
81
|
+
left: 'text-left',
|
|
82
|
+
center: 'text-center',
|
|
83
|
+
right: 'text-right'
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const pad = $derived(compact ? 'px-4 py-2' : 'px-4 py-3');
|
|
87
|
+
|
|
88
|
+
const sorted = $derived.by(() => {
|
|
89
|
+
if (!sort || !sortLocally) return rows;
|
|
90
|
+
const { key, direction } = sort;
|
|
91
|
+
const factor = direction === 'asc' ? 1 : -1;
|
|
92
|
+
return [...rows].sort((a, b) => {
|
|
93
|
+
const x = a[key];
|
|
94
|
+
const y = b[key];
|
|
95
|
+
if (x == null) return 1;
|
|
96
|
+
if (y == null) return -1;
|
|
97
|
+
if (typeof x === 'number' && typeof y === 'number') return (x - y) * factor;
|
|
98
|
+
return String(x).localeCompare(String(y), undefined, { numeric: true }) * factor;
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
function toggleSort(column: TableColumn) {
|
|
103
|
+
if (!column.sortable) return;
|
|
104
|
+
let next: TableSort | null;
|
|
105
|
+
if (sort?.key !== column.key) next = { key: column.key, direction: 'asc' };
|
|
106
|
+
else if (sort.direction === 'asc') next = { key: column.key, direction: 'desc' };
|
|
107
|
+
else next = null;
|
|
108
|
+
sort = next;
|
|
109
|
+
onsort?.(next);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const keyOf = (row: T) => String(row[rowKey] ?? '');
|
|
113
|
+
|
|
114
|
+
const allSelected = $derived(
|
|
115
|
+
sorted.length > 0 && sorted.every((row) => selected.includes(keyOf(row)))
|
|
116
|
+
);
|
|
117
|
+
const someSelected = $derived(
|
|
118
|
+
!allSelected && sorted.some((row) => selected.includes(keyOf(row)))
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
function toggleAll() {
|
|
122
|
+
selected = allSelected ? [] : sorted.map(keyOf);
|
|
123
|
+
onselect?.(selected);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function toggleRow(row: T) {
|
|
127
|
+
const key = keyOf(row);
|
|
128
|
+
selected = selected.includes(key) ? selected.filter((k) => k !== key) : [...selected, key];
|
|
129
|
+
onselect?.(selected);
|
|
130
|
+
}
|
|
131
|
+
</script>
|
|
132
|
+
|
|
133
|
+
<div class="w-full overflow-auto border border-hairline {className}" {...rest}>
|
|
134
|
+
<table class="w-full border-collapse font-sans text-[15px]">
|
|
135
|
+
{#if caption}
|
|
136
|
+
<caption
|
|
137
|
+
class="border-b border-hairline px-4 py-3 text-left font-sans text-sm text-text-muted"
|
|
138
|
+
>
|
|
139
|
+
{caption}
|
|
140
|
+
</caption>
|
|
141
|
+
{/if}
|
|
142
|
+
<thead class={stickyHeader ? 'sticky top-0 z-10' : ''}>
|
|
143
|
+
<tr class="border-b border-hairline bg-bg-alt">
|
|
144
|
+
{#if selectable}
|
|
145
|
+
<th scope="col" class="w-10 {pad}">
|
|
146
|
+
<Checkbox
|
|
147
|
+
checked={allSelected}
|
|
148
|
+
indeterminate={someSelected}
|
|
149
|
+
{tone}
|
|
150
|
+
aria-label={t.current.selectAllRows}
|
|
151
|
+
onchange={toggleAll}
|
|
152
|
+
/>
|
|
153
|
+
</th>
|
|
154
|
+
{/if}
|
|
155
|
+
{#each columns as column (column.key)}
|
|
156
|
+
<th
|
|
157
|
+
scope="col"
|
|
158
|
+
aria-sort={sort?.key === column.key
|
|
159
|
+
? sort.direction === 'asc'
|
|
160
|
+
? 'ascending'
|
|
161
|
+
: 'descending'
|
|
162
|
+
: undefined}
|
|
163
|
+
class="font-mono text-xs font-medium tracking-wide text-text-muted uppercase {pad} {aligns[
|
|
164
|
+
column.align ?? 'left'
|
|
165
|
+
]} {column.class ?? ''}"
|
|
166
|
+
>
|
|
167
|
+
{#if column.sortable}
|
|
168
|
+
<button
|
|
169
|
+
type="button"
|
|
170
|
+
onclick={() => toggleSort(column)}
|
|
171
|
+
class="inline-flex items-center gap-1.5 uppercase transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current
|
|
172
|
+
{sort?.key === column.key ? 'text-text' : ''}"
|
|
173
|
+
>
|
|
174
|
+
{column.label}
|
|
175
|
+
<span class="text-[9px] leading-none" aria-hidden="true">
|
|
176
|
+
{#if sort?.key === column.key}
|
|
177
|
+
{sort.direction === 'asc' ? '▲' : '▼'}
|
|
178
|
+
{:else}
|
|
179
|
+
⇅
|
|
180
|
+
{/if}
|
|
181
|
+
</span>
|
|
182
|
+
</button>
|
|
183
|
+
{:else}
|
|
184
|
+
{column.label}
|
|
185
|
+
{/if}
|
|
186
|
+
</th>
|
|
187
|
+
{/each}
|
|
188
|
+
</tr>
|
|
189
|
+
</thead>
|
|
190
|
+
<tbody>
|
|
191
|
+
{#if sorted.length === 0}
|
|
192
|
+
<tr>
|
|
193
|
+
<td
|
|
194
|
+
colspan={columns.length + (selectable ? 1 : 0)}
|
|
195
|
+
class="px-4 py-10 text-center text-sm text-text-muted"
|
|
196
|
+
>
|
|
197
|
+
{#if empty}{@render empty()}{:else}{t.current.noData}{/if}
|
|
198
|
+
</td>
|
|
199
|
+
</tr>
|
|
200
|
+
{:else}
|
|
201
|
+
{#each sorted as row, i (selectable ? keyOf(row) : i)}
|
|
202
|
+
{@const isSelected = selectable && selected.includes(keyOf(row))}
|
|
203
|
+
<tr
|
|
204
|
+
aria-selected={selectable ? isSelected : undefined}
|
|
205
|
+
class="border-b border-hairline last:border-b-0
|
|
206
|
+
{isSelected ? 'bg-bg-inset' : striped && i % 2 === 1 ? 'bg-bg-alt' : ''}
|
|
207
|
+
{hoverable ? 'transition-colors duration-150 hover:bg-bg-inset' : ''}"
|
|
208
|
+
>
|
|
209
|
+
{#if selectable}
|
|
210
|
+
<td class={pad}>
|
|
211
|
+
<Checkbox
|
|
212
|
+
checked={isSelected}
|
|
213
|
+
{tone}
|
|
214
|
+
aria-label={t.current.selectRow}
|
|
215
|
+
onchange={() => toggleRow(row)}
|
|
216
|
+
/>
|
|
217
|
+
</td>
|
|
218
|
+
{/if}
|
|
219
|
+
{#each columns as column (column.key)}
|
|
220
|
+
<td
|
|
221
|
+
class="text-text-secondary {pad} {aligns[column.align ?? 'left']} {column.class ??
|
|
222
|
+
''}"
|
|
223
|
+
>
|
|
224
|
+
{#if cell}
|
|
225
|
+
{@render cell({ row, column, value: row[column.key] })}
|
|
226
|
+
{:else}
|
|
227
|
+
{row[column.key] ?? ''}
|
|
228
|
+
{/if}
|
|
229
|
+
</td>
|
|
230
|
+
{/each}
|
|
231
|
+
</tr>
|
|
232
|
+
{/each}
|
|
233
|
+
{/if}
|
|
234
|
+
</tbody>
|
|
235
|
+
</table>
|
|
236
|
+
</div>
|
|
@@ -0,0 +1,70 @@
|
|
|
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 font-mono'`. */
|
|
6
|
+
class?: string;
|
|
7
|
+
/** Allow clicking the header to sort by this column. */
|
|
8
|
+
sortable?: boolean;
|
|
9
|
+
}
|
|
10
|
+
export type SortDirection = 'asc' | 'desc';
|
|
11
|
+
export interface TableSort {
|
|
12
|
+
key: string;
|
|
13
|
+
direction: SortDirection;
|
|
14
|
+
}
|
|
15
|
+
import type { Snippet } from 'svelte';
|
|
16
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
17
|
+
import type { Tone } from '../tones.js';
|
|
18
|
+
declare function $$render<T extends Record<string, unknown>>(): {
|
|
19
|
+
props: Omit<HTMLAttributes<HTMLDivElement>, "onselect"> & {
|
|
20
|
+
columns: TableColumn[];
|
|
21
|
+
rows: T[];
|
|
22
|
+
/** Custom cell rendering — falls back to the raw value. */
|
|
23
|
+
cell?: Snippet<[{
|
|
24
|
+
row: T;
|
|
25
|
+
column: TableColumn;
|
|
26
|
+
value: unknown;
|
|
27
|
+
}]>;
|
|
28
|
+
/** Shown instead of the body when `rows` is empty. */
|
|
29
|
+
empty?: Snippet;
|
|
30
|
+
caption?: string;
|
|
31
|
+
striped?: boolean;
|
|
32
|
+
hoverable?: boolean;
|
|
33
|
+
compact?: boolean;
|
|
34
|
+
/** Header stays put while the body scrolls — pair with a `max-h-*` class. */
|
|
35
|
+
stickyHeader?: boolean;
|
|
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
|
+
/** Turns on the checkbox column. Bindable list of selected row keys. */
|
|
42
|
+
selectable?: boolean;
|
|
43
|
+
selected?: string[];
|
|
44
|
+
/** Which field identifies a row. Default `'id'`. */
|
|
45
|
+
rowKey?: string;
|
|
46
|
+
tone?: Tone;
|
|
47
|
+
onselect?: (selected: string[]) => void;
|
|
48
|
+
};
|
|
49
|
+
exports: {};
|
|
50
|
+
bindings: "sort" | "selected";
|
|
51
|
+
slots: {};
|
|
52
|
+
events: {};
|
|
53
|
+
};
|
|
54
|
+
declare class __sveltets_Render<T extends Record<string, unknown>> {
|
|
55
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
56
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
57
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
58
|
+
bindings(): "sort" | "selected";
|
|
59
|
+
exports(): {};
|
|
60
|
+
}
|
|
61
|
+
interface $$IsomorphicComponent {
|
|
62
|
+
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']>> & {
|
|
63
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
64
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
65
|
+
<T extends Record<string, unknown>>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
66
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
67
|
+
}
|
|
68
|
+
declare const Table: $$IsomorphicComponent;
|
|
69
|
+
type Table<T extends Record<string, unknown>> = InstanceType<typeof Table<T>>;
|
|
70
|
+
export default Table;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { focusRing, toneBorder, toneRing, toneSoft, toneText, type Tone } from '../tones.js';
|
|
4
|
+
|
|
5
|
+
export interface TabItem {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
/** Small count/label rendered after the tab title. */
|
|
10
|
+
badge?: string | number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type TabsVariant = 'underline' | 'pill' | 'segmented';
|
|
14
|
+
|
|
15
|
+
interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onchange'> {
|
|
16
|
+
items: TabItem[];
|
|
17
|
+
/** Bindable — the `value` of the active tab. */
|
|
18
|
+
value?: string;
|
|
19
|
+
variant?: TabsVariant;
|
|
20
|
+
tone?: Tone;
|
|
21
|
+
/** Spread tabs evenly across the full width. */
|
|
22
|
+
fullWidth?: boolean;
|
|
23
|
+
onchange?: (value: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let {
|
|
27
|
+
items,
|
|
28
|
+
value = $bindable(items[0]?.value ?? ''),
|
|
29
|
+
variant = 'underline',
|
|
30
|
+
tone = 'brand',
|
|
31
|
+
fullWidth = false,
|
|
32
|
+
onchange,
|
|
33
|
+
class: className = '',
|
|
34
|
+
...rest
|
|
35
|
+
}: Props = $props();
|
|
36
|
+
|
|
37
|
+
function select(item: TabItem) {
|
|
38
|
+
if (item.disabled) return;
|
|
39
|
+
value = item.value;
|
|
40
|
+
onchange?.(item.value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const listClass = $derived(
|
|
44
|
+
{
|
|
45
|
+
underline: 'gap-6 border-b border-hairline',
|
|
46
|
+
pill: 'gap-2',
|
|
47
|
+
segmented: 'gap-0 border border-hairline bg-bg-alt p-1'
|
|
48
|
+
}[variant]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
function tabClass(active: boolean) {
|
|
52
|
+
const base = `inline-flex items-center justify-center gap-2 font-sans text-[15px] whitespace-nowrap transition-all duration-150 ease-brand-out disabled:opacity-40 disabled:pointer-events-none ${focusRing} ${toneRing[tone]}`;
|
|
53
|
+
if (variant === 'underline') {
|
|
54
|
+
return `${base} -mb-px border-b-2 pb-3 ${
|
|
55
|
+
active
|
|
56
|
+
? `${toneBorder[tone]} ${toneText[tone]} font-medium`
|
|
57
|
+
: 'border-transparent text-text-secondary hover:text-text'
|
|
58
|
+
}`;
|
|
59
|
+
}
|
|
60
|
+
if (variant === 'pill') {
|
|
61
|
+
return `${base} h-9 rounded-full px-4 ${
|
|
62
|
+
active ? `${toneSoft[tone]} font-medium` : 'text-text-secondary hover:bg-bg-inset'
|
|
63
|
+
}`;
|
|
64
|
+
}
|
|
65
|
+
return `${base} h-9 px-4 ${
|
|
66
|
+
active ? 'bg-bg font-medium text-text' : 'text-text-secondary hover:text-text'
|
|
67
|
+
}`;
|
|
68
|
+
}
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<div
|
|
72
|
+
role="tablist"
|
|
73
|
+
class="flex items-center overflow-x-auto {listClass} {fullWidth ? '*:flex-1' : ''} {className}"
|
|
74
|
+
{...rest}
|
|
75
|
+
>
|
|
76
|
+
{#each items as item (item.value)}
|
|
77
|
+
<button
|
|
78
|
+
type="button"
|
|
79
|
+
role="tab"
|
|
80
|
+
aria-selected={value === item.value}
|
|
81
|
+
disabled={item.disabled}
|
|
82
|
+
onclick={() => select(item)}
|
|
83
|
+
class={tabClass(value === item.value)}
|
|
84
|
+
>
|
|
85
|
+
{item.label}
|
|
86
|
+
{#if item.badge !== undefined}
|
|
87
|
+
<span class="font-mono text-[11px] text-text-muted">{item.badge}</span>
|
|
88
|
+
{/if}
|
|
89
|
+
</button>
|
|
90
|
+
{/each}
|
|
91
|
+
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import { type Tone } from '../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,54 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
3
|
+
import { toneFocusBorder, type Tone } from '../tones.js';
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLTextareaAttributes {
|
|
6
|
+
value?: string;
|
|
7
|
+
tone?: Tone;
|
|
8
|
+
invalid?: boolean;
|
|
9
|
+
/** Grow with the content instead of showing a scrollbar. */
|
|
10
|
+
autoresize?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
value = $bindable(''),
|
|
15
|
+
tone = 'brand',
|
|
16
|
+
invalid = false,
|
|
17
|
+
autoresize = false,
|
|
18
|
+
rows = 4,
|
|
19
|
+
class: className = '',
|
|
20
|
+
...rest
|
|
21
|
+
}: Props = $props();
|
|
22
|
+
|
|
23
|
+
/** Grows the field to fit its content on input, and once on mount. */
|
|
24
|
+
function autosize(node: HTMLTextAreaElement, enabled: boolean) {
|
|
25
|
+
let on = enabled;
|
|
26
|
+
const resize = () => {
|
|
27
|
+
if (!on) return;
|
|
28
|
+
node.style.height = 'auto';
|
|
29
|
+
node.style.height = `${node.scrollHeight}px`;
|
|
30
|
+
};
|
|
31
|
+
resize();
|
|
32
|
+
node.addEventListener('input', resize);
|
|
33
|
+
return {
|
|
34
|
+
update(next: boolean) {
|
|
35
|
+
on = next;
|
|
36
|
+
if (on) resize();
|
|
37
|
+
else node.style.height = '';
|
|
38
|
+
},
|
|
39
|
+
destroy() {
|
|
40
|
+
node.removeEventListener('input', resize);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<textarea
|
|
47
|
+
use:autosize={autoresize}
|
|
48
|
+
bind:value
|
|
49
|
+
{rows}
|
|
50
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
51
|
+
class="w-full border bg-bg px-4 py-3 font-sans text-[15px] text-text transition-colors duration-150 ease-brand-out placeholder:text-text-muted focus:outline-none disabled:pointer-events-none disabled:opacity-50
|
|
52
|
+
{autoresize ? 'resize-none overflow-hidden' : 'resize-y'}
|
|
53
|
+
{invalid ? 'border-danger' : `border-hairline ${toneFocusBorder[tone]}`} {className}"
|
|
54
|
+
{...rest}></textarea>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { HTMLTextareaAttributes } from 'svelte/elements';
|
|
2
|
+
import { type Tone } from '../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,133 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export type Theme = 'light' | 'dark' | 'system';
|
|
3
|
+
|
|
4
|
+
const STORAGE_KEY = 'nqm-theme';
|
|
5
|
+
|
|
6
|
+
/** Writes the theme classes `theme.css` reads. Safe to call before mount. */
|
|
7
|
+
export function applyTheme(theme: Theme) {
|
|
8
|
+
const root = document.documentElement;
|
|
9
|
+
root.classList.toggle('dark', theme === 'dark');
|
|
10
|
+
root.classList.toggle('light', theme === 'light');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The stored choice, or `'system'` when the visitor has never picked one. */
|
|
14
|
+
export function storedTheme(): Theme {
|
|
15
|
+
if (typeof localStorage === 'undefined') return 'system';
|
|
16
|
+
const saved = localStorage.getItem(STORAGE_KEY);
|
|
17
|
+
return saved === 'light' || saved === 'dark' ? saved : 'system';
|
|
18
|
+
}
|
|
19
|
+
</script>
|
|
20
|
+
|
|
21
|
+
<script lang="ts">
|
|
22
|
+
import { focusRing, toneRing, toneSoft, type Tone } from '../tones.js';
|
|
23
|
+
import { useLocale } from '../locale.svelte.js';
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
/** Bindable. `'system'` follows the OS until the visitor chooses. */
|
|
27
|
+
theme?: Theme;
|
|
28
|
+
/** `'button'` cycles light → dark → system; `'segmented'` shows all three. */
|
|
29
|
+
variant?: 'button' | 'segmented';
|
|
30
|
+
tone?: Tone;
|
|
31
|
+
/** Remember the choice in `localStorage`. Default true. */
|
|
32
|
+
persist?: boolean;
|
|
33
|
+
onchange?: (theme: Theme) => void;
|
|
34
|
+
class?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
theme = $bindable(storedTheme()),
|
|
39
|
+
variant = 'button',
|
|
40
|
+
tone = 'neutral',
|
|
41
|
+
persist = true,
|
|
42
|
+
onchange,
|
|
43
|
+
class: className = ''
|
|
44
|
+
}: Props = $props();
|
|
45
|
+
|
|
46
|
+
const t = useLocale();
|
|
47
|
+
|
|
48
|
+
$effect(() => {
|
|
49
|
+
applyTheme(theme);
|
|
50
|
+
if (persist && typeof localStorage !== 'undefined') {
|
|
51
|
+
if (theme === 'system') localStorage.removeItem(STORAGE_KEY);
|
|
52
|
+
else localStorage.setItem(STORAGE_KEY, theme);
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
function set(next: Theme) {
|
|
57
|
+
theme = next;
|
|
58
|
+
onchange?.(next);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function cycle() {
|
|
62
|
+
set(theme === 'light' ? 'dark' : theme === 'dark' ? 'system' : 'light');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const options = $derived([
|
|
66
|
+
{ value: 'light' as Theme, label: t.current.lightMode },
|
|
67
|
+
{ value: 'dark' as Theme, label: t.current.darkMode },
|
|
68
|
+
{ value: 'system' as Theme, label: 'Auto' }
|
|
69
|
+
]);
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
{#snippet glyph(which: Theme)}
|
|
73
|
+
{#if which === 'light'}
|
|
74
|
+
<svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
75
|
+
<circle cx="10" cy="10" r="3.5" stroke="currentColor" stroke-width="1.5" />
|
|
76
|
+
<path
|
|
77
|
+
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"
|
|
78
|
+
stroke="currentColor"
|
|
79
|
+
stroke-width="1.5"
|
|
80
|
+
/>
|
|
81
|
+
</svg>
|
|
82
|
+
{:else if which === 'dark'}
|
|
83
|
+
<svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
84
|
+
<path
|
|
85
|
+
d="M16 12.3A6.8 6.8 0 0 1 7.7 4a6.8 6.8 0 1 0 8.3 8.3Z"
|
|
86
|
+
stroke="currentColor"
|
|
87
|
+
stroke-width="1.5"
|
|
88
|
+
/>
|
|
89
|
+
</svg>
|
|
90
|
+
{:else}
|
|
91
|
+
<svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
92
|
+
<rect x="2.5" y="4" width="15" height="10" stroke="currentColor" stroke-width="1.5" />
|
|
93
|
+
<path d="M7 17h6" stroke="currentColor" stroke-width="1.5" />
|
|
94
|
+
</svg>
|
|
95
|
+
{/if}
|
|
96
|
+
{/snippet}
|
|
97
|
+
|
|
98
|
+
{#if variant === 'segmented'}
|
|
99
|
+
<div
|
|
100
|
+
role="radiogroup"
|
|
101
|
+
aria-label={t.current.toggleTheme}
|
|
102
|
+
class="inline-flex items-center gap-0 border border-hairline bg-bg-alt p-1 {className}"
|
|
103
|
+
>
|
|
104
|
+
{#each options as option (option.value)}
|
|
105
|
+
<button
|
|
106
|
+
type="button"
|
|
107
|
+
role="radio"
|
|
108
|
+
aria-checked={theme === option.value}
|
|
109
|
+
aria-label={option.label}
|
|
110
|
+
onclick={() => set(option.value)}
|
|
111
|
+
class="inline-flex h-8 items-center gap-1.5 px-3 font-sans text-[13px] transition-colors duration-150 ease-brand-out {focusRing} {toneRing[
|
|
112
|
+
tone
|
|
113
|
+
]}
|
|
114
|
+
{theme === option.value ? 'bg-bg font-medium text-text' : 'text-text-secondary hover:text-text'}"
|
|
115
|
+
>
|
|
116
|
+
{@render glyph(option.value)}
|
|
117
|
+
<span class="hidden sm:inline">{option.label}</span>
|
|
118
|
+
</button>
|
|
119
|
+
{/each}
|
|
120
|
+
</div>
|
|
121
|
+
{:else}
|
|
122
|
+
<button
|
|
123
|
+
type="button"
|
|
124
|
+
onclick={cycle}
|
|
125
|
+
aria-label="{t.current.toggleTheme} ({theme})"
|
|
126
|
+
title={t.current.toggleTheme}
|
|
127
|
+
class="inline-flex size-9 items-center justify-center border border-hairline transition-colors duration-150 ease-brand-out {focusRing} {toneRing[
|
|
128
|
+
tone
|
|
129
|
+
]} {toneSoft[tone]} {className}"
|
|
130
|
+
>
|
|
131
|
+
{@render glyph(theme)}
|
|
132
|
+
</button>
|
|
133
|
+
{/if}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export type Theme = 'light' | 'dark' | 'system';
|
|
2
|
+
/** Writes the theme classes `theme.css` reads. Safe to call before mount. */
|
|
3
|
+
export declare function applyTheme(theme: Theme): void;
|
|
4
|
+
/** The stored choice, or `'system'` when the visitor has never picked one. */
|
|
5
|
+
export declare function storedTheme(): Theme;
|
|
6
|
+
import { type Tone } from '../tones.js';
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Bindable. `'system'` follows the OS until the visitor chooses. */
|
|
9
|
+
theme?: Theme;
|
|
10
|
+
/** `'button'` cycles light → dark → system; `'segmented'` shows all three. */
|
|
11
|
+
variant?: 'button' | 'segmented';
|
|
12
|
+
tone?: Tone;
|
|
13
|
+
/** Remember the choice in `localStorage`. Default true. */
|
|
14
|
+
persist?: boolean;
|
|
15
|
+
onchange?: (theme: Theme) => void;
|
|
16
|
+
class?: string;
|
|
17
|
+
}
|
|
18
|
+
declare const ThemeToggle: import("svelte").Component<Props, {}, "theme">;
|
|
19
|
+
type ThemeToggle = ReturnType<typeof ThemeToggle>;
|
|
20
|
+
export default ThemeToggle;
|