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) {
>}
-
-
+
+
);
}
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; }