ink-cartridge 3.6.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/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/binary-storage/BinaryStorage.d.ts +308 -0
- package/dist/binary-storage/BinaryStorage.js +548 -0
- package/dist/binary-storage/StreamingReader.d.ts +53 -0
- package/dist/binary-storage/StreamingReader.js +530 -0
- package/dist/binary-storage/index.d.ts +91 -0
- package/dist/binary-storage/index.js +91 -0
- package/dist/binary-storage/types.d.ts +400 -0
- package/dist/binary-storage/types.js +15 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +214 -0
- package/dist/cli/initTheme.d.ts +14 -0
- package/dist/cli/initTheme.js +124 -0
- package/dist/cli/makeLanguageType.d.ts +46 -0
- package/dist/cli/makeLanguageType.js +264 -0
- package/dist/cli/makeThemeType.d.ts +43 -0
- package/dist/cli/makeThemeType.js +222 -0
- package/dist/components/badge/Badge.d.ts +7 -0
- package/dist/components/badge/Badge.js +9 -0
- package/dist/components/dialog/ConfirmDialog.d.ts +23 -0
- package/dist/components/dialog/ConfirmDialog.js +57 -0
- package/dist/components/dialog/types.d.ts +20 -0
- package/dist/components/dialog/types.js +1 -0
- package/dist/components/divider/Divider.d.ts +8 -0
- package/dist/components/divider/Divider.js +10 -0
- package/dist/components/fold/Fold.d.ts +3 -0
- package/dist/components/fold/Fold.js +44 -0
- package/dist/components/fold/types.d.ts +28 -0
- package/dist/components/fold/types.js +1 -0
- package/dist/components/form/Field.d.ts +30 -0
- package/dist/components/form/Field.js +59 -0
- package/dist/components/form/Form.d.ts +32 -0
- package/dist/components/form/Form.js +163 -0
- package/dist/components/form/context.d.ts +14 -0
- package/dist/components/form/context.js +20 -0
- package/dist/components/form/types.d.ts +115 -0
- package/dist/components/form/types.js +1 -0
- package/dist/components/key-hint/KeyHint.d.ts +10 -0
- package/dist/components/key-hint/KeyHint.js +12 -0
- package/dist/components/multi-select/MultiSelectInput.d.ts +125 -0
- package/dist/components/multi-select/MultiSelectInput.js +287 -0
- package/dist/components/multi-select/types.d.ts +100 -0
- package/dist/components/multi-select/types.js +1 -0
- package/dist/components/number-input/NumberInput.d.ts +11 -0
- package/dist/components/number-input/NumberInput.js +44 -0
- package/dist/components/progress-bar/ProgressBar.d.ts +11 -0
- package/dist/components/progress-bar/ProgressBar.js +9 -0
- package/dist/components/search-input/SearchInput.d.ts +10 -0
- package/dist/components/search-input/SearchInput.js +15 -0
- package/dist/components/select/SelectInput.d.ts +100 -0
- package/dist/components/select/SelectInput.js +147 -0
- package/dist/components/select/types.d.ts +63 -0
- package/dist/components/select/types.js +1 -0
- package/dist/components/spinner/Spinner.d.ts +12 -0
- package/dist/components/spinner/Spinner.js +24 -0
- package/dist/components/tabs/Tabs.d.ts +3 -0
- package/dist/components/tabs/Tabs.js +57 -0
- package/dist/components/tabs/types.d.ts +32 -0
- package/dist/components/tabs/types.js +1 -0
- package/dist/components/text/TextInput.d.ts +39 -0
- package/dist/components/text/TextInput.js +233 -0
- package/dist/components/text/types.d.ts +62 -0
- package/dist/components/text/types.js +1 -0
- package/dist/dev/dev-screen.d.ts +36 -0
- package/dist/dev/dev-screen.js +142 -0
- package/dist/dev/entrance.d.ts +51 -0
- package/dist/dev/entrance.js +67 -0
- package/dist/dev/globalKey-display.d.ts +2 -0
- package/dist/dev/globalKey-display.js +8 -0
- package/dist/dev/types.d.ts +9 -0
- package/dist/dev/types.js +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +50 -0
- package/dist/keyboard/check-global-key.d.ts +18 -0
- package/dist/keyboard/check-global-key.js +52 -0
- package/dist/keyboard/context.d.ts +398 -0
- package/dist/keyboard/context.js +9 -0
- package/dist/keyboard/global-key-processor/index.d.ts +16 -0
- package/dist/keyboard/global-key-processor/index.js +75 -0
- package/dist/keyboard/global-sequence-processor/index.d.ts +14 -0
- package/dist/keyboard/global-sequence-processor/index.js +165 -0
- package/dist/keyboard/hook.d.ts +51 -0
- package/dist/keyboard/hook.js +105 -0
- package/dist/keyboard/index.d.ts +5 -0
- package/dist/keyboard/index.js +3 -0
- package/dist/keyboard/keyNormalizer.d.ts +37 -0
- package/dist/keyboard/keyNormalizer.js +119 -0
- package/dist/keyboard/layer-handler.d.ts +57 -0
- package/dist/keyboard/layer-handler.js +274 -0
- package/dist/keyboard/modal-processor/index.d.ts +16 -0
- package/dist/keyboard/modal-processor/index.js +97 -0
- package/dist/keyboard/overlay-processor/index.d.ts +16 -0
- package/dist/keyboard/overlay-processor/index.js +30 -0
- package/dist/keyboard/pipeline/chain.d.ts +10 -0
- package/dist/keyboard/pipeline/chain.js +45 -0
- package/dist/keyboard/pipeline/context.d.ts +34 -0
- package/dist/keyboard/pipeline/context.js +42 -0
- package/dist/keyboard/pipeline/index.d.ts +3 -0
- package/dist/keyboard/pipeline/index.js +2 -0
- package/dist/keyboard/provider.d.ts +32 -0
- package/dist/keyboard/provider.js +985 -0
- package/dist/keyboard/screen-stack-processor/index.d.ts +14 -0
- package/dist/keyboard/screen-stack-processor/index.js +32 -0
- package/dist/keyboard/types.d.ts +632 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/language/context.d.ts +7 -0
- package/dist/language/context.js +7 -0
- package/dist/language/hook.d.ts +19 -0
- package/dist/language/hook.js +26 -0
- package/dist/language/index.d.ts +3 -0
- package/dist/language/index.js +2 -0
- package/dist/language/provider.d.ts +30 -0
- package/dist/language/provider.js +155 -0
- package/dist/language/types.d.ts +124 -0
- package/dist/language/types.js +1 -0
- package/dist/screen/ModalContext.d.ts +11 -0
- package/dist/screen/ModalContext.js +12 -0
- package/dist/screen/OverlayContext.d.ts +9 -0
- package/dist/screen/OverlayContext.js +10 -0
- package/dist/screen/context.d.ts +61 -0
- package/dist/screen/context.js +8 -0
- package/dist/screen/current-screen.d.ts +16 -0
- package/dist/screen/current-screen.js +38 -0
- package/dist/screen/hook.d.ts +13 -0
- package/dist/screen/hook.js +20 -0
- package/dist/screen/index.d.ts +7 -0
- package/dist/screen/index.js +5 -0
- package/dist/screen/provider.d.ts +112 -0
- package/dist/screen/provider.js +577 -0
- package/dist/screen/registry.d.ts +27 -0
- package/dist/screen/registry.js +71 -0
- package/dist/screen/types.d.ts +227 -0
- package/dist/screen/types.js +1 -0
- package/dist/storage/index.d.ts +45 -0
- package/dist/storage/index.js +46 -0
- package/dist/storage/storage.d.ts +139 -0
- package/dist/storage/storage.js +236 -0
- package/dist/storage/types.d.ts +145 -0
- package/dist/storage/types.js +1 -0
- package/dist/theme/context.d.ts +7 -0
- package/dist/theme/context.js +7 -0
- package/dist/theme/hook.d.ts +11 -0
- package/dist/theme/hook.js +18 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/provider.d.ts +18 -0
- package/dist/theme/provider.js +308 -0
- package/dist/theme/types.d.ts +53 -0
- package/dist/theme/types.js +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import React, { useState, useEffect, useMemo, useCallback, useRef, } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { useKeyboard, useFocusState } from '../../keyboard/hook.js';
|
|
4
|
+
/**
|
|
5
|
+
* Default indicator: shows a blue ❯ pointer when highlighted,
|
|
6
|
+
* otherwise a blank space.
|
|
7
|
+
*/
|
|
8
|
+
function defaultIndicator({ isHighlighted }) {
|
|
9
|
+
return React.createElement(Box, { marginRight: 1 }, isHighlighted
|
|
10
|
+
? React.createElement(Text, { color: 'blue' }, '\u276F')
|
|
11
|
+
: React.createElement(Text, null, ' '));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Default checkbox: shows a green ◉ when selected,
|
|
15
|
+
* otherwise a plain ○.
|
|
16
|
+
*/
|
|
17
|
+
function defaultCheckbox({ isSelected }) {
|
|
18
|
+
return React.createElement(Box, { marginRight: 1 }, React.createElement(Text, { color: 'green' }, isSelected ? '\u25C9' : '\u25CB'));
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Default item renderer: blue label when highlighted,
|
|
22
|
+
* default color otherwise.
|
|
23
|
+
*/
|
|
24
|
+
function defaultItem(props) {
|
|
25
|
+
return React.createElement(Text, { color: props.isHighlighted ? 'blue' : undefined }, props.label);
|
|
26
|
+
}
|
|
27
|
+
function clamp(n, min, max) {
|
|
28
|
+
return Math.max(min, Math.min(n, max));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Multi-select list component integrated with the ink-cartridge keyboard and
|
|
32
|
+
* focus system.
|
|
33
|
+
*
|
|
34
|
+
* Users navigate with arrow keys (or vim-style j/k), toggle items with
|
|
35
|
+
* Space, and confirm with Enter. Number keys 1-9 toggle the corresponding
|
|
36
|
+
* visible item directly.
|
|
37
|
+
*
|
|
38
|
+
* When the component is not the active focus target, its highlight is
|
|
39
|
+
* hidden but selection checkboxes remain visible.
|
|
40
|
+
*
|
|
41
|
+
* Supports both **controlled** (`selected` prop) and **uncontrolled**
|
|
42
|
+
* (`defaultSelected` prop) usage.
|
|
43
|
+
*
|
|
44
|
+
* @typeParam T - The type of the value associated with each item.
|
|
45
|
+
* @typeParam I - The extended item type, must extend Item<T>.
|
|
46
|
+
* Defaults to Item<T>.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* // Uncontrolled
|
|
51
|
+
* <MultiSelectInput
|
|
52
|
+
* focusId="colors"
|
|
53
|
+
* items={[
|
|
54
|
+
* { label: 'Red', value: 'red' },
|
|
55
|
+
* { label: 'Blue', value: 'blue' },
|
|
56
|
+
* ]}
|
|
57
|
+
* defaultSelected={['red']}
|
|
58
|
+
* onSubmit={(vals) => console.log('Confirmed:', vals)}
|
|
59
|
+
* />
|
|
60
|
+
*
|
|
61
|
+
* // Controlled
|
|
62
|
+
* const [sel, setSel] = useState<string[]>([]);
|
|
63
|
+
* <MultiSelectInput
|
|
64
|
+
* focusId="colors"
|
|
65
|
+
* items={items}
|
|
66
|
+
* selected={sel}
|
|
67
|
+
* onChange={setSel}
|
|
68
|
+
* />
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function MultiSelectInput({ items = [], selected: controlledSelected, defaultSelected = [], onChange, onSubmit, onSelect, onUnselect, onHighlight, indicatorComponent, checkboxComponent, itemComponent, focusId, limit: limitProp = 10, initialIndex = 0, storage, storageKey, }) {
|
|
72
|
+
const isFocused = useFocusState(focusId);
|
|
73
|
+
const { boundKeyboard, focusUnregister } = useKeyboard();
|
|
74
|
+
const IndicatorComp = indicatorComponent ?? defaultIndicator;
|
|
75
|
+
const CheckboxComp = checkboxComponent ?? defaultCheckbox;
|
|
76
|
+
const ItemComp = (itemComponent ??
|
|
77
|
+
defaultItem);
|
|
78
|
+
const isControlled = controlledSelected !== undefined;
|
|
79
|
+
const persistKey = storageKey ?? `multi:${focusId}`;
|
|
80
|
+
const [internalSelected, setInternalSelected] = useState(defaultSelected);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (!storage)
|
|
83
|
+
return;
|
|
84
|
+
let cancelled = false;
|
|
85
|
+
storage.read.arr(persistKey, defaultSelected).then((v) => {
|
|
86
|
+
if (!cancelled)
|
|
87
|
+
setInternalSelected(v);
|
|
88
|
+
});
|
|
89
|
+
return () => { cancelled = true; };
|
|
90
|
+
}, [storage, persistKey, defaultSelected]);
|
|
91
|
+
const selectedValues = isControlled
|
|
92
|
+
? controlledSelected
|
|
93
|
+
: internalSelected;
|
|
94
|
+
const selectedSet = useMemo(() => new Set(selectedValues), [selectedValues]);
|
|
95
|
+
const hasLimit = items.length > limitProp;
|
|
96
|
+
const limit = hasLimit ? limitProp : items.length;
|
|
97
|
+
const [highlightedIndex, setHighlightedIndex] = useState(initialIndex);
|
|
98
|
+
const [scrollOffset, setScrollOffset] = useState(0);
|
|
99
|
+
const visibleItems = useMemo(() => {
|
|
100
|
+
if (!hasLimit)
|
|
101
|
+
return items;
|
|
102
|
+
return items.slice(scrollOffset, scrollOffset + limit);
|
|
103
|
+
}, [items, hasLimit, limit, scrollOffset]);
|
|
104
|
+
// 用 ref 保存最新值,避免键盘回调中的闭包过期
|
|
105
|
+
const highlightedIndexRef = useRef(highlightedIndex);
|
|
106
|
+
highlightedIndexRef.current = highlightedIndex;
|
|
107
|
+
const selectedValuesRef = useRef(selectedValues);
|
|
108
|
+
selectedValuesRef.current = selectedValues;
|
|
109
|
+
const selectedSetRef = useRef(selectedSet);
|
|
110
|
+
selectedSetRef.current = selectedSet;
|
|
111
|
+
const visibleItemsRef = useRef(visibleItems);
|
|
112
|
+
visibleItemsRef.current = visibleItems;
|
|
113
|
+
const onChangeRef = useRef(onChange);
|
|
114
|
+
onChangeRef.current = onChange;
|
|
115
|
+
const onSubmitRef = useRef(onSubmit);
|
|
116
|
+
onSubmitRef.current = onSubmit;
|
|
117
|
+
const onSelectRef = useRef(onSelect);
|
|
118
|
+
onSelectRef.current = onSelect;
|
|
119
|
+
const onUnselectRef = useRef(onUnselect);
|
|
120
|
+
onUnselectRef.current = onUnselect;
|
|
121
|
+
const onHighlightRef = useRef(onHighlight);
|
|
122
|
+
onHighlightRef.current = onHighlight;
|
|
123
|
+
const isControlledRef = useRef(isControlled);
|
|
124
|
+
isControlledRef.current = isControlled;
|
|
125
|
+
const allItemsRef = useRef(items);
|
|
126
|
+
allItemsRef.current = items;
|
|
127
|
+
const storageRef = useRef(storage);
|
|
128
|
+
storageRef.current = storage;
|
|
129
|
+
const persistKeyRef = useRef(persistKey);
|
|
130
|
+
persistKeyRef.current = persistKey;
|
|
131
|
+
// items 变化时修正 scroll / highlight 溢出
|
|
132
|
+
useEffect(() => {
|
|
133
|
+
if (items.length === 0) {
|
|
134
|
+
setHighlightedIndex(0);
|
|
135
|
+
setScrollOffset(0);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (scrollOffset + limit > items.length) {
|
|
139
|
+
setScrollOffset(Math.max(0, items.length - limit));
|
|
140
|
+
}
|
|
141
|
+
if (highlightedIndex >= limit ||
|
|
142
|
+
highlightedIndex >= visibleItems.length) {
|
|
143
|
+
setHighlightedIndex(clamp(items.length > 0 ? items.length - 1 : 0, 0, limit - 1));
|
|
144
|
+
}
|
|
145
|
+
}, [
|
|
146
|
+
items.length,
|
|
147
|
+
scrollOffset,
|
|
148
|
+
limit,
|
|
149
|
+
highlightedIndex,
|
|
150
|
+
visibleItems.length,
|
|
151
|
+
]);
|
|
152
|
+
// 高亮变化时通知外部
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
const item = visibleItems[highlightedIndex];
|
|
155
|
+
if (item && onHighlightRef.current) {
|
|
156
|
+
onHighlightRef.current(item);
|
|
157
|
+
}
|
|
158
|
+
}, [highlightedIndex, visibleItems]);
|
|
159
|
+
// 移动高亮光标
|
|
160
|
+
const moveHighlight = useCallback((delta) => {
|
|
161
|
+
setHighlightedIndex((prev) => {
|
|
162
|
+
if (!hasLimit) {
|
|
163
|
+
return clamp(prev + delta, 0, Math.max(0, items.length - 1));
|
|
164
|
+
}
|
|
165
|
+
const absIdx = scrollOffset + prev;
|
|
166
|
+
const newAbs = clamp(absIdx + delta, 0, Math.max(0, items.length - 1));
|
|
167
|
+
if (newAbs < scrollOffset) {
|
|
168
|
+
setScrollOffset(newAbs);
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
if (newAbs >= scrollOffset + limit) {
|
|
172
|
+
setScrollOffset(newAbs - limit + 1);
|
|
173
|
+
return limit - 1;
|
|
174
|
+
}
|
|
175
|
+
return newAbs - scrollOffset;
|
|
176
|
+
});
|
|
177
|
+
}, [hasLimit, items.length, limit, scrollOffset]);
|
|
178
|
+
// 切换选中状态
|
|
179
|
+
const toggleItem = useCallback((item) => {
|
|
180
|
+
const isCurrentlySelected = selectedSetRef.current.has(item.value);
|
|
181
|
+
let newSelected;
|
|
182
|
+
if (isCurrentlySelected) {
|
|
183
|
+
newSelected = selectedValuesRef.current.filter((v) => v !== item.value);
|
|
184
|
+
onUnselectRef.current?.(item);
|
|
185
|
+
}
|
|
186
|
+
else {
|
|
187
|
+
newSelected = [
|
|
188
|
+
...selectedValuesRef.current,
|
|
189
|
+
item.value,
|
|
190
|
+
];
|
|
191
|
+
onSelectRef.current?.(item);
|
|
192
|
+
}
|
|
193
|
+
onChangeRef.current?.(newSelected);
|
|
194
|
+
if (!isControlledRef.current) {
|
|
195
|
+
setInternalSelected(newSelected);
|
|
196
|
+
storageRef.current?.write.arr(persistKeyRef.current, newSelected);
|
|
197
|
+
}
|
|
198
|
+
}, []);
|
|
199
|
+
// 提交
|
|
200
|
+
const handleSubmit = useCallback(() => {
|
|
201
|
+
onSubmitRef.current?.(selectedValuesRef.current);
|
|
202
|
+
}, []);
|
|
203
|
+
// Unregister old focus target when focusId changes
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
return () => focusUnregister(focusId);
|
|
206
|
+
}, [focusId]);
|
|
207
|
+
// 注册键盘绑定
|
|
208
|
+
useEffect(() => {
|
|
209
|
+
const fid = focusId;
|
|
210
|
+
const unUp = boundKeyboard(['up', 'k'], () => moveHighlight(-1), { focusId: fid });
|
|
211
|
+
const unDown = boundKeyboard(['down', 'j'], () => moveHighlight(1), { focusId: fid });
|
|
212
|
+
// 空格键在 Ink 中规范化后是 ' '(非 'space')
|
|
213
|
+
const unSpace = boundKeyboard([' '], () => {
|
|
214
|
+
const item = visibleItemsRef.current[highlightedIndexRef.current];
|
|
215
|
+
if (item)
|
|
216
|
+
toggleItem(item);
|
|
217
|
+
}, { focusId: fid });
|
|
218
|
+
const unReturn = boundKeyboard(['return'], () => handleSubmit(), { focusId: fid });
|
|
219
|
+
// a 全选
|
|
220
|
+
const unSelectAll = boundKeyboard(['a'], () => {
|
|
221
|
+
const allItems = allItemsRef.current;
|
|
222
|
+
const allValues = allItems.map((item) => item.value);
|
|
223
|
+
for (const item of allItems) {
|
|
224
|
+
if (!selectedSetRef.current.has(item.value)) {
|
|
225
|
+
onSelectRef.current?.(item);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
onChangeRef.current?.(allValues);
|
|
229
|
+
if (!isControlledRef.current) {
|
|
230
|
+
setInternalSelected(allValues);
|
|
231
|
+
storageRef.current?.write.arr(persistKeyRef.current, allValues);
|
|
232
|
+
}
|
|
233
|
+
}, { focusId: fid });
|
|
234
|
+
// q 取消全选
|
|
235
|
+
const unDeselectAll = boundKeyboard(['q'], () => {
|
|
236
|
+
const allItems = allItemsRef.current;
|
|
237
|
+
for (const item of allItems) {
|
|
238
|
+
if (selectedSetRef.current.has(item.value)) {
|
|
239
|
+
onUnselectRef.current?.(item);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
onChangeRef.current?.([]);
|
|
243
|
+
if (!isControlledRef.current) {
|
|
244
|
+
setInternalSelected([]);
|
|
245
|
+
storageRef.current?.write.arr(persistKeyRef.current, []);
|
|
246
|
+
}
|
|
247
|
+
}, { focusId: fid });
|
|
248
|
+
// 数字键 1-9 直接切换对应可见项
|
|
249
|
+
const numUnbinds = [];
|
|
250
|
+
for (let i = 1; i <= Math.min(9, visibleItems.length); i++) {
|
|
251
|
+
const idx = i - 1;
|
|
252
|
+
numUnbinds.push(boundKeyboard([String(i)], () => {
|
|
253
|
+
const item = visibleItemsRef.current[idx];
|
|
254
|
+
if (item)
|
|
255
|
+
toggleItem(item);
|
|
256
|
+
}, { focusId: fid }));
|
|
257
|
+
}
|
|
258
|
+
return () => {
|
|
259
|
+
unUp();
|
|
260
|
+
unDown();
|
|
261
|
+
unSpace();
|
|
262
|
+
unReturn();
|
|
263
|
+
unSelectAll();
|
|
264
|
+
unDeselectAll();
|
|
265
|
+
numUnbinds.forEach((fn) => fn());
|
|
266
|
+
};
|
|
267
|
+
}, [
|
|
268
|
+
focusId,
|
|
269
|
+
boundKeyboard,
|
|
270
|
+
moveHighlight,
|
|
271
|
+
toggleItem,
|
|
272
|
+
handleSubmit,
|
|
273
|
+
visibleItems.length,
|
|
274
|
+
]);
|
|
275
|
+
return React.createElement(Box, { flexDirection: 'column' }, ...visibleItems.map((item, index) => {
|
|
276
|
+
const isItemHighlighted = index === highlightedIndex && isFocused;
|
|
277
|
+
const isItemSelected = selectedSet.has(item.value);
|
|
278
|
+
return React.createElement(Box, { key: item.Key ?? String(item.value) }, React.createElement(IndicatorComp, {
|
|
279
|
+
isHighlighted: isItemHighlighted,
|
|
280
|
+
}), React.createElement(CheckboxComp, {
|
|
281
|
+
isSelected: isItemSelected,
|
|
282
|
+
}), React.createElement(ItemComp, {
|
|
283
|
+
...item,
|
|
284
|
+
isHighlighted: isItemHighlighted,
|
|
285
|
+
}));
|
|
286
|
+
}));
|
|
287
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { Item } from '../select/types.js';
|
|
3
|
+
import type { StorageAPI } from '../../storage/index.js';
|
|
4
|
+
export type { Item };
|
|
5
|
+
/**
|
|
6
|
+
* Props for the MultiSelectInput component.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam T - The type of the value associated with each item.
|
|
9
|
+
* @typeParam I - The extended item type, must extend Item<T>. Defaults to Item<T>.
|
|
10
|
+
*/
|
|
11
|
+
export interface MultiSelectInputProps<T, I extends Item<T> = Item<T>> {
|
|
12
|
+
/**
|
|
13
|
+
* Items to display in the list.
|
|
14
|
+
* Each item must have `label` and `value`, and optionally a `Key` for React key.
|
|
15
|
+
*/
|
|
16
|
+
readonly items: I[];
|
|
17
|
+
/**
|
|
18
|
+
* Controlled mode: currently selected values.
|
|
19
|
+
* When provided, the component defers selection state to the parent.
|
|
20
|
+
* When `undefined`, the component manages its own state (uncontrolled).
|
|
21
|
+
*/
|
|
22
|
+
readonly selected?: T[];
|
|
23
|
+
/**
|
|
24
|
+
* Uncontrolled mode: initially selected values.
|
|
25
|
+
* Only used when `selected` is `undefined`.
|
|
26
|
+
*
|
|
27
|
+
* @default []
|
|
28
|
+
*/
|
|
29
|
+
readonly defaultSelected?: T[];
|
|
30
|
+
/**
|
|
31
|
+
* Called whenever the set of selected values changes.
|
|
32
|
+
* Receives the full array of currently selected values.
|
|
33
|
+
*/
|
|
34
|
+
readonly onChange?: (selected: T[]) => void;
|
|
35
|
+
/**
|
|
36
|
+
* Called when the user presses Enter to confirm their selection.
|
|
37
|
+
* Receives the full array of currently selected values.
|
|
38
|
+
*/
|
|
39
|
+
readonly onSubmit?: (selected: T[]) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Called when an individual item is selected (toggled on).
|
|
42
|
+
*/
|
|
43
|
+
readonly onSelect?: (item: I) => void;
|
|
44
|
+
/**
|
|
45
|
+
* Called when an individual item is unselected (toggled off).
|
|
46
|
+
*/
|
|
47
|
+
readonly onUnselect?: (item: I) => void;
|
|
48
|
+
/**
|
|
49
|
+
* Called when the highlight cursor moves to a different item.
|
|
50
|
+
*/
|
|
51
|
+
readonly onHighlight?: (item: I) => void;
|
|
52
|
+
/**
|
|
53
|
+
* Custom indicator component rendered before the checkbox.
|
|
54
|
+
* Receives `isHighlighted` to show which item the cursor is on.
|
|
55
|
+
*/
|
|
56
|
+
readonly indicatorComponent?: React.ComponentType<{
|
|
57
|
+
isHighlighted: boolean;
|
|
58
|
+
}>;
|
|
59
|
+
/**
|
|
60
|
+
* Custom checkbox component rendered between indicator and item.
|
|
61
|
+
* Receives `isSelected` to show whether the item is checked.
|
|
62
|
+
*/
|
|
63
|
+
readonly checkboxComponent?: React.ComponentType<{
|
|
64
|
+
isSelected: boolean;
|
|
65
|
+
}>;
|
|
66
|
+
/**
|
|
67
|
+
* Custom item renderer.
|
|
68
|
+
* Receives all item properties plus `isHighlighted`.
|
|
69
|
+
*/
|
|
70
|
+
readonly itemComponent?: React.ComponentType<I & {
|
|
71
|
+
isHighlighted: boolean;
|
|
72
|
+
}>;
|
|
73
|
+
/**
|
|
74
|
+
* Focus identifier used by the keyboard system.
|
|
75
|
+
* Must be unique on the current screen.
|
|
76
|
+
*/
|
|
77
|
+
readonly focusId: string;
|
|
78
|
+
/**
|
|
79
|
+
* Maximum number of visible items before enabling scroll.
|
|
80
|
+
*
|
|
81
|
+
* @default 10
|
|
82
|
+
*/
|
|
83
|
+
readonly limit?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Index of the initially highlighted item (0-based).
|
|
86
|
+
*
|
|
87
|
+
* @default 0
|
|
88
|
+
*/
|
|
89
|
+
readonly initialIndex?: number;
|
|
90
|
+
/**
|
|
91
|
+
* Optional persistence instance. When provided, the selected values are
|
|
92
|
+
* automatically saved and restored across sessions.
|
|
93
|
+
*/
|
|
94
|
+
readonly storage?: StorageAPI;
|
|
95
|
+
/**
|
|
96
|
+
* Storage key used for persistence. Defaults to `"multi:<focusId>"` when
|
|
97
|
+
* not provided.
|
|
98
|
+
*/
|
|
99
|
+
readonly storageKey?: string;
|
|
100
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface NumberInputProps {
|
|
3
|
+
focusId: string;
|
|
4
|
+
value: number;
|
|
5
|
+
onChange: (value: number) => void;
|
|
6
|
+
min?: number;
|
|
7
|
+
max?: number;
|
|
8
|
+
step?: number;
|
|
9
|
+
}
|
|
10
|
+
export declare function NumberInput({ focusId, value, onChange, min, max, step, }: NumberInputProps): React.JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
import { useKeyboard, useFocusState } from '../../keyboard/index.js';
|
|
4
|
+
export function NumberInput({ focusId, value, onChange, min = -Infinity, max = Infinity, step = 1, }) {
|
|
5
|
+
const isFocused = useFocusState(focusId);
|
|
6
|
+
const { boundKeyboard, focusUnregister } = useKeyboard();
|
|
7
|
+
// Unregister old focus target when focusId changes
|
|
8
|
+
useEffect(() => {
|
|
9
|
+
return () => focusUnregister(focusId);
|
|
10
|
+
}, [focusId]);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const up = boundKeyboard(['up', 'right'], () => {
|
|
13
|
+
const next = Math.min(value + step, max);
|
|
14
|
+
if (next !== value)
|
|
15
|
+
onChange(next);
|
|
16
|
+
}, { focusId });
|
|
17
|
+
const down = boundKeyboard(['down', 'left'], () => {
|
|
18
|
+
const next = Math.max(value - step, min);
|
|
19
|
+
if (next !== value)
|
|
20
|
+
onChange(next);
|
|
21
|
+
}, { focusId });
|
|
22
|
+
// Capture digit input so number keys don't leak to other
|
|
23
|
+
// focus targets (e.g. SelectInput's 1-9 bindings).
|
|
24
|
+
// Guard: skip non-digit input and reject NaN propagation
|
|
25
|
+
const wildcard = boundKeyboard(['*'], (input) => {
|
|
26
|
+
if (isNaN(value))
|
|
27
|
+
return;
|
|
28
|
+
const digit = parseInt(input, 10);
|
|
29
|
+
if (isNaN(digit))
|
|
30
|
+
return;
|
|
31
|
+
// Append digit: 25 + '3' → 253, clamped to [min, max]
|
|
32
|
+
const next = Math.min(Math.max(Number(String(value) + String(digit)), min), max);
|
|
33
|
+
if (next !== value)
|
|
34
|
+
onChange(next);
|
|
35
|
+
}, { focusId });
|
|
36
|
+
return () => {
|
|
37
|
+
up();
|
|
38
|
+
down();
|
|
39
|
+
wildcard();
|
|
40
|
+
};
|
|
41
|
+
}, [focusId, value, min, max, step, onChange]);
|
|
42
|
+
const text = isFocused ? `${value}█` : String(value);
|
|
43
|
+
return React.createElement(Text, null, text);
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface ProgressBarProps {
|
|
3
|
+
percent?: number;
|
|
4
|
+
width?: number;
|
|
5
|
+
color?: string;
|
|
6
|
+
showPercent?: boolean;
|
|
7
|
+
char?: string;
|
|
8
|
+
emptyChar?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function ProgressBar({ percent, width, color, showPercent, char, emptyChar, }: ProgressBarProps): React.JSX.Element;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Text } from 'ink';
|
|
3
|
+
export function ProgressBar({ percent = 0, width = 20, color = 'cyan', showPercent = true, char = '█', emptyChar = '░', }) {
|
|
4
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
5
|
+
const filled = Math.round((clamped / 100) * width);
|
|
6
|
+
const bar = char.repeat(filled) + emptyChar.repeat(width - filled);
|
|
7
|
+
const content = showPercent ? `[${bar}] ${clamped}%` : `[${bar}]`;
|
|
8
|
+
return React.createElement(Text, { color: color }, content);
|
|
9
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
interface SearchInputProps {
|
|
3
|
+
focusId: string;
|
|
4
|
+
value: string;
|
|
5
|
+
onChange: (value: string) => void;
|
|
6
|
+
placeholder?: string;
|
|
7
|
+
onSubmit?: (value: string) => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function SearchInput({ focusId, value, onChange, placeholder, onSubmit, }: SearchInputProps): React.JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { TextInput } from '../text/TextInput.js';
|
|
4
|
+
import { useKeyboard } from '../../keyboard/index.js';
|
|
5
|
+
export function SearchInput({ focusId, value, onChange, placeholder, onSubmit, }) {
|
|
6
|
+
const { boundKeyboard } = useKeyboard();
|
|
7
|
+
useEffect(() => {
|
|
8
|
+
const unEsc = boundKeyboard(['escape'], () => onChange(''), { focusId });
|
|
9
|
+
return () => { unEsc(); };
|
|
10
|
+
}, [focusId, onChange]);
|
|
11
|
+
return (React.createElement(Box, null,
|
|
12
|
+
React.createElement(Text, { color: "blue" }, "Search "),
|
|
13
|
+
React.createElement(TextInput, { focusId: focusId, value: value, onChange: onChange, placeholder: placeholder, onSubmit: onSubmit }),
|
|
14
|
+
value ? React.createElement(Text, { color: "grey" }, " \u2573") : null));
|
|
15
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { SelectInputProps, Item } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A single-select list component integrated with the ink-cartridge keyboard and
|
|
5
|
+
* focus system.
|
|
6
|
+
*
|
|
7
|
+
* Each instance registers a focus target identified by {@link SelectInputProps.focusId}
|
|
8
|
+
* on the current screen's keyboard layer. Users navigate between multiple
|
|
9
|
+
* SelectInputs on the same screen with Tab / Shift+Tab. Within the active
|
|
10
|
+
* component, arrow keys (or vim-style j/k) move the highlight, and Enter
|
|
11
|
+
* confirms the selection. Number keys 1-9 directly select an item.
|
|
12
|
+
*
|
|
13
|
+
* When the component is not the active focus target, its items are visually
|
|
14
|
+
* dimmed and no key events are delivered to it.
|
|
15
|
+
*
|
|
16
|
+
* @typeParam T - The type of the value associated with each item.
|
|
17
|
+
* @typeParam I - The extended item type, must extend Item<T>. Defaults to Item<T>.
|
|
18
|
+
*/
|
|
19
|
+
export declare function SelectInput<T, I extends Item<T> = Item<T>>({ items, onSelect, itemComponent, indicatorComponent, focusId, limit: limitProp, storage, storageKey, }: SelectInputProps<T, I>): React.FunctionComponentElement<{
|
|
20
|
+
readonly position?: "absolute" | "relative" | "static" | undefined;
|
|
21
|
+
readonly top?: number | string | undefined;
|
|
22
|
+
readonly right?: number | string | undefined;
|
|
23
|
+
readonly bottom?: number | string | undefined;
|
|
24
|
+
readonly left?: number | string | undefined;
|
|
25
|
+
readonly columnGap?: number | undefined;
|
|
26
|
+
readonly rowGap?: number | undefined;
|
|
27
|
+
readonly gap?: number | undefined;
|
|
28
|
+
readonly margin?: number | undefined;
|
|
29
|
+
readonly marginX?: number | undefined;
|
|
30
|
+
readonly marginY?: number | undefined;
|
|
31
|
+
readonly marginTop?: number | undefined;
|
|
32
|
+
readonly marginBottom?: number | undefined;
|
|
33
|
+
readonly marginLeft?: number | undefined;
|
|
34
|
+
readonly marginRight?: number | undefined;
|
|
35
|
+
readonly padding?: number | undefined;
|
|
36
|
+
readonly paddingX?: number | undefined;
|
|
37
|
+
readonly paddingY?: number | undefined;
|
|
38
|
+
readonly paddingTop?: number | undefined;
|
|
39
|
+
readonly paddingBottom?: number | undefined;
|
|
40
|
+
readonly paddingLeft?: number | undefined;
|
|
41
|
+
readonly paddingRight?: number | undefined;
|
|
42
|
+
readonly flexGrow?: number | undefined;
|
|
43
|
+
readonly flexShrink?: number | undefined;
|
|
44
|
+
readonly flexDirection?: "row" | "column" | "row-reverse" | "column-reverse" | undefined;
|
|
45
|
+
readonly flexBasis?: number | string | undefined;
|
|
46
|
+
readonly flexWrap?: "nowrap" | "wrap" | "wrap-reverse" | undefined;
|
|
47
|
+
readonly alignItems?: "flex-start" | "center" | "flex-end" | "stretch" | "baseline" | undefined;
|
|
48
|
+
readonly alignSelf?: "flex-start" | "center" | "flex-end" | "auto" | "stretch" | "baseline" | undefined;
|
|
49
|
+
readonly alignContent?: "flex-start" | "flex-end" | "center" | "stretch" | "space-between" | "space-around" | "space-evenly" | undefined;
|
|
50
|
+
readonly justifyContent?: "flex-start" | "flex-end" | "space-between" | "space-around" | "space-evenly" | "center" | undefined;
|
|
51
|
+
readonly width?: number | string | undefined;
|
|
52
|
+
readonly height?: number | string | undefined;
|
|
53
|
+
readonly minWidth?: number | string | undefined;
|
|
54
|
+
readonly minHeight?: number | string | undefined;
|
|
55
|
+
readonly maxWidth?: number | string | undefined;
|
|
56
|
+
readonly maxHeight?: number | string | undefined;
|
|
57
|
+
readonly aspectRatio?: number | undefined;
|
|
58
|
+
readonly display?: "flex" | "none" | undefined;
|
|
59
|
+
readonly borderStyle?: (keyof import("cli-boxes").Boxes | import("cli-boxes").BoxStyle) | undefined;
|
|
60
|
+
readonly borderTop?: boolean | undefined;
|
|
61
|
+
readonly borderBottom?: boolean | undefined;
|
|
62
|
+
readonly borderLeft?: boolean | undefined;
|
|
63
|
+
readonly borderRight?: boolean | undefined;
|
|
64
|
+
readonly borderColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
65
|
+
readonly borderTopColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
66
|
+
readonly borderBottomColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
67
|
+
readonly borderLeftColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
68
|
+
readonly borderRightColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
69
|
+
readonly borderDimColor?: boolean | undefined;
|
|
70
|
+
readonly borderTopDimColor?: boolean | undefined;
|
|
71
|
+
readonly borderBottomDimColor?: boolean | undefined;
|
|
72
|
+
readonly borderLeftDimColor?: boolean | undefined;
|
|
73
|
+
readonly borderRightDimColor?: boolean | undefined;
|
|
74
|
+
readonly borderBackgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
75
|
+
readonly borderTopBackgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
76
|
+
readonly borderBottomBackgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
77
|
+
readonly borderLeftBackgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
78
|
+
readonly borderRightBackgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
79
|
+
readonly overflow?: "visible" | "hidden" | undefined;
|
|
80
|
+
readonly overflowX?: "visible" | "hidden" | undefined;
|
|
81
|
+
readonly overflowY?: "visible" | "hidden" | undefined;
|
|
82
|
+
readonly backgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
83
|
+
} & {
|
|
84
|
+
readonly 'aria-label'?: string;
|
|
85
|
+
readonly 'aria-hidden'?: boolean;
|
|
86
|
+
readonly 'aria-role'?: "button" | "checkbox" | "combobox" | "list" | "listbox" | "listitem" | "menu" | "menuitem" | "option" | "progressbar" | "radio" | "radiogroup" | "tab" | "tablist" | "table" | "textbox" | "timer" | "toolbar";
|
|
87
|
+
readonly 'aria-state'?: {
|
|
88
|
+
readonly busy?: boolean;
|
|
89
|
+
readonly checked?: boolean;
|
|
90
|
+
readonly disabled?: boolean;
|
|
91
|
+
readonly expanded?: boolean;
|
|
92
|
+
readonly multiline?: boolean;
|
|
93
|
+
readonly multiselectable?: boolean;
|
|
94
|
+
readonly readonly?: boolean;
|
|
95
|
+
readonly required?: boolean;
|
|
96
|
+
readonly selected?: boolean;
|
|
97
|
+
};
|
|
98
|
+
} & {
|
|
99
|
+
children?: React.ReactNode | undefined;
|
|
100
|
+
} & React.RefAttributes<import("ink").DOMElement>>;
|