@urbicon-ui/blocks 6.21.1 → 6.21.3

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 (78) hide show
  1. package/dist/components/NumberInput/NumberInput.svelte +197 -0
  2. package/dist/components/NumberInput/NumberInput.svelte.d.ts +4 -0
  3. package/dist/components/NumberInput/index.d.ts +55 -0
  4. package/dist/components/NumberInput/index.js +1 -0
  5. package/dist/components/index.d.ts +2 -0
  6. package/dist/components/index.js +1 -0
  7. package/dist/i18n/index.d.ts +6 -0
  8. package/dist/primitives/Avatar/avatar.variants.d.ts +6 -6
  9. package/dist/primitives/Badge/Badge.svelte +18 -8
  10. package/dist/primitives/Badge/index.d.ts +26 -6
  11. package/dist/primitives/Breadcrumb/Breadcrumb.svelte +11 -6
  12. package/dist/primitives/Breadcrumb/index.d.ts +2 -2
  13. package/dist/primitives/Button/button.variants.d.ts +4 -4
  14. package/dist/primitives/ButtonGroup/index.d.ts +1 -1
  15. package/dist/primitives/Card/card.variants.d.ts +5 -5
  16. package/dist/primitives/Checkbox/Checkbox.svelte +3 -3
  17. package/dist/primitives/Combobox/Combobox.svelte +478 -104
  18. package/dist/primitives/Combobox/combobox.variants.d.ts +89 -17
  19. package/dist/primitives/Combobox/combobox.variants.js +94 -13
  20. package/dist/primitives/Combobox/index.d.ts +166 -51
  21. package/dist/primitives/ConfirmDialog/index.d.ts +19 -0
  22. package/dist/primitives/Dialog/Dialog.svelte +74 -0
  23. package/dist/primitives/Dialog/dialog.variants.d.ts +9 -9
  24. package/dist/primitives/Dialog/index.d.ts +12 -1
  25. package/dist/primitives/Drawer/Drawer.svelte +2 -1
  26. package/dist/primitives/Drawer/drawer.variants.d.ts +8 -0
  27. package/dist/primitives/Drawer/drawer.variants.js +45 -10
  28. package/dist/primitives/Drawer/index.d.ts +13 -4
  29. package/dist/primitives/FormField/FormField.svelte +6 -6
  30. package/dist/primitives/FormField/index.d.ts +13 -4
  31. package/dist/primitives/Input/Input.svelte +8 -4
  32. package/dist/primitives/Input/input.variants.d.ts +9 -9
  33. package/dist/primitives/Menu/Menu.svelte +66 -4
  34. package/dist/primitives/Menu/index.d.ts +25 -0
  35. package/dist/primitives/Pagination/Pagination.svelte +28 -13
  36. package/dist/primitives/Pagination/index.d.ts +44 -0
  37. package/dist/primitives/Popover/Popover.svelte +34 -3
  38. package/dist/primitives/Popover/index.d.ts +3 -3
  39. package/dist/primitives/RadioGroup/RadioGroup.svelte +3 -3
  40. package/dist/primitives/Select/Select.svelte +21 -11
  41. package/dist/primitives/Select/index.d.ts +7 -0
  42. package/dist/primitives/Select/select.variants.d.ts +17 -17
  43. package/dist/primitives/Select/select.variants.js +18 -0
  44. package/dist/primitives/Skeleton/skeleton.variants.d.ts +3 -3
  45. package/dist/primitives/Slider/Slider.svelte +4 -8
  46. package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
  47. package/dist/primitives/Textarea/Textarea.svelte +13 -5
  48. package/dist/primitives/Textarea/textarea.variants.d.ts +7 -7
  49. package/dist/primitives/Textarea/textarea.variants.js +5 -2
  50. package/dist/primitives/Toast/Toaster.svelte +42 -2
  51. package/dist/primitives/Toast/index.d.ts +30 -0
  52. package/dist/primitives/Toast/toast.store.svelte.d.ts +25 -1
  53. package/dist/primitives/Toast/toast.store.svelte.js +91 -1
  54. package/dist/primitives/Toast/toast.variants.d.ts +18 -0
  55. package/dist/primitives/Toast/toast.variants.js +15 -0
  56. package/dist/primitives/Toggle/Toggle.svelte +17 -3
  57. package/dist/primitives/Toggle/index.d.ts +8 -2
  58. package/dist/primitives/Toggle/toggle.variants.d.ts +7 -0
  59. package/dist/primitives/Toggle/toggle.variants.js +26 -0
  60. package/dist/primitives/Toolbar/toolbar.variants.d.ts +6 -6
  61. package/dist/primitives/Tooltip/Tooltip.svelte +47 -21
  62. package/dist/primitives/Tooltip/Tooltip.svelte.d.ts +1 -1
  63. package/dist/primitives/Tooltip/index.d.ts +31 -3
  64. package/dist/primitives/Tooltip/tooltip.variants.d.ts +4 -4
  65. package/dist/primitives/Tooltip/tooltip.variants.js +2 -2
  66. package/dist/primitives/index.d.ts +3 -3
  67. package/dist/style/semantic.css +55 -29
  68. package/dist/translations/de.d.ts +3 -0
  69. package/dist/translations/de.js +4 -1
  70. package/dist/translations/en.d.ts +3 -0
  71. package/dist/translations/en.js +4 -1
  72. package/dist/utils/figma-token-export.d.ts +13 -0
  73. package/dist/utils/figma-token-export.js +99 -26
  74. package/dist/utils/guide.svelte.d.ts +8 -2
  75. package/dist/utils/guide.svelte.js +86 -5
  76. package/dist/utils/use-form-field.svelte.d.ts +9 -9
  77. package/dist/utils/use-form-field.svelte.js +12 -12
  78. package/package.json +3 -3
@@ -1,4 +1,4 @@
1
- import type { ToastData, ToastInput, ToastShorthandOpts } from './index.js';
1
+ import type { ToastData, ToastInput, ToastPromiseOptions, ToastShorthandOpts } from './index.js';
2
2
  import type { ToastPlacement } from './toast.variants.js';
3
3
  /**
4
4
  * Reactive store that manages the global toast queue.
@@ -24,6 +24,30 @@ declare class ToastStore {
24
24
  registerSubscriber(): () => void;
25
25
  /** Create a toast with full control over all options. Returns the toast ID for programmatic dismissal. */
26
26
  add(input: ToastInput): string;
27
+ /**
28
+ * Merge new fields into an existing toast in place — same id, same stack
29
+ * position — and reset its auto-dismiss timer to the new `duration`. No-op if
30
+ * the toast is gone. Backs `promise()`, which flips a pending toast to
31
+ * success/error without it sliding out and a new one flying in.
32
+ */
33
+ update(id: string, input: ToastInput): void;
34
+ /**
35
+ * Drive a toast through a promise's lifecycle (Sonner-style). Shows a
36
+ * persistent, non-dismissible spinner toast while pending, then flips it in
37
+ * place to success on resolve or danger on reject. `success`/`error` may be a
38
+ * plain title, a full {@link ToastInput}, or a function of the resolved value
39
+ * / rejection reason. Returns the toast id.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * toaster.promise(saveDraft(), {
44
+ * loading: 'Saving…',
45
+ * success: (draft) => `Saved “${draft.title}”`,
46
+ * error: (e) => `Could not save: ${e.message}`
47
+ * });
48
+ * ```
49
+ */
50
+ promise<T>(promise: Promise<T>, opts: ToastPromiseOptions<T>): string;
27
51
  /** Remove a single toast by ID. Clears its auto-dismiss timer. */
28
52
  dismiss(id: string): void;
29
53
  /** Remove all toasts and cancel their timers. */
@@ -50,7 +50,10 @@ class ToastStore {
50
50
  description: input.description,
51
51
  duration: input.duration ?? 5000,
52
52
  dismissible: input.dismissible ?? true,
53
- showProgress: input.showProgress ?? true
53
+ showProgress: input.showProgress ?? true,
54
+ action: input.action,
55
+ cancel: input.cancel,
56
+ loading: input.loading ?? false
54
57
  };
55
58
  this.toasts = [...this.toasts, toast];
56
59
  if (toast.duration > 0) {
@@ -58,6 +61,93 @@ class ToastStore {
58
61
  }
59
62
  return id;
60
63
  }
64
+ /**
65
+ * Merge new fields into an existing toast in place — same id, same stack
66
+ * position — and reset its auto-dismiss timer to the new `duration`. No-op if
67
+ * the toast is gone. Backs `promise()`, which flips a pending toast to
68
+ * success/error without it sliding out and a new one flying in.
69
+ */
70
+ update(id, input) {
71
+ const idx = this.toasts.findIndex((t) => t.id === id);
72
+ if (idx === -1)
73
+ return;
74
+ const next = { ...this.toasts[idx], ...input };
75
+ this.toasts = [...this.toasts.slice(0, idx), next, ...this.toasts.slice(idx + 1)];
76
+ const timer = this.timers.get(id);
77
+ if (timer) {
78
+ clearTimeout(timer);
79
+ this.timers.delete(id);
80
+ }
81
+ if (next.duration > 0) {
82
+ this.timers.set(id, setTimeout(() => this.dismiss(id), next.duration));
83
+ }
84
+ }
85
+ /**
86
+ * Drive a toast through a promise's lifecycle (Sonner-style). Shows a
87
+ * persistent, non-dismissible spinner toast while pending, then flips it in
88
+ * place to success on resolve or danger on reject. `success`/`error` may be a
89
+ * plain title, a full {@link ToastInput}, or a function of the resolved value
90
+ * / rejection reason. Returns the toast id.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * toaster.promise(saveDraft(), {
95
+ * loading: 'Saving…',
96
+ * success: (draft) => `Saved “${draft.title}”`,
97
+ * error: (e) => `Could not save: ${e.message}`
98
+ * });
99
+ * ```
100
+ */
101
+ promise(promise, opts) {
102
+ const loadingInput = typeof opts.loading === 'string' ? { title: opts.loading } : opts.loading;
103
+ const id = this.add({
104
+ intent: 'neutral',
105
+ ...loadingInput,
106
+ loading: true,
107
+ duration: 0,
108
+ dismissible: false,
109
+ showProgress: false
110
+ });
111
+ // Shared settle fields: leave the spinner/persistent/non-dismissible state
112
+ // and become a normal auto-dismissing toast. `description: undefined` clears
113
+ // the loading description unless the success/error config sets its own.
114
+ const settled = {
115
+ loading: false,
116
+ duration: 5000,
117
+ dismissible: true,
118
+ showProgress: true
119
+ };
120
+ // A throwing user formatter must not turn into an unhandled rejection on the
121
+ // internal `.then`; fall back to a bare settled toast (still flips out of the
122
+ // loading state) and dev-warn instead.
123
+ const resolveMessage = (fn, arg, label) => {
124
+ let out;
125
+ try {
126
+ out = typeof fn === 'function' ? fn(arg) : fn;
127
+ }
128
+ catch (err) {
129
+ if (import.meta.env?.DEV)
130
+ console.warn(`[Toast] promise ${label} formatter threw:`, err);
131
+ }
132
+ return typeof out === 'string' ? { title: out } : out;
133
+ };
134
+ promise.then((value) => {
135
+ this.update(id, {
136
+ intent: 'success',
137
+ description: undefined,
138
+ ...settled,
139
+ ...resolveMessage(opts.success, value, 'success')
140
+ });
141
+ }, (reason) => {
142
+ this.update(id, {
143
+ intent: 'danger',
144
+ description: undefined,
145
+ ...settled,
146
+ ...resolveMessage(opts.error, reason, 'error')
147
+ });
148
+ });
149
+ return id;
150
+ }
61
151
  /** Remove a single toast by ID. Clears its auto-dismiss timer. */
62
152
  dismiss(id) {
63
153
  const timer = this.timers.get(id);
@@ -39,6 +39,24 @@ export declare const toastVariants: ((props?: {
39
39
  } & {
40
40
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
41
41
  }) | undefined) => string;
42
+ actions: (props?: ({
43
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
44
+ placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
45
+ } & {
46
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
47
+ }) | undefined) => string;
48
+ actionButton: (props?: ({
49
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
50
+ placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
51
+ } & {
52
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
53
+ }) | undefined) => string;
54
+ cancelButton: (props?: ({
55
+ intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
56
+ placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
57
+ } & {
58
+ class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
59
+ }) | undefined) => string;
42
60
  dismissButton: (props?: ({
43
61
  intent?: "primary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
44
62
  placement?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | "top-center" | "bottom-center" | undefined;
@@ -16,6 +16,21 @@ export const toastVariants = tv({
16
16
  content: 'flex-1 min-w-0',
17
17
  title: 'font-semibold text-sm leading-tight',
18
18
  description: 'text-sm mt-0.5 opacity-80',
19
+ // Action row (Sonner-style). Intent-neutral, prominent primary + quiet
20
+ // cancel — the toast's intent already reads through the icon/progress, so
21
+ // the buttons stay contrast-based rather than intent-coloured.
22
+ actions: 'flex items-center gap-2 mt-2.5',
23
+ // tier: modify — small interactive sub-elements on a contain surface.
24
+ actionButton: [
25
+ 'rounded-modify px-2.5 py-1 text-xs font-medium',
26
+ 'bg-text-primary/10 hover:bg-text-primary/20 text-text-primary',
27
+ 'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/40'
28
+ ],
29
+ cancelButton: [
30
+ 'rounded-modify px-2.5 py-1 text-xs font-medium',
31
+ 'text-text-secondary hover:text-text-primary',
32
+ 'transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-current/40'
33
+ ],
19
34
  // tier: modify — small interactive sub-element on a contain surface.
20
35
  dismissButton: [
21
36
  'shrink-0 rounded-modify p-1',
@@ -11,6 +11,7 @@
11
11
  checked = $bindable(false),
12
12
  label,
13
13
  helper,
14
+ error,
14
15
  tier,
15
16
  size = 'md',
16
17
  intent = 'primary',
@@ -35,7 +36,8 @@
35
36
  const id = $derived(idProp ?? propsId);
36
37
  const ff = useFormField(() => ({
37
38
  fieldId: id,
38
- hint: helper,
39
+ helper,
40
+ error,
39
41
  required,
40
42
  disabled
41
43
  }));
@@ -61,6 +63,7 @@
61
63
  appearance,
62
64
  checked,
63
65
  disabled,
66
+ error: !!error,
64
67
  withBorder
65
68
  });
66
69
 
@@ -111,6 +114,7 @@
111
114
  aria-checked={checked}
112
115
  aria-label={label ? undefined : bt('accessibility.toggle') || 'Toggle'}
113
116
  aria-describedby={ff.describedBy}
117
+ aria-invalid={ff.invalid ? 'true' : undefined}
114
118
  onchange={handleChange}
115
119
  {...restProps}
116
120
  />
@@ -135,9 +139,19 @@
135
139
  {/if}
136
140
  </label>
137
141
 
138
- {#if ff.hintId}
142
+ {#if ff.errorId}
143
+ <div
144
+ id={ff.errorId}
145
+ class={unstyled
146
+ ? (slotClasses?.message ?? '')
147
+ : styles.message({ class: slotClasses?.message })}
148
+ role="alert"
149
+ >
150
+ {error}
151
+ </div>
152
+ {:else if ff.helperId}
139
153
  <div
140
- id={ff.hintId}
154
+ id={ff.helperId}
141
155
  class={unstyled
142
156
  ? (slotClasses?.message ?? '')
143
157
  : styles.message({ class: slotClasses?.message })}
@@ -24,13 +24,19 @@ import type { ToggleSlots, ToggleVariants } from './toggle.variants.js';
24
24
  * />
25
25
  * ```
26
26
  */
27
- export interface ToggleProps extends Omit<ToggleVariants, 'checked'>, Omit<HTMLInputAttributes, 'type' | 'size' | 'checked' | 'class' | 'children'> {
27
+ export interface ToggleProps extends Omit<ToggleVariants, 'checked' | 'error'>, Omit<HTMLInputAttributes, 'type' | 'size' | 'checked' | 'class' | 'children'> {
28
28
  /** Current on/off state. Supports two-way binding via `bind:checked`. */
29
29
  checked?: boolean;
30
30
  /** Text label displayed to the right of the toggle track. */
31
31
  label?: string;
32
- /** Hint text shown below the control. Useful for explaining side-effects of the toggle. */
32
+ /** Helper text shown below the control. Hidden when `error` is set. Useful for explaining side-effects of the toggle. */
33
33
  helper?: string;
34
+ /**
35
+ * Error message shown below the control. Replaces `helper`, flags the
36
+ * hidden input `aria-invalid`, and marks the unchecked track with the
37
+ * danger colour — e.g. for a consent toggle that must be switched on.
38
+ */
39
+ error?: string;
34
40
  /** Prevent interaction and dim the control. */
35
41
  disabled?: boolean;
36
42
  /** Mark the native input as required for form validation. */
@@ -6,6 +6,7 @@ export declare const toggleVariants: ((props?: {
6
6
  appearance?: "default" | "dot" | undefined;
7
7
  checked?: boolean | undefined;
8
8
  disabled?: boolean | undefined;
9
+ error?: boolean | undefined;
9
10
  withBorder?: boolean | undefined;
10
11
  } | undefined) => {
11
12
  wrapper: (props?: ({
@@ -15,6 +16,7 @@ export declare const toggleVariants: ((props?: {
15
16
  appearance?: "default" | "dot" | undefined;
16
17
  checked?: boolean | undefined;
17
18
  disabled?: boolean | undefined;
19
+ error?: boolean | undefined;
18
20
  withBorder?: boolean | undefined;
19
21
  } & {
20
22
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
@@ -26,6 +28,7 @@ export declare const toggleVariants: ((props?: {
26
28
  appearance?: "default" | "dot" | undefined;
27
29
  checked?: boolean | undefined;
28
30
  disabled?: boolean | undefined;
31
+ error?: boolean | undefined;
29
32
  withBorder?: boolean | undefined;
30
33
  } & {
31
34
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
@@ -37,6 +40,7 @@ export declare const toggleVariants: ((props?: {
37
40
  appearance?: "default" | "dot" | undefined;
38
41
  checked?: boolean | undefined;
39
42
  disabled?: boolean | undefined;
43
+ error?: boolean | undefined;
40
44
  withBorder?: boolean | undefined;
41
45
  } & {
42
46
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
@@ -48,6 +52,7 @@ export declare const toggleVariants: ((props?: {
48
52
  appearance?: "default" | "dot" | undefined;
49
53
  checked?: boolean | undefined;
50
54
  disabled?: boolean | undefined;
55
+ error?: boolean | undefined;
51
56
  withBorder?: boolean | undefined;
52
57
  } & {
53
58
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
@@ -59,6 +64,7 @@ export declare const toggleVariants: ((props?: {
59
64
  appearance?: "default" | "dot" | undefined;
60
65
  checked?: boolean | undefined;
61
66
  disabled?: boolean | undefined;
67
+ error?: boolean | undefined;
62
68
  withBorder?: boolean | undefined;
63
69
  } & {
64
70
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
@@ -70,6 +76,7 @@ export declare const toggleVariants: ((props?: {
70
76
  appearance?: "default" | "dot" | undefined;
71
77
  checked?: boolean | undefined;
72
78
  disabled?: boolean | undefined;
79
+ error?: boolean | undefined;
73
80
  withBorder?: boolean | undefined;
74
81
  } & {
75
82
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
@@ -86,6 +86,11 @@ export const toggleVariants = tv({
86
86
  control: 'opacity-50 cursor-not-allowed pointer-events-none'
87
87
  }
88
88
  },
89
+ error: {
90
+ true: {
91
+ message: 'text-danger'
92
+ }
93
+ },
89
94
  withBorder: {
90
95
  true: {},
91
96
  false: {}
@@ -153,6 +158,19 @@ export const toggleVariants = tv({
153
158
  intent: 'neutral',
154
159
  class: { track: 'bg-neutral border-neutral' }
155
160
  },
161
+ // ── Error state: danger boundary on the unchecked track (Switch-Pill) ──
162
+ // Mirrors Checkbox ("error overrides unchecked border"): the off state
163
+ // carries the danger mark, the on state keeps its intent colour — an
164
+ // error on a boolean control usually means "must be switched on".
165
+ // The dot-appearance twin lives at the END of the compound list so it
166
+ // folds over the dot-unchecked `border-border-default` (order is
167
+ // semantic — later compounds win conflicting buckets).
168
+ {
169
+ appearance: 'default',
170
+ error: true,
171
+ checked: false,
172
+ class: { track: 'border-danger peer-focus-visible:ring-danger/40' }
173
+ },
156
174
  // ── Thumb translation per size when checked (Switch-Pill only) ──
157
175
  { appearance: 'default', checked: true, size: 'xs', class: { thumb: 'translate-x-4' } },
158
176
  { appearance: 'default', checked: true, size: 'sm', class: { thumb: 'translate-x-5' } },
@@ -212,6 +230,13 @@ export const toggleVariants = tv({
212
230
  checked: true,
213
231
  intent: 'neutral',
214
232
  class: { track: 'border-neutral bg-neutral' }
233
+ },
234
+ // ── Error state, dot appearance (must fold over dot-unchecked border) ──
235
+ {
236
+ appearance: 'dot',
237
+ error: true,
238
+ checked: false,
239
+ class: { track: 'border-danger' }
215
240
  }
216
241
  ],
217
242
  defaultVariants: {
@@ -221,6 +246,7 @@ export const toggleVariants = tv({
221
246
  appearance: 'default',
222
247
  checked: false,
223
248
  disabled: false,
249
+ error: false,
224
250
  withBorder: false
225
251
  }
226
252
  });
@@ -1,15 +1,15 @@
1
1
  import { type SlotNames, type VariantProps } from '../../utils/variants.js';
2
2
  export declare const toolbarVariants: ((props?: {
3
- variant?: "ghost" | "elevated" | "outlined" | "quiet" | undefined;
3
+ variant?: "ghost" | "quiet" | "elevated" | "outlined" | undefined;
4
4
  orientation?: "horizontal" | "vertical" | undefined;
5
- gap?: "sm" | "md" | "lg" | "xl" | "xs" | undefined;
6
- padding?: "sm" | "md" | "lg" | "xl" | "xs" | undefined;
5
+ gap?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
6
+ padding?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
7
7
  } | undefined) => {
8
8
  base: (props?: ({
9
- variant?: "ghost" | "elevated" | "outlined" | "quiet" | undefined;
9
+ variant?: "ghost" | "quiet" | "elevated" | "outlined" | undefined;
10
10
  orientation?: "horizontal" | "vertical" | undefined;
11
- gap?: "sm" | "md" | "lg" | "xl" | "xs" | undefined;
12
- padding?: "sm" | "md" | "lg" | "xl" | "xs" | undefined;
11
+ gap?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
12
+ padding?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
13
13
  } & {
14
14
  class?: string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | (string | number | false | /*elided*/ any | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined)[] | Record<string, boolean | null | undefined> | null | undefined;
15
15
  }) | undefined) => string;
@@ -25,7 +25,8 @@
25
25
  transitionEasing,
26
26
  disabled = false,
27
27
  arrow = true,
28
- onVisibleChange,
28
+ open = $bindable(false),
29
+ onOpenChange,
29
30
  class: className,
30
31
  unstyled: unstyledProp = false,
31
32
  slotClasses: slotClassesProp = {},
@@ -43,7 +44,6 @@
43
44
  transitionDuration != null ? `${transitionDuration}ms` : undefined
44
45
  );
45
46
 
46
- let visible = $state(false);
47
47
  let triggerElement = $state<HTMLElement>();
48
48
  let tooltipElement = $state<HTMLElement>();
49
49
  let arrowElement = $state<HTMLElement>();
@@ -52,7 +52,7 @@
52
52
  const propsId = $props.id();
53
53
  const tooltipId = `tooltip-${propsId}`;
54
54
 
55
- const variantProps: VariantProps<typeof tooltipVariants> = $derived({ visible, intent, size });
55
+ const variantProps: VariantProps<typeof tooltipVariants> = $derived({ open, intent, size });
56
56
  const styles = $derived(tooltipVariants(variantProps));
57
57
  const slotClasses = $derived(
58
58
  resolveSlotClasses(blocksConfig, 'Tooltip', preset, variantProps, slotClassesProp)
@@ -60,16 +60,38 @@
60
60
 
61
61
  // Inside an open modal <dialog>, a popover shown via showPopover() forms a
62
62
  // second top-layer element WebKit won't render above the dialog (Codeberg
63
- // #23). Evaluated while `visible` so the check runs once the dialog is
63
+ // #23). Evaluated while `open` so the check runs once the dialog is
64
64
  // already modal; the tooltip then renders in place (position:fixed + the
65
- // opacity `visible` variant + pointer-events-none) within the dialog's own
65
+ // opacity `open` variant + pointer-events-none) within the dialog's own
66
66
  // top-layer subtree, no showPopover() needed.
67
- const topLayer = $derived(visible ? !isAnchoredInModalDialog(triggerElement) : true);
67
+ const topLayer = $derived(open ? !isAnchoredInModalDialog(triggerElement) : true);
68
68
 
69
69
  let showTimeout: number;
70
70
  let hideTimeout: number;
71
71
  let cleanup: (() => void) | undefined = undefined;
72
72
 
73
+ // Clear pending show/hide timers on unmount — a hover followed by a
74
+ // teardown within `showDelay` (route change, {#if} removal) would
75
+ // otherwise fire setOpen after destroy: a write to the destroyed
76
+ // $bindable plus a ghost onOpenChange(true) for a tooltip that no
77
+ // longer exists. Dependency-free effect → runs once, teardown on destroy.
78
+ $effect(() => {
79
+ return () => {
80
+ clearTimeout(showTimeout);
81
+ clearTimeout(hideTimeout);
82
+ };
83
+ });
84
+
85
+ // Single mutation point for interaction-driven changes, so `onOpenChange`
86
+ // fires exactly once per transition. The no-change guard keeps a re-hover
87
+ // during the hide delay quiet (open never flipped). Consumer writes via
88
+ // `bind:open` bypass this on purpose — they don't re-announce themselves.
89
+ function setOpen(next: boolean) {
90
+ if (open === next) return;
91
+ open = next;
92
+ onOpenChange?.(next);
93
+ }
94
+
73
95
  function show() {
74
96
  if (disabled) return;
75
97
  clearTimeout(hideTimeout);
@@ -80,24 +102,21 @@
80
102
  // call would briefly flash the tooltip before the effect tears it
81
103
  // back down.
82
104
  if (disabled) return;
83
- visible = true;
84
- onVisibleChange?.(true);
105
+ setOpen(true);
85
106
  }, showDelay);
86
107
  }
87
108
 
88
109
  function hide() {
89
110
  clearTimeout(showTimeout);
90
111
  hideTimeout = window.setTimeout(() => {
91
- visible = false;
92
- onVisibleChange?.(false);
112
+ setOpen(false);
93
113
  }, hideDelay);
94
114
  }
95
115
 
96
116
  function handleKeydown(e: KeyboardEvent) {
97
- if (e.key === 'Escape' && visible) {
117
+ if (e.key === 'Escape' && open) {
98
118
  clearTimeout(showTimeout);
99
- visible = false;
100
- onVisibleChange?.(false);
119
+ setOpen(false);
101
120
  }
102
121
  }
103
122
 
@@ -156,7 +175,7 @@
156
175
  });
157
176
  }
158
177
 
159
- // Drive native popover state from `visible`. `popover="manual"` puts the
178
+ // Drive native popover state from `open`. `popover="manual"` puts the
160
179
  // element in the browser top layer when shown, so the tooltip is never
161
180
  // clipped by ancestor `overflow` (e.g. Toolbar). We deliberately avoid
162
181
  // `popover="auto"` here — auto would dismiss the tooltip on any outside
@@ -170,13 +189,13 @@
170
189
  $effect(() => {
171
190
  if (!triggerElement || !tooltipElement) return;
172
191
 
173
- const shouldShow = !disabled && visible && !!label;
192
+ const shouldShow = !disabled && open && !!label;
174
193
 
175
194
  if (!shouldShow) {
176
- // Force-clear `visible` so a stale show timeout that fired right
195
+ // Force-clear `open` so a stale show timeout that fired right
177
196
  // before `disabled` flipped doesn't immediately re-trigger this
178
197
  // effect into the show branch on the next mouse event.
179
- if (disabled && visible) visible = false;
198
+ if (disabled && open) setOpen(false);
180
199
  cleanup?.();
181
200
  cleanup = undefined;
182
201
  if (tooltipElement.matches(':popover-open')) {
@@ -219,15 +238,22 @@
219
238
  </script>
220
239
 
221
240
  <!-- svelte-ignore a11y_no_static_element_interactions -->
241
+ <!--
242
+ focusin/focusout (not focus/blur): the wrapper span is never focusable
243
+ itself — the actual trigger is the consumer's child element. focus/blur
244
+ don't bubble, so a keyboard focus on the child never reached the wrapper
245
+ and the tooltip silently skipped its WCAG 1.4.13 focus-open path;
246
+ focusin/focusout bubble and cover any focusable descendant.
247
+ -->
222
248
  <span
223
249
  class="inline-flex"
224
250
  bind:this={triggerElement}
225
251
  onmouseenter={show}
226
252
  onmouseleave={hide}
227
- onfocus={show}
228
- onblur={hide}
253
+ onfocusin={show}
254
+ onfocusout={hide}
229
255
  onkeydown={handleKeydown}
230
- aria-describedby={!disabled && label && visible ? tooltipId : undefined}
256
+ aria-describedby={!disabled && label && open ? tooltipId : undefined}
231
257
  >
232
258
  {@render children()}
233
259
  </span>
@@ -239,7 +265,7 @@
239
265
  the arrow node existed). In top-layer mode the `popover="manual"` attribute
240
266
  hides it via the UA stylesheet until `showPopover()` is called; inside a
241
267
  modal dialog (`topLayer === false`) the attribute is dropped and the
242
- `visible` opacity variant drives visibility instead (Codeberg #23).
268
+ `open` opacity variant drives visibility instead (Codeberg #23).
243
269
 
244
270
  Load-bearing attributes (`popover`, `style`, `role`, `id`) intentionally
245
271
  follow `{...restProps}` so a consumer-supplied `popover="auto"`, custom
@@ -1,4 +1,4 @@
1
1
  import type { TooltipProps } from './index.js';
2
- declare const Tooltip: import("svelte").Component<TooltipProps, {}, "">;
2
+ declare const Tooltip: import("svelte").Component<TooltipProps, {}, "open">;
3
3
  type Tooltip = ReturnType<typeof Tooltip>;
4
4
  export default Tooltip;
@@ -4,7 +4,7 @@ import type { Placement } from '../../utils/floating.js';
4
4
  import type { TooltipSlots, TooltipVariants } from './tooltip.variants.js';
5
5
  /**
6
6
  * @description Contextual overlay that displays brief, supplementary text on hover or focus.
7
- * Built with Floating UI for precise positioning and accessibility support.
7
+ * Built on the library's own positioning engine for precise placement and accessibility support.
8
8
  *
9
9
  * @tag display
10
10
  * @related Popover
@@ -22,6 +22,22 @@ import type { TooltipSlots, TooltipVariants } from './tooltip.variants.js';
22
22
  * <Button variant="ghost" size="sm" onclick={handleCopy}>Copy</Button>
23
23
  * </Tooltip>
24
24
  * ```
25
+ *
26
+ * @example Controlled mode — programmatic "Copied!" feedback via `bind:open`
27
+ * ```svelte
28
+ * <script lang="ts">
29
+ * let copied = $state(false);
30
+ * async function copy() {
31
+ * await navigator.clipboard.writeText(text);
32
+ * copied = true;
33
+ * setTimeout(() => (copied = false), 1500);
34
+ * }
35
+ * </script>
36
+ *
37
+ * <Tooltip label="Copied!" bind:open={copied}>
38
+ * <Button onclick={copy}>Copy</Button>
39
+ * </Tooltip>
40
+ * ```
25
41
  */
26
42
  export interface TooltipProps extends TooltipVariants, Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
27
43
  /** Text displayed inside the tooltip bubble. */
@@ -52,8 +68,20 @@ export interface TooltipProps extends TooltipVariants, Omit<HTMLAttributes<HTMLD
52
68
  arrow?: boolean;
53
69
  /** Prevent the tooltip from appearing regardless of hover/focus. @default false */
54
70
  disabled?: boolean;
55
- /** Fires when tooltip visibility changes (hover in/out, Escape key). */
56
- onVisibleChange?: (visible: boolean) => void;
71
+ /**
72
+ * Controls tooltip visibility. Supports `bind:open` for programmatic
73
+ * display (e.g. a transient "Copied!" confirmation). Hover/focus keep
74
+ * driving the state in parallel — the delays apply only to those
75
+ * interaction paths, a `bind:open` write takes effect immediately.
76
+ * @default false
77
+ */
78
+ open?: boolean;
79
+ /**
80
+ * Fires when the tooltip opens or closes from user interaction (hover
81
+ * in/out, focus/blur, Escape). Receives the new open state. Not called
82
+ * when the consumer writes `bind:open` directly.
83
+ */
84
+ onOpenChange?: (open: boolean) => void;
57
85
  /** Remove default tv classes; only consumer classes apply. */
58
86
  unstyled?: boolean;
59
87
  /** Per-slot class overrides merged with (or replacing, when unstyled) tv styles. */