mui-treasury
itemlayout-coresource: ok · refreshed
Layout Components
Core layout primitives (Root, Header, Content, Footer, EdgeSidebar, InsetSidebar) for building app shells with CSS Grid.
npx shadcn add "https://mui-treasury.com/r/layout-core.json"free
Where it goes
navbarfootersidebardashboard-shell
Use cases
dashboard
Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.
Dependencies
Bundle weight · light@mui/material@emotion/react@emotion/styled
Files20 · source cached 10/8/2026, 1:57:58 AM
layout/layout-core/Content.tsx
'use client';
import React from 'react';
import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';
import { layoutClasses } from './layoutClasses';
const StyledContent = styled('main', {
name: 'LayoutContent',
slot: 'root',
})(
memoTheme(() => ({
'--_overflow': 'var(--content-overflow)',
overflow: 'var(--_overflow)' as never,
gridArea: layoutClasses.Content,
minHeight: 0,
})),
);
const Content = React.forwardRef<
HTMLElement,
React.ComponentProps<typeof StyledContent>
>(function Content({ className, ...props }, ref) {
return (
<StyledContent
ref={ref}
className={`${layoutClasses.Content} ${className || ''}`}
{...props}
/>
);
});
export default Content;
layout/layout-core/EdgeDrawerClose.tsx
'use client';
import React from 'react';
import { styled } from '@mui/material/styles';
import { layoutAttrs } from './layoutAttrs';
import { layoutClasses } from './layoutClasses';
const StyledEdgeDrawerClose = styled('button')({
display: 'var(--_drawer, flex) var(--_permanent, none)',
visibility: 'hidden',
opacity: 0,
transition: 'var(--tsn, 0.3s)',
position: 'var(--drawer-pos)' as never,
top: 'calc(0.875rem + var(--jun-H-clip-h))',
right: 'var(--anchorLeft, 0.875rem)',
left: 'var(--anchorRight, 0.875rem)',
zIndex: 2,
width: 40,
height: 40,
color: 'white',
cursor: 'pointer',
backgroundColor: '#999',
borderRadius: '40px',
border: 'none',
alignItems: 'center',
justifyContent: 'center',
'& svg': {
width: '1.5em',
height: '1.5em',
},
[`[${layoutAttrs.isDrawerOpen}] &`]: {
visibility: 'visible',
opacity: 1,
},
});
const EdgeDrawerClose = React.forwardRef<
HTMLButtonElement,
React.ComponentProps<typeof StyledEdgeDrawerClose>
>(function EdgeDrawerClose({ className, children, ...props }, ref) {
return (
<StyledEdgeDrawerClose
ref={ref}
className={`${layoutClasses.EdgeDrawerClose} ${className || ''}`}
{...props}
>
{children || (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
)}
</StyledEdgeDrawerClose>
);
});
export default EdgeDrawerClose;
layout/layout-core/EdgeDrawerTrigger.tsx
'use client';
import React from 'react';
import { triggerEdgeDrawer } from './EdgeSidebar';
import { layoutClasses } from './layoutClasses';
interface EdgeDrawerTriggerProps {
render: React.ReactElement<{
className?: string;
onClick?: React.MouseEventHandler;
}>;
sidebarId?: string;
}
function EdgeDrawerTrigger({ render, sidebarId }: EdgeDrawerTriggerProps) {
return React.cloneElement(render, {
className: layoutClasses.EdgeDrawerTrigger,
onClick: () => triggerEdgeDrawer({ sidebarId }),
});
}
export default EdgeDrawerTrigger;
layout/layout-core/EdgeDrawerTriggerRight.tsx
'use client';
import React from 'react';
import { triggerEdgeDrawerRight } from './EdgeSidebarRight';
import { layoutClasses } from './layoutClasses';
interface EdgeDrawerTriggerRightProps {
render: React.ReactElement<{
className?: string;
onClick?: React.MouseEventHandler;
}>;
sidebarId?: string;
}
function EdgeDrawerTriggerRight({
render,
sidebarId,
}: EdgeDrawerTriggerRightProps) {
return React.cloneElement(render, {
className: layoutClasses.EdgeDrawerTriggerRight,
onClick: () => triggerEdgeDrawerRight({ sidebarId }),
});
}
export default EdgeDrawerTriggerRight;
layout/layout-core/EdgeSidebar.tsx
'use client';
import React, { useMemo } from 'react';
import { Breakpoint } from '@mui/material/styles';
import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';
import type {
DrawerConfig,
EdgeSidebarVariant,
EdgeSidebarVariantInput,
PermanentConfig,
} from './SharedEdgeSidebar';
import {
EdgeSidebarRoot,
internalCollapseSidebar,
internalToggleSidebar,
} from './SharedEdgeSidebar';
import { layoutAttrs } from './layoutAttrs';
import { layoutClasses } from './layoutClasses';
function applyDrawerStyles(params: DrawerConfig) {
const { width = '300px', showHeader, withoutOverlay } = params || {};
return {
'--jun-ES-drawerWidth': '0px',
...(showHeader && {
'--drawer-h': 'calc(var(--jun-h) - var(--jun-H-h))',
'&::before': {
top: 'var(--jun-H-h)',
},
}),
...(withoutOverlay && {
'--drawer-h': 'calc(var(--jun-h) - var(--jun-H-clip-h))',
'&::before': {
display: 'none',
},
}),
[`.${layoutClasses.Root}:has(&)`]: {
'--jun-ES-variant': 'var(--drawer)',
[`.${layoutClasses.EdgeSidebarCollapser}`]: {
display: 'none',
},
},
[`.${layoutClasses.Root}:has(&[${layoutAttrs.isDrawerOpen}])`]: {
[`.${layoutClasses.EdgeDrawerTrigger} .${layoutClasses.EdgeDrawerClosedVisible}`]:
{
display: 'none',
},
},
[`.${layoutClasses.Root}:has(&:not([${layoutAttrs.isDrawerOpen}]))`]: {
[`.${layoutClasses.EdgeDrawerTrigger} .${layoutClasses.EdgeDrawerOpenVisible}`]:
{
display: 'none',
},
},
[`&[${layoutAttrs.isDrawerOpen}], &[${layoutAttrs.isDrawerClosing}]`]: {
'--jun-ES-drawerWidth': width,
},
};
}
function applyPermanentStyles(params: PermanentConfig) {
const { width, collapsedWidth, visibility } = params || {};
const defaultExpandConfig = {
delay: '0s',
shadow: '0 0 10px rgba(0,0,0,0.1)',
};
let expandConfig: undefined | typeof defaultExpandConfig;
if ('hoverUncollapse' in params) {
if (params.hoverUncollapse === true) {
expandConfig = defaultExpandConfig;
} else {
expandConfig = params.hoverUncollapse as typeof defaultExpandConfig;
}
}
return {
'--jun-EC-shadow':
'inset calc(-1 * var(--jun-ES-line-w)) 0px var(--jun-ES-line-color)',
'--jun-EC-width': 'var(--_permanentWidth, 0px)',
'--_collapsed': 'var(--collapsed)',
'--_uncollapsed': 'var(--uncollapsed)',
[`.${layoutClasses.Root}:has(&)`]: {
'--jun-ES-variant': 'var(--permanent)',
...(width && {
'--jun-ES-permanentWidth': width,
}),
...(collapsedWidth && {
'--jun-ES-collapsedWidth': collapsedWidth,
[`.${layoutClasses.EdgeSidebarCollapser}`]: {
display: 'var(--display, inline-flex)',
'--_sidebarCollapsed': 'var(--collapsed, 1)',
},
[`.${layoutClasses.EdgeCollapsedVisible}`]: {
display: 'var(--collapsed, inline-flex) var(--uncollapsed, none)',
},
[`.${layoutClasses.EdgeUncollapsedVisible}`]: {
display: 'var(--collapsed, none) var(--uncollapsed, inline-flex)',
},
}),
[`.${layoutClasses.EdgeDrawerTrigger}`]: {
display: 'none',
},
},
...(collapsedWidth && {
/** Collapsible feature */
[`.${layoutClasses.Root}:has(&[${layoutAttrs.isEdgeSidebarCollapsed}])`]:
{
'--jun-ES-collapsible': 'var(--collapsed)',
},
[`.${layoutClasses.Root}:has(&[${layoutAttrs.isEdgeSidebarUncollapsed}])`]:
{
'--jun-ES-collapsible': 'var(--uncollapsed)',
},
}),
...(visibility === 'hidden' && {
[`.${layoutClasses.Root}:has(>&)`]: {
'--_permanentWidth': '0px',
},
[`& .${layoutClasses.EdgeSidebarContent}`]: {
visibility: 'hidden',
},
}),
...(visibility === 'visible' && {
[`.${layoutClasses.Root}:has(>&)`]: {
'--_permanentWidth': `var(--uncollapsed, var(--jun-ES-permanentWidth))
var(--collapsed, var(--jun-ES-collapsedWidth, 0px))`,
},
[`& .${layoutClasses.EdgeSidebarContent}`]: {
visibility:
'var(--_drawer, hidden) var(--_permanent, visible)' as never,
},
}),
...(expandConfig && {
[`&:has(.${layoutClasses.EdgeSidebarContent}:hover)`]: {
'--_collapsed': 'var(--__,)', // workaround to use fallback, Emotion strips out spaces if the value is " ".
'--_uncollapsed': 'var(--_)',
[`& .${layoutClasses.SidebarRail}`]: {
display: 'none',
},
},
[`& .${layoutClasses.EdgeSidebarContent}:hover`]: {
'--jun-EC-width': 'var(--jun-ES-permanentWidth)',
'--jun-EC-delay': expandConfig.delay,
'--jun-EC-shadow': `var(--_permanent, var(--uncollapsed, inset calc(-1 * var(--jun-ES-line-w)) 0 var(--jun-ES-line-color)) var(--collapsed, ${expandConfig.shadow}, inset calc(-1 * var(--jun-ES-line-w)) 0 var(--jun-ES-line-color)))`,
},
}),
};
}
export function triggerEdgeCollapse(options: {
event: React.MouseEvent;
sidebarId?: string;
state?: boolean;
document?: Document | null;
}) {
const { sidebarId } = options || {};
let selector = `.${layoutClasses.EdgeSidebar}`;
if (sidebarId) {
selector = `#${sidebarId}`;
}
internalCollapseSidebar({ ...options, selector });
}
export function triggerEdgeDrawer(options?: {
sidebarId?: string;
state?: boolean;
document?: Document | null;
}) {
const { sidebarId } = options || {};
let selector = `.${layoutClasses.EdgeSidebar}`;
if (sidebarId) {
selector = `#${sidebarId}`;
}
internalToggleSidebar({ ...options, selector });
}
interface EdgeSidebarOwnerState {
variant?: Partial<Record<Breakpoint, EdgeSidebarVariant>>;
permanentAutoCollapse?: Breakpoint;
}
interface EdgeSidebarProps {
variant?: EdgeSidebarVariantInput;
permanentAutoCollapse?: Breakpoint;
}
const StyledEdgeSidebar = styled(EdgeSidebarRoot, {
name: 'LayoutEdgeSidebar',
slot: 'root',
})<{
ownerState: EdgeSidebarOwnerState;
}>(
memoTheme(({ theme }) => ({
[`.${layoutClasses.Root}:has(&)`]: {
/** Root default settings */
'--jun-ES-variant': 'var(--permanent)',
'--jun-ES-permanentWidth': '256px',
'--jun-ES-collapsible': 'var(--uncollapsed)',
'--jun-ES-collapsedWidth': '0px',
/** DO NOT OVERRIDE, internal variables */
'--drawer': 'var(--jun-ES-variant,)',
'--permanent': 'var(--jun-ES-variant,)',
'--_permanentWidth': `var(--uncollapsed, var(--jun-ES-permanentWidth))
var(--collapsed, var(--jun-ES-collapsedWidth, 0px))`,
'--collapsed': 'var(--jun-ES-collapsible,)',
'--uncollapsed': 'var(--jun-ES-collapsible,)',
},
/** EdgeSidebar default settings */
'--jun-ES-anchor': 'var(--anchorLeft)',
'--jun-EC-width': 'var(--_permanentWidth, 0px)',
'--_drawer': 'var(--drawer)',
'--_permanent': 'var(--permanent)',
'--_collapsed': 'var(--collapsed)',
'--_uncollapsed': 'var(--uncollapsed)',
gridArea: layoutClasses.EdgeSidebar,
width: `var(--drawer, 0)
var(--permanent, var(--_permanentWidth))`,
borderColor: 'var(--jun-ES-line-color)',
[`&:not([${layoutAttrs.isDrawerOpen}], [${layoutAttrs.isDrawerClosing}])`]:
{
overflow: 'var(--drawer, hidden)',
},
variants: [
{
props: ({ variant }: EdgeSidebarOwnerState) => !!variant,
style: ({ variant }: Required<EdgeSidebarOwnerState>) => {
const responsive: Record<string, unknown> = {};
(Object.keys(variant) as Array<Breakpoint>)
.sort(
(a, b) =>
theme.breakpoints.values[a] - theme.breakpoints.values[b],
)
.forEach((breakpoint) => {
const variantTuple = variant[breakpoint];
if (variantTuple) {
const [variantName, params = {}] = variantTuple;
const variantStyles = {
drawer: applyDrawerStyles,
permanent: applyPermanentStyles,
}[variantName](params as never);
if (breakpoint === 'xs') {
Object.assign(responsive, variantStyles);
} else {
responsive[theme.breakpoints.up(breakpoint)] = variantStyles;
}
}
});
return responsive;
},
},
{
props: ({ permanentAutoCollapse }: EdgeSidebarOwnerState) =>
!!permanentAutoCollapse,
style: ({
permanentAutoCollapse,
}: Required<EdgeSidebarOwnerState>) => ({
[`.${layoutClasses.Root}:has(&) .${layoutClasses.EdgeSidebarCollapser}`]:
{
'--_autoCollapse': '1',
},
[theme.breakpoints.down(permanentAutoCollapse)]: {
[`.${layoutClasses.Root}:has(&)`]: {
'--jun-ES-collapsible': 'var(--collapsed)',
},
},
[theme.breakpoints.up(permanentAutoCollapse)]: {
[`.${layoutClasses.Root}:has(&)`]: {
'--jun-ES-collapsible': 'var(--uncollapsed)',
},
[`.${layoutClasses.Root}:has(&) .${layoutClasses.EdgeSidebarCollapser}`]:
{
'--_in-autoCollapse': '1',
},
},
}),
},
],
})),
);
const defaultVariant: EdgeSidebarVariant = ['permanent', { width: '256px' }];
const EdgeSidebar = React.forwardRef<
HTMLDivElement,
Omit<React.ComponentPropsWithoutRef<typeof StyledEdgeSidebar>, 'ownerState'> &
EdgeSidebarProps
>(function EdgeSidebar(
{ className, variant = defaultVariant, permanentAutoCollapse, ...props },
ref,
) {
const normalizedVariant = useMemo(
() => (Array.isArray(variant) ? { xs: variant } : variant),
[variant],
);
const ownerState = useMemo(
() => ({
variant: normalizedVariant,
permanentAutoCollapse,
}),
[normalizedVariant, permanentAutoCollapse],
);
const hasWithoutOverlay = Object.values(normalizedVariant).some(
(v) => v?.[0] === 'drawer' && (v[1] as DrawerConfig)?.withoutOverlay,
);
return (
<StyledEdgeSidebar
ref={ref}
className={`${layoutClasses.EdgeSidebar}${hasWithoutOverlay ? ` ${layoutClasses.DrawerWithoutOverlay}` : ''} ${className || ''}`}
ownerState={ownerState}
{...(hasWithoutOverlay && { 'data-without-overlay': '' })}
{...props}
/>
);
});
export default EdgeSidebar;
layout/layout-core/EdgeSidebarCollapser.tsx
'use client';
import React from 'react';
import { triggerEdgeCollapse } from './EdgeSidebar';
import { triggerEdgeCollapseRight } from './EdgeSidebarRight';
import { layoutClasses } from './layoutClasses';
interface EdgeSidebarCollapserProps {
render: React.ReactElement<{
className?: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
}>;
state?: boolean;
}
function EdgeSidebarCollapser({ render, state }: EdgeSidebarCollapserProps) {
return React.cloneElement(render, {
className: layoutClasses.EdgeSidebarCollapser,
onClick: (event: React.MouseEvent<HTMLButtonElement>) => {
const sidebar = event.currentTarget.closest<HTMLElement>(
`.${layoutClasses.EdgeSidebar}, .${layoutClasses.EdgeSidebarRight}`,
);
const opts = {
event,
state,
...(sidebar?.id && { sidebarId: sidebar.id }),
};
if (sidebar?.classList.contains(layoutClasses.EdgeSidebarRight)) {
triggerEdgeCollapseRight(opts);
} else {
triggerEdgeCollapse(opts);
}
},
});
}
export default EdgeSidebarCollapser;
layout/layout-core/EdgeSidebarContent.tsx
'use client';
import React from 'react';
import { styled } from '@mui/material/styles';
import { layoutAttrs } from './layoutAttrs';
import { layoutClasses } from './layoutClasses';
const StyledEdgeSidebarContent = styled('div', {
name: 'LayoutEdgeSidebarContent',
slot: 'root',
})({
/* configurable */
background: 'var(--EdgeSidebarContent-background)',
boxShadow: 'var(--jun-EC-shadow)',
display: 'flex',
flexDirection: 'column',
flex: '1',
padding: '0px',
overflowX: 'auto', // prevent horizontal content overflow
'--jun-EC-delay': '0s',
opacity: `var(--_drawer, var(--jun-ES-drawerOpen))
var(--_permanent, 1)`,
visibility: `var(--_drawer, hidden) var(--_permanent, visible)` as never,
margin: '0px', // prevent user from customizing it
position:
'var(--_drawer, var(--drawer-pos)) var(--_permanent, relative)' as never,
zIndex: '2',
width:
'var(--_drawer, var(--jun-ES-drawerWidth)) var(--_permanent, var(--jun-EC-width))',
height: 'var(--_drawer, var(--drawer-h))',
top: 'var(--_drawer, calc(var(--jun-h) - var(--drawer-h)))',
overflowY: 'var(--_drawer, auto)' as never,
transition: `var(--tsn, var(--_drawer, opacity 0.3s, transform 0.3s)
var(--_permanent, opacity 0.4s, width 0.3s var(--jun-EC-delay, 0s), transform 0.3s var(--jun-EC-delay, 0s), box-shadow 0.3s var(--jun-EC-delay, 0s)))`,
transform: `var(--_drawer, var(--anchorLeft, translateX(calc((1 - var(--jun-ES-drawerOpen)) * -100%))) var(--anchorRight, translateX(calc(var(--jun-ES-drawerOpen) * -100%))))
var(--_permanent, translateX(var(--jun-ES-permanentSlide, 0)))`,
[`[${layoutAttrs.isEdgeSidebarContentHidden}] &`]: {
visibility: 'hidden',
opacity: '0',
},
[`[${layoutAttrs.isDrawerOpen}] &, [${layoutAttrs.isDrawerClosing}] &`]: {
visibility: 'visible',
},
[`[${layoutAttrs.isDrawerClosing}] &`]: {
transition: 'var(--tsn, transform 0.3s, visibility 0.3s, opacity 0.3s)',
},
});
const EdgeSidebarContent = React.forwardRef<
HTMLDivElement,
React.ComponentPropsWithoutRef<typeof StyledEdgeSidebarContent>
>(function EdgeSidebarContent({ className, ...props }, ref) {
return (
<StyledEdgeSidebarContent
ref={ref}
className={`${layoutClasses.EdgeSidebarContent} ${className || ''}`}
{...props}
/>
);
});
export default EdgeSidebarContent;
layout/layout-core/EdgeSidebarRight.tsx
'use client';
import React, { useMemo } from 'react';
import { Breakpoint } from '@mui/material/styles';
import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';
import type {
DrawerConfig,
EdgeSidebarVariant,
EdgeSidebarVariantInput,
PermanentConfig,
} from './SharedEdgeSidebar';
import {
EdgeSidebarRoot,
internalCollapseSidebar,
internalToggleSidebar,
} from './SharedEdgeSidebar';
import { layoutAttrs } from './layoutAttrs';
import { layoutClasses } from './layoutClasses';
function applyDrawerRightStyles(params: DrawerConfig) {
const { width = '300px', showHeader, withoutOverlay } = params || {};
return {
'--jun-ES-drawerWidth': '0px',
...(showHeader && {
'--drawer-h': 'calc(var(--jun-h) - var(--jun-H-h))',
'&::before': {
top: 'var(--jun-H-h)',
},
}),
...(withoutOverlay && {
'--drawer-h': 'calc(var(--jun-h) - var(--jun-H-clip-h))',
'&::before': {
display: 'none',
},
}),
[`.${layoutClasses.Root}:has(&)`]: {
'--jun-ESR-variant': 'var(--drawer-R)',
[`.${layoutClasses.EdgeSidebarRightCollapser}`]: {
display: 'none',
},
},
[`.${layoutClasses.Root}:has(&[${layoutAttrs.isDrawerOpen}])`]: {
[`.${layoutClasses.EdgeDrawerTriggerRight} .${layoutClasses.EdgeDrawerClosedVisible}`]:
{
display: 'none',
},
},
[`.${layoutClasses.Root}:has(&:not([${layoutAttrs.isDrawerOpen}]))`]: {
[`.${layoutClasses.EdgeDrawerTriggerRight} .${layoutClasses.EdgeDrawerOpenVisible}`]:
{
display: 'none',
},
},
[`&[${layoutAttrs.isDrawerOpen}], &[${layoutAttrs.isDrawerClosing}]`]: {
'--jun-ES-drawerWidth': width,
},
};
}
function applyPermanentRightStyles(params: PermanentConfig) {
const { width, collapsedWidth, visibility } = params;
const defaultExpandConfig = {
delay: '0.3s',
shadow: '0 0 10px rgba(0,0,0,0.1)',
};
let expandConfig: undefined | typeof defaultExpandConfig;
if ('hoverUncollapse' in params) {
if (params.hoverUncollapse === true) {
expandConfig = defaultExpandConfig;
} else {
expandConfig = params.hoverUncollapse as typeof defaultExpandConfig;
}
}
return {
'--jun-EC-shadow':
'inset var(--jun-ES-line-w) 0px var(--jun-ES-line-color)',
'--jun-EC-width': 'var(--_permanentWidth-R, 0px)',
'--_collapsed': 'var(--collapsed-R)',
'--_uncollapsed': 'var(--uncollapsed-R)',
[`.${layoutClasses.Root}:has(&)`]: {
'--jun-ESR-variant': 'var(--permanent-R)',
...(width && {
'--jun-ESR-permanentWidth': width,
}),
...(collapsedWidth && {
'--jun-ESR-collapsedWidth': collapsedWidth,
[`.${layoutClasses.EdgeSidebarRightCollapser}`]: {
display: 'var(--display, inline-flex)',
'--_sidebarCollapsed': 'var(--collapsed-R, 1)',
},
[`.${layoutClasses.EdgeCollapsedVisible}`]: {
display: 'var(--collapsed-R, inline-flex) var(--uncollapsed-R, none)',
},
[`.${layoutClasses.EdgeUncollapsedVisible}`]: {
display: 'var(--collapsed-R, none) var(--uncollapsed-R, inline-flex)',
},
}),
[`.${layoutClasses.EdgeDrawerTriggerRight}`]: {
display: 'none',
},
},
...(collapsedWidth && {
/** Collapsible feature */
[`.${layoutClasses.Root}:has(&[${layoutAttrs.isEdgeSidebarCollapsed}])`]:
{
'--jun-ESR-collapsible': 'var(--collapsed-R)',
},
[`.${layoutClasses.Root}:has(&[${layoutAttrs.isEdgeSidebarUncollapsed}])`]:
{
'--jun-ESR-collapsible': 'var(--uncollapsed-R)',
},
}),
...(visibility === 'hidden' && {
[`.${layoutClasses.Root}:has(>&)`]: {
'--_permanentWidth-R': '0px',
},
[`& .${layoutClasses.EdgeSidebarContent}`]: {
visibility: 'hidden',
},
}),
...(visibility === 'visible' && {
[`.${layoutClasses.Root}:has(>&)`]: {
'--_permanentWidth-R': `var(--uncollapsed-R, var(--jun-ESR-permanentWidth))
var(--collapsed-R, var(--jun-ESR-collapsedWidth, 0px))`,
},
[`& .${layoutClasses.EdgeSidebarContent}`]: {
visibility:
'var(--_drawer, hidden) var(--_permanent, visible)' as never,
},
}),
...(expandConfig && {
[`&:has(.${layoutClasses.EdgeSidebarContent}:hover)`]: {
'--_collapsed': 'var(--__,)', // workaround to use fallback, Emotion strips out spaces if the value is " ".
'--_uncollapsed': 'var(--_)',
[`& .${layoutClasses.SidebarRail}`]: {
display: 'none',
},
},
[`& .${layoutClasses.EdgeSidebarContent}:hover`]: {
'--jun-EC-width': 'var(--jun-ESR-permanentWidth)',
'--jun-EC-delay': expandConfig.delay,
boxShadow: `var(--collapsed-R, ${expandConfig.shadow}) var(--uncollapsed-R, none)`,
'--jun-ES-permanentSlide':
'var(--collapsed-R, calc(var(--jun-ESR-collapsedWidth) - var(--jun-EC-width))) var(--uncollapsed-R, 0)',
},
}),
};
}
export function triggerEdgeCollapseRight(options: {
event: React.MouseEvent;
sidebarId?: string;
state?: boolean;
document?: Document | null;
}) {
const { sidebarId } = options || {};
let selector = `.${layoutClasses.EdgeSidebarRight}`;
if (sidebarId) {
selector = `#${sidebarId}`;
}
internalCollapseSidebar({ ...options, selector });
}
export function triggerEdgeDrawerRight(options?: {
sidebarId?: string;
state?: boolean;
document?: Document | null;
}) {
const { sidebarId } = options || {};
let selector = `.${layoutClasses.EdgeSidebarRight}`;
if (sidebarId) {
selector = `#${sidebarId}`;
}
internalToggleSidebar({ ...options, selector });
}
interface EdgeSidebarRightOwnerState {
variant?: Partial<Record<Breakpoint, EdgeSidebarVariant>>;
permanentAutoCollapse?: Breakpoint;
}
interface EdgeSidebarRightProps {
variant?: EdgeSidebarVariantInput;
permanentAutoCollapse?: Breakpoint;
}
const StyledEdgeSidebarRight = styled(EdgeSidebarRoot, {
name: 'LayoutEdgeSidebarRight',
slot: 'root',
})<{
ownerState: EdgeSidebarRightOwnerState;
}>(
memoTheme(({ theme }) => ({
[`.${layoutClasses.Root}:has(&)`]: {
/** Root default settings */
'--jun-ESR-variant': 'var(--permanent-R)',
'--jun-ESR-permanentWidth': '256px',
'--jun-ESR-collapsible': 'var(--uncollapsed-R)',
'--jun-ESR-collapsedWidth': '0px',
/** DO NOT OVERRIDE, internal variables */
'--drawer-R': 'var(--jun-ESR-variant,)',
'--permanent-R': 'var(--jun-ESR-variant,)',
'--_permanentWidth-R': `var(--uncollapsed-R, var(--jun-ESR-permanentWidth))
var(--collapsed-R, var(--jun-ESR-collapsedWidth, 0px))`,
'--collapsed-R': 'var(--jun-ESR-collapsible,)',
'--uncollapsed-R': 'var(--jun-ESR-collapsible,)',
},
/** EdgeSidebar default settings */
'--jun-ES-anchor': 'var(--anchorRight)',
'--jun-EC-width': 'var(--_permanentWidth-R, 0px)',
'--_drawer': 'var(--drawer-R)',
'--_permanent': 'var(--permanent-R)',
'--_collapsed': 'var(--collapsed-R)',
'--_uncollapsed': 'var(--uncollapsed-R)',
gridArea: layoutClasses.EdgeSidebarRight,
width: `var(--drawer-R, 0)
var(--permanent-R, var(--_permanentWidth-R))`,
[`&:not([${layoutAttrs.isDrawerOpen}], [${layoutAttrs.isDrawerClosing}])`]:
{
overflow: 'var(--drawer-R, hidden)',
},
variants: [
{
props: ({ variant }: EdgeSidebarRightOwnerState) => !!variant,
style: ({ variant }: Required<EdgeSidebarRightOwnerState>) => {
const responsive: Record<string, unknown> = {};
(Object.keys(variant) as Array<Breakpoint>)
.sort(
(a, b) =>
theme.breakpoints.values[a] - theme.breakpoints.values[b],
)
.forEach((breakpoint) => {
const variantTuple = variant[breakpoint];
if (variantTuple) {
const [variantName, params = {}] = variantTuple;
const variantStyles = {
drawer: applyDrawerRightStyles,
permanent: applyPermanentRightStyles,
}[variantName](params as never);
if (breakpoint === 'xs') {
Object.assign(responsive, variantStyles);
} else {
responsive[theme.breakpoints.up(breakpoint)] = variantStyles;
}
}
});
return responsive;
},
},
{
props: ({ permanentAutoCollapse }: EdgeSidebarRightOwnerState) =>
!!permanentAutoCollapse,
style: ({
permanentAutoCollapse,
}: Required<EdgeSidebarRightOwnerState>) => ({
[`.${layoutClasses.Root}:has(&) .${layoutClasses.EdgeSidebarRightCollapser}`]:
{
'--_autoCollapse': '1',
},
[theme.breakpoints.down(permanentAutoCollapse)]: {
[`.${layoutClasses.Root}:has(&)`]: {
'--jun-ESR-collapsible': 'var(--collapsed-R)',
},
},
[theme.breakpoints.up(permanentAutoCollapse)]: {
[`.${layoutClasses.Root}:has(&)`]: {
'--jun-ESR-collapsible': 'var(--uncollapsed-R)',
},
[`.${layoutClasses.Root}:has(&) .${layoutClasses.EdgeSidebarRightCollapser}`]:
{
'--_in-autoCollapse': '1',
},
},
}),
},
],
})),
);
const EdgeSidebarRight = React.forwardRef<
HTMLDivElement,
Omit<
React.ComponentPropsWithoutRef<typeof StyledEdgeSidebarRight>,
'ownerState'
> &
EdgeSidebarRightProps
>(function EdgeSidebarRight(
{ className, variant, permanentAutoCollapse, ...props },
ref,
) {
const normalizedVariant = useMemo(
() => (Array.isArray(variant) ? { xs: variant } : variant),
[variant],
);
const ownerState = useMemo(
() => ({
variant: normalizedVariant,
permanentAutoCollapse,
}),
[normalizedVariant, permanentAutoCollapse],
);
const hasWithoutOverlay =
normalizedVariant &&
Object.values(normalizedVariant).some(
(v) => v?.[0] === 'drawer' && (v[1] as DrawerConfig)?.withoutOverlay,
);
return (
<StyledEdgeSidebarRight
ref={ref}
className={`${layoutClasses.EdgeSidebarRight} ${className || ''}`}
ownerState={ownerState}
{...(hasWithoutOverlay && { 'data-without-overlay': '' })}
{...props}
/>
);
});
export default EdgeSidebarRight;
layout/layout-core/Footer.tsx
'use client';
import React from 'react';
import { BoxProps } from '@mui/material/Box';
import { styled } from '@mui/material/styles';
import { layoutClasses } from './layoutClasses';
const StyledFooter = styled('footer')(({ theme }) => ({
gridArea: layoutClasses.Footer,
transition: 'all 225ms cubic-bezier(0.0, 0, 0.2, 1) 0ms, color 0s',
background: `var(--Footer-background, ${(theme.vars || theme).palette.background.paper})`,
borderTop: 'var(--Footer-overline)',
}));
const Footer = React.forwardRef<HTMLElement, BoxProps>(function Footer(
{ className, ...props },
ref,
) {
return (
<StyledFooter
ref={ref}
className={`${layoutClasses.Footer} ${className || ''}`}
{...props}
/>
);
});
export default Footer;
layout/layout-core/Header.tsx
'use client';
import React, { useMemo } from 'react';
import { Breakpoint } from '@mui/material/styles';
import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';
import { layoutClasses } from './layoutClasses';
type ClipSide = 'left' | 'right' | boolean;
interface HeaderProps {
height?: string | Partial<Record<Breakpoint, string>>;
clip?: ClipSide | Partial<Record<Breakpoint, ClipSide>>;
}
const CLIP_BOTH = `
"${layoutClasses.Header} ${layoutClasses.Header} ${layoutClasses.Header}"
"${layoutClasses.EdgeSidebar} ${layoutClasses.Content} ${layoutClasses.EdgeSidebarRight}"
"${layoutClasses.EdgeSidebar} ${layoutClasses.Footer} ${layoutClasses.EdgeSidebarRight}"
`;
const CLIP_LEFT = `
"${layoutClasses.Header} ${layoutClasses.Header} ${layoutClasses.EdgeSidebarRight}"
"${layoutClasses.EdgeSidebar} ${layoutClasses.Content} ${layoutClasses.EdgeSidebarRight}"
"${layoutClasses.EdgeSidebar} ${layoutClasses.Footer} ${layoutClasses.EdgeSidebarRight}"
`;
const CLIP_RIGHT = `
"${layoutClasses.EdgeSidebar} ${layoutClasses.Header} ${layoutClasses.Header}"
"${layoutClasses.EdgeSidebar} ${layoutClasses.Content} ${layoutClasses.EdgeSidebarRight}"
"${layoutClasses.EdgeSidebar} ${layoutClasses.Footer} ${layoutClasses.EdgeSidebarRight}"
`;
const CLIP_STYLES = {
left: {
gridTemplateAreas: CLIP_LEFT,
[`& .${layoutClasses.EdgeSidebar}`]: {
'--jun-H-clip-h': 'var(--jun-H-h)',
},
},
right: {
gridTemplateAreas: CLIP_RIGHT,
[`& .${layoutClasses.EdgeSidebarRight}`]: {
'--jun-H-clip-h': 'var(--jun-H-h)',
},
},
both: {
gridTemplateAreas: CLIP_BOTH,
'--jun-H-clip-h': 'var(--jun-H-h)',
},
} as const;
const StyledHeader = styled('header', {
name: 'LayoutHeader',
slot: 'root',
})<{
ownerState: HeaderProps;
}>(
memoTheme(({ theme }) => ({
gridArea: layoutClasses.Header,
alignContent: 'center',
display: 'flex',
alignItems: 'center',
zIndex: '1', // to stay on top of the Content, e.g. InsetSidebar
top: '0', // for position sticky to work
position: 'sticky',
background: 'var(--Header-background)',
borderBottom: 'var(--Header-underline)',
height: 'var(--jun-H-h)',
variants: [
{
props: ({ height }: HeaderProps) => !!height,
style: ({ height }: Required<HeaderProps>) =>
theme.unstable_sx({
[`.${layoutClasses.Root}:has(&)`]: {
'--jun-H-h': height,
},
}),
},
{
props: ({ clip }: HeaderProps) => !!clip,
style: ({ clip }: Required<HeaderProps>) => {
const normalized =
typeof clip === 'string' || typeof clip === 'boolean'
? { xs: clip }
: clip;
const rootStyles: Record<string, unknown> = {};
(Object.keys(normalized) as Breakpoint[])
.sort(
(a, b) =>
theme.breakpoints.values[a] - theme.breakpoints.values[b],
)
.forEach((bp) => {
const side = normalized[bp];
if (side) {
if (bp === 'xs') {
Object.assign(
rootStyles,
CLIP_STYLES[side === true ? 'both' : side],
);
} else {
rootStyles[theme.breakpoints.up(bp)] =
CLIP_STYLES[side === true ? 'both' : side];
}
}
});
return {
zIndex: 3,
[`.${layoutClasses.Root}:has(&)`]: rootStyles,
};
},
},
],
})),
);
const Header = React.forwardRef<
HTMLElement,
Omit<React.ComponentPropsWithoutRef<typeof StyledHeader>, 'ownerState'> &
HeaderProps
>(function Header({ className, height, clip, ...props }, ref) {
const ownerState = useMemo(
() => ({
height,
clip,
}),
[height, clip],
);
return (
<StyledHeader
ref={ref}
className={`${layoutClasses.Header} ${className || ''}`}
ownerState={ownerState}
{...props}
/>
);
});
export default Header;
layout/layout-core/InsetAvoidingView.tsx
'use client';
import React from 'react';
import { BoxProps } from '@mui/material/Box';
import { styled } from '@mui/material/styles';
import { layoutClasses } from './layoutClasses';
const StyledInsetAvoidingView = styled('div')({
marginRight: 'var(--jun-ISR-w)',
marginLeft: 'var(--jun-ISL-w)',
});
const InsetAvoidingView = React.forwardRef<HTMLDivElement, BoxProps>(
function InsetAvoidingView({ className, ...props }, ref) {
return (
<StyledInsetAvoidingView
ref={ref}
className={`${layoutClasses.InsetAvoidingView} ${className || ''}`}
{...props}
/>
);
},
);
export default InsetAvoidingView;
layout/layout-core/InsetContent.tsx
'use client';
import React from 'react';
import { BoxProps } from '@mui/material/Box';
import { styled } from '@mui/material/styles';
import { layoutClasses } from './layoutClasses';
const InsetContentRoot = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
backgroundColor: 'inherit',
overflow: 'auto',
background: 'var(--jun-IC-bg)',
boxSizing: 'var(--fixed, content-box) var(--absolute, border-box)' as never,
position: 'var(--fixed, fixed) var(--absolute, absolute)' as never,
height:
'var(--fixed, calc(100% - var(--jun-H-h, 0px))) var(--absolute, var(--jun-IC-absolute-h, 100%))',
width: 'var(--fixed, inherit) var(--absolute, 100%)',
top: 0,
borderRight: 'var(--anchor-left, var(--jun-IC-sideline))',
borderLeft: 'var(--anchor-right, var(--jun-IC-sideline))',
borderColor: theme.palette.divider,
marginLeft:
'var(--fixed, var(--anchor-left, -9999px)) var(--absolute, initial)',
paddingLeft:
'var(--fixed, var(--anchor-left, 9999px)) var(--absolute, initial)',
marginRight:
'var(--fixed, var(--anchor-right, -9999px)) var(--absolute, initial)',
paddingRight:
'var(--fixed, var(--anchor-right, 9999px)) var(--absolute, initial)',
marginTop: 'var(--fixed, var(--jun-H-h))',
}));
const InsetContent = React.forwardRef<HTMLDivElement, BoxProps>(
function InsetContent({ className, children, ...props }, ref) {
return (
<InsetContentRoot
ref={ref}
className={`${layoutClasses.InsetContent} ${className || ''}`}
{...props}
>
{children}
</InsetContentRoot>
);
},
);
export default InsetContent;
layout/layout-core/InsetSidebar.tsx
'use client';
import React, { useMemo } from 'react';
import { Breakpoint } from '@mui/material/styles';
import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';
import { layoutClasses } from './layoutClasses';
interface InsetSidebarProps {
position?: 'fixed' | 'absolute' | 'sticky';
width?: string | Partial<Record<Breakpoint, string>>;
}
const StyledInsetSidebar = styled('aside', {
name: 'LayoutInsetSidebar',
slot: 'root',
})<{
ownerState: InsetSidebarProps;
}>(
memoTheme(({ theme }) => ({
'--jun-IS-position': 'var(--sticky,)',
'--fixed': 'var(--jun-IS-position,)',
'--absolute': 'var(--jun-IS-position,)',
'--anchor-right': 'var(--jun-IS-anchor,)',
'--anchor-left': 'var(--jun-IS-anchor,)',
'*:has(>&)': {
display: 'flex',
flexFlow: 'row nowrap !important',
flexGrow: '1 !important' as never,
[`&:not(.${layoutClasses.Content})`]: {
height: '100%',
},
},
[`*:has(>&) > :where(:not([class*="${layoutClasses.InsetSidebar}"]))`]: {
flexGrow: '1 !important' as never,
overflow: 'auto',
},
width: '200px',
flex: 'none',
position: 'sticky' as const,
height: 'calc(var(--jun-h) - var(--jun-H-h, 0px))',
overflow: 'auto',
top: 'var(--jun-H-h, 0px)',
'&:first-child': {
'--jun-IS-anchor': 'var(--anchor-left)',
},
'&:last-child': {
'--jun-IS-anchor': 'var(--anchor-right)',
},
variants: [
{
props: ({ width }: InsetSidebarProps) => !!width,
style: ({ width }: Required<InsetSidebarProps>) =>
theme.unstable_sx({
width,
[`.${layoutClasses.Root}:has(> .${layoutClasses.Content} > &:first-child) > .${layoutClasses.Footer}`]:
{
'--jun-ISL-w': width,
},
[`.${layoutClasses.Root}:has(> .${layoutClasses.Content} > * > &:first-child) > .${layoutClasses.Footer}`]:
{
'--jun-ISL-w': width,
},
[`.${layoutClasses.Root}:has(> .${layoutClasses.Content} > &:last-child) > .${layoutClasses.Footer}`]:
{
'--jun-ISR-w': width,
},
[`.${layoutClasses.Root}:has(> .${layoutClasses.Content} > * > &:last-child) > .${layoutClasses.Footer}`]:
{
'--jun-ISR-w': width,
},
...(typeof width !== 'string' && {
display: {
xs: 'none',
[Object.keys(width)[0]]: 'block',
},
}),
}),
},
{
props: { position: 'fixed' },
style: {
'--jun-IS-position': 'var(--fixed,)',
position: 'relative' as const,
height: 'auto',
overflow: 'visible',
top: 'auto',
flexShrink: 0,
zIndex: 1,
},
},
{
props: { position: 'absolute' },
style: {
'--jun-IS-position': 'var(--absolute,)',
position: 'relative' as const,
height: 'auto',
overflow: 'visible',
top: 'auto',
flexShrink: 0,
zIndex: 1,
},
},
],
})),
);
const InsetSidebar = React.forwardRef<
HTMLDivElement,
Omit<React.ComponentProps<typeof StyledInsetSidebar>, 'ownerState'> &
InsetSidebarProps
>(function InsetSidebar(
{ className, position = 'sticky', width, ...props },
ref,
) {
const ownerState = useMemo(
() => ({
position,
width,
}),
[position, width],
);
return (
<StyledInsetSidebar
ref={ref}
className={`${layoutClasses.InsetSidebar} ${layoutClasses.InsetSidebar}-${position} ${className || ''}`}
ownerState={ownerState}
{...props}
/>
);
});
export default InsetSidebar;
layout/layout-core/Root.tsx
'use client';
import React, { useMemo } from 'react';
import { Breakpoint } from '@mui/material/styles';
import { styled } from '@mui/material/styles';
import { unstable_memoTheme as memoTheme } from '@mui/material/utils';
import { layoutClasses } from './layoutClasses';
interface RootProps {
height?: string | Record<Breakpoint, string> | Record<string, string>;
standalone?: boolean;
disableTransition?: boolean;
}
const StyledRoot = styled('div', {
name: 'LayoutRoot',
slot: 'root',
})<{
ownerState: RootProps;
}>(
memoTheme(({ theme }) => ({
'--jun-h': 'calc(100svh - env(safe-area-inset-bottom))',
'--jun-H-h': '3rem', // default header height
'--jun-H-clip-h': '0px', // prevent upper layout
'--Header-underline': `1px solid ${(theme.vars || theme).palette.divider}`,
'--Header-background': (theme.vars || theme).palette.background.paper,
'--Footer-background': (theme.vars || theme).palette.background.default,
'--Footer-overline': `1px solid ${(theme.vars || theme).palette.divider}`,
'--drawer-pos': 'fixed',
'--drawer-z': '999',
'--jun-ES-line-w': '1px',
'--jun-ES-line-color': (theme.vars || theme).palette.divider,
'--EdgeSidebarContent-background': (theme.vars || theme).palette.background
.paper,
'--jun-IC-bg': (theme.vars || theme).palette.background.paper,
'--jun-IC-sideline': `1px solid ${(theme.vars || theme).palette.divider}`,
backgroundColor: (theme.vars || theme).palette.background.paper,
minHeight: 'var(--jun-h)',
minWidth: 0,
flex: 1,
display: 'grid',
position: 'relative',
transition: 'var(--tsn, grid-template-columns 0.3s)',
containerType: 'inline-size',
containerName: 'page',
gridTemplateRows: 'auto 1fr',
gridTemplateColumns:
'var(--_start-col, 0px) minmax(0, 1fr) var(--_end-col, 0px)',
gridTemplateAreas: `
"${layoutClasses.EdgeSidebar} ${layoutClasses.Header} ${layoutClasses.EdgeSidebarRight}"
"${layoutClasses.EdgeSidebar} ${layoutClasses.Content} ${layoutClasses.EdgeSidebarRight}"
"${layoutClasses.EdgeSidebar} ${layoutClasses.Footer} ${layoutClasses.EdgeSidebarRight}"
`,
[`&:has(.${layoutClasses.EdgeSidebar})`]: {
'--_start-col': 'max-content',
'--jun-ES-drawerOpen': '0',
},
[`&:has(.${layoutClasses.EdgeSidebarRight})`]: {
'--_end-col': 'max-content',
'--jun-ES-drawerOpen': '0',
},
variants: [
{
props: ({ height }: RootProps) => !!height,
style: ({ height }: Required<RootProps>) =>
theme.unstable_sx({
'--jun-h': height,
}),
},
{
props: { standalone: true },
style: {
'--drawer-pos': 'absolute', // make edge sidebar (drawer) stay within the layout
'--drawer-z': '5',
'--content-overflow': 'auto',
'--jun-IC-absolute-h': 'calc(var(--jun-h) - var(--jun-H-h))',
maxHeight: 'var(--jun-h)',
[`:has(.${layoutClasses.Content} .${layoutClasses.InsetSidebar}-absolute)`]:
{
'--content-overflow': 'initial',
},
},
},
{
props: { disableTransition: true },
style: {
'--tsn': 'none',
},
},
],
})),
);
const Root = React.forwardRef<
HTMLDivElement,
Omit<React.ComponentPropsWithoutRef<typeof StyledRoot>, 'ownerState'> &
RootProps
>(function Root(
{ className, height, standalone, disableTransition, ...props },
ref,
) {
const ownerState = useMemo(
() => ({
height,
standalone,
disableTransition,
}),
[height, standalone, disableTransition],
);
return (
<StyledRoot
ref={ref}
className={`${layoutClasses.Root} ${className || ''}`}
ownerState={ownerState}
{...props}
/>
);
});
export default Root;
layout/layout-core/SharedEdgeSidebar.tsx
'use client';
import React from 'react';
import { Breakpoint } from '@mui/material/styles';
import { styled } from '@mui/material/styles';
import { layoutAttrs } from './layoutAttrs';
import { layoutClasses } from './layoutClasses';
export type DrawerConfig = {
width?: string;
showHeader?: boolean;
withoutOverlay?: boolean;
};
export type PermanentConfig = {
width?: string;
collapsedWidth?: string;
visibility?: 'hidden' | 'visible';
hoverUncollapse?:
| true
| {
delay?: string;
shadow?: string;
};
};
export type EdgeSidebarVariant =
| ['drawer', DrawerConfig?]
| ['permanent', PermanentConfig?];
export type EdgeSidebarVariantInput =
| EdgeSidebarVariant
| Partial<Record<Breakpoint, EdgeSidebarVariant>>;
export function internalCollapseSidebar(options: {
event: React.MouseEvent;
selector: string;
state?: boolean;
document?: Document | null;
}) {
const { state, document: d, selector, event } = options || {};
const doc = d ?? document;
const sidebar = doc.querySelector(selector) as HTMLElement;
if (sidebar) {
const currentCollapsed =
window
.getComputedStyle(event.target as Element)
.getPropertyValue('--_sidebarCollapsed') === '1';
const nextCollapsed = state === undefined ? !currentCollapsed : state;
const autoCollapse =
window
.getComputedStyle(event.target as Element)
.getPropertyValue('--_autoCollapse') === '1';
const inAutoCollapse =
window
.getComputedStyle(event.target as Element)
.getPropertyValue('--_in-autoCollapse') === '1';
if (nextCollapsed) {
sidebar.removeAttribute(layoutAttrs.isEdgeSidebarUncollapsed);
if (!autoCollapse || inAutoCollapse) {
sidebar.setAttribute(layoutAttrs.isEdgeSidebarCollapsed, '');
}
} else {
sidebar.removeAttribute(layoutAttrs.isEdgeSidebarCollapsed);
if (!inAutoCollapse) {
sidebar.setAttribute(layoutAttrs.isEdgeSidebarUncollapsed, '');
}
}
}
}
export function internalToggleSidebar(options: {
selector: string;
state?: boolean;
document?: Document | null;
}) {
const { state, document: d, selector } = options || {};
const doc = d ?? document;
const sidebar = doc.querySelector(selector) as HTMLDivElement | null;
if (sidebar) {
const currentOpen = sidebar.getAttribute(layoutAttrs.isDrawerOpen) !== null;
const nextOpen = state === undefined ? !currentOpen : state;
if (nextOpen) {
sidebar.setAttribute(layoutAttrs.isDrawerOpen, '');
sidebar.style.setProperty('--jun-ES-drawerOpen', '1');
function handleOutsideClick(event: MouseEvent) {
const closer = doc.querySelector(
`.${layoutClasses.EdgeDrawerClose}`,
) as HTMLButtonElement;
if (
// clicking on the backdrop (psuedo element of sidebar) will close the sidebar
event.target === sidebar ||
// clicking on the closer button will close the sidebar
(closer && closer.contains(event.target as Node))
) {
internalToggleSidebar({
...options,
state: false,
});
doc.removeEventListener?.('click', handleOutsideClick);
}
}
setTimeout(() => {
// prevent the `handleOutsideClick` to be called immediately
doc.addEventListener?.('click', handleOutsideClick);
}, 0);
// TODO: add a way to close the sidebar by swiping
// TODO: add a way to close the sidebar by pressing ESC
} else {
sidebar.removeAttribute(layoutAttrs.isDrawerOpen);
sidebar.setAttribute(layoutAttrs.isDrawerClosing, '');
setTimeout(() => {
sidebar.removeAttribute(layoutAttrs.isDrawerClosing);
}, 300);
sidebar.style.setProperty('--jun-ES-drawerOpen', '');
}
}
}
/**
* Observes the EdgeSidebar width and returns whether it is collapsed.
*
* @param edgeSidebarId - The `id` of the EdgeSidebar element to observe.
* @param options.collapsedDelay - Delay in ms before setting collapsed to `true`. Defaults to 200.
* @param options.uncollapsedDelay - Delay in ms before setting collapsed to `false`. Defaults to 200.
* @returns `undefined` before the first measurement, then `true` if collapsed or `false` if expanded.
*/
export function useCollapsedSidebar(
edgeSidebarId: string,
options?: { collapsedDelay?: number; uncollapsedDelay?: number },
) {
const { collapsedDelay = 300, uncollapsedDelay = 0 } = options || {};
const [collapsed, setCollapsed] = React.useState<boolean | undefined>(
undefined,
);
React.useEffect(() => {
const edgeSidebar = document.getElementById(edgeSidebarId);
if (!edgeSidebar) return;
const cssVar = edgeSidebar.classList.contains(
layoutClasses.EdgeSidebarRight,
)
? '--jun-ESR-collapsedWidth'
: '--jun-ES-collapsedWidth';
const content = edgeSidebar.querySelector(
`.${layoutClasses.EdgeSidebarContent}`,
);
if (!content) return;
let timeoutId: ReturnType<typeof setTimeout>;
const ro = new ResizeObserver(() => {
const width = content.getBoundingClientRect().width;
const collapsedWidth =
parseFloat(getComputedStyle(edgeSidebar).getPropertyValue(cssVar)) || 0;
const next = width <= collapsedWidth;
clearTimeout(timeoutId);
timeoutId = setTimeout(
() => setCollapsed(next),
next ? collapsedDelay : uncollapsedDelay,
);
});
ro.observe(content);
return () => {
clearTimeout(timeoutId);
ro.disconnect();
};
}, [edgeSidebarId, collapsedDelay, uncollapsedDelay]);
return collapsed;
}
export const EdgeSidebarRoot = styled('div')({
'--anchorLeft': 'var(--jun-ES-anchor,)',
'--anchorRight': 'var(--jun-ES-anchor,)',
'--drawer-h': 'var(--jun-h)',
transition: 'var(--tsn, width 0.3s)',
display: 'flex',
flexDirection: 'column',
padding: '0px', // prevent user from customizing it
margin: '0px', // prevent user from customizing it
// ==============================
// To keep the EdgeSidebar fixed when the Content is scrollable
position: 'var(--_permanent, sticky)' as never,
top: 'var(--_permanent, var(--jun-H-clip-h))',
zIndex: 'var(--_drawer, 2) var(--_permanent, 1)' as never,
height: 'var(--_permanent, calc(var(--jun-h) - var(--jun-H-clip-h)))',
// ==============================
'&::before': {
display: `var(--_drawer, block)
var(--_permanent, none)`,
position: 'absolute',
content: '""',
inset: '0',
backgroundColor: 'rgba(0, 0, 0, 0.48)',
backdropFilter: 'blur(4px)',
zIndex: '1',
transition: 'var(--tsn, opacity 0.4s, visibility 0.4s)',
visibility: 'hidden',
opacity: 'var(--jun-ES-drawerOpen, 0)',
},
[`&[${layoutAttrs.isDrawerOpen}]`]: {
'&::before': {
visibility: 'visible',
},
},
[`html:has([${layoutAttrs.isDrawerOpen}]:not(.${layoutClasses.DrawerWithoutOverlay}):not(.${layoutClasses.Content} &))`]:
{
overflow: 'hidden',
},
});
layout/layout-core/index.ts
export { default as Content } from './Content';
export * from './Content';
export { default as EdgeDrawerClose } from './EdgeDrawerClose';
export { default as EdgeDrawerTrigger } from './EdgeDrawerTrigger';
export { default as EdgeDrawerTriggerRight } from './EdgeDrawerTriggerRight';
export { default as EdgeSidebar } from './EdgeSidebar';
export * from './EdgeSidebar';
export { default as EdgeSidebarCollapser } from './EdgeSidebarCollapser';
export { default as EdgeSidebarContent } from './EdgeSidebarContent';
export * from './EdgeSidebarContent';
export { default as EdgeSidebarRight } from './EdgeSidebarRight';
export * from './EdgeSidebarRight';
export { default as Header } from './Header';
export * from './Header';
export { default as Footer } from './Footer';
export * from './Footer';
export { default as Root } from './Root';
export * from './Root';
export { default as InsetAvoidingView } from './InsetAvoidingView';
export * from './InsetAvoidingView';
export { default as InsetSidebar } from './InsetSidebar';
export * from './InsetSidebar';
export { default as InsetContent } from './InsetContent';
export * from './InsetContent';
export { layoutAttrs } from './layoutAttrs';
export { layoutClasses } from './layoutClasses';
export { layoutVisibility } from './layoutVisibility';
layout/layout-core/layoutAttrs.ts
export const layoutAttrs = {
isDrawerOpen: 'data-drawer-open',
isDrawerClosing: 'data-mobile-closing',
isEdgeSidebarUncollapsed: 'data-edge-uncollapsed',
isEdgeSidebarCollapsed: 'data-edge-collapsed',
isEdgeSidebarContentHidden: 'data-sidebar-hidden',
};
layout/layout-core/layoutClasses.ts
export const layoutClasses = {
Root: 'Root',
Header: 'Header',
Content: 'Content',
Footer: 'Footer',
EdgeSidebar: 'EdgeSidebar',
EdgeSidebarContent: 'EdgeSidebarContent',
EdgeSidebarCollapser: 'EdgeSidebar-collapser',
EdgeDrawerTrigger: 'EdgeDrawerTrigger',
EdgeDrawerClose: 'EdgeDrawerClose',
EdgeDrawerOpenVisible: 'EdgeDrawerOpen-visible',
EdgeDrawerClosedVisible: 'EdgeDrawerClosed-visible',
EdgeCollapsedVisible: 'EdgeCollapsedVisible',
EdgeUncollapsedVisible: 'EdgeUncollapsedVisible',
EdgeSidebarRight: 'EdgeSidebar-R',
EdgeSidebarRightCollapser: 'EdgeSidebar-R-collapser',
EdgeDrawerTriggerRight: 'EdgeDrawerTrigger-R',
DrawerWithoutOverlay: 'EdgeDrawer-withoutOverlay',
InsetSidebar: 'InsetSidebar',
InsetContent: 'InsetContent',
InsetAvoidingView: 'InsetAvoidingView',
SidebarRail: 'SidebarRail',
};
layout/layout-core/layoutVariables.ts
export const layoutVariables = {
height: '--jun-h',
headerHeight: '--jun-H-h',
headerClipHeight: '--jun-H-clip-h',
edgeSidebarLineWidth: '--jun-ES-line-w',
edgeSidebarLineColor: '--jun-ES-line-color',
edgeSidebarPermanentWidth: '--jun-ES-permanentWidth',
edgeSidebarCollapsedWidth: '--jun-ES-collapsedWidth',
edgeSidebarDrawerWidth: '--jun-ES-drawerWidth',
edgeSidebarRightPermanentWidth: '--jun-ESR-permanentWidth',
edgeSidebarRightCollapsedWidth: '--jun-ESR-collapsedWidth',
insetSidebarLeftWidth: '--jun-ISL-w',
insetSidebarRightWidth: '--jun-ISR-w',
};
layout/layout-core/layoutVisibility.ts
import { layoutClasses } from './layoutClasses';
export const layoutVisibility = {
visibleOnEdgeDrawerOpen: layoutClasses.EdgeDrawerOpenVisible,
visibleOnEdgeDrawerClosed: layoutClasses.EdgeDrawerClosedVisible,
visibleOnEdgePermanentCollapsed: layoutClasses.EdgeCollapsedVisible,
visibleOnEdgePermanentUncollapsed: layoutClasses.EdgeUncollapsedVisible,
};