237 lines
7.0 KiB
TypeScript
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>
|
|
);
|
|
};
|