@lyeve-labs/ui-kit 0.9.1 → 0.10.0

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 +6 -5
  61. package/src/lib/styles/theme.css +30 -4
@@ -1,5 +1,14 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
+ import {
4
+ CONTROL_BASE,
5
+ FIELD_ERROR,
6
+ FIELD_HINT,
7
+ FIELD_LABEL,
8
+ FIELD_WRAP,
9
+ controlBorder,
10
+ describedBy,
11
+ } from '../internal/field.js';
3
12
 
4
13
  type SE = Event & { currentTarget: HTMLSelectElement };
5
14
 
@@ -7,6 +16,8 @@
7
16
  value,
8
17
  id,
9
18
  name,
19
+ label,
20
+ hint,
10
21
  required = false,
11
22
  disabled = false,
12
23
  error,
@@ -17,6 +28,8 @@
17
28
  value?: string | null;
18
29
  id?: string;
19
30
  name?: string;
31
+ label?: string;
32
+ hint?: string;
20
33
  required?: boolean;
21
34
  disabled?: boolean;
22
35
  error?: string;
@@ -25,26 +38,27 @@
25
38
  children?: Snippet;
26
39
  } = $props();
27
40
 
28
- const base =
29
- 'w-full bg-surface-2 rounded-lg px-3 py-2 text-sm text-fg ' +
30
- 'focus:outline-none transition-colors appearance-none cursor-pointer ' +
31
- 'disabled:opacity-50 disabled:cursor-not-allowed';
41
+ const fieldId = $derived(id ?? (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined));
32
42
  </script>
33
43
 
34
- <div class="flex flex-col gap-1 {cls}">
44
+ <div class="{FIELD_WRAP} {cls}">
45
+ {#if label}
46
+ <label for={fieldId} class={FIELD_LABEL}>
47
+ {label}{#if required}<span class="text-danger ml-0.5" aria-label="required">*</span>{/if}
48
+ </label>
49
+ {/if}
50
+
35
51
  <div class="relative">
36
52
  <select
37
- {id}
53
+ id={fieldId}
38
54
  {name}
39
55
  {required}
40
56
  {disabled}
41
57
  value={value ?? ''}
42
58
  {onchange}
43
- class="{base}
44
- {error
45
- ? 'border border-danger focus:border-danger/70'
46
- : 'border border-line focus:border-brand/50'}
47
- pr-8"
59
+ aria-invalid={error ? 'true' : undefined}
60
+ aria-describedby={describedBy(fieldId, error, hint)}
61
+ class="{CONTROL_BASE} {controlBorder(!!error)} appearance-none cursor-pointer pr-8"
48
62
  >
49
63
  {@render children?.()}
50
64
  </select>
@@ -63,7 +77,10 @@
63
77
  </svg>
64
78
  </span>
65
79
  </div>
80
+
66
81
  {#if error}
67
- <p class="text-xs text-danger">{error}</p>
82
+ <p id={fieldId ? `${fieldId}-error` : undefined} class={FIELD_ERROR}>{error}</p>
83
+ {:else if hint}
84
+ <p id={fieldId ? `${fieldId}-hint` : undefined} class={FIELD_HINT}>{hint}</p>
68
85
  {/if}
69
86
  </div>
@@ -6,6 +6,8 @@ type $$ComponentProps = {
6
6
  value?: string | null;
7
7
  id?: string;
8
8
  name?: string;
9
+ label?: string;
10
+ hint?: string;
9
11
  required?: boolean;
10
12
  disabled?: boolean;
11
13
  error?: string;
@@ -29,7 +29,7 @@
29
29
  ? ''
30
30
  : done || active
31
31
  ? 'bg-brand'
32
- : 'bg-line'} h-px transition-colors"
32
+ : 'bg-line'} h-px transition-colors duration-150"
33
33
  ></div>
34
34
 
35
35
  <!-- Step circle -->
@@ -62,7 +62,7 @@
62
62
  ? ''
63
63
  : done
64
64
  ? 'bg-brand'
65
- : 'bg-line'} h-px transition-colors"
65
+ : 'bg-line'} h-px transition-colors duration-150"
66
66
  ></div>
67
67
  </div>
68
68
 
@@ -72,7 +72,7 @@
72
72
  ? 'text-fg'
73
73
  : done
74
74
  ? 'text-muted'
75
- : 'text-faint'} transition-colors"
75
+ : 'text-faint'} transition-colors duration-150"
76
76
  >
77
77
  {step.label}
78
78
  </p>
@@ -117,7 +117,9 @@
117
117
  </div>
118
118
  {#if i < steps.length - 1}
119
119
  <div
120
- class="my-1 w-px flex-1 {done ? 'bg-brand/40' : 'bg-line'} transition-colors"
120
+ class="my-1 w-px flex-1 {done
121
+ ? 'bg-brand/40'
122
+ : 'bg-line'} transition-colors duration-150"
121
123
  ></div>
122
124
  {/if}
123
125
  </div>
@@ -127,7 +129,7 @@
127
129
  ? 'text-fg'
128
130
  : done
129
131
  ? 'text-muted'
130
- : 'text-faint'} transition-colors"
132
+ : 'text-faint'} transition-colors duration-150"
131
133
  >
132
134
  {step.label}
133
135
  </p>
@@ -29,7 +29,9 @@
29
29
  [&_tbody_td]:px-4 [&_tbody_td]:py-3 [&_tbody_td]:text-fg [&_tbody_td]:align-middle
30
30
  [&_tbody_tr]:border-b [&_tbody_tr]:border-line [&_tbody_tr:last-child]:border-0
31
31
  {striped ? '[&_tbody_tr:nth-child(even)]:bg-surface-2/40' : ''}
32
- {hoverable ? '[&_tbody_tr:hover]:bg-surface-2/60 [&_tbody_tr]:transition-colors' : ''}"
32
+ {hoverable
33
+ ? '[&_tbody_tr:hover]:bg-surface-2/60 [&_tbody_tr]:transition-colors duration-150'
34
+ : ''}"
33
35
  >
34
36
  {@render children()}
35
37
  </table>
@@ -19,7 +19,7 @@
19
19
  role="tab"
20
20
  aria-selected={active === tab.id}
21
21
  onclick={() => onchange(tab.id)}
22
- class="relative px-4 py-2.5 text-sm font-medium transition-colors whitespace-nowrap
22
+ class="relative px-4 py-2.5 text-sm font-medium transition-colors duration-150 whitespace-nowrap
23
23
  {active === tab.id
24
24
  ? 'text-fg after:absolute after:bottom-0 after:inset-x-0 after:h-0.5 after:bg-brand after:rounded-t'
25
25
  : 'text-faint hover:text-fg'}"
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">
2
- type Tone = 'neutral' | 'brand' | 'violet' | 'success' | 'warn' | 'danger';
2
+ import type { AccentTone } from '../internal/tone.js';
3
3
 
4
4
  interface Props {
5
5
  label: string;
6
- tone?: Tone;
6
+ tone?: AccentTone;
7
7
  removable?: boolean;
8
8
  onremove?: () => void;
9
9
  class?: string;
@@ -17,7 +17,7 @@
17
17
  class: cls = '',
18
18
  }: Props = $props();
19
19
 
20
- const tones: Record<Tone, string> = {
20
+ const tones: Record<AccentTone, string> = {
21
21
  neutral: 'bg-surface-2 text-muted border-line',
22
22
  brand: 'bg-brand/10 text-brand border-brand/20',
23
23
  violet: 'bg-violet/10 text-violet border-violet/20',
@@ -1,7 +1,7 @@
1
- type Tone = 'neutral' | 'brand' | 'violet' | 'success' | 'warn' | 'danger';
1
+ import type { AccentTone } from '../internal/tone.js';
2
2
  interface Props {
3
3
  label: string;
4
- tone?: Tone;
4
+ tone?: AccentTone;
5
5
  removable?: boolean;
6
6
  onremove?: () => void;
7
7
  class?: string;
@@ -1,4 +1,14 @@
1
1
  <script lang="ts">
2
+ import {
3
+ CONTROL_MULTILINE,
4
+ FIELD_ERROR,
5
+ FIELD_HINT,
6
+ FIELD_LABEL,
7
+ FIELD_WRAP,
8
+ controlBorder,
9
+ describedBy,
10
+ } from '../internal/field.js';
11
+
2
12
  type TA = HTMLTextAreaElement;
3
13
  type TAE = Event & { currentTarget: TA };
4
14
 
@@ -6,6 +16,7 @@
6
16
  value = $bindable(''),
7
17
  id,
8
18
  name,
19
+ label,
9
20
  placeholder,
10
21
  rows = 4,
11
22
  maxlength,
@@ -22,6 +33,7 @@
22
33
  value?: string;
23
34
  id?: string;
24
35
  name?: string;
36
+ label?: string;
25
37
  placeholder?: string;
26
38
  rows?: number;
27
39
  maxlength?: number;
@@ -36,11 +48,7 @@
36
48
  onblur?: (e: FocusEvent & { currentTarget: TA }) => void;
37
49
  } = $props();
38
50
 
39
- const base =
40
- 'w-full bg-surface-2 rounded-lg px-3 py-2 text-sm text-fg ' +
41
- 'focus:outline-none transition-colors ' +
42
- 'placeholder:text-faint ' +
43
- 'disabled:opacity-50 disabled:cursor-not-allowed';
51
+ const fieldId = $derived(id ?? (label ? label.toLowerCase().replace(/\s+/g, '-') : undefined));
44
52
 
45
53
  function handleInput(e: TAE) {
46
54
  value = e.currentTarget.value;
@@ -48,9 +56,15 @@
48
56
  }
49
57
  </script>
50
58
 
51
- <div class="flex flex-col gap-1 {cls}">
59
+ <div class="{FIELD_WRAP} {cls}">
60
+ {#if label}
61
+ <label for={fieldId} class={FIELD_LABEL}>
62
+ {label}{#if required}<span class="text-danger ml-0.5" aria-label="required">*</span>{/if}
63
+ </label>
64
+ {/if}
65
+
52
66
  <textarea
53
- {id}
67
+ id={fieldId}
54
68
  {name}
55
69
  {rows}
56
70
  {maxlength}
@@ -58,18 +72,16 @@
58
72
  {disabled}
59
73
  {readonly}
60
74
  {placeholder}
61
- class="{base}
62
- {error
63
- ? 'border border-danger focus:border-danger/70'
64
- : 'border border-line focus:border-brand/50'}
65
- {resize ? 'resize-y' : 'resize-none'}"
75
+ aria-invalid={error ? 'true' : undefined}
76
+ aria-describedby={describedBy(fieldId, error, hint)}
77
+ class="{CONTROL_MULTILINE} {controlBorder(!!error)} {resize ? 'resize-y' : 'resize-none'}"
66
78
  oninput={handleInput}
67
79
  {onblur}>{value}</textarea
68
80
  >
81
+
69
82
  {#if error}
70
- <p class="text-xs text-danger">{error}</p>
71
- {/if}
72
- {#if hint}
73
- <p class="text-xs text-faint">{hint}</p>
83
+ <p id={fieldId ? `${fieldId}-error` : undefined} class={FIELD_ERROR}>{error}</p>
84
+ {:else if hint}
85
+ <p id={fieldId ? `${fieldId}-hint` : undefined} class={FIELD_HINT}>{hint}</p>
74
86
  {/if}
75
87
  </div>
@@ -6,6 +6,7 @@ type $$ComponentProps = {
6
6
  value?: string;
7
7
  id?: string;
8
8
  name?: string;
9
+ label?: string;
9
10
  placeholder?: string;
10
11
  rows?: number;
11
12
  maxlength?: number;
@@ -23,7 +23,7 @@
23
23
  type="button"
24
24
  onclick={flip}
25
25
  aria-label={theme === 'dark' ? 'Switch to light mode' : 'Switch to dark mode'}
26
- class="rounded-lg p-2 text-faint transition-colors hover:bg-surface-2 hover:text-fg {klass}"
26
+ class="rounded-lg p-2 text-faint transition-colors duration-150 hover:bg-surface-2 hover:text-fg {klass}"
27
27
  >
28
28
  {#if theme === 'dark'}
29
29
  <Sun size={16} aria-hidden="true" />
@@ -1,11 +1,13 @@
1
1
  <script lang="ts">
2
- import { toast, type ToastTone } from '../stores/toast.svelte';
2
+ import { toast } from '../stores/toast.svelte';
3
+ import { TONE_GLYPH, statusTone, type StatusTone } from '../internal/tone.js';
3
4
 
4
- const styles: Record<ToastTone, { bar: string; icon: string; mark: string }> = {
5
- info: { bar: 'bg-brand', icon: 'text-brand', mark: 'ℹ' },
6
- success: { bar: 'bg-success', icon: 'text-success', mark: '✓' },
7
- warn: { bar: 'bg-warn', icon: 'text-warn', mark: '!' },
8
- danger: { bar: 'bg-danger', icon: 'text-danger', mark: '×' },
5
+ const styles: Record<StatusTone, { bar: string; icon: string }> = {
6
+ neutral: { bar: 'bg-muted', icon: 'text-muted' },
7
+ brand: { bar: 'bg-brand', icon: 'text-brand' },
8
+ success: { bar: 'bg-success', icon: 'text-success' },
9
+ warn: { bar: 'bg-warn', icon: 'text-warn' },
10
+ danger: { bar: 'bg-danger', icon: 'text-danger' },
9
11
  };
10
12
  </script>
11
13
 
@@ -18,23 +20,43 @@
18
20
  bg-surface pl-0 pr-3 py-3 shadow-xl animate-[toast-in_140ms_ease-out]"
19
21
  role="status"
20
22
  >
21
- <span class="w-1 self-stretch shrink-0 {styles[t.tone].bar}"></span>
23
+ <span class="w-1 self-stretch shrink-0 {styles[statusTone(t.tone)].bar}"></span>
22
24
  <span
23
- class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-current text-xs font-bold {styles[
24
- t.tone
25
+ class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-current {styles[
26
+ statusTone(t.tone)
25
27
  ].icon}"
26
28
  aria-hidden="true"
27
29
  >
28
- {styles[t.tone].mark}
30
+ <svg
31
+ width="11"
32
+ height="11"
33
+ viewBox="0 0 24 24"
34
+ fill="none"
35
+ stroke="currentColor"
36
+ stroke-width="2.5"
37
+ stroke-linecap="round"
38
+ stroke-linejoin="round"
39
+ >
40
+ <path d={TONE_GLYPH[statusTone(t.tone)]} />
41
+ </svg>
29
42
  </span>
30
43
  <p class="flex-1 text-sm text-fg leading-snug">{t.message}</p>
31
44
  <button
32
45
  type="button"
33
- class="text-faint hover:text-fg transition-colors text-lg leading-none -mt-0.5"
46
+ class="shrink-0 text-faint transition-colors duration-150 hover:text-fg"
34
47
  aria-label="Dismiss"
35
48
  onclick={() => toast.dismiss(t.id)}
36
49
  >
37
- ×
50
+ <svg
51
+ width="16"
52
+ height="16"
53
+ viewBox="0 0 24 24"
54
+ fill="none"
55
+ stroke="currentColor"
56
+ stroke-width="2"
57
+ stroke-linecap="round"
58
+ aria-hidden="true"><path d="M18 6L6 18M6 6l12 12" /></svg
59
+ >
38
60
  </button>
39
61
  </div>
40
62
  {/each}
@@ -1,10 +1,12 @@
1
1
  <script lang="ts">
2
+ import { FIELD_HINT } from '../internal/field.js';
2
3
  let {
3
4
  checked = $bindable(false),
4
5
  label,
5
6
  hint,
6
7
  disabled = false,
7
8
  size = 'md',
9
+ class: cls = '',
8
10
  onchange,
9
11
  }: {
10
12
  checked?: boolean;
@@ -12,6 +14,7 @@
12
14
  hint?: string;
13
15
  disabled?: boolean;
14
16
  size?: 'sm' | 'md';
17
+ class?: string;
15
18
  onchange?: (checked: boolean) => void;
16
19
  } = $props();
17
20
 
@@ -40,7 +43,7 @@
40
43
  <label
41
44
  class="inline-flex items-start gap-2.5 cursor-pointer select-none {disabled
42
45
  ? 'opacity-50 cursor-not-allowed'
43
- : ''}"
46
+ : ''} {cls}"
44
47
  >
45
48
  <button
46
49
  type="button"
@@ -49,7 +52,7 @@
49
52
  aria-label={label ?? 'Toggle'}
50
53
  {disabled}
51
54
  onclick={handleClick}
52
- class="relative shrink-0 rounded-full transition-colors focus:outline-none
55
+ class="relative shrink-0 rounded-full transition-colors duration-150 outline-none
53
56
  focus-visible:ring-2 focus-visible:ring-brand/50 focus-visible:ring-offset-1
54
57
  focus-visible:ring-offset-ink mt-0.5
55
58
  {track[size]}
@@ -67,7 +70,7 @@
67
70
  <span class="text-sm text-fg">{label}</span>
68
71
  {/if}
69
72
  {#if hint}
70
- <span class="text-xs text-faint">{hint}</span>
73
+ <span class={FIELD_HINT}>{hint}</span>
71
74
  {/if}
72
75
  </span>
73
76
  {/if}
@@ -4,6 +4,7 @@ type $$ComponentProps = {
4
4
  hint?: string;
5
5
  disabled?: boolean;
6
6
  size?: 'sm' | 'md';
7
+ class?: string;
7
8
  onchange?: (checked: boolean) => void;
8
9
  };
9
10
  declare const Toggle: import("svelte").Component<$$ComponentProps, {}, "checked">;
@@ -195,7 +195,7 @@
195
195
  {#if !entry.options.persistent}
196
196
  <button
197
197
  class="inline-flex items-center justify-center w-8 h-8 -mr-2 rounded-lg
198
- text-muted hover:text-fg hover:bg-surface-2 transition-colors shrink-0"
198
+ text-muted hover:text-fg hover:bg-surface-2 transition-colors duration-150 shrink-0"
199
199
  onclick={() => handleClose()}
200
200
  aria-label="Close"
201
201
  >
package/dist/index.d.ts CHANGED
@@ -60,4 +60,4 @@ export { openDialog, closeDialog, dismissDialog, dismissAllDialogs, confirm, set
60
60
  export type { DialogOptions, DialogEntry, DialogSize } from './components/dialog/types.js';
61
61
  export { cn, type ClassValue } from './utils/cn.js';
62
62
  export { getTheme, setTheme, toggleTheme, themeBootScript, type Theme } from './utils/theme.js';
63
- export declare const VERSION = "0.8.2";
63
+ export declare const VERSION = "0.10.0";
package/dist/index.js CHANGED
@@ -69,4 +69,6 @@ export { openDialog, closeDialog, dismissDialog, dismissAllDialogs, confirm, set
69
69
  export { cn } from './utils/cn.js';
70
70
  export { getTheme, setTheme, toggleTheme, themeBootScript } from './utils/theme.js';
71
71
  // ── Version ────────────────────────────────────────────────────────────────
72
- export const VERSION = '0.8.2';
72
+ // Generated from package.json by `pnpm version:sync`. Bump package.json, never
73
+ // this line; the build and the test suite fail when the two disagree.
74
+ export const VERSION = '0.10.0';
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The single source of truth for how a form control looks and announces itself.
3
+ *
4
+ * Every text-entry control in the library composes its classes from here rather
5
+ * than spelling them out inline. The nine controls had drifted into three focus
6
+ * treatments, two wrapper gaps, two error-border opacities and two heights, so
7
+ * an Input and a NumberInput placed side by side did not line up. Changing a
8
+ * control's appearance now means changing one of these constants.
9
+ *
10
+ * Not exported from the package entry point - this is an implementation detail.
11
+ */
12
+ /** Vertical rhythm inside a labelled field: label, control, hint/error. */
13
+ export declare const FIELD_WRAP = "flex flex-col gap-1.5";
14
+ /** The label above a control. */
15
+ export declare const FIELD_LABEL = "text-sm font-medium text-fg";
16
+ /** Hint text below a control. Shown only when there is no error. */
17
+ export declare const FIELD_HINT = "text-xs text-faint";
18
+ /** Error text below a control. Replaces the hint rather than stacking with it. */
19
+ export declare const FIELD_ERROR = "text-xs text-danger";
20
+ /**
21
+ * Everything a single-line control needs except its border colour.
22
+ *
23
+ * `h-control` is a theme token (2.375rem / 38px), not a literal, so the height
24
+ * is stated once. Controls that grow with their content - Textarea, the
25
+ * MultiSelect chip well - use `CONTROL_MULTILINE` instead and keep the token as
26
+ * a minimum.
27
+ */
28
+ export declare const CONTROL_BASE: string;
29
+ /** As CONTROL_BASE, for controls whose height is driven by their content. */
30
+ export declare const CONTROL_MULTILINE: string;
31
+ /**
32
+ * The border a control carries at rest and on focus.
33
+ *
34
+ * Focus moves the border to full-strength brand. Two controls previously used
35
+ * `border-brand/50`, which reads as a weaker focus for no reason a user could
36
+ * infer, and the error focus was split between `border-danger` and
37
+ * `border-danger/70`.
38
+ */
39
+ export declare function controlBorder(error: boolean): string;
40
+ /**
41
+ * Wires a control to whichever of its hint or error is on screen.
42
+ *
43
+ * Returns undefined when neither is present, so the attribute is omitted rather
44
+ * than pointing at an element that was never rendered. Only Input carried
45
+ * `aria-invalid` before this; nothing carried `aria-describedby`, so a screen
46
+ * reader announced the control and never the reason it was rejected.
47
+ */
48
+ export declare function describedBy(id: string | undefined, error?: string, hint?: string): string | undefined;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * The single source of truth for how a form control looks and announces itself.
3
+ *
4
+ * Every text-entry control in the library composes its classes from here rather
5
+ * than spelling them out inline. The nine controls had drifted into three focus
6
+ * treatments, two wrapper gaps, two error-border opacities and two heights, so
7
+ * an Input and a NumberInput placed side by side did not line up. Changing a
8
+ * control's appearance now means changing one of these constants.
9
+ *
10
+ * Not exported from the package entry point - this is an implementation detail.
11
+ */
12
+ /** Vertical rhythm inside a labelled field: label, control, hint/error. */
13
+ export const FIELD_WRAP = 'flex flex-col gap-1.5';
14
+ /** The label above a control. */
15
+ export const FIELD_LABEL = 'text-sm font-medium text-fg';
16
+ /** Hint text below a control. Shown only when there is no error. */
17
+ export const FIELD_HINT = 'text-xs text-faint';
18
+ /** Error text below a control. Replaces the hint rather than stacking with it. */
19
+ export const FIELD_ERROR = 'text-xs text-danger';
20
+ /**
21
+ * Everything a single-line control needs except its border colour.
22
+ *
23
+ * `h-control` is a theme token (2.375rem / 38px), not a literal, so the height
24
+ * is stated once. Controls that grow with their content - Textarea, the
25
+ * MultiSelect chip well - use `CONTROL_MULTILINE` instead and keep the token as
26
+ * a minimum.
27
+ */
28
+ export const CONTROL_BASE = 'w-full h-control rounded-lg bg-surface-2 border px-3 text-sm text-fg ' +
29
+ 'placeholder:text-faint transition-colors duration-150 outline-none ' +
30
+ 'disabled:opacity-50 disabled:cursor-not-allowed';
31
+ /** As CONTROL_BASE, for controls whose height is driven by their content. */
32
+ export const CONTROL_MULTILINE = 'w-full min-h-control rounded-lg bg-surface-2 border px-3 py-2 text-sm text-fg ' +
33
+ 'placeholder:text-faint transition-colors duration-150 outline-none ' +
34
+ 'disabled:opacity-50 disabled:cursor-not-allowed';
35
+ /**
36
+ * The border a control carries at rest and on focus.
37
+ *
38
+ * Focus moves the border to full-strength brand. Two controls previously used
39
+ * `border-brand/50`, which reads as a weaker focus for no reason a user could
40
+ * infer, and the error focus was split between `border-danger` and
41
+ * `border-danger/70`.
42
+ */
43
+ export function controlBorder(error) {
44
+ return error ? 'border-danger focus:border-danger' : 'border-line focus:border-brand';
45
+ }
46
+ /**
47
+ * Wires a control to whichever of its hint or error is on screen.
48
+ *
49
+ * Returns undefined when neither is present, so the attribute is omitted rather
50
+ * than pointing at an element that was never rendered. Only Input carried
51
+ * `aria-invalid` before this; nothing carried `aria-describedby`, so a screen
52
+ * reader announced the control and never the reason it was rejected.
53
+ */
54
+ export function describedBy(id, error, hint) {
55
+ if (!id)
56
+ return undefined;
57
+ if (error)
58
+ return `${id}-error`;
59
+ if (hint)
60
+ return `${id}-hint`;
61
+ return undefined;
62
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The tone vocabulary, stated once.
3
+ *
4
+ * Alert, Banner and the toast store each named their neutral-informational tone
5
+ * `info`, while Badge, Tag, Indicator and Progress named the same brand-cyan
6
+ * tone `brand`. A consumer building a status row had to remember which
7
+ * component wanted which word for the same colour. `brand` is canonical because
8
+ * it names the token the tone actually resolves to; `info` still works and maps
9
+ * onto it, so nothing that already ships has to change.
10
+ */
11
+ /** Tones a status surface can carry. */
12
+ export type StatusTone = 'neutral' | 'brand' | 'success' | 'warn' | 'danger';
13
+ /** What a status surface accepts, including the retained `info` spelling. */
14
+ export type StatusToneInput = StatusTone | 'info';
15
+ /** Status tones plus the decorative accent, for labels rather than states. */
16
+ export type AccentTone = StatusTone | 'violet';
17
+ /** Resolves the accepted spelling to the canonical one. */
18
+ export declare function statusTone(tone: StatusToneInput): StatusTone;
19
+ /**
20
+ * The glyph each status tone draws, as SVG path data on a 24x24 grid.
21
+ *
22
+ * Alert and Toaster drew these as the literal characters ℹ ✓ ! ×, which pick up
23
+ * whatever the user's font does with them - the check and the cross landed at
24
+ * different optical weights from every other icon in the library, all of which
25
+ * are stroked SVG.
26
+ */
27
+ export declare const TONE_GLYPH: Record<StatusTone, string>;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The tone vocabulary, stated once.
3
+ *
4
+ * Alert, Banner and the toast store each named their neutral-informational tone
5
+ * `info`, while Badge, Tag, Indicator and Progress named the same brand-cyan
6
+ * tone `brand`. A consumer building a status row had to remember which
7
+ * component wanted which word for the same colour. `brand` is canonical because
8
+ * it names the token the tone actually resolves to; `info` still works and maps
9
+ * onto it, so nothing that already ships has to change.
10
+ */
11
+ /** Resolves the accepted spelling to the canonical one. */
12
+ export function statusTone(tone) {
13
+ return tone === 'info' ? 'brand' : tone;
14
+ }
15
+ /**
16
+ * The glyph each status tone draws, as SVG path data on a 24x24 grid.
17
+ *
18
+ * Alert and Toaster drew these as the literal characters ℹ ✓ ! ×, which pick up
19
+ * whatever the user's font does with them - the check and the cross landed at
20
+ * different optical weights from every other icon in the library, all of which
21
+ * are stroked SVG.
22
+ */
23
+ export const TONE_GLYPH = {
24
+ neutral: 'M12 8h.01M11 12h1v4h1',
25
+ brand: 'M12 8h.01M11 12h1v4h1',
26
+ success: 'M20 6L9 17l-5-5',
27
+ warn: 'M12 8v5M12 17h.01',
28
+ danger: 'M18 6L6 18M6 6l12 12',
29
+ };
@@ -1,4 +1,9 @@
1
- export type ToastTone = 'info' | 'success' | 'warn' | 'danger';
1
+ import type { StatusToneInput } from '../internal/tone.js';
2
+ /**
3
+ * A toast carries the same tone vocabulary as Alert and Banner. `brand` is the
4
+ * canonical name; `info` is kept because `toast.info(...)` already ships.
5
+ */
6
+ export type ToastTone = StatusToneInput;
2
7
  export interface Toast {
3
8
  id: number;
4
9
  tone: ToastTone;
@@ -13,7 +13,7 @@ class ToastStore {
13
13
  this.items = this.items.filter((t) => t.id !== id);
14
14
  }
15
15
  info(message, timeout) {
16
- return this.push('info', message, timeout);
16
+ return this.push('brand', message, timeout);
17
17
  }
18
18
  success(message, timeout) {
19
19
  return this.push('success', message, timeout);