feat: collapsible accordion slots for template editing UX
Template slot editors (notification + command) now use collapsible accordion rows instead of showing all editors at once. Each slot displays a compact header with status pill (empty/valid/warning/error). Adds slot name filtering and a preview toggle button that swaps between editor and rendered preview views.
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
import { providerTypeItems as providerTypeItemsFn, providerTypeFilterItems } from '$lib/grid-items';
|
||||
import Modal from '$lib/components/Modal.svelte';
|
||||
import JinjaEditor from '$lib/components/JinjaEditor.svelte';
|
||||
import CollapsibleSlot from '$lib/components/CollapsibleSlot.svelte';
|
||||
import { snackSuccess, snackError } from '$lib/stores/snackbar.svelte';
|
||||
import { highlightFromUrl } from '$lib/highlight';
|
||||
import { globalProviderFilter } from '$lib/stores/provider-filter.svelte';
|
||||
@@ -60,6 +61,28 @@
|
||||
let varsRef = $state<Record<string, any>>({});
|
||||
let showVarsFor = $state<string | null>(null);
|
||||
let activeLocale = $state<string>('en');
|
||||
let expandedSlots = $state<Set<string>>(new Set());
|
||||
let slotFilter = $state('');
|
||||
let showPreviewFor = $state<Set<string>>(new Set());
|
||||
|
||||
function toggleSlot(key: string) {
|
||||
const next = new Set(expandedSlots);
|
||||
if (next.has(key)) next.delete(key); else next.add(key);
|
||||
expandedSlots = next;
|
||||
}
|
||||
|
||||
function togglePreview(key: string) {
|
||||
const next = new Set(showPreviewFor);
|
||||
if (next.has(key)) next.delete(key); else next.add(key);
|
||||
showPreviewFor = next;
|
||||
}
|
||||
|
||||
function getSlotStatus(key: string): 'empty' | 'valid' | 'error' | 'warning' {
|
||||
if (slotErrors[key] && slotErrorTypes[key] !== 'undefined') return 'error';
|
||||
if (slotErrors[key] && slotErrorTypes[key] === 'undefined') return 'warning';
|
||||
if (getSlotValue(key)) return 'valid';
|
||||
return 'empty';
|
||||
}
|
||||
|
||||
// Provider capabilities
|
||||
let allCapabilities = $state<Record<string, any>>({});
|
||||
@@ -67,6 +90,11 @@
|
||||
let commandSlots = $derived<SlotDef[]>(
|
||||
allCapabilities[form.provider_type]?.command_slots || []
|
||||
);
|
||||
let filteredCmdSlots = $derived(
|
||||
slotFilter
|
||||
? commandSlots.filter(s => s.name.toLowerCase().includes(slotFilter.toLowerCase()) || s.description.toLowerCase().includes(slotFilter.toLowerCase()))
|
||||
: commandSlots
|
||||
);
|
||||
|
||||
const defaultForm = () => ({
|
||||
provider_type: '',
|
||||
@@ -157,6 +185,9 @@
|
||||
activeLocale = 'en';
|
||||
slotPreview = {};
|
||||
slotErrors = {};
|
||||
expandedSlots = new Set();
|
||||
showPreviewFor = new Set();
|
||||
slotFilter = '';
|
||||
}
|
||||
|
||||
function edit(c: CmdTemplateConfig) {
|
||||
@@ -177,6 +208,9 @@
|
||||
activeLocale = 'en';
|
||||
slotPreview = {};
|
||||
slotErrors = {};
|
||||
expandedSlots = new Set();
|
||||
showPreviewFor = new Set();
|
||||
slotFilter = '';
|
||||
setTimeout(() => refreshAllPreviews(), 100);
|
||||
}
|
||||
|
||||
@@ -216,6 +250,9 @@
|
||||
activeLocale = 'en';
|
||||
slotPreview = {};
|
||||
slotErrors = {};
|
||||
expandedSlots = new Set();
|
||||
showPreviewFor = new Set();
|
||||
slotFilter = '';
|
||||
setTimeout(() => refreshAllPreviews(), 100);
|
||||
}
|
||||
|
||||
@@ -293,23 +330,50 @@
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
{#each commandSlots as slot}
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-1">
|
||||
<label class="text-xs text-[var(--color-muted-foreground)]">/{slot.name} — {slot.description}</label>
|
||||
<!-- Slot filter -->
|
||||
{#if commandSlots.length > 4}
|
||||
<div class="mb-3">
|
||||
<input type="text" bind:value={slotFilter} placeholder={t('templateConfig.filterSlots')}
|
||||
class="w-full px-3 py-1.5 border border-[var(--color-border)] rounded-md text-sm bg-[var(--color-background)]" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="space-y-2">
|
||||
{#each filteredCmdSlots as slot}
|
||||
<CollapsibleSlot
|
||||
label={slot.name}
|
||||
description="/{slot.name} — {slot.description}"
|
||||
expanded={expandedSlots.has(slot.name)}
|
||||
status={getSlotStatus(slot.name)}
|
||||
ontoggle={() => toggleSlot(slot.name)}
|
||||
>
|
||||
<div class="flex items-center justify-end gap-2 mb-2">
|
||||
{#if slotPreview[slot.name] && !slotErrors[slot.name]}
|
||||
<button type="button" onclick={() => togglePreview(slot.name)}
|
||||
class="text-xs px-2 py-0.5 rounded-md transition-colors {showPreviewFor.has(slot.name) ? 'bg-[var(--color-primary)] text-[var(--color-primary-foreground)]' : 'text-[var(--color-muted-foreground)] hover:bg-[var(--color-muted)]'}">
|
||||
{t('templateConfig.preview')}
|
||||
</button>
|
||||
{/if}
|
||||
{#if varsRef[slot.name]}
|
||||
<button type="button" onclick={() => showVarsFor = slot.name}
|
||||
class="text-xs text-[var(--color-muted-foreground)] hover:underline">{t('templateConfig.variables')}</button>
|
||||
{/if}
|
||||
</div>
|
||||
<JinjaEditor
|
||||
value={getSlotValue(slot.name)}
|
||||
onchange={(v: string) => { setSlotValue(slot.name, v); validateSlot(slot.name, v); }}
|
||||
rows={3}
|
||||
errorLine={slotErrorLines[slot.name] || null}
|
||||
variables={varsRef[slot.name] || undefined}
|
||||
/>
|
||||
|
||||
{#if showPreviewFor.has(slot.name) && slotPreview[slot.name] && !slotErrors[slot.name]}
|
||||
<div class="p-2 bg-[var(--color-muted)] rounded text-sm mb-2">
|
||||
<pre class="whitespace-pre-wrap text-xs">{@html sanitizePreview(slotPreview[slot.name])}</pre>
|
||||
</div>
|
||||
{:else}
|
||||
<JinjaEditor
|
||||
value={getSlotValue(slot.name)}
|
||||
onchange={(v: string) => { setSlotValue(slot.name, v); validateSlot(slot.name, v); }}
|
||||
rows={3}
|
||||
errorLine={slotErrorLines[slot.name] || null}
|
||||
variables={varsRef[slot.name] || undefined}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if slotErrors[slot.name]}
|
||||
{#if slotErrorTypes[slot.name] === 'undefined'}
|
||||
<p class="mt-1 text-xs" style="color: #d97706;">⚠ {t('common.undefinedVar')}: {slotErrors[slot.name]}</p>
|
||||
@@ -317,12 +381,7 @@
|
||||
<p class="mt-1 text-xs" style="color: var(--color-error-fg);">✕ {t('common.syntaxError')}: {slotErrors[slot.name]}{slotErrorLines[slot.name] ? ` (${t('common.line')} ${slotErrorLines[slot.name]})` : ''}</p>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if slotPreview[slot.name] && !slotErrors[slot.name]}
|
||||
<div class="mt-1 p-2 bg-[var(--color-muted)] rounded text-sm">
|
||||
<pre class="whitespace-pre-wrap text-xs">{@html sanitizePreview(slotPreview[slot.name])}</pre>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</CollapsibleSlot>
|
||||
{/each}
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
Reference in New Issue
Block a user