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)
Changes
@@@ -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(saffont-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; }@@@ -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]');
@@ -82,12 +82,13 @@ main { padding: 1rem max(1rem, env(safe-area-inset-right)) calc(1.5rem + env(saf
@@ -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; }
.online[data-state="connecting"] .dot { animation: pulse 1s ease-in-out infinite; }
.notice { margin: 0 0 0.75rem; }
.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; }
.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 { 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-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. */
/* 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); }
.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
@@ -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; }
font-weight: 600; user-select: none; -webkit-user-select: none; touch-action: none; }
.pad-button kbd { font-weight: 400; color: var(--muted); }
.pad-button kbd { font-weight: 400; color: var(--muted); }
.pad-button.held kbd { color: inherit; border-color: currentColor; background: transparent; }
.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); }
.widget-video_click h2 { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
[data-video-click] { cursor: crosshair; }
[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; }
.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
@@ -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; }
.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); }
[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. */
/* 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) {
@media (orientation: landscape) and (max-height: 32rem) {
.estop-bar { padding-top: 0.3rem; padding-bottom: 0.35rem; } .topbar { min-height: 2.5rem; }
.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
@@ -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. */
/* 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) {
@media (min-width: 44rem) and (min-height: 32.01rem), (min-width: 44rem) and (orientation: portrait) {
.widgets { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.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) {
@media (min-width: 64rem) and (min-height: 32.01rem) {
.widgets { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.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
@@ -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; }
.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. */
/* 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-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-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; }
.camera-screen.sim-live > .camera-icon, .camera-screen.sim-live > .camera-title, .camera-screen.sim-live > .camera-sub { display: none; }
@@ -35,6 +35,15 @@
@@ -35,6 +35,15 @@
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
const $$ = (sel, root = document) => [...root.querySelectorAll(sel)];
const wsUrl = (path = '/control') => `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}${path}`;
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]')) {
for (const b of $$('[data-copy]')) {
b.addEventListener('click', () => {
b.addEventListener('click', () => {
const src = $('[data-copy-source]');
const src = $('[data-copy-source]');