@adea-ai/ui 0.120.0 → 0.122.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 (63) hide show
  1. package/dist/components/composites/account-menu/account-menu.d.ts +8 -0
  2. package/dist/components/composites/account-menu/account-menu.d.ts.map +1 -1
  3. package/dist/components/composites/account-menu/account-menu.js +43 -15
  4. package/dist/components/composites/account-menu/account-menu.js.map +1 -1
  5. package/dist/components/conversation/composer-menu.d.ts +31 -0
  6. package/dist/components/conversation/composer-menu.d.ts.map +1 -0
  7. package/dist/components/conversation/composer-menu.js +63 -0
  8. package/dist/components/conversation/composer-menu.js.map +1 -0
  9. package/dist/components/conversation/conversation-pane.d.ts +38 -0
  10. package/dist/components/conversation/conversation-pane.d.ts.map +1 -0
  11. package/dist/components/conversation/conversation-pane.js +83 -0
  12. package/dist/components/conversation/conversation-pane.js.map +1 -0
  13. package/dist/components/conversation/conversation-surface.d.ts +6 -0
  14. package/dist/components/conversation/conversation-surface.d.ts.map +1 -1
  15. package/dist/components/conversation/conversation-surface.js +10 -4
  16. package/dist/components/conversation/conversation-surface.js.map +1 -1
  17. package/dist/components/conversation/index.d.ts +2 -0
  18. package/dist/components/conversation/index.d.ts.map +1 -1
  19. package/dist/components/conversation/index.js +3 -1
  20. package/dist/components/conversation/message-composer.d.ts +6 -0
  21. package/dist/components/conversation/message-composer.d.ts.map +1 -1
  22. package/dist/components/conversation/message-composer.js +10 -1
  23. package/dist/components/conversation/message-composer.js.map +1 -1
  24. package/dist/components/ui/command/command.d.ts +13 -0
  25. package/dist/components/ui/command/command.d.ts.map +1 -1
  26. package/dist/components/ui/command/command.js +35 -2
  27. package/dist/components/ui/command/command.js.map +1 -1
  28. package/dist/components/ui/command/index.d.ts +1 -1
  29. package/dist/components/ui/command/index.d.ts.map +1 -1
  30. package/dist/components/ui/command/index.js +2 -2
  31. package/dist/components/ui/kbd/index.d.ts +1 -0
  32. package/dist/components/ui/kbd/index.d.ts.map +1 -1
  33. package/dist/components/ui/kbd/index.js +2 -1
  34. package/dist/components/ui/kbd/platform-modifier.d.ts +28 -0
  35. package/dist/components/ui/kbd/platform-modifier.d.ts.map +1 -0
  36. package/dist/components/ui/kbd/platform-modifier.js +37 -0
  37. package/dist/components/ui/kbd/platform-modifier.js.map +1 -0
  38. package/dist/index.js +5 -2
  39. package/dist/r/conversation.json +10 -0
  40. package/dist/r/kbd.json +5 -0
  41. package/dist/r/registry.json +15 -0
  42. package/dist/r/src/components/composites/account-menu/account-menu.tsx +44 -4
  43. package/dist/r/src/components/conversation/composer-menu.tsx +67 -0
  44. package/dist/r/src/components/conversation/conversation-pane.tsx +111 -0
  45. package/dist/r/src/components/conversation/conversation-surface.tsx +12 -1
  46. package/dist/r/src/components/conversation/index.ts +7 -0
  47. package/dist/r/src/components/conversation/message-composer.tsx +8 -0
  48. package/dist/r/src/components/ui/command/command.tsx +35 -0
  49. package/dist/r/src/components/ui/command/index.ts +2 -0
  50. package/dist/r/src/components/ui/kbd/index.ts +5 -0
  51. package/dist/r/src/components/ui/kbd/platform-modifier.ts +37 -0
  52. package/package.json +1 -1
  53. package/registry.json +15 -0
  54. package/src/components/composites/account-menu/account-menu.tsx +44 -4
  55. package/src/components/conversation/composer-menu.tsx +67 -0
  56. package/src/components/conversation/conversation-pane.tsx +111 -0
  57. package/src/components/conversation/conversation-surface.tsx +12 -1
  58. package/src/components/conversation/index.ts +7 -0
  59. package/src/components/conversation/message-composer.tsx +8 -0
  60. package/src/components/ui/command/command.tsx +35 -0
  61. package/src/components/ui/command/index.ts +2 -0
  62. package/src/components/ui/kbd/index.ts +5 -0
  63. package/src/components/ui/kbd/platform-modifier.ts +37 -0
@@ -885,6 +885,11 @@
885
885
  "path": "src/components/ui/kbd/kbd.tsx",
886
886
  "type": "registry:ui",
887
887
  "target": "components/ui/kbd/kbd.tsx"
888
+ },
889
+ {
890
+ "path": "src/components/ui/kbd/platform-modifier.ts",
891
+ "type": "registry:ui",
892
+ "target": "components/ui/kbd/platform-modifier.ts"
888
893
  }
889
894
  ]
890
895
  },
@@ -2374,11 +2379,21 @@
2374
2379
  "type": "registry:ui",
2375
2380
  "target": "components/conversation/chat-composer.tsx"
2376
2381
  },
2382
+ {
2383
+ "path": "src/components/conversation/composer-menu.tsx",
2384
+ "type": "registry:ui",
2385
+ "target": "components/conversation/composer-menu.tsx"
2386
+ },
2377
2387
  {
2378
2388
  "path": "src/components/conversation/conversation-avatar.tsx",
2379
2389
  "type": "registry:ui",
2380
2390
  "target": "components/conversation/conversation-avatar.tsx"
2381
2391
  },
2392
+ {
2393
+ "path": "src/components/conversation/conversation-pane.tsx",
2394
+ "type": "registry:ui",
2395
+ "target": "components/conversation/conversation-pane.tsx"
2396
+ },
2382
2397
  {
2383
2398
  "path": "src/components/conversation/conversation-surface.tsx",
2384
2399
  "type": "registry:ui",
@@ -84,6 +84,14 @@ export type AccountMenuProps = {
84
84
  platform?: 'desktop' | 'web'
85
85
  /** Whether a session exists. Decides the sign-in/sign-out row. */
86
86
  authenticated: boolean
87
+ /**
88
+ * Draw the pending-update indicator: an accent dot on the trigger and on the
89
+ * `updates` row — the id `createAppMenuItems` mints, the same convention the
90
+ * fallback icons follow — and extend both accessible names with "update
91
+ * available", because the dot itself carries no text. The updater remains the
92
+ * caller's: the prop only mirrors its state into the menu.
93
+ */
94
+ updateAvailable?: boolean
87
95
  /** Local-only applications can omit the session actions altogether. */
88
96
  showSession?: boolean
89
97
  /** Disable the session row while a sign-in or sign-out is in flight. */
@@ -116,6 +124,7 @@ export function AccountMenu(props: AccountMenuProps) {
116
124
  'items',
117
125
  'platform',
118
126
  'authenticated',
127
+ 'updateAvailable',
119
128
  'showSession',
120
129
  'busy',
121
130
  'label',
@@ -142,6 +151,21 @@ export function AccountMenu(props: AccountMenuProps) {
142
151
  const visible = () =>
143
152
  local.items.filter((item) => !item.platform || item.platform === (local.platform ?? 'desktop'))
144
153
 
154
+ // The pending update reaches the user twice on each surface: as a dot and as
155
+ // words. The dot is aria-hidden, so the accessible names carry the state —
156
+ // which is why the names are extended here rather than left to the caller.
157
+ const triggerName = () =>
158
+ `${local.label ?? 'Account and settings'}${local.updateAvailable ? ', update available' : ''}`
159
+ const updateDot = () => (
160
+ <Show when={local.updateAvailable}>
161
+ <span
162
+ data-slot="account-menu-update-dot"
163
+ aria-hidden="true"
164
+ class="absolute top-1.5 right-2.5 size-2 rounded-full bg-primary"
165
+ />
166
+ </Show>
167
+ )
168
+
145
169
  // Built by reference at each return, not once up front: Solid fixes a node's
146
170
  // owner chain when the JSX expression runs, and an eagerly created tree would
147
171
  // never see the Tooltip provider below — Kobalte's TooltipTrigger then throws
@@ -164,13 +188,15 @@ export function AccountMenu(props: AccountMenuProps) {
164
188
  ref={(element: HTMLButtonElement) => (trigger = element)}
165
189
  variant="ghost"
166
190
  size={local.size ?? 'icon-md'}
167
- class={cn('text-muted-foreground', local.class)}
168
- aria-label={local.label ?? 'Account and settings'}
191
+ class={cn('relative text-muted-foreground', local.class)}
192
+ data-slot="account-menu-trigger"
193
+ aria-label={triggerName()}
169
194
  onFocus={() => local.onIntent?.()}
170
195
  onPointerEnter={() => local.onIntent?.()}
171
196
  {...rest}
172
197
  >
173
198
  <UserRound aria-hidden="true" />
199
+ {updateDot()}
174
200
  </TooltipTrigger>
175
201
  }
176
202
  >
@@ -180,13 +206,15 @@ export function AccountMenu(props: AccountMenuProps) {
180
206
  <DropdownMenuTrigger
181
207
  as={SideRailButton}
182
208
  ref={(element: HTMLButtonElement) => (trigger = element)}
183
- label={local.label ?? 'Account and settings'}
184
- class={local.class}
209
+ label={triggerName()}
210
+ class={cn('relative', local.class)}
211
+ data-slot="account-menu-trigger"
185
212
  onFocus={() => local.onIntent?.()}
186
213
  onPointerEnter={() => local.onIntent?.()}
187
214
  {...rest}
188
215
  >
189
216
  <UserRound aria-hidden="true" />
217
+ {updateDot()}
190
218
  </DropdownMenuTrigger>
191
219
  </Show>
192
220
  <DropdownMenuContent
@@ -211,6 +239,11 @@ export function AccountMenu(props: AccountMenuProps) {
211
239
  {(item) => (
212
240
  <DropdownMenuItem
213
241
  disabled={item.disabled}
242
+ aria-label={
243
+ local.updateAvailable && item.id === 'updates'
244
+ ? `${item.label}, update available`
245
+ : undefined
246
+ }
214
247
  onSelect={() => {
215
248
  pendingAfterClose = item.onSelectAfterClose
216
249
  ? { callback: item.onSelectAfterClose }
@@ -222,6 +255,13 @@ export function AccountMenu(props: AccountMenuProps) {
222
255
  >
223
256
  {item.icon ?? renderFallbackIcon(item.id)}
224
257
  <span>{item.label}</span>
258
+ <Show when={local.updateAvailable && item.id === 'updates'}>
259
+ <span
260
+ data-slot="account-menu-update-dot"
261
+ aria-hidden="true"
262
+ class="ms-auto size-2 shrink-0 rounded-full bg-primary"
263
+ />
264
+ </Show>
225
265
  </DropdownMenuItem>
226
266
  )}
227
267
  </For>
@@ -0,0 +1,67 @@
1
+ import type { ComponentProps } from 'solid-js'
2
+ import { splitProps } from 'solid-js'
3
+ import { cn } from '../../lib/utils'
4
+ import { overlaySurface } from '../../lib/overlay'
5
+ import { Button } from '../ui/button/button'
6
+
7
+ export type ComposerMenuProps = ComponentProps<'div'> & {
8
+ /** A name for what the menu offers, e.g. "Mention an Agent". */
9
+ label?: string
10
+ }
11
+
12
+ /**
13
+ * ComposerMenu.
14
+ *
15
+ * The panel a composer raises above the field: mention suggestions while the
16
+ * reader types a name, the picker of what can be attached, follow-up prompts.
17
+ * It is deliberately *not* a `Menu`: there is no trigger and no focus trap —
18
+ * the reader keeps typing while it is open, and it opens and closes on the
19
+ * draft's own grammar rather than on a button press.
20
+ *
21
+ * Rows are `ComposerMenuItem`s; anything else a caller renders (a checkbox
22
+ * list, a heading) sits inside the same panel. Placement stays with the host —
23
+ * a suggestion menu renders in the composer's `menu` slot, a picker anchors to
24
+ * its own control.
25
+ */
26
+ export function ComposerMenu(props: ComposerMenuProps) {
27
+ const [local, rest] = splitProps(props, ['class', 'label', 'children'])
28
+
29
+ return (
30
+ <div
31
+ role="group"
32
+ aria-label={local.label}
33
+ data-slot="composer-menu"
34
+ class={cn(
35
+ overlaySurface,
36
+ 'flex max-h-56 min-w-60 flex-col gap-0.5 overflow-y-auto p-1',
37
+ local.class
38
+ )}
39
+ {...rest}
40
+ >
41
+ {local.children}
42
+ </div>
43
+ )
44
+ }
45
+
46
+ export type ComposerMenuItemProps = ComponentProps<typeof Button>
47
+
48
+ /**
49
+ * ComposerMenuItem.
50
+ *
51
+ * One row of a `ComposerMenu`. A ghost button stretched across the panel —
52
+ * the same row every menu in the library draws, so a suggestion never reads as
53
+ * a stray button floating over the draft.
54
+ */
55
+ export function ComposerMenuItem(props: ComposerMenuItemProps) {
56
+ const [local, rest] = splitProps(props, ['class', 'children'])
57
+
58
+ return (
59
+ <Button
60
+ variant="ghost"
61
+ class={cn('w-full justify-start gap-2 text-start font-normal', local.class)}
62
+ {...rest}
63
+ >
64
+ {local.children}
65
+ </Button>
66
+ )
67
+ }
@@ -0,0 +1,111 @@
1
+ import type { ComponentProps, JSX } from 'solid-js'
2
+ import { Show, splitProps } from 'solid-js'
3
+ import { cn } from '../../lib/utils'
4
+
5
+ export type ConversationPaneProps = ComponentProps<'section'> & {
6
+ /** Above the transcript: the conversation's identity and its actions. */
7
+ header?: JSX.Element
8
+ /** The transcript — typically a `ConversationSurface` with `gutter`. */
9
+ children: JSX.Element
10
+ /** Below the transcript: the composer. */
11
+ composer?: JSX.Element
12
+ /**
13
+ * The thread side panel. Its presence opens the second column; at narrow
14
+ * widths the panel overlays the conversation instead of squeezing it, and
15
+ * the conversation chrome steps aside — out of the accessibility tree with
16
+ * it, which is why the overlay hides with `visibility` rather than paint.
17
+ */
18
+ thread?: JSX.Element
19
+ /**
20
+ * Apply the shared page gutters to the composer, tracking the transcript's
21
+ * own gutter (`ConversationSurface`'s `gutter`) so the two read as one pane.
22
+ */
23
+ gutter?: boolean
24
+ }
25
+
26
+ /**
27
+ * ConversationPane.
28
+ *
29
+ * The frame a conversation lives in: a header above, the transcript, a composer
30
+ * below, and — while a thread is open — the thread panel beside all of it.
31
+ *
32
+ * The layout earns its place in the library because the thread is where
33
+ * applications get it wrong. Two impulses both fail: shrinking the transcript
34
+ * to make room turns a busy thread into a one-word-per-line stripe, and
35
+ * *hiding* the transcript with `display: none` unmounts it, losing scroll
36
+ * position and DOM. So the desktop layout gives the thread a fixed column and
37
+ * leaves the transcript fluid; below the collapse width the panel overlays the
38
+ * conversation in full and the conversation steps aside invisibly — still
39
+ * mounted, still in the DOM, gone from the accessibility tree.
40
+ */
41
+ export function ConversationPane(props: ConversationPaneProps) {
42
+ const [local, rest] = splitProps(props, [
43
+ 'class',
44
+ 'header',
45
+ 'composer',
46
+ 'thread',
47
+ 'gutter',
48
+ 'children',
49
+ ])
50
+ const threadOpen = () => local.thread !== undefined
51
+
52
+ return (
53
+ <section
54
+ data-slot="conversation-pane"
55
+ data-conversation-thread-open={threadOpen() || undefined}
56
+ class={cn(
57
+ 'relative grid h-full min-h-0 grid-rows-[auto_minmax(0,1fr)_auto]',
58
+ threadOpen()
59
+ ? 'grid-cols-1 md:grid-cols-[minmax(20rem,1fr)_minmax(18rem,22rem)] lg:grid-cols-[minmax(24rem,1fr)_minmax(20rem,26rem)]'
60
+ : 'grid-cols-1',
61
+ local.class
62
+ )}
63
+ {...rest}
64
+ >
65
+ <Show when={local.header}>
66
+ {(header) => (
67
+ <div class={cn('col-start-1 row-start-1 min-w-0', threadOpen() && 'max-md:invisible')}>
68
+ {header()}
69
+ </div>
70
+ )}
71
+ </Show>
72
+
73
+ <div
74
+ class={cn(
75
+ 'col-start-1 row-start-2 flex min-h-0 min-w-0 flex-col',
76
+ threadOpen() && 'max-md:invisible'
77
+ )}
78
+ >
79
+ {local.children}
80
+ </div>
81
+
82
+ <Show when={local.composer}>
83
+ {(composer) => (
84
+ <div
85
+ class={cn(
86
+ 'col-start-1 row-start-3 min-w-0',
87
+ local.gutter && 'mx-[clamp(0.8rem,3vw,2.5rem)] mb-[1.15rem] max-[30rem]:mx-2',
88
+ threadOpen() && 'max-md:invisible'
89
+ )}
90
+ >
91
+ {composer()}
92
+ </div>
93
+ )}
94
+ </Show>
95
+
96
+ <Show when={local.thread}>
97
+ {(thread) => (
98
+ <div
99
+ class={cn(
100
+ 'min-h-0 min-w-0',
101
+ threadOpen() &&
102
+ 'max-md:absolute max-md:inset-0 max-md:z-(--z-docked) max-md:*:border-s-0 md:col-start-2 md:row-span-3 md:row-start-1'
103
+ )}
104
+ >
105
+ {thread()}
106
+ </div>
107
+ )}
108
+ </Show>
109
+ </section>
110
+ )
111
+ }
@@ -38,6 +38,12 @@ export type ConversationSurfaceProps = ComponentProps<'div'> & {
38
38
  empty?: JSX.Element
39
39
  /** A header that scrolls with the transcript, e.g. a day divider or a banner. */
40
40
  header?: JSX.Element
41
+ /**
42
+ * The shared page gutter: reading padding inside the scroller plus a stable
43
+ * gutter, so the scrollbar never shifts the text when it appears. The page
44
+ * frame around the surface is `ConversationPane`'s job.
45
+ */
46
+ gutter?: boolean
41
47
  }
42
48
 
43
49
  export function ConversationSurface(props: ConversationSurfaceProps) {
@@ -50,6 +56,7 @@ export function ConversationSurface(props: ConversationSurfaceProps) {
50
56
  'onReadingPositionChange',
51
57
  'empty',
52
58
  'header',
59
+ 'gutter',
53
60
  'children',
54
61
  'onScroll',
55
62
  'ref',
@@ -84,7 +91,11 @@ export function ConversationSurface(props: ConversationSurfaceProps) {
84
91
  }}
85
92
  onScroll={onScroll}
86
93
  tabindex={local.tabindex ?? 0}
87
- class="min-h-0 flex-1 overflow-y-auto overscroll-contain"
94
+ class={cn(
95
+ 'min-h-0 flex-1 overflow-y-auto overscroll-contain',
96
+ local.gutter &&
97
+ 'px-[clamp(0.8rem,3vw,2.5rem)] pt-[1.35rem] pb-[1.1rem] [scrollbar-gutter:stable] max-[30rem]:px-2'
98
+ )}
88
99
  {...rest}
89
100
  >
90
101
  <div ref={follow.bindContent} data-slot="conversation-content">
@@ -10,6 +10,13 @@ export {
10
10
  type ConversationSurfaceProps,
11
11
  type ConversationReadingPosition,
12
12
  } from './conversation-surface'
13
+ export { ConversationPane, type ConversationPaneProps } from './conversation-pane'
14
+ export {
15
+ ComposerMenu,
16
+ ComposerMenuItem,
17
+ type ComposerMenuProps,
18
+ type ComposerMenuItemProps,
19
+ } from './composer-menu'
13
20
  export {
14
21
  ComposerAttachmentButton,
15
22
  ComposerHint,
@@ -71,6 +71,12 @@ export type MessageComposerProps = Omit<ComponentProps<'form'>, 'onSubmit'> & {
71
71
  trailing?: JSX.Element
72
72
  /** A menu rendered above the field, e.g. mention suggestions. */
73
73
  menu?: JSX.Element
74
+ /**
75
+ * Host progress rendered in the composer's own live region, e.g. dictation
76
+ * state — plain paragraphs are progress, a `role="alert"` paragraph is a
77
+ * failure. Sending and send-failure copy stays the composer's own.
78
+ */
79
+ status?: JSX.Element
74
80
  /** The reply target, drawn as a strip above the field. */
75
81
  replyTo?: { label: string; onDismiss: () => void }
76
82
  /** Ref to the actual message textarea, for host focus and selection behavior. */
@@ -102,6 +108,7 @@ export function MessageComposer(props: MessageComposerProps) {
102
108
  'leading',
103
109
  'trailing',
104
110
  'menu',
111
+ 'status',
105
112
  'replyTo',
106
113
  'inputRef',
107
114
  'inputId',
@@ -281,6 +288,7 @@ export function MessageComposer(props: MessageComposerProps) {
281
288
  >
282
289
  {pendingLabel()}…
283
290
  </Show>
291
+ <Show when={local.status}>{local.status}</Show>
284
292
  </div>
285
293
  </form>
286
294
  )
@@ -286,3 +286,38 @@ export function CommandShortcut(props: {
286
286
  </span>
287
287
  )
288
288
  }
289
+
290
+ /**
291
+ * The status line a command surface reports under its list: searching, offline,
292
+ * a partial result caveat. The copy and the live-role semantics (`status`,
293
+ * `alert`) are the caller's — a palette's failure modes are app facts — but the
294
+ * line's shape is not something every caller should redraw.
295
+ */
296
+ export function CommandStatus(props: ComponentProps<'p'>) {
297
+ const [local, rest] = splitProps(props, ['class'])
298
+
299
+ return (
300
+ <p
301
+ data-slot="command-status"
302
+ class={cn('text-muted-foreground px-4 py-4 text-center text-sm', local.class)}
303
+ {...rest}
304
+ />
305
+ )
306
+ }
307
+
308
+ /**
309
+ * The keyboard hint a palette footer carries, right-aligned under the list so
310
+ * it reads as furniture rather than a result. Muted on purpose: the arrows are
311
+ * what the user has already been doing by the time they read it.
312
+ */
313
+ export function CommandHint(props: ComponentProps<'p'>) {
314
+ const [local, rest] = splitProps(props, ['class'])
315
+
316
+ return (
317
+ <p
318
+ data-slot="command-hint"
319
+ class={cn('text-muted-foreground px-4 pb-1.5 text-right text-xs', local.class)}
320
+ {...rest}
321
+ />
322
+ )
323
+ }
@@ -3,10 +3,12 @@ export {
3
3
  CommandDialog,
4
4
  CommandEmpty,
5
5
  CommandGroup,
6
+ CommandHint,
6
7
  CommandInput,
7
8
  CommandItem,
8
9
  CommandList,
9
10
  CommandLoading,
10
11
  CommandSeparator,
11
12
  CommandShortcut,
13
+ CommandStatus,
12
14
  } from './command'
@@ -8,3 +8,8 @@ export {
8
8
  type KbdChordProps,
9
9
  type KbdSize,
10
10
  } from './kbd'
11
+ export {
12
+ platformModifierKey,
13
+ searchShortcutKeyshortcuts,
14
+ searchShortcutLabel,
15
+ } from './platform-modifier'
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Which modifier a chord should advertise.
3
+ *
4
+ * Every global chord in the product binds `(metaKey || ctrlKey)`, so the
5
+ * working key is the platform's command modifier wherever the app runs — but
6
+ * Apple platforms draw it as ⌘ and everyone else spells it Ctrl. A chord
7
+ * label drawn from anything else (a hardcoded ⌘, say) advertises a key that
8
+ * does not work as drawn on Windows and Linux.
9
+ *
10
+ * These helpers are the one place that decides the glyph, so every surface
11
+ * that draws or announces a shortcut — rail rows, menus, a pane's search
12
+ * field — draws the same one. They sit next to the `Kbd` caps because the
13
+ * glyph is a property of the drawing, not of the feature that shows it: a
14
+ * spelled-out `Ctrl` needs one cap per word (a `KbdGroup`), while `⌘K` can
15
+ * ride `KbdChord`'s one-cap-per-character split.
16
+ */
17
+
18
+ /** The modifier glyph the running OS renders for Meta: ⌘ on Apple platforms, Ctrl elsewhere. */
19
+ export function platformModifierKey(platform?: string): '⌘' | 'Ctrl' {
20
+ // The guard keeps server renders honest: no navigator means no Apple
21
+ // platform string, so they fall back to the spelled-out modifier.
22
+ const browserPlatform = platform ?? (typeof navigator === 'undefined' ? '' : navigator.platform)
23
+ return /Mac|iPhone|iPad|iPod/i.test(browserPlatform) ? '⌘' : 'Ctrl'
24
+ }
25
+
26
+ /** The workspace search chord label as the rail's hover text draws it: the
27
+ * platform modifier plus K (`⌘K`, or `Ctrl+K` where ⌘ does not exist). */
28
+ export function searchShortcutLabel(platform?: string): string {
29
+ return platformModifierKey(platform) === '⌘' ? '⌘K' : 'Ctrl+K'
30
+ }
31
+
32
+ /**
33
+ * The `aria-keyshortcuts` for the search chord. The binding accepts Meta and
34
+ * Ctrl alike, so the announced shortcuts name both: a hardcoded `Meta+K`
35
+ * would promise a key the row does not keep on non-Apple platforms.
36
+ */
37
+ export const searchShortcutKeyshortcuts = 'Meta+K Control+K'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.120.0",
3
+ "version": "0.122.0",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",
package/registry.json CHANGED
@@ -885,6 +885,11 @@
885
885
  "path": "src/components/ui/kbd/kbd.tsx",
886
886
  "type": "registry:ui",
887
887
  "target": "components/ui/kbd/kbd.tsx"
888
+ },
889
+ {
890
+ "path": "src/components/ui/kbd/platform-modifier.ts",
891
+ "type": "registry:ui",
892
+ "target": "components/ui/kbd/platform-modifier.ts"
888
893
  }
889
894
  ]
890
895
  },
@@ -2374,11 +2379,21 @@
2374
2379
  "type": "registry:ui",
2375
2380
  "target": "components/conversation/chat-composer.tsx"
2376
2381
  },
2382
+ {
2383
+ "path": "src/components/conversation/composer-menu.tsx",
2384
+ "type": "registry:ui",
2385
+ "target": "components/conversation/composer-menu.tsx"
2386
+ },
2377
2387
  {
2378
2388
  "path": "src/components/conversation/conversation-avatar.tsx",
2379
2389
  "type": "registry:ui",
2380
2390
  "target": "components/conversation/conversation-avatar.tsx"
2381
2391
  },
2392
+ {
2393
+ "path": "src/components/conversation/conversation-pane.tsx",
2394
+ "type": "registry:ui",
2395
+ "target": "components/conversation/conversation-pane.tsx"
2396
+ },
2382
2397
  {
2383
2398
  "path": "src/components/conversation/conversation-surface.tsx",
2384
2399
  "type": "registry:ui",
@@ -84,6 +84,14 @@ export type AccountMenuProps = {
84
84
  platform?: 'desktop' | 'web'
85
85
  /** Whether a session exists. Decides the sign-in/sign-out row. */
86
86
  authenticated: boolean
87
+ /**
88
+ * Draw the pending-update indicator: an accent dot on the trigger and on the
89
+ * `updates` row — the id `createAppMenuItems` mints, the same convention the
90
+ * fallback icons follow — and extend both accessible names with "update
91
+ * available", because the dot itself carries no text. The updater remains the
92
+ * caller's: the prop only mirrors its state into the menu.
93
+ */
94
+ updateAvailable?: boolean
87
95
  /** Local-only applications can omit the session actions altogether. */
88
96
  showSession?: boolean
89
97
  /** Disable the session row while a sign-in or sign-out is in flight. */
@@ -116,6 +124,7 @@ export function AccountMenu(props: AccountMenuProps) {
116
124
  'items',
117
125
  'platform',
118
126
  'authenticated',
127
+ 'updateAvailable',
119
128
  'showSession',
120
129
  'busy',
121
130
  'label',
@@ -142,6 +151,21 @@ export function AccountMenu(props: AccountMenuProps) {
142
151
  const visible = () =>
143
152
  local.items.filter((item) => !item.platform || item.platform === (local.platform ?? 'desktop'))
144
153
 
154
+ // The pending update reaches the user twice on each surface: as a dot and as
155
+ // words. The dot is aria-hidden, so the accessible names carry the state —
156
+ // which is why the names are extended here rather than left to the caller.
157
+ const triggerName = () =>
158
+ `${local.label ?? 'Account and settings'}${local.updateAvailable ? ', update available' : ''}`
159
+ const updateDot = () => (
160
+ <Show when={local.updateAvailable}>
161
+ <span
162
+ data-slot="account-menu-update-dot"
163
+ aria-hidden="true"
164
+ class="absolute top-1.5 right-2.5 size-2 rounded-full bg-primary"
165
+ />
166
+ </Show>
167
+ )
168
+
145
169
  // Built by reference at each return, not once up front: Solid fixes a node's
146
170
  // owner chain when the JSX expression runs, and an eagerly created tree would
147
171
  // never see the Tooltip provider below — Kobalte's TooltipTrigger then throws
@@ -164,13 +188,15 @@ export function AccountMenu(props: AccountMenuProps) {
164
188
  ref={(element: HTMLButtonElement) => (trigger = element)}
165
189
  variant="ghost"
166
190
  size={local.size ?? 'icon-md'}
167
- class={cn('text-muted-foreground', local.class)}
168
- aria-label={local.label ?? 'Account and settings'}
191
+ class={cn('relative text-muted-foreground', local.class)}
192
+ data-slot="account-menu-trigger"
193
+ aria-label={triggerName()}
169
194
  onFocus={() => local.onIntent?.()}
170
195
  onPointerEnter={() => local.onIntent?.()}
171
196
  {...rest}
172
197
  >
173
198
  <UserRound aria-hidden="true" />
199
+ {updateDot()}
174
200
  </TooltipTrigger>
175
201
  }
176
202
  >
@@ -180,13 +206,15 @@ export function AccountMenu(props: AccountMenuProps) {
180
206
  <DropdownMenuTrigger
181
207
  as={SideRailButton}
182
208
  ref={(element: HTMLButtonElement) => (trigger = element)}
183
- label={local.label ?? 'Account and settings'}
184
- class={local.class}
209
+ label={triggerName()}
210
+ class={cn('relative', local.class)}
211
+ data-slot="account-menu-trigger"
185
212
  onFocus={() => local.onIntent?.()}
186
213
  onPointerEnter={() => local.onIntent?.()}
187
214
  {...rest}
188
215
  >
189
216
  <UserRound aria-hidden="true" />
217
+ {updateDot()}
190
218
  </DropdownMenuTrigger>
191
219
  </Show>
192
220
  <DropdownMenuContent
@@ -211,6 +239,11 @@ export function AccountMenu(props: AccountMenuProps) {
211
239
  {(item) => (
212
240
  <DropdownMenuItem
213
241
  disabled={item.disabled}
242
+ aria-label={
243
+ local.updateAvailable && item.id === 'updates'
244
+ ? `${item.label}, update available`
245
+ : undefined
246
+ }
214
247
  onSelect={() => {
215
248
  pendingAfterClose = item.onSelectAfterClose
216
249
  ? { callback: item.onSelectAfterClose }
@@ -222,6 +255,13 @@ export function AccountMenu(props: AccountMenuProps) {
222
255
  >
223
256
  {item.icon ?? renderFallbackIcon(item.id)}
224
257
  <span>{item.label}</span>
258
+ <Show when={local.updateAvailable && item.id === 'updates'}>
259
+ <span
260
+ data-slot="account-menu-update-dot"
261
+ aria-hidden="true"
262
+ class="ms-auto size-2 shrink-0 rounded-full bg-primary"
263
+ />
264
+ </Show>
225
265
  </DropdownMenuItem>
226
266
  )}
227
267
  </For>