@nqmcreative/ui 0.2.3 → 0.4.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 (232) hide show
  1. package/README.md +209 -15
  2. package/cli/index.mjs +328 -112
  3. package/dist/core/color.d.ts +24 -0
  4. package/dist/core/color.js +39 -0
  5. package/dist/core/index.d.ts +8 -1
  6. package/dist/core/index.js +5 -1
  7. package/dist/core/locale.svelte.d.ts +8 -0
  8. package/dist/core/locale.svelte.js +14 -0
  9. package/dist/core/number.d.ts +22 -0
  10. package/dist/core/number.js +33 -0
  11. package/dist/core/pin.d.ts +23 -0
  12. package/dist/core/pin.js +38 -0
  13. package/dist/core/tags.d.ts +37 -0
  14. package/dist/core/tags.js +51 -0
  15. package/dist/core/time.d.ts +30 -0
  16. package/dist/core/time.js +78 -0
  17. package/dist/core/toast.svelte.d.ts +3 -0
  18. package/dist/styles/matte/AccordionItem.svelte +5 -0
  19. package/dist/styles/matte/AccordionItem.svelte.d.ts +2 -0
  20. package/dist/styles/matte/Avatar.svelte +15 -0
  21. package/dist/styles/matte/Avatar.svelte.d.ts +3 -0
  22. package/dist/styles/matte/Badge.svelte +5 -0
  23. package/dist/styles/matte/Badge.svelte.d.ts +2 -0
  24. package/dist/styles/matte/Breadcrumb.svelte +11 -4
  25. package/dist/styles/matte/Breadcrumb.svelte.d.ts +3 -0
  26. package/dist/styles/matte/Button.svelte +58 -7
  27. package/dist/styles/matte/Button.svelte.d.ts +14 -2
  28. package/dist/styles/matte/Card.svelte +6 -1
  29. package/dist/styles/matte/Card.svelte.d.ts +2 -0
  30. package/dist/styles/matte/CheckboxGroup.svelte +6 -0
  31. package/dist/styles/matte/CheckboxGroup.svelte.d.ts +2 -0
  32. package/dist/styles/matte/ColorInput.svelte +110 -0
  33. package/dist/styles/matte/ColorInput.svelte.d.ts +16 -0
  34. package/dist/styles/matte/Combobox.svelte +5 -0
  35. package/dist/styles/matte/Combobox.svelte.d.ts +3 -0
  36. package/dist/styles/matte/CommandPalette.svelte +6 -0
  37. package/dist/styles/matte/CommandPalette.svelte.d.ts +3 -0
  38. package/dist/styles/matte/CurrencyInput.svelte +108 -0
  39. package/dist/styles/matte/CurrencyInput.svelte.d.ts +26 -0
  40. package/dist/styles/matte/Drawer.svelte +5 -0
  41. package/dist/styles/matte/Drawer.svelte.d.ts +2 -0
  42. package/dist/styles/matte/FileInput.svelte +194 -0
  43. package/dist/styles/matte/FileInput.svelte.d.ts +32 -0
  44. package/dist/styles/matte/Footer.svelte +6 -2
  45. package/dist/styles/matte/Footer.svelte.d.ts +2 -0
  46. package/dist/styles/matte/InputAddon.svelte +36 -0
  47. package/dist/styles/matte/InputAddon.svelte.d.ts +13 -0
  48. package/dist/styles/matte/Link.svelte +9 -0
  49. package/dist/styles/matte/Link.svelte.d.ts +4 -0
  50. package/dist/styles/matte/MenuItem.svelte +2 -1
  51. package/dist/styles/matte/Modal.svelte +5 -0
  52. package/dist/styles/matte/Modal.svelte.d.ts +2 -0
  53. package/dist/styles/matte/MultiSelect.svelte +7 -1
  54. package/dist/styles/matte/MultiSelect.svelte.d.ts +3 -0
  55. package/dist/styles/matte/Navbar.svelte +18 -4
  56. package/dist/styles/matte/Navbar.svelte.d.ts +2 -0
  57. package/dist/styles/matte/PinInput.svelte +137 -0
  58. package/dist/styles/matte/PinInput.svelte.d.ts +25 -0
  59. package/dist/styles/matte/PricingCard.svelte +16 -8
  60. package/dist/styles/matte/PricingCard.svelte.d.ts +2 -0
  61. package/dist/styles/matte/RadioGroup.svelte +6 -0
  62. package/dist/styles/matte/RadioGroup.svelte.d.ts +2 -0
  63. package/dist/styles/matte/SearchInput.svelte +87 -0
  64. package/dist/styles/matte/SearchInput.svelte.d.ts +24 -0
  65. package/dist/styles/matte/SegmentedControl.svelte +16 -2
  66. package/dist/styles/matte/SegmentedControl.svelte.d.ts +5 -0
  67. package/dist/styles/matte/Sidebar.svelte +2 -1
  68. package/dist/styles/matte/Stat.svelte +8 -1
  69. package/dist/styles/matte/Stat.svelte.d.ts +3 -0
  70. package/dist/styles/matte/Steps.svelte +7 -0
  71. package/dist/styles/matte/Steps.svelte.d.ts +3 -0
  72. package/dist/styles/matte/Table.svelte +153 -97
  73. package/dist/styles/matte/Table.svelte.d.ts +12 -0
  74. package/dist/styles/matte/Tabs.svelte +5 -0
  75. package/dist/styles/matte/Tabs.svelte.d.ts +3 -0
  76. package/dist/styles/matte/TagsInput.svelte +173 -0
  77. package/dist/styles/matte/TagsInput.svelte.d.ts +29 -0
  78. package/dist/styles/matte/TimeInput.svelte +129 -0
  79. package/dist/styles/matte/TimeInput.svelte.d.ts +21 -0
  80. package/dist/styles/matte/Toaster.svelte +3 -0
  81. package/dist/styles/matte/icon.d.ts +21 -0
  82. package/dist/styles/matte/icon.js +21 -0
  83. package/dist/styles/matte/index.d.ts +8 -2
  84. package/dist/styles/matte/index.js +8 -2
  85. package/dist/styles/paper/AccordionItem.svelte +5 -0
  86. package/dist/styles/paper/AccordionItem.svelte.d.ts +2 -0
  87. package/dist/styles/paper/Avatar.svelte +15 -0
  88. package/dist/styles/paper/Avatar.svelte.d.ts +3 -0
  89. package/dist/styles/paper/Badge.svelte +5 -0
  90. package/dist/styles/paper/Badge.svelte.d.ts +2 -0
  91. package/dist/styles/paper/Breadcrumb.svelte +8 -4
  92. package/dist/styles/paper/Breadcrumb.svelte.d.ts +3 -0
  93. package/dist/styles/paper/Button.svelte +58 -7
  94. package/dist/styles/paper/Button.svelte.d.ts +14 -2
  95. package/dist/styles/paper/Card.svelte +6 -1
  96. package/dist/styles/paper/Card.svelte.d.ts +2 -0
  97. package/dist/styles/paper/CheckboxGroup.svelte +6 -0
  98. package/dist/styles/paper/CheckboxGroup.svelte.d.ts +2 -0
  99. package/dist/styles/paper/ColorInput.svelte +112 -0
  100. package/dist/styles/paper/ColorInput.svelte.d.ts +16 -0
  101. package/dist/styles/paper/Combobox.svelte +5 -0
  102. package/dist/styles/paper/Combobox.svelte.d.ts +3 -0
  103. package/dist/styles/paper/CommandPalette.svelte +6 -0
  104. package/dist/styles/paper/CommandPalette.svelte.d.ts +3 -0
  105. package/dist/styles/paper/CurrencyInput.svelte +108 -0
  106. package/dist/styles/paper/CurrencyInput.svelte.d.ts +26 -0
  107. package/dist/styles/paper/Drawer.svelte +5 -0
  108. package/dist/styles/paper/Drawer.svelte.d.ts +2 -0
  109. package/dist/styles/paper/FileInput.svelte +204 -0
  110. package/dist/styles/paper/FileInput.svelte.d.ts +32 -0
  111. package/dist/styles/paper/Footer.svelte +6 -2
  112. package/dist/styles/paper/Footer.svelte.d.ts +2 -0
  113. package/dist/styles/paper/InputAddon.svelte +36 -0
  114. package/dist/styles/paper/InputAddon.svelte.d.ts +13 -0
  115. package/dist/styles/paper/Link.svelte +9 -0
  116. package/dist/styles/paper/Link.svelte.d.ts +4 -0
  117. package/dist/styles/paper/MenuItem.svelte +2 -1
  118. package/dist/styles/paper/Modal.svelte +5 -0
  119. package/dist/styles/paper/Modal.svelte.d.ts +2 -0
  120. package/dist/styles/paper/MultiSelect.svelte +7 -1
  121. package/dist/styles/paper/MultiSelect.svelte.d.ts +3 -0
  122. package/dist/styles/paper/Navbar.svelte +18 -4
  123. package/dist/styles/paper/Navbar.svelte.d.ts +2 -0
  124. package/dist/styles/paper/PinInput.svelte +137 -0
  125. package/dist/styles/paper/PinInput.svelte.d.ts +25 -0
  126. package/dist/styles/paper/PricingCard.svelte +23 -15
  127. package/dist/styles/paper/PricingCard.svelte.d.ts +2 -0
  128. package/dist/styles/paper/RadioGroup.svelte +6 -0
  129. package/dist/styles/paper/RadioGroup.svelte.d.ts +2 -0
  130. package/dist/styles/paper/SearchInput.svelte +92 -0
  131. package/dist/styles/paper/SearchInput.svelte.d.ts +24 -0
  132. package/dist/styles/paper/SegmentedControl.svelte +16 -2
  133. package/dist/styles/paper/SegmentedControl.svelte.d.ts +5 -0
  134. package/dist/styles/paper/Sidebar.svelte +2 -1
  135. package/dist/styles/paper/Stat.svelte +8 -1
  136. package/dist/styles/paper/Stat.svelte.d.ts +3 -0
  137. package/dist/styles/paper/Steps.svelte +7 -0
  138. package/dist/styles/paper/Steps.svelte.d.ts +3 -0
  139. package/dist/styles/paper/Table.svelte +153 -97
  140. package/dist/styles/paper/Table.svelte.d.ts +12 -0
  141. package/dist/styles/paper/Tabs.svelte +5 -0
  142. package/dist/styles/paper/Tabs.svelte.d.ts +3 -0
  143. package/dist/styles/paper/TagsInput.svelte +178 -0
  144. package/dist/styles/paper/TagsInput.svelte.d.ts +29 -0
  145. package/dist/styles/paper/TimeInput.svelte +155 -0
  146. package/dist/styles/paper/TimeInput.svelte.d.ts +21 -0
  147. package/dist/styles/paper/Toaster.svelte +3 -0
  148. package/dist/styles/paper/icon.d.ts +21 -0
  149. package/dist/styles/paper/icon.js +21 -0
  150. package/dist/styles/paper/index.d.ts +8 -2
  151. package/dist/styles/paper/index.js +8 -2
  152. package/dist/styles/sprout/AccordionItem.svelte +5 -0
  153. package/dist/styles/sprout/AccordionItem.svelte.d.ts +2 -0
  154. package/dist/styles/sprout/Avatar.svelte +15 -0
  155. package/dist/styles/sprout/Avatar.svelte.d.ts +3 -0
  156. package/dist/styles/sprout/Badge.svelte +5 -0
  157. package/dist/styles/sprout/Badge.svelte.d.ts +2 -0
  158. package/dist/styles/sprout/Breadcrumb.svelte +8 -4
  159. package/dist/styles/sprout/Breadcrumb.svelte.d.ts +3 -0
  160. package/dist/styles/sprout/Button.svelte +58 -7
  161. package/dist/styles/sprout/Button.svelte.d.ts +14 -2
  162. package/dist/styles/sprout/Card.svelte +6 -1
  163. package/dist/styles/sprout/Card.svelte.d.ts +2 -0
  164. package/dist/styles/sprout/CheckboxGroup.svelte +6 -0
  165. package/dist/styles/sprout/CheckboxGroup.svelte.d.ts +2 -0
  166. package/dist/styles/sprout/ColorInput.svelte +112 -0
  167. package/dist/styles/sprout/ColorInput.svelte.d.ts +16 -0
  168. package/dist/styles/sprout/Combobox.svelte +5 -0
  169. package/dist/styles/sprout/Combobox.svelte.d.ts +3 -0
  170. package/dist/styles/sprout/CommandPalette.svelte +6 -0
  171. package/dist/styles/sprout/CommandPalette.svelte.d.ts +3 -0
  172. package/dist/styles/sprout/CurrencyInput.svelte +108 -0
  173. package/dist/styles/sprout/CurrencyInput.svelte.d.ts +26 -0
  174. package/dist/styles/sprout/Drawer.svelte +5 -0
  175. package/dist/styles/sprout/Drawer.svelte.d.ts +2 -0
  176. package/dist/styles/sprout/FileInput.svelte +205 -0
  177. package/dist/styles/sprout/FileInput.svelte.d.ts +32 -0
  178. package/dist/styles/sprout/Footer.svelte +6 -2
  179. package/dist/styles/sprout/Footer.svelte.d.ts +2 -0
  180. package/dist/styles/sprout/InputAddon.svelte +37 -0
  181. package/dist/styles/sprout/InputAddon.svelte.d.ts +13 -0
  182. package/dist/styles/sprout/Link.svelte +9 -0
  183. package/dist/styles/sprout/Link.svelte.d.ts +4 -0
  184. package/dist/styles/sprout/MenuItem.svelte +2 -1
  185. package/dist/styles/sprout/Modal.svelte +5 -0
  186. package/dist/styles/sprout/Modal.svelte.d.ts +2 -0
  187. package/dist/styles/sprout/MultiSelect.svelte +7 -1
  188. package/dist/styles/sprout/MultiSelect.svelte.d.ts +3 -0
  189. package/dist/styles/sprout/Navbar.svelte +18 -4
  190. package/dist/styles/sprout/Navbar.svelte.d.ts +2 -0
  191. package/dist/styles/sprout/PinInput.svelte +138 -0
  192. package/dist/styles/sprout/PinInput.svelte.d.ts +25 -0
  193. package/dist/styles/sprout/PricingCard.svelte +23 -15
  194. package/dist/styles/sprout/PricingCard.svelte.d.ts +2 -0
  195. package/dist/styles/sprout/RadioGroup.svelte +6 -0
  196. package/dist/styles/sprout/RadioGroup.svelte.d.ts +2 -0
  197. package/dist/styles/sprout/SearchInput.svelte +92 -0
  198. package/dist/styles/sprout/SearchInput.svelte.d.ts +24 -0
  199. package/dist/styles/sprout/SegmentedControl.svelte +16 -2
  200. package/dist/styles/sprout/SegmentedControl.svelte.d.ts +5 -0
  201. package/dist/styles/sprout/Sidebar.svelte +2 -1
  202. package/dist/styles/sprout/Stat.svelte +8 -1
  203. package/dist/styles/sprout/Stat.svelte.d.ts +3 -0
  204. package/dist/styles/sprout/Steps.svelte +7 -0
  205. package/dist/styles/sprout/Steps.svelte.d.ts +3 -0
  206. package/dist/styles/sprout/Table.svelte +153 -97
  207. package/dist/styles/sprout/Table.svelte.d.ts +12 -0
  208. package/dist/styles/sprout/Tabs.svelte +5 -0
  209. package/dist/styles/sprout/Tabs.svelte.d.ts +3 -0
  210. package/dist/styles/sprout/TagsInput.svelte +179 -0
  211. package/dist/styles/sprout/TagsInput.svelte.d.ts +29 -0
  212. package/dist/styles/sprout/TimeInput.svelte +155 -0
  213. package/dist/styles/sprout/TimeInput.svelte.d.ts +21 -0
  214. package/dist/styles/sprout/Toaster.svelte +3 -0
  215. package/dist/styles/sprout/icon.d.ts +21 -0
  216. package/dist/styles/sprout/icon.js +21 -0
  217. package/dist/styles/sprout/index.d.ts +8 -2
  218. package/dist/styles/sprout/index.js +8 -2
  219. package/package.json +98 -26
  220. package/registry.json +143 -35
  221. package/dist/styles/matte/NumberInput.svelte +0 -96
  222. package/dist/styles/matte/NumberInput.svelte.d.ts +0 -15
  223. package/dist/styles/matte/PasswordInput.svelte +0 -95
  224. package/dist/styles/matte/PasswordInput.svelte.d.ts +0 -12
  225. package/dist/styles/paper/NumberInput.svelte +0 -96
  226. package/dist/styles/paper/NumberInput.svelte.d.ts +0 -15
  227. package/dist/styles/paper/PasswordInput.svelte +0 -100
  228. package/dist/styles/paper/PasswordInput.svelte.d.ts +0 -12
  229. package/dist/styles/sprout/NumberInput.svelte +0 -97
  230. package/dist/styles/sprout/NumberInput.svelte.d.ts +0 -15
  231. package/dist/styles/sprout/PasswordInput.svelte +0 -101
  232. package/dist/styles/sprout/PasswordInput.svelte.d.ts +0 -12
@@ -0,0 +1,194 @@
1
+ <script lang="ts">
2
+ import { formatSize as toReadableSize, sortFiles, type RejectedFile } from '../../core/files.js';
3
+ import { useLocale } from '../../core/locale.svelte.js';
4
+ import { toneFocusWithinBorder, type Tone } from '../../core/tones.js';
5
+ import type { InputSize } from './Input.svelte';
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
+ size?: InputSize;
19
+ tone?: Tone;
20
+ invalid?: boolean;
21
+ /** Text on the button. Defaults to the locale's `chooseFile`. */
22
+ label?: string;
23
+ /** Shown while nothing is chosen. Defaults to the locale's `noFile`. */
24
+ placeholder?: string;
25
+ /** Show the list of held files below the row. */
26
+ showList?: boolean;
27
+ /** The `name` the hidden input posts under, for a plain form submit. */
28
+ name?: string;
29
+ onaccept?: (files: File[]) => void;
30
+ onreject?: (rejected: RejectedFile[]) => void;
31
+ class?: string;
32
+ }
33
+
34
+ let {
35
+ files = $bindable([]),
36
+ accept,
37
+ multiple = false,
38
+ maxSize = 0,
39
+ maxFiles = 0,
40
+ disabled = false,
41
+ size = 'md',
42
+ tone = 'brand',
43
+ invalid = false,
44
+ label,
45
+ placeholder,
46
+ showList = true,
47
+ name,
48
+ onaccept,
49
+ onreject,
50
+ class: className = ''
51
+ }: Props = $props();
52
+
53
+ const t = useLocale();
54
+
55
+ let input: HTMLInputElement | null = $state(null);
56
+ let rejected = $state<RejectedFile[]>([]);
57
+
58
+ const formatSize = (bytes: number) => toReadableSize(bytes, t.current.byteUnits);
59
+
60
+ /** One file reads as its name and weight; several read as a count. */
61
+ const summary = $derived.by(() => {
62
+ if (files.length === 0) return placeholder ?? t.current.noFile;
63
+ if (files.length === 1) return `${files[0].name} · ${formatSize(files[0].size)}`;
64
+ return `${files.length} ${t.current.selected}`;
65
+ });
66
+
67
+ function add(incoming: FileList | File[] | null) {
68
+ if (!incoming || disabled) return;
69
+
70
+ const { accepted, rejected: bad } = sortFiles(incoming, {
71
+ accept,
72
+ maxSize,
73
+ maxFiles,
74
+ multiple,
75
+ held: files.length
76
+ });
77
+
78
+ rejected = bad;
79
+ if (bad.length) onreject?.(bad);
80
+ if (accepted.length === 0) return;
81
+
82
+ files = multiple ? [...files, ...accepted] : accepted.slice(0, 1);
83
+ onaccept?.(accepted);
84
+ }
85
+
86
+ function clear() {
87
+ files = [];
88
+ rejected = [];
89
+ if (input) input.value = '';
90
+ }
91
+
92
+ const reasons: Record<RejectedFile['reason'], string> = $derived({
93
+ type: t.current.rejectedType,
94
+ size: t.current.rejectedSize,
95
+ count: t.current.rejectedCount
96
+ });
97
+
98
+ const sizes: Record<InputSize, string> = {
99
+ sm: 'h-8 text-[13px]',
100
+ md: 'h-10 text-[15px]',
101
+ lg: 'h-11 text-base'
102
+ };
103
+
104
+ const buttons: Record<InputSize, string> = {
105
+ sm: 'h-6 px-2 text-[11px]',
106
+ md: 'h-7 px-2.5 text-xs',
107
+ lg: 'h-8 px-3 text-xs'
108
+ };
109
+ </script>
110
+
111
+ <div class="flex w-full flex-col gap-3 {className}">
112
+ <span
113
+ class="inline-flex w-full items-center gap-2 border bg-bg pr-1 pl-1 transition-colors duration-150 ease-brand-out
114
+ {sizes[size]}
115
+ {invalid
116
+ ? 'border-danger focus-within:border-danger'
117
+ : `border-hairline ${toneFocusWithinBorder[tone]}`}
118
+ {disabled ? 'pointer-events-none opacity-50' : ''}"
119
+ >
120
+ <button
121
+ type="button"
122
+ {disabled}
123
+ onclick={() => input?.click()}
124
+ class="shrink-0 border border-hairline bg-bg-alt font-mono tracking-wide text-text-secondary transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current
125
+ {buttons[size]}"
126
+ >
127
+ {label ?? t.current.chooseFile}
128
+ </button>
129
+
130
+ <span
131
+ class="min-w-0 flex-1 truncate font-sans {files.length ? 'text-text' : 'text-text-muted'}"
132
+ >
133
+ {summary}
134
+ </span>
135
+
136
+ {#if files.length}
137
+ <button
138
+ type="button"
139
+ onclick={clear}
140
+ aria-label={t.current.remove}
141
+ class="mr-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"
142
+ >
143
+ <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
144
+ <path d="m2 2 10 10M12 2 2 12" stroke="currentColor" stroke-width="1.5" />
145
+ </svg>
146
+ </button>
147
+ {/if}
148
+ </span>
149
+
150
+ <input
151
+ bind:this={input}
152
+ type="file"
153
+ {name}
154
+ {accept}
155
+ {multiple}
156
+ {disabled}
157
+ aria-invalid={invalid ? 'true' : undefined}
158
+ class="sr-only"
159
+ onchange={(event) => add(event.currentTarget.files)}
160
+ />
161
+
162
+ {#if rejected.length}
163
+ <ul class="flex flex-col gap-1">
164
+ {#each rejected as item (item.file.name + item.reason)}
165
+ <li class="font-sans text-sm text-danger">
166
+ {item.file.name} — {reasons[item.reason]}
167
+ </li>
168
+ {/each}
169
+ </ul>
170
+ {/if}
171
+
172
+ {#if showList && multiple && files.length > 1}
173
+ <ul class="flex flex-col border border-hairline">
174
+ {#each files as file, i (file.name + file.size + i)}
175
+ <li
176
+ class="flex items-center gap-3 border-b border-hairline px-3.5 py-2.5 font-sans last:border-b-0"
177
+ >
178
+ <span class="min-w-0 flex-1 truncate text-sm text-text">{file.name}</span>
179
+ <span class="shrink-0 font-mono text-xs text-text-muted">{formatSize(file.size)}</span>
180
+ <button
181
+ type="button"
182
+ onclick={() => (files = files.filter((_, at) => at !== i))}
183
+ aria-label="{t.current.remove} {file.name}"
184
+ 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"
185
+ >
186
+ <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
187
+ <path d="m2 2 10 10M12 2 2 12" stroke="currentColor" stroke-width="1.5" />
188
+ </svg>
189
+ </button>
190
+ </li>
191
+ {/each}
192
+ </ul>
193
+ {/if}
194
+ </div>
@@ -0,0 +1,32 @@
1
+ import { type RejectedFile } from '../../core/files.js';
2
+ import { type Tone } from '../../core/tones.js';
3
+ import type { InputSize } from './Input.svelte';
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
+ size?: InputSize;
16
+ tone?: Tone;
17
+ invalid?: boolean;
18
+ /** Text on the button. Defaults to the locale's `chooseFile`. */
19
+ label?: string;
20
+ /** Shown while nothing is chosen. Defaults to the locale's `noFile`. */
21
+ placeholder?: string;
22
+ /** Show the list of held files below the row. */
23
+ showList?: boolean;
24
+ /** The `name` the hidden input posts under, for a plain form submit. */
25
+ name?: string;
26
+ onaccept?: (files: File[]) => void;
27
+ onreject?: (rejected: RejectedFile[]) => void;
28
+ class?: string;
29
+ }
30
+ declare const FileInput: import("svelte").Component<Props, {}, "files">;
31
+ type FileInput = ReturnType<typeof FileInput>;
32
+ export default FileInput;
@@ -2,6 +2,8 @@
2
2
  export interface FooterLink {
3
3
  label: string;
4
4
  href?: string;
5
+ /** Leading icon, 14px. */
6
+ icon?: Snippet;
5
7
  external?: boolean;
6
8
  }
7
9
 
@@ -14,6 +16,7 @@
14
16
  <script lang="ts">
15
17
  import type { Snippet } from 'svelte';
16
18
  import { toneHoverText, type Tone } from '../../core/tones.js';
19
+ import { iconSm } from './icon.js';
17
20
 
18
21
  interface Props {
19
22
  columns?: FooterColumn[];
@@ -59,11 +62,12 @@
59
62
  href={item.href}
60
63
  target={item.external ? '_blank' : undefined}
61
64
  rel={item.external ? 'noreferrer' : undefined}
62
- class="font-sans text-sm text-text-secondary transition-colors duration-150 ease-brand-out {toneHoverText[
65
+ class="inline-flex items-center gap-2 font-sans text-sm text-text-secondary transition-colors duration-150 ease-brand-out {toneHoverText[
63
66
  tone
64
67
  ]}"
65
68
  >
66
- {item.label}
69
+ {#if item.icon}<span class={iconSm}>{@render item.icon()}</span
70
+ >{/if}{item.label}
67
71
  {#if item.external}<span aria-hidden="true" class="text-[0.85em]">↗</span
68
72
  >{/if}
69
73
  </a>
@@ -1,6 +1,8 @@
1
1
  export interface FooterLink {
2
2
  label: string;
3
3
  href?: string;
4
+ /** Leading icon, 14px. */
5
+ icon?: Snippet;
4
6
  external?: boolean;
5
7
  }
6
8
  export interface FooterColumn {
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+ import type { InputSize } from './Input.svelte';
5
+
6
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
7
+ /** Match the Input it sits beside — the heights have to agree. */
8
+ size?: InputSize;
9
+ /** Paint it like a field instead of a fixed label. */
10
+ variant?: 'muted' | 'plain';
11
+ children: Snippet;
12
+ }
13
+
14
+ let {
15
+ size = 'md',
16
+ variant = 'muted',
17
+ class: className = '',
18
+ children,
19
+ ...rest
20
+ }: Props = $props();
21
+
22
+ const sizes: Record<InputSize, string> = {
23
+ sm: 'h-8 px-2.5 text-[13px]',
24
+ md: 'h-10 px-3.5 text-[15px]',
25
+ lg: 'h-11 px-3.5 text-base'
26
+ };
27
+ </script>
28
+
29
+ <span
30
+ class="inline-flex shrink-0 items-center gap-1.5 border border-hairline font-mono whitespace-nowrap text-text-secondary
31
+ {sizes[size]}
32
+ {variant === 'muted' ? 'bg-bg-alt' : 'bg-bg'} {className}"
33
+ {...rest}
34
+ >
35
+ {@render children()}
36
+ </span>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import type { InputSize } from './Input.svelte';
4
+ interface Props extends HTMLAttributes<HTMLSpanElement> {
5
+ /** Match the Input it sits beside — the heights have to agree. */
6
+ size?: InputSize;
7
+ /** Paint it like a field instead of a fixed label. */
8
+ variant?: 'muted' | 'plain';
9
+ children: Snippet;
10
+ }
11
+ declare const InputAddon: import("svelte").Component<Props, {}, "">;
12
+ type InputAddon = ReturnType<typeof InputAddon>;
13
+ export default InputAddon;
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAnchorAttributes } from 'svelte/elements';
4
4
  import { focusRing, toneHoverText, toneRing, toneText, type Tone } from '../../core/tones.js';
5
+ import { iconSm } from './icon.js';
5
6
 
6
7
  interface Props extends HTMLAnchorAttributes {
7
8
  tone?: Tone;
@@ -11,6 +12,10 @@
11
12
  external?: boolean;
12
13
  /** Inherit the surrounding text colour instead of the tone. */
13
14
  muted?: boolean;
15
+ /** Leading icon, 14px. */
16
+ icon?: Snippet;
17
+ /** Trailing icon, before the external mark. */
18
+ iconEnd?: Snippet;
14
19
  children: Snippet;
15
20
  }
16
21
 
@@ -19,6 +24,8 @@
19
24
  underline = 'hover',
20
25
  external = false,
21
26
  muted = false,
27
+ icon,
28
+ iconEnd,
22
29
  class: className = '',
23
30
  children,
24
31
  ...rest
@@ -39,6 +46,8 @@
39
46
  {underlines[underline]} {focusRing} {toneRing[tone]} {className}"
40
47
  {...rest}
41
48
  >
49
+ {#if icon}<span class={iconSm}>{@render icon()}</span>{/if}
42
50
  {@render children()}
51
+ {#if iconEnd}<span class={iconSm}>{@render iconEnd()}</span>{/if}
43
52
  {#if external}<span aria-hidden="true" class="text-[0.85em]">↗</span>{/if}
44
53
  </a>
@@ -9,6 +9,10 @@ interface Props extends HTMLAnchorAttributes {
9
9
  external?: boolean;
10
10
  /** Inherit the surrounding text colour instead of the tone. */
11
11
  muted?: boolean;
12
+ /** Leading icon, 14px. */
13
+ icon?: Snippet;
14
+ /** Trailing icon, before the external mark. */
15
+ iconEnd?: Snippet;
12
16
  children: Snippet;
13
17
  }
14
18
  declare const Link: import("svelte").Component<Props, {}, "">;
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLButtonAttributes } from 'svelte/elements';
4
4
  import { toneSoftHover, toneText, type Tone } from '../../core/tones.js';
5
+ import { iconMd } from './icon.js';
5
6
 
6
7
  interface Props extends HTMLButtonAttributes {
7
8
  /** Renders an `<a>` instead of a `<button>`. */
@@ -49,7 +50,7 @@
49
50
  <path d="m2 7 3.2 3.2L12 3.6" stroke="currentColor" stroke-width="1.6" />
50
51
  </svg>
51
52
  {:else if icon}
52
- <span class="flex size-3.5 shrink-0 items-center justify-center">{@render icon()}</span>
53
+ <span class={iconMd}>{@render icon()}</span>
53
54
  {/if}
54
55
  <span class="min-w-0 flex-1 truncate">{@render children()}</span>
55
56
  {#if shortcut}
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { iconLg } from './icon.js';
3
4
  import type { HTMLDialogAttributes } from 'svelte/elements';
4
5
  import { useLocale } from '../../core/locale.svelte.js';
5
6
  import { isBackdropClick, syncDialog } from '../../core/dialog.svelte.js';
@@ -11,6 +12,8 @@
11
12
  open?: boolean;
12
13
  title?: string;
13
14
  description?: string;
15
+ /** Leading icon beside the title. */
16
+ icon?: Snippet;
14
17
  size?: ModalSize;
15
18
  /** Clicking the backdrop or pressing Escape closes the dialog. */
16
19
  dismissible?: boolean;
@@ -26,6 +29,7 @@
26
29
  open = $bindable(false),
27
30
  title,
28
31
  description,
32
+ icon,
29
33
  size = 'md',
30
34
  dismissible = true,
31
35
  showClose,
@@ -75,6 +79,7 @@
75
79
  <div class="flex flex-col">
76
80
  {#if title || closable}
77
81
  <div class="flex items-start gap-4 border-b border-hairline px-5 py-4">
82
+ {#if icon}<span class={iconLg}>{@render icon()}</span>{/if}
78
83
  <div class="flex min-w-0 flex-1 flex-col gap-1">
79
84
  {#if title}
80
85
  <h2 class="font-heading text-lg font-medium tracking-tight">{title}</h2>
@@ -6,6 +6,8 @@ interface Props extends HTMLDialogAttributes {
6
6
  open?: boolean;
7
7
  title?: string;
8
8
  description?: string;
9
+ /** Leading icon beside the title. */
10
+ icon?: Snippet;
9
11
  size?: ModalSize;
10
12
  /** Clicking the backdrop or pressing Escape closes the dialog. */
11
13
  dismissible?: boolean;
@@ -1,7 +1,11 @@
1
1
  <script module lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
2
4
  export interface MultiSelectOption {
3
5
  value: string;
4
6
  label: string;
7
+ /** Leading icon, 16px. */
8
+ icon?: Snippet;
5
9
  description?: string;
6
10
  disabled?: boolean;
7
11
  group?: string;
@@ -13,6 +17,7 @@
13
17
  import { clickOutside, portal } from '../../core/actions/dismissable.js';
14
18
  import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
15
19
  import { useLocale } from '../../core/locale.svelte.js';
20
+ import { iconMd, iconSm } from './icon.js';
16
21
  import { toneFocusWithinBorder, toneSoft, toneText, type Tone } from '../../core/tones.js';
17
22
 
18
23
  interface Props {
@@ -153,7 +158,7 @@
153
158
  tone
154
159
  ]}"
155
160
  >
156
- {option.label}
161
+ {#if option.icon}<span class={iconSm}>{@render option.icon()}</span>{/if}{option.label}
157
162
  <button
158
163
  type="button"
159
164
  onclick={() => remove(option)}
@@ -259,6 +264,7 @@
259
264
  </svg>
260
265
  {/if}
261
266
  </span>
267
+ {#if option.icon}<span class={iconMd}>{@render option.icon()}</span>{/if}
262
268
  <span class="flex min-w-0 flex-1 flex-col gap-0.5">
263
269
  <span class="truncate">{option.label}</span>
264
270
  {#if option.description}
@@ -1,6 +1,9 @@
1
+ import type { Snippet } from 'svelte';
1
2
  export interface MultiSelectOption {
2
3
  value: string;
3
4
  label: string;
5
+ /** Leading icon, 16px. */
6
+ icon?: Snippet;
4
7
  description?: string;
5
8
  disabled?: boolean;
6
9
  group?: string;
@@ -2,6 +2,8 @@
2
2
  export interface NavItem {
3
3
  label: string;
4
4
  href?: string;
5
+ /** Leading icon, 16px. */
6
+ icon?: Snippet;
5
7
  /** Marks the current page — also sets `aria-current`. */
6
8
  active?: boolean;
7
9
  disabled?: boolean;
@@ -15,6 +17,7 @@
15
17
  <script lang="ts">
16
18
  import type { Snippet } from 'svelte';
17
19
  import { useLocale } from '../../core/locale.svelte.js';
20
+ import { iconMd } from './icon.js';
18
21
  import { focusRing, toneRing, toneText, type Tone } from '../../core/tones.js';
19
22
  import Drawer from './Drawer.svelte';
20
23
  import Dropdown from './Dropdown.svelte';
@@ -82,6 +85,7 @@
82
85
  ? toneText[tone]
83
86
  : 'text-text-secondary hover:text-text'}"
84
87
  >
88
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
85
89
  {item.label}
86
90
  <svg class="size-3" viewBox="0 0 16 16" fill="none" aria-hidden="true">
87
91
  <path
@@ -94,7 +98,12 @@
94
98
  </button>
95
99
  {/snippet}
96
100
  {#each item.items as child (child.label)}
97
- <MenuItem href={child.href} disabled={child.disabled} selected={child.active}>
101
+ <MenuItem
102
+ href={child.href}
103
+ disabled={child.disabled}
104
+ selected={child.active}
105
+ icon={child.icon}
106
+ >
98
107
  {child.label}
99
108
  </MenuItem>
100
109
  {/each}
@@ -109,6 +118,7 @@
109
118
  ? 'pointer-events-none opacity-50'
110
119
  : ''}"
111
120
  >
121
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
112
122
  {item.label}
113
123
  {#if item.badge !== undefined}
114
124
  <span class="font-mono text-[11px] text-text-muted">{item.badge}</span>
@@ -154,7 +164,10 @@
154
164
  ? toneText[tone]
155
165
  : 'text-text-secondary'} {item.disabled ? 'pointer-events-none opacity-50' : ''}"
156
166
  >
157
- {item.label}
167
+ <span class="flex min-w-0 items-center gap-2.5">
168
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
169
+ {item.label}
170
+ </span>
158
171
  {#if item.badge !== undefined}
159
172
  <span class="font-mono text-[11px] text-text-muted">{item.badge}</span>
160
173
  {/if}
@@ -166,9 +179,10 @@
166
179
  <a
167
180
  href={child.href}
168
181
  onclick={() => (menuOpen = false)}
169
- class="block py-2 font-sans text-sm text-text-muted"
182
+ class="flex items-center gap-2.5 py-2 font-sans text-sm text-text-muted"
170
183
  >
171
- {child.label}
184
+ {#if child.icon}<span class={iconMd}>{@render child.icon()}</span
185
+ >{/if}{child.label}
172
186
  </a>
173
187
  </li>
174
188
  {/each}
@@ -1,6 +1,8 @@
1
1
  export interface NavItem {
2
2
  label: string;
3
3
  href?: string;
4
+ /** Leading icon, 16px. */
5
+ icon?: Snippet;
4
6
  /** Marks the current page — also sets `aria-current`. */
5
7
  active?: boolean;
6
8
  disabled?: boolean;