yarn-spinner-runner-ts 0.1.3 → 0.1.4-b

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