@kushagradhawan/kookie-ui 0.1.50 → 0.1.52
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 +582 -116
- package/dist/cjs/components/_internal/shell-bottom.d.ts +31 -5
- package/dist/cjs/components/_internal/shell-bottom.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-bottom.js +1 -1
- package/dist/cjs/components/_internal/shell-bottom.js.map +3 -3
- package/dist/cjs/components/_internal/shell-handles.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-handles.js +1 -1
- package/dist/cjs/components/_internal/shell-handles.js.map +3 -3
- package/dist/cjs/components/_internal/shell-inspector.d.ts +23 -5
- package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-inspector.js +1 -1
- package/dist/cjs/components/_internal/shell-inspector.js.map +3 -3
- package/dist/cjs/components/_internal/shell-sidebar.d.ts +24 -6
- package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
- package/dist/cjs/components/_internal/shell-sidebar.js.map +3 -3
- package/dist/cjs/components/chatbar.d.ts +21 -2
- package/dist/cjs/components/chatbar.d.ts.map +1 -1
- package/dist/cjs/components/chatbar.js +1 -1
- package/dist/cjs/components/chatbar.js.map +3 -3
- package/dist/cjs/components/shell.context.d.ts +88 -1
- package/dist/cjs/components/shell.context.d.ts.map +1 -1
- package/dist/cjs/components/shell.context.js +1 -1
- package/dist/cjs/components/shell.context.js.map +3 -3
- package/dist/cjs/components/shell.d.ts +51 -13
- package/dist/cjs/components/shell.d.ts.map +1 -1
- package/dist/cjs/components/shell.hooks.d.ts +7 -1
- package/dist/cjs/components/shell.hooks.d.ts.map +1 -1
- package/dist/cjs/components/shell.hooks.js +1 -1
- package/dist/cjs/components/shell.hooks.js.map +3 -3
- 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 +1 -0
- package/dist/cjs/components/shell.types.d.ts.map +1 -1
- package/dist/cjs/components/shell.types.js +1 -1
- package/dist/cjs/components/shell.types.js.map +2 -2
- package/dist/esm/components/_internal/shell-bottom.d.ts +31 -5
- package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-bottom.js +1 -1
- package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
- package/dist/esm/components/_internal/shell-handles.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-handles.js +1 -1
- package/dist/esm/components/_internal/shell-handles.js.map +3 -3
- package/dist/esm/components/_internal/shell-inspector.d.ts +23 -5
- package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-inspector.js +1 -1
- package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
- package/dist/esm/components/_internal/shell-sidebar.d.ts +24 -6
- package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-sidebar.js +1 -1
- package/dist/esm/components/_internal/shell-sidebar.js.map +3 -3
- package/dist/esm/components/chatbar.d.ts +21 -2
- package/dist/esm/components/chatbar.d.ts.map +1 -1
- package/dist/esm/components/chatbar.js +1 -1
- package/dist/esm/components/chatbar.js.map +3 -3
- package/dist/esm/components/shell.context.d.ts +88 -1
- package/dist/esm/components/shell.context.d.ts.map +1 -1
- package/dist/esm/components/shell.context.js +1 -1
- package/dist/esm/components/shell.context.js.map +3 -3
- package/dist/esm/components/shell.d.ts +51 -13
- package/dist/esm/components/shell.d.ts.map +1 -1
- package/dist/esm/components/shell.hooks.d.ts +7 -1
- package/dist/esm/components/shell.hooks.d.ts.map +1 -1
- package/dist/esm/components/shell.hooks.js +1 -1
- package/dist/esm/components/shell.hooks.js.map +3 -3
- 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 +1 -0
- package/dist/esm/components/shell.types.d.ts.map +1 -1
- package/dist/esm/components/shell.types.js.map +2 -2
- package/package.json +14 -3
- 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/toggle-button.json +1 -1
- package/schemas/toggle-icon-button.json +1 -1
- package/src/components/_internal/base-menu.css +16 -16
- package/src/components/_internal/base-sidebar-menu.css +23 -20
- package/src/components/_internal/base-sidebar.css +13 -0
- package/src/components/_internal/shell-bottom.tsx +176 -49
- package/src/components/_internal/shell-handles.tsx +29 -4
- package/src/components/_internal/shell-inspector.tsx +175 -43
- package/src/components/_internal/shell-sidebar.tsx +177 -93
- package/src/components/chatbar.css +240 -21
- package/src/components/chatbar.tsx +280 -291
- package/src/components/sheet.css +8 -16
- package/src/components/shell.context.tsx +79 -3
- package/src/components/shell.css +0 -1
- package/src/components/shell.hooks.ts +35 -0
- package/src/components/shell.tsx +574 -235
- package/src/components/shell.types.ts +2 -0
- package/src/components/sidebar.css +2 -2
- package/styles.css +582 -116
package/src/components/shell.tsx
CHANGED
|
@@ -28,17 +28,28 @@
|
|
|
28
28
|
import * as React from 'react';
|
|
29
29
|
import classNames from 'classnames';
|
|
30
30
|
import * as Sheet from './sheet.js';
|
|
31
|
-
import { Inset } from './inset.js';
|
|
32
31
|
import { VisuallyHidden } from './visually-hidden.js';
|
|
33
|
-
import { useResponsivePresentation } from './shell.hooks.js';
|
|
32
|
+
import { useResponsivePresentation, useResponsiveValue } from './shell.hooks.js';
|
|
34
33
|
import { PaneResizeContext } from './_internal/shell-resize.js';
|
|
35
34
|
import { PaneHandle, PanelHandle, SidebarHandle, InspectorHandle, BottomHandle } from './_internal/shell-handles.js';
|
|
36
35
|
import { Sidebar } from './_internal/shell-sidebar.js';
|
|
37
36
|
import { Bottom } from './_internal/shell-bottom.js';
|
|
38
37
|
import { Inspector } from './_internal/shell-inspector.js';
|
|
39
|
-
import type { PresentationValue, ResponsivePresentation, PaneMode, SidebarMode,
|
|
38
|
+
import type { PresentationValue, ResponsivePresentation, PaneMode, SidebarMode, PaneSizePersistence, Breakpoint, PaneTarget, Responsive } from './shell.types.js';
|
|
40
39
|
import { BREAKPOINTS } from './shell.types.js';
|
|
41
|
-
import {
|
|
40
|
+
import {
|
|
41
|
+
ShellProvider,
|
|
42
|
+
useShell,
|
|
43
|
+
LeftModeContext,
|
|
44
|
+
PanelModeContext,
|
|
45
|
+
SidebarModeContext,
|
|
46
|
+
InspectorModeContext,
|
|
47
|
+
BottomModeContext,
|
|
48
|
+
PresentationContext,
|
|
49
|
+
PeekContext,
|
|
50
|
+
ActionsContext,
|
|
51
|
+
CompositionContext,
|
|
52
|
+
} from './shell.context.js';
|
|
42
53
|
|
|
43
54
|
// Shell context is provided via ShellProvider (see shell.context.tsx)
|
|
44
55
|
|
|
@@ -76,16 +87,145 @@ function useBreakpoint(): { bp: Breakpoint; ready: boolean } {
|
|
|
76
87
|
};
|
|
77
88
|
|
|
78
89
|
compute();
|
|
79
|
-
|
|
90
|
+
const cleanups: Array<() => void> = [];
|
|
91
|
+
mqls.forEach(([, m]) => {
|
|
92
|
+
const mm = m as MediaQueryList & {
|
|
93
|
+
addEventListener?: (type: 'change', listener: (e: MediaQueryListEvent) => void) => void;
|
|
94
|
+
removeEventListener?: (type: 'change', listener: (e: MediaQueryListEvent) => void) => void;
|
|
95
|
+
addListener?: (listener: (e: MediaQueryListEvent) => void) => void;
|
|
96
|
+
removeListener?: (listener: (e: MediaQueryListEvent) => void) => void;
|
|
97
|
+
};
|
|
98
|
+
if (typeof mm.addEventListener === 'function' && typeof mm.removeEventListener === 'function') {
|
|
99
|
+
mm.addEventListener('change', compute as any);
|
|
100
|
+
cleanups.push(() => mm.removeEventListener?.('change', compute as any));
|
|
101
|
+
} else if (typeof mm.addListener === 'function' && typeof mm.removeListener === 'function') {
|
|
102
|
+
mm.addListener(compute as any);
|
|
103
|
+
cleanups.push(() => mm.removeListener?.(compute as any));
|
|
104
|
+
}
|
|
105
|
+
});
|
|
80
106
|
|
|
81
107
|
return () => {
|
|
82
|
-
|
|
108
|
+
cleanups.forEach((fn) => {
|
|
109
|
+
try {
|
|
110
|
+
fn();
|
|
111
|
+
} catch {}
|
|
112
|
+
});
|
|
83
113
|
};
|
|
84
114
|
}, []);
|
|
85
115
|
|
|
86
116
|
return { bp: currentBp, ready };
|
|
87
117
|
}
|
|
88
118
|
|
|
119
|
+
// Reducer-based pane state management to simplify cascading rules
|
|
120
|
+
type PaneState = {
|
|
121
|
+
leftMode: PaneMode;
|
|
122
|
+
panelMode: PaneMode;
|
|
123
|
+
sidebarMode: SidebarMode;
|
|
124
|
+
inspectorMode: PaneMode;
|
|
125
|
+
bottomMode: PaneMode;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
type PaneAction =
|
|
129
|
+
| { type: 'SET_LEFT_MODE'; mode: PaneMode }
|
|
130
|
+
| { type: 'SET_PANEL_MODE'; mode: PaneMode }
|
|
131
|
+
| { type: 'SET_SIDEBAR_MODE'; mode: SidebarMode }
|
|
132
|
+
| { type: 'SET_INSPECTOR_MODE'; mode: PaneMode }
|
|
133
|
+
| { type: 'SET_BOTTOM_MODE'; mode: PaneMode }
|
|
134
|
+
| { type: 'TOGGLE_PANE'; target: PaneTarget }
|
|
135
|
+
| { type: 'EXPAND_PANE'; target: PaneTarget }
|
|
136
|
+
| { type: 'COLLAPSE_PANE'; target: PaneTarget };
|
|
137
|
+
|
|
138
|
+
function paneReducer(state: PaneState, action: PaneAction): PaneState {
|
|
139
|
+
switch (action.type) {
|
|
140
|
+
case 'SET_LEFT_MODE': {
|
|
141
|
+
// Collapsing left cascades to panel collapse
|
|
142
|
+
if (action.mode === 'collapsed') {
|
|
143
|
+
return { ...state, leftMode: 'collapsed', panelMode: 'collapsed' };
|
|
144
|
+
}
|
|
145
|
+
return { ...state, leftMode: action.mode };
|
|
146
|
+
}
|
|
147
|
+
case 'SET_PANEL_MODE': {
|
|
148
|
+
// Expanding panel ensures left is expanded
|
|
149
|
+
if (action.mode === 'expanded' && state.leftMode !== 'expanded') {
|
|
150
|
+
return { ...state, leftMode: 'expanded', panelMode: 'expanded' };
|
|
151
|
+
}
|
|
152
|
+
return { ...state, panelMode: action.mode };
|
|
153
|
+
}
|
|
154
|
+
case 'SET_SIDEBAR_MODE':
|
|
155
|
+
return { ...state, sidebarMode: action.mode };
|
|
156
|
+
case 'SET_INSPECTOR_MODE':
|
|
157
|
+
return { ...state, inspectorMode: action.mode };
|
|
158
|
+
case 'SET_BOTTOM_MODE':
|
|
159
|
+
return { ...state, bottomMode: action.mode };
|
|
160
|
+
case 'TOGGLE_PANE': {
|
|
161
|
+
switch (action.target) {
|
|
162
|
+
case 'left':
|
|
163
|
+
case 'rail':
|
|
164
|
+
return { ...state, leftMode: state.leftMode === 'expanded' ? 'collapsed' : 'expanded', panelMode: state.leftMode === 'expanded' ? 'collapsed' : state.panelMode };
|
|
165
|
+
case 'panel': {
|
|
166
|
+
if (state.leftMode === 'collapsed') {
|
|
167
|
+
return { ...state, leftMode: 'expanded', panelMode: 'expanded' };
|
|
168
|
+
}
|
|
169
|
+
return { ...state, panelMode: state.panelMode === 'expanded' ? 'collapsed' : 'expanded' };
|
|
170
|
+
}
|
|
171
|
+
case 'sidebar': {
|
|
172
|
+
// Sidebar toggle sequencing is handled externally via setSidebarToggleComputer
|
|
173
|
+
// This reducer only flips between expanded<->collapsed by default; thin is set by caller
|
|
174
|
+
const next: SidebarMode = state.sidebarMode === 'collapsed' ? 'expanded' : state.sidebarMode === 'expanded' ? 'collapsed' : 'expanded';
|
|
175
|
+
return { ...state, sidebarMode: next };
|
|
176
|
+
}
|
|
177
|
+
case 'inspector':
|
|
178
|
+
return { ...state, inspectorMode: state.inspectorMode === 'expanded' ? 'collapsed' : 'expanded' };
|
|
179
|
+
case 'bottom':
|
|
180
|
+
return { ...state, bottomMode: state.bottomMode === 'expanded' ? 'collapsed' : 'expanded' };
|
|
181
|
+
default:
|
|
182
|
+
return state;
|
|
183
|
+
}
|
|
184
|
+
// Fallback to satisfy no-fallthrough in some environments
|
|
185
|
+
return state;
|
|
186
|
+
}
|
|
187
|
+
case 'EXPAND_PANE': {
|
|
188
|
+
switch (action.target) {
|
|
189
|
+
case 'left':
|
|
190
|
+
case 'rail':
|
|
191
|
+
return { ...state, leftMode: 'expanded' };
|
|
192
|
+
case 'panel':
|
|
193
|
+
return { ...state, leftMode: 'expanded', panelMode: 'expanded' };
|
|
194
|
+
case 'sidebar':
|
|
195
|
+
return { ...state, sidebarMode: 'expanded' };
|
|
196
|
+
case 'inspector':
|
|
197
|
+
return { ...state, inspectorMode: 'expanded' };
|
|
198
|
+
case 'bottom':
|
|
199
|
+
return { ...state, bottomMode: 'expanded' };
|
|
200
|
+
default:
|
|
201
|
+
return state;
|
|
202
|
+
}
|
|
203
|
+
// Fallback to satisfy no-fallthrough in some environments
|
|
204
|
+
return state;
|
|
205
|
+
}
|
|
206
|
+
case 'COLLAPSE_PANE': {
|
|
207
|
+
switch (action.target) {
|
|
208
|
+
case 'left':
|
|
209
|
+
case 'rail':
|
|
210
|
+
return { ...state, leftMode: 'collapsed', panelMode: 'collapsed' };
|
|
211
|
+
case 'panel':
|
|
212
|
+
return { ...state, panelMode: 'collapsed' };
|
|
213
|
+
case 'sidebar':
|
|
214
|
+
return { ...state, sidebarMode: 'collapsed' };
|
|
215
|
+
case 'inspector':
|
|
216
|
+
return { ...state, inspectorMode: 'collapsed' };
|
|
217
|
+
case 'bottom':
|
|
218
|
+
return { ...state, bottomMode: 'collapsed' };
|
|
219
|
+
default:
|
|
220
|
+
return state;
|
|
221
|
+
}
|
|
222
|
+
// Fallback to satisfy no-fallthrough in some environments
|
|
223
|
+
return state;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return state;
|
|
227
|
+
}
|
|
228
|
+
|
|
89
229
|
// Root Component
|
|
90
230
|
interface ShellRootProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
91
231
|
children: React.ReactNode;
|
|
@@ -95,14 +235,24 @@ interface ShellRootProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
|
95
235
|
const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, children, height = 'full', ...props }, ref) => {
|
|
96
236
|
const { bp: currentBreakpoint, ready: currentBreakpointReady } = useBreakpoint();
|
|
97
237
|
|
|
98
|
-
//
|
|
99
|
-
const
|
|
100
|
-
const
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const [
|
|
105
|
-
|
|
238
|
+
// Compute initial defaults from immediate children (one-time, uncontrolled defaults)
|
|
239
|
+
const initialChildren = React.Children.toArray(children) as React.ReactElement[];
|
|
240
|
+
const hasPanelDefaultOpen = initialChildren.some((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Panel' && Boolean((el as any).props?.defaultOpen));
|
|
241
|
+
const hasRailDefaultOpen = initialChildren.some((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Rail' && Boolean((el as any).props?.defaultOpen));
|
|
242
|
+
|
|
243
|
+
// Pane state management via reducer
|
|
244
|
+
const [paneState, dispatchPane] = React.useReducer(paneReducer, {
|
|
245
|
+
leftMode: hasPanelDefaultOpen || hasRailDefaultOpen ? 'expanded' : 'collapsed',
|
|
246
|
+
panelMode: hasPanelDefaultOpen ? 'expanded' : 'collapsed',
|
|
247
|
+
sidebarMode: 'expanded',
|
|
248
|
+
inspectorMode: 'collapsed',
|
|
249
|
+
bottomMode: 'collapsed',
|
|
250
|
+
});
|
|
251
|
+
const setLeftMode = React.useCallback((mode: PaneMode) => dispatchPane({ type: 'SET_LEFT_MODE', mode }), []);
|
|
252
|
+
const setPanelMode = React.useCallback((mode: PaneMode) => dispatchPane({ type: 'SET_PANEL_MODE', mode }), []);
|
|
253
|
+
const setSidebarMode = React.useCallback((mode: SidebarMode) => dispatchPane({ type: 'SET_SIDEBAR_MODE', mode }), []);
|
|
254
|
+
const setInspectorMode = React.useCallback((mode: PaneMode) => dispatchPane({ type: 'SET_INSPECTOR_MODE', mode }), []);
|
|
255
|
+
const setBottomMode = React.useCallback((mode: PaneMode) => dispatchPane({ type: 'SET_BOTTOM_MODE', mode }), []);
|
|
106
256
|
|
|
107
257
|
// Removed: defaultMode responsiveness and manual change tracking
|
|
108
258
|
|
|
@@ -116,12 +266,7 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
116
266
|
sidebarToggleComputerRef.current = fn;
|
|
117
267
|
}, []);
|
|
118
268
|
|
|
119
|
-
//
|
|
120
|
-
React.useEffect(() => {
|
|
121
|
-
if (leftMode === 'collapsed') {
|
|
122
|
-
setPanelMode('collapsed');
|
|
123
|
-
}
|
|
124
|
-
}, [leftMode]);
|
|
269
|
+
// Reducer handles left→panel cascade; no effect needed
|
|
125
270
|
|
|
126
271
|
// Composition validation
|
|
127
272
|
React.useEffect(() => {
|
|
@@ -157,107 +302,38 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
157
302
|
|
|
158
303
|
const togglePane = React.useCallback(
|
|
159
304
|
(target: PaneTarget) => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
break;
|
|
165
|
-
case 'panel':
|
|
166
|
-
// Panel toggle: expand left if collapsed, then toggle panel
|
|
167
|
-
if (leftMode === 'collapsed') {
|
|
168
|
-
setLeftMode('expanded');
|
|
169
|
-
setPanelMode('expanded');
|
|
170
|
-
} else {
|
|
171
|
-
setPanelMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
172
|
-
}
|
|
173
|
-
break;
|
|
174
|
-
case 'sidebar': {
|
|
175
|
-
// Orchestrate thin ↔ expanded sequencing: fade out → change mode → fade in
|
|
176
|
-
const next = sidebarToggleComputerRef.current(sidebarMode as SidebarMode);
|
|
177
|
-
const isWidthOnlyChange = sidebarMode !== next && sidebarMode !== 'collapsed' && next !== 'collapsed';
|
|
178
|
-
if (!isWidthOnlyChange) {
|
|
179
|
-
setSidebarMode(next);
|
|
180
|
-
break;
|
|
181
|
-
}
|
|
182
|
-
const SMALL_MS = 150;
|
|
183
|
-
setSidebarPhase('hiding');
|
|
184
|
-
window.setTimeout(() => {
|
|
185
|
-
setSidebarPhase('resizing');
|
|
186
|
-
setSidebarMode(next);
|
|
187
|
-
window.setTimeout(() => {
|
|
188
|
-
setSidebarPhase('showing');
|
|
189
|
-
window.setTimeout(() => setSidebarPhase('idle'), SMALL_MS);
|
|
190
|
-
}, SMALL_MS);
|
|
191
|
-
}, SMALL_MS);
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
case 'inspector':
|
|
195
|
-
setInspectorMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
196
|
-
break;
|
|
197
|
-
case 'bottom':
|
|
198
|
-
setBottomMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
199
|
-
break;
|
|
305
|
+
if (target === 'sidebar') {
|
|
306
|
+
const next = sidebarToggleComputerRef.current(paneState.sidebarMode as SidebarMode);
|
|
307
|
+
setSidebarMode(next);
|
|
308
|
+
return;
|
|
200
309
|
}
|
|
310
|
+
dispatchPane({ type: 'TOGGLE_PANE', target });
|
|
201
311
|
},
|
|
202
|
-
[
|
|
312
|
+
[paneState.sidebarMode],
|
|
203
313
|
);
|
|
204
314
|
|
|
205
315
|
const expandPane = React.useCallback((target: PaneTarget) => {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
case 'rail':
|
|
209
|
-
setLeftMode('expanded');
|
|
210
|
-
break;
|
|
211
|
-
case 'panel':
|
|
212
|
-
setLeftMode('expanded');
|
|
213
|
-
setPanelMode('expanded');
|
|
214
|
-
break;
|
|
215
|
-
case 'sidebar':
|
|
216
|
-
setSidebarMode('expanded');
|
|
217
|
-
break;
|
|
218
|
-
case 'inspector':
|
|
219
|
-
setInspectorMode('expanded');
|
|
220
|
-
break;
|
|
221
|
-
case 'bottom':
|
|
222
|
-
setBottomMode('expanded');
|
|
223
|
-
break;
|
|
224
|
-
}
|
|
316
|
+
if (target === 'sidebar') return setSidebarMode('expanded');
|
|
317
|
+
dispatchPane({ type: 'EXPAND_PANE', target });
|
|
225
318
|
}, []);
|
|
226
319
|
|
|
227
320
|
const collapsePane = React.useCallback((target: PaneTarget) => {
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
case 'rail':
|
|
231
|
-
setLeftMode('collapsed');
|
|
232
|
-
break;
|
|
233
|
-
case 'panel':
|
|
234
|
-
setPanelMode('collapsed');
|
|
235
|
-
break;
|
|
236
|
-
case 'sidebar':
|
|
237
|
-
setSidebarMode('collapsed');
|
|
238
|
-
break;
|
|
239
|
-
case 'inspector':
|
|
240
|
-
setInspectorMode('collapsed');
|
|
241
|
-
break;
|
|
242
|
-
case 'bottom':
|
|
243
|
-
setBottomMode('collapsed');
|
|
244
|
-
break;
|
|
245
|
-
}
|
|
321
|
+
if (target === 'sidebar') return setSidebarMode('collapsed');
|
|
322
|
+
dispatchPane({ type: 'COLLAPSE_PANE', target });
|
|
246
323
|
}, []);
|
|
247
324
|
|
|
248
325
|
const baseContextValue = React.useMemo(
|
|
249
326
|
() => ({
|
|
250
|
-
leftMode,
|
|
327
|
+
leftMode: paneState.leftMode,
|
|
251
328
|
setLeftMode,
|
|
252
|
-
panelMode,
|
|
329
|
+
panelMode: paneState.panelMode,
|
|
253
330
|
setPanelMode,
|
|
254
|
-
sidebarMode,
|
|
331
|
+
sidebarMode: paneState.sidebarMode,
|
|
255
332
|
setSidebarMode,
|
|
256
|
-
inspectorMode,
|
|
333
|
+
inspectorMode: paneState.inspectorMode,
|
|
257
334
|
setInspectorMode,
|
|
258
|
-
bottomMode,
|
|
335
|
+
bottomMode: paneState.bottomMode,
|
|
259
336
|
setBottomMode,
|
|
260
|
-
sidebarPhase,
|
|
261
337
|
hasLeft,
|
|
262
338
|
setHasLeft,
|
|
263
339
|
hasSidebar,
|
|
@@ -274,12 +350,11 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
274
350
|
onPanelDefaults,
|
|
275
351
|
}),
|
|
276
352
|
[
|
|
277
|
-
leftMode,
|
|
278
|
-
panelMode,
|
|
279
|
-
sidebarMode,
|
|
280
|
-
inspectorMode,
|
|
281
|
-
bottomMode,
|
|
282
|
-
sidebarPhase,
|
|
353
|
+
paneState.leftMode,
|
|
354
|
+
paneState.panelMode,
|
|
355
|
+
paneState.sidebarMode,
|
|
356
|
+
paneState.inspectorMode,
|
|
357
|
+
paneState.bottomMode,
|
|
283
358
|
hasLeft,
|
|
284
359
|
hasSidebar,
|
|
285
360
|
currentBreakpoint,
|
|
@@ -307,6 +382,14 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
307
382
|
const inspectorEls = childArray.filter((el) => isType(el, Inspector));
|
|
308
383
|
const bottomEls = childArray.filter((el) => isType(el, Bottom));
|
|
309
384
|
|
|
385
|
+
// Controlled sync in Root: mirror first Rail.open if provided
|
|
386
|
+
const firstRailOpen = (railEls[0] as any)?.props?.open;
|
|
387
|
+
React.useEffect(() => {
|
|
388
|
+
if (typeof firstRailOpen === 'undefined') return;
|
|
389
|
+
const shouldOpen = Boolean(firstRailOpen);
|
|
390
|
+
setLeftMode(shouldOpen ? 'expanded' : 'collapsed');
|
|
391
|
+
}, [firstRailOpen]);
|
|
392
|
+
|
|
310
393
|
const heightStyle = React.useMemo(() => {
|
|
311
394
|
if (height === 'full') return { height: '100vh' };
|
|
312
395
|
if (height === 'auto') return { height: 'auto' };
|
|
@@ -320,6 +403,17 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
320
403
|
const peekPane = React.useCallback((target: PaneTarget) => setPeekTarget(target), []);
|
|
321
404
|
const clearPeek = React.useCallback(() => setPeekTarget(null), []);
|
|
322
405
|
|
|
406
|
+
// Memoized slice context values to avoid notifying unrelated consumers
|
|
407
|
+
const presentationCtxValue = React.useMemo(() => ({ currentBreakpoint, currentBreakpointReady, leftResolvedPresentation: devLeftPres }), [currentBreakpoint, currentBreakpointReady, devLeftPres]);
|
|
408
|
+
const leftModeCtxValue = React.useMemo(() => ({ leftMode: paneState.leftMode, setLeftMode }), [paneState.leftMode, setLeftMode]);
|
|
409
|
+
const panelModeCtxValue = React.useMemo(() => ({ panelMode: paneState.panelMode, setPanelMode }), [paneState.panelMode, setPanelMode]);
|
|
410
|
+
const sidebarModeCtxValue = React.useMemo(() => ({ sidebarMode: paneState.sidebarMode, setSidebarMode }), [paneState.sidebarMode, setSidebarMode]);
|
|
411
|
+
const inspectorModeCtxValue = React.useMemo(() => ({ inspectorMode: paneState.inspectorMode, setInspectorMode }), [paneState.inspectorMode, setInspectorMode]);
|
|
412
|
+
const bottomModeCtxValue = React.useMemo(() => ({ bottomMode: paneState.bottomMode, setBottomMode }), [paneState.bottomMode, setBottomMode]);
|
|
413
|
+
const compositionCtxValue = React.useMemo(() => ({ hasLeft, setHasLeft, hasSidebar, setHasSidebar }), [hasLeft, setHasLeft, hasSidebar, setHasSidebar]);
|
|
414
|
+
const peekCtxValue = React.useMemo(() => ({ peekTarget, setPeekTarget, peekPane, clearPeek }), [peekTarget, setPeekTarget, peekPane, clearPeek]);
|
|
415
|
+
const actionsCtxValue = React.useMemo(() => ({ togglePane, expandPane, collapsePane, setSidebarToggleComputer }), [togglePane, expandPane, collapsePane, setSidebarToggleComputer]);
|
|
416
|
+
|
|
323
417
|
return (
|
|
324
418
|
<div {...props} ref={ref} className={classNames('rt-ShellRoot', className)} style={{ ...heightStyle, ...props.style }}>
|
|
325
419
|
<ShellProvider
|
|
@@ -331,44 +425,63 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
331
425
|
clearPeek,
|
|
332
426
|
}}
|
|
333
427
|
>
|
|
334
|
-
{
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
428
|
+
<PresentationContext.Provider value={presentationCtxValue}>
|
|
429
|
+
<LeftModeContext.Provider value={leftModeCtxValue}>
|
|
430
|
+
<PanelModeContext.Provider value={panelModeCtxValue}>
|
|
431
|
+
<SidebarModeContext.Provider value={sidebarModeCtxValue}>
|
|
432
|
+
<InspectorModeContext.Provider value={inspectorModeCtxValue}>
|
|
433
|
+
<BottomModeContext.Provider value={bottomModeCtxValue}>
|
|
434
|
+
<CompositionContext.Provider value={compositionCtxValue}>
|
|
435
|
+
<PeekContext.Provider value={peekCtxValue}>
|
|
436
|
+
<ActionsContext.Provider value={actionsCtxValue}>
|
|
437
|
+
{headerEls}
|
|
438
|
+
<div
|
|
439
|
+
className="rt-ShellBody"
|
|
440
|
+
data-peek-target={peekTarget ?? undefined}
|
|
441
|
+
style={
|
|
442
|
+
peekTarget === 'rail' || peekTarget === 'panel'
|
|
443
|
+
? ({
|
|
444
|
+
['--peek-rail-width' as any]: `${railDefaultSizeRef.current}px`,
|
|
445
|
+
} as React.CSSProperties)
|
|
446
|
+
: undefined
|
|
447
|
+
}
|
|
448
|
+
>
|
|
449
|
+
{hasLeftChildren && !hasSidebarChildren
|
|
450
|
+
? (() => {
|
|
451
|
+
const firstRail = railEls[0] as any;
|
|
452
|
+
const passthroughProps = firstRail
|
|
453
|
+
? {
|
|
454
|
+
// Notification passthrough used by Left; not spread to DOM in Left
|
|
455
|
+
onOpenChange: firstRail.props?.onOpenChange,
|
|
456
|
+
open: firstRail.props?.open,
|
|
457
|
+
defaultOpen: firstRail.props?.defaultOpen,
|
|
458
|
+
presentation: firstRail.props?.presentation,
|
|
459
|
+
collapsible: firstRail.props?.collapsible,
|
|
460
|
+
onExpand: firstRail.props?.onExpand,
|
|
461
|
+
onCollapse: firstRail.props?.onCollapse,
|
|
462
|
+
}
|
|
463
|
+
: { defaultOpen: hasPanelDefaultOpen ? true : undefined };
|
|
464
|
+
return (
|
|
465
|
+
<Left {...(passthroughProps as any)}>
|
|
466
|
+
{railEls}
|
|
467
|
+
{panelEls}
|
|
468
|
+
</Left>
|
|
469
|
+
);
|
|
470
|
+
})()
|
|
471
|
+
: sidebarEls}
|
|
472
|
+
{contentEls}
|
|
473
|
+
{inspectorEls}
|
|
474
|
+
</div>
|
|
475
|
+
{bottomEls}
|
|
476
|
+
</ActionsContext.Provider>
|
|
477
|
+
</PeekContext.Provider>
|
|
478
|
+
</CompositionContext.Provider>
|
|
479
|
+
</BottomModeContext.Provider>
|
|
480
|
+
</InspectorModeContext.Provider>
|
|
481
|
+
</SidebarModeContext.Provider>
|
|
482
|
+
</PanelModeContext.Provider>
|
|
483
|
+
</LeftModeContext.Provider>
|
|
484
|
+
</PresentationContext.Provider>
|
|
372
485
|
</ShellProvider>
|
|
373
486
|
</div>
|
|
374
487
|
);
|
|
@@ -396,9 +509,6 @@ Header.displayName = 'Shell.Header';
|
|
|
396
509
|
// Pane Props Interface (shared by Panel, Sidebar, Inspector, Bottom)
|
|
397
510
|
interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
398
511
|
presentation?: ResponsivePresentation;
|
|
399
|
-
mode?: PaneMode;
|
|
400
|
-
defaultMode?: ResponsiveMode;
|
|
401
|
-
onModeChange?: (mode: PaneMode) => void;
|
|
402
512
|
expandedSize?: number;
|
|
403
513
|
minSize?: number;
|
|
404
514
|
maxSize?: number;
|
|
@@ -421,29 +531,32 @@ interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
|
421
531
|
// Left container (auto-created for Rail+Panel)
|
|
422
532
|
interface LeftProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
423
533
|
presentation?: ResponsivePresentation;
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
534
|
+
// New: passthrough from Rail
|
|
535
|
+
open?: boolean;
|
|
536
|
+
defaultOpen?: boolean;
|
|
537
|
+
onOpenChange?: (open: boolean, meta: { reason: 'init' | 'toggle' | 'panel' | 'responsive' }) => void;
|
|
427
538
|
collapsible?: boolean;
|
|
428
539
|
onExpand?: () => void;
|
|
429
540
|
onCollapse?: () => void;
|
|
430
541
|
}
|
|
431
542
|
|
|
432
543
|
// Rail (special case)
|
|
433
|
-
|
|
544
|
+
type LeftOpenChangeMeta = { reason: 'init' | 'toggle' | 'responsive' | 'panel' };
|
|
545
|
+
|
|
546
|
+
type RailControlledProps = { open: boolean; onOpenChange?: (open: boolean, meta: LeftOpenChangeMeta) => void; defaultOpen?: never };
|
|
547
|
+
type RailUncontrolledProps = { defaultOpen?: boolean; onOpenChange?: (open: boolean, meta: LeftOpenChangeMeta) => void; open?: never };
|
|
548
|
+
|
|
549
|
+
type RailProps = React.ComponentPropsWithoutRef<'div'> & {
|
|
434
550
|
presentation?: ResponsivePresentation;
|
|
435
|
-
mode?: PaneMode;
|
|
436
|
-
defaultMode?: ResponsiveMode;
|
|
437
|
-
onModeChange?: (mode: PaneMode) => void;
|
|
438
551
|
expandedSize?: number;
|
|
439
552
|
collapsible?: boolean;
|
|
440
553
|
onExpand?: () => void;
|
|
441
554
|
onCollapse?: () => void;
|
|
442
|
-
}
|
|
555
|
+
} & (RailControlledProps | RailUncontrolledProps);
|
|
443
556
|
|
|
444
557
|
// Left container - behaves like Inspector but contains Rail+Panel
|
|
445
558
|
const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
446
|
-
({ className, presentation = { initial: '
|
|
559
|
+
({ className, presentation = { initial: 'fixed', sm: 'fixed' }, collapsible = true, onExpand, onCollapse, children, style, ...props }, ref) => {
|
|
447
560
|
const shell = useShell();
|
|
448
561
|
const resolvedPresentation = useResponsivePresentation(presentation);
|
|
449
562
|
const isOverlay = resolvedPresentation === 'overlay';
|
|
@@ -468,50 +581,48 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
468
581
|
return () => shell.setHasLeft(false);
|
|
469
582
|
}, [shell]);
|
|
470
583
|
|
|
471
|
-
// Always-follow responsive defaultMode for uncontrolled Left (Rail stack)
|
|
472
|
-
const resolveResponsiveMode = React.useCallback((): PaneMode => {
|
|
473
|
-
if (typeof defaultMode === 'string') return defaultMode as PaneMode;
|
|
474
|
-
const dm = defaultMode as Partial<Record<Breakpoint, PaneMode>> | undefined;
|
|
475
|
-
if (dm && dm[shell.currentBreakpoint as Breakpoint]) {
|
|
476
|
-
return dm[shell.currentBreakpoint as Breakpoint] as PaneMode;
|
|
477
|
-
}
|
|
478
|
-
const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
|
|
479
|
-
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat('initial' as Breakpoint);
|
|
480
|
-
const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
|
|
481
|
-
for (let i = startIdx + 1; i < order.length; i++) {
|
|
482
|
-
const bp = order[i];
|
|
483
|
-
if (dm && dm[bp]) {
|
|
484
|
-
return dm[bp] as PaneMode;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
return 'collapsed';
|
|
488
|
-
}, [defaultMode, shell.currentBreakpoint]);
|
|
489
|
-
|
|
490
584
|
const lastBpRef = React.useRef<Breakpoint | null>(null);
|
|
585
|
+
const lastLeftModeRef = React.useRef<PaneMode | null>(null);
|
|
586
|
+
const initNotifiedRef = React.useRef(false);
|
|
587
|
+
const resolvedDefaultOpen = useResponsiveValue((props as any).defaultOpen as any);
|
|
588
|
+
|
|
589
|
+
// Initialize from responsive defaultOpen once when uncontrolled and breakpoint ready
|
|
590
|
+
const didInitFromDefaultOpenRef = React.useRef(false);
|
|
591
|
+
React.useEffect(() => {
|
|
592
|
+
if (didInitFromDefaultOpenRef.current) return;
|
|
593
|
+
if (!shell.currentBreakpointReady) return;
|
|
594
|
+
if (typeof (props as any).open !== 'undefined') return; // controlled
|
|
595
|
+
if (typeof (props as any).defaultOpen === 'undefined') return;
|
|
596
|
+
didInitFromDefaultOpenRef.current = true;
|
|
597
|
+
const initial = Boolean(resolvedDefaultOpen);
|
|
598
|
+
shell.setLeftMode(initial ? 'expanded' : 'collapsed');
|
|
599
|
+
(props as any).onOpenChange?.(initial, { reason: 'init' });
|
|
600
|
+
}, [shell.currentBreakpointReady, (props as any).open, (props as any).defaultOpen, resolvedDefaultOpen]);
|
|
491
601
|
React.useEffect(() => {
|
|
492
|
-
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (next !== shell.leftMode) {
|
|
498
|
-
shell.setLeftMode(next);
|
|
602
|
+
// Controlled Left via Rail.open
|
|
603
|
+
if (typeof (props as any).open !== 'undefined') {
|
|
604
|
+
const shouldOpen = Boolean((props as any).open);
|
|
605
|
+
shell.setLeftMode(shouldOpen ? 'expanded' : 'collapsed');
|
|
606
|
+
return;
|
|
499
607
|
}
|
|
500
|
-
|
|
608
|
+
// defaultOpen is applied in Rail; Left no longer follows responsive defaults
|
|
609
|
+
}, [shell, (props as any).open]);
|
|
501
610
|
|
|
502
611
|
// Sync controlled mode
|
|
503
|
-
|
|
504
|
-
if (mode !== undefined && shell.leftMode !== mode) {
|
|
505
|
-
shell.setLeftMode(mode);
|
|
506
|
-
}
|
|
507
|
-
}, [mode, shell]);
|
|
612
|
+
// removed mode sync
|
|
508
613
|
|
|
509
|
-
// Emit mode changes
|
|
614
|
+
// Emit mode changes (uncontrolled toggles + init)
|
|
510
615
|
React.useEffect(() => {
|
|
511
|
-
if (
|
|
512
|
-
|
|
616
|
+
if (typeof (props as any).open !== 'undefined') return; // controlled, notifications only via parent changes
|
|
617
|
+
if (!initNotifiedRef.current && Boolean(resolvedDefaultOpen) && shell.leftMode === 'expanded') {
|
|
618
|
+
(props as any).onOpenChange?.(true, { reason: 'init' });
|
|
619
|
+
initNotifiedRef.current = true;
|
|
620
|
+
}
|
|
621
|
+
if (lastLeftModeRef.current !== null && lastLeftModeRef.current !== shell.leftMode) {
|
|
622
|
+
(props as any).onOpenChange?.(shell.leftMode === 'expanded', { reason: 'toggle' });
|
|
513
623
|
}
|
|
514
|
-
|
|
624
|
+
lastLeftModeRef.current = shell.leftMode;
|
|
625
|
+
}, [shell.leftMode, resolvedDefaultOpen]);
|
|
515
626
|
|
|
516
627
|
// Emit expand/collapse events
|
|
517
628
|
React.useEffect(() => {
|
|
@@ -568,11 +679,13 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
568
679
|
const hasRail = Boolean(railEl);
|
|
569
680
|
const hasPanel = Boolean(panelEl);
|
|
570
681
|
const includePanel = hasPanel && (shell.panelMode === 'expanded' || shell.peekTarget === 'panel');
|
|
571
|
-
|
|
682
|
+
|
|
683
|
+
// Strip control props from DOM spread
|
|
684
|
+
const { open: _openIgnored, defaultOpen: _defaultOpenIgnored, onOpenChange: _onOpenChangeIgnored, ...stackDomProps } = props as any;
|
|
572
685
|
|
|
573
686
|
return (
|
|
574
687
|
<div
|
|
575
|
-
{...
|
|
688
|
+
{...stackDomProps}
|
|
576
689
|
ref={setRef}
|
|
577
690
|
className={classNames('rt-ShellLeft', className)}
|
|
578
691
|
data-mode={shell.leftMode}
|
|
@@ -588,9 +701,21 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
588
701
|
);
|
|
589
702
|
}
|
|
590
703
|
|
|
704
|
+
// Strip control/legacy props from DOM spread
|
|
705
|
+
const {
|
|
706
|
+
open: _openIgnored,
|
|
707
|
+
defaultOpen: _defaultOpenIgnored,
|
|
708
|
+
onOpenChange: _onOpenChangeIgnored,
|
|
709
|
+
// legacy
|
|
710
|
+
mode: _legacyModeIgnored,
|
|
711
|
+
defaultMode: _legacyDefaultModeIgnored,
|
|
712
|
+
onModeChange: _legacyOnModeChangeIgnored,
|
|
713
|
+
...domProps
|
|
714
|
+
} = props as any;
|
|
715
|
+
|
|
591
716
|
return (
|
|
592
717
|
<div
|
|
593
|
-
{...
|
|
718
|
+
{...domProps}
|
|
594
719
|
ref={setRef}
|
|
595
720
|
className={classNames('rt-ShellLeft', className)}
|
|
596
721
|
data-mode={shell.leftMode}
|
|
@@ -607,48 +732,89 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
607
732
|
);
|
|
608
733
|
Left.displayName = 'Shell.Left';
|
|
609
734
|
|
|
610
|
-
const Rail = React.forwardRef<HTMLDivElement, RailProps>(
|
|
611
|
-
|
|
612
|
-
const shell = useShell();
|
|
735
|
+
const Rail = React.forwardRef<HTMLDivElement, RailProps>(({ className, presentation, expandedSize = 64, collapsible, onExpand, onCollapse, children, style, ...props }, ref) => {
|
|
736
|
+
const shell = useShell();
|
|
613
737
|
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
738
|
+
// Dev guards
|
|
739
|
+
const wasControlledRef = React.useRef<boolean | null>(null);
|
|
740
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
741
|
+
if (typeof props.open !== 'undefined' && typeof props.defaultOpen !== 'undefined') {
|
|
742
|
+
// eslint-disable-next-line no-console
|
|
743
|
+
console.error('Shell.Rail: Do not pass both `open` and `defaultOpen`. Choose one.');
|
|
744
|
+
}
|
|
745
|
+
}
|
|
618
746
|
|
|
619
|
-
|
|
747
|
+
// Warn on controlled/uncontrolled mode switch
|
|
748
|
+
React.useEffect(() => {
|
|
749
|
+
const isControlled = typeof props.open !== 'undefined';
|
|
750
|
+
if (wasControlledRef.current === null) {
|
|
751
|
+
wasControlledRef.current = isControlled;
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
if (wasControlledRef.current !== isControlled) {
|
|
755
|
+
// eslint-disable-next-line no-console
|
|
756
|
+
console.warn('Shell.Rail: Switching between controlled and uncontrolled `open` is not supported.');
|
|
757
|
+
wasControlledRef.current = isControlled;
|
|
758
|
+
}
|
|
759
|
+
}, [props.open]);
|
|
620
760
|
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
761
|
+
// Register expanded size with Left container
|
|
762
|
+
React.useEffect(() => {
|
|
763
|
+
(shell as any).onRailDefaults?.(expandedSize);
|
|
764
|
+
}, [shell, expandedSize]);
|
|
765
|
+
|
|
766
|
+
const isExpanded = shell.leftMode === 'expanded';
|
|
767
|
+
|
|
768
|
+
// Strip unknown open/defaultOpen props from DOM by not spreading them
|
|
769
|
+
const { defaultOpen: _defaultOpenIgnored, open: _openIgnored, onOpenChange: _onOpenChangeIgnored, ...domProps } = props as any;
|
|
770
|
+
|
|
771
|
+
return (
|
|
772
|
+
<div
|
|
773
|
+
{...domProps}
|
|
774
|
+
ref={ref}
|
|
775
|
+
className={classNames('rt-ShellRail', className)}
|
|
776
|
+
data-mode={shell.leftMode}
|
|
777
|
+
data-peek={(shell.currentBreakpointReady && shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') || undefined}
|
|
778
|
+
style={{
|
|
779
|
+
...style,
|
|
780
|
+
['--rail-size' as any]: `${expandedSize}px`,
|
|
781
|
+
}}
|
|
782
|
+
>
|
|
783
|
+
<div className="rt-ShellRailContent" data-visible={(shell.currentBreakpointReady && (isExpanded || (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail'))) || undefined}>
|
|
784
|
+
{children}
|
|
636
785
|
</div>
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
);
|
|
786
|
+
</div>
|
|
787
|
+
);
|
|
788
|
+
});
|
|
640
789
|
Rail.displayName = 'Shell.Rail';
|
|
641
790
|
|
|
642
791
|
// Panel
|
|
643
792
|
type HandleComponent = React.ForwardRefExoticComponent<React.ComponentPropsWithoutRef<'div'> & React.RefAttributes<HTMLDivElement>>;
|
|
644
793
|
|
|
645
|
-
type
|
|
794
|
+
type PanelOpenChangeMeta = { reason: 'toggle' | 'left' | 'init' };
|
|
795
|
+
type PanelControlledProps = { open: boolean; onOpenChange?: (open: boolean, meta: PanelOpenChangeMeta) => void; defaultOpen?: never };
|
|
796
|
+
type PanelUncontrolledProps = { defaultOpen?: boolean; onOpenChange?: (open: boolean, meta: PanelOpenChangeMeta) => void; open?: never };
|
|
797
|
+
|
|
798
|
+
type PanelSizeControlledProps = { size: number | string; defaultSize?: never };
|
|
799
|
+
type PanelSizeUncontrolledProps = { defaultSize?: number | string; size?: never };
|
|
800
|
+
|
|
801
|
+
type PanelSizeChangeMeta = { reason: 'init' | 'resize' | 'controlled' };
|
|
802
|
+
type PanelPublicProps = Omit<PaneProps, 'presentation' | 'defaultMode'> &
|
|
803
|
+
(PanelControlledProps | PanelUncontrolledProps) &
|
|
804
|
+
(PanelSizeControlledProps | PanelSizeUncontrolledProps) & {
|
|
805
|
+
onSizeChange?: (size: number, meta: PanelSizeChangeMeta) => void;
|
|
806
|
+
sizeUpdate?: 'throttle' | 'debounce';
|
|
807
|
+
sizeUpdateMs?: number;
|
|
808
|
+
};
|
|
809
|
+
type PanelComponent = React.ForwardRefExoticComponent<PanelPublicProps & React.RefAttributes<HTMLDivElement>> & {
|
|
810
|
+
Handle: HandleComponent;
|
|
811
|
+
};
|
|
646
812
|
|
|
647
813
|
type SidebarComponent = React.ForwardRefExoticComponent<
|
|
648
814
|
(Omit<PaneProps, 'mode' | 'defaultMode' | 'onModeChange'> & {
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
815
|
+
state?: Responsive<SidebarMode>;
|
|
816
|
+
defaultState?: SidebarMode;
|
|
817
|
+
onStateChange?: (mode: SidebarMode) => void;
|
|
652
818
|
thinSize?: number;
|
|
653
819
|
toggleModes?: 'both' | 'single';
|
|
654
820
|
}) &
|
|
@@ -659,12 +825,15 @@ type InspectorComponent = React.ForwardRefExoticComponent<PaneProps & React.RefA
|
|
|
659
825
|
|
|
660
826
|
type BottomComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
|
|
661
827
|
|
|
662
|
-
const Panel = React.forwardRef<HTMLDivElement,
|
|
828
|
+
const Panel = React.forwardRef<HTMLDivElement, PanelPublicProps>(
|
|
663
829
|
(
|
|
664
830
|
{
|
|
665
831
|
className,
|
|
666
|
-
|
|
667
|
-
|
|
832
|
+
defaultOpen,
|
|
833
|
+
open,
|
|
834
|
+
onOpenChange,
|
|
835
|
+
size,
|
|
836
|
+
defaultSize,
|
|
668
837
|
expandedSize = 288,
|
|
669
838
|
minSize,
|
|
670
839
|
maxSize,
|
|
@@ -682,11 +851,102 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
682
851
|
persistence,
|
|
683
852
|
children,
|
|
684
853
|
style,
|
|
854
|
+
onSizeChange,
|
|
855
|
+
sizeUpdate,
|
|
856
|
+
sizeUpdateMs = 50,
|
|
685
857
|
...props
|
|
686
858
|
},
|
|
687
859
|
ref,
|
|
688
860
|
) => {
|
|
861
|
+
// Throttled/debounced emitter for onSizeChange
|
|
862
|
+
const emitSizeChange = React.useMemo(() => {
|
|
863
|
+
if (!onSizeChange) return () => {};
|
|
864
|
+
if (sizeUpdate === 'debounce') {
|
|
865
|
+
let t: any = null;
|
|
866
|
+
const fn = (s: number, meta: PanelSizeChangeMeta) => {
|
|
867
|
+
if (t) clearTimeout(t);
|
|
868
|
+
t = setTimeout(() => {
|
|
869
|
+
onSizeChange?.(s, meta);
|
|
870
|
+
}, sizeUpdateMs);
|
|
871
|
+
};
|
|
872
|
+
return fn;
|
|
873
|
+
}
|
|
874
|
+
if (sizeUpdate === 'throttle') {
|
|
875
|
+
let last = 0;
|
|
876
|
+
return (s: number, meta: PanelSizeChangeMeta) => {
|
|
877
|
+
const now = Date.now();
|
|
878
|
+
if (now - last >= sizeUpdateMs) {
|
|
879
|
+
last = now;
|
|
880
|
+
onSizeChange?.(s, meta);
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
return (s: number, meta: PanelSizeChangeMeta) => onSizeChange?.(s, meta);
|
|
885
|
+
}, [onSizeChange, sizeUpdate, sizeUpdateMs]);
|
|
689
886
|
const shell = useShell();
|
|
887
|
+
const prevPanelModeRef = React.useRef<PaneMode | null>(null);
|
|
888
|
+
const prevLeftModeRef = React.useRef<PaneMode | null>(null);
|
|
889
|
+
const initNotifiedRef = React.useRef(false);
|
|
890
|
+
|
|
891
|
+
// Dev-only runtime guard
|
|
892
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
893
|
+
if (typeof open !== 'undefined' && typeof defaultOpen !== 'undefined') {
|
|
894
|
+
// eslint-disable-next-line no-console
|
|
895
|
+
console.error('Shell.Panel: Do not pass both `open` and `defaultOpen`. Choose one.');
|
|
896
|
+
}
|
|
897
|
+
if (typeof size !== 'undefined' && typeof defaultSize !== 'undefined') {
|
|
898
|
+
// eslint-disable-next-line no-console
|
|
899
|
+
console.error('Shell.Panel: Do not pass both `size` and `defaultSize`. Choose one.');
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
// Initialize uncontrolled open state from defaultOpen on first mount
|
|
904
|
+
React.useEffect(() => {
|
|
905
|
+
if (typeof open === 'undefined' && typeof defaultOpen === 'boolean') {
|
|
906
|
+
if (defaultOpen) {
|
|
907
|
+
// Ensure Left is expanded before expanding Panel
|
|
908
|
+
shell.setLeftMode('expanded');
|
|
909
|
+
shell.setPanelMode('expanded');
|
|
910
|
+
} else {
|
|
911
|
+
shell.setPanelMode('collapsed');
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
// run only on mount
|
|
915
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
916
|
+
}, []);
|
|
917
|
+
|
|
918
|
+
// Controlled sync: mirror shell state when `open` is provided
|
|
919
|
+
React.useEffect(() => {
|
|
920
|
+
if (typeof open === 'undefined') return;
|
|
921
|
+
if (open) {
|
|
922
|
+
if (shell.leftMode !== 'expanded') shell.setLeftMode('expanded');
|
|
923
|
+
if (shell.panelMode !== 'expanded') shell.setPanelMode('expanded');
|
|
924
|
+
} else {
|
|
925
|
+
if (shell.panelMode !== 'collapsed') shell.setPanelMode('collapsed');
|
|
926
|
+
}
|
|
927
|
+
}, [open, shell.leftMode, shell.panelMode]);
|
|
928
|
+
|
|
929
|
+
// Dev-only warning if switching controlled/uncontrolled between renders
|
|
930
|
+
React.useEffect(() => {
|
|
931
|
+
const isControlled = typeof open !== 'undefined';
|
|
932
|
+
(Panel as any)._wasControlled = (Panel as any)._wasControlled ?? isControlled;
|
|
933
|
+
if ((Panel as any)._wasControlled !== isControlled) {
|
|
934
|
+
// eslint-disable-next-line no-console
|
|
935
|
+
console.warn('Shell.Panel: Switching between controlled and uncontrolled `open` is not supported.');
|
|
936
|
+
(Panel as any)._wasControlled = isControlled;
|
|
937
|
+
}
|
|
938
|
+
}, [open]);
|
|
939
|
+
|
|
940
|
+
// Notify init open
|
|
941
|
+
React.useEffect(() => {
|
|
942
|
+
if (initNotifiedRef.current) return;
|
|
943
|
+
if (typeof open === 'undefined' && defaultOpen && shell.panelMode === 'expanded') {
|
|
944
|
+
onOpenChange?.(true, { reason: 'init' });
|
|
945
|
+
initNotifiedRef.current = true;
|
|
946
|
+
}
|
|
947
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
948
|
+
}, []);
|
|
949
|
+
|
|
690
950
|
React.useEffect(() => {
|
|
691
951
|
(shell as any).onPanelDefaults?.(expandedSize);
|
|
692
952
|
}, [shell, expandedSize]);
|
|
@@ -705,6 +965,27 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
705
965
|
|
|
706
966
|
const isOverlay = shell.leftResolvedPresentation === 'overlay';
|
|
707
967
|
|
|
968
|
+
// Normalize CSS lengths to px
|
|
969
|
+
const normalizeToPx = React.useCallback((value: number | string | undefined): number | undefined => {
|
|
970
|
+
if (value == null) return undefined;
|
|
971
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
972
|
+
const str = String(value).trim();
|
|
973
|
+
if (!str) return undefined;
|
|
974
|
+
if (str.endsWith('px')) return Number.parseFloat(str);
|
|
975
|
+
if (str.endsWith('rem')) {
|
|
976
|
+
const rem = Number.parseFloat(getComputedStyle(document.documentElement).fontSize || '16') || 16;
|
|
977
|
+
return Number.parseFloat(str) * rem;
|
|
978
|
+
}
|
|
979
|
+
if (str.endsWith('%')) {
|
|
980
|
+
const pct = Number.parseFloat(str);
|
|
981
|
+
const base = document.documentElement.clientWidth || window.innerWidth || 0;
|
|
982
|
+
return (pct / 100) * base;
|
|
983
|
+
}
|
|
984
|
+
// Bare number-like string
|
|
985
|
+
const n = Number.parseFloat(str);
|
|
986
|
+
return Number.isFinite(n) ? n : undefined;
|
|
987
|
+
}, []);
|
|
988
|
+
|
|
708
989
|
// Derive a default persistence adapter from paneId if none provided
|
|
709
990
|
const persistenceAdapter = React.useMemo(() => {
|
|
710
991
|
if (!paneId || persistence) return persistence;
|
|
@@ -747,6 +1028,37 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
747
1028
|
}
|
|
748
1029
|
}, [isOverlay, expandedSize]);
|
|
749
1030
|
|
|
1031
|
+
// Apply defaultSize on mount when uncontrolled
|
|
1032
|
+
React.useEffect(() => {
|
|
1033
|
+
if (!localRef.current) return;
|
|
1034
|
+
if (typeof size === 'undefined' && typeof defaultSize !== 'undefined') {
|
|
1035
|
+
const px = normalizeToPx(defaultSize);
|
|
1036
|
+
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
1037
|
+
// Clamp to min/max if provided
|
|
1038
|
+
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
1039
|
+
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
1040
|
+
const clamped = Math.min(maxPx ?? px, Math.max(minPx ?? px, px));
|
|
1041
|
+
localRef.current.style.setProperty('--panel-size', `${clamped}px`);
|
|
1042
|
+
emitSizeChange(clamped, { reason: 'init' });
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1046
|
+
}, []);
|
|
1047
|
+
|
|
1048
|
+
// Controlled size sync
|
|
1049
|
+
React.useEffect(() => {
|
|
1050
|
+
if (!localRef.current) return;
|
|
1051
|
+
if (typeof size === 'undefined') return;
|
|
1052
|
+
const px = normalizeToPx(size);
|
|
1053
|
+
if (typeof px === 'number' && Number.isFinite(px)) {
|
|
1054
|
+
const minPx = typeof minSize === 'number' ? minSize : undefined;
|
|
1055
|
+
const maxPx = typeof maxSize === 'number' ? maxSize : undefined;
|
|
1056
|
+
const clamped = Math.min(maxPx ?? px, Math.max(minPx ?? px, px));
|
|
1057
|
+
localRef.current.style.setProperty('--panel-size', `${clamped}px`);
|
|
1058
|
+
emitSizeChange(clamped, { reason: 'controlled' });
|
|
1059
|
+
}
|
|
1060
|
+
}, [size, minSize, maxSize, normalizeToPx]);
|
|
1061
|
+
|
|
750
1062
|
// Ensure Left container width is auto whenever Panel is expanded in fixed presentation
|
|
751
1063
|
React.useEffect(() => {
|
|
752
1064
|
if (!localRef.current) return;
|
|
@@ -760,6 +1072,22 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
760
1072
|
|
|
761
1073
|
const isExpanded = shell.leftMode === 'expanded' && shell.panelMode === 'expanded';
|
|
762
1074
|
|
|
1075
|
+
// Notify on internal toggles and left cascade
|
|
1076
|
+
React.useEffect(() => {
|
|
1077
|
+
const prevPanel = prevPanelModeRef.current;
|
|
1078
|
+
const prevLeft = prevLeftModeRef.current;
|
|
1079
|
+
if (prevPanel !== null && prevPanel !== shell.panelMode) {
|
|
1080
|
+
const open = shell.panelMode === 'expanded';
|
|
1081
|
+
let reason: PanelOpenChangeMeta['reason'] = 'toggle';
|
|
1082
|
+
if (prevLeft !== shell.leftMode && shell.leftMode === 'collapsed' && !open) {
|
|
1083
|
+
reason = 'left';
|
|
1084
|
+
}
|
|
1085
|
+
onOpenChange?.(open, { reason });
|
|
1086
|
+
}
|
|
1087
|
+
prevPanelModeRef.current = shell.panelMode;
|
|
1088
|
+
prevLeftModeRef.current = shell.leftMode;
|
|
1089
|
+
}, [shell.panelMode, shell.leftMode, onOpenChange]);
|
|
1090
|
+
|
|
763
1091
|
// Provide resizer handle when fixed (not overlay)
|
|
764
1092
|
const handleEl =
|
|
765
1093
|
resizable && shell.leftResolvedPresentation !== 'overlay' && isExpanded ? (
|
|
@@ -789,6 +1117,7 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
789
1117
|
},
|
|
790
1118
|
onResizeEnd: (size) => {
|
|
791
1119
|
onResizeEnd?.(size);
|
|
1120
|
+
emitSizeChange(size, { reason: 'resize' });
|
|
792
1121
|
persistenceAdapter?.save?.(size);
|
|
793
1122
|
},
|
|
794
1123
|
target: 'panel',
|
|
@@ -804,14 +1133,24 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
804
1133
|
</PaneResizeContext.Provider>
|
|
805
1134
|
) : null;
|
|
806
1135
|
|
|
1136
|
+
// Strip control props from DOM spread
|
|
1137
|
+
const {
|
|
1138
|
+
defaultOpen: _panelDefaultOpenIgnored,
|
|
1139
|
+
open: _panelOpenIgnored,
|
|
1140
|
+
onOpenChange: _panelOnOpenChangeIgnored,
|
|
1141
|
+
size: _panelSizeIgnored,
|
|
1142
|
+
defaultSize: _panelDefaultSizeIgnored,
|
|
1143
|
+
...panelDomProps
|
|
1144
|
+
} = props as any;
|
|
1145
|
+
|
|
807
1146
|
return (
|
|
808
1147
|
<div
|
|
809
|
-
{...
|
|
1148
|
+
{...panelDomProps}
|
|
810
1149
|
ref={setRef}
|
|
811
1150
|
className={classNames('rt-ShellPanel', className)}
|
|
812
1151
|
data-mode={shell.panelMode}
|
|
813
|
-
data-visible={isExpanded || (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') || undefined}
|
|
814
|
-
data-peek={(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') || undefined}
|
|
1152
|
+
data-visible={(shell.currentBreakpointReady && (isExpanded || (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel'))) || undefined}
|
|
1153
|
+
data-peek={(shell.currentBreakpointReady && shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') || undefined}
|
|
815
1154
|
style={{
|
|
816
1155
|
...style,
|
|
817
1156
|
['--panel-size' as any]: `${expandedSize}px`,
|