diff --git a/openpilot/selfdrive/carrot/README.md b/openpilot/selfdrive/carrot/README.md index d499db44fbd..f1be576f84f 100644 --- a/openpilot/selfdrive/carrot/README.md +++ b/openpilot/selfdrive/carrot/README.md @@ -405,6 +405,9 @@ web/js/realtime/ carrot_map.js home_drive.js hud_card.js + mini_hud.js + mini_hud_mode.js + mini_hud_model.js nav_hud.js raw_capnp.js raw_capnp_worker.js @@ -419,6 +422,9 @@ web/js/realtime/ | `realtime/app_realtime.js` | Live runtime polling/raw stream wiring and HUD bridge. | | `realtime/home_drive.js` | Drive/Carrot Vision renderer and overlay canvas. | | `realtime/hud_card.js` | HUD card data rendering. | +| `realtime/mini_hud_mode.js` | Compact HUD eligibility, viewport hysteresis, and activation events. | +| `realtime/mini_hud_model.js` | Pure STOCK/NAV/WAZE compact HUD data normalization. | +| `realtime/mini_hud.js` | Compact HUD DOM rendering and responsive typography. | | `realtime/carrot_map.js` | Kakao minimap iframe bridge (kmap), `FRAME_VERSION` gated to limit SDK quota. | | `realtime/nav_hud.js` | Carrot Nav HUD V2 — top-center guidance card from `carrotMan` state, no iframe/network. | | `realtime/raw_capnp.js` | Raw capnp decode entry. | @@ -428,6 +434,23 @@ web/js/realtime/ | `realtime/vision_raw.js` | Raw WebSocket client and worker bridge. | | `realtime/vision_diag.js` | Silent WebRTC/video diagnostic recorder (localStorage ring buffer) feeding the vision diag upload. | +### Compact HUD mode + +Compact HUD is an adaptive CWeb surface for resizable launcher panels and +Android multi-window use. By default it enters at viewport widths up to `480px` +when the viewport is at least 22% smaller than the exposed screen or outer +window. Widths up to `280px` are treated as unambiguously compact. The mode +stays active through `539px` and exits at `540px`. + +`?mini_hud=auto` bypasses the windowed-evidence check but keeps the width +thresholds. `?mini_hud=force` always enables the surface, and `?mini_hud=off` +disables adaptive detection. Browser JavaScript has no reliable equivalent of +Android `Activity.isInMultiWindowMode()`, so the default evidence check keeps +ordinary full-screen phones out while supporting launcher panels, popup +windows, desktop responsive windows, and multi-window layouts. While active, +raw HUD realtime remains connected and Vision/WebRTC is suspended until the +window leaves Compact HUD mode. + ### Translations JS ```text diff --git a/openpilot/selfdrive/carrot/web/css/base.css b/openpilot/selfdrive/carrot/web/css/base.css index 994cde8ce13..983a6dc4521 100644 --- a/openpilot/selfdrive/carrot/web/css/base.css +++ b/openpilot/selfdrive/carrot/web/css/base.css @@ -526,7 +526,7 @@ body:has(.page-transitioning) { line-height: 1.4; } -@media (max-width: 640px), (orientation: portrait) { +@media (max-width: 640px), (aspect-ratio < 13/10) { .settings-diff-summary { grid-template-columns: repeat(4, minmax(0, 1fr)); } diff --git a/openpilot/selfdrive/carrot/web/css/components.css b/openpilot/selfdrive/carrot/web/css/components.css index 7aa49b7224a..e15e98aa633 100644 --- a/openpilot/selfdrive/carrot/web/css/components.css +++ b/openpilot/selfdrive/carrot/web/css/components.css @@ -72,7 +72,17 @@ body[data-page="terminal"] .app-toast-host { .app-dialog { position: fixed; - inset: 0; + /* Anchor to the *visual* viewport, not the layout viewport. With the + on-screen keyboard open, inset:0 centered the sheet in the full-screen + height — so it floated up behind the keyboard (the near-square/landscape + case never hit the bottom-sheet fallback below). viewport.js keeps + --app-vv-top / --app-vv-height in sync with visualViewport, so centering + here always lands in the visible area above the keyboard. Same pattern as + .setting-search-panel. */ + top: var(--app-vv-top, 0px); + left: 0; + right: 0; + height: var(--app-vv-height, 100dvh); z-index: var(--z-modal); display: flex; align-items: center; @@ -334,7 +344,7 @@ body[data-page="terminal"] .app-toast-host { line-height: 1.4; } -@media (max-width: 640px) and (orientation: portrait) { +@media (max-width: 640px) and (aspect-ratio < 13/10) { .app-dialog { align-items: center; padding: calc(12px + env(safe-area-inset-top, 0px)) 8px calc(12px + env(safe-area-inset-bottom, 0px)); @@ -368,30 +378,14 @@ body[data-page="terminal"] .app-toast-host { min-height: 40px; } - .app-dialog--form { - align-items: flex-end; - padding: env(safe-area-inset-top, 0px) 0 0; - } - - .app-dialog--form .app-dialog__sheet { - width: 100vw; - max-height: min(72dvh, var(--dialog-sheet-mobile-max-height)); - padding: var(--sp-lg) var(--sp-lg) calc(var(--sp-lg) + env(safe-area-inset-bottom, 0px)); - border-right: 0; - border-bottom: 0; - border-left: 0; - border-radius: var(--dialog-sheet-radius-mobile) var(--dialog-sheet-radius-mobile) 0 0; - transform: translateY(24px); - } - - .app-dialog--form .app-dialog__actions { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - } - - .app-dialog--form .app-dialog__actions .smallBtn { - width: 100%; - } + /* Form dialogs (stream key, mapbox token, device actions, …) render as the + SAME centered floating card as every other dialog — no full-width bottom + sheet jammed onto the keyboard. They inherit the mobile card sheet above + (side margins, all corners rounded, full border) and the right-aligned + flex actions from .app-dialog__actions. Because .app-dialog is anchored to + --app-vv-height, the card floats just above the on-screen keyboard when one + is open, and simply centers when there is none — consistent across phone, + foldable and desktop (no keyboard-toolbar dependency). */ } .app-branch-picker__sheet { @@ -1069,6 +1063,27 @@ body[data-page="terminal"] .app-toast-host { margin-top: var(--sp-sm); } +/* Form dialogs anchor to the bottom of the visible area (which is + --app-vv-height = above the on-screen keyboard) so the floating card sits + just above the keyboard — the input auto-focuses and the keyboard opens + immediately — instead of floating centered far above it. The container's + bottom padding gives the breathing gap between card and keyboard. */ +/* Any dialog that shows a text input (prompt OR form) centers when no keyboard + is up (desktop, keyboard-less devices, or before the field focuses), and hugs + just above the on-screen keyboard with a consistent gap when it is — toggled + by html[data-kb-open] from viewport.js. --kb-inset is the exact keyboard + height incl. the Samsung toolbar on Chromium (VirtualKeyboard API), 0 + elsewhere (where --app-vv-height already excludes the keyboard). One shared + --kb-gap keeps this consistent with the terminal and search panel. */ +.app-dialog--input { + align-items: center; +} + +html[data-kb-open="1"] .app-dialog--input { + align-items: flex-end; + padding-bottom: calc(var(--kb-gap) + var(--kb-inset) + env(safe-area-inset-bottom, 0px)); +} + .app-dialog__inputError { color: var(--md-error); font-size: var(--fs-label-sm); diff --git a/openpilot/selfdrive/carrot/web/css/components/mini_hud.css b/openpilot/selfdrive/carrot/web/css/components/mini_hud.css new file mode 100644 index 00000000000..e3b9d7787bb --- /dev/null +++ b/openpilot/selfdrive/carrot/web/css/components/mini_hud.css @@ -0,0 +1,447 @@ +/* Compact HUD (mini_hud) — pixel-matched to carrot-hud-only-preview.html. + Intentionally SEPARATE from the green drive HUD (hud_card): this file owns the + compact/HUD-only surface only. The token model, colors, gradients, shapes and + spacing mirror the preview 1:1; the only differences from the preview are the + class namespace (carrot-mini-hud__*), the runtime var names (--mini-*, driven + by mini_hud.js applyLayout), and the fixed-inset window wrapper so the surface + fills a small/multi-window viewport. */ + +.carrot-mini-hud[hidden] { + display: none !important; +} + +.carrot-mini-hud { + /* Runtime-fit fonts/sizes (mini_hud.js applyLayout). Defaults mirror the + preview's .hudWrap token defaults. */ + --mini-ui-scale: 1; + --mini-top-font: 32px; + --mini-speed-font: 128px; + --mini-set-font: 60px; + --mini-speed-scale-x: 1; + --mini-set-scale-x: 1; + --mini-limit-size: 240px; + --mini-limit-font: 96px; + --mini-limit-label-font: 34px; + --mini-badge-font: 46px; + --mini-alert-font: 34px; + --mini-mode-font: 62px; + --mini-gear-font: 136px; + + --mini-radius: calc(20px * var(--mini-ui-scale)); + --mini-padding: calc(12px * var(--mini-ui-scale)); + --mini-section-gap: calc(10px * var(--mini-ui-scale)); + --mini-band-height: calc(54px * var(--mini-ui-scale)); + --mini-panel-top: rgba(13, 56, 38, 1); + --mini-panel-mid: rgba(18, 65, 44, 1); + --mini-panel-bottom: rgba(8, 31, 21, 1); + --mini-panel-border: rgba(255, 255, 255, 0.18); + --mini-text-shadow-strong: 0 1.4px 3.6px rgba(0, 0, 0, 0.94), 0 0 1.2px rgba(0, 0, 0, 0.62); + + position: fixed; + inset: 0; + z-index: 5000; + overflow: hidden; + display: flex; + flex-direction: column; + gap: 0; + padding: var(--mini-padding); + border: 1px solid var(--mini-panel-border); + border-radius: var(--mini-radius); + background: + linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.01) 38%, transparent 72%), + linear-gradient(145deg, var(--mini-panel-top), var(--mini-panel-mid) 58%, var(--mini-panel-bottom)); + box-shadow: 0 12px 20px rgba(0, 0, 0, 0.54); + color: #fff; + isolation: isolate; + contain: strict; + user-select: none; + touch-action: none; + pointer-events: auto; +} + +.carrot-mini-hud * { + box-sizing: border-box; +} + +.carrot-mini-hud::before { + content: ""; + position: absolute; + inset: 0; + background: + radial-gradient(circle at 16% 12%, rgba(255, 255, 255, 0.05), transparent 28%), + linear-gradient(180deg, rgba(255, 255, 255, 0.04), transparent 22%); + pointer-events: none; +} + +.carrot-mini-hud > * { + position: relative; + z-index: 1; +} + +/* ── Top metric bar (green) ──────────────────────────────────── */ +.carrot-mini-hud__top { + min-height: var(--mini-band-height); + flex: 0 0 var(--mini-band-height); + border-radius: 0; + background: #187047; + border: 0; + overflow: hidden; + display: flex; + align-items: stretch; + margin: + calc(var(--mini-padding) * -1) + calc(var(--mini-padding) * -1) + calc(var(--mini-section-gap) * 0.20); + padding: 0 var(--mini-padding); +} + +.carrot-mini-hud__top-cell { + flex: 1 1 0; + min-width: 0; + display: flex; + align-items: center; + justify-content: center; + padding: 0 calc(var(--mini-section-gap) * 0.24); + overflow: hidden; + color: #fff; + font-size: var(--mini-top-font); + font-weight: 950; + line-height: 1; + letter-spacing: 0; + text-shadow: var(--mini-text-shadow-strong); + white-space: nowrap; +} + +.carrot-mini-hud__top-cell + .carrot-mini-hud__top-cell { + border-left: 1px solid rgba(255, 255, 255, 0.24); +} + +/* ── Main region: limit | detail, or stock ───────────────────── */ +.carrot-mini-hud__main { + position: relative; + flex: 1 1 auto; + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 48fr) minmax(0, 52fr); + column-gap: calc(var(--mini-section-gap) * 1.10); + align-items: center; + padding: 0 calc(var(--mini-padding) * 0.18); +} + +.carrot-mini-hud__limit { + grid-column: 1; + grid-row: 1; + justify-self: stretch; + align-self: stretch; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + pointer-events: none; +} + +.carrot-mini-hud__limit-sign { + position: relative; + width: var(--mini-limit-size); + height: var(--mini-limit-size); + flex: 0 0 auto; + border-radius: 50%; + border: max(3px, calc(var(--mini-limit-size) * 0.095)) solid #ff3348; + background: radial-gradient(circle at 50% 48%, #ffffff 0 55%, #fff7f7 56% 100%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: calc(var(--mini-limit-size) * 0.025); + box-shadow: + 0 14px 30px rgba(0, 0, 0, 0.40), + 0 0 26px rgba(255, 51, 72, 0.30), + inset 0 0 0 2px rgba(255, 255, 255, 0.70); +} + +.carrot-mini-hud__limit-sign > span { + color: #050807; + font-size: var(--mini-limit-font); + font-weight: 900; + line-height: 0.92; + letter-spacing: 0; + text-shadow: none; + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.carrot-mini-hud__limit-sign > small { + color: #050807; + font-size: var(--mini-limit-label-font); + font-weight: 950; + line-height: 1; + letter-spacing: -0.02em; + white-space: nowrap; +} + +.carrot-mini-hud__limit-badge { + position: relative; + z-index: 1; + margin-top: calc(var(--mini-limit-size) * -0.08); + min-width: calc(var(--mini-limit-size) * 0.72); + height: calc(var(--mini-limit-size) * 0.30); + border-radius: calc(var(--mini-limit-size) * 0.045); + background: linear-gradient(180deg, #f04a23, #d93318); + display: grid; + place-items: center; + padding: 0 calc(var(--mini-limit-size) * 0.08); + box-shadow: 0 8px 18px rgba(0, 0, 0, 0.32); + color: #fff; + font-size: var(--mini-badge-font); + font-weight: 950; + line-height: 1; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.34); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.carrot-mini-hud__limit-badge[hidden], +.carrot-mini-hud__limit-sign > small[hidden] { + display: none !important; +} + +/* ── Detail (alert) zone: countdown / distance / type ────────── */ +.carrot-mini-hud__alert { + grid-column: 2; + grid-row: 1; + justify-self: stretch; + align-self: stretch; + min-width: 0; + min-height: 0; + display: flex; + flex-direction: column; + justify-content: center; + gap: calc(var(--mini-section-gap) * 1.90); + padding: + 0 + calc(var(--mini-padding) * 0.06) + 0 + clamp(6px, calc(var(--mini-padding) * 1.05), 24px); +} + +.carrot-mini-hud__alert > * { + min-width: 0; + color: #fff; + font-size: var(--mini-alert-font); + font-weight: 950; + line-height: 0.92; + letter-spacing: 0; + text-shadow: var(--mini-text-shadow-strong); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.carrot-mini-hud__alert > strong { + color: #ff6a76; +} + +.carrot-mini-hud[data-alert-kind="police"] .carrot-mini-hud__alert > strong, +.carrot-mini-hud[data-alert-kind="bump"] .carrot-mini-hud__alert > strong { + color: #ffc24b; +} + +/* ── Stock zone (source = STOCK): drive mode + gear ──────────── */ +.carrot-mini-hud__stock { + grid-column: 1 / -1; + grid-row: 1; + align-self: stretch; + min-width: 0; + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 48fr) minmax(0, 52fr); + align-items: center; +} + +.carrot-mini-hud__stock[hidden], +.carrot-mini-hud__limit[hidden], +.carrot-mini-hud__alert[hidden] { + display: none !important; +} + +.carrot-mini-hud__drive-mode, +.carrot-mini-hud__gear { + min-width: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.carrot-mini-hud__drive-mode { + width: 92%; + justify-self: center; + min-height: calc(var(--mini-mode-font) * 1.62); + padding: 0 calc(var(--mini-mode-font) * 0.18); + border: max(1px, calc(2px * var(--mini-ui-scale))) solid rgba(255, 255, 255, 0.28); + border-radius: 999px; + background: rgba(255, 255, 255, 0.035); + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.10); +} + +.carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="eco"] { + border-color: rgba(73, 221, 130, 0.42); +} + +.carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="safe"] { + border-color: rgba(120, 213, 238, 0.42); +} + +.carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="sport"] { + border-color: rgba(255, 102, 115, 0.42); +} + +.carrot-mini-hud__drive-mode > span { + max-width: 100%; + overflow: hidden; + color: #fff; + font-family: "Roboto Condensed", "Arial Narrow", "Noto Sans Condensed", "sans-serif-condensed", sans-serif; + font-size: var(--mini-mode-font); + font-stretch: condensed; + font-weight: 950; + line-height: 0.94; + letter-spacing: 0; + text-overflow: clip; + text-shadow: var(--mini-text-shadow-strong); + white-space: nowrap; +} + +.carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="eco"] > span { + color: #49dd82; +} + +.carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="safe"] > span { + color: #78d5ee; +} + +.carrot-mini-hud__drive-mode[data-mini-hud-drive-kind="sport"] > span { + color: #ff6673; +} + +.carrot-mini-hud__gear { + align-items: baseline; + gap: calc(4px * var(--mini-ui-scale)); + white-space: nowrap; +} + +.carrot-mini-hud__gear small { + color: rgba(255, 255, 255, 0.58); + font-size: calc(var(--mini-gear-font) * 0.34); + font-weight: 900; + line-height: 1; + text-shadow: var(--mini-text-shadow-strong); +} + +.carrot-mini-hud__gear small[hidden] { + display: none !important; +} + +.carrot-mini-hud__gear strong { + color: #fff; + font-size: var(--mini-gear-font); + font-weight: 950; + line-height: 0.9; + letter-spacing: 0; + font-variant-numeric: tabular-nums; + text-shadow: var(--mini-text-shadow-strong); +} + +/* ── Speed row: current speed (70%) | set speed (30%) ────────── */ +.carrot-mini-hud__speed-row { + flex: 0 0 34%; + min-height: 0; + display: grid; + grid-template-columns: minmax(0, 70fr) minmax(0, 30fr); + align-items: end; + column-gap: 0; + margin-bottom: calc(var(--mini-section-gap) * 0.26); +} + +.carrot-mini-hud__speed, +.carrot-mini-hud__set-speed { + min-width: 0; + width: 100%; + display: flex; + align-items: flex-end; + overflow: visible; + font-weight: 900; + letter-spacing: 0; + font-variant-numeric: tabular-nums; + text-shadow: 0 1.4px 3.6px rgba(0, 0, 0, 0.94); + white-space: nowrap; +} + +.carrot-mini-hud__speed { + justify-content: flex-start; + padding: + calc(var(--mini-padding) * 0.18) + calc(var(--mini-padding) * 0.10) + calc(var(--mini-padding) * 0.10) + calc(var(--mini-padding) * 0.16); + color: #fff; + font-size: var(--mini-speed-font); + line-height: 0.84; +} + +.carrot-mini-hud__set-speed { + justify-content: flex-end; + padding: + calc(var(--mini-padding) * 0.18) + calc(var(--mini-padding) * 0.16) + calc(var(--mini-padding) * 0.10) + calc(var(--mini-padding) * 0.10); + color: #f4f7fb; + font-size: var(--mini-set-font); + line-height: 0.92; +} + +.carrot-mini-hud__speed > span, +.carrot-mini-hud__set-speed > span { + display: inline-block; + line-height: inherit; + transform-origin: center bottom; + text-rendering: optimizeLegibility; +} + +.carrot-mini-hud__speed > span { + transform: scaleX(var(--mini-speed-scale-x)); +} + +.carrot-mini-hud__set-speed > span { + transform: scaleX(var(--mini-set-scale-x)); +} + +.carrot-mini-hud.is-three-digit .carrot-mini-hud__speed, +.carrot-mini-hud.is-three-digit .carrot-mini-hud__set-speed { + justify-content: center; + text-align: center; +} + +.carrot-mini-hud.is-three-digit .carrot-mini-hud__speed > span, +.carrot-mini-hud.is-three-digit .carrot-mini-hud__set-speed > span { + font-family: "Roboto Condensed", "Arial Narrow", "Noto Sans Condensed", "sans-serif-condensed", sans-serif; + font-stretch: condensed; +} + +/* ── Drive-page integration: while active, hide the normal CWeb UI ── */ +html[data-carrot-mini-hud="1"], +html[data-carrot-mini-hud="1"] body { + overflow: hidden !important; +} + +html[data-carrot-mini-hud="1"] body .topbar, +html[data-carrot-mini-hud="1"] body > .wrap, +html[data-carrot-mini-hud="1"] body #driveHudCard { + visibility: hidden !important; + pointer-events: none !important; +} + +html[data-carrot-mini-hud="1"] body #carrotMiniHud { + visibility: visible !important; + pointer-events: auto !important; +} diff --git a/openpilot/selfdrive/carrot/web/css/components/nav_hud.css b/openpilot/selfdrive/carrot/web/css/components/nav_hud.css index b86b415def2..bed337bce57 100644 --- a/openpilot/selfdrive/carrot/web/css/components/nav_hud.css +++ b/openpilot/selfdrive/carrot/web/css/components/nav_hud.css @@ -1,4 +1,4 @@ -/* +/* * Carrot Nav HUD * * Stable layout: @@ -440,13 +440,13 @@ color: #fff; } -@media (max-width: 980px) and (orientation: landscape) { +@media (max-width: 980px) and (min-aspect-ratio: 13/10), (max-width: 980px) and (horizontal-viewport-segments: 2), (max-width: 980px) and (vertical-viewport-segments: 2), (min-width: 640px) and (max-width: 980px) and (min-height: 650px) { .carrot-nav-hud { --nav-top: 52px; } } -@media (orientation: portrait) { +@media (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .carrot-nav-hud { display: none !important; } diff --git a/openpilot/selfdrive/carrot/web/css/layout_tokens.css b/openpilot/selfdrive/carrot/web/css/layout_tokens.css index 1dc0e8dc955..21a16e874da 100644 --- a/openpilot/selfdrive/carrot/web/css/layout_tokens.css +++ b/openpilot/selfdrive/carrot/web/css/layout_tokens.css @@ -39,7 +39,9 @@ } } -@media (orientation: landscape) { +/* Wide layout — offset page content for the left nav rail. Must track the + rail gate in responsive.css / viewport.js exactly. */ +@media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) { :root { --page-shell-margin-left: var(--app-nav-left-gap, var(--nav-rail-width)); --page-shell-margin-right: 0px; diff --git a/openpilot/selfdrive/carrot/web/css/pages/drive.css b/openpilot/selfdrive/carrot/web/css/pages/drive.css index 215654e184d..ba1520f2f9c 100644 --- a/openpilot/selfdrive/carrot/web/css/pages/drive.css +++ b/openpilot/selfdrive/carrot/web/css/pages/drive.css @@ -1,4 +1,4 @@ -/* ── WebRTC Video ─────────────────────────────────────────── */ +/* ?€?€ WebRTC Video ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ */ .rtcVideo { width: 100%; border-radius: var(--r-md); @@ -285,6 +285,10 @@ body[data-page="carrot"] #driveHudCard { transition: opacity 0.3s ease; } +.vision-start-overlay[hidden] { + display: none !important; +} + .vision-start-overlay__message { position: absolute; top: 50%; @@ -470,7 +474,7 @@ body[data-page="carrot"] #driveHudCard { .carrot-stage__loadingActions { width: 100%; display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 0.72fr); + grid-template-columns: minmax(0, 1fr); gap: 8px; margin-top: 2px; } @@ -483,12 +487,6 @@ body[data-page="carrot"] #driveHudCard { background: color-mix(in srgb, var(--md-surface-cont-h) 84%, transparent); } -.carrot-stage__loadingAction--primary { - border-color: color-mix(in srgb, var(--md-primary) 58%, var(--md-outline-var)); - background: color-mix(in srgb, var(--md-primary) 18%, var(--md-surface-cont-h)); - color: var(--md-primary); -} - .carrot-stage__loadingAction:disabled { opacity: 0.5; cursor: wait; @@ -547,7 +545,7 @@ body[data-page="carrot"] .carrot-hud-dock { overflow: hidden; } -@media (orientation: landscape) { +@media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) { .page--drive { display: block; height: var(--app-vv-height, 100dvh); @@ -680,7 +678,7 @@ body[data-page="carrot"] #driveHudCard.driveHudCard--overlay .hudWrap { background: transparent; } -/* Zoom-bias controls — bottom edge of the map dock. They nudge the global +/* Zoom-bias controls ??bottom edge of the map dock. They nudge the global zoom offset (added on top of the automatic speed-based zoom) so each device/resolution can be tuned. */ .carrot-map-zoom { @@ -773,7 +771,7 @@ body[data-page="carrot"] #driveHudCard.driveHudCard--overlay .hudWrap { display: none !important; } -@media (orientation: portrait) { +@media (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .carrot-map-dock { display: none !important; } @@ -817,7 +815,7 @@ body[data-page="carrot"] #driveHudCard.driveHudCard--loading { background: transparent; } -@media (orientation: landscape) { +@media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) { .rtcVideo { max-height: 40vh; width: auto; @@ -879,7 +877,7 @@ body[data-page="carrot"] #driveHudCard.driveHudCard--loading { z-index: 2; } -@media (orientation: portrait) { +@media (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .carrot-rtc-perf-summary { display: none !important; } diff --git a/openpilot/selfdrive/carrot/web/css/pages/logs.css b/openpilot/selfdrive/carrot/web/css/pages/logs.css index 0a9e6ffa3c4..76253a0098d 100644 --- a/openpilot/selfdrive/carrot/web/css/pages/logs.css +++ b/openpilot/selfdrive/carrot/web/css/pages/logs.css @@ -1,12 +1,12 @@ -/* ── Logs / Dashcam ───────────────────────────────────────── */ +/* ?€?€ Logs / Dashcam ?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€?€ */ .page--logs { - height: calc(var(--app-vv-height, 100dvh) - var(--nav-bar-height)); + height: calc(var(--app-vv-height, 100dvh) - var(--app-nav-bottom-gap, var(--nav-bar-height))); min-height: 0; background: var(--md-surface); overflow: hidden; } -@media (orientation: landscape) { +@media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) { .page--logs { height: var(--page-content-height, calc(var(--app-vv-height, 100dvh) - 24px)); } @@ -186,7 +186,7 @@ overscroll-behavior: contain; overflow-anchor: none; scrollbar-gutter: stable; - padding: 6px var(--logs-content-inline) calc(var(--nav-bar-height) + 16px + env(safe-area-inset-bottom, 0px)); + padding: 6px var(--logs-content-inline) calc(var(--app-nav-bottom-gap, var(--nav-bar-height)) + 16px + env(safe-area-inset-bottom, 0px)); display: flex; flex-direction: column; gap: 10px; @@ -427,7 +427,7 @@ .dashcam-segment-loader { flex: 0 0 auto; width: 100%; - /* Fixed height in both states — prevents layout shift / "shake" when the + /* Fixed height in both states ??prevents layout shift / "shake" when the loader toggles on near the bottom of the list. */ height: 22px; position: relative; @@ -584,7 +584,7 @@ transform: translateZ(0); } -/* Group (route) overflow menu — range select / sort. +/* Group (route) overflow menu ??range select / sort. Uses the shared boxed control shape in both portrait and landscape. Pushed to the right end in portrait. */ .dashcam-group-menu-btn { @@ -840,7 +840,7 @@ opacity: 0.62; } -@media (orientation: landscape) { +@media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) { .dashcam-share-card { gap: 8px; } @@ -1117,7 +1117,7 @@ overflow: auto; overscroll-behavior: contain; scrollbar-gutter: stable; - padding: 10px var(--logs-content-inline) calc(var(--nav-bar-height) + 16px + env(safe-area-inset-bottom, 0px)); + padding: 10px var(--logs-content-inline) calc(var(--app-nav-bottom-gap, var(--nav-bar-height)) + 16px + env(safe-area-inset-bottom, 0px)); display: flex; flex-direction: column; gap: 8px; @@ -1285,7 +1285,7 @@ } } -@media (orientation: landscape) { +@media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) { .dashcam-route-card { display: grid; grid-template-columns: minmax(230px, 32%) minmax(0, 1fr); @@ -1420,7 +1420,7 @@ } } -@media (orientation: landscape) and (max-height: 620px) { +@media (max-height: 620px) and (min-aspect-ratio: 13/10), (max-height: 620px) and (horizontal-viewport-segments: 2), (max-height: 620px) and (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) and (max-height: 620px) { .logs-tabbar { padding-top: 6px; padding-bottom: 4px; diff --git a/openpilot/selfdrive/carrot/web/css/pages/settings/device.css b/openpilot/selfdrive/carrot/web/css/pages/settings/device.css index f27e0d50f3f..60c70a29bea 100644 --- a/openpilot/selfdrive/carrot/web/css/pages/settings/device.css +++ b/openpilot/selfdrive/carrot/web/css/pages/settings/device.css @@ -1,4 +1,4 @@ -.app-dialog--settings-diff .app-dialog__sheet { +.app-dialog--settings-diff .app-dialog__sheet { width: min(calc(100vw - 24px), 520px); max-height: min(calc(100dvh - 24px), 680px); overflow: hidden; @@ -27,10 +27,10 @@ white-space: nowrap; } -/* setting-content-enter(opacity 0→1 + translateY 4px→0) 제거 — - 단계 전환이 끝나며(전환 중 `animation:none` 억제가 풀릴 때) 이 애니메이션이 재생돼 - 콘텐츠가 한 번 페이드(깜박)+4px 세로 점프(하단 위아래 흔들)하던 직접 원인이었다. - 좌우 슬라이드가 이미 화면 진입을 표현하므로 별도 콘텐츠 진입 애니메이션은 불필요하다. */ +/* setting-content-enter(opacity 0?? + translateY 4px??) ?쒓굅 ?? + ?④퀎 ?꾪솚???앸굹硫??꾪솚 以?`animation:none` ?듭젣媛€ ?€由??? ???좊땲硫붿씠?섏씠 ?ъ깮?? + 肄섑뀗痢좉? ??踰??섏씠??源쒕컯)+4px ?몃줈 ?먰봽(?섎떒 ?꾩븘???붾뱾)?섎뜕 吏곸젒 ?먯씤?댁뿀?? + 醫뚯슦 ?щ씪?대뱶媛€ ?대? ?붾㈃ 吏꾩엯???쒗쁽?섎?濡?蹂꾨룄 肄섑뀗痢?吏꾩엯 ?좊땲硫붿씠?섏? 遺덊븘?뷀븯?? */ .setting-search-result { width: 100%; @@ -112,7 +112,7 @@ pointer-events: none; } -@media (min-width: 769px) { +@media (min-width: 769px) and (min-aspect-ratio: 13/10), (min-width: 769px) and (horizontal-viewport-segments: 2), (min-width: 769px) and (vertical-viewport-segments: 2), (min-width: 769px) and (min-height: 650px) { .setting-search-panel { --setting-search-form-width: clamp(360px, 34vw, 540px); --setting-search-results-width: min(64vw, 820px); @@ -123,7 +123,7 @@ } } -@media (max-width: 640px) { +@media (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .page--setting .settingTop { flex-direction: column; align-items: stretch; @@ -152,13 +152,16 @@ } /* Portrait at ANY width (phone / foldable / tablet / wide window): the - group↔items slide in runSettingScreenSlide() assumes both screens are + group?봧tems slide in runSettingScreenSlide() assumes both screens are stacked as position:absolute; inset:0 inside the host so the transition is - pure transform. That overlap must exist for every portrait width — not just - ≤640px — otherwise the screens fall back to normal flow and the submenu + pure transform. That overlap must exist for every portrait width ??not just + ??40px ??otherwise the screens fall back to normal flow and the submenu appears to slide in vertically / only after the slide finishes. Landscape - has its own absolute-overlap block below, so the two together leave no gap. */ -@media (orientation: portrait) { + has its own absolute-overlap block below, so the two together leave no gap. + Narrow layout (aspect-ratio < 13/10) covers portrait phones AND near-square + foldables ??the complement of the wide (>= 13/10) split block below, so the + two stay mutually exclusive and exhaustive (no square-aspect gap). */ +@media (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .page--setting #settingScreenItems { padding-top: 0; } @@ -194,9 +197,9 @@ display: grid; } - /* ── 세로(phone): 설정을 뷰포트 고정 높이로, 두 화면을 absolute inset:0 로 '항상' 겹쳐 - 각자 내부 스크롤시킨다 → 단계 전환이 순수 transform(레이아웃 변화 0)이라 끝에 - 흔들림/깜박임이 구조적으로 생기지 않는다. (가로 split 모드와 동일 원리) ── */ + /* ?€?€ ?몃줈(phone): ?ㅼ젙??酉고룷??怨좎젙 ?믪씠濡? ???붾㈃??absolute inset:0 濡?'??긽' 寃뱀퀜 + 媛곸옄 ?대? ?ㅽ겕濡ㅼ떆?⑤떎 ???④퀎 ?꾪솚???쒖닔 transform(?덉씠?꾩썐 蹂€??0)?대씪 ?앹뿉 + ?붾뱾由?源쒕컯?꾩씠 援ъ“?곸쑝濡??앷린吏€ ?딅뒗?? (媛€濡?split 紐⑤뱶?€ ?숈씪 ?먮━) ?€?€ */ .page.page--setting { display: flex; flex-direction: column; @@ -229,8 +232,8 @@ will-change: transform; } - /* 최상위 그룹 화면은 탭과 첫 버튼을 선명하게 유지한다. - 스크롤 가장자리 페이드는 하위 설정 화면에만 적용한다. */ + /* 理쒖긽??洹몃9 ?붾㈃?€ ??낵 泥?踰꾪듉???좊챸?섍쾶 ?좎??쒕떎. + ?ㅽ겕濡?媛€?μ옄由??섏씠?쒕뒗 ?섏쐞 ?ㅼ젙 ?붾㈃?먮쭔 ?곸슜?쒕떎. */ .page.page--setting > #settingScreenHost > #settingScreenGroups { -webkit-mask-image: none; mask-image: none; @@ -251,7 +254,10 @@ } } -@media (orientation: landscape) { +/* Wide layout ??two-column split (group list | detail). Complement of the + narrow single-column block above; only engages at aspect-ratio >= 13/10 so + near-square foldables don't get the cramped split. */ +@media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) { .page.page--setting { --setting-landscape-left-width: clamp(208px, 28vw, 260px); --setting-landscape-gap: 12px; @@ -306,7 +312,7 @@ mask-image: none; } - /* 가로모드도 하위 설정 화면에만 스크롤 가장자리 페이드를 유지한다. */ + /* 媛€濡쒕え?쒕룄 ?섏쐞 ?ㅼ젙 ?붾㈃?먮쭔 ?ㅽ겕濡?媛€?μ옄由??섏씠?쒕? ?좎??쒕떎. */ .page.page--setting > #settingScreenHost > #settingScreenItems { -webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 6px), transparent 100%); mask-image: linear-gradient(to bottom, transparent 0, #000 12px, #000 calc(100% - 6px), transparent 100%); @@ -439,7 +445,7 @@ /* Flow the menu inside the page-anchored absolute layer instead of leaving it viewport-fixed. A position:fixed element re-anchors to the page's transform during page transitions (the page briefly gets will-change/transform), so - when the transition ends the FAB snaps by the nav-rail width — that is the + when the transition ends the FAB snaps by the nav-rail width ??that is the "jumps to another spot then returns" jitter. Page-relative flow stays put. */ .page.page--setting > .page-fab-layer--setting .setting-fab-menu { position: relative; @@ -503,7 +509,7 @@ } } -@media (orientation: landscape) and (max-width: 760px) { +@media (max-width: 760px) and (min-aspect-ratio: 13/10), (max-width: 760px) and (horizontal-viewport-segments: 2), (max-width: 760px) and (vertical-viewport-segments: 2), (min-width: 640px) and (max-width: 760px) and (min-height: 650px) { .page.page--setting { --setting-landscape-left-width: clamp(184px, 27vw, 220px); } @@ -536,7 +542,7 @@ } /* Long descriptions / range legends with no break opportunities (e.g. English - "0:stock,1,2:REC,..." — unlike CJK, Latin runs without spaces don't break) + "0:stock,1,2:REC,..." ??unlike CJK, Latin runs without spaces don't break) must wrap instead of forcing the row wider than the viewport, which would push the right-side control off-screen. CJK wraps per-character on its own, which is why this only surfaced in English. The landscape split layout @@ -873,8 +879,8 @@ color: var(--md-on-surface); } -/* Footer actions row — holds the optional unit-cycle (배율) and the - reset-to-default (기본값) button, right-aligned under the item. */ +/* Footer actions row ??holds the optional unit-cycle (諛곗쑉) and the + reset-to-default (湲곕낯媛? button, right-aligned under the item. */ .page--setting #settingScreenItems .setting--has-actions { padding-bottom: var(--sp-md); } @@ -1336,22 +1342,22 @@ text-align: left; } -@media (max-width: 640px) { +@media (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .page--setting #settingScreenItems .ctrl { justify-content: center; } } -@media (orientation: landscape) and (max-width: 760px) { +@media (max-width: 760px) and (min-aspect-ratio: 13/10), (max-width: 760px) and (horizontal-viewport-segments: 2), (max-width: 760px) and (vertical-viewport-segments: 2), (min-width: 640px) and (max-width: 760px) and (min-height: 650px) { .page--setting #settingScreenItems .ctrl { justify-content: start; } } -@media (max-width: 640px) and (orientation: portrait) { +@media (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .page--setting #settingScreenItems > .row-between.mb-sm { /* min-height was 48px while the row content (42px back-icon + title) is - shorter, and align-items:end pushed the ~6px slack ABOVE the title — + shorter, and align-items:end pushed the ~6px slack ABOVE the title ?? the thin band the user saw. Collapse to content height so the title starts at the top edge, like the groups screen. */ min-height: 0; @@ -1637,7 +1643,7 @@ overflow: hidden; } -@media (max-width: 640px) { +@media (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .page--setting #deviceItems .device-ssh-control { grid-column: 1 / -1; width: 100%; @@ -1900,12 +1906,12 @@ /* Portrait (phone): anchor the settings FAB as page-relative ABSOLUTE flow, the same approach landscape already uses. As position:fixed the FAB was captured by .page-active's transform (an ancestor transform is the - containing block for fixed children), so it followed the page geometry — - --app-vv-height settling on load, page transitions — and drifted up/down. + containing block for fixed children), so it followed the page geometry ?? + --app-vv-height settling on load, page transitions ??and drifted up/down. As a normal absolute child of the page it simply sits at the page's bottom-right and stays put. The page already excludes the nav bar + safe area from its height, so the offset here is just the small visual gap. */ -@media (max-width: 640px) and (orientation: portrait) { +@media (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .page.page--setting > .page-fab-layer--setting { position: absolute; top: auto; diff --git a/openpilot/selfdrive/carrot/web/css/pages/settings/panels.css b/openpilot/selfdrive/carrot/web/css/pages/settings/panels.css index 1d1cd084635..e4ffdc8dc7c 100644 --- a/openpilot/selfdrive/carrot/web/css/pages/settings/panels.css +++ b/openpilot/selfdrive/carrot/web/css/pages/settings/panels.css @@ -18,7 +18,7 @@ align-items: center; justify-content: flex-start; gap: 14px; - padding: calc(18px + env(safe-area-inset-top, 0px)) var(--sp-lg) calc(var(--app-nav-bottom-gap, var(--nav-bar-height)) + 18px + env(safe-area-inset-bottom, 0px)); + padding: calc(18px + env(safe-area-inset-top, 0px)) var(--sp-lg) calc(var(--app-nav-bottom-gap, var(--nav-bar-height)) + 18px + var(--kb-inset) + env(safe-area-inset-bottom, 0px)); pointer-events: none; } diff --git a/openpilot/selfdrive/carrot/web/css/pages/settings/youtube_live.css b/openpilot/selfdrive/carrot/web/css/pages/settings/youtube_live.css index 104bf0ef201..5cda71ba69e 100644 --- a/openpilot/selfdrive/carrot/web/css/pages/settings/youtube_live.css +++ b/openpilot/selfdrive/carrot/web/css/pages/settings/youtube_live.css @@ -1,4 +1,4 @@ -.youtube-live-section { +.youtube-live-section { display: grid; gap: var(--sp-md); margin: 0; @@ -91,7 +91,7 @@ box-shadow: none; } -@media (max-width: 430px) { +@media (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .youtube-live-metrics { grid-template-columns: 1fr; } diff --git a/openpilot/selfdrive/carrot/web/css/pages/terminal.css b/openpilot/selfdrive/carrot/web/css/pages/terminal.css index 781f1a653a1..e298946eb79 100644 --- a/openpilot/selfdrive/carrot/web/css/pages/terminal.css +++ b/openpilot/selfdrive/carrot/web/css/pages/terminal.css @@ -1,4 +1,4 @@ -.page--terminal { +.page--terminal { --terminal-bottom-gap: calc(var(--nav-bar-height) + env(safe-area-inset-bottom, 0px)); --terminal-form-bottom: calc(8px + env(safe-area-inset-bottom, 0px)); --terminal-inline: max(var(--sp-lg), calc((100vw - var(--content-max-width)) / 2)); @@ -303,7 +303,7 @@ } } -@media (orientation: landscape) { +@media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) { body[data-page="terminal"] .app-toast-host { left: var(--terminal-toast-left, calc(var(--nav-rail-width) + 20px)); right: auto; diff --git a/openpilot/selfdrive/carrot/web/css/pages/tools/main.css b/openpilot/selfdrive/carrot/web/css/pages/tools/main.css index 2f27db66057..201ec59e3fd 100644 --- a/openpilot/selfdrive/carrot/web/css/pages/tools/main.css +++ b/openpilot/selfdrive/carrot/web/css/pages/tools/main.css @@ -1,4 +1,4 @@ -@media (max-width: 640px), (orientation: portrait) { +@media (max-width: 640px), (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .app-dialog--web-settings .app-dialog__sheet { --web-settings-sheet-width: 420px; --web-settings-sheet-height: clamp(440px, 72dvh, 560px); @@ -76,7 +76,7 @@ } } -@media (max-height: 430px) and (orientation: landscape) { +@media (max-height: 430px) and (min-aspect-ratio: 13/10) { .app-dialog--web-settings .app-dialog__sheet { --web-settings-sheet-height: calc(100dvh - 20px); --web-settings-row-min: 56px; @@ -167,7 +167,7 @@ /* One-shot fold (same pattern as the settings profile section). JS adds the motion class then removes it after the run, so the finish lands on the - static open/closed state instead of a lingering transition — that lingering + static open/closed state instead of a lingering transition ??that lingering transition was what made the finish look off with taller (2-line) buttons. */ .tools-group.is-collapsing > .tools-group__body { animation: tools-group-fold var(--motion-medium) var(--ease-emphasized) both; @@ -842,7 +842,7 @@ } /* Tools notifications: landscape keeps commands left and the log dock right. */ -@media (orientation: landscape) { +@media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) { .page--tools { --tools-detail-duration: 340ms; --tools-log-scroll-gap: 10px; @@ -1107,7 +1107,7 @@ } /* Tools notifications: portrait becomes a page-contained notification center. */ -@media (orientation: portrait) { +@media (aspect-ratio < 13/10) and (max-width: 639px), (aspect-ratio < 13/10) and (max-height: 649px) { .page--tools { --tools-detail-duration: 360ms; --tools-log-scroll-gap: 6px; diff --git a/openpilot/selfdrive/carrot/web/css/responsive.css b/openpilot/selfdrive/carrot/web/css/responsive.css index efe7d055522..d4502d39092 100644 --- a/openpilot/selfdrive/carrot/web/css/responsive.css +++ b/openpilot/selfdrive/carrot/web/css/responsive.css @@ -32,7 +32,7 @@ margin-left: 0; } - .page { + .page:not(.page--full):not(.page--setting) { padding-bottom: var(--page-bottom-gutter, calc(var(--nav-bar-height-desktop) + 22px + env(safe-area-inset-bottom, 0px))); min-height: var(--page-min-height, calc(var(--app-vv-height, 100dvh) - var(--nav-bar-height-desktop) - env(safe-area-inset-bottom, 0px))); } @@ -71,7 +71,9 @@ } -@media (orientation: landscape) { +/* Wide layout: left nav rail. Tracks viewport.js LAYOUT_WIDE_QUERY: + landscape-ish viewports, fold/viewport segments, and large fold/tablet panels. */ +@media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) { :root { --nav-bar-height: 0px; --nav-rail-width: 92px; diff --git a/openpilot/selfdrive/carrot/web/css/tokens.css b/openpilot/selfdrive/carrot/web/css/tokens.css index 7187fb9daa9..74abcd18e85 100644 --- a/openpilot/selfdrive/carrot/web/css/tokens.css +++ b/openpilot/selfdrive/carrot/web/css/tokens.css @@ -105,6 +105,17 @@ --dialog-sheet-max-height: calc(100dvh - var(--sp-lg) * 2); --dialog-sheet-mobile-max-height: min(88dvh, 720px); + /* ── On-screen keyboard ─────────────────────────────────────── + --kb-inset: exact keyboard height (INCLUDING the Samsung suggestion/autofill + toolbar) on Chromium via the VirtualKeyboard API (viewport.js opts in); + resolves to 0 on iOS Safari / Firefox / desktop, where the visualViewport + path handles the keyboard instead. + --kb-gap: the breathing space every keyboard-anchored surface keeps above + the keyboard. One source of truth so dialogs, terminal and the settings + search panel are consistent. */ + --kb-inset: env(keyboard-inset-height, 0px); + --kb-gap: 36px; + /* ── Popup surface ──────────────────────────────────────────── One surface for every floating popup (dialog sheet, dropdown menu, popover) so they share background / border / shadow / diff --git a/openpilot/selfdrive/carrot/web/index.html b/openpilot/selfdrive/carrot/web/index.html index 6f6404c54f4..37816d76825 100644 --- a/openpilot/selfdrive/carrot/web/index.html +++ b/openpilot/selfdrive/carrot/web/index.html @@ -3,6 +3,12 @@ + CarrotPilot @@ -11,9 +17,12 @@ var primaryPages = { carrot: 1, setting: 1, tools: 1, logs: 1, terminal: 1 }; var startPage = "carrot"; try { + var miniHudRequested = /(?:^|[?&])mini_hud=(?:auto|force|1)(?:&|$)/.test(window.location.search || ""); var webSettings = window.__CARROT_BOOTSTRAP__ && window.__CARROT_BOOTSTRAP__.webSettings; var setting = (webSettings && webSettings.start_page) || "last"; - if (setting === "last") { + if (miniHudRequested) { + startPage = "carrot"; + } else if (setting === "last") { var lastPage = localStorage.getItem("carrot_web_last_page"); startPage = primaryPages[lastPage] ? lastPage : "carrot"; } else { @@ -39,6 +48,23 @@ s.setProperty("--app-vv-height", h + "px"); s.setProperty("--app-vv-width", w + "px"); s.setProperty("--app-vv-top", t + "px"); + // Mirror the coarse layout mode before first paint so CSS attribute / + // container hooks (and JS read on early pages) match viewport.js. + var lw = window.innerWidth || w; + var lh = window.innerHeight || h; + var ratio = lh > 0 ? lw / lh : 1; + var segmented = false; + try { + segmented = + (window.viewport && Array.isArray(window.viewport.segments) && window.viewport.segments.length > 1) || + (window.matchMedia && ( + window.matchMedia("(horizontal-viewport-segments: 2)").matches || + window.matchMedia("(vertical-viewport-segments: 2)").matches + )); + } catch (e) {} + var wide = ratio >= 1.3 || segmented || (lw >= 640 && lh >= 650); + document.documentElement.dataset.layout = + wide ? "wide" : (ratio >= 0.92 ? "square" : "tall"); } catch (e) {} })(); @@ -67,7 +93,7 @@ display: block !important; } - @media (orientation: landscape) { + @media (min-aspect-ratio: 13/10), (horizontal-viewport-segments: 2), (vertical-viewport-segments: 2), (min-width: 640px) and (min-height: 650px) { html[data-carrot-start-page="tools"]:not([data-carrot-bootstrapped]) #pageTools { display: grid !important; } @@ -89,26 +115,27 @@ background: color-mix(in srgb, var(--md-primary) 14%, transparent) !important; } - - + + - + - + - - + + - - - + + + - - - - + + + + + @@ -491,7 +518,6 @@

Home

-
@@ -512,6 +538,47 @@

Home

+ +