@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
@@ -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
  import { edge } from './lift.js';
13
12
 
14
13
  export type AlertVariant = 'soft' | 'outline' | 'accent';
@@ -37,13 +36,12 @@
37
36
  ...rest
38
37
  }: Props = $props();
39
38
 
40
- const t = useLocale();
41
39
  let open = $state(true);
42
40
 
43
41
  const variants = $derived({
44
42
  soft: `${toneSurface[tone]} border ${toneBorderSoft[tone]}`,
45
43
  outline: `bg-bg ${edge} border ${toneBorderSoft[tone]}`,
46
- accent: `${toneSurface[tone]} border-l-4 ${toneBorder[tone]}`
44
+ accent: `${toneSurface[tone]} border-l-2 ${toneBorder[tone]}`
47
45
  });
48
46
 
49
47
  function dismiss() {
@@ -72,7 +70,7 @@
72
70
 
73
71
  <div class="flex min-w-0 flex-1 flex-col gap-1">
74
72
  {#if title}
75
- <p class="text-sm leading-snug font-semibold {toneText[tone]}">{title}</p>
73
+ <p class="font-heading text-sm leading-snug font-semibold {toneText[tone]}">{title}</p>
76
74
  {/if}
77
75
  {#if children}
78
76
  <div class="text-[13px] leading-relaxed text-text-secondary">{@render children()}</div>
@@ -83,8 +81,8 @@
83
81
  <button
84
82
  type="button"
85
83
  onclick={dismiss}
86
- aria-label={t.current.dismiss}
87
- 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"
84
+ aria-label="Dismiss"
85
+ class="-m-1 shrink-0 rounded-xl 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"
88
86
  >
89
87
  <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
90
88
  <path
@@ -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-2xl' : 'rounded-full'} {sizes[size]} {src
69
+ {squared ? 'rounded-2xl' : '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">
@@ -33,7 +30,7 @@
33
30
  {#if item.href && i < items.length - 1}
34
31
  <a
35
32
  href={item.href}
36
- class="inline-flex items-center gap-1.5 rounded px-1 py-0.5 text-text-muted transition-colors duration-150 ease-brand-out hover:bg-bg-inset {toneHoverText[
33
+ class="inline-flex items-center gap-1.5 rounded-md px-1 py-0.5 text-text-muted transition-colors duration-150 ease-brand-out hover:bg-bg-inset {toneHoverText[
37
34
  tone
38
35
  ]}"
39
36
  >
@@ -14,7 +14,7 @@
14
14
  toneText,
15
15
  type Tone
16
16
  } from '../../core/tones.js';
17
- import { edge, lift } from './lift.js';
17
+ import { lift, liftSoft } from './lift.js';
18
18
 
19
19
  export type ButtonVariant = 'solid' | 'soft' | 'outline' | 'ghost' | 'link' | 'primary';
20
20
  export type ButtonSize = 'sm' | 'md' | 'lg' | 'xl';
@@ -92,11 +92,11 @@
92
92
  case 'solid':
93
93
  return `border-transparent ${toneSolid[tone]} ${toneSolidHover[tone]} ${lift[tone]}`;
94
94
  case 'soft':
95
- return `border-transparent ${toneSoft[tone]} ${toneSoftHover[tone]} ${edge}`;
95
+ return `border-transparent ${toneSoft[tone]} ${toneSoftHover[tone]} ${liftSoft[tone]}`;
96
96
  // Outline keeps its own colours on hover — swapping to a solid fill is
97
97
  // matte's move, and it fights sprout's soft surfaces.
98
98
  case 'outline':
99
- return `bg-bg ${edge} ${toneText[tone]} ${toneBorderSoft[tone]} hover:bg-bg-alt`;
99
+ return `bg-bg ${liftSoft[tone]} ${toneText[tone]} ${toneBorderSoft[tone]} hover:bg-bg-alt`;
100
100
  // Nothing raised, so nothing to sink: ghost stays flat on the page.
101
101
  case 'ghost':
102
102
  return `border-transparent bg-transparent ${toneText[tone]} ${toneSoftHover[tone]} active:translate-y-0`;
@@ -116,6 +116,10 @@
116
116
  const iconClass = $derived(size === 'sm' ? iconSm : iconMd);
117
117
 
118
118
  async function handleClick(event: MouseEvent) {
119
+ if (inert) {
120
+ event.preventDefault();
121
+ return;
122
+ }
119
123
  const result = onclick?.(event);
120
124
  // Anything else is a plain handler: nothing to wait for, nothing to lock.
121
125
  if (!(result instanceof Promise)) return;
@@ -134,7 +138,7 @@
134
138
  disabled={href ? undefined : inert}
135
139
  aria-disabled={href && inert ? 'true' : undefined}
136
140
  aria-busy={busy ? 'true' : undefined}
137
- role={href ? 'button' : undefined}
141
+ tabindex={href && inert ? -1 : undefined}
138
142
  class="{base} {focusRing} {toneRing[tone]} {variantClass} {isLink
139
143
  ? ''
140
144
  : 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
  >
@@ -192,11 +189,11 @@
192
189
  type="button"
193
190
  onclick={() => pick(now)}
194
191
  disabled={blocked(now)}
195
- class="self-start rounded px-1 text-[13px] font-medium transition-colors duration-150 disabled:pointer-events-none disabled:opacity-40 {toneText[
192
+ class="self-start rounded-md px-1 text-[13px] font-medium transition-colors duration-150 disabled:pointer-events-none disabled:opacity-40 {toneText[
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>
@@ -40,7 +40,7 @@
40
40
  ...rest
41
41
  }: Props = $props();
42
42
 
43
- // The default card is the one the style is named for: cream, a hairline, and
43
+ // The default card is the one the style is named for: white, a hairline, and
44
44
  // the lip. `filled` and `tinted` stay flat on purpose — a tinted panel that
45
45
  // also floats is two claims about the same box.
46
46
  const variants = $derived({
@@ -52,7 +52,7 @@
52
52
 
53
53
  <div
54
54
  class="flex flex-col rounded-[28px] {variants[variant]}
55
- {accent ? `border-l-4 ${toneBorderLeft[tone]}` : ''}
55
+ {accent ? `border-l-2 ${toneBorderLeft[tone]}` : ''}
56
56
  {interactive
57
57
  ? `transition-[box-shadow,transform,border-color] duration-150 ease-brand-out hover:-translate-y-0.5 ${softOnHover} ${toneHoverBorder[tone]}`
58
58
  : ''}
@@ -26,6 +26,8 @@
26
26
  ...rest
27
27
  }: Props = $props();
28
28
 
29
+ const uid = $props.id();
30
+
29
31
  let el: HTMLInputElement | null = $state(null);
30
32
 
31
33
  $effect(() => {
@@ -33,44 +35,61 @@
33
35
  });
34
36
  </script>
35
37
 
36
- <label
37
- class="group inline-flex items-start gap-2.5 font-sans text-sm
38
- {disabled ? 'pointer-events-none opacity-50' : 'cursor-pointer'} {className}"
38
+ <div
39
+ class="inline-block font-sans text-sm {disabled
40
+ ? 'pointer-events-none opacity-50'
41
+ : ''} {className}"
39
42
  >
40
- <input bind:this={el} type="checkbox" bind:checked {disabled} class="peer sr-only" {...rest} />
41
- <span
42
- class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded border border-hairline-strong bg-bg {edge} transition-colors duration-150 ease-brand-out
43
+ <label class="group inline-flex items-start gap-2.5 {disabled ? '' : 'cursor-pointer'}">
44
+ <input
45
+ bind:this={el}
46
+ type="checkbox"
47
+ bind:checked
48
+ {disabled}
49
+ aria-describedby={description ? `${uid}-description` : undefined}
50
+ class="peer sr-only"
51
+ {...rest}
52
+ />
53
+ <span
54
+ class="mt-0.5 flex size-[18px] shrink-0 items-center justify-center rounded-md border border-hairline-strong bg-bg {edge} transition-colors duration-150 ease-brand-out
43
55
  {tonePeerChecked[tone]} {peerFocusRing} {tonePeerFocus[tone]}"
44
- aria-hidden="true"
45
- >
46
- {#if indeterminate}
47
- <span class="h-0.5 w-2.5 rounded-full bg-text-muted"></span>
48
- {:else}
49
- <svg
50
- class="size-3 text-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
51
- viewBox="0 0 12 12"
52
- fill="none"
53
- >
54
- <path
55
- d="m2 6 2.6 2.6L10 3.2"
56
- stroke="currentColor"
57
- stroke-width="2"
58
- stroke-linecap="round"
59
- stroke-linejoin="round"
60
- />
61
- </svg>
62
- {/if}
63
- </span>
64
- {#if label || description || children}
65
- <span class="flex flex-col gap-0.5">
66
- {#if children}
67
- {@render children()}
68
- {:else if label}
69
- <span class="leading-snug text-text">{label}</span>
70
- {/if}
71
- {#if description}
72
- <span class="text-[13px] leading-snug text-text-muted">{description}</span>
56
+ aria-hidden="true"
57
+ >
58
+ {#if indeterminate}
59
+ <span class="h-0.5 w-2.5 rounded-full bg-text-muted"></span>
60
+ {:else}
61
+ <svg
62
+ class="size-3 text-text-inverse opacity-0 transition-opacity duration-150 group-has-[:checked]:opacity-100"
63
+ viewBox="0 0 12 12"
64
+ fill="none"
65
+ >
66
+ <path
67
+ d="m2 6 2.6 2.6L10 3.2"
68
+ stroke="currentColor"
69
+ stroke-width="2"
70
+ stroke-linecap="round"
71
+ stroke-linejoin="round"
72
+ />
73
+ </svg>
73
74
  {/if}
74
75
  </span>
76
+ {#if label || children}
77
+ <span class="flex flex-col gap-0.5">
78
+ {#if children}
79
+ {@render children()}
80
+ {:else if label}
81
+ <span class="leading-snug text-text">{label}</span>
82
+ {/if}
83
+ </span>
84
+ {/if}
85
+ </label>
86
+ {#if description}
87
+ <!-- ponytail: the indent tracks the 18px box plus the label's gap; move one, move both. -->
88
+ <span
89
+ id="{uid}-description"
90
+ class="mt-0.5 block pl-[28px] text-[13px] leading-snug text-text-muted"
91
+ >
92
+ {description}
93
+ </span>
75
94
  {/if}
76
- </label>
95
+ </div>
@@ -51,6 +51,11 @@
51
51
  ...rest
52
52
  }: Props = $props();
53
53
 
54
+ const uid = $props.id();
55
+ // The group's own hint and error sit outside every control in it, so the
56
+ // fieldset is what has to point at them.
57
+ const messageId = $derived(error ? `${uid}-error` : hint ? `${uid}-hint` : undefined);
58
+
54
59
  const full = $derived(max > 0 && value.length >= max);
55
60
 
56
61
  function toggle(option: CheckboxOption) {
@@ -61,11 +66,17 @@
61
66
  }
62
67
  </script>
63
68
 
64
- <fieldset class="flex flex-col gap-3 border-0 p-0 {className}" {...rest}>
69
+ <fieldset
70
+ aria-describedby={messageId}
71
+ class="flex flex-col gap-3 border-0 p-0 {className}"
72
+ {...rest}
73
+ >
65
74
  {#if legend}
66
75
  <legend class="mb-1 font-sans text-sm font-medium text-text">
67
76
  {legend}
68
- {#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span>{/if}
77
+ {#if required}<span class="ml-0.5 text-danger" aria-hidden="true">*</span><span
78
+ class="sr-only">(required)</span
79
+ >{/if}
69
80
  </legend>
70
81
  {/if}
71
82
 
@@ -111,8 +122,8 @@
111
122
  </div>
112
123
 
113
124
  {#if error}
114
- <p class="font-sans text-[13px] text-danger">{error}</p>
125
+ <p id="{uid}-error" role="alert" class="font-sans text-[13px] text-danger">{error}</p>
115
126
  {:else if hint}
116
- <p class="font-sans text-[13px] text-text-muted">{hint}</p>
127
+ <p id="{uid}-hint" class="font-sans text-[13px] text-text-muted">{hint}</p>
117
128
  {/if}
118
129
  </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('');
@@ -77,14 +74,14 @@
77
74
  >
78
75
  {#snippet prefix()}
79
76
  <span
80
- class="relative grid size-5 place-items-center overflow-hidden rounded border border-hairline-strong"
77
+ class="relative grid size-5 place-items-center overflow-hidden rounded-lg border border-hairline-strong"
81
78
  >
82
79
  <span class="absolute inset-0" style="background-color: {resolved}"></span>
83
80
  <input
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
  import { edge, soft } from './lift.js';
10
16
 
@@ -52,8 +58,6 @@
52
58
  class: className = ''
53
59
  }: Props = $props();
54
60
 
55
- const t = useLocale();
56
-
57
61
  let open = $state(false);
58
62
  let query = $state('');
59
63
  let wrapper: HTMLElement | null = $state(null);
@@ -147,7 +151,9 @@
147
151
 
148
152
  <div
149
153
  bind:this={wrapper}
150
- class="relative inline-flex w-full items-center gap-2 rounded-xl border bg-bg pr-1.5 pl-3 {edge} transition-colors duration-150 ease-brand-out
154
+ class="relative inline-flex w-full items-center gap-2 rounded-xl border bg-bg pr-1.5 pl-3 {edge} transition-colors duration-150 ease-brand-out {focusWithinRing} {toneFocusWithinRing[
155
+ tone
156
+ ]}
151
157
  {invalid ? 'border-danger' : `border-hairline-strong ${toneFocusWithinBorder[tone]}`}
152
158
  {disabled ? 'pointer-events-none opacity-50' : ''} {className}"
153
159
  >
@@ -162,7 +168,7 @@
162
168
  aria-controls="{id ?? 'combobox'}-listbox"
163
169
  aria-activedescendant={open && flat[active] ? `${id ?? 'combobox'}-opt-${active}` : undefined}
164
170
  aria-invalid={invalid ? 'true' : undefined}
165
- placeholder={selected ? selected.label : (placeholder ?? t.current.comboboxPlaceholder)}
171
+ placeholder={selected ? selected.label : (placeholder ?? 'Search…')}
166
172
  value={open ? query : (selected?.label ?? '')}
167
173
  oninput={(event) => {
168
174
  query = event.currentTarget.value;
@@ -180,8 +186,8 @@
180
186
  <button
181
187
  type="button"
182
188
  onclick={clear}
183
- aria-label={t.current.clear}
184
- 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"
189
+ aria-label="Clear selection"
190
+ class="shrink-0 rounded-xl p-1 text-text-muted transition-colors duration-150 hover:bg-bg-inset hover:text-text focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
185
191
  >
186
192
  <svg class="size-3.5" viewBox="0 0 14 14" fill="none" aria-hidden="true">
187
193
  <path
@@ -218,7 +224,7 @@
218
224
  >
219
225
  {#if flat.length === 0}
220
226
  <p class="px-3 py-6 text-center text-sm text-text-muted">
221
- {emptyText ?? t.current.noMatches}
227
+ {emptyText ?? 'No matches'}
222
228
  </p>
223
229
  {:else}
224
230
  {#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';
@@ -62,8 +61,6 @@
62
61
  class: className = ''
63
62
  }: Props = $props();
64
63
 
65
- const t = useLocale();
66
-
67
64
  let dialog: HTMLDialogElement | null = $state(null);
68
65
  let inputEl: HTMLInputElement | null = $state(null);
69
66
  let listEl: HTMLElement | null = $state(null);
@@ -183,8 +180,8 @@
183
180
  oninput={() => cursor.reset()}
184
181
  type="text"
185
182
  autocomplete="off"
186
- aria-label={placeholder ?? t.current.commandPlaceholder}
187
- placeholder={placeholder ?? t.current.commandPlaceholder}
183
+ aria-label={placeholder ?? 'Type a command or search…'}
184
+ placeholder={placeholder ?? 'Type a command or search…'}
188
185
  class="h-12 w-full min-w-0 bg-transparent font-sans text-sm text-text placeholder:text-text-muted focus:outline-none"
189
186
  />
190
187
  <Kbd>esc</Kbd>
@@ -198,7 +195,7 @@
198
195
  >
199
196
  {#if flat.length === 0}
200
197
  <p class="px-4 py-10 text-center font-sans text-sm text-text-muted">
201
- {emptyText ?? t.current.noResults}
198
+ {emptyText ?? 'No results'}
202
199
  </p>
203
200
  {:else}
204
201
  {#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
  import { soft } from './lift.js';
8
7
 
9
8
  interface Props {
@@ -28,8 +27,6 @@
28
27
  menuClass = ''
29
28
  }: Props = $props();
30
29
 
31
- const t = useLocale();
32
-
33
30
  let menuEl: HTMLElement | null = $state(null);
34
31
  /**
35
32
  * A 0×0 element parked at the pointer, so the same `anchored` action that
@@ -80,7 +77,7 @@
80
77
  if ((event.target as HTMLElement).closest('[role="menuitem"]:not([disabled])')) open = false;
81
78
  }}
82
79
  role="menu"
83
- aria-label={label ?? t.current.menu}
80
+ aria-label={label ?? 'Menu'}
84
81
  tabindex="-1"
85
82
  class="z-50 flex max-h-[min(24rem,80vh)] min-w-48 flex-col overflow-y-auto rounded-2xl border border-hairline bg-bg py-1 font-sans {soft} {menuClass}"
86
83
  >
@@ -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;