mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 12:52:29 +00:00
Puts every external `<script>` in the v3 shell into the deferred queue, and
keeps each script's boot() firing at DOMContentLoaded exactly as it does today.
Behaviourally a no-op; it is what makes the ES-module flips safe.
WHY. `type="module"` defers execution to after HTML parse. Classic-`defer` and
module scripts share ONE "execute after parsing" list and run in DOCUMENT ORDER,
but a plain classic script runs DURING parse — ahead of all of them. So the
moment capabilities.js becomes a module while app.js is still plain, app.js runs
FIRST, and its 11 top-level `window.feedBack.on(...)` calls (app.js:6245-6722)
hit a bare `{}` — `_ensureFeedBackEventBus()` (capabilities.js:33), which
attaches .on/.emit/.off, would not have run yet. TypeError, app.js dies
mid-parse. Deferring everything now keeps document order == execution order
through the rest of the migration.
THE CATCH (Codex preflight caught this — a real ordering change). 22 scripts
guard their boot with `if (document.readyState === 'loading')`. A deferred
script runs at readyState 'interactive', so that test is FALSE and the else-branch
fires boot() immediately, at the script's position in document order — instead of
at DOMContentLoaded, after every script has evaluated.
That matters far more than one call site: a scan of the shell's scripts found
**43 forward references** where a script's boot() reads a global that a LATER
script defines (shell.js -> profile.js's window.v3Onboarding, songs.js ->
settings.js's window._confirmDialog, badges.js -> songs.js's
window.displayTuningName, ...). Every one of them resolves today only because
all boots happen at DOMContentLoaded. So the guards now treat 'interactive' as
not-ready (`!== 'complete'`), restoring that exactly.
Codex's specific finding (first-run onboarding silently skipped) did NOT
reproduce — shell.js's boot() awaits /api/profile, and that yield lets the
remaining deferred scripts run first. But the race it described is real, the
guard is silent when it fails (`&& window.v3Onboarding`), and the other 42
forward refs have no such await protecting them. Fixed at the root rather than
at the one site.
VERIFIED. A/B against origin/main on a fresh profile, 13 probes (onboarding
overlay, v3Onboarding/v3Songs/v3Profile/fbNotify/v3Badges/uiPrompt/showScreen,
bus, capabilities.version, createHighway, plugin scripts, mounted screens):
IDENTICAL, zero console/page errors on both. pytest 2396, node 1028/1028,
ESLint 0 errors, Codex 0.
New guard: test_every_external_script_defers_so_document_order_is_execution_order
fails if any external tag is plain classic — verified to fail on a single
reverted tag, so it actually bites.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
122 lines
4.9 KiB
JavaScript
122 lines
4.9 KiB
JavaScript
// Core interface-scale capability — the app-wide "Interface size" preference.
|
|
//
|
|
// Owns a single user setting: a multiplier applied to the ROOT font-size, so
|
|
// the rem-based v3 chrome (menus, buttons, text, spacing) scales together. This
|
|
// is the DOM lever — it deliberately does NOT touch the gameplay highway canvas
|
|
// (which is sized in device pixels, not rem), so scaling the UI never changes
|
|
// playback resolution or FPS.
|
|
//
|
|
// It is exposed as a host read/write API on `window.feedBack.scale` so surfaces
|
|
// that can't inherit `rem` — canvas / WebGL renderers such as the note-highway
|
|
// HUD or results scorecards — can read the number via `feedBack.scale.get()`
|
|
// and follow `scale:changed`. Shape mirrors the other host capabilities (a
|
|
// frozen, versioned object) and the working-tuning read-API: synchronous
|
|
// `get()`, a `set()` mutator, and a change event that also fires once on load.
|
|
//
|
|
// The visual apply ALSO runs pre-paint from a tiny inline <head> script (see
|
|
// index.html) so there is no flash-of-reflow on load; this module is the
|
|
// authoritative owner and re-applies idempotently.
|
|
(function () {
|
|
'use strict';
|
|
window.feedBack = window.feedBack || {};
|
|
if (window.feedBack.scale && window.feedBack.scale.version === 1) return;
|
|
|
|
var STORE_KEY = 'v3-interface-scale';
|
|
var MIN = 0.85, MAX = 1.50, DEFAULT = 1.0;
|
|
// The named presets rendered by the Settings segmented control. Kept here so
|
|
// the control and any consumer read the ladder from one source of truth.
|
|
var PRESETS = [
|
|
{ step: 'small', value: 0.90 },
|
|
{ step: 'medium', value: 1.00 },
|
|
{ step: 'large', value: 1.15 },
|
|
{ step: 'x-large', value: 1.30 },
|
|
];
|
|
|
|
function clamp(n) {
|
|
n = Number(n);
|
|
if (!isFinite(n)) return DEFAULT;
|
|
return Math.min(MAX, Math.max(MIN, n));
|
|
}
|
|
|
|
function stepFor(value) {
|
|
for (var i = 0; i < PRESETS.length; i++) {
|
|
if (Math.abs(PRESETS[i].value - value) < 0.001) return PRESETS[i].step;
|
|
}
|
|
return 'custom';
|
|
}
|
|
|
|
function read() {
|
|
try {
|
|
var raw = localStorage.getItem(STORE_KEY);
|
|
if (raw == null) return DEFAULT;
|
|
return clamp(parseFloat(raw));
|
|
} catch (_) { return DEFAULT; }
|
|
}
|
|
|
|
var current = read();
|
|
|
|
// Apply to the DOM. The lever is a RELATIVE root font-size (a percentage of
|
|
// the user-agent base), never a px literal — so a user who raised their
|
|
// browser/OS base font size is respected, not silently overridden. We also
|
|
// publish the always-present `--fb-scale` token for canvas consumers and CSS.
|
|
function apply(value) {
|
|
var el = document.documentElement;
|
|
if (!el) return;
|
|
el.style.setProperty('--fb-scale', String(value));
|
|
// Medium (1.0) clears the inline override so default rendering is
|
|
// byte-identical to before this feature existed (zero blast radius).
|
|
el.style.fontSize = (Math.abs(value - 1) < 0.001) ? '' : (value * 100).toFixed(2) + '%';
|
|
}
|
|
|
|
function persist(value) {
|
|
try {
|
|
if (Math.abs(value - DEFAULT) < 0.001) localStorage.removeItem(STORE_KEY);
|
|
else localStorage.setItem(STORE_KEY, String(value));
|
|
} catch (_) { /* private mode */ }
|
|
}
|
|
|
|
function announce() {
|
|
try {
|
|
if (typeof window.feedBack.emit === 'function') {
|
|
window.feedBack.emit('scale:changed', { value: current, step: stepFor(current) });
|
|
}
|
|
} catch (_) { /* noop */ }
|
|
}
|
|
|
|
// Hydrate on load (idempotent with the pre-paint inline script).
|
|
apply(current);
|
|
|
|
window.feedBack.scale = Object.freeze({
|
|
version: 1,
|
|
min: MIN,
|
|
max: MAX,
|
|
default: DEFAULT,
|
|
// Synchronous — valid immediately after this module parses.
|
|
get: function () { return { value: current, step: stepFor(current) }; },
|
|
// A copy of the preset ladder, so a UI can render it from one source.
|
|
presets: function () {
|
|
return PRESETS.map(function (p) { return { step: p.step, value: p.value }; });
|
|
},
|
|
// Set + apply + persist + announce. Pass { persist:false } for a
|
|
// transient preview (e.g. a live slider drag) that shouldn't be written.
|
|
set: function (value, opts) {
|
|
var v = clamp(value);
|
|
current = v;
|
|
apply(v);
|
|
if (!opts || opts.persist !== false) persist(v);
|
|
announce();
|
|
return current;
|
|
},
|
|
});
|
|
|
|
// Announce once after the document parses, so any listener wired during page
|
|
// load can sync without special-casing (consumers may also just call get()).
|
|
// `defer` runs this at readyState 'interactive' — later scripts have not
|
|
// evaluated yet, so wait for DOMContentLoaded (see static/v3/index.html).
|
|
if (document.readyState !== 'complete') {
|
|
document.addEventListener('DOMContentLoaded', announce, { once: true });
|
|
} else {
|
|
announce();
|
|
}
|
|
})();
|