prototype unified administration page layouts
This commit is contained in:
+22
@@ -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.
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
Reference in New Issue
Block a user