Feat: Add responsive workspace shell
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
.browserLayer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@include respond-down(mobile) {
|
||||
.browserLayer {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: grid;
|
||||
z-index: calc(var(--z-popover, 20) + 2);
|
||||
background: color-mix(in srgb, var(--color-canvas) 96%, black 4%);
|
||||
}
|
||||
|
||||
.sheet {
|
||||
min-height: 100dvh;
|
||||
display: grid;
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
background:
|
||||
linear-gradient(180deg, color-mix(in srgb, var(--color-surface) 84%, transparent) 0%, transparent 8rem),
|
||||
color-mix(in srgb, var(--color-canvas) 97%, black 3%);
|
||||
overflow: clip;
|
||||
}
|
||||
|
||||
.sheetHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
padding: calc(var(--space-5) + env(safe-area-inset-top, 0px)) var(--space-4) var(--space-4);
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 84%, transparent);
|
||||
}
|
||||
|
||||
.brandBlock {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.sectionLabel {
|
||||
@include text-caption;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.brandEyebrow {
|
||||
@include text-caption;
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
|
||||
.brandTitle {
|
||||
@include text-title;
|
||||
color: var(--color-text);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.brandContext {
|
||||
@include text-caption;
|
||||
color: var(--color-text-subtle);
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: var(--control-size-md);
|
||||
height: var(--control-size-md);
|
||||
padding: 0;
|
||||
border: 1px solid color-mix(in srgb, var(--color-border) 92%, transparent);
|
||||
border-radius: 999px;
|
||||
background: color-mix(in srgb, var(--color-surface) 92%, transparent);
|
||||
color: var(--color-text-subtle);
|
||||
}
|
||||
|
||||
.sheetBody {
|
||||
min-height: 0;
|
||||
display: grid;
|
||||
gap: var(--space-5);
|
||||
padding: var(--space-5) var(--space-5) calc(var(--space-12) + var(--space-10) + env(safe-area-inset-bottom, 0px));
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.sectionBlock {
|
||||
display: grid;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.treeList,
|
||||
.treeListItem {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.treeList {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
border-top: 1px solid color-mix(in srgb, var(--color-border) 92%, transparent);
|
||||
}
|
||||
|
||||
.treeListNested {
|
||||
display: grid;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.treeRow {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
min-height: calc(var(--control-size-lg) + var(--space-3));
|
||||
padding: var(--space-4) var(--space-2) var(--space-4) calc(var(--space-2) + (var(--tree-depth, 0) * var(--space-4)));
|
||||
border: 0;
|
||||
border-bottom: 1px solid color-mix(in srgb, var(--color-border) 92%, transparent);
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.treeRowActive {
|
||||
color: var(--color-text);
|
||||
background: color-mix(in srgb, var(--color-surface) 72%, transparent);
|
||||
}
|
||||
|
||||
.treeRowBranch {
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.treeRowLead,
|
||||
.treeRowTrail {
|
||||
min-width: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.treeRowTrail {
|
||||
flex: 0 0 auto;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.treeLabel {
|
||||
@include text-label;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.treeMeta {
|
||||
@include text-caption;
|
||||
min-width: 1rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.treeChevron {
|
||||
color: var(--color-text-subtle);
|
||||
}
|
||||
|
||||
.treeListNested > .treeListItem > .treeRow {
|
||||
min-height: calc(var(--control-size-lg) + var(--space-1));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
import { For, Show, type JSX } from "solid-js";
|
||||
import { ChevronRight, X } from "../../../lib/icons";
|
||||
import { activeProject, activeServer, workspaceStaticItems, workspaceTree, type SidebarItem, type WorkspaceTreeNode } from "../data/shell.data";
|
||||
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 (
|
||||
<li class={styles.treeListItem}>
|
||||
<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>
|
||||
|
||||
<Show when={hasChildren}>
|
||||
<ul class={styles.treeListNested}>
|
||||
<For each={props.node.children}>{(child): JSX.Element => <TreeRow node={child} depth={depth + 1} />}</For>
|
||||
</ul>
|
||||
</Show>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const StaticRow = (props: { item: SidebarItem }): JSX.Element => {
|
||||
const Icon = props.item.icon;
|
||||
|
||||
return (
|
||||
<li class={styles.treeListItem}>
|
||||
<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>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export const MobileWorkspaceBrowser = (props: MobileWorkspaceBrowserProps): JSX.Element => {
|
||||
const sectionNodes = workspaceTree.filter((node) => (node.children?.length ?? 0) > 0);
|
||||
const looseNodes = workspaceTree.filter((node) => (node.children?.length ?? 0) === 0);
|
||||
|
||||
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}>
|
||||
<span class={styles.brandEyebrow}>Moku Work</span>
|
||||
<strong class={styles.brandTitle}>{activeProject.name}</strong>
|
||||
<span class={styles.brandContext}>{activeServer.name}</span>
|
||||
</div>
|
||||
|
||||
<button class={styles.closeButton} type="button" aria-label="Close workspace browser" onClick={props.onClose}>
|
||||
<X size={18} strokeWidth={2} />
|
||||
</button>
|
||||
</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 => <StaticRow item={item} />}</For>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section class={styles.sectionBlock}>
|
||||
<span class={styles.sectionLabel}>Items</span>
|
||||
<ul class={styles.treeList}>
|
||||
<For each={sectionNodes}>{(node): JSX.Element => <TreeRow node={node} />}</For>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<Show when={looseNodes.length > 0}>
|
||||
<section class={styles.sectionBlock}>
|
||||
<span class={styles.sectionLabel}>More</span>
|
||||
<ul class={styles.treeList}>
|
||||
<For each={looseNodes}>{(node): JSX.Element => <TreeRow node={node} />}</For>
|
||||
</ul>
|
||||
</section>
|
||||
</Show>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user