@ncukondo/reference-manager 0.15.3 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunks/alternate-screen-DcxkOKfW.js +19 -0
- package/dist/chunks/alternate-screen-DcxkOKfW.js.map +1 -0
- package/dist/chunks/format-C6FA-7hE.js +397 -0
- package/dist/chunks/format-C6FA-7hE.js.map +1 -0
- package/dist/chunks/{index-TKhWlXl1.js → index-4SVOiraD.js} +104 -76
- package/dist/chunks/index-4SVOiraD.js.map +1 -0
- package/dist/chunks/{index-DVFYLuQx.js → index-Bzl4_3Ki.js} +2 -2
- package/dist/chunks/index-Bzl4_3Ki.js.map +1 -0
- package/dist/chunks/index-CEYp8OSj.js +531 -0
- package/dist/chunks/index-CEYp8OSj.js.map +1 -0
- package/dist/chunks/jsx-runtime-Q5cUjSur.js +322 -0
- package/dist/chunks/jsx-runtime-Q5cUjSur.js.map +1 -0
- package/dist/chunks/reference-select-CgM-RBIa.js +214 -0
- package/dist/chunks/reference-select-CgM-RBIa.js.map +1 -0
- package/dist/chunks/{style-select-BNQHC79W.js → style-select-tmKOHx_B.js} +6 -31
- package/dist/chunks/style-select-tmKOHx_B.js.map +1 -0
- package/dist/cli/commands/attach.d.ts.map +1 -1
- package/dist/cli/commands/cite.d.ts.map +1 -1
- package/dist/cli/commands/edit.d.ts.map +1 -1
- package/dist/cli/commands/fulltext.d.ts.map +1 -1
- package/dist/cli/commands/remove.d.ts.map +1 -1
- package/dist/cli/commands/search.d.ts +1 -1
- package/dist/cli/commands/search.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/features/interactive/action-menu.d.ts +10 -12
- package/dist/features/interactive/action-menu.d.ts.map +1 -1
- package/dist/features/interactive/alternate-screen.d.ts +42 -0
- package/dist/features/interactive/alternate-screen.d.ts.map +1 -0
- package/dist/features/interactive/apps/CiteFlowApp.d.ts +52 -0
- package/dist/features/interactive/apps/CiteFlowApp.d.ts.map +1 -0
- package/dist/features/interactive/apps/SearchFlowApp.d.ts +34 -0
- package/dist/features/interactive/apps/SearchFlowApp.d.ts.map +1 -0
- package/dist/features/interactive/apps/index.d.ts +11 -0
- package/dist/features/interactive/apps/index.d.ts.map +1 -0
- package/dist/features/interactive/apps/runCiteFlow.d.ts +42 -0
- package/dist/features/interactive/apps/runCiteFlow.d.ts.map +1 -0
- package/dist/features/interactive/apps/runSearchFlow.d.ts +28 -0
- package/dist/features/interactive/apps/runSearchFlow.d.ts.map +1 -0
- package/dist/features/interactive/components/SearchableMultiSelect.d.ts +49 -0
- package/dist/features/interactive/components/SearchableMultiSelect.d.ts.map +1 -0
- package/dist/features/interactive/components/Select.d.ts +26 -0
- package/dist/features/interactive/components/Select.d.ts.map +1 -0
- package/dist/features/interactive/components/index.d.ts +8 -0
- package/dist/features/interactive/components/index.d.ts.map +1 -0
- package/dist/features/interactive/search-prompt.d.ts +15 -12
- package/dist/features/interactive/search-prompt.d.ts.map +1 -1
- package/dist/features/interactive/style-select.d.ts +3 -11
- package/dist/features/interactive/style-select.d.ts.map +1 -1
- package/package.json +7 -4
- package/dist/chunks/action-menu-yzuB0WT3.js +0 -119
- package/dist/chunks/action-menu-yzuB0WT3.js.map +0 -1
- package/dist/chunks/index-DVFYLuQx.js.map +0 -1
- package/dist/chunks/index-TKhWlXl1.js.map +0 -1
- package/dist/chunks/reference-select-B9w9CLa1.js +0 -52
- package/dist/chunks/reference-select-B9w9CLa1.js.map +0 -1
- package/dist/chunks/search-prompt-BrWpOcij.js +0 -265
- package/dist/chunks/search-prompt-BrWpOcij.js.map +0 -1
- package/dist/chunks/style-select-BNQHC79W.js.map +0 -1
- /package/bin/{reference-manager.js → cli.js} +0 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const ENTER_ALT_SCREEN = "\x1B[?1049h";
|
|
2
|
+
const EXIT_ALT_SCREEN = "\x1B[?1049l";
|
|
3
|
+
async function withAlternateScreen(fn) {
|
|
4
|
+
process.stdout.write(ENTER_ALT_SCREEN);
|
|
5
|
+
try {
|
|
6
|
+
return await fn();
|
|
7
|
+
} finally {
|
|
8
|
+
process.stdout.write(EXIT_ALT_SCREEN);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function restoreStdinAfterInk() {
|
|
12
|
+
setTimeout(() => {
|
|
13
|
+
}, 100);
|
|
14
|
+
}
|
|
15
|
+
export {
|
|
16
|
+
restoreStdinAfterInk,
|
|
17
|
+
withAlternateScreen
|
|
18
|
+
};
|
|
19
|
+
//# sourceMappingURL=alternate-screen-DcxkOKfW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"alternate-screen-DcxkOKfW.js","sources":["../../src/features/interactive/alternate-screen.ts"],"sourcesContent":["/**\n * Alternate screen buffer utilities for TUI sessions.\n *\n * When running fullscreen TUI applications, we switch to the alternate\n * screen buffer to preserve the terminal's scrollback history.\n * This is similar to how vim, less, and other TUI apps work.\n */\n\n// ANSI escape sequences for alternate screen buffer\nconst ENTER_ALT_SCREEN = \"\\x1b[?1049h\";\nconst EXIT_ALT_SCREEN = \"\\x1b[?1049l\";\n\n/**\n * Run a function within an alternate screen buffer session.\n *\n * This preserves the terminal's scrollback history by:\n * 1. Switching to alternate screen buffer before running the function\n * 2. Switching back to the main screen buffer after the function completes\n *\n * Use this to wrap TUI sessions that may take over the full terminal.\n *\n * @param fn - Async function to run in alternate screen\n * @returns The result of the function\n *\n * @example\n * ```typescript\n * const result = await withAlternateScreen(async () => {\n * const refs = await runReferenceSelect(...);\n * const style = await runStyleSelect(...);\n * return { refs, style };\n * });\n * ```\n */\nexport async function withAlternateScreen<T>(fn: () => Promise<T>): Promise<T> {\n process.stdout.write(ENTER_ALT_SCREEN);\n try {\n return await fn();\n } finally {\n process.stdout.write(EXIT_ALT_SCREEN);\n }\n}\n\n/**\n * Restore stdin after Ink exits.\n *\n * Ink calls stdin.unref() when it exits, which can cause the Node.js process\n * to exit prematurely if there's still async work to do. This function uses\n * a short-lived ref'd timer to keep the event loop alive long enough for\n * subsequent async operations to start (e.g., file I/O, database operations),\n * which will then keep the event loop alive on their own.\n *\n * Call this after Ink's waitUntilExit() resolves.\n */\nexport function restoreStdinAfterInk(): void {\n // Use a ref'd timer to keep the event loop alive for 100ms.\n // This gives async operations time to start and take over keeping\n // the event loop alive. The timer will then expire naturally.\n setTimeout(() => {}, 100);\n}\n"],"names":[],"mappings":"AASA,MAAM,mBAAmB;AACzB,MAAM,kBAAkB;AAuBxB,eAAsB,oBAAuB,IAAkC;AAC7E,UAAQ,OAAO,MAAM,gBAAgB;AACrC,MAAI;AACF,WAAO,MAAM,GAAA;AAAA,EACf,UAAA;AACE,YAAQ,OAAO,MAAM,eAAe;AAAA,EACtC;AACF;AAaO,SAAS,uBAA6B;AAI3C,aAAW,MAAM;AAAA,EAAC,GAAG,GAAG;AAC1B;"}
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { j as jsxRuntimeExports } from "./jsx-runtime-Q5cUjSur.js";
|
|
2
|
+
import { useFocus, useStdout, useInput, Box, Text } from "ink";
|
|
3
|
+
import { useState, useMemo, useEffect, useCallback } from "react";
|
|
4
|
+
const SORT_OPTIONS = [
|
|
5
|
+
{ id: "updated-desc", label: "Updated (newest first)", requiresQuery: false },
|
|
6
|
+
{ id: "updated-asc", label: "Updated (oldest first)", requiresQuery: false },
|
|
7
|
+
{ id: "created-desc", label: "Created (newest first)", requiresQuery: false },
|
|
8
|
+
{ id: "created-asc", label: "Created (oldest first)", requiresQuery: false },
|
|
9
|
+
{ id: "published-desc", label: "Published (newest first)", requiresQuery: false },
|
|
10
|
+
{ id: "published-asc", label: "Published (oldest first)", requiresQuery: false },
|
|
11
|
+
{ id: "relevance", label: "Relevance", requiresQuery: true }
|
|
12
|
+
];
|
|
13
|
+
function getSortShortLabel(sortOption) {
|
|
14
|
+
switch (sortOption) {
|
|
15
|
+
case "updated-desc":
|
|
16
|
+
return "Updated ↓";
|
|
17
|
+
case "updated-asc":
|
|
18
|
+
return "Updated ↑";
|
|
19
|
+
case "created-desc":
|
|
20
|
+
return "Created ↓";
|
|
21
|
+
case "created-asc":
|
|
22
|
+
return "Created ↑";
|
|
23
|
+
case "published-desc":
|
|
24
|
+
return "Published ↓";
|
|
25
|
+
case "published-asc":
|
|
26
|
+
return "Published ↑";
|
|
27
|
+
case "relevance":
|
|
28
|
+
return "Relevance";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function createDateComparator(getDate, descending) {
|
|
32
|
+
return (a, b) => {
|
|
33
|
+
const dateA = getDate(a)?.getTime() ?? 0;
|
|
34
|
+
const dateB = getDate(b)?.getTime() ?? 0;
|
|
35
|
+
return descending ? dateB - dateA : dateA - dateB;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function sortChoices(choices, sortOption) {
|
|
39
|
+
if (sortOption === "relevance") return [...choices];
|
|
40
|
+
const sorted = [...choices];
|
|
41
|
+
const comparators = {
|
|
42
|
+
"updated-desc": createDateComparator((c) => c.updatedDate, true),
|
|
43
|
+
"updated-asc": createDateComparator((c) => c.updatedDate, false),
|
|
44
|
+
"created-desc": createDateComparator((c) => c.createdDate, true),
|
|
45
|
+
"created-asc": createDateComparator((c) => c.createdDate, false),
|
|
46
|
+
"published-desc": createDateComparator((c) => c.publishedDate, true),
|
|
47
|
+
"published-asc": createDateComparator((c) => c.publishedDate, false)
|
|
48
|
+
};
|
|
49
|
+
return sorted.sort(comparators[sortOption]);
|
|
50
|
+
}
|
|
51
|
+
function defaultFilter(query, choices) {
|
|
52
|
+
if (!query.trim()) return choices;
|
|
53
|
+
const lowerQuery = query.toLowerCase();
|
|
54
|
+
return choices.filter(
|
|
55
|
+
(choice) => choice.title.toLowerCase().includes(lowerQuery) || choice.subtitle?.toLowerCase().includes(lowerQuery) || choice.meta?.toLowerCase().includes(lowerQuery)
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
function truncate(text, maxWidth) {
|
|
59
|
+
if (text.length <= maxWidth) return text;
|
|
60
|
+
return `${text.slice(0, maxWidth - 1)}…`;
|
|
61
|
+
}
|
|
62
|
+
function getNavigationDelta(key, input, visibleCount, maxIndex) {
|
|
63
|
+
if (key.upArrow) return -1;
|
|
64
|
+
if (key.downArrow) return 1;
|
|
65
|
+
if (key.pageUp) return -visibleCount;
|
|
66
|
+
if (key.pageDown) return visibleCount;
|
|
67
|
+
if (key.ctrl && input === "a") return -maxIndex - 1;
|
|
68
|
+
if (key.ctrl && input === "e") return maxIndex + 1;
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function parseKeyAction(input, key, visibleCount, maxIndex) {
|
|
72
|
+
if (key.escape) return { type: "cancel" };
|
|
73
|
+
if (key.return) return { type: "submit" };
|
|
74
|
+
const navDelta = getNavigationDelta(key, input, visibleCount, maxIndex);
|
|
75
|
+
if (navDelta !== null) return { type: "navigate", delta: navDelta };
|
|
76
|
+
if (key.ctrl && input === "s") return { type: "sort" };
|
|
77
|
+
if (key.tab) return { type: "toggle" };
|
|
78
|
+
if (key.backspace || key.delete) return { type: "backspace" };
|
|
79
|
+
if (input && !key.ctrl && !key.meta) return { type: "input", char: input };
|
|
80
|
+
return { type: "none" };
|
|
81
|
+
}
|
|
82
|
+
function ChoiceItem({
|
|
83
|
+
choice,
|
|
84
|
+
isSelected,
|
|
85
|
+
isFocused,
|
|
86
|
+
contentWidth
|
|
87
|
+
}) {
|
|
88
|
+
const indent = " ";
|
|
89
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { flexDirection: "column", paddingY: 0, children: [
|
|
90
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { flexDirection: "row", children: [
|
|
91
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Box, { width: 2, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { color: "cyan", children: isFocused ? "❯" : " " }) }),
|
|
92
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Box, { width: 2, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { color: isSelected ? "green" : "gray", children: isSelected ? "◉" : "○" }) }),
|
|
93
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Box, { width: 1, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { children: " " }) }),
|
|
94
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Box, { children: isFocused ? /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { color: "cyan", bold: true, children: truncate(choice.title, contentWidth) }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { color: "blue", children: truncate(choice.title, contentWidth) }) })
|
|
95
|
+
] }),
|
|
96
|
+
choice.subtitle && /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Text, { dimColor: true, children: [
|
|
97
|
+
indent,
|
|
98
|
+
truncate(choice.subtitle, contentWidth)
|
|
99
|
+
] }) }),
|
|
100
|
+
choice.meta && /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Text, { dimColor: true, children: [
|
|
101
|
+
indent,
|
|
102
|
+
truncate(choice.meta, contentWidth)
|
|
103
|
+
] }) })
|
|
104
|
+
] });
|
|
105
|
+
}
|
|
106
|
+
function ScrollIndicator({
|
|
107
|
+
direction,
|
|
108
|
+
count,
|
|
109
|
+
visible
|
|
110
|
+
}) {
|
|
111
|
+
if (!visible) return null;
|
|
112
|
+
const arrow = direction === "up" ? "↑" : "↓";
|
|
113
|
+
const label = direction === "up" ? "more above" : "more below";
|
|
114
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { height: 1, children: count > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs(Text, { dimColor: true, children: [
|
|
115
|
+
" ",
|
|
116
|
+
arrow,
|
|
117
|
+
" ",
|
|
118
|
+
count,
|
|
119
|
+
" ",
|
|
120
|
+
label
|
|
121
|
+
] }) : /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { children: " " }) });
|
|
122
|
+
}
|
|
123
|
+
function ChoiceList({
|
|
124
|
+
choices,
|
|
125
|
+
selectedIds,
|
|
126
|
+
focusIndex,
|
|
127
|
+
scrollOffset,
|
|
128
|
+
contentWidth
|
|
129
|
+
}) {
|
|
130
|
+
if (choices.length === 0) {
|
|
131
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { paddingY: 1, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { dimColor: true, children: "No results found" }) });
|
|
132
|
+
}
|
|
133
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { flexDirection: "column", children: choices.map((choice, visibleIndex) => {
|
|
134
|
+
const actualIndex = scrollOffset + visibleIndex;
|
|
135
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { flexDirection: "row", children: /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
136
|
+
ChoiceItem,
|
|
137
|
+
{
|
|
138
|
+
choice,
|
|
139
|
+
isSelected: selectedIds.has(choice.id),
|
|
140
|
+
isFocused: actualIndex === focusIndex,
|
|
141
|
+
contentWidth
|
|
142
|
+
}
|
|
143
|
+
) }, choice.id);
|
|
144
|
+
}) });
|
|
145
|
+
}
|
|
146
|
+
function SortMenu({
|
|
147
|
+
options,
|
|
148
|
+
focusIndex,
|
|
149
|
+
currentSort
|
|
150
|
+
}) {
|
|
151
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs(
|
|
152
|
+
Box,
|
|
153
|
+
{
|
|
154
|
+
flexDirection: "column",
|
|
155
|
+
marginBottom: 1,
|
|
156
|
+
paddingX: 1,
|
|
157
|
+
borderStyle: "round",
|
|
158
|
+
borderColor: "yellow",
|
|
159
|
+
children: [
|
|
160
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { bold: true, color: "yellow", children: "Sort by:" }) }),
|
|
161
|
+
options.map((opt, index) => {
|
|
162
|
+
const isFocused = index === focusIndex;
|
|
163
|
+
const isCurrent = opt.id === currentSort;
|
|
164
|
+
const checkMark = isCurrent ? " ✓" : "";
|
|
165
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { children: isFocused ? /* @__PURE__ */ jsxRuntimeExports.jsxs(Text, { color: "cyan", children: [
|
|
166
|
+
"❯ ",
|
|
167
|
+
opt.label,
|
|
168
|
+
checkMark
|
|
169
|
+
] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(Text, { children: [
|
|
170
|
+
" ",
|
|
171
|
+
opt.label,
|
|
172
|
+
checkMark
|
|
173
|
+
] }) }, opt.id);
|
|
174
|
+
}),
|
|
175
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { dimColor: true, children: "↑↓:select Enter:confirm Esc:cancel" }) })
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
function SearchableMultiSelect({
|
|
181
|
+
choices,
|
|
182
|
+
filterFn = defaultFilter,
|
|
183
|
+
visibleCount: visibleCountProp,
|
|
184
|
+
onSubmit,
|
|
185
|
+
onCancel,
|
|
186
|
+
placeholder = "Type to search...",
|
|
187
|
+
header,
|
|
188
|
+
footer,
|
|
189
|
+
defaultSort = "updated-desc"
|
|
190
|
+
}) {
|
|
191
|
+
const [query, setQuery] = useState("");
|
|
192
|
+
const [selectedIds, setSelectedIds] = useState(/* @__PURE__ */ new Set());
|
|
193
|
+
const [focusIndex, setFocusIndex] = useState(0);
|
|
194
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
195
|
+
const [sortOption, setSortOption] = useState(defaultSort);
|
|
196
|
+
const [showSortMenu, setShowSortMenu] = useState(false);
|
|
197
|
+
const [sortMenuIndex, setSortMenuIndex] = useState(0);
|
|
198
|
+
const { isFocused } = useFocus({ autoFocus: true });
|
|
199
|
+
const { stdout } = useStdout();
|
|
200
|
+
const terminalWidth = stdout?.columns ?? 80;
|
|
201
|
+
const terminalHeight = stdout?.rows ?? 24;
|
|
202
|
+
const contentWidth = terminalWidth - 8;
|
|
203
|
+
const itemHeight = 3;
|
|
204
|
+
const reservedLines = 10;
|
|
205
|
+
const calculatedVisibleCount = Math.max(
|
|
206
|
+
1,
|
|
207
|
+
Math.floor((terminalHeight - reservedLines) / itemHeight)
|
|
208
|
+
);
|
|
209
|
+
const visibleCount = visibleCountProp ?? calculatedVisibleCount;
|
|
210
|
+
const filteredChoices = useMemo(() => filterFn(query, choices), [query, choices, filterFn]);
|
|
211
|
+
const availableSortOptions = useMemo(
|
|
212
|
+
() => SORT_OPTIONS.filter((opt) => !opt.requiresQuery || query.trim().length > 0),
|
|
213
|
+
[query]
|
|
214
|
+
);
|
|
215
|
+
const sortedChoices = useMemo(
|
|
216
|
+
() => sortOption === "relevance" ? filteredChoices : sortChoices(filteredChoices, sortOption),
|
|
217
|
+
[filteredChoices, sortOption]
|
|
218
|
+
);
|
|
219
|
+
const maxIndex = sortedChoices.length - 1;
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
setFocusIndex(0);
|
|
222
|
+
setScrollOffset(0);
|
|
223
|
+
}, [query]);
|
|
224
|
+
useEffect(() => {
|
|
225
|
+
if (focusIndex < scrollOffset) {
|
|
226
|
+
setScrollOffset(focusIndex);
|
|
227
|
+
} else if (focusIndex >= scrollOffset + visibleCount) {
|
|
228
|
+
setScrollOffset(focusIndex - visibleCount + 1);
|
|
229
|
+
}
|
|
230
|
+
}, [focusIndex, scrollOffset, visibleCount]);
|
|
231
|
+
const visibleChoices = useMemo(
|
|
232
|
+
() => sortedChoices.slice(scrollOffset, scrollOffset + visibleCount),
|
|
233
|
+
[sortedChoices, scrollOffset, visibleCount]
|
|
234
|
+
);
|
|
235
|
+
const toggleSelection = useCallback(() => {
|
|
236
|
+
const currentChoice = sortedChoices[focusIndex];
|
|
237
|
+
if (!currentChoice) return;
|
|
238
|
+
setSelectedIds((prev) => {
|
|
239
|
+
const newSet = new Set(prev);
|
|
240
|
+
if (newSet.has(currentChoice.id)) {
|
|
241
|
+
newSet.delete(currentChoice.id);
|
|
242
|
+
} else {
|
|
243
|
+
newSet.add(currentChoice.id);
|
|
244
|
+
}
|
|
245
|
+
return newSet;
|
|
246
|
+
});
|
|
247
|
+
}, [sortedChoices, focusIndex]);
|
|
248
|
+
useInput(
|
|
249
|
+
(_input, key) => {
|
|
250
|
+
if (key.escape) {
|
|
251
|
+
setShowSortMenu(false);
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (key.return) {
|
|
255
|
+
const selected = availableSortOptions[sortMenuIndex];
|
|
256
|
+
if (selected) {
|
|
257
|
+
setSortOption(selected.id);
|
|
258
|
+
setFocusIndex(0);
|
|
259
|
+
setScrollOffset(0);
|
|
260
|
+
}
|
|
261
|
+
setShowSortMenu(false);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (key.upArrow) {
|
|
265
|
+
setSortMenuIndex((prev) => Math.max(0, prev - 1));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (key.downArrow) {
|
|
269
|
+
setSortMenuIndex((prev) => Math.min(availableSortOptions.length - 1, prev + 1));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
{ isActive: isFocused && showSortMenu }
|
|
274
|
+
);
|
|
275
|
+
useInput(
|
|
276
|
+
(input, key) => {
|
|
277
|
+
const action = parseKeyAction(input, key, visibleCount, maxIndex);
|
|
278
|
+
switch (action.type) {
|
|
279
|
+
case "cancel":
|
|
280
|
+
onCancel();
|
|
281
|
+
break;
|
|
282
|
+
case "submit":
|
|
283
|
+
onSubmit(choices.filter((c) => selectedIds.has(c.id)));
|
|
284
|
+
break;
|
|
285
|
+
case "navigate":
|
|
286
|
+
setFocusIndex((prev) => Math.max(0, Math.min(maxIndex, prev + action.delta)));
|
|
287
|
+
break;
|
|
288
|
+
case "toggle":
|
|
289
|
+
toggleSelection();
|
|
290
|
+
break;
|
|
291
|
+
case "backspace":
|
|
292
|
+
setQuery((prev) => prev.slice(0, -1));
|
|
293
|
+
break;
|
|
294
|
+
case "input":
|
|
295
|
+
setQuery((prev) => prev + action.char);
|
|
296
|
+
break;
|
|
297
|
+
case "sort":
|
|
298
|
+
setSortMenuIndex(availableSortOptions.findIndex((o) => o.id === sortOption));
|
|
299
|
+
setShowSortMenu(true);
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
{ isActive: isFocused && !showSortMenu }
|
|
304
|
+
);
|
|
305
|
+
const totalItems = sortedChoices.length;
|
|
306
|
+
const showScrollIndicator = totalItems > visibleCount;
|
|
307
|
+
const footerText = footer ?? (showScrollIndicator ? `↑↓:move Tab:select ^S:sort Enter:confirm (${focusIndex + 1}/${totalItems})` : "↑↓:move Tab:select ^S:sort Enter:confirm Esc:cancel");
|
|
308
|
+
return /* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { flexDirection: "column", paddingX: 1, children: [
|
|
309
|
+
header && /* @__PURE__ */ jsxRuntimeExports.jsx(Box, { marginBottom: 1, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { bold: true, color: "cyan", children: header }) }),
|
|
310
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { marginBottom: 1, paddingX: 1, borderStyle: "round", borderColor: "cyan", children: [
|
|
311
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Text, { color: "green", children: "❯ " }),
|
|
312
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Text, { children: query || /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { dimColor: true, children: placeholder }) }),
|
|
313
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Text, { color: "gray", children: "▎" })
|
|
314
|
+
] }),
|
|
315
|
+
/* @__PURE__ */ jsxRuntimeExports.jsxs(Box, { marginBottom: 1, paddingX: 1, justifyContent: "space-between", children: [
|
|
316
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Box, { children: selectedIds.size > 0 ? /* @__PURE__ */ jsxRuntimeExports.jsxs(Text, { color: "yellow", children: [
|
|
317
|
+
selectedIds.size,
|
|
318
|
+
" selected / ",
|
|
319
|
+
totalItems,
|
|
320
|
+
" results"
|
|
321
|
+
] }) : /* @__PURE__ */ jsxRuntimeExports.jsxs(Text, { dimColor: true, children: [
|
|
322
|
+
totalItems,
|
|
323
|
+
" results"
|
|
324
|
+
] }) }),
|
|
325
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Box, { children: /* @__PURE__ */ jsxRuntimeExports.jsxs(Text, { dimColor: true, children: [
|
|
326
|
+
"Sort: ",
|
|
327
|
+
getSortShortLabel(sortOption)
|
|
328
|
+
] }) })
|
|
329
|
+
] }),
|
|
330
|
+
showSortMenu ? /* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
331
|
+
SortMenu,
|
|
332
|
+
{
|
|
333
|
+
options: availableSortOptions,
|
|
334
|
+
focusIndex: sortMenuIndex,
|
|
335
|
+
currentSort: sortOption
|
|
336
|
+
}
|
|
337
|
+
) : /* @__PURE__ */ jsxRuntimeExports.jsxs(jsxRuntimeExports.Fragment, { children: [
|
|
338
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(ScrollIndicator, { direction: "up", count: scrollOffset, visible: showScrollIndicator }),
|
|
339
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
340
|
+
ChoiceList,
|
|
341
|
+
{
|
|
342
|
+
choices: visibleChoices,
|
|
343
|
+
selectedIds,
|
|
344
|
+
focusIndex,
|
|
345
|
+
scrollOffset,
|
|
346
|
+
contentWidth
|
|
347
|
+
}
|
|
348
|
+
),
|
|
349
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(
|
|
350
|
+
ScrollIndicator,
|
|
351
|
+
{
|
|
352
|
+
direction: "down",
|
|
353
|
+
count: totalItems - scrollOffset - visibleCount,
|
|
354
|
+
visible: showScrollIndicator
|
|
355
|
+
}
|
|
356
|
+
),
|
|
357
|
+
/* @__PURE__ */ jsxRuntimeExports.jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsxRuntimeExports.jsx(Text, { dimColor: true, children: footerText }) })
|
|
358
|
+
] })
|
|
359
|
+
] });
|
|
360
|
+
}
|
|
361
|
+
function formatSingleAuthor(author) {
|
|
362
|
+
if (author.literal) {
|
|
363
|
+
return author.literal;
|
|
364
|
+
}
|
|
365
|
+
if (author.family) {
|
|
366
|
+
if (author.given) {
|
|
367
|
+
const initial = author.given.charAt(0).toUpperCase();
|
|
368
|
+
return `${author.family}, ${initial}.`;
|
|
369
|
+
}
|
|
370
|
+
return author.family;
|
|
371
|
+
}
|
|
372
|
+
return "";
|
|
373
|
+
}
|
|
374
|
+
function formatAuthors(authors) {
|
|
375
|
+
if (!authors || authors.length === 0) {
|
|
376
|
+
return "";
|
|
377
|
+
}
|
|
378
|
+
if (authors.length > 3) {
|
|
379
|
+
const first = authors[0];
|
|
380
|
+
if (!first) {
|
|
381
|
+
return "";
|
|
382
|
+
}
|
|
383
|
+
return `${formatSingleAuthor(first)}, et al.`;
|
|
384
|
+
}
|
|
385
|
+
const formatted = authors.map(formatSingleAuthor);
|
|
386
|
+
if (formatted.length === 1) {
|
|
387
|
+
return formatted[0] ?? "";
|
|
388
|
+
}
|
|
389
|
+
const allButLast = formatted.slice(0, -1).join(", ");
|
|
390
|
+
const last = formatted[formatted.length - 1] ?? "";
|
|
391
|
+
return `${allButLast}, & ${last}`;
|
|
392
|
+
}
|
|
393
|
+
export {
|
|
394
|
+
SearchableMultiSelect as S,
|
|
395
|
+
formatAuthors as f
|
|
396
|
+
};
|
|
397
|
+
//# sourceMappingURL=format-C6FA-7hE.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"format-C6FA-7hE.js","sources":["../../src/features/interactive/components/SearchableMultiSelect.tsx","../../src/features/interactive/format.ts"],"sourcesContent":["/**\n * SearchableMultiSelect - A searchable multi-select component for React Ink\n *\n * This component combines text input for filtering with multi-select functionality,\n * which is not available in ink-ui out of the box.\n */\n\nimport { Box, Text, useFocus, useInput, useStdout } from \"ink\";\nimport type React from \"react\";\nimport { useCallback, useEffect, useMemo, useState } from \"react\";\n\nexport interface Choice<T = unknown> {\n /** Unique identifier for the choice */\n id: string;\n /** Primary text (title) - displayed in cyan when focused */\n title: string;\n /** Secondary text (e.g., authors) */\n subtitle?: string;\n /** Tertiary text (e.g., year, type, identifiers) */\n meta?: string;\n /** Associated value */\n value: T;\n /** Date when the item was last updated (custom.timestamp) */\n updatedDate?: Date;\n /** Date when the item was created (custom.created_at) */\n createdDate?: Date;\n /** Date when the item was published (for sorting) */\n publishedDate?: Date;\n}\n\n/** Sort option identifier */\nexport type SortOption =\n | \"updated-desc\"\n | \"updated-asc\"\n | \"created-desc\"\n | \"created-asc\"\n | \"published-desc\"\n | \"published-asc\"\n | \"relevance\";\n\n/** Sort option configuration */\ninterface SortOptionConfig {\n id: SortOption;\n label: string;\n requiresQuery: boolean;\n}\n\nexport interface SearchableMultiSelectProps<T> {\n /** Available choices */\n choices: Choice<T>[];\n /** Filter function for search */\n filterFn?: (query: string, choices: Choice<T>[]) => Choice<T>[];\n /** Maximum visible items */\n visibleCount?: number;\n /** Callback when selection is confirmed */\n onSubmit: (selected: Choice<T>[]) => void;\n /** Callback when cancelled */\n onCancel: () => void;\n /** Placeholder text for search input */\n placeholder?: string;\n /** Header text */\n header?: string;\n /** Footer text */\n footer?: string;\n /** Default sort option */\n defaultSort?: SortOption;\n}\n\n/** All available sort options */\nconst SORT_OPTIONS: SortOptionConfig[] = [\n { id: \"updated-desc\", label: \"Updated (newest first)\", requiresQuery: false },\n { id: \"updated-asc\", label: \"Updated (oldest first)\", requiresQuery: false },\n { id: \"created-desc\", label: \"Created (newest first)\", requiresQuery: false },\n { id: \"created-asc\", label: \"Created (oldest first)\", requiresQuery: false },\n { id: \"published-desc\", label: \"Published (newest first)\", requiresQuery: false },\n { id: \"published-asc\", label: \"Published (oldest first)\", requiresQuery: false },\n { id: \"relevance\", label: \"Relevance\", requiresQuery: true },\n];\n\n/** Get short label for status bar */\nfunction getSortShortLabel(sortOption: SortOption): string {\n switch (sortOption) {\n case \"updated-desc\":\n return \"Updated ↓\";\n case \"updated-asc\":\n return \"Updated ↑\";\n case \"created-desc\":\n return \"Created ↓\";\n case \"created-asc\":\n return \"Created ↑\";\n case \"published-desc\":\n return \"Published ↓\";\n case \"published-asc\":\n return \"Published ↑\";\n case \"relevance\":\n return \"Relevance\";\n }\n}\n\n/** Create a date comparator for sorting */\nfunction createDateComparator<T>(\n getDate: (choice: Choice<T>) => Date | undefined,\n descending: boolean\n): (a: Choice<T>, b: Choice<T>) => number {\n return (a, b) => {\n const dateA = getDate(a)?.getTime() ?? 0;\n const dateB = getDate(b)?.getTime() ?? 0;\n return descending ? dateB - dateA : dateA - dateB;\n };\n}\n\n/**\n * Sort choices by the given option\n */\nfunction sortChoices<T>(choices: Choice<T>[], sortOption: SortOption): Choice<T>[] {\n if (sortOption === \"relevance\") return [...choices];\n\n const sorted = [...choices];\n const comparators: Record<\n Exclude<SortOption, \"relevance\">,\n (a: Choice<T>, b: Choice<T>) => number\n > = {\n \"updated-desc\": createDateComparator((c) => c.updatedDate, true),\n \"updated-asc\": createDateComparator((c) => c.updatedDate, false),\n \"created-desc\": createDateComparator((c) => c.createdDate, true),\n \"created-asc\": createDateComparator((c) => c.createdDate, false),\n \"published-desc\": createDateComparator((c) => c.publishedDate, true),\n \"published-asc\": createDateComparator((c) => c.publishedDate, false),\n };\n\n return sorted.sort(comparators[sortOption]);\n}\n\n/**\n * Default filter function - case-insensitive substring match\n */\nfunction defaultFilter<T>(query: string, choices: Choice<T>[]): Choice<T>[] {\n if (!query.trim()) return choices;\n const lowerQuery = query.toLowerCase();\n return choices.filter(\n (choice) =>\n choice.title.toLowerCase().includes(lowerQuery) ||\n choice.subtitle?.toLowerCase().includes(lowerQuery) ||\n choice.meta?.toLowerCase().includes(lowerQuery)\n );\n}\n\n/**\n * Truncate text to fit within maxWidth\n */\nfunction truncate(text: string, maxWidth: number): string {\n if (text.length <= maxWidth) return text;\n return `${text.slice(0, maxWidth - 1)}…`;\n}\n\n/**\n * Key handler result type\n */\ntype KeyAction =\n | { type: \"cancel\" }\n | { type: \"submit\" }\n | { type: \"navigate\"; delta: number }\n | { type: \"toggle\" }\n | { type: \"backspace\" }\n | { type: \"input\"; char: string }\n | { type: \"sort\" }\n | { type: \"none\" };\n\n/** Key state type for parsing */\ninterface KeyState {\n escape: boolean;\n return: boolean;\n upArrow: boolean;\n downArrow: boolean;\n pageUp: boolean;\n pageDown: boolean;\n tab: boolean;\n backspace: boolean;\n delete: boolean;\n ctrl: boolean;\n meta: boolean;\n}\n\n/** Get navigation delta from key state */\nfunction getNavigationDelta(\n key: KeyState,\n input: string,\n visibleCount: number,\n maxIndex: number\n): number | null {\n if (key.upArrow) return -1;\n if (key.downArrow) return 1;\n if (key.pageUp) return -visibleCount;\n if (key.pageDown) return visibleCount;\n if (key.ctrl && input === \"a\") return -maxIndex - 1;\n if (key.ctrl && input === \"e\") return maxIndex + 1;\n return null;\n}\n\n/**\n * Parse key input into action\n */\nfunction parseKeyAction(\n input: string,\n key: KeyState,\n visibleCount: number,\n maxIndex: number\n): KeyAction {\n if (key.escape) return { type: \"cancel\" };\n if (key.return) return { type: \"submit\" };\n\n const navDelta = getNavigationDelta(key, input, visibleCount, maxIndex);\n if (navDelta !== null) return { type: \"navigate\", delta: navDelta };\n\n if (key.ctrl && input === \"s\") return { type: \"sort\" };\n if (key.tab) return { type: \"toggle\" };\n if (key.backspace || key.delete) return { type: \"backspace\" };\n if (input && !key.ctrl && !key.meta) return { type: \"input\", char: input };\n return { type: \"none\" };\n}\n\n/**\n * Choice item component - renders a single multi-line choice\n */\nfunction ChoiceItem<T>({\n choice,\n isSelected,\n isFocused,\n contentWidth,\n}: {\n choice: Choice<T>;\n isSelected: boolean;\n isFocused: boolean;\n contentWidth: number;\n}): React.ReactElement {\n const indent = \" \"; // 6 spaces to align with title (after indicator + checkbox)\n\n return (\n <Box flexDirection=\"column\" paddingY={0}>\n {/* Row 1: Focus indicator + Checkbox + Title */}\n <Box flexDirection=\"row\">\n {/* Focus indicator - fixed width */}\n <Box width={2}>\n <Text color=\"cyan\">{isFocused ? \"❯\" : \" \"}</Text>\n </Box>\n {/* Selection checkbox */}\n <Box width={2}>\n <Text color={isSelected ? \"green\" : \"gray\"}>{isSelected ? \"◉\" : \"○\"}</Text>\n </Box>\n <Box width={1}>\n <Text> </Text>\n </Box>\n {/* Title */}\n <Box>\n {isFocused ? (\n <Text color=\"cyan\" bold>\n {truncate(choice.title, contentWidth)}\n </Text>\n ) : (\n <Text color=\"blue\">{truncate(choice.title, contentWidth)}</Text>\n )}\n </Box>\n </Box>\n\n {/* Row 2: Subtitle (authors) */}\n {choice.subtitle && (\n <Box>\n <Text dimColor>\n {indent}\n {truncate(choice.subtitle, contentWidth)}\n </Text>\n </Box>\n )}\n\n {/* Row 3: Meta (year, type, identifiers) */}\n {choice.meta && (\n <Box>\n <Text dimColor>\n {indent}\n {truncate(choice.meta, contentWidth)}\n </Text>\n </Box>\n )}\n </Box>\n );\n}\n\n/**\n * Scroll indicator component\n */\nfunction ScrollIndicator({\n direction,\n count,\n visible,\n}: {\n direction: \"up\" | \"down\";\n count: number;\n visible: boolean;\n}): React.ReactElement | null {\n if (!visible) return null;\n const arrow = direction === \"up\" ? \"↑\" : \"↓\";\n const label = direction === \"up\" ? \"more above\" : \"more below\";\n return (\n <Box height={1}>\n {count > 0 ? (\n <Text dimColor>\n {\" \"}\n {arrow} {count} {label}\n </Text>\n ) : (\n <Text> </Text>\n )}\n </Box>\n );\n}\n\n/**\n * Choice list component\n */\nfunction ChoiceList<T>({\n choices,\n selectedIds,\n focusIndex,\n scrollOffset,\n contentWidth,\n}: {\n choices: Choice<T>[];\n selectedIds: Set<string>;\n focusIndex: number;\n scrollOffset: number;\n contentWidth: number;\n}): React.ReactElement {\n if (choices.length === 0) {\n return (\n <Box paddingY={1}>\n <Text dimColor>No results found</Text>\n </Box>\n );\n }\n\n return (\n <Box flexDirection=\"column\">\n {choices.map((choice, visibleIndex) => {\n const actualIndex = scrollOffset + visibleIndex;\n return (\n <Box key={choice.id} flexDirection=\"row\">\n <ChoiceItem\n choice={choice}\n isSelected={selectedIds.has(choice.id)}\n isFocused={actualIndex === focusIndex}\n contentWidth={contentWidth}\n />\n </Box>\n );\n })}\n </Box>\n );\n}\n\n/**\n * Sort menu component\n */\nfunction SortMenu({\n options,\n focusIndex,\n currentSort,\n}: {\n options: SortOptionConfig[];\n focusIndex: number;\n currentSort: SortOption;\n}): React.ReactElement {\n return (\n <Box\n flexDirection=\"column\"\n marginBottom={1}\n paddingX={1}\n borderStyle=\"round\"\n borderColor=\"yellow\"\n >\n <Box marginBottom={1}>\n <Text bold color=\"yellow\">\n Sort by:\n </Text>\n </Box>\n {options.map((opt, index) => {\n const isFocused = index === focusIndex;\n const isCurrent = opt.id === currentSort;\n const checkMark = isCurrent ? \" ✓\" : \"\";\n return (\n <Box key={opt.id}>\n {isFocused ? (\n <Text color=\"cyan\">\n ❯ {opt.label}\n {checkMark}\n </Text>\n ) : (\n <Text>\n {\" \"}\n {opt.label}\n {checkMark}\n </Text>\n )}\n </Box>\n );\n })}\n <Box marginTop={1}>\n <Text dimColor>↑↓:select Enter:confirm Esc:cancel</Text>\n </Box>\n </Box>\n );\n}\n\nexport function SearchableMultiSelect<T>({\n choices,\n filterFn = defaultFilter,\n visibleCount: visibleCountProp,\n onSubmit,\n onCancel,\n placeholder = \"Type to search...\",\n header,\n footer,\n defaultSort = \"updated-desc\",\n}: SearchableMultiSelectProps<T>): React.ReactElement {\n const [query, setQuery] = useState(\"\");\n const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());\n const [focusIndex, setFocusIndex] = useState(0);\n const [scrollOffset, setScrollOffset] = useState(0);\n const [sortOption, setSortOption] = useState<SortOption>(defaultSort);\n const [showSortMenu, setShowSortMenu] = useState(false);\n const [sortMenuIndex, setSortMenuIndex] = useState(0);\n const { isFocused } = useFocus({ autoFocus: true });\n const { stdout } = useStdout();\n\n // Get terminal dimensions\n const terminalWidth = stdout?.columns ?? 80;\n const terminalHeight = stdout?.rows ?? 24;\n const contentWidth = terminalWidth - 8; // Reserve space for checkbox and padding\n\n // Calculate visible count based on terminal height\n // Each item takes 3 lines (title + subtitle + meta)\n // Reserve lines for: header(2) + search(3) + status(1) + scroll indicators(2) + footer(2) = 10\n const itemHeight = 3;\n const reservedLines = 10;\n const calculatedVisibleCount = Math.max(\n 1,\n Math.floor((terminalHeight - reservedLines) / itemHeight)\n );\n const visibleCount = visibleCountProp ?? calculatedVisibleCount;\n\n // Filter choices based on query\n const filteredChoices = useMemo(() => filterFn(query, choices), [query, choices, filterFn]);\n\n // Get available sort options (relevance only when query exists)\n const availableSortOptions = useMemo(\n () => SORT_OPTIONS.filter((opt) => !opt.requiresQuery || query.trim().length > 0),\n [query]\n );\n\n // Apply sorting (skip for relevance as filter function handles it)\n const sortedChoices = useMemo(\n () => (sortOption === \"relevance\" ? filteredChoices : sortChoices(filteredChoices, sortOption)),\n [filteredChoices, sortOption]\n );\n\n const maxIndex = sortedChoices.length - 1;\n\n // Reset focus and scroll when query changes\n // biome-ignore lint/correctness/useExhaustiveDependencies: intentionally reset on query change\n useEffect(() => {\n setFocusIndex(0);\n setScrollOffset(0);\n }, [query]);\n\n // Adjust scroll offset when focus changes\n useEffect(() => {\n if (focusIndex < scrollOffset) {\n setScrollOffset(focusIndex);\n } else if (focusIndex >= scrollOffset + visibleCount) {\n setScrollOffset(focusIndex - visibleCount + 1);\n }\n }, [focusIndex, scrollOffset, visibleCount]);\n\n // Get visible choices\n const visibleChoices = useMemo(\n () => sortedChoices.slice(scrollOffset, scrollOffset + visibleCount),\n [sortedChoices, scrollOffset, visibleCount]\n );\n\n // Toggle selection\n const toggleSelection = useCallback(() => {\n const currentChoice = sortedChoices[focusIndex];\n if (!currentChoice) return;\n setSelectedIds((prev) => {\n const newSet = new Set(prev);\n if (newSet.has(currentChoice.id)) {\n newSet.delete(currentChoice.id);\n } else {\n newSet.add(currentChoice.id);\n }\n return newSet;\n });\n }, [sortedChoices, focusIndex]);\n\n // Handle keyboard input for sort menu\n useInput(\n (_input, key) => {\n if (key.escape) {\n setShowSortMenu(false);\n return;\n }\n if (key.return) {\n const selected = availableSortOptions[sortMenuIndex];\n if (selected) {\n setSortOption(selected.id);\n setFocusIndex(0);\n setScrollOffset(0);\n }\n setShowSortMenu(false);\n return;\n }\n if (key.upArrow) {\n setSortMenuIndex((prev) => Math.max(0, prev - 1));\n return;\n }\n if (key.downArrow) {\n setSortMenuIndex((prev) => Math.min(availableSortOptions.length - 1, prev + 1));\n return;\n }\n },\n { isActive: isFocused && showSortMenu }\n );\n\n // Handle keyboard input for main list\n useInput(\n (input, key) => {\n const action = parseKeyAction(input, key, visibleCount, maxIndex);\n switch (action.type) {\n case \"cancel\":\n onCancel();\n break;\n case \"submit\":\n onSubmit(choices.filter((c) => selectedIds.has(c.id)));\n break;\n case \"navigate\":\n setFocusIndex((prev) => Math.max(0, Math.min(maxIndex, prev + action.delta)));\n break;\n case \"toggle\":\n toggleSelection();\n break;\n case \"backspace\":\n setQuery((prev) => prev.slice(0, -1));\n break;\n case \"input\":\n setQuery((prev) => prev + action.char);\n break;\n case \"sort\":\n setSortMenuIndex(availableSortOptions.findIndex((o) => o.id === sortOption));\n setShowSortMenu(true);\n break;\n }\n },\n { isActive: isFocused && !showSortMenu }\n );\n\n const totalItems = sortedChoices.length;\n const showScrollIndicator = totalItems > visibleCount;\n\n const footerText =\n footer ??\n (showScrollIndicator\n ? `↑↓:move Tab:select ^S:sort Enter:confirm (${focusIndex + 1}/${totalItems})`\n : \"↑↓:move Tab:select ^S:sort Enter:confirm Esc:cancel\");\n\n return (\n <Box flexDirection=\"column\" paddingX={1}>\n {/* Header */}\n {header && (\n <Box marginBottom={1}>\n <Text bold color=\"cyan\">\n {header}\n </Text>\n </Box>\n )}\n\n {/* Search input */}\n <Box marginBottom={1} paddingX={1} borderStyle=\"round\" borderColor=\"cyan\">\n <Text color=\"green\">❯ </Text>\n <Text>{query || <Text dimColor>{placeholder}</Text>}</Text>\n <Text color=\"gray\">▎</Text>\n </Box>\n\n {/* Status bar */}\n <Box marginBottom={1} paddingX={1} justifyContent=\"space-between\">\n <Box>\n {selectedIds.size > 0 ? (\n <Text color=\"yellow\">\n {selectedIds.size} selected / {totalItems} results\n </Text>\n ) : (\n <Text dimColor>{totalItems} results</Text>\n )}\n </Box>\n <Box>\n <Text dimColor>Sort: {getSortShortLabel(sortOption)}</Text>\n </Box>\n </Box>\n\n {/* Sort menu (replaces list when shown) */}\n {showSortMenu ? (\n <SortMenu\n options={availableSortOptions}\n focusIndex={sortMenuIndex}\n currentSort={sortOption}\n />\n ) : (\n <>\n <ScrollIndicator direction=\"up\" count={scrollOffset} visible={showScrollIndicator} />\n <ChoiceList\n choices={visibleChoices}\n selectedIds={selectedIds}\n focusIndex={focusIndex}\n scrollOffset={scrollOffset}\n contentWidth={contentWidth}\n />\n <ScrollIndicator\n direction=\"down\"\n count={totalItems - scrollOffset - visibleCount}\n visible={showScrollIndicator}\n />\n <Box marginTop={1}>\n <Text dimColor>{footerText}</Text>\n </Box>\n </>\n )}\n </Box>\n );\n}\n","/**\n * Display format functions for interactive search\n */\n\nimport type { CslItem } from \"../../core/csl-json/types.js\";\n\n/**\n * CSL name type (author structure)\n */\ntype CslName = NonNullable<CslItem[\"author\"]>[number];\n\n/**\n * Format a single author name\n * - Personal: \"Smith, J.\" (family + initial of given)\n * - Institutional: \"World Health Organization\" (literal)\n */\nfunction formatSingleAuthor(author: CslName): string {\n if (author.literal) {\n return author.literal;\n }\n if (author.family) {\n if (author.given) {\n const initial = author.given.charAt(0).toUpperCase();\n return `${author.family}, ${initial}.`;\n }\n return author.family;\n }\n return \"\";\n}\n\n/**\n * Format author list for display\n * - Single author: \"Smith, J.\"\n * - Two authors: \"Smith, J., & Doe, A.\"\n * - Three authors: \"Smith, J., Doe, A., & Johnson, B.\"\n * - More than three: \"Smith, J., et al.\"\n *\n * @param authors - Array of CSL author objects\n * @returns Formatted author string\n */\nexport function formatAuthors(authors: CslName[] | undefined): string {\n if (!authors || authors.length === 0) {\n return \"\";\n }\n\n if (authors.length > 3) {\n const first = authors[0];\n if (!first) {\n return \"\";\n }\n return `${formatSingleAuthor(first)}, et al.`;\n }\n\n const formatted = authors.map(formatSingleAuthor);\n if (formatted.length === 1) {\n return formatted[0] ?? \"\";\n }\n\n // Join all but last with \", \" and append \"& \" + last\n const allButLast = formatted.slice(0, -1).join(\", \");\n const last = formatted[formatted.length - 1] ?? \"\";\n return `${allButLast}, & ${last}`;\n}\n\n/**\n * Truncate title to fit terminal width\n *\n * @param title - Title string\n * @param maxWidth - Maximum display width\n * @returns Truncated title with ellipsis if needed\n */\nexport function formatTitle(title: string | undefined, maxWidth: number): string {\n if (!title) {\n return \"\";\n }\n\n if (title.length <= maxWidth) {\n return title;\n }\n\n // Truncate and add ellipsis, keeping total length at maxWidth\n return `${title.slice(0, maxWidth - 3)}...`;\n}\n\n/**\n * Format identifiers (DOI, PMID, PMCID, ISBN) for display\n *\n * @param item - CSL item\n * @returns Formatted identifier string (e.g., \"DOI: 10.1000/example | PMID: 12345678\")\n */\nexport function formatIdentifiers(item: CslItem): string {\n const identifiers: string[] = [];\n\n if (item.DOI) {\n identifiers.push(`DOI: ${item.DOI}`);\n }\n if (item.PMID) {\n identifiers.push(`PMID: ${item.PMID}`);\n }\n if (item.PMCID) {\n identifiers.push(`PMCID: ${item.PMCID}`);\n }\n if (item.ISBN) {\n identifiers.push(`ISBN: ${item.ISBN}`);\n }\n\n return identifiers.join(\" | \");\n}\n\n/**\n * Extract year from CSL item\n */\nfunction extractYear(item: CslItem): number | undefined {\n const dateParts = item.issued?.[\"date-parts\"];\n if (!dateParts || dateParts.length === 0) {\n return undefined;\n }\n const firstDatePart = dateParts[0];\n if (!firstDatePart || firstDatePart.length === 0) {\n return undefined;\n }\n return firstDatePart[0];\n}\n\n/**\n * Compose a complete search result line\n *\n * Format:\n * ```\n * [1] Smith, J., & Doe, A. (2020)\n * Machine learning in medicine: A comprehensive review\n * DOI: 10.1000/example | PMID: 12345678\n * ```\n *\n * @param item - CSL item\n * @param index - Display index (1-based)\n * @param terminalWidth - Terminal width for title truncation\n * @returns Multi-line formatted string\n */\nexport function formatSearchResult(item: CslItem, index: number, terminalWidth: number): string {\n const lines: string[] = [];\n\n // Line 1: [index] Authors (year)\n const authors = formatAuthors(item.author);\n const year = extractYear(item);\n const yearPart = year !== undefined ? ` (${year})` : \"\";\n const line1 = `[${index}] ${authors}${yearPart}`;\n lines.push(line1);\n\n // Line 2: Title (indented, truncated)\n const indent = \" \";\n const titleMaxWidth = terminalWidth - indent.length;\n const title = formatTitle(item.title, titleMaxWidth);\n if (title) {\n lines.push(`${indent}${title}`);\n }\n\n // Line 3: Identifiers (indented)\n const identifiers = formatIdentifiers(item);\n if (identifiers) {\n lines.push(`${indent}${identifiers}`);\n }\n\n return lines.join(\"\\n\");\n}\n"],"names":["jsxs","jsx","Fragment"],"mappings":"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"}
|