Adopt shared WebUI structural primitives

This commit is contained in:
2026-08-18 13:17:32 +02:00
parent b9c2d061e5
commit f4739efd86
2 changed files with 32 additions and 99 deletions
+32 -16
View File
@@ -13,6 +13,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { Link } from "react-router"; import { Link } from "react-router";
import { FormGrid, ToolbarGroup, import { FormGrid, ToolbarGroup,
ActionToolbar,
AdminIconButton, AdminIconButton,
Button, Button,
DateTimeField, DateTimeField,
@@ -20,10 +21,14 @@ import { FormGrid, ToolbarGroup,
DismissibleAlert, DismissibleAlert,
DocumentationHelpLink, DocumentationHelpLink,
FormField, FormField,
FilterBar,
SegmentedControl, SegmentedControl,
SelectionList, SelectionList,
SelectionListItem, SelectionListItem,
StatePanel,
StatusBadge, StatusBadge,
WorkspaceFrame,
WorkspaceLayout,
hasScope, hasScope,
type ApiSettings, type ApiSettings,
type AuthInfo type AuthInfo
@@ -120,10 +125,22 @@ export default function TasksPage({ settings, auth }: { settings: ApiSettings; a
} }
return ( return (
<main className="tasks-page" data-help-context-id="tasks.page.inbox"> <WorkspaceFrame as="main" height="viewport" surface="plain" className="tasks-page" label="Task inbox" data-help-context-id="tasks.page.inbox">
<div className="tasks-shell"> <WorkspaceLayout
<aside className="tasks-sidebar"> variant="split"
<div className="tasks-sidebar-bar"> primarySize="default"
primaryScrollable={false}
contentScrollable={false}
surface="contained"
primaryClassName="tasks-sidebar"
contentClassName="tasks-workspace"
primaryLabel="i18n:govoplan-tasks.work"
contentLabel="i18n:govoplan-tasks.work_details"
interfaceId="tasks.inbox.workspace"
helpContextId="tasks.page.inbox"
helpModuleId="tasks"
primary={<>
<ActionToolbar surface="panel-header" className="tasks-sidebar-bar">
<span className="tasks-title"><ListChecks size={18} /><strong>i18n:govoplan-tasks.work</strong></span> <span className="tasks-title"><ListChecks size={18} /><strong>i18n:govoplan-tasks.work</strong></span>
<ToolbarGroup align="end" className="tasks-toolbar-actions"> <ToolbarGroup align="end" className="tasks-toolbar-actions">
<AdminIconButton <AdminIconButton
@@ -142,8 +159,8 @@ export default function TasksPage({ settings, auth }: { settings: ApiSettings; a
/> />
) : null} ) : null}
</ToolbarGroup> </ToolbarGroup>
</div> </ActionToolbar>
<form className="tasks-search" onSubmit={submitSearch} role="search"> <FilterBar as="form" surface="control" wrap="never" className="tasks-search" onSubmit={submitSearch} role="search">
<Search size={15} aria-hidden="true" /> <Search size={15} aria-hidden="true" />
<input <input
value={searchDraft} value={searchDraft}
@@ -151,7 +168,7 @@ export default function TasksPage({ settings, auth }: { settings: ApiSettings; a
placeholder="i18n:govoplan-tasks.search_placeholder" placeholder="i18n:govoplan-tasks.search_placeholder"
aria-label="i18n:govoplan-tasks.search" aria-label="i18n:govoplan-tasks.search"
/> />
</form> </FilterBar>
<SegmentedControl <SegmentedControl
className="tasks-status-filter" className="tasks-status-filter"
options={[ options={[
@@ -168,7 +185,7 @@ export default function TasksPage({ settings, auth }: { settings: ApiSettings; a
{loading ? <p className="tasks-note">i18n:govoplan-tasks.loading</p> : null} {loading ? <p className="tasks-note">i18n:govoplan-tasks.loading</p> : null}
{!loading && items.length === 0 ? <p className="tasks-note">i18n:govoplan-tasks.empty</p> : null} {!loading && items.length === 0 ? <p className="tasks-note">i18n:govoplan-tasks.empty</p> : null}
{items.length ? ( {items.length ? (
<SelectionList label="i18n:govoplan-tasks.work_items"> <SelectionList variant="navigation" label="i18n:govoplan-tasks.work_items">
{items.map((item) => ( {items.map((item) => (
<SelectionListItem <SelectionListItem
key={`${item.provider_id}:${item.id}`} key={`${item.provider_id}:${item.id}`}
@@ -183,25 +200,24 @@ export default function TasksPage({ settings, auth }: { settings: ApiSettings; a
</SelectionList> </SelectionList>
) : null} ) : null}
</div> </div>
</aside> </>}
>
<section className="tasks-workspace" data-help-context-id="tasks.page.detail"> <ActionToolbar surface="panel-header" className="tasks-topbar" data-help-context-id="tasks.page.detail">
<div className="tasks-topbar">
<span className="tasks-detail-title"><ListChecks size={18} /><strong>{selected?.title ?? "i18n:govoplan-tasks.work_details"}</strong></span> <span className="tasks-detail-title"><ListChecks size={18} /><strong>{selected?.title ?? "i18n:govoplan-tasks.work_details"}</strong></span>
<ToolbarGroup align="end" className="tasks-toolbar-actions"> <ToolbarGroup align="end" className="tasks-toolbar-actions">
<DocumentationHelpLink reference={DOCUMENTATION} /> <DocumentationHelpLink reference={DOCUMENTATION} />
{selected?.provider_id === "tasks.explicit" && canWrite ? <TaskActions item={selected} busy={busy} onAction={(action) => action === "defer" ? setDeferOpen(true) : void runAction(action)} /> : null} {selected?.provider_id === "tasks.explicit" && canWrite ? <TaskActions item={selected} busy={busy} onAction={(action) => action === "defer" ? setDeferOpen(true) : void runAction(action)} /> : null}
</ToolbarGroup> </ToolbarGroup>
</div> </ActionToolbar>
{error ? <DismissibleAlert tone="danger" compact resetKey={error}>{error}</DismissibleAlert> : null} {error ? <DismissibleAlert tone="danger" compact resetKey={error}>{error}</DismissibleAlert> : null}
{diagnostics.map((message) => <DismissibleAlert key={message} tone="warning" compact resetKey={message}>{message}</DismissibleAlert>)} {diagnostics.map((message) => <DismissibleAlert key={message} tone="warning" compact resetKey={message}>{message}</DismissibleAlert>)}
{selected ? <TaskDetails item={selected} /> : ( {selected ? <TaskDetails item={selected} /> : (
<div className="tasks-empty-detail"><ListChecks size={24} /><h1>i18n:govoplan-tasks.work</h1><p>i18n:govoplan-tasks.select_help</p></div> <StatePanel size="fill" icon={<ListChecks size={24} />} title="i18n:govoplan-tasks.work" description="i18n:govoplan-tasks.select_help" />
)} )}
</section> </WorkspaceLayout>
</div>
<CreateTaskDialog <CreateTaskDialog
open={createOpen} open={createOpen}
@@ -230,7 +246,7 @@ export default function TasksPage({ settings, auth }: { settings: ApiSettings; a
<DateTimeField value={deferredUntil} onChange={setDeferredUntil} min={localDateTime(new Date())} /> <DateTimeField value={deferredUntil} onChange={setDeferredUntil} min={localDateTime(new Date())} />
</FormField> </FormField>
</Dialog> </Dialog>
</main> </WorkspaceFrame>
); );
} }
-83
View File
@@ -1,28 +1,3 @@
.tasks-page {
box-sizing: border-box;
height: calc(100vh - 115px);
min-height: 0;
overflow: hidden;
color: var(--text);
background: var(--bg);
}
.tasks-page *,
.tasks-page *::before,
.tasks-page *::after {
box-sizing: border-box;
}
.tasks-shell {
height: 100%;
min-height: 0;
display: grid;
grid-template-columns: minmax(285px, 350px) minmax(0, 1fr);
border: var(--border-line);
background: var(--panel);
overflow: hidden;
}
.tasks-sidebar, .tasks-sidebar,
.tasks-workspace { .tasks-workspace {
min-width: 0; min-width: 0;
@@ -32,13 +7,6 @@
overflow: hidden; overflow: hidden;
} }
.tasks-sidebar {
border-right: var(--border-line);
background: var(--panel-soft);
}
.tasks-sidebar-bar,
.tasks-topbar,
.tasks-title, .tasks-title,
.tasks-detail-title, .tasks-detail-title,
.tasks-toolbar-actions, .tasks-toolbar-actions,
@@ -51,15 +19,6 @@
gap: 8px; gap: 8px;
} }
.tasks-sidebar-bar,
.tasks-topbar {
min-height: 54px;
justify-content: space-between;
border-bottom: var(--border-line);
background: var(--panel-header);
padding: 9px 12px;
}
.tasks-detail-title { .tasks-detail-title {
min-width: 0; min-width: 0;
} }
@@ -82,19 +41,7 @@
} }
.tasks-search { .tasks-search {
height: 36px;
display: flex;
align-items: center;
gap: 7px;
border: var(--border-line);
border-radius: 5px;
background: var(--surface);
margin: 8px 8px 0; margin: 8px 8px 0;
padding: 0 9px;
}
.tasks-search:focus-within {
border-color: var(--accent);
} }
.tasks-search input { .tasks-search input {
@@ -232,22 +179,6 @@
color: var(--muted); color: var(--muted);
} }
.tasks-empty-detail {
min-height: 100%;
display: grid;
place-content: center;
justify-items: center;
color: var(--muted);
text-align: center;
padding: 24px;
}
.tasks-empty-detail h1 {
margin: 10px 0 0;
color: var(--text-strong);
font-size: 20px;
}
.tasks-create-form { .tasks-create-form {
width: min(620px, 75vw); width: min(620px, 75vw);
display: grid; display: grid;
@@ -275,10 +206,6 @@
} }
@media (max-width: 820px) { @media (max-width: 820px) {
.tasks-shell {
grid-template-columns: minmax(230px, 42%) minmax(0, 1fr);
}
.tasks-topbar { .tasks-topbar {
align-items: flex-start; align-items: flex-start;
} }
@@ -289,16 +216,6 @@
} }
@media (max-width: 620px) { @media (max-width: 620px) {
.tasks-shell {
grid-template-columns: 1fr;
grid-template-rows: minmax(250px, 44%) minmax(0, 1fr);
}
.tasks-sidebar {
border-right: 0;
border-bottom: var(--border-line);
}
.tasks-create-form { .tasks-create-form {
width: min(100%, 88vw); width: min(100%, 88vw);
} }