Files
resona/src/lib/components/variable-input.svelte

168 lines
4.2 KiB
Svelte

<script lang="ts">
import { Input } from "$lib/components/ui/input/index.js";
import type { ResolvedVariable } from "$lib/types/variable";
type Props = {
value: string;
placeholder?: string;
class?: string;
variables: ResolvedVariable[];
oninput?: (value: string) => void;
};
let {
value = "",
placeholder = "",
class: className = "",
variables = [],
oninput,
}: Props = $props();
let showSuggestions = $state(false);
let filteredVariables = $state<ResolvedVariable[]>([]);
let selectedIndex = $state(0);
let inputElement: HTMLInputElement | null = $state(null);
let cursorPosition = $state(0);
function findVariableContext(
text: string,
position: number
): { start: number; query: string } | null {
const beforeCursor = text.substring(0, position);
const match = beforeCursor.match(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)?$/);
if (match) {
return {
start: match.index! + 2,
query: (match[1] || "").toLowerCase(),
};
}
return null;
}
function handleInput(e: Event) {
const target = e.target as HTMLInputElement;
cursorPosition = target.selectionStart || 0;
const context = findVariableContext(target.value, cursorPosition);
if (context) {
filteredVariables = variables.filter((v) =>
v.name.toLowerCase().includes(context.query)
);
showSuggestions = filteredVariables.length > 0;
selectedIndex = 0;
} else {
showSuggestions = false;
}
if (oninput) {
oninput(target.value);
}
}
function handleKeydown(e: KeyboardEvent) {
if (!showSuggestions) return;
switch (e.key) {
case "ArrowDown":
e.preventDefault();
selectedIndex = Math.min(
selectedIndex + 1,
filteredVariables.length - 1
);
break;
case "ArrowUp":
e.preventDefault();
selectedIndex = Math.max(selectedIndex - 1, 0);
break;
case "Enter":
case "Tab":
if (filteredVariables.length > 0) {
e.preventDefault();
insertVariable(filteredVariables[selectedIndex]);
}
break;
case "Escape":
showSuggestions = false;
break;
}
}
function insertVariable(variable: ResolvedVariable) {
const context = findVariableContext(value, cursorPosition);
if (!context) return;
const before = value.substring(0, context.start);
const after = value.substring(cursorPosition);
const newValue = `${before}${variable.name}}}${after}`;
if (oninput) {
oninput(newValue);
}
showSuggestions = false;
setTimeout(() => {
if (inputElement) {
const newPosition = context.start + variable.name.length + 2;
inputElement.setSelectionRange(newPosition, newPosition);
inputElement.focus();
}
}, 0);
}
function handleBlur() {
setTimeout(() => {
showSuggestions = false;
}, 150);
}
function getScopeColor(scope: string): string {
const colors: Record<string, string> = {
global: "text-purple-500",
workspace: "text-blue-500",
collection: "text-green-500",
request: "text-orange-500",
};
return colors[scope] || "text-gray-500";
}
</script>
<div class="relative {className}">
<Input
bind:ref={inputElement}
{value}
{placeholder}
oninput={handleInput}
onkeydown={handleKeydown}
onblur={handleBlur}
class="font-mono text-sm"
/>
{#if showSuggestions}
<div
class="absolute z-50 top-full left-0 right-0 mt-1 bg-popover border rounded-md shadow-lg max-h-48 overflow-auto"
>
{#each filteredVariables as variable, i (variable.name)}
<button
type="button"
class="w-full px-3 py-2 text-left text-sm hover:bg-accent flex items-center justify-between gap-2 {i ===
selectedIndex
? 'bg-accent'
: ''}"
onmousedown={() => insertVariable(variable)}
>
<span class="font-mono">
{#if variable.isSecret}
🔒
{/if}
{variable.name}
</span>
<span class="text-xs {getScopeColor(variable.scope)}"
>{variable.scope}</span
>
</button>
{/each}
</div>
{/if}
</div>