feat: add UI conformance and launch context

This commit is contained in:
2026-08-18 21:32:25 +02:00
parent 7685a103e8
commit 8a925782ab
35 changed files with 1125 additions and 150 deletions
+2
View File
@@ -149,6 +149,8 @@ webui/.module-test-build/
webui/.policy-test-build/ webui/.policy-test-build/
webui/.template-preview-test-build/ webui/.template-preview-test-build/
webui/.import-test-build/ webui/.import-test-build/
webui/dist-conformance/
webui/test-results/
# Security audit reports # Security audit reports
audit-reports/ audit-reports/
+210
View File
@@ -0,0 +1,210 @@
import { useMemo, useState } from "react";
import { FileText, GitBranch, Inbox, Search, ShieldCheck } from "lucide-react";
import { useLocation } from "react-router";
import ActionToolbar, { ToolbarGroup } from "../src/components/ActionToolbar";
import Button from "../src/components/Button";
import Card from "../src/components/Card";
import ContentGrid, { FormGrid } from "../src/components/ContentGrid";
import ContentSection from "../src/components/ContentSection";
import CountBadge from "../src/components/CountBadge";
import DefinitionNodeIcon from "../src/components/DefinitionNodeIcon";
import DefinitionPalette, { DefinitionPaletteGroup, DefinitionPaletteItem } from "../src/components/DefinitionPalette";
import DescriptionList, { DescriptionItem } from "../src/components/DescriptionList";
import Dialog from "../src/components/Dialog";
import { DialogForm, DialogSection } from "../src/components/DialogAnatomy";
import FilterBar from "../src/components/FilterBar";
import FloatingStatus from "../src/components/FloatingStatus";
import FormSection from "../src/components/FormSection";
import MetricCard from "../src/components/MetricCard";
import MetricGrid from "../src/components/MetricGrid";
import PageLayout from "../src/components/PageLayout";
import SelectionList, { SelectionListItem, SelectionListItemContent } from "../src/components/SelectionList";
import StatePanel from "../src/components/StatePanel";
import WorkspaceFrame from "../src/components/WorkspaceFrame";
import WorkspaceLayout from "../src/components/WorkspaceLayout";
import BreadcrumbBar from "../src/layout/BreadcrumbBar";
import { useGuardedNavigate } from "../src/components/UnsavedChangesGuard";
import {
createQuickAccessLaunchContext,
quickAccessLaunchState
} from "../src/platform/launchContext";
import type { AuthInfo } from "../src/types";
export default function ConformanceApp() {
const [dialogOpen, setDialogOpen] = useState(false);
return (
<main className="conformance-root" data-conformance-id="shared-ui-lab">
<PageLayout
mode="embedded"
title="Zentrale GovOPlaN-Oberflächen"
description="Diese Prüfseite rendert reale gemeinsame Komponenten mit langen deutschen Beschriftungen, Zuständen und responsiven Zusammensetzungen."
actions={<Button variant="primary" data-testid="open-dialog" onClick={() => setDialogOpen(true)}>Prüfdialog öffnen</Button>}
>
<section className="conformance-section" aria-labelledby="actions-heading">
<h2 id="actions-heading">Aktionen, Filter und Status</h2>
<ActionToolbar surface="subtle" justify="between" aria-label="Vorgangsaktionen">
<ToolbarGroup>
<Button variant="primary">Änderungen speichern</Button>
<Button>Vorschau öffnen</Button>
<Button variant="ghost">Verwerfen</Button>
</ToolbarGroup>
<Button variant="danger" disabledReason="Nur die federführende Stelle darf diesen Vorgang endgültig löschen.">Löschen</Button>
</ActionToolbar>
<FilterBar surface="control" aria-label="Vorgangsliste filtern">
<label>Suche<input type="search" placeholder="Aktenzeichen oder verantwortliche Stelle" /></label>
<label>Status<select defaultValue="open"><option value="open">Offen</option><option value="done">Abgeschlossen</option></select></label>
<Button><Search size={16} aria-hidden="true" /> Anwenden</Button>
<CountBadge aria-label="27 Treffer">27</CountBadge>
</FilterBar>
<ContentGrid columns={3} collapseAt="standard" spacing="block">
<StatePanel title="Keine Einträge" description="Für den gewählten Zeitraum liegen noch keine Einträge vor." surface="dashed" size="compact" icon={<Inbox size={22} />} />
<StatePanel title="Berechtigung erforderlich" description="Die zuständige Administration kann den fehlenden Zugriff prüfen." tone="warning" surface="subtle" size="compact" icon={<ShieldCheck size={22} />} />
<StatePanel title="Verbindung unterbrochen" description="Gespeicherte Daten bleiben erhalten. Erneut versuchen, sobald die Verbindung verfügbar ist." tone="danger" surface="subtle" size="compact" actions={<Button>Erneut versuchen</Button>} />
</ContentGrid>
</section>
<section className="conformance-section" aria-labelledby="metrics-heading">
<h2 id="metrics-heading">Kennzahlen und Eigenschaften</h2>
<MetricGrid columns={4} collapseAt="standard">
<MetricCard label="Offene Aufgaben" value="18" detail="3 heute fällig" />
<MetricCard label="Fristgerecht" value="94 %" tone="good" detail="Letzte 30 Tage" />
<MetricCard label="Klärung erforderlich" value="4" tone="warning" density="compact" />
<MetricCard label="Fehlgeschlagen" value="1" tone="danger" surface="subtle" />
</MetricGrid>
<ContentSection density="default" surface="subtle">
<h3>Nachvollziehbare Entscheidung</h3>
<DescriptionList columns={3} collapseAt="standard">
<DescriptionItem term="Aktenzeichen">A-2026-004218</DescriptionItem>
<DescriptionItem term="Verantwortliche Stelle">Fachbereich Öffentlicher Raum und nachhaltige Mobilität</DescriptionItem>
<DescriptionItem term="Letzte Änderung">18. August 2026, 14:42 Uhr</DescriptionItem>
</DescriptionList>
</ContentSection>
</section>
<section className="conformance-section" aria-labelledby="workspace-heading">
<h2 id="workspace-heading">Listen- und Arbeitsbereich</h2>
<WorkspaceFrame className="conformance-workspace" label="Vorgangsauswahl">
<WorkspaceLayout
variant="split"
primarySize="compact"
surface="contained"
primaryLabel="Vorgänge"
contentLabel="Ausgewählter Vorgang"
primary={
<SelectionList label="Vorgänge" variant="navigation">
<SelectionListItem selected>
<SelectionListItemContent leading={<FileText size={18} />} title="Anwohnerparkausweis" description="A-2026-004218 · Prüfung läuft" />
</SelectionListItem>
<SelectionListItem selected={false}>
<SelectionListItemContent leading={<FileText size={18} />} title="Sondernutzung öffentlicher Fläche" description="A-2026-004219 · Rückfrage offen" />
</SelectionListItem>
</SelectionList>
}
>
<Card title="Anwohnerparkausweis" actions={<Button variant="primary">Bearbeiten</Button>}>
<p>Die Identität wurde geprüft. Ein aktueller Wohnsitznachweis muss noch bestätigt werden.</p>
<FormSection title="Nächster Arbeitsschritt" description="Die Entscheidung bleibt nachvollziehbar und kann vor Abschluss korrigiert werden." variant="panel">
<FormGrid columns={2}>
<label>Zuständigkeit<input defaultValue="Bürgerdienste Mitte" /></label>
<label>Bearbeitungsfrist<input type="date" defaultValue="2026-08-28" /></label>
</FormGrid>
</FormSection>
</Card>
</WorkspaceLayout>
</WorkspaceFrame>
</section>
<section className="conformance-section" aria-labelledby="definition-heading">
<h2 id="definition-heading">Definitionsarbeitsbereich</h2>
<div className="conformance-definition">
<DefinitionPalette label="Bausteine" description="Mit Tastatur oder Schaltfläche einfügen">
<DefinitionPaletteGroup label="Verarbeitung">
<DefinitionPaletteItem icon={<DefinitionNodeIcon><GitBranch size={16} /></DefinitionNodeIcon>} label="Bedingung prüfen" />
<DefinitionPaletteItem icon={<DefinitionNodeIcon><ShieldCheck size={16} /></DefinitionNodeIcon>} label="Freigabe anfordern" />
</DefinitionPaletteGroup>
</DefinitionPalette>
<ContentSection className="conformance-canvas" density="default" spacing="none">
<StatePanel title="Definition auswählen" description="Wählen Sie links einen Baustein oder öffnen Sie eine vorhandene Definition." size="fill" />
<FloatingStatus>Automatische Prüfung läuft </FloatingStatus>
</ContentSection>
</div>
</section>
<LaunchContextScenario />
</PageLayout>
<Dialog
open={dialogOpen}
title="Konsequenz vor Ausführung prüfen"
description="Die Änderung betrifft nachgelagerte Aufgaben und wird mit Verantwortlichkeit und Zeitpunkt protokolliert."
size="default"
onClose={() => setDialogOpen(false)}
footer={<><Button onClick={() => setDialogOpen(false)}>Abbrechen</Button><Button variant="primary" onClick={() => setDialogOpen(false)}>Änderung bestätigen</Button></>}
>
<DialogForm onSubmit={(event) => event.preventDefault()}>
<DialogSection title="Begründung">
<label>Begründung<textarea defaultValue="Die vorliegenden Nachweise wurden vollständig geprüft." /></label>
</DialogSection>
</DialogForm>
</Dialog>
</main>
);
}
function LaunchContextScenario() {
const location = useLocation();
const navigate = useGuardedNavigate();
const launchContext = useMemo(() => createQuickAccessLaunchContext({
pathname: location.pathname,
search: location.search,
hash: location.hash,
historyIndex: browserHistoryIndex(),
auth: CONFORMANCE_AUTH,
activeObject: {
ownerModule: "cases",
kind: "case",
objectId: "case-1",
tenantId: "tenant-1",
label: "RPP-2026-0001 · Anwohnerparkausweis",
version: "4",
path: "/cases/case-1"
},
temporalContext: { validityMode: "current", validAt: null, recordedAt: null }
}), [location.hash, location.pathname, location.search]);
return (
<section className="conformance-section" aria-labelledby="launch-heading">
<h2 id="launch-heading">Start- und Rücksprungkontext</h2>
<p>Ein begrenzter Objektverweis öffnet das vollständige Werkzeug und erhält einen eindeutigen Rücksprung zum Vorgang.</p>
<BreadcrumbBar
pathname={location.pathname === "/" ? "/cases/case-1" : location.pathname}
locationState={location.state}
/>
<ActionToolbar surface="subtle">
<Button
data-testid="launch-full-page"
onClick={() => navigate("/files", { state: quickAccessLaunchState(launchContext) })}
>
Vollständige Dateiansicht öffnen
</Button>
</ActionToolbar>
</section>
);
}
const CONFORMANCE_AUTH = {
user: { id: "user-1", account_id: "account-1", email: "case@example.test" },
tenant: { id: "tenant-1", slug: "reference", name: "Referenzkommune" },
scopes: [],
roles: [],
groups: [],
profile_loaded: true,
roles_loaded: true,
groups_loaded: true
} satisfies AuthInfo;
function browserHistoryIndex(): number | null {
const value = (window.history.state as { idx?: unknown } | null)?.idx;
return typeof value === "number" && Number.isInteger(value) ? value : null;
}
+33
View File
@@ -0,0 +1,33 @@
:root {
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
color: var(--text);
background: var(--bg);
}
* { box-sizing: border-box; }
body { margin: 0; min-width: 320px; background: var(--bg); }
button, input, select, textarea { font: inherit; }
.conformance-root { min-height: 100vh; }
.conformance-section { display: grid; gap: 14px; padding-block: 20px; border-top: var(--border-line); }
.conformance-section:first-child { border-top: 0; }
.conformance-section > h2, .conformance-section h3 { margin: 0; color: var(--text-strong); }
.conformance-section p { margin-block: 0 12px; }
.conformance-section label { display: grid; gap: 5px; color: var(--text-label); font-size: 13px; font-weight: 700; }
.conformance-workspace { height: 430px; }
.conformance-definition { display: grid; grid-template-columns: minmax(220px, 280px) minmax(0, 1fr); min-height: 330px; gap: 12px; }
.conformance-canvas { position: relative; min-height: 330px; overflow: hidden; }
@media (max-width: 760px) {
.conformance-definition { grid-template-columns: 1fr; }
.conformance-workspace { height: auto; min-height: 560px; }
}
@media (prefers-reduced-motion: reduce) {
.conformance-root *, .conformance-root *::before, .conformance-root *::after {
scroll-behavior: auto !important;
transition-duration: .01ms !important;
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
}
}
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>GovOPlaN WebUI Conformance Lab</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/main.tsx"></script>
</body>
</html>
+26
View File
@@ -0,0 +1,26 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router";
import ConformanceApp from "./ConformanceApp";
import { UnsavedChangesProvider } from "../src/components/UnsavedChangesGuard";
import "../src/styles/tokens.css";
import "../src/styles/layout.css";
import "../src/styles/forms.css";
import "../src/styles/tables.css";
import "../src/styles/badges.css";
import "../src/styles/components.css";
import "../src/styles/dialogs.css";
import "./conformance.css";
const theme = new URLSearchParams(window.location.search).get("theme");
if (theme === "dark" || theme === "light") document.documentElement.dataset.theme = theme;
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<UnsavedChangesProvider>
<ConformanceApp />
</UnsavedChangesProvider>
</BrowserRouter>
</React.StrictMode>
);
Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 211 KiB

@@ -0,0 +1,75 @@
import { createRequire } from "node:module";
import { expect, test } from "@playwright/test";
const require = createRequire(import.meta.url);
const axePath = require.resolve("axe-core/axe.min.js");
async function expectNoAccessibilityViolations(page: import("@playwright/test").Page) {
await page.addScriptTag({ path: axePath });
const violations = await page.evaluate(async () => {
const axe = (window as typeof window & { axe: { run: (options: unknown) => Promise<{ violations: Array<{ id: string; impact?: string | null; help: string; nodes: Array<{ target: unknown; failureSummary?: string }> }> }> } }).axe;
const result = await axe.run({ runOnly: { type: "tag", values: ["wcag2a", "wcag2aa", "wcag21aa"] } });
return result.violations.map(({ id, impact, help, nodes }) => ({
id,
impact,
help,
nodes: nodes.map((node: { target: unknown; failureSummary?: string }) => ({ target: node.target, failureSummary: node.failureSummary }))
}));
});
expect(violations).toEqual([]);
}
test("shared components remain accessible and keyboard operable", async ({ page }) => {
await page.goto("/?theme=light");
await expect(page.getByRole("heading", { level: 1, name: "Zentrale GovOPlaN-Oberflächen" })).toBeVisible();
await expectNoAccessibilityViolations(page);
const opener = page.getByTestId("open-dialog");
await opener.focus();
await page.keyboard.press("Enter");
await expect(page.getByRole("dialog", { name: "Konsequenz vor Ausführung prüfen" })).toBeVisible();
await expectNoAccessibilityViolations(page);
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog")).toBeHidden();
await expect(opener).toBeFocused();
});
test("full-page tool launch preserves a bounded return context", async ({ page }) => {
await page.goto("/?theme=light");
await page.getByTestId("launch-full-page").click();
await expect(page).toHaveURL(/\/files$/);
const returnButton = page.getByRole("button", { name: /RPP-2026-0001.*Anwohnerparkausweis/ });
await expect(returnButton).toBeVisible();
await returnButton.click();
await expect(page).toHaveURL(/\/?\?theme=light$/);
});
test("light and dark desktop geometry remains stable", async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 1000 });
await page.goto("/?theme=light");
await expect(page.locator("[data-conformance-id='shared-ui-lab']")).toHaveScreenshot("shared-ui-light-desktop.png", { animations: "disabled", maxDiffPixelRatio: 0.005 });
await page.goto("/?theme=dark");
await expect(page.locator("[data-conformance-id='shared-ui-lab']")).toHaveScreenshot("shared-ui-dark-desktop.png", { animations: "disabled", maxDiffPixelRatio: 0.005 });
});
test("narrow layout preserves task order without horizontal overflow", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await page.goto("/?theme=light");
await expect(page.getByText("Bedingung prüfen")).toBeVisible();
const overflowing = await page.evaluate(() => Array.from(document.querySelectorAll<HTMLElement>("body *"))
.map((element) => {
const rect = element.getBoundingClientRect();
return {
element: `${element.tagName.toLowerCase()}.${Array.from(element.classList).join(".")}`,
left: Math.round(rect.left),
right: Math.round(rect.right),
scrollWidth: element.scrollWidth,
clientWidth: element.clientWidth
};
})
.filter(({ left, right }) => left < -1 || right > window.innerWidth + 1)
.slice(0, 20));
expect(overflowing).toEqual([]);
await expect(page.locator("[data-conformance-id='shared-ui-lab']")).toHaveScreenshot("shared-ui-light-narrow.png", { animations: "disabled", maxDiffPixelRatio: 0.005 });
});
+95 -2
View File
@@ -55,10 +55,13 @@
"@tiptap/starter-kit": "^3.29.2" "@tiptap/starter-kit": "^3.29.2"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.62.1",
"@types/node": "^26.2.0",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.2.0", "@vitejs/plugin-react": "^5.2.0",
"@xyflow/react": "^12.11.2", "@xyflow/react": "^12.11.2",
"axe-core": "^4.13.0",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
@@ -576,7 +579,7 @@
}, },
"../../govoplan-records/webui": { "../../govoplan-records/webui": {
"name": "@govoplan/records-webui", "name": "@govoplan/records-webui",
"version": "0.1.18", "version": "0.1.19",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
@@ -659,7 +662,7 @@
}, },
"../../govoplan-tasks/webui": { "../../govoplan-tasks/webui": {
"name": "@govoplan/tasks-webui", "name": "@govoplan/tasks-webui",
"version": "0.1.18", "version": "0.1.19",
"peerDependencies": { "peerDependencies": {
"@govoplan/core-webui": "^0.1.18", "@govoplan/core-webui": "^0.1.18",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
@@ -1716,6 +1719,22 @@
"@jridgewell/sourcemap-codec": "^1.4.14" "@jridgewell/sourcemap-codec": "^1.4.14"
} }
}, },
"node_modules/@playwright/test": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
"integrity": "sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.62.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@rolldown/pluginutils": { "node_modules/@rolldown/pluginutils": {
"version": "1.0.0-rc.3", "version": "1.0.0-rc.3",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
@@ -2662,6 +2681,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": {
"version": "26.2.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz",
"integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~8.3.0"
}
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.17", "version": "19.2.17",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
@@ -2761,6 +2790,16 @@
"d3-zoom": "^3.0.0" "d3-zoom": "^3.0.0"
} }
}, },
"node_modules/axe-core": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz",
"integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==",
"dev": true,
"license": "MPL-2.0",
"engines": {
"node": ">=4"
}
},
"node_modules/baseline-browser-mapping": { "node_modules/baseline-browser-mapping": {
"version": "2.10.38", "version": "2.10.38",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz",
@@ -3241,6 +3280,53 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/playwright": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz",
"integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.62.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz",
"integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=20"
}
},
"node_modules/playwright/node_modules/fsevents": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.23", "version": "8.5.23",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
@@ -3585,6 +3671,13 @@
"node": ">=14.17" "node": ">=14.17"
} }
}, },
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/unzipper-esm": { "node_modules/unzipper-esm": {
"version": "0.13.2", "version": "0.13.2",
"resolved": "https://registry.npmjs.org/unzipper-esm/-/unzipper-esm-0.13.2.tgz", "resolved": "https://registry.npmjs.org/unzipper-esm/-/unzipper-esm-0.13.2.tgz",
+14 -8
View File
@@ -26,8 +26,11 @@
}, },
"scripts": { "scripts": {
"dev": "vite --host 127.0.0.1 --port 5173", "dev": "vite --host 127.0.0.1 --port 5173",
"dev:conformance": "vite --config vite.conformance.config.ts --host 127.0.0.1 --port 4174",
"prebuild": "npm run audit:i18n-structural && npm run test:theme-contract && npm run test:core-interface-patterns", "prebuild": "npm run audit:i18n-structural && npm run test:theme-contract && npm run test:core-interface-patterns",
"build": "tsc && vite build && node scripts/check-bundle-budget.mjs", "build": "tsc && vite build && node scripts/check-bundle-budget.mjs",
"build:conformance": "tsc -p tsconfig.conformance.json && vite build --config vite.conformance.config.ts",
"test:conformance": "playwright test --config playwright.conformance.config.ts",
"check:bundle-budget": "node scripts/check-bundle-budget.mjs", "check:bundle-budget": "node scripts/check-bundle-budget.mjs",
"preview": "vite preview --host 127.0.0.1 --port 4173", "preview": "vite preview --host 127.0.0.1 --port 4173",
"audit:i18n-structural": "node scripts/audit-i18n-structural.mjs", "audit:i18n-structural": "node scripts/audit-i18n-structural.mjs",
@@ -40,7 +43,7 @@
"test:explorer-tree": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/explorer-tree.test.js", "test:explorer-tree": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/explorer-tree.test.js",
"test:icon-button": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/icon-button.test.js", "test:icon-button": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/icon-button.test.js",
"test:layout-primitives": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/layout-primitives.test.js", "test:layout-primitives": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/layout-primitives.test.js",
"test:module-capabilities": "rm -rf .module-test-build && mkdir -p .module-test-build && printf '{\"type\":\"commonjs\"}\n' > .module-test-build/package.json && tsc -p tsconfig.module-tests.json && node .module-test-build/tests/module-capabilities.test.js && node .module-test-build/tests/privacy-policy.test.js && node .module-test-build/tests/help-context.test.js && node .module-test-build/tests/definition-graph.test.js", "test:module-capabilities": "rm -rf .module-test-build && mkdir -p .module-test-build && printf '{\"type\":\"commonjs\"}\n' > .module-test-build/package.json && tsc -p tsconfig.module-tests.json && node .module-test-build/tests/module-capabilities.test.js && node .module-test-build/tests/privacy-policy.test.js && node .module-test-build/tests/help-context.test.js && node .module-test-build/tests/launch-context.test.js && node .module-test-build/tests/definition-graph.test.js",
"test:module-permutations": "node scripts/test-module-permutations.mjs", "test:module-permutations": "node scripts/test-module-permutations.mjs",
"test:mail-components": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/mail-components.test.js", "test:mail-components": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/mail-components.test.js",
"test:metric-card": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/metric-card.test.js", "test:metric-card": "rm -rf .component-test-build && mkdir -p .component-test-build && printf '{\"type\":\"commonjs\"}\\n' > .component-test-build/package.json && tsc -p tsconfig.component-tests.json && node .component-test-build/tests/metric-card.test.js",
@@ -56,13 +59,13 @@
}, },
"dependencies": { "dependencies": {
"@govoplan/access-webui": "file:../../govoplan-access/webui", "@govoplan/access-webui": "file:../../govoplan-access/webui",
"@govoplan/approvals-webui": "file:../../govoplan-approvals/webui",
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui", "@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
"@govoplan/admin-webui": "file:../../govoplan-admin/webui", "@govoplan/admin-webui": "file:../../govoplan-admin/webui",
"@govoplan/approvals-webui": "file:../../govoplan-approvals/webui",
"@govoplan/audit-webui": "file:../../govoplan-audit/webui", "@govoplan/audit-webui": "file:../../govoplan-audit/webui",
"@govoplan/calendar-webui": "file:../../govoplan-calendar/webui", "@govoplan/calendar-webui": "file:../../govoplan-calendar/webui",
"@govoplan/cases-webui": "file:../../govoplan-cases/webui",
"@govoplan/campaign-webui": "file:../../govoplan-campaign/webui", "@govoplan/campaign-webui": "file:../../govoplan-campaign/webui",
"@govoplan/cases-webui": "file:../../govoplan-cases/webui",
"@govoplan/committee-webui": "file:../../govoplan-committee/webui", "@govoplan/committee-webui": "file:../../govoplan-committee/webui",
"@govoplan/dashboard-webui": "file:../../govoplan-dashboard/webui", "@govoplan/dashboard-webui": "file:../../govoplan-dashboard/webui",
"@govoplan/dataflow-webui": "file:../../govoplan-dataflow/webui", "@govoplan/dataflow-webui": "file:../../govoplan-dataflow/webui",
@@ -71,10 +74,10 @@
"@govoplan/docs-webui": "file:../../govoplan-docs/webui", "@govoplan/docs-webui": "file:../../govoplan-docs/webui",
"@govoplan/encryption-webui": "file:../../govoplan-encryption/webui", "@govoplan/encryption-webui": "file:../../govoplan-encryption/webui",
"@govoplan/files-webui": "file:../../govoplan-files/webui", "@govoplan/files-webui": "file:../../govoplan-files/webui",
"@govoplan/forms-webui": "file:../../govoplan-forms/webui",
"@govoplan/forms-runtime-webui": "file:../../govoplan-forms-runtime/webui", "@govoplan/forms-runtime-webui": "file:../../govoplan-forms-runtime/webui",
"@govoplan/idm-webui": "file:../../govoplan-idm/webui", "@govoplan/forms-webui": "file:../../govoplan-forms/webui",
"@govoplan/identity-trust-webui": "file:../../govoplan-identity-trust/webui", "@govoplan/identity-trust-webui": "file:../../govoplan-identity-trust/webui",
"@govoplan/idm-webui": "file:../../govoplan-idm/webui",
"@govoplan/mail-webui": "file:../../govoplan-mail/webui", "@govoplan/mail-webui": "file:../../govoplan-mail/webui",
"@govoplan/notifications-webui": "file:../../govoplan-notifications/webui", "@govoplan/notifications-webui": "file:../../govoplan-notifications/webui",
"@govoplan/ops-webui": "file:../../govoplan-ops/webui", "@govoplan/ops-webui": "file:../../govoplan-ops/webui",
@@ -84,14 +87,14 @@
"@govoplan/postbox-webui": "file:../../govoplan-postbox/webui", "@govoplan/postbox-webui": "file:../../govoplan-postbox/webui",
"@govoplan/projects-webui": "file:../../govoplan-projects/webui", "@govoplan/projects-webui": "file:../../govoplan-projects/webui",
"@govoplan/quick-access-webui": "file:../../govoplan-quick-access/webui", "@govoplan/quick-access-webui": "file:../../govoplan-quick-access/webui",
"@govoplan/reporting-webui": "file:../../govoplan-reporting/webui",
"@govoplan/records-webui": "file:../../govoplan-records/webui", "@govoplan/records-webui": "file:../../govoplan-records/webui",
"@govoplan/reporting-webui": "file:../../govoplan-reporting/webui",
"@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui", "@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui",
"@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui", "@govoplan/scheduling-webui": "file:../../govoplan-scheduling/webui",
"@govoplan/search-webui": "file:../../govoplan-search/webui", "@govoplan/search-webui": "file:../../govoplan-search/webui",
"@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui",
"@govoplan/templates-webui": "file:../../govoplan-templates/webui",
"@govoplan/tasks-webui": "file:../../govoplan-tasks/webui", "@govoplan/tasks-webui": "file:../../govoplan-tasks/webui",
"@govoplan/templates-webui": "file:../../govoplan-templates/webui",
"@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui",
"@govoplan/views-webui": "file:../../govoplan-views/webui", "@govoplan/views-webui": "file:../../govoplan-views/webui",
"@govoplan/voting-webui": "file:../../govoplan-voting/webui", "@govoplan/voting-webui": "file:../../govoplan-voting/webui",
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui", "@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
@@ -102,10 +105,13 @@
"@tiptap/starter-kit": "^3.29.2" "@tiptap/starter-kit": "^3.29.2"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "^1.62.1",
"@types/node": "^26.2.0",
"@types/react": "^19.2.17", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.2.0", "@vitejs/plugin-react": "^5.2.0",
"@xyflow/react": "^12.11.2", "@xyflow/react": "^12.11.2",
"axe-core": "^4.13.0",
"lucide-react": "^1.23.0", "lucide-react": "^1.23.0",
"react": "^19.2.8", "react": "^19.2.8",
"react-dom": "^19.2.8", "react-dom": "^19.2.8",
+47
View File
@@ -0,0 +1,47 @@
import { existsSync, readdirSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { chromium, defineConfig } from "@playwright/test";
function chromiumExecutable(): string | undefined {
const configured = process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH;
if (configured) return configured;
const managed = chromium.executablePath();
if (existsSync(managed)) return managed;
const roots = [
join(homedir(), ".cache", "ms-playwright"),
join(homedir(), ".var", "app", "com.vscodium.codium", "cache", "ms-playwright")
];
for (const root of roots) {
if (!existsSync(root)) continue;
const installations = readdirSync(root).filter((entry) => entry.startsWith("chromium-")).sort().reverse();
for (const installation of installations) {
const candidate = join(root, installation, "chrome-linux64", "chrome");
if (existsSync(candidate)) return candidate;
}
}
return undefined;
}
export default defineConfig({
testDir: "./conformance/tests",
snapshotPathTemplate: "{testDir}/snapshots/{arg}{ext}",
fullyParallel: false,
workers: 1,
reporter: "line",
use: {
baseURL: "http://127.0.0.1:4174",
browserName: "chromium",
colorScheme: "light",
locale: "de-DE",
reducedMotion: "reduce",
launchOptions: chromiumExecutable() ? { executablePath: chromiumExecutable() } : undefined
},
webServer: {
command: "npm run dev:conformance",
url: "http://127.0.0.1:4174",
reuseExistingServer: false,
timeout: 120_000
}
});
+3
View File
@@ -12,6 +12,7 @@ import { firstAccessibleRoute, loadInstalledPublicWebModules, loadInstalledWebMo
import { PlatformModulesProvider } from "./platform/ModuleContext"; import { PlatformModulesProvider } from "./platform/ModuleContext";
import { PlatformViewProvider } from "./platform/ViewContext"; import { PlatformViewProvider } from "./platform/ViewContext";
import { PlatformTemporalProvider } from "./platform/TemporalContext"; import { PlatformTemporalProvider } from "./platform/TemporalContext";
import { PlatformActiveObjectProvider } from "./platform/ActiveObjectContext";
import { PLATFORM_TEMPORAL_CONTEXT_CHANGED_EVENT } from "./platform/temporal"; import { PLATFORM_TEMPORAL_CONTEXT_CHANGED_EVENT } from "./platform/temporal";
import { import {
PLATFORM_VIEW_CHANGED_EVENT, PLATFORM_VIEW_CHANGED_EVENT,
@@ -538,6 +539,7 @@ export default function App() {
<PlatformTemporalProvider scopeKey={`${auth.user?.id ?? "account"}:${(auth.active_tenant ?? auth.tenant).id}`}> <PlatformTemporalProvider scopeKey={`${auth.user?.id ?? "account"}:${(auth.active_tenant ?? auth.tenant).id}`}>
<DocumentationHelpProvider localDocsAvailable={localDocsAvailable}> <DocumentationHelpProvider localDocsAvailable={localDocsAvailable}>
<PlatformViewProvider modules={webModules} projection={viewProjection}> <PlatformViewProvider modules={webModules} projection={viewProjection}>
<PlatformActiveObjectProvider>
<UnsavedChangesProvider> <UnsavedChangesProvider>
<AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}> <AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
<ModuleLoadBoundary resetKey={`${location.pathname}:${temporalRevision}`} loading={webModulesLoading}> <ModuleLoadBoundary resetKey={`${location.pathname}:${temporalRevision}`} loading={webModulesLoading}>
@@ -583,6 +585,7 @@ export default function App() {
} }
</AppShell> </AppShell>
</UnsavedChangesProvider> </UnsavedChangesProvider>
</PlatformActiveObjectProvider>
</PlatformViewProvider> </PlatformViewProvider>
</DocumentationHelpProvider> </DocumentationHelpProvider>
</PlatformTemporalProvider> </PlatformTemporalProvider>
+3 -2
View File
@@ -82,7 +82,7 @@ export default function PageLayout({
notices, notices,
mode = "standalone", mode = "standalone",
scrollable, scrollable,
stickyHeader = true, stickyHeader,
showHeader = true, showHeader = true,
documentationType = "user", documentationType = "user",
className = "", className = "",
@@ -96,6 +96,7 @@ export default function PageLayout({
}: PageLayoutProps) { }: PageLayoutProps) {
const shouldScroll = scrollable ?? mode === "standalone"; const shouldScroll = scrollable ?? mode === "standalone";
const shouldInset = mode !== "embedded"; const shouldInset = mode !== "embedded";
const shouldStickHeader = stickyHeader ?? mode !== "embedded";
const layoutClasses = [ const layoutClasses = [
"page-layout", "page-layout",
`page-layout-${mode}`, `page-layout-${mode}`,
@@ -120,7 +121,7 @@ export default function PageLayout({
description={description} description={description}
actions={actions} actions={actions}
loading={headerLoading ?? loading} loading={headerLoading ?? loading}
sticky={stickyHeader} sticky={shouldStickHeader}
className={headerClassName} className={headerClassName}
/> />
)} )}
@@ -171,8 +171,6 @@ export function UnsavedChangesProvider({ children }: {children: ReactNode;}) {
open open
role="alertdialog" role="alertdialog"
title={registration.title ?? "i18n:govoplan-core.unsaved_changes.29267269"} title={registration.title ?? "i18n:govoplan-core.unsaved_changes.29267269"}
className="unsaved-changes-dialog"
footerClassName="unsaved-changes-actions"
closeOnBackdrop={!saving} closeOnBackdrop={!saving}
closeDisabled={saving} closeDisabled={saving}
onClose={() => setPendingAction(null)} onClose={() => setPendingAction(null)}
+16
View File
@@ -78,13 +78,21 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.application_notices": "Application notices", "i18n:govoplan-core.application_notices": "Application notices",
"i18n:govoplan-core.more_tools": "More tools", "i18n:govoplan-core.more_tools": "More tools",
"i18n:govoplan-core.product_area.work": "Work", "i18n:govoplan-core.product_area.work": "Work",
"i18n:govoplan-core.product_area.services_cases": "Services and cases",
"i18n:govoplan-core.product_area.communication": "Communication", "i18n:govoplan-core.product_area.communication": "Communication",
"i18n:govoplan-core.product_area.records_documents": "Records and documents", "i18n:govoplan-core.product_area.records_documents": "Records and documents",
"i18n:govoplan-core.product_area.meetings_decisions": "Meetings and decisions", "i18n:govoplan-core.product_area.meetings_decisions": "Meetings and decisions",
"i18n:govoplan-core.product_area.data_assurance": "Data and assurance",
"i18n:govoplan-core.product_area.people_responsibility": "People and responsibility",
"i18n:govoplan-core.product_area.work_description": "Tasks, approvals, handoffs, and other work requiring attention.", "i18n:govoplan-core.product_area.work_description": "Tasks, approvals, handoffs, and other work requiring attention.",
"i18n:govoplan-core.product_area.services_cases_description": "Services, forms, requests, and the cases that deliver them.",
"i18n:govoplan-core.product_area.communication_description": "Messages, campaigns, notifications, and institutional communication.", "i18n:govoplan-core.product_area.communication_description": "Messages, campaigns, notifications, and institutional communication.",
"i18n:govoplan-core.product_area.records_documents_description": "Documents, files, evidence, templates, and records.", "i18n:govoplan-core.product_area.records_documents_description": "Documents, files, evidence, templates, and records.",
"i18n:govoplan-core.product_area.meetings_decisions_description": "Scheduling, meetings, participation, and institutional decisions.", "i18n:govoplan-core.product_area.meetings_decisions_description": "Scheduling, meetings, participation, and institutional decisions.",
"i18n:govoplan-core.product_area.data_assurance_description": "Data sources, processing, reporting, risk, and assurance.",
"i18n:govoplan-core.product_area.people_responsibility_description": "People, organizations, identities, and responsibility assignments.",
"i18n:govoplan-core.quick_access.return_to_origin": "Return to {label}",
"i18n:govoplan-core.quick_access.previous_context": "previous context",
"i18n:govoplan-core.application.b291beb8": "Application", "i18n:govoplan-core.application.b291beb8": "Application",
"i18n:govoplan-core.attachment_base_path.66e9940b": "Attachment base path", "i18n:govoplan-core.attachment_base_path.66e9940b": "Attachment base path",
"i18n:govoplan-core.attachment_value.01801a54": "Attachment {value0}", "i18n:govoplan-core.attachment_value.01801a54": "Attachment {value0}",
@@ -745,13 +753,21 @@ export const generatedTranslations: PlatformTranslations = {
"i18n:govoplan-core.application_notices": "Anwendungshinweise", "i18n:govoplan-core.application_notices": "Anwendungshinweise",
"i18n:govoplan-core.more_tools": "Weitere Werkzeuge", "i18n:govoplan-core.more_tools": "Weitere Werkzeuge",
"i18n:govoplan-core.product_area.work": "Arbeit", "i18n:govoplan-core.product_area.work": "Arbeit",
"i18n:govoplan-core.product_area.services_cases": "Leistungen und Vorgänge",
"i18n:govoplan-core.product_area.communication": "Kommunikation", "i18n:govoplan-core.product_area.communication": "Kommunikation",
"i18n:govoplan-core.product_area.records_documents": "Akten und Dokumente", "i18n:govoplan-core.product_area.records_documents": "Akten und Dokumente",
"i18n:govoplan-core.product_area.meetings_decisions": "Termine und Entscheidungen", "i18n:govoplan-core.product_area.meetings_decisions": "Termine und Entscheidungen",
"i18n:govoplan-core.product_area.data_assurance": "Daten und Qualitätssicherung",
"i18n:govoplan-core.product_area.people_responsibility": "Personen und Verantwortung",
"i18n:govoplan-core.product_area.work_description": "Aufgaben, Freigaben, Übergaben und andere zu bearbeitende Arbeit.", "i18n:govoplan-core.product_area.work_description": "Aufgaben, Freigaben, Übergaben und andere zu bearbeitende Arbeit.",
"i18n:govoplan-core.product_area.services_cases_description": "Leistungen, Formulare, Anträge und die zugehörigen Vorgänge.",
"i18n:govoplan-core.product_area.communication_description": "Nachrichten, Kampagnen, Benachrichtigungen und institutionelle Kommunikation.", "i18n:govoplan-core.product_area.communication_description": "Nachrichten, Kampagnen, Benachrichtigungen und institutionelle Kommunikation.",
"i18n:govoplan-core.product_area.records_documents_description": "Akten, Dokumente, Nachweise, Vorlagen und Dateien.", "i18n:govoplan-core.product_area.records_documents_description": "Akten, Dokumente, Nachweise, Vorlagen und Dateien.",
"i18n:govoplan-core.product_area.meetings_decisions_description": "Terminplanung, Sitzungen, Beteiligung und institutionelle Entscheidungen.", "i18n:govoplan-core.product_area.meetings_decisions_description": "Terminplanung, Sitzungen, Beteiligung und institutionelle Entscheidungen.",
"i18n:govoplan-core.product_area.data_assurance_description": "Datenquellen, Verarbeitung, Berichte, Risiken und Qualitätssicherung.",
"i18n:govoplan-core.product_area.people_responsibility_description": "Personen, Organisationen, Identitäten und Verantwortungszuordnungen.",
"i18n:govoplan-core.quick_access.return_to_origin": "Zurück zu {label}",
"i18n:govoplan-core.quick_access.previous_context": "vorherigem Kontext",
"i18n:govoplan-core.application.b291beb8": "Application", "i18n:govoplan-core.application.b291beb8": "Application",
"i18n:govoplan-core.attachment_base_path.66e9940b": "Attachment base path", "i18n:govoplan-core.attachment_base_path.66e9940b": "Attachment base path",
"i18n:govoplan-core.attachment_value.01801a54": "Attachment {value0}", "i18n:govoplan-core.attachment_value.01801a54": "Attachment {value0}",
+2
View File
@@ -34,6 +34,8 @@ export * from "./platform/ViewContext";
export * from "./platform/views"; export * from "./platform/views";
export * from "./platform/temporal"; export * from "./platform/temporal";
export * from "./platform/TemporalContext"; export * from "./platform/TemporalContext";
export * from "./platform/ActiveObjectContext";
export * from "./platform/launchContext";
export * from "./platform/wizards"; export * from "./platform/wizards";
export * from "./utils/permissions"; export * from "./utils/permissions";
+27 -3
View File
@@ -6,6 +6,9 @@ import Titlebar from "./Titlebar";
import BreadcrumbBar from "./BreadcrumbBar"; import BreadcrumbBar from "./BreadcrumbBar";
import { usePlatformModules, usePlatformUiCapability } from "../platform/ModuleContext"; import { usePlatformModules, usePlatformUiCapability } from "../platform/ModuleContext";
import { useEffectiveView, useViewSurfaces } from "../platform/ViewContext"; import { useEffectiveView, useViewSurfaces } from "../platform/ViewContext";
import { useTemporalDataContext } from "../platform/TemporalContext";
import { useActiveObject } from "../platform/ActiveObjectContext";
import { createQuickAccessLaunchContext } from "../platform/launchContext";
import { isViewSurfaceVisible } from "../platform/views"; import { isViewSurfaceVisible } from "../platform/views";
import { hasAnyScope, hasScope } from "../utils/permissions"; import { hasAnyScope, hasScope } from "../utils/permissions";
@@ -37,6 +40,21 @@ export default function AppShell({
const quickAccess = usePlatformUiCapability<QuickAccessRuntimeUiCapability>("quickAccess.runtime"); const quickAccess = usePlatformUiCapability<QuickAccessRuntimeUiCapability>("quickAccess.runtime");
const projection = useEffectiveView(); const projection = useEffectiveView();
const surfaces = useViewSurfaces(); const surfaces = useViewSurfaces();
const temporal = useTemporalDataContext();
const activeObject = useActiveObject();
const launchContext = useMemo(
() => auth ? createQuickAccessLaunchContext({
pathname: location.pathname,
search: location.search,
hash: location.hash,
historyIndex: browserHistoryIndex(),
auth,
activeObject,
temporalContext: temporal.selection,
viewContext: projection
}) : null,
[activeObject, auth, location.hash, location.pathname, location.search, projection, temporal.selection]
);
const quickAccessTools = useMemo( const quickAccessTools = useMemo(
() => modules.flatMap((module) => module.quickAccessTools ?? []).filter((tool) => { () => modules.flatMap((module) => module.quickAccessTools ?? []).filter((tool) => {
if (tool.allOf?.length && !tool.allOf.every((scope) => hasScope(auth, scope))) return false; if (tool.allOf?.length && !tool.allOf.every((scope) => hasScope(auth, scope))) return false;
@@ -73,12 +91,18 @@ export default function AppShell({
/> />
<div className="app-main"> <div className="app-main">
<Titlebar settings={settings} auth={auth} onSettingsChange={onSettingsChange} onAuthChange={onAuthChange} maintenanceMode={maintenanceMode} backendReachable={backendReachable} /> <Titlebar settings={settings} auth={auth} onSettingsChange={onSettingsChange} onAuthChange={onAuthChange} maintenanceMode={maintenanceMode} backendReachable={backendReachable} />
<BreadcrumbBar pathname={location.pathname} /> <BreadcrumbBar pathname={location.pathname} locationState={location.state} />
<main className="app-content">{children}</main> <main className="app-content">{children}</main>
</div> </div>
{QuickAccessRail && auth && ( {QuickAccessRail && auth && launchContext && (
<QuickAccessRail settings={settings} auth={auth} tools={quickAccessTools} /> <QuickAccessRail settings={settings} auth={auth} tools={quickAccessTools} launchContext={launchContext} />
)} )}
</div> </div>
); );
} }
function browserHistoryIndex(): number | null {
if (typeof window === "undefined") return null;
const value = (window.history.state as { idx?: unknown } | null)?.idx;
return typeof value === "number" && Number.isInteger(value) ? value : null;
}
+48 -3
View File
@@ -1,11 +1,37 @@
import { Link } from "react-router"; import { Link } from "react-router";
import { ChevronRight } from "lucide-react"; import { ArrowLeft, ChevronRight } from "lucide-react";
import { usePlatformLanguage } from "../i18n/LanguageContext"; import { i18nMessage, usePlatformLanguage } from "../i18n/LanguageContext";
import { useGuardedNavigate } from "../components/UnsavedChangesGuard";
import {
quickAccessLaunchContextFromState,
quickAccessReturnPath
} from "../platform/launchContext";
export default function BreadcrumbBar({ pathname }: {pathname: string;}) { export default function BreadcrumbBar({
pathname,
locationState
}: {
pathname: string;
locationState?: unknown;
}) {
const parts = pathname.split("/").filter(Boolean); const parts = pathname.split("/").filter(Boolean);
const labels = parts.length ? parts : ["campaigns"]; const labels = parts.length ? parts : ["campaigns"];
const { translateText } = usePlatformLanguage(); const { translateText } = usePlatformLanguage();
const navigate = useGuardedNavigate();
const launchContext = quickAccessLaunchContextFromState(locationState);
function returnToOrigin() {
if (!launchContext) return;
const historyIndex = browserHistoryIndex();
if (
launchContext.origin.historyIndex !== null
&& historyIndex === launchContext.origin.historyIndex + 1
) {
navigate(-1);
return;
}
navigate(quickAccessReturnPath(launchContext), { replace: true });
}
return ( return (
<div className="breadcrumb-bar"> <div className="breadcrumb-bar">
@@ -20,10 +46,29 @@ export default function BreadcrumbBar({ pathname }: {pathname: string;}) {
})} })}
</nav> </nav>
{launchContext ? (
<button
type="button"
className="ghost-button launch-return-button"
onClick={returnToOrigin}
>
<ArrowLeft size={16} aria-hidden="true" />
{translateText(i18nMessage(
"i18n:govoplan-core.quick_access.return_to_origin",
{ label: launchContext.activeObject?.label ?? translateText("i18n:govoplan-core.quick_access.previous_context") }
))}
</button>
) : null}
</div>); </div>);
} }
function browserHistoryIndex(): number | null {
if (typeof window === "undefined") return null;
const value = (window.history.state as { idx?: unknown } | null)?.idx;
return typeof value === "number" && Number.isInteger(value) ? value : null;
}
const campaignRouteLabels: Record<string, string> = { const campaignRouteLabels: Record<string, string> = {
data: "i18n:govoplan-core.sender_recipients.922c6d24", data: "i18n:govoplan-core.sender_recipients.922c6d24",
campaign: "i18n:govoplan-core.sender_recipients.922c6d24", campaign: "i18n:govoplan-core.sender_recipients.922c6d24",
@@ -0,0 +1,55 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
type ReactNode
} from "react";
import type { ActiveObjectReference } from "../types";
type ActiveObjectContextValue = {
activeObject: ActiveObjectReference | null;
register: (reference: ActiveObjectReference) => () => void;
};
const fallbackContext: ActiveObjectContextValue = {
activeObject: null,
register: () => () => undefined
};
const ActiveObjectContext = createContext<ActiveObjectContextValue>(fallbackContext);
export function PlatformActiveObjectProvider({ children }: { children: ReactNode }) {
const nextId = useRef(1);
const [registrations, setRegistrations] = useState<Array<{
id: number;
reference: ActiveObjectReference;
}>>([]);
const activeObject = registrations[registrations.length - 1]?.reference ?? null;
const register = useCallback((reference: ActiveObjectReference) => {
const id = nextId.current;
nextId.current += 1;
setRegistrations((current) => [...current, { id, reference }]);
return () => setRegistrations((current) => current.filter((item) => item.id !== id));
}, []);
const value = useMemo(() => ({ activeObject, register }), [activeObject, register]);
return (
<ActiveObjectContext.Provider value={value}>
{children}
</ActiveObjectContext.Provider>
);
}
export function useActiveObject(): ActiveObjectReference | null {
return useContext(ActiveObjectContext).activeObject;
}
export function useRegisterActiveObject(reference: ActiveObjectReference | null): void {
const { register } = useContext(ActiveObjectContext);
useEffect(() => reference ? register(reference) : undefined, [reference, register]);
}
+126
View File
@@ -0,0 +1,126 @@
import type {
ActiveObjectReference,
AuthInfo,
EffectiveViewProjection,
QuickAccessLaunchContext,
QuickAccessResult
} from "../types";
import type { TemporalDataSelection } from "./temporal";
export const QUICK_ACCESS_LAUNCH_CONTEXT_VERSION = "1" as const;
export const QUICK_ACCESS_LAUNCH_STATE_KEY = "govoplanQuickAccessLaunch";
export const QUICK_ACCESS_RESULT_EVENT = "govoplan:quick-access-result";
export type QuickAccessLaunchContextInput = {
pathname: string;
search?: string;
hash?: string;
historyIndex?: number | null;
auth: AuthInfo;
activeObject?: ActiveObjectReference | null;
temporalContext: TemporalDataSelection;
viewContext?: EffectiveViewProjection | null;
};
export function createQuickAccessLaunchContext({
pathname,
search = "",
hash = "",
historyIndex = null,
auth,
activeObject = null,
temporalContext,
viewContext = null
}: QuickAccessLaunchContextInput): QuickAccessLaunchContext {
const tenant = auth.active_tenant ?? auth.tenant;
const principal = auth.principal;
return {
contractVersion: QUICK_ACCESS_LAUNCH_CONTEXT_VERSION,
origin: {
pathname: normalizePathname(pathname),
search: normalizeSearch(search),
hash: normalizeHash(hash),
historyIndex: Number.isInteger(historyIndex) ? historyIndex ?? null : null
},
tenantId: tenant.id,
accountId: auth.user.account_id,
activeObject: activeObject?.tenantId === tenant.id ? activeObject : null,
actingContext: principal?.acting_assignment_id || principal?.acting_for_account_id
? {
assignmentId: principal.acting_assignment_id ?? null,
actingForAccountId: principal.acting_for_account_id ?? null
}
: null,
temporalContext: {
validityMode: temporalContext.validityMode,
validAt: temporalContext.validAt ?? null,
recordedAt: temporalContext.recordedAt ?? null
},
viewContext: viewContext?.activeViewId
? {
viewId: viewContext.activeViewId,
revisionId: viewContext.activeRevisionId,
name: viewContext.activeViewName
}
: null
};
}
export function quickAccessLaunchState(
context: QuickAccessLaunchContext
): Record<string, QuickAccessLaunchContext> {
return { [QUICK_ACCESS_LAUNCH_STATE_KEY]: context };
}
export function quickAccessLaunchContextFromState(
value: unknown
): QuickAccessLaunchContext | null {
if (!isRecord(value)) return null;
const candidate = value[QUICK_ACCESS_LAUNCH_STATE_KEY];
if (!isRecord(candidate) || candidate.contractVersion !== QUICK_ACCESS_LAUNCH_CONTEXT_VERSION) {
return null;
}
if (!isRecord(candidate.origin) || typeof candidate.origin.pathname !== "string") {
return null;
}
if (
typeof candidate.tenantId !== "string"
|| typeof candidate.accountId !== "string"
|| !candidate.origin.pathname.startsWith("/")
) {
return null;
}
return candidate as QuickAccessLaunchContext;
}
export function quickAccessReturnPath(context: QuickAccessLaunchContext): string {
return `${context.origin.pathname}${context.origin.search}${context.origin.hash}`;
}
export function dispatchQuickAccessResult(
toolId: string,
launchContext: QuickAccessLaunchContext,
result: QuickAccessResult
): void {
if (typeof window === "undefined") return;
window.dispatchEvent(new CustomEvent(QUICK_ACCESS_RESULT_EVENT, {
detail: { toolId, launchContext, result }
}));
}
function normalizePathname(value: string): string {
return value.startsWith("/") ? value : "/";
}
function normalizeSearch(value: string): string {
return !value ? "" : value.startsWith("?") ? value : "";
}
function normalizeHash(value: string): string {
return !value ? "" : value.startsWith("#") ? value : "";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object" && !Array.isArray(value));
}
+4 -4
View File
@@ -49,7 +49,7 @@
.public-card h1 { .public-card h1 {
margin: 0; margin: 0;
max-width: 620px; max-width: 680px;
font-size: clamp(30px, 4vw, 46px); font-size: clamp(30px, 4vw, 46px);
line-height: 1.08; line-height: 1.08;
color: var(--text-strong); color: var(--text-strong);
@@ -115,7 +115,7 @@
font-weight: 700; font-weight: 700;
padding: 8px 10px; padding: 8px 10px;
margin: -8px -10px; margin: -8px -10px;
border-radius: 7px; border-radius: var(--radius-md);
cursor: pointer; cursor: pointer;
transition: background-color .12s ease, color .12s ease, box-shadow .12s ease; transition: background-color .12s ease, color .12s ease, box-shadow .12s ease;
} }
@@ -125,7 +125,7 @@
.account-pill { .account-pill {
padding: 8px 10px; padding: 8px 10px;
margin: 0; margin: 0;
border-radius: 7px; border-radius: var(--radius-md);
transition: background-color .12s ease, color .12s ease, box-shadow .12s ease; transition: background-color .12s ease, color .12s ease, box-shadow .12s ease;
cursor: pointer; cursor: pointer;
} }
@@ -145,7 +145,7 @@
} }
.dropdown-item { .dropdown-item {
border-radius: 6px; border-radius: var(--radius-compact);
transition: background-color .12s ease, color .12s ease; transition: background-color .12s ease, color .12s ease;
} }
+1 -1
View File
@@ -1,4 +1,4 @@
.status-badge { display: inline-flex; align-items: center; height: 24px; border-radius: 99px; padding: 0 9px; font-size: 12px; font-weight: 800; background: var(--status-neutral-bg); color: var(--text-soft); text-transform: uppercase; } .status-badge { display: inline-flex; align-items: center; height: 24px; border-radius: var(--radius-pill); padding: 0 9px; font-size: 12px; font-weight: 800; background: var(--status-neutral-bg); color: var(--text-soft); text-transform: uppercase; }
.status-ready, .status-sent, .status-appended, .status-success, .status-active { background: var(--success-soft); color: var(--success-text-strong); } .status-ready, .status-sent, .status-appended, .status-success, .status-active { background: var(--success-soft); color: var(--success-text-strong); }
.status-warning, .status-needs-review, .status-pending { background: var(--warning-soft); color: var(--warning-text-strong); } .status-warning, .status-needs-review, .status-pending { background: var(--warning-soft); color: var(--warning-text-strong); }
.status-blocked, .status-error, .status-danger, .status-failed, .status-failed-permanent { background: var(--danger-bg); color: var(--danger-text-strong); } .status-blocked, .status-error, .status-danger, .status-failed, .status-failed-permanent { background: var(--danger-bg); color: var(--danger-text-strong); }
+75 -83
View File
@@ -20,7 +20,7 @@
width: 18px; width: 18px;
height: 13px; height: 13px;
border: 2px solid var(--loading-line); border: 2px solid var(--loading-line);
border-radius: 3px; border-radius: var(--radius-tight);
background: var(--loading-envelope-bg); background: var(--loading-envelope-bg);
animation: loading-envelope-float .74s ease-in-out infinite alternate; animation: loading-envelope-float .74s ease-in-out infinite alternate;
} }
@@ -65,7 +65,7 @@
gap: 7px; gap: 7px;
max-width: 100%; max-width: 100%;
border: 1px solid var(--control-border); border: 1px solid var(--control-border);
border-radius: 999px; border-radius: var(--radius-pill);
background: linear-gradient(var(--control-gradient-start), var(--control-gradient-end)); background: linear-gradient(var(--control-gradient-start), var(--control-gradient-end));
box-shadow: var(--shadow-control-strong); box-shadow: var(--shadow-control-strong);
padding: 5px 8px 5px 11px; padding: 5px 8px 5px 11px;
@@ -98,7 +98,7 @@
width: 18px; width: 18px;
height: 18px; height: 18px;
border: 0; border: 0;
border-radius: 999px; border-radius: var(--radius-pill);
background: var(--hover-tint); background: var(--hover-tint);
color: var(--control-text-muted); color: var(--control-text-muted);
cursor: pointer; cursor: pointer;
@@ -221,7 +221,7 @@
width: 28px; width: 28px;
height: 28px; height: 28px;
border: 0; border: 0;
border-radius: 4px; border-radius: var(--radius-sm);
background: transparent; background: transparent;
color: var(--muted); color: var(--muted);
cursor: pointer; cursor: pointer;
@@ -264,7 +264,7 @@
color: var(--muted); color: var(--muted);
font-size: 0.875rem; font-size: 0.875rem;
} }
@media (max-width: 520px) { @media (max-width: 560px) {
.password-generator-character-sets { .password-generator-character-sets {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -326,7 +326,7 @@
width: 18px; width: 18px;
height: 18px; height: 18px;
border: var(--border-line-dark); border: var(--border-line-dark);
border-radius: 4px; border-radius: var(--radius-sm);
box-shadow: var(--shadow-inset-highlight); box-shadow: var(--shadow-inset-highlight);
} }
@@ -341,7 +341,7 @@
width: 30px; width: 30px;
height: 30px; height: 30px;
border: 0; border: 0;
border-radius: 4px; border-radius: var(--radius-sm);
background: transparent; background: transparent;
color: var(--muted); color: var(--muted);
cursor: pointer; cursor: pointer;
@@ -372,7 +372,7 @@
min-width: 220px; min-width: 220px;
padding: 10px; padding: 10px;
border: var(--border-line-dark); border: var(--border-line-dark);
border-radius: 8px; border-radius: var(--radius-md);
background: var(--panel); background: var(--panel);
box-shadow: var(--shadow-menu); box-shadow: var(--shadow-menu);
} }
@@ -387,7 +387,7 @@
width: 28px; width: 28px;
height: 28px; height: 28px;
border: 1px solid var(--black-border-soft); border: 1px solid var(--black-border-soft);
border-radius: 6px; border-radius: var(--radius-compact);
cursor: pointer; cursor: pointer;
box-shadow: var(--shadow-inset-highlight); box-shadow: var(--shadow-inset-highlight);
} }
@@ -420,7 +420,7 @@
width: 32px; width: 32px;
height: 32px; height: 32px;
border: var(--border-line); border: var(--border-line);
border-radius: 4px; border-radius: var(--radius-sm);
background: var(--surface); background: var(--surface);
color: var(--text); color: var(--text);
cursor: pointer; cursor: pointer;
@@ -448,7 +448,7 @@
min-width: 30px; min-width: 30px;
height: 30px; height: 30px;
border: 1px solid transparent; border: 1px solid transparent;
border-radius: 5px; border-radius: var(--radius-xs);
background: transparent; background: transparent;
color: var(--text); color: var(--text);
cursor: pointer; cursor: pointer;
@@ -491,7 +491,7 @@
.admin-overview-link { .admin-overview-link {
border: var(--border-line); border: var(--border-line);
border-radius: 6px; border-radius: var(--radius-compact);
background: var(--panel-soft); background: var(--panel-soft);
padding: 14px; padding: 14px;
text-align: left; text-align: left;
@@ -528,7 +528,7 @@
align-items: center; align-items: center;
gap: 18px; gap: 18px;
border: var(--border-line); border: var(--border-line);
border-radius: 6px; border-radius: var(--radius-compact);
background: var(--panel); background: var(--panel);
box-shadow: var(--shadow); box-shadow: var(--shadow);
padding: 14px 16px; padding: 14px 16px;
@@ -612,7 +612,7 @@
align-items: center; align-items: center;
gap: 14px; gap: 14px;
border: var(--border-line); border: var(--border-line);
border-radius: 6px; border-radius: var(--radius-compact);
background: var(--panel); background: var(--panel);
box-shadow: var(--shadow); box-shadow: var(--shadow);
padding: 14px 16px; padding: 14px 16px;
@@ -713,7 +713,7 @@
display: grid; display: grid;
gap: 10px; gap: 10px;
border: var(--border-line); border: var(--border-line);
border-radius: 6px; border-radius: var(--radius-compact);
background: var(--panel-soft); background: var(--panel-soft);
padding: 12px 14px; padding: 12px 14px;
} }
@@ -752,7 +752,7 @@
.module-install-preflight-issue { .module-install-preflight-issue {
min-height: 30px; min-height: 30px;
padding: 6px 8px; padding: 6px 8px;
border-radius: 4px; border-radius: var(--radius-sm);
background: var(--panel-glass); background: var(--panel-glass);
} }
@@ -783,7 +783,7 @@
align-content: start; align-content: start;
min-height: 86px; min-height: 86px;
border: var(--border-line); border: var(--border-line);
border-radius: 6px; border-radius: var(--radius-compact);
background: var(--panel-glass); background: var(--panel-glass);
padding: 9px 10px; padding: 9px 10px;
} }
@@ -795,7 +795,7 @@
.module-install-checklist-item span { .module-install-checklist-item span {
justify-self: start; justify-self: start;
border-radius: 999px; border-radius: var(--radius-pill);
background: var(--panel); background: var(--panel);
color: var(--muted); color: var(--muted);
font-size: 11px; font-size: 11px;
@@ -840,7 +840,7 @@
gap: 12px; gap: 12px;
align-items: end; align-items: end;
border: var(--border-line); border: var(--border-line);
border-radius: 6px; border-radius: var(--radius-compact);
background: var(--panel); background: var(--panel);
box-shadow: var(--shadow); box-shadow: var(--shadow);
padding: 14px 16px; padding: 14px 16px;
@@ -915,7 +915,7 @@
align-items: center; align-items: center;
gap: 14px; gap: 14px;
border: var(--border-line); border: var(--border-line);
border-radius: 6px; border-radius: var(--radius-compact);
background: var(--panel); background: var(--panel);
box-shadow: var(--shadow); box-shadow: var(--shadow);
padding: 14px 16px; padding: 14px 16px;
@@ -1063,7 +1063,7 @@
align-items: start; align-items: start;
gap: 9px; gap: 9px;
padding: 8px 9px; padding: 8px 9px;
border-radius: 7px; border-radius: var(--radius-md);
cursor: pointer; cursor: pointer;
} }
@@ -1180,7 +1180,7 @@
min-width: 0; min-width: 0;
padding: 15px; padding: 15px;
border: var(--border-line); border: var(--border-line);
border-radius: 7px; border-radius: var(--radius-md);
background: var(--panel); background: var(--panel);
} }
@@ -1205,7 +1205,7 @@
.advanced-options-panel { .advanced-options-panel {
border: var(--border-line); border: var(--border-line);
border-radius: 7px; border-radius: var(--radius-md);
background: var(--panel); background: var(--panel);
} }
@@ -1255,7 +1255,7 @@
gap: 10px; gap: 10px;
padding: 12px 13px; padding: 12px 13px;
border: 1px solid color-mix(in srgb, var(--amber) 45%, var(--line)); border: 1px solid color-mix(in srgb, var(--amber) 45%, var(--line));
border-radius: 7px; border-radius: var(--radius-md);
background: color-mix(in srgb, var(--amber) 12%, var(--panel)); background: color-mix(in srgb, var(--amber) 12%, var(--panel));
} }
@@ -1330,7 +1330,7 @@
.guided-review-list.is-empty { .guided-review-list.is-empty {
padding: 12px; padding: 12px;
border: 1px dashed var(--line); border: 1px dashed var(--line);
border-radius: 7px; border-radius: var(--radius-md);
color: var(--muted); color: var(--muted);
} }
@@ -1344,7 +1344,7 @@
border: var(--border-line); border: var(--border-line);
border-left-width: 4px; border-left-width: 4px;
border-left-color: var(--line-dark); border-left-color: var(--line-dark);
border-radius: 7px; border-radius: var(--radius-md);
background: var(--panel); background: var(--panel);
} }
@@ -1390,7 +1390,7 @@
line-height: 1.4; line-height: 1.4;
} }
@media (max-width: 840px) { @media (max-width: 900px) {
.guided-config-shell { .guided-config-shell {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
@@ -1421,7 +1421,7 @@
gap: 7px; gap: 7px;
min-height: 44px; min-height: 44px;
border: var(--border-line); border: var(--border-line);
border-radius: 8px; border-radius: var(--radius-md);
background: var(--surface); background: var(--surface);
box-shadow: var(--shadow-control-surface); box-shadow: var(--shadow-control-surface);
padding: 7px 8px; padding: 7px 8px;
@@ -1473,7 +1473,7 @@
width: 28px; width: 28px;
height: 28px; height: 28px;
border: 1px solid var(--control-border); border: 1px solid var(--control-border);
border-radius: 999px; border-radius: var(--radius-pill);
background: linear-gradient(var(--control-gradient-start), var(--control-gradient-end-muted)); background: linear-gradient(var(--control-gradient-start), var(--control-gradient-end-muted));
color: var(--control-text); color: var(--control-text);
cursor: pointer; cursor: pointer;
@@ -1495,7 +1495,7 @@
display: grid; display: grid;
gap: 10px; gap: 10px;
border: var(--border-line-dark); border: var(--border-line-dark);
border-radius: 8px; border-radius: var(--radius-md);
background: var(--surface); background: var(--surface);
box-shadow: var(--shadow-popover); box-shadow: var(--shadow-popover);
padding: 14px; padding: 14px;
@@ -1520,7 +1520,7 @@
gap: 4px; gap: 4px;
max-width: 560px; max-width: 560px;
border: var(--border-line); border: var(--border-line);
border-radius: 8px; border-radius: var(--radius-md);
background: var(--surface); background: var(--surface);
box-shadow: 0 8px 24px var(--hover-tint); box-shadow: 0 8px 24px var(--hover-tint);
padding: 6px; padding: 6px;
@@ -1531,7 +1531,7 @@
justify-content: space-between; justify-content: space-between;
gap: 12px; gap: 12px;
border: 0; border: 0;
border-radius: 6px; border-radius: var(--radius-compact);
background: transparent; background: transparent;
color: var(--text-strong); color: var(--text-strong);
cursor: pointer; cursor: pointer;
@@ -1744,7 +1744,7 @@
max-height: 360px; max-height: 360px;
overflow: auto; overflow: auto;
border: var(--border-line-dark); border: var(--border-line-dark);
border-radius: 8px; border-radius: var(--radius-md);
background: var(--surface); background: var(--surface);
box-shadow: var(--shadow-popover); box-shadow: var(--shadow-popover);
padding: 5px; padding: 5px;
@@ -1773,7 +1773,7 @@
gap: 16px; gap: 16px;
width: 100%; width: 100%;
border: 0; border: 0;
border-radius: 6px; border-radius: var(--radius-compact);
background: transparent; background: transparent;
color: var(--text); color: var(--text);
cursor: pointer; cursor: pointer;
@@ -1831,7 +1831,7 @@
gap: 12px; gap: 12px;
width: 100%; width: 100%;
border: var(--border-line); border: var(--border-line);
border-radius: 8px; border-radius: var(--radius-md);
background: var(--surface-subtle); background: var(--surface-subtle);
padding: 12px; padding: 12px;
} }
@@ -1896,7 +1896,7 @@
width: 42px; width: 42px;
height: 24px; height: 24px;
border: 1px solid var(--toggle-track-border); border: 1px solid var(--toggle-track-border);
border-radius: 999px; border-radius: var(--radius-pill);
background: var(--toggle-track-bg); background: var(--toggle-track-bg);
box-shadow: var(--shadow-control-inset-strong); box-shadow: var(--shadow-control-inset-strong);
transition: background-color .16s ease, border-color .16s ease, box-shadow .16s ease; transition: background-color .16s ease, border-color .16s ease, box-shadow .16s ease;
@@ -1907,7 +1907,7 @@
left: 2px; left: 2px;
width: 18px; width: 18px;
height: 18px; height: 18px;
border-radius: 999px; border-radius: var(--radius-pill);
background: var(--surface); background: var(--surface);
box-shadow: var(--shadow-thumb); box-shadow: var(--shadow-thumb);
transition: transform .16s ease; transition: transform .16s ease;
@@ -1988,7 +1988,7 @@
justify-content: center; justify-content: center;
width: 16px; width: 16px;
height: 16px; height: 16px;
border-radius: 999px; border-radius: var(--radius-pill);
color: var(--text-subtle); color: var(--text-subtle);
background: var(--line-dark); background: var(--line-dark);
font-size: 11px; font-size: 11px;
@@ -2005,7 +2005,7 @@
flex: 0 0 auto; flex: 0 0 auto;
width: 18px; width: 18px;
height: 18px; height: 18px;
border-radius: 3px; border-radius: var(--radius-tight);
color: var(--text-subtle); color: var(--text-subtle);
text-decoration: none; text-decoration: none;
} }
@@ -2119,7 +2119,7 @@
max-width: min(320px, calc(100vw - 48px)); max-width: min(320px, calc(100vw - 48px));
transform: translate(-50%, 3px); transform: translate(-50%, 3px);
border: var(--border-line-dark); border: var(--border-line-dark);
border-radius: 7px; border-radius: var(--radius-md);
background: var(--surface); background: var(--surface);
box-shadow: var(--shadow-popover); box-shadow: var(--shadow-popover);
color: var(--text); color: var(--text);
@@ -2220,7 +2220,7 @@
width: min(280px, 72vw); width: min(280px, 72vw);
padding: 9px 11px; padding: 9px 11px;
border: 1px solid var(--border-danger-soft); border: 1px solid var(--border-danger-soft);
border-radius: var(--radius-sm, 8px); border-radius: var(--radius-md);
color: var(--danger-text-tooltip); color: var(--danger-text-tooltip);
background: var(--danger-muted-bg); background: var(--danger-muted-bg);
box-shadow: var(--shadow-popover); box-shadow: var(--shadow-popover);
@@ -2253,7 +2253,7 @@
place-items: center; place-items: center;
min-height: 120px; min-height: 120px;
padding: 1.25rem; padding: 1.25rem;
border-radius: var(--radius-lg, 18px); border-radius: var(--radius-lg);
background: var(--transparent-surface); background: var(--transparent-surface);
backdrop-filter: blur(1.5px); backdrop-filter: blur(1.5px);
margin: -10px; margin: -10px;
@@ -2265,7 +2265,7 @@
gap: 0.65rem; gap: 0.65rem;
padding: 0.75rem 1rem; padding: 0.75rem 1rem;
border: 1px solid var(--border-soft); border: 1px solid var(--border-soft);
border-radius: 999px; border-radius: var(--radius-pill);
color: var(--text); color: var(--text);
background: var(--panel-glass-strong); background: var(--panel-glass-strong);
box-shadow: var(--shadow-soft); box-shadow: var(--shadow-soft);
@@ -2284,7 +2284,7 @@
.module-load-error .alert { .module-load-error .alert {
width: min(640px, 100%); width: min(640px, 100%);
max-width: 640px; max-width: 680px;
margin: 0; margin: 0;
} }
@@ -2314,7 +2314,7 @@
width: 30px; width: 30px;
height: 30px; height: 30px;
border: 1px solid var(--control-border); border: 1px solid var(--control-border);
border-radius: 999px; border-radius: var(--radius-pill);
background: linear-gradient(var(--control-gradient-start), var(--control-gradient-end-muted)); background: linear-gradient(var(--control-gradient-start), var(--control-gradient-end-muted));
color: var(--control-text); color: var(--control-text);
cursor: pointer; cursor: pointer;
@@ -2408,7 +2408,7 @@
width: 100%; width: 100%;
margin: 0; margin: 0;
border: 1px solid var(--border-soft); border: 1px solid var(--border-soft);
border-radius: 12px; border-radius: var(--radius-lg);
box-shadow: var(--shadow-floating); box-shadow: var(--shadow-floating);
pointer-events: auto; pointer-events: auto;
} }
@@ -2421,7 +2421,7 @@
cursor: pointer; cursor: pointer;
line-height: 1; line-height: 1;
padding: 2px; padding: 2px;
border-radius: 999px; border-radius: var(--radius-pill);
flex: 0 0 auto; flex: 0 0 auto;
} }
@@ -2500,7 +2500,7 @@
cursor: pointer; cursor: pointer;
font-weight: 800; font-weight: 800;
padding: 4px 6px; padding: 4px 6px;
border-radius: 7px; border-radius: var(--radius-md);
} }
.file-breadcrumb:hover:not(:disabled), .file-breadcrumb:hover:not(:disabled),
@@ -2651,7 +2651,7 @@
grid-template-columns: 26px minmax(0, 1fr); grid-template-columns: 26px minmax(0, 1fr);
align-items: center; align-items: center;
gap: 2px; gap: 2px;
border-radius: 9px; border-radius: var(--radius-md);
} }
.explorer-tree-node-wrap.has-actions { .explorer-tree-node-wrap.has-actions {
@@ -2671,7 +2671,7 @@
place-items: center; place-items: center;
width: 26px; width: 26px;
height: 32px; height: 32px;
border-radius: 9px; border-radius: var(--radius-md);
color: var(--muted); color: var(--muted);
} }
@@ -2692,7 +2692,7 @@
min-width: 0; min-width: 0;
width: 100%; width: 100%;
padding: 8px 9px; padding: 8px 9px;
border-radius: 9px; border-radius: var(--radius-md);
font: inherit; font: inherit;
text-align: left; text-align: left;
user-select: none; user-select: none;
@@ -2844,7 +2844,7 @@
.file-tree-select { .file-tree-select {
width: 24px; width: 24px;
height: 24px; height: 24px;
border-radius: 999px; border-radius: var(--radius-pill);
color: var(--text-strong); color: var(--text-strong);
font-weight: 900; font-weight: 900;
line-height: 1; line-height: 1;
@@ -2890,7 +2890,7 @@
max-width: 100%; max-width: 100%;
overflow: hidden; overflow: hidden;
border: var(--border-line-dark); border: var(--border-line-dark);
border-radius: 8px; border-radius: var(--radius-md);
background: var(--line-dark); background: var(--line-dark);
box-shadow: var(--shadow-control); box-shadow: var(--shadow-control);
} }
@@ -2931,11 +2931,11 @@
} }
.segmented-control-option:first-child { .segmented-control-option:first-child {
border-radius: 7px 0 0 7px; border-radius: var(--radius-md) 0 0 var(--radius-md);
} }
.segmented-control-option:last-child { .segmented-control-option:last-child {
border-radius: 0 7px 7px 0; border-radius: 0 var(--radius-md) var(--radius-md) 0;
} }
.segmented-control-option:hover, .segmented-control-option:hover,
@@ -3052,7 +3052,7 @@
min-width: 21px; min-width: 21px;
height: 21px; height: 21px;
padding: 0 6px; padding: 0 6px;
border-radius: 999px; border-radius: var(--radius-pill);
font-size: 12px; font-size: 12px;
font-weight: 800; font-weight: 800;
line-height: 1; line-height: 1;
@@ -3060,7 +3060,7 @@
} }
.count-badge-compact { min-width: 18px; height: 18px; padding-inline: 5px; font-size: 10px; } .count-badge-compact { min-width: 18px; height: 18px; padding-inline: 5px; font-size: 10px; }
.count-badge-accent { background: var(--accent); color: var(--on-accent); } .count-badge-accent { background: var(--accent); color: var(--badge-accent-text); }
.count-badge-neutral { border: var(--border-line); background: var(--surface-subtle); color: var(--text); } .count-badge-neutral { border: var(--border-line); background: var(--surface-subtle); color: var(--text); }
.count-badge-danger { background: var(--red); color: var(--on-accent); } .count-badge-danger { background: var(--red); color: var(--on-accent); }
@@ -3094,7 +3094,7 @@
.definition-graph-canvas .react-flow__edge.selected .react-flow__edge-path, .definition-graph-canvas .react-flow__edge.selected .react-flow__edge-path,
.definition-graph-canvas .react-flow__edge:hover .react-flow__edge-path { stroke: var(--accent); stroke-width: 3; } .definition-graph-canvas .react-flow__edge:hover .react-flow__edge-path { stroke: var(--accent); stroke-width: 3; }
.definition-graph-canvas-empty { position: absolute; inset: 0; pointer-events: none; } .definition-graph-canvas-empty { position: absolute; inset: 0; pointer-events: none; }
.definition-node-icon { display: grid; width: 28px; height: 28px; flex: 0 0 28px; place-items: center; border-radius: 5px; background: var(--panel-soft); color: var(--text-strong); } .definition-node-icon { display: grid; width: 28px; height: 28px; flex: 0 0 28px; place-items: center; border-radius: var(--radius-xs); background: var(--panel-soft); color: var(--text-strong); }
.definition-node-handle { width: 13px; height: 13px; border: 3px solid var(--panel); background: var(--line-dark); } .definition-node-handle { width: 13px; height: 13px; border: 3px solid var(--panel); background: var(--line-dark); }
.definition-node-handle:hover, .definition-node-handle:hover,
.definition-node-handle.connectingto, .definition-node-handle.connectingto,
@@ -3202,7 +3202,7 @@
.theme-preview-header i { .theme-preview-header i {
width: 18px; width: 18px;
height: 18px; height: 18px;
border-radius: 50%; border-radius: var(--radius-round);
background: var(--accent); background: var(--accent);
box-shadow: 0 0 0 3px var(--accent-soft); box-shadow: 0 0 0 3px var(--accent-soft);
} }
@@ -3223,7 +3223,7 @@
.theme-preview-body span { .theme-preview-body span {
display: block; display: block;
height: 8px; height: 8px;
border-radius: 4px; border-radius: var(--radius-sm);
background: var(--preview-line); background: var(--preview-line);
} }
@@ -3517,7 +3517,7 @@
} }
.mail-server-toggle-row { .mail-server-toggle-row {
border: var(--border-line); border: var(--border-line);
border-radius: 8px; border-radius: var(--radius-md);
background: var(--panel-soft); background: var(--panel-soft);
} }
.mail-server-plain-toggle-row { .mail-server-plain-toggle-row {
@@ -3547,40 +3547,32 @@
align-items: center; align-items: center;
min-height: 24px; min-height: 24px;
border: var(--border-line); border: var(--border-line);
border-radius: 99px; border-radius: var(--radius-pill);
background: var(--panel-soft); background: var(--panel-soft);
padding: 3px 9px; padding: 3px 9px;
color: var(--text-strong); color: var(--text-strong);
font-size: 12px; font-size: 12px;
font-weight: 700; font-weight: 700;
} }
@media (max-width: 1000px) { @media (max-width: 1100px) {
.mail-server-settings-grid { grid-template-columns: 1fr; } .mail-server-settings-grid { grid-template-columns: 1fr; }
} }
@media (max-width: 900px) { @media (max-width: 900px) {
.mail-server-segmented-control { width: min(320px, 100%); } .mail-server-segmented-control { width: min(320px, 100%); }
} }
@media (max-width: 720px) { @media (max-width: 760px) {
.credential-panel-grid { grid-template-columns: 1fr; } .credential-panel-grid { grid-template-columns: 1fr; }
.mail-server-section-heading.split { align-items: flex-start; flex-direction: column; } .mail-server-section-heading.split { align-items: flex-start; flex-direction: column; }
.mail-server-segmented-control { width: 100%; } .mail-server-segmented-control { width: 100%; }
} }
.unsaved-changes-dialog {
max-width: 520px;
}
.unsaved-changes-actions {
justify-content: flex-end;
}
.file-drop-zone { .file-drop-zone {
display: grid; display: grid;
place-items: center; place-items: center;
gap: 8px; gap: 8px;
min-height: 170px; min-height: 170px;
border: 1px dashed var(--line-dark); border: 1px dashed var(--line-dark);
border-radius: 8px; border-radius: var(--radius-md);
background: var(--panel-soft); background: var(--panel-soft);
color: var(--muted); color: var(--muted);
text-align: center; text-align: center;
@@ -3614,7 +3606,7 @@
place-items: center; place-items: center;
width: 58px; width: 58px;
height: 58px; height: 58px;
border-radius: 50%; border-radius: var(--radius-round);
background: conic-gradient(var(--primary) var(--file-drop-progress), var(--primary-soft-strong) 0); background: conic-gradient(var(--primary) var(--file-drop-progress), var(--primary-soft-strong) 0);
color: var(--text-strong); color: var(--text-strong);
font-size: 12px; font-size: 12px;
@@ -3625,7 +3617,7 @@
content: ""; content: "";
position: absolute; position: absolute;
inset: 6px; inset: 6px;
border-radius: 50%; border-radius: var(--radius-round);
background: var(--panel-soft); background: var(--panel-soft);
box-shadow: var(--primary-inset-ring); box-shadow: var(--primary-inset-ring);
} }
@@ -3650,7 +3642,7 @@
.placeholder-stack span { .placeholder-stack span {
display: block; display: block;
border: 1px dashed var(--line-dark); border: 1px dashed var(--line-dark);
border-radius: 6px; border-radius: var(--radius-compact);
background: var(--panel-soft); background: var(--panel-soft);
padding: 10px 12px; padding: 10px 12px;
color: var(--muted); color: var(--muted);
@@ -3661,7 +3653,7 @@
gap: 12px; gap: 12px;
} }
.settings-target-row { .settings-target-row {
max-width: 520px; max-width: 560px;
} }
.admin-secret { .admin-secret {
display: block; display: block;
@@ -3901,12 +3893,12 @@ a.dashboard-contribution-row:hover {
.description-list-inline.description-list-collapse-wide > div { grid-template-columns: minmax(0, 1fr); } .description-list-inline.description-list-collapse-wide > div { grid-template-columns: minmax(0, 1fr); }
} }
@media (max-width: 980px) { @media (max-width: 1100px) {
.description-list-collapse-standard { grid-template-columns: 1fr; } .description-list-collapse-standard { grid-template-columns: 1fr; }
.description-list-inline.description-list-collapse-standard > div { grid-template-columns: minmax(0, 1fr); } .description-list-inline.description-list-collapse-standard > div { grid-template-columns: minmax(0, 1fr); }
} }
@media (max-width: 700px) { @media (max-width: 760px) {
.description-list-collapse-narrow { grid-template-columns: 1fr; } .description-list-collapse-narrow { grid-template-columns: 1fr; }
.description-list-inline.description-list-collapse-narrow > div { grid-template-columns: minmax(0, 1fr); } .description-list-inline.description-list-collapse-narrow > div { grid-template-columns: minmax(0, 1fr); }
} }
@@ -3962,7 +3954,7 @@ button.stage-rail-item:focus-visible {
height: 38px; height: 38px;
place-items: center; place-items: center;
border: 1px solid var(--stage-rail-color); border: 1px solid var(--stage-rail-color);
border-radius: 50%; border-radius: var(--radius-round);
background: color-mix(in srgb, var(--stage-rail-color) 8%, var(--surface)); background: color-mix(in srgb, var(--stage-rail-color) 8%, var(--surface));
color: color-mix(in srgb, var(--stage-rail-color) 82%, var(--text)); color: color-mix(in srgb, var(--stage-rail-color) 82%, var(--text));
transition: background .16s ease, box-shadow .16s ease; transition: background .16s ease, box-shadow .16s ease;
@@ -4015,7 +4007,7 @@ button.stage-rail-item:focus-visible {
min-width: 0; min-width: 0;
overflow: hidden; overflow: hidden;
border: var(--border-line); border: var(--border-line);
border-radius: 5px; border-radius: var(--radius-xs);
background: var(--surface); background: var(--surface);
box-shadow: var(--shadow-control-inset); box-shadow: var(--shadow-control-inset);
transition: border-color .16s ease, box-shadow .16s ease; transition: border-color .16s ease, box-shadow .16s ease;
@@ -4160,7 +4152,7 @@ button.stage-rail-item:focus-visible {
.wysiwyg-prosemirror pre { .wysiwyg-prosemirror pre {
overflow: auto; overflow: auto;
padding: 10px 12px; padding: 10px 12px;
border-radius: 4px; border-radius: var(--radius-sm);
background: var(--code-bg); background: var(--code-bg);
color: var(--code-text); color: var(--code-text);
} }
@@ -4189,7 +4181,7 @@ button.stage-rail-item:focus-visible {
margin: 0 2px; margin: 0 2px;
padding: 1px 6px; padding: 1px 6px;
border: 1px solid var(--primary-border); border: 1px solid var(--primary-border);
border-radius: 4px; border-radius: var(--radius-sm);
background: var(--primary-soft); background: var(--primary-soft);
color: var(--info-text-deep); color: var(--info-text-deep);
font-family: var(--font-mono, monospace); font-family: var(--font-mono, monospace);
+3 -3
View File
@@ -16,7 +16,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: var(--border-line); border: var(--border-line);
border-radius: 8px; border-radius: var(--radius-md);
background: var(--surface); background: var(--surface);
box-shadow: var(--shadow-modal); box-shadow: var(--shadow-modal);
} }
@@ -80,7 +80,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
border: 1px solid var(--line); border: 1px solid var(--line);
border-radius: var(--radius-lg, 18px); border-radius: var(--radius-lg);
background: var(--surface); background: var(--surface);
color: var(--text); color: var(--text);
box-shadow: var(--shadow-strong); box-shadow: var(--shadow-strong);
@@ -117,7 +117,7 @@
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
border: 0; border: 0;
border-radius: 999px; border-radius: var(--radius-pill);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
+6 -6
View File
@@ -28,7 +28,7 @@
.form-field { display: grid; gap: 7px; } .form-field { display: grid; gap: 7px; }
.form-label { font-weight: 700; font-size: 13px; color: var(--text-label); } .form-label { font-weight: 700; font-size: 13px; color: var(--text-label); }
.form-help { font-size: 12px; color: var(--muted); } .form-help { font-size: 12px; color: var(--muted); }
input:not([type="checkbox"]):not([type="radio"]), select, textarea { border: var(--border-line); border-radius: 5px; background: var(--surface); font: inherit; padding: 10px 12px; color: var(--text); width: 100%; box-shadow: var(--shadow-control-inset); } input:not([type="checkbox"]):not([type="radio"]), select, textarea { border: var(--border-line); border-radius: var(--radius-xs); background: var(--surface); font: inherit; padding: 10px 12px; color: var(--text); width: 100%; box-shadow: var(--shadow-control-inset); }
input[type="checkbox"]:not(.toggle-switch-input), input[type="checkbox"]:not(.toggle-switch-input),
input[type="radio"] { input[type="radio"] {
accent-color: var(--primary); accent-color: var(--primary);
@@ -49,7 +49,7 @@ textarea { resize: vertical; }
.content-grid-collapse-wide > .content-grid-item-two { grid-column: auto; } .content-grid-collapse-wide > .content-grid-item-two { grid-column: auto; }
} }
@media (max-width: 980px) { @media (max-width: 1100px) {
.content-grid-collapse-standard { grid-template-columns: minmax(0, 1fr); } .content-grid-collapse-standard { grid-template-columns: minmax(0, 1fr); }
.content-grid-collapse-standard > .content-grid-item-two { grid-column: auto; } .content-grid-collapse-standard > .content-grid-item-two { grid-column: auto; }
} }
@@ -59,7 +59,7 @@ textarea { resize: vertical; }
.content-grid-collapse-workspace > .content-grid-item-two { grid-column: auto; } .content-grid-collapse-workspace > .content-grid-item-two { grid-column: auto; }
} }
@media (max-width: 700px) { @media (max-width: 760px) {
.content-grid-collapse-narrow { grid-template-columns: minmax(0, 1fr); } .content-grid-collapse-narrow { grid-template-columns: minmax(0, 1fr); }
.content-grid-collapse-narrow > .content-grid-item-two { grid-column: auto; } .content-grid-collapse-narrow > .content-grid-item-two { grid-column: auto; }
.form-section-header { flex-direction: column; } .form-section-header { flex-direction: column; }
@@ -93,8 +93,8 @@ textarea[readonly]::placeholder {
color: var(--control-disabled-placeholder); color: var(--control-disabled-placeholder);
opacity: 1; opacity: 1;
} }
.btn { align-items: center; border: var(--border-line-dark); border-radius: 4px; cursor: pointer; display: inline-flex; font: inherit; font-weight: 700; gap: 7px; justify-content: center; padding: 9px 14px; background: var(--control-bg); color: var(--text); box-shadow: 0 1px 2px var(--hover-tint-strong); } .btn { align-items: center; border: var(--border-line-dark); border-radius: var(--radius-sm); cursor: pointer; display: inline-flex; font: inherit; font-weight: 700; gap: 7px; justify-content: center; padding: 9px 14px; background: var(--control-bg); color: var(--text); box-shadow: 0 1px 2px var(--hover-tint-strong); }
.btn-primary { background: var(--green); border-color: var(--success-border); color: var(--on-accent); } .btn-primary { background: var(--action-primary-bg); border-color: var(--action-primary-border); color: var(--action-primary-text); }
.btn-ghost { border-color: transparent; background: transparent; box-shadow: none; } .btn-ghost { border-color: transparent; background: transparent; box-shadow: none; }
.btn-danger { background: var(--red); color: var(--on-accent); border-color: var(--danger-border-strong); } .btn-danger { background: var(--action-danger-bg); color: var(--action-danger-text); border-color: var(--danger-border-strong); }
.btn:disabled { opacity: .55; cursor: not-allowed; } .btn:disabled { opacity: .55; cursor: not-allowed; }
+28 -23
View File
@@ -2,13 +2,13 @@
.icon-rail { width: 58px; background: var(--rail-bg); color: var(--rail-text); display: flex; flex-direction: column; align-items: center; height: 100vh; min-height: 0; box-shadow: var(--shadow-rail); z-index: 1000; transition: width .16s ease; } .icon-rail { width: 58px; background: var(--rail-bg); color: var(--rail-text); display: flex; flex-direction: column; align-items: center; height: 100vh; min-height: 0; box-shadow: var(--shadow-rail); z-index: 1000; transition: width .16s ease; }
.icon-rail.expanded { width: 208px; align-items: stretch; } .icon-rail.expanded { width: 208px; align-items: stretch; }
.icon-rail-header { width: 100%; min-height: 63px; display: flex; flex: 0 0 auto; align-items: center; justify-content: center; padding: 0 10px; box-sizing: border-box; } .icon-rail-header { width: 100%; min-height: 63px; display: flex; flex: 0 0 auto; align-items: center; justify-content: center; padding: 0 10px; box-sizing: border-box; }
.brand-mark { width: 34px; height: 34px; flex: 0 0 auto; border-radius: 50%; background: conic-gradient(var(--accent) 0 20%, var(--amber) 0 40%, var(--green) 0 60%, var(--blue) 0 80%, var(--muted) 0); color: transparent; font-size: 0; position: relative; } .brand-mark { width: 34px; height: 34px; flex: 0 0 auto; border-radius: var(--radius-round); background: conic-gradient(var(--accent) 0 20%, var(--amber) 0 40%, var(--green) 0 60%, var(--blue) 0 80%, var(--muted) 0); color: transparent; font-size: 0; position: relative; }
.brand-mark::after { position: absolute; .brand-mark::after { position: absolute;
top: 9px; top: 9px;
left: 9px; left: 9px;
width: 15px; width: 15px;
height: 15px; height: 15px;
border-radius: 50%; border-radius: var(--radius-round);
content: ""; content: "";
background-color: var(--rail-bg); background-color: var(--rail-bg);
} }
@@ -46,7 +46,7 @@
.small-note { font-size: 12px; } .small-note { font-size: 12px; }
.titlebar-spacer { flex: 1; } .titlebar-spacer { flex: 1; }
.titlebar-link, .titlebar-icon-link, .account-pill { border: 0; background: transparent; display: inline-flex; align-items: center; gap: 7px; color: var(--muted); font: inherit; } .titlebar-link, .titlebar-icon-link, .account-pill { border: 0; background: transparent; display: inline-flex; align-items: center; gap: 7px; color: var(--muted); font: inherit; }
.titlebar-icon-link { width: 34px; height: 34px; justify-content: center; border-radius: 4px; cursor: pointer; } .titlebar-icon-link { width: 34px; height: 34px; justify-content: center; border-radius: var(--radius-sm); cursor: pointer; }
.titlebar-icon-link:hover, .titlebar-link:hover { background: var(--titlebar-hover-bg); color: var(--text-strong); } .titlebar-icon-link:hover, .titlebar-link:hover { background: var(--titlebar-hover-bg); color: var(--text-strong); }
.titlebar-icon-link.is-context-active { background: var(--accent-hover-bg); color: var(--accent); } .titlebar-icon-link.is-context-active { background: var(--accent-hover-bg); color: var(--accent); }
.titlebar-icon-link.is-context-active:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); } .titlebar-icon-link.is-context-active:hover { background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); }
@@ -54,7 +54,7 @@
.titlebar-notification-badge { position: absolute; top: 4px; right: 3px; border: 2px solid var(--titlebar-bg); transform: translate(35%, -35%); } .titlebar-notification-badge { position: absolute; top: 4px; right: 3px; border: 2px solid var(--titlebar-bg); transform: translate(35%, -35%); }
.account-pill { color: var(--text); } .account-pill { color: var(--text); }
.maintenance-topbar-link, .maintenance-topbar-link,
.backend-offline-topbar-alert { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); min-height: 32px; max-width: min(560px, calc(100vw - 24px)); box-sizing: border-box; border-radius: 6px; display: inline-flex; align-items: center; justify-content: center; padding: 0 14px; overflow: hidden; font: inherit; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; box-shadow: 0 1px 2px var(--hover-tint); z-index: 1; } .backend-offline-topbar-alert { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); min-height: 32px; max-width: min(560px, calc(100vw - 24px)); box-sizing: border-box; border-radius: var(--radius-compact); display: inline-flex; align-items: center; justify-content: center; padding: 0 14px; overflow: hidden; font: inherit; font-weight: 800; text-overflow: ellipsis; white-space: nowrap; box-shadow: 0 1px 2px var(--hover-tint); z-index: 1; }
.maintenance-topbar-link { border: 1px solid var(--warning-border-soft); background: var(--warning-bg); color: var(--warning-text); cursor: pointer; } .maintenance-topbar-link { border: 1px solid var(--warning-border-soft); background: var(--warning-bg); color: var(--warning-text); cursor: pointer; }
.maintenance-topbar-link:hover { background: var(--warning-bg-hover); color: var(--warning-text-hover); } .maintenance-topbar-link:hover { background: var(--warning-bg-hover); color: var(--warning-text-hover); }
.backend-offline-topbar-alert { border: 1px solid var(--danger-border-deep); background: var(--red); color: var(--on-accent); } .backend-offline-topbar-alert { border: 1px solid var(--danger-border-deep); background: var(--red); color: var(--on-accent); }
@@ -79,6 +79,9 @@
.breadcrumbs { display: flex; gap: 6px; text-transform: uppercase; font-weight: 700; font-size: 13px; color: var(--text-breadcrumb); } .breadcrumbs { display: flex; gap: 6px; text-transform: uppercase; font-weight: 700; font-size: 13px; color: var(--text-breadcrumb); }
.crumb { display: inline-flex; align-items: center; gap: 4px; } .crumb { display: inline-flex; align-items: center; gap: 4px; }
.breadcrumb-actions { margin-left: auto; display: flex; gap: 10px; } .breadcrumb-actions { margin-left: auto; display: flex; gap: 10px; }
.launch-return-button { margin-left: auto; display: inline-flex; align-items: center; gap: 7px; flex: 0 0 auto; cursor: pointer; }
.launch-return-button:hover,
.launch-return-button:focus-visible { color: var(--text-strong); }
.ghost-button { border: 0; background: transparent; color: var(--muted); font-weight: 700; } .ghost-button { border: 0; background: transparent; color: var(--muted); font-weight: 700; }
.action-toolbar { min-width: 0; display: flex; align-items: center; gap: 10px; } .action-toolbar { min-width: 0; display: flex; align-items: center; gap: 10px; }
.action-toolbar-density-compact { gap: 7px; } .action-toolbar-density-compact { gap: 7px; }
@@ -298,7 +301,7 @@
.stepper { list-style: none; padding: 22px 0; margin: 0; background: var(--panel-soft); border-right: var(--border-line); } .stepper { list-style: none; padding: 22px 0; margin: 0; background: var(--panel-soft); border-right: var(--border-line); }
.step button { width: 100%; min-height: 72px; border: 0; background: transparent; display: flex; gap: 14px; text-align: left; padding: 12px 20px; color: var(--muted); cursor: pointer; } .step button { width: 100%; min-height: 72px; border: 0; background: transparent; display: flex; gap: 14px; text-align: left; padding: 12px 20px; color: var(--muted); cursor: pointer; }
.step.active button { background: var(--surface); color: var(--text-strong); } .step.active button { background: var(--surface); color: var(--text-strong); }
.step-number { width: 32px; height: 32px; border-radius: 50%; background: var(--step-number-bg); color: var(--on-accent); display: grid; place-items: center; font-weight: 800; flex: 0 0 auto; } .step-number { width: 32px; height: 32px; border-radius: var(--radius-round); background: var(--step-number-bg); color: var(--on-accent); display: grid; place-items: center; font-weight: 800; flex: 0 0 auto; }
.step.active .step-number { background: var(--accent); } .step.active .step-number { background: var(--accent); }
.step small { display: block; margin-top: 3px; color: var(--muted); } .step small { display: block; margin-top: 3px; color: var(--muted); }
.step-intro h2 { margin: 0; color: var(--text-strong); } .step-intro h2 { margin: 0; color: var(--text-strong); }
@@ -321,12 +324,14 @@
.table-row-link { display: grid; grid-template-columns: 1fr auto; text-decoration: none; color: var(--text); padding: 14px 16px; border-bottom: var(--border-line); } .table-row-link { display: grid; grid-template-columns: 1fr auto; text-decoration: none; color: var(--text); padding: 14px 16px; border-bottom: var(--border-line); }
.table-row-link:hover { background: var(--panel-soft); } .table-row-link:hover { background: var(--panel-soft); }
.mono-small { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted); } .mono-small { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; font-size: 12px; color: var(--muted); }
.code-panel { background: var(--code-bg); color: var(--code-text); padding: 18px; border-radius: 4px; overflow: auto; } .code-panel { background: var(--code-bg); color: var(--code-text); padding: 18px; border-radius: var(--radius-sm); overflow: auto; }
@media (max-width: 900px) { @media (max-width: 900px) {
.api-mini { display: none; } .api-mini { display: none; }
.workspace { grid-template-columns: 1fr; } .workspace { grid-template-columns: 1fr; }
.workspace-layout-navigation > .workspace-layout-primary { display: none; } .workspace-layout-navigation > .workspace-layout-primary { display: none; }
.workspace-layout-split { grid-template-columns: 1fr; grid-template-rows: minmax(160px, 34%) minmax(0, 1fr); } .workspace-layout-split,
.workspace-layout-split.workspace-layout-primary-compact,
.workspace-layout-split.workspace-layout-primary-wide { grid-template-columns: 1fr; grid-template-rows: minmax(160px, 34%) minmax(0, 1fr); }
.workspace-layout-surface-contained > .workspace-layout-primary { border-right: 0; border-bottom: var(--border-line); } .workspace-layout-surface-contained > .workspace-layout-primary { border-right: 0; border-bottom: var(--border-line); }
.section-sidebar { display: none; } .section-sidebar { display: none; }
.wizard-card { grid-template-columns: 1fr; } .wizard-card { grid-template-columns: 1fr; }
@@ -338,15 +343,15 @@
.metric-group-collapse-wide { grid-template-columns: minmax(0, 1fr); } .metric-group-collapse-wide { grid-template-columns: minmax(0, 1fr); }
} }
@media (max-width: 980px) { @media (max-width: 1100px) {
.metric-group-collapse-standard { grid-template-columns: minmax(0, 1fr); } .metric-group-collapse-standard { grid-template-columns: minmax(0, 1fr); }
} }
@media (max-width: 700px) { @media (max-width: 760px) {
.metric-group-collapse-narrow { grid-template-columns: minmax(0, 1fr); } .metric-group-collapse-narrow { grid-template-columns: minmax(0, 1fr); }
} }
@media (max-width: 700px) { @media (max-width: 760px) {
.action-toolbar-wrap-responsive { align-items: stretch; flex-wrap: wrap; } .action-toolbar-wrap-responsive { align-items: stretch; flex-wrap: wrap; }
.action-toolbar-wrap-responsive > .action-toolbar-group { flex: 1 1 100%; } .action-toolbar-wrap-responsive > .action-toolbar-group { flex: 1 1 100%; }
.action-toolbar-wrap-responsive > .action-toolbar-group-end { margin-inline-start: 0; justify-content: flex-start; } .action-toolbar-wrap-responsive > .action-toolbar-group-end { margin-inline-start: 0; justify-content: flex-start; }
@@ -359,24 +364,24 @@
/* Layout additions: login and help dropdown */ /* Layout additions: login and help dropdown */
.tenant-selector.disabled { opacity: .86; cursor: default; } .tenant-selector.disabled { opacity: .86; cursor: default; }
.context-menu-wrap { position: relative; } .context-menu-wrap { position: relative; }
.dropdown-menu { position: absolute; right: 0; top: calc(100% + 10px); min-width: 230px; background: var(--surface); border: var(--border-line); border-radius: 6px; box-shadow: var(--shadow-menu); padding: 8px; z-index: 5000; } .dropdown-menu { position: absolute; right: 0; top: calc(100% + 10px); min-width: 230px; background: var(--surface); border: var(--border-line); border-radius: var(--radius-compact); box-shadow: var(--shadow-menu); padding: 8px; z-index: 5000; }
.dropdown-menu hr { border: 0; border-top: var(--border-line); margin: 8px 0; } .dropdown-menu hr { border: 0; border-top: var(--border-line); margin: 8px 0; }
.dropdown-item { width: 100%; min-height: 34px; border: 0; background: transparent; display: flex; align-items: center; gap: 8px; padding: 7px 9px; border-radius: 4px; text-align: left; color: var(--text); font: inherit; text-decoration: none; cursor: pointer; } .dropdown-item { width: 100%; min-height: 34px; border: 0; background: transparent; display: flex; align-items: center; gap: 8px; padding: 7px 9px; border-radius: var(--radius-sm); text-align: left; color: var(--text); font: inherit; text-decoration: none; cursor: pointer; }
.dropdown-item:hover { background: var(--panel-soft); color: var(--text-strong); } .dropdown-item:hover { background: var(--panel-soft); color: var(--text-strong); }
.dropdown-item small { margin-left: auto; color: var(--muted); } .dropdown-item small { margin-left: auto; color: var(--muted); }
.account-menu { min-width: 260px; } .account-menu { min-width: 260px; }
.account-menu-header { padding: 8px 9px 10px; border-bottom: var(--border-line); margin-bottom: 8px; } .account-menu-header { padding: 8px 9px 10px; border-bottom: var(--border-line); margin-bottom: 8px; }
.account-menu-header strong { display: block; color: var(--text-strong); } .account-menu-header strong { display: block; color: var(--text-strong); }
.account-menu-header span { color: var(--muted); font-size: 13px; } .account-menu-header span { color: var(--muted); font-size: 13px; }
.login-hint { background: var(--panel-soft); border: var(--border-line); padding: 10px 12px; border-radius: 4px; color: var(--muted); font-size: 13px; } .login-hint { background: var(--panel-soft); border: var(--border-line); padding: 10px 12px; border-radius: var(--radius-sm); color: var(--muted); font-size: 13px; }
.help-panel-section { margin-bottom: 16px; } .help-panel-section { margin-bottom: 16px; }
.help-panel-section h3 { margin: 0 0 7px; color: var(--text-strong); } .help-panel-section h3 { margin: 0 0 7px; color: var(--text-strong); }
.about-logo { width: 52px; height: 52px; border-radius: 50%; background: conic-gradient(var(--accent) 0 20%, var(--amber) 0 40%, var(--green) 0 60%, var(--blue) 0 80%, var(--muted) 0); margin-bottom: 12px; } .about-logo { width: 52px; height: 52px; border-radius: var(--radius-round); background: conic-gradient(var(--accent) 0 20%, var(--amber) 0 40%, var(--green) 0 60%, var(--blue) 0 80%, var(--muted) 0); margin-bottom: 12px; }
.kbd { display: inline-flex; min-width: 22px; height: 22px; align-items: center; justify-content: center; border: var(--border-line-dark); border-bottom-width: 2px; border-radius: 4px; padding: 0 6px; background: var(--surface); font-size: 12px; font-weight: 700; color: var(--text-soft); } .kbd { display: inline-flex; min-width: 22px; height: 22px; align-items: center; justify-content: center; border: var(--border-line-dark); border-bottom-width: 2px; border-radius: var(--radius-sm); padding: 0 6px; background: var(--surface); font-size: 12px; font-weight: 700; color: var(--text-soft); }
.titlebar .tenant-selector strong { max-width: 210px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .titlebar .tenant-selector strong { max-width: 210px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
/* Campaign workspace polish */ /* Campaign workspace polish */
.crumb-link { color: inherit; text-decoration: none; border-radius: 4px; padding: 4px 5px; margin: -4px -5px; } .crumb-link { color: inherit; text-decoration: none; border-radius: var(--radius-sm); padding: 4px 5px; margin: -4px -5px; }
.crumb-link:hover { background: var(--sidebar-hover-bg); color: var(--text-strong); } .crumb-link:hover { background: var(--sidebar-hover-bg); color: var(--text-strong); }
.compact-actions { margin: 0; align-items: center; } .compact-actions { margin: 0; align-items: center; }
.below-grid { margin-top: 18px; } .below-grid { margin-top: 18px; }
@@ -387,12 +392,12 @@
.next-action-warning { border-left-color: var(--amber); } .next-action-warning { border-left-color: var(--amber); }
.next-action-info { border-left-color: var(--blue); } .next-action-info { border-left-color: var(--blue); }
.summary-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px; } .summary-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 14px; }
.summary-tile { background: var(--panel-soft); border: var(--border-line); border-radius: 6px; padding: 14px; } .summary-tile { background: var(--panel-soft); border: var(--border-line); border-radius: var(--radius-compact); padding: 14px; }
.summary-tile span { display: block; color: var(--muted); font-size: 12px; text-transform: uppercase; font-weight: 800; letter-spacing: .04em; } .summary-tile span { display: block; color: var(--muted); font-size: 12px; text-transform: uppercase; font-weight: 800; letter-spacing: .04em; }
.summary-tile strong { display: block; margin-top: 6px; color: var(--text-strong); font-size: 24px; } .summary-tile strong { display: block; margin-top: 6px; color: var(--text-strong); font-size: 24px; }
.empty-state { min-height: 220px; display: grid; place-items: center; text-align: center; padding: 32px; } .empty-state { min-height: 220px; display: grid; place-items: center; text-align: center; padding: 32px; }
.empty-state h2 { margin: 0; color: var(--text-strong); } .empty-state h2 { margin: 0; color: var(--text-strong); }
.empty-state p { max-width: 520px; margin: 8px auto 18px; color: var(--muted); } .empty-state p { max-width: 560px; margin: 8px auto 18px; color: var(--muted); }
.state-panel { box-sizing: border-box; min-width: 0; display: grid; justify-items: center; align-content: center; gap: 8px; color: var(--muted); text-align: center; } .state-panel { box-sizing: border-box; min-width: 0; display: grid; justify-items: center; align-content: center; gap: 8px; color: var(--muted); text-align: center; }
.state-panel-size-inline { min-height: 0; padding: 8px 10px; } .state-panel-size-inline { min-height: 0; padding: 8px 10px; }
.state-panel-size-compact { min-height: 110px; padding: 18px; } .state-panel-size-compact { min-height: 110px; padding: 18px; }
@@ -407,7 +412,7 @@
.state-panel-icon { display: grid; place-items: center; color: currentColor; } .state-panel-icon { display: grid; place-items: center; color: currentColor; }
.state-panel-title { margin: 0; color: var(--text-strong); font-size: 18px; letter-spacing: 0; } .state-panel-title { margin: 0; color: var(--text-strong); font-size: 18px; letter-spacing: 0; }
.state-panel-description, .state-panel-description,
.state-panel-content { max-width: 520px; line-height: 1.45; } .state-panel-content { max-width: 560px; line-height: 1.45; }
.state-panel-description > :first-child, .state-panel-description > :first-child,
.state-panel-content > :first-child { margin-top: 0; } .state-panel-content > :first-child { margin-top: 0; }
.state-panel-description > :last-child, .state-panel-description > :last-child,
@@ -444,21 +449,21 @@
.docs-tree-node { min-width: 0; } .docs-tree-node { min-width: 0; }
.docs-tree-row { display: grid; grid-template-columns: 22px minmax(0, 1fr); align-items: start; gap: 3px; } .docs-tree-row { display: grid; grid-template-columns: 22px minmax(0, 1fr); align-items: start; gap: 3px; }
.docs-tree-toggle, .docs-tree-toggle-placeholder { width: 22px; height: 32px; display: inline-grid; place-items: center; flex: 0 0 auto; } .docs-tree-toggle, .docs-tree-toggle-placeholder { width: 22px; height: 32px; display: inline-grid; place-items: center; flex: 0 0 auto; }
.docs-tree-toggle { border: 0; border-radius: 4px; background: transparent; color: var(--text-subtle); cursor: pointer; } .docs-tree-toggle { border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--text-subtle); cursor: pointer; }
.docs-tree-toggle:hover { background: var(--sidebar-hover-bg); color: var(--text-strong); } .docs-tree-toggle:hover { background: var(--sidebar-hover-bg); color: var(--text-strong); }
.docs-tree-toggle-placeholder { opacity: 0; } .docs-tree-toggle-placeholder { opacity: 0; }
.docs-tree-children { display: grid; gap: 2px; } .docs-tree-children { display: grid; gap: 2px; }
.docs-tree-page { width: 100%; min-height: 32px; border: 0; border-radius: 4px; background: transparent; color: var(--text-subtle); font: inherit; font-size: 13px; line-height: 1.25; text-align: left; cursor: pointer; padding: 7px 9px; overflow-wrap: anywhere; } .docs-tree-page { width: 100%; min-height: 32px; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--text-subtle); font: inherit; font-size: 13px; line-height: 1.25; text-align: left; cursor: pointer; padding: 7px 9px; overflow-wrap: anywhere; }
.docs-tree-page:hover, .docs-tree-page.is-active { background: var(--sidebar-hover-bg-strong); color: var(--text-strong); } .docs-tree-page:hover, .docs-tree-page.is-active { background: var(--sidebar-hover-bg-strong); color: var(--text-strong); }
.docs-tree-page.is-active { box-shadow: inset 3px 0 var(--accent); font-weight: 700; } .docs-tree-page.is-active { box-shadow: inset 3px 0 var(--accent); font-weight: 700; }
.docs-tree-empty { color: var(--muted); font-size: 12px; line-height: 1.35; padding: 8px 9px; } .docs-tree-empty { color: var(--muted); font-size: 12px; line-height: 1.35; padding: 8px 9px; }
.docs-page { min-height: 100%; background: var(--panel); } .docs-page { min-height: 100%; background: var(--panel); }
.docs-content { width: min(1180px, 100%); margin: 0 auto; display: grid; grid-template-columns: minmax(0, 1fr) 260px; align-items: start; gap: 32px; } .docs-content { width: min(1180px, 100%); margin: 0 auto; display: grid; grid-template-columns: minmax(0, 1fr) 260px; align-items: start; gap: 32px; }
.docs-page-main { min-width: 0; } .docs-page-main { min-width: 0; }
.docs-page-outline-box { position: sticky; top: 24px; border: var(--border-line); border-radius: 6px; background: var(--surface-raised); padding: 16px; box-shadow: var(--shadow-xs); } .docs-page-outline-box { position: sticky; top: 24px; border: var(--border-line); border-radius: var(--radius-compact); background: var(--surface-raised); padding: 16px; box-shadow: var(--shadow-xs); }
.docs-page-outline-box h2 { margin: 0 0 10px; color: var(--text-strong); font-size: 13px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; } .docs-page-outline-box h2 { margin: 0 0 10px; color: var(--text-strong); font-size: 13px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
.docs-page-outline-box nav { display: grid; gap: 4px; } .docs-page-outline-box nav { display: grid; gap: 4px; }
.docs-page-outline-box a { border-radius: 4px; color: var(--muted); font-size: 13px; line-height: 1.3; padding: 5px 6px; text-decoration: none; } .docs-page-outline-box a { border-radius: var(--radius-sm); color: var(--muted); font-size: 13px; line-height: 1.3; padding: 5px 6px; text-decoration: none; }
.docs-page-outline-box a:hover { background: var(--panel); color: var(--text-strong); } .docs-page-outline-box a:hover { background: var(--panel); color: var(--text-strong); }
.docs-section { scroll-margin-top: 24px; padding-bottom: 34px; border-bottom: var(--border-line); } .docs-section { scroll-margin-top: 24px; padding-bottom: 34px; border-bottom: var(--border-line); }
.docs-section:last-child { border-bottom: 0; } .docs-section:last-child { border-bottom: 0; }
+1 -1
View File
@@ -4,7 +4,7 @@
} }
.retention-policy-target-row { .retention-policy-target-row {
max-width: 520px; max-width: 560px;
} }
.retention-policy-editor { .retention-policy-editor {
+8 -8
View File
@@ -2,7 +2,7 @@
.mapping-table { .mapping-table {
margin-top: 18px; margin-top: 18px;
border: var(--border-line); border: var(--border-line);
border-radius: 6px; border-radius: var(--radius-compact);
overflow: hidden; overflow: hidden;
} }
.mapping-header, .mapping-header,
@@ -31,7 +31,7 @@
margin: 18px 0; margin: 18px 0;
background: var(--surface); background: var(--surface);
border: var(--border-line); border: var(--border-line);
border-radius: 6px; border-radius: var(--radius-compact);
padding: 18px; padding: 18px;
} }
.attachment-rule-header { .attachment-rule-header {
@@ -358,7 +358,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex: 0 0 auto; flex: 0 0 auto;
border-radius: 999px; border-radius: var(--radius-pill);
} }
.data-grid-resize-handle { .data-grid-resize-handle {
@@ -420,7 +420,7 @@
background: transparent; background: transparent;
color: var(--muted); color: var(--muted);
cursor: pointer; cursor: pointer;
border-radius: 999px; border-radius: var(--radius-pill);
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
@@ -610,7 +610,7 @@
.data-grid-list-option-remove, .data-grid-list-option-remove,
.data-grid-list-option-add button { .data-grid-list-option-add button {
border: 0; border: 0;
border-radius: 999px; border-radius: var(--radius-pill);
background: transparent; background: transparent;
color: var(--muted); color: var(--muted);
width: 28px; width: 28px;
@@ -744,7 +744,7 @@
width: 32px; width: 32px;
height: 32px; height: 32px;
border: var(--border-line); border: var(--border-line);
border-radius: var(--radius-xs, 5px); border-radius: var(--radius-xs);
background: var(--surface); background: var(--surface);
color: var(--text); color: var(--text);
display: inline-grid; display: inline-grid;
@@ -762,7 +762,7 @@
cursor: not-allowed; cursor: not-allowed;
} }
@media (max-width: 700px) { @media (max-width: 760px) {
.data-grid-pagination { .data-grid-pagination {
flex-wrap: wrap; flex-wrap: wrap;
justify-content: space-between; justify-content: space-between;
@@ -882,7 +882,7 @@
font-weight: 500; font-weight: 500;
} }
@media (max-width: 820px) { @media (max-width: 760px) {
.connection-tree { .connection-tree {
overflow-x: auto; overflow-x: auto;
} }
+51 -1
View File
@@ -48,7 +48,7 @@
--text: #48494c; --text: #48494c;
--text-strong: #303135; --text-strong: #303135;
--muted: #8a8781; --muted: #686560;
--muted-text: var(--muted); --muted-text: var(--muted);
--text-label: #6b6863; --text-label: #6b6863;
--text-subtle: #686560; --text-subtle: #686560;
@@ -132,6 +132,12 @@
--danger-text-tooltip: #7a241c; --danger-text-tooltip: #7a241c;
--on-accent: #ffffff; --on-accent: #ffffff;
--on-dark: #ffffff; --on-dark: #ffffff;
--action-primary-bg: #34796d;
--action-primary-border: #315f55;
--action-primary-text: #ffffff;
--action-danger-bg: #b13e35;
--action-danger-text: #ffffff;
--badge-accent-text: #242424;
--code-bg: #292929; --code-bg: #292929;
--code-text: #f1f1f1; --code-text: #f1f1f1;
--status-neutral-bg: #e7e4df; --status-neutral-bg: #e7e4df;
@@ -195,13 +201,35 @@
--shadow-campaign-soft: 0 1px 2px rgba(38, 35, 30, .04); --shadow-campaign-soft: 0 1px 2px rgba(38, 35, 30, .04);
--shadow-campaign-small: 0 4px 12px rgba(0,0,0,.09); --shadow-campaign-small: 0 4px 12px rgba(0,0,0,.09);
--shadow-footer: 0 -8px 24px rgba(15, 23, 42, .06); --shadow-footer: 0 -8px 24px rgba(15, 23, 42, .06);
--shadow-drawer-side: -12px 0 30px rgba(0, 0, 0, .18);
--shadow-drawer-bottom: 0 -12px 30px rgba(0, 0, 0, .18);
--radius-hairline: 2px;
--radius-tight: 3px;
--radius: 8px; --radius: 8px;
--radius-xs: 5px; --radius-xs: 5px;
--radius-sm: 4px; --radius-sm: 4px;
--radius-compact: 6px;
--radius-md: var(--radius); --radius-md: var(--radius);
--radius-lg: 12px; --radius-lg: 12px;
--radius-xl: 16px;
--radius-round: 50%;
--radius-pill: 999px; --radius-pill: 999px;
/* Theme-aware categorical colors. Modules retain the meaning and mapping. */
--data-category-blue: #3d6f9e;
--data-category-green: #2f7d6d;
--data-category-amber: #b7791f;
--data-category-purple: #76569b;
--data-category-rose: #9d4e63;
--data-series-1: #2f7d6e;
--data-series-2: #3366a8;
--data-series-3: #c28b2c;
--data-series-4: #9a4f71;
--data-series-5: #5f7f3a;
--data-series-6: #b85c3b;
--data-series-7: #586176;
--data-series-8: #2e8b9a;
--theme-preview-light-bg: #e9e7e4; --theme-preview-light-bg: #e9e7e4;
--theme-preview-light-bar: #d4d0ca; --theme-preview-light-bar: #d4d0ca;
--theme-preview-light-surface: #ffffff; --theme-preview-light-surface: #ffffff;
@@ -346,6 +374,12 @@
--info-soft: #243d4e; --info-soft: #243d4e;
--info-text: #dce9f3; --info-text: #dce9f3;
--info-text-strong: #d8e8f4; --info-text-strong: #d8e8f4;
--action-primary-bg: #63b8a7;
--action-primary-border: #79c8b8;
--action-primary-text: #171819;
--action-danger-bg: #e06a5f;
--action-danger-text: #171819;
--badge-accent-text: #171819;
--shadow: 0 1px 2px rgba(0,0,0,.35), 0 10px 30px rgba(0,0,0,.18); --shadow: 0 1px 2px rgba(0,0,0,.35), 0 10px 30px rgba(0,0,0,.18);
--shadow-menu: 0 18px 44px rgba(0,0,0,.42); --shadow-menu: 0 18px 44px rgba(0,0,0,.42);
--shadow-popover: 0 8px 22px rgba(0,0,0,.36); --shadow-popover: 0 8px 22px rgba(0,0,0,.36);
@@ -365,6 +399,22 @@
--shadow-campaign-soft: 0 1px 2px rgba(0,0,0,.24); --shadow-campaign-soft: 0 1px 2px rgba(0,0,0,.24);
--shadow-campaign-small: 0 4px 12px rgba(0,0,0,.32); --shadow-campaign-small: 0 4px 12px rgba(0,0,0,.32);
--shadow-footer: 0 -8px 24px rgba(0,0,0,.28); --shadow-footer: 0 -8px 24px rgba(0,0,0,.28);
--shadow-drawer-side: -12px 0 30px rgba(0, 0, 0, .46);
--shadow-drawer-bottom: 0 -12px 30px rgba(0, 0, 0, .46);
--data-category-blue: #79a7d3;
--data-category-green: #63b8a7;
--data-category-amber: #d6a64a;
--data-category-purple: #aa8bd0;
--data-category-rose: #cf7a91;
--data-series-1: #63b8a7;
--data-series-2: #79a7d3;
--data-series-3: #d6a64a;
--data-series-4: #cf86aa;
--data-series-5: #91b964;
--data-series-6: #d27c5c;
--data-series-7: #9aa4bc;
--data-series-8: #63b7c3;
--calendar-grid-bg: #242523; --calendar-grid-bg: #242523;
--calendar-muted-bg: #34342f; --calendar-muted-bg: #34342f;
--calendar-today-bg: #24473f; --calendar-today-bg: #24473f;
+45
View File
@@ -548,15 +548,60 @@ export type SearchRuntimeUiCapability = {
export type QuickAccessRailProps = PlatformRouteContext & { export type QuickAccessRailProps = PlatformRouteContext & {
tools: QuickAccessToolMetadata[]; tools: QuickAccessToolMetadata[];
launchContext: QuickAccessLaunchContext;
}; };
export type QuickAccessRuntimeUiCapability = { export type QuickAccessRuntimeUiCapability = {
Rail: ComponentType<QuickAccessRailProps>; Rail: ComponentType<QuickAccessRailProps>;
}; };
export type ActiveObjectReference = {
ownerModule: string;
kind: string;
objectId: string;
tenantId: string;
label: string;
version?: string | null;
path?: string | null;
};
export type QuickAccessLaunchContext = {
contractVersion: "1";
origin: {
pathname: string;
search: string;
hash: string;
historyIndex: number | null;
};
tenantId: string;
accountId: string;
activeObject: ActiveObjectReference | null;
actingContext: {
assignmentId: string | null;
actingForAccountId: string | null;
} | null;
temporalContext: {
validityMode: "current" | "at" | "all";
validAt: string | null;
recordedAt: string | null;
};
viewContext: {
viewId: string;
revisionId: string | null;
name: string | null;
} | null;
};
export type QuickAccessResult = {
action: "created" | "selected" | "updated" | "completed";
reference?: ActiveObjectReference | null;
};
export type QuickAccessToolRenderContext = PlatformRouteContext & { export type QuickAccessToolRenderContext = PlatformRouteContext & {
close: () => void; close: () => void;
active: boolean; active: boolean;
launchContext: QuickAccessLaunchContext;
complete: (result: QuickAccessResult) => void;
}; };
export type QuickAccessToolContribution = { export type QuickAccessToolContribution = {
+88
View File
@@ -0,0 +1,88 @@
import type { AuthInfo } from "../src/types";
import {
createQuickAccessLaunchContext,
quickAccessLaunchContextFromState,
quickAccessLaunchState,
quickAccessReturnPath
} from "../src/platform/launchContext";
function assert(condition: unknown, message: string): void {
if (!condition) throw new Error(message);
}
const auth = {
user: { id: "user-1", account_id: "account-1", email: "case@example.test" },
tenant: { id: "tenant-1", slug: "test", name: "Test" },
scopes: [],
roles: [],
groups: [],
principal: {
account_id: "account-1",
tenant_id: "tenant-1",
scopes: [],
group_ids: [],
auth_method: "session",
acting_assignment_id: "assignment-1"
},
profile_loaded: true,
roles_loaded: true,
groups_loaded: true
} satisfies AuthInfo;
const context = createQuickAccessLaunchContext({
pathname: "/cases/case-1",
search: "?tab=history",
hash: "#revision-4",
historyIndex: 7,
auth,
activeObject: {
ownerModule: "cases",
kind: "case",
objectId: "case-1",
tenantId: "tenant-1",
label: "V-2026-0042 · Resident parking permit",
version: "4"
},
temporalContext: {
validityMode: "at",
validAt: "2026-08-01T10:00:00.000Z",
recordedAt: "2026-08-02T10:00:00.000Z"
},
viewContext: {
activeViewId: "case-worker",
activeRevisionId: "view-revision-3",
activeViewName: "Case worker",
visibleSurfaceIds: [],
locked: false,
availableViews: [],
provenance: [],
diagnostics: []
}
});
assert(context.contractVersion === "1", "launch context must be explicitly versioned");
assert(context.activeObject?.objectId === "case-1", "active object reference must survive launch");
assert(context.actingContext?.assignmentId === "assignment-1", "acting assignment must survive launch");
assert(context.temporalContext.validityMode === "at", "temporal selection must survive launch");
assert(context.viewContext?.revisionId === "view-revision-3", "exact View revision must survive launch");
assert(
quickAccessReturnPath(context) === "/cases/case-1?tab=history#revision-4",
"return path must preserve route, query and fragment"
);
assert(
quickAccessLaunchContextFromState(quickAccessLaunchState(context))?.accountId === "account-1",
"router state must decode a valid launch context"
);
assert(
quickAccessLaunchContextFromState({ govoplanQuickAccessLaunch: { contractVersion: "2" } }) === null,
"unknown launch context versions must fail closed"
);
const crossTenant = createQuickAccessLaunchContext({
pathname: "/cases/case-1",
auth,
activeObject: { ...context.activeObject!, tenantId: "tenant-2" },
temporalContext: { validityMode: "current", validAt: null, recordedAt: null }
});
assert(crossTenant.activeObject === null, "cross-tenant object references must be discarded");
+7
View File
@@ -0,0 +1,7 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"types": ["node"]
},
"include": ["src", "conformance", "vite.conformance.config.ts"]
}
+2
View File
@@ -18,9 +18,11 @@
"tests/module-capabilities.test.ts", "tests/module-capabilities.test.ts",
"tests/privacy-policy.test.ts", "tests/privacy-policy.test.ts",
"tests/help-context.test.ts", "tests/help-context.test.ts",
"tests/launch-context.test.ts",
"tests/definition-graph.test.ts", "tests/definition-graph.test.ts",
"src/platform/moduleLogic.ts", "src/platform/moduleLogic.ts",
"src/platform/productAreas.ts", "src/platform/productAreas.ts",
"src/platform/launchContext.ts",
"src/utils/helpContext.ts", "src/utils/helpContext.ts",
"src/features/privacy/policyLogic.ts", "src/features/privacy/policyLogic.ts",
"src/definitionGraph.ts", "src/definitionGraph.ts",
+12
View File
@@ -0,0 +1,12 @@
import { resolve } from "node:path";
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
root: resolve(import.meta.dirname, "conformance"),
plugins: [react()],
build: {
outDir: resolve(import.meta.dirname, "dist-conformance"),
emptyOutDir: true
}
});