@exegia/corpora-ui 0.10.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.
- package/dist-lib/components/agents/ai-sidebar.d.ts +2 -0
- package/dist-lib/components/agents/index.d.ts +12 -0
- package/dist-lib/components/agents/marquee-label.d.ts +4 -0
- package/dist-lib/components/agents/sidebar-icon.d.ts +2 -0
- package/dist-lib/components/agents/sidebar-row.d.ts +2 -0
- package/dist-lib/components/agents/type.d.ts +77 -0
- package/dist-lib/components/agents/use-ai-sidebar.d.ts +31 -0
- package/dist-lib/components/agents/utils.d.ts +16 -0
- package/dist-lib/components/beste/piece/browser-frame.d.ts +2 -0
- package/dist-lib/components/beste/piece/type.d.ts +9 -0
- package/dist-lib/components/beste/piece/utils.d.ts +3 -0
- package/dist-lib/components/blocks/auth/signup-block.d.ts +5 -5
- package/dist-lib/components/blocks/nav/sidebar-block.d.ts +3 -56
- package/dist-lib/components/blocks/nav/sidebar-nav-row.d.ts +3 -0
- package/dist-lib/components/blocks/nav/types.d.ts +60 -0
- package/dist-lib/components/blocks/nav/utils.d.ts +1 -0
- package/dist-lib/components/blocks/profile/profile-card-block.d.ts +6 -1
- package/dist-lib/components/blocks/shell/index.d.ts +1 -0
- package/dist-lib/components/blocks/shell/shell-layout.d.ts +3 -0
- package/dist-lib/components/blocks/shell/type.d.ts +26 -0
- package/dist-lib/components/blocks/shell/utils.d.ts +1 -0
- package/dist-lib/components/composed/social-providers.d.ts +9 -1
- package/dist-lib/components/docs/demo-controls.d.ts +4 -4
- package/dist-lib/components/motion/animated-sidebar.d.ts +12 -1
- package/dist-lib/components/motion/popover-morph.d.ts +35 -0
- package/dist-lib/components/motion/popover-position.d.ts +15 -0
- package/dist-lib/components/ui/badge.d.ts +7 -0
- package/dist-lib/components/ui/select.d.ts +30 -0
- package/dist-lib/index.d.ts +1 -0
- package/dist-lib/index.js +644 -524
- package/dist-lib/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/agents/ai-sidebar.tsx +116 -0
- package/src/components/agents/index.ts +14 -0
- package/src/components/agents/marquee-label.tsx +59 -0
- package/src/components/agents/sidebar-icon.tsx +14 -0
- package/src/components/agents/sidebar-row.tsx +271 -0
- package/src/components/agents/type.ts +89 -0
- package/src/components/agents/use-ai-sidebar.ts +386 -0
- package/src/components/agents/utils.ts +142 -0
- package/src/components/beste/piece/browser-frame.tsx +55 -0
- package/src/components/beste/piece/type.ts +11 -0
- package/src/components/beste/piece/utils.ts +21 -0
- package/src/components/blocks/auth/linked-accounts-block.tsx +55 -57
- package/src/components/blocks/auth/passkey-manager-block.tsx +88 -81
- package/src/components/blocks/auth/signup-block.tsx +6 -6
- package/src/components/blocks/nav/sidebar-block.tsx +25 -155
- package/src/components/blocks/nav/sidebar-nav-row.tsx +79 -0
- package/src/components/blocks/nav/types.ts +65 -0
- package/src/components/blocks/nav/utils.ts +4 -0
- package/src/components/blocks/profile/profile-card-block.tsx +17 -6
- package/src/components/blocks/shell/index.ts +1 -0
- package/src/components/blocks/shell/shell-layout.tsx +115 -0
- package/src/components/blocks/shell/type.ts +50 -0
- package/src/components/blocks/shell/utils.ts +2 -0
- package/src/components/composed/social-providers.tsx +83 -27
- package/src/components/docs/component-preview.tsx +13 -12
- package/src/components/docs/demo-controls.tsx +37 -21
- package/src/components/motion/animated-sidebar.tsx +392 -364
- package/src/components/motion/popover-morph.tsx +292 -0
- package/src/components/motion/popover-position.ts +86 -0
- package/src/components/ui/badge.tsx +52 -0
- package/src/components/ui/select.tsx +257 -0
- package/src/index.css +18 -0
- package/src/index.ts +1 -0
package/package.json
CHANGED
|
@@ -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
|
+
}
|