@nqmcreative/ui 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +7 -5
  2. package/dist/core/index.d.ts +1 -1
  3. package/dist/core/index.js +1 -1
  4. package/dist/core/trigger.d.ts +6 -0
  5. package/dist/core/trigger.js +13 -0
  6. package/dist/styles/matte/Button.svelte +4 -4
  7. package/dist/styles/matte/Card.svelte +3 -3
  8. package/dist/styles/matte/Combobox.svelte +2 -2
  9. package/dist/styles/matte/CommandPalette.svelte +32 -7
  10. package/dist/styles/matte/CommandPalette.svelte.d.ts +5 -0
  11. package/dist/styles/matte/DatePicker.svelte +2 -2
  12. package/dist/styles/matte/Drawer.svelte +2 -2
  13. package/dist/styles/matte/Dropzone.svelte +1 -1
  14. package/dist/styles/matte/FeatureCard.svelte +1 -1
  15. package/dist/styles/matte/Input.svelte +4 -4
  16. package/dist/styles/matte/MenuItem.svelte +1 -1
  17. package/dist/styles/matte/Modal.svelte +3 -3
  18. package/dist/styles/matte/NumberInput.svelte +2 -2
  19. package/dist/styles/matte/PasswordInput.svelte +2 -2
  20. package/dist/styles/matte/PricingCard.svelte +3 -3
  21. package/dist/styles/matte/SegmentedControl.svelte +1 -1
  22. package/dist/styles/matte/Select.svelte +1 -1
  23. package/dist/styles/matte/Sidebar.svelte +75 -26
  24. package/dist/styles/matte/Sidebar.svelte.d.ts +15 -1
  25. package/dist/styles/matte/StatsBand.svelte +1 -1
  26. package/dist/styles/matte/Table.svelte +3 -3
  27. package/dist/styles/matte/Testimonial.svelte +2 -2
  28. package/dist/styles/matte/Textarea.svelte +1 -1
  29. package/dist/styles/matte/index.d.ts +1 -1
  30. package/dist/styles/paper/CommandPalette.svelte +25 -2
  31. package/dist/styles/paper/CommandPalette.svelte.d.ts +5 -0
  32. package/dist/styles/paper/Modal.svelte +9 -1
  33. package/dist/styles/paper/Sidebar.svelte +75 -26
  34. package/dist/styles/paper/Sidebar.svelte.d.ts +15 -1
  35. package/dist/styles/paper/fonts.css +34 -7
  36. package/dist/styles/paper/index.d.ts +1 -1
  37. package/dist/styles/paper/theme.css +11 -8
  38. package/dist/styles/sprout/Accordion.svelte +17 -0
  39. package/dist/styles/sprout/Accordion.svelte.d.ts +8 -0
  40. package/dist/styles/sprout/AccordionItem.svelte +46 -0
  41. package/dist/styles/sprout/AccordionItem.svelte.d.ts +14 -0
  42. package/dist/styles/sprout/Alert.svelte +100 -0
  43. package/dist/styles/sprout/Alert.svelte.d.ts +18 -0
  44. package/dist/styles/sprout/Avatar.svelte +60 -0
  45. package/dist/styles/sprout/Avatar.svelte.d.ts +16 -0
  46. package/dist/styles/sprout/AvatarGroup.svelte +17 -0
  47. package/dist/styles/sprout/AvatarGroup.svelte.d.ts +8 -0
  48. package/dist/styles/sprout/Badge.svelte +58 -0
  49. package/dist/styles/sprout/Badge.svelte.d.ts +16 -0
  50. package/dist/styles/sprout/Breadcrumb.svelte +49 -0
  51. package/dist/styles/sprout/Breadcrumb.svelte.d.ts +14 -0
  52. package/dist/styles/sprout/Button.svelte +105 -0
  53. package/dist/styles/sprout/Button.svelte.d.ts +21 -0
  54. package/dist/styles/sprout/CTASection.svelte +77 -0
  55. package/dist/styles/sprout/CTASection.svelte.d.ts +18 -0
  56. package/dist/styles/sprout/Calendar.svelte +202 -0
  57. package/dist/styles/sprout/Calendar.svelte.d.ts +22 -0
  58. package/dist/styles/sprout/Card.svelte +82 -0
  59. package/dist/styles/sprout/Card.svelte.d.ts +21 -0
  60. package/dist/styles/sprout/Checkbox.svelte +76 -0
  61. package/dist/styles/sprout/Checkbox.svelte.d.ts +15 -0
  62. package/dist/styles/sprout/CheckboxGroup.svelte +112 -0
  63. package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +30 -0
  64. package/dist/styles/sprout/Combobox.svelte +267 -0
  65. package/dist/styles/sprout/Combobox.svelte.d.ts +29 -0
  66. package/dist/styles/sprout/CommandPalette.svelte +233 -0
  67. package/dist/styles/sprout/CommandPalette.svelte.d.ts +37 -0
  68. package/dist/styles/sprout/ConfirmDialog.svelte +97 -0
  69. package/dist/styles/sprout/ConfirmDialog.svelte.d.ts +22 -0
  70. package/dist/styles/sprout/ContextMenu.svelte +89 -0
  71. package/dist/styles/sprout/ContextMenu.svelte.d.ts +15 -0
  72. package/dist/styles/sprout/DatePicker.svelte +193 -0
  73. package/dist/styles/sprout/DatePicker.svelte.d.ts +23 -0
  74. package/dist/styles/sprout/Divider.svelte +30 -0
  75. package/dist/styles/sprout/Divider.svelte.d.ts +10 -0
  76. package/dist/styles/sprout/Drawer.svelte +166 -0
  77. package/dist/styles/sprout/Drawer.svelte.d.ts +20 -0
  78. package/dist/styles/sprout/Dropdown.svelte +95 -0
  79. package/dist/styles/sprout/Dropdown.svelte.d.ts +23 -0
  80. package/dist/styles/sprout/Dropzone.svelte +211 -0
  81. package/dist/styles/sprout/Dropzone.svelte.d.ts +29 -0
  82. package/dist/styles/sprout/EmptyState.svelte +71 -0
  83. package/dist/styles/sprout/EmptyState.svelte.d.ts +18 -0
  84. package/dist/styles/sprout/FeatureCard.svelte +61 -0
  85. package/dist/styles/sprout/FeatureCard.svelte.d.ts +16 -0
  86. package/dist/styles/sprout/FeatureGrid.svelte +62 -0
  87. package/dist/styles/sprout/FeatureGrid.svelte.d.ts +16 -0
  88. package/dist/styles/sprout/Field.svelte +42 -0
  89. package/dist/styles/sprout/Field.svelte.d.ts +16 -0
  90. package/dist/styles/sprout/Footer.svelte +91 -0
  91. package/dist/styles/sprout/Footer.svelte.d.ts +25 -0
  92. package/dist/styles/sprout/HeroSection.svelte +86 -0
  93. package/dist/styles/sprout/HeroSection.svelte.d.ts +21 -0
  94. package/dist/styles/sprout/Input.svelte +61 -0
  95. package/dist/styles/sprout/Input.svelte.d.ts +18 -0
  96. package/dist/styles/sprout/InputGroup.svelte +20 -0
  97. package/dist/styles/sprout/InputGroup.svelte.d.ts +11 -0
  98. package/dist/styles/sprout/Kbd.svelte +18 -0
  99. package/dist/styles/sprout/Kbd.svelte.d.ts +8 -0
  100. package/dist/styles/sprout/Label.svelte +16 -0
  101. package/dist/styles/sprout/Label.svelte.d.ts +9 -0
  102. package/dist/styles/sprout/Link.svelte +44 -0
  103. package/dist/styles/sprout/Link.svelte.d.ts +16 -0
  104. package/dist/styles/sprout/LocaleProvider.svelte +24 -0
  105. package/dist/styles/sprout/LocaleProvider.svelte.d.ts +10 -0
  106. package/dist/styles/sprout/LogoCloud.svelte +61 -0
  107. package/dist/styles/sprout/LogoCloud.svelte.d.ts +19 -0
  108. package/dist/styles/sprout/MenuItem.svelte +64 -0
  109. package/dist/styles/sprout/MenuItem.svelte.d.ts +18 -0
  110. package/dist/styles/sprout/MenuSeparator.svelte +22 -0
  111. package/dist/styles/sprout/MenuSeparator.svelte.d.ts +8 -0
  112. package/dist/styles/sprout/Modal.svelte +127 -0
  113. package/dist/styles/sprout/Modal.svelte.d.ts +21 -0
  114. package/dist/styles/sprout/MultiSelect.svelte +288 -0
  115. package/dist/styles/sprout/MultiSelect.svelte.d.ts +31 -0
  116. package/dist/styles/sprout/Navbar.svelte +201 -0
  117. package/dist/styles/sprout/Navbar.svelte.d.ts +33 -0
  118. package/dist/styles/sprout/NumberInput.svelte +97 -0
  119. package/dist/styles/sprout/NumberInput.svelte.d.ts +15 -0
  120. package/dist/styles/sprout/Pagination.svelte +84 -0
  121. package/dist/styles/sprout/Pagination.svelte.d.ts +15 -0
  122. package/dist/styles/sprout/PasswordInput.svelte +101 -0
  123. package/dist/styles/sprout/PasswordInput.svelte.d.ts +12 -0
  124. package/dist/styles/sprout/Popover.svelte +90 -0
  125. package/dist/styles/sprout/Popover.svelte.d.ts +19 -0
  126. package/dist/styles/sprout/PricingCard.svelte +109 -0
  127. package/dist/styles/sprout/PricingCard.svelte.d.ts +25 -0
  128. package/dist/styles/sprout/Progress.svelte +59 -0
  129. package/dist/styles/sprout/Progress.svelte.d.ts +17 -0
  130. package/dist/styles/sprout/Radio.svelte +56 -0
  131. package/dist/styles/sprout/Radio.svelte.d.ts +15 -0
  132. package/dist/styles/sprout/RadioGroup.svelte +96 -0
  133. package/dist/styles/sprout/RadioGroup.svelte.d.ts +27 -0
  134. package/dist/styles/sprout/SegmentedControl.svelte +89 -0
  135. package/dist/styles/sprout/SegmentedControl.svelte.d.ts +25 -0
  136. package/dist/styles/sprout/Select.svelte +62 -0
  137. package/dist/styles/sprout/Select.svelte.d.ts +20 -0
  138. package/dist/styles/sprout/Sidebar.svelte +255 -0
  139. package/dist/styles/sprout/Sidebar.svelte.d.ts +50 -0
  140. package/dist/styles/sprout/Skeleton.svelte +46 -0
  141. package/dist/styles/sprout/Skeleton.svelte.d.ts +13 -0
  142. package/dist/styles/sprout/Slider.svelte +80 -0
  143. package/dist/styles/sprout/Slider.svelte.d.ts +19 -0
  144. package/dist/styles/sprout/Spinner.svelte +34 -0
  145. package/dist/styles/sprout/Spinner.svelte.d.ts +13 -0
  146. package/dist/styles/sprout/Stat.svelte +51 -0
  147. package/dist/styles/sprout/Stat.svelte.d.ts +16 -0
  148. package/dist/styles/sprout/StatsBand.svelte +64 -0
  149. package/dist/styles/sprout/StatsBand.svelte.d.ts +20 -0
  150. package/dist/styles/sprout/Steps.svelte +143 -0
  151. package/dist/styles/sprout/Steps.svelte.d.ts +24 -0
  152. package/dist/styles/sprout/Switch.svelte +65 -0
  153. package/dist/styles/sprout/Switch.svelte.d.ts +14 -0
  154. package/dist/styles/sprout/Table.svelte +218 -0
  155. package/dist/styles/sprout/Table.svelte.d.ts +66 -0
  156. package/dist/styles/sprout/Tabs.svelte +103 -0
  157. package/dist/styles/sprout/Tabs.svelte.d.ts +23 -0
  158. package/dist/styles/sprout/Testimonial.svelte +65 -0
  159. package/dist/styles/sprout/Testimonial.svelte.d.ts +19 -0
  160. package/dist/styles/sprout/Textarea.svelte +55 -0
  161. package/dist/styles/sprout/Textarea.svelte.d.ts +12 -0
  162. package/dist/styles/sprout/ThemeToggle.svelte +131 -0
  163. package/dist/styles/sprout/ThemeToggle.svelte.d.ts +16 -0
  164. package/dist/styles/sprout/Toaster.svelte +133 -0
  165. package/dist/styles/sprout/Toaster.svelte.d.ts +8 -0
  166. package/dist/styles/sprout/Tooltip.svelte +34 -0
  167. package/dist/styles/sprout/Tooltip.svelte.d.ts +12 -0
  168. package/dist/styles/sprout/fonts.css +77 -0
  169. package/dist/styles/sprout/index.d.ts +109 -0
  170. package/dist/styles/sprout/index.js +83 -0
  171. package/dist/styles/sprout/lift.d.ts +40 -0
  172. package/dist/styles/sprout/lift.js +36 -0
  173. package/dist/styles/sprout/theme.css +255 -0
  174. package/package.json +269 -3
  175. package/registry.json +139 -66
@@ -0,0 +1,211 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { toneBorder, toneSoft, toneSurface, toneText, type Tone } from '../../core/tones.js';
4
+ import { useLocale } from '../../core/locale.svelte.js';
5
+ import { formatSize as toReadableSize, sortFiles, type RejectedFile } from '../../core/files.js';
6
+ import { edge } from './lift.js';
7
+
8
+ interface Props {
9
+ /** Bindable list of accepted files. */
10
+ files?: File[];
11
+ /** Same syntax as the input's `accept` — `'image/*,.pdf'`. */
12
+ accept?: string;
13
+ multiple?: boolean;
14
+ /** Per-file ceiling in bytes. `0` means no limit. */
15
+ maxSize?: number;
16
+ /** Cap on how many files may be held at once. `0` means no limit. */
17
+ maxFiles?: number;
18
+ disabled?: boolean;
19
+ tone?: Tone;
20
+ /** Headline inside the zone. Defaults to the locale's `dropFiles`. */
21
+ label?: string;
22
+ hint?: string;
23
+ /** Hide the built-in file list to render your own. */
24
+ showList?: boolean;
25
+ onaccept?: (files: File[]) => void;
26
+ onreject?: (rejected: RejectedFile[]) => void;
27
+ /** Replaces the glyph above the label. */
28
+ icon?: Snippet;
29
+ class?: string;
30
+ }
31
+
32
+ let {
33
+ files = $bindable([]),
34
+ accept,
35
+ multiple = true,
36
+ maxSize = 0,
37
+ maxFiles = 0,
38
+ disabled = false,
39
+ tone = 'brand',
40
+ label,
41
+ hint,
42
+ showList = true,
43
+ onaccept,
44
+ onreject,
45
+ icon,
46
+ class: className = ''
47
+ }: Props = $props();
48
+
49
+ const t = useLocale();
50
+
51
+ let input: HTMLInputElement | null = $state(null);
52
+ let dragging = $state(false);
53
+ let rejected = $state<RejectedFile[]>([]);
54
+ /** Nested elements fire dragleave too, so count depth instead of toggling. */
55
+ let depth = 0;
56
+
57
+ const formatSize = (bytes: number) => toReadableSize(bytes, t.current.byteUnits);
58
+
59
+ const defaultHint = $derived.by(() => {
60
+ if (hint) return hint;
61
+ // Symbols rather than words, so the auto-hint needs no translation.
62
+ const parts: string[] = [];
63
+ if (accept) parts.push(accept.replace(/,\s*/g, ', '));
64
+ if (maxSize) parts.push(`≤ ${formatSize(maxSize)}`);
65
+ if (maxFiles) parts.push(`×${maxFiles}`);
66
+ return parts.join(' · ');
67
+ });
68
+
69
+ function add(incoming: FileList | File[] | null) {
70
+ if (!incoming || disabled) return;
71
+
72
+ const { accepted, rejected: bad } = sortFiles(incoming, {
73
+ accept,
74
+ maxSize,
75
+ maxFiles,
76
+ multiple,
77
+ held: files.length
78
+ });
79
+
80
+ rejected = bad;
81
+ if (bad.length) onreject?.(bad);
82
+ if (accepted.length === 0) return;
83
+
84
+ files = multiple ? [...files, ...accepted] : accepted.slice(0, 1);
85
+ onaccept?.(accepted);
86
+ }
87
+
88
+ function remove(index: number) {
89
+ files = files.filter((_, i) => i !== index);
90
+ }
91
+
92
+ function onDrop(event: DragEvent) {
93
+ event.preventDefault();
94
+ depth = 0;
95
+ dragging = false;
96
+ add(event.dataTransfer?.files ?? null);
97
+ }
98
+
99
+ const reasons = $derived({
100
+ type: t.current.rejectedType,
101
+ size: t.current.rejectedSize,
102
+ count: t.current.rejectedCount
103
+ });
104
+ </script>
105
+
106
+ <div class="flex w-full flex-col gap-3 {className}">
107
+ <button
108
+ type="button"
109
+ {disabled}
110
+ onclick={() => input?.click()}
111
+ ondragenter={(event) => {
112
+ event.preventDefault();
113
+ depth++;
114
+ dragging = true;
115
+ }}
116
+ ondragover={(event) => event.preventDefault()}
117
+ ondragleave={() => {
118
+ depth = Math.max(0, depth - 1);
119
+ if (depth === 0) dragging = false;
120
+ }}
121
+ ondrop={onDrop}
122
+ class="flex w-full flex-col items-center gap-3 rounded-[28px] border-2 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
123
+ {dragging
124
+ ? `${toneBorder[tone]} ${toneSurface[tone]}`
125
+ : 'border-hairline-strong bg-bg-alt/50 hover:bg-bg-alt'}
126
+ {toneText[tone]}"
127
+ >
128
+ <span class="flex size-12 items-center justify-center rounded-full {toneSoft[tone]}">
129
+ {#if icon}
130
+ {@render icon()}
131
+ {:else}
132
+ <svg class="size-5" viewBox="0 0 20 20" fill="none" aria-hidden="true">
133
+ <path
134
+ d="M10 14V3.5M6 7.5 10 3.5l4 4"
135
+ stroke="currentColor"
136
+ stroke-width="1.5"
137
+ stroke-linecap="round"
138
+ stroke-linejoin="round"
139
+ />
140
+ <path
141
+ d="M3.5 13v3.5h13V13"
142
+ stroke="currentColor"
143
+ stroke-width="1.5"
144
+ stroke-linecap="round"
145
+ />
146
+ </svg>
147
+ {/if}
148
+ </span>
149
+ <span class="flex flex-col gap-1">
150
+ <span class="text-sm font-semibold text-text">{label ?? t.current.dropFiles}</span>
151
+ <span class="text-[13px] text-text-secondary">
152
+ {t.current.or}
153
+ <span class="underline underline-offset-2">{t.current.browse}</span>
154
+ </span>
155
+ {#if defaultHint}
156
+ <span class="text-xs text-text-muted">{defaultHint}</span>
157
+ {/if}
158
+ </span>
159
+ </button>
160
+
161
+ <input
162
+ bind:this={input}
163
+ type="file"
164
+ {accept}
165
+ {multiple}
166
+ {disabled}
167
+ class="sr-only"
168
+ onchange={(event) => {
169
+ add(event.currentTarget.files);
170
+ event.currentTarget.value = '';
171
+ }}
172
+ />
173
+
174
+ {#if rejected.length}
175
+ <ul class="flex flex-col gap-1">
176
+ {#each rejected as item (item.file.name + item.reason)}
177
+ <li class="font-sans text-[13px] text-danger">
178
+ {item.file.name} — {reasons[item.reason]}
179
+ </li>
180
+ {/each}
181
+ </ul>
182
+ {/if}
183
+
184
+ {#if showList && files.length}
185
+ <ul class="flex flex-col rounded-[20px] border border-hairline bg-bg {edge}">
186
+ {#each files as file, i (file.name + file.size + i)}
187
+ <li
188
+ class="flex items-center gap-3 border-b border-hairline px-4 py-2.5 font-sans last:border-b-0"
189
+ >
190
+ <span class="min-w-0 flex-1 truncate text-sm text-text">{file.name}</span>
191
+ <span class="shrink-0 text-xs text-text-muted tabular-nums">{formatSize(file.size)}</span>
192
+ <button
193
+ type="button"
194
+ onclick={() => remove(i)}
195
+ aria-label="{t.current.remove} {file.name}"
196
+ class="-m-1 shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
197
+ >
198
+ <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
199
+ <path
200
+ d="m2 2 10 10M12 2 2 12"
201
+ stroke="currentColor"
202
+ stroke-width="1.6"
203
+ stroke-linecap="round"
204
+ />
205
+ </svg>
206
+ </button>
207
+ </li>
208
+ {/each}
209
+ </ul>
210
+ {/if}
211
+ </div>
@@ -0,0 +1,29 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { type Tone } from '../../core/tones.js';
3
+ import { type RejectedFile } from '../../core/files.js';
4
+ interface Props {
5
+ /** Bindable list of accepted files. */
6
+ files?: File[];
7
+ /** Same syntax as the input's `accept` — `'image/*,.pdf'`. */
8
+ accept?: string;
9
+ multiple?: boolean;
10
+ /** Per-file ceiling in bytes. `0` means no limit. */
11
+ maxSize?: number;
12
+ /** Cap on how many files may be held at once. `0` means no limit. */
13
+ maxFiles?: number;
14
+ disabled?: boolean;
15
+ tone?: Tone;
16
+ /** Headline inside the zone. Defaults to the locale's `dropFiles`. */
17
+ label?: string;
18
+ hint?: string;
19
+ /** Hide the built-in file list to render your own. */
20
+ showList?: boolean;
21
+ onaccept?: (files: File[]) => void;
22
+ onreject?: (rejected: RejectedFile[]) => void;
23
+ /** Replaces the glyph above the label. */
24
+ icon?: Snippet;
25
+ class?: string;
26
+ }
27
+ declare const Dropzone: import("svelte").Component<Props, {}, "files">;
28
+ type Dropzone = ReturnType<typeof Dropzone>;
29
+ export default Dropzone;
@@ -0,0 +1,71 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { toneSoft, type Tone } from '../../core/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 rounded-[28px] px-6 py-12 text-center font-sans
34
+ {bordered ? 'border border-dashed border-hairline-strong bg-bg-alt/50' : ''} {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
43
+ x="3"
44
+ y="3"
45
+ width="14"
46
+ height="14"
47
+ rx="2.5"
48
+ stroke="currentColor"
49
+ stroke-width="1.5"
50
+ />
51
+ <path
52
+ d="M3 13.5 7 9.5l3 3 3-3 4 4"
53
+ stroke="currentColor"
54
+ stroke-width="1.5"
55
+ stroke-linecap="round"
56
+ stroke-linejoin="round"
57
+ />
58
+ </svg>
59
+ {/if}
60
+ </span>
61
+ <div class="flex flex-col gap-1.5">
62
+ <p class="font-heading text-base font-semibold text-text">{title}</p>
63
+ {#if description}
64
+ <p class="max-w-sm text-sm leading-relaxed text-text-secondary">{description}</p>
65
+ {/if}
66
+ </div>
67
+ {#if children}{@render children()}{/if}
68
+ {#if action}
69
+ <div class="flex flex-wrap items-center justify-center gap-3 pt-1">{@render action()}</div>
70
+ {/if}
71
+ </div>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../../core/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,61 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import { focusRing, toneHoverText, toneRing, toneSolid, type Tone } from '../../core/tones.js';
5
+ import { liftSm, softOnHover } from './lift.js';
6
+
7
+ interface Props extends HTMLAttributes<HTMLDivElement> {
8
+ title: string;
9
+ description?: string;
10
+ tone?: Tone;
11
+ /** Turns the whole card into a link. */
12
+ href?: string;
13
+ /** Glyph inside the tinted square. */
14
+ icon?: Snippet;
15
+ children?: Snippet;
16
+ }
17
+
18
+ let {
19
+ title,
20
+ description,
21
+ tone = 'brand',
22
+ href,
23
+ icon,
24
+ class: className = '',
25
+ children,
26
+ ...rest
27
+ }: Props = $props();
28
+ </script>
29
+
30
+ <svelte:element
31
+ this={href ? 'a' : 'div'}
32
+ {href}
33
+ class="group flex flex-col gap-3 rounded-[28px] bg-bg p-6 {href
34
+ ? `transition-[box-shadow,transform] duration-150 ease-brand-out hover:-translate-y-0.5 ${softOnHover} ${focusRing} ${toneRing[tone]}`
35
+ : ''} {className}"
36
+ {...rest}
37
+ >
38
+ {#if icon}
39
+ <span
40
+ class="flex size-10 items-center justify-center rounded-xl {toneSolid[tone]} {liftSm[tone]}"
41
+ >
42
+ {@render icon()}
43
+ </span>
44
+ {/if}
45
+
46
+ <h3
47
+ class="font-heading text-base font-semibold text-text {href
48
+ ? `transition-colors duration-150 ${toneHoverText[tone]}`
49
+ : ''}"
50
+ >
51
+ {title}
52
+ </h3>
53
+
54
+ {#if description}
55
+ <p class="font-sans text-sm leading-relaxed text-text-secondary">{description}</p>
56
+ {/if}
57
+
58
+ {#if children}
59
+ {@render children()}
60
+ {/if}
61
+ </svelte:element>
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type Tone } from '../../core/tones.js';
4
+ interface Props extends HTMLAttributes<HTMLDivElement> {
5
+ title: string;
6
+ description?: string;
7
+ tone?: Tone;
8
+ /** Turns the whole card into a link. */
9
+ href?: string;
10
+ /** Glyph inside the tinted square. */
11
+ icon?: Snippet;
12
+ children?: Snippet;
13
+ }
14
+ declare const FeatureCard: import("svelte").Component<Props, {}, "">;
15
+ type FeatureCard = ReturnType<typeof FeatureCard>;
16
+ export default FeatureCard;
@@ -0,0 +1,62 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ export type FeatureColumns = 2 | 3 | 4;
6
+
7
+ interface Props extends HTMLAttributes<HTMLElement> {
8
+ eyebrow?: string;
9
+ title?: string;
10
+ description?: string;
11
+ columns?: FeatureColumns;
12
+ /** Cards in a bordered grid rather than free-standing. */
13
+ bordered?: boolean;
14
+ /** `FeatureCard`s, or anything else. */
15
+ children: Snippet;
16
+ }
17
+
18
+ let {
19
+ eyebrow,
20
+ title,
21
+ description,
22
+ columns = 3,
23
+ bordered = false,
24
+ class: className = '',
25
+ children,
26
+ ...rest
27
+ }: Props = $props();
28
+
29
+ const cols: Record<FeatureColumns, string> = {
30
+ 2: 'sm:grid-cols-2',
31
+ 3: 'sm:grid-cols-2 lg:grid-cols-3',
32
+ 4: 'sm:grid-cols-2 lg:grid-cols-4'
33
+ };
34
+ </script>
35
+
36
+ <section class="w-full px-6 py-20 {className}" {...rest}>
37
+ <div class="mx-auto flex w-full max-w-6xl flex-col gap-12">
38
+ {#if eyebrow || title || description}
39
+ <div class="flex max-w-xl flex-col gap-3">
40
+ {#if eyebrow}
41
+ <span class="font-sans text-[13px] font-semibold text-text-muted">{eyebrow}</span>
42
+ {/if}
43
+ {#if title}
44
+ <h2 class="font-heading text-3xl font-semibold tracking-tight text-balance text-text">
45
+ {title}
46
+ </h2>
47
+ {/if}
48
+ {#if description}
49
+ <p class="font-sans leading-relaxed text-pretty text-text-secondary">{description}</p>
50
+ {/if}
51
+ </div>
52
+ {/if}
53
+
54
+ <div
55
+ class="grid {cols[columns]} {bordered
56
+ ? 'gap-px overflow-hidden rounded-[28px] border border-hairline bg-hairline'
57
+ : 'gap-6'}"
58
+ >
59
+ {@render children()}
60
+ </div>
61
+ </div>
62
+ </section>
@@ -0,0 +1,16 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ export type FeatureColumns = 2 | 3 | 4;
4
+ interface Props extends HTMLAttributes<HTMLElement> {
5
+ eyebrow?: string;
6
+ title?: string;
7
+ description?: string;
8
+ columns?: FeatureColumns;
9
+ /** Cards in a bordered grid rather than free-standing. */
10
+ bordered?: boolean;
11
+ /** `FeatureCard`s, or anything else. */
12
+ children: Snippet;
13
+ }
14
+ declare const FeatureGrid: import("svelte").Component<Props, {}, "">;
15
+ type FeatureGrid = ReturnType<typeof FeatureGrid>;
16
+ export default FeatureGrid;
@@ -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-1.5 {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="ml-0.5 text-danger" aria-hidden="true">*</span>{/if}
34
+ </label>
35
+ {/if}
36
+ {@render children()}
37
+ {#if error}
38
+ <p class="font-sans text-[13px] text-danger">{error}</p>
39
+ {:else if hint}
40
+ <p class="font-sans text-[13px] 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,91 @@
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 '../../core/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-alt {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-sans text-[13px] font-semibold text-text">{column.title}</p>
53
+ <ul class="flex flex-col gap-2">
54
+ {#each column.links as item (item.label)}
55
+ <li>
56
+ <a
57
+ href={item.href}
58
+ target={item.external ? '_blank' : undefined}
59
+ rel={item.external ? 'noreferrer' : undefined}
60
+ class="font-sans text-[13px] text-text-muted transition-colors duration-150 ease-brand-out {toneHoverText[
61
+ tone
62
+ ]}"
63
+ >
64
+ {item.label}
65
+ {#if item.external}<span aria-hidden="true" class="text-[0.85em]">↗</span
66
+ >{/if}
67
+ </a>
68
+ </li>
69
+ {/each}
70
+ </ul>
71
+ </div>
72
+ {/each}
73
+ </div>
74
+ {/if}
75
+ </div>
76
+ {/if}
77
+
78
+ {#if copyright || bottom}
79
+ <div
80
+ class="flex flex-col gap-4 border-t border-hairline pt-6 sm:flex-row sm:items-center sm:justify-between"
81
+ >
82
+ {#if copyright}
83
+ <p class="font-sans text-[13px] text-text-muted">{copyright}</p>
84
+ {/if}
85
+ {#if bottom}
86
+ <div class="flex items-center gap-4">{@render bottom()}</div>
87
+ {/if}
88
+ </div>
89
+ {/if}
90
+ </div>
91
+ </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 '../../core/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;