Adopt shared WebUI structural primitives
This commit is contained in:
@@ -15,15 +15,23 @@ import { DialogSection, ToolbarGroup, ActionToolbar,
|
|||||||
ActionBlockerHint,
|
ActionBlockerHint,
|
||||||
Button,
|
Button,
|
||||||
ConfirmDialog,
|
ConfirmDialog,
|
||||||
|
ContentSection,
|
||||||
Dialog,
|
Dialog,
|
||||||
DocumentationHelpLink,
|
DocumentationHelpLink,
|
||||||
DismissibleAlert,
|
DismissibleAlert,
|
||||||
|
FilterBar,
|
||||||
FormField,
|
FormField,
|
||||||
IconButton,
|
IconButton,
|
||||||
LoadingFrame,
|
LoadingFrame,
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
|
SelectionList,
|
||||||
|
SelectionListItem,
|
||||||
|
SelectionListItemContent,
|
||||||
|
StatePanel,
|
||||||
StatusBadge,
|
StatusBadge,
|
||||||
ToggleSwitch,
|
ToggleSwitch,
|
||||||
|
WorkspaceFrame,
|
||||||
|
WorkspaceLayout,
|
||||||
formatDateTime,
|
formatDateTime,
|
||||||
hasScope,
|
hasScope,
|
||||||
useUnsavedChanges,
|
useUnsavedChanges,
|
||||||
@@ -293,20 +301,27 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="templates-page">
|
<WorkspaceFrame as="main" height="viewport" surface="plain" className="templates-page" label="Template workspace">
|
||||||
<div className="templates-shell">
|
<WorkspaceLayout
|
||||||
<aside className="templates-sidebar">
|
variant="split"
|
||||||
<ActionToolbar className="templates-sidebar-toolbar">
|
primarySize="compact"
|
||||||
|
surface="contained"
|
||||||
|
primaryScrollable={false}
|
||||||
|
contentScrollable={false}
|
||||||
|
primaryLabel="Template library"
|
||||||
|
contentLabel="Template workspace"
|
||||||
|
contentClassName="templates-workspace"
|
||||||
|
primary={<>
|
||||||
|
<ActionToolbar surface="panel-header">
|
||||||
<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))} />
|
||||||
</ActionToolbar>
|
</ActionToolbar>
|
||||||
<div className="templates-search"><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search templates" /></div>
|
<FilterBar surface="panel"><input value={search} onChange={(event) => setSearch(event.target.value)} placeholder="Search templates" /></FilterBar>
|
||||||
<div className="templates-list">
|
<SelectionList variant="navigation" label="Templates">
|
||||||
{visibleItems.map((item) => (
|
{visibleItems.map((item) => (
|
||||||
<button
|
<SelectionListItem
|
||||||
key={item.id}
|
key={item.id}
|
||||||
type="button"
|
selected={item.id === selectedId}
|
||||||
className={item.id === selectedId ? "is-selected" : ""}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item.id === selectedId) return;
|
if (item.id === selectedId) return;
|
||||||
requestDiscard(() => {
|
requestDiscard(() => {
|
||||||
@@ -315,16 +330,15 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span><strong>{item.name}</strong><small>{typeLabel(item.template_type)} · revision {item.current_revision}</small></span>
|
<SelectionListItemContent title={item.name} description={`${typeLabel(item.template_type)} · revision ${item.current_revision}`} />
|
||||||
<StatusBadge status={item.status} label={item.status} />
|
<StatusBadge status={item.status} label={item.status} />
|
||||||
</button>
|
</SelectionListItem>
|
||||||
))}
|
))}
|
||||||
{!visibleItems.length && <p className="templates-empty">No matching templates.</p>}
|
{!visibleItems.length && <StatePanel size="compact" description="No matching templates." />}
|
||||||
</div>
|
</SelectionList>
|
||||||
</aside>
|
</>}
|
||||||
|
>
|
||||||
<section className="templates-workspace">
|
<ActionToolbar surface="panel-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>
|
||||||
@@ -363,7 +377,7 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
|
|
||||||
<LoadingFrame loading={loading} label="Loading templates">
|
<LoadingFrame loading={loading} label="Loading templates">
|
||||||
<div className="templates-content">
|
<div className="templates-content">
|
||||||
{!selected ? <p className="templates-empty">Create or select a reusable template.</p> : view === "definition" ? <>
|
{!selected ? <StatePanel size="fill" title="Templates" description="Create or select a reusable template." /> : view === "definition" ? <>
|
||||||
<DefinitionEditor draft={draft} disabled={readOnly || busy} auth={auth} onChange={setDraft} />
|
<DefinitionEditor draft={draft} disabled={readOnly || busy} auth={auth} onChange={setDraft} />
|
||||||
<RevisionHistory revisions={revisions} currentRevisionId={selected.current_revision_id} publishedRevisionId={selected.published_revision_id ?? null} />
|
<RevisionHistory revisions={revisions} currentRevisionId={selected.current_revision_id} publishedRevisionId={selected.published_revision_id ?? null} />
|
||||||
</> : <>
|
</> : <>
|
||||||
@@ -388,8 +402,7 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
</LoadingFrame>
|
</LoadingFrame>
|
||||||
</section>
|
</WorkspaceLayout>
|
||||||
</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></>}>
|
||||||
<DialogSection className="templates-dialog-form">
|
<DialogSection className="templates-dialog-form">
|
||||||
@@ -400,13 +413,13 @@ export default function TemplatesPage({ settings, auth }: Props) {
|
|||||||
<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)} />
|
||||||
<ConfirmDialog open={deleteOpen} title="Delete template?" message="Existing render evidence remains until module retention removes it. Consumers can no longer select this template." confirmLabel="Delete" tone="danger" busy={busy} onCancel={() => setDeleteOpen(false)} onConfirm={() => void remove()} />
|
<ConfirmDialog open={deleteOpen} title="Delete template?" message="Existing render evidence remains until module retention removes it. Consumers can no longer select this template." confirmLabel="Delete" tone="danger" busy={busy} onCancel={() => setDeleteOpen(false)} onConfirm={() => void remove()} />
|
||||||
</main>
|
</WorkspaceFrame>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function RevisionHistory({ revisions, currentRevisionId, publishedRevisionId }: { revisions: TemplateRevision[]; currentRevisionId: string; publishedRevisionId: string | null }) {
|
function RevisionHistory({ revisions, currentRevisionId, publishedRevisionId }: { revisions: TemplateRevision[]; currentRevisionId: string; publishedRevisionId: string | null }) {
|
||||||
return <section className="templates-section templates-history">
|
return <ContentSection className="templates-history">
|
||||||
<div className="templates-section-heading"><strong>Revision history</strong><small>{revisions.length} immutable revision(s)</small></div>
|
<ActionToolbar surface="section-header" className="templates-section-heading"><strong>Revision history</strong><small>{revisions.length} immutable revision(s)</small></ActionToolbar>
|
||||||
<div className="templates-history-list">
|
<div className="templates-history-list">
|
||||||
{revisions.map((revision) => <div key={revision.id}>
|
{revisions.map((revision) => <div key={revision.id}>
|
||||||
<span><strong>Revision {revision.revision}</strong><small>{formatDateTime(revision.created_at)} · {shortHash(revision.definition_hash)}</small></span>
|
<span><strong>Revision {revision.revision}</strong><small>{formatDateTime(revision.created_at)} · {shortHash(revision.definition_hash)}</small></span>
|
||||||
@@ -415,22 +428,22 @@ function RevisionHistory({ revisions, currentRevisionId, publishedRevisionId }:
|
|||||||
{revision.id === publishedRevisionId && <StatusBadge status="active" label="Published" />}
|
{revision.id === publishedRevisionId && <StatusBadge status="active" label="Published" />}
|
||||||
</span>
|
</span>
|
||||||
</div>)}
|
</div>)}
|
||||||
{!revisions.length && <p className="templates-inline-empty">No revision evidence is available.</p>}
|
{!revisions.length && <StatePanel size="inline" description="No revision evidence is available." />}
|
||||||
</div>
|
</div>
|
||||||
</section>;
|
</ContentSection>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function RenderHistory({ renders, settings, onError }: { renders: TemplateRender[]; settings: ApiSettings; onError: (message: string) => void }) {
|
function RenderHistory({ renders, settings, onError }: { renders: TemplateRender[]; settings: ApiSettings; onError: (message: string) => void }) {
|
||||||
return <section className="templates-section templates-history">
|
return <ContentSection className="templates-history">
|
||||||
<div className="templates-section-heading"><strong>Output history</strong><small>{renders.length} recent render(s)</small></div>
|
<ActionToolbar surface="section-header" className="templates-section-heading"><strong>Output history</strong><small>{renders.length} recent render(s)</small></ActionToolbar>
|
||||||
<div className="templates-history-list">
|
<div className="templates-history-list">
|
||||||
{renders.map((item) => <div key={item.render_id}>
|
{renders.map((item) => <div key={item.render_id}>
|
||||||
<span><strong>{item.filename}</strong><small>{item.generated_at ? formatDateTime(item.generated_at) : "Generated"} · {item.item_count} item(s) · {shortHash(item.output_sha256)}</small></span>
|
<span><strong>{item.filename}</strong><small>{item.generated_at ? formatDateTime(item.generated_at) : "Generated"} · {item.item_count} item(s) · {shortHash(item.output_sha256)}</small></span>
|
||||||
<Button disabled={!item.artifact?.download_path} onClick={() => void downloadTemplateRender(settings, item).catch((caught) => onError(errorMessage(caught)))}><Download size={15} /> Download</Button>
|
<Button disabled={!item.artifact?.download_path} onClick={() => void downloadTemplateRender(settings, item).catch((caught) => onError(errorMessage(caught)))}><Download size={15} /> Download</Button>
|
||||||
</div>)}
|
</div>)}
|
||||||
{!renders.length && <p className="templates-inline-empty">No output has been rendered for this template.</p>}
|
{!renders.length && <StatePanel size="inline" description="No output has been rendered for this template." />}
|
||||||
</div>
|
</div>
|
||||||
</section>;
|
</ContentSection>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function DefinitionEditor({ draft, disabled, auth, onChange }: { draft: TemplatePayload; disabled: boolean; auth: AuthInfo; onChange: (draft: TemplatePayload) => void }) {
|
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
|
|||||||
<FormField label="Description"><input disabled={disabled} value={draft.description ?? ""} onChange={(event) => update("description", event.target.value || null)} /></FormField>
|
<FormField label="Description"><input disabled={disabled} value={draft.description ?? ""} onChange={(event) => update("description", event.target.value || null)} /></FormField>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section className="templates-section">
|
<ContentSection>
|
||||||
<div className="templates-section-heading"><strong>Required data contract</strong><Button disabled={disabled} onClick={() => update("required_fields", [...draft.required_fields, emptyField()])}><Plus size={15} /> Add field</Button></div>
|
<ActionToolbar surface="section-header" className="templates-section-heading"><strong>Required data contract</strong><Button disabled={disabled} onClick={() => update("required_fields", [...draft.required_fields, emptyField()])}><Plus size={15} /> Add field</Button></ActionToolbar>
|
||||||
<div className="templates-fields-table">
|
<div className="templates-fields-table">
|
||||||
{draft.required_fields.map((field, index) => (
|
{draft.required_fields.map((field, index) => (
|
||||||
<div className="templates-field-row" key={`${index}:${field.path}`}>
|
<div className="templates-field-row" key={`${index}:${field.path}`}>
|
||||||
@@ -465,12 +478,12 @@ function DefinitionEditor({ draft, disabled, auth, onChange }: { draft: Template
|
|||||||
<IconButton label="Remove field" icon={<X size={16} />} variant="ghost" disabled={disabled} onClick={() => update("required_fields", draft.required_fields.filter((_, fieldIndex) => fieldIndex !== index))} />
|
<IconButton label="Remove field" icon={<X size={16} />} variant="ghost" disabled={disabled} onClick={() => update("required_fields", draft.required_fields.filter((_, fieldIndex) => fieldIndex !== index))} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{!draft.required_fields.length && <p className="templates-inline-empty">No required fields. Tokens still resolve from supplied parameters and items.</p>}
|
{!draft.required_fields.length && <StatePanel size="inline" description="No required fields. Tokens still resolve from supplied parameters and items." />}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</ContentSection>
|
||||||
|
|
||||||
<section className="templates-section">
|
<ContentSection>
|
||||||
<div className="templates-section-heading"><strong>Page and media</strong></div>
|
<ActionToolbar surface="section-header" className="templates-section-heading"><strong>Page and media</strong></ActionToolbar>
|
||||||
<div className="templates-layout-fields">
|
<div className="templates-layout-fields">
|
||||||
<FormField label="Page size"><select disabled={disabled} value={String(layout.page_size ?? pageSizeForType(draft.template_type))} onChange={(event) => update("layout", { ...layout, page_size: event.target.value })}>{["A3", "A4", "A5", "Letter", "Legal", "DL"].map((value) => <option key={value} value={value}>{value}</option>)}</select></FormField>
|
<FormField label="Page size"><select disabled={disabled} value={String(layout.page_size ?? pageSizeForType(draft.template_type))} onChange={(event) => update("layout", { ...layout, page_size: event.target.value })}>{["A3", "A4", "A5", "Letter", "Legal", "DL"].map((value) => <option key={value} value={value}>{value}</option>)}</select></FormField>
|
||||||
<FormField label="Margin (mm)"><input type="number" min="0" max="60" disabled={disabled} value={Number(layout.margin_mm ?? 15)} onChange={(event) => update("layout", { ...layout, margin_mm: Number(event.target.value) })} /></FormField>
|
<FormField label="Margin (mm)"><input type="number" min="0" max="60" disabled={disabled} value={Number(layout.margin_mm ?? 15)} onChange={(event) => update("layout", { ...layout, margin_mm: Number(event.target.value) })} /></FormField>
|
||||||
@@ -480,12 +493,12 @@ function DefinitionEditor({ draft, disabled, auth, onChange }: { draft: Template
|
|||||||
<FormField label="Gap (mm)"><input type="number" min="0" max="20" disabled={disabled} value={Number(layout.gap_mm ?? 2)} onChange={(event) => update("layout", { ...layout, gap_mm: Number(event.target.value) })} /></FormField>
|
<FormField label="Gap (mm)"><input type="number" min="0" max="20" disabled={disabled} value={Number(layout.gap_mm ?? 2)} onChange={(event) => update("layout", { ...layout, gap_mm: Number(event.target.value) })} /></FormField>
|
||||||
</>}
|
</>}
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</ContentSection>
|
||||||
|
|
||||||
<section className="templates-section templates-body-section">
|
<ContentSection className="templates-body-section">
|
||||||
<div className="templates-section-heading"><strong>Template body</strong><small>Use tokens such as {"{{name}}"} or {"{{recipient.address}}"}.</small></div>
|
<ActionToolbar surface="section-header" className="templates-section-heading"><strong>Template body</strong><small>Use tokens such as {"{{name}}"} or {"{{recipient.address}}"}.</small></ActionToolbar>
|
||||||
<WysiwygEditor disabled={disabled} value={draft.content_html ?? ""} onChange={(value) => update("content_html", value || null)} minHeight={300} />
|
<WysiwygEditor disabled={disabled} value={draft.content_html ?? ""} onChange={(value) => update("content_html", value || null)} minHeight={300} />
|
||||||
</section>
|
</ContentSection>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -509,8 +522,8 @@ function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, c
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="templates-preview">
|
<div className="templates-preview">
|
||||||
<section className="templates-section">
|
<ContentSection>
|
||||||
<div className="templates-section-heading"><strong>Validated sample input</strong><small>Preview and final output use the same pinned revision and canonical input.</small></div>
|
<ActionToolbar surface="section-header" className="templates-section-heading"><strong>Validated sample input</strong><small>Preview and final output use the same pinned revision and canonical input.</small></ActionToolbar>
|
||||||
<div className="templates-preview-controls">
|
<div className="templates-preview-controls">
|
||||||
<FormField label="Usage" documentation={TEMPLATE_OUTPUT_DOCUMENTATION}><select value={usage} onChange={(event) => onUsage(event.target.value)}>{item.revision.usages.map((value) => <option key={value} value={value}>{value}</option>)}</select></FormField>
|
<FormField label="Usage" documentation={TEMPLATE_OUTPUT_DOCUMENTATION}><select value={usage} onChange={(event) => onUsage(event.target.value)}>{item.revision.usages.map((value) => <option key={value} value={value}>{value}</option>)}</select></FormField>
|
||||||
<FormField label="Output" documentation={TEMPLATE_OUTPUT_DOCUMENTATION}><SegmentedControl value={outputFormat} onChange={onOutputFormat} options={[{ id: "html", label: "Printable HTML" }, { id: "text", label: "Plain text" }]} ariaLabel="Output format" /></FormField>
|
<FormField label="Output" documentation={TEMPLATE_OUTPUT_DOCUMENTATION}><SegmentedControl value={outputFormat} onChange={onOutputFormat} options={[{ id: "html", label: "Printable HTML" }, { id: "text", label: "Plain text" }]} ariaLabel="Output format" /></FormField>
|
||||||
@@ -521,7 +534,7 @@ function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, c
|
|||||||
<Button disabled={disabled} disabledReason={disabled ? disabledReason : undefined} onClick={() => onRender(false)}><Eye size={16} /> Validate and preview</Button>
|
<Button disabled={disabled} disabledReason={disabled ? disabledReason : undefined} onClick={() => onRender(false)}><Eye size={16} /> Validate and preview</Button>
|
||||||
<Button variant="primary" disabled={disabled || !item.revision.published_at} disabledReason={disabled ? disabledReason : !item.revision.published_at ? "Publish this revision before producing final output." : undefined} onClick={() => onRender(true)}><Send size={16} /> Render final output</Button>
|
<Button variant="primary" disabled={disabled || !item.revision.published_at} disabledReason={disabled ? disabledReason : !item.revision.published_at ? "Publish this revision before producing final output." : undefined} onClick={() => onRender(true)}><Send size={16} /> Render final output</Button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</ContentSection>
|
||||||
|
|
||||||
{compatibility && <DismissibleAlert tone={compatibility.compatible ? "success" : "danger"}>
|
{compatibility && <DismissibleAlert tone={compatibility.compatible ? "success" : "danger"}>
|
||||||
{compatibility.compatible
|
{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(" ")}
|
: compatibility.diagnostics.map((item) => String(item.message ?? item.code ?? "Incompatible input")).join(" ")}
|
||||||
</DismissibleAlert>}
|
</DismissibleAlert>}
|
||||||
|
|
||||||
{render && <section className="templates-section templates-render-result">
|
{render && <ContentSection className="templates-render-result">
|
||||||
<div className="templates-section-heading"><strong>Render evidence</strong><Button disabled={!render.artifact?.download_path} onClick={onDownload}><Download size={16} /> Download</Button></div>
|
<ActionToolbar surface="section-header" className="templates-section-heading"><strong>Render evidence</strong><Button disabled={!render.artifact?.download_path} onClick={onDownload}><Download size={16} /> Download</Button></ActionToolbar>
|
||||||
<dl>
|
<dl>
|
||||||
<div><dt>Revision</dt><dd>{render.revision} · {shortHash(render.template_hash)}</dd></div>
|
<div><dt>Revision</dt><dd>{render.revision} · {shortHash(render.template_hash)}</dd></div>
|
||||||
<div><dt>Input</dt><dd>{shortHash(render.input_hash)}</dd></div>
|
<div><dt>Input</dt><dd>{shortHash(render.input_hash)}</dd></div>
|
||||||
@@ -540,7 +553,7 @@ function PreviewPanel({ item, sampleText, usage, outputFormat, persistToFiles, c
|
|||||||
<div><dt>Generated</dt><dd>{render.generated_at ? formatDateTime(render.generated_at) : "Now"}</dd></div>
|
<div><dt>Generated</dt><dd>{render.generated_at ? formatDateTime(render.generated_at) : "Now"}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
<p>{render.artifact?.kind === "managed_file" ? "Managed by Files" : "Bounded Templates download"} · {render.output_size_bytes.toLocaleString()} bytes</p>
|
<p>{render.artifact?.kind === "managed_file" ? "Managed by Files" : "Bounded Templates download"} · {render.output_size_bytes.toLocaleString()} bytes</p>
|
||||||
</section>}
|
</ContentSection>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +1,19 @@
|
|||||||
.templates-page {
|
.templates-workspace-toolbar { min-width: 0; }
|
||||||
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-toolbar-actions { display: flex; align-items: center; gap: 7px; flex: 0 0 auto; }
|
.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-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 { 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 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-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 { flex: 0 0 auto; padding: 0 12px; }
|
||||||
.templates-alerts:empty { display: none; }
|
.templates-alerts:empty { display: none; }
|
||||||
.templates-alerts .alert { margin: 10px 0 0; }
|
.templates-alerts .alert { margin: 10px 0 0; }
|
||||||
.templates-workspace > .loading-frame { flex: 1 1 auto; min-height: 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-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 { 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 .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-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 small { color: var(--muted); font-weight: 400; }
|
||||||
.templates-section-heading .btn { display: inline-flex; align-items: center; gap: 6px; }
|
.templates-section-heading .btn { display: inline-flex; align-items: center; gap: 6px; }
|
||||||
.templates-fields-table { overflow: auto; padding: 8px; }
|
.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); }
|
.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) {
|
@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 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
.templates-definition-fields .form-field:nth-child(5) { grid-column: auto; }
|
.templates-definition-fields .form-field:nth-child(5) { grid-column: auto; }
|
||||||
.templates-layout-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
.templates-layout-fields { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||||
@@ -104,9 +51,6 @@
|
|||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.templates-page { height: auto; min-height: calc(100vh - 100px); overflow: visible; }
|
.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-workspace-toolbar { align-items: flex-start; flex-wrap: wrap; }
|
||||||
.templates-toolbar-actions { flex-wrap: wrap; }
|
.templates-toolbar-actions { flex-wrap: wrap; }
|
||||||
.templates-content { height: auto; overflow: visible; }
|
.templates-content { height: auto; overflow: visible; }
|
||||||
|
|||||||
Reference in New Issue
Block a user