Skip to content

Commit 9c8df00

Browse files
ghirparaclaude
andcommitted
Add missing source files to git tracking
lib/renpyValidator.ts, lib/renpyValidator.test.ts, and components/DialoguePreview.tsx were present locally but never committed, causing the CI build to fail when resolving imports from EditorView.tsx. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 2764715 commit 9c8df00

File tree

3 files changed

+1245
-0
lines changed

3 files changed

+1245
-0
lines changed

components/DialoguePreview.tsx

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
/**
2+
* @file DialoguePreview.tsx
3+
* @description Inline "Player View" panel for the code editor.
4+
* Shows a mock Ren'Py textbox for dialogue lines or a choice screen for menu blocks,
5+
* updating in real-time as the cursor moves through the file.
6+
*/
7+
8+
import React from 'react';
9+
10+
// --- Types ---
11+
12+
interface DialogueData {
13+
kind: 'dialogue';
14+
charName: string | null;
15+
charColor: string | null;
16+
text: string;
17+
whoPrefix?: string;
18+
whoSuffix?: string;
19+
whatPrefix?: string;
20+
whatSuffix?: string;
21+
}
22+
23+
export interface MenuChoice {
24+
text: string;
25+
condition?: string;
26+
destination?: string;
27+
}
28+
29+
interface MenuData {
30+
kind: 'menu';
31+
prompt?: string;
32+
choices: MenuChoice[];
33+
}
34+
35+
export type DialoguePreviewData = DialogueData | MenuData;
36+
37+
interface DialoguePreviewProps {
38+
data: DialoguePreviewData | null;
39+
isExpanded: boolean;
40+
onToggleExpand: () => void;
41+
}
42+
43+
// --- Text tag renderer ---
44+
45+
/** Parse Ren'Py text tags and variable interpolations into styled React nodes. */
46+
function renderRenpyText(text: string): React.ReactNode {
47+
const segments: React.ReactNode[] = [];
48+
const tagRe = /\{([^}]*)\}|\[([^\]]*)\]/g;
49+
50+
let pos = 0;
51+
let key = 0;
52+
let bold = false;
53+
let italic = false;
54+
let underline = false;
55+
let strike = false;
56+
let color: string | undefined;
57+
58+
const push = (str: string) => {
59+
if (!str) return;
60+
const style: React.CSSProperties = {};
61+
if (bold) style.fontWeight = 'bold';
62+
if (italic) style.fontStyle = 'italic';
63+
if (underline && strike) style.textDecoration = 'underline line-through';
64+
else if (underline) style.textDecoration = 'underline';
65+
else if (strike) style.textDecoration = 'line-through';
66+
if (color) style.color = color;
67+
68+
if (Object.keys(style).length > 0) {
69+
segments.push(<span key={key++} style={style}>{str}</span>);
70+
} else {
71+
segments.push(<React.Fragment key={key++}>{str}</React.Fragment>);
72+
}
73+
};
74+
75+
let match: RegExpExecArray | null;
76+
while ((match = tagRe.exec(text)) !== null) {
77+
push(text.slice(pos, match.index));
78+
pos = match.index + match[0].length;
79+
80+
if (match[2] !== undefined) {
81+
segments.push(
82+
<span key={key++} className="opacity-50 italic">[{match[2]}]</span>
83+
);
84+
} else {
85+
const tag = match[1];
86+
if (tag === 'b') bold = true;
87+
else if (tag === '/b') bold = false;
88+
else if (tag === 'i') italic = true;
89+
else if (tag === '/i') italic = false;
90+
else if (tag === 'u') underline = true;
91+
else if (tag === '/u') underline = false;
92+
else if (tag === 's') strike = true;
93+
else if (tag === '/s') strike = false;
94+
else if (tag === '/color') color = undefined;
95+
else if (tag === '/size') { /* no-op */ }
96+
else {
97+
const colorMatch = tag.match(/^color=(#[0-9a-fA-F]{3,8}|[a-z]+)$/i);
98+
if (colorMatch) color = colorMatch[1];
99+
}
100+
}
101+
}
102+
103+
push(text.slice(pos));
104+
return <>{segments}</>;
105+
}
106+
107+
// --- Sub-renderers ---
108+
109+
const DialogueBox: React.FC<{ data: DialogueData }> = ({ data }) => (
110+
<div
111+
className="rounded"
112+
style={{
113+
background: 'rgba(0,0,0,0.82)',
114+
border: '1px solid rgba(255,255,255,0.12)',
115+
padding: '10px 14px',
116+
}}
117+
>
118+
{data.charName && (
119+
<div className="mb-1.5">
120+
<span
121+
className="inline-block text-xs font-bold px-2 py-0.5 rounded"
122+
style={{
123+
background: data.charColor ? `${data.charColor}33` : 'rgba(255,255,255,0.12)',
124+
color: data.charColor ?? '#ffffff',
125+
border: `1px solid ${data.charColor ?? 'rgba(255,255,255,0.25)'}55`,
126+
}}
127+
>
128+
{data.whoPrefix}{data.charName}{data.whoSuffix}
129+
</span>
130+
</div>
131+
)}
132+
<p className="text-sm leading-relaxed" style={{ color: '#f0f0f0', fontFamily: 'serif' }}>
133+
{data.whatPrefix && <span className="opacity-60">{data.whatPrefix}</span>}
134+
{renderRenpyText(data.text)}
135+
{data.whatSuffix && <span className="opacity-60">{data.whatSuffix}</span>}
136+
</p>
137+
</div>
138+
);
139+
140+
const ChoiceScreen: React.FC<{ data: MenuData }> = ({ data }) => (
141+
<div
142+
className="rounded"
143+
style={{
144+
background: 'rgba(0,0,0,0.82)',
145+
border: '1px solid rgba(255,255,255,0.12)',
146+
padding: '10px 14px',
147+
}}
148+
>
149+
{data.prompt && (
150+
<p className="text-sm mb-2" style={{ color: '#d0d0d0', fontFamily: 'serif' }}>
151+
{renderRenpyText(data.prompt)}
152+
</p>
153+
)}
154+
<div className="space-y-1">
155+
{data.choices.map((choice, i) => (
156+
<div key={i} className="flex items-center gap-2">
157+
<div
158+
className="flex-1 px-3 py-1 rounded text-sm"
159+
style={{
160+
background: 'rgba(255,255,255,0.08)',
161+
color: '#f0f0f0',
162+
border: '1px solid rgba(255,255,255,0.18)',
163+
}}
164+
>
165+
{renderRenpyText(choice.text)}
166+
{choice.condition && (
167+
<span className="ml-2 font-mono opacity-40" style={{ fontSize: '10px' }}>
168+
if {choice.condition}
169+
</span>
170+
)}
171+
</div>
172+
{choice.destination && (
173+
<span className="text-xs opacity-50 whitespace-nowrap" style={{ color: '#818cf8' }}>
174+
{choice.destination}
175+
</span>
176+
)}
177+
</div>
178+
))}
179+
</div>
180+
</div>
181+
);
182+
183+
// --- Main component ---
184+
185+
const DialoguePreview: React.FC<DialoguePreviewProps> = ({ data, isExpanded, onToggleExpand }) => {
186+
const label = data?.kind === 'menu' ? 'Choice Preview' : 'Dialogue Preview';
187+
188+
return (
189+
<div className="flex-none border-t border-gray-700 bg-gray-900 select-none">
190+
<button
191+
onClick={onToggleExpand}
192+
className="w-full flex items-center justify-between px-3 py-1 text-xs text-gray-400 hover:text-gray-200 hover:bg-gray-800 transition-colors"
193+
aria-label={isExpanded ? 'Collapse player view' : 'Expand player view'}
194+
>
195+
<span className="font-medium tracking-wide uppercase" style={{ fontSize: '10px', letterSpacing: '0.08em' }}>
196+
{label}
197+
</span>
198+
<svg
199+
xmlns="http://www.w3.org/2000/svg"
200+
className={`h-3.5 w-3.5 transition-transform ${isExpanded ? '' : 'rotate-180'}`}
201+
viewBox="0 0 20 20"
202+
fill="currentColor"
203+
>
204+
<path fillRule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clipRule="evenodd" />
205+
</svg>
206+
</button>
207+
208+
{isExpanded && (
209+
<div className="px-3 pb-3">
210+
{data ? (
211+
data.kind === 'dialogue'
212+
? <DialogueBox data={data} />
213+
: <ChoiceScreen data={data} />
214+
) : (
215+
<p className="text-xs text-gray-600 italic py-1">No dialogue or menu at cursor</p>
216+
)}
217+
</div>
218+
)}
219+
</div>
220+
);
221+
};
222+
223+
export default DialoguePreview;

0 commit comments

Comments
 (0)