Adopt shared WebUI structural primitives
This commit is contained in:
@@ -19,11 +19,18 @@ import { ActionToolbar,
|
|||||||
DocumentationHelpLink,
|
DocumentationHelpLink,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
FieldLabel,
|
FieldLabel,
|
||||||
|
FilterBar,
|
||||||
|
FormLayout,
|
||||||
IconButton,
|
IconButton,
|
||||||
LoadingIndicator,
|
LoadingIndicator,
|
||||||
PageScrollViewport,
|
PageScrollViewport,
|
||||||
|
SelectionList,
|
||||||
|
SelectionListItem,
|
||||||
|
SelectionListItemContent,
|
||||||
|
StatePanel,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
hasScope,
|
hasScope,
|
||||||
|
WorkspaceFrame,
|
||||||
type PlatformRouteContext
|
type PlatformRouteContext
|
||||||
} from "@govoplan/core-webui";
|
} from "@govoplan/core-webui";
|
||||||
import {
|
import {
|
||||||
@@ -205,9 +212,9 @@ export default function ProjectsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="projects-page">
|
<main className="projects-page">
|
||||||
<div className="projects-shell">
|
<WorkspaceFrame className="projects-shell" label="Projects workspace" interfaceId="projects.workspace" helpContextId="projects.page.workspace" helpModuleId="projects">
|
||||||
<ActionToolbar className="projects-toolbar">
|
<ActionToolbar surface="panel-header" className="projects-toolbar">
|
||||||
<form className="projects-search" onSubmit={submitSearch}>
|
<FilterBar as="form" surface="control" wrap="never" width="default" className="projects-search" onSubmit={submitSearch}>
|
||||||
<Search size={17} aria-hidden="true" />
|
<Search size={17} aria-hidden="true" />
|
||||||
<input
|
<input
|
||||||
value={query}
|
value={query}
|
||||||
@@ -216,7 +223,7 @@ export default function ProjectsPage({ settings, auth }: PlatformRouteContext) {
|
|||||||
placeholder="Search portfolios, projects, and milestones"
|
placeholder="Search portfolios, projects, and milestones"
|
||||||
/>
|
/>
|
||||||
<Button type="submit" variant="primary">Search</Button>
|
<Button type="submit" variant="primary">Search</Button>
|
||||||
</form>
|
</FilterBar>
|
||||||
<label className="projects-kind-filter">
|
<label className="projects-kind-filter">
|
||||||
<span>Type</span>
|
<span>Type</span>
|
||||||
<select value={kind} onChange={(event) => setKind(event.target.value as ProjectObjectKind | "")}>
|
<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">
|
<PageScrollViewport className="projects-list-viewport">
|
||||||
{loading && <LoadingIndicator label="Loading projects" />}
|
{loading && <LoadingIndicator label="Loading projects" />}
|
||||||
{!loading && objects.length === 0 &&
|
{!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) =>
|
{objects.map((item) =>
|
||||||
<button
|
<SelectionListItem
|
||||||
type="button"
|
|
||||||
role="listitem"
|
|
||||||
key={objectKey(item)}
|
key={objectKey(item)}
|
||||||
className={`project-row${objectKey(item) === selectedKey ? " is-selected" : ""}`}
|
selected={objectKey(item) === selectedKey}
|
||||||
onClick={() => setSelectedKey(objectKey(item))}>
|
onClick={() => setSelectedKey(objectKey(item))}>
|
||||||
<span className="project-row-icon">{kindIcon(item.object_kind)}</span>
|
<SelectionListItemContent leading={kindIcon(item.object_kind)} title={item.title} description={`${item.object_key} · ${formatDate(item.due_at)}`} />
|
||||||
<span className="project-row-main">
|
|
||||||
<strong>{item.title}</strong>
|
|
||||||
<small>{item.object_key}</small>
|
|
||||||
</span>
|
|
||||||
<StatusBadge status={statusTone(item.state)} label={humanize(item.state)} />
|
<StatusBadge status={statusTone(item.state)} label={humanize(item.state)} />
|
||||||
<span className="project-row-date">{formatDate(item.due_at)}</span>
|
</SelectionListItem>
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</SelectionList>
|
||||||
</PageScrollViewport>
|
</PageScrollViewport>
|
||||||
<PageScrollViewport className="project-detail-viewport">
|
<PageScrollViewport className="project-detail-viewport">
|
||||||
{selected ?
|
{selected ?
|
||||||
<ProjectDetail record={selected} canWrite={canWrite} onEdit={openEdit} /> :
|
<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>
|
</PageScrollViewport>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</WorkspaceFrame>
|
||||||
<ProjectEditorDialog
|
<ProjectEditorDialog
|
||||||
open={editorOpen}
|
open={editorOpen}
|
||||||
record={editing}
|
record={editing}
|
||||||
@@ -397,7 +397,7 @@ function ProjectEditorDialog({ open, record, objects, saving, error, onClose, on
|
|||||||
</>
|
</>
|
||||||
}>
|
}>
|
||||||
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
|
{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>
|
<label>
|
||||||
<FieldLabel>Type</FieldLabel>
|
<FieldLabel>Type</FieldLabel>
|
||||||
<select
|
<select
|
||||||
@@ -413,7 +413,7 @@ function ProjectEditorDialog({ open, record, objects, saving, error, onClose, on
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
{values.kind !== "portfolio" &&
|
{values.kind !== "portfolio" &&
|
||||||
<label className="project-editor-wide">
|
<label className="wide">
|
||||||
<FieldLabel>{values.kind === "milestone" ? "Parent project" : "Portfolio"}</FieldLabel>
|
<FieldLabel>{values.kind === "milestone" ? "Parent project" : "Portfolio"}</FieldLabel>
|
||||||
<select
|
<select
|
||||||
value={values.parentRef}
|
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>
|
<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)} />
|
<input value={values.key} disabled={Boolean(record)} required maxLength={120} onChange={(event) => set("key", event.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label className="project-editor-wide">
|
<label className="wide">
|
||||||
<FieldLabel>Title</FieldLabel>
|
<FieldLabel>Title</FieldLabel>
|
||||||
<input value={values.title} required maxLength={500} onChange={(event) => set("title", event.target.value)} />
|
<input value={values.title} required maxLength={500} onChange={(event) => set("title", event.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
@@ -462,7 +462,7 @@ function ProjectEditorDialog({ open, record, objects, saving, error, onClose, on
|
|||||||
<FieldLabel>Due</FieldLabel>
|
<FieldLabel>Due</FieldLabel>
|
||||||
<input type="date" value={values.dueAt} onChange={(event) => set("dueAt", event.target.value)} />
|
<input type="date" value={values.dueAt} onChange={(event) => set("dueAt", event.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
<label className="project-editor-wide">
|
<label className="wide">
|
||||||
<FieldLabel>Description</FieldLabel>
|
<FieldLabel>Description</FieldLabel>
|
||||||
<textarea rows={5} value={values.description} onChange={(event) => set("description", event.target.value)} />
|
<textarea rows={5} value={values.description} onChange={(event) => set("description", event.target.value)} />
|
||||||
</label>
|
</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>
|
<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)} />
|
<input value={values.changeReason} required maxLength={1000} onChange={(event) => set("changeReason", event.target.value)} />
|
||||||
</label>
|
</label>
|
||||||
</form>
|
</FormLayout>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +1,11 @@
|
|||||||
.projects-page,
|
.projects-page {
|
||||||
.projects-shell {
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow: hidden;
|
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 {
|
.projects-search {
|
||||||
display: flex;
|
flex: 1 1 560px;
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
width: min(560px, 48vw);
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-search input {
|
|
||||||
min-width: 160px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects-kind-filter {
|
.projects-kind-filter {
|
||||||
@@ -67,69 +42,6 @@
|
|||||||
background: var(--surface-subtle, var(--surface));
|
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 {
|
.project-detail {
|
||||||
width: min(100%, 980px);
|
width: min(100%, 980px);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
@@ -228,22 +140,10 @@
|
|||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.projects-empty {
|
|
||||||
padding: 36px 10px;
|
|
||||||
color: var(--text-soft);
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.project-editor-dialog {
|
.project-editor-dialog {
|
||||||
width: min(720px, calc(100vw - 32px));
|
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 {
|
.project-editor-form label {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -256,20 +156,7 @@
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-editor-wide {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 920px) {
|
@media (max-width: 920px) {
|
||||||
.projects-toolbar {
|
|
||||||
align-items: stretch;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-search {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.projects-count {
|
.projects-count {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
@@ -291,19 +178,4 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 560px) {
|
@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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user