ink-cartridge 3.8.1 → 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
@@ -56,7 +56,7 @@ npx ink-cartridge init my-tui
56
56
  - [useKeyboard](docs/keyboard/useKeyboard-API.md)
57
57
  - [boundKeyboard](docs/keyboard/boundKeyboard-API.md)
58
58
  - [boundSequence](docs/keyboard/boundSequence-API.md)
59
- - [blockedKey](docs/keyboard/blockedKey-API.md)
59
+ - [penetration](docs/keyboard/penetration-API.md)
60
60
  - [stop](docs/keyboard/stop-API.md)
61
61
  - [globalKeys](docs/keyboard/globalKeys-API.md)
62
62
  - [globalSequence](docs/keyboard/globalSequence-API.md)
@@ -166,7 +166,7 @@ npx ink-cartridge init my-tui
166
166
 
167
167
  ## Other
168
168
 
169
- 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.
170
170
 
171
171
  ## License
172
172
 
@@ -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).
@@ -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).
@@ -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" },
@@ -213,18 +213,18 @@ export default function LayerKeyDisplayBox({ top: initialTop, left, screenCompon
213
213
  Key: `stop-${i}`,
214
214
  });
215
215
  });
216
- // Blocked keys
217
- layer.blockedKeys.forEach((rule, i) => {
216
+ // Penetration keys
217
+ layer.penetrationKeys.forEach((rule, i) => {
218
218
  items.push({
219
- label: `[Blk] ${keyRuleLabel(rule)}`,
220
- value: { kind: 'blocked', idx: i, rule },
219
+ label: `[Pen] ${keyRuleLabel(rule)}`,
220
+ value: { kind: 'penetrated', idx: i, rule },
221
221
  Key: `blk-${i}`,
222
222
  });
223
223
  });
224
224
  // Focus targets
225
225
  for (const [focusId, target] of layer.focusTargets) {
226
226
  items.push({
227
- 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})`,
228
228
  value: { kind: 'focusTarget', focusId, target },
229
229
  Key: `focus-${focusId}`,
230
230
  });
@@ -249,8 +249,8 @@ export default function LayerKeyDisplayBox({ top: initialTop, left, screenCompon
249
249
  return React.createElement(SequenceDetail, { firstKey: expandedEntry.firstKey, entry: expandedEntry.entry });
250
250
  case 'stopped':
251
251
  return React.createElement(KeyRuleDetail, { label: "Stopped Key", rule: expandedEntry.rule });
252
- case 'blocked':
253
- 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 });
254
254
  case 'focusTarget':
255
255
  return React.createElement(FocusTargetDetail, { focusId: expandedEntry.focusId, target: expandedEntry.target });
256
256
  }
@@ -266,7 +266,7 @@ export default function LayerKeyDisplayBox({ top: initialTop, left, screenCompon
266
266
  layer.currentFocusId ?? 'none')),
267
267
  React.createElement(Sep, null),
268
268
  expandedEntry ? (renderDetail()) : items.length === 0 ? (React.createElement(Box, { flexGrow: 1, justifyContent: "center", alignItems: "center" },
269
- 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 })),
270
270
  React.createElement(Sep, null),
271
271
  React.createElement(Box, { paddingTop: 1 },
272
272
  React.createElement(Text, { dimColor: true }, expandedEntry
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).
@@ -45,7 +45,7 @@ export interface KeyboardContextValue {
45
45
  * @param options If `focusId` is provided, marks transparent only
46
46
  * within that focus target.
47
47
  */
48
- blockedKey: (keys: string[], options?: BlockedKeyOptions) => () => void;
48
+ penetration: (keys: string[], options?: PenetrationOptions) => () => void;
49
49
  /**
50
50
  * Prevent one or more keys from propagating to layers below.
51
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,
@@ -6,7 +6,7 @@ import { useScreenSystem } from "../screen/hook.js";
6
6
  /**
7
7
  * Access the keyboard API from within a React component.
8
8
  *
9
- * Returns `{ boundKeyboard, blockedKey, stop, globalKeys, ... }`.
9
+ * Returns `{ boundKeyboard, penetration, stop, globalKeys, ... }`.
10
10
  *
11
11
  * When called inside an overlay component (wrapped in OverlayContext.Provider),
12
12
  * keyboard bindings are automatically isolated to the overlay's own layer,
@@ -2,4 +2,4 @@ export { KeyboardProvider } from "./provider.js";
2
2
  export type { KeyboardProviderProps } from "./provider.js";
3
3
  export { useKeyboard, useFocusState, useModalMissListener } from "./hook.js";
4
4
  export { normalizeKeyNames, isNormalCharacter } from "./keyNormalizer.js";
5
- export type { KeyHandler, BoundKeyboardOptions, BlockedKeyOptions, AllowModalOptions, StopOptions, BoundKeyEntry, ScreenKeyboardLayer, LayerKind, FocusTarget, GlobalKeyEntry, GlobalSequenceEntry, ShortcutOperationEntry, SequenceOperationEntry, SequenceOptions, SequenceBinding, PendingSequence, ModalMissEvent, ModalMissCallback, ModalMissOptions, ResolvedGlobalKeyEntry, } from "./types.js";
5
+ export type { KeyHandler, BoundKeyboardOptions, PenetrationOptions, AllowModalOptions, StopOptions, BoundKeyEntry, ScreenKeyboardLayer, LayerKind, FocusTarget, GlobalKeyEntry, GlobalSequenceEntry, ShortcutOperationEntry, SequenceOperationEntry, SequenceOptions, SequenceBinding, PendingSequence, ModalMissEvent, ModalMissCallback, ModalMissOptions, ResolvedGlobalKeyEntry, } from "./types.js";
@@ -25,14 +25,14 @@ export declare function keyMatchesRule(keyName: string, rules: KeyRule[]): boole
25
25
  * all must pass for the binding to fire.
26
26
  *
27
27
  * @param bindings Ordered list of key bindings to try.
28
- * @param unblockedKeys Normalized key names not blocked at this layer.
28
+ * @param availableKeys Normalized key names available for matching at this layer.
29
29
  * @param input Raw character from Ink's useInput.
30
30
  * @param key Full Key descriptor from Ink.
31
31
  * @param skipBinding Optional predicate to skip individual bindings
32
32
  * (used for `onlyThis` enforcement).
33
33
  * @returns `true` if a binding matched and consumed the event.
34
34
  */
35
- export declare function tryMatchBindings(bindings: BoundKeyEntry[], unblockedKeys: string[], input: string, key: Key, skipBinding?: (binding: BoundKeyEntry) => boolean): boolean;
35
+ export declare function tryMatchBindings(bindings: BoundKeyEntry[], availableKeys: string[], input: string, key: Key, skipBinding?: (binding: BoundKeyEntry) => boolean): boolean;
36
36
  /**
37
37
  * Built-in Tab / Shift+Tab focus rotation for a given layer.
38
38
  *
@@ -46,7 +46,7 @@ export declare function handleTabNavigation(layer: ScreenKeyboardLayer, eventNam
46
46
  /**
47
47
  * Handle a keyboard event against a single layer.
48
48
  *
49
- * Evaluates tab navigation, blocked keys, wildcard priority, sequence
49
+ * Evaluates tab navigation, penetration keys, wildcard priority, sequence
50
50
  * matching, focus-target bindings, layer-level bindings, and stopped
51
51
  * keys — in that order.
52
52
  *
@@ -33,22 +33,22 @@ export function keyMatchesRule(keyName, rules) {
33
33
  * all must pass for the binding to fire.
34
34
  *
35
35
  * @param bindings Ordered list of key bindings to try.
36
- * @param unblockedKeys Normalized key names not blocked at this layer.
36
+ * @param availableKeys Normalized key names available for matching at this layer.
37
37
  * @param input Raw character from Ink's useInput.
38
38
  * @param key Full Key descriptor from Ink.
39
39
  * @param skipBinding Optional predicate to skip individual bindings
40
40
  * (used for `onlyThis` enforcement).
41
41
  * @returns `true` if a binding matched and consumed the event.
42
42
  */
43
- export function tryMatchBindings(bindings, unblockedKeys, input, key, skipBinding) {
44
- if (unblockedKeys.length === 0)
43
+ export function tryMatchBindings(bindings, availableKeys, input, key, skipBinding) {
44
+ if (availableKeys.length === 0)
45
45
  return false;
46
46
  for (const binding of bindings) {
47
47
  if (skipBinding && skipBinding(binding))
48
48
  continue;
49
49
  if (binding.when?.() === false)
50
50
  continue;
51
- if (binding.keys.some((k) => unblockedKeys.includes(k))) {
51
+ if (binding.keys.some((k) => availableKeys.includes(k))) {
52
52
  binding.handler(input, key);
53
53
  return true;
54
54
  }
@@ -91,7 +91,7 @@ export function handleTabNavigation(layer, eventNames, shift, notifyFocusChange)
91
91
  /**
92
92
  * Handle a keyboard event against a single layer.
93
93
  *
94
- * Evaluates tab navigation, blocked keys, wildcard priority, sequence
94
+ * Evaluates tab navigation, penetration keys, wildcard priority, sequence
95
95
  * matching, focus-target bindings, layer-level bindings, and stopped
96
96
  * keys — in that order.
97
97
  *
@@ -105,8 +105,8 @@ export function handleLayer(layer, eventNames, input, key, isTop, notifyFocusCha
105
105
  // they can also be bound to business-specific keys.
106
106
  if (isTop && handleTabNavigation(layer, eventNames, key.shift, notifyFocusChange))
107
107
  return true;
108
- const blocked = layer.blockedKeys;
109
- const unblocked = eventNames.filter((n) => !keyMatchesRule(n, blocked));
108
+ const penetrated = layer.penetrationKeys;
109
+ const available = eventNames.filter((n) => !keyMatchesRule(n, penetrated));
110
110
  // onlyThis semantics differ between screens and overlays:
111
111
  // - Screen: skip when any overlay is active (activeOverlayCount > 0)
112
112
  // - Overlay: skip only when multiple overlays compete (activeOverlayCount > 1)
@@ -120,14 +120,14 @@ export function handleLayer(layer, eventNames, input, key, isTop, notifyFocusCha
120
120
  // Wildcard priority pre-check: when enabled, wildcard `*` bindings
121
121
  // are evaluated before sequences, exact matches, and everything else.
122
122
  // Only normal characters are affected — special keys fall through.
123
- if (isTop && wildcardFirst && unblocked.length > 0) {
123
+ if (isTop && wildcardFirst && available.length > 0) {
124
124
  // Check focus-target wildcard first
125
125
  if (layer.currentFocusId) {
126
126
  const ft = layer.focusTargets.get(layer.currentFocusId);
127
127
  if (ft) {
128
- const fBlocked = ft.blockedKeys;
129
- const fUnblocked = unblocked.filter(n => !keyMatchesRule(n, fBlocked));
130
- if (fUnblocked.length > 0) {
128
+ const fPenetrated = ft.penetrationKeys;
129
+ const fAvailable = available.filter(n => !keyMatchesRule(n, fPenetrated));
130
+ if (fAvailable.length > 0) {
131
131
  const wb = ft.bindings.find(b => b.keys.includes('*'));
132
132
  if (wb && isNormalCharacter(input, key)) {
133
133
  if (wb.when?.() === false) { /* skip */ }
@@ -151,7 +151,7 @@ export function handleLayer(layer, eventNames, input, key, isTop, notifyFocusCha
151
151
  }
152
152
  // Sequence matching: only for the top layer (isTop).
153
153
  // Sequences have priority over ordinary boundKeyboard bindings.
154
- if (isTop && unblocked.length > 0) {
154
+ if (isTop && available.length > 0) {
155
155
  const pending = layer.pendingSequence;
156
156
  // We already have a pending sequence in progress.
157
157
  if (pending !== null) {
@@ -164,14 +164,14 @@ export function handleLayer(layer, eventNames, input, key, isTop, notifyFocusCha
164
164
  }
165
165
  else {
166
166
  const expectedKey = pending.sequences[pending.nextIndex];
167
- if (unblocked.includes(expectedKey)) {
167
+ if (available.includes(expectedKey)) {
168
168
  // Matched the next key in the sequence.
169
169
  clearTimeout(pending.timer);
170
170
  pending.nextIndex++;
171
171
  // Narrow candidates to only those whose next key also matches.
172
172
  if (pending.candidates && pending.candidates.length > 1) {
173
173
  const nextIdx = pending.nextIndex - 1;
174
- const narrowed = pending.candidates.filter(c => c.keys.length > nextIdx && unblocked.includes(c.keys[nextIdx]));
174
+ const narrowed = pending.candidates.filter(c => c.keys.length > nextIdx && available.includes(c.keys[nextIdx]));
175
175
  pending.candidates = narrowed.length <= 1 ? undefined : narrowed;
176
176
  }
177
177
  if (pending.nextIndex === pending.sequences.length) {
@@ -198,7 +198,7 @@ export function handleLayer(layer, eventNames, input, key, isTop, notifyFocusCha
198
198
  // Non-exclusive with multiple candidates: try the current key
199
199
  // against every candidate's next expected key to disambiguate.
200
200
  const nextIdx = pending.nextIndex;
201
- const stillPossible = pending.candidates.filter(c => c.keys.length > nextIdx && unblocked.includes(c.keys[nextIdx]));
201
+ const stillPossible = pending.candidates.filter(c => c.keys.length > nextIdx && available.includes(c.keys[nextIdx]));
202
202
  if (stillPossible.length === 0) {
203
203
  // No candidate matches — cancel all and fall through.
204
204
  clearTimeout(pending.timer);
@@ -240,11 +240,11 @@ export function handleLayer(layer, eventNames, input, key, isTop, notifyFocusCha
240
240
  }
241
241
  }
242
242
  }
243
- // No pending sequence — try to start a new one from the first unblocked key.
243
+ // No pending sequence — try to start a new one from the first available key.
244
244
  if (layer.pendingSequence === null) {
245
- // Check each unblocked key name (not just the first) to handle
245
+ // Check each available key name (not just the first) to handle
246
246
  // modifier combinations like 'ctrl+w' which appear after 'w'.
247
- for (const keyName of unblocked) {
247
+ for (const keyName of available) {
248
248
  // When ctrl/meta modifier is held (but not shift), a bare key name
249
249
  // (without '+') does not represent the keystroke the user intended.
250
250
  // normalizeKeyNames expands ctrl+d into ['d', 'ctrl+d']; matching 'd'
@@ -311,16 +311,16 @@ export function handleLayer(layer, eventNames, input, key, isTop, notifyFocusCha
311
311
  if (isTop && layer.currentFocusId) {
312
312
  const ft = layer.focusTargets.get(layer.currentFocusId);
313
313
  if (ft) {
314
- const fBlocked = ft.blockedKeys;
315
- const fUnblocked = unblocked.filter((n) => !keyMatchesRule(n, fBlocked));
316
- if (tryMatchBindings(ft.bindings, fUnblocked, input, key, shouldSkipOnlyThis))
314
+ const fPenetrated = ft.penetrationKeys;
315
+ const fAvailable = available.filter((n) => !keyMatchesRule(n, fPenetrated));
316
+ if (tryMatchBindings(ft.bindings, fAvailable, input, key, shouldSkipOnlyThis))
317
317
  return true;
318
318
  if (eventNames.some((n) => keyMatchesRule(n, ft.stoppedKeys))) {
319
319
  return true;
320
320
  }
321
321
  }
322
322
  }
323
- if (tryMatchBindings(layer.bindings, unblocked, input, key, shouldSkipOnlyThis))
323
+ if (tryMatchBindings(layer.bindings, available, input, key, shouldSkipOnlyThis))
324
324
  return true;
325
325
  if (isTop && eventNames.some((n) => keyMatchesRule(n, layer.stoppedKeys))) {
326
326
  return true;
@@ -46,9 +46,9 @@ function invokeMissIfNeeded(layer, handled, key, input, eventNames) {
46
46
  if (!layer.onMiss)
47
47
  return false;
48
48
  const opts = layer.onMissOptions ?? {};
49
- // fix: The stop API and blockedKey API cases are no longer handled.
49
+ // fix: The stop API and penetration API cases are no longer handled.
50
50
  // Instead, it is left to handlerLayer to handle natural
51
- // So the expectation is that, So the Stop API returns miss: false, but the BlockedKeys API returns miss: true
51
+ // So the expectation is that, So the Stop API returns miss: false, but the penetration API returns miss: true
52
52
  // TODO: You need to modify the corresponding test and do it later.
53
53
  // @2026-06-23 3.6.1
54
54
  if (handled) {
@@ -17,7 +17,7 @@ export interface KeyboardProviderProps {
17
17
  /**
18
18
  * Keyboard context provider for layered key handling.
19
19
  *
20
- * Manages per-screen-layer key bindings, transparent keys (`blockedKey`),
20
+ * Manages per-screen-layer key bindings, transparent keys (`penetration`),
21
21
  * key-stop propagation barriers (`stop`), and global keys (`globalKeys`).
22
22
  * Handles the full event priority chain:
23
23
  * 1. Global keys with `affectOverlay: true`
@@ -196,7 +196,7 @@ function finalizeBoundKeyboard(bindingsArray, actionKeysMap, layer, entry, handl
196
196
  /**
197
197
  * Keyboard context provider for layered key handling.
198
198
  *
199
- * Manages per-screen-layer key bindings, transparent keys (`blockedKey`),
199
+ * Manages per-screen-layer key bindings, transparent keys (`penetration`),
200
200
  * key-stop propagation barriers (`stop`), and global keys (`globalKeys`).
201
201
  * Handles the full event priority chain:
202
202
  * 1. Global keys with `affectOverlay: true`
@@ -346,7 +346,7 @@ export function KeyboardProvider({ children }) {
346
346
  layer = {
347
347
  kind,
348
348
  bindings: [],
349
- blockedKeys: [],
349
+ penetrationKeys: [],
350
350
  stoppedKeys: [],
351
351
  allowedKeys: [],
352
352
  globalKeyOverrides: new Set(),
@@ -408,7 +408,7 @@ export function KeyboardProvider({ children }) {
408
408
  if (!target) {
409
409
  target = {
410
410
  bindings: [],
411
- blockedKeys: [],
411
+ penetrationKeys: [],
412
412
  stoppedKeys: [],
413
413
  allowedKeys: [],
414
414
  actionKeysMap: new Map(),
@@ -567,14 +567,14 @@ export function KeyboardProvider({ children }) {
567
567
  const penetration = useCallback((keys, options) => {
568
568
  const owner = getCurrentOwner();
569
569
  if (!owner) {
570
- throw new Error('[Ink-Cartridge] blockedKey() must be called inside a screen component or overlay.');
570
+ throw new Error('[Ink-Cartridge] penetration() must be called inside a screen component or overlay.');
571
571
  }
572
572
  const layer = getLayer(owner);
573
573
  const compiledWhen = options?.when;
574
574
  const container = options?.focusId
575
575
  ? getOrCreateFocusTarget(layer, options.focusId)
576
576
  : layer;
577
- return pushKeyEntries(container, 'blockedKeys', keys, (key) => ({
577
+ return pushKeyEntries(container, 'penetrationKeys', keys, (key) => ({
578
578
  key,
579
579
  when: compiledWhen,
580
580
  }));
@@ -970,7 +970,7 @@ export function KeyboardProvider({ children }) {
970
970
  const readLayer = useCallback((owner) => layersRef.current.get(owner), []);
971
971
  const value = useMemo(() => ({
972
972
  boundKeyboard,
973
- blockedKey: penetration,
973
+ penetration,
974
974
  stop,
975
975
  globalKeys,
976
976
  getGlobalKeys,
@@ -3,7 +3,7 @@ import type { OverlayEntry } from "../screen/types.js";
3
3
  /**
4
4
  * A single key rule with an optional when condition.
5
5
  *
6
- * Used internally for blockedKeys and stoppedKeys to support
6
+ * Used internally for penetrationKeys and stoppedKeys to support
7
7
  * conditional transparency and conditional propagation barriers.
8
8
  */
9
9
  export interface KeyRule {
@@ -132,7 +132,7 @@ export interface FocusTarget {
132
132
  /** Registered key bindings (evaluation order). */
133
133
  bindings: BoundKeyEntry[];
134
134
  /** Key rules marked as transparent on this target (pass-through). */
135
- blockedKeys: KeyRule[];
135
+ penetrationKeys: KeyRule[];
136
136
  /** Key rules stopped on this target (propagation barrier). */
137
137
  stoppedKeys: KeyRule[];
138
138
  /**
@@ -256,7 +256,7 @@ export interface ScreenKeyboardLayer {
256
256
  /** Registered screen-level key bindings (evaluation order). */
257
257
  bindings: BoundKeyEntry[];
258
258
  /** Key rules marked as transparent at the screen level (pass-through). */
259
- blockedKeys: KeyRule[];
259
+ penetrationKeys: KeyRule[];
260
260
  /** Key rules stopped at the screen level (propagation barrier). */
261
261
  stoppedKeys: KeyRule[];
262
262
  /**
@@ -303,7 +303,7 @@ export interface ScreenKeyboardLayer {
303
303
  * Event object passed to the {@link ModalMissCallback}.
304
304
  *
305
305
  * When `miss` is `false`, the key was handled (by a binding, Tab
306
- * navigation, sequence, or — depending on options — stop/blockedKey).
306
+ * navigation, sequence, or — depending on options — stop/penetration).
307
307
  * When `miss` is `true`, the remaining fields describe the key that
308
308
  * was not handled by any mechanism visible to the miss detector.
309
309
  */
@@ -365,16 +365,16 @@ export interface StopOptions {
365
365
  when?: () => boolean;
366
366
  }
367
367
  /**
368
- * Options for {@link KeyboardContextValue.blockedKey} when marking keys
368
+ * Options for {@link KeyboardContextValue.penetration} when marking keys
369
369
  * as transparent within a specific focus target.
370
370
  */
371
- export interface BlockedKeyOptions {
372
- /** If provided, blocks only within the named focus target. */
371
+ export interface PenetrationOptions {
372
+ /** If provided, penetrates only within the named focus target. */
373
373
  focusId?: string;
374
374
  /**
375
375
  * Optional condition callback. When provided, the key is only transparent
376
- * (blocked) when this returns `true`. When `false`, the blocked-key rule
377
- * is ignored and the key is not blocked.
376
+ * when this returns `true`. When `false`, the penetration rule
377
+ * is ignored and the key is not passed through.
378
378
  */
379
379
  when?: () => boolean;
380
380
  }
@@ -251,9 +251,11 @@ function recalcActiveAfterNavigation(persistentOverlays, persistentModals, newTo
251
251
  }
252
252
  }
253
253
  let activeModalId = null;
254
+ let maxZ = -1;
254
255
  for (const m of persistentModals) {
255
- if (m.originComponent === newTopScreen) {
256
+ if (m.originComponent === newTopScreen && m.zIndex > maxZ) {
256
257
  activeModalId = m.id;
258
+ maxZ = m.zIndex;
257
259
  }
258
260
  }
259
261
  return { activeOverlayIds, activeModalId };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ink-cartridge",
3
- "version": "3.8.1",
3
+ "version": "3.8.2",
4
4
  "description": "Ready-to-use Ink components and screen management system for building terminal UIs.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",