@exegia/corpora-ui 0.22.0 → 0.24.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-flow-block.test.d.ts +1 -0
- package/dist-lib/components/blocks/auth/auth-flow-block.d.ts +123 -0
- package/dist-lib/components/blocks/scaffold/__tests__/scaffold-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/scaffold/index.d.ts +2 -0
- package/dist-lib/components/blocks/scaffold/scaffold-atom.d.ts +97 -0
- package/dist-lib/components/blocks/scaffold/scaffold-context.d.ts +3 -1
- package/dist-lib/components/blocks/scaffold/scaffold-root.d.ts +12 -3
- package/dist-lib/components/blocks/scaffold/type.d.ts +58 -18
- package/dist-lib/components/blocks/scaffold/use-scaffold-state.d.ts +26 -0
- package/dist-lib/components/blocks/scaffold/use-scaffold.d.ts +7 -1
- package/dist-lib/components/blocks/scaffold/utils.d.ts +11 -1
- package/dist-lib/components/ui/switch.d.ts +4 -0
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.js +1683 -1495
- package/dist-lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/beste/piece/browser-frame.tsx +1 -1
- package/src/components/beste/piece/utils.ts +4 -4
- package/src/components/blocks/auth/__tests__/auth-flow-block.test.tsx +183 -0
- package/src/components/blocks/auth/auth-flow-block.tsx +296 -0
- package/src/components/blocks/scaffold/__tests__/scaffold-atom.test.tsx +210 -0
- package/src/components/blocks/scaffold/index.ts +15 -0
- package/src/components/blocks/scaffold/scaffold-actions.tsx +5 -1
- package/src/components/blocks/scaffold/scaffold-atom.ts +383 -0
- package/src/components/blocks/scaffold/scaffold-canvas.tsx +12 -4
- package/src/components/blocks/scaffold/scaffold-context.ts +3 -1
- package/src/components/blocks/scaffold/scaffold-inspector.tsx +9 -2
- package/src/components/blocks/scaffold/scaffold-panel.tsx +24 -10
- package/src/components/blocks/scaffold/scaffold-root.tsx +64 -32
- package/src/components/blocks/scaffold/scaffold-sub-panel.tsx +12 -10
- package/src/components/blocks/scaffold/scaffold-tab.tsx +13 -9
- package/src/components/blocks/scaffold/type.ts +64 -18
- package/src/components/blocks/scaffold/use-scaffold-state.ts +58 -0
- package/src/components/blocks/scaffold/use-scaffold.ts +30 -19
- package/src/components/blocks/scaffold/utils.ts +45 -1
- package/src/components/blocks/shell/shell-layout.tsx +8 -3
- package/src/components/docs/component-preview.tsx +7 -2
- package/src/components/docs/demo-controls.tsx +32 -23
- package/src/components/ui/switch.tsx +30 -0
- package/src/index.ts +1 -0
- package/dist-lib/components/blocks/scaffold/use-panel-visibility.d.ts +0 -19
- package/src/components/blocks/scaffold/use-panel-visibility.ts +0 -180
|
@@ -3,8 +3,14 @@
|
|
|
3
3
|
import { motion, useReducedMotion } from "motion/react"
|
|
4
4
|
import * as React from "react"
|
|
5
5
|
|
|
6
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
7
|
+
|
|
6
8
|
import { cn } from "@/lib/utils"
|
|
7
9
|
import { SCAFFOLD_EASE, SCAFFOLD_EDGE_GUTTER, SCAFFOLD_MORPH_DURATION } from "./constants"
|
|
10
|
+
import {
|
|
11
|
+
scaffoldInspectorOpenAtom,
|
|
12
|
+
toggleScaffoldInspectorAtom,
|
|
13
|
+
} from "./scaffold-atom"
|
|
8
14
|
import { useScaffoldContext } from "./scaffold-context"
|
|
9
15
|
import type { ScaffoldInspectorProps } from "./type"
|
|
10
16
|
import { GlassContainer } from "@/components/ui/glasscn/glass-container.tsx"
|
|
@@ -23,8 +29,9 @@ export function ScaffoldInspector({
|
|
|
23
29
|
className,
|
|
24
30
|
...rest
|
|
25
31
|
}: ScaffoldInspectorProps): React.ReactElement {
|
|
26
|
-
const {
|
|
27
|
-
|
|
32
|
+
const { scaffoldId, inspectorWidth } = useScaffoldContext()
|
|
33
|
+
const inspectorOpen = useAtomValue(scaffoldInspectorOpenAtom(scaffoldId))
|
|
34
|
+
const toggleInspector = useSetAtom(toggleScaffoldInspectorAtom(scaffoldId))
|
|
28
35
|
const reducedMotion = useReducedMotion()
|
|
29
36
|
|
|
30
37
|
// Slide fully past the edge gutter and the drop shadow's blur radius.
|
|
@@ -10,7 +10,10 @@ import {
|
|
|
10
10
|
SCAFFOLD_MORPH_DURATION,
|
|
11
11
|
SCAFFOLD_PANEL_MIN_WIDTH,
|
|
12
12
|
} from "./constants"
|
|
13
|
+
import { useAtomValue } from "jotai"
|
|
14
|
+
|
|
13
15
|
import { PanelMenuButton } from "./panel-menu-button.tsx"
|
|
16
|
+
import { scaffoldPanelDimmedAtom } from "./scaffold-atom"
|
|
14
17
|
import { useScaffoldContext } from "./scaffold-context"
|
|
15
18
|
import type { ScaffoldPanelProps, TSubPanelPosition } from "./type"
|
|
16
19
|
import { ScaffoldSubPanel } from "@/components/blocks/scaffold/scaffold-sub-panel.tsx"
|
|
@@ -33,13 +36,14 @@ export function ScaffoldPanel({
|
|
|
33
36
|
sound = true,
|
|
34
37
|
className,
|
|
35
38
|
}: ScaffoldPanelProps): React.ReactElement {
|
|
36
|
-
const {
|
|
39
|
+
const { scaffoldId } = useScaffoldContext()
|
|
37
40
|
const reducedMotion = useReducedMotion()
|
|
38
41
|
|
|
39
|
-
// A hovered tab spotlights its own panel — every other id'd panel
|
|
40
|
-
//
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
// A hovered tab spotlights its own panel — every other id'd panel fades
|
|
43
|
+
// back so the pairing reads at a glance. The per-panel atom keeps a hover
|
|
44
|
+
// moving between two tabs from re-rendering the panels that stay dimmed;
|
|
45
|
+
// the empty-key sentinel reads false for a panel without an `id`.
|
|
46
|
+
const dimmed = useAtomValue(scaffoldPanelDimmedAtom(scaffoldId, id ?? ""))
|
|
43
47
|
|
|
44
48
|
const transition = {
|
|
45
49
|
duration: reducedMotion ? 0 : SCAFFOLD_MORPH_DURATION,
|
|
@@ -78,7 +82,6 @@ export function ScaffoldPanel({
|
|
|
78
82
|
// floor would overflow it.
|
|
79
83
|
minWidth: id !== undefined ? SCAFFOLD_PANEL_MIN_WIDTH : 0,
|
|
80
84
|
scaleX: 1,
|
|
81
|
-
flexDirection: isSwapped ? "column-reverse" : "column",
|
|
82
85
|
// The hovered tab's panel keeps full opacity; the rest fade back
|
|
83
86
|
// so the tab↔panel pairing reads at a glance. Inline (not a
|
|
84
87
|
// class) because motion owns this element's opacity.
|
|
@@ -89,10 +92,15 @@ export function ScaffoldPanel({
|
|
|
89
92
|
style={{ originX: 0.5 }}
|
|
90
93
|
aria-label={name}
|
|
91
94
|
className={cn(
|
|
92
|
-
"group/panel relative flex min-w-0 flex-1
|
|
95
|
+
"group/panel relative flex w-full min-w-0 flex-1",
|
|
96
|
+
// Swap flips the direction as a class, NOT an animate target:
|
|
97
|
+
// flex-direction is discrete, so motion would apply it instantly —
|
|
98
|
+
// the flip lands in one commit and the children's `layout` FLIP is
|
|
99
|
+
// what glides the cards past each other.
|
|
100
|
+
isSwapped ? "flex-col-reverse" : "flex-col",
|
|
93
101
|
className
|
|
94
102
|
)}
|
|
95
|
-
layout
|
|
103
|
+
layout
|
|
96
104
|
data-slot="scaffold-panel"
|
|
97
105
|
data-dimmed={dimmed ? "" : undefined}
|
|
98
106
|
data-swapped={isSwapped ? "" : undefined}
|
|
@@ -102,7 +110,13 @@ export function ScaffoldPanel({
|
|
|
102
110
|
{children}
|
|
103
111
|
</ScaffoldSubPanel>
|
|
104
112
|
{(onSwap || onCloseSecondary) && SecondaryPanel && (
|
|
105
|
-
|
|
113
|
+
// `layout` keeps the seam glued to the moving boundary while the
|
|
114
|
+
// cards trade places around it.
|
|
115
|
+
<motion.div
|
|
116
|
+
layout
|
|
117
|
+
transition={transition}
|
|
118
|
+
className="relative flex h-2 items-center justify-center"
|
|
119
|
+
>
|
|
106
120
|
<PanelMenuButton
|
|
107
121
|
label={swapLabel}
|
|
108
122
|
onClick={handleSwap}
|
|
@@ -112,7 +126,7 @@ export function ScaffoldPanel({
|
|
|
112
126
|
swapped={isSwapped}
|
|
113
127
|
sound={sound}
|
|
114
128
|
/>
|
|
115
|
-
</div>
|
|
129
|
+
</motion.div>
|
|
116
130
|
)}
|
|
117
131
|
{SecondaryPanel && (
|
|
118
132
|
<ScaffoldSubPanel id="bottom" expanded={subPanelPosition === "bottom"}>
|
|
@@ -1,20 +1,41 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
+
import { useSetAtom } from "jotai"
|
|
4
5
|
|
|
5
6
|
import { cn } from "@/lib/utils"
|
|
6
7
|
import { SCAFFOLD_INSPECTOR_WIDTH } from "./constants"
|
|
8
|
+
import {
|
|
9
|
+
projectScaffoldPropsAtom,
|
|
10
|
+
removeScaffoldInstance,
|
|
11
|
+
seedScaffoldInspectorAtom,
|
|
12
|
+
setScaffoldHandlersAtom,
|
|
13
|
+
} from "./scaffold-atom"
|
|
7
14
|
import { ScaffoldContext } from "./scaffold-context"
|
|
8
|
-
import {
|
|
9
|
-
|
|
15
|
+
import type {
|
|
16
|
+
ScaffoldConfig,
|
|
17
|
+
ScaffoldContextValue,
|
|
18
|
+
ScaffoldHandlers,
|
|
19
|
+
ScaffoldRootProps,
|
|
20
|
+
} from "./type"
|
|
10
21
|
import type { ClassNameValue } from "tailwind-merge"
|
|
11
22
|
|
|
12
23
|
/**
|
|
13
24
|
* The scaffold's viewport: desktop backdrop + horizontal row of rail and
|
|
14
|
-
* main region, and the
|
|
15
|
-
* container — size it from the outside (e.g.
|
|
25
|
+
* main region, and the binding between this instance's props and its slice
|
|
26
|
+
* of the store. Fills its container — size it from the outside (e.g.
|
|
27
|
+
* `h-svh` for a full page).
|
|
28
|
+
*
|
|
29
|
+
* State lives in the store under `scaffoldId`; the context carries only the
|
|
30
|
+
* id and the drawer width, so its value never changes while the scaffold is
|
|
31
|
+
* mounted and parts subscribe to exactly the atoms they render from. A
|
|
32
|
+
* controlled `inspectorOpen` stays the source of truth: it is projected
|
|
33
|
+
* one-way into the store (so remote readers see current data), and the
|
|
34
|
+
* inspector actions report through `onInspectorOpenChange` instead of
|
|
35
|
+
* writing.
|
|
16
36
|
*/
|
|
17
37
|
export function ScaffoldRoot({
|
|
38
|
+
scaffoldId: scaffoldIdProp,
|
|
18
39
|
inspectorOpen: inspectorOpenProp,
|
|
19
40
|
defaultInspectorOpen,
|
|
20
41
|
onInspectorOpenChange,
|
|
@@ -23,42 +44,53 @@ export function ScaffoldRoot({
|
|
|
23
44
|
children,
|
|
24
45
|
...rest
|
|
25
46
|
}: ScaffoldRootProps): React.ReactElement {
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
)
|
|
29
|
-
const inspectorOpen = inspectorOpenProp ?? uncontrolledOpen
|
|
47
|
+
const generatedId = React.useId()
|
|
48
|
+
const scaffoldId = scaffoldIdProp ?? generatedId
|
|
30
49
|
const background: ClassNameValue = `bg-linear-to-tr/increasing from-neutral-200 via-neutral-100 to-stone-200 dark:from-neutral-900 dark:via-neutral-950 dark:to-stone-950`
|
|
31
50
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
51
|
+
const controlsInspector = inspectorOpenProp !== undefined
|
|
52
|
+
const config = React.useMemo<ScaffoldConfig>(
|
|
53
|
+
() => ({ controlsInspector }),
|
|
54
|
+
[controlsInspector]
|
|
55
|
+
)
|
|
56
|
+
const handlers = React.useMemo<ScaffoldHandlers>(
|
|
57
|
+
() => ({ onInspectorOpenChange }),
|
|
58
|
+
[onInspectorOpenChange]
|
|
38
59
|
)
|
|
39
60
|
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
61
|
+
const project = useSetAtom(projectScaffoldPropsAtom(scaffoldId))
|
|
62
|
+
const publishHandlers = useSetAtom(setScaffoldHandlersAtom(scaffoldId))
|
|
63
|
+
const seedInspector = useSetAtom(seedScaffoldInspectorAtom(scaffoldId))
|
|
64
|
+
|
|
65
|
+
// Seed the uncontrolled inspector once per mount, before the projection —
|
|
66
|
+
// `defaultInspectorOpen` describes the mount, not every render. Layout
|
|
67
|
+
// effects so children read settled values in the same commit; primitive
|
|
68
|
+
// deps, so no loop guard is needed.
|
|
69
|
+
const [seed] = React.useState(
|
|
70
|
+
() => inspectorOpenProp ?? defaultInspectorOpen ?? false
|
|
43
71
|
)
|
|
72
|
+
React.useLayoutEffect(() => {
|
|
73
|
+
if (!controlsInspector) seedInspector(seed)
|
|
74
|
+
// Mount-time seed only.
|
|
75
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
76
|
+
}, [])
|
|
77
|
+
// Handlers before the projection — they must be in the store before any
|
|
78
|
+
// action can fire.
|
|
79
|
+
React.useLayoutEffect(() => {
|
|
80
|
+
publishHandlers(handlers)
|
|
81
|
+
}, [publishHandlers, handlers])
|
|
82
|
+
React.useLayoutEffect(() => {
|
|
83
|
+
project(config, inspectorOpenProp)
|
|
84
|
+
}, [project, config, inspectorOpenProp])
|
|
44
85
|
|
|
45
|
-
|
|
86
|
+
React.useEffect(() => {
|
|
87
|
+
if (scaffoldIdProp !== undefined) return
|
|
88
|
+
return () => removeScaffoldInstance(scaffoldId)
|
|
89
|
+
}, [scaffoldIdProp, scaffoldId])
|
|
46
90
|
|
|
47
91
|
const value = React.useMemo<ScaffoldContextValue>(
|
|
48
|
-
() => ({
|
|
49
|
-
|
|
50
|
-
inspectorWidth,
|
|
51
|
-
setInspectorOpen,
|
|
52
|
-
toggleInspector,
|
|
53
|
-
...panelVisibility,
|
|
54
|
-
}),
|
|
55
|
-
[
|
|
56
|
-
inspectorOpen,
|
|
57
|
-
inspectorWidth,
|
|
58
|
-
setInspectorOpen,
|
|
59
|
-
toggleInspector,
|
|
60
|
-
panelVisibility,
|
|
61
|
-
]
|
|
92
|
+
() => ({ scaffoldId, inspectorWidth }),
|
|
93
|
+
[scaffoldId, inspectorWidth]
|
|
62
94
|
)
|
|
63
95
|
|
|
64
96
|
return (
|
|
@@ -1,26 +1,28 @@
|
|
|
1
|
-
|
|
2
1
|
import * as React from "react"
|
|
3
|
-
import { motion, useReducedMotion } from "
|
|
2
|
+
import { motion, useReducedMotion } from "motion/react"
|
|
4
3
|
import { cn } from "@/lib/utils"
|
|
5
4
|
import type { ScaffoldSubPanelProps } from "./type"
|
|
6
5
|
import { subPanelVariant } from "@/components/blocks/scaffold/utils.ts"
|
|
7
6
|
import { SCAFFOLD_EASE, SCAFFOLD_MORPH_DURATION } from "./constants"
|
|
8
7
|
|
|
9
8
|
export function ScaffoldSubPanel({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
className,
|
|
10
|
+
children,
|
|
11
|
+
variant = "card",
|
|
12
|
+
primary = false,
|
|
13
|
+
expanded,
|
|
14
|
+
...rest
|
|
15
|
+
}: ScaffoldSubPanelProps): React.ReactElement {
|
|
17
16
|
const reduce = useReducedMotion()
|
|
18
17
|
// The primary card holds the flexible slot unless the panel says otherwise.
|
|
19
18
|
const isExpanded = expanded ?? primary
|
|
20
19
|
|
|
21
20
|
return (
|
|
22
21
|
<motion.div
|
|
23
|
-
layout=
|
|
22
|
+
// Full `layout` (position + size), not `layout="size"`: a swap flips
|
|
23
|
+
// the panel's flex direction in a single commit, and only a position
|
|
24
|
+
// FLIP makes the two cards glide past each other instead of teleport.
|
|
25
|
+
layout
|
|
24
26
|
id={`scaffold-sub-panel-${variant}`}
|
|
25
27
|
// Grow is animated as a style, not a class flip — a class flip rides the
|
|
26
28
|
// layout FLIP, which scales both cards into a swap-like morph. Animating
|
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
import { useReducedMotion } from "motion/react"
|
|
4
4
|
import * as React from "react"
|
|
5
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
5
6
|
|
|
6
7
|
import { cn } from "@/lib/utils"
|
|
7
8
|
import { SPRING_LAYOUT } from "@/lib/ease.ts"
|
|
9
|
+
import {
|
|
10
|
+
scaffoldPanelHiddenAtom,
|
|
11
|
+
setScaffoldPanelHoveredAtom,
|
|
12
|
+
toggleScaffoldPanelAtom,
|
|
13
|
+
} from "./scaffold-atom"
|
|
8
14
|
import { useScaffoldContext } from "./scaffold-context"
|
|
9
15
|
import type { ScaffoldTabProps } from "./type"
|
|
10
16
|
import { segmentVariants } from "./utils"
|
|
@@ -32,13 +38,15 @@ export function ScaffoldTab({
|
|
|
32
38
|
className,
|
|
33
39
|
...rest
|
|
34
40
|
}: ScaffoldTabProps): React.ReactElement {
|
|
35
|
-
const {
|
|
36
|
-
|
|
41
|
+
const { scaffoldId } = useScaffoldContext()
|
|
42
|
+
// The per-panel atom keeps this tab still while its siblings toggle; the
|
|
43
|
+
// empty-key sentinel reads false for a tab without a `panelId`.
|
|
44
|
+
const hidden = useAtomValue(scaffoldPanelHiddenAtom(scaffoldId, panelId ?? ""))
|
|
45
|
+
const togglePanel = useSetAtom(toggleScaffoldPanelAtom(scaffoldId))
|
|
46
|
+
const setPanelHovered = useSetAtom(setScaffoldPanelHoveredAtom(scaffoldId))
|
|
37
47
|
const reduce = useReducedMotion()
|
|
38
48
|
const transition = reduce ? { duration: 0 } : SPRING_LAYOUT
|
|
39
49
|
|
|
40
|
-
const hidden = panelId !== undefined && isPanelHidden(panelId)
|
|
41
|
-
|
|
42
50
|
// A tab closed mid-hover never fires pointer-leave — release the
|
|
43
51
|
// spotlight it holds so the canvas doesn't stay dimmed.
|
|
44
52
|
React.useEffect(() => {
|
|
@@ -70,11 +78,7 @@ export function ScaffoldTab({
|
|
|
70
78
|
>
|
|
71
79
|
<Button
|
|
72
80
|
aria-pressed={panelId !== undefined ? !hidden : undefined}
|
|
73
|
-
onClick={
|
|
74
|
-
panelId !== undefined
|
|
75
|
-
? () => togglePanelVisibility(panelId)
|
|
76
|
-
: undefined
|
|
77
|
-
}
|
|
81
|
+
onClick={panelId !== undefined ? () => togglePanel(panelId) : undefined}
|
|
78
82
|
size="sm"
|
|
79
83
|
sound={sound}
|
|
80
84
|
variant="ghost"
|
|
@@ -1,37 +1,77 @@
|
|
|
1
1
|
import React, { type ComponentProps, type ReactElement, type ReactNode, } from "react"
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/** Key of one scaffold's slice of the store. */
|
|
4
|
+
export type ScaffoldInstanceId = string
|
|
5
|
+
|
|
6
|
+
/** The identity every scaffold part shares through ScaffoldContext. State
|
|
7
|
+
* itself lives in the store — parts subscribe to the atoms they render from,
|
|
8
|
+
* so this value never changes identity while the scaffold is mounted. */
|
|
4
9
|
export interface ScaffoldContextValue {
|
|
5
|
-
|
|
6
|
-
inspectorOpen: boolean
|
|
10
|
+
scaffoldId: ScaffoldInstanceId
|
|
7
11
|
/** Drawer width in px — Actions reads it to slide out of the drawer's way. */
|
|
8
12
|
inspectorWidth: number
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** One scaffold's whole state, as `useScaffoldState` returns it. A component
|
|
16
|
+
* that watches one field should subscribe to that field's atom instead. */
|
|
17
|
+
export interface ScaffoldState {
|
|
18
|
+
/** Whether the inspector drawer is currently shown. */
|
|
19
|
+
inspectorOpen: boolean
|
|
11
20
|
/** How many panels currently fit side by side, from the measured canvas
|
|
12
21
|
* width at `SCAFFOLD_PANEL_MIN_WIDTH` per panel (1..`SCAFFOLD_PANEL_CAPACITY`). */
|
|
13
22
|
panelCapacity: number
|
|
14
23
|
/** Ids of id'd panels the scaffold is currently hiding — auto-hidden by
|
|
15
24
|
* capacity pressure or toggled away from a tab. */
|
|
16
25
|
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
26
|
/** Id of the panel whose tab the pointer is resting on — the canvas
|
|
22
27
|
* spotlights it by fading every other id'd panel to 0.35 opacity. */
|
|
23
28
|
hoveredPanelId: string | null
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Drive a scaffold by id from anywhere, as `useScaffoldActions` returns it. */
|
|
32
|
+
export interface ScaffoldStateActions {
|
|
33
|
+
setInspectorOpen: (open: boolean) => void
|
|
34
|
+
toggleInspector: () => void
|
|
35
|
+
/** Show/hide an id'd panel. Showing past capacity auto-hides the
|
|
36
|
+
* least-recently-activated visible panel; the last visible one never hides. */
|
|
37
|
+
togglePanel: (panelId: string) => void
|
|
38
|
+
reset: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @internal Which props currently control the scaffold — write gates for
|
|
42
|
+
* the action atoms, so the store never overwrites a prop. */
|
|
43
|
+
export interface ScaffoldConfig {
|
|
44
|
+
controlsInspector: boolean
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** @internal The mounted root's callbacks, published into the store so
|
|
48
|
+
* action atoms can report changes wherever they were fired from. */
|
|
49
|
+
export interface ScaffoldHandlers {
|
|
50
|
+
onInspectorOpenChange?: (open: boolean) => void
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** @internal Bookkeeping for responsive panel hiding.
|
|
54
|
+
* - `visibleOrder`: visible panel ids, least-recently-activated first — the
|
|
55
|
+
* front is what capacity pressure evicts next.
|
|
56
|
+
* - `autoHidden`: panels the scaffold hid because the canvas shrank, most
|
|
57
|
+
* recent last. They return on their own when room comes back.
|
|
58
|
+
* - `userHidden`: panels hidden by a tab press. They stay hidden until the
|
|
59
|
+
* tab is pressed again, however wide the canvas grows. */
|
|
60
|
+
export interface ScaffoldPanelVisibility {
|
|
61
|
+
visibleOrder: string[]
|
|
62
|
+
autoHidden: string[]
|
|
63
|
+
userHidden: string[]
|
|
30
64
|
}
|
|
31
65
|
|
|
32
66
|
export interface ScaffoldRootProps extends ComponentProps<"div"> {
|
|
33
|
-
/**
|
|
34
|
-
*
|
|
67
|
+
/** Names this scaffold's slice of the store, so `useScaffoldState` /
|
|
68
|
+
* `useScaffoldActions` can drive it by id and its state outlives the
|
|
69
|
+
* component. Omitted, the root keys off `useId` and drops its state on
|
|
70
|
+
* unmount. `useScaffold().providerProps` carries one. */
|
|
71
|
+
scaffoldId?: ScaffoldInstanceId
|
|
72
|
+
/** Controlled inspector state — pair with `onInspectorOpenChange`. The
|
|
73
|
+
* prop stays the source of truth: store writes are gated off, and actions
|
|
74
|
+
* report through the callback instead. */
|
|
35
75
|
inspectorOpen?: boolean
|
|
36
76
|
/** Initial inspector state when uncontrolled. Closed by default. */
|
|
37
77
|
defaultInspectorOpen?: boolean
|
|
@@ -182,6 +222,9 @@ export interface PanelMenuButtonProps extends PanelFloatingButtonProps {
|
|
|
182
222
|
}
|
|
183
223
|
|
|
184
224
|
export interface UseScaffoldOptions {
|
|
225
|
+
/** Names the scaffold's slice of the store. Omitted, the hook generates
|
|
226
|
+
* one and drops its state on unmount. */
|
|
227
|
+
scaffoldId?: ScaffoldInstanceId
|
|
185
228
|
/** Initial inspector state. Closed by default. */
|
|
186
229
|
defaultInspectorOpen?: boolean
|
|
187
230
|
/** Fires on every inspector open/close. */
|
|
@@ -189,12 +232,15 @@ export interface UseScaffoldOptions {
|
|
|
189
232
|
}
|
|
190
233
|
|
|
191
234
|
export interface ScaffoldControls {
|
|
235
|
+
/** The id the hook and the root share — hand it to `useScaffoldState` /
|
|
236
|
+
* `useScaffoldActions` to drive the scaffold from elsewhere. */
|
|
237
|
+
scaffoldId: ScaffoldInstanceId
|
|
192
238
|
inspectorOpen: boolean
|
|
193
239
|
setInspectorOpen: (open: boolean) => void
|
|
194
240
|
toggleInspector: () => void
|
|
195
241
|
/** Spread onto Scaffold.Root. */
|
|
196
242
|
providerProps: Pick<
|
|
197
243
|
ScaffoldRootProps,
|
|
198
|
-
"
|
|
244
|
+
"scaffoldId" | "defaultInspectorOpen" | "onInspectorOpenChange"
|
|
199
245
|
>
|
|
200
246
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react"
|
|
4
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
resetScaffoldAtom,
|
|
8
|
+
scaffoldStateAtom,
|
|
9
|
+
setScaffoldInspectorOpenAtom,
|
|
10
|
+
toggleScaffoldInspectorAtom,
|
|
11
|
+
toggleScaffoldPanelAtom,
|
|
12
|
+
} from "./scaffold-atom"
|
|
13
|
+
import type {
|
|
14
|
+
ScaffoldInstanceId,
|
|
15
|
+
ScaffoldState,
|
|
16
|
+
ScaffoldStateActions,
|
|
17
|
+
} from "./type"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read the scaffold registered under `scaffoldId` from anywhere below
|
|
21
|
+
* `ExegiaProvider` — no controller, no props, no provider of its own.
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { inspectorOpen, hiddenPanelIds } = useScaffoldState("workspace")
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* This returns the whole state object, so the caller re-renders on every
|
|
28
|
+
* change, hovers included. A component that reads one field should subscribe
|
|
29
|
+
* to that field's atom instead:
|
|
30
|
+
* `useAtomValue(scaffoldInspectorOpenAtom("workspace"))`.
|
|
31
|
+
*/
|
|
32
|
+
export function useScaffoldState(scaffoldId: ScaffoldInstanceId): ScaffoldState {
|
|
33
|
+
return useAtomValue(scaffoldStateAtom(scaffoldId))
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Drive the scaffold registered under `scaffoldId` from anywhere. Writes
|
|
38
|
+
* only — the caller never re-renders when the scaffold changes, so this is
|
|
39
|
+
* what a command palette or a keyboard shortcut should reach for.
|
|
40
|
+
*
|
|
41
|
+
* ```tsx
|
|
42
|
+
* const scaffold = useScaffoldActions("workspace")
|
|
43
|
+
* <Button onClick={scaffold.toggleInspector}>Inspect</Button>
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useScaffoldActions(
|
|
47
|
+
scaffoldId: ScaffoldInstanceId
|
|
48
|
+
): ScaffoldStateActions {
|
|
49
|
+
const setInspectorOpen = useSetAtom(setScaffoldInspectorOpenAtom(scaffoldId))
|
|
50
|
+
const toggleInspector = useSetAtom(toggleScaffoldInspectorAtom(scaffoldId))
|
|
51
|
+
const togglePanel = useSetAtom(toggleScaffoldPanelAtom(scaffoldId))
|
|
52
|
+
const reset = useSetAtom(resetScaffoldAtom(scaffoldId))
|
|
53
|
+
|
|
54
|
+
return useMemo(
|
|
55
|
+
() => ({ setInspectorOpen, toggleInspector, togglePanel, reset }),
|
|
56
|
+
[setInspectorOpen, toggleInspector, togglePanel, reset]
|
|
57
|
+
)
|
|
58
|
+
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useEffect, useId, useMemo } from "react"
|
|
4
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
4
5
|
|
|
6
|
+
import {
|
|
7
|
+
removeScaffoldInstance,
|
|
8
|
+
scaffoldInspectorOpenAtom,
|
|
9
|
+
setScaffoldInspectorOpenAtom,
|
|
10
|
+
toggleScaffoldInspectorAtom,
|
|
11
|
+
} from "./scaffold-atom"
|
|
5
12
|
import type { ScaffoldControls, UseScaffoldOptions } from "./type"
|
|
6
13
|
|
|
7
14
|
/**
|
|
@@ -9,37 +16,41 @@ import type { ScaffoldControls, UseScaffoldOptions } from "./type"
|
|
|
9
16
|
* (title-bar buttons, command palette, shortcuts). Spread the returned
|
|
10
17
|
* `providerProps` onto `Scaffold.Root`; without this hook the root manages
|
|
11
18
|
* the same state internally via `defaultInspectorOpen`.
|
|
19
|
+
*
|
|
20
|
+
* The hook and the root meet in the store: `providerProps` carries a
|
|
21
|
+
* `scaffoldId`, both sides read and write that instance's atoms, and
|
|
22
|
+
* `useScaffoldState` / `useScaffoldActions` reach the same slice from
|
|
23
|
+
* anywhere else by id. Unnamed scaffolds key off `useId` and are dropped on
|
|
24
|
+
* unmount; an explicit `scaffoldId` outlives its component.
|
|
12
25
|
*/
|
|
13
26
|
export function useScaffold({
|
|
27
|
+
scaffoldId: scaffoldIdProp,
|
|
14
28
|
defaultInspectorOpen,
|
|
15
29
|
onInspectorChange,
|
|
16
30
|
}: UseScaffoldOptions = {}): ScaffoldControls {
|
|
17
|
-
const
|
|
31
|
+
const generatedId = useId()
|
|
32
|
+
const scaffoldId = scaffoldIdProp ?? generatedId
|
|
18
33
|
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (prev !== open) onInspectorChange?.(open)
|
|
23
|
-
return open
|
|
24
|
-
})
|
|
25
|
-
},
|
|
26
|
-
[onInspectorChange]
|
|
27
|
-
)
|
|
34
|
+
const inspectorOpen = useAtomValue(scaffoldInspectorOpenAtom(scaffoldId))
|
|
35
|
+
const setInspectorOpen = useSetAtom(setScaffoldInspectorOpenAtom(scaffoldId))
|
|
36
|
+
const toggleInspector = useSetAtom(toggleScaffoldInspectorAtom(scaffoldId))
|
|
28
37
|
|
|
29
|
-
|
|
30
|
-
(
|
|
31
|
-
|
|
32
|
-
)
|
|
38
|
+
useEffect(() => {
|
|
39
|
+
if (scaffoldIdProp !== undefined) return
|
|
40
|
+
return () => removeScaffoldInstance(scaffoldId)
|
|
41
|
+
}, [scaffoldIdProp, scaffoldId])
|
|
33
42
|
|
|
34
|
-
const providerProps = useMemo(
|
|
43
|
+
const providerProps = useMemo<ScaffoldControls["providerProps"]>(
|
|
35
44
|
() => ({
|
|
36
|
-
|
|
37
|
-
|
|
45
|
+
scaffoldId,
|
|
46
|
+
defaultInspectorOpen,
|
|
47
|
+
onInspectorOpenChange: onInspectorChange,
|
|
38
48
|
}),
|
|
39
|
-
[
|
|
49
|
+
[scaffoldId, defaultInspectorOpen, onInspectorChange]
|
|
40
50
|
)
|
|
41
51
|
|
|
42
52
|
return {
|
|
53
|
+
scaffoldId,
|
|
43
54
|
inspectorOpen,
|
|
44
55
|
setInspectorOpen,
|
|
45
56
|
toggleInspector,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { Variants } from "motion/react"
|
|
2
2
|
import type { ClassNameValue } from "tailwind-merge"
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ScaffoldPanelVisibility,
|
|
5
|
+
TSubPanelVariant,
|
|
6
|
+
} from "@/components/blocks/scaffold/type.ts"
|
|
4
7
|
import { EASE_OUT } from "@/lib/ease.ts"
|
|
5
8
|
import {
|
|
6
9
|
SCAFFOLD_CANVAS_GAP,
|
|
@@ -22,6 +25,47 @@ export function getPanelCapacity(width: number | null): number {
|
|
|
22
25
|
return Math.max(1, Math.min(SCAFFOLD_PANEL_CAPACITY, fits))
|
|
23
26
|
}
|
|
24
27
|
|
|
28
|
+
/** @internal An untracked scaffold: nothing registered, nothing hidden. */
|
|
29
|
+
export const EMPTY_SCAFFOLD_VISIBILITY: ScaffoldPanelVisibility = {
|
|
30
|
+
visibleOrder: [],
|
|
31
|
+
autoHidden: [],
|
|
32
|
+
userHidden: [],
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @internal Fold the registered ids and current capacity into a settled
|
|
37
|
+
* visibility: prune departed panels, admit new ones, evict the
|
|
38
|
+
* least-recently-activated visible panels past capacity, and restore
|
|
39
|
+
* auto-hidden ones when room returns. Returns `prev` untouched when nothing
|
|
40
|
+
* changes, so callers can compare identities instead of contents.
|
|
41
|
+
*/
|
|
42
|
+
export function reconcileVisibility(
|
|
43
|
+
prev: ScaffoldPanelVisibility,
|
|
44
|
+
ids: readonly string[],
|
|
45
|
+
capacity: number
|
|
46
|
+
): ScaffoldPanelVisibility {
|
|
47
|
+
const known = new Set(ids)
|
|
48
|
+
const visibleOrder = prev.visibleOrder.filter((id) => known.has(id))
|
|
49
|
+
const autoHidden = prev.autoHidden.filter((id) => known.has(id))
|
|
50
|
+
const userHidden = prev.userHidden.filter((id) => known.has(id))
|
|
51
|
+
|
|
52
|
+
const tracked = new Set([...visibleOrder, ...autoHidden, ...userHidden])
|
|
53
|
+
for (const id of ids) if (!tracked.has(id)) visibleOrder.push(id)
|
|
54
|
+
|
|
55
|
+
while (visibleOrder.length > capacity && visibleOrder.length > 1) {
|
|
56
|
+
autoHidden.push(visibleOrder.shift() as string)
|
|
57
|
+
}
|
|
58
|
+
while (visibleOrder.length < capacity && autoHidden.length > 0) {
|
|
59
|
+
visibleOrder.push(autoHidden.pop() as string)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const unchanged =
|
|
63
|
+
visibleOrder.join(" ") === prev.visibleOrder.join(" ") &&
|
|
64
|
+
autoHidden.join(" ") === prev.autoHidden.join(" ") &&
|
|
65
|
+
userHidden.join(" ") === prev.userHidden.join(" ")
|
|
66
|
+
return unchanged ? prev : { visibleOrder, autoHidden, userHidden }
|
|
67
|
+
}
|
|
68
|
+
|
|
25
69
|
/** Desktop backdrop the whole scaffold sits on — a soft warm-gray wash. */
|
|
26
70
|
export const scaffoldBackgroundClass: ClassNameValue =
|
|
27
71
|
"bg-linear-to-tr/increasing from-neutral-400 via-stone-200 to-neutral-300 dark:from-neutral-900 dark:via-neutral-950 dark:to-stone-950"
|