mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
refactor: compose UI shell with explicit application operations and component owners
This commit is contained in:
1 parent
8a226a2fe3
commit
f5ef9091f0
67 files changed
+15799
-15059
No files matched your search
@@ -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; }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user