@kushagradhawan/kookie-ui 0.1.70 → 0.1.72

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 (164) hide show
  1. package/README.md +4 -0
  2. package/components.css +63 -380
  3. package/dist/cjs/components/_internal/base-button.d.ts.map +1 -1
  4. package/dist/cjs/components/_internal/base-button.js +1 -1
  5. package/dist/cjs/components/_internal/base-button.js.map +3 -3
  6. package/dist/cjs/components/_internal/shell-bottom.d.ts +2 -21
  7. package/dist/cjs/components/_internal/shell-bottom.d.ts.map +1 -1
  8. package/dist/cjs/components/_internal/shell-bottom.js +1 -1
  9. package/dist/cjs/components/_internal/shell-bottom.js.map +3 -3
  10. package/dist/cjs/components/_internal/shell-inspector.d.ts +10 -21
  11. package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -1
  12. package/dist/cjs/components/_internal/shell-inspector.js +1 -1
  13. package/dist/cjs/components/_internal/shell-inspector.js.map +3 -3
  14. package/dist/cjs/components/_internal/shell-prop-helpers.d.ts +7 -0
  15. package/dist/cjs/components/_internal/shell-prop-helpers.d.ts.map +1 -0
  16. package/dist/cjs/components/_internal/shell-prop-helpers.js +2 -0
  17. package/dist/cjs/components/_internal/shell-prop-helpers.js.map +7 -0
  18. package/dist/cjs/components/_internal/shell-sidebar.d.ts +4 -21
  19. package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -1
  20. package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
  21. package/dist/cjs/components/_internal/shell-sidebar.js.map +3 -3
  22. package/dist/cjs/components/button.d.ts.map +1 -1
  23. package/dist/cjs/components/button.js +1 -1
  24. package/dist/cjs/components/button.js.map +3 -3
  25. package/dist/cjs/components/chatbar.d.ts +11 -2
  26. package/dist/cjs/components/chatbar.d.ts.map +1 -1
  27. package/dist/cjs/components/chatbar.js +1 -1
  28. package/dist/cjs/components/chatbar.js.map +3 -3
  29. package/dist/cjs/components/icon-button.d.ts.map +1 -1
  30. package/dist/cjs/components/icon-button.js +2 -2
  31. package/dist/cjs/components/icon-button.js.map +3 -3
  32. package/dist/cjs/components/schemas/shell.schema.d.ts +70 -70
  33. package/dist/cjs/components/shell.context.d.ts +1 -0
  34. package/dist/cjs/components/shell.context.d.ts.map +1 -1
  35. package/dist/cjs/components/shell.context.js.map +2 -2
  36. package/dist/cjs/components/shell.d.ts +6 -26
  37. package/dist/cjs/components/shell.d.ts.map +1 -1
  38. package/dist/cjs/components/shell.hooks.d.ts +19 -2
  39. package/dist/cjs/components/shell.hooks.d.ts.map +1 -1
  40. package/dist/cjs/components/shell.hooks.js +1 -1
  41. package/dist/cjs/components/shell.hooks.js.map +3 -3
  42. package/dist/cjs/components/shell.js +1 -1
  43. package/dist/cjs/components/shell.js.map +3 -3
  44. package/dist/cjs/components/shell.types.d.ts +21 -0
  45. package/dist/cjs/components/shell.types.d.ts.map +1 -1
  46. package/dist/cjs/components/shell.types.js +1 -1
  47. package/dist/cjs/components/shell.types.js.map +2 -2
  48. package/dist/cjs/components/toggle-button.d.ts.map +1 -1
  49. package/dist/cjs/components/toggle-button.js +1 -1
  50. package/dist/cjs/components/toggle-button.js.map +3 -3
  51. package/dist/cjs/components/toggle-icon-button.d.ts.map +1 -1
  52. package/dist/cjs/components/toggle-icon-button.js +1 -1
  53. package/dist/cjs/components/toggle-icon-button.js.map +3 -3
  54. package/dist/cjs/hooks/index.d.ts +2 -0
  55. package/dist/cjs/hooks/index.d.ts.map +1 -1
  56. package/dist/cjs/hooks/index.js +1 -1
  57. package/dist/cjs/hooks/index.js.map +3 -3
  58. package/dist/cjs/hooks/use-live-announcer.d.ts.map +1 -1
  59. package/dist/cjs/hooks/use-live-announcer.js +2 -2
  60. package/dist/cjs/hooks/use-live-announcer.js.map +3 -3
  61. package/dist/cjs/hooks/use-toggle-state.d.ts +37 -0
  62. package/dist/cjs/hooks/use-toggle-state.d.ts.map +1 -0
  63. package/dist/cjs/hooks/use-toggle-state.js +2 -0
  64. package/dist/cjs/hooks/use-toggle-state.js.map +7 -0
  65. package/dist/cjs/hooks/use-tooltip-wrapper.d.ts +29 -0
  66. package/dist/cjs/hooks/use-tooltip-wrapper.d.ts.map +1 -0
  67. package/dist/cjs/hooks/use-tooltip-wrapper.js +2 -0
  68. package/dist/cjs/hooks/use-tooltip-wrapper.js.map +7 -0
  69. package/dist/esm/components/_internal/base-button.d.ts.map +1 -1
  70. package/dist/esm/components/_internal/base-button.js +1 -1
  71. package/dist/esm/components/_internal/base-button.js.map +3 -3
  72. package/dist/esm/components/_internal/shell-bottom.d.ts +2 -21
  73. package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
  74. package/dist/esm/components/_internal/shell-bottom.js +1 -1
  75. package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
  76. package/dist/esm/components/_internal/shell-inspector.d.ts +10 -21
  77. package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
  78. package/dist/esm/components/_internal/shell-inspector.js +1 -1
  79. package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
  80. package/dist/esm/components/_internal/shell-prop-helpers.d.ts +7 -0
  81. package/dist/esm/components/_internal/shell-prop-helpers.d.ts.map +1 -0
  82. package/dist/esm/components/_internal/shell-prop-helpers.js +2 -0
  83. package/dist/esm/components/_internal/shell-prop-helpers.js.map +7 -0
  84. package/dist/esm/components/_internal/shell-sidebar.d.ts +4 -21
  85. package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
  86. package/dist/esm/components/_internal/shell-sidebar.js +1 -1
  87. package/dist/esm/components/_internal/shell-sidebar.js.map +3 -3
  88. package/dist/esm/components/button.d.ts.map +1 -1
  89. package/dist/esm/components/button.js +1 -1
  90. package/dist/esm/components/button.js.map +3 -3
  91. package/dist/esm/components/chatbar.d.ts +11 -2
  92. package/dist/esm/components/chatbar.d.ts.map +1 -1
  93. package/dist/esm/components/chatbar.js +1 -1
  94. package/dist/esm/components/chatbar.js.map +3 -3
  95. package/dist/esm/components/icon-button.d.ts.map +1 -1
  96. package/dist/esm/components/icon-button.js +2 -2
  97. package/dist/esm/components/icon-button.js.map +3 -3
  98. package/dist/esm/components/schemas/shell.schema.d.ts +70 -70
  99. package/dist/esm/components/shell.context.d.ts +1 -0
  100. package/dist/esm/components/shell.context.d.ts.map +1 -1
  101. package/dist/esm/components/shell.context.js.map +2 -2
  102. package/dist/esm/components/shell.d.ts +6 -26
  103. package/dist/esm/components/shell.d.ts.map +1 -1
  104. package/dist/esm/components/shell.hooks.d.ts +19 -2
  105. package/dist/esm/components/shell.hooks.d.ts.map +1 -1
  106. package/dist/esm/components/shell.hooks.js +1 -1
  107. package/dist/esm/components/shell.hooks.js.map +3 -3
  108. package/dist/esm/components/shell.js +1 -1
  109. package/dist/esm/components/shell.js.map +3 -3
  110. package/dist/esm/components/shell.types.d.ts +21 -0
  111. package/dist/esm/components/shell.types.d.ts.map +1 -1
  112. package/dist/esm/components/shell.types.js.map +2 -2
  113. package/dist/esm/components/toggle-button.d.ts.map +1 -1
  114. package/dist/esm/components/toggle-button.js +1 -1
  115. package/dist/esm/components/toggle-button.js.map +3 -3
  116. package/dist/esm/components/toggle-icon-button.d.ts.map +1 -1
  117. package/dist/esm/components/toggle-icon-button.js +1 -1
  118. package/dist/esm/components/toggle-icon-button.js.map +3 -3
  119. package/dist/esm/hooks/index.d.ts +2 -0
  120. package/dist/esm/hooks/index.d.ts.map +1 -1
  121. package/dist/esm/hooks/index.js +1 -1
  122. package/dist/esm/hooks/index.js.map +3 -3
  123. package/dist/esm/hooks/use-live-announcer.d.ts.map +1 -1
  124. package/dist/esm/hooks/use-live-announcer.js +2 -2
  125. package/dist/esm/hooks/use-live-announcer.js.map +3 -3
  126. package/dist/esm/hooks/use-toggle-state.d.ts +37 -0
  127. package/dist/esm/hooks/use-toggle-state.d.ts.map +1 -0
  128. package/dist/esm/hooks/use-toggle-state.js +2 -0
  129. package/dist/esm/hooks/use-toggle-state.js.map +7 -0
  130. package/dist/esm/hooks/use-tooltip-wrapper.d.ts +29 -0
  131. package/dist/esm/hooks/use-tooltip-wrapper.d.ts.map +1 -0
  132. package/dist/esm/hooks/use-tooltip-wrapper.js +2 -0
  133. package/dist/esm/hooks/use-tooltip-wrapper.js.map +7 -0
  134. package/package.json +4 -4
  135. package/schemas/base-button.json +1 -1
  136. package/schemas/button.json +1 -1
  137. package/schemas/icon-button.json +1 -1
  138. package/schemas/index.json +6 -6
  139. package/schemas/toggle-button.json +1 -1
  140. package/schemas/toggle-icon-button.json +1 -1
  141. package/src/components/_internal/base-button.css +136 -614
  142. package/src/components/_internal/base-button.tsx +15 -13
  143. package/src/components/_internal/shell-bottom.tsx +305 -321
  144. package/src/components/_internal/shell-inspector.tsx +310 -320
  145. package/src/components/_internal/shell-prop-helpers.ts +53 -0
  146. package/src/components/_internal/shell-sidebar.tsx +370 -384
  147. package/src/components/button.tsx +13 -42
  148. package/src/components/chatbar.tsx +7 -3
  149. package/src/components/icon-button.tsx +20 -44
  150. package/src/components/image.css +10 -8
  151. package/src/components/shell.context.tsx +1 -0
  152. package/src/components/shell.hooks.ts +67 -2
  153. package/src/components/shell.tsx +199 -209
  154. package/src/components/shell.types.ts +23 -0
  155. package/src/components/toggle-button.tsx +30 -59
  156. package/src/components/toggle-icon-button.tsx +29 -51
  157. package/src/hooks/index.ts +2 -0
  158. package/src/hooks/use-live-announcer.ts +34 -7
  159. package/src/hooks/use-toggle-state.ts +72 -0
  160. package/src/hooks/use-tooltip-wrapper.ts +28 -0
  161. package/src/styles/tokens/color.css +11 -1
  162. package/styles.css +70 -381
  163. package/tokens/base.css +7 -1
  164. package/tokens.css +7 -1
@@ -29,13 +29,14 @@ import * as React from 'react';
29
29
  import classNames from 'classnames';
30
30
  import * as Sheet from './sheet.js';
31
31
  import { VisuallyHidden } from './visually-hidden.js';
32
- import { useResponsivePresentation, useResponsiveValue } from './shell.hooks.js';
32
+ import { useResponsivePresentation, useResponsiveInitialState } from './shell.hooks.js';
33
33
  import { PaneResizeContext } from './_internal/shell-resize.js';
34
34
  import { PaneHandle, PanelHandle } from './_internal/shell-handles.js';
35
+ import { omitPaneProps, extractPaneDomProps, mapResponsiveBooleanToPaneMode } from './_internal/shell-prop-helpers.js';
35
36
  import { Sidebar } from './_internal/shell-sidebar.js';
36
37
  import { Bottom } from './_internal/shell-bottom.js';
37
38
  import { Inspector } from './_internal/shell-inspector.js';
38
- import type { PresentationValue, ResponsivePresentation, PaneMode, SidebarMode, PaneSizePersistence, Breakpoint, PaneTarget, Responsive } from './shell.types.js';
39
+ import type { PresentationValue, ResponsivePresentation, PaneMode, SidebarMode, PaneSizePersistence, Breakpoint, PaneTarget, Responsive, PaneBaseProps } from './shell.types.js';
39
40
  import { _BREAKPOINTS } from './shell.types.js';
40
41
  import {
41
42
  ShellProvider,
@@ -135,6 +136,26 @@ type PaneAction =
135
136
  | { type: 'EXPAND_PANE'; target: PaneTarget }
136
137
  | { type: 'COLLAPSE_PANE'; target: PaneTarget };
137
138
 
139
+ const SHELL_SLOT = Symbol('rtShellSlot');
140
+
141
+ function assignShellSlot<T extends React.ComponentType<any>>(component: T, slot: string): T {
142
+ (component as any)[SHELL_SLOT] = slot;
143
+ return component;
144
+ }
145
+
146
+ function isShellComponent(element: React.ReactElement, component: any): boolean {
147
+ if (!React.isValidElement(element)) return false;
148
+ const type: any = element.type;
149
+ if (type === component) return true;
150
+ const targetSlot = (component as any)?.[SHELL_SLOT];
151
+ return Boolean(type?.[SHELL_SLOT] && targetSlot && type[SHELL_SLOT] === targetSlot);
152
+ }
153
+
154
+ // Tag imported slot components so isType remains stable after minification
155
+ assignShellSlot(Sidebar as any, 'Shell.Sidebar');
156
+ assignShellSlot(Inspector as any, 'Shell.Inspector');
157
+ assignShellSlot(Bottom as any, 'Shell.Bottom');
158
+
138
159
  function paneReducer(state: PaneState, action: PaneAction): PaneState {
139
160
  switch (action.type) {
140
161
  case 'SET_LEFT_MODE': {
@@ -239,13 +260,17 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
239
260
  const initialChildren = React.Children.toArray(children) as React.ReactElement[];
240
261
  const hasPanelDefaultOpen = initialChildren.some((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Panel' && Boolean((el as any).props?.defaultOpen));
241
262
  const hasRailDefaultOpen = initialChildren.some((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Rail' && Boolean((el as any).props?.defaultOpen));
263
+ const hasInspectorDefaultOpen = initialChildren.some((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Inspector' && Boolean((el as any).props?.defaultOpen));
264
+ const hasInspectorOpenControlled = initialChildren.some(
265
+ (el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Inspector' && typeof (el as any).props?.open !== 'undefined' && Boolean((el as any).props?.open),
266
+ );
242
267
 
243
268
  // Pane state management via reducer
244
269
  const [paneState, dispatchPane] = React.useReducer(paneReducer, {
245
270
  leftMode: hasPanelDefaultOpen || hasRailDefaultOpen ? 'expanded' : 'collapsed',
246
271
  panelMode: hasPanelDefaultOpen ? 'expanded' : 'collapsed',
247
272
  sidebarMode: 'expanded',
248
- inspectorMode: 'collapsed',
273
+ inspectorMode: hasInspectorDefaultOpen || hasInspectorOpenControlled ? 'expanded' : 'collapsed',
249
274
  bottomMode: 'collapsed',
250
275
  });
251
276
  const setLeftMode = React.useCallback((mode: PaneMode) => dispatchPane({ type: 'SET_LEFT_MODE', mode }), []);
@@ -425,17 +450,21 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
425
450
  const peekCtxValue = React.useMemo(() => ({ peekTarget, setPeekTarget, peekPane, clearPeek }), [peekTarget, setPeekTarget, peekPane, clearPeek]);
426
451
  const actionsCtxValue = React.useMemo(() => ({ togglePane, expandPane, collapsePane, setSidebarToggleComputer }), [togglePane, expandPane, collapsePane, setSidebarToggleComputer]);
427
452
 
453
+ // Memoized full context value for ShellProvider to prevent unnecessary effect re-runs
454
+ const shellContextValue = React.useMemo(
455
+ () => ({
456
+ ...baseContextValue,
457
+ peekTarget,
458
+ setPeekTarget,
459
+ peekPane,
460
+ clearPeek,
461
+ }),
462
+ [baseContextValue, peekTarget, setPeekTarget, peekPane, clearPeek],
463
+ );
464
+
428
465
  return (
429
466
  <div {...props} ref={ref} className={classNames('rt-ShellRoot', className)} style={{ ...heightStyle, ...props.style }}>
430
- <ShellProvider
431
- value={{
432
- ...baseContextValue,
433
- peekTarget,
434
- setPeekTarget,
435
- peekPane,
436
- clearPeek,
437
- }}
438
- >
467
+ <ShellProvider value={shellContextValue}>
439
468
  <PresentationContext.Provider value={presentationCtxValue}>
440
469
  <LeftModeContext.Provider value={leftModeCtxValue}>
441
470
  <PanelModeContext.Provider value={panelModeCtxValue}>
@@ -496,7 +525,7 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
496
525
  </ShellProvider>
497
526
  </div>
498
527
  );
499
- });
528
+ }) as PanelComponent;
500
529
  Root.displayName = 'Shell.Root';
501
530
 
502
531
  // Header
@@ -518,26 +547,7 @@ const Header = React.forwardRef<HTMLElement, ShellHeaderProps>(({ className, hei
518
547
  Header.displayName = 'Shell.Header';
519
548
 
520
549
  // Pane Props Interface (shared by Panel, Sidebar, Inspector, Bottom)
521
- interface PaneProps extends React.ComponentPropsWithoutRef<'div'> {
522
- presentation?: ResponsivePresentation;
523
- expandedSize?: number;
524
- minSize?: number;
525
- maxSize?: number;
526
- resizable?: boolean;
527
- collapsible?: boolean;
528
- onExpand?: () => void;
529
- onCollapse?: () => void;
530
- onResize?: (size: number) => void;
531
- /** Optional custom content inside the resizer handle (kept unstyled). */
532
- resizer?: React.ReactNode;
533
- onResizeStart?: (size: number) => void;
534
- onResizeEnd?: (size: number) => void;
535
- snapPoints?: number[];
536
- snapTolerance?: number;
537
- collapseThreshold?: number;
538
- paneId?: string;
539
- persistence?: PaneSizePersistence;
540
- }
550
+ type PaneProps = PaneBaseProps;
541
551
 
542
552
  // Left container (auto-created for Rail+Panel)
543
553
  interface LeftProps extends React.ComponentPropsWithoutRef<'div'> {
@@ -549,6 +559,9 @@ interface LeftProps extends React.ComponentPropsWithoutRef<'div'> {
549
559
  collapsible?: boolean;
550
560
  onExpand?: () => void;
551
561
  onCollapse?: () => void;
562
+ mode?: never;
563
+ defaultMode?: never;
564
+ onModeChange?: never;
552
565
  }
553
566
 
554
567
  // Rail (special case)
@@ -566,166 +579,120 @@ type RailProps = React.ComponentPropsWithoutRef<'div'> & {
566
579
  } & (RailControlledProps | RailUncontrolledProps);
567
580
 
568
581
  // Left container - behaves like Inspector but contains Rail+Panel
569
- const Left = React.forwardRef<HTMLDivElement, LeftProps>(
570
- ({ className, presentation = { initial: 'fixed', sm: 'fixed' }, collapsible: _collapsible = true, onExpand, onCollapse, children, style, ...props }, ref) => {
571
- const shell = useShell();
572
- const resolvedPresentation = useResponsivePresentation(presentation);
573
- const isOverlay = resolvedPresentation === 'overlay';
574
- const isStacked = resolvedPresentation === 'stacked';
575
- const localRef = React.useRef<HTMLDivElement | null>(null);
576
- // Publish resolved presentation so Root can gate peeking in overlay
577
- React.useEffect(() => {
578
- (shell as any).onLeftPres?.(resolvedPresentation);
579
- }, [shell, resolvedPresentation]);
580
- const setRef = React.useCallback(
581
- (node: HTMLDivElement | null) => {
582
- localRef.current = node;
583
- if (typeof ref === 'function') ref(node);
584
- else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
585
- },
586
- [ref],
587
- );
588
-
589
- // Register with shell
590
- React.useEffect(() => {
591
- shell.setHasLeft(true);
592
- return () => shell.setHasLeft(false);
593
- }, [shell]);
582
+ const LEFT_DOM_OMIT_PROPS = ['open', 'defaultOpen', 'onOpenChange', 'mode', 'defaultMode', 'onModeChange'] as const;
583
+
584
+ const Left = React.forwardRef<HTMLDivElement, LeftProps>((initialProps, ref) => {
585
+ const { className, presentation = { initial: 'fixed', sm: 'fixed' }, collapsible: _collapsible = true, onExpand, onCollapse, children, style, ...restProps } = initialProps;
586
+ const propsOpen = restProps.open;
587
+ const propsDefaultOpen = restProps.defaultOpen;
588
+ const propsOnOpenChange = restProps.onOpenChange;
589
+ const domProps = omitPaneProps(restProps, LEFT_DOM_OMIT_PROPS);
590
+ const shell = useShell();
591
+ const resolvedPresentation = useResponsivePresentation(presentation);
592
+ const isOverlay = resolvedPresentation === 'overlay';
593
+ const isStacked = resolvedPresentation === 'stacked';
594
+ const localRef = React.useRef<HTMLDivElement | null>(null);
595
+ // Publish resolved presentation so Root can gate peeking in overlay
596
+ React.useEffect(() => {
597
+ (shell as any).onLeftPres?.(resolvedPresentation);
598
+ }, [shell, resolvedPresentation]);
599
+ const setRef = React.useCallback(
600
+ (node: HTMLDivElement | null) => {
601
+ localRef.current = node;
602
+ if (typeof ref === 'function') ref(node);
603
+ else if (ref) (ref as React.MutableRefObject<HTMLDivElement | null>).current = node;
604
+ },
605
+ [ref],
606
+ );
594
607
 
595
- const _lastBpRef = React.useRef<Breakpoint | null>(null);
596
- const lastLeftModeRef = React.useRef<PaneMode | null>(null);
597
- const initNotifiedRef = React.useRef(false);
598
- const resolvedDefaultOpen = useResponsiveValue((props as any).defaultOpen as any);
608
+ // Register with shell
609
+ React.useEffect(() => {
610
+ shell.setHasLeft(true);
611
+ return () => shell.setHasLeft(false);
612
+ }, [shell]);
613
+
614
+ const lastLeftModeRef = React.useRef<PaneMode | null>(null);
615
+ const initNotifiedRef = React.useRef(false);
616
+ const normalizedLeftControlled = React.useMemo(() => {
617
+ if (typeof propsOpen === 'undefined') return undefined;
618
+ return propsOpen ? 'expanded' : 'collapsed';
619
+ }, [propsOpen]);
620
+ const normalizedLeftDefault = React.useMemo(() => mapResponsiveBooleanToPaneMode(propsDefaultOpen), [propsDefaultOpen]);
621
+ useResponsiveInitialState<PaneMode>({
622
+ controlledValue: normalizedLeftControlled,
623
+ defaultValue: normalizedLeftDefault,
624
+ currentValue: shell.leftMode,
625
+ setValue: shell.setLeftMode,
626
+ breakpointReady: shell.currentBreakpointReady,
627
+ onInit: (initial) => propsOnOpenChange?.(initial === 'expanded', { reason: 'init' }),
628
+ });
599
629
 
600
- // Initialize from responsive defaultOpen once when uncontrolled and breakpoint ready
601
- const didInitFromDefaultOpenRef = React.useRef(false);
602
- const propsOpen = (props as any).open;
603
- const propsDefaultOpen = (props as any).defaultOpen;
604
- React.useEffect(() => {
605
- if (didInitFromDefaultOpenRef.current) return;
606
- if (!shell.currentBreakpointReady) return;
607
- if (typeof propsOpen !== 'undefined') return; // controlled
608
- if (typeof propsDefaultOpen === 'undefined') return;
609
- didInitFromDefaultOpenRef.current = true;
610
- const initial = Boolean(resolvedDefaultOpen);
611
- shell.setLeftMode(initial ? 'expanded' : 'collapsed');
612
- (props as any).onOpenChange?.(initial, { reason: 'init' });
613
- }, [shell, propsOpen, propsDefaultOpen, resolvedDefaultOpen, props]);
614
- React.useEffect(() => {
615
- // Controlled Left via Rail.open
616
- if (typeof propsOpen !== 'undefined') {
617
- const shouldOpen = Boolean(propsOpen);
618
- shell.setLeftMode(shouldOpen ? 'expanded' : 'collapsed');
619
- return;
620
- }
621
- // defaultOpen is applied in Rail; Left no longer follows responsive defaults
622
- }, [shell, propsOpen]);
630
+ // Emit mode changes (uncontrolled toggles + init)
631
+ React.useEffect(() => {
632
+ if (typeof propsOpen !== 'undefined') return; // controlled, notifications only via parent changes
633
+ if (lastLeftModeRef.current !== null && lastLeftModeRef.current !== shell.leftMode) {
634
+ propsOnOpenChange?.(shell.leftMode === 'expanded', { reason: 'toggle' });
635
+ }
636
+ lastLeftModeRef.current = shell.leftMode;
637
+ }, [shell, propsOnOpenChange, propsOpen]);
623
638
 
624
- // Sync controlled mode
625
- // removed mode sync
639
+ // Emit expand/collapse events
640
+ React.useEffect(() => {
641
+ if (shell.leftMode === 'expanded') {
642
+ onExpand?.();
643
+ } else {
644
+ onCollapse?.();
645
+ }
646
+ }, [shell.leftMode, onExpand, onCollapse]);
626
647
 
627
- // Emit mode changes (uncontrolled toggles + init)
628
- React.useEffect(() => {
629
- if (typeof (props as any).open !== 'undefined') return; // controlled, notifications only via parent changes
630
- if (!initNotifiedRef.current && Boolean(resolvedDefaultOpen) && shell.leftMode === 'expanded') {
631
- (props as any).onOpenChange?.(true, { reason: 'init' });
632
- initNotifiedRef.current = true;
633
- }
634
- if (lastLeftModeRef.current !== null && lastLeftModeRef.current !== shell.leftMode) {
635
- (props as any).onOpenChange?.(shell.leftMode === 'expanded', { reason: 'toggle' });
636
- }
637
- lastLeftModeRef.current = shell.leftMode;
638
- }, [shell, resolvedDefaultOpen, props]);
648
+ const _isExpanded = shell.leftMode === 'expanded';
639
649
 
640
- // Emit expand/collapse events
641
- React.useEffect(() => {
642
- if (shell.leftMode === 'expanded') {
643
- onExpand?.();
644
- } else {
645
- onCollapse?.();
646
- }
647
- }, [shell.leftMode, onExpand, onCollapse]);
648
-
649
- const _isExpanded = shell.leftMode === 'expanded';
650
-
651
- // Left is not resizable; width derives from Rail/Panel.
652
-
653
- if (isOverlay) {
654
- const open = shell.leftMode === 'expanded';
655
- // Compute overlay width from child Rail/Panel expanded sizes
656
- const childArray = React.Children.toArray(children) as React.ReactElement[];
657
- const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && el.type === comp;
658
- const railEl = childArray.find((el) => isType(el, Rail));
659
- const panelEl = childArray.find((el) => isType(el, Panel));
660
- const railSize = typeof (railEl as any)?.props?.expandedSize === 'number' ? (railEl as any).props.expandedSize : 64;
661
- const panelSize = typeof (panelEl as any)?.props?.expandedSize === 'number' ? (panelEl as any).props.expandedSize : 288;
662
- const hasRail = Boolean(railEl);
663
- const hasPanel = Boolean(panelEl);
664
- const overlayPx = (hasRail ? railSize : 0) + (shell.panelMode === 'expanded' && hasPanel ? panelSize : 0);
665
- return (
666
- <Sheet.Root open={open} onOpenChange={(o) => shell.setLeftMode(o ? 'expanded' : 'collapsed')}>
667
- <Sheet.Content
668
- side="start"
669
- style={{ padding: 0 }}
670
- width={{
671
- initial: `${overlayPx}px`,
672
- }}
673
- >
674
- <VisuallyHidden>
675
- <Sheet.Title>Navigation</Sheet.Title>
676
- </VisuallyHidden>
677
- <div className="rt-ShellLeft">{children}</div>
678
- </Sheet.Content>
679
- </Sheet.Root>
680
- );
681
- }
650
+ // Left is not resizable; width derives from Rail/Panel.
682
651
 
683
- if (isStacked) {
684
- const open = shell.leftMode === 'expanded';
685
- // Compute floating width from child Rail/Panel expanded sizes (like overlay)
686
- const childArray = React.Children.toArray(children) as React.ReactElement[];
687
- const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && el.type === comp;
688
- const railEl = childArray.find((el) => isType(el, Rail));
689
- const panelEl = childArray.find((el) => isType(el, Panel));
690
- const _railSize = typeof (railEl as any)?.props?.expandedSize === 'number' ? (railEl as any).props.expandedSize : 64;
691
- const _panelSize = typeof (panelEl as any)?.props?.expandedSize === 'number' ? (panelEl as any).props.expandedSize : 288;
692
- const _hasRail = Boolean(railEl);
693
- const _hasPanel = Boolean(panelEl);
694
- const _includePanel = _hasPanel && (shell.panelMode === 'expanded' || shell.peekTarget === 'panel');
695
-
696
- // Strip control props from DOM spread
697
- const { open: _openIgnored, defaultOpen: _defaultOpenIgnored, onOpenChange: _onOpenChangeIgnored, ...stackDomProps } = props as any;
698
-
699
- return (
700
- <div
701
- {...stackDomProps}
702
- ref={setRef}
703
- className={classNames('rt-ShellLeft', className)}
704
- data-mode={shell.leftMode}
705
- data-peek={shell.peekTarget === 'left' || shell.peekTarget === 'rail' || shell.peekTarget === 'panel' || undefined}
706
- data-presentation={resolvedPresentation}
707
- style={{
708
- ...style,
652
+ if (isOverlay) {
653
+ const open = shell.leftMode === 'expanded';
654
+ // Compute overlay width from child Rail/Panel expanded sizes
655
+ const childArray = React.Children.toArray(children) as React.ReactElement[];
656
+ const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && el.type === comp;
657
+ const railEl = childArray.find((el) => isType(el, Rail));
658
+ const panelEl = childArray.find((el) => isType(el, Panel));
659
+ const railSize = typeof (railEl as any)?.props?.expandedSize === 'number' ? (railEl as any).props.expandedSize : 64;
660
+ const panelSize = typeof (panelEl as any)?.props?.expandedSize === 'number' ? (panelEl as any).props.expandedSize : 288;
661
+ const hasRail = Boolean(railEl);
662
+ const hasPanel = Boolean(panelEl);
663
+ const overlayPx = (hasRail ? railSize : 0) + (shell.panelMode === 'expanded' && hasPanel ? panelSize : 0);
664
+ return (
665
+ <Sheet.Root open={open} onOpenChange={(o) => shell.setLeftMode(o ? 'expanded' : 'collapsed')}>
666
+ <Sheet.Content
667
+ side="start"
668
+ style={{ padding: 0 }}
669
+ width={{
670
+ initial: `${overlayPx}px`,
709
671
  }}
710
- data-open={open || undefined}
711
672
  >
712
- {children}
713
- </div>
714
- );
715
- }
673
+ <VisuallyHidden>
674
+ <Sheet.Title>Navigation</Sheet.Title>
675
+ </VisuallyHidden>
676
+ <div className="rt-ShellLeft">{children}</div>
677
+ </Sheet.Content>
678
+ </Sheet.Root>
679
+ );
680
+ }
716
681
 
717
- // Strip control/legacy props from DOM spread
718
- const {
719
- open: _openIgnored,
720
- defaultOpen: _defaultOpenIgnored,
721
- onOpenChange: _onOpenChangeIgnored,
722
- // legacy
723
- mode: _legacyModeIgnored,
724
- defaultMode: _legacyDefaultModeIgnored,
725
- onModeChange: _legacyOnModeChangeIgnored,
726
- ...domProps
727
- } = props as any;
682
+ if (isStacked) {
683
+ const open = shell.leftMode === 'expanded';
684
+ // Compute floating width from child Rail/Panel expanded sizes (like overlay)
685
+ const childArray = React.Children.toArray(children) as React.ReactElement[];
686
+ const isType = (el: React.ReactElement, comp: any) => React.isValidElement(el) && el.type === comp;
687
+ const railEl = childArray.find((el) => isType(el, Rail));
688
+ const panelEl = childArray.find((el) => isType(el, Panel));
689
+ const _railSize = typeof (railEl as any)?.props?.expandedSize === 'number' ? (railEl as any).props.expandedSize : 64;
690
+ const _panelSize = typeof (panelEl as any)?.props?.expandedSize === 'number' ? (panelEl as any).props.expandedSize : 288;
691
+ const _hasRail = Boolean(railEl);
692
+ const _hasPanel = Boolean(panelEl);
693
+ const _includePanel = _hasPanel && (shell.panelMode === 'expanded' || shell.peekTarget === 'panel');
728
694
 
695
+ // Strip control props from DOM spread
729
696
  return (
730
697
  <div
731
698
  {...domProps}
@@ -737,28 +704,48 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>(
737
704
  style={{
738
705
  ...style,
739
706
  }}
707
+ data-open={open || undefined}
740
708
  >
741
709
  {children}
742
710
  </div>
743
711
  );
744
- },
745
- );
712
+ }
713
+
714
+ // Strip control/legacy props from DOM spread
715
+ return (
716
+ <div
717
+ {...domProps}
718
+ ref={setRef}
719
+ className={classNames('rt-ShellLeft', className)}
720
+ data-mode={shell.leftMode}
721
+ data-peek={shell.peekTarget === 'left' || shell.peekTarget === 'rail' || shell.peekTarget === 'panel' || undefined}
722
+ data-presentation={resolvedPresentation}
723
+ style={{
724
+ ...style,
725
+ }}
726
+ >
727
+ {children}
728
+ </div>
729
+ );
730
+ });
746
731
  Left.displayName = 'Shell.Left';
732
+ assignShellSlot(Left as any, 'Shell.Left');
747
733
 
748
- const Rail = React.forwardRef<HTMLDivElement, RailProps>(({ className, presentation, expandedSize = 64, collapsible, onExpand, onCollapse, children, style, ...props }, ref) => {
734
+ const Rail = React.forwardRef<HTMLDivElement, RailProps>((initialProps, ref) => {
735
+ const { className, presentation, expandedSize = 64, collapsible, onExpand, onCollapse, children, style, open, defaultOpen, onOpenChange, ...domProps } = initialProps;
749
736
  const shell = useShell();
750
737
 
751
738
  // Dev guards
752
739
  const wasControlledRef = React.useRef<boolean | null>(null);
753
740
  if (process.env.NODE_ENV !== 'production') {
754
- if (typeof props.open !== 'undefined' && typeof props.defaultOpen !== 'undefined') {
741
+ if (typeof open !== 'undefined' && typeof defaultOpen !== 'undefined') {
755
742
  console.error('Shell.Rail: Do not pass both `open` and `defaultOpen`. Choose one.');
756
743
  }
757
744
  }
758
745
 
759
746
  // Warn on controlled/uncontrolled mode switch
760
747
  React.useEffect(() => {
761
- const isControlled = typeof props.open !== 'undefined';
748
+ const isControlled = typeof open !== 'undefined';
762
749
  if (wasControlledRef.current === null) {
763
750
  wasControlledRef.current = isControlled;
764
751
  return;
@@ -767,7 +754,7 @@ const Rail = React.forwardRef<HTMLDivElement, RailProps>(({ className, presentat
767
754
  console.warn('Shell.Rail: Switching between controlled and uncontrolled `open` is not supported.');
768
755
  wasControlledRef.current = isControlled;
769
756
  }
770
- }, [props.open]);
757
+ }, [open]);
771
758
 
772
759
  // Register expanded size with Left container
773
760
  React.useEffect(() => {
@@ -777,8 +764,6 @@ const Rail = React.forwardRef<HTMLDivElement, RailProps>(({ className, presentat
777
764
  const isExpanded = shell.leftMode === 'expanded';
778
765
 
779
766
  // Strip unknown open/defaultOpen props from DOM by not spreading them
780
- const { defaultOpen: _defaultOpenIgnored, open: _openIgnored, onOpenChange: _onOpenChangeIgnored, ...domProps } = props as any;
781
-
782
767
  return (
783
768
  <div
784
769
  {...domProps}
@@ -798,6 +783,7 @@ const Rail = React.forwardRef<HTMLDivElement, RailProps>(({ className, presentat
798
783
  );
799
784
  });
800
785
  Rail.displayName = 'Shell.Rail';
786
+ assignShellSlot(Rail as any, 'Shell.Rail');
801
787
 
802
788
  // Panel
803
789
  type HandleComponent = React.ForwardRefExoticComponent<React.ComponentPropsWithoutRef<'div'> & React.RefAttributes<HTMLDivElement>>;
@@ -836,9 +822,23 @@ type _InspectorComponent = React.ForwardRefExoticComponent<PaneProps & React.Ref
836
822
 
837
823
  type _BottomComponent = React.ForwardRefExoticComponent<PaneProps & React.RefAttributes<HTMLDivElement>> & { Handle: HandleComponent };
838
824
 
839
- const Panel = React.forwardRef<HTMLDivElement, PanelPublicProps>(
840
- (
841
- {
825
+ const PANEL_DOM_PROP_KEYS = [
826
+ 'className',
827
+ 'children',
828
+ 'defaultOpen',
829
+ 'open',
830
+ 'onOpenChange',
831
+ 'size',
832
+ 'defaultSize',
833
+ 'onSizeChange',
834
+ 'sizeUpdate',
835
+ 'sizeUpdateMs',
836
+ 'style',
837
+ ] as const satisfies readonly (keyof PanelPublicProps)[];
838
+
839
+ const Panel = assignShellSlot(
840
+ React.forwardRef<HTMLDivElement, PanelPublicProps>((initialProps, ref) => {
841
+ const {
842
842
  className,
843
843
  defaultOpen,
844
844
  open,
@@ -865,10 +865,8 @@ const Panel = React.forwardRef<HTMLDivElement, PanelPublicProps>(
865
865
  onSizeChange,
866
866
  sizeUpdate,
867
867
  sizeUpdateMs = 50,
868
- ...props
869
- },
870
- ref,
871
- ) => {
868
+ } = initialProps;
869
+ const panelDomProps = extractPaneDomProps(initialProps, PANEL_DOM_PROP_KEYS);
872
870
  // Throttled/debounced emitter for onSizeChange
873
871
  const emitSizeChange = React.useMemo(() => {
874
872
  if (!onSizeChange) return () => {};
@@ -1141,16 +1139,6 @@ const Panel = React.forwardRef<HTMLDivElement, PanelPublicProps>(
1141
1139
  </PaneResizeContext.Provider>
1142
1140
  ) : null;
1143
1141
 
1144
- // Strip control props from DOM spread
1145
- const {
1146
- defaultOpen: _panelDefaultOpenIgnored,
1147
- open: _panelOpenIgnored,
1148
- onOpenChange: _panelOnOpenChangeIgnored,
1149
- size: _panelSizeIgnored,
1150
- defaultSize: _panelDefaultSizeIgnored,
1151
- ...panelDomProps
1152
- } = props as any;
1153
-
1154
1142
  return (
1155
1143
  <div
1156
1144
  {...panelDomProps}
@@ -1170,7 +1158,8 @@ const Panel = React.forwardRef<HTMLDivElement, PanelPublicProps>(
1170
1158
  {handleEl}
1171
1159
  </div>
1172
1160
  );
1173
- },
1161
+ }),
1162
+ 'Shell.Panel',
1174
1163
  ) as PanelComponent;
1175
1164
  Panel.displayName = 'Shell.Panel';
1176
1165
  Panel.Handle = PanelHandle;
@@ -1182,6 +1171,7 @@ interface ShellContentProps extends React.ComponentPropsWithoutRef<'main'> {}
1182
1171
 
1183
1172
  const Content = React.forwardRef<HTMLElement, ShellContentProps>(({ className, ...props }, ref) => <main {...props} ref={ref} className={classNames('rt-ShellContent', className)} />);
1184
1173
  Content.displayName = 'Shell.Content';
1174
+ assignShellSlot(Content as any, 'Shell.Content');
1185
1175
 
1186
1176
  // Inspector moved to ./_internal/shell-inspector
1187
1177
 
@@ -1,3 +1,5 @@
1
+ import type * as React from 'react';
2
+
1
3
  export type PresentationValue = 'fixed' | 'overlay' | 'stacked';
2
4
 
3
5
  export type ResponsivePresentation = PresentationValue | Partial<Record<'initial' | 'xs' | 'sm' | 'md' | 'lg' | 'xl', PresentationValue>>;
@@ -16,6 +18,27 @@ export type PaneSizePersistence = {
16
18
  save?: (size: number) => void | Promise<void>;
17
19
  };
18
20
 
21
+ export interface PaneBaseProps extends React.ComponentPropsWithoutRef<'div'> {
22
+ presentation?: ResponsivePresentation;
23
+ expandedSize?: number;
24
+ minSize?: number;
25
+ maxSize?: number;
26
+ height?: string | number;
27
+ resizable?: boolean;
28
+ collapsible?: boolean;
29
+ onExpand?: () => void;
30
+ onCollapse?: () => void;
31
+ onResize?: (size: number) => void;
32
+ resizer?: React.ReactNode;
33
+ onResizeStart?: (size: number) => void;
34
+ onResizeEnd?: (size: number) => void;
35
+ snapPoints?: number[];
36
+ snapTolerance?: number;
37
+ collapseThreshold?: number;
38
+ paneId?: string;
39
+ persistence?: PaneSizePersistence;
40
+ }
41
+
19
42
  export const _BREAKPOINTS = {
20
43
  xs: '(min-width: 520px)',
21
44
  sm: '(min-width: 768px)',