feat: add UI conformance and launch context
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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 });
|
||||
});
|
||||
Generated
+95
-2
@@ -55,10 +55,13 @@
|
||||
"@tiptap/starter-kit": "^3.29.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"@xyflow/react": "^12.11.2",
|
||||
"axe-core": "^4.13.0",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
@@ -576,7 +579,7 @@
|
||||
},
|
||||
"../../govoplan-records/webui": {
|
||||
"name": "@govoplan/records-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
@@ -659,7 +662,7 @@
|
||||
},
|
||||
"../../govoplan-tasks/webui": {
|
||||
"name": "@govoplan/tasks-webui",
|
||||
"version": "0.1.18",
|
||||
"version": "0.1.19",
|
||||
"peerDependencies": {
|
||||
"@govoplan/core-webui": "^0.1.18",
|
||||
"lucide-react": "^1.23.0",
|
||||
@@ -1716,6 +1719,22 @@
|
||||
"@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": {
|
||||
"version": "1.0.0-rc.3",
|
||||
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz",
|
||||
@@ -2662,6 +2681,16 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "19.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz",
|
||||
@@ -2761,6 +2790,16 @@
|
||||
"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": {
|
||||
"version": "2.10.38",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "8.5.23",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
|
||||
@@ -3585,6 +3671,13 @@
|
||||
"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": {
|
||||
"version": "0.13.2",
|
||||
"resolved": "https://registry.npmjs.org/unzipper-esm/-/unzipper-esm-0.13.2.tgz",
|
||||
|
||||
+14
-8
@@ -26,8 +26,11 @@
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"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",
|
||||
"preview": "vite preview --host 127.0.0.1 --port 4173",
|
||||
"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: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: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: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",
|
||||
@@ -56,13 +59,13 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@govoplan/access-webui": "file:../../govoplan-access/webui",
|
||||
"@govoplan/approvals-webui": "file:../../govoplan-approvals/webui",
|
||||
"@govoplan/addresses-webui": "file:../../govoplan-addresses/webui",
|
||||
"@govoplan/admin-webui": "file:../../govoplan-admin/webui",
|
||||
"@govoplan/approvals-webui": "file:../../govoplan-approvals/webui",
|
||||
"@govoplan/audit-webui": "file:../../govoplan-audit/webui",
|
||||
"@govoplan/calendar-webui": "file:../../govoplan-calendar/webui",
|
||||
"@govoplan/cases-webui": "file:../../govoplan-cases/webui",
|
||||
"@govoplan/campaign-webui": "file:../../govoplan-campaign/webui",
|
||||
"@govoplan/cases-webui": "file:../../govoplan-cases/webui",
|
||||
"@govoplan/committee-webui": "file:../../govoplan-committee/webui",
|
||||
"@govoplan/dashboard-webui": "file:../../govoplan-dashboard/webui",
|
||||
"@govoplan/dataflow-webui": "file:../../govoplan-dataflow/webui",
|
||||
@@ -71,10 +74,10 @@
|
||||
"@govoplan/docs-webui": "file:../../govoplan-docs/webui",
|
||||
"@govoplan/encryption-webui": "file:../../govoplan-encryption/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/idm-webui": "file:../../govoplan-idm/webui",
|
||||
"@govoplan/forms-webui": "file:../../govoplan-forms/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/notifications-webui": "file:../../govoplan-notifications/webui",
|
||||
"@govoplan/ops-webui": "file:../../govoplan-ops/webui",
|
||||
@@ -84,14 +87,14 @@
|
||||
"@govoplan/postbox-webui": "file:../../govoplan-postbox/webui",
|
||||
"@govoplan/projects-webui": "file:../../govoplan-projects/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/reporting-webui": "file:../../govoplan-reporting/webui",
|
||||
"@govoplan/risk-compliance-webui": "file:../../govoplan-risk-compliance/webui",
|
||||
"@govoplan/scheduling-webui": "file:../../govoplan-scheduling/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/templates-webui": "file:../../govoplan-templates/webui",
|
||||
"@govoplan/tenancy-webui": "file:../../govoplan-tenancy/webui",
|
||||
"@govoplan/views-webui": "file:../../govoplan-views/webui",
|
||||
"@govoplan/voting-webui": "file:../../govoplan-voting/webui",
|
||||
"@govoplan/workflow-webui": "file:../../govoplan-workflow/webui",
|
||||
@@ -102,10 +105,13 @@
|
||||
"@tiptap/starter-kit": "^3.29.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.62.1",
|
||||
"@types/node": "^26.2.0",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^5.2.0",
|
||||
"@xyflow/react": "^12.11.2",
|
||||
"axe-core": "^4.13.0",
|
||||
"lucide-react": "^1.23.0",
|
||||
"react": "^19.2.8",
|
||||
"react-dom": "^19.2.8",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
@@ -12,6 +12,7 @@ import { firstAccessibleRoute, loadInstalledPublicWebModules, loadInstalledWebMo
|
||||
import { PlatformModulesProvider } from "./platform/ModuleContext";
|
||||
import { PlatformViewProvider } from "./platform/ViewContext";
|
||||
import { PlatformTemporalProvider } from "./platform/TemporalContext";
|
||||
import { PlatformActiveObjectProvider } from "./platform/ActiveObjectContext";
|
||||
import { PLATFORM_TEMPORAL_CONTEXT_CHANGED_EVENT } from "./platform/temporal";
|
||||
import {
|
||||
PLATFORM_VIEW_CHANGED_EVENT,
|
||||
@@ -538,6 +539,7 @@ export default function App() {
|
||||
<PlatformTemporalProvider scopeKey={`${auth.user?.id ?? "account"}:${(auth.active_tenant ?? auth.tenant).id}`}>
|
||||
<DocumentationHelpProvider localDocsAvailable={localDocsAvailable}>
|
||||
<PlatformViewProvider modules={webModules} projection={viewProjection}>
|
||||
<PlatformActiveObjectProvider>
|
||||
<UnsavedChangesProvider>
|
||||
<AppShell settings={settings} auth={auth} onSettingsChange={updateSettings} onAuthChange={updateAuth} navItems={navItems} maintenanceMode={maintenanceMode} backendReachable={backendReachable}>
|
||||
<ModuleLoadBoundary resetKey={`${location.pathname}:${temporalRevision}`} loading={webModulesLoading}>
|
||||
@@ -583,6 +585,7 @@ export default function App() {
|
||||
}
|
||||
</AppShell>
|
||||
</UnsavedChangesProvider>
|
||||
</PlatformActiveObjectProvider>
|
||||
</PlatformViewProvider>
|
||||
</DocumentationHelpProvider>
|
||||
</PlatformTemporalProvider>
|
||||
|
||||
@@ -82,7 +82,7 @@ export default function PageLayout({
|
||||
notices,
|
||||
mode = "standalone",
|
||||
scrollable,
|
||||
stickyHeader = true,
|
||||
stickyHeader,
|
||||
showHeader = true,
|
||||
documentationType = "user",
|
||||
className = "",
|
||||
@@ -96,6 +96,7 @@ export default function PageLayout({
|
||||
}: PageLayoutProps) {
|
||||
const shouldScroll = scrollable ?? mode === "standalone";
|
||||
const shouldInset = mode !== "embedded";
|
||||
const shouldStickHeader = stickyHeader ?? mode !== "embedded";
|
||||
const layoutClasses = [
|
||||
"page-layout",
|
||||
`page-layout-${mode}`,
|
||||
@@ -120,7 +121,7 @@ export default function PageLayout({
|
||||
description={description}
|
||||
actions={actions}
|
||||
loading={headerLoading ?? loading}
|
||||
sticky={stickyHeader}
|
||||
sticky={shouldStickHeader}
|
||||
className={headerClassName}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -171,8 +171,6 @@ export function UnsavedChangesProvider({ children }: {children: ReactNode;}) {
|
||||
open
|
||||
role="alertdialog"
|
||||
title={registration.title ?? "i18n:govoplan-core.unsaved_changes.29267269"}
|
||||
className="unsaved-changes-dialog"
|
||||
footerClassName="unsaved-changes-actions"
|
||||
closeOnBackdrop={!saving}
|
||||
closeDisabled={saving}
|
||||
onClose={() => setPendingAction(null)}
|
||||
|
||||
@@ -78,13 +78,21 @@ export const generatedTranslations: PlatformTranslations = {
|
||||
"i18n:govoplan-core.application_notices": "Application notices",
|
||||
"i18n:govoplan-core.more_tools": "More tools",
|
||||
"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.records_documents": "Records and documents",
|
||||
"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.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.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.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.attachment_base_path.66e9940b": "Attachment base path",
|
||||
"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.more_tools": "Weitere Werkzeuge",
|
||||
"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.records_documents": "Akten und Dokumente",
|
||||
"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.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.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.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.attachment_base_path.66e9940b": "Attachment base path",
|
||||
"i18n:govoplan-core.attachment_value.01801a54": "Attachment {value0}",
|
||||
|
||||
@@ -34,6 +34,8 @@ export * from "./platform/ViewContext";
|
||||
export * from "./platform/views";
|
||||
export * from "./platform/temporal";
|
||||
export * from "./platform/TemporalContext";
|
||||
export * from "./platform/ActiveObjectContext";
|
||||
export * from "./platform/launchContext";
|
||||
export * from "./platform/wizards";
|
||||
|
||||
export * from "./utils/permissions";
|
||||
|
||||
@@ -6,6 +6,9 @@ import Titlebar from "./Titlebar";
|
||||
import BreadcrumbBar from "./BreadcrumbBar";
|
||||
import { usePlatformModules, usePlatformUiCapability } from "../platform/ModuleContext";
|
||||
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 { hasAnyScope, hasScope } from "../utils/permissions";
|
||||
|
||||
@@ -37,6 +40,21 @@ export default function AppShell({
|
||||
const quickAccess = usePlatformUiCapability<QuickAccessRuntimeUiCapability>("quickAccess.runtime");
|
||||
const projection = useEffectiveView();
|
||||
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(
|
||||
() => modules.flatMap((module) => module.quickAccessTools ?? []).filter((tool) => {
|
||||
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">
|
||||
<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>
|
||||
</div>
|
||||
{QuickAccessRail && auth && (
|
||||
<QuickAccessRail settings={settings} auth={auth} tools={quickAccessTools} />
|
||||
{QuickAccessRail && auth && launchContext && (
|
||||
<QuickAccessRail settings={settings} auth={auth} tools={quickAccessTools} launchContext={launchContext} />
|
||||
)}
|
||||
</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;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,37 @@
|
||||
import { Link } from "react-router";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { usePlatformLanguage } from "../i18n/LanguageContext";
|
||||
import { ArrowLeft, ChevronRight } from "lucide-react";
|
||||
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 labels = parts.length ? parts : ["campaigns"];
|
||||
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 (
|
||||
<div className="breadcrumb-bar">
|
||||
@@ -20,10 +46,29 @@ export default function BreadcrumbBar({ pathname }: {pathname: string;}) {
|
||||
|
||||
})}
|
||||
</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>);
|
||||
|
||||
}
|
||||
|
||||
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> = {
|
||||
data: "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]);
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -49,7 +49,7 @@
|
||||
|
||||
.public-card h1 {
|
||||
margin: 0;
|
||||
max-width: 620px;
|
||||
max-width: 680px;
|
||||
font-size: clamp(30px, 4vw, 46px);
|
||||
line-height: 1.08;
|
||||
color: var(--text-strong);
|
||||
@@ -115,7 +115,7 @@
|
||||
font-weight: 700;
|
||||
padding: 8px 10px;
|
||||
margin: -8px -10px;
|
||||
border-radius: 7px;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
transition: background-color .12s ease, color .12s ease, box-shadow .12s ease;
|
||||
}
|
||||
@@ -125,7 +125,7 @@
|
||||
.account-pill {
|
||||
padding: 8px 10px;
|
||||
margin: 0;
|
||||
border-radius: 7px;
|
||||
border-radius: var(--radius-md);
|
||||
transition: background-color .12s ease, color .12s ease, box-shadow .12s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -145,7 +145,7 @@
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
transition: background-color .12s ease, color .12s ease;
|
||||
}
|
||||
|
||||
|
||||
@@ -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-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); }
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
width: 18px;
|
||||
height: 13px;
|
||||
border: 2px solid var(--loading-line);
|
||||
border-radius: 3px;
|
||||
border-radius: var(--radius-tight);
|
||||
background: var(--loading-envelope-bg);
|
||||
animation: loading-envelope-float .74s ease-in-out infinite alternate;
|
||||
}
|
||||
@@ -65,7 +65,7 @@
|
||||
gap: 7px;
|
||||
max-width: 100%;
|
||||
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));
|
||||
box-shadow: var(--shadow-control-strong);
|
||||
padding: 5px 8px 5px 11px;
|
||||
@@ -98,7 +98,7 @@
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--hover-tint);
|
||||
color: var(--control-text-muted);
|
||||
cursor: pointer;
|
||||
@@ -221,7 +221,7 @@
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
@@ -264,7 +264,7 @@
|
||||
color: var(--muted);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
@media (max-width: 560px) {
|
||||
.password-generator-character-sets {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -326,7 +326,7 @@
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border: var(--border-line-dark);
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
box-shadow: var(--shadow-inset-highlight);
|
||||
}
|
||||
|
||||
@@ -341,7 +341,7 @@
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 0;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
@@ -372,7 +372,7 @@
|
||||
min-width: 220px;
|
||||
padding: 10px;
|
||||
border: var(--border-line-dark);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow-menu);
|
||||
}
|
||||
@@ -387,7 +387,7 @@
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 1px solid var(--black-border-soft);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
cursor: pointer;
|
||||
box-shadow: var(--shadow-inset-highlight);
|
||||
}
|
||||
@@ -420,7 +420,7 @@
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: var(--border-line);
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
@@ -448,7 +448,7 @@
|
||||
min-width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 5px;
|
||||
border-radius: var(--radius-xs);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
@@ -491,7 +491,7 @@
|
||||
|
||||
.admin-overview-link {
|
||||
border: var(--border-line);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
background: var(--panel-soft);
|
||||
padding: 14px;
|
||||
text-align: left;
|
||||
@@ -528,7 +528,7 @@
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
border: var(--border-line);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 14px 16px;
|
||||
@@ -612,7 +612,7 @@
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
border: var(--border-line);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 14px 16px;
|
||||
@@ -713,7 +713,7 @@
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
border: var(--border-line);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
background: var(--panel-soft);
|
||||
padding: 12px 14px;
|
||||
}
|
||||
@@ -752,7 +752,7 @@
|
||||
.module-install-preflight-issue {
|
||||
min-height: 30px;
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--panel-glass);
|
||||
}
|
||||
|
||||
@@ -783,7 +783,7 @@
|
||||
align-content: start;
|
||||
min-height: 86px;
|
||||
border: var(--border-line);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
background: var(--panel-glass);
|
||||
padding: 9px 10px;
|
||||
}
|
||||
@@ -795,7 +795,7 @@
|
||||
|
||||
.module-install-checklist-item span {
|
||||
justify-self: start;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--panel);
|
||||
color: var(--muted);
|
||||
font-size: 11px;
|
||||
@@ -840,7 +840,7 @@
|
||||
gap: 12px;
|
||||
align-items: end;
|
||||
border: var(--border-line);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 14px 16px;
|
||||
@@ -915,7 +915,7 @@
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
border: var(--border-line);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
background: var(--panel);
|
||||
box-shadow: var(--shadow);
|
||||
padding: 14px 16px;
|
||||
@@ -1063,7 +1063,7 @@
|
||||
align-items: start;
|
||||
gap: 9px;
|
||||
padding: 8px 9px;
|
||||
border-radius: 7px;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@@ -1180,7 +1180,7 @@
|
||||
min-width: 0;
|
||||
padding: 15px;
|
||||
border: var(--border-line);
|
||||
border-radius: 7px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
@@ -1205,7 +1205,7 @@
|
||||
|
||||
.advanced-options-panel {
|
||||
border: var(--border-line);
|
||||
border-radius: 7px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
@@ -1255,7 +1255,7 @@
|
||||
gap: 10px;
|
||||
padding: 12px 13px;
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -1330,7 +1330,7 @@
|
||||
.guided-review-list.is-empty {
|
||||
padding: 12px;
|
||||
border: 1px dashed var(--line);
|
||||
border-radius: 7px;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@@ -1344,7 +1344,7 @@
|
||||
border: var(--border-line);
|
||||
border-left-width: 4px;
|
||||
border-left-color: var(--line-dark);
|
||||
border-radius: 7px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--panel);
|
||||
}
|
||||
|
||||
@@ -1390,7 +1390,7 @@
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
@media (max-width: 840px) {
|
||||
@media (max-width: 900px) {
|
||||
.guided-config-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1421,7 +1421,7 @@
|
||||
gap: 7px;
|
||||
min-height: 44px;
|
||||
border: var(--border-line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-control-surface);
|
||||
padding: 7px 8px;
|
||||
@@ -1473,7 +1473,7 @@
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
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));
|
||||
color: var(--control-text);
|
||||
cursor: pointer;
|
||||
@@ -1495,7 +1495,7 @@
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
border: var(--border-line-dark);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-popover);
|
||||
padding: 14px;
|
||||
@@ -1520,7 +1520,7 @@
|
||||
gap: 4px;
|
||||
max-width: 560px;
|
||||
border: var(--border-line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
box-shadow: 0 8px 24px var(--hover-tint);
|
||||
padding: 6px;
|
||||
@@ -1531,7 +1531,7 @@
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
background: transparent;
|
||||
color: var(--text-strong);
|
||||
cursor: pointer;
|
||||
@@ -1744,7 +1744,7 @@
|
||||
max-height: 360px;
|
||||
overflow: auto;
|
||||
border: var(--border-line-dark);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-popover);
|
||||
padding: 5px;
|
||||
@@ -1773,7 +1773,7 @@
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
@@ -1831,7 +1831,7 @@
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
border: var(--border-line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface-subtle);
|
||||
padding: 12px;
|
||||
}
|
||||
@@ -1896,7 +1896,7 @@
|
||||
width: 42px;
|
||||
height: 24px;
|
||||
border: 1px solid var(--toggle-track-border);
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--toggle-track-bg);
|
||||
box-shadow: var(--shadow-control-inset-strong);
|
||||
transition: background-color .16s ease, border-color .16s ease, box-shadow .16s ease;
|
||||
@@ -1907,7 +1907,7 @@
|
||||
left: 2px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-thumb);
|
||||
transition: transform .16s ease;
|
||||
@@ -1988,7 +1988,7 @@
|
||||
justify-content: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
color: var(--text-subtle);
|
||||
background: var(--line-dark);
|
||||
font-size: 11px;
|
||||
@@ -2005,7 +2005,7 @@
|
||||
flex: 0 0 auto;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 3px;
|
||||
border-radius: var(--radius-tight);
|
||||
color: var(--text-subtle);
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -2119,7 +2119,7 @@
|
||||
max-width: min(320px, calc(100vw - 48px));
|
||||
transform: translate(-50%, 3px);
|
||||
border: var(--border-line-dark);
|
||||
border-radius: 7px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-popover);
|
||||
color: var(--text);
|
||||
@@ -2220,7 +2220,7 @@
|
||||
width: min(280px, 72vw);
|
||||
padding: 9px 11px;
|
||||
border: 1px solid var(--border-danger-soft);
|
||||
border-radius: var(--radius-sm, 8px);
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--danger-text-tooltip);
|
||||
background: var(--danger-muted-bg);
|
||||
box-shadow: var(--shadow-popover);
|
||||
@@ -2253,7 +2253,7 @@
|
||||
place-items: center;
|
||||
min-height: 120px;
|
||||
padding: 1.25rem;
|
||||
border-radius: var(--radius-lg, 18px);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--transparent-surface);
|
||||
backdrop-filter: blur(1.5px);
|
||||
margin: -10px;
|
||||
@@ -2265,7 +2265,7 @@
|
||||
gap: 0.65rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
color: var(--text);
|
||||
background: var(--panel-glass-strong);
|
||||
box-shadow: var(--shadow-soft);
|
||||
@@ -2284,7 +2284,7 @@
|
||||
|
||||
.module-load-error .alert {
|
||||
width: min(640px, 100%);
|
||||
max-width: 640px;
|
||||
max-width: 680px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@@ -2314,7 +2314,7 @@
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
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));
|
||||
color: var(--control-text);
|
||||
cursor: pointer;
|
||||
@@ -2408,7 +2408,7 @@
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: 12px;
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-floating);
|
||||
pointer-events: auto;
|
||||
}
|
||||
@@ -2421,7 +2421,7 @@
|
||||
cursor: pointer;
|
||||
line-height: 1;
|
||||
padding: 2px;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
@@ -2500,7 +2500,7 @@
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
padding: 4px 6px;
|
||||
border-radius: 7px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.file-breadcrumb:hover:not(:disabled),
|
||||
@@ -2651,7 +2651,7 @@
|
||||
grid-template-columns: 26px minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
border-radius: 9px;
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.explorer-tree-node-wrap.has-actions {
|
||||
@@ -2671,7 +2671,7 @@
|
||||
place-items: center;
|
||||
width: 26px;
|
||||
height: 32px;
|
||||
border-radius: 9px;
|
||||
border-radius: var(--radius-md);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
@@ -2692,7 +2692,7 @@
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
padding: 8px 9px;
|
||||
border-radius: 9px;
|
||||
border-radius: var(--radius-md);
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
user-select: none;
|
||||
@@ -2844,7 +2844,7 @@
|
||||
.file-tree-select {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
color: var(--text-strong);
|
||||
font-weight: 900;
|
||||
line-height: 1;
|
||||
@@ -2890,7 +2890,7 @@
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
border: var(--border-line-dark);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--line-dark);
|
||||
box-shadow: var(--shadow-control);
|
||||
}
|
||||
@@ -2931,11 +2931,11 @@
|
||||
}
|
||||
|
||||
.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 {
|
||||
border-radius: 0 7px 7px 0;
|
||||
border-radius: 0 var(--radius-md) var(--radius-md) 0;
|
||||
}
|
||||
|
||||
.segmented-control-option:hover,
|
||||
@@ -3052,7 +3052,7 @@
|
||||
min-width: 21px;
|
||||
height: 21px;
|
||||
padding: 0 6px;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
@@ -3060,7 +3060,7 @@
|
||||
}
|
||||
|
||||
.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-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:hover .react-flow__edge-path { stroke: var(--accent); stroke-width: 3; }
|
||||
.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:hover,
|
||||
.definition-node-handle.connectingto,
|
||||
@@ -3202,7 +3202,7 @@
|
||||
.theme-preview-header i {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 50%;
|
||||
border-radius: var(--radius-round);
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 0 3px var(--accent-soft);
|
||||
}
|
||||
@@ -3223,7 +3223,7 @@
|
||||
.theme-preview-body span {
|
||||
display: block;
|
||||
height: 8px;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--preview-line);
|
||||
}
|
||||
|
||||
@@ -3517,7 +3517,7 @@
|
||||
}
|
||||
.mail-server-toggle-row {
|
||||
border: var(--border-line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--panel-soft);
|
||||
}
|
||||
.mail-server-plain-toggle-row {
|
||||
@@ -3547,40 +3547,32 @@
|
||||
align-items: center;
|
||||
min-height: 24px;
|
||||
border: var(--border-line);
|
||||
border-radius: 99px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: var(--panel-soft);
|
||||
padding: 3px 9px;
|
||||
color: var(--text-strong);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
@media (max-width: 1000px) {
|
||||
@media (max-width: 1100px) {
|
||||
.mail-server-settings-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
@media (max-width: 900px) {
|
||||
.mail-server-segmented-control { width: min(320px, 100%); }
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
@media (max-width: 760px) {
|
||||
.credential-panel-grid { grid-template-columns: 1fr; }
|
||||
.mail-server-section-heading.split { align-items: flex-start; flex-direction: column; }
|
||||
.mail-server-segmented-control { width: 100%; }
|
||||
}
|
||||
|
||||
.unsaved-changes-dialog {
|
||||
max-width: 520px;
|
||||
}
|
||||
|
||||
.unsaved-changes-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.file-drop-zone {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
gap: 8px;
|
||||
min-height: 170px;
|
||||
border: 1px dashed var(--line-dark);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--panel-soft);
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
@@ -3614,7 +3606,7 @@
|
||||
place-items: center;
|
||||
width: 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);
|
||||
color: var(--text-strong);
|
||||
font-size: 12px;
|
||||
@@ -3625,7 +3617,7 @@
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 6px;
|
||||
border-radius: 50%;
|
||||
border-radius: var(--radius-round);
|
||||
background: var(--panel-soft);
|
||||
box-shadow: var(--primary-inset-ring);
|
||||
}
|
||||
@@ -3650,7 +3642,7 @@
|
||||
.placeholder-stack span {
|
||||
display: block;
|
||||
border: 1px dashed var(--line-dark);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
background: var(--panel-soft);
|
||||
padding: 10px 12px;
|
||||
color: var(--muted);
|
||||
@@ -3661,7 +3653,7 @@
|
||||
gap: 12px;
|
||||
}
|
||||
.settings-target-row {
|
||||
max-width: 520px;
|
||||
max-width: 560px;
|
||||
}
|
||||
.admin-secret {
|
||||
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); }
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
@media (max-width: 1100px) {
|
||||
.description-list-collapse-standard { grid-template-columns: 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-inline.description-list-collapse-narrow > div { grid-template-columns: minmax(0, 1fr); }
|
||||
}
|
||||
@@ -3962,7 +3954,7 @@ button.stage-rail-item:focus-visible {
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
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));
|
||||
color: color-mix(in srgb, var(--stage-rail-color) 82%, var(--text));
|
||||
transition: background .16s ease, box-shadow .16s ease;
|
||||
@@ -4015,7 +4007,7 @@ button.stage-rail-item:focus-visible {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border: var(--border-line);
|
||||
border-radius: 5px;
|
||||
border-radius: var(--radius-xs);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-control-inset);
|
||||
transition: border-color .16s ease, box-shadow .16s ease;
|
||||
@@ -4160,7 +4152,7 @@ button.stage-rail-item:focus-visible {
|
||||
.wysiwyg-prosemirror pre {
|
||||
overflow: auto;
|
||||
padding: 10px 12px;
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--code-bg);
|
||||
color: var(--code-text);
|
||||
}
|
||||
@@ -4189,7 +4181,7 @@ button.stage-rail-item:focus-visible {
|
||||
margin: 0 2px;
|
||||
padding: 1px 6px;
|
||||
border: 1px solid var(--primary-border);
|
||||
border-radius: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--primary-soft);
|
||||
color: var(--info-text-deep);
|
||||
font-family: var(--font-mono, monospace);
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: var(--border-line);
|
||||
border-radius: 8px;
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--surface);
|
||||
box-shadow: var(--shadow-modal);
|
||||
}
|
||||
@@ -80,7 +80,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius-lg, 18px);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
box-shadow: var(--shadow-strong);
|
||||
@@ -117,7 +117,7 @@
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
.form-field { display: grid; gap: 7px; }
|
||||
.form-label { font-weight: 700; font-size: 13px; color: var(--text-label); }
|
||||
.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="radio"] {
|
||||
accent-color: var(--primary);
|
||||
@@ -49,7 +49,7 @@ textarea { resize: vertical; }
|
||||
.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 > .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; }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
@media (max-width: 760px) {
|
||||
.content-grid-collapse-narrow { grid-template-columns: minmax(0, 1fr); }
|
||||
.content-grid-collapse-narrow > .content-grid-item-two { grid-column: auto; }
|
||||
.form-section-header { flex-direction: column; }
|
||||
@@ -93,8 +93,8 @@ textarea[readonly]::placeholder {
|
||||
color: var(--control-disabled-placeholder);
|
||||
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-primary { background: var(--green); border-color: var(--success-border); color: var(--on-accent); }
|
||||
.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(--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-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; }
|
||||
|
||||
+28
-23
@@ -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.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; }
|
||||
.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;
|
||||
top: 9px;
|
||||
left: 9px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
border-radius: 50%;
|
||||
border-radius: var(--radius-round);
|
||||
content: "";
|
||||
background-color: var(--rail-bg);
|
||||
}
|
||||
@@ -46,7 +46,7 @@
|
||||
.small-note { font-size: 12px; }
|
||||
.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-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.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); }
|
||||
@@ -54,7 +54,7 @@
|
||||
.titlebar-notification-badge { position: absolute; top: 4px; right: 3px; border: 2px solid var(--titlebar-bg); transform: translate(35%, -35%); }
|
||||
.account-pill { color: var(--text); }
|
||||
.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: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); }
|
||||
@@ -79,6 +79,9 @@
|
||||
.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; }
|
||||
.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; }
|
||||
.action-toolbar { min-width: 0; display: flex; align-items: center; gap: 10px; }
|
||||
.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); }
|
||||
.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-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 small { display: block; margin-top: 3px; color: var(--muted); }
|
||||
.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:hover { background: var(--panel-soft); }
|
||||
.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) {
|
||||
.api-mini { display: none; }
|
||||
.workspace { grid-template-columns: 1fr; }
|
||||
.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); }
|
||||
.section-sidebar { display: none; }
|
||||
.wizard-card { grid-template-columns: 1fr; }
|
||||
@@ -338,15 +343,15 @@
|
||||
.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); }
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
@media (max-width: 760px) {
|
||||
.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 > .action-toolbar-group { flex: 1 1 100%; }
|
||||
.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 */
|
||||
.tenant-selector.disabled { opacity: .86; cursor: default; }
|
||||
.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-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 small { margin-left: auto; color: var(--muted); }
|
||||
.account-menu { min-width: 260px; }
|
||||
.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 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 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; }
|
||||
.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); }
|
||||
.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: 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; }
|
||||
|
||||
/* 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); }
|
||||
.compact-actions { margin: 0; align-items: center; }
|
||||
.below-grid { margin-top: 18px; }
|
||||
@@ -387,12 +392,12 @@
|
||||
.next-action-warning { border-left-color: var(--amber); }
|
||||
.next-action-info { border-left-color: var(--blue); }
|
||||
.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 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 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-size-inline { min-height: 0; padding: 8px 10px; }
|
||||
.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-title { margin: 0; color: var(--text-strong); font-size: 18px; letter-spacing: 0; }
|
||||
.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-content > :first-child { margin-top: 0; }
|
||||
.state-panel-description > :last-child,
|
||||
@@ -444,21 +449,21 @@
|
||||
.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-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-placeholder { opacity: 0; }
|
||||
.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.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-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-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 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-section { scroll-margin-top: 24px; padding-bottom: 34px; border-bottom: var(--border-line); }
|
||||
.docs-section:last-child { border-bottom: 0; }
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
}
|
||||
|
||||
.retention-policy-target-row {
|
||||
max-width: 520px;
|
||||
max-width: 560px;
|
||||
}
|
||||
|
||||
.retention-policy-editor {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
.mapping-table {
|
||||
margin-top: 18px;
|
||||
border: var(--border-line);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
overflow: hidden;
|
||||
}
|
||||
.mapping-header,
|
||||
@@ -31,7 +31,7 @@
|
||||
margin: 18px 0;
|
||||
background: var(--surface);
|
||||
border: var(--border-line);
|
||||
border-radius: 6px;
|
||||
border-radius: var(--radius-compact);
|
||||
padding: 18px;
|
||||
}
|
||||
.attachment-rule-header {
|
||||
@@ -358,7 +358,7 @@
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
}
|
||||
|
||||
.data-grid-resize-handle {
|
||||
@@ -420,7 +420,7 @@
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
cursor: pointer;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -610,7 +610,7 @@
|
||||
.data-grid-list-option-remove,
|
||||
.data-grid-list-option-add button {
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
border-radius: var(--radius-pill);
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
width: 28px;
|
||||
@@ -744,7 +744,7 @@
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: var(--border-line);
|
||||
border-radius: var(--radius-xs, 5px);
|
||||
border-radius: var(--radius-xs);
|
||||
background: var(--surface);
|
||||
color: var(--text);
|
||||
display: inline-grid;
|
||||
@@ -762,7 +762,7 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
@media (max-width: 760px) {
|
||||
.data-grid-pagination {
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
@@ -882,7 +882,7 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@media (max-width: 820px) {
|
||||
@media (max-width: 760px) {
|
||||
.connection-tree {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
|
||||
--text: #48494c;
|
||||
--text-strong: #303135;
|
||||
--muted: #8a8781;
|
||||
--muted: #686560;
|
||||
--muted-text: var(--muted);
|
||||
--text-label: #6b6863;
|
||||
--text-subtle: #686560;
|
||||
@@ -132,6 +132,12 @@
|
||||
--danger-text-tooltip: #7a241c;
|
||||
--on-accent: #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-text: #f1f1f1;
|
||||
--status-neutral-bg: #e7e4df;
|
||||
@@ -195,13 +201,35 @@
|
||||
--shadow-campaign-soft: 0 1px 2px rgba(38, 35, 30, .04);
|
||||
--shadow-campaign-small: 0 4px 12px rgba(0,0,0,.09);
|
||||
--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-xs: 5px;
|
||||
--radius-sm: 4px;
|
||||
--radius-compact: 6px;
|
||||
--radius-md: var(--radius);
|
||||
--radius-lg: 12px;
|
||||
--radius-xl: 16px;
|
||||
--radius-round: 50%;
|
||||
--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-bar: #d4d0ca;
|
||||
--theme-preview-light-surface: #ffffff;
|
||||
@@ -346,6 +374,12 @@
|
||||
--info-soft: #243d4e;
|
||||
--info-text: #dce9f3;
|
||||
--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-menu: 0 18px 44px rgba(0,0,0,.42);
|
||||
--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-small: 0 4px 12px rgba(0,0,0,.32);
|
||||
--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-muted-bg: #34342f;
|
||||
--calendar-today-bg: #24473f;
|
||||
|
||||
@@ -548,15 +548,60 @@ export type SearchRuntimeUiCapability = {
|
||||
|
||||
export type QuickAccessRailProps = PlatformRouteContext & {
|
||||
tools: QuickAccessToolMetadata[];
|
||||
launchContext: QuickAccessLaunchContext;
|
||||
};
|
||||
|
||||
export type QuickAccessRuntimeUiCapability = {
|
||||
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 & {
|
||||
close: () => void;
|
||||
active: boolean;
|
||||
launchContext: QuickAccessLaunchContext;
|
||||
complete: (result: QuickAccessResult) => void;
|
||||
};
|
||||
|
||||
export type QuickAccessToolContribution = {
|
||||
|
||||
@@ -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");
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src", "conformance", "vite.conformance.config.ts"]
|
||||
}
|
||||
@@ -18,9 +18,11 @@
|
||||
"tests/module-capabilities.test.ts",
|
||||
"tests/privacy-policy.test.ts",
|
||||
"tests/help-context.test.ts",
|
||||
"tests/launch-context.test.ts",
|
||||
"tests/definition-graph.test.ts",
|
||||
"src/platform/moduleLogic.ts",
|
||||
"src/platform/productAreas.ts",
|
||||
"src/platform/launchContext.ts",
|
||||
"src/utils/helpContext.ts",
|
||||
"src/features/privacy/policyLogic.ts",
|
||||
"src/definitionGraph.ts",
|
||||
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user