refactor: compose UI shell with explicit application operations and component owners

This commit is contained in:
Sameh Khamis committed 2026-09-13 04:26:09 -07:00
1 parent 8a226a2fe3
commit f5ef9091f0
67 files changed
+15799 -15059

No files matched your search

+315
View File
@@ -0,0 +1,315 @@
/* ── First-run mission launcher ───────────────────────────────────
Compact and non-modal: it gives a new visitor a meaningful starting
choice without blocking manual map interaction or spending API quota. */
#first-run-launcher {
position: fixed;
z-index: 175;
top: 50%;
left: 50%;
width: min(34rem, calc(100vw - 2rem));
/* Five tiles do not fit a short landscape phone (667 x 390), where the card
would otherwise render past BOTH ends of the viewport. Cap it to the
viewport and let the mission list scroll inside — the heading, the checkbox
and the status line must stay reachable at every height. */
max-height: calc(100vh - 1.5rem);
max-height: calc(100dvh - 1.5rem);
display: flex;
flex-direction: column;
padding: 1.15rem;
overflow: hidden;
color: var(--text-primary);
background:
linear-gradient(145deg, rgba(10, 27, 37, 0.97), rgba(7, 13, 22, 0.95)),
rgba(7, 17, 25, 0.96);
border: 1px solid rgba(54, 220, 255, 0.42);
border-radius: 0.65rem;
box-shadow:
0 1.4rem 4.5rem rgba(0, 0, 0, 0.72),
0 0 2.5rem rgba(0, 212, 255, 0.09),
inset 0 0 0 1px rgba(255, 255, 255, 0.025);
backdrop-filter: blur(28px) saturate(1.25);
-webkit-backdrop-filter: blur(28px) saturate(1.25);
opacity: 0;
transform: translate(-50%, calc(-50% + 0.8rem)) scale(0.985);
transition: opacity 220ms ease, transform 280ms cubic-bezier(0.2, 0.8, 0.2, 1);
/* Inert until revealed AND again while fading out, so a click during the
dismiss animation cannot launch a second mission behind the fade. */
pointer-events: none;
}
/* An AUTHOR `display` on this id beats the UA's `[hidden] { display: none }` in
the cascade, so the flex above would otherwise leave the card in the
accessibility tree from page parse until it is revealed — or, on a share
link, until it is removed. Restore the attribute's meaning explicitly. */
#first-run-launcher[hidden] {
display: none;
}
#first-run-launcher.visible {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
pointer-events: auto;
}
.first-run-scanline {
position: absolute;
inset: 0 0 auto;
height: 2px;
background: linear-gradient(90deg, transparent, rgba(54, 220, 255, 0.8), transparent);
box-shadow: 0 0 1rem rgba(54, 220, 255, 0.45);
pointer-events: none;
}
.first-run-header,
.first-run-footer {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
color: rgba(171, 210, 220, 0.6);
font-family: var(--font-mono);
font-size: 0.58rem;
letter-spacing: 0.1rem;
text-transform: uppercase;
}
.first-run-kicker { color: rgba(54, 220, 255, 0.86); }
#first-run-launcher h2 {
margin-top: 0.6rem;
font-family: var(--font-mono);
font-size: clamp(1.25rem, 2.4vw, 1.75rem);
font-weight: 500;
letter-spacing: -0.045rem;
}
#first-run-description {
max-width: 29rem;
margin-top: 0.3rem;
color: rgba(215, 231, 235, 0.66);
font-size: 0.78rem;
line-height: 1.5;
}
.first-run-choices {
display: grid;
gap: 0.42rem;
margin-top: 1rem;
/* The one part of the card allowed to scroll when the viewport is too short.
min-height:0 is what actually lets this flex child shrink below its content. */
min-height: 0;
overflow-y: auto;
overscroll-behavior: contain;
scroll-behavior: smooth;
/* A visible, non-overlaying affordance that the list continues. */
scrollbar-width: thin;
scrollbar-color: rgba(54, 220, 255, 0.45) transparent;
scrollbar-gutter: stable;
}
.first-run-choices::-webkit-scrollbar { width: 6px; }
.first-run-choices::-webkit-scrollbar-thumb {
background: rgba(54, 220, 255, 0.45);
border-radius: 3px;
}
.first-run-choices button {
display: grid;
grid-template-columns: 1.7rem 1fr 1.3rem;
align-items: center;
gap: 0.7rem;
width: 100%;
min-height: 3.55rem;
padding: 0.6rem 0.7rem;
color: rgba(225, 244, 248, 0.92);
text-align: left;
background: rgba(29, 69, 81, 0.18);
border: 1px solid rgba(100, 200, 220, 0.18);
border-radius: 0.42rem;
cursor: pointer;
transition: background var(--transition-fast), border-color var(--transition-fast), transform var(--transition-fast);
}
.first-run-choices button:hover,
.first-run-choices button:focus-visible {
background: rgba(25, 151, 178, 0.16);
border-color: rgba(54, 220, 255, 0.62);
outline: none;
transform: translateX(2px);
}
/* aria-disabled, not :disabled — the JS keeps the buttons focusable while a
mission runs so the keyboard is never dumped out of the launcher mid-flight. */
.first-run-choices button[aria-disabled='true'] {
cursor: wait;
opacity: 0.48;
transform: none;
pointer-events: none;
}
.first-run-choices button > .material-symbols-outlined:first-child {
color: #55dff5;
font-size: 1.35rem;
}
.first-run-choices button > span:nth-child(2) {
display: flex;
flex-direction: column;
gap: 0.15rem;
}
.first-run-choices strong {
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.08rem;
}
.first-run-choices small {
color: rgba(198, 220, 226, 0.55);
font-size: 0.66rem;
}
.first-run-arrow {
color: rgba(54, 220, 255, 0.4);
font-size: 1rem;
transition: transform var(--transition-fast), color var(--transition-fast);
}
.first-run-choices button:hover .first-run-arrow,
.first-run-choices button:focus-visible .first-run-arrow {
color: rgba(54, 220, 255, 0.9);
transform: translateX(2px);
}
/* Ordered after the shared button rule rather than shouting !important at it. */
.first-run-choices button.first-run-explore {
background: rgba(255, 255, 255, 0.025);
}
.first-run-footer {
margin-top: 0.85rem;
font-size: 0.48rem;
letter-spacing: 0.055rem;
}
.first-run-suppress {
display: flex;
align-items: center;
gap: 0.4rem;
color: rgba(199, 225, 231, 0.66);
text-transform: none;
font-size: 0.62rem;
letter-spacing: 0.02rem;
cursor: pointer;
}
.first-run-suppress input {
width: 0.82rem;
height: 0.82rem;
margin: 0;
accent-color: #36dcff;
cursor: pointer;
}
.first-run-suppress:hover { color: rgba(225, 244, 248, 0.92); }
/* The live region carries mission progress and failures, so it owns a full row
and reserves its height — otherwise the card resizes as it speaks. */
.first-run-note {
min-height: 1.1rem;
margin-top: 0.5rem;
color: rgba(199, 225, 231, 0.62);
font-family: var(--font-mono);
font-size: 0.62rem;
line-height: 1.35;
}
#first-run-launcher[data-state='loading'] .first-run-note {
color: rgba(54, 220, 255, 0.85);
}
/* Every surface that claims the screen hides the launcher. This list is the CSS
half of the ESC arbitration: src/firstRunExperience.js watches these same
classes and makes the launcher step aside, so a hidden card is never left
holding a keyboard handler. Keep the two lists in step —
EXCLUSIVE_SURFACE_CLASSES pins them together. */
body.ui-clean-view #first-run-launcher,
body.recording-mode #first-run-launcher,
body.cockpit-mode #first-run-launcher,
body.scene-playback-mode #first-run-launcher {
display: none;
}
@media (max-width: 620px) {
#first-run-launcher {
top: auto;
bottom: 5.8rem;
padding: 0.9rem;
transform: translate(-50%, 0.8rem) scale(0.985);
}
#first-run-launcher.visible { transform: translate(-50%, 0) scale(1); }
/* Five tiles have to clear the dock on a 375 x 812 phone, so the rows tighten
and the redundant "ESC to dismiss" hint (no ESC key on a phone) drops. */
#first-run-launcher h2 { margin-top: 0.45rem; }
#first-run-description { margin-top: 0.25rem; font-size: 0.72rem; }
.first-run-choices { margin-top: 0.75rem; }
.first-run-choices button { min-height: 3rem; padding: 0.45rem 0.6rem; }
.first-run-choices small { font-size: 0.62rem; }
.first-run-footer > span:last-child { display: none; }
}
/* Short viewports — landscape phones, small laptop windows. The chrome tightens
so the mission list keeps a usable share of the height. The intro line STAYS:
it is the only copy that says what this thing is, and with four tiles it
fits. (It was dropped here while there were five; the tile that went is
exactly why it can come back.) */
@media (max-height: 620px) {
#first-run-launcher { padding: 0.75rem 0.85rem; }
#first-run-launcher h2 { margin-top: 0.3rem; font-size: 1.1rem; }
#first-run-description { margin-top: 0.2rem; font-size: 0.7rem; line-height: 1.4; }
.first-run-choices { margin-top: 0.55rem; }
.first-run-choices button { min-height: 2.7rem; padding: 0.36rem 0.6rem; }
.first-run-choices strong { font-size: 0.68rem; }
.first-run-choices small { font-size: 0.6rem; }
.first-run-footer { margin-top: 0.5rem; }
.first-run-note { margin-top: 0.3rem; min-height: 0; }
}
/* A soft edge on the list, so a tile cut by the fold reads as "more below"
rather than as a rendering seam. Gated on `data-scrollable`, which the JS sets
only when the list ACTUALLY overflows — a fade on a list that fits would
promise content that is not there, which is the opposite of the affordance
it is meant to be. Decorative only: the list scrolls under it. */
.first-run-choices[data-scrollable='true'] {
-webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 1.4rem), transparent 100%);
mask-image: linear-gradient(to bottom, #000 calc(100% - 1.4rem), transparent 100%);
}
/* Scrolled to the end, there is nothing left to promise, so the fade lifts.
Browsers without scroll-driven animations keep the static fade, which is
still honest — there IS more list, it is just above rather than below. */
@supports (animation-timeline: scroll(self block)) {
.first-run-choices[data-scrollable='true'] {
animation: first-run-scroll-fade linear both;
animation-timeline: scroll(self block);
animation-range: contain 80% contain 100%;
}
}
@keyframes first-run-scroll-fade {
to {
-webkit-mask-image: linear-gradient(to bottom, #000 100%, transparent 100%);
mask-image: linear-gradient(to bottom, #000 100%, transparent 100%);
}
}
@media (prefers-reduced-motion: reduce) {
#first-run-launcher,
.first-run-choices button,
.first-run-arrow { transition: none; }
/* Scrolling a tile into view must not animate either. */
.first-run-choices { scroll-behavior: auto; }
}