Add text size and keyboard settings (#38)
Settings sets the text size (100, 125 or 150 %) and the key of each button, kept in this browser's storage and validated on load. Every font size scales with --text-scale, and the dialogue box scrolls instead of growing past the screen. The game screen's label, the Play message and Help's control list name the current keys. Screen reader announcements for every native screen and touch control remapping remain. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
29248172bd
commit
f289615bd2
10 files changed
+588
-41
No files matched your search
+4
-13
@@ -1,23 +1,14 @@
|
||||
import { type Button } from "./types";
|
||||
import { InputController } from "./controller";
|
||||
|
||||
const KEYS: Record<string, Button> = {
|
||||
ArrowUp: "up",
|
||||
ArrowDown: "down",
|
||||
ArrowLeft: "left",
|
||||
ArrowRight: "right",
|
||||
KeyX: "a",
|
||||
KeyZ: "b",
|
||||
Enter: "start",
|
||||
ShiftLeft: "select",
|
||||
ShiftRight: "select",
|
||||
};
|
||||
|
||||
/** `keys` maps keyboard codes to buttons; it is read on every key press,
|
||||
* so a changed mapping applies at once. */
|
||||
export function bindControls(
|
||||
root: HTMLElement,
|
||||
input: InputController,
|
||||
active: () => boolean,
|
||||
unlock: () => void,
|
||||
keys: () => ReadonlyMap<string, Button>,
|
||||
) {
|
||||
const abort = new AbortController();
|
||||
const options = { signal: abort.signal };
|
||||
@@ -130,7 +121,7 @@ export function bindControls(
|
||||
)
|
||||
)
|
||||
return;
|
||||
const button = KEYS[event.code];
|
||||
const button = keys().get(event.code);
|
||||
if (!button) return;
|
||||
event.preventDefault();
|
||||
unlock();
|
||||
|
||||
+170
-2
@@ -7,6 +7,21 @@ import {
|
||||
import "./ui/styles.css";
|
||||
import { renderShell } from "./ui/shell";
|
||||
import { renderPokedexScreen, renderPokedexTab } from "./ui/pokedex";
|
||||
import {
|
||||
RESERVED_KEYS,
|
||||
TEXT_SCALES,
|
||||
applyTextScale,
|
||||
buttonKeys,
|
||||
defaultPreferences,
|
||||
keyMap,
|
||||
keyName,
|
||||
loadPreferences,
|
||||
rebind,
|
||||
savePreferences,
|
||||
type Preferences,
|
||||
type TextScale,
|
||||
} from "./ui/preferences";
|
||||
import type { Button as GameButton } from "./input/types";
|
||||
import { ENGINE_FLAGS } from "./game/rom/profile";
|
||||
import { bindTabs } from "./ui/tabs";
|
||||
import { NativeRuntime } from "./game/runtime";
|
||||
@@ -113,7 +128,7 @@ function showWorld(world: GameWorld) {
|
||||
canvas.dataset.intro = String(world.intro);
|
||||
canvas.setAttribute(
|
||||
"aria-label",
|
||||
`${world.map.name}. Position ${world.x}, ${world.y}. Arrow keys move. X interacts. Z continues dialogue.`,
|
||||
`${world.map.name}. Position ${world.x}, ${world.y}. ${controlsSummary()}`,
|
||||
);
|
||||
}
|
||||
function showChoice(choice: Choice | undefined) {
|
||||
@@ -1555,11 +1570,15 @@ const session = new GameSession(
|
||||
const input = new InputController((button, pressed) =>
|
||||
runtime.setButton(button, pressed),
|
||||
);
|
||||
let preferences = loadPreferences();
|
||||
let keys = keyMap(preferences);
|
||||
applyTextScale(preferences.textScale);
|
||||
const controls = bindControls(
|
||||
root,
|
||||
input,
|
||||
() => session.state === "playing" && !busy && tabs.active() === "game",
|
||||
() => {},
|
||||
() => keys,
|
||||
);
|
||||
const ready = () => ["paused", "playing"].includes(session.state);
|
||||
const tabs = bindTabs(
|
||||
@@ -1654,7 +1673,9 @@ function play() {
|
||||
tabs.select("game");
|
||||
session.play();
|
||||
canvas.focus({ preventScroll: true });
|
||||
message("Press X or A to talk and read. Up and Down change choices.");
|
||||
message(
|
||||
`Press ${buttonKeys(preferences, "a")} or A to talk and read. Up and Down change choices.`,
|
||||
);
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -1766,11 +1787,158 @@ get<HTMLInputElement>("save-file").addEventListener("change", async (event) => {
|
||||
|
||||
get("help").addEventListener("click", () => {
|
||||
pause();
|
||||
showHelpKeys();
|
||||
get<HTMLDialogElement>("help-dialog").showModal();
|
||||
});
|
||||
get("close-help").addEventListener("click", () =>
|
||||
get<HTMLDialogElement>("help-dialog").close(),
|
||||
);
|
||||
|
||||
const BUTTON_NAMES: Record<GameButton, string> = {
|
||||
a: "A",
|
||||
b: "B",
|
||||
start: "START",
|
||||
select: "SELECT",
|
||||
up: "Up",
|
||||
down: "Down",
|
||||
left: "Left",
|
||||
right: "Right",
|
||||
};
|
||||
const BUTTON_ORDER: GameButton[] = ["a", "b", "start", "select", "up", "down", "left", "right"];
|
||||
|
||||
/** One sentence naming the current keys, for the game screen's label. */
|
||||
function controlsSummary() {
|
||||
const directions = (["up", "down", "left", "right"] as const)
|
||||
.map((button) => buttonKeys(preferences, button))
|
||||
.join(" ");
|
||||
const buttons = (["a", "b", "start", "select"] as const)
|
||||
.map((button) => `${buttonKeys(preferences, button)} is ${BUTTON_NAMES[button]}`)
|
||||
.join(", ");
|
||||
return `${directions} move. ${buttons}.`;
|
||||
}
|
||||
function showHelpKeys() {
|
||||
const list = get("help-keys");
|
||||
list.replaceChildren();
|
||||
const add = (term: string, detail: string) => {
|
||||
const dt = document.createElement("dt"),
|
||||
dd = document.createElement("dd");
|
||||
dt.textContent = term;
|
||||
dd.textContent = detail;
|
||||
list.append(dt, dd);
|
||||
};
|
||||
const key = (button: GameButton) => buttonKeys(preferences, button);
|
||||
add("Move", (["up", "down", "left", "right"] as const).map(key).join(" "));
|
||||
add("Interact / Continue", `${key("a")} (A)`);
|
||||
add("Back / No", `${key("b")} (B)`);
|
||||
add("Start menu", `${key("start")} (START)`);
|
||||
add("Registered item", `${key("select")} (SELECT)`);
|
||||
}
|
||||
get("game-screen").setAttribute("aria-label", `Game screen. ${controlsSummary()}`);
|
||||
|
||||
/** Settings: the text size and the keyboard controls, kept for this
|
||||
* browser only. */
|
||||
let capturing: GameButton | undefined;
|
||||
/** Escape ended a key change, so the dialog's cancel must not close it. */
|
||||
let keepOpen = false;
|
||||
function setPreferences(next: Preferences, status = "") {
|
||||
preferences = next;
|
||||
keys = keyMap(preferences);
|
||||
savePreferences(preferences);
|
||||
applyTextScale(preferences.textScale);
|
||||
get("game-screen").setAttribute("aria-label", `Game screen. ${controlsSummary()}`);
|
||||
showSettings();
|
||||
get("settings-status").textContent = status;
|
||||
}
|
||||
function showSettings() {
|
||||
for (const radio of document.querySelectorAll<HTMLInputElement>('input[name="text-scale"]'))
|
||||
radio.checked = Number(radio.value) === preferences.textScale;
|
||||
get("key-table").replaceChildren(
|
||||
...BUTTON_ORDER.map((button) => {
|
||||
const row = document.createElement("tr"),
|
||||
name = document.createElement("th"),
|
||||
key = document.createElement("td"),
|
||||
action = document.createElement("td"),
|
||||
change = document.createElement("button");
|
||||
name.scope = "row";
|
||||
name.textContent = BUTTON_NAMES[button];
|
||||
key.textContent = capturing === button ? "Press a key…" : buttonKeys(preferences, button);
|
||||
change.type = "button";
|
||||
change.textContent = capturing === button ? "Cancel" : "Change";
|
||||
change.dataset.button = button;
|
||||
change.setAttribute("aria-label", `${change.textContent} the key for ${BUTTON_NAMES[button]}`);
|
||||
change.addEventListener("click", () => {
|
||||
capturing = capturing === button ? undefined : button;
|
||||
showSettings();
|
||||
get("settings-status").textContent = capturing
|
||||
? `Press the key for ${BUTTON_NAMES[button]}.`
|
||||
: "";
|
||||
get("key-table").querySelector<HTMLButtonElement>(`button[data-button="${button}"]`)?.focus();
|
||||
});
|
||||
action.append(change);
|
||||
row.append(name, key, action);
|
||||
return row;
|
||||
}),
|
||||
);
|
||||
}
|
||||
const settingsDialog = get<HTMLDialogElement>("settings-dialog");
|
||||
get("settings").addEventListener("click", () => {
|
||||
pause();
|
||||
capturing = undefined;
|
||||
showSettings();
|
||||
get("settings-status").textContent = "";
|
||||
settingsDialog.showModal();
|
||||
});
|
||||
get("close-settings").addEventListener("click", () => settingsDialog.close());
|
||||
settingsDialog.addEventListener("close", () => {
|
||||
capturing = undefined;
|
||||
});
|
||||
// While a key is awaited, the next key press becomes the button's key.
|
||||
settingsDialog.addEventListener(
|
||||
"keydown",
|
||||
(event) => {
|
||||
const button = capturing;
|
||||
if (!button) return;
|
||||
if (event.code === "Tab") return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
capturing = undefined;
|
||||
if (event.code === "Escape") {
|
||||
// Some browsers skip the cancel event once the key is handled here.
|
||||
keepOpen = true;
|
||||
requestAnimationFrame(() => (keepOpen = false));
|
||||
showSettings();
|
||||
get("settings-status").textContent = "Key change cancelled.";
|
||||
} else if (RESERVED_KEYS.includes(event.code)) {
|
||||
showSettings();
|
||||
get("settings-status").textContent = `${keyName(event.code)} keeps its usual role. Choose another key.`;
|
||||
} else {
|
||||
const previous = BUTTON_ORDER.find(
|
||||
(other) => other !== button && preferences.keys[other].includes(event.code),
|
||||
);
|
||||
setPreferences(
|
||||
rebind(preferences, button, event.code),
|
||||
`${BUTTON_NAMES[button]} is now ${keyName(event.code)}.` +
|
||||
(previous ? ` ${BUTTON_NAMES[previous]} has no key until you choose one.` : ""),
|
||||
);
|
||||
}
|
||||
get("key-table").querySelector<HTMLButtonElement>(`button[data-button="${button}"]`)?.focus();
|
||||
},
|
||||
true,
|
||||
);
|
||||
// Escape cancels a key change rather than closing the dialog.
|
||||
settingsDialog.addEventListener("cancel", (event) => {
|
||||
if (capturing || keepOpen) event.preventDefault();
|
||||
keepOpen = false;
|
||||
});
|
||||
for (const radio of document.querySelectorAll<HTMLInputElement>('input[name="text-scale"]'))
|
||||
radio.addEventListener("change", () => {
|
||||
const scale = Number(radio.value) as TextScale;
|
||||
if (TEXT_SCALES.includes(scale))
|
||||
setPreferences({ ...preferences, textScale: scale }, `Text size: ${radio.parentElement?.textContent?.trim()}.`);
|
||||
});
|
||||
get("reset-keys").addEventListener("click", () =>
|
||||
setPreferences({ ...preferences, keys: defaultPreferences().keys }, "Controls reset to the defaults."),
|
||||
);
|
||||
window.addEventListener("blur", pause);
|
||||
document.addEventListener("visibilitychange", () => {
|
||||
if (document.hidden) pause();
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import "../ui/reference.css";
|
||||
import { defaultPreferences, keyMap } from "../ui/preferences";
|
||||
import { renderShell } from "./shell";
|
||||
import { BinjgbEmulator } from "../emulator/binjgb";
|
||||
import { GameSession } from "../app/session";
|
||||
@@ -29,11 +30,14 @@ const session = new GameSession(emulator, new BrowserSaveRepository());
|
||||
const input = new InputController((button, pressed) =>
|
||||
emulator.setButton(button, pressed),
|
||||
);
|
||||
// The reference keeps the default keys; preferences belong to the native player.
|
||||
const referenceKeys = keyMap(defaultPreferences());
|
||||
const controls = bindControls(
|
||||
root,
|
||||
input,
|
||||
() => session.state === "playing" && !busy,
|
||||
() => emulator.unlockAudio(),
|
||||
() => referenceKeys,
|
||||
);
|
||||
const ready = () => ["paused", "playing"].includes(session.state);
|
||||
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
import { BUTTONS, type Button } from "../input/types";
|
||||
|
||||
/** Interface preferences for this browser. They are not game state, so
|
||||
* they stay out of saves and backups. */
|
||||
export interface Preferences {
|
||||
/** The factor every font size is multiplied by. */
|
||||
textScale: TextScale;
|
||||
/** The keyboard codes (KeyboardEvent.code) that press each button. */
|
||||
keys: Record<Button, string[]>;
|
||||
}
|
||||
|
||||
export const TEXT_SCALES = [1, 1.25, 1.5] as const;
|
||||
export type TextScale = (typeof TEXT_SCALES)[number];
|
||||
|
||||
export const DEFAULT_KEYS: Readonly<Record<Button, readonly string[]>> = {
|
||||
up: ["ArrowUp"],
|
||||
down: ["ArrowDown"],
|
||||
left: ["ArrowLeft"],
|
||||
right: ["ArrowRight"],
|
||||
a: ["KeyX"],
|
||||
b: ["KeyZ"],
|
||||
start: ["Enter"],
|
||||
select: ["ShiftLeft", "ShiftRight"],
|
||||
};
|
||||
|
||||
/** Keys that keep their browser role: focus moves, dialogs close and
|
||||
* pages scroll with them, so they cannot press a game button. */
|
||||
export const RESERVED_KEYS: readonly string[] = ["Tab", "Escape", "Space"];
|
||||
|
||||
const STORAGE_KEY = "crystal-preferences-v1";
|
||||
|
||||
export const defaultPreferences = (): Preferences => ({
|
||||
textScale: 1,
|
||||
keys: Object.fromEntries(
|
||||
BUTTONS.map((button) => [button, [...DEFAULT_KEYS[button]]]),
|
||||
) as Record<Button, string[]>,
|
||||
});
|
||||
|
||||
/** Accept stored preferences only when every part is valid; anything
|
||||
* else falls back to the defaults for that part. */
|
||||
function parse(value: unknown): Preferences {
|
||||
const preferences = defaultPreferences();
|
||||
if (!value || typeof value !== "object") return preferences;
|
||||
const stored = value as Partial<Preferences>;
|
||||
if (TEXT_SCALES.includes(stored.textScale as TextScale))
|
||||
preferences.textScale = stored.textScale as TextScale;
|
||||
const keys = stored.keys;
|
||||
if (keys && typeof keys === "object") {
|
||||
const seen = new Set<string>();
|
||||
const valid = BUTTONS.every((button) => {
|
||||
const codes = (keys as Record<string, unknown>)[button];
|
||||
return (
|
||||
Array.isArray(codes) &&
|
||||
codes.every(
|
||||
(code) =>
|
||||
typeof code === "string" &&
|
||||
/^[A-Za-z0-9]{1,24}$/.test(code) &&
|
||||
!RESERVED_KEYS.includes(code) &&
|
||||
!seen.has(code) &&
|
||||
seen.add(code),
|
||||
)
|
||||
);
|
||||
});
|
||||
if (valid)
|
||||
for (const button of BUTTONS)
|
||||
preferences.keys[button] = [
|
||||
...(keys as Record<Button, string[]>)[button],
|
||||
];
|
||||
}
|
||||
return preferences;
|
||||
}
|
||||
|
||||
/** Storage can be missing or refuse access; the defaults apply then. */
|
||||
export function loadPreferences(): Preferences {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
return parse(raw ? JSON.parse(raw) : undefined);
|
||||
} catch {
|
||||
return defaultPreferences();
|
||||
}
|
||||
}
|
||||
export function savePreferences(preferences: Preferences) {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(preferences));
|
||||
} catch {
|
||||
// The preferences still apply until the page reloads.
|
||||
}
|
||||
}
|
||||
|
||||
/** Assign `code` to `button` alone: it leaves any other button it
|
||||
* pressed, and replaces the button's previous keys. */
|
||||
export function rebind(
|
||||
preferences: Preferences,
|
||||
button: Button,
|
||||
code: string,
|
||||
): Preferences {
|
||||
if (RESERVED_KEYS.includes(code)) return preferences;
|
||||
const keys = { ...preferences.keys };
|
||||
for (const other of BUTTONS)
|
||||
keys[other] = keys[other].filter((existing) => existing !== code);
|
||||
keys[button] = [code];
|
||||
return { ...preferences, keys };
|
||||
}
|
||||
|
||||
/** The keyboard map the controls read: code to button. */
|
||||
export function keyMap(preferences: Preferences): Map<string, Button> {
|
||||
const map = new Map<string, Button>();
|
||||
for (const button of BUTTONS)
|
||||
for (const code of preferences.keys[button]) map.set(code, button);
|
||||
return map;
|
||||
}
|
||||
|
||||
const KEY_NAMES: Record<string, string> = {
|
||||
ArrowUp: "↑",
|
||||
ArrowDown: "↓",
|
||||
ArrowLeft: "←",
|
||||
ArrowRight: "→",
|
||||
ShiftLeft: "Shift",
|
||||
ShiftRight: "Shift",
|
||||
ControlLeft: "Ctrl",
|
||||
ControlRight: "Ctrl",
|
||||
AltLeft: "Alt",
|
||||
AltRight: "Alt",
|
||||
Enter: "Enter",
|
||||
Backspace: "Backspace",
|
||||
};
|
||||
|
||||
/** A readable name for a keyboard code: "KeyX" is "X", "Digit1" is "1". */
|
||||
export function keyName(code: string): string {
|
||||
return (
|
||||
KEY_NAMES[code] ??
|
||||
code
|
||||
.replace(/^Key/, "")
|
||||
.replace(/^Digit/, "")
|
||||
.replace(/^Numpad/, "Numpad ")
|
||||
);
|
||||
}
|
||||
|
||||
/** The names of a button's keys, without repeats ("Shift" once). */
|
||||
export function buttonKeys(preferences: Preferences, button: Button): string {
|
||||
const names = [...new Set(preferences.keys[button].map(keyName))];
|
||||
return names.length ? names.join(" / ") : "None";
|
||||
}
|
||||
|
||||
/** Apply the text scale to the page. */
|
||||
export function applyTextScale(scale: TextScale) {
|
||||
document.documentElement.style.setProperty("--text-scale", String(scale));
|
||||
}
|
||||
+4
-3
@@ -8,7 +8,7 @@ export function renderShell(root: HTMLElement) {
|
||||
root.innerHTML = `
|
||||
<header class="site-header">
|
||||
<a class="brand" href="./" aria-label="Crystal home">CRYSTAL</a>
|
||||
<div class="header-state"><span class="power"><i id="power-light"></i><span id="power-label">STANDBY</span></span><button id="help" class="text-button">Help</button></div>
|
||||
<div class="header-state"><span class="power"><i id="power-light"></i><span id="power-label">STANDBY</span></span><button id="settings" class="text-button">Settings</button><button id="help" class="text-button">Help</button></div>
|
||||
</header>
|
||||
<main class="app-shell">
|
||||
<nav class="tabs" role="tablist" aria-label="Crystal sections">
|
||||
@@ -18,7 +18,7 @@ export function renderShell(root: HTMLElement) {
|
||||
<div class="console">
|
||||
<div class="screen-bezel">
|
||||
<div class="screen">
|
||||
<canvas id="game-screen" width="160" height="144" tabindex="0" aria-label="Game screen. Arrow keys move, X is A, Z is B, Enter is Start, Shift is Select."></canvas>
|
||||
<canvas id="game-screen" width="160" height="144" tabindex="0" aria-label="Game screen"></canvas>
|
||||
<div id="screen-empty" class="screen-empty">
|
||||
<form class="load-card" onsubmit="return false">
|
||||
<p class="menu-title">NEW SESSION</p><h1>Load your Crystal ROM</h1><p>English US/Europe · Revision 1.1</p>
|
||||
@@ -80,5 +80,6 @@ export function renderShell(root: HTMLElement) {
|
||||
<p id="save-status" class="save-status">Session backups stay in this browser.</p>
|
||||
</div>
|
||||
<input id="rom-file" class="file-input" type="file" accept=".gbc,.gb,.rom,application/octet-stream">
|
||||
<dialog id="help-dialog"><button id="close-help" class="dialog-close" aria-label="Close help">×</button><p class="menu-title">HOW TO PLAY</p><h2>From New Bark Town to Mr. Pokémon</h2><p>Choose your own English US/Europe revision 1.1 ROM, then press Play. Graphics, maps, sprites and dialogue are read locally from your ROM.</p><dl><dt>Move</dt><dd>Arrow keys</dd><dt>Interact / Continue</dt><dd>X / Z or A / B</dd></dl><p>Sessions save every 10 seconds and when paused. Switching tabs or apps pauses play.</p><p>This milestone runs from your home and Professor Elm's lab, with the original scenes and your first Pokémon, along Route 29 to Cherrygrove City and up Route 30 to Mr. Pokémon's house: wild and trainer battles, items, the Poké Mart, the Pokémon Center and the first rival battle. Catch Pokémon with Poké Balls and heal with Potions from the battle PACK, and switch Pokémon with PKMN. The Pokémon Center PC stores Pokémon and items. Party and bag menus and audio are still planned.</p><dl><dt>Choose</dt><dd>Up / Down, then X or A. Z or B answers No.</dd></dl><p>An optional walking companion may support egg progress after party, eggs and hatching are complete.</p></dialog>`;
|
||||
<dialog id="help-dialog" aria-labelledby="help-title"><button id="close-help" class="dialog-close" aria-label="Close help">×</button><p class="menu-title">HOW TO PLAY</p><h2 id="help-title">Playing Crystal in your browser</h2><p>Choose your own English US/Europe revision 1.1 ROM, then press Play. Graphics, maps, sprites and dialogue are read locally from your ROM.</p><dl id="help-keys"></dl><p>On a touch screen, use the on-screen pad and buttons. Settings changes the keys and the text size.</p><p>Sessions save every 10 seconds and when paused. Switching tabs or apps pauses play. The Map tab lists the areas open in this port; the others appear at the edges but cannot be entered yet.</p></dialog>
|
||||
<dialog id="settings-dialog" aria-labelledby="settings-title"><button id="close-settings" class="dialog-close" aria-label="Close settings">×</button><p class="menu-title">SETTINGS</p><h2 id="settings-title">Display and controls</h2><fieldset class="text-size"><legend>Text size</legend><label><input type="radio" name="text-scale" value="1"> Standard</label><label><input type="radio" name="text-scale" value="1.25"> Large</label><label><input type="radio" name="text-scale" value="1.5"> Largest</label></fieldset><section class="key-settings" aria-labelledby="keys-title"><h3 id="keys-title">Keyboard controls</h3><p>Choose Change, then press the key to use. Tab, Escape and Space keep their usual roles.</p><table><thead><tr><th scope="col">Button</th><th scope="col">Key</th><th scope="col"><span class="visually-hidden">Change</span></th></tr></thead><tbody id="key-table"></tbody></table><button id="reset-keys" class="menu-button">Reset controls</button><p id="settings-status" class="settings-status" role="status" aria-live="polite"></p></section></dialog>`;
|
||||
}
|
||||
+83
-21
@@ -1,10 +1,11 @@
|
||||
:root {
|
||||
--text-scale: 1;
|
||||
font-family:
|
||||
"SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", monospace;
|
||||
color: #282831;
|
||||
background: #d9e2dd;
|
||||
font-synthesis: none;
|
||||
font-size: 14px;
|
||||
font-size: calc(14px * var(--text-scale));
|
||||
line-height: 1.45;
|
||||
--cream: #fffbea;
|
||||
--cream-dark: #eee8ce;
|
||||
@@ -95,7 +96,7 @@ label:focus-within {
|
||||
gap: 12px;
|
||||
}
|
||||
.power {
|
||||
font-size: 10px;
|
||||
font-size: calc(10px * var(--text-scale));
|
||||
letter-spacing: 0.08em;
|
||||
}
|
||||
.power i {
|
||||
@@ -142,7 +143,7 @@ label:focus-within {
|
||||
border: 2px solid #566675;
|
||||
border-bottom: 0;
|
||||
color: #27394a;
|
||||
font-size: 12px;
|
||||
font-size: calc(12px * var(--text-scale));
|
||||
}
|
||||
.tabs button[aria-selected="true"] {
|
||||
position: relative;
|
||||
@@ -153,7 +154,7 @@ label:focus-within {
|
||||
}
|
||||
.tabs button span {
|
||||
margin-right: 5px;
|
||||
font-size: 9px;
|
||||
font-size: calc(9px * var(--text-scale));
|
||||
}
|
||||
.tab-panel {
|
||||
min-height: 0;
|
||||
@@ -203,7 +204,7 @@ label:focus-within {
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: #c5dced;
|
||||
font-size: 9px;
|
||||
font-size: calc(9px * var(--text-scale));
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ label:focus-within {
|
||||
.menu-title {
|
||||
margin: 0 0 7px;
|
||||
color: var(--blue);
|
||||
font-size: 11px;
|
||||
font-size: calc(11px * var(--text-scale));
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.15em;
|
||||
}
|
||||
@@ -248,12 +249,12 @@ p {
|
||||
.menu-panel h2,
|
||||
dialog h2 {
|
||||
margin: 0 0 10px;
|
||||
font-size: clamp(17px, 2.5vw, 25px);
|
||||
font-size: calc(clamp(17px, 2.5vw, 25px) * var(--text-scale));
|
||||
line-height: 1.15;
|
||||
}
|
||||
.load-card > p:not(.menu-title) {
|
||||
margin: 7px 0 12px;
|
||||
font-size: 11px;
|
||||
font-size: calc(11px * var(--text-scale));
|
||||
}
|
||||
.primary-button,
|
||||
.menu-button {
|
||||
@@ -294,6 +295,9 @@ dialog h2 {
|
||||
left: 12px;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
/* Large text never pushes the box above the screen; it scrolls. */
|
||||
max-height: calc(100% - 24px);
|
||||
overflow-y: auto;
|
||||
padding: 12px;
|
||||
color: var(--ink);
|
||||
background: var(--cream);
|
||||
@@ -801,7 +805,7 @@ dialog h2 {
|
||||
max-width: 128px;
|
||||
margin: 3px 0 0;
|
||||
text-align: center;
|
||||
font-size: 8px;
|
||||
font-size: calc(8px * var(--text-scale));
|
||||
}
|
||||
|
||||
.controls {
|
||||
@@ -876,7 +880,7 @@ dialog h2 {
|
||||
}
|
||||
.system-keys .game-key {
|
||||
padding: 4px 7px;
|
||||
font-size: 8px;
|
||||
font-size: calc(8px * var(--text-scale));
|
||||
background: #6d7478;
|
||||
}
|
||||
|
||||
@@ -1006,7 +1010,7 @@ dialog h2 {
|
||||
}
|
||||
.atlas-legend {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-size: calc(12px * var(--text-scale));
|
||||
color: #5f6763;
|
||||
}
|
||||
.atlas-legend span {
|
||||
@@ -1033,7 +1037,7 @@ dialog h2 {
|
||||
}
|
||||
.planned-panel > p:last-child {
|
||||
max-width: 680px;
|
||||
font-size: 15px;
|
||||
font-size: calc(15px * var(--text-scale));
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
@@ -1056,13 +1060,13 @@ dialog h2 {
|
||||
.status {
|
||||
grid-area: status;
|
||||
margin: 0;
|
||||
font-size: 11px;
|
||||
font-size: calc(11px * var(--text-scale));
|
||||
}
|
||||
.game-notice {
|
||||
grid-area: notice;
|
||||
margin: 0;
|
||||
color: #b8d4e6;
|
||||
font-size: 10px;
|
||||
font-size: calc(10px * var(--text-scale));
|
||||
}
|
||||
.game-notice:empty {
|
||||
display: none;
|
||||
@@ -1073,7 +1077,7 @@ dialog h2 {
|
||||
max-width: 280px;
|
||||
text-align: right;
|
||||
color: #b8c7ce;
|
||||
font-size: 9px;
|
||||
font-size: calc(9px * var(--text-scale));
|
||||
}
|
||||
.error {
|
||||
color: #ffb0a1 !important;
|
||||
@@ -1101,10 +1105,68 @@ dialog dl {
|
||||
dialog dd {
|
||||
margin: 0;
|
||||
}
|
||||
/* Read by screen readers, not shown. */
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
clip-path: inset(50%);
|
||||
white-space: nowrap;
|
||||
}
|
||||
/* Settings: text size choices and the key table. */
|
||||
#settings-dialog {
|
||||
max-height: calc(100% - 24px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
.text-size {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 18px;
|
||||
margin: 0 0 18px;
|
||||
padding: 10px 12px;
|
||||
border: 2px solid var(--ink);
|
||||
}
|
||||
.text-size label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 32px;
|
||||
}
|
||||
.key-settings h3 {
|
||||
margin: 0 0 6px;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.key-settings table {
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.key-settings th,
|
||||
.key-settings td {
|
||||
padding: 4px 6px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid var(--cream-dark);
|
||||
}
|
||||
.key-settings td:last-child {
|
||||
text-align: right;
|
||||
}
|
||||
.key-settings td button {
|
||||
min-height: 32px;
|
||||
padding: 2px 10px;
|
||||
font: inherit;
|
||||
color: var(--ink);
|
||||
background: var(--cream-dark);
|
||||
border: 2px solid var(--ink);
|
||||
}
|
||||
.settings-status {
|
||||
min-height: 1.5em;
|
||||
margin: 8px 0 0;
|
||||
}
|
||||
.dialog-close {
|
||||
float: right;
|
||||
background: transparent;
|
||||
font-size: 22px;
|
||||
font-size: calc(22px * var(--text-scale));
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -1117,7 +1179,7 @@ dialog dd {
|
||||
.tabs button {
|
||||
min-width: 83px;
|
||||
padding: 6px 9px;
|
||||
font-size: 10px;
|
||||
font-size: calc(10px * var(--text-scale));
|
||||
}
|
||||
.console {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -1177,7 +1239,7 @@ dialog dd {
|
||||
box-shadow: 3px 3px 0 #2d6685;
|
||||
}
|
||||
.load-card h1 {
|
||||
font-size: 16px;
|
||||
font-size: calc(16px * var(--text-scale));
|
||||
}
|
||||
.minimap-overlay {
|
||||
top: 4px;
|
||||
@@ -1195,10 +1257,10 @@ dialog dd {
|
||||
padding-block: 4px;
|
||||
}
|
||||
.status {
|
||||
font-size: 9px;
|
||||
font-size: calc(9px * var(--text-scale));
|
||||
}
|
||||
.save-status {
|
||||
font-size: 8px;
|
||||
font-size: calc(8px * var(--text-scale));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1598,7 +1660,7 @@ dialog dd {
|
||||
}
|
||||
.pokedex-tab-status {
|
||||
max-width: 680px;
|
||||
font-size: 15px;
|
||||
font-size: calc(15px * var(--text-scale));
|
||||
line-height: 1.8;
|
||||
}
|
||||
.pokedex-tab-body {
|
||||
|
||||
Reference in new issue
Block a user