@rohal12/spindle 0.2.0 → 0.3.0
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/dist/pkg/format.js +1 -1
- package/package.json +4 -2
- package/src/action-registry.ts +83 -0
- package/src/automation/index.ts +11 -0
- package/src/automation/load-yaml.ts +20 -0
- package/src/automation/runner.ts +139 -0
- package/src/automation/types.ts +38 -0
- package/src/components/PassageLink.tsx +10 -0
- package/src/components/macros/Back.tsx +10 -0
- package/src/components/macros/Button.tsx +10 -0
- package/src/components/macros/Checkbox.tsx +11 -0
- package/src/components/macros/Cycle.tsx +18 -0
- package/src/components/macros/Forward.tsx +10 -0
- package/src/components/macros/Listbox.tsx +14 -0
- package/src/components/macros/MacroLink.tsx +16 -6
- package/src/components/macros/Numberbox.tsx +11 -0
- package/src/components/macros/QuickLoad.tsx +10 -0
- package/src/components/macros/QuickSave.tsx +9 -0
- package/src/components/macros/Radiobutton.tsx +11 -0
- package/src/components/macros/Restart.tsx +9 -0
- package/src/components/macros/Textarea.tsx +11 -0
- package/src/components/macros/Textbox.tsx +11 -0
- package/src/hooks/use-action.ts +49 -0
- package/src/index.tsx +10 -0
- package/src/story-api.ts +91 -0
- package/src/story-variables.ts +3 -1
- package/src/utils/extract-text.ts +8 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { useLayoutEffect, useRef } from 'preact/hooks';
|
|
2
|
+
import {
|
|
3
|
+
registerAction,
|
|
4
|
+
generateActionId,
|
|
5
|
+
type ActionType,
|
|
6
|
+
type StoryAction,
|
|
7
|
+
} from '../action-registry';
|
|
8
|
+
|
|
9
|
+
export interface UseActionOptions {
|
|
10
|
+
type: ActionType;
|
|
11
|
+
key: string;
|
|
12
|
+
authorId?: string;
|
|
13
|
+
label: string;
|
|
14
|
+
target?: string;
|
|
15
|
+
variable?: string;
|
|
16
|
+
options?: string[];
|
|
17
|
+
value?: unknown;
|
|
18
|
+
disabled?: boolean;
|
|
19
|
+
perform: (value?: unknown) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function useAction(opts: UseActionOptions): string {
|
|
23
|
+
const idRef = useRef<string>('');
|
|
24
|
+
|
|
25
|
+
// Generate ID only once on first call
|
|
26
|
+
if (!idRef.current) {
|
|
27
|
+
idRef.current = generateActionId(opts.type, opts.key, opts.authorId);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const id = idRef.current;
|
|
31
|
+
|
|
32
|
+
useLayoutEffect(() => {
|
|
33
|
+
const action: StoryAction = {
|
|
34
|
+
id,
|
|
35
|
+
type: opts.type,
|
|
36
|
+
label: opts.label,
|
|
37
|
+
perform: opts.perform,
|
|
38
|
+
};
|
|
39
|
+
if (opts.target !== undefined) action.target = opts.target;
|
|
40
|
+
if (opts.variable !== undefined) action.variable = opts.variable;
|
|
41
|
+
if (opts.options !== undefined) action.options = opts.options;
|
|
42
|
+
if (opts.value !== undefined) action.value = opts.value;
|
|
43
|
+
if (opts.disabled !== undefined) action.disabled = opts.disabled;
|
|
44
|
+
|
|
45
|
+
return registerAction(action);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return id;
|
|
49
|
+
}
|
package/src/index.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { App } from './components/App';
|
|
|
3
3
|
import { parseStoryData } from './parser';
|
|
4
4
|
import { useStoryStore } from './store';
|
|
5
5
|
import { installStoryAPI } from './story-api';
|
|
6
|
+
import { resetIdCounters } from './action-registry';
|
|
6
7
|
import { executeStoryInit } from './story-init';
|
|
7
8
|
import {
|
|
8
9
|
parseStoryVariables,
|
|
@@ -101,6 +102,15 @@ function boot() {
|
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
// Reset action ID counters on passage change
|
|
106
|
+
let prevPassage = '';
|
|
107
|
+
useStoryStore.subscribe((state) => {
|
|
108
|
+
if (state.currentPassage !== prevPassage) {
|
|
109
|
+
prevPassage = state.currentPassage;
|
|
110
|
+
resetIdCounters();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
104
114
|
const root = document.getElementById('root');
|
|
105
115
|
if (!root) {
|
|
106
116
|
throw new Error('spindle: No <div id="root"> element found.');
|
package/src/story-api.ts
CHANGED
|
@@ -3,6 +3,20 @@ import { settings } from './settings';
|
|
|
3
3
|
import type { SavePayload } from './saves/types';
|
|
4
4
|
import { setTitleGenerator } from './saves/save-manager';
|
|
5
5
|
import { registerClass } from './class-registry';
|
|
6
|
+
import {
|
|
7
|
+
getActions,
|
|
8
|
+
getAction,
|
|
9
|
+
onActionsChanged,
|
|
10
|
+
type StoryAction,
|
|
11
|
+
} from './action-registry';
|
|
12
|
+
|
|
13
|
+
export type { StoryAction };
|
|
14
|
+
|
|
15
|
+
type NavigateCallback = (to: string, from: string) => void;
|
|
16
|
+
type ActionsChangedCallback = () => void;
|
|
17
|
+
type VariableChangedCallback = (
|
|
18
|
+
changed: Record<string, { from: unknown; to: unknown }>,
|
|
19
|
+
) => void;
|
|
6
20
|
|
|
7
21
|
export interface StoryAPI {
|
|
8
22
|
get(name: string): unknown;
|
|
@@ -24,11 +38,18 @@ export interface StoryAPI {
|
|
|
24
38
|
hasRenderedAny(...names: string[]): boolean;
|
|
25
39
|
hasRenderedAll(...names: string[]): boolean;
|
|
26
40
|
readonly title: string;
|
|
41
|
+
readonly passage: string;
|
|
27
42
|
readonly settings: typeof settings;
|
|
28
43
|
registerClass(name: string, ctor: new (...args: any[]) => any): void;
|
|
29
44
|
readonly saves: {
|
|
30
45
|
setTitleGenerator(fn: (payload: SavePayload) => string): void;
|
|
31
46
|
};
|
|
47
|
+
getActions(): StoryAction[];
|
|
48
|
+
performAction(id: string, value?: unknown): void;
|
|
49
|
+
on(event: 'navigate', callback: NavigateCallback): () => void;
|
|
50
|
+
on(event: 'actionsChanged', callback: ActionsChangedCallback): () => void;
|
|
51
|
+
on(event: 'variableChanged', callback: VariableChangedCallback): () => void;
|
|
52
|
+
waitForActions(): Promise<StoryAction[]>;
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
function createStoryAPI(): StoryAPI {
|
|
@@ -116,6 +137,10 @@ function createStoryAPI(): StoryAPI {
|
|
|
116
137
|
return useStoryStore.getState().storyData?.name || '';
|
|
117
138
|
},
|
|
118
139
|
|
|
140
|
+
get passage(): string {
|
|
141
|
+
return useStoryStore.getState().currentPassage;
|
|
142
|
+
},
|
|
143
|
+
|
|
119
144
|
settings,
|
|
120
145
|
|
|
121
146
|
registerClass(name: string, ctor: new (...args: any[]) => any): void {
|
|
@@ -127,6 +152,72 @@ function createStoryAPI(): StoryAPI {
|
|
|
127
152
|
setTitleGenerator(fn);
|
|
128
153
|
},
|
|
129
154
|
},
|
|
155
|
+
|
|
156
|
+
getActions(): StoryAction[] {
|
|
157
|
+
return getActions();
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
performAction(id: string, value?: unknown): void {
|
|
161
|
+
const action = getAction(id);
|
|
162
|
+
if (!action) {
|
|
163
|
+
throw new Error(`spindle: Action "${id}" not found.`);
|
|
164
|
+
}
|
|
165
|
+
if (action.disabled) {
|
|
166
|
+
throw new Error(`spindle: Action "${id}" is disabled.`);
|
|
167
|
+
}
|
|
168
|
+
action.perform(value);
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
on(event: string, callback: (...args: any[]) => void): () => void {
|
|
172
|
+
if (event === 'navigate') {
|
|
173
|
+
let prev = useStoryStore.getState().currentPassage;
|
|
174
|
+
return useStoryStore.subscribe((state) => {
|
|
175
|
+
if (state.currentPassage !== prev) {
|
|
176
|
+
const from = prev;
|
|
177
|
+
prev = state.currentPassage;
|
|
178
|
+
(callback as NavigateCallback)(state.currentPassage, from);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (event === 'actionsChanged') {
|
|
184
|
+
return onActionsChanged(callback as ActionsChangedCallback);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (event === 'variableChanged') {
|
|
188
|
+
let prevVars = { ...useStoryStore.getState().variables };
|
|
189
|
+
return useStoryStore.subscribe((state) => {
|
|
190
|
+
const changed: Record<string, { from: unknown; to: unknown }> = {};
|
|
191
|
+
let hasChanges = false;
|
|
192
|
+
const allKeys = new Set([
|
|
193
|
+
...Object.keys(prevVars),
|
|
194
|
+
...Object.keys(state.variables),
|
|
195
|
+
]);
|
|
196
|
+
for (const key of allKeys) {
|
|
197
|
+
if (state.variables[key] !== prevVars[key]) {
|
|
198
|
+
changed[key] = { from: prevVars[key], to: state.variables[key] };
|
|
199
|
+
hasChanges = true;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
prevVars = { ...state.variables };
|
|
203
|
+
if (hasChanges) {
|
|
204
|
+
(callback as VariableChangedCallback)(changed);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
throw new Error(`spindle: Unknown event "${event}".`);
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
waitForActions(): Promise<StoryAction[]> {
|
|
213
|
+
return new Promise((resolve) => {
|
|
214
|
+
requestAnimationFrame(() => {
|
|
215
|
+
requestAnimationFrame(() => {
|
|
216
|
+
resolve(getActions());
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
},
|
|
130
221
|
};
|
|
131
222
|
}
|
|
132
223
|
|
package/src/story-variables.ts
CHANGED
|
@@ -114,7 +114,9 @@ function validateRef(
|
|
|
114
114
|
}
|
|
115
115
|
const fieldSchema = current.fields.get(parts[i]);
|
|
116
116
|
if (!fieldSchema) {
|
|
117
|
-
|
|
117
|
+
// Unknown fields on objects are allowed — classes registered via
|
|
118
|
+
// Story.registerClass() can add methods/getters not in the defaults.
|
|
119
|
+
return null;
|
|
118
120
|
}
|
|
119
121
|
current = fieldSchema;
|
|
120
122
|
}
|