mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
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:
1 parent
452292001a
commit
4192a6f87d
8 files changed
+157
-15
No files matched your search
@@ -1176,7 +1176,8 @@ The panel is titled Authoring and is laid out in four labelled steps: SCENE
|
||||
(picker on its own row, NEW SCENE, DELETE), SHOTS (+ CAPTURE SHOT, UPDATE SHOT,
|
||||
the numbered shot list and a select/rename hint), PREVIEW (PLAY, STOP, NEXT) and
|
||||
SAVE & SHARE (EXPORT, IMPORT, RUN LOG, then the EDIT DETAILS / SHARE SCENE row).
|
||||
Every action has a tooltip, and the status line is a polite live region. Outside
|
||||
Every button in the panel's step rows has a tooltip, and the status line is a
|
||||
polite live region. Outside
|
||||
playback a button that would do nothing is disabled: DELETE and CAPTURE SHOT
|
||||
without a scene, UPDATE SHOT without a selected shot, PLAY and NEXT on a scene
|
||||
with no shots. During playback everything but STOP stays disabled as before.
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Воспроизвести только кадр после выбранного",
|
||||
|
||||
@@ -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": "新建場景",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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'],
|
||||
|
||||
Reference in new issue
Block a user