@exegia/corpora-ui 0.18.0 → 0.20.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 (78) hide show
  1. package/dist-lib/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.d.ts +1 -0
  2. package/dist-lib/components/blocks/nav/sidebar/ai-sidebar.d.ts +11 -2
  3. package/dist-lib/components/blocks/nav/sidebar/type.d.ts +83 -1
  4. package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +22 -31
  5. package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +5 -0
  6. package/dist-lib/components/blocks/scaffold/constants.d.ts +8 -0
  7. package/dist-lib/components/blocks/scaffold/index.d.ts +3 -1
  8. package/dist-lib/components/blocks/scaffold/panel-menu-button.d.ts +4 -3
  9. package/dist-lib/components/blocks/scaffold/scaffold-actions.d.ts +5 -4
  10. package/dist-lib/components/blocks/scaffold/scaffold-canvas.d.ts +2 -0
  11. package/dist-lib/components/blocks/scaffold/scaffold-panel.d.ts +1 -1
  12. package/dist-lib/components/blocks/scaffold/scaffold-tab.d.ts +14 -0
  13. package/dist-lib/components/blocks/scaffold/type.d.ts +58 -28
  14. package/dist-lib/components/blocks/scaffold/use-panel-visibility.d.ts +19 -0
  15. package/dist-lib/components/blocks/scaffold/utils.d.ts +12 -0
  16. package/dist-lib/components/composed/tree/__tests__/tree.test.d.ts +1 -0
  17. package/dist-lib/components/composed/tree/__tests__/use-tree.test.d.ts +1 -0
  18. package/dist-lib/components/composed/tree/constants.d.ts +35 -0
  19. package/dist-lib/components/composed/tree/index.d.ts +4 -0
  20. package/dist-lib/components/composed/tree/tree-context.d.ts +4 -0
  21. package/dist-lib/components/composed/tree/tree-node.d.ts +7 -0
  22. package/dist-lib/components/composed/tree/tree.d.ts +19 -0
  23. package/dist-lib/components/composed/tree/type.d.ts +192 -0
  24. package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +7 -0
  25. package/dist-lib/components/composed/tree/use-tree.d.ts +20 -0
  26. package/dist-lib/components/composed/tree/utils.d.ts +28 -0
  27. package/dist-lib/components/user-avatar/audio-wave.d.ts +2 -0
  28. package/dist-lib/components/user-avatar/component.d.ts +10 -0
  29. package/dist-lib/components/user-avatar/fallback.d.ts +5 -0
  30. package/dist-lib/components/user-avatar/index.d.ts +13 -0
  31. package/dist-lib/components/user-avatar/type.d.ts +26 -0
  32. package/dist-lib/components/user-avatar/utils.d.ts +2 -0
  33. package/dist-lib/index.d.ts +1 -0
  34. package/dist-lib/index.js +2309 -1285
  35. package/dist-lib/index.js.map +1 -1
  36. package/package.json +2 -1
  37. package/src/components/beste/piece/browser-frame.tsx +9 -6
  38. package/src/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.tsx +310 -0
  39. package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +90 -60
  40. package/src/components/blocks/nav/sidebar/type.ts +105 -1
  41. package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +293 -116
  42. package/src/components/blocks/nav/sidebar/utils.ts +28 -0
  43. package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +268 -26
  44. package/src/components/blocks/scaffold/constants.ts +11 -0
  45. package/src/components/blocks/scaffold/index.ts +4 -1
  46. package/src/components/blocks/scaffold/panel-menu-button.tsx +96 -115
  47. package/src/components/blocks/scaffold/scaffold-actions.tsx +41 -50
  48. package/src/components/blocks/scaffold/scaffold-canvas.tsx +48 -5
  49. package/src/components/blocks/scaffold/scaffold-inspector.tsx +27 -3
  50. package/src/components/blocks/scaffold/scaffold-main.tsx +1 -1
  51. package/src/components/blocks/scaffold/scaffold-panel.tsx +56 -30
  52. package/src/components/blocks/scaffold/scaffold-root.tsx +18 -3
  53. package/src/components/blocks/scaffold/scaffold-sidebar.tsx +1 -1
  54. package/src/components/blocks/scaffold/scaffold-tab.tsx +104 -0
  55. package/src/components/blocks/scaffold/type.ts +62 -33
  56. package/src/components/blocks/scaffold/use-panel-visibility.ts +180 -0
  57. package/src/components/blocks/scaffold/utils.ts +35 -0
  58. package/src/components/composed/tree/__tests__/tree.test.tsx +400 -0
  59. package/src/components/composed/tree/__tests__/use-tree.test.tsx +332 -0
  60. package/src/components/composed/tree/constants.ts +60 -0
  61. package/src/components/composed/tree/index.ts +13 -0
  62. package/src/components/composed/tree/tree-context.ts +13 -0
  63. package/src/components/composed/tree/tree-node.tsx +451 -0
  64. package/src/components/composed/tree/tree.tsx +278 -0
  65. package/src/components/composed/tree/type.ts +212 -0
  66. package/src/components/composed/tree/use-tree-dnd.ts +148 -0
  67. package/src/components/composed/tree/use-tree.ts +306 -0
  68. package/src/components/composed/tree/utils.ts +172 -0
  69. package/src/components/composed/user-avatar.tsx +4 -1
  70. package/src/components/docs/component-preview.tsx +11 -11
  71. package/src/components/user-avatar/audio-wave.tsx +22 -0
  72. package/src/components/user-avatar/component.tsx +53 -0
  73. package/src/components/user-avatar/fallback.tsx +26 -0
  74. package/src/components/user-avatar/index.ts +15 -0
  75. package/src/components/user-avatar/type.ts +35 -0
  76. package/src/components/user-avatar/utils.ts +8 -0
  77. package/src/index.css +211 -153
  78. package/src/index.ts +1 -0
@@ -0,0 +1,332 @@
1
+ import { describe, expect, mock, test } from "bun:test"
2
+ import { act, render, renderHook, screen } from "@testing-library/react"
3
+ import userEvent from "@testing-library/user-event"
4
+
5
+ import { Tree, useTree } from "../index"
6
+ import type { TreeController, TreeNode } from "../type"
7
+
8
+ const NAV: TreeNode[] = [
9
+ {
10
+ id: "research",
11
+ label: "Research",
12
+ children: [
13
+ { id: "search", label: "Search", href: "/search" },
14
+ {
15
+ id: "library",
16
+ label: "Library",
17
+ children: [{ id: "codices", label: "Codices", href: "/c" }],
18
+ },
19
+ ],
20
+ },
21
+ ]
22
+
23
+ const FILES: TreeNode[] = [
24
+ {
25
+ id: "src",
26
+ label: "src",
27
+ defaultOpen: true,
28
+ children: [
29
+ { id: "index-ts", label: "index.ts" },
30
+ { id: "utils-ts", label: "utils.ts" },
31
+ ],
32
+ },
33
+ { id: "readme", label: "README.md" },
34
+ ]
35
+
36
+ /** Branch exits run through AnimatePresence — let them settle before
37
+ * querying (happy-dom serves stale results while an exit is in flight). */
38
+ const settleExit = () => new Promise((resolve) => setTimeout(resolve, 400))
39
+
40
+ describe("useTree · expansion", () => {
41
+ test("expand, collapse and toggle drive a single node", () => {
42
+ const { result } = renderHook(() =>
43
+ useTree({ variant: "navigation", items: NAV })
44
+ )
45
+ expect(result.current.isExpanded("library")).toBe(false)
46
+
47
+ act(() => result.current.expand("library"))
48
+ expect(result.current.isExpanded("library")).toBe(true)
49
+ // Expanding twice is a no-op rather than a toggle.
50
+ act(() => result.current.expand("library"))
51
+ expect(result.current.isExpanded("library")).toBe(true)
52
+
53
+ act(() => result.current.toggleExpanded("library"))
54
+ expect(result.current.isExpanded("library")).toBe(false)
55
+ act(() => result.current.collapse("library"))
56
+ expect(result.current.isExpanded("library")).toBe(false)
57
+ })
58
+
59
+ test("expandAll opens every branch, collapseAll closes them", () => {
60
+ const { result } = renderHook(() =>
61
+ useTree({ variant: "navigation", items: NAV })
62
+ )
63
+ act(() => result.current.expandAll())
64
+ expect(result.current.isExpanded("research")).toBe(true)
65
+ expect(result.current.isExpanded("library")).toBe(true)
66
+
67
+ act(() => result.current.collapseAll())
68
+ expect([...result.current.expandedIds]).toEqual([])
69
+ })
70
+
71
+ test("expandAll counts empty folders in files, not empty leaves elsewhere", () => {
72
+ const items: TreeNode[] = [{ id: "empty", label: "empty", children: [] }]
73
+ const { result } = renderHook(() =>
74
+ useTree({ variant: "files", items })
75
+ )
76
+ act(() => result.current.expandAll())
77
+ expect(result.current.isExpanded("empty")).toBe(true)
78
+
79
+ const nav = renderHook(() => useTree({ variant: "navigation", items }))
80
+ act(() => nav.result.current.expandAll())
81
+ expect(nav.result.current.isExpanded("empty")).toBe(false)
82
+ })
83
+
84
+ test("reveal opens ancestors only", () => {
85
+ const { result } = renderHook(() =>
86
+ useTree({ variant: "navigation", items: NAV })
87
+ )
88
+ act(() => result.current.reveal("codices"))
89
+ expect(result.current.isExpanded("research")).toBe(true)
90
+ expect(result.current.isExpanded("library")).toBe(true)
91
+ // The revealed node itself is not opened.
92
+ expect(result.current.isExpanded("codices")).toBe(false)
93
+ })
94
+
95
+ test("sectionIds names the heading rows of a 3-level navigation tree", () => {
96
+ const { result } = renderHook(() =>
97
+ useTree({ variant: "navigation", items: NAV })
98
+ )
99
+ expect(result.current.sectioned).toBe(true)
100
+ expect(result.current.sectionIds).toEqual(["research"])
101
+ // Sections start open, so collapsing one is a real change.
102
+ act(() => result.current.collapse(result.current.sectionIds[0]!))
103
+ expect(result.current.isExpanded("research")).toBe(false)
104
+ })
105
+
106
+ test("onExpandedChange reports the new set", () => {
107
+ const onExpandedChange = mock(() => {})
108
+ const { result } = renderHook(() =>
109
+ useTree({
110
+ variant: "files",
111
+ items: FILES,
112
+ defaultExpandedIds: [],
113
+ onExpandedChange,
114
+ })
115
+ )
116
+ act(() => result.current.expand("src"))
117
+ expect(onExpandedChange).toHaveBeenCalledWith(["src"])
118
+ })
119
+ })
120
+
121
+ describe("useTree · rail", () => {
122
+ test("collapsed is hook-owned from defaultCollapsed", () => {
123
+ const onCollapsedChange = mock(() => {})
124
+ const { result } = renderHook(() =>
125
+ useTree({ variant: "sidebar", items: NAV, onCollapsedChange })
126
+ )
127
+ expect(result.current.collapsed).toBe(false)
128
+
129
+ act(() => result.current.toggleCollapsed())
130
+ expect(result.current.collapsed).toBe(true)
131
+ expect(onCollapsedChange).toHaveBeenCalledWith(true)
132
+
133
+ act(() => result.current.setCollapsed(false))
134
+ expect(result.current.collapsed).toBe(false)
135
+ })
136
+
137
+ test("a controlled collapsed prop wins and setCollapsed only reports", () => {
138
+ const onCollapsedChange = mock(() => {})
139
+ const { result } = renderHook(() =>
140
+ useTree({
141
+ variant: "sidebar",
142
+ items: NAV,
143
+ collapsed: true,
144
+ onCollapsedChange,
145
+ })
146
+ )
147
+ act(() => result.current.setCollapsed(false))
148
+ expect(result.current.collapsed).toBe(true)
149
+ expect(onCollapsedChange).toHaveBeenCalledWith(false)
150
+ })
151
+
152
+ test("only sidebar has a rail — other variants read as expanded", () => {
153
+ const { result } = renderHook(() =>
154
+ useTree({ variant: "files", items: FILES, defaultCollapsed: true })
155
+ )
156
+ expect(result.current.collapsed).toBe(false)
157
+ })
158
+ })
159
+
160
+ describe("useTree · selection", () => {
161
+ test("select runs onSelect then onNavigate and holds the active id", () => {
162
+ const order: string[] = []
163
+ const items: TreeNode[] = [
164
+ { id: "a", label: "Alpha", onSelect: () => order.push("onSelect") },
165
+ { id: "off", label: "Off", disabled: true },
166
+ ]
167
+ const { result } = renderHook(() =>
168
+ useTree({
169
+ variant: "navigation",
170
+ items,
171
+ onNavigate: () => order.push("onNavigate"),
172
+ })
173
+ )
174
+ act(() => result.current.select("a"))
175
+ expect(order).toEqual(["onSelect", "onNavigate"])
176
+ expect(result.current.activeId).toBe("a")
177
+
178
+ // Disabled and unknown nodes are inert.
179
+ act(() => result.current.select("off"))
180
+ act(() => result.current.select("nope"))
181
+ expect(result.current.activeId).toBe("a")
182
+ expect(order).toEqual(["onSelect", "onNavigate"])
183
+ })
184
+
185
+ test("a controlled activeId is never overwritten by select", () => {
186
+ const { result } = renderHook(() =>
187
+ useTree({ variant: "navigation", items: NAV, activeId: "search" })
188
+ )
189
+ act(() => result.current.select("codices"))
190
+ expect(result.current.activeId).toBe("search")
191
+ })
192
+
193
+ test("selecting a nested node reveals it on the next render", () => {
194
+ const { result } = renderHook(() =>
195
+ useTree({ variant: "navigation", items: NAV })
196
+ )
197
+ act(() => result.current.select("codices"))
198
+ expect(result.current.isExpanded("library")).toBe(true)
199
+ })
200
+ })
201
+
202
+ describe("useTree · rename", () => {
203
+ test("hook-owned data renames itself and leaves rename mode", () => {
204
+ const { result } = renderHook(() =>
205
+ useTree({ variant: "files", defaultItems: FILES })
206
+ )
207
+ expect(result.current.canRename).toBe(true)
208
+
209
+ act(() => result.current.startRename("readme"))
210
+ expect(result.current.renamingId).toBe("readme")
211
+
212
+ act(() => result.current.rename("readme", " GUIDE.md "))
213
+ expect(result.current.renamingId).toBeNull()
214
+ expect(result.current.getNode("readme")?.label).toBe("GUIDE.md")
215
+ })
216
+
217
+ test("blank and unchanged labels are dropped", () => {
218
+ const onRename = mock(() => {})
219
+ const { result } = renderHook(() =>
220
+ useTree({ variant: "files", defaultItems: FILES, onRename })
221
+ )
222
+ act(() => result.current.rename("readme", " "))
223
+ act(() => result.current.rename("readme", "README.md"))
224
+ expect(onRename).not.toHaveBeenCalled()
225
+ expect(result.current.getNode("readme")?.label).toBe("README.md")
226
+ })
227
+
228
+ test("controlled data only reports the rename", () => {
229
+ const onRename = mock(() => {})
230
+ const { result } = renderHook(() =>
231
+ useTree({ variant: "files", items: FILES, onRename })
232
+ )
233
+ act(() => result.current.rename("readme", "GUIDE.md"))
234
+ expect(onRename).toHaveBeenCalledWith("readme", "GUIDE.md")
235
+ expect(result.current.getNode("readme")?.label).toBe("README.md")
236
+ })
237
+
238
+ test("rename is off outside files and without a way to apply it", () => {
239
+ const { result } = renderHook(() =>
240
+ useTree({ variant: "navigation", defaultItems: NAV })
241
+ )
242
+ expect(result.current.canRename).toBe(false)
243
+ act(() => result.current.startRename("search"))
244
+ expect(result.current.renamingId).toBeNull()
245
+
246
+ const controlled = renderHook(() =>
247
+ useTree({ variant: "files", items: FILES })
248
+ )
249
+ expect(controlled.result.current.canRename).toBe(false)
250
+ })
251
+ })
252
+
253
+ describe("useTree · reorder", () => {
254
+ test("move reorders hook-owned data", () => {
255
+ const { result } = renderHook(() =>
256
+ useTree({ variant: "files", defaultItems: FILES })
257
+ )
258
+ expect(result.current.canMove).toBe(true)
259
+
260
+ act(() => result.current.move("readme", "src", 0))
261
+ expect(result.current.items).toHaveLength(1)
262
+ expect(result.current.getNode("src")?.children?.[0]?.id).toBe("readme")
263
+ })
264
+
265
+ test("controlled data with onItemsChange gets the next tree", () => {
266
+ const onItemsChange = mock((items: TreeNode[]) => void items)
267
+ const { result } = renderHook(() =>
268
+ useTree({ variant: "files", items: FILES, onItemsChange })
269
+ )
270
+ act(() => result.current.move("readme", null, 0))
271
+ expect(onItemsChange).toHaveBeenCalled()
272
+ const next = onItemsChange.mock.calls[0]![0]
273
+ expect(next[0]!.id).toBe("readme")
274
+ // The source array is never mutated.
275
+ expect(FILES[0]!.id).toBe("src")
276
+ })
277
+
278
+ test("move is inert outside files", () => {
279
+ const onMove = mock(() => {})
280
+ const { result } = renderHook(() =>
281
+ useTree({ variant: "navigation", items: NAV, onMove })
282
+ )
283
+ expect(result.current.canMove).toBe(false)
284
+ act(() => result.current.move("search", null, 0))
285
+ expect(onMove).not.toHaveBeenCalled()
286
+ })
287
+ })
288
+
289
+ describe("Tree · controller form", () => {
290
+ function Harness({
291
+ onReady,
292
+ }: {
293
+ onReady: (tree: TreeController) => void
294
+ }): React.ReactElement {
295
+ const tree = useTree({ variant: "files", defaultItems: FILES })
296
+ onReady(tree)
297
+ return <Tree tree={tree} />
298
+ }
299
+
300
+ test("renders from the controller and reacts to calls made outside it", async () => {
301
+ let tree!: TreeController
302
+ render(<Harness onReady={(next) => (tree = next)} />)
303
+
304
+ expect(screen.getByText("index.ts")).toBeDefined()
305
+ await act(async () => tree.collapseAll())
306
+ await settleExit()
307
+ expect(screen.queryByText("index.ts")).toBeNull()
308
+
309
+ await act(async () => tree.expand("src"))
310
+ expect(await screen.findByText("index.ts")).toBeDefined()
311
+ })
312
+
313
+ test("a row press routes through the controller's select", async () => {
314
+ const user = userEvent.setup()
315
+ let tree!: TreeController
316
+ render(<Harness onReady={(next) => (tree = next)} />)
317
+
318
+ await user.click(screen.getByRole("button", { name: "README.md" }))
319
+ expect(tree.activeId).toBe("readme")
320
+ })
321
+
322
+ test("startRename from outside opens the row's rename input", async () => {
323
+ let tree!: TreeController
324
+ render(<Harness onReady={(next) => (tree = next)} />)
325
+
326
+ await act(async () => tree.startRename("readme"))
327
+ const input = document.querySelector<HTMLInputElement>(
328
+ '[data-slot="tree-rename-input"]'
329
+ )
330
+ expect(input?.value).toBe("README.md")
331
+ })
332
+ })
@@ -0,0 +1,60 @@
1
+ import type { Transition, Variants } from "motion/react"
2
+
3
+ /** `--ease-smooth-out` as a motion/react easing tuple. */
4
+ export const TREE_EASE = [0.22, 1, 0.36, 1] as const
5
+
6
+ /** Branch open — `--duration-fast` territory (accordion open). */
7
+ export const TREE_OPEN_DURATION = 0.25
8
+
9
+ /** Branch close — quicker than open, per open/close asymmetry. */
10
+ export const TREE_CLOSE_DURATION = 0.15
11
+
12
+ /** Chevron flip and label fades. */
13
+ export const TREE_MICRO_DURATION = 0.15
14
+
15
+ /** Sidebar rail label reveal/hide when `collapsed` flips. */
16
+ export const TREE_COLLAPSE_DURATION = 0.2
17
+
18
+ /** The collapsed rail's width in px — an icon row plus its padding. Kept in
19
+ * sync with the `w-11` resting class the rail falls back to before its
20
+ * expanded width has been measured. */
21
+ export const RAIL_COLLAPSED_WIDTH = 44
22
+
23
+ /** Rail label text fade — starts as the width growth is finishing, so the
24
+ * label never reads as clipped mid-grow. */
25
+ export const TREE_LABEL_REVEAL_DELAY = 0.01
26
+
27
+ /** A branch's children group: grows open with a soft stagger, folds shut
28
+ * faster. Height rides layout ("auto"), paint fades. */
29
+ export const TREE_BRANCH_VARIANTS: Variants = {
30
+ closed: { height: 0, opacity: 0 },
31
+ open: {
32
+ height: "auto",
33
+ opacity: 1,
34
+ transition: {
35
+ duration: TREE_OPEN_DURATION,
36
+ ease: TREE_EASE,
37
+ staggerChildren: 0.03,
38
+ },
39
+ },
40
+ }
41
+
42
+ /** Branch close — a plain target, NOT the "closed" variant label: a
43
+ * labeled exit orchestrates every variant child and AnimatePresence then
44
+ * waits on the whole subtree before unmounting. */
45
+ export const TREE_BRANCH_EXIT = {
46
+ height: 0,
47
+ opacity: 0,
48
+ transition: { duration: TREE_CLOSE_DURATION, ease: TREE_EASE },
49
+ }
50
+
51
+ /** One row inside an opening branch — a short blurred rise. */
52
+ export const TREE_ROW_VARIANTS: Variants = {
53
+ closed: { opacity: 0, x: -4 },
54
+ open: { opacity: 1, x: 0 },
55
+ }
56
+
57
+ export const TREE_MICRO_TRANSITION: Transition = {
58
+ duration: TREE_MICRO_DURATION,
59
+ ease: TREE_EASE,
60
+ }
@@ -0,0 +1,13 @@
1
+ export { Tree } from "./tree"
2
+ export { useTree } from "./use-tree"
3
+ export { moveNode, renameNode } from "./utils"
4
+ export type {
5
+ TreeController,
6
+ TreeControllerProps,
7
+ TreeDataProps,
8
+ TreeDropPosition,
9
+ TreeNode,
10
+ TreeProps,
11
+ TreeVariant,
12
+ UseTreeOptions,
13
+ } from "./type"
@@ -0,0 +1,13 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+
5
+ import type { TreeContextValue } from "./type"
6
+
7
+ export const TreeContext = React.createContext<TreeContextValue | null>(null)
8
+
9
+ export function useTreeContext(): TreeContextValue {
10
+ const context = React.useContext(TreeContext)
11
+ if (!context) throw new Error("Tree rows must render inside <Tree>.")
12
+ return context
13
+ }