@tooee/choose 0.1.22 → 0.2.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.
Files changed (50) hide show
  1. package/README.md +28 -2
  2. package/dist/Choose.d.ts +14 -17
  3. package/dist/Choose.d.ts.map +1 -1
  4. package/dist/Choose.js +42 -266
  5. package/dist/Choose.js.map +1 -1
  6. package/dist/ChooseFilter.d.ts +12 -0
  7. package/dist/ChooseFilter.d.ts.map +1 -0
  8. package/dist/ChooseFilter.js +9 -0
  9. package/dist/ChooseFilter.js.map +1 -0
  10. package/dist/ChooseHighlightedText.d.ts +9 -0
  11. package/dist/ChooseHighlightedText.d.ts.map +1 -0
  12. package/dist/ChooseHighlightedText.js +30 -0
  13. package/dist/ChooseHighlightedText.js.map +1 -0
  14. package/dist/ChooseList.d.ts +28 -0
  15. package/dist/ChooseList.d.ts.map +1 -0
  16. package/dist/ChooseList.js +51 -0
  17. package/dist/ChooseList.js.map +1 -0
  18. package/dist/ChooseOverlay.d.ts +34 -5
  19. package/dist/ChooseOverlay.d.ts.map +1 -1
  20. package/dist/ChooseOverlay.js +29 -200
  21. package/dist/ChooseOverlay.js.map +1 -1
  22. package/dist/ChoosePanel.d.ts +30 -0
  23. package/dist/ChoosePanel.d.ts.map +1 -0
  24. package/dist/ChoosePanel.js +25 -0
  25. package/dist/ChoosePanel.js.map +1 -0
  26. package/dist/index.d.ts +13 -1
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +6 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/source.d.ts +5 -0
  31. package/dist/source.d.ts.map +1 -0
  32. package/dist/source.js +12 -0
  33. package/dist/source.js.map +1 -0
  34. package/dist/types.d.ts +2 -0
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/use-choose.d.ts +80 -0
  37. package/dist/use-choose.d.ts.map +1 -0
  38. package/dist/use-choose.js +365 -0
  39. package/dist/use-choose.js.map +1 -0
  40. package/package.json +11 -11
  41. package/src/Choose.tsx +68 -355
  42. package/src/ChooseFilter.tsx +45 -0
  43. package/src/ChooseHighlightedText.tsx +42 -0
  44. package/src/ChooseList.tsx +137 -0
  45. package/src/ChooseOverlay.tsx +73 -307
  46. package/src/ChoosePanel.tsx +123 -0
  47. package/src/index.ts +26 -1
  48. package/src/source.ts +12 -0
  49. package/src/types.ts +6 -0
  50. package/src/use-choose.ts +479 -0
@@ -0,0 +1,137 @@
1
+ import { useEffect, useRef, type ReactNode } from "react"
2
+ import type { ScrollBoxRenderable } from "@opentui/core"
3
+ import { useTheme } from "@tooee/themes"
4
+ import { ChooseHighlightedText } from "./ChooseHighlightedText.js"
5
+ import type { FuzzyMatch } from "./fuzzy.js"
6
+ import type { ChooseItem } from "./types.js"
7
+ import type { UseChooseResult } from "./use-choose.js"
8
+
9
+ export interface ChooseItemRenderContext {
10
+ item: ChooseItem
11
+ match: FuzzyMatch
12
+ index: number
13
+ originalIndex: number
14
+ positions: number[]
15
+ isActive: boolean
16
+ isSelected: boolean
17
+ defaultContent: ReactNode
18
+ }
19
+
20
+ export interface ChooseListProps {
21
+ choose: UseChooseResult
22
+ /** Fullscreen defaults to activate; picker overlays default to submit. */
23
+ rowClick?: "activate" | "submit" | "none"
24
+ renderItem?: (context: ChooseItemRenderContext) => ReactNode
25
+ loadingContent?: ReactNode
26
+ errorContent?: (error: string) => ReactNode
27
+ emptyContent?: ReactNode
28
+ /** Mouse guard for visible rows covered by another surface. */
29
+ suspended?: boolean
30
+ }
31
+
32
+ function ThemedLine({ content, color }: { content: ReactNode; color: string }) {
33
+ return (
34
+ <box height={1} style={{ paddingLeft: 2 }}>
35
+ {typeof content === "string" ? <text content={content} fg={color} /> : content}
36
+ </box>
37
+ )
38
+ }
39
+
40
+ function defaultErrorContent(error: string): ReactNode {
41
+ return `Error: ${error}`
42
+ }
43
+
44
+ /** Scrollable chooser rows plus loading/error/empty presentation. */
45
+ export function ChooseList({
46
+ choose,
47
+ rowClick = "activate",
48
+ renderItem,
49
+ loadingContent = "Loading...",
50
+ errorContent = defaultErrorContent,
51
+ emptyContent,
52
+ suspended,
53
+ }: ChooseListProps) {
54
+ const { theme } = useTheme()
55
+ const scrollRef = useRef<ScrollBoxRenderable>(null)
56
+ const { state, controller } = choose
57
+ const interactionSuspended = choose.view.suspended || (suspended ?? false)
58
+
59
+ useEffect(() => {
60
+ if (scrollRef.current && state.matches.length > 0) {
61
+ scrollRef.current.scrollTop = Math.max(0, state.activeIndex - 5)
62
+ }
63
+ }, [state.activeIndex, state.matches.length])
64
+
65
+ return (
66
+ <scrollbox ref={scrollRef} flexDirection="column" style={{ flexGrow: 1 }} focused={false}>
67
+ {state.loading && <ThemedLine content={loadingContent} color={theme.textMuted} />}
68
+
69
+ {!state.loading && state.error && (
70
+ <ThemedLine content={errorContent(state.error)} color={theme.error} />
71
+ )}
72
+
73
+ {!state.loading && !state.error && state.matches.length === 0 && emptyContent != null && (
74
+ <ThemedLine content={emptyContent} color={theme.textMuted} />
75
+ )}
76
+
77
+ {state.matches.map((match, index) => {
78
+ const isActive = index === state.activeIndex
79
+ const isSelected = state.selectedOriginalIndices.has(match.originalIndex)
80
+ const defaultContent = (
81
+ <>
82
+ {state.multi && (
83
+ <text
84
+ content={isSelected ? "✓ " : " "}
85
+ fg={isSelected ? theme.accent : theme.textMuted}
86
+ />
87
+ )}
88
+ {match.item.icon && <text content={`${match.item.icon} `} fg={theme.textMuted} />}
89
+ <text fg={isActive ? theme.primary : theme.text}>
90
+ <ChooseHighlightedText
91
+ text={match.item.text}
92
+ positions={match.positions}
93
+ highlightColor={theme.warning}
94
+ />
95
+ </text>
96
+ {match.item.description && (
97
+ <text content={` ${match.item.description}`} fg={theme.textMuted} />
98
+ )}
99
+ </>
100
+ )
101
+ const context: ChooseItemRenderContext = {
102
+ item: match.item,
103
+ match,
104
+ index,
105
+ originalIndex: match.originalIndex,
106
+ positions: match.positions,
107
+ isActive,
108
+ isSelected,
109
+ defaultContent,
110
+ }
111
+
112
+ return (
113
+ <box
114
+ key={match.originalIndex}
115
+ flexDirection="row"
116
+ height={1}
117
+ backgroundColor={isActive ? theme.backgroundElement : undefined}
118
+ style={{ paddingLeft: 1 }}
119
+ onMouseDown={(event) => {
120
+ if (interactionSuspended || event.button !== 0 || rowClick === "none") return
121
+ if (rowClick === "submit") {
122
+ event.preventDefault()
123
+ event.stopPropagation()
124
+ controller.setActiveIndex(index)
125
+ controller.submit()
126
+ } else {
127
+ controller.setActiveIndex(index)
128
+ }
129
+ }}
130
+ >
131
+ {renderItem ? renderItem(context) : defaultContent}
132
+ </box>
133
+ )
134
+ })}
135
+ </scrollbox>
136
+ )
137
+ }
@@ -1,322 +1,88 @@
1
- import { useState, useEffect, useRef, useCallback, useMemo } from "react"
2
- import type { ScrollBoxRenderable } from "@opentui/core"
3
- import { useTheme, CloseButton } from "@tooee/themes"
4
- import { useCommand, useMode, useSetMode } from "@tooee/commands"
5
- import type { ChooseItem } from "./types.js"
6
- import { fuzzyFilter } from "./fuzzy.js"
7
-
8
- export interface ChooseOverlayProps {
9
- items: ChooseItem[] | (() => Promise<ChooseItem[]>)
1
+ import { useImperativeHandle, type ReactNode, type Ref } from "react"
2
+ import type { ActionDefinition } from "@tooee/commands"
3
+ import { ChooseFilter } from "./ChooseFilter.js"
4
+ import { ChooseList, type ChooseListProps } from "./ChooseList.js"
5
+ import { ChoosePanel, type ChoosePanelProps } from "./ChoosePanel.js"
6
+ import type { ChooseItem, ChooseResult, ChooseSource } from "./types.js"
7
+ import { useChoose, type ChooseController } from "./use-choose.js"
8
+
9
+ interface ChooseOverlayBaseProps {
10
+ items: ChooseSource
10
11
  prompt?: string
11
12
  placeholder?: string
12
13
  emptyMessage?: string
13
- onSelect: (item: ChooseItem) => void | Promise<void>
14
14
  onCancel: () => void
15
+ commands?: ActionDefinition[]
16
+ controllerRef?: Ref<ChooseController>
17
+ renderItem?: ChooseListProps["renderItem"]
18
+ hints?: ChoosePanelProps["hints"]
19
+ statusRight?: ReactNode
20
+ footer?: ReactNode
21
+ inset?: ChoosePanelProps["inset"]
22
+ /** Nested modal content rendered above the chooser panel. */
23
+ children?: ReactNode
24
+ /** Explicit interaction guard for legacy child overlays without a command surface. */
25
+ suspended?: boolean
15
26
  }
16
27
 
17
- export function ChooseOverlay({
18
- items: itemsOrLoader,
19
- prompt,
20
- placeholder,
21
- emptyMessage,
22
- onSelect,
23
- onCancel,
24
- }: ChooseOverlayProps) {
25
- const { theme } = useTheme()
26
- const mode = useMode()
27
- const setMode = useSetMode()
28
-
29
- const [items, setItems] = useState<ChooseItem[]>(
30
- Array.isArray(itemsOrLoader) ? itemsOrLoader : [],
31
- )
32
- const [loading, setLoading] = useState(!Array.isArray(itemsOrLoader))
33
- const [error, setError] = useState<string | null>(null)
34
- const [filterQuery, setFilterQuery] = useState("")
35
- const [activeIndex, setActiveIndex] = useState(0)
36
- const scrollRef = useRef<ScrollBoxRenderable>(null)
37
-
38
- // Load items if async loader provided
39
- useEffect(() => {
40
- if (typeof itemsOrLoader === "function") {
41
- let active = true
42
- setLoading(true)
43
- setError(null)
44
- itemsOrLoader()
45
- .then((loaded) => {
46
- // Ignore stale results after itemsOrLoader changed
47
- if (!active) return
48
- setItems(loaded)
49
- setLoading(false)
50
- })
51
- .catch((err: unknown) => {
52
- if (!active) return
53
- setItems([])
54
- setError(err instanceof Error ? err.message : String(err))
55
- setLoading(false)
56
- })
57
- return () => {
58
- active = false
28
+ export type ChooseOverlayProps = ChooseOverlayBaseProps &
29
+ (
30
+ | {
31
+ multi?: false
32
+ onSelect: (item: ChooseItem) => void | Promise<void>
33
+ onSubmit?: never
59
34
  }
60
- }
61
- }, [itemsOrLoader])
62
-
63
- // Filtered items via fuzzy matching
64
- const filteredItems = useMemo(() => fuzzyFilter(items, filterQuery), [items, filterQuery])
65
-
66
- // Reset activeIndex when filter changes
67
- const [prevFilterQuery, setPrevFilterQuery] = useState("")
68
- if (filterQuery !== prevFilterQuery) {
69
- setPrevFilterQuery(filterQuery)
70
- setActiveIndex(0)
71
- }
72
-
73
- const moveUp = useCallback(() => {
74
- setActiveIndex((i) => Math.max(0, i - 1))
75
- }, [])
76
-
77
- const moveDown = useCallback(() => {
78
- setActiveIndex((i) => Math.min(filteredItems.length - 1, i + 1))
79
- }, [filteredItems.length])
80
-
81
- const confirm = useCallback(() => {
82
- const match = filteredItems[activeIndex]
83
- if (match) {
84
- onSelect(match.item)
85
- }
86
- }, [filteredItems, activeIndex, onSelect])
87
-
88
- const leaveInsertModeOrCancel = useCallback(() => {
89
- if (mode === "insert") {
90
- setMode("cursor")
91
- } else {
92
- onCancel()
93
- }
94
- }, [mode, onCancel, setMode])
95
-
96
- const enterInsertMode = useCallback(() => {
97
- setMode("insert")
98
- }, [setMode])
35
+ | {
36
+ multi: true
37
+ onSubmit: (result: ChooseResult) => void | Promise<void>
38
+ onSelect?: never
39
+ }
40
+ )
99
41
 
100
- useCommand({
101
- id: "choose-overlay:escape",
102
- title: "Back / cancel",
103
- hotkey: "Escape",
104
- modes: ["insert", "cursor"],
105
- hidden: true,
106
- handler: leaveInsertModeOrCancel,
107
- })
108
- useCommand({
109
- id: "choose-overlay:insert-mode-i",
110
- title: "Insert mode",
111
- hotkey: "i",
112
- modes: ["cursor"],
113
- hidden: true,
114
- handler: enterInsertMode,
115
- })
116
- useCommand({
117
- id: "choose-overlay:insert-mode-a",
118
- title: "Insert mode",
119
- hotkey: "a",
120
- modes: ["cursor"],
121
- hidden: true,
122
- handler: enterInsertMode,
123
- })
124
- useCommand({
125
- id: "choose-overlay:move-down-vim",
126
- title: "Move down",
127
- hotkey: "j",
128
- modes: ["cursor"],
129
- hidden: true,
130
- handler: moveDown,
131
- })
132
- useCommand({
133
- id: "choose-overlay:move-up-vim",
134
- title: "Move up",
135
- hotkey: "k",
136
- modes: ["cursor"],
137
- hidden: true,
138
- handler: moveUp,
139
- })
140
- useCommand({
141
- id: "choose-overlay:confirm",
142
- title: "Confirm",
143
- hotkey: "Enter",
144
- modes: ["insert", "cursor"],
145
- hidden: true,
146
- handler: confirm,
42
+ /**
43
+ * Composed picker surface. Hosts should mount it on an owned modal command
44
+ * surface (as Tooee's overlay manager does) so nested surfaces suspend it.
45
+ */
46
+ export function ChooseOverlay(props: ChooseOverlayProps) {
47
+ const multi = props.multi === true
48
+ const choose = useChoose({
49
+ source: props.items,
50
+ multi,
51
+ onSubmit: (result) => {
52
+ if (multi) return props.onSubmit(result)
53
+ const item = result.items[0]
54
+ if (item) return props.onSelect(item)
55
+ },
56
+ onCancel: props.onCancel,
57
+ commands: props.commands,
58
+ commandScope: "choose-overlay",
59
+ suspended: props.suspended,
147
60
  })
148
- useCommand({
149
- id: "choose-overlay:move-up",
150
- title: "Move up",
151
- hotkey: "up",
152
- modes: ["insert", "cursor"],
153
- hidden: true,
154
- handler: moveUp,
155
- })
156
- useCommand({
157
- id: "choose-overlay:move-up-ctrl-p",
158
- title: "Move up",
159
- hotkey: "ctrl+p",
160
- modes: ["insert", "cursor"],
161
- hidden: true,
162
- handler: moveUp,
163
- })
164
- useCommand({
165
- id: "choose-overlay:move-down",
166
- title: "Move down",
167
- hotkey: "down",
168
- modes: ["insert", "cursor"],
169
- hidden: true,
170
- handler: moveDown,
171
- })
172
- useCommand({
173
- id: "choose-overlay:move-down-ctrl-n",
174
- title: "Move down",
175
- hotkey: "ctrl+n",
176
- modes: ["insert", "cursor"],
177
- hidden: true,
178
- handler: moveDown,
179
- })
180
-
181
- // Auto-scroll to keep active item visible
182
- useEffect(() => {
183
- if (scrollRef.current && filteredItems.length > 0) {
184
- scrollRef.current.scrollTop = Math.max(0, activeIndex - 5)
185
- }
186
- }, [activeIndex, filteredItems.length])
187
61
 
188
- const hintText =
189
- mode === "insert" ? "Enter confirm Esc commands" : "j/k navigate i insert Esc cancel"
62
+ useImperativeHandle(props.controllerRef, () => choose.controller, [choose.controller])
190
63
 
191
64
  return (
192
- <box
193
- position="absolute"
194
- left="20%"
195
- right="20%"
196
- top="20%"
197
- bottom="20%"
198
- flexDirection="column"
199
- backgroundColor={theme.backgroundPanel}
200
- border
201
- borderColor={theme.borderActive}
202
- >
203
- {/* Title bar */}
204
- {prompt && (
205
- <box height={1} paddingLeft={1} paddingRight={1} backgroundColor={theme.backgroundElement}>
206
- <text content={prompt} fg={theme.accent} />
207
- </box>
208
- )}
209
-
210
- {/* Filter input row */}
211
- <box flexDirection="row" height={1} style={{ paddingLeft: 1, paddingRight: 1 }}>
212
- <text content="> " fg={theme.accent} />
213
- <input
214
- focused={mode === "insert"}
215
- placeholder={placeholder ?? "Filter..."}
216
- onInput={setFilterQuery}
217
- backgroundColor="transparent"
218
- textColor={theme.text}
219
- placeholderColor={theme.textMuted}
220
- cursorColor={theme.primary}
221
- style={{ flexGrow: 1 }}
65
+ <>
66
+ <ChoosePanel
67
+ title={props.prompt}
68
+ filter={
69
+ <ChooseFilter choose={choose} placeholder={props.placeholder} onClose={props.onCancel} />
70
+ }
71
+ multi={multi}
72
+ hints={props.hints}
73
+ statusRight={props.statusRight}
74
+ footer={props.footer}
75
+ inset={props.inset}
76
+ >
77
+ <ChooseList
78
+ choose={choose}
79
+ rowClick="submit"
80
+ renderItem={props.renderItem}
81
+ emptyContent={props.emptyMessage}
82
+ suspended={props.suspended}
222
83
  />
223
- <text content={` ${filteredItems.length}/${items.length}`} fg={theme.textMuted} />
224
- <CloseButton onClose={onCancel} />
225
- </box>
226
-
227
- {/* Scrollable item list */}
228
- <scrollbox ref={scrollRef} flexDirection="column" style={{ flexGrow: 1 }} focused={false}>
229
- {loading && (
230
- <box height={1} style={{ paddingLeft: 2 }}>
231
- <text content="Loading..." fg={theme.textMuted} />
232
- </box>
233
- )}
234
-
235
- {!loading && error && (
236
- <box height={1} style={{ paddingLeft: 2 }}>
237
- <text content={`Error: ${error}`} fg={theme.error} />
238
- </box>
239
- )}
240
-
241
- {!loading && !error && filteredItems.length === 0 && emptyMessage && (
242
- <box height={1} style={{ paddingLeft: 2 }}>
243
- <text content={emptyMessage} fg={theme.textMuted} />
244
- </box>
245
- )}
246
-
247
- {filteredItems.map((match, idx) => {
248
- const isActive = idx === activeIndex
249
- return (
250
- <box
251
- key={match.originalIndex}
252
- flexDirection="row"
253
- height={1}
254
- backgroundColor={isActive ? theme.backgroundElement : undefined}
255
- style={{ paddingLeft: 1 }}
256
- onMouseDown={(event) => {
257
- // Left-click picks the item — same code path as Enter confirm.
258
- // (Standalone Choose keeps click-to-highlight; a modal picker
259
- // overlay follows menu semantics like ContextMenu instead.)
260
- if (event.button !== 0) return
261
- event.preventDefault()
262
- event.stopPropagation()
263
- onSelect(match.item)
264
- }}
265
- >
266
- {match.item.icon && <text content={`${match.item.icon} `} fg={theme.textMuted} />}
267
- <text fg={isActive ? theme.primary : theme.text}>
268
- {renderHighlightedText(match.item.text, match.positions, theme.warning)}
269
- </text>
270
- {match.item.description && (
271
- <text content={` ${match.item.description}`} fg={theme.textMuted} />
272
- )}
273
- </box>
274
- )
275
- })}
276
- </scrollbox>
277
-
278
- {/* Hint line */}
279
- <box height={1} paddingLeft={1} paddingRight={1} backgroundColor={theme.backgroundElement}>
280
- <text content={hintText} fg={theme.textMuted} />
281
- </box>
282
- </box>
283
- )
284
- }
285
-
286
- function renderHighlightedText(text: string, positions: number[], highlightColor: string) {
287
- if (positions.length === 0) {
288
- return text
289
- }
290
-
291
- const posSet = new Set(positions)
292
- const parts: Array<{ text: string; highlight: boolean }> = []
293
- let current = ""
294
- let currentHighlight = false
295
-
296
- for (let i = 0; i < text.length; i++) {
297
- const isHighlight = posSet.has(i)
298
- if (i === 0) {
299
- currentHighlight = isHighlight
300
- current = text[i]
301
- } else if (isHighlight === currentHighlight) {
302
- current += text[i]
303
- } else {
304
- parts.push({ text: current, highlight: currentHighlight })
305
- current = text[i]
306
- currentHighlight = isHighlight
307
- }
308
- }
309
- if (current) {
310
- parts.push({ text: current, highlight: currentHighlight })
311
- }
312
-
313
- return parts.map((part, i) =>
314
- part.highlight ? (
315
- <span key={i} fg={highlightColor}>
316
- {part.text}
317
- </span>
318
- ) : (
319
- part.text
320
- ),
84
+ </ChoosePanel>
85
+ {props.children}
86
+ </>
321
87
  )
322
88
  }
@@ -0,0 +1,123 @@
1
+ import type { ReactNode } from "react"
2
+ import { useMode, type Mode } from "@tooee/commands"
3
+ import { CloseButton, useTheme } from "@tooee/themes"
4
+
5
+ export function buildChooseHints(
6
+ mode: Mode,
7
+ options: { multi?: boolean; extra?: string[] } = {},
8
+ ): string[] {
9
+ const base =
10
+ mode === "insert"
11
+ ? ["↑↓ navigate", "Enter confirm", ...(options.multi ? ["Tab toggle"] : []), "Esc commands"]
12
+ : [
13
+ "j/k navigate",
14
+ "i insert",
15
+ ...(options.multi ? ["Tab toggle"] : []),
16
+ "Esc/q cancel",
17
+ "Enter confirm",
18
+ ]
19
+ return options.extra ? [...base, ...options.extra] : base
20
+ }
21
+
22
+ export type ChoosePanelInsetValue = number | "auto" | `${number}%`
23
+
24
+ export interface ChoosePanelInset {
25
+ left?: ChoosePanelInsetValue
26
+ right?: ChoosePanelInsetValue
27
+ top?: ChoosePanelInsetValue
28
+ bottom?: ChoosePanelInsetValue
29
+ }
30
+
31
+ export interface ChoosePanelProps {
32
+ title?: ReactNode
33
+ filter: ReactNode
34
+ children: ReactNode
35
+ multi?: boolean
36
+ hints?: (context: { mode: Mode; defaults: string[] }) => ReactNode
37
+ statusRight?: ReactNode
38
+ footer?: ReactNode
39
+ onClose?: () => void
40
+ inset?: ChoosePanelInset
41
+ }
42
+
43
+ /** Bordered chooser chrome with filter, content, hint/status, and footer slots. */
44
+ export function ChoosePanel({
45
+ title,
46
+ filter,
47
+ children,
48
+ multi,
49
+ hints,
50
+ statusRight,
51
+ footer,
52
+ onClose,
53
+ inset,
54
+ }: ChoosePanelProps) {
55
+ const { theme } = useTheme()
56
+ const mode = useMode()
57
+ const defaults = buildChooseHints(mode, { multi })
58
+ const hintContent = hints ? hints({ mode, defaults }) : defaults.join(" ")
59
+
60
+ return (
61
+ <box
62
+ position="absolute"
63
+ left={inset?.left ?? "20%"}
64
+ right={inset?.right ?? "20%"}
65
+ top={inset?.top ?? "20%"}
66
+ bottom={inset?.bottom ?? "20%"}
67
+ flexDirection="column"
68
+ backgroundColor={theme.backgroundPanel}
69
+ border
70
+ borderColor={theme.borderActive}
71
+ >
72
+ {title != null && (
73
+ <box
74
+ flexDirection="row"
75
+ height={1}
76
+ paddingLeft={1}
77
+ paddingRight={1}
78
+ backgroundColor={theme.backgroundElement}
79
+ >
80
+ {typeof title === "string" ? (
81
+ <text content={title} fg={theme.accent} style={{ flexGrow: 1 }} />
82
+ ) : (
83
+ <box flexDirection="row" style={{ flexGrow: 1 }}>
84
+ {title}
85
+ </box>
86
+ )}
87
+ {onClose && <CloseButton onClose={onClose} />}
88
+ </box>
89
+ )}
90
+
91
+ {filter}
92
+ {children}
93
+
94
+ {footer != null && (
95
+ <box height={1} paddingLeft={1} paddingRight={1}>
96
+ {footer}
97
+ </box>
98
+ )}
99
+
100
+ <box
101
+ flexDirection="row"
102
+ height={1}
103
+ paddingLeft={1}
104
+ paddingRight={1}
105
+ backgroundColor={theme.backgroundElement}
106
+ >
107
+ <box flexDirection="row" style={{ flexGrow: 1 }}>
108
+ {typeof hintContent === "string" ? (
109
+ <text content={hintContent} fg={theme.textMuted} />
110
+ ) : (
111
+ hintContent
112
+ )}
113
+ </box>
114
+ {statusRight != null &&
115
+ (typeof statusRight === "string" ? (
116
+ <text content={statusRight} fg={theme.textMuted} />
117
+ ) : (
118
+ statusRight
119
+ ))}
120
+ </box>
121
+ </box>
122
+ )
123
+ }
package/src/index.ts CHANGED
@@ -1,9 +1,34 @@
1
1
  export { Choose } from "./Choose.js"
2
+ export type { ChooseProps } from "./Choose.js"
2
3
  export { ChooseOverlay } from "./ChooseOverlay.js"
3
4
  export type { ChooseOverlayProps } from "./ChooseOverlay.js"
5
+ export { ChoosePanel, buildChooseHints } from "./ChoosePanel.js"
6
+ export type { ChoosePanelProps, ChoosePanelInset, ChoosePanelInsetValue } from "./ChoosePanel.js"
7
+ export { ChooseFilter } from "./ChooseFilter.js"
8
+ export type { ChooseFilterProps } from "./ChooseFilter.js"
9
+ export { ChooseList } from "./ChooseList.js"
10
+ export type { ChooseItemRenderContext, ChooseListProps } from "./ChooseList.js"
11
+ export { ChooseHighlightedText } from "./ChooseHighlightedText.js"
12
+ export type { ChooseHighlightedTextProps } from "./ChooseHighlightedText.js"
13
+ export { useChoose } from "./use-choose.js"
14
+ export type {
15
+ ChooseCommandGroup,
16
+ ChooseController,
17
+ ChooseState,
18
+ ChooseViewModel,
19
+ UseChooseOptions,
20
+ UseChooseResult,
21
+ } from "./use-choose.js"
22
+ export { loadChooseSource } from "./source.js"
4
23
  export { launch } from "./launch.js"
5
24
  export type { ChooseLaunchOptions } from "./launch.js"
6
25
  export { createStdinChooseProvider, createStaticProvider } from "./default-provider.js"
7
26
  export { fuzzyFilter } from "./fuzzy.js"
8
27
  export type { FuzzyMatch } from "./fuzzy.js"
9
- export type { ChooseItem, ChooseContentProvider, ChooseResult, ChooseOptions } from "./types.js"
28
+ export type {
29
+ ChooseItem,
30
+ ChooseContentProvider,
31
+ ChooseSource,
32
+ ChooseResult,
33
+ ChooseOptions,
34
+ } from "./types.js"