164 lines
4.5 KiB
TypeScript
164 lines
4.5 KiB
TypeScript
// Path: Frontend/src/components/shell/WorkspaceSidebar/WorkspaceSidebar.tsx
|
|
|
|
import { For, Show, createSignal, type JSX } from "solid-js";
|
|
import { ChevronLeft, ChevronRight } from "../../../lib/icons";
|
|
import { ProjectSelector } from "../ProjectSelector/ProjectSelector";
|
|
import { workspaceSidebarHeaderActions, workspaceStaticItems, workspaceTree, type SidebarItem, type WorkspaceTreeNode } from "../data/shell.data";
|
|
import styles from "./WorkspaceSidebar.module.scss";
|
|
|
|
type WorkspaceSidebarProps = {
|
|
collapsed: boolean;
|
|
railCollapsed: boolean;
|
|
onToggleRailCollapse: () => void;
|
|
};
|
|
|
|
const WorkspaceHomeEntry = (props: { item: SidebarItem }): JSX.Element => {
|
|
const Icon = props.item.icon;
|
|
|
|
return (
|
|
<li>
|
|
<button
|
|
type="button"
|
|
classList={{
|
|
[styles.navItem]: true,
|
|
[styles.navItemActive]: !!props.item.active,
|
|
}}
|
|
aria-current={props.item.active ? "page" : undefined}
|
|
aria-label={props.item.label}
|
|
title={props.item.label}
|
|
>
|
|
<Icon class={styles.icon} size={18} strokeWidth={2} />
|
|
<span class={styles.label}>{props.item.label}</span>
|
|
<Show when={props.item.meta}>
|
|
<span class={styles.itemMeta}>{props.item.meta}</span>
|
|
</Show>
|
|
</button>
|
|
</li>
|
|
);
|
|
};
|
|
|
|
const WorkspaceTreeBranch = (props: { nodes: readonly WorkspaceTreeNode[]; depth?: number }): JSX.Element => {
|
|
const depth = () => props.depth ?? 0;
|
|
|
|
return (
|
|
<ul class={styles.treeList} role="list">
|
|
<For each={props.nodes}>
|
|
{(node): JSX.Element => {
|
|
const Icon = node.icon;
|
|
|
|
return (
|
|
<li>
|
|
<button
|
|
type="button"
|
|
classList={{
|
|
[styles.treeItem]: true,
|
|
[styles.treeItemActive]: !!node.active,
|
|
[styles.treeItemFolder]: node.kind === "folder",
|
|
}}
|
|
style={{ "--tree-depth": String(depth()) }}
|
|
aria-current={node.active ? "page" : undefined}
|
|
aria-label={node.label}
|
|
title={node.label}
|
|
>
|
|
<Icon class={styles.icon} size={18} strokeWidth={2} />
|
|
<span class={styles.label}>{node.label}</span>
|
|
<Show when={node.meta}>
|
|
<span class={styles.itemMeta}>{node.meta}</span>
|
|
</Show>
|
|
</button>
|
|
|
|
<Show when={node.children?.length}>
|
|
<WorkspaceTreeBranch nodes={node.children ?? []} depth={depth() + 1} />
|
|
</Show>
|
|
</li>
|
|
);
|
|
}}
|
|
</For>
|
|
</ul>
|
|
);
|
|
};
|
|
|
|
export const WorkspaceSidebar = (props: WorkspaceSidebarProps): JSX.Element => {
|
|
const [isProjectDrawerOpen, setIsProjectDrawerOpen] = createSignal(false);
|
|
const railToggleLabel = (): string => (props.railCollapsed ? "Expand server rail" : "Collapse server rail");
|
|
|
|
return (
|
|
<aside
|
|
classList={{
|
|
[styles.sidebar]: true,
|
|
[styles.sidebarCollapsed]: props.collapsed,
|
|
}}
|
|
aria-label="Left workspace sidebar"
|
|
>
|
|
<div
|
|
classList={{
|
|
[styles.header]: true,
|
|
[styles.headerDrawerOpen]: isProjectDrawerOpen(),
|
|
}}
|
|
>
|
|
<div class={styles.headerActions}>
|
|
<button
|
|
type="button"
|
|
classList={{
|
|
[styles.headerActionButton]: true,
|
|
[styles.headerCollapseButton]: true,
|
|
}}
|
|
aria-label={railToggleLabel()}
|
|
title={railToggleLabel()}
|
|
onClick={props.onToggleRailCollapse}
|
|
>
|
|
{props.railCollapsed ? <ChevronRight size={16} strokeWidth={2} /> : <ChevronLeft size={16} strokeWidth={2} />}
|
|
</button>
|
|
|
|
<For each={workspaceSidebarHeaderActions}>
|
|
{(action): JSX.Element => {
|
|
const Icon = action.icon;
|
|
|
|
return (
|
|
<button type="button" class={styles.headerActionButton} aria-label={action.label} title={action.label}>
|
|
<Icon size={16} strokeWidth={2} />
|
|
</button>
|
|
);
|
|
}}
|
|
</For>
|
|
</div>
|
|
|
|
<div class={styles.headerControls}>
|
|
<ProjectSelector
|
|
compact={props.collapsed}
|
|
isOpen={isProjectDrawerOpen()}
|
|
onToggle={(): void => {
|
|
setIsProjectDrawerOpen(true);
|
|
}}
|
|
onClose={(): void => {
|
|
setIsProjectDrawerOpen(false);
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
classList={{
|
|
[styles.section]: true,
|
|
[styles.sectionHidden]: isProjectDrawerOpen(),
|
|
}}
|
|
>
|
|
<Show when={!props.collapsed}>
|
|
<span class={styles.sectionLabel}>Workspace</span>
|
|
</Show>
|
|
<div class={styles.navScroller}>
|
|
<ul class={styles.navList} role="list">
|
|
<For each={workspaceStaticItems}>{(item): JSX.Element => <WorkspaceHomeEntry item={item} />}</For>
|
|
</ul>
|
|
|
|
<Show when={!props.collapsed}>
|
|
<div class={styles.treeSectionLabel}>Items</div>
|
|
</Show>
|
|
|
|
<WorkspaceTreeBranch nodes={workspaceTree} />
|
|
</div>
|
|
</div>
|
|
</aside>
|
|
);
|
|
};
|