feat(tokens): app-local non-player tokens with session moves and UI polish
Add token library/placements, keep play-time moves for the session, lock presentation interactions, and fix export/import modal layout plus freeform trap label. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
||||
type StorylineSelection,
|
||||
} from '../../shared/graph/storylineExportImport';
|
||||
import type { Project, ProjectId, SceneId } from '../../shared/types';
|
||||
import { Button } from '../shared/ui/controls';
|
||||
import { Button, Select } from '../shared/ui/controls';
|
||||
|
||||
import styles from './EditorApp.module.css';
|
||||
import { useEditorI18n } from './i18n/EditorI18nContext';
|
||||
@@ -135,18 +135,16 @@ export function ExportProjectModal({
|
||||
|
||||
<div className={styles.fieldGrid}>
|
||||
<div className={styles.fieldLabel}>{t('export.project')}</div>
|
||||
<select
|
||||
className={styles.selectInput}
|
||||
<Select
|
||||
value={projectId ?? ''}
|
||||
onChange={(e) => setProjectId((e.target.value as ProjectId) || null)}
|
||||
onChange={(next) => setProjectId((next as ProjectId) || null)}
|
||||
disabled={projects.length === 0}
|
||||
>
|
||||
{projects.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name} ({p.fileName})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
ariaLabel={t('export.project')}
|
||||
options={projects.map((p) => ({
|
||||
value: p.id,
|
||||
label: `${p.name} (${p.fileName})`,
|
||||
}))}
|
||||
/>
|
||||
|
||||
<div className={styles.fieldLabel}>{t('storyline.section')}</div>
|
||||
{loadingStorylines ? (
|
||||
@@ -309,43 +307,46 @@ export function ImportSourceModal({
|
||||
|
||||
<div className={styles.fieldGrid}>
|
||||
{canImportFromProject ? (
|
||||
<>
|
||||
<div className={styles.fieldGroup}>
|
||||
<div className={styles.fieldLabel}>{t('importSource.type')}</div>
|
||||
<select
|
||||
className={styles.selectInput}
|
||||
<Select
|
||||
value={importKind}
|
||||
onChange={(e) => setImportKind(e.target.value as 'project' | 'file')}
|
||||
onChange={(next) => setImportKind(next as 'project' | 'file')}
|
||||
disabled={submitting}
|
||||
>
|
||||
<option value="project">{t('importSource.fromProject')}</option>
|
||||
<option value="file">{t('importSource.fromFile')}</option>
|
||||
</select>
|
||||
</>
|
||||
ariaLabel={t('importSource.type')}
|
||||
options={[
|
||||
{ value: 'project', label: t('importSource.fromProject') },
|
||||
{ value: 'file', label: t('importSource.fromFile') },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.muted}>{t('importSource.fileOnlyHint')}</div>
|
||||
)}
|
||||
|
||||
{importKind === 'project' && canImportFromProject ? (
|
||||
<>
|
||||
<div className={[styles.fieldGroup, canImportFromProject ? styles.fieldGroupSpaced : ''].filter(Boolean).join(' ')}>
|
||||
<div className={styles.fieldLabel}>{t('importSource.project')}</div>
|
||||
<select
|
||||
className={styles.selectInput}
|
||||
<Select
|
||||
value={sourceProjectId ?? ''}
|
||||
onChange={(e) => setSourceProjectId((e.target.value as ProjectId) || null)}
|
||||
onChange={(next) => setSourceProjectId((next as ProjectId) || null)}
|
||||
disabled={availableProjects.length === 0 || submitting}
|
||||
>
|
||||
{availableProjects.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name} ({p.fileName})
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
ariaLabel={t('importSource.project')}
|
||||
options={availableProjects.map((p) => ({
|
||||
value: p.id,
|
||||
label: `${p.name} (${p.fileName})`,
|
||||
}))}
|
||||
/>
|
||||
{availableProjects.length === 0 ? (
|
||||
<div className={styles.muted}>{t('importSource.noOtherProjects')}</div>
|
||||
) : null}
|
||||
</>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div
|
||||
className={[styles.fieldGroup, canImportFromProject ? styles.fieldGroupSpaced : '']
|
||||
.filter(Boolean)
|
||||
.join(' ')}
|
||||
>
|
||||
<div className={styles.fieldLabel}>{t('importSource.file')}</div>
|
||||
<div className={styles.importFileRow}>
|
||||
<Button
|
||||
@@ -366,7 +367,7 @@ export function ImportSourceModal({
|
||||
{pickedFile ? pickedFile.name : t('importSource.noFileSelected')}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -572,24 +573,23 @@ export function SceneConflictModal({ open, conflicts, onClose, onConfirm }: Scen
|
||||
{conflicts.map((c) => (
|
||||
<div key={c.sourceSceneId} className={styles.conflictRow}>
|
||||
<div className={styles.conflictTitle}>{c.sourceTitle}</div>
|
||||
<select
|
||||
className={styles.selectInput}
|
||||
<Select
|
||||
value={choices[c.sourceSceneId] ?? 'create'}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
onChange={(v) => {
|
||||
setChoices((prev) => ({
|
||||
...prev,
|
||||
[c.sourceSceneId]: v === 'create' ? 'create' : (v as SceneId),
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<option value="create">{t('importStoryline.createNewScene')}</option>
|
||||
{c.matches.map((m) => (
|
||||
<option key={m.sceneId} value={m.sceneId}>
|
||||
{t('importStoryline.useExistingScene', { title: m.title })}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
ariaLabel={c.sourceTitle}
|
||||
options={[
|
||||
{ value: 'create', label: t('importStoryline.createNewScene') },
|
||||
...c.matches.map((m) => ({
|
||||
value: m.sceneId,
|
||||
label: t('importStoryline.useExistingScene', { title: m.title }),
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -687,24 +687,23 @@ function NpcConflictModalBody({
|
||||
{conflicts.map((c) => (
|
||||
<div key={c.sourceNpcId} className={styles.conflictRow}>
|
||||
<div className={styles.conflictTitle}>{c.sourceName}</div>
|
||||
<select
|
||||
className={styles.selectInput}
|
||||
<Select
|
||||
value={choices[c.sourceNpcId] ?? 'create'}
|
||||
onChange={(e) => {
|
||||
const v = e.target.value;
|
||||
onChange={(v) => {
|
||||
setChoices((prev) => ({
|
||||
...prev,
|
||||
[c.sourceNpcId]: v === 'create' ? 'create' : v,
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<option value="create">{t('importStoryline.createNewNpc')}</option>
|
||||
{c.matches.map((m) => (
|
||||
<option key={m.npcId} value={m.npcId}>
|
||||
{t('importStoryline.useExistingNpc', { name: m.name })}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
ariaLabel={c.sourceName}
|
||||
options={[
|
||||
{ value: 'create', label: t('importStoryline.createNewNpc') },
|
||||
...c.matches.map((m) => ({
|
||||
value: m.npcId,
|
||||
label: t('importStoryline.useExistingNpc', { name: m.name }),
|
||||
})),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user