@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,155 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+ import { useLocale } from '../../core/locale.svelte.js';
4
+ import { clampTime, formatTime, parseTime, stepTime } from '../../core/time.js';
5
+ import type { Tone } from '../../core/tones.js';
6
+ import Input, { type InputSize } from './Input.svelte';
7
+
8
+ interface Props extends Omit<
9
+ HTMLInputAttributes,
10
+ 'size' | 'prefix' | 'suffix' | 'min' | 'max' | 'step'
11
+ > {
12
+ /** Bindable `HH:MM`, or `''` for empty. */
13
+ value?: string;
14
+ /** Inclusive bounds, `HH:MM`. */
15
+ min?: string;
16
+ max?: string;
17
+ /** Minutes one press of a stepper moves, and what typing snaps to. */
18
+ step?: number;
19
+ size?: InputSize;
20
+ tone?: Tone;
21
+ invalid?: boolean;
22
+ /** Hide the steppers and let the field be typed into only. */
23
+ steppers?: boolean;
24
+ class?: string;
25
+ }
26
+
27
+ let {
28
+ value = $bindable(''),
29
+ min,
30
+ max,
31
+ step = 15,
32
+ size = 'md',
33
+ tone = 'brand',
34
+ invalid = false,
35
+ disabled = false,
36
+ steppers = true,
37
+ class: className = '',
38
+ ...rest
39
+ }: Props = $props();
40
+
41
+ const t = useLocale();
42
+
43
+ // Typed text and the bound value are separate until the field settles:
44
+ // `9` is a valid thing to be halfway through typing and an invalid time.
45
+ let text = $state('');
46
+ let editing = $state(false);
47
+
48
+ $effect(() => {
49
+ if (!editing) text = value;
50
+ });
51
+
52
+ function settle() {
53
+ editing = false;
54
+ const minutes = parseTime(text);
55
+ value = minutes === null ? '' : formatTime(clampTime(minutes, min, max, step));
56
+ text = value;
57
+ }
58
+
59
+ function nudge(direction: 1 | -1) {
60
+ value = stepTime(value, direction, step, min, max);
61
+ text = value;
62
+ }
63
+
64
+ function onKeydown(event: KeyboardEvent) {
65
+ if (event.key === 'ArrowUp') {
66
+ event.preventDefault();
67
+ nudge(1);
68
+ } else if (event.key === 'ArrowDown') {
69
+ event.preventDefault();
70
+ nudge(-1);
71
+ } else if (event.key === 'Enter') {
72
+ settle();
73
+ }
74
+ }
75
+
76
+ const stepper =
77
+ 'grid size-6 place-items-center rounded text-text-secondary transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-current';
78
+ </script>
79
+
80
+ <Input
81
+ bind:value={text}
82
+ {size}
83
+ {tone}
84
+ {invalid}
85
+ {disabled}
86
+ inputmode="numeric"
87
+ placeholder="--:--"
88
+ onfocus={() => (editing = true)}
89
+ onblur={settle}
90
+ onkeydown={onKeydown}
91
+ class="[&_input]:tabular-nums {className}"
92
+ {...rest}
93
+ >
94
+ {#snippet prefix()}
95
+ <svg class="size-4" viewBox="0 0 20 20" fill="none" aria-hidden="true">
96
+ <circle
97
+ cx="10"
98
+ cy="10"
99
+ r="6.5"
100
+ stroke="currentColor"
101
+ stroke-width="1.6"
102
+ stroke-linecap="round"
103
+ stroke-linejoin="round"
104
+ />
105
+ <path
106
+ d="M10 6.5V10l2.5 1.5"
107
+ stroke="currentColor"
108
+ stroke-width="1.6"
109
+ stroke-linecap="round"
110
+ stroke-linejoin="round"
111
+ />
112
+ </svg>
113
+ {/snippet}
114
+
115
+ {#snippet suffix()}
116
+ {#if steppers}
117
+ <span class="-mr-2 flex flex-col">
118
+ <button
119
+ type="button"
120
+ class={stepper}
121
+ {disabled}
122
+ onclick={() => nudge(1)}
123
+ aria-label={t.current.increase}
124
+ >
125
+ <svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
126
+ <path
127
+ d="m3 7.5 3-3 3 3"
128
+ stroke="currentColor"
129
+ stroke-width="1.6"
130
+ stroke-linecap="round"
131
+ stroke-linejoin="round"
132
+ />
133
+ </svg>
134
+ </button>
135
+ <button
136
+ type="button"
137
+ class={stepper}
138
+ {disabled}
139
+ onclick={() => nudge(-1)}
140
+ aria-label={t.current.decrease}
141
+ >
142
+ <svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
143
+ <path
144
+ d="m3 4.5 3 3 3-3"
145
+ stroke="currentColor"
146
+ stroke-width="1.6"
147
+ stroke-linecap="round"
148
+ stroke-linejoin="round"
149
+ />
150
+ </svg>
151
+ </button>
152
+ </span>
153
+ {/if}
154
+ {/snippet}
155
+ </Input>
@@ -0,0 +1,21 @@
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' | 'min' | 'max' | 'step'> {
5
+ /** Bindable `HH:MM`, or `''` for empty. */
6
+ value?: string;
7
+ /** Inclusive bounds, `HH:MM`. */
8
+ min?: string;
9
+ max?: string;
10
+ /** Minutes one press of a stepper moves, and what typing snaps to. */
11
+ step?: number;
12
+ size?: InputSize;
13
+ tone?: Tone;
14
+ invalid?: boolean;
15
+ /** Hide the steppers and let the field be typed into only. */
16
+ steppers?: boolean;
17
+ class?: string;
18
+ }
19
+ declare const TimeInput: import("svelte").Component<Props, {}, "value">;
20
+ type TimeInput = ReturnType<typeof TimeInput>;
21
+ export default TimeInput;
@@ -4,6 +4,7 @@
4
4
  import { portal } from '../../core/actions/dismissable.js';
5
5
  import { toast } from '../../core/toast.svelte.js';
6
6
  import { toneText } from '../../core/tones.js';
7
+ import { iconMd } from './icon.js';
7
8
  import Spinner from './Spinner.svelte';
8
9
  import { useLocale } from '../../core/locale.svelte.js';
9
10
 
@@ -61,6 +62,8 @@
61
62
  <span class="mt-0.5 shrink-0 {toneText[item.tone]}">
62
63
  {#if item.loading}
63
64
  <Spinner size="sm" label="" />
65
+ {:else if item.icon}
66
+ <span class={iconMd}>{@render item.icon()}</span>
64
67
  {:else if item.tone === 'success'}
65
68
  <svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
66
69
  <circle cx="8" cy="8" r="6.5" stroke="currentColor" stroke-width="1.5" />
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The box a consumer-supplied icon renders into.
3
+ *
4
+ * Every icon in the library is a `Snippet` the app writes — the package ships
5
+ * no icon set and no runtime dependency — so the component has to size the
6
+ * glyph rather than trust it. The `*:` variant hits the snippet's own element,
7
+ * which is what keeps a bare `<svg viewBox="0 0 16 16">` from laying out at its
8
+ * intrinsic 300×150.
9
+ *
10
+ * Kept per style rather than in `core/`: an app only `@source`s the style
11
+ * folder it installed, so a class string living outside it would never be
12
+ * generated by Tailwind.
13
+ */
14
+ /** 14px — menu rows, chips, anything sitting on one line of small text. */
15
+ export declare const iconSm = "grid size-3.5 shrink-0 place-items-center *:size-3.5";
16
+ /** 16px — the default: nav items, tabs, buttons, list rows. */
17
+ export declare const iconMd = "grid size-4 shrink-0 place-items-center *:size-4";
18
+ /** 20px — headers, stats, anything sitting beside a heading. */
19
+ export declare const iconLg = "grid size-5 shrink-0 place-items-center *:size-5";
20
+ /** 24px — dialog and empty-state marks. */
21
+ export declare const iconXl = "grid size-6 shrink-0 place-items-center *:size-6";
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The box a consumer-supplied icon renders into.
3
+ *
4
+ * Every icon in the library is a `Snippet` the app writes — the package ships
5
+ * no icon set and no runtime dependency — so the component has to size the
6
+ * glyph rather than trust it. The `*:` variant hits the snippet's own element,
7
+ * which is what keeps a bare `<svg viewBox="0 0 16 16">` from laying out at its
8
+ * intrinsic 300×150.
9
+ *
10
+ * Kept per style rather than in `core/`: an app only `@source`s the style
11
+ * folder it installed, so a class string living outside it would never be
12
+ * generated by Tailwind.
13
+ */
14
+ /** 14px — menu rows, chips, anything sitting on one line of small text. */
15
+ export const iconSm = 'grid size-3.5 shrink-0 place-items-center *:size-3.5';
16
+ /** 16px — the default: nav items, tabs, buttons, list rows. */
17
+ export const iconMd = 'grid size-4 shrink-0 place-items-center *:size-4';
18
+ /** 20px — headers, stats, anything sitting beside a heading. */
19
+ export const iconLg = 'grid size-5 shrink-0 place-items-center *:size-5';
20
+ /** 24px — dialog and empty-state marks. */
21
+ export const iconXl = 'grid size-6 shrink-0 place-items-center *:size-6';
@@ -22,8 +22,6 @@ export { default as Switch } from './Switch.svelte';
22
22
  export type { SwitchSize } from './Switch.svelte';
23
23
  export { default as Field } from './Field.svelte';
24
24
  export { default as Label } from './Label.svelte';
25
- export { default as PasswordInput } from './PasswordInput.svelte';
26
- export { default as NumberInput } from './NumberInput.svelte';
27
25
  export { default as Slider } from './Slider.svelte';
28
26
  export { default as RadioGroup } from './RadioGroup.svelte';
29
27
  export type { RadioOption } from './RadioGroup.svelte';
@@ -32,6 +30,14 @@ export type { CheckboxOption } from './CheckboxGroup.svelte';
32
30
  export { default as SegmentedControl } from './SegmentedControl.svelte';
33
31
  export type { SegmentOption, SegmentedSize } from './SegmentedControl.svelte';
34
32
  export { default as InputGroup } from './InputGroup.svelte';
33
+ export { default as InputAddon } from './InputAddon.svelte';
34
+ export { default as SearchInput } from './SearchInput.svelte';
35
+ export { default as PinInput } from './PinInput.svelte';
36
+ export { default as TagsInput } from './TagsInput.svelte';
37
+ export { default as CurrencyInput } from './CurrencyInput.svelte';
38
+ export { default as TimeInput } from './TimeInput.svelte';
39
+ export { default as ColorInput } from './ColorInput.svelte';
40
+ export { default as FileInput } from './FileInput.svelte';
35
41
  export { default as Combobox } from './Combobox.svelte';
36
42
  export type { ComboboxOption } from './Combobox.svelte';
37
43
  export { default as MultiSelect } from './MultiSelect.svelte';
@@ -21,13 +21,19 @@ export { default as Radio } from './Radio.svelte';
21
21
  export { default as Switch } from './Switch.svelte';
22
22
  export { default as Field } from './Field.svelte';
23
23
  export { default as Label } from './Label.svelte';
24
- export { default as PasswordInput } from './PasswordInput.svelte';
25
- export { default as NumberInput } from './NumberInput.svelte';
26
24
  export { default as Slider } from './Slider.svelte';
27
25
  export { default as RadioGroup } from './RadioGroup.svelte';
28
26
  export { default as CheckboxGroup } from './CheckboxGroup.svelte';
29
27
  export { default as SegmentedControl } from './SegmentedControl.svelte';
30
28
  export { default as InputGroup } from './InputGroup.svelte';
29
+ export { default as InputAddon } from './InputAddon.svelte';
30
+ export { default as SearchInput } from './SearchInput.svelte';
31
+ export { default as PinInput } from './PinInput.svelte';
32
+ export { default as TagsInput } from './TagsInput.svelte';
33
+ export { default as CurrencyInput } from './CurrencyInput.svelte';
34
+ export { default as TimeInput } from './TimeInput.svelte';
35
+ export { default as ColorInput } from './ColorInput.svelte';
36
+ export { default as FileInput } from './FileInput.svelte';
31
37
  export { default as Combobox } from './Combobox.svelte';
32
38
  export { default as MultiSelect } from './MultiSelect.svelte';
33
39
  export { default as DatePicker } from './DatePicker.svelte';
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { toneText, type Tone } from '../../core/tones.js';
5
+ import { iconMd } from './icon.js';
5
6
 
6
7
  interface Props extends HTMLAttributes<HTMLDetailsElement> {
7
8
  title: string;
@@ -9,6 +10,8 @@
9
10
  tone?: Tone;
10
11
  /** Short text on the right of the summary row. */
11
12
  meta?: string;
13
+ /** Leading icon on the summary row. */
14
+ icon?: Snippet;
12
15
  children: Snippet;
13
16
  }
14
17
 
@@ -17,6 +20,7 @@
17
20
  open = $bindable(false),
18
21
  tone = 'brand',
19
22
  meta,
23
+ icon,
20
24
  class: className = '',
21
25
  children,
22
26
  ...rest
@@ -27,6 +31,7 @@
27
31
  <summary
28
32
  class="flex cursor-pointer list-none items-center gap-4 px-4 py-3.5 font-sans text-sm font-medium text-text transition-colors duration-150 ease-brand-out select-none hover:bg-bg-alt focus-visible:outline-2 focus-visible:outline-offset-[-2px] focus-visible:outline-current [&::-webkit-details-marker]:hidden"
29
33
  >
34
+ {#if icon}<span class={iconMd}>{@render icon()}</span>{/if}
30
35
  <span class="flex-1">{title}</span>
31
36
  {#if meta}<span class="text-[13px] font-normal text-text-muted">{meta}</span>{/if}
32
37
  <svg
@@ -7,6 +7,8 @@ interface Props extends HTMLAttributes<HTMLDetailsElement> {
7
7
  tone?: Tone;
8
8
  /** Short text on the right of the summary row. */
9
9
  meta?: string;
10
+ /** Leading icon on the summary row. */
11
+ icon?: Snippet;
10
12
  children: Snippet;
11
13
  }
12
14
  declare const AccordionItem: import("svelte").Component<Props, {}, "open">;
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { HTMLAttributes } from 'svelte/elements';
3
4
  import { toneSoft, type Tone } from '../../core/tones.js';
4
5
 
@@ -13,6 +14,8 @@
13
14
  tone?: Tone;
14
15
  /** Rounded square instead of the default circle. */
15
16
  squared?: boolean;
17
+ /** Drawn instead of the initials when there is no `src` — a user mark. */
18
+ fallback?: Snippet;
16
19
  }
17
20
 
18
21
  let {
@@ -22,6 +25,7 @@
22
25
  size = 'md',
23
26
  tone = 'brand',
24
27
  squared = false,
28
+ fallback,
25
29
  class: className = '',
26
30
  ...rest
27
31
  }: Props = $props();
@@ -34,6 +38,15 @@
34
38
  xl: 'size-16 text-lg'
35
39
  };
36
40
 
41
+ // The mark tracks the initials: about half the circle, whatever the size.
42
+ const glyphs: Record<AvatarSize, string> = {
43
+ xs: 'grid place-items-center *:size-3',
44
+ sm: 'grid place-items-center *:size-4',
45
+ md: 'grid place-items-center *:size-5',
46
+ lg: 'grid place-items-center *:size-6',
47
+ xl: 'grid place-items-center *:size-8'
48
+ };
49
+
37
50
  const initials = $derived(
38
51
  name
39
52
  .trim()
@@ -54,6 +67,8 @@
54
67
  >
55
68
  {#if src}
56
69
  <img {src} alt={alt ?? name} class="size-full object-cover" />
70
+ {:else if fallback}
71
+ <span class={glyphs[size]}>{@render fallback()}</span>
57
72
  {:else}
58
73
  {initials}
59
74
  {/if}
@@ -1,3 +1,4 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { HTMLAttributes } from 'svelte/elements';
2
3
  import { type Tone } from '../../core/tones.js';
3
4
  export type AvatarSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
@@ -10,6 +11,8 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
10
11
  tone?: Tone;
11
12
  /** Rounded square instead of the default circle. */
12
13
  squared?: boolean;
14
+ /** Drawn instead of the initials when there is no `src` — a user mark. */
15
+ fallback?: Snippet;
13
16
  }
14
17
  declare const Avatar: import("svelte").Component<Props, {}, "">;
15
18
  type Avatar = ReturnType<typeof Avatar>;
@@ -9,6 +9,7 @@
9
9
  toneText,
10
10
  type Tone
11
11
  } from '../../core/tones.js';
12
+ import { iconSm } from './icon.js';
12
13
 
13
14
  export type BadgeVariant = 'soft' | 'solid' | 'outline';
14
15
  export type BadgeSize = 'sm' | 'md';
@@ -19,6 +20,8 @@
19
20
  size?: BadgeSize;
20
21
  /** Small filled circle before the label. */
21
22
  dot?: boolean;
23
+ /** Leading icon, 14px — sits where the dot would. */
24
+ icon?: Snippet;
22
25
  children: Snippet;
23
26
  }
24
27
 
@@ -27,6 +30,7 @@
27
30
  variant = 'soft',
28
31
  size = 'md',
29
32
  dot = false,
33
+ icon,
30
34
  class: className = '',
31
35
  children,
32
36
  ...rest
@@ -54,5 +58,6 @@
54
58
  class="size-1.5 shrink-0 rounded-full {variant === 'solid' ? 'bg-current' : toneFill[tone]}"
55
59
  ></span>
56
60
  {/if}
61
+ {#if icon}<span class={iconSm}>{@render icon()}</span>{/if}
57
62
  {@render children()}
58
63
  </span>
@@ -9,6 +9,8 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
9
9
  size?: BadgeSize;
10
10
  /** Small filled circle before the label. */
11
11
  dot?: boolean;
12
+ /** Leading icon, 14px — sits where the dot would. */
13
+ icon?: Snippet;
12
14
  children: Snippet;
13
15
  }
14
16
  declare const Badge: import("svelte").Component<Props, {}, "">;
@@ -1,11 +1,15 @@
1
1
  <script lang="ts">
2
+ import type { Snippet } from 'svelte';
2
3
  import type { HTMLAttributes } from 'svelte/elements';
3
4
  import { toneHoverText, type Tone } from '../../core/tones.js';
4
5
  import { useLocale } from '../../core/locale.svelte.js';
6
+ import { iconMd } from './icon.js';
5
7
 
6
8
  export interface BreadcrumbItem {
7
9
  label: string;
8
10
  href?: string;
11
+ /** Leading icon — a home mark on the first crumb, say. */
12
+ icon?: Snippet;
9
13
  }
10
14
 
11
15
  interface Props extends HTMLAttributes<HTMLElement> {
@@ -29,18 +33,18 @@
29
33
  {#if item.href && i < items.length - 1}
30
34
  <a
31
35
  href={item.href}
32
- class="rounded px-1 py-0.5 text-text-muted transition-colors duration-150 ease-brand-out hover:bg-bg-inset {toneHoverText[
36
+ class="inline-flex items-center gap-1.5 rounded px-1 py-0.5 text-text-muted transition-colors duration-150 ease-brand-out hover:bg-bg-inset {toneHoverText[
33
37
  tone
34
38
  ]}"
35
39
  >
36
- {item.label}
40
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}{item.label}
37
41
  </a>
38
42
  {:else}
39
43
  <span
40
- class="px-1 py-0.5 font-medium text-text"
44
+ class="inline-flex items-center gap-1.5 px-1 py-0.5 font-medium text-text"
41
45
  aria-current={i === items.length - 1 ? 'page' : undefined}
42
46
  >
43
- {item.label}
47
+ {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}{item.label}
44
48
  </span>
45
49
  {/if}
46
50
  </li>
@@ -1,8 +1,11 @@
1
+ import type { Snippet } from 'svelte';
1
2
  import type { HTMLAttributes } from 'svelte/elements';
2
3
  import { type Tone } from '../../core/tones.js';
3
4
  export interface BreadcrumbItem {
4
5
  label: string;
5
6
  href?: string;
7
+ /** Leading icon — a home mark on the first crumb, say. */
8
+ icon?: Snippet;
6
9
  }
7
10
  interface Props extends HTMLAttributes<HTMLElement> {
8
11
  items: BreadcrumbItem[];
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLButtonAttributes } from 'svelte/elements';
4
4
  import Spinner from './Spinner.svelte';
5
+ import { iconMd, iconSm } from './icon.js';
5
6
  import {
6
7
  focusRing,
7
8
  toneBorderSoft,
@@ -18,7 +19,7 @@
18
19
  export type ButtonVariant = 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'primary';
19
20
  export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
20
21
 
21
- interface Props extends HTMLButtonAttributes {
22
+ interface Props extends Omit<HTMLButtonAttributes, 'onclick'> {
22
23
  variant?: ButtonVariant;
23
24
  tone?: Tone;
24
25
  size?: ButtonSize;
@@ -29,7 +30,19 @@
29
30
  loading?: boolean;
30
31
  /** Stretch to the container's full width. */
31
32
  block?: boolean;
32
- children: Snippet;
33
+ /** Leading icon. The spinner takes its place while the button is busy. */
34
+ icon?: Snippet;
35
+ /** Trailing icon. */
36
+ iconEnd?: Snippet;
37
+ /** Square button with no label — pass an `aria-label`. */
38
+ iconOnly?: boolean;
39
+ /**
40
+ * Return a promise — a save, a submit, a fetch — and the button spins and
41
+ * locks itself until it settles, without any state in the parent. It
42
+ * unlocks on a rejection too, so catch failures inside the handler.
43
+ */
44
+ onclick?: (event: MouseEvent) => unknown;
45
+ children?: Snippet;
33
46
  }
34
47
 
35
48
  let {
@@ -39,6 +52,10 @@
39
52
  href,
40
53
  loading = false,
41
54
  block = false,
55
+ icon,
56
+ iconEnd,
57
+ iconOnly = false,
58
+ onclick,
42
59
  disabled = false,
43
60
  type = 'button',
44
61
  class: className = '',
@@ -62,6 +79,13 @@
62
79
  xl: 'h-14 px-8 text-base'
63
80
  };
64
81
 
82
+ const squares: Record<ButtonSize, string> = {
83
+ sm: 'size-9',
84
+ md: 'size-11',
85
+ lg: 'size-12',
86
+ xl: 'size-14'
87
+ };
88
+
65
89
  const variantClass = $derived.by(() => {
66
90
  switch (variant) {
67
91
  case 'primary':
@@ -83,7 +107,24 @@
83
107
  });
84
108
 
85
109
  const isLink = $derived(variant === 'link');
86
- const inert = $derived(disabled || loading);
110
+
111
+ /** Set by an `onclick` that returned a promise, on top of the `loading` prop. */
112
+ let pending = $state(false);
113
+ const busy = $derived(loading || pending);
114
+ const inert = $derived(disabled || busy);
115
+
116
+ const iconClass = $derived(size === 'sm' ? iconSm : iconMd);
117
+
118
+ async function handleClick(event: MouseEvent) {
119
+ const result = onclick?.(event);
120
+ // Anything else is a plain handler: nothing to wait for, nothing to lock.
121
+ if (!(result instanceof Promise)) return;
122
+ pending = true;
123
+ // The handler owns its own errors — catch them there to show a message.
124
+ // All the button needs to know is that the work has settled.
125
+ await result.catch(() => {});
126
+ pending = false;
127
+ }
87
128
  </script>
88
129
 
89
130
  <svelte:element
@@ -92,14 +133,24 @@
92
133
  type={href ? undefined : type}
93
134
  disabled={href ? undefined : inert}
94
135
  aria-disabled={href && inert ? 'true' : undefined}
95
- aria-busy={loading ? 'true' : undefined}
136
+ aria-busy={busy ? 'true' : undefined}
96
137
  role={href ? 'button' : undefined}
97
- class="{base} {focusRing} {toneRing[tone]} {variantClass} {isLink ? '' : sizes[size]}
138
+ class="{base} {focusRing} {toneRing[tone]} {variantClass} {isLink
139
+ ? ''
140
+ : iconOnly
141
+ ? squares[size]
142
+ : sizes[size]}
98
143
  {block ? 'w-full' : ''} {className}"
99
144
  {...rest}
145
+ onclick={handleClick}
100
146
  >
101
- {#if loading}
147
+ {#if busy}
102
148
  <Spinner size={size === 'sm' ? 'xs' : 'sm'} label="" />
149
+ {:else if icon}
150
+ <span class={iconClass} aria-hidden="true">{@render icon()}</span>
151
+ {/if}
152
+ {@render children?.()}
153
+ {#if iconEnd && !iconOnly}
154
+ <span class={iconClass} aria-hidden="true">{@render iconEnd()}</span>
103
155
  {/if}
104
- {@render children()}
105
156
  </svelte:element>
@@ -3,7 +3,7 @@ import type { HTMLButtonAttributes } from 'svelte/elements';
3
3
  import { type Tone } from '../../core/tones.js';
4
4
  export type ButtonVariant = 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'primary';
5
5
  export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
6
- interface Props extends HTMLButtonAttributes {
6
+ interface Props extends Omit<HTMLButtonAttributes, 'onclick'> {
7
7
  variant?: ButtonVariant;
8
8
  tone?: Tone;
9
9
  size?: ButtonSize;
@@ -14,7 +14,19 @@ interface Props extends HTMLButtonAttributes {
14
14
  loading?: boolean;
15
15
  /** Stretch to the container's full width. */
16
16
  block?: boolean;
17
- children: Snippet;
17
+ /** Leading icon. The spinner takes its place while the button is busy. */
18
+ icon?: Snippet;
19
+ /** Trailing icon. */
20
+ iconEnd?: Snippet;
21
+ /** Square button with no label — pass an `aria-label`. */
22
+ iconOnly?: boolean;
23
+ /**
24
+ * Return a promise — a save, a submit, a fetch — and the button spins and
25
+ * locks itself until it settles, without any state in the parent. It
26
+ * unlocks on a rejection too, so catch failures inside the handler.
27
+ */
28
+ onclick?: (event: MouseEvent) => unknown;
29
+ children?: Snippet;
18
30
  }
19
31
  declare const Button: import("svelte").Component<Props, {}, "">;
20
32
  type Button = ReturnType<typeof Button>;
@@ -2,6 +2,7 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { toneBorderLeft, toneHoverBorder, toneSurface, type Tone } from '../../core/tones.js';
5
+ import { iconLg } from './icon.js';
5
6
  import { soft, softOnHover } from './lift.js';
6
7
 
7
8
  export type CardVariant = 'outline' | 'filled' | 'tinted';
@@ -16,6 +17,8 @@
16
17
  padded?: boolean;
17
18
  title?: string;
18
19
  eyebrow?: string;
20
+ /** Leading icon above the title. Ignored when you pass `header`. */
21
+ icon?: Snippet;
19
22
  header?: Snippet;
20
23
  footer?: Snippet;
21
24
  children: Snippet;
@@ -29,6 +32,7 @@
29
32
  padded = true,
30
33
  title,
31
34
  eyebrow,
35
+ icon,
32
36
  header,
33
37
  footer,
34
38
  class: className = '',
@@ -55,11 +59,12 @@
55
59
  {className}"
56
60
  {...rest}
57
61
  >
58
- {#if header || title || eyebrow}
62
+ {#if header || title || eyebrow || icon}
59
63
  <div class="flex flex-col gap-1 {padded ? 'px-6 pt-6' : ''}">
60
64
  {#if header}
61
65
  {@render header()}
62
66
  {:else}
67
+ {#if icon}<span class={iconLg}>{@render icon()}</span>{/if}
63
68
  {#if eyebrow}
64
69
  <span class="font-sans text-xs font-semibold text-text-muted">{eyebrow}</span>
65
70
  {/if}