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 CHANGED
@@ -1,6 +1,15 @@
1
- # ink-cartridge
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
- > A component kit for rapidly building complex, multi-page, interaction-heavy terminal applications — filling the critical gaps Ink leaves open.
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
  [![CI](https://github.com/BAIGAOa/ink-cartridge/actions/workflows/ci.yml/badge.svg)](https://github.com/BAIGAOa/ink-trc/actions/workflows/ci.yml)
6
15
  [![npm version](https://img.shields.io/npm/v/ink-cartridge.svg)](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
- - [blockedKey](docs/keyboard/blockedKey-API.md)
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 `blockedKey` is poorly named it means *pass-through*, not "block." The internal name is `penetration`. Too late to rename now.
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.FunctionComponentElement<{
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;
@@ -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 blocks all keyboard events from reaching overlays
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;
@@ -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 blocked keys.
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 blocked = layer.blockedKeys.map(r => Array.isArray(r.key) ? r.key.join(',') : r.key).join(' ');
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
- blocked && (React.createElement(Box, { flexDirection: "row" },
132
- React.createElement(Text, { color: "gray" }, "Blocked: "),
133
- React.createElement(Text, { dimColor: true }, blocked)))));
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 blocks all keyboard events from reaching overlays
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 modalId = useContext(ModalContext);
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)));
@@ -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. Repeated calls while the modal is
7
- * already open will throw guard with a ref or state flag to implement
8
- * a toggle (see example).
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. The modal provides its own close mechanism
12
- * via the Escape key.
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 - Initial vertical position in rows (0 = top of terminal).
15
- * @param left - Horizontal position in columns.
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, or the modal
18
- * ID `_Dev-Tool_` is already open.
27
+ * @throws If `ScenarioManagementProvider` is not mounted.
19
28
  *
20
29
  * @example
21
30
  * ```ts
22
- * // Toggle with a ref to avoid duplicate-open errors:
23
- * const devOpenRef = useRef(false);
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
- * if (devOpenRef.current) {
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 — acts as
41
- * a no-op in that case to support toggle patterns where the modal
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
@@ -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. Repeated calls while the modal is
8
- * already open will throw guard with a ref or state flag to implement
9
- * a toggle (see example).
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. The modal provides its own close mechanism
13
- * via the Escape key.
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 - Initial vertical position in rows (0 = top of terminal).
16
- * @param left - Horizontal position in columns.
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, or the modal
19
- * ID `_Dev-Tool_` is already open.
28
+ * @throws If `ScenarioManagementProvider` is not mounted.
20
29
  *
21
30
  * @example
22
31
  * ```ts
23
- * // Toggle with a ref to avoid duplicate-open errors:
24
- * const devOpenRef = useRef(false);
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
- * if (devOpenRef.current) {
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 — acts as
50
- * a no-op in that case to support toggle patterns where the modal
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
- try {
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 modalId = useContext(ModalContext);
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 modalId = useContext(ModalContext);
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: "Blocked" },
120
- React.createElement(Text, { color: "white" }, target.blockedKeys.length)),
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.blockedKeys.length > 0 && (React.createElement(Box, { paddingTop: 1, flexDirection: "column" },
130
- React.createElement(Text, { color: "gray", dimColor: true }, "Blocked:"),
131
- target.blockedKeys.map((r, i) => (React.createElement(Text, { key: i, color: "gray" },
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 modalId = useContext(ModalContext);
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
- // Blocked keys
216
- layer.blockedKeys.forEach((rule, i) => {
216
+ // Penetration keys
217
+ layer.penetrationKeys.forEach((rule, i) => {
217
218
  items.push({
218
- label: `[Blk] ${keyRuleLabel(rule)}`,
219
- value: { kind: 'blocked', idx: i, rule },
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} blk:${target.blockedKeys.length} stp:${target.stoppedKeys.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 'blocked':
252
- return React.createElement(KeyRuleDetail, { label: "Blocked Key (pass-through)", rule: expandedEntry.rule });
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/blocked keys, or focus targets."))) : (React.createElement(SelectInput, { items: items, onSelect: handleSelect, focusId: "layerKey-list", limit: 9 })),
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
@@ -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 { BlockedKeyOptions, AllowModalOptions, StopOptions, LayerKind, FocusTarget, SequenceOptions, ShortcutOperationEntry, SequenceOperationEntry, ModalMissEvent, ModalMissCallback, ModalMissOptions, ResolvedGlobalKeyEntry, } from "./keyboard/index.js";
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, BlockedKeyOptions, StopOptions, AllowModalOptions, GlobalKeyEntry, GlobalSequenceEntry, ShortcutOperationEntry, SequenceOperationEntry, SequenceOptions, ModalMissCallback, ModalMissOptions, ResolvedGlobalKeyEntry, ResolvedGlobalSequenceEntry, GlobalPendingSequence, ScreenKeyboardLayer } from "./types.js";
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: BoundKeyboardOptions)` —
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: BoundKeyboardOptions): () => void;
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
- blockedKey: (keys: string[], options?: BlockedKeyOptions) => () => void;
48
+ penetration: (keys: string[], options?: PenetrationOptions) => () => void;
45
49
  /**
46
50
  * Prevent one or more keys from propagating to layers below.
47
51
  *
@@ -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, blockedKey, stop, globalKeys, ... }`.
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,