@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
@@ -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}>
@@ -41,13 +49,29 @@
41
49
  >
42
50
  <div class="flex items-baseline gap-2">
43
51
  <span
44
- class="font-heading text-3xl font-medium tracking-tight {tone ? toneText[tone] : 'text-text'}"
52
+ class="font-heading text-3xl font-medium tracking-tight tabular-nums {tone
53
+ ? toneText[tone]
54
+ : 'text-text'}"
45
55
  >
46
56
  {value}
47
57
  </span>
48
58
  {#if delta}
49
59
  <span class="font-mono text-xs {toneText[trendTone[trend]]}">
50
- {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>
51
75
  {delta}
52
76
  </span>
53
77
  {/if}
@@ -30,6 +30,8 @@
30
30
  md: { track: 'h-6 w-11', thumb: 'size-5', travel: 'translate-x-5' }
31
31
  };
32
32
 
33
+ const uid = $props.id();
34
+
33
35
  function toggle() {
34
36
  checked = !checked;
35
37
  onchange?.(checked);
@@ -42,6 +44,7 @@
42
44
  role="switch"
43
45
  aria-checked={checked}
44
46
  aria-label={label}
47
+ aria-describedby={description ? `${uid}-description` : undefined}
45
48
  {disabled}
46
49
  onclick={toggle}
47
50
  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
@@ -57,7 +60,9 @@
57
60
  <span class="flex flex-col gap-0.5 font-sans text-[15px]">
58
61
  {#if label}<span class="leading-snug text-text">{label}</span>{/if}
59
62
  {#if description}
60
- <span class="text-sm leading-snug text-text-muted">{description}</span>
63
+ <span id="{uid}-description" class="text-sm leading-snug text-text-muted"
64
+ >{description}</span
65
+ >
61
66
  {/if}
62
67
  </span>
63
68
  {/if}
@@ -16,7 +16,6 @@
16
16
  import Checkbox from './Checkbox.svelte';
17
17
  import type { Tone } from '../../core/tones.js';
18
18
  import { nextSort, sortRows, toggleKey, type TableSort } from '../../core/table.js';
19
- import { useLocale } from '../../core/locale.svelte.js';
20
19
 
21
20
  interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'onselect'> {
22
21
  columns: TableColumn[];
@@ -82,8 +81,6 @@
82
81
  ...rest
83
82
  }: Props = $props();
84
83
 
85
- const t = useLocale();
86
-
87
84
  const aligns = {
88
85
  left: 'text-left',
89
86
  center: 'text-center',
@@ -136,14 +133,14 @@
136
133
  >
137
134
  <span class="font-mono text-xs font-medium tracking-wide text-text uppercase">
138
135
  {selected.length}
139
- {t.current.selected}
136
+ selected
140
137
  </span>
141
138
  <button
142
139
  type="button"
143
140
  onclick={clearSelection}
144
141
  class="font-mono text-xs tracking-wide text-text-muted uppercase transition-colors duration-150 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
145
142
  >
146
- {t.current.clear}
143
+ Clear selection
147
144
  </button>
148
145
  <div class="ml-auto flex flex-wrap items-center gap-2">
149
146
  {@render bulkActions?.({ selected, clear: clearSelection })}
@@ -173,7 +170,7 @@
173
170
  checked={allSelected}
174
171
  indeterminate={someSelected}
175
172
  {tone}
176
- aria-label={t.current.selectAllRows}
173
+ aria-label="Select all rows"
177
174
  onchange={toggleAll}
178
175
  />
179
176
  </th>
@@ -220,7 +217,7 @@
220
217
  colspan={columns.length + (selectable ? 1 : 0)}
221
218
  class="px-3.5 py-10 text-center text-sm text-text-muted"
222
219
  >
223
- {#if empty}{@render empty()}{:else}{t.current.noData}{/if}
220
+ {#if empty}{@render empty()}{:else}No data{/if}
224
221
  </td>
225
222
  </tr>
226
223
  {:else}
@@ -237,7 +234,7 @@
237
234
  <Checkbox
238
235
  checked={isSelected}
239
236
  {tone}
240
- aria-label={t.current.selectRow}
237
+ aria-label="Select row"
241
238
  onchange={() => toggleRow(row)}
242
239
  />
243
240
  </td>
@@ -74,7 +74,7 @@
74
74
  }`;
75
75
  }
76
76
  return `${base} h-9 px-4 ${
77
- active ? 'bg-bg font-medium text-text' : 'text-text-secondary hover:text-text'
77
+ active ? `bg-bg font-medium ${toneText[tone]}` : 'text-text-secondary hover:text-text'
78
78
  }`;
79
79
  }
80
80
  </script>
@@ -1,5 +1,4 @@
1
1
  <script lang="ts">
2
- import { useLocale } from '../../core/locale.svelte.js';
3
2
  import { addTag, splitTags, type TagRejection } from '../../core/tags.js';
4
3
  import { toneFocusWithinBorder, toneSoft, type Tone } from '../../core/tones.js';
5
4
 
@@ -46,8 +45,6 @@
46
45
  class: className = ''
47
46
  }: Props = $props();
48
47
 
49
- const t = useLocale();
50
-
51
48
  let draft = $state('');
52
49
  let field: HTMLInputElement | null = $state(null);
53
50
  /** The last refusal, cleared as soon as the next keystroke lands. */
@@ -111,8 +108,8 @@
111
108
  const messages: Record<TagRejection, string> = $derived({
112
109
  empty: '',
113
110
  short: `${minLength}+`,
114
- duplicate: t.current.duplicateTag,
115
- full: `${t.current.selected} ${tags.length}/${max}`
111
+ duplicate: 'Already added',
112
+ full: `$selected ${tags.length}/${max}`
116
113
  });
117
114
  </script>
118
115
 
@@ -136,7 +133,7 @@
136
133
  <button
137
134
  type="button"
138
135
  onclick={() => remove(index)}
139
- aria-label="{t.current.removeItem} {tag}"
136
+ aria-label="Remove {tag}"
140
137
  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"
141
138
  >
142
139
  <svg class="size-2.5" viewBox="0 0 10 10" fill="none" aria-hidden="true">
@@ -151,8 +148,8 @@
151
148
  bind:value={draft}
152
149
  {disabled}
153
150
  readonly={full}
154
- placeholder={tags.length === 0 ? (placeholder ?? t.current.addTag) : ''}
155
- aria-label={placeholder ?? t.current.addTag}
151
+ placeholder={tags.length === 0 ? (placeholder ?? 'Add a tag') : ''}
152
+ aria-label={placeholder ?? 'Add a tag'}
156
153
  aria-invalid={invalid ? 'true' : undefined}
157
154
  onkeydown={onKeydown}
158
155
  onpaste={onPaste}
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLTextareaAttributes } from 'svelte/elements';
3
- import { toneFocusBorder, type Tone } from '../../core/tones.js';
3
+ import { focusRing, toneFocusBorder, toneRing, type Tone } from '../../core/tones.js';
4
4
 
5
5
  interface Props extends HTMLTextareaAttributes {
6
6
  value?: string;
@@ -48,7 +48,9 @@
48
48
  bind:value
49
49
  {rows}
50
50
  aria-invalid={invalid ? 'true' : undefined}
51
- class="w-full border bg-bg px-3.5 py-2.5 font-sans text-[15px] text-text transition-colors duration-150 ease-brand-out placeholder:text-text-muted focus:outline-none disabled:pointer-events-none disabled:opacity-50
51
+ class="{focusRing} {toneRing[
52
+ tone
53
+ ]} w-full border bg-bg px-3.5 py-2.5 font-sans text-[15px] text-text transition-colors duration-150 ease-brand-out placeholder:text-text-muted disabled:pointer-events-none disabled:opacity-50
52
54
  {autoresize ? 'resize-none overflow-hidden' : 'resize-y'}
53
55
  {invalid ? 'border-danger' : `border-hairline ${toneFocusBorder[tone]}`} {className}"
54
56
  {...rest}></textarea>
@@ -10,7 +10,6 @@
10
10
  storedTheme,
11
11
  type Theme
12
12
  } from '../../core/theme.svelte.js';
13
- import { useLocale } from '../../core/locale.svelte.js';
14
13
 
15
14
  interface Props {
16
15
  /** Bindable. `'system'` follows the OS until the visitor chooses. */
@@ -33,8 +32,6 @@
33
32
  class: className = ''
34
33
  }: Props = $props();
35
34
 
36
- const t = useLocale();
37
-
38
35
  $effect(() => {
39
36
  applyTheme(theme);
40
37
  if (persist) persistTheme(theme);
@@ -50,8 +47,8 @@
50
47
  }
51
48
 
52
49
  const options = $derived([
53
- { value: 'light' as Theme, label: t.current.lightMode },
54
- { value: 'dark' as Theme, label: t.current.darkMode },
50
+ { value: 'light' as Theme, label: 'Light' },
51
+ { value: 'dark' as Theme, label: 'Dark' },
55
52
  { value: 'system' as Theme, label: 'Auto' }
56
53
  ]);
57
54
  </script>
@@ -85,7 +82,7 @@
85
82
  {#if variant === 'segmented'}
86
83
  <div
87
84
  role="radiogroup"
88
- aria-label={t.current.toggleTheme}
85
+ aria-label="Toggle theme"
89
86
  class="inline-flex items-center gap-0 border border-hairline bg-bg-alt p-1 {className}"
90
87
  >
91
88
  {#each options as option (option.value)}
@@ -109,8 +106,8 @@
109
106
  <button
110
107
  type="button"
111
108
  onclick={cycle}
112
- aria-label="{t.current.toggleTheme} ({theme})"
113
- title={t.current.toggleTheme}
109
+ aria-label="Toggle theme ({theme})"
110
+ title="Toggle theme"
114
111
  class="inline-flex size-9 items-center justify-center border border-hairline transition-colors duration-150 ease-brand-out {focusRing} {toneRing[
115
112
  tone
116
113
  ]} {toneSoft[tone]} {className}"
@@ -1,6 +1,5 @@
1
1
  <script lang="ts">
2
2
  import type { HTMLInputAttributes } from 'svelte/elements';
3
- import { useLocale } from '../../core/locale.svelte.js';
4
3
  import { clampTime, formatTime, parseTime, stepTime } from '../../core/time.js';
5
4
  import type { Tone } from '../../core/tones.js';
6
5
  import Input, { type InputSize } from './Input.svelte';
@@ -38,8 +37,6 @@
38
37
  ...rest
39
38
  }: Props = $props();
40
39
 
41
- const t = useLocale();
42
-
43
40
  // Typed text and the bound value are separate until the field settles:
44
41
  // `9` is a valid thing to be halfway through typing and an invalid time.
45
42
  let text = $state('');
@@ -106,7 +103,7 @@
106
103
  class={stepper}
107
104
  {disabled}
108
105
  onclick={() => nudge(1)}
109
- aria-label={t.current.increase}
106
+ aria-label="Increase"
110
107
  >
111
108
  <svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
112
109
  <path d="m3 7.5 3-3 3 3" stroke="currentColor" stroke-width="1.5" />
@@ -117,7 +114,7 @@
117
114
  class={stepper}
118
115
  {disabled}
119
116
  onclick={() => nudge(-1)}
120
- aria-label={t.current.decrease}
117
+ aria-label="Decrease"
121
118
  >
122
119
  <svg class="size-3" viewBox="0 0 12 12" fill="none" aria-hidden="true">
123
120
  <path d="m3 4.5 3 3 3-3" stroke="currentColor" stroke-width="1.5" />
@@ -6,7 +6,6 @@
6
6
  import { toneBorderSoft, toneSurface, toneText } from '../../core/tones.js';
7
7
  import { iconMd } from './icon.js';
8
8
  import Spinner from './Spinner.svelte';
9
- import { useLocale } from '../../core/locale.svelte.js';
10
9
 
11
10
  export type ToasterPosition =
12
11
  'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
@@ -18,8 +17,6 @@
18
17
 
19
18
  let { position = 'bottom-right', class: className = '' }: Props = $props();
20
19
 
21
- const t = useLocale();
22
-
23
20
  const positions: Record<ToasterPosition, string> = {
24
21
  'top-left': 'top-4 left-4 items-start',
25
22
  'top-center': 'top-4 left-1/2 -translate-x-1/2 items-center',
@@ -33,22 +30,38 @@
33
30
  const fromLeft = $derived(position.endsWith('left'));
34
31
  const fromCenter = $derived(position.endsWith('center'));
35
32
 
33
+ /**
34
+ * A card sliding in from the edge of the screen, and a stack of them shuffling
35
+ * to make room, is exactly the motion this preference exists to stop. Under it
36
+ * the toast fades in place and the stack reorders instantly — the state change
37
+ * still reads, it just stops travelling.
38
+ *
39
+ * ponytail: read once at mount. Someone who flips the OS setting mid-session
40
+ * gets the new behaviour on the next load; a live listener is the upgrade.
41
+ */
42
+ const stillness =
43
+ typeof matchMedia === 'function' && matchMedia('(prefers-reduced-motion: reduce)').matches;
44
+
36
45
  const enter = $derived(
37
- fromCenter ? { y: fromTop ? -16 : 16, x: 0 } : { x: fromLeft ? -16 : 16, y: 0 }
46
+ stillness
47
+ ? { x: 0, y: 0 }
48
+ : fromCenter
49
+ ? { y: fromTop ? -16 : 16, x: 0 }
50
+ : { x: fromLeft ? -16 : 16, y: 0 }
38
51
  );
39
52
  </script>
40
53
 
41
54
  <div
42
55
  use:portal
43
56
  aria-live="polite"
44
- aria-label={t.current.notifications}
57
+ aria-label="Notifications"
45
58
  class="pointer-events-none fixed z-[100] flex w-[min(24rem,calc(100vw-2rem))] flex-col gap-3 {positions[
46
59
  position
47
60
  ]} {fromTop ? '' : 'flex-col-reverse'} {className}"
48
61
  >
49
62
  {#each toast.items as item (item.id)}
50
63
  <div
51
- animate:flip={{ duration: 200 }}
64
+ animate:flip={{ duration: stillness ? 0 : 200 }}
52
65
  in:fly={{ ...enter, duration: 220 }}
53
66
  out:fly={{ ...enter, duration: 160 }}
54
67
  role={item.tone === 'danger' ? 'alert' : 'status'}
@@ -78,7 +91,9 @@
78
91
  </span>
79
92
 
80
93
  <div class="flex min-w-0 flex-1 flex-col gap-1">
81
- <p class="text-[15px] leading-snug font-medium {toneText[item.tone]}">{item.title}</p>
94
+ <p class="font-heading text-[15px] leading-snug font-medium {toneText[item.tone]}">
95
+ {item.title}
96
+ </p>
82
97
  {#if item.description}
83
98
  <p class="text-sm leading-relaxed text-text-secondary">{item.description}</p>
84
99
  {/if}
@@ -101,7 +116,7 @@
101
116
  <button
102
117
  type="button"
103
118
  onclick={() => toast.dismiss(item.id)}
104
- aria-label={t.current.dismiss}
119
+ aria-label="Dismiss"
105
120
  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-current"
106
121
  >
107
122
  <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import { describedBy } from '../../core/trigger.js';
3
4
  import type { HTMLAttributes } from 'svelte/elements';
4
5
 
5
6
  export type TooltipPlacement = 'top' | 'bottom' | 'left' | 'right';
@@ -13,6 +14,11 @@
13
14
 
14
15
  let { content, placement = 'top', class: className = '', children, ...rest }: Props = $props();
15
16
 
17
+ const uid = $props.id();
18
+ let wrapper: HTMLElement | null = $state(null);
19
+
20
+ $effect(() => describedBy(wrapper, `${uid}-tip`));
21
+
16
22
  const placements: Record<TooltipPlacement, string> = {
17
23
  top: 'bottom-full left-1/2 mb-2 -translate-x-1/2',
18
24
  bottom: 'top-full left-1/2 mt-2 -translate-x-1/2',
@@ -21,9 +27,10 @@
21
27
  };
22
28
  </script>
23
29
 
24
- <span class="group/tt relative inline-flex" {...rest}>
30
+ <span bind:this={wrapper} class="group/tt relative inline-flex" {...rest}>
25
31
  {@render children()}
26
32
  <span
33
+ id="{uid}-tip"
27
34
  role="tooltip"
28
35
  class="pointer-events-none invisible absolute z-50 bg-text px-2.5 py-1.5 font-sans text-xs whitespace-nowrap text-text-inverse opacity-0 transition-opacity duration-150 ease-brand-out group-focus-within/tt:visible group-focus-within/tt:opacity-100 group-hover/tt:visible group-hover/tt:opacity-100
29
36
  {placements[placement]} {className}"
@@ -5,9 +5,8 @@
5
5
  * `@nqmcreative/ui/core`, re-exported below so this one import covers
6
6
  * the whole style.
7
7
  */
8
- export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast, locale, setLocale, provideLocale, useLocale, enUS, idID, LocaleStore, applyTheme, storedTheme, toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from '../../core/index.js';
9
- export type { Tone, Placement, AnchorOptions, ClickOutsideOptions, FocusTrapOptions, Toast, ToastOptions, ToastAction, Locale, Theme, DateFormat, TableSort, SortDirection, RejectedFile } from '../../core/index.js';
10
- export { default as LocaleProvider } from './LocaleProvider.svelte';
8
+ export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, focusWithinRing, toneFocusWithinRing, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast, applyTheme, storedTheme, toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from '../../core/index.js';
9
+ export type { Tone, Placement, AnchorOptions, ClickOutsideOptions, FocusTrapOptions, Toast, ToastOptions, ToastAction, Theme, DateFormat, TableSort, SortDirection, RejectedFile } from '../../core/index.js';
11
10
  export { default as Button } from './Button.svelte';
12
11
  export type { ButtonVariant, ButtonSize } from './Button.svelte';
13
12
  export { default as Link } from './Link.svelte';
@@ -6,9 +6,7 @@
6
6
  * the whole style.
7
7
  */
8
8
  /* --- shared behaviour --- */
9
- export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast, locale, setLocale, provideLocale, useLocale, enUS, idID, LocaleStore, applyTheme, storedTheme, toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from '../../core/index.js';
10
- /* --- locale --- */
11
- export { default as LocaleProvider } from './LocaleProvider.svelte';
9
+ export { TONES, focusRing, peerFocusRing, toneBorder, toneBorderLeft, toneBorderSoft, toneFill, toneFocusBorder, toneFocusWithinBorder, focusWithinRing, toneFocusWithinRing, toneHoverBorder, toneHoverText, tonePeerChecked, tonePeerFocus, toneRing, toneSoft, toneSoftHover, toneSolid, toneSolidHover, toneSurface, toneText, anchored, clickOutside, focusTrap, focusable, navigateList, portal, toast, applyTheme, storedTheme, toISO, fromISO, today, isSameDay, addDays, addMonths, monthGrid, weekdayNames, monthLabel, isOutOfRange, formatISO, parseFormatted } from '../../core/index.js';
12
10
  /* --- actions --- */
13
11
  export { default as Button } from './Button.svelte';
14
12
  export { default as Link } from './Link.svelte';
@@ -207,3 +207,24 @@
207
207
  --color-neutral-border: #334155;
208
208
  }
209
209
  }
210
+
211
+ /*
212
+ * The surfaces the browser draws for us.
213
+ *
214
+ * A caret and a highlight in the user-agent's blue belong to no design system,
215
+ * and they are the two the reader touches most: one sits in every field they
216
+ * type into, the other appears the moment they drag across a paragraph. The
217
+ * selection is mixed from the brand rather than pinned to a token so it stays
218
+ * a wash — the text underneath keeps its own colour and its own contrast.
219
+ */
220
+ @layer base {
221
+ ::selection {
222
+ background-color: color-mix(in oklab, var(--color-brand) 24%, transparent);
223
+ }
224
+
225
+ input,
226
+ textarea,
227
+ [contenteditable] {
228
+ caret-color: var(--color-brand);
229
+ }
230
+ }
@@ -8,7 +8,6 @@
8
8
  toneText,
9
9
  type Tone
10
10
  } from '../../core/tones.js';
11
- import { useLocale } from '../../core/locale.svelte.js';
12
11
 
13
12
  export type AlertVariant = 'soft' | 'outline' | 'accent';
14
13
 
@@ -36,13 +35,12 @@
36
35
  ...rest
37
36
  }: Props = $props();
38
37
 
39
- const t = useLocale();
40
38
  let open = $state(true);
41
39
 
42
40
  const variants = $derived({
43
41
  soft: `${toneSurface[tone]} border ${toneBorderSoft[tone]}`,
44
42
  outline: `bg-bg shadow-xs border ${toneBorderSoft[tone]}`,
45
- accent: `${toneSurface[tone]} border-l-4 ${toneBorder[tone]}`
43
+ accent: `${toneSurface[tone]} border-l-2 ${toneBorder[tone]}`
46
44
  });
47
45
 
48
46
  function dismiss() {
@@ -71,7 +69,7 @@
71
69
 
72
70
  <div class="flex min-w-0 flex-1 flex-col gap-1">
73
71
  {#if title}
74
- <p class="text-sm leading-snug font-semibold {toneText[tone]}">{title}</p>
72
+ <p class="font-heading text-sm leading-snug font-semibold {toneText[tone]}">{title}</p>
75
73
  {/if}
76
74
  {#if children}
77
75
  <div class="text-[13px] leading-relaxed text-text-secondary">{@render children()}</div>
@@ -82,7 +80,7 @@
82
80
  <button
83
81
  type="button"
84
82
  onclick={dismiss}
85
- aria-label={t.current.dismiss}
83
+ aria-label="Dismiss"
86
84
  class="-m-1 shrink-0 rounded p-1 text-text-muted transition-colors duration-150 hover:bg-bg/60 hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
87
85
  >
88
86
  <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
@@ -47,6 +47,12 @@
47
47
  xl: 'grid place-items-center *:size-8'
48
48
  };
49
49
 
50
+ // A src that 404s left the reader with the browser's broken-image mark while
51
+ // the initials sat right there unused. Holding the failed URL rather than a
52
+ // flag means a new src is tried again on its own.
53
+ let failed = $state('');
54
+ const broken = $derived(failed !== '' && failed === src);
55
+
50
56
  const initials = $derived(
51
57
  name
52
58
  .trim()
@@ -60,13 +66,18 @@
60
66
 
61
67
  <span
62
68
  class="inline-flex shrink-0 items-center justify-center overflow-hidden font-sans font-semibold select-none
63
- {squared ? 'rounded-lg' : 'rounded-full'} {sizes[size]} {src
69
+ {squared ? 'rounded-lg' : 'rounded-full'} {sizes[size]} {src && !broken
64
70
  ? 'bg-bg-inset'
65
71
  : toneSoft[tone]} {className}"
66
72
  {...rest}
67
73
  >
68
- {#if src}
69
- <img {src} alt={alt ?? name} class="size-full object-cover" />
74
+ {#if src && !broken}
75
+ <img
76
+ {src}
77
+ alt={alt ?? name}
78
+ onerror={() => (failed = src ?? '')}
79
+ class="size-full object-cover"
80
+ />
70
81
  {:else if fallback}
71
82
  <span class={glyphs[size]}>{@render fallback()}</span>
72
83
  {:else}
@@ -2,7 +2,6 @@
2
2
  import type { Snippet } from 'svelte';
3
3
  import type { HTMLAttributes } from 'svelte/elements';
4
4
  import { toneHoverText, type Tone } from '../../core/tones.js';
5
- import { useLocale } from '../../core/locale.svelte.js';
6
5
  import { iconMd } from './icon.js';
7
6
 
8
7
  export interface BreadcrumbItem {
@@ -19,11 +18,9 @@
19
18
  }
20
19
 
21
20
  let { items, separator = '/', tone = 'brand', class: className = '', ...rest }: Props = $props();
22
-
23
- const t = useLocale();
24
21
  </script>
25
22
 
26
- <nav aria-label={t.current.breadcrumb} class="font-sans text-sm {className}" {...rest}>
23
+ <nav aria-label="Breadcrumb" class="font-sans text-sm {className}" {...rest}>
27
24
  <ol class="flex flex-wrap items-center gap-1.5">
28
25
  {#each items as item, i (item.label)}
29
26
  <li class="flex items-center gap-1.5">
@@ -107,6 +107,10 @@
107
107
  const iconClass = $derived(size === 'sm' ? iconSm : iconMd);
108
108
 
109
109
  async function handleClick(event: MouseEvent) {
110
+ if (inert) {
111
+ event.preventDefault();
112
+ return;
113
+ }
110
114
  const result = onclick?.(event);
111
115
  // Anything else is a plain handler: nothing to wait for, nothing to lock.
112
116
  if (!(result instanceof Promise)) return;
@@ -125,7 +129,7 @@
125
129
  disabled={href ? undefined : inert}
126
130
  aria-disabled={href && inert ? 'true' : undefined}
127
131
  aria-busy={busy ? 'true' : undefined}
128
- role={href ? 'button' : undefined}
132
+ tabindex={href && inert ? -1 : undefined}
129
133
  class="{base} {focusRing} {toneRing[tone]} {variantClass} {isLink
130
134
  ? ''
131
135
  : iconOnly
@@ -11,7 +11,6 @@
11
11
  weekdayNames
12
12
  } from '../../core/date.js';
13
13
  import { calendarKeyMove } from '../../core/calendar.js';
14
- import { useLocale } from '../../core/locale.svelte.js';
15
14
  import {
16
15
  focusRing,
17
16
  toneFill,
@@ -53,8 +52,6 @@
53
52
  class: className = ''
54
53
  }: Props = $props();
55
54
 
56
- const t = useLocale();
57
-
58
55
  const selected = $derived(fromISO(value));
59
56
  const now = today();
60
57
 
@@ -71,7 +68,7 @@
71
68
  });
72
69
 
73
70
  const days = $derived(monthGrid(view, weekStart));
74
- const weekdays = $derived(weekdayNames(t.current.dateLocale, weekStart));
71
+ const weekdays = $derived(weekdayNames(undefined, weekStart));
75
72
 
76
73
  function blocked(date: Date) {
77
74
  return isOutOfRange(date, min, max) || (isDisabled?.(date) ?? false);
@@ -119,7 +116,7 @@
119
116
  <button
120
117
  type="button"
121
118
  onclick={() => show(addMonths(view, -1))}
122
- aria-label={t.current.previousMonth}
119
+ aria-label="Previous month"
123
120
  class="{arrow} {focusRing} {toneRing[tone]}"
124
121
  >
125
122
  <svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
@@ -128,13 +125,13 @@
128
125
  </button>
129
126
 
130
127
  <span aria-live="polite" class="text-sm font-semibold text-text">
131
- {monthLabel(view, t.current.dateLocale)}
128
+ {monthLabel(view)}
132
129
  </span>
133
130
 
134
131
  <button
135
132
  type="button"
136
133
  onclick={() => show(addMonths(view, 1))}
137
- aria-label={t.current.nextMonth}
134
+ aria-label="Next month"
138
135
  class="{arrow} {focusRing} {toneRing[tone]}"
139
136
  >
140
137
  <svg class="size-4" viewBox="0 0 16 16" fill="none" aria-hidden="true">
@@ -154,7 +151,7 @@
154
151
  bind:this={grid}
155
152
  role="grid"
156
153
  tabindex="-1"
157
- aria-label={t.current.selectDate}
154
+ aria-label="Select a date"
158
155
  onkeydown={onKeydown}
159
156
  class="grid grid-cols-7 gap-0.5 focus:outline-none"
160
157
  >
@@ -196,7 +193,7 @@
196
193
  tone
197
194
  ]} hover:underline hover:underline-offset-2 {focusRing} {toneRing[tone]}"
198
195
  >
199
- {t.current.today}
196
+ Today
200
197
  </button>
201
198
  {/if}
202
199
  </div>
@@ -48,7 +48,7 @@
48
48
 
49
49
  <div
50
50
  class="flex flex-col rounded-lg {variants[variant]}
51
- {accent ? `border-l-4 ${toneBorderLeft[tone]}` : ''}
51
+ {accent ? `border-l-2 ${toneBorderLeft[tone]}` : ''}
52
52
  {interactive
53
53
  ? `transition-shadow duration-150 ease-brand-out hover:shadow-md ${toneHoverBorder[tone]}`
54
54
  : ''}