openvibers/openvibe.bot Commits

Bot panel on a phone: the camera stays pinned under the e-stop bar while the controls scroll, and the drive pad comes right under it (it was last, below every readout), so you can watch while you drive (#132)

OP OpenVibers committed 28 days ago · 457b59ce62 · 2 files, +29 −7

Changes

ddiff --git a/public/panel.css b/public/panel.css
iindex 67e1834..689914d 100644
---- a/public/panel.css
++++ b/public/panel.css
@@@ -82,12 +82,13 @@ main { padding: 1rem max(1rem, env(safe-area-inset-right)) calc(1.5rem + env(saf
.online[data-state="connecting"] .dot { animation: pulse 1s ease-in-out infinite; }
.notice { margin: 0 0 0.75rem; }
--/* Widgets: one column on a phone held upright, with the camera first and the drive pad last (thumb reach). */
++/* Widgets: one column on a phone held upright. The camera comes first and the drive pad right under it, so the
++ picture and the controls are on screen together; the readouts and the other controls follow. */
.widgets { display: grid; gap: 0.75rem; grid-template-columns: minmax(0, 1fr); grid-auto-flow: dense; }
.widget { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius); padding: 0.9rem; min-width: 0; }
--.widget-camera { order: -1; padding: 0; overflow: hidden; }
++.widget-camera { order: -2; padding: 0; overflow: hidden; }
.widget-camera h2 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
--.widget-drive { order: 1; }
++.widget-drive { order: -1; }
/* Camera tiles: one per camera the profile lists. */
.cameras { display: grid; gap: 2px; grid-template-columns: repeat(auto-fit, minmax(14rem, 1fr)); background: var(--line); }
@@@ -148,7 +149,7 @@ main { padding: 1rem max(1rem, env(safe-area-inset-right)) calc(1.5rem + env(saf
font-weight: 600; user-select: none; -webkit-user-select: none; touch-action: none; }
.pad-button kbd { font-weight: 400; color: var(--muted); }
.pad-button.held kbd { color: inherit; border-color: currentColor; background: transparent; }
--.widget-video_click { order: -1; padding: 0; overflow: hidden; }
++.widget-video_click { order: -2; padding: 0; overflow: hidden; }
.widget-video_click h2 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
[data-video-click] { cursor: crosshair; }
.inline-form { display: flex; gap: 0.5rem; flex-wrap: wrap; } .inline-form > input, .inline-form > select { flex: 1; min-width: 8rem; }
@@@ -162,6 +163,19 @@ main { padding: 1rem max(1rem, env(safe-area-inset-right)) calc(1.5rem + env(saf
.stat-label { color: var(--muted); font-size: 0.72rem; }
[data-quality="good"] { color: var(--ok); } [data-quality="fair"] { color: var(--warn); } [data-quality="poor"] { color: var(--danger); }
++/* On a phone the picture never scrolls away while driving: the camera stays pinned under the e-stop bar (its height,
++ --banner-h, is measured by public/panel.js) and everything else scrolls beneath it. Upright, it takes at most a
++ third of the screen, and several cameras sit side by side and swipe instead of stacking. */
++@media (max-width: 43.99rem) and (orientation: portrait), (orientation: landscape) and (max-height: 32rem) {
++ .widget-camera, .widget-video_click { position: sticky; top: calc(var(--banner-h, 6.5rem) + 0.4rem); z-index: 6; align-self: start;
++ box-shadow: 0 0.75rem 1.5rem -0.75rem rgba(0, 0, 0, 0.85); }
++}
++@media (max-width: 43.99rem) and (orientation: portrait) {
++ .widget-camera .camera-screen, .widget-video_click .camera-screen { max-height: 34vh; }
++ .widget-camera .cameras { grid-auto-flow: column; grid-template-columns: none; grid-auto-columns: 100%; overflow-x: auto; scroll-snap-type: x mandatory; overscroll-behavior-x: contain; }
++ .widget-camera .camera { scroll-snap-align: start; }
++ .stick { width: min(13rem, 30vh); }
++}
/* Phone on its side: the camera on the left, the drive pad on the right, both in view without scrolling. */
@media (orientation: landscape) and (max-height: 32rem) {
.estop-bar { padding-top: 0.3rem; padding-bottom: 0.35rem; } .topbar { min-height: 2.5rem; }
@@@ -176,8 +190,7 @@ main { padding: 1rem max(1rem, env(safe-area-inset-right)) calc(1.5rem + env(saf
/* Tablet and up: more columns, the camera large and the drive pad beside it. */
@media (min-width: 44rem) and (min-height: 32.01rem), (min-width: 44rem) and (orientation: portrait) {
.widgets { grid-template-columns: repeat(2, minmax(0, 1fr)); }
-- .widget-camera { grid-column: 1 / -1; order: 0; }
-- .widget-drive { order: 0; }
++ .widget-camera { grid-column: 1 / -1; }
}
@media (min-width: 64rem) and (min-height: 32.01rem) {
.widgets { grid-template-columns: repeat(3, minmax(0, 1fr)); }
@@@ -287,7 +300,7 @@ main { padding: 1rem max(1rem, env(safe-area-inset-right)) calc(1.5rem + env(saf
.embed-sign-in { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
/* A simulated rover's camera (public/sim-view.js): the drawn view fills the tile, the arena map sits in a corner. */
--.camera-screen > .sim-camera { position: absolute; inset: 0; width: 100%; height: 100%; display: block; background: #000; }
++.camera-screen > .sim-camera { position: absolute; inset: 0; width: 100%; height: 100%; display: block; background: #000; object-fit: contain; }
.camera-screen > .sim-map { position: absolute; right: 0.5rem; bottom: 0.5rem; width: clamp(6.5rem, 26%, 12rem); height: auto; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.18); }
.camera-screen > .sim-hud { position: absolute; left: 0.6rem; bottom: 0.6rem; padding: 0.15rem 0.5rem; border-radius: 6px; background: rgba(0, 0, 0, 0.55); color: #e6edf3; font: 600 0.8rem ui-monospace, monospace; font-variant-numeric: tabular-nums; }
.camera-screen.sim-live > .camera-icon, .camera-screen.sim-live > .camera-title, .camera-screen.sim-live > .camera-sub { display: none; }
ddiff --git a/public/panel.js b/public/panel.js
iindex 193655e..0b2062c 100644
---- a/public/panel.js
++++ b/public/panel.js
@@@ -35,6 +35,15 @@
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
const wsUrl = (path = '/control') => `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}${path}`;
++ // The camera pins under the sticky e-stop bar on phones (public/panel.css): the bar's height, kept current as it
++ // wraps, rotates or shows the latched note.
++ const banner = $('[data-estop-banner]');
++ if (banner) {
++ const setTop = () => document.documentElement.style.setProperty('--banner-h', `${banner.offsetHeight}px`);
++ setTop();
++ if ('ResizeObserver' in window) new ResizeObserver(setTop).observe(banner); else window.addEventListener('resize', setTop);
++ }
++
for (const b of $$('[data-copy]')) {
b.addEventListener('click', () => {
const src = $('[data-copy-source]');