feat(journal): improve command completion behavior

- Scroll the selected completion item into view
- Filter commands based on prefix when typing a slash command
- Keep completion menu open while typing a command
- Add data attributes to track completion indices for scrolling
This commit is contained in:
hypercross 2026-07-06 17:20:20 +08:00
parent e0d61cf91e
commit 276241f3b0
1 changed files with 28 additions and 16 deletions

View File

@ -12,6 +12,7 @@
import { import {
Component, Component,
createSignal, createSignal,
createEffect,
onMount, onMount,
onCleanup, onCleanup,
Show, Show,
@ -126,18 +127,34 @@ export const JournalInput: Component = () => {
revertLatest(); revertLatest();
} }
// ---- Scroll selected completion into view ----
createEffect(() => {
const idx = selectedIdx();
if (!showCompletions() || !completionsRef) return;
const el = completionsRef.querySelector(`[data-comp-idx="${idx}"]`);
if (el) el.scrollIntoView({ block: "nearest" });
});
// ---- Completions ---- // ---- Completions ----
function buildCompletions(): CompletionItem[] { function buildCompletions(): CompletionItem[] {
const raw = text().trim(); const raw = text().trim();
const data = comp.data; const data = comp.data;
const commands = [
{ label: "/roll", kind: "command" as const, insertText: "/roll " },
{ label: "/link", kind: "command" as const, insertText: "/link " },
];
// Show commands when user types / // Show commands when user types / or starts typing a command name
if (raw === "" || raw === "/") { if (raw.startsWith("/") && !raw.includes(" ")) {
return [ const prefix = raw.toLowerCase();
{ label: "/roll", kind: "command", insertText: "/roll " }, const matches = commands.filter((c) =>
{ label: "/link", kind: "command", insertText: "/link " }, c.label.toLowerCase().startsWith(prefix),
]; );
return matches.length > 0
? matches
: [{ label: "Unknown command", kind: "no-results", insertText: "" }];
} }
// After /roll — show dice suggestions // After /roll — show dice suggestions
@ -226,9 +243,9 @@ export const JournalInput: Component = () => {
} }
return; return;
} }
// If not open and typing /, open completions // If not open and starts with /, open completions
const raw = text(); const raw = text();
if (raw === "/" || raw.startsWith("/roll") || raw.startsWith("/link")) { if (raw.startsWith("/")) {
e.preventDefault(); e.preventDefault();
setShowCompletions(true); setShowCompletions(true);
setSelectedIdx(0); setSelectedIdx(0);
@ -271,14 +288,8 @@ export const JournalInput: Component = () => {
const raw = input.value; const raw = input.value;
setText(raw); setText(raw);
// Completions visibility // Completions visibility — keep open while user types any /
if ( if (raw.startsWith("/")) {
raw === "/" ||
raw.startsWith("/roll ") ||
raw.startsWith("/link ") ||
raw === "/roll" ||
raw === "/link"
) {
setShowCompletions(true); setShowCompletions(true);
setSelectedIdx(0); setSelectedIdx(0);
} else { } else {
@ -317,6 +328,7 @@ export const JournalInput: Component = () => {
<For each={comps}> <For each={comps}>
{(item, idx) => ( {(item, idx) => (
<div <div
data-comp-idx={idx()}
class={`flex items-center gap-2 px-3 py-1.5 cursor-pointer text-sm ${ class={`flex items-center gap-2 px-3 py-1.5 cursor-pointer text-sm ${
item.kind === "no-results" item.kind === "no-results"
? "text-gray-400 italic cursor-default" ? "text-gray-400 italic cursor-default"