Adopt shared WebUI structural primitives

This commit is contained in:
2026-08-18 13:17:32 +02:00
parent e4663d545a
commit 139dc0cd75
2 changed files with 27 additions and 155 deletions
+25 -25
View File
@@ -19,11 +19,18 @@ import { ActionToolbar,
DocumentationHelpLink,
DismissibleAlert,
FieldLabel,
FilterBar,
FormLayout,
IconButton,
LoadingIndicator,
PageScrollViewport,
SelectionList,
SelectionListItem,
SelectionListItemContent,
StatePanel,
StatusBadge,
hasScope,
WorkspaceFrame,
type PlatformRouteContext
} from "@govoplan/core-webui";
import {
@@ -205,9 +212,9 @@ export default function ProjectsPage({ settings, auth }: PlatformRouteContext) {
return (
<main className="projects-page">
<div className="projects-shell">
<ActionToolbar className="projects-toolbar">
<form className="projects-search" onSubmit={submitSearch}>
<WorkspaceFrame className="projects-shell" label="Projects workspace" interfaceId="projects.workspace" helpContextId="projects.page.workspace" helpModuleId="projects">
<ActionToolbar surface="panel-header" className="projects-toolbar">
<FilterBar as="form" surface="control" wrap="never" width="default" className="projects-search" onSubmit={submitSearch}>
<Search size={17} aria-hidden="true" />
<input
value={query}
@@ -216,7 +223,7 @@ export default function ProjectsPage({ settings, auth }: PlatformRouteContext) {
placeholder="Search portfolios, projects, and milestones"
/>
<Button type="submit" variant="primary">Search</Button>
</form>
</FilterBar>
<label className="projects-kind-filter">
<span>Type</span>
<select value={kind} onChange={(event) => setKind(event.target.value as ProjectObjectKind | "")}>
@@ -246,35 +253,28 @@ export default function ProjectsPage({ settings, auth }: PlatformRouteContext) {
<PageScrollViewport className="projects-list-viewport">
{loading && <LoadingIndicator label="Loading projects" />}
{!loading && objects.length === 0 &&
<div className="projects-empty">No matching planning objects.</div>
<StatePanel size="compact" description="No matching planning objects." />
}
<div className="projects-list" role="list">
<SelectionList variant="navigation" label="Planning objects">
{objects.map((item) =>
<button
type="button"
role="listitem"
<SelectionListItem
key={objectKey(item)}
className={`project-row${objectKey(item) === selectedKey ? " is-selected" : ""}`}
selected={objectKey(item) === selectedKey}
onClick={() => setSelectedKey(objectKey(item))}>
<span className="project-row-icon">{kindIcon(item.object_kind)}</span>
<span className="project-row-main">
<strong>{item.title}</strong>
<small>{item.object_key}</small>
</span>
<SelectionListItemContent leading={kindIcon(item.object_kind)} title={item.title} description={`${item.object_key} · ${formatDate(item.due_at)}`} />
<StatusBadge status={statusTone(item.state)} label={humanize(item.state)} />
<span className="project-row-date">{formatDate(item.due_at)}</span>
</button>
</SelectionListItem>
)}
</div>
</SelectionList>
</PageScrollViewport>
<PageScrollViewport className="project-detail-viewport">
{selected ?
<ProjectDetail record={selected} canWrite={canWrite} onEdit={openEdit} /> :
<div className="projects-empty">Select a portfolio, project, or milestone.</div>
<StatePanel size="fill" title="Planning objects" description="Select a portfolio, project, or milestone." />
}
</PageScrollViewport>
</div>
</div>
</WorkspaceFrame>
<ProjectEditorDialog
open={editorOpen}
record={editing}
@@ -397,7 +397,7 @@ function ProjectEditorDialog({ open, record, objects, saving, error, onClose, on
</>
}>
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
<form id="project-editor-form" className="project-editor-form" onSubmit={submit}>
<FormLayout id="project-editor-form" columns={2} gap="compact" collapseAt="narrow" className="project-editor-form" onSubmit={submit}>
<label>
<FieldLabel>Type</FieldLabel>
<select
@@ -413,7 +413,7 @@ function ProjectEditorDialog({ open, record, objects, saving, error, onClose, on
</select>
</label>
{values.kind !== "portfolio" &&
<label className="project-editor-wide">
<label className="wide">
<FieldLabel>{values.kind === "milestone" ? "Parent project" : "Portfolio"}</FieldLabel>
<select
value={values.parentRef}
@@ -437,7 +437,7 @@ function ProjectEditorDialog({ open, record, objects, saving, error, onClose, on
<FieldLabel help="Stable identifier used in links and external mappings.">Key</FieldLabel>
<input value={values.key} disabled={Boolean(record)} required maxLength={120} onChange={(event) => set("key", event.target.value)} />
</label>
<label className="project-editor-wide">
<label className="wide">
<FieldLabel>Title</FieldLabel>
<input value={values.title} required maxLength={500} onChange={(event) => set("title", event.target.value)} />
</label>
@@ -462,7 +462,7 @@ function ProjectEditorDialog({ open, record, objects, saving, error, onClose, on
<FieldLabel>Due</FieldLabel>
<input type="date" value={values.dueAt} onChange={(event) => set("dueAt", event.target.value)} />
</label>
<label className="project-editor-wide">
<label className="wide">
<FieldLabel>Description</FieldLabel>
<textarea rows={5} value={values.description} onChange={(event) => set("description", event.target.value)} />
</label>
@@ -470,7 +470,7 @@ function ProjectEditorDialog({ open, record, objects, saving, error, onClose, on
<FieldLabel help="Recorded with the immutable project revision and lifecycle evidence.">Change reason</FieldLabel>
<input value={values.changeReason} required maxLength={1000} onChange={(event) => set("changeReason", event.target.value)} />
</label>
</form>
</FormLayout>
</Dialog>
);
}
+2 -130
View File
@@ -1,36 +1,11 @@
.projects-page,
.projects-shell {
.projects-page {
height: 100%;
min-height: 0;
overflow: hidden;
}
.projects-shell {
display: flex;
flex-direction: column;
background: var(--surface);
}
.projects-toolbar {
display: flex;
align-items: center;
gap: 12px;
min-height: 58px;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
background: var(--surface-raised);
}
.projects-search {
display: flex;
align-items: center;
gap: 8px;
width: min(560px, 48vw);
}
.projects-search input {
min-width: 160px;
flex: 1;
flex: 1 1 560px;
}
.projects-kind-filter {
@@ -67,69 +42,6 @@
background: var(--surface-subtle, var(--surface));
}
.projects-list {
overflow: hidden;
border: 1px solid var(--border);
border-radius: 6px;
background: var(--surface-raised);
}
.project-row {
display: grid;
grid-template-columns: 28px minmax(0, 1fr) auto minmax(96px, auto);
align-items: center;
gap: 10px;
width: 100%;
min-height: 62px;
padding: 9px 12px;
border: 0;
border-bottom: 1px solid var(--border);
background: transparent;
color: inherit;
text-align: left;
cursor: pointer;
}
.project-row:last-child {
border-bottom: 0;
}
.project-row:hover,
.project-row:focus-visible,
.project-row.is-selected {
background: var(--hover-bg);
}
.project-row.is-selected {
box-shadow: inset 3px 0 0 var(--accent);
}
.project-row-icon {
display: grid;
color: var(--text-soft);
place-items: center;
}
.project-row-main {
display: flex;
min-width: 0;
flex-direction: column;
gap: 2px;
}
.project-row-main strong,
.project-row-main small {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.project-row-main small,
.project-row-date {
color: var(--text-soft);
font-size: 0.78rem;
}
.project-detail {
width: min(100%, 980px);
margin: 0 auto;
@@ -228,22 +140,10 @@
font-size: 0.82rem;
}
.projects-empty {
padding: 36px 10px;
color: var(--text-soft);
text-align: center;
}
.project-editor-dialog {
width: min(720px, calc(100vw - 32px));
}
.project-editor-form {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.project-editor-form label {
display: flex;
min-width: 0;
@@ -256,20 +156,7 @@
font-size: 0.8rem;
}
.project-editor-wide {
grid-column: 1 / -1;
}
@media (max-width: 920px) {
.projects-toolbar {
align-items: stretch;
flex-wrap: wrap;
}
.projects-search {
width: 100%;
}
.projects-count {
margin-left: 0;
}
@@ -291,19 +178,4 @@
}
@media (max-width: 560px) {
.project-row {
grid-template-columns: 26px minmax(0, 1fr) auto;
}
.project-row-date {
display: none;
}
.project-editor-form {
grid-template-columns: 1fr;
}
.project-editor-wide {
grid-column: auto;
}
}