@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.
- package/components.css +187 -133
- package/dist/cjs/components/_internal/shell-bottom.d.ts +2 -0
- package/dist/cjs/components/_internal/shell-bottom.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-bottom.js +1 -1
- package/dist/cjs/components/_internal/shell-bottom.js.map +3 -3
- package/dist/cjs/components/_internal/shell-inspector.d.ts +2 -0
- package/dist/cjs/components/_internal/shell-inspector.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-inspector.js +1 -1
- package/dist/cjs/components/_internal/shell-inspector.js.map +3 -3
- package/dist/cjs/components/_internal/shell-sidebar.d.ts +2 -0
- package/dist/cjs/components/_internal/shell-sidebar.d.ts.map +1 -1
- package/dist/cjs/components/_internal/shell-sidebar.js +1 -1
- package/dist/cjs/components/_internal/shell-sidebar.js.map +3 -3
- package/dist/cjs/components/shell.context.d.ts +13 -0
- package/dist/cjs/components/shell.context.d.ts.map +1 -1
- package/dist/cjs/components/shell.context.js +1 -1
- package/dist/cjs/components/shell.context.js.map +3 -3
- package/dist/cjs/components/shell.d.ts +14 -6
- 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/components/sidebar.d.ts.map +1 -1
- package/dist/cjs/components/sidebar.js +1 -1
- package/dist/cjs/components/sidebar.js.map +3 -3
- package/dist/cjs/components/sidebar.props.d.ts +1 -1
- package/dist/cjs/components/sidebar.props.js +1 -1
- package/dist/cjs/components/sidebar.props.js.map +2 -2
- package/dist/esm/components/_internal/shell-bottom.d.ts +2 -0
- package/dist/esm/components/_internal/shell-bottom.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-bottom.js +1 -1
- package/dist/esm/components/_internal/shell-bottom.js.map +3 -3
- package/dist/esm/components/_internal/shell-inspector.d.ts +2 -0
- package/dist/esm/components/_internal/shell-inspector.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-inspector.js +1 -1
- package/dist/esm/components/_internal/shell-inspector.js.map +3 -3
- package/dist/esm/components/_internal/shell-sidebar.d.ts +2 -0
- package/dist/esm/components/_internal/shell-sidebar.d.ts.map +1 -1
- package/dist/esm/components/_internal/shell-sidebar.js +1 -1
- package/dist/esm/components/_internal/shell-sidebar.js.map +3 -3
- package/dist/esm/components/shell.context.d.ts +13 -0
- package/dist/esm/components/shell.context.d.ts.map +1 -1
- package/dist/esm/components/shell.context.js +1 -1
- package/dist/esm/components/shell.context.js.map +3 -3
- package/dist/esm/components/shell.d.ts +14 -6
- package/dist/esm/components/shell.d.ts.map +1 -1
- package/dist/esm/components/shell.js +1 -1
- package/dist/esm/components/shell.js.map +3 -3
- package/dist/esm/components/sidebar.d.ts.map +1 -1
- package/dist/esm/components/sidebar.js +1 -1
- package/dist/esm/components/sidebar.js.map +3 -3
- package/dist/esm/components/sidebar.props.d.ts +1 -1
- package/dist/esm/components/sidebar.props.js +1 -1
- package/dist/esm/components/sidebar.props.js.map +2 -2
- 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/_internal/base-button.css +6 -32
- package/src/components/_internal/base-card.css +0 -3
- package/src/components/_internal/base-checkbox.css +0 -2
- package/src/components/_internal/base-radio.css +0 -2
- package/src/components/_internal/shell-bottom.tsx +15 -1
- package/src/components/_internal/shell-inspector.tsx +15 -1
- package/src/components/_internal/shell-sidebar.tsx +15 -1
- package/src/components/avatar.css +0 -1
- package/src/components/segmented-control.css +37 -37
- package/src/components/select.css +0 -2
- package/src/components/shell.context.tsx +14 -0
- package/src/components/shell.css +51 -28
- package/src/components/shell.tsx +150 -81
- package/src/components/sidebar.css +110 -6
- package/src/components/sidebar.props.tsx +1 -1
- package/src/components/sidebar.tsx +45 -2
- package/src/components/text-area.css +0 -1
- package/src/components/text-field.css +0 -1
- package/styles.css +187 -133
package/src/components/shell.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
-
{
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
|
616
|
-
type RailUncontrolledProps = { defaultOpen?: boolean
|
|
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
|
|
838
|
-
type PanelUncontrolledProps = { defaultOpen?: boolean
|
|
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
|
-
//
|
|
967
|
-
React.
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
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
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
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:
|
|
204
|
-
text-overflow:
|
|
250
|
+
overflow: visible;
|
|
251
|
+
text-overflow: clip;
|
|
205
252
|
white-space: nowrap;
|
|
206
253
|
text-align: center;
|
|
207
|
-
color: var(--
|
|
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={
|
|
569
|
+
<DropdownMenu.Content size={dropdownSize} variant={visual?.menuVariant} className={classNames(className)}>
|
|
527
570
|
<DropdownMenu.Group>{normalized}</DropdownMenu.Group>
|
|
528
571
|
</DropdownMenu.Content>
|
|
529
572
|
);
|