@adea-ai/ui 0.89.1 → 0.93.2

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 (102) hide show
  1. package/README.md +22 -5
  2. package/dist/NOTICE +19 -0
  3. package/dist/components/composites/list-row/index.d.ts +1 -0
  4. package/dist/components/composites/list-row/index.d.ts.map +1 -1
  5. package/dist/components/composites/list-row/index.js +2 -1
  6. package/dist/components/composites/list-row/list-row-control.d.ts +20 -0
  7. package/dist/components/composites/list-row/list-row-control.d.ts.map +1 -0
  8. package/dist/components/composites/list-row/list-row-control.js +96 -0
  9. package/dist/components/composites/list-row/list-row-control.js.map +1 -0
  10. package/dist/components/composites/list-row/list-row.d.ts +15 -35
  11. package/dist/components/composites/list-row/list-row.d.ts.map +1 -1
  12. package/dist/components/composites/list-row/list-row.js +88 -135
  13. package/dist/components/composites/list-row/list-row.js.map +1 -1
  14. package/dist/components/composites/tree/index.d.ts +4 -0
  15. package/dist/components/composites/tree/index.d.ts.map +1 -0
  16. package/dist/components/composites/tree/index.js +2 -0
  17. package/dist/components/composites/tree/tree-navigation.d.ts +46 -0
  18. package/dist/components/composites/tree/tree-navigation.d.ts.map +1 -0
  19. package/dist/components/composites/tree/tree-navigation.js +95 -0
  20. package/dist/components/composites/tree/tree-navigation.js.map +1 -0
  21. package/dist/components/composites/tree/tree.d.ts +64 -0
  22. package/dist/components/composites/tree/tree.d.ts.map +1 -0
  23. package/dist/components/composites/tree/tree.js +311 -0
  24. package/dist/components/composites/tree/tree.js.map +1 -0
  25. package/dist/components/conversation/message-composer.d.ts +18 -3
  26. package/dist/components/conversation/message-composer.d.ts.map +1 -1
  27. package/dist/components/conversation/message-composer.js +93 -27
  28. package/dist/components/conversation/message-composer.js.map +1 -1
  29. package/dist/components/layout/sidebar-nav/index.d.ts +1 -1
  30. package/dist/components/layout/sidebar-nav/index.d.ts.map +1 -1
  31. package/dist/components/layout/sidebar-nav/index.js +2 -2
  32. package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts +19 -1
  33. package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts.map +1 -1
  34. package/dist/components/layout/sidebar-nav/sidebar-nav.js +83 -17
  35. package/dist/components/layout/sidebar-nav/sidebar-nav.js.map +1 -1
  36. package/dist/components/layout/top-bar/top-bar.d.ts +6 -5
  37. package/dist/components/layout/top-bar/top-bar.d.ts.map +1 -1
  38. package/dist/components/layout/top-bar/top-bar.js +30 -7
  39. package/dist/components/layout/top-bar/top-bar.js.map +1 -1
  40. package/dist/components/ui/dialog/dialog.d.ts +2 -0
  41. package/dist/components/ui/dialog/dialog.d.ts.map +1 -1
  42. package/dist/components/ui/dialog/dialog.js +4 -3
  43. package/dist/components/ui/dialog/dialog.js.map +1 -1
  44. package/dist/components/ui/modal-dialog/modal-dialog.d.ts +5 -2
  45. package/dist/components/ui/modal-dialog/modal-dialog.d.ts.map +1 -1
  46. package/dist/components/ui/modal-dialog/modal-dialog.js +33 -24
  47. package/dist/components/ui/modal-dialog/modal-dialog.js.map +1 -1
  48. package/dist/components/ui/textarea/index.d.ts +1 -1
  49. package/dist/components/ui/textarea/index.d.ts.map +1 -1
  50. package/dist/components/ui/textarea/index.js +2 -2
  51. package/dist/components/ui/textarea/textarea.d.ts +15 -2
  52. package/dist/components/ui/textarea/textarea.d.ts.map +1 -1
  53. package/dist/components/ui/textarea/textarea.js +47 -3
  54. package/dist/components/ui/textarea/textarea.js.map +1 -1
  55. package/dist/components/ui/tooltip/tooltip.d.ts.map +1 -1
  56. package/dist/components/ui/tooltip/tooltip.js +239 -17
  57. package/dist/components/ui/tooltip/tooltip.js.map +1 -1
  58. package/dist/index.d.ts +1 -0
  59. package/dist/index.d.ts.map +1 -1
  60. package/dist/index.js +5 -3
  61. package/dist/lib/variants.js +2 -2
  62. package/dist/r/conversation.json +1 -0
  63. package/dist/r/list-row.json +5 -0
  64. package/dist/r/registry.json +41 -2
  65. package/dist/r/src/components/composites/list-row/index.ts +1 -0
  66. package/dist/r/src/components/composites/list-row/list-row-control.tsx +108 -0
  67. package/dist/r/src/components/composites/list-row/list-row.tsx +60 -127
  68. package/dist/r/src/components/composites/tree/index.ts +3 -0
  69. package/dist/r/src/components/composites/tree/tree-navigation.ts +114 -0
  70. package/dist/r/src/components/composites/tree/tree.tsx +405 -0
  71. package/dist/r/src/components/conversation/message-composer.tsx +103 -22
  72. package/dist/r/src/components/layout/sidebar-nav/index.ts +3 -0
  73. package/dist/r/src/components/layout/sidebar-nav/sidebar-nav.tsx +75 -6
  74. package/dist/r/src/components/layout/top-bar/top-bar.tsx +34 -11
  75. package/dist/r/src/components/ui/dialog/dialog.tsx +4 -2
  76. package/dist/r/src/components/ui/modal-dialog/modal-dialog.tsx +20 -4
  77. package/dist/r/src/components/ui/textarea/index.ts +1 -1
  78. package/dist/r/src/components/ui/textarea/textarea.tsx +52 -10
  79. package/dist/r/src/components/ui/tooltip/tooltip.tsx +305 -10
  80. package/dist/r/src/styles/base.css +40 -0
  81. package/dist/r/textarea.json +3 -1
  82. package/dist/r/top-bar.json +3 -1
  83. package/dist/r/tree.json +29 -0
  84. package/package.json +4 -1
  85. package/registry.json +41 -2
  86. package/src/components/composites/list-row/index.ts +1 -0
  87. package/src/components/composites/list-row/list-row-control.tsx +108 -0
  88. package/src/components/composites/list-row/list-row.tsx +60 -127
  89. package/src/components/composites/tree/index.ts +3 -0
  90. package/src/components/composites/tree/tree-navigation.ts +114 -0
  91. package/src/components/composites/tree/tree.tsx +405 -0
  92. package/src/components/conversation/message-composer.tsx +103 -22
  93. package/src/components/layout/sidebar-nav/index.ts +3 -0
  94. package/src/components/layout/sidebar-nav/sidebar-nav.tsx +75 -6
  95. package/src/components/layout/top-bar/top-bar.tsx +34 -11
  96. package/src/components/ui/dialog/dialog.tsx +4 -2
  97. package/src/components/ui/modal-dialog/modal-dialog.tsx +20 -4
  98. package/src/components/ui/textarea/index.ts +1 -1
  99. package/src/components/ui/textarea/textarea.tsx +52 -10
  100. package/src/components/ui/tooltip/tooltip.tsx +305 -10
  101. package/src/index.ts +1 -0
  102. package/src/styles/base.css +40 -0
@@ -1,4 +1,4 @@
1
- import { cva } from "class-variance-authority";
1
+ import { cva as cva$1 } from "class-variance-authority";
2
2
  //#region src/lib/variants.ts
3
3
  /**
4
4
  * The shared control-size ladder, in Tailwind height utilities.
@@ -44,6 +44,6 @@ var controlInteractive = "transition-colors ease-out outline-none focus-visible:
44
44
  /** The shared focus treatment for surfaces that are not focusable controls. */
45
45
  var surfaceInteractive = "transition-colors ease-out outline-none";
46
46
  //#endregion
47
- export { controlInteractive, controlSize, controlSizeIcon, controlSizes, cva, surfaceInteractive };
47
+ export { controlInteractive, controlSize, controlSizeIcon, controlSizes, cva$1 as cva, surfaceInteractive };
48
48
 
49
49
  //# sourceMappingURL=variants.js.map
@@ -8,6 +8,7 @@
8
8
  "lucide-solid@^1.48.0"
9
9
  ],
10
10
  "registryDependencies": [
11
+ "@adea-ai/ui/action-button",
11
12
  "@adea-ai/ui/button",
12
13
  "@adea-ai/ui/button-group",
13
14
  "@adea-ai/ui/code-block",
@@ -16,6 +16,11 @@
16
16
  "type": "registry:ui",
17
17
  "target": "components/composites/list-row/index.ts"
18
18
  },
19
+ {
20
+ "path": "src/components/composites/list-row/list-row-control.tsx",
21
+ "type": "registry:ui",
22
+ "target": "components/composites/list-row/list-row-control.tsx"
23
+ },
19
24
  {
20
25
  "path": "src/components/composites/list-row/list-row.tsx",
21
26
  "type": "registry:ui",
@@ -1374,7 +1374,9 @@
1374
1374
  "type": "registry:ui",
1375
1375
  "title": "Textarea",
1376
1376
  "description": "",
1377
- "dependencies": [],
1377
+ "dependencies": [
1378
+ "class-variance-authority@^0.7.1"
1379
+ ],
1378
1380
  "registryDependencies": [
1379
1381
  "lib"
1380
1382
  ],
@@ -1740,7 +1742,9 @@
1740
1742
  "type": "registry:ui",
1741
1743
  "title": "Top Bar",
1742
1744
  "description": "",
1743
- "dependencies": [],
1745
+ "dependencies": [
1746
+ "lucide-solid@^1.48.0"
1747
+ ],
1744
1748
  "registryDependencies": [
1745
1749
  "lib"
1746
1750
  ],
@@ -1946,6 +1950,11 @@
1946
1950
  "type": "registry:ui",
1947
1951
  "target": "components/composites/list-row/index.ts"
1948
1952
  },
1953
+ {
1954
+ "path": "src/components/composites/list-row/list-row-control.tsx",
1955
+ "type": "registry:ui",
1956
+ "target": "components/composites/list-row/list-row-control.tsx"
1957
+ },
1949
1958
  {
1950
1959
  "path": "src/components/composites/list-row/list-row.tsx",
1951
1960
  "type": "registry:ui",
@@ -2011,6 +2020,35 @@
2011
2020
  }
2012
2021
  ]
2013
2022
  },
2023
+ {
2024
+ "name": "tree",
2025
+ "type": "registry:ui",
2026
+ "title": "Tree",
2027
+ "description": "Keyboard-accessible tree rows with virtualization-aware focus handoff.",
2028
+ "dependencies": [
2029
+ "lucide-solid@^1.48.0"
2030
+ ],
2031
+ "registryDependencies": [
2032
+ "lib"
2033
+ ],
2034
+ "files": [
2035
+ {
2036
+ "path": "src/components/composites/tree/index.ts",
2037
+ "type": "registry:ui",
2038
+ "target": "components/composites/tree/index.ts"
2039
+ },
2040
+ {
2041
+ "path": "src/components/composites/tree/tree-navigation.ts",
2042
+ "type": "registry:ui",
2043
+ "target": "components/composites/tree/tree-navigation.ts"
2044
+ },
2045
+ {
2046
+ "path": "src/components/composites/tree/tree.tsx",
2047
+ "type": "registry:ui",
2048
+ "target": "components/composites/tree/tree.tsx"
2049
+ }
2050
+ ]
2051
+ },
2014
2052
  {
2015
2053
  "name": "update-dialog",
2016
2054
  "type": "registry:ui",
@@ -2072,6 +2110,7 @@
2072
2110
  "lucide-solid@^1.48.0"
2073
2111
  ],
2074
2112
  "registryDependencies": [
2113
+ "@adea-ai/ui/action-button",
2075
2114
  "@adea-ai/ui/button",
2076
2115
  "@adea-ai/ui/button-group",
2077
2116
  "@adea-ai/ui/code-block",
@@ -1 +1,2 @@
1
1
  export { ListGroup, ListRow, type ListRowProps } from './list-row'
2
+ export { ListRowControl, type ListRowControlProps } from './list-row-control'
@@ -0,0 +1,108 @@
1
+ import type { PolymorphicProps } from '@kobalte/core/polymorphic'
2
+ import { Polymorphic } from '@kobalte/core/polymorphic'
3
+ import type { ComponentProps, JSX, ValidComponent } from 'solid-js'
4
+ import { Show, splitProps } from 'solid-js'
5
+ import { cn } from '../../../lib/utils'
6
+
7
+ export type ListRowControlProps<T extends ValidComponent = 'div'> = PolymorphicProps<
8
+ T,
9
+ {
10
+ class?: string
11
+ /** Marks the row as current. Sets `aria-current` for a link row. */
12
+ selected?: boolean
13
+ /** Draw the row at the compact height. */
14
+ dense?: boolean
15
+ /** Leading slot: an avatar, an icon, a checkbox. */
16
+ leading?: JSX.Element
17
+ /** Trailing slot: a count, a menu trigger, a timestamp. */
18
+ trailing?: JSX.Element
19
+ /** A second line that wraps and grows the row to fit its content. */
20
+ description?: string
21
+ /** Native button type; defaults to `button` for button rows. */
22
+ type?: ComponentProps<'button'>['type']
23
+ }
24
+ >
25
+
26
+ type ListRowControlBaseProps<T extends ValidComponent = 'div'> = ListRowControlProps<T> & {
27
+ as?: T
28
+ }
29
+
30
+ /** A tree-shakeable ListRow renderer without optional tooltip behavior. */
31
+ export function ListRowControl<T extends ValidComponent = 'div'>(props: ListRowControlProps<T>) {
32
+ const [local, rest] = splitProps(props as ListRowControlBaseProps, [
33
+ 'as',
34
+ 'class',
35
+ 'selected',
36
+ 'dense',
37
+ 'leading',
38
+ 'trailing',
39
+ 'description',
40
+ 'type',
41
+ 'tabIndex',
42
+ 'children',
43
+ ])
44
+ const hasClickHandler = () => Boolean(rest.onClick || rest['on:click'])
45
+ const rowAs = () => (local.as ?? (hasClickHandler() ? 'button' : 'div')) as T
46
+
47
+ return (
48
+ <Polymorphic
49
+ as={rowAs()}
50
+ type={rowAs() === 'button' ? (local.type ?? 'button') : local.type}
51
+ tabIndex={local.tabIndex ?? (rowAs() === 'button' || rowAs() === 'a' ? 0 : undefined)}
52
+ aria-current={local.selected ? 'true' : undefined}
53
+ data-selected={local.selected ? '' : undefined}
54
+ data-description={local.description ? '' : undefined}
55
+ class={cn(
56
+ 'group/row list-row-description flex min-w-0 items-center gap-2.5 rounded-md px-2 text-sm',
57
+ 'transition-colors ease-out outline-none',
58
+ 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
59
+ {
60
+ 'h-row-sm': local.dense && !local.description,
61
+ 'h-row-md': !local.dense && !local.description,
62
+ },
63
+ {
64
+ 'bg-primary-subtle text-foreground': local.selected,
65
+ 'hover:bg-surface-hover': !local.selected,
66
+ },
67
+ local.class
68
+ )}
69
+ {...(rest as ComponentProps<'div'>)}
70
+ >
71
+ <Show when={local.leading}>
72
+ <span
73
+ data-slot="list-row-leading"
74
+ class="flex shrink-0 items-center justify-center [&_svg]:size-4"
75
+ >
76
+ {local.leading}
77
+ </span>
78
+ </Show>
79
+ <div data-slot="list-row-content" class="flex min-w-0 flex-1 items-center gap-2.5">
80
+ <span data-slot="list-row-label" class="flex min-w-0 flex-1 flex-col">
81
+ <span class="truncate">{local.children}</span>
82
+ <Show when={local.description}>
83
+ <span
84
+ data-slot="list-row-description"
85
+ class={cn(
86
+ 'text-xs break-words',
87
+ local.selected ? 'text-foreground' : 'text-muted-foreground'
88
+ )}
89
+ >
90
+ {local.description}
91
+ </span>
92
+ </Show>
93
+ </span>
94
+ <Show when={local.trailing}>
95
+ <span
96
+ data-slot="list-row-trailing"
97
+ class={cn(
98
+ 'ms-auto flex shrink-0 items-center gap-1',
99
+ local.selected ? 'text-foreground' : 'text-muted-foreground'
100
+ )}
101
+ >
102
+ {local.trailing}
103
+ </span>
104
+ </Show>
105
+ </div>
106
+ </Polymorphic>
107
+ )
108
+ }
@@ -1,161 +1,93 @@
1
1
  import type { PolymorphicProps } from '@kobalte/core/polymorphic'
2
- import { Polymorphic } from '@kobalte/core/polymorphic'
3
- import type { ComponentProps, JSX, ValidComponent } from 'solid-js'
4
- import { Show, splitProps } from 'solid-js'
2
+ import type { ComponentProps, ValidComponent } from 'solid-js'
3
+ import { createSignal, createUniqueId, Show, splitProps } from 'solid-js'
5
4
  import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
6
5
  import { cn } from '../../../lib/utils'
6
+ import { ListRowControl, type ListRowControlProps } from './list-row-control'
7
7
 
8
- /**
9
- * ListRow.
10
- *
11
- * One row in a list of like things: a project, a session, a file, a person. It
12
- * is the highest-frequency component in an application of this shape, and the
13
- * one where a half-pixel of drift is most visible — twenty rows of a sidebar
14
- * make any inconsistency in height or padding obvious.
15
- *
16
- * The structure is fixed at four slots — `leading`, `children`, `meta`,
17
- * `trailing` — so rows that show an avatar, an icon, a status dot, a timestamp
18
- * and an overflow menu all sit at the same height without the caller doing
19
- * arithmetic.
20
- *
21
- * A row is only interactive when it is given an `as` that can be activated
22
- * (an anchor, a button) or an `onClick`. Click handlers default to a native
23
- * button; a row without either remains a `div` with no hover treatment.
24
- */
25
8
  export type ListRowProps<T extends ValidComponent = 'div'> = PolymorphicProps<
26
9
  T,
27
- {
28
- class?: string
29
- /** Marks the row as current. Sets `aria-current` for a link row. */
30
- selected?: boolean
31
- /** Draw the row at the compact height. */
32
- dense?: boolean
33
- /** Leading slot: an avatar, an icon, a checkbox. */
34
- leading?: JSX.Element
35
- /** Trailing slot: a count, a menu trigger, a timestamp. */
36
- trailing?: JSX.Element
37
- /** A dimmed second line under the main content. */
38
- description?: string
10
+ ListRowControlProps<T> & {
39
11
  /** A short explanation shown on pointer hover and keyboard focus. */
40
12
  tooltip?: string
41
13
  /** Native button type; defaults to `button` for button rows. */
42
14
  type?: ComponentProps<'button'>['type']
15
+ /** Additional accessible description preserved on plain and rich rows. */
16
+ 'aria-describedby'?: string
43
17
  }
44
18
  >
45
19
 
46
- type ListRowBaseProps<T extends ValidComponent = 'div'> = ListRowProps<T> & {
47
- as?: T
48
- type?: ComponentProps<'button'>['type']
49
- }
50
-
51
- function ListRowBase<T extends ValidComponent = 'div'>(props: ListRowBaseProps<T>) {
52
- const [local, rest] = splitProps(props as ListRowProps, [
53
- 'as',
54
- 'class',
55
- 'selected',
56
- 'dense',
57
- 'leading',
58
- 'trailing',
59
- 'description',
60
- 'tooltip',
61
- 'type',
62
- 'tabIndex',
63
- 'children',
64
- ])
65
-
66
- return (
67
- <Polymorphic
68
- as={(local.as ?? 'div') as T}
69
- type={local.as === 'button' ? (local.type ?? 'button') : local.type}
70
- tabIndex={local.tabIndex ?? (local.as === 'button' || local.as === 'a' ? 0 : undefined)}
71
- aria-current={local.selected ? 'true' : undefined}
72
- data-selected={local.selected ? '' : undefined}
73
- class={cn(
74
- 'group/row flex min-w-0 items-center gap-2.5 rounded-md px-2 text-sm',
75
- 'transition-colors ease-out outline-none',
76
- 'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
77
- {
78
- 'h-row-sm': local.dense && !local.description,
79
- 'h-row-md': !local.dense && !local.description,
80
- 'min-h-row-sm py-1': local.dense && local.description,
81
- 'min-h-row-md py-1.5': !local.dense && local.description,
82
- },
83
- {
84
- 'bg-primary-subtle text-foreground': local.selected,
85
- 'hover:bg-surface-hover': !local.selected,
86
- },
87
- local.class
88
- )}
89
- {...(rest as ComponentProps<'div'>)}
90
- >
91
- <Show when={local.leading}>
92
- <span class="flex size-4 shrink-0 items-center justify-center [&_svg]:size-4">
93
- {local.leading}
94
- </span>
95
- </Show>
96
- <span class="flex min-w-0 flex-1 flex-col">
97
- <span class="truncate">{local.children}</span>
98
- <Show when={local.description}>
99
- <span
100
- data-slot="list-row-description"
101
- class={cn(
102
- 'truncate text-xs',
103
- local.selected ? 'text-foreground' : 'text-muted-foreground'
104
- )}
105
- >
106
- {local.description}
107
- </span>
108
- </Show>
109
- </span>
110
- <Show when={local.trailing}>
111
- <span
112
- data-slot="list-row-trailing"
113
- class={cn(
114
- 'ms-auto flex shrink-0 items-center gap-1',
115
- local.selected ? 'text-foreground' : 'text-muted-foreground'
116
- )}
117
- >
118
- {local.trailing}
119
- </span>
120
- </Show>
121
- </Polymorphic>
122
- )
20
+ type ListRowTooltipTargetProps<T extends ValidComponent = 'div'> = Omit<ListRowProps<T>, 'as'> & {
21
+ rowAs?: T
22
+ externalDescribedBy?: string
23
+ tooltipId?: string
24
+ tooltipOpen?: () => boolean
123
25
  }
124
26
 
125
- type ListRowTooltipTargetProps<T extends ValidComponent = 'div'> = Omit<
126
- ListRowBaseProps<T>,
127
- 'as'
128
- > & { rowAs?: T }
129
-
130
27
  /** Adapts TooltipTrigger's polymorphic target to ListRow's semantic element. */
131
28
  function ListRowTooltipTarget<T extends ValidComponent = 'div'>(
132
29
  props: ListRowTooltipTargetProps<T>
133
30
  ) {
134
- const [local, rest] = splitProps(props as ListRowTooltipTargetProps, ['rowAs'])
135
- return <ListRowBase as={local.rowAs} {...(rest as ListRowBaseProps<T>)} />
31
+ const [local, rest] = splitProps(props as ListRowTooltipTargetProps, [
32
+ 'rowAs',
33
+ 'externalDescribedBy',
34
+ 'tooltipId',
35
+ 'tooltipOpen',
36
+ ])
37
+ const describedBy = () => {
38
+ const ids = [
39
+ rest['aria-describedby'],
40
+ local.externalDescribedBy,
41
+ local.tooltipOpen?.() ? local.tooltipId : undefined,
42
+ ]
43
+ .flatMap((value) => value?.split(/\s+/) ?? [])
44
+ .filter(Boolean)
45
+ return [...new Set(ids)].join(' ') || undefined
46
+ }
47
+
48
+ return (
49
+ <ListRowControl
50
+ as={local.rowAs}
51
+ {...(rest as unknown as ListRowControlProps<T>)}
52
+ aria-describedby={describedBy()}
53
+ />
54
+ )
136
55
  }
137
56
 
57
+ /**
58
+ * Rich row composition with optional hover and keyboard tooltip behavior.
59
+ * Import ListRowControl when the tooltip API is not needed and the consumer
60
+ * wants the plain renderer to tree-shake independently.
61
+ */
138
62
  export function ListRow<T extends ValidComponent = 'div'>(props: ListRowProps<T>) {
139
- const [local, rest] = splitProps(props as ListRowProps, ['as', 'tooltip'])
63
+ const [local, rest] = splitProps(props as ListRowProps<T>, ['as', 'tooltip', 'aria-describedby'])
64
+ const [tooltipOpen, setTooltipOpen] = createSignal(false)
65
+ const tooltipId = `list-row-tooltip-${createUniqueId()}`
140
66
  const hasClickHandler = () => Boolean(rest.onClick || rest['on:click'])
141
67
  const rowAs = () => (local.as ?? (hasClickHandler() ? 'button' : 'div')) as T
142
68
  const rowType = () => (rowAs() === 'button' ? (rest.type ?? 'button') : rest.type)
69
+ const rowProps = () =>
70
+ ({
71
+ ...rest,
72
+ type: rowType(),
73
+ 'aria-describedby': local['aria-describedby'],
74
+ }) as unknown as ListRowControlProps<T>
143
75
 
144
76
  return (
145
- <Show
146
- when={local.tooltip}
147
- fallback={<ListRowBase as={rowAs()} type={rowType()} {...(rest as ListRowBaseProps<T>)} />}
148
- >
149
- <Tooltip>
77
+ <Show when={local.tooltip} fallback={<ListRowControl as={rowAs()} {...rowProps()} />}>
78
+ <Tooltip open={tooltipOpen()} onOpenChange={setTooltipOpen}>
150
79
  <TooltipTrigger
151
80
  as={ListRowTooltipTarget}
152
81
  rowAs={rowAs()}
153
82
  type={rowType()}
83
+ externalDescribedBy={local['aria-describedby']}
84
+ tooltipId={tooltipId}
85
+ tooltipOpen={tooltipOpen}
154
86
  {...(rest as ListRowTooltipTargetProps<T>)}
155
87
  >
156
88
  {rest.children}
157
89
  </TooltipTrigger>
158
- <TooltipContent>{local.tooltip}</TooltipContent>
90
+ <TooltipContent id={tooltipId}>{local.tooltip}</TooltipContent>
159
91
  </Tooltip>
160
92
  </Show>
161
93
  )
@@ -163,12 +95,13 @@ export function ListRow<T extends ValidComponent = 'div'>(props: ListRowProps<T>
163
95
 
164
96
  /**
165
97
  * A group of rows with an optional heading and a scrolling body. The heading is
166
- * a plain label rather than a control: grouping is for reading, and a
167
- * disclosure on a list of five rows costs more attention than it saves.
98
+ * a plain label rather than a control: grouping is for reading, and a disclosure
99
+ * on a list of five rows costs more attention than it saves.
168
100
  */
169
- export function ListGroup(props: ComponentProps<'div'> & { label?: string; action?: JSX.Element }) {
101
+ export function ListGroup(
102
+ props: ComponentProps<'div'> & { label?: string; action?: import('solid-js').JSX.Element }
103
+ ) {
170
104
  const [local, rest] = splitProps(props, ['class', 'label', 'action', 'children'])
171
-
172
105
  return (
173
106
  <div data-slot="list-group" class={cn('flex flex-col gap-0.5', local.class)} {...rest}>
174
107
  <Show when={local.label}>
@@ -0,0 +1,3 @@
1
+ /** Keyboard-accessible tree rows with virtualization-aware focus handoff. */
2
+ export { Tree, TreeRow, type TreeProps, type TreeRowProps } from './tree'
3
+ export type { TreeItemDescriptor } from './tree-navigation'
@@ -0,0 +1,114 @@
1
+ /** A visible tree row's host-owned projection metadata. */
2
+ export type TreeItemDescriptor = {
3
+ /** Stable identity shared with the host's domain item. */
4
+ id: string
5
+ /** Stable parent identity, or null for a root item. */
6
+ parentId: string | null
7
+ /** One-based depth exposed to assistive technology. */
8
+ level: number
9
+ /** Whether this item can be expanded. */
10
+ expandable: boolean
11
+ /** Whether this item is currently expanded. */
12
+ expanded: boolean
13
+ }
14
+
15
+ export type TreeNavigationIndex = {
16
+ ids: readonly string[]
17
+ byId: ReadonlyMap<string, TreeItemDescriptor>
18
+ indexById: ReadonlyMap<string, number>
19
+ firstChildByParent: ReadonlyMap<string, string>
20
+ positionById: ReadonlyMap<string, { position: number; setSize: number }>
21
+ }
22
+
23
+ export type TreeNavigationAction =
24
+ | { type: 'focus'; id: string }
25
+ | { type: 'expand'; id: string; expanded: boolean }
26
+ | { type: 'activate'; id: string }
27
+ | { type: 'select'; id: string }
28
+
29
+ /**
30
+ * Indexes the complete expanded projection once per host update. The DOM may
31
+ * contain only a window of these rows; this index keeps keyboard movement and
32
+ * aria-setsize independent of the mounted window.
33
+ */
34
+ export function indexTreeItems(items: readonly TreeItemDescriptor[]): TreeNavigationIndex {
35
+ const byId = new Map<string, TreeItemDescriptor>()
36
+ const indexById = new Map<string, number>()
37
+ const firstChildByParent = new Map<string, string>()
38
+ const siblingsByParent = new Map<string | null, string[]>()
39
+
40
+ items.forEach((item, index) => {
41
+ if (byId.has(item.id)) {
42
+ throw new Error(`[adea-ui]: Tree visibleItems contains duplicate id "${item.id}".`)
43
+ }
44
+ byId.set(item.id, item)
45
+ indexById.set(item.id, index)
46
+
47
+ const siblings = siblingsByParent.get(item.parentId) ?? []
48
+ siblings.push(item.id)
49
+ siblingsByParent.set(item.parentId, siblings)
50
+
51
+ if (item.parentId !== null && !firstChildByParent.has(item.parentId)) {
52
+ firstChildByParent.set(item.parentId, item.id)
53
+ }
54
+ })
55
+
56
+ const positionById = new Map<string, { position: number; setSize: number }>()
57
+ for (const siblings of siblingsByParent.values()) {
58
+ siblings.forEach((id, index) => {
59
+ positionById.set(id, { position: index + 1, setSize: siblings.length })
60
+ })
61
+ }
62
+
63
+ return {
64
+ ids: items.map((item) => item.id),
65
+ byId,
66
+ indexById,
67
+ firstChildByParent,
68
+ positionById,
69
+ }
70
+ }
71
+
72
+ /** Resolves one keyboard event without owning tree data or host actions. */
73
+ export function treeNavigationAction(
74
+ index: TreeNavigationIndex,
75
+ activeId: string | null,
76
+ key: string
77
+ ): TreeNavigationAction | null {
78
+ const { ids, byId, indexById, firstChildByParent } = index
79
+ if (ids.length === 0) return null
80
+
81
+ const activeIndex = activeId === null ? undefined : indexById.get(activeId)
82
+ const active = activeId === null ? undefined : byId.get(activeId)
83
+
84
+ if (key === 'ArrowDown' || key === 'ArrowUp') {
85
+ const step = key === 'ArrowDown' ? 1 : -1
86
+ const start = activeIndex ?? (step > 0 ? -1 : 0)
87
+ const nextIndex = (start + step + ids.length) % ids.length
88
+ const id = ids[nextIndex]
89
+ return id === undefined ? null : { type: 'focus', id }
90
+ }
91
+
92
+ if (key === 'Home') return { type: 'focus', id: ids[0]! }
93
+ if (key === 'End') return { type: 'focus', id: ids[ids.length - 1]! }
94
+ if (!active) return null
95
+
96
+ if (key === 'ArrowRight' && active.expandable) {
97
+ if (!active.expanded) return { type: 'expand', id: active.id, expanded: true }
98
+ const childId = firstChildByParent.get(active.id)
99
+ return childId ? { type: 'focus', id: childId } : null
100
+ }
101
+
102
+ if (key === 'ArrowLeft') {
103
+ if (active.expandable && active.expanded) {
104
+ return { type: 'expand', id: active.id, expanded: false }
105
+ }
106
+ if (active.parentId !== null && byId.has(active.parentId)) {
107
+ return { type: 'focus', id: active.parentId }
108
+ }
109
+ }
110
+
111
+ if (key === 'Enter') return { type: 'activate', id: active.id }
112
+ if (key === ' ' || key === 'Spacebar') return { type: 'select', id: active.id }
113
+ return null
114
+ }