Adopt shared WebUI structural primitives

This commit is contained in:
2026-08-18 13:17:29 +02:00
parent 9a2f13bc9a
commit 06f773e4eb
5 changed files with 81 additions and 177 deletions
@@ -1,6 +1,6 @@
import { useState } from "react";
import { Link2, X } from "lucide-react";
import { Button, Dialog, DismissibleAlert, i18nMessage } from "@govoplan/core-webui";
import { Button, Dialog, DismissibleAlert, MetricCard, MetricGrid, i18nMessage } from "@govoplan/core-webui";
import type {
CampaignAttachmentPreviewFile,
@@ -10,7 +10,6 @@ import {
attachmentPreviewLinkableFiles,
attachmentPreviewMatchedFiles
} from "../utils/attachmentPreview";
import { WorkflowFact } from "./WorkflowNavigation";
export default function AttachmentLinkingPreview({
preview,
@@ -70,8 +69,8 @@ export default function AttachmentLinkingPreview({
</Button>
</div>
</div>
<div className="review-flow-fact-grid">
<WorkflowFact
<MetricGrid columns={4} density="compact" spacing="block" minimum="compact">
<MetricCard density="compact" surface="subtle"
label="i18n:govoplan-campaign.matched.1bf3ec5b"
value={
!loading && matchedFiles.length > 0 ? (
@@ -95,19 +94,19 @@ export default function AttachmentLinkingPreview({
)
}
/>
<WorkflowFact
<MetricCard density="compact" surface="subtle"
label="i18n:govoplan-campaign.linked.a089f600"
value={loading ? "..." : linkedCount || "—"}
/>
<WorkflowFact
<MetricCard density="compact" surface="subtle"
label="i18n:govoplan-campaign.need_link.fa4ab530"
value={loading ? "..." : unlinkedCount || "—"}
/>
<WorkflowFact
<MetricCard density="compact" surface="subtle"
label="i18n:govoplan-campaign.shared_source.7d4a1bf2"
value={loading ? "..." : preview?.shared_file_count ?? "—"}
/>
</div>
</MetricGrid>
{error && (
<DismissibleAlert tone="danger" compact dismissible={false}>
{error}
@@ -1,6 +1,7 @@
import { useState, type CSSProperties, type ReactNode } from "react";
import { ChevronDown, LockKeyhole, type LucideIcon } from "lucide-react";
import { type CSSProperties, type ReactNode } from "react";
import { LockKeyhole, type LucideIcon } from "lucide-react";
import {
Card,
InlineHelp,
StageRail,
i18nMessage,
@@ -107,7 +108,6 @@ export function WorkflowStage({
}) {
const Icon = stage.icon;
const locked = stage.state === "locked";
const [collapsed, setCollapsed] = useState(false);
const title = i18nMessage(stage.title);
const description = i18nMessage(stage.description);
const stateText = i18nMessage(stage.stateLabel);
@@ -127,11 +127,16 @@ export function WorkflowStage({
</div>
{nextState && <div className="review-flow-stage-line" />}
</div>
<article
className={`card card-collapsible review-flow-stage-card${locked ? " is-locked" : ""}${collapsed ? " is-collapsed" : ""}`}
<Card
as="article"
collapsible
persistCollapse={false}
className={`review-flow-stage-card${locked ? " is-locked" : ""}`}
headerClassName="review-flow-stage-header"
bodyClassName="review-flow-stage-content"
actionsClassName="review-flow-stage-header-actions"
aria-disabled={locked || undefined}
>
<header className="card-header review-flow-stage-header">
title={
<h2>
<span>{title}</span>
{locked && (
@@ -153,54 +158,22 @@ export function WorkflowStage({
)}
<InlineHelp>{description}</InlineHelp>
</h2>
<div className="review-flow-stage-header-actions">
<button
type="button"
className="card-collapse-toggle"
aria-expanded={!collapsed}
aria-label={
collapsed
? i18nMessage("i18n:govoplan-campaign.expand_value.be085ae4", { value0: title })
: i18nMessage("i18n:govoplan-campaign.collapse_value.29095640", { value0: title })
}
title={
collapsed
? "i18n:govoplan-campaign.show_content.0528d8d2"
: "i18n:govoplan-campaign.show_header_only.24afefca"
}
onClick={() => setCollapsed((value) => !value)}
>
<ChevronDown size={18} strokeWidth={2.4} aria-hidden="true" />
</button>
</div>
</header>
{!collapsed && (
<>
<div className="card-body review-flow-stage-content">{children}</div>
{locked && (
}
afterBody={locked ? (
<div className="review-flow-lock-message">
<span className="review-flow-lock-icon">
<LockKeyhole size={20} aria-hidden="true" />
</span>
<span>{lockReason}</span>
</div>
)}
</>
)}
</article>
) : undefined}
>
{children}
</Card>
</section>
);
}
export function WorkflowFact({ label, value }: { label: string; value: ReactNode }) {
return (
<div className="review-flow-fact">
<span>{label}</span>
<strong>{value}</strong>
</div>
);
}
export function stateLabel(state: FlowState): string {
switch (state) {
case "complete":