ink-cartridge 3.8.0 → 3.8.2
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/README.md +14 -5
- package/dist/components/multi-select/MultiSelectInput.d.ts +1 -82
- package/dist/dev/dev-screen.d.ts +2 -2
- package/dist/dev/dev-screen.js +13 -9
- package/dist/dev/entrance.d.ts +27 -22
- package/dist/dev/entrance.js +32 -32
- package/dist/dev/globalKey-display.js +2 -1
- package/dist/dev/globalSeq-display.js +2 -1
- package/dist/dev/layerKey-display.js +15 -14
- package/dist/dev/types.d.ts +4 -0
- package/dist/index.d.ts +1 -1
- package/dist/keyboard/context.d.ts +9 -5
- package/dist/keyboard/hook.d.ts +1 -1
- package/dist/keyboard/hook.js +57 -18
- package/dist/keyboard/index.d.ts +1 -1
- package/dist/keyboard/layer-handler.d.ts +3 -3
- package/dist/keyboard/layer-handler.js +22 -22
- package/dist/keyboard/modal-processor/index.js +2 -2
- package/dist/keyboard/provider.d.ts +1 -1
- package/dist/keyboard/provider.js +6 -6
- package/dist/keyboard/types.d.ts +9 -9
- package/dist/screen/ModalContext.d.ts +12 -3
- package/dist/screen/ModalContext.js +6 -2
- package/dist/screen/OverlayContext.d.ts +14 -3
- package/dist/screen/OverlayContext.js +8 -2
- package/dist/screen/current-screen.js +6 -3
- package/dist/screen/provider.d.ts +13 -4
- package/dist/screen/provider.js +89 -26
- package/dist/screen/types.d.ts +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<br>
|
|
3
|
+
<br>
|
|
4
|
+
<img width="440" alt="cartridge" src="static/cartridge.png">
|
|
5
|
+
<br>
|
|
6
|
+
<br>
|
|
7
|
+
<br>
|
|
8
|
+
</div>
|
|
2
9
|
|
|
3
|
-
>
|
|
10
|
+
<h1 align="center">Cartridge</h1>
|
|
11
|
+
|
|
12
|
+
>A component kit for rapidly building complex, multi-page, interaction-heavy terminal applications — filling the critical gaps Ink leaves open.
|
|
4
13
|
|
|
5
14
|
[](https://github.com/BAIGAOa/ink-trc/actions/workflows/ci.yml)
|
|
6
15
|
[](https://www.npmjs.com/package/ink-cartridge)
|
|
@@ -47,7 +56,7 @@ npx ink-cartridge init my-tui
|
|
|
47
56
|
- [useKeyboard](docs/keyboard/useKeyboard-API.md)
|
|
48
57
|
- [boundKeyboard](docs/keyboard/boundKeyboard-API.md)
|
|
49
58
|
- [boundSequence](docs/keyboard/boundSequence-API.md)
|
|
50
|
-
- [
|
|
59
|
+
- [penetration](docs/keyboard/penetration-API.md)
|
|
51
60
|
- [stop](docs/keyboard/stop-API.md)
|
|
52
61
|
- [globalKeys](docs/keyboard/globalKeys-API.md)
|
|
53
62
|
- [globalSequence](docs/keyboard/globalSequence-API.md)
|
|
@@ -157,8 +166,8 @@ npx ink-cartridge init my-tui
|
|
|
157
166
|
|
|
158
167
|
## Other
|
|
159
168
|
|
|
160
|
-
The method `
|
|
169
|
+
The method `penetration` (formerly `blockedKey`) marks keys as transparent (pass-through). Renamed in v3.8.1 to eliminate the historical naming confusion.
|
|
161
170
|
|
|
162
171
|
## License
|
|
163
172
|
|
|
164
|
-
[MIT](LICENSE)
|
|
173
|
+
[MIT](LICENSE)
|
|
@@ -41,85 +41,4 @@ import type { MultiSelectInputProps, Item } from './types.js';
|
|
|
41
41
|
* />
|
|
42
42
|
* ```
|
|
43
43
|
*/
|
|
44
|
-
export declare function MultiSelectInput<T, I extends Item<T> = Item<T>>({ items, selected: controlledSelected, defaultSelected, onChange, onSubmit, onSelect, onUnselect, onHighlight, indicatorComponent, checkboxComponent, itemComponent, focusId, limit: limitProp, initialIndex, storage, storageKey, }: MultiSelectInputProps<T, I>): React.
|
|
45
|
-
readonly position?: "absolute" | "relative" | "static" | undefined;
|
|
46
|
-
readonly top?: number | string | undefined;
|
|
47
|
-
readonly right?: number | string | undefined;
|
|
48
|
-
readonly bottom?: number | string | undefined;
|
|
49
|
-
readonly left?: number | string | undefined;
|
|
50
|
-
readonly columnGap?: number | undefined;
|
|
51
|
-
readonly rowGap?: number | undefined;
|
|
52
|
-
readonly gap?: number | undefined;
|
|
53
|
-
readonly margin?: number | undefined;
|
|
54
|
-
readonly marginX?: number | undefined;
|
|
55
|
-
readonly marginY?: number | undefined;
|
|
56
|
-
readonly marginTop?: number | undefined;
|
|
57
|
-
readonly marginBottom?: number | undefined;
|
|
58
|
-
readonly marginLeft?: number | undefined;
|
|
59
|
-
readonly marginRight?: number | undefined;
|
|
60
|
-
readonly padding?: number | undefined;
|
|
61
|
-
readonly paddingX?: number | undefined;
|
|
62
|
-
readonly paddingY?: number | undefined;
|
|
63
|
-
readonly paddingTop?: number | undefined;
|
|
64
|
-
readonly paddingBottom?: number | undefined;
|
|
65
|
-
readonly paddingLeft?: number | undefined;
|
|
66
|
-
readonly paddingRight?: number | undefined;
|
|
67
|
-
readonly flexGrow?: number | undefined;
|
|
68
|
-
readonly flexShrink?: number | undefined;
|
|
69
|
-
readonly flexDirection?: "row" | "column" | "row-reverse" | "column-reverse" | undefined;
|
|
70
|
-
readonly flexBasis?: number | string | undefined;
|
|
71
|
-
readonly flexWrap?: "nowrap" | "wrap" | "wrap-reverse" | undefined;
|
|
72
|
-
readonly alignItems?: "flex-start" | "center" | "flex-end" | "stretch" | "baseline" | undefined;
|
|
73
|
-
readonly alignSelf?: "flex-start" | "center" | "flex-end" | "auto" | "stretch" | "baseline" | undefined;
|
|
74
|
-
readonly alignContent?: "flex-start" | "flex-end" | "center" | "stretch" | "space-between" | "space-around" | "space-evenly" | undefined;
|
|
75
|
-
readonly justifyContent?: "flex-start" | "flex-end" | "space-between" | "space-around" | "space-evenly" | "center" | undefined;
|
|
76
|
-
readonly width?: number | string | undefined;
|
|
77
|
-
readonly height?: number | string | undefined;
|
|
78
|
-
readonly minWidth?: number | string | undefined;
|
|
79
|
-
readonly minHeight?: number | string | undefined;
|
|
80
|
-
readonly maxWidth?: number | string | undefined;
|
|
81
|
-
readonly maxHeight?: number | string | undefined;
|
|
82
|
-
readonly aspectRatio?: number | undefined;
|
|
83
|
-
readonly display?: "flex" | "none" | undefined;
|
|
84
|
-
readonly borderStyle?: (keyof import("cli-boxes").Boxes | import("cli-boxes").BoxStyle) | undefined;
|
|
85
|
-
readonly borderTop?: boolean | undefined;
|
|
86
|
-
readonly borderBottom?: boolean | undefined;
|
|
87
|
-
readonly borderLeft?: boolean | undefined;
|
|
88
|
-
readonly borderRight?: boolean | undefined;
|
|
89
|
-
readonly borderColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
90
|
-
readonly borderTopColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
91
|
-
readonly borderBottomColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
92
|
-
readonly borderLeftColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
93
|
-
readonly borderRightColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
94
|
-
readonly borderDimColor?: boolean | undefined;
|
|
95
|
-
readonly borderTopDimColor?: boolean | undefined;
|
|
96
|
-
readonly borderBottomDimColor?: boolean | undefined;
|
|
97
|
-
readonly borderLeftDimColor?: boolean | undefined;
|
|
98
|
-
readonly borderRightDimColor?: boolean | undefined;
|
|
99
|
-
readonly borderBackgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
100
|
-
readonly borderTopBackgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
101
|
-
readonly borderBottomBackgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
102
|
-
readonly borderLeftBackgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
103
|
-
readonly borderRightBackgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
104
|
-
readonly overflow?: "visible" | "hidden" | undefined;
|
|
105
|
-
readonly overflowX?: "visible" | "hidden" | undefined;
|
|
106
|
-
readonly overflowY?: "visible" | "hidden" | undefined;
|
|
107
|
-
readonly backgroundColor?: import("type-fest").LiteralUnion<import("ansi-styles").ForegroundColorName, string> | undefined;
|
|
108
|
-
} & {
|
|
109
|
-
readonly 'aria-label'?: string;
|
|
110
|
-
readonly 'aria-hidden'?: boolean;
|
|
111
|
-
readonly 'aria-role'?: "button" | "checkbox" | "combobox" | "list" | "listbox" | "listitem" | "menu" | "menuitem" | "option" | "progressbar" | "radio" | "radiogroup" | "tab" | "tablist" | "table" | "textbox" | "timer" | "toolbar";
|
|
112
|
-
readonly 'aria-state'?: {
|
|
113
|
-
readonly busy?: boolean;
|
|
114
|
-
readonly checked?: boolean;
|
|
115
|
-
readonly disabled?: boolean;
|
|
116
|
-
readonly expanded?: boolean;
|
|
117
|
-
readonly multiline?: boolean;
|
|
118
|
-
readonly multiselectable?: boolean;
|
|
119
|
-
readonly readonly?: boolean;
|
|
120
|
-
readonly required?: boolean;
|
|
121
|
-
readonly selected?: boolean;
|
|
122
|
-
};
|
|
123
|
-
} & {
|
|
124
|
-
children?: React.ReactNode | undefined;
|
|
125
|
-
} & React.RefAttributes<import("ink").DOMElement>>;
|
|
44
|
+
export declare function MultiSelectInput<T, I extends Item<T> = Item<T>>({ items, selected: controlledSelected, defaultSelected, onChange, onSubmit, onSelect, onUnselect, onHighlight, indicatorComponent, checkboxComponent, itemComponent, focusId, limit: limitProp, initialIndex, storage, storageKey, }: MultiSelectInputProps<T, I>): React.JSX.Element;
|
package/dist/dev/dev-screen.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { DevProps } from "./types.js";
|
|
|
18
18
|
* The panel registers itself via `registerComponent` so it participates
|
|
19
19
|
* in the modal keyboard layer automatically.
|
|
20
20
|
*
|
|
21
|
-
* As a modal, DevScreen
|
|
21
|
+
* As a modal, DevScreen consumes all keyboard events from reaching overlays
|
|
22
22
|
* and screens while open.
|
|
23
23
|
*
|
|
24
24
|
* @param top - Initial vertical position in rows (0 = top of terminal).
|
|
@@ -33,4 +33,4 @@ import { DevProps } from "./types.js";
|
|
|
33
33
|
*
|
|
34
34
|
* @2026-06-23 3.6.1
|
|
35
35
|
*/
|
|
36
|
-
export declare function DevScreen({ top: initialTop, left }: DevProps): React.JSX.Element;
|
|
36
|
+
export declare function DevScreen({ top: initialTop, left, allowKeys }: DevProps): React.JSX.Element;
|
package/dist/dev/dev-screen.js
CHANGED
|
@@ -92,7 +92,7 @@ function AllFocusSummary({ currentPath, activeOverlayIds, activeModalId, readLay
|
|
|
92
92
|
}
|
|
93
93
|
/**
|
|
94
94
|
* Renders a compact summary of the keyboard layer for the top screen
|
|
95
|
-
* component: bindings, sequences, stopped keys, and
|
|
95
|
+
* component: bindings, sequences, stopped keys, and penetration keys.
|
|
96
96
|
*
|
|
97
97
|
* Focus targets are displayed separately by {@link AllFocusSummary}
|
|
98
98
|
* so that all layers' targets are visible at once.
|
|
@@ -107,7 +107,7 @@ function LayerSummary({ topComponent, readLayer }) {
|
|
|
107
107
|
const seqCount = [...layer.sequences.values()].reduce((s, a) => s + a.length, 0);
|
|
108
108
|
const seqFirstKeys = [...layer.sequences.keys()].join(' ');
|
|
109
109
|
const stopped = layer.stoppedKeys.map(r => Array.isArray(r.key) ? r.key.join(',') : r.key).join(' ');
|
|
110
|
-
const
|
|
110
|
+
const penetrated = layer.penetrationKeys.map(r => Array.isArray(r.key) ? r.key.join(',') : r.key).join(' ');
|
|
111
111
|
return (React.createElement(Box, { flexDirection: "column", paddingY: 1 },
|
|
112
112
|
React.createElement(Box, { flexDirection: "row" },
|
|
113
113
|
React.createElement(Text, { color: "cyan" },
|
|
@@ -128,9 +128,9 @@ function LayerSummary({ topComponent, readLayer }) {
|
|
|
128
128
|
stopped && (React.createElement(Box, { flexDirection: "row" },
|
|
129
129
|
React.createElement(Text, { color: "red" }, "Stopped: "),
|
|
130
130
|
React.createElement(Text, { dimColor: true }, stopped))),
|
|
131
|
-
|
|
132
|
-
React.createElement(Text, { color: "gray" }, "
|
|
133
|
-
React.createElement(Text, { dimColor: true },
|
|
131
|
+
penetrated && (React.createElement(Box, { flexDirection: "row" },
|
|
132
|
+
React.createElement(Text, { color: "gray" }, "Penetr: "),
|
|
133
|
+
React.createElement(Text, { dimColor: true }, penetrated)))));
|
|
134
134
|
}
|
|
135
135
|
/**
|
|
136
136
|
* Developer debugging modal for the ink-cartridge screen system.
|
|
@@ -150,7 +150,7 @@ function LayerSummary({ topComponent, readLayer }) {
|
|
|
150
150
|
* The panel registers itself via `registerComponent` so it participates
|
|
151
151
|
* in the modal keyboard layer automatically.
|
|
152
152
|
*
|
|
153
|
-
* As a modal, DevScreen
|
|
153
|
+
* As a modal, DevScreen consumes all keyboard events from reaching overlays
|
|
154
154
|
* and screens while open.
|
|
155
155
|
*
|
|
156
156
|
* @param top - Initial vertical position in rows (0 = top of terminal).
|
|
@@ -165,10 +165,11 @@ function LayerSummary({ topComponent, readLayer }) {
|
|
|
165
165
|
*
|
|
166
166
|
* @2026-06-23 3.6.1
|
|
167
167
|
*/
|
|
168
|
-
export function DevScreen({ top: initialTop, left }) {
|
|
168
|
+
export function DevScreen({ top: initialTop, left, allowKeys }) {
|
|
169
169
|
const { currentPath, displayedOverlays, activeOverlayIds, modalQueue, activeModalId, } = useScreenSystem();
|
|
170
|
-
const { boundKeyboard, readLayer } = useKeyboard();
|
|
171
|
-
const
|
|
170
|
+
const { boundKeyboard, readLayer, allowModal } = useKeyboard();
|
|
171
|
+
const modalCtx = useContext(ModalContext);
|
|
172
|
+
const modalId = modalCtx?.id ?? null;
|
|
172
173
|
const { rows } = useWindowSize();
|
|
173
174
|
const { openModal } = useScreenSystem();
|
|
174
175
|
const [offsetTop, setOffsetTop] = useState(initialTop);
|
|
@@ -200,6 +201,9 @@ export function DevScreen({ top: initialTop, left }) {
|
|
|
200
201
|
clearTimeout(flashTimerRef.current);
|
|
201
202
|
};
|
|
202
203
|
}, []);
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
return allowModal(allowKeys ?? []);
|
|
206
|
+
}, [allowModal]);
|
|
203
207
|
useEffect(() => {
|
|
204
208
|
const u1 = boundKeyboard(['up'], () => setOffsetTop(prev => clampTopRef.current(prev - 1)));
|
|
205
209
|
const u2 = boundKeyboard(['down'], () => setOffsetTop(prev => clampTopRef.current(prev + 1)));
|
package/dist/dev/entrance.d.ts
CHANGED
|
@@ -3,43 +3,48 @@ import { DevProps } from "./types.js";
|
|
|
3
3
|
* Open the developer debugging modal.
|
|
4
4
|
*
|
|
5
5
|
* Calls {@link openModal} with the fixed modal ID `_Dev-Tool_` and
|
|
6
|
-
* the {@link DevScreen} component
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* the {@link DevScreen} component, using `persistent: true` by default
|
|
7
|
+
* so the panel survives screen navigation. Keyboard focus is
|
|
8
|
+
* automatically suspended when navigating away and restored when
|
|
9
|
+
* returning to the originating screen.
|
|
10
|
+
*
|
|
11
|
+
* Safe to call when the modal is already open — the underlying
|
|
12
|
+
* {@link openModal} treats duplicate IDs as a no-op, so a simple key
|
|
13
|
+
* binding without a toggle ref suffices:
|
|
14
|
+
*
|
|
15
|
+
* {@link closeDevTool} is the inverse.
|
|
9
16
|
*
|
|
10
17
|
* When open, DevScreen blocks all keyboard events from reaching
|
|
11
|
-
* overlays and screens.
|
|
12
|
-
*
|
|
18
|
+
* overlays and screens. Pass `allowKeys` to let specific keys through
|
|
19
|
+
* (e.g. `allowKeys: ['ctrl+d']` to toggle the tool itself).
|
|
13
20
|
*
|
|
14
|
-
* @param top
|
|
15
|
-
* @param left
|
|
21
|
+
* @param top - Initial vertical position in rows (0 = top of terminal).
|
|
22
|
+
* @param left - Horizontal position in columns.
|
|
23
|
+
* @param zindex - Optional modal z-index for stacking order.
|
|
24
|
+
* @param allowKeys - Keys allowed to pass through the modal to layers below.
|
|
25
|
+
* @param persistent - Whether the dev tool survives screen navigation. Defaults to `true`.
|
|
16
26
|
*
|
|
17
|
-
* @throws If `ScenarioManagementProvider` is not mounted
|
|
18
|
-
* ID `_Dev-Tool_` is already open.
|
|
27
|
+
* @throws If `ScenarioManagementProvider` is not mounted.
|
|
19
28
|
*
|
|
20
29
|
* @example
|
|
21
30
|
* ```ts
|
|
22
|
-
* //
|
|
23
|
-
*
|
|
31
|
+
* // Press Ctrl+D to open the dev tool (no-op if already open):
|
|
32
|
+
* boundKeyboard(['ctrl+d'], () => openDevTool({ top: 0, left: 0 }));
|
|
33
|
+
*
|
|
34
|
+
* // Toggle requires allowKeys so the second press can pass through:
|
|
24
35
|
* boundKeyboard(['ctrl+d'], () => {
|
|
25
|
-
*
|
|
26
|
-
* closeDevTool();
|
|
27
|
-
* devOpenRef.current = false;
|
|
28
|
-
* } else {
|
|
29
|
-
* openDevTool({ top: 0, left: 0 });
|
|
30
|
-
* devOpenRef.current = true;
|
|
31
|
-
* }
|
|
36
|
+
* try { openDevTool({ top: 0, left: 0, allowKeys: ['ctrl+d'] }); } catch {}
|
|
32
37
|
* });
|
|
33
38
|
* ```
|
|
39
|
+
* @2026-07-04 v3.8.0
|
|
34
40
|
*/
|
|
35
|
-
export declare function openDevTool({ top, left, zindex }: DevProps): void;
|
|
41
|
+
export declare function openDevTool({ top, left, zindex, allowKeys, persistent }: DevProps): void;
|
|
36
42
|
/**
|
|
37
43
|
* Close the developer debugging modal.
|
|
38
44
|
*
|
|
39
45
|
* Calls {@link closeModal} for the fixed modal ID `_Dev-Tool_`.
|
|
40
|
-
* Safe to call even if the modal is not currently open —
|
|
41
|
-
*
|
|
42
|
-
* may have been closed by its own Escape binding.
|
|
46
|
+
* Safe to call even if the modal is not currently open — the underlying
|
|
47
|
+
* {@link closeModal} treats non-existent IDs as a no-op.
|
|
43
48
|
*
|
|
44
49
|
* @example
|
|
45
50
|
* ```ts
|
package/dist/dev/entrance.js
CHANGED
|
@@ -4,51 +4,58 @@ import { DevScreen } from "./dev-screen.js";
|
|
|
4
4
|
* Open the developer debugging modal.
|
|
5
5
|
*
|
|
6
6
|
* Calls {@link openModal} with the fixed modal ID `_Dev-Tool_` and
|
|
7
|
-
* the {@link DevScreen} component
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* the {@link DevScreen} component, using `persistent: true` by default
|
|
8
|
+
* so the panel survives screen navigation. Keyboard focus is
|
|
9
|
+
* automatically suspended when navigating away and restored when
|
|
10
|
+
* returning to the originating screen.
|
|
11
|
+
*
|
|
12
|
+
* Safe to call when the modal is already open — the underlying
|
|
13
|
+
* {@link openModal} treats duplicate IDs as a no-op, so a simple key
|
|
14
|
+
* binding without a toggle ref suffices:
|
|
15
|
+
*
|
|
16
|
+
* {@link closeDevTool} is the inverse.
|
|
10
17
|
*
|
|
11
18
|
* When open, DevScreen blocks all keyboard events from reaching
|
|
12
|
-
* overlays and screens.
|
|
13
|
-
*
|
|
19
|
+
* overlays and screens. Pass `allowKeys` to let specific keys through
|
|
20
|
+
* (e.g. `allowKeys: ['ctrl+d']` to toggle the tool itself).
|
|
14
21
|
*
|
|
15
|
-
* @param top
|
|
16
|
-
* @param left
|
|
22
|
+
* @param top - Initial vertical position in rows (0 = top of terminal).
|
|
23
|
+
* @param left - Horizontal position in columns.
|
|
24
|
+
* @param zindex - Optional modal z-index for stacking order.
|
|
25
|
+
* @param allowKeys - Keys allowed to pass through the modal to layers below.
|
|
26
|
+
* @param persistent - Whether the dev tool survives screen navigation. Defaults to `true`.
|
|
17
27
|
*
|
|
18
|
-
* @throws If `ScenarioManagementProvider` is not mounted
|
|
19
|
-
* ID `_Dev-Tool_` is already open.
|
|
28
|
+
* @throws If `ScenarioManagementProvider` is not mounted.
|
|
20
29
|
*
|
|
21
30
|
* @example
|
|
22
31
|
* ```ts
|
|
23
|
-
* //
|
|
24
|
-
*
|
|
32
|
+
* // Press Ctrl+D to open the dev tool (no-op if already open):
|
|
33
|
+
* boundKeyboard(['ctrl+d'], () => openDevTool({ top: 0, left: 0 }));
|
|
34
|
+
*
|
|
35
|
+
* // Toggle requires allowKeys so the second press can pass through:
|
|
25
36
|
* boundKeyboard(['ctrl+d'], () => {
|
|
26
|
-
*
|
|
27
|
-
* closeDevTool();
|
|
28
|
-
* devOpenRef.current = false;
|
|
29
|
-
* } else {
|
|
30
|
-
* openDevTool({ top: 0, left: 0 });
|
|
31
|
-
* devOpenRef.current = true;
|
|
32
|
-
* }
|
|
37
|
+
* try { openDevTool({ top: 0, left: 0, allowKeys: ['ctrl+d'] }); } catch {}
|
|
33
38
|
* });
|
|
34
39
|
* ```
|
|
40
|
+
* @2026-07-04 v3.8.0
|
|
35
41
|
*/
|
|
36
|
-
export function openDevTool({ top, left, zindex }) {
|
|
42
|
+
export function openDevTool({ top, left, zindex, allowKeys, persistent = true }) {
|
|
37
43
|
openModal('_Dev-Tool_', DevScreen, {
|
|
38
44
|
top: top,
|
|
39
|
-
left: left
|
|
45
|
+
left: left,
|
|
46
|
+
allowKeys: allowKeys,
|
|
40
47
|
}, {
|
|
41
48
|
zIndex: zindex,
|
|
42
|
-
renderNow: true
|
|
49
|
+
renderNow: true,
|
|
50
|
+
persistent,
|
|
43
51
|
});
|
|
44
52
|
}
|
|
45
53
|
/**
|
|
46
54
|
* Close the developer debugging modal.
|
|
47
55
|
*
|
|
48
56
|
* Calls {@link closeModal} for the fixed modal ID `_Dev-Tool_`.
|
|
49
|
-
* Safe to call even if the modal is not currently open —
|
|
50
|
-
*
|
|
51
|
-
* may have been closed by its own Escape binding.
|
|
57
|
+
* Safe to call even if the modal is not currently open — the underlying
|
|
58
|
+
* {@link closeModal} treats non-existent IDs as a no-op.
|
|
52
59
|
*
|
|
53
60
|
* @example
|
|
54
61
|
* ```ts
|
|
@@ -56,12 +63,5 @@ export function openDevTool({ top, left, zindex }) {
|
|
|
56
63
|
* ```
|
|
57
64
|
*/
|
|
58
65
|
export function closeDevTool() {
|
|
59
|
-
|
|
60
|
-
closeModal('_Dev-Tool_');
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
// Modal already closed — no-op. This handles the case where
|
|
64
|
-
// the Escape key inside DevScreen closed the modal, leaving
|
|
65
|
-
// an external toggle ref stale.
|
|
66
|
-
}
|
|
66
|
+
closeModal('_Dev-Tool_');
|
|
67
67
|
}
|
|
@@ -51,7 +51,8 @@ const Sep = () => (React.createElement(Box, null,
|
|
|
51
51
|
export default function GlobalKeyDisplayBox({ top: initialTop, left }) {
|
|
52
52
|
const { getGlobalKeys, boundKeyboard } = useKeyboard();
|
|
53
53
|
const { closeModal } = useScreenSystem();
|
|
54
|
-
const
|
|
54
|
+
const modalCtx = useContext(ModalContext);
|
|
55
|
+
const modalId = modalCtx?.id ?? null;
|
|
55
56
|
const { rows } = useWindowSize();
|
|
56
57
|
const [offsetTop, setOffsetTop] = useState(initialTop);
|
|
57
58
|
const [expandedEntry, setExpandedEntry] = useState(null);
|
|
@@ -56,7 +56,8 @@ const Sep = () => (React.createElement(Box, null,
|
|
|
56
56
|
export default function GlobalSequenceDisplayBox({ top: initialTop, left }) {
|
|
57
57
|
const { getGlobalSequences, getGlobalPendingSequence, boundKeyboard } = useKeyboard();
|
|
58
58
|
const { closeModal } = useScreenSystem();
|
|
59
|
-
const
|
|
59
|
+
const modalCtx = useContext(ModalContext);
|
|
60
|
+
const modalId = modalCtx?.id ?? null;
|
|
60
61
|
const { rows } = useWindowSize();
|
|
61
62
|
const [offsetTop, setOffsetTop] = useState(initialTop);
|
|
62
63
|
const [expandedEntry, setExpandedEntry] = useState(null);
|
|
@@ -116,8 +116,8 @@ function FocusTargetDetail({ focusId, target }) {
|
|
|
116
116
|
React.createElement(Text, { color: "white" }, focusId)),
|
|
117
117
|
React.createElement(Row, { label: "Bindings" },
|
|
118
118
|
React.createElement(Text, { color: "white" }, target.bindings.length)),
|
|
119
|
-
React.createElement(Row, { label: "
|
|
120
|
-
React.createElement(Text, { color: "white" }, target.
|
|
119
|
+
React.createElement(Row, { label: "Penetr." },
|
|
120
|
+
React.createElement(Text, { color: "white" }, target.penetrationKeys.length)),
|
|
121
121
|
React.createElement(Row, { label: "Stopped" },
|
|
122
122
|
React.createElement(Text, { color: "white" }, target.stoppedKeys.length))),
|
|
123
123
|
target.bindings.length > 0 && (React.createElement(Box, { paddingTop: 1, flexDirection: "column" },
|
|
@@ -126,9 +126,9 @@ function FocusTargetDetail({ focusId, target }) {
|
|
|
126
126
|
" [",
|
|
127
127
|
b.keys.join(', '),
|
|
128
128
|
"]"))))),
|
|
129
|
-
target.
|
|
130
|
-
React.createElement(Text, { color: "gray", dimColor: true }, "
|
|
131
|
-
target.
|
|
129
|
+
target.penetrationKeys.length > 0 && (React.createElement(Box, { paddingTop: 1, flexDirection: "column" },
|
|
130
|
+
React.createElement(Text, { color: "gray", dimColor: true }, "Penetration:"),
|
|
131
|
+
target.penetrationKeys.map((r, i) => (React.createElement(Text, { key: i, color: "gray" },
|
|
132
132
|
" ",
|
|
133
133
|
fmtKeys(r.key)))))),
|
|
134
134
|
target.stoppedKeys.length > 0 && (React.createElement(Box, { paddingTop: 1, flexDirection: "column" },
|
|
@@ -140,7 +140,8 @@ function FocusTargetDetail({ focusId, target }) {
|
|
|
140
140
|
export default function LayerKeyDisplayBox({ top: initialTop, left, screenComponent }) {
|
|
141
141
|
const { readLayer, boundKeyboard } = useKeyboard();
|
|
142
142
|
const { closeModal } = useScreenSystem();
|
|
143
|
-
const
|
|
143
|
+
const modalCtx = useContext(ModalContext);
|
|
144
|
+
const modalId = modalCtx?.id ?? null;
|
|
144
145
|
const { rows } = useWindowSize();
|
|
145
146
|
const [offsetTop, setOffsetTop] = useState(initialTop);
|
|
146
147
|
const [expandedEntry, setExpandedEntry] = useState(null);
|
|
@@ -212,18 +213,18 @@ export default function LayerKeyDisplayBox({ top: initialTop, left, screenCompon
|
|
|
212
213
|
Key: `stop-${i}`,
|
|
213
214
|
});
|
|
214
215
|
});
|
|
215
|
-
//
|
|
216
|
-
layer.
|
|
216
|
+
// Penetration keys
|
|
217
|
+
layer.penetrationKeys.forEach((rule, i) => {
|
|
217
218
|
items.push({
|
|
218
|
-
label: `[
|
|
219
|
-
value: { kind: '
|
|
219
|
+
label: `[Pen] ${keyRuleLabel(rule)}`,
|
|
220
|
+
value: { kind: 'penetrated', idx: i, rule },
|
|
220
221
|
Key: `blk-${i}`,
|
|
221
222
|
});
|
|
222
223
|
});
|
|
223
224
|
// Focus targets
|
|
224
225
|
for (const [focusId, target] of layer.focusTargets) {
|
|
225
226
|
items.push({
|
|
226
|
-
label: `[Foc] ${focusId} (bind:${target.bindings.length}
|
|
227
|
+
label: `[Foc] ${focusId} (bind:${target.bindings.length} pen:${target.penetrationKeys.length} stp:${target.stoppedKeys.length})`,
|
|
227
228
|
value: { kind: 'focusTarget', focusId, target },
|
|
228
229
|
Key: `focus-${focusId}`,
|
|
229
230
|
});
|
|
@@ -248,8 +249,8 @@ export default function LayerKeyDisplayBox({ top: initialTop, left, screenCompon
|
|
|
248
249
|
return React.createElement(SequenceDetail, { firstKey: expandedEntry.firstKey, entry: expandedEntry.entry });
|
|
249
250
|
case 'stopped':
|
|
250
251
|
return React.createElement(KeyRuleDetail, { label: "Stopped Key", rule: expandedEntry.rule });
|
|
251
|
-
case '
|
|
252
|
-
return React.createElement(KeyRuleDetail, { label: "
|
|
252
|
+
case 'penetrated':
|
|
253
|
+
return React.createElement(KeyRuleDetail, { label: "Penetration Key (pass-through)", rule: expandedEntry.rule });
|
|
253
254
|
case 'focusTarget':
|
|
254
255
|
return React.createElement(FocusTargetDetail, { focusId: expandedEntry.focusId, target: expandedEntry.target });
|
|
255
256
|
}
|
|
@@ -265,7 +266,7 @@ export default function LayerKeyDisplayBox({ top: initialTop, left, screenCompon
|
|
|
265
266
|
layer.currentFocusId ?? 'none')),
|
|
266
267
|
React.createElement(Sep, null),
|
|
267
268
|
expandedEntry ? (renderDetail()) : items.length === 0 ? (React.createElement(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center" },
|
|
268
|
-
React.createElement(Text, { color: "gray" }, "No bindings, sequences, stopped/
|
|
269
|
+
React.createElement(Text, { color: "gray" }, "No bindings, sequences, stopped/penetration keys, or focus targets."))) : (React.createElement(SelectInput, { items: items, onSelect: handleSelect, focusId: "layerKey-list", limit: 9 })),
|
|
269
270
|
React.createElement(Sep, null),
|
|
270
271
|
React.createElement(Box, { paddingTop: 1 },
|
|
271
272
|
React.createElement(Text, { dimColor: true }, expandedEntry
|
package/dist/dev/types.d.ts
CHANGED
|
@@ -7,6 +7,10 @@ export interface DevProps {
|
|
|
7
7
|
/** Horizontal position in columns — 0 is the left edge. */
|
|
8
8
|
left: number;
|
|
9
9
|
zindex?: number;
|
|
10
|
+
/** Keys allowed to pass through the dev tool modal to layers below. */
|
|
11
|
+
allowKeys?: string[];
|
|
12
|
+
/** Whether the dev tool survives screen navigation. Defaults to `true`. */
|
|
13
|
+
persistent?: boolean;
|
|
10
14
|
}
|
|
11
15
|
/**
|
|
12
16
|
* Props accepted by the {@link GlobalKeyDisplayBox} global-keys inspector modal.
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { SkipOptions, SkipFn, BackFn, GotoScreenFn, OpenOverlayFn, CloseOve
|
|
|
3
3
|
export { KeyboardProvider, useKeyboard } from "./keyboard/index.js";
|
|
4
4
|
export { normalizeKeyNames, isNormalCharacter } from "./keyboard/index.js";
|
|
5
5
|
export type { KeyHandler, BoundKeyboardOptions, BoundKeyEntry, ScreenKeyboardLayer, KeyboardProviderProps, GlobalKeyEntry, GlobalSequenceEntry, } from "./keyboard/index.js";
|
|
6
|
-
export type {
|
|
6
|
+
export type { PenetrationOptions, AllowModalOptions, StopOptions, LayerKind, FocusTarget, SequenceOptions, ShortcutOperationEntry, SequenceOperationEntry, ModalMissEvent, ModalMissCallback, ModalMissOptions, ResolvedGlobalKeyEntry, } from "./keyboard/index.js";
|
|
7
7
|
export { useFocusState } from "./keyboard/index.js";
|
|
8
8
|
export { SelectInput } from "./components/select/SelectInput.js";
|
|
9
9
|
export type { Item } from "./components/select/types.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { KeyHandler, BoundKeyboardOptions,
|
|
1
|
+
import type { KeyHandler, BoundKeyboardOptions, PenetrationOptions, StopOptions, AllowModalOptions, GlobalKeyEntry, GlobalSequenceEntry, ShortcutOperationEntry, SequenceOperationEntry, SequenceOptions, ModalMissCallback, ModalMissOptions, ResolvedGlobalKeyEntry, ResolvedGlobalSequenceEntry, GlobalPendingSequence, ScreenKeyboardLayer } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Type for the owner stack used to track overlay context.
|
|
4
4
|
* Can be a component type (for screens) or a string (for overlay IDs).
|
|
@@ -18,17 +18,21 @@ export interface KeyboardContextValue {
|
|
|
18
18
|
* Overloads:
|
|
19
19
|
* 1. `(keys: string | string[], handler: KeyHandler, options?: BoundKeyboardOptions)` —
|
|
20
20
|
* explicit keys and handler. A single string is normalized to `[string]`.
|
|
21
|
-
* 2. `(actionId: string, options
|
|
21
|
+
* 2. `(keys: string | string[], actionId: string, options?: BoundKeyboardOptions)` —
|
|
22
|
+
* explicit keys referencing a registered shortcut action by id.
|
|
23
|
+
* 3. `(actionId: string, options?: BoundKeyboardOptions)` —
|
|
22
24
|
* uses a registered shortcut action's predefined keys and callback.
|
|
23
25
|
*
|
|
24
26
|
* @param keys Key name(s) to bind (e.g. `"s"`, `["s", "ctrl+q", "return"]`).
|
|
25
|
-
* @param handler Callback receiving the raw `input` and `key` from Ink
|
|
27
|
+
* @param handler Callback receiving the raw `input` and `key` from Ink,
|
|
28
|
+
* or a registered shortcut action id string.
|
|
26
29
|
* @param options Optional binding behavior (`onlyThis`, `focusId`, `once`, `times`, `when`).
|
|
27
30
|
* @returns An unbind function that removes this binding when called.
|
|
28
31
|
*/
|
|
29
32
|
boundKeyboard: {
|
|
30
33
|
(keys: string | string[], handler: KeyHandler, options?: BoundKeyboardOptions): () => void;
|
|
31
|
-
(actionId: string, options
|
|
34
|
+
(keys: string | string[], actionId: string, options?: BoundKeyboardOptions): () => void;
|
|
35
|
+
(actionId: string, options?: BoundKeyboardOptions): () => void;
|
|
32
36
|
};
|
|
33
37
|
/**
|
|
34
38
|
* Mark one or more keys as "transparent" on the current layer.
|
|
@@ -41,7 +45,7 @@ export interface KeyboardContextValue {
|
|
|
41
45
|
* @param options If `focusId` is provided, marks transparent only
|
|
42
46
|
* within that focus target.
|
|
43
47
|
*/
|
|
44
|
-
|
|
48
|
+
penetration: (keys: string[], options?: PenetrationOptions) => () => void;
|
|
45
49
|
/**
|
|
46
50
|
* Prevent one or more keys from propagating to layers below.
|
|
47
51
|
*
|
package/dist/keyboard/hook.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { ModalMissCallback, ModalMissOptions } from "./types.js";
|
|
|
3
3
|
/**
|
|
4
4
|
* Access the keyboard API from within a React component.
|
|
5
5
|
*
|
|
6
|
-
* Returns `{ boundKeyboard,
|
|
6
|
+
* Returns `{ boundKeyboard, penetration, stop, globalKeys, ... }`.
|
|
7
7
|
*
|
|
8
8
|
* When called inside an overlay component (wrapped in OverlayContext.Provider),
|
|
9
9
|
* keyboard bindings are automatically isolated to the overlay's own layer,
|