feat: implement command autocomplete for journal input
Refactor journal input command parsing into a dedicated module and add a `CompletionsDropdown` component to provide real-time suggestions for commands (/roll, /spark, /link, /stat) and their arguments.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* CompletionsDropdown — autocomplete popover for journal command input.
|
||||
*/
|
||||
|
||||
import { Component, For, Show, createEffect, onMount, onCleanup } from "solid-js";
|
||||
import type { CompletionItem } from "./command-parser";
|
||||
import type { CompletionsState } from "./completions";
|
||||
|
||||
export interface CompletionsDropdownProps {
|
||||
show: boolean;
|
||||
items: CompletionItem[];
|
||||
selectedIdx: number;
|
||||
onSelect: (item: CompletionItem) => void;
|
||||
onHover: (idx: number) => void;
|
||||
onClose: () => void;
|
||||
state: CompletionsState;
|
||||
}
|
||||
|
||||
export const CompletionsDropdown: Component<CompletionsDropdownProps> = (
|
||||
props,
|
||||
) => {
|
||||
let containerRef!: HTMLDivElement;
|
||||
|
||||
// Scroll selected into view
|
||||
createEffect(() => {
|
||||
const idx = props.selectedIdx;
|
||||
if (!props.show || !containerRef) return;
|
||||
const el = containerRef.querySelector(`[data-comp-idx="${idx}"]`);
|
||||
if (el) el.scrollIntoView({ block: "nearest" });
|
||||
});
|
||||
|
||||
// Click outside
|
||||
onMount(() => {
|
||||
const handler = (e: MouseEvent) => {
|
||||
if (containerRef && !containerRef.contains(e.target as Node)) {
|
||||
props.onClose();
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handler);
|
||||
onCleanup(() => document.removeEventListener("mousedown", handler));
|
||||
});
|
||||
|
||||
return (
|
||||
<Show when={props.show}>
|
||||
<Show
|
||||
when={props.state.status === "loaded" || props.state.status === "empty"}
|
||||
fallback={
|
||||
<Show
|
||||
when={props.state.status === "loading"}
|
||||
fallback={
|
||||
<div
|
||||
ref={containerRef}
|
||||
class="absolute bottom-full left-0 right-0 bg-white border border-red-200 rounded-t shadow-lg mb-0.5 z-50 px-3 py-2 text-xs text-red-500"
|
||||
>
|
||||
{(props.state as any).message || "无法加载补全数据"}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
class="absolute bottom-full left-0 right-0 bg-white border border-gray-200 rounded-t shadow-lg mb-0.5 z-50 px-3 py-2 text-xs text-gray-400 italic"
|
||||
>
|
||||
正在加载补全数据…
|
||||
</div>
|
||||
</Show>
|
||||
}
|
||||
>
|
||||
<Show when={props.items.length > 0}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
class="absolute bottom-full left-0 right-0 bg-white border border-gray-200 rounded-t shadow-lg mb-0.5 max-h-40 overflow-y-auto z-50"
|
||||
>
|
||||
<For each={props.items}>
|
||||
{(item, idx) => (
|
||||
<div
|
||||
data-comp-idx={idx()}
|
||||
class={`flex items-center gap-2 px-3 py-1.5 cursor-pointer text-sm ${
|
||||
item.kind === "no-results"
|
||||
? "text-gray-400 italic cursor-default"
|
||||
: idx() === props.selectedIdx
|
||||
? "bg-blue-50 text-blue-700"
|
||||
: "hover:bg-gray-100 text-gray-700"
|
||||
}`}
|
||||
onClick={() => props.onSelect(item)}
|
||||
onMouseEnter={() =>
|
||||
item.kind !== "no-results" && props.onHover(idx())
|
||||
}
|
||||
>
|
||||
<Show
|
||||
when={item.kind !== "no-results"}
|
||||
fallback={
|
||||
<span class="text-xs text-gray-300 shrink-0">—</span>
|
||||
}
|
||||
>
|
||||
<span
|
||||
class={`text-xs px-1 rounded shrink-0 ${
|
||||
item.kind === "command"
|
||||
? "bg-purple-100 text-purple-700"
|
||||
: "bg-gray-100 text-gray-500"
|
||||
}`}
|
||||
>
|
||||
{item.kind === "command" ? "cmd" : "val"}
|
||||
</span>
|
||||
</Show>
|
||||
<span class="font-mono truncate flex-1">{item.label}</span>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</Show>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user