@kushagradhawan/kookie-ui 0.1.49 → 0.1.51
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 +880 -243
- 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 +9 -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 -0
- 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/cjs/components/sidebar.d.ts +7 -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 -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 +9 -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 -0
- 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/dist/esm/components/sidebar.d.ts +7 -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 +3 -3
- 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 +17 -18
- package/src/components/_internal/base-sidebar-menu.css +23 -21
- package/src/components/_internal/base-sidebar.css +20 -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 +176 -69
- package/src/components/chatbar.css +240 -21
- package/src/components/chatbar.tsx +246 -290
- package/src/components/sheet.css +8 -16
- package/src/components/shell.context.tsx +79 -0
- package/src/components/shell.css +28 -2
- package/src/components/shell.hooks.ts +35 -0
- package/src/components/shell.tsx +574 -214
- package/src/components/shell.types.ts +2 -0
- package/src/components/sidebar.css +233 -33
- package/src/components/sidebar.tsx +247 -213
- package/styles.css +841 -204
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,12 +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
|
-
|
|
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 }), []);
|
|
104
256
|
|
|
105
257
|
// Removed: defaultMode responsiveness and manual change tracking
|
|
106
258
|
|
|
@@ -114,12 +266,7 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
114
266
|
sidebarToggleComputerRef.current = fn;
|
|
115
267
|
}, []);
|
|
116
268
|
|
|
117
|
-
//
|
|
118
|
-
React.useEffect(() => {
|
|
119
|
-
if (leftMode === 'collapsed') {
|
|
120
|
-
setPanelMode('collapsed');
|
|
121
|
-
}
|
|
122
|
-
}, [leftMode]);
|
|
269
|
+
// Reducer handles left→panel cascade; no effect needed
|
|
123
270
|
|
|
124
271
|
// Composition validation
|
|
125
272
|
React.useEffect(() => {
|
|
@@ -155,88 +302,37 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
155
302
|
|
|
156
303
|
const togglePane = React.useCallback(
|
|
157
304
|
(target: PaneTarget) => {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
break;
|
|
163
|
-
case 'panel':
|
|
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
|
-
}
|
|
171
|
-
break;
|
|
172
|
-
case 'sidebar':
|
|
173
|
-
setSidebarMode((prev) => sidebarToggleComputerRef.current(prev as SidebarMode));
|
|
174
|
-
break;
|
|
175
|
-
case 'inspector':
|
|
176
|
-
setInspectorMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
177
|
-
break;
|
|
178
|
-
case 'bottom':
|
|
179
|
-
setBottomMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
|
|
180
|
-
break;
|
|
305
|
+
if (target === 'sidebar') {
|
|
306
|
+
const next = sidebarToggleComputerRef.current(paneState.sidebarMode as SidebarMode);
|
|
307
|
+
setSidebarMode(next);
|
|
308
|
+
return;
|
|
181
309
|
}
|
|
310
|
+
dispatchPane({ type: 'TOGGLE_PANE', target });
|
|
182
311
|
},
|
|
183
|
-
[
|
|
312
|
+
[paneState.sidebarMode],
|
|
184
313
|
);
|
|
185
314
|
|
|
186
315
|
const expandPane = React.useCallback((target: PaneTarget) => {
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
}
|
|
316
|
+
if (target === 'sidebar') return setSidebarMode('expanded');
|
|
317
|
+
dispatchPane({ type: 'EXPAND_PANE', target });
|
|
206
318
|
}, []);
|
|
207
319
|
|
|
208
320
|
const collapsePane = React.useCallback((target: PaneTarget) => {
|
|
209
|
-
|
|
210
|
-
|
|
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
|
-
}
|
|
321
|
+
if (target === 'sidebar') return setSidebarMode('collapsed');
|
|
322
|
+
dispatchPane({ type: 'COLLAPSE_PANE', target });
|
|
227
323
|
}, []);
|
|
228
324
|
|
|
229
325
|
const baseContextValue = React.useMemo(
|
|
230
326
|
() => ({
|
|
231
|
-
leftMode,
|
|
327
|
+
leftMode: paneState.leftMode,
|
|
232
328
|
setLeftMode,
|
|
233
|
-
panelMode,
|
|
329
|
+
panelMode: paneState.panelMode,
|
|
234
330
|
setPanelMode,
|
|
235
|
-
sidebarMode,
|
|
331
|
+
sidebarMode: paneState.sidebarMode,
|
|
236
332
|
setSidebarMode,
|
|
237
|
-
inspectorMode,
|
|
333
|
+
inspectorMode: paneState.inspectorMode,
|
|
238
334
|
setInspectorMode,
|
|
239
|
-
bottomMode,
|
|
335
|
+
bottomMode: paneState.bottomMode,
|
|
240
336
|
setBottomMode,
|
|
241
337
|
hasLeft,
|
|
242
338
|
setHasLeft,
|
|
@@ -254,11 +350,11 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
254
350
|
onPanelDefaults,
|
|
255
351
|
}),
|
|
256
352
|
[
|
|
257
|
-
leftMode,
|
|
258
|
-
panelMode,
|
|
259
|
-
sidebarMode,
|
|
260
|
-
inspectorMode,
|
|
261
|
-
bottomMode,
|
|
353
|
+
paneState.leftMode,
|
|
354
|
+
paneState.panelMode,
|
|
355
|
+
paneState.sidebarMode,
|
|
356
|
+
paneState.inspectorMode,
|
|
357
|
+
paneState.bottomMode,
|
|
262
358
|
hasLeft,
|
|
263
359
|
hasSidebar,
|
|
264
360
|
currentBreakpoint,
|
|
@@ -286,6 +382,14 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
286
382
|
const inspectorEls = childArray.filter((el) => isType(el, Inspector));
|
|
287
383
|
const bottomEls = childArray.filter((el) => isType(el, Bottom));
|
|
288
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
|
+
|
|
289
393
|
const heightStyle = React.useMemo(() => {
|
|
290
394
|
if (height === 'full') return { height: '100vh' };
|
|
291
395
|
if (height === 'auto') return { height: 'auto' };
|
|
@@ -299,6 +403,17 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
299
403
|
const peekPane = React.useCallback((target: PaneTarget) => setPeekTarget(target), []);
|
|
300
404
|
const clearPeek = React.useCallback(() => setPeekTarget(null), []);
|
|
301
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
|
+
|
|
302
417
|
return (
|
|
303
418
|
<div {...props} ref={ref} className={classNames('rt-ShellRoot', className)} style={{ ...heightStyle, ...props.style }}>
|
|
304
419
|
<ShellProvider
|
|
@@ -310,44 +425,63 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
310
425
|
clearPeek,
|
|
311
426
|
}}
|
|
312
427
|
>
|
|
313
|
-
{
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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>
|
|
351
485
|
</ShellProvider>
|
|
352
486
|
</div>
|
|
353
487
|
);
|
|
@@ -375,9 +509,6 @@ Header.displayName = 'Shell.Header';
|
|
|
375
509
|
// Pane Props Interface (shared by Panel, Sidebar, Inspector, Bottom)
|
|
376
510
|
interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
377
511
|
presentation?: ResponsivePresentation;
|
|
378
|
-
mode?: PaneMode;
|
|
379
|
-
defaultMode?: ResponsiveMode;
|
|
380
|
-
onModeChange?: (mode: PaneMode) => void;
|
|
381
512
|
expandedSize?: number;
|
|
382
513
|
minSize?: number;
|
|
383
514
|
maxSize?: number;
|
|
@@ -400,29 +531,32 @@ interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
|
400
531
|
// Left container (auto-created for Rail+Panel)
|
|
401
532
|
interface LeftProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
402
533
|
presentation?: ResponsivePresentation;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
534
|
+
// New: passthrough from Rail
|
|
535
|
+
open?: boolean;
|
|
536
|
+
defaultOpen?: boolean;
|
|
537
|
+
onOpenChange?: (open: boolean, meta: { reason: 'init' | 'toggle' | 'panel' | 'responsive' }) => void;
|
|
406
538
|
collapsible?: boolean;
|
|
407
539
|
onExpand?: () => void;
|
|
408
540
|
onCollapse?: () => void;
|
|
409
541
|
}
|
|
410
542
|
|
|
411
543
|
// Rail (special case)
|
|
412
|
-
|
|
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'> & {
|
|
413
550
|
presentation?: ResponsivePresentation;
|
|
414
|
-
mode?: PaneMode;
|
|
415
|
-
defaultMode?: ResponsiveMode;
|
|
416
|
-
onModeChange?: (mode: PaneMode) => void;
|
|
417
551
|
expandedSize?: number;
|
|
418
552
|
collapsible?: boolean;
|
|
419
553
|
onExpand?: () => void;
|
|
420
554
|
onCollapse?: () => void;
|
|
421
|
-
}
|
|
555
|
+
} & (RailControlledProps | RailUncontrolledProps);
|
|
422
556
|
|
|
423
557
|
// Left container - behaves like Inspector but contains Rail+Panel
|
|
424
558
|
const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
425
|
-
({ className, presentation = { initial: '
|
|
559
|
+
({ className, presentation = { initial: 'fixed', sm: 'fixed' }, collapsible = true, onExpand, onCollapse, children, style, ...props }, ref) => {
|
|
426
560
|
const shell = useShell();
|
|
427
561
|
const resolvedPresentation = useResponsivePresentation(presentation);
|
|
428
562
|
const isOverlay = resolvedPresentation === 'overlay';
|
|
@@ -447,50 +581,48 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
447
581
|
return () => shell.setHasLeft(false);
|
|
448
582
|
}, [shell]);
|
|
449
583
|
|
|
450
|
-
// Always-follow responsive defaultMode for uncontrolled Left (Rail stack)
|
|
451
|
-
const resolveResponsiveMode = React.useCallback((): PaneMode => {
|
|
452
|
-
if (typeof defaultMode === 'string') return defaultMode as PaneMode;
|
|
453
|
-
const dm = defaultMode as Partial<Record<Breakpoint, PaneMode>> | undefined;
|
|
454
|
-
if (dm && dm[shell.currentBreakpoint as Breakpoint]) {
|
|
455
|
-
return dm[shell.currentBreakpoint as Breakpoint] as PaneMode;
|
|
456
|
-
}
|
|
457
|
-
const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
|
|
458
|
-
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat('initial' as Breakpoint);
|
|
459
|
-
const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
|
|
460
|
-
for (let i = startIdx + 1; i < order.length; i++) {
|
|
461
|
-
const bp = order[i];
|
|
462
|
-
if (dm && dm[bp]) {
|
|
463
|
-
return dm[bp] as PaneMode;
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
return 'collapsed';
|
|
467
|
-
}, [defaultMode, shell.currentBreakpoint]);
|
|
468
|
-
|
|
469
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]);
|
|
470
601
|
React.useEffect(() => {
|
|
471
|
-
|
|
472
|
-
if (
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (next !== shell.leftMode) {
|
|
477
|
-
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;
|
|
478
607
|
}
|
|
479
|
-
|
|
608
|
+
// defaultOpen is applied in Rail; Left no longer follows responsive defaults
|
|
609
|
+
}, [shell, (props as any).open]);
|
|
480
610
|
|
|
481
611
|
// Sync controlled mode
|
|
482
|
-
|
|
483
|
-
if (mode !== undefined && shell.leftMode !== mode) {
|
|
484
|
-
shell.setLeftMode(mode);
|
|
485
|
-
}
|
|
486
|
-
}, [mode, shell]);
|
|
612
|
+
// removed mode sync
|
|
487
613
|
|
|
488
|
-
// Emit mode changes
|
|
614
|
+
// Emit mode changes (uncontrolled toggles + init)
|
|
489
615
|
React.useEffect(() => {
|
|
490
|
-
if (
|
|
491
|
-
|
|
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' });
|
|
492
623
|
}
|
|
493
|
-
|
|
624
|
+
lastLeftModeRef.current = shell.leftMode;
|
|
625
|
+
}, [shell.leftMode, resolvedDefaultOpen]);
|
|
494
626
|
|
|
495
627
|
// Emit expand/collapse events
|
|
496
628
|
React.useEffect(() => {
|
|
@@ -547,11 +679,13 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
547
679
|
const hasRail = Boolean(railEl);
|
|
548
680
|
const hasPanel = Boolean(panelEl);
|
|
549
681
|
const includePanel = hasPanel && (shell.panelMode === 'expanded' || shell.peekTarget === 'panel');
|
|
550
|
-
|
|
682
|
+
|
|
683
|
+
// Strip control props from DOM spread
|
|
684
|
+
const { open: _openIgnored, defaultOpen: _defaultOpenIgnored, onOpenChange: _onOpenChangeIgnored, ...stackDomProps } = props as any;
|
|
551
685
|
|
|
552
686
|
return (
|
|
553
687
|
<div
|
|
554
|
-
{...
|
|
688
|
+
{...stackDomProps}
|
|
555
689
|
ref={setRef}
|
|
556
690
|
className={classNames('rt-ShellLeft', className)}
|
|
557
691
|
data-mode={shell.leftMode}
|
|
@@ -567,9 +701,21 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
567
701
|
);
|
|
568
702
|
}
|
|
569
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
|
+
|
|
570
716
|
return (
|
|
571
717
|
<div
|
|
572
|
-
{...
|
|
718
|
+
{...domProps}
|
|
573
719
|
ref={setRef}
|
|
574
720
|
className={classNames('rt-ShellLeft', className)}
|
|
575
721
|
data-mode={shell.leftMode}
|
|
@@ -586,48 +732,89 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
|
|
|
586
732
|
);
|
|
587
733
|
Left.displayName = 'Shell.Left';
|
|
588
734
|
|
|
589
|
-
const Rail = React.forwardRef<HTMLDivElement, RailProps>(
|
|
590
|
-
|
|
591
|
-
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();
|
|
592
737
|
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
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
|
+
}
|
|
597
746
|
|
|
598
|
-
|
|
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]);
|
|
599
760
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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}
|
|
615
785
|
</div>
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
);
|
|
786
|
+
</div>
|
|
787
|
+
);
|
|
788
|
+
});
|
|
619
789
|
Rail.displayName = 'Shell.Rail';
|
|
620
790
|
|
|
621
791
|
// Panel
|
|
622
792
|
type HandleComponent = React.ForwardRefExoticComponent<React.ComponentPropsWithoutRef<'div'> & React.RefAttributes<HTMLDivElement>>;
|
|
623
793
|
|
|
624
|
-
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
|
+
};
|
|
625
812
|
|
|
626
813
|
type SidebarComponent = React.ForwardRefExoticComponent<
|
|
627
814
|
(Omit<PaneProps, 'mode' | 'defaultMode' | 'onModeChange'> & {
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
815
|
+
state?: Responsive<SidebarMode>;
|
|
816
|
+
defaultState?: SidebarMode;
|
|
817
|
+
onStateChange?: (mode: SidebarMode) => void;
|
|
631
818
|
thinSize?: number;
|
|
632
819
|
toggleModes?: 'both' | 'single';
|
|
633
820
|
}) &
|
|
@@ -638,12 +825,15 @@ type InspectorComponent = React.ForwardRefExoticComponent<PaneProps & React.RefA
|
|
|
638
825
|
|
|
639
826
|
type BottomComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
|
|
640
827
|
|
|
641
|
-
const Panel = React.forwardRef<HTMLDivElement,
|
|
828
|
+
const Panel = React.forwardRef<HTMLDivElement, PanelPublicProps>(
|
|
642
829
|
(
|
|
643
830
|
{
|
|
644
831
|
className,
|
|
645
|
-
|
|
646
|
-
|
|
832
|
+
defaultOpen,
|
|
833
|
+
open,
|
|
834
|
+
onOpenChange,
|
|
835
|
+
size,
|
|
836
|
+
defaultSize,
|
|
647
837
|
expandedSize = 288,
|
|
648
838
|
minSize,
|
|
649
839
|
maxSize,
|
|
@@ -661,11 +851,102 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
661
851
|
persistence,
|
|
662
852
|
children,
|
|
663
853
|
style,
|
|
854
|
+
onSizeChange,
|
|
855
|
+
sizeUpdate,
|
|
856
|
+
sizeUpdateMs = 50,
|
|
664
857
|
...props
|
|
665
858
|
},
|
|
666
859
|
ref,
|
|
667
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]);
|
|
668
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
|
+
|
|
669
950
|
React.useEffect(() => {
|
|
670
951
|
(shell as any).onPanelDefaults?.(expandedSize);
|
|
671
952
|
}, [shell, expandedSize]);
|
|
@@ -684,6 +965,27 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
684
965
|
|
|
685
966
|
const isOverlay = shell.leftResolvedPresentation === 'overlay';
|
|
686
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
|
+
|
|
687
989
|
// Derive a default persistence adapter from paneId if none provided
|
|
688
990
|
const persistenceAdapter = React.useMemo(() => {
|
|
689
991
|
if (!paneId || persistence) return persistence;
|
|
@@ -726,6 +1028,37 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
726
1028
|
}
|
|
727
1029
|
}, [isOverlay, expandedSize]);
|
|
728
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
|
+
|
|
729
1062
|
// Ensure Left container width is auto whenever Panel is expanded in fixed presentation
|
|
730
1063
|
React.useEffect(() => {
|
|
731
1064
|
if (!localRef.current) return;
|
|
@@ -739,6 +1072,22 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
739
1072
|
|
|
740
1073
|
const isExpanded = shell.leftMode === 'expanded' && shell.panelMode === 'expanded';
|
|
741
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
|
+
|
|
742
1091
|
// Provide resizer handle when fixed (not overlay)
|
|
743
1092
|
const handleEl =
|
|
744
1093
|
resizable && shell.leftResolvedPresentation !== 'overlay' && isExpanded ? (
|
|
@@ -768,6 +1117,7 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
768
1117
|
},
|
|
769
1118
|
onResizeEnd: (size) => {
|
|
770
1119
|
onResizeEnd?.(size);
|
|
1120
|
+
emitSizeChange(size, { reason: 'resize' });
|
|
771
1121
|
persistenceAdapter?.save?.(size);
|
|
772
1122
|
},
|
|
773
1123
|
target: 'panel',
|
|
@@ -783,14 +1133,24 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
|
|
|
783
1133
|
</PaneResizeContext.Provider>
|
|
784
1134
|
) : null;
|
|
785
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
|
+
|
|
786
1146
|
return (
|
|
787
1147
|
<div
|
|
788
|
-
{...
|
|
1148
|
+
{...panelDomProps}
|
|
789
1149
|
ref={setRef}
|
|
790
1150
|
className={classNames('rt-ShellPanel', className)}
|
|
791
1151
|
data-mode={shell.panelMode}
|
|
792
|
-
data-visible={isExpanded || (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') || undefined}
|
|
793
|
-
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}
|
|
794
1154
|
style={{
|
|
795
1155
|
...style,
|
|
796
1156
|
['--panel-size' as any]: `${expandedSize}px`,
|