Sidebar Layout
v0.4.0A dumb CSS Grid skeleton with sidebar, topbar, and content regions. It has no opinion on what the sidebar or topbar contain — the elements passed in are responsible for their own `grid-area: sidebar` / `grid-area: topbar` placement (e.g. via `@behivetech/atoms.sidebar`).
registry/behivetech/templates/sidebar-layout · depends on @behivetech/get-class-name
sidebar-layout.tsx
1import { type HTMLAttributes, type ReactNode } from "react";2import { getClassName } from "@behivetech/get-class-name";3import styles from "./sidebar-layout.module.scss";45export interface SidebarLayoutProps extends HTMLAttributes<HTMLDivElement> {6 /** Additional class names to merge with the component root element */7 className?: string;8 /** Sidebar region content; the element rendered here owns its own `grid-area: sidebar` placement */9 sidebar: ReactNode;10 /** Topbar region content; the element rendered here owns its own `grid-area: topbar` placement */11 topbar: ReactNode;12 /** Main content region */13 children?: ReactNode;14}1516/**17 * Dumb CSS Grid skeleton with sidebar, topbar, and content regions.18 * Composition of the actual sidebar/topbar/content is left entirely to the19 * consumer — this only owns the grid tracks and the content scroll area.20 */21export const SidebarLayout = ({22 className,23 sidebar,24 topbar,25 children,26 ...rest27}: SidebarLayoutProps) => {28 const [rootClass, getChildClass] = getClassName({29 className,30 rootClass: "BHT__SidebarLayout",31 styles,32 });3334 return (35 <div {...rest} className={rootClass}>36 {sidebar}37 <div className={getChildClass("main")}>38 {topbar}39 <main className={getChildClass("content")}>40 <div className={getChildClass("inner")}>{children}</div>41 </main>42 </div>43 </div>44 );45};
sidebar-layout.module.scss
1.BHT__SidebarLayout {2 display: grid;3 grid-template-columns: var(--sidebar-layout-sidebar-width, 13.75rem) 1fr;4 grid-template-rows: var(--sidebar-layout-topbar-height, 3.5rem) 1fr;5 grid-template-areas:6 "sidebar topbar"7 "sidebar content";8 min-height: 100vh;910 &__main {11 grid-column: 2;12 display: contents;13 }1415 &__content {16 grid-area: content;17 background: var(--md-sys-color-background, var(--color-background));18 color: var(--md-sys-color-on-background, var(--color-text-body));19 overflow-y: auto;20 }2122 &__inner {23 padding: var(--spacing-8);24 max-width: var(--sidebar-layout-content-max-width, 75rem);25 }26}
index.ts
1export { SidebarLayout } from "./sidebar-layout.js";2export type { SidebarLayoutProps } from "./sidebar-layout.js";
sidebar-layout.composition.tsx
1import { SidebarLayout } from "./sidebar-layout.js";23export const BasicSidebarLayout = () => (4 <SidebarLayout sidebar={<nav>Sidebar</nav>} topbar={<header>Topbar</header>}>5 Page content6 </SidebarLayout>7);
sidebar-layout.spec.tsx
1import { render, screen } from "@testing-library/react";2import { SidebarLayout } from "./sidebar-layout.js";34describe("SidebarLayout", () => {5 it("renders children", () => {6 render(7 <SidebarLayout sidebar={<nav>Nav</nav>} topbar={<header>Topbar</header>}>8 Hello9 </SidebarLayout>,10 );11 expect(screen.getByText("Hello")).toBeTruthy();12 });1314 it("renders the sidebar and topbar content", () => {15 render(16 <SidebarLayout sidebar={<nav>Nav</nav>} topbar={<header>Topbar</header>}>17 Content18 </SidebarLayout>,19 );20 expect(screen.getByText("Nav")).toBeTruthy();21 expect(screen.getByText("Topbar")).toBeTruthy();22 });2324 it("applies additional className", () => {25 const { container } = render(26 <SidebarLayout className="custom" sidebar={<nav />} topbar={<header />}>27 Content28 </SidebarLayout>,29 );30 expect((container.firstChild as HTMLElement).className).toContain("custom");31 });32});