@womp/kookie-ui 0.3.23 → 0.3.24

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.
@@ -222,6 +222,113 @@ interface ShellRootProps extends React.ComponentPropsWithoutRef<'div'> {
222
222
  height?: 'full' | 'auto' | string | number;
223
223
  }
224
224
 
225
+ /**
226
+ * Read the pane modes a set of Shell children asks for.
227
+ *
228
+ * Runs once at mount, for the reducer's initial state, and again whenever a
229
+ * pane family enters after mount. Pure - it only reads the children's props.
230
+ */
231
+ function readPaneStateFromChildren(initialChildren: React.ReactNode): PaneState & { hasPanelDefaultOpen: boolean } {
232
+ const childArray = React.Children.toArray(initialChildren) as React.ReactElement[];
233
+
234
+ // Compute initial defaults from immediate children (one-time, uncontrolled defaults)
235
+ const hasPanelDefaultOpen = childArray.some(
236
+ (el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Panel' && Boolean((el as any).props?.defaultOpen),
237
+ );
238
+
239
+ // Rail defaults to open (true) unless explicitly set to false
240
+ // Supports responsive objects: { initial: false, md: true }
241
+ const railEl = childArray.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Rail');
242
+ const railDefaultOpen = railEl ? (railEl as any).props?.defaultOpen : undefined;
243
+ const hasRailDefaultOpen = (() => {
244
+ if (!railEl) return false;
245
+ if (railDefaultOpen === undefined) return true; // Default to open
246
+ if (typeof railDefaultOpen === 'boolean') return railDefaultOpen;
247
+ // Responsive object - use 'initial' value, or first defined value, or default true
248
+ if (typeof railDefaultOpen === 'object') {
249
+ return railDefaultOpen.initial ?? Object.values(railDefaultOpen)[0] ?? true;
250
+ }
251
+ return true;
252
+ })();
253
+
254
+ const hasInspectorDefaultOpen = childArray.some(
255
+ (el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Inspector' && Boolean((el as any).props?.defaultOpen),
256
+ );
257
+ const hasInspectorOpenControlled = childArray.some(
258
+ (el) =>
259
+ React.isValidElement(el) &&
260
+ (el as any).type?.displayName === 'Shell.Inspector' &&
261
+ typeof (el as any).props?.open !== 'undefined' &&
262
+ Boolean((el as any).props?.open),
263
+ );
264
+
265
+ // Detect Panel controlled open state for initial reducer state
266
+ const hasPanelOpenControlled = childArray.some((el) => {
267
+ if (!React.isValidElement(el) || (el as any).type?.displayName !== 'Shell.Panel') return false;
268
+ const openProp = (el as any).props?.open;
269
+ if (typeof openProp === 'undefined') return false;
270
+ if (typeof openProp === 'boolean') return openProp;
271
+ // Responsive object - check 'initial' or first truthy value
272
+ if (typeof openProp === 'object' && openProp !== null) {
273
+ return openProp.initial ?? Object.values(openProp)[0] ?? false;
274
+ }
275
+ return false;
276
+ });
277
+
278
+ // Detect Sidebar initial state from props
279
+ const getSidebarInitialState = (): SidebarMode => {
280
+ const sidebarEl = childArray.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Sidebar');
281
+ if (!sidebarEl) return 'expanded';
282
+ const sidebarProps = (sidebarEl as any).props;
283
+ // Check controlled state first
284
+ if (typeof sidebarProps?.state !== 'undefined') {
285
+ if (typeof sidebarProps.state === 'string') return sidebarProps.state as SidebarMode;
286
+ // Responsive object - use 'initial' breakpoint or first defined value
287
+ if (typeof sidebarProps.state === 'object') {
288
+ return (sidebarProps.state.initial ?? Object.values(sidebarProps.state)[0] ?? 'expanded') as SidebarMode;
289
+ }
290
+ }
291
+ // Check defaultState
292
+ if (typeof sidebarProps?.defaultState !== 'undefined') {
293
+ if (typeof sidebarProps.defaultState === 'string') return sidebarProps.defaultState as SidebarMode;
294
+ if (typeof sidebarProps.defaultState === 'object') {
295
+ return (sidebarProps.defaultState.initial ?? Object.values(sidebarProps.defaultState)[0] ?? 'expanded') as SidebarMode;
296
+ }
297
+ }
298
+ return 'expanded';
299
+ };
300
+
301
+ // Detect Bottom initial state from props
302
+ const getBottomInitialState = (): PaneMode => {
303
+ const bottomEl = childArray.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Bottom');
304
+ if (!bottomEl) return 'collapsed';
305
+ const bottomProps = (bottomEl as any).props;
306
+ // Check controlled open first
307
+ if (typeof bottomProps?.open !== 'undefined') {
308
+ if (typeof bottomProps.open === 'boolean') return bottomProps.open ? 'expanded' : 'collapsed';
309
+ // Responsive object - use 'initial' breakpoint or first defined value
310
+ if (typeof bottomProps.open === 'object') {
311
+ const val = bottomProps.open.initial ?? Object.values(bottomProps.open)[0];
312
+ return val ? 'expanded' : 'collapsed';
313
+ }
314
+ }
315
+ // Check defaultOpen
316
+ if (typeof bottomProps?.defaultOpen !== 'undefined') {
317
+ return bottomProps.defaultOpen ? 'expanded' : 'collapsed';
318
+ }
319
+ return 'collapsed';
320
+ };
321
+
322
+ return {
323
+ hasPanelDefaultOpen,
324
+ leftMode: hasPanelDefaultOpen || hasPanelOpenControlled || hasRailDefaultOpen ? 'expanded' : ('collapsed' as PaneMode),
325
+ panelMode: hasPanelDefaultOpen || hasPanelOpenControlled ? 'expanded' : ('collapsed' as PaneMode),
326
+ sidebarMode: getSidebarInitialState(),
327
+ inspectorMode: hasInspectorDefaultOpen || hasInspectorOpenControlled ? 'expanded' : ('collapsed' as PaneMode),
328
+ bottomMode: getBottomInitialState(),
329
+ };
330
+ }
331
+
225
332
  const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, children, height = 'full', ...props }, ref) => {
226
333
  const { breakpoint: currentBreakpoint, ready: currentBreakpointReady } = useBreakpoint();
227
334
 
@@ -231,105 +338,10 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
231
338
  // Pane state management via reducer with lazy initialization
232
339
  // This computation only runs once on mount, not on every render
233
340
  const [paneState, dispatchPane] = React.useReducer(paneReducer, children, (initialChildren) => {
234
- const childArray = React.Children.toArray(initialChildren) as React.ReactElement[];
235
-
236
- // Compute initial defaults from immediate children (one-time, uncontrolled defaults)
237
- const hasPanelDefaultOpen = childArray.some(
238
- (el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Panel' && Boolean((el as any).props?.defaultOpen),
239
- );
341
+ const { hasPanelDefaultOpen, ...modes } = readPaneStateFromChildren(initialChildren);
240
342
  // Store for use in passthrough props
241
343
  hasPanelDefaultOpenRef.current = hasPanelDefaultOpen;
242
-
243
- // Rail defaults to open (true) unless explicitly set to false
244
- // Supports responsive objects: { initial: false, md: true }
245
- const railEl = childArray.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Rail');
246
- const railDefaultOpen = railEl ? (railEl as any).props?.defaultOpen : undefined;
247
- const hasRailDefaultOpen = (() => {
248
- if (!railEl) return false;
249
- if (railDefaultOpen === undefined) return true; // Default to open
250
- if (typeof railDefaultOpen === 'boolean') return railDefaultOpen;
251
- // Responsive object - use 'initial' value, or first defined value, or default true
252
- if (typeof railDefaultOpen === 'object') {
253
- return railDefaultOpen.initial ?? Object.values(railDefaultOpen)[0] ?? true;
254
- }
255
- return true;
256
- })();
257
-
258
- const hasInspectorDefaultOpen = childArray.some(
259
- (el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Inspector' && Boolean((el as any).props?.defaultOpen),
260
- );
261
- const hasInspectorOpenControlled = childArray.some(
262
- (el) =>
263
- React.isValidElement(el) &&
264
- (el as any).type?.displayName === 'Shell.Inspector' &&
265
- typeof (el as any).props?.open !== 'undefined' &&
266
- Boolean((el as any).props?.open),
267
- );
268
-
269
- // Detect Panel controlled open state for initial reducer state
270
- const hasPanelOpenControlled = childArray.some((el) => {
271
- if (!React.isValidElement(el) || (el as any).type?.displayName !== 'Shell.Panel') return false;
272
- const openProp = (el as any).props?.open;
273
- if (typeof openProp === 'undefined') return false;
274
- if (typeof openProp === 'boolean') return openProp;
275
- // Responsive object - check 'initial' or first truthy value
276
- if (typeof openProp === 'object' && openProp !== null) {
277
- return openProp.initial ?? Object.values(openProp)[0] ?? false;
278
- }
279
- return false;
280
- });
281
-
282
- // Detect Sidebar initial state from props
283
- const getSidebarInitialState = (): SidebarMode => {
284
- const sidebarEl = childArray.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Sidebar');
285
- if (!sidebarEl) return 'expanded';
286
- const sidebarProps = (sidebarEl as any).props;
287
- // Check controlled state first
288
- if (typeof sidebarProps?.state !== 'undefined') {
289
- if (typeof sidebarProps.state === 'string') return sidebarProps.state as SidebarMode;
290
- // Responsive object - use 'initial' breakpoint or first defined value
291
- if (typeof sidebarProps.state === 'object') {
292
- return (sidebarProps.state.initial ?? Object.values(sidebarProps.state)[0] ?? 'expanded') as SidebarMode;
293
- }
294
- }
295
- // Check defaultState
296
- if (typeof sidebarProps?.defaultState !== 'undefined') {
297
- if (typeof sidebarProps.defaultState === 'string') return sidebarProps.defaultState as SidebarMode;
298
- if (typeof sidebarProps.defaultState === 'object') {
299
- return (sidebarProps.defaultState.initial ?? Object.values(sidebarProps.defaultState)[0] ?? 'expanded') as SidebarMode;
300
- }
301
- }
302
- return 'expanded';
303
- };
304
-
305
- // Detect Bottom initial state from props
306
- const getBottomInitialState = (): PaneMode => {
307
- const bottomEl = childArray.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Bottom');
308
- if (!bottomEl) return 'collapsed';
309
- const bottomProps = (bottomEl as any).props;
310
- // Check controlled open first
311
- if (typeof bottomProps?.open !== 'undefined') {
312
- if (typeof bottomProps.open === 'boolean') return bottomProps.open ? 'expanded' : 'collapsed';
313
- // Responsive object - use 'initial' breakpoint or first defined value
314
- if (typeof bottomProps.open === 'object') {
315
- const val = bottomProps.open.initial ?? Object.values(bottomProps.open)[0];
316
- return val ? 'expanded' : 'collapsed';
317
- }
318
- }
319
- // Check defaultOpen
320
- if (typeof bottomProps?.defaultOpen !== 'undefined') {
321
- return bottomProps.defaultOpen ? 'expanded' : 'collapsed';
322
- }
323
- return 'collapsed';
324
- };
325
-
326
- return {
327
- leftMode: hasPanelDefaultOpen || hasPanelOpenControlled || hasRailDefaultOpen ? 'expanded' : ('collapsed' as PaneMode),
328
- panelMode: hasPanelDefaultOpen || hasPanelOpenControlled ? 'expanded' : ('collapsed' as PaneMode),
329
- sidebarMode: getSidebarInitialState(),
330
- inspectorMode: hasInspectorDefaultOpen || hasInspectorOpenControlled ? 'expanded' : ('collapsed' as PaneMode),
331
- bottomMode: getBottomInitialState(),
332
- };
344
+ return modes;
333
345
  });
334
346
  const setLeftMode = React.useCallback((mode: PaneMode) => dispatchPane({ type: 'SET_LEFT_MODE', mode }), []);
335
347
  const setPanelMode = React.useCallback((mode: PaneMode) => dispatchPane({ type: 'SET_PANEL_MODE', mode }), []);
@@ -381,6 +393,41 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
381
393
  return childArray.some((el) => isShellComponentType(el, Sidebar));
382
394
  }, [children]);
383
395
 
396
+ // Re-read the defaults when a pane family enters after mount.
397
+ //
398
+ // The initial modes are computed once, from the children present at mount.
399
+ // That is right until an app swaps families at runtime - a Sidebar giving
400
+ // way to Rail+Panel on one route, and back on the next. The entering pane
401
+ // then renders against the mode left behind by the family that just went:
402
+ // a Rail mounting 'collapsed', at width 0, with its content faded out, put
403
+ // right one layout effect later. The reader sees a flicker in place where
404
+ // the pane should have been there already.
405
+ //
406
+ // Adjusting state during render keeps this out of the paint: React re-runs
407
+ // Root before the children render, so the entering pane never sees the
408
+ // mode of the family it replaced. The ref guard is what makes the update
409
+ // terminate - it fires only on the edge, never on a steady render.
410
+ const enteredFamilyRef = React.useRef({ left: hasLeftChildren, sidebar: hasSidebarChildren });
411
+ if (enteredFamilyRef.current.left !== hasLeftChildren || enteredFamilyRef.current.sidebar !== hasSidebarChildren) {
412
+ const leftEntered = hasLeftChildren && !enteredFamilyRef.current.left;
413
+ const sidebarEntered = hasSidebarChildren && !enteredFamilyRef.current.sidebar;
414
+ enteredFamilyRef.current = { left: hasLeftChildren, sidebar: hasSidebarChildren };
415
+
416
+ if (leftEntered || sidebarEntered) {
417
+ const { hasPanelDefaultOpen, ...modes } = readPaneStateFromChildren(children);
418
+ if (leftEntered) {
419
+ hasPanelDefaultOpenRef.current = hasPanelDefaultOpen;
420
+ // Only when the mode actually differs, so a family that enters
421
+ // already in the right mode dispatches nothing.
422
+ if (modes.leftMode !== paneState.leftMode) dispatchPane({ type: 'SET_LEFT_MODE', mode: modes.leftMode });
423
+ if (modes.panelMode !== paneState.panelMode) dispatchPane({ type: 'SET_PANEL_MODE', mode: modes.panelMode });
424
+ }
425
+ if (sidebarEntered && modes.sidebarMode !== paneState.sidebarMode) {
426
+ dispatchPane({ type: 'SET_SIDEBAR_MODE', mode: modes.sidebarMode });
427
+ }
428
+ }
429
+ }
430
+
384
431
  // Keep a ref to sidebar mode so togglePane doesn't depend on it,
385
432
  // preventing ActionsContext churn on every sidebar mode change
386
433
  const sidebarModeRef = React.useRef(paneState.sidebarMode);