@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
@@ -22,7 +22,7 @@
22
22
  columns?: FooterColumn[];
23
23
  /** Logo, wordmark and a line of copy on the left. */
24
24
  brand?: Snippet;
25
- /** Right side of the bottom bar — social links, a locale picker. */
25
+ /** Right side of the bottom bar — social links, a language picker. */
26
26
  bottom?: Snippet;
27
27
  /** Left side of the bottom bar. Ignored when `bottom` covers both. */
28
28
  copyright?: string;
@@ -61,15 +61,26 @@
61
61
  <a
62
62
  href={item.href}
63
63
  target={item.external ? '_blank' : undefined}
64
- rel={item.external ? 'noreferrer' : undefined}
64
+ rel={item.external ? 'noopener noreferrer' : undefined}
65
65
  class="inline-flex items-center gap-2 font-sans text-sm text-text-secondary transition-colors duration-150 ease-brand-out {toneHoverText[
66
66
  tone
67
67
  ]}"
68
68
  >
69
69
  {#if item.icon}<span class={iconSm}>{@render item.icon()}</span
70
70
  >{/if}{item.label}
71
- {#if item.external}<span aria-hidden="true" class="text-[0.85em]">↗</span
72
- >{/if}
71
+ {#if item.external}<svg
72
+ class="inline-block size-[0.85em] align-[-0.05em]"
73
+ viewBox="0 0 14 14"
74
+ fill="none"
75
+ aria-hidden="true"
76
+ ><path
77
+ d="M5.5 2.5h6v6M11.5 2.5 3.5 10.5"
78
+ stroke="currentColor"
79
+ stroke-width="1.5"
80
+ stroke-linecap="round"
81
+ stroke-linejoin="round"
82
+ /></svg
83
+ ><span class="sr-only">(opens in a new tab)</span>{/if}
73
84
  </a>
74
85
  </li>
75
86
  {/each}
@@ -15,7 +15,7 @@ interface Props {
15
15
  columns?: FooterColumn[];
16
16
  /** Logo, wordmark and a line of copy on the left. */
17
17
  brand?: Snippet;
18
- /** Right side of the bottom bar — social links, a locale picker. */
18
+ /** Right side of the bottom bar — social links, a language picker. */
19
19
  bottom?: Snippet;
20
20
  /** Left side of the bottom bar. Ignored when `bottom` covers both. */
21
21
  copyright?: string;
@@ -1,7 +1,12 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLInputAttributes } from 'svelte/elements';
4
- import { toneFocusWithinBorder, type Tone } from '../../core/tones.js';
4
+ import {
5
+ focusWithinRing,
6
+ toneFocusWithinBorder,
7
+ toneFocusWithinRing,
8
+ type Tone
9
+ } from '../../core/tones.js';
5
10
 
6
11
  export type InputSize = 'sm' | 'md' | 'lg';
7
12
 
@@ -37,7 +42,9 @@
37
42
  </script>
38
43
 
39
44
  <span
40
- class="inline-flex w-full items-center gap-2 border bg-bg px-3.5 transition-colors duration-150 ease-brand-out
45
+ class="inline-flex w-full items-center gap-2 border bg-bg px-3.5 transition-colors duration-150 ease-brand-out {focusWithinRing} {toneFocusWithinRing[
46
+ tone
47
+ ]}
41
48
  {sizes[size]}
42
49
  {invalid
43
50
  ? 'border-danger focus-within:border-danger'
@@ -12,5 +12,7 @@
12
12
 
13
13
  <label class="font-sans text-sm font-medium text-text {className}" {...rest}>
14
14
  {@render children()}
15
- {#if required}<span class="text-danger" aria-hidden="true">*</span>{/if}
15
+ {#if required}<span class="text-danger" aria-hidden="true">*</span><span class="sr-only"
16
+ >(required)</span
17
+ >{/if}
16
18
  </label>
@@ -40,7 +40,7 @@
40
40
 
41
41
  <a
42
42
  target={external ? '_blank' : undefined}
43
- rel={external ? 'noreferrer' : undefined}
43
+ rel={external ? 'noopener noreferrer' : undefined}
44
44
  class="inline-flex items-center gap-1 font-sans transition-colors duration-150 ease-brand-out
45
45
  {muted ? `text-text-secondary ${toneHoverText[tone]}` : toneText[tone]}
46
46
  {underlines[underline]} {focusRing} {toneRing[tone]} {className}"
@@ -49,5 +49,16 @@
49
49
  {#if icon}<span class={iconSm}>{@render icon()}</span>{/if}
50
50
  {@render children()}
51
51
  {#if iconEnd}<span class={iconSm}>{@render iconEnd()}</span>{/if}
52
- {#if external}<span aria-hidden="true" class="text-[0.85em]">↗</span>{/if}
52
+ {#if external}
53
+ <svg class="size-[0.85em] shrink-0" viewBox="0 0 14 14" fill="none" aria-hidden="true">
54
+ <path
55
+ d="M5.5 2.5h6v6M11.5 2.5 3.5 10.5"
56
+ stroke="currentColor"
57
+ stroke-width="1.5"
58
+ stroke-linecap="round"
59
+ stroke-linejoin="round"
60
+ />
61
+ </svg>
62
+ <span class="sr-only">(opens in a new tab)</span>
63
+ {/if}
53
64
  </a>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLButtonAttributes } from 'svelte/elements';
4
- import { toneSoftHover, toneText, type Tone } from '../../core/tones.js';
4
+ import { toneSoft, toneSoftHover, toneText, type Tone } from '../../core/tones.js';
5
5
  import { iconMd } from './icon.js';
6
6
 
7
7
  interface Props extends HTMLButtonAttributes {
@@ -28,6 +28,11 @@
28
28
  children,
29
29
  ...rest
30
30
  }: Props = $props();
31
+
32
+ // A chosen row speaks in the brand, the same as an active nav link — a tick
33
+ // alone leaves it the same grey as every row above it. An item that already
34
+ // carries a tone (a red `Delete`) keeps its own.
35
+ const chosen = $derived(toneSoft[tone === 'neutral' ? 'brand' : tone]);
31
36
  </script>
32
37
 
33
38
  <svelte:element
@@ -39,9 +44,11 @@
39
44
  aria-disabled={disabled ? 'true' : undefined}
40
45
  tabindex={disabled ? undefined : -1}
41
46
  class="flex w-full items-center gap-2.5 px-3 py-1.5 text-left text-[14px] transition-colors duration-100 ease-brand-out focus:outline-none disabled:pointer-events-none disabled:opacity-40
42
- {tone === 'neutral'
43
- ? 'text-text-secondary hover:bg-bg-inset focus-visible:bg-bg-inset'
44
- : `${toneText[tone]} ${toneSoftHover[tone]}`}
47
+ {selected
48
+ ? `${chosen} font-medium`
49
+ : tone === 'neutral'
50
+ ? 'text-text-secondary hover:bg-bg-inset focus-visible:bg-bg-inset'
51
+ : `${toneText[tone]} ${toneSoftHover[tone]}`}
45
52
  {className}"
46
53
  {...rest}
47
54
  >
@@ -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
 
8
7
  export type ModalSize = 'sm' | 'md' | 'lg';
@@ -40,7 +39,8 @@
40
39
  ...rest
41
40
  }: Props = $props();
42
41
 
43
- const t = useLocale();
42
+ const uid = $props.id();
43
+
44
44
  let dialog: HTMLDialogElement | null = $state(null);
45
45
 
46
46
  const closable = $derived(showClose ?? dismissible);
@@ -66,6 +66,8 @@
66
66
 
67
67
  <dialog
68
68
  bind:this={dialog}
69
+ aria-labelledby={title ? `${uid}-title` : undefined}
70
+ aria-describedby={description ? `${uid}-description` : undefined}
69
71
  onclose={close}
70
72
  onclick={onBackdrop}
71
73
  oncancel={(event) => {
@@ -82,17 +84,21 @@
82
84
  {#if icon}<span class={iconLg}>{@render icon()}</span>{/if}
83
85
  <div class="flex min-w-0 flex-1 flex-col gap-1">
84
86
  {#if title}
85
- <h2 class="font-heading text-lg font-medium tracking-tight">{title}</h2>
87
+ <h2 id="{uid}-title" class="font-heading text-lg font-medium tracking-tight">
88
+ {title}
89
+ </h2>
86
90
  {/if}
87
91
  {#if description}
88
- <p class="font-sans text-sm text-text-secondary">{description}</p>
92
+ <p id="{uid}-description" class="font-sans text-sm text-text-secondary">
93
+ {description}
94
+ </p>
89
95
  {/if}
90
96
  </div>
91
97
  {#if closable}
92
98
  <button
93
99
  type="button"
94
100
  onclick={close}
95
- aria-label={t.current.close}
101
+ aria-label="Close"
96
102
  class="-m-1 shrink-0 p-1 text-text-muted transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-brand"
97
103
  >
98
104
  <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
 
@@ -58,8 +57,6 @@
58
57
  class: className = ''
59
58
  }: Props = $props();
60
59
 
61
- const t = useLocale();
62
-
63
60
  let open = $state(false);
64
61
  let query = $state('');
65
62
  let wrapper: HTMLElement | null = $state(null);
@@ -149,7 +146,7 @@
149
146
  class="inline-flex items-center rounded-full px-2.5 py-1 font-mono text-xs {toneSoft[tone]}"
150
147
  >
151
148
  {chosen.length}
152
- {t.current.selected}
149
+ selected
153
150
  </span>
154
151
  {:else}
155
152
  {#each chosen as option (option.value)}
@@ -162,7 +159,7 @@
162
159
  <button
163
160
  type="button"
164
161
  onclick={() => remove(option)}
165
- aria-label="{t.current.removeItem} {option.label}"
162
+ aria-label="Remove {option.label}"
166
163
  class="inline-flex size-4 items-center justify-center rounded-full transition-colors duration-150 hover:bg-bg focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-current"
167
164
  >
168
165
  <svg class="size-2.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
@@ -184,7 +181,7 @@
184
181
  aria-expanded={open}
185
182
  aria-controls="{id ?? 'multiselect'}-listbox"
186
183
  aria-invalid={invalid ? 'true' : undefined}
187
- placeholder={chosen.length ? '' : (placeholder ?? t.current.comboboxPlaceholder)}
184
+ placeholder={chosen.length ? '' : (placeholder ?? 'Search…')}
188
185
  oninput={() => {
189
186
  open = true;
190
187
  cursor.index = 0;
@@ -206,7 +203,7 @@
206
203
  onchange?.([]);
207
204
  inputEl?.focus();
208
205
  }}
209
- aria-label={t.current.clear}
206
+ aria-label="Clear selection"
210
207
  class="shrink-0 p-1.5 text-text-muted transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
211
208
  >
212
209
  <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
@@ -229,7 +226,7 @@
229
226
  >
230
227
  {#if flat.length === 0}
231
228
  <p class="px-3 py-6 text-center text-sm text-text-muted">
232
- {emptyText ?? t.current.noMatches}
229
+ {emptyText ?? 'No matches'}
233
230
  </p>
234
231
  {:else}
235
232
  {#each groups as [group, items] (group)}
@@ -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, 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
  const link =
59
56
  'inline-flex items-center gap-2 font-sans text-[15px] transition-colors duration-150 ease-brand-out';
60
57
  </script>
@@ -64,10 +61,7 @@
64
61
  ? 'border-b border-hairline'
65
62
  : ''} {className}"
66
63
  >
67
- <nav
68
- aria-label={t.current.navigation}
69
- class="mx-auto flex h-16 w-full max-w-6xl items-center gap-6 px-6"
70
- >
64
+ <nav aria-label="Navigation" class="mx-auto flex h-16 w-full max-w-6xl items-center gap-6 px-6">
71
65
  {#if brand}
72
66
  <div class="flex shrink-0 items-center">{@render brand()}</div>
73
67
  {/if}
@@ -115,7 +109,7 @@
115
109
  class="{link} {focusRing} {toneRing[tone]} {item.active
116
110
  ? toneText[tone]
117
111
  : 'text-text-secondary hover:text-text'} {item.disabled
118
- ? 'pointer-events-none opacity-50'
112
+ ? 'pointer-events-none opacity-40'
119
113
  : ''}"
120
114
  >
121
115
  {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
@@ -138,7 +132,7 @@
138
132
  <button
139
133
  type="button"
140
134
  onclick={() => (menuOpen = true)}
141
- aria-label={t.current.openMenu}
135
+ aria-label="Open menu"
142
136
  aria-expanded={menuOpen}
143
137
  class="inline-flex size-9 items-center justify-center text-text-secondary transition-colors duration-150 hover:text-text md:hidden {focusRing} {toneRing[
144
138
  tone
@@ -152,7 +146,7 @@
152
146
  </nav>
153
147
  </header>
154
148
 
155
- <Drawer bind:open={menuOpen} side="right" size="sm" title={t.current.navigation}>
149
+ <Drawer bind:open={menuOpen} side="right" size="sm" title="Navigation">
156
150
  <ul class="flex flex-col">
157
151
  {#each items as item (item.label)}
158
152
  <li class="border-b border-hairline last:border-b-0">
@@ -162,7 +156,7 @@
162
156
  aria-current={item.active ? 'page' : undefined}
163
157
  class="flex items-center justify-between gap-3 py-3 font-sans text-[15px] {item.active
164
158
  ? toneText[tone]
165
- : 'text-text-secondary'} {item.disabled ? 'pointer-events-none opacity-50' : ''}"
159
+ : 'text-text-secondary'} {item.disabled ? 'pointer-events-none opacity-40' : ''}"
166
160
  >
167
161
  <span class="flex min-w-0 items-center gap-2.5">
168
162
  {#if item.icon}<span class={iconMd}>{@render item.icon()}</span>{/if}
@@ -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
 
7
6
  interface Props extends Omit<HTMLAttributes<HTMLElement>, 'onchange'> {
@@ -25,8 +24,6 @@
25
24
  ...rest
26
25
  }: Props = $props();
27
26
 
28
- const t = useLocale();
29
-
30
27
  const pages = $derived(pageRange(page, total, siblings));
31
28
 
32
29
  function go(next: number) {
@@ -39,12 +36,12 @@
39
36
  const cell = `inline-flex h-9 min-w-9 items-center justify-center px-2 font-mono text-[13px] transition-colors duration-150 ease-brand-out disabled:opacity-40 disabled:pointer-events-none ${focusRing}`;
40
37
  </script>
41
38
 
42
- <nav aria-label={t.current.pagination} class="flex items-center gap-1 {className}" {...rest}>
39
+ <nav aria-label="Pagination" class="flex items-center gap-1 {className}" {...rest}>
43
40
  <button
44
41
  type="button"
45
42
  class="{cell} {toneRing[tone]} text-text-secondary hover:bg-bg-inset"
46
43
  disabled={page <= 1}
47
- aria-label={t.current.previousPage}
44
+ aria-label="Previous page"
48
45
  onclick={() => go(page - 1)}
49
46
  >
50
47
 
@@ -71,7 +68,7 @@
71
68
  type="button"
72
69
  class="{cell} {toneRing[tone]} text-text-secondary hover:bg-bg-inset"
73
70
  disabled={page >= total}
74
- aria-label={t.current.nextPage}
71
+ aria-label="Next page"
75
72
  onclick={() => go(page + 1)}
76
73
  >
77
74
 
@@ -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
 
@@ -38,8 +37,6 @@
38
37
  class: className = ''
39
38
  }: Props = $props();
40
39
 
41
- const t = useLocale();
42
-
43
40
  let boxes: HTMLInputElement[] = $state([]);
44
41
 
45
42
  const chars = $derived(Array.from({ length }, (_, i) => value[i] ?? ''));
@@ -119,7 +116,7 @@
119
116
  autocapitalize="characters"
120
117
  spellcheck="false"
121
118
  maxlength={length}
122
- aria-label="{t.current.digit} {index + 1}"
119
+ aria-label="Digit {index + 1}"
123
120
  aria-invalid={invalid ? 'true' : undefined}
124
121
  oninput={(event) => onInput(event, index)}
125
122
  onkeydown={(event) => onKeydown(event, index)}
@@ -24,32 +24,51 @@
24
24
  children,
25
25
  ...rest
26
26
  }: Props = $props();
27
+
28
+ const uid = $props.id();
27
29
  </script>
28
30
 
29
- <label
30
- class="group inline-flex items-start gap-3 font-sans text-[15px]
31
- {disabled ? 'pointer-events-none opacity-50' : 'cursor-pointer'} {className}"
31
+ <div
32
+ class="inline-block font-sans text-[15px] {disabled
33
+ ? 'pointer-events-none opacity-50'
34
+ : ''} {className}"
32
35
  >
33
- <input type="radio" bind:group {value} {disabled} class="peer sr-only" {...rest} />
34
- <span
35
- class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded-full border border-hairline-strong bg-bg transition-colors duration-150 ease-brand-out
36
+ <label class="group inline-flex items-start gap-3 {disabled ? '' : 'cursor-pointer'}">
37
+ <input
38
+ type="radio"
39
+ bind:group
40
+ {value}
41
+ {disabled}
42
+ aria-describedby={description ? `${uid}-description` : undefined}
43
+ class="peer sr-only"
44
+ {...rest}
45
+ />
46
+ <span
47
+ class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded-full border border-hairline-strong bg-bg transition-colors duration-150 ease-brand-out
36
48
  {tonePeerChecked[tone]} {peerFocusRing} {tonePeerFocus[tone]}"
37
- aria-hidden="true"
38
- >
49
+ aria-hidden="true"
50
+ >
51
+ <span
52
+ class="size-1.5 rounded-full bg-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
53
+ ></span>
54
+ </span>
55
+ {#if label || children}
56
+ <span class="flex flex-col gap-0.5">
57
+ {#if children}
58
+ {@render children()}
59
+ {:else if label}
60
+ <span class="leading-snug text-text">{label}</span>
61
+ {/if}
62
+ </span>
63
+ {/if}
64
+ </label>
65
+ {#if description}
66
+ <!-- ponytail: the indent tracks the 18px dot plus the label's gap; move one, move both. -->
39
67
  <span
40
- class="size-1.5 rounded-full bg-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
41
- ></span>
42
- </span>
43
- {#if label || description || children}
44
- <span class="flex flex-col gap-0.5">
45
- {#if children}
46
- {@render children()}
47
- {:else if label}
48
- <span class="leading-snug text-text">{label}</span>
49
- {/if}
50
- {#if description}
51
- <span class="text-sm leading-snug text-text-muted">{description}</span>
52
- {/if}
68
+ id="{uid}-description"
69
+ class="mt-0.5 block pl-[30px] text-sm leading-snug text-text-muted"
70
+ >
71
+ {description}
53
72
  </span>
54
73
  {/if}
55
- </label>
74
+ </div>
@@ -44,13 +44,24 @@
44
44
  children,
45
45
  ...rest
46
46
  }: Props = $props();
47
+
48
+ const uid = $props.id();
49
+ // The group's own hint and error sit outside every control in it, so the
50
+ // fieldset is what has to point at them.
51
+ const messageId = $derived(error ? `${uid}-error` : hint ? `${uid}-hint` : undefined);
47
52
  </script>
48
53
 
49
- <fieldset class="flex flex-col gap-3 border-0 p-0 {className}" {...rest}>
54
+ <fieldset
55
+ aria-describedby={messageId}
56
+ class="flex flex-col gap-3 border-0 p-0 {className}"
57
+ {...rest}
58
+ >
50
59
  {#if legend}
51
60
  <legend class="mb-1 font-sans text-sm font-medium text-text">
52
61
  {legend}
53
- {#if required}<span class="text-danger" aria-hidden="true">*</span>{/if}
62
+ {#if required}<span class="text-danger" aria-hidden="true">*</span><span class="sr-only"
63
+ >(required)</span
64
+ >{/if}
54
65
  </legend>
55
66
  {/if}
56
67
 
@@ -92,8 +103,8 @@
92
103
  </div>
93
104
 
94
105
  {#if error}
95
- <p class="font-sans text-sm text-danger">{error}</p>
106
+ <p id="{uid}-error" role="alert" class="font-sans text-sm text-danger">{error}</p>
96
107
  {:else if hint}
97
- <p class="font-sans text-sm text-text-muted">{hint}</p>
108
+ <p id="{uid}-hint" class="font-sans text-sm text-text-muted">{hint}</p>
98
109
  {/if}
99
110
  </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,7 +72,7 @@
75
72
  <button
76
73
  type="button"
77
74
  onclick={clear}
78
- aria-label={t.current.clearSearch}
75
+ aria-label="Clear search"
79
76
  class="-mr-1 p-1 text-text-muted transition-colors duration-150 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">
@@ -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
 
6
6
  export interface SelectOption {
7
7
  value: string;
@@ -35,7 +35,9 @@
35
35
  <select
36
36
  bind:value
37
37
  aria-invalid={invalid ? 'true' : undefined}
38
- class="h-10 w-full appearance-none border bg-bg py-0 pr-10 pl-3.5 font-sans text-[15px] text-text transition-colors duration-150 ease-brand-out focus:outline-none disabled:pointer-events-none disabled:opacity-50
38
+ class="{focusRing} {toneRing[
39
+ tone
40
+ ]} h-10 w-full appearance-none border bg-bg py-0 pr-10 pl-3.5 font-sans text-[15px] text-text transition-colors duration-150 ease-brand-out disabled:pointer-events-none disabled:opacity-50
39
41
  {invalid ? 'border-danger' : `border-hairline ${toneFocusBorder[tone]}`} {className}"
40
42
  {...rest}
41
43
  >
@@ -35,7 +35,6 @@
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';
40
39
  import { focusRing, toneRing, toneSoft, type Tone } from '../../core/tones.js';
41
40
 
@@ -69,8 +68,6 @@
69
68
  class: className = ''
70
69
  }: Props = $props();
71
70
 
72
- const t = useLocale();
73
-
74
71
  const keyOf = (item: SidebarItem) => item.id ?? item.href ?? item.label;
75
72
 
76
73
  /** Groups start open when asked, or when they contain the active item. */
@@ -141,7 +138,7 @@
141
138
  </div>
142
139
  {/if}
143
140
 
144
- <nav aria-label={t.current.navigation} class="flex-1 overflow-y-auto py-3">
141
+ <nav aria-label="Navigation" class="flex-1 overflow-y-auto py-3">
145
142
  {#each sections as section, i (section.label ?? i)}
146
143
  <div class="flex flex-col gap-0.5 {i > 0 ? 'mt-4' : ''}">
147
144
  {#if section.label && !collapsed}
@@ -230,7 +227,7 @@
230
227
  <button
231
228
  type="button"
232
229
  onclick={() => (collapsed = !collapsed)}
233
- aria-label={collapsed ? t.current.expandSidebar : t.current.collapseSidebar}
230
+ aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
234
231
  class="inline-flex h-8 items-center justify-center gap-2 font-sans text-xs text-text-muted transition-colors duration-150 hover:text-text {focusRing} {toneRing[
235
232
  tone
236
233
  ]}"
@@ -248,7 +245,7 @@
248
245
  stroke-linecap="square"
249
246
  />
250
247
  </svg>
251
- {#if !collapsed}<span>{t.current.collapseSidebar}</span>{/if}
248
+ {#if !collapsed}<span>Collapse sidebar</span>{/if}
252
249
  </button>
253
250
  {/if}
254
251
  </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 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 border-t-transparent
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, {}, "">;