feat: окно инструкций, свёрнутая история и обновление спонсоров

Инструкции вынесены в отдельное окно Electron; токены игроков не пропадают при смене сцены; предпросмотр и плейлисты адаптированы; история свёрнута по умолчанию.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-09-17 06:21:42 +08:00
parent 712086ddca
commit a29c72b307
24 changed files with 506 additions and 290 deletions
+34 -2
View File
@@ -339,19 +339,39 @@
.previewHeader {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px 12px;
}
.previewTitle {
font-size: var(--text-md);
font-weight: 900;
flex: 0 1 auto;
display: flex;
align-items: center;
min-height: 34px;
line-height: 1.2;
}
.previewActions {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
gap: 10px;
min-width: 0;
flex: 1 1 240px;
}
.previewTokenTools {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
align-items: center;
gap: 8px 12px;
min-width: 0;
flex: 1 1 200px;
}
.npcTokenScale {
@@ -359,6 +379,7 @@
align-items: center;
gap: 8px;
min-width: 0;
min-height: 34px;
}
.snapToGrid {
@@ -366,26 +387,37 @@
align-items: center;
gap: 6px;
min-width: 0;
min-height: 34px;
cursor: pointer;
user-select: none;
}
.snapToGrid input {
margin: 0;
flex-shrink: 0;
}
.snapToGridLabel {
color: var(--text2);
font-size: var(--text-xs);
font-weight: 700;
white-space: nowrap;
line-height: 1.25;
}
.npcTokenScaleLabel {
color: var(--text2);
font-size: var(--text-xs);
font-weight: 700;
line-height: 1.25;
white-space: nowrap;
}
.npcTokenScaleRange {
width: min(140px, 22vw);
flex: 1 1 auto;
min-width: 72px;
width: min(140px, 100%);
height: 34px;
margin: 0;
accent-color: var(--color-accent, #c9a227);
}
+6 -6
View File
@@ -210,7 +210,7 @@ export function ControlApp() {
const [session, setSession] = useState<SessionState | null>(null);
const historyRef = useRef<GraphNodeId[]>([]);
const [history, setHistory] = useState<GraphNodeId[]>([]);
const [historyOpen, setHistoryOpen] = useState(true);
const [historyOpen, setHistoryOpen] = useState(false);
const [scenesOpen, setScenesOpen] = useState(false);
const [darknessBrushShape, setDarknessBrushShape] = useState<SceneDarknessBrushShape>('circle');
const darknessBrushShapeRef = useRef(darknessBrushShape);
@@ -2385,6 +2385,7 @@ export function ControlApp() {
<div className={styles.previewHeader}>
<div className={styles.previewTitle}>{t('control.screenPreview')}</div>
<div className={styles.previewActions}>
<div className={styles.previewTokenTools}>
<label
className={styles.snapToGrid}
title={
@@ -2408,7 +2409,6 @@ export function ControlApp() {
<span className={styles.snapToGridLabel}>{t('control.snapTokensToGrid')}</span>
</label>
{USERS_BRANCH_FEATURES_ENABLED ? (
<>
<label className={styles.npcTokenScale}>
<span className={styles.npcTokenScaleLabel}>{t('control.npcTokenScale')}</span>
<input
@@ -2427,7 +2427,9 @@ export function ControlApp() {
}}
/>
</label>
{scenePlayerTokensSession.selectedPlayerIds.length > 0 ? (
) : null}
</div>
{USERS_BRANCH_FEATURES_ENABLED && scenePlayerTokensSession.selectedPlayerIds.length > 0 ? (
<Button
data-testid="toggle-session-players"
onClick={() => {
@@ -2473,8 +2475,6 @@ export function ControlApp() {
? t('control.hidePlayers')
: t('control.showPlayers')}
</Button>
) : null}
</>
) : null}
<Button onClick={() => void api.invoke(ipcChannels.windows.closeMultiWindow, {})}>
{t('control.stopPresentation')}
@@ -3054,7 +3054,7 @@ export function ControlApp() {
style={{ zIndex: 48 }}
/>
) : null}
<SessionTimerLayer viewport={previewContentRect} editable />
<SessionTimerLayer viewport={markerViewport} editable />
</div>
</Surface>
@@ -135,16 +135,20 @@ void test('ControlApp: чекбокс привязки токенов к сет
assert.ok(src.includes('snapAllTokensToGrid'));
assert.ok(src.includes('snapNorm={snapNormActive}'));
assert.ok(css.includes('.snapToGrid'));
assert.ok(css.includes('.previewTokenTools'));
assert.match(css, /\.previewTokenTools[\s\S]*?repeat\(auto-fit/);
assert.match(css, /\.previewHeader[\s\S]*?flex-wrap:\s*wrap/);
assert.match(css, /\.previewHeader[\s\S]*?align-items:\s*center/);
});
void test('ControlApp: история — колонка сверху вниз и фон как у карточек ветвления', () => {
void test('ControlApp: история — свёрнута по умолчанию, колонка сверху вниз и фон как у карточек ветвления', () => {
const src = readControlApp();
const css = readControlAppCss();
const story = src.indexOf("t('control.storyLine')");
assert.ok(story !== -1);
assert.ok(src.includes('data-testid="control-history-accordion"'));
assert.ok(src.includes('aria-expanded={historyOpen}'));
assert.ok(src.includes('const [historyOpen, setHistoryOpen] = useState(true)'));
assert.ok(src.includes('const [historyOpen, setHistoryOpen] = useState(false)'));
assert.ok(src.includes('className={styles.storyScroll}'));
assert.match(css, /\.storyScroll[\s\S]*?justify-content:\s*flex-start/);
assert.match(css, /\.storyScroll[\s\S]*?background:\s*var\(--color-overlay-dark-2\)/);