@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
|
@@ -8,9 +8,16 @@ import {
|
|
|
8
8
|
UserPlusIcon,
|
|
9
9
|
UsersIcon,
|
|
10
10
|
} from "lucide-react"
|
|
11
|
+
import { motion, useReducedMotion } from "motion/react"
|
|
11
12
|
import * as React from "react"
|
|
12
13
|
|
|
13
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
AnimatedSidebarPanelContext,
|
|
16
|
+
LABEL_ENTER_TRANSITION,
|
|
17
|
+
LABEL_EXIT_TRANSITION,
|
|
18
|
+
REDUCED_TRANSITION,
|
|
19
|
+
} from "@/components/blocks/shell/utils"
|
|
20
|
+
import { UserAvatar } from "@/components/user-avatar"
|
|
14
21
|
import { Button } from "@/components/ui/button"
|
|
15
22
|
import {
|
|
16
23
|
DropdownMenu,
|
|
@@ -26,48 +33,25 @@ import {
|
|
|
26
33
|
import { playCue } from "@/lib/sound"
|
|
27
34
|
import { cn } from "@/lib/utils"
|
|
28
35
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/** Fallback initials. Derived from `name` when omitted. */
|
|
37
|
-
initials?: string
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** An actionable row of the menu. */
|
|
41
|
-
export interface ProfileCardAction {
|
|
42
|
-
type?: "item"
|
|
43
|
-
id: string
|
|
44
|
-
label: string
|
|
45
|
-
icon?: React.ReactNode
|
|
46
|
-
/** Right-aligned hint, e.g. "⌘K". Purely decorative — bind the key yourself. */
|
|
47
|
-
shortcut?: string
|
|
48
|
-
disabled?: boolean
|
|
49
|
-
variant?: "default" | "destructive"
|
|
50
|
-
/** Returning a promise puts the card in its loading state until it settles. */
|
|
51
|
-
onSelect?: () => void | Promise<void>
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** A heading over the rows that follow it, up to the next separator. */
|
|
55
|
-
export interface ProfileCardLabel {
|
|
56
|
-
type: "label"
|
|
57
|
-
label: string
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** A rule between two sections. */
|
|
61
|
-
export interface ProfileCardSeparator {
|
|
62
|
-
type: "separator"
|
|
63
|
-
}
|
|
36
|
+
import { useProfileCard } from "./use-profile-card"
|
|
37
|
+
import type {
|
|
38
|
+
ProfileCardAction,
|
|
39
|
+
ProfileCardItem,
|
|
40
|
+
ProfileCardUser,
|
|
41
|
+
ProfileCardVariant,
|
|
42
|
+
} from "./type"
|
|
64
43
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
44
|
+
export type {
|
|
45
|
+
ProfileCardAction,
|
|
46
|
+
ProfileCardActions,
|
|
47
|
+
ProfileCardInstanceId,
|
|
48
|
+
ProfileCardItem,
|
|
49
|
+
ProfileCardLabel,
|
|
50
|
+
ProfileCardSeparator,
|
|
51
|
+
ProfileCardState,
|
|
52
|
+
ProfileCardUser,
|
|
53
|
+
ProfileCardVariant,
|
|
54
|
+
} from "./type"
|
|
71
55
|
|
|
72
56
|
/**
|
|
73
57
|
* The menu the card ships with. Spread it to keep the shape and attach your
|
|
@@ -114,6 +98,31 @@ export interface ProfileCardBlockProps {
|
|
|
114
98
|
* width, so `align` stops mattering horizontally.
|
|
115
99
|
*/
|
|
116
100
|
menuWidth?: "content" | "card"
|
|
101
|
+
/**
|
|
102
|
+
* `expanded` shows avatar, name and handle; `collapsed` folds the card to
|
|
103
|
+
* its avatar — for an icon-collapsed sidebar rail. The fold animates: the
|
|
104
|
+
* identity lines and chevron slide to zero width, the avatar stays put.
|
|
105
|
+
* Controlled when passed. Left unset, the card follows the `AnimatedPanel`
|
|
106
|
+
* it sits in (a `SidebarBlock` footer folds with the rail on its own), and
|
|
107
|
+
* outside a panel starts from `defaultVariant`.
|
|
108
|
+
*/
|
|
109
|
+
variant?: ProfileCardVariant
|
|
110
|
+
defaultVariant?: ProfileCardVariant
|
|
111
|
+
onVariantChange?: (variant: ProfileCardVariant) => void
|
|
112
|
+
/**
|
|
113
|
+
* Name this card's slice of the store so `useProfileCardState(id)` /
|
|
114
|
+
* `useProfileCardActions(id)` can read or drive it (fold it, open its
|
|
115
|
+
* menu) from anywhere under `ExegiaProvider`. Unnamed cards key off
|
|
116
|
+
* `useId` and are dropped on unmount.
|
|
117
|
+
*/
|
|
118
|
+
profileCardId?: string
|
|
119
|
+
/**
|
|
120
|
+
* Presence badge on the avatar — overrides `user.presence`. Name the avatar
|
|
121
|
+
* with `avatarId` and `useUserAvatarActions(id).setPresence()` can flip it
|
|
122
|
+
* from a socket handler instead.
|
|
123
|
+
*/
|
|
124
|
+
presence?: ProfileCardUser["presence"]
|
|
125
|
+
avatarId?: string
|
|
117
126
|
/**
|
|
118
127
|
* Emit cuelume press/release on the card and play the open/close cues.
|
|
119
128
|
* Inert unless the app opts into interaction sound via `bindSounds()`.
|
|
@@ -164,6 +173,12 @@ export function ProfileCardBlock({
|
|
|
164
173
|
side = "bottom",
|
|
165
174
|
sideOffset = 8,
|
|
166
175
|
menuWidth = "content",
|
|
176
|
+
variant: variantProp,
|
|
177
|
+
defaultVariant,
|
|
178
|
+
onVariantChange,
|
|
179
|
+
profileCardId,
|
|
180
|
+
presence,
|
|
181
|
+
avatarId,
|
|
167
182
|
open,
|
|
168
183
|
defaultOpen,
|
|
169
184
|
onOpenChange,
|
|
@@ -171,81 +186,145 @@ export function ProfileCardBlock({
|
|
|
171
186
|
onError,
|
|
172
187
|
className,
|
|
173
188
|
}: ProfileCardBlockProps): React.ReactElement {
|
|
174
|
-
const [pending, setPending] = React.useState<string | null>(null)
|
|
175
|
-
// Actions can outlive the card (sign-out unmounts it, typically).
|
|
176
|
-
const mounted = React.useRef(true)
|
|
177
|
-
React.useEffect(() => {
|
|
178
|
-
mounted.current = true
|
|
179
|
-
return () => {
|
|
180
|
-
mounted.current = false
|
|
181
|
-
}
|
|
182
|
-
}, [])
|
|
183
|
-
|
|
184
189
|
const groups = React.useMemo(() => toGroups(items), [items])
|
|
185
190
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
191
|
+
// Optional, not required: the card is a standalone block that also lands
|
|
192
|
+
// in sidebar footers, so it reads the panel context when there is one and
|
|
193
|
+
// does not throw when there is not. Inside a panel the rail is the truth
|
|
194
|
+
// for the fold, so it is projected as a controlled variant.
|
|
195
|
+
const panel = React.useContext(AnimatedSidebarPanelContext)
|
|
196
|
+
const card = useProfileCard({
|
|
197
|
+
profileCardId,
|
|
198
|
+
variant:
|
|
199
|
+
variantProp ??
|
|
200
|
+
(panel ? (panel.collapsed ? "collapsed" : "expanded") : undefined),
|
|
201
|
+
defaultVariant,
|
|
202
|
+
onVariantChange,
|
|
203
|
+
open,
|
|
204
|
+
defaultOpen,
|
|
205
|
+
onError,
|
|
206
|
+
})
|
|
207
|
+
const { collapsed, busy, menuOpen, setMenuOpen, select } = card
|
|
208
|
+
const reduce = useReducedMotion()
|
|
209
|
+
const identity = `${user.name}${user.username ? ` ${user.username}` : ""}`
|
|
198
210
|
|
|
199
211
|
const handleOpenChange: MenuPrimitive.Root.Props["onOpenChange"] = (
|
|
200
212
|
nextOpen,
|
|
201
213
|
details
|
|
202
214
|
) => {
|
|
203
215
|
if (sound) playCue(nextOpen ? "bloom" : "droplet")
|
|
216
|
+
setMenuOpen(nextOpen)
|
|
204
217
|
onOpenChange?.(nextOpen, details)
|
|
205
218
|
}
|
|
206
219
|
|
|
207
220
|
return (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
>
|
|
221
|
+
// The store mirrors the menu: `open` (when passed) is projected into it,
|
|
222
|
+
// and this always renders from it, so `useProfileCardActions(id).openMenu()`
|
|
223
|
+
// works the same whether or not the consumer controls the prop.
|
|
224
|
+
<DropdownMenu onOpenChange={handleOpenChange} open={menuOpen}>
|
|
213
225
|
<DropdownMenuTrigger
|
|
214
226
|
disabled={busy}
|
|
215
227
|
render={
|
|
216
228
|
<Button
|
|
217
229
|
// Spelled out rather than left to the name-from-contents rule: the
|
|
218
230
|
// avatar sits inside the button and the handle is a second line.
|
|
219
|
-
aria-label={`${
|
|
231
|
+
aria-label={`${identity}, account menu`}
|
|
220
232
|
className={cn(
|
|
221
233
|
// Quiet at rest — the background only surfaces on hover or
|
|
222
234
|
// while the menu is open.
|
|
223
235
|
"h-auto w-full justify-between py-1.5 pl-1.5 data-popup-open:bg-accent sm:h-auto",
|
|
236
|
+
// Folded: a 40px avatar tile, the size the tree rail uses. It
|
|
237
|
+
// is 8px wider than a footer's 32px inner box on purpose — the
|
|
238
|
+
// -mx-1 lets it borrow the footer padding so the 32px avatar
|
|
239
|
+
// clears the 1px border and sits dead centre. The label spans
|
|
240
|
+
// below fold to zero width rather than unmounting so the change
|
|
241
|
+
// animates. Button's own gap-2 stays: its always-mounted loading
|
|
242
|
+
// slot carries a -ms-2 that cancels it, so zeroing the gap would
|
|
243
|
+
// pull the avatar 8px left; the folded chevron cancels its own
|
|
244
|
+
// gap instead (see below). `sm:size-10` because the base carries
|
|
245
|
+
// an `sm:h-auto` that an unprefixed size would not beat.
|
|
246
|
+
collapsed &&
|
|
247
|
+
"-mx-1 size-10 shrink-0 justify-center rounded-xl p-0 sm:size-10",
|
|
224
248
|
className
|
|
225
249
|
)}
|
|
250
|
+
data-collapsed={collapsed ? "" : undefined}
|
|
226
251
|
data-slot="profile-card"
|
|
252
|
+
data-variant={collapsed ? "collapsed" : "expanded"}
|
|
227
253
|
loading={busy}
|
|
228
254
|
sound={sound}
|
|
255
|
+
// The visible name is gone while folded — name the tile on hover
|
|
256
|
+
// the way the rail's menu buttons do.
|
|
257
|
+
title={collapsed ? identity : undefined}
|
|
229
258
|
variant="ghost"
|
|
230
259
|
>
|
|
231
|
-
<span
|
|
260
|
+
<span
|
|
261
|
+
className={cn(
|
|
262
|
+
"flex min-w-0 items-center justify-start",
|
|
263
|
+
collapsed ? "w-auto gap-0" : "w-full gap-2"
|
|
264
|
+
)}
|
|
265
|
+
>
|
|
232
266
|
<UserAvatar
|
|
233
267
|
// Decorative: the name beside it already labels the card.
|
|
234
268
|
alt=""
|
|
269
|
+
avatarId={avatarId}
|
|
235
270
|
initials={user.initials}
|
|
236
271
|
name={user.name}
|
|
272
|
+
presence={presence ?? user.presence}
|
|
237
273
|
src={user.avatar}
|
|
238
274
|
/>
|
|
239
|
-
<span
|
|
275
|
+
<motion.span
|
|
276
|
+
animate={{
|
|
277
|
+
width: collapsed ? 0 : "auto",
|
|
278
|
+
opacity: collapsed ? 0 : 1,
|
|
279
|
+
x: collapsed ? -4 : 0,
|
|
280
|
+
}}
|
|
281
|
+
aria-hidden={collapsed || undefined}
|
|
282
|
+
className={cn(
|
|
283
|
+
"flex min-w-0 flex-col overflow-hidden text-left whitespace-nowrap",
|
|
284
|
+
collapsed && "pointer-events-none"
|
|
285
|
+
)}
|
|
286
|
+
data-slot="profile-card-identity"
|
|
287
|
+
initial={false}
|
|
288
|
+
transition={
|
|
289
|
+
reduce
|
|
290
|
+
? REDUCED_TRANSITION
|
|
291
|
+
: collapsed
|
|
292
|
+
? LABEL_EXIT_TRANSITION
|
|
293
|
+
: LABEL_ENTER_TRANSITION
|
|
294
|
+
}
|
|
295
|
+
>
|
|
240
296
|
<span className="truncate font-medium">{user.name}</span>
|
|
241
297
|
{user.username ? (
|
|
242
298
|
<span className="truncate text-xs font-normal text-muted-foreground">
|
|
243
299
|
{user.username}
|
|
244
300
|
</span>
|
|
245
301
|
) : null}
|
|
246
|
-
</span>
|
|
302
|
+
</motion.span>
|
|
247
303
|
</span>
|
|
248
|
-
<
|
|
304
|
+
<motion.span
|
|
305
|
+
animate={{
|
|
306
|
+
width: collapsed ? 0 : "auto",
|
|
307
|
+
opacity: collapsed ? 0 : 1,
|
|
308
|
+
x: collapsed ? 4 : 0,
|
|
309
|
+
}}
|
|
310
|
+
aria-hidden="true"
|
|
311
|
+
className={cn(
|
|
312
|
+
"flex shrink-0 items-center overflow-hidden",
|
|
313
|
+
// 0px wide while folded, but the flex gap before it would
|
|
314
|
+
// still offset the avatar — pull it back by that gap.
|
|
315
|
+
collapsed && "-ml-2"
|
|
316
|
+
)}
|
|
317
|
+
initial={false}
|
|
318
|
+
transition={
|
|
319
|
+
reduce
|
|
320
|
+
? REDUCED_TRANSITION
|
|
321
|
+
: collapsed
|
|
322
|
+
? LABEL_EXIT_TRANSITION
|
|
323
|
+
: LABEL_ENTER_TRANSITION
|
|
324
|
+
}
|
|
325
|
+
>
|
|
326
|
+
<ChevronsUpDownIcon aria-hidden="true" className="size-3.5" />
|
|
327
|
+
</motion.span>
|
|
249
328
|
</Button>
|
|
250
329
|
}
|
|
251
330
|
/>
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type * as React from "react"
|
|
2
|
+
|
|
3
|
+
import type { UserPresence } from "@/components/user-avatar"
|
|
4
|
+
|
|
5
|
+
/** The identity shown on the card. */
|
|
6
|
+
export interface ProfileCardUser {
|
|
7
|
+
name: string
|
|
8
|
+
/** Secondary line under the name — a handle, an email, a role. */
|
|
9
|
+
username?: string
|
|
10
|
+
/** Avatar image URL. Falls back to the initials when absent or broken. */
|
|
11
|
+
avatar?: string
|
|
12
|
+
/** Fallback initials. Derived from `name` when omitted. */
|
|
13
|
+
initials?: string
|
|
14
|
+
/** Presence badge on the avatar. Omitted, no badge. */
|
|
15
|
+
presence?: UserPresence
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** An actionable row of the menu. */
|
|
19
|
+
export interface ProfileCardAction {
|
|
20
|
+
type?: "item"
|
|
21
|
+
id: string
|
|
22
|
+
label: string
|
|
23
|
+
icon?: React.ReactNode
|
|
24
|
+
/** Right-aligned hint, e.g. "⌘K". Purely decorative — bind the key yourself. */
|
|
25
|
+
shortcut?: string
|
|
26
|
+
disabled?: boolean
|
|
27
|
+
variant?: "default" | "destructive"
|
|
28
|
+
/** Returning a promise puts the card in its loading state until it settles. */
|
|
29
|
+
onSelect?: () => void | Promise<void>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** A heading over the rows that follow it, up to the next separator. */
|
|
33
|
+
export interface ProfileCardLabel {
|
|
34
|
+
type: "label"
|
|
35
|
+
label: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A rule between two sections. */
|
|
39
|
+
export interface ProfileCardSeparator {
|
|
40
|
+
type: "separator"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* One entry of `items`. Authored flat; the block renders each run between
|
|
45
|
+
* separators as its own menu group so a label actually names its section.
|
|
46
|
+
*/
|
|
47
|
+
export type ProfileCardItem =
|
|
48
|
+
| ProfileCardAction
|
|
49
|
+
| ProfileCardLabel
|
|
50
|
+
| ProfileCardSeparator
|
|
51
|
+
|
|
52
|
+
/** `expanded` shows avatar, name and handle; `collapsed` folds to the avatar
|
|
53
|
+
* alone — for an icon-collapsed sidebar rail. */
|
|
54
|
+
export type ProfileCardVariant = "expanded" | "collapsed"
|
|
55
|
+
|
|
56
|
+
/** Key for one card's state in the store. Any stable string; `useProfileCard`
|
|
57
|
+
* generates one when the component does not name itself. */
|
|
58
|
+
export type ProfileCardInstanceId = string
|
|
59
|
+
|
|
60
|
+
/** Everything the store knows about one card. */
|
|
61
|
+
export interface ProfileCardState {
|
|
62
|
+
variant: ProfileCardVariant
|
|
63
|
+
menuOpen: boolean
|
|
64
|
+
/** Id of the action whose promise is in flight, `null` while idle. */
|
|
65
|
+
pendingActionId: string | null
|
|
66
|
+
/** `pendingActionId !== null` — the trigger is disabled and shows a spinner. */
|
|
67
|
+
busy: boolean
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Write-only handles onto one card. Nothing here re-renders the caller. */
|
|
71
|
+
export interface ProfileCardActions {
|
|
72
|
+
setVariant: (variant: ProfileCardVariant) => void
|
|
73
|
+
toggleVariant: () => void
|
|
74
|
+
/** Convenience over `setVariant`. */
|
|
75
|
+
collapse: () => void
|
|
76
|
+
expand: () => void
|
|
77
|
+
setMenuOpen: (open: boolean) => void
|
|
78
|
+
openMenu: () => void
|
|
79
|
+
closeMenu: () => void
|
|
80
|
+
reset: () => void
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** @internal Which fields the mounted component controls from props. The
|
|
84
|
+
* store must never overwrite a controlled value. */
|
|
85
|
+
export interface ProfileCardConfig {
|
|
86
|
+
controlsVariant: boolean
|
|
87
|
+
controlsMenuOpen: boolean
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @internal Latest option callbacks. Only write atoms read this, so it can
|
|
91
|
+
* be refreshed every commit without re-rendering anything. */
|
|
92
|
+
export interface ProfileCardHandlers {
|
|
93
|
+
onVariantChange?: (variant: ProfileCardVariant) => void
|
|
94
|
+
onError?: (error: unknown) => void
|
|
95
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
profileCardStateAtom,
|
|
8
|
+
resetProfileCardAtom,
|
|
9
|
+
setProfileCardMenuOpenAtom,
|
|
10
|
+
setProfileCardVariantAtom,
|
|
11
|
+
toggleProfileCardVariantAtom,
|
|
12
|
+
} from "./profile-card-atom"
|
|
13
|
+
import type {
|
|
14
|
+
ProfileCardActions,
|
|
15
|
+
ProfileCardInstanceId,
|
|
16
|
+
ProfileCardState,
|
|
17
|
+
} from "./type"
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Read the card registered under `profileCardId` from anywhere below
|
|
21
|
+
* `ExegiaProvider` — no props, no ref, no provider of its own.
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { variant, busy } = useProfileCardState("account")
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Returns the whole state object, so the caller re-renders on any change. A
|
|
28
|
+
* component that watches one field should subscribe to that atom instead:
|
|
29
|
+
* `useAtomValue(profileCardVariantAtom("account"))`.
|
|
30
|
+
*/
|
|
31
|
+
export function useProfileCardState(
|
|
32
|
+
profileCardId: ProfileCardInstanceId
|
|
33
|
+
): ProfileCardState {
|
|
34
|
+
return useAtomValue(profileCardStateAtom(profileCardId))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Write-only handles onto the card registered under `profileCardId`. Nothing
|
|
39
|
+
* here subscribes, so a rail toggle can fold the card without re-rendering
|
|
40
|
+
* when the card changes.
|
|
41
|
+
*
|
|
42
|
+
* ```tsx
|
|
43
|
+
* const { collapse, expand } = useProfileCardActions("account")
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useProfileCardActions(
|
|
47
|
+
profileCardId: ProfileCardInstanceId
|
|
48
|
+
): ProfileCardActions {
|
|
49
|
+
const setVariant = useSetAtom(setProfileCardVariantAtom(profileCardId))
|
|
50
|
+
const toggleVariant = useSetAtom(toggleProfileCardVariantAtom(profileCardId))
|
|
51
|
+
const setMenuOpen = useSetAtom(setProfileCardMenuOpenAtom(profileCardId))
|
|
52
|
+
const reset = useSetAtom(resetProfileCardAtom(profileCardId))
|
|
53
|
+
|
|
54
|
+
return React.useMemo(
|
|
55
|
+
() => ({
|
|
56
|
+
setVariant,
|
|
57
|
+
toggleVariant,
|
|
58
|
+
collapse: () => setVariant("collapsed"),
|
|
59
|
+
expand: () => setVariant("expanded"),
|
|
60
|
+
setMenuOpen,
|
|
61
|
+
openMenu: () => setMenuOpen(true),
|
|
62
|
+
closeMenu: () => setMenuOpen(false),
|
|
63
|
+
reset,
|
|
64
|
+
}),
|
|
65
|
+
[setVariant, toggleVariant, setMenuOpen, reset]
|
|
66
|
+
)
|
|
67
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
profileCardBusyAtom,
|
|
8
|
+
profileCardMenuOpenAtom,
|
|
9
|
+
profileCardVariantAtom,
|
|
10
|
+
projectProfileCardPropsAtom,
|
|
11
|
+
removeProfileCardInstance,
|
|
12
|
+
seedProfileCardVariantAtom,
|
|
13
|
+
selectProfileCardActionAtom,
|
|
14
|
+
setProfileCardHandlersAtom,
|
|
15
|
+
setProfileCardMenuOpenAtom,
|
|
16
|
+
} from "./profile-card-atom"
|
|
17
|
+
import type {
|
|
18
|
+
ProfileCardAction,
|
|
19
|
+
ProfileCardConfig,
|
|
20
|
+
ProfileCardHandlers,
|
|
21
|
+
ProfileCardInstanceId,
|
|
22
|
+
ProfileCardVariant,
|
|
23
|
+
} from "./type"
|
|
24
|
+
|
|
25
|
+
export interface UseProfileCardOptions {
|
|
26
|
+
profileCardId?: ProfileCardInstanceId
|
|
27
|
+
/** Controlled fold. `undefined` leaves the store (and `defaultVariant`) in charge. */
|
|
28
|
+
variant?: ProfileCardVariant
|
|
29
|
+
defaultVariant?: ProfileCardVariant
|
|
30
|
+
onVariantChange?: (variant: ProfileCardVariant) => void
|
|
31
|
+
/** Controlled menu. */
|
|
32
|
+
open?: boolean
|
|
33
|
+
defaultOpen?: boolean
|
|
34
|
+
onError?: (error: unknown) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ProfileCardBinding {
|
|
38
|
+
profileCardId: ProfileCardInstanceId
|
|
39
|
+
variant: ProfileCardVariant
|
|
40
|
+
collapsed: boolean
|
|
41
|
+
menuOpen: boolean
|
|
42
|
+
busy: boolean
|
|
43
|
+
/** Hand to the menu's `onOpenChange` (first argument). Stable. */
|
|
44
|
+
setMenuOpen: (open: boolean) => void
|
|
45
|
+
/** Run an action: `onSelect`, promise → busy, rejection → `onError`. Stable. */
|
|
46
|
+
select: (action: ProfileCardAction) => void
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Binds one profile card to its slice of the store: projects the controlled
|
|
51
|
+
* `variant` / `open` props (one-way, with write gates so the store never
|
|
52
|
+
* overwrites a prop), publishes the latest handlers, and returns the values
|
|
53
|
+
* the component renders from.
|
|
54
|
+
*
|
|
55
|
+
* Unnamed cards key off `useId` and are dropped on unmount; a named
|
|
56
|
+
* `profileCardId` outlives its component, so a fold set from elsewhere
|
|
57
|
+
* (`useProfileCardActions(id).collapse()`) survives a route change.
|
|
58
|
+
*/
|
|
59
|
+
export function useProfileCard(options: UseProfileCardOptions): ProfileCardBinding {
|
|
60
|
+
const {
|
|
61
|
+
variant: variantProp,
|
|
62
|
+
defaultVariant = "expanded",
|
|
63
|
+
onVariantChange,
|
|
64
|
+
open: openProp,
|
|
65
|
+
onError,
|
|
66
|
+
} = options
|
|
67
|
+
const generatedId = React.useId()
|
|
68
|
+
const profileCardId = options.profileCardId ?? generatedId
|
|
69
|
+
|
|
70
|
+
const controlsVariant = variantProp !== undefined
|
|
71
|
+
const controlsMenuOpen = openProp !== undefined
|
|
72
|
+
const config = React.useMemo<ProfileCardConfig>(
|
|
73
|
+
() => ({ controlsVariant, controlsMenuOpen }),
|
|
74
|
+
[controlsVariant, controlsMenuOpen]
|
|
75
|
+
)
|
|
76
|
+
const handlers = React.useMemo<ProfileCardHandlers>(
|
|
77
|
+
() => ({ onVariantChange, onError }),
|
|
78
|
+
[onVariantChange, onError]
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
const project = useSetAtom(projectProfileCardPropsAtom(profileCardId))
|
|
82
|
+
const publishHandlers = useSetAtom(setProfileCardHandlersAtom(profileCardId))
|
|
83
|
+
const seedVariant = useSetAtom(seedProfileCardVariantAtom(profileCardId))
|
|
84
|
+
const setMenuOpen = useSetAtom(setProfileCardMenuOpenAtom(profileCardId))
|
|
85
|
+
const select = useSetAtom(selectProfileCardActionAtom(profileCardId))
|
|
86
|
+
|
|
87
|
+
// Seed the uncontrolled fold once per mount, before the projection — the
|
|
88
|
+
// `default*` options describe the mount, not every render. Layout effects
|
|
89
|
+
// so children read settled values in the same commit; primitive deps, so
|
|
90
|
+
// no loop guard is needed.
|
|
91
|
+
const [seed] = React.useState(() => ({
|
|
92
|
+
variant: variantProp ?? defaultVariant,
|
|
93
|
+
open: openProp ?? options.defaultOpen ?? false,
|
|
94
|
+
}))
|
|
95
|
+
React.useLayoutEffect(() => {
|
|
96
|
+
if (!controlsVariant) seedVariant(seed.variant)
|
|
97
|
+
if (!controlsMenuOpen) setMenuOpen(seed.open)
|
|
98
|
+
// Mount-time seed only.
|
|
99
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
100
|
+
}, [])
|
|
101
|
+
React.useLayoutEffect(() => {
|
|
102
|
+
project(config, variantProp, openProp)
|
|
103
|
+
}, [project, config, variantProp, openProp])
|
|
104
|
+
React.useLayoutEffect(() => {
|
|
105
|
+
publishHandlers(handlers)
|
|
106
|
+
}, [publishHandlers, handlers])
|
|
107
|
+
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
if (options.profileCardId !== undefined) return
|
|
110
|
+
return () => removeProfileCardInstance(profileCardId)
|
|
111
|
+
}, [options.profileCardId, profileCardId])
|
|
112
|
+
|
|
113
|
+
const variant = useAtomValue(profileCardVariantAtom(profileCardId))
|
|
114
|
+
const menuOpen = useAtomValue(profileCardMenuOpenAtom(profileCardId))
|
|
115
|
+
const busy = useAtomValue(profileCardBusyAtom(profileCardId))
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
profileCardId,
|
|
119
|
+
variant,
|
|
120
|
+
collapsed: variant === "collapsed",
|
|
121
|
+
menuOpen,
|
|
122
|
+
busy,
|
|
123
|
+
setMenuOpen,
|
|
124
|
+
select,
|
|
125
|
+
}
|
|
126
|
+
}
|