/** * wishlist-overlay.tsx — Modal popup Ink component for quick wishlist browsing * and adding items. * * Replaces the neo-blessed WishlistOverlay class from tui-wishlist.ts. * * Layout: * +----------------------------------------------+ * | Wishlist (3 items) | * +----------------------------------------------+ * | > Buy new keyboard 2025-12-01 | * | Research TUI frameworks 2025-12-05 | * | Add dark mode support 2025-12-10 | * +----------------------------------------------+ * | [____________________________________] | * +----------------------------------------------+ * | A:add D/X:delete Esc/W:close | * +----------------------------------------------+ * * Keybinds: * Up/Down — navigate items * A — add new item (inline text input) * D / X — delete selected item * Escape/W — close the overlay * S-Up/Down — move item up/down in order */ import React, { useState, useCallback } from "react"; import { Box, Text, useInput } from "ink"; import type { WishlistItem } from "../lib/wishlist-store"; import { useWishlistStore } from "./use-wishlist-store"; import { formatDate, truncateText } from "./wishlist-helpers"; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface WishlistOverlayProps { /** Project root directory. */ projectRoot: string; /** Called when the overlay is closed (Esc or W). */ onClose: () => void; } // --------------------------------------------------------------------------- // Sub-components // --------------------------------------------------------------------------- /** * OverlayItemList — scrollable list of wishlist items. */ function OverlayItemList({ items, selectedIndex, }: { items: WishlistItem[]; selectedIndex: number; }): React.ReactElement { if (items.length === 0) { return ( No wishlist items. Press A to add one. ); } return ( {items.map((item, i) => { const isSelected = i === selectedIndex; const pos = String(i + 1).padStart(2, " "); const text = truncateText(item.text, 48); const date = formatDate(item.created_at); const tagsBadge = item.tags.length > 0 ? ` [${item.tags.join(", ")}]` : ""; return ( {isSelected ? ( {` ${pos}. ${text}${tagsBadge} ${date} `} ) : ( {` ${pos}. `} {text} {tagsBadge} {` ${date}`} )} ); })} ); } /** * AddInput — inline text input shown when user presses A to add a new item. */ function AddInput({ value, onChange, }: { value: string; onChange: (v: string) => void; }): React.ReactElement { return ( {"> "} {value} {" "} ); } /** * OverlayFooter — keybind hints at the bottom. */ function OverlayFooter({ addingItem, }: { addingItem: boolean; }): React.ReactElement { if (addingItem) { return ( Enter save Esc cancel ); } return ( A add D/X delete S-{"\u2191"}/{"\u2193"} reorder Esc/W close ); } // --------------------------------------------------------------------------- // Main Component // --------------------------------------------------------------------------- /** * WishlistOverlay — modal popup component for quick wishlist browsing/adding. */ export function WishlistOverlay({ projectRoot, onClose, }: WishlistOverlayProps): React.ReactElement { const store = useWishlistStore({ projectRoot }); const [addingItem, setAddingItem] = useState(false); const [inputValue, setInputValue] = useState(""); const showAddInput = useCallback(() => { setAddingItem(true); setInputValue(""); }, []); const hideAddInput = useCallback(() => { setAddingItem(false); setInputValue(""); }, []); const submitNewItem = useCallback(() => { const trimmed = inputValue.trim(); if (!trimmed) return; store.addNewItem(trimmed); hideAddInput(); }, [inputValue, store, hideAddInput]); useInput((input, key) => { if (addingItem) { // In add mode, handle text input if (key.return) { submitNewItem(); return; } if (key.escape) { hideAddInput(); return; } if (key.backspace) { setInputValue((prev) => prev.slice(0, -1)); return; } // Filter control chars if (key.ctrl || key.meta || key.tab) return; if (key.upArrow || key.downArrow || key.leftArrow || key.rightArrow) return; // Regular character input if (input && input.length > 0) { setInputValue((prev) => prev + input); } return; } // Browse mode // Navigation if (key.downArrow && !key.shift) { store.selectNext(); return; } if (key.upArrow && !key.shift) { store.selectPrev(); return; } // Shift+Up/Down — reorder if (key.upArrow && key.shift) { store.moveSelectedUp(); return; } if (key.downArrow && key.shift) { store.moveSelectedDown(); return; } // A — add new item if (input === "a") { showAddInput(); return; } // D / X — delete selected item if (input === "d" || input === "x") { store.deleteSelected(); return; } // Escape / W — close overlay if (key.escape || input === "w") { onClose(); return; } }); const count = store.items.length; const label = `Wishlist (${count} item${count !== 1 ? "s" : ""})`; return ( {/* Label / Header */} {label} {/* Item list */} {/* Add input (shown when A is pressed) */} {addingItem && ( )} {/* Footer */} ); }