From 3856765520d59cebfb2b934dec5dd22b76051bf7 Mon Sep 17 00:00:00 2001 From: Albrecht Degering Date: Tue, 18 Aug 2026 13:17:32 +0200 Subject: [PATCH] Adopt shared WebUI structural primitives --- .../src/features/templates/TemplatesPage.tsx | 103 ++++++++++-------- webui/src/styles/templates.css | 58 +--------- 2 files changed, 59 insertions(+), 102 deletions(-) diff --git a/webui/src/features/templates/TemplatesPage.tsx b/webui/src/features/templates/TemplatesPage.tsx index 2d339a4..5e97cba 100644 --- a/webui/src/features/templates/TemplatesPage.tsx +++ b/webui/src/features/templates/TemplatesPage.tsx @@ -15,15 +15,23 @@ import { DialogSection, ToolbarGroup, ActionToolbar, ActionBlockerHint, Button, ConfirmDialog, + ContentSection, Dialog, DocumentationHelpLink, DismissibleAlert, + FilterBar, FormField, IconButton, LoadingFrame, SegmentedControl, + SelectionList, + SelectionListItem, + SelectionListItemContent, + StatePanel, StatusBadge, ToggleSwitch, + WorkspaceFrame, + WorkspaceLayout, formatDateTime, hasScope, useUnsavedChanges, @@ -293,20 +301,27 @@ export default function TemplatesPage({ settings, auth }: Props) { }; return ( -
-
- - -
- + {!visibleItems.length && } + + } + > + {selected?.name ?? "Select a template"} {selected ? `${typeLabel(selected.template_type)} · ${selected.revision.locale}` : ""} @@ -363,7 +377,7 @@ export default function TemplatesPage({ settings, auth }: Props) {
- {!selected ?

Create or select a reusable template.

: view === "definition" ? <> + {!selected ? : view === "definition" ? <> : <> @@ -388,8 +402,7 @@ export default function TemplatesPage({ settings, auth }: Props) { }
-
-
+ }> @@ -400,13 +413,13 @@ export default function TemplatesPage({ settings, auth }: Props) { setPublishOpen(false)} onConfirm={() => void publish()} /> setFinalRenderOpen(false)} onConfirm={() => void runRender(true)} /> setDeleteOpen(false)} onConfirm={() => void remove()} /> -
+ ); } function RevisionHistory({ revisions, currentRevisionId, publishedRevisionId }: { revisions: TemplateRevision[]; currentRevisionId: string; publishedRevisionId: string | null }) { - return
-
Revision history{revisions.length} immutable revision(s)
+ return + Revision history{revisions.length} immutable revision(s)
{revisions.map((revision) =>
Revision {revision.revision}{formatDateTime(revision.created_at)} · {shortHash(revision.definition_hash)} @@ -415,22 +428,22 @@ function RevisionHistory({ revisions, currentRevisionId, publishedRevisionId }: {revision.id === publishedRevisionId && }
)} - {!revisions.length &&

No revision evidence is available.

} + {!revisions.length && }
-
; + ; } function RenderHistory({ renders, settings, onError }: { renders: TemplateRender[]; settings: ApiSettings; onError: (message: string) => void }) { - return
-
Output history{renders.length} recent render(s)
+ return + Output history{renders.length} recent render(s)
{renders.map((item) =>
{item.filename}{item.generated_at ? formatDateTime(item.generated_at) : "Generated"} · {item.item_count} item(s) · {shortHash(item.output_sha256)}
)} - {!renders.length &&

No output has been rendered for this template.

} + {!renders.length && }
-
; + ; } function DefinitionEditor({ draft, disabled, auth, onChange }: { draft: TemplatePayload; disabled: boolean; auth: AuthInfo; onChange: (draft: TemplatePayload) => void }) { @@ -453,8 +466,8 @@ function DefinitionEditor({ draft, disabled, auth, onChange }: { draft: Template update("description", event.target.value || null)} /> -
-
Required data contract
+ + Required data contract
{draft.required_fields.map((field, index) => (
@@ -465,12 +478,12 @@ function DefinitionEditor({ draft, disabled, auth, onChange }: { draft: Template } variant="ghost" disabled={disabled} onClick={() => update("required_fields", draft.required_fields.filter((_, fieldIndex) => fieldIndex !== index))} />
))} - {!draft.required_fields.length &&

No required fields. Tokens still resolve from supplied parameters and items.

} + {!draft.required_fields.length && }
-
+ -
-
Page and media
+ + Page and media
update("layout", { ...layout, margin_mm: Number(event.target.value) })} /> @@ -480,12 +493,12 @@ function DefinitionEditor({ draft, disabled, auth, onChange }: { draft: Template update("layout", { ...layout, gap_mm: Number(event.target.value) })} /> }
-
+ -
-
Template bodyUse tokens such as {"{{name}}"} or {"{{recipient.address}}"}.
+ + Template bodyUse tokens such as {"{{name}}"} or {"{{recipient.address}}"}. update("content_html", value || null)} minHeight={300} /> -
+ ); } @@ -509,8 +522,8 @@ function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, c }) { return (
-
-
Validated sample inputPreview and final output use the same pinned revision and canonical input.
+ + Validated sample inputPreview and final output use the same pinned revision and canonical input.
@@ -521,7 +534,7 @@ function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, c
-
+ {compatibility && {compatibility.compatible @@ -529,8 +542,8 @@ function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, c : compatibility.diagnostics.map((item) => String(item.message ?? item.code ?? "Incompatible input")).join(" ")} } - {render &&
-
Render evidence
+ {render && + Render evidence
Revision
{render.revision} · {shortHash(render.template_hash)}
Input
{shortHash(render.input_hash)}
@@ -540,7 +553,7 @@ function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, c
Generated
{render.generated_at ? formatDateTime(render.generated_at) : "Now"}

{render.artifact?.kind === "managed_file" ? "Managed by Files" : "Bounded Templates download"} · {render.output_size_bytes.toLocaleString()} bytes

-
} + }
); } diff --git a/webui/src/styles/templates.css b/webui/src/styles/templates.css index 9de7de9..441bccc 100644 --- a/webui/src/styles/templates.css +++ b/webui/src/styles/templates.css @@ -1,71 +1,19 @@ -.templates-page { - height: calc(100vh - 115px); - min-width: 0; - min-height: 0; - padding: 0; - overflow: hidden; - color: var(--text); - background: var(--bg); -} - -.templates-page *, .templates-page *::before, .templates-page *::after { box-sizing: border-box; } - -.templates-shell { - display: grid; - grid-template-columns: minmax(250px, 300px) minmax(0, 1fr); - width: 100%; - height: 100%; - min-width: 0; - min-height: 0; - overflow: hidden; - border: var(--border-line); - background: var(--panel); -} - -.templates-sidebar, .templates-workspace { min-width: 0; min-height: 0; } -.templates-sidebar { display: flex; flex-direction: column; overflow: hidden; border-right: var(--border-line); background: var(--panel-soft); } -.templates-workspace { display: flex; flex-direction: column; overflow: hidden; background: var(--bg); } - -.templates-sidebar-toolbar, .templates-workspace-toolbar, .templates-section-heading { - display: flex; - align-items: center; - justify-content: space-between; - gap: 10px; - flex: 0 0 auto; - border-bottom: var(--border-line); - background: var(--panel-header); -} - -.templates-sidebar-toolbar { min-height: 52px; padding: 8px 10px 8px 14px; } -.templates-workspace-toolbar { min-height: 58px; padding: 8px 10px 8px 14px; } +.templates-workspace-toolbar { min-width: 0; } .templates-toolbar-actions { display: flex; align-items: center; gap: 7px; flex: 0 0 auto; } .templates-toolbar-actions .btn { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; } .templates-current-title { min-width: 0; flex: 1 1 auto; } .templates-current-title strong, .templates-current-title small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .templates-current-title small { margin-top: 3px; color: var(--muted); font-size: 11px; } -.templates-search { padding: 9px; border-bottom: var(--border-line); background: var(--panel); } -.templates-search input { width: 100%; min-height: 34px; padding: 7px 9px; } -.templates-list { flex: 1 1 auto; min-height: 0; overflow: auto; padding: 6px; } -.templates-list > button { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 8px; width: 100%; min-height: 56px; padding: 8px 9px; border: 0; border-radius: var(--radius-sm); color: var(--text); background: transparent; cursor: pointer; text-align: left; } -.templates-list > button:hover, .templates-list > button:focus-visible { background: var(--primary-soft); outline: 0; } -.templates-list > button.is-selected { background: var(--primary-soft-strong); box-shadow: inset 3px 0 0 var(--accent); } -.templates-list strong, .templates-list small { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.templates-list small { margin-top: 3px; color: var(--muted); font-size: 11px; } - .templates-alerts { flex: 0 0 auto; padding: 0 12px; } .templates-alerts:empty { display: none; } .templates-alerts .alert { margin: 10px 0 0; } .templates-workspace > .loading-frame { flex: 1 1 auto; min-height: 0; } .templates-content { height: 100%; min-width: 0; min-height: 0; overflow: auto; padding: 14px; } -.templates-empty, .templates-inline-empty { display: grid; place-items: center; min-height: 90px; padding: 16px; color: var(--muted); font-size: 13px; text-align: center; } - .templates-definition-fields { display: grid; grid-template-columns: minmax(220px, 1.4fr) repeat(3, minmax(130px, .7fr)); gap: 12px; margin-bottom: 14px; } .templates-definition-fields .form-field:nth-child(5) { grid-column: span 2; } .templates-definition-fields input, .templates-definition-fields select, .templates-dialog-form input, .templates-dialog-form select, .templates-layout-fields input, .templates-layout-fields select, .templates-preview-controls select { width: 100%; } -.templates-section { min-width: 0; margin-bottom: 14px; border: var(--border-line); background: var(--panel); } -.templates-section-heading { min-height: 44px; padding: 7px 10px; } .templates-section-heading small { color: var(--muted); font-weight: 400; } .templates-section-heading .btn { display: inline-flex; align-items: center; gap: 6px; } .templates-fields-table { overflow: auto; padding: 8px; } @@ -94,7 +42,6 @@ .templates-dialog-form { display: grid; grid-template-columns: minmax(220px, 1fr) minmax(180px, .7fr); gap: 12px; min-width: min(560px, 80vw); } @media (max-width: 980px) { - .templates-shell { grid-template-columns: minmax(210px, 250px) minmax(0, 1fr); } .templates-definition-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); } .templates-definition-fields .form-field:nth-child(5) { grid-column: auto; } .templates-layout-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); } @@ -104,9 +51,6 @@ @media (max-width: 720px) { .templates-page { height: auto; min-height: calc(100vh - 100px); overflow: visible; } - .templates-shell { display: flex; flex-direction: column; height: auto; overflow: visible; } - .templates-sidebar { max-height: 280px; border-right: 0; border-bottom: var(--border-line); } - .templates-workspace { overflow: visible; } .templates-workspace-toolbar { align-items: flex-start; flex-wrap: wrap; } .templates-toolbar-actions { flex-wrap: wrap; } .templates-content { height: auto; overflow: visible; }