@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rohal12/spindle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A Preact-based story format for Twine 2.",
|
|
6
6
|
"license": "Unlicense",
|
|
@@ -58,11 +58,13 @@
|
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"@preact/preset-vite": "^2.9.0",
|
|
61
|
+
"@rohal12/twee-ts": "^1.0.0",
|
|
62
|
+
"@types/js-yaml": "^4.0.9",
|
|
61
63
|
"@vitest/coverage-v8": "^4.0.18",
|
|
62
64
|
"happy-dom": "^20.7.0",
|
|
65
|
+
"js-yaml": "^4.1.1",
|
|
63
66
|
"playwright": "^1.58.2",
|
|
64
67
|
"prettier": "^3.8.1",
|
|
65
|
-
"@rohal12/twee-ts": "^1.0.0",
|
|
66
68
|
"typescript": "^5.7.0",
|
|
67
69
|
"vite": "^6.0.0",
|
|
68
70
|
"vite-plugin-singlefile": "^2.0.0",
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
export type ActionType =
|
|
2
|
+
| 'link'
|
|
3
|
+
| 'button'
|
|
4
|
+
| 'cycle'
|
|
5
|
+
| 'textbox'
|
|
6
|
+
| 'numberbox'
|
|
7
|
+
| 'textarea'
|
|
8
|
+
| 'checkbox'
|
|
9
|
+
| 'radiobutton'
|
|
10
|
+
| 'listbox'
|
|
11
|
+
| 'back'
|
|
12
|
+
| 'forward'
|
|
13
|
+
| 'restart'
|
|
14
|
+
| 'save'
|
|
15
|
+
| 'load';
|
|
16
|
+
|
|
17
|
+
export interface StoryAction {
|
|
18
|
+
id: string;
|
|
19
|
+
type: ActionType;
|
|
20
|
+
label: string;
|
|
21
|
+
target?: string;
|
|
22
|
+
variable?: string;
|
|
23
|
+
options?: string[];
|
|
24
|
+
value?: unknown;
|
|
25
|
+
disabled?: boolean;
|
|
26
|
+
perform: (value?: unknown) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const actions = new Map<string, StoryAction>();
|
|
30
|
+
const listeners = new Set<() => void>();
|
|
31
|
+
const idCounters = new Map<string, number>();
|
|
32
|
+
|
|
33
|
+
export function generateActionId(
|
|
34
|
+
type: ActionType,
|
|
35
|
+
key: string,
|
|
36
|
+
authorId?: string,
|
|
37
|
+
): string {
|
|
38
|
+
if (authorId) return authorId;
|
|
39
|
+
|
|
40
|
+
const base = `${type}:${key}`;
|
|
41
|
+
const count = (idCounters.get(base) ?? 0) + 1;
|
|
42
|
+
idCounters.set(base, count);
|
|
43
|
+
return count === 1 ? base : `${base}:${count}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function registerAction(action: StoryAction): () => void {
|
|
47
|
+
actions.set(action.id, action);
|
|
48
|
+
notify();
|
|
49
|
+
return () => {
|
|
50
|
+
actions.delete(action.id);
|
|
51
|
+
notify();
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function getActions(): StoryAction[] {
|
|
56
|
+
return Array.from(actions.values());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function getAction(id: string): StoryAction | undefined {
|
|
60
|
+
return actions.get(id);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function clearActions(): void {
|
|
64
|
+
actions.clear();
|
|
65
|
+
notify();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function resetIdCounters(): void {
|
|
69
|
+
idCounters.clear();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function onActionsChanged(fn: () => void): () => void {
|
|
73
|
+
listeners.add(fn);
|
|
74
|
+
return () => {
|
|
75
|
+
listeners.delete(fn);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function notify(): void {
|
|
80
|
+
for (const fn of listeners) {
|
|
81
|
+
fn();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { runAutomation } from './runner';
|
|
2
|
+
export type { RunOptions } from './runner';
|
|
3
|
+
export { parseAutomationYaml } from './load-yaml';
|
|
4
|
+
export type {
|
|
5
|
+
AutomationScript,
|
|
6
|
+
AutomationStep,
|
|
7
|
+
AssertStep,
|
|
8
|
+
ActionMatcher,
|
|
9
|
+
RunResult,
|
|
10
|
+
StepError,
|
|
11
|
+
} from './types';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
import type { AutomationScript } from './types';
|
|
3
|
+
|
|
4
|
+
export function parseAutomationYaml(yamlContent: string): AutomationScript {
|
|
5
|
+
const doc = yaml.load(yamlContent) as Record<string, unknown>;
|
|
6
|
+
|
|
7
|
+
if (!doc || typeof doc !== 'object') {
|
|
8
|
+
throw new Error('Invalid YAML: expected an object');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (typeof doc.name !== 'string') {
|
|
12
|
+
throw new Error('Invalid automation script: missing "name" field');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (!Array.isArray(doc.steps)) {
|
|
16
|
+
throw new Error('Invalid automation script: missing "steps" array');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return doc as unknown as AutomationScript;
|
|
20
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AutomationScript,
|
|
3
|
+
AutomationStep,
|
|
4
|
+
RunResult,
|
|
5
|
+
StepError,
|
|
6
|
+
ActionMatcher,
|
|
7
|
+
} from './types';
|
|
8
|
+
import type { StoryAPI } from '../story-api';
|
|
9
|
+
import type { StoryAction } from '../action-registry';
|
|
10
|
+
|
|
11
|
+
export interface RunOptions {
|
|
12
|
+
onStep?: (stepIndex: number, step: AutomationStep) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function runAutomation(
|
|
16
|
+
story: StoryAPI,
|
|
17
|
+
script: AutomationScript,
|
|
18
|
+
options: RunOptions = {},
|
|
19
|
+
): Promise<RunResult> {
|
|
20
|
+
const errors: StepError[] = [];
|
|
21
|
+
let stepsRun = 0;
|
|
22
|
+
|
|
23
|
+
// Navigate to start passage if specified
|
|
24
|
+
if (script.start) {
|
|
25
|
+
story.goto(script.start);
|
|
26
|
+
await story.waitForActions();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (let i = 0; i < script.steps.length; i++) {
|
|
30
|
+
const step = script.steps[i];
|
|
31
|
+
options.onStep?.(i, step);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
await executeStep(story, step, i, errors);
|
|
35
|
+
} catch (err) {
|
|
36
|
+
errors.push({
|
|
37
|
+
step: i,
|
|
38
|
+
message: err instanceof Error ? err.message : String(err),
|
|
39
|
+
});
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
stepsRun = i + 1;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
success: errors.length === 0,
|
|
48
|
+
stepsRun,
|
|
49
|
+
errors,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function executeStep(
|
|
54
|
+
story: StoryAPI,
|
|
55
|
+
step: AutomationStep,
|
|
56
|
+
index: number,
|
|
57
|
+
errors: StepError[],
|
|
58
|
+
): Promise<void> {
|
|
59
|
+
if (step.set) {
|
|
60
|
+
story.set(step.set);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (step.action !== undefined) {
|
|
64
|
+
const id = typeof step.action === 'string' ? step.action : step.action.id;
|
|
65
|
+
const value =
|
|
66
|
+
typeof step.action === 'string' ? undefined : step.action.value;
|
|
67
|
+
story.performAction(id, value);
|
|
68
|
+
await story.waitForActions();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (step.wait !== undefined) {
|
|
72
|
+
await new Promise((resolve) => setTimeout(resolve, step.wait));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (step.assert) {
|
|
76
|
+
const { assert } = step;
|
|
77
|
+
|
|
78
|
+
if (assert.passage !== undefined) {
|
|
79
|
+
if (story.passage !== assert.passage) {
|
|
80
|
+
errors.push({
|
|
81
|
+
step: index,
|
|
82
|
+
message: `Expected passage "${assert.passage}", got "${story.passage}"`,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (assert.variables) {
|
|
88
|
+
for (const [key, expected] of Object.entries(assert.variables)) {
|
|
89
|
+
const actual = story.get(key);
|
|
90
|
+
if (actual !== expected) {
|
|
91
|
+
errors.push({
|
|
92
|
+
step: index,
|
|
93
|
+
message: `Variable "${key}": expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (assert.actionCount !== undefined) {
|
|
100
|
+
const actions = story.getActions();
|
|
101
|
+
if (actions.length !== assert.actionCount) {
|
|
102
|
+
errors.push({
|
|
103
|
+
step: index,
|
|
104
|
+
message: `Expected ${assert.actionCount} actions, got ${actions.length}`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (assert.actions) {
|
|
110
|
+
const actions = story.getActions();
|
|
111
|
+
for (const matcher of assert.actions) {
|
|
112
|
+
if (!findMatchingAction(actions, matcher)) {
|
|
113
|
+
errors.push({
|
|
114
|
+
step: index,
|
|
115
|
+
message: `No action matching ${JSON.stringify(matcher)}`,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function findMatchingAction(
|
|
124
|
+
actions: StoryAction[],
|
|
125
|
+
matcher: ActionMatcher,
|
|
126
|
+
): StoryAction | undefined {
|
|
127
|
+
return actions.find((action) => {
|
|
128
|
+
if (matcher.id !== undefined && action.id !== matcher.id) return false;
|
|
129
|
+
if (matcher.type !== undefined && action.type !== matcher.type)
|
|
130
|
+
return false;
|
|
131
|
+
if (matcher.target !== undefined && action.target !== matcher.target)
|
|
132
|
+
return false;
|
|
133
|
+
if (matcher.variable !== undefined && action.variable !== matcher.variable)
|
|
134
|
+
return false;
|
|
135
|
+
if (matcher.label !== undefined && action.label !== matcher.label)
|
|
136
|
+
return false;
|
|
137
|
+
return true;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface ActionMatcher {
|
|
2
|
+
type?: string;
|
|
3
|
+
id?: string;
|
|
4
|
+
target?: string;
|
|
5
|
+
variable?: string;
|
|
6
|
+
label?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AssertStep {
|
|
10
|
+
passage?: string;
|
|
11
|
+
variables?: Record<string, unknown>;
|
|
12
|
+
actions?: ActionMatcher[];
|
|
13
|
+
actionCount?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface AutomationStep {
|
|
17
|
+
action?: string | { id: string; value?: unknown };
|
|
18
|
+
assert?: AssertStep;
|
|
19
|
+
wait?: number;
|
|
20
|
+
set?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AutomationScript {
|
|
24
|
+
name: string;
|
|
25
|
+
start?: string;
|
|
26
|
+
steps: AutomationStep[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface StepError {
|
|
30
|
+
step: number;
|
|
31
|
+
message: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface RunResult {
|
|
35
|
+
success: boolean;
|
|
36
|
+
stepsRun: number;
|
|
37
|
+
errors: StepError[];
|
|
38
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../store';
|
|
2
|
+
import { useAction } from '../hooks/use-action';
|
|
2
3
|
|
|
3
4
|
interface PassageLinkProps {
|
|
4
5
|
target: string;
|
|
@@ -20,6 +21,15 @@ export function PassageLink({
|
|
|
20
21
|
navigate(target);
|
|
21
22
|
};
|
|
22
23
|
|
|
24
|
+
useAction({
|
|
25
|
+
type: 'link',
|
|
26
|
+
key: target,
|
|
27
|
+
authorId: id,
|
|
28
|
+
label: typeof children === 'string' ? children : target,
|
|
29
|
+
target,
|
|
30
|
+
perform: () => navigate(target),
|
|
31
|
+
});
|
|
32
|
+
|
|
23
33
|
const cls = className ? `passage-link ${className}` : 'passage-link';
|
|
24
34
|
|
|
25
35
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
|
+
import { useAction } from '../../hooks/use-action';
|
|
2
3
|
|
|
3
4
|
interface BackProps {
|
|
4
5
|
className?: string;
|
|
@@ -10,6 +11,15 @@ export function Back({ className, id }: BackProps) {
|
|
|
10
11
|
const canGoBack = useStoryStore((s) => s.historyIndex > 0);
|
|
11
12
|
const cls = className ? `menubar-button ${className}` : 'menubar-button';
|
|
12
13
|
|
|
14
|
+
useAction({
|
|
15
|
+
type: 'back',
|
|
16
|
+
key: 'back',
|
|
17
|
+
authorId: id,
|
|
18
|
+
label: 'Back',
|
|
19
|
+
disabled: !canGoBack,
|
|
20
|
+
perform: () => goBack(),
|
|
21
|
+
});
|
|
22
|
+
|
|
13
23
|
return (
|
|
14
24
|
<button
|
|
15
25
|
id={id}
|
|
@@ -2,6 +2,8 @@ import { useStoryStore } from '../../store';
|
|
|
2
2
|
import { execute } from '../../expression';
|
|
3
3
|
import { renderInlineNodes } from '../../markup/render';
|
|
4
4
|
import { deepClone } from '../../class-registry';
|
|
5
|
+
import { collectText } from '../../utils/extract-text';
|
|
6
|
+
import { useAction } from '../../hooks/use-action';
|
|
5
7
|
import type { ASTNode } from '../../markup/ast';
|
|
6
8
|
|
|
7
9
|
interface ButtonProps {
|
|
@@ -36,6 +38,14 @@ export function Button({ rawArgs, children, className, id }: ButtonProps) {
|
|
|
36
38
|
}
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
useAction({
|
|
42
|
+
type: 'button',
|
|
43
|
+
key: rawArgs,
|
|
44
|
+
authorId: id,
|
|
45
|
+
label: collectText(children) || rawArgs,
|
|
46
|
+
perform: handleClick,
|
|
47
|
+
});
|
|
48
|
+
|
|
39
49
|
const cls = className ? `macro-button ${className}` : 'macro-button';
|
|
40
50
|
|
|
41
51
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
|
+
import { useAction } from '../../hooks/use-action';
|
|
2
3
|
|
|
3
4
|
interface CheckboxProps {
|
|
4
5
|
rawArgs: string;
|
|
@@ -23,6 +24,16 @@ export function Checkbox({ rawArgs, className, id }: CheckboxProps) {
|
|
|
23
24
|
const value = useStoryStore((s) => s.variables[name]);
|
|
24
25
|
const setVariable = useStoryStore((s) => s.setVariable);
|
|
25
26
|
|
|
27
|
+
useAction({
|
|
28
|
+
type: 'checkbox',
|
|
29
|
+
key: `$${name}`,
|
|
30
|
+
authorId: id,
|
|
31
|
+
label: label || name,
|
|
32
|
+
variable: name,
|
|
33
|
+
value: !!value,
|
|
34
|
+
perform: (v) => setVariable(name, v !== undefined ? !!v : !value),
|
|
35
|
+
});
|
|
36
|
+
|
|
26
37
|
const cls = className ? `macro-checkbox ${className}` : 'macro-checkbox';
|
|
27
38
|
|
|
28
39
|
return (
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
2
|
import { extractOptions } from './option-utils';
|
|
3
|
+
import { useAction } from '../../hooks/use-action';
|
|
3
4
|
import type { ASTNode } from '../../markup/ast';
|
|
4
5
|
|
|
5
6
|
interface CycleProps {
|
|
@@ -25,6 +26,23 @@ export function Cycle({ rawArgs, children, className, id }: CycleProps) {
|
|
|
25
26
|
setVariable(name, options[nextIndex]);
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
useAction({
|
|
30
|
+
type: 'cycle',
|
|
31
|
+
key: `$${name}`,
|
|
32
|
+
authorId: id,
|
|
33
|
+
label: value == null ? options[0] || '' : String(value),
|
|
34
|
+
variable: name,
|
|
35
|
+
options,
|
|
36
|
+
value,
|
|
37
|
+
perform: (v) => {
|
|
38
|
+
if (v !== undefined) {
|
|
39
|
+
setVariable(name, v);
|
|
40
|
+
} else {
|
|
41
|
+
handleClick();
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
28
46
|
const cls = className ? `macro-cycle ${className}` : 'macro-cycle';
|
|
29
47
|
|
|
30
48
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
|
+
import { useAction } from '../../hooks/use-action';
|
|
2
3
|
|
|
3
4
|
interface ForwardProps {
|
|
4
5
|
className?: string;
|
|
@@ -12,6 +13,15 @@ export function Forward({ className, id }: ForwardProps) {
|
|
|
12
13
|
);
|
|
13
14
|
const cls = className ? `menubar-button ${className}` : 'menubar-button';
|
|
14
15
|
|
|
16
|
+
useAction({
|
|
17
|
+
type: 'forward',
|
|
18
|
+
key: 'forward',
|
|
19
|
+
authorId: id,
|
|
20
|
+
label: 'Forward',
|
|
21
|
+
disabled: !canGoForward,
|
|
22
|
+
perform: () => goForward(),
|
|
23
|
+
});
|
|
24
|
+
|
|
15
25
|
return (
|
|
16
26
|
<button
|
|
17
27
|
id={id}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
2
|
import { extractOptions } from './option-utils';
|
|
3
|
+
import { useAction } from '../../hooks/use-action';
|
|
3
4
|
import type { ASTNode } from '../../markup/ast';
|
|
4
5
|
|
|
5
6
|
interface ListboxProps {
|
|
@@ -18,6 +19,19 @@ export function Listbox({ rawArgs, children, className, id }: ListboxProps) {
|
|
|
18
19
|
|
|
19
20
|
const options = extractOptions(children);
|
|
20
21
|
|
|
22
|
+
useAction({
|
|
23
|
+
type: 'listbox',
|
|
24
|
+
key: `$${name}`,
|
|
25
|
+
authorId: id,
|
|
26
|
+
label: name,
|
|
27
|
+
variable: name,
|
|
28
|
+
options,
|
|
29
|
+
value,
|
|
30
|
+
perform: (v) => {
|
|
31
|
+
if (v !== undefined) setVariable(name, String(v));
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
|
|
21
35
|
const cls = className ? `macro-listbox ${className}` : 'macro-listbox';
|
|
22
36
|
|
|
23
37
|
return (
|
|
@@ -31,12 +31,8 @@ function parseArgs(rawArgs: string): {
|
|
|
31
31
|
return { display: rawArgs.trim(), passage: null };
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
function collectText(nodes: ASTNode[]): string {
|
|
38
|
-
return nodes.map((n) => (n.type === 'text' ? n.value : '')).join('');
|
|
39
|
-
}
|
|
34
|
+
import { collectText } from '../../utils/extract-text';
|
|
35
|
+
import { useAction } from '../../hooks/use-action';
|
|
40
36
|
|
|
41
37
|
/**
|
|
42
38
|
* Execute the children imperatively: walk AST for {set} and {do} macros.
|
|
@@ -93,6 +89,20 @@ export function MacroLink({
|
|
|
93
89
|
}
|
|
94
90
|
};
|
|
95
91
|
|
|
92
|
+
useAction({
|
|
93
|
+
type: 'link',
|
|
94
|
+
key: passage || display,
|
|
95
|
+
authorId: id,
|
|
96
|
+
label: display,
|
|
97
|
+
target: passage ?? undefined,
|
|
98
|
+
perform: () => {
|
|
99
|
+
executeChildren(children);
|
|
100
|
+
if (passage) {
|
|
101
|
+
useStoryStore.getState().navigate(passage);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
96
106
|
const cls = className ? `macro-link ${className}` : 'macro-link';
|
|
97
107
|
|
|
98
108
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
|
+
import { useAction } from '../../hooks/use-action';
|
|
2
3
|
|
|
3
4
|
interface NumberboxProps {
|
|
4
5
|
rawArgs: string;
|
|
@@ -23,6 +24,16 @@ export function Numberbox({ rawArgs, className, id }: NumberboxProps) {
|
|
|
23
24
|
const value = useStoryStore((s) => s.variables[name]);
|
|
24
25
|
const setVariable = useStoryStore((s) => s.setVariable);
|
|
25
26
|
|
|
27
|
+
useAction({
|
|
28
|
+
type: 'numberbox',
|
|
29
|
+
key: `$${name}`,
|
|
30
|
+
authorId: id,
|
|
31
|
+
label: placeholder || name,
|
|
32
|
+
variable: name,
|
|
33
|
+
value,
|
|
34
|
+
perform: (v) => setVariable(name, v !== undefined ? Number(v) : 0),
|
|
35
|
+
});
|
|
36
|
+
|
|
26
37
|
const cls = className ? `macro-numberbox ${className}` : 'macro-numberbox';
|
|
27
38
|
|
|
28
39
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
|
+
import { useAction } from '../../hooks/use-action';
|
|
2
3
|
|
|
3
4
|
interface QuickLoadProps {
|
|
4
5
|
className?: string;
|
|
@@ -19,6 +20,15 @@ export function QuickLoad({ className, id }: QuickLoadProps) {
|
|
|
19
20
|
}
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
useAction({
|
|
24
|
+
type: 'load',
|
|
25
|
+
key: 'quickload',
|
|
26
|
+
authorId: id,
|
|
27
|
+
label: 'QuickLoad',
|
|
28
|
+
disabled,
|
|
29
|
+
perform: () => load(),
|
|
30
|
+
});
|
|
31
|
+
|
|
22
32
|
return (
|
|
23
33
|
<button
|
|
24
34
|
id={id}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
|
+
import { useAction } from '../../hooks/use-action';
|
|
2
3
|
|
|
3
4
|
interface QuickSaveProps {
|
|
4
5
|
className?: string;
|
|
@@ -9,6 +10,14 @@ export function QuickSave({ className, id }: QuickSaveProps) {
|
|
|
9
10
|
const save = useStoryStore((s) => s.save);
|
|
10
11
|
const cls = className ? `menubar-button ${className}` : 'menubar-button';
|
|
11
12
|
|
|
13
|
+
useAction({
|
|
14
|
+
type: 'save',
|
|
15
|
+
key: 'quicksave',
|
|
16
|
+
authorId: id,
|
|
17
|
+
label: 'QuickSave',
|
|
18
|
+
perform: () => save(),
|
|
19
|
+
});
|
|
20
|
+
|
|
12
21
|
return (
|
|
13
22
|
<button
|
|
14
23
|
id={id}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
|
+
import { useAction } from '../../hooks/use-action';
|
|
2
3
|
|
|
3
4
|
interface RadiobuttonProps {
|
|
4
5
|
rawArgs: string;
|
|
@@ -38,6 +39,16 @@ export function Radiobutton({ rawArgs, className, id }: RadiobuttonProps) {
|
|
|
38
39
|
const currentValue = useStoryStore((s) => s.variables[name]);
|
|
39
40
|
const setVariable = useStoryStore((s) => s.setVariable);
|
|
40
41
|
|
|
42
|
+
useAction({
|
|
43
|
+
type: 'radiobutton',
|
|
44
|
+
key: `$${name}:${radioValue}`,
|
|
45
|
+
authorId: id,
|
|
46
|
+
label: label || radioValue,
|
|
47
|
+
variable: name,
|
|
48
|
+
value: currentValue,
|
|
49
|
+
perform: () => setVariable(name, radioValue),
|
|
50
|
+
});
|
|
51
|
+
|
|
41
52
|
const cls = className
|
|
42
53
|
? `macro-radiobutton ${className}`
|
|
43
54
|
: 'macro-radiobutton';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
|
+
import { useAction } from '../../hooks/use-action';
|
|
2
3
|
|
|
3
4
|
interface RestartProps {
|
|
4
5
|
className?: string;
|
|
@@ -15,6 +16,14 @@ export function Restart({ className, id }: RestartProps) {
|
|
|
15
16
|
}
|
|
16
17
|
};
|
|
17
18
|
|
|
19
|
+
useAction({
|
|
20
|
+
type: 'restart',
|
|
21
|
+
key: 'restart',
|
|
22
|
+
authorId: id,
|
|
23
|
+
label: 'Restart',
|
|
24
|
+
perform: () => restart(),
|
|
25
|
+
});
|
|
26
|
+
|
|
18
27
|
return (
|
|
19
28
|
<button
|
|
20
29
|
id={id}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
|
+
import { useAction } from '../../hooks/use-action';
|
|
2
3
|
|
|
3
4
|
interface TextareaProps {
|
|
4
5
|
rawArgs: string;
|
|
@@ -23,6 +24,16 @@ export function Textarea({ rawArgs, className, id }: TextareaProps) {
|
|
|
23
24
|
const value = useStoryStore((s) => s.variables[name]);
|
|
24
25
|
const setVariable = useStoryStore((s) => s.setVariable);
|
|
25
26
|
|
|
27
|
+
useAction({
|
|
28
|
+
type: 'textarea',
|
|
29
|
+
key: `$${name}`,
|
|
30
|
+
authorId: id,
|
|
31
|
+
label: placeholder || name,
|
|
32
|
+
variable: name,
|
|
33
|
+
value,
|
|
34
|
+
perform: (v) => setVariable(name, v !== undefined ? String(v) : ''),
|
|
35
|
+
});
|
|
36
|
+
|
|
26
37
|
const cls = className ? `macro-textarea ${className}` : 'macro-textarea';
|
|
27
38
|
|
|
28
39
|
return (
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useStoryStore } from '../../store';
|
|
2
|
+
import { useAction } from '../../hooks/use-action';
|
|
2
3
|
|
|
3
4
|
interface TextboxProps {
|
|
4
5
|
rawArgs: string;
|
|
@@ -23,6 +24,16 @@ export function Textbox({ rawArgs, className, id }: TextboxProps) {
|
|
|
23
24
|
const value = useStoryStore((s) => s.variables[name]);
|
|
24
25
|
const setVariable = useStoryStore((s) => s.setVariable);
|
|
25
26
|
|
|
27
|
+
useAction({
|
|
28
|
+
type: 'textbox',
|
|
29
|
+
key: `$${name}`,
|
|
30
|
+
authorId: id,
|
|
31
|
+
label: placeholder || name,
|
|
32
|
+
variable: name,
|
|
33
|
+
value,
|
|
34
|
+
perform: (v) => setVariable(name, v !== undefined ? String(v) : ''),
|
|
35
|
+
});
|
|
36
|
+
|
|
26
37
|
const cls = className ? `macro-textbox ${className}` : 'macro-textbox';
|
|
27
38
|
|
|
28
39
|
return (
|