diff --git a/docs/LOCALIZATION_AND_HELP_QUALITY.md b/docs/LOCALIZATION_AND_HELP_QUALITY.md index 13d4782..c3c3eca 100644 --- a/docs/LOCALIZATION_AND_HELP_QUALITY.md +++ b/docs/LOCALIZATION_AND_HELP_QUALITY.md @@ -103,6 +103,14 @@ modal at narrow widths, closes with Escape, and restores focus to the triggering control. Module journeys should add their own exact high-risk mappings; they do not need to reimplement the keyboard or dialog mechanics. +The same conformance suite mounts the production Forms Runtime self-service and +assisted Anwohnerparkausweis surfaces with German module translations. Desktop +and mobile runs traverse native controls by keyboard, inspect accessible names +and landmarks, run WCAG 2.1 A/AA automation, verify responsive overflow, and +retain independent per-field assisted provenance. Physical assistive-technology +spot checks remain release evidence rather than being represented as browser +automation. + ## Verification ```bash diff --git a/docs/UI_UX_DECISION_LEDGER.md b/docs/UI_UX_DECISION_LEDGER.md index 5e04e61..6eb5d30 100644 --- a/docs/UI_UX_DECISION_LEDGER.md +++ b/docs/UI_UX_DECISION_LEDGER.md @@ -236,7 +236,9 @@ instead of reproducing their behavior. not self-explanatory. - `help` content is contextual guidance, not the accessible name. The persisted `show_inline_help_hints` user preference hides only the `InlineHelp` marker by - applying `ui-hide-help-hints` at the document root. + applying `ui-hide-help-hints` at the document root. When shown, the shared + marker is a labelled, keyboard-focusable help control and exposes its tooltip + on focus as well as pointer hover. - Shared action-bearing components accept an optional disabled reason. In particular, `MailServerSettingsPanel` forwards protocol-specific test blockers into the shared focusable disabled-action tooltip; modules provide diff --git a/pyproject.toml b/pyproject.toml index c00f520..a40dcb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "govoplan-core" -version = "0.1.40" +version = "0.1.41" description = "Reusable GovOPlaN platform core, access, tenancy, and RBAC components." readme = "README.md" requires-python = ">=3.12" diff --git a/webui/conformance/ConformanceApp.tsx b/webui/conformance/ConformanceApp.tsx index a3943d4..67300a4 100644 --- a/webui/conformance/ConformanceApp.tsx +++ b/webui/conformance/ConformanceApp.tsx @@ -1,6 +1,9 @@ import { useMemo, useState } from "react"; import { FileText, GitBranch, Inbox, Search, ShieldCheck } from "lucide-react"; import { useLocation } from "react-router"; +import FormInstancePage from "../../../govoplan-forms-runtime/webui/src/features/forms/FormInstancePage"; +import FormsRuntimePage from "../../../govoplan-forms-runtime/webui/src/features/forms/FormsRuntimePage"; +import PublicFormPage from "../../../govoplan-forms-runtime/webui/src/features/forms/PublicFormPage"; import QuickAccessRail from "../../../govoplan-quick-access/webui/src/components/QuickAccessRail"; import ActionToolbar from "../src/components/ActionToolbar"; import Button from "../src/components/Button"; @@ -40,6 +43,16 @@ export default function ConformanceApp() { const [editorDirty, setEditorDirty] = useState(true); const [metricDrilldown, setMetricDrilldown] = useState(""); + if (location.pathname.startsWith("/forms/public/")) { + return ; + } + if (location.pathname === "/forms-runtime") { + return ; + } + if (location.pathname.startsWith("/forms-runtime/")) { + return ; + } + return (
- + - + + } /> + } /> + } /> + diff --git a/webui/conformance/tests/ui-conformance.spec.ts b/webui/conformance/tests/ui-conformance.spec.ts index 30641e0..2094e17 100644 --- a/webui/conformance/tests/ui-conformance.spec.ts +++ b/webui/conformance/tests/ui-conformance.spec.ts @@ -19,6 +19,125 @@ async function expectNoAccessibilityViolations(page: import("@playwright/test"). expect(violations).toEqual([]); } +for (const viewport of [ + { name: "desktop", width: 1280, height: 900 }, + { name: "mobile", width: 390, height: 844 } +]) { + test(`resident permit self-service is keyboard and accessibility conformant on ${viewport.name}`, async ({ page }) => { + const journey = await mockPublicResidentPermitJourney(page); + await page.setViewportSize(viewport); + await page.goto("/forms/public/resident-parking-permit?theme=light"); + + await expect(page.getByRole("heading", { level: 1, name: "Anwohnerparkausweis beantragen" })).toBeVisible(); + await expectNoAccessibilityViolations(page); + + const name = page.getByLabel("Name der antragstellenden Person"); + await name.focus(); + await page.keyboard.type("Ada Lovelace"); + await page.keyboard.press("Tab"); + await expect(page.getByLabel("E-Mail-Adresse")).toBeFocused(); + await page.keyboard.type("ada.lovelace@example.test"); + await page.keyboard.press("Tab"); + await expect(page.getByLabel("Hauptwohnsitz")).toBeFocused(); + await page.keyboard.type("Musterstraße 17, 10115 Berlin"); + await page.keyboard.press("Tab"); + await expect(page.getByLabel("Kfz-Kennzeichen")).toBeFocused(); + await page.keyboard.type("B-AL 1843"); + await page.keyboard.press("Tab"); + await expect(page.getByRole("button", { name: "Entwurf speichern" })).toBeFocused(); + await page.keyboard.press("Enter"); + await expect.poll(() => journey.savedValues()).toEqual({ + applicant_name: "Ada Lovelace", + applicant_email: "ada.lovelace@example.test", + residence_address: "Musterstraße 17, 10115 Berlin", + licence_plate: "B-AL 1843" + }); + + await page.getByRole("button", { name: "Absenden" }).click(); + const confirm = page.getByRole("alertdialog", { name: "Formular absenden" }); + await expect(confirm).toBeVisible(); + await expectNoAccessibilityViolations(page); + await confirm.getByRole("button", { name: "Absenden" }).click(); + await expect(page.getByText("Übermittlung eingegangen")).toBeVisible(); + await expect(page.getByText("receipt-rpp-2026-0001")).toBeVisible(); + await expectNoHorizontalOverflow(page); + }); + + test(`resident permit assisted intake preserves per-field provenance on ${viewport.name}`, async ({ page }) => { + const journey = await mockAssistedResidentPermitJourney(page); + await page.setViewportSize(viewport); + await page.goto("/forms-runtime?theme=light"); + + await page.getByRole("button", { name: "Assistierte Erfassung" }).click(); + const startDialog = page.getByRole("dialog", { name: "Assistierte Erfassung starten" }); + await expect(startDialog).toBeVisible(); + await expectNoAccessibilityViolations(page); + await startDialog.getByLabel("Referenz der betroffenen Partei").fill("party:resident-ada-lovelace"); + await startDialog.getByLabel("Referenz der zuständigen Funktion").fill("function:parking-permits"); + await startDialog.getByLabel("Zweck").fill("Anwohnerparkausweis beantragen"); + await startDialog.getByLabel("Referenz der Rechtsgrundlage").fill("law:resident-parking-permit"); + await startDialog.getByLabel("Barrierefreiheits- oder Kommunikationsunterstützung").fill("Leichte Sprache"); + const notice = startDialog.getByRole("checkbox", { name: "Datenschutz- und Verfahrenshinweis wurde erteilt" }); + await notice.focus(); + await page.keyboard.press("Space"); + await expect(notice).toBeChecked(); + await startDialog.getByLabel("Referenz der betroffenen Partei").focus(); + await page.keyboard.press("Tab"); + await expect(page.locator(":focus")).toHaveAttribute("aria-label", "Feldhilfe anzeigen"); + await page.keyboard.press("Tab"); + await expect(startDialog.getByLabel("Referenz der vertretenen Partei")).toBeFocused(); + await startDialog.getByRole("button", { name: "Sitzung starten" }).click(); + + await expect(page).toHaveURL(/\/forms-runtime\/assisted-rpp-1$/); + await expect(page.getByRole("heading", { level: 1, name: "Anwohnerparkausweis beantragen" })).toBeVisible(); + await page.getByLabel("Name der antragstellenden Person").fill("Ada Lovelace"); + await page.getByLabel("E-Mail-Adresse").fill("ada.lovelace@example.test"); + await page.getByLabel("Hauptwohnsitz").fill("Musterstraße 17, 10115 Berlin"); + await page.getByLabel("Kfz-Kennzeichen").fill("B-AL 1843"); + await page.getByLabel("Änderungsgrund").fill("Angaben gemeinsam mit der antragstellenden Person erfasst."); + await page.getByRole("button", { name: "Entwurf speichern" }).click(); + + await page.getByRole("button", { name: "Rücklesen und absenden" }).click(); + const readback = page.getByRole("dialog", { name: "Assistiertes Rücklesen erfassen" }); + await expect(readback).toBeVisible(); + await expect(readback.getByRole("group", { name: "Hauptwohnsitz" })).toBeVisible(); + await expectNoAccessibilityViolations(page); + + const addressSource = readback.getByRole("group", { name: "Hauptwohnsitz" }); + await addressSource.getByLabel("Wertquelle").selectOption("document"); + await addressSource.getByLabel("Quellenvertrauen").selectOption("verified"); + await addressSource.getByLabel("Erklärende Partei oder Quellenreferenz").fill("files:residence-proof-2026"); + const plateSource = readback.getByRole("group", { name: "Kfz-Kennzeichen" }); + await plateSource.getByLabel("Wertquelle").focus(); + await page.keyboard.press("ArrowDown"); + await page.keyboard.press("ArrowDown"); + await page.keyboard.press("ArrowDown"); + await page.keyboard.press("Enter"); + await plateSource.getByLabel("Quellenvertrauen").selectOption("verified"); + await plateSource.getByLabel("Erklärende Partei oder Quellenreferenz").fill("register:vehicle-B-AL-1843"); + await readback.getByRole("button", { name: "Erfassen und fortfahren" }).click(); + + await expect.poll(() => journey.confirmationSources()).toMatchObject({ + applicant_name: { source: "person_statement", confidence: "stated" }, + residence_address: { + source: "document", + confidence: "verified", + declared_by_ref: "files:residence-proof-2026" + }, + licence_plate: { + source: "system", + confidence: "verified", + declared_by_ref: "register:vehicle-B-AL-1843" + } + }); + const submit = page.getByRole("alertdialog", { name: "Formular absenden" }); + await submit.getByRole("button", { name: "Absenden" }).click(); + await expect(page.getByText("receipt-assisted-rpp-2026-0001")).toBeVisible(); + await expect(page.getByText("Rücklesen erfasst")).toBeVisible(); + await expectNoHorizontalOverflow(page); + }); +} + 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(); @@ -229,6 +348,301 @@ test("narrow layout preserves task order without horizontal overflow", async ({ await expect(page.locator("[data-conformance-id='shared-ui-lab']")).toHaveScreenshot("shared-ui-light-narrow.png", { animations: "disabled", maxDiffPixelRatio: 0.005 }); }); +async function expectNoHorizontalOverflow(page: import("@playwright/test").Page) { + const overflowing = await page.evaluate(() => Array.from(document.querySelectorAll("body *")) + .filter((element) => { + const style = window.getComputedStyle(element); + return style.display !== "none" && style.visibility !== "hidden"; + }) + .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) + }; + }) + .filter(({ left, right }) => left < -1 || right > window.innerWidth + 1) + .slice(0, 20)); + expect(overflowing).toEqual([]); +} + +async function mockPublicResidentPermitJourney(page: import("@playwright/test").Page) { + let current = residentPermitInstance("public-rpp-1", "started", 1, {}); + let savedValues: Record = {}; + + await page.route("**/api/v1/forms-runtime/**", async (route) => { + const request = route.request(); + const path = new URL(request.url()).pathname; + const method = request.method(); + if (path.endsWith("/public/profiles/resident-parking-permit/start") && method === "POST") { + return fulfillJson(route, { + session_id: "session-public-rpp-1", + mode: "anonymous", + status: "active", + expires_at: "2026-08-25T10:00:00Z", + instance: current, + token: "public-rpp-token", + replayed: false + }); + } + if (path.endsWith("/public/intake") && method === "GET") { + return fulfillJson(route, { instance: current, definition: residentPermitDefinition() }); + } + if (path.endsWith("/public/intake") && method === "PATCH") { + const payload = request.postDataJSON() as { values: Record }; + savedValues = payload.values; + current = residentPermitInstance("public-rpp-1", "draft", 2, payload.values); + return fulfillJson(route, current); + } + if (path.endsWith("/public/intake/submit") && method === "POST") { + const payload = request.postDataJSON() as { values: Record }; + current = { + ...residentPermitInstance("public-rpp-1", "submitted", 3, payload.values), + receipt_id: "receipt-rpp-2026-0001" + }; + return fulfillJson(route, current); + } + return route.abort("failed"); + }); + + return { savedValues: () => savedValues }; +} + +async function mockAssistedResidentPermitJourney(page: import("@playwright/test").Page) { + let current = residentPermitInstance("assisted-rpp-1", "started", 1, {}, true); + let confirmationSources: Record = {}; + let confirmations: unknown[] = []; + + await page.route("**/api/v1/forms-runtime/**", async (route) => { + const request = route.request(); + const path = new URL(request.url()).pathname; + const method = request.method(); + + if (path.endsWith("/instances") && method === "GET") { + return fulfillJson(route, { instances: [], total: 0, offset: 0, limit: 200 }); + } + if (path.endsWith("/assisted-intake/profiles") && method === "GET") { + return fulfillJson(route, { profiles: [{ + profile_id: "assisted-profile-rpp", + public_id: "resident-parking-permit", + definition_ref: residentPermitDefinition().reference, + mode: "assisted", + enabled: true, + revision: 1, + draft_ttl_seconds: 2_592_000, + invitation_ttl_seconds: 1_209_600, + rate_limit_per_minute: 60, + metadata: { definition_title: "Anwohnerparkausweis beantragen" } + }] }); + } + if (path.endsWith("/assisted-intake/start") && method === "POST") { + return fulfillJson(route, { + session_id: "assisted-session-rpp-1", + mode: "assisted", + status: "active", + expires_at: "2026-08-25T10:00:00Z", + instance: current, + token: null, + replayed: false + }); + } + if (path.endsWith("/instances/assisted-rpp-1/definition") && method === "GET") { + return fulfillJson(route, residentPermitDefinition()); + } + if (path.endsWith("/instances/assisted-rpp-1/history") && method === "GET") { + return fulfillJson(route, { revisions: [current] }); + } + if (path.endsWith("/instances/assisted-rpp-1/events") && method === "GET") { + return fulfillJson(route, { events: [{ + event_id: `event-${current.revision}`, + event_type: current.status === "submitted" ? "submitted" : "draft_saved", + instance_revision: current.revision, + status: current.status, + occurred_at: current.recorded_at, + actor_id: "operator-1", + payload: {} + }] }); + } + if (path.endsWith("/instances/assisted-rpp-1/handoffs") && method === "GET") { + return fulfillJson(route, { handoffs: [] }); + } + if (path.endsWith("/instances/assisted-rpp-1/assisted-confirmations") && method === "GET") { + return fulfillJson(route, { confirmations }); + } + if (path.endsWith("/instances/assisted-rpp-1/assisted-confirmations") && method === "POST") { + const payload = request.postDataJSON() as { field_sources: Record }; + confirmationSources = payload.field_sources; + const confirmation = { + confirmation_id: "confirmation-rpp-1", + instance_id: "assisted-rpp-1", + instance_revision: current.revision, + outcome: "confirmed", + method: "spoken_readback", + confirmed_by_ref: "party:resident-ada-lovelace", + operator_actor_id: "operator-1", + confirmed_at: "2026-08-24T10:10:00Z", + payload_sha256: "a".repeat(64), + correction_note: null, + metadata: { field_sources: confirmationSources } + }; + confirmations = [confirmation]; + return fulfillJson(route, confirmation); + } + if (path.endsWith("/instances/assisted-rpp-1/submit") && method === "POST") { + const payload = request.postDataJSON() as { values: Record }; + current = { + ...residentPermitInstance("assisted-rpp-1", "submitted", current.revision + 1, payload.values, true), + receipt_id: "receipt-assisted-rpp-2026-0001" + }; + return fulfillJson(route, current); + } + if (path.endsWith("/instances/assisted-rpp-1") && method === "PATCH") { + const payload = request.postDataJSON() as { values: Record }; + current = residentPermitInstance("assisted-rpp-1", "draft", current.revision + 1, payload.values, true); + return fulfillJson(route, current); + } + if (path.endsWith("/instances/assisted-rpp-1") && method === "GET") { + return fulfillJson(route, current); + } + return route.abort("failed"); + }); + + return { confirmationSources: () => confirmationSources }; +} + +function residentPermitDefinition() { + return { + reference: { + kind: "form", + owner_module: "forms", + object_id: "resident-parking-permit-application", + tenant_id: "tenant-1", + version: "3", + label: "Anwohnerparkausweis beantragen" + }, + key: "resident_parking_permit.apply", + temporal: { revision: "3", recorded_at: "2026-08-24T10:00:00Z" }, + title: "Resident parking permit application", + description: "Apply digitally or together with an authorized service worker.", + fields: [ + residentPermitField("applicant_name", "Applicant name", "text", { min_length: 2, max_length: 200 }), + residentPermitField("applicant_email", "Applicant email", "email", { format: "email" }), + residentPermitField("residence_address", "Primary residence", "text", { max_length: 500 }), + residentPermitField("licence_plate", "Licence plate", "text", { max_length: 20 }) + ], + publication_state: "published", + allow_drafts: true, + max_attachments: 0, + signature_requirement: "none", + policy_refs: ["law:resident-parking-permit"], + handoff_kinds: [], + fallback_locale: "de", + localizations: [{ + locale: "de", + title: "Anwohnerparkausweis beantragen", + description: "Beantragen Sie den Anwohnerparkausweis digital oder gemeinsam mit einer berechtigten Servicestelle.", + field_labels: { + applicant_name: "Name der antragstellenden Person", + applicant_email: "E-Mail-Adresse", + residence_address: "Hauptwohnsitz", + licence_plate: "Kfz-Kennzeichen" + }, + field_help_texts: {}, + option_labels: {}, + page_titles: {}, + section_titles: {} + }] + }; +} + +function residentPermitField( + key: string, + label: string, + valueType: "text" | "email", + constraints: Record +) { + return { + key, + label, + value_type: valueType, + required: true, + help_text: null, + options: [], + constraints, + default_value: null, + visibility_condition: null + }; +} + +function residentPermitInstance( + instanceId: string, + status: string, + revision: number, + values: Record, + assisted = false +) { + return { + reference: { + kind: "form_instance", + owner_module: "forms_runtime", + object_id: instanceId, + tenant_id: "tenant-1", + version: String(revision), + label: "Anwohnerparkausweis beantragen" + }, + tenant_id: "tenant-1", + instance_id: instanceId, + revision, + status, + definition_ref: residentPermitDefinition().reference, + values, + validation_results: [], + attachment_refs: [], + signature_refs: [], + handoff_refs: [], + service_ref: null, + receipt_id: null as string | null, + recorded_at: "2026-08-24T10:00:00Z", + change_reason: revision === 1 ? "Assisted session started." : "Draft saved.", + created_by: "operator-1", + changed_by: "operator-1", + metadata: assisted ? { + intake: { + session_id: "assisted-session-rpp-1", + profile_id: "assisted-profile-rpp", + mode: "assisted", + channel: "counter", + affected_party_ref: "party:resident-ada-lovelace", + represented_party_ref: null, + authority_basis: "self", + purpose: "Anwohnerparkausweis beantragen", + legal_basis_ref: "law:resident-parking-permit", + consent_basis: "in-person-confirmation", + notice_given: true, + responsible_function_ref: "function:parking-permits", + language: "de", + accessibility_needs: ["Leichte Sprache"], + field_sources: {}, + operator: { actor_id: "operator-1", auth_method: "session" } + } + } : {}, + status_access: null, + replayed: false + }; +} + +async function fulfillJson( + route: import("@playwright/test").Route, + body: unknown +) { + await route.fulfill({ + status: 200, + contentType: "application/json", + body: JSON.stringify(body) + }); +} + function quickAccessPayload(includeMessages: boolean) { const files = { id: "files", diff --git a/webui/package-lock.json b/webui/package-lock.json index f839c7e..07150c3 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -1,12 +1,12 @@ { "name": "@govoplan/core-webui", - "version": "0.1.40", + "version": "0.1.41", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@govoplan/core-webui", - "version": "0.1.40", + "version": "0.1.41", "dependencies": { "@govoplan/access-webui": "file:../../govoplan-access/webui", "@govoplan/addresses-webui": "file:../../govoplan-addresses/webui", @@ -378,7 +378,7 @@ }, "../../govoplan-forms-runtime/webui": { "name": "@govoplan/forms-runtime-webui", - "version": "0.1.18", + "version": "0.1.20", "peerDependencies": { "@govoplan/core-webui": "^0.1.18", "lucide-react": "^1.23.0", diff --git a/webui/package-lock.release.json b/webui/package-lock.release.json index f70c61d..b252175 100644 --- a/webui/package-lock.release.json +++ b/webui/package-lock.release.json @@ -1,12 +1,12 @@ { "name": "@govoplan/core-webui", - "version": "0.1.40", + "version": "0.1.41", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@govoplan/core-webui", - "version": "0.1.40", + "version": "0.1.41", "dependencies": { "@govoplan/access-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-access.git#v0.1.23", "@govoplan/admin-webui": "git+ssh://git@git.add-ideas.de/GovOPlaN/govoplan-admin.git#v0.1.22", diff --git a/webui/package.json b/webui/package.json index eee3746..c67349c 100644 --- a/webui/package.json +++ b/webui/package.json @@ -1,6 +1,6 @@ { "name": "@govoplan/core-webui", - "version": "0.1.40", + "version": "0.1.41", "private": true, "type": "module", "main": "src/index.ts", diff --git a/webui/package.release.json b/webui/package.release.json index c7b4d3f..cef0074 100644 --- a/webui/package.release.json +++ b/webui/package.release.json @@ -1,6 +1,6 @@ { "name": "@govoplan/core-webui", - "version": "0.1.40", + "version": "0.1.41", "private": true, "type": "module", "main": "src/index.ts", diff --git a/webui/src/components/HoverTooltip.tsx b/webui/src/components/HoverTooltip.tsx index 5526204..cfb4d5c 100644 --- a/webui/src/components/HoverTooltip.tsx +++ b/webui/src/components/HoverTooltip.tsx @@ -207,6 +207,7 @@ export default function HoverTooltip({ + triggerTabIndex={0}> ); diff --git a/webui/src/i18n/generatedTranslations.ts b/webui/src/i18n/generatedTranslations.ts index 7f8c3de..02aaee2 100644 --- a/webui/src/i18n/generatedTranslations.ts +++ b/webui/src/i18n/generatedTranslations.ts @@ -1276,8 +1276,8 @@ export const generatedTranslations: PlatformTranslations = { "i18n:govoplan-core.share.09ca55ca": "Freigabe", "i18n:govoplan-core.set_concrete_system_retention_values_blank_day_f.98b9a627": "Set concrete system retention values. Blank day fields mean unlimited retention.", "i18n:govoplan-core.settings.c7f73bb5": "Einstellungen", - "i18n:govoplan-core.show_content.0528d8d2": "Show content", - "i18n:govoplan-core.show_field_help.e3dfe98f": "Show field help", + "i18n:govoplan-core.show_content.0528d8d2": "Inhalt anzeigen", + "i18n:govoplan-core.show_field_help.e3dfe98f": "Feldhilfe anzeigen", "i18n:govoplan-core.show_guided_warnings_while_editing.bc5dba85": "Show guided warnings while editing", "i18n:govoplan-core.show_header_only.24afefca": "Show header only", "i18n:govoplan-core.show_inline_guidance_and_warnings_while_campaign.a892f5e9": "Show inline guidance and warnings while campaign data is being edited.", diff --git a/webui/src/styles/badges.css b/webui/src/styles/badges.css index b65f7a6..640e7da 100644 --- a/webui/src/styles/badges.css +++ b/webui/src/styles/badges.css @@ -1,5 +1,5 @@ .status-badge { display: inline-flex; align-items: center; height: 24px; border-radius: var(--radius-pill); padding: 0 9px; font-size: 12px; font-weight: 800; background: var(--status-neutral-bg); color: var(--text-soft); text-transform: uppercase; } -.status-ready, .status-sent, .status-appended, .status-success, .status-active { background: var(--success-soft); color: var(--success-text-strong); } +.status-ready, .status-sent, .status-appended, .status-success, .status-active { background: var(--success-soft); color: var(--success-text); } .status-warning, .status-needs-review, .status-pending { background: var(--warning-soft); color: var(--warning-text-strong); } .status-blocked, .status-error, .status-danger, .status-failed, .status-failed-permanent { background: var(--danger-bg); color: var(--danger-text-strong); } .status-queued, .status-sending { background: var(--info-soft); color: var(--info-text-strong); }