feat(ms-ai-architect): playground v3 theme toggle with localStorage persistence [skip-docs]

Step 13 (Wave 5). Adds light/dark theme toggle to v3 playground.

- Inline <script> in <head> reads ms-ai-architect-theme from localStorage and
  sets <html data-theme="..."> BEFORE stylesheets parse (avoids FOUC).
- New .theme-toggle button in topbar (vendored design-system class).
- ACTIONS['toggle-theme'] flips data-theme, persists to localStorage, and
  syncs all [data-theme-label] elements + aria-label in-place (no re-render).
- Default behavior (no localStorage value or unsupported value) keeps existing
  data-theme="dark" hard-coded on <html>.
This commit is contained in:
Kjell Tore Guttormsen 2026-05-03 20:01:53 +02:00
commit bebe070236

View file

@ -5,6 +5,21 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ms-ai-architect — Playground v3</title>
<!-- Theme bootstrap (Step 13). Må kjøre før stylesheets parses for å unngå
flash-of-wrong-theme (FOUC). Leser ms-ai-architect-theme fra localStorage
og overstyrer <html data-theme="..."> før .css evaluerer.
Wrappes i try/catch — file:// + privatmodus kan blokkere localStorage. -->
<script>
(function () {
try {
var saved = localStorage.getItem('ms-ai-architect-theme');
if (saved === 'light' || saved === 'dark') {
document.documentElement.setAttribute('data-theme', saved);
}
} catch (e) { /* localStorage utilgjengelig — behold default fra HTML-attributtet */ }
})();
</script>
<!-- Vendored design-system. Kilden er shared/playground-design-system/ — synces via
scripts/sync-design-system.mjs ved marketplace-rot. Aldri rediger filer under
playground/vendor/ direkte; endringer går i shared/ + re-sync. -->
@ -1456,6 +1471,9 @@
const crumbHtml = (orgName || crumb)
? '<span class="topbar__crumb">' + (orgName ? escapeHtml(orgName) : '') + (orgName && crumb ? ' · ' : '') + (crumb || '') + '</span>'
: '';
const currentTheme = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
const themeLabel = currentTheme === 'light' ? 'Lys' : 'Mørk';
const themeNext = currentTheme === 'light' ? 'mørk' : 'lys';
return (
'<header class="topbar">' +
'<div class="topbar__brand">' +
@ -1470,6 +1488,9 @@
'<button type="button" class="btn btn--secondary btn--sm" data-action="export-state" aria-label="Eksporter state til JSON">Eksporter</button>' +
'<button type="button" class="btn btn--secondary btn--sm" data-action="import-state" aria-label="Importer state fra JSON">Importer</button>' +
'<input type="file" accept="application/json,.json" data-import-input hidden>' +
'<button type="button" class="theme-toggle" data-action="toggle-theme" aria-label="Bytt til ' + themeNext + ' modus">' +
'<span data-theme-label>' + themeLabel + '</span>' +
'</button>' +
'</nav>' +
'</header>'
);
@ -3625,6 +3646,25 @@
ACTIONS['goto-catalog'] = function () { navigate('catalog'); };
ACTIONS['goto-onboarding'] = function () { navigate('onboarding'); };
// Theme toggle (Step 13). Veksler data-theme på <html>, persisterer i
// localStorage('ms-ai-architect-theme'). Tar høyde for begrensning fra
// file:// + privatmodus. Re-renderer ikke surfaces — endrer kun attributt
// og synkroniserer alle [data-theme-label]-elementer in-place.
ACTIONS['toggle-theme'] = function () {
const current = document.documentElement.getAttribute('data-theme') === 'light' ? 'light' : 'dark';
const next = current === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem('ms-ai-architect-theme', next); } catch (e) { /* ignore */ }
const labels = document.querySelectorAll('[data-theme-label]');
for (let i = 0; i < labels.length; i++) {
labels[i].textContent = next === 'dark' ? 'Mørk' : 'Lys';
}
const buttons = document.querySelectorAll('[data-action="toggle-theme"]');
for (let j = 0; j < buttons.length; j++) {
buttons[j].setAttribute('aria-label', 'Bytt til ' + (next === 'dark' ? 'lys' : 'mørk') + ' modus');
}
};
ACTIONS['open-project'] = function (ev, el) {
const id = el.dataset.projectId;
if (!id) return;