Adopt shared WebUI structural primitives

This commit is contained in:
2026-08-18 13:17:32 +02:00
parent f0f0286866
commit dc9fdc9143
2 changed files with 44 additions and 107 deletions
+44 -30
View File
@@ -10,8 +10,15 @@ import { ActionToolbar,
FormField,
IconButton,
LoadingIndicator,
MetricCard,
MetricGrid,
PageScrollViewport,
SelectionList,
SelectionListItem,
SelectionListItemContent,
StatePanel,
StatusBadge,
WorkspaceLayout,
hasScope,
i18nMessage,
usePlatformLanguage,
@@ -144,28 +151,40 @@ export default function VotingPage({ settings, auth }: PlatformRouteContext) {
return (
<main className="voting-page">
<div className="voting-shell">
<aside className="voting-catalogue">
<ActionToolbar className="voting-toolbar">
<WorkspaceLayout
variant="split"
primarySize="default"
primaryScrollable={false}
contentScrollable={false}
surface="contained"
primaryClassName="voting-catalogue"
contentClassName="voting-workspace"
primaryLabel="Ballots"
contentLabel="Ballot details"
interfaceId="voting.workspace"
helpContextId="voting.workspace"
helpModuleId="voting"
primary={<>
<ActionToolbar surface="panel-header" className="voting-toolbar">
<IconButton label="Refresh ballots" icon={<RefreshCw size={16} />} disabled={loading || busy} disabledReason={loading ? VOTING_I18N.loading : busy ? VOTING_I18N.busy : undefined} onClick={() => void loadList()} />
<Button variant="primary" disabled={!canManage} disabledReason={!canManage ? VOTING_I18N.manageReason : undefined} onClick={() => setEditing("new")}><Plus size={16} aria-hidden="true" />New ballot</Button>
<DocumentationHelpLink reference={VOTING_DOCUMENTATION} />
</ActionToolbar>
<PageScrollViewport className="voting-list-viewport">
{loading && <LoadingIndicator label="Loading ballots" />}
{!loading && items.length === 0 && <div className="voting-empty">No ballots.</div>}
<div className="voting-list" role="list">
{items.map((item) => <button type="button" role="listitem" className={`voting-list-row${item.id === selectedId ? " is-selected" : ""}`} key={item.id} onClick={() => setSelectedId(item.id)}>
<span><strong>{item.title}</strong><small>{humanize(item.assurance_profile)}</small></span>
{!loading && items.length === 0 && <StatePanel size="compact" description="No ballots." />}
<SelectionList variant="navigation" label="Ballots">
{items.map((item) => <SelectionListItem selected={item.id === selectedId} key={item.id} onClick={() => setSelectedId(item.id)}>
<SelectionListItemContent title={item.title} description={humanize(item.assurance_profile)} />
<StatusBadge status={statusTone(item.state)} label={humanize(item.state)} />
</button>)}
</div>
</SelectionListItem>)}
</SelectionList>
</PageScrollViewport>
</aside>
<section className="voting-workspace">
</>}
>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
{notice && <DismissibleAlert tone="success" resetKey={notice}>{notice}</DismissibleAlert>}
{!selected && !loading && <div className="voting-empty">Select a ballot.</div>}
{!selected && !loading && <StatePanel size="fill" title="Ballots" description="Select a ballot." />}
{selected && <PageScrollViewport className="voting-detail-viewport">
<div className="voting-detail-heading">
<div><h2>{selected.title}</h2><span>Revision {selected.revision}</span></div>
@@ -179,12 +198,12 @@ export default function VotingPage({ settings, auth }: PlatformRouteContext) {
{selected.state !== "annulled" && <Button variant="danger" disabled={busy || !canAdmin} disabledReason={busy ? VOTING_I18N.busy : !canAdmin ? VOTING_I18N.adminReason : undefined} onClick={() => setReasonAction("annul")}>Annul</Button>}
</div>
</div>
<div className="voting-metrics">
<Metric label="Electors" value={selected.electorate.length} />
<Metric label="Eligible weight" value={selected.electorate.reduce((total, item) => total + item.weight, 0)} />
<Metric label="Quorum" value={selected.quorum_weight} />
<Metric label="Assurance" value={humanize(selected.assurance_profile)} />
</div>
<MetricGrid columns={4} spacing="block">
<MetricCard density="compact" label="Electors" value={selected.electorate.length} />
<MetricCard density="compact" label="Eligible weight" value={selected.electorate.reduce((total, item) => total + item.weight, 0)} />
<MetricCard density="compact" label="Quorum" value={selected.quorum_weight} />
<MetricCard density="compact" label="Assurance" value={humanize(selected.assurance_profile)} />
</MetricGrid>
{selected.description && <p className="voting-description">{selected.description}</p>}
{selected.state === "open" && selected.assurance_profile === "recorded" && canCast && eligible && <section className="voting-cast-panel">
<h3>Cast vote</h3>
@@ -215,12 +234,12 @@ export default function VotingPage({ settings, auth }: PlatformRouteContext) {
</section>
{selected.result && <section className="voting-section">
<h3>Result</h3>
<div className="voting-metrics">
<Metric label="Votes" value={`${selected.result.cast_count} / ${selected.result.eligible_count}`} />
<Metric label="Cast weight" value={`${selected.result.cast_weight} / ${selected.result.eligible_weight}`} />
<Metric label="Quorum" value={selected.result.quorum_met ? "Met" : "Not met"} />
<Metric label="Threshold" value={selected.result.threshold_met ? "Met" : "Not met"} />
</div>
<MetricGrid columns={4} spacing="block">
<MetricCard density="compact" label="Votes" value={`${selected.result.cast_count} / ${selected.result.eligible_count}`} />
<MetricCard density="compact" label="Cast weight" value={`${selected.result.cast_weight} / ${selected.result.eligible_weight}`} />
<MetricCard density="compact" label="Quorum" value={selected.result.quorum_met ? "Met" : "Not met"} />
<MetricCard density="compact" label="Threshold" value={selected.result.threshold_met ? "Met" : "Not met"} />
</MetricGrid>
<Hash label="Result hash" value={selected.result.result_sha256} />
</section>}
<section className="voting-section voting-assurance">
@@ -235,8 +254,7 @@ export default function VotingPage({ settings, auth }: PlatformRouteContext) {
</div>
</section>
</PageScrollViewport>}
</section>
</div>
</WorkspaceLayout>
{editing && <VotingBallotDialog
open
settings={settings}
@@ -299,10 +317,6 @@ export default function VotingPage({ settings, auth }: PlatformRouteContext) {
);
}
function Metric({ label, value }: { label: string; value: string | number }) {
return <div><span>{label}</span><strong>{value}</strong></div>;
}
function Hash({ label, value }: { label: string; value?: string | null }) {
return <div className="voting-hash"><span>{label}</span><code>{value || "Not frozen"}</code></div>;
}