Files
Work/Frontend/src/components/shell/MobileWorkspaceBrowser/MobileWorkspaceBrowser.tsx
2026-06-18 11:16:54 +01:00

237 lines
7.0 KiB
TypeScript

import { For, Show, createSignal, type JSX } from "solid-js";
import { ChevronRight, Plus, X } from "../../../lib/icons";
import { createLongPressGesture } from "../createLongPressGesture";
import {
activeProject,
activeServer,
createWorkspaceStaticTarget,
createWorkspaceSurfaceTarget,
createWorkspaceTreeTarget,
workspaceStaticItems,
workspaceTree,
type SidebarItem,
type WorkspaceContextMenuAction,
type WorkspaceContextMenuTarget,
type WorkspaceStaticItem,
type WorkspaceTreeNode,
} from "../data/shell.data";
import { WorkspaceMobileActionSheet } from "../WorkspaceMobileActionSheet/WorkspaceMobileActionSheet";
import styles from "./MobileWorkspaceBrowser.module.scss";
type MobileWorkspaceBrowserProps = {
open: boolean;
onClose: VoidFunction;
};
const TreeRow = (props: { node: WorkspaceTreeNode; depth?: number }): JSX.Element => {
const depth = props.depth ?? 0;
const Icon = props.node.icon;
const hasChildren = (props.node.children?.length ?? 0) > 0;
return (
<button
classList={{
[styles.treeRow]: true,
[styles.treeRowActive]: props.node.active ?? false,
[styles.treeRowBranch]: hasChildren,
}}
type="button"
style={{ "--tree-depth": `${depth}` }}
>
<span class={styles.treeRowLead}>
<Icon size={16} strokeWidth={2} />
<span class={styles.treeLabel}>{props.node.label}</span>
</span>
<span class={styles.treeRowTrail}>
<Show when={props.node.meta}>
<span class={styles.treeMeta}>{props.node.meta}</span>
</Show>
<Show when={hasChildren}>
<ChevronRight size={14} strokeWidth={2} class={styles.treeChevron} />
</Show>
</span>
</button>
);
};
const StaticRow = (props: { item: SidebarItem }): JSX.Element => {
const Icon = props.item.icon;
return (
<button classList={{ [styles.treeRow]: true, [styles.treeRowActive]: props.item.active ?? false }} type="button" style={{ "--tree-depth": "0" }}>
<span class={styles.treeRowLead}>
<Icon size={16} strokeWidth={2} />
<span class={styles.treeLabel}>{props.item.label}</span>
</span>
<span class={styles.treeRowTrail}>
<Show when={props.item.meta}>
<span class={styles.treeMeta}>{props.item.meta}</span>
</Show>
<ChevronRight size={14} strokeWidth={2} class={styles.treeChevron} />
</span>
</button>
);
};
const WorkspaceStaticRow = (props: {
item: WorkspaceStaticItem;
onOpenActionSheet: (target: WorkspaceContextMenuTarget) => void;
}): JSX.Element => {
const target = createWorkspaceStaticTarget(props.item);
const longPress = createLongPressGesture({
onLongPress: () => {
props.onOpenActionSheet(target);
},
});
return (
<li
class={styles.treeListItem}
onContextMenu={(event): void => {
event.preventDefault();
props.onOpenActionSheet(target);
}}
{...longPress}
>
<StaticRow item={props.item} />
</li>
);
};
const WorkspaceTreeBranch = (props: {
nodes: readonly WorkspaceTreeNode[];
depth?: number;
onOpenActionSheet: (target: WorkspaceContextMenuTarget) => void;
}): JSX.Element => {
const depth = props.depth ?? 0;
return (
<For each={props.nodes}>
{(node): JSX.Element => {
const target = createWorkspaceTreeTarget(node);
const longPress = createLongPressGesture({
onLongPress: () => {
props.onOpenActionSheet(target);
},
});
return (
<li
class={styles.treeListItem}
onContextMenu={(event): void => {
event.preventDefault();
props.onOpenActionSheet(target);
}}
{...longPress}
>
<TreeRow node={node} depth={depth} />
<Show when={node.children?.length}>
<ul class={styles.treeListNested}>
<WorkspaceTreeBranch nodes={node.children ?? []} depth={depth + 1} onOpenActionSheet={props.onOpenActionSheet} />
</ul>
</Show>
</li>
);
}}
</For>
);
};
export const MobileWorkspaceBrowser = (props: MobileWorkspaceBrowserProps): JSX.Element => {
const [actionSheetTarget, setActionSheetTarget] = createSignal<WorkspaceContextMenuTarget | null>(null);
const sectionNodes = workspaceTree.filter((node) => (node.children?.length ?? 0) > 0);
const looseNodes = workspaceTree.filter((node) => (node.children?.length ?? 0) === 0);
const workspaceTarget = createWorkspaceSurfaceTarget(activeProject);
const openActionSheet = (target: WorkspaceContextMenuTarget): void => {
setActionSheetTarget(target);
};
const closeActionSheet = (): void => {
setActionSheetTarget(null);
};
const openWorkspaceActionSheet = (): void => {
openActionSheet(workspaceTarget);
};
const handleActionSelect = (_action: WorkspaceContextMenuAction, _target: WorkspaceContextMenuTarget): void => {
// Mobile first pass only establishes the action-sheet IA and long-press behavior.
};
const workspaceLongPress = createLongPressGesture({
onLongPress: openWorkspaceActionSheet,
});
return (
<Show when={props.open}>
<div class={styles.browserLayer}>
<section class={styles.sheet} aria-label="Mobile workspace browser">
<header class={styles.sheetHeader}>
<div
class={styles.brandBlock}
onContextMenu={(event): void => {
event.preventDefault();
openWorkspaceActionSheet();
}}
{...workspaceLongPress}
>
{/* Long-pressing the browser header exposes workspace-level actions on mobile. */}
<span class={styles.brandEyebrow}>Moku Work</span>
<strong class={styles.brandTitle}>{activeProject.name}</strong>
<span class={styles.brandContext}>{activeServer.name}</span>
</div>
<div class={styles.headerActions}>
<button
class={styles.createButton}
type="button"
aria-label="Create"
onClick={openWorkspaceActionSheet}
>
<Plus size={16} strokeWidth={2.25} />
<span>Create</span>
</button>
<button class={styles.closeButton} type="button" aria-label="Close workspace browser" onClick={props.onClose}>
<X size={18} strokeWidth={2} />
</button>
</div>
</header>
<div class={styles.sheetBody}>
<section class={styles.sectionBlock}>
<span class={styles.sectionLabel}>Workspace</span>
<ul class={styles.treeList}>
<For each={workspaceStaticItems}>
{(item): JSX.Element => <WorkspaceStaticRow item={item} onOpenActionSheet={openActionSheet} />}
</For>
</ul>
</section>
<section class={styles.sectionBlock}>
<span class={styles.sectionLabel}>Items</span>
<ul class={styles.treeList}>
<WorkspaceTreeBranch nodes={sectionNodes} onOpenActionSheet={openActionSheet} />
</ul>
</section>
<Show when={looseNodes.length > 0}>
<section class={styles.sectionBlock}>
<span class={styles.sectionLabel}>More</span>
<ul class={styles.treeList}>
<WorkspaceTreeBranch nodes={looseNodes} onOpenActionSheet={openActionSheet} />
</ul>
</section>
</Show>
</div>
</section>
<WorkspaceMobileActionSheet
target={actionSheetTarget()}
onClose={closeActionSheet}
onSelect={handleActionSelect}
/>
</div>
</Show>
);
};