Jinja2 syntax highlighting + description field + preview toggle
Some checks failed
Validate / Hassfest (push) Has been cancelled

JinjaEditor:
- Custom StreamLanguage parser for Jinja2 syntax highlighting:
  {{ variables }} in blue, {% statements %} in purple, {# comments #} in gray
- Replaced HTML mode (didn't understand Jinja2 syntax)
- Proper monospace font (Consolas/Monaco)

TemplateConfig:
- Added `description` field to model + seed defaults with descriptions
- Description shown on template cards instead of raw template text
- Description input in create/edit form

Preview:
- Toggle behavior: clicking Preview again hides the preview
- Per-slot preview uses preview-raw API (renders current editor content)

i18n: added common.description, templateConfig.descriptionPlaceholder

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 19:28:00 +03:00
parent 68b104ed40
commit ce21733ae6
15 changed files with 141 additions and 45 deletions

View File

@@ -10,6 +10,7 @@
import MdiIcon from '$lib/components/MdiIcon.svelte';
import ConfirmModal from '$lib/components/ConfirmModal.svelte';
import Hint from '$lib/components/Hint.svelte';
import IconButton from '$lib/components/IconButton.svelte';
import Modal from '$lib/components/Modal.svelte';
import JinjaEditor from '$lib/components/JinjaEditor.svelte';
@@ -24,7 +25,7 @@
let slotPreview = $state<Record<string, string>>({});
const defaultForm = () => ({
name: '', icon: '',
name: '', description: '', icon: '',
message_assets_added: '',
message_assets_removed: '',
message_album_renamed: '',
@@ -79,12 +80,25 @@
}
async function previewSlot(slotKey: string) {
// Toggle: if already showing, hide it
if (slotPreview[slotKey]) { delete slotPreview[slotKey]; slotPreview = { ...slotPreview }; return; }
const template = (form as any)[slotKey] || '';
if (!template) { slotPreview[slotKey] = '(empty)'; return; }
if (!template) { slotPreview = { ...slotPreview, [slotKey]: '(empty)' }; return; }
try {
const res = await api('/template-configs/preview-raw', { method: 'POST', body: JSON.stringify({ template }) });
slotPreview[slotKey] = res.error ? `Error: ${res.error}` : res.rendered;
} catch (err: any) { slotPreview[slotKey] = `Error: ${err.message}`; }
slotPreview = { ...slotPreview, [slotKey]: res.error ? `Error: ${res.error}` : res.rendered };
} catch (err: any) { slotPreview = { ...slotPreview, [slotKey]: `Error: ${err.message}` }; }
}
async function preview(configId: number, slotKey: string) {
const config = configs.find(c => c.id === configId);
if (!config) return;
const template = config[slotKey] || '';
if (!template) return;
try {
const res = await api('/template-configs/preview-raw', { method: 'POST', body: JSON.stringify({ template }) });
slotPreview[slotKey + '_' + configId] = res.error ? `Error: ${res.error}` : res.rendered;
} catch (err: any) { slotPreview[slotKey + '_' + configId] = `Error: ${err.message}`; }
}
function remove(id: number) {
@@ -121,6 +135,11 @@
class="flex-1 px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div>
</div>
<div>
<label for="tpc-desc" class="block text-sm font-medium mb-1">{t('common.description')}</label>
<input id="tpc-desc" bind:value={form.description} placeholder={t('templateConfig.descriptionPlaceholder')}
class="w-full px-3 py-2 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
</div>
{#each templateSlots as group}
<fieldset class="border border-[var(--color-border)] rounded-md p-3">
@@ -178,11 +197,17 @@
{#if config.icon}<MdiIcon name={config.icon} />{/if}
<p class="font-medium">{config.name}</p>
</div>
<pre class="text-xs text-[var(--color-muted-foreground)] mt-1 whitespace-pre-wrap font-mono bg-[var(--color-muted)] rounded p-2">{config.message_assets_added?.slice(0, 150)}...</pre>
<pre class="text-xs text-[var(--color-muted-foreground)] mt-1 whitespace-pre-wrap font-mono bg-[var(--color-muted)] rounded p-2">{config.message_assets_added?.slice(0, 120)}...</pre>
{#if slotPreview['message_assets_added_' + config.id]}
<div class="mt-2 p-2 bg-[var(--color-success-bg)] rounded text-sm">
<pre class="whitespace-pre-wrap">{slotPreview['message_assets_added_' + config.id]}</pre>
</div>
{/if}
</div>
<div class="flex items-center gap-3 ml-4">
<button onclick={() => edit(config)} class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('common.edit')}</button>
<button onclick={() => remove(config.id)} class="text-xs text-[var(--color-destructive)] hover:underline">{t('common.delete')}</button>
<div class="flex items-center gap-1 ml-4">
<IconButton icon="mdiEye" title={t('templateConfig.preview')} onclick={() => preview(config.id, 'message_assets_added')} />
<IconButton icon="mdiPencil" title={t('common.edit')} onclick={() => edit(config)} />
<IconButton icon="mdiDelete" title={t('common.delete')} onclick={() => remove(config.id)} variant="danger" />
</div>
</div>
</Card>