@adea-ai/ui 0.89.1 → 0.93.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +22 -5
  2. package/dist/NOTICE +19 -0
  3. package/dist/components/composites/list-row/index.d.ts +1 -0
  4. package/dist/components/composites/list-row/index.d.ts.map +1 -1
  5. package/dist/components/composites/list-row/index.js +2 -1
  6. package/dist/components/composites/list-row/list-row-control.d.ts +20 -0
  7. package/dist/components/composites/list-row/list-row-control.d.ts.map +1 -0
  8. package/dist/components/composites/list-row/list-row-control.js +96 -0
  9. package/dist/components/composites/list-row/list-row-control.js.map +1 -0
  10. package/dist/components/composites/list-row/list-row.d.ts +15 -35
  11. package/dist/components/composites/list-row/list-row.d.ts.map +1 -1
  12. package/dist/components/composites/list-row/list-row.js +88 -135
  13. package/dist/components/composites/list-row/list-row.js.map +1 -1
  14. package/dist/components/composites/tree/index.d.ts +4 -0
  15. package/dist/components/composites/tree/index.d.ts.map +1 -0
  16. package/dist/components/composites/tree/index.js +2 -0
  17. package/dist/components/composites/tree/tree-navigation.d.ts +46 -0
  18. package/dist/components/composites/tree/tree-navigation.d.ts.map +1 -0
  19. package/dist/components/composites/tree/tree-navigation.js +95 -0
  20. package/dist/components/composites/tree/tree-navigation.js.map +1 -0
  21. package/dist/components/composites/tree/tree.d.ts +64 -0
  22. package/dist/components/composites/tree/tree.d.ts.map +1 -0
  23. package/dist/components/composites/tree/tree.js +311 -0
  24. package/dist/components/composites/tree/tree.js.map +1 -0
  25. package/dist/components/conversation/message-composer.d.ts +18 -3
  26. package/dist/components/conversation/message-composer.d.ts.map +1 -1
  27. package/dist/components/conversation/message-composer.js +93 -27
  28. package/dist/components/conversation/message-composer.js.map +1 -1
  29. package/dist/components/layout/sidebar-nav/index.d.ts +1 -1
  30. package/dist/components/layout/sidebar-nav/index.d.ts.map +1 -1
  31. package/dist/components/layout/sidebar-nav/index.js +2 -2
  32. package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts +19 -1
  33. package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts.map +1 -1
  34. package/dist/components/layout/sidebar-nav/sidebar-nav.js +83 -17
  35. package/dist/components/layout/sidebar-nav/sidebar-nav.js.map +1 -1
  36. package/dist/components/layout/top-bar/top-bar.d.ts +6 -5
  37. package/dist/components/layout/top-bar/top-bar.d.ts.map +1 -1
  38. package/dist/components/layout/top-bar/top-bar.js +30 -7
  39. package/dist/components/layout/top-bar/top-bar.js.map +1 -1
  40. package/dist/components/ui/dialog/dialog.d.ts +2 -0
  41. package/dist/components/ui/dialog/dialog.d.ts.map +1 -1
  42. package/dist/components/ui/dialog/dialog.js +4 -3
  43. package/dist/components/ui/dialog/dialog.js.map +1 -1
  44. package/dist/components/ui/modal-dialog/modal-dialog.d.ts +5 -2
  45. package/dist/components/ui/modal-dialog/modal-dialog.d.ts.map +1 -1
  46. package/dist/components/ui/modal-dialog/modal-dialog.js +33 -24
  47. package/dist/components/ui/modal-dialog/modal-dialog.js.map +1 -1
  48. package/dist/components/ui/textarea/index.d.ts +1 -1
  49. package/dist/components/ui/textarea/index.d.ts.map +1 -1
  50. package/dist/components/ui/textarea/index.js +2 -2
  51. package/dist/components/ui/textarea/textarea.d.ts +15 -2
  52. package/dist/components/ui/textarea/textarea.d.ts.map +1 -1
  53. package/dist/components/ui/textarea/textarea.js +47 -3
  54. package/dist/components/ui/textarea/textarea.js.map +1 -1
  55. package/dist/components/ui/tooltip/tooltip.d.ts.map +1 -1
  56. package/dist/components/ui/tooltip/tooltip.js +239 -17
  57. package/dist/components/ui/tooltip/tooltip.js.map +1 -1
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +5 -3
  61. package/dist/lib/variants.js +2 -2
  62. package/dist/r/conversation.json +1 -0
  63. package/dist/r/list-row.json +5 -0
  64. package/dist/r/registry.json +41 -2
  65. package/dist/r/src/components/composites/list-row/index.ts +1 -0
  66. package/dist/r/src/components/composites/list-row/list-row-control.tsx +108 -0
  67. package/dist/r/src/components/composites/list-row/list-row.tsx +60 -127
  68. package/dist/r/src/components/composites/tree/index.ts +3 -0
  69. package/dist/r/src/components/composites/tree/tree-navigation.ts +114 -0
  70. package/dist/r/src/components/composites/tree/tree.tsx +405 -0
  71. package/dist/r/src/components/conversation/message-composer.tsx +103 -22
  72. package/dist/r/src/components/layout/sidebar-nav/index.ts +3 -0
  73. package/dist/r/src/components/layout/sidebar-nav/sidebar-nav.tsx +75 -6
  74. package/dist/r/src/components/layout/top-bar/top-bar.tsx +34 -11
  75. package/dist/r/src/components/ui/dialog/dialog.tsx +4 -2
  76. package/dist/r/src/components/ui/modal-dialog/modal-dialog.tsx +20 -4
  77. package/dist/r/src/components/ui/textarea/index.ts +1 -1
  78. package/dist/r/src/components/ui/textarea/textarea.tsx +52 -10
  79. package/dist/r/src/components/ui/tooltip/tooltip.tsx +305 -10
  80. package/dist/r/src/styles/base.css +40 -0
  81. package/dist/r/textarea.json +3 -1
  82. package/dist/r/top-bar.json +3 -1
  83. package/dist/r/tree.json +29 -0
  84. package/package.json +4 -1
  85. package/registry.json +41 -2
  86. package/src/components/composites/list-row/index.ts +1 -0
  87. package/src/components/composites/list-row/list-row-control.tsx +108 -0
  88. package/src/components/composites/list-row/list-row.tsx +60 -127
  89. package/src/components/composites/tree/index.ts +3 -0
  90. package/src/components/composites/tree/tree-navigation.ts +114 -0
  91. package/src/components/composites/tree/tree.tsx +405 -0
  92. package/src/components/conversation/message-composer.tsx +103 -22
  93. package/src/components/layout/sidebar-nav/index.ts +3 -0
  94. package/src/components/layout/sidebar-nav/sidebar-nav.tsx +75 -6
  95. package/src/components/layout/top-bar/top-bar.tsx +34 -11
  96. package/src/components/ui/dialog/dialog.tsx +4 -2
  97. package/src/components/ui/modal-dialog/modal-dialog.tsx +20 -4
  98. package/src/components/ui/textarea/index.ts +1 -1
  99. package/src/components/ui/textarea/textarea.tsx +52 -10
  100. package/src/components/ui/tooltip/tooltip.tsx +305 -10
  101. package/src/index.ts +1 -0
  102. package/src/styles/base.css +40 -0
@@ -44,7 +44,7 @@ export function SidebarNav<T extends ValidComponent = 'nav'>(props: SidebarNavPr
44
44
  as="nav"
45
45
  aria-label={local['aria-label'] ?? 'Section'}
46
46
  class={cn(
47
- 'bg-sidebar text-sidebar-foreground flex h-full w-sidebar shrink-0 flex-col border-e border-sidebar-border',
47
+ 'bg-sidebar text-sidebar-foreground flex h-full w-sidebar min-w-0 max-w-full shrink-0 flex-col border-e border-sidebar-border',
48
48
  local.class
49
49
  )}
50
50
  {...(rest as ComponentProps<'nav'>)}
@@ -128,6 +128,8 @@ export type SidebarNavSectionProps = ComponentProps<'div'> & {
128
128
  /** Render a disclosure control instead of a plain heading. */
129
129
  collapsible?: boolean
130
130
  defaultOpen?: boolean
131
+ /** Mark the current project/group without changing its disclosure state. */
132
+ active?: boolean
131
133
  /** Controlled disclosure state. When supplied, the host owns changes. */
132
134
  open?: boolean
133
135
  /** Called for user-initiated disclosure changes. */
@@ -148,6 +150,7 @@ export function SidebarNavSection(props: SidebarNavSectionProps) {
148
150
  'label',
149
151
  'collapsible',
150
152
  'defaultOpen',
153
+ 'active',
151
154
  'open',
152
155
  'onOpenChange',
153
156
  'triggerProps',
@@ -188,17 +191,29 @@ export function SidebarNavSection(props: SidebarNavSectionProps) {
188
191
 
189
192
  return (
190
193
  <div data-slot="sidebar-nav-section" class={cn('flex flex-col gap-0.5', local.class)} {...rest}>
191
- <div class="group/section-header flex items-center gap-1 rounded-md px-2 py-1.5 hover:bg-sidebar-accent/60">
194
+ <div
195
+ data-slot="sidebar-nav-section-header"
196
+ data-active={local.active ? '' : undefined}
197
+ class={cn(
198
+ 'group/section-header flex items-center gap-1 rounded-md px-2 py-1.5',
199
+ { '[@media(pointer:coarse)]:py-0': local.collapsible },
200
+ local.active ? 'bg-primary-subtle text-foreground' : 'hover:bg-sidebar-accent/60'
201
+ )}
202
+ >
192
203
  <Polymorphic
193
204
  as={local.headingAs ?? 'div'}
194
205
  aria-label={local.headingAs ? local.label : undefined}
195
- class="flex min-w-0 flex-1 items-center gap-1.5 text-2xs font-medium tracking-wide text-sidebar-muted-foreground uppercase"
206
+ class={cn(
207
+ 'flex min-w-0 flex-1 items-center gap-1.5 text-2xs font-medium tracking-wide uppercase',
208
+ local.active ? 'text-foreground' : 'text-sidebar-muted-foreground'
209
+ )}
196
210
  >
197
211
  <Show when={local.collapsible} fallback={heading}>
198
212
  <button
199
213
  {...(triggerRest as ComponentProps<'button'>)}
200
214
  type="button"
201
215
  aria-expanded={open()}
216
+ aria-current={local.active ? 'page' : triggerRest['aria-current']}
202
217
  onClick={(event) => {
203
218
  invokeEventHandler(trigger.onClick, event)
204
219
  if (event.defaultPrevented) return
@@ -212,7 +227,10 @@ export function SidebarNavSection(props: SidebarNavSectionProps) {
212
227
  event.stopPropagation()
213
228
  trigger.onReorder?.(event.key === 'ArrowUp' ? 'up' : 'down')
214
229
  }}
215
- class="flex min-w-0 flex-1 items-center gap-1.5 rounded-sm text-2xs font-medium tracking-wide text-sidebar-muted-foreground uppercase outline-none transition-colors ease-out hover:text-sidebar-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle"
230
+ class={cn(
231
+ 'flex min-w-0 flex-1 items-center gap-1.5 rounded-sm text-2xs font-medium tracking-wide uppercase outline-none transition-colors ease-out hover:text-sidebar-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle [@media(pointer:coarse)]:min-h-11',
232
+ local.active ? 'text-foreground' : 'text-sidebar-muted-foreground'
233
+ )}
216
234
  >
217
235
  {heading}
218
236
  </button>
@@ -231,6 +249,53 @@ export function SidebarNavSection(props: SidebarNavSectionProps) {
231
249
  )
232
250
  }
233
251
 
252
+ export type SidebarNavRowProps = ComponentProps<'div'> & {
253
+ /** Adjacent controls, outside the navigation item's interactive element. */
254
+ actions?: JSX.Element
255
+ }
256
+
257
+ /**
258
+ * A navigation item and its adjacent controls. Actions stay outside the item
259
+ * to avoid nested buttons; touch, keyboard focus, and expanded menus expose
260
+ * them without requiring a pointer hover. A narrow lane or enlarged text
261
+ * may move actions onto a second line rather than clipping the label or controls.
262
+ */
263
+ export function SidebarNavRow(props: SidebarNavRowProps) {
264
+ const [local, rest] = splitProps(props, ['class', 'children', 'actions'])
265
+ return (
266
+ <div
267
+ data-slot="sidebar-nav-row"
268
+ class={cn(
269
+ 'group/sidebar-nav-row flex min-w-0 flex-wrap items-center gap-0.5 [&>[data-slot=sidebar-nav-item]]:flex-1 [&>[data-slot=sidebar-nav-item]]:basis-36',
270
+ local.class
271
+ )}
272
+ {...rest}
273
+ >
274
+ {local.children}
275
+ <Show when={local.actions}>
276
+ <span
277
+ data-slot="sidebar-nav-row-actions"
278
+ class="ms-auto flex shrink-0 items-center gap-0.5 opacity-0 transition-opacity ease-out group-hover/sidebar-nav-row:opacity-100 group-focus-within/sidebar-nav-row:opacity-100 has-[[aria-haspopup=menu][aria-expanded=true]]:opacity-100 has-[[aria-haspopup=true][aria-expanded=true]]:opacity-100 [@media(hover:none)]:opacity-100"
279
+ >
280
+ {local.actions}
281
+ </span>
282
+ </Show>
283
+ </div>
284
+ )
285
+ }
286
+
287
+ /** Row text may truncate visually; its complete content remains the accessible name. */
288
+ export function SidebarNavLabel(props: ComponentProps<'span'>) {
289
+ const [local, rest] = splitProps(props, ['class'])
290
+ return (
291
+ <span
292
+ data-slot="sidebar-nav-label"
293
+ class={cn('min-w-0 flex-1 truncate', local.class)}
294
+ {...rest}
295
+ />
296
+ )
297
+ }
298
+
234
299
  export type SidebarNavItemProps<T extends ValidComponent = 'a'> = PolymorphicProps<
235
300
  T,
236
301
  {
@@ -243,10 +308,13 @@ export type SidebarNavItemProps<T extends ValidComponent = 'a'> = PolymorphicPro
243
308
  }
244
309
  >
245
310
 
246
- /** A sidebar row. Exported classes, so a caller's own element can match it. */
311
+ /**
312
+ * A sidebar row. Exported classes, so a caller's own element can match it.
313
+ * Coarse pointers get a 44px target without changing Kiro's desktop density.
314
+ */
247
315
  export const sidebarNavItemClass = [
248
316
  'group/nav-item relative flex min-w-0 items-center gap-2.5 rounded-md px-3 py-2 text-sm font-medium',
249
- 'whitespace-nowrap transition-colors ease-out outline-none select-none',
317
+ 'whitespace-nowrap transition-colors ease-out outline-none select-none [@media(pointer:coarse)]:min-h-11',
250
318
  'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
251
319
  '[&_svg]:size-4 [&_svg]:shrink-0',
252
320
  ].join(' ')
@@ -268,6 +336,7 @@ export function SidebarNavItem<T extends ValidComponent = 'a'>(props: SidebarNav
268
336
  return (
269
337
  <Polymorphic
270
338
  as="a"
339
+ data-slot="sidebar-nav-item"
271
340
  aria-current={local.active ? 'page' : undefined}
272
341
  data-active={local.active ? '' : undefined}
273
342
  class={cn(
@@ -1,15 +1,16 @@
1
1
  import type { ComponentProps } from 'solid-js'
2
2
  import { Show, splitProps } from 'solid-js'
3
+ import { Search } from 'lucide-solid'
3
4
  import { cn } from '../../../lib/utils'
4
5
 
5
6
  /**
6
7
  * TopBar.
7
8
  *
8
9
  * The window's title row and primary toolbar. Three tracks: a leading group, a
9
- * centred search, and a trailing group. The search is a fixed function of the
10
- * window width and the two side groups are equal remainders, which is what
11
- * makes the search *exactly* window-centred by construction — no measurement,
12
- * no observer, no absolutely-positioned overlay to keep clear of.
10
+ * centred search, and a trailing group. On wide screens the search is a fixed
11
+ * function of the window width and the side groups are equal remainders. On
12
+ * narrow screens the middle track yields space to the controls, and each side
13
+ * group scrolls when necessary so keyboard focus can reveal every action.
13
14
  *
14
15
  * On a frameless desktop window this row is also the drag region, so it carries
15
16
  * `window-drag` and the controls inside it opt back out. `macos-inset` and
@@ -41,7 +42,7 @@ export function TopBar(props: TopBarProps) {
41
42
  <header
42
43
  data-slot="top-bar"
43
44
  class={cn(
44
- 'h-topbar relative z-(--z-sticky) grid shrink-0 grid-cols-[auto_minmax(0,1fr)_auto] items-center gap-3 border-b border-border bg-card text-card-foreground px-3 md:grid-cols-[minmax(0,1fr)_clamp(240px,22vw,480px)_minmax(0,1fr)]',
45
+ 'h-topbar relative z-(--z-sticky) grid shrink-0 grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center gap-3 border-b border-border bg-card text-card-foreground px-3 md:grid-cols-[minmax(0,1fr)_clamp(240px,22vw,480px)_minmax(0,1fr)]',
45
46
  local.glass && 'glass-panel',
46
47
  local.draggable && 'window-drag',
47
48
  local.macosInset && 'window-inset-macos',
@@ -53,25 +54,46 @@ export function TopBar(props: TopBarProps) {
53
54
  )
54
55
  }
55
56
 
56
- /** A side group. Both sides are equal remainders, which centres the search. */
57
+ function invokeFocusHandler(handler: unknown, event: Event) {
58
+ if (Array.isArray(handler)) {
59
+ const [callback, data] = handler as [(data: unknown, event: Event) => void, unknown]
60
+ callback(data, event)
61
+ } else if (typeof handler === 'function') {
62
+ const callback = handler as (event: Event) => void
63
+ callback(event)
64
+ }
65
+ }
66
+
67
+ /** A scrollable side group; equal wide-screen tracks centre the search. */
57
68
  export function TopBarSection(props: ComponentProps<'div'> & { align?: 'start' | 'end' }) {
58
- const [local, rest] = splitProps(props, ['class', 'align'])
69
+ const [local, rest] = splitProps(props, ['class', 'align', 'onFocusIn'])
59
70
 
60
71
  return (
61
72
  <div
62
73
  data-slot="top-bar-section"
63
74
  class={cn(
64
75
  'window-no-drag flex min-w-0 items-center gap-2 overflow-x-auto overflow-y-hidden',
65
- local.align === 'end' && 'justify-end',
76
+ local.align === 'end' && 'md:justify-end',
66
77
  local.align !== 'end' && 'justify-start',
67
78
  local.class
68
79
  )}
80
+ onFocusIn={(event) => {
81
+ if (event.target instanceof HTMLElement) {
82
+ event.target.scrollIntoView({ block: 'nearest', inline: 'nearest' })
83
+ }
84
+ invokeFocusHandler(local.onFocusIn, event)
85
+ }}
69
86
  {...rest}
70
87
  />
71
88
  )
72
89
  }
73
90
 
74
- export function TopBarTitle(props: ComponentProps<'div'> & { align?: 'start' | 'center' }) {
91
+ export function TopBarTitle(
92
+ props: ComponentProps<'div'> & {
93
+ /** Aligns text within this slot; it does not position the slot. */
94
+ align?: 'start' | 'center'
95
+ }
96
+ ) {
75
97
  const [local, rest] = splitProps(props, ['class', 'align'])
76
98
  return (
77
99
  <div
@@ -122,7 +144,7 @@ export function TopBarSearch(props: TopBarSearchProps) {
122
144
  type="button"
123
145
  data-slot="top-bar-search"
124
146
  class={cn(
125
- 'text-muted-foreground flex h-control-md w-full items-center gap-2 rounded-md border border-input px-3 text-sm',
147
+ 'text-muted-foreground flex h-control-md w-full min-w-(--control-height-md) items-center justify-center gap-2 rounded-md border border-input px-3 text-sm sm:justify-start',
126
148
  'transition-colors ease-out outline-none',
127
149
  'hover:bg-surface-hover hover:text-foreground',
128
150
  'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
@@ -131,7 +153,8 @@ export function TopBarSearch(props: TopBarSearchProps) {
131
153
  )}
132
154
  {...rest}
133
155
  >
134
- <span class="truncate">{local.placeholder ?? 'Search'}</span>
156
+ <Search aria-hidden="true" class="size-4 shrink-0 sm:hidden" />
157
+ <span class="sr-only sm:not-sr-only sm:truncate">{local.placeholder ?? 'Search'}</span>
135
158
  <Show when={local.shortcut}>
136
159
  <kbd class="bg-muted ms-auto hidden h-5 shrink-0 items-center rounded-sm border border-border px-1.5 font-mono text-2xs sm:flex">
137
160
  {local.shortcut}
@@ -257,15 +257,17 @@ export type DialogContentProps = ComponentProps<typeof KobalteDialogContent> & {
257
257
  * user can escape from needs the button.
258
258
  */
259
259
  closeButton?: JSX.Element | false
260
+ /** Use a 1rem inset as the positioner bounds for tall, viewport-filling surfaces. */
261
+ positioner?: 'default' | 'inset'
260
262
  }
261
263
 
262
264
  export function DialogContent(props: DialogContentProps) {
263
- const [local, rest] = splitProps(props, ['class', 'children', 'closeButton'])
265
+ const [local, rest] = splitProps(props, ['class', 'children', 'closeButton', 'positioner'])
264
266
 
265
267
  return (
266
268
  <KobalteDialog.Portal>
267
269
  <DialogOverlay />
268
- <div class={overlayPositioner}>
270
+ <div class={cn(overlayPositioner, local.positioner === 'inset' && 'inset-4 grid-rows-1 p-0')}>
269
271
  <KobalteDialogContent
270
272
  class={cn(
271
273
  overlaySurface,
@@ -35,12 +35,15 @@ import {
35
35
  * cannot be inferred from the focused element.
36
36
  *
37
37
  * Use `Dialog` directly when you need its composition; use this when you want a
38
- * dialog that is correct by default.
38
+ * dialog that is correct by default. `size="settings"` supplies a wide, bounded
39
+ * shell for a shared settings layout; ordinary dialogs keep their compact default.
39
40
  */
40
41
  export type ModalDialogProps = Omit<
41
42
  ComponentProps<typeof DialogContent>,
42
- 'children' | 'aria-label' | 'aria-labelledby'
43
+ 'children' | 'aria-label' | 'aria-labelledby' | 'positioner'
43
44
  > & {
45
+ /** Selects the bounded, scrollable shell used for grouped workspace settings. */
46
+ size?: 'default' | 'settings'
44
47
  open: boolean
45
48
  onClose: () => void
46
49
  title: string
@@ -70,6 +73,7 @@ export function ModalDialog(props: ModalDialogProps) {
70
73
  'onClose',
71
74
  'title',
72
75
  'modal',
76
+ 'size',
73
77
  'description',
74
78
  'headerLeading',
75
79
  'class',
@@ -114,7 +118,13 @@ export function ModalDialog(props: ModalDialogProps) {
114
118
  <Dialog open modal={local.modal ?? true} onOpenChange={(next) => !next && local.onClose()}>
115
119
  <DialogContent
116
120
  ref={setContent}
117
- class={cn('gap-4', local.class)}
121
+ positioner={local.size === 'settings' ? 'inset' : 'default'}
122
+ class={cn(
123
+ 'gap-4',
124
+ local.size === 'settings' &&
125
+ 'flex h-208 max-h-full min-h-0 max-w-6xl flex-col gap-0 overflow-hidden p-0',
126
+ local.class
127
+ )}
118
128
  aria-label={local['aria-label'] ?? local.title}
119
129
  aria-labelledby={
120
130
  local['aria-labelledby'] ?? (local['aria-label'] !== undefined ? '' : undefined)
@@ -123,7 +133,13 @@ export function ModalDialog(props: ModalDialogProps) {
123
133
  onOpenAutoFocus={focusRestoration.onOpenAutoFocus}
124
134
  onCloseAutoFocus={focusRestoration.onCloseAutoFocus}
125
135
  >
126
- <DialogHeader>
136
+ <DialogHeader
137
+ class={
138
+ local.size === 'settings'
139
+ ? 'shrink-0 gap-1 border-b border-border px-5 pt-4 pb-3 pe-14'
140
+ : undefined
141
+ }
142
+ >
127
143
  <DialogTitle class="flex items-center gap-2">
128
144
  <Show when={local.headerLeading}>{local.headerLeading}</Show>
129
145
  {local.title}
@@ -1 +1 @@
1
- export { Textarea } from './textarea'
1
+ export { Textarea, textareaVariants, type TextareaProps } from './textarea'
@@ -1,8 +1,49 @@
1
- import type { ComponentProps } from 'solid-js'
1
+ import type { ComponentProps, Ref } from 'solid-js'
2
2
  import { splitProps } from 'solid-js'
3
+ import { cva, type VariantProps } from 'class-variance-authority'
3
4
  import { useFormFieldControl } from '../../../lib/form-field'
4
5
  import { cn } from '../../../lib/utils'
5
6
 
7
+ export const textareaVariants = cva(
8
+ [
9
+ 'flex field-sizing-content w-full rounded-md border border-input bg-transparent px-control-md py-2 text-sm',
10
+ 'transition-[color,box-shadow,border-color] ease-out outline-none',
11
+ 'placeholder:text-muted-foreground',
12
+ 'selection:bg-primary selection:text-primary-foreground',
13
+ 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
14
+ 'aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive-subtle',
15
+ 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
16
+ ],
17
+ {
18
+ variants: {
19
+ variant: {
20
+ default: '',
21
+ composer: 'border-0 bg-transparent p-0 shadow-none focus-visible:ring-0',
22
+ },
23
+ size: {
24
+ default: 'min-h-16',
25
+ comfortable: 'min-h-16 max-h-48',
26
+ },
27
+ resize: {
28
+ vertical: 'resize-y',
29
+ none: 'resize-none',
30
+ both: 'resize',
31
+ },
32
+ },
33
+ defaultVariants: {
34
+ variant: 'default',
35
+ size: 'default',
36
+ resize: 'vertical',
37
+ },
38
+ }
39
+ )
40
+
41
+ export type TextareaProps = Omit<ComponentProps<'textarea'>, 'size'> &
42
+ VariantProps<typeof textareaVariants> & {
43
+ /** Solid's actual textarea element, rather than its containing component or form. */
44
+ ref?: Ref<HTMLTextAreaElement>
45
+ }
46
+
6
47
  /**
7
48
  * Textarea.
8
49
  *
@@ -13,10 +54,17 @@ import { cn } from '../../../lib/utils'
13
54
  * `field-sizing: content` lets the element grow with its content where the
14
55
  * engine supports it, which is the behaviour a chat composer wants; `rows`
15
56
  * remains the fallback so an unsupported engine still gets a usable height.
57
+ * `variant="composer"` removes the standalone field surface when nested in a
58
+ * composer shell. `size="comfortable"` caps growth at the shared 12rem rung, and
59
+ * the finite `resize` variants keep browser resize behavior explicit.
16
60
  */
17
- export function Textarea(props: ComponentProps<'textarea'>) {
61
+ export function Textarea(props: TextareaProps) {
18
62
  const [local, rest] = splitProps(props, [
19
63
  'class',
64
+ 'variant',
65
+ 'size',
66
+ 'resize',
67
+ 'ref',
20
68
  'id',
21
69
  'aria-label',
22
70
  'aria-labelledby',
@@ -42,15 +90,9 @@ export function Textarea(props: ComponentProps<'textarea'>) {
42
90
  aria-describedby={field?.['aria-describedby'] ?? local['aria-describedby']}
43
91
  aria-invalid={field?.['aria-invalid'] ?? local['aria-invalid']}
44
92
  aria-errormessage={field?.['aria-errormessage'] ?? local['aria-errormessage']}
93
+ ref={local.ref}
45
94
  class={cn(
46
- 'flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-control-md py-2 text-sm',
47
- 'transition-[color,box-shadow,border-color] ease-out outline-none',
48
- 'placeholder:text-muted-foreground',
49
- 'selection:bg-primary selection:text-primary-foreground',
50
- 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
51
- 'aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive-subtle',
52
- 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
53
- 'resize-y',
95
+ textareaVariants({ variant: local.variant, size: local.size, resize: local.resize }),
54
96
  local.class
55
97
  )}
56
98
  {...rest}