@kushagradhawan/kookie-ui 0.1.48 → 0.1.50
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/components.css +1094 -95
- package/dist/cjs/components/_internal/shell-bottom.d.ts +31 -0
- package/dist/cjs/components/_internal/shell-bottom.d.ts.map +1 -0
- package/dist/cjs/components/_internal/shell-bottom.js +2 -0
- package/dist/cjs/components/_internal/shell-bottom.js.map +7 -0
- package/dist/cjs/components/_internal/shell-handles.d.ts +7 -0
- package/dist/cjs/components/_internal/shell-handles.d.ts.map +1 -0
- package/dist/cjs/components/_internal/shell-handles.js +2 -0
- package/dist/cjs/components/_internal/shell-handles.js.map +7 -0
- package/dist/cjs/components/_internal/shell-inspector.d.ts +31 -0
- package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -0
- package/dist/cjs/components/_internal/shell-inspector.js +2 -0
- package/dist/cjs/components/_internal/shell-inspector.js.map +7 -0
- package/dist/cjs/components/_internal/shell-resize.d.ts +24 -0
- package/dist/cjs/components/_internal/shell-resize.d.ts.map +1 -0
- package/dist/cjs/components/_internal/shell-resize.js +2 -0
- package/dist/cjs/components/_internal/shell-resize.js.map +7 -0
- package/dist/cjs/components/_internal/shell-sidebar.d.ts +37 -0
- package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -0
- package/dist/cjs/components/_internal/shell-sidebar.js +2 -0
- package/dist/cjs/components/_internal/shell-sidebar.js.map +7 -0
- package/dist/cjs/components/schemas/index.d.ts +2 -0
- package/dist/cjs/components/schemas/index.d.ts.map +1 -1
- package/dist/cjs/components/schemas/index.js +1 -1
- package/dist/cjs/components/schemas/index.js.map +3 -3
- package/dist/cjs/components/schemas/shell.schema.d.ts +1025 -0
- package/dist/cjs/components/schemas/shell.schema.d.ts.map +1 -0
- package/dist/cjs/components/schemas/shell.schema.js +2 -0
- package/dist/cjs/components/schemas/shell.schema.js.map +7 -0
- package/dist/cjs/components/shell.context.d.ts +38 -0
- package/dist/cjs/components/shell.context.d.ts.map +1 -0
- package/dist/cjs/components/shell.context.js +2 -0
- package/dist/cjs/components/shell.context.js.map +7 -0
- package/dist/cjs/components/shell.d.ts +6 -68
- package/dist/cjs/components/shell.d.ts.map +1 -1
- package/dist/cjs/components/shell.hooks.d.ts +3 -0
- package/dist/cjs/components/shell.hooks.d.ts.map +1 -0
- package/dist/cjs/components/shell.hooks.js +2 -0
- package/dist/cjs/components/shell.hooks.js.map +7 -0
- package/dist/cjs/components/shell.js +1 -1
- package/dist/cjs/components/shell.js.map +3 -3
- package/dist/cjs/components/shell.types.d.ts +20 -0
- package/dist/cjs/components/shell.types.d.ts.map +1 -0
- package/dist/cjs/components/shell.types.js +2 -0
- package/dist/cjs/components/shell.types.js.map +7 -0
- package/dist/cjs/components/sidebar.d.ts +8 -2
- package/dist/cjs/components/sidebar.d.ts.map +1 -1
- package/dist/cjs/components/sidebar.js +1 -1
- package/dist/cjs/components/sidebar.js.map +3 -3
- package/dist/esm/components/_internal/shell-bottom.d.ts +31 -0
- package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -0
- package/dist/esm/components/_internal/shell-bottom.js +2 -0
- package/dist/esm/components/_internal/shell-bottom.js.map +7 -0
- package/dist/esm/components/_internal/shell-handles.d.ts +7 -0
- package/dist/esm/components/_internal/shell-handles.d.ts.map +1 -0
- package/dist/esm/components/_internal/shell-handles.js +2 -0
- package/dist/esm/components/_internal/shell-handles.js.map +7 -0
- package/dist/esm/components/_internal/shell-inspector.d.ts +31 -0
- package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -0
- package/dist/esm/components/_internal/shell-inspector.js +2 -0
- package/dist/esm/components/_internal/shell-inspector.js.map +7 -0
- package/dist/esm/components/_internal/shell-resize.d.ts +24 -0
- package/dist/esm/components/_internal/shell-resize.d.ts.map +1 -0
- package/dist/esm/components/_internal/shell-resize.js +2 -0
- package/dist/esm/components/_internal/shell-resize.js.map +7 -0
- package/dist/esm/components/_internal/shell-sidebar.d.ts +37 -0
- package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -0
- package/dist/esm/components/_internal/shell-sidebar.js +2 -0
- package/dist/esm/components/_internal/shell-sidebar.js.map +7 -0
- package/dist/esm/components/schemas/index.d.ts +2 -0
- package/dist/esm/components/schemas/index.d.ts.map +1 -1
- package/dist/esm/components/schemas/index.js +1 -1
- package/dist/esm/components/schemas/index.js.map +3 -3
- package/dist/esm/components/schemas/shell.schema.d.ts +1025 -0
- package/dist/esm/components/schemas/shell.schema.d.ts.map +1 -0
- package/dist/esm/components/schemas/shell.schema.js +2 -0
- package/dist/esm/components/schemas/shell.schema.js.map +7 -0
- package/dist/esm/components/shell.context.d.ts +38 -0
- package/dist/esm/components/shell.context.d.ts.map +1 -0
- package/dist/esm/components/shell.context.js +2 -0
- package/dist/esm/components/shell.context.js.map +7 -0
- package/dist/esm/components/shell.d.ts +6 -68
- package/dist/esm/components/shell.d.ts.map +1 -1
- package/dist/esm/components/shell.hooks.d.ts +3 -0
- package/dist/esm/components/shell.hooks.d.ts.map +1 -0
- package/dist/esm/components/shell.hooks.js +2 -0
- package/dist/esm/components/shell.hooks.js.map +7 -0
- package/dist/esm/components/shell.js +1 -1
- package/dist/esm/components/shell.js.map +3 -3
- package/dist/esm/components/shell.types.d.ts +20 -0
- package/dist/esm/components/shell.types.d.ts.map +1 -0
- package/dist/esm/components/shell.types.js +2 -0
- package/dist/esm/components/shell.types.js.map +7 -0
- package/dist/esm/components/sidebar.d.ts +8 -2
- package/dist/esm/components/sidebar.d.ts.map +1 -1
- package/dist/esm/components/sidebar.js +1 -1
- package/dist/esm/components/sidebar.js.map +3 -3
- package/layout/utilities.css +168 -84
- package/layout.css +168 -84
- package/package.json +2 -1
- package/schemas/base-button.json +1 -1
- package/schemas/button.json +1 -1
- package/schemas/icon-button.json +1 -1
- package/schemas/index.json +6 -6
- package/schemas/shell-bottom.json +168 -0
- package/schemas/shell-content.json +34 -0
- package/schemas/shell-handle.json +34 -0
- package/schemas/shell-header.json +42 -0
- package/schemas/shell-inspector.json +171 -0
- package/schemas/shell-panel.json +167 -0
- package/schemas/shell-rail.json +132 -0
- package/schemas/shell-root.json +54 -0
- package/schemas/shell-sidebar.json +182 -0
- package/schemas/shell-trigger.json +76 -0
- package/schemas/toggle-button.json +1 -1
- package/schemas/toggle-icon-button.json +1 -1
- package/src/components/_internal/base-menu.css +4 -5
- package/src/components/_internal/base-sidebar-menu.css +0 -1
- package/src/components/_internal/base-sidebar.css +7 -0
- package/src/components/_internal/shell-bottom.tsx +251 -0
- package/src/components/_internal/shell-handles.tsx +193 -0
- package/src/components/_internal/shell-inspector.tsx +242 -0
- package/src/components/_internal/shell-resize.tsx +30 -0
- package/src/components/_internal/shell-sidebar.tsx +370 -0
- package/src/components/schemas/index.ts +46 -0
- package/src/components/schemas/shell.schema.ts +403 -0
- package/src/components/shell.context.tsx +59 -0
- package/src/components/shell.css +33 -18
- package/src/components/shell.hooks.ts +31 -0
- package/src/components/shell.tsx +387 -1682
- package/src/components/shell.types.ts +27 -0
- package/src/components/sidebar.css +233 -33
- package/src/components/sidebar.tsx +248 -214
- package/src/styles/tokens/blur.css +2 -2
- package/src/styles/tokens/color.css +2 -2
- package/styles.css +1267 -181
- package/tokens/base.css +5 -2
- package/tokens.css +5 -2
- package/utilities.css +168 -84
package/src/components/shell.tsx
CHANGED
|
@@ -30,377 +30,28 @@ import classNames from 'classnames';
|
|
|
30
30
|
import * as Sheet from './sheet.js';
|
|
31
31
|
import { Inset } from './inset.js';
|
|
32
32
|
import { VisuallyHidden } from './visually-hidden.js';
|
|
33
|
+
import { useResponsivePresentation } from './shell.hooks.js';
|
|
34
|
+
import { PaneResizeContext } from './_internal/shell-resize.js';
|
|
35
|
+
import { PaneHandle, PanelHandle, SidebarHandle, InspectorHandle, BottomHandle } from './_internal/shell-handles.js';
|
|
36
|
+
import { Sidebar } from './_internal/shell-sidebar.js';
|
|
37
|
+
import { Bottom } from './_internal/shell-bottom.js';
|
|
38
|
+
import { Inspector } from './_internal/shell-inspector.js';
|
|
39
|
+
import type { PresentationValue, ResponsivePresentation, PaneMode, SidebarMode, ResponsiveMode, ResponsiveSidebarMode, PaneSizePersistence, Breakpoint, PaneTarget } from './shell.types.js';
|
|
40
|
+
import { BREAKPOINTS } from './shell.types.js';
|
|
41
|
+
import { ShellProvider, useShell } from './shell.context.js';
|
|
33
42
|
|
|
34
|
-
//
|
|
35
|
-
type PresentationValue = 'fixed' | 'overlay' | 'stacked';
|
|
36
|
-
type ResponsivePresentation =
|
|
37
|
-
| PresentationValue
|
|
38
|
-
| Partial<Record<'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', PresentationValue>>;
|
|
39
|
-
type PaneMode = 'expanded' | 'collapsed';
|
|
40
|
-
type SidebarMode = 'collapsed' | 'thin' | 'expanded';
|
|
41
|
-
type ResponsiveMode =
|
|
42
|
-
| PaneMode
|
|
43
|
-
| Partial<Record<'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', PaneMode>>;
|
|
44
|
-
|
|
45
|
-
// Sidebar responsive mode (includes 'thin')
|
|
46
|
-
type ResponsiveSidebarMode =
|
|
47
|
-
| SidebarMode
|
|
48
|
-
| Partial<Record<'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', SidebarMode>>;
|
|
49
|
-
|
|
50
|
-
// Persistence adapter for pane sizes
|
|
51
|
-
type PaneSizePersistence = {
|
|
52
|
-
load?: () => number | Promise<number | undefined> | undefined;
|
|
53
|
-
save?: (size: number) => void | Promise<void>;
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
// Breakpoint system
|
|
57
|
-
const BREAKPOINTS = {
|
|
58
|
-
xs: '(min-width: 520px)',
|
|
59
|
-
sm: '(min-width: 768px)',
|
|
60
|
-
md: '(min-width: 1024px)',
|
|
61
|
-
lg: '(min-width: 1280px)',
|
|
62
|
-
xl: '(min-width: 1640px)',
|
|
63
|
-
} as const;
|
|
64
|
-
|
|
65
|
-
type Breakpoint = 'initial' | keyof typeof BREAKPOINTS;
|
|
66
|
-
|
|
67
|
-
// Shell context
|
|
68
|
-
interface ShellContextValue {
|
|
69
|
-
// Pane states
|
|
70
|
-
leftMode: PaneMode;
|
|
71
|
-
setLeftMode: (mode: PaneMode) => void;
|
|
72
|
-
panelMode: PaneMode; // Panel state within left container
|
|
73
|
-
setPanelMode: (mode: PaneMode) => void;
|
|
74
|
-
sidebarMode: SidebarMode;
|
|
75
|
-
setSidebarMode: (mode: SidebarMode) => void;
|
|
76
|
-
inspectorMode: PaneMode;
|
|
77
|
-
setInspectorMode: (mode: PaneMode) => void;
|
|
78
|
-
bottomMode: PaneMode;
|
|
79
|
-
setBottomMode: (mode: PaneMode) => void;
|
|
80
|
-
|
|
81
|
-
// Peek state (layout-only, ephemeral)
|
|
82
|
-
peekTarget: PaneTarget | null;
|
|
83
|
-
setPeekTarget: (target: PaneTarget | null) => void;
|
|
84
|
-
peekPane: (target: PaneTarget) => void;
|
|
85
|
-
clearPeek: () => void;
|
|
86
|
-
|
|
87
|
-
// Composition detection
|
|
88
|
-
hasLeft: boolean;
|
|
89
|
-
setHasLeft: (has: boolean) => void;
|
|
90
|
-
hasSidebar: boolean;
|
|
91
|
-
setHasSidebar: (has: boolean) => void;
|
|
92
|
-
|
|
93
|
-
// Presentation resolution
|
|
94
|
-
currentBreakpoint: Breakpoint;
|
|
95
|
-
currentBreakpointReady: boolean;
|
|
96
|
-
leftResolvedPresentation?: PresentationValue;
|
|
97
|
-
|
|
98
|
-
// Actions
|
|
99
|
-
togglePane: (target: PaneTarget) => void;
|
|
100
|
-
expandPane: (target: PaneTarget) => void;
|
|
101
|
-
collapsePane: (target: PaneTarget) => void;
|
|
102
|
-
// Toggle customization
|
|
103
|
-
setSidebarToggleComputer?: (fn: (current: SidebarMode) => SidebarMode) => void;
|
|
104
|
-
// Dev-only hooks for presentation warnings
|
|
105
|
-
onLeftPres?: (p: PresentationValue) => void;
|
|
106
|
-
// Sizing info for overlay grouping
|
|
107
|
-
onLeftDefaults?: (size: number) => void;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const ShellContext = React.createContext<ShellContextValue | null>(null);
|
|
111
|
-
|
|
112
|
-
function useShell() {
|
|
113
|
-
const ctx = React.useContext(ShellContext);
|
|
114
|
-
if (!ctx) throw new Error('Shell components must be used within <Shell.Root>');
|
|
115
|
-
return ctx;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Pane resize context for composed Handles
|
|
119
|
-
interface PaneResizeContextValue {
|
|
120
|
-
containerRef: React.RefObject<HTMLDivElement | null>;
|
|
121
|
-
cssVarName: string;
|
|
122
|
-
minSize: number;
|
|
123
|
-
maxSize: number;
|
|
124
|
-
defaultSize: number;
|
|
125
|
-
orientation: 'vertical' | 'horizontal';
|
|
126
|
-
edge: 'start' | 'end';
|
|
127
|
-
computeNext: (clientPos: number, startClientPos: number, startSize: number) => number;
|
|
128
|
-
onResize?: (size: number) => void;
|
|
129
|
-
onResizeStart?: (size: number) => void;
|
|
130
|
-
onResizeEnd?: (size: number) => void;
|
|
131
|
-
// new features
|
|
132
|
-
target: PaneTarget;
|
|
133
|
-
collapsible: boolean;
|
|
134
|
-
snapPoints?: number[];
|
|
135
|
-
snapTolerance: number;
|
|
136
|
-
collapseThreshold?: number;
|
|
137
|
-
requestCollapse?: () => void;
|
|
138
|
-
requestToggle?: () => void;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const PaneResizeContext = React.createContext<PaneResizeContextValue | null>(null);
|
|
142
|
-
|
|
143
|
-
function usePaneResize() {
|
|
144
|
-
const ctx = React.useContext(PaneResizeContext);
|
|
145
|
-
if (!ctx) throw new Error('Shell.Handle must be used within a resizable pane');
|
|
146
|
-
return ctx;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const PaneHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
|
|
150
|
-
({ className, children, ...props }, ref) => {
|
|
151
|
-
const {
|
|
152
|
-
containerRef,
|
|
153
|
-
cssVarName,
|
|
154
|
-
minSize,
|
|
155
|
-
maxSize,
|
|
156
|
-
defaultSize,
|
|
157
|
-
orientation,
|
|
158
|
-
edge,
|
|
159
|
-
computeNext,
|
|
160
|
-
onResize,
|
|
161
|
-
onResizeStart,
|
|
162
|
-
onResizeEnd,
|
|
163
|
-
snapPoints,
|
|
164
|
-
snapTolerance,
|
|
165
|
-
collapseThreshold,
|
|
166
|
-
collapsible,
|
|
167
|
-
target,
|
|
168
|
-
requestCollapse,
|
|
169
|
-
requestToggle,
|
|
170
|
-
} = usePaneResize();
|
|
171
|
-
|
|
172
|
-
// Track active drag cleanup to avoid leaking listeners if unmounted mid-drag
|
|
173
|
-
const activeCleanupRef = React.useRef<(() => void) | null>(null);
|
|
174
|
-
React.useEffect(
|
|
175
|
-
() => () => {
|
|
176
|
-
// Cleanup any in-flight drag on unmount
|
|
177
|
-
try {
|
|
178
|
-
activeCleanupRef.current?.();
|
|
179
|
-
} catch {}
|
|
180
|
-
activeCleanupRef.current = null;
|
|
181
|
-
},
|
|
182
|
-
[],
|
|
183
|
-
);
|
|
43
|
+
// Shell context is provided via ShellProvider (see shell.context.tsx)
|
|
184
44
|
|
|
185
|
-
|
|
45
|
+
// Pane resize context moved to ./_internal/shell-resize
|
|
186
46
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
{...props}
|
|
190
|
-
ref={ref}
|
|
191
|
-
className={classNames('rt-ShellResizer', className)}
|
|
192
|
-
data-orientation={orientation}
|
|
193
|
-
data-edge={edge}
|
|
194
|
-
role="slider"
|
|
195
|
-
aria-orientation={ariaOrientation}
|
|
196
|
-
aria-valuemin={minSize}
|
|
197
|
-
aria-valuemax={maxSize}
|
|
198
|
-
aria-valuenow={defaultSize}
|
|
199
|
-
tabIndex={0}
|
|
200
|
-
onPointerDown={(e) => {
|
|
201
|
-
if (!containerRef.current) return;
|
|
202
|
-
e.preventDefault();
|
|
203
|
-
const container = containerRef.current;
|
|
204
|
-
const handleEl = e.currentTarget as HTMLElement;
|
|
205
|
-
const pointerId = e.pointerId;
|
|
206
|
-
// If a previous drag didn't finalize properly, force cleanup first
|
|
207
|
-
try {
|
|
208
|
-
activeCleanupRef.current?.();
|
|
209
|
-
} catch {}
|
|
210
|
-
container.setAttribute('data-resizing', '');
|
|
211
|
-
try {
|
|
212
|
-
handleEl.setPointerCapture(pointerId);
|
|
213
|
-
} catch {}
|
|
214
|
-
const startClient = orientation === 'vertical' ? e.clientX : e.clientY;
|
|
215
|
-
const startSize = parseFloat(
|
|
216
|
-
getComputedStyle(container).getPropertyValue(cssVarName) || `${defaultSize}`,
|
|
217
|
-
);
|
|
218
|
-
const clamp = (v: number) => Math.min(Math.max(v, minSize), maxSize);
|
|
219
|
-
const body = document.body;
|
|
220
|
-
const prevCursor = body.style.cursor;
|
|
221
|
-
const prevUserSelect = body.style.userSelect;
|
|
222
|
-
body.style.cursor = orientation === 'vertical' ? 'col-resize' : 'row-resize';
|
|
223
|
-
body.style.userSelect = 'none';
|
|
224
|
-
onResizeStart?.(startSize);
|
|
225
|
-
const handleMove = (ev: PointerEvent) => {
|
|
226
|
-
const client = orientation === 'vertical' ? ev.clientX : ev.clientY;
|
|
227
|
-
const next = clamp(computeNext(client, startClient, startSize));
|
|
228
|
-
container.style.setProperty(cssVarName, `${next}px`);
|
|
229
|
-
handleEl.setAttribute('aria-valuenow', String(next));
|
|
230
|
-
onResize?.(next);
|
|
231
|
-
};
|
|
232
|
-
const cleanup = () => {
|
|
233
|
-
try {
|
|
234
|
-
handleEl.releasePointerCapture(pointerId);
|
|
235
|
-
} catch {}
|
|
236
|
-
window.removeEventListener('pointermove', handleMove as any);
|
|
237
|
-
window.removeEventListener('pointerup', handleUp as any);
|
|
238
|
-
window.removeEventListener('pointercancel', handleUp as any);
|
|
239
|
-
window.removeEventListener('keydown', handleKey as any);
|
|
240
|
-
handleEl.removeEventListener('lostpointercapture', handleUp as any);
|
|
241
|
-
container.removeAttribute('data-resizing');
|
|
242
|
-
body.style.cursor = prevCursor;
|
|
243
|
-
body.style.userSelect = prevUserSelect;
|
|
244
|
-
activeCleanupRef.current = null;
|
|
245
|
-
};
|
|
246
|
-
const handleUp = () => {
|
|
247
|
-
const finalSize = parseFloat(
|
|
248
|
-
getComputedStyle(container).getPropertyValue(cssVarName) || `${defaultSize}`,
|
|
249
|
-
);
|
|
250
|
-
// snap logic
|
|
251
|
-
let snapped = finalSize;
|
|
252
|
-
if (snapPoints && snapPoints.length) {
|
|
253
|
-
const nearest = snapPoints.reduce(
|
|
254
|
-
(acc, p) => (Math.abs(p - finalSize) < Math.abs(acc - finalSize) ? p : acc),
|
|
255
|
-
snapPoints[0],
|
|
256
|
-
);
|
|
257
|
-
if (Math.abs(nearest - finalSize) <= (snapTolerance ?? 8)) {
|
|
258
|
-
snapped = nearest;
|
|
259
|
-
container.style.setProperty(cssVarName, `${snapped}px`);
|
|
260
|
-
handleEl.setAttribute('aria-valuenow', String(snapped));
|
|
261
|
-
onResize?.(snapped);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
// collapse threshold
|
|
265
|
-
if (
|
|
266
|
-
collapsible &&
|
|
267
|
-
typeof collapseThreshold === 'number' &&
|
|
268
|
-
finalSize <= collapseThreshold
|
|
269
|
-
) {
|
|
270
|
-
requestCollapse?.();
|
|
271
|
-
}
|
|
272
|
-
onResizeEnd?.(snapped);
|
|
273
|
-
cleanup();
|
|
274
|
-
};
|
|
275
|
-
const handleKey = (kev: KeyboardEvent) => {
|
|
276
|
-
if (kev.key === 'Escape') {
|
|
277
|
-
// cancel to start size
|
|
278
|
-
container.style.setProperty(cssVarName, `${startSize}px`);
|
|
279
|
-
handleEl.setAttribute('aria-valuenow', String(startSize));
|
|
280
|
-
onResizeEnd?.(startSize);
|
|
281
|
-
cleanup();
|
|
282
|
-
}
|
|
283
|
-
};
|
|
284
|
-
window.addEventListener('pointermove', handleMove as any);
|
|
285
|
-
window.addEventListener('pointerup', handleUp as any);
|
|
286
|
-
window.addEventListener('pointercancel', handleUp as any);
|
|
287
|
-
window.addEventListener('keydown', handleKey as any);
|
|
288
|
-
handleEl.addEventListener('lostpointercapture', handleUp as any);
|
|
289
|
-
// Store cleanup so unmounts or re-entries can clean up properly
|
|
290
|
-
activeCleanupRef.current = cleanup;
|
|
291
|
-
}}
|
|
292
|
-
onDoubleClick={() => {
|
|
293
|
-
if (collapsible) requestToggle?.();
|
|
294
|
-
}}
|
|
295
|
-
onKeyDown={(e) => {
|
|
296
|
-
if (!containerRef.current) return;
|
|
297
|
-
const container = containerRef.current;
|
|
298
|
-
const current = parseFloat(
|
|
299
|
-
getComputedStyle(container).getPropertyValue(cssVarName) || `${defaultSize}`,
|
|
300
|
-
);
|
|
301
|
-
const clamp = (v: number) => Math.min(Math.max(v, minSize), maxSize);
|
|
302
|
-
const step = e.shiftKey ? 32 : 8;
|
|
303
|
-
let delta = 0;
|
|
304
|
-
if (orientation === 'vertical') {
|
|
305
|
-
if (e.key === 'ArrowRight') delta = step;
|
|
306
|
-
else if (e.key === 'ArrowLeft') delta = -step;
|
|
307
|
-
} else {
|
|
308
|
-
if (e.key === 'ArrowDown') delta = step;
|
|
309
|
-
else if (e.key === 'ArrowUp') delta = -step;
|
|
310
|
-
}
|
|
311
|
-
if (e.key === 'Home') {
|
|
312
|
-
e.preventDefault();
|
|
313
|
-
onResizeStart?.(current);
|
|
314
|
-
const next = clamp(minSize);
|
|
315
|
-
container.style.setProperty(cssVarName, `${next}px`);
|
|
316
|
-
(e.currentTarget as HTMLElement).setAttribute('aria-valuenow', String(next));
|
|
317
|
-
onResize?.(next);
|
|
318
|
-
onResizeEnd?.(next);
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
if (e.key === 'End') {
|
|
322
|
-
e.preventDefault();
|
|
323
|
-
onResizeStart?.(current);
|
|
324
|
-
const next = clamp(maxSize);
|
|
325
|
-
container.style.setProperty(cssVarName, `${next}px`);
|
|
326
|
-
(e.currentTarget as HTMLElement).setAttribute('aria-valuenow', String(next));
|
|
327
|
-
onResize?.(next);
|
|
328
|
-
onResizeEnd?.(next);
|
|
329
|
-
return;
|
|
330
|
-
}
|
|
331
|
-
if (delta !== 0) {
|
|
332
|
-
e.preventDefault();
|
|
333
|
-
onResizeStart?.(current);
|
|
334
|
-
// approximate computeNext with delta from keyboard
|
|
335
|
-
const next = clamp(
|
|
336
|
-
current + (edge === 'start' && orientation === 'vertical' ? -delta : delta),
|
|
337
|
-
);
|
|
338
|
-
container.style.setProperty(cssVarName, `${next}px`);
|
|
339
|
-
(e.currentTarget as HTMLElement).setAttribute('aria-valuenow', String(next));
|
|
340
|
-
onResize?.(next);
|
|
341
|
-
onResizeEnd?.(next);
|
|
342
|
-
}
|
|
343
|
-
}}
|
|
344
|
-
>
|
|
345
|
-
{children}
|
|
346
|
-
</div>
|
|
347
|
-
);
|
|
348
|
-
},
|
|
349
|
-
);
|
|
350
|
-
PaneHandle.displayName = 'Shell.Handle';
|
|
47
|
+
// Local PaneHandle moved to ./_internal/shell-handles
|
|
48
|
+
// Removed local PaneHandle implementation; using internal PaneHandle
|
|
351
49
|
|
|
352
50
|
// Composed Handle wrappers per pane
|
|
353
|
-
|
|
354
|
-
(props, ref) => <PaneHandle {...props} ref={ref} />,
|
|
355
|
-
);
|
|
356
|
-
PanelHandle.displayName = 'Shell.Panel.Handle';
|
|
357
|
-
|
|
358
|
-
const SidebarHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
|
|
359
|
-
(props, ref) => <PaneHandle {...props} ref={ref} />,
|
|
360
|
-
);
|
|
361
|
-
SidebarHandle.displayName = 'Shell.Sidebar.Handle';
|
|
362
|
-
|
|
363
|
-
const InspectorHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
|
|
364
|
-
(props, ref) => <PaneHandle {...props} ref={ref} />,
|
|
365
|
-
);
|
|
366
|
-
InspectorHandle.displayName = 'Shell.Inspector.Handle';
|
|
367
|
-
|
|
368
|
-
const BottomHandle = React.forwardRef<HTMLDivElement, React.ComponentPropsWithoutRef<'div'>>(
|
|
369
|
-
(props, ref) => <PaneHandle {...props} ref={ref} />,
|
|
370
|
-
);
|
|
371
|
-
BottomHandle.displayName = 'Shell.Bottom.Handle';
|
|
51
|
+
// Handles moved to ./_internal/shell-handles
|
|
372
52
|
|
|
373
53
|
// Hook to resolve responsive presentation
|
|
374
|
-
|
|
375
|
-
const { currentBreakpoint } = useShell();
|
|
376
|
-
|
|
377
|
-
return React.useMemo(() => {
|
|
378
|
-
if (typeof presentation === 'string') {
|
|
379
|
-
return presentation;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
// Try current breakpoint first
|
|
383
|
-
if (presentation[currentBreakpoint]) {
|
|
384
|
-
return presentation[currentBreakpoint]!;
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
// Cascade down to smaller breakpoints based on configured BREAKPOINTS
|
|
388
|
-
const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
|
|
389
|
-
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat(
|
|
390
|
-
'initial' as Breakpoint,
|
|
391
|
-
);
|
|
392
|
-
const startIdx = order.indexOf(currentBreakpoint as Breakpoint);
|
|
393
|
-
|
|
394
|
-
for (let i = startIdx + 1; i < order.length; i++) {
|
|
395
|
-
const bp = order[i];
|
|
396
|
-
if (presentation[bp]) {
|
|
397
|
-
return presentation[bp]!;
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
return 'fixed'; // Default fallback
|
|
402
|
-
}, [presentation, currentBreakpoint]);
|
|
403
|
-
}
|
|
54
|
+
// useResponsivePresentation moved to shell.hooks.ts
|
|
404
55
|
|
|
405
56
|
// Hook to resolve responsive mode defaults
|
|
406
57
|
// Removed: defaultMode responsiveness
|
|
@@ -413,9 +64,7 @@ function useBreakpoint(): { bp: Breakpoint; ready: boolean } {
|
|
|
413
64
|
React.useEffect(() => {
|
|
414
65
|
if (typeof window === 'undefined') return;
|
|
415
66
|
|
|
416
|
-
const queries: [key: keyof typeof BREAKPOINTS, query: string][] = Object.entries(
|
|
417
|
-
BREAKPOINTS,
|
|
418
|
-
) as any;
|
|
67
|
+
const queries: [key: keyof typeof BREAKPOINTS, query: string][] = Object.entries(BREAKPOINTS) as any;
|
|
419
68
|
const mqls = queries.map(([k, q]) => [k, window.matchMedia(q)] as const);
|
|
420
69
|
|
|
421
70
|
const compute = () => {
|
|
@@ -443,287 +92,287 @@ interface ShellRootProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
|
443
92
|
height?: 'full' | 'auto' | string | number;
|
|
444
93
|
}
|
|
445
94
|
|
|
446
|
-
const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(
|
|
447
|
-
|
|
448
|
-
const { bp: currentBreakpoint, ready: currentBreakpointReady } = useBreakpoint();
|
|
95
|
+
const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, children, height = 'full', ...props }, ref) => {
|
|
96
|
+
const { bp: currentBreakpoint, ready: currentBreakpointReady } = useBreakpoint();
|
|
449
97
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
98
|
+
// Pane state management
|
|
99
|
+
const [leftMode, setLeftMode] = React.useState<PaneMode>('collapsed');
|
|
100
|
+
const [panelMode, setPanelMode] = React.useState<PaneMode>('collapsed');
|
|
101
|
+
const [sidebarMode, setSidebarMode] = React.useState<SidebarMode>('expanded');
|
|
102
|
+
// Library-managed phase for sidebar presentation changes (thin ↔ expanded)
|
|
103
|
+
const [sidebarPhase, setSidebarPhase] = React.useState<'idle' | 'hiding' | 'resizing' | 'showing'>('idle');
|
|
104
|
+
const [inspectorMode, setInspectorMode] = React.useState<PaneMode>('collapsed');
|
|
105
|
+
const [bottomMode, setBottomMode] = React.useState<PaneMode>('collapsed');
|
|
456
106
|
|
|
457
|
-
|
|
107
|
+
// Removed: defaultMode responsiveness and manual change tracking
|
|
458
108
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
109
|
+
// Composition detection
|
|
110
|
+
const [hasLeft, setHasLeft] = React.useState(false);
|
|
111
|
+
const [hasSidebar, setHasSidebar] = React.useState(false);
|
|
462
112
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const setSidebarToggleComputer = React.useCallback(
|
|
469
|
-
(fn: (current: SidebarMode) => SidebarMode) => {
|
|
470
|
-
sidebarToggleComputerRef.current = fn;
|
|
471
|
-
},
|
|
472
|
-
[],
|
|
473
|
-
);
|
|
113
|
+
// Customizable sidebar toggle sequencing
|
|
114
|
+
const sidebarToggleComputerRef = React.useRef<(current: SidebarMode) => SidebarMode>((current) => (current === 'collapsed' ? 'thin' : current === 'thin' ? 'expanded' : 'collapsed'));
|
|
115
|
+
const setSidebarToggleComputer = React.useCallback((fn: (current: SidebarMode) => SidebarMode) => {
|
|
116
|
+
sidebarToggleComputerRef.current = fn;
|
|
117
|
+
}, []);
|
|
474
118
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
119
|
+
// Left collapse cascades to Panel
|
|
120
|
+
React.useEffect(() => {
|
|
121
|
+
if (leftMode === 'collapsed') {
|
|
122
|
+
setPanelMode('collapsed');
|
|
123
|
+
}
|
|
124
|
+
}, [leftMode]);
|
|
481
125
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
panelDefaultSizeRef.current = size;
|
|
501
|
-
}, []);
|
|
502
|
-
|
|
503
|
-
// Determine children presence for left composition
|
|
504
|
-
const hasLeftChildren = React.useMemo(() => {
|
|
505
|
-
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
506
|
-
const isType = (el: React.ReactElement, comp: any) =>
|
|
507
|
-
React.isValidElement(el) &&
|
|
508
|
-
(el.type === comp || (el as any).type?.displayName === comp.displayName);
|
|
509
|
-
return childArray.some((el) => isType(el, Rail) || isType(el, Panel));
|
|
510
|
-
}, [children]);
|
|
126
|
+
// Composition validation
|
|
127
|
+
React.useEffect(() => {
|
|
128
|
+
if (hasSidebar && hasLeft) {
|
|
129
|
+
console.warn('Shell: Sidebar cannot coexist with Rail or Panel. Use either Rail+Panel OR Sidebar.');
|
|
130
|
+
}
|
|
131
|
+
}, [hasSidebar, hasLeft]);
|
|
132
|
+
|
|
133
|
+
// Left presentation + defaults from children
|
|
134
|
+
const [devLeftPres, setDevLeftPres] = React.useState<PresentationValue | undefined>(undefined);
|
|
135
|
+
const onLeftPres = React.useCallback((p: PresentationValue) => setDevLeftPres(p), []);
|
|
136
|
+
const railDefaultSizeRef = React.useRef<number>(64);
|
|
137
|
+
const panelDefaultSizeRef = React.useRef<number>(288);
|
|
138
|
+
const onRailDefaults = React.useCallback((size: number) => {
|
|
139
|
+
railDefaultSizeRef.current = size;
|
|
140
|
+
}, []);
|
|
141
|
+
const onPanelDefaults = React.useCallback((size: number) => {
|
|
142
|
+
panelDefaultSizeRef.current = size;
|
|
143
|
+
}, []);
|
|
511
144
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
}, [children]);
|
|
519
|
-
|
|
520
|
-
const togglePane = React.useCallback(
|
|
521
|
-
(target: PaneTarget) => {
|
|
522
|
-
switch (target) {
|
|
523
|
-
case 'left':
|
|
524
|
-
case 'rail':
|
|
525
|
-
setLeftMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
526
|
-
break;
|
|
527
|
-
case 'panel':
|
|
528
|
-
// Panel toggle: expand left if collapsed, then toggle panel
|
|
529
|
-
if (leftMode === 'collapsed') {
|
|
530
|
-
setLeftMode('expanded');
|
|
531
|
-
setPanelMode('expanded');
|
|
532
|
-
} else {
|
|
533
|
-
setPanelMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
534
|
-
}
|
|
535
|
-
break;
|
|
536
|
-
case 'sidebar':
|
|
537
|
-
setSidebarMode((prev) => sidebarToggleComputerRef.current(prev as SidebarMode));
|
|
538
|
-
break;
|
|
539
|
-
case 'inspector':
|
|
540
|
-
setInspectorMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
541
|
-
break;
|
|
542
|
-
case 'bottom':
|
|
543
|
-
setBottomMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
544
|
-
break;
|
|
545
|
-
}
|
|
546
|
-
},
|
|
547
|
-
[leftMode],
|
|
548
|
-
);
|
|
145
|
+
// Determine children presence for left composition
|
|
146
|
+
const hasLeftChildren = React.useMemo(() => {
|
|
147
|
+
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
148
|
+
const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && (el.type === comp || (el as any).type?.displayName === comp.displayName);
|
|
149
|
+
return childArray.some((el) => isType(el, Rail) || isType(el, Panel));
|
|
150
|
+
}, [children]);
|
|
549
151
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
break;
|
|
556
|
-
case 'panel':
|
|
557
|
-
setLeftMode('expanded');
|
|
558
|
-
setPanelMode('expanded');
|
|
559
|
-
break;
|
|
560
|
-
case 'sidebar':
|
|
561
|
-
setSidebarMode('expanded');
|
|
562
|
-
break;
|
|
563
|
-
case 'inspector':
|
|
564
|
-
setInspectorMode('expanded');
|
|
565
|
-
break;
|
|
566
|
-
case 'bottom':
|
|
567
|
-
setBottomMode('expanded');
|
|
568
|
-
break;
|
|
569
|
-
}
|
|
570
|
-
}, []);
|
|
152
|
+
const hasSidebarChildren = React.useMemo(() => {
|
|
153
|
+
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
154
|
+
const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && (el.type === comp || (el as any).type?.displayName === comp.displayName);
|
|
155
|
+
return childArray.some((el) => isType(el, Sidebar));
|
|
156
|
+
}, [children]);
|
|
571
157
|
|
|
572
|
-
|
|
158
|
+
const togglePane = React.useCallback(
|
|
159
|
+
(target: PaneTarget) => {
|
|
573
160
|
switch (target) {
|
|
574
161
|
case 'left':
|
|
575
162
|
case 'rail':
|
|
576
|
-
setLeftMode('collapsed');
|
|
163
|
+
setLeftMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
577
164
|
break;
|
|
578
165
|
case 'panel':
|
|
579
|
-
|
|
166
|
+
// Panel toggle: expand left if collapsed, then toggle panel
|
|
167
|
+
if (leftMode === 'collapsed') {
|
|
168
|
+
setLeftMode('expanded');
|
|
169
|
+
setPanelMode('expanded');
|
|
170
|
+
} else {
|
|
171
|
+
setPanelMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
172
|
+
}
|
|
580
173
|
break;
|
|
581
|
-
case 'sidebar':
|
|
582
|
-
|
|
174
|
+
case 'sidebar': {
|
|
175
|
+
// Orchestrate thin ↔ expanded sequencing: fade out → change mode → fade in
|
|
176
|
+
const next = sidebarToggleComputerRef.current(sidebarMode as SidebarMode);
|
|
177
|
+
const isWidthOnlyChange = sidebarMode !== next && sidebarMode !== 'collapsed' && next !== 'collapsed';
|
|
178
|
+
if (!isWidthOnlyChange) {
|
|
179
|
+
setSidebarMode(next);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
const SMALL_MS = 150;
|
|
183
|
+
setSidebarPhase('hiding');
|
|
184
|
+
window.setTimeout(() => {
|
|
185
|
+
setSidebarPhase('resizing');
|
|
186
|
+
setSidebarMode(next);
|
|
187
|
+
window.setTimeout(() => {
|
|
188
|
+
setSidebarPhase('showing');
|
|
189
|
+
window.setTimeout(() => setSidebarPhase('idle'), SMALL_MS);
|
|
190
|
+
}, SMALL_MS);
|
|
191
|
+
}, SMALL_MS);
|
|
583
192
|
break;
|
|
193
|
+
}
|
|
584
194
|
case 'inspector':
|
|
585
|
-
setInspectorMode('collapsed');
|
|
195
|
+
setInspectorMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
586
196
|
break;
|
|
587
197
|
case 'bottom':
|
|
588
|
-
setBottomMode('collapsed');
|
|
198
|
+
setBottomMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
589
199
|
break;
|
|
590
200
|
}
|
|
591
|
-
},
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
onLeftPres,
|
|
617
|
-
onRailDefaults,
|
|
618
|
-
onPanelDefaults,
|
|
619
|
-
}),
|
|
620
|
-
[
|
|
621
|
-
leftMode,
|
|
622
|
-
panelMode,
|
|
623
|
-
sidebarMode,
|
|
624
|
-
inspectorMode,
|
|
625
|
-
bottomMode,
|
|
626
|
-
hasLeft,
|
|
627
|
-
hasSidebar,
|
|
628
|
-
currentBreakpoint,
|
|
629
|
-
currentBreakpointReady,
|
|
630
|
-
devLeftPres,
|
|
631
|
-
togglePane,
|
|
632
|
-
expandPane,
|
|
633
|
-
collapsePane,
|
|
634
|
-
setSidebarToggleComputer,
|
|
635
|
-
onLeftPres,
|
|
636
|
-
onRailDefaults,
|
|
637
|
-
onPanelDefaults,
|
|
638
|
-
],
|
|
639
|
-
);
|
|
201
|
+
},
|
|
202
|
+
[leftMode, sidebarMode],
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const expandPane = React.useCallback((target: PaneTarget) => {
|
|
206
|
+
switch (target) {
|
|
207
|
+
case 'left':
|
|
208
|
+
case 'rail':
|
|
209
|
+
setLeftMode('expanded');
|
|
210
|
+
break;
|
|
211
|
+
case 'panel':
|
|
212
|
+
setLeftMode('expanded');
|
|
213
|
+
setPanelMode('expanded');
|
|
214
|
+
break;
|
|
215
|
+
case 'sidebar':
|
|
216
|
+
setSidebarMode('expanded');
|
|
217
|
+
break;
|
|
218
|
+
case 'inspector':
|
|
219
|
+
setInspectorMode('expanded');
|
|
220
|
+
break;
|
|
221
|
+
case 'bottom':
|
|
222
|
+
setBottomMode('expanded');
|
|
223
|
+
break;
|
|
224
|
+
}
|
|
225
|
+
}, []);
|
|
640
226
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}, [height]);
|
|
662
|
-
|
|
663
|
-
// Peek state (layout-only overlay without mode changes)
|
|
664
|
-
const [peekTarget, setPeekTarget] = React.useState<PaneTarget | null>(null);
|
|
665
|
-
const peekPane = React.useCallback((target: PaneTarget) => setPeekTarget(target), []);
|
|
666
|
-
const clearPeek = React.useCallback(() => setPeekTarget(null), []);
|
|
227
|
+
const collapsePane = React.useCallback((target: PaneTarget) => {
|
|
228
|
+
switch (target) {
|
|
229
|
+
case 'left':
|
|
230
|
+
case 'rail':
|
|
231
|
+
setLeftMode('collapsed');
|
|
232
|
+
break;
|
|
233
|
+
case 'panel':
|
|
234
|
+
setPanelMode('collapsed');
|
|
235
|
+
break;
|
|
236
|
+
case 'sidebar':
|
|
237
|
+
setSidebarMode('collapsed');
|
|
238
|
+
break;
|
|
239
|
+
case 'inspector':
|
|
240
|
+
setInspectorMode('collapsed');
|
|
241
|
+
break;
|
|
242
|
+
case 'bottom':
|
|
243
|
+
setBottomMode('collapsed');
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}, []);
|
|
667
247
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
248
|
+
const baseContextValue = React.useMemo(
|
|
249
|
+
() => ({
|
|
250
|
+
leftMode,
|
|
251
|
+
setLeftMode,
|
|
252
|
+
panelMode,
|
|
253
|
+
setPanelMode,
|
|
254
|
+
sidebarMode,
|
|
255
|
+
setSidebarMode,
|
|
256
|
+
inspectorMode,
|
|
257
|
+
setInspectorMode,
|
|
258
|
+
bottomMode,
|
|
259
|
+
setBottomMode,
|
|
260
|
+
sidebarPhase,
|
|
261
|
+
hasLeft,
|
|
262
|
+
setHasLeft,
|
|
263
|
+
hasSidebar,
|
|
264
|
+
setHasSidebar,
|
|
265
|
+
currentBreakpoint,
|
|
266
|
+
currentBreakpointReady,
|
|
267
|
+
leftResolvedPresentation: devLeftPres,
|
|
268
|
+
togglePane,
|
|
269
|
+
expandPane,
|
|
270
|
+
collapsePane,
|
|
271
|
+
setSidebarToggleComputer,
|
|
272
|
+
onLeftPres,
|
|
273
|
+
onRailDefaults,
|
|
274
|
+
onPanelDefaults,
|
|
275
|
+
}),
|
|
276
|
+
[
|
|
277
|
+
leftMode,
|
|
278
|
+
panelMode,
|
|
279
|
+
sidebarMode,
|
|
280
|
+
inspectorMode,
|
|
281
|
+
bottomMode,
|
|
282
|
+
sidebarPhase,
|
|
283
|
+
hasLeft,
|
|
284
|
+
hasSidebar,
|
|
285
|
+
currentBreakpoint,
|
|
286
|
+
currentBreakpointReady,
|
|
287
|
+
devLeftPres,
|
|
288
|
+
togglePane,
|
|
289
|
+
expandPane,
|
|
290
|
+
collapsePane,
|
|
291
|
+
setSidebarToggleComputer,
|
|
292
|
+
onLeftPres,
|
|
293
|
+
onRailDefaults,
|
|
294
|
+
onPanelDefaults,
|
|
295
|
+
],
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Organize children by type
|
|
299
|
+
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
300
|
+
const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && (el.type === comp || (el as any).type?.displayName === comp.displayName);
|
|
301
|
+
|
|
302
|
+
const headerEls = childArray.filter((el) => isType(el, Header));
|
|
303
|
+
const railEls = childArray.filter((el) => isType(el, Rail));
|
|
304
|
+
const panelEls = childArray.filter((el) => isType(el, Panel));
|
|
305
|
+
const sidebarEls = childArray.filter((el) => isType(el, Sidebar));
|
|
306
|
+
const contentEls = childArray.filter((el) => isType(el, Content));
|
|
307
|
+
const inspectorEls = childArray.filter((el) => isType(el, Inspector));
|
|
308
|
+
const bottomEls = childArray.filter((el) => isType(el, Bottom));
|
|
309
|
+
|
|
310
|
+
const heightStyle = React.useMemo(() => {
|
|
311
|
+
if (height === 'full') return { height: '100vh' };
|
|
312
|
+
if (height === 'auto') return { height: 'auto' };
|
|
313
|
+
if (typeof height === 'string') return { height };
|
|
314
|
+
if (typeof height === 'number') return { height: `${height}px` };
|
|
315
|
+
return {};
|
|
316
|
+
}, [height]);
|
|
317
|
+
|
|
318
|
+
// Peek state (layout-only overlay without mode changes)
|
|
319
|
+
const [peekTarget, setPeekTarget] = React.useState<PaneTarget | null>(null);
|
|
320
|
+
const peekPane = React.useCallback((target: PaneTarget) => setPeekTarget(target), []);
|
|
321
|
+
const clearPeek = React.useCallback(() => setPeekTarget(null), []);
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<div {...props} ref={ref} className={classNames('rt-ShellRoot', className)} style={{ ...heightStyle, ...props.style }}>
|
|
325
|
+
<ShellProvider
|
|
326
|
+
value={{
|
|
327
|
+
...baseContextValue,
|
|
328
|
+
peekTarget,
|
|
329
|
+
setPeekTarget,
|
|
330
|
+
peekPane,
|
|
331
|
+
clearPeek,
|
|
332
|
+
}}
|
|
674
333
|
>
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
334
|
+
{headerEls}
|
|
335
|
+
<div
|
|
336
|
+
className="rt-ShellBody"
|
|
337
|
+
data-peek-target={peekTarget ?? undefined}
|
|
338
|
+
style={
|
|
339
|
+
peekTarget === 'rail' || peekTarget === 'panel'
|
|
340
|
+
? ({
|
|
341
|
+
['--peek-rail-width' as any]: `${railDefaultSizeRef.current}px`,
|
|
342
|
+
} as React.CSSProperties)
|
|
343
|
+
: undefined
|
|
344
|
+
}
|
|
683
345
|
>
|
|
684
|
-
{
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
</Left>
|
|
715
|
-
);
|
|
716
|
-
})()
|
|
717
|
-
: sidebarEls}
|
|
718
|
-
{contentEls}
|
|
719
|
-
{inspectorEls}
|
|
720
|
-
</div>
|
|
721
|
-
{bottomEls}
|
|
722
|
-
</ShellContext.Provider>
|
|
723
|
-
</div>
|
|
724
|
-
);
|
|
725
|
-
},
|
|
726
|
-
);
|
|
346
|
+
{hasLeftChildren && !hasSidebarChildren
|
|
347
|
+
? (() => {
|
|
348
|
+
const firstRail = railEls[0] as any;
|
|
349
|
+
const passthroughProps = firstRail
|
|
350
|
+
? {
|
|
351
|
+
mode: firstRail.props?.mode,
|
|
352
|
+
defaultMode: firstRail.props?.defaultMode,
|
|
353
|
+
onModeChange: firstRail.props?.onModeChange,
|
|
354
|
+
presentation: firstRail.props?.presentation,
|
|
355
|
+
collapsible: firstRail.props?.collapsible,
|
|
356
|
+
onExpand: firstRail.props?.onExpand,
|
|
357
|
+
onCollapse: firstRail.props?.onCollapse,
|
|
358
|
+
}
|
|
359
|
+
: {};
|
|
360
|
+
return (
|
|
361
|
+
<Left {...(passthroughProps as any)}>
|
|
362
|
+
{railEls}
|
|
363
|
+
{panelEls}
|
|
364
|
+
</Left>
|
|
365
|
+
);
|
|
366
|
+
})()
|
|
367
|
+
: sidebarEls}
|
|
368
|
+
{contentEls}
|
|
369
|
+
{inspectorEls}
|
|
370
|
+
</div>
|
|
371
|
+
{bottomEls}
|
|
372
|
+
</ShellProvider>
|
|
373
|
+
</div>
|
|
374
|
+
);
|
|
375
|
+
});
|
|
727
376
|
Root.displayName = 'Shell.Root';
|
|
728
377
|
|
|
729
378
|
// Header
|
|
@@ -731,19 +380,17 @@ interface ShellHeaderProps extends React.ComponentPropsWithoutRef<'header'> {
|
|
|
731
380
|
height?: number;
|
|
732
381
|
}
|
|
733
382
|
|
|
734
|
-
const Header = React.forwardRef<HTMLElement, ShellHeaderProps>(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
style
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
),
|
|
746
|
-
);
|
|
383
|
+
const Header = React.forwardRef<HTMLElement, ShellHeaderProps>(({ className, height = 64, style, ...props }, ref) => (
|
|
384
|
+
<header
|
|
385
|
+
{...props}
|
|
386
|
+
ref={ref}
|
|
387
|
+
className={classNames('rt-ShellHeader', className)}
|
|
388
|
+
style={{
|
|
389
|
+
...style,
|
|
390
|
+
['--shell-header-height' as any]: `${height}px`,
|
|
391
|
+
}}
|
|
392
|
+
/>
|
|
393
|
+
));
|
|
747
394
|
Header.displayName = 'Shell.Header';
|
|
748
395
|
|
|
749
396
|
// Pane Props Interface (shared by Panel, Sidebar, Inspector, Bottom)
|
|
@@ -796,22 +443,7 @@ interface RailProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
|
796
443
|
|
|
797
444
|
// Left container - behaves like Inspector but contains Rail+Panel
|
|
798
445
|
const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
799
|
-
(
|
|
800
|
-
{
|
|
801
|
-
className,
|
|
802
|
-
presentation = { initial: 'overlay', sm: 'fixed' },
|
|
803
|
-
mode,
|
|
804
|
-
defaultMode = 'collapsed',
|
|
805
|
-
onModeChange,
|
|
806
|
-
collapsible = true,
|
|
807
|
-
onExpand,
|
|
808
|
-
onCollapse,
|
|
809
|
-
children,
|
|
810
|
-
style,
|
|
811
|
-
...props
|
|
812
|
-
},
|
|
813
|
-
ref,
|
|
814
|
-
) => {
|
|
446
|
+
({ className, presentation = { initial: 'overlay', sm: 'fixed' }, mode, defaultMode = 'collapsed', onModeChange, collapsible = true, onExpand, onCollapse, children, style, ...props }, ref) => {
|
|
815
447
|
const shell = useShell();
|
|
816
448
|
const resolvedPresentation = useResponsivePresentation(presentation);
|
|
817
449
|
const isOverlay = resolvedPresentation === 'overlay';
|
|
@@ -844,9 +476,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
844
476
|
return dm[shell.currentBreakpoint as Breakpoint] as PaneMode;
|
|
845
477
|
}
|
|
846
478
|
const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
|
|
847
|
-
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat(
|
|
848
|
-
'initial' as Breakpoint,
|
|
849
|
-
);
|
|
479
|
+
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat('initial' as Breakpoint);
|
|
850
480
|
const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
|
|
851
481
|
for (let i = startIdx + 1; i < order.length; i++) {
|
|
852
482
|
const bp = order[i];
|
|
@@ -867,14 +497,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
867
497
|
if (next !== shell.leftMode) {
|
|
868
498
|
shell.setLeftMode(next);
|
|
869
499
|
}
|
|
870
|
-
}, [
|
|
871
|
-
mode,
|
|
872
|
-
shell.currentBreakpoint,
|
|
873
|
-
shell.currentBreakpointReady,
|
|
874
|
-
resolveResponsiveMode,
|
|
875
|
-
shell.leftMode,
|
|
876
|
-
shell.setLeftMode,
|
|
877
|
-
]);
|
|
500
|
+
}, [mode, shell.currentBreakpoint, shell.currentBreakpointReady, resolveResponsiveMode, shell.leftMode, shell.setLeftMode]);
|
|
878
501
|
|
|
879
502
|
// Sync controlled mode
|
|
880
503
|
React.useEffect(() => {
|
|
@@ -907,27 +530,16 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
907
530
|
const open = shell.leftMode === 'expanded';
|
|
908
531
|
// Compute overlay width from child Rail/Panel expanded sizes
|
|
909
532
|
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
910
|
-
const isType = (el: React.ReactElement, comp: any) =>
|
|
911
|
-
React.isValidElement(el) && el.type === comp;
|
|
533
|
+
const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && el.type === comp;
|
|
912
534
|
const railEl = childArray.find((el) => isType(el, Rail));
|
|
913
535
|
const panelEl = childArray.find((el) => isType(el, Panel));
|
|
914
|
-
const railSize =
|
|
915
|
-
|
|
916
|
-
? (railEl as any).props.expandedSize
|
|
917
|
-
: 64;
|
|
918
|
-
const panelSize =
|
|
919
|
-
typeof (panelEl as any)?.props?.expandedSize === 'number'
|
|
920
|
-
? (panelEl as any).props.expandedSize
|
|
921
|
-
: 288;
|
|
536
|
+
const railSize = typeof (railEl as any)?.props?.expandedSize === 'number' ? (railEl as any).props.expandedSize : 64;
|
|
537
|
+
const panelSize = typeof (panelEl as any)?.props?.expandedSize === 'number' ? (panelEl as any).props.expandedSize : 288;
|
|
922
538
|
const hasRail = Boolean(railEl);
|
|
923
539
|
const hasPanel = Boolean(panelEl);
|
|
924
|
-
const overlayPx =
|
|
925
|
-
(hasRail ? railSize : 0) + (shell.panelMode === 'expanded' && hasPanel ? panelSize : 0);
|
|
540
|
+
const overlayPx = (hasRail ? railSize : 0) + (shell.panelMode === 'expanded' && hasPanel ? panelSize : 0);
|
|
926
541
|
return (
|
|
927
|
-
<Sheet.Root
|
|
928
|
-
open={open}
|
|
929
|
-
onOpenChange={(o) => shell.setLeftMode(o ? 'expanded' : 'collapsed')}
|
|
930
|
-
>
|
|
542
|
+
<Sheet.Root open={open} onOpenChange={(o) => shell.setLeftMode(o ? 'expanded' : 'collapsed')}>
|
|
931
543
|
<Sheet.Content
|
|
932
544
|
side="start"
|
|
933
545
|
style={{ padding: 0 }}
|
|
@@ -948,22 +560,14 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
948
560
|
const open = shell.leftMode === 'expanded';
|
|
949
561
|
// Compute floating width from child Rail/Panel expanded sizes (like overlay)
|
|
950
562
|
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
951
|
-
const isType = (el: React.ReactElement, comp: any) =>
|
|
952
|
-
React.isValidElement(el) && el.type === comp;
|
|
563
|
+
const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && el.type === comp;
|
|
953
564
|
const railEl = childArray.find((el) => isType(el, Rail));
|
|
954
565
|
const panelEl = childArray.find((el) => isType(el, Panel));
|
|
955
|
-
const railSize =
|
|
956
|
-
|
|
957
|
-
? (railEl as any).props.expandedSize
|
|
958
|
-
: 64;
|
|
959
|
-
const panelSize =
|
|
960
|
-
typeof (panelEl as any)?.props?.expandedSize === 'number'
|
|
961
|
-
? (panelEl as any).props.expandedSize
|
|
962
|
-
: 288;
|
|
566
|
+
const railSize = typeof (railEl as any)?.props?.expandedSize === 'number' ? (railEl as any).props.expandedSize : 64;
|
|
567
|
+
const panelSize = typeof (panelEl as any)?.props?.expandedSize === 'number' ? (panelEl as any).props.expandedSize : 288;
|
|
963
568
|
const hasRail = Boolean(railEl);
|
|
964
569
|
const hasPanel = Boolean(panelEl);
|
|
965
|
-
const includePanel =
|
|
966
|
-
hasPanel && (shell.panelMode === 'expanded' || shell.peekTarget === 'panel');
|
|
570
|
+
const includePanel = hasPanel && (shell.panelMode === 'expanded' || shell.peekTarget === 'panel');
|
|
967
571
|
const floatingWidthPx = (hasRail ? railSize : 0) + (includePanel ? panelSize : 0);
|
|
968
572
|
|
|
969
573
|
return (
|
|
@@ -972,12 +576,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
972
576
|
ref={setRef}
|
|
973
577
|
className={classNames('rt-ShellLeft', className)}
|
|
974
578
|
data-mode={shell.leftMode}
|
|
975
|
-
data-peek={
|
|
976
|
-
shell.peekTarget === 'left' ||
|
|
977
|
-
shell.peekTarget === 'rail' ||
|
|
978
|
-
shell.peekTarget === 'panel' ||
|
|
979
|
-
undefined
|
|
980
|
-
}
|
|
579
|
+
data-peek={shell.peekTarget === 'left' || shell.peekTarget === 'rail' || shell.peekTarget === 'panel' || undefined}
|
|
981
580
|
data-presentation={resolvedPresentation}
|
|
982
581
|
style={{
|
|
983
582
|
...style,
|
|
@@ -995,12 +594,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
995
594
|
ref={setRef}
|
|
996
595
|
className={classNames('rt-ShellLeft', className)}
|
|
997
596
|
data-mode={shell.leftMode}
|
|
998
|
-
data-peek={
|
|
999
|
-
shell.peekTarget === 'left' ||
|
|
1000
|
-
shell.peekTarget === 'rail' ||
|
|
1001
|
-
shell.peekTarget === 'panel' ||
|
|
1002
|
-
undefined
|
|
1003
|
-
}
|
|
597
|
+
data-peek={shell.peekTarget === 'left' || shell.peekTarget === 'rail' || shell.peekTarget === 'panel' || undefined}
|
|
1004
598
|
data-presentation={resolvedPresentation}
|
|
1005
599
|
style={{
|
|
1006
600
|
...style,
|
|
@@ -1014,23 +608,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
1014
608
|
Left.displayName = 'Shell.Left';
|
|
1015
609
|
|
|
1016
610
|
const Rail = React.forwardRef<HTMLDivElement, RailProps>(
|
|
1017
|
-
(
|
|
1018
|
-
{
|
|
1019
|
-
className,
|
|
1020
|
-
presentation,
|
|
1021
|
-
mode,
|
|
1022
|
-
defaultMode,
|
|
1023
|
-
onModeChange,
|
|
1024
|
-
expandedSize = 64,
|
|
1025
|
-
collapsible,
|
|
1026
|
-
onExpand,
|
|
1027
|
-
onCollapse,
|
|
1028
|
-
children,
|
|
1029
|
-
style,
|
|
1030
|
-
...props
|
|
1031
|
-
},
|
|
1032
|
-
ref,
|
|
1033
|
-
) => {
|
|
611
|
+
({ className, presentation, mode, defaultMode, onModeChange, expandedSize = 64, collapsible, onExpand, onCollapse, children, style, ...props }, ref) => {
|
|
1034
612
|
const shell = useShell();
|
|
1035
613
|
|
|
1036
614
|
// Register expanded size with Left container
|
|
@@ -1046,22 +624,13 @@ const Rail = React.forwardRef<HTMLDivElement, RailProps>(
|
|
|
1046
624
|
ref={ref}
|
|
1047
625
|
className={classNames('rt-ShellRail', className)}
|
|
1048
626
|
data-mode={shell.leftMode}
|
|
1049
|
-
data-peek={
|
|
1050
|
-
(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') || undefined
|
|
1051
|
-
}
|
|
627
|
+
data-peek={(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') || undefined}
|
|
1052
628
|
style={{
|
|
1053
629
|
...style,
|
|
1054
630
|
['--rail-size' as any]: `${expandedSize}px`,
|
|
1055
631
|
}}
|
|
1056
632
|
>
|
|
1057
|
-
<div
|
|
1058
|
-
className="rt-ShellRailContent"
|
|
1059
|
-
data-visible={
|
|
1060
|
-
isExpanded ||
|
|
1061
|
-
(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') ||
|
|
1062
|
-
undefined
|
|
1063
|
-
}
|
|
1064
|
-
>
|
|
633
|
+
<div className="rt-ShellRailContent" data-visible={isExpanded || (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') || undefined}>
|
|
1065
634
|
{children}
|
|
1066
635
|
</div>
|
|
1067
636
|
</div>
|
|
@@ -1071,13 +640,9 @@ const Rail = React.forwardRef<HTMLDivElement, RailProps>(
|
|
|
1071
640
|
Rail.displayName = 'Shell.Rail';
|
|
1072
641
|
|
|
1073
642
|
// Panel
|
|
1074
|
-
type HandleComponent = React.ForwardRefExoticComponent<
|
|
1075
|
-
React.ComponentPropsWithoutRef<'div'> & React.RefAttributes<HTMLDivElement>
|
|
1076
|
-
>;
|
|
643
|
+
type HandleComponent = React.ForwardRefExoticComponent<React.ComponentPropsWithoutRef<'div'> & React.RefAttributes<HTMLDivElement>>;
|
|
1077
644
|
|
|
1078
|
-
type PanelComponent = React.ForwardRefExoticComponent<
|
|
1079
|
-
Omit<PaneProps, 'defaultMode'> & React.RefAttributes<HTMLDivElement>
|
|
1080
|
-
> & { Handle: HandleComponent };
|
|
645
|
+
type PanelComponent = React.ForwardRefExoticComponent<Omit<PaneProps, 'defaultMode'> & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
|
|
1081
646
|
|
|
1082
647
|
type SidebarComponent = React.ForwardRefExoticComponent<
|
|
1083
648
|
(Omit<PaneProps, 'mode' | 'defaultMode' | 'onModeChange'> & {
|
|
@@ -1090,13 +655,9 @@ type SidebarComponent = React.ForwardRefExoticComponent<
|
|
|
1090
655
|
React.RefAttributes<HTMLDivElement>
|
|
1091
656
|
> & { Handle: HandleComponent };
|
|
1092
657
|
|
|
1093
|
-
type InspectorComponent = React.ForwardRefExoticComponent<
|
|
1094
|
-
PaneProps & React.RefAttributes<HTMLDivElement>
|
|
1095
|
-
> & { Handle: HandleComponent };
|
|
658
|
+
type InspectorComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
|
|
1096
659
|
|
|
1097
|
-
type BottomComponent = React.ForwardRefExoticComponent<
|
|
1098
|
-
PaneProps & React.RefAttributes<HTMLDivElement>
|
|
1099
|
-
> & { Handle: HandleComponent };
|
|
660
|
+
type BottomComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
|
|
1100
661
|
|
|
1101
662
|
const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' | 'defaultMode'>>(
|
|
1102
663
|
(
|
|
@@ -1139,12 +700,8 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
1139
700
|
[ref],
|
|
1140
701
|
);
|
|
1141
702
|
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
1142
|
-
const handleChildren = childArray.filter(
|
|
1143
|
-
|
|
1144
|
-
);
|
|
1145
|
-
const contentChildren = childArray.filter(
|
|
1146
|
-
(el: React.ReactElement) => !(React.isValidElement(el) && el.type === PanelHandle),
|
|
1147
|
-
);
|
|
703
|
+
const handleChildren = childArray.filter((el: React.ReactElement) => React.isValidElement(el) && el.type === PanelHandle);
|
|
704
|
+
const contentChildren = childArray.filter((el: React.ReactElement) => !(React.isValidElement(el) && el.type === PanelHandle));
|
|
1148
705
|
|
|
1149
706
|
const isOverlay = shell.leftResolvedPresentation === 'overlay';
|
|
1150
707
|
|
|
@@ -1193,11 +750,7 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
1193
750
|
// Ensure Left container width is auto whenever Panel is expanded in fixed presentation
|
|
1194
751
|
React.useEffect(() => {
|
|
1195
752
|
if (!localRef.current) return;
|
|
1196
|
-
if (
|
|
1197
|
-
shell.leftResolvedPresentation !== 'overlay' &&
|
|
1198
|
-
shell.leftMode === 'expanded' &&
|
|
1199
|
-
shell.panelMode === 'expanded'
|
|
1200
|
-
) {
|
|
753
|
+
if (shell.leftResolvedPresentation !== 'overlay' && shell.leftMode === 'expanded' && shell.panelMode === 'expanded') {
|
|
1201
754
|
const leftEl = (localRef.current.parentElement as HTMLElement) || null;
|
|
1202
755
|
try {
|
|
1203
756
|
leftEl?.style.removeProperty('width');
|
|
@@ -1247,11 +800,7 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
1247
800
|
requestToggle: () => shell.togglePane('panel'),
|
|
1248
801
|
}}
|
|
1249
802
|
>
|
|
1250
|
-
{handleChildren.length > 0 ? (
|
|
1251
|
-
handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i }))
|
|
1252
|
-
) : (
|
|
1253
|
-
<PaneHandle />
|
|
1254
|
-
)}
|
|
803
|
+
{handleChildren.length > 0 ? handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i })) : <PaneHandle />}
|
|
1255
804
|
</PaneResizeContext.Provider>
|
|
1256
805
|
) : null;
|
|
1257
806
|
|
|
@@ -1261,15 +810,8 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
1261
810
|
ref={setRef}
|
|
1262
811
|
className={classNames('rt-ShellPanel', className)}
|
|
1263
812
|
data-mode={shell.panelMode}
|
|
1264
|
-
data-visible={
|
|
1265
|
-
|
|
1266
|
-
(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') ||
|
|
1267
|
-
undefined
|
|
1268
|
-
}
|
|
1269
|
-
data-peek={
|
|
1270
|
-
(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') ||
|
|
1271
|
-
undefined
|
|
1272
|
-
}
|
|
813
|
+
data-visible={isExpanded || (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') || undefined}
|
|
814
|
+
data-peek={(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') || undefined}
|
|
1273
815
|
style={{
|
|
1274
816
|
...style,
|
|
1275
817
|
['--panel-size' as any]: `${expandedSize}px`,
|
|
@@ -1286,838 +828,23 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
1286
828
|
Panel.displayName = 'Shell.Panel';
|
|
1287
829
|
Panel.Handle = PanelHandle;
|
|
1288
830
|
|
|
1289
|
-
// Sidebar
|
|
1290
|
-
const Sidebar = React.forwardRef<
|
|
1291
|
-
HTMLDivElement,
|
|
1292
|
-
Omit<PaneProps, 'mode' | 'defaultMode' | 'onModeChange'> & {
|
|
1293
|
-
mode?: SidebarMode;
|
|
1294
|
-
defaultMode?: ResponsiveSidebarMode;
|
|
1295
|
-
onModeChange?: (mode: SidebarMode) => void;
|
|
1296
|
-
thinSize?: number;
|
|
1297
|
-
toggleModes?: 'both' | 'single';
|
|
1298
|
-
}
|
|
1299
|
-
>(
|
|
1300
|
-
(
|
|
1301
|
-
{
|
|
1302
|
-
className,
|
|
1303
|
-
presentation = { initial: 'overlay', md: 'fixed' },
|
|
1304
|
-
mode,
|
|
1305
|
-
defaultMode = 'expanded',
|
|
1306
|
-
onModeChange,
|
|
1307
|
-
expandedSize = 288,
|
|
1308
|
-
minSize = 200,
|
|
1309
|
-
maxSize = 400,
|
|
1310
|
-
resizable = false,
|
|
1311
|
-
collapsible = true,
|
|
1312
|
-
onExpand,
|
|
1313
|
-
onCollapse,
|
|
1314
|
-
onResize,
|
|
1315
|
-
onResizeStart,
|
|
1316
|
-
onResizeEnd,
|
|
1317
|
-
snapPoints,
|
|
1318
|
-
snapTolerance,
|
|
1319
|
-
collapseThreshold,
|
|
1320
|
-
paneId,
|
|
1321
|
-
persistence,
|
|
1322
|
-
children,
|
|
1323
|
-
style,
|
|
1324
|
-
thinSize = 64,
|
|
1325
|
-
toggleModes,
|
|
1326
|
-
...props
|
|
1327
|
-
},
|
|
1328
|
-
ref,
|
|
1329
|
-
) => {
|
|
1330
|
-
const shell = useShell();
|
|
1331
|
-
const resolvedPresentation = useResponsivePresentation(presentation);
|
|
1332
|
-
const isOverlay = resolvedPresentation === 'overlay';
|
|
1333
|
-
const isStacked = resolvedPresentation === 'stacked';
|
|
1334
|
-
const localRef = React.useRef<HTMLDivElement | null>(null);
|
|
1335
|
-
const setRef = React.useCallback(
|
|
1336
|
-
(node: HTMLDivElement | null) => {
|
|
1337
|
-
localRef.current = node;
|
|
1338
|
-
if (typeof ref === 'function') ref(node);
|
|
1339
|
-
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
1340
|
-
},
|
|
1341
|
-
[ref],
|
|
1342
|
-
);
|
|
1343
|
-
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
1344
|
-
const handleChildren = childArray.filter(
|
|
1345
|
-
(el: React.ReactElement) => React.isValidElement(el) && el.type === SidebarHandle,
|
|
1346
|
-
);
|
|
1347
|
-
const contentChildren = childArray.filter(
|
|
1348
|
-
(el: React.ReactElement) => !(React.isValidElement(el) && el.type === SidebarHandle),
|
|
1349
|
-
);
|
|
1350
|
-
|
|
1351
|
-
// Register with shell
|
|
1352
|
-
const sidebarId = React.useId();
|
|
1353
|
-
React.useEffect(() => {
|
|
1354
|
-
shell.setHasSidebar(true);
|
|
1355
|
-
return () => {
|
|
1356
|
-
shell.setHasSidebar(false);
|
|
1357
|
-
};
|
|
1358
|
-
}, [shell, sidebarId]);
|
|
831
|
+
// Sidebar moved to ./_internal/shell-sidebar
|
|
1359
832
|
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
React.useEffect(() => {
|
|
1363
|
-
if (didInitRef.current) return;
|
|
1364
|
-
didInitRef.current = true;
|
|
1365
|
-
if (mode === undefined && shell.sidebarMode !== (defaultMode as SidebarMode)) {
|
|
1366
|
-
shell.setSidebarMode(defaultMode as SidebarMode);
|
|
1367
|
-
}
|
|
1368
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1369
|
-
}, []);
|
|
833
|
+
// Content (always required)
|
|
834
|
+
interface ShellContentProps extends React.ComponentPropsWithoutRef<'main'> {}
|
|
1370
835
|
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
if (mode !== undefined && shell.sidebarMode !== mode) {
|
|
1374
|
-
shell.setSidebarMode(mode);
|
|
1375
|
-
}
|
|
1376
|
-
}, [mode, shell]);
|
|
836
|
+
const Content = React.forwardRef<HTMLElement, ShellContentProps>(({ className, ...props }, ref) => <main {...props} ref={ref} className={classNames('rt-ShellContent', className)} />);
|
|
837
|
+
Content.displayName = 'Shell.Content';
|
|
1377
838
|
|
|
1378
|
-
|
|
1379
|
-
React.useEffect(() => {
|
|
1380
|
-
if (mode === undefined) {
|
|
1381
|
-
onModeChange?.(shell.sidebarMode);
|
|
1382
|
-
}
|
|
1383
|
-
}, [shell.sidebarMode, mode, onModeChange]);
|
|
839
|
+
// Inspector moved to ./_internal/shell-inspector
|
|
1384
840
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
onExpand?.();
|
|
1389
|
-
} else {
|
|
1390
|
-
onCollapse?.();
|
|
1391
|
-
}
|
|
1392
|
-
}, [shell.sidebarMode, onExpand, onCollapse]);
|
|
841
|
+
// Bottom
|
|
842
|
+
// Bottom moved to ./_internal/shell-bottom
|
|
843
|
+
// (Bottom implementation extracted)
|
|
1393
844
|
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
// Default persistence if paneId provided and none supplied (fixed only)
|
|
1398
|
-
const persistenceAdapter = React.useMemo(() => {
|
|
1399
|
-
if (!paneId || persistence) return persistence;
|
|
1400
|
-
const key = `kookie-ui:shell:sidebar:${paneId}`;
|
|
1401
|
-
const adapter: PaneSizePersistence = {
|
|
1402
|
-
load: () => {
|
|
1403
|
-
if (typeof window === 'undefined') return undefined;
|
|
1404
|
-
const v = window.localStorage.getItem(key);
|
|
1405
|
-
return v ? Number(v) : undefined;
|
|
1406
|
-
},
|
|
1407
|
-
save: (size: number) => {
|
|
1408
|
-
if (typeof window === 'undefined') return;
|
|
1409
|
-
window.localStorage.setItem(key, String(size));
|
|
1410
|
-
},
|
|
1411
|
-
};
|
|
1412
|
-
return adapter;
|
|
1413
|
-
}, [paneId, persistence]);
|
|
1414
|
-
|
|
1415
|
-
React.useEffect(() => {
|
|
1416
|
-
let mounted = true;
|
|
1417
|
-
(async () => {
|
|
1418
|
-
if (!resizable || !persistenceAdapter?.load || isOverlay) return;
|
|
1419
|
-
const loaded = await persistenceAdapter.load();
|
|
1420
|
-
if (mounted && typeof loaded === 'number' && localRef.current) {
|
|
1421
|
-
localRef.current.style.setProperty('--sidebar-size', `${loaded}px`);
|
|
1422
|
-
onResize?.(loaded);
|
|
1423
|
-
}
|
|
1424
|
-
})();
|
|
1425
|
-
return () => {
|
|
1426
|
-
mounted = false;
|
|
1427
|
-
};
|
|
1428
|
-
}, [resizable, persistenceAdapter, onResize, isOverlay]);
|
|
1429
|
-
|
|
1430
|
-
// Always-follow responsive defaultMode for uncontrolled Sidebar (on breakpoint change only)
|
|
1431
|
-
const resolveResponsiveMode = React.useCallback((): SidebarMode => {
|
|
1432
|
-
if (typeof defaultMode === 'string') return defaultMode as SidebarMode;
|
|
1433
|
-
const dm = defaultMode as Partial<Record<Breakpoint, SidebarMode>> | undefined;
|
|
1434
|
-
if (dm && dm[shell.currentBreakpoint as Breakpoint]) {
|
|
1435
|
-
return dm[shell.currentBreakpoint as Breakpoint] as SidebarMode;
|
|
1436
|
-
}
|
|
1437
|
-
const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
|
|
1438
|
-
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat(
|
|
1439
|
-
'initial' as Breakpoint,
|
|
1440
|
-
);
|
|
1441
|
-
const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
|
|
1442
|
-
for (let i = startIdx + 1; i < order.length; i++) {
|
|
1443
|
-
const bp = order[i];
|
|
1444
|
-
if (dm && dm[bp]) return dm[bp] as SidebarMode;
|
|
1445
|
-
}
|
|
1446
|
-
return 'collapsed';
|
|
1447
|
-
}, [defaultMode, shell.currentBreakpoint]);
|
|
1448
|
-
|
|
1449
|
-
// Register custom toggle behavior based on toggleModes (both|single)
|
|
1450
|
-
const shellForToggle = useShell();
|
|
1451
|
-
const resolveDefaultSidebarMode = React.useCallback((): SidebarMode => {
|
|
1452
|
-
const resolved = resolveResponsiveMode();
|
|
1453
|
-
return resolved === 'thin' || resolved === 'expanded' ? resolved : 'expanded';
|
|
1454
|
-
}, [resolveResponsiveMode]);
|
|
1455
|
-
|
|
1456
|
-
React.useEffect(() => {
|
|
1457
|
-
if (!shellForToggle.setSidebarToggleComputer) return;
|
|
1458
|
-
const strategy: 'both' | 'single' = toggleModes ?? 'both';
|
|
1459
|
-
const compute = (current: SidebarMode): SidebarMode => {
|
|
1460
|
-
if (strategy === 'both') {
|
|
1461
|
-
// collapsed -> thin -> expanded -> collapsed
|
|
1462
|
-
if (current === 'collapsed') return 'thin';
|
|
1463
|
-
if (current === 'thin') return 'expanded';
|
|
1464
|
-
return 'collapsed';
|
|
1465
|
-
}
|
|
1466
|
-
// single: toggle between collapsed and resolved default mode
|
|
1467
|
-
const target = resolveDefaultSidebarMode();
|
|
1468
|
-
if (current === 'collapsed') return target;
|
|
1469
|
-
if (current === target) return 'collapsed';
|
|
1470
|
-
// if in the other non-collapsed state, jump to the target
|
|
1471
|
-
return target;
|
|
1472
|
-
};
|
|
1473
|
-
shellForToggle.setSidebarToggleComputer(compute);
|
|
1474
|
-
return () => {
|
|
1475
|
-
// default fallback sequence when unmounting
|
|
1476
|
-
shellForToggle.setSidebarToggleComputer?.((cur) =>
|
|
1477
|
-
cur === 'collapsed' ? 'thin' : cur === 'thin' ? 'expanded' : 'collapsed',
|
|
1478
|
-
);
|
|
1479
|
-
};
|
|
1480
|
-
}, [shellForToggle, toggleModes, resolveDefaultSidebarMode]);
|
|
1481
|
-
|
|
1482
|
-
// Preserve last non-collapsed width for smooth overlay close animation
|
|
1483
|
-
const lastOverlayWidthRef = React.useRef<number>(expandedSize);
|
|
1484
|
-
const lastOverlayModeRef = React.useRef<SidebarMode>('expanded');
|
|
1485
|
-
React.useEffect(() => {
|
|
1486
|
-
if (shell.sidebarMode !== 'collapsed') {
|
|
1487
|
-
lastOverlayModeRef.current = shell.sidebarMode as SidebarMode;
|
|
1488
|
-
lastOverlayWidthRef.current = shell.sidebarMode === 'thin' ? thinSize : expandedSize;
|
|
1489
|
-
}
|
|
1490
|
-
}, [shell.sidebarMode, thinSize, expandedSize]);
|
|
1491
|
-
|
|
1492
|
-
// (moved above)
|
|
1493
|
-
|
|
1494
|
-
const lastSidebarBpRef = React.useRef<Breakpoint | null>(null);
|
|
1495
|
-
React.useEffect(() => {
|
|
1496
|
-
if (mode !== undefined) return; // controlled wins
|
|
1497
|
-
if (!shell.currentBreakpointReady) return; // avoid SSR mismatch
|
|
1498
|
-
if (lastSidebarBpRef.current === shell.currentBreakpoint) return; // only on bp change
|
|
1499
|
-
lastSidebarBpRef.current = shell.currentBreakpoint as Breakpoint;
|
|
1500
|
-
const next = resolveResponsiveMode();
|
|
1501
|
-
if (next !== shell.sidebarMode) shell.setSidebarMode(next);
|
|
1502
|
-
}, [
|
|
1503
|
-
mode,
|
|
1504
|
-
shell.currentBreakpoint,
|
|
1505
|
-
shell.currentBreakpointReady,
|
|
1506
|
-
resolveResponsiveMode,
|
|
1507
|
-
shell.sidebarMode,
|
|
1508
|
-
shell.setSidebarMode,
|
|
1509
|
-
]);
|
|
1510
|
-
|
|
1511
|
-
const handleEl =
|
|
1512
|
-
resizable && !isOverlay && shell.sidebarMode === 'expanded' ? (
|
|
1513
|
-
<PaneResizeContext.Provider
|
|
1514
|
-
value={{
|
|
1515
|
-
containerRef: localRef,
|
|
1516
|
-
cssVarName: '--sidebar-size',
|
|
1517
|
-
minSize,
|
|
1518
|
-
maxSize,
|
|
1519
|
-
defaultSize: expandedSize,
|
|
1520
|
-
orientation: 'vertical',
|
|
1521
|
-
edge: 'end',
|
|
1522
|
-
computeNext: (client, startClient, startSize) => {
|
|
1523
|
-
const isRtl = getComputedStyle(localRef.current!).direction === 'rtl';
|
|
1524
|
-
const delta = client - startClient;
|
|
1525
|
-
return startSize + (isRtl ? -delta : delta);
|
|
1526
|
-
},
|
|
1527
|
-
onResize,
|
|
1528
|
-
onResizeStart,
|
|
1529
|
-
onResizeEnd: (size) => {
|
|
1530
|
-
onResizeEnd?.(size);
|
|
1531
|
-
persistenceAdapter?.save?.(size);
|
|
1532
|
-
},
|
|
1533
|
-
target: 'sidebar',
|
|
1534
|
-
collapsible,
|
|
1535
|
-
snapPoints,
|
|
1536
|
-
snapTolerance: snapTolerance ?? 8,
|
|
1537
|
-
collapseThreshold,
|
|
1538
|
-
requestCollapse: () => shell.setSidebarMode('collapsed'),
|
|
1539
|
-
requestToggle: () => shell.togglePane('sidebar'),
|
|
1540
|
-
}}
|
|
1541
|
-
>
|
|
1542
|
-
{handleChildren.length > 0 ? (
|
|
1543
|
-
handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i }))
|
|
1544
|
-
) : (
|
|
1545
|
-
<PaneHandle />
|
|
1546
|
-
)}
|
|
1547
|
-
</PaneResizeContext.Provider>
|
|
1548
|
-
) : null;
|
|
1549
|
-
|
|
1550
|
-
if (isOverlay) {
|
|
1551
|
-
const open = shell.sidebarMode !== 'collapsed';
|
|
1552
|
-
return (
|
|
1553
|
-
<Sheet.Root
|
|
1554
|
-
open={open}
|
|
1555
|
-
onOpenChange={(o) => shell.setSidebarMode(o ? 'expanded' : 'collapsed')}
|
|
1556
|
-
>
|
|
1557
|
-
<Sheet.Content
|
|
1558
|
-
side="start"
|
|
1559
|
-
style={{ padding: 0 }}
|
|
1560
|
-
width={{
|
|
1561
|
-
initial: `${open ? (shell.sidebarMode === 'thin' ? thinSize : expandedSize) : lastOverlayWidthRef.current}px`,
|
|
1562
|
-
}}
|
|
1563
|
-
>
|
|
1564
|
-
<VisuallyHidden>
|
|
1565
|
-
<Sheet.Title>Sidebar</Sheet.Title>
|
|
1566
|
-
</VisuallyHidden>
|
|
1567
|
-
{contentChildren}
|
|
1568
|
-
</Sheet.Content>
|
|
1569
|
-
</Sheet.Root>
|
|
1570
|
-
);
|
|
1571
|
-
}
|
|
1572
|
-
|
|
1573
|
-
return (
|
|
1574
|
-
<div
|
|
1575
|
-
{...props}
|
|
1576
|
-
ref={setRef}
|
|
1577
|
-
className={classNames('rt-ShellSidebar', className)}
|
|
1578
|
-
data-mode={shell.sidebarMode}
|
|
1579
|
-
data-peek={shell.peekTarget === 'sidebar' || undefined}
|
|
1580
|
-
data-presentation={resolvedPresentation}
|
|
1581
|
-
data-open={(isStacked && isContentVisible) || undefined}
|
|
1582
|
-
style={{
|
|
1583
|
-
...style,
|
|
1584
|
-
['--sidebar-size' as any]: `${expandedSize}px`,
|
|
1585
|
-
['--sidebar-thin-size' as any]: `${thinSize}px`,
|
|
1586
|
-
['--sidebar-min-size' as any]: `${minSize}px`,
|
|
1587
|
-
['--sidebar-max-size' as any]: `${maxSize}px`,
|
|
1588
|
-
// When peeking in fixed presentation and collapsed, preview next state's width
|
|
1589
|
-
...(shell.peekTarget === 'sidebar' && shell.sidebarMode === 'collapsed' && !isOverlay
|
|
1590
|
-
? (() => {
|
|
1591
|
-
const strategy: 'both' | 'single' = toggleModes ?? 'both';
|
|
1592
|
-
const current = shell.sidebarMode as SidebarMode;
|
|
1593
|
-
let next: SidebarMode;
|
|
1594
|
-
if (strategy === 'both') {
|
|
1595
|
-
next =
|
|
1596
|
-
current === 'collapsed'
|
|
1597
|
-
? 'thin'
|
|
1598
|
-
: current === 'thin'
|
|
1599
|
-
? 'expanded'
|
|
1600
|
-
: 'collapsed';
|
|
1601
|
-
} else {
|
|
1602
|
-
const target = resolveDefaultSidebarMode();
|
|
1603
|
-
next = current === 'collapsed' ? target : 'collapsed';
|
|
1604
|
-
}
|
|
1605
|
-
if (next === 'thin') {
|
|
1606
|
-
return {
|
|
1607
|
-
['--peek-sidebar-width' as any]: `${thinSize}px`,
|
|
1608
|
-
} as React.CSSProperties;
|
|
1609
|
-
}
|
|
1610
|
-
return {
|
|
1611
|
-
['--peek-sidebar-width' as any]: `var(--sidebar-size, ${expandedSize}px)`,
|
|
1612
|
-
} as React.CSSProperties;
|
|
1613
|
-
})()
|
|
1614
|
-
: {}),
|
|
1615
|
-
}}
|
|
1616
|
-
>
|
|
1617
|
-
<div className="rt-ShellSidebarContent" data-visible={isContentVisible || undefined}>
|
|
1618
|
-
{contentChildren}
|
|
1619
|
-
</div>
|
|
1620
|
-
{handleEl}
|
|
1621
|
-
</div>
|
|
1622
|
-
);
|
|
1623
|
-
},
|
|
1624
|
-
) as SidebarComponent;
|
|
1625
|
-
Sidebar.displayName = 'Shell.Sidebar';
|
|
1626
|
-
Sidebar.Handle = SidebarHandle;
|
|
1627
|
-
|
|
1628
|
-
// Content (always required)
|
|
1629
|
-
interface ShellContentProps extends React.ComponentPropsWithoutRef<'main'> {}
|
|
1630
|
-
|
|
1631
|
-
const Content = React.forwardRef<HTMLElement, ShellContentProps>(({ className, ...props }, ref) => (
|
|
1632
|
-
<main {...props} ref={ref} className={classNames('rt-ShellContent', className)} />
|
|
1633
|
-
));
|
|
1634
|
-
Content.displayName = 'Shell.Content';
|
|
1635
|
-
|
|
1636
|
-
// Inspector
|
|
1637
|
-
const Inspector = React.forwardRef<HTMLDivElement, PaneProps>(
|
|
1638
|
-
(
|
|
1639
|
-
{
|
|
1640
|
-
className,
|
|
1641
|
-
presentation = { initial: 'overlay', lg: 'fixed' },
|
|
1642
|
-
mode,
|
|
1643
|
-
defaultMode = 'collapsed',
|
|
1644
|
-
onModeChange,
|
|
1645
|
-
expandedSize = 320,
|
|
1646
|
-
minSize = 200,
|
|
1647
|
-
maxSize = 500,
|
|
1648
|
-
resizable = false,
|
|
1649
|
-
collapsible = true,
|
|
1650
|
-
onExpand,
|
|
1651
|
-
onCollapse,
|
|
1652
|
-
onResize,
|
|
1653
|
-
onResizeStart,
|
|
1654
|
-
onResizeEnd,
|
|
1655
|
-
snapPoints,
|
|
1656
|
-
snapTolerance,
|
|
1657
|
-
collapseThreshold,
|
|
1658
|
-
paneId,
|
|
1659
|
-
persistence,
|
|
1660
|
-
children,
|
|
1661
|
-
style,
|
|
1662
|
-
...props
|
|
1663
|
-
},
|
|
1664
|
-
ref,
|
|
1665
|
-
) => {
|
|
1666
|
-
const shell = useShell();
|
|
1667
|
-
const resolvedPresentation = useResponsivePresentation(presentation);
|
|
1668
|
-
const isOverlay = resolvedPresentation === 'overlay';
|
|
1669
|
-
const isStacked = resolvedPresentation === 'stacked';
|
|
1670
|
-
const localRef = React.useRef<HTMLDivElement | null>(null);
|
|
1671
|
-
const setRef = React.useCallback(
|
|
1672
|
-
(node: HTMLDivElement | null) => {
|
|
1673
|
-
localRef.current = node;
|
|
1674
|
-
if (typeof ref === 'function') ref(node);
|
|
1675
|
-
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
1676
|
-
},
|
|
1677
|
-
[ref],
|
|
1678
|
-
);
|
|
1679
|
-
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
1680
|
-
const handleChildren = childArray.filter(
|
|
1681
|
-
(el: React.ReactElement) => React.isValidElement(el) && el.type === InspectorHandle,
|
|
1682
|
-
);
|
|
1683
|
-
const contentChildren = childArray.filter(
|
|
1684
|
-
(el: React.ReactElement) => !(React.isValidElement(el) && el.type === InspectorHandle),
|
|
1685
|
-
);
|
|
1686
|
-
|
|
1687
|
-
// Apply responsive defaultMode only on mount and when breakpoint changes (uncontrolled Inspector)
|
|
1688
|
-
const resolveResponsiveMode = React.useCallback((): PaneMode => {
|
|
1689
|
-
if (typeof defaultMode === 'string') return defaultMode as PaneMode;
|
|
1690
|
-
const dm = defaultMode as Partial<Record<Breakpoint, PaneMode>> | undefined;
|
|
1691
|
-
if (dm && dm[shell.currentBreakpoint as Breakpoint]) {
|
|
1692
|
-
return dm[shell.currentBreakpoint as Breakpoint] as PaneMode;
|
|
1693
|
-
}
|
|
1694
|
-
const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
|
|
1695
|
-
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat(
|
|
1696
|
-
'initial' as Breakpoint,
|
|
1697
|
-
);
|
|
1698
|
-
const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
|
|
1699
|
-
for (let i = startIdx + 1; i < order.length; i++) {
|
|
1700
|
-
const bp = order[i];
|
|
1701
|
-
if (dm && dm[bp]) {
|
|
1702
|
-
return dm[bp] as PaneMode;
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
return 'collapsed';
|
|
1706
|
-
}, [defaultMode, shell.currentBreakpoint]);
|
|
1707
|
-
|
|
1708
|
-
const lastInspectorBpRef = React.useRef<Breakpoint | null>(null);
|
|
1709
|
-
React.useEffect(() => {
|
|
1710
|
-
if (mode !== undefined) return; // controlled wins
|
|
1711
|
-
if (!shell.currentBreakpointReady) return; // avoid SSR mismatch
|
|
1712
|
-
if (lastInspectorBpRef.current === shell.currentBreakpoint) return; // only on bp change
|
|
1713
|
-
lastInspectorBpRef.current = shell.currentBreakpoint as Breakpoint;
|
|
1714
|
-
const next = resolveResponsiveMode();
|
|
1715
|
-
if (next !== shell.inspectorMode) {
|
|
1716
|
-
shell.setInspectorMode(next);
|
|
1717
|
-
}
|
|
1718
|
-
}, [
|
|
1719
|
-
mode,
|
|
1720
|
-
shell.currentBreakpoint,
|
|
1721
|
-
shell.currentBreakpointReady,
|
|
1722
|
-
resolveResponsiveMode,
|
|
1723
|
-
shell.inspectorMode,
|
|
1724
|
-
shell.setInspectorMode,
|
|
1725
|
-
]);
|
|
1726
|
-
|
|
1727
|
-
// Sync controlled mode
|
|
1728
|
-
React.useEffect(() => {
|
|
1729
|
-
if (mode !== undefined && shell.inspectorMode !== mode) {
|
|
1730
|
-
shell.setInspectorMode(mode);
|
|
1731
|
-
}
|
|
1732
|
-
}, [mode, shell]);
|
|
1733
|
-
|
|
1734
|
-
// Emit mode changes
|
|
1735
|
-
React.useEffect(() => {
|
|
1736
|
-
if (mode === undefined) {
|
|
1737
|
-
onModeChange?.(shell.inspectorMode);
|
|
1738
|
-
}
|
|
1739
|
-
}, [shell.inspectorMode, mode, onModeChange]);
|
|
1740
|
-
|
|
1741
|
-
// Emit expand/collapse events
|
|
1742
|
-
React.useEffect(() => {
|
|
1743
|
-
if (shell.inspectorMode === 'expanded') {
|
|
1744
|
-
onExpand?.();
|
|
1745
|
-
} else {
|
|
1746
|
-
onCollapse?.();
|
|
1747
|
-
}
|
|
1748
|
-
}, [shell.inspectorMode, onExpand, onCollapse]);
|
|
1749
|
-
|
|
1750
|
-
const isExpanded = shell.inspectorMode === 'expanded';
|
|
1751
|
-
|
|
1752
|
-
// Default persistence if paneId provided and none supplied (fixed only)
|
|
1753
|
-
const persistenceAdapter = React.useMemo(() => {
|
|
1754
|
-
if (!paneId || persistence) return persistence;
|
|
1755
|
-
const key = `kookie-ui:shell:inspector:${paneId}`;
|
|
1756
|
-
const adapter: PaneSizePersistence = {
|
|
1757
|
-
load: () => {
|
|
1758
|
-
if (typeof window === 'undefined') return undefined;
|
|
1759
|
-
const v = window.localStorage.getItem(key);
|
|
1760
|
-
return v ? Number(v) : undefined;
|
|
1761
|
-
},
|
|
1762
|
-
save: (size: number) => {
|
|
1763
|
-
if (typeof window === 'undefined') return;
|
|
1764
|
-
window.localStorage.setItem(key, String(size));
|
|
1765
|
-
},
|
|
1766
|
-
};
|
|
1767
|
-
return adapter;
|
|
1768
|
-
}, [paneId, persistence]);
|
|
1769
|
-
|
|
1770
|
-
React.useEffect(() => {
|
|
1771
|
-
let mounted = true;
|
|
1772
|
-
(async () => {
|
|
1773
|
-
if (!resizable || !persistenceAdapter?.load || isOverlay) return;
|
|
1774
|
-
const loaded = await persistenceAdapter.load();
|
|
1775
|
-
if (mounted && typeof loaded === 'number' && localRef.current) {
|
|
1776
|
-
localRef.current.style.setProperty('--inspector-size', `${loaded}px`);
|
|
1777
|
-
onResize?.(loaded);
|
|
1778
|
-
}
|
|
1779
|
-
})();
|
|
1780
|
-
return () => {
|
|
1781
|
-
mounted = false;
|
|
1782
|
-
};
|
|
1783
|
-
}, [resizable, persistenceAdapter, onResize, isOverlay]);
|
|
1784
|
-
|
|
1785
|
-
const handleEl =
|
|
1786
|
-
resizable && !isOverlay && isExpanded ? (
|
|
1787
|
-
<PaneResizeContext.Provider
|
|
1788
|
-
value={{
|
|
1789
|
-
containerRef: localRef,
|
|
1790
|
-
cssVarName: '--inspector-size',
|
|
1791
|
-
minSize,
|
|
1792
|
-
maxSize,
|
|
1793
|
-
defaultSize: expandedSize,
|
|
1794
|
-
orientation: 'vertical',
|
|
1795
|
-
edge: 'start',
|
|
1796
|
-
computeNext: (client, startClient, startSize) => {
|
|
1797
|
-
const isRtl = getComputedStyle(localRef.current!).direction === 'rtl';
|
|
1798
|
-
const delta = client - startClient;
|
|
1799
|
-
// start edge; reverse for LTR
|
|
1800
|
-
return startSize + (isRtl ? delta : -delta);
|
|
1801
|
-
},
|
|
1802
|
-
onResize,
|
|
1803
|
-
onResizeStart,
|
|
1804
|
-
onResizeEnd: (size) => {
|
|
1805
|
-
onResizeEnd?.(size);
|
|
1806
|
-
persistenceAdapter?.save?.(size);
|
|
1807
|
-
},
|
|
1808
|
-
target: 'inspector',
|
|
1809
|
-
collapsible,
|
|
1810
|
-
snapPoints,
|
|
1811
|
-
snapTolerance: snapTolerance ?? 8,
|
|
1812
|
-
collapseThreshold,
|
|
1813
|
-
requestCollapse: () => shell.setInspectorMode('collapsed'),
|
|
1814
|
-
requestToggle: () => shell.togglePane('inspector'),
|
|
1815
|
-
}}
|
|
1816
|
-
>
|
|
1817
|
-
{handleChildren.length > 0 ? (
|
|
1818
|
-
handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i }))
|
|
1819
|
-
) : (
|
|
1820
|
-
<PaneHandle />
|
|
1821
|
-
)}
|
|
1822
|
-
</PaneResizeContext.Provider>
|
|
1823
|
-
) : null;
|
|
1824
|
-
|
|
1825
|
-
if (isOverlay) {
|
|
1826
|
-
const open = shell.inspectorMode === 'expanded';
|
|
1827
|
-
return (
|
|
1828
|
-
<Sheet.Root
|
|
1829
|
-
open={open}
|
|
1830
|
-
onOpenChange={(o) => shell.setInspectorMode(o ? 'expanded' : 'collapsed')}
|
|
1831
|
-
>
|
|
1832
|
-
<Sheet.Content side="end" style={{ padding: 0 }} width={{ initial: `${expandedSize}px` }}>
|
|
1833
|
-
<VisuallyHidden>
|
|
1834
|
-
<Sheet.Title>Inspector</Sheet.Title>
|
|
1835
|
-
</VisuallyHidden>
|
|
1836
|
-
{contentChildren}
|
|
1837
|
-
</Sheet.Content>
|
|
1838
|
-
</Sheet.Root>
|
|
1839
|
-
);
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
return (
|
|
1843
|
-
<div
|
|
1844
|
-
{...props}
|
|
1845
|
-
ref={setRef}
|
|
1846
|
-
className={classNames('rt-ShellInspector', className)}
|
|
1847
|
-
data-mode={shell.inspectorMode}
|
|
1848
|
-
data-peek={shell.peekTarget === 'inspector' || undefined}
|
|
1849
|
-
data-presentation={resolvedPresentation}
|
|
1850
|
-
data-open={(isStacked && isExpanded) || undefined}
|
|
1851
|
-
style={{
|
|
1852
|
-
...style,
|
|
1853
|
-
['--inspector-size' as any]: `${expandedSize}px`,
|
|
1854
|
-
['--inspector-min-size' as any]: `${minSize}px`,
|
|
1855
|
-
['--inspector-max-size' as any]: `${maxSize}px`,
|
|
1856
|
-
}}
|
|
1857
|
-
>
|
|
1858
|
-
<div className="rt-ShellInspectorContent" data-visible={isExpanded || undefined}>
|
|
1859
|
-
{contentChildren}
|
|
1860
|
-
</div>
|
|
1861
|
-
{handleEl}
|
|
1862
|
-
</div>
|
|
1863
|
-
);
|
|
1864
|
-
},
|
|
1865
|
-
) as InspectorComponent;
|
|
1866
|
-
Inspector.displayName = 'Shell.Inspector';
|
|
1867
|
-
Inspector.Handle = InspectorHandle;
|
|
1868
|
-
|
|
1869
|
-
// Bottom
|
|
1870
|
-
const Bottom = React.forwardRef<HTMLDivElement, PaneProps>(
|
|
1871
|
-
(
|
|
1872
|
-
{
|
|
1873
|
-
className,
|
|
1874
|
-
presentation = 'fixed', // Bottom is usually fixed
|
|
1875
|
-
mode,
|
|
1876
|
-
defaultMode = 'collapsed',
|
|
1877
|
-
onModeChange,
|
|
1878
|
-
expandedSize = 200,
|
|
1879
|
-
minSize = 100,
|
|
1880
|
-
maxSize = 400,
|
|
1881
|
-
resizable = false,
|
|
1882
|
-
collapsible = true,
|
|
1883
|
-
onExpand,
|
|
1884
|
-
onCollapse,
|
|
1885
|
-
onResize,
|
|
1886
|
-
onResizeStart,
|
|
1887
|
-
onResizeEnd,
|
|
1888
|
-
snapPoints,
|
|
1889
|
-
snapTolerance,
|
|
1890
|
-
collapseThreshold,
|
|
1891
|
-
paneId,
|
|
1892
|
-
persistence,
|
|
1893
|
-
children,
|
|
1894
|
-
style,
|
|
1895
|
-
...props
|
|
1896
|
-
},
|
|
1897
|
-
ref,
|
|
1898
|
-
) => {
|
|
1899
|
-
const shell = useShell();
|
|
1900
|
-
const resolvedPresentation = useResponsivePresentation(presentation);
|
|
1901
|
-
const isOverlay = resolvedPresentation === 'overlay';
|
|
1902
|
-
const isStacked = resolvedPresentation === 'stacked';
|
|
1903
|
-
const localRef = React.useRef<HTMLDivElement | null>(null);
|
|
1904
|
-
const setRef = React.useCallback(
|
|
1905
|
-
(node: HTMLDivElement | null) => {
|
|
1906
|
-
localRef.current = node;
|
|
1907
|
-
if (typeof ref === 'function') ref(node);
|
|
1908
|
-
else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
|
|
1909
|
-
},
|
|
1910
|
-
[ref],
|
|
1911
|
-
);
|
|
1912
|
-
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
1913
|
-
const handleChildren = childArray.filter(
|
|
1914
|
-
(el: React.ReactElement) => React.isValidElement(el) && el.type === BottomHandle,
|
|
1915
|
-
);
|
|
1916
|
-
const contentChildren = childArray.filter(
|
|
1917
|
-
(el: React.ReactElement) => !(React.isValidElement(el) && el.type === BottomHandle),
|
|
1918
|
-
);
|
|
1919
|
-
|
|
1920
|
-
// Resolve responsive defaultMode for uncontrolled Bottom (on mount and breakpoint change)
|
|
1921
|
-
const resolveResponsiveMode = React.useCallback((): PaneMode => {
|
|
1922
|
-
if (typeof defaultMode === 'string') return defaultMode as PaneMode;
|
|
1923
|
-
const dm = defaultMode as Partial<Record<Breakpoint, PaneMode>> | undefined;
|
|
1924
|
-
if (dm && dm[shell.currentBreakpoint as Breakpoint]) {
|
|
1925
|
-
return dm[shell.currentBreakpoint as Breakpoint] as PaneMode;
|
|
1926
|
-
}
|
|
1927
|
-
const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
|
|
1928
|
-
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat(
|
|
1929
|
-
'initial' as Breakpoint,
|
|
1930
|
-
);
|
|
1931
|
-
const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
|
|
1932
|
-
for (let i = startIdx + 1; i < order.length; i++) {
|
|
1933
|
-
const bp = order[i];
|
|
1934
|
-
if (dm && dm[bp]) {
|
|
1935
|
-
return dm[bp] as PaneMode;
|
|
1936
|
-
}
|
|
1937
|
-
}
|
|
1938
|
-
return 'collapsed';
|
|
1939
|
-
}, [defaultMode, shell.currentBreakpoint]);
|
|
1940
|
-
|
|
1941
|
-
// Honor defaultMode on mount when uncontrolled (including responsive objects)
|
|
1942
|
-
const didInitRef = React.useRef(false);
|
|
1943
|
-
React.useEffect(() => {
|
|
1944
|
-
if (didInitRef.current) return;
|
|
1945
|
-
didInitRef.current = true;
|
|
1946
|
-
if (mode === undefined) {
|
|
1947
|
-
const initial = resolveResponsiveMode();
|
|
1948
|
-
if (shell.bottomMode !== initial) shell.setBottomMode(initial);
|
|
1949
|
-
}
|
|
1950
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1951
|
-
}, []);
|
|
1952
|
-
|
|
1953
|
-
// Apply responsive defaultMode on breakpoint change when uncontrolled
|
|
1954
|
-
const lastBottomBpRef = React.useRef<Breakpoint | null>(null);
|
|
1955
|
-
const lastResolvedBottomRef = React.useRef<PaneMode | null>(null);
|
|
1956
|
-
React.useEffect(() => {
|
|
1957
|
-
if (mode !== undefined) return; // controlled wins
|
|
1958
|
-
if (!shell.currentBreakpointReady) return; // avoid SSR mismatch
|
|
1959
|
-
if (lastBottomBpRef.current === shell.currentBreakpoint) return; // only on bp change
|
|
1960
|
-
lastBottomBpRef.current = shell.currentBreakpoint as Breakpoint;
|
|
1961
|
-
const next = resolveResponsiveMode();
|
|
1962
|
-
if (lastResolvedBottomRef.current === next) return; // no-op transition
|
|
1963
|
-
lastResolvedBottomRef.current = next;
|
|
1964
|
-
if (next !== shell.bottomMode) shell.setBottomMode(next);
|
|
1965
|
-
}, [
|
|
1966
|
-
mode,
|
|
1967
|
-
shell.currentBreakpoint,
|
|
1968
|
-
shell.currentBreakpointReady,
|
|
1969
|
-
resolveResponsiveMode,
|
|
1970
|
-
shell.bottomMode,
|
|
1971
|
-
shell.setBottomMode,
|
|
1972
|
-
]);
|
|
1973
|
-
|
|
1974
|
-
// Sync controlled mode
|
|
1975
|
-
React.useEffect(() => {
|
|
1976
|
-
if (mode !== undefined && shell.bottomMode !== mode) {
|
|
1977
|
-
shell.setBottomMode(mode);
|
|
1978
|
-
}
|
|
1979
|
-
}, [mode, shell]);
|
|
1980
|
-
|
|
1981
|
-
// Emit mode changes
|
|
1982
|
-
React.useEffect(() => {
|
|
1983
|
-
if (mode === undefined) {
|
|
1984
|
-
onModeChange?.(shell.bottomMode);
|
|
1985
|
-
}
|
|
1986
|
-
}, [shell.bottomMode, mode, onModeChange]);
|
|
1987
|
-
|
|
1988
|
-
// Emit expand/collapse events
|
|
1989
|
-
React.useEffect(() => {
|
|
1990
|
-
if (shell.bottomMode === 'expanded') {
|
|
1991
|
-
onExpand?.();
|
|
1992
|
-
} else {
|
|
1993
|
-
onCollapse?.();
|
|
1994
|
-
}
|
|
1995
|
-
}, [shell.bottomMode, onExpand, onCollapse]);
|
|
1996
|
-
|
|
1997
|
-
const isExpanded = shell.bottomMode === 'expanded';
|
|
1998
|
-
|
|
1999
|
-
// Default persistence if paneId provided and none supplied (fixed only)
|
|
2000
|
-
const persistenceAdapter = React.useMemo(() => {
|
|
2001
|
-
if (!paneId || persistence) return persistence;
|
|
2002
|
-
const key = `kookie-ui:shell:bottom:${paneId}`;
|
|
2003
|
-
const adapter: PaneSizePersistence = {
|
|
2004
|
-
load: () => {
|
|
2005
|
-
if (typeof window === 'undefined') return undefined;
|
|
2006
|
-
const v = window.localStorage.getItem(key);
|
|
2007
|
-
return v ? Number(v) : undefined;
|
|
2008
|
-
},
|
|
2009
|
-
save: (size: number) => {
|
|
2010
|
-
if (typeof window === 'undefined') return;
|
|
2011
|
-
window.localStorage.setItem(key, String(size));
|
|
2012
|
-
},
|
|
2013
|
-
};
|
|
2014
|
-
return adapter;
|
|
2015
|
-
}, [paneId, persistence]);
|
|
2016
|
-
|
|
2017
|
-
React.useEffect(() => {
|
|
2018
|
-
let mounted = true;
|
|
2019
|
-
(async () => {
|
|
2020
|
-
if (!resizable || !persistenceAdapter?.load || isOverlay) return;
|
|
2021
|
-
const loaded = await persistenceAdapter.load();
|
|
2022
|
-
if (mounted && typeof loaded === 'number' && localRef.current) {
|
|
2023
|
-
localRef.current.style.setProperty('--bottom-size', `${loaded}px`);
|
|
2024
|
-
onResize?.(loaded);
|
|
2025
|
-
}
|
|
2026
|
-
})();
|
|
2027
|
-
return () => {
|
|
2028
|
-
mounted = false;
|
|
2029
|
-
};
|
|
2030
|
-
}, [resizable, persistenceAdapter, onResize, isOverlay]);
|
|
2031
|
-
|
|
2032
|
-
const handleEl =
|
|
2033
|
-
resizable && !isOverlay && isExpanded ? (
|
|
2034
|
-
<PaneResizeContext.Provider
|
|
2035
|
-
value={{
|
|
2036
|
-
containerRef: localRef,
|
|
2037
|
-
cssVarName: '--bottom-size',
|
|
2038
|
-
minSize,
|
|
2039
|
-
maxSize,
|
|
2040
|
-
defaultSize: expandedSize,
|
|
2041
|
-
orientation: 'horizontal',
|
|
2042
|
-
edge: 'start',
|
|
2043
|
-
computeNext: (client, startClient, startSize) => {
|
|
2044
|
-
const delta = client - startClient;
|
|
2045
|
-
return startSize - delta; // drag up reduces size
|
|
2046
|
-
},
|
|
2047
|
-
onResize,
|
|
2048
|
-
onResizeStart,
|
|
2049
|
-
onResizeEnd: (size) => {
|
|
2050
|
-
onResizeEnd?.(size);
|
|
2051
|
-
persistenceAdapter?.save?.(size);
|
|
2052
|
-
},
|
|
2053
|
-
target: 'bottom',
|
|
2054
|
-
collapsible,
|
|
2055
|
-
snapPoints,
|
|
2056
|
-
snapTolerance: snapTolerance ?? 8,
|
|
2057
|
-
collapseThreshold,
|
|
2058
|
-
requestCollapse: () => shell.setBottomMode('collapsed'),
|
|
2059
|
-
requestToggle: () => shell.togglePane('bottom'),
|
|
2060
|
-
}}
|
|
2061
|
-
>
|
|
2062
|
-
{handleChildren.length > 0 ? (
|
|
2063
|
-
handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i }))
|
|
2064
|
-
) : (
|
|
2065
|
-
<PaneHandle />
|
|
2066
|
-
)}
|
|
2067
|
-
</PaneResizeContext.Provider>
|
|
2068
|
-
) : null;
|
|
2069
|
-
|
|
2070
|
-
if (isOverlay) {
|
|
2071
|
-
const open = shell.bottomMode === 'expanded';
|
|
2072
|
-
return (
|
|
2073
|
-
<Sheet.Root
|
|
2074
|
-
open={open}
|
|
2075
|
-
onOpenChange={(o) => shell.setBottomMode(o ? 'expanded' : 'collapsed')}
|
|
2076
|
-
>
|
|
2077
|
-
<Sheet.Content
|
|
2078
|
-
side="bottom"
|
|
2079
|
-
style={{ padding: 0 }}
|
|
2080
|
-
height={{ initial: `${expandedSize}px` }}
|
|
2081
|
-
>
|
|
2082
|
-
<VisuallyHidden>
|
|
2083
|
-
<Sheet.Title>Bottom panel</Sheet.Title>
|
|
2084
|
-
</VisuallyHidden>
|
|
2085
|
-
{contentChildren}
|
|
2086
|
-
</Sheet.Content>
|
|
2087
|
-
</Sheet.Root>
|
|
2088
|
-
);
|
|
2089
|
-
}
|
|
2090
|
-
|
|
2091
|
-
return (
|
|
2092
|
-
<div
|
|
2093
|
-
{...props}
|
|
2094
|
-
ref={setRef}
|
|
2095
|
-
className={classNames('rt-ShellBottom', className)}
|
|
2096
|
-
data-mode={shell.bottomMode}
|
|
2097
|
-
data-peek={shell.peekTarget === 'bottom' || undefined}
|
|
2098
|
-
data-presentation={resolvedPresentation}
|
|
2099
|
-
data-open={(isStacked && isExpanded) || undefined}
|
|
2100
|
-
style={{
|
|
2101
|
-
...style,
|
|
2102
|
-
['--bottom-size' as any]: `${expandedSize}px`,
|
|
2103
|
-
['--bottom-min-size' as any]: `${minSize}px`,
|
|
2104
|
-
['--bottom-max-size' as any]: `${maxSize}px`,
|
|
2105
|
-
}}
|
|
2106
|
-
>
|
|
2107
|
-
<div className="rt-ShellBottomContent" data-visible={isExpanded || undefined}>
|
|
2108
|
-
{contentChildren}
|
|
2109
|
-
</div>
|
|
2110
|
-
{handleEl}
|
|
2111
|
-
</div>
|
|
2112
|
-
);
|
|
2113
|
-
},
|
|
2114
|
-
) as BottomComponent;
|
|
2115
|
-
Bottom.displayName = 'Shell.Bottom';
|
|
2116
|
-
Bottom.Handle = BottomHandle;
|
|
2117
|
-
|
|
2118
|
-
// Trigger
|
|
2119
|
-
type PaneTarget = 'left' | 'rail' | 'panel' | 'sidebar' | 'inspector' | 'bottom';
|
|
2120
|
-
type TriggerAction = 'toggle' | 'expand' | 'collapse';
|
|
845
|
+
// Trigger
|
|
846
|
+
// PaneTarget type moved to shell.types.ts
|
|
847
|
+
type TriggerAction = 'toggle' | 'expand' | 'collapse';
|
|
2121
848
|
|
|
2122
849
|
interface TriggerProps extends React.ComponentPropsWithoutRef<'button'> {
|
|
2123
850
|
target: PaneTarget;
|
|
@@ -2129,98 +856,76 @@ interface TriggerProps extends React.ComponentPropsWithoutRef<'button'> {
|
|
|
2129
856
|
peekOnHover?: boolean;
|
|
2130
857
|
}
|
|
2131
858
|
|
|
2132
|
-
const Trigger = React.forwardRef<HTMLButtonElement, TriggerProps>(
|
|
2133
|
-
(
|
|
2134
|
-
{
|
|
2135
|
-
target,
|
|
2136
|
-
action = 'toggle',
|
|
2137
|
-
peekOnHover,
|
|
2138
|
-
onClick,
|
|
2139
|
-
onMouseEnter,
|
|
2140
|
-
onMouseLeave,
|
|
2141
|
-
children,
|
|
2142
|
-
...props
|
|
2143
|
-
},
|
|
2144
|
-
ref,
|
|
2145
|
-
) => {
|
|
2146
|
-
const shell = useShell();
|
|
2147
|
-
|
|
2148
|
-
const handleClick = React.useCallback(
|
|
2149
|
-
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
2150
|
-
onClick?.(event);
|
|
2151
|
-
|
|
2152
|
-
// Clear any active peek on this target before toggling to avoid sticky peek state
|
|
2153
|
-
if ((shell as any).peekTarget === target) {
|
|
2154
|
-
shell.clearPeek();
|
|
2155
|
-
}
|
|
859
|
+
const Trigger = React.forwardRef<HTMLButtonElement, TriggerProps>(({ target, action = 'toggle', peekOnHover, onClick, onMouseEnter, onMouseLeave, children, ...props }, ref) => {
|
|
860
|
+
const shell = useShell();
|
|
2156
861
|
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
break;
|
|
2161
|
-
case 'expand':
|
|
2162
|
-
shell.expandPane(target);
|
|
2163
|
-
break;
|
|
2164
|
-
case 'collapse':
|
|
2165
|
-
shell.collapsePane(target);
|
|
2166
|
-
break;
|
|
2167
|
-
}
|
|
2168
|
-
},
|
|
2169
|
-
[shell, target, action, onClick],
|
|
2170
|
-
);
|
|
862
|
+
const handleClick = React.useCallback(
|
|
863
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
864
|
+
onClick?.(event);
|
|
2171
865
|
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
case 'rail':
|
|
2176
|
-
return shell.leftMode === 'collapsed';
|
|
2177
|
-
case 'panel':
|
|
2178
|
-
return shell.leftMode === 'collapsed' || shell.panelMode === 'collapsed';
|
|
2179
|
-
case 'sidebar':
|
|
2180
|
-
return shell.sidebarMode === 'collapsed';
|
|
2181
|
-
case 'inspector':
|
|
2182
|
-
return shell.inspectorMode === 'collapsed';
|
|
2183
|
-
case 'bottom':
|
|
2184
|
-
return shell.bottomMode === 'collapsed';
|
|
866
|
+
// Clear any active peek on this target before toggling to avoid sticky peek state
|
|
867
|
+
if ((shell as any).peekTarget === target) {
|
|
868
|
+
shell.clearPeek();
|
|
2185
869
|
}
|
|
2186
|
-
})();
|
|
2187
|
-
|
|
2188
|
-
const handleMouseEnter = React.useCallback(
|
|
2189
|
-
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
2190
|
-
onMouseEnter?.(event);
|
|
2191
|
-
if (!peekOnHover || !isCollapsed) return;
|
|
2192
|
-
// Use the actual target for peek behavior (not mapped to left)
|
|
2193
|
-
shell.peekPane(target);
|
|
2194
|
-
},
|
|
2195
|
-
[onMouseEnter, peekOnHover, isCollapsed, shell, target],
|
|
2196
|
-
);
|
|
2197
870
|
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
shell.
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
871
|
+
switch (action) {
|
|
872
|
+
case 'toggle':
|
|
873
|
+
shell.togglePane(target);
|
|
874
|
+
break;
|
|
875
|
+
case 'expand':
|
|
876
|
+
shell.expandPane(target);
|
|
877
|
+
break;
|
|
878
|
+
case 'collapse':
|
|
879
|
+
shell.collapsePane(target);
|
|
880
|
+
break;
|
|
881
|
+
}
|
|
882
|
+
},
|
|
883
|
+
[shell, target, action, onClick],
|
|
884
|
+
);
|
|
885
|
+
|
|
886
|
+
const isCollapsed = (() => {
|
|
887
|
+
switch (target) {
|
|
888
|
+
case 'left':
|
|
889
|
+
case 'rail':
|
|
890
|
+
return shell.leftMode === 'collapsed';
|
|
891
|
+
case 'panel':
|
|
892
|
+
return shell.leftMode === 'collapsed' || shell.panelMode === 'collapsed';
|
|
893
|
+
case 'sidebar':
|
|
894
|
+
return shell.sidebarMode === 'collapsed';
|
|
895
|
+
case 'inspector':
|
|
896
|
+
return shell.inspectorMode === 'collapsed';
|
|
897
|
+
case 'bottom':
|
|
898
|
+
return shell.bottomMode === 'collapsed';
|
|
899
|
+
}
|
|
900
|
+
})();
|
|
901
|
+
|
|
902
|
+
const handleMouseEnter = React.useCallback(
|
|
903
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
904
|
+
onMouseEnter?.(event);
|
|
905
|
+
if (!peekOnHover || !isCollapsed) return;
|
|
906
|
+
// Use the actual target for peek behavior (not mapped to left)
|
|
907
|
+
shell.peekPane(target);
|
|
908
|
+
},
|
|
909
|
+
[onMouseEnter, peekOnHover, isCollapsed, shell, target],
|
|
910
|
+
);
|
|
2208
911
|
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
912
|
+
const handleMouseLeave = React.useCallback(
|
|
913
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
914
|
+
onMouseLeave?.(event);
|
|
915
|
+
if (!peekOnHover) return;
|
|
916
|
+
if ((shell as any).peekTarget === target) {
|
|
917
|
+
shell.clearPeek();
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
[onMouseLeave, peekOnHover, shell, target],
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
return (
|
|
924
|
+
<button {...props} ref={ref} onClick={handleClick} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} data-shell-trigger={target} data-shell-action={action}>
|
|
925
|
+
{children}
|
|
926
|
+
</button>
|
|
927
|
+
);
|
|
928
|
+
});
|
|
2224
929
|
Trigger.displayName = 'Shell.Trigger';
|
|
2225
930
|
|
|
2226
931
|
// Exports
|