@exegia/corpora-ui 0.19.0 → 0.21.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/blocks/auth/__tests__/auth-state-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/auth/auth-flow-atom.d.ts +77 -0
- package/dist-lib/components/blocks/auth/auth-session-atom.d.ts +28 -0
- package/dist-lib/components/blocks/auth/auth-state-type.d.ts +71 -0
- package/dist-lib/components/blocks/auth/auth-state.d.ts +9 -0
- package/dist-lib/components/blocks/auth/use-auth-state.d.ts +52 -0
- package/dist-lib/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.d.ts +1 -0
- package/dist-lib/components/blocks/nav/sidebar/ai-sidebar-atom.d.ts +217 -0
- package/dist-lib/components/blocks/nav/sidebar/ai-sidebar.d.ts +11 -2
- package/dist-lib/components/blocks/nav/sidebar/index.d.ts +3 -1
- package/dist-lib/components/blocks/nav/sidebar/sidebar-context.d.ts +3 -0
- package/dist-lib/components/blocks/nav/sidebar/sidebar-row.d.ts +3 -1
- package/dist-lib/components/blocks/nav/sidebar/type.d.ts +191 -29
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar-state.d.ts +29 -0
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +27 -31
- package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +6 -1
- package/dist-lib/components/blocks/profile/index.d.ts +7 -0
- package/dist-lib/components/blocks/profile/profile-card-atom.d.ts +68 -0
- package/dist-lib/components/blocks/profile/profile-card-block.d.ts +28 -39
- package/dist-lib/components/blocks/profile/type.d.ts +80 -0
- package/dist-lib/components/blocks/profile/use-profile-card-state.d.ts +24 -0
- package/dist-lib/components/blocks/profile/use-profile-card.d.ts +34 -0
- package/dist-lib/components/blocks/scaffold/constants.d.ts +5 -0
- package/dist-lib/components/blocks/scaffold/scaffold-canvas.d.ts +2 -0
- package/dist-lib/components/blocks/scaffold/scaffold-panel.d.ts +1 -1
- package/dist-lib/components/blocks/scaffold/scaffold-tab.d.ts +6 -1
- package/dist-lib/components/blocks/scaffold/type.d.ts +28 -0
- package/dist-lib/components/blocks/scaffold/use-panel-visibility.d.ts +19 -0
- package/dist-lib/components/blocks/scaffold/utils.d.ts +6 -0
- package/dist-lib/components/blocks/shell/__tests__/shell-fit-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/shell/__tests__/shell-metrics.test.d.ts +1 -0
- package/dist-lib/components/blocks/shell/animated-panel-provider.d.ts +1 -1
- package/dist-lib/components/blocks/shell/animated-panel.d.ts +1 -1
- package/dist-lib/components/blocks/shell/index.d.ts +4 -0
- package/dist-lib/components/blocks/shell/shell-fit-atom.d.ts +68 -0
- package/dist-lib/components/blocks/shell/shell-metrics.d.ts +54 -0
- package/dist-lib/components/blocks/shell/type.d.ts +115 -3
- package/dist-lib/components/blocks/shell/use-shell-fit-state.d.ts +25 -0
- package/dist-lib/components/blocks/shell/use-shell-fit.d.ts +14 -0
- package/dist-lib/components/blocks/shell/use-shell-panels.d.ts +12 -1
- package/dist-lib/components/blocks/shell/utils.d.ts +32 -0
- package/dist-lib/components/composed/tree/__tests__/tree-atom.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/__tests__/tree.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/__tests__/use-tree.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/constants.d.ts +35 -0
- package/dist-lib/components/composed/tree/index.d.ts +6 -0
- package/dist-lib/components/composed/tree/tree-atom.d.ts +160 -0
- package/dist-lib/components/composed/tree/tree-context.d.ts +4 -0
- package/dist-lib/components/composed/tree/tree-node.d.ts +12 -0
- package/dist-lib/components/composed/tree/tree.d.ts +19 -0
- package/dist-lib/components/composed/tree/type.d.ts +290 -0
- package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +18 -0
- package/dist-lib/components/composed/tree/use-tree-state.d.ts +25 -0
- package/dist-lib/components/composed/tree/use-tree.d.ts +24 -0
- package/dist-lib/components/composed/tree/utils.d.ts +28 -0
- package/dist-lib/components/composed/user-avatar.d.ts +5 -28
- package/dist-lib/components/user-avatar/__tests__/user-avatar.test.d.ts +1 -0
- package/dist-lib/components/user-avatar/audio-wave.d.ts +2 -0
- package/dist-lib/components/user-avatar/component.d.ts +3 -0
- package/dist-lib/components/user-avatar/fallback.d.ts +5 -0
- package/dist-lib/components/user-avatar/index.d.ts +18 -0
- package/dist-lib/components/user-avatar/presence-badge.d.ts +16 -0
- package/dist-lib/components/user-avatar/type.d.ts +71 -0
- package/dist-lib/components/user-avatar/use-user-avatar-state.d.ts +26 -0
- package/dist-lib/components/user-avatar/use-user-avatar.d.ts +32 -0
- package/dist-lib/components/user-avatar/user-avatar-atom.d.ts +55 -0
- package/dist-lib/components/user-avatar/utils.d.ts +2 -0
- package/dist-lib/index.d.ts +6 -2
- package/dist-lib/index.js +3576 -1366
- package/dist-lib/index.js.map +1 -1
- package/dist-lib/state/exegia-provider.d.ts +51 -0
- package/dist-lib/state/index.d.ts +4 -0
- package/dist-lib/state/store.d.ts +17 -0
- package/package.json +16 -12
- package/src/components/beste/piece/browser-frame.tsx +9 -6
- package/src/components/blocks/auth/__tests__/auth-state-atom.test.tsx +247 -0
- package/src/components/blocks/auth/auth-flow-atom.ts +238 -0
- package/src/components/blocks/auth/auth-session-atom.ts +82 -0
- package/src/components/blocks/auth/auth-state-type.ts +97 -0
- package/src/components/blocks/auth/auth-state.ts +52 -0
- package/src/components/blocks/auth/use-auth-state.ts +127 -0
- package/src/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.tsx +325 -0
- package/src/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.tsx +310 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar-atom.ts +698 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +112 -76
- package/src/components/blocks/nav/sidebar/index.ts +53 -1
- package/src/components/blocks/nav/sidebar/sidebar-context.ts +15 -0
- package/src/components/blocks/nav/sidebar/sidebar-row.tsx +119 -50
- package/src/components/blocks/nav/sidebar/type.ts +228 -29
- package/src/components/blocks/nav/sidebar/use-ai-sidebar-state.ts +123 -0
- package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +455 -225
- package/src/components/blocks/nav/sidebar/utils.ts +29 -1
- package/src/components/blocks/profile/__tests__/profile-card-block.test.tsx +194 -0
- package/src/components/blocks/profile/index.ts +28 -0
- package/src/components/blocks/profile/profile-card-atom.ts +247 -0
- package/src/components/blocks/profile/profile-card-block.tsx +153 -74
- package/src/components/blocks/profile/type.ts +95 -0
- package/src/components/blocks/profile/use-profile-card-state.ts +67 -0
- package/src/components/blocks/profile/use-profile-card.ts +126 -0
- package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +160 -1
- package/src/components/blocks/scaffold/constants.ts +7 -0
- package/src/components/blocks/scaffold/panel-menu-button.tsx +3 -0
- package/src/components/blocks/scaffold/scaffold-canvas.tsx +48 -5
- package/src/components/blocks/scaffold/scaffold-inspector.tsx +3 -2
- package/src/components/blocks/scaffold/scaffold-main.tsx +1 -1
- package/src/components/blocks/scaffold/scaffold-panel.tsx +41 -5
- package/src/components/blocks/scaffold/scaffold-root.tsx +18 -3
- package/src/components/blocks/scaffold/scaffold-sidebar.tsx +1 -1
- package/src/components/blocks/scaffold/scaffold-tab.tsx +45 -7
- package/src/components/blocks/scaffold/type.ts +28 -0
- package/src/components/blocks/scaffold/use-panel-visibility.ts +180 -0
- package/src/components/blocks/scaffold/utils.ts +19 -0
- package/src/components/blocks/shell/__tests__/shell-fit-atom.test.tsx +360 -0
- package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +192 -3
- package/src/components/blocks/shell/__tests__/shell-metrics.test.ts +108 -0
- package/src/components/blocks/shell/animated-panel-inset.tsx +5 -1
- package/src/components/blocks/shell/animated-panel-provider.tsx +77 -11
- package/src/components/blocks/shell/animated-panel-trigger.tsx +4 -0
- package/src/components/blocks/shell/animated-panel.tsx +126 -29
- package/src/components/blocks/shell/index.ts +18 -0
- package/src/components/blocks/shell/shell-fit-atom.ts +243 -0
- package/src/components/blocks/shell/shell-layout.tsx +55 -53
- package/src/components/blocks/shell/shell-metrics.ts +79 -0
- package/src/components/blocks/shell/type.ts +130 -3
- package/src/components/blocks/shell/use-shell-fit-state.ts +49 -0
- package/src/components/blocks/shell/use-shell-fit.ts +135 -0
- package/src/components/blocks/shell/use-shell-panels.ts +57 -4
- package/src/components/blocks/shell/utils.ts +44 -4
- package/src/components/composed/tree/CLAUDE.md +132 -0
- package/src/components/composed/tree/__tests__/tree-atom.test.tsx +217 -0
- package/src/components/composed/tree/__tests__/tree.test.tsx +525 -0
- package/src/components/composed/tree/__tests__/use-tree.test.tsx +333 -0
- package/src/components/composed/tree/constants.ts +60 -0
- package/src/components/composed/tree/index.ts +51 -0
- package/src/components/composed/tree/tree-atom.ts +590 -0
- package/src/components/composed/tree/tree-context.ts +13 -0
- package/src/components/composed/tree/tree-node.tsx +490 -0
- package/src/components/composed/tree/tree.tsx +286 -0
- package/src/components/composed/tree/type.ts +322 -0
- package/src/components/composed/tree/use-tree-dnd.ts +179 -0
- package/src/components/composed/tree/use-tree-state.ts +105 -0
- package/src/components/composed/tree/use-tree.ts +307 -0
- package/src/components/composed/tree/utils.ts +172 -0
- package/src/components/composed/user-avatar.tsx +11 -96
- package/src/components/docs/component-preview.tsx +11 -11
- package/src/components/user-avatar/__tests__/user-avatar.test.tsx +245 -0
- package/src/components/user-avatar/audio-wave.tsx +22 -0
- package/src/components/user-avatar/component.tsx +152 -0
- package/src/components/user-avatar/fallback.tsx +26 -0
- package/src/components/user-avatar/index.ts +34 -0
- package/src/components/user-avatar/presence-badge.tsx +56 -0
- package/src/components/user-avatar/type.ts +85 -0
- package/src/components/user-avatar/use-user-avatar-state.ts +60 -0
- package/src/components/user-avatar/use-user-avatar.ts +144 -0
- package/src/components/user-avatar/user-avatar-atom.ts +198 -0
- package/src/components/user-avatar/utils.ts +8 -0
- package/src/index.css +211 -153
- package/src/index.ts +35 -2
- package/src/state/exegia-provider.tsx +79 -0
- package/src/state/index.ts +4 -0
- package/src/state/store.ts +19 -0
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from "bun:test"
|
|
2
|
-
import { render, screen, waitFor } from "@testing-library/react"
|
|
2
|
+
import { act, render, screen, waitFor } from "@testing-library/react"
|
|
3
3
|
import userEvent from "@testing-library/user-event"
|
|
4
4
|
import type { ReactNode } from "react"
|
|
5
5
|
|
|
6
6
|
import { Scaffold, useScaffold } from "../index"
|
|
7
7
|
import type { TScaffoldPanelChild } from "../type"
|
|
8
|
+
import { getPanelCapacity } from "../utils"
|
|
9
|
+
|
|
10
|
+
// The canvas measures itself through ResizeObserver; this stub hands the
|
|
11
|
+
// callback to the test so it can drive the width deterministically.
|
|
12
|
+
let observeCanvas: ResizeObserverCallback | undefined
|
|
13
|
+
globalThis.ResizeObserver = class {
|
|
14
|
+
constructor(callback: ResizeObserverCallback) {
|
|
15
|
+
observeCanvas = callback
|
|
16
|
+
}
|
|
17
|
+
observe() {}
|
|
18
|
+
unobserve() {}
|
|
19
|
+
disconnect() {}
|
|
20
|
+
} as unknown as typeof ResizeObserver
|
|
21
|
+
|
|
22
|
+
const resizeCanvas = (width: number) =>
|
|
23
|
+
act(() => {
|
|
24
|
+
observeCanvas?.(
|
|
25
|
+
[{ contentRect: { width } }] as unknown as ResizeObserverEntry[],
|
|
26
|
+
undefined as unknown as ResizeObserver
|
|
27
|
+
)
|
|
28
|
+
})
|
|
8
29
|
|
|
9
30
|
function Strip() {
|
|
10
31
|
return <span>Strip</span>
|
|
@@ -268,6 +289,16 @@ describe("Scaffold", () => {
|
|
|
268
289
|
expect(inspector().getAttribute("aria-hidden")).toBe("true")
|
|
269
290
|
})
|
|
270
291
|
|
|
292
|
+
test("capacity grants each panel 320px plus the gaps between them", () => {
|
|
293
|
+
expect(getPanelCapacity(null)).toBe(3)
|
|
294
|
+
expect(getPanelCapacity(976)).toBe(3) // 3×320 + 2×8
|
|
295
|
+
expect(getPanelCapacity(975)).toBe(2)
|
|
296
|
+
expect(getPanelCapacity(648)).toBe(2) // 2×320 + 8
|
|
297
|
+
expect(getPanelCapacity(647)).toBe(1)
|
|
298
|
+
expect(getPanelCapacity(100)).toBe(1) // never below one panel
|
|
299
|
+
expect(getPanelCapacity(5000)).toBe(3) // never above the cap
|
|
300
|
+
})
|
|
301
|
+
|
|
271
302
|
test("root manages inspector state itself when uncontrolled", () => {
|
|
272
303
|
render(
|
|
273
304
|
<Scaffold.Root defaultInspectorOpen>
|
|
@@ -284,3 +315,131 @@ describe("Scaffold", () => {
|
|
|
284
315
|
).toBeDefined()
|
|
285
316
|
})
|
|
286
317
|
})
|
|
318
|
+
|
|
319
|
+
const PANEL_IDS = ["one", "two", "three"] as const
|
|
320
|
+
|
|
321
|
+
function ResponsiveWorkspace() {
|
|
322
|
+
return (
|
|
323
|
+
<Scaffold.Root>
|
|
324
|
+
<Scaffold.Main>
|
|
325
|
+
<Scaffold.Actions>
|
|
326
|
+
{PANEL_IDS.map((id) => (
|
|
327
|
+
<Scaffold.Tab key={id} panelId={id}>
|
|
328
|
+
{`Tab ${id}`}
|
|
329
|
+
</Scaffold.Tab>
|
|
330
|
+
))}
|
|
331
|
+
</Scaffold.Actions>
|
|
332
|
+
<Scaffold.Canvas>
|
|
333
|
+
{PANEL_IDS.map((id) => (
|
|
334
|
+
<Scaffold.Panel key={id} id={id} name={`Panel ${id}`}>
|
|
335
|
+
<span>{`Body ${id}`}</span>
|
|
336
|
+
</Scaffold.Panel>
|
|
337
|
+
))}
|
|
338
|
+
</Scaffold.Canvas>
|
|
339
|
+
</Scaffold.Main>
|
|
340
|
+
</Scaffold.Root>
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const panel = (id: string) =>
|
|
345
|
+
screen.queryByRole("region", { name: `Panel ${id}` })
|
|
346
|
+
|
|
347
|
+
const tab = (id: string) => screen.getByRole("button", { name: `Tab ${id}` })
|
|
348
|
+
|
|
349
|
+
describe("Scaffold responsive panels", () => {
|
|
350
|
+
test("narrowing hides the oldest panel; widening restores it", async () => {
|
|
351
|
+
render(<ResponsiveWorkspace />)
|
|
352
|
+
|
|
353
|
+
resizeCanvas(976)
|
|
354
|
+
for (const id of PANEL_IDS) expect(panel(id)).not.toBeNull()
|
|
355
|
+
|
|
356
|
+
// Two panels' worth of room — the least-recently-activated hides.
|
|
357
|
+
resizeCanvas(700)
|
|
358
|
+
await settleExit()
|
|
359
|
+
expect(panel("one")).toBeNull()
|
|
360
|
+
expect(panel("two")).not.toBeNull()
|
|
361
|
+
expect(panel("three")).not.toBeNull()
|
|
362
|
+
expect(
|
|
363
|
+
tab("one")
|
|
364
|
+
.closest('[data-slot="scaffold-tab"]')
|
|
365
|
+
?.getAttribute("data-panel-hidden")
|
|
366
|
+
).toBe("")
|
|
367
|
+
expect(tab("one").getAttribute("aria-pressed")).toBe("false")
|
|
368
|
+
|
|
369
|
+
// Room returns — the auto-hidden panel comes back by itself.
|
|
370
|
+
resizeCanvas(1200)
|
|
371
|
+
expect(await screen.findByRole("region", { name: "Panel one" })).toBeDefined()
|
|
372
|
+
expect(tab("one").getAttribute("aria-pressed")).toBe("true")
|
|
373
|
+
})
|
|
374
|
+
|
|
375
|
+
test("pressing a hidden tab shows its panel, hiding the oldest visible one", async () => {
|
|
376
|
+
const user = userEvent.setup()
|
|
377
|
+
render(<ResponsiveWorkspace />)
|
|
378
|
+
|
|
379
|
+
resizeCanvas(700)
|
|
380
|
+
await settleExit()
|
|
381
|
+
expect(panel("one")).toBeNull()
|
|
382
|
+
|
|
383
|
+
await user.click(tab("one"))
|
|
384
|
+
await settleExit()
|
|
385
|
+
expect(panel("one")).not.toBeNull()
|
|
386
|
+
expect(panel("two")).toBeNull()
|
|
387
|
+
expect(panel("three")).not.toBeNull()
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
test("pressing a visible tab hides its panel; the last one never hides", async () => {
|
|
391
|
+
const user = userEvent.setup()
|
|
392
|
+
render(<ResponsiveWorkspace />)
|
|
393
|
+
|
|
394
|
+
resizeCanvas(400)
|
|
395
|
+
await settleExit()
|
|
396
|
+
// One panel's worth of room — only the newest activation stays.
|
|
397
|
+
expect(panel("one")).toBeNull()
|
|
398
|
+
expect(panel("two")).toBeNull()
|
|
399
|
+
expect(panel("three")).not.toBeNull()
|
|
400
|
+
|
|
401
|
+
// The sole visible panel refuses to hide.
|
|
402
|
+
await user.click(tab("three"))
|
|
403
|
+
await settleExit()
|
|
404
|
+
expect(panel("three")).not.toBeNull()
|
|
405
|
+
|
|
406
|
+
// Activating another trades places with it.
|
|
407
|
+
await user.click(tab("one"))
|
|
408
|
+
await settleExit()
|
|
409
|
+
expect(panel("one")).not.toBeNull()
|
|
410
|
+
expect(panel("three")).toBeNull()
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
test("hovering a tab spotlights its panel and dims the others", async () => {
|
|
414
|
+
const user = userEvent.setup()
|
|
415
|
+
render(<ResponsiveWorkspace />)
|
|
416
|
+
|
|
417
|
+
resizeCanvas(1200)
|
|
418
|
+
await user.hover(tab("two"))
|
|
419
|
+
expect(panel("two")?.getAttribute("data-dimmed")).toBeNull()
|
|
420
|
+
expect(panel("one")?.getAttribute("data-dimmed")).toBe("")
|
|
421
|
+
expect(panel("three")?.getAttribute("data-dimmed")).toBe("")
|
|
422
|
+
|
|
423
|
+
await user.unhover(tab("two"))
|
|
424
|
+
expect(panel("one")?.getAttribute("data-dimmed")).toBeNull()
|
|
425
|
+
expect(panel("three")?.getAttribute("data-dimmed")).toBeNull()
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
test("user-hidden panels stay hidden when the canvas widens", async () => {
|
|
429
|
+
const user = userEvent.setup()
|
|
430
|
+
render(<ResponsiveWorkspace />)
|
|
431
|
+
|
|
432
|
+
resizeCanvas(1200)
|
|
433
|
+
await user.click(tab("two"))
|
|
434
|
+
await settleExit()
|
|
435
|
+
expect(panel("two")).toBeNull()
|
|
436
|
+
|
|
437
|
+
resizeCanvas(1400)
|
|
438
|
+
await settleExit()
|
|
439
|
+
expect(panel("two")).toBeNull()
|
|
440
|
+
|
|
441
|
+
// Its tab presses back to visible.
|
|
442
|
+
await user.click(tab("two"))
|
|
443
|
+
expect(await screen.findByRole("region", { name: "Panel two" })).toBeDefined()
|
|
444
|
+
})
|
|
445
|
+
})
|
|
@@ -8,6 +8,13 @@ export const SCAFFOLD_INSPECTOR_WIDTH = 272
|
|
|
8
8
|
* count reaches it so the Add segment hides. */
|
|
9
9
|
export const SCAFFOLD_PANEL_CAPACITY = 3
|
|
10
10
|
|
|
11
|
+
/** Narrowest a panel may render, in px. When the canvas can't grant every
|
|
12
|
+
* panel this width, the scaffold hides panels until the rest fit. */
|
|
13
|
+
export const SCAFFOLD_PANEL_MIN_WIDTH = 320
|
|
14
|
+
|
|
15
|
+
/** Gap in px between side-by-side panels on the canvas (`gap-2`). */
|
|
16
|
+
export const SCAFFOLD_CANVAS_GAP = 8
|
|
17
|
+
|
|
11
18
|
/** Gap in px between the drawer/actions and the viewport edges. */
|
|
12
19
|
export const SCAFFOLD_EDGE_GUTTER = 10
|
|
13
20
|
|
|
@@ -145,6 +145,9 @@ export function PanelMenuButton({
|
|
|
145
145
|
size="icon-sm"
|
|
146
146
|
variant="ghost"
|
|
147
147
|
sound={sound}
|
|
148
|
+
// The hidden Expand|Swap|Close side stretches the pill to the sm
|
|
149
|
+
// button height — match it so the collapsed toggle stays square.
|
|
150
|
+
className="size-8 sm:size-7"
|
|
148
151
|
data-slot="scaffold-panel-menu-toggle"
|
|
149
152
|
onClick={handleOnExpand}
|
|
150
153
|
onPointerEnter={handleToggleHover}
|
|
@@ -4,26 +4,69 @@ import { AnimatePresence } from "motion/react"
|
|
|
4
4
|
import * as React from "react"
|
|
5
5
|
|
|
6
6
|
import { cn } from "@/lib/utils"
|
|
7
|
-
import
|
|
7
|
+
import { useScaffoldContext } from "./scaffold-context"
|
|
8
|
+
import type { ScaffoldCanvasProps, ScaffoldPanelProps } from "./type"
|
|
9
|
+
|
|
10
|
+
/** A canvas child's panel id when it opted into responsive hiding. */
|
|
11
|
+
function panelIdOf(child: React.ReactNode): string | undefined {
|
|
12
|
+
if (!React.isValidElement<Pick<ScaffoldPanelProps, "id">>(child))
|
|
13
|
+
return undefined
|
|
14
|
+
const id = child.props.id
|
|
15
|
+
return typeof id === "string" ? id : undefined
|
|
16
|
+
}
|
|
8
17
|
|
|
9
18
|
/**
|
|
10
19
|
* The panel row below the action cluster: 8px gutters, 8px gaps, panels
|
|
11
20
|
* side by side. Key each `Scaffold.Panel` child so closes animate out.
|
|
21
|
+
* Measures its own width and hides id'd panels (oldest activation first)
|
|
22
|
+
* whenever it can't grant each one `SCAFFOLD_PANEL_MIN_WIDTH`.
|
|
12
23
|
*/
|
|
13
24
|
export function ScaffoldCanvas({
|
|
14
25
|
className,
|
|
15
26
|
children,
|
|
16
27
|
...rest
|
|
17
28
|
}: ScaffoldCanvasProps): React.ReactElement {
|
|
29
|
+
const { hiddenPanelIds, registerPanelIds, setCanvasWidth } =
|
|
30
|
+
useScaffoldContext()
|
|
31
|
+
const ref = React.useRef<HTMLDivElement>(null)
|
|
32
|
+
|
|
33
|
+
React.useLayoutEffect(() => {
|
|
34
|
+
const element = ref.current
|
|
35
|
+
if (!element || typeof ResizeObserver === "undefined") return
|
|
36
|
+
const observer = new ResizeObserver(([entry]) =>
|
|
37
|
+
setCanvasWidth(entry.contentRect.width)
|
|
38
|
+
)
|
|
39
|
+
observer.observe(element)
|
|
40
|
+
return () => observer.disconnect()
|
|
41
|
+
}, [setCanvasWidth])
|
|
42
|
+
|
|
43
|
+
const childArray = React.Children.toArray(children)
|
|
44
|
+
const panelIds = childArray
|
|
45
|
+
.map(panelIdOf)
|
|
46
|
+
.filter((id): id is string => id !== undefined)
|
|
47
|
+
const idsKey = panelIds.join(" ")
|
|
48
|
+
|
|
49
|
+
React.useLayoutEffect(() => {
|
|
50
|
+
registerPanelIds(panelIds)
|
|
51
|
+
// panelIds is rebuilt each render; idsKey carries its identity.
|
|
52
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
53
|
+
}, [idsKey, registerPanelIds])
|
|
54
|
+
|
|
55
|
+
const visibleChildren = childArray.filter((child) => {
|
|
56
|
+
const id = panelIdOf(child)
|
|
57
|
+
return id === undefined || !hiddenPanelIds.includes(id)
|
|
58
|
+
})
|
|
59
|
+
|
|
18
60
|
return (
|
|
19
61
|
<div
|
|
20
|
-
className={cn("relative flex w-full flex-1 gap-2", className)}
|
|
62
|
+
className={cn("relative flex min-h-0 w-full flex-1 gap-2", className)}
|
|
21
63
|
data-slot="scaffold-canvas"
|
|
64
|
+
ref={ref}
|
|
22
65
|
{...rest}
|
|
23
66
|
>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
</AnimatePresence>
|
|
67
|
+
{/* Sync mode (no `mode="wait"`): an exiting panel shrinks in place
|
|
68
|
+
while its siblings grow into the freed room in the same beat. */}
|
|
69
|
+
<AnimatePresence initial={false}>{visibleChildren}</AnimatePresence>
|
|
27
70
|
</div>
|
|
28
71
|
)
|
|
29
72
|
}
|
|
@@ -53,7 +53,7 @@ export function ScaffoldInspector({
|
|
|
53
53
|
{...rest}
|
|
54
54
|
>
|
|
55
55
|
<GlassContainer
|
|
56
|
-
className="flex flex-1 flex-col rounded-md border-t border-white bg-neutral-50/
|
|
56
|
+
className="flex flex-1 flex-col rounded-md border-t border-white bg-neutral-50/20 shadow-inner shadow-lg dark:border-neutral-800 dark:bg-black/5"
|
|
57
57
|
glassVariant="subtle"
|
|
58
58
|
>
|
|
59
59
|
<div
|
|
@@ -65,7 +65,8 @@ export function ScaffoldInspector({
|
|
|
65
65
|
</span>
|
|
66
66
|
<Button
|
|
67
67
|
size="icon-xs"
|
|
68
|
-
variant="
|
|
68
|
+
variant="glass"
|
|
69
|
+
glassVariant="subtle"
|
|
69
70
|
className="scale-85"
|
|
70
71
|
aria-label="Close inspector"
|
|
71
72
|
onClick={toggleInspector}
|
|
@@ -18,7 +18,7 @@ export function ScaffoldMain({
|
|
|
18
18
|
return (
|
|
19
19
|
<div
|
|
20
20
|
id="scaffold-main"
|
|
21
|
-
className={cn("relative flex
|
|
21
|
+
className={cn("relative flex min-w-0 flex-1 flex-col", className)}
|
|
22
22
|
data-slot="scaffold-main"
|
|
23
23
|
{...rest}
|
|
24
24
|
>
|
|
@@ -5,8 +5,13 @@ import * as React from "react"
|
|
|
5
5
|
import { useState } from "react"
|
|
6
6
|
|
|
7
7
|
import { cn } from "@/lib/utils"
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
SCAFFOLD_EASE,
|
|
10
|
+
SCAFFOLD_MORPH_DURATION,
|
|
11
|
+
SCAFFOLD_PANEL_MIN_WIDTH,
|
|
12
|
+
} from "./constants"
|
|
9
13
|
import { PanelMenuButton } from "./panel-menu-button.tsx"
|
|
14
|
+
import { useScaffoldContext } from "./scaffold-context"
|
|
10
15
|
import type { ScaffoldPanelProps, TSubPanelPosition } from "./type"
|
|
11
16
|
import { ScaffoldSubPanel } from "@/components/blocks/scaffold/scaffold-sub-panel.tsx"
|
|
12
17
|
|
|
@@ -18,6 +23,7 @@ import { ScaffoldSubPanel } from "@/components/blocks/scaffold/scaffold-sub-pane
|
|
|
18
23
|
* primary slot while the primary card shrinks down into the strip.
|
|
19
24
|
*/
|
|
20
25
|
export function ScaffoldPanel({
|
|
26
|
+
id,
|
|
21
27
|
children,
|
|
22
28
|
SecondaryPanel,
|
|
23
29
|
onSwap,
|
|
@@ -27,8 +33,14 @@ export function ScaffoldPanel({
|
|
|
27
33
|
sound = true,
|
|
28
34
|
className,
|
|
29
35
|
}: ScaffoldPanelProps): React.ReactElement {
|
|
36
|
+
const { hoveredPanelId } = useScaffoldContext()
|
|
30
37
|
const reducedMotion = useReducedMotion()
|
|
31
38
|
|
|
39
|
+
// A hovered tab spotlights its own panel — every other id'd panel
|
|
40
|
+
// fades back so the pairing reads at a glance.
|
|
41
|
+
const dimmed =
|
|
42
|
+
id !== undefined && hoveredPanelId !== null && hoveredPanelId !== id
|
|
43
|
+
|
|
32
44
|
const transition = {
|
|
33
45
|
duration: reducedMotion ? 0 : SCAFFOLD_MORPH_DURATION,
|
|
34
46
|
ease: SCAFFOLD_EASE,
|
|
@@ -48,17 +60,41 @@ export function ScaffoldPanel({
|
|
|
48
60
|
setSubPanelPosition((prev) => (prev === "top" ? "bottom" : "top"))
|
|
49
61
|
}
|
|
50
62
|
|
|
63
|
+
// Enter/exit grow and shrink the panel's slot in the flex row: flexGrow
|
|
64
|
+
// carries the width (the panel is basis-0, so `width` itself is inert on
|
|
65
|
+
// the main axis) and minWidth releases the 320px floor so the slot can
|
|
66
|
+
// reach zero. scaleX pins the visual anchor to the panel's center —
|
|
67
|
+
// flexbox collapses a slot toward whichever edge its neighbors grow
|
|
68
|
+
// from, so without it the panel would wipe from one side.
|
|
69
|
+
const hidden = { flexGrow: 0, minWidth: 0, scaleX: 0, opacity: 0 }
|
|
70
|
+
|
|
51
71
|
return (
|
|
52
72
|
<motion.section
|
|
53
|
-
id="scaffold-panel"
|
|
54
|
-
animate={{
|
|
73
|
+
id={id ?? "scaffold-panel"}
|
|
74
|
+
animate={{
|
|
75
|
+
flexGrow: 1,
|
|
76
|
+
// id panels opt into responsive hiding, so each visible one can
|
|
77
|
+
// hold the 320px floor — the canvas hides siblings before this
|
|
78
|
+
// floor would overflow it.
|
|
79
|
+
minWidth: id !== undefined ? SCAFFOLD_PANEL_MIN_WIDTH : 0,
|
|
80
|
+
scaleX: 1,
|
|
81
|
+
flexDirection: isSwapped ? "column-reverse" : "column",
|
|
82
|
+
// The hovered tab's panel keeps full opacity; the rest fade back
|
|
83
|
+
// so the tab↔panel pairing reads at a glance. Inline (not a
|
|
84
|
+
// class) because motion owns this element's opacity.
|
|
85
|
+
opacity: dimmed ? 0.35 : 1,
|
|
86
|
+
}}
|
|
87
|
+
initial={hidden}
|
|
88
|
+
exit={hidden}
|
|
89
|
+
style={{ originX: 0.5 }}
|
|
55
90
|
aria-label={name}
|
|
56
91
|
className={cn(
|
|
57
|
-
"group/panel relative flex min-w-
|
|
92
|
+
"group/panel relative flex min-w-0 flex-1 flex-col w-full",
|
|
58
93
|
className
|
|
59
94
|
)}
|
|
60
|
-
layout
|
|
95
|
+
layout={"size"}
|
|
61
96
|
data-slot="scaffold-panel"
|
|
97
|
+
data-dimmed={dimmed ? "" : undefined}
|
|
62
98
|
data-swapped={isSwapped ? "" : undefined}
|
|
63
99
|
transition={transition}
|
|
64
100
|
>
|
|
@@ -5,6 +5,7 @@ import * as React from "react"
|
|
|
5
5
|
import { cn } from "@/lib/utils"
|
|
6
6
|
import { SCAFFOLD_INSPECTOR_WIDTH } from "./constants"
|
|
7
7
|
import { ScaffoldContext } from "./scaffold-context"
|
|
8
|
+
import { usePanelVisibility } from "./use-panel-visibility"
|
|
8
9
|
import type { ScaffoldContextValue, ScaffoldRootProps } from "./type"
|
|
9
10
|
import type { ClassNameValue } from "tailwind-merge"
|
|
10
11
|
|
|
@@ -41,9 +42,23 @@ export function ScaffoldRoot({
|
|
|
41
42
|
[inspectorOpen, setInspectorOpen]
|
|
42
43
|
)
|
|
43
44
|
|
|
45
|
+
const panelVisibility = usePanelVisibility()
|
|
46
|
+
|
|
44
47
|
const value = React.useMemo<ScaffoldContextValue>(
|
|
45
|
-
() => ({
|
|
46
|
-
|
|
48
|
+
() => ({
|
|
49
|
+
inspectorOpen,
|
|
50
|
+
inspectorWidth,
|
|
51
|
+
setInspectorOpen,
|
|
52
|
+
toggleInspector,
|
|
53
|
+
...panelVisibility,
|
|
54
|
+
}),
|
|
55
|
+
[
|
|
56
|
+
inspectorOpen,
|
|
57
|
+
inspectorWidth,
|
|
58
|
+
setInspectorOpen,
|
|
59
|
+
toggleInspector,
|
|
60
|
+
panelVisibility,
|
|
61
|
+
]
|
|
47
62
|
)
|
|
48
63
|
|
|
49
64
|
return (
|
|
@@ -54,7 +69,7 @@ export function ScaffoldRoot({
|
|
|
54
69
|
// `clip`, not `hidden`: the off canvas inspector extends the
|
|
55
70
|
// scrollable overflow, and focus/scrollIntoView would scroll a
|
|
56
71
|
// hidden-overflow root sideways to reveal it.
|
|
57
|
-
"relative isolate flex
|
|
72
|
+
"relative isolate flex size-full flex-1 overflow-clip pr-2 pb-2",
|
|
58
73
|
background,
|
|
59
74
|
className
|
|
60
75
|
)}
|
|
@@ -19,7 +19,7 @@ export function ScaffoldSidebar({
|
|
|
19
19
|
id="scaffold-sidebar"
|
|
20
20
|
aria-label="Primary"
|
|
21
21
|
className={cn(
|
|
22
|
-
"flex min-w-12 shrink-0 flex-col items-center gap-1 pt-10 pb-2",
|
|
22
|
+
"flex h-full min-w-12 shrink-0 flex-col items-center gap-1 overflow-y-auto pt-10 pb-2",
|
|
23
23
|
className
|
|
24
24
|
)}
|
|
25
25
|
data-slot="scaffold-sidebar"
|
|
@@ -5,10 +5,11 @@ import * as React from "react"
|
|
|
5
5
|
|
|
6
6
|
import { cn } from "@/lib/utils"
|
|
7
7
|
import { SPRING_LAYOUT } from "@/lib/ease.ts"
|
|
8
|
+
import { useScaffoldContext } from "./scaffold-context"
|
|
8
9
|
import type { ScaffoldTabProps } from "./type"
|
|
9
10
|
import { segmentVariants } from "./utils"
|
|
10
11
|
import { Button } from "@/components/ui/button"
|
|
11
|
-
import { LucideAppWindow, LucideX } from "lucide-react"
|
|
12
|
+
import { LucideAppWindow, LucideEyeOff, LucideX } from "lucide-react"
|
|
12
13
|
import { GlassButtonGroup } from "@/components/ui/glasscn/glass-button-group.tsx"
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -16,25 +17,51 @@ import { GlassButtonGroup } from "@/components/ui/glasscn/glass-button-group.tsx
|
|
|
16
17
|
* affordance. Render one per open panel as Actions children, keyed —
|
|
17
18
|
* closes animate out of the pill. Omit `onClose` on the last remaining
|
|
18
19
|
* panel's tab; the canvas keeps at least one panel open.
|
|
20
|
+
*
|
|
21
|
+
* With `panelId` (matching a `Scaffold.Panel`'s `id`), the tab also fronts
|
|
22
|
+
* that panel's visibility: a hidden panel dims its tab behind an eye-off
|
|
23
|
+
* icon, and pressing the label toggles the panel — showing one past
|
|
24
|
+
* capacity hides the least-recently-activated panel in its place.
|
|
19
25
|
*/
|
|
20
26
|
export function ScaffoldTab({
|
|
21
27
|
children,
|
|
28
|
+
panelId,
|
|
22
29
|
onClose,
|
|
23
30
|
closeLabel = "Close panel",
|
|
24
31
|
sound = true,
|
|
25
32
|
className,
|
|
26
33
|
...rest
|
|
27
34
|
}: ScaffoldTabProps): React.ReactElement {
|
|
35
|
+
const { isPanelHidden, togglePanelVisibility, setPanelHovered } =
|
|
36
|
+
useScaffoldContext()
|
|
28
37
|
const reduce = useReducedMotion()
|
|
29
38
|
const transition = reduce ? { duration: 0 } : SPRING_LAYOUT
|
|
30
39
|
|
|
40
|
+
const hidden = panelId !== undefined && isPanelHidden(panelId)
|
|
41
|
+
|
|
42
|
+
// A tab closed mid-hover never fires pointer-leave — release the
|
|
43
|
+
// spotlight it holds so the canvas doesn't stay dimmed.
|
|
44
|
+
React.useEffect(() => {
|
|
45
|
+
if (panelId === undefined) return
|
|
46
|
+
return () => setPanelHovered(panelId, false)
|
|
47
|
+
}, [panelId, setPanelHovered])
|
|
48
|
+
|
|
31
49
|
return (
|
|
32
50
|
<GlassButtonGroup
|
|
33
51
|
layout
|
|
34
|
-
animate={reduce ? { opacity: 1 } : "visible"}
|
|
52
|
+
animate={reduce ? { opacity: 1, width: "auto" } : "visible"}
|
|
35
53
|
data-slot="scaffold-tab"
|
|
36
|
-
|
|
37
|
-
|
|
54
|
+
data-panel-hidden={hidden ? "" : undefined}
|
|
55
|
+
onPointerEnter={
|
|
56
|
+
panelId !== undefined ? () => setPanelHovered(panelId, true) : undefined
|
|
57
|
+
}
|
|
58
|
+
onPointerLeave={
|
|
59
|
+
panelId !== undefined
|
|
60
|
+
? () => setPanelHovered(panelId, false)
|
|
61
|
+
: undefined
|
|
62
|
+
}
|
|
63
|
+
exit={reduce ? { opacity: 0, width: 0 } : "exit"}
|
|
64
|
+
initial={reduce ? { opacity: 0, width: 0 } : "hidden"}
|
|
38
65
|
transition={transition}
|
|
39
66
|
variants={segmentVariants}
|
|
40
67
|
glassVariant="subtle"
|
|
@@ -42,11 +69,22 @@ export function ScaffoldTab({
|
|
|
42
69
|
{...rest}
|
|
43
70
|
>
|
|
44
71
|
<Button
|
|
72
|
+
aria-pressed={panelId !== undefined ? !hidden : undefined}
|
|
73
|
+
onClick={
|
|
74
|
+
panelId !== undefined
|
|
75
|
+
? () => togglePanelVisibility(panelId)
|
|
76
|
+
: undefined
|
|
77
|
+
}
|
|
45
78
|
size="sm"
|
|
79
|
+
sound={sound}
|
|
46
80
|
variant="ghost"
|
|
47
|
-
className={cn(
|
|
81
|
+
className={cn(
|
|
82
|
+
"gap-2.5 rounded-sm pl-2",
|
|
83
|
+
onClose && "pr-2",
|
|
84
|
+
hidden && "text-muted-foreground/70"
|
|
85
|
+
)}
|
|
48
86
|
>
|
|
49
|
-
<LucideAppWindow size={16} />
|
|
87
|
+
{hidden ? <LucideEyeOff size={16} /> : <LucideAppWindow size={16} />}
|
|
50
88
|
{children}
|
|
51
89
|
</Button>
|
|
52
90
|
{onClose && (
|
|
@@ -56,7 +94,7 @@ export function ScaffoldTab({
|
|
|
56
94
|
size="icon-sm"
|
|
57
95
|
sound={sound}
|
|
58
96
|
variant="ghost"
|
|
59
|
-
className="rounded-none!"
|
|
97
|
+
className="self-center rounded-none!"
|
|
60
98
|
>
|
|
61
99
|
<LucideX size={16} />
|
|
62
100
|
</Button>
|
|
@@ -8,6 +8,25 @@ export interface ScaffoldContextValue {
|
|
|
8
8
|
inspectorWidth: number
|
|
9
9
|
setInspectorOpen: (open: boolean) => void
|
|
10
10
|
toggleInspector: () => void
|
|
11
|
+
/** How many panels currently fit side by side, from the measured canvas
|
|
12
|
+
* width at `SCAFFOLD_PANEL_MIN_WIDTH` per panel (1..`SCAFFOLD_PANEL_CAPACITY`). */
|
|
13
|
+
panelCapacity: number
|
|
14
|
+
/** Ids of id'd panels the scaffold is currently hiding — auto-hidden by
|
|
15
|
+
* capacity pressure or toggled away from a tab. */
|
|
16
|
+
hiddenPanelIds: readonly string[]
|
|
17
|
+
isPanelHidden: (id: string) => boolean
|
|
18
|
+
/** Show/hide an id'd panel. Showing past capacity auto-hides the
|
|
19
|
+
* least-recently-activated visible panel; the last visible one never hides. */
|
|
20
|
+
togglePanelVisibility: (id: string) => void
|
|
21
|
+
/** Id of the panel whose tab the pointer is resting on — the canvas
|
|
22
|
+
* spotlights it by fading every other id'd panel to 0.35 opacity. */
|
|
23
|
+
hoveredPanelId: string | null
|
|
24
|
+
/** @internal Tabs report pointer enter/leave on their label. */
|
|
25
|
+
setPanelHovered: (id: string, hovered: boolean) => void
|
|
26
|
+
/** @internal Canvas reports its ordered panel ids. */
|
|
27
|
+
registerPanelIds: (ids: readonly string[]) => void
|
|
28
|
+
/** @internal Canvas reports its measured width. */
|
|
29
|
+
setCanvasWidth: (width: number) => void
|
|
11
30
|
}
|
|
12
31
|
|
|
13
32
|
export interface ScaffoldRootProps extends ComponentProps<"div"> {
|
|
@@ -62,6 +81,11 @@ export type TScaffoldPanelChild<
|
|
|
62
81
|
> = ReactElement<T, React.JSXElementConstructor<T>>
|
|
63
82
|
|
|
64
83
|
export interface ScaffoldPanelProps {
|
|
84
|
+
/** Stable id opting the panel into responsive hiding: the canvas hides
|
|
85
|
+
* id'd panels when it can't grant each `SCAFFOLD_PANEL_MIN_WIDTH`, and a
|
|
86
|
+
* `Scaffold.Tab` with the matching `panelId` reflects and toggles it.
|
|
87
|
+
* Omitted, the panel always renders. */
|
|
88
|
+
id?: string
|
|
65
89
|
/** The panel's content, rendered in a card with a drop shadow. */
|
|
66
90
|
children: ReactNode
|
|
67
91
|
/** The panel's content, rendered in a card with a drop shadow. */
|
|
@@ -88,6 +112,10 @@ export interface ScaffoldTabProps extends MotionSafe<
|
|
|
88
112
|
> {
|
|
89
113
|
/** The tab's label. */
|
|
90
114
|
children?: ReactNode
|
|
115
|
+
/** Id of the panel this tab fronts (`Scaffold.Panel`'s `id`). The tab
|
|
116
|
+
* then shows whether the panel is hidden and pressing its label toggles
|
|
117
|
+
* visibility — showing past capacity hides another panel in its place. */
|
|
118
|
+
panelId?: string
|
|
91
119
|
/** Renders the tab's close button. The button renders only while this
|
|
92
120
|
* is present — omit it to pin the tab, e.g. on the last remaining
|
|
93
121
|
* panel (the canvas keeps at least one open). */
|