@exegia/corpora-ui 0.27.0 → 0.29.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.
- package/dist-lib/components/composed/tree/constants.d.ts +0 -4
- package/dist-lib/index.js +18 -38
- package/dist-lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/blocks/profile/__tests__/profile-card-block.test.tsx +4 -2
- package/src/components/blocks/profile/profile-card-block.tsx +11 -11
- package/src/components/composed/tree/CLAUDE.md +23 -10
- package/src/components/composed/tree/__tests__/tree.test.tsx +18 -21
- package/src/components/composed/tree/constants.ts +0 -5
- package/src/components/composed/tree/tree-node.tsx +7 -1
- package/src/components/composed/tree/tree.tsx +9 -62
package/package.json
CHANGED
|
@@ -151,9 +151,11 @@ describe("ProfileCardBlock", () => {
|
|
|
151
151
|
const identity = trigger.querySelector('[data-slot="profile-card-identity"]');
|
|
152
152
|
expect(identity?.getAttribute("aria-hidden")).toBe("true");
|
|
153
153
|
expect(identity?.textContent).toContain("Jenny Hamilton");
|
|
154
|
-
//
|
|
154
|
+
// Full-width h-10 row, like the tree rail's tiles: avatar centred on
|
|
155
|
+
// the rail's own centre line, no inset.
|
|
155
156
|
expect(trigger.className).toContain("justify-center");
|
|
156
|
-
expect(trigger.className).toContain("
|
|
157
|
+
expect(trigger.className).toContain("h-10");
|
|
158
|
+
expect(trigger.className).toContain("w-full");
|
|
157
159
|
expect(trigger.className).toContain("p-0");
|
|
158
160
|
|
|
159
161
|
rerender(<ProfileCardBlock user={USER} variant="expanded" />);
|
|
@@ -233,18 +233,18 @@ export function ProfileCardBlock({
|
|
|
233
233
|
// Quiet at rest — the background only surfaces on hover or
|
|
234
234
|
// while the menu is open.
|
|
235
235
|
"h-auto w-full justify-between py-1.5 pl-1.5 data-popup-open:bg-accent sm:h-auto",
|
|
236
|
-
// Folded: a
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
236
|
+
// Folded: a full-width h-10 row matching the tree rail's tiles,
|
|
237
|
+
// avatar centred — a fixed-width tile drifted off the rail's
|
|
238
|
+
// centre line whenever the collapsed panel was wider than it.
|
|
239
|
+
// The label spans below fold to zero width rather than
|
|
240
|
+
// unmounting so the change animates. Button's own gap-2 stays:
|
|
241
|
+
// its always-mounted loading slot carries a -ms-2 that cancels
|
|
242
|
+
// it, so zeroing the gap would pull the avatar 8px left; the
|
|
243
|
+
// folded chevron cancels its own gap instead (see below).
|
|
244
|
+
// `sm:h-10` because the base carries an `sm:h-auto` that an
|
|
245
|
+
// unprefixed height would not beat.
|
|
246
246
|
collapsed &&
|
|
247
|
-
"-
|
|
247
|
+
"h-10 w-full shrink-0 justify-center rounded-xl p-0 sm:h-10",
|
|
248
248
|
className
|
|
249
249
|
)}
|
|
250
250
|
data-collapsed={collapsed ? "" : undefined}
|
|
@@ -15,9 +15,9 @@ first; this file only holds what is specific to the tree.
|
|
|
15
15
|
| `use-tree-state.ts` | `useTreeState(id)` (reads, re-renders on any change) and `useTreeActions(id)` (writes only, never re-renders). |
|
|
16
16
|
| `use-tree-dnd.ts` | Stable drag handlers for `files`; they read `draggedId`/`dropTarget` out of the store at call time. |
|
|
17
17
|
| `tree-context.ts` | Context value = `{ treeId, renderTrailing, dnd }` only. Never the controller. |
|
|
18
|
-
| `tree.tsx` | `Tree` (props form → `UncontrolledTree`, controller form → `TreeView`),
|
|
18
|
+
| `tree.tsx` | `Tree` (props form → `UncontrolledTree`, controller form → `TreeView`), roving keyboard nav. |
|
|
19
19
|
| `tree-node.tsx` | `TreeRow` (memoized). Per-node atom subscriptions, row kind resolution, rename input, tooltip, toc overlay toggle, trailing slot, branch animation. |
|
|
20
|
-
| `constants.ts` | Durations, easings,
|
|
20
|
+
| `constants.ts` | Durations, easings, motion variants. |
|
|
21
21
|
| `utils.ts` | Pure tree helpers (`findNode`, `moveNode`, `renameNode`, ancestor walks). |
|
|
22
22
|
| `index.ts` | Barrel. Public atoms are listed explicitly — never `export *`. |
|
|
23
23
|
| `__tests__/` | `tree.test.tsx` (rendering per variant), `use-tree.test.tsx` (controller/hook), `tree-atom.test.tsx` (store + memo guard). |
|
|
@@ -43,10 +43,10 @@ first; this file only holds what is specific to the tree.
|
|
|
43
43
|
`rowInteractionProps`, spread onto the Button; Base UI's `mergeProps` lets
|
|
44
44
|
them override Button's own `data-slot="button"`. `sound` is forwarded to the
|
|
45
45
|
Button so its press/release cues follow the tree's `sound` prop.
|
|
46
|
-
- Collapsed sidebar rows are `h-10! rounded-xl! justify-center px-0` tiles
|
|
47
|
-
the rail
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
- Collapsed sidebar rows are `h-10! rounded-xl! justify-center px-0` tiles
|
|
47
|
+
spanning the **full rail width** — the rail itself stays `w-full` in both
|
|
48
|
+
states; its fold is the container's width change (an icon-collapsed panel)
|
|
49
|
+
plus the labels folding. Folded icons step up to `size-5 text-foreground`.
|
|
50
50
|
- Row kinds (`rowKindOf`): `link` (selectable, `aria-expanded` absent),
|
|
51
51
|
`toggle` (parent in navigation/files, `aria-expanded` on the row),
|
|
52
52
|
`section` (top level of a 3-level navigation tree). `toc` parents are
|
|
@@ -68,11 +68,11 @@ first; this file only holds what is specific to the tree.
|
|
|
68
68
|
3. **Keep the rail label mounted.** Unmounting the label on collapse killed
|
|
69
69
|
the fold animation; it animates `width/opacity/x/display` in place and the
|
|
70
70
|
test asserts `display: none`, not absence. (12b6a1f)
|
|
71
|
-
4. **Both rail width endpoints must be px
|
|
71
|
+
4. **Both rail width endpoints must be px** *(historical — the rail no
|
|
72
|
+
longer animates its own width; the container's fold carries it, see the
|
|
73
|
+
fix/tree-collapsed-behavior entry)*. Motion cannot tween a number
|
|
72
74
|
against `"100%"`; doing so pinned the inline width at the collapsed value
|
|
73
|
-
and the rail never reopened.
|
|
74
|
-
container, only while expanded, with the list's inline width cleared for
|
|
75
|
-
the sample. (a69072d, 2f7539f)
|
|
75
|
+
and the rail never reopened. (a69072d, 2f7539f)
|
|
76
76
|
5. **Tooltip is disabled, not unmounted, when the rail expands.** Swapping the
|
|
77
77
|
wrapper remounts the row and cuts the label fold short. Test asserts row
|
|
78
78
|
identity across collapse.
|
|
@@ -109,6 +109,19 @@ first; this file only holds what is specific to the tree.
|
|
|
109
109
|
|
|
110
110
|
## Change log
|
|
111
111
|
|
|
112
|
+
- **2026-08-22 — fix/tree-collapsed-behavior** (on `release/v0.28.0`)
|
|
113
|
+
- `tree.tsx`: the rail no longer narrows itself — the `w-10` collapsed
|
|
114
|
+
class, the width measurement (`railRef`/ResizeObserver) and the px width
|
|
115
|
+
tween are gone. The list is `w-full` in both states (root `motion.ul` →
|
|
116
|
+
plain `ul`); the nav wrapper dropped `justify-center` for `w-full`. The
|
|
117
|
+
fold is the hosting panel's width change plus the labels folding.
|
|
118
|
+
- `tree-node.tsx`: folded rail icons step up to `size-5` and take
|
|
119
|
+
`text-foreground` (rows are icon-only, so muted-at-rest read as disabled).
|
|
120
|
+
- `constants.ts`: `RAIL_COLLAPSED_WIDTH` removed (was internal-only).
|
|
121
|
+
- `blocks/profile/profile-card-block.tsx`: the folded card is a full-width
|
|
122
|
+
`h-10 w-full` row (was a `-mx-1 size-10` tile) so the avatar centres on
|
|
123
|
+
the rail's own centre line regardless of panel width.
|
|
124
|
+
|
|
112
125
|
- **2026-08-18 — fix/tree-collapsed** (on `release/v0.21.0`)
|
|
113
126
|
- `tree-node.tsx`: link/toggle rows render the shared `Button` (ghost /
|
|
114
127
|
secondary when active on sidebar); anchor branch removed; collapsed rail
|
|
@@ -3,7 +3,6 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
|
|
|
3
3
|
import userEvent from "@testing-library/user-event"
|
|
4
4
|
import { FileTextIcon, FolderIcon, SearchIcon } from "lucide-react"
|
|
5
5
|
|
|
6
|
-
import { RAIL_COLLAPSED_WIDTH } from "../constants"
|
|
7
6
|
import { Tree } from "../index"
|
|
8
7
|
import type { TreeNode } from "../type"
|
|
9
8
|
import { moveNode } from "../utils"
|
|
@@ -271,12 +270,10 @@ describe("Tree · toc", () => {
|
|
|
271
270
|
})
|
|
272
271
|
|
|
273
272
|
describe("Tree · sidebar", () => {
|
|
274
|
-
test("the rail
|
|
275
|
-
//
|
|
276
|
-
// the
|
|
277
|
-
//
|
|
278
|
-
// px-against-"100%" motion target, and motion sets no inline styles
|
|
279
|
-
// under happy-dom. Only a real browser catches that one.
|
|
273
|
+
test("the rail spans its container in both states", async () => {
|
|
274
|
+
// The rail no longer narrows itself to a fixed px column — its fold is
|
|
275
|
+
// the container's width change (an icon-collapsed panel). Both states
|
|
276
|
+
// must keep w-full so rows span the rail edge to edge.
|
|
280
277
|
const { rerender } = render(<Tree items={RAIL} variant="sidebar" />)
|
|
281
278
|
const rail = () => document.querySelector('[data-slot="tree"]')
|
|
282
279
|
|
|
@@ -285,30 +282,30 @@ describe("Tree · sidebar", () => {
|
|
|
285
282
|
|
|
286
283
|
rerender(<Tree collapsed items={RAIL} variant="sidebar" />)
|
|
287
284
|
await settleExit()
|
|
288
|
-
expect(rail()?.className).toContain("w-
|
|
285
|
+
expect(rail()?.className).toContain("w-full")
|
|
286
|
+
expect(rail()?.className).not.toContain("w-10")
|
|
289
287
|
expect(rail()?.getAttribute("data-collapsed")).toBe("")
|
|
290
|
-
// Collapsed rows are
|
|
288
|
+
// Collapsed rows are full-width tiles with the icon centred, stepped up
|
|
289
|
+
// to size-5 and foreground colour — the icon is the whole row.
|
|
291
290
|
const row = screen.getByRole("button", { name: "Search" })
|
|
292
291
|
expect(row.className).toContain("h-10!")
|
|
293
292
|
expect(row.className).toContain("rounded-xl!")
|
|
294
293
|
expect(row.className).toContain("justify-center")
|
|
294
|
+
expect(row.className).toContain("w-full")
|
|
295
|
+
const icon = row.querySelector('[data-slot="tree-row-icon"]')
|
|
296
|
+
expect(icon?.className).toContain("size-5")
|
|
297
|
+
expect(icon?.className).toContain("text-foreground")
|
|
295
298
|
|
|
296
|
-
// Back open — the
|
|
299
|
+
// Back open — rows drop the tile treatment, icons step back down.
|
|
297
300
|
rerender(<Tree items={RAIL} variant="sidebar" />)
|
|
298
301
|
await settleExit()
|
|
299
302
|
expect(rail()?.className).toContain("w-full")
|
|
300
|
-
expect(rail()?.className).not.toContain("w-10")
|
|
301
303
|
expect(rail()?.getAttribute("data-collapsed")).toBeNull()
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
test("the collapsed px target matches the resting width class", () => {
|
|
308
|
-
// tree.tsx animates the rail to RAIL_COLLAPSED_WIDTH once measured, and
|
|
309
|
-
// falls back to the class before that. Tailwind's w-10 is 2.5rem = 40px;
|
|
310
|
-
// the two must agree or the rail jumps on its first collapse.
|
|
311
|
-
expect(RAIL_COLLAPSED_WIDTH).toBe(40)
|
|
304
|
+
const reopened = screen.getByRole("button", { name: "Search" })
|
|
305
|
+
expect(reopened.className).not.toContain("h-10!")
|
|
306
|
+
expect(
|
|
307
|
+
reopened.querySelector('[data-slot="tree-row-icon"]')?.className
|
|
308
|
+
).not.toContain("size-5")
|
|
312
309
|
})
|
|
313
310
|
|
|
314
311
|
test("collapsed shows icon-only rows that keep an accessible name", async () => {
|
|
@@ -15,11 +15,6 @@ export const TREE_MICRO_DURATION = 0.15
|
|
|
15
15
|
/** Sidebar rail label reveal/hide when `collapsed` flips. */
|
|
16
16
|
export const TREE_COLLAPSE_DURATION = 0.2
|
|
17
17
|
|
|
18
|
-
/** The collapsed rail's width in px — one `h-10 rounded-xl` icon row, edge
|
|
19
|
-
* to edge. Kept in sync with the `w-10` resting class the rail falls back to
|
|
20
|
-
* before its expanded width has been measured. */
|
|
21
|
-
export const RAIL_COLLAPSED_WIDTH = 40
|
|
22
|
-
|
|
23
18
|
/** Rail label text fade — starts as the width growth is finishing, so the
|
|
24
19
|
* label never reads as clipped mid-grow. */
|
|
25
20
|
export const TREE_LABEL_REVEAL_DELAY = 0.01
|
|
@@ -295,9 +295,15 @@ function TreeRowImpl({ node, depth }: TreeRowProps): React.ReactElement {
|
|
|
295
295
|
{kind !== "section" && node.icon && (
|
|
296
296
|
<span
|
|
297
297
|
className={cn(
|
|
298
|
-
"flex size-4 shrink-0 items-center justify-center [&>svg]:size-4",
|
|
298
|
+
"flex size-4 shrink-0 items-center justify-center transition-[width,height,color] duration-150 [&>svg]:size-4",
|
|
299
|
+
// Folded rail: the icon is the whole row, so it steps up a size
|
|
300
|
+
// and takes the foreground colour instead of muted.
|
|
301
|
+
variant === "sidebar" &&
|
|
302
|
+
collapsed &&
|
|
303
|
+
"size-5 text-foreground [&>svg]:size-5",
|
|
299
304
|
active && "text-accent-foreground"
|
|
300
305
|
)}
|
|
306
|
+
data-slot="tree-row-icon"
|
|
301
307
|
>
|
|
302
308
|
{node.icon}
|
|
303
309
|
</span>
|
|
@@ -13,14 +13,8 @@ import type {
|
|
|
13
13
|
TreeNode,
|
|
14
14
|
TreeProps,
|
|
15
15
|
} from "./type"
|
|
16
|
-
import {
|
|
17
|
-
RAIL_COLLAPSED_WIDTH,
|
|
18
|
-
TREE_COLLAPSE_DURATION,
|
|
19
|
-
TREE_EASE,
|
|
20
|
-
} from "./constants"
|
|
21
16
|
import { useTree } from "./use-tree"
|
|
22
17
|
import { useTreeDndHandlers } from "./use-tree-dnd"
|
|
23
|
-
import { motion, useReducedMotion } from "motion/react"
|
|
24
18
|
|
|
25
19
|
const DEFAULT_LABELS: Record<TreeController["variant"], string> = {
|
|
26
20
|
navigation: "Main",
|
|
@@ -109,43 +103,6 @@ function TreeView({
|
|
|
109
103
|
}: TreeViewProps): React.ReactElement {
|
|
110
104
|
const { variant, items, sectioned, collapsed } = tree
|
|
111
105
|
const rootRef = React.useRef<HTMLUListElement>(null)
|
|
112
|
-
const reduce = useReducedMotion()
|
|
113
|
-
|
|
114
|
-
// The rail animates between two px widths, so the expanded one has to be
|
|
115
|
-
// measured off the container. Watched rather than read once: the rail is
|
|
116
|
-
// as wide as whatever holds it, which can change on resize.
|
|
117
|
-
//
|
|
118
|
-
// A container with no width of its own sizes to the list — so it shrinks
|
|
119
|
-
// around the 40px rail once collapsed and follows the list mid-tween. Both
|
|
120
|
-
// would feed the list's own width back in as its target and pin it. So:
|
|
121
|
-
// no observing while collapsed (the last expanded width stands), and each
|
|
122
|
-
// sample is taken with the list's inline width cleared, so the classes'
|
|
123
|
-
// resting width — not the tween — is what the container is holding.
|
|
124
|
-
const railRef = React.useRef<HTMLElement>(null)
|
|
125
|
-
const [railWidth, setRailWidth] = React.useState<number | null>(null)
|
|
126
|
-
React.useLayoutEffect(() => {
|
|
127
|
-
const container = railRef.current
|
|
128
|
-
const list = rootRef.current
|
|
129
|
-
if (variant !== "sidebar" || collapsed || !container || !list) return
|
|
130
|
-
const measure = () => {
|
|
131
|
-
const inline = list.style.width
|
|
132
|
-
list.style.width = ""
|
|
133
|
-
const width = container.clientWidth
|
|
134
|
-
list.style.width = inline
|
|
135
|
-
setRailWidth(width)
|
|
136
|
-
}
|
|
137
|
-
measure()
|
|
138
|
-
const observer = new ResizeObserver(measure)
|
|
139
|
-
observer.observe(container)
|
|
140
|
-
return () => observer.disconnect()
|
|
141
|
-
}, [variant, collapsed])
|
|
142
|
-
|
|
143
|
-
// Until the measurement lands the className carries the width, so the
|
|
144
|
-
// rail is never wrong — just not animated on its very first frame.
|
|
145
|
-
const railTarget =
|
|
146
|
-
variant === "sidebar" && railWidth !== null
|
|
147
|
-
? { width: collapsed ? RAIL_COLLAPSED_WIDTH : railWidth }
|
|
148
|
-
: undefined
|
|
149
106
|
|
|
150
107
|
// Roving over the DOM rather than a parallel model: collapsed branches
|
|
151
108
|
// unmount, so a row query is exactly the visible set in order.
|
|
@@ -225,26 +182,17 @@ function TreeView({
|
|
|
225
182
|
|
|
226
183
|
const label = ariaLabel ?? DEFAULT_LABELS[variant]
|
|
227
184
|
let rendered = (
|
|
228
|
-
<
|
|
185
|
+
<ul
|
|
229
186
|
aria-label={label}
|
|
230
187
|
className={cn(
|
|
231
|
-
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
|
|
188
|
+
// The list always spans its container — collapsed included. The
|
|
189
|
+
// rail's fold is its container's width change (an icon-collapsed
|
|
190
|
+
// panel) plus the labels folding; rows keep the full width so their
|
|
191
|
+
// hit areas and hover surfaces span the rail edge to edge.
|
|
192
|
+
"flex h-full w-full min-w-0 flex-col",
|
|
193
|
+
collapsed && "gap-y-2!",
|
|
235
194
|
variant === "files" ? "gap-px" : sectioned ? "gap-3" : "gap-0.5"
|
|
236
195
|
)}
|
|
237
|
-
// Both endpoints must be px: motion cannot interpolate a number
|
|
238
|
-
// against "100%", and animating 40 -> "100%" pinned the inline width
|
|
239
|
-
// at 40px, so the rail collapsed once and never reopened. The
|
|
240
|
-
// expanded target is the rail's own measured width.
|
|
241
|
-
initial={false}
|
|
242
|
-
animate={railTarget}
|
|
243
|
-
transition={
|
|
244
|
-
reduce
|
|
245
|
-
? { duration: 0 }
|
|
246
|
-
: { duration: TREE_COLLAPSE_DURATION, ease: TREE_EASE }
|
|
247
|
-
}
|
|
248
196
|
data-collapsed={collapsed ? "" : undefined}
|
|
249
197
|
data-slot="tree"
|
|
250
198
|
data-variant={variant}
|
|
@@ -255,7 +203,7 @@ function TreeView({
|
|
|
255
203
|
{items.map((node) => (
|
|
256
204
|
<TreeRow depth={0} key={node.id} node={node} />
|
|
257
205
|
))}
|
|
258
|
-
</
|
|
206
|
+
</ul>
|
|
259
207
|
)
|
|
260
208
|
|
|
261
209
|
// The collapsed rail's rows carry tooltips — one provider groups their
|
|
@@ -274,9 +222,8 @@ function TreeView({
|
|
|
274
222
|
) : (
|
|
275
223
|
<nav
|
|
276
224
|
aria-label={label}
|
|
277
|
-
className={cn("flex h-full min-w-0
|
|
225
|
+
className={cn("flex h-full w-full min-w-0", className)}
|
|
278
226
|
data-slot="tree-root"
|
|
279
|
-
ref={railRef}
|
|
280
227
|
>
|
|
281
228
|
{rendered}
|
|
282
229
|
</nav>
|