// Slash menu — appears when the user types `/`. Shows the four chip kinds. // On selection, inserts a chip with a placeholder label that the user can // then rename inline. "use client"; import { useEffect, useImperativeHandle, useState, forwardRef } from "react"; import type { ChipKind } from "../../lib/fixtures/aristotle"; export interface SlashItem { kind: ChipKind; label: string; hint: string; glyph: string; } export const SLASH_ITEMS: SlashItem[] = [ { kind: "block", label: "Block", hint: "an entity in the system", glyph: "▢" }, { kind: "property", label: "Property", hint: "an attribute of a block", glyph: "·" }, { kind: "association", label: "Association", hint: "a relationship", glyph: "→" }, { kind: "requirement", label: "Requirement", hint: "a stated goal (REQ-NNN)", glyph: "§" }, ]; export interface SlashMenuHandle { onKeyDown: (event: KeyboardEvent) => boolean; } interface SlashMenuProps { query: string; command: (item: SlashItem) => void; } export const SlashMenu = forwardRef(function SlashMenu( { query, command }, ref ) { const filtered = SLASH_ITEMS.filter( item => query.length === 0 || item.kind.toLowerCase().startsWith(query.toLowerCase()) || item.label.toLowerCase().startsWith(query.toLowerCase()) ); const [activeIndex, setActiveIndex] = useState(0); useEffect(() => { setActiveIndex(0); }, [query]); useImperativeHandle(ref, () => ({ onKeyDown(event: KeyboardEvent) { if (filtered.length === 0) return false; if (event.key === "ArrowUp") { setActiveIndex(prev => (prev - 1 + filtered.length) % filtered.length); return true; } if (event.key === "ArrowDown") { setActiveIndex(prev => (prev + 1) % filtered.length); return true; } if (event.key === "Enter" || event.key === "Tab") { const choice = filtered[activeIndex]; if (choice) { command(choice); return true; } } // Number-key shortcut: 1–4 picks the corresponding item const num = parseInt(event.key, 10); if (!Number.isNaN(num) && num >= 1 && num <= filtered.length) { const choice = filtered[num - 1]; if (choice) { command(choice); return true; } } return false; }, })); if (filtered.length === 0) { return (
no matches for “{query}”
); } return (
{filtered.map((item, idx) => ( ))}
); });