@adea-ai/ui 0.113.1 → 0.115.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 (67) hide show
  1. package/dist/components/composites/list-row/list-row-control.d.ts +6 -0
  2. package/dist/components/composites/list-row/list-row-control.d.ts.map +1 -1
  3. package/dist/components/composites/list-row/list-row-control.js +4 -0
  4. package/dist/components/composites/list-row/list-row-control.js.map +1 -1
  5. package/dist/components/composites/tree/index.d.ts +1 -1
  6. package/dist/components/composites/tree/index.d.ts.map +1 -1
  7. package/dist/components/composites/tree/index.js +2 -2
  8. package/dist/components/composites/tree/tree.d.ts +12 -0
  9. package/dist/components/composites/tree/tree.d.ts.map +1 -1
  10. package/dist/components/composites/tree/tree.js +27 -2
  11. package/dist/components/composites/tree/tree.js.map +1 -1
  12. package/dist/components/layout/contextual-sidebar/contextual-sidebar.d.ts +7 -0
  13. package/dist/components/layout/contextual-sidebar/contextual-sidebar.d.ts.map +1 -1
  14. package/dist/components/layout/contextual-sidebar/contextual-sidebar.js +24 -6
  15. package/dist/components/layout/contextual-sidebar/contextual-sidebar.js.map +1 -1
  16. package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts +6 -0
  17. package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts.map +1 -1
  18. package/dist/components/layout/sidebar-nav/sidebar-nav.js +3 -1
  19. package/dist/components/layout/sidebar-nav/sidebar-nav.js.map +1 -1
  20. package/dist/components/layout/top-bar/top-bar.d.ts.map +1 -1
  21. package/dist/components/layout/top-bar/top-bar.js +2 -2
  22. package/dist/components/layout/top-bar/top-bar.js.map +1 -1
  23. package/dist/components/ui/dialog/dialog.d.ts +10 -1
  24. package/dist/components/ui/dialog/dialog.d.ts.map +1 -1
  25. package/dist/components/ui/dialog/dialog.js +9 -2
  26. package/dist/components/ui/dialog/dialog.js.map +1 -1
  27. package/dist/components/ui/input/input-control.d.ts +7 -1
  28. package/dist/components/ui/input/input-control.d.ts.map +1 -1
  29. package/dist/components/ui/input/input-control.js +10 -2
  30. package/dist/components/ui/input/input-control.js.map +1 -1
  31. package/dist/components/ui/input/input.d.ts +5 -4
  32. package/dist/components/ui/input/input.d.ts.map +1 -1
  33. package/dist/components/ui/input/input.js.map +1 -1
  34. package/dist/components/ui/sheet/sheet.d.ts +28 -4
  35. package/dist/components/ui/sheet/sheet.d.ts.map +1 -1
  36. package/dist/components/ui/sheet/sheet.js +26 -6
  37. package/dist/components/ui/sheet/sheet.js.map +1 -1
  38. package/dist/components/ui/status-chip/status-chip.d.ts +5 -3
  39. package/dist/components/ui/status-chip/status-chip.d.ts.map +1 -1
  40. package/dist/components/ui/status-chip/status-chip.js +5 -1
  41. package/dist/components/ui/status-chip/status-chip.js.map +1 -1
  42. package/dist/index.js +2 -2
  43. package/dist/r/src/components/composites/list-row/list-row-control.tsx +8 -0
  44. package/dist/r/src/components/composites/tree/index.ts +1 -1
  45. package/dist/r/src/components/composites/tree/tree.tsx +31 -1
  46. package/dist/r/src/components/layout/contextual-sidebar/contextual-sidebar.tsx +20 -4
  47. package/dist/r/src/components/layout/sidebar-nav/sidebar-nav.tsx +14 -1
  48. package/dist/r/src/components/layout/top-bar/top-bar.tsx +7 -2
  49. package/dist/r/src/components/ui/dialog/dialog.tsx +10 -3
  50. package/dist/r/src/components/ui/input/input-control.tsx +14 -3
  51. package/dist/r/src/components/ui/input/input.tsx +5 -5
  52. package/dist/r/src/components/ui/sheet/sheet.tsx +28 -6
  53. package/dist/r/src/components/ui/status-chip/status-chip.tsx +13 -2
  54. package/dist/r/src/styles/base.css +7 -6
  55. package/package.json +1 -1
  56. package/src/components/composites/list-row/list-row-control.tsx +8 -0
  57. package/src/components/composites/tree/index.ts +1 -1
  58. package/src/components/composites/tree/tree.tsx +31 -1
  59. package/src/components/layout/contextual-sidebar/contextual-sidebar.tsx +20 -4
  60. package/src/components/layout/sidebar-nav/sidebar-nav.tsx +14 -1
  61. package/src/components/layout/top-bar/top-bar.tsx +7 -2
  62. package/src/components/ui/dialog/dialog.tsx +10 -3
  63. package/src/components/ui/input/input-control.tsx +14 -3
  64. package/src/components/ui/input/input.tsx +5 -5
  65. package/src/components/ui/sheet/sheet.tsx +28 -6
  66. package/src/components/ui/status-chip/status-chip.tsx +13 -2
  67. package/src/styles/base.css +7 -6
@@ -34,8 +34,9 @@ import { createDialogFocusRestoration, DialogOverlay } from '../dialog/dialog'
34
34
  *
35
35
  * Its parts read alike everywhere: `SheetHeader` holds a heading, a description
36
36
  * and a full-width rule beneath, with the close button in the corner;
37
- * `SheetBody` scrolls on its own; `SheetFooter` is a full-width tinted band that
38
- * holds the decision.
37
+ * `SheetBody` scrolls on its own; `SheetFooter` is a full-width band that holds
38
+ * the decision. Pass `band` to the header, the footer, or both to step those
39
+ * parts onto the ladder's subtle-fill rung (see `SheetFooter`).
39
40
  *
40
41
  * Controlled sheets opened from outside a `SheetTrigger` restore focus to the
41
42
  * element focused before opening. Pass `restoreFocusRef` when a stable external
@@ -174,13 +175,20 @@ export function SheetCloseButton(props: ComponentProps<typeof KobalteDialog.Clos
174
175
  return <KobalteDialog.CloseButton class={cn(local.class)} {...rest} />
175
176
  }
176
177
 
177
- export function SheetHeader(props: ComponentProps<'div'>) {
178
- const [local, rest] = splitProps(props, ['class'])
178
+ /**
179
+ * The sheet's title band: a heading, a description and the full-width rule
180
+ * beneath, with the close button in the corner. `band` steps the band onto the
181
+ * subtle-fill rung (see `SheetFooter`) — the two-toned anatomy of a sheet that
182
+ * reads as a panel beside the work rather than one flat canvas.
183
+ */
184
+ export function SheetHeader(props: ComponentProps<'div'> & { band?: boolean }) {
185
+ const [local, rest] = splitProps(props, ['class', 'band'])
179
186
  return (
180
187
  <div
181
188
  data-slot="sheet-header"
182
189
  class={cn(
183
190
  'flex shrink-0 flex-col gap-1.5 border-b border-border p-4 pe-12 text-start',
191
+ { 'bg-muted': local.band },
184
192
  local.class
185
193
  )}
186
194
  {...rest}
@@ -199,13 +207,27 @@ export function SheetBody(props: ComponentProps<'div'>) {
199
207
  )
200
208
  }
201
209
 
202
- export function SheetFooter(props: ComponentProps<'div'>) {
203
- const [local, rest] = splitProps(props, ['class'])
210
+ /**
211
+ * The sheet's decision band: a full-width, hairline-ruled strip that holds the
212
+ * actions. `band` paints it with `--muted` — the elevation ladder's
213
+ * subtle-fill rung, one step up from the panel's `--background` — so the sheet
214
+ * reads two-toned: title band and decision band on the raised rung, scrolling
215
+ * body on the panel surface. The rung already exists for cards, sidebars and
216
+ * input fills; this is the panel-shaped overlay family claiming it. Opt-in
217
+ * because the flat panel is the documented default: the centred modal
218
+ * separates with veil, hairline and shadow instead (the elevated overlay rung
219
+ * amplifies the canvas chroma, which reads as a cast across a large panel),
220
+ * and a host opts a whole sheet family in by passing the prop where it composes
221
+ * the parts — not by restyling them from the outside.
222
+ */
223
+ export function SheetFooter(props: ComponentProps<'div'> & { band?: boolean }) {
224
+ const [local, rest] = splitProps(props, ['class', 'band'])
204
225
  return (
205
226
  <div
206
227
  data-slot="sheet-footer"
207
228
  class={cn(
208
229
  'flex shrink-0 flex-wrap items-center justify-end gap-2 border-t border-border bg-background px-4 py-3',
230
+ { 'bg-muted': local.band },
209
231
  local.class
210
232
  )}
211
233
  {...rest}
@@ -13,7 +13,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'
13
13
  * with no text (unreadable to anyone who cannot see it) and a bare word with no
14
14
  * explanation ("Needs configuration" — of what?).
15
15
  *
16
- * The tones are deliberately not a rainbow. Six cover what a status can actually
16
+ * The tones are deliberately not a rainbow. Seven cover what a status can actually
17
17
  * mean:
18
18
  *
19
19
  * - `neutral` — fine, and there is nothing to say about it.
@@ -21,6 +21,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'
21
21
  * - `warning` — degraded, or needs a decision from the user.
22
22
  * - `danger` — blocked, failed, unrecoverable without intervention.
23
23
  * - `info` — in flight or scheduled; work is happening, nothing is wrong.
24
+ * - `pending` — running now with no result yet (a check, a build). The ring
25
+ * turns, so it reads as live rather than as a settled state.
24
26
  * - `unknown` — the honest absence of information.
25
27
  *
26
28
  * `unknown` is not the same as `neutral`, and collapsing them is the mistake
@@ -39,6 +41,7 @@ export const statusDotVariants = cva('size-2 shrink-0 rounded-full', {
39
41
  warning: 'bg-warning',
40
42
  danger: 'bg-destructive',
41
43
  info: 'bg-info',
44
+ pending: 'border-2 border-info border-r-transparent motion-safe:animate-spin',
42
45
  unknown: 'border border-muted-foreground bg-transparent',
43
46
  },
44
47
  },
@@ -51,10 +54,18 @@ const labelTone: Record<StatusTone, string> = {
51
54
  warning: 'text-foreground',
52
55
  danger: 'text-foreground',
53
56
  info: 'text-foreground',
57
+ pending: 'text-foreground',
54
58
  unknown: 'text-muted-foreground',
55
59
  }
56
60
 
57
- export type StatusTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info' | 'unknown'
61
+ export type StatusTone =
62
+ | 'neutral'
63
+ | 'success'
64
+ | 'warning'
65
+ | 'danger'
66
+ | 'info'
67
+ | 'pending'
68
+ | 'unknown'
58
69
 
59
70
  export type StatusChipProps = Omit<ComponentProps<'span'>, 'children'> & {
60
71
  tone?: StatusTone
@@ -96,7 +96,8 @@
96
96
  }
97
97
 
98
98
  /* Emitted only when ListRowControl is used. A described row changes its own
99
- height and stacks trailing actions when its available width gets tight. */
99
+ height and stacks trailing actions when its available width gets tight,
100
+ unless the row opts out with `stackTrailing={false}`. */
100
101
  @utility list-row-description {
101
102
  &[data-description] {
102
103
  align-items: flex-start;
@@ -111,23 +112,23 @@
111
112
  }
112
113
 
113
114
  @container list-row (width < 28rem) {
114
- &[data-description] [data-slot='list-row-content'] {
115
+ &[data-description]:not([data-stack-trailing='never']) [data-slot='list-row-content'] {
115
116
  align-items: stretch;
116
117
  flex-direction: column;
117
118
  }
118
119
 
119
- &[data-description] [data-slot='list-row-label'],
120
- &[data-description] [data-slot='list-row-trailing'] {
120
+ &[data-description]:not([data-stack-trailing='never']) [data-slot='list-row-label'],
121
+ &[data-description]:not([data-stack-trailing='never']) [data-slot='list-row-trailing'] {
121
122
  flex: none;
122
123
  width: 100%;
123
124
  }
124
125
 
125
- &[data-description] [data-slot='list-row-trailing'] {
126
+ &[data-description]:not([data-stack-trailing='never']) [data-slot='list-row-trailing'] {
126
127
  flex-wrap: wrap;
127
128
  justify-content: flex-end;
128
129
  }
129
130
 
130
- &[data-description] [data-slot='list-row-trailing'] button {
131
+ &[data-description]:not([data-stack-trailing='never']) [data-slot='list-row-trailing'] button {
131
132
  max-width: 100%;
132
133
  white-space: normal;
133
134
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.113.1",
3
+ "version": "0.115.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",
@@ -18,6 +18,12 @@ export type ListRowControlProps<T extends ValidComponent = 'div'> = PolymorphicP
18
18
  trailing?: JSX.Element
19
19
  /** A second line that wraps and grows the row to fit its content. */
20
20
  description?: string
21
+ /**
22
+ * Whether a described row may drop its trailing slot onto its own line when
23
+ * the row gets narrow (the default). Pass `false` for a short, truncating
24
+ * label whose single action must stay beside it.
25
+ */
26
+ stackTrailing?: boolean
21
27
  /** Native button type; defaults to `button` for button rows. */
22
28
  type?: ComponentProps<'button'>['type']
23
29
  }
@@ -37,6 +43,7 @@ export function ListRowControl<T extends ValidComponent = 'div'>(props: ListRowC
37
43
  'leading',
38
44
  'trailing',
39
45
  'description',
46
+ 'stackTrailing',
40
47
  'type',
41
48
  'tabIndex',
42
49
  'children',
@@ -52,6 +59,7 @@ export function ListRowControl<T extends ValidComponent = 'div'>(props: ListRowC
52
59
  aria-current={local.selected ? 'true' : undefined}
53
60
  data-selected={local.selected ? '' : undefined}
54
61
  data-description={local.description ? '' : undefined}
62
+ data-stack-trailing={local.stackTrailing === false ? 'never' : undefined}
55
63
  class={cn(
56
64
  'group/row list-row-description flex min-w-0 items-center gap-2.5 rounded-md px-2 text-sm',
57
65
  'transition-colors ease-out outline-none',
@@ -1,3 +1,3 @@
1
1
  /** Keyboard-accessible tree rows with virtualization-aware focus handoff. */
2
- export { Tree, TreeRow, type TreeProps, type TreeRowProps } from './tree'
2
+ export { Tree, TreeRow, TreeRowActions, type TreeProps, type TreeRowProps } from './tree'
3
3
  export type { TreeItemDescriptor } from './tree-navigation'
@@ -238,7 +238,12 @@ export function Tree(props: TreeProps) {
238
238
  role="tree"
239
239
  aria-multiselectable={local.selectionMode === 'multiple' ? 'true' : undefined}
240
240
  data-slot="tree"
241
- class={cn('flex min-w-0 flex-col', local.class)}
241
+ class={cn(
242
+ /* A virtualized host remounts rows as it scrolls; scroll anchoring
243
+ would latch onto the focused row and fight the scrollbar. */
244
+ 'flex min-w-0 flex-col [overflow-anchor:none]',
245
+ local.class
246
+ )}
242
247
  onKeyDown={handleKeyDown}
243
248
  >
244
249
  {local.children}
@@ -382,6 +387,31 @@ export function TreeRow(props: TreeRowProps) {
382
387
  )
383
388
  }
384
389
 
390
+ /**
391
+ * TreeRowActions.
392
+ *
393
+ * Row-level commands (rename, copy, delete) that stay out of the way until the
394
+ * row is hovered or holds focus, so a long tree reads as names rather than a
395
+ * wall of buttons. Put it in `TreeRow`'s `trailing` slot beside any status that
396
+ * must stay visible. `pinned` keeps the actions shown, e.g. while a destructive
397
+ * action waits for its confirming second press.
398
+ */
399
+ export function TreeRowActions(props: ComponentProps<'span'> & { pinned?: boolean }) {
400
+ const [local, rest] = splitProps(props, ['class', 'pinned'])
401
+ return (
402
+ <span
403
+ data-slot="tree-row-actions"
404
+ data-pinned={local.pinned ? '' : undefined}
405
+ class={cn(
406
+ 'flex shrink-0 items-center gap-1 opacity-0 transition-opacity ease-out',
407
+ 'group-hover/tree-row:opacity-100 group-focus-within/tree-row:opacity-100 data-pinned:opacity-100',
408
+ local.class
409
+ )}
410
+ {...rest}
411
+ />
412
+ )
413
+ }
414
+
385
415
  function isActionTarget(target: EventTarget | null, currentTarget: HTMLElement): boolean {
386
416
  if (!(target instanceof Element) || target === currentTarget) return false
387
417
  return Boolean(
@@ -27,6 +27,13 @@ export type ContextualSidebarProps = {
27
27
  label: string
28
28
  /** Visible navigation title and mobile dialog title. */
29
29
  title: string
30
+ /**
31
+ * Where the title row shows. `always` (default) draws it on desktop and in
32
+ * the mobile sheet; `mobile` keeps the desktop title for assistive
33
+ * technology only and drops the desktop row, for hosts whose top bar
34
+ * already names the context. A `header` slot keeps the row visible.
35
+ */
36
+ titleVisibility?: 'always' | 'mobile'
30
37
  /** Host-controlled expanded state for the desktop panel and mobile dialog. */
31
38
  open: boolean
32
39
  onOpenChange(open: boolean): void
@@ -169,10 +176,19 @@ export function ContextualSidebar(props: ContextualSidebarProps) {
169
176
  />
170
177
  </Show>
171
178
  <Show when={!mobile || mobileMount()?.connected}>
172
- <SidebarNavHeader class={cn(mobile && 'pe-12', props.headerClass)}>
173
- <SidebarNavTitle as={props.headingAs ?? 'h2'}>{props.title}</SidebarNavTitle>
174
- {props.header?.(context)}
175
- </SidebarNavHeader>
179
+ <Show
180
+ when={mobile || props.titleVisibility !== 'mobile' || props.header}
181
+ fallback={
182
+ <SidebarNavTitle as={props.headingAs ?? 'h2'} class="sr-only">
183
+ {props.title}
184
+ </SidebarNavTitle>
185
+ }
186
+ >
187
+ <SidebarNavHeader class={cn(mobile && 'pe-12', props.headerClass)}>
188
+ <SidebarNavTitle as={props.headingAs ?? 'h2'}>{props.title}</SidebarNavTitle>
189
+ {props.header?.(context)}
190
+ </SidebarNavHeader>
191
+ </Show>
176
192
  <SidebarNavContent class={props.contentClass}>{props.content(context)}</SidebarNavContent>
177
193
  {props.footer ? (
178
194
  <SidebarNavFooter class={props.footerClass}>{props.footer(context)}</SidebarNavFooter>
@@ -140,6 +140,12 @@ export type SidebarNavSectionProps = ComponentProps<'div'> & {
140
140
  count?: number
141
141
  /** A control at the trailing edge of the heading row. */
142
142
  action?: JSX.Element
143
+ /**
144
+ * When the trailing action shows. `hover` (default) reveals it on hover or
145
+ * focus; `always` keeps it visible, for a section whose primary action is
146
+ * creating its first item.
147
+ */
148
+ actionVisibility?: 'hover' | 'always'
143
149
  /** Semantic heading element for a host's section hierarchy. */
144
150
  headingAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
145
151
  }
@@ -156,6 +162,7 @@ export function SidebarNavSection(props: SidebarNavSectionProps) {
156
162
  'triggerProps',
157
163
  'count',
158
164
  'action',
165
+ 'actionVisibility',
159
166
  'headingAs',
160
167
  'children',
161
168
  ])
@@ -237,7 +244,13 @@ export function SidebarNavSection(props: SidebarNavSectionProps) {
237
244
  </Show>
238
245
  </Polymorphic>
239
246
  <Show when={local.action}>
240
- <span class="shrink-0 opacity-0 transition-opacity ease-out group-hover/section-header:opacity-100 focus-within:opacity-100 [@media(any-pointer:coarse)]:opacity-100">
247
+ <span
248
+ data-slot="sidebar-nav-section-action"
249
+ class={cn('shrink-0', {
250
+ 'opacity-0 transition-opacity ease-out group-hover/section-header:opacity-100 focus-within:opacity-100 [@media(any-pointer:coarse)]:opacity-100':
251
+ local.actionVisibility !== 'always',
252
+ })}
253
+ >
241
254
  {local.action}
242
255
  </span>
243
256
  </Show>
@@ -99,7 +99,9 @@ export function TopBarTitle(
99
99
  <div
100
100
  data-slot="top-bar-title"
101
101
  class={cn(
102
- 'truncate text-sm font-medium',
102
+ /* The title slot sits in the drag region; a link, button or field
103
+ placed in it (a breadcrumb, a view's search) must stay pressable. */
104
+ 'truncate text-sm font-medium [&_:is(a,button,input)]:window-no-drag',
103
105
  local.align === 'center' && 'text-center',
104
106
  local.class
105
107
  )}
@@ -114,7 +116,10 @@ export function TopBarBreadcrumb(props: ComponentProps<'div'>) {
114
116
  return (
115
117
  <div
116
118
  data-slot="top-bar-breadcrumb"
117
- class={cn('flex min-w-0 items-center gap-1.5 text-sm', local.class)}
119
+ class={cn(
120
+ 'flex min-w-0 items-center gap-1.5 text-sm [&_:is(a,button,input)]:window-no-drag',
121
+ local.class
122
+ )}
118
123
  {...rest}
119
124
  />
120
125
  )
@@ -314,12 +314,19 @@ export function DialogContent(props: DialogContentProps) {
314
314
  )
315
315
  }
316
316
 
317
- export function DialogHeader(props: ComponentProps<'div'>) {
318
- const [local, rest] = splitProps(props, ['class'])
317
+ /**
318
+ * The dialog's title block. `band` steps it onto the elevation ladder's
319
+ * subtle-fill rung (`--muted`), the same treatment as the sheet parts' band —
320
+ * for the dialog family whose panel anatomy matches a sheet's (a title band
321
+ * over a full-bleed shell, such as a settings dialog) rather than the centred
322
+ * modal's documented flat surface. See `SheetFooter` for the rung's rationale.
323
+ */
324
+ export function DialogHeader(props: ComponentProps<'div'> & { band?: boolean }) {
325
+ const [local, rest] = splitProps(props, ['class', 'band'])
319
326
  return (
320
327
  <div
321
328
  data-slot="dialog-header"
322
- class={cn('flex flex-col gap-1.5 pe-6 text-start', local.class)}
329
+ class={cn('flex flex-col gap-1.5 pe-6 text-start', { 'bg-muted': local.band }, local.class)}
323
330
  {...rest}
324
331
  />
325
332
  )
@@ -9,17 +9,28 @@ import { cn } from '#lib/utils'
9
9
  * when a consumer needs only the control; `Input` adds native datalist
10
10
  * suggestions for forms that need them.
11
11
  */
12
- export type InputControlProps = ComponentProps<'input'>
12
+ export type InputControlProps = Omit<ComponentProps<'input'>, 'size'> & {
13
+ /**
14
+ * The control rung, matching `SelectTrigger`: `sm` for a dense toolbar or
15
+ * pane filter beside `icon-sm` actions, `md` (the default) everywhere else.
16
+ */
17
+ size?: 'sm' | 'md' | 'lg'
18
+ }
13
19
 
14
20
  export function InputControl(props: InputControlProps) {
15
- const [local, rest] = splitProps(props, ['class', 'type'])
21
+ const [local, rest] = splitProps(props, ['class', 'type', 'size'])
16
22
 
17
23
  return (
18
24
  <input
19
25
  data-slot="input"
20
26
  type={local.type ?? 'text'}
21
27
  class={cn(
22
- 'h-control-md w-full min-w-0 rounded-md border border-input bg-transparent px-control-md py-1 text-sm',
28
+ 'w-full min-w-0 rounded-md border border-input bg-transparent py-1',
29
+ {
30
+ 'h-control-sm px-control-sm text-xs': local.size === 'sm',
31
+ 'h-control-md px-control-md text-sm': local.size === 'md' || local.size === undefined,
32
+ 'h-control-lg px-control-lg text-sm': local.size === 'lg',
33
+ },
23
34
  'transition-[color,box-shadow,border-color] ease-out outline-none',
24
35
  'placeholder:text-muted-foreground',
25
36
  'selection:bg-primary selection:text-primary-foreground',
@@ -1,7 +1,6 @@
1
- import type { ComponentProps } from 'solid-js'
2
1
  import { createUniqueId, For, Show, splitProps } from 'solid-js'
3
2
  import { useFormFieldControl } from '#lib/form-field'
4
- import { InputControl } from './input-control'
3
+ import { InputControl, type InputControlProps } from './input-control'
5
4
 
6
5
  /**
7
6
  * Input.
@@ -12,10 +11,11 @@ import { InputControl } from './input-control'
12
11
  * them separate is what lets an input sit inside a custom control without
13
12
  * inheriting a form's ARIA graph.
14
13
  *
15
- * Height comes from `h-control-md`; an input cannot be given a new height
16
- * without a new token, which is what keeps it aligned with Button and Select.
14
+ * Height comes from the control tokens (`size`, `md` by default); an input
15
+ * cannot be given a height off that ladder, which is what keeps it aligned
16
+ * with Button and Select.
17
17
  */
18
- export type InputProps = ComponentProps<'input'> & {
18
+ export type InputProps = InputControlProps & {
19
19
  /** Native text suggestions; custom values remain valid and submit unchanged. */
20
20
  suggestions?: readonly string[]
21
21
  }
@@ -34,8 +34,9 @@ import { createDialogFocusRestoration, DialogOverlay } from '../dialog/dialog'
34
34
  *
35
35
  * Its parts read alike everywhere: `SheetHeader` holds a heading, a description
36
36
  * and a full-width rule beneath, with the close button in the corner;
37
- * `SheetBody` scrolls on its own; `SheetFooter` is a full-width tinted band that
38
- * holds the decision.
37
+ * `SheetBody` scrolls on its own; `SheetFooter` is a full-width band that holds
38
+ * the decision. Pass `band` to the header, the footer, or both to step those
39
+ * parts onto the ladder's subtle-fill rung (see `SheetFooter`).
39
40
  *
40
41
  * Controlled sheets opened from outside a `SheetTrigger` restore focus to the
41
42
  * element focused before opening. Pass `restoreFocusRef` when a stable external
@@ -174,13 +175,20 @@ export function SheetCloseButton(props: ComponentProps<typeof KobalteDialog.Clos
174
175
  return <KobalteDialog.CloseButton class={cn(local.class)} {...rest} />
175
176
  }
176
177
 
177
- export function SheetHeader(props: ComponentProps<'div'>) {
178
- const [local, rest] = splitProps(props, ['class'])
178
+ /**
179
+ * The sheet's title band: a heading, a description and the full-width rule
180
+ * beneath, with the close button in the corner. `band` steps the band onto the
181
+ * subtle-fill rung (see `SheetFooter`) — the two-toned anatomy of a sheet that
182
+ * reads as a panel beside the work rather than one flat canvas.
183
+ */
184
+ export function SheetHeader(props: ComponentProps<'div'> & { band?: boolean }) {
185
+ const [local, rest] = splitProps(props, ['class', 'band'])
179
186
  return (
180
187
  <div
181
188
  data-slot="sheet-header"
182
189
  class={cn(
183
190
  'flex shrink-0 flex-col gap-1.5 border-b border-border p-4 pe-12 text-start',
191
+ { 'bg-muted': local.band },
184
192
  local.class
185
193
  )}
186
194
  {...rest}
@@ -199,13 +207,27 @@ export function SheetBody(props: ComponentProps<'div'>) {
199
207
  )
200
208
  }
201
209
 
202
- export function SheetFooter(props: ComponentProps<'div'>) {
203
- const [local, rest] = splitProps(props, ['class'])
210
+ /**
211
+ * The sheet's decision band: a full-width, hairline-ruled strip that holds the
212
+ * actions. `band` paints it with `--muted` — the elevation ladder's
213
+ * subtle-fill rung, one step up from the panel's `--background` — so the sheet
214
+ * reads two-toned: title band and decision band on the raised rung, scrolling
215
+ * body on the panel surface. The rung already exists for cards, sidebars and
216
+ * input fills; this is the panel-shaped overlay family claiming it. Opt-in
217
+ * because the flat panel is the documented default: the centred modal
218
+ * separates with veil, hairline and shadow instead (the elevated overlay rung
219
+ * amplifies the canvas chroma, which reads as a cast across a large panel),
220
+ * and a host opts a whole sheet family in by passing the prop where it composes
221
+ * the parts — not by restyling them from the outside.
222
+ */
223
+ export function SheetFooter(props: ComponentProps<'div'> & { band?: boolean }) {
224
+ const [local, rest] = splitProps(props, ['class', 'band'])
204
225
  return (
205
226
  <div
206
227
  data-slot="sheet-footer"
207
228
  class={cn(
208
229
  'flex shrink-0 flex-wrap items-center justify-end gap-2 border-t border-border bg-background px-4 py-3',
230
+ { 'bg-muted': local.band },
209
231
  local.class
210
232
  )}
211
233
  {...rest}
@@ -13,7 +13,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'
13
13
  * with no text (unreadable to anyone who cannot see it) and a bare word with no
14
14
  * explanation ("Needs configuration" — of what?).
15
15
  *
16
- * The tones are deliberately not a rainbow. Six cover what a status can actually
16
+ * The tones are deliberately not a rainbow. Seven cover what a status can actually
17
17
  * mean:
18
18
  *
19
19
  * - `neutral` — fine, and there is nothing to say about it.
@@ -21,6 +21,8 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'
21
21
  * - `warning` — degraded, or needs a decision from the user.
22
22
  * - `danger` — blocked, failed, unrecoverable without intervention.
23
23
  * - `info` — in flight or scheduled; work is happening, nothing is wrong.
24
+ * - `pending` — running now with no result yet (a check, a build). The ring
25
+ * turns, so it reads as live rather than as a settled state.
24
26
  * - `unknown` — the honest absence of information.
25
27
  *
26
28
  * `unknown` is not the same as `neutral`, and collapsing them is the mistake
@@ -39,6 +41,7 @@ export const statusDotVariants = cva('size-2 shrink-0 rounded-full', {
39
41
  warning: 'bg-warning',
40
42
  danger: 'bg-destructive',
41
43
  info: 'bg-info',
44
+ pending: 'border-2 border-info border-r-transparent motion-safe:animate-spin',
42
45
  unknown: 'border border-muted-foreground bg-transparent',
43
46
  },
44
47
  },
@@ -51,10 +54,18 @@ const labelTone: Record<StatusTone, string> = {
51
54
  warning: 'text-foreground',
52
55
  danger: 'text-foreground',
53
56
  info: 'text-foreground',
57
+ pending: 'text-foreground',
54
58
  unknown: 'text-muted-foreground',
55
59
  }
56
60
 
57
- export type StatusTone = 'neutral' | 'success' | 'warning' | 'danger' | 'info' | 'unknown'
61
+ export type StatusTone =
62
+ | 'neutral'
63
+ | 'success'
64
+ | 'warning'
65
+ | 'danger'
66
+ | 'info'
67
+ | 'pending'
68
+ | 'unknown'
58
69
 
59
70
  export type StatusChipProps = Omit<ComponentProps<'span'>, 'children'> & {
60
71
  tone?: StatusTone
@@ -96,7 +96,8 @@
96
96
  }
97
97
 
98
98
  /* Emitted only when ListRowControl is used. A described row changes its own
99
- height and stacks trailing actions when its available width gets tight. */
99
+ height and stacks trailing actions when its available width gets tight,
100
+ unless the row opts out with `stackTrailing={false}`. */
100
101
  @utility list-row-description {
101
102
  &[data-description] {
102
103
  align-items: flex-start;
@@ -111,23 +112,23 @@
111
112
  }
112
113
 
113
114
  @container list-row (width < 28rem) {
114
- &[data-description] [data-slot='list-row-content'] {
115
+ &[data-description]:not([data-stack-trailing='never']) [data-slot='list-row-content'] {
115
116
  align-items: stretch;
116
117
  flex-direction: column;
117
118
  }
118
119
 
119
- &[data-description] [data-slot='list-row-label'],
120
- &[data-description] [data-slot='list-row-trailing'] {
120
+ &[data-description]:not([data-stack-trailing='never']) [data-slot='list-row-label'],
121
+ &[data-description]:not([data-stack-trailing='never']) [data-slot='list-row-trailing'] {
121
122
  flex: none;
122
123
  width: 100%;
123
124
  }
124
125
 
125
- &[data-description] [data-slot='list-row-trailing'] {
126
+ &[data-description]:not([data-stack-trailing='never']) [data-slot='list-row-trailing'] {
126
127
  flex-wrap: wrap;
127
128
  justify-content: flex-end;
128
129
  }
129
130
 
130
- &[data-description] [data-slot='list-row-trailing'] button {
131
+ &[data-description]:not([data-stack-trailing='never']) [data-slot='list-row-trailing'] button {
131
132
  max-width: 100%;
132
133
  white-space: normal;
133
134
  }