@urbicon-ui/blocks 6.21.1 → 6.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) 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/Badge/Badge.svelte +12 -4
  9. package/dist/primitives/Badge/index.d.ts +26 -6
  10. package/dist/primitives/Breadcrumb/Breadcrumb.svelte +11 -6
  11. package/dist/primitives/Breadcrumb/index.d.ts +2 -2
  12. package/dist/primitives/Checkbox/Checkbox.svelte +3 -3
  13. package/dist/primitives/Combobox/Combobox.svelte +478 -104
  14. package/dist/primitives/Combobox/combobox.variants.d.ts +89 -17
  15. package/dist/primitives/Combobox/combobox.variants.js +94 -13
  16. package/dist/primitives/Combobox/index.d.ts +166 -51
  17. package/dist/primitives/ConfirmDialog/index.d.ts +19 -0
  18. package/dist/primitives/Dialog/Dialog.svelte +74 -0
  19. package/dist/primitives/Dialog/index.d.ts +12 -1
  20. package/dist/primitives/Drawer/Drawer.svelte +2 -1
  21. package/dist/primitives/Drawer/drawer.variants.d.ts +8 -0
  22. package/dist/primitives/Drawer/drawer.variants.js +45 -10
  23. package/dist/primitives/Drawer/index.d.ts +13 -4
  24. package/dist/primitives/FormField/FormField.svelte +6 -6
  25. package/dist/primitives/FormField/index.d.ts +13 -4
  26. package/dist/primitives/Input/Input.svelte +8 -4
  27. package/dist/primitives/Menu/Menu.svelte +66 -4
  28. package/dist/primitives/Menu/index.d.ts +25 -0
  29. package/dist/primitives/Pagination/Pagination.svelte +28 -13
  30. package/dist/primitives/Pagination/index.d.ts +44 -0
  31. package/dist/primitives/Popover/Popover.svelte +10 -0
  32. package/dist/primitives/Popover/index.d.ts +3 -3
  33. package/dist/primitives/RadioGroup/RadioGroup.svelte +3 -3
  34. package/dist/primitives/Select/Select.svelte +21 -11
  35. package/dist/primitives/Select/index.d.ts +7 -0
  36. package/dist/primitives/Select/select.variants.d.ts +17 -17
  37. package/dist/primitives/Select/select.variants.js +18 -0
  38. package/dist/primitives/Slider/Slider.svelte +4 -8
  39. package/dist/primitives/Textarea/Textarea.svelte +4 -5
  40. package/dist/primitives/Textarea/textarea.variants.d.ts +7 -7
  41. package/dist/primitives/Textarea/textarea.variants.js +5 -2
  42. package/dist/primitives/Toast/Toaster.svelte +42 -2
  43. package/dist/primitives/Toast/index.d.ts +30 -0
  44. package/dist/primitives/Toast/toast.store.svelte.d.ts +25 -1
  45. package/dist/primitives/Toast/toast.store.svelte.js +91 -1
  46. package/dist/primitives/Toast/toast.variants.d.ts +18 -0
  47. package/dist/primitives/Toast/toast.variants.js +15 -0
  48. package/dist/primitives/Toggle/Toggle.svelte +17 -3
  49. package/dist/primitives/Toggle/index.d.ts +8 -2
  50. package/dist/primitives/Toggle/toggle.variants.d.ts +7 -0
  51. package/dist/primitives/Toggle/toggle.variants.js +26 -0
  52. package/dist/primitives/Tooltip/Tooltip.svelte +47 -21
  53. package/dist/primitives/Tooltip/Tooltip.svelte.d.ts +1 -1
  54. package/dist/primitives/Tooltip/index.d.ts +31 -3
  55. package/dist/primitives/Tooltip/tooltip.variants.d.ts +4 -4
  56. package/dist/primitives/Tooltip/tooltip.variants.js +2 -2
  57. package/dist/primitives/index.d.ts +3 -3
  58. package/dist/translations/de.d.ts +3 -0
  59. package/dist/translations/de.js +4 -1
  60. package/dist/translations/en.d.ts +3 -0
  61. package/dist/translations/en.js +4 -1
  62. package/dist/utils/guide.svelte.d.ts +8 -2
  63. package/dist/utils/guide.svelte.js +86 -5
  64. package/dist/utils/use-form-field.svelte.d.ts +9 -9
  65. package/dist/utils/use-form-field.svelte.js +12 -12
  66. package/package.json +3 -3
@@ -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. */
@@ -1,18 +1,18 @@
1
1
  import { type SlotNames, type VariantProps } from '../../utils/variants.js';
2
2
  export declare const tooltipVariants: ((props?: {
3
- visible?: boolean | undefined;
3
+ open?: boolean | undefined;
4
4
  intent?: "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
5
5
  size?: "sm" | "md" | "lg" | undefined;
6
6
  } | undefined) => {
7
7
  base: (props?: ({
8
- visible?: boolean | undefined;
8
+ open?: boolean | undefined;
9
9
  intent?: "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
10
10
  size?: "sm" | "md" | "lg" | undefined;
11
11
  } & {
12
12
  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;
13
13
  }) | undefined) => string;
14
14
  arrow: (props?: ({
15
- visible?: boolean | undefined;
15
+ open?: boolean | undefined;
16
16
  intent?: "primary" | "secondary" | "info" | "success" | "warning" | "danger" | "neutral" | undefined;
17
17
  size?: "sm" | "md" | "lg" | undefined;
18
18
  } & {
@@ -21,6 +21,6 @@ export declare const tooltipVariants: ((props?: {
21
21
  }) & {
22
22
  readonly config: import("../../utils/variants.js").TVConfig;
23
23
  };
24
- export type TooltipVariants = Omit<VariantProps<typeof tooltipVariants>, 'visible'>;
24
+ export type TooltipVariants = Omit<VariantProps<typeof tooltipVariants>, 'open'>;
25
25
  /** Slot names derived from the `tv()` config above — single source of truth for `slotClasses`. */
26
26
  export type TooltipSlots = SlotNames<typeof tooltipVariants>;
@@ -33,7 +33,7 @@ export const tooltipVariants = tv({
33
33
  arrow: ['absolute w-2 h-2', 'bg-inherit transform rotate-45']
34
34
  },
35
35
  variants: {
36
- visible: {
36
+ open: {
37
37
  true: { base: 'opacity-100' },
38
38
  false: { base: 'opacity-0' }
39
39
  },
@@ -53,7 +53,7 @@ export const tooltipVariants = tv({
53
53
  }
54
54
  },
55
55
  defaultVariants: {
56
- visible: false,
56
+ open: false,
57
57
  intent: 'neutral',
58
58
  size: 'md'
59
59
  }
@@ -18,7 +18,7 @@ export type { CheckboxProps } from './Checkbox/index.js';
18
18
  export * from './Checkbox/index.js';
19
19
  export type { CollapsibleProps } from './Collapsible/index.js';
20
20
  export * from './Collapsible/index.js';
21
- export type { ComboboxOptionType, ComboboxProps } from './Combobox/index.js';
21
+ export type { ComboboxGroup, ComboboxOptionType, ComboboxProps } from './Combobox/index.js';
22
22
  export * from './Combobox/index.js';
23
23
  export type { ConfirmDialogProps, ConfirmIntent } from './ConfirmDialog/index.js';
24
24
  export * from './ConfirmDialog/index.js';
@@ -34,7 +34,7 @@ export type { JourneyNode, JourneyStatus, JourneyTimelineProps } from './Journey
34
34
  export * from './JourneyTimeline/index.js';
35
35
  export type { MenuContext, MenuCustomSlots, MenuItemType, MenuObjectOption, MenuOption, MenuProps, MenuSectionHeader } from './Menu/index.js';
36
36
  export * from './Menu/index.js';
37
- export type { PaginationItemProps, PaginationProps } from './Pagination/index.js';
37
+ export type { PaginationItemContext, PaginationItemProps, PaginationProps } from './Pagination/index.js';
38
38
  export * from './Pagination/index.js';
39
39
  export * from './Popover/index.js';
40
40
  export type { ProgressProps } from './Progress/index.js';
@@ -61,7 +61,7 @@ export type { TabItemProps, TabPanelProps, TabProps } from './Tab/index.js';
61
61
  export * from './Tab/index.js';
62
62
  export type { TextareaProps } from './Textarea/index.js';
63
63
  export * from './Textarea/index.js';
64
- export type { ToastData, ToastInput, ToastIntent, ToastPlacement, ToastProps, ToastShorthandOpts } from './Toast/index.js';
64
+ export type { ToastAction, ToastData, ToastInput, ToastIntent, ToastPlacement, ToastPromiseOptions, ToastProps, ToastShorthandOpts } from './Toast/index.js';
65
65
  export * from './Toast/index.js';
66
66
  export { TOAST_INTENTS } from './Toast/index.js';
67
67
  export type { ToggleProps } from './Toggle/index.js';
@@ -1,6 +1,8 @@
1
1
  declare const _default: {
2
2
  readonly accessibility: {
3
3
  readonly avatar: "Avatar";
4
+ readonly breadcrumb: "Pfadnavigation";
5
+ readonly breadcrumbExpand: "Alle Pfadebenen anzeigen";
4
6
  readonly clearInput: "Eingabe löschen";
5
7
  readonly clearSearch: "Suche löschen";
6
8
  readonly clearSelection: "Auswahl löschen";
@@ -19,6 +21,7 @@ declare const _default: {
19
21
  readonly removableBadge: "Entfernbarer Badge";
20
22
  readonly removeBadge: "Badge entfernen";
21
23
  readonly removeFile: "{{name}} entfernen";
24
+ readonly removeTag: "{{label}} entfernen";
22
25
  };
23
26
  readonly button: {
24
27
  readonly close: "Schließen";
@@ -1,6 +1,8 @@
1
1
  export default {
2
2
  accessibility: {
3
3
  avatar: 'Avatar',
4
+ breadcrumb: 'Pfadnavigation',
5
+ breadcrumbExpand: 'Alle Pfadebenen anzeigen',
4
6
  clearInput: 'Eingabe löschen',
5
7
  clearSearch: 'Suche löschen',
6
8
  clearSelection: 'Auswahl löschen',
@@ -18,7 +20,8 @@ export default {
18
20
  toggleOptions: 'Optionen umschalten',
19
21
  removableBadge: 'Entfernbarer Badge',
20
22
  removeBadge: 'Badge entfernen',
21
- removeFile: '{{name}} entfernen'
23
+ removeFile: '{{name}} entfernen',
24
+ removeTag: '{{label}} entfernen'
22
25
  },
23
26
  button: {
24
27
  close: 'Schließen',
@@ -1,6 +1,8 @@
1
1
  declare const _default: {
2
2
  readonly accessibility: {
3
3
  readonly avatar: "Avatar";
4
+ readonly breadcrumb: "Breadcrumb";
5
+ readonly breadcrumbExpand: "Show all breadcrumb items";
4
6
  readonly clearInput: "Clear input";
5
7
  readonly clearSearch: "Clear search";
6
8
  readonly clearSelection: "Clear selection";
@@ -19,6 +21,7 @@ declare const _default: {
19
21
  readonly removableBadge: "Removable badge";
20
22
  readonly removeBadge: "Remove badge";
21
23
  readonly removeFile: "Remove {{name}}";
24
+ readonly removeTag: "Remove {{label}}";
22
25
  };
23
26
  readonly button: {
24
27
  readonly close: "Close";
@@ -1,6 +1,8 @@
1
1
  export default {
2
2
  accessibility: {
3
3
  avatar: 'Avatar',
4
+ breadcrumb: 'Breadcrumb',
5
+ breadcrumbExpand: 'Show all breadcrumb items',
4
6
  clearInput: 'Clear input',
5
7
  clearSearch: 'Clear search',
6
8
  clearSelection: 'Clear selection',
@@ -18,7 +20,8 @@ export default {
18
20
  toggleOptions: 'Toggle options',
19
21
  removableBadge: 'Removable badge',
20
22
  removeBadge: 'Remove badge',
21
- removeFile: 'Remove {{name}}'
23
+ removeFile: 'Remove {{name}}',
24
+ removeTag: 'Remove {{label}}'
22
25
  },
23
26
  button: {
24
27
  close: 'Close',
@@ -103,8 +103,14 @@ export interface GuideTour {
103
103
  * consumer can inject a server-state adapter. Kept deliberately tiny.
104
104
  */
105
105
  export interface GuideStorageAdapter {
106
- /** Returns the persisted set of seen/completed ids. */
107
- load(): string[];
106
+ /**
107
+ * Returns the persisted set of seen/completed ids. Return a `string[]`
108
+ * synchronously (localStorage, the default) or a `Promise<string[]>` for a
109
+ * DB-/remote-backed store. While an async load is pending the seen set starts
110
+ * empty and is merged (union) with the resolved ids once they arrive — so a
111
+ * tour may briefly be startable before the store answers.
112
+ */
113
+ load(): string[] | Promise<string[]>;
108
114
  /** Persists the full set of seen/completed ids. */
109
115
  save(ids: string[]): void;
110
116
  }
@@ -156,6 +156,22 @@ export class GuideController {
156
156
  #targets = new SvelteMap();
157
157
  /** Persisted "seen" ids. */
158
158
  #seen;
159
+ /** False only while an async `storage.load()` is still pending. */
160
+ #seenLoaded = true;
161
+ /**
162
+ * Set when the user writes (markSeen/resetSeen) while an async load is still
163
+ * pending, so the load's `.then` persists the merged union once — writing
164
+ * mid-load directly would clobber the remote store before its ids are merged in.
165
+ */
166
+ #pendingSeenWrite = false;
167
+ /**
168
+ * Ids the user forgot during the load window, so the merge doesn't resurrect
169
+ * them from the freshly-loaded remote set. A plain `Set` — pure bookkeeping,
170
+ * never rendered, so the `SvelteSet` rule doesn't apply.
171
+ */
172
+ #pendingRemovals = new Set();
173
+ /** `resetSeen()` (clear-all) during the load window: drop the remote set entirely. */
174
+ #pendingClearAll = false;
159
175
  #storage;
160
176
  #overlayStack;
161
177
  #dev;
@@ -209,7 +225,58 @@ export class GuideController {
209
225
  // `?? false` keeps `#dev` a strict boolean and means non-Vite consumers
210
226
  // simply get no dev warnings (rather than a crash).
211
227
  this.#dev = options.dev ?? import.meta.env?.DEV ?? false;
212
- this.#seen = new SvelteSet(this.#storage.load());
228
+ // Sync adapters (localStorage) seed the set immediately. Async adapters
229
+ // (DB/remote) start empty and get their ids merged in when the promise
230
+ // settles — union, not replace, so anything marked seen in the meantime
231
+ // survives. A rejected load is swallowed (dev-warned): the tour just stays
232
+ // "unseen", which fails open rather than hiding help behind a broken store.
233
+ const loaded = this.#storage.load();
234
+ if (Array.isArray(loaded)) {
235
+ this.#seen = new SvelteSet(loaded);
236
+ }
237
+ else {
238
+ this.#seen = new SvelteSet();
239
+ this.#seenLoaded = false;
240
+ loaded
241
+ .then((ids) => {
242
+ // Merge the loaded set in, but honour writes made during the window:
243
+ // skip everything if the user cleared all, and don't resurrect ids the
244
+ // user explicitly forgot. Adds are safe (union is idempotent).
245
+ if (!this.#pendingClearAll) {
246
+ for (const id of ids) {
247
+ if (!this.#pendingRemovals.has(id))
248
+ this.#seen.add(id);
249
+ }
250
+ }
251
+ this.#seenLoaded = true;
252
+ // Reconcile persistence: if the user wrote during the window, the store
253
+ // still holds the pre-merge state (we deliberately didn't save then), so
254
+ // persist the union now. Nothing wrote → leave the store untouched (no
255
+ // write-amplification of a remote we just read).
256
+ if (this.#pendingSeenWrite)
257
+ this.#storage.save([...this.#seen]);
258
+ })
259
+ .catch((err) => {
260
+ // Read failed: fail open (tour stays "unseen"). We don't persist the
261
+ // window's in-memory writes here — the store is in an unknown state, and
262
+ // blindly saving could destroy data we simply couldn't read ("read
263
+ // tolerant, write strict"). Later writes persist normally.
264
+ this.#seenLoaded = true;
265
+ if (this.#dev)
266
+ console.warn('[Guide] storage.load() rejected:', err);
267
+ });
268
+ }
269
+ }
270
+ /**
271
+ * Persist the seen-set — unless an async load is still pending, in which case a
272
+ * direct save would clobber the not-yet-loaded remote store. Defer to the load's
273
+ * `.then` reconciliation instead (see the constructor).
274
+ */
275
+ #persistSeen() {
276
+ if (this.#seenLoaded)
277
+ this.#storage.save([...this.#seen]);
278
+ else
279
+ this.#pendingSeenWrite = true;
213
280
  }
214
281
  // ─── Target registry ──────────────────────────────────────────────────────
215
282
  /**
@@ -738,15 +805,29 @@ export class GuideController {
738
805
  if (this.#seen.has(id))
739
806
  return;
740
807
  this.#seen.add(id);
741
- this.#storage.save([...this.#seen]);
808
+ // Re-seeing an id undoes a forget that's still pending reconciliation.
809
+ if (!this.#seenLoaded)
810
+ this.#pendingRemovals.delete(id);
811
+ this.#persistSeen();
742
812
  }
743
813
  /** Forget one id, or all of them when called without an argument. */
744
814
  resetSeen(id) {
745
- if (id === undefined)
815
+ if (id === undefined) {
746
816
  this.#seen.clear();
747
- else
817
+ // Clear-all during a pending load must survive the merge: drop the whole
818
+ // remote set, superseding any earlier pending removals.
819
+ if (!this.#seenLoaded) {
820
+ this.#pendingClearAll = true;
821
+ this.#pendingRemovals.clear();
822
+ }
823
+ }
824
+ else {
748
825
  this.#seen.delete(id);
749
- this.#storage.save([...this.#seen]);
826
+ // Record the forget so the pending-load merge doesn't resurrect it.
827
+ if (!this.#seenLoaded)
828
+ this.#pendingRemovals.add(id);
829
+ }
830
+ this.#persistSeen();
750
831
  }
751
832
  /** Snapshot of all seen ids. */
752
833
  get seenIds() {
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * `useFormField` — Single source of truth for the ARIA wiring that every
3
- * Form primitive duplicates: error/hint id derivation, `aria-describedby`,
4
- * `aria-invalid`, and the mutually-exclusive error-over-hint convention.
3
+ * Form primitive duplicates: error/helper id derivation, `aria-describedby`,
4
+ * `aria-invalid`, and the mutually-exclusive error-over-helper convention.
5
5
  *
6
- * Layout (label position, error/hint placement, required marker) stays
6
+ * Layout (label position, error/helper placement, required marker) stays
7
7
  * component-specific because real Form primitives differ in shape — Input
8
8
  * stacks the label on top, Checkbox/Toggle put it inline, RadioGroup and
9
9
  * SegmentGroup have a group caption + per-item labels. The hook does not
@@ -23,11 +23,11 @@
23
23
  * ```svelte
24
24
  * <script lang="ts">
25
25
  * import { useFormField } from './index.js';
26
- * let { id: idProp, hint, error, required, disabled, label } = $props();
26
+ * let { id: idProp, helper, error, required, disabled, label } = $props();
27
27
  * const propsId = $props.id();
28
28
  * const ff = useFormField(() => ({
29
29
  * fieldId: idProp ?? `field-${propsId}`,
30
- * hint,
30
+ * helper,
31
31
  * error,
32
32
  * required,
33
33
  * disabled
@@ -47,8 +47,8 @@ export interface UseFormFieldInputs {
47
47
  /** The DOM id to apply to the field element. Compute once in the caller via `$props.id()`. */
48
48
  fieldId: string;
49
49
  /** Helper text shown below the field when no error is set. */
50
- hint?: string;
51
- /** Error message — when truthy, replaces `hint` and flags the field invalid. */
50
+ helper?: string;
51
+ /** Error message — when truthy, replaces `helper` and flags the field invalid. */
52
52
  error?: string;
53
53
  /** Required flag — exposed verbatim for the caller's label markup. */
54
54
  required?: boolean;
@@ -60,8 +60,8 @@ export interface UseFormFieldReturn {
60
60
  readonly fieldId: string;
61
61
  /** Id of the error description, or undefined when no error is set. */
62
62
  readonly errorId: string | undefined;
63
- /** Id of the hint description, or undefined when error is set or no hint is given. */
64
- readonly hintId: string | undefined;
63
+ /** Id of the helper description, or undefined when error is set or no helper is given. */
64
+ readonly helperId: string | undefined;
65
65
  /** Concatenation suitable for `aria-describedby` (`undefined` when both are unset). */
66
66
  readonly describedBy: string | undefined;
67
67
  /** `true` iff `error` is truthy. Use for `aria-invalid={ff.invalid || undefined}`. */
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * `useFormField` — Single source of truth for the ARIA wiring that every
3
- * Form primitive duplicates: error/hint id derivation, `aria-describedby`,
4
- * `aria-invalid`, and the mutually-exclusive error-over-hint convention.
3
+ * Form primitive duplicates: error/helper id derivation, `aria-describedby`,
4
+ * `aria-invalid`, and the mutually-exclusive error-over-helper convention.
5
5
  *
6
- * Layout (label position, error/hint placement, required marker) stays
6
+ * Layout (label position, error/helper placement, required marker) stays
7
7
  * component-specific because real Form primitives differ in shape — Input
8
8
  * stacks the label on top, Checkbox/Toggle put it inline, RadioGroup and
9
9
  * SegmentGroup have a group caption + per-item labels. The hook does not
@@ -23,11 +23,11 @@
23
23
  * ```svelte
24
24
  * <script lang="ts">
25
25
  * import { useFormField } from './index.js';
26
- * let { id: idProp, hint, error, required, disabled, label } = $props();
26
+ * let { id: idProp, helper, error, required, disabled, label } = $props();
27
27
  * const propsId = $props.id();
28
28
  * const ff = useFormField(() => ({
29
29
  * fieldId: idProp ?? `field-${propsId}`,
30
- * hint,
30
+ * helper,
31
31
  * error,
32
32
  * required,
33
33
  * disabled
@@ -51,20 +51,20 @@
51
51
  export function computeFormFieldAria(input) {
52
52
  const fieldId = input.fieldId;
53
53
  const errorId = input.error ? `${fieldId}-error` : undefined;
54
- // Hint is suppressed when an error is present — the error message is
54
+ // Helper is suppressed when an error is present — the error message is
55
55
  // the more important description at that point. This is the same
56
56
  // exclusive convention as Material / Carbon / Polaris.
57
- const hintId = !input.error && input.hint ? `${fieldId}-hint` : undefined;
58
- // Error first, hint second — error is the more semantically urgent
57
+ const helperId = !input.error && input.helper ? `${fieldId}-helper` : undefined;
58
+ // Error first, helper second — error is the more semantically urgent
59
59
  // description and should reach assistive tech first.
60
- const describedBy = [errorId, hintId].filter(Boolean).join(' ') || undefined;
60
+ const describedBy = [errorId, helperId].filter(Boolean).join(' ') || undefined;
61
61
  const invalid = !!input.error;
62
62
  const required = !!input.required;
63
63
  const disabled = !!input.disabled;
64
64
  return {
65
65
  fieldId,
66
66
  errorId,
67
- hintId,
67
+ helperId,
68
68
  describedBy,
69
69
  invalid,
70
70
  required,
@@ -80,8 +80,8 @@ export function useFormField(inputs) {
80
80
  get errorId() {
81
81
  return result.errorId;
82
82
  },
83
- get hintId() {
84
- return result.hintId;
83
+ get helperId() {
84
+ return result.helperId;
85
85
  },
86
86
  get describedBy() {
87
87
  return result.describedBy;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@urbicon-ui/blocks",
3
- "version": "6.21.1",
3
+ "version": "6.21.2",
4
4
  "description": "Svelte 5 UI component library with Tailwind CSS 4, OKLCH design tokens and zero runtime dependencies",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -91,8 +91,8 @@
91
91
  "@sveltejs/package": "^2.5.8",
92
92
  "@sveltejs/vite-plugin-svelte": "^7.0.0",
93
93
  "@tailwindcss/vite": "^4.3.1",
94
- "@urbicon-ui/i18n": "6.21.1",
95
- "@urbicon-ui/shared-types": "6.21.1",
94
+ "@urbicon-ui/i18n": "6.21.2",
95
+ "@urbicon-ui/shared-types": "6.21.2",
96
96
  "prettier": "^3.8.4",
97
97
  "prettier-plugin-svelte": "^4.1.1",
98
98
  "prettier-plugin-tailwindcss": "^0.8.0",