/**
* 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 */}
);
}