@kushagradhawan/kookie-ui 0.1.48 → 0.1.50

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