feat(lab): phase 4 -- hash-router for sim deep-links

URL #sim/<name> deep-links: F5 restores sim, browser back/forward
switches between sims, click on sim-card updates URL.

34 sims mapped via _SIM_HASH_MAP (built dynamically from SIMS array).
Unknown hash -> console.warn fallback.
Recursion guard prevents double-activation on programmatic navigate.
closeSim clears hash via history.pushState (no hashchange loop).
Embed mode excluded from hash updates (?embed=1 workflow unaffected).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-22 22:56:18 +03:00
parent 6792a4a5c7
commit 0b9685bc5e
+63
View File
@@ -818,8 +818,71 @@
} else { } else {
renderSims(); renderSims();
if (_autoSim) openSim(_autoSim); if (_autoSim) openSim(_autoSim);
// hash-router: activate sim from URL fragment after catalogue renders
else _activateFromHash();
} }
}); });
lucide.createIcons(); lucide.createIcons();
LS.notif.init(); LS.notif.init();
} }
/* ─── Hash router for sim deep-links ─────────────────────────────────────
URL pattern: /lab#sim/<name>
<name> matches SIMS[i].id (e.g. 'projectile', 'graph', 'chemsandbox').
F5 restores sim. Browser back/forward switches between sims.
Click on sim-card updates URL via wrapped openSim.
──────────────────────────────────────────────────────────────────────── */
// Build valid-id set from SIMS catalogue (filters out "coming soon" entries)
const _SIM_HASH_MAP = {};
SIMS.forEach(function(s) { if (s.id) { _SIM_HASH_MAP[s.id] = s.id; } });
var _routerNavigating = false;
function _activateFromHash() {
var m = (location.hash || '').match(/^#sim\/([\w-]+)/);
if (!m) return false;
var simName = m[1];
if (!_SIM_HASH_MAP[simName]) {
// eslint-disable-next-line no-console
window.console && window.console.warn('lab-router: unknown sim', simName);
return false;
}
openSim(simName);
return true;
}
// Intercept openSim to push URL hash on user-initiated navigation
var _origOpenSim = openSim;
openSim = function(id) {
_origOpenSim(id);
if (!_routerNavigating && !_embedMode) {
var baseId = id.includes(':') ? id.split(':')[0] : id;
if (_SIM_HASH_MAP[baseId]) {
_routerNavigating = true;
location.hash = '#sim/' + baseId;
// use setTimeout so hashchange fires after flag is set
setTimeout(function() { _routerNavigating = false; }, 0);
}
}
};
// Intercept closeSim to clear hash when returning to home grid
var _origCloseSim = closeSim;
closeSim = function() {
_origCloseSim();
if (!_embedMode) {
_routerNavigating = true;
history.pushState(null, '', location.pathname + location.search);
setTimeout(function() { _routerNavigating = false; }, 0);
}
};
// Browser back/forward navigation
window.addEventListener('hashchange', function() {
if (_routerNavigating) return;
var hasHash = _activateFromHash();
if (!hasHash && document.getElementById('lab-sim').classList.contains('open')) {
_origCloseSim();
}
});