prototype unified administration page layouts

This commit is contained in:
Codex
2026-09-10 16:48:25 +02:00
parent 3943022a97
commit debb63d87b
13 changed files with 638 additions and 0 deletions
+22
View File
@@ -571,3 +571,25 @@ _Avoid_: Sensitive Data Suggestion Event
**Introspection Capability** — Una categoria di struttura fisica che una Database Binding
può osservare, come tabelle, colonne, relazioni, indici o enum. Una capability non disponibile
è distinta da una capability osservata che non ha restituito elementi.
## Amministrazione e integrazione
**Administration Surface** — Una superficie amministrativa autonoma per configurare o curare una
parte dell'installazione. Workspace, Evidence, Memory, Database e Pi sono superfici peer e non
dipendono dall'esistenza di una sessione attiva.
**Administration Page** — La rappresentazione a pagina intera di una Administration Surface, con
una gerarchia condivisa per identità, stato, azioni e contenuto. Un form amministrativo appartiene
alla pagina e non a una popup come contenitore principale.
_Avoid_: management popup, settings modal
**Administration Route** — L'identità navigabile di una Administration Surface nel browser. Deve
essere ripristinabile con refresh e cronologia e non contiene valori transitori o segreti dei form.
**Embedded Thoth Shell** — L'esperienza Thoth ospitata dentro il documento e il contesto visuale di
un portale host. Conserva la propria gerarchia funzionale, ma deve rispettare la geometria,
l'autenticazione e le regole responsive del portale host.
**Administrative Page Family** — L'insieme delle cinque Administration Page che condividono shell,
navigazione, tipografia e regole responsive, pur mantenendo contenuti e operazioni specifici:
Workspace, Evidence, Memory, Database e Pi.
@@ -0,0 +1,15 @@
# Unify administration pages and use namespaced routes
status: accepted
Workspace and Pi management will become full Administration Pages alongside Evidence, Memory and
Database, using one shared Administrative Page Family. The Embedded Thoth Shell will identify the
active Administration Surface through a namespaced browser route, `thoth_route=administration/<surface>`,
so links remain reloadable and support Back/Forward without requiring new host-portal server routes.
Transient form values and secrets stay outside the route; destructive confirmations may remain
short-lived dialogs because they are actions, not page containers.
We considered React-only state, path-based routes and hash routes. React-only state loses refresh and
deep-link behavior, path routes require a host catch-all route that does not yet exist in Omics Portal,
and hash routes can conflict with host-page fragments. A namespaced query route preserves the current
same-document integration boundary while leaving room for a future path adapter.
+1
View File
@@ -5,6 +5,7 @@
"scripts": {
"dev": "vite",
"prototype:database-management": "vite --config vite.database-management-prototype.config.ts",
"prototype:administration-pages": "vite --config vite.administration-pages-prototype.config.ts",
"build": "tsc -b && vite build",
"preview": "vite preview",
"test": "vitest run",
@@ -0,0 +1,196 @@
import { useEffect, useMemo, useState, type ReactNode } from "react";
import {
Activity,
ArrowLeft,
ArrowRight,
Check,
ChevronDown,
CircleHelp,
ExternalLink,
FileCheck2,
FlaskConical,
LayoutList,
Menu,
MonitorCog,
Search,
Server,
Settings2,
ShieldCheck,
Sparkles,
Table2,
TerminalSquare,
X,
} from "lucide-react";
import { InspectorVariant } from "./variants/InspectorVariant";
import { OperationsVariant } from "./variants/OperationsVariant";
import { WorkbenchVariant } from "./variants/WorkbenchVariant";
import { SURFACES, VARIANTS, WORKSPACE, type SurfaceKey, type VariantKey, type ViewMode } from "./model";
const VARIANT_COMPONENTS = {
A: WorkbenchVariant,
B: InspectorVariant,
C: OperationsVariant,
} as const;
function readRoute(): SurfaceKey {
const route = new URLSearchParams(window.location.search).get("thoth_route") ?? "administration/workspace";
const surface = route.split("/").at(-1);
return SURFACES.some((item) => item.key === surface) ? surface as SurfaceKey : "workspace";
}
function readVariant(): VariantKey {
const candidate = new URLSearchParams(window.location.search).get("variant")?.toUpperCase();
return candidate === "B" || candidate === "C" ? candidate : "A";
}
function writeQuery(updates: Record<string, string | null>, mode: "push" | "replace" = "replace") {
const url = new URL(window.location.href);
Object.entries(updates).forEach(([key, value]) => {
if (value === null) url.searchParams.delete(key);
else url.searchParams.set(key, value);
});
window.history[mode === "push" ? "pushState" : "replaceState"]({}, "", url);
}
function isTypingTarget(target: EventTarget | null): boolean {
return target instanceof HTMLElement && target.matches("input, textarea, select, [contenteditable='true']");
}
function PortalChrome({
surface,
onSurface,
children,
}: {
surface: SurfaceKey;
onSurface: (surface: SurfaceKey) => void;
children: ReactNode;
}) {
const [mobilePortalNav, setMobilePortalNav] = useState(false);
return (
<div className="portal-frame">
<header className="portal-topbar">
<button className="mobile-menu-button" type="button" onClick={() => setMobilePortalNav((value) => !value)} aria-label="Open portal navigation">
<Menu size={18} />
</button>
<div className="portal-wordmark"><span className="portal-mark">C</span><span>Chirone</span><small>OMICS PORTAL</small></div>
<div className="portal-topbar-context"><span>Clinical operations</span><strong>Administration</strong></div>
<div className="portal-user"><span className="user-avatar">MP</span><span className="user-name">Marco P.</span><ChevronDown size={14} /></div>
</header>
<div className="portal-body">
<aside className={`portal-sidebar ${mobilePortalNav ? "portal-sidebar--open" : ""}`} aria-label="Omics Portal navigation">
<div className="sidebar-section-title">Workspace</div>
<button type="button" className="portal-nav-item"><LayoutList size={16} /> Dashboard</button>
<button type="button" className="portal-nav-item"><Table2 size={16} /> Patients</button>
<button type="button" className="portal-nav-item"><Activity size={16} /> Clinical data</button>
<div className="sidebar-section-title sidebar-section-title--spaced">Administration</div>
<button type="button" className="portal-nav-item portal-nav-item--active"><Settings2 size={16} /> ThothII</button>
<button type="button" className="portal-nav-item"><ShieldCheck size={16} /> Access control</button>
<div className="sidebar-spacer" />
<div className="portal-sidebar-footer"><span className="status-dot" /> Portal connected <small>v4.5</small></div>
</aside>
<main className="portal-main">
<div className="portal-breadcrumb"><span>Omics Portal</span><span>/</span><strong>ThothII administration</strong></div>
<div className="thoth-workspace">
<div className="thoth-content">{children}</div>
<aside className="thoth-admin-rail" aria-label="ThothII administration navigation">
<div className="rail-heading"><span className="rail-kicker">THOTHII</span><strong>Administration</strong></div>
<div className="rail-rule" />
{SURFACES.map((item) => (
<button key={item.key} type="button" className={`thoth-rail-item ${surface === item.key ? "thoth-rail-item--active" : ""}`} onClick={() => onSurface(item.key)} aria-current={surface === item.key ? "page" : undefined}>
<span className="rail-item-icon">{item.key === "workspace" ? <MonitorCog size={15} /> : item.key === "database" ? <Server size={15} /> : item.key === "memory" ? <Sparkles size={15} /> : item.key === "evidence" ? <FileCheck2 size={15} /> : <TerminalSquare size={15} />}</span>
<span><strong>{item.label}</strong><small>{item.hint}</small></span>
</button>
))}
<div className="rail-footer"><CircleHelp size={14} /><span>Need help?</span></div>
</aside>
</div>
</main>
</div>
</div>
);
}
function PrototypeSwitcher({ variant, onVariant }: { variant: VariantKey; onVariant: (variant: VariantKey) => void }) {
const index = VARIANTS.findIndex((item) => item.key === variant);
const cycle = (delta: number) => onVariant(VARIANTS[(index + delta + VARIANTS.length) % VARIANTS.length].key);
return (
<div className="prototype-switcher" aria-label="Prototype variant switcher">
<button type="button" onClick={() => cycle(-1)} aria-label="Previous variant"><ArrowLeft size={16} /></button>
<span><small>UI LAB · PROTOTYPE</small><strong>{variant} · {VARIANTS[index].name}</strong></span>
<button type="button" onClick={() => cycle(1)} aria-label="Next variant"><ArrowRight size={16} /></button>
</div>
);
}
export type VariantProps = {
surface: SurfaceKey;
mode: ViewMode;
notice: string | null;
onMode: (mode: ViewMode) => void;
onAction: (message: string) => void;
};
export function PrototypeApp() {
const [variant, setVariant] = useState<VariantKey>(readVariant);
const [surface, setSurface] = useState<SurfaceKey>(readRoute);
const [mode, setMode] = useState<ViewMode>("overview");
const [notice, setNotice] = useState<string | null>(null);
const [query, setQuery] = useState("");
const [mobileSearch, setMobileSearch] = useState(false);
const currentSurface = useMemo(() => SURFACES.find((item) => item.key === surface) ?? SURFACES[3], [surface]);
const Variant = VARIANT_COMPONENTS[variant];
useEffect(() => {
const onPopState = () => {
setVariant(readVariant());
setSurface(readRoute());
setMode("overview");
};
window.addEventListener("popstate", onPopState);
return () => window.removeEventListener("popstate", onPopState);
}, []);
useEffect(() => {
const onKeyDown = (event: KeyboardEvent) => {
if (isTypingTarget(event.target)) return;
if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
event.preventDefault();
const index = VARIANTS.findIndex((item) => item.key === variant);
const next = VARIANTS[(index + (event.key === "ArrowRight" ? 1 : -1) + VARIANTS.length) % VARIANTS.length].key;
setVariant(next);
writeQuery({ variant: next });
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [variant]);
useEffect(() => {
if (!notice) return;
const timeout = window.setTimeout(() => setNotice(null), 3200);
return () => window.clearTimeout(timeout);
}, [notice]);
const chooseVariant = (next: VariantKey) => { setVariant(next); writeQuery({ variant: next }); };
const chooseSurface = (next: SurfaceKey) => {
setSurface(next);
setMode("overview");
setNotice(null);
writeQuery({ thoth_route: `administration/${next}` }, "push");
};
const action = (message: string) => setNotice(message);
return (
<div className="prototype-root">
<PortalChrome surface={surface} onSurface={chooseSurface}>
<div className="prototype-meta-bar"><span className="prototype-label"><FlaskConical size={13} /> Prototype · same-document host</span><span className="prototype-meta-actions"><button type="button" onClick={() => setMobileSearch((value) => !value)} aria-label="Search"><Search size={15} /></button><button type="button" onClick={() => action("Workspace health is current")}>Health check</button><a href="?thoth_route=administration/workspace&variant=A"><ExternalLink size={13} /> Open link</a></span></div>
{mobileSearch && <div className="prototype-search-row"><Search size={15} /><input value={query} onChange={(event) => setQuery(event.target.value)} placeholder="Search administration" aria-label="Search administration" /></div>}
<div className="prototype-page-context"><span>Administration / {currentSurface.label}</span><span className="context-divider">•</span><span>{currentSurface.hint}</span></div>
<Variant surface={surface} mode={mode} notice={notice} onMode={setMode} onAction={action} />
</PortalChrome>
{notice && <div className="prototype-toast" role="status"><Check size={15} /> {notice}<button type="button" onClick={() => setNotice(null)} aria-label="Dismiss notification"><X size={14} /></button></div>}
<PrototypeSwitcher variant={variant} onVariant={chooseVariant} />
</div>
);
}
@@ -0,0 +1,25 @@
# Administration pages prototype
> Throwaway, development-only prototype. It does not call the backend and never persists changes.
Question being tested: **which shared page structure should replace the Workspace and Pi popups while
bringing Workspace, Evidence, Memory, Database and Pi into one responsive administration family?**
Run it from `frontend/`:
```bash
npm run prototype:administration-pages
```
Then open <http://127.0.0.1:5174/?thoth_route=administration/workspace&variant=A>.
The three variants are:
- `A`, Administration Workbench: a dense index beside a persistent detail surface.
- `B`, Administration Inspector: a reading-first page with an explicit configuration form and provenance column.
- `C`, Operations Deck: a status-led page with a full-width inventory and an activity console.
All variants simulate the same Workspace data, the same five administration links, and the same
read-only form interactions. Use the floating switcher or `?variant=A|B|C` to compare them. The
prototype is intentionally isolated from production routes and is expected to be deleted after the
winning structure is captured in the implementation specification.
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Administration pages prototype</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>
@@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { PrototypeApp } from "./App";
import "./prototype.css";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<PrototypeApp />
</StrictMode>,
);
@@ -0,0 +1,37 @@
export type VariantKey = "A" | "B" | "C";
export type SurfaceKey = "workspace" | "evidence" | "memory" | "database" | "pi";
export type ViewMode = "overview" | "edit";
export type WorkspaceRecord = {
id: string;
name: string;
description: string;
status: "Ready" | "Preprocessing" | "Needs configuration";
database: string;
revision: string;
updated: string;
};
export const WORKSPACE: WorkspaceRecord = {
id: "psd-clinical",
name: "PSD Clinical",
description: "Cardiology and genetics datamart for clinical review and research questions.",
status: "Ready",
database: "clinical-dwh",
revision: "a71c9f2",
updated: "Today, 09:42",
};
export const SURFACES: Array<{ key: SurfaceKey; label: string; hint: string }> = [
{ key: "database", label: "Database", hint: "Bindings and metadata" },
{ key: "memory", label: "Memory", hint: "Reusable decisions" },
{ key: "evidence", label: "Evidence", hint: "Curated domain context" },
{ key: "workspace", label: "Workspace", hint: "Scope and readiness" },
{ key: "pi", label: "Pi", hint: "Runtime management" },
];
export const VARIANTS: Array<{ key: VariantKey; name: string; bestFor: string }> = [
{ key: "A", name: "Workbench", bestFor: "fast list-to-detail work" },
{ key: "B", name: "Inspector", bestFor: "careful form review" },
{ key: "C", name: "Operations deck", bestFor: "status and diagnostics" },
];
@@ -0,0 +1,204 @@
:root {
--red: #cb333b;
--red-deep: #8f1204;
--red-soft: #f4dfe1;
--red-softer: #fbf0f0;
--paper: #fdfcfc;
--paper-strong: #fffefe;
--worktop: #f6f5f5;
--sunken: #ecebeb;
--ink: #2a2426;
--muted: #6b6568;
--border: #e3e0e1;
--border-strong: #c9c5c6;
--green: #277458;
--green-soft: #e3f0ea;
--blue: #446a87;
--blue-soft: #e9eff4;
--amber: #90621d;
--amber-soft: #f7edda;
--shadow-soft: 0 1px 2px rgb(42 36 38 / 0.05), 0 8px 24px rgb(42 36 38 / 0.04);
--shadow-float: 0 10px 30px rgb(42 36 38 / 0.14);
--sans: "Manrope", "Avenir Next", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--serif: "Fraunces", "Source Serif Pro", Georgia, serif;
--mono: "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
html, body, #root { min-height: 100%; margin: 0; }
body {
background: var(--paper);
color: var(--ink);
font-family: var(--sans);
font-size: 14px;
line-height: 1.5;
}
button, input, textarea, select { font: inherit; }
button { color: inherit; }
button:focus-visible, input:focus-visible, textarea:focus-visible, select:focus-visible { outline: 3px solid rgb(203 51 59 / 0.28); outline-offset: 2px; }
button:not(:disabled) { cursor: pointer; }
code { font-family: var(--mono); font-size: 0.86em; }
.prototype-root { min-height: 100vh; background: var(--paper); }
.portal-frame { min-height: 100vh; }
.portal-topbar {
align-items: center;
background: var(--red);
color: var(--paper);
display: flex;
gap: 22px;
height: 64px;
padding: 0 26px;
}
.portal-wordmark { align-items: center; display: flex; font-size: 17px; font-weight: 700; gap: 9px; letter-spacing: -0.01em; min-width: 205px; }
.portal-wordmark small { align-self: flex-end; font-family: var(--mono); font-size: 8px; font-weight: 700; letter-spacing: 0.09em; margin: 0 0 5px -3px; opacity: 0.78; }
.portal-mark { align-items: center; border: 1px solid rgb(253 252 252 / 0.72); border-radius: 50%; display: inline-flex; font-family: var(--serif); font-size: 15px; height: 26px; justify-content: center; width: 26px; }
.portal-topbar-context { border-left: 1px solid rgb(253 252 252 / 0.28); display: flex; flex-direction: column; gap: 1px; line-height: 1.2; padding-left: 22px; }
.portal-topbar-context span { font-size: 11px; opacity: 0.76; }
.portal-topbar-context strong { font-size: 13px; font-weight: 700; }
.portal-user { align-items: center; display: flex; gap: 8px; margin-left: auto; }
.user-avatar { align-items: center; background: rgb(253 252 252 / 0.18); border: 1px solid rgb(253 252 252 / 0.34); border-radius: 50%; display: inline-flex; font-size: 10px; font-weight: 700; height: 27px; justify-content: center; width: 27px; }
.user-name { font-size: 12px; font-weight: 600; }
.mobile-menu-button { background: transparent; border: 0; color: inherit; display: none; padding: 6px; }
.portal-body { display: flex; min-height: calc(100vh - 64px); }
.portal-sidebar { background: var(--worktop); border-right: 1px solid var(--border); display: flex; flex: 0 0 214px; flex-direction: column; padding: 24px 12px 18px; }
.sidebar-section-title { color: var(--muted); font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.1em; padding: 0 10px 8px; text-transform: uppercase; }
.sidebar-section-title--spaced { margin-top: 25px; }
.portal-nav-item { align-items: center; background: transparent; border: 0; border-radius: 6px; color: var(--muted); display: flex; font-size: 13px; gap: 10px; margin: 2px 0; padding: 10px; text-align: left; width: 100%; }
.portal-nav-item:hover { background: var(--sunken); color: var(--ink); }
.portal-nav-item--active { background: var(--red-soft); color: var(--red-deep); font-weight: 700; }
.sidebar-spacer { flex: 1; }
.portal-sidebar-footer { align-items: center; border-top: 1px solid var(--border); color: var(--muted); display: flex; font-size: 11px; gap: 6px; padding: 16px 10px 0; }
.portal-sidebar-footer small { font-family: var(--mono); margin-left: auto; }
.status-dot { background: var(--green); border-radius: 50%; height: 7px; width: 7px; }
.portal-main { background: var(--paper); flex: 1; min-width: 0; }
.portal-breadcrumb { align-items: center; color: var(--muted); display: flex; font-size: 11px; gap: 9px; padding: 19px 30px 0; }
.portal-breadcrumb strong { color: var(--ink); font-weight: 700; }
.thoth-workspace { display: flex; min-width: 0; }
.thoth-content { flex: 1; min-width: 0; padding: 19px 32px 64px; }
.thoth-admin-rail { border-left: 1px solid var(--border); flex: 0 0 184px; margin: 19px 24px 0 0; padding: 16px 0 0 16px; }
.rail-heading { display: flex; flex-direction: column; gap: 3px; padding: 0 10px 13px; }
.rail-heading strong { font-family: var(--serif); font-size: 17px; font-weight: 600; }
.rail-kicker, .section-kicker, .eyebrow, .prototype-label, .prototype-switcher small { color: var(--muted); font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.085em; text-transform: uppercase; }
.rail-rule { background: var(--border); height: 1px; margin: 0 10px 9px; }
.thoth-rail-item { align-items: flex-start; background: transparent; border: 0; border-radius: 7px; color: var(--muted); display: flex; gap: 9px; margin: 3px 0; padding: 9px 10px; text-align: left; width: 100%; }
.thoth-rail-item:hover { background: var(--sunken); color: var(--ink); }
.thoth-rail-item--active { background: var(--red-soft); color: var(--red-deep); }
.thoth-rail-item strong, .thoth-rail-item small { display: block; }
.thoth-rail-item strong { font-size: 12px; font-weight: 700; line-height: 1.25; }
.thoth-rail-item small { font-size: 10px; line-height: 1.3; margin-top: 3px; opacity: 0.78; }
.rail-item-icon { padding-top: 1px; }
.rail-footer { align-items: center; color: var(--muted); display: flex; font-size: 10px; gap: 7px; margin: 24px 10px; padding-top: 14px; border-top: 1px solid var(--border); }
.prototype-meta-bar { align-items: center; border-bottom: 1px solid var(--border); display: flex; justify-content: space-between; min-height: 30px; padding-bottom: 11px; }
.prototype-label { align-items: center; display: inline-flex; gap: 6px; }
.prototype-meta-actions { align-items: center; display: flex; gap: 13px; }
.prototype-meta-actions button, .prototype-meta-actions a { align-items: center; background: transparent; border: 0; color: var(--muted); display: inline-flex; font-size: 11px; gap: 5px; padding: 2px; text-decoration: none; }
.prototype-meta-actions button:hover, .prototype-meta-actions a:hover { color: var(--red-deep); }
.prototype-search-row { align-items: center; background: var(--worktop); border: 1px solid var(--border); display: flex; gap: 8px; margin-top: 12px; padding: 8px 11px; }
.prototype-search-row input { background: transparent; border: 0; min-width: 0; outline: 0; width: 100%; }
.prototype-page-context { align-items: center; color: var(--muted); display: flex; font-family: var(--mono); font-size: 10px; gap: 8px; padding: 20px 0 15px; }
.prototype-page-context span:first-child { color: var(--red-deep); font-weight: 700; }
.context-divider { color: var(--border-strong); }
.variant { max-width: 1050px; }
.page-heading { align-items: flex-start; display: flex; gap: 24px; justify-content: space-between; }
.page-heading h1 { font-family: var(--serif); font-size: 31px; font-weight: 600; letter-spacing: -0.025em; line-height: 1.06; margin: 7px 0 9px; }
.page-heading p { color: var(--muted); font-size: 13px; line-height: 1.6; margin: 0; max-width: 64ch; }
.page-heading--editorial h1 { font-size: 36px; }
.page-heading--editorial p { max-width: 58ch; }
.page-heading--operations h1 { margin-bottom: 7px; }
.heading-actions { align-items: center; display: flex; flex-wrap: wrap; gap: 8px; justify-content: flex-end; padding-top: 20px; }
.button { align-items: center; border: 1px solid var(--border-strong); border-radius: 6px; display: inline-flex; font-size: 12px; font-weight: 700; gap: 7px; justify-content: center; min-height: 34px; padding: 7px 12px; }
.button:hover { filter: brightness(0.98); transform: translateY(-1px); }
.button-primary { background: var(--red); border-color: var(--red); color: var(--paper); }
.button-primary:hover { background: var(--red-deep); border-color: var(--red-deep); }
.button-quiet { background: var(--paper-strong); color: var(--ink); }
.button-quiet:hover { background: var(--worktop); }
.icon-button { align-items: center; background: transparent; border: 1px solid transparent; border-radius: 6px; color: var(--muted); display: inline-flex; height: 30px; justify-content: center; padding: 5px; width: 30px; }
.icon-button:hover { background: var(--sunken); color: var(--ink); }
.icon-button--danger:hover { background: var(--red-soft); color: var(--red-deep); }
.inline-action { align-items: center; background: transparent; border: 0; color: var(--red-deep); display: inline-flex; font-size: 11px; font-weight: 700; gap: 4px; padding: 0; }
.inline-action:hover { text-decoration: underline; }
.workbench-status-line { align-items: center; border-bottom: 1px solid var(--border); color: var(--muted); display: flex; flex-wrap: wrap; font-size: 11px; gap: 10px; margin-top: 23px; padding-bottom: 14px; }
.status-badge { align-items: center; border-radius: 5px; display: inline-flex; font-size: 11px; font-weight: 700; gap: 6px; padding: 4px 8px; }
.status-badge--ready { background: var(--green-soft); color: var(--green); }
.status-separator { background: var(--border-strong); height: 13px; width: 1px; }
.workbench-status-line code { color: var(--ink); }
.workbench-toolbar { align-items: center; display: flex; gap: 8px; margin: 18px 0 12px; }
.search-control { align-items: center; background: var(--paper-strong); border: 1px solid var(--border); border-radius: 6px; color: var(--muted); display: flex; gap: 7px; min-height: 34px; padding: 0 10px; }
.search-control input { background: transparent; border: 0; color: var(--ink); min-width: 120px; outline: 0; width: 100%; }
.filter-count { align-items: center; background: var(--red-soft); border-radius: 8px; color: var(--red-deep); display: inline-flex; font-size: 10px; height: 17px; justify-content: center; min-width: 17px; }
.workbench-toolbar > :last-child { margin-left: auto; }
.workbench-grid { border: 1px solid var(--border); display: grid; grid-template-columns: minmax(220px, 0.74fr) minmax(0, 1.55fr); min-height: 465px; }
.workbench-index { background: var(--worktop); border-right: 1px solid var(--border); display: flex; flex-direction: column; padding: 18px 12px 0; }
.section-heading, .detail-heading, .detail-section-heading, .console-heading, .operations-toolbar { align-items: flex-start; display: flex; justify-content: space-between; }
.section-heading h2, .detail-heading h2, .operations-toolbar h2, .console-heading h2 { font-family: var(--serif); font-size: 21px; font-weight: 600; letter-spacing: -0.015em; margin: 4px 0 0; }
.section-heading h2 span { color: var(--muted); font-family: var(--mono); font-size: 11px; font-weight: 600; margin-left: 4px; }
.workspace-row { align-items: center; background: transparent; border: 1px solid transparent; border-radius: 7px; display: flex; gap: 9px; margin: 5px 0; padding: 11px 8px; text-align: left; }
.workspace-row:hover { background: var(--sunken); }
.workspace-row--selected { background: var(--paper-strong); border-color: var(--border); box-shadow: var(--shadow-soft); }
.workspace-row-mark { align-items: center; background: var(--red-soft); border-radius: 6px; color: var(--red-deep); display: inline-flex; height: 31px; justify-content: center; width: 31px; }
.workspace-row-mark--muted { background: var(--sunken); color: var(--muted); }
.workspace-row-copy { min-width: 0; }
.workspace-row-copy strong, .workspace-row-copy small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.workspace-row-copy strong { font-size: 12px; }
.workspace-row-copy small { color: var(--muted); font-size: 10px; margin-top: 2px; }
.row-status-dot { border-radius: 50%; height: 7px; margin-left: auto; width: 7px; }
.row-status-dot--ready { background: var(--green); }.row-status-dot--warning { background: var(--amber); }.row-status-dot--idle { background: var(--border-strong); }
.index-footer { border-top: 1px solid var(--border); color: var(--muted); display: flex; font-size: 10px; justify-content: space-between; margin-top: auto; padding: 14px 4px 16px; }
.workbench-detail { background: var(--paper-strong); padding: 21px 24px; }
.detail-lede { color: var(--muted); font-size: 13px; line-height: 1.65; margin: 16px 0 22px; max-width: 66ch; }
.fact-grid { border-bottom: 1px solid var(--border); border-top: 1px solid var(--border); display: grid; grid-template-columns: repeat(4, 1fr); margin: 0; padding: 16px 0; }
.fact-grid div { border-right: 1px solid var(--border); padding: 0 14px; }.fact-grid div:first-child { padding-left: 0; }.fact-grid div:last-child { border-right: 0; }
.fact-grid dt { color: var(--muted); font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; }.fact-grid dd { align-items: center; display: flex; font-size: 12px; font-weight: 700; gap: 5px; margin: 7px 0 0; }
.detail-section { margin-top: 23px; }.detail-section h3 { font-size: 14px; margin: 4px 0 0; }.detail-section p { color: var(--muted); font-size: 12px; margin: 10px 0 14px; }.detail-section-heading { align-items: center; }
.text-status { font-size: 11px; font-weight: 700; }.text-status--ready { color: var(--green); }
.progress-track { background: var(--sunken); height: 6px; overflow: hidden; }.progress-track span { background: var(--green); display: block; height: 100%; }.progress-meta { color: var(--muted); display: flex; font-family: var(--mono); font-size: 10px; justify-content: space-between; margin-top: 8px; }
.detail-actions { align-items: center; border-top: 1px solid var(--border); display: flex; flex-wrap: wrap; gap: 8px; margin-top: 27px; padding-top: 17px; }.detail-actions .icon-button { margin-left: auto; }
.variant-footnote { align-items: center; border-top: 1px solid var(--border); color: var(--muted); display: flex; font-size: 10px; justify-content: space-between; margin-top: 20px; padding-top: 13px; }.variant-footnote span { align-items: center; display: inline-flex; gap: 6px; }
.workspace-form { display: grid; gap: 17px; }.workspace-form label, .field-section label { color: var(--ink); display: grid; font-size: 11px; font-weight: 700; gap: 6px; }.workspace-form input, .workspace-form textarea, .workspace-form select, .field-section input, .field-section textarea, .field-section select { background: var(--paper); border: 1px solid var(--border-strong); border-radius: 5px; color: var(--ink); padding: 9px 10px; resize: vertical; width: 100%; }.workspace-form fieldset { border: 1px solid var(--border); margin: 0; padding: 13px; }.workspace-form legend { color: var(--muted); font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.07em; text-transform: uppercase; }.form-two-up { display: grid; gap: 12px; grid-template-columns: 1fr 1fr; }.checkbox-line { align-items: center; display: flex !important; font-weight: 500 !important; gap: 8px !important; margin: 9px 0; }.checkbox-line input { accent-color: var(--red); width: auto; }.muted-icon { color: var(--muted); margin-top: 4px; }
.record-state { align-items: flex-end; display: flex; flex-direction: column; gap: 7px; padding-top: 22px; }.record-state-label { color: var(--muted); font-family: var(--mono); font-size: 9px; letter-spacing: 0.06em; text-transform: uppercase; }
.inspector-tabs { border-bottom: 1px solid var(--border); display: flex; gap: 4px; margin-top: 24px; }.inspector-tab { background: transparent; border: 1px solid transparent; border-bottom: 0; border-radius: 7px 7px 0 0; color: var(--muted); font-size: 12px; font-weight: 700; padding: 10px 14px; }.inspector-tab:hover { background: var(--worktop); color: var(--ink); }.inspector-tab--active { background: var(--paper-strong); border-color: var(--border); color: var(--red-deep); }.inspector-tab span { font-family: var(--mono); font-size: 9px; margin-left: 4px; }
.inspector-layout { display: grid; gap: 28px; grid-template-columns: minmax(0, 1.3fr) minmax(220px, 0.68fr); padding-top: 28px; }.inspector-form-column { min-width: 0; }.inspector-section-intro h2 { font-family: var(--serif); font-size: 22px; font-weight: 600; margin: 6px 0 5px; }.inspector-section-intro p { color: var(--muted); font-size: 12px; line-height: 1.6; margin: 0; max-width: 59ch; }.field-section { display: grid; gap: 15px; margin-top: 20px; }.field-section input:disabled, .field-section textarea:disabled, .field-section select:disabled { background: var(--worktop); color: var(--ink); opacity: 1; }.inspector-divider { background: var(--border); height: 1px; margin: 28px 0; }.read-only-field { background: var(--worktop); border: 1px solid var(--border); display: grid; gap: 4px; padding: 10px; }.read-only-field span, .read-only-field small { color: var(--muted); font-size: 10px; }.read-only-field strong { align-items: center; color: var(--green); display: flex; font-size: 12px; gap: 6px; }.inspector-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 25px; }.inspector-context-column { display: flex; flex-direction: column; gap: 12px; }.context-panel { background: var(--worktop); border: 1px solid var(--border); padding: 15px; }.context-panel--accent { background: var(--red-softer); border-color: #edc4c6; }.context-panel--provenance { background: var(--paper-strong); }.context-panel-heading { align-items: center; color: var(--muted); display: flex; justify-content: space-between; margin-bottom: 10px; }.context-panel > strong { font-family: var(--mono); font-size: 13px; }.context-panel > p { color: var(--muted); font-size: 11px; line-height: 1.6; margin: 7px 0 12px; }.context-panel-meta { border-top: 1px solid rgb(143 18 4 / 0.14); display: flex; font-size: 10px; justify-content: space-between; margin-top: 14px; padding-top: 10px; }.context-panel-meta span { color: var(--muted); }.context-panel-meta strong { font-weight: 700; }.dependency-list { list-style: none; margin: 0; padding: 0; }.dependency-list li { align-items: flex-start; display: flex; gap: 8px; padding: 8px 0; }.dependency-list li + li { border-top: 1px solid var(--border); }.dependency-list svg { color: var(--green); flex: 0 0 auto; margin-top: 2px; }.dependency-list .warning-icon { color: var(--amber); }.dependency-list span { display: grid; font-size: 11px; font-weight: 700; gap: 2px; }.dependency-list small { color: var(--muted); font-size: 10px; font-weight: 400; }
.operations-readout { border-bottom: 1px solid var(--border); border-top: 1px solid var(--border); display: grid; grid-template-columns: repeat(4, 1fr); margin-top: 26px; }.readout-item { border-right: 1px solid var(--border); display: grid; gap: 4px; min-height: 100px; padding: 16px 15px; }.readout-item:first-child { padding-left: 0; }.readout-item:last-child { border-right: 0; }.readout-item > span { align-items: center; color: var(--muted); display: flex; font-family: var(--mono); font-size: 9px; font-weight: 700; gap: 5px; letter-spacing: 0.045em; text-transform: uppercase; }.readout-item strong { font-family: var(--serif); font-size: 22px; font-weight: 600; }.readout-item small { color: var(--muted); font-size: 10px; }.readout-item--ready > span, .readout-item--ready strong { color: var(--green); }
.operations-toolbar { align-items: end; margin-top: 28px; }.operations-toolbar h2 { font-size: 23px; }.toolbar-actions { align-items: center; display: flex; gap: 7px; }.operations-table-wrap { border: 1px solid var(--border); margin-top: 13px; overflow-x: auto; }.operations-table { border-collapse: collapse; min-width: 680px; width: 100%; }.operations-table th { background: var(--worktop); color: var(--muted); font-family: var(--mono); font-size: 9px; font-weight: 700; letter-spacing: 0.06em; padding: 11px 12px; text-align: left; text-transform: uppercase; }.operations-table td { border-top: 1px solid var(--border); font-size: 11px; padding: 13px 12px; vertical-align: middle; }.operations-table tbody tr:hover { background: var(--red-softer); }.service-cell { align-items: center; display: flex; gap: 9px; }.service-cell strong, .service-cell small { display: block; }.service-cell strong { font-size: 12px; }.service-cell small { color: var(--muted); font-size: 10px; margin-top: 2px; }.service-icon { align-items: center; background: var(--green-soft); border-radius: 6px; color: var(--green); display: inline-flex; height: 31px; justify-content: center; width: 31px; }.service-icon--blue { background: var(--blue-soft); color: var(--blue); }.service-icon--amber { background: var(--amber-soft); color: var(--amber); }.service-icon--red { background: var(--red-soft); color: var(--red-deep); }.table-state { align-items: center; display: inline-flex; font-weight: 700; gap: 5px; }.table-state--ready { color: var(--green); }.table-state--warning { color: var(--amber); }
.operations-console { border: 1px solid var(--border); margin-top: 18px; }.console-heading { align-items: center; background: var(--worktop); border-bottom: 1px solid var(--border); padding: 14px 15px; }.console-heading h2 { font-size: 18px; }.activity-list { padding: 2px 15px; }.activity-row { align-items: center; border-bottom: 1px solid var(--border); display: grid; gap: 10px; grid-template-columns: 8px 58px minmax(0, 1fr) 30px; padding: 12px 0; }.activity-marker { border-radius: 50%; height: 7px; width: 7px; }.activity-marker--success { background: var(--green); }.activity-marker--warning { background: var(--amber); }.activity-marker--info { background: var(--blue); }.activity-row time { color: var(--muted); font-family: var(--mono); font-size: 10px; }.activity-copy strong, .activity-copy small { display: block; }.activity-copy strong { font-size: 11px; }.activity-copy small { color: var(--muted); font-size: 10px; margin-top: 2px; }.console-footer { align-items: center; display: flex; justify-content: space-between; padding: 12px 15px; }.console-footer > span { align-items: center; color: var(--muted); display: inline-flex; font-size: 10px; gap: 6px; }
.prototype-switcher { align-items: center; background: var(--ink); border: 1px solid rgb(253 252 252 / 0.12); border-radius: 11px; bottom: 18px; box-shadow: var(--shadow-float); color: var(--paper); display: flex; gap: 3px; left: 50%; padding: 5px; position: fixed; transform: translateX(-50%); z-index: 20; }.prototype-switcher button { align-items: center; background: transparent; border: 0; border-radius: 7px; color: var(--paper); display: inline-flex; height: 34px; justify-content: center; width: 34px; }.prototype-switcher button:hover { background: rgb(253 252 252 / 0.14); }.prototype-switcher span { display: flex; flex-direction: column; gap: 1px; min-width: 123px; padding: 0 7px; }.prototype-switcher small { color: rgb(253 252 252 / 0.58); font-size: 8px; }.prototype-switcher strong { font-size: 11px; }
.prototype-toast { align-items: center; background: var(--ink); border-radius: 7px; bottom: 82px; box-shadow: var(--shadow-float); color: var(--paper); display: flex; font-size: 12px; gap: 8px; left: 50%; padding: 10px 12px; position: fixed; transform: translateX(-50%); z-index: 21; }.prototype-toast > svg { color: #9ed9bd; }.prototype-toast button { background: transparent; border: 0; color: var(--paper); display: inline-flex; padding: 2px; }
@media (max-width: 1080px) {
.portal-sidebar { flex-basis: 186px; }
.thoth-admin-rail { flex-basis: 165px; margin-right: 15px; }
.thoth-content { padding-left: 24px; padding-right: 24px; }
.fact-grid { grid-template-columns: repeat(2, 1fr); row-gap: 14px; }.fact-grid div:nth-child(2) { border-right: 0; }.fact-grid div:nth-child(3) { border-top: 1px solid var(--border); padding-left: 0; padding-top: 14px; }.fact-grid div:nth-child(4) { border-top: 1px solid var(--border); padding-top: 14px; }
.inspector-layout { grid-template-columns: minmax(0, 1fr) 220px; }
}
@media (max-width: 820px) {
.portal-topbar { gap: 12px; padding: 0 16px; }.portal-wordmark { min-width: auto; }.portal-wordmark small, .portal-topbar-context, .user-name { display: none; }.mobile-menu-button { display: inline-flex; }.portal-user { margin-left: auto; }
.portal-sidebar { display: none; left: 0; position: fixed; top: 64px; z-index: 15; }.portal-sidebar--open { display: flex; height: calc(100vh - 64px); width: 220px; }
.portal-breadcrumb { padding-left: 20px; padding-right: 20px; }.thoth-content { padding-left: 20px; padding-right: 20px; }.thoth-admin-rail { border-left: 0; border-top: 1px solid var(--border); flex-basis: auto; margin: 0 20px; padding: 15px 0 0; }.thoth-workspace { flex-direction: column; }.rail-heading { padding-left: 0; }.rail-rule { margin-left: 0; }.thoth-rail-item { display: inline-flex; margin-right: 4px; vertical-align: top; width: calc(20% - 6px); }.thoth-rail-item small { display: none; }.thoth-rail-item strong { font-size: 11px; }.rail-footer { display: none; }
.page-heading { flex-direction: column; }.heading-actions { justify-content: flex-start; padding-top: 3px; }.page-heading--editorial h1 { font-size: 31px; }.workbench-grid { grid-template-columns: 1fr; }.workbench-index { border-bottom: 1px solid var(--border); border-right: 0; }.workbench-detail { min-height: 430px; }.index-footer { margin-top: 15px; }.inspector-layout { grid-template-columns: 1fr; }.inspector-context-column { display: grid; grid-template-columns: 1fr 1fr; }.context-panel--provenance { grid-column: 1 / -1; }.record-state { align-items: flex-start; padding-top: 0; }.operations-readout { grid-template-columns: repeat(2, 1fr); }.readout-item:nth-child(2) { border-right: 0; }.readout-item:nth-child(3), .readout-item:nth-child(4) { border-top: 1px solid var(--border); }.readout-item:nth-child(3) { padding-left: 0; }.operations-toolbar { align-items: flex-start; flex-direction: column; gap: 12px; }.toolbar-actions { width: 100%; }.toolbar-actions .search-control { flex: 1; }.console-footer { align-items: flex-start; flex-direction: column; gap: 10px; }
}
@media (max-width: 560px) {
.portal-topbar { height: 56px; }.portal-body { min-height: calc(100vh - 56px); }.portal-sidebar { top: 56px; }.portal-breadcrumb { font-size: 10px; padding-top: 13px; }.thoth-content { padding: 14px 14px 78px; }.thoth-admin-rail { margin: 0 14px; }.thoth-rail-item { flex: 1 1 30%; width: auto; }.thoth-rail-item strong { font-size: 10px; }.prototype-meta-actions button:not(:first-child), .prototype-meta-actions a { display: none; }.prototype-page-context { font-size: 9px; padding-top: 15px; }.page-heading h1 { font-size: 27px; }.page-heading--editorial h1 { font-size: 30px; }.page-heading p { font-size: 12px; }.heading-actions { width: 100%; }.heading-actions .button { flex: 1; }.workbench-toolbar { align-items: stretch; flex-wrap: wrap; }.workbench-toolbar .search-control { flex: 1 1 100%; }.workbench-toolbar > :last-child { margin-left: 0; }.workbench-toolbar .button { flex: 1; }.fact-grid { grid-template-columns: 1fr; }.fact-grid div, .fact-grid div:nth-child(2), .fact-grid div:nth-child(3), .fact-grid div:nth-child(4) { border-bottom: 1px solid var(--border); border-right: 0; border-top: 0; padding: 0 0 10px; }.fact-grid div:last-child { border-bottom: 0; padding-bottom: 0; }.form-two-up { grid-template-columns: 1fr; }.inspector-tabs { overflow-x: auto; }.inspector-tab { white-space: nowrap; }.inspector-context-column { display: flex; }.operations-readout { grid-template-columns: 1fr 1fr; }.readout-item { min-height: 90px; padding: 12px 10px; }.readout-item:first-child, .readout-item:nth-child(3) { padding-left: 0; }.readout-item strong { font-size: 19px; }.prototype-switcher { bottom: 12px; }.prototype-toast { bottom: 72px; max-width: calc(100vw - 28px); text-align: center; }.variant-footnote { align-items: flex-start; flex-direction: column; gap: 8px; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { scroll-behavior: auto !important; transition-duration: 0.01ms !important; }
.button:hover { transform: none; }
}
@@ -0,0 +1,32 @@
import { CheckCircle2, ChevronRight, CircleAlert, Edit3, FileText, KeyRound, LockKeyhole, Save, Server, ShieldCheck, Sparkles } from "lucide-react";
import type { VariantProps } from "../App";
import { WORKSPACE } from "../model";
export function InspectorVariant({ surface, mode, onMode, onAction }: VariantProps) {
return (
<div className="variant variant-inspector">
<header className="page-heading page-heading--editorial">
<div><span className="eyebrow">ThothII / configuration record</span><h1>{WORKSPACE.name}</h1><p>Workspace identity and scope, presented as a document you can verify before changing.</p></div>
<div className="record-state"><span className="status-badge status-badge--ready"><CheckCircle2 size={14} /> Ready</span><span className="record-state-label">Catalog record</span></div>
</header>
<nav className="inspector-tabs" aria-label="Workspace sections"><button className="inspector-tab inspector-tab--active" type="button">Configuration</button><button className="inspector-tab" type="button" onClick={() => onAction("Evidence section selected")}>Evidence <span>86</span></button><button className="inspector-tab" type="button" onClick={() => onAction("Activity section selected")}>Activity <span>12</span></button></nav>
<div className="inspector-layout">
<section className="inspector-form-column">
<div className="inspector-section-intro"><span className="section-kicker">01 · Identity</span><h2>Workspace configuration</h2><p>These values identify the analytical scope available to the guided SQL workflow.</p></div>
<div className="field-section"><label>Display name<input defaultValue={WORKSPACE.name} disabled={mode !== "edit"} /></label><label>Purpose<textarea defaultValue={WORKSPACE.description} rows={4} disabled={mode !== "edit"} /></label></div>
<div className="inspector-divider" />
<div className="inspector-section-intro"><span className="section-kicker">02 · Runtime binding</span><h2>Database and readiness</h2><p>Runtime values are resolved from the installation catalog and checked before a session starts.</p></div>
<div className="field-section"><label>Database binding<select defaultValue={WORKSPACE.database} disabled={mode !== "edit"}><option>clinical-dwh</option><option>genetics-dwh</option></select></label><div className="read-only-field"><span>Preprocessing state</span><strong><CheckCircle2 size={15} /> Ready</strong><small>Last completed today at 09:42</small></div></div>
<div className="inspector-actions"><button className="button button-primary" type="button" onClick={() => { if (mode === "edit") { onMode("overview"); onAction("Workspace changes saved in prototype only"); } else onMode("edit"); }}><Edit3 size={15} /> {mode === "edit" ? "Save changes" : "Edit workspace"}</button><button className="button button-quiet" type="button" onClick={() => onAction("Configuration validation passed")}><ShieldCheck size={15} /> Validate</button></div>
</section>
<aside className="inspector-context-column">
<div className="context-panel context-panel--accent"><div className="context-panel-heading"><span className="section-kicker">Current record</span><KeyRound size={15} /></div><strong>{WORKSPACE.id}</strong><p>Revision <code>{WORKSPACE.revision}</code></p><div className="context-panel-meta"><span>Updated</span><strong>{WORKSPACE.updated}</strong></div></div>
<div className="context-panel"><div className="context-panel-heading"><span className="section-kicker">Scope promise</span><Sparkles size={15} /></div><p>Answers questions about patients, samples, genetics and clinical events in the PSD context.</p><button className="inline-action" type="button" onClick={() => onAction("Scope document opened")}>Read scope document <ChevronRight size={13} /></button></div>
<div className="context-panel"><div className="context-panel-heading"><span className="section-kicker">Dependencies</span><Server size={15} /></div><ul className="dependency-list"><li><CheckCircle2 size={14} /><span>Database binding<small>clinical-dwh · reachable</small></span></li><li><CheckCircle2 size={14} /><span>Reference vectors<small>86 evidence units</small></span></li><li><CircleAlert size={14} className="warning-icon" /><span>Memory index<small>2 updates pending</small></span></li></ul></div>
<div className="context-panel context-panel--provenance"><div className="context-panel-heading"><span className="section-kicker">Provenance</span><FileText size={15} /></div><p>Registry revision <code>{WORKSPACE.revision}</code> came from the installation model catalog.</p><button className="inline-action" type="button" onClick={() => onAction("Provenance copied")}>Copy provenance</button></div>
</aside>
</div>
<footer className="variant-footnote"><span><LockKeyhole size={14} /> Secrets are never displayed in this page</span><button className="inline-action" type="button" onClick={() => onAction("Security policy opened")}>Security policy <ChevronRight size={13} /></button></footer>
</div>
);
}
@@ -0,0 +1,22 @@
import { Activity, AlertTriangle, CheckCircle2, ChevronRight, CircleDot, Clock3, Database, MoreHorizontal, Play, RefreshCw, Search, Server, ShieldCheck, TerminalSquare, XCircle } from "lucide-react";
import type { VariantProps } from "../App";
import { WORKSPACE } from "../model";
const activity = [
["09:42", "Preprocessing completed", "4 stages · 86 evidence units", "success"],
["09:18", "Memory index update pending", "2 cards waiting for retry", "warning"],
["Yesterday", "Database connection tested", "clinical-dwh · 184 ms", "info"],
] as const;
export function OperationsVariant({ surface, onAction }: VariantProps) {
return (
<div className="variant variant-operations">
<header className="page-heading page-heading--operations"><div><span className="eyebrow">Administration operations · {surface}</span><h1>Workspace operations</h1><p>One operational view for readiness, catalog state and actions that affect the next session.</p></div><div className="heading-actions"><button className="button button-quiet" type="button" onClick={() => onAction("Operation history refreshed")}><RefreshCw size={15} /> Refresh</button><button className="button button-primary" type="button" onClick={() => onAction("Preprocessing started in prototype only")}><Play size={15} /> Run preprocessing</button></div></header>
<div className="operations-readout"><div className="readout-item readout-item--ready"><span><CircleDot size={14} /> Overall state</span><strong>Ready</strong><small>safe to start a session</small></div><div className="readout-item"><span><Database size={14} /> Catalog revision</span><strong>{WORKSPACE.revision}</strong><small>captured today at 09:42</small></div><div className="readout-item"><span><Activity size={14} /> Pending actions</span><strong>2</strong><small>memory index updates</small></div><div className="readout-item"><span><Clock3 size={14} /> Last operation</span><strong>184 ms</strong><small>database connection test</small></div></div>
<div className="operations-toolbar"><div><span className="section-kicker">Resource inventory</span><h2>Workspace services</h2></div><div className="toolbar-actions"><div className="search-control"><Search size={15} /><input placeholder="Search services" aria-label="Search services" /></div><button className="icon-button" type="button" aria-label="Filter services"><MoreHorizontal size={17} /></button></div></div>
<div className="operations-table-wrap"><table className="operations-table"><thead><tr><th>Service</th><th>State</th><th>Last change</th><th>Owner</th><th aria-label="Actions" /></tr></thead><tbody><tr><td><span className="service-cell"><span className="service-icon"><Server size={16} /></span><span><strong>Database binding</strong><small>clinical-dwh · PostgreSQL</small></span></span></td><td><span className="table-state table-state--ready"><CheckCircle2 size={14} /> Ready</span></td><td>Today, 09:42</td><td>Catalog</td><td><button className="icon-button" type="button" aria-label="Open database binding" onClick={() => onAction("Database binding opened")}><ChevronRight size={16} /></button></td></tr><tr><td><span className="service-cell"><span className="service-icon service-icon--blue"><ShieldCheck size={16} /></span><span><strong>Reference index</strong><small>86 published evidence units</small></span></span></td><td><span className="table-state table-state--ready"><CheckCircle2 size={14} /> Current</span></td><td>Today, 09:42</td><td>Evidence</td><td><button className="icon-button" type="button" aria-label="Open reference index" onClick={() => onAction("Reference index opened")}><ChevronRight size={16} /></button></td></tr><tr><td><span className="service-cell"><span className="service-icon service-icon--amber"><TerminalSquare size={16} /></span><span><strong>Memory index</strong><small>2 updates waiting for retry</small></span></span></td><td><span className="table-state table-state--warning"><AlertTriangle size={14} /> Attention</span></td><td>Today, 09:18</td><td>Memory</td><td><button className="icon-button" type="button" aria-label="Open memory index" onClick={() => onAction("Memory index opened")}><ChevronRight size={16} /></button></td></tr><tr><td><span className="service-cell"><span className="service-icon service-icon--red"><TerminalSquare size={16} /></span><span><strong>Pi runtime</strong><small>GLM 5.3 · medium thinking</small></span></span></td><td><span className="table-state table-state--ready"><CheckCircle2 size={14} /> Available</span></td><td>Yesterday, 16:05</td><td>Installation</td><td><button className="icon-button" type="button" aria-label="Open Pi runtime" onClick={() => onAction("Pi runtime opened")}><ChevronRight size={16} /></button></td></tr></tbody></table></div>
<section className="operations-console" aria-labelledby="activity-heading"><div className="console-heading"><div><span className="section-kicker">Activity console</span><h2 id="activity-heading">Recent operations</h2></div><button className="inline-action" type="button" onClick={() => onAction("Full operation history opened")}>View full history <ChevronRight size={13} /></button></div><div className="activity-list">{activity.map(([time, title, detail, state]) => <div className="activity-row" key={title}><span className={`activity-marker activity-marker--${state}`} /> <time>{time}</time><span className="activity-copy"><strong>{title}</strong><small>{detail}</small></span><button className="icon-button" type="button" aria-label={`Open ${title}`} onClick={() => onAction(`${title} opened`)}><ChevronRight size={16} /></button></div>)}</div><div className="console-footer"><span><ShieldCheck size={14} /> Audit events are retained by the installation catalog</span><button className="button button-quiet" type="button" onClick={() => onAction("Operation retry requested")}><RefreshCw size={14} /> Retry pending updates</button></div></section>
<footer className="variant-footnote"><span><TerminalSquare size={14} /> Operational actions are deliberately explicit</span><button className="inline-action" type="button" onClick={() => onAction("Runbook opened")}>Open runbook <ChevronRight size={13} /></button></footer>
</div>
);
}
@@ -0,0 +1,42 @@
import { ArrowUpRight, CheckCircle2, ChevronRight, Database, Edit3, Filter, GitBranch, LockKeyhole, MoreHorizontal, Plus, RefreshCw, Search, ShieldCheck, Trash2 } from "lucide-react";
import type { VariantProps } from "../App";
import { WORKSPACE } from "../model";
export function WorkbenchVariant({ surface, mode, onMode, onAction }: VariantProps) {
return (
<div className="variant variant-workbench">
<header className="page-heading page-heading--split">
<div>
<span className="eyebrow">Administration surface · {surface}</span>
<h1>{surface === "workspace" ? "Workspace management" : `${surface[0].toUpperCase()}${surface.slice(1)} management`}</h1>
<p>Keep the installation scope, its supporting knowledge, and runtime readiness in one inspectable place.</p>
</div>
<div className="heading-actions"><button className="button button-quiet" type="button" onClick={() => onAction("Workspace health is current")}><RefreshCw size={15} /> Check status</button><button className="button button-primary" type="button" onClick={() => onMode(mode === "edit" ? "overview" : "edit")}><Edit3 size={15} /> {mode === "edit" ? "Close editor" : "Edit workspace"}</button></div>
</header>
<div className="workbench-status-line"><span className="status-badge status-badge--ready"><CheckCircle2 size={14} /> Ready</span><span>Last updated {WORKSPACE.updated}</span><span className="status-separator" /><span>Revision <code>{WORKSPACE.revision}</code></span><button className="inline-action" type="button" onClick={() => onAction("Revision copied to clipboard")}>Copy revision</button></div>
<div className="workbench-toolbar"><div className="search-control"><Search size={15} /><input aria-label="Filter workspaces" placeholder="Filter workspaces" /></div><button className="button button-quiet" type="button"><Filter size={15} /> Filters <span className="filter-count">1</span></button><button className="button button-quiet" type="button"><Plus size={15} /> Add workspace</button></div>
<div className="workbench-grid">
<section className="workbench-index" aria-labelledby="workspaces-heading">
<div className="section-heading"><div><span className="section-kicker">Registry</span><h2 id="workspaces-heading">Workspaces <span>3</span></h2></div><button className="icon-button" type="button" aria-label="Workspace actions"><MoreHorizontal size={17} /></button></div>
<button className="workspace-row workspace-row--selected" type="button"><span className="workspace-row-mark"><Database size={16} /></span><span className="workspace-row-copy"><strong>{WORKSPACE.name}</strong><small>{WORKSPACE.database}</small></span><span className="row-status-dot row-status-dot--ready" /></button>
<button className="workspace-row" type="button"><span className="workspace-row-mark workspace-row-mark--muted"><Database size={16} /></span><span className="workspace-row-copy"><strong>Genetics archive</strong><small>genetics-dwh</small></span><span className="row-status-dot row-status-dot--warning" /></button>
<button className="workspace-row" type="button"><span className="workspace-row-mark workspace-row-mark--muted"><Database size={16} /></span><span className="workspace-row-copy"><strong>Research sandbox</strong><small>research-dwh</small></span><span className="row-status-dot row-status-dot--idle" /></button>
<div className="index-footer"><span>Showing 3 workspaces</span><button className="inline-action" type="button">Manage registry <ArrowUpRight size={13} /></button></div>
</section>
<section className="workbench-detail" aria-labelledby="workspace-detail-heading">
{mode === "overview" ? <>
<div className="detail-heading"><div><span className="section-kicker">Selected workspace</span><h2 id="workspace-detail-heading">{WORKSPACE.name}</h2></div><button className="icon-button" type="button" aria-label="More workspace actions"><MoreHorizontal size={17} /></button></div>
<p className="detail-lede">{WORKSPACE.description}</p>
<dl className="fact-grid"><div><dt>Database</dt><dd><Database size={14} /> {WORKSPACE.database}</dd></div><div><dt>Scope state</dt><dd><ShieldCheck size={14} /> Approved</dd></div><div><dt>Evidence</dt><dd><GitBranch size={14} /> 86 published units</dd></div><div><dt>Schema index</dt><dd><CheckCircle2 size={14} /> Current</dd></div></dl>
<div className="detail-section"><div className="detail-section-heading"><div><span className="section-kicker">Operational readiness</span><h3>Preprocessing</h3></div><span className="text-status text-status--ready">Ready</span></div><p>Schema, relationships and reference vectors were built from the current catalog revision.</p><div className="progress-track"><span style={{ width: "100%" }} /></div><div className="progress-meta"><span>4 of 4 stages complete</span><code>{WORKSPACE.revision}</code></div></div>
<div className="detail-actions"><button className="button button-primary" type="button" onClick={() => onMode("edit")}><Edit3 size={15} /> Edit configuration</button><button className="button button-quiet" type="button" onClick={() => onAction("Workspace details copied")}>Copy details</button><button className="icon-button icon-button--danger" type="button" aria-label="Delete workspace" onClick={() => onAction("Delete requires a deliberate confirmation") }><Trash2 size={16} /></button></div>
</> : <form className="workspace-form" onSubmit={(event) => { event.preventDefault(); onMode("overview"); onAction("Workspace changes saved in prototype only"); }}>
<div className="detail-heading"><div><span className="section-kicker">Editing workspace</span><h2 id="workspace-detail-heading">{WORKSPACE.name}</h2></div><LockKeyhole size={17} className="muted-icon" /></div>
<label>Display name<input defaultValue={WORKSPACE.name} /></label><label>Workspace description<textarea defaultValue={WORKSPACE.description} rows={3} /></label><div className="form-two-up"><label>Database binding<select defaultValue={WORKSPACE.database}><option>clinical-dwh</option><option>genetics-dwh</option></select></label><label>Revision<input defaultValue={WORKSPACE.revision} readOnly /></label></div><fieldset><legend>Capabilities</legend><label className="checkbox-line"><input type="checkbox" defaultChecked /> Schema linking</label><label className="checkbox-line"><input type="checkbox" defaultChecked /> Evidence retrieval</label><label className="checkbox-line"><input type="checkbox" defaultChecked /> Memory retrieval</label></fieldset><div className="detail-actions"><button className="button button-primary" type="submit"><Save size={15} /> Save workspace</button><button className="button button-quiet" type="button" onClick={() => onMode("overview")}>Cancel</button></div>
</form>}
</section>
</div>
<footer className="variant-footnote"><span><ShieldCheck size={14} /> Changes are persisted by the installation catalog</span><button className="inline-action" type="button" onClick={() => onAction("Audit history is available in the catalog")}>View audit history <ChevronRight size={13} /></button></footer>
</div>
);
}
@@ -0,0 +1,20 @@
import path from "path";
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig(({ command }) => {
if (command !== "serve") {
throw new Error("The administration-pages prototype is dev-only. Run npm run prototype:administration-pages.");
}
return {
root: path.resolve(__dirname, "prototypes/administration-pages"),
plugins: [react()],
appType: "spa",
server: {
host: "127.0.0.1",
port: 5174,
strictPort: true,
},
};
});