@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,226 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import { toneBorder, toneSoft, toneSurface, toneText, type Tone } from '../tones.js';
|
|
4
|
+
import { useLocale } from '../locale.svelte.js';
|
|
5
|
+
|
|
6
|
+
export interface RejectedFile {
|
|
7
|
+
file: File;
|
|
8
|
+
reason: 'type' | 'size' | 'count';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** Bindable list of accepted files. */
|
|
13
|
+
files?: File[];
|
|
14
|
+
/** Same syntax as the input's `accept` — `'image/*,.pdf'`. */
|
|
15
|
+
accept?: string;
|
|
16
|
+
multiple?: boolean;
|
|
17
|
+
/** Per-file ceiling in bytes. `0` means no limit. */
|
|
18
|
+
maxSize?: number;
|
|
19
|
+
/** Cap on how many files may be held at once. `0` means no limit. */
|
|
20
|
+
maxFiles?: number;
|
|
21
|
+
disabled?: boolean;
|
|
22
|
+
tone?: Tone;
|
|
23
|
+
/** Headline inside the zone. Defaults to the locale's `dropFiles`. */
|
|
24
|
+
label?: string;
|
|
25
|
+
hint?: string;
|
|
26
|
+
/** Hide the built-in file list to render your own. */
|
|
27
|
+
showList?: boolean;
|
|
28
|
+
onaccept?: (files: File[]) => void;
|
|
29
|
+
onreject?: (rejected: RejectedFile[]) => void;
|
|
30
|
+
/** Replaces the glyph above the label. */
|
|
31
|
+
icon?: Snippet;
|
|
32
|
+
class?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let {
|
|
36
|
+
files = $bindable([]),
|
|
37
|
+
accept,
|
|
38
|
+
multiple = true,
|
|
39
|
+
maxSize = 0,
|
|
40
|
+
maxFiles = 0,
|
|
41
|
+
disabled = false,
|
|
42
|
+
tone = 'brand',
|
|
43
|
+
label,
|
|
44
|
+
hint,
|
|
45
|
+
showList = true,
|
|
46
|
+
onaccept,
|
|
47
|
+
onreject,
|
|
48
|
+
icon,
|
|
49
|
+
class: className = ''
|
|
50
|
+
}: Props = $props();
|
|
51
|
+
|
|
52
|
+
const t = useLocale();
|
|
53
|
+
|
|
54
|
+
let input: HTMLInputElement | null = $state(null);
|
|
55
|
+
let dragging = $state(false);
|
|
56
|
+
let rejected = $state<RejectedFile[]>([]);
|
|
57
|
+
/** Nested elements fire dragleave too, so count depth instead of toggling. */
|
|
58
|
+
let depth = 0;
|
|
59
|
+
|
|
60
|
+
const defaultHint = $derived.by(() => {
|
|
61
|
+
if (hint) return hint;
|
|
62
|
+
// Symbols rather than words, so the auto-hint needs no translation.
|
|
63
|
+
const parts: string[] = [];
|
|
64
|
+
if (accept) parts.push(accept.replace(/,\s*/g, ', '));
|
|
65
|
+
if (maxSize) parts.push(`≤ ${formatSize(maxSize)}`);
|
|
66
|
+
if (maxFiles) parts.push(`×${maxFiles}`);
|
|
67
|
+
return parts.join(' · ');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
function formatSize(bytes: number) {
|
|
71
|
+
const units = t.current.byteUnits;
|
|
72
|
+
if (bytes < 1024) return `${bytes} ${units[0]}`;
|
|
73
|
+
let value = bytes / 1024;
|
|
74
|
+
let i = 1;
|
|
75
|
+
while (value >= 1024 && i < units.length - 1) {
|
|
76
|
+
value /= 1024;
|
|
77
|
+
i++;
|
|
78
|
+
}
|
|
79
|
+
return `${value.toFixed(Number.isInteger(value) || value >= 10 ? 0 : 1)} ${units[i]}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function matchesAccept(file: File) {
|
|
83
|
+
if (!accept) return true;
|
|
84
|
+
return accept.split(',').some((raw) => {
|
|
85
|
+
const rule = raw.trim().toLowerCase();
|
|
86
|
+
if (!rule) return true;
|
|
87
|
+
if (rule.startsWith('.')) return file.name.toLowerCase().endsWith(rule);
|
|
88
|
+
if (rule.endsWith('/*')) return file.type.startsWith(rule.slice(0, -1));
|
|
89
|
+
return file.type.toLowerCase() === rule;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function add(incoming: FileList | File[] | null) {
|
|
94
|
+
if (!incoming || disabled) return;
|
|
95
|
+
|
|
96
|
+
const accepted: File[] = [];
|
|
97
|
+
const bad: RejectedFile[] = [];
|
|
98
|
+
let room = maxFiles ? maxFiles - (multiple ? files.length : 0) : Infinity;
|
|
99
|
+
|
|
100
|
+
for (const file of Array.from(incoming)) {
|
|
101
|
+
if (!matchesAccept(file)) bad.push({ file, reason: 'type' });
|
|
102
|
+
else if (maxSize && file.size > maxSize) bad.push({ file, reason: 'size' });
|
|
103
|
+
else if (room <= 0) bad.push({ file, reason: 'count' });
|
|
104
|
+
else {
|
|
105
|
+
accepted.push(file);
|
|
106
|
+
room--;
|
|
107
|
+
}
|
|
108
|
+
if (!multiple && accepted.length === 1) break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
rejected = bad;
|
|
112
|
+
if (bad.length) onreject?.(bad);
|
|
113
|
+
if (accepted.length === 0) return;
|
|
114
|
+
|
|
115
|
+
files = multiple ? [...files, ...accepted] : accepted.slice(0, 1);
|
|
116
|
+
onaccept?.(accepted);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function remove(index: number) {
|
|
120
|
+
files = files.filter((_, i) => i !== index);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function onDrop(event: DragEvent) {
|
|
124
|
+
event.preventDefault();
|
|
125
|
+
depth = 0;
|
|
126
|
+
dragging = false;
|
|
127
|
+
add(event.dataTransfer?.files ?? null);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const reasons: Record<RejectedFile['reason'], string> = $derived({
|
|
131
|
+
type: t.current.rejectedType,
|
|
132
|
+
size: t.current.rejectedSize,
|
|
133
|
+
count: t.current.rejectedCount
|
|
134
|
+
});
|
|
135
|
+
</script>
|
|
136
|
+
|
|
137
|
+
<div class="flex w-full flex-col gap-3 {className}">
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
{disabled}
|
|
141
|
+
onclick={() => input?.click()}
|
|
142
|
+
ondragenter={(event) => {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
depth++;
|
|
145
|
+
dragging = true;
|
|
146
|
+
}}
|
|
147
|
+
ondragover={(event) => event.preventDefault()}
|
|
148
|
+
ondragleave={() => {
|
|
149
|
+
depth = Math.max(0, depth - 1);
|
|
150
|
+
if (depth === 0) dragging = false;
|
|
151
|
+
}}
|
|
152
|
+
ondrop={onDrop}
|
|
153
|
+
class="flex w-full flex-col items-center gap-3 border border-dashed px-6 py-10 text-center font-sans transition-colors duration-150 ease-brand-out focus-visible:outline-2 focus-visible:outline-offset-2 disabled:pointer-events-none disabled:opacity-50
|
|
154
|
+
{dragging
|
|
155
|
+
? `${toneBorder[tone]} ${toneSurface[tone]}`
|
|
156
|
+
: 'border-hairline-strong bg-bg hover:bg-bg-alt'}
|
|
157
|
+
{toneText[tone]}"
|
|
158
|
+
>
|
|
159
|
+
<span class="flex size-12 items-center justify-center rounded-full {toneSoft[tone]}">
|
|
160
|
+
{#if icon}
|
|
161
|
+
{@render icon()}
|
|
162
|
+
{:else}
|
|
163
|
+
<svg class="size-5" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
164
|
+
<path d="M10 14V3.5M6 7.5 10 3.5l4 4" stroke="currentColor" stroke-width="1.5" />
|
|
165
|
+
<path d="M3.5 13v3.5h13V13" stroke="currentColor" stroke-width="1.5" />
|
|
166
|
+
</svg>
|
|
167
|
+
{/if}
|
|
168
|
+
</span>
|
|
169
|
+
<span class="flex flex-col gap-1">
|
|
170
|
+
<span class="text-[15px] font-medium text-text">{label ?? t.current.dropFiles}</span>
|
|
171
|
+
<span class="text-sm text-text-secondary">
|
|
172
|
+
{t.current.or}
|
|
173
|
+
<span class="underline underline-offset-4">{t.current.browse}</span>
|
|
174
|
+
</span>
|
|
175
|
+
{#if defaultHint}
|
|
176
|
+
<span class="font-mono text-xs text-text-muted">{defaultHint}</span>
|
|
177
|
+
{/if}
|
|
178
|
+
</span>
|
|
179
|
+
</button>
|
|
180
|
+
|
|
181
|
+
<input
|
|
182
|
+
bind:this={input}
|
|
183
|
+
type="file"
|
|
184
|
+
{accept}
|
|
185
|
+
{multiple}
|
|
186
|
+
{disabled}
|
|
187
|
+
class="sr-only"
|
|
188
|
+
onchange={(event) => {
|
|
189
|
+
add(event.currentTarget.files);
|
|
190
|
+
event.currentTarget.value = '';
|
|
191
|
+
}}
|
|
192
|
+
/>
|
|
193
|
+
|
|
194
|
+
{#if rejected.length}
|
|
195
|
+
<ul class="flex flex-col gap-1">
|
|
196
|
+
{#each rejected as item (item.file.name + item.reason)}
|
|
197
|
+
<li class="font-sans text-sm text-danger">
|
|
198
|
+
{item.file.name} — {reasons[item.reason]}
|
|
199
|
+
</li>
|
|
200
|
+
{/each}
|
|
201
|
+
</ul>
|
|
202
|
+
{/if}
|
|
203
|
+
|
|
204
|
+
{#if showList && files.length}
|
|
205
|
+
<ul class="flex flex-col border border-hairline">
|
|
206
|
+
{#each files as file, i (file.name + file.size + i)}
|
|
207
|
+
<li
|
|
208
|
+
class="flex items-center gap-3 border-b border-hairline px-4 py-3 font-sans last:border-b-0"
|
|
209
|
+
>
|
|
210
|
+
<span class="min-w-0 flex-1 truncate text-sm text-text">{file.name}</span>
|
|
211
|
+
<span class="shrink-0 font-mono text-xs text-text-muted">{formatSize(file.size)}</span>
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
onclick={() => remove(i)}
|
|
215
|
+
aria-label="{t.current.remove} {file.name}"
|
|
216
|
+
class="-m-1 shrink-0 p-1 text-text-muted transition-colors duration-150 hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
|
|
217
|
+
>
|
|
218
|
+
<svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
|
|
219
|
+
<path d="m2 2 10 10M12 2 2 12" stroke="currentColor" stroke-width="1.5" />
|
|
220
|
+
</svg>
|
|
221
|
+
</button>
|
|
222
|
+
</li>
|
|
223
|
+
{/each}
|
|
224
|
+
</ul>
|
|
225
|
+
{/if}
|
|
226
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import { type Tone } from '../tones.js';
|
|
3
|
+
export interface RejectedFile {
|
|
4
|
+
file: File;
|
|
5
|
+
reason: 'type' | 'size' | 'count';
|
|
6
|
+
}
|
|
7
|
+
interface Props {
|
|
8
|
+
/** Bindable list of accepted files. */
|
|
9
|
+
files?: File[];
|
|
10
|
+
/** Same syntax as the input's `accept` — `'image/*,.pdf'`. */
|
|
11
|
+
accept?: string;
|
|
12
|
+
multiple?: boolean;
|
|
13
|
+
/** Per-file ceiling in bytes. `0` means no limit. */
|
|
14
|
+
maxSize?: number;
|
|
15
|
+
/** Cap on how many files may be held at once. `0` means no limit. */
|
|
16
|
+
maxFiles?: number;
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
tone?: Tone;
|
|
19
|
+
/** Headline inside the zone. Defaults to the locale's `dropFiles`. */
|
|
20
|
+
label?: string;
|
|
21
|
+
hint?: string;
|
|
22
|
+
/** Hide the built-in file list to render your own. */
|
|
23
|
+
showList?: boolean;
|
|
24
|
+
onaccept?: (files: File[]) => void;
|
|
25
|
+
onreject?: (rejected: RejectedFile[]) => void;
|
|
26
|
+
/** Replaces the glyph above the label. */
|
|
27
|
+
icon?: Snippet;
|
|
28
|
+
class?: string;
|
|
29
|
+
}
|
|
30
|
+
declare const Dropzone: import("svelte").Component<Props, {}, "files">;
|
|
31
|
+
type Dropzone = ReturnType<typeof Dropzone>;
|
|
32
|
+
export default Dropzone;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import { toneSoft, type Tone } from '../tones.js';
|
|
5
|
+
|
|
6
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
tone?: Tone;
|
|
10
|
+
/** Glyph inside the tinted circle. Defaults to a plain outline. */
|
|
11
|
+
icon?: Snippet;
|
|
12
|
+
/** Buttons or links under the copy. */
|
|
13
|
+
action?: Snippet;
|
|
14
|
+
/** Draws a dashed placeholder border around the block. */
|
|
15
|
+
bordered?: boolean;
|
|
16
|
+
children?: Snippet;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let {
|
|
20
|
+
title,
|
|
21
|
+
description,
|
|
22
|
+
tone = 'neutral',
|
|
23
|
+
icon,
|
|
24
|
+
action,
|
|
25
|
+
bordered = true,
|
|
26
|
+
class: className = '',
|
|
27
|
+
children,
|
|
28
|
+
...rest
|
|
29
|
+
}: Props = $props();
|
|
30
|
+
</script>
|
|
31
|
+
|
|
32
|
+
<div
|
|
33
|
+
class="flex flex-col items-center gap-4 px-6 py-12 text-center font-sans
|
|
34
|
+
{bordered ? 'border border-dashed border-hairline-strong' : ''} {className}"
|
|
35
|
+
{...rest}
|
|
36
|
+
>
|
|
37
|
+
<span class="flex size-12 items-center justify-center rounded-full {toneSoft[tone]}">
|
|
38
|
+
{#if icon}
|
|
39
|
+
{@render icon()}
|
|
40
|
+
{:else}
|
|
41
|
+
<svg class="size-5" viewBox="0 0 20 20" fill="none" aria-hidden="true">
|
|
42
|
+
<rect x="3" y="3" width="14" height="14" stroke="currentColor" stroke-width="1.5" />
|
|
43
|
+
<path d="M3 13.5 7 9.5l3 3 3-3 4 4" stroke="currentColor" stroke-width="1.5" />
|
|
44
|
+
</svg>
|
|
45
|
+
{/if}
|
|
46
|
+
</span>
|
|
47
|
+
<div class="flex flex-col gap-1.5">
|
|
48
|
+
<p class="font-heading text-base font-medium text-text">{title}</p>
|
|
49
|
+
{#if description}
|
|
50
|
+
<p class="max-w-sm text-sm leading-relaxed text-text-secondary">{description}</p>
|
|
51
|
+
{/if}
|
|
52
|
+
</div>
|
|
53
|
+
{#if children}{@render children()}{/if}
|
|
54
|
+
{#if action}
|
|
55
|
+
<div class="flex flex-wrap items-center justify-center gap-3 pt-1">{@render action()}</div>
|
|
56
|
+
{/if}
|
|
57
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { type Tone } from '../tones.js';
|
|
4
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
5
|
+
title: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
tone?: Tone;
|
|
8
|
+
/** Glyph inside the tinted circle. Defaults to a plain outline. */
|
|
9
|
+
icon?: Snippet;
|
|
10
|
+
/** Buttons or links under the copy. */
|
|
11
|
+
action?: Snippet;
|
|
12
|
+
/** Draws a dashed placeholder border around the block. */
|
|
13
|
+
bordered?: boolean;
|
|
14
|
+
children?: Snippet;
|
|
15
|
+
}
|
|
16
|
+
declare const EmptyState: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type EmptyState = ReturnType<typeof EmptyState>;
|
|
18
|
+
export default EmptyState;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
label?: string;
|
|
7
|
+
/** Helper text under the control. Hidden while `error` is set. */
|
|
8
|
+
hint?: string;
|
|
9
|
+
/** Replaces the hint and turns it red. */
|
|
10
|
+
error?: string;
|
|
11
|
+
required?: boolean;
|
|
12
|
+
/** `id` of the control — set the same value on the input inside. */
|
|
13
|
+
for?: string;
|
|
14
|
+
children: Snippet;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let {
|
|
18
|
+
label,
|
|
19
|
+
hint,
|
|
20
|
+
error,
|
|
21
|
+
required = false,
|
|
22
|
+
for: htmlFor,
|
|
23
|
+
class: className = '',
|
|
24
|
+
children,
|
|
25
|
+
...rest
|
|
26
|
+
}: Props = $props();
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div class="flex w-full flex-col gap-2 {className}" {...rest}>
|
|
30
|
+
{#if label}
|
|
31
|
+
<label for={htmlFor} class="font-sans text-sm font-medium text-text">
|
|
32
|
+
{label}
|
|
33
|
+
{#if required}<span class="text-danger" aria-hidden="true">*</span>{/if}
|
|
34
|
+
</label>
|
|
35
|
+
{/if}
|
|
36
|
+
{@render children()}
|
|
37
|
+
{#if error}
|
|
38
|
+
<p class="font-sans text-sm text-danger">{error}</p>
|
|
39
|
+
{:else if hint}
|
|
40
|
+
<p class="font-sans text-sm text-text-muted">{hint}</p>
|
|
41
|
+
{/if}
|
|
42
|
+
</div>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
label?: string;
|
|
5
|
+
/** Helper text under the control. Hidden while `error` is set. */
|
|
6
|
+
hint?: string;
|
|
7
|
+
/** Replaces the hint and turns it red. */
|
|
8
|
+
error?: string;
|
|
9
|
+
required?: boolean;
|
|
10
|
+
/** `id` of the control — set the same value on the input inside. */
|
|
11
|
+
for?: string;
|
|
12
|
+
children: Snippet;
|
|
13
|
+
}
|
|
14
|
+
declare const Field: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type Field = ReturnType<typeof Field>;
|
|
16
|
+
export default Field;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
<script module lang="ts">
|
|
2
|
+
export interface FooterLink {
|
|
3
|
+
label: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
external?: boolean;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface FooterColumn {
|
|
9
|
+
title: string;
|
|
10
|
+
links: FooterLink[];
|
|
11
|
+
}
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<script lang="ts">
|
|
15
|
+
import type { Snippet } from 'svelte';
|
|
16
|
+
import { toneHoverText, type Tone } from '../tones.js';
|
|
17
|
+
|
|
18
|
+
interface Props {
|
|
19
|
+
columns?: FooterColumn[];
|
|
20
|
+
/** Logo, wordmark and a line of copy on the left. */
|
|
21
|
+
brand?: Snippet;
|
|
22
|
+
/** Right side of the bottom bar — social links, a locale picker. */
|
|
23
|
+
bottom?: Snippet;
|
|
24
|
+
/** Left side of the bottom bar. Ignored when `bottom` covers both. */
|
|
25
|
+
copyright?: string;
|
|
26
|
+
tone?: Tone;
|
|
27
|
+
class?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
columns = [],
|
|
32
|
+
brand,
|
|
33
|
+
bottom,
|
|
34
|
+
copyright,
|
|
35
|
+
tone = 'brand',
|
|
36
|
+
class: className = ''
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<footer class="w-full border-t border-hairline bg-bg {className}">
|
|
41
|
+
<div class="mx-auto flex w-full max-w-6xl flex-col gap-12 px-6 py-14">
|
|
42
|
+
{#if brand || columns.length}
|
|
43
|
+
<div class="flex flex-col gap-10 md:flex-row md:justify-between">
|
|
44
|
+
{#if brand}
|
|
45
|
+
<div class="flex max-w-xs flex-col gap-3">{@render brand()}</div>
|
|
46
|
+
{/if}
|
|
47
|
+
|
|
48
|
+
{#if columns.length}
|
|
49
|
+
<div class="grid grid-cols-2 gap-10 sm:grid-cols-3 md:gap-16">
|
|
50
|
+
{#each columns as column (column.title)}
|
|
51
|
+
<div class="flex flex-col gap-3">
|
|
52
|
+
<p class="font-mono text-[10px] tracking-wide text-text-muted uppercase">
|
|
53
|
+
{column.title}
|
|
54
|
+
</p>
|
|
55
|
+
<ul class="flex flex-col gap-2">
|
|
56
|
+
{#each column.links as item (item.label)}
|
|
57
|
+
<li>
|
|
58
|
+
<a
|
|
59
|
+
href={item.href}
|
|
60
|
+
target={item.external ? '_blank' : undefined}
|
|
61
|
+
rel={item.external ? 'noreferrer' : undefined}
|
|
62
|
+
class="font-sans text-sm text-text-secondary transition-colors duration-150 ease-brand-out {toneHoverText[
|
|
63
|
+
tone
|
|
64
|
+
]}"
|
|
65
|
+
>
|
|
66
|
+
{item.label}
|
|
67
|
+
{#if item.external}<span aria-hidden="true" class="text-[0.85em]">↗</span
|
|
68
|
+
>{/if}
|
|
69
|
+
</a>
|
|
70
|
+
</li>
|
|
71
|
+
{/each}
|
|
72
|
+
</ul>
|
|
73
|
+
</div>
|
|
74
|
+
{/each}
|
|
75
|
+
</div>
|
|
76
|
+
{/if}
|
|
77
|
+
</div>
|
|
78
|
+
{/if}
|
|
79
|
+
|
|
80
|
+
{#if copyright || bottom}
|
|
81
|
+
<div
|
|
82
|
+
class="flex flex-col gap-4 border-t border-hairline pt-6 sm:flex-row sm:items-center sm:justify-between"
|
|
83
|
+
>
|
|
84
|
+
{#if copyright}
|
|
85
|
+
<p class="font-mono text-xs text-text-muted">{copyright}</p>
|
|
86
|
+
{/if}
|
|
87
|
+
{#if bottom}
|
|
88
|
+
<div class="flex items-center gap-4">{@render bottom()}</div>
|
|
89
|
+
{/if}
|
|
90
|
+
</div>
|
|
91
|
+
{/if}
|
|
92
|
+
</div>
|
|
93
|
+
</footer>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface FooterLink {
|
|
2
|
+
label: string;
|
|
3
|
+
href?: string;
|
|
4
|
+
external?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface FooterColumn {
|
|
7
|
+
title: string;
|
|
8
|
+
links: FooterLink[];
|
|
9
|
+
}
|
|
10
|
+
import type { Snippet } from 'svelte';
|
|
11
|
+
import { type Tone } from '../tones.js';
|
|
12
|
+
interface Props {
|
|
13
|
+
columns?: FooterColumn[];
|
|
14
|
+
/** Logo, wordmark and a line of copy on the left. */
|
|
15
|
+
brand?: Snippet;
|
|
16
|
+
/** Right side of the bottom bar — social links, a locale picker. */
|
|
17
|
+
bottom?: Snippet;
|
|
18
|
+
/** Left side of the bottom bar. Ignored when `bottom` covers both. */
|
|
19
|
+
copyright?: string;
|
|
20
|
+
tone?: Tone;
|
|
21
|
+
class?: string;
|
|
22
|
+
}
|
|
23
|
+
declare const Footer: import("svelte").Component<Props, {}, "">;
|
|
24
|
+
type Footer = ReturnType<typeof Footer>;
|
|
25
|
+
export default Footer;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
4
|
+
import { toneFocusWithinBorder, type Tone } from '../tones.js';
|
|
5
|
+
|
|
6
|
+
export type InputSize = 'sm' | 'md' | 'lg';
|
|
7
|
+
|
|
8
|
+
interface Props extends Omit<HTMLInputAttributes, 'size' | 'prefix' | 'suffix'> {
|
|
9
|
+
value?: string;
|
|
10
|
+
size?: InputSize;
|
|
11
|
+
tone?: Tone;
|
|
12
|
+
/** Paints the field red and sets `aria-invalid`. */
|
|
13
|
+
invalid?: boolean;
|
|
14
|
+
/** Leading adornment — an icon, a currency symbol, `https://` … */
|
|
15
|
+
prefix?: Snippet;
|
|
16
|
+
/** Trailing adornment. */
|
|
17
|
+
suffix?: Snippet;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let {
|
|
21
|
+
value = $bindable(''),
|
|
22
|
+
size = 'md',
|
|
23
|
+
tone = 'brand',
|
|
24
|
+
invalid = false,
|
|
25
|
+
disabled = false,
|
|
26
|
+
prefix,
|
|
27
|
+
suffix,
|
|
28
|
+
class: className = '',
|
|
29
|
+
...rest
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
const sizes: Record<InputSize, string> = {
|
|
33
|
+
sm: 'h-9 text-[13px]',
|
|
34
|
+
md: 'h-11 text-[15px]',
|
|
35
|
+
lg: 'h-12 text-base'
|
|
36
|
+
};
|
|
37
|
+
</script>
|
|
38
|
+
|
|
39
|
+
<span
|
|
40
|
+
class="inline-flex w-full items-center gap-2 border bg-bg px-4 transition-colors duration-150 ease-brand-out
|
|
41
|
+
{sizes[size]}
|
|
42
|
+
{invalid
|
|
43
|
+
? 'border-danger focus-within:border-danger'
|
|
44
|
+
: `border-hairline ${toneFocusWithinBorder[tone]}`}
|
|
45
|
+
{disabled ? 'pointer-events-none opacity-50' : ''} {className}"
|
|
46
|
+
>
|
|
47
|
+
{#if prefix}
|
|
48
|
+
<span class="shrink-0 text-text-muted">{@render prefix()}</span>
|
|
49
|
+
{/if}
|
|
50
|
+
<input
|
|
51
|
+
bind:value
|
|
52
|
+
{disabled}
|
|
53
|
+
aria-invalid={invalid ? 'true' : undefined}
|
|
54
|
+
class="w-full min-w-0 bg-transparent font-sans text-text placeholder:text-text-muted focus:outline-none"
|
|
55
|
+
{...rest}
|
|
56
|
+
/>
|
|
57
|
+
{#if suffix}
|
|
58
|
+
<span class="shrink-0 text-text-muted">{@render suffix()}</span>
|
|
59
|
+
{/if}
|
|
60
|
+
</span>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLInputAttributes } from 'svelte/elements';
|
|
3
|
+
import { type Tone } from '../tones.js';
|
|
4
|
+
export type InputSize = 'sm' | 'md' | 'lg';
|
|
5
|
+
interface Props extends Omit<HTMLInputAttributes, 'size' | 'prefix' | 'suffix'> {
|
|
6
|
+
value?: string;
|
|
7
|
+
size?: InputSize;
|
|
8
|
+
tone?: Tone;
|
|
9
|
+
/** Paints the field red and sets `aria-invalid`. */
|
|
10
|
+
invalid?: boolean;
|
|
11
|
+
/** Leading adornment — an icon, a currency symbol, `https://` … */
|
|
12
|
+
prefix?: Snippet;
|
|
13
|
+
/** Trailing adornment. */
|
|
14
|
+
suffix?: Snippet;
|
|
15
|
+
}
|
|
16
|
+
declare const Input: import("svelte").Component<Props, {}, "value">;
|
|
17
|
+
type Input = ReturnType<typeof Input>;
|
|
18
|
+
export default Input;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
6
|
+
/** Children sit flush against each other with shared hairlines —
|
|
7
|
+
* an Input plus a Button, a Select plus an Input, and so on. */
|
|
8
|
+
children: Snippet;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let { class: className = '', children, ...rest }: Props = $props();
|
|
12
|
+
</script>
|
|
13
|
+
|
|
14
|
+
<div
|
|
15
|
+
class="flex w-full items-stretch [&>*]:min-w-0 [&>*]:rounded-none [&>*:focus-within]:relative [&>*:focus-within]:z-10 [&>*:not(:first-child)]:-ml-px {className}"
|
|
16
|
+
{...rest}
|
|
17
|
+
>
|
|
18
|
+
{@render children()}
|
|
19
|
+
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends HTMLAttributes<HTMLDivElement> {
|
|
4
|
+
/** Children sit flush against each other with shared hairlines —
|
|
5
|
+
* an Input plus a Button, a Select plus an Input, and so on. */
|
|
6
|
+
children: Snippet;
|
|
7
|
+
}
|
|
8
|
+
declare const InputGroup: import("svelte").Component<Props, {}, "">;
|
|
9
|
+
type InputGroup = ReturnType<typeof InputGroup>;
|
|
10
|
+
export default InputGroup;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLAttributes<HTMLElement> {
|
|
6
|
+
children: Snippet;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
let { class: className = '', children, ...rest }: Props = $props();
|
|
10
|
+
</script>
|
|
11
|
+
|
|
12
|
+
<kbd
|
|
13
|
+
class="inline-flex h-6 min-w-6 items-center justify-center border border-b-2 border-hairline-strong bg-bg-alt px-1.5 font-mono text-[11px] text-text-secondary {className}"
|
|
14
|
+
{...rest}
|
|
15
|
+
>
|
|
16
|
+
{@render children()}
|
|
17
|
+
</kbd>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends HTMLAttributes<HTMLElement> {
|
|
4
|
+
children: Snippet;
|
|
5
|
+
}
|
|
6
|
+
declare const Kbd: import("svelte").Component<Props, {}, "">;
|
|
7
|
+
type Kbd = ReturnType<typeof Kbd>;
|
|
8
|
+
export default Kbd;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLLabelAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
interface Props extends HTMLLabelAttributes {
|
|
6
|
+
required?: boolean;
|
|
7
|
+
children: Snippet;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
let { required = false, class: className = '', children, ...rest }: Props = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<label class="font-sans text-sm font-medium text-text {className}" {...rest}>
|
|
14
|
+
{@render children()}
|
|
15
|
+
{#if required}<span class="text-danger" aria-hidden="true">*</span>{/if}
|
|
16
|
+
</label>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLLabelAttributes } from 'svelte/elements';
|
|
3
|
+
interface Props extends HTMLLabelAttributes {
|
|
4
|
+
required?: boolean;
|
|
5
|
+
children: Snippet;
|
|
6
|
+
}
|
|
7
|
+
declare const Label: import("svelte").Component<Props, {}, "">;
|
|
8
|
+
type Label = ReturnType<typeof Label>;
|
|
9
|
+
export default Label;
|