@urbicon-ui/blocks 6.22.0 → 6.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/components/Calendar/CalendarWeekdayHeader.svelte +4 -1
  2. package/dist/components/Calendar/calendar.variants.d.ts +63 -63
  3. package/dist/components/Sparkline/Sparkline.svelte +10 -4
  4. package/dist/components/Sparkline/index.d.ts +9 -1
  5. package/dist/i18n/index.d.ts +2 -378
  6. package/dist/primitives/Alert/alert.variants.d.ts +8 -8
  7. package/dist/primitives/Badge/Badge.svelte +8 -2
  8. package/dist/primitives/Badge/index.d.ts +67 -32
  9. package/dist/primitives/ButtonGroup/ButtonGroup.svelte +120 -3
  10. package/dist/primitives/Card/card.variants.js +2 -2
  11. package/dist/primitives/Checkbox/Checkbox.svelte +11 -2
  12. package/dist/primitives/Combobox/Combobox.svelte +35 -3
  13. package/dist/primitives/ConfirmDialog/index.d.ts +2 -1
  14. package/dist/primitives/Dialog/index.d.ts +4 -4
  15. package/dist/primitives/JourneyTimeline/journey-timeline.variants.d.ts +22 -22
  16. package/dist/primitives/Pagination/Pagination.svelte +28 -15
  17. package/dist/primitives/RadioGroup/RadioGroup.svelte +16 -3
  18. package/dist/primitives/RadioGroup/RadioItem.svelte +1 -1
  19. package/dist/primitives/Select/Select.svelte +20 -5
  20. package/dist/primitives/Spinner/spinner.variants.d.ts +16 -16
  21. package/dist/primitives/Stepper/stepper.variants.d.ts +11 -11
  22. package/dist/primitives/Textarea/Textarea.svelte +10 -1
  23. package/dist/primitives/Toast/Toaster.svelte +122 -4
  24. package/dist/primitives/Toast/toast.store.svelte.d.ts +32 -1
  25. package/dist/primitives/Toast/toast.store.svelte.js +84 -19
  26. package/dist/primitives/Toast/toast.variants.d.ts +12 -12
  27. package/dist/primitives/Toggle/Toggle.svelte +16 -3
  28. package/dist/primitives/Tooltip/tooltip.variants.d.ts +3 -3
  29. package/dist/style/semantic.css +1 -1
  30. package/package.json +3 -3
@@ -3,24 +3,13 @@ import type { HTMLAttributes } from 'svelte/elements';
3
3
  import type { MintProp } from '../../mint/index.js';
4
4
  import type { BadgePlacement, BadgeSlots, BadgeVariants } from './badge.variants.js';
5
5
  /**
6
- * Shared fields that apply to every Badge variant `dot` and the
7
- * label-style variants both accept these.
6
+ * Shared fields that apply to every Badge arm. `variant`, `purpose`, and the
7
+ * label-only props (`children` / `counter` / `removable` / `interactive` /
8
+ * `onRemove`) are declared per-arm instead — that split is what lets *both*
9
+ * dot spellings (`variant="dot"` and the canonical `purpose="dot"`) forbid the
10
+ * label-only props at the type level.
8
11
  */
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';
12
+ interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter' | 'removable' | 'interactive'>, Omit<HTMLAttributes<HTMLElement>, 'children'> {
24
13
  /** Add a pulsing animation to draw attention (e.g. for live indicators). */
25
14
  pulse?: boolean;
26
15
  /** Visually disable the badge (reduced opacity, no pointer events). */
@@ -29,7 +18,7 @@ interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omi
29
18
  border?: boolean;
30
19
  /** Anchor the badge absolutely within a `position: relative` parent. */
31
20
  placement?: BadgePlacement;
32
- /** Click handler. Automatically enables interactive styles. */
21
+ /** Click handler. Automatically enables interactive styles (and `role="button"`). */
33
22
  onclick?: (event: MouseEvent) => void;
34
23
  /** Called when the hover state changes. */
35
24
  onHover?: (hovered: boolean) => void;
@@ -46,30 +35,73 @@ interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omi
46
35
  * and make the custom look reusable across the project.
47
36
  */
48
37
  preset?: string;
49
- /** ARIA role. Defaults to `"status"`. Use `"alert"` for time-sensitive notifications. */
50
- role?: 'status' | 'alert' | 'badge';
38
+ /**
39
+ * ARIA role. A static badge is announced as `"status"`; an interactive badge
40
+ * (`onclick` or `purpose="chip"`, when not `disabled`) defaults to `"button"`
41
+ * so assistive tech announces its activation semantics. Set explicitly to
42
+ * override — e.g. `"alert"` for time-sensitive notifications. An explicit
43
+ * value always wins over the derived default.
44
+ */
45
+ role?: 'status' | 'alert' | 'badge' | 'button';
51
46
  /** Micro-interaction preset. Only applies when the badge is interactive. */
52
47
  mint?: MintProp;
53
48
  }
54
49
  /**
55
- * Dot-style badge a pure indicator. Content is `sr-only` (visually
56
- * hidden), so `children` / `counter` / `removable` / `interactive` /
57
- * `onRemove` are excluded by the type: an invisible remove-button or
58
- * hover-scale on a 2.5 × 2.5 px dot is never the intended UI.
50
+ * The label-only props a pure-indicator dot forbids. An invisible remove
51
+ * button, counter shape, or hover-scale on a 2.5 × 2.5 px dot is never the
52
+ * intended UI, so every dot arm excludes them at the type level.
59
53
  */
60
- interface BadgeDotProps extends BadgeBaseProps {
61
- /** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. */
62
- variant: 'dot';
54
+ interface BadgeDotForbiddenProps {
63
55
  children?: never;
64
56
  counter?: never;
65
57
  removable?: never;
66
58
  interactive?: never;
67
59
  onRemove?: never;
68
60
  }
61
+ /**
62
+ * Dot badge selected by the canonical `purpose="dot"` — a pure indicator whose
63
+ * content is hidden. `variant` is inert here (the dot look always wins), and
64
+ * the label-only props are excluded by {@link BadgeDotForbiddenProps}.
65
+ */
66
+ interface BadgeDotByPurposeProps extends BadgeBaseProps, BadgeDotForbiddenProps {
67
+ /**
68
+ * The canonical dot spelling — forces the pure-indicator look regardless of
69
+ * `variant`. For the label roles use `status` / `tag` / `counter` / `chip`
70
+ * (their own arm).
71
+ */
72
+ purpose: 'dot';
73
+ /** Inert under `purpose="dot"`; accepted only so a leftover `variant` compiles. */
74
+ variant?: 'filled' | 'outlined' | 'soft' | 'dot';
75
+ }
76
+ /**
77
+ * Dot badge selected by the deprecated `variant="dot"`. Prefer `purpose="dot"`.
78
+ * Same exclusions as {@link BadgeDotByPurposeProps}.
79
+ */
80
+ interface BadgeDotProps extends BadgeBaseProps, BadgeDotForbiddenProps {
81
+ /** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. */
82
+ variant: 'dot';
83
+ /** Only the dot purpose is non-contradictory with `variant="dot"`. */
84
+ purpose?: 'dot';
85
+ }
69
86
  /**
70
87
  * Label-style badge — accepts content, counter shape, remove button, etc.
71
88
  */
72
89
  interface BadgeStandardProps extends BadgeBaseProps {
90
+ /**
91
+ * The badge's semantic purpose — the canonical axis that resolves what the
92
+ * badge *is*, since a bare Badge served overlapping roles. Orchestrates the
93
+ * low-level visual props so you rarely set them directly:
94
+ * - `status` — a state marker (Active, Failed); pairs with `intent`.
95
+ * - `tag` — a neutral inline label (category, version).
96
+ * - `counter` — a compact numeric pill (replaces the `counter` boolean).
97
+ * - `chip` — a removable, interactive filter chip; pair with `removable`.
98
+ *
99
+ * For a pure indicator use `purpose="dot"` (its own arm — it forbids
100
+ * content / counter / remove). Leave unset to drive the badge purely by the
101
+ * low-level props (back-compat); when set, `purpose` wins over the `counter`
102
+ * boolean.
103
+ */
104
+ purpose?: 'status' | 'tag' | 'counter' | 'chip';
73
105
  /** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. @default 'filled' */
74
106
  variant?: 'filled' | 'outlined' | 'soft';
75
107
  /** Badge content (text, icons, numbers). */
@@ -93,10 +125,13 @@ interface BadgeStandardProps extends BadgeBaseProps {
93
125
  * @related Alert
94
126
  * @related Toast
95
127
  *
96
- * Badge props are a discriminated union on `variant`: `variant="dot"`
97
- * forbids `children` / `counter` / `removable` / `interactive` /
98
- * `onRemove` at the type level, while `filled` / `outlined` / `soft`
99
- * accept the full surface.
128
+ * Badge props are a discriminated union. The pure-indicator dot — spelled
129
+ * canonically as `purpose="dot"` or via the deprecated `variant="dot"`
130
+ * forbids `children` / `counter` / `removable` / `interactive` / `onRemove`
131
+ * at the type level, while the label arms (`filled` / `outlined` / `soft`;
132
+ * `purpose` `status` / `tag` / `counter` / `chip`) accept the full surface.
133
+ * An interactive badge (`onclick` or `purpose="chip"`) is announced as a
134
+ * `button`, a static one as `status` (override via `role`).
100
135
  *
101
136
  * @example Purpose-driven (canonical) — the intent reads from `purpose`
102
137
  * ```svelte
@@ -114,6 +149,6 @@ interface BadgeStandardProps extends BadgeBaseProps {
114
149
  * </div>
115
150
  * ```
116
151
  */
117
- export type BadgeProps = BadgeDotProps | BadgeStandardProps;
152
+ export type BadgeProps = BadgeDotByPurposeProps | BadgeDotProps | BadgeStandardProps;
118
153
  export { default as Badge } from './Badge.svelte';
119
154
  export { type BadgePlacement, type BadgeVariants, badgeVariants, PLACEMENT_VALUES } from './badge.variants.js';
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import { buttonGroupVariants, type ButtonGroupVariants } from '..';
3
3
  import { getBlocksConfig, resolveSlotClasses } from '../../provider';
4
+ import { edgeEnabledIndex, nextEnabledIndex } from '../../utils';
4
5
  import { getTierContext, setTierContext } from '../../utils/tier-context';
5
6
  import type { ButtonGroupContext, ButtonGroupProps } from './index';
6
7
  import { setButtonGroupContext } from './buttonGroup.context';
@@ -65,7 +66,15 @@
65
66
  }
66
67
  });
67
68
 
69
+ // Child Button values in registration (= document) order. Buttons register
70
+ // during their render pass (before any effect runs), so this is fully
71
+ // populated in DOM order by the time roving reads it — it lets the radiogroup
72
+ // map the reactive selection back to a radio's position without the shared
73
+ // <Button> exposing its value in the DOM.
74
+ const buttonOrder: string[] = [];
75
+
68
76
  function registerButton(buttonValue: string | undefined) {
77
+ if (buttonValue && !buttonOrder.includes(buttonValue)) buttonOrder.push(buttonValue);
69
78
  return {
70
79
  get isSelected() {
71
80
  return buttonValue ? selectedValues.has(buttonValue) : false;
@@ -91,8 +100,14 @@
91
100
  onSelectionChange?.(value, Array.from(selectedValues));
92
101
  },
93
102
  getButtonProps() {
94
- if (selection === 'none') return {};
95
- const checked = buttonValue ? selectedValues.has(buttonValue) : false;
103
+ // A value-less Button is an action button, not a selection option, so it
104
+ // gets no radio/checkbox role. This also keeps `rovingRadios()` (which
105
+ // collects `[role="radio"]`) index-aligned with `buttonOrder` (gated on
106
+ // `buttonValue`): a value-less radio would otherwise sit in the roving
107
+ // array but not the order registry, drifting the two index spaces and
108
+ // misplacing the tab stop / arrow-nav origin.
109
+ if (selection === 'none' || !buttonValue) return {};
110
+ const checked = selectedValues.has(buttonValue);
96
111
  return {
97
112
  role: selection === 'single' ? ('radio' as const) : ('checkbox' as const),
98
113
  'aria-checked': checked
@@ -132,17 +147,119 @@
132
147
  registerButton
133
148
  });
134
149
 
150
+ let containerElement = $state<HTMLDivElement>();
151
+
152
+ // Single-select is a WAI-ARIA radiogroup: ONE tab stop, arrow keys move the
153
+ // selection (roving tabindex). Tab/SegmentGroup drive this from a value→element
154
+ // registry their own item components populate; ButtonGroup's items are shared
155
+ // <Button>s (outside this component), so the radiogroup owns the roving from
156
+ // the container instead — it queries the radio *elements* from the DOM (for
157
+ // focus + disabled state), reads the *selected* one from reactive state (see
158
+ // selectedRadioIndex), reuses the shared index math (utils/roving), skips
159
+ // disabled radios, and drives selection by clicking the target radio (Button's
160
+ // own click handler does the rest). `multiple`/`none` keep every button
161
+ // natively tabbable (checkbox-group / plain toolbar convention — not roved).
162
+ function rovingRadios(): HTMLButtonElement[] {
163
+ return containerElement
164
+ ? Array.from(containerElement.querySelectorAll<HTMLButtonElement>('[role="radio"]'))
165
+ : [];
166
+ }
167
+
168
+ // Position of the selected radio, read from the reactive selection (not the
169
+ // DOM's aria-checked) so it is correct even on the first paint — a parent
170
+ // effect can run before the child <Button>s commit their aria, so the DOM
171
+ // isn't a reliable source there. `buttonOrder` is in document order, matching
172
+ // the radios queried above.
173
+ function selectedRadioIndex(): number {
174
+ for (let i = 0; i < buttonOrder.length; i++) {
175
+ if (selectedValues.has(buttonOrder[i])) return i;
176
+ }
177
+ return -1;
178
+ }
179
+
180
+ $effect(() => {
181
+ if (selection !== 'single' || !containerElement) return;
182
+ const radios = rovingRadios();
183
+ if (radios.length === 0) return;
184
+
185
+ const checkedIndex = selectedRadioIndex();
186
+ // Nothing selected yet → the first enabled radio holds the tab stop, so the
187
+ // group stays reachable with Tab (standard radiogroup entry behaviour).
188
+ const activeIndex =
189
+ checkedIndex >= 0
190
+ ? checkedIndex
191
+ : edgeEnabledIndex(radios.length, 1, (i) => radios[i].disabled);
192
+
193
+ radios.forEach((radio, i) => {
194
+ radio.tabIndex = i === activeIndex ? 0 : -1;
195
+ });
196
+
197
+ // Restore native tabbability when the group stops roving (selection flips away
198
+ // from single, or it unmounts) — otherwise the imposed -1 would strand the
199
+ // buttons out of the tab order.
200
+ return () => {
201
+ for (const radio of radios) radio.removeAttribute('tabindex');
202
+ };
203
+ });
204
+
205
+ // A disabled radio renders a `<button disabled>`, which can't hold focus, so
206
+ // arrow navigation must step over it — otherwise selection strands on an
207
+ // unfocusable radio (aria-checked set, focus stuck on the previous one). The
208
+ // index math lives in the shared roving helpers (utils/roving).
209
+ function handleKeyDown(event: KeyboardEvent) {
210
+ if (disabled || selection !== 'single') return;
211
+
212
+ const radios = rovingRadios();
213
+ if (radios.length === 0) return;
214
+
215
+ const currentIndex = selectedRadioIndex();
216
+ const isDisabled = (i: number) => radios[i].disabled;
217
+ let newIndex: number;
218
+
219
+ switch (event.key) {
220
+ case 'ArrowRight':
221
+ case 'ArrowDown':
222
+ event.preventDefault();
223
+ newIndex = nextEnabledIndex(radios.length, currentIndex, 1, isDisabled);
224
+ break;
225
+ case 'ArrowLeft':
226
+ case 'ArrowUp':
227
+ event.preventDefault();
228
+ newIndex = nextEnabledIndex(radios.length, currentIndex, -1, isDisabled);
229
+ break;
230
+ case 'Home':
231
+ event.preventDefault();
232
+ newIndex = edgeEnabledIndex(radios.length, 1, isDisabled);
233
+ break;
234
+ case 'End':
235
+ event.preventDefault();
236
+ newIndex = edgeEnabledIndex(radios.length, -1, isDisabled);
237
+ break;
238
+ default:
239
+ return;
240
+ }
241
+
242
+ if (newIndex !== currentIndex && newIndex >= 0) {
243
+ const target = radios[newIndex];
244
+ target.click(); // Button's own click handler performs the selection
245
+ target.focus();
246
+ }
247
+ }
248
+
135
249
  const ariaRole = $derived(selection === 'single' ? 'radiogroup' : 'group');
136
250
  </script>
137
251
 
138
252
  <div
253
+ bind:this={containerElement}
139
254
  role={ariaRole}
140
255
  class={unstyled
141
256
  ? [slotClasses?.base, className].filter(Boolean).join(' ')
142
257
  : styles.base({ class: [slotClasses?.base, className] })}
143
258
  aria-label={ariaLabel}
144
259
  aria-labelledby={ariaLabelledBy}
145
- aria-disabled={disabled}
260
+ aria-orientation={orientation}
261
+ aria-disabled={disabled || undefined}
262
+ onkeydown={handleKeyDown}
146
263
  {...restProps}
147
264
  >
148
265
  {@render children?.()}
@@ -66,10 +66,10 @@ export const cardVariants = tv({
66
66
  footer: 'pt-5'
67
67
  }
68
68
  },
69
- // Opt-in slot separators. Default `false` (Lighter): header/footer
69
+ // Opt-in slot separators. Default `false`: header/footer
70
70
  // sit flush against the body, the slots are separated by spacing
71
71
  // only. Set `dividers={true}` for traditional card-with-header look.
72
- // Uses `border-hairline` (Lighter token) — leiser als border-subtle.
72
+ // Uses `border-hairline` — leiser als border-subtle.
73
73
  dividers: {
74
74
  true: {
75
75
  header: 'border-b border-border-hairline',
@@ -32,6 +32,7 @@
32
32
  unstyled: unstyledProp = false,
33
33
  slotClasses: slotClassesProp = {},
34
34
  preset,
35
+ 'aria-describedby': ariaDescribedby,
35
36
  ...restProps
36
37
  }: CheckboxProps = $props();
37
38
 
@@ -51,6 +52,14 @@
51
52
  disabled
52
53
  }));
53
54
 
55
+ // Consumer-supplied `aria-describedby` (e.g. an external hint rendered
56
+ // outside the component) merges with the internal error/helper chain instead
57
+ // of replacing it — internal descriptions first, the consumer's supplemental
58
+ // one last (mirrors the Input role model, XC-2).
59
+ const describedBy = $derived(
60
+ [ff.describedBy, ariaDescribedby].filter(Boolean).join(' ') || undefined
61
+ );
62
+
54
63
  const blocksConfig = getBlocksConfig();
55
64
  const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
56
65
 
@@ -117,6 +126,7 @@
117
126
  for={id}
118
127
  >
119
128
  <input
129
+ {...restProps}
120
130
  bind:this={inputRef}
121
131
  {id}
122
132
  type="checkbox"
@@ -128,9 +138,8 @@
128
138
  class="peer sr-only"
129
139
  aria-checked={indeterminate ? 'mixed' : undefined}
130
140
  aria-invalid={ff.invalid ? 'true' : undefined}
131
- aria-describedby={ff.describedBy}
141
+ aria-describedby={describedBy}
132
142
  onchange={handleChange}
133
- {...restProps}
134
143
  />
135
144
 
136
145
  <span
@@ -56,6 +56,8 @@
56
56
  slotClasses: slotClassesProp = {},
57
57
  preset,
58
58
  id: idProp,
59
+ 'aria-describedby': ariaDescribedby,
60
+ 'aria-label': ariaLabel,
59
61
  ...restProps
60
62
  }: ComboboxProps<T> = $props();
61
63
 
@@ -80,6 +82,15 @@
80
82
  disabled
81
83
  }));
82
84
 
85
+ // Consumer-supplied `aria-describedby` (e.g. an external hint) merges with the
86
+ // internal error/helper chain instead of being dropped — restProps land on the
87
+ // wrapper div, so without this the description would never reach the focusable
88
+ // input. Internal descriptions first, the consumer's supplemental one last
89
+ // (mirrors Select + Input).
90
+ const describedBy = $derived(
91
+ [ff.describedBy, ariaDescribedby].filter(Boolean).join(' ') || undefined
92
+ );
93
+
83
94
  const blocksConfig = getBlocksConfig();
84
95
  const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
85
96
 
@@ -207,6 +218,18 @@
207
218
  filteredGroups ? filteredGroups.flatMap((g) => g.options) : allOptions.filter(matchesQuery)
208
219
  );
209
220
 
221
+ // Flat index of each option within `filtered`, precomputed so the grouped
222
+ // render path reads an option's keyboard-cursor index in O(1) instead of
223
+ // `filtered.indexOf(opt)` per option (O(n²) per keystroke on a large grouped
224
+ // list). Same first-occurrence semantics as `indexOf`.
225
+ const filteredIndexByOption = $derived.by(() => {
226
+ const map = new Map<ComboboxOption<T>, number>();
227
+ filtered.forEach((opt, i) => {
228
+ if (!map.has(opt)) map.set(opt, i);
229
+ });
230
+ return map;
231
+ });
232
+
210
233
  // Debounced query runner. The effect tracks `query` (+ debounceMs); each change
211
234
  // resets the timer, and the fetch runs a tick later so bursty typing collapses
212
235
  // into one request. A per-run AbortController lets a newer query cancel an
@@ -750,7 +773,7 @@
750
773
  {noResultsText}
751
774
  </div>
752
775
  {:else if filteredGroups}
753
- {#each filteredGroups as group (group.label)}
776
+ {#each filteredGroups as group, i (`${group.label}-${i}`)}
754
777
  <div
755
778
  class={unstyled
756
779
  ? (slotClasses?.group ?? '')
@@ -766,7 +789,7 @@
766
789
  {group.label}
767
790
  </div>
768
791
  {#each group.options as opt (opt.value)}
769
- {@render optionButton(opt, filtered.indexOf(opt))}
792
+ {@render optionButton(opt, filteredIndexByOption.get(opt) ?? -1)}
770
793
  {/each}
771
794
  </div>
772
795
  {/each}
@@ -855,6 +878,14 @@
855
878
  wiring lives in one place. `required` is single-mode only — in multi the input
856
879
  is transient search text (cleared after each pick), so a native `required` on
857
880
  it would wrongly block submit even with tags selected.
881
+
882
+ A consumer `aria-label` is forwarded onto the input (destructured out of
883
+ restProps so it never lands on the role-less wrapper `<div>` — axe
884
+ aria-prohibited-attr), but ONLY when no visible `label` renders. Unlike Select —
885
+ which names its trigger via `aria-labelledby` (HIGHER ARIA precedence than
886
+ aria-label) — Combobox names the input via a native `<label for>` (LOWER
887
+ precedence than aria-label), so an unconditional aria-label would override a
888
+ visible label. Gating on `label` keeps the visible label authoritative.
858
889
  -->
859
890
  {#snippet searchField(cls: string)}
860
891
  <input
@@ -867,7 +898,8 @@
867
898
  aria-activedescendant={activeDescendant}
868
899
  aria-autocomplete="list"
869
900
  aria-invalid={ff.invalid ? 'true' : undefined}
870
- aria-describedby={ff.describedBy}
901
+ aria-describedby={describedBy}
902
+ aria-label={label ? undefined : ariaLabel}
871
903
  autocomplete="off"
872
904
  class={cls}
873
905
  placeholder={effectivePlaceholder}
@@ -1,4 +1,5 @@
1
1
  import type { Snippet } from 'svelte';
2
+ import type { HTMLDialogAttributes } from 'svelte/elements';
2
3
  import type { DialogIntent } from '../Dialog/index.js';
3
4
  import type { DialogSlots } from '../Dialog/dialog.variants.js';
4
5
  /**
@@ -36,7 +37,7 @@ export type ConfirmIntent = Exclude<DialogIntent, 'neutral'>;
36
37
  * />
37
38
  * ```
38
39
  */
39
- export interface ConfirmDialogProps {
40
+ export interface ConfirmDialogProps extends Omit<HTMLDialogAttributes, 'children' | 'open' | 'title' | 'draggable'> {
40
41
  /** Controls visibility. Supports bind:open. */
41
42
  open?: boolean;
42
43
  /** Heading shown in the dialog header. */
@@ -41,10 +41,10 @@ export interface DialogProps extends Omit<HTMLDialogAttributes, 'children' | 'op
41
41
  /** Vertical placement within the viewport. Use 'top' for command-palette style positioning. @default 'center' */
42
42
  placement?: DialogVariants['placement'];
43
43
  /**
44
- * Semantic purpose marker (e.g. `danger` for destructive actions). After the
45
- * Lighter-Refactor, the Dialog itself no longer paints an accent bar — the
46
- * value is exposed on the panel as `data-intent="…"` so consumers can hook
47
- * presets, CSS overrides, or icon/title color via their own snippets.
44
+ * Semantic purpose marker (e.g. `danger` for destructive actions). The
45
+ * Dialog itself no longer paints an accent bar — the value is exposed on
46
+ * the panel as `data-intent="…"` so consumers can hook presets, CSS
47
+ * overrides, or icon/title color via their own snippets.
48
48
  * @default 'neutral'
49
49
  */
50
50
  intent?: DialogVariants['intent'];