@exegia/corpora-ui 0.13.0 → 0.15.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 (39) hide show
  1. package/dist-lib/components/blocks/layout.d.ts +5 -0
  2. package/dist-lib/components/{agents → blocks/nav/sidebar}/ai-sidebar.d.ts +1 -1
  3. package/dist-lib/components/{agents → blocks/nav/sidebar}/sidebar-row.d.ts +1 -1
  4. package/dist-lib/components/blocks/shell/__tests__/shell-layout.test.d.ts +1 -0
  5. package/dist-lib/components/blocks/shell/index.d.ts +3 -1
  6. package/dist-lib/components/blocks/shell/shell-layout.d.ts +1 -1
  7. package/dist-lib/components/blocks/shell/type.d.ts +58 -15
  8. package/dist-lib/components/blocks/shell/use-shell-panels.d.ts +9 -0
  9. package/dist-lib/components/motion/animated-sidebar.d.ts +25 -25
  10. package/dist-lib/index.d.ts +3 -1
  11. package/dist-lib/index.js +1107 -1029
  12. package/dist-lib/index.js.map +1 -1
  13. package/package.json +3 -2
  14. package/src/components/blocks/layout.ts +7 -0
  15. package/src/components/{agents → blocks/nav/sidebar}/ai-sidebar.tsx +13 -17
  16. package/src/components/{agents → blocks/nav/sidebar}/marquee-label.tsx +1 -1
  17. package/src/components/{agents → blocks/nav/sidebar}/sidebar-icon.tsx +1 -1
  18. package/src/components/{agents → blocks/nav/sidebar}/sidebar-row.tsx +4 -4
  19. package/src/components/{agents → blocks/nav/sidebar}/use-ai-sidebar.ts +2 -2
  20. package/src/components/{agents → blocks/nav/sidebar}/utils.ts +1 -1
  21. package/src/components/blocks/nav/sidebar-block.tsx +15 -7
  22. package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +178 -0
  23. package/src/components/blocks/shell/index.ts +5 -1
  24. package/src/components/blocks/shell/shell-layout.tsx +29 -24
  25. package/src/components/blocks/shell/type.ts +77 -35
  26. package/src/components/blocks/shell/use-shell-panels.ts +78 -0
  27. package/src/components/motion/animated-sidebar.tsx +113 -90
  28. package/src/index.css +2 -1
  29. package/src/index.ts +3 -3
  30. /package/dist-lib/components/{agents → blocks/nav/sidebar}/__tests__/ai-sidebar.test.d.ts +0 -0
  31. /package/dist-lib/components/{agents → blocks/nav/sidebar}/index.d.ts +0 -0
  32. /package/dist-lib/components/{agents → blocks/nav/sidebar}/marquee-label.d.ts +0 -0
  33. /package/dist-lib/components/{agents → blocks/nav/sidebar}/sidebar-icon.d.ts +0 -0
  34. /package/dist-lib/components/{agents → blocks/nav/sidebar}/type.d.ts +0 -0
  35. /package/dist-lib/components/{agents → blocks/nav/sidebar}/use-ai-sidebar.d.ts +0 -0
  36. /package/dist-lib/components/{agents → blocks/nav/sidebar}/utils.d.ts +0 -0
  37. /package/src/components/{agents → blocks/nav/sidebar}/__tests__/ai-sidebar.test.tsx +0 -0
  38. /package/src/components/{agents → blocks/nav/sidebar}/index.ts +0 -0
  39. /package/src/components/{agents → blocks/nav/sidebar}/type.ts +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exegia/corpora-ui",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "shadcn-ready React UI library for the corpora manuscript-research apps.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -46,8 +46,9 @@
46
46
  "class-variance-authority": "^0.7.1",
47
47
  "clsx": "^2.1.1",
48
48
  "cuelume": "^0.2.1",
49
- "lucide-react": "^1.27.0",
49
+ "lucide-react": "^1.31.0",
50
50
  "motion": "^12.43.0",
51
+ "motion-icons-react": "^1.1.0",
51
52
  "tailwind-merge": "^3.6.0"
52
53
  },
53
54
  "devDependencies": {
@@ -0,0 +1,7 @@
1
+ import ShellLayout from "@/components/blocks/shell"
2
+
3
+ const Layout = {
4
+ Main: ShellLayout
5
+ }
6
+
7
+ export default Layout
@@ -2,9 +2,9 @@
2
2
 
3
3
  import { AnimatePresence } from "motion/react";
4
4
  import { cn } from "@/lib/utils";
5
- import type { AISidebarProps } from "@/components/agents/type.ts"
6
- import { useAISidebar } from "@/components/agents/use-ai-sidebar.ts"
7
- import { ResourceRow } from "@/components/agents/sidebar-row.tsx"
5
+ import type { AISidebarProps, FlatResource } from "./type"
6
+ import { useAISidebar, type UseAISidebarOptions } from "./use-ai-sidebar"
7
+ import { ResourceRow } from "./sidebar-row"
8
8
 
9
9
  export function AISidebar({
10
10
  renderIcon,
@@ -41,7 +41,7 @@ export function AISidebar({
41
41
  commitRename,
42
42
  handleMenuOpenChange,
43
43
  setRowRef,
44
- } = useAISidebar(options);
44
+ } = useAISidebar(options as UseAISidebarOptions);
45
45
 
46
46
  return (
47
47
  <>
@@ -53,13 +53,13 @@ export function AISidebar({
53
53
  onDrop={handleDrop}
54
54
  onMouseLeave={clearHover}
55
55
  className={cn(
56
- "relative flex min-w-0 flex-col gap-0.5 [overflow-anchor:none] group-data-[state=collapsed]/sidebar:hidden",
57
- draggingId && "select-none pb-9",
58
- className,
56
+ "relative min-w-0 flex-col gap-0.5 [overflow-anchor:none] group-data-[state=collapsed]/sidebar:hidden",
57
+ draggingId && "pb-9 select-none",
58
+ className
59
59
  )}
60
60
  >
61
61
  <AnimatePresence initial={false}>
62
- {flat.map((row) => (
62
+ {flat.map((row: FlatResource) => (
63
63
  <ResourceRow
64
64
  key={row.item.id}
65
65
  row={row}
@@ -72,14 +72,10 @@ export function AISidebar({
72
72
  renaming={renamingId === row.item.id}
73
73
  hoverActive={hoveredId !== null && !draggingId}
74
74
  hoverPill={
75
- hoveredId === row.item.id &&
76
- !draggingId &&
77
- !row.item.disabled
75
+ hoveredId === row.item.id && !draggingId && !row.item.disabled
78
76
  }
79
77
  hoverLayoutId={hoverLayoutId}
80
- onHoverChange={(hovered) =>
81
- handleRowHover(row.item.id, hovered)
82
- }
78
+ onHoverChange={(hovered) => handleRowHover(row.item.id, hovered)}
83
79
  onFocus={() => setFocusedId(row.item.id)}
84
80
  onSelect={() => select(row.item.id)}
85
81
  onToggle={() => toggle(row.item.id)}
@@ -100,7 +96,7 @@ export function AISidebar({
100
96
  ))}
101
97
  </AnimatePresence>
102
98
 
103
- {draggingId ? (
99
+ {draggingId && (
104
100
  <div
105
101
  aria-hidden="true"
106
102
  data-active={dropTarget?.id === null || undefined}
@@ -108,11 +104,11 @@ export function AISidebar({
108
104
  >
109
105
  Move to top level
110
106
  </div>
111
- ) : null}
107
+ )}
112
108
  </div>
113
109
  <span className="sr-only" aria-live="polite">
114
110
  {announcement}
115
111
  </span>
116
112
  </>
117
- );
113
+ )
118
114
  }
@@ -1,6 +1,6 @@
1
1
  import { motion, useReducedMotion } from "motion/react"
2
2
  import { useEffect, useRef, useState } from "react"
3
- import { ROW_REVEAL } from "@/components/agents/utils.ts"
3
+ import { ROW_REVEAL } from "./utils.ts"
4
4
 
5
5
  export function MarqueeLabel({
6
6
  active,
@@ -1,4 +1,4 @@
1
- import type { SidebarResource } from "@/components/agents/type.ts"
1
+ import type { SidebarResource } from "./type.ts"
2
2
  import { Bookmark, FileText, Folder, FolderOpen } from "lucide-react"
3
3
 
4
4
  export function SidebarIcon(item: SidebarResource, expanded: boolean) {
@@ -5,7 +5,7 @@ import {
5
5
  useState,
6
6
  type MouseEvent,
7
7
  } from "react"
8
- import type { ResourceRowProps } from "@/components/agents/type.ts"
8
+ import type { ResourceRowProps } from "./type"
9
9
  import {
10
10
  AnimatePresence,
11
11
  motion,
@@ -15,9 +15,9 @@ import {
15
15
  import { ChevronRight, MoreHorizontal, Pencil } from "lucide-react"
16
16
  import { SPRING_LAYOUT } from "@/lib/ease.ts"
17
17
  import { cn } from "@/lib/utils.ts"
18
- import { canContain } from "@/components/agents/utils.ts"
19
- import { SidebarIcon } from "@/components/agents/sidebar-icon.tsx"
20
- import { MarqueeLabel } from "@/components/agents/marquee-label.tsx"
18
+ import { canContain } from "./utils"
19
+ import { SidebarIcon } from "./sidebar-icon"
20
+ import { MarqueeLabel } from "./marquee-label"
21
21
  import {
22
22
  MorphPopover,
23
23
  MorphPopoverContent,
@@ -16,7 +16,7 @@ import type {
16
16
  FlatResource,
17
17
  SidebarResource,
18
18
  SidebarResourceMove,
19
- } from "@/components/agents/type.ts"
19
+ } from "./type.ts"
20
20
  import {
21
21
  canContain,
22
22
  containsResource,
@@ -24,7 +24,7 @@ import {
24
24
  flattenResources,
25
25
  moveResource,
26
26
  renameResource,
27
- } from "@/components/agents/utils.ts"
27
+ } from "./utils.ts"
28
28
 
29
29
  export type UseAISidebarOptions = Pick<
30
30
  AISidebarProps,
@@ -1,5 +1,5 @@
1
1
  import { EASE_OUT } from "@/lib/ease.ts"
2
- import type { FlatResource, SidebarResource, SidebarResourceDropPosition, SidebarResourceMove } from "@/components/agents/type.ts"
2
+ import type { FlatResource, SidebarResource, SidebarResourceDropPosition, SidebarResourceMove } from "./type.ts"
3
3
 
4
4
  export const ROW_REVEAL = {
5
5
  duration: 0.16,
@@ -62,14 +62,22 @@ export function SidebarBlock({
62
62
  return (
63
63
  <AnimatedSidebarProvider
64
64
  // The provider ships a full-page flex shell; this block is just the
65
- // panel, so the shell is flattened back to the panel's own box.
65
+ // panel, so the shell is flattened back to the panel's own box. The
66
+ // block is a single panel, so its scalar open props map onto the
67
+ // provider's side-keyed records under the block's own `side`.
66
68
  className="h-full min-h-0 w-auto"
67
- defaultOpen={defaultOpen}
68
- defaultOpenMobile={defaultOpenMobile}
69
- onOpenChange={onOpenChange}
70
- onOpenMobileChange={onOpenMobileChange}
71
- open={open}
72
- openMobile={openMobile}
69
+ defaultOpen={{ [side]: defaultOpen ?? true }}
70
+ defaultOpenMobile={
71
+ defaultOpenMobile === undefined ? undefined : { [side]: defaultOpenMobile }
72
+ }
73
+ onOpenChange={(nextOpen, changedSide) => {
74
+ if (changedSide === side) onOpenChange?.(nextOpen)
75
+ }}
76
+ onOpenMobileChange={(nextOpen, changedSide) => {
77
+ if (changedSide === side) onOpenMobileChange?.(nextOpen)
78
+ }}
79
+ open={open === undefined ? undefined : { [side]: open }}
80
+ openMobile={openMobile === undefined ? undefined : { [side]: openMobile }}
73
81
  style={{
74
82
  ...(width ? { "--sidebar-width": width } : null),
75
83
  ...(iconWidth ? { "--sidebar-width-icon": iconWidth } : null),
@@ -0,0 +1,178 @@
1
+ import { describe, expect, mock, test } from "bun:test"
2
+ import { render, screen } from "@testing-library/react"
3
+ import userEvent from "@testing-library/user-event"
4
+
5
+ import { ShellLayout } from "../shell-layout"
6
+ import { useShellPanels } from "../use-shell-panels"
7
+ import type { TPanelMap, UseShellPanelsOptions } from "../type"
8
+
9
+ const PANELS: TPanelMap = {
10
+ left: {
11
+ id: "nav",
12
+ name: "Navigation",
13
+ component: <div>Nav body</div>,
14
+ open: true,
15
+ side: "left",
16
+ },
17
+ right: {
18
+ id: "inspector",
19
+ name: "Inspector",
20
+ component: <div>Inspector body</div>,
21
+ open: false,
22
+ side: "right",
23
+ },
24
+ }
25
+
26
+ /** The desktop rail / drawer elements, queried by their landmark roles. */
27
+ function leftRail() {
28
+ return screen.getByRole("navigation", { name: "Primary navigation" })
29
+ }
30
+
31
+ function rightDrawer(name = "Inspector") {
32
+ return screen.getByRole("complementary", { name })
33
+ }
34
+
35
+ describe("ShellLayout", () => {
36
+ test("renders panel content by side and children in the inset", () => {
37
+ render(
38
+ <ShellLayout panels={PANELS} variant="web">
39
+ <div>Main content</div>
40
+ </ShellLayout>
41
+ )
42
+
43
+ expect(leftRail().textContent).toContain("Nav body")
44
+ expect(rightDrawer().textContent).toContain("Inspector body")
45
+ expect(screen.getByRole("main").textContent).toContain("Main content")
46
+ })
47
+
48
+ test("names the right drawer after its panel", () => {
49
+ render(<ShellLayout panels={PANELS} variant="web" />)
50
+
51
+ expect(rightDrawer("Inspector")).toBeDefined()
52
+ })
53
+
54
+ test("the header triggers toggle their own side", async () => {
55
+ const user = userEvent.setup()
56
+ render(<ShellLayout panels={PANELS} variant="web" />)
57
+
58
+ const leftTrigger = screen.getByRole("button", { name: "Toggle sidebar" })
59
+ const rightTrigger = screen.getByRole("button", { name: "Toggle panel" })
60
+
61
+ expect(leftRail().getAttribute("data-state")).toBe("expanded")
62
+ expect(rightDrawer().getAttribute("data-state")).toBe("collapsed")
63
+
64
+ await user.click(leftTrigger)
65
+ expect(leftRail().getAttribute("data-state")).toBe("collapsed")
66
+ expect(leftTrigger.getAttribute("aria-expanded")).toBe("false")
67
+ // The other side is untouched.
68
+ expect(rightDrawer().getAttribute("data-state")).toBe("collapsed")
69
+
70
+ await user.click(rightTrigger)
71
+ expect(rightDrawer().getAttribute("data-state")).toBe("expanded")
72
+ expect(rightTrigger.getAttribute("aria-expanded")).toBe("true")
73
+ expect(leftRail().getAttribute("data-state")).toBe("collapsed")
74
+ })
75
+
76
+ test("a panel's own open flag seeds its side's initial state", () => {
77
+ render(
78
+ <ShellLayout
79
+ panels={{
80
+ ...PANELS,
81
+ right: { ...PANELS.right!, open: true },
82
+ }}
83
+ variant="web"
84
+ />
85
+ )
86
+
87
+ expect(rightDrawer().getAttribute("data-state")).toBe("expanded")
88
+ })
89
+
90
+ test("an explicit defaultOpen record wins over the panel's flag", () => {
91
+ render(
92
+ <ShellLayout
93
+ defaultOpen={{ left: false, right: false }}
94
+ panels={{
95
+ ...PANELS,
96
+ right: { ...PANELS.right!, open: true },
97
+ }}
98
+ variant="web"
99
+ />
100
+ )
101
+
102
+ expect(leftRail().getAttribute("data-state")).toBe("collapsed")
103
+ expect(rightDrawer().getAttribute("data-state")).toBe("collapsed")
104
+ })
105
+
106
+ test("⌘B toggles the left rail", async () => {
107
+ const user = userEvent.setup()
108
+ render(<ShellLayout panels={PANELS} variant="web" />)
109
+
110
+ expect(leftRail().getAttribute("data-state")).toBe("expanded")
111
+
112
+ await user.keyboard("{Meta>}b{/Meta}")
113
+ expect(leftRail().getAttribute("data-state")).toBe("collapsed")
114
+
115
+ await user.keyboard("{Meta>}b{/Meta}")
116
+ expect(leftRail().getAttribute("data-state")).toBe("expanded")
117
+ })
118
+ })
119
+
120
+ function HookedShell({
121
+ onPanelChange,
122
+ }: {
123
+ onPanelChange?: UseShellPanelsOptions["onPanelChange"]
124
+ }) {
125
+ const panels = useShellPanels({ onPanelChange })
126
+
127
+ return (
128
+ <ShellLayout {...panels.providerProps} panels={PANELS} variant="web">
129
+ <button onClick={() => panels.toggle("right")} type="button">
130
+ External toggle
131
+ </button>
132
+ </ShellLayout>
133
+ )
134
+ }
135
+
136
+ describe("useShellPanels", () => {
137
+ test("reports every change as (open, side)", async () => {
138
+ const user = userEvent.setup()
139
+ const onPanelChange = mock(() => {})
140
+ render(<HookedShell onPanelChange={onPanelChange} />)
141
+
142
+ await user.click(screen.getByRole("button", { name: "Toggle panel" }))
143
+ expect(onPanelChange).toHaveBeenLastCalledWith(true, "right")
144
+
145
+ await user.click(screen.getByRole("button", { name: "Toggle sidebar" }))
146
+ expect(onPanelChange).toHaveBeenLastCalledWith(false, "left")
147
+
148
+ expect(onPanelChange).toHaveBeenCalledTimes(2)
149
+ })
150
+
151
+ test("controls the shell from outside it", async () => {
152
+ const user = userEvent.setup()
153
+ render(<HookedShell />)
154
+
155
+ expect(rightDrawer().getAttribute("data-state")).toBe("collapsed")
156
+
157
+ await user.click(screen.getByRole("button", { name: "External toggle" }))
158
+ expect(rightDrawer().getAttribute("data-state")).toBe("expanded")
159
+
160
+ await user.click(screen.getByRole("button", { name: "External toggle" }))
161
+ expect(rightDrawer().getAttribute("data-state")).toBe("collapsed")
162
+ })
163
+
164
+ test("the shell's own triggers round-trip through the hook's state", async () => {
165
+ const user = userEvent.setup()
166
+ render(<HookedShell />)
167
+
168
+ // Trigger click flows through providerProps into hook state and back —
169
+ // the drawer only opens because the hook re-rendered the controlled
170
+ // provider.
171
+ await user.click(screen.getByRole("button", { name: "Toggle panel" }))
172
+ expect(rightDrawer().getAttribute("data-state")).toBe("expanded")
173
+
174
+ // The external toggle sees that state, so it closes rather than opens.
175
+ await user.click(screen.getByRole("button", { name: "External toggle" }))
176
+ expect(rightDrawer().getAttribute("data-state")).toBe("collapsed")
177
+ })
178
+ })
@@ -1,3 +1,7 @@
1
- export * from "./shell-layout"
1
+ import { ShellLayout } from "./shell-layout"
2
+
2
3
  export type * from "./type"
4
+ export * from "./use-shell-panels"
3
5
  export * from "./utils"
6
+
7
+ export { ShellLayout as default }
@@ -1,6 +1,6 @@
1
1
  "use client"
2
2
 
3
- import { MenuIcon, PanelRightIcon } from "lucide-react"
3
+ import { MotionIcon } from "motion-icons-react"
4
4
  import * as React from "react"
5
5
 
6
6
  import {
@@ -10,44 +10,51 @@ import {
10
10
  AnimatedSidebarTrigger,
11
11
  } from "@/components/motion/animated-sidebar"
12
12
  import { cn } from "@/lib/utils"
13
- import type { ShellLayoutProps } from "./type"
13
+ import type { ShellLayoutProps, ShellPanelControlProps } from "./type"
14
14
  import { TITLE_BAR_HEIGHT } from "./utils"
15
15
  import type { ClassNameValue } from "tailwind-merge"
16
16
 
17
17
  export function ShellLayout({
18
18
  children,
19
- open,
20
- defaultOpen,
21
- onOpenChange,
22
- rightDrawer,
23
- rightOpen,
24
- defaultRightOpen,
25
- onRightOpenChange,
26
19
  variant = "desktop",
20
+ panels,
27
21
  className,
22
+ defaultOpen,
23
+ ...panelControlProps
28
24
  }: ShellLayoutProps): React.ReactElement {
29
25
 
30
26
  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`
27
+
28
+ // Each panel seeds its own side's initial state (`defaultOpen ?? open`);
29
+ // an explicit `defaultOpen` record — usually from useShellPanels — wins
30
+ // per side.
31
+ const initialOpen: ShellPanelControlProps["defaultOpen"] = {
32
+ ...(panels?.left && {
33
+ left: panels.left.defaultOpen ?? panels.left.open,
34
+ }),
35
+ ...(panels?.right && {
36
+ right: panels.right.defaultOpen ?? panels.right.open,
37
+ }),
38
+ ...(defaultOpen)
39
+ }
40
+
31
41
  return (
32
42
  <AnimatedSidebarProvider
33
- className={cn("block-full", className, background, 'relative pr-2')}
34
- defaultOpen={defaultOpen}
35
- defaultOpenRight={defaultRightOpen}
36
- onOpenChange={onOpenChange}
37
- onOpenRightChange={onRightOpenChange}
38
- open={open}
39
- openRight={rightOpen}
43
+ {...panelControlProps}
44
+ defaultOpen={initialOpen}
45
+ className={cn("block-full", className, background, "relative pr-2")}
40
46
  style={{
41
47
  paddingTop: variant === "desktop" ? TITLE_BAR_HEIGHT : 0
42
48
  }}
43
49
  >
44
50
  <AnimatedSidebar
45
51
  ariaLabel="Primary navigation"
46
- className={cn("")}
47
52
  collapsible="icon"
48
53
  role="navigation"
49
54
  variant="inset"
50
- ></AnimatedSidebar>
55
+ >
56
+ {panels?.left?.component}
57
+ </AnimatedSidebar>
51
58
 
52
59
  <AnimatedSidebarInset
53
60
  className={cn(
@@ -58,12 +65,12 @@ export function ShellLayout({
58
65
  <header className="flex h-16 items-center justify-between gap-3 px-4 sm:px-6">
59
66
  <div className="flex min-w-0 items-center gap-3">
60
67
  <AnimatedSidebarTrigger>
61
- <MenuIcon className="size-5" />
68
+ <MotionIcon name="PanelLeft" size={24} animation="press" />
62
69
  </AnimatedSidebarTrigger>
63
70
  </div>
64
71
  <div className="flex shrink-0 items-center gap-3">
65
72
  <AnimatedSidebarTrigger aria-label="Toggle panel" side="right">
66
- <PanelRightIcon className="size-5" />
73
+ <MotionIcon name="PanelRight" size={24} animation="press" />
67
74
  </AnimatedSidebarTrigger>
68
75
  </div>
69
76
  </header>
@@ -72,10 +79,8 @@ export function ShellLayout({
72
79
  </div>
73
80
  </AnimatedSidebarInset>
74
81
 
75
-
76
-
77
82
  <AnimatedSidebar
78
- ariaLabel="Secondary panel"
83
+ ariaLabel={panels?.right?.name ?? "Secondary panel"}
79
84
  // Below md the panel is portal led over the page, so it carries the
80
85
  // surface itself; the desktop rail keeps it on the inner panel.
81
86
  className={cn("max-md:border-l max-md:border-neutral-200 max-md:bg-white dark:max-md:border-neutral-700 dark:max-md:bg-card")}
@@ -92,7 +97,7 @@ export function ShellLayout({
92
97
  side="right"
93
98
  variant="inset"
94
99
  >
95
- {rightDrawer}
100
+ {panels?.right?.component}
96
101
  </AnimatedSidebar>
97
102
  </AnimatedSidebarProvider>
98
103
  )
@@ -1,50 +1,92 @@
1
- // import type {
2
- // ISidebarNavItem,
3
- // ISidebarNavSection,
4
- // ISidebarNavSubItem,
5
- // } from "@/components/blocks/nav/types"
1
+ import type { ReactNode } from "react"
6
2
 
3
+ import type {
4
+ AnimatedSidebarProviderProps,
5
+ SidebarSide,
6
+ } from "@/components/motion/animated-sidebar"
7
7
 
8
8
  export interface ShellAction {
9
9
  id: string
10
10
  label: string
11
- icon: React.ReactNode
12
- badge?: React.ReactNode
11
+ icon: ReactNode
12
+ badge?: ReactNode
13
13
  onSelect?: () => void
14
14
  }
15
15
 
16
16
  export interface ShellWorkspace {
17
17
  name: string
18
- logo?: React.ReactNode
19
- meta?: React.ReactNode
18
+ logo?: ReactNode
19
+ meta?: ReactNode
20
20
  }
21
21
 
22
- export interface ShellLayoutProps {
23
- children?: React.ReactNode
24
- // sections?: ISidebarNavSection[]
25
- //activeId?: string
26
- // onNavigate?: (item: ISidebarNavItem | ISidebarNavSubItem) => void
27
- // workspace?: ShellWorkspace
28
- //sidebarFooter?: React.ReactNode
29
- // topActions?: ShellAction[]
30
- //title?: React.ReactNode
31
- //eyebrow?: React.ReactNode
32
- //searchPlaceholder?: string
33
- //searchShortcut?: React.ReactNode
34
- open?: boolean
22
+ export interface IPanel {
23
+ id: string
24
+ name: string
25
+ component: ReactNode
26
+ open: boolean
35
27
  defaultOpen?: boolean
36
- onOpenChange?: (open: boolean) => void
37
- /** Content of the right drawer. It has no icon rail — it is either fully
38
- * shown or fully hidden. */
39
- rightDrawer?: React.ReactNode
40
- rightOpen?: boolean
41
- defaultRightOpen?: boolean
42
- onRightOpenChange?: (open: boolean) => void
43
- // openMobile?: boolean
44
- //defaultOpenMobile?: boolean
45
- //onOpenMobileChange?: (open: boolean) => void
28
+ side: "left" | "bottom" | "right" | "sidebar"
29
+ }
30
+
31
+ /** The side a panel docks to, as a value union (`IPanel["side"]`).
32
+ * `Pick<IPanel, "side">` would be the object type `{ side: ... }`, which can
33
+ * neither key a Record nor travel as a plain callback argument. */
34
+ export type TPanelSide = IPanel["side"]
35
+
36
+ /** Panel content keyed by the side it docks to. `Side` narrows the map to
37
+ * the sides a surface actually renders (the shell renders left and right).
38
+ * Entries are optional — a shell with no bottom dock simply has no `bottom`
39
+ * key. Where an entry's own `side` field disagrees with its key, the key
40
+ * wins. */
41
+ export type TPanelMap<Side extends TPanelSide = TPanelSide> = Partial<
42
+ Record<Side, IPanel>
43
+ >
44
+
45
+ /** The open/close surface of the shell's panels, lifted straight from
46
+ * AnimatedSidebarProvider — every prop is keyed by side, there are no
47
+ * explicit per-side props. `useShellPanels` produces the controlled subset
48
+ * of these as `providerProps`. */
49
+ export type ShellPanelControlProps = Pick<
50
+ AnimatedSidebarProviderProps,
51
+ | "open"
52
+ | "defaultOpen"
53
+ | "onOpenChange"
54
+ | "openMobile"
55
+ | "defaultOpenMobile"
56
+ | "onOpenMobileChange"
57
+ >
58
+
59
+ export interface UseShellPanelsOptions {
60
+ /** Initial desktop open state per side, merged over
61
+ * `{ left: true, right: false }`. */
62
+ defaultOpen?: AnimatedSidebarProviderProps["defaultOpen"]
63
+ /** Initial mobile overlay state per side — every side starts closed. */
64
+ defaultOpenMobile?: AnimatedSidebarProviderProps["defaultOpenMobile"]
65
+ /** Panel change event returning both the next open state and the side of
66
+ * the panel the change comes from. Mobile overlay changes report the same
67
+ * side as their desktop counterpart. */
68
+ onPanelChange?: (open: boolean, side: TPanelSide) => void
69
+ }
70
+
71
+ export interface ShellPanelControls {
72
+ /** Live desktop open state, keyed by side. */
73
+ open: Record<SidebarSide, boolean>
74
+ /** Live mobile overlay state, keyed by side. */
75
+ openMobile: Record<SidebarSide, boolean>
76
+ setOpen: (open: boolean, side: SidebarSide) => void
77
+ setOpenMobile: (open: boolean, side: SidebarSide) => void
78
+ /** Desktop-only convenience — the in-shell triggers already pick the
79
+ * mobile state themselves when the viewport is narrow. */
80
+ toggle: (side: SidebarSide) => void
81
+ /** Spread onto ShellLayout (or AnimatedSidebarProvider directly). */
82
+ providerProps: ShellPanelControlProps
83
+ }
84
+
85
+ export interface ShellLayoutProps extends ShellPanelControlProps {
86
+ children?: ReactNode
87
+ /** Content for the shell's panels, keyed by the side. The shell renders
88
+ * the `left` rail and the `right` drawer; other sides are reserved. */
89
+ panels?: TPanelMap
46
90
  className?: string
47
- variant: 'web' | 'desktop'
48
- //sidebarClassName?: string
49
- //contentClassName?: string
91
+ variant?: "web" | "desktop"
50
92
  }