@kushagradhawan/kookie-ui 0.1.47 → 0.1.49
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 +858 -30
- 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/alert-dialog.d.ts.map +1 -1
- package/dist/cjs/components/alert-dialog.js +1 -1
- package/dist/cjs/components/alert-dialog.js.map +2 -2
- package/dist/cjs/components/dialog.d.ts.map +1 -1
- package/dist/cjs/components/dialog.js +1 -1
- package/dist/cjs/components/dialog.js.map +2 -2
- 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 +37 -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 +1 -1
- 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/alert-dialog.d.ts.map +1 -1
- package/dist/esm/components/alert-dialog.js +1 -1
- package/dist/esm/components/alert-dialog.js.map +2 -2
- package/dist/esm/components/dialog.d.ts.map +1 -1
- package/dist/esm/components/dialog.js +1 -1
- package/dist/esm/components/dialog.js.map +2 -2
- 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 +37 -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 +1 -1
- 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 +2 -2
- 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/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 +347 -0
- package/src/components/alert-dialog.tsx +6 -0
- package/src/components/dialog.tsx +6 -0
- package/src/components/schemas/index.ts +46 -0
- package/src/components/schemas/shell.schema.ts +403 -0
- package/src/components/shell.context.tsx +56 -0
- package/src/components/shell.css +5 -17
- package/src/components/shell.hooks.ts +31 -0
- package/src/components/shell.tsx +368 -1684
- package/src/components/shell.types.ts +27 -0
- package/src/components/sidebar.tsx +1 -1
- package/src/styles/tokens/blur.css +2 -2
- package/src/styles/tokens/color.css +2 -2
- package/styles.css +1031 -116
- 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,266 @@ 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
|
+
const [inspectorMode, setInspectorMode] = React.useState<PaneMode>('collapsed');
|
|
103
|
+
const [bottomMode, setBottomMode] = React.useState<PaneMode>('collapsed');
|
|
456
104
|
|
|
457
|
-
|
|
105
|
+
// Removed: defaultMode responsiveness and manual change tracking
|
|
458
106
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
107
|
+
// Composition detection
|
|
108
|
+
const [hasLeft, setHasLeft] = React.useState(false);
|
|
109
|
+
const [hasSidebar, setHasSidebar] = React.useState(false);
|
|
462
110
|
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
const setSidebarToggleComputer = React.useCallback(
|
|
469
|
-
(fn: (current: SidebarMode) => SidebarMode) => {
|
|
470
|
-
sidebarToggleComputerRef.current = fn;
|
|
471
|
-
},
|
|
472
|
-
[],
|
|
473
|
-
);
|
|
111
|
+
// Customizable sidebar toggle sequencing
|
|
112
|
+
const sidebarToggleComputerRef = React.useRef<(current: SidebarMode) => SidebarMode>((current) => (current === 'collapsed' ? 'thin' : current === 'thin' ? 'expanded' : 'collapsed'));
|
|
113
|
+
const setSidebarToggleComputer = React.useCallback((fn: (current: SidebarMode) => SidebarMode) => {
|
|
114
|
+
sidebarToggleComputerRef.current = fn;
|
|
115
|
+
}, []);
|
|
474
116
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
117
|
+
// Left collapse cascades to Panel
|
|
118
|
+
React.useEffect(() => {
|
|
119
|
+
if (leftMode === 'collapsed') {
|
|
120
|
+
setPanelMode('collapsed');
|
|
121
|
+
}
|
|
122
|
+
}, [leftMode]);
|
|
481
123
|
|
|
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]);
|
|
124
|
+
// Composition validation
|
|
125
|
+
React.useEffect(() => {
|
|
126
|
+
if (hasSidebar && hasLeft) {
|
|
127
|
+
console.warn('Shell: Sidebar cannot coexist with Rail or Panel. Use either Rail+Panel OR Sidebar.');
|
|
128
|
+
}
|
|
129
|
+
}, [hasSidebar, hasLeft]);
|
|
130
|
+
|
|
131
|
+
// Left presentation + defaults from children
|
|
132
|
+
const [devLeftPres, setDevLeftPres] = React.useState<PresentationValue | undefined>(undefined);
|
|
133
|
+
const onLeftPres = React.useCallback((p: PresentationValue) => setDevLeftPres(p), []);
|
|
134
|
+
const railDefaultSizeRef = React.useRef<number>(64);
|
|
135
|
+
const panelDefaultSizeRef = React.useRef<number>(288);
|
|
136
|
+
const onRailDefaults = React.useCallback((size: number) => {
|
|
137
|
+
railDefaultSizeRef.current = size;
|
|
138
|
+
}, []);
|
|
139
|
+
const onPanelDefaults = React.useCallback((size: number) => {
|
|
140
|
+
panelDefaultSizeRef.current = size;
|
|
141
|
+
}, []);
|
|
511
142
|
|
|
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
|
-
);
|
|
143
|
+
// Determine children presence for left composition
|
|
144
|
+
const hasLeftChildren = React.useMemo(() => {
|
|
145
|
+
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
146
|
+
const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && (el.type === comp || (el as any).type?.displayName === comp.displayName);
|
|
147
|
+
return childArray.some((el) => isType(el, Rail) || isType(el, Panel));
|
|
148
|
+
}, [children]);
|
|
549
149
|
|
|
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
|
-
}, []);
|
|
150
|
+
const hasSidebarChildren = React.useMemo(() => {
|
|
151
|
+
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
152
|
+
const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && (el.type === comp || (el as any).type?.displayName === comp.displayName);
|
|
153
|
+
return childArray.some((el) => isType(el, Sidebar));
|
|
154
|
+
}, [children]);
|
|
571
155
|
|
|
572
|
-
|
|
156
|
+
const togglePane = React.useCallback(
|
|
157
|
+
(target: PaneTarget) => {
|
|
573
158
|
switch (target) {
|
|
574
159
|
case 'left':
|
|
575
160
|
case 'rail':
|
|
576
|
-
setLeftMode('collapsed');
|
|
161
|
+
setLeftMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
577
162
|
break;
|
|
578
163
|
case 'panel':
|
|
579
|
-
|
|
164
|
+
// Panel toggle: expand left if collapsed, then toggle panel
|
|
165
|
+
if (leftMode === 'collapsed') {
|
|
166
|
+
setLeftMode('expanded');
|
|
167
|
+
setPanelMode('expanded');
|
|
168
|
+
} else {
|
|
169
|
+
setPanelMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
170
|
+
}
|
|
580
171
|
break;
|
|
581
172
|
case 'sidebar':
|
|
582
|
-
setSidebarMode(
|
|
173
|
+
setSidebarMode((prev) => sidebarToggleComputerRef.current(prev as SidebarMode));
|
|
583
174
|
break;
|
|
584
175
|
case 'inspector':
|
|
585
|
-
setInspectorMode('collapsed');
|
|
176
|
+
setInspectorMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
586
177
|
break;
|
|
587
178
|
case 'bottom':
|
|
588
|
-
setBottomMode('collapsed');
|
|
179
|
+
setBottomMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
589
180
|
break;
|
|
590
181
|
}
|
|
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
|
-
);
|
|
182
|
+
},
|
|
183
|
+
[leftMode],
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
const expandPane = React.useCallback((target: PaneTarget) => {
|
|
187
|
+
switch (target) {
|
|
188
|
+
case 'left':
|
|
189
|
+
case 'rail':
|
|
190
|
+
setLeftMode('expanded');
|
|
191
|
+
break;
|
|
192
|
+
case 'panel':
|
|
193
|
+
setLeftMode('expanded');
|
|
194
|
+
setPanelMode('expanded');
|
|
195
|
+
break;
|
|
196
|
+
case 'sidebar':
|
|
197
|
+
setSidebarMode('expanded');
|
|
198
|
+
break;
|
|
199
|
+
case 'inspector':
|
|
200
|
+
setInspectorMode('expanded');
|
|
201
|
+
break;
|
|
202
|
+
case 'bottom':
|
|
203
|
+
setBottomMode('expanded');
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
}, []);
|
|
640
207
|
|
|
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), []);
|
|
208
|
+
const collapsePane = React.useCallback((target: PaneTarget) => {
|
|
209
|
+
switch (target) {
|
|
210
|
+
case 'left':
|
|
211
|
+
case 'rail':
|
|
212
|
+
setLeftMode('collapsed');
|
|
213
|
+
break;
|
|
214
|
+
case 'panel':
|
|
215
|
+
setPanelMode('collapsed');
|
|
216
|
+
break;
|
|
217
|
+
case 'sidebar':
|
|
218
|
+
setSidebarMode('collapsed');
|
|
219
|
+
break;
|
|
220
|
+
case 'inspector':
|
|
221
|
+
setInspectorMode('collapsed');
|
|
222
|
+
break;
|
|
223
|
+
case 'bottom':
|
|
224
|
+
setBottomMode('collapsed');
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}, []);
|
|
667
228
|
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
229
|
+
const baseContextValue = React.useMemo(
|
|
230
|
+
() => ({
|
|
231
|
+
leftMode,
|
|
232
|
+
setLeftMode,
|
|
233
|
+
panelMode,
|
|
234
|
+
setPanelMode,
|
|
235
|
+
sidebarMode,
|
|
236
|
+
setSidebarMode,
|
|
237
|
+
inspectorMode,
|
|
238
|
+
setInspectorMode,
|
|
239
|
+
bottomMode,
|
|
240
|
+
setBottomMode,
|
|
241
|
+
hasLeft,
|
|
242
|
+
setHasLeft,
|
|
243
|
+
hasSidebar,
|
|
244
|
+
setHasSidebar,
|
|
245
|
+
currentBreakpoint,
|
|
246
|
+
currentBreakpointReady,
|
|
247
|
+
leftResolvedPresentation: devLeftPres,
|
|
248
|
+
togglePane,
|
|
249
|
+
expandPane,
|
|
250
|
+
collapsePane,
|
|
251
|
+
setSidebarToggleComputer,
|
|
252
|
+
onLeftPres,
|
|
253
|
+
onRailDefaults,
|
|
254
|
+
onPanelDefaults,
|
|
255
|
+
}),
|
|
256
|
+
[
|
|
257
|
+
leftMode,
|
|
258
|
+
panelMode,
|
|
259
|
+
sidebarMode,
|
|
260
|
+
inspectorMode,
|
|
261
|
+
bottomMode,
|
|
262
|
+
hasLeft,
|
|
263
|
+
hasSidebar,
|
|
264
|
+
currentBreakpoint,
|
|
265
|
+
currentBreakpointReady,
|
|
266
|
+
devLeftPres,
|
|
267
|
+
togglePane,
|
|
268
|
+
expandPane,
|
|
269
|
+
collapsePane,
|
|
270
|
+
setSidebarToggleComputer,
|
|
271
|
+
onLeftPres,
|
|
272
|
+
onRailDefaults,
|
|
273
|
+
onPanelDefaults,
|
|
274
|
+
],
|
|
275
|
+
);
|
|
276
|
+
|
|
277
|
+
// Organize children by type
|
|
278
|
+
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
279
|
+
const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && (el.type === comp || (el as any).type?.displayName === comp.displayName);
|
|
280
|
+
|
|
281
|
+
const headerEls = childArray.filter((el) => isType(el, Header));
|
|
282
|
+
const railEls = childArray.filter((el) => isType(el, Rail));
|
|
283
|
+
const panelEls = childArray.filter((el) => isType(el, Panel));
|
|
284
|
+
const sidebarEls = childArray.filter((el) => isType(el, Sidebar));
|
|
285
|
+
const contentEls = childArray.filter((el) => isType(el, Content));
|
|
286
|
+
const inspectorEls = childArray.filter((el) => isType(el, Inspector));
|
|
287
|
+
const bottomEls = childArray.filter((el) => isType(el, Bottom));
|
|
288
|
+
|
|
289
|
+
const heightStyle = React.useMemo(() => {
|
|
290
|
+
if (height === 'full') return { height: '100vh' };
|
|
291
|
+
if (height === 'auto') return { height: 'auto' };
|
|
292
|
+
if (typeof height === 'string') return { height };
|
|
293
|
+
if (typeof height === 'number') return { height: `${height}px` };
|
|
294
|
+
return {};
|
|
295
|
+
}, [height]);
|
|
296
|
+
|
|
297
|
+
// Peek state (layout-only overlay without mode changes)
|
|
298
|
+
const [peekTarget, setPeekTarget] = React.useState<PaneTarget | null>(null);
|
|
299
|
+
const peekPane = React.useCallback((target: PaneTarget) => setPeekTarget(target), []);
|
|
300
|
+
const clearPeek = React.useCallback(() => setPeekTarget(null), []);
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<div {...props} ref={ref} className={classNames('rt-ShellRoot', className)} style={{ ...heightStyle, ...props.style }}>
|
|
304
|
+
<ShellProvider
|
|
305
|
+
value={{
|
|
306
|
+
...baseContextValue,
|
|
307
|
+
peekTarget,
|
|
308
|
+
setPeekTarget,
|
|
309
|
+
peekPane,
|
|
310
|
+
clearPeek,
|
|
311
|
+
}}
|
|
674
312
|
>
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
313
|
+
{headerEls}
|
|
314
|
+
<div
|
|
315
|
+
className="rt-ShellBody"
|
|
316
|
+
data-peek-target={peekTarget ?? undefined}
|
|
317
|
+
style={
|
|
318
|
+
peekTarget === 'rail' || peekTarget === 'panel'
|
|
319
|
+
? ({
|
|
320
|
+
['--peek-rail-width' as any]: `${railDefaultSizeRef.current}px`,
|
|
321
|
+
} as React.CSSProperties)
|
|
322
|
+
: undefined
|
|
323
|
+
}
|
|
683
324
|
>
|
|
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
|
-
);
|
|
325
|
+
{hasLeftChildren && !hasSidebarChildren
|
|
326
|
+
? (() => {
|
|
327
|
+
const firstRail = railEls[0] as any;
|
|
328
|
+
const passthroughProps = firstRail
|
|
329
|
+
? {
|
|
330
|
+
mode: firstRail.props?.mode,
|
|
331
|
+
defaultMode: firstRail.props?.defaultMode,
|
|
332
|
+
onModeChange: firstRail.props?.onModeChange,
|
|
333
|
+
presentation: firstRail.props?.presentation,
|
|
334
|
+
collapsible: firstRail.props?.collapsible,
|
|
335
|
+
onExpand: firstRail.props?.onExpand,
|
|
336
|
+
onCollapse: firstRail.props?.onCollapse,
|
|
337
|
+
}
|
|
338
|
+
: {};
|
|
339
|
+
return (
|
|
340
|
+
<Left {...(passthroughProps as any)}>
|
|
341
|
+
{railEls}
|
|
342
|
+
{panelEls}
|
|
343
|
+
</Left>
|
|
344
|
+
);
|
|
345
|
+
})()
|
|
346
|
+
: sidebarEls}
|
|
347
|
+
{contentEls}
|
|
348
|
+
{inspectorEls}
|
|
349
|
+
</div>
|
|
350
|
+
{bottomEls}
|
|
351
|
+
</ShellProvider>
|
|
352
|
+
</div>
|
|
353
|
+
);
|
|
354
|
+
});
|
|
727
355
|
Root.displayName = 'Shell.Root';
|
|
728
356
|
|
|
729
357
|
// Header
|
|
@@ -731,19 +359,17 @@ interface ShellHeaderProps extends React.ComponentPropsWithoutRef<'header'> {
|
|
|
731
359
|
height?: number;
|
|
732
360
|
}
|
|
733
361
|
|
|
734
|
-
const Header = React.forwardRef<HTMLElement, ShellHeaderProps>(
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
style
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
),
|
|
746
|
-
);
|
|
362
|
+
const Header = React.forwardRef<HTMLElement, ShellHeaderProps>(({ className, height = 64, style, ...props }, ref) => (
|
|
363
|
+
<header
|
|
364
|
+
{...props}
|
|
365
|
+
ref={ref}
|
|
366
|
+
className={classNames('rt-ShellHeader', className)}
|
|
367
|
+
style={{
|
|
368
|
+
...style,
|
|
369
|
+
['--shell-header-height' as any]: `${height}px`,
|
|
370
|
+
}}
|
|
371
|
+
/>
|
|
372
|
+
));
|
|
747
373
|
Header.displayName = 'Shell.Header';
|
|
748
374
|
|
|
749
375
|
// Pane Props Interface (shared by Panel, Sidebar, Inspector, Bottom)
|
|
@@ -796,22 +422,7 @@ interface RailProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
|
796
422
|
|
|
797
423
|
// Left container - behaves like Inspector but contains Rail+Panel
|
|
798
424
|
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
|
-
) => {
|
|
425
|
+
({ className, presentation = { initial: 'overlay', sm: 'fixed' }, mode, defaultMode = 'collapsed', onModeChange, collapsible = true, onExpand, onCollapse, children, style, ...props }, ref) => {
|
|
815
426
|
const shell = useShell();
|
|
816
427
|
const resolvedPresentation = useResponsivePresentation(presentation);
|
|
817
428
|
const isOverlay = resolvedPresentation === 'overlay';
|
|
@@ -844,9 +455,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
844
455
|
return dm[shell.currentBreakpoint as Breakpoint] as PaneMode;
|
|
845
456
|
}
|
|
846
457
|
const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
|
|
847
|
-
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat(
|
|
848
|
-
'initial' as Breakpoint,
|
|
849
|
-
);
|
|
458
|
+
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat('initial' as Breakpoint);
|
|
850
459
|
const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
|
|
851
460
|
for (let i = startIdx + 1; i < order.length; i++) {
|
|
852
461
|
const bp = order[i];
|
|
@@ -867,14 +476,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
867
476
|
if (next !== shell.leftMode) {
|
|
868
477
|
shell.setLeftMode(next);
|
|
869
478
|
}
|
|
870
|
-
}, [
|
|
871
|
-
mode,
|
|
872
|
-
shell.currentBreakpoint,
|
|
873
|
-
shell.currentBreakpointReady,
|
|
874
|
-
resolveResponsiveMode,
|
|
875
|
-
shell.leftMode,
|
|
876
|
-
shell.setLeftMode,
|
|
877
|
-
]);
|
|
479
|
+
}, [mode, shell.currentBreakpoint, shell.currentBreakpointReady, resolveResponsiveMode, shell.leftMode, shell.setLeftMode]);
|
|
878
480
|
|
|
879
481
|
// Sync controlled mode
|
|
880
482
|
React.useEffect(() => {
|
|
@@ -907,27 +509,16 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
907
509
|
const open = shell.leftMode === 'expanded';
|
|
908
510
|
// Compute overlay width from child Rail/Panel expanded sizes
|
|
909
511
|
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
910
|
-
const isType = (el: React.ReactElement, comp: any) =>
|
|
911
|
-
React.isValidElement(el) && el.type === comp;
|
|
512
|
+
const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && el.type === comp;
|
|
912
513
|
const railEl = childArray.find((el) => isType(el, Rail));
|
|
913
514
|
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;
|
|
515
|
+
const railSize = typeof (railEl as any)?.props?.expandedSize === 'number' ? (railEl as any).props.expandedSize : 64;
|
|
516
|
+
const panelSize = typeof (panelEl as any)?.props?.expandedSize === 'number' ? (panelEl as any).props.expandedSize : 288;
|
|
922
517
|
const hasRail = Boolean(railEl);
|
|
923
518
|
const hasPanel = Boolean(panelEl);
|
|
924
|
-
const overlayPx =
|
|
925
|
-
(hasRail ? railSize : 0) + (shell.panelMode === 'expanded' && hasPanel ? panelSize : 0);
|
|
519
|
+
const overlayPx = (hasRail ? railSize : 0) + (shell.panelMode === 'expanded' && hasPanel ? panelSize : 0);
|
|
926
520
|
return (
|
|
927
|
-
<Sheet.Root
|
|
928
|
-
open={open}
|
|
929
|
-
onOpenChange={(o) => shell.setLeftMode(o ? 'expanded' : 'collapsed')}
|
|
930
|
-
>
|
|
521
|
+
<Sheet.Root open={open} onOpenChange={(o) => shell.setLeftMode(o ? 'expanded' : 'collapsed')}>
|
|
931
522
|
<Sheet.Content
|
|
932
523
|
side="start"
|
|
933
524
|
style={{ padding: 0 }}
|
|
@@ -948,22 +539,14 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
948
539
|
const open = shell.leftMode === 'expanded';
|
|
949
540
|
// Compute floating width from child Rail/Panel expanded sizes (like overlay)
|
|
950
541
|
const childArray = React.Children.toArray(children) as React.ReactElement[];
|
|
951
|
-
const isType = (el: React.ReactElement, comp: any) =>
|
|
952
|
-
React.isValidElement(el) && el.type === comp;
|
|
542
|
+
const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && el.type === comp;
|
|
953
543
|
const railEl = childArray.find((el) => isType(el, Rail));
|
|
954
544
|
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;
|
|
545
|
+
const railSize = typeof (railEl as any)?.props?.expandedSize === 'number' ? (railEl as any).props.expandedSize : 64;
|
|
546
|
+
const panelSize = typeof (panelEl as any)?.props?.expandedSize === 'number' ? (panelEl as any).props.expandedSize : 288;
|
|
963
547
|
const hasRail = Boolean(railEl);
|
|
964
548
|
const hasPanel = Boolean(panelEl);
|
|
965
|
-
const includePanel =
|
|
966
|
-
hasPanel && (shell.panelMode === 'expanded' || shell.peekTarget === 'panel');
|
|
549
|
+
const includePanel = hasPanel && (shell.panelMode === 'expanded' || shell.peekTarget === 'panel');
|
|
967
550
|
const floatingWidthPx = (hasRail ? railSize : 0) + (includePanel ? panelSize : 0);
|
|
968
551
|
|
|
969
552
|
return (
|
|
@@ -972,12 +555,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
972
555
|
ref={setRef}
|
|
973
556
|
className={classNames('rt-ShellLeft', className)}
|
|
974
557
|
data-mode={shell.leftMode}
|
|
975
|
-
data-peek={
|
|
976
|
-
shell.peekTarget === 'left' ||
|
|
977
|
-
shell.peekTarget === 'rail' ||
|
|
978
|
-
shell.peekTarget === 'panel' ||
|
|
979
|
-
undefined
|
|
980
|
-
}
|
|
558
|
+
data-peek={shell.peekTarget === 'left' || shell.peekTarget === 'rail' || shell.peekTarget === 'panel' || undefined}
|
|
981
559
|
data-presentation={resolvedPresentation}
|
|
982
560
|
style={{
|
|
983
561
|
...style,
|
|
@@ -995,12 +573,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
995
573
|
ref={setRef}
|
|
996
574
|
className={classNames('rt-ShellLeft', className)}
|
|
997
575
|
data-mode={shell.leftMode}
|
|
998
|
-
data-peek={
|
|
999
|
-
shell.peekTarget === 'left' ||
|
|
1000
|
-
shell.peekTarget === 'rail' ||
|
|
1001
|
-
shell.peekTarget === 'panel' ||
|
|
1002
|
-
undefined
|
|
1003
|
-
}
|
|
576
|
+
data-peek={shell.peekTarget === 'left' || shell.peekTarget === 'rail' || shell.peekTarget === 'panel' || undefined}
|
|
1004
577
|
data-presentation={resolvedPresentation}
|
|
1005
578
|
style={{
|
|
1006
579
|
...style,
|
|
@@ -1014,23 +587,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
1014
587
|
Left.displayName = 'Shell.Left';
|
|
1015
588
|
|
|
1016
589
|
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
|
-
) => {
|
|
590
|
+
({ className, presentation, mode, defaultMode, onModeChange, expandedSize = 64, collapsible, onExpand, onCollapse, children, style, ...props }, ref) => {
|
|
1034
591
|
const shell = useShell();
|
|
1035
592
|
|
|
1036
593
|
// Register expanded size with Left container
|
|
@@ -1046,22 +603,13 @@ const Rail = React.forwardRef<HTMLDivElement, RailProps>(
|
|
|
1046
603
|
ref={ref}
|
|
1047
604
|
className={classNames('rt-ShellRail', className)}
|
|
1048
605
|
data-mode={shell.leftMode}
|
|
1049
|
-
data-peek={
|
|
1050
|
-
(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') || undefined
|
|
1051
|
-
}
|
|
606
|
+
data-peek={(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') || undefined}
|
|
1052
607
|
style={{
|
|
1053
608
|
...style,
|
|
1054
609
|
['--rail-size' as any]: `${expandedSize}px`,
|
|
1055
610
|
}}
|
|
1056
611
|
>
|
|
1057
|
-
<div
|
|
1058
|
-
className="rt-ShellRailContent"
|
|
1059
|
-
data-visible={
|
|
1060
|
-
isExpanded ||
|
|
1061
|
-
(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') ||
|
|
1062
|
-
undefined
|
|
1063
|
-
}
|
|
1064
|
-
>
|
|
612
|
+
<div className="rt-ShellRailContent" data-visible={isExpanded || (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') || undefined}>
|
|
1065
613
|
{children}
|
|
1066
614
|
</div>
|
|
1067
615
|
</div>
|
|
@@ -1071,13 +619,9 @@ const Rail = React.forwardRef<HTMLDivElement, RailProps>(
|
|
|
1071
619
|
Rail.displayName = 'Shell.Rail';
|
|
1072
620
|
|
|
1073
621
|
// Panel
|
|
1074
|
-
type HandleComponent = React.ForwardRefExoticComponent<
|
|
1075
|
-
React.ComponentPropsWithoutRef<'div'> & React.RefAttributes<HTMLDivElement>
|
|
1076
|
-
>;
|
|
622
|
+
type HandleComponent = React.ForwardRefExoticComponent<React.ComponentPropsWithoutRef<'div'> & React.RefAttributes<HTMLDivElement>>;
|
|
1077
623
|
|
|
1078
|
-
type PanelComponent = React.ForwardRefExoticComponent<
|
|
1079
|
-
Omit<PaneProps, 'defaultMode'> & React.RefAttributes<HTMLDivElement>
|
|
1080
|
-
> & { Handle: HandleComponent };
|
|
624
|
+
type PanelComponent = React.ForwardRefExoticComponent<Omit<PaneProps, 'defaultMode'> & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
|
|
1081
625
|
|
|
1082
626
|
type SidebarComponent = React.ForwardRefExoticComponent<
|
|
1083
627
|
(Omit<PaneProps, 'mode' | 'defaultMode' | 'onModeChange'> & {
|
|
@@ -1090,13 +634,9 @@ type SidebarComponent = React.ForwardRefExoticComponent<
|
|
|
1090
634
|
React.RefAttributes<HTMLDivElement>
|
|
1091
635
|
> & { Handle: HandleComponent };
|
|
1092
636
|
|
|
1093
|
-
type InspectorComponent = React.ForwardRefExoticComponent<
|
|
1094
|
-
PaneProps & React.RefAttributes<HTMLDivElement>
|
|
1095
|
-
> & { Handle: HandleComponent };
|
|
637
|
+
type InspectorComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
|
|
1096
638
|
|
|
1097
|
-
type BottomComponent = React.ForwardRefExoticComponent<
|
|
1098
|
-
PaneProps & React.RefAttributes<HTMLDivElement>
|
|
1099
|
-
> & { Handle: HandleComponent };
|
|
639
|
+
type BottomComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
|
|
1100
640
|
|
|
1101
641
|
const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' | 'defaultMode'>>(
|
|
1102
642
|
(
|
|
@@ -1139,12 +679,8 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
1139
679
|
[ref],
|
|
1140
680
|
);
|
|
1141
681
|
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
|
-
);
|
|
682
|
+
const handleChildren = childArray.filter((el: React.ReactElement) => React.isValidElement(el) && el.type === PanelHandle);
|
|
683
|
+
const contentChildren = childArray.filter((el: React.ReactElement) => !(React.isValidElement(el) && el.type === PanelHandle));
|
|
1148
684
|
|
|
1149
685
|
const isOverlay = shell.leftResolvedPresentation === 'overlay';
|
|
1150
686
|
|
|
@@ -1193,11 +729,7 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
1193
729
|
// Ensure Left container width is auto whenever Panel is expanded in fixed presentation
|
|
1194
730
|
React.useEffect(() => {
|
|
1195
731
|
if (!localRef.current) return;
|
|
1196
|
-
if (
|
|
1197
|
-
shell.leftResolvedPresentation !== 'overlay' &&
|
|
1198
|
-
shell.leftMode === 'expanded' &&
|
|
1199
|
-
shell.panelMode === 'expanded'
|
|
1200
|
-
) {
|
|
732
|
+
if (shell.leftResolvedPresentation !== 'overlay' && shell.leftMode === 'expanded' && shell.panelMode === 'expanded') {
|
|
1201
733
|
const leftEl = (localRef.current.parentElement as HTMLElement) || null;
|
|
1202
734
|
try {
|
|
1203
735
|
leftEl?.style.removeProperty('width');
|
|
@@ -1247,11 +779,7 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
1247
779
|
requestToggle: () => shell.togglePane('panel'),
|
|
1248
780
|
}}
|
|
1249
781
|
>
|
|
1250
|
-
{handleChildren.length > 0 ? (
|
|
1251
|
-
handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i }))
|
|
1252
|
-
) : (
|
|
1253
|
-
<PaneHandle />
|
|
1254
|
-
)}
|
|
782
|
+
{handleChildren.length > 0 ? handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i })) : <PaneHandle />}
|
|
1255
783
|
</PaneResizeContext.Provider>
|
|
1256
784
|
) : null;
|
|
1257
785
|
|
|
@@ -1261,15 +789,8 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
1261
789
|
ref={setRef}
|
|
1262
790
|
className={classNames('rt-ShellPanel', className)}
|
|
1263
791
|
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
|
-
}
|
|
792
|
+
data-visible={isExpanded || (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') || undefined}
|
|
793
|
+
data-peek={(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') || undefined}
|
|
1273
794
|
style={{
|
|
1274
795
|
...style,
|
|
1275
796
|
['--panel-size' as any]: `${expandedSize}px`,
|
|
@@ -1286,941 +807,104 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
1286
807
|
Panel.displayName = 'Shell.Panel';
|
|
1287
808
|
Panel.Handle = PanelHandle;
|
|
1288
809
|
|
|
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
|
-
);
|
|
810
|
+
// Sidebar moved to ./_internal/shell-sidebar
|
|
1350
811
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
React.useEffect(() => {
|
|
1354
|
-
shell.setHasSidebar(true);
|
|
1355
|
-
return () => {
|
|
1356
|
-
shell.setHasSidebar(false);
|
|
1357
|
-
};
|
|
1358
|
-
}, [shell, sidebarId]);
|
|
812
|
+
// Content (always required)
|
|
813
|
+
interface ShellContentProps extends React.ComponentPropsWithoutRef<'main'> {}
|
|
1359
814
|
|
|
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
|
-
}, []);
|
|
815
|
+
const Content = React.forwardRef<HTMLElement, ShellContentProps>(({ className, ...props }, ref) => <main {...props} ref={ref} className={classNames('rt-ShellContent', className)} />);
|
|
816
|
+
Content.displayName = 'Shell.Content';
|
|
1370
817
|
|
|
1371
|
-
|
|
1372
|
-
React.useEffect(() => {
|
|
1373
|
-
if (mode !== undefined && shell.sidebarMode !== mode) {
|
|
1374
|
-
shell.setSidebarMode(mode);
|
|
1375
|
-
}
|
|
1376
|
-
}, [mode, shell]);
|
|
818
|
+
// Inspector moved to ./_internal/shell-inspector
|
|
1377
819
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
onModeChange?.(shell.sidebarMode);
|
|
1382
|
-
}
|
|
1383
|
-
}, [shell.sidebarMode, mode, onModeChange]);
|
|
820
|
+
// Bottom
|
|
821
|
+
// Bottom moved to ./_internal/shell-bottom
|
|
822
|
+
// (Bottom implementation extracted)
|
|
1384
823
|
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
onExpand?.();
|
|
1389
|
-
} else {
|
|
1390
|
-
onCollapse?.();
|
|
1391
|
-
}
|
|
1392
|
-
}, [shell.sidebarMode, onExpand, onCollapse]);
|
|
824
|
+
// Trigger
|
|
825
|
+
// PaneTarget type moved to shell.types.ts
|
|
826
|
+
type TriggerAction = 'toggle' | 'expand' | 'collapse';
|
|
1393
827
|
|
|
1394
|
-
|
|
1395
|
-
|
|
828
|
+
interface TriggerProps extends React.ComponentPropsWithoutRef<'button'> {
|
|
829
|
+
target: PaneTarget;
|
|
830
|
+
action?: TriggerAction;
|
|
831
|
+
/**
|
|
832
|
+
* Whether to show peek preview on hover when the target pane is collapsed.
|
|
833
|
+
* Defaults to false.
|
|
834
|
+
*/
|
|
835
|
+
peekOnHover?: boolean;
|
|
836
|
+
}
|
|
1396
837
|
|
|
1397
|
-
|
|
1398
|
-
|
|
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]);
|
|
838
|
+
const Trigger = React.forwardRef<HTMLButtonElement, TriggerProps>(({ target, action = 'toggle', peekOnHover, onClick, onMouseEnter, onMouseLeave, children, ...props }, ref) => {
|
|
839
|
+
const shell = useShell();
|
|
1414
840
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
(
|
|
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]);
|
|
841
|
+
const handleClick = React.useCallback(
|
|
842
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
843
|
+
onClick?.(event);
|
|
1429
844
|
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
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;
|
|
845
|
+
// Clear any active peek on this target before toggling to avoid sticky peek state
|
|
846
|
+
if ((shell as any).peekTarget === target) {
|
|
847
|
+
shell.clearPeek();
|
|
1445
848
|
}
|
|
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
849
|
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
850
|
+
switch (action) {
|
|
851
|
+
case 'toggle':
|
|
852
|
+
shell.togglePane(target);
|
|
853
|
+
break;
|
|
854
|
+
case 'expand':
|
|
855
|
+
shell.expandPane(target);
|
|
856
|
+
break;
|
|
857
|
+
case 'collapse':
|
|
858
|
+
shell.collapsePane(target);
|
|
859
|
+
break;
|
|
1489
860
|
}
|
|
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
861
|
},
|
|
1664
|
-
|
|
1665
|
-
)
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
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
|
-
);
|
|
862
|
+
[shell, target, action, onClick],
|
|
863
|
+
);
|
|
864
|
+
|
|
865
|
+
const isCollapsed = (() => {
|
|
866
|
+
switch (target) {
|
|
867
|
+
case 'left':
|
|
868
|
+
case 'rail':
|
|
869
|
+
return shell.leftMode === 'collapsed';
|
|
870
|
+
case 'panel':
|
|
871
|
+
return shell.leftMode === 'collapsed' || shell.panelMode === 'collapsed';
|
|
872
|
+
case 'sidebar':
|
|
873
|
+
return shell.sidebarMode === 'collapsed';
|
|
874
|
+
case 'inspector':
|
|
875
|
+
return shell.inspectorMode === 'collapsed';
|
|
876
|
+
case 'bottom':
|
|
877
|
+
return shell.bottomMode === 'collapsed';
|
|
1840
878
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
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
|
|
879
|
+
})();
|
|
880
|
+
|
|
881
|
+
const handleMouseEnter = React.useCallback(
|
|
882
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
883
|
+
onMouseEnter?.(event);
|
|
884
|
+
if (!peekOnHover || !isCollapsed) return;
|
|
885
|
+
// Use the actual target for peek behavior (not mapped to left)
|
|
886
|
+
shell.peekPane(target);
|
|
1896
887
|
},
|
|
1897
|
-
|
|
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
|
-
);
|
|
888
|
+
[onMouseEnter, peekOnHover, isCollapsed, shell, target],
|
|
889
|
+
);
|
|
1919
890
|
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
if (
|
|
1925
|
-
|
|
891
|
+
const handleMouseLeave = React.useCallback(
|
|
892
|
+
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
893
|
+
onMouseLeave?.(event);
|
|
894
|
+
if (!peekOnHover) return;
|
|
895
|
+
if ((shell as any).peekTarget === target) {
|
|
896
|
+
shell.clearPeek();
|
|
1926
897
|
}
|
|
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';
|
|
2121
|
-
|
|
2122
|
-
interface TriggerProps extends React.ComponentPropsWithoutRef<'button'> {
|
|
2123
|
-
target: PaneTarget;
|
|
2124
|
-
action?: TriggerAction;
|
|
2125
|
-
/**
|
|
2126
|
-
* Whether to show peek preview on hover when the target pane is collapsed.
|
|
2127
|
-
* Defaults to false.
|
|
2128
|
-
*/
|
|
2129
|
-
peekOnHover?: boolean;
|
|
2130
|
-
}
|
|
2131
|
-
|
|
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
898
|
},
|
|
2144
|
-
|
|
2145
|
-
)
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
if ((shell as any).peekTarget === target) {
|
|
2154
|
-
shell.clearPeek();
|
|
2155
|
-
}
|
|
2156
|
-
|
|
2157
|
-
switch (action) {
|
|
2158
|
-
case 'toggle':
|
|
2159
|
-
shell.togglePane(target);
|
|
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
|
-
);
|
|
2171
|
-
|
|
2172
|
-
const isCollapsed = (() => {
|
|
2173
|
-
switch (target) {
|
|
2174
|
-
case 'left':
|
|
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';
|
|
2185
|
-
}
|
|
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
|
-
|
|
2198
|
-
const handleMouseLeave = React.useCallback(
|
|
2199
|
-
(event: React.MouseEvent<HTMLButtonElement>) => {
|
|
2200
|
-
onMouseLeave?.(event);
|
|
2201
|
-
if (!peekOnHover) return;
|
|
2202
|
-
if ((shell as any).peekTarget === target) {
|
|
2203
|
-
shell.clearPeek();
|
|
2204
|
-
}
|
|
2205
|
-
},
|
|
2206
|
-
[onMouseLeave, peekOnHover, shell, target],
|
|
2207
|
-
);
|
|
2208
|
-
|
|
2209
|
-
return (
|
|
2210
|
-
<button
|
|
2211
|
-
{...props}
|
|
2212
|
-
ref={ref}
|
|
2213
|
-
onClick={handleClick}
|
|
2214
|
-
onMouseEnter={handleMouseEnter}
|
|
2215
|
-
onMouseLeave={handleMouseLeave}
|
|
2216
|
-
data-shell-trigger={target}
|
|
2217
|
-
data-shell-action={action}
|
|
2218
|
-
>
|
|
2219
|
-
{children}
|
|
2220
|
-
</button>
|
|
2221
|
-
);
|
|
2222
|
-
},
|
|
2223
|
-
);
|
|
899
|
+
[onMouseLeave, peekOnHover, shell, target],
|
|
900
|
+
);
|
|
901
|
+
|
|
902
|
+
return (
|
|
903
|
+
<button {...props} ref={ref} onClick={handleClick} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} data-shell-trigger={target} data-shell-action={action}>
|
|
904
|
+
{children}
|
|
905
|
+
</button>
|
|
906
|
+
);
|
|
907
|
+
});
|
|
2224
908
|
Trigger.displayName = 'Shell.Trigger';
|
|
2225
909
|
|
|
2226
910
|
// Exports
|