@urbicon-ui/blocks 6.30.0 → 6.31.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 (31) hide show
  1. package/dist/components/Calendar/Calendar.svelte +17 -21
  2. package/dist/components/Calendar/CalendarAgendaView.svelte +9 -3
  3. package/dist/components/Calendar/CalendarDayView.svelte +9 -3
  4. package/dist/components/Calendar/CalendarGrid.svelte +9 -3
  5. package/dist/components/Calendar/CalendarWeekGrid.svelte +9 -3
  6. package/dist/components/Calendar/CalendarYearGrid.svelte +12 -3
  7. package/dist/components/CommandPalette/commandPalette.variants.js +9 -4
  8. package/dist/primitives/Accordion/accordion.variants.d.ts +7 -7
  9. package/dist/primitives/Accordion/accordion.variants.js +4 -4
  10. package/dist/primitives/Accordion/index.d.ts +2 -2
  11. package/dist/primitives/Button/Button.svelte +23 -7
  12. package/dist/primitives/Combobox/Combobox.svelte +11 -3
  13. package/dist/primitives/Combobox/combobox.variants.d.ts +9 -0
  14. package/dist/primitives/Combobox/combobox.variants.js +22 -4
  15. package/dist/primitives/Combobox/index.d.ts +2 -2
  16. package/dist/primitives/Menu/menu.variants.js +13 -5
  17. package/dist/primitives/SegmentGroup/SegmentGroup.svelte +4 -4
  18. package/dist/primitives/SegmentGroup/SegmentItem.svelte +1 -1
  19. package/dist/primitives/SegmentGroup/index.d.ts +1 -1
  20. package/dist/primitives/SegmentGroup/segmentgroup.variants.d.ts +4 -4
  21. package/dist/primitives/SegmentGroup/segmentgroup.variants.js +15 -15
  22. package/dist/primitives/Select/select.variants.js +21 -7
  23. package/dist/primitives/Slider/Slider.svelte +2 -2
  24. package/dist/primitives/Slider/slider.variants.d.ts +14 -14
  25. package/dist/primitives/Slider/slider.variants.js +13 -13
  26. package/dist/primitives/Toggle/Toggle.svelte +2 -2
  27. package/dist/primitives/Toggle/index.d.ts +1 -1
  28. package/dist/primitives/Toggle/toggle.variants.d.ts +7 -7
  29. package/dist/primitives/Toggle/toggle.variants.js +32 -32
  30. package/docs/MIGRATION-v5.md +7 -5
  31. package/package.json +3 -3
@@ -178,10 +178,6 @@
178
178
  view === 'year' || view === 'agenda' ? 'month' : view
179
179
  );
180
180
 
181
- // --- Uncontrolled fallback ---
182
- let internalValue = $state<CalendarProps['value']>(undefined);
183
- const effectiveValue = $derived(value !== undefined ? value : internalValue);
184
-
185
181
  // --- Extra disable predicate (min/max handled by the controller) ---
186
182
  const disabledDatesSet = $derived(new Set(disabledDates.map((d) => toIso(d))));
187
183
  function checkExtraDisabled(date: Date): boolean {
@@ -210,7 +206,7 @@
210
206
  return selectionMode;
211
207
  },
212
208
  get selection() {
213
- return effectiveValue as DateGridSelection | undefined;
209
+ return value as DateGridSelection | undefined;
214
210
  },
215
211
  get rangeStart() {
216
212
  return undefined;
@@ -255,12 +251,13 @@
255
251
  onMonthChange?.(anchor.getMonth(), anchor.getFullYear());
256
252
  }
257
253
  onDateClick?.(date);
254
+ // Always assign the $bindable — no controlled/uncontrolled split. With
255
+ // `bind:value` the write-back reaches the consumer even from the empty
256
+ // (undefined) initial, the only type-correct "no selection"; without a
257
+ // binding Svelte 5 keeps the write local until the parent passes a new
258
+ // prop value, so uncontrolled and callback-driven usage work unchanged.
258
259
  const next = selection as CalendarProps['value'];
259
- if (value !== undefined) {
260
- value = next;
261
- } else {
262
- internalValue = next;
263
- }
260
+ value = next;
264
261
  onValueChange?.(next!);
265
262
  }
266
263
 
@@ -351,10 +348,10 @@
351
348
 
352
349
  // --- Derived: selected date for event list ---
353
350
  const selectedDateForList = $derived.by(() => {
354
- if (!effectiveValue) return null;
355
- if (effectiveValue instanceof Date) return effectiveValue;
356
- if (Array.isArray(effectiveValue)) return effectiveValue[effectiveValue.length - 1] ?? null;
357
- return effectiveValue.start;
351
+ if (!value) return null;
352
+ if (value instanceof Date) return value;
353
+ if (Array.isArray(value)) return value[value.length - 1] ?? null;
354
+ return value.start;
358
355
  });
359
356
 
360
357
  // --- Event helpers ---
@@ -500,15 +497,14 @@
500
497
  return selectedDateForList;
501
498
  },
502
499
  get selectedDates() {
503
- if (!effectiveValue) return [];
504
- if (effectiveValue instanceof Date) return [effectiveValue];
505
- if (Array.isArray(effectiveValue)) return effectiveValue;
506
- return [effectiveValue.start, effectiveValue.end];
500
+ if (!value) return [];
501
+ if (value instanceof Date) return [value];
502
+ if (Array.isArray(value)) return value;
503
+ return [value.start, value.end];
507
504
  },
508
505
  get selectedRange() {
509
- if (!effectiveValue || effectiveValue instanceof Date || Array.isArray(effectiveValue))
510
- return null;
511
- return effectiveValue as DateRange;
506
+ if (!value || value instanceof Date || Array.isArray(value)) return null;
507
+ return value as DateRange;
512
508
  },
513
509
  get events() {
514
510
  return expandedEvents;
@@ -82,9 +82,15 @@
82
82
  aria-label={bt('calendar.agendaView')}
83
83
  onkeydown={handleKeydown}
84
84
  {@attach swipeable({
85
- onSwipeLeft: () => ctx.navigate(1),
86
- onSwipeRight: () => ctx.navigate(-1),
87
- enabled: ctx.swipeable
85
+ // Direction-gated like the header arrows (the DateGridScaffold pattern): a
86
+ // swipe at the bound is inert — no navDirection flip, no clamped no-op emit.
87
+ onSwipeLeft: () => {
88
+ if (ctx.canGoForward) ctx.navigate(1);
89
+ },
90
+ onSwipeRight: () => {
91
+ if (ctx.canGoBack) ctx.navigate(-1);
92
+ },
93
+ enabled: ctx.swipeable && !ctx.disabled
88
94
  })}
89
95
  style="overflow: hidden;"
90
96
  >
@@ -54,9 +54,15 @@
54
54
  aria-label={dateLabel}
55
55
  onkeydown={handleKeydown}
56
56
  {@attach swipeable({
57
- onSwipeLeft: () => ctx.navigate(1),
58
- onSwipeRight: () => ctx.navigate(-1),
59
- enabled: ctx.swipeable
57
+ // Direction-gated like the header arrows (the DateGridScaffold pattern): a
58
+ // swipe at the bound is inert — no navDirection flip, no clamped no-op emit.
59
+ onSwipeLeft: () => {
60
+ if (ctx.canGoForward) ctx.navigate(1);
61
+ },
62
+ onSwipeRight: () => {
63
+ if (ctx.canGoBack) ctx.navigate(-1);
64
+ },
65
+ enabled: ctx.swipeable && !ctx.disabled
60
66
  })}
61
67
  >
62
68
  <div class="grid [&>*]:col-start-1 [&>*]:row-start-1">
@@ -72,9 +72,15 @@
72
72
  onmouseleave={() => ctx.setHoveredDate(null)}
73
73
  style="overflow: hidden; position: relative;"
74
74
  {@attach swipeable({
75
- onSwipeLeft: () => ctx.navigate(1),
76
- onSwipeRight: () => ctx.navigate(-1),
77
- enabled: ctx.swipeable
75
+ // Direction-gated like the header arrows (the DateGridScaffold pattern): a
76
+ // swipe at the bound is inert — no navDirection flip, no clamped no-op emit.
77
+ onSwipeLeft: () => {
78
+ if (ctx.canGoForward) ctx.navigate(1);
79
+ },
80
+ onSwipeRight: () => {
81
+ if (ctx.canGoBack) ctx.navigate(-1);
82
+ },
83
+ enabled: ctx.swipeable && !ctx.disabled
78
84
  })}
79
85
  >
80
86
  <CalendarWeekdayHeader />
@@ -99,9 +99,15 @@
99
99
  onkeydown={handleKeydown}
100
100
  style="overflow: hidden;"
101
101
  {@attach swipeable({
102
- onSwipeLeft: () => ctx.navigate(1),
103
- onSwipeRight: () => ctx.navigate(-1),
104
- enabled: ctx.swipeable
102
+ // Direction-gated like the header arrows (the DateGridScaffold pattern): a
103
+ // swipe at the bound is inert — no navDirection flip, no clamped no-op emit.
104
+ onSwipeLeft: () => {
105
+ if (ctx.canGoForward) ctx.navigate(1);
106
+ },
107
+ onSwipeRight: () => {
108
+ if (ctx.canGoBack) ctx.navigate(-1);
109
+ },
110
+ enabled: ctx.swipeable && !ctx.disabled
105
111
  })}
106
112
  >
107
113
  <div class="grid [&>*]:col-start-1 [&>*]:row-start-1">
@@ -85,9 +85,18 @@
85
85
  onkeydown={handleKeydown}
86
86
  style="overflow: hidden;"
87
87
  {@attach swipeable({
88
- onSwipeLeft: () => ctx.navigate(1),
89
- onSwipeRight: () => ctx.navigate(-1),
90
- enabled: ctx.swipeable
88
+ // Direction-gated like the header arrows (the DateGridScaffold pattern): a
89
+ // swipe at the bound is inert — no navDirection flip, no clamped no-op
90
+ // emit. In year view the gate is month-granular (the controller maps year
91
+ // onto month bounds): whenever it blocks, the year step would have clamped
92
+ // back onto the displayed month/year anyway — never a real change.
93
+ onSwipeLeft: () => {
94
+ if (ctx.canGoForward) ctx.navigate(1);
95
+ },
96
+ onSwipeRight: () => {
97
+ if (ctx.canGoBack) ctx.navigate(-1);
98
+ },
99
+ enabled: ctx.swipeable && !ctx.disabled
91
100
  })}
92
101
  >
93
102
  <div class="grid [&>*]:col-start-1 [&>*]:row-start-1">
@@ -15,11 +15,16 @@ export const commandPaletteVariants = tv({
15
15
  ],
16
16
  inputIcon: 'h-4 w-4 shrink-0 text-text-tertiary',
17
17
  clearButton: 'text-text-quaternary hover:text-text-secondary',
18
- list: 'max-h-72 overflow-y-auto p-1.5',
19
- groupLabel: ['px-2 py-1.5 text-2xs font-semibold uppercase tracking-wide text-text-quaternary'],
20
- // tier: modify palette items are selectable actions.
18
+ // `p-1` + `space-y-0.5` = the shared listbox rhythm (XC-9): 4px edge
19
+ // inset, 2px item-to-item gap same as Select/Combobox/Menu panels.
20
+ list: 'max-h-72 overflow-y-auto p-1 space-y-0.5',
21
+ // `px-3` shares the item's horizontal inset; the micro-typo (2xs,
22
+ // quaternary) is the palette's own command-surface voice.
23
+ groupLabel: ['px-3 py-1.5 text-2xs font-semibold uppercase tracking-wide text-text-quaternary'],
24
+ // tier: modify — palette items are selectable actions. Rhythm = the md
25
+ // listbox baseline (px-3 py-2 text-sm min-h-2.5rem, gap-2 — XC-9).
21
26
  item: [
22
- 'flex w-full items-center gap-3 rounded-modify px-3 py-2 text-sm',
27
+ 'flex w-full items-center gap-2 rounded-modify px-3 py-2 text-sm min-h-[2.5rem]',
23
28
  'transition-colors cursor-pointer'
24
29
  ],
25
30
  itemHighlighted: 'bg-primary-subtle text-primary',
@@ -1,40 +1,40 @@
1
1
  import { type SlotNames, type VariantProps } from '../../utils/variants.js';
2
2
  export declare const accordionVariants: ((props?: {
3
- variant?: "default" | "ghost" | "separated" | undefined;
3
+ variant?: "default" | "ghost" | "card" | undefined;
4
4
  size?: "sm" | "md" | "lg" | undefined;
5
5
  } | undefined) => {
6
6
  base: (props?: ({
7
- variant?: "default" | "ghost" | "separated" | undefined;
7
+ variant?: "default" | "ghost" | "card" | undefined;
8
8
  size?: "sm" | "md" | "lg" | undefined;
9
9
  } & {
10
10
  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;
11
11
  }) | undefined) => string;
12
12
  item: (props?: ({
13
- variant?: "default" | "ghost" | "separated" | undefined;
13
+ variant?: "default" | "ghost" | "card" | undefined;
14
14
  size?: "sm" | "md" | "lg" | undefined;
15
15
  } & {
16
16
  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;
17
17
  }) | undefined) => string;
18
18
  trigger: (props?: ({
19
- variant?: "default" | "ghost" | "separated" | undefined;
19
+ variant?: "default" | "ghost" | "card" | undefined;
20
20
  size?: "sm" | "md" | "lg" | undefined;
21
21
  } & {
22
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;
23
23
  }) | undefined) => string;
24
24
  chevron: (props?: ({
25
- variant?: "default" | "ghost" | "separated" | undefined;
25
+ variant?: "default" | "ghost" | "card" | undefined;
26
26
  size?: "sm" | "md" | "lg" | undefined;
27
27
  } & {
28
28
  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;
29
29
  }) | undefined) => string;
30
30
  content: (props?: ({
31
- variant?: "default" | "ghost" | "separated" | undefined;
31
+ variant?: "default" | "ghost" | "card" | undefined;
32
32
  size?: "sm" | "md" | "lg" | undefined;
33
33
  } & {
34
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;
35
35
  }) | undefined) => string;
36
36
  contentInner: (props?: ({
37
- variant?: "default" | "ghost" | "separated" | undefined;
37
+ variant?: "default" | "ghost" | "card" | undefined;
38
38
  size?: "sm" | "md" | "lg" | undefined;
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;
@@ -27,13 +27,13 @@ export const accordionVariants = tv({
27
27
  contentInner: 'pb-4 text-text-secondary'
28
28
  },
29
29
  // Variant contract (see docs/MIGRATION-v5.md §2):
30
- // default → hairlines between items (divide-y)
31
- // separated → items as standalone blocks with spacing, no borders
32
- // ghost → no separators, hover-tint per item
30
+ // default → hairlines between items (divide-y)
31
+ // card → items as standalone card blocks with spacing, no borders
32
+ // ghost → no separators, hover-tint per item
33
33
  variants: {
34
34
  variant: {
35
35
  default: {},
36
- separated: {
36
+ card: {
37
37
  base: 'divide-y-0 space-y-2',
38
38
  item: 'rounded-contain bg-surface-quiet px-4'
39
39
  },
@@ -33,7 +33,7 @@ export interface AccordionContext {
33
33
  *
34
34
  * @example
35
35
  * ```svelte
36
- * <Accordion type="multiple" variant="separated" bind:value={openItems}>
36
+ * <Accordion type="multiple" variant="card" bind:value={openItems}>
37
37
  * <AccordionItem value="section-1" title="Section 1">Content</AccordionItem>
38
38
  * <AccordionItem value="section-2" title="Section 2">Content</AccordionItem>
39
39
  * </Accordion>
@@ -43,7 +43,7 @@ export interface AccordionProps extends AccordionVariants, Omit<HTMLAttributes<H
43
43
  /** Allow single or multiple items open at once @default 'single' */
44
44
  type?: 'single' | 'multiple';
45
45
  /** Visual style @default 'default' */
46
- variant?: 'default' | 'separated' | 'ghost';
46
+ variant?: 'default' | 'card' | 'ghost';
47
47
  /** Size @default 'md' */
48
48
  size?: 'sm' | 'md' | 'lg';
49
49
  /** Controlled open item(s) – string for single, string[] for multiple */
@@ -106,8 +106,23 @@
106
106
  }
107
107
  </script>
108
108
 
109
+ <!--
110
+ restProps spreads FIRST so component-owned state wins (COMPONENT-API-CONVENTIONS
111
+ §restProps ordering). The ARIA/selection attributes after the spread are
112
+ conditional merges, not plain overrides: an explicit `undefined` after a spread
113
+ REMOVES the attribute in Svelte, so a naive `role={ariaProps.role}` would strip
114
+ a standalone consumer's own `role="link"`. Each merge lets the ButtonGroup
115
+ selection wiring (role/aria-checked/data-value) and the modeled state
116
+ (pressed/disabled/loading) win when the component has something to say, and
117
+ falls back to the consumer's restProps value when it doesn't. Inside a
118
+ selection group aria-pressed is forced off even against restProps — a selection
119
+ role announces via aria-checked, and doubling up is an ARIA violation.
120
+ `type`/`disabled`/`class`/`onclick` are destructured (never in restProps);
121
+ a consumer onclick is composed inside handleClick.
122
+ -->
109
123
  <button
110
124
  bind:this={buttonElement}
125
+ {...restProps}
111
126
  {type}
112
127
  disabled={effectiveDisabled}
113
128
  class={[
@@ -118,13 +133,14 @@
118
133
  : styles.base({ class: [slotClasses?.base, className] })
119
134
  ]}
120
135
  onclick={handleClick}
121
- role={ariaProps.role}
122
- aria-checked={ariaProps['aria-checked']}
123
- data-value={ariaProps['data-value']}
124
- aria-pressed={ariaProps.role ? undefined : effectiveActive || effectivePressed || undefined}
125
- aria-disabled={effectiveDisabled}
126
- aria-busy={loading}
127
- {...restProps}
136
+ role={ariaProps.role ?? restProps.role}
137
+ aria-checked={ariaProps['aria-checked'] ?? restProps['aria-checked']}
138
+ data-value={ariaProps['data-value'] ?? restProps['data-value']}
139
+ aria-pressed={ariaProps.role
140
+ ? undefined
141
+ : effectiveActive || effectivePressed || restProps['aria-pressed']}
142
+ aria-disabled={effectiveDisabled || (restProps['aria-disabled'] ?? false)}
143
+ aria-busy={loading || (restProps['aria-busy'] ?? false)}
128
144
  >
129
145
  <span
130
146
  class={unstyled
@@ -895,9 +895,17 @@
895
895
  {@render customOption(opt, selected)}
896
896
  {:else}
897
897
  <span class="flex-1 truncate">{opt.label}</span>
898
- {#if selected}
899
- <CheckMarkIcon class="text-primary h-4 w-4 shrink-0" />
900
- {/if}
898
+ <!--
899
+ Always rendered (reserved space) and faded in via opacity — parity
900
+ with Select's optionCheck mechanic, so selecting never shifts layout.
901
+ -->
902
+ <CheckMarkIcon
903
+ class={unstyled
904
+ ? (slotClasses?.optionCheck ?? '')
905
+ : styles.optionCheck({
906
+ class: [slotClasses?.optionCheck, selected ? 'opacity-100' : undefined]
907
+ })}
908
+ />
901
909
  {/if}
902
910
  </button>
903
911
  {/snippet}
@@ -105,6 +105,15 @@ export declare const comboboxVariants: ((props?: {
105
105
  } & {
106
106
  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;
107
107
  }) | undefined) => string;
108
+ optionCheck: (props?: ({
109
+ tier?: "commit" | "modify" | undefined;
110
+ variant?: "ghost" | "filled" | "outlined" | "underline" | undefined;
111
+ size?: "sm" | "md" | "lg" | "xs" | "xl" | undefined;
112
+ open?: boolean | undefined;
113
+ disabled?: boolean | undefined;
114
+ } & {
115
+ 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;
116
+ }) | undefined) => string;
108
117
  group: (props?: ({
109
118
  tier?: "commit" | "modify" | undefined;
110
119
  variant?: "ghost" | "filled" | "outlined" | "underline" | undefined;
@@ -34,13 +34,21 @@ export const comboboxVariants = tv({
34
34
  'flex w-full items-center gap-2 rounded-modify px-3 py-2 text-left',
35
35
  'text-text-primary cursor-pointer select-none',
36
36
  'transition-colors duration-[var(--blocks-duration-fast)]',
37
- 'focus-visible:outline-none'
37
+ 'focus-visible:outline-none',
38
+ 'disabled:opacity-50 disabled:cursor-not-allowed'
38
39
  ],
39
40
  optionActive: 'bg-surface-hover',
40
41
  optionSelected: 'bg-surface-selected font-medium',
41
- // Grouped options (parity with Select). `group` is a bare container; the
42
- // label is a quiet, uppercase section header.
43
- group: '',
42
+ // Trailing selected-check (parity with Select `optionCheck`): always
43
+ // rendered with reserved space, fades in via opacity — no layout shift
44
+ // when a row becomes selected. Sized one step with the option per size.
45
+ optionCheck: [
46
+ 'shrink-0 text-primary opacity-0 transition-opacity duration-[var(--blocks-duration-fast)]'
47
+ ],
48
+ // Grouped options (parity with Select). `group` carries the same
49
+ // item-to-item rhythm as the flat listbox (`space-y-0.5` there lives on
50
+ // the `listbox` slot); the label is a quiet, uppercase section header.
51
+ group: 'space-y-0.5',
44
52
  groupLabel: 'px-3 py-1.5 text-xs font-medium text-text-tertiary uppercase tracking-wider',
45
53
  noResults: 'px-3 py-4 text-center text-sm text-text-tertiary',
46
54
  // Async-search in-flight message (queryFn). Mirrors noResults so both listbox
@@ -137,11 +145,15 @@ export const comboboxVariants = tv({
137
145
  // The multi-mode tokenizer (`control`/`search`/`tag`) tracks the same
138
146
  // ladder: `control` uses `min-h-*` (not `h-*`) so it grows as tags wrap,
139
147
  // and reserves right padding (`pr-*`) for the absolute chevron/clear.
148
+ // Option rows follow the shared listbox item rhythm (XC-9); the group
149
+ // label always shares the option's horizontal inset.
140
150
  xs: {
141
151
  // `pointer-coarse:text-base` floors the input to 16px on touch-primary
142
152
  // devices so iOS Safari doesn't auto-zoom (and never un-zoom) on focus.
143
153
  input: 'h-7 px-2 pr-7 text-xs pointer-coarse:text-base',
144
154
  option: 'px-2 py-1 text-xs min-h-[1.75rem]',
155
+ optionCheck: 'w-3 h-3',
156
+ groupLabel: 'px-2',
145
157
  control: 'min-h-7 gap-1 px-1.5 py-1 pr-7',
146
158
  search: 'text-xs pointer-coarse:text-base',
147
159
  tag: 'text-xs px-1.5 py-0.5',
@@ -151,6 +163,8 @@ export const comboboxVariants = tv({
151
163
  // See `xs` — floor to 16px on touch to avoid iOS Safari focus-zoom.
152
164
  input: 'h-8 px-3 pr-8 text-sm pointer-coarse:text-base',
153
165
  option: 'px-2 py-1.5 text-sm min-h-[2rem]',
166
+ optionCheck: 'w-3.5 h-3.5',
167
+ groupLabel: 'px-2',
154
168
  control: 'min-h-8 gap-1 px-2 py-1 pr-8',
155
169
  search: 'text-sm pointer-coarse:text-base',
156
170
  tag: 'text-xs px-2 py-0.5',
@@ -159,6 +173,7 @@ export const comboboxVariants = tv({
159
173
  md: {
160
174
  input: 'h-10 px-3 pr-8 text-base',
161
175
  option: 'px-3 py-2 text-sm min-h-[2.5rem]',
176
+ optionCheck: 'w-4 h-4',
162
177
  control: 'min-h-10 gap-1.5 px-2 py-1.5 pr-8',
163
178
  search: 'text-base',
164
179
  tag: 'text-sm px-2 py-0.5',
@@ -167,6 +182,7 @@ export const comboboxVariants = tv({
167
182
  lg: {
168
183
  input: 'h-12 px-4 pr-10 text-lg',
169
184
  option: 'px-3 py-2.5 text-base min-h-[3rem]',
185
+ optionCheck: 'w-5 h-5',
170
186
  control: 'min-h-12 gap-2 px-3 py-2 pr-10',
171
187
  search: 'text-lg',
172
188
  tag: 'text-base px-2.5 py-1',
@@ -175,6 +191,8 @@ export const comboboxVariants = tv({
175
191
  xl: {
176
192
  input: 'h-14 px-6 pr-12 text-xl',
177
193
  option: 'px-4 py-3 text-lg min-h-[3.5rem]',
194
+ optionCheck: 'w-6 h-6',
195
+ groupLabel: 'px-4',
178
196
  control: 'min-h-14 gap-2 px-4 py-2.5 pr-12',
179
197
  search: 'text-xl',
180
198
  tag: 'text-lg px-3 py-1',
@@ -138,8 +138,8 @@ interface ComboboxBaseProps<T extends SelectValue = string> extends ComboboxVari
138
138
  unstyled?: boolean;
139
139
  /** Per-slot class overrides merged with tv() styles. Slots: base | label | requiredMark |
140
140
  * inputWrapper | input | message | helper | listbox | option | optionActive | optionSelected |
141
- * group | groupLabel | loading | noResults | clear | chevron | control | search | tag |
142
- * tagLabel | tagRemove */
141
+ * optionCheck | group | groupLabel | loading | noResults | clear | chevron | control | search |
142
+ * tag | tagLabel | tagRemove */
143
143
  slotClasses?: Partial<Record<ComboboxSlots, string>>;
144
144
  /**
145
145
  * Apply a named preset registered via `<BlocksProvider presets={{ Combobox: {...} }}>`.
@@ -27,7 +27,9 @@ export const menuVariants = tv({
27
27
  header: ['sticky top-0 z-10 bg-surface-elevated', 'px-2 py-1 border-b border-border-hairline'],
28
28
  section: ['px-3 py-1.5 text-xs font-medium text-text-tertiary'],
29
29
  divider: ['my-1 h-px bg-border-hairline'],
30
- items: ['py-1 space-y-0.5'],
30
+ // No extra `py` — the `content` slot's `p-1` is the single edge inset
31
+ // (symmetric 4px, same rhythm as the Select/Combobox listboxes, XC-9).
32
+ items: ['space-y-0.5'],
31
33
  // tier: modify — menu items are momentary affordances inside the contain panel.
32
34
  item: [
33
35
  'flex w-full items-center gap-2 rounded-modify text-left',
@@ -35,7 +37,9 @@ export const menuVariants = tv({
35
37
  'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-primary/50',
36
38
  'cursor-pointer select-none'
37
39
  ],
38
- indicator: ['mr-2 inline-flex items-center self-center'],
40
+ // No `mr-*` — the item's `gap-2` owns the icon↔label distance (8px, the
41
+ // same gap every listbox row in the library uses).
42
+ indicator: ['inline-flex items-center self-center'],
39
43
  submenu: ['ml-4 mt-2 border-l border-border-hairline pl-2'],
40
44
  footer: [
41
45
  'sticky bottom-0 z-10 bg-surface-elevated',
@@ -57,11 +61,15 @@ export const menuVariants = tv({
57
61
  content: 'min-w-48'
58
62
  }
59
63
  },
60
- // Size for menu items
64
+ // Size for menu items. Typo tracks the Button ladder (text-sm/base/lg —
65
+ // Action-family items read like the Button that opened them); py/min-h
66
+ // sit on the shared listbox baseline (2 / 2.5 / 3 rem, XC-9). The section
67
+ // header always shares the item's horizontal inset (px-3 lives on the
68
+ // `section` slot base for md).
61
69
  itemSize: {
62
- sm: { item: 'px-2 py-1.5 text-sm min-h-[2rem]' },
70
+ sm: { item: 'px-2 py-1.5 text-sm min-h-[2rem]', section: 'px-2' },
63
71
  md: { item: 'px-3 py-2 text-base min-h-[2.5rem]' },
64
- lg: { item: 'px-4 py-2.5 text-lg min-h-[3rem]' }
72
+ lg: { item: 'px-4 py-2.5 text-lg min-h-[3rem]', section: 'px-4' }
65
73
  },
66
74
  disabled: {
67
75
  true: {
@@ -12,7 +12,7 @@
12
12
  value = $bindable(),
13
13
  onValueChange,
14
14
  size = 'md',
15
- appearance = 'default',
15
+ variant = 'default',
16
16
  tier,
17
17
  fullWidth = false,
18
18
  disabled = false,
@@ -52,7 +52,7 @@
52
52
 
53
53
  const variantProps: SegmentGroupVariants = $derived({
54
54
  size,
55
- appearance,
55
+ variant,
56
56
  tier: effectiveTier,
57
57
  fullWidth,
58
58
  disabled
@@ -111,8 +111,8 @@
111
111
  get size() {
112
112
  return size;
113
113
  },
114
- get appearance() {
115
- return appearance;
114
+ get variant() {
115
+ return variant;
116
116
  },
117
117
  get tier() {
118
118
  return effectiveTier;
@@ -28,7 +28,7 @@
28
28
 
29
29
  const variantProps: SegmentGroupVariants = $derived({
30
30
  size: ctx.size,
31
- appearance: ctx.appearance,
31
+ variant: ctx.variant,
32
32
  tier: ctx.tier
33
33
  });
34
34
 
@@ -11,7 +11,7 @@ export interface SegmentGroupContext {
11
11
  selectItem: (value: string) => void;
12
12
  isActive: (value: string) => boolean;
13
13
  readonly size: NonNullable<SegmentGroupVariants['size']>;
14
- readonly appearance: NonNullable<SegmentGroupVariants['appearance']>;
14
+ readonly variant: NonNullable<SegmentGroupVariants['variant']>;
15
15
  readonly tier: InteractiveTier;
16
16
  readonly disabled: boolean;
17
17
  readonly unstyled: boolean;
@@ -1,14 +1,14 @@
1
1
  import { type SlotNames, type VariantProps } from '../../utils/variants.js';
2
2
  export declare const segmentGroupVariants: ((props?: {
3
3
  tier?: "commit" | "modify" | undefined;
4
- appearance?: "text" | "default" | undefined;
4
+ variant?: "text" | "default" | undefined;
5
5
  size?: "sm" | "md" | "lg" | undefined;
6
6
  fullWidth?: boolean | undefined;
7
7
  disabled?: boolean | undefined;
8
8
  } | undefined) => {
9
9
  base: (props?: ({
10
10
  tier?: "commit" | "modify" | undefined;
11
- appearance?: "text" | "default" | undefined;
11
+ variant?: "text" | "default" | undefined;
12
12
  size?: "sm" | "md" | "lg" | undefined;
13
13
  fullWidth?: boolean | undefined;
14
14
  disabled?: boolean | undefined;
@@ -17,7 +17,7 @@ export declare const segmentGroupVariants: ((props?: {
17
17
  }) | undefined) => string;
18
18
  indicator: (props?: ({
19
19
  tier?: "commit" | "modify" | undefined;
20
- appearance?: "text" | "default" | undefined;
20
+ variant?: "text" | "default" | undefined;
21
21
  size?: "sm" | "md" | "lg" | undefined;
22
22
  fullWidth?: boolean | undefined;
23
23
  disabled?: boolean | undefined;
@@ -26,7 +26,7 @@ export declare const segmentGroupVariants: ((props?: {
26
26
  }) | undefined) => string;
27
27
  item: (props?: ({
28
28
  tier?: "commit" | "modify" | undefined;
29
- appearance?: "text" | "default" | undefined;
29
+ variant?: "text" | "default" | undefined;
30
30
  size?: "sm" | "md" | "lg" | undefined;
31
31
  fullWidth?: boolean | undefined;
32
32
  disabled?: boolean | undefined;