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",