@lyeve-labs/ui-kit 0.9.1 → 0.10.1

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 (61) hide show
  1. package/dist/components/AccordionItem.svelte +1 -1
  2. package/dist/components/Alert.svelte +42 -15
  3. package/dist/components/Alert.svelte.d.ts +2 -2
  4. package/dist/components/Autocomplete.svelte +32 -12
  5. package/dist/components/Avatar.svelte +4 -8
  6. package/dist/components/Badge.svelte +4 -4
  7. package/dist/components/Badge.svelte.d.ts +2 -2
  8. package/dist/components/Banner.svelte +10 -9
  9. package/dist/components/Banner.svelte.d.ts +2 -2
  10. package/dist/components/Breadcrumb.svelte +4 -1
  11. package/dist/components/Button.svelte +4 -2
  12. package/dist/components/Button.svelte.d.ts +1 -0
  13. package/dist/components/Card.svelte +1 -1
  14. package/dist/components/Checkbox.svelte +7 -4
  15. package/dist/components/Checkbox.svelte.d.ts +1 -0
  16. package/dist/components/DatePicker.svelte +19 -11
  17. package/dist/components/Drawer.svelte +13 -2
  18. package/dist/components/Dropdown.svelte +1 -1
  19. package/dist/components/FileInput.svelte +14 -5
  20. package/dist/components/Indicator.svelte +3 -3
  21. package/dist/components/Indicator.svelte.d.ts +2 -2
  22. package/dist/components/Input.svelte +17 -8
  23. package/dist/components/Modal.svelte +11 -2
  24. package/dist/components/MultiSelect.svelte +33 -12
  25. package/dist/components/NumberInput.svelte +68 -13
  26. package/dist/components/NumberInput.svelte.d.ts +3 -0
  27. package/dist/components/Pagination.svelte +1 -1
  28. package/dist/components/Progress.svelte +4 -3
  29. package/dist/components/Progress.svelte.d.ts +2 -2
  30. package/dist/components/Radio.svelte +6 -3
  31. package/dist/components/Radio.svelte.d.ts +1 -0
  32. package/dist/components/RadioGroup.svelte +6 -5
  33. package/dist/components/SearchInput.svelte +64 -49
  34. package/dist/components/SearchInput.svelte.d.ts +3 -0
  35. package/dist/components/Select.svelte +29 -12
  36. package/dist/components/Select.svelte.d.ts +2 -0
  37. package/dist/components/StepIndicator.svelte +7 -5
  38. package/dist/components/Table.svelte +3 -1
  39. package/dist/components/Tabs.svelte +1 -1
  40. package/dist/components/Tag.svelte +3 -3
  41. package/dist/components/Tag.svelte.d.ts +2 -2
  42. package/dist/components/Textarea.svelte +28 -16
  43. package/dist/components/Textarea.svelte.d.ts +1 -0
  44. package/dist/components/ThemeToggle.svelte +1 -1
  45. package/dist/components/Toaster.svelte +34 -12
  46. package/dist/components/Toggle.svelte +6 -3
  47. package/dist/components/Toggle.svelte.d.ts +1 -0
  48. package/dist/components/dialog/Dialog.svelte +1 -1
  49. package/dist/index.d.ts +1 -1
  50. package/dist/index.js +3 -1
  51. package/dist/internal/field.d.ts +48 -0
  52. package/dist/internal/field.js +62 -0
  53. package/dist/internal/tone.d.ts +27 -0
  54. package/dist/internal/tone.js +29 -0
  55. package/dist/stores/toast.svelte.d.ts +6 -1
  56. package/dist/stores/toast.svelte.js +1 -1
  57. package/dist/styles/theme.css +30 -4
  58. package/dist/utils/broken-image.d.ts +21 -0
  59. package/dist/utils/broken-image.js +40 -0
  60. package/package.json +7 -6
  61. package/src/lib/styles/theme.css +30 -4
@@ -29,7 +29,7 @@
29
29
  onclick={() => ctx?.toggle(id)}
30
30
  aria-expanded={open}
31
31
  class="flex w-full items-center justify-between px-5 py-4 text-left text-sm font-medium
32
- text-fg transition-colors hover:bg-surface-2/50"
32
+ text-fg transition-colors duration-150 hover:bg-surface-2/50"
33
33
  >
34
34
  <span>{title}</span>
35
35
  <svg
@@ -1,10 +1,14 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
-
4
- type Tone = 'info' | 'success' | 'warn' | 'danger';
3
+ import {
4
+ TONE_GLYPH,
5
+ statusTone,
6
+ type StatusTone,
7
+ type StatusToneInput,
8
+ } from '../internal/tone.js';
5
9
 
6
10
  interface Props {
7
- tone?: Tone;
11
+ tone?: StatusToneInput;
8
12
  title?: string;
9
13
  dismissible?: boolean;
10
14
  ondismiss?: () => void;
@@ -13,7 +17,7 @@
13
17
  }
14
18
 
15
19
  let {
16
- tone = 'info',
20
+ tone = 'brand',
17
21
  title = undefined,
18
22
  dismissible = false,
19
23
  ondismiss = undefined,
@@ -21,25 +25,39 @@
21
25
  children,
22
26
  }: Props = $props();
23
27
 
24
- const tones: Record<Tone, { wrap: string; icon: string; mark: string }> = {
25
- info: { wrap: 'bg-brand/8 border-brand/25', icon: 'text-brand', mark: 'ℹ' },
26
- success: { wrap: 'bg-success/8 border-success/25', icon: 'text-success', mark: '✓' },
27
- warn: { wrap: 'bg-warn/8 border-warn/25', icon: 'text-warn', mark: '!' },
28
- danger: { wrap: 'bg-danger/8 border-danger/25', icon: 'text-danger', mark: '×' },
28
+ const tones: Record<StatusTone, { wrap: string; icon: string }> = {
29
+ neutral: { wrap: 'bg-surface-2 border-line', icon: 'text-muted' },
30
+ brand: { wrap: 'bg-brand/8 border-brand/25', icon: 'text-brand' },
31
+ success: { wrap: 'bg-success/8 border-success/25', icon: 'text-success' },
32
+ warn: { wrap: 'bg-warn/8 border-warn/25', icon: 'text-warn' },
33
+ danger: { wrap: 'bg-danger/8 border-danger/25', icon: 'text-danger' },
29
34
  };
35
+
36
+ const t = $derived(statusTone(tone));
30
37
  </script>
31
38
 
32
39
  <div
33
- class="flex items-start gap-3 rounded-lg border px-4 py-3 {tones[tone].wrap} {klass}"
40
+ class="flex items-start gap-3 rounded-lg border px-4 py-3 {tones[t].wrap} {klass}"
34
41
  role="alert"
35
42
  >
36
43
  <span
37
- class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-current text-xs font-bold {tones[
38
- tone
44
+ class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-current {tones[
45
+ t
39
46
  ].icon}"
40
47
  aria-hidden="true"
41
48
  >
42
- {tones[tone].mark}
49
+ <svg
50
+ width="11"
51
+ height="11"
52
+ viewBox="0 0 24 24"
53
+ fill="none"
54
+ stroke="currentColor"
55
+ stroke-width="2.5"
56
+ stroke-linecap="round"
57
+ stroke-linejoin="round"
58
+ >
59
+ <path d={TONE_GLYPH[t]} />
60
+ </svg>
43
61
  </span>
44
62
  <div class="flex-1 min-w-0">
45
63
  {#if title}<p class="text-sm font-semibold text-fg">{title}</p>{/if}
@@ -51,10 +69,19 @@
51
69
  <button
52
70
  type="button"
53
71
  onclick={ondismiss}
54
- class="text-faint hover:text-fg transition-colors text-lg leading-none -mt-0.5"
72
+ class="shrink-0 text-faint transition-colors duration-150 hover:text-fg"
55
73
  aria-label="Dismiss"
56
74
  >
57
- ×
75
+ <svg
76
+ width="16"
77
+ height="16"
78
+ viewBox="0 0 24 24"
79
+ fill="none"
80
+ stroke="currentColor"
81
+ stroke-width="2"
82
+ stroke-linecap="round"
83
+ aria-hidden="true"><path d="M18 6L6 18M6 6l12 12" /></svg
84
+ >
58
85
  </button>
59
86
  {/if}
60
87
  </div>
@@ -1,7 +1,7 @@
1
1
  import type { Snippet } from 'svelte';
2
- type Tone = 'info' | 'success' | 'warn' | 'danger';
2
+ import { type StatusToneInput } from '../internal/tone.js';
3
3
  interface Props {
4
- tone?: Tone;
4
+ tone?: StatusToneInput;
5
5
  title?: string;
6
6
  dismissible?: boolean;
7
7
  ondismiss?: () => void;
@@ -1,4 +1,13 @@
1
1
  <script lang="ts">
2
+ import {
3
+ CONTROL_BASE,
4
+ FIELD_ERROR,
5
+ FIELD_HINT,
6
+ FIELD_LABEL,
7
+ FIELD_WRAP,
8
+ controlBorder,
9
+ describedBy,
10
+ } from '../internal/field.js';
2
11
  // Autocomplete (combobox): type to filter a list, navigate with ↑/↓, select with
3
12
  // Enter. Binds `value` to the chosen option's value; shows its label in the input.
4
13
  interface Option {
@@ -111,10 +120,10 @@
111
120
  });
112
121
  </script>
113
122
 
114
- <div class="flex flex-col gap-1.5 {cls}" bind:this={containerEl}>
123
+ <div class="{FIELD_WRAP} {cls}" bind:this={containerEl}>
115
124
  {#if label}
116
- <label for={id} class="text-sm font-medium text-fg">
117
- {label}{#if required}<span class="text-danger ml-0.5">*</span>{/if}
125
+ <label for={id} class={FIELD_LABEL}>
126
+ {label}{#if required}<span class="text-danger ml-0.5" aria-label="required">*</span>{/if}
118
127
  </label>
119
128
  {/if}
120
129
 
@@ -135,19 +144,30 @@
135
144
  onkeydown={onKeydown}
136
145
  onfocus={() => (open = true)}
137
146
  onblur={() => setTimeout(() => (open = false), 150)}
138
- class="w-full rounded-lg bg-surface-2 border px-3 py-2 text-sm text-fg placeholder:text-faint
139
- transition-colors outline-none disabled:opacity-50 disabled:cursor-not-allowed
140
- {allowClear && value ? 'pr-8' : 'pr-3'}
141
- {error ? 'border-danger focus:border-danger' : 'border-line focus:border-brand'}"
147
+ aria-invalid={error ? 'true' : undefined}
148
+ aria-describedby={describedBy(id, error, hint)}
149
+ class="{CONTROL_BASE} {controlBorder(!!error)} {allowClear && value ? 'pr-8' : 'pr-3'}"
142
150
  />
143
151
  {#if allowClear && value && !disabled}
144
152
  <button
145
153
  type="button"
146
154
  aria-label="Clear"
147
155
  onclick={clear}
148
- class="absolute right-2.5 top-1/2 -translate-y-1/2 text-faint hover:text-fg leading-none"
149
- >×</button
156
+ class="absolute right-2.5 top-1/2 -translate-y-1/2 text-faint transition-colors duration-150 hover:text-fg"
150
157
  >
158
+ <svg
159
+ width="12"
160
+ height="12"
161
+ viewBox="0 0 24 24"
162
+ fill="none"
163
+ stroke="currentColor"
164
+ stroke-width="2.5"
165
+ stroke-linecap="round"
166
+ aria-hidden="true"
167
+ >
168
+ <path d="M18 6L6 18M6 6l12 12" />
169
+ </svg>
170
+ </button>
151
171
  {/if}
152
172
 
153
173
  {#if open}
@@ -165,7 +185,7 @@
165
185
  disabled={opt.disabled}
166
186
  onmouseenter={() => (active = i)}
167
187
  onclick={() => choose(opt)}
168
- class="w-full flex items-center justify-between px-3 py-2 text-sm text-left transition-colors
188
+ class="w-full flex items-center justify-between px-3 py-2 text-sm text-left transition-colors duration-150
169
189
  disabled:opacity-40 disabled:cursor-not-allowed
170
190
  {i === active ? 'bg-surface-2' : ''}
171
191
  {opt.value === value ? 'text-brand' : 'text-fg'}"
@@ -191,8 +211,8 @@
191
211
  </div>
192
212
 
193
213
  {#if error}
194
- <p class="text-xs text-danger">{error}</p>
214
+ <p id={id ? `${id}-error` : undefined} class={FIELD_ERROR}>{error}</p>
195
215
  {:else if hint}
196
- <p class="text-xs text-faint">{hint}</p>
216
+ <p id={id ? `${id}-hint` : undefined} class={FIELD_HINT}>{hint}</p>
197
217
  {/if}
198
218
  </div>
@@ -1,4 +1,6 @@
1
1
  <script lang="ts">
2
+ import { brokenImage } from '../utils/broken-image.js';
3
+
2
4
  let {
3
5
  name,
4
6
  src,
@@ -49,16 +51,10 @@
49
51
  class="inline-flex items-center justify-center rounded-full font-semibold shrink-0
50
52
  {s.wrap} {hue} text-ink {cls}"
51
53
  title={name}
54
+ use:brokenImage={() => (imgError = true)}
52
55
  >
53
56
  {#if src && !imgError}
54
- <img
55
- {src}
56
- alt={name}
57
- class="w-full h-full rounded-full object-cover"
58
- onerror={() => {
59
- imgError = true;
60
- }}
61
- />
57
+ <img {src} alt={name} class="w-full h-full rounded-full object-cover" />
62
58
  {:else}
63
59
  <span class={s.text}>{initials}</span>
64
60
  {/if}
@@ -1,11 +1,11 @@
1
1
  <script lang="ts">
2
+ import type { AccentTone } from '../internal/tone.js';
2
3
  import type { Snippet } from 'svelte';
3
4
 
4
- type Tone = 'neutral' | 'brand' | 'violet' | 'success' | 'warn' | 'danger';
5
5
  type Size = 'sm' | 'md';
6
6
 
7
7
  interface Props {
8
- tone?: Tone;
8
+ tone?: AccentTone;
9
9
  size?: Size;
10
10
  dot?: boolean;
11
11
  class?: string;
@@ -14,7 +14,7 @@
14
14
 
15
15
  let { tone = 'neutral', size = 'sm', dot = false, class: klass = '', children }: Props = $props();
16
16
 
17
- const tones: Record<Tone, string> = {
17
+ const tones: Record<AccentTone, string> = {
18
18
  neutral: 'bg-surface-2 text-muted border-line',
19
19
  brand: 'bg-brand/10 text-brand border-brand/20',
20
20
  violet: 'bg-violet/10 text-violet border-violet/20',
@@ -23,7 +23,7 @@
23
23
  danger: 'bg-danger/10 text-danger border-danger/20',
24
24
  };
25
25
 
26
- const dotColor: Record<Tone, string> = {
26
+ const dotColor: Record<AccentTone, string> = {
27
27
  neutral: 'bg-muted',
28
28
  brand: 'bg-brand',
29
29
  violet: 'bg-violet',
@@ -1,8 +1,8 @@
1
+ import type { AccentTone } from '../internal/tone.js';
1
2
  import type { Snippet } from 'svelte';
2
- type Tone = 'neutral' | 'brand' | 'violet' | 'success' | 'warn' | 'danger';
3
3
  type Size = 'sm' | 'md';
4
4
  interface Props {
5
- tone?: Tone;
5
+ tone?: AccentTone;
6
6
  size?: Size;
7
7
  dot?: boolean;
8
8
  class?: string;
@@ -1,10 +1,9 @@
1
1
  <script lang="ts">
2
+ import { statusTone, type StatusTone, type StatusToneInput } from '../internal/tone.js';
2
3
  import type { Snippet } from 'svelte';
3
4
 
4
- type Tone = 'info' | 'success' | 'warn' | 'danger' | 'neutral';
5
-
6
5
  interface Props {
7
- tone?: Tone;
6
+ tone?: StatusToneInput;
8
7
  dismissible?: boolean;
9
8
  ondismiss?: () => void;
10
9
  class?: string;
@@ -13,7 +12,7 @@
13
12
  }
14
13
 
15
14
  let {
16
- tone = 'info',
15
+ tone = 'brand',
17
16
  dismissible = false,
18
17
  ondismiss = undefined,
19
18
  class: cls = '',
@@ -28,19 +27,21 @@
28
27
  ondismiss?.();
29
28
  }
30
29
 
31
- const tones: Record<Tone, string> = {
32
- info: 'bg-brand/10 border-brand/20',
30
+ const tones: Record<StatusTone, string> = {
31
+ neutral: 'bg-surface-2 border-line',
32
+ brand: 'bg-brand/10 border-brand/20',
33
33
  success: 'bg-success/10 border-success/20',
34
34
  warn: 'bg-warn/10 border-warn/20',
35
35
  danger: 'bg-danger/10 border-danger/20',
36
- neutral: 'bg-surface-2 border-line',
37
36
  };
37
+
38
+ const t = $derived(statusTone(tone));
38
39
  </script>
39
40
 
40
41
  {#if visible}
41
42
  <div
42
43
  role="status"
43
- class="relative flex items-center gap-3 border-b px-4 py-3 text-sm {tones[tone]} {cls}"
44
+ class="relative flex items-center gap-3 border-b px-4 py-3 text-sm {tones[t]} {cls}"
44
45
  >
45
46
  <div class="flex flex-1 items-center justify-center gap-3 text-fg">
46
47
  {@render children()}
@@ -55,7 +56,7 @@
55
56
  type="button"
56
57
  onclick={dismiss}
57
58
  aria-label="Dismiss"
58
- class="shrink-0 text-faint transition-colors hover:text-fg"
59
+ class="shrink-0 text-faint transition-colors duration-150 hover:text-fg"
59
60
  >
60
61
  <svg
61
62
  width="16"
@@ -1,7 +1,7 @@
1
+ import { type StatusToneInput } from '../internal/tone.js';
1
2
  import type { Snippet } from 'svelte';
2
- type Tone = 'info' | 'success' | 'warn' | 'danger' | 'neutral';
3
3
  interface Props {
4
- tone?: Tone;
4
+ tone?: StatusToneInput;
5
5
  dismissible?: boolean;
6
6
  ondismiss?: () => void;
7
7
  class?: string;
@@ -32,7 +32,10 @@
32
32
  {/if}
33
33
 
34
34
  {#if item.href && i < items.length - 1}
35
- <a href={safeUrl(item.href)} class="text-muted hover:text-fg transition-colors truncate">
35
+ <a
36
+ href={safeUrl(item.href)}
37
+ class="text-muted hover:text-fg transition-colors duration-150 truncate"
38
+ >
36
39
  {item.label}
37
40
  </a>
38
41
  {:else}
@@ -13,6 +13,7 @@
13
13
  type?: 'button' | 'submit' | 'reset';
14
14
  href?: string;
15
15
  full?: boolean;
16
+ class?: string;
16
17
  onclick?: (e: MouseEvent) => void;
17
18
  children: Snippet;
18
19
  [key: string]: unknown;
@@ -26,6 +27,7 @@
26
27
  type = 'button',
27
28
  href = undefined,
28
29
  full = false,
30
+ class: klass = '',
29
31
  onclick,
30
32
  children,
31
33
  ...rest
@@ -49,9 +51,9 @@
49
51
 
50
52
  const spinnerSize = $derived(size === 'lg' ? 18 : 15);
51
53
  const cls = $derived(
52
- `inline-flex items-center justify-center font-medium transition-all duration-150 ` +
54
+ `inline-flex items-center justify-center font-medium transition-colors duration-150 ` +
53
55
  `disabled:opacity-50 disabled:cursor-not-allowed select-none ` +
54
- `${full ? 'w-full' : ''} ${variants[variant]} ${sizes[size]}`,
56
+ `${full ? 'w-full' : ''} ${variants[variant]} ${sizes[size]} ${klass}`,
55
57
  );
56
58
 
57
59
  // Reject javascript: and data: URIs - only allow standard schemes and relative URLs.
@@ -9,6 +9,7 @@ interface Props {
9
9
  type?: 'button' | 'submit' | 'reset';
10
10
  href?: string;
11
11
  full?: boolean;
12
+ class?: string;
12
13
  onclick?: (e: MouseEvent) => void;
13
14
  children: Snippet;
14
15
  [key: string]: unknown;
@@ -41,7 +41,7 @@
41
41
  so the element is interactive; the compiler can't narrow the dynamic role. -->
42
42
  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
43
43
  <div
44
- class="bg-surface border border-line rounded-xl overflow-hidden transition-colors {hover
44
+ class="bg-surface border border-line rounded-xl overflow-hidden transition-colors duration-150 {hover
45
45
  ? 'hover:border-line/0 hover:ring-1 hover:ring-brand/30'
46
46
  : ''} {onclick ? 'cursor-pointer' : ''} {klass}"
47
47
  role={onclick ? 'button' : undefined}
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { FIELD_HINT } from '../internal/field.js';
2
3
  let {
3
4
  checked = $bindable(false),
4
5
  label,
@@ -8,6 +9,7 @@
8
9
  id,
9
10
  name,
10
11
  value,
12
+ class: cls = '',
11
13
  onchange,
12
14
  }: {
13
15
  checked?: boolean;
@@ -18,6 +20,7 @@
18
20
  id?: string;
19
21
  name?: string;
20
22
  value?: string;
23
+ class?: string;
21
24
  onchange?: (checked: boolean) => void;
22
25
  } = $props();
23
26
 
@@ -28,9 +31,9 @@
28
31
  </script>
29
32
 
30
33
  <label
31
- class="flex items-start gap-2.5 cursor-pointer select-none {disabled
34
+ class="inline-flex items-start gap-2.5 cursor-pointer select-none {disabled
32
35
  ? 'opacity-50 cursor-not-allowed'
33
- : ''}"
36
+ : ''} {cls}"
34
37
  >
35
38
  <span class="relative flex items-center justify-center mt-0.5 shrink-0">
36
39
  <input
@@ -45,7 +48,7 @@
45
48
  class="peer sr-only"
46
49
  />
47
50
  <span
48
- class="w-4 h-4 rounded border transition-colors flex items-center justify-center
51
+ class="w-4 h-4 rounded border transition-colors duration-150 flex items-center justify-center
49
52
  {checked
50
53
  ? 'bg-brand border-brand'
51
54
  : 'bg-surface-2 border-line peer-focus-visible:border-brand'}"
@@ -73,7 +76,7 @@
73
76
  </span>
74
77
  {/if}
75
78
  {#if hint}
76
- <span class="text-xs text-faint">{hint}</span>
79
+ <span class={FIELD_HINT}>{hint}</span>
77
80
  {/if}
78
81
  </span>
79
82
  {/if}
@@ -7,6 +7,7 @@ type $$ComponentProps = {
7
7
  id?: string;
8
8
  name?: string;
9
9
  value?: string;
10
+ class?: string;
10
11
  onchange?: (checked: boolean) => void;
11
12
  };
12
13
  declare const Checkbox: import("svelte").Component<$$ComponentProps, {}, "checked">;
@@ -1,4 +1,13 @@
1
1
  <script lang="ts">
2
+ import {
3
+ CONTROL_BASE,
4
+ FIELD_ERROR,
5
+ FIELD_HINT,
6
+ FIELD_LABEL,
7
+ FIELD_WRAP,
8
+ controlBorder,
9
+ describedBy,
10
+ } from '../internal/field.js';
2
11
  // DatePicker: a text trigger + calendar popover. Binds `value` to an ISO date
3
12
  // string ("YYYY-MM-DD"). Pure local-date math (no timezone surprises).
4
13
  let {
@@ -151,10 +160,10 @@
151
160
  });
152
161
  </script>
153
162
 
154
- <div class="flex flex-col gap-1.5 {cls}" bind:this={containerEl}>
163
+ <div class="{FIELD_WRAP} {cls}" bind:this={containerEl}>
155
164
  {#if label}
156
- <label for={id} class="text-sm font-medium text-fg">
157
- {label}{#if required}<span class="text-danger ml-0.5">*</span>{/if}
165
+ <label for={id} class={FIELD_LABEL}>
166
+ {label}{#if required}<span class="text-danger ml-0.5" aria-label="required">*</span>{/if}
158
167
  </label>
159
168
  {/if}
160
169
 
@@ -164,9 +173,8 @@
164
173
  {id}
165
174
  {disabled}
166
175
  onclick={() => (open ? (open = false) : openCal())}
167
- class="w-full flex items-center justify-between rounded-lg border bg-surface-2 px-3 py-2 text-sm
168
- transition-colors outline-none disabled:opacity-50 disabled:cursor-not-allowed
169
- {error ? 'border-danger focus:border-danger' : 'border-line focus:border-brand'}"
176
+ aria-describedby={describedBy(id, error, hint)}
177
+ class="{CONTROL_BASE} {controlBorder(!!error)} flex items-center justify-between text-left"
170
178
  >
171
179
  <span class={display ? 'text-fg' : 'text-faint'}>{display || placeholder}</span>
172
180
  <span class="text-faint shrink-0" aria-hidden="true">
@@ -200,7 +208,7 @@
200
208
  type="button"
201
209
  aria-label="Previous month"
202
210
  onclick={prevMonth}
203
- class="p-1.5 rounded-md text-muted hover:bg-surface-2 hover:text-fg transition-colors"
211
+ class="p-1.5 rounded-md text-muted hover:bg-surface-2 hover:text-fg transition-colors duration-150"
204
212
  >
205
213
  <svg width="14" height="14" viewBox="0 0 12 12" fill="none"
206
214
  ><path
@@ -217,7 +225,7 @@
217
225
  type="button"
218
226
  aria-label="Next month"
219
227
  onclick={nextMonth}
220
- class="p-1.5 rounded-md text-muted hover:bg-surface-2 hover:text-fg transition-colors"
228
+ class="p-1.5 rounded-md text-muted hover:bg-surface-2 hover:text-fg transition-colors duration-150"
221
229
  >
222
230
  <svg width="14" height="14" viewBox="0 0 12 12" fill="none"
223
231
  ><path
@@ -251,7 +259,7 @@
251
259
  onclick={() => pick(d)}
252
260
  aria-current={iso === todayISO ? 'date' : undefined}
253
261
  aria-label={iso}
254
- class="h-8 w-8 mx-auto flex items-center justify-center rounded-md text-sm transition-colors
262
+ class="h-8 w-8 mx-auto flex items-center justify-center rounded-md text-sm transition-colors duration-150
255
263
  disabled:opacity-30 disabled:cursor-not-allowed
256
264
  {iso === value
257
265
  ? 'bg-brand text-ink font-medium'
@@ -267,8 +275,8 @@
267
275
  </div>
268
276
 
269
277
  {#if error}
270
- <p class="text-xs text-danger">{error}</p>
278
+ <p id={id ? `${id}-error` : undefined} class={FIELD_ERROR}>{error}</p>
271
279
  {:else if hint}
272
- <p class="text-xs text-faint">{hint}</p>
280
+ <p id={id ? `${id}-hint` : undefined} class={FIELD_HINT}>{hint}</p>
273
281
  {/if}
274
282
  </div>
@@ -75,9 +75,20 @@
75
75
  <button
76
76
  type="button"
77
77
  onclick={close}
78
- class="-mt-0.5 text-xl leading-none text-faint transition-colors hover:text-fg"
79
- aria-label="Close">×</button
78
+ class="-mt-0.5 text-xl leading-none text-faint transition-colors duration-150 hover:text-fg"
79
+ aria-label="Close"
80
80
  >
81
+ <svg
82
+ width="16"
83
+ height="16"
84
+ viewBox="0 0 24 24"
85
+ fill="none"
86
+ stroke="currentColor"
87
+ stroke-width="2"
88
+ stroke-linecap="round"
89
+ aria-hidden="true"><path d="M18 6L6 18M6 6l12 12" /></svg
90
+ >
91
+ </button>
81
92
  </div>
82
93
  {/if}
83
94
 
@@ -70,7 +70,7 @@
70
70
  role="menuitem"
71
71
  disabled={item.disabled}
72
72
  onclick={() => handleItemClick(item)}
73
- class="w-full flex items-center gap-2.5 px-3 py-2 text-sm transition-colors
73
+ class="w-full flex items-center gap-2.5 px-3 py-2 text-sm transition-colors duration-150
74
74
  disabled:opacity-40 disabled:cursor-not-allowed
75
75
  {item.variant === 'danger'
76
76
  ? 'text-danger hover:bg-danger/10'
@@ -1,4 +1,11 @@
1
1
  <script lang="ts">
2
+ import {
3
+ FIELD_ERROR,
4
+ FIELD_HINT,
5
+ FIELD_LABEL,
6
+ FIELD_WRAP,
7
+ describedBy,
8
+ } from '../internal/field.js';
2
9
  interface Props {
3
10
  id?: string;
4
11
  name?: string;
@@ -39,15 +46,15 @@
39
46
  }
40
47
  </script>
41
48
 
42
- <div class="flex flex-col gap-1.5 {cls}">
49
+ <div class="{FIELD_WRAP} {cls}">
43
50
  {#if label}
44
- <label for={id} class="text-sm font-medium text-fg">{label}</label>
51
+ <label for={id} class={FIELD_LABEL}>{label}</label>
45
52
  {/if}
46
53
 
47
54
  <!-- svelte-ignore a11y_no_static_element_interactions -->
48
55
  <label
49
56
  class="flex w-full cursor-pointer flex-col items-center justify-center gap-2 rounded-xl
50
- border-2 border-dashed px-4 py-8 text-center transition-colors
57
+ border-2 border-dashed px-4 py-8 text-center transition-colors duration-150
51
58
  {error
52
59
  ? 'border-danger bg-danger/5 hover:bg-danger/8'
53
60
  : dragOver
@@ -90,13 +97,15 @@
90
97
  {multiple}
91
98
  {disabled}
92
99
  onchange={handleChange}
100
+ aria-invalid={error ? 'true' : undefined}
101
+ aria-describedby={describedBy(id, error, hint)}
93
102
  class="sr-only"
94
103
  />
95
104
  </label>
96
105
 
97
106
  {#if error}
98
- <p class="text-xs text-danger">{error}</p>
107
+ <p id={id ? `${id}-error` : undefined} class={FIELD_ERROR}>{error}</p>
99
108
  {:else if hint}
100
- <p class="text-xs text-faint">{hint}</p>
109
+ <p id={id ? `${id}-hint` : undefined} class={FIELD_HINT}>{hint}</p>
101
110
  {/if}
102
111
  </div>
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">
2
- type Tone = 'brand' | 'success' | 'warn' | 'danger' | 'neutral' | 'violet';
2
+ import type { AccentTone } from '../internal/tone.js';
3
3
  type Size = 'xs' | 'sm' | 'md' | 'lg';
4
4
 
5
5
  interface Props {
6
- tone?: Tone;
6
+ tone?: AccentTone;
7
7
  pulse?: boolean;
8
8
  size?: Size;
9
9
  class?: string;
@@ -11,7 +11,7 @@
11
11
 
12
12
  let { tone = 'success', pulse = false, size = 'sm', class: cls = '' }: Props = $props();
13
13
 
14
- const tones: Record<Tone, string> = {
14
+ const tones: Record<AccentTone, string> = {
15
15
  brand: 'bg-brand',
16
16
  success: 'bg-success',
17
17
  warn: 'bg-warn',
@@ -1,7 +1,7 @@
1
- type Tone = 'brand' | 'success' | 'warn' | 'danger' | 'neutral' | 'violet';
1
+ import type { AccentTone } from '../internal/tone.js';
2
2
  type Size = 'xs' | 'sm' | 'md' | 'lg';
3
3
  interface Props {
4
- tone?: Tone;
4
+ tone?: AccentTone;
5
5
  pulse?: boolean;
6
6
  size?: Size;
7
7
  class?: string;