@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,16 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import type { Tone } from '../../core/tones.js';
3
+ import { type InputSize } from './Input.svelte';
4
+ interface Props extends Omit<HTMLInputAttributes, 'size' | 'prefix' | 'suffix' | 'type'> {
5
+ /** Bindable `#rrggbb`. Shorthand and alpha are normalised away on blur. */
6
+ value?: string;
7
+ size?: InputSize;
8
+ tone?: Tone;
9
+ invalid?: boolean;
10
+ /** Fixed choices under the field. Empty hides the row. */
11
+ swatches?: string[];
12
+ class?: string;
13
+ }
14
+ declare const ColorInput: import("svelte").Component<Props, {}, "value">;
15
+ type ColorInput = ReturnType<typeof ColorInput>;
16
+ export default ColorInput;
@@ -1,13 +1,17 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import { anchored } from '../../core/actions/anchor.js';
3
4
  import { clickOutside, portal } from '../../core/actions/dismissable.js';
4
5
  import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
5
6
  import { toneFocusWithinBorder, toneSoft, toneText, type Tone } from '../../core/tones.js';
6
7
  import { useLocale } from '../../core/locale.svelte.js';
8
+ import { iconMd } from './icon.js';
7
9
 
8
10
  export interface ComboboxOption {
9
11
  value: string;
10
12
  label: string;
13
+ /** Leading icon, 16px. */
14
+ icon?: Snippet;
11
15
  /** Second line under the label. */
12
16
  description?: string;
13
17
  disabled?: boolean;
@@ -237,6 +241,7 @@
237
241
  {index === active ? toneSoft[tone] : 'text-text-secondary'}"
238
242
  >
239
243
  <span class="flex items-center gap-2">
244
+ {#if option.icon}<span class={iconMd}>{@render option.icon()}</span>{/if}
240
245
  <span class="min-w-0 flex-1 truncate">{option.label}</span>
241
246
  {#if option.value === value}
242
247
  <svg
@@ -1,7 +1,10 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import { type Tone } from '../../core/tones.js';
2
3
  export interface ComboboxOption {
3
4
  value: string;
4
5
  label: string;
6
+ /** Leading icon, 16px. */
7
+ icon?: Snippet;
5
8
  /** Second line under the label. */
6
9
  description?: string;
7
10
  disabled?: boolean;
@@ -1,7 +1,11 @@
1
1
  <script module lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+
2
4
  export interface CommandItem {
3
5
  id: string;
4
6
  label: string;
7
+ /** Leading icon, 16px. */
8
+ icon?: Snippet;
5
9
  /** Second line under the label. */
6
10
  description?: string;
7
11
  /** Items sharing a group are rendered under one heading. */
@@ -22,6 +26,7 @@
22
26
  import { useLocale } from '../../core/locale.svelte.js';
23
27
  import { toneSoft, type Tone } from '../../core/tones.js';
24
28
  import { isTypingTarget } from '../../core/trigger.js';
29
+ import { iconMd } from './icon.js';
25
30
  import Kbd from './Kbd.svelte';
26
31
 
27
32
  interface Props {
@@ -214,6 +219,7 @@
214
219
  {item.disabled ? 'pointer-events-none opacity-40' : ''}
215
220
  {index === active ? toneSoft[tone] : 'text-text-secondary'}"
216
221
  >
222
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
217
223
  <span class="flex min-w-0 flex-1 flex-col gap-0.5">
218
224
  <span class="truncate">{item.label}</span>
219
225
  {#if item.description}
@@ -1,6 +1,9 @@
1
+ import type { Snippet } from 'svelte';
1
2
  export interface CommandItem {
2
3
  id: string;
3
4
  label: string;
5
+ /** Leading icon, 16px. */
6
+ icon?: Snippet;
4
7
  /** Second line under the label. */
5
8
  description?: string;
6
9
  /** Items sharing a group are rendered under one heading. */
@@ -0,0 +1,108 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import { formatGrouped, parseGrouped } from '../../core/number.js';
4
+ import type { Tone } from '../../core/tones.js';
5
+ import Input, { type InputSize } from './Input.svelte';
6
+
7
+ interface Props extends Omit<
8
+ HTMLInputAttributes,
9
+ 'size' | 'prefix' | 'suffix' | 'value' | 'min' | 'max'
10
+ > {
11
+ /** Bindable amount. `null` is an empty field, which is not the same as 0. */
12
+ value?: number | null;
13
+ min?: number;
14
+ max?: number;
15
+ /** Thousands separator. */
16
+ group?: string;
17
+ /** Decimal separator. */
18
+ decimal?: string;
19
+ /** Decimal places the amount settles to when the field loses focus. */
20
+ precision?: number;
21
+ /** Leading mark — `Rp`, `$`, `€`. */
22
+ currency?: string;
23
+ /** Trailing mark instead, for the locales that write it that way. */
24
+ unit?: string;
25
+ size?: InputSize;
26
+ tone?: Tone;
27
+ invalid?: boolean;
28
+ class?: string;
29
+ }
30
+
31
+ let {
32
+ value = $bindable(null),
33
+ min,
34
+ max,
35
+ group = ',',
36
+ decimal = '.',
37
+ precision = 2,
38
+ currency,
39
+ unit,
40
+ size = 'md',
41
+ tone = 'brand',
42
+ invalid = false,
43
+ disabled = false,
44
+ class: className = '',
45
+ ...rest
46
+ }: Props = $props();
47
+
48
+ const options = $derived({ group, decimal, precision });
49
+
50
+ /**
51
+ * What the field shows. Two states on purpose: grouped and padded while the
52
+ * caret is elsewhere, and exactly what was typed while it is not — grouping
53
+ * a number as it is typed moves the caret out from under the fingers.
54
+ */
55
+ let text = $state('');
56
+ let editing = $state(false);
57
+
58
+ $effect(() => {
59
+ if (editing) return;
60
+ text = value === null || value === undefined ? '' : formatGrouped(value, options);
61
+ });
62
+
63
+ // Read off the element, not off `text`: the binding that feeds `text` has
64
+ // not flushed yet when this fires, so it still holds the previous keystroke.
65
+ function onInput(event: Event & { currentTarget: HTMLInputElement }) {
66
+ const parsed = parseGrouped(event.currentTarget.value, options);
67
+ value = parsed === null ? null : clamp(parsed);
68
+ }
69
+
70
+ function clamp(amount: number) {
71
+ if (min !== undefined && amount < min) return min;
72
+ if (max !== undefined && amount > max) return max;
73
+ return amount;
74
+ }
75
+
76
+ function settle() {
77
+ editing = false;
78
+ const parsed = parseGrouped(text, options);
79
+ value = parsed === null ? null : clamp(parsed);
80
+ text = value === null ? '' : formatGrouped(value, options);
81
+ }
82
+ </script>
83
+
84
+ <Input
85
+ bind:value={text}
86
+ {size}
87
+ {tone}
88
+ {invalid}
89
+ {disabled}
90
+ inputmode="decimal"
91
+ oninput={onInput}
92
+ onfocus={() => (editing = true)}
93
+ onblur={settle}
94
+ class={className}
95
+ {...rest}
96
+ >
97
+ {#snippet prefix()}
98
+ {#if currency}
99
+ <span class="text-xs font-medium text-text-muted">{currency}</span>
100
+ {/if}
101
+ {/snippet}
102
+
103
+ {#snippet suffix()}
104
+ {#if unit}
105
+ <span class="text-xs font-medium text-text-muted">{unit}</span>
106
+ {/if}
107
+ {/snippet}
108
+ </Input>
@@ -0,0 +1,26 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import type { Tone } from '../../core/tones.js';
3
+ import { type InputSize } from './Input.svelte';
4
+ interface Props extends Omit<HTMLInputAttributes, 'size' | 'prefix' | 'suffix' | 'value' | 'min' | 'max'> {
5
+ /** Bindable amount. `null` is an empty field, which is not the same as 0. */
6
+ value?: number | null;
7
+ min?: number;
8
+ max?: number;
9
+ /** Thousands separator. */
10
+ group?: string;
11
+ /** Decimal separator. */
12
+ decimal?: string;
13
+ /** Decimal places the amount settles to when the field loses focus. */
14
+ precision?: number;
15
+ /** Leading mark — `Rp`, `$`, `€`. */
16
+ currency?: string;
17
+ /** Trailing mark instead, for the locales that write it that way. */
18
+ unit?: string;
19
+ size?: InputSize;
20
+ tone?: Tone;
21
+ invalid?: boolean;
22
+ class?: string;
23
+ }
24
+ declare const CurrencyInput: import("svelte").Component<Props, {}, "value">;
25
+ type CurrencyInput = ReturnType<typeof CurrencyInput>;
26
+ export default CurrencyInput;
@@ -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';
@@ -15,6 +16,8 @@
15
16
  size?: DrawerSize;
16
17
  title?: string;
17
18
  description?: string;
19
+ /** Leading icon beside the title. */
20
+ icon?: Snippet;
18
21
  dismissible?: boolean;
19
22
  footer?: Snippet;
20
23
  onclose?: () => void;
@@ -27,6 +30,7 @@
27
30
  size = 'md',
28
31
  title,
29
32
  description,
33
+ icon,
30
34
  dismissible = true,
31
35
  footer,
32
36
  onclose,
@@ -92,6 +96,7 @@
92
96
  <div class="flex h-full flex-col">
93
97
  {#if title || dismissible}
94
98
  <div class="flex items-start gap-4 border-b border-hairline px-5 py-4">
99
+ {#if icon}<span class={iconLg}>{@render icon()}</span>{/if}
95
100
  <div class="flex min-w-0 flex-1 flex-col gap-1">
96
101
  {#if title}
97
102
  <h2 class="font-heading text-base font-semibold text-text">{title}</h2>
@@ -10,6 +10,8 @@ interface Props extends HTMLDialogAttributes {
10
10
  size?: DrawerSize;
11
11
  title?: string;
12
12
  description?: string;
13
+ /** Leading icon beside the title. */
14
+ icon?: Snippet;
13
15
  dismissible?: boolean;
14
16
  footer?: Snippet;
15
17
  onclose?: () => void;
@@ -0,0 +1,204 @@
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-sm',
101
+ lg: 'h-11 text-[15px]'
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 rounded-md border bg-bg px-1 shadow-xs transition-colors duration-150 ease-brand-out
114
+ {sizes[size]}
115
+ {invalid
116
+ ? 'border-danger focus-within:border-danger'
117
+ : `border-hairline-strong ${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 rounded border border-hairline-strong bg-bg-alt font-sans font-medium 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 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"
142
+ >
143
+ <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
144
+ <path
145
+ d="m2 2 10 10M12 2 2 12"
146
+ stroke="currentColor"
147
+ stroke-width="1.6"
148
+ stroke-linecap="round"
149
+ />
150
+ </svg>
151
+ </button>
152
+ {/if}
153
+ </span>
154
+
155
+ <input
156
+ bind:this={input}
157
+ type="file"
158
+ {name}
159
+ {accept}
160
+ {multiple}
161
+ {disabled}
162
+ aria-invalid={invalid ? 'true' : undefined}
163
+ class="sr-only"
164
+ onchange={(event) => add(event.currentTarget.files)}
165
+ />
166
+
167
+ {#if rejected.length}
168
+ <ul class="flex flex-col gap-1">
169
+ {#each rejected as item (item.file.name + item.reason)}
170
+ <li class="font-sans text-sm text-danger">
171
+ {item.file.name} — {reasons[item.reason]}
172
+ </li>
173
+ {/each}
174
+ </ul>
175
+ {/if}
176
+
177
+ {#if showList && multiple && files.length > 1}
178
+ <ul class="flex flex-col rounded-lg border border-hairline bg-bg shadow-xs">
179
+ {#each files as file, i (file.name + file.size + i)}
180
+ <li
181
+ class="flex items-center gap-3 border-b border-hairline px-4 py-2.5 font-sans last:border-b-0"
182
+ >
183
+ <span class="min-w-0 flex-1 truncate text-sm text-text">{file.name}</span>
184
+ <span class="shrink-0 text-xs text-text-muted tabular-nums">{formatSize(file.size)}</span>
185
+ <button
186
+ type="button"
187
+ onclick={() => (files = files.filter((_, at) => at !== i))}
188
+ aria-label="{t.current.remove} {file.name}"
189
+ 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"
190
+ >
191
+ <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
192
+ <path
193
+ d="m2 2 10 10M12 2 2 12"
194
+ stroke="currentColor"
195
+ stroke-width="1.6"
196
+ stroke-linecap="round"
197
+ />
198
+ </svg>
199
+ </button>
200
+ </li>
201
+ {/each}
202
+ </ul>
203
+ {/if}
204
+ </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[];
@@ -57,11 +60,12 @@
57
60
  href={item.href}
58
61
  target={item.external ? '_blank' : undefined}
59
62
  rel={item.external ? 'noreferrer' : undefined}
60
- class="font-sans text-[13px] text-text-muted transition-colors duration-150 ease-brand-out {toneHoverText[
63
+ class="inline-flex items-center gap-2 font-sans text-[13px] text-text-muted transition-colors duration-150 ease-brand-out {toneHoverText[
61
64
  tone
62
65
  ]}"
63
66
  >
64
- {item.label}
67
+ {#if item.icon}<span class={iconSm}>{@render item.icon()}</span
68
+ >{/if}{item.label}
65
69
  {#if item.external}<span aria-hidden="true" class="text-[0.85em]">↗</span
66
70
  >{/if}
67
71
  </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 text-sm',
25
+ lg: 'h-11 px-3 text-[15px]'
26
+ };
27
+ </script>
28
+
29
+ <span
30
+ class="inline-flex shrink-0 items-center gap-1.5 rounded-md border border-hairline-strong font-sans whitespace-nowrap text-text-secondary shadow-xs
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>`. */
@@ -55,7 +56,7 @@
55
56
  />
56
57
  </svg>
57
58
  {:else if icon}
58
- <span class="flex size-3.5 shrink-0 items-center justify-center">{@render icon()}</span>
59
+ <span class={iconMd}>{@render icon()}</span>
59
60
  {/if}
60
61
  <span class="min-w-0 flex-1 truncate">{@render children()}</span>
61
62
  {#if shortcut}