@exegia/corpora-ui 0.9.0 → 0.11.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 (67) hide show
  1. package/dist-lib/components/agents/ai-sidebar.d.ts +2 -0
  2. package/dist-lib/components/agents/index.d.ts +12 -0
  3. package/dist-lib/components/agents/marquee-label.d.ts +4 -0
  4. package/dist-lib/components/agents/sidebar-icon.d.ts +2 -0
  5. package/dist-lib/components/agents/sidebar-row.d.ts +2 -0
  6. package/dist-lib/components/agents/type.d.ts +77 -0
  7. package/dist-lib/components/agents/use-ai-sidebar.d.ts +31 -0
  8. package/dist-lib/components/agents/utils.d.ts +16 -0
  9. package/dist-lib/components/beste/piece/browser-frame.d.ts +2 -0
  10. package/dist-lib/components/beste/piece/type.d.ts +9 -0
  11. package/dist-lib/components/beste/piece/utils.d.ts +3 -0
  12. package/dist-lib/components/blocks/auth/linked-accounts-block.d.ts +7 -1
  13. package/dist-lib/components/blocks/auth/signup-block.d.ts +5 -5
  14. package/dist-lib/components/blocks/nav/sidebar-block.d.ts +3 -56
  15. package/dist-lib/components/blocks/nav/sidebar-nav-row.d.ts +3 -0
  16. package/dist-lib/components/blocks/nav/types.d.ts +60 -0
  17. package/dist-lib/components/blocks/nav/utils.d.ts +1 -0
  18. package/dist-lib/components/blocks/profile/profile-card-block.d.ts +6 -1
  19. package/dist-lib/components/blocks/shell/index.d.ts +1 -0
  20. package/dist-lib/components/blocks/shell/shell-layout.d.ts +3 -0
  21. package/dist-lib/components/blocks/shell/type.d.ts +26 -0
  22. package/dist-lib/components/blocks/shell/utils.d.ts +1 -0
  23. package/dist-lib/components/composed/social-providers.d.ts +9 -1
  24. package/dist-lib/components/docs/demo-controls.d.ts +4 -4
  25. package/dist-lib/components/motion/animated-sidebar.d.ts +12 -1
  26. package/dist-lib/components/motion/popover-morph.d.ts +35 -0
  27. package/dist-lib/components/motion/popover-position.d.ts +15 -0
  28. package/dist-lib/components/ui/badge.d.ts +7 -0
  29. package/dist-lib/components/ui/select.d.ts +30 -0
  30. package/dist-lib/index.d.ts +1 -0
  31. package/dist-lib/index.js +669 -549
  32. package/dist-lib/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/components/agents/ai-sidebar.tsx +116 -0
  35. package/src/components/agents/index.ts +14 -0
  36. package/src/components/agents/marquee-label.tsx +59 -0
  37. package/src/components/agents/sidebar-icon.tsx +14 -0
  38. package/src/components/agents/sidebar-row.tsx +271 -0
  39. package/src/components/agents/type.ts +89 -0
  40. package/src/components/agents/use-ai-sidebar.ts +386 -0
  41. package/src/components/agents/utils.ts +142 -0
  42. package/src/components/beste/piece/browser-frame.tsx +55 -0
  43. package/src/components/beste/piece/type.ts +11 -0
  44. package/src/components/beste/piece/utils.ts +21 -0
  45. package/src/components/blocks/auth/__tests__/linked-accounts-block.test.tsx +18 -0
  46. package/src/components/blocks/auth/linked-accounts-block.tsx +63 -57
  47. package/src/components/blocks/auth/passkey-manager-block.tsx +88 -81
  48. package/src/components/blocks/auth/signup-block.tsx +6 -6
  49. package/src/components/blocks/nav/sidebar-block.tsx +25 -155
  50. package/src/components/blocks/nav/sidebar-nav-row.tsx +79 -0
  51. package/src/components/blocks/nav/types.ts +65 -0
  52. package/src/components/blocks/nav/utils.ts +4 -0
  53. package/src/components/blocks/profile/profile-card-block.tsx +17 -6
  54. package/src/components/blocks/shell/index.ts +1 -0
  55. package/src/components/blocks/shell/shell-layout.tsx +115 -0
  56. package/src/components/blocks/shell/type.ts +50 -0
  57. package/src/components/blocks/shell/utils.ts +2 -0
  58. package/src/components/composed/social-providers.tsx +83 -27
  59. package/src/components/docs/component-preview.tsx +13 -12
  60. package/src/components/docs/demo-controls.tsx +37 -21
  61. package/src/components/motion/animated-sidebar.tsx +392 -364
  62. package/src/components/motion/popover-morph.tsx +292 -0
  63. package/src/components/motion/popover-position.ts +86 -0
  64. package/src/components/ui/badge.tsx +52 -0
  65. package/src/components/ui/select.tsx +257 -0
  66. package/src/index.css +18 -0
  67. package/src/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@exegia/corpora-ui",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "shadcn-ready React UI library for the corpora manuscript-research apps.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,116 @@
1
+ "use client";
2
+
3
+ import { AnimatePresence } from "motion/react";
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"
8
+
9
+ export function AISidebar({
10
+ renderIcon,
11
+ renderMenu,
12
+ ariaLabel = "Resources",
13
+ className,
14
+ ...options
15
+ }: AISidebarProps) {
16
+ const {
17
+ flat,
18
+ selectedId,
19
+ expandedIds,
20
+ focusedId,
21
+ setFocusedId,
22
+ draggingId,
23
+ dropTarget,
24
+ hoveredId,
25
+ hoverLayoutId,
26
+ handleRowHover,
27
+ clearHover,
28
+ menuOpenId,
29
+ renamingId,
30
+ setRenamingId,
31
+ announcement,
32
+ select,
33
+ toggle,
34
+ handleKeyDown,
35
+ handleRootDragOver,
36
+ handleDrop,
37
+ handleRowDragStart,
38
+ handleRowDragEnd,
39
+ handleRowDragOver,
40
+ commitRename,
41
+ handleMenuOpenChange,
42
+ setRowRef,
43
+ } = useAISidebar(options);
44
+
45
+ return (
46
+ <>
47
+ <div
48
+ role="tree"
49
+ aria-label={ariaLabel}
50
+ aria-multiselectable="false"
51
+ onDragOver={handleRootDragOver}
52
+ onDrop={handleDrop}
53
+ onMouseLeave={clearHover}
54
+ className={cn(
55
+ "relative flex min-w-0 flex-col gap-0.5 [overflow-anchor:none] group-data-[state=collapsed]/sidebar:hidden",
56
+ draggingId && "select-none pb-9",
57
+ className,
58
+ )}
59
+ >
60
+ <AnimatePresence initial={false}>
61
+ {flat.map((row) => (
62
+ <ResourceRow
63
+ key={row.item.id}
64
+ row={row}
65
+ active={selectedId === row.item.id}
66
+ expanded={expandedIds.has(row.item.id)}
67
+ focused={focusedId === row.item.id}
68
+ draggingId={draggingId}
69
+ dropTarget={dropTarget}
70
+ menuOpen={menuOpenId === row.item.id}
71
+ renaming={renamingId === row.item.id}
72
+ hoverActive={hoveredId !== null && !draggingId}
73
+ hoverPill={
74
+ hoveredId === row.item.id &&
75
+ !draggingId &&
76
+ !row.item.disabled
77
+ }
78
+ hoverLayoutId={hoverLayoutId}
79
+ onHoverChange={(hovered) =>
80
+ handleRowHover(row.item.id, hovered)
81
+ }
82
+ onFocus={() => setFocusedId(row.item.id)}
83
+ onSelect={() => select(row.item.id)}
84
+ onToggle={() => toggle(row.item.id)}
85
+ onKeyDown={(event) => handleKeyDown(event, row)}
86
+ onRenameStart={() => setRenamingId(row.item.id)}
87
+ onRenameCancel={() => setRenamingId(null)}
88
+ onRenameCommit={(label) => commitRename(row, label)}
89
+ onMenuOpenChange={(open) => handleMenuOpenChange(row, open)}
90
+ onDragStart={handleRowDragStart}
91
+ onDragEnd={handleRowDragEnd}
92
+ onDragOver={handleRowDragOver}
93
+ onDrop={handleDrop}
94
+ renderIcon={renderIcon}
95
+ renderMenu={renderMenu}
96
+ setRef={(node) => setRowRef(row.item.id, node)}
97
+ />
98
+ ))}
99
+ </AnimatePresence>
100
+
101
+ {draggingId ? (
102
+ <div
103
+ aria-hidden="true"
104
+ data-active={dropTarget?.id === null || undefined}
105
+ className="absolute inset-x-1 bottom-0 flex h-8 items-center justify-center rounded-lg border border-dashed border-border text-[10px] text-muted-foreground data-[active=true]:border-primary/50 data-[active=true]:bg-primary/10 data-[active=true]:text-foreground"
106
+ >
107
+ Move to top level
108
+ </div>
109
+ ) : null}
110
+ </div>
111
+ <span className="sr-only" aria-live="polite">
112
+ {announcement}
113
+ </span>
114
+ </>
115
+ );
116
+ }
@@ -0,0 +1,14 @@
1
+
2
+ import { AISidebar } from "./ai-sidebar"
3
+ import { MarqueeLabel } from "./marquee-label.tsx"
4
+ import { ResourceRow } from "./sidebar-row.tsx"
5
+ import { SidebarIcon } from "./sidebar-icon.tsx"
6
+ export { useAISidebar, type UseAISidebarOptions } from "./use-ai-sidebar.ts"
7
+ export type * from "./type.ts"
8
+
9
+ export const Sidebar = {
10
+ Wrapper: AISidebar,
11
+ MarqueeLabel,
12
+ Row: ResourceRow,
13
+ Icon: SidebarIcon
14
+ }
@@ -0,0 +1,59 @@
1
+ import { motion, useReducedMotion } from "motion/react"
2
+ import { useEffect, useRef, useState } from "react"
3
+ import { ROW_REVEAL } from "@/components/agents/utils.ts"
4
+
5
+ export function MarqueeLabel({
6
+ active,
7
+ children,
8
+ }: {
9
+ active: boolean
10
+ children: string
11
+ }) {
12
+ const reduce = useReducedMotion() ?? false
13
+ const viewportRef = useRef<HTMLSpanElement>(null)
14
+ const labelRef = useRef<HTMLSpanElement>(null)
15
+ const [distance, setDistance] = useState(0)
16
+
17
+ useEffect(() => {
18
+ const measure = () => {
19
+ const viewport = viewportRef.current
20
+ const label = labelRef.current
21
+ if (!viewport || !label) return
22
+ setDistance(
23
+ label.scrollWidth > viewport.clientWidth ? label.scrollWidth + 24 : 0
24
+ )
25
+ }
26
+ measure()
27
+ const observer = new ResizeObserver(measure)
28
+ if (viewportRef.current) observer.observe(viewportRef.current)
29
+ if (labelRef.current) observer.observe(labelRef.current)
30
+ return () => observer.disconnect()
31
+ }, [])
32
+
33
+ const running = active && distance > 0 && !reduce
34
+
35
+ return (
36
+ <span
37
+ ref={viewportRef}
38
+ className="relative block min-w-0 flex-1 overflow-hidden"
39
+ >
40
+ <motion.span
41
+ className="flex w-max items-center gap-6 whitespace-nowrap"
42
+ animate={{ x: running ? [0, -distance] : 0 }}
43
+ transition={
44
+ running
45
+ ? {
46
+ duration: Math.max(2.4, distance / 34),
47
+ ease: "linear",
48
+ repeat: Number.POSITIVE_INFINITY,
49
+ repeatDelay: 2,
50
+ }
51
+ : ROW_REVEAL
52
+ }
53
+ >
54
+ <span ref={labelRef}>{children}</span>
55
+ {running ? <span aria-hidden="true">{children}</span> : null}
56
+ </motion.span>
57
+ </span>
58
+ )
59
+ }
@@ -0,0 +1,14 @@
1
+ import type { SidebarResource } from "@/components/agents/type.ts"
2
+ import { Bookmark, FileText, Folder, FolderOpen } from "lucide-react"
3
+
4
+ export function SidebarIcon(item: SidebarResource, expanded: boolean) {
5
+ const Icon =
6
+ item.kind === "folder" || item.kind === "project"
7
+ ? expanded
8
+ ? FolderOpen
9
+ : Folder
10
+ : item.kind === "bookmark"
11
+ ? Bookmark
12
+ : FileText
13
+ return <Icon className="size-4" />
14
+ }
@@ -0,0 +1,271 @@
1
+ import {
2
+ useEffect,
3
+ useRef,
4
+ useState,
5
+ } from "react"
6
+ import type { ResourceRowProps } from "@/components/agents/type.ts"
7
+ import {
8
+ AnimatePresence,
9
+ motion,
10
+ useReducedMotion,
11
+ type Variants,
12
+ } from "motion/react"
13
+ import { MoreHorizontal, Pencil } from "lucide-react"
14
+ import { SPRING_LAYOUT } from "@/lib/ease.ts"
15
+ import { cn } from "@/lib/utils.ts"
16
+ import { canContain } from "@/components/agents/utils.ts"
17
+ import { SidebarIcon } from "@/components/agents/sidebar-icon.tsx"
18
+ import { MarqueeLabel } from "@/components/agents/marquee-label.tsx"
19
+ import {
20
+ MorphPopover,
21
+ MorphPopoverContent,
22
+ MorphPopoverTrigger,
23
+ } from "@/components/motion/popover-morph.tsx"
24
+
25
+ // Same choreography as SharedLayoutBg: the wrapper only fades/blurs out when
26
+ // the pointer leaves the whole tree; a row-to-row move exits instantly so the
27
+ // layoutId pill slides instead of crossfading.
28
+ const pillVariants: Variants = {
29
+ initial: { opacity: 0, filter: "blur(6px)" },
30
+ animate: { opacity: 1, filter: "blur(0px)" },
31
+ exit: (hoverActive: boolean) =>
32
+ !hoverActive ? { opacity: 0, filter: "blur(6px)" } : {},
33
+ }
34
+
35
+ const reducedPillVariants: Variants = {
36
+ initial: { opacity: 0 },
37
+ animate: { opacity: 1 },
38
+ exit: (hoverActive: boolean) => (!hoverActive ? { opacity: 0 } : {}),
39
+ }
40
+
41
+ export function ResourceRow({
42
+ row,
43
+ active,
44
+ expanded,
45
+ focused,
46
+ draggingId,
47
+ dropTarget,
48
+ menuOpen,
49
+ renaming,
50
+ onDragEnd,
51
+ onDragOver,
52
+ onDragStart,
53
+ onDrop,
54
+ onFocus,
55
+ onKeyDown,
56
+ onMenuOpenChange,
57
+ onRenameCancel,
58
+ onRenameCommit,
59
+ onRenameStart,
60
+ onSelect,
61
+ onToggle,
62
+ hoverActive = false,
63
+ hoverPill = false,
64
+ hoverLayoutId,
65
+ onHoverChange,
66
+ renderIcon,
67
+ renderMenu,
68
+ setRef,
69
+ }: ResourceRowProps) {
70
+ const reduce = useReducedMotion() ?? false
71
+ const [hovered, setHovered] = useState(false)
72
+ const inputRef = useRef<HTMLInputElement>(null)
73
+ const skipRenameBlurRef = useRef(false)
74
+ const draggedRef = useRef(false)
75
+ const [draft, setDraft] = useState(row.item.label)
76
+ const acceptsChildren = canContain(row.item)
77
+ const isDragging = draggingId === row.item.id
78
+ const dropPosition =
79
+ dropTarget?.id === row.item.id ? dropTarget.position : null
80
+
81
+ useEffect(() => {
82
+ if (!renaming) return
83
+ skipRenameBlurRef.current = false
84
+ // setDraft(row.item.label);
85
+ requestAnimationFrame(() => {
86
+ inputRef.current?.focus()
87
+ inputRef.current?.select()
88
+ })
89
+ }, [renaming, row.item.label])
90
+
91
+ const menu = renderMenu?.(row.item, {
92
+ close: () => onMenuOpenChange(false),
93
+ rename: () => {
94
+ onMenuOpenChange(false)
95
+ onRenameStart()
96
+ },
97
+ }) ?? (
98
+ <button
99
+ type="button"
100
+ onClick={() => {
101
+ onMenuOpenChange(false)
102
+ onRenameStart()
103
+ }}
104
+ className="flex h-8 w-full items-center gap-2 rounded-lg px-2.5 text-left text-xs text-foreground transition-colors outline-none hover:bg-muted focus-visible:bg-muted focus-visible:ring-2 focus-visible:ring-ring"
105
+ >
106
+ <Pencil aria-hidden="true" className="size-3.5" />
107
+ Rename
108
+ </button>
109
+ )
110
+
111
+ return (
112
+ <motion.div
113
+ ref={setRef}
114
+ layout="position"
115
+ transition={reduce ? { duration: 0 } : SPRING_LAYOUT}
116
+ role="treeitem"
117
+ aria-level={row.depth + 1}
118
+ aria-selected={acceptsChildren ? undefined : active}
119
+ aria-expanded={acceptsChildren ? expanded : undefined}
120
+ aria-disabled={row.item.disabled || undefined}
121
+ tabIndex={focused ? 0 : -1}
122
+ draggable={!row.item.disabled && !renaming}
123
+ data-menu-open={menuOpen || undefined}
124
+ data-drop={dropPosition ?? undefined}
125
+ data-dragging={isDragging || undefined}
126
+ onFocus={onFocus}
127
+ onKeyDown={onKeyDown}
128
+ onClick={(event) => {
129
+ if (
130
+ event.defaultPrevented ||
131
+ draggedRef.current ||
132
+ renaming ||
133
+ row.item.disabled
134
+ )
135
+ return
136
+ if (acceptsChildren) onToggle()
137
+ else onSelect()
138
+ }}
139
+ onDoubleClick={(event) => {
140
+ if (acceptsChildren || row.item.disabled) return
141
+ event.preventDefault()
142
+ onRenameStart()
143
+ }}
144
+ onMouseEnter={() => {
145
+ setHovered(true)
146
+ onHoverChange?.(true)
147
+ }}
148
+ onMouseLeave={() => {
149
+ setHovered(false)
150
+ onHoverChange?.(false)
151
+ }}
152
+ onDragStartCapture={(event) => {
153
+ draggedRef.current = true
154
+ onDragStart(event, row.item.id)
155
+ }}
156
+ onDragEndCapture={() => {
157
+ onDragEnd()
158
+ requestAnimationFrame(() => {
159
+ draggedRef.current = false
160
+ })
161
+ }}
162
+ onDragOver={(event) => onDragOver(event, row)}
163
+ onDrop={onDrop}
164
+ className={cn(
165
+ "group/resource relative flex min-h-9 min-w-0 cursor-pointer items-center gap-2.5 rounded-md pr-3 text-sm outline-none",
166
+ "text-muted-foreground transition-[color,background-color,scale] duration-150 ease-smooth-out hover:text-foreground motion-reduce:transition-none",
167
+ !row.item.disabled &&
168
+ !renaming &&
169
+ "active:scale-97 motion-reduce:active:scale-100",
170
+ // Fallback for standalone rows that render without the shared pill.
171
+ hoverLayoutId === undefined && "hover:bg-muted",
172
+ "focus-visible:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
173
+ "data-[menu-open=true]:bg-muted data-[menu-open=true]:text-foreground",
174
+ "data-[dragging=true]:opacity-40",
175
+ "data-[drop=inside]:bg-primary/10 data-[drop=inside]:ring-1 data-[drop=inside]:ring-primary/45",
176
+ "data-[drop=before]:before:absolute data-[drop=before]:before:-top-0.5 data-[drop=before]:before:right-2 data-[drop=before]:before:left-2 data-[drop=before]:before:h-0.5 data-[drop=before]:before:rounded-full data-[drop=before]:before:bg-primary",
177
+ "data-[drop=after]:after:absolute data-[drop=after]:after:right-2 data-[drop=after]:after:-bottom-0.5 data-[drop=after]:after:left-2 data-[drop=after]:after:h-0.5 data-[drop=after]:after:rounded-full data-[drop=after]:after:bg-primary",
178
+ !acceptsChildren && active && "bg-accent text-foreground",
179
+ row.item.disabled && "cursor-not-allowed opacity-45"
180
+ )}
181
+ style={{ paddingLeft: `${12 + row.depth * 16}px` }}
182
+ >
183
+ {hoverLayoutId ? (
184
+ <AnimatePresence custom={hoverActive}>
185
+ {hoverActive ? (
186
+ <motion.span
187
+ aria-hidden="true"
188
+ variants={reduce ? reducedPillVariants : pillVariants}
189
+ initial="initial"
190
+ animate="animate"
191
+ exit="exit"
192
+ custom={hoverActive}
193
+ className="pointer-events-none absolute inset-0"
194
+ >
195
+ {hoverPill ? (
196
+ <motion.span
197
+ layoutId={hoverLayoutId}
198
+ transition={reduce ? { duration: 0 } : SPRING_LAYOUT}
199
+ className="block h-full w-full rounded-md bg-muted/50"
200
+ />
201
+ ) : null}
202
+ </motion.span>
203
+ ) : null}
204
+ </AnimatePresence>
205
+ ) : null}
206
+ <span
207
+ aria-hidden="true"
208
+ className="relative grid size-5 shrink-0 place-items-center"
209
+ >
210
+ {renderIcon?.(row.item) ?? SidebarIcon(row.item, expanded)}
211
+ </span>
212
+
213
+ {renaming ? (
214
+ <input
215
+ ref={inputRef}
216
+ value={draft}
217
+ aria-label={`Rename ${row.item.label}`}
218
+ onChange={(event) => setDraft(event.target.value)}
219
+ draggable={false}
220
+ onClick={(event) => event.stopPropagation()}
221
+ onDoubleClick={(event) => event.stopPropagation()}
222
+ onBlur={() => {
223
+ if (!skipRenameBlurRef.current) onRenameCommit(draft)
224
+ }}
225
+ onKeyDown={(event) => {
226
+ event.stopPropagation()
227
+ if (event.key === "Enter") {
228
+ skipRenameBlurRef.current = true
229
+ onRenameCommit(draft)
230
+ }
231
+ if (event.key === "Escape") {
232
+ skipRenameBlurRef.current = true
233
+ onRenameCancel()
234
+ }
235
+ }}
236
+ className="relative mx-1 h-7 min-w-0 flex-1 rounded-md border border-border bg-background px-2 text-sm text-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring"
237
+ />
238
+ ) : (
239
+ <MarqueeLabel active={hovered || menuOpen}>
240
+ {row.item.label}
241
+ </MarqueeLabel>
242
+ )}
243
+
244
+ {!renaming && !row.item.disabled ? (
245
+ <MorphPopover open={menuOpen} onOpenChange={onMenuOpenChange}>
246
+ <MorphPopoverTrigger>
247
+ <button
248
+ type="button"
249
+ draggable={false}
250
+ tabIndex={-1}
251
+ aria-label={`Actions for ${row.item.label}`}
252
+ onClick={(event) => event.stopPropagation()}
253
+ className="relative grid size-7 shrink-0 place-items-center rounded-lg opacity-0 transition-opacity duration-150 ease-smooth-out outline-none group-hover/resource:opacity-80 group-hover/resource:delay-150 group-data-[menu-open=true]/resource:opacity-100 hover:bg-foreground/5 focus-visible:opacity-100 focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none"
254
+ >
255
+ <MoreHorizontal aria-hidden="true" className="size-4" />
256
+ </button>
257
+ </MorphPopoverTrigger>
258
+ <MorphPopoverContent
259
+ side="bottom"
260
+ align="end"
261
+ sideOffset={8}
262
+ radius={12}
263
+ className="w-40 p-1.5"
264
+ >
265
+ <div data-sidebar-resource-menu={row.item.id}>{menu}</div>
266
+ </MorphPopoverContent>
267
+ </MorphPopover>
268
+ ) : null}
269
+ </motion.div>
270
+ )
271
+ }
@@ -0,0 +1,89 @@
1
+ import type { ReactNode, DragEvent, KeyboardEvent } from "react"
2
+
3
+ export type SidebarResourceKind = "folder" | "project" | "file" | "bookmark"
4
+
5
+ export interface SidebarResource {
6
+ id: string
7
+ label: string
8
+ kind: SidebarResourceKind
9
+ children?: SidebarResource[]
10
+ disabled?: boolean
11
+ }
12
+
13
+ export type SidebarResourceDropPosition = "before" | "inside" | "after"
14
+
15
+ export interface SidebarResourceMove {
16
+ itemId: string
17
+ targetId: string | null
18
+ position: SidebarResourceDropPosition
19
+ }
20
+
21
+ export interface SidebarResourceMenuControls {
22
+ close: () => void
23
+ rename: () => void
24
+ }
25
+
26
+ export interface AISidebarProps {
27
+ items?: SidebarResource[]
28
+ defaultItems?: SidebarResource[]
29
+ onItemsChange?: (items: SidebarResource[]) => void
30
+ /** Reject the promise to roll the optimistic move back. */
31
+ onMove?: (move: SidebarResourceMove) => void | Promise<void>
32
+ onMoveError?: (error: unknown, move: SidebarResourceMove) => void
33
+ onRename?: (item: SidebarResource, label: string) => void | Promise<void>
34
+ activeId?: string | null
35
+ defaultActiveId?: string | null
36
+ onActiveChange?: (id: string) => void
37
+ defaultExpandedIds?: string[]
38
+ renderIcon?: (item: SidebarResource) => ReactNode
39
+ renderMenu?: (
40
+ item: SidebarResource,
41
+ controls: SidebarResourceMenuControls
42
+ ) => ReactNode
43
+ ariaLabel?: string
44
+ className?: string
45
+ }
46
+
47
+ export interface FlatResource {
48
+ item: SidebarResource
49
+ depth: number
50
+ parentId: string | null
51
+ }
52
+
53
+ export interface DropTarget {
54
+ id: string | null
55
+ position: SidebarResourceDropPosition
56
+ }
57
+
58
+ export interface ResourceRowProps {
59
+ row: FlatResource
60
+ active: boolean
61
+ expanded: boolean
62
+ focused: boolean
63
+ draggingId: string | null
64
+ dropTarget: DropTarget | null
65
+ menuOpen: boolean
66
+ renaming: boolean
67
+ onDragEnd: () => void
68
+ onDragOver: (event: DragEvent<HTMLDivElement>, row: FlatResource) => void
69
+ onDragStart: (event: DragEvent<HTMLDivElement>, id: string) => void
70
+ onDrop: (event: DragEvent<HTMLDivElement>) => void
71
+ onFocus: () => void
72
+ onKeyDown: (event: KeyboardEvent<HTMLDivElement>) => void
73
+ onMenuOpenChange: (open: boolean) => void
74
+ onRenameCancel: () => void
75
+ onRenameCommit: (label: string) => void
76
+ onRenameStart: () => void
77
+ onSelect: () => void
78
+ onToggle: () => void
79
+ /** Some row in the tree is hovered — mounts the pill fade wrapper. */
80
+ hoverActive?: boolean
81
+ /** This row owns the sliding hover pill. */
82
+ hoverPill?: boolean
83
+ /** Shared motion layoutId that lets the pill travel between rows. */
84
+ hoverLayoutId?: string
85
+ onHoverChange?: (hovered: boolean) => void
86
+ renderIcon?: (item: SidebarResource) => ReactNode
87
+ renderMenu?: AISidebarProps["renderMenu"]
88
+ setRef: (node: HTMLDivElement | null) => void
89
+ }