@exegia/corpora-ui 0.10.0 → 0.12.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/__tests__/ai-sidebar.test.d.ts +1 -0
  2. package/dist-lib/components/agents/ai-sidebar.d.ts +2 -0
  3. package/dist-lib/components/agents/index.d.ts +12 -0
  4. package/dist-lib/components/agents/marquee-label.d.ts +4 -0
  5. package/dist-lib/components/agents/sidebar-icon.d.ts +2 -0
  6. package/dist-lib/components/agents/sidebar-row.d.ts +2 -0
  7. package/dist-lib/components/agents/type.d.ts +83 -0
  8. package/dist-lib/components/agents/use-ai-sidebar.d.ts +31 -0
  9. package/dist-lib/components/agents/utils.d.ts +16 -0
  10. package/dist-lib/components/beste/piece/browser-frame.d.ts +2 -0
  11. package/dist-lib/components/beste/piece/type.d.ts +9 -0
  12. package/dist-lib/components/beste/piece/utils.d.ts +3 -0
  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 +644 -524
  32. package/dist-lib/index.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/components/agents/__tests__/ai-sidebar.test.tsx +93 -0
  35. package/src/components/agents/ai-sidebar.tsx +118 -0
  36. package/src/components/agents/index.ts +14 -0
  37. package/src/components/agents/marquee-label.tsx +59 -0
  38. package/src/components/agents/sidebar-icon.tsx +14 -0
  39. package/src/components/agents/sidebar-row.tsx +309 -0
  40. package/src/components/agents/type.ts +95 -0
  41. package/src/components/agents/use-ai-sidebar.ts +386 -0
  42. package/src/components/agents/utils.ts +142 -0
  43. package/src/components/beste/piece/browser-frame.tsx +55 -0
  44. package/src/components/beste/piece/type.ts +11 -0
  45. package/src/components/beste/piece/utils.ts +21 -0
  46. package/src/components/blocks/auth/linked-accounts-block.tsx +55 -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.10.0",
3
+ "version": "0.12.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,93 @@
1
+ import { describe, expect, test } from "bun:test"
2
+ import { render, screen } from "@testing-library/react"
3
+ import userEvent from "@testing-library/user-event"
4
+
5
+ import { AISidebar } from "../ai-sidebar"
6
+ import type { SidebarResource } from "../type.ts"
7
+
8
+ const RESOURCES: SidebarResource[] = [
9
+ {
10
+ id: "corpora",
11
+ label: "Corpora",
12
+ kind: "project",
13
+ children: [
14
+ { id: "manuscripts", label: "Manuscripts", kind: "folder" },
15
+ { id: "readme", label: "Field notes", kind: "file" },
16
+ ],
17
+ },
18
+ { id: "reading-list", label: "Reading list", kind: "bookmark" },
19
+ { id: "empty-folder", label: "Empty folder", kind: "folder" },
20
+ ]
21
+
22
+ const chevronOf = (row: HTMLElement) =>
23
+ row.querySelector("svg.lucide-chevron-right")
24
+
25
+ describe("AISidebar", () => {
26
+ test("renders the tree and marks the active row", () => {
27
+ render(
28
+ <AISidebar defaultItems={RESOURCES} defaultActiveId="reading-list" />
29
+ )
30
+
31
+ expect(screen.getByRole("tree", { name: "Resources" })).toBeDefined()
32
+ const active = screen.getByRole("treeitem", { name: /Reading list/ })
33
+ expect(active.getAttribute("aria-selected")).toBe("true")
34
+ expect(active.className).toContain("bg-primary/10")
35
+ })
36
+
37
+ test("shows a trailing chevron only on root rows with children", () => {
38
+ render(
39
+ <AISidebar defaultItems={RESOURCES} defaultExpandedIds={["corpora"]} />
40
+ )
41
+
42
+ const root = screen.getByRole("treeitem", { name: /Corpora/ })
43
+ const chevron = chevronOf(root)
44
+ expect(chevron).not.toBeNull()
45
+ // Expanded root rows rotate the chevron to point down.
46
+ expect(chevron?.getAttribute("class")).toContain("rotate-90")
47
+
48
+ // Childless root folder and nested folder both render without one.
49
+ expect(
50
+ chevronOf(screen.getByRole("treeitem", { name: /Empty folder/ }))
51
+ ).toBeNull()
52
+ expect(
53
+ chevronOf(screen.getByRole("treeitem", { name: /Manuscripts/ }))
54
+ ).toBeNull()
55
+ })
56
+
57
+ test("chevron sits flat while the root row is collapsed", () => {
58
+ render(<AISidebar defaultItems={RESOURCES} />)
59
+
60
+ const chevron = chevronOf(
61
+ screen.getByRole("treeitem", { name: /Corpora/ })
62
+ )
63
+ expect(chevron).not.toBeNull()
64
+ expect(chevron?.getAttribute("class")).not.toContain("rotate-90")
65
+ })
66
+
67
+ test("renderActionsTrigger replaces the default actions button", async () => {
68
+ const user = userEvent.setup()
69
+ const selected: string[] = []
70
+ render(
71
+ <AISidebar
72
+ defaultItems={RESOURCES}
73
+ onActiveChange={(id) => selected.push(id)}
74
+ renderActionsTrigger={(item) => (
75
+ <button type="button" aria-label={`Custom actions ${item.label}`}>
76
+ *
77
+ </button>
78
+ )}
79
+ />
80
+ )
81
+
82
+ expect(
83
+ screen.queryByRole("button", { name: "Actions for Reading list" })
84
+ ).toBeNull()
85
+
86
+ // Opening the menu from the custom trigger must not select the row.
87
+ await user.click(
88
+ screen.getByRole("button", { name: "Custom actions Reading list" })
89
+ )
90
+ expect(await screen.findByText("Rename")).toBeDefined()
91
+ expect(selected).toEqual([])
92
+ })
93
+ })
@@ -0,0 +1,118 @@
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
+ renderActionsTrigger,
13
+ ariaLabel = "Resources",
14
+ className,
15
+ ...options
16
+ }: AISidebarProps) {
17
+ const {
18
+ flat,
19
+ selectedId,
20
+ expandedIds,
21
+ focusedId,
22
+ setFocusedId,
23
+ draggingId,
24
+ dropTarget,
25
+ hoveredId,
26
+ hoverLayoutId,
27
+ handleRowHover,
28
+ clearHover,
29
+ menuOpenId,
30
+ renamingId,
31
+ setRenamingId,
32
+ announcement,
33
+ select,
34
+ toggle,
35
+ handleKeyDown,
36
+ handleRootDragOver,
37
+ handleDrop,
38
+ handleRowDragStart,
39
+ handleRowDragEnd,
40
+ handleRowDragOver,
41
+ commitRename,
42
+ handleMenuOpenChange,
43
+ setRowRef,
44
+ } = useAISidebar(options);
45
+
46
+ return (
47
+ <>
48
+ <div
49
+ role="tree"
50
+ aria-label={ariaLabel}
51
+ aria-multiselectable="false"
52
+ onDragOver={handleRootDragOver}
53
+ onDrop={handleDrop}
54
+ onMouseLeave={clearHover}
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,
59
+ )}
60
+ >
61
+ <AnimatePresence initial={false}>
62
+ {flat.map((row) => (
63
+ <ResourceRow
64
+ key={row.item.id}
65
+ row={row}
66
+ active={selectedId === row.item.id}
67
+ expanded={expandedIds.has(row.item.id)}
68
+ focused={focusedId === row.item.id}
69
+ draggingId={draggingId}
70
+ dropTarget={dropTarget}
71
+ menuOpen={menuOpenId === row.item.id}
72
+ renaming={renamingId === row.item.id}
73
+ hoverActive={hoveredId !== null && !draggingId}
74
+ hoverPill={
75
+ hoveredId === row.item.id &&
76
+ !draggingId &&
77
+ !row.item.disabled
78
+ }
79
+ hoverLayoutId={hoverLayoutId}
80
+ onHoverChange={(hovered) =>
81
+ handleRowHover(row.item.id, hovered)
82
+ }
83
+ onFocus={() => setFocusedId(row.item.id)}
84
+ onSelect={() => select(row.item.id)}
85
+ onToggle={() => toggle(row.item.id)}
86
+ onKeyDown={(event) => handleKeyDown(event, row)}
87
+ onRenameStart={() => setRenamingId(row.item.id)}
88
+ onRenameCancel={() => setRenamingId(null)}
89
+ onRenameCommit={(label) => commitRename(row, label)}
90
+ onMenuOpenChange={(open) => handleMenuOpenChange(row, open)}
91
+ onDragStart={handleRowDragStart}
92
+ onDragEnd={handleRowDragEnd}
93
+ onDragOver={handleRowDragOver}
94
+ onDrop={handleDrop}
95
+ renderIcon={renderIcon}
96
+ renderMenu={renderMenu}
97
+ renderActionsTrigger={renderActionsTrigger}
98
+ setRef={(node) => setRowRef(row.item.id, node)}
99
+ />
100
+ ))}
101
+ </AnimatePresence>
102
+
103
+ {draggingId ? (
104
+ <div
105
+ aria-hidden="true"
106
+ data-active={dropTarget?.id === null || undefined}
107
+ className="absolute inset-x-1 bottom-0 flex h-8 items-center justify-center rounded-md 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"
108
+ >
109
+ Move to top level
110
+ </div>
111
+ ) : null}
112
+ </div>
113
+ <span className="sr-only" aria-live="polite">
114
+ {announcement}
115
+ </span>
116
+ </>
117
+ );
118
+ }
@@ -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,309 @@
1
+ import {
2
+ cloneElement,
3
+ useEffect,
4
+ useRef,
5
+ useState,
6
+ type MouseEvent,
7
+ } from "react"
8
+ import type { ResourceRowProps } from "@/components/agents/type.ts"
9
+ import {
10
+ AnimatePresence,
11
+ motion,
12
+ useReducedMotion,
13
+ type Variants,
14
+ } from "motion/react"
15
+ import { ChevronRight, MoreHorizontal, Pencil } from "lucide-react"
16
+ import { SPRING_LAYOUT } from "@/lib/ease.ts"
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"
21
+ import {
22
+ MorphPopover,
23
+ MorphPopoverContent,
24
+ MorphPopoverTrigger,
25
+ } from "@/components/motion/popover-morph.tsx"
26
+ import { Button } from "@/components/ui/button";
27
+
28
+ // Same choreography as SharedLayoutBg: the wrapper only fades/blurs out when
29
+ // the pointer leaves the whole tree; a row-to-row move exits instantly so the
30
+ // layoutId pill slides instead of crossfading.
31
+ const pillVariants: Variants = {
32
+ initial: { opacity: 0, filter: "blur(6px)" },
33
+ animate: { opacity: 1, filter: "blur(0px)" },
34
+ exit: (hoverActive: boolean) =>
35
+ !hoverActive ? { opacity: 0, filter: "blur(6px)" } : {},
36
+ }
37
+
38
+ const reducedPillVariants: Variants = {
39
+ initial: { opacity: 0 },
40
+ animate: { opacity: 1 },
41
+ exit: (hoverActive: boolean) => (!hoverActive ? { opacity: 0 } : {}),
42
+ }
43
+
44
+ export function ResourceRow({
45
+ row,
46
+ active,
47
+ expanded,
48
+ focused,
49
+ draggingId,
50
+ dropTarget,
51
+ menuOpen,
52
+ renaming,
53
+ onDragEnd,
54
+ onDragOver,
55
+ onDragStart,
56
+ onDrop,
57
+ onFocus,
58
+ onKeyDown,
59
+ onMenuOpenChange,
60
+ onRenameCancel,
61
+ onRenameCommit,
62
+ onRenameStart,
63
+ onSelect,
64
+ onToggle,
65
+ hoverActive = false,
66
+ hoverPill = false,
67
+ hoverLayoutId,
68
+ onHoverChange,
69
+ renderIcon,
70
+ renderMenu,
71
+ renderActionsTrigger,
72
+ setRef,
73
+ }: ResourceRowProps) {
74
+ const reduce = useReducedMotion() ?? false
75
+ const [hovered, setHovered] = useState(false)
76
+ const inputRef = useRef<HTMLInputElement>(null)
77
+ const skipRenameBlurRef = useRef(false)
78
+ const draggedRef = useRef(false)
79
+ const [draft, setDraft] = useState(row.item.label)
80
+ const acceptsChildren = canContain(row.item)
81
+ const isDragging = draggingId === row.item.id
82
+ const dropPosition =
83
+ dropTarget?.id === row.item.id ? dropTarget.position : null
84
+
85
+ useEffect(() => {
86
+ if (!renaming) return
87
+ skipRenameBlurRef.current = false
88
+ // setDraft(row.item.label);
89
+ requestAnimationFrame(() => {
90
+ inputRef.current?.focus()
91
+ inputRef.current?.select()
92
+ })
93
+ }, [renaming, row.item.label])
94
+
95
+ const menu = renderMenu?.(row.item, {
96
+ close: () => onMenuOpenChange(false),
97
+ rename: () => {
98
+ onMenuOpenChange(false)
99
+ onRenameStart()
100
+ },
101
+ }) ?? (
102
+ <button
103
+ type="button"
104
+ onClick={() => {
105
+ onMenuOpenChange(false)
106
+ onRenameStart()
107
+ }}
108
+ 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"
109
+ >
110
+ <Pencil aria-hidden="true" className="size-3.5" />
111
+ Rename
112
+ </button>
113
+ )
114
+
115
+ return (
116
+ <motion.div
117
+ ref={setRef}
118
+ layout="position"
119
+ transition={reduce ? { duration: 0 } : SPRING_LAYOUT}
120
+ role="treeitem"
121
+ aria-level={row.depth + 1}
122
+ aria-selected={acceptsChildren ? undefined : active}
123
+ aria-expanded={acceptsChildren ? expanded : undefined}
124
+ aria-disabled={row.item.disabled || undefined}
125
+ tabIndex={focused ? 0 : -1}
126
+ draggable={!row.item.disabled && !renaming}
127
+ data-menu-open={menuOpen || undefined}
128
+ data-drop={dropPosition ?? undefined}
129
+ data-dragging={isDragging || undefined}
130
+ onFocus={onFocus}
131
+ onKeyDown={onKeyDown}
132
+ onClick={(event) => {
133
+ if (
134
+ event.defaultPrevented ||
135
+ draggedRef.current ||
136
+ renaming ||
137
+ row.item.disabled
138
+ )
139
+ return
140
+ if (acceptsChildren) onToggle()
141
+ else onSelect()
142
+ }}
143
+ onDoubleClick={(event) => {
144
+ if (acceptsChildren || row.item.disabled) return
145
+ event.preventDefault()
146
+ onRenameStart()
147
+ }}
148
+ onMouseEnter={() => {
149
+ setHovered(true)
150
+ onHoverChange?.(true)
151
+ }}
152
+ onMouseLeave={() => {
153
+ setHovered(false)
154
+ onHoverChange?.(false)
155
+ }}
156
+ onDragStartCapture={(event) => {
157
+ draggedRef.current = true
158
+ onDragStart(event, row.item.id)
159
+ }}
160
+ onDragEndCapture={() => {
161
+ onDragEnd()
162
+ requestAnimationFrame(() => {
163
+ draggedRef.current = false
164
+ })
165
+ }}
166
+ onDragOver={(event) => onDragOver(event, row)}
167
+ onDrop={onDrop}
168
+ className={cn(
169
+ "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",
170
+ "text-muted-foreground transition-[color,background-color,scale] duration-150 ease-smooth-out hover:text-foreground motion-reduce:transition-none",
171
+ !row.item.disabled &&
172
+ !renaming &&
173
+ "active:scale-97 motion-reduce:active:scale-100",
174
+ // Fallback for standalone rows that render without the shared pill.
175
+ hoverLayoutId === undefined && "hover:bg-muted",
176
+ "focus-visible:bg-muted/70 focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset",
177
+ "data-[menu-open=true]:bg-muted data-[menu-open=true]:text-foreground",
178
+ "data-[dragging=true]:opacity-40",
179
+ "data-[drop=inside]:bg-primary/10 data-[drop=inside]:ring-1 data-[drop=inside]:ring-primary/45",
180
+ "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",
181
+ "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",
182
+ !acceptsChildren &&
183
+ active &&
184
+ "bg-primary/10 text-primary dark:bg-primary/25 dark:text-primary-foreground",
185
+ row.item.disabled && "cursor-not-allowed opacity-45"
186
+ )}
187
+ style={{ paddingLeft: `${12 + row.depth * 16}px` }}
188
+ >
189
+ {hoverLayoutId ? (
190
+ <AnimatePresence custom={hoverActive}>
191
+ {hoverActive ? (
192
+ <motion.span
193
+ aria-hidden="true"
194
+ variants={reduce ? reducedPillVariants : pillVariants}
195
+ initial="initial"
196
+ animate="animate"
197
+ exit="exit"
198
+ custom={hoverActive}
199
+ className="pointer-events-none absolute inset-0"
200
+ >
201
+ {hoverPill ? (
202
+ <motion.span
203
+ layoutId={hoverLayoutId}
204
+ transition={reduce ? { duration: 0 } : SPRING_LAYOUT}
205
+ className="block h-full w-full rounded-md bg-foreground/8 dark:bg-muted/50"
206
+ />
207
+ ) : null}
208
+ </motion.span>
209
+ ) : null}
210
+ </AnimatePresence>
211
+ ) : null}
212
+ <span
213
+ aria-hidden="true"
214
+ className="relative grid size-5 shrink-0 place-items-center"
215
+ >
216
+ {renderIcon?.(row.item) ?? SidebarIcon(row.item, expanded)}
217
+ </span>
218
+
219
+ {renaming ? (
220
+ <input
221
+ ref={inputRef}
222
+ value={draft}
223
+ aria-label={`Rename ${row.item.label}`}
224
+ onChange={(event) => setDraft(event.target.value)}
225
+ draggable={false}
226
+ onClick={(event) => event.stopPropagation()}
227
+ onDoubleClick={(event) => event.stopPropagation()}
228
+ onBlur={() => {
229
+ if (!skipRenameBlurRef.current) onRenameCommit(draft)
230
+ }}
231
+ onKeyDown={(event) => {
232
+ event.stopPropagation()
233
+ if (event.key === "Enter") {
234
+ skipRenameBlurRef.current = true
235
+ onRenameCommit(draft)
236
+ }
237
+ if (event.key === "Escape") {
238
+ skipRenameBlurRef.current = true
239
+ onRenameCancel()
240
+ }
241
+ }}
242
+ className="relative h-7 min-w-0 flex-1 text-sm text-foreground outline-none focus-visible:ring-0 focus-visible:ring-ring"
243
+ />
244
+ ) : (
245
+ <MarqueeLabel active={hovered || menuOpen}>
246
+ {row.item.label}
247
+ </MarqueeLabel>
248
+ )}
249
+
250
+ {!renaming && !row.item.disabled ? (
251
+ <MorphPopover open={menuOpen} onOpenChange={onMenuOpenChange}>
252
+ <MorphPopoverTrigger>
253
+ {(() => {
254
+ const custom = renderActionsTrigger?.(row.item)
255
+ if (custom) {
256
+ // A row click selects/toggles — the trigger must not bubble.
257
+ const customOnClick = (
258
+ custom.props as {
259
+ onClick?: (event: MouseEvent<HTMLElement>) => void
260
+ }
261
+ ).onClick
262
+ return cloneElement(custom, {
263
+ draggable: false,
264
+ onClick: (event: MouseEvent<HTMLElement>) => {
265
+ event.stopPropagation()
266
+ customOnClick?.(event)
267
+ },
268
+ } as Partial<unknown>)
269
+ }
270
+ return (
271
+ <Button
272
+ draggable={false}
273
+ tabIndex={-1}
274
+ variant="link"
275
+ aria-label={`Actions for ${row.item.label}`}
276
+ onClick={(event) => event.stopPropagation()}
277
+ 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"
278
+ >
279
+ <MoreHorizontal aria-hidden="true" className="size-4" />
280
+ </Button>
281
+ )
282
+ })()}
283
+ </MorphPopoverTrigger>
284
+ <MorphPopoverContent
285
+ side="bottom"
286
+ align="end"
287
+ sideOffset={8}
288
+ radius={12}
289
+ className="w-40 p-1.5"
290
+ >
291
+ <div data-sidebar-resource-menu={row.item.id}>{menu}</div>
292
+ </MorphPopoverContent>
293
+ </MorphPopover>
294
+ ) : null}
295
+
296
+ {row.depth === 0 &&
297
+ acceptsChildren &&
298
+ (row.item.children?.length ?? 0) > 0 ? (
299
+ <ChevronRight
300
+ aria-hidden="true"
301
+ className={cn(
302
+ "relative size-3.5 shrink-0 text-muted-foreground transition-transform duration-200 ease-smooth-out motion-reduce:transition-none",
303
+ expanded && "rotate-90"
304
+ )}
305
+ />
306
+ ) : null}
307
+ </motion.div>
308
+ )
309
+ }