Files
kjol/go/jsruntime/uikit/Sidebar.tsx

104 lines
4.0 KiB
TypeScript

import { For, createSignal, Show, JSXElement } from "solid-js";
const NAV_ROOT = "bg-surface rounded-default shadow-sm border border-line py-2";
const NAV_LIST = "list-none m-0 p-0";
const NAV_BTN = "w-full text-left py-2 pr-3 pl-4 text-sm cursor-pointer bg-transparent text-ink-soft border-0 hover:text-ink hover:bg-surface-muted";
const NAV_SUBBTN = "w-full text-left py-1.5 pr-3 pl-8 text-ss cursor-pointer bg-transparent text-ink-muted border-0 hover:text-ink hover:bg-surface-muted";
const NAV_ICON = "mr-2";
const LAYOUT_ROOT = "grid grid-cols-1 gap-8 min-h-screen items-start lg:grid-cols-12";
const LAYOUT_SIDEBAR = "hidden lg:block lg:col-span-2 lg:self-start lg:h-full";
const LAYOUT_STICKY = "sticky top-20 max-h-[calc(100vh_-_7rem)] overflow-y-auto";
const LAYOUT_STICKY_FULL = "sticky top-0 h-screen overflow-y-auto";
// lg:pr-8 mirrors the grid's gap-8 (the content's left spacing from the sidebar)
// so the content has matching breathing room on the right instead of hugging the
// viewport edge. Scoped to lg, where the sidebar/gap exists.
const LAYOUT_MAIN = "col-span-1 min-w-0 lg:col-span-10 lg:pr-8";
export interface SidebarNavItem {
id: string;
label: string;
icon?: JSXElement;
// Optional second-level items that jump to sub-sections within this item.
children?: SidebarNavItem[];
}
interface SidebarNavProps {
items: SidebarNavItem[];
onItemClick?: (id: string) => void;
class?: string;
}
export function SidebarNav(props: SidebarNavProps) {
const handleClick = (id: string) => {
if (props.onItemClick) {
props.onItemClick(id);
}
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: "smooth", block: "start" });
}
};
return (
<nav class={NAV_ROOT + (props.class ? " " + props.class : "")}>
<ul class={NAV_LIST}>
<For each={props.items}>{(item: SidebarNavItem) => (
<li>
<button onclick={() => handleClick(item.id)} class={NAV_BTN}>
{item.icon ? <span class={NAV_ICON}>{item.icon}</span> : null}
{item.label}
</button>
<Show when={item.children && item.children.length}>
<ul class={NAV_LIST}>
<For each={item.children}>{(sub: SidebarNavItem) => (
<li>
<button onclick={() => handleClick(sub.id)} class={NAV_SUBBTN}>
{sub.icon ? <span class={NAV_ICON}>{sub.icon}</span> : null}
{sub.label}
</button>
</li>
)}</For>
</ul>
</Show>
</li>
)}</For>
</ul>
</nav>
);
}
interface SidebarLayoutProps {
sidebar: JSXElement;
class?: string;
children?: JSXElement;
fullHeight?: boolean;
collapsible?: boolean;
}
export function SidebarLayout(props: SidebarLayoutProps) {
const [collapsed, setCollapsed] = createSignal(false);
const toggleCollapse = () => {
if (props.collapsible) {
setCollapsed(!collapsed());
}
};
return (
<div class={LAYOUT_ROOT + (props.class ? " " + props.class : "")}>
<aside class={LAYOUT_SIDEBAR}>
<div class={props.fullHeight ? LAYOUT_STICKY_FULL : LAYOUT_STICKY}>
{props.sidebar}
</div>
<Show when={props.collapsible}>
<button type="button" onclick={toggleCollapse} aria-label="Toggle sidebar"></button>
</Show>
</aside>
<main class={LAYOUT_MAIN}>
{props.children}
</main>
</div>
);
}