Adopt shared WebUI layout primitives

This commit is contained in:
2026-08-18 10:42:54 +02:00
parent 21ed6270a3
commit cf4205f780
@@ -10,7 +10,7 @@ import {
X X
} from "lucide-react"; } from "lucide-react";
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { import { DialogSection, ToolbarGroup, ActionToolbar,
ApiError, ApiError,
ActionBlockerHint, ActionBlockerHint,
Button, Button,
@@ -296,10 +296,10 @@ export default function TemplatesPage({ settings, auth }: Props) {
<main className="templates-page"> <main className="templates-page">
<div className="templates-shell"> <div className="templates-shell">
<aside className="templates-sidebar"> <aside className="templates-sidebar">
<div className="templates-sidebar-toolbar"> <ActionToolbar className="templates-sidebar-toolbar">
<strong>Template library</strong> <strong>Template library</strong>
<IconButton label="Add template" icon={<Plus size={17} />} variant="primary" disabled={!canWrite} disabledReason={!canWrite ? TEMPLATES_I18N.writeReason : undefined} onClick={() => requestDiscard(() => setCreateOpen(true))} /> <IconButton label="Add template" icon={<Plus size={17} />} variant="primary" disabled={!canWrite} disabledReason={!canWrite ? TEMPLATES_I18N.writeReason : undefined} onClick={() => requestDiscard(() => setCreateOpen(true))} />
</div> </ActionToolbar>
<div className="templates-search"><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search templates" /></div> <div className="templates-search"><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search templates" /></div>
<div className="templates-list"> <div className="templates-list">
{visibleItems.map((item) => ( {visibleItems.map((item) => (
@@ -324,12 +324,12 @@ export default function TemplatesPage({ settings, auth }: Props) {
</aside> </aside>
<section className="templates-workspace"> <section className="templates-workspace">
<header className="templates-workspace-toolbar"> <ActionToolbar className="templates-workspace-toolbar">
<span className="templates-current-title"> <span className="templates-current-title">
<strong>{selected?.name ?? "Select a template"}</strong> <strong>{selected?.name ?? "Select a template"}</strong>
<small>{selected ? `${typeLabel(selected.template_type)} · ${selected.revision.locale}` : ""}</small> <small>{selected ? `${typeLabel(selected.template_type)} · ${selected.revision.locale}` : ""}</small>
</span> </span>
<div className="templates-toolbar-actions"> <ToolbarGroup align="end" className="templates-toolbar-actions">
<DocumentationHelpLink reference={TEMPLATES_DOCUMENTATION} /> <DocumentationHelpLink reference={TEMPLATES_DOCUMENTATION} />
<IconButton label="Discard and reload" icon={<RefreshCw size={17} />} disabled={loading || busy} disabledReason={loading ? TEMPLATES_I18N.loading : busy ? TEMPLATES_I18N.busy : undefined} onClick={() => requestDiscard(() => void reload(selectedId))} /> <IconButton label="Discard and reload" icon={<RefreshCw size={17} />} disabled={loading || busy} disabledReason={loading ? TEMPLATES_I18N.loading : busy ? TEMPLATES_I18N.busy : undefined} onClick={() => requestDiscard(() => void reload(selectedId))} />
<Button variant="primary" disabled={!selected || readOnly || !dirty || busy} disabledReason={busy ? TEMPLATES_I18N.busy : !selected ? TEMPLATES_I18N.noSelection : readOnly ? (canWrite ? TEMPLATES_I18N.readOnlyReason : TEMPLATES_I18N.writeReason) : !dirty ? TEMPLATES_I18N.noChanges : undefined} onClick={() => void save()}><Save size={16} /> Save revision</Button> <Button variant="primary" disabled={!selected || readOnly || !dirty || busy} disabledReason={busy ? TEMPLATES_I18N.busy : !selected ? TEMPLATES_I18N.noSelection : readOnly ? (canWrite ? TEMPLATES_I18N.readOnlyReason : TEMPLATES_I18N.writeReason) : !dirty ? TEMPLATES_I18N.noChanges : undefined} onClick={() => void save()}><Save size={16} /> Save revision</Button>
@@ -341,8 +341,8 @@ export default function TemplatesPage({ settings, auth }: Props) {
options={[{ id: "definition", label: "Definition" }, { id: "preview", label: "Preview" }]} options={[{ id: "definition", label: "Definition" }, { id: "preview", label: "Preview" }]}
ariaLabel="Template workspace" ariaLabel="Template workspace"
/> />
</div> </ToolbarGroup>
</header> </ActionToolbar>
<div className="templates-alerts"> <div className="templates-alerts">
{error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>} {error && <DismissibleAlert tone="danger" resetKey={error}>{error}</DismissibleAlert>}
@@ -392,10 +392,10 @@ export default function TemplatesPage({ settings, auth }: Props) {
</div> </div>
<Dialog open={createOpen} title="Add template" onClose={closeCreate} closeDisabled={busy} footer={<><Button onClick={closeCreate} disabled={busy} disabledReason={busy ? TEMPLATES_I18N.busy : undefined}>Cancel</Button><Button variant="primary" disabled={!createName.trim() || busy} disabledReason={busy ? TEMPLATES_I18N.busy : !createName.trim() ? TEMPLATES_I18N.incomplete : undefined} onClick={() => void create()}>Create</Button></>}> <Dialog open={createOpen} title="Add template" onClose={closeCreate} closeDisabled={busy} footer={<><Button onClick={closeCreate} disabled={busy} disabledReason={busy ? TEMPLATES_I18N.busy : undefined}>Cancel</Button><Button variant="primary" disabled={!createName.trim() || busy} disabledReason={busy ? TEMPLATES_I18N.busy : !createName.trim() ? TEMPLATES_I18N.incomplete : undefined} onClick={() => void create()}>Create</Button></>}>
<div className="templates-dialog-form"> <DialogSection className="templates-dialog-form">
<FormField label="Name" documentation={TEMPLATE_FIELDS_DOCUMENTATION}><input autoFocus value={createName} onChange={(event) => setCreateName(event.target.value)} /></FormField> <FormField label="Name" documentation={TEMPLATE_FIELDS_DOCUMENTATION}><input autoFocus value={createName} onChange={(event) => setCreateName(event.target.value)} /></FormField>
<FormField label="Type" documentation={TEMPLATE_FIELDS_DOCUMENTATION}><select value={createType} onChange={(event) => setCreateType(event.target.value as TemplateType)}>{TEMPLATE_TYPES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</select></FormField> <FormField label="Type" documentation={TEMPLATE_FIELDS_DOCUMENTATION}><select value={createType} onChange={(event) => setCreateType(event.target.value as TemplateType)}>{TEMPLATE_TYPES.map((item) => <option key={item.value} value={item.value}>{item.label}</option>)}</select></FormField>
</div> </DialogSection>
</Dialog> </Dialog>
<ConfirmDialog open={publishOpen} title="i18n:govoplan-templates.publish_title" message="i18n:govoplan-templates.publish_message" confirmLabel="Publish" busy={busy} onCancel={() => setPublishOpen(false)} onConfirm={() => void publish()} /> <ConfirmDialog open={publishOpen} title="i18n:govoplan-templates.publish_title" message="i18n:govoplan-templates.publish_message" confirmLabel="Publish" busy={busy} onCancel={() => setPublishOpen(false)} onConfirm={() => void publish()} />
<ConfirmDialog open={finalRenderOpen} title="i18n:govoplan-templates.render_title" message="i18n:govoplan-templates.render_message" confirmLabel="Render final output" busy={busy} onCancel={() => setFinalRenderOpen(false)} onConfirm={() => void runRender(true)} /> <ConfirmDialog open={finalRenderOpen} title="i18n:govoplan-templates.render_title" message="i18n:govoplan-templates.render_message" confirmLabel="Render final output" busy={busy} onCancel={() => setFinalRenderOpen(false)} onConfirm={() => void runRender(true)} />