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:
@@ -328,6 +328,10 @@
|
||||
transform: translate(-50%, -50%);
|
||||
width: 520px;
|
||||
max-width: calc(100vw - 32px);
|
||||
max-height: calc(100vh - 32px);
|
||||
overflow: auto;
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--stroke);
|
||||
background: var(--color-surface-elevated);
|
||||
@@ -335,6 +339,7 @@
|
||||
padding: 16px;
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.modalHeader {
|
||||
@@ -364,6 +369,17 @@
|
||||
.fieldGrid {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fieldGroup {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.fieldGroupSpaced {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.fieldLabel {
|
||||
@@ -377,22 +393,6 @@
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
.selectInput {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 8px 10px;
|
||||
padding-right: 28px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--stroke);
|
||||
background-color: var(--bg0);
|
||||
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;
|
||||
color: var(--text0);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.rowFlex {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -402,8 +402,13 @@
|
||||
.modalFooter {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
margin-top: 4px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.licenseBlockTitle {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
|
||||
import { Button } from '../shared/ui/controls';
|
||||
import { Button, Select } from '../shared/ui/controls';
|
||||
|
||||
import styles from './EditorApp.module.css';
|
||||
import { useEditorI18n } from './i18n/EditorI18nContext';
|
||||
@@ -74,19 +74,20 @@ export function FoundryImportModal({ open, pickSource, onClose, onImport }: Foun
|
||||
<div className={styles.muted}>{t('foundryImport.hint')}</div>
|
||||
|
||||
<div className={styles.fieldLabel}>{t('foundryImport.sourceType')}</div>
|
||||
<select
|
||||
className={styles.selectInput}
|
||||
<Select
|
||||
value={mode}
|
||||
disabled={submitting}
|
||||
onChange={(e) => {
|
||||
setMode(e.target.value as 'folder' | 'archive');
|
||||
ariaLabel={t('foundryImport.sourceType')}
|
||||
options={[
|
||||
{ value: 'folder', label: t('foundryImport.folder') },
|
||||
{ value: 'archive', label: t('foundryImport.archive') },
|
||||
]}
|
||||
onChange={(next) => {
|
||||
setMode(next as 'folder' | 'archive');
|
||||
setPicked(null);
|
||||
setError(null);
|
||||
}}
|
||||
>
|
||||
<option value="folder">{t('foundryImport.folder')}</option>
|
||||
<option value="archive">{t('foundryImport.archive')}</option>
|
||||
</select>
|
||||
/>
|
||||
|
||||
<div className={styles.fieldLabel}>{t('foundryImport.source')}</div>
|
||||
<div className={styles.importFileRow}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -350,6 +350,7 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
|
||||
previewRotationDeg: 0,
|
||||
darkenScene: false,
|
||||
traps: [],
|
||||
tokens: [],
|
||||
grid: { enabled: false, type: 'square', sizeN: 0.06, color: '#ffffff' },
|
||||
media: { videos: [], audios: [] },
|
||||
settings: { autoplayVideo: false, autoplayAudio: true, loopVideo: true, loopAudio: true },
|
||||
@@ -592,6 +593,7 @@ export function useProjectState(licenseActive: boolean, opts?: ProjectStateOpts)
|
||||
: null),
|
||||
...(patch.darkenScene !== undefined ? { darkenScene: patch.darkenScene } : null),
|
||||
...(patch.traps !== undefined ? { traps: patch.traps } : null),
|
||||
...(patch.tokens !== undefined ? { tokens: patch.tokens } : null),
|
||||
...(patch.grid !== undefined ? { grid: patch.grid } : null),
|
||||
...(patch.settings ? { settings: { ...scene.settings, ...patch.settings } } : null),
|
||||
...(patch.media ? { media: { ...scene.media, ...patch.media } } : null),
|
||||
|
||||
Reference in New Issue
Block a user