@adea-ai/ui 0.109.1 → 0.110.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 (31) hide show
  1. package/README.md +14 -0
  2. package/dist/components/composites/account-menu/account-menu.js +1 -1
  3. package/dist/components/composites/action-button/action-button.js +1 -1
  4. package/dist/components/layout/split-layout/index.js +2 -2
  5. package/dist/components/layout/split-layout/model.d.ts +13 -0
  6. package/dist/components/layout/split-layout/model.d.ts.map +1 -1
  7. package/dist/components/layout/split-layout/model.js +55 -18
  8. package/dist/components/layout/split-layout/model.js.map +1 -1
  9. package/dist/components/layout/split-layout/split-layout.d.ts.map +1 -1
  10. package/dist/components/layout/split-layout/split-layout.js +5 -2
  11. package/dist/components/layout/split-layout/split-layout.js.map +1 -1
  12. package/dist/components/ui/dialog/dialog.d.ts.map +1 -1
  13. package/dist/components/ui/dialog/dialog.js +15 -9
  14. package/dist/components/ui/dialog/dialog.js.map +1 -1
  15. package/dist/components/ui/tooltip/tooltip.d.ts.map +1 -1
  16. package/dist/components/ui/tooltip/tooltip.js +2 -2
  17. package/dist/components/ui/tooltip/tooltip.js.map +1 -1
  18. package/dist/index.js +3 -3
  19. package/dist/r/dialog.json +1 -0
  20. package/dist/r/registry.json +2 -1
  21. package/dist/r/split-layout.json +1 -1
  22. package/dist/r/src/components/layout/split-layout/model.ts +62 -10
  23. package/dist/r/src/components/layout/split-layout/split-layout.tsx +4 -3
  24. package/dist/r/src/components/ui/dialog/dialog.tsx +17 -10
  25. package/dist/r/src/components/ui/tooltip/tooltip.tsx +6 -1
  26. package/package.json +1 -1
  27. package/registry.json +2 -1
  28. package/src/components/layout/split-layout/model.ts +62 -10
  29. package/src/components/layout/split-layout/split-layout.tsx +4 -3
  30. package/src/components/ui/dialog/dialog.tsx +17 -10
  31. package/src/components/ui/tooltip/tooltip.tsx +6 -1
@@ -41,6 +41,10 @@ export type SplitPaneInput<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readon
41
41
  splitId: string
42
42
  }>
43
43
 
44
+ export type BalancedSplitPaneInput<L extends SplitLayoutLeaf = SplitLayoutLeaf> = Readonly<
45
+ Pick<SplitPaneInput<L>, 'placement' | 'leaf' | 'splitId'>
46
+ >
47
+
44
48
  function replaceLeaf<L extends SplitLayoutLeaf>(
45
49
  node: SplitLayoutNode<L>,
46
50
  leafId: string,
@@ -86,14 +90,19 @@ function assertUniqueInput<L extends SplitLayoutLeaf>(
86
90
  if (typeof splitId !== 'string' || splitId.length === 0)
87
91
  throw new Error('invalid_state: split identity must be nonempty')
88
92
  const ids = new Set<string>()
93
+ const splitIds: string[] = []
89
94
  const visit = (node: SplitLayoutNode<L>) => {
90
95
  if (ids.has(node.id)) throw new Error('invalid_state: duplicate pane id')
91
96
  ids.add(node.id)
92
- if (node.kind === 'split') node.children.forEach(visit)
97
+ if (node.kind === 'split') {
98
+ splitIds.push(node.id)
99
+ node.children.forEach(visit)
100
+ }
93
101
  }
94
102
  visit(state.center)
95
103
  if (ids.has(leaf.id) || ids.has(splitId) || leaf.id === splitId)
96
104
  throw new Error('invalid_state: duplicate pane id')
105
+ return splitIds
97
106
  }
98
107
 
99
108
  /** Visual-tree validation only; applications still decode and scope persisted preferences. */
@@ -163,6 +172,51 @@ export function splitPane<L extends SplitLayoutLeaf>(
163
172
  return { ...state, center, focusedLeafId: input.leaf.id, closed: [] }
164
173
  }
165
174
 
175
+ /**
176
+ * Add a pane beside the selected leaf and reflow the visual tree into a
177
+ * balanced row-major grid. One or two leaves share one row; from three leaves
178
+ * onward, leaves fill a first row and then a second row, with no more than four
179
+ * columns in either. Each row gives its leaves equal width and the two rows
180
+ * equal height. Existing opaque leaf objects are preserved. Split IDs are
181
+ * recycled across rebuilt branches, whose axis and descendants can change;
182
+ * resize actions must resolve against the current tree. Ratios are recomputed.
183
+ * This is opt-in: callers
184
+ * that need an explicit axis and ratio should keep using `splitPane`.
185
+ */
186
+ export function splitPaneBalanced<L extends SplitLayoutLeaf>(
187
+ state: SplitLayoutState<L>,
188
+ targetLeafId: string,
189
+ input: BalancedSplitPaneInput<L>
190
+ ): SplitLayoutState<L> {
191
+ const leaves = [...listLeaves(state.center)]
192
+ const targetIndex = leaves.findIndex((leaf) => leaf.id === targetLeafId)
193
+ if (targetIndex < 0) return state
194
+ if (leaves.length >= MAX_LAYOUT_LEAVES)
195
+ throw new Error('limit_exceeded: center layout has eight leaves')
196
+ const splitIds = assertUniqueInput(state, input.leaf, input.splitId)
197
+
198
+ const insertionIndex = targetIndex + (input.placement === 'before' ? 0 : 1)
199
+ leaves.splice(insertionIndex, 0, input.leaf)
200
+
201
+ const build = (start: number, end: number, vertical = false): SplitLayoutNode<L> => {
202
+ if (end - start === 1) return leaves[start]!
203
+ const middle = start + Math.ceil((end - start) / 2)
204
+ // The new identity belongs to the lowest branch separating the selected
205
+ // pane from its new neighbour. Other branches reuse the existing ID pool.
206
+ const id = middle === targetIndex + 1 ? input.splitId : splitIds.shift()!
207
+ return {
208
+ kind: 'split',
209
+ id,
210
+ direction: vertical ? 'column' : 'row',
211
+ ratio: vertical ? 0.5 : (middle - start) / (end - start),
212
+ children: [build(start, middle), build(middle, end)],
213
+ }
214
+ }
215
+ const center = build(0, leaves.length, leaves.length > 2)
216
+ assertLayoutTree(center)
217
+ return { ...state, center, focusedLeafId: input.leaf.id, closed: [] }
218
+ }
219
+
166
220
  export function closePane<L extends SplitLayoutLeaf>(
167
221
  state: SplitLayoutState<L>,
168
222
  leafId: string,
@@ -287,21 +341,19 @@ export function resizeSplit<L extends SplitLayoutLeaf>(
287
341
  export function normalizeLayout<L extends SplitLayoutLeaf>(
288
342
  state: SplitLayoutState<L>
289
343
  ): SplitLayoutState<L> {
290
- const visit = (node: SplitLayoutNode<L>): { node: SplitLayoutNode<L>; changed: boolean } => {
291
- if (node.kind === 'leaf') return { node, changed: false }
344
+ const visit = (node: SplitLayoutNode<L>): SplitLayoutNode<L> => {
345
+ if (node.kind === 'leaf') return node
292
346
  const first = visit(node.children[0])
293
347
  const second = visit(node.children[1])
294
348
  const ratio = Number.isFinite(node.ratio)
295
349
  ? Math.min(MAX_SPLIT_RATIO, Math.max(MIN_SPLIT_RATIO, node.ratio))
296
350
  : (MIN_SPLIT_RATIO + MAX_SPLIT_RATIO) / 2
297
- const changed = first.changed || second.changed || ratio !== node.ratio
298
- return {
299
- node: changed ? { ...node, ratio, children: [first.node, second.node] } : node,
300
- changed,
301
- }
351
+ return first === node.children[0] && second === node.children[1] && ratio === node.ratio
352
+ ? node
353
+ : { ...node, ratio, children: [first, second] }
302
354
  }
303
- const result = visit(state.center)
304
- return result.changed ? { ...state, center: result.node } : state
355
+ const center = visit(state.center)
356
+ return center === state.center ? state : { ...state, center }
305
357
  }
306
358
 
307
359
  /** The leaf immediately after (`1`) or before (`-1`) the given leaf in reading order. */
@@ -11,7 +11,7 @@ import {
11
11
  type JSX,
12
12
  } from 'solid-js'
13
13
  import { X, GripVertical } from 'lucide-solid'
14
- import { Button } from '../../ui/button'
14
+ import { ActionButton } from '../../composites/action-button'
15
15
  import { cn } from '#lib/utils'
16
16
  import { computeLayoutFrames, type LayoutRect } from './geometry'
17
17
  import { paneDropIntent, type PaneDropIntent } from './drop'
@@ -420,14 +420,15 @@ export function SplitLayout<L extends SplitLayoutLeaf>(props: SplitLayoutProps<L
420
420
  </span>
421
421
  {actions}
422
422
  {props.onClose ? (
423
- <Button
423
+ <ActionButton
424
424
  variant="ghost"
425
425
  size="icon-xs"
426
426
  aria-label={`Close ${props.labelForLeaf(leaf())}`}
427
+ tooltip={`Close ${props.labelForLeaf(leaf())}.`}
427
428
  onClick={() => close(id)}
428
429
  >
429
430
  <X />
430
- </Button>
431
+ </ActionButton>
431
432
  ) : null}
432
433
  </header>
433
434
  <div class="min-h-0 min-w-0 flex-1 overflow-auto">{content}</div>
@@ -15,6 +15,7 @@ import {
15
15
  overlayTitle,
16
16
  } from '#lib/overlay'
17
17
  import { cn } from '#lib/utils'
18
+ import { Tooltip, TooltipContent, TooltipTrigger } from '../tooltip'
18
19
 
19
20
  type FocusCycle = {
20
21
  content: HTMLElement
@@ -290,16 +291,22 @@ export function DialogContent(props: DialogContentProps) {
290
291
  >
291
292
  {local.children}
292
293
  <Show when={local.closeButton !== false}>
293
- <KobalteDialog.CloseButton
294
- aria-label="Close"
295
- class={cn(
296
- 'absolute top-3.5 end-3.5 rounded-md p-1 text-muted-foreground',
297
- 'transition-colors ease-out hover:bg-surface-hover hover:text-foreground',
298
- 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle outline-none'
299
- )}
300
- >
301
- {local.closeButton ?? <X class="size-4" />}
302
- </KobalteDialog.CloseButton>
294
+ {/* The corner close is icon-only, so it carries the shared tooltip
295
+ treatment while its accessible name stays "Close". */}
296
+ <Tooltip>
297
+ <TooltipTrigger
298
+ as={KobalteDialog.CloseButton}
299
+ aria-label="Close"
300
+ class={cn(
301
+ 'absolute top-3.5 end-3.5 rounded-md p-1 text-muted-foreground',
302
+ 'transition-colors ease-out hover:bg-surface-hover hover:text-foreground',
303
+ 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle outline-none'
304
+ )}
305
+ >
306
+ {local.closeButton ?? <X class="size-4" />}
307
+ </TooltipTrigger>
308
+ <TooltipContent>Close dialog.</TooltipContent>
309
+ </Tooltip>
303
310
  </Show>
304
311
  </KobalteDialogContent>
305
312
  </div>
@@ -10,7 +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
+ import { tooltipTip, topLayerProps } from '#lib/overlay'
14
14
  import { cn } from '#lib/utils'
15
15
 
16
16
  /**
@@ -397,6 +397,11 @@ export function TooltipContent(props: TooltipContentProps) {
397
397
  invokeEventHandler(local.onPointerDownOutside, event)
398
398
  if (!event.defaultPrevented) interaction?.markCloseIntent()
399
399
  }}
400
+ // A tooltip opened from inside a modal dialog is part of that dialog's
401
+ // surface, so it carries the same top-layer exemption as a menu or a
402
+ // popover — without it the dialog's hide-outside observer aria-hides
403
+ // the tip while it is on screen.
404
+ {...topLayerProps}
400
405
  {...rest}
401
406
  >
402
407
  {local.children}