@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.
Files changed (135) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +507 -0
  3. package/cli/index.mjs +410 -0
  4. package/dist/actions/anchor.d.ts +29 -0
  5. package/dist/actions/anchor.js +122 -0
  6. package/dist/actions/dismissable.d.ts +44 -0
  7. package/dist/actions/dismissable.js +147 -0
  8. package/dist/components/Accordion.svelte +14 -0
  9. package/dist/components/Accordion.svelte.d.ts +8 -0
  10. package/dist/components/AccordionItem.svelte +46 -0
  11. package/dist/components/AccordionItem.svelte.d.ts +14 -0
  12. package/dist/components/Alert.svelte +88 -0
  13. package/dist/components/Alert.svelte.d.ts +18 -0
  14. package/dist/components/Avatar.svelte +58 -0
  15. package/dist/components/Avatar.svelte.d.ts +16 -0
  16. package/dist/components/AvatarGroup.svelte +17 -0
  17. package/dist/components/AvatarGroup.svelte.d.ts +8 -0
  18. package/dist/components/Badge.svelte +53 -0
  19. package/dist/components/Badge.svelte.d.ts +18 -0
  20. package/dist/components/Breadcrumb.svelte +46 -0
  21. package/dist/components/Breadcrumb.svelte.d.ts +14 -0
  22. package/dist/components/Button.svelte +94 -0
  23. package/dist/components/Button.svelte.d.ts +21 -0
  24. package/dist/components/Calendar.svelte +206 -0
  25. package/dist/components/Calendar.svelte.d.ts +22 -0
  26. package/dist/components/Card.svelte +76 -0
  27. package/dist/components/Card.svelte.d.ts +21 -0
  28. package/dist/components/Checkbox.svelte +69 -0
  29. package/dist/components/Checkbox.svelte.d.ts +15 -0
  30. package/dist/components/CheckboxGroup.svelte +109 -0
  31. package/dist/components/CheckboxGroup.svelte.d.ts +30 -0
  32. package/dist/components/Combobox.svelte +268 -0
  33. package/dist/components/Combobox.svelte.d.ts +29 -0
  34. package/dist/components/CommandPalette.svelte +226 -0
  35. package/dist/components/CommandPalette.svelte.d.ts +32 -0
  36. package/dist/components/ConfirmDialog.svelte +97 -0
  37. package/dist/components/ConfirmDialog.svelte.d.ts +22 -0
  38. package/dist/components/ContextMenu.svelte +90 -0
  39. package/dist/components/ContextMenu.svelte.d.ts +15 -0
  40. package/dist/components/DatePicker.svelte +186 -0
  41. package/dist/components/DatePicker.svelte.d.ts +23 -0
  42. package/dist/components/Divider.svelte +30 -0
  43. package/dist/components/Divider.svelte.d.ts +10 -0
  44. package/dist/components/Drawer.svelte +161 -0
  45. package/dist/components/Drawer.svelte.d.ts +20 -0
  46. package/dist/components/Dropdown.svelte +112 -0
  47. package/dist/components/Dropdown.svelte.d.ts +23 -0
  48. package/dist/components/Dropzone.svelte +226 -0
  49. package/dist/components/Dropzone.svelte.d.ts +32 -0
  50. package/dist/components/EmptyState.svelte +57 -0
  51. package/dist/components/EmptyState.svelte.d.ts +18 -0
  52. package/dist/components/Field.svelte +42 -0
  53. package/dist/components/Field.svelte.d.ts +16 -0
  54. package/dist/components/Footer.svelte +93 -0
  55. package/dist/components/Footer.svelte.d.ts +25 -0
  56. package/dist/components/Input.svelte +60 -0
  57. package/dist/components/Input.svelte.d.ts +18 -0
  58. package/dist/components/InputGroup.svelte +19 -0
  59. package/dist/components/InputGroup.svelte.d.ts +10 -0
  60. package/dist/components/Kbd.svelte +17 -0
  61. package/dist/components/Kbd.svelte.d.ts +8 -0
  62. package/dist/components/Label.svelte +16 -0
  63. package/dist/components/Label.svelte.d.ts +9 -0
  64. package/dist/components/Link.svelte +44 -0
  65. package/dist/components/Link.svelte.d.ts +16 -0
  66. package/dist/components/LocaleProvider.svelte +24 -0
  67. package/dist/components/LocaleProvider.svelte.d.ts +10 -0
  68. package/dist/components/MenuItem.svelte +58 -0
  69. package/dist/components/MenuItem.svelte.d.ts +18 -0
  70. package/dist/components/MenuSeparator.svelte +22 -0
  71. package/dist/components/MenuSeparator.svelte.d.ts +8 -0
  72. package/dist/components/Modal.svelte +114 -0
  73. package/dist/components/Modal.svelte.d.ts +21 -0
  74. package/dist/components/MultiSelect.svelte +292 -0
  75. package/dist/components/MultiSelect.svelte.d.ts +31 -0
  76. package/dist/components/Navbar.svelte +190 -0
  77. package/dist/components/Navbar.svelte.d.ts +33 -0
  78. package/dist/components/NumberInput.svelte +100 -0
  79. package/dist/components/NumberInput.svelte.d.ts +15 -0
  80. package/dist/components/Pagination.svelte +90 -0
  81. package/dist/components/Pagination.svelte.d.ts +15 -0
  82. package/dist/components/PasswordInput.svelte +104 -0
  83. package/dist/components/PasswordInput.svelte.d.ts +12 -0
  84. package/dist/components/Popover.svelte +114 -0
  85. package/dist/components/Popover.svelte.d.ts +19 -0
  86. package/dist/components/Progress.svelte +56 -0
  87. package/dist/components/Progress.svelte.d.ts +17 -0
  88. package/dist/components/Radio.svelte +55 -0
  89. package/dist/components/Radio.svelte.d.ts +15 -0
  90. package/dist/components/RadioGroup.svelte +93 -0
  91. package/dist/components/RadioGroup.svelte.d.ts +27 -0
  92. package/dist/components/SegmentedControl.svelte +88 -0
  93. package/dist/components/SegmentedControl.svelte.d.ts +25 -0
  94. package/dist/components/Select.svelte +61 -0
  95. package/dist/components/Select.svelte.d.ts +20 -0
  96. package/dist/components/Sidebar.svelte +206 -0
  97. package/dist/components/Sidebar.svelte.d.ts +36 -0
  98. package/dist/components/Skeleton.svelte +46 -0
  99. package/dist/components/Skeleton.svelte.d.ts +13 -0
  100. package/dist/components/Slider.svelte +79 -0
  101. package/dist/components/Slider.svelte.d.ts +19 -0
  102. package/dist/components/Spinner.svelte +34 -0
  103. package/dist/components/Spinner.svelte.d.ts +13 -0
  104. package/dist/components/Stat.svelte +49 -0
  105. package/dist/components/Stat.svelte.d.ts +16 -0
  106. package/dist/components/Steps.svelte +136 -0
  107. package/dist/components/Steps.svelte.d.ts +24 -0
  108. package/dist/components/Switch.svelte +64 -0
  109. package/dist/components/Switch.svelte.d.ts +14 -0
  110. package/dist/components/Table.svelte +236 -0
  111. package/dist/components/Table.svelte.d.ts +70 -0
  112. package/dist/components/Tabs.svelte +91 -0
  113. package/dist/components/Tabs.svelte.d.ts +23 -0
  114. package/dist/components/Textarea.svelte +54 -0
  115. package/dist/components/Textarea.svelte.d.ts +12 -0
  116. package/dist/components/ThemeToggle.svelte +133 -0
  117. package/dist/components/ThemeToggle.svelte.d.ts +20 -0
  118. package/dist/components/Toaster.svelte +110 -0
  119. package/dist/components/Toaster.svelte.d.ts +8 -0
  120. package/dist/components/Tooltip.svelte +33 -0
  121. package/dist/components/Tooltip.svelte.d.ts +12 -0
  122. package/dist/date.d.ts +35 -0
  123. package/dist/date.js +108 -0
  124. package/dist/fonts.css +193 -0
  125. package/dist/index.d.ts +105 -0
  126. package/dist/index.js +77 -0
  127. package/dist/locale.svelte.d.ts +93 -0
  128. package/dist/locale.svelte.js +153 -0
  129. package/dist/theme.css +210 -0
  130. package/dist/toast.svelte.d.ts +47 -0
  131. package/dist/toast.svelte.js +60 -0
  132. package/dist/tones.d.ts +48 -0
  133. package/dist/tones.js +209 -0
  134. package/package.json +349 -0
  135. 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;