@exegia/corpora-ui 0.19.0 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/__tests__/use-ai-sidebar.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/ai-sidebar.d.ts +11 -2
- 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 +191 -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 +27 -31
- package/dist-lib/components/blocks/nav/sidebar/utils.d.ts +6 -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/scaffold/constants.d.ts +5 -0
- package/dist-lib/components/blocks/scaffold/scaffold-canvas.d.ts +2 -0
- package/dist-lib/components/blocks/scaffold/scaffold-panel.d.ts +1 -1
- package/dist-lib/components/blocks/scaffold/scaffold-tab.d.ts +6 -1
- package/dist-lib/components/blocks/scaffold/type.d.ts +28 -0
- package/dist-lib/components/blocks/scaffold/use-panel-visibility.d.ts +19 -0
- package/dist-lib/components/blocks/scaffold/utils.d.ts +6 -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/__tests__/tree.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/__tests__/use-tree.test.d.ts +1 -0
- package/dist-lib/components/composed/tree/constants.d.ts +35 -0
- package/dist-lib/components/composed/tree/index.d.ts +6 -0
- package/dist-lib/components/composed/tree/tree-atom.d.ts +160 -0
- package/dist-lib/components/composed/tree/tree-context.d.ts +4 -0
- package/dist-lib/components/composed/tree/tree-node.d.ts +12 -0
- package/dist-lib/components/composed/tree/tree.d.ts +19 -0
- package/dist-lib/components/composed/tree/type.d.ts +290 -0
- package/dist-lib/components/composed/tree/use-tree-dnd.d.ts +18 -0
- package/dist-lib/components/composed/tree/use-tree-state.d.ts +25 -0
- package/dist-lib/components/composed/tree/use-tree.d.ts +24 -0
- package/dist-lib/components/composed/tree/utils.d.ts +28 -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/audio-wave.d.ts +2 -0
- package/dist-lib/components/user-avatar/component.d.ts +3 -0
- package/dist-lib/components/user-avatar/fallback.d.ts +5 -0
- package/dist-lib/components/user-avatar/index.d.ts +18 -0
- package/dist-lib/components/user-avatar/presence-badge.d.ts +16 -0
- package/dist-lib/components/user-avatar/type.d.ts +71 -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/components/user-avatar/utils.d.ts +2 -0
- package/dist-lib/index.d.ts +6 -2
- package/dist-lib/index.js +3576 -1366
- 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 +16 -12
- package/src/components/beste/piece/browser-frame.tsx +9 -6
- 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/__tests__/use-ai-sidebar.test.tsx +310 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar-atom.ts +698 -0
- package/src/components/blocks/nav/sidebar/ai-sidebar.tsx +112 -76
- 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 +228 -29
- package/src/components/blocks/nav/sidebar/use-ai-sidebar-state.ts +123 -0
- package/src/components/blocks/nav/sidebar/use-ai-sidebar.ts +455 -225
- package/src/components/blocks/nav/sidebar/utils.ts +29 -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/scaffold/__tests__/scaffold.test.tsx +160 -1
- package/src/components/blocks/scaffold/constants.ts +7 -0
- package/src/components/blocks/scaffold/panel-menu-button.tsx +3 -0
- package/src/components/blocks/scaffold/scaffold-canvas.tsx +48 -5
- package/src/components/blocks/scaffold/scaffold-inspector.tsx +3 -2
- package/src/components/blocks/scaffold/scaffold-main.tsx +1 -1
- package/src/components/blocks/scaffold/scaffold-panel.tsx +41 -5
- package/src/components/blocks/scaffold/scaffold-root.tsx +18 -3
- package/src/components/blocks/scaffold/scaffold-sidebar.tsx +1 -1
- package/src/components/blocks/scaffold/scaffold-tab.tsx +45 -7
- package/src/components/blocks/scaffold/type.ts +28 -0
- package/src/components/blocks/scaffold/use-panel-visibility.ts +180 -0
- package/src/components/blocks/scaffold/utils.ts +19 -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 +525 -0
- package/src/components/composed/tree/__tests__/use-tree.test.tsx +333 -0
- package/src/components/composed/tree/constants.ts +60 -0
- package/src/components/composed/tree/index.ts +51 -0
- package/src/components/composed/tree/tree-atom.ts +590 -0
- package/src/components/composed/tree/tree-context.ts +13 -0
- package/src/components/composed/tree/tree-node.tsx +490 -0
- package/src/components/composed/tree/tree.tsx +286 -0
- package/src/components/composed/tree/type.ts +322 -0
- package/src/components/composed/tree/use-tree-dnd.ts +179 -0
- package/src/components/composed/tree/use-tree-state.ts +105 -0
- package/src/components/composed/tree/use-tree.ts +307 -0
- package/src/components/composed/tree/utils.ts +172 -0
- package/src/components/composed/user-avatar.tsx +11 -96
- package/src/components/docs/component-preview.tsx +11 -11
- package/src/components/user-avatar/__tests__/user-avatar.test.tsx +245 -0
- package/src/components/user-avatar/audio-wave.tsx +22 -0
- package/src/components/user-avatar/component.tsx +152 -0
- package/src/components/user-avatar/fallback.tsx +26 -0
- package/src/components/user-avatar/index.ts +34 -0
- package/src/components/user-avatar/presence-badge.tsx +56 -0
- package/src/components/user-avatar/type.ts +85 -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/components/user-avatar/utils.ts +8 -0
- package/src/index.css +211 -153
- package/src/index.ts +35 -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,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shell's layout arithmetic, as plain functions over px.
|
|
3
|
+
*
|
|
4
|
+
* Nothing here touches React or the DOM. This module IS the contract of who
|
|
5
|
+
* gets which column, which keeps the rule unit-testable on its own and lets
|
|
6
|
+
* the shell-fit atoms (`shellFitFitsAtom`, `shellFitPanelWidthAtom`) call it
|
|
7
|
+
* without restating it: `useShellFit` only supplies the measurements.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** The px the shell lays itself out with. Every field is measured from a CSS
|
|
11
|
+
* variable (see `SHELL_WIDTHS`), never hard-coded, so a consumer's override
|
|
12
|
+
* flows straight through the rule. */
|
|
13
|
+
export interface ShellMetrics {
|
|
14
|
+
/** What the left rail occupies right now — its expanded width, its icon
|
|
15
|
+
* width, or 0 when there is no rail at all (or it is off canvas). */
|
|
16
|
+
rail: number
|
|
17
|
+
/** The floor the body refuses to go below. */
|
|
18
|
+
insetMin: number
|
|
19
|
+
/** The secondary panel's floor, which is also the width it opens at. */
|
|
20
|
+
panelMin: number
|
|
21
|
+
/** The viewport all three columns share. */
|
|
22
|
+
viewport: number
|
|
23
|
+
/** px the shell's own frame eats before any column gets a share: its
|
|
24
|
+
* padding, plus the gap between columns.
|
|
25
|
+
*
|
|
26
|
+
* Deliberately absent from `fitsPanel` — that rule is stated against the raw
|
|
27
|
+
* viewport — but subtracted from the resize ceiling, where ignoring it lets
|
|
28
|
+
* a full-width drag push the row past the shell by exactly this much. */
|
|
29
|
+
chrome: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** px the shell needs before a secondary panel can exist: the rail as it
|
|
33
|
+
* stands, plus the body and the panel at their own floors. */
|
|
34
|
+
export function requiredWidth({ rail, insetMin, panelMin }: ShellMetrics) {
|
|
35
|
+
return rail + insetMin + panelMin
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Whether the shell can hold a secondary panel at all. Strictly `<`: a shell
|
|
40
|
+
* that fits its columns exactly has no room left to give one.
|
|
41
|
+
*
|
|
42
|
+
* An unmeasurable shell fails open. A server render, a `display: none` host
|
|
43
|
+
* or a test environment with no layout engine all report 0, and a panel must
|
|
44
|
+
* never disappear over a reading the layout could not produce.
|
|
45
|
+
*/
|
|
46
|
+
export function fitsPanel(metrics: ShellMetrics) {
|
|
47
|
+
if (metrics.viewport <= 0 || metrics.panelMin <= 0) return true
|
|
48
|
+
return requiredWidth(metrics) < metrics.viewport
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** How wide the secondary panel may be: never under its own floor, never past
|
|
52
|
+
* the slack the body holds above its floor. `max` never drops below `min`, so
|
|
53
|
+
* a shell that does not fit reports a degenerate range instead of an inverted
|
|
54
|
+
* one — `fitsPanel` is what hides the panel, not a negative bound. */
|
|
55
|
+
export function panelBounds(metrics: ShellMetrics) {
|
|
56
|
+
const { chrome, insetMin, panelMin, rail, viewport } = metrics
|
|
57
|
+
return {
|
|
58
|
+
min: panelMin,
|
|
59
|
+
max: Math.max(panelMin, viewport - chrome - rail - insetMin),
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function clampPanelWidth(width: number, metrics: ShellMetrics) {
|
|
64
|
+
const { min, max } = panelBounds(metrics)
|
|
65
|
+
return Math.min(Math.max(width, min), max)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Resizes fire per pointer move and per resize event, most of them landing
|
|
69
|
+
* on the same numbers — comparing fields keeps those from re-rendering the
|
|
70
|
+
* whole shell. */
|
|
71
|
+
export function metricsEqual(a: ShellMetrics, b: ShellMetrics) {
|
|
72
|
+
return (
|
|
73
|
+
a.rail === b.rail &&
|
|
74
|
+
a.insetMin === b.insetMin &&
|
|
75
|
+
a.panelMin === b.panelMin &&
|
|
76
|
+
a.viewport === b.viewport &&
|
|
77
|
+
a.chrome === b.chrome
|
|
78
|
+
)
|
|
79
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ReactNode } from "react"
|
|
1
|
+
import type { ReactNode, RefObject } from "react"
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
type ButtonHTMLAttributes,
|
|
@@ -7,6 +7,83 @@ import {
|
|
|
7
7
|
} from "react"
|
|
8
8
|
import type { HTMLMotionProps } from "motion/react"
|
|
9
9
|
|
|
10
|
+
import type { ShellMetrics } from "./shell-metrics"
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Shell fit — the shell's measurement of itself, kept in Jotai atoms keyed by
|
|
14
|
+
// `shellId` (see `shell-fit-atom.ts`). No atom imports here: this file stays
|
|
15
|
+
// dependency-free so the types can travel without the store.
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** The key a shell's fit state is filed under. Pass your own to reach it from
|
|
19
|
+
* anywhere (`useShellFitState("app-shell")`); leave it out and the provider
|
|
20
|
+
* generates one that dies with it. */
|
|
21
|
+
export type ShellFitInstanceId = string
|
|
22
|
+
|
|
23
|
+
/** The range a secondary-panel resize may land in, in px. */
|
|
24
|
+
export interface ShellFitPanelBounds {
|
|
25
|
+
min: number
|
|
26
|
+
max: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** What the shell measured of itself — readable by id through
|
|
30
|
+
* `useShellFitState`. */
|
|
31
|
+
export interface ShellFitState {
|
|
32
|
+
/** The px the shell last measured, or null before its first measurement
|
|
33
|
+
* (a server render, a host with no layout). */
|
|
34
|
+
metrics: ShellMetrics | null
|
|
35
|
+
/** Whether the shell can hold a secondary panel. True while unmeasured, so
|
|
36
|
+
* a panel never flickers out over a reading that has not happened yet. */
|
|
37
|
+
fits: boolean
|
|
38
|
+
/** The secondary panel's width in px, already clamped to `bounds` — null
|
|
39
|
+
* before the first measurement, where the panel falls back to
|
|
40
|
+
* `--panel-width`. */
|
|
41
|
+
panelWidth: number | null
|
|
42
|
+
/** The range a resize may land in. */
|
|
43
|
+
bounds: ShellFitPanelBounds
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Writes only — drive a shell's secondary panel by id through
|
|
47
|
+
* `useShellFitActions` without re-rendering when it moves. */
|
|
48
|
+
export interface ShellFitActions {
|
|
49
|
+
/** Resize the secondary panel. Clamped on the way in, so a drag past the
|
|
50
|
+
* gutter parks at the bound instead of banking travel it has to give back. */
|
|
51
|
+
resizePanel: (width: number) => void
|
|
52
|
+
/** Back to the width the shell mounted with — `defaultPanelWidth`, or
|
|
53
|
+
* `--panel-width` when there was none. */
|
|
54
|
+
resetPanelWidth: () => void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What `useShellFit` hands the provider: the state, the actions and the id
|
|
58
|
+
* they are filed under. */
|
|
59
|
+
export interface ShellFitController extends ShellFitState, ShellFitActions {
|
|
60
|
+
shellId: ShellFitInstanceId
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface UseShellFitOptions {
|
|
64
|
+
/** File the fit under this id so it can be read elsewhere and outlive the
|
|
65
|
+
* shell. Generated (and dropped on unmount) when omitted. */
|
|
66
|
+
shellId?: ShellFitInstanceId
|
|
67
|
+
/** The element the shell's CSS variables live on (the provider wrapper). */
|
|
68
|
+
hostRef: RefObject<HTMLElement | null>
|
|
69
|
+
/** Whether the left rail is expanded right now — a render input, so a fold
|
|
70
|
+
* counts the moment React commits it rather than a frame later when the
|
|
71
|
+
* width animation has moved. */
|
|
72
|
+
railOpen: boolean
|
|
73
|
+
/** The width the secondary panel opens at, in px, instead of
|
|
74
|
+
* `--panel-width`. Read once, on mount. */
|
|
75
|
+
defaultPanelWidth?: number
|
|
76
|
+
/** Fired whenever a measurement lands on "no room": the shell uses it to
|
|
77
|
+
* retire the panel's open state instead of parking it. */
|
|
78
|
+
onUnfit?: () => void
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** @internal What an instance starts from, restored by `resetShellPanelWidthAtom`. */
|
|
82
|
+
export interface ShellFitSeed {
|
|
83
|
+
/** The requested panel width at mount — null for `--panel-width`. */
|
|
84
|
+
panelWidth: number | null
|
|
85
|
+
}
|
|
86
|
+
|
|
10
87
|
export interface ShellAction {
|
|
11
88
|
id: string
|
|
12
89
|
label: string
|
|
@@ -60,14 +137,24 @@ export type ShellPanelControlProps = Pick<
|
|
|
60
137
|
| "openMobile"
|
|
61
138
|
| "defaultOpenMobile"
|
|
62
139
|
| "onOpenMobileChange"
|
|
140
|
+
| "shellId"
|
|
141
|
+
| "defaultPanelWidth"
|
|
63
142
|
>
|
|
64
143
|
|
|
65
144
|
export interface UseShellPanelsOptions {
|
|
145
|
+
/** The id the shell's fit state is filed under. Name it to read the same
|
|
146
|
+
* shell from elsewhere (`useShellFitState("app-shell")`) and to keep a
|
|
147
|
+
* dragged panel width across a route change; otherwise the hook generates
|
|
148
|
+
* one that dies with it. */
|
|
149
|
+
shellId?: ShellFitInstanceId
|
|
66
150
|
/** Initial desktop open state per side, merged over
|
|
67
151
|
* `{ left: true, right: false }`. */
|
|
68
152
|
defaultOpen?: AnimatedSidebarProviderProps["defaultOpen"]
|
|
69
153
|
/** Initial mobile overlay state per side — every side starts closed. */
|
|
70
154
|
defaultOpenMobile?: AnimatedSidebarProviderProps["defaultOpenMobile"]
|
|
155
|
+
/** The width the secondary panel opens at, in px, instead of
|
|
156
|
+
* `--panel-width`. */
|
|
157
|
+
defaultPanelWidth?: number
|
|
71
158
|
/** Panel change event returning both the next open state and the side of
|
|
72
159
|
* the panel the change comes from. Mobile overlay changes report the same
|
|
73
160
|
* side as their desktop counterpart. */
|
|
@@ -75,14 +162,32 @@ export interface UseShellPanelsOptions {
|
|
|
75
162
|
}
|
|
76
163
|
|
|
77
164
|
export interface ShellPanelControls {
|
|
165
|
+
/** The id the shell's fit state is filed under — hand it to
|
|
166
|
+
* `useShellFitState` / `useShellFitActions` anywhere below `ExegiaProvider`. */
|
|
167
|
+
shellId: ShellFitInstanceId
|
|
168
|
+
/** The viewport cannot hold the secondary panel beside the rail and the
|
|
169
|
+
* body at their floors, so the shell has dropped the right panel and its
|
|
170
|
+
* trigger — UI outside the shell should stand down with them. Read straight
|
|
171
|
+
* out of the shell's fit atoms; it is only ever `true` once the shell is
|
|
172
|
+
* mounted and measured. */
|
|
173
|
+
isNarrow: boolean
|
|
174
|
+
/** The secondary panel's current width in px, or null until the shell has
|
|
175
|
+
* measured itself (the panel then sits at `--panel-width`). */
|
|
176
|
+
panelWidth: number | null
|
|
177
|
+
/** Resize the secondary panel from outside the shell — clamped to the room
|
|
178
|
+
* the shell has. */
|
|
179
|
+
resizePanel: (width: number) => void
|
|
78
180
|
/** Live desktop open state, keyed by side. */
|
|
79
181
|
open: Record<SidebarSide, boolean>
|
|
80
182
|
/** Live mobile overlay state, keyed by side. */
|
|
81
183
|
openMobile: Record<SidebarSide, boolean>
|
|
184
|
+
/** Refuses to OPEN the right panel while `isNarrow` — there is nothing on
|
|
185
|
+
* screen to open. Closing it always goes through. */
|
|
82
186
|
setOpen: (open: boolean, side: SidebarSide) => void
|
|
83
187
|
setOpenMobile: (open: boolean, side: SidebarSide) => void
|
|
84
188
|
/** Desktop-only convenience — the in-shell triggers already pick the
|
|
85
|
-
* mobile state themselves when the viewport is narrow.
|
|
189
|
+
* mobile state themselves when the viewport is narrow. Carries the same
|
|
190
|
+
* `isNarrow` refusal as `setOpen`. */
|
|
86
191
|
toggle: (side: SidebarSide) => void
|
|
87
192
|
/** Spread onto ShellLayout (or AnimatedPanelProvider directly). */
|
|
88
193
|
providerProps: ShellPanelControlProps
|
|
@@ -98,7 +203,6 @@ export interface ShellLayoutProps extends ShellPanelControlProps {
|
|
|
98
203
|
variant?: "web" | "desktop"
|
|
99
204
|
}
|
|
100
205
|
|
|
101
|
-
|
|
102
206
|
export type SidebarSide = "left" | "right"
|
|
103
207
|
/** Open flags keyed by the side. Sides left out stay uncontrolled / at their
|
|
104
208
|
* default — there is no per-side prop, the record IS the API. */
|
|
@@ -108,6 +212,10 @@ export type SidebarCollapsible = "offcanvas" | "icon" | "none"
|
|
|
108
212
|
|
|
109
213
|
export interface AnimatedSidebarContextValue {
|
|
110
214
|
isMobile: boolean
|
|
215
|
+
/** What the shell measured of itself: whether it can hold a secondary panel
|
|
216
|
+
* at all, how wide that panel is, and the range a resize may land in. The
|
|
217
|
+
* right panel and its trigger stand down when `fit.fits` is false. */
|
|
218
|
+
fit: ShellFitController
|
|
111
219
|
layoutId: string
|
|
112
220
|
/** Desktop open state, keyed by side. */
|
|
113
221
|
open: Record<SidebarSide, boolean>
|
|
@@ -122,6 +230,14 @@ export interface AnimatedSidebarContextValue {
|
|
|
122
230
|
}
|
|
123
231
|
|
|
124
232
|
export interface AnimatedSidebarProviderProps extends HTMLAttributes<HTMLDivElement> {
|
|
233
|
+
/** The id this shell's fit state (whether a secondary panel fits, how wide
|
|
234
|
+
* it is) is filed under in the store. Name it to read or drive the shell
|
|
235
|
+
* from elsewhere and to keep a dragged width across a route change; omit it
|
|
236
|
+
* and the provider generates one that is dropped on unmount. */
|
|
237
|
+
shellId?: ShellFitInstanceId
|
|
238
|
+
/** The width the secondary panel opens at, in px, instead of
|
|
239
|
+
* `--panel-width`. Read once, on mount. */
|
|
240
|
+
defaultPanelWidth?: number
|
|
125
241
|
/** Controlled desktop open state, keyed by the side. A side left undefined
|
|
126
242
|
* stays uncontrolled. */
|
|
127
243
|
open?: SidebarOpenState
|
|
@@ -133,13 +249,24 @@ export interface AnimatedSidebarProviderProps extends HTMLAttributes<HTMLDivElem
|
|
|
133
249
|
/** Initial mobile overlay state — every side starts closed. */
|
|
134
250
|
defaultOpenMobile?: SidebarOpenState
|
|
135
251
|
onOpenMobileChange?: (open: boolean, side: SidebarSide) => void
|
|
252
|
+
/** Fires when the shell crosses the width a secondary panel needs (rail +
|
|
253
|
+
* body + panel at their floors). An imperative escape hatch for a consumer
|
|
254
|
+
* that mounts the provider on its own; anything under `ExegiaProvider` can
|
|
255
|
+
* subscribe by id instead with `useShellFitState(shellId).fits`. */
|
|
256
|
+
onNarrowChange?: (isNarrow: boolean) => void
|
|
136
257
|
style?: SidebarProviderStyle
|
|
137
258
|
}
|
|
138
259
|
|
|
139
260
|
export type SidebarProviderStyle = CSSProperties & {
|
|
261
|
+
/** Left rail, expanded. */
|
|
140
262
|
"--sidebar-width"?: string
|
|
263
|
+
/** Left rail, folded to icons. */
|
|
141
264
|
"--sidebar-width-icon"?: string
|
|
142
265
|
"--sidebar-width-mobile"?: string
|
|
266
|
+
/** The secondary panel's floor, and the width it opens at. */
|
|
267
|
+
"--panel-width"?: string
|
|
268
|
+
/** The body's floor — the secondary panel may never squeeze it past this. */
|
|
269
|
+
"--inset-min-width"?: string
|
|
143
270
|
}
|
|
144
271
|
|
|
145
272
|
export type AnimatedSidebarInsetProps = HTMLMotionProps<"main">
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useMemo } from "react"
|
|
4
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
resetShellPanelWidthAtom,
|
|
8
|
+
resizeShellPanelAtom,
|
|
9
|
+
shellFitStateAtom,
|
|
10
|
+
} from "./shell-fit-atom"
|
|
11
|
+
import type { ShellFitActions, ShellFitInstanceId, ShellFitState } from "./type"
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read the fit of the shell registered under `shellId` from anywhere below
|
|
15
|
+
* `ExegiaProvider` — no controller, no props, no provider of its own.
|
|
16
|
+
*
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const { fits, panelWidth } = useShellFitState("app-shell")
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* This returns the whole state object, so the caller re-renders on every
|
|
22
|
+
* measurement. A component that reads one field should subscribe to that
|
|
23
|
+
* field's atom instead: `useAtomValue(shellFitFitsAtom("app-shell"))`.
|
|
24
|
+
*/
|
|
25
|
+
export function useShellFitState(shellId: ShellFitInstanceId): ShellFitState {
|
|
26
|
+
return useAtomValue(shellFitStateAtom(shellId))
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Drive the secondary panel of the shell registered under `shellId` from
|
|
31
|
+
* anywhere. Writes only — the caller never re-renders when the shell moves,
|
|
32
|
+
* so this is what a command palette or a keyboard shortcut should reach for.
|
|
33
|
+
*
|
|
34
|
+
* ```tsx
|
|
35
|
+
* const shell = useShellFitActions("app-shell")
|
|
36
|
+
* <Button onClick={() => shell.resizePanel(480)}>Wide inspector</Button>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function useShellFitActions(
|
|
40
|
+
shellId: ShellFitInstanceId
|
|
41
|
+
): ShellFitActions {
|
|
42
|
+
const resizePanel = useSetAtom(resizeShellPanelAtom(shellId))
|
|
43
|
+
const resetPanelWidth = useSetAtom(resetShellPanelWidthAtom(shellId))
|
|
44
|
+
|
|
45
|
+
return useMemo(
|
|
46
|
+
() => ({ resizePanel, resetPanelWidth }),
|
|
47
|
+
[resizePanel, resetPanelWidth]
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useEffect, useId, useMemo, useRef, useState } from "react"
|
|
4
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
5
|
+
import { useIsomorphicLayoutEffect } from "motion/react"
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
measureShellFitAtom,
|
|
9
|
+
mountShellFitAtom,
|
|
10
|
+
removeShellFitInstance,
|
|
11
|
+
resetShellPanelWidthAtom,
|
|
12
|
+
resizeShellPanelAtom,
|
|
13
|
+
shellFitStateAtom,
|
|
14
|
+
} from "./shell-fit-atom"
|
|
15
|
+
import { fitsPanel, type ShellMetrics } from "./shell-metrics"
|
|
16
|
+
import type {
|
|
17
|
+
ShellFitController,
|
|
18
|
+
ShellFitSeed,
|
|
19
|
+
UseShellFitOptions,
|
|
20
|
+
} from "./type"
|
|
21
|
+
import { resolveLength } from "./utils"
|
|
22
|
+
|
|
23
|
+
/** Read the shell's columns out of the DOM. The widths come from CSS
|
|
24
|
+
* variables; the rail's share comes from its own state, not its box, so a
|
|
25
|
+
* fold counts the moment React commits it rather than a frame later when the
|
|
26
|
+
* width animation has moved. */
|
|
27
|
+
function readMetrics(host: HTMLElement, railOpen: boolean): ShellMetrics {
|
|
28
|
+
// Whether a shell has a rail at all is the caller's composition, so it is
|
|
29
|
+
// read off the DOM rather than tracked in state.
|
|
30
|
+
const rail = host.querySelector<HTMLElement>(
|
|
31
|
+
'[data-slot="sidebar"][data-side="left"]'
|
|
32
|
+
)
|
|
33
|
+
const collapsible = rail?.dataset.collapsible
|
|
34
|
+
|
|
35
|
+
const frame = getComputedStyle(host)
|
|
36
|
+
const gap = Number.parseFloat(frame.columnGap) || 0
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
rail: !rail
|
|
40
|
+
? 0
|
|
41
|
+
: railOpen || collapsible === "none"
|
|
42
|
+
? resolveLength(host, "var(--sidebar-width)")
|
|
43
|
+
: collapsible === "offcanvas"
|
|
44
|
+
? 0
|
|
45
|
+
: resolveLength(host, "var(--sidebar-width-icon)"),
|
|
46
|
+
insetMin: resolveLength(host, "var(--inset-min-width)"),
|
|
47
|
+
panelMin: resolveLength(host, "var(--panel-width)"),
|
|
48
|
+
viewport: window.innerWidth,
|
|
49
|
+
chrome:
|
|
50
|
+
(Number.parseFloat(frame.paddingLeft) || 0) +
|
|
51
|
+
(Number.parseFloat(frame.paddingRight) || 0) +
|
|
52
|
+
gap * Math.max(0, host.children.length - 1),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Measures the shell and decides what the secondary panel may do: whether it
|
|
58
|
+
* exists at all, and how wide it may be dragged.
|
|
59
|
+
*
|
|
60
|
+
* The three things that move the answer are all observed here — the viewport
|
|
61
|
+
* (`resize`), the rail's fold (`railOpen`, a render input) and the user's own
|
|
62
|
+
* drag (`resizePanel`) — so no caller has to re-derive it. The measurement is
|
|
63
|
+
* the only thing this hook keeps to itself: the numbers land in the shell-fit
|
|
64
|
+
* atoms keyed by `shellId`, so anything under `ExegiaProvider` can read the
|
|
65
|
+
* verdict (`useShellFitState`) or move the panel (`useShellFitActions`)
|
|
66
|
+
* without holding this controller.
|
|
67
|
+
*/
|
|
68
|
+
export function useShellFit({
|
|
69
|
+
shellId: explicitId,
|
|
70
|
+
hostRef,
|
|
71
|
+
railOpen,
|
|
72
|
+
defaultPanelWidth,
|
|
73
|
+
onUnfit,
|
|
74
|
+
}: UseShellFitOptions): ShellFitController {
|
|
75
|
+
// A generated key isolates unnamed shells from each other; an explicit
|
|
76
|
+
// `shellId` is the app's handle on this one.
|
|
77
|
+
const generatedId = useId()
|
|
78
|
+
const shellId = explicitId ?? generatedId
|
|
79
|
+
|
|
80
|
+
// Read once: `defaultPanelWidth` describes the mount, not every render.
|
|
81
|
+
const [seed] = useState<ShellFitSeed>(() => ({
|
|
82
|
+
panelWidth: defaultPanelWidth ?? null,
|
|
83
|
+
}))
|
|
84
|
+
|
|
85
|
+
const mount = useSetAtom(mountShellFitAtom(shellId))
|
|
86
|
+
const measure = useSetAtom(measureShellFitAtom(shellId))
|
|
87
|
+
const resizePanel = useSetAtom(resizeShellPanelAtom(shellId))
|
|
88
|
+
const resetPanelWidth = useSetAtom(resetShellPanelWidthAtom(shellId))
|
|
89
|
+
|
|
90
|
+
// Before paint, and before the first measurement below, so a seeded width
|
|
91
|
+
// is in the store by the time the panel first reads its width.
|
|
92
|
+
useIsomorphicLayoutEffect(() => {
|
|
93
|
+
mount(seed)
|
|
94
|
+
}, [mount, seed])
|
|
95
|
+
|
|
96
|
+
// The listener below is bound once per fold, so the callback rides a ref
|
|
97
|
+
// rather than rebinding it whenever a consumer passes a fresh arrow.
|
|
98
|
+
const onUnfitRef = useRef(onUnfit)
|
|
99
|
+
useIsomorphicLayoutEffect(() => {
|
|
100
|
+
onUnfitRef.current = onUnfit
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
useIsomorphicLayoutEffect(() => {
|
|
104
|
+
const host = hostRef.current
|
|
105
|
+
if (!host) return
|
|
106
|
+
|
|
107
|
+
const takeMeasurement = () => {
|
|
108
|
+
const next = readMetrics(host, railOpen)
|
|
109
|
+
measure(next)
|
|
110
|
+
if (!fitsPanel(next)) onUnfitRef.current?.()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Measured before paint so a shell that cannot hold the panel never
|
|
114
|
+
// flashes one, and re-measured on every fold because the rail's column is
|
|
115
|
+
// part of the room the panel needs.
|
|
116
|
+
takeMeasurement()
|
|
117
|
+
window.addEventListener("resize", takeMeasurement)
|
|
118
|
+
return () => window.removeEventListener("resize", takeMeasurement)
|
|
119
|
+
}, [hostRef, railOpen, measure])
|
|
120
|
+
|
|
121
|
+
// A shell the hook keyed is scrap once its component goes. An explicit
|
|
122
|
+
// `shellId` is the app's key and outlives the mount — that is what lets a
|
|
123
|
+
// dragged width survive a route change.
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
if (explicitId !== undefined) return
|
|
126
|
+
return () => removeShellFitInstance(shellId)
|
|
127
|
+
}, [shellId, explicitId])
|
|
128
|
+
|
|
129
|
+
const state = useAtomValue(shellFitStateAtom(shellId))
|
|
130
|
+
|
|
131
|
+
return useMemo<ShellFitController>(
|
|
132
|
+
() => ({ shellId, ...state, resizePanel, resetPanelWidth }),
|
|
133
|
+
[shellId, state, resizePanel, resetPanelWidth]
|
|
134
|
+
)
|
|
135
|
+
}
|
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
|
-
import { useCallback, useMemo, useState } from "react"
|
|
3
|
+
import { useCallback, useEffect, useId, useMemo, useState } from "react"
|
|
4
|
+
import { useAtomValue, useSetAtom } from "jotai"
|
|
4
5
|
|
|
6
|
+
import {
|
|
7
|
+
removeShellFitInstance,
|
|
8
|
+
resizeShellPanelAtom,
|
|
9
|
+
shellFitFitsAtom,
|
|
10
|
+
shellFitPanelWidthAtom,
|
|
11
|
+
} from "./shell-fit-atom"
|
|
5
12
|
import type {
|
|
6
13
|
ShellPanelControlProps,
|
|
7
14
|
ShellPanelControls,
|
|
@@ -15,12 +22,36 @@ import type {
|
|
|
15
22
|
* callback. Spread the returned `providerProps` onto ShellLayout; the
|
|
16
23
|
* setters and `toggle` are for UI that lives outside the shell (title-bar
|
|
17
24
|
* buttons, command palette, shortcuts).
|
|
25
|
+
*
|
|
26
|
+
* The way back down is the store: `providerProps` carries a `shellId`, the
|
|
27
|
+
* shell files its measurement under it, and this hook reads `isNarrow` and
|
|
28
|
+
* `panelWidth` straight out of those atoms — so outside UI stands down with
|
|
29
|
+
* the panel instead of measuring `--sidebar-width` a second time. Name the
|
|
30
|
+
* `shellId` and any component below `ExegiaProvider` can do the same with
|
|
31
|
+
* `useShellFitState(shellId)`.
|
|
32
|
+
*
|
|
33
|
+
* Call it under the same `ExegiaProvider` as the shell it drives (or under
|
|
34
|
+
* none at all, on both sides): mounted above the provider it would read
|
|
35
|
+
* Jotai's default store while the shell writes to the provider's.
|
|
18
36
|
*/
|
|
19
37
|
export function useShellPanels({
|
|
38
|
+
shellId: explicitId,
|
|
20
39
|
defaultOpen,
|
|
21
40
|
defaultOpenMobile,
|
|
41
|
+
defaultPanelWidth,
|
|
22
42
|
onPanelChange,
|
|
23
43
|
}: UseShellPanelsOptions = {}): ShellPanelControls {
|
|
44
|
+
// The hook, not the provider, keys the shell: it renders above the provider
|
|
45
|
+
// and has to read the same atoms the provider writes. Whoever generates the
|
|
46
|
+
// key drops it — the provider treats a passed-in id as the app's and leaves
|
|
47
|
+
// it alone on unmount.
|
|
48
|
+
const generatedId = useId()
|
|
49
|
+
const shellId = explicitId ?? generatedId
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
if (explicitId !== undefined) return
|
|
52
|
+
return () => removeShellFitInstance(shellId)
|
|
53
|
+
}, [shellId, explicitId])
|
|
54
|
+
|
|
24
55
|
const [open, setOpenState] = useState<Record<SidebarSide, boolean>>(() => ({
|
|
25
56
|
left: defaultOpen?.left ?? true,
|
|
26
57
|
right: defaultOpen?.right ?? false,
|
|
@@ -32,24 +63,40 @@ export function useShellPanels({
|
|
|
32
63
|
right: defaultOpenMobile?.right ?? false,
|
|
33
64
|
}))
|
|
34
65
|
|
|
66
|
+
// Reads false until the shell has measured itself — an unmeasured shell
|
|
67
|
+
// fails open, so nothing out here stands down over a reading that has not
|
|
68
|
+
// happened yet.
|
|
69
|
+
const fits = useAtomValue(shellFitFitsAtom(shellId))
|
|
70
|
+
const isNarrow = !fits
|
|
71
|
+
const panelWidth = useAtomValue(shellFitPanelWidthAtom(shellId))
|
|
72
|
+
const resizePanel = useSetAtom(resizeShellPanelAtom(shellId))
|
|
73
|
+
|
|
74
|
+
// The shell drops the right panel when the viewport cannot hold it, so
|
|
75
|
+
// opening it from out here would only surface later as a panel nobody
|
|
76
|
+
// asked for. Closing always goes through — that is how state left over
|
|
77
|
+
// from a wider viewport clears.
|
|
35
78
|
const setOpen = useCallback(
|
|
36
79
|
(nextOpen: boolean, side: SidebarSide) => {
|
|
80
|
+
if (nextOpen && side === "right" && isNarrow) return
|
|
81
|
+
|
|
37
82
|
setOpenState((prev) =>
|
|
38
83
|
prev[side] === nextOpen ? prev : { ...prev, [side]: nextOpen }
|
|
39
84
|
)
|
|
40
85
|
onPanelChange?.(nextOpen, side)
|
|
41
86
|
},
|
|
42
|
-
[onPanelChange]
|
|
87
|
+
[isNarrow, onPanelChange]
|
|
43
88
|
)
|
|
44
89
|
|
|
45
90
|
const setOpenMobile = useCallback(
|
|
46
91
|
(nextOpen: boolean, side: SidebarSide) => {
|
|
92
|
+
if (nextOpen && side === "right" && isNarrow) return
|
|
93
|
+
|
|
47
94
|
setOpenMobileState((prev) =>
|
|
48
95
|
prev[side] === nextOpen ? prev : { ...prev, [side]: nextOpen }
|
|
49
96
|
)
|
|
50
97
|
onPanelChange?.(nextOpen, side)
|
|
51
98
|
},
|
|
52
|
-
[onPanelChange]
|
|
99
|
+
[isNarrow, onPanelChange]
|
|
53
100
|
)
|
|
54
101
|
|
|
55
102
|
const toggle = useCallback(
|
|
@@ -59,15 +106,21 @@ export function useShellPanels({
|
|
|
59
106
|
|
|
60
107
|
const providerProps = useMemo<ShellPanelControlProps>(
|
|
61
108
|
() => ({
|
|
109
|
+
shellId,
|
|
110
|
+
defaultPanelWidth,
|
|
62
111
|
open,
|
|
63
112
|
onOpenChange: setOpen,
|
|
64
113
|
openMobile,
|
|
65
114
|
onOpenMobileChange: setOpenMobile,
|
|
66
115
|
}),
|
|
67
|
-
[open, openMobile, setOpen, setOpenMobile]
|
|
116
|
+
[shellId, defaultPanelWidth, open, openMobile, setOpen, setOpenMobile]
|
|
68
117
|
)
|
|
69
118
|
|
|
70
119
|
return {
|
|
120
|
+
shellId,
|
|
121
|
+
isNarrow,
|
|
122
|
+
panelWidth,
|
|
123
|
+
resizePanel,
|
|
71
124
|
open,
|
|
72
125
|
openMobile,
|
|
73
126
|
setOpen,
|
|
@@ -92,6 +92,49 @@ export const FOCUSABLE_SELECTOR = [
|
|
|
92
92
|
export const MOBILE_QUERY = "(max-width: 767px)"
|
|
93
93
|
export const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The shell's layout contract, in px. Every entry lands on the provider's
|
|
97
|
+
* wrapper as a CSS variable, so a consumer overrides a column by restyling it
|
|
98
|
+
* rather than by passing a prop:
|
|
99
|
+
*
|
|
100
|
+
* | variable | column |
|
|
101
|
+
* | ---------------------- | ----------------------------------------- |
|
|
102
|
+
* | `--sidebar-width` | left rail, expanded |
|
|
103
|
+
* | `--sidebar-width-icon` | left rail, folded to icons |
|
|
104
|
+
* | `--panel-width` | secondary panel: its floor AND its default |
|
|
105
|
+
* | `--inset-min-width` | the body's floor |
|
|
106
|
+
*
|
|
107
|
+
* These are only the defaults. Every measurement resolves the live variable,
|
|
108
|
+
* so an override wins over the value written here.
|
|
109
|
+
*/
|
|
110
|
+
export const SHELL_WIDTHS = {
|
|
111
|
+
"--sidebar-width": "256px",
|
|
112
|
+
"--sidebar-width-icon": "56px",
|
|
113
|
+
"--sidebar-width-mobile": "18rem",
|
|
114
|
+
"--panel-width": "320px",
|
|
115
|
+
"--inset-min-width": "360px",
|
|
116
|
+
} as const
|
|
117
|
+
|
|
118
|
+
/** The expanded width of a panel docked to `side`: the secondary panel opens
|
|
119
|
+
* at `--panel-width`, the primary rail at `--sidebar-width`. */
|
|
120
|
+
export function expandedWidthVar(side: "left" | "right") {
|
|
121
|
+
return side === "right" ? "var(--panel-width)" : "var(--sidebar-width)"
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Resolve a CSS length — `var()` included — to px inside `host`'s cascade.
|
|
125
|
+
* Custom properties inherit, so a throwaway probe mounted in `host` reads the
|
|
126
|
+
* very `--sidebar-width` the shell lays out with, including a value a
|
|
127
|
+
* consumer overrode on the provider. Returns 0 when `host` has no layout
|
|
128
|
+
* (server render, `display: none`), so callers must fail open on 0 rather
|
|
129
|
+
* than treat it as a real measurement. */
|
|
130
|
+
export function resolveLength(host: HTMLElement, value: string) {
|
|
131
|
+
const probe = document.createElement("div")
|
|
132
|
+
probe.style.cssText = `position:absolute;visibility:hidden;pointer-events:none;width:${value}`
|
|
133
|
+
host.appendChild(probe)
|
|
134
|
+
const px = probe.getBoundingClientRect().width
|
|
135
|
+
probe.remove()
|
|
136
|
+
return px
|
|
137
|
+
}
|
|
95
138
|
|
|
96
139
|
export const AnimatedSidebarContext =
|
|
97
140
|
createContext<AnimatedSidebarContextValue | null>(null)
|
|
@@ -131,13 +174,10 @@ export function useIsMobile() {
|
|
|
131
174
|
)
|
|
132
175
|
}
|
|
133
176
|
|
|
134
|
-
|
|
135
177
|
export function useAnimatedSidebarPanel() {
|
|
136
178
|
const context = useContext(AnimatedSidebarPanelContext)
|
|
137
179
|
if (!context) {
|
|
138
|
-
throw new Error(
|
|
139
|
-
"Animated Sidebar parts must be used inside AnimatedPanel."
|
|
140
|
-
)
|
|
180
|
+
throw new Error("Animated Sidebar parts must be used inside AnimatedPanel.")
|
|
141
181
|
}
|
|
142
182
|
return context
|
|
143
183
|
}
|