@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
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
clampPanelWidth,
|
|
5
|
+
fitsPanel,
|
|
6
|
+
metricsEqual,
|
|
7
|
+
panelBounds,
|
|
8
|
+
requiredWidth,
|
|
9
|
+
type ShellMetrics,
|
|
10
|
+
} from "../shell-metrics"
|
|
11
|
+
|
|
12
|
+
/** The columns the shell ships with: rail 256 (56 folded), body floor 360,
|
|
13
|
+
* secondary panel floor 320 — 936px before a panel can exist — plus the 32px
|
|
14
|
+
* of shell padding and column gaps that only the ceiling pays for. */
|
|
15
|
+
function shell(over: Partial<ShellMetrics> = {}): ShellMetrics {
|
|
16
|
+
return {
|
|
17
|
+
rail: 256,
|
|
18
|
+
insetMin: 360,
|
|
19
|
+
panelMin: 320,
|
|
20
|
+
viewport: 1024,
|
|
21
|
+
chrome: 32,
|
|
22
|
+
...over,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe("fitsPanel", () => {
|
|
27
|
+
test("wants the rail plus both floors to clear the viewport", () => {
|
|
28
|
+
expect(requiredWidth(shell())).toBe(936)
|
|
29
|
+
expect(fitsPanel(shell())).toBe(true)
|
|
30
|
+
expect(fitsPanel(shell({ viewport: 800 }))).toBe(false)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
test("is strict — a viewport that fits exactly has nothing left to give", () => {
|
|
34
|
+
expect(fitsPanel(shell({ viewport: 936 }))).toBe(false)
|
|
35
|
+
expect(fitsPanel(shell({ viewport: 937 }))).toBe(true)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
test("a folded rail hands its column back", () => {
|
|
39
|
+
expect(fitsPanel(shell({ rail: 56, viewport: 800 }))).toBe(true)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("a shell with no rail spends nothing on one", () => {
|
|
43
|
+
expect(fitsPanel(shell({ rail: 0, viewport: 700 }))).toBe(true)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("an unmeasurable shell fails open rather than hiding the panel", () => {
|
|
47
|
+
expect(
|
|
48
|
+
fitsPanel({
|
|
49
|
+
rail: 0,
|
|
50
|
+
insetMin: 0,
|
|
51
|
+
panelMin: 0,
|
|
52
|
+
viewport: 0,
|
|
53
|
+
chrome: 0,
|
|
54
|
+
})
|
|
55
|
+
).toBe(true)
|
|
56
|
+
expect(fitsPanel(shell({ panelMin: 0 }))).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
test("ignores the shell's own frame — the rule is stated on the viewport", () => {
|
|
60
|
+
expect(fitsPanel(shell({ chrome: 0 }))).toBe(
|
|
61
|
+
fitsPanel(shell({ chrome: 300 }))
|
|
62
|
+
)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe("panelBounds", () => {
|
|
67
|
+
test("the panel may take every px the body holds above its floor", () => {
|
|
68
|
+
// 1024 - 32 frame - 256 rail - 360 body floor.
|
|
69
|
+
expect(panelBounds(shell())).toEqual({ min: 320, max: 376 })
|
|
70
|
+
expect(panelBounds(shell({ rail: 56 })).max).toBe(576)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
test("the shell's frame comes out of the ceiling, not the floor", () => {
|
|
74
|
+
expect(panelBounds(shell({ chrome: 0 })).max).toBe(408)
|
|
75
|
+
expect(panelBounds(shell({ chrome: 0 })).min).toBe(320)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
test("never inverts — a shell with no room reports its floor twice", () => {
|
|
79
|
+
expect(panelBounds(shell({ viewport: 800 }))).toEqual({
|
|
80
|
+
min: 320,
|
|
81
|
+
max: 320,
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe("clampPanelWidth", () => {
|
|
87
|
+
test("holds a drag inside the bounds and leaves the middle alone", () => {
|
|
88
|
+
expect(clampPanelWidth(600, shell())).toBe(376)
|
|
89
|
+
expect(clampPanelWidth(100, shell())).toBe(320)
|
|
90
|
+
expect(clampPanelWidth(360, shell())).toBe(360)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test("a narrowing viewport pulls a resized panel back down", () => {
|
|
94
|
+
const resized = clampPanelWidth(500, shell({ rail: 56 }))
|
|
95
|
+
expect(resized).toBe(500)
|
|
96
|
+
expect(clampPanelWidth(resized, shell({ rail: 56, viewport: 900 }))).toBe(
|
|
97
|
+
452
|
|
98
|
+
)
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
describe("metricsEqual", () => {
|
|
103
|
+
test("compares fields, so a fresh object off the same layout is quiet", () => {
|
|
104
|
+
expect(metricsEqual(shell(), shell())).toBe(true)
|
|
105
|
+
expect(metricsEqual(shell(), shell({ viewport: 1025 }))).toBe(false)
|
|
106
|
+
expect(metricsEqual(shell(), shell({ rail: 56 }))).toBe(false)
|
|
107
|
+
})
|
|
108
|
+
})
|
|
@@ -13,7 +13,11 @@ export const AnimatedPanelInset = forwardRef<
|
|
|
13
13
|
ref={forwardedRef}
|
|
14
14
|
data-slot="sidebar-inset"
|
|
15
15
|
className={cn(
|
|
16
|
-
|
|
16
|
+
// The body takes every column the panels leave (flex-1 = width 100%)
|
|
17
|
+
// but never yields past its own floor: a resized panel may only take
|
|
18
|
+
// the slack above --inset-min-width, and the shell drops the secondary
|
|
19
|
+
// panel entirely once even that no longer fits.
|
|
20
|
+
"relative flex min-w-(--inset-min-width) flex-1 flex-col bg-neutral-50 dark:bg-neutral-900",
|
|
17
21
|
"outline-offset-0.5 overflow-hidden border-t-3 border-white outline-neutral-100 dark:border-neutral-800 dark:inset-ring-black",
|
|
18
22
|
"rounded-lg shadow-md shadow-neutral-200 dark:shadow-neutral-950",
|
|
19
23
|
className
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react"
|
|
2
|
-
import { useReducedMotion } from "motion/react"
|
|
2
|
+
import { useIsomorphicLayoutEffect, useReducedMotion } from "motion/react"
|
|
3
3
|
import { cn } from "@/lib/utils.ts"
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
SidebarSide,
|
|
7
|
-
} from "./type"
|
|
4
|
+
import type { AnimatedSidebarProviderProps, SidebarSide } from "./type"
|
|
5
|
+
import { useShellFit } from "./use-shell-fit"
|
|
8
6
|
import {
|
|
9
7
|
AnimatedSidebarContext,
|
|
8
|
+
SHELL_WIDTHS,
|
|
10
9
|
SIDEBAR_KEYBOARD_SHORTCUT,
|
|
11
10
|
useIsMobile,
|
|
12
11
|
} from "./utils"
|
|
13
12
|
|
|
14
13
|
export function AnimatedPanelProvider({
|
|
15
14
|
children,
|
|
15
|
+
shellId,
|
|
16
|
+
defaultPanelWidth,
|
|
16
17
|
open,
|
|
17
18
|
defaultOpen,
|
|
18
19
|
onOpenChange,
|
|
19
20
|
openMobile,
|
|
20
21
|
defaultOpenMobile,
|
|
21
22
|
onOpenMobileChange,
|
|
23
|
+
onNarrowChange,
|
|
22
24
|
className,
|
|
23
25
|
style,
|
|
24
26
|
...props
|
|
@@ -40,6 +42,12 @@ export function AnimatedPanelProvider({
|
|
|
40
42
|
const generatedId = useId()
|
|
41
43
|
const leftTriggerRef = useRef<HTMLButtonElement>(null)
|
|
42
44
|
const rightTriggerRef = useRef<HTMLButtonElement>(null)
|
|
45
|
+
const wrapperRef = useRef<HTMLDivElement>(null)
|
|
46
|
+
// The narrow gate below is consulted by the setters, but the verdict comes
|
|
47
|
+
// from `useShellFit` further down — which needs those very setters to retire
|
|
48
|
+
// the panel. A ref unwinds the cycle: a setter only ever runs from an event,
|
|
49
|
+
// by which time the layout effect has published the current verdict.
|
|
50
|
+
const fitsRef = useRef(true)
|
|
43
51
|
|
|
44
52
|
// Per-side controlled/uncontrolled merge — a side is controlled exactly
|
|
45
53
|
// when its key is present on the controlled record.
|
|
@@ -63,8 +71,14 @@ export function AnimatedPanelProvider({
|
|
|
63
71
|
[controlledMobileLeft, controlledMobileRight, internalOpenMobile]
|
|
64
72
|
)
|
|
65
73
|
|
|
74
|
+
// The right panel is not on screen while the shell is too narrow to hold
|
|
75
|
+
// it, so opening it would only ever surface later as a panel nobody asked
|
|
76
|
+
// for. Closing always goes through — that is how state left over from a
|
|
77
|
+
// wider viewport clears.
|
|
66
78
|
const setOpen = useCallback(
|
|
67
79
|
(nextOpen: boolean, side: SidebarSide) => {
|
|
80
|
+
if (nextOpen && side === "right" && !fitsRef.current) return
|
|
81
|
+
|
|
68
82
|
const controlled = side === "left" ? controlledLeft : controlledRight
|
|
69
83
|
if (controlled === undefined) {
|
|
70
84
|
setInternalOpen((prev) =>
|
|
@@ -78,6 +92,8 @@ export function AnimatedPanelProvider({
|
|
|
78
92
|
|
|
79
93
|
const setOpenMobile = useCallback(
|
|
80
94
|
(nextOpen: boolean, side: SidebarSide) => {
|
|
95
|
+
if (nextOpen && side === "right" && !fitsRef.current) return
|
|
96
|
+
|
|
81
97
|
const controlled =
|
|
82
98
|
side === "left" ? controlledMobileLeft : controlledMobileRight
|
|
83
99
|
if (controlled === undefined) {
|
|
@@ -103,6 +119,58 @@ export function AnimatedPanelProvider({
|
|
|
103
119
|
[]
|
|
104
120
|
)
|
|
105
121
|
|
|
122
|
+
// A measurement that lands on "no room" retires the right panel's open
|
|
123
|
+
// state instead of parking it: a panel that survived the crossing would
|
|
124
|
+
// spring back the moment the viewport widened, long after the user asked
|
|
125
|
+
// for it. Closing is the one write the narrow gate lets through, and the
|
|
126
|
+
// guards keep a repeated verdict from re-reporting a panel already shut.
|
|
127
|
+
//
|
|
128
|
+
// It rides a ref so `useShellFit` can call the latest one from a listener
|
|
129
|
+
// it bound once, and it is a layout effect declared FIRST so the ref is
|
|
130
|
+
// filled before that listener takes its opening measurement.
|
|
131
|
+
const retireRightPanelRef = useRef<() => void>(() => {})
|
|
132
|
+
useIsomorphicLayoutEffect(() => {
|
|
133
|
+
retireRightPanelRef.current = () => {
|
|
134
|
+
if (openState.right) setOpen(false, "right")
|
|
135
|
+
if (openMobileState.right) setOpenMobile(false, "right")
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
const retireRightPanel = useCallback(() => retireRightPanelRef.current(), [])
|
|
139
|
+
|
|
140
|
+
// Everything about the shell's columns — whether a secondary panel fits,
|
|
141
|
+
// how wide it is, how far it may be dragged — lives in this one hook, and
|
|
142
|
+
// in the shell-fit atoms it writes to under `shellId`. The rail's fold is an
|
|
143
|
+
// input because its column is part of the room the panel needs.
|
|
144
|
+
const fit = useShellFit({
|
|
145
|
+
shellId,
|
|
146
|
+
hostRef: wrapperRef,
|
|
147
|
+
railOpen: openState.left,
|
|
148
|
+
defaultPanelWidth,
|
|
149
|
+
onUnfit: retireRightPanel,
|
|
150
|
+
})
|
|
151
|
+
const isNarrow = !fit.fits
|
|
152
|
+
useIsomorphicLayoutEffect(() => {
|
|
153
|
+
fitsRef.current = fit.fits
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
// The verdict has to travel up to whoever renders the provider —
|
|
157
|
+
// useShellPanels, a title bar — since context only flows down. The
|
|
158
|
+
// listener above is bound once, so the callback rides a ref rather than
|
|
159
|
+
// rebinding it whenever a consumer passes a fresh arrow.
|
|
160
|
+
const onNarrowChangeRef = useRef(onNarrowChange)
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
onNarrowChangeRef.current = onNarrowChange
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
// Only real crossings are news: a resize that leaves the verdict alone is
|
|
166
|
+
// silent, and so is the mount default the consumer already holds.
|
|
167
|
+
const reportedNarrowRef = useRef(false)
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (reportedNarrowRef.current === isNarrow) return
|
|
170
|
+
reportedNarrowRef.current = isNarrow
|
|
171
|
+
onNarrowChangeRef.current?.(isNarrow)
|
|
172
|
+
}, [isNarrow])
|
|
173
|
+
|
|
106
174
|
useEffect(() => {
|
|
107
175
|
const handleShortcut = (event: KeyboardEvent) => {
|
|
108
176
|
if (
|
|
@@ -122,6 +190,7 @@ export function AnimatedPanelProvider({
|
|
|
122
190
|
<AnimatedSidebarContext.Provider
|
|
123
191
|
value={{
|
|
124
192
|
isMobile,
|
|
193
|
+
fit,
|
|
125
194
|
layoutId: `${generatedId}-active`,
|
|
126
195
|
open: openState,
|
|
127
196
|
openMobile: openMobileState,
|
|
@@ -134,15 +203,12 @@ export function AnimatedPanelProvider({
|
|
|
134
203
|
>
|
|
135
204
|
<div
|
|
136
205
|
{...props}
|
|
206
|
+
ref={wrapperRef}
|
|
137
207
|
data-slot="sidebar-wrapper"
|
|
208
|
+
data-narrow={isNarrow ? "" : undefined}
|
|
138
209
|
data-state-left={openState.left ? "expanded" : "collapsed"}
|
|
139
210
|
data-state-right={openState.right ? "expanded" : "collapsed"}
|
|
140
|
-
style={{
|
|
141
|
-
"--sidebar-width": "19rem",
|
|
142
|
-
"--sidebar-width-icon": "4.25rem",
|
|
143
|
-
"--sidebar-width-mobile": "18rem",
|
|
144
|
-
...style,
|
|
145
|
-
}}
|
|
211
|
+
style={{ ...SHELL_WIDTHS, ...style }}
|
|
146
212
|
className={cn(
|
|
147
213
|
"group/sidebar-wrapper flex w-full min-w-0 gap-x-2",
|
|
148
214
|
className
|
|
@@ -16,6 +16,10 @@ export const AnimatedPanelTrigger = forwardRef<
|
|
|
16
16
|
: context.open[side]
|
|
17
17
|
const triggerRef = context.triggerRefs[side]
|
|
18
18
|
|
|
19
|
+
// The right panel drops out of a viewport too narrow to hold it, so its
|
|
20
|
+
// toggle would otherwise sit in the header driving nothing.
|
|
21
|
+
if (side === "right" && !context.fit.fits) return null
|
|
22
|
+
|
|
19
23
|
return (
|
|
20
24
|
<Button
|
|
21
25
|
{...props}
|
|
@@ -1,18 +1,30 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
// beui.dev/components/motion/animated-sidebar
|
|
3
3
|
|
|
4
|
-
import { motion } from "motion/react"
|
|
4
|
+
import { motion, type PanInfo } from "motion/react"
|
|
5
|
+
import {
|
|
6
|
+
useCallback,
|
|
7
|
+
useEffect,
|
|
8
|
+
useRef,
|
|
9
|
+
useState,
|
|
10
|
+
type KeyboardEvent,
|
|
11
|
+
} from "react"
|
|
5
12
|
import { EASE_OUT } from "@/lib/ease.ts"
|
|
6
13
|
import { cn } from "@/lib/utils"
|
|
7
14
|
import type { AnimatedSidebarProps } from "./type"
|
|
8
15
|
import {
|
|
9
16
|
AnimatedSidebarPanelContext,
|
|
17
|
+
expandedWidthVar,
|
|
10
18
|
SIDEBAR_MORPH_TRANSITION,
|
|
11
19
|
PANEL_TRANSITION,
|
|
12
20
|
REDUCED_TRANSITION,
|
|
13
|
-
useAnimatedSidebar
|
|
21
|
+
useAnimatedSidebar,
|
|
14
22
|
} from "./utils"
|
|
15
23
|
|
|
24
|
+
/** How far one arrow key moves the panel edge (shift = coarse step). */
|
|
25
|
+
const RESIZE_STEP = 16
|
|
26
|
+
const RESIZE_STEP_COARSE = 64
|
|
27
|
+
|
|
16
28
|
export function AnimatedPanel({
|
|
17
29
|
side = "left",
|
|
18
30
|
variant = "sidebar",
|
|
@@ -26,21 +38,97 @@ export function AnimatedPanel({
|
|
|
26
38
|
...props
|
|
27
39
|
}: AnimatedSidebarProps) {
|
|
28
40
|
const context = useAnimatedSidebar()
|
|
41
|
+
const { bounds, panelWidth, resetPanelWidth, resizePanel } = context.fit
|
|
29
42
|
const sideOpen = context.open[side]
|
|
30
43
|
const collapsed = collapsible !== "none" && !sideOpen
|
|
31
44
|
const offcanvas = collapsed && collapsible === "offcanvas"
|
|
32
|
-
|
|
33
|
-
|
|
45
|
+
|
|
46
|
+
// Only the expanded secondary panel is resizable: the rail is pinned to its
|
|
47
|
+
// own columns, and a collapsed panel has no edge left to grab.
|
|
48
|
+
const resizable =
|
|
49
|
+
side === "right" && collapsible === "offcanvas" && !collapsed
|
|
50
|
+
|
|
51
|
+
const resizeStartRef = useRef(0)
|
|
52
|
+
const [resizing, setResizing] = useState(false)
|
|
53
|
+
|
|
54
|
+
const baseWidth = offcanvas
|
|
55
|
+
? 0
|
|
34
56
|
: collapsed
|
|
35
57
|
? "var(--sidebar-width-icon)"
|
|
36
|
-
:
|
|
58
|
+
: expandedWidthVar(side)
|
|
59
|
+
// The secondary panel's width is shell state, not panel state: the shell
|
|
60
|
+
// measured it, re-clamps it when the room changes, and hands back the same
|
|
61
|
+
// width after a collapse. Everything else rides the CSS variables, which
|
|
62
|
+
// also covers the server render, where nothing has been measured yet.
|
|
63
|
+
const width = resizable && panelWidth !== null ? panelWidth : baseWidth
|
|
64
|
+
|
|
65
|
+
const setPanelRef = useCallback(
|
|
66
|
+
(node: HTMLElement | null) => {
|
|
67
|
+
if (typeof ref === "function") ref(node)
|
|
68
|
+
else if (ref) ref.current = node
|
|
69
|
+
},
|
|
70
|
+
[ref]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
const handleResizeStart = useCallback(() => {
|
|
74
|
+
resizeStartRef.current = panelWidth ?? bounds.min
|
|
75
|
+
setResizing(true)
|
|
76
|
+
}, [bounds.min, panelWidth])
|
|
77
|
+
|
|
78
|
+
const handleResize = useCallback(
|
|
79
|
+
(_event: PointerEvent, info: PanInfo) => {
|
|
80
|
+
// The handle rides the panel's inner edge, so the panel grows as the
|
|
81
|
+
// pointer travels left. `offset` is measured from the pointer-down
|
|
82
|
+
// point, so the edge tracks it 1:1 instead of accumulating rounding.
|
|
83
|
+
resizePanel(resizeStartRef.current - info.offset.x)
|
|
84
|
+
},
|
|
85
|
+
[resizePanel]
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
const handleResizeEnd = useCallback(() => setResizing(false), [])
|
|
89
|
+
|
|
90
|
+
const handleResizeKeyDown = useCallback(
|
|
91
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
92
|
+
const direction =
|
|
93
|
+
event.key === "ArrowLeft" ? 1 : event.key === "ArrowRight" ? -1 : 0
|
|
94
|
+
if (!direction) return
|
|
95
|
+
|
|
96
|
+
event.preventDefault()
|
|
97
|
+
const step = event.shiftKey ? RESIZE_STEP_COARSE : RESIZE_STEP
|
|
98
|
+
resizePanel((panelWidth ?? bounds.min) + direction * step)
|
|
99
|
+
},
|
|
100
|
+
[bounds.min, panelWidth, resizePanel]
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// The pointer leaves the 2px handle the moment the drag starts, so the
|
|
104
|
+
// cursor and the text selection have to be held on the document instead.
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (!resizing) return
|
|
107
|
+
|
|
108
|
+
const { body } = document
|
|
109
|
+
const previousCursor = body.style.cursor
|
|
110
|
+
const previousUserSelect = body.style.userSelect
|
|
111
|
+
body.style.cursor = "col-resize"
|
|
112
|
+
body.style.userSelect = "none"
|
|
113
|
+
|
|
114
|
+
return () => {
|
|
115
|
+
body.style.cursor = previousCursor
|
|
116
|
+
body.style.userSelect = previousUserSelect
|
|
117
|
+
}
|
|
118
|
+
}, [resizing])
|
|
119
|
+
|
|
120
|
+
// A secondary panel exists only while the viewport can carry it beside the
|
|
121
|
+
// rail and the body at their own floors. Below that there is nothing to dock
|
|
122
|
+
// into, so the panel stands down rather than squeezing the content column —
|
|
123
|
+
// and its trigger stands down with it.
|
|
124
|
+
if (side === "right" && !context.fit.fits) return null
|
|
37
125
|
|
|
38
126
|
if (context.isMobile) return <div>TODO: Create Mobile nav experience</div>
|
|
39
127
|
|
|
40
128
|
return (
|
|
41
129
|
<motion.aside
|
|
42
130
|
{...props}
|
|
43
|
-
ref={
|
|
131
|
+
ref={setPanelRef}
|
|
44
132
|
initial={false}
|
|
45
133
|
aria-label={ariaLabel}
|
|
46
134
|
data-slot="sidebar"
|
|
@@ -48,6 +136,7 @@ export function AnimatedPanel({
|
|
|
48
136
|
data-collapsible={collapsible}
|
|
49
137
|
data-variant={variant}
|
|
50
138
|
data-side={side}
|
|
139
|
+
data-resizing={resizing ? "" : undefined}
|
|
51
140
|
animate={{
|
|
52
141
|
width,
|
|
53
142
|
// Swallow the shell's gap-x-2 column gap while off canvas so the
|
|
@@ -57,7 +146,11 @@ export function AnimatedPanel({
|
|
|
57
146
|
? { marginLeft: offcanvas ? "-0.5rem" : "0rem" }
|
|
58
147
|
: { marginRight: offcanvas ? "-0.5rem" : "0rem" }),
|
|
59
148
|
}}
|
|
60
|
-
|
|
149
|
+
// A drag has to land on the frame it happened — the morph spring would
|
|
150
|
+
// trail the pointer and keep settling after the handle is released.
|
|
151
|
+
transition={
|
|
152
|
+
resizing || context.reduce ? { duration: 0 } : SIDEBAR_MORPH_TRANSITION
|
|
153
|
+
}
|
|
61
154
|
style={style}
|
|
62
155
|
className={cn(
|
|
63
156
|
"group/sidebar relative hidden h-full shrink-0 will-change-[width] md:block",
|
|
@@ -66,14 +159,28 @@ export function AnimatedPanel({
|
|
|
66
159
|
className
|
|
67
160
|
)}
|
|
68
161
|
>
|
|
69
|
-
{
|
|
162
|
+
{resizable && (
|
|
70
163
|
// The handle occupies the shell's gap-x-2 column gap. It must stay
|
|
71
164
|
// outside the clipping mask below or the gap would never receive
|
|
72
165
|
// hover — clipping cuts hit-testing, not just painting.
|
|
73
166
|
<motion.div
|
|
74
|
-
|
|
167
|
+
role="separator"
|
|
168
|
+
aria-label={`Resize ${ariaLabel}`}
|
|
169
|
+
aria-orientation="vertical"
|
|
170
|
+
tabIndex={0}
|
|
171
|
+
className="absolute inset-y-0 -left-2 z-10 w-2 cursor-col-resize touch-none"
|
|
75
172
|
initial="idle"
|
|
173
|
+
// Hover alone would drop the glow the instant the drag leaves the
|
|
174
|
+
// handle, so the gesture pins it open for the whole drag.
|
|
175
|
+
animate={resizing ? "hover" : "idle"}
|
|
76
176
|
whileHover="hover"
|
|
177
|
+
whileFocus="hover"
|
|
178
|
+
onPanStart={handleResizeStart}
|
|
179
|
+
onPan={handleResize}
|
|
180
|
+
onPanEnd={handleResizeEnd}
|
|
181
|
+
onKeyDown={handleResizeKeyDown}
|
|
182
|
+
// The usual escape hatch out of a width you dragged to.
|
|
183
|
+
onDoubleClick={resetPanelWidth}
|
|
77
184
|
>
|
|
78
185
|
<motion.div
|
|
79
186
|
variants={{ idle: { scaleY: 0 }, hover: { scaleY: 1 } }}
|
|
@@ -86,30 +193,20 @@ export function AnimatedPanel({
|
|
|
86
193
|
/>
|
|
87
194
|
</motion.div>
|
|
88
195
|
)}
|
|
89
|
-
{/* The
|
|
90
|
-
|
|
91
|
-
clipping the aside itself — the aside must stay
|
|
92
|
-
gap handle above to be hoverable. */}
|
|
93
|
-
<div
|
|
94
|
-
className={
|
|
95
|
-
collapsible === "offcanvas"
|
|
96
|
-
? "absolute inset-0 overflow-hidden rounded-[inherit]"
|
|
97
|
-
: "contents"
|
|
98
|
-
}
|
|
99
|
-
>
|
|
196
|
+
{/* The aside's own width animation is the whole layout story: the
|
|
197
|
+
inner panel just fills it and fades, so it is masked to the aside's
|
|
198
|
+
box here instead of clipping the aside itself — the aside must stay
|
|
199
|
+
unclipped for the gap handle above to be hoverable. */}
|
|
200
|
+
<div className={"contents"}>
|
|
100
201
|
<motion.div
|
|
101
202
|
initial={false}
|
|
102
|
-
animate={{
|
|
103
|
-
opacity: offcanvas ? 0 : 1,
|
|
104
|
-
x: offcanvas ? (side === "left" ? "-100%" : "100%") : "0%",
|
|
105
|
-
}}
|
|
203
|
+
animate={{ opacity: offcanvas ? 0 : 1 }}
|
|
106
204
|
transition={context.reduce ? REDUCED_TRANSITION : PANEL_TRANSITION}
|
|
107
205
|
className={cn(
|
|
108
|
-
"sticky top-0 flex flex-col overflow-hidden",
|
|
109
|
-
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// detached strip.
|
|
206
|
+
"sticky top-0 flex w-full flex-col overflow-hidden",
|
|
207
|
+
// Pin the right panel to the aside's trailing edge so the aside's
|
|
208
|
+
// width animation expands the panel out of it instead of
|
|
209
|
+
// revealing a detached strip.
|
|
113
210
|
side === "right" && "ml-auto",
|
|
114
211
|
|
|
115
212
|
// Panels fill the shell's container, not the viewport — the shell
|
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
import { ShellLayout } from "./shell-layout"
|
|
2
2
|
|
|
3
3
|
export type * from "./type"
|
|
4
|
+
export * from "./shell-metrics"
|
|
4
5
|
export * from "./use-shell-panels"
|
|
6
|
+
export { useShellFit } from "./use-shell-fit"
|
|
7
|
+
export { useShellFitActions, useShellFitState } from "./use-shell-fit-state"
|
|
8
|
+
// The public atom surface. `@internal` atoms (measure, mount) stay unexported
|
|
9
|
+
// on purpose — `export *` would make every internal a breaking-change surface
|
|
10
|
+
// for consumer apps.
|
|
11
|
+
export {
|
|
12
|
+
removeShellFitInstance,
|
|
13
|
+
resetShellPanelWidthAtom,
|
|
14
|
+
resizeShellPanelAtom,
|
|
15
|
+
shellFitFitsAtom,
|
|
16
|
+
shellFitMeasuredAtom,
|
|
17
|
+
shellFitMetricsAtom,
|
|
18
|
+
shellFitPanelBoundsAtom,
|
|
19
|
+
shellFitPanelWidthAtom,
|
|
20
|
+
shellFitRequestedWidthAtom,
|
|
21
|
+
shellFitStateAtom,
|
|
22
|
+
} from "./shell-fit-atom"
|
|
5
23
|
export * from "./utils"
|
|
6
24
|
|
|
7
25
|
export * from "./animated-panel"
|
|
@@ -9,17 +27,4 @@ export * from "./animated-panel-inset"
|
|
|
9
27
|
export * from "./animated-panel-provider"
|
|
10
28
|
export * from "./animated-panel-trigger"
|
|
11
29
|
|
|
12
|
-
export * from "./animated-sidebar-header"
|
|
13
|
-
export * from "./animated-sidebar-content"
|
|
14
|
-
export * from "./animated-sidebar-footer"
|
|
15
|
-
export * from "./animated-sidebar-group"
|
|
16
|
-
export * from "./animated-sidebar-group-label"
|
|
17
|
-
export * from "./animated-sidebar-group-content"
|
|
18
|
-
export * from "./animated-sidebar-menu"
|
|
19
|
-
export * from "./animated-sidebar-menu-item"
|
|
20
|
-
export * from "./animated-sidebar-menu-sub"
|
|
21
|
-
export * from "./animated-sidebar-menu-sub-item"
|
|
22
|
-
export * from "./animated-sidebar-menu-sub-button"
|
|
23
|
-
export * from "./animated-sidebar-menu-button"
|
|
24
|
-
|
|
25
30
|
export { ShellLayout as default }
|