@womp/kookie-ui 0.3.23 → 0.3.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components.css +105 -206
- package/dist/cjs/components/index.js +5 -5
- package/dist/cjs/components/index.js.map +3 -3
- package/dist/cjs/components/shell.d.ts.map +1 -1
- package/dist/cjs/components/shell.js +1 -1
- package/dist/cjs/components/shell.js.map +3 -3
- package/dist/cjs/index.js +5 -5
- package/dist/cjs/index.js.map +3 -3
- package/dist/esm/chunk-ARGX2ZZK.js +2 -0
- package/dist/esm/chunk-ARGX2ZZK.js.map +7 -0
- package/dist/esm/components/index.js +1 -1
- package/dist/esm/components/shell.d.ts.map +1 -1
- package/dist/esm/components/shell.js +1 -1
- package/dist/esm/index.js +1 -1
- package/package.json +1 -1
- package/schemas/base-button.json +1 -1
- package/schemas/button.json +1 -1
- package/schemas/icon-button.json +1 -1
- package/schemas/index.json +6 -6
- package/schemas/toggle-button.json +1 -1
- package/schemas/toggle-icon-button.json +1 -1
- package/src/components/shell.tsx +168 -97
- package/styles.css +105 -206
- package/dist/esm/chunk-RMBS6CAO.js +0 -2
- package/dist/esm/chunk-RMBS6CAO.js.map +0 -7
package/src/components/shell.tsx
CHANGED
|
@@ -36,6 +36,7 @@ import { omitPaneProps, extractPaneDomProps, mapResponsiveBooleanToPaneMode } fr
|
|
|
36
36
|
import { Sidebar } from './_internal/shell-sidebar.js';
|
|
37
37
|
import { Bottom } from './_internal/shell-bottom.js';
|
|
38
38
|
import { Inspector } from './_internal/shell-inspector.js';
|
|
39
|
+
import { _BREAKPOINTS } from './shell.types.js';
|
|
39
40
|
import type { PresentationValue, ResponsivePresentation, PaneMode, SidebarMode, PaneSizePersistence, Breakpoint, PaneTarget, Responsive, PaneBaseProps, CSSPropertiesWithVars } from './shell.types.js';
|
|
40
41
|
import { normalizeToPx } from '../helpers/normalize-to-px.js';
|
|
41
42
|
import { useBreakpoint } from '../hooks/use-breakpoint.js';
|
|
@@ -222,114 +223,143 @@ interface ShellRootProps extends React.ComponentPropsWithoutRef<'div'> {
|
|
|
222
223
|
height?: 'full' | 'auto' | string | number;
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
/**
|
|
227
|
+
* Read the pane modes a set of Shell children asks for.
|
|
228
|
+
*
|
|
229
|
+
* Runs once at mount, for the reducer's initial state, and again whenever a
|
|
230
|
+
* pane family enters after mount. Pure - it only reads the children's props.
|
|
231
|
+
*/
|
|
232
|
+
/**
|
|
233
|
+
* Pick the entry of a responsive map that applies at `breakpoint`, falling
|
|
234
|
+
* back down the scale the way useResponsiveValue does. With no breakpoint it
|
|
235
|
+
* takes `initial`, which is what the mount path has always done.
|
|
236
|
+
*/
|
|
237
|
+
function pickResponsive<T>(map: Partial<Record<Breakpoint, T>>, breakpoint?: Breakpoint): T | undefined {
|
|
238
|
+
if (breakpoint === undefined) return (map.initial ?? Object.values(map)[0]) as T | undefined;
|
|
239
|
+
if (map[breakpoint] !== undefined) return map[breakpoint];
|
|
240
|
+
|
|
241
|
+
const bpKeys = Object.keys(_BREAKPOINTS) as Array<keyof typeof _BREAKPOINTS>;
|
|
242
|
+
const order: Breakpoint[] = ([...bpKeys].reverse() as Breakpoint[]).concat('initial' as Breakpoint);
|
|
243
|
+
for (let i = order.indexOf(breakpoint) + 1; i < order.length; i++) {
|
|
244
|
+
if (map[order[i]] !== undefined) return map[order[i]];
|
|
245
|
+
}
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
230
248
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
const [paneState, dispatchPane] = React.useReducer(paneReducer, children, (initialChildren) => {
|
|
234
|
-
const childArray = React.Children.toArray(initialChildren) as React.ReactElement[];
|
|
249
|
+
function readPaneStateFromChildren(initialChildren: React.ReactNode, breakpoint?: Breakpoint): PaneState & { hasPanelDefaultOpen: boolean } {
|
|
250
|
+
const childArray = React.Children.toArray(initialChildren) as React.ReactElement[];
|
|
235
251
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// Store for use in passthrough props
|
|
241
|
-
hasPanelDefaultOpenRef.current = hasPanelDefaultOpen;
|
|
252
|
+
// Compute initial defaults from immediate children (one-time, uncontrolled defaults)
|
|
253
|
+
const hasPanelDefaultOpen = childArray.some(
|
|
254
|
+
(el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Panel' && Boolean((el as any).props?.defaultOpen),
|
|
255
|
+
);
|
|
242
256
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
// Rail defaults to open (true) unless explicitly set to false
|
|
258
|
+
// Supports responsive objects: { initial: false, md: true }
|
|
259
|
+
const railEl = childArray.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Rail');
|
|
260
|
+
const railDefaultOpen = railEl ? (railEl as any).props?.defaultOpen : undefined;
|
|
261
|
+
const hasRailDefaultOpen = (() => {
|
|
262
|
+
if (!railEl) return false;
|
|
263
|
+
if (railDefaultOpen === undefined) return true; // Default to open
|
|
264
|
+
if (typeof railDefaultOpen === 'boolean') return railDefaultOpen;
|
|
265
|
+
// Responsive object - use 'initial' value, or first defined value, or default true
|
|
266
|
+
if (typeof railDefaultOpen === 'object') {
|
|
267
|
+
return railDefaultOpen.initial ?? Object.values(railDefaultOpen)[0] ?? true;
|
|
268
|
+
}
|
|
269
|
+
return true;
|
|
270
|
+
})();
|
|
257
271
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
272
|
+
const hasInspectorDefaultOpen = childArray.some(
|
|
273
|
+
(el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Inspector' && Boolean((el as any).props?.defaultOpen),
|
|
274
|
+
);
|
|
275
|
+
const hasInspectorOpenControlled = childArray.some(
|
|
276
|
+
(el) =>
|
|
277
|
+
React.isValidElement(el) &&
|
|
278
|
+
(el as any).type?.displayName === 'Shell.Inspector' &&
|
|
279
|
+
typeof (el as any).props?.open !== 'undefined' &&
|
|
280
|
+
Boolean((el as any).props?.open),
|
|
281
|
+
);
|
|
268
282
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
283
|
+
// Detect Panel controlled open state for initial reducer state
|
|
284
|
+
const hasPanelOpenControlled = childArray.some((el) => {
|
|
285
|
+
if (!React.isValidElement(el) || (el as any).type?.displayName !== 'Shell.Panel') return false;
|
|
286
|
+
const openProp = (el as any).props?.open;
|
|
287
|
+
if (typeof openProp === 'undefined') return false;
|
|
288
|
+
if (typeof openProp === 'boolean') return openProp;
|
|
289
|
+
// Responsive object - check 'initial' or first truthy value
|
|
290
|
+
if (typeof openProp === 'object' && openProp !== null) {
|
|
291
|
+
return openProp.initial ?? Object.values(openProp)[0] ?? false;
|
|
292
|
+
}
|
|
293
|
+
return false;
|
|
294
|
+
});
|
|
281
295
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
296
|
+
// Detect Sidebar initial state from props
|
|
297
|
+
const getSidebarInitialState = (): SidebarMode => {
|
|
298
|
+
const sidebarEl = childArray.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Sidebar');
|
|
299
|
+
if (!sidebarEl) return 'expanded';
|
|
300
|
+
const sidebarProps = (sidebarEl as any).props;
|
|
301
|
+
// Check controlled state first
|
|
302
|
+
if (typeof sidebarProps?.state !== 'undefined') {
|
|
303
|
+
if (typeof sidebarProps.state === 'string') return sidebarProps.state as SidebarMode;
|
|
304
|
+
// Responsive object - use 'initial' breakpoint or first defined value
|
|
305
|
+
if (typeof sidebarProps.state === 'object') {
|
|
306
|
+
return (pickResponsive<SidebarMode>(sidebarProps.state, breakpoint) ?? 'expanded') as SidebarMode;
|
|
294
307
|
}
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
308
|
+
}
|
|
309
|
+
// Check defaultState
|
|
310
|
+
if (typeof sidebarProps?.defaultState !== 'undefined') {
|
|
311
|
+
if (typeof sidebarProps.defaultState === 'string') return sidebarProps.defaultState as SidebarMode;
|
|
312
|
+
if (typeof sidebarProps.defaultState === 'object') {
|
|
313
|
+
return (pickResponsive<SidebarMode>(sidebarProps.defaultState, breakpoint) ?? 'expanded') as SidebarMode;
|
|
301
314
|
}
|
|
302
|
-
|
|
303
|
-
|
|
315
|
+
}
|
|
316
|
+
return 'expanded';
|
|
317
|
+
};
|
|
304
318
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
}
|
|
319
|
+
// Detect Bottom initial state from props
|
|
320
|
+
const getBottomInitialState = (): PaneMode => {
|
|
321
|
+
const bottomEl = childArray.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Bottom');
|
|
322
|
+
if (!bottomEl) return 'collapsed';
|
|
323
|
+
const bottomProps = (bottomEl as any).props;
|
|
324
|
+
// Check controlled open first
|
|
325
|
+
if (typeof bottomProps?.open !== 'undefined') {
|
|
326
|
+
if (typeof bottomProps.open === 'boolean') return bottomProps.open ? 'expanded' : 'collapsed';
|
|
327
|
+
// Responsive object - use 'initial' breakpoint or first defined value
|
|
328
|
+
if (typeof bottomProps.open === 'object') {
|
|
329
|
+
const val = bottomProps.open.initial ?? Object.values(bottomProps.open)[0];
|
|
330
|
+
return val ? 'expanded' : 'collapsed';
|
|
318
331
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
332
|
+
}
|
|
333
|
+
// Check defaultOpen
|
|
334
|
+
if (typeof bottomProps?.defaultOpen !== 'undefined') {
|
|
335
|
+
return bottomProps.defaultOpen ? 'expanded' : 'collapsed';
|
|
336
|
+
}
|
|
337
|
+
return 'collapsed';
|
|
338
|
+
};
|
|
325
339
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
340
|
+
return {
|
|
341
|
+
hasPanelDefaultOpen,
|
|
342
|
+
leftMode: hasPanelDefaultOpen || hasPanelOpenControlled || hasRailDefaultOpen ? 'expanded' : ('collapsed' as PaneMode),
|
|
343
|
+
panelMode: hasPanelDefaultOpen || hasPanelOpenControlled ? 'expanded' : ('collapsed' as PaneMode),
|
|
344
|
+
sidebarMode: getSidebarInitialState(),
|
|
345
|
+
inspectorMode: hasInspectorDefaultOpen || hasInspectorOpenControlled ? 'expanded' : ('collapsed' as PaneMode),
|
|
346
|
+
bottomMode: getBottomInitialState(),
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, children, height = 'full', ...props }, ref) => {
|
|
351
|
+
const { breakpoint: currentBreakpoint, ready: currentBreakpointReady } = useBreakpoint();
|
|
352
|
+
|
|
353
|
+
// Store hasPanelDefaultOpen for use in passthrough props (computed once during lazy init)
|
|
354
|
+
const hasPanelDefaultOpenRef = React.useRef<boolean>(false);
|
|
355
|
+
|
|
356
|
+
// Pane state management via reducer with lazy initialization
|
|
357
|
+
// This computation only runs once on mount, not on every render
|
|
358
|
+
const [paneState, dispatchPane] = React.useReducer(paneReducer, children, (initialChildren) => {
|
|
359
|
+
const { hasPanelDefaultOpen, ...modes } = readPaneStateFromChildren(initialChildren);
|
|
360
|
+
// Store for use in passthrough props
|
|
361
|
+
hasPanelDefaultOpenRef.current = hasPanelDefaultOpen;
|
|
362
|
+
return modes;
|
|
333
363
|
});
|
|
334
364
|
const setLeftMode = React.useCallback((mode: PaneMode) => dispatchPane({ type: 'SET_LEFT_MODE', mode }), []);
|
|
335
365
|
const setPanelMode = React.useCallback((mode: PaneMode) => dispatchPane({ type: 'SET_PANEL_MODE', mode }), []);
|
|
@@ -381,6 +411,47 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
|
|
|
381
411
|
return childArray.some((el) => isShellComponentType(el, Sidebar));
|
|
382
412
|
}, [children]);
|
|
383
413
|
|
|
414
|
+
// Re-read the defaults when a pane family enters after mount.
|
|
415
|
+
//
|
|
416
|
+
// The initial modes are computed once, from the children present at mount.
|
|
417
|
+
// That is right until an app swaps families at runtime - a Sidebar giving
|
|
418
|
+
// way to Rail+Panel on one route, and back on the next. The entering pane
|
|
419
|
+
// then renders against the mode left behind by the family that just went:
|
|
420
|
+
// a Rail mounting 'collapsed', at width 0, with its content faded out, put
|
|
421
|
+
// right one layout effect later. The reader sees a flicker in place where
|
|
422
|
+
// the pane should have been there already.
|
|
423
|
+
//
|
|
424
|
+
// Adjusting state during render keeps this out of the paint: React re-runs
|
|
425
|
+
// Root before the children render, so the entering pane never sees the
|
|
426
|
+
// mode of the family it replaced. The ref guard is what makes the update
|
|
427
|
+
// terminate - it fires only on the edge, never on a steady render.
|
|
428
|
+
const enteredFamilyRef = React.useRef({ left: hasLeftChildren, sidebar: hasSidebarChildren });
|
|
429
|
+
if (enteredFamilyRef.current.left !== hasLeftChildren || enteredFamilyRef.current.sidebar !== hasSidebarChildren) {
|
|
430
|
+
const leftEntered = hasLeftChildren && !enteredFamilyRef.current.left;
|
|
431
|
+
const sidebarEntered = hasSidebarChildren && !enteredFamilyRef.current.sidebar;
|
|
432
|
+
enteredFamilyRef.current = { left: hasLeftChildren, sidebar: hasSidebarChildren };
|
|
433
|
+
|
|
434
|
+
if (leftEntered || sidebarEntered) {
|
|
435
|
+
// The live breakpoint, not the mount default: a responsive
|
|
436
|
+
// defaultState like { initial: 'collapsed', md: 'expanded' } must be
|
|
437
|
+
// read for the width the reader is on. Taking `initial` would send a
|
|
438
|
+
// desktop sidebar in collapsed and let its own start-up effect open it
|
|
439
|
+
// again after the paint, which is the blink this whole block exists to
|
|
440
|
+
// remove.
|
|
441
|
+
const { hasPanelDefaultOpen, ...modes } = readPaneStateFromChildren(children, currentBreakpoint as Breakpoint);
|
|
442
|
+
if (leftEntered) {
|
|
443
|
+
hasPanelDefaultOpenRef.current = hasPanelDefaultOpen;
|
|
444
|
+
// Only when the mode actually differs, so a family that enters
|
|
445
|
+
// already in the right mode dispatches nothing.
|
|
446
|
+
if (modes.leftMode !== paneState.leftMode) dispatchPane({ type: 'SET_LEFT_MODE', mode: modes.leftMode });
|
|
447
|
+
if (modes.panelMode !== paneState.panelMode) dispatchPane({ type: 'SET_PANEL_MODE', mode: modes.panelMode });
|
|
448
|
+
}
|
|
449
|
+
if (sidebarEntered && modes.sidebarMode !== paneState.sidebarMode) {
|
|
450
|
+
dispatchPane({ type: 'SET_SIDEBAR_MODE', mode: modes.sidebarMode });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
384
455
|
// Keep a ref to sidebar mode so togglePane doesn't depend on it,
|
|
385
456
|
// preventing ActionsContext churn on every sidebar mode change
|
|
386
457
|
const sidebarModeRef = React.useRef(paneState.sidebarMode);
|