@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
@@ -0,0 +1,197 @@
1
+ <script lang="ts">
2
+ import { Input } from '../../primitives/Input';
3
+ import { resolveIcon } from '../../icons';
4
+ import ChevronDownIconDefault from '../../icons/ChevronDownIcon.svelte';
5
+ import type { NumberInputProps } from './index';
6
+
7
+ const ChevronIcon = resolveIcon('chevronDown', ChevronDownIconDefault);
8
+
9
+ let {
10
+ value = $bindable(null),
11
+ min,
12
+ max,
13
+ step = 1,
14
+ precision,
15
+ hideStepper = false,
16
+ disabled = false,
17
+ readonly = false,
18
+ name,
19
+ onValueChange,
20
+ rightIcon: userRightIcon,
21
+ ...inputProps
22
+ }: NumberInputProps = $props();
23
+
24
+ // Tracks focus so the wheel handler only steers a focused field (scrolling the
25
+ // page over an unfocused one must not change it). Set by the input's focus/blur.
26
+ let focused = $state(false);
27
+
28
+ // Display mirrors `value` (derived) but is overridden with the raw text while
29
+ // typing, so intermediate states — a trailing separator, a lone leading `-` —
30
+ // survive until blur. Same override pattern as CurrencyInput.
31
+ let display = $derived(value == null ? '' : formatNumber(value));
32
+
33
+ function decimalsOf(n: number): number {
34
+ if (!Number.isFinite(n)) return 0;
35
+ const s = String(n);
36
+ // Exponential notation (e.g. `1e-7` → "1e-7") carries no literal `.`; derive
37
+ // the decimal count from the exponent plus any mantissa fraction so a tiny
38
+ // step isn't silently rounded down to 0 decimals (which would swallow it).
39
+ const e = s.indexOf('e');
40
+ if (e !== -1) {
41
+ const mantissa = s.slice(0, e);
42
+ const exp = Number(s.slice(e + 1));
43
+ const dot = mantissa.indexOf('.');
44
+ const mantissaDecimals = dot === -1 ? 0 : mantissa.length - dot - 1;
45
+ return Math.max(0, mantissaDecimals - exp);
46
+ }
47
+ const dot = s.indexOf('.');
48
+ return dot === -1 ? 0 : s.length - dot - 1;
49
+ }
50
+
51
+ function roundTo(v: number, decimals: number): number {
52
+ const factor = 10 ** decimals;
53
+ return Math.round(v * factor) / factor;
54
+ }
55
+
56
+ function formatNumber(v: number): string {
57
+ return precision != null ? v.toFixed(precision) : String(v);
58
+ }
59
+
60
+ function parseNumber(raw: string): number | null {
61
+ // Accept a comma as the decimal separator so `1,5` works under DE keyboards.
62
+ const t = raw.trim().replace(',', '.');
63
+ if (t === '') return null;
64
+ const n = Number(t);
65
+ // NaN (e.g. a lone `-` mid-compose, or junk) leaves the value untouched so
66
+ // the raw text can stay in the field until it parses or the user blurs.
67
+ return Number.isFinite(n) ? n : (value ?? null);
68
+ }
69
+
70
+ function clampValue(v: number | null): number | null {
71
+ if (v == null) return null;
72
+ let r = v;
73
+ if (min != null && r < min) r = min;
74
+ if (max != null && r > max) r = max;
75
+ if (precision != null) r = roundTo(r, precision);
76
+ return r;
77
+ }
78
+
79
+ function commit(next: number | null) {
80
+ if (next !== value) {
81
+ value = next;
82
+ onValueChange?.(next);
83
+ }
84
+ display = next == null ? '' : formatNumber(next);
85
+ }
86
+
87
+ function stepBy(dir: 1 | -1) {
88
+ if (disabled || readonly) return;
89
+ // Round on the step's own scale so 0.1 + 0.2 lands on 0.3, not 0.30000000004.
90
+ const decimals = precision ?? decimalsOf(step);
91
+ // From an empty field, the first step lands *on* the near bound (min for ↑,
92
+ // max for ↓) when one is set, instead of overshooting it by a full step.
93
+ if (value == null) {
94
+ const bound = dir === 1 ? min : max;
95
+ commit(clampValue(roundTo(bound ?? dir * step, decimals)));
96
+ return;
97
+ }
98
+ commit(clampValue(roundTo(value + dir * step, decimals)));
99
+ }
100
+
101
+ function handleInput(event: Event) {
102
+ const raw = (event.currentTarget as HTMLInputElement).value;
103
+ const parsed = parseNumber(raw);
104
+ if (parsed !== value) {
105
+ value = parsed;
106
+ onValueChange?.(parsed);
107
+ }
108
+ display = raw; // keep the raw text; clamp/format happens on blur
109
+ }
110
+
111
+ function handleFocus() {
112
+ focused = true;
113
+ }
114
+
115
+ function handleBlur() {
116
+ focused = false;
117
+ commit(clampValue(value));
118
+ }
119
+
120
+ function handleKeydown(event: KeyboardEvent) {
121
+ // Bail before preventDefault so a disabled/readonly field keeps native caret
122
+ // motion (ArrowUp/Down jump to start/end) instead of swallowing the keys.
123
+ if (disabled || readonly) return;
124
+ if (event.key === 'ArrowUp') {
125
+ event.preventDefault();
126
+ stepBy(1);
127
+ } else if (event.key === 'ArrowDown') {
128
+ event.preventDefault();
129
+ stepBy(-1);
130
+ }
131
+ }
132
+
133
+ function handleWheel(event: WheelEvent) {
134
+ // Only steer the value when the field is focused, so scrolling the page with
135
+ // the pointer over an unfocused field doesn't silently change it. Bail on
136
+ // disabled/readonly before preventDefault so page scroll isn't blocked either.
137
+ if (disabled || readonly || !focused) return;
138
+ event.preventDefault();
139
+ stepBy(event.deltaY < 0 ? 1 : -1);
140
+ }
141
+
142
+ const atMin = $derived(min != null && value != null && value <= min);
143
+ const atMax = $derived(max != null && value != null && value >= max);
144
+ </script>
145
+
146
+ {#snippet stepper()}
147
+ <!-- Re-enable pointer events: Input renders a right-side snippet inside a
148
+ `pointer-events-none` decoration container (input.variants.ts), so without
149
+ this the stepper buttons would be dead to mouse clicks in a real browser. -->
150
+ <span class="pointer-events-auto -my-1 flex flex-col justify-center">
151
+ <button
152
+ type="button"
153
+ class="text-text-tertiary hover:text-text-primary flex items-center justify-center px-0.5 transition-colors disabled:pointer-events-none disabled:opacity-30 focus-visible:outline-none"
154
+ tabindex={-1}
155
+ aria-hidden="true"
156
+ disabled={disabled || readonly || atMax}
157
+ onmousedown={(e) => e.preventDefault()}
158
+ onclick={() => stepBy(1)}
159
+ >
160
+ <ChevronIcon class="h-3.5 w-3.5 rotate-180" />
161
+ </button>
162
+ <button
163
+ type="button"
164
+ class="text-text-tertiary hover:text-text-primary flex items-center justify-center px-0.5 transition-colors disabled:pointer-events-none disabled:opacity-30 focus-visible:outline-none"
165
+ tabindex={-1}
166
+ aria-hidden="true"
167
+ disabled={disabled || readonly || atMin}
168
+ onmousedown={(e) => e.preventDefault()}
169
+ onclick={() => stepBy(-1)}
170
+ >
171
+ <ChevronIcon class="h-3.5 w-3.5" />
172
+ </button>
173
+ </span>
174
+ {/snippet}
175
+
176
+ <Input
177
+ {...inputProps}
178
+ {disabled}
179
+ {readonly}
180
+ type="text"
181
+ inputmode="decimal"
182
+ role="spinbutton"
183
+ aria-valuenow={value ?? undefined}
184
+ aria-valuemin={min}
185
+ aria-valuemax={max}
186
+ value={display}
187
+ oninput={handleInput}
188
+ onfocus={handleFocus}
189
+ onblur={handleBlur}
190
+ onkeydown={handleKeydown}
191
+ onwheel={handleWheel}
192
+ rightIcon={userRightIcon ?? (hideStepper ? undefined : stepper)}
193
+ />
194
+
195
+ {#if name}
196
+ <input type="hidden" {name} value={value ?? ''} />
197
+ {/if}
@@ -0,0 +1,4 @@
1
+ import type { NumberInputProps } from './index.js';
2
+ declare const NumberInput: import("svelte").Component<NumberInputProps, {}, "value">;
3
+ type NumberInput = ReturnType<typeof NumberInput>;
4
+ export default NumberInput;
@@ -0,0 +1,55 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { InputProps } from '../../primitives/Input/index.js';
3
+ /**
4
+ * @description Generic numeric input with min/max/step bounds, stepper buttons,
5
+ * and Arrow-key / mouse-wheel increment. Built on {@link Input}, so it inherits
6
+ * labels, validation, sizes and variants. Values are plain numbers (not minor
7
+ * units) — {@link CurrencyInput} is the domain-specific specialization for money.
8
+ *
9
+ * The field accepts a leading `-`, a single decimal separator (`.` or `,`), and
10
+ * clamps to `[min, max]` on blur; the stepper and Arrow keys clamp immediately.
11
+ * Set {@link precision} to fix the number of decimal places.
12
+ *
13
+ * @tag form
14
+ * @related Input
15
+ * @related CurrencyInput
16
+ * @related Slider
17
+ * @stability beta
18
+ *
19
+ * @example
20
+ * ```svelte
21
+ * <script>
22
+ * let qty = $state(1);
23
+ * </script>
24
+ * <NumberInput label="Quantity" bind:value={qty} min={0} max={99} step={1} />
25
+ * ```
26
+ *
27
+ * @example Decimal step with fixed precision
28
+ * ```svelte
29
+ * <NumberInput label="Rate" bind:value={rate} min={0} max={1} step={0.05} precision={2} />
30
+ * ```
31
+ */
32
+ export interface NumberInputProps extends Omit<InputProps, 'value' | 'type' | 'inputmode' | 'oninput' | 'onfocus' | 'onblur' | 'onkeydown' | 'onwheel' | 'clearable' | 'onClear' | 'children'> {
33
+ /** Current numeric value. `null` when the field is empty. Supports `bind:value`. */
34
+ value?: number | null;
35
+ /** Minimum allowed value. Clamped on step / Arrow / blur. */
36
+ min?: number;
37
+ /** Maximum allowed value. Clamped on step / Arrow / blur. */
38
+ max?: number;
39
+ /** Increment applied by the stepper buttons, Arrow keys, and wheel. @default 1 */
40
+ step?: number;
41
+ /**
42
+ * Fixed number of decimal places for display and rounding. When unset, the
43
+ * value is shown as typed and the step's own decimals drive rounding.
44
+ */
45
+ precision?: number;
46
+ /** Hide the up/down stepper buttons (Arrow keys + wheel still work). @default false */
47
+ hideStepper?: boolean;
48
+ /** Shared `name` for a hidden input for native form submission. */
49
+ name?: string;
50
+ /** Fires after the value changes (typing, stepper, Arrow, wheel, or clamp). */
51
+ onValueChange?: (value: number | null) => void;
52
+ /** A custom right-side adornment. Overrides the stepper — pair with `hideStepper` or provide your own controls. */
53
+ rightIcon?: Snippet;
54
+ }
55
+ export { default as NumberInput } from './NumberInput.svelte';
@@ -0,0 +1 @@
1
+ export { default as NumberInput } from './NumberInput.svelte';
@@ -18,6 +18,8 @@ export * from './Guide/index.js';
18
18
  export * from './LineChart/index.js';
19
19
  export type { LocaleSwitcherProps } from './LocaleSwitcher/index.js';
20
20
  export * from './LocaleSwitcher/index.js';
21
+ export type { NumberInputProps } from './NumberInput/index.js';
22
+ export * from './NumberInput/index.js';
21
23
  export type { PlannerCellContext, PlannerDayContext, PlannerHeaderContext, PlannerProps, PlannerRange, PlannerSlotName, PlannerView } from './Planner/index.js';
22
24
  export * from './Planner/index.js';
23
25
  export type { SankeyIntent, SankeyLaidOutLinkWithMeta, SankeyLaidOutNodeWithMeta, SankeyLink, SankeyNode, SankeyProps } from './Sankey/index.js';
@@ -15,6 +15,7 @@ export * from './FileUpload/index.js';
15
15
  export * from './Guide/index.js';
16
16
  export * from './LineChart/index.js';
17
17
  export * from './LocaleSwitcher/index.js';
18
+ export * from './NumberInput/index.js';
18
19
  export * from './Planner/index.js';
19
20
  export * from './Sankey/index.js';
20
21
  export * from './SidebarLayout/index.js';
@@ -3,6 +3,8 @@ declare const blocksTranslations: {
3
3
  en: {
4
4
  readonly accessibility: {
5
5
  readonly avatar: "Avatar";
6
+ readonly breadcrumb: "Breadcrumb";
7
+ readonly breadcrumbExpand: "Show all breadcrumb items";
6
8
  readonly clearInput: "Clear input";
7
9
  readonly clearSearch: "Clear search";
8
10
  readonly clearSelection: "Clear selection";
@@ -21,6 +23,7 @@ declare const blocksTranslations: {
21
23
  readonly removableBadge: "Removable badge";
22
24
  readonly removeBadge: "Remove badge";
23
25
  readonly removeFile: "Remove {{name}}";
26
+ readonly removeTag: "Remove {{label}}";
24
27
  };
25
28
  readonly button: {
26
29
  readonly close: "Close";
@@ -189,6 +192,8 @@ declare const blocksTranslations: {
189
192
  de: {
190
193
  readonly accessibility: {
191
194
  readonly avatar: "Avatar";
195
+ readonly breadcrumb: "Pfadnavigation";
196
+ readonly breadcrumbExpand: "Alle Pfadebenen anzeigen";
192
197
  readonly clearInput: "Eingabe löschen";
193
198
  readonly clearSearch: "Suche löschen";
194
199
  readonly clearSelection: "Auswahl löschen";
@@ -207,6 +212,7 @@ declare const blocksTranslations: {
207
212
  readonly removableBadge: "Entfernbarer Badge";
208
213
  readonly removeBadge: "Badge entfernen";
209
214
  readonly removeFile: "{{name}} entfernen";
215
+ readonly removeTag: "{{label}} entfernen";
210
216
  };
211
217
  readonly button: {
212
218
  readonly close: "Schließen";
@@ -15,6 +15,7 @@
15
15
  let {
16
16
  tier,
17
17
  intent = 'primary',
18
+ purpose,
18
19
  variant = 'filled',
19
20
  size = 'md',
20
21
  counter = false,
@@ -46,16 +47,22 @@
46
47
  let badgeElement = $state<HTMLElement>();
47
48
  let isHovered = $state(false);
48
49
 
49
- const isDot = $derived(variant === 'dot');
50
- const isInteractive = $derived(interactive || !!onclick);
50
+ // `purpose` is the canonical semantic axis; it orchestrates the low-level
51
+ // visual props so the tv() config stays as-is. When set it wins over the
52
+ // deprecated `variant="dot"` / `counter` boolean; when unset those apply
53
+ // directly (back-compat).
54
+ const effVariant = $derived(purpose === 'dot' ? 'dot' : variant);
55
+ const effCounter = $derived(purpose === 'counter' || counter);
56
+ const isDot = $derived(effVariant === 'dot');
57
+ const isInteractive = $derived(purpose === 'chip' || interactive || !!onclick);
51
58
  const isRemovable = $derived(removable && !isDot);
52
59
 
53
60
  const variantProps: BadgeVariants = $derived({
54
61
  tier: effectiveTier,
55
62
  intent,
56
- variant,
63
+ variant: effVariant,
57
64
  size,
58
- counter: counter || undefined,
65
+ counter: effCounter || undefined,
59
66
  pulse: pulse || undefined,
60
67
  removable: isRemovable || undefined,
61
68
  interactive: isInteractive || undefined,
@@ -117,6 +124,7 @@
117
124
  : styles.base({ class: [slotClasses?.base, className] })
118
125
  ]}
119
126
  {role}
127
+ data-purpose={purpose}
120
128
  tabindex={isInteractive ? 0 : undefined}
121
129
  onmouseenter={handleMouseEnter}
122
130
  onmouseleave={handleMouseLeave}
@@ -6,7 +6,21 @@ import type { BadgePlacement, BadgeSlots, BadgeVariants } from './badge.variants
6
6
  * Shared fields that apply to every Badge variant — `dot` and the
7
7
  * label-style variants both accept these.
8
8
  */
9
- interface BadgeBaseProps extends Omit<BadgeVariants, 'variant'>, Omit<HTMLAttributes<HTMLElement>, 'children'> {
9
+ interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omit<HTMLAttributes<HTMLElement>, 'children'> {
10
+ /**
11
+ * The badge's semantic purpose — the canonical axis that resolves what the
12
+ * badge *is*, since a bare Badge served five overlapping roles. Orchestrates
13
+ * the low-level visual props so you rarely set them directly:
14
+ * - `status` — a state marker (Active, Failed); pairs with `intent`.
15
+ * - `tag` — a neutral inline label (category, version).
16
+ * - `counter` — a compact numeric pill (replaces the `counter` boolean).
17
+ * - `dot` — a pure indicator, content hidden (replaces `variant="dot"`).
18
+ * - `chip` — a removable, interactive filter chip; pair with `removable`.
19
+ *
20
+ * Leave unset to drive the badge purely by the low-level props (back-compat).
21
+ * When set, `purpose` wins over `variant="dot"` / the `counter` boolean.
22
+ */
23
+ purpose?: 'status' | 'tag' | 'counter' | 'dot' | 'chip';
10
24
  /** Add a pulsing animation to draw attention (e.g. for live indicators). */
11
25
  pulse?: boolean;
12
26
  /** Visually disable the badge (reduced opacity, no pointer events). */
@@ -60,7 +74,10 @@ interface BadgeStandardProps extends BadgeBaseProps {
60
74
  variant?: 'filled' | 'outlined' | 'soft';
61
75
  /** Badge content (text, icons, numbers). */
62
76
  children?: Snippet;
63
- /** Display as a compact pill for numeric counts. Tightens padding and uses tabular-nums so digits align. */
77
+ /**
78
+ * Display as a compact pill for numeric counts (tightens padding, tabular-nums).
79
+ * @deprecated Prefer `purpose="counter"` — the canonical semantic axis. Kept for back-compat.
80
+ */
64
81
  counter?: boolean;
65
82
  /** Show a remove (×) button. */
66
83
  removable?: boolean;
@@ -81,16 +98,19 @@ interface BadgeStandardProps extends BadgeBaseProps {
81
98
  * `onRemove` at the type level, while `filled` / `outlined` / `soft`
82
99
  * accept the full surface.
83
100
  *
84
- * @example
101
+ * @example Purpose-driven (canonical) — the intent reads from `purpose`
85
102
  * ```svelte
86
- * <Badge variant="filled" intent="primary">New Feature</Badge>
103
+ * <Badge purpose="status" intent="success">Active</Badge>
104
+ * <Badge purpose="counter" intent="danger">5</Badge>
105
+ * <Badge purpose="chip" removable onRemove={() => …}>React</Badge>
106
+ * <Badge purpose="dot" intent="warning" />
87
107
  * ```
88
108
  *
89
- * @example
109
+ * @example Notification counter anchored to a trigger
90
110
  * ```svelte
91
111
  * <div class="relative">
92
112
  * <Button>Notifications</Button>
93
- * <Badge intent="danger" counter placement="top-end" border>5</Badge>
113
+ * <Badge purpose="counter" intent="danger" placement="top-end" border>5</Badge>
94
114
  * </div>
95
115
  * ```
96
116
  */
@@ -1,9 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { tick } from 'svelte';
3
+ import { useBlocksI18n } from '../..';
3
4
  import { getBlocksConfig, resolveSlotClasses } from '../../provider';
4
5
  import { breadcrumbVariants, type BreadcrumbVariants } from './breadcrumb.variants';
5
6
  import type { BreadcrumbProps, BreadcrumbItem } from './index';
6
7
 
8
+ const bt = useBlocksI18n();
9
+
7
10
  let {
8
11
  items,
9
12
  size = 'md',
@@ -12,8 +15,11 @@
12
15
  maxItems,
13
16
  itemsBeforeCollapse = 1,
14
17
  itemsAfterCollapse = 1,
15
- expandLabel = 'Show all breadcrumb items',
16
- 'aria-label': ariaLabel = 'Breadcrumb',
18
+ // A11y labels default to the localized `accessibility.*` strings in the
19
+ // markup below (resolved per render, so a locale switch updates them) —
20
+ // Breadcrumb was the last primitive with built-in English-only strings.
21
+ expandLabel,
22
+ 'aria-label': ariaLabel,
17
23
  class: className = '',
18
24
  unstyled: unstyledProp = false,
19
25
  slotClasses: slotClassesProp = {},
@@ -43,8 +49,7 @@
43
49
  const after = $derived(Math.max(1, itemsAfterCollapse));
44
50
 
45
51
  type BreadcrumbEntry =
46
- | { kind: 'item'; item: BreadcrumbItem; current: boolean }
47
- | { kind: 'ellipsis' };
52
+ { kind: 'item'; item: BreadcrumbItem; current: boolean } | { kind: 'ellipsis' };
48
53
 
49
54
  const entries = $derived.by((): BreadcrumbEntry[] => {
50
55
  const last = items.length - 1;
@@ -90,7 +95,7 @@
90
95
  class={unstyled
91
96
  ? [slotClasses?.nav, className].filter(Boolean).join(' ')
92
97
  : styles.nav({ class: [slotClasses?.nav, className] })}
93
- aria-label={ariaLabel}
98
+ aria-label={ariaLabel ?? bt('accessibility.breadcrumb')}
94
99
  {...restProps}
95
100
  >
96
101
  <ol class={unstyled ? (slotClasses?.list ?? '') : styles.list({ class: slotClasses?.list })}>
@@ -102,7 +107,7 @@
102
107
  class={unstyled
103
108
  ? (slotClasses?.ellipsis ?? '')
104
109
  : styles.ellipsis({ class: slotClasses?.ellipsis })}
105
- aria-label={expandLabel}
110
+ aria-label={expandLabel ?? bt('accessibility.breadcrumbExpand')}
106
111
  onclick={expand}
107
112
  >
108
113
 
@@ -72,9 +72,9 @@ export interface BreadcrumbProps extends BreadcrumbVariants, Omit<HTMLAttributes
72
72
  itemsBeforeCollapse?: number;
73
73
  /** Trailing items kept visible when collapsed; the current page is always included. @default 1 */
74
74
  itemsAfterCollapse?: number;
75
- /** Accessible label for the "…" button that expands a collapsed trail. @default 'Show all breadcrumb items' */
75
+ /** Accessible label for the "…" button that expands a collapsed trail. Defaults to the localized `accessibility.breadcrumbExpand`. */
76
76
  expandLabel?: string;
77
- /** Accessible label for the nav element @default 'Breadcrumb' */
77
+ /** Accessible label for the nav element. Defaults to the localized `accessibility.breadcrumb`. */
78
78
  'aria-label'?: string;
79
79
  /** Custom CSS class */
80
80
  class?: string;
@@ -45,7 +45,7 @@
45
45
  const id = $derived(idProp ?? propsId);
46
46
  const ff = useFormField(() => ({
47
47
  fieldId: id,
48
- hint: helper,
48
+ helper,
49
49
  error,
50
50
  required,
51
51
  disabled
@@ -169,9 +169,9 @@
169
169
  >
170
170
  {error}
171
171
  </div>
172
- {:else if ff.hintId}
172
+ {:else if ff.helperId}
173
173
  <div
174
- id={ff.hintId}
174
+ id={ff.helperId}
175
175
  class={unstyled
176
176
  ? (slotClasses?.message ?? '')
177
177
  : styles.message({ class: slotClasses?.message })}