@exegia/corpora-ui 0.18.0 → 0.19.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.
@@ -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,27 @@ 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/10 shadow-inner shadow-md 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="secondary"
69
+ className="scale-85"
70
+ aria-label="Close inspector"
71
+ onClick={toggleInspector}
72
+ >
73
+ <LucideX className="stroke-3" />
74
+ </Button>
75
+ </div>
53
76
  {children}
54
77
  </GlassContainer>
55
78
  </motion.aside>
@@ -2,14 +2,13 @@
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
8
  import { SCAFFOLD_EASE, SCAFFOLD_MORPH_DURATION } from "./constants"
8
- import { PanelCloseButton } from "./panel-close-button"
9
9
  import { PanelMenuButton } from "./panel-menu-button.tsx"
10
10
  import type { ScaffoldPanelProps, TSubPanelPosition } from "./type"
11
11
  import { ScaffoldSubPanel } from "@/components/blocks/scaffold/scaffold-sub-panel.tsx"
12
- import { useState } from "react"
13
12
 
14
13
  /**
15
14
  * A canvas panel: a primary card plus an optional secondary strip below it,
@@ -21,15 +20,13 @@ import { useState } from "react"
21
20
  export function ScaffoldPanel({
22
21
  children,
23
22
  SecondaryPanel,
24
- onClose,
25
23
  onSwap,
24
+ onCloseSecondary,
26
25
  name,
27
- closeLabel = "Close panel",
28
26
  swapLabel = "Swap panel content",
29
27
  sound = true,
30
28
  className,
31
29
  }: ScaffoldPanelProps): React.ReactElement {
32
-
33
30
  const reducedMotion = useReducedMotion()
34
31
 
35
32
  const transition = {
@@ -37,18 +34,19 @@ export function ScaffoldPanel({
37
34
  ease: SCAFFOLD_EASE,
38
35
  }
39
36
 
40
- const [subPanelPosition, setSubPanelPosition] = useState<TSubPanelPosition>("top")
37
+ const [subPanelPosition, setSubPanelPosition] =
38
+ useState<TSubPanelPosition>("top")
41
39
  const [isSwapped, setIsSwapped] = useState<boolean>(false)
42
40
  const handleSwap = () => {
43
- setIsSwapped((prev) => !prev);
44
- onSwap?.();
45
- };
41
+ setIsSwapped((prev) => !prev)
42
+ onSwap?.()
43
+ }
46
44
 
47
45
  // Trades which sub-panel holds the flexible slot — the collapsed card grows
48
46
  // into `flex-1` while the expanded one shrinks to the `min-h-14` strip.
49
47
  const handleExpand = () => {
50
- setSubPanelPosition((prev) => (prev === "top" ? "bottom" : "top"));
51
- };
48
+ setSubPanelPosition((prev) => (prev === "top" ? "bottom" : "top"))
49
+ }
52
50
 
53
51
  return (
54
52
  <motion.section
@@ -64,35 +62,27 @@ export function ScaffoldPanel({
64
62
  data-swapped={isSwapped ? "" : undefined}
65
63
  transition={transition}
66
64
  >
67
- <ScaffoldSubPanel
68
- id="top"
69
- expanded={subPanelPosition === "top"}
70
- primary
71
- >
65
+ <ScaffoldSubPanel id="top" expanded={subPanelPosition === "top"} primary>
72
66
  {children}
73
67
  </ScaffoldSubPanel>
74
- {onSwap && SecondaryPanel && (
75
- <div className="relative flex items-center justify-center h-2">
68
+ {(onSwap || onCloseSecondary) && SecondaryPanel && (
69
+ <div className="relative flex h-2 items-center justify-center">
76
70
  <PanelMenuButton
77
71
  label={swapLabel}
78
72
  onClick={handleSwap}
79
73
  onExpand={handleExpand}
74
+ onCloseSecondary={onCloseSecondary}
80
75
  secondaryExpanded={subPanelPosition === "bottom"}
76
+ swapped={isSwapped}
81
77
  sound={sound}
82
78
  />
83
79
  </div>
84
80
  )}
85
81
  {SecondaryPanel && (
86
- <ScaffoldSubPanel
87
- id="bottom"
88
- expanded={subPanelPosition === "bottom"}
89
- >
82
+ <ScaffoldSubPanel id="bottom" expanded={subPanelPosition === "bottom"}>
90
83
  {SecondaryPanel}
91
84
  </ScaffoldSubPanel>
92
85
  )}
93
- {onClose && (
94
- <PanelCloseButton label={closeLabel} onClick={onClose} sound={sound} />
95
- )}
96
86
  </motion.section>
97
87
  )
98
88
  }
@@ -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-20 shrink-0 flex-col items-center gap-1 pt-8 pb-2",
22
+ "flex min-w-12 shrink-0 flex-col items-center gap-1 pt-10 pb-2",
23
23
  className
24
24
  )}
25
25
  data-slot="scaffold-sidebar"
@@ -0,0 +1,66 @@
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 type { ScaffoldTabProps } from "./type"
9
+ import { segmentVariants } from "./utils"
10
+ import { Button } from "@/components/ui/button"
11
+ import { LucideAppWindow, LucideX } from "lucide-react"
12
+ import { GlassButtonGroup } from "@/components/ui/glasscn/glass-button-group.tsx"
13
+
14
+ /**
15
+ * A panel's tab inside the action cluster: its label plus a close
16
+ * affordance. Render one per open panel as Actions children, keyed —
17
+ * closes animate out of the pill. Omit `onClose` on the last remaining
18
+ * panel's tab; the canvas keeps at least one panel open.
19
+ */
20
+ export function ScaffoldTab({
21
+ children,
22
+ onClose,
23
+ closeLabel = "Close panel",
24
+ sound = true,
25
+ className,
26
+ ...rest
27
+ }: ScaffoldTabProps): React.ReactElement {
28
+ const reduce = useReducedMotion()
29
+ const transition = reduce ? { duration: 0 } : SPRING_LAYOUT
30
+
31
+ return (
32
+ <GlassButtonGroup
33
+ layout
34
+ animate={reduce ? { opacity: 1 } : "visible"}
35
+ data-slot="scaffold-tab"
36
+ exit={reduce ? { opacity: 0 } : "exit"}
37
+ initial={reduce ? { opacity: 0 } : "hidden"}
38
+ transition={transition}
39
+ variants={segmentVariants}
40
+ glassVariant="subtle"
41
+ className={cn("h-full rounded-sm", className)}
42
+ {...rest}
43
+ >
44
+ <Button
45
+ size="sm"
46
+ variant="ghost"
47
+ className={cn("gap-2.5 rounded-sm pl-2", onClose && "pr-2")}
48
+ >
49
+ <LucideAppWindow size={16} />
50
+ {children}
51
+ </Button>
52
+ {onClose && (
53
+ <Button
54
+ aria-label={closeLabel}
55
+ onClick={onClose}
56
+ size="icon-sm"
57
+ sound={sound}
58
+ variant="ghost"
59
+ className="rounded-none!"
60
+ >
61
+ <LucideX size={16} />
62
+ </Button>
63
+ )}
64
+ </GlassButtonGroup>
65
+ )
66
+ }
@@ -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 {
@@ -46,23 +42,16 @@ type MotionSafe<T> = Omit<
46
42
  export interface ScaffoldActionsProps extends MotionSafe<
47
43
  Omit<ComponentProps<"div">, "children">
48
44
  > {
49
- /** Called when the Add segment is pressed. */
45
+ /** Called when the Add segment is pressed. The segment renders only
46
+ * while this is present — omit it to hide Add, e.g. once the canvas
47
+ * holds `SCAFFOLD_PANEL_CAPACITY` panels. */
50
48
  onAdd?: () => void
51
49
  /** Label of the Add segment. */
52
50
  addLabel?: ReactNode
53
51
  /** Replaces the default add icon. */
54
52
  addIcon?: ReactNode
55
- /** Number of non-visible panels tucked into the overflow dropdown —
56
- * the badge on the browse segment. The segment renders when this or
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. */
53
+ /** The panel tabs (`Scaffold.Tab`, one per open panel) and any extra
54
+ * segments, rendered before Add. Key each tab — closes animate out. */
66
55
  children?: ReactNode
67
56
  /** Emit interaction-sound attributes (inert until an app binds cuelume). */
68
57
  sound?: boolean
@@ -77,32 +66,38 @@ export interface ScaffoldPanelProps {
77
66
  children: ReactNode
78
67
  /** The panel's content, rendered in a card with a drop shadow. */
79
68
  SecondaryPanel?: TScaffoldPanelChild
80
- /** Renders the floating close button in the panel's top-right corner. */
81
- onClose?: () => void
82
- /** Renders the floating swap button over the primary/secondary gap.
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. */
69
+ /** Reveals the Swap action in the seam menu (needs `SecondaryPanel`).
70
+ * Swapping trades the two cards with a layout morph — flip the slot
71
+ * content here so each card carries its subject to its new home. */
86
72
  onSwap?: () => void
87
- /** Controlled swap state — while true the strip's card sits in the
88
- * primary slot. Omit letting the panel toggle it on swap clicks. */
89
- swapped?: boolean
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
73
+ /** Reveals the Close action in the seam menu — remove `SecondaryPanel`
74
+ * here; the strip and the menu leave together. */
75
+ onCloseSecondary?: () => void
94
76
  /** Accessible name of the panel region. */
95
77
  name?: string
96
78
  /** Emit interaction-sound attributes on the floating buttons. */
97
79
  sound?: boolean
98
- /** Accessible name of the close button. */
99
- closeLabel?: string
100
- /** Accessible name of the swap button. */
80
+ /** Accessible name of the seam-menu toggle. */
101
81
  swapLabel?: string
102
82
  /** Extra class names for the panel's root element. */
103
83
  className?: string
104
84
  }
105
85
 
86
+ export interface ScaffoldTabProps extends MotionSafe<
87
+ Omit<ComponentProps<"div">, "children">
88
+ > {
89
+ /** The tab's label. */
90
+ children?: ReactNode
91
+ /** Renders the tab's close button. The button renders only while this
92
+ * is present — omit it to pin the tab, e.g. on the last remaining
93
+ * panel (the canvas keeps at least one open). */
94
+ onClose?: () => void
95
+ /** Accessible name of the close button. */
96
+ closeLabel?: string
97
+ /** Emit interaction-sound attributes on the close button. */
98
+ sound?: boolean
99
+ }
100
+
106
101
  export type TSubPanelPosition = "bottom" | "top"
107
102
  export type TSubPanelVariant = "card" | "subtle" | "inset"
108
103
 
@@ -147,9 +142,15 @@ export interface PanelMenuButtonProps extends PanelFloatingButtonProps {
147
142
  /** Called when the Expand action is pressed — the panel trades which
148
143
  * sub-panel holds the flexible slot. */
149
144
  onExpand?: () => void
145
+ /** Renders the Close action; called when it is pressed — the consumer
146
+ * removes the secondary strip. */
147
+ onCloseSecondary?: () => void
150
148
  /** Whether the secondary strip currently holds the flexible slot —
151
149
  * flips the Expand chevron between down and up. */
152
150
  secondaryExpanded?: boolean
151
+ /** Whether the panel is swapped (column-reverse). Mirrors the Expand
152
+ * chevron so it keeps pointing at the sub-panel that grows next. */
153
+ swapped?: boolean
153
154
  }
154
155
 
155
156
  export interface UseScaffoldOptions {
@@ -1,5 +1,7 @@
1
+ import type { Variants } from "motion/react"
1
2
  import type { ClassNameValue } from "tailwind-merge"
2
3
  import type { TSubPanelVariant } from "@/components/blocks/scaffold/type.ts"
4
+ import { EASE_OUT } from "@/lib/ease.ts"
3
5
 
4
6
  /** Desktop backdrop the whole scaffold sits on — a soft warm-gray wash. */
5
7
  export const scaffoldBackgroundClass: ClassNameValue =
@@ -22,6 +24,20 @@ export const revealOnPanelHoverClass: ClassNameValue =
22
24
  export const actionSegmentClass: ClassNameValue =
23
25
  "flex h-[30px] items-center rounded-full text-neutral-700 transition-[background-color,color,scale,box-shadow] duration-150 ease-smooth-out hover:bg-white hover:text-neutral-900 hover:shadow-xs active:scale-97 dark:text-neutral-300 dark:hover:bg-neutral-700 dark:hover:text-white motion-reduce:transition-none motion-reduce:active:scale-100"
24
26
 
27
+ /** Pill segments (seam-menu actions, tabs, Add) blur/fade in place while
28
+ * the track morphs around them. The exit is a fast tween, not the layout
29
+ * spring — the track stays overflow-visible, so popped-out segments must be
30
+ * gone before the shrinking pill leaves them behind as a ghost. */
31
+ export const segmentVariants: Variants = {
32
+ hidden: { opacity: 0, filter: "blur(4px)" },
33
+ visible: { opacity: 1, filter: "blur(0px)" },
34
+ exit: {
35
+ opacity: 0,
36
+ filter: "blur(4px)",
37
+ transition: { duration: 0.18, ease: EASE_OUT },
38
+ },
39
+ }
40
+
25
41
  /** Map of sub-panel variants to their corresponding side. */
26
42
  export const subPanelVariant: Record<TSubPanelVariant, ClassNameValue> = {
27
43
  card: "bg-neutral-50 dark:bg-neutral-900 rounded-md border-t border-b border-t-white dark:border-t-neutral-700/50 border-b-neutral-900/10 dark:border-b-black/70 shadow-sm shadow-neutral-900/10 dark:shadow-black/50 inset-shadow-sm inset-shadow-white dark:inset-shadow-neutral-800",