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 +2 -2
- package/dist/dev/dev-screen.d.ts +1 -1
- package/dist/dev/dev-screen.js +6 -6
- package/dist/dev/layerKey-display.js +13 -13
- package/dist/index.d.ts +1 -1
- package/dist/keyboard/context.d.ts +2 -2
- package/dist/keyboard/hook.d.ts +1 -1
- package/dist/keyboard/hook.js +1 -1
- 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/provider.js +3 -1
- package/package.json +1 -1
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
|
-
- [
|
|
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 `
|
|
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
|
|
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).
|
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).
|
|
@@ -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" },
|
|
@@ -213,18 +213,18 @@ export default function LayerKeyDisplayBox({ top: initialTop, left, screenCompon
|
|
|
213
213
|
Key: `stop-${i}`,
|
|
214
214
|
});
|
|
215
215
|
});
|
|
216
|
-
//
|
|
217
|
-
layer.
|
|
216
|
+
// Penetration keys
|
|
217
|
+
layer.penetrationKeys.forEach((rule, i) => {
|
|
218
218
|
items.push({
|
|
219
|
-
label: `[
|
|
220
|
-
value: { kind: '
|
|
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}
|
|
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 '
|
|
253
|
-
return React.createElement(KeyRuleDetail, { label: "
|
|
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/
|
|
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 {
|
|
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).
|
|
@@ -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
|
-
|
|
48
|
+
penetration: (keys: string[], options?: PenetrationOptions) => () => void;
|
|
49
49
|
/**
|
|
50
50
|
* Prevent one or more keys from propagating to layers below.
|
|
51
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,
|
package/dist/keyboard/hook.js
CHANGED
|
@@ -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,
|
|
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,
|
package/dist/keyboard/index.d.ts
CHANGED
|
@@ -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,
|
|
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
|
|
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[],
|
|
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,
|
|
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
|
|
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,
|
|
44
|
-
if (
|
|
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) =>
|
|
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,
|
|
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
|
|
109
|
-
const
|
|
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 &&
|
|
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
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
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 &&
|
|
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 (
|
|
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 &&
|
|
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 &&
|
|
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
|
|
243
|
+
// No pending sequence — try to start a new one from the first available key.
|
|
244
244
|
if (layer.pendingSequence === null) {
|
|
245
|
-
// Check each
|
|
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
|
|
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
|
|
315
|
-
const
|
|
316
|
-
if (tryMatchBindings(ft.bindings,
|
|
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,
|
|
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
|
|
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
|
|
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 (`
|
|
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 (`
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
|
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, '
|
|
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
|
-
|
|
973
|
+
penetration,
|
|
974
974
|
stop,
|
|
975
975
|
globalKeys,
|
|
976
976
|
getGlobalKeys,
|
package/dist/keyboard/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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.
|
|
368
|
+
* Options for {@link KeyboardContextValue.penetration} when marking keys
|
|
369
369
|
* as transparent within a specific focus target.
|
|
370
370
|
*/
|
|
371
|
-
export interface
|
|
372
|
-
/** If provided,
|
|
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
|
-
*
|
|
377
|
-
* is ignored and the key is not
|
|
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
|
}
|
package/dist/screen/provider.js
CHANGED
|
@@ -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 };
|