@adea-ai/ui 0.114.0 → 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 (55) 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/input/input-control.d.ts +7 -1
  24. package/dist/components/ui/input/input-control.d.ts.map +1 -1
  25. package/dist/components/ui/input/input-control.js +10 -2
  26. package/dist/components/ui/input/input-control.js.map +1 -1
  27. package/dist/components/ui/input/input.d.ts +5 -4
  28. package/dist/components/ui/input/input.d.ts.map +1 -1
  29. package/dist/components/ui/input/input.js.map +1 -1
  30. package/dist/components/ui/status-chip/status-chip.d.ts +5 -3
  31. package/dist/components/ui/status-chip/status-chip.d.ts.map +1 -1
  32. package/dist/components/ui/status-chip/status-chip.js +5 -1
  33. package/dist/components/ui/status-chip/status-chip.js.map +1 -1
  34. package/dist/index.js +2 -2
  35. package/dist/r/src/components/composites/list-row/list-row-control.tsx +8 -0
  36. package/dist/r/src/components/composites/tree/index.ts +1 -1
  37. package/dist/r/src/components/composites/tree/tree.tsx +31 -1
  38. package/dist/r/src/components/layout/contextual-sidebar/contextual-sidebar.tsx +20 -4
  39. package/dist/r/src/components/layout/sidebar-nav/sidebar-nav.tsx +14 -1
  40. package/dist/r/src/components/layout/top-bar/top-bar.tsx +7 -2
  41. package/dist/r/src/components/ui/input/input-control.tsx +14 -3
  42. package/dist/r/src/components/ui/input/input.tsx +5 -5
  43. package/dist/r/src/components/ui/status-chip/status-chip.tsx +13 -2
  44. package/dist/r/src/styles/base.css +7 -6
  45. package/package.json +1 -1
  46. package/src/components/composites/list-row/list-row-control.tsx +8 -0
  47. package/src/components/composites/tree/index.ts +1 -1
  48. package/src/components/composites/tree/tree.tsx +31 -1
  49. package/src/components/layout/contextual-sidebar/contextual-sidebar.tsx +20 -4
  50. package/src/components/layout/sidebar-nav/sidebar-nav.tsx +14 -1
  51. package/src/components/layout/top-bar/top-bar.tsx +7 -2
  52. package/src/components/ui/input/input-control.tsx +14 -3
  53. package/src/components/ui/input/input.tsx +5 -5
  54. package/src/components/ui/status-chip/status-chip.tsx +13 -2
  55. package/src/styles/base.css +7 -6
@@ -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
  )
@@ -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
  }
@@ -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.114.0",
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
  )
@@ -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
  }
@@ -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
  }