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:
@@ -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>
|
||||
|
||||
@@ -13,8 +13,7 @@ import {
|
||||
} from '../editor/fileDrop';
|
||||
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
||||
import matStyles from '../editor/MaterialsModals.module.css';
|
||||
import { Button, Input } from '../shared/ui/controls';
|
||||
import controlStyles from '../shared/ui/Controls.module.css';
|
||||
import { Button, Input, Select } from '../shared/ui/controls';
|
||||
import { useAssetUrl } from '../shared/useAssetImageUrl';
|
||||
|
||||
import { NpcBindingFields } from './NpcBindingFields';
|
||||
@@ -166,18 +165,15 @@ export function NpcEditModal({
|
||||
{!initial && groupOptions.length > 0 ? (
|
||||
<div className={editorStyles.fieldGrid}>
|
||||
<div className={editorStyles.fieldLabel}>{t('npcs.group')}</div>
|
||||
<select
|
||||
className={controlStyles.input}
|
||||
<Select
|
||||
value={groupId}
|
||||
onChange={(e) => setGroupId(e.target.value as NpcGroupId | '')}
|
||||
>
|
||||
<option value="">{t('npcs.ungrouped')}</option>
|
||||
{groupOptions.map((g) => (
|
||||
<option key={g.id} value={g.id}>
|
||||
{g.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
ariaLabel={t('npcs.group')}
|
||||
onChange={(next) => setGroupId(next as NpcGroupId | '')}
|
||||
options={[
|
||||
{ value: '', label: t('npcs.ungrouped') },
|
||||
...groupOptions.map((g) => ({ value: g.id, label: g.label })),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
||||
@@ -164,15 +164,9 @@
|
||||
|
||||
.filterSelect {
|
||||
min-width: 160px;
|
||||
padding: 6px 10px;
|
||||
padding-right: 28px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--stroke);
|
||||
background-color: rgba(24, 24, 27, 0.92);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12' fill='none'%3E%3Cpath d='M2.5 4.25L6 7.75L9.5 4.25' stroke='rgba(255,255,255,0.72)' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 12px 12px;
|
||||
width: auto;
|
||||
min-height: 30px;
|
||||
background: rgba(24, 24, 27, 0.92);
|
||||
color: var(--text1);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -31,6 +31,7 @@ import type {
|
||||
ProjectNpcGroup,
|
||||
ProjectNpcRelation,
|
||||
} from '../../shared/types';
|
||||
import { Select } from '../shared/ui/controls';
|
||||
import { useAssetUrl } from '../shared/useAssetImageUrl';
|
||||
|
||||
import styles from './NpcGraph.module.css';
|
||||
@@ -300,20 +301,17 @@ function FilterToolbar({
|
||||
}) {
|
||||
return (
|
||||
<Panel position="top-left">
|
||||
<select
|
||||
<Select
|
||||
className={styles.filterSelect}
|
||||
aria-label={ui.graphFilter}
|
||||
ariaLabel={ui.graphFilter}
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value as GraphGroupFilter)}
|
||||
>
|
||||
<option value="all">{ui.graphFilterAll}</option>
|
||||
<option value="ungrouped">{ui.graphFilterUngrouped}</option>
|
||||
{groups.map((g) => (
|
||||
<option key={g.id} value={g.id}>
|
||||
{g.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
onChange={(next) => onChange(next as GraphGroupFilter)}
|
||||
options={[
|
||||
{ value: 'all', label: ui.graphFilterAll },
|
||||
{ value: 'ungrouped', label: ui.graphFilterUngrouped },
|
||||
...groups.map((g) => ({ value: g.id, label: g.name })),
|
||||
]}
|
||||
/>
|
||||
</Panel>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,8 +15,7 @@ import type {
|
||||
import editorStyles from '../editor/EditorApp.module.css';
|
||||
import { useEditorI18n } from '../editor/i18n/EditorI18nContext';
|
||||
import { getDndApi } from '../shared/dndApi';
|
||||
import { Button, Input } from '../shared/ui/controls';
|
||||
import controlStyles from '../shared/ui/Controls.module.css';
|
||||
import { Button, Input, Select } from '../shared/ui/controls';
|
||||
import { useAssetUrl } from '../shared/useAssetImageUrl';
|
||||
|
||||
import { NpcBindingFields } from './NpcBindingFields';
|
||||
@@ -785,24 +784,21 @@ export function NpcsEditorApp() {
|
||||
|
||||
<div>
|
||||
<div className={styles.fieldLabel}>{t('npcs.group')}</div>
|
||||
<select
|
||||
className={controlStyles.input}
|
||||
<Select
|
||||
value={selected.groupId ?? ''}
|
||||
onChange={(e) => {
|
||||
const groupId = (e.target.value || null) as NpcGroupId | null;
|
||||
ariaLabel={t('npcs.group')}
|
||||
onChange={(next) => {
|
||||
const groupId = (next || null) as NpcGroupId | null;
|
||||
void api.invoke(ipcChannels.project.updateNpcFields, {
|
||||
npcId: selected.id,
|
||||
groupId,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<option value="">{t('npcs.ungrouped')}</option>
|
||||
{groupOptions.map((g) => (
|
||||
<option key={g.id} value={g.id}>
|
||||
{g.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
options={[
|
||||
{ value: '', label: t('npcs.ungrouped') },
|
||||
...groupOptions.map((g) => ({ value: g.id, label: g.label })),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user