@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.
- 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.d.ts +11 -2
- package/dist-lib/components/blocks/nav/sidebar/type.d.ts +83 -1
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +22 -31
- package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +5 -0
- package/dist-lib/components/blocks/scaffold/constants.d.ts +8 -0
- package/dist-lib/components/blocks/scaffold/index.d.ts +3 -1
- package/dist-lib/components/blocks/scaffold/panel-menu-button.d.ts +4 -3
- package/dist-lib/components/blocks/scaffold/scaffold-actions.d.ts +5 -4
- 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 +14 -0
- package/dist-lib/components/blocks/scaffold/type.d.ts +58 -28
- package/dist-lib/components/blocks/scaffold/use-panel-visibility.d.ts +19 -0
- package/dist-lib/components/blocks/scaffold/utils.d.ts +12 -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 +4 -0
- package/dist-lib/components/composed/tree/tree-context.d.ts +4 -0
- package/dist-lib/components/composed/tree/tree-node.d.ts +7 -0
- package/dist-lib/components/composed/tree/tree.d.ts +19 -0
- package/dist-lib/components/composed/tree/type.d.ts +192 -0
- package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +7 -0
- package/dist-lib/components/composed/tree/use-tree.d.ts +20 -0
- package/dist-lib/components/composed/tree/utils.d.ts +28 -0
- package/dist-lib/components/user-avatar/audio-wave.d.ts +2 -0
- package/dist-lib/components/user-avatar/component.d.ts +10 -0
- package/dist-lib/components/user-avatar/fallback.d.ts +5 -0
- package/dist-lib/components/user-avatar/index.d.ts +13 -0
- package/dist-lib/components/user-avatar/type.d.ts +26 -0
- package/dist-lib/components/user-avatar/utils.d.ts +2 -0
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.js +2309 -1285
- package/dist-lib/index.js.map +1 -1
- package/package.json +2 -1
- package/src/components/beste/piece/browser-frame.tsx +9 -6
- package/src/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.tsx +310 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +90 -60
- package/src/components/blocks/nav/sidebar/type.ts +105 -1
- package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +293 -116
- package/src/components/blocks/nav/sidebar/utils.ts +28 -0
- package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +268 -26
- package/src/components/blocks/scaffold/constants.ts +11 -0
- package/src/components/blocks/scaffold/index.ts +4 -1
- package/src/components/blocks/scaffold/panel-menu-button.tsx +96 -115
- package/src/components/blocks/scaffold/scaffold-actions.tsx +41 -50
- package/src/components/blocks/scaffold/scaffold-canvas.tsx +48 -5
- package/src/components/blocks/scaffold/scaffold-inspector.tsx +27 -3
- package/src/components/blocks/scaffold/scaffold-main.tsx +1 -1
- package/src/components/blocks/scaffold/scaffold-panel.tsx +56 -30
- 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 +104 -0
- package/src/components/blocks/scaffold/type.ts +62 -33
- package/src/components/blocks/scaffold/use-panel-visibility.ts +180 -0
- package/src/components/blocks/scaffold/utils.ts +35 -0
- package/src/components/composed/tree/__tests__/tree.test.tsx +400 -0
- package/src/components/composed/tree/__tests__/use-tree.test.tsx +332 -0
- package/src/components/composed/tree/constants.ts +60 -0
- package/src/components/composed/tree/index.ts +13 -0
- package/src/components/composed/tree/tree-context.ts +13 -0
- package/src/components/composed/tree/tree-node.tsx +451 -0
- package/src/components/composed/tree/tree.tsx +278 -0
- package/src/components/composed/tree/type.ts +212 -0
- package/src/components/composed/tree/use-tree-dnd.ts +148 -0
- package/src/components/composed/tree/use-tree.ts +306 -0
- package/src/components/composed/tree/utils.ts +172 -0
- package/src/components/composed/user-avatar.tsx +4 -1
- package/src/components/docs/component-preview.tsx +11 -11
- package/src/components/user-avatar/audio-wave.tsx +22 -0
- package/src/components/user-avatar/component.tsx +53 -0
- package/src/components/user-avatar/fallback.tsx +26 -0
- package/src/components/user-avatar/index.ts +15 -0
- package/src/components/user-avatar/type.ts +35 -0
- package/src/components/user-avatar/utils.ts +8 -0
- package/src/index.css +211 -153
- package/src/index.ts +1 -0
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
import { motion, useReducedMotion } from "motion/react"
|
|
4
4
|
import * as React from "react"
|
|
5
|
+
import { useState } from "react"
|
|
5
6
|
|
|
6
7
|
import { cn } from "@/lib/utils"
|
|
7
|
-
import {
|
|
8
|
-
|
|
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
|
-
import { useState } from "react"
|
|
13
17
|
|
|
14
18
|
/**
|
|
15
19
|
* A canvas panel: a primary card plus an optional secondary strip below it,
|
|
@@ -19,80 +23,102 @@ import { useState } from "react"
|
|
|
19
23
|
* primary slot while the primary card shrinks down into the strip.
|
|
20
24
|
*/
|
|
21
25
|
export function ScaffoldPanel({
|
|
26
|
+
id,
|
|
22
27
|
children,
|
|
23
28
|
SecondaryPanel,
|
|
24
|
-
onClose,
|
|
25
29
|
onSwap,
|
|
30
|
+
onCloseSecondary,
|
|
26
31
|
name,
|
|
27
|
-
closeLabel = "Close panel",
|
|
28
32
|
swapLabel = "Swap panel content",
|
|
29
33
|
sound = true,
|
|
30
34
|
className,
|
|
31
35
|
}: ScaffoldPanelProps): React.ReactElement {
|
|
32
|
-
|
|
36
|
+
const { hoveredPanelId } = useScaffoldContext()
|
|
33
37
|
const reducedMotion = useReducedMotion()
|
|
34
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
|
+
|
|
35
44
|
const transition = {
|
|
36
45
|
duration: reducedMotion ? 0 : SCAFFOLD_MORPH_DURATION,
|
|
37
46
|
ease: SCAFFOLD_EASE,
|
|
38
47
|
}
|
|
39
48
|
|
|
40
|
-
const [subPanelPosition, setSubPanelPosition] =
|
|
49
|
+
const [subPanelPosition, setSubPanelPosition] =
|
|
50
|
+
useState<TSubPanelPosition>("top")
|
|
41
51
|
const [isSwapped, setIsSwapped] = useState<boolean>(false)
|
|
42
52
|
const handleSwap = () => {
|
|
43
|
-
setIsSwapped((prev) => !prev)
|
|
44
|
-
onSwap?.()
|
|
45
|
-
}
|
|
53
|
+
setIsSwapped((prev) => !prev)
|
|
54
|
+
onSwap?.()
|
|
55
|
+
}
|
|
46
56
|
|
|
47
57
|
// Trades which sub-panel holds the flexible slot — the collapsed card grows
|
|
48
58
|
// into `flex-1` while the expanded one shrinks to the `min-h-14` strip.
|
|
49
59
|
const handleExpand = () => {
|
|
50
|
-
setSubPanelPosition((prev) => (prev === "top" ? "bottom" : "top"))
|
|
51
|
-
}
|
|
60
|
+
setSubPanelPosition((prev) => (prev === "top" ? "bottom" : "top"))
|
|
61
|
+
}
|
|
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 }
|
|
52
70
|
|
|
53
71
|
return (
|
|
54
72
|
<motion.section
|
|
55
|
-
id="scaffold-panel"
|
|
56
|
-
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 }}
|
|
57
90
|
aria-label={name}
|
|
58
91
|
className={cn(
|
|
59
|
-
"group/panel relative flex min-w-
|
|
92
|
+
"group/panel relative flex min-w-0 flex-1 flex-col w-full",
|
|
60
93
|
className
|
|
61
94
|
)}
|
|
62
|
-
layout
|
|
95
|
+
layout={"size"}
|
|
63
96
|
data-slot="scaffold-panel"
|
|
97
|
+
data-dimmed={dimmed ? "" : undefined}
|
|
64
98
|
data-swapped={isSwapped ? "" : undefined}
|
|
65
99
|
transition={transition}
|
|
66
100
|
>
|
|
67
|
-
<ScaffoldSubPanel
|
|
68
|
-
id="top"
|
|
69
|
-
expanded={subPanelPosition === "top"}
|
|
70
|
-
primary
|
|
71
|
-
>
|
|
101
|
+
<ScaffoldSubPanel id="top" expanded={subPanelPosition === "top"} primary>
|
|
72
102
|
{children}
|
|
73
103
|
</ScaffoldSubPanel>
|
|
74
|
-
{onSwap && SecondaryPanel && (
|
|
75
|
-
<div className="relative flex items-center justify-center
|
|
104
|
+
{(onSwap || onCloseSecondary) && SecondaryPanel && (
|
|
105
|
+
<div className="relative flex h-2 items-center justify-center">
|
|
76
106
|
<PanelMenuButton
|
|
77
107
|
label={swapLabel}
|
|
78
108
|
onClick={handleSwap}
|
|
79
109
|
onExpand={handleExpand}
|
|
110
|
+
onCloseSecondary={onCloseSecondary}
|
|
80
111
|
secondaryExpanded={subPanelPosition === "bottom"}
|
|
112
|
+
swapped={isSwapped}
|
|
81
113
|
sound={sound}
|
|
82
114
|
/>
|
|
83
115
|
</div>
|
|
84
116
|
)}
|
|
85
117
|
{SecondaryPanel && (
|
|
86
|
-
<ScaffoldSubPanel
|
|
87
|
-
id="bottom"
|
|
88
|
-
expanded={subPanelPosition === "bottom"}
|
|
89
|
-
>
|
|
118
|
+
<ScaffoldSubPanel id="bottom" expanded={subPanelPosition === "bottom"}>
|
|
90
119
|
{SecondaryPanel}
|
|
91
120
|
</ScaffoldSubPanel>
|
|
92
121
|
)}
|
|
93
|
-
{onClose && (
|
|
94
|
-
<PanelCloseButton label={closeLabel} onClick={onClose} sound={sound} />
|
|
95
|
-
)}
|
|
96
122
|
</motion.section>
|
|
97
123
|
)
|
|
98
124
|
}
|
|
@@ -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-
|
|
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"
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useReducedMotion } from "motion/react"
|
|
4
|
+
import * as React from "react"
|
|
5
|
+
|
|
6
|
+
import { cn } from "@/lib/utils"
|
|
7
|
+
import { SPRING_LAYOUT } from "@/lib/ease.ts"
|
|
8
|
+
import { useScaffoldContext } from "./scaffold-context"
|
|
9
|
+
import type { ScaffoldTabProps } from "./type"
|
|
10
|
+
import { segmentVariants } from "./utils"
|
|
11
|
+
import { Button } from "@/components/ui/button"
|
|
12
|
+
import { LucideAppWindow, LucideEyeOff, LucideX } from "lucide-react"
|
|
13
|
+
import { GlassButtonGroup } from "@/components/ui/glasscn/glass-button-group.tsx"
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* A panel's tab inside the action cluster: its label plus a close
|
|
17
|
+
* affordance. Render one per open panel as Actions children, keyed —
|
|
18
|
+
* closes animate out of the pill. Omit `onClose` on the last remaining
|
|
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.
|
|
25
|
+
*/
|
|
26
|
+
export function ScaffoldTab({
|
|
27
|
+
children,
|
|
28
|
+
panelId,
|
|
29
|
+
onClose,
|
|
30
|
+
closeLabel = "Close panel",
|
|
31
|
+
sound = true,
|
|
32
|
+
className,
|
|
33
|
+
...rest
|
|
34
|
+
}: ScaffoldTabProps): React.ReactElement {
|
|
35
|
+
const { isPanelHidden, togglePanelVisibility, setPanelHovered } =
|
|
36
|
+
useScaffoldContext()
|
|
37
|
+
const reduce = useReducedMotion()
|
|
38
|
+
const transition = reduce ? { duration: 0 } : SPRING_LAYOUT
|
|
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
|
+
|
|
49
|
+
return (
|
|
50
|
+
<GlassButtonGroup
|
|
51
|
+
layout
|
|
52
|
+
animate={reduce ? { opacity: 1, width: "auto" } : "visible"}
|
|
53
|
+
data-slot="scaffold-tab"
|
|
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"}
|
|
65
|
+
transition={transition}
|
|
66
|
+
variants={segmentVariants}
|
|
67
|
+
glassVariant="subtle"
|
|
68
|
+
className={cn("h-full rounded-sm", className)}
|
|
69
|
+
{...rest}
|
|
70
|
+
>
|
|
71
|
+
<Button
|
|
72
|
+
aria-pressed={panelId !== undefined ? !hidden : undefined}
|
|
73
|
+
onClick={
|
|
74
|
+
panelId !== undefined
|
|
75
|
+
? () => togglePanelVisibility(panelId)
|
|
76
|
+
: undefined
|
|
77
|
+
}
|
|
78
|
+
size="sm"
|
|
79
|
+
sound={sound}
|
|
80
|
+
variant="ghost"
|
|
81
|
+
className={cn(
|
|
82
|
+
"gap-2.5 rounded-sm pl-2",
|
|
83
|
+
onClose && "pr-2",
|
|
84
|
+
hidden && "text-muted-foreground/70"
|
|
85
|
+
)}
|
|
86
|
+
>
|
|
87
|
+
{hidden ? <LucideEyeOff size={16} /> : <LucideAppWindow size={16} />}
|
|
88
|
+
{children}
|
|
89
|
+
</Button>
|
|
90
|
+
{onClose && (
|
|
91
|
+
<Button
|
|
92
|
+
aria-label={closeLabel}
|
|
93
|
+
onClick={onClose}
|
|
94
|
+
size="icon-sm"
|
|
95
|
+
sound={sound}
|
|
96
|
+
variant="ghost"
|
|
97
|
+
className="self-center rounded-none!"
|
|
98
|
+
>
|
|
99
|
+
<LucideX size={16} />
|
|
100
|
+
</Button>
|
|
101
|
+
)}
|
|
102
|
+
</GlassButtonGroup>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import React, {
|
|
2
|
-
type ComponentProps,
|
|
3
|
-
type ReactElement,
|
|
4
|
-
type ReactNode,
|
|
5
|
-
} from "react"
|
|
1
|
+
import React, { type ComponentProps, type ReactElement, type ReactNode, } from "react"
|
|
6
2
|
|
|
7
3
|
/** Inspector state shared by every scaffold part through ScaffoldContext. */
|
|
8
4
|
export interface ScaffoldContextValue {
|
|
@@ -12,6 +8,25 @@ export interface ScaffoldContextValue {
|
|
|
12
8
|
inspectorWidth: number
|
|
13
9
|
setInspectorOpen: (open: boolean) => void
|
|
14
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
|
|
15
30
|
}
|
|
16
31
|
|
|
17
32
|
export interface ScaffoldRootProps extends ComponentProps<"div"> {
|
|
@@ -46,23 +61,16 @@ type MotionSafe<T> = Omit<
|
|
|
46
61
|
export interface ScaffoldActionsProps extends MotionSafe<
|
|
47
62
|
Omit<ComponentProps<"div">, "children">
|
|
48
63
|
> {
|
|
49
|
-
/** Called when the Add segment is pressed.
|
|
64
|
+
/** Called when the Add segment is pressed. The segment renders only
|
|
65
|
+
* while this is present — omit it to hide Add, e.g. once the canvas
|
|
66
|
+
* holds `SCAFFOLD_PANEL_CAPACITY` panels. */
|
|
50
67
|
onAdd?: () => void
|
|
51
68
|
/** Label of the Add segment. */
|
|
52
69
|
addLabel?: ReactNode
|
|
53
70
|
/** Replaces the default add icon. */
|
|
54
71
|
addIcon?: ReactNode
|
|
55
|
-
/**
|
|
56
|
-
*
|
|
57
|
-
* `onBrowse` is present; the badge only when > 0. Per the design
|
|
58
|
-
* comments the dropdown menu itself is a later iteration; for now the
|
|
59
|
-
* segment just keeps the count. */
|
|
60
|
-
overflowCount?: number
|
|
61
|
-
/** Called when the overflow (stacked-panels) segment is pressed. */
|
|
62
|
-
onBrowse?: () => void
|
|
63
|
-
/** Accessible name of the overflow segment. */
|
|
64
|
-
browseLabel?: string
|
|
65
|
-
/** Extra segments appended after the built-in ones. */
|
|
72
|
+
/** The panel tabs (`Scaffold.Tab`, one per open panel) and any extra
|
|
73
|
+
* segments, rendered before Add. Key each tab — closes animate out. */
|
|
66
74
|
children?: ReactNode
|
|
67
75
|
/** Emit interaction-sound attributes (inert until an app binds cuelume). */
|
|
68
76
|
sound?: boolean
|
|
@@ -73,36 +81,51 @@ export type TScaffoldPanelChild<
|
|
|
73
81
|
> = ReactElement<T, React.JSXElementConstructor<T>>
|
|
74
82
|
|
|
75
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
|
|
76
89
|
/** The panel's content, rendered in a card with a drop shadow. */
|
|
77
90
|
children: ReactNode
|
|
78
91
|
/** The panel's content, rendered in a card with a drop shadow. */
|
|
79
92
|
SecondaryPanel?: TScaffoldPanelChild
|
|
80
|
-
/**
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
* Only shown when `secondary` is present. Clicking it trades the two
|
|
84
|
-
* cards with a layout morph — flip the slot content here so each card
|
|
85
|
-
* carries its subject to its new home. */
|
|
93
|
+
/** Reveals the Swap action in the seam menu (needs `SecondaryPanel`).
|
|
94
|
+
* Swapping trades the two cards with a layout morph — flip the slot
|
|
95
|
+
* content here so each card carries its subject to its new home. */
|
|
86
96
|
onSwap?: () => void
|
|
87
|
-
/**
|
|
88
|
-
*
|
|
89
|
-
|
|
90
|
-
/** Initial swap state when uncontrolled. */
|
|
91
|
-
defaultSwapped?: boolean
|
|
92
|
-
/** Fixed width in px; omitted, the panel flexes to share the canvas. */
|
|
93
|
-
width?: number
|
|
97
|
+
/** Reveals the Close action in the seam menu — remove `SecondaryPanel`
|
|
98
|
+
* here; the strip and the menu leave together. */
|
|
99
|
+
onCloseSecondary?: () => void
|
|
94
100
|
/** Accessible name of the panel region. */
|
|
95
101
|
name?: string
|
|
96
102
|
/** Emit interaction-sound attributes on the floating buttons. */
|
|
97
103
|
sound?: boolean
|
|
98
|
-
/** Accessible name of the
|
|
99
|
-
closeLabel?: string
|
|
100
|
-
/** Accessible name of the swap button. */
|
|
104
|
+
/** Accessible name of the seam-menu toggle. */
|
|
101
105
|
swapLabel?: string
|
|
102
106
|
/** Extra class names for the panel's root element. */
|
|
103
107
|
className?: string
|
|
104
108
|
}
|
|
105
109
|
|
|
110
|
+
export interface ScaffoldTabProps extends MotionSafe<
|
|
111
|
+
Omit<ComponentProps<"div">, "children">
|
|
112
|
+
> {
|
|
113
|
+
/** The tab's label. */
|
|
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
|
|
119
|
+
/** Renders the tab's close button. The button renders only while this
|
|
120
|
+
* is present — omit it to pin the tab, e.g. on the last remaining
|
|
121
|
+
* panel (the canvas keeps at least one open). */
|
|
122
|
+
onClose?: () => void
|
|
123
|
+
/** Accessible name of the close button. */
|
|
124
|
+
closeLabel?: string
|
|
125
|
+
/** Emit interaction-sound attributes on the close button. */
|
|
126
|
+
sound?: boolean
|
|
127
|
+
}
|
|
128
|
+
|
|
106
129
|
export type TSubPanelPosition = "bottom" | "top"
|
|
107
130
|
export type TSubPanelVariant = "card" | "subtle" | "inset"
|
|
108
131
|
|
|
@@ -147,9 +170,15 @@ export interface PanelMenuButtonProps extends PanelFloatingButtonProps {
|
|
|
147
170
|
/** Called when the Expand action is pressed — the panel trades which
|
|
148
171
|
* sub-panel holds the flexible slot. */
|
|
149
172
|
onExpand?: () => void
|
|
173
|
+
/** Renders the Close action; called when it is pressed — the consumer
|
|
174
|
+
* removes the secondary strip. */
|
|
175
|
+
onCloseSecondary?: () => void
|
|
150
176
|
/** Whether the secondary strip currently holds the flexible slot —
|
|
151
177
|
* flips the Expand chevron between down and up. */
|
|
152
178
|
secondaryExpanded?: boolean
|
|
179
|
+
/** Whether the panel is swapped (column-reverse). Mirrors the Expand
|
|
180
|
+
* chevron so it keeps pointing at the sub-panel that grows next. */
|
|
181
|
+
swapped?: boolean
|
|
153
182
|
}
|
|
154
183
|
|
|
155
184
|
export interface UseScaffoldOptions {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
|
|
5
|
+
import { SCAFFOLD_PANEL_CAPACITY } from "./constants"
|
|
6
|
+
import { getPanelCapacity } from "./utils"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Internal bookkeeping for responsive panel hiding.
|
|
10
|
+
*
|
|
11
|
+
* - `visibleOrder`: visible panel ids, least-recently-activated first — the
|
|
12
|
+
* front is what capacity pressure evicts next.
|
|
13
|
+
* - `autoHidden`: panels the scaffold hid because the canvas shrank, most
|
|
14
|
+
* recent last. They return on their own when room comes back.
|
|
15
|
+
* - `userHidden`: panels hidden by a tab press. They stay hidden until the
|
|
16
|
+
* tab is pressed again, however wide the canvas grows.
|
|
17
|
+
*/
|
|
18
|
+
interface PanelVisibilityState {
|
|
19
|
+
visibleOrder: string[]
|
|
20
|
+
autoHidden: string[]
|
|
21
|
+
userHidden: string[]
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const EMPTY_STATE: PanelVisibilityState = {
|
|
25
|
+
visibleOrder: [],
|
|
26
|
+
autoHidden: [],
|
|
27
|
+
userHidden: [],
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Fold the registered ids and current capacity into a settled state:
|
|
31
|
+
* prune departed panels, admit new ones, evict the least-recently-activated
|
|
32
|
+
* visible panels past capacity, and restore auto-hidden ones when room
|
|
33
|
+
* returns. Returns `prev` untouched when nothing changes. */
|
|
34
|
+
function reconcile(
|
|
35
|
+
prev: PanelVisibilityState,
|
|
36
|
+
ids: readonly string[],
|
|
37
|
+
capacity: number
|
|
38
|
+
): PanelVisibilityState {
|
|
39
|
+
const known = new Set(ids)
|
|
40
|
+
const visibleOrder = prev.visibleOrder.filter((id) => known.has(id))
|
|
41
|
+
const autoHidden = prev.autoHidden.filter((id) => known.has(id))
|
|
42
|
+
const userHidden = prev.userHidden.filter((id) => known.has(id))
|
|
43
|
+
|
|
44
|
+
const tracked = new Set([...visibleOrder, ...autoHidden, ...userHidden])
|
|
45
|
+
for (const id of ids) if (!tracked.has(id)) visibleOrder.push(id)
|
|
46
|
+
|
|
47
|
+
while (visibleOrder.length > capacity && visibleOrder.length > 1) {
|
|
48
|
+
autoHidden.push(visibleOrder.shift() as string)
|
|
49
|
+
}
|
|
50
|
+
while (visibleOrder.length < capacity && autoHidden.length > 0) {
|
|
51
|
+
visibleOrder.push(autoHidden.pop() as string)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const unchanged =
|
|
55
|
+
visibleOrder.join(" ") === prev.visibleOrder.join(" ") &&
|
|
56
|
+
autoHidden.join(" ") === prev.autoHidden.join(" ") &&
|
|
57
|
+
userHidden.join(" ") === prev.userHidden.join(" ")
|
|
58
|
+
return unchanged ? prev : { visibleOrder, autoHidden, userHidden }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface PanelVisibility {
|
|
62
|
+
/** How many panels currently fit side by side (1..capacity cap). */
|
|
63
|
+
panelCapacity: number
|
|
64
|
+
/** Ids of panels currently hidden (auto + user), for tabs to reflect. */
|
|
65
|
+
hiddenPanelIds: readonly string[]
|
|
66
|
+
isPanelHidden: (id: string) => boolean
|
|
67
|
+
togglePanelVisibility: (id: string) => void
|
|
68
|
+
/** Id of the panel whose tab is hovered — the canvas spotlights it. */
|
|
69
|
+
hoveredPanelId: string | null
|
|
70
|
+
/** @internal Tabs report pointer enter/leave on their label. */
|
|
71
|
+
setPanelHovered: (id: string, hovered: boolean) => void
|
|
72
|
+
/** @internal Canvas reports its ordered panel ids each render. */
|
|
73
|
+
registerPanelIds: (ids: readonly string[]) => void
|
|
74
|
+
/** @internal Canvas reports its measured width. */
|
|
75
|
+
setCanvasWidth: (width: number) => void
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Owns which panels are hidden as the canvas gains and loses room.
|
|
79
|
+
* Lives in `Scaffold.Root`; Canvas and Tab reach it through context. */
|
|
80
|
+
export function usePanelVisibility(): PanelVisibility {
|
|
81
|
+
const [panelIds, setPanelIds] = React.useState<readonly string[]>([])
|
|
82
|
+
const [measuredCapacity, setMeasuredCapacity] = React.useState<number | null>(
|
|
83
|
+
null
|
|
84
|
+
)
|
|
85
|
+
const [state, setState] = React.useState<PanelVisibilityState>(EMPTY_STATE)
|
|
86
|
+
const [hoveredPanelId, setHoveredPanelId] = React.useState<string | null>(
|
|
87
|
+
null
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
const panelCapacity = measuredCapacity ?? SCAFFOLD_PANEL_CAPACITY
|
|
91
|
+
|
|
92
|
+
const registerPanelIds = React.useCallback((ids: readonly string[]) => {
|
|
93
|
+
setPanelIds((prev) =>
|
|
94
|
+
prev.length === ids.length && prev.every((id, i) => id === ids[i])
|
|
95
|
+
? prev
|
|
96
|
+
: [...ids]
|
|
97
|
+
)
|
|
98
|
+
}, [])
|
|
99
|
+
|
|
100
|
+
// Store the derived capacity, not the raw width — resize storms only
|
|
101
|
+
// re-render the tree when a panel actually gains or loses room.
|
|
102
|
+
const setCanvasWidth = React.useCallback((width: number) => {
|
|
103
|
+
setMeasuredCapacity(getPanelCapacity(width))
|
|
104
|
+
}, [])
|
|
105
|
+
|
|
106
|
+
// Settle during render (the derive-state-while-rendering pattern): ids
|
|
107
|
+
// and capacity arrive as state, so an effect here would only add a
|
|
108
|
+
// cascading render after each change.
|
|
109
|
+
const settled = reconcile(state, panelIds, panelCapacity)
|
|
110
|
+
if (settled !== state) setState(settled)
|
|
111
|
+
|
|
112
|
+
const togglePanelVisibility = React.useCallback(
|
|
113
|
+
(id: string) => {
|
|
114
|
+
setState((prev) => {
|
|
115
|
+
if (!panelIds.includes(id)) return prev
|
|
116
|
+
const hidden =
|
|
117
|
+
prev.autoHidden.includes(id) || prev.userHidden.includes(id)
|
|
118
|
+
if (!hidden && prev.visibleOrder.length <= 1) return prev
|
|
119
|
+
|
|
120
|
+
const next: PanelVisibilityState = hidden
|
|
121
|
+
? {
|
|
122
|
+
// Showing: newest activation — reconcile evicts the oldest
|
|
123
|
+
// visible panel if the canvas can't fit one more.
|
|
124
|
+
visibleOrder: [...prev.visibleOrder, id],
|
|
125
|
+
autoHidden: prev.autoHidden.filter((other) => other !== id),
|
|
126
|
+
userHidden: prev.userHidden.filter((other) => other !== id),
|
|
127
|
+
}
|
|
128
|
+
: {
|
|
129
|
+
visibleOrder: prev.visibleOrder.filter((other) => other !== id),
|
|
130
|
+
autoHidden: prev.autoHidden,
|
|
131
|
+
userHidden: [...prev.userHidden, id],
|
|
132
|
+
}
|
|
133
|
+
return reconcile(next, panelIds, panelCapacity)
|
|
134
|
+
})
|
|
135
|
+
},
|
|
136
|
+
[panelIds, panelCapacity]
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// Leaving clears only its own id — a tab that unmounts mid-hover must
|
|
140
|
+
// not wipe out a hover the pointer has already moved on to.
|
|
141
|
+
const setPanelHovered = React.useCallback((id: string, hovered: boolean) => {
|
|
142
|
+
setHoveredPanelId((prev) => {
|
|
143
|
+
if (hovered) return id
|
|
144
|
+
return prev === id ? null : prev
|
|
145
|
+
})
|
|
146
|
+
}, [])
|
|
147
|
+
|
|
148
|
+
const hiddenPanelIds = React.useMemo(
|
|
149
|
+
() => [...settled.autoHidden, ...settled.userHidden],
|
|
150
|
+
[settled]
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
const isPanelHidden = React.useCallback(
|
|
154
|
+
(id: string) => hiddenPanelIds.includes(id),
|
|
155
|
+
[hiddenPanelIds]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
return React.useMemo(
|
|
159
|
+
() => ({
|
|
160
|
+
panelCapacity,
|
|
161
|
+
hiddenPanelIds,
|
|
162
|
+
isPanelHidden,
|
|
163
|
+
togglePanelVisibility,
|
|
164
|
+
hoveredPanelId,
|
|
165
|
+
setPanelHovered,
|
|
166
|
+
registerPanelIds,
|
|
167
|
+
setCanvasWidth,
|
|
168
|
+
}),
|
|
169
|
+
[
|
|
170
|
+
panelCapacity,
|
|
171
|
+
hiddenPanelIds,
|
|
172
|
+
isPanelHidden,
|
|
173
|
+
togglePanelVisibility,
|
|
174
|
+
hoveredPanelId,
|
|
175
|
+
setPanelHovered,
|
|
176
|
+
registerPanelIds,
|
|
177
|
+
setCanvasWidth,
|
|
178
|
+
]
|
|
179
|
+
)
|
|
180
|
+
}
|