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:
parent
d910a32083
commit
313a3514ca
|
|
@ -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,10 +316,37 @@ export function renderTextOverlay(
|
||||||
|
|
||||||
const delay = overlay.cellDelays[i];
|
const delay = overlay.cellDelays[i];
|
||||||
|
|
||||||
|
if (dark) {
|
||||||
|
// Dark/knockout mode: letters punch black holes in the fluid
|
||||||
if (dissolveProgress > 0) {
|
if (dissolveProgress > 0) {
|
||||||
// Dissolve: each cell scrambles then vanishes (staggered by delay)
|
|
||||||
const cellProg = dissolveProgress / (1 - delay * 0.4);
|
const cellProg = dissolveProgress / (1 - delay * 0.4);
|
||||||
if (cellProg >= 1) continue; // fully dissolved, fluid takes over
|
if (cellProg >= 1) continue;
|
||||||
|
if (cellProg > 0.3) {
|
||||||
|
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 {
|
||||||
|
span.textContent = " ";
|
||||||
|
span.style.color = "";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Bright mode: cyan text overlay
|
||||||
|
if (dissolveProgress > 0) {
|
||||||
|
const cellProg = dissolveProgress / (1 - delay * 0.4);
|
||||||
|
if (cellProg >= 1) continue;
|
||||||
if (cellProg > 0.3) {
|
if (cellProg > 0.3) {
|
||||||
span.textContent = SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
|
span.textContent = SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
|
||||||
span.style.color = "hsl(190,80%,45%)";
|
span.style.color = "hsl(190,80%,45%)";
|
||||||
|
|
@ -228,9 +355,8 @@ export function renderTextOverlay(
|
||||||
span.style.color = "hsl(190,90%,75%)";
|
span.style.color = "hsl(190,90%,75%)";
|
||||||
}
|
}
|
||||||
} else if (assembleProgress < 1) {
|
} else if (assembleProgress < 1) {
|
||||||
// Assemble: each cell scrambles then locks (staggered by delay)
|
|
||||||
const cellProg = (assembleProgress - delay * 0.4) / 0.6;
|
const cellProg = (assembleProgress - delay * 0.4) / 0.6;
|
||||||
if (cellProg <= 0) continue; // not started, fluid still showing
|
if (cellProg <= 0) continue;
|
||||||
if (cellProg < 0.6) {
|
if (cellProg < 0.6) {
|
||||||
span.textContent = SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
|
span.textContent = SCRAMBLE_CHARS[Math.floor(Math.random() * SCRAMBLE_CHARS.length)];
|
||||||
span.style.color = "hsl(190,80%,45%)";
|
span.style.color = "hsl(190,80%,45%)";
|
||||||
|
|
@ -239,9 +365,9 @@ export function renderTextOverlay(
|
||||||
span.style.color = "hsl(190,90%,75%)";
|
span.style.color = "hsl(190,90%,75%)";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Hold: show text clearly
|
|
||||||
span.textContent = char;
|
span.textContent = char;
|
||||||
span.style.color = "hsl(190,90%,75%)";
|
span.style.color = "hsl(190,90%,75%)";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"}`}>></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"}`}>></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"}`}>→</span>
|
||||||
<span className="text-terminal-dim mx-2">→</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">█</span>}
|
{streamingId === null && <span className="terminal-cursor" aria-hidden="true">█</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"}`}>></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"}`}>→</span>
|
||||||
<span className="text-terminal-dim mx-2">→</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"
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue