diff options
| author | Adam Malczewski <[email protected]> | 2026-06-27 01:13:28 +0900 |
|---|---|---|
| committer | Adam Malczewski <[email protected]> | 2026-06-27 01:13:31 +0900 |
| commit | 2fa03f8d7410c2b8d6be8e10ad088863e83d7177 (patch) | |
| tree | 94e1923180ae38d571d34b578afecb0a18913c24 /src/features/system-prompt | |
| parent | 80f99665034a0e510300793205c162fc7a46769f (diff) | |
| parent | 08b12478636f4a5c86a1f3c40a843f2906b7c82f (diff) | |
| download | dispatch-web-2fa03f8d7410c2b8d6be8e10ad088863e83d7177.tar.gz dispatch-web-2fa03f8d7410c2b8d6be8e10ad088863e83d7177.zip | |
Merge branch 'dev' into feature/heartbeat
# Conflicts:
# src/app/App.svelte
# src/app/store.svelte.ts
# src/app/store.test.ts
# src/features/workspaces/ui/WorkspaceCard.test.ts
Diffstat (limited to 'src/features/system-prompt')
| -rw-r--r-- | src/features/system-prompt/index.ts | 18 | ||||
| -rw-r--r-- | src/features/system-prompt/logic/view-model.test.ts | 148 | ||||
| -rw-r--r-- | src/features/system-prompt/logic/view-model.ts | 74 | ||||
| -rw-r--r-- | src/features/system-prompt/ui/SystemPromptBuilder.svelte | 467 |
4 files changed, 354 insertions, 353 deletions
diff --git a/src/features/system-prompt/index.ts b/src/features/system-prompt/index.ts index 142d4b1..50d9d21 100644 --- a/src/features/system-prompt/index.ts +++ b/src/features/system-prompt/index.ts @@ -1,17 +1,17 @@ export type { - LoadSystemPrompt, - LoadSystemPromptVariables, - SaveSystemPrompt, - SystemPromptLoadResult, - SystemPromptSaveResult, - SystemPromptVariablesResult, - VariableGroup, + LoadSystemPrompt, + LoadSystemPromptVariables, + SaveSystemPrompt, + SystemPromptLoadResult, + SystemPromptSaveResult, + SystemPromptVariablesResult, + VariableGroup, } from "./logic/view-model"; export { buildTag, groupVariables, insertTag, isDynamicVariable } from "./logic/view-model"; export { default as SystemPromptBuilder } from "./ui/SystemPromptBuilder.svelte"; /** Public module manifest — aggregated by the shell's "Loaded Modules" view. */ export const manifest = { - name: "system-prompt", - description: "Global system prompt template builder with variable placeholders", + name: "system-prompt", + description: "Global system prompt template builder with variable placeholders", } as const; diff --git a/src/features/system-prompt/logic/view-model.test.ts b/src/features/system-prompt/logic/view-model.test.ts index a0736cc..223327b 100644 --- a/src/features/system-prompt/logic/view-model.test.ts +++ b/src/features/system-prompt/logic/view-model.test.ts @@ -1,90 +1,90 @@ import type { SystemPromptVariable } from "@dispatch/transport-contract"; import { describe, expect, it } from "vitest"; import { - buildIfNotTag, - buildIfTag, - buildTag, - groupVariables, - insertTag, - isDynamicVariable, + buildIfNotTag, + buildIfTag, + buildTag, + groupVariables, + insertTag, + isDynamicVariable, } from "./view-model"; describe("system-prompt view-model", () => { - describe("buildTag", () => { - it("builds a variable placeholder", () => { - expect(buildTag("system", "time")).toBe("[system:time]"); - }); - }); + describe("buildTag", () => { + it("builds a variable placeholder", () => { + expect(buildTag("system", "time")).toBe("[system:time]"); + }); + }); - describe("buildIfTag", () => { - it("builds an opening conditional tag", () => { - expect(buildIfTag("file", "AGENTS.md")).toBe("[if file:AGENTS.md]"); - }); - }); + describe("buildIfTag", () => { + it("builds an opening conditional tag", () => { + expect(buildIfTag("file", "AGENTS.md")).toBe("[if file:AGENTS.md]"); + }); + }); - describe("buildIfNotTag", () => { - it("builds a negated opening conditional tag", () => { - expect(buildIfNotTag("prompt", "cwd")).toBe("[if !prompt:cwd]"); - }); - }); + describe("buildIfNotTag", () => { + it("builds a negated opening conditional tag", () => { + expect(buildIfNotTag("prompt", "cwd")).toBe("[if !prompt:cwd]"); + }); + }); - describe("insertTag", () => { - it("inserts a tag at the cursor position", () => { - expect(insertTag("Hello world", "[system:time]", 5, 5)).toEqual({ - template: "Hello[system:time] world", - cursor: 18, - }); - }); + describe("insertTag", () => { + it("inserts a tag at the cursor position", () => { + expect(insertTag("Hello world", "[system:time]", 5, 5)).toEqual({ + template: "Hello[system:time] world", + cursor: 18, + }); + }); - it("replaces the selected text", () => { - const tag = buildTag("file", "README.md"); - expect(insertTag("Hello world", tag, 6, 11)).toEqual({ - template: `Hello ${tag}`, - cursor: 6 + tag.length, - }); - }); + it("replaces the selected text", () => { + const tag = buildTag("file", "README.md"); + expect(insertTag("Hello world", tag, 6, 11)).toEqual({ + template: `Hello ${tag}`, + cursor: 6 + tag.length, + }); + }); - it("inserts at the end", () => { - const tag = buildTag("git", "branch"); - expect(insertTag("", tag, 0, 0)).toEqual({ - template: tag, - cursor: tag.length, - }); - }); - }); + it("inserts at the end", () => { + const tag = buildTag("git", "branch"); + expect(insertTag("", tag, 0, 0)).toEqual({ + template: tag, + cursor: tag.length, + }); + }); + }); - describe("groupVariables", () => { - it("groups by type in first-appearing order", () => { - const variables: SystemPromptVariable[] = [ - { type: "system", name: "time", description: "" }, - { type: "prompt", name: "cwd", description: "" }, - { type: "system", name: "date", description: "" }, - { type: "git", name: "branch", description: "" }, - ]; - const groups = groupVariables(variables); - expect(groups.map((g) => g.type)).toEqual(["system", "prompt", "git"]); - expect(groups[0]?.variables.map((v) => v.name)).toEqual(["time", "date"]); - expect(groups[1]?.variables.map((v) => v.name)).toEqual(["cwd"]); - expect(groups[2]?.variables.map((v) => v.name)).toEqual(["branch"]); - }); + describe("groupVariables", () => { + it("groups by type in first-appearing order", () => { + const variables: SystemPromptVariable[] = [ + { type: "system", name: "time", description: "" }, + { type: "prompt", name: "cwd", description: "" }, + { type: "system", name: "date", description: "" }, + { type: "git", name: "branch", description: "" }, + ]; + const groups = groupVariables(variables); + expect(groups.map((g) => g.type)).toEqual(["system", "prompt", "git"]); + expect(groups[0]?.variables.map((v) => v.name)).toEqual(["time", "date"]); + expect(groups[1]?.variables.map((v) => v.name)).toEqual(["cwd"]); + expect(groups[2]?.variables.map((v) => v.name)).toEqual(["branch"]); + }); - it("returns an empty array when no variables", () => { - expect(groupVariables([])).toEqual([]); - }); - }); + it("returns an empty array when no variables", () => { + expect(groupVariables([])).toEqual([]); + }); + }); - describe("isDynamicVariable", () => { - it("returns true when dynamic is true", () => { - expect( - isDynamicVariable({ type: "file", name: "path", description: "", dynamic: true }), - ).toBe(true); - }); + describe("isDynamicVariable", () => { + it("returns true when dynamic is true", () => { + expect( + isDynamicVariable({ type: "file", name: "path", description: "", dynamic: true }), + ).toBe(true); + }); - it("returns false when dynamic is missing or false", () => { - expect(isDynamicVariable({ type: "system", name: "time", description: "" })).toBe(false); - expect( - isDynamicVariable({ type: "system", name: "time", description: "", dynamic: false }), - ).toBe(false); - }); - }); + it("returns false when dynamic is missing or false", () => { + expect(isDynamicVariable({ type: "system", name: "time", description: "" })).toBe(false); + expect( + isDynamicVariable({ type: "system", name: "time", description: "", dynamic: false }), + ).toBe(false); + }); + }); }); diff --git a/src/features/system-prompt/logic/view-model.ts b/src/features/system-prompt/logic/view-model.ts index 6924208..e202ee3 100644 --- a/src/features/system-prompt/logic/view-model.ts +++ b/src/features/system-prompt/logic/view-model.ts @@ -14,20 +14,20 @@ import type { SystemPromptVariable } from "@dispatch/transport-contract"; // ── Injected ports (composition root adapts the store to these shapes) ───────── export type SystemPromptLoadResult = - | { readonly ok: true; readonly template: string } - | { readonly ok: false; readonly error: string }; + | { readonly ok: true; readonly template: string } + | { readonly ok: false; readonly error: string }; export type LoadSystemPrompt = () => Promise<SystemPromptLoadResult>; export type SystemPromptSaveResult = - | { readonly ok: true; readonly template: string } - | { readonly ok: false; readonly error: string }; + | { readonly ok: true; readonly template: string } + | { readonly ok: false; readonly error: string }; export type SaveSystemPrompt = (template: string) => Promise<SystemPromptSaveResult>; export type SystemPromptVariablesResult = - | { readonly ok: true; readonly variables: readonly SystemPromptVariable[] } - | { readonly ok: false; readonly error: string }; + | { readonly ok: true; readonly variables: readonly SystemPromptVariable[] } + | { readonly ok: false; readonly error: string }; export type LoadSystemPromptVariables = () => Promise<SystemPromptVariablesResult>; @@ -35,24 +35,24 @@ export type LoadSystemPromptVariables = () => Promise<SystemPromptVariablesResul /** Build the literal placeholder `[type:name]` for a variable. */ export function buildTag(type: string, name: string): string { - return `[${type}:${name}]`; + return `[${type}:${name}]`; } /** Build the literal placeholder `[if type:name]` for a conditional block. */ export function buildIfTag(type: string, name: string): string { - return `[if ${type}:${name}]`; + return `[if ${type}:${name}]`; } /** Build the literal placeholder `[if !type:name]` for a negated conditional block. */ export function buildIfNotTag(type: string, name: string): string { - return `[if !${type}:${name}]`; + return `[if !${type}:${name}]`; } export interface Insertion { - /** Template text after insertion. */ - template: string; - /** New cursor position (caret index) after insertion. */ - cursor: number; + /** Template text after insertion. */ + template: string; + /** New cursor position (caret index) after insertion. */ + cursor: number; } /** @@ -61,24 +61,24 @@ export interface Insertion { * the caller can restore the caret after the inserted tag. */ export function insertTag( - template: string, - tag: string, - selectionStart: number, - selectionEnd: number, + template: string, + tag: string, + selectionStart: number, + selectionEnd: number, ): Insertion { - const before = template.slice(0, selectionStart); - const after = template.slice(selectionEnd); - const next = before + tag + after; - return { template: next, cursor: selectionStart + tag.length }; + const before = template.slice(0, selectionStart); + const after = template.slice(selectionEnd); + const next = before + tag + after; + return { template: next, cursor: selectionStart + tag.length }; } // ── Variable grouping ───────────────────────────────────────────────────────── export interface VariableGroup { - /** Variable type (e.g. `"system"`, `"file"`, `"prompt"`, `"git"`). */ - readonly type: string; - /** Variables of this type. */ - readonly variables: readonly SystemPromptVariable[]; + /** Variable type (e.g. `"system"`, `"file"`, `"prompt"`, `"git"`). */ + readonly type: string; + /** Variables of this type. */ + readonly variables: readonly SystemPromptVariable[]; } /** @@ -86,21 +86,21 @@ export interface VariableGroup { * group and the order of first-appearing types. */ export function groupVariables( - variables: readonly SystemPromptVariable[], + variables: readonly SystemPromptVariable[], ): readonly VariableGroup[] { - const order: string[] = []; - const map = new Map<string, SystemPromptVariable[]>(); - for (const v of variables) { - if (!map.has(v.type)) { - map.set(v.type, []); - order.push(v.type); - } - map.get(v.type)?.push(v); - } - return order.map((type) => ({ type, variables: map.get(type) ?? [] })); + const order: string[] = []; + const map = new Map<string, SystemPromptVariable[]>(); + for (const v of variables) { + if (!map.has(v.type)) { + map.set(v.type, []); + order.push(v.type); + } + map.get(v.type)?.push(v); + } + return order.map((type) => ({ type, variables: map.get(type) ?? [] })); } /** Whether a variable is "dynamic" (any name is valid, e.g. `file:<path>`). */ export function isDynamicVariable(variable: SystemPromptVariable): boolean { - return variable.dynamic === true; + return variable.dynamic === true; } diff --git a/src/features/system-prompt/ui/SystemPromptBuilder.svelte b/src/features/system-prompt/ui/SystemPromptBuilder.svelte index 4e07317..2e92e26 100644 --- a/src/features/system-prompt/ui/SystemPromptBuilder.svelte +++ b/src/features/system-prompt/ui/SystemPromptBuilder.svelte @@ -1,242 +1,243 @@ <script lang="ts"> - import type { SystemPromptVariable } from "@dispatch/transport-contract"; - import { tick, untrack } from "svelte"; - import { - buildTag, - groupVariables, - insertTag, - isDynamicVariable, - type LoadSystemPrompt, - type LoadSystemPromptVariables, - type SaveSystemPrompt, - } from "../logic/view-model"; - - let { - loadPrompt, - savePrompt, - loadVariables, - onClose, - }: { - loadPrompt: LoadSystemPrompt; - savePrompt: SaveSystemPrompt; - loadVariables: LoadSystemPromptVariables; - onClose: () => void; - } = $props(); - - let value = $state(""); - let loadedTemplate = $state(""); - let loading = $state(false); - let saving = $state(false); - let error = $state<string | null>(null); - let justSaved = $state(false); - let variables = $state<readonly SystemPromptVariable[]>([]); - let filePath = $state(""); - let textarea: HTMLTextAreaElement | null = null; - - const groups = $derived(groupVariables(variables)); - const hasChanges = $derived(value !== loadedTemplate); - - async function load() { - untrack(() => { - loading = true; - error = null; - }); - - const [templateResult, variablesResult] = await Promise.all([loadPrompt(), loadVariables()]); - - loading = false; - - if (!templateResult.ok || !variablesResult.ok) { - const parts: string[] = []; - if (!templateResult.ok) parts.push(templateResult.error); - if (!variablesResult.ok) parts.push(variablesResult.error); - error = parts.join("; "); - return; - } - - value = templateResult.template; - loadedTemplate = templateResult.template; - variables = variablesResult.variables; - } - - async function save() { - if (saving || loading) return; - saving = true; - error = null; - justSaved = false; - const result = await savePrompt(value); - saving = false; - if (!result.ok) { - error = result.error; - return; - } - loadedTemplate = result.template; - value = result.template; - justSaved = true; - } - - function reset() { - value = loadedTemplate; - error = null; - justSaved = false; - } - - async function insertTagAtCursor(tag: string) { - if (textarea === null) return; - const start = textarea.selectionStart; - const end = textarea.selectionEnd; - const insertion = insertTag(value, tag, start, end); - value = insertion.template; - await tick(); - textarea.focus(); - textarea.setSelectionRange(insertion.cursor, insertion.cursor); - } - - async function insertFileVariable(type: string) { - const path = filePath.trim(); - if (path.length === 0) return; - await insertTagAtCursor(buildTag(type, path)); - filePath = ""; - } - - function onKeydown(e: KeyboardEvent) { - if (e.key === "Escape") onClose(); - } - - // Load on mount once. - $effect(() => { - void load(); - }); + import type { SystemPromptVariable } from "@dispatch/transport-contract"; + import { tick, untrack } from "svelte"; + import { + buildTag, + groupVariables, + insertTag, + isDynamicVariable, + type LoadSystemPrompt, + type LoadSystemPromptVariables, + type SaveSystemPrompt, + } from "../logic/view-model"; + + let { + loadPrompt, + savePrompt, + loadVariables, + onClose, + }: { + loadPrompt: LoadSystemPrompt; + savePrompt: SaveSystemPrompt; + loadVariables: LoadSystemPromptVariables; + onClose: () => void; + } = $props(); + + let value = $state(""); + let loadedTemplate = $state(""); + let loading = $state(false); + let saving = $state(false); + let error = $state<string | null>(null); + let justSaved = $state(false); + let variables = $state<readonly SystemPromptVariable[]>([]); + let filePath = $state(""); + let textarea: HTMLTextAreaElement | null = null; + + const groups = $derived(groupVariables(variables)); + const hasChanges = $derived(value !== loadedTemplate); + + async function load() { + untrack(() => { + loading = true; + error = null; + }); + + const [templateResult, variablesResult] = await Promise.all([loadPrompt(), loadVariables()]); + + loading = false; + + if (!templateResult.ok || !variablesResult.ok) { + const parts: string[] = []; + if (!templateResult.ok) parts.push(templateResult.error); + if (!variablesResult.ok) parts.push(variablesResult.error); + error = parts.join("; "); + return; + } + + value = templateResult.template; + loadedTemplate = templateResult.template; + variables = variablesResult.variables; + } + + async function save() { + if (saving || loading) return; + saving = true; + error = null; + justSaved = false; + const result = await savePrompt(value); + saving = false; + if (!result.ok) { + error = result.error; + return; + } + loadedTemplate = result.template; + value = result.template; + justSaved = true; + } + + function reset() { + value = loadedTemplate; + error = null; + justSaved = false; + } + + async function insertTagAtCursor(tag: string) { + if (textarea === null) return; + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const insertion = insertTag(value, tag, start, end); + value = insertion.template; + await tick(); + textarea.focus(); + textarea.setSelectionRange(insertion.cursor, insertion.cursor); + } + + async function insertFileVariable(type: string) { + const path = filePath.trim(); + if (path.length === 0) return; + await insertTagAtCursor(buildTag(type, path)); + filePath = ""; + } + + function onKeydown(e: KeyboardEvent) { + if (e.key === "Escape") onClose(); + } + + // Load on mount once. + $effect(() => { + void load(); + }); </script> <svelte:window onkeydown={onKeydown} /> <!-- svelte-ignore a11y_no_static_element_interactions --> <div - class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" - role="dialog" - aria-modal="true" - aria-label="System prompt builder" - tabindex="-1" - onclick={onClose} - onkeydown={onKeydown} + class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4" + role="dialog" + aria-modal="true" + aria-label="System prompt builder" + tabindex="-1" + onclick={onClose} + onkeydown={onKeydown} > - <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions --> - <div - class="flex h-[85vh] w-full max-w-6xl flex-col overflow-hidden rounded-box bg-base-100 shadow-2xl" - onclick={(e) => e.stopPropagation()} - > - <!-- Header --> - <div class="flex shrink-0 items-center justify-between border-b border-base-300 px-4 py-3"> - <div class="flex items-center gap-2"> - <h2 class="text-sm font-semibold">System Prompt</h2> - {#if loading} - <span class="loading loading-spinner loading-xs"></span> - {/if} - </div> - <button - type="button" - class="btn btn-ghost btn-sm btn-square" - onclick={onClose} - aria-label="Close system prompt builder" - > - ✕ - </button> - </div> - - <!-- Body: half editor / half variables --> - <div class="flex min-h-0 flex-1"> - <!-- Left: template editor --> - <div class="flex w-1/2 min-w-0 flex-col gap-2 border-r border-base-300 p-4"> - <textarea - bind:this={textarea} - bind:value - class="textarea textarea-bordered min-h-0 w-full flex-1 resize-none font-mono text-xs" - placeholder={loading ? "Loading template..." : "Edit the global system prompt template..."} - disabled={loading} - aria-label="System prompt template" - ></textarea> - - <div class="flex shrink-0 items-center gap-2"> - <button - type="button" - class="btn btn-primary btn-sm" - disabled={loading || saving || !hasChanges} - onclick={save} - > - {#if saving} - <span class="loading loading-spinner loading-xs"></span> - {:else} - Save - {/if} - </button> - <button - type="button" - class="btn btn-ghost btn-sm" - disabled={loading || !hasChanges} - onclick={reset} - > - Reset - </button> - {#if justSaved && !hasChanges} - <span class="text-xs text-success">Saved.</span> - {:else if hasChanges} - <span class="text-xs opacity-60">Unsaved changes</span> - {/if} - </div> - - {#if error} - <p class="shrink-0 text-xs text-error">{error}</p> - {/if} - </div> - - <!-- Right: variable palette --> - <div class="flex w-1/2 min-w-0 flex-col overflow-y-auto p-4"> - <h3 class="mb-2 shrink-0 text-xs font-semibold uppercase opacity-60">Variables</h3> - {#if groups.length === 0 && !loading} - <p class="text-xs opacity-60">No variables available.</p> - {/if} - <div class="flex flex-col gap-3"> - {#each groups as group (group.type)} - <div class="rounded-box bg-base-200 p-3"> - <span class="text-xs font-semibold uppercase opacity-70">{group.type}</span> - <div class="mt-2 flex flex-wrap gap-1"> - {#each group.variables as variable (variable.type + variable.name)} - {#if isDynamicVariable(variable)} - <div class="flex items-center gap-1"> - <input - type="text" - class="input input-bordered input-xs w-32 font-mono" - bind:value={filePath} - placeholder={variable.name} - onkeydown={(e) => { - if (e.key === "Enter") void insertFileVariable(variable.type); - }} - /> - <button - type="button" - class="btn btn-xs" - onclick={() => void insertFileVariable(variable.type)} - > - Insert - </button> - </div> - {:else} - <button - type="button" - class="btn btn-xs" - title={variable.description} - onclick={() => void insertTagAtCursor(buildTag(variable.type, variable.name))} - > - {variable.name} - </button> - {/if} - {/each} - </div> - </div> - {/each} - </div> - </div> - </div> - </div> + <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions --> + <div + class="flex h-[85vh] w-full max-w-6xl flex-col overflow-hidden rounded-box bg-base-100 shadow-2xl" + onclick={(e) => e.stopPropagation()} + > + <!-- Header --> + <div class="flex shrink-0 items-center justify-between border-b border-base-300 px-4 py-3"> + <div class="flex items-center gap-2"> + <h2 class="text-sm font-semibold">System Prompt</h2> + {#if loading} + <span class="loading loading-spinner loading-xs"></span> + {/if} + </div> + <button + type="button" + class="btn btn-ghost btn-sm btn-square" + onclick={onClose} + aria-label="Close system prompt builder" + > + ✕ + </button> + </div> + + <!-- Body: half editor / half variables --> + <div class="flex min-h-0 flex-1"> + <!-- Left: template editor --> + <div class="flex w-1/2 min-w-0 flex-col gap-2 border-r border-base-300 p-4"> + <textarea + bind:this={textarea} + bind:value + class="textarea textarea-bordered min-h-0 w-full flex-1 resize-none font-mono text-xs" + placeholder={loading + ? "Loading template..." + : "Edit the global system prompt template..."} + disabled={loading} + aria-label="System prompt template"></textarea> + + <div class="flex shrink-0 items-center gap-2"> + <button + type="button" + class="btn btn-primary btn-sm" + disabled={loading || saving || !hasChanges} + onclick={save} + > + {#if saving} + <span class="loading loading-spinner loading-xs"></span> + {:else} + Save + {/if} + </button> + <button + type="button" + class="btn btn-ghost btn-sm" + disabled={loading || !hasChanges} + onclick={reset} + > + Reset + </button> + {#if justSaved && !hasChanges} + <span class="text-xs text-success">Saved.</span> + {:else if hasChanges} + <span class="text-xs opacity-60">Unsaved changes</span> + {/if} + </div> + + {#if error} + <p class="shrink-0 text-xs text-error">{error}</p> + {/if} + </div> + + <!-- Right: variable palette --> + <div class="flex w-1/2 min-w-0 flex-col overflow-y-auto p-4"> + <h3 class="mb-2 shrink-0 text-xs font-semibold uppercase opacity-60">Variables</h3> + {#if groups.length === 0 && !loading} + <p class="text-xs opacity-60">No variables available.</p> + {/if} + <div class="flex flex-col gap-3"> + {#each groups as group (group.type)} + <div class="rounded-box bg-base-200 p-3"> + <span class="text-xs font-semibold uppercase opacity-70">{group.type}</span> + <div class="mt-2 flex flex-wrap gap-1"> + {#each group.variables as variable (variable.type + variable.name)} + {#if isDynamicVariable(variable)} + <div class="flex items-center gap-1"> + <input + type="text" + class="input input-bordered input-xs w-32 font-mono" + bind:value={filePath} + placeholder={variable.name} + onkeydown={(e) => { + if (e.key === "Enter") void insertFileVariable(variable.type); + }} + /> + <button + type="button" + class="btn btn-xs" + onclick={() => void insertFileVariable(variable.type)} + > + Insert + </button> + </div> + {:else} + <button + type="button" + class="btn btn-xs" + title={variable.description} + onclick={() => void insertTagAtCursor(buildTag(variable.type, variable.name))} + > + {variable.name} + </button> + {/if} + {/each} + </div> + </div> + {/each} + </div> + </div> + </div> + </div> </div> |
