@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
@@ -25,6 +25,8 @@
25
25
  ...rest
26
26
  }: Props = $props();
27
27
 
28
+ const uid = $props.id();
29
+
28
30
  let el: HTMLInputElement | null = $state(null);
29
31
 
30
32
  $effect(() => {
@@ -32,44 +34,61 @@
32
34
  });
33
35
  </script>
34
36
 
35
- <label
36
- class="group inline-flex items-start gap-2.5 font-sans text-sm
37
- {disabled ? 'pointer-events-none opacity-50' : 'cursor-pointer'} {className}"
37
+ <div
38
+ class="inline-block font-sans text-sm {disabled
39
+ ? 'pointer-events-none opacity-50'
40
+ : ''} {className}"
38
41
  >
39
- <input bind:this={el} type="checkbox" bind:checked {disabled} class="peer sr-only" {...rest} />
40
- <span
41
- class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded border border-hairline-strong bg-bg shadow-xs transition-colors duration-150 ease-brand-out
42
+ <label class="group inline-flex items-start gap-2.5 {disabled ? '' : 'cursor-pointer'}">
43
+ <input
44
+ bind:this={el}
45
+ type="checkbox"
46
+ bind:checked
47
+ {disabled}
48
+ aria-describedby={description ? `${uid}-description` : undefined}
49
+ class="peer sr-only"
50
+ {...rest}
51
+ />
52
+ <span
53
+ class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded border border-hairline-strong bg-bg shadow-xs transition-colors duration-150 ease-brand-out
42
54
  {tonePeerChecked[tone]} {peerFocusRing} {tonePeerFocus[tone]}"
43
- aria-hidden="true"
44
- >
45
- {#if indeterminate}
46
- <span class="h-0.5 w-2.5 rounded-full bg-text-muted"></span>
47
- {:else}
48
- <svg
49
- class="size-3 text-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
50
- viewBox="0 0 12 12"
51
- fill="none"
52
- >
53
- <path
54
- d="m2 6 2.6 2.6L10 3.2"
55
- stroke="currentColor"
56
- stroke-width="2"
57
- stroke-linecap="round"
58
- stroke-linejoin="round"
59
- />
60
- </svg>
61
- {/if}
62
- </span>
63
- {#if label || description || children}
64
- <span class="flex flex-col gap-0.5">
65
- {#if children}
66
- {@render children()}
67
- {:else if label}
68
- <span class="leading-snug text-text">{label}</span>
69
- {/if}
70
- {#if description}
71
- <span class="text-[13px] leading-snug text-text-muted">{description}</span>
55
+ aria-hidden="true"
56
+ >
57
+ {#if indeterminate}
58
+ <span class="h-0.5 w-2.5 rounded-full bg-text-muted"></span>
59
+ {:else}
60
+ <svg
61
+ class="size-3 text-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
62
+ viewBox="0 0 12 12"
63
+ fill="none"
64
+ >
65
+ <path
66
+ d="m2 6 2.6 2.6L10 3.2"
67
+ stroke="currentColor"
68
+ stroke-width="2"
69
+ stroke-linecap="round"
70
+ stroke-linejoin="round"
71
+ />
72
+ </svg>
72
73
  {/if}
73
74
  </span>
75
+ {#if label || children}
76
+ <span class="flex flex-col gap-0.5">
77
+ {#if children}
78
+ {@render children()}
79
+ {:else if label}
80
+ <span class="leading-snug text-text">{label}</span>
81
+ {/if}
82
+ </span>
83
+ {/if}
84
+ </label>
85
+ {#if description}
86
+ <!-- ponytail: the indent tracks the 18px box plus the label's gap; move one, move both. -->
87
+ <span
88
+ id="{uid}-description"
89
+ class="mt-0.5 block pl-[28px] text-[13px] leading-snug text-text-muted"
90
+ >
91
+ {description}
92
+ </span>
74
93
  {/if}
75
- </label>
94
+ </div>
@@ -50,6 +50,11 @@
50
50
  ...rest
51
51
  }: Props = $props();
52
52
 
53
+ const uid = $props.id();
54
+ // The group's own hint and error sit outside every control in it, so the
55
+ // fieldset is what has to point at them.
56
+ const messageId = $derived(error ? `${uid}-error` : hint ? `${uid}-hint` : undefined);
57
+
53
58
  const full = $derived(max > 0 && value.length >= max);
54
59
 
55
60
  function toggle(option: CheckboxOption) {
@@ -60,11 +65,17 @@
60
65
  }
61
66
  </script>
62
67
 
63
- <fieldset class="flex flex-col gap-3 border-0 p-0 {className}" {...rest}>
68
+ <fieldset
69
+ aria-describedby={messageId}
70
+ class="flex flex-col gap-3 border-0 p-0 {className}"
71
+ {...rest}
72
+ >
64
73
  {#if legend}
65
74
  <legend class="mb-1 font-sans text-sm font-medium text-text">
66
75
  {legend}
67
- {#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span>{/if}
76
+ {#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span><span
77
+ class="sr-only">(required)</span
78
+ >{/if}
68
79
  </legend>
69
80
  {/if}
70
81
 
@@ -110,8 +121,8 @@
110
121
  </div>
111
122
 
112
123
  {#if error}
113
- <p class="font-sans text-[13px] text-danger">{error}</p>
124
+ <p id="{uid}-error" role="alert" class="font-sans text-[13px] text-danger">{error}</p>
114
125
  {:else if hint}
115
- <p class="font-sans text-[13px] text-text-muted">{hint}</p>
126
+ <p id="{uid}-hint" class="font-sans text-[13px] text-text-muted">{hint}</p>
116
127
  {/if}
117
128
  </fieldset>
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLInputAttributes } from 'svelte/elements';
3
3
  import { normaliseHex } from '../../core/color.js';
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
 
@@ -27,8 +26,6 @@
27
26
  ...rest
28
27
  }: Props = $props();
29
28
 
30
- const t = useLocale();
31
-
32
29
  // The text can be mid-typing (`#a2`) while the swatch needs a real colour,
33
30
  // so the swatch follows the last value that parsed rather than the field.
34
31
  let text = $state('');
@@ -84,7 +81,7 @@
84
81
  type="color"
85
82
  value={resolved}
86
83
  {disabled}
87
- aria-label={t.current.pickColour}
84
+ aria-label="Pick a colour"
88
85
  oninput={(event) => choose(event.currentTarget.value)}
89
86
  class="absolute inset-0 cursor-pointer opacity-0 disabled:cursor-not-allowed"
90
87
  />
@@ -3,8 +3,14 @@
3
3
  import { anchored } from '../../core/actions/anchor.js';
4
4
  import { clickOutside, portal } from '../../core/actions/dismissable.js';
5
5
  import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
6
- import { toneFocusWithinBorder, toneSoft, toneText, type Tone } from '../../core/tones.js';
7
- import { useLocale } from '../../core/locale.svelte.js';
6
+ import {
7
+ focusWithinRing,
8
+ toneFocusWithinBorder,
9
+ toneFocusWithinRing,
10
+ toneSoft,
11
+ toneText,
12
+ type Tone
13
+ } from '../../core/tones.js';
8
14
  import { iconMd } from './icon.js';
9
15
 
10
16
  export interface ComboboxOption {
@@ -51,8 +57,6 @@
51
57
  class: className = ''
52
58
  }: Props = $props();
53
59
 
54
- const t = useLocale();
55
-
56
60
  let open = $state(false);
57
61
  let query = $state('');
58
62
  let wrapper: HTMLElement | null = $state(null);
@@ -146,7 +150,9 @@
146
150
 
147
151
  <div
148
152
  bind:this={wrapper}
149
- class="relative inline-flex w-full items-center gap-2 rounded-md border bg-bg pr-1.5 pl-3 shadow-xs transition-colors duration-150 ease-brand-out
153
+ class="relative inline-flex w-full items-center gap-2 rounded-md border bg-bg pr-1.5 pl-3 shadow-xs transition-colors duration-150 ease-brand-out {focusWithinRing} {toneFocusWithinRing[
154
+ tone
155
+ ]}
150
156
  {invalid ? 'border-danger' : `border-hairline-strong ${toneFocusWithinBorder[tone]}`}
151
157
  {disabled ? 'pointer-events-none opacity-50' : ''} {className}"
152
158
  >
@@ -161,7 +167,7 @@
161
167
  aria-controls="{id ?? 'combobox'}-listbox"
162
168
  aria-activedescendant={open && flat[active] ? `${id ?? 'combobox'}-opt-${active}` : undefined}
163
169
  aria-invalid={invalid ? 'true' : undefined}
164
- placeholder={selected ? selected.label : (placeholder ?? t.current.comboboxPlaceholder)}
170
+ placeholder={selected ? selected.label : (placeholder ?? 'Search…')}
165
171
  value={open ? query : (selected?.label ?? '')}
166
172
  oninput={(event) => {
167
173
  query = event.currentTarget.value;
@@ -179,7 +185,7 @@
179
185
  <button
180
186
  type="button"
181
187
  onclick={clear}
182
- aria-label={t.current.clear}
188
+ aria-label="Clear selection"
183
189
  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"
184
190
  >
185
191
  <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
@@ -217,7 +223,7 @@
217
223
  >
218
224
  {#if flat.length === 0}
219
225
  <p class="px-3 py-6 text-center text-sm text-text-muted">
220
- {emptyText ?? t.current.noMatches}
226
+ {emptyText ?? 'No matches'}
221
227
  </p>
222
228
  {:else}
223
229
  {#each groups as [group, items] (group)}
@@ -23,7 +23,6 @@
23
23
  import { focusTrap } from '../../core/actions/dismissable.js';
24
24
  import { syncDialog } from '../../core/dialog.svelte.js';
25
25
  import { ListCursor, groupItems, matchQuery, revealIndex } from '../../core/list.svelte.js';
26
- import { useLocale } from '../../core/locale.svelte.js';
27
26
  import { toneSoft, type Tone } from '../../core/tones.js';
28
27
  import { isTypingTarget } from '../../core/trigger.js';
29
28
  import { iconMd } from './icon.js';
@@ -61,8 +60,6 @@
61
60
  class: className = ''
62
61
  }: Props = $props();
63
62
 
64
- const t = useLocale();
65
-
66
63
  let dialog: HTMLDialogElement | null = $state(null);
67
64
  let inputEl: HTMLInputElement | null = $state(null);
68
65
  let listEl: HTMLElement | null = $state(null);
@@ -182,8 +179,8 @@
182
179
  oninput={() => cursor.reset()}
183
180
  type="text"
184
181
  autocomplete="off"
185
- aria-label={placeholder ?? t.current.commandPlaceholder}
186
- placeholder={placeholder ?? t.current.commandPlaceholder}
182
+ aria-label={placeholder ?? 'Type a command or search…'}
183
+ placeholder={placeholder ?? 'Type a command or search…'}
187
184
  class="h-12 w-full min-w-0 bg-transparent font-sans text-sm text-text placeholder:text-text-muted focus:outline-none"
188
185
  />
189
186
  <Kbd>esc</Kbd>
@@ -197,7 +194,7 @@
197
194
  >
198
195
  {#if flat.length === 0}
199
196
  <p class="px-4 py-10 text-center font-sans text-sm text-text-muted">
200
- {emptyText ?? t.current.noResults}
197
+ {emptyText ?? 'No results'}
201
198
  </p>
202
199
  {:else}
203
200
  {#each groups as [group, groupItems] (group)}
@@ -2,7 +2,6 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import Button from './Button.svelte';
4
4
  import Modal from './Modal.svelte';
5
- import { useLocale } from '../../core/locale.svelte.js';
6
5
  import { toneSoft, type Tone } from '../../core/tones.js';
7
6
 
8
7
  interface Props {
@@ -37,7 +36,7 @@
37
36
  children
38
37
  }: Props = $props();
39
38
 
40
- const t = useLocale();
39
+ const uid = $props.id();
41
40
 
42
41
  async function confirm() {
43
42
  try {
@@ -58,6 +57,8 @@
58
57
 
59
58
  <Modal
60
59
  bind:open
60
+ aria-labelledby="{uid}-title"
61
+ aria-describedby={description ? `${uid}-description` : undefined}
61
62
  size="sm"
62
63
  dismissible={!loading}
63
64
  showClose={false}
@@ -76,9 +77,11 @@
76
77
  {/if}
77
78
  </span>
78
79
  <div class="flex min-w-0 flex-1 flex-col gap-1.5">
79
- <h2 class="font-heading text-base font-semibold text-text">{title}</h2>
80
+ <h2 id="{uid}-title" class="font-heading text-base font-semibold text-text">{title}</h2>
80
81
  {#if description}
81
- <p class="text-[13px] leading-relaxed text-text-secondary">{description}</p>
82
+ <p id="{uid}-description" class="text-[13px] leading-relaxed text-text-secondary">
83
+ {description}
84
+ </p>
82
85
  {/if}
83
86
  {#if children}
84
87
  {@render children()}
@@ -88,10 +91,10 @@
88
91
 
89
92
  {#snippet footer()}
90
93
  <Button variant="outline" tone="neutral" onclick={cancel} disabled={loading}>
91
- {cancelLabel ?? t.current.cancel}
94
+ {cancelLabel ?? 'Cancel'}
92
95
  </Button>
93
96
  <Button {tone} onclick={confirm} {loading}>
94
- {confirmLabel ?? t.current.confirm}
97
+ {confirmLabel ?? 'Confirm'}
95
98
  </Button>
96
99
  {/snippet}
97
100
  </Modal>
@@ -3,7 +3,6 @@
3
3
  import { anchored } from '../../core/actions/anchor.js';
4
4
  import { clickOutside, focusTrap, navigateList, portal } from '../../core/actions/dismissable.js';
5
5
  import { menuItems } from '../../core/trigger.js';
6
- import { useLocale } from '../../core/locale.svelte.js';
7
6
 
8
7
  interface Props {
9
8
  /** The region that responds to a right-click. */
@@ -27,8 +26,6 @@
27
26
  menuClass = ''
28
27
  }: Props = $props();
29
28
 
30
- const t = useLocale();
31
-
32
29
  let menuEl: HTMLElement | null = $state(null);
33
30
  /**
34
31
  * A 0×0 element parked at the pointer, so the same `anchored` action that
@@ -79,7 +76,7 @@
79
76
  if ((event.target as HTMLElement).closest('[role="menuitem"]:not([disabled])')) open = false;
80
77
  }}
81
78
  role="menu"
82
- aria-label={label ?? t.current.menu}
79
+ aria-label={label ?? 'Menu'}
83
80
  tabindex="-1"
84
81
  class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {menuClass}"
85
82
  >
@@ -20,7 +20,7 @@
20
20
  precision?: number;
21
21
  /** Leading mark — `Rp`, `$`, `€`. */
22
22
  currency?: string;
23
- /** Trailing mark instead, for the locales that write it that way. */
23
+ /** Trailing mark instead, for the regions that write it that way. */
24
24
  unit?: string;
25
25
  size?: InputSize;
26
26
  tone?: Tone;
@@ -14,7 +14,7 @@ interface Props extends Omit<HTMLInputAttributes, 'size' | 'prefix' | 'suffix' |
14
14
  precision?: number;
15
15
  /** Leading mark — `Rp`, `$`, `€`. */
16
16
  currency?: string;
17
- /** Trailing mark instead, for the locales that write it that way. */
17
+ /** Trailing mark instead, for the regions that write it that way. */
18
18
  unit?: string;
19
19
  size?: InputSize;
20
20
  tone?: Tone;
@@ -2,9 +2,13 @@
2
2
  import { anchored } from '../../core/actions/anchor.js';
3
3
  import { clickOutside, portal } from '../../core/actions/dismissable.js';
4
4
  import { dateHint } from '../../core/calendar.js';
5
- import { formatISO, parseFormatted } from '../../core/date.js';
6
- import { useLocale } from '../../core/locale.svelte.js';
7
- import { toneFocusWithinBorder, type Tone } from '../../core/tones.js';
5
+ import { formatISO, parseFormatted, type DateFormat } from '../../core/date.js';
6
+ import {
7
+ focusWithinRing,
8
+ toneFocusWithinBorder,
9
+ toneFocusWithinRing,
10
+ type Tone
11
+ } from '../../core/tones.js';
8
12
  import Calendar from './Calendar.svelte';
9
13
 
10
14
  interface Props {
@@ -15,6 +19,8 @@
15
19
  max?: string;
16
20
  isDisabled?: (date: Date) => boolean;
17
21
  weekStart?: 0 | 1;
22
+ /** Order of the date parts the field accepts and prints. */
23
+ format?: DateFormat;
18
24
  tone?: Tone;
19
25
  invalid?: boolean;
20
26
  disabled?: boolean;
@@ -33,6 +39,7 @@
33
39
  max,
34
40
  isDisabled,
35
41
  weekStart = 1,
42
+ format = 'dmy',
36
43
  tone = 'brand',
37
44
  invalid = false,
38
45
  disabled = false,
@@ -44,8 +51,6 @@
44
51
  class: className = ''
45
52
  }: Props = $props();
46
53
 
47
- const t = useLocale();
48
-
49
54
  let open = $state(false);
50
55
  let text = $state('');
51
56
  let editing = $state(false);
@@ -53,8 +58,8 @@
53
58
  let inputEl: HTMLInputElement | null = $state(null);
54
59
 
55
60
  /** While typing the field shows the raw text; otherwise the formatted value. */
56
- const display = $derived(editing ? text : formatISO(value, t.current.dateFormat));
57
- const hint = $derived(placeholder ?? dateHint(t.current.dateFormat));
61
+ const display = $derived(editing ? text : formatISO(value, format));
62
+ const hint = $derived(placeholder ?? dateHint(format));
58
63
 
59
64
  function commit() {
60
65
  editing = false;
@@ -65,7 +70,7 @@
65
70
  }
66
71
  return;
67
72
  }
68
- const parsed = parseFormatted(text, t.current.dateFormat);
73
+ const parsed = parseFormatted(text, format);
69
74
  if (parsed && parsed !== value) {
70
75
  value = parsed;
71
76
  onchange?.(parsed);
@@ -94,7 +99,9 @@
94
99
 
95
100
  <div
96
101
  bind:this={wrapper}
97
- class="inline-flex w-full items-center gap-1 rounded-md border bg-bg pr-1 pl-3 shadow-xs transition-colors duration-150 ease-brand-out
102
+ class="inline-flex w-full items-center gap-1 rounded-md border bg-bg pr-1 pl-3 shadow-xs transition-colors duration-150 ease-brand-out {focusWithinRing} {toneFocusWithinRing[
103
+ tone
104
+ ]}
98
105
  {invalid ? 'border-danger' : `border-hairline-strong ${toneFocusWithinBorder[tone]}`}
99
106
  {disabled ? 'pointer-events-none opacity-50' : ''} {className}"
100
107
  >
@@ -132,7 +139,7 @@
132
139
  onchange?.('');
133
140
  inputEl?.focus();
134
141
  }}
135
- aria-label={t.current.clear}
142
+ aria-label="Clear selection"
136
143
  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"
137
144
  >
138
145
  <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
@@ -150,7 +157,7 @@
150
157
  type="button"
151
158
  {disabled}
152
159
  onclick={() => (open = !open)}
153
- aria-label={t.current.selectDate}
160
+ aria-label="Select a date"
154
161
  aria-expanded={open}
155
162
  class="shrink-0 rounded p-1.5 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"
156
163
  >
@@ -1,3 +1,4 @@
1
+ import { type DateFormat } from '../../core/date.js';
1
2
  import { type Tone } from '../../core/tones.js';
2
3
  interface Props {
3
4
  /** Bindable `YYYY-MM-DD` — the same shape as `<input type="date">`. */
@@ -7,6 +8,8 @@ interface Props {
7
8
  max?: string;
8
9
  isDisabled?: (date: Date) => boolean;
9
10
  weekStart?: 0 | 1;
11
+ /** Order of the date parts the field accepts and prints. */
12
+ format?: DateFormat;
10
13
  tone?: Tone;
11
14
  invalid?: boolean;
12
15
  disabled?: boolean;
@@ -20,7 +20,7 @@
20
20
  {...rest}
21
21
  ></div>
22
22
  {:else if label}
23
- <div class="flex items-center gap-3 {className}" {...rest}>
23
+ <div role="separator" class="flex items-center gap-3 {className}" {...rest}>
24
24
  <span class="h-px flex-1 bg-hairline"></span>
25
25
  <span class="font-sans text-xs font-medium text-text-muted">{label}</span>
26
26
  <span class="h-px flex-1 bg-hairline"></span>
@@ -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 DrawerSide = 'left' | 'right' | 'top' | 'bottom';
@@ -39,7 +38,8 @@
39
38
  ...rest
40
39
  }: Props = $props();
41
40
 
42
- const t = useLocale();
41
+ const uid = $props.id();
42
+
43
43
  let dialog: HTMLDialogElement | null = $state(null);
44
44
 
45
45
  const horizontal = $derived(side === 'left' || side === 'right');
@@ -82,6 +82,8 @@
82
82
 
83
83
  <dialog
84
84
  bind:this={dialog}
85
+ aria-labelledby={title ? `${uid}-title` : undefined}
86
+ aria-describedby={description ? `${uid}-description` : undefined}
85
87
  onclose={close}
86
88
  onclick={(event) => {
87
89
  if (dismissible && isBackdropClick(event, dialog)) close();
@@ -99,17 +101,19 @@
99
101
  {#if icon}<span class={iconLg}>{@render icon()}</span>{/if}
100
102
  <div class="flex min-w-0 flex-1 flex-col gap-1">
101
103
  {#if title}
102
- <h2 class="font-heading text-base font-semibold text-text">{title}</h2>
104
+ <h2 id="{uid}-title" class="font-heading text-base font-semibold text-text">{title}</h2>
103
105
  {/if}
104
106
  {#if description}
105
- <p class="font-sans text-[13px] text-text-muted">{description}</p>
107
+ <p id="{uid}-description" class="font-sans text-[13px] text-text-muted">
108
+ {description}
109
+ </p>
106
110
  {/if}
107
111
  </div>
108
112
  {#if dismissible}
109
113
  <button
110
114
  type="button"
111
115
  onclick={close}
112
- aria-label={t.current.close}
116
+ aria-label="Close"
113
117
  class="-m-1 shrink-0 rounded-md 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"
114
118
  >
115
119
  <svg class="size-4" viewBox="0 0 14 14" fill="none" aria-hidden="true">
@@ -3,7 +3,6 @@
3
3
  import { anchored, type Placement } from '../../core/actions/anchor.js';
4
4
  import { clickOutside, focusTrap, navigateList, portal } from '../../core/actions/dismissable.js';
5
5
  import { describeTrigger, menuItems, popupTrigger } from '../../core/trigger.js';
6
- import { useLocale } from '../../core/locale.svelte.js';
7
6
 
8
7
  interface Props {
9
8
  /** The control that opens the menu — put a `Button` in here. */
@@ -17,7 +16,7 @@
17
16
  matchWidth?: boolean;
18
17
  /** Close as soon as an item inside is activated. Default true. */
19
18
  closeOnSelect?: boolean;
20
- /** Accessible name for the menu. Defaults to the locale's `menu`. */
19
+ /** Accessible name for the menu. Defaults to `Menu`. */
21
20
  label?: string;
22
21
  class?: string;
23
22
  /** Classes for the wrapper around the trigger. */
@@ -37,8 +36,6 @@
37
36
  triggerClass = ''
38
37
  }: Props = $props();
39
38
 
40
- const t = useLocale();
41
-
42
39
  let anchorEl: HTMLElement | null = $state(null);
43
40
  let menuEl: HTMLElement | null = $state(null);
44
41
 
@@ -85,7 +82,7 @@
85
82
  onkeydown={onMenuKeydown}
86
83
  onclick={onMenuClick}
87
84
  role="menu"
88
- aria-label={label ?? t.current.menu}
85
+ aria-label={label ?? 'Menu'}
89
86
  tabindex="-1"
90
87
  class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 flex-col overflow-y-auto rounded-lg border border-hairline bg-bg py-1 font-sans shadow-lg {className}"
91
88
  >
@@ -12,7 +12,7 @@ interface Props {
12
12
  matchWidth?: boolean;
13
13
  /** Close as soon as an item inside is activated. Default true. */
14
14
  closeOnSelect?: boolean;
15
- /** Accessible name for the menu. Defaults to the locale's `menu`. */
15
+ /** Accessible name for the menu. Defaults to `Menu`. */
16
16
  label?: string;
17
17
  class?: string;
18
18
  /** Classes for the wrapper around the trigger. */
@@ -1,7 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import { toneBorder, toneSoft, toneSurface, toneText, type Tone } from '../../core/tones.js';
4
- import { useLocale } from '../../core/locale.svelte.js';
5
4
  import { formatSize as toReadableSize, sortFiles, type RejectedFile } from '../../core/files.js';
6
5
 
7
6
  interface Props {
@@ -16,7 +15,7 @@
16
15
  maxFiles?: number;
17
16
  disabled?: boolean;
18
17
  tone?: Tone;
19
- /** Headline inside the zone. Defaults to the locale's `dropFiles`. */
18
+ /** Headline inside the zone. Defaults to `Drop files here`. */
20
19
  label?: string;
21
20
  hint?: string;
22
21
  /** Hide the built-in file list to render your own. */
@@ -45,15 +44,13 @@
45
44
  class: className = ''
46
45
  }: Props = $props();
47
46
 
48
- const t = useLocale();
49
-
50
47
  let input: HTMLInputElement | null = $state(null);
51
48
  let dragging = $state(false);
52
49
  let rejected = $state<RejectedFile[]>([]);
53
50
  /** Nested elements fire dragleave too, so count depth instead of toggling. */
54
51
  let depth = 0;
55
52
 
56
- const formatSize = (bytes: number) => toReadableSize(bytes, t.current.byteUnits);
53
+ const formatSize = (bytes: number) => toReadableSize(bytes);
57
54
 
58
55
  const defaultHint = $derived.by(() => {
59
56
  if (hint) return hint;
@@ -96,9 +93,9 @@
96
93
  }
97
94
 
98
95
  const reasons = $derived({
99
- type: t.current.rejectedType,
100
- size: t.current.rejectedSize,
101
- count: t.current.rejectedCount
96
+ type: 'wrong file type',
97
+ size: 'too large',
98
+ count: 'over the file limit'
102
99
  });
103
100
  </script>
104
101
 
@@ -146,10 +143,10 @@
146
143
  {/if}
147
144
  </span>
148
145
  <span class="flex flex-col gap-1">
149
- <span class="text-sm font-semibold text-text">{label ?? t.current.dropFiles}</span>
146
+ <span class="text-sm font-semibold text-text">{label ?? 'Drop files here'}</span>
150
147
  <span class="text-[13px] text-text-secondary">
151
- {t.current.or}
152
- <span class="underline underline-offset-2">{t.current.browse}</span>
148
+ or
149
+ <span class="underline underline-offset-2">browse</span>
153
150
  </span>
154
151
  {#if defaultHint}
155
152
  <span class="text-xs text-text-muted">{defaultHint}</span>
@@ -191,7 +188,7 @@
191
188
  <button
192
189
  type="button"
193
190
  onclick={() => remove(i)}
194
- aria-label="{t.current.remove} {file.name}"
191
+ aria-label="Remove {file.name}"
195
192
  class="-m-1 shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-danger focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-danger"
196
193
  >
197
194
  <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
@@ -13,7 +13,7 @@ interface Props {
13
13
  maxFiles?: number;
14
14
  disabled?: boolean;
15
15
  tone?: Tone;
16
- /** Headline inside the zone. Defaults to the locale's `dropFiles`. */
16
+ /** Headline inside the zone. Defaults to `Drop files here`. */
17
17
  label?: string;
18
18
  hint?: string;
19
19
  /** Hide the built-in file list to render your own. */