ink-cartridge 3.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/binary-storage/BinaryStorage.d.ts +308 -0
- package/dist/binary-storage/BinaryStorage.js +548 -0
- package/dist/binary-storage/StreamingReader.d.ts +53 -0
- package/dist/binary-storage/StreamingReader.js +530 -0
- package/dist/binary-storage/index.d.ts +91 -0
- package/dist/binary-storage/index.js +91 -0
- package/dist/binary-storage/types.d.ts +400 -0
- package/dist/binary-storage/types.js +15 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +214 -0
- package/dist/cli/initTheme.d.ts +14 -0
- package/dist/cli/initTheme.js +124 -0
- package/dist/cli/makeLanguageType.d.ts +46 -0
- package/dist/cli/makeLanguageType.js +264 -0
- package/dist/cli/makeThemeType.d.ts +43 -0
- package/dist/cli/makeThemeType.js +222 -0
- package/dist/components/badge/Badge.d.ts +7 -0
- package/dist/components/badge/Badge.js +9 -0
- package/dist/components/dialog/ConfirmDialog.d.ts +23 -0
- package/dist/components/dialog/ConfirmDialog.js +57 -0
- package/dist/components/dialog/types.d.ts +20 -0
- package/dist/components/dialog/types.js +1 -0
- package/dist/components/divider/Divider.d.ts +8 -0
- package/dist/components/divider/Divider.js +10 -0
- package/dist/components/fold/Fold.d.ts +3 -0
- package/dist/components/fold/Fold.js +44 -0
- package/dist/components/fold/types.d.ts +28 -0
- package/dist/components/fold/types.js +1 -0
- package/dist/components/form/Field.d.ts +30 -0
- package/dist/components/form/Field.js +59 -0
- package/dist/components/form/Form.d.ts +32 -0
- package/dist/components/form/Form.js +163 -0
- package/dist/components/form/context.d.ts +14 -0
- package/dist/components/form/context.js +20 -0
- package/dist/components/form/types.d.ts +115 -0
- package/dist/components/form/types.js +1 -0
- package/dist/components/key-hint/KeyHint.d.ts +10 -0
- package/dist/components/key-hint/KeyHint.js +12 -0
- package/dist/components/multi-select/MultiSelectInput.d.ts +125 -0
- package/dist/components/multi-select/MultiSelectInput.js +287 -0
- package/dist/components/multi-select/types.d.ts +100 -0
- package/dist/components/multi-select/types.js +1 -0
- package/dist/components/number-input/NumberInput.d.ts +11 -0
- package/dist/components/number-input/NumberInput.js +44 -0
- package/dist/components/progress-bar/ProgressBar.d.ts +11 -0
- package/dist/components/progress-bar/ProgressBar.js +9 -0
- package/dist/components/search-input/SearchInput.d.ts +10 -0
- package/dist/components/search-input/SearchInput.js +15 -0
- package/dist/components/select/SelectInput.d.ts +100 -0
- package/dist/components/select/SelectInput.js +147 -0
- package/dist/components/select/types.d.ts +63 -0
- package/dist/components/select/types.js +1 -0
- package/dist/components/spinner/Spinner.d.ts +12 -0
- package/dist/components/spinner/Spinner.js +24 -0
- package/dist/components/tabs/Tabs.d.ts +3 -0
- package/dist/components/tabs/Tabs.js +57 -0
- package/dist/components/tabs/types.d.ts +32 -0
- package/dist/components/tabs/types.js +1 -0
- package/dist/components/text/TextInput.d.ts +39 -0
- package/dist/components/text/TextInput.js +233 -0
- package/dist/components/text/types.d.ts +62 -0
- package/dist/components/text/types.js +1 -0
- package/dist/dev/dev-screen.d.ts +36 -0
- package/dist/dev/dev-screen.js +142 -0
- package/dist/dev/entrance.d.ts +51 -0
- package/dist/dev/entrance.js +67 -0
- package/dist/dev/globalKey-display.d.ts +2 -0
- package/dist/dev/globalKey-display.js +8 -0
- package/dist/dev/types.d.ts +9 -0
- package/dist/dev/types.js +1 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +50 -0
- package/dist/keyboard/check-global-key.d.ts +18 -0
- package/dist/keyboard/check-global-key.js +52 -0
- package/dist/keyboard/context.d.ts +398 -0
- package/dist/keyboard/context.js +9 -0
- package/dist/keyboard/global-key-processor/index.d.ts +16 -0
- package/dist/keyboard/global-key-processor/index.js +75 -0
- package/dist/keyboard/global-sequence-processor/index.d.ts +14 -0
- package/dist/keyboard/global-sequence-processor/index.js +165 -0
- package/dist/keyboard/hook.d.ts +51 -0
- package/dist/keyboard/hook.js +105 -0
- package/dist/keyboard/index.d.ts +5 -0
- package/dist/keyboard/index.js +3 -0
- package/dist/keyboard/keyNormalizer.d.ts +37 -0
- package/dist/keyboard/keyNormalizer.js +119 -0
- package/dist/keyboard/layer-handler.d.ts +57 -0
- package/dist/keyboard/layer-handler.js +274 -0
- package/dist/keyboard/modal-processor/index.d.ts +16 -0
- package/dist/keyboard/modal-processor/index.js +97 -0
- package/dist/keyboard/overlay-processor/index.d.ts +16 -0
- package/dist/keyboard/overlay-processor/index.js +30 -0
- package/dist/keyboard/pipeline/chain.d.ts +10 -0
- package/dist/keyboard/pipeline/chain.js +45 -0
- package/dist/keyboard/pipeline/context.d.ts +34 -0
- package/dist/keyboard/pipeline/context.js +42 -0
- package/dist/keyboard/pipeline/index.d.ts +3 -0
- package/dist/keyboard/pipeline/index.js +2 -0
- package/dist/keyboard/provider.d.ts +32 -0
- package/dist/keyboard/provider.js +985 -0
- package/dist/keyboard/screen-stack-processor/index.d.ts +14 -0
- package/dist/keyboard/screen-stack-processor/index.js +32 -0
- package/dist/keyboard/types.d.ts +632 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/language/context.d.ts +7 -0
- package/dist/language/context.js +7 -0
- package/dist/language/hook.d.ts +19 -0
- package/dist/language/hook.js +26 -0
- package/dist/language/index.d.ts +3 -0
- package/dist/language/index.js +2 -0
- package/dist/language/provider.d.ts +30 -0
- package/dist/language/provider.js +155 -0
- package/dist/language/types.d.ts +124 -0
- package/dist/language/types.js +1 -0
- package/dist/screen/ModalContext.d.ts +11 -0
- package/dist/screen/ModalContext.js +12 -0
- package/dist/screen/OverlayContext.d.ts +9 -0
- package/dist/screen/OverlayContext.js +10 -0
- package/dist/screen/context.d.ts +61 -0
- package/dist/screen/context.js +8 -0
- package/dist/screen/current-screen.d.ts +16 -0
- package/dist/screen/current-screen.js +38 -0
- package/dist/screen/hook.d.ts +13 -0
- package/dist/screen/hook.js +20 -0
- package/dist/screen/index.d.ts +7 -0
- package/dist/screen/index.js +5 -0
- package/dist/screen/provider.d.ts +112 -0
- package/dist/screen/provider.js +577 -0
- package/dist/screen/registry.d.ts +27 -0
- package/dist/screen/registry.js +71 -0
- package/dist/screen/types.d.ts +227 -0
- package/dist/screen/types.js +1 -0
- package/dist/storage/index.d.ts +45 -0
- package/dist/storage/index.js +46 -0
- package/dist/storage/storage.d.ts +139 -0
- package/dist/storage/storage.js +236 -0
- package/dist/storage/types.d.ts +145 -0
- package/dist/storage/types.js +1 -0
- package/dist/theme/context.d.ts +7 -0
- package/dist/theme/context.js +7 -0
- package/dist/theme/hook.d.ts +11 -0
- package/dist/theme/hook.js +18 -0
- package/dist/theme/index.d.ts +4 -0
- package/dist/theme/index.js +2 -0
- package/dist/theme/provider.d.ts +18 -0
- package/dist/theme/provider.js +308 -0
- package/dist/theme/types.d.ts +53 -0
- package/dist/theme/types.js +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import React, { ReactNode } from 'react';
|
|
2
|
+
import { SkipOptions, OpenOverlayOptions, OpenModalOptions } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Clear all registered provider dispatchers.
|
|
5
|
+
* Intended for test cleanup — prevents stale dispatch references
|
|
6
|
+
* from leaking between test runs when providers are not properly
|
|
7
|
+
* unmounted.
|
|
8
|
+
*/
|
|
9
|
+
export declare function clearDispatchers(): void;
|
|
10
|
+
/**
|
|
11
|
+
* Navigate down the tree to a direct child of the current screen.
|
|
12
|
+
*/
|
|
13
|
+
export declare function skip<C extends React.ComponentType<any>>(component: C, params: React.ComponentProps<C>, options?: SkipOptions): void;
|
|
14
|
+
/**
|
|
15
|
+
* Navigate up the tree to the parent of the current screen.
|
|
16
|
+
*/
|
|
17
|
+
export declare function back(levels?: number): void;
|
|
18
|
+
/**
|
|
19
|
+
* Jump to any registered screen across branches of the tree.
|
|
20
|
+
*/
|
|
21
|
+
export declare function gotoScreen<C extends React.ComponentType<any>>(component: C, params: React.ComponentProps<C>): void;
|
|
22
|
+
/**
|
|
23
|
+
* Open a floating overlay on top of the current screen stack.
|
|
24
|
+
*
|
|
25
|
+
* Multiple overlays can be open simultaneously, distinguished by unique IDs.
|
|
26
|
+
*
|
|
27
|
+
* @param id Unique identifier for this overlay.
|
|
28
|
+
* @param component The overlay component (must be registered).
|
|
29
|
+
* @param params Props to pass to the overlay component.
|
|
30
|
+
* @param options Optional activation and zIndex settings.
|
|
31
|
+
*
|
|
32
|
+
* @throws If the provider is not mounted, the component is not registered,
|
|
33
|
+
* or the ID is already in use.
|
|
34
|
+
*/
|
|
35
|
+
export declare function openOverlay<C extends React.ComponentType<any>>(id: string, component: C, params: React.ComponentProps<C>, options?: OpenOverlayOptions): void;
|
|
36
|
+
/**
|
|
37
|
+
* Close a specific overlay by its ID.
|
|
38
|
+
*
|
|
39
|
+
* @param id The ID of the overlay to close.
|
|
40
|
+
*
|
|
41
|
+
* @throws If the provider is not mounted.
|
|
42
|
+
*/
|
|
43
|
+
export declare function closeOverlay(id: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Close all open overlays.
|
|
46
|
+
*
|
|
47
|
+
* @throws If the provider is not mounted.
|
|
48
|
+
*/
|
|
49
|
+
export declare function closeAllOverlays(): void;
|
|
50
|
+
/**
|
|
51
|
+
* Activate an overlay by its ID (so it receives keyboard events).
|
|
52
|
+
*
|
|
53
|
+
* @param id The ID of the overlay to activate.
|
|
54
|
+
*
|
|
55
|
+
* @throws If the provider is not mounted or the overlay ID does not exist.
|
|
56
|
+
*/
|
|
57
|
+
export declare function activateOverlay(id: string): void;
|
|
58
|
+
/**
|
|
59
|
+
* Deactivate an overlay by its ID (so it no longer receives keyboard events).
|
|
60
|
+
*
|
|
61
|
+
* @param id The ID of the overlay to deactivate.
|
|
62
|
+
*
|
|
63
|
+
* @throws If the provider is not mounted.
|
|
64
|
+
*/
|
|
65
|
+
export declare function deactivateOverlay(id: string): void;
|
|
66
|
+
/**
|
|
67
|
+
* Open a modal on top of all overlays.
|
|
68
|
+
*
|
|
69
|
+
* Only one modal is active at a time (the one with the highest zIndex).
|
|
70
|
+
* The active modal receives absolute keyboard priority, consuming all
|
|
71
|
+
* keyboard events before they reach overlays or screens.
|
|
72
|
+
*
|
|
73
|
+
* @param id Unique identifier for this modal.
|
|
74
|
+
* @param component The modal component (must be registered).
|
|
75
|
+
* @param params Props to pass to the modal component.
|
|
76
|
+
* @param options Optional zIndex and renderNow settings.
|
|
77
|
+
*
|
|
78
|
+
* @throws If the provider is not mounted, the component is not registered,
|
|
79
|
+
* or the ID is already in use.
|
|
80
|
+
*/
|
|
81
|
+
export declare function openModal<C extends React.ComponentType<any>>(id: string, component: C, params: React.ComponentProps<C>, options?: OpenModalOptions): void;
|
|
82
|
+
/**
|
|
83
|
+
* Close a specific modal by its ID.
|
|
84
|
+
*
|
|
85
|
+
* When the active modal is closed, the modal with the next highest zIndex
|
|
86
|
+
* automatically becomes active.
|
|
87
|
+
*
|
|
88
|
+
* @param id The ID of the modal to close.
|
|
89
|
+
*
|
|
90
|
+
* @throws If the provider is not mounted.
|
|
91
|
+
*/
|
|
92
|
+
export declare function closeModal(id: string): void;
|
|
93
|
+
/**
|
|
94
|
+
* Close all open modals.
|
|
95
|
+
*
|
|
96
|
+
* @throws If the provider is not mounted.
|
|
97
|
+
*/
|
|
98
|
+
export declare function closeAllModals(): void;
|
|
99
|
+
export interface ScenarioManagementProviderProps {
|
|
100
|
+
children: ReactNode;
|
|
101
|
+
/** 默认屏幕组件(必填,需先 registerComponent) */
|
|
102
|
+
defaultScreen: React.ComponentType<any>;
|
|
103
|
+
/** 默认参数(可选,未传则使用注册时的模板参数) */
|
|
104
|
+
defaultParams?: Record<string, unknown>;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Screen-management context provider.
|
|
108
|
+
*
|
|
109
|
+
* Wraps the application and enables tree-based screen navigation, overlays,
|
|
110
|
+
* and module-level navigation functions.
|
|
111
|
+
*/
|
|
112
|
+
export declare function ScenarioManagementProvider({ children, defaultScreen, defaultParams, }: ScenarioManagementProviderProps): React.JSX.Element;
|
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import React, { useReducer, useMemo, useEffect } from 'react';
|
|
2
|
+
import { ScreenSystemContext } from './context.js';
|
|
3
|
+
import { getTemplate, hasComponent, isChildOf, getParent, } from './registry.js';
|
|
4
|
+
const _dispatchers = new Set();
|
|
5
|
+
/**
|
|
6
|
+
* Clear all registered provider dispatchers.
|
|
7
|
+
* Intended for test cleanup — prevents stale dispatch references
|
|
8
|
+
* from leaking between test runs when providers are not properly
|
|
9
|
+
* unmounted.
|
|
10
|
+
*/
|
|
11
|
+
export function clearDispatchers() {
|
|
12
|
+
_dispatchers.clear();
|
|
13
|
+
}
|
|
14
|
+
function getDispatch() {
|
|
15
|
+
if (_dispatchers.size === 0) {
|
|
16
|
+
throw new Error('[Ink-Cartridge] Navigation function called before Provider is mounted. Please ensure <ScenarioManagementProvider> is mounted in the component tree.');
|
|
17
|
+
}
|
|
18
|
+
return [..._dispatchers][_dispatchers.size - 1];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Sort overlays by zIndex ascending, then by createdAt for tie-breaking.
|
|
22
|
+
*/
|
|
23
|
+
function sortOverlays(overlays) {
|
|
24
|
+
return [...overlays].sort((a, b) => {
|
|
25
|
+
if (a.zIndex !== b.zIndex)
|
|
26
|
+
return a.zIndex - b.zIndex;
|
|
27
|
+
return a.createdAt - b.createdAt;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Sort modals by zIndex ascending, then by createdAt for tie-breaking.
|
|
32
|
+
*
|
|
33
|
+
* The modal with the highest zIndex becomes the active modal.
|
|
34
|
+
* Architecturally symmetric to {@link sortOverlays}.
|
|
35
|
+
*/
|
|
36
|
+
function sortModals(modals) {
|
|
37
|
+
return [...modals].sort((a, b) => {
|
|
38
|
+
if (a.zIndex !== b.zIndex)
|
|
39
|
+
return a.zIndex - b.zIndex;
|
|
40
|
+
return a.createdAt - b.createdAt;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Navigate down the tree to a direct child of the current screen.
|
|
45
|
+
*/
|
|
46
|
+
export function skip(component, params, options) {
|
|
47
|
+
if (!hasComponent(component)) {
|
|
48
|
+
throw new Error(`[Ink-Cartridge] Component "${component.displayName || component.name || 'anonymous'}" is not registered. Please call registerComponent() first.`);
|
|
49
|
+
}
|
|
50
|
+
getDispatch()({
|
|
51
|
+
type: 'skip',
|
|
52
|
+
component,
|
|
53
|
+
params: params,
|
|
54
|
+
onlyAttribute: options?.onlyAttribute ?? false,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Navigate up the tree to the parent of the current screen.
|
|
59
|
+
*/
|
|
60
|
+
export function back(levels = 1) {
|
|
61
|
+
if (levels < 1) {
|
|
62
|
+
throw new Error('[Ink-Cartridge] back() levels must be >= 1.');
|
|
63
|
+
}
|
|
64
|
+
getDispatch()({ type: 'back', levels });
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Jump to any registered screen across branches of the tree.
|
|
68
|
+
*/
|
|
69
|
+
export function gotoScreen(component, params) {
|
|
70
|
+
if (!hasComponent(component)) {
|
|
71
|
+
throw new Error(`[Ink-Cartridge] Component "${component.displayName || component.name || 'anonymous'}" is not registered. Please call registerComponent() first.`);
|
|
72
|
+
}
|
|
73
|
+
getDispatch()({
|
|
74
|
+
type: 'gotoScreen',
|
|
75
|
+
component,
|
|
76
|
+
params: params,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Open a floating overlay on top of the current screen stack.
|
|
81
|
+
*
|
|
82
|
+
* Multiple overlays can be open simultaneously, distinguished by unique IDs.
|
|
83
|
+
*
|
|
84
|
+
* @param id Unique identifier for this overlay.
|
|
85
|
+
* @param component The overlay component (must be registered).
|
|
86
|
+
* @param params Props to pass to the overlay component.
|
|
87
|
+
* @param options Optional activation and zIndex settings.
|
|
88
|
+
*
|
|
89
|
+
* @throws If the provider is not mounted, the component is not registered,
|
|
90
|
+
* or the ID is already in use.
|
|
91
|
+
*/
|
|
92
|
+
export function openOverlay(id, component, params, options) {
|
|
93
|
+
if (!hasComponent(component)) {
|
|
94
|
+
throw new Error(`[Ink-Cartridge] Component "${component.displayName || component.name || 'anonymous'}" is not registered. Please call registerComponent() first.`);
|
|
95
|
+
}
|
|
96
|
+
getDispatch()({
|
|
97
|
+
type: 'openOverlay',
|
|
98
|
+
id,
|
|
99
|
+
component,
|
|
100
|
+
params: params,
|
|
101
|
+
activate: options?.activate ?? true,
|
|
102
|
+
zIndex: options?.zIndex,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Close a specific overlay by its ID.
|
|
107
|
+
*
|
|
108
|
+
* @param id The ID of the overlay to close.
|
|
109
|
+
*
|
|
110
|
+
* @throws If the provider is not mounted.
|
|
111
|
+
*/
|
|
112
|
+
export function closeOverlay(id) {
|
|
113
|
+
getDispatch()({ type: 'closeOverlay', id });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Close all open overlays.
|
|
117
|
+
*
|
|
118
|
+
* @throws If the provider is not mounted.
|
|
119
|
+
*/
|
|
120
|
+
export function closeAllOverlays() {
|
|
121
|
+
getDispatch()({ type: 'closeAllOverlays' });
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Activate an overlay by its ID (so it receives keyboard events).
|
|
125
|
+
*
|
|
126
|
+
* @param id The ID of the overlay to activate.
|
|
127
|
+
*
|
|
128
|
+
* @throws If the provider is not mounted or the overlay ID does not exist.
|
|
129
|
+
*/
|
|
130
|
+
export function activateOverlay(id) {
|
|
131
|
+
getDispatch()({ type: 'activateOverlay', id });
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Deactivate an overlay by its ID (so it no longer receives keyboard events).
|
|
135
|
+
*
|
|
136
|
+
* @param id The ID of the overlay to deactivate.
|
|
137
|
+
*
|
|
138
|
+
* @throws If the provider is not mounted.
|
|
139
|
+
*/
|
|
140
|
+
export function deactivateOverlay(id) {
|
|
141
|
+
getDispatch()({ type: 'deactivateOverlay', id });
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Open a modal on top of all overlays.
|
|
145
|
+
*
|
|
146
|
+
* Only one modal is active at a time (the one with the highest zIndex).
|
|
147
|
+
* The active modal receives absolute keyboard priority, consuming all
|
|
148
|
+
* keyboard events before they reach overlays or screens.
|
|
149
|
+
*
|
|
150
|
+
* @param id Unique identifier for this modal.
|
|
151
|
+
* @param component The modal component (must be registered).
|
|
152
|
+
* @param params Props to pass to the modal component.
|
|
153
|
+
* @param options Optional zIndex and renderNow settings.
|
|
154
|
+
*
|
|
155
|
+
* @throws If the provider is not mounted, the component is not registered,
|
|
156
|
+
* or the ID is already in use.
|
|
157
|
+
*/
|
|
158
|
+
export function openModal(id, component, params, options) {
|
|
159
|
+
if (!hasComponent(component)) {
|
|
160
|
+
throw new Error(`[Ink-Cartridge] Component "${component.displayName || component.name || 'anonymous'}" is not registered. Please call registerComponent() first.`);
|
|
161
|
+
}
|
|
162
|
+
getDispatch()({
|
|
163
|
+
type: 'openModal',
|
|
164
|
+
id,
|
|
165
|
+
component,
|
|
166
|
+
params: params,
|
|
167
|
+
zIndex: options?.zIndex,
|
|
168
|
+
renderNow: options?.renderNow,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Close a specific modal by its ID.
|
|
173
|
+
*
|
|
174
|
+
* When the active modal is closed, the modal with the next highest zIndex
|
|
175
|
+
* automatically becomes active.
|
|
176
|
+
*
|
|
177
|
+
* @param id The ID of the modal to close.
|
|
178
|
+
*
|
|
179
|
+
* @throws If the provider is not mounted.
|
|
180
|
+
*/
|
|
181
|
+
export function closeModal(id) {
|
|
182
|
+
getDispatch()({ type: 'closeModal', id });
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Close all open modals.
|
|
186
|
+
*
|
|
187
|
+
* @throws If the provider is not mounted.
|
|
188
|
+
*/
|
|
189
|
+
export function closeAllModals() {
|
|
190
|
+
getDispatch()({ type: 'closeAllModals' });
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* 从树中查找共同祖先
|
|
194
|
+
* 从 currentPath 栈底向上找到第一个在 targetAncestors 中的节点
|
|
195
|
+
*/
|
|
196
|
+
function findCommonAncestor(currentPath, target) {
|
|
197
|
+
const targetAncestors = new Set();
|
|
198
|
+
let node = target;
|
|
199
|
+
while (node) {
|
|
200
|
+
targetAncestors.add(node);
|
|
201
|
+
node = getParent(node);
|
|
202
|
+
}
|
|
203
|
+
for (let i = currentPath.length - 1; i >= 0; i--) {
|
|
204
|
+
if (targetAncestors.has(currentPath[i])) {
|
|
205
|
+
return currentPath[i];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
throw new Error(`[Ink-Cartridge] Cannot find common ancestor. The target component may not be in the same tree.`);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* 构建从祖先到目标节点的路径(不含祖先本身)
|
|
212
|
+
*/
|
|
213
|
+
function buildPathFrom(ancestor, target) {
|
|
214
|
+
const path = [];
|
|
215
|
+
let node = target;
|
|
216
|
+
while (node && node !== ancestor) {
|
|
217
|
+
path.push(node);
|
|
218
|
+
node = getParent(node);
|
|
219
|
+
}
|
|
220
|
+
if (!node) {
|
|
221
|
+
throw new Error(`[Ink-Cartridge] Target component is not a descendant of the ancestor.`);
|
|
222
|
+
}
|
|
223
|
+
path.reverse();
|
|
224
|
+
return path;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Pure reducer for {@link ScreenState}.
|
|
228
|
+
*
|
|
229
|
+
* Handles all navigation actions: skip (down), back (up), gotoScreen
|
|
230
|
+
* (cross-branch), openOverlay, closeOverlay, closeAllOverlays,
|
|
231
|
+
* activateOverlay, and deactivateOverlay.
|
|
232
|
+
*
|
|
233
|
+
* Navigation actions (skip / back / gotoScreen) clear all open overlays.
|
|
234
|
+
*/
|
|
235
|
+
function screenReducer(state, action) {
|
|
236
|
+
switch (action.type) {
|
|
237
|
+
case 'skip': {
|
|
238
|
+
const current = state.path[state.path.length - 1];
|
|
239
|
+
if (!isChildOf(action.component, current)) {
|
|
240
|
+
throw new Error(`[Ink-Cartridge] "${action.component.displayName || action.component.name || 'anonymous'}" is not a child of "${current.displayName || current.name || 'anonymous'}". Use skip to navigate down the tree, or gotoScreen to jump across branches.`);
|
|
241
|
+
}
|
|
242
|
+
const sameComponent = action.component === current;
|
|
243
|
+
const counter = sameComponent && action.onlyAttribute
|
|
244
|
+
? state.counter
|
|
245
|
+
: state.counter + 1;
|
|
246
|
+
const template = getTemplate(action.component) ?? {};
|
|
247
|
+
const mergedParams = { ...template, ...action.params };
|
|
248
|
+
return {
|
|
249
|
+
path: [...state.path, action.component],
|
|
250
|
+
pathParams: [...state.pathParams, mergedParams],
|
|
251
|
+
overlays: [],
|
|
252
|
+
activeOverlayIds: new Set(),
|
|
253
|
+
modals: [],
|
|
254
|
+
activeModalId: null,
|
|
255
|
+
counter,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
case 'back': {
|
|
259
|
+
const levels = action.levels ?? 1;
|
|
260
|
+
if (state.path.length <= levels) {
|
|
261
|
+
throw new Error(levels === 1
|
|
262
|
+
? '[Ink-Cartridge] back() failed: already at the root node, cannot go back.'
|
|
263
|
+
: `[Ink-Cartridge] back(${levels}) failed: current depth is ${state.path.length}, cannot go back ${levels} levels.`);
|
|
264
|
+
}
|
|
265
|
+
return {
|
|
266
|
+
path: state.path.slice(0, -levels),
|
|
267
|
+
pathParams: state.pathParams.slice(0, -levels),
|
|
268
|
+
overlays: [],
|
|
269
|
+
activeOverlayIds: new Set(),
|
|
270
|
+
modals: [],
|
|
271
|
+
activeModalId: null,
|
|
272
|
+
counter: state.counter + 1,
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
case 'gotoScreen': {
|
|
276
|
+
const commonAncestor = findCommonAncestor(state.path, action.component);
|
|
277
|
+
const ancestorIndex = state.path.indexOf(commonAncestor);
|
|
278
|
+
if (ancestorIndex === -1) {
|
|
279
|
+
throw new Error(`[Ink-Cartridge] gotoScreen failed: cannot locate common ancestor.`);
|
|
280
|
+
}
|
|
281
|
+
const suffix = buildPathFrom(commonAncestor, action.component);
|
|
282
|
+
const newPath = [
|
|
283
|
+
...state.path.slice(0, ancestorIndex + 1),
|
|
284
|
+
...suffix,
|
|
285
|
+
];
|
|
286
|
+
const template = getTemplate(action.component) ?? {};
|
|
287
|
+
const mergedParams = { ...template, ...action.params };
|
|
288
|
+
const newPathParams = [
|
|
289
|
+
...state.pathParams.slice(0, ancestorIndex + 1),
|
|
290
|
+
...suffix.map((comp) => {
|
|
291
|
+
const tpl = getTemplate(comp) ?? {};
|
|
292
|
+
return comp === action.component ? mergedParams : tpl;
|
|
293
|
+
}),
|
|
294
|
+
];
|
|
295
|
+
return {
|
|
296
|
+
path: newPath,
|
|
297
|
+
pathParams: newPathParams,
|
|
298
|
+
overlays: [],
|
|
299
|
+
activeOverlayIds: new Set(),
|
|
300
|
+
modals: [],
|
|
301
|
+
activeModalId: null,
|
|
302
|
+
counter: state.counter + 1,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
case 'openOverlay': {
|
|
306
|
+
if (state.overlays.some(o => o.id === action.id)) {
|
|
307
|
+
throw new Error(`[Ink-Cartridge] Overlay with id "${action.id}" already exists. Use a unique id for each overlay.`);
|
|
308
|
+
}
|
|
309
|
+
if (state.modals.some(m => m.id === action.id)) {
|
|
310
|
+
throw new Error(`[Ink-Cartridge] Cannot open overlay "${action.id}": this ID is already in use by a modal. Use a unique id.`);
|
|
311
|
+
}
|
|
312
|
+
const newEntry = {
|
|
313
|
+
id: action.id,
|
|
314
|
+
component: action.component,
|
|
315
|
+
props: action.params,
|
|
316
|
+
zIndex: action.zIndex ?? state.overlays.length,
|
|
317
|
+
createdAt: Date.now(),
|
|
318
|
+
};
|
|
319
|
+
const newOverlays = sortOverlays([...state.overlays, newEntry]);
|
|
320
|
+
const newActiveIds = new Set(state.activeOverlayIds);
|
|
321
|
+
if (action.activate) {
|
|
322
|
+
newActiveIds.add(action.id);
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
...state,
|
|
326
|
+
overlays: newOverlays,
|
|
327
|
+
activeOverlayIds: newActiveIds,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
case 'closeOverlay': {
|
|
331
|
+
if (!state.overlays.some(o => o.id === action.id)) {
|
|
332
|
+
throw new Error(`[Ink-Cartridge] Cannot close overlay "${action.id}": no overlay with that ID exists.`);
|
|
333
|
+
}
|
|
334
|
+
const newOverlays = state.overlays.filter(o => o.id !== action.id);
|
|
335
|
+
const newActiveIds = new Set(state.activeOverlayIds);
|
|
336
|
+
newActiveIds.delete(action.id);
|
|
337
|
+
return {
|
|
338
|
+
...state,
|
|
339
|
+
overlays: newOverlays,
|
|
340
|
+
activeOverlayIds: newActiveIds,
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
case 'closeAllOverlays': {
|
|
344
|
+
return {
|
|
345
|
+
...state,
|
|
346
|
+
overlays: [],
|
|
347
|
+
activeOverlayIds: new Set(),
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
case 'activateOverlay': {
|
|
351
|
+
if (!state.overlays.some(o => o.id === action.id)) {
|
|
352
|
+
throw new Error(`[Ink-Cartridge] Cannot activate overlay "${action.id}": no overlay with that ID exists.`);
|
|
353
|
+
}
|
|
354
|
+
const newActiveIds = new Set(state.activeOverlayIds);
|
|
355
|
+
newActiveIds.add(action.id);
|
|
356
|
+
return {
|
|
357
|
+
...state,
|
|
358
|
+
activeOverlayIds: newActiveIds,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
case 'deactivateOverlay': {
|
|
362
|
+
if (!state.overlays.some(o => o.id === action.id)) {
|
|
363
|
+
throw new Error(`[Ink-Cartridge] Cannot deactivate overlay "${action.id}": no overlay with that ID exists.`);
|
|
364
|
+
}
|
|
365
|
+
const newActiveIds = new Set(state.activeOverlayIds);
|
|
366
|
+
newActiveIds.delete(action.id);
|
|
367
|
+
return {
|
|
368
|
+
...state,
|
|
369
|
+
activeOverlayIds: newActiveIds,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
case 'openModal': {
|
|
373
|
+
if (state.modals.some(m => m.id === action.id)) {
|
|
374
|
+
throw new Error(`[Ink-Cartridge] Modal with id "${action.id}" already exists. Use a unique id for each modal.`);
|
|
375
|
+
}
|
|
376
|
+
if (state.overlays.some(o => o.id === action.id)) {
|
|
377
|
+
throw new Error(`[Ink-Cartridge] Cannot open modal "${action.id}": this ID is already in use by an overlay. Use a unique id.`);
|
|
378
|
+
}
|
|
379
|
+
const newEntry = {
|
|
380
|
+
id: action.id,
|
|
381
|
+
component: action.component,
|
|
382
|
+
props: action.params,
|
|
383
|
+
zIndex: action.zIndex ?? state.modals.length,
|
|
384
|
+
createdAt: Date.now(),
|
|
385
|
+
renderNow: action.renderNow ?? false,
|
|
386
|
+
};
|
|
387
|
+
const newModals = sortModals([...state.modals, newEntry]);
|
|
388
|
+
// The last element (highest zIndex) is the active modal
|
|
389
|
+
const activeId = newModals.length > 0 ? newModals[newModals.length - 1].id : null;
|
|
390
|
+
return {
|
|
391
|
+
...state,
|
|
392
|
+
modals: newModals,
|
|
393
|
+
activeModalId: activeId,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
case 'closeModal': {
|
|
397
|
+
if (!state.modals.some(m => m.id === action.id)) {
|
|
398
|
+
throw new Error(`[Ink-Cartridge] Cannot close modal "${action.id}": no modal with that ID exists.`);
|
|
399
|
+
}
|
|
400
|
+
const newModals = state.modals.filter(m => m.id !== action.id);
|
|
401
|
+
const activeId = newModals.length > 0 ? newModals[newModals.length - 1].id : null;
|
|
402
|
+
return {
|
|
403
|
+
...state,
|
|
404
|
+
modals: newModals,
|
|
405
|
+
activeModalId: activeId,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
case 'closeAllModals': {
|
|
409
|
+
return {
|
|
410
|
+
...state,
|
|
411
|
+
modals: [],
|
|
412
|
+
activeModalId: null,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
default:
|
|
416
|
+
return state;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Screen-management context provider.
|
|
421
|
+
*
|
|
422
|
+
* Wraps the application and enables tree-based screen navigation, overlays,
|
|
423
|
+
* and module-level navigation functions.
|
|
424
|
+
*/
|
|
425
|
+
export function ScenarioManagementProvider({ children, defaultScreen, defaultParams, }) {
|
|
426
|
+
if (!hasComponent(defaultScreen)) {
|
|
427
|
+
throw new Error(`[Ink-Cartridge] defaultScreen "${defaultScreen.displayName || defaultScreen.name || 'anonymous'}" is not registered. Please call registerComponent() first.`);
|
|
428
|
+
}
|
|
429
|
+
const initialParams = defaultParams ?? getTemplate(defaultScreen) ?? {};
|
|
430
|
+
const [state, dispatch] = useReducer(screenReducer, {
|
|
431
|
+
path: [defaultScreen],
|
|
432
|
+
pathParams: [initialParams],
|
|
433
|
+
overlays: [],
|
|
434
|
+
activeOverlayIds: new Set(),
|
|
435
|
+
modals: [],
|
|
436
|
+
activeModalId: null,
|
|
437
|
+
counter: 0,
|
|
438
|
+
});
|
|
439
|
+
useEffect(() => {
|
|
440
|
+
_dispatchers.add(dispatch);
|
|
441
|
+
return () => {
|
|
442
|
+
_dispatchers.delete(dispatch);
|
|
443
|
+
};
|
|
444
|
+
}, []);
|
|
445
|
+
const topComponent = state.path[state.path.length - 1];
|
|
446
|
+
const topParams = state.pathParams[state.pathParams.length - 1];
|
|
447
|
+
const currentScreen = useMemo(() => React.createElement(topComponent, {
|
|
448
|
+
...topParams,
|
|
449
|
+
key: state.counter,
|
|
450
|
+
}), [topComponent, topParams, state.counter]);
|
|
451
|
+
// Render all overlay elements (sorted by zIndex)
|
|
452
|
+
const currentOverlays = useMemo(() => state.overlays.map((entry) => React.createElement(entry.component, {
|
|
453
|
+
...entry.props,
|
|
454
|
+
key: entry.id,
|
|
455
|
+
})), [state.overlays]);
|
|
456
|
+
// Determine which modals should be rendered: active modal
|
|
457
|
+
// (highest zIndex) plus any modals with renderNow: true.
|
|
458
|
+
const renderedModalEntries = useMemo(() => state.modals.filter(entry => entry.id === state.activeModalId || entry.renderNow), [state.modals, state.activeModalId]);
|
|
459
|
+
// Render modal elements for the modals that should be displayed.
|
|
460
|
+
// Sorted by zIndex ascending so the last element (highest zIndex)
|
|
461
|
+
// renders on top.
|
|
462
|
+
const currentModals = useMemo(() => renderedModalEntries.map((entry) => React.createElement(entry.component, {
|
|
463
|
+
...entry.props,
|
|
464
|
+
key: entry.id,
|
|
465
|
+
})), [renderedModalEntries]);
|
|
466
|
+
// Context 内的导航方法
|
|
467
|
+
const skipInContext = useMemo(() => (component, params, options) => {
|
|
468
|
+
if (!hasComponent(component)) {
|
|
469
|
+
throw new Error(`[Ink-Cartridge] Component "${component.displayName || component.name || 'anonymous'}" is not registered.`);
|
|
470
|
+
}
|
|
471
|
+
dispatch({
|
|
472
|
+
type: 'skip',
|
|
473
|
+
component,
|
|
474
|
+
params: params,
|
|
475
|
+
onlyAttribute: options?.onlyAttribute ?? false,
|
|
476
|
+
});
|
|
477
|
+
}, []);
|
|
478
|
+
const backInContext = useMemo(() => (levels = 1) => {
|
|
479
|
+
if (levels < 1) {
|
|
480
|
+
throw new Error('[Ink-Cartridge] back() levels must be >= 1.');
|
|
481
|
+
}
|
|
482
|
+
dispatch({ type: 'back', levels });
|
|
483
|
+
}, []);
|
|
484
|
+
const gotoScreenInContext = useMemo(() => (component, params) => {
|
|
485
|
+
if (!hasComponent(component)) {
|
|
486
|
+
throw new Error(`[Ink-Cartridge] Component "${component.displayName || component.name || 'anonymous'}" is not registered.`);
|
|
487
|
+
}
|
|
488
|
+
dispatch({
|
|
489
|
+
type: 'gotoScreen',
|
|
490
|
+
component,
|
|
491
|
+
params: params,
|
|
492
|
+
});
|
|
493
|
+
}, []);
|
|
494
|
+
const openOverlayInContext = useMemo(() => (id, component, params, options) => {
|
|
495
|
+
if (!hasComponent(component)) {
|
|
496
|
+
throw new Error(`[Ink-Cartridge] Component "${component.displayName || component.name || 'anonymous'}" is not registered.`);
|
|
497
|
+
}
|
|
498
|
+
dispatch({
|
|
499
|
+
type: 'openOverlay',
|
|
500
|
+
id,
|
|
501
|
+
component,
|
|
502
|
+
params: params,
|
|
503
|
+
activate: options?.activate ?? true,
|
|
504
|
+
zIndex: options?.zIndex,
|
|
505
|
+
});
|
|
506
|
+
}, []);
|
|
507
|
+
const closeOverlayInContext = useMemo(() => (id) => dispatch({ type: 'closeOverlay', id }), []);
|
|
508
|
+
const closeAllOverlaysInContext = useMemo(() => () => dispatch({ type: 'closeAllOverlays' }), []);
|
|
509
|
+
const activateOverlayInContext = useMemo(() => (id) => dispatch({ type: 'activateOverlay', id }), []);
|
|
510
|
+
const deactivateOverlayInContext = useMemo(() => (id) => dispatch({ type: 'deactivateOverlay', id }), []);
|
|
511
|
+
const openModalInContext = useMemo(() => (id, component, params, options) => {
|
|
512
|
+
if (!hasComponent(component)) {
|
|
513
|
+
throw new Error(`[Ink-Cartridge] Component "${component.displayName || component.name || 'anonymous'}" is not registered.`);
|
|
514
|
+
}
|
|
515
|
+
dispatch({
|
|
516
|
+
type: 'openModal',
|
|
517
|
+
id,
|
|
518
|
+
component,
|
|
519
|
+
params: params,
|
|
520
|
+
zIndex: options?.zIndex,
|
|
521
|
+
renderNow: options?.renderNow,
|
|
522
|
+
});
|
|
523
|
+
}, []);
|
|
524
|
+
const closeModalInContext = useMemo(() => (id) => dispatch({ type: 'closeModal', id }), []);
|
|
525
|
+
const closeAllModalsInContext = useMemo(() => () => dispatch({ type: 'closeAllModals' }), []);
|
|
526
|
+
const activeOverlayIdsArray = useMemo(() => [...state.activeOverlayIds], [state.activeOverlayIds]);
|
|
527
|
+
// Compute activeModal from state
|
|
528
|
+
const activeModal = state.activeModalId
|
|
529
|
+
? state.modals.find(m => m.id === state.activeModalId) ?? null
|
|
530
|
+
: null;
|
|
531
|
+
const value = useMemo(() => ({
|
|
532
|
+
currentScreen,
|
|
533
|
+
currentOverlays,
|
|
534
|
+
currentModals,
|
|
535
|
+
currentPath: state.path,
|
|
536
|
+
skip: skipInContext,
|
|
537
|
+
back: backInContext,
|
|
538
|
+
gotoScreen: gotoScreenInContext,
|
|
539
|
+
openOverlay: openOverlayInContext,
|
|
540
|
+
closeOverlay: closeOverlayInContext,
|
|
541
|
+
closeAllOverlays: closeAllOverlaysInContext,
|
|
542
|
+
activateOverlay: activateOverlayInContext,
|
|
543
|
+
deactivateOverlay: deactivateOverlayInContext,
|
|
544
|
+
activeOverlayIds: activeOverlayIdsArray,
|
|
545
|
+
displayedOverlays: state.overlays,
|
|
546
|
+
displayedModals: state.modals,
|
|
547
|
+
renderedModalEntries,
|
|
548
|
+
activeModalId: state.activeModalId,
|
|
549
|
+
activeModal,
|
|
550
|
+
modalQueue: state.modals,
|
|
551
|
+
openModal: openModalInContext,
|
|
552
|
+
closeModal: closeModalInContext,
|
|
553
|
+
closeAllModals: closeAllModalsInContext,
|
|
554
|
+
}), [
|
|
555
|
+
currentScreen,
|
|
556
|
+
currentOverlays,
|
|
557
|
+
currentModals,
|
|
558
|
+
state.path,
|
|
559
|
+
state.overlays,
|
|
560
|
+
state.modals,
|
|
561
|
+
renderedModalEntries,
|
|
562
|
+
activeModal,
|
|
563
|
+
activeOverlayIdsArray,
|
|
564
|
+
skipInContext,
|
|
565
|
+
backInContext,
|
|
566
|
+
gotoScreenInContext,
|
|
567
|
+
openOverlayInContext,
|
|
568
|
+
closeOverlayInContext,
|
|
569
|
+
closeAllOverlaysInContext,
|
|
570
|
+
activateOverlayInContext,
|
|
571
|
+
deactivateOverlayInContext,
|
|
572
|
+
openModalInContext,
|
|
573
|
+
closeModalInContext,
|
|
574
|
+
closeAllModalsInContext,
|
|
575
|
+
]);
|
|
576
|
+
return (React.createElement(ScreenSystemContext.Provider, { value: value }, children));
|
|
577
|
+
}
|