diff --git a/webui/src/features/approvals/ApprovalsPage.tsx b/webui/src/features/approvals/ApprovalsPage.tsx
index 44dfa9b..52302f1 100644
--- a/webui/src/features/approvals/ApprovalsPage.tsx
+++ b/webui/src/features/approvals/ApprovalsPage.tsx
@@ -1,6 +1,6 @@
import { AlarmClock, Check, Plus, RefreshCw, X } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
-import { ActionToolbar, ActionBlockerHint, Button, ConfirmDialog, Dialog, DocumentationHelpLink, DismissibleAlert, FormField, IconButton, LoadingIndicator, PageScrollViewport, StatusBadge, WorkspaceLayout, hasScope, usePlatformLanguage, useUnsavedChanges, useUnsavedDraftGuard, type PlatformRouteContext } from "@govoplan/core-webui";
+import { ActionToolbar, ActionBlockerHint, Button, ConfirmDialog, Dialog, DocumentationHelpLink, DismissibleAlert, FormField, IconButton, LoadingIndicator, MetricCard, MetricGrid, PageScrollViewport, SelectionList, SelectionListItem, SelectionListItemContent, StatePanel, StatusBadge, WorkspaceLayout, hasScope, usePlatformLanguage, useUnsavedChanges, useUnsavedDraftGuard, type PlatformRouteContext } from "@govoplan/core-webui";
import { approvalHistory, decideApproval, escalateApproval, getApproval, listApprovals, type ApprovalEvent, type ApprovalRequest } from "../../api/approvals";
import ApprovalRequestDialog from "./ApprovalRequestDialog";
import { APPROVALS_DOCUMENTATION, APPROVALS_FIELD_DOCUMENTATION, APPROVALS_I18N } from "./interfacePatterns";
@@ -59,6 +59,7 @@ export default function ApprovalsPage({ settings, auth }: PlatformRouteContext)
primarySize="compact"
primaryScrollable={false}
contentScrollable={false}
+ surface="contained"
className="approvals-shell"
primaryClassName="approvals-list-panel"
contentClassName="approvals-workspace"
@@ -68,20 +69,20 @@ export default function ApprovalsPage({ settings, auth }: PlatformRouteContext)
helpContextId="approvals.workspace"
helpModuleId="approvals"
primary={<>
- } disabled={loading || busy} disabledReason={loading ? APPROVALS_I18N.loading : busy ? APPROVALS_I18N.busy : undefined} onClick={() => void load()} />
- {loading && }{items.map((item) => )}
{!loading && items.length === 0 && No approval requests
}
+ } disabled={loading || busy} disabledReason={loading ? APPROVALS_I18N.loading : busy ? APPROVALS_I18N.busy : undefined} onClick={() => void load()} />
+ {loading && }{items.map((item) => setSelectedId(item.id)}>)}{!loading && items.length === 0 && }
>}
>
{error && {error}}
{!canWrite && }
{selected &&
-
+
{selected.description &&
{selected.description}
}
Approval chain
{selected.steps.map((step, index) =>
{index + 1}{step.label}{step.required_approvals} required / {step.selectors.map((item) => `${humanize(item.kind)}: ${item.label || item.value}`).join(", ")}{step.signature_required && Signature}
)}
History
{history.map((event) =>
{event.sequence}{humanize(event.event_type)}
)}
}
- {!selected && !loading &&
Select or create an approval request.
}
+ {!selected && !loading && }
{creating && setCreating(false)} onSaved={(item) => { setCreating(false); setSelectedId(item.id); void load(undefined, item.id); }} />}
{selected && decision && setDecision(null)} onConfirm={async (reason, signatureId) => { setBusy(true); setError(""); try { await decideApproval(settings, selected, decision, reason, signatureId ? { owner_module: "signatures", object_id: signatureId } : undefined); setDecision(null); await reload(selected.id); return true; } catch (failure) { setError(text(failure, "The Approval decision could not be recorded.")); return false; } finally { setBusy(false); } }} />}
@@ -100,7 +101,6 @@ function DecisionDialog({ outcome, busy, signatureRequired, onClose, onConfirm }
return ;
}
-function Metric({ label, value }: { label: string; value: string | number }) { return {label}{value}
; }
function tone(state: ApprovalRequest["state"]): "active" | "inactive" | "warning" { if (state === "approved") return "active"; if (["rejected", "cancelled", "expired"].includes(state)) return "inactive"; return "warning"; }
function humanize(value: string): string { return value.replace(/[_:.-]+/g, " ").replace(/\b\w/g, (letter) => letter.toUpperCase()); }
function text(value: unknown, fallback: string): string { return value instanceof Error ? value.message : fallback; }
diff --git a/webui/src/styles/approvals.css b/webui/src/styles/approvals.css
index c396ae4..8986865 100644
--- a/webui/src/styles/approvals.css
+++ b/webui/src/styles/approvals.css
@@ -1,22 +1,13 @@
.approvals-page { height: 100%; min-height: 0; overflow: hidden; }
-.approvals-list-panel { display: flex; min-height: 0; flex-direction: column; border-right: 1px solid var(--border-color, #d8dde3); }
-.approvals-toolbar { display: flex; align-items: center; gap: 8px; min-height: 50px; padding: 8px 12px; border-bottom: 1px solid var(--border-color, #d8dde3); }
+.approvals-list-panel { display: flex; min-height: 0; flex-direction: column; }
.approvals-list-viewport, .approvals-detail-viewport { min-height: 0; flex: 1; }
-.approvals-list { display: flex; flex-direction: column; gap: 2px; padding: 6px; }
-.approvals-list button { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; min-height: 52px; padding: 7px 8px; border: 0; background: transparent; color: inherit; text-align: left; cursor: pointer; }
-.approvals-list button:hover, .approvals-list button.is-selected { background: var(--hover-bg, rgba(54, 99, 135, 0.1)); }
-.approvals-list button > span { display: flex; min-width: 0; flex-direction: column; }
-.approvals-list small, .approvals-detail header span { color: var(--text-muted, #65717e); }
+.approvals-detail header span { color: var(--text-muted, #65717e); }
.approvals-workspace { display: flex; min-width: 0; min-height: 0; flex-direction: column; }
.approvals-workspace > .action-blocker-hint { margin: 12px 16px 0; }
-.approvals-empty { display: grid; min-height: 120px; place-items: center; padding: 16px; color: var(--text-muted, #65717e); text-align: center; }
.approvals-detail { padding: 16px 20px 28px; }
.approvals-detail header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-bottom: 10px; border-bottom: 1px solid var(--border-color, #d8dde3); }
.approvals-detail header > div:last-child { display: flex; align-items: center; gap: 8px; }
.approvals-detail h2, .approvals-detail h3, .approval-editor-heading h3 { margin: 0; font-size: 1rem; letter-spacing: 0; }
-.approval-metrics { display: grid; grid-template-columns: repeat(4, minmax(110px, 1fr)); gap: 10px; margin: 16px 0; }
-.approval-metrics > div { display: flex; flex-direction: column; padding: 10px 12px; border: 1px solid var(--border-color, #d8dde3); border-radius: 4px; }
-.approval-metrics span { color: var(--text-muted, #65717e); font-size: .75rem; text-transform: uppercase; }
.approvals-detail section { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--border-color, #d8dde3); }
.approval-chain, .approval-history { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
.approval-chain > div, .approval-history > div { display: grid; grid-template-columns: 32px minmax(120px, .6fr) minmax(0, 1fr) auto; align-items: center; gap: 10px; min-height: 40px; padding: 7px 9px; background: var(--surface-muted, rgba(127,137,147,.08)); }
@@ -43,4 +34,4 @@
.approval-compare-toolbar { display: grid; grid-template-columns: minmax(180px, 1fr) minmax(180px, 1fr) auto; align-items: end; gap: 8px; }
.approval-diff-value { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.approval-decision-form { display: flex; min-width: min(520px, 75vw); flex-direction: column; gap: 10px; }
-@media (max-width: 900px) { .approvals-list-panel { border-right: 0; border-bottom: 1px solid var(--border-color, #d8dde3); } .approval-metrics, .approval-step-fields, .approval-selector-list > div, .approval-compare-toolbar { grid-template-columns: 1fr; } .approval-editor-wide { grid-column: auto; } }
+@media (max-width: 900px) { .approval-step-fields, .approval-selector-list > div, .approval-compare-toolbar { grid-template-columns: 1fr; } .approval-editor-wide { grid-column: auto; } }