@adea-ai/ui 0.97.7 → 0.99.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 (52) hide show
  1. package/dist/components/composites/action-button/action-button.d.ts +1 -0
  2. package/dist/components/composites/action-button/action-button.d.ts.map +1 -1
  3. package/dist/components/composites/action-button/action-button.js +1 -1
  4. package/dist/components/composites/action-button/action-button.js.map +1 -1
  5. package/dist/components/layout/side-rail/side-rail.d.ts.map +1 -1
  6. package/dist/components/layout/side-rail/side-rail.js +10 -7
  7. package/dist/components/layout/side-rail/side-rail.js.map +1 -1
  8. package/dist/components/layout/split-layout/split-layout.d.ts.map +1 -1
  9. package/dist/components/layout/split-layout/split-layout.js +25 -16
  10. package/dist/components/layout/split-layout/split-layout.js.map +1 -1
  11. package/dist/components/layout/top-bar/top-bar.d.ts +7 -0
  12. package/dist/components/layout/top-bar/top-bar.d.ts.map +1 -1
  13. package/dist/components/layout/top-bar/top-bar.js +11 -5
  14. package/dist/components/layout/top-bar/top-bar.js.map +1 -1
  15. package/dist/components/ui/kbd/kbd.d.ts.map +1 -1
  16. package/dist/components/ui/kbd/kbd.js +1 -1
  17. package/dist/components/ui/kbd/kbd.js.map +1 -1
  18. package/dist/components/ui/status-chip/status-chip.d.ts.map +1 -1
  19. package/dist/components/ui/status-chip/status-chip.js +0 -1
  20. package/dist/components/ui/status-chip/status-chip.js.map +1 -1
  21. package/dist/components/ui/tooltip/tooltip.d.ts +1 -4
  22. package/dist/components/ui/tooltip/tooltip.d.ts.map +1 -1
  23. package/dist/components/ui/tooltip/tooltip.js +3 -3
  24. package/dist/components/ui/tooltip/tooltip.js.map +1 -1
  25. package/dist/lib/overlay.d.ts +7 -0
  26. package/dist/lib/overlay.d.ts.map +1 -1
  27. package/dist/lib/overlay.js +8 -1
  28. package/dist/lib/overlay.js.map +1 -1
  29. package/dist/lib/tokens.d.ts.map +1 -1
  30. package/dist/lib/tokens.js +5 -0
  31. package/dist/lib/tokens.js.map +1 -1
  32. package/dist/r/src/components/composites/action-button/action-button.tsx +2 -1
  33. package/dist/r/src/components/layout/side-rail/side-rail.tsx +7 -3
  34. package/dist/r/src/components/layout/split-layout/split-layout.tsx +19 -3
  35. package/dist/r/src/components/layout/top-bar/top-bar.tsx +13 -2
  36. package/dist/r/src/components/ui/kbd/kbd.tsx +0 -2
  37. package/dist/r/src/components/ui/status-chip/status-chip.tsx +1 -3
  38. package/dist/r/src/components/ui/tooltip/tooltip.tsx +7 -7
  39. package/dist/r/src/lib/overlay.ts +9 -0
  40. package/dist/r/src/lib/tokens.ts +6 -0
  41. package/dist/r/src/styles/theme.css +5 -0
  42. package/package.json +1 -1
  43. package/src/components/composites/action-button/action-button.tsx +2 -1
  44. package/src/components/layout/side-rail/side-rail.tsx +7 -3
  45. package/src/components/layout/split-layout/split-layout.tsx +19 -3
  46. package/src/components/layout/top-bar/top-bar.tsx +13 -2
  47. package/src/components/ui/kbd/kbd.tsx +0 -2
  48. package/src/components/ui/status-chip/status-chip.tsx +1 -3
  49. package/src/components/ui/tooltip/tooltip.tsx +7 -7
  50. package/src/lib/overlay.ts +9 -0
  51. package/src/lib/tokens.ts +6 -0
  52. package/src/styles/theme.css +5 -0
@@ -27,6 +27,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
27
27
  export type ActionButtonProps<T extends ValidComponent = 'button'> = ButtonProps<T> & {
28
28
  /** A short explanation, also shown on keyboard focus. The Button still needs its own name. */
29
29
  tooltip?: string
30
+ /** Placement of the explanation; icon actions live in bars, so it defaults below the control. */
30
31
  tooltipSide?: 'top' | 'right' | 'bottom' | 'left'
31
32
  /** Disables the action and announces the busy label through a polite status region. */
32
33
  busy?: boolean
@@ -173,7 +174,7 @@ export function ActionButton<T extends ValidComponent = 'button'>(props: ActionB
173
174
  >
174
175
  {contents()}
175
176
  </TooltipTrigger>
176
- <TooltipContent side={local.tooltipSide}>{local.tooltip}</TooltipContent>
177
+ <TooltipContent side={local.tooltipSide ?? 'bottom'}>{local.tooltip}</TooltipContent>
177
178
  </Tooltip>
178
179
  </Show>
179
180
  <Show when={busy()}>
@@ -2,6 +2,7 @@ import { Polymorphic, type PolymorphicProps } from '@kobalte/core/polymorphic'
2
2
  import type { ComponentProps, JSX, ValidComponent } from 'solid-js'
3
3
  import { Show, createMemo, createSignal, splitProps } from 'solid-js'
4
4
  import { Portal } from 'solid-js/web'
5
+ import { tooltipTip } from '../../../lib/overlay'
5
6
  import { cn } from '../../../lib/utils'
6
7
  import { SideRailContext, createSideRailValue } from './side-rail-context'
7
8
 
@@ -305,8 +306,8 @@ export function SideRailItem<T extends ValidComponent = 'a'>(props: SideRailItem
305
306
  aria-hidden="true"
306
307
  data-slot="side-rail-tip"
307
308
  class={cn(
308
- 'bg-card text-card-foreground border-border pointer-events-none fixed z-(--z-tooltip)',
309
- 'flex items-center gap-2.5 rounded-md border ps-3 pe-3 text-sm font-medium whitespace-nowrap shadow-lg'
309
+ tooltipTip,
310
+ 'pointer-events-none fixed z-(--z-tooltip) flex items-center gap-2.5 ps-3 pe-3 whitespace-nowrap'
310
311
  )}
311
312
  style={{
312
313
  top: `${position().top}px`,
@@ -398,7 +399,10 @@ export function SideRailButton(
398
399
  <div
399
400
  aria-hidden="true"
400
401
  data-slot="side-rail-tip"
401
- class="bg-card text-card-foreground border-border pointer-events-none fixed z-(--z-tooltip) flex items-center gap-2.5 rounded-md border ps-3 pe-3 text-sm font-medium whitespace-nowrap shadow-lg"
402
+ class={cn(
403
+ tooltipTip,
404
+ 'pointer-events-none fixed z-(--z-tooltip) flex items-center gap-2.5 ps-3 pe-3 whitespace-nowrap'
405
+ )}
402
406
  style={{
403
407
  top: `${position().top}px`,
404
408
  left: `${position().left}px`,
@@ -171,7 +171,8 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
171
171
  role="group"
172
172
  aria-label={props.label}
173
173
  data-slot="split-layout"
174
- class={cn('relative size-full min-h-0 min-w-0', props.class)}
174
+ data-dragging={drag() ? '' : undefined}
175
+ class={cn('relative size-full min-h-0 min-w-0 data-[dragging]:cursor-grabbing', props.class)}
175
176
  >
176
177
  <For each={leaves().map((leaf) => leaf.id)}>
177
178
  {(id) => {
@@ -194,6 +195,7 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
194
195
  tabIndex={props.paneTabIndex ?? -1}
195
196
  data-pane-id={id}
196
197
  data-focused={props.state.focusedLeafId === id ? '' : undefined}
198
+ data-drag-source={drag()?.id === id ? '' : undefined}
197
199
  data-drop-direction={intent()?.direction}
198
200
  data-drop-placement={intent()?.placement}
199
201
  onDragOver={(event) => {
@@ -234,14 +236,16 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
234
236
  props.onMove?.(current.id, id, next)
235
237
  }}
236
238
  style={rectStyle(frames().get(id)?.rect ?? { x: 0, y: 0, width: 0, height: 0 })}
237
- class="absolute flex min-h-0 min-w-0 flex-col overflow-hidden border border-border data-[focused]:border-primary bg-background text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
239
+ class="absolute flex min-h-0 min-w-0 flex-col overflow-hidden border border-border transition-opacity data-[drag-source]:opacity-40 data-[focused]:border-primary bg-background text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
238
240
  onFocusIn={() => props.onFocus?.(id)}
239
241
  >
240
242
  <header class="flex min-w-0 shrink-0 items-center gap-2 bg-surface px-2">
241
243
  <span
242
244
  data-pane-drag-handle=""
243
245
  draggable={Boolean(props.onMove)}
244
- class="flex min-w-0 flex-1 items-center gap-2 truncate text-sm"
246
+ class={cn('flex min-w-0 flex-1 items-center gap-2 truncate text-sm', {
247
+ 'cursor-grab': Boolean(props.onMove),
248
+ })}
245
249
  title={props.onMove ? `Drag ${props.labelForLeaf(leaf())} to move` : undefined}
246
250
  onDragStart={(event) => {
247
251
  if (!props.onMove || !event.dataTransfer) {
@@ -252,6 +256,18 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
252
256
  setDrag({ id, token })
253
257
  event.dataTransfer.setData(PANE_DRAG_TYPE, token)
254
258
  event.dataTransfer.effectAllowed = 'move'
259
+ // The ghost is the pane itself, not the handle strip: the
260
+ // browser snapshots the region before the drag state dims
261
+ // this section, so the dragged pane stays readable while
262
+ // its source visibly recedes.
263
+ const source = refs.get(id)
264
+ const frame = source?.getBoundingClientRect()
265
+ if (source && frame && typeof event.dataTransfer.setDragImage === 'function')
266
+ event.dataTransfer.setDragImage(
267
+ source,
268
+ Math.max(0, Math.round(event.clientX - frame.left)),
269
+ Math.max(0, Math.round(event.clientY - frame.top))
270
+ )
255
271
  }}
256
272
  onDragEnd={clearDrag}
257
273
  >
@@ -125,6 +125,13 @@ export type TopBarSearchProps = ComponentProps<'button'> & {
125
125
  placeholder?: string
126
126
  /** The shortcut chip drawn at the trailing edge, e.g. "⌘K". */
127
127
  shortcut?: string
128
+ /**
129
+ * The parseable chord behind `shortcut`, e.g. `Meta+K`. The chip is
130
+ * `aria-hidden` — reading it out would pollute the button's accessible
131
+ * name — so this is what assistive technology announces instead. Supply it
132
+ * whenever `shortcut` is drawn.
133
+ */
134
+ keyshortcuts?: string
128
135
  }
129
136
 
130
137
  /**
@@ -137,7 +144,7 @@ export type TopBarSearchProps = ComponentProps<'button'> & {
137
144
  * like from the user's side.
138
145
  */
139
146
  export function TopBarSearch(props: TopBarSearchProps) {
140
- const [local, rest] = splitProps(props, ['class', 'placeholder', 'shortcut'])
147
+ const [local, rest] = splitProps(props, ['class', 'placeholder', 'shortcut', 'keyshortcuts'])
141
148
 
142
149
  return (
143
150
  <button
@@ -151,12 +158,16 @@ export function TopBarSearch(props: TopBarSearchProps) {
151
158
  'window-no-drag',
152
159
  local.class
153
160
  )}
161
+ aria-keyshortcuts={local.keyshortcuts}
154
162
  {...rest}
155
163
  >
156
164
  <Search aria-hidden="true" class="size-4 shrink-0 sm:hidden" />
157
165
  <span class="sr-only sm:not-sr-only sm:truncate">{local.placeholder ?? 'Search'}</span>
158
166
  <Show when={local.shortcut}>
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">
167
+ <kbd
168
+ aria-hidden="true"
169
+ 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"
170
+ >
160
171
  {local.shortcut}
161
172
  </kbd>
162
173
  </Show>
@@ -20,8 +20,6 @@ export function Kbd(props: ComponentProps<'kbd'>) {
20
20
  data-slot="kbd"
21
21
  class={cn(
22
22
  'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 min-w-5 items-center justify-center gap-1 rounded-sm border border-border px-1 font-mono text-2xs font-medium select-none',
23
- /* Inside a hover/selected row the key needs to recede with the row. */
24
- 'in-data-[slot=br-tooltip-content]:bg-background/20 in-data-[slot=br-tooltip-content]:text-scrim-foreground in-data-[slot=br-tooltip-content]:border-transparent',
25
23
  local.class
26
24
  )}
27
25
  {...rest}
@@ -99,9 +99,7 @@ export function StatusChip(props: StatusChipProps) {
99
99
  <TooltipTrigger as="span" class="inline-flex">
100
100
  {chip}
101
101
  </TooltipTrigger>
102
- <TooltipContent data-slot="tooltip-content" hideArrow>
103
- {detail()}
104
- </TooltipContent>
102
+ <TooltipContent data-slot="tooltip-content">{detail()}</TooltipContent>
105
103
  </Tooltip>
106
104
  )}
107
105
  </Show>
@@ -10,6 +10,7 @@ import {
10
10
  useContext,
11
11
  } from 'solid-js'
12
12
  import { isServer } from 'solid-js/web'
13
+ import { tooltipTip } from '../../../lib/overlay'
13
14
  import { cn } from '../../../lib/utils'
14
15
 
15
16
  /**
@@ -348,16 +349,12 @@ export function TooltipTrigger(props: ComponentProps<typeof KobalteTooltip.Trigg
348
349
  )
349
350
  }
350
351
 
351
- export type TooltipContentProps = ComponentProps<typeof KobalteTooltip.Content> & {
352
- /** Hide the caret pointing at the trigger. */
353
- hideArrow?: boolean
354
- }
352
+ export type TooltipContentProps = ComponentProps<typeof KobalteTooltip.Content>
355
353
 
356
354
  export function TooltipContent(props: TooltipContentProps) {
357
355
  const interaction = useContext(TooltipInteractionContext)
358
356
  const [local, rest] = splitProps(props, [
359
357
  'class',
360
- 'hideArrow',
361
358
  'children',
362
359
  'aria-hidden',
363
360
  'onEscapeKeyDown',
@@ -371,7 +368,11 @@ export function TooltipContent(props: TooltipContentProps) {
371
368
  <KobalteTooltip.Portal>
372
369
  <KobalteTooltip.Content
373
370
  class={cn(
374
- 'bg-scrim text-scrim-foreground z-(--z-tooltip) w-fit max-w-64 rounded-md px-2 py-1 text-xs',
371
+ tooltipTip,
372
+ // The max width is a token, not a fixed measure: it has to fall
373
+ // behind the viewport at large root font sizes or the tip — which
374
+ // Kobalte can shift but never shrink — forces a horizontal scroll.
375
+ 'z-(--z-tooltip) w-fit max-w-(--tooltip-max-width) px-2 py-1',
375
376
  'origin-(--kb-tooltip-content-transform-origin) text-balance',
376
377
  'data-expanded:animate-in data-expanded:fade-in-0 data-expanded:zoom-in-95',
377
378
  'data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
@@ -390,7 +391,6 @@ export function TooltipContent(props: TooltipContentProps) {
390
391
  {...rest}
391
392
  >
392
393
  {local.children}
393
- {!local.hideArrow && <KobalteTooltip.Arrow aria-hidden="true" />}
394
394
  </KobalteTooltip.Content>
395
395
  </KobalteTooltip.Portal>
396
396
  )
@@ -21,6 +21,15 @@ export const overlayPositioner = 'fixed inset-0 z-(--z-dialog) grid place-items-
21
21
  export const overlayScrim =
22
22
  'fixed inset-0 z-(--z-dialog) bg-scrim/50 supports-[backdrop-filter]:backdrop-blur-xs'
23
23
 
24
+ /**
25
+ * The tip: the card-toned bubble an explanation shows in. The side rail draws
26
+ * it flush to a collapsed row and every floating tooltip composes the same
27
+ * fill, edge, and type, so a control never explains itself in two visual
28
+ * languages.
29
+ */
30
+ export const tooltipTip =
31
+ 'bg-card text-card-foreground rounded-md border border-border text-sm font-medium shadow-lg'
32
+
24
33
  /**
25
34
  * Entrance and exit motion, expressed as data-state classes.
26
35
  *
@@ -542,6 +542,12 @@ export const densityTokens: TokenDefinition[] = [
542
542
  },
543
543
  { name: 'topbar-height', kind: 'dimension', description: '48px — the window title row.' },
544
544
  { name: 'statusbar-height', kind: 'dimension', description: '28px — the bottom readout strip.' },
545
+ {
546
+ name: 'tooltip-max-width',
547
+ kind: 'dimension',
548
+ description:
549
+ 'The lesser of the 16rem readable measure and one viewport minus a gutter — a tip wraps before it can push its document wider than itself.',
550
+ },
545
551
  ]
546
552
 
547
553
  export const elevationTokens: TokenDefinition[] = [
@@ -614,6 +614,11 @@
614
614
  --sidebar-width-compact: 3rem;
615
615
  --topbar-height: 3rem;
616
616
  --statusbar-height: 1.75rem;
617
+ /* A tip wraps before it can overflow the window carrying it: the 16rem
618
+ readable measure governs on a desktop, but the measure scales with the root
619
+ font while the window does not, so at a 200% zoom the viewport term wins and
620
+ a tooltip can never push its document wider than itself. */
621
+ --tooltip-max-width: min(16rem, calc(100vw - 1.5rem));
617
622
 
618
623
  /* Shadows. Two parts on purpose: a tinted spread that reads as elevation, plus a
619
624
  hairline that keeps an edge visible when the surface behind a panel is the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.97.7",
3
+ "version": "0.99.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",
@@ -27,6 +27,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
27
27
  export type ActionButtonProps<T extends ValidComponent = 'button'> = ButtonProps<T> & {
28
28
  /** A short explanation, also shown on keyboard focus. The Button still needs its own name. */
29
29
  tooltip?: string
30
+ /** Placement of the explanation; icon actions live in bars, so it defaults below the control. */
30
31
  tooltipSide?: 'top' | 'right' | 'bottom' | 'left'
31
32
  /** Disables the action and announces the busy label through a polite status region. */
32
33
  busy?: boolean
@@ -173,7 +174,7 @@ export function ActionButton<T extends ValidComponent = 'button'>(props: ActionB
173
174
  >
174
175
  {contents()}
175
176
  </TooltipTrigger>
176
- <TooltipContent side={local.tooltipSide}>{local.tooltip}</TooltipContent>
177
+ <TooltipContent side={local.tooltipSide ?? 'bottom'}>{local.tooltip}</TooltipContent>
177
178
  </Tooltip>
178
179
  </Show>
179
180
  <Show when={busy()}>
@@ -2,6 +2,7 @@ import { Polymorphic, type PolymorphicProps } from '@kobalte/core/polymorphic'
2
2
  import type { ComponentProps, JSX, ValidComponent } from 'solid-js'
3
3
  import { Show, createMemo, createSignal, splitProps } from 'solid-js'
4
4
  import { Portal } from 'solid-js/web'
5
+ import { tooltipTip } from '#lib/overlay'
5
6
  import { cn } from '#lib/utils'
6
7
  import { SideRailContext, createSideRailValue } from './side-rail-context'
7
8
 
@@ -305,8 +306,8 @@ export function SideRailItem<T extends ValidComponent = 'a'>(props: SideRailItem
305
306
  aria-hidden="true"
306
307
  data-slot="side-rail-tip"
307
308
  class={cn(
308
- 'bg-card text-card-foreground border-border pointer-events-none fixed z-(--z-tooltip)',
309
- 'flex items-center gap-2.5 rounded-md border ps-3 pe-3 text-sm font-medium whitespace-nowrap shadow-lg'
309
+ tooltipTip,
310
+ 'pointer-events-none fixed z-(--z-tooltip) flex items-center gap-2.5 ps-3 pe-3 whitespace-nowrap'
310
311
  )}
311
312
  style={{
312
313
  top: `${position().top}px`,
@@ -398,7 +399,10 @@ export function SideRailButton(
398
399
  <div
399
400
  aria-hidden="true"
400
401
  data-slot="side-rail-tip"
401
- class="bg-card text-card-foreground border-border pointer-events-none fixed z-(--z-tooltip) flex items-center gap-2.5 rounded-md border ps-3 pe-3 text-sm font-medium whitespace-nowrap shadow-lg"
402
+ class={cn(
403
+ tooltipTip,
404
+ 'pointer-events-none fixed z-(--z-tooltip) flex items-center gap-2.5 ps-3 pe-3 whitespace-nowrap'
405
+ )}
402
406
  style={{
403
407
  top: `${position().top}px`,
404
408
  left: `${position().left}px`,
@@ -171,7 +171,8 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
171
171
  role="group"
172
172
  aria-label={props.label}
173
173
  data-slot="split-layout"
174
- class={cn('relative size-full min-h-0 min-w-0', props.class)}
174
+ data-dragging={drag() ? '' : undefined}
175
+ class={cn('relative size-full min-h-0 min-w-0 data-[dragging]:cursor-grabbing', props.class)}
175
176
  >
176
177
  <For each={leaves().map((leaf) => leaf.id)}>
177
178
  {(id) => {
@@ -194,6 +195,7 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
194
195
  tabIndex={props.paneTabIndex ?? -1}
195
196
  data-pane-id={id}
196
197
  data-focused={props.state.focusedLeafId === id ? '' : undefined}
198
+ data-drag-source={drag()?.id === id ? '' : undefined}
197
199
  data-drop-direction={intent()?.direction}
198
200
  data-drop-placement={intent()?.placement}
199
201
  onDragOver={(event) => {
@@ -234,14 +236,16 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
234
236
  props.onMove?.(current.id, id, next)
235
237
  }}
236
238
  style={rectStyle(frames().get(id)?.rect ?? { x: 0, y: 0, width: 0, height: 0 })}
237
- class="absolute flex min-h-0 min-w-0 flex-col overflow-hidden border border-border data-[focused]:border-primary bg-background text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
239
+ class="absolute flex min-h-0 min-w-0 flex-col overflow-hidden border border-border transition-opacity data-[drag-source]:opacity-40 data-[focused]:border-primary bg-background text-foreground focus-visible:outline focus-visible:outline-2 focus-visible:outline-ring"
238
240
  onFocusIn={() => props.onFocus?.(id)}
239
241
  >
240
242
  <header class="flex min-w-0 shrink-0 items-center gap-2 bg-surface px-2">
241
243
  <span
242
244
  data-pane-drag-handle=""
243
245
  draggable={Boolean(props.onMove)}
244
- class="flex min-w-0 flex-1 items-center gap-2 truncate text-sm"
246
+ class={cn('flex min-w-0 flex-1 items-center gap-2 truncate text-sm', {
247
+ 'cursor-grab': Boolean(props.onMove),
248
+ })}
245
249
  title={props.onMove ? `Drag ${props.labelForLeaf(leaf())} to move` : undefined}
246
250
  onDragStart={(event) => {
247
251
  if (!props.onMove || !event.dataTransfer) {
@@ -252,6 +256,18 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
252
256
  setDrag({ id, token })
253
257
  event.dataTransfer.setData(PANE_DRAG_TYPE, token)
254
258
  event.dataTransfer.effectAllowed = 'move'
259
+ // The ghost is the pane itself, not the handle strip: the
260
+ // browser snapshots the region before the drag state dims
261
+ // this section, so the dragged pane stays readable while
262
+ // its source visibly recedes.
263
+ const source = refs.get(id)
264
+ const frame = source?.getBoundingClientRect()
265
+ if (source && frame && typeof event.dataTransfer.setDragImage === 'function')
266
+ event.dataTransfer.setDragImage(
267
+ source,
268
+ Math.max(0, Math.round(event.clientX - frame.left)),
269
+ Math.max(0, Math.round(event.clientY - frame.top))
270
+ )
255
271
  }}
256
272
  onDragEnd={clearDrag}
257
273
  >
@@ -125,6 +125,13 @@ export type TopBarSearchProps = ComponentProps<'button'> & {
125
125
  placeholder?: string
126
126
  /** The shortcut chip drawn at the trailing edge, e.g. "⌘K". */
127
127
  shortcut?: string
128
+ /**
129
+ * The parseable chord behind `shortcut`, e.g. `Meta+K`. The chip is
130
+ * `aria-hidden` — reading it out would pollute the button's accessible
131
+ * name — so this is what assistive technology announces instead. Supply it
132
+ * whenever `shortcut` is drawn.
133
+ */
134
+ keyshortcuts?: string
128
135
  }
129
136
 
130
137
  /**
@@ -137,7 +144,7 @@ export type TopBarSearchProps = ComponentProps<'button'> & {
137
144
  * like from the user's side.
138
145
  */
139
146
  export function TopBarSearch(props: TopBarSearchProps) {
140
- const [local, rest] = splitProps(props, ['class', 'placeholder', 'shortcut'])
147
+ const [local, rest] = splitProps(props, ['class', 'placeholder', 'shortcut', 'keyshortcuts'])
141
148
 
142
149
  return (
143
150
  <button
@@ -151,12 +158,16 @@ export function TopBarSearch(props: TopBarSearchProps) {
151
158
  'window-no-drag',
152
159
  local.class
153
160
  )}
161
+ aria-keyshortcuts={local.keyshortcuts}
154
162
  {...rest}
155
163
  >
156
164
  <Search aria-hidden="true" class="size-4 shrink-0 sm:hidden" />
157
165
  <span class="sr-only sm:not-sr-only sm:truncate">{local.placeholder ?? 'Search'}</span>
158
166
  <Show when={local.shortcut}>
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">
167
+ <kbd
168
+ aria-hidden="true"
169
+ 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"
170
+ >
160
171
  {local.shortcut}
161
172
  </kbd>
162
173
  </Show>
@@ -20,8 +20,6 @@ export function Kbd(props: ComponentProps<'kbd'>) {
20
20
  data-slot="kbd"
21
21
  class={cn(
22
22
  'bg-muted text-muted-foreground pointer-events-none inline-flex h-5 min-w-5 items-center justify-center gap-1 rounded-sm border border-border px-1 font-mono text-2xs font-medium select-none',
23
- /* Inside a hover/selected row the key needs to recede with the row. */
24
- 'in-data-[slot=br-tooltip-content]:bg-background/20 in-data-[slot=br-tooltip-content]:text-scrim-foreground in-data-[slot=br-tooltip-content]:border-transparent',
25
23
  local.class
26
24
  )}
27
25
  {...rest}
@@ -99,9 +99,7 @@ export function StatusChip(props: StatusChipProps) {
99
99
  <TooltipTrigger as="span" class="inline-flex">
100
100
  {chip}
101
101
  </TooltipTrigger>
102
- <TooltipContent data-slot="tooltip-content" hideArrow>
103
- {detail()}
104
- </TooltipContent>
102
+ <TooltipContent data-slot="tooltip-content">{detail()}</TooltipContent>
105
103
  </Tooltip>
106
104
  )}
107
105
  </Show>
@@ -10,6 +10,7 @@ import {
10
10
  useContext,
11
11
  } from 'solid-js'
12
12
  import { isServer } from 'solid-js/web'
13
+ import { tooltipTip } from '#lib/overlay'
13
14
  import { cn } from '#lib/utils'
14
15
 
15
16
  /**
@@ -348,16 +349,12 @@ export function TooltipTrigger(props: ComponentProps<typeof KobalteTooltip.Trigg
348
349
  )
349
350
  }
350
351
 
351
- export type TooltipContentProps = ComponentProps<typeof KobalteTooltip.Content> & {
352
- /** Hide the caret pointing at the trigger. */
353
- hideArrow?: boolean
354
- }
352
+ export type TooltipContentProps = ComponentProps<typeof KobalteTooltip.Content>
355
353
 
356
354
  export function TooltipContent(props: TooltipContentProps) {
357
355
  const interaction = useContext(TooltipInteractionContext)
358
356
  const [local, rest] = splitProps(props, [
359
357
  'class',
360
- 'hideArrow',
361
358
  'children',
362
359
  'aria-hidden',
363
360
  'onEscapeKeyDown',
@@ -371,7 +368,11 @@ export function TooltipContent(props: TooltipContentProps) {
371
368
  <KobalteTooltip.Portal>
372
369
  <KobalteTooltip.Content
373
370
  class={cn(
374
- 'bg-scrim text-scrim-foreground z-(--z-tooltip) w-fit max-w-64 rounded-md px-2 py-1 text-xs',
371
+ tooltipTip,
372
+ // The max width is a token, not a fixed measure: it has to fall
373
+ // behind the viewport at large root font sizes or the tip — which
374
+ // Kobalte can shift but never shrink — forces a horizontal scroll.
375
+ 'z-(--z-tooltip) w-fit max-w-(--tooltip-max-width) px-2 py-1',
375
376
  'origin-(--kb-tooltip-content-transform-origin) text-balance',
376
377
  'data-expanded:animate-in data-expanded:fade-in-0 data-expanded:zoom-in-95',
377
378
  'data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
@@ -390,7 +391,6 @@ export function TooltipContent(props: TooltipContentProps) {
390
391
  {...rest}
391
392
  >
392
393
  {local.children}
393
- {!local.hideArrow && <KobalteTooltip.Arrow aria-hidden="true" />}
394
394
  </KobalteTooltip.Content>
395
395
  </KobalteTooltip.Portal>
396
396
  )
@@ -21,6 +21,15 @@ export const overlayPositioner = 'fixed inset-0 z-(--z-dialog) grid place-items-
21
21
  export const overlayScrim =
22
22
  'fixed inset-0 z-(--z-dialog) bg-scrim/50 supports-[backdrop-filter]:backdrop-blur-xs'
23
23
 
24
+ /**
25
+ * The tip: the card-toned bubble an explanation shows in. The side rail draws
26
+ * it flush to a collapsed row and every floating tooltip composes the same
27
+ * fill, edge, and type, so a control never explains itself in two visual
28
+ * languages.
29
+ */
30
+ export const tooltipTip =
31
+ 'bg-card text-card-foreground rounded-md border border-border text-sm font-medium shadow-lg'
32
+
24
33
  /**
25
34
  * Entrance and exit motion, expressed as data-state classes.
26
35
  *
package/src/lib/tokens.ts CHANGED
@@ -542,6 +542,12 @@ export const densityTokens: TokenDefinition[] = [
542
542
  },
543
543
  { name: 'topbar-height', kind: 'dimension', description: '48px — the window title row.' },
544
544
  { name: 'statusbar-height', kind: 'dimension', description: '28px — the bottom readout strip.' },
545
+ {
546
+ name: 'tooltip-max-width',
547
+ kind: 'dimension',
548
+ description:
549
+ 'The lesser of the 16rem readable measure and one viewport minus a gutter — a tip wraps before it can push its document wider than itself.',
550
+ },
545
551
  ]
546
552
 
547
553
  export const elevationTokens: TokenDefinition[] = [
@@ -614,6 +614,11 @@
614
614
  --sidebar-width-compact: 3rem;
615
615
  --topbar-height: 3rem;
616
616
  --statusbar-height: 1.75rem;
617
+ /* A tip wraps before it can overflow the window carrying it: the 16rem
618
+ readable measure governs on a desktop, but the measure scales with the root
619
+ font while the window does not, so at a 200% zoom the viewport term wins and
620
+ a tooltip can never push its document wider than itself. */
621
+ --tooltip-max-width: min(16rem, calc(100vw - 1.5rem));
617
622
 
618
623
  /* Shadows. Two parts on purpose: a tinted spread that reads as elevation, plus a
619
624
  hairline that keeps an edge visible when the surface behind a panel is the