@kushagradhawan/kookie-ui 0.1.108 → 0.1.109

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 (56) hide show
  1. package/components.css +72 -51
  2. package/dist/cjs/components/_internal/shell-bottom.d.ts +2 -0
  3. package/dist/cjs/components/_internal/shell-bottom.d.ts.map +1 -1
  4. package/dist/cjs/components/_internal/shell-bottom.js +1 -1
  5. package/dist/cjs/components/_internal/shell-bottom.js.map +3 -3
  6. package/dist/cjs/components/_internal/shell-inspector.d.ts +2 -0
  7. package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -1
  8. package/dist/cjs/components/_internal/shell-inspector.js +1 -1
  9. package/dist/cjs/components/_internal/shell-inspector.js.map +3 -3
  10. package/dist/cjs/components/_internal/shell-sidebar.d.ts +2 -0
  11. package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -1
  12. package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
  13. package/dist/cjs/components/_internal/shell-sidebar.js.map +3 -3
  14. package/dist/cjs/components/shell.context.d.ts +13 -0
  15. package/dist/cjs/components/shell.context.d.ts.map +1 -1
  16. package/dist/cjs/components/shell.context.js +1 -1
  17. package/dist/cjs/components/shell.context.js.map +3 -3
  18. package/dist/cjs/components/shell.d.ts +14 -6
  19. package/dist/cjs/components/shell.d.ts.map +1 -1
  20. package/dist/cjs/components/shell.js +1 -1
  21. package/dist/cjs/components/shell.js.map +3 -3
  22. package/dist/esm/components/_internal/shell-bottom.d.ts +2 -0
  23. package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
  24. package/dist/esm/components/_internal/shell-bottom.js +1 -1
  25. package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
  26. package/dist/esm/components/_internal/shell-inspector.d.ts +2 -0
  27. package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
  28. package/dist/esm/components/_internal/shell-inspector.js +1 -1
  29. package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
  30. package/dist/esm/components/_internal/shell-sidebar.d.ts +2 -0
  31. package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
  32. package/dist/esm/components/_internal/shell-sidebar.js +1 -1
  33. package/dist/esm/components/_internal/shell-sidebar.js.map +3 -3
  34. package/dist/esm/components/shell.context.d.ts +13 -0
  35. package/dist/esm/components/shell.context.d.ts.map +1 -1
  36. package/dist/esm/components/shell.context.js +1 -1
  37. package/dist/esm/components/shell.context.js.map +3 -3
  38. package/dist/esm/components/shell.d.ts +14 -6
  39. package/dist/esm/components/shell.d.ts.map +1 -1
  40. package/dist/esm/components/shell.js +1 -1
  41. package/dist/esm/components/shell.js.map +3 -3
  42. package/package.json +1 -1
  43. package/schemas/base-button.json +1 -1
  44. package/schemas/button.json +1 -1
  45. package/schemas/icon-button.json +1 -1
  46. package/schemas/index.json +6 -6
  47. package/schemas/toggle-button.json +1 -1
  48. package/schemas/toggle-icon-button.json +1 -1
  49. package/src/components/_internal/shell-bottom.tsx +15 -1
  50. package/src/components/_internal/shell-inspector.tsx +15 -1
  51. package/src/components/_internal/shell-sidebar.tsx +15 -1
  52. package/src/components/segmented-control.css +37 -37
  53. package/src/components/shell.context.tsx +14 -0
  54. package/src/components/shell.css +51 -28
  55. package/src/components/shell.tsx +150 -81
  56. package/styles.css +72 -51
@@ -51,6 +51,9 @@ import {
51
51
  PeekContext,
52
52
  ActionsContext,
53
53
  CompositionContext,
54
+ InsetContext,
55
+ useInset,
56
+ type InsetPaneId,
54
57
  } from './shell.context.js';
55
58
 
56
59
  // Shell context is provided via ShellProvider (see shell.context.tsx)
@@ -260,7 +263,20 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
260
263
  // Compute initial defaults from immediate children (one-time, uncontrolled defaults)
261
264
  const initialChildren = React.Children.toArray(children) as React.ReactElement[];
262
265
  const hasPanelDefaultOpen = initialChildren.some((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Panel' && Boolean((el as any).props?.defaultOpen));
263
- const hasRailDefaultOpen = initialChildren.some((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Rail' && Boolean((el as any).props?.defaultOpen));
266
+ // Rail defaults to open (true) unless explicitly set to false
267
+ // Supports responsive objects: { initial: false, md: true }
268
+ const railEl = initialChildren.find((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Rail');
269
+ const railDefaultOpen = railEl ? (railEl as any).props?.defaultOpen : undefined;
270
+ const hasRailDefaultOpen = (() => {
271
+ if (!railEl) return false;
272
+ if (railDefaultOpen === undefined) return true; // Default to open
273
+ if (typeof railDefaultOpen === 'boolean') return railDefaultOpen;
274
+ // Responsive object - use 'initial' value, or first defined value, or default true
275
+ if (typeof railDefaultOpen === 'object') {
276
+ return railDefaultOpen.initial ?? Object.values(railDefaultOpen)[0] ?? true;
277
+ }
278
+ return true;
279
+ })();
264
280
  const hasInspectorDefaultOpen = initialChildren.some((el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Inspector' && Boolean((el as any).props?.defaultOpen));
265
281
  const hasInspectorOpenControlled = initialChildren.some(
266
282
  (el) => React.isValidElement(el) && (el as any).type?.displayName === 'Shell.Inspector' && typeof (el as any).props?.open !== 'undefined' && Boolean((el as any).props?.open),
@@ -492,6 +508,30 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
492
508
  const inspectorModeCtxValue = React.useMemo(() => ({ inspectorMode: paneState.inspectorMode, setInspectorMode }), [paneState.inspectorMode, setInspectorMode]);
493
509
  const bottomModeCtxValue = React.useMemo(() => ({ bottomMode: paneState.bottomMode, setBottomMode }), [paneState.bottomMode, setBottomMode]);
494
510
  const compositionCtxValue = React.useMemo(() => ({ hasLeft, setHasLeft, hasSidebar, setHasSidebar }), [hasLeft, setHasLeft, hasSidebar, setHasSidebar]);
511
+
512
+ // Inset state management
513
+ const [insetPanes, setInsetPanes] = React.useState<Set<InsetPaneId>>(new Set());
514
+ const registerInset = React.useCallback((id: InsetPaneId) => {
515
+ setInsetPanes((prev) => {
516
+ if (prev.has(id)) return prev;
517
+ const next = new Set(prev);
518
+ next.add(id);
519
+ return next;
520
+ });
521
+ }, []);
522
+ const unregisterInset = React.useCallback((id: InsetPaneId) => {
523
+ setInsetPanes((prev) => {
524
+ if (!prev.has(id)) return prev;
525
+ const next = new Set(prev);
526
+ next.delete(id);
527
+ return next;
528
+ });
529
+ }, []);
530
+ const hasAnyInset = insetPanes.size > 0;
531
+ const insetCtxValue = React.useMemo(
532
+ () => ({ insetPanes, registerInset, unregisterInset, hasAnyInset }),
533
+ [insetPanes, registerInset, unregisterInset, hasAnyInset],
534
+ );
495
535
  const peekCtxValue = React.useMemo(() => ({ peekTarget, setPeekTarget, peekPane, clearPeek }), [peekTarget, setPeekTarget, peekPane, clearPeek]);
496
536
  const actionsCtxValue = React.useMemo(() => ({ togglePane, expandPane, collapsePane, setSidebarToggleComputer }), [togglePane, expandPane, collapsePane, setSidebarToggleComputer]);
497
537
 
@@ -519,45 +559,51 @@ const Root = React.forwardRef<HTMLDivElement, ShellRootProps>(({ className, chil
519
559
  <CompositionContext.Provider value={compositionCtxValue}>
520
560
  <PeekContext.Provider value={peekCtxValue}>
521
561
  <ActionsContext.Provider value={actionsCtxValue}>
522
- {headerEls}
523
- <div
524
- className="rt-ShellBody"
525
- data-peek-target={peekTarget ?? undefined}
526
- style={
527
- peekTarget === 'rail' || peekTarget === 'panel'
528
- ? ({
529
- ['--peek-rail-width' as any]: `${railDefaultSizeRef.current}px`,
530
- } as React.CSSProperties)
531
- : undefined
532
- }
533
- >
534
- {hasLeftChildren && !hasSidebarChildren
535
- ? (() => {
536
- const firstRail = railEls[0] as any;
537
- const passthroughProps = firstRail
538
- ? {
539
- // Notification passthrough used by Left; not spread to DOM in Left
540
- onOpenChange: firstRail.props?.onOpenChange,
541
- open: firstRail.props?.open,
542
- defaultOpen: firstRail.props?.defaultOpen,
543
- presentation: firstRail.props?.presentation,
544
- collapsible: firstRail.props?.collapsible,
545
- onExpand: firstRail.props?.onExpand,
546
- onCollapse: firstRail.props?.onCollapse,
547
- }
548
- : { defaultOpen: hasPanelDefaultOpen ? true : undefined };
549
- return (
550
- <Left {...(passthroughProps as any)}>
551
- {railEls}
552
- {panelEls}
553
- </Left>
554
- );
555
- })()
556
- : sidebarEls}
557
- {contentEls}
558
- {inspectorEls}
559
- </div>
560
- {bottomEls}
562
+ <InsetContext.Provider value={insetCtxValue}>
563
+ {headerEls}
564
+ <div
565
+ className="rt-ShellBody"
566
+ data-peek-target={peekTarget ?? undefined}
567
+ data-has-inset={hasAnyInset || undefined}
568
+ style={
569
+ peekTarget === 'rail' || peekTarget === 'panel'
570
+ ? ({
571
+ ['--peek-rail-width' as any]: `${railDefaultSizeRef.current}px`,
572
+ } as React.CSSProperties)
573
+ : undefined
574
+ }
575
+ >
576
+ {hasLeftChildren && !hasSidebarChildren
577
+ ? (() => {
578
+ const firstRail = railEls[0] as any;
579
+ const firstPanel = panelEls[0] as any;
580
+ const leftInset = Boolean(firstRail?.props?.inset) || Boolean(firstPanel?.props?.inset);
581
+ const passthroughProps = firstRail
582
+ ? {
583
+ // Notification passthrough used by Left; not spread to DOM in Left
584
+ onOpenChange: firstRail.props?.onOpenChange,
585
+ open: firstRail.props?.open,
586
+ defaultOpen: firstRail.props?.defaultOpen,
587
+ presentation: firstRail.props?.presentation,
588
+ collapsible: firstRail.props?.collapsible,
589
+ onExpand: firstRail.props?.onExpand,
590
+ onCollapse: firstRail.props?.onCollapse,
591
+ inset: leftInset,
592
+ }
593
+ : { defaultOpen: hasPanelDefaultOpen ? true : undefined, inset: leftInset };
594
+ return (
595
+ <Left {...(passthroughProps as any)}>
596
+ {railEls}
597
+ {panelEls}
598
+ </Left>
599
+ );
600
+ })()
601
+ : sidebarEls}
602
+ {contentEls}
603
+ {inspectorEls}
604
+ </div>
605
+ {bottomEls}
606
+ </InsetContext.Provider>
561
607
  </ActionsContext.Provider>
562
608
  </PeekContext.Provider>
563
609
  </CompositionContext.Provider>
@@ -607,13 +653,15 @@ interface LeftProps extends React.ComponentPropsWithoutRef<'div'> {
607
653
  mode?: never;
608
654
  defaultMode?: never;
609
655
  onModeChange?: never;
656
+ /** When true, adds margin and triggers gray backdrop on Shell. */
657
+ inset?: boolean;
610
658
  }
611
659
 
612
660
  // Rail (special case)
613
661
  type LeftOpenChangeMeta = { reason: 'init' | 'toggle' | 'responsive' | 'panel' };
614
662
 
615
- type RailControlledProps = { open: boolean; onOpenChange?: (open: boolean, meta: LeftOpenChangeMeta) => void; defaultOpen?: never };
616
- type RailUncontrolledProps = { defaultOpen?: boolean; onOpenChange?: (open: boolean, meta: LeftOpenChangeMeta) => void; open?: never };
663
+ type RailControlledProps = { open: boolean | Partial<Record<Breakpoint, boolean>>; onOpenChange?: (open: boolean, meta: LeftOpenChangeMeta) => void; defaultOpen?: never };
664
+ type RailUncontrolledProps = { defaultOpen?: boolean | Partial<Record<Breakpoint, boolean>>; onOpenChange?: (open: boolean, meta: LeftOpenChangeMeta) => void; open?: never };
617
665
 
618
666
  type RailProps = React.ComponentPropsWithoutRef<'div'> & {
619
667
  presentation?: ResponsivePresentation;
@@ -621,13 +669,24 @@ type RailProps = React.ComponentPropsWithoutRef<'div'> & {
621
669
  collapsible?: boolean;
622
670
  onExpand?: () => void;
623
671
  onCollapse?: () => void;
672
+ /** When true, adds margin to Rail+Panel and triggers gray backdrop on Shell. */
673
+ inset?: boolean;
624
674
  } & (RailControlledProps | RailUncontrolledProps);
625
675
 
626
676
  // Left container - behaves like Inspector but contains Rail+Panel
627
677
  const LEFT_DOM_OMIT_PROPS = ['open', 'defaultOpen', 'onOpenChange', 'mode', 'defaultMode', 'onModeChange'] as const;
628
678
 
629
679
  const Left = React.forwardRef<HTMLDivElement, LeftProps>((initialProps, ref) => {
630
- const { className, presentation = { initial: 'fixed', sm: 'fixed' }, collapsible: _collapsible = true, onExpand, onCollapse, children, style, ...restProps } = initialProps;
680
+ const { className, presentation = { initial: 'fixed', sm: 'fixed' }, collapsible: _collapsible = true, onExpand, onCollapse, children, style, inset, ...restProps } = initialProps;
681
+ const { registerInset, unregisterInset } = useInset();
682
+
683
+ // Register/unregister inset
684
+ React.useEffect(() => {
685
+ if (inset) {
686
+ registerInset('left');
687
+ return () => unregisterInset('left');
688
+ }
689
+ }, [inset, registerInset, unregisterInset]);
631
690
  const propsOpen = restProps.open;
632
691
  const propsDefaultOpen = restProps.defaultOpen;
633
692
  const propsOnOpenChange = restProps.onOpenChange;
@@ -746,6 +805,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>((initialProps, ref) =>
746
805
  data-mode={shell.leftMode}
747
806
  data-peek={shell.peekTarget === 'left' || shell.peekTarget === 'rail' || shell.peekTarget === 'panel' || undefined}
748
807
  data-presentation={resolvedPresentation}
808
+ data-inset={inset || undefined}
749
809
  style={{
750
810
  ...style,
751
811
  }}
@@ -765,6 +825,7 @@ const Left = React.forwardRef<HTMLDivElement, LeftProps>((initialProps, ref) =>
765
825
  data-mode={shell.leftMode}
766
826
  data-peek={shell.peekTarget === 'left' || shell.peekTarget === 'rail' || shell.peekTarget === 'panel' || undefined}
767
827
  data-presentation={resolvedPresentation}
828
+ data-inset={inset || undefined}
768
829
  style={{
769
830
  ...style,
770
831
  }}
@@ -833,9 +894,9 @@ assignShellSlot(Rail as any, 'Shell.Rail');
833
894
  // Panel
834
895
  type HandleComponent = React.ForwardRefExoticComponent<React.ComponentPropsWithoutRef<'div'> & React.RefAttributes<HTMLDivElement>>;
835
896
 
836
- type PanelOpenChangeMeta = { reason: 'toggle' | 'left' | 'init' };
837
- type PanelControlledProps = { open: boolean; onOpenChange?: (open: boolean, meta: PanelOpenChangeMeta) => void; defaultOpen?: never };
838
- type PanelUncontrolledProps = { defaultOpen?: boolean; onOpenChange?: (open: boolean, meta: PanelOpenChangeMeta) => void; open?: never };
897
+ type PanelOpenChangeMeta = { reason: 'toggle' | 'left' | 'init' | 'responsive' };
898
+ type PanelControlledProps = { open: boolean | Partial<Record<Breakpoint, boolean>>; onOpenChange?: (open: boolean, meta: PanelOpenChangeMeta) => void; defaultOpen?: never };
899
+ type PanelUncontrolledProps = { defaultOpen?: boolean | Partial<Record<Breakpoint, boolean>>; onOpenChange?: (open: boolean, meta: PanelOpenChangeMeta) => void; open?: never };
839
900
 
840
901
  type PanelSizeControlledProps = { size: number | string; defaultSize?: never };
841
902
  type PanelSizeUncontrolledProps = { defaultSize?: number | string; size?: never };
@@ -847,6 +908,8 @@ type PanelPublicProps = Omit<PaneProps, 'presentation' | 'defaultMode'> &
847
908
  onSizeChange?: (size: number, meta: PanelSizeChangeMeta) => void;
848
909
  sizeUpdate?: 'throttle' | 'debounce';
849
910
  sizeUpdateMs?: number;
911
+ /** When true, adds margin to Rail+Panel and triggers gray backdrop on Shell. */
912
+ inset?: boolean;
850
913
  };
851
914
  type PanelComponent = React.ForwardRefExoticComponent<PanelPublicProps & React.RefAttributes<HTMLDivElement>> & {
852
915
  Handle: HandleComponent;
@@ -963,31 +1026,32 @@ const Panel = assignShellSlot(
963
1026
  }
964
1027
  }
965
1028
 
966
- // Initialize uncontrolled open state from defaultOpen on first mount
967
- React.useEffect(() => {
968
- if (typeof open === 'undefined' && typeof defaultOpen === 'boolean') {
969
- if (defaultOpen) {
970
- // Ensure Left is expanded before expanding Panel
1029
+ // Normalize responsive open/defaultOpen to PaneMode
1030
+ const normalizedControlledOpen = React.useMemo(() => mapResponsiveBooleanToPaneMode(open), [open]);
1031
+ const normalizedDefaultOpen = React.useMemo(() => mapResponsiveBooleanToPaneMode(defaultOpen), [defaultOpen]);
1032
+ const openIsResponsive = typeof open === 'object' && open !== null;
1033
+
1034
+ // Use responsive initial state hook for proper breakpoint handling
1035
+ useResponsiveInitialState<PaneMode>({
1036
+ controlledValue: normalizedControlledOpen,
1037
+ defaultValue: normalizedDefaultOpen,
1038
+ currentValue: shell.panelMode,
1039
+ setValue: (mode) => {
1040
+ // Ensure Left is expanded when Panel is expanded
1041
+ if (mode === 'expanded' && shell.leftMode !== 'expanded') {
971
1042
  shell.setLeftMode('expanded');
972
- shell.setPanelMode('expanded');
973
- } else {
974
- shell.setPanelMode('collapsed');
975
1043
  }
976
- }
977
- // run only on mount
978
- // eslint-disable-next-line react-hooks/exhaustive-deps
979
- }, []);
980
-
981
- // Controlled sync: mirror shell state when `open` is provided
982
- React.useEffect(() => {
983
- if (typeof open === 'undefined') return;
984
- if (open) {
985
- if (shell.leftMode !== 'expanded') shell.setLeftMode('expanded');
986
- if (shell.panelMode !== 'expanded') shell.setPanelMode('expanded');
987
- } else {
988
- if (shell.panelMode !== 'collapsed') shell.setPanelMode('collapsed');
989
- }
990
- }, [shell, open]);
1044
+ shell.setPanelMode(mode);
1045
+ },
1046
+ breakpointReady: shell.currentBreakpointReady,
1047
+ controlledIsResponsive: openIsResponsive,
1048
+ onResponsiveChange: (next) => onOpenChange?.(next === 'expanded', { reason: 'responsive' }),
1049
+ onInit: (initial) => {
1050
+ if (typeof open === 'undefined') {
1051
+ onOpenChange?.(initial === 'expanded', { reason: 'init' });
1052
+ }
1053
+ },
1054
+ });
991
1055
 
992
1056
  // Dev-only warning if switching controlled/uncontrolled between renders
993
1057
  const wasControlledRef = React.useRef<boolean | null>(null);
@@ -1003,16 +1067,6 @@ const Panel = assignShellSlot(
1003
1067
  }
1004
1068
  }, [open]);
1005
1069
 
1006
- // Notify init open
1007
- React.useEffect(() => {
1008
- if (initNotifiedRef.current) return;
1009
- if (typeof open === 'undefined' && defaultOpen && shell.panelMode === 'expanded') {
1010
- onOpenChange?.(true, { reason: 'init' });
1011
- initNotifiedRef.current = true;
1012
- }
1013
- // eslint-disable-next-line react-hooks/exhaustive-deps
1014
- }, []);
1015
-
1016
1070
  React.useEffect(() => {
1017
1071
  (shell as any).onPanelDefaults?.(expandedSize);
1018
1072
  }, [shell, expandedSize]);
@@ -1209,9 +1263,24 @@ Panel.Handle = PanelHandle;
1209
1263
  // Sidebar moved to ./_internal/shell-sidebar
1210
1264
 
1211
1265
  // Content (always required)
1212
- interface ShellContentProps extends React.ComponentPropsWithoutRef<'main'> {}
1266
+ interface ShellContentProps extends React.ComponentPropsWithoutRef<'main'> {
1267
+ /** When true, adds margin and triggers gray backdrop on Shell. */
1268
+ inset?: boolean;
1269
+ }
1270
+
1271
+ const Content = React.forwardRef<HTMLElement, ShellContentProps>(({ className, inset, ...props }, ref) => {
1272
+ const { registerInset, unregisterInset } = useInset();
1213
1273
 
1214
- const Content = React.forwardRef<HTMLElement, ShellContentProps>(({ className, ...props }, ref) => <main {...props} ref={ref} className={classNames('rt-ShellContent', className)} />);
1274
+ // Register/unregister inset
1275
+ React.useEffect(() => {
1276
+ if (inset) {
1277
+ registerInset('content');
1278
+ return () => unregisterInset('content');
1279
+ }
1280
+ }, [inset, registerInset, unregisterInset]);
1281
+
1282
+ return <main {...props} ref={ref} className={classNames('rt-ShellContent', className)} data-inset={inset || undefined} />;
1283
+ });
1215
1284
  Content.displayName = 'Shell.Content';
1216
1285
  assignShellSlot(Content as any, 'Shell.Content');
1217
1286