@ncukondo/reference-manager 0.16.0 → 0.16.1
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/format-BPeCbpFG.js +924 -0
- package/dist/chunks/format-BPeCbpFG.js.map +1 -0
- package/dist/chunks/{index-CEYp8OSj.js → index-7bmkSgzh.js} +3 -3
- package/dist/chunks/{index-CEYp8OSj.js.map → index-7bmkSgzh.js.map} +1 -1
- package/dist/chunks/{index-Bzl4_3Ki.js → index-BzHJQ7Or.js} +2 -2
- package/dist/chunks/index-BzHJQ7Or.js.map +1 -0
- package/dist/chunks/{index-4SVOiraD.js → index-Ddq16wFm.js} +15 -15
- package/dist/chunks/{index-4SVOiraD.js.map → index-Ddq16wFm.js.map} +1 -1
- package/dist/chunks/{reference-select-CgM-RBIa.js → reference-select-Cs3hsy-1.js} +2 -2
- package/dist/chunks/{reference-select-CgM-RBIa.js.map → reference-select-Cs3hsy-1.js.map} +1 -1
- package/dist/cli.js +1 -1
- package/dist/features/interactive/components/SearchableMultiSelect.d.ts.map +1 -1
- package/package.json +1 -1
- package/dist/chunks/format-C6FA-7hE.js +0 -397
- package/dist/chunks/format-C6FA-7hE.js.map +0 -1
- package/dist/chunks/index-Bzl4_3Ki.js.map +0 -1
|
@@ -2,7 +2,7 @@ import { t as tokenize, s as search } from "./file-watcher-B_WpVHSV.js";
|
|
|
2
2
|
import { render, useApp } from "ink";
|
|
3
3
|
import { createElement } from "react";
|
|
4
4
|
import { restoreStdinAfterInk } from "./alternate-screen-DcxkOKfW.js";
|
|
5
|
-
import { f as formatAuthors, S as SearchableMultiSelect } from "./format-
|
|
5
|
+
import { f as formatAuthors, S as SearchableMultiSelect } from "./format-BPeCbpFG.js";
|
|
6
6
|
import "./jsx-runtime-Q5cUjSur.js";
|
|
7
7
|
import { checkTTY } from "./tty-BMyaEOhX.js";
|
|
8
8
|
function getTerminalHeight() {
|
|
@@ -211,4 +211,4 @@ export {
|
|
|
211
211
|
selectReferenceItemsOrExit,
|
|
212
212
|
selectReferencesOrExit
|
|
213
213
|
};
|
|
214
|
-
//# sourceMappingURL=reference-select-
|
|
214
|
+
//# sourceMappingURL=reference-select-Cs3hsy-1.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reference-select-CgM-RBIa.js","sources":["../../src/features/interactive/search-prompt.ts","../../src/features/interactive/reference-select.ts"],"sourcesContent":["/**\n * Interactive search prompt using React Ink\n *\n * Provides real-time incremental search with multiple selection support.\n */\n\nimport { render, useApp } from \"ink\";\nimport type React from \"react\";\nimport { createElement } from \"react\";\nimport type { CslItem } from \"../../core/csl-json/types.js\";\nimport type { SearchResult } from \"../search/types.js\";\nimport { restoreStdinAfterInk } from \"./alternate-screen.js\";\nimport { type Choice, SearchableMultiSelect, type SortOption } from \"./components/index.js\";\nimport { formatAuthors } from \"./format.js\";\n\n/**\n * Configuration for the search prompt\n */\nexport interface SearchPromptConfig {\n /** Maximum number of results to display */\n limit: number;\n /** Debounce delay in milliseconds (not used in Ink version, kept for API compatibility) */\n debounceMs: number;\n}\n\n/**\n * Search function type for filtering references\n */\nexport type SearchFunction = (query: string) => SearchResult[];\n\n/**\n * Result from the search prompt\n */\nexport interface SearchPromptResult {\n /** Selected references */\n selected: CslItem[];\n /** Whether the prompt was cancelled */\n cancelled: boolean;\n}\n\n/**\n * Gets terminal width, falling back to 80 if not available\n */\nexport function getTerminalWidth(): number {\n return process.stdout.columns ?? 80;\n}\n\n/**\n * Gets terminal height, falling back to 24 if not available\n */\nexport function getTerminalHeight(): number {\n return process.stdout.rows ?? 24;\n}\n\n/**\n * Calculates the effective limit for the autocomplete list\n * based on terminal height to prevent input field from being hidden.\n * Reserves space for: prompt header (1), input line (1), footer hint (1), and padding (2)\n * Each item displays up to 3 lines (author/year, title, identifiers)\n */\nexport function calculateEffectiveLimit(configLimit: number): number {\n const terminalHeight = getTerminalHeight();\n // Reserve lines for: header(2) + search box(3) + status(1) + scroll indicators(2) + footer(2) = 10\n const reservedLines = 10;\n const linesPerItem = 3; // each search result shows up to 3 lines\n const availableLines = terminalHeight - reservedLines;\n const maxVisibleChoices = Math.max(1, Math.floor(availableLines / linesPerItem));\n return configLimit > 0 ? Math.min(configLimit, maxVisibleChoices) : maxVisibleChoices;\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) return undefined;\n const firstDatePart = dateParts[0];\n if (!firstDatePart || firstDatePart.length === 0) return undefined;\n return firstDatePart[0];\n}\n\n/**\n * Extract published date from CSL item\n */\nfunction extractPublishedDate(item: CslItem): Date | undefined {\n const dateParts = item.issued?.[\"date-parts\"];\n if (!dateParts || dateParts.length === 0) return undefined;\n const firstDatePart = dateParts[0];\n if (!firstDatePart || firstDatePart.length === 0) return undefined;\n const [year, month = 1, day = 1] = firstDatePart;\n if (year === undefined) return undefined;\n return new Date(year, month - 1, day);\n}\n\n/**\n * Extract updated date from CSL item (from custom.timestamp)\n */\nfunction extractUpdatedDate(item: CslItem): Date | undefined {\n const dateStr = item.custom?.timestamp;\n if (!dateStr || typeof dateStr !== \"string\") return undefined;\n const date = new Date(dateStr);\n return Number.isNaN(date.getTime()) ? undefined : date;\n}\n\n/**\n * Extract created date from CSL item (from custom.created_at)\n */\nfunction extractCreatedDate(item: CslItem): Date | undefined {\n const dateStr = item.custom?.created_at;\n if (!dateStr || typeof dateStr !== \"string\") return undefined;\n const date = new Date(dateStr);\n return Number.isNaN(date.getTime()) ? undefined : date;\n}\n\n/**\n * Format identifiers for meta line\n */\nfunction formatIdentifiers(item: CslItem): string {\n const parts: string[] = [];\n if (item.DOI) parts.push(`DOI: ${item.DOI}`);\n if (item.PMID) parts.push(`PMID: ${item.PMID}`);\n if (item.PMCID) parts.push(`PMCID: ${item.PMCID}`);\n if (item.ISBN) parts.push(`ISBN: ${item.ISBN}`);\n return parts.join(\" · \");\n}\n\n/**\n * Format item type for display\n */\nfunction formatType(type: string): string {\n const typeMap: Record<string, string> = {\n \"article-journal\": \"Journal article\",\n \"article-magazine\": \"Magazine article\",\n \"article-newspaper\": \"Newspaper article\",\n book: \"Book\",\n chapter: \"Book chapter\",\n \"paper-conference\": \"Conference paper\",\n thesis: \"Thesis\",\n report: \"Report\",\n webpage: \"Web page\",\n };\n return typeMap[type] ?? type;\n}\n\n/**\n * Convert CslItem to Choice for SearchableMultiSelect\n */\nfunction toChoice(item: CslItem): Choice<CslItem> {\n const authors = formatAuthors(item.author);\n const year = extractYear(item);\n const identifiers = formatIdentifiers(item);\n const itemType = formatType(item.type);\n\n // Build meta line: Year · Type · Identifiers\n const metaParts: string[] = [];\n if (year) metaParts.push(String(year));\n metaParts.push(itemType);\n if (identifiers) metaParts.push(identifiers);\n\n const updatedDate = extractUpdatedDate(item);\n const createdDate = extractCreatedDate(item);\n const publishedDate = extractPublishedDate(item);\n\n return {\n id: item.id,\n title: item.title ?? \"(No title)\",\n subtitle: authors || \"(No authors)\",\n meta: metaParts.join(\" · \"),\n value: item,\n ...(updatedDate && { updatedDate }),\n ...(createdDate && { createdDate }),\n ...(publishedDate && { publishedDate }),\n };\n}\n\n/**\n * Props for the SearchPromptApp component\n */\ninterface SearchPromptAppProps {\n choices: Choice<CslItem>[];\n filterFn: (query: string, choices: Choice<CslItem>[]) => Choice<CslItem>[];\n visibleCount: number;\n defaultSort: SortOption;\n onSubmit: (selected: Choice<CslItem>[]) => void;\n onCancel: () => void;\n}\n\n/**\n * SearchPromptApp component - wraps SearchableMultiSelect for search prompt\n */\nfunction SearchPromptApp({\n choices,\n filterFn,\n visibleCount,\n defaultSort,\n onSubmit,\n onCancel,\n}: SearchPromptAppProps): React.ReactElement {\n const { exit } = useApp();\n\n const handleSubmit = (selected: Choice<CslItem>[]): void => {\n onSubmit(selected);\n exit();\n };\n\n const handleCancel = (): void => {\n onCancel();\n exit();\n };\n\n return createElement(SearchableMultiSelect<CslItem>, {\n choices,\n filterFn,\n visibleCount,\n onSubmit: handleSubmit,\n onCancel: handleCancel,\n header: \"Search references\",\n placeholder: \"Type to search...\",\n defaultSort,\n });\n}\n\n/**\n * Creates and runs an interactive search prompt\n */\nexport async function runSearchPrompt(\n allReferences: CslItem[],\n searchFn: SearchFunction,\n config: SearchPromptConfig,\n _initialQuery = \"\" // kept for API compatibility, not used in Ink version\n): Promise<SearchPromptResult> {\n // Convert references to choices\n const choices = allReferences.map(toChoice);\n\n // Calculate effective visible count\n const effectiveLimit = calculateEffectiveLimit(config.limit);\n\n // Create filter function using the provided search function\n const filterFn = (query: string, choices: Choice<CslItem>[]): Choice<CslItem>[] => {\n if (!query.trim()) return choices;\n\n const results = searchFn(query);\n return results.map((r) => toChoice(r.reference));\n };\n\n // Create a promise to capture the result\n return new Promise<SearchPromptResult>((resolve) => {\n let result: SearchPromptResult = { selected: [], cancelled: true };\n\n const handleSubmit = (selected: Choice<CslItem>[]): void => {\n result = {\n selected: selected.map((c) => c.value),\n cancelled: false,\n };\n };\n\n const handleCancel = (): void => {\n result = {\n selected: [],\n cancelled: true,\n };\n };\n\n // Render the Ink app\n const { waitUntilExit, clear } = render(\n createElement(SearchPromptApp, {\n choices,\n filterFn,\n visibleCount: effectiveLimit,\n defaultSort: \"updated-desc\",\n onSubmit: handleSubmit,\n onCancel: handleCancel,\n })\n );\n\n // Wait for the app to exit, clear the screen, then resolve\n waitUntilExit()\n .then(() => {\n clear();\n restoreStdinAfterInk();\n resolve(result);\n })\n .catch(() => {\n clear();\n restoreStdinAfterInk();\n resolve({\n selected: [],\n cancelled: true,\n });\n });\n });\n}\n\n// Export legacy functions for backward compatibility with existing tests\n// These are no longer used by the React Ink implementation\nexport interface AutoCompleteChoice {\n name: string;\n message: string;\n}\n\n/**\n * Creates choices from search results (legacy, for test compatibility)\n */\nexport function createChoices(\n results: SearchResult[],\n _terminalWidth: number\n): AutoCompleteChoice[] {\n return results.map((result, index) => ({\n name: JSON.stringify({ index, item: result.reference }),\n message: `[${index + 1}] ${result.reference.title ?? \"(No title)\"}`,\n }));\n}\n\n/**\n * Parses selected values back to CslItems (legacy, for test compatibility)\n */\nexport function parseSelectedValues(values: string | string[]): CslItem[] {\n const valueArray = Array.isArray(values) ? values : [values];\n const items: CslItem[] = [];\n\n for (const value of valueArray) {\n if (!value) continue;\n try {\n const data = JSON.parse(value) as { index: number; item: CslItem };\n items.push(data.item);\n } catch {\n // Ignore parse errors\n }\n }\n\n return items;\n}\n","/**\n * Shared reference selection utility for interactive ID selection.\n *\n * This module provides a reusable function to select references interactively\n * using the existing search prompt infrastructure.\n */\n\nimport type { CslItem } from \"../../core/csl-json/types.js\";\nimport { search } from \"../search/matcher.js\";\nimport { tokenize } from \"../search/tokenizer.js\";\nimport { type SearchPromptConfig, runSearchPrompt } from \"./search-prompt.js\";\nimport { checkTTY } from \"./tty.js\";\n\n/**\n * Options for reference selection\n */\nexport interface ReferenceSelectOptions {\n /** Whether to allow multiple selection (default: true) */\n multiSelect: boolean;\n /** Custom prompt message */\n prompt?: string;\n /** Initial search query */\n initialQuery?: string;\n}\n\n/**\n * Result from reference selection\n */\nexport interface ReferenceSelectResult {\n /** Selected references */\n selected: CslItem[];\n /** Whether the selection was cancelled */\n cancelled: boolean;\n}\n\n/**\n * Run interactive reference selection.\n *\n * Launches an interactive search prompt to select references from the library.\n * Supports both single and multiple selection modes.\n *\n * @param allReferences - All references available for selection\n * @param options - Selection options\n * @param config - Interactive prompt configuration\n * @returns Selection result with selected references\n * @throws TTYError if not running in a TTY environment\n */\nexport async function runReferenceSelect(\n allReferences: CslItem[],\n options: ReferenceSelectOptions,\n config: SearchPromptConfig\n): Promise<ReferenceSelectResult> {\n // Check TTY requirement\n checkTTY();\n\n // Create search function for runSearchPrompt\n const searchFn = (query: string) => {\n const { tokens } = tokenize(query);\n return search(allReferences, tokens);\n };\n\n // Run search prompt\n const searchResult = await runSearchPrompt(\n allReferences,\n searchFn,\n config,\n options.initialQuery ?? \"\"\n );\n\n if (searchResult.cancelled) {\n return { selected: [], cancelled: true };\n }\n\n // For single-select mode, return only the first selected item\n const selected = options.multiSelect ? searchResult.selected : searchResult.selected.slice(0, 1);\n\n return {\n selected,\n cancelled: false,\n };\n}\n\n/**\n * Options for selectReferencesOrExit helper\n */\nexport interface SelectReferencesOrExitOptions {\n /** Whether to allow multiple selection */\n multiSelect: boolean;\n /** Initial search query */\n initialQuery?: string;\n}\n\n/**\n * Select references interactively or exit if cancelled/empty.\n *\n * This is a convenience wrapper around runReferenceSelect that handles\n * common patterns:\n * - Exits with code 0 if library is empty\n * - Exits with code 0 if selection is cancelled\n * - Returns selected identifiers\n *\n * @param allReferences - All references available for selection\n * @param options - Selection options\n * @param config - Interactive prompt configuration\n * @returns Array of selected reference IDs (never empty)\n */\nexport async function selectReferencesOrExit(\n allReferences: CslItem[],\n options: SelectReferencesOrExitOptions,\n config: SearchPromptConfig\n): Promise<string[]> {\n if (allReferences.length === 0) {\n process.stderr.write(\"No references in library.\\n\");\n process.exit(0);\n }\n\n const selectResult = await runReferenceSelect(allReferences, options, config);\n\n if (selectResult.cancelled || selectResult.selected.length === 0) {\n process.exit(0);\n }\n\n return selectResult.selected.map((item) => item.id);\n}\n\n/**\n * Select reference items interactively or exit if cancelled/empty.\n *\n * Similar to selectReferencesOrExit but returns full CslItem objects\n * instead of just identifiers. Useful when the caller needs access to\n * the full reference data (e.g., for confirmation dialogs).\n *\n * @param allReferences - All references available for selection\n * @param options - Selection options\n * @param config - Interactive prompt configuration\n * @returns Array of selected CslItem objects (never empty)\n */\nexport async function selectReferenceItemsOrExit(\n allReferences: CslItem[],\n options: SelectReferencesOrExitOptions,\n config: SearchPromptConfig\n): Promise<CslItem[]> {\n if (allReferences.length === 0) {\n process.stderr.write(\"No references in library.\\n\");\n process.exit(0);\n }\n\n const selectResult = await runReferenceSelect(allReferences, options, config);\n\n if (selectResult.cancelled || selectResult.selected.length === 0) {\n process.exit(0);\n }\n\n return selectResult.selected;\n}\n"],"names":["choices"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"reference-select-Cs3hsy-1.js","sources":["../../src/features/interactive/search-prompt.ts","../../src/features/interactive/reference-select.ts"],"sourcesContent":["/**\n * Interactive search prompt using React Ink\n *\n * Provides real-time incremental search with multiple selection support.\n */\n\nimport { render, useApp } from \"ink\";\nimport type React from \"react\";\nimport { createElement } from \"react\";\nimport type { CslItem } from \"../../core/csl-json/types.js\";\nimport type { SearchResult } from \"../search/types.js\";\nimport { restoreStdinAfterInk } from \"./alternate-screen.js\";\nimport { type Choice, SearchableMultiSelect, type SortOption } from \"./components/index.js\";\nimport { formatAuthors } from \"./format.js\";\n\n/**\n * Configuration for the search prompt\n */\nexport interface SearchPromptConfig {\n /** Maximum number of results to display */\n limit: number;\n /** Debounce delay in milliseconds (not used in Ink version, kept for API compatibility) */\n debounceMs: number;\n}\n\n/**\n * Search function type for filtering references\n */\nexport type SearchFunction = (query: string) => SearchResult[];\n\n/**\n * Result from the search prompt\n */\nexport interface SearchPromptResult {\n /** Selected references */\n selected: CslItem[];\n /** Whether the prompt was cancelled */\n cancelled: boolean;\n}\n\n/**\n * Gets terminal width, falling back to 80 if not available\n */\nexport function getTerminalWidth(): number {\n return process.stdout.columns ?? 80;\n}\n\n/**\n * Gets terminal height, falling back to 24 if not available\n */\nexport function getTerminalHeight(): number {\n return process.stdout.rows ?? 24;\n}\n\n/**\n * Calculates the effective limit for the autocomplete list\n * based on terminal height to prevent input field from being hidden.\n * Reserves space for: prompt header (1), input line (1), footer hint (1), and padding (2)\n * Each item displays up to 3 lines (author/year, title, identifiers)\n */\nexport function calculateEffectiveLimit(configLimit: number): number {\n const terminalHeight = getTerminalHeight();\n // Reserve lines for: header(2) + search box(3) + status(1) + scroll indicators(2) + footer(2) = 10\n const reservedLines = 10;\n const linesPerItem = 3; // each search result shows up to 3 lines\n const availableLines = terminalHeight - reservedLines;\n const maxVisibleChoices = Math.max(1, Math.floor(availableLines / linesPerItem));\n return configLimit > 0 ? Math.min(configLimit, maxVisibleChoices) : maxVisibleChoices;\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) return undefined;\n const firstDatePart = dateParts[0];\n if (!firstDatePart || firstDatePart.length === 0) return undefined;\n return firstDatePart[0];\n}\n\n/**\n * Extract published date from CSL item\n */\nfunction extractPublishedDate(item: CslItem): Date | undefined {\n const dateParts = item.issued?.[\"date-parts\"];\n if (!dateParts || dateParts.length === 0) return undefined;\n const firstDatePart = dateParts[0];\n if (!firstDatePart || firstDatePart.length === 0) return undefined;\n const [year, month = 1, day = 1] = firstDatePart;\n if (year === undefined) return undefined;\n return new Date(year, month - 1, day);\n}\n\n/**\n * Extract updated date from CSL item (from custom.timestamp)\n */\nfunction extractUpdatedDate(item: CslItem): Date | undefined {\n const dateStr = item.custom?.timestamp;\n if (!dateStr || typeof dateStr !== \"string\") return undefined;\n const date = new Date(dateStr);\n return Number.isNaN(date.getTime()) ? undefined : date;\n}\n\n/**\n * Extract created date from CSL item (from custom.created_at)\n */\nfunction extractCreatedDate(item: CslItem): Date | undefined {\n const dateStr = item.custom?.created_at;\n if (!dateStr || typeof dateStr !== \"string\") return undefined;\n const date = new Date(dateStr);\n return Number.isNaN(date.getTime()) ? undefined : date;\n}\n\n/**\n * Format identifiers for meta line\n */\nfunction formatIdentifiers(item: CslItem): string {\n const parts: string[] = [];\n if (item.DOI) parts.push(`DOI: ${item.DOI}`);\n if (item.PMID) parts.push(`PMID: ${item.PMID}`);\n if (item.PMCID) parts.push(`PMCID: ${item.PMCID}`);\n if (item.ISBN) parts.push(`ISBN: ${item.ISBN}`);\n return parts.join(\" · \");\n}\n\n/**\n * Format item type for display\n */\nfunction formatType(type: string): string {\n const typeMap: Record<string, string> = {\n \"article-journal\": \"Journal article\",\n \"article-magazine\": \"Magazine article\",\n \"article-newspaper\": \"Newspaper article\",\n book: \"Book\",\n chapter: \"Book chapter\",\n \"paper-conference\": \"Conference paper\",\n thesis: \"Thesis\",\n report: \"Report\",\n webpage: \"Web page\",\n };\n return typeMap[type] ?? type;\n}\n\n/**\n * Convert CslItem to Choice for SearchableMultiSelect\n */\nfunction toChoice(item: CslItem): Choice<CslItem> {\n const authors = formatAuthors(item.author);\n const year = extractYear(item);\n const identifiers = formatIdentifiers(item);\n const itemType = formatType(item.type);\n\n // Build meta line: Year · Type · Identifiers\n const metaParts: string[] = [];\n if (year) metaParts.push(String(year));\n metaParts.push(itemType);\n if (identifiers) metaParts.push(identifiers);\n\n const updatedDate = extractUpdatedDate(item);\n const createdDate = extractCreatedDate(item);\n const publishedDate = extractPublishedDate(item);\n\n return {\n id: item.id,\n title: item.title ?? \"(No title)\",\n subtitle: authors || \"(No authors)\",\n meta: metaParts.join(\" · \"),\n value: item,\n ...(updatedDate && { updatedDate }),\n ...(createdDate && { createdDate }),\n ...(publishedDate && { publishedDate }),\n };\n}\n\n/**\n * Props for the SearchPromptApp component\n */\ninterface SearchPromptAppProps {\n choices: Choice<CslItem>[];\n filterFn: (query: string, choices: Choice<CslItem>[]) => Choice<CslItem>[];\n visibleCount: number;\n defaultSort: SortOption;\n onSubmit: (selected: Choice<CslItem>[]) => void;\n onCancel: () => void;\n}\n\n/**\n * SearchPromptApp component - wraps SearchableMultiSelect for search prompt\n */\nfunction SearchPromptApp({\n choices,\n filterFn,\n visibleCount,\n defaultSort,\n onSubmit,\n onCancel,\n}: SearchPromptAppProps): React.ReactElement {\n const { exit } = useApp();\n\n const handleSubmit = (selected: Choice<CslItem>[]): void => {\n onSubmit(selected);\n exit();\n };\n\n const handleCancel = (): void => {\n onCancel();\n exit();\n };\n\n return createElement(SearchableMultiSelect<CslItem>, {\n choices,\n filterFn,\n visibleCount,\n onSubmit: handleSubmit,\n onCancel: handleCancel,\n header: \"Search references\",\n placeholder: \"Type to search...\",\n defaultSort,\n });\n}\n\n/**\n * Creates and runs an interactive search prompt\n */\nexport async function runSearchPrompt(\n allReferences: CslItem[],\n searchFn: SearchFunction,\n config: SearchPromptConfig,\n _initialQuery = \"\" // kept for API compatibility, not used in Ink version\n): Promise<SearchPromptResult> {\n // Convert references to choices\n const choices = allReferences.map(toChoice);\n\n // Calculate effective visible count\n const effectiveLimit = calculateEffectiveLimit(config.limit);\n\n // Create filter function using the provided search function\n const filterFn = (query: string, choices: Choice<CslItem>[]): Choice<CslItem>[] => {\n if (!query.trim()) return choices;\n\n const results = searchFn(query);\n return results.map((r) => toChoice(r.reference));\n };\n\n // Create a promise to capture the result\n return new Promise<SearchPromptResult>((resolve) => {\n let result: SearchPromptResult = { selected: [], cancelled: true };\n\n const handleSubmit = (selected: Choice<CslItem>[]): void => {\n result = {\n selected: selected.map((c) => c.value),\n cancelled: false,\n };\n };\n\n const handleCancel = (): void => {\n result = {\n selected: [],\n cancelled: true,\n };\n };\n\n // Render the Ink app\n const { waitUntilExit, clear } = render(\n createElement(SearchPromptApp, {\n choices,\n filterFn,\n visibleCount: effectiveLimit,\n defaultSort: \"updated-desc\",\n onSubmit: handleSubmit,\n onCancel: handleCancel,\n })\n );\n\n // Wait for the app to exit, clear the screen, then resolve\n waitUntilExit()\n .then(() => {\n clear();\n restoreStdinAfterInk();\n resolve(result);\n })\n .catch(() => {\n clear();\n restoreStdinAfterInk();\n resolve({\n selected: [],\n cancelled: true,\n });\n });\n });\n}\n\n// Export legacy functions for backward compatibility with existing tests\n// These are no longer used by the React Ink implementation\nexport interface AutoCompleteChoice {\n name: string;\n message: string;\n}\n\n/**\n * Creates choices from search results (legacy, for test compatibility)\n */\nexport function createChoices(\n results: SearchResult[],\n _terminalWidth: number\n): AutoCompleteChoice[] {\n return results.map((result, index) => ({\n name: JSON.stringify({ index, item: result.reference }),\n message: `[${index + 1}] ${result.reference.title ?? \"(No title)\"}`,\n }));\n}\n\n/**\n * Parses selected values back to CslItems (legacy, for test compatibility)\n */\nexport function parseSelectedValues(values: string | string[]): CslItem[] {\n const valueArray = Array.isArray(values) ? values : [values];\n const items: CslItem[] = [];\n\n for (const value of valueArray) {\n if (!value) continue;\n try {\n const data = JSON.parse(value) as { index: number; item: CslItem };\n items.push(data.item);\n } catch {\n // Ignore parse errors\n }\n }\n\n return items;\n}\n","/**\n * Shared reference selection utility for interactive ID selection.\n *\n * This module provides a reusable function to select references interactively\n * using the existing search prompt infrastructure.\n */\n\nimport type { CslItem } from \"../../core/csl-json/types.js\";\nimport { search } from \"../search/matcher.js\";\nimport { tokenize } from \"../search/tokenizer.js\";\nimport { type SearchPromptConfig, runSearchPrompt } from \"./search-prompt.js\";\nimport { checkTTY } from \"./tty.js\";\n\n/**\n * Options for reference selection\n */\nexport interface ReferenceSelectOptions {\n /** Whether to allow multiple selection (default: true) */\n multiSelect: boolean;\n /** Custom prompt message */\n prompt?: string;\n /** Initial search query */\n initialQuery?: string;\n}\n\n/**\n * Result from reference selection\n */\nexport interface ReferenceSelectResult {\n /** Selected references */\n selected: CslItem[];\n /** Whether the selection was cancelled */\n cancelled: boolean;\n}\n\n/**\n * Run interactive reference selection.\n *\n * Launches an interactive search prompt to select references from the library.\n * Supports both single and multiple selection modes.\n *\n * @param allReferences - All references available for selection\n * @param options - Selection options\n * @param config - Interactive prompt configuration\n * @returns Selection result with selected references\n * @throws TTYError if not running in a TTY environment\n */\nexport async function runReferenceSelect(\n allReferences: CslItem[],\n options: ReferenceSelectOptions,\n config: SearchPromptConfig\n): Promise<ReferenceSelectResult> {\n // Check TTY requirement\n checkTTY();\n\n // Create search function for runSearchPrompt\n const searchFn = (query: string) => {\n const { tokens } = tokenize(query);\n return search(allReferences, tokens);\n };\n\n // Run search prompt\n const searchResult = await runSearchPrompt(\n allReferences,\n searchFn,\n config,\n options.initialQuery ?? \"\"\n );\n\n if (searchResult.cancelled) {\n return { selected: [], cancelled: true };\n }\n\n // For single-select mode, return only the first selected item\n const selected = options.multiSelect ? searchResult.selected : searchResult.selected.slice(0, 1);\n\n return {\n selected,\n cancelled: false,\n };\n}\n\n/**\n * Options for selectReferencesOrExit helper\n */\nexport interface SelectReferencesOrExitOptions {\n /** Whether to allow multiple selection */\n multiSelect: boolean;\n /** Initial search query */\n initialQuery?: string;\n}\n\n/**\n * Select references interactively or exit if cancelled/empty.\n *\n * This is a convenience wrapper around runReferenceSelect that handles\n * common patterns:\n * - Exits with code 0 if library is empty\n * - Exits with code 0 if selection is cancelled\n * - Returns selected identifiers\n *\n * @param allReferences - All references available for selection\n * @param options - Selection options\n * @param config - Interactive prompt configuration\n * @returns Array of selected reference IDs (never empty)\n */\nexport async function selectReferencesOrExit(\n allReferences: CslItem[],\n options: SelectReferencesOrExitOptions,\n config: SearchPromptConfig\n): Promise<string[]> {\n if (allReferences.length === 0) {\n process.stderr.write(\"No references in library.\\n\");\n process.exit(0);\n }\n\n const selectResult = await runReferenceSelect(allReferences, options, config);\n\n if (selectResult.cancelled || selectResult.selected.length === 0) {\n process.exit(0);\n }\n\n return selectResult.selected.map((item) => item.id);\n}\n\n/**\n * Select reference items interactively or exit if cancelled/empty.\n *\n * Similar to selectReferencesOrExit but returns full CslItem objects\n * instead of just identifiers. Useful when the caller needs access to\n * the full reference data (e.g., for confirmation dialogs).\n *\n * @param allReferences - All references available for selection\n * @param options - Selection options\n * @param config - Interactive prompt configuration\n * @returns Array of selected CslItem objects (never empty)\n */\nexport async function selectReferenceItemsOrExit(\n allReferences: CslItem[],\n options: SelectReferencesOrExitOptions,\n config: SearchPromptConfig\n): Promise<CslItem[]> {\n if (allReferences.length === 0) {\n process.stderr.write(\"No references in library.\\n\");\n process.exit(0);\n }\n\n const selectResult = await runReferenceSelect(allReferences, options, config);\n\n if (selectResult.cancelled || selectResult.selected.length === 0) {\n process.exit(0);\n }\n\n return selectResult.selected;\n}\n"],"names":["choices"],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SearchableMultiSelect.d.ts","sourceRoot":"","sources":["../../../../src/features/interactive/components/SearchableMultiSelect.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"SearchableMultiSelect.d.ts","sourceRoot":"","sources":["../../../../src/features/interactive/components/SearchableMultiSelect.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAK/B,MAAM,WAAW,MAAM,CAAC,CAAC,GAAG,OAAO;IACjC,uCAAuC;IACvC,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oDAAoD;IACpD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,KAAK,EAAE,CAAC,CAAC;IACT,6DAA6D;IAC7D,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,yDAAyD;IACzD,WAAW,CAAC,EAAE,IAAI,CAAC;IACnB,qDAAqD;IACrD,aAAa,CAAC,EAAE,IAAI,CAAC;CACtB;AAED,6BAA6B;AAC7B,MAAM,MAAM,UAAU,GAClB,cAAc,GACd,aAAa,GACb,cAAc,GACd,aAAa,GACb,gBAAgB,GAChB,eAAe,GACf,WAAW,CAAC;AAShB,MAAM,WAAW,0BAA0B,CAAC,CAAC;IAC3C,wBAAwB;IACxB,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IACrB,iCAAiC;IACjC,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAChE,4BAA4B;IAC5B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,2CAA2C;IAC3C,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,IAAI,CAAC;IAC1C,8BAA8B;IAC9B,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kBAAkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,0BAA0B;IAC1B,WAAW,CAAC,EAAE,UAAU,CAAC;CAC1B;AAwVD,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,EACvC,OAAO,EACP,QAAwB,EACxB,YAAY,EAAE,gBAAgB,EAC9B,QAAQ,EACR,QAAQ,EACR,WAAiC,EACjC,MAAM,EACN,MAAM,EACN,WAA4B,GAC7B,EAAE,0BAA0B,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC,YAAY,CAwNpD"}
|
package/package.json
CHANGED
|
@@ -1,397 +0,0 @@
|
|
|
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
|
|
@@ -1 +0,0 @@
|
|
|
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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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index-Bzl4_3Ki.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|