Keep Authoring within its stack and theme, and cover its wiring

- Keep the documented 72px / flex-grow 1 action sizing and the 360px
  panel width; the larger type applies to the Authoring panel only, so
  CCTV and Street Level buttons are unchanged.
- Scope the primary and file-row styles to the panel so Cyber's shared
  button surface no longer overrides them.
- Shorten the scene PLAY in Spanish and Russian so it does not break
  mid-word, and fix two translation inconsistencies.
- Map the spoken name "authoring" to the panel.
- Clear the has-shots marker when the controls are destroyed.
- Test the idle button states through published changes, the PLAY
  translation context, and where the sharing row is placed.
This commit is contained in:
Said Turksever committed 2026-10-29 11:48:52 +00:00
1 parent 452292001a
commit 4192a6f87d
8 files changed
+157 -15

No files matched your search

+2 -2
View File
@@ -3012,9 +3012,9 @@
"Add the current view, style and layers as the next shot": "Añade la vista, el estilo y las capas actuales como el siguiente plano",
"Replace the selected shot with the current view": "Sustituye el plano seleccionado por la vista actual",
"Click a shot to select it, double-click to rename.": "Haz clic en un plano para seleccionarlo y doble clic para cambiarle el nombre.",
"scene|PLAY": "REPRODUCIR",
"scene|PLAY": "REPROD.",
"Play the whole scene from its first shot": "Reproduce toda la escena desde su primer plano",
"Stop playback (Esc)": "Detiene la reproducción (Esc)",
"Stop playback (Esc)": "Detén la reproducción (Esc)",
"Play only the shot after the selected one": "Reproduce solo el plano posterior al seleccionado",
"SAVE & SHARE": "GUARDAR Y COMPARTIR",
"Download every scene as a JSON file you can import later": "Descarga todas las escenas como un archivo JSON que puedes importar más tarde",
+1 -1
View File
@@ -3081,7 +3081,7 @@
"Add the current view, style and layers as the next shot": "Добавить текущий вид, стиль и слои следующим кадром",
"Replace the selected shot with the current view": "Заменить выбранный кадр текущим видом",
"Click a shot to select it, double-click to rename.": "Щёлкните кадр, чтобы выбрать его, дважды — чтобы переименовать.",
"scene|PLAY": "ВОСПРОИЗВЕСТИ",
"scene|PLAY": "ПУСК",
"Play the whole scene from its first shot": "Воспроизвести всю сцену с первого кадра",
"Stop playback (Esc)": "Остановить воспроизведение (Esc)",
"Play only the shot after the selected one": "Воспроизвести только кадр после выбранного",
+1 -1
View File
@@ -2862,7 +2862,7 @@
"routing busy — too many routes at once": "路線服務繁忙:同時請求的路線過多",
"route proxy error": "路線代理出錯",
"AUTHORING": "創作",
"Create and edit scenes": "建立和編輯場景",
"Create and edit scenes": "創建和編輯場景",
"SCENE": "場景",
"Scene": "場景",
"NEW SCENE": "新建場景",
+1
View File
@@ -272,6 +272,7 @@ export class SceneControls {
])
remove();
if (this.elements.shots) this.elements.shots.textContent = '';
this.elements.panel?.classList.toggle('has-shots', false);
presentSceneRuntime(this.elements.runtime, '');
this.setPlaybackActive(false);
}
+141
View File
@@ -3,6 +3,8 @@ import assert from 'node:assert/strict';
import { createStateChannel } from '../app/stateChannel.js';
import { SceneControls } from './sceneControls.js';
import { SceneDirector } from '../scenes/director.js';
import { mountSceneSharing } from './sceneSharing.js';
import { setLocale } from '../i18n/index.js';
function fixture() {
class Element extends EventTarget {
@@ -466,3 +468,142 @@ test('Scene controls consume current state, preserve rows on progress, and unsub
f.restore();
}
});
test('Authoring refreshes idle buttons from published changes', () => {
const f = fixture();
try {
f.owner.destroy();
const channel = createStateChannel(() => f.state);
const owner = new SceneControls({
read: () => f.state,
actions: f.actions,
elements: f.elements,
subscribe: (listener) => channel.subscribe(listener),
});
const disabled = (name) => f.elements[name].disabled;
assert.equal(disabled('start'), false);
assert.equal(f.elements.panel.classList.contains('has-shots'), true);
f.state.selectedShotId = null;
channel.publish({ type: 'selection-changed' });
assert.equal(disabled('update'), true);
f.state.scenes[0].shots = [];
channel.publish({ type: 'shot-deleted' });
assert.equal(disabled('start'), true);
assert.equal(disabled('next'), true);
assert.equal(f.elements.panel.classList.contains('has-shots'), false);
// A run that ends hands the buttons back with the idle rules applied.
f.state.running = true;
channel.publish({ type: 'buttons-changed' });
assert.equal(disabled('capture'), true);
assert.equal(disabled('stop'), false);
f.state.running = false;
channel.publish({ type: 'buttons-changed' });
assert.equal(disabled('capture'), false);
assert.equal(disabled('start'), true);
f.state.scenes = [];
f.state.selectedSceneId = null;
channel.publish({ type: 'scene-deleted' });
assert.equal(disabled('delete'), true);
assert.equal(disabled('capture'), true);
assert.equal(disabled('new'), false);
f.state.scenes = [{ id: 'scene-b', title: 'B', shots: [{ id: 's' }] }];
f.state.selectedSceneId = 'scene-b';
f.state.selectedShotId = 's';
channel.publish({ type: 'project-imported' });
assert.equal(disabled('start'), false);
assert.equal(f.elements.panel.classList.contains('has-shots'), true);
owner.destroy();
assert.equal(f.elements.panel.classList.contains('has-shots'), false);
channel.destroy();
} finally {
f.restore();
}
});
test('Authoring PLAY uses the scene context, not the radio PLAY', (context) => {
context.after(() => setLocale('en'));
setLocale('es');
const f = fixture();
try {
assert.equal(f.elements.start.textContent, 'REPROD.');
} finally {
f.restore();
}
});
test('The sharing row sits under SAVE & SHARE, after the file row', () => {
const saved = globalThis.document;
const node = (className = '') => {
const element = {
className,
children: [],
dataset: {},
parent: null,
addEventListener() {},
removeEventListener() {},
append(...items) {
for (const item of items) {
item.parent = element;
element.children.push(item);
}
},
appendChild(item) {
element.append(item);
},
after(item) {
const siblings = element.parent.children;
item.parent = element.parent;
siblings.splice(siblings.indexOf(element) + 1, 0, item);
},
remove() {
const siblings = element.parent?.children;
if (siblings) siblings.splice(siblings.indexOf(element), 1);
},
querySelector(selector) {
const name = selector.slice(1);
const walk = (at) => {
for (const child of at.children) {
if (child.className.split(' ').includes(name)) return child;
const found = walk(child);
if (found) return found;
}
return null;
};
return walk(element);
},
};
return element;
};
globalThis.document = { createElement: () => node() };
try {
const panel = node();
const inner = node('scene-panel-inner');
const fileRow = node('scene-controls scene-file-controls');
const status = node('scene-status');
panel.append(inner);
inner.append(fileRow, status);
const dispose = mountSceneSharing({ edit() {}, share() {} }, panel);
const bar = inner.children[1];
assert.equal(bar.className, 'scene-controls');
assert.equal(inner.children[2], status);
assert.ok(bar.children.every((button) => button.title));
dispose();
assert.deepEqual(inner.children, [fileRow, status]);
// Without the file row, the bar still lands inside the panel.
const plain = node();
const plainInner = node('scene-panel-inner');
plain.append(plainInner);
mountSceneSharing({ edit() {}, share() {} }, plain)();
mountSceneSharing({ edit() {}, share() {} }, plain);
assert.equal(plainInner.children.length, 1);
} finally {
globalThis.document = saved;
}
});
+8 -10
View File
@@ -1,6 +1,6 @@
/* ── Scene Director Panel ───────────────────── */
#scene-panel {
--panel-expanded-width: 372px;
--panel-expanded-width: 360px;
/* Secondary text in Authoring: dimmer than labels, but legible on the globe. */
--scene-quiet: rgba(232, 234, 237, 0.62);
position: fixed;
@@ -179,6 +179,9 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
.scene-btn {
flex: 1;
min-width: 72px;
}
#scene-panel .scene-btn {
font-size: 10px;
padding: 8px 10px;
}
@@ -206,21 +209,16 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
margin-top: 2px;
}
/* One primary action per step: CAPTURE SHOT and PLAY. */
.scene-btn-primary:not(:disabled) {
/* One primary action per step: CAPTURE SHOT and PLAY. The panel id keeps
this above Cyber's shared button surface. */
#scene-panel .scene-btn-primary:not(:disabled) {
color: var(--accent);
border-color: color-mix(in srgb, var(--accent) 55%, transparent);
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
#scene-capture-btn {
flex: 2 1 0;
}
/* The file row is housekeeping; it stays quieter than the steps above. */
.scene-file-controls .scene-btn {
flex: 1 1 0;
min-width: 0;
#scene-panel .scene-file-controls .scene-btn {
font-size: 9px;
background: transparent;
}
+1
View File
@@ -98,6 +98,7 @@ const PANEL_ALIASES = new Map([
['global context', 'global-context-panel'],
['right context', 'global-context-panel'],
['context right panel', 'global-context-panel'],
['authoring', 'scene-panel'],
['scenes', 'scene-panel'],
['scene', 'scene-panel'],
['post processing', 'pp-toggles'],