@kushagradhawan/kookie-ui 0.1.108 → 0.1.110

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 (79) hide show
  1. package/components.css +187 -133
  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/cjs/components/sidebar.d.ts.map +1 -1
  23. package/dist/cjs/components/sidebar.js +1 -1
  24. package/dist/cjs/components/sidebar.js.map +3 -3
  25. package/dist/cjs/components/sidebar.props.d.ts +1 -1
  26. package/dist/cjs/components/sidebar.props.js +1 -1
  27. package/dist/cjs/components/sidebar.props.js.map +2 -2
  28. package/dist/esm/components/_internal/shell-bottom.d.ts +2 -0
  29. package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
  30. package/dist/esm/components/_internal/shell-bottom.js +1 -1
  31. package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
  32. package/dist/esm/components/_internal/shell-inspector.d.ts +2 -0
  33. package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
  34. package/dist/esm/components/_internal/shell-inspector.js +1 -1
  35. package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
  36. package/dist/esm/components/_internal/shell-sidebar.d.ts +2 -0
  37. package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
  38. package/dist/esm/components/_internal/shell-sidebar.js +1 -1
  39. package/dist/esm/components/_internal/shell-sidebar.js.map +3 -3
  40. package/dist/esm/components/shell.context.d.ts +13 -0
  41. package/dist/esm/components/shell.context.d.ts.map +1 -1
  42. package/dist/esm/components/shell.context.js +1 -1
  43. package/dist/esm/components/shell.context.js.map +3 -3
  44. package/dist/esm/components/shell.d.ts +14 -6
  45. package/dist/esm/components/shell.d.ts.map +1 -1
  46. package/dist/esm/components/shell.js +1 -1
  47. package/dist/esm/components/shell.js.map +3 -3
  48. package/dist/esm/components/sidebar.d.ts.map +1 -1
  49. package/dist/esm/components/sidebar.js +1 -1
  50. package/dist/esm/components/sidebar.js.map +3 -3
  51. package/dist/esm/components/sidebar.props.d.ts +1 -1
  52. package/dist/esm/components/sidebar.props.js +1 -1
  53. package/dist/esm/components/sidebar.props.js.map +2 -2
  54. package/package.json +1 -1
  55. package/schemas/base-button.json +1 -1
  56. package/schemas/button.json +1 -1
  57. package/schemas/icon-button.json +1 -1
  58. package/schemas/index.json +6 -6
  59. package/schemas/toggle-button.json +1 -1
  60. package/schemas/toggle-icon-button.json +1 -1
  61. package/src/components/_internal/base-button.css +6 -32
  62. package/src/components/_internal/base-card.css +0 -3
  63. package/src/components/_internal/base-checkbox.css +0 -2
  64. package/src/components/_internal/base-radio.css +0 -2
  65. package/src/components/_internal/shell-bottom.tsx +15 -1
  66. package/src/components/_internal/shell-inspector.tsx +15 -1
  67. package/src/components/_internal/shell-sidebar.tsx +15 -1
  68. package/src/components/avatar.css +0 -1
  69. package/src/components/segmented-control.css +37 -37
  70. package/src/components/select.css +0 -2
  71. package/src/components/shell.context.tsx +14 -0
  72. package/src/components/shell.css +51 -28
  73. package/src/components/shell.tsx +150 -81
  74. package/src/components/sidebar.css +110 -6
  75. package/src/components/sidebar.props.tsx +1 -1
  76. package/src/components/sidebar.tsx +45 -2
  77. package/src/components/text-area.css +0 -1
  78. package/src/components/text-field.css +0 -1
  79. package/styles.css +187 -133
@@ -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
 
@@ -36,10 +36,27 @@
36
36
  text-align: center !important;
37
37
  font-size: var(--font-size-0) !important;
38
38
  line-height: var(--line-height-0) !important;
39
- padding-top: var(--space-2) !important;
40
- padding-bottom: var(--space-2) !important;
41
39
  padding-inline-start: var(--space-1) !important;
42
40
  padding-inline-end: var(--space-1) !important;
41
+ /* Add margin for balanced spacing like normal variant */
42
+ margin-top: 1px;
43
+ margin-bottom: 1px;
44
+ }
45
+
46
+ /* Size-specific vertical padding for thin mode buttons */
47
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-r-size-1) :where(.rt-SidebarMenuButton, .rt-SidebarMenuSubTrigger) {
48
+ padding-top: var(--space-1) !important;
49
+ padding-bottom: var(--space-1) !important;
50
+ }
51
+
52
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-r-size-2) :where(.rt-SidebarMenuButton, .rt-SidebarMenuSubTrigger) {
53
+ padding-top: var(--space-1) !important;
54
+ padding-bottom: var(--space-1) !important;
55
+ }
56
+
57
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-r-size-3) :where(.rt-SidebarMenuButton, .rt-SidebarMenuSubTrigger) {
58
+ padding-top: var(--space-2) !important;
59
+ padding-bottom: var(--space-2) !important;
43
60
  }
44
61
 
45
62
  /* Consolidated thin mode size blocks */
@@ -195,20 +212,107 @@
195
212
  display: block;
196
213
  }
197
214
 
198
- /* Truncate labels inside an explicit label span */
215
+ /* Icon wrapper for thin mode - receives hover/active background */
216
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarMenuIconWrapper) {
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ /* Override the 100% width from .rt-SidebarMenuButton > * - icon wrapper should fit content */
221
+ width: auto;
222
+ padding: var(--space-3) var(--space-2);
223
+ border-radius: var(--radius-2);
224
+ transition:
225
+ background-color var(--motion-duration-micro) var(--motion-ease-standard),
226
+ color var(--motion-duration-small) var(--motion-ease-standard);
227
+ }
228
+
229
+ /* Size-specific icon wrapper padding */
230
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-r-size-1 .rt-SidebarMenuIconWrapper) {
231
+ padding: var(--space-2) var(--space-2);
232
+ border-radius: var(--radius-1);
233
+ }
234
+
235
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-r-size-2 .rt-SidebarMenuIconWrapper) {
236
+ padding: var(--space-2);
237
+ border-radius: var(--radius-2);
238
+ }
239
+
240
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-r-size-3 .rt-SidebarMenuIconWrapper) {
241
+ padding: var(--space-3);
242
+ border-radius: var(--radius-2);
243
+ }
244
+
245
+ /* Label styling in thin mode - outside the background area */
199
246
  :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarMenuButton .rt-SidebarMenuLabel) {
200
247
  display: block;
201
248
  max-width: 100%;
202
249
  min-width: 0;
203
- overflow: hidden;
204
- text-overflow: ellipsis;
250
+ overflow: visible;
251
+ text-overflow: clip;
205
252
  white-space: nowrap;
206
253
  text-align: center;
207
- color: var(--accent-11);
254
+ color: var(--gray-11);
208
255
  font-size: var(--font-size-0);
209
256
  line-height: var(--line-height-0);
210
257
  }
211
258
 
259
+ /* Active label gets accent color and medium weight */
260
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarMenuButton[data-active] .rt-SidebarMenuLabel) {
261
+ color: var(--accent-11);
262
+ font-weight: var(--font-weight-medium);
263
+ }
264
+
265
+ /* Highlighted label gets slightly darker color */
266
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarMenuButton[data-highlighted] .rt-SidebarMenuLabel) {
267
+ color: var(--accent-11);
268
+ }
269
+
270
+ /* Remove background from button itself in thin mode - styles are on icon wrapper */
271
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarMenuButton[data-highlighted]),
272
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarMenuButton[data-active]) {
273
+ background-color: transparent !important;
274
+ }
275
+
276
+ /* Soft variant icon wrapper states in thin mode */
277
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-menu-variant-soft .rt-SidebarMenuButton[data-highlighted] .rt-SidebarMenuIconWrapper) {
278
+ background-color: var(--accent-4);
279
+ }
280
+
281
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-menu-variant-soft .rt-SidebarMenuButton[data-active] .rt-SidebarMenuIconWrapper) {
282
+ background-color: var(--accent-3);
283
+ }
284
+
285
+ /* Translucent panel support for soft variant */
286
+ :where([data-panel-background='translucent']) :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-menu-variant-soft .rt-SidebarMenuButton[data-highlighted] .rt-SidebarMenuIconWrapper) {
287
+ background-color: var(--accent-a4);
288
+ }
289
+
290
+ :where([data-panel-background='translucent']) :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-menu-variant-soft .rt-SidebarMenuButton[data-active] .rt-SidebarMenuIconWrapper) {
291
+ background-color: var(--accent-a3);
292
+ }
293
+
294
+ /* Solid variant icon wrapper states in thin mode */
295
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-menu-variant-solid .rt-SidebarMenuButton[data-highlighted] .rt-SidebarMenuIconWrapper) {
296
+ background-color: var(--accent-9);
297
+ color: var(--accent-contrast);
298
+ }
299
+
300
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-menu-variant-solid .rt-SidebarMenuButton[data-active] .rt-SidebarMenuIconWrapper) {
301
+ background-color: var(--accent-9);
302
+ color: var(--accent-contrast);
303
+ }
304
+
305
+ /* High contrast solid variant in thin mode */
306
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-menu-variant-solid[data-high-contrast] .rt-SidebarMenuButton[data-highlighted] .rt-SidebarMenuIconWrapper) {
307
+ background-color: var(--accent-12);
308
+ color: var(--accent-1);
309
+ }
310
+
311
+ :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarContent.rt-menu-variant-solid[data-high-contrast] .rt-SidebarMenuButton[data-active] .rt-SidebarMenuIconWrapper) {
312
+ background-color: var(--accent-12);
313
+ color: var(--accent-1);
314
+ }
315
+
212
316
  :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarMenuBadge),
213
317
  :where(.rt-SidebarContainer[data-presentation='thin'] .rt-SidebarMenuShortcut),
214
318
  :where(.rt-SidebarContainer[data-presentation='thin'] .rt-Badge),
@@ -13,7 +13,7 @@ export {
13
13
  } from './_internal/base-menu.props.js';
14
14
 
15
15
  // Sidebar container props
16
- const sizes = ['1', '2'] as const;
16
+ const sizes = ['1', '2', '3'] as const;
17
17
  const variants = ['soft', 'outline', 'surface', 'ghost'] as const;
18
18
  const menuVariants = ['solid', 'soft'] as const;
19
19
  const types = ['sidebar'] as const;
@@ -32,7 +32,7 @@ type BadgeConfig = {
32
32
 
33
33
  // Internal presentational context (not exported) for size/menu variant
34
34
  type SidebarVisualContextValue = {
35
- size: '1' | '2';
35
+ size: '1' | '2' | '3';
36
36
  menuVariant: 'solid' | 'soft';
37
37
  presentation?: 'thin' | 'expanded';
38
38
  color?: string;
@@ -236,6 +236,7 @@ const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonP
236
236
  const [isHighlighted, setIsHighlighted] = React.useState(false);
237
237
  const visual = useSidebarVisual();
238
238
  const sidebarSize = visual?.size ?? '2';
239
+ const isThinMode = visual?.presentation === 'thin';
239
240
 
240
241
  const Comp = asChild ? Slot : 'button';
241
242
 
@@ -268,6 +269,30 @@ const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonP
268
269
  [onClick, onKeyDown],
269
270
  );
270
271
 
272
+ // Separate icons and labels for thin mode styling
273
+ const separateIconsAndLabels = (node: React.ReactNode): { icons: React.ReactNode[]; labels: React.ReactNode[] } => {
274
+ const icons: React.ReactNode[] = [];
275
+ const labels: React.ReactNode[] = [];
276
+
277
+ React.Children.forEach(node, (child) => {
278
+ if (typeof child === 'string' || typeof child === 'number') {
279
+ labels.push(<span key={labels.length} className="rt-SidebarMenuLabel">{child}</span>);
280
+ } else if (React.isValidElement(child)) {
281
+ const el = child as React.ReactElement<any>;
282
+ // Check if it's an SVG or icon component
283
+ if (el.type === 'svg' || (el.props && el.props.icon) || (typeof el.type === 'function' && ((el.type as any).displayName?.includes('Icon') || (el.type as any).name?.includes('Icon')))) {
284
+ icons.push(child);
285
+ } else {
286
+ labels.push(child);
287
+ }
288
+ } else if (child) {
289
+ labels.push(child);
290
+ }
291
+ });
292
+
293
+ return { icons, labels };
294
+ };
295
+
271
296
  // Wrap bare text nodes so CSS can target labels (e.g., for truncation in thin mode)
272
297
  const wrapTextNodes = (node: React.ReactNode): React.ReactNode => {
273
298
  if (typeof node === 'string' || typeof node === 'number') {
@@ -289,6 +314,9 @@ const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonP
289
314
 
290
315
  const processedChildren = wrapTextNodes(children);
291
316
 
317
+ // For thin mode, separate icons into a wrapper for targeted background styling
318
+ const { icons, labels } = isThinMode ? separateIconsAndLabels(children) : { icons: [], labels: [] };
319
+
292
320
  // When rendering asChild, Slot expects a single child element. We still want to
293
321
  // append optional badge/shortcut inside that element so they render with Link.
294
322
  const slottedChildren = React.useMemo(() => {
@@ -347,6 +375,19 @@ const SidebarMenuButton = React.forwardRef<HTMLButtonElement, SidebarMenuButtonP
347
375
  >
348
376
  {asChild ? (
349
377
  slottedChildren
378
+ ) : isThinMode && icons.length > 0 ? (
379
+ <>
380
+ <span className="rt-SidebarMenuIconWrapper">{icons}</span>
381
+ {labels.map((label, index) => (
382
+ <React.Fragment key={index}>
383
+ {typeof label === 'string' || typeof label === 'number' ? (
384
+ <span className="rt-SidebarMenuLabel">{label}</span>
385
+ ) : (
386
+ label
387
+ )}
388
+ </React.Fragment>
389
+ ))}
390
+ </>
350
391
  ) : (
351
392
  <>
352
393
  {processedChildren}
@@ -522,8 +563,10 @@ const SidebarMenuSubContent = React.forwardRef<React.ElementRef<typeof Accordion
522
563
  );
523
564
  });
524
565
 
566
+ // DropdownMenu only supports sizes 1 and 2, so map size 3 to 2
567
+ const dropdownSize = visual?.size === '3' ? '2' : visual?.size;
525
568
  return (
526
- <DropdownMenu.Content size={visual?.size} variant={visual?.menuVariant} className={classNames(className)}>
569
+ <DropdownMenu.Content size={dropdownSize} variant={visual?.menuVariant} className={classNames(className)}>
527
570
  <DropdownMenu.Group>{normalized}</DropdownMenu.Group>
528
571
  </DropdownMenu.Content>
529
572
  );
@@ -299,7 +299,6 @@
299
299
  &:where(:has(.rt-TextAreaInput:where(:disabled, :read-only))) {
300
300
  /* Blend with grey */
301
301
  background-image: linear-gradient(var(--gray-a2), var(--gray-a2));
302
- box-shadow: var(--shadow-1);
303
302
  }
304
303
  }
305
304
 
@@ -476,7 +476,6 @@
476
476
  &:where(:has(.rt-TextFieldInput:where(:disabled, :read-only))) {
477
477
  /* Blend with grey */
478
478
  background-image: linear-gradient(var(--gray-a2), var(--gray-a2));
479
- box-shadow: var(--shadow-1);
480
479
  }
481
480
  }
482
481