Redesign the shell and console with a liquid-glass look.

- Title bar: floating glass capsule aligned to each page's content edge, new
  NETX mark (links-as-X), task badge, user menu with sign-out, globe locale.
- Console: resource strip on top, module launcher as the main area with
  fixed-size tiles that fill rows as modules are added, task rail sized to
  the left column; dropped health pill, refresh button and filler copy.
- Glass layer (theme/glass.css, loaded last): black-blue wallpaper with a
  faint dot grid, clear panels with specular rims, translucent controls and
  table headers; sidebar pages float their nav and work area on a 16px gutter.
- Removed ~280 lines of duplicated header CSS from primitives/theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
oliver 2026-10-13 01:51:37 +08:00
parent 0ecff329cf
commit 8ecbb13f52
15 changed files with 1035 additions and 895 deletions

View file

@ -1,70 +1,33 @@
import { useMemo, useState } from "react";
import { Button } from "@heroui/react";
import type { ModuleDefinition } from "../../../config/modules";
import { useI18n } from "../../../i18n";
import { openOrFocusModule } from "../../../utils/moduleWindows";
import { ConsoleIcon } from "./ConsoleIcon";
import { WorkbenchCardIcon } from "./WorkbenchCardIcon";
export type DisplaySection = { key: "operations" | "system"; mods: ModuleDefinition[] };
function moduleDescription(mod: ModuleDefinition, t: (key: string) => string) {
const key = `workbench.descriptions.${mod.moduleId}`;
const description = t(key);
return description === key ? t(mod.titleKey) : description;
}
/** Primary modules as a grid of tiles, system admin as a quiet link row. */
export function WorkbenchModules({ sections }: { sections: DisplaySection[] }) {
const { t } = useI18n();
const [search, setSearch] = useState("");
const filtered = useMemo(() => {
const term = search.trim().toLocaleLowerCase();
return sections.map((section) => ({
...section,
mods: section.mods.filter((mod) =>
[t(mod.labelKey), moduleDescription(mod, t), mod.moduleId]
.some((value) => value.toLocaleLowerCase().includes(term)),
),
})).filter((section) => section.mods.length > 0);
}, [sections, search, t]);
const count = filtered.reduce((sum, section) => sum + section.mods.length, 0);
return (
<section className="wb-modules wb-panel" aria-label={t("workbench.modules")}>
<div className="wb-modules__head">
<div>
<h2 className="wb-panel-title">{t("workbench.modules")}</h2>
</div>
<div className="wb-search">
<ConsoleIcon kind="search" />
<input
type="search"
value={search}
aria-label={t("workbench.searchModules")}
placeholder={t("workbench.searchModules")}
onChange={(event) => setSearch(event.target.value)}
onKeyDown={(event) => { if (event.key === "Escape") setSearch(""); }}
/>
{search && <Button isIconOnly size="sm" variant="ghost" className="wb-search__clear" aria-label={t("workbench.clearSearch")} onPress={() => setSearch("")}><ConsoleIcon kind="close" /></Button>}
</div>
</div>
<span className="wb-sr-only" role="status">{t("workbench.moduleCount", { count })}</span>
{filtered.map((section) => (
<div key={section.key} className={`wb-section wb-section--${section.key}`}>
<div className={section.key === "operations" ? "wb-sr-only" : "wb-section__head"}>
<h3 className="wb-section__title">{t(`workbench.${section.key}`)}</h3>
</div>
<div className="wb-tiles">
{section.mods.map((mod) => (
<Button key={mod.moduleId} variant="ghost" className={`wb-tile wb-tile--${mod.iconTone}`} aria-label={`${t(mod.labelKey)} · ${t("workbench.openModule")}`} onPress={() => openOrFocusModule({ moduleId: mod.moduleId, path: mod.path })}>
<WorkbenchCardIcon tone={mod.iconTone} kind={mod.iconKind} />
<span className="wb-tile__label">{t(mod.labelKey)}</span>
</Button>
))}
</div>
{sections.map((section) => (
<div key={section.key} className={`wb-section wb-section--${section.key}`} role="group" aria-label={t(`workbench.${section.key}`)}>
{section.mods.map((mod) => (
<Button
key={mod.moduleId}
variant="ghost"
className={`wb-tile wb-tile--${mod.iconTone}`}
aria-label={`${t(mod.labelKey)} · ${t("workbench.openModule")}`}
onPress={() => openOrFocusModule({ moduleId: mod.moduleId, path: mod.path })}
>
<WorkbenchCardIcon tone={mod.iconTone} kind={mod.iconKind} />
<span className="wb-tile__label">{t(mod.labelKey)}</span>
</Button>
))}
</div>
))}
{count === 0 && <div className="wb-modules__empty"><ConsoleIcon kind="search" /><p>{t("workbench.noModules")}</p><Button size="sm" variant="ghost" onPress={() => setSearch("")}>{t("workbench.clearSearch")}</Button></div>}
</section>
);
}