Skip to content

Commit b2c9390

Browse files
committed
Migrate editor components to TypeScript
Convert VariableEditor, VariablesScreen, CharacterManager, and CharactersScreen from JSX to TypeScript with full type safety. Add proper prop interfaces for each component, replace event handlers with typed React event types, and remove redundant type aliases from App.tsx. Export CharacterReferenceScanNode interface from storyReferences.ts for reuse.
1 parent fd3b954 commit b2c9390

8 files changed

Lines changed: 546 additions & 456 deletions

File tree

src/App.tsx

Lines changed: 3 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -44,12 +44,7 @@ import {
4444
type PrepareStoryPlayImportResult,
4545
} from "./utils/importStoryPlayProject";
4646
import type { MiniGameEditorDraft } from "./hooks/useMiniGameEditorState";
47-
import type {
48-
StoryCharacter,
49-
StoryNode,
50-
StoryVariables,
51-
VariableMetaMap,
52-
} from "./types/story";
47+
import type { StoryVariables } from "./types/story";
5348

5449
declare global {
5550
interface Window {
@@ -81,39 +76,9 @@ type SidebarEditorAppProps = UseStoryStateResult & {
8176
onOpenVariables?: () => void;
8277
};
8378

84-
type VariablesScreenAppProps = {
85-
variables: StoryVariables;
86-
setVariables: UseStoryStateResult["setVariables"];
87-
variableMeta: VariableMetaMap;
88-
setVariableMeta: UseStoryStateResult["setVariableMeta"];
89-
onBack: () => void;
90-
activeTemplateLabel: string;
91-
onOpenTemplates: () => void;
92-
onExport: () => void;
93-
onImport: () => void;
94-
onOpenMiniGameEditor: () => void;
95-
canOpenMiniGameEditor: boolean;
96-
miniGameEditorTitle: string;
97-
};
98-
99-
type CharactersScreenAppProps = {
100-
characters: StoryCharacter[];
101-
nodes: StoryNode[];
102-
onBack: () => void;
103-
onAddCharacter: UseStoryStateResult["addCharacter"];
104-
onUpdateCharacter: UseStoryStateResult["updateCharacter"];
105-
onDeleteCharacter: UseStoryStateResult["deleteCharacter"];
106-
onOpenTemplates: () => void;
107-
activeTemplateLabel: string;
108-
};
109-
11079
const StoryCanvasView = StoryCanvas as unknown as ComponentType<StoryCanvasAppProps>;
11180
const SidebarEditorView =
11281
SidebarEditor as unknown as ComponentType<SidebarEditorAppProps>;
113-
const VariablesScreenView =
114-
VariablesScreen as unknown as ComponentType<VariablesScreenAppProps>;
115-
const CharactersScreenView =
116-
CharactersScreen as unknown as ComponentType<CharactersScreenAppProps>;
11782

11883
function EditorApp() {
11984
const story = useStoryState();
@@ -452,7 +417,7 @@ function EditorApp() {
452417
/>
453418

454419
{activeScreen === "variables" ? (
455-
<VariablesScreenView
420+
<VariablesScreen
456421
variables={story.variables}
457422
setVariables={story.setVariables}
458423
variableMeta={story.variableMeta}
@@ -467,7 +432,7 @@ function EditorApp() {
467432
miniGameEditorTitle={miniGameEditorTitle}
468433
/>
469434
) : activeScreen === "characters" ? (
470-
<CharactersScreenView
435+
<CharactersScreen
471436
characters={story.characters}
472437
nodes={story.nodes}
473438
onBack={handleCloseCharactersWorkspace}
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,142 @@
1-
import { useEffect, useMemo, useRef, useState } from "react";
2-
3-
export default function NodeSearchBar({ nodes, onJumpToNode }) {
4-
const [query, setQuery] = useState("");
5-
const inputRef = useRef(null);
6-
7-
const results = useMemo(() => {
8-
const trimmed = query.trim().toLowerCase();
9-
if (!trimmed) return [];
10-
11-
return nodes
12-
.filter((node) => {
13-
const title = node.data?.title || "";
14-
return title.toLowerCase().includes(trimmed);
15-
})
16-
.slice(0, 8);
17-
}, [nodes, query]);
18-
19-
useEffect(() => {
20-
function handleGlobalKeyDown(event) {
21-
const activeTag = document.activeElement?.tagName || "";
22-
const isTypingInInput =
23-
activeTag === "INPUT" ||
24-
activeTag === "TEXTAREA" ||
25-
document.activeElement?.isContentEditable;
26-
27-
if (event.key === "/" && !isTypingInInput) {
28-
event.preventDefault();
29-
inputRef.current?.focus();
30-
}
31-
32-
if (event.key === "Escape" && document.activeElement === inputRef.current) {
33-
setQuery("");
34-
inputRef.current?.blur();
35-
}
36-
}
37-
38-
window.addEventListener("keydown", handleGlobalKeyDown);
39-
return () => window.removeEventListener("keydown", handleGlobalKeyDown);
40-
}, []);
41-
42-
function handleSelect(nodeId) {
43-
onJumpToNode(nodeId);
44-
setQuery("");
45-
inputRef.current?.blur();
46-
}
47-
48-
function handleInputKeyDown(event) {
49-
if (event.key === "Enter" && results.length) {
50-
event.preventDefault();
51-
handleSelect(results[0].id);
52-
}
53-
54-
if (event.key === "Escape") {
55-
setQuery("");
56-
inputRef.current?.blur();
57-
}
58-
}
59-
60-
function highlightMatch(text) {
61-
const safeText = text || "Untitled Block";
62-
const trimmed = query.trim();
63-
64-
if (!trimmed) return safeText;
65-
66-
const lowerText = safeText.toLowerCase();
67-
const lowerQuery = trimmed.toLowerCase();
68-
const matchIndex = lowerText.indexOf(lowerQuery);
69-
70-
if (matchIndex === -1) return safeText;
71-
72-
const before = safeText.slice(0, matchIndex);
73-
const match = safeText.slice(matchIndex, matchIndex + trimmed.length);
74-
const after = safeText.slice(matchIndex + trimmed.length);
75-
76-
return (
77-
<>
78-
{before}
79-
<mark>{match}</mark>
80-
{after}
81-
</>
82-
);
83-
}
84-
85-
return (
86-
<div className="node-search">
87-
<input
88-
ref={inputRef}
89-
className="node-search-input"
90-
type="text"
91-
placeholder="Search nodes..."
92-
value={query}
93-
onChange={(e) => setQuery(e.target.value)}
94-
onKeyDown={handleInputKeyDown}
95-
/>
96-
97-
{query.trim() && (
98-
<div className="node-search-results">
99-
{results.length === 0 ? (
100-
<div className="node-search-empty">No matching nodes</div>
101-
) : (
102-
results.map((node) => (
103-
<button
104-
key={node.id}
105-
className="node-search-result"
106-
onClick={() => handleSelect(node.id)}
107-
>
108-
<span className="node-search-title">
109-
{highlightMatch(node.data?.title || "Untitled Block")}
110-
</span>
111-
<span className="node-search-type">
112-
{node.data?.blockType || "narrative"}
113-
</span>
114-
</button>
115-
))
116-
)}
117-
</div>
118-
)}
119-
</div>
120-
);
121-
}
1+
import {
2+
useEffect,
3+
useMemo,
4+
useRef,
5+
useState,
6+
type ChangeEvent,
7+
type KeyboardEvent,
8+
type ReactNode,
9+
} from "react";
10+
import type { StoryNode } from "../../types/story";
11+
12+
/**
13+
* Canvas node search control.
14+
*/
15+
export interface NodeSearchBarProps {
16+
nodes: readonly StoryNode[];
17+
onJumpToNode: (nodeId: string) => void;
18+
}
19+
20+
export default function NodeSearchBar({
21+
nodes,
22+
onJumpToNode,
23+
}: NodeSearchBarProps) {
24+
const [query, setQuery] = useState("");
25+
const inputRef = useRef<HTMLInputElement | null>(null);
26+
27+
const results = useMemo(() => {
28+
const trimmed = query.trim().toLowerCase();
29+
if (!trimmed) return [];
30+
31+
return nodes
32+
.filter((node) => {
33+
const title = node.data?.title || "";
34+
return title.toLowerCase().includes(trimmed);
35+
})
36+
.slice(0, 8);
37+
}, [nodes, query]);
38+
39+
useEffect(() => {
40+
function handleGlobalKeyDown(event: globalThis.KeyboardEvent) {
41+
const active = document.activeElement as HTMLElement | null;
42+
const activeTag = active?.tagName || "";
43+
const isTypingInInput =
44+
activeTag === "INPUT" ||
45+
activeTag === "TEXTAREA" ||
46+
active?.isContentEditable;
47+
48+
if (event.key === "/" && !isTypingInInput) {
49+
event.preventDefault();
50+
inputRef.current?.focus();
51+
}
52+
53+
if (event.key === "Escape" && active === inputRef.current) {
54+
setQuery("");
55+
inputRef.current?.blur();
56+
}
57+
}
58+
59+
window.addEventListener("keydown", handleGlobalKeyDown);
60+
return () => window.removeEventListener("keydown", handleGlobalKeyDown);
61+
}, []);
62+
63+
function handleSelect(nodeId: string) {
64+
onJumpToNode(nodeId);
65+
setQuery("");
66+
inputRef.current?.blur();
67+
}
68+
69+
function handleInputKeyDown(event: KeyboardEvent<HTMLInputElement>) {
70+
if (event.key === "Enter" && results.length) {
71+
event.preventDefault();
72+
handleSelect(results[0].id);
73+
}
74+
75+
if (event.key === "Escape") {
76+
setQuery("");
77+
inputRef.current?.blur();
78+
}
79+
}
80+
81+
function highlightMatch(text: string | undefined): ReactNode {
82+
const safeText = text || "Untitled Block";
83+
const trimmed = query.trim();
84+
85+
if (!trimmed) return safeText;
86+
87+
const lowerText = safeText.toLowerCase();
88+
const lowerQuery = trimmed.toLowerCase();
89+
const matchIndex = lowerText.indexOf(lowerQuery);
90+
91+
if (matchIndex === -1) return safeText;
92+
93+
const before = safeText.slice(0, matchIndex);
94+
const match = safeText.slice(matchIndex, matchIndex + trimmed.length);
95+
const after = safeText.slice(matchIndex + trimmed.length);
96+
97+
return (
98+
<>
99+
{before}
100+
<mark>{match}</mark>
101+
{after}
102+
</>
103+
);
104+
}
105+
106+
return (
107+
<div className="node-search">
108+
<input
109+
ref={inputRef}
110+
className="node-search-input"
111+
type="text"
112+
placeholder="Search nodes..."
113+
value={query}
114+
onChange={(e: ChangeEvent<HTMLInputElement>) => setQuery(e.target.value)}
115+
onKeyDown={handleInputKeyDown}
116+
/>
117+
118+
{query.trim() && (
119+
<div className="node-search-results">
120+
{results.length === 0 ? (
121+
<div className="node-search-empty">No matching nodes</div>
122+
) : (
123+
results.map((node) => (
124+
<button
125+
key={node.id}
126+
className="node-search-result"
127+
onClick={() => handleSelect(node.id)}
128+
>
129+
<span className="node-search-title">
130+
{highlightMatch(node.data?.title || "Untitled Block")}
131+
</span>
132+
<span className="node-search-type">
133+
{node.data?.blockType || "narrative"}
134+
</span>
135+
</button>
136+
))
137+
)}
138+
</div>
139+
)}
140+
</div>
141+
);
142+
}

src/components/canvas/StoryCanvas.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ export default function StoryCanvas({
238238
<div className="canvas-searchbar-wrap">
239239
<NodeSearchBar
240240
nodes={nodes}
241-
onSelectNode={(nodeId) => setSelectedNodeId?.(nodeId)}
241+
onJumpToNode={(nodeId) => setSelectedNodeId?.(nodeId)}
242242
/>
243243
</div>
244244

0 commit comments

Comments
 (0)