/** * Every visibility decision this interface makes, as one pure function. * * `renderLegend()` in `main.ts` made about forty of them — `walkButton.hidden = * inside ? office?.walker === null : city.actorState() === null`, and thirty-nine * more like it — interleaved with the DOM writes that acted on them, reading * eight pieces of module-level mutable state, and with **zero** test assertions * behind any of it. On a product whose entire pitch is the interface, the * interface was the only subsystem in the repo with no coverage at all. It was * not testable in principle either: every decision needed a live `Stage`, a built * heightfield and a WebGL context to reach. * * This module is the decisions and nothing else. No DOM, no THREE, no import of * `main.ts`, no clock, no `localStorage`, no network — hand it a plain object, * get a plain object back. `mount.ts` is the only thing that writes to the page, * and it writes exactly what this returned. * * Two properties fall out of that split and both are the point: * * - **The matrix is enumerable.** Three access tiers × six control modes × * inside/outside × coarse/fine pointer is 72 states, and * `src/test/ui/chromeState.test.ts` walks all of them. That is a thing you can * only do to a function. * - **The integration workstream makes one call.** `apply(chromeState(inputs))` * replaces forty scattered mutations, so a new piece of chrome is a field here * and a write there, rather than a fortieth line in a function nobody wants to * touch. * * ### On the input shape * * The twelve top-level keys are fixed by the build spec and are the contract with * `integration`; they are not negotiable and are not reordered. Everything past * them is optional, defaulted, and exists because the forty decisions genuinely * depend on more than twelve facts — the label on the Fly button needs to know * which board you are on, and the label on the touch action button needs to know * whether the crow is currently in the air. Making those optional rather than * required is deliberate: an integration stub that passes only the twelve * documented keys type-checks and returns a sane, if plainer, chrome. */ import type { Tier } from "../access.ts"; import type { DeviceDeclaration } from "../devices/types.ts"; import type { ControlMode } from "../play/controlMode.ts"; import { formatAircraftDetail, formatPlayHud } from "./hud.ts"; import type { AircraftDetailInput, AircraftDetailView, PlayHudView, PlayTelemetry } from "./hud.ts"; import { railHints } from "./shortcuts.ts"; import { BREAKPOINT } from "./tokens.ts"; // ---- Inputs --------------------------------------------------------------- /** What this visitor is, as the chrome needs to know it. */ export interface ChromeAccess { tier: Tier; /** `null` when this deployment has no door to knock on — a clean self-hosted clone. */ signInUrl: string | null; /** The auth subject, when there is one. */ subject: string | null; /** The name this person chose, which outranks the subject in the badge. */ displayName?: string | null; /** True once a local profile exists, which is what the Character button edits. */ hasProfile?: boolean; } export interface ChromeViewport { width: number; height: number; /** * A finger, not a mouse. * * Keyed off the pointer rather than the width, because the defect this decides * is about fingers: an 820px iPad has no keyboard either, and it lands on the * desktop layout by design (see `deviceProfile` in `engine/stage.ts`). */ coarsePointer: boolean; } /** Which of the three feeds is answering, and what the page may therefore claim. */ export interface ChromeFeeds { markers: boolean; weather: boolean; flights: boolean; /** A sky somebody typed retires the live-weather claim for as long as it is up. */ weatherOverridden?: boolean; /** The people in this office are invented, and the caption must say so. */ sampleOccupancy?: boolean; /** Licence lines from whichever live sources are answering. Shown in the `?` card. */ credits?: readonly string[]; } /** Which board and which building, for the labels that name them. */ export interface ChromeBoard { cityId?: string; cityLabel?: string; officeId?: string; officeLabel?: string; officeStatus?: "active" | "building"; /** The aircraft route only exists on the California board. */ isCalifornia?: boolean; } /** * One tab in the board strip — a world outside a building, a building inside one. * * The lists live in the inputs rather than being fetched by the applier, so that * `apply(chromeState(inputs))` really is *one* call: a second `setBoards()` * method on the handle would be a second place for the chrome to be half * updated, which is the class of bug this whole module exists to remove. */ export interface ChromeBoardTab { id: string; label: string; /** "active" or "building" for an office; `null` for a city, which has no status. */ status?: "active" | "building" | null; } /** One row of the chapter list: a city chapter and an office viewpoint are both this. */ export interface ChromeView { id: string; /** The authored number, or `null` to take the ordinal. */ number?: string | null; shortLabel: string; description?: string; } /** * What the detail card is showing. * * Two shapes, because there are two things worth picking: a marker, which is one * authored sentence, and an aircraft, which is five observed fields and is the * moment the map stops looking like a diagram. Modelled as a union rather than a * string so the aircraft case cannot be flattened back into one on its way here, * which is exactly what used to happen. */ export type ChromeDetail = | { kind: "text"; text: string } | { kind: "aircraft"; aircraft: AircraftDetailInput }; /** The body currently under control, for the labels that describe it. */ export interface ChromeActor { kind?: "crow" | "dog" | "humanoid" | null; /** A crow on the wing takes a different action pad from one on the ground. */ flying?: boolean; } /** Facts about the building you are standing in. */ export interface ChromeOffice { /** Screens authored in this pack that a full-depth viewer may drive. */ mediaSurfaceCount?: number; /** How many of them are showing something right now. */ mediaSurfaceActiveCount?: number; /** `resolveRobotOperations`' one-sentence disclosure, when the pack has robots. */ robotDisclosure?: string | null; /** * `smokeCaption`'s sentence, when the fires on the board have browned this * building's sky. `null` on every ordinary day. * * It sits with the other two disclosures rather than in the fire panel * because it is a statement about *this room's* picture: the sky in here is * derived from incidents on the map, not measured at this address, and a * brown courtyard with nothing beside it saying why is the failure this * exists to prevent. */ smokeNote?: string | null; /** The pack could not give this office a walker — a dollhouse-only building. */ walkable?: boolean; } export interface ChromeInputs { mode: ControlMode; /** Every mode reachable right now, per `controlModeAvailable`. */ available: readonly ControlMode[]; access: ChromeAccess; inside: boolean; /** `null` outside a building. "public" is the shell; "full" is the people too. */ officeDepth: "public" | "full" | null; viewport: ChromeViewport; feeds: ChromeFeeds; /** One sentence per demotion the server reported. Admin-visible. */ degraded: readonly string[]; /** Authored hardware in this room. Empty outside one. */ devices: readonly DeviceDeclaration[]; /** Nothing in `localStorage` says this browser has been here before. */ firstVisit: boolean; panelOpen: boolean; planOpen: boolean; // ---- Optional refinements; see the header ---- board?: ChromeBoard; actor?: ChromeActor; office?: ChromeOffice; /** A marker or aircraft is picked, so the detail card has something to show. */ detailOpen?: boolean; /** A camera is live in this tab, which raises the top-right column over modals. */ cameraLive?: boolean; /** Hosted presence is connected, which adds a row to the top-right column. */ presenceVisible?: boolean; /** The board strip's entries: cities outside a building, offices inside one. */ boards?: readonly ChromeBoardTab[]; /** The chapter list: city chapters outside, authored viewpoints inside. */ views?: readonly ChromeView[]; activeViewId?: string | null; /** Whatever the current mode's controller is reporting, for the play HUD. */ telemetry?: PlayTelemetry | null; /** The picked marker or aircraft, or `null`. Supersedes `detailOpen`. */ detail?: ChromeDetail | null; /** The clock line, already formatted by whoever owns the clock. */ clockLabel?: string; } // ---- Output --------------------------------------------------------------- /** * Three layouts, not two breakpoints' worth of tweaks. * * `phone` is a different design and not a narrow desktop: no hover, so nothing * may depend on it; no keyboard, so a card explaining the keyboard is furniture; * and one thumb, which reaches the bottom third of the screen and not the top * corners. */ export type ChromeLayout = "phone" | "compact" | "desktop"; export interface ChromeModeButton { mode: ControlMode; label: string; visible: boolean; pressed: boolean; } export interface ChromeTouchAction { /** The DOM id in `index.html`, so the applier needs no second table. */ id: string; label: string; visible: boolean; /** The held control it feeds, for the buttons that hold one. */ control: string | null; /** The one-shot request it fires, for the buttons that fire one. */ edge: string | null; } export interface ChromeRailHint { id: string; keys: readonly string[]; label: string; } export interface ChromeBoardButton extends ChromeBoardTab { active: boolean; } export interface ChromeViewRow { id: string; /** Always a string by the time it reaches the applier: authored, or the ordinal. */ number: string; label: string; active: boolean; } /** The detail card, resolved. `null` when nothing is picked. */ export type ChromeDetailView = | { kind: "text"; text: string } | { kind: "aircraft"; view: AircraftDetailView }; export interface ChromeState { // ---- Frame ---- layout: ChromeLayout; /** A finger drives this session, so on-screen controls are the primary path. */ touchFirst: boolean; /** Standing in a building rather than looking at a board. */ inside: boolean; /** Every class the applier sets on ``, computed once so nothing drifts. */ bodyClasses: readonly string[]; // ---- The left column ---- panelVisible: boolean; panelOpen: boolean; /** * Carried through so the applier can compute the *next* value for a toggle * without keeping its own copy of the state — which is how a toggle ends up * disagreeing with the thing it toggles. */ planOpen: boolean; panelToggleVisible: boolean; panelToggleLabel: string; scrimVisible: boolean; panelTitle: string; panelSubtitle: string; /** Which list the board strip is pointed at: worlds outside, buildings inside. */ boardPicker: "city" | "office"; boards: readonly ChromeBoardButton[]; clockLabel: string; enterVisible: boolean; enterLabel: string; walkVisible: boolean; walkPressed: boolean; walkLabel: string; flyVisible: boolean; flyPressed: boolean; flyLabel: string; screensVisible: boolean; screensLabel: string; devicePanelVisible: boolean; devicePanelLabel: string; /** * The declarations themselves, carried through so the applier can build the * panel without a second call. Pass-through, not a decision — but it belongs in * the state for the same reason the board and view lists do: one call, one * consistent frame of chrome. */ deviceDeclarations: readonly DeviceDeclaration[]; chaptersVisible: boolean; views: readonly ChromeViewRow[]; blurbVisible: boolean; blurbText: string; // ---- Top right ---- tierVisible: boolean; tierTone: Tier; tierLabel: string; /** * What signing in **adds**, for a visitor who has not. * * `null` for anyone it would not help. The badge used to read "Public view" * and stop there, which names the visitor rather than the offer and is the one * place in the product where the anon-first promise reads as a demotion. */ tierAdds: string | null; whoLabel: string | null; characterVisible: boolean; signInVisible: boolean; signInLabel: string; signInHref: string | null; presenceVisible: boolean; cameraAlert: boolean; minimapVisible: boolean; minimapReadoutVisible: boolean; // ---- The dock and the HUD ---- modeDockVisible: boolean; modeButtons: readonly ChromeModeButton[]; playHudVisible: boolean; /** The three strings and the warning flag, or `null` in the orbit modes. */ playHud: PlayHudView | null; // ---- The rail ---- railVisible: boolean; detailVisible: boolean; detail: ChromeDetailView | null; helpVisible: boolean; planToggleVisible: boolean; planTogglePressed: boolean; railHintsVisible: boolean; railHints: readonly ChromeRailHint[]; // ---- Touch ---- touchControlsVisible: boolean; stickVisible: boolean; stickLabel: string; touchActions: readonly ChromeTouchAction[]; // ---- Honesty, and the office ---- sourceVisible: boolean; sourceLabel: string; sourceLive: boolean; officeInviteVisible: boolean; officeInviteText: string; officeInviteLinkLabel: string | null; officeNoteVisible: boolean; officeNoteText: string; degradedVisible: boolean; degradedLines: readonly string[]; /** * The credit lines the live sources ask for, shown in the `?` card. * * MET Norway and Open-Meteo publish under CC BY 4.0 and the API forwards the * line each of them asks for; adsb.lol asks to be named for the positions. All * of it arrived, was parsed, and was then read by nobody — a licence * obligation plumbed to within one line of being met. Empty when nothing live * is on screen, because a credit for data nobody is looking at is noise and * the zero-config build owes nobody anything. */ credits: readonly string[]; // ---- First run ---- onboardingVisible: boolean; canvasLabel: string; } // ---- The dock's five buttons --------------------------------------------- const DOCK_MODES: readonly { mode: ControlMode; label: string }[] = [ { mode: "overview", label: "View" }, { mode: "drive", label: "Drive" }, { mode: "actor", label: "Explore" }, { mode: "aircraft", label: "Fly" }, { mode: "office-walk", label: "Walk" }, ]; /** * Modes in which a body is under control and the touch surface applies. * * `office-overview` is not one of them, and neither is `overview`: both are * orbit-camera states where the joystick would move nothing. */ const PLAY_MODES: readonly ControlMode[] = ["drive", "actor", "aircraft", "office-walk"]; function isPlayMode(mode: ControlMode): boolean { return PLAY_MODES.includes(mode); } /** * The liveness phrase, restated here rather than imported. * * `describeLiveness` lives in `adapters/http.ts`, which cannot be imported under * Node's type stripping today and which pulls a network client behind it. The * wording is the same and the rule is the same: all three live is the only case * that may say "live data", because "live data" printed over invented companies * because a weather station answered is precisely the claim the flag exists to * prevent. */ function describeFeeds(feeds: ChromeFeeds): string { const weather = feeds.weather && feeds.weatherOverridden !== true; const parts: string[] = []; if (feeds.markers && weather && feeds.flights) return "live data"; if (feeds.markers) parts.push("live markers"); if (weather) parts.push("live weather"); if (feeds.flights) parts.push("live traffic"); return parts.join(" · "); } function layoutFor(viewport: ChromeViewport): ChromeLayout { const width = Number.isFinite(viewport.width) ? viewport.width : BREAKPOINT.compact + 1; if (width <= BREAKPOINT.phone) return "phone"; if (width <= BREAKPOINT.compact) return "compact"; return "desktop"; } function actorNoun(actor: ChromeActor | undefined): string { const kind = actor?.kind ?? null; if (kind === "crow") return "your crow"; if (kind === "dog") return "your dog"; if (kind === "humanoid") return "your humanoid"; return "a body"; } // ---- Seeds ---------------------------------------------------------------- /** * Should the left column start open at this width? * * The width only *seeds* these two; the moment somebody presses the toggle the * viewport stops having an opinion. That rule was already right and is kept — * what was wrong was that the seed lived inline in `main.ts` next to the * `resize` listener that re-ran one of the two and not the other, so a rotation * from portrait to landscape re-seeded the plan and left the panel sheet closed * over a desktop-width layout. */ export function seedPanelOpen(width: number): boolean { return !(Number.isFinite(width) && width <= BREAKPOINT.compact); } /** * Should the plan start visible at this width? * * **Yes, at every width, and that is the change.** It used to be * `window.innerWidth > 600`, which meant a phone started with the plan off — and * a phone has no `M`. Every visible control at 390px was enumerated once and * none of them could turn it on, so the bottom-sheet plan layout `index.html` * had carried for months was unreachable on the device it was drawn for. The * `#plan-toggle` button closed the "no way to turn it on" half; this closes the * "it was off to begin with" half, and the phone stylesheet gives the plan a * glanceable 8.5rem home in the top-right column rather than a sheet that eats * the map. */ export function seedPlanOpen(_width: number): boolean { return true; } // ---- The function --------------------------------------------------------- /** * Resolve the whole interface for one instant. Pure, total, and cheap enough to * call on every frame if the caller wants to. */ export function chromeState(inputs: ChromeInputs): ChromeState { const layout = layoutFor(inputs.viewport); const phone = layout === "phone"; const touchFirst = inputs.viewport.coarsePointer; const inside = inputs.inside; const mode = inputs.mode; const available = inputs.available; const board = inputs.board ?? {}; const office = inputs.office ?? {}; const actor = inputs.actor ?? {}; const tier = inputs.access.tier; const can = (candidate: ControlMode): boolean => available.includes(candidate); const walking = mode === "office-walk"; const exploring = mode === "actor"; const flying = mode === "aircraft"; const driving = mode === "drive"; const playing = isPlayMode(mode); const crowFlying = exploring && actor.kind === "crow" && actor.flying === true; const cityLabel = board.cityLabel ?? ""; const officeLabel = board.officeLabel ?? "the studio"; const officeStatus = board.officeStatus ?? "building"; // ---- The dock ----------------------------------------------------------- // // Hidden entirely when `overview` is the only thing available. On two of the // three city boards this rendered as a lit pill holding one already-pressed // button that did nothing — a control whose only state is its current one is // not a control, it is a label pretending to be one. const modeButtons: ChromeModeButton[] = DOCK_MODES.map(({ mode: dockMode, label }) => { // The "View" button stands for both orbit states: `overview` outside a // building and `office-overview` inside one. They are one idea — the camera // is yours and nothing else is — and two buttons for it would swap under the // viewer as they walked through a door. const visible = dockMode === "overview" ? can("overview") || can("office-overview") : can(dockMode); const pressed = dockMode === "overview" ? mode === "overview" || mode === "office-overview" : mode === dockMode; return { mode: dockMode, label, visible, pressed }; }); const modeDockVisible = modeButtons.filter((button) => button.visible).length > 1; // ---- The touch action pad ---------------------------------------------- // // Built from the mode rather than from a pile of independent `hidden` // assignments, which is what this was: eight `if (touchX) touchX.hidden = …` // lines whose conditions overlapped and could not all be true or all be false. const touchActions: readonly ChromeTouchAction[] = [ { id: "touch-primary", label: driving ? "Handbrake" : flying ? "Throttle" : crowFlying ? "Climb" : "Sprint", visible: playing && !walking, control: crowFlying ? "ascend" : "primary", edge: null, }, { id: "touch-secondary", label: "Glide", visible: crowFlying, control: "secondary", edge: null, }, { id: "touch-pitch-up", label: "Pitch +", visible: crowFlying || flying, control: "pitch-up", edge: null }, { id: "touch-pitch-down", label: "Pitch −", visible: crowFlying || flying, control: "pitch-down", edge: null }, { id: "touch-assist", label: "Assist", visible: driving || flying, control: null, edge: "assist" }, { id: "touch-reset", label: "Reset", visible: driving || flying, control: null, edge: "reset" }, { id: "touch-camera", label: "Camera", visible: driving, control: null, edge: "camera" }, { id: "touch-map", label: "Map", visible: true, control: null, edge: null }, ]; /** * The joystick, and the defect it fixes. * * `pointerStick.ts` has carried a real analogue stick since the play modes * landed, and it was shown only while a body was already under control — which * on a phone in VIEW mode meant the screen offered no movement control at all, * and no way to discover that one existed. It is now visible in every play * mode *and* announced by the onboarding coach, so the first thing a thumb * finds is the thing that moves. */ const touchControlsVisible = touchFirst && playing; // ---- The rail ----------------------------------------------------------- // // `?` is a keyboard reference. On a device with no keyboard it was still being // drawn, which is the interface offering a person a list of keys they cannot // press. On a coarse pointer the same button remains — the sheet now carries // the touch and on-screen half of every row — but it is labelled for what it // is there. const hints = railHints(mode, touchFirst).map((entry) => ({ id: entry.id, keys: entry.keys, // `rail`, not `meaning`: the sheet gets the sentence, the rail gets the // glance. See the field's note in `shortcuts.ts` for what reusing the // sentence here actually looked like on screen. label: entry.rail ?? entry.meaning, })); // ---- The honesty line --------------------------------------------------- const sampleOccupancy = inside && inputs.feeds.sampleOccupancy === true; const liveness = describeFeeds(inputs.feeds); const sourceLabel = sampleOccupancy ? "sample occupancy · these people are invented" : liveness; // ---- The tier badge ----------------------------------------------------- // // The label names what you get; the second line names what you would gain. // "Public view" said neither, and on a product designed anon-first it was the // one string that framed the default visitor as a lesser one. const tierLabel = tier === "god" ? "Godmode" : tier === "member" ? "Full view" : "Open demo"; const tierAdds = tier === "anon" && inputs.access.signInUrl !== null ? "Sign in adds the people: who is in the room, your own character on the floor, and the studio desk." : null; // ---- The studio's one invitation --------------------------------------- // // It used to live on `#office-badge` inside `#panel`, and on a phone `#panel` // is a bottom sheet that starts closed — so the only in-office call to sign in // was behind a hamburger on the device least likely to open one. This is the // same move `renderSource` already made for the occupancy disclosure, applied // to the message that is an offer rather than a disclosure. const publicOffice = inside && inputs.officeDepth === "public"; const officeInviteText = publicOffice ? inputs.access.signInUrl !== null ? "You are seeing the building, not the people." : "This deployment has no sign-in, so the floor is always the building alone." : ""; const mediaCount = office.mediaSurfaceCount ?? 0; const mediaActive = office.mediaSurfaceActiveCount ?? 0; const robotDisclosure = office.robotDisclosure ?? null; const mediaNote = mediaCount === 0 ? "" : mediaActive > 0 ? `${mediaActive} of ${mediaCount} ${mediaCount === 1 ? "screen" : "screens"} active · stop control in Office screens.` : `${mediaCount} ${mediaCount === 1 ? "screen" : "screens"} ready · media stays off until you opt in.`; const officeNoteText = [mediaNote, robotDisclosure ?? "", office.smokeNote ?? ""] .filter((part) => part !== "") .join(" "); // ---- Onboarding --------------------------------------------------------- // // Not while a modal-ish state is up, not inside a building, and not while a // body is already under control — a coach that teaches "drag to orbit" over a // scene somebody is already driving is a coach interrupting the thing it was // written to cause. const onboardingVisible = inputs.firstVisit && !inside && mode === "overview"; // ---- The two lists ------------------------------------------------------- // // Resolved here rather than in the applier so the applier has no decisions // left to make. The board strip is one control pointed at whichever list is // currently the answer to "which of these am I in"; a second, separate office // strip was the obvious alternative and is worse, because it would sit dead // and greyed out for the whole time anybody is looking at the city. const activeBoardId = inside ? board.officeId : board.cityId; const boards: readonly ChromeBoardButton[] = (inputs.boards ?? []).map((tab) => ({ ...tab, active: tab.id === activeBoardId, })); const views: readonly ChromeViewRow[] = (inputs.views ?? []).map((view, i) => ({ id: view.id, number: view.number ?? String(i + 1).padStart(2, "0"), label: view.shortLabel, active: view.id === inputs.activeViewId, })); const activeView = (inputs.views ?? []).find((view) => view.id === inputs.activeViewId); const blurbText = activeView?.description ?? ""; const detailInput = inputs.detail ?? null; const detail: ChromeDetailView | null = detailInput === null ? null : detailInput.kind === "text" ? { kind: "text", text: detailInput.text } : { kind: "aircraft", view: formatAircraftDetail(detailInput.aircraft) }; const playHud = formatPlayHud(inputs.telemetry ?? null); const canvasLabel = inside ? walking ? `${officeLabel}, following ${actorNoun(actor)}. Use W A S D to move, or the joystick.` : `${officeLabel}, seen from above. Drag to orbit, scroll to zoom.` : driving ? `${cityLabel}, following your car. Use W A S D to drive.` : flying ? `${cityLabel}, following your electric aircraft. Use W A S D to fly, or P to resume assisted flight.` : exploring ? `${cityLabel}, following ${actorNoun(actor)}. Use W A S D to move, Q and E for altitude, G to glide.` : `Map of ${cityLabel}, seen from above. Drag to orbit, scroll to zoom.`; const bodyClasses: string[] = []; if (!inputs.panelOpen) bodyClasses.push("panel-closed"); if (!inputs.planOpen) bodyClasses.push("minimap-off"); if (sampleOccupancy) bodyClasses.push("sample-occupancy"); if (inputs.presenceVisible === true) bodyClasses.push("presence-on"); if (touchFirst) bodyClasses.push("touch-first"); if (inside) bodyClasses.push("inside"); if (playing) bodyClasses.push("playing"); if (inputs.cameraLive === true) bodyClasses.push("camera-live"); bodyClasses.push(`layout-${layout}`); return { layout, touchFirst, inside, bodyClasses, panelVisible: true, panelOpen: inputs.panelOpen, planOpen: inputs.planOpen, // Above 900 the column is furniture and always there, so the toggle would be // a button that hides the thing it is standing on top of. panelToggleVisible: layout !== "desktop", panelToggleLabel: inside ? officeLabel : cityLabel, scrimVisible: phone && inputs.panelOpen, panelTitle: inside ? officeLabel : cityLabel, panelSubtitle: inside ? `Tera · ${officeStatus} environment` : "Tera · Lumbridge Simulate", boardPicker: inside ? "office" : "city", boards, clockLabel: inputs.clockLabel ?? "", enterVisible: true, enterLabel: inside ? "← Back to the city" : officeStatus === "active" ? `Open ${officeLabel} →` : `Preview ${officeLabel} · building →`, walkVisible: inside ? office.walkable !== false && can("office-walk") : can("actor"), walkPressed: walking || exploring, walkLabel: inside ? walking ? "Return to overview ↑" : `Walk as ${actorNoun(actor)} →` : exploring ? "Return to flyover ↑" : `Explore as ${actorNoun(actor)} →`, flyVisible: !inside && board.isCalifornia === true && can("aircraft"), flyPressed: flying, flyLabel: flying ? "Return to flyover ↑" : "Fly the California route →", screensVisible: inside && inputs.officeDepth === "full" && mediaCount > 0, screensLabel: "Office screens →", // Anonymous visitors see the hardware too. The declarations are authored into // the pack and are therefore public by construction; what an account buys is // the live *readings*, and the panel says which it is showing. devicePanelVisible: inside && inputs.devices.length > 0, devicePanelLabel: inputs.devices.length === 1 ? "Studio hardware →" : `Studio hardware · ${inputs.devices.length} →`, deviceDeclarations: inputs.devices, chaptersVisible: views.length > 0, views, // On a phone the panel is a bottom sheet with a thumb-sized list in it; a // paragraph of description under that list pushes the door off the screen. blurbVisible: !phone && blurbText !== "", blurbText, tierVisible: true, tierTone: tier, tierLabel, tierAdds, whoLabel: inputs.access.displayName ?? inputs.access.subject, characterVisible: inputs.access.hasProfile === true, signInVisible: inputs.access.subject === null && inputs.access.signInUrl !== null, signInLabel: "Sign in", signInHref: inputs.access.signInUrl, presenceVisible: inputs.presenceVisible === true, cameraAlert: inputs.cameraLive === true, minimapVisible: inputs.planOpen, // Hover-only text on a device with no hover. minimapReadoutVisible: !touchFirst, modeDockVisible, modeButtons, playHudVisible: playing && playHud !== null, playHud, railVisible: true, detailVisible: detail !== null || inputs.detailOpen === true, detail, helpVisible: true, // For pointers, and only for pointers: where there is a keyboard the `M` // hint is the affordance and a button saying the same thing is a second row // of glass for nothing. planToggleVisible: touchFirst || phone, planTogglePressed: inputs.planOpen, railHintsVisible: hints.length > 0, railHints: hints, touchControlsVisible, stickVisible: touchControlsVisible, stickLabel: walking ? "Walk" : driving ? "Steer" : flying ? "Fly" : crowFlying ? "Bank" : "Move", touchActions, sourceVisible: sourceLabel !== "", sourceLabel, sourceLive: !sampleOccupancy && liveness !== "", officeInviteVisible: publicOffice, officeInviteText, officeInviteLinkLabel: publicOffice && inputs.access.signInUrl !== null ? "Sign in for the live floor" : null, officeNoteVisible: inside && officeNoteText !== "", officeNoteText, // The demotions are an operator's diagnostic, not a visitor's caption. They // are shown in the `?` card to the tier that could act on them. degradedVisible: tier === "god" && inputs.degraded.length > 0, degradedLines: inputs.degraded, // The weather override is somebody's invention; it is not MET Norway's sky // and must not be attributed to them. credits: [...new Set((inputs.feeds.credits ?? []).filter((line) => line.trim() !== ""))], onboardingVisible, canvasLabel, }; }