@kushagradhawan/kookie-ui 0.1.47 → 0.1.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (149) hide show
  1. package/components.css +858 -30
  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/alert-dialog.d.ts.map +1 -1
  23. package/dist/cjs/components/alert-dialog.js +1 -1
  24. package/dist/cjs/components/alert-dialog.js.map +2 -2
  25. package/dist/cjs/components/dialog.d.ts.map +1 -1
  26. package/dist/cjs/components/dialog.js +1 -1
  27. package/dist/cjs/components/dialog.js.map +2 -2
  28. package/dist/cjs/components/schemas/index.d.ts +2 -0
  29. package/dist/cjs/components/schemas/index.d.ts.map +1 -1
  30. package/dist/cjs/components/schemas/index.js +1 -1
  31. package/dist/cjs/components/schemas/index.js.map +3 -3
  32. package/dist/cjs/components/schemas/shell.schema.d.ts +1025 -0
  33. package/dist/cjs/components/schemas/shell.schema.d.ts.map +1 -0
  34. package/dist/cjs/components/schemas/shell.schema.js +2 -0
  35. package/dist/cjs/components/schemas/shell.schema.js.map +7 -0
  36. package/dist/cjs/components/shell.context.d.ts +37 -0
  37. package/dist/cjs/components/shell.context.d.ts.map +1 -0
  38. package/dist/cjs/components/shell.context.js +2 -0
  39. package/dist/cjs/components/shell.context.js.map +7 -0
  40. package/dist/cjs/components/shell.d.ts +6 -68
  41. package/dist/cjs/components/shell.d.ts.map +1 -1
  42. package/dist/cjs/components/shell.hooks.d.ts +3 -0
  43. package/dist/cjs/components/shell.hooks.d.ts.map +1 -0
  44. package/dist/cjs/components/shell.hooks.js +2 -0
  45. package/dist/cjs/components/shell.hooks.js.map +7 -0
  46. package/dist/cjs/components/shell.js +1 -1
  47. package/dist/cjs/components/shell.js.map +3 -3
  48. package/dist/cjs/components/shell.types.d.ts +20 -0
  49. package/dist/cjs/components/shell.types.d.ts.map +1 -0
  50. package/dist/cjs/components/shell.types.js +2 -0
  51. package/dist/cjs/components/shell.types.js.map +7 -0
  52. package/dist/cjs/components/sidebar.d.ts +1 -1
  53. package/dist/cjs/components/sidebar.d.ts.map +1 -1
  54. package/dist/cjs/components/sidebar.js +1 -1
  55. package/dist/cjs/components/sidebar.js.map +3 -3
  56. package/dist/esm/components/_internal/shell-bottom.d.ts +31 -0
  57. package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -0
  58. package/dist/esm/components/_internal/shell-bottom.js +2 -0
  59. package/dist/esm/components/_internal/shell-bottom.js.map +7 -0
  60. package/dist/esm/components/_internal/shell-handles.d.ts +7 -0
  61. package/dist/esm/components/_internal/shell-handles.d.ts.map +1 -0
  62. package/dist/esm/components/_internal/shell-handles.js +2 -0
  63. package/dist/esm/components/_internal/shell-handles.js.map +7 -0
  64. package/dist/esm/components/_internal/shell-inspector.d.ts +31 -0
  65. package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -0
  66. package/dist/esm/components/_internal/shell-inspector.js +2 -0
  67. package/dist/esm/components/_internal/shell-inspector.js.map +7 -0
  68. package/dist/esm/components/_internal/shell-resize.d.ts +24 -0
  69. package/dist/esm/components/_internal/shell-resize.d.ts.map +1 -0
  70. package/dist/esm/components/_internal/shell-resize.js +2 -0
  71. package/dist/esm/components/_internal/shell-resize.js.map +7 -0
  72. package/dist/esm/components/_internal/shell-sidebar.d.ts +37 -0
  73. package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -0
  74. package/dist/esm/components/_internal/shell-sidebar.js +2 -0
  75. package/dist/esm/components/_internal/shell-sidebar.js.map +7 -0
  76. package/dist/esm/components/alert-dialog.d.ts.map +1 -1
  77. package/dist/esm/components/alert-dialog.js +1 -1
  78. package/dist/esm/components/alert-dialog.js.map +2 -2
  79. package/dist/esm/components/dialog.d.ts.map +1 -1
  80. package/dist/esm/components/dialog.js +1 -1
  81. package/dist/esm/components/dialog.js.map +2 -2
  82. package/dist/esm/components/schemas/index.d.ts +2 -0
  83. package/dist/esm/components/schemas/index.d.ts.map +1 -1
  84. package/dist/esm/components/schemas/index.js +1 -1
  85. package/dist/esm/components/schemas/index.js.map +3 -3
  86. package/dist/esm/components/schemas/shell.schema.d.ts +1025 -0
  87. package/dist/esm/components/schemas/shell.schema.d.ts.map +1 -0
  88. package/dist/esm/components/schemas/shell.schema.js +2 -0
  89. package/dist/esm/components/schemas/shell.schema.js.map +7 -0
  90. package/dist/esm/components/shell.context.d.ts +37 -0
  91. package/dist/esm/components/shell.context.d.ts.map +1 -0
  92. package/dist/esm/components/shell.context.js +2 -0
  93. package/dist/esm/components/shell.context.js.map +7 -0
  94. package/dist/esm/components/shell.d.ts +6 -68
  95. package/dist/esm/components/shell.d.ts.map +1 -1
  96. package/dist/esm/components/shell.hooks.d.ts +3 -0
  97. package/dist/esm/components/shell.hooks.d.ts.map +1 -0
  98. package/dist/esm/components/shell.hooks.js +2 -0
  99. package/dist/esm/components/shell.hooks.js.map +7 -0
  100. package/dist/esm/components/shell.js +1 -1
  101. package/dist/esm/components/shell.js.map +3 -3
  102. package/dist/esm/components/shell.types.d.ts +20 -0
  103. package/dist/esm/components/shell.types.d.ts.map +1 -0
  104. package/dist/esm/components/shell.types.js +2 -0
  105. package/dist/esm/components/shell.types.js.map +7 -0
  106. package/dist/esm/components/sidebar.d.ts +1 -1
  107. package/dist/esm/components/sidebar.d.ts.map +1 -1
  108. package/dist/esm/components/sidebar.js +1 -1
  109. package/dist/esm/components/sidebar.js.map +2 -2
  110. package/layout/utilities.css +168 -84
  111. package/layout.css +168 -84
  112. package/package.json +2 -1
  113. package/schemas/base-button.json +1 -1
  114. package/schemas/button.json +1 -1
  115. package/schemas/icon-button.json +1 -1
  116. package/schemas/index.json +6 -6
  117. package/schemas/shell-bottom.json +168 -0
  118. package/schemas/shell-content.json +34 -0
  119. package/schemas/shell-handle.json +34 -0
  120. package/schemas/shell-header.json +42 -0
  121. package/schemas/shell-inspector.json +171 -0
  122. package/schemas/shell-panel.json +167 -0
  123. package/schemas/shell-rail.json +132 -0
  124. package/schemas/shell-root.json +54 -0
  125. package/schemas/shell-sidebar.json +182 -0
  126. package/schemas/shell-trigger.json +76 -0
  127. package/schemas/toggle-button.json +1 -1
  128. package/schemas/toggle-icon-button.json +1 -1
  129. package/src/components/_internal/shell-bottom.tsx +251 -0
  130. package/src/components/_internal/shell-handles.tsx +193 -0
  131. package/src/components/_internal/shell-inspector.tsx +242 -0
  132. package/src/components/_internal/shell-resize.tsx +30 -0
  133. package/src/components/_internal/shell-sidebar.tsx +347 -0
  134. package/src/components/alert-dialog.tsx +6 -0
  135. package/src/components/dialog.tsx +6 -0
  136. package/src/components/schemas/index.ts +46 -0
  137. package/src/components/schemas/shell.schema.ts +403 -0
  138. package/src/components/shell.context.tsx +56 -0
  139. package/src/components/shell.css +5 -17
  140. package/src/components/shell.hooks.ts +31 -0
  141. package/src/components/shell.tsx +368 -1684
  142. package/src/components/shell.types.ts +27 -0
  143. package/src/components/sidebar.tsx +1 -1
  144. package/src/styles/tokens/blur.css +2 -2
  145. package/src/styles/tokens/color.css +2 -2
  146. package/styles.css +1031 -116
  147. package/tokens/base.css +5 -2
  148. package/tokens.css +5 -2
  149. 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,266 @@ interface ShellRootProps extends React.ComponentPropsWithoutRef<'div'> {
443
92
  height?: 'full' | 'auto' | string | number;
444
93
  }
445
94
 
446
- const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(
447
- ({ 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
+ const [inspectorMode, setInspectorMode] = React.useState<PaneMode>('collapsed');
103
+ const [bottomMode, setBottomMode] = React.useState<PaneMode>('collapsed');
456
104
 
457
- // Removed: defaultMode responsiveness and manual change tracking
105
+ // Removed: defaultMode responsiveness and manual change tracking
458
106
 
459
- // Composition detection
460
- const [hasLeft, setHasLeft] = React.useState(false);
461
- const [hasSidebar, setHasSidebar] = React.useState(false);
107
+ // Composition detection
108
+ const [hasLeft, setHasLeft] = React.useState(false);
109
+ const [hasSidebar, setHasSidebar] = React.useState(false);
462
110
 
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
- );
111
+ // Customizable sidebar toggle sequencing
112
+ const sidebarToggleComputerRef = React.useRef<(current: SidebarMode) => SidebarMode>((current) => (current === 'collapsed' ? 'thin' : current === 'thin' ? 'expanded' : 'collapsed'));
113
+ const setSidebarToggleComputer = React.useCallback((fn: (current: SidebarMode) => SidebarMode) => {
114
+ sidebarToggleComputerRef.current = fn;
115
+ }, []);
474
116
 
475
- // Left collapse cascades to Panel
476
- React.useEffect(() => {
477
- if (leftMode === 'collapsed') {
478
- setPanelMode('collapsed');
479
- }
480
- }, [leftMode]);
117
+ // Left collapse cascades to Panel
118
+ React.useEffect(() => {
119
+ if (leftMode === 'collapsed') {
120
+ setPanelMode('collapsed');
121
+ }
122
+ }, [leftMode]);
481
123
 
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]);
124
+ // Composition validation
125
+ React.useEffect(() => {
126
+ if (hasSidebar && hasLeft) {
127
+ console.warn('Shell: Sidebar cannot coexist with Rail or Panel. Use either Rail+Panel OR Sidebar.');
128
+ }
129
+ }, [hasSidebar, hasLeft]);
130
+
131
+ // Left presentation + defaults from children
132
+ const [devLeftPres, setDevLeftPres] = React.useState<PresentationValue | undefined>(undefined);
133
+ const onLeftPres = React.useCallback((p: PresentationValue) => setDevLeftPres(p), []);
134
+ const railDefaultSizeRef = React.useRef<number>(64);
135
+ const panelDefaultSizeRef = React.useRef<number>(288);
136
+ const onRailDefaults = React.useCallback((size: number) => {
137
+ railDefaultSizeRef.current = size;
138
+ }, []);
139
+ const onPanelDefaults = React.useCallback((size: number) => {
140
+ panelDefaultSizeRef.current = size;
141
+ }, []);
511
142
 
512
- 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
- );
143
+ // Determine children presence for left composition
144
+ const hasLeftChildren = React.useMemo(() => {
145
+ const childArray = React.Children.toArray(children) as React.ReactElement[];
146
+ const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && (el.type === comp || (el as any).type?.displayName === comp.displayName);
147
+ return childArray.some((el) => isType(el, Rail) || isType(el, Panel));
148
+ }, [children]);
549
149
 
550
- 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
- }, []);
150
+ const hasSidebarChildren = React.useMemo(() => {
151
+ const childArray = React.Children.toArray(children) as React.ReactElement[];
152
+ const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && (el.type === comp || (el as any).type?.displayName === comp.displayName);
153
+ return childArray.some((el) => isType(el, Sidebar));
154
+ }, [children]);
571
155
 
572
- const collapsePane = React.useCallback((target: PaneTarget) => {
156
+ const togglePane = React.useCallback(
157
+ (target: PaneTarget) => {
573
158
  switch (target) {
574
159
  case 'left':
575
160
  case 'rail':
576
- setLeftMode('collapsed');
161
+ setLeftMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
577
162
  break;
578
163
  case 'panel':
579
- setPanelMode('collapsed');
164
+ // Panel toggle: expand left if collapsed, then toggle panel
165
+ if (leftMode === 'collapsed') {
166
+ setLeftMode('expanded');
167
+ setPanelMode('expanded');
168
+ } else {
169
+ setPanelMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
170
+ }
580
171
  break;
581
172
  case 'sidebar':
582
- setSidebarMode('collapsed');
173
+ setSidebarMode((prev) => sidebarToggleComputerRef.current(prev as SidebarMode));
583
174
  break;
584
175
  case 'inspector':
585
- setInspectorMode('collapsed');
176
+ setInspectorMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
586
177
  break;
587
178
  case 'bottom':
588
- setBottomMode('collapsed');
179
+ setBottomMode((prev) => (prev === 'expanded' ? 'collapsed' : 'expanded'));
589
180
  break;
590
181
  }
591
- }, []);
592
-
593
- 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
- );
182
+ },
183
+ [leftMode],
184
+ );
185
+
186
+ const expandPane = React.useCallback((target: PaneTarget) => {
187
+ switch (target) {
188
+ case 'left':
189
+ case 'rail':
190
+ setLeftMode('expanded');
191
+ break;
192
+ case 'panel':
193
+ setLeftMode('expanded');
194
+ setPanelMode('expanded');
195
+ break;
196
+ case 'sidebar':
197
+ setSidebarMode('expanded');
198
+ break;
199
+ case 'inspector':
200
+ setInspectorMode('expanded');
201
+ break;
202
+ case 'bottom':
203
+ setBottomMode('expanded');
204
+ break;
205
+ }
206
+ }, []);
640
207
 
641
- // 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), []);
208
+ const collapsePane = React.useCallback((target: PaneTarget) => {
209
+ switch (target) {
210
+ case 'left':
211
+ case 'rail':
212
+ setLeftMode('collapsed');
213
+ break;
214
+ case 'panel':
215
+ setPanelMode('collapsed');
216
+ break;
217
+ case 'sidebar':
218
+ setSidebarMode('collapsed');
219
+ break;
220
+ case 'inspector':
221
+ setInspectorMode('collapsed');
222
+ break;
223
+ case 'bottom':
224
+ setBottomMode('collapsed');
225
+ break;
226
+ }
227
+ }, []);
667
228
 
668
- return (
669
- <div
670
- {...props}
671
- ref={ref}
672
- className={classNames('rt-ShellRoot', className)}
673
- style={{ ...heightStyle, ...props.style }}
229
+ const baseContextValue = React.useMemo(
230
+ () => ({
231
+ leftMode,
232
+ setLeftMode,
233
+ panelMode,
234
+ setPanelMode,
235
+ sidebarMode,
236
+ setSidebarMode,
237
+ inspectorMode,
238
+ setInspectorMode,
239
+ bottomMode,
240
+ setBottomMode,
241
+ hasLeft,
242
+ setHasLeft,
243
+ hasSidebar,
244
+ setHasSidebar,
245
+ currentBreakpoint,
246
+ currentBreakpointReady,
247
+ leftResolvedPresentation: devLeftPres,
248
+ togglePane,
249
+ expandPane,
250
+ collapsePane,
251
+ setSidebarToggleComputer,
252
+ onLeftPres,
253
+ onRailDefaults,
254
+ onPanelDefaults,
255
+ }),
256
+ [
257
+ leftMode,
258
+ panelMode,
259
+ sidebarMode,
260
+ inspectorMode,
261
+ bottomMode,
262
+ hasLeft,
263
+ hasSidebar,
264
+ currentBreakpoint,
265
+ currentBreakpointReady,
266
+ devLeftPres,
267
+ togglePane,
268
+ expandPane,
269
+ collapsePane,
270
+ setSidebarToggleComputer,
271
+ onLeftPres,
272
+ onRailDefaults,
273
+ onPanelDefaults,
274
+ ],
275
+ );
276
+
277
+ // Organize children by type
278
+ const childArray = React.Children.toArray(children) as React.ReactElement[];
279
+ const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && (el.type === comp || (el as any).type?.displayName === comp.displayName);
280
+
281
+ const headerEls = childArray.filter((el) => isType(el, Header));
282
+ const railEls = childArray.filter((el) => isType(el, Rail));
283
+ const panelEls = childArray.filter((el) => isType(el, Panel));
284
+ const sidebarEls = childArray.filter((el) => isType(el, Sidebar));
285
+ const contentEls = childArray.filter((el) => isType(el, Content));
286
+ const inspectorEls = childArray.filter((el) => isType(el, Inspector));
287
+ const bottomEls = childArray.filter((el) => isType(el, Bottom));
288
+
289
+ const heightStyle = React.useMemo(() => {
290
+ if (height === 'full') return { height: '100vh' };
291
+ if (height === 'auto') return { height: 'auto' };
292
+ if (typeof height === 'string') return { height };
293
+ if (typeof height === 'number') return { height: `${height}px` };
294
+ return {};
295
+ }, [height]);
296
+
297
+ // Peek state (layout-only overlay without mode changes)
298
+ const [peekTarget, setPeekTarget] = React.useState<PaneTarget | null>(null);
299
+ const peekPane = React.useCallback((target: PaneTarget) => setPeekTarget(target), []);
300
+ const clearPeek = React.useCallback(() => setPeekTarget(null), []);
301
+
302
+ return (
303
+ <div {...props} ref={ref} className={classNames('rt-ShellRoot', className)} style={{ ...heightStyle, ...props.style }}>
304
+ <ShellProvider
305
+ value={{
306
+ ...baseContextValue,
307
+ peekTarget,
308
+ setPeekTarget,
309
+ peekPane,
310
+ clearPeek,
311
+ }}
674
312
  >
675
- <ShellContext.Provider
676
- value={{
677
- ...baseContextValue,
678
- peekTarget,
679
- setPeekTarget,
680
- peekPane,
681
- clearPeek,
682
- }}
313
+ {headerEls}
314
+ <div
315
+ className="rt-ShellBody"
316
+ data-peek-target={peekTarget ?? undefined}
317
+ style={
318
+ peekTarget === 'rail' || peekTarget === 'panel'
319
+ ? ({
320
+ ['--peek-rail-width' as any]: `${railDefaultSizeRef.current}px`,
321
+ } as React.CSSProperties)
322
+ : undefined
323
+ }
683
324
  >
684
- {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
- );
325
+ {hasLeftChildren && !hasSidebarChildren
326
+ ? (() => {
327
+ const firstRail = railEls[0] as any;
328
+ const passthroughProps = firstRail
329
+ ? {
330
+ mode: firstRail.props?.mode,
331
+ defaultMode: firstRail.props?.defaultMode,
332
+ onModeChange: firstRail.props?.onModeChange,
333
+ presentation: firstRail.props?.presentation,
334
+ collapsible: firstRail.props?.collapsible,
335
+ onExpand: firstRail.props?.onExpand,
336
+ onCollapse: firstRail.props?.onCollapse,
337
+ }
338
+ : {};
339
+ return (
340
+ <Left {...(passthroughProps as any)}>
341
+ {railEls}
342
+ {panelEls}
343
+ </Left>
344
+ );
345
+ })()
346
+ : sidebarEls}
347
+ {contentEls}
348
+ {inspectorEls}
349
+ </div>
350
+ {bottomEls}
351
+ </ShellProvider>
352
+ </div>
353
+ );
354
+ });
727
355
  Root.displayName = 'Shell.Root';
728
356
 
729
357
  // Header
@@ -731,19 +359,17 @@ interface ShellHeaderProps extends React.ComponentPropsWithoutRef<'header'> {
731
359
  height?: number;
732
360
  }
733
361
 
734
- const Header = React.forwardRef<HTMLElement, ShellHeaderProps>(
735
- ({ 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
- );
362
+ const Header = React.forwardRef<HTMLElement, ShellHeaderProps>(({ className, height = 64, style, ...props }, ref) => (
363
+ <header
364
+ {...props}
365
+ ref={ref}
366
+ className={classNames('rt-ShellHeader', className)}
367
+ style={{
368
+ ...style,
369
+ ['--shell-header-height' as any]: `${height}px`,
370
+ }}
371
+ />
372
+ ));
747
373
  Header.displayName = 'Shell.Header';
748
374
 
749
375
  // Pane Props Interface (shared by Panel, Sidebar, Inspector, Bottom)
@@ -796,22 +422,7 @@ interface RailProps extends React.ComponentPropsWithoutRef<'div'> {
796
422
 
797
423
  // Left container - behaves like Inspector but contains Rail+Panel
798
424
  const Left = React.forwardRef<HTMLDivElement, LeftProps>(
799
- (
800
- {
801
- className,
802
- presentation = { initial: 'overlay', sm: 'fixed' },
803
- mode,
804
- defaultMode = 'collapsed',
805
- onModeChange,
806
- collapsible = true,
807
- onExpand,
808
- onCollapse,
809
- children,
810
- style,
811
- ...props
812
- },
813
- ref,
814
- ) => {
425
+ ({ className, presentation = { initial: 'overlay', sm: 'fixed' }, mode, defaultMode = 'collapsed', onModeChange, collapsible = true, onExpand, onCollapse, children, style, ...props }, ref) => {
815
426
  const shell = useShell();
816
427
  const resolvedPresentation = useResponsivePresentation(presentation);
817
428
  const isOverlay = resolvedPresentation === 'overlay';
@@ -844,9 +455,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
844
455
  return dm[shell.currentBreakpoint as Breakpoint] as PaneMode;
845
456
  }
846
457
  const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
847
- const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat(
848
- 'initial' as Breakpoint,
849
- );
458
+ const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat('initial' as Breakpoint);
850
459
  const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
851
460
  for (let i = startIdx + 1; i < order.length; i++) {
852
461
  const bp = order[i];
@@ -867,14 +476,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
867
476
  if (next !== shell.leftMode) {
868
477
  shell.setLeftMode(next);
869
478
  }
870
- }, [
871
- mode,
872
- shell.currentBreakpoint,
873
- shell.currentBreakpointReady,
874
- resolveResponsiveMode,
875
- shell.leftMode,
876
- shell.setLeftMode,
877
- ]);
479
+ }, [mode, shell.currentBreakpoint, shell.currentBreakpointReady, resolveResponsiveMode, shell.leftMode, shell.setLeftMode]);
878
480
 
879
481
  // Sync controlled mode
880
482
  React.useEffect(() => {
@@ -907,27 +509,16 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
907
509
  const open = shell.leftMode === 'expanded';
908
510
  // Compute overlay width from child Rail/Panel expanded sizes
909
511
  const childArray = React.Children.toArray(children) as React.ReactElement[];
910
- const isType = (el: React.ReactElement, comp: any) =>
911
- React.isValidElement(el) && el.type === comp;
512
+ const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && el.type === comp;
912
513
  const railEl = childArray.find((el) => isType(el, Rail));
913
514
  const panelEl = childArray.find((el) => isType(el, Panel));
914
- const railSize =
915
- 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;
515
+ const railSize = typeof (railEl as any)?.props?.expandedSize === 'number' ? (railEl as any).props.expandedSize : 64;
516
+ const panelSize = typeof (panelEl as any)?.props?.expandedSize === 'number' ? (panelEl as any).props.expandedSize : 288;
922
517
  const hasRail = Boolean(railEl);
923
518
  const hasPanel = Boolean(panelEl);
924
- const overlayPx =
925
- (hasRail ? railSize : 0) + (shell.panelMode === 'expanded' && hasPanel ? panelSize : 0);
519
+ const overlayPx = (hasRail ? railSize : 0) + (shell.panelMode === 'expanded' && hasPanel ? panelSize : 0);
926
520
  return (
927
- <Sheet.Root
928
- open={open}
929
- onOpenChange={(o) => shell.setLeftMode(o ? 'expanded' : 'collapsed')}
930
- >
521
+ <Sheet.Root open={open} onOpenChange={(o) => shell.setLeftMode(o ? 'expanded' : 'collapsed')}>
931
522
  <Sheet.Content
932
523
  side="start"
933
524
  style={{ padding: 0 }}
@@ -948,22 +539,14 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
948
539
  const open = shell.leftMode === 'expanded';
949
540
  // Compute floating width from child Rail/Panel expanded sizes (like overlay)
950
541
  const childArray = React.Children.toArray(children) as React.ReactElement[];
951
- const isType = (el: React.ReactElement, comp: any) =>
952
- React.isValidElement(el) && el.type === comp;
542
+ const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && el.type === comp;
953
543
  const railEl = childArray.find((el) => isType(el, Rail));
954
544
  const panelEl = childArray.find((el) => isType(el, Panel));
955
- const railSize =
956
- 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;
545
+ const railSize = typeof (railEl as any)?.props?.expandedSize === 'number' ? (railEl as any).props.expandedSize : 64;
546
+ const panelSize = typeof (panelEl as any)?.props?.expandedSize === 'number' ? (panelEl as any).props.expandedSize : 288;
963
547
  const hasRail = Boolean(railEl);
964
548
  const hasPanel = Boolean(panelEl);
965
- const includePanel =
966
- hasPanel && (shell.panelMode === 'expanded' || shell.peekTarget === 'panel');
549
+ const includePanel = hasPanel && (shell.panelMode === 'expanded' || shell.peekTarget === 'panel');
967
550
  const floatingWidthPx = (hasRail ? railSize : 0) + (includePanel ? panelSize : 0);
968
551
 
969
552
  return (
@@ -972,12 +555,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
972
555
  ref={setRef}
973
556
  className={classNames('rt-ShellLeft', className)}
974
557
  data-mode={shell.leftMode}
975
- data-peek={
976
- shell.peekTarget === 'left' ||
977
- shell.peekTarget === 'rail' ||
978
- shell.peekTarget === 'panel' ||
979
- undefined
980
- }
558
+ data-peek={shell.peekTarget === 'left' || shell.peekTarget === 'rail' || shell.peekTarget === 'panel' || undefined}
981
559
  data-presentation={resolvedPresentation}
982
560
  style={{
983
561
  ...style,
@@ -995,12 +573,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
995
573
  ref={setRef}
996
574
  className={classNames('rt-ShellLeft', className)}
997
575
  data-mode={shell.leftMode}
998
- data-peek={
999
- shell.peekTarget === 'left' ||
1000
- shell.peekTarget === 'rail' ||
1001
- shell.peekTarget === 'panel' ||
1002
- undefined
1003
- }
576
+ data-peek={shell.peekTarget === 'left' || shell.peekTarget === 'rail' || shell.peekTarget === 'panel' || undefined}
1004
577
  data-presentation={resolvedPresentation}
1005
578
  style={{
1006
579
  ...style,
@@ -1014,23 +587,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
1014
587
  Left.displayName = 'Shell.Left';
1015
588
 
1016
589
  const Rail = React.forwardRef<HTMLDivElement, RailProps>(
1017
- (
1018
- {
1019
- className,
1020
- presentation,
1021
- mode,
1022
- defaultMode,
1023
- onModeChange,
1024
- expandedSize = 64,
1025
- collapsible,
1026
- onExpand,
1027
- onCollapse,
1028
- children,
1029
- style,
1030
- ...props
1031
- },
1032
- ref,
1033
- ) => {
590
+ ({ className, presentation, mode, defaultMode, onModeChange, expandedSize = 64, collapsible, onExpand, onCollapse, children, style, ...props }, ref) => {
1034
591
  const shell = useShell();
1035
592
 
1036
593
  // Register expanded size with Left container
@@ -1046,22 +603,13 @@ const Rail = React.forwardRef<HTMLDivElement, RailProps>(
1046
603
  ref={ref}
1047
604
  className={classNames('rt-ShellRail', className)}
1048
605
  data-mode={shell.leftMode}
1049
- data-peek={
1050
- (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') || undefined
1051
- }
606
+ data-peek={(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') || undefined}
1052
607
  style={{
1053
608
  ...style,
1054
609
  ['--rail-size' as any]: `${expandedSize}px`,
1055
610
  }}
1056
611
  >
1057
- <div
1058
- className="rt-ShellRailContent"
1059
- data-visible={
1060
- isExpanded ||
1061
- (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') ||
1062
- undefined
1063
- }
1064
- >
612
+ <div className="rt-ShellRailContent" data-visible={isExpanded || (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'rail') || undefined}>
1065
613
  {children}
1066
614
  </div>
1067
615
  </div>
@@ -1071,13 +619,9 @@ const Rail = React.forwardRef<HTMLDivElement, RailProps>(
1071
619
  Rail.displayName = 'Shell.Rail';
1072
620
 
1073
621
  // Panel
1074
- type HandleComponent = React.ForwardRefExoticComponent<
1075
- React.ComponentPropsWithoutRef<'div'> & React.RefAttributes<HTMLDivElement>
1076
- >;
622
+ type HandleComponent = React.ForwardRefExoticComponent<React.ComponentPropsWithoutRef<'div'> & React.RefAttributes<HTMLDivElement>>;
1077
623
 
1078
- type PanelComponent = React.ForwardRefExoticComponent<
1079
- Omit<PaneProps, 'defaultMode'> & React.RefAttributes<HTMLDivElement>
1080
- > & { Handle: HandleComponent };
624
+ type PanelComponent = React.ForwardRefExoticComponent<Omit<PaneProps, 'defaultMode'> & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
1081
625
 
1082
626
  type SidebarComponent = React.ForwardRefExoticComponent<
1083
627
  (Omit<PaneProps, 'mode' | 'defaultMode' | 'onModeChange'> & {
@@ -1090,13 +634,9 @@ type SidebarComponent = React.ForwardRefExoticComponent<
1090
634
  React.RefAttributes<HTMLDivElement>
1091
635
  > & { Handle: HandleComponent };
1092
636
 
1093
- type InspectorComponent = React.ForwardRefExoticComponent<
1094
- PaneProps & React.RefAttributes<HTMLDivElement>
1095
- > & { Handle: HandleComponent };
637
+ type InspectorComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
1096
638
 
1097
- type BottomComponent = React.ForwardRefExoticComponent<
1098
- PaneProps & React.RefAttributes<HTMLDivElement>
1099
- > & { Handle: HandleComponent };
639
+ type BottomComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
1100
640
 
1101
641
  const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' | 'defaultMode'>>(
1102
642
  (
@@ -1139,12 +679,8 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
1139
679
  [ref],
1140
680
  );
1141
681
  const childArray = React.Children.toArray(children) as React.ReactElement[];
1142
- const handleChildren = childArray.filter(
1143
- (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
- );
682
+ const handleChildren = childArray.filter((el: React.ReactElement) => React.isValidElement(el) && el.type === PanelHandle);
683
+ const contentChildren = childArray.filter((el: React.ReactElement) => !(React.isValidElement(el) && el.type === PanelHandle));
1148
684
 
1149
685
  const isOverlay = shell.leftResolvedPresentation === 'overlay';
1150
686
 
@@ -1193,11 +729,7 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
1193
729
  // Ensure Left container width is auto whenever Panel is expanded in fixed presentation
1194
730
  React.useEffect(() => {
1195
731
  if (!localRef.current) return;
1196
- if (
1197
- shell.leftResolvedPresentation !== 'overlay' &&
1198
- shell.leftMode === 'expanded' &&
1199
- shell.panelMode === 'expanded'
1200
- ) {
732
+ if (shell.leftResolvedPresentation !== 'overlay' && shell.leftMode === 'expanded' && shell.panelMode === 'expanded') {
1201
733
  const leftEl = (localRef.current.parentElement as HTMLElement) || null;
1202
734
  try {
1203
735
  leftEl?.style.removeProperty('width');
@@ -1247,11 +779,7 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
1247
779
  requestToggle: () => shell.togglePane('panel'),
1248
780
  }}
1249
781
  >
1250
- {handleChildren.length > 0 ? (
1251
- handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i }))
1252
- ) : (
1253
- <PaneHandle />
1254
- )}
782
+ {handleChildren.length > 0 ? handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i })) : <PaneHandle />}
1255
783
  </PaneResizeContext.Provider>
1256
784
  ) : null;
1257
785
 
@@ -1261,15 +789,8 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
1261
789
  ref={setRef}
1262
790
  className={classNames('rt-ShellPanel', className)}
1263
791
  data-mode={shell.panelMode}
1264
- data-visible={
1265
- isExpanded ||
1266
- (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') ||
1267
- undefined
1268
- }
1269
- data-peek={
1270
- (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') ||
1271
- undefined
1272
- }
792
+ data-visible={isExpanded || (shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') || undefined}
793
+ data-peek={(shell.leftResolvedPresentation !== 'overlay' && shell.peekTarget === 'panel') || undefined}
1273
794
  style={{
1274
795
  ...style,
1275
796
  ['--panel-size' as any]: `${expandedSize}px`,
@@ -1286,941 +807,104 @@ const Panel = React.forwardRef<HTMLDivElement, Omit<PaneProps, 'presentation' |
1286
807
  Panel.displayName = 'Shell.Panel';
1287
808
  Panel.Handle = PanelHandle;
1288
809
 
1289
- // Sidebar (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
- );
810
+ // Sidebar moved to ./_internal/shell-sidebar
1350
811
 
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]);
812
+ // Content (always required)
813
+ interface ShellContentProps extends React.ComponentPropsWithoutRef<'main'> {}
1359
814
 
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
- }, []);
815
+ const Content = React.forwardRef<HTMLElement, ShellContentProps>(({ className, ...props }, ref) => <main {...props} ref={ref} className={classNames('rt-ShellContent', className)} />);
816
+ Content.displayName = 'Shell.Content';
1370
817
 
1371
- // Sync controlled mode
1372
- React.useEffect(() => {
1373
- if (mode !== undefined && shell.sidebarMode !== mode) {
1374
- shell.setSidebarMode(mode);
1375
- }
1376
- }, [mode, shell]);
818
+ // Inspector moved to ./_internal/shell-inspector
1377
819
 
1378
- // Emit mode changes
1379
- React.useEffect(() => {
1380
- if (mode === undefined) {
1381
- onModeChange?.(shell.sidebarMode);
1382
- }
1383
- }, [shell.sidebarMode, mode, onModeChange]);
820
+ // Bottom
821
+ // Bottom moved to ./_internal/shell-bottom
822
+ // (Bottom implementation extracted)
1384
823
 
1385
- // Emit expand/collapse events
1386
- React.useEffect(() => {
1387
- if (shell.sidebarMode === 'expanded') {
1388
- onExpand?.();
1389
- } else {
1390
- onCollapse?.();
1391
- }
1392
- }, [shell.sidebarMode, onExpand, onCollapse]);
824
+ // Trigger
825
+ // PaneTarget type moved to shell.types.ts
826
+ type TriggerAction = 'toggle' | 'expand' | 'collapse';
1393
827
 
1394
- // Option A: thin is width-only; content remains visible whenever not collapsed
1395
- const isContentVisible = shell.sidebarMode !== 'collapsed';
828
+ interface TriggerProps extends React.ComponentPropsWithoutRef<'button'> {
829
+ target: PaneTarget;
830
+ action?: TriggerAction;
831
+ /**
832
+ * Whether to show peek preview on hover when the target pane is collapsed.
833
+ * Defaults to false.
834
+ */
835
+ peekOnHover?: boolean;
836
+ }
1396
837
 
1397
- // 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]);
838
+ const Trigger = React.forwardRef<HTMLButtonElement, TriggerProps>(({ target, action = 'toggle', peekOnHover, onClick, onMouseEnter, onMouseLeave, children, ...props }, ref) => {
839
+ const shell = useShell();
1414
840
 
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]);
841
+ const handleClick = React.useCallback(
842
+ (event: React.MouseEvent<HTMLButtonElement>) => {
843
+ onClick?.(event);
1429
844
 
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;
845
+ // Clear any active peek on this target before toggling to avoid sticky peek state
846
+ if ((shell as any).peekTarget === target) {
847
+ shell.clearPeek();
1445
848
  }
1446
- return 'collapsed';
1447
- }, [defaultMode, shell.currentBreakpoint]);
1448
-
1449
- // Register custom toggle behavior based on toggleModes (both|single)
1450
- const shellForToggle = useShell();
1451
- const resolveDefaultSidebarMode = React.useCallback((): SidebarMode => {
1452
- const resolved = resolveResponsiveMode();
1453
- return resolved === 'thin' || resolved === 'expanded' ? resolved : 'expanded';
1454
- }, [resolveResponsiveMode]);
1455
-
1456
- React.useEffect(() => {
1457
- if (!shellForToggle.setSidebarToggleComputer) return;
1458
- const strategy: 'both' | 'single' = toggleModes ?? 'both';
1459
- const compute = (current: SidebarMode): SidebarMode => {
1460
- if (strategy === 'both') {
1461
- // collapsed -> thin -> expanded -> collapsed
1462
- if (current === 'collapsed') return 'thin';
1463
- if (current === 'thin') return 'expanded';
1464
- return 'collapsed';
1465
- }
1466
- // single: toggle between collapsed and resolved default mode
1467
- const target = resolveDefaultSidebarMode();
1468
- if (current === 'collapsed') return target;
1469
- if (current === target) return 'collapsed';
1470
- // if in the other non-collapsed state, jump to the target
1471
- return target;
1472
- };
1473
- shellForToggle.setSidebarToggleComputer(compute);
1474
- return () => {
1475
- // default fallback sequence when unmounting
1476
- shellForToggle.setSidebarToggleComputer?.((cur) =>
1477
- cur === 'collapsed' ? 'thin' : cur === 'thin' ? 'expanded' : 'collapsed',
1478
- );
1479
- };
1480
- }, [shellForToggle, toggleModes, resolveDefaultSidebarMode]);
1481
849
 
1482
- // 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;
850
+ switch (action) {
851
+ case 'toggle':
852
+ shell.togglePane(target);
853
+ break;
854
+ case 'expand':
855
+ shell.expandPane(target);
856
+ break;
857
+ case 'collapse':
858
+ shell.collapsePane(target);
859
+ break;
1489
860
  }
1490
- }, [shell.sidebarMode, thinSize, expandedSize]);
1491
-
1492
- // (moved above)
1493
-
1494
- const lastSidebarBpRef = React.useRef<Breakpoint | null>(null);
1495
- React.useEffect(() => {
1496
- if (mode !== undefined) return; // controlled wins
1497
- if (!shell.currentBreakpointReady) return; // avoid SSR mismatch
1498
- if (lastSidebarBpRef.current === shell.currentBreakpoint) return; // only on bp change
1499
- lastSidebarBpRef.current = shell.currentBreakpoint as Breakpoint;
1500
- const next = resolveResponsiveMode();
1501
- if (next !== shell.sidebarMode) shell.setSidebarMode(next);
1502
- }, [
1503
- mode,
1504
- shell.currentBreakpoint,
1505
- shell.currentBreakpointReady,
1506
- resolveResponsiveMode,
1507
- shell.sidebarMode,
1508
- shell.setSidebarMode,
1509
- ]);
1510
-
1511
- const handleEl =
1512
- resizable && !isOverlay && shell.sidebarMode === 'expanded' ? (
1513
- <PaneResizeContext.Provider
1514
- value={{
1515
- containerRef: localRef,
1516
- cssVarName: '--sidebar-size',
1517
- minSize,
1518
- maxSize,
1519
- defaultSize: expandedSize,
1520
- orientation: 'vertical',
1521
- edge: 'end',
1522
- computeNext: (client, startClient, startSize) => {
1523
- const isRtl = getComputedStyle(localRef.current!).direction === 'rtl';
1524
- const delta = client - startClient;
1525
- return startSize + (isRtl ? -delta : delta);
1526
- },
1527
- onResize,
1528
- onResizeStart,
1529
- onResizeEnd: (size) => {
1530
- onResizeEnd?.(size);
1531
- persistenceAdapter?.save?.(size);
1532
- },
1533
- target: 'sidebar',
1534
- collapsible,
1535
- snapPoints,
1536
- snapTolerance: snapTolerance ?? 8,
1537
- collapseThreshold,
1538
- requestCollapse: () => shell.setSidebarMode('collapsed'),
1539
- requestToggle: () => shell.togglePane('sidebar'),
1540
- }}
1541
- >
1542
- {handleChildren.length > 0 ? (
1543
- handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i }))
1544
- ) : (
1545
- <PaneHandle />
1546
- )}
1547
- </PaneResizeContext.Provider>
1548
- ) : null;
1549
-
1550
- if (isOverlay) {
1551
- const open = shell.sidebarMode !== 'collapsed';
1552
- return (
1553
- <Sheet.Root
1554
- open={open}
1555
- onOpenChange={(o) => shell.setSidebarMode(o ? 'expanded' : 'collapsed')}
1556
- >
1557
- <Sheet.Content
1558
- side="start"
1559
- style={{ padding: 0 }}
1560
- width={{
1561
- initial: `${open ? (shell.sidebarMode === 'thin' ? thinSize : expandedSize) : lastOverlayWidthRef.current}px`,
1562
- }}
1563
- >
1564
- <VisuallyHidden>
1565
- <Sheet.Title>Sidebar</Sheet.Title>
1566
- </VisuallyHidden>
1567
- {contentChildren}
1568
- </Sheet.Content>
1569
- </Sheet.Root>
1570
- );
1571
- }
1572
-
1573
- return (
1574
- <div
1575
- {...props}
1576
- ref={setRef}
1577
- className={classNames('rt-ShellSidebar', className)}
1578
- data-mode={shell.sidebarMode}
1579
- data-peek={shell.peekTarget === 'sidebar' || undefined}
1580
- data-presentation={resolvedPresentation}
1581
- data-open={(isStacked && isContentVisible) || undefined}
1582
- style={{
1583
- ...style,
1584
- ['--sidebar-size' as any]: `${expandedSize}px`,
1585
- ['--sidebar-thin-size' as any]: `${thinSize}px`,
1586
- ['--sidebar-min-size' as any]: `${minSize}px`,
1587
- ['--sidebar-max-size' as any]: `${maxSize}px`,
1588
- // When peeking in fixed presentation and collapsed, preview next state's width
1589
- ...(shell.peekTarget === 'sidebar' && shell.sidebarMode === 'collapsed' && !isOverlay
1590
- ? (() => {
1591
- const strategy: 'both' | 'single' = toggleModes ?? 'both';
1592
- const current = shell.sidebarMode as SidebarMode;
1593
- let next: SidebarMode;
1594
- if (strategy === 'both') {
1595
- next =
1596
- current === 'collapsed'
1597
- ? 'thin'
1598
- : current === 'thin'
1599
- ? 'expanded'
1600
- : 'collapsed';
1601
- } else {
1602
- const target = resolveDefaultSidebarMode();
1603
- next = current === 'collapsed' ? target : 'collapsed';
1604
- }
1605
- if (next === 'thin') {
1606
- return {
1607
- ['--peek-sidebar-width' as any]: `${thinSize}px`,
1608
- } as React.CSSProperties;
1609
- }
1610
- return {
1611
- ['--peek-sidebar-width' as any]: `var(--sidebar-size, ${expandedSize}px)`,
1612
- } as React.CSSProperties;
1613
- })()
1614
- : {}),
1615
- }}
1616
- >
1617
- <div className="rt-ShellSidebarContent" data-visible={isContentVisible || undefined}>
1618
- {contentChildren}
1619
- </div>
1620
- {handleEl}
1621
- </div>
1622
- );
1623
- },
1624
- ) as SidebarComponent;
1625
- Sidebar.displayName = 'Shell.Sidebar';
1626
- Sidebar.Handle = SidebarHandle;
1627
-
1628
- // Content (always required)
1629
- interface ShellContentProps extends React.ComponentPropsWithoutRef<'main'> {}
1630
-
1631
- const Content = React.forwardRef<HTMLElement, ShellContentProps>(({ className, ...props }, ref) => (
1632
- <main {...props} ref={ref} className={classNames('rt-ShellContent', className)} />
1633
- ));
1634
- Content.displayName = 'Shell.Content';
1635
-
1636
- // Inspector
1637
- const Inspector = React.forwardRef<HTMLDivElement, PaneProps>(
1638
- (
1639
- {
1640
- className,
1641
- presentation = { initial: 'overlay', lg: 'fixed' },
1642
- mode,
1643
- defaultMode = 'collapsed',
1644
- onModeChange,
1645
- expandedSize = 320,
1646
- minSize = 200,
1647
- maxSize = 500,
1648
- resizable = false,
1649
- collapsible = true,
1650
- onExpand,
1651
- onCollapse,
1652
- onResize,
1653
- onResizeStart,
1654
- onResizeEnd,
1655
- snapPoints,
1656
- snapTolerance,
1657
- collapseThreshold,
1658
- paneId,
1659
- persistence,
1660
- children,
1661
- style,
1662
- ...props
1663
861
  },
1664
- 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
- );
862
+ [shell, target, action, onClick],
863
+ );
864
+
865
+ const isCollapsed = (() => {
866
+ switch (target) {
867
+ case 'left':
868
+ case 'rail':
869
+ return shell.leftMode === 'collapsed';
870
+ case 'panel':
871
+ return shell.leftMode === 'collapsed' || shell.panelMode === 'collapsed';
872
+ case 'sidebar':
873
+ return shell.sidebarMode === 'collapsed';
874
+ case 'inspector':
875
+ return shell.inspectorMode === 'collapsed';
876
+ case 'bottom':
877
+ return shell.bottomMode === 'collapsed';
1840
878
  }
1841
-
1842
- 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
879
+ })();
880
+
881
+ const handleMouseEnter = React.useCallback(
882
+ (event: React.MouseEvent<HTMLButtonElement>) => {
883
+ onMouseEnter?.(event);
884
+ if (!peekOnHover || !isCollapsed) return;
885
+ // Use the actual target for peek behavior (not mapped to left)
886
+ shell.peekPane(target);
1896
887
  },
1897
- 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
- );
888
+ [onMouseEnter, peekOnHover, isCollapsed, shell, target],
889
+ );
1919
890
 
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;
891
+ const handleMouseLeave = React.useCallback(
892
+ (event: React.MouseEvent<HTMLButtonElement>) => {
893
+ onMouseLeave?.(event);
894
+ if (!peekOnHover) return;
895
+ if ((shell as any).peekTarget === target) {
896
+ shell.clearPeek();
1926
897
  }
1927
- const bpKeys = Object.keys(BREAKPOINTS) as Array<keyof typeof BREAKPOINTS>;
1928
- const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat(
1929
- 'initial' as Breakpoint,
1930
- );
1931
- const startIdx = order.indexOf(shell.currentBreakpoint as Breakpoint);
1932
- for (let i = startIdx + 1; i < order.length; i++) {
1933
- const bp = order[i];
1934
- if (dm && dm[bp]) {
1935
- return dm[bp] as PaneMode;
1936
- }
1937
- }
1938
- return 'collapsed';
1939
- }, [defaultMode, shell.currentBreakpoint]);
1940
-
1941
- // Honor defaultMode on mount when uncontrolled (including responsive objects)
1942
- const didInitRef = React.useRef(false);
1943
- React.useEffect(() => {
1944
- if (didInitRef.current) return;
1945
- didInitRef.current = true;
1946
- if (mode === undefined) {
1947
- const initial = resolveResponsiveMode();
1948
- if (shell.bottomMode !== initial) shell.setBottomMode(initial);
1949
- }
1950
- // eslint-disable-next-line react-hooks/exhaustive-deps
1951
- }, []);
1952
-
1953
- // Apply responsive defaultMode on breakpoint change when uncontrolled
1954
- const lastBottomBpRef = React.useRef<Breakpoint | null>(null);
1955
- const lastResolvedBottomRef = React.useRef<PaneMode | null>(null);
1956
- React.useEffect(() => {
1957
- if (mode !== undefined) return; // controlled wins
1958
- if (!shell.currentBreakpointReady) return; // avoid SSR mismatch
1959
- if (lastBottomBpRef.current === shell.currentBreakpoint) return; // only on bp change
1960
- lastBottomBpRef.current = shell.currentBreakpoint as Breakpoint;
1961
- const next = resolveResponsiveMode();
1962
- if (lastResolvedBottomRef.current === next) return; // no-op transition
1963
- lastResolvedBottomRef.current = next;
1964
- if (next !== shell.bottomMode) shell.setBottomMode(next);
1965
- }, [
1966
- mode,
1967
- shell.currentBreakpoint,
1968
- shell.currentBreakpointReady,
1969
- resolveResponsiveMode,
1970
- shell.bottomMode,
1971
- shell.setBottomMode,
1972
- ]);
1973
-
1974
- // Sync controlled mode
1975
- React.useEffect(() => {
1976
- if (mode !== undefined && shell.bottomMode !== mode) {
1977
- shell.setBottomMode(mode);
1978
- }
1979
- }, [mode, shell]);
1980
-
1981
- // Emit mode changes
1982
- React.useEffect(() => {
1983
- if (mode === undefined) {
1984
- onModeChange?.(shell.bottomMode);
1985
- }
1986
- }, [shell.bottomMode, mode, onModeChange]);
1987
-
1988
- // Emit expand/collapse events
1989
- React.useEffect(() => {
1990
- if (shell.bottomMode === 'expanded') {
1991
- onExpand?.();
1992
- } else {
1993
- onCollapse?.();
1994
- }
1995
- }, [shell.bottomMode, onExpand, onCollapse]);
1996
-
1997
- const isExpanded = shell.bottomMode === 'expanded';
1998
-
1999
- // Default persistence if paneId provided and none supplied (fixed only)
2000
- const persistenceAdapter = React.useMemo(() => {
2001
- if (!paneId || persistence) return persistence;
2002
- const key = `kookie-ui:shell:bottom:${paneId}`;
2003
- const adapter: PaneSizePersistence = {
2004
- load: () => {
2005
- if (typeof window === 'undefined') return undefined;
2006
- const v = window.localStorage.getItem(key);
2007
- return v ? Number(v) : undefined;
2008
- },
2009
- save: (size: number) => {
2010
- if (typeof window === 'undefined') return;
2011
- window.localStorage.setItem(key, String(size));
2012
- },
2013
- };
2014
- return adapter;
2015
- }, [paneId, persistence]);
2016
-
2017
- React.useEffect(() => {
2018
- let mounted = true;
2019
- (async () => {
2020
- if (!resizable || !persistenceAdapter?.load || isOverlay) return;
2021
- const loaded = await persistenceAdapter.load();
2022
- if (mounted && typeof loaded === 'number' && localRef.current) {
2023
- localRef.current.style.setProperty('--bottom-size', `${loaded}px`);
2024
- onResize?.(loaded);
2025
- }
2026
- })();
2027
- return () => {
2028
- mounted = false;
2029
- };
2030
- }, [resizable, persistenceAdapter, onResize, isOverlay]);
2031
-
2032
- const handleEl =
2033
- resizable && !isOverlay && isExpanded ? (
2034
- <PaneResizeContext.Provider
2035
- value={{
2036
- containerRef: localRef,
2037
- cssVarName: '--bottom-size',
2038
- minSize,
2039
- maxSize,
2040
- defaultSize: expandedSize,
2041
- orientation: 'horizontal',
2042
- edge: 'start',
2043
- computeNext: (client, startClient, startSize) => {
2044
- const delta = client - startClient;
2045
- return startSize - delta; // drag up reduces size
2046
- },
2047
- onResize,
2048
- onResizeStart,
2049
- onResizeEnd: (size) => {
2050
- onResizeEnd?.(size);
2051
- persistenceAdapter?.save?.(size);
2052
- },
2053
- target: 'bottom',
2054
- collapsible,
2055
- snapPoints,
2056
- snapTolerance: snapTolerance ?? 8,
2057
- collapseThreshold,
2058
- requestCollapse: () => shell.setBottomMode('collapsed'),
2059
- requestToggle: () => shell.togglePane('bottom'),
2060
- }}
2061
- >
2062
- {handleChildren.length > 0 ? (
2063
- handleChildren.map((el, i) => React.cloneElement(el, { key: el.key ?? i }))
2064
- ) : (
2065
- <PaneHandle />
2066
- )}
2067
- </PaneResizeContext.Provider>
2068
- ) : null;
2069
-
2070
- if (isOverlay) {
2071
- const open = shell.bottomMode === 'expanded';
2072
- return (
2073
- <Sheet.Root
2074
- open={open}
2075
- onOpenChange={(o) => shell.setBottomMode(o ? 'expanded' : 'collapsed')}
2076
- >
2077
- <Sheet.Content
2078
- side="bottom"
2079
- style={{ padding: 0 }}
2080
- height={{ initial: `${expandedSize}px` }}
2081
- >
2082
- <VisuallyHidden>
2083
- <Sheet.Title>Bottom panel</Sheet.Title>
2084
- </VisuallyHidden>
2085
- {contentChildren}
2086
- </Sheet.Content>
2087
- </Sheet.Root>
2088
- );
2089
- }
2090
-
2091
- return (
2092
- <div
2093
- {...props}
2094
- ref={setRef}
2095
- className={classNames('rt-ShellBottom', className)}
2096
- data-mode={shell.bottomMode}
2097
- data-peek={shell.peekTarget === 'bottom' || undefined}
2098
- data-presentation={resolvedPresentation}
2099
- data-open={(isStacked && isExpanded) || undefined}
2100
- style={{
2101
- ...style,
2102
- ['--bottom-size' as any]: `${expandedSize}px`,
2103
- ['--bottom-min-size' as any]: `${minSize}px`,
2104
- ['--bottom-max-size' as any]: `${maxSize}px`,
2105
- }}
2106
- >
2107
- <div className="rt-ShellBottomContent" data-visible={isExpanded || undefined}>
2108
- {contentChildren}
2109
- </div>
2110
- {handleEl}
2111
- </div>
2112
- );
2113
- },
2114
- ) as BottomComponent;
2115
- Bottom.displayName = 'Shell.Bottom';
2116
- Bottom.Handle = BottomHandle;
2117
-
2118
- // Trigger
2119
- type PaneTarget = 'left' | 'rail' | 'panel' | 'sidebar' | 'inspector' | 'bottom';
2120
- type TriggerAction = 'toggle' | 'expand' | 'collapse';
2121
-
2122
- interface TriggerProps extends React.ComponentPropsWithoutRef<'button'> {
2123
- target: PaneTarget;
2124
- action?: TriggerAction;
2125
- /**
2126
- * Whether to show peek preview on hover when the target pane is collapsed.
2127
- * Defaults to false.
2128
- */
2129
- peekOnHover?: boolean;
2130
- }
2131
-
2132
- const Trigger = React.forwardRef<HTMLButtonElement, TriggerProps>(
2133
- (
2134
- {
2135
- target,
2136
- action = 'toggle',
2137
- peekOnHover,
2138
- onClick,
2139
- onMouseEnter,
2140
- onMouseLeave,
2141
- children,
2142
- ...props
2143
898
  },
2144
- 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
- }
2156
-
2157
- switch (action) {
2158
- case 'toggle':
2159
- shell.togglePane(target);
2160
- break;
2161
- case 'expand':
2162
- shell.expandPane(target);
2163
- break;
2164
- case 'collapse':
2165
- shell.collapsePane(target);
2166
- break;
2167
- }
2168
- },
2169
- [shell, target, action, onClick],
2170
- );
2171
-
2172
- const isCollapsed = (() => {
2173
- switch (target) {
2174
- case 'left':
2175
- case 'rail':
2176
- return shell.leftMode === 'collapsed';
2177
- case 'panel':
2178
- return shell.leftMode === 'collapsed' || shell.panelMode === 'collapsed';
2179
- case 'sidebar':
2180
- return shell.sidebarMode === 'collapsed';
2181
- case 'inspector':
2182
- return shell.inspectorMode === 'collapsed';
2183
- case 'bottom':
2184
- return shell.bottomMode === 'collapsed';
2185
- }
2186
- })();
2187
-
2188
- const handleMouseEnter = React.useCallback(
2189
- (event: React.MouseEvent<HTMLButtonElement>) => {
2190
- onMouseEnter?.(event);
2191
- if (!peekOnHover || !isCollapsed) return;
2192
- // Use the actual target for peek behavior (not mapped to left)
2193
- shell.peekPane(target);
2194
- },
2195
- [onMouseEnter, peekOnHover, isCollapsed, shell, target],
2196
- );
2197
-
2198
- const handleMouseLeave = React.useCallback(
2199
- (event: React.MouseEvent<HTMLButtonElement>) => {
2200
- onMouseLeave?.(event);
2201
- if (!peekOnHover) return;
2202
- if ((shell as any).peekTarget === target) {
2203
- shell.clearPeek();
2204
- }
2205
- },
2206
- [onMouseLeave, peekOnHover, shell, target],
2207
- );
2208
-
2209
- return (
2210
- <button
2211
- {...props}
2212
- ref={ref}
2213
- onClick={handleClick}
2214
- onMouseEnter={handleMouseEnter}
2215
- onMouseLeave={handleMouseLeave}
2216
- data-shell-trigger={target}
2217
- data-shell-action={action}
2218
- >
2219
- {children}
2220
- </button>
2221
- );
2222
- },
2223
- );
899
+ [onMouseLeave, peekOnHover, shell, target],
900
+ );
901
+
902
+ return (
903
+ <button {...props} ref={ref} onClick={handleClick} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} data-shell-trigger={target} data-shell-action={action}>
904
+ {children}
905
+ </button>
906
+ );
907
+ });
2224
908
  Trigger.displayName = 'Shell.Trigger';
2225
909
 
2226
910
  // Exports