yarn-spinner-runner-ts 0.1.4 → 0.1.5
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 +34 -16
- package/dist/compile/compiler.js +2 -2
- package/dist/compile/compiler.js.map +1 -1
- package/dist/compile/ir.d.ts +1 -0
- package/dist/markup/parser.js +12 -2
- package/dist/markup/parser.js.map +1 -1
- package/dist/model/ast.d.ts +1 -0
- package/dist/parse/lexer.js +5 -5
- package/dist/parse/lexer.js.map +1 -1
- package/dist/parse/parser.js +12 -2
- package/dist/parse/parser.js.map +1 -1
- package/dist/react/DialogueExample.js +12 -10
- package/dist/react/DialogueExample.js.map +1 -1
- package/dist/react/DialogueView.d.ts +10 -4
- package/dist/react/DialogueView.js +38 -12
- package/dist/react/DialogueView.js.map +1 -1
- package/dist/react/MarkupRenderer.js +1 -1
- package/dist/react/MarkupRenderer.js.map +1 -1
- package/dist/react/useYarnRunner.js +63 -10
- package/dist/react/useYarnRunner.js.map +1 -1
- package/dist/runtime/evaluator.js +3 -2
- package/dist/runtime/evaluator.js.map +1 -1
- package/dist/runtime/runner.d.ts +2 -0
- package/dist/runtime/runner.js +66 -10
- package/dist/runtime/runner.js.map +1 -1
- package/dist/tests/dialogue_view.test.d.ts +1 -0
- package/dist/tests/dialogue_view.test.js +18 -0
- package/dist/tests/dialogue_view.test.js.map +1 -0
- package/dist/tests/markup.test.js +7 -0
- package/dist/tests/markup.test.js.map +1 -1
- package/dist/tests/options.test.js +164 -9
- package/dist/tests/options.test.js.map +1 -1
- package/dist/tests/variables_flow_cmds.test.js +52 -10
- package/dist/tests/variables_flow_cmds.test.js.map +1 -1
- package/docs/markup.md +33 -33
- package/eslint.config.cjs +39 -39
- package/package.json +6 -6
- package/src/compile/compiler.ts +2 -2
- package/src/compile/ir.ts +1 -1
- package/src/markup/parser.ts +53 -43
- package/src/model/ast.ts +1 -0
- package/src/parse/lexer.ts +18 -18
- package/src/parse/parser.ts +33 -22
- package/src/react/DialogueExample.tsx +16 -14
- package/src/react/DialogueView.tsx +312 -275
- package/src/react/MarkupRenderer.tsx +1 -2
- package/src/react/useYarnRunner.tsx +101 -34
- package/src/runtime/evaluator.ts +15 -14
- package/src/runtime/runner.ts +102 -37
- package/src/tests/dialogue_view.test.tsx +26 -0
- package/src/tests/markup.test.ts +17 -1
- package/src/tests/options.test.ts +206 -36
- package/src/tests/variables_flow_cmds.test.ts +72 -28
|
@@ -1,275 +1,312 @@
|
|
|
1
|
-
import React, { useRef, useEffect, useState } from "react";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import { MarkupRenderer } from "./MarkupRenderer.js";
|
|
7
|
-
// Note: CSS is imported in the browser demo entry point (examples/browser/main.tsx)
|
|
8
|
-
// This prevents Node.js from trying to resolve CSS imports during tests
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<div className="yd-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
}
|
|
275
|
-
|
|
1
|
+
import React, { useRef, useEffect, useState } from "react";
|
|
2
|
+
import { DialogueScene } from "./DialogueScene.js";
|
|
3
|
+
import type { SceneCollection } from "../scene/types.js";
|
|
4
|
+
import { TypingText } from "./TypingText.js";
|
|
5
|
+
import { useYarnRunner } from "./useYarnRunner.js";
|
|
6
|
+
import { MarkupRenderer } from "./MarkupRenderer.js";
|
|
7
|
+
// Note: CSS is imported in the browser demo entry point (examples/browser/main.tsx)
|
|
8
|
+
// This prevents Node.js from trying to resolve CSS imports during tests
|
|
9
|
+
|
|
10
|
+
import type { IRProgram } from "../compile/ir.js";
|
|
11
|
+
|
|
12
|
+
export interface DialogueViewProps {
|
|
13
|
+
program: IRProgram;
|
|
14
|
+
startNode?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
scenes?: SceneCollection;
|
|
17
|
+
actorTransitionDuration?: number;
|
|
18
|
+
// Custom functions and callbacks
|
|
19
|
+
functions?: Record<string, (...args: unknown[]) => unknown>;
|
|
20
|
+
variables?: Record<string, unknown>;
|
|
21
|
+
onStoryEnd?: (info: { variables: Readonly<Record<string, unknown>>; storyEnd: true }) => void;
|
|
22
|
+
// Typing animation options
|
|
23
|
+
enableTypingAnimation?: boolean;
|
|
24
|
+
typingSpeed?: number;
|
|
25
|
+
showTypingCursor?: boolean;
|
|
26
|
+
cursorCharacter?: string;
|
|
27
|
+
// Auto-advance after typing completes
|
|
28
|
+
autoAdvanceAfterTyping?: boolean;
|
|
29
|
+
autoAdvanceDelay?: number; // Delay in ms after typing completes before auto-advancing
|
|
30
|
+
// Pause before advance
|
|
31
|
+
pauseBeforeAdvance?: number; // Delay in ms before advancing when clicking (0 = no pause)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Helper to parse CSS string into object
|
|
35
|
+
function parseCss(cssStr: string | undefined): React.CSSProperties {
|
|
36
|
+
if (!cssStr) return {};
|
|
37
|
+
const styles: React.CSSProperties = {};
|
|
38
|
+
// Improved parser: handles quoted values and commas
|
|
39
|
+
// Split by semicolon, but preserve quoted strings
|
|
40
|
+
const rules: string[] = [];
|
|
41
|
+
let currentRule = "";
|
|
42
|
+
let inQuotes = false;
|
|
43
|
+
let quoteChar = "";
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < cssStr.length; i++) {
|
|
46
|
+
const char = cssStr[i];
|
|
47
|
+
if ((char === '"' || char === "'") && !inQuotes) {
|
|
48
|
+
inQuotes = true;
|
|
49
|
+
quoteChar = char;
|
|
50
|
+
currentRule += char;
|
|
51
|
+
} else if (char === quoteChar && inQuotes) {
|
|
52
|
+
inQuotes = false;
|
|
53
|
+
quoteChar = "";
|
|
54
|
+
currentRule += char;
|
|
55
|
+
} else if (char === ";" && !inQuotes) {
|
|
56
|
+
rules.push(currentRule.trim());
|
|
57
|
+
currentRule = "";
|
|
58
|
+
} else {
|
|
59
|
+
currentRule += char;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (currentRule.trim()) {
|
|
63
|
+
rules.push(currentRule.trim());
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
rules.forEach((rule) => {
|
|
67
|
+
if (!rule) return;
|
|
68
|
+
const colonIndex = rule.indexOf(":");
|
|
69
|
+
if (colonIndex === -1) return;
|
|
70
|
+
const prop = rule.slice(0, colonIndex).trim();
|
|
71
|
+
const value = rule.slice(colonIndex + 1).trim();
|
|
72
|
+
if (prop && value) {
|
|
73
|
+
// Convert kebab-case to camelCase
|
|
74
|
+
const camelProp = prop.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
75
|
+
// Remove quotes from value if present, and strip !important (React doesn't support it)
|
|
76
|
+
let cleanValue = value.trim();
|
|
77
|
+
if (cleanValue.endsWith("!important")) {
|
|
78
|
+
cleanValue = cleanValue.slice(0, -10).trim();
|
|
79
|
+
}
|
|
80
|
+
if (cleanValue.startsWith('"') && cleanValue.endsWith('"')) {
|
|
81
|
+
cleanValue = cleanValue.slice(1, -1);
|
|
82
|
+
} else if (cleanValue.startsWith("'") && cleanValue.endsWith("'")) {
|
|
83
|
+
cleanValue = cleanValue.slice(1, -1);
|
|
84
|
+
}
|
|
85
|
+
(styles as any)[camelProp] = cleanValue;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
return styles;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function DialogueView({
|
|
92
|
+
program,
|
|
93
|
+
startNode = "Start",
|
|
94
|
+
className,
|
|
95
|
+
scenes,
|
|
96
|
+
actorTransitionDuration = 350,
|
|
97
|
+
functions,
|
|
98
|
+
variables,
|
|
99
|
+
onStoryEnd,
|
|
100
|
+
enableTypingAnimation = false,
|
|
101
|
+
typingSpeed = 50, // Characters per second (50 cps = ~20ms per character)
|
|
102
|
+
showTypingCursor = true,
|
|
103
|
+
cursorCharacter = "|",
|
|
104
|
+
autoAdvanceAfterTyping = false,
|
|
105
|
+
autoAdvanceDelay = 500,
|
|
106
|
+
pauseBeforeAdvance = 0,
|
|
107
|
+
}: DialogueViewProps) {
|
|
108
|
+
const { result, advance, runner } = useYarnRunner(program, {
|
|
109
|
+
startAt: startNode,
|
|
110
|
+
functions,
|
|
111
|
+
variables,
|
|
112
|
+
});
|
|
113
|
+
const sceneName = result?.type === "text" || result?.type === "options" ? result.scene : undefined;
|
|
114
|
+
const speaker = result?.type === "text" ? result.speaker : undefined;
|
|
115
|
+
const sceneCollection = scenes || { scenes: {} };
|
|
116
|
+
|
|
117
|
+
const [typingComplete, setTypingComplete] = useState(false);
|
|
118
|
+
const [currentTextKey, setCurrentTextKey] = useState(0);
|
|
119
|
+
const [skipTyping, setSkipTyping] = useState(false);
|
|
120
|
+
const advanceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
121
|
+
const storyEndTriggeredRef = useRef(false);
|
|
122
|
+
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
storyEndTriggeredRef.current = false;
|
|
125
|
+
}, [program, startNode]);
|
|
126
|
+
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (!result || result.type !== "command") {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
const timer = setTimeout(() => advance(), 50);
|
|
132
|
+
return () => clearTimeout(timer);
|
|
133
|
+
}, [result, advance]);
|
|
134
|
+
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
if (!onStoryEnd || !result || storyEndTriggeredRef.current) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (!result.isDialogueEnd) {
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (result.type === "options") {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
storyEndTriggeredRef.current = true;
|
|
147
|
+
const variablesSnapshot = Object.freeze({ ...(runner?.getVariables?.() ?? {}) });
|
|
148
|
+
onStoryEnd({ storyEnd: true, variables: variablesSnapshot });
|
|
149
|
+
}, [result, onStoryEnd, runner]);
|
|
150
|
+
|
|
151
|
+
// Reset typing completion when text changes
|
|
152
|
+
useEffect(() => {
|
|
153
|
+
if (result?.type === "text") {
|
|
154
|
+
setTypingComplete(false);
|
|
155
|
+
setSkipTyping(false);
|
|
156
|
+
setCurrentTextKey((prev) => prev + 1); // Force re-render of TypingText
|
|
157
|
+
}
|
|
158
|
+
// Cleanup any pending advance timeouts when text changes
|
|
159
|
+
return () => {
|
|
160
|
+
if (advanceTimeoutRef.current) {
|
|
161
|
+
clearTimeout(advanceTimeoutRef.current);
|
|
162
|
+
advanceTimeoutRef.current = null;
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}, [result?.type === "text" ? result.text : null]);
|
|
166
|
+
|
|
167
|
+
// Handle auto-advance after typing completes
|
|
168
|
+
useEffect(() => {
|
|
169
|
+
if (
|
|
170
|
+
autoAdvanceAfterTyping &&
|
|
171
|
+
typingComplete &&
|
|
172
|
+
result?.type === "text" &&
|
|
173
|
+
!result.isDialogueEnd
|
|
174
|
+
) {
|
|
175
|
+
const timer = setTimeout(() => {
|
|
176
|
+
advance();
|
|
177
|
+
}, autoAdvanceDelay);
|
|
178
|
+
return () => clearTimeout(timer);
|
|
179
|
+
}
|
|
180
|
+
}, [autoAdvanceAfterTyping, typingComplete, result, advance, autoAdvanceDelay]);
|
|
181
|
+
|
|
182
|
+
if (!result) {
|
|
183
|
+
return (
|
|
184
|
+
<div className={`yd-empty ${className || ""}`}>
|
|
185
|
+
<p>Dialogue ended or not started.</p>
|
|
186
|
+
</div>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (result.type === "text") {
|
|
191
|
+
const nodeStyles = parseCss(result.nodeCss);
|
|
192
|
+
const displayText = result.text || "\u00A0";
|
|
193
|
+
const shouldShowContinue = !result.isDialogueEnd && !enableTypingAnimation;
|
|
194
|
+
|
|
195
|
+
const handleClick = () => {
|
|
196
|
+
if (result.isDialogueEnd) return;
|
|
197
|
+
|
|
198
|
+
// If typing is in progress, skip it; otherwise advance
|
|
199
|
+
if (enableTypingAnimation && !typingComplete) {
|
|
200
|
+
// Skip typing animation
|
|
201
|
+
setSkipTyping(true);
|
|
202
|
+
setTypingComplete(true);
|
|
203
|
+
} else {
|
|
204
|
+
// Clear any pending timeout
|
|
205
|
+
if (advanceTimeoutRef.current) {
|
|
206
|
+
clearTimeout(advanceTimeoutRef.current);
|
|
207
|
+
advanceTimeoutRef.current = null;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Apply pause before advance if configured
|
|
211
|
+
if (pauseBeforeAdvance > 0) {
|
|
212
|
+
advanceTimeoutRef.current = setTimeout(() => {
|
|
213
|
+
advance();
|
|
214
|
+
advanceTimeoutRef.current = null;
|
|
215
|
+
}, pauseBeforeAdvance);
|
|
216
|
+
} else {
|
|
217
|
+
advance();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
return (
|
|
223
|
+
<div className="yd-container">
|
|
224
|
+
<DialogueScene
|
|
225
|
+
sceneName={sceneName}
|
|
226
|
+
speaker={speaker}
|
|
227
|
+
scenes={sceneCollection}
|
|
228
|
+
actorTransitionDuration={actorTransitionDuration}
|
|
229
|
+
/>
|
|
230
|
+
<div
|
|
231
|
+
className={`yd-dialogue-box ${result.isDialogueEnd ? "yd-text-box-end" : ""} ${className || ""}`}
|
|
232
|
+
style={nodeStyles} // Only apply dynamic node CSS
|
|
233
|
+
onClick={handleClick}
|
|
234
|
+
>
|
|
235
|
+
<div className="yd-text-box">
|
|
236
|
+
{result.speaker && (
|
|
237
|
+
<div className="yd-speaker">
|
|
238
|
+
{result.speaker}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
<p className={`yd-text ${result.speaker ? "yd-text-with-speaker" : ""}`}>
|
|
242
|
+
{enableTypingAnimation ? (
|
|
243
|
+
<TypingText
|
|
244
|
+
key={currentTextKey}
|
|
245
|
+
text={displayText}
|
|
246
|
+
markup={result.markup}
|
|
247
|
+
typingSpeed={typingSpeed}
|
|
248
|
+
showCursor={showTypingCursor}
|
|
249
|
+
cursorCharacter={cursorCharacter}
|
|
250
|
+
disabled={skipTyping}
|
|
251
|
+
onComplete={() => setTypingComplete(true)}
|
|
252
|
+
/>
|
|
253
|
+
) : (
|
|
254
|
+
<MarkupRenderer text={displayText} markup={result.markup} />
|
|
255
|
+
)}
|
|
256
|
+
</p>
|
|
257
|
+
{shouldShowContinue && (
|
|
258
|
+
<div className="yd-continue">
|
|
259
|
+
▼
|
|
260
|
+
</div>
|
|
261
|
+
)}
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (result.type === "options") {
|
|
269
|
+
const nodeStyles = parseCss(result.nodeCss);
|
|
270
|
+
return (
|
|
271
|
+
<div className="yd-container">
|
|
272
|
+
<DialogueScene
|
|
273
|
+
sceneName={sceneName}
|
|
274
|
+
speaker={speaker}
|
|
275
|
+
scenes={sceneCollection}
|
|
276
|
+
actorTransitionDuration={actorTransitionDuration}
|
|
277
|
+
/>
|
|
278
|
+
<div className={`yd-options-container ${className || ""}`}>
|
|
279
|
+
<div className="yd-options-box" style={nodeStyles}>
|
|
280
|
+
<div className="yd-options-title">Choose an option:</div>
|
|
281
|
+
<div className="yd-options-list">
|
|
282
|
+
{result.options.map((option, index) => {
|
|
283
|
+
const optionStyles = parseCss(option.css);
|
|
284
|
+
return (
|
|
285
|
+
<button
|
|
286
|
+
key={index}
|
|
287
|
+
className="yd-option-button"
|
|
288
|
+
onClick={() => advance(index)}
|
|
289
|
+
style={optionStyles} // Only apply dynamic option CSS
|
|
290
|
+
>
|
|
291
|
+
<MarkupRenderer text={option.text} markup={option.markup} />
|
|
292
|
+
</button>
|
|
293
|
+
);
|
|
294
|
+
})}
|
|
295
|
+
</div>
|
|
296
|
+
</div>
|
|
297
|
+
</div>
|
|
298
|
+
</div>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Command result - auto-advance
|
|
303
|
+
if (result.type === "command") {
|
|
304
|
+
return (
|
|
305
|
+
<div className={`yd-command ${className || ""}`}>
|
|
306
|
+
<p>Executing: {result.command}</p>
|
|
307
|
+
</div>
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import type { MarkupParseResult, MarkupWrapper } from "../markup/types.js";
|
|
3
3
|
|
|
4
|
-
const DEFAULT_HTML_TAGS = new Set(["b", "em", "small", "strong", "sub", "sup", "ins", "del", "mark"]);
|
|
4
|
+
const DEFAULT_HTML_TAGS = new Set(["b", "em", "small", "strong", "sub", "sup", "ins", "del", "mark", "br"]);
|
|
5
5
|
|
|
6
6
|
interface RenderPiece {
|
|
7
7
|
text: string;
|
|
@@ -107,4 +107,3 @@ function createWrapperElement(
|
|
|
107
107
|
function sanitizeClassName(name: string): string {
|
|
108
108
|
return name.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
109
109
|
}
|
|
110
|
-
|