refactor(ui): share Scene and CCTV action buttons

This commit is contained in:
Mazeyar Moeini Feizabadi committed 2026-10-21 15:22:00 +02:00
1 parent 5a1598fd9e
commit 538f2f1bc0
15 files changed
+505 -86

No files matched your search

+9
View File
@@ -374,6 +374,15 @@ renderer, persistence or services. The facade retains panel visibility, share
restore claims and render scheduling. Both controls are destroyed before the restore claims and render scheduling. Both controls are destroyed before the
facade's asynchronous teardown can yield. facade's asynchronous teardown can yield.
## Ordinary action buttons
`src/ui/actionButton.js` is a native, listener-free view helper used by Scene
presentation and sharing-toolbar owners. It receives translated text and explicit
presentation/accessibility properties. Feature controllers own action dispatch,
state, persistence and cleanup. Static Scenes/CCTV templates use the same CSS
surface and preserve IDs and layout hooks. The helper imports no application,
renderer, storage or services; it belongs to the consuming package closures.
## Display controls ## Display controls
`ui/display` owns Display button, selector and slider subscriptions. It receives `ui/display` owns Display button, selector and slider subscriptions. It receives
+20
View File
@@ -1233,6 +1233,26 @@ Feature owners retain bounds, steps, formatted values, input/change timing and
listener cleanup. The radio tuning dial, imagery comparison divider and event listener cleanup. The radio tuning dial, imagery comparison divider and event
comparison/progression controls keep their specialized presentation and behavior. comparison/progression controls keep their specialized presentation and behavior.
## Ordinary action buttons
Scene panel actions, shot LOAD/DEL actions, the EDIT DETAILS / SHARE SCENE
bar and one-shot CCTV actions use `ui/actionButton.js` and the shared
`gev-action-button` surface in `ui/styles/action-button.css`. Static templates
apply the same classes as generated buttons. The native builder accepts translated
text, default/danger variants, default/compact sizes and disabled/accessibility
properties; it installs no listeners and owns no application state. Feature
controllers retain callbacks, enable/disable rules, confirmation prompts,
persistence and teardown. The smaller shot actions retain their existing sizing
and hover treatment. Feature CSS still owns panel layout and translated wrapping.
CCTV enable, coverage, auto-hop, projection and adjustment toggles, editable
calibration-value chips and the scene authoring dialogs retain their specialized
controls. Existing `scene-btn` hooks remain compatible with Street Level and
legacy toggles. The shared CSS, explicit input/action boundary and behavior tests
can carry to a Vue view while Vue replaces native DOM construction.
Browser acceptance: `QA_BASE_URL=http://127.0.0.1:4179 node scripts/qa-action-buttons.mjs`;
`qa-scene-controls.mjs` also covers real Scene authoring/playback with native Space capture.
## Segmented selectors ## Segmented selectors
Segmented selectors use the controlled DOM adapter in `ui/segmentedControl.js` Segmented selectors use the controlled DOM adapter in `ui/segmentedControl.js`
+2
View File
@@ -281,6 +281,7 @@
"src/ui/scenePresentation.js", "src/ui/scenePresentation.js",
"src/ui/sceneControls.test.mjs", "src/ui/sceneControls.test.mjs",
"scripts/qa-scene-controls.mjs", "scripts/qa-scene-controls.mjs",
"scripts/qa-action-buttons.mjs",
"src/ui.js", "src/ui.js",
"src/standalone/ui.js", "src/standalone/ui.js",
"src/ui/applicationShell.js", "src/ui/applicationShell.js",
@@ -305,6 +306,7 @@
"src/ui/styles/layers.css", "src/ui/styles/layers.css",
"src/ui/styles/controls.css", "src/ui/styles/controls.css",
"src/ui/styles/slider.css", "src/ui/styles/slider.css",
"src/ui/styles/action-button.css",
"src/ui/styles/cctv.css", "src/ui/styles/cctv.css",
"src/ui/styles/foundation.css", "src/ui/styles/foundation.css",
"src/ui/styles/first-run.css", "src/ui/styles/first-run.css",
+4
View File
@@ -614,6 +614,7 @@
"src/ui/scenes.js", "src/ui/scenes.js",
"src/ui/sceneControls.js", "src/ui/sceneControls.js",
"src/ui/scenePresentation.js", "src/ui/scenePresentation.js",
"src/ui/actionButton.js",
"src/i18n/index.js", "src/i18n/index.js",
"src/i18n/locale.js", "src/i18n/locale.js",
"src/i18n/locales/es.json", "src/i18n/locales/es.json",
@@ -737,6 +738,7 @@
"src/ui/rightPanelRail.js", "src/ui/rightPanelRail.js",
"src/ui/sceneControls.js", "src/ui/sceneControls.js",
"src/ui/scenePresentation.js", "src/ui/scenePresentation.js",
"src/ui/actionButton.js",
"src/ui/scenes.js", "src/ui/scenes.js",
"src/ui/shareRestoration.js", "src/ui/shareRestoration.js",
"src/ui/shellElements.js", "src/ui/shellElements.js",
@@ -849,6 +851,7 @@
"src/ui/scenes.js", "src/ui/scenes.js",
"src/ui/sceneControls.js", "src/ui/sceneControls.js",
"src/ui/scenePresentation.js", "src/ui/scenePresentation.js",
"src/ui/actionButton.js",
"src/contextModePolicy.js", "src/contextModePolicy.js",
"src/director/clock.js", "src/director/clock.js",
"src/director/timeline.js", "src/director/timeline.js",
@@ -2214,6 +2217,7 @@
"src/ui/rightPanelRail.js", "src/ui/rightPanelRail.js",
"src/ui/sceneControls.js", "src/ui/sceneControls.js",
"src/ui/scenePresentation.js", "src/ui/scenePresentation.js",
"src/ui/actionButton.js",
"src/ui/scenes.js", "src/ui/scenes.js",
"src/ui/shareRestoration.js", "src/ui/shareRestoration.js",
"src/ui/shellElements.js", "src/ui/shellElements.js",
+335
View File
@@ -0,0 +1,335 @@
#!/usr/bin/env node
/**
* Scenes/CCTV action-button acceptance against dev or production preview:
* QA_BASE_URL=http://127.0.0.1:4179 node scripts/qa-action-buttons.mjs
* Uses the installed CCTV controller with fixture camera/action ports to prove
* native dispatch/disabled/teardown without providers or world calibration writes.
* Real Scene authoring/playback remains covered by qa-scene-controls.mjs.
*/
import fs from 'node:fs';
import puppeteer from 'puppeteer';
const browser = await puppeteer.launch({
headless: true,
args: ['--use-gl=angle', '--use-angle=swiftshader'],
});
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
let failures = 0;
const check = (name, passed, detail) => {
console.log(`[${passed ? 'PASS' : 'FAIL'}] ${name}`);
if (!passed) {
failures++;
console.log(JSON.stringify(detail));
}
};
const shots = 'qa-shots/action-buttons';
fs.mkdirSync(shots, { recursive: true });
try {
await page.setViewport({ width: 1440, height: 900 });
await page.goto(
`${process.env.QA_BASE_URL || 'http://127.0.0.1:4179'}/?welcome=0`,
{ waitUntil: 'domcontentloaded' },
);
await page.waitForFunction(
() =>
window.__godsEyeView?.styleManager?._cctvControls &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 60000 },
);
const inventory = await page.evaluate(() => {
const ids = [
'scene-new-btn',
'scene-delete-btn',
'scene-capture-btn',
'scene-update-shot-btn',
'scene-start-btn',
'scene-stop-btn',
'scene-next-btn',
'scene-export-btn',
'scene-import-btn',
'scene-download-btn',
'cctv-nearest-btn',
'cctv-prev-btn',
'cctv-next-btn',
'cctv-focus-btn',
'cctv-calib-save-btn',
'cctv-calib-reset-btn',
];
const ordinary = ids.map((id) => document.getElementById(id));
const generated = [
...document.querySelectorAll(
'.scene-shot-btn, [data-director-authoring] button',
),
];
const toggles = [
'cctv-enable-btn',
'cctv-coverage-btn',
'cctv-auto-hop-btn',
'cctv-projection-btn',
'cctv-adjust-btn',
].map((id) => document.getElementById(id));
return {
shared: [...ordinary, ...generated].every(
(node) =>
node?.type === 'button' &&
node.classList.contains('gev-action-button'),
),
generated: generated.length,
layout: ordinary.every((node) => {
const style = getComputedStyle(node);
return style.minWidth === '72px' && style.flexGrow === '1';
}),
togglesSeparate: toggles.every(
(node) => node && !node.classList.contains('gev-action-button'),
),
compactDanger: !!document.querySelector(
'.scene-shot-danger.gev-action-button--compact.gev-action-button--danger',
),
};
});
check(
'static and generated actions share the native component',
inventory.shared && inventory.generated >= 4 && inventory.compactDanger,
inventory,
);
check(
'persistent CCTV toggles retain their existing controls',
inventory.togglesSeparate,
);
check('ordinary actions retain their panel sizing', inventory.layout);
await page.evaluate(() => {
const manager = window.__godsEyeView.styleManager;
const owner = manager._cctvControls;
owner._cctvUnsubscribe?.();
owner._cctvUnsubscribe = null;
window.__qaButtonCalls = [];
const record = (...args) => window.__qaButtonCalls.push(args);
owner.actions = {
...owner.actions,
isEnabled: () => true,
toggleEnabled: async () => {
record('enable');
return true;
},
runExplicitFocus: (select, focus) => focus(select()),
setParams: (params, options) => record('params', params, options),
showToast: () => {},
};
owner.cctv = {
focusNearest: () => {
record('nearest');
return 'a';
},
cycleCamera: (direction) => {
record('cycle', direction);
return 'b';
},
focusCamera: (id, seconds) => record('focus', id, seconds),
};
window.__qaCameraState = {
enabled: true,
activeCameraId: 'a',
cameras: [
{ id: 'a', city: 'Fixture', name: 'A' },
{ id: 'b', city: 'Fixture', name: 'B' },
],
activeCamera: { id: 'a', name: 'A', basePose: {} },
};
owner._renderCctvState(window.__qaCameraState);
manager.setPanelCollapsed('cctv-panel', false, {
persist: false,
syncShare: false,
});
});
await page.focus('#cctv-next-btn');
await page.keyboard.press('Space');
await page.waitForFunction(() => window.__qaButtonCalls.length === 3);
check(
'native Space dispatches Next and its focus once',
await page.evaluate(
() =>
JSON.stringify(window.__qaButtonCalls) ===
JSON.stringify([['enable'], ['cycle', 1], ['focus', 'b', 1.4]]),
),
);
check(
'native keyboard activation keeps a visible focus ring',
await page.$eval(
'#cctv-next-btn',
(node) =>
document.activeElement === node &&
getComputedStyle(node).outlineStyle === 'solid',
),
);
await page.evaluate(() => {
window.__qaButtonCalls = [];
});
await page.focus('#cctv-prev-btn');
await page.keyboard.press('Enter');
await page.waitForFunction(() => window.__qaButtonCalls.length === 3);
check(
'native Enter dispatches Previous once',
await page.evaluate(
() =>
JSON.stringify(window.__qaButtonCalls) ===
JSON.stringify([['enable'], ['cycle', -1], ['focus', 'b', 1.4]]),
),
);
for (const [id, expected] of [
['cctv-nearest-btn', [['enable'], ['nearest'], ['focus', 'a', 1.8]]],
[
'cctv-focus-btn',
[
['enable'],
['focus', 'a', 1.9],
['params', { selectedCameraId: 'a' }, { origin: 'user' }],
],
],
]) {
await page.evaluate(() => {
window.__qaButtonCalls = [];
});
await page.click(`#${id}`);
await page.waitForFunction(() => window.__qaButtonCalls.length === 3);
const calls = await page.evaluate(() => window.__qaButtonCalls);
check(
`${id} retains its existing action and focus timing`,
JSON.stringify(calls) === JSON.stringify(expected),
calls,
);
}
await page.evaluate(() => {
window.__qaButtonCalls = [];
});
await page.click('#cctv-calib-save-btn');
await page.click('#cctv-calib-reset-btn');
const calibration = await page.evaluate(() => window.__qaButtonCalls);
check(
'Save and Reset retain camera identity and user-origin dispatch',
JSON.stringify(calibration) ===
JSON.stringify([
[
'params',
{ selectedCameraId: 'a', calibration: { cameraId: 'a', save: true } },
{ origin: 'user' },
],
[
'params',
{
selectedCameraId: 'a',
calibration: { cameraId: 'a', reset: true },
},
{ origin: 'user' },
],
]),
calibration,
);
await page.evaluate(() => {
window.__qaButtonCalls = [];
window.__godsEyeView.styleManager._cctvControls._renderCctvState({
enabled: false,
});
});
await page.click('#cctv-next-btn');
await page.waitForFunction(
() => {
const node = document.getElementById('cctv-next-btn');
return node.disabled && Number(getComputedStyle(node).opacity) === 0.45;
},
{ timeout: 3000 },
);
check(
'disabled actions stay inert and keep disabled presentation',
await page.$eval(
'#cctv-next-btn',
(node) =>
node.disabled &&
getComputedStyle(node).opacity === '0.45' &&
window.__qaButtonCalls.length === 0,
),
);
for (const width of [1440, 620, 390]) {
await page.setViewport({ width, height: 900 });
for (const panel of ['scene-panel', 'cctv-panel']) {
await page.evaluate((panel) => {
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
persist: false,
syncShare: false,
});
}, panel);
const fit = await page.$eval(`#${panel}`, (root) => {
const buttons = [...root.querySelectorAll('.gev-action-button')];
const rect = root.getBoundingClientRect();
return {
count: buttons.length,
fits:
buttons.length > 0 &&
buttons.every((button) => {
const box = button.getBoundingClientRect();
return (
box.width > 0 &&
box.left >= rect.left &&
box.right <= rect.right + 1 &&
box.left >= 0 &&
box.right <= innerWidth + 1 &&
button.scrollWidth <= button.clientWidth
);
}),
};
});
check(`${panel} actions fit at ${width}px`, fit.fits, fit);
if (width !== 620)
await page.screenshot({ path: `${shots}/${panel}-${width}.png` });
}
}
await page.evaluate(() => {
const owner = window.__godsEyeView.styleManager._cctvControls;
owner.destroy();
document.getElementById('cctv-next-btn').disabled = false;
window.__qaButtonCalls = [];
});
await page.click('#cctv-next-btn');
check(
'destroyed controllers revoke action listeners',
await page.evaluate(() => window.__qaButtonCalls.length === 0),
);
const localized = new URL(page.url());
localized.searchParams.set('lang', 'ru');
await page.goto(localized.href, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(
() =>
window.__godsEyeView?.styleManager?._cctvControls &&
document.getElementById('loading-screen')?.classList.contains('hidden'),
{ timeout: 60000 },
);
for (const panel of ['scene-panel', 'cctv-panel']) {
await page.evaluate((panel) => {
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
persist: false,
syncShare: false,
});
}, panel);
const translated = await page.$eval(`#${panel}`, (root) => {
const buttons = [...root.querySelectorAll('.gev-action-button')];
return (
document.documentElement.lang === 'ru' &&
buttons.length > 0 &&
buttons.every((node) => node.scrollWidth <= node.clientWidth)
);
});
check(`Russian ${panel} action labels fit at 390px`, translated);
await page.screenshot({ path: `${shots}/${panel}-ru-390.png` });
}
check('no uncaught browser errors', errors.length === 0, errors);
} finally {
await browser.close();
}
console.log(`RESULT: ${failures} failures`);
process.exitCode = failures ? 1 : 0;
+2 -1
View File
@@ -66,7 +66,8 @@ try {
); );
}), }),
); );
await page.click('#scene-capture-btn'); await page.focus('#scene-capture-btn');
await page.keyboard.press('Space');
await page.waitForFunction( await page.waitForFunction(
() => document.querySelectorAll('.scene-shot-row').length === 1, () => document.querySelectorAll('.scene-shot-row').length === 1,
); );
+1 -1
View File
@@ -69,7 +69,7 @@ test('focusable controls do not animate the global outline', () => {
'#top-center-actions button {', '#top-center-actions button {',
'.data-toggle-chip {', '.data-toggle-chip {',
'.scene-btn {', '.scene-btn {',
'.scene-shot-btn {', '.gev-action-button--compact {',
]) { ]) {
assert.doesNotMatch( assert.doesNotMatch(
ruleBody(selector), ruleBody(selector),
+41
View File
@@ -0,0 +1,41 @@
/**
* Create an ordinary native action button. Static templates use the same CSS
* classes. Labels are already translated; feature owners retain callbacks,
* disabled-state updates and listener cleanup. Construction binds no listeners.
* @param {object} options
* @param {Document} [options.documentRef] Document that owns the button.
* @param {string} options.label Visible text, never interpreted as markup.
* @param {string} [options.className] Additional feature/layout hooks.
* @param {'default'|'danger'} [options.variant]
* @param {'default'|'compact'} [options.size]
* @param {boolean} [options.disabled]
* @param {string} [options.ariaLabel] Accessible name when visible text needs context.
* @param {string} [options.title]
* @returns {HTMLButtonElement}
*/
export function createActionButton({
documentRef = globalThis.document,
label,
className = '',
variant = 'default',
size = 'default',
disabled = false,
ariaLabel,
title,
}) {
const button = documentRef.createElement('button');
button.type = 'button';
button.className = [
'gev-action-button',
variant === 'danger' && 'gev-action-button--danger',
size === 'compact' && 'gev-action-button--compact',
className,
]
.filter(Boolean)
.join(' ');
button.textContent = label;
button.disabled = disabled;
if (ariaLabel !== undefined) button.setAttribute('aria-label', ariaLabel);
if (title !== undefined) button.title = title;
return button;
}
+12 -6
View File
@@ -1,5 +1,6 @@
/** Scene panel elements and text-only project presentation. */ /** Scene panel elements and text-only project presentation. */
import { formatNumber, msg, t } from '../i18n/index.js'; import { formatNumber, msg, t } from '../i18n/index.js';
import { createActionButton } from './actionButton.js';
// Labels for what a shot stores; anything else shows as stored. Style names // Labels for what a shot stores; anything else shows as stored. Style names
// match STYLE_STATUS_LABELS in visualPresets.js, which this package cannot // match STYLE_STATUS_LABELS in visualPresets.js, which this package cannot
@@ -121,13 +122,18 @@ export function renderSceneShots(
}); });
const actions = document.createElement('div'); const actions = document.createElement('div');
actions.className = 'scene-shot-actions'; actions.className = 'scene-shot-actions';
const loadButton = document.createElement('button'); const loadButton = createActionButton({
loadButton.className = 'scene-shot-btn'; label: t('LOAD'),
loadButton.textContent = t('LOAD'); className: 'scene-shot-btn',
size: 'compact',
});
listen(loadButton, 'click', () => load(scene.id, shot.id)); listen(loadButton, 'click', () => load(scene.id, shot.id));
const deleteButton = document.createElement('button'); const deleteButton = createActionButton({
deleteButton.className = 'scene-shot-btn scene-shot-danger'; label: t('DEL'),
deleteButton.textContent = t('DEL'); className: 'scene-shot-btn scene-shot-danger',
size: 'compact',
variant: 'danger',
});
listen(deleteButton, 'click', () => remove(scene.id, shot.id)); listen(deleteButton, 'click', () => remove(scene.id, shot.id));
actions.appendChild(loadButton); actions.appendChild(loadButton);
actions.appendChild(deleteButton); actions.appendChild(deleteButton);
+2 -4
View File
@@ -1,4 +1,5 @@
import { t, tn } from '../i18n/index.js'; import { t, tn } from '../i18n/index.js';
import { createActionButton } from './actionButton.js';
/** Small owned modal for scene authoring and import review; text never becomes markup. */ /** Small owned modal for scene authoring and import review; text never becomes markup. */
export function createSceneDialog(title, onClose) { export function createSceneDialog(title, onClose) {
@@ -185,10 +186,7 @@ export function mountSceneSharing(
]; ];
const removers = []; const removers = [];
for (const [text, fn] of entries) { for (const [text, fn] of entries) {
const b = document.createElement('button'); const b = createActionButton({ label: text, className: 'scene-btn' });
b.type = 'button';
b.textContent = text;
b.className = 'scene-btn';
b.addEventListener('click', fn); b.addEventListener('click', fn);
removers.push(() => b.removeEventListener('click', fn)); removers.push(() => b.removeEventListener('click', fn));
bar.append(b); bar.append(b);
+59
View File
@@ -0,0 +1,59 @@
/* Ordinary actions share one surface; feature classes own placement and width.
* Legacy scene-btn hooks keep untouched toggles/Street Level controls stable. */
.gev-action-button,
.scene-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 7px;
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 1.2px;
padding: 6px 9px;
cursor: pointer;
transition:
background-color var(--transition-fast),
border-color var(--transition-fast),
color var(--transition-fast),
opacity var(--transition-fast);
white-space: nowrap;
}
.gev-action-button:hover:not(:disabled),
.scene-btn:hover:not(:disabled) {
color: var(--accent);
border-color: var(--accent);
background: rgba(0, 212, 255, 0.14);
}
.gev-action-button:disabled,
.scene-btn:disabled {
opacity: 0.45;
cursor: default;
}
.gev-action-button--danger:hover:not(:disabled),
.scene-btn-danger:hover:not(:disabled) {
color: #ff7272;
border-color: #ff7272;
background: rgba(255, 88, 88, 0.15);
}
/* Shot-row actions retain their smaller footprint and quieter hover treatment. */
.gev-action-button--compact {
border-radius: 6px;
font-size: 8px;
letter-spacing: 0.8px;
padding: 4px 6px;
}
.gev-action-button--compact:hover:not(:disabled) {
color: var(--text-primary);
border-color: var(--glass-border-hover);
background: rgba(255, 255, 255, 0.05);
}
.gev-action-button--compact.gev-action-button--danger:hover:not(:disabled) {
color: #ff7e7e;
border-color: #ff7e7e;
}
+1
View File
@@ -171,6 +171,7 @@
.dock-tray-toggle, .dock-tray-toggle,
.dock-popover, .dock-popover,
.scene-btn, .scene-btn,
.gev-action-button:not(.gev-action-button--compact),
.context-mode-button, .context-mode-button,
.map-stack-chip, .map-stack-chip,
.key-setup-chip, .key-setup-chip,
-58
View File
@@ -184,21 +184,6 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
} }
.scene-btn { .scene-btn {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 7px;
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: 9px;
letter-spacing: 1.2px;
padding: 6px 9px;
cursor: pointer;
transition:
background-color var(--transition-fast),
border-color var(--transition-fast),
color var(--transition-fast),
opacity var(--transition-fast);
white-space: nowrap;
flex: 1; flex: 1;
min-width: 72px; min-width: 72px;
} }
@@ -295,49 +280,6 @@ html:not([lang='en']) #scene-panel .scene-controls .scene-btn {
gap: 6px; gap: 6px;
} }
.scene-shot-btn {
border: 1px solid rgba(255, 255, 255, 0.14);
border-radius: 6px;
background: rgba(255, 255, 255, 0.05);
color: var(--text-secondary);
font-family: var(--font-mono);
font-size: 8px;
letter-spacing: 0.8px;
padding: 4px 6px;
cursor: pointer;
transition:
background-color var(--transition-fast),
border-color var(--transition-fast),
color var(--transition-fast);
}
.scene-shot-btn:hover {
color: var(--text-primary);
border-color: var(--glass-border-hover);
}
.scene-shot-btn.scene-shot-danger:hover {
color: #ff7e7e;
border-color: #ff7e7e;
}
.scene-btn:hover:not(:disabled) {
color: var(--accent);
border-color: var(--accent);
background: rgba(0, 212, 255, 0.14);
}
.scene-btn:disabled {
opacity: 0.45;
cursor: default;
}
.scene-btn-danger:hover:not(:disabled) {
color: #ff7272;
border-color: #ff7272;
background: rgba(255, 88, 88, 0.15);
}
.scene-progress { .scene-progress {
margin-top: 8px; margin-top: 8px;
width: 100%; width: 100%;
+16 -16
View File
@@ -31,15 +31,15 @@
<div id="cctv-meta" data-i18n>Enable CCTV to load camera intersections</div> <div id="cctv-meta" data-i18n>Enable CCTV to load camera intersections</div>
<div class="cctv-controls"> <div class="cctv-controls">
<button id="cctv-enable-btn" class="scene-btn" data-i18n>CCTV OFF</button> <button id="cctv-enable-btn" class="scene-btn" data-i18n>CCTV OFF</button>
<button id="cctv-nearest-btn" class="scene-btn" data-i18n>NEAREST</button> <button id="cctv-nearest-btn" type="button" class="gev-action-button scene-btn" data-i18n>NEAREST</button>
</div> </div>
<div class="cctv-controls"> <div class="cctv-controls">
<button id="cctv-prev-btn" class="scene-btn" data-i18n>PREV</button> <button id="cctv-prev-btn" type="button" class="gev-action-button scene-btn" data-i18n>PREV</button>
<select id="cctv-camera-select" aria-label="CCTV camera" data-i18n-aria-label></select> <select id="cctv-camera-select" aria-label="CCTV camera" data-i18n-aria-label></select>
<button id="cctv-next-btn" class="scene-btn" data-i18n>NEXT</button> <button id="cctv-next-btn" type="button" class="gev-action-button scene-btn" data-i18n>NEXT</button>
</div> </div>
<div class="cctv-controls"> <div class="cctv-controls">
<button id="cctv-focus-btn" class="scene-btn" data-i18n>FOCUS</button> <button id="cctv-focus-btn" type="button" class="gev-action-button scene-btn" data-i18n>FOCUS</button>
<button id="cctv-coverage-btn" class="scene-btn" data-i18n>COVERAGE OFF</button> <button id="cctv-coverage-btn" class="scene-btn" data-i18n>COVERAGE OFF</button>
<button id="cctv-auto-hop-btn" class="scene-btn" data-i18n>AUTO HOP OFF</button> <button id="cctv-auto-hop-btn" class="scene-btn" data-i18n>AUTO HOP OFF</button>
</div> </div>
@@ -62,8 +62,8 @@
<button class="cctv-cal-value" data-cal-field="east" title="East offset from catalog position (m) — click to type" data-i18n-title>ΔE --</button> <button class="cctv-cal-value" data-cal-field="east" title="East offset from catalog position (m) — click to type" data-i18n-title>ΔE --</button>
</div> </div>
<div class="cctv-controls cctv-calibration-actions"> <div class="cctv-controls cctv-calibration-actions">
<button id="cctv-calib-save-btn" class="scene-btn" data-i18n>SAVE CAL</button> <button id="cctv-calib-save-btn" type="button" class="gev-action-button scene-btn" data-i18n>SAVE CAL</button>
<button id="cctv-calib-reset-btn" class="scene-btn" data-i18n>RESET CAL</button> <button id="cctv-calib-reset-btn" type="button" class="gev-action-button scene-btn" data-i18n>RESET CAL</button>
</div> </div>
</div> </div>
<div class="cctv-summary-label" data-i18n>SCENE SUMMARY</div> <div class="cctv-summary-label" data-i18n>SCENE SUMMARY</div>
@@ -151,23 +151,23 @@
</div> </div>
<div class="scene-controls"> <div class="scene-controls">
<select id="scene-select" aria-label="Scene recipe" data-i18n-aria-label></select> <select id="scene-select" aria-label="Scene recipe" data-i18n-aria-label></select>
<button id="scene-new-btn" class="scene-btn" data-i18n>NEW</button> <button id="scene-new-btn" type="button" class="gev-action-button scene-btn" data-i18n>NEW</button>
<button id="scene-delete-btn" class="scene-btn scene-btn-danger" data-i18n>DEL</button> <button id="scene-delete-btn" type="button" class="gev-action-button gev-action-button--danger scene-btn scene-btn-danger" data-i18n>DEL</button>
</div> </div>
<div class="scene-controls"> <div class="scene-controls">
<button id="scene-capture-btn" class="scene-btn" data-i18n>CAPTURE SHOT</button> <button id="scene-capture-btn" type="button" class="gev-action-button scene-btn" data-i18n>CAPTURE SHOT</button>
<button id="scene-update-shot-btn" class="scene-btn" data-i18n>UPDATE SHOT</button> <button id="scene-update-shot-btn" type="button" class="gev-action-button scene-btn" data-i18n>UPDATE SHOT</button>
</div> </div>
<div id="scene-shot-list" class="scene-shot-list"></div> <div id="scene-shot-list" class="scene-shot-list"></div>
<div class="scene-controls"> <div class="scene-controls">
<button id="scene-start-btn" class="scene-btn" data-i18n>START</button> <button id="scene-start-btn" type="button" class="gev-action-button scene-btn" data-i18n>START</button>
<button id="scene-stop-btn" class="scene-btn scene-btn-danger" data-i18n>STOP</button> <button id="scene-stop-btn" type="button" class="gev-action-button gev-action-button--danger scene-btn scene-btn-danger" data-i18n>STOP</button>
<button id="scene-next-btn" class="scene-btn" data-i18n>NEXT</button> <button id="scene-next-btn" type="button" class="gev-action-button scene-btn" data-i18n>NEXT</button>
</div> </div>
<div class="scene-controls"> <div class="scene-controls">
<button id="scene-export-btn" class="scene-btn" data-i18n>EXPORT PRESETS</button> <button id="scene-export-btn" type="button" class="gev-action-button scene-btn" data-i18n>EXPORT PRESETS</button>
<button id="scene-import-btn" class="scene-btn" data-i18n>IMPORT</button> <button id="scene-import-btn" type="button" class="gev-action-button scene-btn" data-i18n>IMPORT</button>
<button id="scene-download-btn" class="scene-btn" data-i18n>RUN LOG</button> <button id="scene-download-btn" type="button" class="gev-action-button scene-btn" data-i18n>RUN LOG</button>
<input type="file" id="scene-import-file" accept="application/json,.json" hidden /> <input type="file" id="scene-import-file" accept="application/json,.json" hidden />
</div> </div>
<div class="scene-progress"> <div class="scene-progress">
+1
View File
@@ -1,5 +1,6 @@
/* Preserve component and responsive rule order in one stylesheet entry. */ /* Preserve component and responsive rule order in one stylesheet entry. */
@import './src/ui/styles/foundation.css'; @import './src/ui/styles/foundation.css';
@import './src/ui/styles/action-button.css';
@import './src/ui/styles/slider.css'; @import './src/ui/styles/slider.css';
@import './src/ui/styles/controls.css'; @import './src/ui/styles/controls.css';
@import './src/ui/styles/location.css'; @import './src/ui/styles/location.css';