yarn-spinner-runner-ts 0.1.1 → 0.1.2-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 (55) hide show
  1. package/.github/workflows/npm-publish-github-packages.yml +39 -0
  2. package/README.md +97 -89
  3. package/dist/compile/compiler.js +5 -3
  4. package/dist/compile/compiler.js.map +1 -1
  5. package/dist/compile/ir.d.ts +1 -0
  6. package/dist/index.d.ts +6 -0
  7. package/dist/index.js +6 -0
  8. package/dist/index.js.map +1 -1
  9. package/dist/parse/parser.js +5 -2
  10. package/dist/parse/parser.js.map +1 -1
  11. package/dist/react/DialogueExample.d.ts +1 -0
  12. package/dist/react/DialogueExample.js +86 -0
  13. package/dist/react/DialogueExample.js.map +1 -0
  14. package/dist/react/DialogueScene.d.ts +11 -0
  15. package/dist/react/DialogueScene.js +83 -0
  16. package/dist/react/DialogueScene.js.map +1 -0
  17. package/dist/react/DialogueView.d.ts +17 -0
  18. package/dist/react/DialogueView.js +156 -0
  19. package/dist/react/DialogueView.js.map +1 -0
  20. package/dist/react/TypingText.d.ts +12 -0
  21. package/dist/react/TypingText.js +102 -0
  22. package/dist/react/TypingText.js.map +1 -0
  23. package/dist/react/useYarnRunner.d.ts +8 -0
  24. package/dist/react/useYarnRunner.js +18 -0
  25. package/dist/react/useYarnRunner.js.map +1 -0
  26. package/dist/runtime/commands.js +1 -1
  27. package/dist/runtime/commands.js.map +1 -1
  28. package/dist/runtime/evaluator.js +0 -1
  29. package/dist/runtime/evaluator.js.map +1 -1
  30. package/dist/runtime/results.d.ts +4 -0
  31. package/dist/runtime/runner.js +3 -3
  32. package/dist/runtime/runner.js.map +1 -1
  33. package/dist/scene/parser.d.ts +9 -0
  34. package/dist/scene/parser.js +78 -0
  35. package/dist/scene/parser.js.map +1 -0
  36. package/dist/scene/types.d.ts +13 -0
  37. package/dist/scene/types.js +5 -0
  38. package/dist/scene/types.js.map +1 -0
  39. package/dist/tests/typing-text.test.d.ts +1 -0
  40. package/dist/tests/typing-text.test.js +12 -0
  41. package/dist/tests/typing-text.test.js.map +1 -0
  42. package/docs/typing-animation.md +44 -0
  43. package/eslint.config.cjs +6 -0
  44. package/examples/browser/index.html +1 -1
  45. package/examples/browser/main.tsx +2 -2
  46. package/package.json +4 -3
  47. package/scripts/run-tests.js +57 -0
  48. package/src/react/DialogueExample.tsx +14 -40
  49. package/src/react/DialogueScene.tsx +19 -3
  50. package/src/react/DialogueView.tsx +107 -6
  51. package/src/react/TypingText.tsx +132 -0
  52. package/src/react/css.d.ts +9 -0
  53. package/src/react/useYarnRunner.tsx +3 -1
  54. package/src/runtime/commands.ts +1 -4
  55. package/src/runtime/evaluator.ts +0 -1
@@ -1,14 +1,26 @@
1
- import React from "react";
1
+ import React, { useRef, useEffect, useState } from "react";
2
2
  import type { RuntimeResult } from "../runtime/results.js";
3
3
  import { DialogueScene } from "./DialogueScene.js";
4
4
  import type { SceneCollection } from "../scene/types.js";
5
- import "./dialogue.css";
5
+ import { TypingText } from "./TypingText.js";
6
+ // Note: CSS is imported in the browser demo entry point (examples/browser/main.tsx)
7
+ // This prevents Node.js from trying to resolve CSS imports during tests
6
8
 
7
9
  export interface DialogueViewProps {
8
10
  result: RuntimeResult | null;
9
11
  onAdvance: (optionIndex?: number) => void;
10
12
  className?: string;
11
13
  scenes?: SceneCollection;
14
+ // Typing animation options
15
+ enableTypingAnimation?: boolean;
16
+ typingSpeed?: number;
17
+ showTypingCursor?: boolean;
18
+ cursorCharacter?: string;
19
+ // Auto-advance after typing completes
20
+ autoAdvanceAfterTyping?: boolean;
21
+ autoAdvanceDelay?: number; // Delay in ms after typing completes before auto-advancing
22
+ // Pause before advance
23
+ pauseBeforeAdvance?: number; // Delay in ms before advancing when clicking (0 = no pause)
12
24
  }
13
25
 
14
26
  // Helper to parse CSS string into object
@@ -68,10 +80,57 @@ function parseCss(cssStr: string | undefined): React.CSSProperties {
68
80
  return styles;
69
81
  }
70
82
 
71
- export function DialogueView({ result, onAdvance, className, scenes }: DialogueViewProps) {
83
+ export function DialogueView({
84
+ result,
85
+ onAdvance,
86
+ className,
87
+ scenes,
88
+ enableTypingAnimation = false,
89
+ typingSpeed = 50, // Characters per second (50 cps = ~20ms per character)
90
+ showTypingCursor = true,
91
+ cursorCharacter = "|",
92
+ autoAdvanceAfterTyping = false,
93
+ autoAdvanceDelay = 500,
94
+ pauseBeforeAdvance = 0,
95
+ }: DialogueViewProps) {
72
96
  const sceneName = result?.type === "text" || result?.type === "options" ? result.scene : undefined;
73
97
  const speaker = result?.type === "text" ? result.speaker : undefined;
74
98
  const sceneCollection = scenes || { scenes: {} };
99
+ const [typingComplete, setTypingComplete] = useState(false);
100
+ const [currentTextKey, setCurrentTextKey] = useState(0);
101
+ const [skipTyping, setSkipTyping] = useState(false);
102
+ const advanceTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
103
+
104
+ // Reset typing completion when text changes
105
+ useEffect(() => {
106
+ if (result?.type === "text") {
107
+ setTypingComplete(false);
108
+ setSkipTyping(false);
109
+ setCurrentTextKey((prev) => prev + 1); // Force re-render of TypingText
110
+ }
111
+ // Cleanup any pending advance timeouts when text changes
112
+ return () => {
113
+ if (advanceTimeoutRef.current) {
114
+ clearTimeout(advanceTimeoutRef.current);
115
+ advanceTimeoutRef.current = null;
116
+ }
117
+ };
118
+ }, [result?.type === "text" ? result.text : null]);
119
+
120
+ // Handle auto-advance after typing completes
121
+ useEffect(() => {
122
+ if (
123
+ autoAdvanceAfterTyping &&
124
+ typingComplete &&
125
+ result?.type === "text" &&
126
+ !result.isDialogueEnd
127
+ ) {
128
+ const timer = setTimeout(() => {
129
+ onAdvance();
130
+ }, autoAdvanceDelay);
131
+ return () => clearTimeout(timer);
132
+ }
133
+ }, [autoAdvanceAfterTyping, typingComplete, result, onAdvance, autoAdvanceDelay]);
75
134
 
76
135
  if (!result) {
77
136
  return (
@@ -83,13 +142,43 @@ export function DialogueView({ result, onAdvance, className, scenes }: DialogueV
83
142
 
84
143
  if (result.type === "text") {
85
144
  const nodeStyles = parseCss(result.nodeCss);
145
+ const displayText = result.text || "\u00A0";
146
+ const shouldShowContinue = !result.isDialogueEnd && !enableTypingAnimation;
147
+
148
+ const handleClick = () => {
149
+ if (result.isDialogueEnd) return;
150
+
151
+ // If typing is in progress, skip it; otherwise advance
152
+ if (enableTypingAnimation && !typingComplete) {
153
+ // Skip typing animation
154
+ setSkipTyping(true);
155
+ setTypingComplete(true);
156
+ } else {
157
+ // Clear any pending timeout
158
+ if (advanceTimeoutRef.current) {
159
+ clearTimeout(advanceTimeoutRef.current);
160
+ advanceTimeoutRef.current = null;
161
+ }
162
+
163
+ // Apply pause before advance if configured
164
+ if (pauseBeforeAdvance > 0) {
165
+ advanceTimeoutRef.current = setTimeout(() => {
166
+ onAdvance();
167
+ advanceTimeoutRef.current = null;
168
+ }, pauseBeforeAdvance);
169
+ } else {
170
+ onAdvance();
171
+ }
172
+ }
173
+ };
174
+
86
175
  return (
87
176
  <div className="yd-container">
88
177
  <DialogueScene sceneName={sceneName} speaker={speaker} scenes={sceneCollection} />
89
178
  <div
90
179
  className={`yd-dialogue-box ${result.isDialogueEnd ? "yd-text-box-end" : ""} ${className || ""}`}
91
180
  style={nodeStyles} // Only apply dynamic node CSS
92
- onClick={() => !result.isDialogueEnd && onAdvance()}
181
+ onClick={handleClick}
93
182
  >
94
183
  <div className="yd-text-box">
95
184
  {result.speaker && (
@@ -98,9 +187,21 @@ export function DialogueView({ result, onAdvance, className, scenes }: DialogueV
98
187
  </div>
99
188
  )}
100
189
  <p className={`yd-text ${result.speaker ? "yd-text-with-speaker" : ""}`}>
101
- {result.text || "\u00A0"}
190
+ {enableTypingAnimation ? (
191
+ <TypingText
192
+ key={currentTextKey}
193
+ text={displayText}
194
+ typingSpeed={typingSpeed}
195
+ showCursor={showTypingCursor}
196
+ cursorCharacter={cursorCharacter}
197
+ disabled={skipTyping}
198
+ onComplete={() => setTypingComplete(true)}
199
+ />
200
+ ) : (
201
+ displayText
202
+ )}
102
203
  </p>
103
- {!result.isDialogueEnd && (
204
+ {shouldShowContinue && (
104
205
  <div className="yd-continue">
105
206
 
106
207
  </div>
@@ -0,0 +1,132 @@
1
+ import React, { useState, useEffect, useRef } from "react";
2
+
3
+ export interface TypingTextProps {
4
+ text: string;
5
+ typingSpeed?: number;
6
+ showCursor?: boolean;
7
+ cursorCharacter?: string;
8
+ cursorBlinkDuration?: number;
9
+ cursorClassName?: string;
10
+ className?: string;
11
+ onComplete?: () => void;
12
+ disabled?: boolean;
13
+ }
14
+
15
+ export function TypingText({
16
+ text,
17
+ typingSpeed = 100,
18
+ showCursor = true,
19
+ cursorCharacter = "|",
20
+ cursorBlinkDuration = 530,
21
+ cursorClassName = "",
22
+ className = "",
23
+ onComplete,
24
+ disabled = false,
25
+ }: TypingTextProps) {
26
+ const [displayedText, setDisplayedText] = useState("");
27
+ const [cursorVisible, setCursorVisible] = useState(true);
28
+ const cursorIntervalRef = useRef<ReturnType<typeof setInterval> | null>(null);
29
+ const typingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
30
+ const onCompleteRef = useRef(onComplete);
31
+
32
+ useEffect(() => {
33
+ onCompleteRef.current = onComplete;
34
+ }, [onComplete]);
35
+
36
+ // Handle cursor blinking
37
+ useEffect(() => {
38
+ if (!showCursor || disabled) {
39
+ return;
40
+ }
41
+ cursorIntervalRef.current = setInterval(() => {
42
+ setCursorVisible((prev) => !prev);
43
+ }, cursorBlinkDuration);
44
+ return () => {
45
+ if (cursorIntervalRef.current) {
46
+ clearInterval(cursorIntervalRef.current);
47
+ }
48
+ };
49
+ }, [showCursor, cursorBlinkDuration, disabled]);
50
+
51
+ // Handle typing animation
52
+ useEffect(() => {
53
+ if (disabled) {
54
+ // If disabled, show full text immediately
55
+ setDisplayedText(text);
56
+ if (onCompleteRef.current && text.length > 0) {
57
+ onCompleteRef.current();
58
+ }
59
+ return;
60
+ }
61
+
62
+ // Reset when text changes
63
+ setDisplayedText("");
64
+
65
+ if (text.length === 0) {
66
+ if (onCompleteRef.current) {
67
+ onCompleteRef.current();
68
+ }
69
+ return;
70
+ }
71
+
72
+ let index = 0;
73
+ const typeNextCharacter = () => {
74
+ if (index < text.length) {
75
+ index++;
76
+ setDisplayedText(text.slice(0, index));
77
+
78
+
79
+ // If speed is 0 or very small, type next character immediately (use requestAnimationFrame for smoother animation)
80
+ if (typingSpeed <= 0) {
81
+ // Use requestAnimationFrame for instant/smooth rendering
82
+ requestAnimationFrame(() => {
83
+ typeNextCharacter();
84
+ });
85
+ } else {
86
+ typingTimeoutRef.current = setTimeout(() => {
87
+ typeNextCharacter();
88
+ }, typingSpeed);
89
+ }
90
+ } else {
91
+ if (onCompleteRef.current) {
92
+ onCompleteRef.current();
93
+ }
94
+ }
95
+ };
96
+
97
+ // Start typing
98
+ if (typingSpeed <= 0) {
99
+ // Start immediately if speed is 0
100
+ requestAnimationFrame(() => {
101
+ typeNextCharacter();
102
+ });
103
+ } else {
104
+ typingTimeoutRef.current = setTimeout(() => {
105
+ typeNextCharacter();
106
+ }, typingSpeed);
107
+ }
108
+
109
+ return () => {
110
+ if (typingTimeoutRef.current) {
111
+ clearTimeout(typingTimeoutRef.current);
112
+ }
113
+ };
114
+ }, [text, disabled]);
115
+
116
+ return (
117
+ <span className={className}>
118
+ <span>{displayedText}</span>
119
+ {showCursor && !disabled && (
120
+ <span
121
+ className={`yd-typing-cursor ${cursorClassName}`}
122
+ style={{
123
+ opacity: cursorVisible ? 1 : 0,
124
+ transition: `opacity ${cursorBlinkDuration / 2}ms ease-in-out`,
125
+ }}
126
+ >
127
+ {cursorCharacter}
128
+ </span>
129
+ )}
130
+ </span>
131
+ );
132
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Type declarations for CSS module imports
3
+ * This allows TypeScript to recognize CSS imports without errors
4
+ */
5
+ declare module "*.css" {
6
+ const content: string;
7
+ export default content;
8
+ }
9
+
@@ -1,5 +1,7 @@
1
1
  import { useState, useCallback, useRef } from "react";
2
- import { YarnRunner, type IRProgram, type RunnerOptions, type RuntimeResult } from "../runtime/runner.js";
2
+ import { YarnRunner, type RunnerOptions } from "../runtime/runner.js";
3
+ import type { IRProgram } from "../compile/ir.js";
4
+ import type { RuntimeResult } from "../runtime/results.js";
3
5
 
4
6
  export function useYarnRunner(
5
7
  program: IRProgram,
@@ -162,7 +162,7 @@ export class CommandHandler {
162
162
  } else if (value.startsWith(".")) {
163
163
  // Shorthand - we can't infer enum type from declaration alone
164
164
  // Store as-is, will be resolved on first use if variable has enum type
165
- value = value;
165
+ // Value is already set correctly above
166
166
  }
167
167
  }
168
168
 
@@ -178,6 +178,3 @@ export class CommandHandler {
178
178
  }
179
179
  }
180
180
 
181
- // Forward reference type (avoid circular import)
182
- type ExpressionEvaluator = import("./evaluator").ExpressionEvaluator;
183
-
@@ -202,7 +202,6 @@ export class ExpressionEvaluator {
202
202
 
203
203
  // Try shorthand enum: .CaseName (requires context from variables)
204
204
  if (expr.startsWith(".") && expr.length > 1) {
205
- const caseName = expr.slice(1);
206
205
  // Try to infer enum from variable types - for now, return as-is and let validation handle it
207
206
  return expr;
208
207
  }