diff --git a/CONTEXT.md b/CONTEXT.md index 58f1d73c..116056e1 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -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. diff --git a/docs/adr/0020-unify-administration-pages-and-use-namespaced-routes.md b/docs/adr/0020-unify-administration-pages-and-use-namespaced-routes.md new file mode 100644 index 00000000..0f3be97e --- /dev/null +++ b/docs/adr/0020-unify-administration-pages-and-use-namespaced-routes.md @@ -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/`, +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. diff --git a/frontend/package.json b/frontend/package.json index 41b69b4c..911ee76f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -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", diff --git a/frontend/prototypes/administration-pages/App.tsx b/frontend/prototypes/administration-pages/App.tsx new file mode 100644 index 00000000..fe5ac0d5 --- /dev/null +++ b/frontend/prototypes/administration-pages/App.tsx @@ -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, 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 ( +
+
+ +
CChironeOMICS PORTAL
+
Clinical operationsAdministration
+
MPMarco P.
+
+
+ +
+
Omics Portal/ThothII administration
+
+
{children}
+ +
+
+
+
+ ); +} + +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 ( +
+ + UI LAB · PROTOTYPE{variant} · {VARIANTS[index].name} + +
+ ); +} + +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(readVariant); + const [surface, setSurface] = useState(readRoute); + const [mode, setMode] = useState("overview"); + const [notice, setNotice] = useState(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 ( +
+ +
Prototype · same-document host Open link
+ {mobileSearch &&
setQuery(event.target.value)} placeholder="Search administration" aria-label="Search administration" />
} +
Administration / {currentSurface.label}•{currentSurface.hint}
+ +
+ {notice &&
{notice}
} + +
+ ); +} diff --git a/frontend/prototypes/administration-pages/README.md b/frontend/prototypes/administration-pages/README.md new file mode 100644 index 00000000..e5de3d13 --- /dev/null +++ b/frontend/prototypes/administration-pages/README.md @@ -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 . + +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. diff --git a/frontend/prototypes/administration-pages/index.html b/frontend/prototypes/administration-pages/index.html new file mode 100644 index 00000000..d961f302 --- /dev/null +++ b/frontend/prototypes/administration-pages/index.html @@ -0,0 +1,12 @@ + + + + + + Administration pages prototype + + +
+ + + diff --git a/frontend/prototypes/administration-pages/main.tsx b/frontend/prototypes/administration-pages/main.tsx new file mode 100644 index 00000000..b861aad5 --- /dev/null +++ b/frontend/prototypes/administration-pages/main.tsx @@ -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( + + + , +); diff --git a/frontend/prototypes/administration-pages/model.ts b/frontend/prototypes/administration-pages/model.ts new file mode 100644 index 00000000..fae8ee16 --- /dev/null +++ b/frontend/prototypes/administration-pages/model.ts @@ -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" }, +]; diff --git a/frontend/prototypes/administration-pages/prototype.css b/frontend/prototypes/administration-pages/prototype.css new file mode 100644 index 00000000..4080e829 --- /dev/null +++ b/frontend/prototypes/administration-pages/prototype.css @@ -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; } +} diff --git a/frontend/prototypes/administration-pages/variants/InspectorVariant.tsx b/frontend/prototypes/administration-pages/variants/InspectorVariant.tsx new file mode 100644 index 00000000..a0f9cf42 --- /dev/null +++ b/frontend/prototypes/administration-pages/variants/InspectorVariant.tsx @@ -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 ( +
+
+
ThothII / configuration record

{WORKSPACE.name}

Workspace identity and scope, presented as a document you can verify before changing.

+
ReadyCatalog record
+
+ +
+
+
01 · Identity

Workspace configuration

These values identify the analytical scope available to the guided SQL workflow.

+