@adea-ai/ui 0.98.0 → 0.99.1

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 (62) 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/composites/appearance-editor/appearance-choices.d.ts.map +1 -1
  6. package/dist/components/composites/appearance-editor/appearance-choices.js +32 -28
  7. package/dist/components/composites/appearance-editor/appearance-choices.js.map +1 -1
  8. package/dist/components/composites/appearance-editor/appearance-rows.d.ts.map +1 -1
  9. package/dist/components/composites/appearance-editor/appearance-rows.js +1 -1
  10. package/dist/components/composites/appearance-editor/appearance-rows.js.map +1 -1
  11. package/dist/components/layout/side-rail/side-rail.d.ts.map +1 -1
  12. package/dist/components/layout/side-rail/side-rail.js +10 -7
  13. package/dist/components/layout/side-rail/side-rail.js.map +1 -1
  14. package/dist/components/layout/top-bar/top-bar.d.ts +7 -0
  15. package/dist/components/layout/top-bar/top-bar.d.ts.map +1 -1
  16. package/dist/components/layout/top-bar/top-bar.js +11 -5
  17. package/dist/components/layout/top-bar/top-bar.js.map +1 -1
  18. package/dist/components/ui/dropdown-menu/dropdown-menu.d.ts.map +1 -1
  19. package/dist/components/ui/dropdown-menu/dropdown-menu.js +4 -2
  20. package/dist/components/ui/dropdown-menu/dropdown-menu.js.map +1 -1
  21. package/dist/components/ui/kbd/kbd.d.ts.map +1 -1
  22. package/dist/components/ui/kbd/kbd.js +1 -1
  23. package/dist/components/ui/kbd/kbd.js.map +1 -1
  24. package/dist/components/ui/status-chip/status-chip.d.ts.map +1 -1
  25. package/dist/components/ui/status-chip/status-chip.js +0 -1
  26. package/dist/components/ui/status-chip/status-chip.js.map +1 -1
  27. package/dist/components/ui/tooltip/tooltip.d.ts +1 -4
  28. package/dist/components/ui/tooltip/tooltip.d.ts.map +1 -1
  29. package/dist/components/ui/tooltip/tooltip.js +3 -3
  30. package/dist/components/ui/tooltip/tooltip.js.map +1 -1
  31. package/dist/lib/overlay.d.ts +7 -0
  32. package/dist/lib/overlay.d.ts.map +1 -1
  33. package/dist/lib/overlay.js +8 -1
  34. package/dist/lib/overlay.js.map +1 -1
  35. package/dist/lib/tokens.d.ts.map +1 -1
  36. package/dist/lib/tokens.js +5 -0
  37. package/dist/lib/tokens.js.map +1 -1
  38. package/dist/r/src/components/composites/action-button/action-button.tsx +2 -1
  39. package/dist/r/src/components/composites/appearance-editor/appearance-choices.tsx +35 -20
  40. package/dist/r/src/components/composites/appearance-editor/appearance-rows.tsx +6 -2
  41. package/dist/r/src/components/layout/side-rail/side-rail.tsx +7 -3
  42. package/dist/r/src/components/layout/top-bar/top-bar.tsx +13 -2
  43. package/dist/r/src/components/ui/dropdown-menu/dropdown-menu.tsx +10 -2
  44. package/dist/r/src/components/ui/kbd/kbd.tsx +0 -2
  45. package/dist/r/src/components/ui/status-chip/status-chip.tsx +1 -3
  46. package/dist/r/src/components/ui/tooltip/tooltip.tsx +7 -7
  47. package/dist/r/src/lib/overlay.ts +9 -0
  48. package/dist/r/src/lib/tokens.ts +6 -0
  49. package/dist/r/src/styles/theme.css +5 -0
  50. package/package.json +1 -1
  51. package/src/components/composites/action-button/action-button.tsx +2 -1
  52. package/src/components/composites/appearance-editor/appearance-choices.tsx +35 -20
  53. package/src/components/composites/appearance-editor/appearance-rows.tsx +6 -2
  54. package/src/components/layout/side-rail/side-rail.tsx +7 -3
  55. package/src/components/layout/top-bar/top-bar.tsx +13 -2
  56. package/src/components/ui/dropdown-menu/dropdown-menu.tsx +10 -2
  57. package/src/components/ui/kbd/kbd.tsx +0 -2
  58. package/src/components/ui/status-chip/status-chip.tsx +1 -3
  59. package/src/components/ui/tooltip/tooltip.tsx +7 -7
  60. package/src/lib/overlay.ts +9 -0
  61. package/src/lib/tokens.ts +6 -0
  62. package/src/styles/theme.css +5 -0
@@ -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.98.0",
3
+ "version": "0.99.1",
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()}>
@@ -83,6 +83,11 @@ export function ModeChoices(props: AppearanceEditorProps) {
83
83
  }
84
84
 
85
85
  export function AccentChoices(props: AppearanceEditorProps) {
86
+ // The resolved appearance decides which side of a preset pair — and which
87
+ // theme accent — the swatches paint, so the row previews each choice as the
88
+ // user will see it.
89
+ const resolvedAppearance = () =>
90
+ props.draft.mode === 'system' ? props.resolvedAppearance : props.draft.mode
86
91
  const custom = () =>
87
92
  props.draft.accent !== 'theme' &&
88
93
  !props.accentOptions.some((option) => option.id === props.draft.accent)
@@ -93,9 +98,9 @@ export function AccentChoices(props: AppearanceEditorProps) {
93
98
  disabled={props.saving}
94
99
  orientation="horizontal"
95
100
  aria-label="Accent"
96
- // Two chips in a column beside a 3x2 swatch grid: one line of six
97
- // swatches plus chips overflows the settings sidebar's control column,
98
- // and the column keeps both rows of the pair balanced (2 x 28px + gap).
101
+ // One chip (custom) beside a capped swatch grid: a chip-per-state column
102
+ // beside a full-width swatch line overflowed the settings sidebar's
103
+ // control column, so the pair stays a narrow chip plus a wrapped grid.
99
104
  class="flex items-center gap-3"
100
105
  onChange={(accent) =>
101
106
  props.onChange({
@@ -103,21 +108,29 @@ export function AccentChoices(props: AppearanceEditorProps) {
103
108
  })
104
109
  }
105
110
  >
106
- <div class="flex flex-col gap-2">
107
- <Radio.Item value="theme">
111
+ <div class="grid grid-cols-4 gap-2">
112
+ {/* The theme's own accent is the first swatch. Picking it is the same
113
+ act the old dedicated "Theme default" chip performed, without a
114
+ second control for the state the row starts in. */}
115
+ <Radio.Item
116
+ value="theme"
117
+ class="rounded-full focus-within:ring-3 focus-within:ring-ring/50"
118
+ >
108
119
  <Radio.ItemInput />
109
- <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
110
- Theme default
120
+ <Radio.ItemLabel class="block size-7 cursor-pointer rounded-full border p-0.5 data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
121
+ <span
122
+ class="block size-full rounded-full"
123
+ aria-hidden="true"
124
+ style={{
125
+ 'background-color':
126
+ resolvedAppearance() === 'light'
127
+ ? props.lightTheme.colors.accent
128
+ : props.darkTheme.colors.accent,
129
+ }}
130
+ />
131
+ <span class="sr-only">Theme default</span>
111
132
  </Radio.ItemLabel>
112
133
  </Radio.Item>
113
- <Radio.Item value="custom">
114
- <Radio.ItemInput />
115
- <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
116
- Custom
117
- </Radio.ItemLabel>
118
- </Radio.Item>
119
- </div>
120
- <div class="grid grid-cols-3 gap-2">
121
134
  <For each={props.accentOptions}>
122
135
  {(option) => (
123
136
  <Radio.Item
@@ -131,11 +144,7 @@ export function AccentChoices(props: AppearanceEditorProps) {
131
144
  aria-hidden="true"
132
145
  style={{
133
146
  'background-color':
134
- (props.draft.mode === 'system'
135
- ? props.resolvedAppearance
136
- : props.draft.mode) === 'light'
137
- ? option.light
138
- : option.dark,
147
+ resolvedAppearance() === 'light' ? option.light : option.dark,
139
148
  }}
140
149
  />
141
150
  <span class="sr-only">{option.label}</span>
@@ -144,6 +153,12 @@ export function AccentChoices(props: AppearanceEditorProps) {
144
153
  )}
145
154
  </For>
146
155
  </div>
156
+ <Radio.Item value="custom">
157
+ <Radio.ItemInput />
158
+ <Radio.ItemLabel class="flex h-7 cursor-pointer items-center rounded-md border px-2 text-xs whitespace-nowrap data-[checked]:border-primary data-[checked]:ring-1 data-[checked]:ring-primary">
159
+ Custom
160
+ </Radio.ItemLabel>
161
+ </Radio.Item>
147
162
  </Radio>
148
163
  )
149
164
  }
@@ -210,10 +210,14 @@ function ThemeSelectMenu(props: {
210
210
  </DropdownMenuTrigger>
211
211
  <DropdownMenuContent
212
212
  portalMount={portalMount()}
213
- // Sized to content (at least the trigger width, never wider than the
213
+ // A fixed height with its own scroll: an adaptive cap measured the
214
+ // host panel instead, and opening a long theme list scrolled the
215
+ // sidebar to reveal the menu. The content scrolls internally at the
216
+ // cap, so the panel around the trigger never moves. Width still
217
+ // follows content (at least the trigger width, never wider than the
214
218
  // popper allows) so long theme names render in full with padding for
215
219
  // the selection indicator instead of truncating at the anchor width.
216
- class="max-h-(--kb-popper-content-available-height) w-max min-w-(--kb-popper-anchor-width) max-w-(--kb-popper-content-available-width) overflow-x-hidden overflow-y-auto"
220
+ class="max-h-80 w-max min-w-(--kb-popper-anchor-width) max-w-(--kb-popper-content-available-width) overflow-x-hidden overflow-y-auto"
217
221
  onInteractOutside={(event) => {
218
222
  if (event.detail.originalEvent.type !== 'pointerdown') return
219
223
  const parentDialog = portalMount()?.closest<HTMLElement>('[role="dialog"]')
@@ -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`,
@@ -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>
@@ -153,7 +153,10 @@ export function DropdownMenuCheckboxItem(
153
153
  return (
154
154
  <KobalteDropdownMenu.CheckboxItem class={cn(menuItem, 'pe-8', local.class)} {...rest}>
155
155
  {local.children}
156
- <KobalteDropdownMenu.ItemIndicator class="absolute end-2 flex size-4 items-center justify-center">
156
+ <KobalteDropdownMenu.ItemIndicator
157
+ data-slot="dropdown-menu-indicator"
158
+ class="absolute end-2 flex size-4 items-center justify-center text-primary"
159
+ >
157
160
  <Check class="size-4" />
158
161
  </KobalteDropdownMenu.ItemIndicator>
159
162
  </KobalteDropdownMenu.CheckboxItem>
@@ -172,7 +175,12 @@ export function DropdownMenuRadioItem(props: ComponentProps<typeof KobalteDropdo
172
175
  return (
173
176
  <KobalteDropdownMenu.RadioItem class={cn(menuItem, 'pe-8', local.class)} {...rest}>
174
177
  {local.children}
175
- <KobalteDropdownMenu.ItemIndicator class="absolute end-2 flex size-4 items-center justify-center">
178
+ {/* The indicator reads in the accent, like every other selected state in
179
+ the system — a canvas-colored dot on a checked row reads as decoration. */}
180
+ <KobalteDropdownMenu.ItemIndicator
181
+ data-slot="dropdown-menu-indicator"
182
+ class="absolute end-2 flex size-4 items-center justify-center text-primary"
183
+ >
176
184
  <Circle class="size-2 fill-current" />
177
185
  </KobalteDropdownMenu.ItemIndicator>
178
186
  </KobalteDropdownMenu.RadioItem>
@@ -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