@exegia/corpora-ui 0.20.0 → 0.22.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/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/layout.d.ts +4 -1
- 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 -12
- 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/__tests__/logo.test.d.ts +1 -0
- package/dist-lib/components/composed/logo.d.ts +34 -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 +53 -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 +37 -0
- package/dist-lib/components/user-avatar/user-avatar-atom.d.ts +64 -0
- package/dist-lib/components/user-avatar/utils.d.ts +22 -0
- package/dist-lib/index.d.ts +6 -3
- package/dist-lib/index.js +3732 -2670
- 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/layout.ts +30 -2
- 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 -13
- 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/__tests__/logo.test.tsx +60 -0
- package/src/components/composed/logo.tsx +159 -0
- 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 +286 -0
- package/src/components/user-avatar/component.tsx +151 -22
- package/src/components/user-avatar/fallback.tsx +3 -1
- 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 +58 -0
- package/src/components/user-avatar/use-user-avatar-state.ts +60 -0
- package/src/components/user-avatar/use-user-avatar.ts +180 -0
- package/src/components/user-avatar/user-avatar-atom.ts +218 -0
- package/src/components/user-avatar/utils.ts +94 -0
- package/src/index.ts +35 -3
- package/src/state/exegia-provider.tsx +79 -0
- package/src/state/index.ts +4 -0
- package/src/state/store.ts +19 -0
- package/dist-lib/components/blocks/nav/sidebar-block.d.ts +0 -14
- package/dist-lib/components/blocks/nav/sidebar-nav-row.d.ts +0 -3
- package/dist-lib/components/blocks/nav/types.d.ts +0 -60
- package/dist-lib/components/blocks/nav/utils.d.ts +0 -1
- package/dist-lib/components/blocks/shell/animated-sidebar-content.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-footer.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-group-content.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-group-label.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-group.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-header.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-button.d.ts +0 -3
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-item.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-button.d.ts +0 -3
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub-item.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu-sub.d.ts +0 -2
- package/dist-lib/components/blocks/shell/animated-sidebar-menu.d.ts +0 -2
- package/src/components/blocks/nav/__tests__/sidebar-block.test.tsx +0 -125
- package/src/components/blocks/nav/sidebar-block.tsx +0 -142
- package/src/components/blocks/nav/sidebar-nav-row.tsx +0 -78
- package/src/components/blocks/nav/types.ts +0 -65
- package/src/components/blocks/nav/utils.ts +0 -4
- package/src/components/blocks/shell/animated-sidebar-content.tsx +0 -19
- package/src/components/blocks/shell/animated-sidebar-footer.tsx +0 -19
- package/src/components/blocks/shell/animated-sidebar-group-content.tsx +0 -16
- package/src/components/blocks/shell/animated-sidebar-group-label.tsx +0 -29
- package/src/components/blocks/shell/animated-sidebar-group.tsx +0 -16
- package/src/components/blocks/shell/animated-sidebar-header.tsx +0 -16
- package/src/components/blocks/shell/animated-sidebar-menu-button.tsx +0 -151
- package/src/components/blocks/shell/animated-sidebar-menu-item.tsx +0 -20
- package/src/components/blocks/shell/animated-sidebar-menu-sub-button.tsx +0 -85
- package/src/components/blocks/shell/animated-sidebar-menu-sub-item.tsx +0 -19
- package/src/components/blocks/shell/animated-sidebar-menu-sub.tsx +0 -44
- package/src/components/blocks/shell/animated-sidebar-menu.tsx +0 -26
- /package/dist-lib/components/blocks/{nav/__tests__/sidebar-block.test.d.ts → auth/__tests__/auth-state-atom.test.d.ts} +0 -0
|
@@ -1,19 +1,47 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import * as React from "react"
|
|
4
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
4
5
|
|
|
5
|
-
import { playCue } from "@/lib/sound"
|
|
6
|
-
import type { TreeController, TreeNode, UseTreeOptions } from "./type"
|
|
7
|
-
import { useTreeDnd } from "./use-tree-dnd"
|
|
8
6
|
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
7
|
+
cancelTreeRenameAtom,
|
|
8
|
+
collapseAllTreeNodesAtom,
|
|
9
|
+
collapseTreeNodeAtom,
|
|
10
|
+
expandAllTreeNodesAtom,
|
|
11
|
+
expandTreeNodeAtom,
|
|
12
|
+
mountTreeAtom,
|
|
13
|
+
moveTreeNodeAtom,
|
|
14
|
+
projectTreeActiveIdAtom,
|
|
15
|
+
projectTreeCollapsedAtom,
|
|
16
|
+
projectTreeItemsAtom,
|
|
17
|
+
removeTreeInstance,
|
|
18
|
+
renameTreeNodeAtom,
|
|
19
|
+
resetTreeAtom,
|
|
20
|
+
revealTreeAncestorsAtom,
|
|
21
|
+
revealTreeNodeAtom,
|
|
22
|
+
selectTreeNodeAtom,
|
|
23
|
+
setTreeCollapsedAtom,
|
|
24
|
+
setTreeHandlersAtom,
|
|
25
|
+
startTreeRenameAtom,
|
|
26
|
+
toggleTreeCollapsedAtom,
|
|
27
|
+
toggleTreeNodeAtom,
|
|
28
|
+
treeActiveIdAtom,
|
|
29
|
+
treeCanMoveAtom,
|
|
30
|
+
treeCanRenameAtom,
|
|
31
|
+
treeCollapsedAtom,
|
|
32
|
+
treeExpandedIdsAtom,
|
|
33
|
+
treeOwnedItemsAtom,
|
|
34
|
+
treeRenamingIdAtom,
|
|
35
|
+
} from "./tree-atom"
|
|
36
|
+
import type {
|
|
37
|
+
TreeConfig,
|
|
38
|
+
TreeController,
|
|
39
|
+
TreeHandlers,
|
|
40
|
+
TreeSeed,
|
|
41
|
+
UseTreeOptions,
|
|
42
|
+
} from "./type"
|
|
43
|
+
import { useTreeDnd } from "./use-tree-dnd"
|
|
44
|
+
import { findNode, hasThreeLevels } from "./utils"
|
|
17
45
|
|
|
18
46
|
/**
|
|
19
47
|
* The tree's headless controller — every behaviour the component exposes,
|
|
@@ -32,6 +60,10 @@ import {
|
|
|
32
60
|
* <Tree tree={tree} />
|
|
33
61
|
* <Button onClick={tree.collapseAll}>Collapse all</Button>
|
|
34
62
|
* ```
|
|
63
|
+
*
|
|
64
|
+
* State lives in Jotai atoms keyed by `treeId`, so anything under
|
|
65
|
+
* `ExegiaProvider` can drive this tree without the controller — name it and
|
|
66
|
+
* reach for `useTreeState(treeId)` / `useTreeActions(treeId)` elsewhere.
|
|
35
67
|
*/
|
|
36
68
|
export function useTree(options: UseTreeOptions): TreeController {
|
|
37
69
|
const {
|
|
@@ -45,206 +77,172 @@ export function useTree(options: UseTreeOptions): TreeController {
|
|
|
45
77
|
sound = true,
|
|
46
78
|
} = options
|
|
47
79
|
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const items = options.items ?? ownItems
|
|
53
|
-
// The hook may only rewrite the data when it owns it, or when the
|
|
54
|
-
// consumer asked for the next tree through `onItemsChange`.
|
|
55
|
-
const managesItems =
|
|
56
|
-
options.items === undefined || onItemsChange !== undefined
|
|
57
|
-
|
|
58
|
-
const setItems = React.useCallback(
|
|
59
|
-
(next: TreeNode[]) => {
|
|
60
|
-
if (options.items === undefined) setOwnItems(next)
|
|
61
|
-
onItemsChange?.(next)
|
|
62
|
-
},
|
|
63
|
-
[options.items, onItemsChange]
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
const sectioned = variant === "navigation" && hasThreeLevels(items)
|
|
80
|
+
// A generated key isolates unnamed trees from each other; an explicit
|
|
81
|
+
// `treeId` is the app's handle on this one.
|
|
82
|
+
const generatedId = React.useId()
|
|
83
|
+
const treeId = options.treeId ?? generatedId
|
|
67
84
|
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
const
|
|
85
|
+
const controlsItems = options.items !== undefined
|
|
86
|
+
const controlsActiveId = options.activeId !== undefined
|
|
87
|
+
const controlsCollapsed = options.collapsed !== undefined
|
|
88
|
+
// The hook may only rewrite the data when it owns it, or when the consumer
|
|
89
|
+
// asked for the next tree through `onItemsChange`.
|
|
90
|
+
const managesItems = !controlsItems || onItemsChange !== undefined
|
|
91
|
+
const hasRenameHandler = onRename !== undefined
|
|
92
|
+
const hasMoveHandler = onMove !== undefined
|
|
71
93
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
94
|
+
// Primitives only, so this object is stable and the store write below runs
|
|
95
|
+
// once per real change rather than once per render.
|
|
96
|
+
const config = React.useMemo<TreeConfig>(
|
|
97
|
+
() => ({
|
|
98
|
+
variant,
|
|
99
|
+
sound,
|
|
100
|
+
controlsItems,
|
|
101
|
+
controlsActiveId,
|
|
102
|
+
controlsCollapsed,
|
|
103
|
+
managesItems,
|
|
104
|
+
hasRenameHandler,
|
|
105
|
+
hasMoveHandler,
|
|
106
|
+
}),
|
|
107
|
+
[
|
|
108
|
+
variant,
|
|
109
|
+
sound,
|
|
110
|
+
controlsItems,
|
|
111
|
+
controlsActiveId,
|
|
112
|
+
controlsCollapsed,
|
|
113
|
+
managesItems,
|
|
114
|
+
hasRenameHandler,
|
|
115
|
+
hasMoveHandler,
|
|
116
|
+
]
|
|
90
117
|
)
|
|
91
118
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
119
|
+
const handlers = React.useMemo<TreeHandlers>(
|
|
120
|
+
() => ({
|
|
121
|
+
onNavigate,
|
|
122
|
+
onItemsChange,
|
|
123
|
+
onExpandedChange,
|
|
124
|
+
onCollapsedChange,
|
|
125
|
+
onRename,
|
|
126
|
+
onMove,
|
|
127
|
+
}),
|
|
128
|
+
[
|
|
129
|
+
onNavigate,
|
|
130
|
+
onItemsChange,
|
|
131
|
+
onExpandedChange,
|
|
132
|
+
onCollapsedChange,
|
|
133
|
+
onRename,
|
|
134
|
+
onMove,
|
|
135
|
+
]
|
|
109
136
|
)
|
|
110
|
-
const collapsed =
|
|
111
|
-
variant === "sidebar" ? (options.collapsed ?? ownCollapsed) : false
|
|
112
137
|
|
|
113
|
-
|
|
138
|
+
// Read once: the `default*` options describe the mount, not every render.
|
|
139
|
+
const [seed] = React.useState<TreeSeed>(() => ({
|
|
140
|
+
items: options.items ?? options.defaultItems ?? [],
|
|
141
|
+
activeId: options.activeId ?? options.defaultActiveId,
|
|
142
|
+
collapsed: options.collapsed ?? options.defaultCollapsed ?? false,
|
|
143
|
+
expandedIds: options.defaultExpandedIds
|
|
144
|
+
? [...options.defaultExpandedIds]
|
|
145
|
+
: undefined,
|
|
146
|
+
}))
|
|
114
147
|
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
const
|
|
148
|
+
const mount = useSetAtom(mountTreeAtom(treeId))
|
|
149
|
+
const publishHandlers = useSetAtom(setTreeHandlersAtom(treeId))
|
|
150
|
+
const projectItems = useSetAtom(projectTreeItemsAtom(treeId))
|
|
151
|
+
const projectActiveId = useSetAtom(projectTreeActiveIdAtom(treeId))
|
|
152
|
+
const projectCollapsed = useSetAtom(projectTreeCollapsedAtom(treeId))
|
|
153
|
+
const revealAncestors = useSetAtom(revealTreeAncestorsAtom(treeId))
|
|
118
154
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
)
|
|
155
|
+
// Handlers first — they must be in the store before any action can fire.
|
|
156
|
+
// Rewritten every commit; no read atom depends on them, so nobody
|
|
157
|
+
// re-renders for it.
|
|
158
|
+
React.useLayoutEffect(() => {
|
|
159
|
+
publishHandlers(handlers)
|
|
160
|
+
}, [publishHandlers, handlers])
|
|
123
161
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (sound) playCue("toggle")
|
|
129
|
-
},
|
|
130
|
-
[commitExpanded, expandedIds, sound]
|
|
131
|
-
)
|
|
162
|
+
// Before paint, so a seeded branch never paints closed for a frame.
|
|
163
|
+
React.useLayoutEffect(() => {
|
|
164
|
+
mount(config, seed)
|
|
165
|
+
}, [mount, config, seed])
|
|
132
166
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
commitExpanded(next)
|
|
139
|
-
if (sound) playCue("toggle")
|
|
140
|
-
},
|
|
141
|
-
[commitExpanded, expandedIds, sound]
|
|
142
|
-
)
|
|
167
|
+
// Controlled props stay the source of truth; the store carries a projection
|
|
168
|
+
// so the action atoms and remote readers see current data.
|
|
169
|
+
React.useLayoutEffect(() => {
|
|
170
|
+
if (options.items !== undefined) projectItems(options.items)
|
|
171
|
+
}, [options.items, projectItems])
|
|
143
172
|
|
|
144
|
-
|
|
145
|
-
(
|
|
146
|
-
|
|
147
|
-
if (next.has(id)) next.delete(id)
|
|
148
|
-
else next.add(id)
|
|
149
|
-
commitExpanded(next)
|
|
150
|
-
if (sound) playCue("toggle")
|
|
151
|
-
},
|
|
152
|
-
[commitExpanded, expandedIds, sound]
|
|
153
|
-
)
|
|
173
|
+
React.useLayoutEffect(() => {
|
|
174
|
+
if (options.activeId !== undefined) projectActiveId(options.activeId)
|
|
175
|
+
}, [options.activeId, projectActiveId])
|
|
154
176
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}, [
|
|
177
|
+
React.useLayoutEffect(() => {
|
|
178
|
+
if (options.collapsed !== undefined) projectCollapsed(options.collapsed)
|
|
179
|
+
}, [options.collapsed, projectCollapsed])
|
|
158
180
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
181
|
+
// A tree the hook keyed is scrap once its component goes. An explicit
|
|
182
|
+
// `treeId` is the app's key and outlives the mount.
|
|
183
|
+
React.useEffect(() => {
|
|
184
|
+
if (options.treeId !== undefined) return
|
|
185
|
+
return () => removeTreeInstance(treeId)
|
|
186
|
+
}, [treeId, options.treeId])
|
|
162
187
|
|
|
163
|
-
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
)
|
|
188
|
+
// Never `treeItemsAtom` — see the note on `treeOwnedItemsAtom`.
|
|
189
|
+
const ownedItems = useAtomValue(treeOwnedItemsAtom(treeId))
|
|
190
|
+
const items = options.items ?? ownedItems
|
|
191
|
+
const activeId = useAtomValue(treeActiveIdAtom(treeId))
|
|
192
|
+
const expandedIds = useAtomValue(treeExpandedIdsAtom(treeId))
|
|
193
|
+
const collapsed = useAtomValue(treeCollapsedAtom(treeId))
|
|
194
|
+
const renamingId = useAtomValue(treeRenamingIdAtom(treeId))
|
|
195
|
+
const canRename = useAtomValue(treeCanRenameAtom(treeId))
|
|
196
|
+
const canMove = useAtomValue(treeCanMoveAtom(treeId))
|
|
172
197
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
198
|
+
// Navigating into a nested entry from elsewhere reveals its ancestors, even
|
|
199
|
+
// ones the reader had collapsed. In a layout effect, so the branch is open
|
|
200
|
+
// before the frame paints.
|
|
201
|
+
const lastActiveId = React.useRef(activeId)
|
|
202
|
+
React.useLayoutEffect(() => {
|
|
203
|
+
if (lastActiveId.current === activeId) return
|
|
204
|
+
lastActiveId.current = activeId
|
|
205
|
+
if (activeId !== undefined) revealAncestors(activeId)
|
|
206
|
+
}, [activeId, revealAncestors])
|
|
181
207
|
|
|
182
|
-
const
|
|
183
|
-
() =>
|
|
184
|
-
[
|
|
208
|
+
const sectioned = React.useMemo(
|
|
209
|
+
() => variant === "navigation" && hasThreeLevels(items),
|
|
210
|
+
[variant, items]
|
|
211
|
+
)
|
|
212
|
+
const sectionIds = React.useMemo(
|
|
213
|
+
() => (sectioned ? items.map((node) => node.id) : []),
|
|
214
|
+
[items, sectioned]
|
|
185
215
|
)
|
|
186
|
-
|
|
187
216
|
const getNode = React.useCallback(
|
|
188
217
|
(id: string) => findNode(items, id),
|
|
189
218
|
[items]
|
|
190
219
|
)
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
const select = React.useCallback(
|
|
195
|
-
(id: string) => {
|
|
196
|
-
const node = findNode(items, id)
|
|
197
|
-
if (!node || node.disabled) return
|
|
198
|
-
if (options.activeId === undefined) setOwnActiveId(id)
|
|
199
|
-
node.onSelect?.()
|
|
200
|
-
onNavigate?.(node)
|
|
201
|
-
},
|
|
202
|
-
[items, options.activeId, onNavigate]
|
|
203
|
-
)
|
|
204
|
-
|
|
205
|
-
const startRename = React.useCallback(
|
|
206
|
-
(id: string) => {
|
|
207
|
-
// An id from elsewhere (a stale selection, another tree) would arm
|
|
208
|
-
// rename mode against a row that never renders.
|
|
209
|
-
if (!canRename || !findNode(items, id)) return
|
|
210
|
-
setRenamingId(id)
|
|
211
|
-
},
|
|
212
|
-
[canRename, items]
|
|
213
|
-
)
|
|
214
|
-
|
|
215
|
-
const cancelRename = React.useCallback(() => setRenamingId(null), [])
|
|
216
|
-
|
|
217
|
-
const rename = React.useCallback(
|
|
218
|
-
(id: string, label: string) => {
|
|
219
|
-
setRenamingId(null)
|
|
220
|
-
const next = label.trim()
|
|
221
|
-
if (!canRename || !next) return
|
|
222
|
-
const node = findNode(items, id)
|
|
223
|
-
if (!node || node.label === next) return
|
|
224
|
-
onRename?.(id, next)
|
|
225
|
-
if (managesItems) setItems(renameNode(items, id, next))
|
|
226
|
-
},
|
|
227
|
-
[canRename, items, managesItems, onRename, setItems]
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
const move = React.useCallback(
|
|
231
|
-
(id: string, parentId: string | null, index: number) => {
|
|
232
|
-
if (!canMove) return
|
|
233
|
-
onMove?.(id, parentId, index)
|
|
234
|
-
if (managesItems) setItems(moveNode(items, id, parentId, index))
|
|
235
|
-
},
|
|
236
|
-
[canMove, items, managesItems, onMove, setItems]
|
|
220
|
+
const isExpanded = React.useCallback(
|
|
221
|
+
(id: string) => expandedIds.has(id),
|
|
222
|
+
[expandedIds]
|
|
237
223
|
)
|
|
238
224
|
|
|
239
|
-
|
|
225
|
+
// `useSetAtom` setters are already stable — no useCallback needed.
|
|
226
|
+
const expand = useSetAtom(expandTreeNodeAtom(treeId))
|
|
227
|
+
const collapse = useSetAtom(collapseTreeNodeAtom(treeId))
|
|
228
|
+
const toggleExpanded = useSetAtom(toggleTreeNodeAtom(treeId))
|
|
229
|
+
const expandAll = useSetAtom(expandAllTreeNodesAtom(treeId))
|
|
230
|
+
const collapseAll = useSetAtom(collapseAllTreeNodesAtom(treeId))
|
|
231
|
+
const reveal = useSetAtom(revealTreeNodeAtom(treeId))
|
|
232
|
+
const setCollapsed = useSetAtom(setTreeCollapsedAtom(treeId))
|
|
233
|
+
const toggleCollapsed = useSetAtom(toggleTreeCollapsedAtom(treeId))
|
|
234
|
+
const select = useSetAtom(selectTreeNodeAtom(treeId))
|
|
235
|
+
const startRename = useSetAtom(startTreeRenameAtom(treeId))
|
|
236
|
+
const cancelRename = useSetAtom(cancelTreeRenameAtom(treeId))
|
|
237
|
+
const rename = useSetAtom(renameTreeNodeAtom(treeId))
|
|
238
|
+
const move = useSetAtom(moveTreeNodeAtom(treeId))
|
|
239
|
+
const reset = useSetAtom(resetTreeAtom(treeId))
|
|
240
240
|
|
|
241
|
-
const
|
|
242
|
-
() => (sectioned ? items.map((node) => node.id) : []),
|
|
243
|
-
[items, sectioned]
|
|
244
|
-
)
|
|
241
|
+
const dnd = useTreeDnd(treeId)
|
|
245
242
|
|
|
246
243
|
return React.useMemo<TreeController>(
|
|
247
244
|
() => ({
|
|
245
|
+
treeId,
|
|
248
246
|
variant,
|
|
249
247
|
items,
|
|
250
248
|
sectioned,
|
|
@@ -271,9 +269,11 @@ export function useTree(options: UseTreeOptions): TreeController {
|
|
|
271
269
|
rename,
|
|
272
270
|
canMove,
|
|
273
271
|
move,
|
|
272
|
+
reset,
|
|
274
273
|
dnd,
|
|
275
274
|
}),
|
|
276
275
|
[
|
|
276
|
+
treeId,
|
|
277
277
|
variant,
|
|
278
278
|
items,
|
|
279
279
|
sectioned,
|
|
@@ -300,6 +300,7 @@ export function useTree(options: UseTreeOptions): TreeController {
|
|
|
300
300
|
rename,
|
|
301
301
|
canMove,
|
|
302
302
|
move,
|
|
303
|
+
reset,
|
|
303
304
|
dnd,
|
|
304
305
|
]
|
|
305
306
|
)
|
|
@@ -1,101 +1,13 @@
|
|
|
1
|
-
"use client"
|
|
2
|
-
|
|
3
|
-
import * as React from "react"
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
Avatar,
|
|
7
|
-
AvatarFallback,
|
|
8
|
-
AvatarImage,
|
|
9
|
-
type AvatarPrimitive,
|
|
10
|
-
} from "@/components/ui/avatar"
|
|
11
|
-
import { Skeleton } from "@/components/ui/skeleton"
|
|
12
|
-
import { cn } from "@/lib/utils"
|
|
13
|
-
|
|
14
|
-
type ImageStatus = Parameters<
|
|
15
|
-
NonNullable<AvatarPrimitive.Image.Props["onLoadingStatusChange"]>
|
|
16
|
-
>[0]
|
|
17
|
-
|
|
18
|
-
export interface UserAvatarProps extends Omit<
|
|
19
|
-
AvatarPrimitive.Root.Props,
|
|
20
|
-
"children"
|
|
21
|
-
> {
|
|
22
|
-
/** Image URL. Without one the initials show immediately — no skeleton. */
|
|
23
|
-
src?: string
|
|
24
|
-
/** Drives the initials, and the alt text unless `alt` overrides it. */
|
|
25
|
-
name?: string
|
|
26
|
-
/** Overrides the initials derived from `name`. */
|
|
27
|
-
initials?: string
|
|
28
|
-
/**
|
|
29
|
-
* Alt text for the image. Pass "" when adjacent text already names the
|
|
30
|
-
* person — the image is then decorative.
|
|
31
|
-
*/
|
|
32
|
-
alt?: string
|
|
33
|
-
/**
|
|
34
|
-
* Force the skeleton, for when the identity itself is still being fetched.
|
|
35
|
-
* Omitted, it follows the image: a passed `src` skeletons until it resolves.
|
|
36
|
-
*/
|
|
37
|
-
loading?: boolean
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** "Jenny Hamilton" → "JH", "luna" → "L". */
|
|
41
|
-
export function initialsFrom(name: string): string {
|
|
42
|
-
const parts = name.trim().split(/\s+/).filter(Boolean)
|
|
43
|
-
if (parts.length === 0) return ""
|
|
44
|
-
const first = parts[0]?.[0] ?? ""
|
|
45
|
-
const last = parts.length > 1 ? (parts.at(-1)?.[0] ?? "") : ""
|
|
46
|
-
return (first + last).toUpperCase()
|
|
47
|
-
}
|
|
48
|
-
|
|
49
1
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
* the image resolves rather than flashing initials that are about to be
|
|
54
|
-
* replaced. A failed load settles on the initials.
|
|
2
|
+
* Back-compat path. The avatar moved to `components/user-avatar/` (split
|
|
3
|
+
* into component / atoms / hooks); this module re-exports it so existing
|
|
4
|
+
* imports keep resolving to the one implementation. Prefer the new path.
|
|
55
5
|
*/
|
|
56
|
-
export
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}: UserAvatarProps): React.ReactElement {
|
|
65
|
-
const [status, setStatus] = React.useState<ImageStatus>("idle")
|
|
66
|
-
const loading =
|
|
67
|
-
loadingProp ??
|
|
68
|
-
(src !== undefined && (status === "idle" || status === "loading"))
|
|
69
|
-
|
|
70
|
-
return (
|
|
71
|
-
<Avatar
|
|
72
|
-
className={cn(
|
|
73
|
-
"size-8 border-2 border-white bg-neutral-800 text-xl",
|
|
74
|
-
className
|
|
75
|
-
)}
|
|
76
|
-
data-loading={loading || undefined}
|
|
77
|
-
data-slot="user-avatar"
|
|
78
|
-
{...props}
|
|
79
|
-
>
|
|
80
|
-
{/* Held back while forced-loading: a loaded image would hide the
|
|
81
|
-
fallback the skeleton lives in. */}
|
|
82
|
-
{src === undefined || loadingProp === true ? null : (
|
|
83
|
-
<AvatarImage
|
|
84
|
-
alt={alt ?? name}
|
|
85
|
-
onLoadingStatusChange={setStatus}
|
|
86
|
-
src={src}
|
|
87
|
-
/>
|
|
88
|
-
)}
|
|
89
|
-
<AvatarFallback>
|
|
90
|
-
{loading ? (
|
|
91
|
-
<Skeleton
|
|
92
|
-
className="size-full rounded-full"
|
|
93
|
-
data-slot="avatar-skeleton"
|
|
94
|
-
/>
|
|
95
|
-
) : (
|
|
96
|
-
(initials ?? initialsFrom(name))
|
|
97
|
-
)}
|
|
98
|
-
</AvatarFallback>
|
|
99
|
-
</Avatar>
|
|
100
|
-
)
|
|
101
|
-
}
|
|
6
|
+
export {
|
|
7
|
+
UserAvatar,
|
|
8
|
+
initialsFrom,
|
|
9
|
+
useUserAvatar,
|
|
10
|
+
useUserAvatarActions,
|
|
11
|
+
useUserAvatarState,
|
|
12
|
+
} from "@/components/user-avatar"
|
|
13
|
+
export type { UserAvatarProps, UserPresence } from "@/components/user-avatar"
|