@exegia/corpora-ui 0.19.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.
Files changed (162) hide show
  1. package/dist-lib/components/blocks/auth/__tests__/auth-state-atom.test.d.ts +1 -0
  2. package/dist-lib/components/blocks/auth/auth-flow-atom.d.ts +77 -0
  3. package/dist-lib/components/blocks/auth/auth-session-atom.d.ts +28 -0
  4. package/dist-lib/components/blocks/auth/auth-state-type.d.ts +71 -0
  5. package/dist-lib/components/blocks/auth/auth-state.d.ts +9 -0
  6. package/dist-lib/components/blocks/auth/use-auth-state.d.ts +52 -0
  7. package/dist-lib/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.d.ts +1 -0
  8. package/dist-lib/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.d.ts +1 -0
  9. package/dist-lib/components/blocks/nav/sidebar/ai-sidebar-atom.d.ts +217 -0
  10. package/dist-lib/components/blocks/nav/sidebar/ai-sidebar.d.ts +11 -2
  11. package/dist-lib/components/blocks/nav/sidebar/index.d.ts +3 -1
  12. package/dist-lib/components/blocks/nav/sidebar/sidebar-context.d.ts +3 -0
  13. package/dist-lib/components/blocks/nav/sidebar/sidebar-row.d.ts +3 -1
  14. package/dist-lib/components/blocks/nav/sidebar/type.d.ts +191 -29
  15. package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar-state.d.ts +29 -0
  16. package/dist-lib/components/blocks/nav/sidebar/use-ai-sidebar.d.ts +27 -31
  17. package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +6 -1
  18. package/dist-lib/components/blocks/profile/index.d.ts +7 -0
  19. package/dist-lib/components/blocks/profile/profile-card-atom.d.ts +68 -0
  20. package/dist-lib/components/blocks/profile/profile-card-block.d.ts +28 -39
  21. package/dist-lib/components/blocks/profile/type.d.ts +80 -0
  22. package/dist-lib/components/blocks/profile/use-profile-card-state.d.ts +24 -0
  23. package/dist-lib/components/blocks/profile/use-profile-card.d.ts +34 -0
  24. package/dist-lib/components/blocks/scaffold/constants.d.ts +5 -0
  25. package/dist-lib/components/blocks/scaffold/scaffold-canvas.d.ts +2 -0
  26. package/dist-lib/components/blocks/scaffold/scaffold-panel.d.ts +1 -1
  27. package/dist-lib/components/blocks/scaffold/scaffold-tab.d.ts +6 -1
  28. package/dist-lib/components/blocks/scaffold/type.d.ts +28 -0
  29. package/dist-lib/components/blocks/scaffold/use-panel-visibility.d.ts +19 -0
  30. package/dist-lib/components/blocks/scaffold/utils.d.ts +6 -0
  31. package/dist-lib/components/blocks/shell/__tests__/shell-fit-atom.test.d.ts +1 -0
  32. package/dist-lib/components/blocks/shell/__tests__/shell-metrics.test.d.ts +1 -0
  33. package/dist-lib/components/blocks/shell/animated-panel-provider.d.ts +1 -1
  34. package/dist-lib/components/blocks/shell/animated-panel.d.ts +1 -1
  35. package/dist-lib/components/blocks/shell/index.d.ts +4 -0
  36. package/dist-lib/components/blocks/shell/shell-fit-atom.d.ts +68 -0
  37. package/dist-lib/components/blocks/shell/shell-metrics.d.ts +54 -0
  38. package/dist-lib/components/blocks/shell/type.d.ts +115 -3
  39. package/dist-lib/components/blocks/shell/use-shell-fit-state.d.ts +25 -0
  40. package/dist-lib/components/blocks/shell/use-shell-fit.d.ts +14 -0
  41. package/dist-lib/components/blocks/shell/use-shell-panels.d.ts +12 -1
  42. package/dist-lib/components/blocks/shell/utils.d.ts +32 -0
  43. package/dist-lib/components/composed/tree/__tests__/tree-atom.test.d.ts +1 -0
  44. package/dist-lib/components/composed/tree/__tests__/tree.test.d.ts +1 -0
  45. package/dist-lib/components/composed/tree/__tests__/use-tree.test.d.ts +1 -0
  46. package/dist-lib/components/composed/tree/constants.d.ts +35 -0
  47. package/dist-lib/components/composed/tree/index.d.ts +6 -0
  48. package/dist-lib/components/composed/tree/tree-atom.d.ts +160 -0
  49. package/dist-lib/components/composed/tree/tree-context.d.ts +4 -0
  50. package/dist-lib/components/composed/tree/tree-node.d.ts +12 -0
  51. package/dist-lib/components/composed/tree/tree.d.ts +19 -0
  52. package/dist-lib/components/composed/tree/type.d.ts +290 -0
  53. package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +18 -0
  54. package/dist-lib/components/composed/tree/use-tree-state.d.ts +25 -0
  55. package/dist-lib/components/composed/tree/use-tree.d.ts +24 -0
  56. package/dist-lib/components/composed/tree/utils.d.ts +28 -0
  57. package/dist-lib/components/composed/user-avatar.d.ts +5 -28
  58. package/dist-lib/components/user-avatar/__tests__/user-avatar.test.d.ts +1 -0
  59. package/dist-lib/components/user-avatar/audio-wave.d.ts +2 -0
  60. package/dist-lib/components/user-avatar/component.d.ts +3 -0
  61. package/dist-lib/components/user-avatar/fallback.d.ts +5 -0
  62. package/dist-lib/components/user-avatar/index.d.ts +18 -0
  63. package/dist-lib/components/user-avatar/presence-badge.d.ts +16 -0
  64. package/dist-lib/components/user-avatar/type.d.ts +71 -0
  65. package/dist-lib/components/user-avatar/use-user-avatar-state.d.ts +26 -0
  66. package/dist-lib/components/user-avatar/use-user-avatar.d.ts +32 -0
  67. package/dist-lib/components/user-avatar/user-avatar-atom.d.ts +55 -0
  68. package/dist-lib/components/user-avatar/utils.d.ts +2 -0
  69. package/dist-lib/index.d.ts +6 -2
  70. package/dist-lib/index.js +3576 -1366
  71. package/dist-lib/index.js.map +1 -1
  72. package/dist-lib/state/exegia-provider.d.ts +51 -0
  73. package/dist-lib/state/index.d.ts +4 -0
  74. package/dist-lib/state/store.d.ts +17 -0
  75. package/package.json +16 -12
  76. package/src/components/beste/piece/browser-frame.tsx +9 -6
  77. package/src/components/blocks/auth/__tests__/auth-state-atom.test.tsx +247 -0
  78. package/src/components/blocks/auth/auth-flow-atom.ts +238 -0
  79. package/src/components/blocks/auth/auth-session-atom.ts +82 -0
  80. package/src/components/blocks/auth/auth-state-type.ts +97 -0
  81. package/src/components/blocks/auth/auth-state.ts +52 -0
  82. package/src/components/blocks/auth/use-auth-state.ts +127 -0
  83. package/src/components/blocks/nav/sidebar/__tests__/ai-sidebar-atom.test.tsx +325 -0
  84. package/src/components/blocks/nav/sidebar/__tests__/use-ai-sidebar.test.tsx +310 -0
  85. package/src/components/blocks/nav/sidebar/ai-sidebar-atom.ts +698 -0
  86. package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +112 -76
  87. package/src/components/blocks/nav/sidebar/index.ts +53 -1
  88. package/src/components/blocks/nav/sidebar/sidebar-context.ts +15 -0
  89. package/src/components/blocks/nav/sidebar/sidebar-row.tsx +119 -50
  90. package/src/components/blocks/nav/sidebar/type.ts +228 -29
  91. package/src/components/blocks/nav/sidebar/use-ai-sidebar-state.ts +123 -0
  92. package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +455 -225
  93. package/src/components/blocks/nav/sidebar/utils.ts +29 -1
  94. package/src/components/blocks/profile/__tests__/profile-card-block.test.tsx +194 -0
  95. package/src/components/blocks/profile/index.ts +28 -0
  96. package/src/components/blocks/profile/profile-card-atom.ts +247 -0
  97. package/src/components/blocks/profile/profile-card-block.tsx +153 -74
  98. package/src/components/blocks/profile/type.ts +95 -0
  99. package/src/components/blocks/profile/use-profile-card-state.ts +67 -0
  100. package/src/components/blocks/profile/use-profile-card.ts +126 -0
  101. package/src/components/blocks/scaffold/__tests__/scaffold.test.tsx +160 -1
  102. package/src/components/blocks/scaffold/constants.ts +7 -0
  103. package/src/components/blocks/scaffold/panel-menu-button.tsx +3 -0
  104. package/src/components/blocks/scaffold/scaffold-canvas.tsx +48 -5
  105. package/src/components/blocks/scaffold/scaffold-inspector.tsx +3 -2
  106. package/src/components/blocks/scaffold/scaffold-main.tsx +1 -1
  107. package/src/components/blocks/scaffold/scaffold-panel.tsx +41 -5
  108. package/src/components/blocks/scaffold/scaffold-root.tsx +18 -3
  109. package/src/components/blocks/scaffold/scaffold-sidebar.tsx +1 -1
  110. package/src/components/blocks/scaffold/scaffold-tab.tsx +45 -7
  111. package/src/components/blocks/scaffold/type.ts +28 -0
  112. package/src/components/blocks/scaffold/use-panel-visibility.ts +180 -0
  113. package/src/components/blocks/scaffold/utils.ts +19 -0
  114. package/src/components/blocks/shell/__tests__/shell-fit-atom.test.tsx +360 -0
  115. package/src/components/blocks/shell/__tests__/shell-layout.test.tsx +192 -3
  116. package/src/components/blocks/shell/__tests__/shell-metrics.test.ts +108 -0
  117. package/src/components/blocks/shell/animated-panel-inset.tsx +5 -1
  118. package/src/components/blocks/shell/animated-panel-provider.tsx +77 -11
  119. package/src/components/blocks/shell/animated-panel-trigger.tsx +4 -0
  120. package/src/components/blocks/shell/animated-panel.tsx +126 -29
  121. package/src/components/blocks/shell/index.ts +18 -0
  122. package/src/components/blocks/shell/shell-fit-atom.ts +243 -0
  123. package/src/components/blocks/shell/shell-layout.tsx +55 -53
  124. package/src/components/blocks/shell/shell-metrics.ts +79 -0
  125. package/src/components/blocks/shell/type.ts +130 -3
  126. package/src/components/blocks/shell/use-shell-fit-state.ts +49 -0
  127. package/src/components/blocks/shell/use-shell-fit.ts +135 -0
  128. package/src/components/blocks/shell/use-shell-panels.ts +57 -4
  129. package/src/components/blocks/shell/utils.ts +44 -4
  130. package/src/components/composed/tree/CLAUDE.md +132 -0
  131. package/src/components/composed/tree/__tests__/tree-atom.test.tsx +217 -0
  132. package/src/components/composed/tree/__tests__/tree.test.tsx +525 -0
  133. package/src/components/composed/tree/__tests__/use-tree.test.tsx +333 -0
  134. package/src/components/composed/tree/constants.ts +60 -0
  135. package/src/components/composed/tree/index.ts +51 -0
  136. package/src/components/composed/tree/tree-atom.ts +590 -0
  137. package/src/components/composed/tree/tree-context.ts +13 -0
  138. package/src/components/composed/tree/tree-node.tsx +490 -0
  139. package/src/components/composed/tree/tree.tsx +286 -0
  140. package/src/components/composed/tree/type.ts +322 -0
  141. package/src/components/composed/tree/use-tree-dnd.ts +179 -0
  142. package/src/components/composed/tree/use-tree-state.ts +105 -0
  143. package/src/components/composed/tree/use-tree.ts +307 -0
  144. package/src/components/composed/tree/utils.ts +172 -0
  145. package/src/components/composed/user-avatar.tsx +11 -96
  146. package/src/components/docs/component-preview.tsx +11 -11
  147. package/src/components/user-avatar/__tests__/user-avatar.test.tsx +245 -0
  148. package/src/components/user-avatar/audio-wave.tsx +22 -0
  149. package/src/components/user-avatar/component.tsx +152 -0
  150. package/src/components/user-avatar/fallback.tsx +26 -0
  151. package/src/components/user-avatar/index.ts +34 -0
  152. package/src/components/user-avatar/presence-badge.tsx +56 -0
  153. package/src/components/user-avatar/type.ts +85 -0
  154. package/src/components/user-avatar/use-user-avatar-state.ts +60 -0
  155. package/src/components/user-avatar/use-user-avatar.ts +144 -0
  156. package/src/components/user-avatar/user-avatar-atom.ts +198 -0
  157. package/src/components/user-avatar/utils.ts +8 -0
  158. package/src/index.css +211 -153
  159. package/src/index.ts +35 -2
  160. package/src/state/exegia-provider.tsx +79 -0
  161. package/src/state/index.ts +4 -0
  162. package/src/state/store.ts +19 -0
@@ -1,57 +1,125 @@
1
- "use client";
1
+ "use client"
2
2
 
3
- import { AnimatePresence } from "motion/react";
4
- import { cn } from "@/lib/utils";
5
- import type { AISidebarProps, FlatResource } from "./type"
6
- import { useAISidebar, type UseAISidebarOptions } from "./use-ai-sidebar"
3
+ import { useMemo } from "react"
4
+ import { AnimatePresence } from "motion/react"
5
+ import { cn } from "@/lib/utils"
6
+ import type {
7
+ AISidebarComponentProps,
8
+ AISidebarContextValue,
9
+ AISidebarController,
10
+ AISidebarProps,
11
+ AISidebarViewProps,
12
+ FlatResource,
13
+ } from "./type"
14
+ import { useAISidebar } from "./use-ai-sidebar"
15
+ import { AISidebarContext } from "./sidebar-context"
7
16
  import { ResourceRow } from "./sidebar-row"
8
17
 
9
- export function AISidebar({
18
+ /**
19
+ * Resource tree for the app rail: nested rows with drag-and-drop reorder,
20
+ * inline rename, per-row action menus and full keyboard navigation.
21
+ *
22
+ * Two ways to drive it. Pass the data and handlers as props and the block
23
+ * owns its state, or hold a `useAISidebar` controller and pass it as
24
+ * `controller` — the same rendering, but select, expand, rename, reorder
25
+ * and menu state are then callable from anywhere in your app.
26
+ */
27
+ export function AISidebar(props: AISidebarComponentProps) {
28
+ // Two components rather than one: hooks may not be called conditionally,
29
+ // and the controller form has no options to build a fallback controller
30
+ // from. Nobody switches a sidebar between the two forms at runtime.
31
+ return props.controller ? (
32
+ <AISidebarView {...viewProps(props)} controller={props.controller} />
33
+ ) : (
34
+ <UncontrolledAISidebar {...props} />
35
+ )
36
+ }
37
+
38
+ /** The props form: builds its own controller and renders through it. */
39
+ function UncontrolledAISidebar(props: AISidebarProps) {
40
+ const {
41
+ renderIcon: _icon,
42
+ renderMenu: _menu,
43
+ renderActionsTrigger: _trigger,
44
+ ariaLabel: _label,
45
+ className: _className,
46
+ controller: _controller,
47
+ ...options
48
+ } = props
49
+ const controller = useAISidebar(options)
50
+ return <AISidebarView {...viewProps(props)} controller={controller} />
51
+ }
52
+
53
+ function viewProps(props: AISidebarComponentProps): AISidebarViewProps {
54
+ return {
55
+ renderIcon: props.renderIcon,
56
+ renderMenu: props.renderMenu,
57
+ renderActionsTrigger: props.renderActionsTrigger,
58
+ ariaLabel: props.ariaLabel,
59
+ className: props.className,
60
+ }
61
+ }
62
+
63
+ /** Rendering only — every piece of state lives on the controller. */
64
+ function AISidebarView({
65
+ controller,
10
66
  renderIcon,
11
67
  renderMenu,
12
68
  renderActionsTrigger,
13
69
  ariaLabel = "Resources",
14
70
  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 as UseAISidebarOptions);
71
+ }: AISidebarViewProps & { controller: AISidebarController }) {
72
+ const { dnd, hover } = controller
73
+ const { draggingId, dropTarget } = dnd
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
+ )
45
113
 
46
114
  return (
47
- <>
115
+ <AISidebarContext.Provider value={context}>
48
116
  <div
49
117
  role="tree"
50
118
  aria-label={ariaLabel}
51
119
  aria-multiselectable="false"
52
- onDragOver={handleRootDragOver}
53
- onDrop={handleDrop}
54
- onMouseLeave={clearHover}
120
+ onDragOver={dnd.onRootDragOver}
121
+ onDrop={dnd.onDrop}
122
+ onMouseLeave={hover.clear}
55
123
  className={cn(
56
124
  "relative min-w-0 flex-col gap-0.5 [overflow-anchor:none] group-data-[state=collapsed]/sidebar:hidden",
57
125
  draggingId && "pb-9 select-none",
@@ -59,40 +127,8 @@ export function AISidebar({
59
127
  )}
60
128
  >
61
129
  <AnimatePresence initial={false}>
62
- {flat.map((row: FlatResource) => (
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 && !draggingId && !row.item.disabled
76
- }
77
- hoverLayoutId={hoverLayoutId}
78
- onHoverChange={(hovered) => handleRowHover(row.item.id, hovered)}
79
- onFocus={() => setFocusedId(row.item.id)}
80
- onSelect={() => select(row.item.id)}
81
- onToggle={() => toggle(row.item.id)}
82
- onKeyDown={(event) => handleKeyDown(event, row)}
83
- onRenameStart={() => setRenamingId(row.item.id)}
84
- onRenameCancel={() => setRenamingId(null)}
85
- onRenameCommit={(label) => commitRename(row, label)}
86
- onMenuOpenChange={(open) => handleMenuOpenChange(row, open)}
87
- onDragStart={handleRowDragStart}
88
- onDragEnd={handleRowDragEnd}
89
- onDragOver={handleRowDragOver}
90
- onDrop={handleDrop}
91
- renderIcon={renderIcon}
92
- renderMenu={renderMenu}
93
- renderActionsTrigger={renderActionsTrigger}
94
- setRef={(node) => setRowRef(row.item.id, node)}
95
- />
130
+ {controller.flat.map((row: FlatResource) => (
131
+ <ResourceRow key={row.item.id} row={row} />
96
132
  ))}
97
133
  </AnimatePresence>
98
134
 
@@ -107,8 +143,8 @@ export function AISidebar({
107
143
  )}
108
144
  </div>
109
145
  <span className="sr-only" aria-live="polite">
110
- {announcement}
146
+ {controller.announcement}
111
147
  </span>
112
- </>
148
+ </AISidebarContext.Provider>
113
149
  )
114
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
- 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) {
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
- onRenameStart()
151
+ startRename(row.item.id)
100
152
  },
101
153
  }) ?? (
102
154
  <button
103
155
  type="button"
104
156
  onClick={() => {
105
157
  onMenuOpenChange(false)
106
- onRenameStart()
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={onFocus}
131
- onKeyDown={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) onToggle()
141
- else onSelect()
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
- onRenameStart()
198
+ startRename(row.item.id)
147
199
  }}
148
200
  onMouseEnter={() => {
149
201
  setHovered(true)
150
- onHoverChange?.(true)
202
+ onRowHover(row.item.id, true)
151
203
  }}
152
204
  onMouseLeave={() => {
153
205
  setHovered(false)
154
- onHoverChange?.(false)
206
+ onRowHover(row.item.id, false)
155
207
  }}
156
208
  onDragStartCapture={(event) => {
157
209
  draggedRef.current = true
158
- onDragStart(event, row.item.id)
210
+ dnd.onRowDragStart(event, row.item.id)
159
211
  }}
160
212
  onDragEndCapture={() => {
161
- onDragEnd()
213
+ dnd.onRowDragEnd()
162
214
  requestAnimationFrame(() => {
163
215
  draggedRef.current = false
164
216
  })
165
217
  }}
166
- onDragOver={(event) => onDragOver(event, row)}
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) onRenameCommit(draft)
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
- onRenameCommit(draft)
287
+ rename(row.item.id, draft)
236
288
  }
237
289
  if (event.key === "Escape") {
238
290
  skipRenameBlurRef.current = true
239
- onRenameCancel()
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"