Fix keyboard nav, add inverted selector, big knockout canvas labels

Replace broken flatIndex counter with stable indexOf lookup so desktop
keyboard navigation works. Menu selector now uses full-row colour
inversion (cyan bg, black text) instead of a > prefix. "kamil banc"
heading is selectable at index 0 and links to /about. Selector persists
after executing commands so you can keep browsing.

Canvas labels for menu items (CLAIMS, STRATEGY, etc.) render as large
block letters using a 5x7 bitmap font with auto-scaling. Text punches
black knockout holes in the fluid simulation instead of glowing cyan.
Oracle responses and SHOW YOURSELF still use single-line bright overlay.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kbanc85 2026-02-15 14:29:04 -05:00
parent d910a32083
commit 313a3514ca
3 changed files with 220 additions and 73 deletions

View File

@ -150,6 +150,103 @@ export function renderDensity(
} }
} }
/* ── Bitmap font: 5×7 per character, stored as 7 rows of 5-bit numbers ── */
const BITMAP_FONT: Record<string, number[]> = {
A: [0b01110, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
B: [0b11110, 0b10001, 0b10001, 0b11110, 0b10001, 0b10001, 0b11110],
C: [0b01110, 0b10001, 0b10000, 0b10000, 0b10000, 0b10001, 0b01110],
D: [0b11110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b11110],
E: [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b11111],
F: [0b11111, 0b10000, 0b10000, 0b11110, 0b10000, 0b10000, 0b10000],
G: [0b01110, 0b10001, 0b10000, 0b10111, 0b10001, 0b10001, 0b01110],
H: [0b10001, 0b10001, 0b10001, 0b11111, 0b10001, 0b10001, 0b10001],
I: [0b01110, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b01110],
J: [0b00111, 0b00010, 0b00010, 0b00010, 0b00010, 0b10010, 0b01100],
K: [0b10001, 0b10010, 0b10100, 0b11000, 0b10100, 0b10010, 0b10001],
L: [0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b10000, 0b11111],
M: [0b10001, 0b11011, 0b10101, 0b10101, 0b10001, 0b10001, 0b10001],
N: [0b10001, 0b11001, 0b10101, 0b10011, 0b10001, 0b10001, 0b10001],
O: [0b01110, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
P: [0b11110, 0b10001, 0b10001, 0b11110, 0b10000, 0b10000, 0b10000],
Q: [0b01110, 0b10001, 0b10001, 0b10001, 0b10101, 0b10010, 0b01101],
R: [0b11110, 0b10001, 0b10001, 0b11110, 0b10100, 0b10010, 0b10001],
S: [0b01110, 0b10001, 0b10000, 0b01110, 0b00001, 0b10001, 0b01110],
T: [0b11111, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100, 0b00100],
U: [0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b10001, 0b01110],
V: [0b10001, 0b10001, 0b10001, 0b10001, 0b01010, 0b01010, 0b00100],
W: [0b10001, 0b10001, 0b10001, 0b10101, 0b10101, 0b11011, 0b10001],
X: [0b10001, 0b10001, 0b01010, 0b00100, 0b01010, 0b10001, 0b10001],
Y: [0b10001, 0b10001, 0b01010, 0b00100, 0b00100, 0b00100, 0b00100],
Z: [0b11111, 0b00001, 0b00010, 0b00100, 0b01000, 0b10000, 0b11111],
" ": [0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000, 0b00000],
};
/**
* Build a big-text overlay using the bitmap font.
* Each "on" pixel becomes a scale×scale block of █ cells,
* auto-scaled to fill ~80% of grid width.
*/
export function createBigTextOverlay(
text: string,
gridWidth: number,
gridHeight: number,
): TextOverlayState {
const chars = text.toUpperCase().split("");
// Calculate bitmap dimensions (5 wide per char + 1 gap, minus trailing gap)
const charW = 5;
const charH = 7;
const gap = 1;
const bitmapWidth = chars.length * (charW + gap) - gap;
// Auto-scale to fill ~80% of grid width, minimum 1
const scale = Math.max(1, Math.floor((gridWidth * 0.8) / bitmapWidth));
const totalW = bitmapWidth * scale;
const totalH = charH * scale;
const offsetX = Math.floor((gridWidth - totalW) / 2);
const offsetY = Math.floor((gridHeight - totalH) / 2);
const cells: TextOverlayCell[] = [];
const cellDelays: number[] = [];
for (let ci = 0; ci < chars.length; ci++) {
const glyph = BITMAP_FONT[chars[ci]];
if (!glyph) continue;
const charOffsetX = ci * (charW + gap);
for (let row = 0; row < charH; row++) {
const bits = glyph[row];
for (let col = 0; col < charW; col++) {
// Check if this pixel is "on" (MSB-first: bit 4 = leftmost)
if (bits & (1 << (charW - 1 - col))) {
// Expand to scale×scale block
const delay = Math.random();
for (let sy = 0; sy < scale; sy++) {
for (let sx = 0; sx < scale; sx++) {
const gx = offsetX + (charOffsetX + col) * scale + sx;
const gy = offsetY + row * scale + sy;
if (gx < 0 || gx >= gridWidth || gy < 0 || gy >= gridHeight) continue;
const spanIdx = gy * gridWidth + gx;
cells.push({
spanIdx,
char: "\u2588", // █
gx: gx + 1, // 1-indexed for fluid grid
gy: gy + 1,
});
cellDelays.push(delay);
}
}
}
}
}
}
return { startTime: performance.now(), cells, cellDelays, dark: true };
}
/* ── Text overlay: stamps text into the simulation grid ── */ /* ── Text overlay: stamps text into the simulation grid ── */
const SCRAMBLE_CHARS = "!@#$%^&*<>{}[]|/\\?~+="; const SCRAMBLE_CHARS = "!@#$%^&*<>{}[]|/\\?~+=";
@ -165,6 +262,7 @@ export interface TextOverlayState {
startTime: number; startTime: number;
cells: TextOverlayCell[]; cells: TextOverlayCell[];
cellDelays: number[]; cellDelays: number[];
dark?: boolean;
} }
/** /**
@ -207,6 +305,8 @@ export function renderTextOverlay(
assembleProgress: number, assembleProgress: number,
dissolveProgress: number, dissolveProgress: number,
): void { ): void {
const dark = overlay.dark ?? false;
for (let i = 0; i < overlay.cells.length; i++) { for (let i = 0; i < overlay.cells.length; i++) {
const { spanIdx, char } = overlay.cells[i]; const { spanIdx, char } = overlay.cells[i];
if (char === " ") continue; if (char === " ") continue;
@ -216,32 +316,58 @@ export function renderTextOverlay(
const delay = overlay.cellDelays[i]; const delay = overlay.cellDelays[i];
if (dissolveProgress > 0) { if (dark) {
// Dissolve: each cell scrambles then vanishes (staggered by delay) // Dark/knockout mode: letters punch black holes in the fluid
const cellProg = dissolveProgress / (1 - delay * 0.4); if (dissolveProgress > 0) {
if (cellProg >= 1) continue; // fully dissolved, fluid takes over const cellProg = dissolveProgress / (1 - delay * 0.4);
if (cellProg > 0.3) { if (cellProg >= 1) continue;
span.textContent = SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)]; if (cellProg > 0.3) {
span.style.color = "hsl(190,80%,45%)"; span.textContent = SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
span.style.color = "hsl(190,15%,18%)";
} else {
span.textContent = " ";
span.style.color = "";
}
} else if (assembleProgress < 1) {
const cellProg = (assembleProgress - delay * 0.4) / 0.6;
if (cellProg <= 0) continue;
if (cellProg < 0.6) {
span.textContent = SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
span.style.color = "hsl(190,15%,18%)";
} else {
span.textContent = " ";
span.style.color = "";
}
} else { } else {
span.textContent = char; span.textContent = " ";
span.style.color = "hsl(190,90%,75%)"; span.style.color = "";
}
} else if (assembleProgress < 1) {
// Assemble: each cell scrambles then locks (staggered by delay)
const cellProg = (assembleProgress - delay * 0.4) / 0.6;
if (cellProg <= 0) continue; // not started, fluid still showing
if (cellProg < 0.6) {
span.textContent = SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
span.style.color = "hsl(190,80%,45%)";
} else {
span.textContent = char;
span.style.color = "hsl(190,90%,75%)";
} }
} else { } else {
// Hold: show text clearly // Bright mode: cyan text overlay
span.textContent = char; if (dissolveProgress > 0) {
span.style.color = "hsl(190,90%,75%)"; const cellProg = dissolveProgress / (1 - delay * 0.4);
if (cellProg >= 1) continue;
if (cellProg > 0.3) {
span.textContent = SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
span.style.color = "hsl(190,80%,45%)";
} else {
span.textContent = char;
span.style.color = "hsl(190,90%,75%)";
}
} else if (assembleProgress < 1) {
const cellProg = (assembleProgress - delay * 0.4) / 0.6;
if (cellProg <= 0) continue;
if (cellProg < 0.6) {
span.textContent = SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
span.style.color = "hsl(190,80%,45%)";
} else {
span.textContent = char;
span.style.color = "hsl(190,90%,75%)";
}
} else {
span.textContent = char;
span.style.color = "hsl(190,90%,75%)";
}
} }
} }
} }

View File

@ -123,21 +123,30 @@ function MenuItem({
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className={`block w-full text-left text-sm transition-all ${ className={`block w-full text-left text-sm px-1 -mx-1 transition-all ${
isHighlighted isHighlighted
? "text-terminal-cyan glow-cyan" ? "bg-[hsl(var(--terminal-cyan))] text-black"
: "text-terminal-cyan hover:glow-cyan" : "text-terminal-cyan hover:glow-cyan"
}`} }`}
> >
<span className={`inline-block w-4 ${isHighlighted ? "text-terminal-cyan glow-cyan" : "invisible"}`}>&gt;</span>
{entry.command} {entry.command}
{entry.hint && ( {entry.hint && (
<span className="text-terminal-dim ml-4 text-xs">{entry.hint}</span> <span className={`ml-4 text-xs ${isHighlighted ? "text-black/60" : "text-terminal-dim"}`}>{entry.hint}</span>
)} )}
</button> </button>
); );
} }
// Name entry: always index 0 in the selectable list, navigates to /about
const NAME_ENTRY: CommandEntry = {
command: "about",
aliases: [],
href: "/about",
summary: "",
hint: "",
group: "pages",
};
// ─── Main component (inner, needs CanvasSceneProvider above it) ── // ─── Main component (inner, needs CanvasSceneProvider above it) ──
function TerminalHomepageInner({ function TerminalHomepageInner({
@ -150,7 +159,7 @@ function TerminalHomepageInner({
const { pushScene, popScene } = useCanvasScene(); const { pushScene, popScene } = useCanvasScene();
const [history, setHistory] = useState<HistoryEntry[]>([]); const [history, setHistory] = useState<HistoryEntry[]>([]);
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const [activeItemIndex, setActiveItemIndex] = useState(-1); const [activeItemIndex, setActiveItemIndex] = useState(0);
const [streamingId, setStreamingId] = useState<number | null>(null); const [streamingId, setStreamingId] = useState<number | null>(null);
const [completedIds, setCompletedIds] = useState<Set<number>>(new Set()); const [completedIds, setCompletedIds] = useState<Set<number>>(new Set());
const [pendingNavigation, setPendingNavigation] = useState<string | null>(null); const [pendingNavigation, setPendingNavigation] = useState<string | null>(null);
@ -160,15 +169,15 @@ function TerminalHomepageInner({
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
// All selectable menu items in display order // All selectable menu items: NAME_ENTRY at index 0, then COMMANDS
const selectableItems = useMemo(() => COMMANDS, []); const selectableItems = useMemo(() => [NAME_ENTRY, ...COMMANDS], []);
const groups = useMemo(() => { const groups = useMemo(() => {
const g: Record<string, CommandEntry[]> = {}; const g: Record<string, CommandEntry[]> = {};
for (const cmd of selectableItems) { for (const cmd of COMMANDS) {
(g[cmd.group] ??= []).push(cmd); (g[cmd.group] ??= []).push(cmd);
} }
return g; return g;
}, [selectableItems]); }, []);
// Autocomplete suggestion // Autocomplete suggestion
const suggestion = useMemo(() => { const suggestion = useMemo(() => {
@ -446,7 +455,6 @@ function TerminalHomepageInner({
const value = inputValue.trim(); const value = inputValue.trim();
if (!value) return; if (!value) return;
setInputValue(""); setInputValue("");
setActiveItemIndex(-1);
executeCommand(value); executeCommand(value);
}, },
[inputValue, streamingId, history, completedIds, executeCommand], [inputValue, streamingId, history, completedIds, executeCommand],
@ -469,18 +477,16 @@ function TerminalHomepageInner({
setActiveItemIndex((prev) => (prev >= selectableItems.length - 1 ? 0 : prev + 1)); setActiveItemIndex((prev) => (prev >= selectableItems.length - 1 ? 0 : prev + 1));
return; return;
} }
if (e.key === "Enter" && activeItemIndex >= 0) { if (e.key === "Enter" && activeItemIndex >= 0 && !inputValue.trim()) {
e.preventDefault(); e.preventDefault();
const item = selectableItems[activeItemIndex]; const item = selectableItems[activeItemIndex];
if (item) { if (item) {
setInputValue("");
setActiveItemIndex(-1);
executeCommand(item.command); executeCommand(item.command);
} }
return; return;
} }
}, },
[suggestion, selectableItems, activeItemIndex, executeCommand], [suggestion, selectableItems, activeItemIndex, inputValue, executeCommand],
); );
const handleContainerClick = useCallback(() => { const handleContainerClick = useCallback(() => {
@ -489,9 +495,6 @@ function TerminalHomepageInner({
const dynamicHint = `${claimCount} atomic claims, citation-ready`; const dynamicHint = `${claimCount} atomic claims, citation-ready`;
// Track flat index for highlight across all groups
let flatIndex = 0;
return ( return (
// Black page background with padding around the terminal // Black page background with padding around the terminal
<div className="h-screen bg-black flex items-center justify-center p-3 md:p-6 lg:p-8"> <div className="h-screen bg-black flex items-center justify-center p-3 md:p-6 lg:p-8">
@ -506,9 +509,16 @@ function TerminalHomepageInner({
<div className="flex-1 overflow-y-auto p-4"> <div className="flex-1 overflow-y-auto p-4">
{/* Intro */} {/* Intro */}
<div className="mb-4"> <div className="mb-4">
<p className="text-foreground text-sm"> <button
onClick={() => { setActiveItemIndex(0); executeCommand("about"); }}
className={`block text-left text-sm px-1 -mx-1 transition-all ${
activeItemIndex === 0
? "bg-[hsl(var(--terminal-cyan))] text-black"
: "text-foreground"
}`}
>
kamil banc. i build AI cultures. kamil banc. i build AI cultures.
</p> </button>
</div> </div>
{/* Mobile canvas */} {/* Mobile canvas */}
@ -521,49 +531,48 @@ function TerminalHomepageInner({
<div className="space-y-3"> <div className="space-y-3">
<MenuGroup label="projects"> <MenuGroup label="projects">
{(groups["projects"] ?? []).map((entry) => { {(groups["projects"] ?? []).map((entry) => {
const idx = flatIndex++; const idx = selectableItems.indexOf(entry);
return ( return (
<MenuItem <MenuItem
key={entry.command} key={entry.command}
entry={{ ...entry, hint: entry.command === "claims-library" ? dynamicHint : entry.hint }} entry={{ ...entry, hint: entry.command === "claims-library" ? dynamicHint : entry.hint }}
isHighlighted={activeItemIndex === idx} isHighlighted={activeItemIndex === idx}
onClick={() => { setInputValue(""); setActiveItemIndex(-1); executeCommand(entry.command); }} onClick={() => { setInputValue(""); setActiveItemIndex(idx); executeCommand(entry.command); }}
/> />
); );
})} })}
</MenuGroup> </MenuGroup>
<MenuGroup label="topics"> <MenuGroup label="topics">
{(groups["topics"] ?? []).map((entry) => { {(groups["topics"] ?? []).map((entry) => {
const idx = flatIndex++; const idx = selectableItems.indexOf(entry);
return ( return (
<MenuItem key={entry.command} entry={entry} isHighlighted={activeItemIndex === idx} <MenuItem key={entry.command} entry={entry} isHighlighted={activeItemIndex === idx}
onClick={() => { setInputValue(""); setActiveItemIndex(-1); executeCommand(entry.command); }} onClick={() => { setInputValue(""); setActiveItemIndex(idx); executeCommand(entry.command); }}
/> />
); );
})} })}
</MenuGroup> </MenuGroup>
<MenuGroup label="pages"> <MenuGroup label="pages">
{(groups["pages"] ?? []).map((entry) => { {(groups["pages"] ?? []).map((entry) => {
const idx = flatIndex++; const idx = selectableItems.indexOf(entry);
return ( return (
<MenuItem key={entry.command} entry={entry} isHighlighted={activeItemIndex === idx} <MenuItem key={entry.command} entry={entry} isHighlighted={activeItemIndex === idx}
onClick={() => { setInputValue(""); setActiveItemIndex(-1); executeCommand(entry.command); }} onClick={() => { setInputValue(""); setActiveItemIndex(idx); executeCommand(entry.command); }}
/> />
); );
})} })}
</MenuGroup> </MenuGroup>
<MenuGroup label="links"> <MenuGroup label="links">
{(groups["links"] ?? []).map((entry) => { {(groups["links"] ?? []).map((entry) => {
const idx = flatIndex++; const idx = selectableItems.indexOf(entry);
return ( return (
<button key={entry.command} <button key={entry.command}
onClick={() => { setInputValue(""); setActiveItemIndex(-1); executeCommand(entry.command); }} onClick={() => { setInputValue(""); setActiveItemIndex(idx); executeCommand(entry.command); }}
className={`block w-full text-left text-sm transition-all ${activeItemIndex === idx ? "glow-cyan" : ""}`} className={`block w-full text-left text-sm px-1 -mx-1 transition-all ${activeItemIndex === idx ? "bg-[hsl(var(--terminal-cyan))] text-black" : ""}`}
> >
<span className={`inline-block w-4 ${activeItemIndex === idx ? "text-terminal-cyan glow-cyan" : "invisible"}`}>&gt;</span> <span className={activeItemIndex === idx ? "text-black/60" : "text-terminal-dim"}>{entry.command}</span>
<span className="text-terminal-dim">{entry.command}</span> <span className={`mx-2 ${activeItemIndex === idx ? "text-black/40" : "text-terminal-dim"}`}>&rarr;</span>
<span className="text-terminal-dim mx-2">&rarr;</span> <span className={activeItemIndex === idx ? "text-black" : "text-terminal-cyan hover:glow-cyan transition-all"}>{entry.hint}</span>
<span className={`text-terminal-cyan ${activeItemIndex === idx ? "glow-cyan" : "hover:glow-cyan"} transition-all`}>{entry.hint}</span>
</button> </button>
); );
})} })}
@ -592,7 +601,7 @@ function TerminalHomepageInner({
{streamingId === null && <span className="terminal-cursor" aria-hidden="true">&#x2588;</span>} {streamingId === null && <span className="terminal-cursor" aria-hidden="true">&#x2588;</span>}
<div className="relative flex-1"> <div className="relative flex-1">
<input ref={inputRef} type="text" value={inputValue} <input ref={inputRef} type="text" value={inputValue}
onChange={(e) => { setInputValue(e.target.value); setActiveItemIndex(-1); }} onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown} disabled={streamingId !== null} onKeyDown={handleKeyDown} disabled={streamingId !== null}
className="bg-transparent border-none outline-none text-foreground placeholder:text-terminal-dim w-full font-mono disabled:opacity-50" className="bg-transparent border-none outline-none text-foreground placeholder:text-terminal-dim w-full font-mono disabled:opacity-50"
placeholder={history.length === 0 ? "type a command or ask a question..." : ""} placeholder={history.length === 0 ? "type a command or ask a question..." : ""}
@ -611,15 +620,22 @@ function TerminalHomepageInner({
{/* Left pane: menu (fixed, always visible) */} {/* Left pane: menu (fixed, always visible) */}
<div className="w-[320px] xl:w-[360px] flex-shrink-0 border-r border-border overflow-y-auto p-5"> <div className="w-[320px] xl:w-[360px] flex-shrink-0 border-r border-border overflow-y-auto p-5">
<div className="mb-6"> <div className="mb-6">
<p className="text-foreground"> <button
onClick={() => { setActiveItemIndex(0); executeCommand("about"); }}
className={`block text-left px-1 -mx-1 transition-all ${
activeItemIndex === 0
? "bg-[hsl(var(--terminal-cyan))] text-black"
: "text-foreground"
}`}
>
kamil banc. i build AI cultures. kamil banc. i build AI cultures.
</p> </button>
</div> </div>
<div className="space-y-4"> <div className="space-y-4">
<MenuGroup label="projects"> <MenuGroup label="projects">
{(groups["projects"] ?? []).map((entry) => { {(groups["projects"] ?? []).map((entry) => {
const idx = flatIndex++; const idx = selectableItems.indexOf(entry);
return ( return (
<MenuItem <MenuItem
key={entry.command} key={entry.command}
@ -628,7 +644,7 @@ function TerminalHomepageInner({
hint: entry.command === "claims-library" ? dynamicHint : entry.hint, hint: entry.command === "claims-library" ? dynamicHint : entry.hint,
}} }}
isHighlighted={activeItemIndex === idx} isHighlighted={activeItemIndex === idx}
onClick={() => { setInputValue(""); setActiveItemIndex(-1); executeCommand(entry.command); }} onClick={() => { setInputValue(""); setActiveItemIndex(idx); executeCommand(entry.command); }}
/> />
); );
})} })}
@ -636,10 +652,10 @@ function TerminalHomepageInner({
<MenuGroup label="topics"> <MenuGroup label="topics">
{(groups["topics"] ?? []).map((entry) => { {(groups["topics"] ?? []).map((entry) => {
const idx = flatIndex++; const idx = selectableItems.indexOf(entry);
return ( return (
<MenuItem key={entry.command} entry={entry} isHighlighted={activeItemIndex === idx} <MenuItem key={entry.command} entry={entry} isHighlighted={activeItemIndex === idx}
onClick={() => { setInputValue(""); setActiveItemIndex(-1); executeCommand(entry.command); }} onClick={() => { setInputValue(""); setActiveItemIndex(idx); executeCommand(entry.command); }}
/> />
); );
})} })}
@ -647,10 +663,10 @@ function TerminalHomepageInner({
<MenuGroup label="pages"> <MenuGroup label="pages">
{(groups["pages"] ?? []).map((entry) => { {(groups["pages"] ?? []).map((entry) => {
const idx = flatIndex++; const idx = selectableItems.indexOf(entry);
return ( return (
<MenuItem key={entry.command} entry={entry} isHighlighted={activeItemIndex === idx} <MenuItem key={entry.command} entry={entry} isHighlighted={activeItemIndex === idx}
onClick={() => { setInputValue(""); setActiveItemIndex(-1); executeCommand(entry.command); }} onClick={() => { setInputValue(""); setActiveItemIndex(idx); executeCommand(entry.command); }}
/> />
); );
})} })}
@ -658,17 +674,16 @@ function TerminalHomepageInner({
<MenuGroup label="links"> <MenuGroup label="links">
{(groups["links"] ?? []).map((entry) => { {(groups["links"] ?? []).map((entry) => {
const idx = flatIndex++; const idx = selectableItems.indexOf(entry);
return ( return (
<button <button
key={entry.command} key={entry.command}
onClick={() => { setInputValue(""); setActiveItemIndex(-1); executeCommand(entry.command); }} onClick={() => { setInputValue(""); setActiveItemIndex(idx); executeCommand(entry.command); }}
className={`block w-full text-left text-sm transition-all ${activeItemIndex === idx ? "glow-cyan" : ""}`} className={`block w-full text-left text-sm px-1 -mx-1 transition-all ${activeItemIndex === idx ? "bg-[hsl(var(--terminal-cyan))] text-black" : ""}`}
> >
<span className={`inline-block w-4 ${activeItemIndex === idx ? "text-terminal-cyan glow-cyan" : "invisible"}`}>&gt;</span> <span className={activeItemIndex === idx ? "text-black/60" : "text-terminal-dim"}>{entry.command}</span>
<span className="text-terminal-dim">{entry.command}</span> <span className={`mx-2 ${activeItemIndex === idx ? "text-black/40" : "text-terminal-dim"}`}>&rarr;</span>
<span className="text-terminal-dim mx-2">&rarr;</span> <span className={activeItemIndex === idx ? "text-black" : "text-terminal-cyan hover:glow-cyan transition-all"}>
<span className={`text-terminal-cyan ${activeItemIndex === idx ? "glow-cyan" : "hover:glow-cyan"} transition-all`}>
{entry.hint} {entry.hint}
</span> </span>
</button> </button>
@ -696,7 +711,7 @@ function TerminalHomepageInner({
ref={inputRef} ref={inputRef}
type="text" type="text"
value={inputValue} value={inputValue}
onChange={(e) => { setInputValue(e.target.value); setActiveItemIndex(-1); }} onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
disabled={streamingId !== null} disabled={streamingId !== null}
className="bg-transparent border-none outline-none text-foreground placeholder:text-terminal-dim w-full font-mono disabled:opacity-50" className="bg-transparent border-none outline-none text-foreground placeholder:text-terminal-dim w-full font-mono disabled:opacity-50"

View File

@ -6,6 +6,7 @@ import {
import { import {
renderDensity, renderDensity,
createTextOverlay, createTextOverlay,
createBigTextOverlay,
renderTextOverlay, renderTextOverlay,
type RenderTarget, type RenderTarget,
type TextOverlayState, type TextOverlayState,
@ -44,7 +45,12 @@ export function createOracleScene(opts: OracleSceneOptions): SceneDriver {
return { return {
enter(state: FluidState, target: RenderTarget) { enter(state: FluidState, target: RenderTarget) {
overlay = createTextOverlay(overlayText, target.width, target.height); // Short uppercase labels (menu items) get big block letters;
// longer text (oracle responses, "SHOW YOURSELF") stays single-line
const isBigLabel = overlayText.length <= 10 && overlayText === overlayText.toUpperCase();
overlay = isBigLabel
? createBigTextOverlay(overlayText, target.width, target.height)
: createTextOverlay(overlayText, target.width, target.height);
done = false; done = false;
}, },