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.
- package/.github/workflows/npm-publish-github-packages.yml +39 -0
- package/README.md +97 -89
- package/dist/compile/compiler.js +5 -3
- package/dist/compile/compiler.js.map +1 -1
- package/dist/compile/ir.d.ts +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/parse/parser.js +5 -2
- package/dist/parse/parser.js.map +1 -1
- package/dist/react/DialogueExample.d.ts +1 -0
- package/dist/react/DialogueExample.js +86 -0
- package/dist/react/DialogueExample.js.map +1 -0
- package/dist/react/DialogueScene.d.ts +11 -0
- package/dist/react/DialogueScene.js +83 -0
- package/dist/react/DialogueScene.js.map +1 -0
- package/dist/react/DialogueView.d.ts +17 -0
- package/dist/react/DialogueView.js +156 -0
- package/dist/react/DialogueView.js.map +1 -0
- package/dist/react/TypingText.d.ts +12 -0
- package/dist/react/TypingText.js +102 -0
- package/dist/react/TypingText.js.map +1 -0
- package/dist/react/useYarnRunner.d.ts +8 -0
- package/dist/react/useYarnRunner.js +18 -0
- package/dist/react/useYarnRunner.js.map +1 -0
- package/dist/runtime/commands.js +1 -1
- package/dist/runtime/commands.js.map +1 -1
- package/dist/runtime/evaluator.js +0 -1
- package/dist/runtime/evaluator.js.map +1 -1
- package/dist/runtime/results.d.ts +4 -0
- package/dist/runtime/runner.js +3 -3
- package/dist/runtime/runner.js.map +1 -1
- package/dist/scene/parser.d.ts +9 -0
- package/dist/scene/parser.js +78 -0
- package/dist/scene/parser.js.map +1 -0
- package/dist/scene/types.d.ts +13 -0
- package/dist/scene/types.js +5 -0
- package/dist/scene/types.js.map +1 -0
- package/dist/tests/typing-text.test.d.ts +1 -0
- package/dist/tests/typing-text.test.js +12 -0
- package/dist/tests/typing-text.test.js.map +1 -0
- package/docs/typing-animation.md +44 -0
- package/eslint.config.cjs +6 -0
- package/examples/browser/index.html +1 -1
- package/examples/browser/main.tsx +2 -2
- package/package.json +4 -3
- package/scripts/run-tests.js +57 -0
- package/src/react/DialogueExample.tsx +14 -40
- package/src/react/DialogueScene.tsx +19 -3
- package/src/react/DialogueView.tsx +107 -6
- package/src/react/TypingText.tsx +132 -0
- package/src/react/css.d.ts +9 -0
- package/src/react/useYarnRunner.tsx +3 -1
- package/src/runtime/commands.ts +1 -4
- 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 "./
|
|
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({
|
|
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={
|
|
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
|
-
{
|
|
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
|
-
{
|
|
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
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { useState, useCallback, useRef } from "react";
|
|
2
|
-
import { YarnRunner, type
|
|
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,
|
package/src/runtime/commands.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
package/src/runtime/evaluator.ts
CHANGED
|
@@ -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
|
}
|