@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
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
5
|
+
import { useReducedMotion } from "motion/react"
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
ImageStatus,
|
|
9
|
+
UserAvatarConfig,
|
|
10
|
+
UserAvatarInstanceId,
|
|
11
|
+
UserPresence,
|
|
12
|
+
} from "./type"
|
|
13
|
+
import {
|
|
14
|
+
projectUserAvatarPropsAtom,
|
|
15
|
+
removeUserAvatarInstance,
|
|
16
|
+
setUserAvatarBezelAngleAtom,
|
|
17
|
+
setUserAvatarImageStatusAtom,
|
|
18
|
+
userAvatarBezelAngleAtom,
|
|
19
|
+
userAvatarImageStatusAtom,
|
|
20
|
+
userAvatarPresenceAtom,
|
|
21
|
+
} from "./user-avatar-atom"
|
|
22
|
+
|
|
23
|
+
export interface UseUserAvatarOptions {
|
|
24
|
+
avatarId?: UserAvatarInstanceId
|
|
25
|
+
/** Controlled presence. `undefined` leaves the store in charge. */
|
|
26
|
+
presence?: UserPresence
|
|
27
|
+
/** Track the pointer for the bezel highlight. */
|
|
28
|
+
bezel: boolean
|
|
29
|
+
/** Only meaningful with an image — no image, no status to track. */
|
|
30
|
+
hasImage: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface UserAvatarBinding {
|
|
34
|
+
avatarId: UserAvatarInstanceId
|
|
35
|
+
presence: UserPresence | null
|
|
36
|
+
bezelAngle: number
|
|
37
|
+
imageStatus: ImageStatus
|
|
38
|
+
/** Hand to the image's `onLoadingStatusChange`. Stable. */
|
|
39
|
+
setImageStatus: (status: ImageStatus) => void
|
|
40
|
+
/** Attach to the avatar root — the bezel is lit relative to its centre. */
|
|
41
|
+
ref: React.RefObject<HTMLElement | null>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Below this the angle write is skipped: a rim highlight cannot show a
|
|
45
|
+
* sub-degree change, and skipping keeps the pointer loop store-quiet. */
|
|
46
|
+
const ANGLE_EPSILON = 1.5
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Binds one avatar to its slice of the store: projects the controlled
|
|
50
|
+
* `presence` prop, tracks the image status, and — while `bezel` is on —
|
|
51
|
+
* follows the pointer with a rAF-coalesced `pointermove` listener that turns
|
|
52
|
+
* the pointer's bearing from the avatar's centre into `bezelAngle`.
|
|
53
|
+
*
|
|
54
|
+
* Unnamed avatars key off `useId` and are dropped from the store on unmount;
|
|
55
|
+
* a named `avatarId` outlives its component so a presence set elsewhere
|
|
56
|
+
* survives a remount.
|
|
57
|
+
*/
|
|
58
|
+
export function useUserAvatar(options: UseUserAvatarOptions): UserAvatarBinding {
|
|
59
|
+
const { presence: presenceProp, bezel, hasImage } = options
|
|
60
|
+
const generatedId = React.useId()
|
|
61
|
+
const avatarId = options.avatarId ?? generatedId
|
|
62
|
+
const controlsPresence = presenceProp !== undefined
|
|
63
|
+
|
|
64
|
+
const config = React.useMemo<UserAvatarConfig>(
|
|
65
|
+
() => ({ controlsPresence }),
|
|
66
|
+
[controlsPresence]
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const project = useSetAtom(projectUserAvatarPropsAtom(avatarId))
|
|
70
|
+
const setBezelAngle = useSetAtom(setUserAvatarBezelAngleAtom(avatarId))
|
|
71
|
+
const setImageStatus = useSetAtom(setUserAvatarImageStatusAtom(avatarId))
|
|
72
|
+
|
|
73
|
+
// Layout effect: the projection must land before children read the store
|
|
74
|
+
// in the same commit. Primitive deps, so no loop guard is needed here.
|
|
75
|
+
React.useLayoutEffect(() => {
|
|
76
|
+
project(config, presenceProp)
|
|
77
|
+
}, [project, config, presenceProp])
|
|
78
|
+
|
|
79
|
+
// A generated key belongs to this mount only; a given `avatarId` is the
|
|
80
|
+
// app's handle and outlives it.
|
|
81
|
+
React.useEffect(() => {
|
|
82
|
+
if (options.avatarId !== undefined) return
|
|
83
|
+
return () => removeUserAvatarInstance(avatarId)
|
|
84
|
+
}, [options.avatarId, avatarId])
|
|
85
|
+
|
|
86
|
+
const presence = useAtomValue(userAvatarPresenceAtom(avatarId))
|
|
87
|
+
const bezelAngle = useAtomValue(userAvatarBezelAngleAtom(avatarId))
|
|
88
|
+
const imageStatus = useAtomValue(userAvatarImageStatusAtom(avatarId))
|
|
89
|
+
|
|
90
|
+
const ref = React.useRef<HTMLElement | null>(null)
|
|
91
|
+
const reduce = useReducedMotion()
|
|
92
|
+
|
|
93
|
+
// Pointer → bearing. Coalesced to one write per animation frame, and only
|
|
94
|
+
// when the angle moved enough to matter, so a room full of avatars is
|
|
95
|
+
// still one cheap listener each and near-zero store traffic while idle.
|
|
96
|
+
React.useEffect(() => {
|
|
97
|
+
if (!bezel || reduce || typeof window === "undefined") return
|
|
98
|
+
let frame = 0
|
|
99
|
+
let last = Number.NaN
|
|
100
|
+
let pointer: { x: number; y: number } | null = null
|
|
101
|
+
|
|
102
|
+
const flush = () => {
|
|
103
|
+
frame = 0
|
|
104
|
+
const host = ref.current
|
|
105
|
+
if (!host || !pointer) return
|
|
106
|
+
const rect = host.getBoundingClientRect()
|
|
107
|
+
if (rect.width === 0) return
|
|
108
|
+
const dx = pointer.x - (rect.left + rect.width / 2)
|
|
109
|
+
const dy = pointer.y - (rect.top + rect.height / 2)
|
|
110
|
+
// atan2 measures from 3 o'clock anticlockwise-positive in screen
|
|
111
|
+
// space; rotate so 0 is 12 o'clock and clockwise grows.
|
|
112
|
+
const bearing = ((Math.atan2(dy, dx) * 180) / Math.PI + 90 + 360) % 360
|
|
113
|
+
// Unwrap: step from the last value by the shortest arc, so the stored
|
|
114
|
+
// angle is continuous and the rim never spins the long way round.
|
|
115
|
+
let angle = bearing
|
|
116
|
+
if (!Number.isNaN(last)) {
|
|
117
|
+
let delta = bearing - (((last % 360) + 360) % 360)
|
|
118
|
+
if (delta > 180) delta -= 360
|
|
119
|
+
if (delta < -180) delta += 360
|
|
120
|
+
if (Math.abs(delta) < ANGLE_EPSILON) return
|
|
121
|
+
angle = last + delta
|
|
122
|
+
}
|
|
123
|
+
last = angle
|
|
124
|
+
setBezelAngle(angle)
|
|
125
|
+
}
|
|
126
|
+
const onMove = (event: PointerEvent) => {
|
|
127
|
+
pointer = { x: event.clientX, y: event.clientY }
|
|
128
|
+
if (frame === 0) frame = window.requestAnimationFrame(flush)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
window.addEventListener("pointermove", onMove, { passive: true })
|
|
132
|
+
return () => {
|
|
133
|
+
window.removeEventListener("pointermove", onMove)
|
|
134
|
+
if (frame !== 0) window.cancelAnimationFrame(frame)
|
|
135
|
+
}
|
|
136
|
+
}, [bezel, reduce, setBezelAngle])
|
|
137
|
+
|
|
138
|
+
// No image, no status: keep the store honest when `src` goes away.
|
|
139
|
+
React.useEffect(() => {
|
|
140
|
+
if (!hasImage) setImageStatus("idle")
|
|
141
|
+
}, [hasImage, setImageStatus])
|
|
142
|
+
|
|
143
|
+
return { avatarId, presence, bezelAngle, imageStatus, setImageStatus, ref }
|
|
144
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User avatar state — Jotai atom families keyed by an avatar id.
|
|
3
|
+
*
|
|
4
|
+
* Follows the tree's layout (`components/composed/tree/tree-atom.ts`): one
|
|
5
|
+
* in-house `keyed()` family per field, write-only action atoms for every
|
|
6
|
+
* mutation, a composed read-only state atom, and `removeUserAvatarInstance`
|
|
7
|
+
* to drop an id from every family. Nothing here touches the DOM — the
|
|
8
|
+
* pointer maths that feeds `bezelAngle` lives in `use-user-avatar.ts`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { atom } from "jotai"
|
|
12
|
+
import type { Getter, Setter } from "jotai"
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
ImageStatus,
|
|
16
|
+
UserAvatarConfig,
|
|
17
|
+
UserAvatarInstanceId,
|
|
18
|
+
UserAvatarState,
|
|
19
|
+
UserPresence,
|
|
20
|
+
} from "./type"
|
|
21
|
+
|
|
22
|
+
/** Light from the top-left, the classic emboss, until a pointer moves. */
|
|
23
|
+
export const DEFAULT_BEZEL_ANGLE = 315
|
|
24
|
+
|
|
25
|
+
const DEFAULT_CONFIG: UserAvatarConfig = { controlsPresence: false }
|
|
26
|
+
|
|
27
|
+
// ── families ───────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
type Family<AtomType> = ((id: UserAvatarInstanceId) => AtomType) & {
|
|
30
|
+
remove: (id: UserAvatarInstanceId) => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const families: { remove: (id: UserAvatarInstanceId) => void }[] = []
|
|
34
|
+
|
|
35
|
+
function keyed<AtomType>(create: (id: UserAvatarInstanceId) => AtomType) {
|
|
36
|
+
const cache = new Map<UserAvatarInstanceId, AtomType>()
|
|
37
|
+
const family = ((id: UserAvatarInstanceId) => {
|
|
38
|
+
let instance = cache.get(id)
|
|
39
|
+
if (instance === undefined) {
|
|
40
|
+
instance = create(id)
|
|
41
|
+
cache.set(id, instance)
|
|
42
|
+
}
|
|
43
|
+
return instance
|
|
44
|
+
}) as Family<AtomType>
|
|
45
|
+
family.remove = (id: UserAvatarInstanceId) => {
|
|
46
|
+
cache.delete(id)
|
|
47
|
+
}
|
|
48
|
+
families.push(family)
|
|
49
|
+
return family
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stateFamily<Value>(name: string, initialValue: Value) {
|
|
53
|
+
return keyed((id) => {
|
|
54
|
+
const instance = atom(initialValue)
|
|
55
|
+
instance.debugLabel = `userAvatar/${id}/${name}`
|
|
56
|
+
return instance
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readFamily<Value>(
|
|
61
|
+
name: string,
|
|
62
|
+
read: (get: Getter, id: UserAvatarInstanceId) => Value
|
|
63
|
+
) {
|
|
64
|
+
return keyed((id) => {
|
|
65
|
+
const instance = atom((get) => read(get, id))
|
|
66
|
+
instance.debugLabel = `userAvatar/${id}/${name}`
|
|
67
|
+
return instance
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function actionFamily<Args extends unknown[]>(
|
|
72
|
+
name: string,
|
|
73
|
+
write: (
|
|
74
|
+
get: Getter,
|
|
75
|
+
set: Setter,
|
|
76
|
+
id: UserAvatarInstanceId,
|
|
77
|
+
...args: Args
|
|
78
|
+
) => void
|
|
79
|
+
) {
|
|
80
|
+
return keyed((id) => {
|
|
81
|
+
const instance = atom(null, (get, set, ...args: Args) =>
|
|
82
|
+
write(get, set, id, ...args)
|
|
83
|
+
)
|
|
84
|
+
instance.debugLabel = `userAvatar/${id}/${name}`
|
|
85
|
+
return instance
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── primitives ─────────────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
/** `null` = no badge. */
|
|
92
|
+
export const userAvatarPresenceAtom = stateFamily<UserPresence | null>(
|
|
93
|
+
"presence",
|
|
94
|
+
null
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
/** Degrees clockwise from 12 o'clock — where the bezel highlight sits. */
|
|
98
|
+
export const userAvatarBezelAngleAtom = stateFamily<number>(
|
|
99
|
+
"bezelAngle",
|
|
100
|
+
DEFAULT_BEZEL_ANGLE
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
export const userAvatarImageStatusAtom = stateFamily<ImageStatus>(
|
|
104
|
+
"imageStatus",
|
|
105
|
+
"idle"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
/** @internal Which fields the mounted component controls from props. */
|
|
109
|
+
export const userAvatarConfigAtom = stateFamily<UserAvatarConfig>(
|
|
110
|
+
"config",
|
|
111
|
+
DEFAULT_CONFIG
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
// ── derived ────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
export const userAvatarIsOnlineAtom = readFamily(
|
|
117
|
+
"isOnline",
|
|
118
|
+
(get, id) => get(userAvatarPresenceAtom(id)) === "online"
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
/** Whole-state read for callers that want the object; field atoms above are
|
|
122
|
+
* cheaper for components that watch one thing. */
|
|
123
|
+
export const userAvatarStateAtom = readFamily<UserAvatarState>(
|
|
124
|
+
"state",
|
|
125
|
+
(get, id) => ({
|
|
126
|
+
presence: get(userAvatarPresenceAtom(id)),
|
|
127
|
+
bezelAngle: get(userAvatarBezelAngleAtom(id)),
|
|
128
|
+
imageStatus: get(userAvatarImageStatusAtom(id)),
|
|
129
|
+
})
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
// ── actions ────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/** Inert while a `presence` prop controls the avatar — the prop is truth. */
|
|
135
|
+
export const setUserAvatarPresenceAtom = actionFamily<
|
|
136
|
+
[presence: UserPresence | null]
|
|
137
|
+
>("setPresence", (get, set, id, presence) => {
|
|
138
|
+
if (get(userAvatarConfigAtom(id)).controlsPresence) return
|
|
139
|
+
set(userAvatarPresenceAtom(id), presence)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
/** online ⇄ offline; a badge-less avatar comes online. */
|
|
143
|
+
export const toggleUserAvatarPresenceAtom = actionFamily<[]>(
|
|
144
|
+
"togglePresence",
|
|
145
|
+
(get, set, id) => {
|
|
146
|
+
const next = get(userAvatarPresenceAtom(id)) === "online" ? "offline" : "online"
|
|
147
|
+
set(setUserAvatarPresenceAtom(id), next)
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
/** Stored as given — continuous, not wrapped into [0, 360): the rim is
|
|
152
|
+
* rotated with a CSS transition, and an unwrapped angle is what lets a
|
|
153
|
+
* 359° → 1° move take the short way round. Written on every pointer frame,
|
|
154
|
+
* so it stays a bare set — no config gate, no side effects. */
|
|
155
|
+
export const setUserAvatarBezelAngleAtom = actionFamily<[angle: number]>(
|
|
156
|
+
"setBezelAngle",
|
|
157
|
+
(_get, set, id, angle) => {
|
|
158
|
+
if (!Number.isFinite(angle)) return
|
|
159
|
+
set(userAvatarBezelAngleAtom(id), angle)
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
/** @internal Reported by the image element; drives the skeleton. */
|
|
164
|
+
export const setUserAvatarImageStatusAtom = actionFamily<[status: ImageStatus]>(
|
|
165
|
+
"setImageStatus",
|
|
166
|
+
(_get, set, id, status) => {
|
|
167
|
+
set(userAvatarImageStatusAtom(id), status)
|
|
168
|
+
}
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
/** @internal Projection of the mounted component's controlled props. */
|
|
172
|
+
export const projectUserAvatarPropsAtom = actionFamily<
|
|
173
|
+
[config: UserAvatarConfig, presence: UserPresence | null | undefined]
|
|
174
|
+
>("projectProps", (get, set, id, config, presence) => {
|
|
175
|
+
const wasControlled = get(userAvatarConfigAtom(id)).controlsPresence
|
|
176
|
+
set(userAvatarConfigAtom(id), config)
|
|
177
|
+
if (config.controlsPresence) set(userAvatarPresenceAtom(id), presence ?? null)
|
|
178
|
+
// Releasing control (`presence` prop → undefined) clears the badge rather
|
|
179
|
+
// than leaving the last projected value stranded in the store — going
|
|
180
|
+
// from "online" to no presence is a real transition for a user object.
|
|
181
|
+
else if (wasControlled) set(userAvatarPresenceAtom(id), null)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
export const resetUserAvatarAtom = actionFamily<[]>(
|
|
185
|
+
"reset",
|
|
186
|
+
(_get, set, id) => {
|
|
187
|
+
set(userAvatarPresenceAtom(id), null)
|
|
188
|
+
set(userAvatarBezelAngleAtom(id), DEFAULT_BEZEL_ANGLE)
|
|
189
|
+
set(userAvatarImageStatusAtom(id), "idle")
|
|
190
|
+
set(userAvatarConfigAtom(id), DEFAULT_CONFIG)
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
/** Forget an avatar entirely — every family drops the key so the store can
|
|
195
|
+
* release its state. Call on teardown of a named instance. */
|
|
196
|
+
export function removeUserAvatarInstance(id: UserAvatarInstanceId): void {
|
|
197
|
+
for (const family of families) family.remove(id)
|
|
198
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -27,10 +27,39 @@ export * from "./components/ui/textarea"
|
|
|
27
27
|
export * from "./components/composed/password-input"
|
|
28
28
|
export * from "./components/composed/social-providers"
|
|
29
29
|
export * from "./components/composed/tree"
|
|
30
|
-
export
|
|
30
|
+
// user-avatar: named, not `export *` — its barrel also exports an `Avatar`
|
|
31
|
+
// namespace object that would collide with ui/avatar's component above.
|
|
32
|
+
export {
|
|
33
|
+
DEFAULT_BEZEL_ANGLE,
|
|
34
|
+
PresenceBadge,
|
|
35
|
+
UserAvatar,
|
|
36
|
+
initialsFrom,
|
|
37
|
+
removeUserAvatarInstance,
|
|
38
|
+
resetUserAvatarAtom,
|
|
39
|
+
setUserAvatarBezelAngleAtom,
|
|
40
|
+
setUserAvatarPresenceAtom,
|
|
41
|
+
toggleUserAvatarPresenceAtom,
|
|
42
|
+
useUserAvatar,
|
|
43
|
+
useUserAvatarActions,
|
|
44
|
+
useUserAvatarState,
|
|
45
|
+
userAvatarBezelAngleAtom,
|
|
46
|
+
userAvatarIsOnlineAtom,
|
|
47
|
+
userAvatarPresenceAtom,
|
|
48
|
+
userAvatarStateAtom,
|
|
49
|
+
} from "./components/user-avatar"
|
|
50
|
+
export type {
|
|
51
|
+
UseUserAvatarOptions,
|
|
52
|
+
UserAvatarActions,
|
|
53
|
+
UserAvatarBinding,
|
|
54
|
+
UserAvatarInstanceId,
|
|
55
|
+
UserAvatarProps,
|
|
56
|
+
UserAvatarState,
|
|
57
|
+
UserPresence,
|
|
58
|
+
} from "./components/user-avatar"
|
|
31
59
|
|
|
32
60
|
// blocks
|
|
33
61
|
export * from "./components/blocks/auth/auth-shell"
|
|
62
|
+
export * from "./components/blocks/auth/auth-state"
|
|
34
63
|
export * from "./components/blocks/auth/code-auth-block"
|
|
35
64
|
export * from "./components/blocks/auth/forgot-password-block"
|
|
36
65
|
export * from "./components/blocks/auth/linked-accounts-block"
|
|
@@ -46,11 +75,14 @@ export * from "./components/blocks/shell"
|
|
|
46
75
|
export { default as ShellLayout } from "./components/blocks/shell"
|
|
47
76
|
export * from "./components/blocks/scaffold"
|
|
48
77
|
export { default as Layout } from "./components/blocks/layout"
|
|
49
|
-
export * from "./components/blocks/profile
|
|
78
|
+
export * from "./components/blocks/profile"
|
|
50
79
|
|
|
51
80
|
// motion primitives
|
|
52
81
|
export * from "./components/motion/shared-layout-bg"
|
|
53
82
|
|
|
83
|
+
// state
|
|
84
|
+
export * from "./state"
|
|
85
|
+
|
|
54
86
|
// The v0.17 sidebar monolith became the shell block's AnimatedPanel parts.
|
|
55
87
|
// `blocks/shell` above already exports every part under its new name; these
|
|
56
88
|
// aliases keep the published AnimatedSidebar* names working for consumers.
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/* eslint-disable react-refresh/only-export-components */
|
|
4
|
+
/**
|
|
5
|
+
* The library's one and only state provider.
|
|
6
|
+
*
|
|
7
|
+
* Mount it once at the app root. Every library state hook then works
|
|
8
|
+
* anywhere below it — there are no per-component providers to nest.
|
|
9
|
+
*
|
|
10
|
+
* One provider suffices because library state lives in module-level Jotai
|
|
11
|
+
* atom families keyed by an instance id, not in a context created per
|
|
12
|
+
* component. A new stateful component adds atoms to that module scope; it
|
|
13
|
+
* never adds a provider. Consumer code therefore stays exactly as it is as
|
|
14
|
+
* the library grows.
|
|
15
|
+
*
|
|
16
|
+
* Without this provider the hooks still run, which is the trap: Jotai falls
|
|
17
|
+
* back to its implicit default store, and that store is NOT `exegiaStore`,
|
|
18
|
+
* so imperative access would read a different store than the one the
|
|
19
|
+
* components render from. Mount the provider.
|
|
20
|
+
*
|
|
21
|
+
* ```tsx
|
|
22
|
+
* export function App() {
|
|
23
|
+
* return (
|
|
24
|
+
* <ExegiaProvider sound>
|
|
25
|
+
* <Routes />
|
|
26
|
+
* </ExegiaProvider>
|
|
27
|
+
* )
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
import * as React from "react"
|
|
32
|
+
import { Provider, useStore } from "jotai"
|
|
33
|
+
|
|
34
|
+
import { ThemeProvider } from "@/components/theme-provider"
|
|
35
|
+
import { bindSounds } from "@/lib/sound"
|
|
36
|
+
import { exegiaStore } from "./store"
|
|
37
|
+
import type { ExegiaStore } from "./store"
|
|
38
|
+
|
|
39
|
+
/** Options forwarded to the library's `ThemeProvider`. */
|
|
40
|
+
export interface ExegiaThemeOptions {
|
|
41
|
+
defaultTheme?: "dark" | "light" | "system"
|
|
42
|
+
storageKey?: string
|
|
43
|
+
disableTransitionOnChange?: boolean
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface ExegiaProviderProps {
|
|
47
|
+
children: React.ReactNode
|
|
48
|
+
/** Store to mount. Defaults to the library's `exegiaStore`. */
|
|
49
|
+
store?: ExegiaStore
|
|
50
|
+
/** Opt into interaction sound. Off unless the app asks for it. */
|
|
51
|
+
sound?: boolean
|
|
52
|
+
/** Mount `ThemeProvider` with these options. Omit it and none is mounted —
|
|
53
|
+
* it reads `localStorage` in its state initializer, so an unconditional
|
|
54
|
+
* mount would break SSR consumers, and `useTheme` already throws without
|
|
55
|
+
* one. */
|
|
56
|
+
theme?: ExegiaThemeOptions
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function ExegiaProvider({
|
|
60
|
+
children,
|
|
61
|
+
store,
|
|
62
|
+
sound = false,
|
|
63
|
+
theme,
|
|
64
|
+
}: ExegiaProviderProps): React.ReactElement {
|
|
65
|
+
React.useEffect(() => {
|
|
66
|
+
// Idempotent, and deliberately not at module scope: opting into sound is
|
|
67
|
+
// the consuming app's decision, not an import side effect.
|
|
68
|
+
if (sound) bindSounds()
|
|
69
|
+
}, [sound])
|
|
70
|
+
|
|
71
|
+
return (
|
|
72
|
+
<Provider store={store ?? exegiaStore}>
|
|
73
|
+
{theme ? <ThemeProvider {...theme}>{children}</ThemeProvider> : children}
|
|
74
|
+
</Provider>
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The store mounted by the nearest `ExegiaProvider` above the caller. */
|
|
79
|
+
export const useExegiaStore = useStore
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createStore } from "jotai"
|
|
2
|
+
|
|
3
|
+
/** A Jotai store as created by `createStore()`. */
|
|
4
|
+
export type ExegiaStore = ReturnType<typeof createStore>
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The library's default store — the one `ExegiaProvider` mounts unless it is
|
|
8
|
+
* handed another. Exported so non-React code (a router guard, a command
|
|
9
|
+
* palette, a test) can read and write library state imperatively:
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* exegiaStore.get(treeExpandedIdsAtom("app-nav"))
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* A single module-level store is wrong for SSR — state would leak between
|
|
16
|
+
* requests — so render-per-request apps pass a fresh `createStore()` to
|
|
17
|
+
* `ExegiaProvider` instead.
|
|
18
|
+
*/
|
|
19
|
+
export const exegiaStore: ExegiaStore = createStore()
|