Add cross-module operational and interaction contracts
This commit is contained in:
+2
-1
@@ -26,12 +26,13 @@
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite --host 127.0.0.1 --port 5173",
|
||||
"prebuild": "npm run audit:i18n-structural",
|
||||
"prebuild": "npm run audit:i18n-structural && npm run test:theme-contract",
|
||||
"build": "tsc && vite build && node scripts/check-bundle-budget.mjs",
|
||||
"check:bundle-budget": "node scripts/check-bundle-budget.mjs",
|
||||
"preview": "vite preview --host 127.0.0.1 --port 4173",
|
||||
"audit:i18n-structural": "node scripts/audit-i18n-structural.mjs",
|
||||
"test:i18n-catalog": "node --test tests/i18n-catalog-validation.test.mjs",
|
||||
"test:theme-contract": "node scripts/test-theme-contract.mjs",
|
||||
"test:file-drop-zone": "rm -rf .file-drop-test-build && mkdir -p .file-drop-test-build && printf '{\"type\":\"commonjs\"}\\n' > .file-drop-test-build/package.json && tsc -p tsconfig.file-drop-tests.json && node .file-drop-test-build/tests/file-drop-resolver.test.js && node scripts/test-file-drop-zone-structure.mjs",
|
||||
"test:data-grid-actions": "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/data-grid-actions.test.js && node .component-test-build/tests/data-grid-sizing.test.js",
|
||||
"test:dialog-focus": "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/dialog-focus.test.js && node scripts/test-dialog-focus-structure.mjs",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const webuiRoot = resolve(import.meta.dirname, "..");
|
||||
const repositoryRoot = resolve(webuiRoot, "..", "..");
|
||||
const tokens = readFileSync(resolve(webuiRoot, "src/styles/tokens.css"), "utf8");
|
||||
const app = readFileSync(resolve(webuiRoot, "src/App.tsx"), "utf8");
|
||||
const settings = readFileSync(resolve(webuiRoot, "src/features/settings/SettingsPage.tsx"), "utf8");
|
||||
|
||||
assert.match(tokens, /:root\[data-theme="dark"\]/, "dark token overrides are required");
|
||||
assert.match(tokens, /color-scheme:\s*dark/, "native controls must receive the dark color scheme");
|
||||
assert.match(app, /matchMedia\?\.\("\(prefers-color-scheme: dark\)"\)/, "system theme must follow OS changes");
|
||||
assert.match(app, /dataset\.themePreference/, "the selected preference must remain inspectable");
|
||||
for (const theme of ["system", "light", "dark"]) {
|
||||
assert.match(settings, new RegExp(`value:\\s*"${theme}"`), `Settings must expose ${theme}`);
|
||||
}
|
||||
|
||||
const representativeModules = [
|
||||
"govoplan-campaign/webui/src/styles/campaign-workspace.css",
|
||||
"govoplan-calendar/webui/src/styles/calendar.css",
|
||||
"govoplan-files/webui/src/styles/file-manager.css",
|
||||
"govoplan-mail/webui/src/styles/mail-profiles.css",
|
||||
];
|
||||
for (const relativePath of representativeModules) {
|
||||
const css = readFileSync(resolve(repositoryRoot, relativePath), "utf8");
|
||||
assert.match(css, /var\(--/, `${relativePath} must consume shared theme tokens`);
|
||||
}
|
||||
|
||||
console.log("Theme contract passed for core and representative module surfaces.");
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useRef, useState, useEffect } from "react";
|
||||
import { Bell, Check, LogOut, Settings, UserCircle } from "lucide-react";
|
||||
import type { ApiSettings, AuthInfo, AuthTenantMembership, AuthUpdate, LoginResponse, SearchRuntimeUiCapability, ViewsRuntimeUiCapability } from "../types";
|
||||
import type { ActingContextRuntimeUiCapability, ApiSettings, AuthInfo, AuthTenantMembership, AuthUpdate, LoginResponse, SearchRuntimeUiCapability, ViewsRuntimeUiCapability } from "../types";
|
||||
import HelpMenu from "./HelpMenu";
|
||||
import LanguageMenu from "./LanguageMenu";
|
||||
import LoginModal from "../features/auth/LoginModal";
|
||||
@@ -37,6 +37,8 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
|
||||
const projection = useEffectiveView();
|
||||
const viewsRuntime = usePlatformUiCapability<ViewsRuntimeUiCapability>("views.runtime");
|
||||
const ViewSelector = viewsRuntime?.Selector ?? null;
|
||||
const actingRuntime = usePlatformUiCapability<ActingContextRuntimeUiCapability>("access.actingContext");
|
||||
const ActingContextSelector = actingRuntime?.Selector ?? null;
|
||||
const searchRuntime = usePlatformUiCapability<SearchRuntimeUiCapability>("search.runtime");
|
||||
const GlobalSearch = searchRuntime?.GlobalSearch ?? null;
|
||||
const canUseGlobalSearch = Boolean(
|
||||
@@ -60,7 +62,7 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
|
||||
);
|
||||
const showTenantControl = Boolean(activeTenant && (canSwitchTenant || canAdministerTenants));
|
||||
const showContextSelectors = Boolean(
|
||||
auth && ((activeTenant && showTenantControl) || ViewSelector)
|
||||
auth && ((activeTenant && showTenantControl) || ViewSelector || ActingContextSelector)
|
||||
);
|
||||
const notificationsAvailable = modules.some((module) => module.id === "notifications" && module.routes?.some((route) => route.path === "/notifications"));
|
||||
const notificationSummary = useSharedNotificationSummary(settings, {
|
||||
@@ -206,6 +208,9 @@ export default function Titlebar({ settings, auth, onAuthChange, maintenanceMode
|
||||
{ViewSelector &&
|
||||
<ViewSelector settings={settings} auth={auth} projection={projection} />
|
||||
}
|
||||
{ActingContextSelector &&
|
||||
<ActingContextSelector settings={settings} auth={auth} onAuthChange={onAuthChange} />
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -33,6 +33,9 @@
|
||||
.titlebar-actions { grid-column: 2; display: flex; align-items: center; justify-self: end; min-width: 0; gap: 10px; }
|
||||
.titlebar.has-global-search .titlebar-actions { grid-column: 3; }
|
||||
.titlebar-context-selectors { display: flex; align-items: center; min-width: 0; gap: 12px; }
|
||||
.acting-context-selector { display: inline-flex; align-items: center; min-width: 0; gap: 7px; color: var(--muted); }
|
||||
.acting-context-selector select { width: auto; max-width: 260px; min-height: 32px; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--text-strong); font: inherit; font-weight: 700; padding: 5px 24px 5px 8px; }
|
||||
.acting-context-selector select:hover, .acting-context-selector select:focus-visible { background: var(--titlebar-hover-bg); }
|
||||
.tenant-selector { height: 40px; min-width: 210px; border: var(--border-line); border-radius: var(--radius-sm); background: var(--surface); display: flex; align-items: center; padding: 0 12px; gap: 5px; box-shadow: var(--shadow-xs); }
|
||||
.tenant-label, .tenant-caret, .muted { color: var(--muted); }
|
||||
.block { display: block; }
|
||||
@@ -265,6 +268,8 @@
|
||||
.docs-outline { padding-top: 22px; }
|
||||
.docs-sidebar-header { padding: 0 16px 16px; }
|
||||
.docs-sidebar-header .section-title { padding: 0 6px 12px; }
|
||||
.docs-version-selector { display: grid; gap: 5px; margin-top: 12px; color: var(--muted); font-size: 12px; font-weight: 700; }
|
||||
.docs-version-selector select { width: 100%; min-height: 34px; border: var(--border-line); border-radius: var(--radius-sm); background: var(--input-bg); color: var(--text); font: inherit; padding: 5px 8px; }
|
||||
.docs-audience-toggle { width: 100%; }
|
||||
.docs-tree { display: grid; gap: 2px; padding: 0 10px 24px; }
|
||||
.docs-tree-node { min-width: 0; }
|
||||
|
||||
@@ -92,11 +92,22 @@ export type PrincipalContext = {
|
||||
api_key_id?: string | null;
|
||||
session_id?: string | null;
|
||||
service_account_id?: string | null;
|
||||
acting_assignment_id?: string | null;
|
||||
acting_for_account_id?: string | null;
|
||||
email?: string | null;
|
||||
display_name?: string | null;
|
||||
};
|
||||
|
||||
export type ActingContextSelectorProps = {
|
||||
settings: ApiSettings;
|
||||
auth: AuthInfo;
|
||||
onAuthChange: (auth: AuthUpdate | null, accessToken?: string) => void;
|
||||
};
|
||||
|
||||
export type ActingContextRuntimeUiCapability = {
|
||||
Selector: ComponentType<ActingContextSelectorProps>;
|
||||
};
|
||||
|
||||
export type AuthInfo = {
|
||||
user: AuthUser;
|
||||
// Backwards-compatible active tenant alias returned by older/newer APIs.
|
||||
|
||||
Reference in New Issue
Block a user