diff --git a/index.html b/index.html index d0a761c..1e360ba 100644 --- a/index.html +++ b/index.html @@ -696,11 +696,26 @@ align-items: center; gap: var(--s1) var(--s3); font-size: 10px; + /* Was unset, so it inherited the body's and a wrapped chip drew its + second line straight through the chip beneath it. The rail sits over a + moving scene and cannot rely on a background to hide the collision. */ + line-height: 1.5; letter-spacing: 0.04em; + /* The rail is a corner, not a column. Without a ceiling it grew to meet + the mode pill in the middle of the screen. */ + max-width: min(30rem, 42vw); color: var(--ink-2); text-shadow: 0 1px 3px rgba(3, 6, 10, 0.8); } - .hint__chip { display: inline-flex; align-items: center; gap: 5px; } + /* One chip is one line. Wrapping happens *between* chips, which the flex + container already does; a chip that wraps inside itself is the thing + that made this unreadable. */ + .hint__chip { + display: inline-flex; + align-items: center; + gap: 5px; + white-space: nowrap; + } .hint__label { color: var(--ink-3); } kbd { font: inherit; diff --git a/src/test/ui/shortcuts.test.ts b/src/test/ui/shortcuts.test.ts index ee3309b..e4080a5 100644 --- a/src/test/ui/shortcuts.test.ts +++ b/src/test/ui/shortcuts.test.ts @@ -244,3 +244,60 @@ describe("the rail hint", () => { } }); }); + +/** + * The rail is a glance, not a reference. + * + * Every hint the rail can show reused `meaning` — a full sentence written for + * the `?` sheet. In a `flex-wrap` container over a live 3D board that wrapped, + * and the wrapped line drew straight through the chip beneath it: on the + * deployed build "Fly to a chapter of the tour, or to a viewpoint inside a + * studio" and "Walk into the studio, or step back out to the city" overlapped + * and neither could be read. Nothing failed; it just looked broken. + * + * So: every id `railHints` is capable of returning must carry a short `rail` + * label, and it must be short enough to stay on one line. + */ +describe("rail hint labels", () => { + /** Every mode the rail is asked about, from `controlMode.ts`. */ + const MODES = [ + "overview", + "drive", + "actor", + "aircraft", + "office-overview", + "office-walk", + ] as const; + + it("gives every rail-selectable shortcut a short label of its own", () => { + for (const mode of MODES) { + for (const entry of railHints(mode, false)) { + assert.ok( + entry.rail !== undefined, + `${mode}: shortcut "${entry.id}" can reach the rail with no rail label, ` + + `so it would fall back to its ${entry.meaning.length}-character sentence`, + ); + assert.ok( + entry.rail.length <= 16, + `${mode}: rail label "${entry.rail}" is ${entry.rail.length} characters; ` + + `the rail has room for a glance, not a clause`, + ); + assert.ok( + !entry.rail.includes(","), + `${mode}: rail label "${entry.rail}" carries a clause; that belongs in meaning`, + ); + } + } + }); + + it("keeps the long form for the sheet, which is where it reads correctly", () => { + const [chapters] = railHints("overview", false); + assert.ok(chapters !== undefined); + assert.notEqual(chapters.rail, chapters.meaning); + assert.ok(chapters.meaning.length > 20, "the sheet should still get the sentence"); + }); + + it("shows nothing at all on a coarse pointer, which has no keys to press", () => { + for (const mode of MODES) assert.deepEqual(railHints(mode, true), []); + }); +}); diff --git a/src/ui/chromeState.ts b/src/ui/chromeState.ts index bca3af4..9a9e847 100644 --- a/src/ui/chromeState.ts +++ b/src/ui/chromeState.ts @@ -565,7 +565,10 @@ export function chromeState(inputs: ChromeInputs): ChromeState { const hints = railHints(mode, touchFirst).map((entry) => ({ id: entry.id, keys: entry.keys, - label: entry.meaning, + // `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 --------------------------------------------------- diff --git a/src/ui/shortcuts.ts b/src/ui/shortcuts.ts index 6450e54..ea60d56 100644 --- a/src/ui/shortcuts.ts +++ b/src/ui/shortcuts.ts @@ -95,6 +95,21 @@ export interface Shortcut { gesture?: string; /** What it does, in one clause, true in every mode the scope admits. */ meaning: string; + /** + * The two-or-three-word form, for the rail in the corner of the scene. + * + * `meaning` is written for the `?` sheet, where a full sentence is exactly + * right — it is a reference somebody is reading. The rail is a glance over a + * moving 3D board with room for one line, and reusing the sentence there put + * "Fly to a chapter of the tour, or to a viewpoint inside a studio" in a + * flex-wrap container: it wrapped, the wrapped line collided with the chip + * below it, and both were illegible over a bright sky. Two labels for two + * jobs, rather than one label doing neither well. + * + * Optional: a shortcut the rail never selects does not need one, and + * `railHints` only ever picks from a fixed set of ids. + */ + rail?: string; section: ShortcutSection; scope: ShortcutScope; /** @@ -128,6 +143,7 @@ export const KEYMAP: readonly Shortcut[] = [ id: "move", keys: ["W", "A", "S", "D"], meaning: "Move: drive, walk, or fly whatever you are controlling", + rail: "Move", section: "move", scope: "play", control: "forward", @@ -142,6 +158,7 @@ export const KEYMAP: readonly Shortcut[] = [ * in one of the four modes it fires in. */ meaning: "The primary action of whatever you are controlling — sprint, handbrake, throttle, climb", + rail: "Boost", section: "move", scope: "play", control: "primary", @@ -151,6 +168,7 @@ export const KEYMAP: readonly Shortcut[] = [ id: "altitude", keys: ["Q", "E"], meaning: "Descend / climb, flying as a crow or an aircraft", + rail: "Altitude", section: "move", scope: "fly", control: "ascend", @@ -160,6 +178,7 @@ export const KEYMAP: readonly Shortcut[] = [ id: "pitch", keys: ["I", "K"], meaning: "Pitch the nose up / down while flying", + rail: "Pitch", section: "move", scope: "fly", control: "pitch-up", @@ -175,6 +194,7 @@ export const KEYMAP: readonly Shortcut[] = [ */ keys: ["G"], meaning: "Glide — stop beating and hold the air, as a crow", + rail: "Glide", section: "move", scope: "crow", control: "secondary", @@ -184,6 +204,7 @@ export const KEYMAP: readonly Shortcut[] = [ id: "assist", keys: ["P"], meaning: "Hand the vehicle or aircraft back to its assisted route", + rail: "Autopilot", section: "move", scope: "drive", edge: "assist", @@ -213,6 +234,7 @@ export const KEYMAP: readonly Shortcut[] = [ id: "office", keys: ["O"], meaning: "Walk into the studio, or step back out to the city", + rail: "Studio", section: "world", scope: "always", onScreen: "the door button at the top of the left column", @@ -221,6 +243,7 @@ export const KEYMAP: readonly Shortcut[] = [ id: "walk", keys: ["V"], meaning: "Take control of a body — walk inside a studio, explore outside one", + rail: "Take control", section: "world", scope: "always", onScreen: "Walk / Explore in the left column, or the mode dock", @@ -237,6 +260,7 @@ export const KEYMAP: readonly Shortcut[] = [ id: "chapters", keys: ["1", "…", "9"], meaning: "Fly to a chapter of the tour, or to a viewpoint inside a studio", + rail: "Chapters", section: "world", scope: "always", onScreen: "the numbered list in the left column",