1
0
Fork 0
tsgames/src/games/storywriter/components/editors/lore.tsx

204 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { ContentEditable } from "@common/components/ContentEditable";
import { useState } from "preact/hooks";
import styles from '../../assets/lore-editor.module.css';
import { useAppState, type LoreEntry } from "../../contexts/state";
export const LoreEditor = ({ visible }: { visible: boolean }) => {
const { currentWorld, currentStory, dispatch } = useAppState();
const [editingId, setEditingId] = useState<string | null>(null);
const [newTitle, setNewTitle] = useState('');
const [showDeleteConfirm, setShowDeleteConfirm] = useState<string | null>(null);
if (!currentWorld || !visible) {
return null;
}
// When a story is selected, edit story-level lore; otherwise world-level lore
const storyId = currentStory?.id ?? null;
const worldId = currentWorld.id;
const lore = currentStory ? currentStory.lore : currentWorld.lore;
const handleAddEntry = () => {
if (!newTitle.trim()) return;
dispatch({
type: 'ADD_LORE_ENTRY',
worldId,
storyId,
entry: {
id: crypto.randomUUID(),
title: newTitle.trim(),
text: '',
},
});
setNewTitle('');
setEditingId(null);
};
const handleEditEntry = (entryId: string, field: keyof LoreEntry, value: string) => {
dispatch({
type: 'EDIT_LORE_ENTRY',
worldId,
storyId,
entryId,
updates: { [field]: value },
});
};
const handleDeleteEntry = (entryId: string) => {
dispatch({
type: 'DELETE_LORE_ENTRY',
worldId,
storyId,
entryId,
});
};
const handleMoveUp = (index: number) => {
if (index === 0) return;
const entryIds = lore.map(e => e.id);
[entryIds[index - 1], entryIds[index]] = [entryIds[index], entryIds[index - 1]];
dispatch({
type: 'REORDER_LORE_ENTRIES',
worldId,
storyId,
entryIds,
});
};
const handleMoveDown = (index: number) => {
if (index === lore.length - 1) return;
const entryIds = lore.map(e => e.id);
[entryIds[index], entryIds[index + 1]] = [entryIds[index + 1], entryIds[index]];
dispatch({
type: 'REORDER_LORE_ENTRIES',
worldId,
storyId,
entryIds,
});
};
return (
<div class={styles.loreEditor}>
<div class={styles.header}>
<h2>{currentStory ? 'Story Lore' : 'World Lore'}</h2>
<div class={styles.addEntry}>
<input
type="text"
class={styles.titleInput}
value={newTitle}
onInput={(e) => setNewTitle(e.currentTarget.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
handleAddEntry();
}
}}
placeholder="New lore entry title..."
/>
<button class={styles.addButton} onClick={handleAddEntry}>
+ Add Entry
</button>
</div>
</div>
<div class={styles.list}>
{lore.length === 0 && (
<p class={styles.empty}>No lore entries yet. Add your first entry!</p>
)}
{lore.map((entry, index) => (
<div key={entry.id} class={styles.entryCard}>
<div class={styles.cardHeader}>
<div class={styles.titleRow}>
{editingId === entry.id ? (
<input
type="text"
class={styles.titleEditInput}
value={entry.title}
onInput={(e) => handleEditEntry(entry.id, 'title', e.currentTarget.value)}
onBlur={() => setEditingId(null)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
e.preventDefault();
setEditingId(null);
} else if (e.key === 'Escape') {
setEditingId(null);
}
}}
autoFocus
/>
) : (
<h3
class={styles.entryTitle}
onClick={() => setEditingId(entry.id)}
title="Click to edit"
>
{entry.title}
</h3>
)}
</div>
<div class={styles.actions}>
<button
class={styles.moveButton}
onClick={() => handleMoveUp(index)}
disabled={index === 0}
title="Move up"
>
</button>
<button
class={styles.moveButton}
onClick={() => handleMoveDown(index)}
disabled={index === lore.length - 1}
title="Move down"
>
</button>
{showDeleteConfirm === entry.id ? (
<div class={styles.deleteConfirm}>
<span>Delete?</span>
<button
class={styles.confirmButton}
onClick={() => {
handleDeleteEntry(entry.id);
setShowDeleteConfirm(null);
}}
>
Yes
</button>
<button
class={styles.cancelButton}
onClick={() => setShowDeleteConfirm(null)}
>
No
</button>
</div>
) : (
<button
class={styles.deleteButton}
onClick={() => setShowDeleteConfirm(entry.id)}
title="Delete"
>
×
</button>
)}
</div>
</div>
<div class={styles.content}>
<ContentEditable
autoLines
class={styles.textarea}
value={entry.text}
onInput={(e) => handleEditEntry(entry.id, 'text', e.currentTarget.textContent || '')}
placeholder="Enter lore content..."
/>
</div>
</div>
))}
</div>
</div>
);
};