@nqmcreative/ui 0.4.0 → 0.4.2

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 (203) hide show
  1. package/README.md +17 -19
  2. package/dist/core/calendar.d.ts +1 -1
  3. package/dist/core/calendar.js +1 -1
  4. package/dist/core/date.d.ts +3 -3
  5. package/dist/core/date.js +1 -1
  6. package/dist/core/files.d.ts +2 -3
  7. package/dist/core/files.js +2 -3
  8. package/dist/core/index.d.ts +2 -4
  9. package/dist/core/index.js +2 -4
  10. package/dist/core/password.d.ts +1 -1
  11. package/dist/core/password.js +1 -1
  12. package/dist/core/tones.d.ts +10 -0
  13. package/dist/core/tones.js +19 -0
  14. package/dist/core/trigger.d.ts +8 -0
  15. package/dist/core/trigger.js +11 -0
  16. package/dist/styles/matte/Alert.svelte +2 -4
  17. package/dist/styles/matte/Avatar.svelte +16 -3
  18. package/dist/styles/matte/Breadcrumb.svelte +1 -4
  19. package/dist/styles/matte/Button.svelte +6 -2
  20. package/dist/styles/matte/Calendar.svelte +6 -9
  21. package/dist/styles/matte/Checkbox.svelte +49 -30
  22. package/dist/styles/matte/CheckboxGroup.svelte +15 -4
  23. package/dist/styles/matte/ColorInput.svelte +1 -4
  24. package/dist/styles/matte/Combobox.svelte +14 -8
  25. package/dist/styles/matte/CommandPalette.svelte +3 -6
  26. package/dist/styles/matte/ConfirmDialog.svelte +11 -6
  27. package/dist/styles/matte/ContextMenu.svelte +1 -4
  28. package/dist/styles/matte/CurrencyInput.svelte +1 -1
  29. package/dist/styles/matte/CurrencyInput.svelte.d.ts +1 -1
  30. package/dist/styles/matte/DatePicker.svelte +18 -11
  31. package/dist/styles/matte/DatePicker.svelte.d.ts +3 -0
  32. package/dist/styles/matte/Divider.svelte +1 -1
  33. package/dist/styles/matte/Drawer.svelte +11 -5
  34. package/dist/styles/matte/Dropdown.svelte +2 -5
  35. package/dist/styles/matte/Dropdown.svelte.d.ts +1 -1
  36. package/dist/styles/matte/Dropzone.svelte +9 -12
  37. package/dist/styles/matte/Dropzone.svelte.d.ts +1 -1
  38. package/dist/styles/matte/Field.svelte +59 -3
  39. package/dist/styles/matte/FileInput.svelte +11 -14
  40. package/dist/styles/matte/FileInput.svelte.d.ts +2 -2
  41. package/dist/styles/matte/Footer.svelte +15 -4
  42. package/dist/styles/matte/Footer.svelte.d.ts +1 -1
  43. package/dist/styles/matte/Input.svelte +9 -2
  44. package/dist/styles/matte/Label.svelte +3 -1
  45. package/dist/styles/matte/Link.svelte +13 -2
  46. package/dist/styles/matte/MenuItem.svelte +11 -4
  47. package/dist/styles/matte/Modal.svelte +11 -5
  48. package/dist/styles/matte/MultiSelect.svelte +5 -8
  49. package/dist/styles/matte/Navbar.svelte +5 -11
  50. package/dist/styles/matte/Pagination.svelte +3 -6
  51. package/dist/styles/matte/PinInput.svelte +1 -4
  52. package/dist/styles/matte/Radio.svelte +41 -22
  53. package/dist/styles/matte/RadioGroup.svelte +15 -4
  54. package/dist/styles/matte/SearchInput.svelte +1 -4
  55. package/dist/styles/matte/Select.svelte +4 -2
  56. package/dist/styles/matte/Sidebar.svelte +3 -6
  57. package/dist/styles/matte/Skeleton.svelte +2 -1
  58. package/dist/styles/matte/Spinner.svelte +2 -5
  59. package/dist/styles/matte/Spinner.svelte.d.ts +1 -1
  60. package/dist/styles/matte/Stat.svelte +27 -3
  61. package/dist/styles/matte/Switch.svelte +6 -1
  62. package/dist/styles/matte/Table.svelte +5 -8
  63. package/dist/styles/matte/Tabs.svelte +1 -1
  64. package/dist/styles/matte/TagsInput.svelte +5 -8
  65. package/dist/styles/matte/Textarea.svelte +4 -2
  66. package/dist/styles/matte/ThemeToggle.svelte +5 -8
  67. package/dist/styles/matte/TimeInput.svelte +2 -5
  68. package/dist/styles/matte/Toaster.svelte +23 -8
  69. package/dist/styles/matte/Tooltip.svelte +8 -1
  70. package/dist/styles/matte/index.d.ts +2 -3
  71. package/dist/styles/matte/index.js +1 -3
  72. package/dist/styles/matte/theme.css +21 -0
  73. package/dist/styles/paper/Alert.svelte +3 -5
  74. package/dist/styles/paper/Avatar.svelte +14 -3
  75. package/dist/styles/paper/Breadcrumb.svelte +1 -4
  76. package/dist/styles/paper/Button.svelte +5 -1
  77. package/dist/styles/paper/Calendar.svelte +6 -9
  78. package/dist/styles/paper/Card.svelte +1 -1
  79. package/dist/styles/paper/Checkbox.svelte +55 -36
  80. package/dist/styles/paper/CheckboxGroup.svelte +15 -4
  81. package/dist/styles/paper/ColorInput.svelte +1 -4
  82. package/dist/styles/paper/Combobox.svelte +14 -8
  83. package/dist/styles/paper/CommandPalette.svelte +3 -6
  84. package/dist/styles/paper/ConfirmDialog.svelte +9 -6
  85. package/dist/styles/paper/ContextMenu.svelte +1 -4
  86. package/dist/styles/paper/CurrencyInput.svelte +1 -1
  87. package/dist/styles/paper/CurrencyInput.svelte.d.ts +1 -1
  88. package/dist/styles/paper/DatePicker.svelte +18 -11
  89. package/dist/styles/paper/DatePicker.svelte.d.ts +3 -0
  90. package/dist/styles/paper/Divider.svelte +1 -1
  91. package/dist/styles/paper/Drawer.svelte +9 -5
  92. package/dist/styles/paper/Dropdown.svelte +2 -5
  93. package/dist/styles/paper/Dropdown.svelte.d.ts +1 -1
  94. package/dist/styles/paper/Dropzone.svelte +9 -12
  95. package/dist/styles/paper/Dropzone.svelte.d.ts +1 -1
  96. package/dist/styles/paper/Field.svelte +59 -3
  97. package/dist/styles/paper/FileInput.svelte +11 -14
  98. package/dist/styles/paper/FileInput.svelte.d.ts +2 -2
  99. package/dist/styles/paper/Footer.svelte +15 -4
  100. package/dist/styles/paper/Footer.svelte.d.ts +1 -1
  101. package/dist/styles/paper/Input.svelte +9 -2
  102. package/dist/styles/paper/Label.svelte +3 -1
  103. package/dist/styles/paper/Link.svelte +13 -2
  104. package/dist/styles/paper/MenuItem.svelte +11 -4
  105. package/dist/styles/paper/Modal.svelte +9 -5
  106. package/dist/styles/paper/MultiSelect.svelte +5 -8
  107. package/dist/styles/paper/Navbar.svelte +5 -11
  108. package/dist/styles/paper/Pagination.svelte +3 -6
  109. package/dist/styles/paper/PinInput.svelte +1 -4
  110. package/dist/styles/paper/Radio.svelte +41 -22
  111. package/dist/styles/paper/RadioGroup.svelte +15 -4
  112. package/dist/styles/paper/SearchInput.svelte +1 -4
  113. package/dist/styles/paper/SegmentedControl.svelte +2 -2
  114. package/dist/styles/paper/Select.svelte +4 -2
  115. package/dist/styles/paper/Sidebar.svelte +3 -6
  116. package/dist/styles/paper/Skeleton.svelte +2 -1
  117. package/dist/styles/paper/Spinner.svelte +2 -5
  118. package/dist/styles/paper/Spinner.svelte.d.ts +1 -1
  119. package/dist/styles/paper/Stat.svelte +24 -2
  120. package/dist/styles/paper/Switch.svelte +6 -1
  121. package/dist/styles/paper/Table.svelte +5 -8
  122. package/dist/styles/paper/Tabs.svelte +1 -1
  123. package/dist/styles/paper/TagsInput.svelte +5 -8
  124. package/dist/styles/paper/Textarea.svelte +4 -2
  125. package/dist/styles/paper/ThemeToggle.svelte +5 -8
  126. package/dist/styles/paper/TimeInput.svelte +2 -5
  127. package/dist/styles/paper/Toaster.svelte +22 -9
  128. package/dist/styles/paper/Tooltip.svelte +8 -1
  129. package/dist/styles/paper/index.d.ts +2 -3
  130. package/dist/styles/paper/index.js +1 -3
  131. package/dist/styles/paper/theme.css +21 -0
  132. package/dist/styles/sprout/Alert.svelte +4 -6
  133. package/dist/styles/sprout/Avatar.svelte +14 -3
  134. package/dist/styles/sprout/Breadcrumb.svelte +2 -5
  135. package/dist/styles/sprout/Button.svelte +8 -4
  136. package/dist/styles/sprout/Calendar.svelte +7 -10
  137. package/dist/styles/sprout/Card.svelte +2 -2
  138. package/dist/styles/sprout/Checkbox.svelte +55 -36
  139. package/dist/styles/sprout/CheckboxGroup.svelte +15 -4
  140. package/dist/styles/sprout/ColorInput.svelte +2 -5
  141. package/dist/styles/sprout/Combobox.svelte +15 -9
  142. package/dist/styles/sprout/CommandPalette.svelte +3 -6
  143. package/dist/styles/sprout/ConfirmDialog.svelte +9 -6
  144. package/dist/styles/sprout/ContextMenu.svelte +1 -4
  145. package/dist/styles/sprout/CurrencyInput.svelte +1 -1
  146. package/dist/styles/sprout/CurrencyInput.svelte.d.ts +1 -1
  147. package/dist/styles/sprout/DatePicker.svelte +20 -13
  148. package/dist/styles/sprout/DatePicker.svelte.d.ts +3 -0
  149. package/dist/styles/sprout/Divider.svelte +1 -1
  150. package/dist/styles/sprout/Drawer.svelte +9 -5
  151. package/dist/styles/sprout/Dropdown.svelte +2 -5
  152. package/dist/styles/sprout/Dropdown.svelte.d.ts +1 -1
  153. package/dist/styles/sprout/Dropzone.svelte +10 -13
  154. package/dist/styles/sprout/Dropzone.svelte.d.ts +1 -1
  155. package/dist/styles/sprout/Field.svelte +59 -3
  156. package/dist/styles/sprout/FileInput.svelte +14 -17
  157. package/dist/styles/sprout/FileInput.svelte.d.ts +2 -2
  158. package/dist/styles/sprout/Footer.svelte +15 -4
  159. package/dist/styles/sprout/Footer.svelte.d.ts +1 -1
  160. package/dist/styles/sprout/Input.svelte +9 -2
  161. package/dist/styles/sprout/Label.svelte +3 -1
  162. package/dist/styles/sprout/Link.svelte +13 -2
  163. package/dist/styles/sprout/LogoCloud.svelte +1 -1
  164. package/dist/styles/sprout/MenuItem.svelte +11 -4
  165. package/dist/styles/sprout/Modal.svelte +9 -5
  166. package/dist/styles/sprout/MultiSelect.svelte +8 -11
  167. package/dist/styles/sprout/Navbar.svelte +5 -11
  168. package/dist/styles/sprout/Pagination.svelte +3 -6
  169. package/dist/styles/sprout/PinInput.svelte +1 -4
  170. package/dist/styles/sprout/Radio.svelte +41 -22
  171. package/dist/styles/sprout/RadioGroup.svelte +15 -4
  172. package/dist/styles/sprout/SearchInput.svelte +2 -5
  173. package/dist/styles/sprout/SegmentedControl.svelte +2 -2
  174. package/dist/styles/sprout/Select.svelte +4 -2
  175. package/dist/styles/sprout/Sidebar.svelte +5 -7
  176. package/dist/styles/sprout/Skeleton.svelte +2 -1
  177. package/dist/styles/sprout/Spinner.svelte +2 -5
  178. package/dist/styles/sprout/Spinner.svelte.d.ts +1 -1
  179. package/dist/styles/sprout/Stat.svelte +24 -2
  180. package/dist/styles/sprout/Switch.svelte +6 -1
  181. package/dist/styles/sprout/Table.svelte +6 -9
  182. package/dist/styles/sprout/Tabs.svelte +1 -1
  183. package/dist/styles/sprout/TagsInput.svelte +6 -9
  184. package/dist/styles/sprout/Textarea.svelte +4 -2
  185. package/dist/styles/sprout/ThemeToggle.svelte +5 -8
  186. package/dist/styles/sprout/TimeInput.svelte +3 -6
  187. package/dist/styles/sprout/Toaster.svelte +25 -12
  188. package/dist/styles/sprout/Tooltip.svelte +8 -1
  189. package/dist/styles/sprout/index.d.ts +3 -4
  190. package/dist/styles/sprout/index.js +2 -4
  191. package/dist/styles/sprout/lift.d.ts +14 -0
  192. package/dist/styles/sprout/lift.js +23 -0
  193. package/dist/styles/sprout/theme.css +50 -24
  194. package/package.json +2 -19
  195. package/registry.json +7 -46
  196. package/dist/core/locale.svelte.d.ts +0 -101
  197. package/dist/core/locale.svelte.js +0 -167
  198. package/dist/styles/matte/LocaleProvider.svelte +0 -24
  199. package/dist/styles/matte/LocaleProvider.svelte.d.ts +0 -10
  200. package/dist/styles/paper/LocaleProvider.svelte +0 -24
  201. package/dist/styles/paper/LocaleProvider.svelte.d.ts +0 -10
  202. package/dist/styles/sprout/LocaleProvider.svelte +0 -24
  203. package/dist/styles/sprout/LocaleProvider.svelte.d.ts +0 -10
@@ -2,7 +2,6 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import { iconLg } from './icon.js';
4
4
  import type { HTMLDialogAttributes } from 'svelte/elements';
5
- import { useLocale } from '../../core/locale.svelte.js';
6
5
  import { isBackdropClick, syncDialog } from '../../core/dialog.svelte.js';
7
6
  import { float } from './lift.js';
8
7
 
@@ -41,7 +40,8 @@
41
40
  ...rest
42
41
  }: Props = $props();
43
42
 
44
- const t = useLocale();
43
+ const uid = $props.id();
44
+
45
45
  let dialog: HTMLDialogElement | null = $state(null);
46
46
 
47
47
  const closable = $derived(showClose ?? dismissible);
@@ -67,6 +67,8 @@
67
67
 
68
68
  <dialog
69
69
  bind:this={dialog}
70
+ aria-labelledby={title ? `${uid}-title` : undefined}
71
+ aria-describedby={description ? `${uid}-description` : undefined}
70
72
  onclose={close}
71
73
  onclick={onBackdrop}
72
74
  oncancel={(event) => {
@@ -83,17 +85,19 @@
83
85
  {#if icon}<span class={iconLg}>{@render icon()}</span>{/if}
84
86
  <div class="flex min-w-0 flex-1 flex-col gap-1">
85
87
  {#if title}
86
- <h2 class="font-heading text-base font-semibold text-text">{title}</h2>
88
+ <h2 id="{uid}-title" class="font-heading text-base font-semibold text-text">{title}</h2>
87
89
  {/if}
88
90
  {#if description}
89
- <p class="font-sans text-[13px] text-text-muted">{description}</p>
91
+ <p id="{uid}-description" class="font-sans text-[13px] text-text-muted">
92
+ {description}
93
+ </p>
90
94
  {/if}
91
95
  </div>
92
96
  {#if closable}
93
97
  <button
94
98
  type="button"
95
99
  onclick={close}
96
- aria-label={t.current.close}
100
+ aria-label="Close"
97
101
  class="-m-1 shrink-0 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
98
102
  >
99
103
  <svg class="size-4" viewBox="0 0 14 14" fill="none" aria-hidden="true">
@@ -16,7 +16,6 @@
16
16
  import { anchored } from '../../core/actions/anchor.js';
17
17
  import { clickOutside, portal } from '../../core/actions/dismissable.js';
18
18
  import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
19
- import { useLocale } from '../../core/locale.svelte.js';
20
19
  import { iconMd, iconSm } from './icon.js';
21
20
  import { toneFocusWithinBorder, toneSoft, toneText, type Tone } from '../../core/tones.js';
22
21
  import { edge, soft } from './lift.js';
@@ -59,8 +58,6 @@
59
58
  class: className = ''
60
59
  }: Props = $props();
61
60
 
62
- const t = useLocale();
63
-
64
61
  let open = $state(false);
65
62
  let query = $state('');
66
63
  let wrapper: HTMLElement | null = $state(null);
@@ -150,7 +147,7 @@
150
147
  class="inline-flex items-center rounded-xl px-2 py-0.5 text-xs font-medium {toneSoft[tone]}"
151
148
  >
152
149
  {chosen.length}
153
- {t.current.selected}
150
+ selected
154
151
  </span>
155
152
  {:else}
156
153
  {#each chosen as option (option.value)}
@@ -163,8 +160,8 @@
163
160
  <button
164
161
  type="button"
165
162
  onclick={() => remove(option)}
166
- aria-label="{t.current.removeItem} {option.label}"
167
- class="inline-flex size-4 items-center justify-center rounded transition-colors duration-150 hover:bg-bg/70 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-current"
163
+ aria-label="Remove {option.label}"
164
+ class="inline-flex size-4 items-center justify-center rounded-full transition-colors duration-150 hover:bg-bg/70 focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-current"
168
165
  >
169
166
  <svg class="size-2.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
170
167
  <path
@@ -190,7 +187,7 @@
190
187
  aria-expanded={open}
191
188
  aria-controls="{id ?? 'multiselect'}-listbox"
192
189
  aria-invalid={invalid ? 'true' : undefined}
193
- placeholder={chosen.length ? '' : (placeholder ?? t.current.comboboxPlaceholder)}
190
+ placeholder={chosen.length ? '' : (placeholder ?? 'Search…')}
194
191
  oninput={() => {
195
192
  open = true;
196
193
  cursor.index = 0;
@@ -212,8 +209,8 @@
212
209
  onchange?.([]);
213
210
  inputEl?.focus();
214
211
  }}
215
- aria-label={t.current.clear}
216
- class="shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
212
+ aria-label="Clear selection"
213
+ class="shrink-0 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
217
214
  >
218
215
  <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
219
216
  <path
@@ -240,7 +237,7 @@
240
237
  >
241
238
  {#if flat.length === 0}
242
239
  <p class="px-3 py-6 text-center text-sm text-text-muted">
243
- {emptyText ?? t.current.noMatches}
240
+ {emptyText ?? 'No matches'}
244
241
  </p>
245
242
  {:else}
246
243
  {#each groups as [group, items] (group)}
@@ -264,7 +261,7 @@
264
261
  {index === active ? toneSoft[tone] : 'text-text-secondary'}"
265
262
  >
266
263
  <span
267
- class="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded border transition-colors duration-100
264
+ class="mt-0.5 flex size-4 shrink-0 items-center justify-center rounded-md border transition-colors duration-100
268
265
  {checked ? `border-transparent ${toneText[tone]}` : 'border-hairline-strong'}"
269
266
  >
270
267
  {#if checked}
@@ -16,7 +16,6 @@
16
16
 
17
17
  <script lang="ts">
18
18
  import type { Snippet } from 'svelte';
19
- import { useLocale } from '../../core/locale.svelte.js';
20
19
  import { iconMd } from './icon.js';
21
20
  import { focusRing, toneRing, toneSoft, toneText, type Tone } from '../../core/tones.js';
22
21
  import Drawer from './Drawer.svelte';
@@ -53,8 +52,6 @@
53
52
  class: className = ''
54
53
  }: Props = $props();
55
54
 
56
- const t = useLocale();
57
-
58
55
  // sprout gives the active link a filled pill rather than a colour change, so
59
56
  // the current page is legible without relying on hue alone.
60
57
  const link =
@@ -66,10 +63,7 @@
66
63
  ? 'border-b border-hairline'
67
64
  : ''} {className}"
68
65
  >
69
- <nav
70
- aria-label={t.current.navigation}
71
- class="mx-auto flex h-16 w-full max-w-6xl items-center gap-6 px-6"
72
- >
66
+ <nav aria-label="Navigation" class="mx-auto flex h-16 w-full max-w-6xl items-center gap-6 px-6">
73
67
  {#if brand}
74
68
  <div class="flex shrink-0 items-center">{@render brand()}</div>
75
69
  {/if}
@@ -117,7 +111,7 @@
117
111
  class="{link} {focusRing} {toneRing[tone]} {item.active
118
112
  ? toneSoft[tone]
119
113
  : 'text-text-secondary hover:bg-bg-inset hover:text-text'} {item.disabled
120
- ? 'pointer-events-none opacity-50'
114
+ ? 'pointer-events-none opacity-40'
121
115
  : ''}"
122
116
  >
123
117
  {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
@@ -140,7 +134,7 @@
140
134
  <button
141
135
  type="button"
142
136
  onclick={() => (menuOpen = true)}
143
- aria-label={t.current.openMenu}
137
+ aria-label="Open menu"
144
138
  aria-expanded={menuOpen}
145
139
  class="inline-flex size-9 items-center justify-center rounded-xl text-text-secondary transition-colors duration-150 hover:bg-bg-inset hover:text-text md:hidden {focusRing} {toneRing[
146
140
  tone
@@ -159,7 +153,7 @@
159
153
  </nav>
160
154
  </header>
161
155
 
162
- <Drawer bind:open={menuOpen} side="right" size="sm" title={t.current.navigation}>
156
+ <Drawer bind:open={menuOpen} side="right" size="sm" title="Navigation">
163
157
  <ul class="flex flex-col gap-0.5">
164
158
  {#each items as item (item.label)}
165
159
  <li>
@@ -170,7 +164,7 @@
170
164
  class="flex items-center justify-between gap-3 rounded-xl px-3 py-2.5 font-sans text-sm font-medium {item.active
171
165
  ? `${toneSoft[tone]}`
172
166
  : 'text-text-secondary hover:bg-bg-inset'} {item.disabled
173
- ? 'pointer-events-none opacity-50'
167
+ ? 'pointer-events-none opacity-40'
174
168
  : ''}"
175
169
  >
176
170
  <span class="flex min-w-0 items-center gap-2.5">
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import { focusRing, toneRing, toneSolid, type Tone } from '../../core/tones.js';
4
- import { useLocale } from '../../core/locale.svelte.js';
5
4
  import { pageRange } from '../../core/pagination.js';
6
5
  import { liftSm } from './lift.js';
7
6
 
@@ -26,8 +25,6 @@
26
25
  ...rest
27
26
  }: Props = $props();
28
27
 
29
- const t = useLocale();
30
-
31
28
  const pages = $derived(pageRange(page, total, siblings));
32
29
 
33
30
  function go(next: number) {
@@ -40,12 +37,12 @@
40
37
  const cell = `inline-flex h-9 min-w-9 items-center justify-center rounded-full px-2 font-sans text-sm font-medium tabular-nums transition-colors duration-150 ease-brand-out disabled:opacity-40 disabled:pointer-events-none ${focusRing}`;
41
38
  </script>
42
39
 
43
- <nav aria-label={t.current.pagination} class="flex items-center gap-1 {className}" {...rest}>
40
+ <nav aria-label="Pagination" class="flex items-center gap-1 {className}" {...rest}>
44
41
  <button
45
42
  type="button"
46
43
  class="{cell} {toneRing[tone]} text-text-secondary hover:bg-bg-inset"
47
44
  disabled={page <= 1}
48
- aria-label={t.current.previousPage}
45
+ aria-label="Previous page"
49
46
  onclick={() => go(page - 1)}
50
47
  >
51
48
  <svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
@@ -74,7 +71,7 @@
74
71
  type="button"
75
72
  class="{cell} {toneRing[tone]} text-text-secondary hover:bg-bg-inset"
76
73
  disabled={page >= total}
77
- aria-label={t.current.nextPage}
74
+ aria-label="Next page"
78
75
  onclick={() => go(page + 1)}
79
76
  >
80
77
  <svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import { useLocale } from '../../core/locale.svelte.js';
3
2
  import { caretIndex, clearFrom, fillFrom, sanitisePin, type PinMode } from '../../core/pin.js';
4
3
  import { toneFocusBorder, type Tone } from '../../core/tones.js';
5
4
  import { edge } from './lift.js';
@@ -39,8 +38,6 @@
39
38
  class: className = ''
40
39
  }: Props = $props();
41
40
 
42
- const t = useLocale();
43
-
44
41
  let boxes: HTMLInputElement[] = $state([]);
45
42
 
46
43
  const chars = $derived(Array.from({ length }, (_, i) => value[i] ?? ''));
@@ -120,7 +117,7 @@
120
117
  autocapitalize="characters"
121
118
  spellcheck="false"
122
119
  maxlength={length}
123
- aria-label="{t.current.digit} {index + 1}"
120
+ aria-label="Digit {index + 1}"
124
121
  aria-invalid={invalid ? 'true' : undefined}
125
122
  oninput={(event) => onInput(event, index)}
126
123
  onkeydown={(event) => onKeydown(event, index)}
@@ -25,32 +25,51 @@
25
25
  children,
26
26
  ...rest
27
27
  }: Props = $props();
28
+
29
+ const uid = $props.id();
28
30
  </script>
29
31
 
30
- <label
31
- class="group inline-flex items-start gap-2.5 font-sans text-sm
32
- {disabled ? 'pointer-events-none opacity-50' : 'cursor-pointer'} {className}"
32
+ <div
33
+ class="inline-block font-sans text-sm {disabled
34
+ ? 'pointer-events-none opacity-50'
35
+ : ''} {className}"
33
36
  >
34
- <input type="radio" bind:group {value} {disabled} class="peer sr-only" {...rest} />
35
- <span
36
- class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded-full border border-hairline-strong bg-bg {edge} transition-colors duration-150 ease-brand-out
37
+ <label class="group inline-flex items-start gap-2.5 {disabled ? '' : 'cursor-pointer'}">
38
+ <input
39
+ type="radio"
40
+ bind:group
41
+ {value}
42
+ {disabled}
43
+ aria-describedby={description ? `${uid}-description` : undefined}
44
+ class="peer sr-only"
45
+ {...rest}
46
+ />
47
+ <span
48
+ class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded-full border border-hairline-strong bg-bg {edge} transition-colors duration-150 ease-brand-out
37
49
  {tonePeerChecked[tone]} {peerFocusRing} {tonePeerFocus[tone]}"
38
- aria-hidden="true"
39
- >
50
+ aria-hidden="true"
51
+ >
52
+ <span
53
+ class="size-1.5 rounded-full bg-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
54
+ ></span>
55
+ </span>
56
+ {#if label || children}
57
+ <span class="flex flex-col gap-0.5">
58
+ {#if children}
59
+ {@render children()}
60
+ {:else if label}
61
+ <span class="leading-snug text-text">{label}</span>
62
+ {/if}
63
+ </span>
64
+ {/if}
65
+ </label>
66
+ {#if description}
67
+ <!-- ponytail: the indent tracks the 18px dot plus the label's gap; move one, move both. -->
40
68
  <span
41
- class="size-1.5 rounded-full bg-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
42
- ></span>
43
- </span>
44
- {#if label || description || children}
45
- <span class="flex flex-col gap-0.5">
46
- {#if children}
47
- {@render children()}
48
- {:else if label}
49
- <span class="leading-snug text-text">{label}</span>
50
- {/if}
51
- {#if description}
52
- <span class="text-[13px] leading-snug text-text-muted">{description}</span>
53
- {/if}
69
+ id="{uid}-description"
70
+ class="mt-0.5 block pl-[28px] text-[13px] leading-snug text-text-muted"
71
+ >
72
+ {description}
54
73
  </span>
55
74
  {/if}
56
- </label>
75
+ </div>
@@ -45,13 +45,24 @@
45
45
  children,
46
46
  ...rest
47
47
  }: Props = $props();
48
+
49
+ const uid = $props.id();
50
+ // The group's own hint and error sit outside every control in it, so the
51
+ // fieldset is what has to point at them.
52
+ const messageId = $derived(error ? `${uid}-error` : hint ? `${uid}-hint` : undefined);
48
53
  </script>
49
54
 
50
- <fieldset class="flex flex-col gap-3 border-0 p-0 {className}" {...rest}>
55
+ <fieldset
56
+ aria-describedby={messageId}
57
+ class="flex flex-col gap-3 border-0 p-0 {className}"
58
+ {...rest}
59
+ >
51
60
  {#if legend}
52
61
  <legend class="mb-1 font-sans text-sm font-medium text-text">
53
62
  {legend}
54
- {#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span>{/if}
63
+ {#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span><span
64
+ class="sr-only">(required)</span
65
+ >{/if}
55
66
  </legend>
56
67
  {/if}
57
68
 
@@ -95,8 +106,8 @@
95
106
  </div>
96
107
 
97
108
  {#if error}
98
- <p class="font-sans text-[13px] text-danger">{error}</p>
109
+ <p id="{uid}-error" role="alert" class="font-sans text-[13px] text-danger">{error}</p>
99
110
  {:else if hint}
100
- <p class="font-sans text-[13px] text-text-muted">{hint}</p>
111
+ <p id="{uid}-hint" class="font-sans text-[13px] text-text-muted">{hint}</p>
101
112
  {/if}
102
113
  </fieldset>
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLInputAttributes } from 'svelte/elements';
4
- import { useLocale } from '../../core/locale.svelte.js';
5
4
  import type { Tone } from '../../core/tones.js';
6
5
  import Input, { type InputSize } from './Input.svelte';
7
6
 
@@ -37,8 +36,6 @@
37
36
  ...rest
38
37
  }: Props = $props();
39
38
 
40
- const t = useLocale();
41
-
42
39
  // The timer is cleared by the effect's own teardown, so a keystroke that
43
40
  // lands mid-wait restarts the clock rather than queueing a second call.
44
41
  $effect(() => {
@@ -75,8 +72,8 @@
75
72
  <button
76
73
  type="button"
77
74
  onclick={clear}
78
- aria-label={t.current.clearSearch}
79
- class="-mr-1 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
75
+ aria-label="Clear search"
76
+ class="-mr-1 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
80
77
  >
81
78
  <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
82
79
  <path
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
- import { focusRing, toneRing, type Tone } from '../../core/tones.js';
4
+ import { focusRing, toneRing, toneText, type Tone } from '../../core/tones.js';
5
5
  import { iconMd } from './icon.js';
6
6
  import { edge } from './lift.js';
7
7
 
@@ -93,7 +93,7 @@
93
93
  onclick={() => select(option)}
94
94
  class="inline-flex items-center justify-center rounded-xl font-sans font-medium whitespace-nowrap transition-all duration-150 ease-brand-out disabled:pointer-events-none disabled:opacity-40
95
95
  {iconOnly ? squares[size] : sizes[size]} {focusRing} {toneRing[tone]}
96
- {value === option.value ? `bg-bg text-text ${edge}` : 'text-text-muted hover:text-text'}"
96
+ {value === option.value ? `bg-bg ${toneText[tone]} ${edge}` : 'text-text-muted hover:text-text'}"
97
97
  >
98
98
  {#if option.icon}<span class={iconMd}>{@render option.icon()}</span>{/if}
99
99
  {#if !iconOnly}{option.label}{/if}
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLSelectAttributes } from 'svelte/elements';
4
- import { toneFocusBorder, type Tone } from '../../core/tones.js';
4
+ import { focusRing, toneFocusBorder, toneRing, type Tone } from '../../core/tones.js';
5
5
  import { edge } from './lift.js';
6
6
 
7
7
  export interface SelectOption {
@@ -36,7 +36,9 @@
36
36
  <select
37
37
  bind:value
38
38
  aria-invalid={invalid ? 'true' : undefined}
39
- class="h-10 w-full appearance-none rounded-xl border bg-bg py-0 pr-9 pl-3 font-sans text-sm text-text {edge} transition-colors duration-150 ease-brand-out focus:outline-none disabled:pointer-events-none disabled:opacity-50
39
+ class="{focusRing} {toneRing[
40
+ tone
41
+ ]} h-10 w-full appearance-none rounded-xl border bg-bg py-0 pr-9 pl-3 font-sans text-sm text-text {edge} transition-colors duration-150 ease-brand-out disabled:pointer-events-none disabled:opacity-50
40
42
  {invalid ? 'border-danger' : `border-hairline-strong ${toneFocusBorder[tone]}`} {className}"
41
43
  {...rest}
42
44
  >
@@ -35,8 +35,8 @@
35
35
  </script>
36
36
 
37
37
  <script lang="ts">
38
- import { useLocale } from '../../core/locale.svelte.js';
39
38
  import { iconMd } from './icon.js';
39
+ import { soft } from './lift.js';
40
40
  import { focusRing, toneRing, toneSoft, type Tone } from '../../core/tones.js';
41
41
 
42
42
  interface Props {
@@ -69,8 +69,6 @@
69
69
  class: className = ''
70
70
  }: Props = $props();
71
71
 
72
- const t = useLocale();
73
-
74
72
  const keyOf = (item: SidebarItem) => item.id ?? item.href ?? item.label;
75
73
 
76
74
  /** Groups start open when asked, or when they contain the active item. */
@@ -93,7 +91,7 @@
93
91
  const surfaces: Record<SidebarVariant, string> = {
94
92
  plain: 'h-full bg-transparent',
95
93
  filled: 'h-full border-r border-hairline bg-bg-alt',
96
- floating: 'm-3 h-[calc(100%-1.5rem)] rounded-2xl border border-hairline bg-bg-alt shadow-sm'
94
+ floating: `m-3 h-[calc(100%-1.5rem)] rounded-2xl border border-hairline bg-bg-alt ${soft}`
97
95
  };
98
96
 
99
97
  const row =
@@ -141,7 +139,7 @@
141
139
  </div>
142
140
  {/if}
143
141
 
144
- <nav aria-label={t.current.navigation} class="flex-1 overflow-y-auto p-2">
142
+ <nav aria-label="Navigation" class="flex-1 overflow-y-auto p-2">
145
143
  {#each sections as section, i (section.label ?? i)}
146
144
  <div class="flex flex-col gap-0.5 {i > 0 ? 'mt-4' : ''}">
147
145
  {#if section.label && !collapsed}
@@ -230,7 +228,7 @@
230
228
  <button
231
229
  type="button"
232
230
  onclick={() => (collapsed = !collapsed)}
233
- aria-label={collapsed ? t.current.expandSidebar : t.current.collapseSidebar}
231
+ aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
234
232
  class="inline-flex h-8 items-center justify-center gap-2 rounded-xl font-sans text-xs text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text {focusRing} {toneRing[
235
233
  tone
236
234
  ]}"
@@ -248,7 +246,7 @@
248
246
  stroke-linecap="round"
249
247
  />
250
248
  </svg>
251
- {#if !collapsed}<span>{t.current.collapseSidebar}</span>{/if}
249
+ {#if !collapsed}<span>Collapse sidebar</span>{/if}
252
250
  </button>
253
251
  {/if}
254
252
  </div>
@@ -29,7 +29,7 @@
29
29
  </script>
30
30
 
31
31
  {#if variant === 'text' && lines > 1}
32
- <div class="flex w-full flex-col gap-2 {className}" {...rest}>
32
+ <div aria-hidden="true" class="flex w-full flex-col gap-2 {className}" {...rest}>
33
33
  {#each { length: lines }, i (i)}
34
34
  <div
35
35
  class="h-4 animate-pulse rounded-xl bg-bg-inset {i === lines - 1 ? 'w-2/3' : 'w-full'}"
@@ -39,6 +39,7 @@
39
39
  </div>
40
40
  {:else}
41
41
  <div
42
+ aria-hidden="true"
42
43
  class="animate-pulse bg-bg-inset {variants[variant]} {className}"
43
44
  style="{width ? `width:${width};` : ''}{height ? `height:${height};` : ''}"
44
45
  {...rest}
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLAttributes } from 'svelte/elements';
3
3
  import { toneText, type Tone } from '../../core/tones.js';
4
- import { useLocale } from '../../core/locale.svelte.js';
5
4
 
6
5
  export type SpinnerSize = 'xs' | 'sm' | 'md' | 'lg';
7
6
 
@@ -9,12 +8,10 @@
9
8
  size?: SpinnerSize;
10
9
  /** Omit to inherit the parent's `currentColor` (e.g. inside a Button). */
11
10
  tone?: Tone;
12
- /** Defaults to the locale's loading string; pass `''` to hide it. */
11
+ /** Defaults to `Loading`; pass `''` to hide it. */
13
12
  label?: string;
14
13
  }
15
14
 
16
- const t = useLocale();
17
-
18
15
  let { size = 'md', tone, label, class: className = '', ...rest }: Props = $props();
19
16
 
20
17
  const sizes: Record<SpinnerSize, string> = {
@@ -27,7 +24,7 @@
27
24
 
28
25
  <span
29
26
  role="status"
30
- aria-label={label ?? t.current.loading}
27
+ aria-label={label ?? 'Loading'}
31
28
  class="inline-block shrink-0 animate-spin rounded-full border-current/25 border-t-current
32
29
  {sizes[size]} {tone ? toneText[tone] : ''} {className}"
33
30
  {...rest}
@@ -5,7 +5,7 @@ interface Props extends HTMLAttributes<HTMLSpanElement> {
5
5
  size?: SpinnerSize;
6
6
  /** Omit to inherit the parent's `currentColor` (e.g. inside a Button). */
7
7
  tone?: Tone;
8
- /** Defaults to the locale's loading string; pass `''` to hide it. */
8
+ /** Defaults to `Loading`; pass `''` to hide it. */
9
9
  label?: string;
10
10
  }
11
11
  declare const Spinner: import("svelte").Component<Props, {}, "">;
@@ -32,7 +32,15 @@
32
32
  }: Props = $props();
33
33
 
34
34
  const trendTone: Record<StatTrend, Tone> = { up: 'success', down: 'danger', flat: 'neutral' };
35
- const trendGlyph: Record<StatTrend, string> = { up: '↑', down: '↓', flat: '→' };
35
+ // Drawn rather than typed. `↑` renders in whatever face the reader's system
36
+ // substitutes for it and is announced as "up arrow" or nothing at all, and
37
+ // the colour beside it says nothing either — so the word ships with it.
38
+ const trendPath: Record<StatTrend, string> = {
39
+ up: 'M7 11V3.5M7 3l3.5 3.5M7 3 3.5 6.5',
40
+ down: 'M7 3v7.5M7 11l3.5-3.5M7 11 3.5 7.5',
41
+ flat: 'M3 7h7.5M11 7 7.5 3.5M11 7l-3.5 3.5'
42
+ };
43
+ const trendWord: Record<StatTrend, string> = { up: 'up', down: 'down', flat: 'no change' };
36
44
  </script>
37
45
 
38
46
  <div class="flex flex-col gap-1 font-sans {className}" {...rest}>
@@ -49,7 +57,21 @@
49
57
  </span>
50
58
  {#if delta}
51
59
  <span class="text-sm font-medium {toneText[trendTone[trend]]}">
52
- {trendGlyph[trend]}
60
+ <svg
61
+ class="inline-block size-[1em] align-[-0.115em]"
62
+ viewBox="0 0 14 14"
63
+ fill="none"
64
+ aria-hidden="true"
65
+ >
66
+ <path
67
+ d={trendPath[trend]}
68
+ stroke="currentColor"
69
+ stroke-width="1.6"
70
+ stroke-linecap="round"
71
+ stroke-linejoin="round"
72
+ />
73
+ </svg>
74
+ <span class="sr-only">{trendWord[trend]}</span>
53
75
  {delta}
54
76
  </span>
55
77
  {/if}
@@ -31,6 +31,8 @@
31
31
  md: { track: 'h-6 w-11', thumb: 'size-5', travel: 'translate-x-5' }
32
32
  };
33
33
 
34
+ const uid = $props.id();
35
+
34
36
  function toggle() {
35
37
  checked = !checked;
36
38
  onchange?.(checked);
@@ -43,6 +45,7 @@
43
45
  role="switch"
44
46
  aria-checked={checked}
45
47
  aria-label={label}
48
+ aria-describedby={description ? `${uid}-description` : undefined}
46
49
  {disabled}
47
50
  onclick={toggle}
48
51
  class="relative mt-0.5 inline-flex shrink-0 items-center rounded-full p-0.5 transition-colors duration-200 ease-brand-out disabled:pointer-events-none
@@ -58,7 +61,9 @@
58
61
  <span class="flex flex-col gap-0.5 font-sans text-sm">
59
62
  {#if label}<span class="leading-snug text-text">{label}</span>{/if}
60
63
  {#if description}
61
- <span class="text-[13px] leading-snug text-text-muted">{description}</span>
64
+ <span id="{uid}-description" class="text-[13px] leading-snug text-text-muted"
65
+ >{description}</span
66
+ >
62
67
  {/if}
63
68
  </span>
64
69
  {/if}