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:
Ivan Fontosh
2026-07-27 11:04:12 +08:00
parent bdeb64e356
commit f270812219
44 changed files with 2617 additions and 341 deletions
+26 -31
View File
@@ -4,7 +4,7 @@ import { isNpcBindingNone, listStorylineOptionsForBinding, noneBinding } from '.
import type { GraphNodeId, NpcBinding, Project, SceneId } from '../../shared/types';
import editorStyles from '../editor/EditorApp.module.css';
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
import controlStyles from '../shared/ui/Controls.module.css';
import { Select } from '../shared/ui/controls';
type NpcBindingFieldsProps = {
project: Project;
@@ -65,11 +65,14 @@ export function NpcBindingFields({ project, binding, onChange }: NpcBindingField
{enabled ? (
<>
<div className={editorStyles.fieldLabel}>{t('npcs.bindingKind')}</div>
<select
className={controlStyles.input}
<Select
value={kind}
onChange={(e) => {
const nextKind = e.target.value;
ariaLabel={t('npcs.bindingKind')}
options={[
{ value: 'storyline', label: t('npcs.bindingStoryline') },
{ value: 'scene', label: t('npcs.bindingScene') },
]}
onChange={(nextKind) => {
if (nextKind === 'scene') {
const first = sceneOptions[0];
onChange(first ? { kind: 'scene', sceneId: first.id } : noneBinding());
@@ -86,17 +89,26 @@ export function NpcBindingFields({ project, binding, onChange }: NpcBindingField
onChange(noneBinding());
}
}}
>
<option value="storyline">{t('npcs.bindingStoryline')}</option>
<option value="scene">{t('npcs.bindingScene')}</option>
</select>
/>
<div className={editorStyles.fieldLabel}>{t('npcs.bindingSelect')}</div>
<select
className={controlStyles.input}
<Select
value={bindingTargetValue}
onChange={(e) => {
const v = e.target.value;
ariaLabel={t('npcs.bindingSelect')}
options={
kind === 'storyline'
? [
...(storylineOpts.main
? [{ value: 'main', label: t('npcs.bindingMain') }]
: []),
...storylineOpts.sides.map((s) => ({
value: `side:${s.startGraphNodeId}`,
label: s.label,
})),
]
: sceneOptions.map((s) => ({ value: s.id, label: s.title }))
}
onChange={(v) => {
if (kind === 'scene') {
onChange({ kind: 'scene', sceneId: v as SceneId });
return;
@@ -115,24 +127,7 @@ export function NpcBindingFields({ project, binding, onChange }: NpcBindingField
});
}
}}
>
{kind === 'storyline' ? (
<>
{storylineOpts.main ? <option value="main">{t('npcs.bindingMain')}</option> : null}
{storylineOpts.sides.map((s) => (
<option key={s.startGraphNodeId} value={`side:${s.startGraphNodeId}`}>
{s.label}
</option>
))}
</>
) : (
sceneOptions.map((s) => (
<option key={s.id} value={s.id}>
{s.title}
</option>
))
)}
</select>
/>
</>
) : null}
</div>