@adea-ai/ui 0.118.2 → 0.119.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 (62) hide show
  1. package/README.md +7 -3
  2. package/dist/components/composites/settings/index.d.ts +1 -1
  3. package/dist/components/composites/settings/index.d.ts.map +1 -1
  4. package/dist/components/composites/settings/settings-layout.d.ts +18 -2
  5. package/dist/components/composites/settings/settings-layout.d.ts.map +1 -1
  6. package/dist/components/composites/settings/settings-layout.js +40 -5
  7. package/dist/components/composites/settings/settings-layout.js.map +1 -1
  8. package/dist/components/composites/settings/settings-navigation.d.ts.map +1 -1
  9. package/dist/components/composites/settings/settings-navigation.js +36 -14
  10. package/dist/components/composites/settings/settings-navigation.js.map +1 -1
  11. package/dist/components/ui/code-block/code-block.d.ts +13 -0
  12. package/dist/components/ui/code-block/code-block.d.ts.map +1 -1
  13. package/dist/components/ui/code-block/code-block.js +22 -3
  14. package/dist/components/ui/code-block/code-block.js.map +1 -1
  15. package/dist/components/ui/combobox/combobox.d.ts.map +1 -1
  16. package/dist/components/ui/combobox/combobox.js +1 -0
  17. package/dist/components/ui/combobox/combobox.js.map +1 -1
  18. package/dist/components/ui/context-menu/context-menu.d.ts.map +1 -1
  19. package/dist/components/ui/context-menu/context-menu.js +2 -0
  20. package/dist/components/ui/context-menu/context-menu.js.map +1 -1
  21. package/dist/components/ui/item/index.d.ts +1 -1
  22. package/dist/components/ui/item/index.d.ts.map +1 -1
  23. package/dist/components/ui/item/item.d.ts +17 -1
  24. package/dist/components/ui/item/item.d.ts.map +1 -1
  25. package/dist/components/ui/item/item.js +17 -10
  26. package/dist/components/ui/item/item.js.map +1 -1
  27. package/dist/components/ui/menubar/menubar.d.ts.map +1 -1
  28. package/dist/components/ui/menubar/menubar.js +2 -0
  29. package/dist/components/ui/menubar/menubar.js.map +1 -1
  30. package/dist/components/ui/select/select.d.ts.map +1 -1
  31. package/dist/components/ui/select/select.js +1 -0
  32. package/dist/components/ui/select/select.js.map +1 -1
  33. package/dist/lib/overlay.d.ts +6 -0
  34. package/dist/lib/overlay.d.ts.map +1 -1
  35. package/dist/lib/overlay.js +9 -2
  36. package/dist/lib/overlay.js.map +1 -1
  37. package/dist/r/registry.json +3 -1
  38. package/dist/r/settings.json +3 -1
  39. package/dist/r/src/components/composites/settings/index.ts +5 -1
  40. package/dist/r/src/components/composites/settings/settings-layout.tsx +48 -5
  41. package/dist/r/src/components/composites/settings/settings-navigation.tsx +44 -16
  42. package/dist/r/src/components/ui/code-block/code-block.tsx +27 -2
  43. package/dist/r/src/components/ui/combobox/combobox.tsx +4 -1
  44. package/dist/r/src/components/ui/context-menu/context-menu.tsx +8 -2
  45. package/dist/r/src/components/ui/item/index.ts +1 -0
  46. package/dist/r/src/components/ui/item/item.tsx +41 -5
  47. package/dist/r/src/components/ui/menubar/menubar.tsx +8 -2
  48. package/dist/r/src/components/ui/select/select.tsx +4 -1
  49. package/dist/r/src/lib/overlay.ts +9 -2
  50. package/package.json +1 -1
  51. package/registry.json +3 -1
  52. package/src/components/composites/settings/index.ts +5 -1
  53. package/src/components/composites/settings/settings-layout.tsx +48 -5
  54. package/src/components/composites/settings/settings-navigation.tsx +44 -16
  55. package/src/components/ui/code-block/code-block.tsx +27 -2
  56. package/src/components/ui/combobox/combobox.tsx +4 -1
  57. package/src/components/ui/context-menu/context-menu.tsx +8 -2
  58. package/src/components/ui/item/index.ts +1 -0
  59. package/src/components/ui/item/item.tsx +41 -5
  60. package/src/components/ui/menubar/menubar.tsx +8 -2
  61. package/src/components/ui/select/select.tsx +4 -1
  62. package/src/lib/overlay.ts +9 -2
@@ -1,5 +1,5 @@
1
1
  import type { ComponentProps, JSX } from 'solid-js'
2
- import { splitProps } from 'solid-js'
2
+ import { createSignal, onCleanup, onMount, splitProps } from 'solid-js'
3
3
  import { cn } from '#lib/utils'
4
4
  import { Tabs } from '../../ui/tabs'
5
5
  import { SettingsNavigation, type SettingsNavigationGroup } from './settings-navigation'
@@ -10,7 +10,7 @@ export type SettingsLayoutProps = Omit<
10
10
  > & {
11
11
  /** Grouped navigation entries; the host owns their values and panel content. */
12
12
  groups: readonly SettingsNavigationGroup[]
13
- /** Accessible name for the vertical tab list. */
13
+ /** Accessible name for the tab list. */
14
14
  'aria-label': string
15
15
  /** The host-controlled selected destination. */
16
16
  value: string
@@ -20,16 +20,42 @@ export type SettingsLayoutProps = Omit<
20
20
  onReselect?: (value: string) => void
21
21
  /** Reveal the selected row on value changes and window resizes. Defaults to nearest scroll. */
22
22
  revealSelected?: boolean | ((trigger: HTMLButtonElement | undefined) => void)
23
+ /**
24
+ * How the section navigation sits beside the panels. `auto` (the default) is
25
+ * the vertical rail from 48rem of viewport width up and a horizontal,
26
+ * scrollable strip of tabs above the panels below it. `rail` and `strip` pin
27
+ * one shape at every width.
28
+ */
29
+ navigationLayout?: SettingsNavigationLayout
23
30
  /** Panels must be rendered as `TabsContent` descendants. */
24
31
  children?: JSX.Element
25
32
  }
26
33
 
34
+ export type SettingsNavigationLayout = 'auto' | 'rail' | 'strip'
35
+
36
+ /** The `md` breakpoint the rail has always widened at; below it the rail becomes a strip. */
37
+ const RAIL_MEDIA_QUERY = '(min-width: 48rem)'
38
+
39
+ function railQuery(): MediaQueryList | undefined {
40
+ return typeof window === 'undefined' || typeof window.matchMedia !== 'function'
41
+ ? undefined
42
+ : window.matchMedia(RAIL_MEDIA_QUERY)
43
+ }
44
+
27
45
  /**
28
46
  * SettingsLayout.
29
47
  *
30
48
  * A controlled, vertical settings rail beside its scrollable panel viewport.
31
49
  * The host keeps ownership of selection, URL/hash state and panel content.
32
- * At narrow viewports the rail contracts while both columns remain shrinkable.
50
+ *
51
+ * Below 48rem the rail becomes a horizontal strip of tabs above the panels.
52
+ * A 6rem rail at phone width clips every label, so this is the default rather
53
+ * than an opt-in. It changes the tab root's orientation, not just its
54
+ * styling: the list's `aria-orientation` and Kobalte's arrow keys (Up/Down on
55
+ * the rail, Left/Right on the strip) follow the axis the reader sees. The
56
+ * breakpoint is the viewport's, like CatalogBrowser's and ContextualSidebar's,
57
+ * and is read before the first render so a phone never paints the rail. A
58
+ * server render, which has no viewport, renders the rail.
33
59
  */
34
60
  export function SettingsLayout(props: SettingsLayoutProps) {
35
61
  const [local, rest] = splitProps(props, [
@@ -40,12 +66,29 @@ export function SettingsLayout(props: SettingsLayoutProps) {
40
66
  'value',
41
67
  'onReselect',
42
68
  'revealSelected',
69
+ 'navigationLayout',
43
70
  ])
44
71
 
72
+ const [wide, setWide] = createSignal(railQuery()?.matches ?? true)
73
+ onMount(() => {
74
+ const query = railQuery()
75
+ if (!query) return
76
+ const update = () => setWide(query.matches)
77
+ update()
78
+ query.addEventListener('change', update)
79
+ onCleanup(() => query.removeEventListener('change', update))
80
+ })
81
+ const layout = (): 'rail' | 'strip' => {
82
+ const requested = local.navigationLayout ?? 'auto'
83
+ if (requested !== 'auto') return requested
84
+ return wide() ? 'rail' : 'strip'
85
+ }
86
+
45
87
  return (
46
88
  <Tabs
47
89
  {...rest}
48
- orientation="vertical"
90
+ orientation={layout() === 'rail' ? 'vertical' : 'horizontal'}
91
+ data-navigation-layout={layout()}
49
92
  value={local.value}
50
93
  class={cn('min-h-0 min-w-0 flex-1 gap-0', local.class)}
51
94
  >
@@ -55,7 +98,7 @@ export function SettingsLayout(props: SettingsLayoutProps) {
55
98
  value={local.value}
56
99
  onReselect={local.onReselect}
57
100
  revealSelected={local.revealSelected}
58
- class="w-24 md:w-sidebar"
101
+ class={layout() === 'rail' ? 'w-24 md:w-sidebar' : undefined}
59
102
  />
60
103
  <div data-slot="settings-layout-panels" class="min-h-0 min-w-0 flex-1 overflow-y-auto p-4">
61
104
  {local.children}
@@ -1,5 +1,6 @@
1
+ import { useTabsContext } from '@kobalte/core/tabs'
1
2
  import type { JSX } from 'solid-js'
2
- import { createEffect, createUniqueId, onCleanup, splitProps } from 'solid-js'
3
+ import { createEffect, createUniqueId, onCleanup, Show, splitProps } from 'solid-js'
3
4
  import { cn } from '#lib/utils'
4
5
  import { sidebarNavItemClass, sidebarNavItemStateClass } from '../../layout/sidebar-nav/sidebar-nav'
5
6
  import { TabsList, TabsTrigger, type TabsListProps } from '../../ui/tabs'
@@ -47,6 +48,14 @@ export type SettingsNavigationProps = Omit<
47
48
  * the controlled value, panel content, icons and any persistence or routing.
48
49
  * Use ordinary tabs for a short, ungrouped row of view choices.
49
50
  *
51
+ * The shape follows the enclosing `Tabs` orientation rather than a prop of its
52
+ * own, so the visual axis can never disagree with the arrow keys Kobalte
53
+ * binds: `vertical` is the grouped rail, `horizontal` is a single scrollable
54
+ * strip of tabs (what `SettingsLayout` shows below 48rem). The strip keeps the
55
+ * groups in order, separated by a rule; their visible headings drop out, since
56
+ * an overline between tabs reads as another tab, but each tab still carries its
57
+ * group as its accessible description.
58
+ *
50
59
  * Kobalte owns roving focus, arrow keys, selection and tab/panel ARIA. This
51
60
  * component adds no URL, storage or host-specific state. Set
52
61
  * `revealSelected={false}` when the surrounding layout does not use an internal
@@ -88,12 +97,16 @@ export function SettingsNavigation(props: SettingsNavigationProps) {
88
97
  'onReselect',
89
98
  'revealSelected',
90
99
  ])
100
+ const tabs = useTabsContext()
101
+ const strip = () => tabs.orientation() === 'horizontal'
91
102
  const triggers = new Map<string, HTMLButtonElement>()
92
103
  const groupIdPrefix = `settings-navigation-${createUniqueId()}`
93
104
 
94
105
  createEffect(() => {
95
106
  const value = local.value
96
107
  const reveal = local.revealSelected
108
+ // Tracked so a rail that becomes a strip (or back) re-reveals on the new axis.
109
+ strip()
97
110
  if (reveal === false) return
98
111
 
99
112
  const revealCurrent = () => {
@@ -111,9 +124,17 @@ export function SettingsNavigation(props: SettingsNavigationProps) {
111
124
  <TabsList
112
125
  data-slot="settings-navigation"
113
126
  aria-label={local['aria-label']}
114
- orientation="vertical"
127
+ orientation={tabs.orientation()}
115
128
  class={cn(
116
- 'flex max-h-full min-h-0 min-w-0 shrink-0 flex-col items-stretch justify-start gap-2 overflow-x-hidden overflow-y-auto px-2 py-2',
129
+ // Positioned so the visually hidden group names are laid out (and
130
+ // clipped) inside the list: unpositioned, their absolute boxes escape a
131
+ // scrolled strip and widen the page by however far it has scrolled.
132
+ 'relative flex min-w-0 shrink-0 justify-start',
133
+ strip()
134
+ ? // The block padding is the room the focus ring needs: a scroller
135
+ // clips at its padding edge, and this one scrolls sideways only.
136
+ 'scroll-fade w-full flex-row items-center gap-1 overflow-x-auto overflow-y-hidden border-b border-border px-2 py-1.5'
137
+ : 'max-h-full min-h-0 flex-col items-stretch gap-2 overflow-x-hidden overflow-y-auto px-2 py-2',
117
138
  local.class
118
139
  )}
119
140
  {...rest}
@@ -124,24 +145,30 @@ export function SettingsNavigation(props: SettingsNavigationProps) {
124
145
  <div
125
146
  role="none"
126
147
  data-slot="settings-navigation-group"
127
- class="flex min-w-0 flex-col gap-0.5"
148
+ class={cn(
149
+ 'flex gap-0.5',
150
+ strip()
151
+ ? 'shrink-0 flex-row items-center not-first:ms-1 not-first:border-s not-first:border-border not-first:ps-2'
152
+ : 'min-w-0 flex-col'
153
+ )}
128
154
  >
129
155
  <span id={groupId} class="sr-only">
130
156
  {group.label}
131
157
  </span>
132
- {/* The overline role on the sidebar's own muted foreground. */}
133
- <Text
134
- variant="overline"
135
- tone="inherit"
136
- aria-hidden="true"
137
- class="px-2 py-1.5 text-sidebar-muted-foreground"
138
- >
139
- {group.label}
140
- </Text>
158
+ <Show when={!strip()}>
159
+ {/* The overline role on the sidebar's own muted foreground. */}
160
+ <Text
161
+ variant="overline"
162
+ tone="inherit"
163
+ aria-hidden="true"
164
+ class="px-2 py-1.5 text-sidebar-muted-foreground"
165
+ >
166
+ {group.label}
167
+ </Text>
168
+ </Show>
141
169
  {group.items.map((item) => (
142
170
  <TabsTrigger
143
171
  data-slot="settings-navigation-trigger"
144
- orientation="vertical"
145
172
  aria-describedby={groupId}
146
173
  value={item.value}
147
174
  disabled={item.disabled}
@@ -155,11 +182,12 @@ export function SettingsNavigation(props: SettingsNavigationProps) {
155
182
  class={cn(
156
183
  sidebarNavItemClass,
157
184
  sidebarNavItemStateClass.idle,
158
- 'h-row-lg w-full justify-start border-0 px-3 py-2 text-left data-[selected]:bg-primary-subtle data-[selected]:text-foreground'
185
+ 'h-row-lg justify-start border-0 px-3 py-2 text-left data-[selected]:bg-primary-subtle data-[selected]:text-foreground',
186
+ strip() ? 'w-auto shrink-0' : 'w-full'
159
187
  )}
160
188
  >
161
189
  {item.icon}
162
- <span class="min-w-0 truncate">{item.label}</span>
190
+ <span class={cn('min-w-0', { truncate: !strip() })}>{item.label}</span>
163
191
  </TabsTrigger>
164
192
  ))}
165
193
  </div>
@@ -69,6 +69,19 @@ export type CodeBlockProps = Omit<ComponentProps<'div'>, 'children'> & {
69
69
  showLineNumbers?: boolean
70
70
  /** The height above which the action row is repeated below the block. */
71
71
  tallThreshold?: number
72
+ /**
73
+ * Cap the code body's height and scroll it vertically past that. A number is
74
+ * pixels; a string is any CSS length (`'20rem'`, `'50vh'`). Unset, the block
75
+ * grows with its content.
76
+ */
77
+ maxHeight?: number | string
78
+ /**
79
+ * Soft-wrap long lines inside the block instead of scrolling sideways. Off by
80
+ * default: wrapped code loses its column alignment, so it is for prose-like
81
+ * output (logs, long commands) in a narrow column. Wraps the plain renderer and
82
+ * any `pre` inside `highlight` output; line numbers stay on the first visual line.
83
+ */
84
+ wrap?: boolean
72
85
  }
73
86
 
74
87
  /** The height a block must exceed before its actions are repeated. */
@@ -103,6 +116,8 @@ export function CodeBlock(props: CodeBlockProps) {
103
116
  'lineHeight',
104
117
  'showLineNumbers',
105
118
  'tallThreshold',
119
+ 'maxHeight',
120
+ 'wrap',
106
121
  'class',
107
122
  'ref',
108
123
  ])
@@ -114,6 +129,8 @@ export function CodeBlock(props: CodeBlockProps) {
114
129
  const label = () => local.title ?? local.language ?? 'code'
115
130
  const regionLabel = () => (local.language ? `${local.language} code` : 'code')
116
131
  const lines = createMemo(() => local.code.replace(/\n$/, '').split('\n'))
132
+ const maxHeight = () =>
133
+ typeof local.maxHeight === 'number' ? `${local.maxHeight}px` : local.maxHeight
117
134
 
118
135
  const copy = async () => {
119
136
  const ok = await copyText(local.code)
@@ -160,7 +177,15 @@ export function CodeBlock(props: CodeBlockProps) {
160
177
  <Actions />
161
178
  </div>
162
179
  <div
163
- class="scroll-fade overflow-x-auto"
180
+ data-slot="code-block-body"
181
+ data-wrap={local.wrap ? '' : undefined}
182
+ class={cn('scroll-fade overflow-x-auto', {
183
+ // A capped body scrolls on both axes; the region is already a tab stop,
184
+ // so the keyboard reaches the vertical overflow the same way.
185
+ 'overflow-y-auto': local.maxHeight !== undefined,
186
+ '[&_pre]:whitespace-pre-wrap [&_pre]:[overflow-wrap:anywhere]': local.wrap,
187
+ })}
188
+ style={maxHeight() === undefined ? undefined : { 'max-height': maxHeight() }}
164
189
  // A region that scrolls must be focusable, and a tab stop needs a name.
165
190
  tabindex="0"
166
191
  role="region"
@@ -181,7 +206,7 @@ export function CodeBlock(props: CodeBlockProps) {
181
206
  >
182
207
  {index() + 1}
183
208
  </span>
184
- <span>{line || ' '}</span>
209
+ <span class={cn({ 'min-w-0': local.wrap })}>{line || ' '}</span>
185
210
  </span>
186
211
  )}
187
212
  </For>
@@ -135,7 +135,10 @@ export function ComboboxItem(props: ComponentProps<typeof KobalteCombobox.Item>)
135
135
  <KobalteCombobox.ItemLabel>{local.children}</KobalteCombobox.ItemLabel>
136
136
  {/* The indicator reads in the accent, like every other selected state in
137
137
  the system. */}
138
- <KobalteCombobox.ItemIndicator class="absolute end-2 flex size-4 items-center justify-center text-primary">
138
+ <KobalteCombobox.ItemIndicator
139
+ data-slot="combobox-indicator"
140
+ class="absolute end-2 flex size-4 items-center justify-center text-primary"
141
+ >
139
142
  <Check class="size-4" />
140
143
  </KobalteCombobox.ItemIndicator>
141
144
  </KobalteCombobox.Item>
@@ -111,7 +111,10 @@ export function ContextMenuCheckboxItem(
111
111
  {local.children}
112
112
  {/* The indicator reads in the accent, like every other selected state in
113
113
  the system. */}
114
- <KobalteContextMenu.ItemIndicator class="absolute end-2 flex size-4 items-center justify-center text-primary">
114
+ <KobalteContextMenu.ItemIndicator
115
+ data-slot="context-menu-indicator"
116
+ class="absolute end-2 flex size-4 items-center justify-center text-primary"
117
+ >
115
118
  <Check class="size-4" />
116
119
  </KobalteContextMenu.ItemIndicator>
117
120
  </KobalteContextMenu.CheckboxItem>
@@ -130,7 +133,10 @@ export function ContextMenuRadioItem(props: ComponentProps<typeof KobalteContext
130
133
  {local.children}
131
134
  {/* The indicator reads in the accent, like every other selected state in
132
135
  the system. */}
133
- <KobalteContextMenu.ItemIndicator class="absolute end-2 flex size-4 items-center justify-center text-primary">
136
+ <KobalteContextMenu.ItemIndicator
137
+ data-slot="context-menu-indicator"
138
+ class="absolute end-2 flex size-4 items-center justify-center text-primary"
139
+ >
134
140
  <Circle class="size-2 fill-current" />
135
141
  </KobalteContextMenu.ItemIndicator>
136
142
  </KobalteContextMenu.RadioItem>
@@ -6,5 +6,6 @@ export {
6
6
  ItemGroupEntry,
7
7
  ItemTitle,
8
8
  itemVariants,
9
+ type ItemDescriptionProps,
9
10
  type ItemProps,
10
11
  } from './item'
@@ -47,6 +47,14 @@ export type ItemProps = ComponentProps<'div'> &
47
47
  media?: JSX.Element
48
48
  /** A trailing slot: a badge, a timestamp, a menu trigger. */
49
49
  trailing?: JSX.Element
50
+ /**
51
+ * Move the trailing slot onto its own line, end-aligned under the body, once
52
+ * the item is narrower than 28rem — the width `ListRow` stacks its trailing
53
+ * controls at. Off by default: a row of badges and an icon button fits beside
54
+ * the body, but a pair of labelled buttons squeezes a phone-width body to a
55
+ * few characters.
56
+ */
57
+ stackTrailing?: boolean
50
58
  }
51
59
 
52
60
  export function Item(props: ItemProps) {
@@ -56,13 +64,22 @@ export function Item(props: ItemProps) {
56
64
  'size',
57
65
  'media',
58
66
  'trailing',
67
+ 'stackTrailing',
59
68
  'children',
60
69
  ])
61
70
 
62
71
  return (
63
72
  <div
64
73
  data-slot="item"
65
- class={cn(itemVariants({ variant: local.variant, size: local.size }), local.class)}
74
+ data-stack-trailing={local.stackTrailing ? '' : undefined}
75
+ class={cn(
76
+ itemVariants({ variant: local.variant, size: local.size }),
77
+ // The item is its own query container: the trailing slot answers to the
78
+ // width the item was given, not the viewport, so an item in a narrow
79
+ // column of a wide window stacks too.
80
+ { '@container/item flex-wrap': local.stackTrailing },
81
+ local.class
82
+ )}
66
83
  {...rest}
67
84
  >
68
85
  <Show when={local.media}>
@@ -77,7 +94,13 @@ export function Item(props: ItemProps) {
77
94
  {local.children}
78
95
  </div>
79
96
  <Show when={local.trailing}>
80
- <span data-slot="item-trailing" class="flex shrink-0 items-center gap-2">
97
+ <span
98
+ data-slot="item-trailing"
99
+ class={cn('flex shrink-0 items-center gap-2', {
100
+ '@max-md/item:w-full @max-md/item:flex-wrap @max-md/item:justify-end':
101
+ local.stackTrailing,
102
+ })}
103
+ >
81
104
  {local.trailing}
82
105
  </span>
83
106
  </Show>
@@ -90,12 +113,25 @@ export function ItemTitle(props: ComponentProps<'div'>) {
90
113
  return <div data-slot="item-title" class={cn('truncate font-medium', local.class)} {...rest} />
91
114
  }
92
115
 
93
- export function ItemDescription(props: ComponentProps<'div'>) {
94
- const [local, rest] = splitProps(props, ['class'])
116
+ export type ItemDescriptionProps = ComponentProps<'div'> & {
117
+ /**
118
+ * Clamp to two lines. On by default, so a grid of items keeps one height; pass
119
+ * `false` where the description is the content — a setting's explanation, an
120
+ * error the reader has to read in full.
121
+ */
122
+ clamp?: boolean
123
+ }
124
+
125
+ export function ItemDescription(props: ItemDescriptionProps) {
126
+ const [local, rest] = splitProps(props, ['class', 'clamp'])
95
127
  return (
96
128
  <div
97
129
  data-slot="item-description"
98
- class={cn('text-muted-foreground line-clamp-2 text-xs text-pretty', local.class)}
130
+ class={cn(
131
+ 'text-muted-foreground text-xs text-pretty',
132
+ { 'line-clamp-2': local.clamp !== false },
133
+ local.class
134
+ )}
99
135
  {...rest}
100
136
  />
101
137
  )
@@ -141,7 +141,10 @@ export function MenubarCheckboxItem(props: ComponentProps<typeof KobalteMenubar.
141
141
  {local.children}
142
142
  {/* The indicator reads in the accent, like every other selected state in
143
143
  the system. */}
144
- <KobalteMenubar.ItemIndicator class="absolute end-2 flex size-4 items-center justify-center text-primary">
144
+ <KobalteMenubar.ItemIndicator
145
+ data-slot="menubar-indicator"
146
+ class="absolute end-2 flex size-4 items-center justify-center text-primary"
147
+ >
145
148
  <Check class="size-4" />
146
149
  </KobalteMenubar.ItemIndicator>
147
150
  </KobalteMenubar.CheckboxItem>
@@ -159,7 +162,10 @@ export function MenubarRadioItem(props: ComponentProps<typeof KobalteMenubar.Rad
159
162
  {local.children}
160
163
  {/* The indicator reads in the accent, like every other selected state in
161
164
  the system. */}
162
- <KobalteMenubar.ItemIndicator class="absolute end-2 flex size-4 items-center justify-center text-primary">
165
+ <KobalteMenubar.ItemIndicator
166
+ data-slot="menubar-indicator"
167
+ class="absolute end-2 flex size-4 items-center justify-center text-primary"
168
+ >
163
169
  <Circle class="size-2 fill-current" />
164
170
  </KobalteMenubar.ItemIndicator>
165
171
  </KobalteMenubar.RadioItem>
@@ -192,7 +192,10 @@ export function SelectItem(
192
192
  <KobalteSelect.ItemLabel>{local.children}</KobalteSelect.ItemLabel>
193
193
  {/* The indicator reads in the accent, like every other selected state in
194
194
  the system — the row's own foreground reads as decoration. */}
195
- <KobalteSelect.ItemIndicator class="absolute end-2 flex size-4 items-center justify-center text-primary">
195
+ <KobalteSelect.ItemIndicator
196
+ data-slot="select-indicator"
197
+ class="absolute end-2 flex size-4 items-center justify-center text-primary"
198
+ >
196
199
  <Check class="size-4" />
197
200
  </KobalteSelect.ItemIndicator>
198
201
  </KobalteSelect.Item>
@@ -83,14 +83,21 @@ export const popoverArrow = 'fill-popover stroke-border'
83
83
  /**
84
84
  * A menu row: the shared height, radius, hover fill and focus treatment for
85
85
  * every item inside a menu, listbox or command list.
86
+ *
87
+ * The row sizes and dims the icons a caller puts in it, but not the glyph of
88
+ * its selection indicator (any `[data-slot$=-indicator]`): a descendant `svg`
89
+ * rule outranks a single utility on the glyph, so without the exclusion a
90
+ * radio item's `size-2` dot drew as a 16px muted disc and every check lost the
91
+ * indicator's accent.
86
92
  */
87
93
  export const menuItem = [
88
94
  'relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none select-none',
89
95
  'transition-colors ease-out',
90
96
  'data-[highlighted]:bg-surface-hover data-[highlighted]:text-foreground',
91
97
  'data-[disabled]:pointer-events-none data-[disabled]:text-muted-foreground',
92
- '[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg]:size-4',
93
- '[&_svg:not([class*=text-])]:text-muted-foreground',
98
+ '[&_svg]:pointer-events-none [&_svg]:shrink-0',
99
+ '[&_svg:not([data-slot$=-indicator]_*)]:size-4',
100
+ '[&_svg:not([class*=text-]):not([data-slot$=-indicator]_*)]:text-muted-foreground',
94
101
  ].join(' ')
95
102
 
96
103
  /** A destructive action keeps readable text and marks its row with a status edge. */