shadimage
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,
};

More from mui-treasury

Browse all components in this library →