@exegia/corpora-ui 0.20.0 → 0.21.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/blocks/auth/__tests__/auth-state-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/auth/auth-flow-atom.d.ts +77 -0
- package/dist-lib/components/blocks/auth/auth-session-atom.d.ts +28 -0
- package/dist-lib/components/blocks/auth/auth-state-type.d.ts +71 -0
- package/dist-lib/components/blocks/auth/auth-state.d.ts +9 -0
- package/dist-lib/components/blocks/auth/use-auth-state.d.ts +52 -0
- package/dist-lib/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/nav/sidebar/ai-sidebar-atom.d.ts +217 -0
- package/dist-lib/components/blocks/nav/sidebar/index.d.ts +3 -1
- package/dist-lib/components/blocks/nav/sidebar/sidebar-context.d.ts +3 -0
- package/dist-lib/components/blocks/nav/sidebar/sidebar-row.d.ts +3 -1
- package/dist-lib/components/blocks/nav/sidebar/type.d.ts +109 -29
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar-state.d.ts +29 -0
- package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +6 -1
- package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +1 -1
- package/dist-lib/components/blocks/profile/index.d.ts +7 -0
- package/dist-lib/components/blocks/profile/profile-card-atom.d.ts +68 -0
- package/dist-lib/components/blocks/profile/profile-card-block.d.ts +28 -39
- package/dist-lib/components/blocks/profile/type.d.ts +80 -0
- package/dist-lib/components/blocks/profile/use-profile-card-state.d.ts +24 -0
- package/dist-lib/components/blocks/profile/use-profile-card.d.ts +34 -0
- package/dist-lib/components/blocks/shell/__tests__/shell-fit-atom.test.d.ts +1 -0
- package/dist-lib/components/blocks/shell/__tests__/shell-metrics.test.d.ts +1 -0
- package/dist-lib/components/blocks/shell/animated-panel-provider.d.ts +1 -1
- package/dist-lib/components/blocks/shell/animated-panel.d.ts +1 -1
- package/dist-lib/components/blocks/shell/index.d.ts +4 -0
- package/dist-lib/components/blocks/shell/shell-fit-atom.d.ts +68 -0
- package/dist-lib/components/blocks/shell/shell-metrics.d.ts +54 -0
- package/dist-lib/components/blocks/shell/type.d.ts +115 -3
- package/dist-lib/components/blocks/shell/use-shell-fit-state.d.ts +25 -0
- package/dist-lib/components/blocks/shell/use-shell-fit.d.ts +14 -0
- package/dist-lib/components/blocks/shell/use-shell-panels.d.ts +12 -1
- package/dist-lib/components/blocks/shell/utils.d.ts +32 -0
- package/dist-lib/components/composed/tree/__tests__/tree-atom.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/constants.d.ts +4 -4
- package/dist-lib/components/composed/tree/index.d.ts +3 -1
- package/dist-lib/components/composed/tree/tree-atom.d.ts +160 -0
- package/dist-lib/components/composed/tree/tree-node.d.ts +6 -1
- package/dist-lib/components/composed/tree/type.d.ts +116 -18
- package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +18 -7
- package/dist-lib/components/composed/tree/use-tree-state.d.ts +25 -0
- package/dist-lib/components/composed/tree/use-tree.d.ts +4 -0
- package/dist-lib/components/composed/user-avatar.d.ts +5 -28
- package/dist-lib/components/user-avatar/__tests__/user-avatar.test.d.ts +1 -0
- package/dist-lib/components/user-avatar/component.d.ts +1 -8
- package/dist-lib/components/user-avatar/index.d.ts +5 -0
- package/dist-lib/components/user-avatar/presence-badge.d.ts +16 -0
- package/dist-lib/components/user-avatar/type.d.ts +45 -0
- package/dist-lib/components/user-avatar/use-user-avatar-state.d.ts +26 -0
- package/dist-lib/components/user-avatar/use-user-avatar.d.ts +32 -0
- package/dist-lib/components/user-avatar/user-avatar-atom.d.ts +55 -0
- package/dist-lib/index.d.ts +5 -2
- package/dist-lib/index.js +3004 -1796
- package/dist-lib/index.js.map +1 -1
- package/dist-lib/state/exegia-provider.d.ts +51 -0
- package/dist-lib/state/index.d.ts +4 -0
- package/dist-lib/state/store.d.ts +17 -0
- package/package.json +15 -12
- package/src/components/blocks/auth/__tests__/auth-state-atom.test.tsx +247 -0
- package/src/components/blocks/auth/auth-flow-atom.ts +238 -0
- package/src/components/blocks/auth/auth-session-atom.ts +82 -0
- package/src/components/blocks/auth/auth-state-type.ts +97 -0
- package/src/components/blocks/auth/auth-state.ts +52 -0
- package/src/components/blocks/auth/use-auth-state.ts +127 -0
- package/src/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.tsx +325 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar-atom.ts +698 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +48 -42
- package/src/components/blocks/nav/sidebar/index.ts +53 -1
- package/src/components/blocks/nav/sidebar/sidebar-context.ts +15 -0
- package/src/components/blocks/nav/sidebar/sidebar-row.tsx +119 -50
- package/src/components/blocks/nav/sidebar/type.ts +125 -30
- package/src/components/blocks/nav/sidebar/use-ai-sidebar-state.ts +123 -0
- package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +301 -248
- package/src/components/blocks/nav/sidebar/utils.ts +1 -1
- package/src/components/blocks/profile/__tests__/profile-card-block.test.tsx +194 -0
- package/src/components/blocks/profile/index.ts +28 -0
- package/src/components/blocks/profile/profile-card-atom.ts +247 -0
- package/src/components/blocks/profile/profile-card-block.tsx +153 -74
- package/src/components/blocks/profile/type.ts +95 -0
- package/src/components/blocks/profile/use-profile-card-state.ts +67 -0
- package/src/components/blocks/profile/use-profile-card.ts +126 -0
- package/src/components/blocks/shell/__tests__/shell-fit-atom.test.tsx +360 -0
- package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +192 -3
- package/src/components/blocks/shell/__tests__/shell-metrics.test.ts +108 -0
- package/src/components/blocks/shell/animated-panel-inset.tsx +5 -1
- package/src/components/blocks/shell/animated-panel-provider.tsx +77 -11
- package/src/components/blocks/shell/animated-panel-trigger.tsx +4 -0
- package/src/components/blocks/shell/animated-panel.tsx +126 -29
- package/src/components/blocks/shell/index.ts +18 -0
- package/src/components/blocks/shell/shell-fit-atom.ts +243 -0
- package/src/components/blocks/shell/shell-layout.tsx +55 -53
- package/src/components/blocks/shell/shell-metrics.ts +79 -0
- package/src/components/blocks/shell/type.ts +130 -3
- package/src/components/blocks/shell/use-shell-fit-state.ts +49 -0
- package/src/components/blocks/shell/use-shell-fit.ts +135 -0
- package/src/components/blocks/shell/use-shell-panels.ts +57 -4
- package/src/components/blocks/shell/utils.ts +44 -4
- package/src/components/composed/tree/CLAUDE.md +132 -0
- package/src/components/composed/tree/__tests__/tree-atom.test.tsx +217 -0
- package/src/components/composed/tree/__tests__/tree.test.tsx +162 -37
- package/src/components/composed/tree/__tests__/use-tree.test.tsx +2 -1
- package/src/components/composed/tree/constants.ts +4 -4
- package/src/components/composed/tree/index.ts +38 -0
- package/src/components/composed/tree/tree-atom.ts +590 -0
- package/src/components/composed/tree/tree-node.tsx +117 -78
- package/src/components/composed/tree/tree.tsx +24 -16
- package/src/components/composed/tree/type.ts +125 -15
- package/src/components/composed/tree/use-tree-dnd.ts +82 -51
- package/src/components/composed/tree/use-tree-state.ts +105 -0
- package/src/components/composed/tree/use-tree.ts +185 -184
- package/src/components/composed/user-avatar.tsx +11 -99
- package/src/components/user-avatar/__tests__/user-avatar.test.tsx +245 -0
- package/src/components/user-avatar/component.tsx +121 -22
- package/src/components/user-avatar/index.ts +19 -0
- package/src/components/user-avatar/presence-badge.tsx +56 -0
- package/src/components/user-avatar/type.ts +50 -0
- package/src/components/user-avatar/use-user-avatar-state.ts +60 -0
- package/src/components/user-avatar/use-user-avatar.ts +144 -0
- package/src/components/user-avatar/user-avatar-atom.ts +198 -0
- package/src/index.ts +34 -2
- package/src/state/exegia-provider.tsx +79 -0
- package/src/state/index.ts +4 -0
- package/src/state/store.ts +19 -0
|
@@ -1,15 +1,18 @@
|
|
|
1
|
-
"use client"
|
|
1
|
+
"use client"
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { useMemo } from "react"
|
|
4
|
+
import { AnimatePresence } from "motion/react"
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
5
6
|
import type {
|
|
6
7
|
AISidebarComponentProps,
|
|
8
|
+
AISidebarContextValue,
|
|
7
9
|
AISidebarController,
|
|
8
10
|
AISidebarProps,
|
|
9
11
|
AISidebarViewProps,
|
|
10
12
|
FlatResource,
|
|
11
13
|
} from "./type"
|
|
12
14
|
import { useAISidebar } from "./use-ai-sidebar"
|
|
15
|
+
import { AISidebarContext } from "./sidebar-context"
|
|
13
16
|
import { ResourceRow } from "./sidebar-row"
|
|
14
17
|
|
|
15
18
|
/**
|
|
@@ -69,8 +72,47 @@ function AISidebarView({
|
|
|
69
72
|
const { dnd, hover } = controller
|
|
70
73
|
const { draggingId, dropTarget } = dnd
|
|
71
74
|
|
|
75
|
+
// Rows take the sidebar's id and the stable handlers, never the controller
|
|
76
|
+
// — so this value keeps its identity and a hover or a toggle re-renders
|
|
77
|
+
// only the rows whose own atoms changed. Deps name each function because
|
|
78
|
+
// `dnd` itself carries live drag state and changes identity with it.
|
|
79
|
+
const context = useMemo<AISidebarContextValue>(
|
|
80
|
+
() => ({
|
|
81
|
+
sidebarId: controller.sidebarId,
|
|
82
|
+
hoverLayoutId: hover.layoutId,
|
|
83
|
+
renderIcon,
|
|
84
|
+
renderMenu,
|
|
85
|
+
renderActionsTrigger,
|
|
86
|
+
setRowRef: controller.setRowRef,
|
|
87
|
+
onRowKeyDown: controller.onRowKeyDown,
|
|
88
|
+
onRowHover: hover.onRowHover,
|
|
89
|
+
closeMenu: controller.closeMenu,
|
|
90
|
+
dnd: {
|
|
91
|
+
onDrop: dnd.onDrop,
|
|
92
|
+
onRowDragStart: dnd.onRowDragStart,
|
|
93
|
+
onRowDragEnd: dnd.onRowDragEnd,
|
|
94
|
+
onRowDragOver: dnd.onRowDragOver,
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
[
|
|
98
|
+
controller.sidebarId,
|
|
99
|
+
controller.setRowRef,
|
|
100
|
+
controller.onRowKeyDown,
|
|
101
|
+
controller.closeMenu,
|
|
102
|
+
hover.layoutId,
|
|
103
|
+
hover.onRowHover,
|
|
104
|
+
renderIcon,
|
|
105
|
+
renderMenu,
|
|
106
|
+
renderActionsTrigger,
|
|
107
|
+
dnd.onDrop,
|
|
108
|
+
dnd.onRowDragStart,
|
|
109
|
+
dnd.onRowDragEnd,
|
|
110
|
+
dnd.onRowDragOver,
|
|
111
|
+
]
|
|
112
|
+
)
|
|
113
|
+
|
|
72
114
|
return (
|
|
73
|
-
|
|
115
|
+
<AISidebarContext.Provider value={context}>
|
|
74
116
|
<div
|
|
75
117
|
role="tree"
|
|
76
118
|
aria-label={ariaLabel}
|
|
@@ -86,43 +128,7 @@ function AISidebarView({
|
|
|
86
128
|
>
|
|
87
129
|
<AnimatePresence initial={false}>
|
|
88
130
|
{controller.flat.map((row: FlatResource) => (
|
|
89
|
-
<ResourceRow
|
|
90
|
-
key={row.item.id}
|
|
91
|
-
row={row}
|
|
92
|
-
active={controller.selectedId === row.item.id}
|
|
93
|
-
expanded={controller.isExpanded(row.item.id)}
|
|
94
|
-
focused={controller.focusedId === row.item.id}
|
|
95
|
-
draggingId={draggingId}
|
|
96
|
-
dropTarget={dropTarget}
|
|
97
|
-
menuOpen={controller.menuOpenId === row.item.id}
|
|
98
|
-
renaming={controller.renamingId === row.item.id}
|
|
99
|
-
hoverActive={hover.hoveredId !== null && !draggingId}
|
|
100
|
-
hoverPill={
|
|
101
|
-
hover.hoveredId === row.item.id &&
|
|
102
|
-
!draggingId &&
|
|
103
|
-
!row.item.disabled
|
|
104
|
-
}
|
|
105
|
-
hoverLayoutId={hover.layoutId}
|
|
106
|
-
onHoverChange={(hovered) => hover.onRowHover(row.item.id, hovered)}
|
|
107
|
-
onFocus={() => controller.focus(row.item.id)}
|
|
108
|
-
onSelect={() => controller.select(row.item.id)}
|
|
109
|
-
onToggle={() => controller.toggleExpanded(row.item.id)}
|
|
110
|
-
onKeyDown={(event) => controller.onRowKeyDown(event, row)}
|
|
111
|
-
onRenameStart={() => controller.startRename(row.item.id)}
|
|
112
|
-
onRenameCancel={controller.cancelRename}
|
|
113
|
-
onRenameCommit={(label) => controller.rename(row.item.id, label)}
|
|
114
|
-
onMenuOpenChange={(open) =>
|
|
115
|
-
open ? controller.openMenu(row.item.id) : controller.closeMenu()
|
|
116
|
-
}
|
|
117
|
-
onDragStart={dnd.onRowDragStart}
|
|
118
|
-
onDragEnd={dnd.onRowDragEnd}
|
|
119
|
-
onDragOver={dnd.onRowDragOver}
|
|
120
|
-
onDrop={dnd.onDrop}
|
|
121
|
-
renderIcon={renderIcon}
|
|
122
|
-
renderMenu={renderMenu}
|
|
123
|
-
renderActionsTrigger={renderActionsTrigger}
|
|
124
|
-
setRef={(node) => controller.setRowRef(row.item.id, node)}
|
|
125
|
-
/>
|
|
131
|
+
<ResourceRow key={row.item.id} row={row} />
|
|
126
132
|
))}
|
|
127
133
|
</AnimatePresence>
|
|
128
134
|
|
|
@@ -139,6 +145,6 @@ function AISidebarView({
|
|
|
139
145
|
<span className="sr-only" aria-live="polite">
|
|
140
146
|
{controller.announcement}
|
|
141
147
|
</span>
|
|
142
|
-
|
|
148
|
+
</AISidebarContext.Provider>
|
|
143
149
|
)
|
|
144
150
|
}
|
|
@@ -3,7 +3,59 @@ import { MarqueeLabel } from "./marquee-label.tsx"
|
|
|
3
3
|
import { ResourceRow } from "./sidebar-row.tsx"
|
|
4
4
|
import { SidebarIcon } from "./sidebar-icon.tsx"
|
|
5
5
|
export { useAISidebar, type UseAISidebarOptions } from "./use-ai-sidebar.ts"
|
|
6
|
+
export {
|
|
7
|
+
useAISidebarActions,
|
|
8
|
+
useAISidebarState,
|
|
9
|
+
} from "./use-ai-sidebar-state.ts"
|
|
6
10
|
export type * from "./type.ts"
|
|
11
|
+
// The public atom surface. `@internal` atoms (config, handlers, mount, the
|
|
12
|
+
// projections, the owned-* loop guards) stay unexported on purpose —
|
|
13
|
+
// `export *` would make every internal a breaking-change surface for
|
|
14
|
+
// consumer apps.
|
|
15
|
+
export {
|
|
16
|
+
aiSidebarActiveIdAtom,
|
|
17
|
+
aiSidebarAnnouncementAtom,
|
|
18
|
+
aiSidebarDraggingIdAtom,
|
|
19
|
+
aiSidebarDropTargetAtom,
|
|
20
|
+
aiSidebarExpandedIdsAtom,
|
|
21
|
+
aiSidebarFlatAtom,
|
|
22
|
+
aiSidebarFocusedIdAtom,
|
|
23
|
+
aiSidebarHoverActiveAtom,
|
|
24
|
+
aiSidebarHoveredIdAtom,
|
|
25
|
+
aiSidebarItemsAtom,
|
|
26
|
+
aiSidebarMenuOpenIdAtom,
|
|
27
|
+
aiSidebarMovePendingAtom,
|
|
28
|
+
aiSidebarRenamingIdAtom,
|
|
29
|
+
aiSidebarRowDraggingAtom,
|
|
30
|
+
aiSidebarRowDropAtom,
|
|
31
|
+
aiSidebarRowExpandedAtom,
|
|
32
|
+
aiSidebarRowFocusedAtom,
|
|
33
|
+
aiSidebarRowHoveredAtom,
|
|
34
|
+
aiSidebarRowMenuOpenAtom,
|
|
35
|
+
aiSidebarRowRenamingAtom,
|
|
36
|
+
aiSidebarRowSelectedAtom,
|
|
37
|
+
aiSidebarStateAtom,
|
|
38
|
+
cancelAISidebarRenameAtom,
|
|
39
|
+
clearAISidebarHoverAtom,
|
|
40
|
+
closeAISidebarMenuAtom,
|
|
41
|
+
collapseAISidebarRowAtom,
|
|
42
|
+
collapseAllAISidebarRowsAtom,
|
|
43
|
+
endAISidebarDragAtom,
|
|
44
|
+
expandAISidebarRowAtom,
|
|
45
|
+
expandAllAISidebarRowsAtom,
|
|
46
|
+
focusAISidebarRowAtom,
|
|
47
|
+
moveAISidebarRowAtom,
|
|
48
|
+
openAISidebarMenuAtom,
|
|
49
|
+
removeAISidebarInstance,
|
|
50
|
+
renameAISidebarRowAtom,
|
|
51
|
+
resetAISidebarAtom,
|
|
52
|
+
revealAISidebarRowAtom,
|
|
53
|
+
selectAISidebarRowAtom,
|
|
54
|
+
setAISidebarHoveredAtom,
|
|
55
|
+
setAISidebarItemsAtom,
|
|
56
|
+
startAISidebarRenameAtom,
|
|
57
|
+
toggleAISidebarRowAtom,
|
|
58
|
+
} from "./ai-sidebar-atom.ts"
|
|
7
59
|
|
|
8
60
|
export { AISidebar, MarqueeLabel, ResourceRow, SidebarIcon }
|
|
9
61
|
|
|
@@ -11,5 +63,5 @@ export const Sidebar = {
|
|
|
11
63
|
Wrapper: AISidebar,
|
|
12
64
|
MarqueeLabel,
|
|
13
65
|
Row: ResourceRow,
|
|
14
|
-
Icon: SidebarIcon
|
|
66
|
+
Icon: SidebarIcon,
|
|
15
67
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
import type { AISidebarContextValue } from "./type.ts";
|
|
6
|
+
|
|
7
|
+
export const AISidebarContext = createContext<AISidebarContextValue | null>(
|
|
8
|
+
null
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
export function useAISidebarContext(): AISidebarContextValue {
|
|
12
|
+
const context = useContext(AISidebarContext);
|
|
13
|
+
if (!context) throw new Error("Sidebar rows must render inside <AISidebar>.");
|
|
14
|
+
return context;
|
|
15
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
cloneElement,
|
|
5
|
+
memo,
|
|
6
|
+
useCallback,
|
|
3
7
|
useEffect,
|
|
4
8
|
useRef,
|
|
5
9
|
useState,
|
|
6
10
|
type MouseEvent,
|
|
7
11
|
} from "react"
|
|
12
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
8
13
|
import type { ResourceRowProps } from "./type"
|
|
9
14
|
import {
|
|
10
15
|
AnimatePresence,
|
|
@@ -15,6 +20,25 @@ import {
|
|
|
15
20
|
import { ChevronRight, MoreHorizontal, Pencil } from "lucide-react"
|
|
16
21
|
import { SPRING_LAYOUT } from "@/lib/ease.ts"
|
|
17
22
|
import { cn } from "@/lib/utils.ts"
|
|
23
|
+
import {
|
|
24
|
+
aiSidebarHoverActiveAtom,
|
|
25
|
+
aiSidebarRowDraggingAtom,
|
|
26
|
+
aiSidebarRowDropAtom,
|
|
27
|
+
aiSidebarRowExpandedAtom,
|
|
28
|
+
aiSidebarRowFocusedAtom,
|
|
29
|
+
aiSidebarRowHoveredAtom,
|
|
30
|
+
aiSidebarRowMenuOpenAtom,
|
|
31
|
+
aiSidebarRowRenamingAtom,
|
|
32
|
+
aiSidebarRowSelectedAtom,
|
|
33
|
+
cancelAISidebarRenameAtom,
|
|
34
|
+
focusAISidebarRowAtom,
|
|
35
|
+
openAISidebarMenuAtom,
|
|
36
|
+
renameAISidebarRowAtom,
|
|
37
|
+
selectAISidebarRowAtom,
|
|
38
|
+
startAISidebarRenameAtom,
|
|
39
|
+
toggleAISidebarRowAtom,
|
|
40
|
+
} from "./ai-sidebar-atom.ts"
|
|
41
|
+
import { useAISidebarContext } from "./sidebar-context.ts"
|
|
18
42
|
import { canContain } from "./utils"
|
|
19
43
|
import { SidebarIcon } from "./sidebar-icon"
|
|
20
44
|
import { MarqueeLabel } from "./marquee-label"
|
|
@@ -23,7 +47,7 @@ import {
|
|
|
23
47
|
MorphPopoverContent,
|
|
24
48
|
MorphPopoverTrigger,
|
|
25
49
|
} from "@/components/motion/popover-morph.tsx"
|
|
26
|
-
import { Button } from "@/components/ui/button"
|
|
50
|
+
import { Button } from "@/components/ui/button"
|
|
27
51
|
|
|
28
52
|
// Same choreography as SharedLayoutBg: the wrapper only fades/blurs out when
|
|
29
53
|
// the pointer leaves the whole tree; a row-to-row move exits instantly so the
|
|
@@ -41,46 +65,74 @@ const reducedPillVariants: Variants = {
|
|
|
41
65
|
exit: (hoverActive: boolean) => (!hoverActive ? { opacity: 0 } : {}),
|
|
42
66
|
}
|
|
43
67
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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) {
|
|
68
|
+
function ResourceRowImpl({ row }: ResourceRowProps) {
|
|
69
|
+
const {
|
|
70
|
+
sidebarId,
|
|
71
|
+
hoverLayoutId,
|
|
72
|
+
renderIcon,
|
|
73
|
+
renderMenu,
|
|
74
|
+
renderActionsTrigger,
|
|
75
|
+
setRowRef,
|
|
76
|
+
onRowKeyDown,
|
|
77
|
+
onRowHover,
|
|
78
|
+
closeMenu,
|
|
79
|
+
dnd,
|
|
80
|
+
} = useAISidebarContext()
|
|
74
81
|
const reduce = useReducedMotion() ?? false
|
|
82
|
+
|
|
83
|
+
// One subscription per field, per row. A row re-renders when its own
|
|
84
|
+
// selection, expansion, focus, rename, menu, drag or hover state changes
|
|
85
|
+
// and sits still while a sibling's does — reading the controller off
|
|
86
|
+
// context instead re-rendered every row on every change.
|
|
87
|
+
const active = useAtomValue(aiSidebarRowSelectedAtom(sidebarId, row.item.id))
|
|
88
|
+
const expanded = useAtomValue(
|
|
89
|
+
aiSidebarRowExpandedAtom(sidebarId, row.item.id)
|
|
90
|
+
)
|
|
91
|
+
const focused = useAtomValue(aiSidebarRowFocusedAtom(sidebarId, row.item.id))
|
|
92
|
+
const renaming = useAtomValue(
|
|
93
|
+
aiSidebarRowRenamingAtom(sidebarId, row.item.id)
|
|
94
|
+
)
|
|
95
|
+
const menuOpen = useAtomValue(
|
|
96
|
+
aiSidebarRowMenuOpenAtom(sidebarId, row.item.id)
|
|
97
|
+
)
|
|
98
|
+
const isDragging = useAtomValue(
|
|
99
|
+
aiSidebarRowDraggingAtom(sidebarId, row.item.id)
|
|
100
|
+
)
|
|
101
|
+
const dropPosition = useAtomValue(
|
|
102
|
+
aiSidebarRowDropAtom(sidebarId, row.item.id)
|
|
103
|
+
)
|
|
104
|
+
const hoverActive = useAtomValue(aiSidebarHoverActiveAtom(sidebarId))
|
|
105
|
+
const hoverPill =
|
|
106
|
+
useAtomValue(aiSidebarRowHoveredAtom(sidebarId, row.item.id)) &&
|
|
107
|
+
!row.item.disabled
|
|
108
|
+
|
|
109
|
+
const select = useSetAtom(selectAISidebarRowAtom(sidebarId))
|
|
110
|
+
const toggleExpanded = useSetAtom(toggleAISidebarRowAtom(sidebarId))
|
|
111
|
+
const setFocusedRow = useSetAtom(focusAISidebarRowAtom(sidebarId))
|
|
112
|
+
const startRename = useSetAtom(startAISidebarRenameAtom(sidebarId))
|
|
113
|
+
const cancelRename = useSetAtom(cancelAISidebarRenameAtom(sidebarId))
|
|
114
|
+
const rename = useSetAtom(renameAISidebarRowAtom(sidebarId))
|
|
115
|
+
const openMenu = useSetAtom(openAISidebarMenuAtom(sidebarId))
|
|
116
|
+
|
|
117
|
+
const onMenuOpenChange = useCallback(
|
|
118
|
+
(open: boolean) => {
|
|
119
|
+
if (open) openMenu(row.item.id)
|
|
120
|
+
else closeMenu()
|
|
121
|
+
},
|
|
122
|
+
[closeMenu, openMenu, row.item.id]
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
const setRef = useCallback(
|
|
126
|
+
(node: HTMLDivElement | null) => setRowRef(row.item.id, node),
|
|
127
|
+
[row.item.id, setRowRef]
|
|
128
|
+
)
|
|
129
|
+
|
|
75
130
|
const [hovered, setHovered] = useState(false)
|
|
76
131
|
const inputRef = useRef<HTMLInputElement>(null)
|
|
77
132
|
const skipRenameBlurRef = useRef(false)
|
|
78
133
|
const draggedRef = useRef(false)
|
|
79
134
|
const [draft, setDraft] = useState(row.item.label)
|
|
80
135
|
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
136
|
|
|
85
137
|
useEffect(() => {
|
|
86
138
|
if (!renaming) return
|
|
@@ -96,14 +148,14 @@ export function ResourceRow({
|
|
|
96
148
|
close: () => onMenuOpenChange(false),
|
|
97
149
|
rename: () => {
|
|
98
150
|
onMenuOpenChange(false)
|
|
99
|
-
|
|
151
|
+
startRename(row.item.id)
|
|
100
152
|
},
|
|
101
153
|
}) ?? (
|
|
102
154
|
<button
|
|
103
155
|
type="button"
|
|
104
156
|
onClick={() => {
|
|
105
157
|
onMenuOpenChange(false)
|
|
106
|
-
|
|
158
|
+
startRename(row.item.id)
|
|
107
159
|
}}
|
|
108
160
|
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
161
|
>
|
|
@@ -127,8 +179,8 @@ export function ResourceRow({
|
|
|
127
179
|
data-menu-open={menuOpen || undefined}
|
|
128
180
|
data-drop={dropPosition ?? undefined}
|
|
129
181
|
data-dragging={isDragging || undefined}
|
|
130
|
-
onFocus={
|
|
131
|
-
onKeyDown={
|
|
182
|
+
onFocus={() => setFocusedRow(row.item.id)}
|
|
183
|
+
onKeyDown={(event) => onRowKeyDown(event, row)}
|
|
132
184
|
onClick={(event) => {
|
|
133
185
|
if (
|
|
134
186
|
event.defaultPrevented ||
|
|
@@ -137,34 +189,34 @@ export function ResourceRow({
|
|
|
137
189
|
row.item.disabled
|
|
138
190
|
)
|
|
139
191
|
return
|
|
140
|
-
if (acceptsChildren)
|
|
141
|
-
else
|
|
192
|
+
if (acceptsChildren) toggleExpanded(row.item.id)
|
|
193
|
+
else select(row.item.id)
|
|
142
194
|
}}
|
|
143
195
|
onDoubleClick={(event) => {
|
|
144
196
|
if (acceptsChildren || row.item.disabled) return
|
|
145
197
|
event.preventDefault()
|
|
146
|
-
|
|
198
|
+
startRename(row.item.id)
|
|
147
199
|
}}
|
|
148
200
|
onMouseEnter={() => {
|
|
149
201
|
setHovered(true)
|
|
150
|
-
|
|
202
|
+
onRowHover(row.item.id, true)
|
|
151
203
|
}}
|
|
152
204
|
onMouseLeave={() => {
|
|
153
205
|
setHovered(false)
|
|
154
|
-
|
|
206
|
+
onRowHover(row.item.id, false)
|
|
155
207
|
}}
|
|
156
208
|
onDragStartCapture={(event) => {
|
|
157
209
|
draggedRef.current = true
|
|
158
|
-
|
|
210
|
+
dnd.onRowDragStart(event, row.item.id)
|
|
159
211
|
}}
|
|
160
212
|
onDragEndCapture={() => {
|
|
161
|
-
|
|
213
|
+
dnd.onRowDragEnd()
|
|
162
214
|
requestAnimationFrame(() => {
|
|
163
215
|
draggedRef.current = false
|
|
164
216
|
})
|
|
165
217
|
}}
|
|
166
|
-
onDragOver={(event) =>
|
|
167
|
-
onDrop={onDrop}
|
|
218
|
+
onDragOver={(event) => dnd.onRowDragOver(event, row)}
|
|
219
|
+
onDrop={dnd.onDrop}
|
|
168
220
|
className={cn(
|
|
169
221
|
"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
222
|
"text-muted-foreground transition-[color,background-color,scale] duration-150 ease-smooth-out hover:text-foreground motion-reduce:transition-none",
|
|
@@ -226,17 +278,17 @@ export function ResourceRow({
|
|
|
226
278
|
onClick={(event) => event.stopPropagation()}
|
|
227
279
|
onDoubleClick={(event) => event.stopPropagation()}
|
|
228
280
|
onBlur={() => {
|
|
229
|
-
if (!skipRenameBlurRef.current)
|
|
281
|
+
if (!skipRenameBlurRef.current) rename(row.item.id, draft)
|
|
230
282
|
}}
|
|
231
283
|
onKeyDown={(event) => {
|
|
232
284
|
event.stopPropagation()
|
|
233
285
|
if (event.key === "Enter") {
|
|
234
286
|
skipRenameBlurRef.current = true
|
|
235
|
-
|
|
287
|
+
rename(row.item.id, draft)
|
|
236
288
|
}
|
|
237
289
|
if (event.key === "Escape") {
|
|
238
290
|
skipRenameBlurRef.current = true
|
|
239
|
-
|
|
291
|
+
cancelRename()
|
|
240
292
|
}
|
|
241
293
|
}}
|
|
242
294
|
className="relative h-7 min-w-0 flex-1 text-sm text-foreground outline-none focus-visible:ring-0 focus-visible:ring-ring"
|
|
@@ -307,3 +359,20 @@ export function ResourceRow({
|
|
|
307
359
|
</motion.div>
|
|
308
360
|
)
|
|
309
361
|
}
|
|
362
|
+
|
|
363
|
+
/** `flat` is rebuilt whenever expansion changes, so its `FlatResource`
|
|
364
|
+
* wrappers are fresh objects even for rows nothing happened to. The
|
|
365
|
+
* comparator looks through the wrapper at what a row actually renders from —
|
|
366
|
+
* its item (stable unless the data changed), depth and parent — so a sibling
|
|
367
|
+
* opening does not walk every row. Each row's own state reaches it through
|
|
368
|
+
* its atoms instead. */
|
|
369
|
+
function sameRow(prev: ResourceRowProps, next: ResourceRowProps): boolean {
|
|
370
|
+
return (
|
|
371
|
+
prev.row.item === next.row.item &&
|
|
372
|
+
prev.row.depth === next.row.depth &&
|
|
373
|
+
prev.row.parentId === next.row.parentId
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export const ResourceRow = memo(ResourceRowImpl, sameRow)
|
|
378
|
+
ResourceRow.displayName = "ResourceRow"
|
|
@@ -23,10 +23,19 @@ export interface SidebarResourceMenuControls {
|
|
|
23
23
|
rename: () => void
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/** Key for one sidebar's state in the store. Any stable string;
|
|
27
|
+
* `useAISidebar` generates one when you don't pass it. */
|
|
28
|
+
export type AISidebarInstanceId = string
|
|
29
|
+
|
|
26
30
|
/** Options for `useAISidebar`. `items`, `activeId` and `expandedIds` are
|
|
27
31
|
* each controlled when passed and hook-owned via their `default*` twin
|
|
28
32
|
* otherwise. */
|
|
29
33
|
export interface UseAISidebarOptions {
|
|
34
|
+
/** Key this sidebar's state under a name your app can address —
|
|
35
|
+
* `useAISidebarState("app-resources")` / `useAISidebarActions(...)` reach
|
|
36
|
+
* it from anywhere under `ExegiaProvider`. Without one the hook generates
|
|
37
|
+
* a key and the state is dropped when the component unmounts. */
|
|
38
|
+
sidebarId?: AISidebarInstanceId
|
|
30
39
|
items?: SidebarResource[]
|
|
31
40
|
defaultItems?: SidebarResource[]
|
|
32
41
|
onItemsChange?: (items: SidebarResource[]) => void
|
|
@@ -46,6 +55,8 @@ export interface UseAISidebarOptions {
|
|
|
46
55
|
* toolbar, a command palette, a route change. Returned by `useAISidebar`
|
|
47
56
|
* and accepted by `<AISidebar controller={…} />`. */
|
|
48
57
|
export interface AISidebarController {
|
|
58
|
+
/** The key this sidebar's state is stored under. */
|
|
59
|
+
sidebarId: AISidebarInstanceId
|
|
49
60
|
items: SidebarResource[]
|
|
50
61
|
/** Visible rows in order, each with its depth and parent id. */
|
|
51
62
|
flat: FlatResource[]
|
|
@@ -89,19 +100,31 @@ export interface AISidebarController {
|
|
|
89
100
|
/** Live-region text for the last move or rename outcome. */
|
|
90
101
|
announcement: string
|
|
91
102
|
|
|
103
|
+
/** Back to the values the sidebar mounted with. */
|
|
104
|
+
reset: () => void
|
|
105
|
+
|
|
92
106
|
/** @internal Row wiring the view threads through — drag state, the
|
|
93
107
|
* hover pill and roving keyboard nav all belong to the rendering. */
|
|
94
108
|
dnd: AISidebarDndState
|
|
95
109
|
hover: AISidebarHoverState
|
|
96
|
-
onRowKeyDown: (
|
|
110
|
+
onRowKeyDown: (
|
|
111
|
+
event: KeyboardEvent<HTMLDivElement>,
|
|
112
|
+
row: FlatResource
|
|
113
|
+
) => void
|
|
97
114
|
setRowRef: (id: string, node: HTMLDivElement | null) => void
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
/** @internal */
|
|
101
|
-
export interface AISidebarDndState {
|
|
118
|
+
export interface AISidebarDndState extends AISidebarRowDndHandlers {
|
|
102
119
|
draggingId: string | null
|
|
103
120
|
dropTarget: DropTarget | null
|
|
104
121
|
onRootDragOver: (event: DragEvent<HTMLDivElement>) => void
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** @internal The drag handlers a row needs, stable for the life of the
|
|
125
|
+
* sidebar — they read drag state out of the store when they run instead of
|
|
126
|
+
* closing over it, so handing them to every row costs no re-renders. */
|
|
127
|
+
export interface AISidebarRowDndHandlers {
|
|
105
128
|
onDrop: (event: DragEvent<HTMLDivElement>) => void
|
|
106
129
|
onRowDragStart: (event: DragEvent<HTMLDivElement>, id: string) => void
|
|
107
130
|
onRowDragEnd: () => void
|
|
@@ -164,36 +187,108 @@ export interface DropTarget {
|
|
|
164
187
|
position: SidebarResourceDropPosition
|
|
165
188
|
}
|
|
166
189
|
|
|
190
|
+
/** A row takes its place in the tree and nothing else: selection,
|
|
191
|
+
* expansion, focus, rename, drag and hover state all arrive through its own
|
|
192
|
+
* atoms, and the rendering options through the sidebar context. */
|
|
167
193
|
export interface ResourceRowProps {
|
|
168
194
|
row: FlatResource
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
onRenameCancel: () => void
|
|
184
|
-
onRenameCommit: (label: string) => void
|
|
185
|
-
onRenameStart: () => void
|
|
186
|
-
onSelect: () => void
|
|
187
|
-
onToggle: () => void
|
|
188
|
-
/** Some row in the tree is hovered — mounts the pill fade wrapper. */
|
|
189
|
-
hoverActive?: boolean
|
|
190
|
-
/** This row owns the sliding hover pill. */
|
|
191
|
-
hoverPill?: boolean
|
|
192
|
-
/** Shared motion layoutId that lets the pill travel between rows. */
|
|
193
|
-
hoverLayoutId?: string
|
|
194
|
-
onHoverChange?: (hovered: boolean) => void
|
|
195
|
-
renderIcon?: (item: SidebarResource) => ReactNode
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** @internal What a row needs from the block, threaded through context so
|
|
198
|
+
* the rows stay prop-light.
|
|
199
|
+
*
|
|
200
|
+
* Deliberately free of sidebar state and of the controller. Rows read state
|
|
201
|
+
* from per-row atoms, so this value keeps its identity for the life of the
|
|
202
|
+
* sidebar and hovering or expanding one row no longer re-renders every row
|
|
203
|
+
* through context. */
|
|
204
|
+
export interface AISidebarContextValue {
|
|
205
|
+
sidebarId: AISidebarInstanceId
|
|
206
|
+
/** Shared motion layoutId that lets the hover pill travel between rows. */
|
|
207
|
+
hoverLayoutId: string
|
|
208
|
+
renderIcon?: AISidebarProps["renderIcon"]
|
|
196
209
|
renderMenu?: AISidebarProps["renderMenu"]
|
|
197
210
|
renderActionsTrigger?: AISidebarProps["renderActionsTrigger"]
|
|
198
|
-
|
|
211
|
+
/** Registers the row's DOM node with the block, which owns the ref map —
|
|
212
|
+
* DOM nodes never go in the store. */
|
|
213
|
+
setRowRef: (id: string, node: HTMLDivElement | null) => void
|
|
214
|
+
onRowKeyDown: (
|
|
215
|
+
event: KeyboardEvent<HTMLDivElement>,
|
|
216
|
+
row: FlatResource
|
|
217
|
+
) => void
|
|
218
|
+
onRowHover: (id: string, hovered: boolean) => void
|
|
219
|
+
/** Closing returns DOM focus to the row that owned the menu, so it goes
|
|
220
|
+
* through the block rather than the store. */
|
|
221
|
+
closeMenu: () => void
|
|
222
|
+
dnd: AISidebarRowDndHandlers
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Everything observable about one sidebar, for consumers reading it by id. */
|
|
226
|
+
export interface AISidebarState {
|
|
227
|
+
items: SidebarResource[]
|
|
228
|
+
/** Visible rows in order, each with its depth and parent id. */
|
|
229
|
+
flat: FlatResource[]
|
|
230
|
+
selectedId: string | null
|
|
231
|
+
expandedIds: ReadonlySet<string>
|
|
232
|
+
focusedId: string | null
|
|
233
|
+
renamingId: string | null
|
|
234
|
+
menuOpenId: string | null
|
|
235
|
+
draggingId: string | null
|
|
236
|
+
dropTarget: DropTarget | null
|
|
237
|
+
hoveredId: string | null
|
|
238
|
+
/** A move is in flight — further moves are refused until it settles. */
|
|
239
|
+
movePending: boolean
|
|
240
|
+
/** Live-region text for the last move or rename outcome. */
|
|
241
|
+
announcement: string
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Everything doable to one sidebar from outside its component. */
|
|
245
|
+
export interface AISidebarActions {
|
|
246
|
+
select: (id: string) => void
|
|
247
|
+
expand: (id: string) => void
|
|
248
|
+
collapse: (id: string) => void
|
|
249
|
+
toggleExpanded: (id: string) => void
|
|
250
|
+
expandAll: () => void
|
|
251
|
+
collapseAll: () => void
|
|
252
|
+
reveal: (id: string) => void
|
|
253
|
+
/** Moves the roving-focus target. DOM focus follows only for callers that
|
|
254
|
+
* hold the controller — the ref map lives in the block, not the store. */
|
|
255
|
+
focus: (id: string) => void
|
|
256
|
+
startRename: (id: string) => void
|
|
257
|
+
cancelRename: () => void
|
|
258
|
+
rename: (id: string, label: string) => void
|
|
259
|
+
openMenu: (id: string) => void
|
|
260
|
+
closeMenu: () => void
|
|
261
|
+
move: (move: SidebarResourceMove) => Promise<void>
|
|
262
|
+
setItems: (items: SidebarResource[]) => void
|
|
263
|
+
reset: () => void
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/** @internal Projection of `useAISidebar`'s options — primitives only, so
|
|
267
|
+
* the store write runs once per real change instead of once per render. */
|
|
268
|
+
export interface AISidebarConfig {
|
|
269
|
+
controlsItems: boolean
|
|
270
|
+
controlsActiveId: boolean
|
|
271
|
+
controlsExpandedIds: boolean
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** @internal Latest option callbacks. Only write atoms read this, so it can
|
|
275
|
+
* be refreshed every commit without re-rendering anything. */
|
|
276
|
+
export interface AISidebarHandlers {
|
|
277
|
+
onItemsChange?: (items: SidebarResource[]) => void
|
|
278
|
+
onMove?: (move: SidebarResourceMove) => void | Promise<void>
|
|
279
|
+
onMoveError?: (error: unknown, move: SidebarResourceMove) => void
|
|
280
|
+
onRename?: (item: SidebarResource, label: string) => void | Promise<void>
|
|
281
|
+
onActiveChange?: (id: string) => void
|
|
282
|
+
onExpandedChange?: (ids: string[]) => void
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** @internal What an instance starts from, replayed by
|
|
286
|
+
* `resetAISidebarAtom`. */
|
|
287
|
+
export interface AISidebarSeed {
|
|
288
|
+
items: SidebarResource[]
|
|
289
|
+
/** Hook-owned selection — the `defaultActiveId`, never the controlled
|
|
290
|
+
* `activeId`. */
|
|
291
|
+
activeId: string | null
|
|
292
|
+
focusedId: string | null
|
|
293
|
+
expandedIds: string[]
|
|
199
294
|
}
|