@exegia/corpora-ui 0.18.0 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist-lib/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.d.ts +1 -0
  2. package/dist-lib/components/blocks/nav/sidebar/ai-sidebar.d.ts +11 -2
  3. package/dist-lib/components/blocks/nav/sidebar/type.d.ts +83 -1
  4. package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +22 -31
  5. package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +5 -0
  6. package/dist-lib/components/blocks/scaffold/constants.d.ts +8 -0
  7. package/dist-lib/components/blocks/scaffold/index.d.ts +3 -1
  8. package/dist-lib/components/blocks/scaffold/panel-menu-button.d.ts +4 -3
  9. package/dist-lib/components/blocks/scaffold/scaffold-actions.d.ts +5 -4
  10. package/dist-lib/components/blocks/scaffold/scaffold-canvas.d.ts +2 -0
  11. package/dist-lib/components/blocks/scaffold/scaffold-panel.d.ts +1 -1
  12. package/dist-lib/components/blocks/scaffold/scaffold-tab.d.ts +14 -0
  13. package/dist-lib/components/blocks/scaffold/type.d.ts +58 -28
  14. package/dist-lib/components/blocks/scaffold/use-panel-visibility.d.ts +19 -0
  15. package/dist-lib/components/blocks/scaffold/utils.d.ts +12 -0
  16. package/dist-lib/components/composed/tree/__tests__/tree.test.d.ts +1 -0
  17. package/dist-lib/components/composed/tree/__tests__/use-tree.test.d.ts +1 -0
  18. package/dist-lib/components/composed/tree/constants.d.ts +35 -0
  19. package/dist-lib/components/composed/tree/index.d.ts +4 -0
  20. package/dist-lib/components/composed/tree/tree-context.d.ts +4 -0
  21. package/dist-lib/components/composed/tree/tree-node.d.ts +7 -0
  22. package/dist-lib/components/composed/tree/tree.d.ts +19 -0
  23. package/dist-lib/components/composed/tree/type.d.ts +192 -0
  24. package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +7 -0
  25. package/dist-lib/components/composed/tree/use-tree.d.ts +20 -0
  26. package/dist-lib/components/composed/tree/utils.d.ts +28 -0
  27. package/dist-lib/components/user-avatar/audio-wave.d.ts +2 -0
  28. package/dist-lib/components/user-avatar/component.d.ts +10 -0
  29. package/dist-lib/components/user-avatar/fallback.d.ts +5 -0
  30. package/dist-lib/components/user-avatar/index.d.ts +13 -0
  31. package/dist-lib/components/user-avatar/type.d.ts +26 -0
  32. package/dist-lib/components/user-avatar/utils.d.ts +2 -0
  33. package/dist-lib/index.d.ts +1 -0
  34. package/dist-lib/index.js +2309 -1285
  35. package/dist-lib/index.js.map +1 -1
  36. package/package.json +2 -1
  37. package/src/components/beste/piece/browser-frame.tsx +9 -6
  38. package/src/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.tsx +310 -0
  39. package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +90 -60
  40. package/src/components/blocks/nav/sidebar/type.ts +105 -1
  41. package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +293 -116
  42. package/src/components/blocks/nav/sidebar/utils.ts +28 -0
  43. package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +268 -26
  44. package/src/components/blocks/scaffold/constants.ts +11 -0
  45. package/src/components/blocks/scaffold/index.ts +4 -1
  46. package/src/components/blocks/scaffold/panel-menu-button.tsx +96 -115
  47. package/src/components/blocks/scaffold/scaffold-actions.tsx +41 -50
  48. package/src/components/blocks/scaffold/scaffold-canvas.tsx +48 -5
  49. package/src/components/blocks/scaffold/scaffold-inspector.tsx +27 -3
  50. package/src/components/blocks/scaffold/scaffold-main.tsx +1 -1
  51. package/src/components/blocks/scaffold/scaffold-panel.tsx +56 -30
  52. package/src/components/blocks/scaffold/scaffold-root.tsx +18 -3
  53. package/src/components/blocks/scaffold/scaffold-sidebar.tsx +1 -1
  54. package/src/components/blocks/scaffold/scaffold-tab.tsx +104 -0
  55. package/src/components/blocks/scaffold/type.ts +62 -33
  56. package/src/components/blocks/scaffold/use-panel-visibility.ts +180 -0
  57. package/src/components/blocks/scaffold/utils.ts +35 -0
  58. package/src/components/composed/tree/__tests__/tree.test.tsx +400 -0
  59. package/src/components/composed/tree/__tests__/use-tree.test.tsx +332 -0
  60. package/src/components/composed/tree/constants.ts +60 -0
  61. package/src/components/composed/tree/index.ts +13 -0
  62. package/src/components/composed/tree/tree-context.ts +13 -0
  63. package/src/components/composed/tree/tree-node.tsx +451 -0
  64. package/src/components/composed/tree/tree.tsx +278 -0
  65. package/src/components/composed/tree/type.ts +212 -0
  66. package/src/components/composed/tree/use-tree-dnd.ts +148 -0
  67. package/src/components/composed/tree/use-tree.ts +306 -0
  68. package/src/components/composed/tree/utils.ts +172 -0
  69. package/src/components/composed/user-avatar.tsx +4 -1
  70. package/src/components/docs/component-preview.tsx +11 -11
  71. package/src/components/user-avatar/audio-wave.tsx +22 -0
  72. package/src/components/user-avatar/component.tsx +53 -0
  73. package/src/components/user-avatar/fallback.tsx +26 -0
  74. package/src/components/user-avatar/index.ts +15 -0
  75. package/src/components/user-avatar/type.ts +35 -0
  76. package/src/components/user-avatar/utils.ts +8 -0
  77. package/src/index.css +211 -153
  78. package/src/index.ts +1 -0
@@ -2,61 +2,45 @@
2
2
 
3
3
  import * as React from "react"
4
4
 
5
- import {
6
- AnimatePresence,
7
- motion,
8
- useReducedMotion,
9
- type Variants,
10
- } from "motion/react"
11
5
  import { More } from "iconsax-reactjs"
12
- import { LucideArrowUpDown, LucideChevronDown } from "lucide-react"
6
+ import { LucideArrowUpDown, LucideChevronDown, LucideX } from "lucide-react"
13
7
 
14
8
  import { cn } from "@/lib/utils"
15
9
  import type { PanelMenuButtonProps } from "./type"
16
10
  import { floatingButtonClass, revealOnPanelHoverClass } from "./utils"
17
- import { Button } from "@/components/ui/button";
11
+ import { Button } from "@/components/ui/button"
18
12
  import { GlassButtonGroup } from "@/components/ui/glasscn/glass-button-group.tsx"
19
- import { EASE_OUT, SPRING_LAYOUT } from "@/lib/ease.ts"
20
13
  import { useHoverCapable } from "@/lib/hooks/use-hover-capable"
21
14
  import { SCAFFOLD_MENU_COLLAPSE_DELAY } from "./constants"
22
15
 
23
- /** Menu segments blur/fade in place while the track morphs around them. The
24
- * exit is a fast tween, not the layout spring — the track stays
25
- * overflow-visible, so the popped-out buttons must be gone before the
26
- * collapsing pill leaves them behind as a ghost. */
27
- const MENU_SEGMENT_VARIANTS: Variants = {
28
- hidden: { opacity: 0, filter: "blur(4px)" },
29
- visible: { opacity: 1, filter: "blur(0px)" },
30
- exit: {
31
- opacity: 0,
32
- filter: "blur(4px)",
33
- transition: { duration: 0.18, ease: EASE_OUT },
34
- },
35
- }
16
+ /** One side of the seam menu. Both sides stay mounted; the pill morphs by
17
+ * collapsing one side's grid column to 0fr while the other grows — pure
18
+ * CSS, driven by `data-expanded` on the group. The hidden side is inert
19
+ * and aria-hidden so it can't be clicked, focused or found by queries. */
20
+ const menuCellClass =
21
+ "grid transition-[grid-template-columns,opacity] duration-300 ease-smooth-out motion-reduce:transition-none"
36
22
 
37
23
  /**
38
- * Floating swap affordance centered on the seam between a panel's primary
39
- * surface and its secondary strip.
24
+ * Floating menu affordance centered on the seam between a panel's primary
25
+ * surface and its secondary strip: a compact ⋯ toggle that morphs into the
26
+ * Expand | Swap (| Close) actions.
40
27
  */
41
28
  export function PanelMenuButton({
42
29
  onClick,
43
30
  onExpand,
31
+ onCloseSecondary,
44
32
  secondaryExpanded = false,
33
+ swapped = false,
45
34
  label,
46
35
  sound = true,
47
36
  className,
48
37
  }: PanelMenuButtonProps): React.ReactElement {
49
-
50
- const reduce = useReducedMotion()
51
38
  const canHover = useHoverCapable()
52
39
  const actionsId = React.useId()
53
40
  const groupRef = React.useRef<HTMLDivElement>(null)
54
41
  const actionsRef = React.useRef<HTMLDivElement>(null)
55
- const actionsLeftRef = React.useRef(0)
56
42
  const [expanded, setExpanded] = React.useState(false);
57
43
 
58
- const transition = reduce ? { duration: 0 } : SPRING_LAYOUT
59
-
60
44
  const collapseTimerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined)
61
45
  const suppressHoverOpenUntilRef = React.useRef(0)
62
46
  const focusActionsOnOpenRef = React.useRef(false)
@@ -94,7 +78,7 @@ export function PanelMenuButton({
94
78
  }, [expanded, canHover, armCollapse])
95
79
 
96
80
  // Click-opens replace the toggle with the actions — hand focus over so
97
- // keyboard users aren't dropped onto <body>.
81
+ // keyboard users aren't dropped onto an inert control.
98
82
  React.useEffect(() => {
99
83
  if (!expanded || !focusActionsOnOpenRef.current) return
100
84
  focusActionsOnOpenRef.current = false
@@ -130,111 +114,108 @@ export function PanelMenuButton({
130
114
  setExpanded(true)
131
115
  }
132
116
 
133
- // popLayout pops the exiting actions group out of the flow — pin it back to
134
- // where it sat so it stays visually attached to the toggle while leaving.
135
- React.useLayoutEffect(() => {
136
- const actionsNode = actionsRef.current
137
- if (!actionsNode) return
138
-
139
- if (!expanded) {
140
- actionsNode.style.left = `${
141
- actionsLeftRef.current - actionsNode.getBoundingClientRect().left
142
- }px`
143
- return
144
- }
145
-
146
- actionsNode.style.left = ""
147
- actionsLeftRef.current = actionsNode.getBoundingClientRect().left
148
- }, [expanded])
149
-
150
117
  return (
151
118
  <GlassButtonGroup
152
119
  ref={groupRef}
153
- layout
154
- transition={transition}
155
120
  onPointerEnter={cancelCollapse}
156
121
  onPointerLeave={armCollapse}
157
122
  glassVariant="subtle"
123
+ data-expanded={expanded ? "" : undefined}
158
124
  className={cn(
159
125
  floatingButtonClass,
160
126
  revealOnPanelHoverClass,
161
- // Centered on the seam: secondary height (52px) + half the 10px gap.
162
- "absolute top-1/2 z-10 -translate-y-1/2 overflow-visible!",
127
+ "group/menu absolute top-1/2 z-10 -translate-y-1/2",
163
128
  className
164
129
  )}
165
130
  >
166
- <AnimatePresence mode="popLayout" initial={false}>
167
- {expanded ? (
168
- <motion.div
169
- key="menu-actions"
170
- ref={actionsRef}
171
- id={actionsId}
172
- layout
173
- variants={MENU_SEGMENT_VARIANTS}
174
- initial={reduce ? { opacity: 0 } : "hidden"}
175
- animate={reduce ? { opacity: 1 } : "visible"}
176
- exit={reduce ? { opacity: 0 } : "exit"}
177
- transition={transition}
178
- className="relative flex w-max items-stretch"
131
+ <div
132
+ aria-hidden={expanded || undefined}
133
+ inert={expanded || undefined}
134
+ className={cn(
135
+ menuCellClass,
136
+ "grid-cols-[1fr] opacity-100 group-data-expanded/menu:grid-cols-[0fr] group-data-expanded/menu:opacity-0"
137
+ )}
138
+ >
139
+ <div className="flex min-w-0 overflow-hidden">
140
+ <Button
141
+ id="scaffold-panel-menu-toggle-button"
142
+ aria-label={label}
143
+ aria-expanded={expanded}
144
+ aria-controls={actionsId}
145
+ size="icon-sm"
146
+ variant="ghost"
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"
151
+ data-slot="scaffold-panel-menu-toggle"
152
+ onClick={handleOnExpand}
153
+ onPointerEnter={handleToggleHover}
179
154
  >
155
+ <More size={16} />
156
+ </Button>
157
+ </div>
158
+ </div>
159
+ <div
160
+ id={actionsId}
161
+ ref={actionsRef}
162
+ aria-hidden={!expanded || undefined}
163
+ inert={!expanded || undefined}
164
+ className={cn(
165
+ menuCellClass,
166
+ "grid-cols-[0fr] opacity-0 group-data-expanded/menu:grid-cols-[1fr] group-data-expanded/menu:opacity-100"
167
+ )}
168
+ >
169
+ <div className="flex min-w-0 overflow-hidden">
170
+ <Button
171
+ id="scaffold-panel-menu-expand"
172
+ size="sm"
173
+ variant="ghost"
174
+ sound={sound}
175
+ data-slot="scaffold-panel-menu-expand"
176
+ className="rounded-r-none"
177
+ onClick={() => handleAction(onExpand)}
178
+ >
179
+ {/* Points at the sub-panel that grows next, in VISUAL space —
180
+ a swapped panel is column-reverse, so the direction mirrors.
181
+ Button's [&_svg]:transition-transform eases the flip. */}
182
+ <LucideChevronDown
183
+ size={16}
184
+ className={cn(secondaryExpanded !== swapped && "rotate-180")}
185
+ />
186
+ Expand
187
+ </Button>
188
+ <Button
189
+ id="scaffold-panel-menu-swap"
190
+ size="sm"
191
+ variant="ghost"
192
+ sound={sound}
193
+ data-slot="scaffold-panel-menu-swap"
194
+ className={cn(
195
+ "border-l-0",
196
+ onCloseSecondary ? "rounded-none" : "rounded-l-none"
197
+ )}
198
+ onClick={() => handleAction(onClick)}
199
+ >
200
+ <LucideArrowUpDown size={16} />
201
+ Swap
202
+ </Button>
203
+ {onCloseSecondary && (
180
204
  <Button
181
- id="scaffold-panel-menu-expand"
182
- size="sm"
183
- variant="ghost"
184
- sound={sound}
185
- data-slot="scaffold-panel-menu-swap"
186
- // The motion wrapper hides these from the group's corner
187
- // selectors — restore what ButtonGroup would have applied.
188
- className="rounded-r-none"
189
- onClick={() => handleAction(onExpand)}
190
- >
191
- {/* Button's [&_svg]:transition-transform eases the flip. */}
192
- <LucideChevronDown
193
- size={16}
194
- className={cn(secondaryExpanded && "rotate-180")}
195
- />
196
- Expand
197
- </Button>
198
- <Button
199
- id="scaffold-panel-menu-swap"
205
+ id="scaffold-panel-menu-close"
200
206
  size="sm"
201
207
  variant="ghost"
202
208
  sound={sound}
203
- data-slot="scaffold-panel-menu-swap"
209
+ data-slot="scaffold-panel-menu-close"
204
210
  className="rounded-l-none border-l-0"
205
- onClick={() => handleAction(onClick)}
206
- >
207
- <LucideArrowUpDown size={16} />
208
- Swap
209
- </Button>
210
- </motion.div>
211
- ) : (
212
- <motion.div
213
- key="menu-toggle"
214
- layout
215
- variants={MENU_SEGMENT_VARIANTS}
216
- initial={reduce ? { opacity: 0 } : "hidden"}
217
- animate={reduce ? { opacity: 1 } : "visible"}
218
- exit={reduce ? { opacity: 0 } : "exit"}
219
- transition={transition}
220
- className="flex shrink-0"
221
- >
222
- <Button
223
- id="scaffold-panel-menu-toggle-button"
224
- aria-label={label}
225
- aria-expanded={expanded}
226
- size="icon-sm"
227
- variant="ghost"
228
- sound={sound}
229
- data-slot="scaffold-panel-menu-toggle"
230
- onClick={handleOnExpand}
231
- onPointerEnter={handleToggleHover}
211
+ onClick={() => handleAction(onCloseSecondary)}
232
212
  >
233
- <More size={16} />
213
+ <LucideX size={16} />
214
+ Close
234
215
  </Button>
235
- </motion.div>
236
- )}
237
- </AnimatePresence>
216
+ )}
217
+ </div>
218
+ </div>
238
219
  </GlassButtonGroup>
239
220
  )
240
221
  }
@@ -1,31 +1,28 @@
1
1
  "use client"
2
2
 
3
- import { motion, useReducedMotion } from "motion/react"
3
+ import { AnimatePresence, motion, useReducedMotion } from "motion/react"
4
4
  import * as React from "react"
5
5
 
6
6
  import { cn } from "@/lib/utils"
7
7
  import { SCAFFOLD_EASE, SCAFFOLD_EDGE_GUTTER, SCAFFOLD_MORPH_DURATION } from "./constants"
8
8
  import { useScaffoldContext } from "./scaffold-context"
9
9
  import type { ScaffoldActionsProps } from "./type"
10
- import { actionSegmentClass } from "./utils"
11
- import { GroupSeparator } from "@/components/ui/group"
12
- import { GlassButtonGroup } from "@/components/ui/glasscn/glass-button-group"
13
- import { Button } from "@/components/ui/button";
10
+ import { segmentVariants } from "./utils"
11
+ import { Button } from "@/components/ui/button"
12
+ import { SPRING_LAYOUT } from "@/lib/ease.ts"
14
13
  import { LucidePlus } from "lucide-react"
15
14
 
16
15
  /**
17
16
  * The floating pill cluster in the main region's top-right corner — the
18
- * design's "Panel Context Menu": an Add segment plus an overflow segment
19
- * whose badge counts the non-visible panels. Slides left in step with the
20
- * inspector opening.
17
+ * design's "Panel Context Menu": one tab per open panel (`Scaffold.Tab`
18
+ * children) plus an Add segment. Add renders only while `onAdd` is
19
+ * present — omit it once the canvas holds `SCAFFOLD_PANEL_CAPACITY`
20
+ * panels. Slides left in step with the inspector opening.
21
21
  */
22
22
  export function ScaffoldActions({
23
23
  onAdd,
24
24
  addLabel = "Panel",
25
25
  addIcon,
26
- overflowCount,
27
- onBrowse,
28
- browseLabel = "Browse panels",
29
26
  sound = true,
30
27
  className,
31
28
  children,
@@ -33,12 +30,7 @@ export function ScaffoldActions({
33
30
  }: ScaffoldActionsProps): React.ReactElement {
34
31
  const { inspectorOpen, inspectorWidth } = useScaffoldContext()
35
32
  const reducedMotion = useReducedMotion()
36
-
37
- const soundProps = {
38
- "data-cuelume-press": sound ? "" : undefined,
39
- "data-cuelume-release": sound ? "" : undefined,
40
- }
41
- const showBrowse = overflowCount !== undefined || onBrowse !== undefined
33
+ const transition = reducedMotion ? { duration: 0 } : SPRING_LAYOUT
42
34
 
43
35
  return (
44
36
  <motion.div
@@ -47,7 +39,7 @@ export function ScaffoldActions({
47
39
  x: inspectorOpen ? -(inspectorWidth + SCAFFOLD_EDGE_GUTTER) : 0,
48
40
  }}
49
41
  className={cn(
50
- "sticky inline-flex items-center gap-0.5 h-12",
42
+ "sticky inline-flex h-12 items-center gap-0.5",
51
43
  "justify-end",
52
44
  className
53
45
  )}
@@ -59,39 +51,38 @@ export function ScaffoldActions({
59
51
  }}
60
52
  {...rest}
61
53
  >
62
- {children}
63
- <GlassButtonGroup glassVariant="liquid-refract" className="rounded-md">
64
- <Button
65
- onClick={onAdd}
66
- size="sm"
67
- variant="ghost"
68
- >
69
- {addIcon ?? <LucidePlus className="stroke-3" />}
70
- {addLabel}
71
- </Button>
72
- {!showBrowse && <GroupSeparator />}
73
- {showBrowse && (
74
- <button
75
- aria-label={browseLabel}
76
- className={cn(actionSegmentClass, "gap-0.5 px-2")}
77
- onClick={onBrowse}
78
- type="button"
79
- {...soundProps}
54
+ <motion.div
55
+ layout
56
+ transition={transition}
57
+ className="flex items-center gap-1.5"
58
+ >
59
+ <AnimatePresence mode="popLayout" initial={false}>
60
+ {onAdd && (
61
+ <motion.div
62
+ key="scaffold-add"
63
+ layout
64
+ animate={reducedMotion ? { opacity: 1 } : "visible"}
65
+ className="flex shrink-0"
66
+ exit={reducedMotion ? { opacity: 0 } : "exit"}
67
+ initial={reducedMotion ? { opacity: 0 } : "hidden"}
68
+ transition={transition}
69
+ variants={segmentVariants}
80
70
  >
81
- <span className="relative">
82
-
83
- {overflowCount !== undefined && overflowCount > 0 && (
84
- <span className="absolute -top-1.5 -right-1.5 flex size-3.5 items-center justify-center rounded-full bg-neutral-700 text-[9px] font-semibold text-white dark:bg-neutral-200 dark:text-neutral-900">
85
- {overflowCount}
86
- </span>
87
- )}
88
- </span>
89
-
90
- </button>
91
- )}
92
-
93
- {children}
94
- </GlassButtonGroup>
71
+ <Button
72
+ onClick={onAdd}
73
+ size="sm"
74
+ sound={sound}
75
+ variant="ghost"
76
+ className="h-full gap-2 rounded-sm py-1.5 pr-3 pl-2"
77
+ >
78
+ {addIcon ?? <LucidePlus className="stroke-2" />}
79
+ {addLabel}
80
+ </Button>
81
+ </motion.div>
82
+ )}
83
+ {children}
84
+ </AnimatePresence>
85
+ </motion.div>
95
86
  </motion.div>
96
87
  )
97
88
  }
@@ -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 type { ScaffoldCanvasProps } from "./type"
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
- <AnimatePresence initial={false} mode="popLayout">
25
- {children}
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
  }
@@ -8,6 +8,8 @@ import { SCAFFOLD_EASE, SCAFFOLD_EDGE_GUTTER, SCAFFOLD_MORPH_DURATION } from "./
8
8
  import { useScaffoldContext } from "./scaffold-context"
9
9
  import type { ScaffoldInspectorProps } from "./type"
10
10
  import { GlassContainer } from "@/components/ui/glasscn/glass-container.tsx"
11
+ import { Button } from "@/components/ui/button"
12
+ import { LucideX } from "lucide-react"
11
13
 
12
14
  /**
13
15
  * The right-hand drawer: a brighter card that slides in over the canvas,
@@ -21,7 +23,8 @@ export function ScaffoldInspector({
21
23
  className,
22
24
  ...rest
23
25
  }: ScaffoldInspectorProps): React.ReactElement {
24
- const { inspectorOpen, inspectorWidth } = useScaffoldContext()
26
+ const { inspectorOpen, inspectorWidth, toggleInspector } =
27
+ useScaffoldContext()
25
28
  const reducedMotion = useReducedMotion()
26
29
 
27
30
  // Slide fully past the edge gutter and the drop shadow's blur radius.
@@ -34,7 +37,7 @@ export function ScaffoldInspector({
34
37
  aria-hidden={inspectorOpen ? undefined : true}
35
38
  aria-label={name}
36
39
  className={cn(
37
- "absolute right-0 w-80 z-20 my-2 flex h-full max-h-[calc(100%-1rem)] flex-col",
40
+ "absolute right-0 z-20 my-2 flex h-full max-h-[calc(100%-0.5rem)] w-96 flex-col",
38
41
  !inspectorOpen && "pointer-events-none",
39
42
  className
40
43
  )}
@@ -49,7 +52,28 @@ export function ScaffoldInspector({
49
52
  }}
50
53
  {...rest}
51
54
  >
52
- <GlassContainer className="flex flex-col shadow-inner shadow-md rounded-md flex-1 border-t border-white dark:border-neutral-900 bg-neutral-50/10 dark:bg-black/10" glassVariant="subtle">
55
+ <GlassContainer
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
+ glassVariant="subtle"
58
+ >
59
+ <div
60
+ id="scaffold-inspector-header"
61
+ className="flex h-12 items-center justify-between gap-2 border-b pr-3 pl-4"
62
+ >
63
+ <span className="text-base font-normal text-card-foreground/70">
64
+ {name}
65
+ </span>
66
+ <Button
67
+ size="icon-xs"
68
+ variant="glass"
69
+ glassVariant="subtle"
70
+ className="scale-85"
71
+ aria-label="Close inspector"
72
+ onClick={toggleInspector}
73
+ >
74
+ <LucideX className="stroke-3" />
75
+ </Button>
76
+ </div>
53
77
  {children}
54
78
  </GlassContainer>
55
79
  </motion.aside>
@@ -18,7 +18,7 @@ export function ScaffoldMain({
18
18
  return (
19
19
  <div
20
20
  id="scaffold-main"
21
- className={cn("relative flex h-full min-w-0 flex-1 flex-col", className)}
21
+ className={cn("relative flex min-w-0 flex-1 flex-col", className)}
22
22
  data-slot="scaffold-main"
23
23
  {...rest}
24
24
  >