{
  "$schema": "https://mian.ro/schema/v2/registry.json",
  "schemaVersion": "2.0.0",
  "generatedAt": "2026-10-02T13:14:06.957Z",
  "id": "theme-controller",
  "name": "Theme controller",
  "category": "Layout & utilities",
  "description": "Switch noodle and light/dark mode for a page, or scope a noodle to any element.",
  "tags": [
    "theme",
    "dark mode",
    "light mode",
    "noodle switcher",
    "color scheme"
  ],
  "requiresJs": true,
  "license": "LicenseRef-MIAN-1.0",
  "composition": {
    "role": null,
    "intents": [
      "theme switcher",
      "dark mode toggle",
      "change noodle",
      "appearance setting"
    ],
    "prominence": null,
    "density": null,
    "worksAfter": [],
    "worksBefore": [],
    "alternatives": [],
    "avoid": null
  },
  "url": "https://mian.ro/components/theme-controller",
  "markdownUrl": "https://mian.ro/ai/components/theme-controller.md",
  "classes": [
    {
      "name": "data-noodle=\"…\" · data-mode=\"light | dark\"",
      "description": "On <html> for the page, or on any element to scope it."
    },
    {
      "name": "select[data-mn-noodle-select]",
      "description": "A select that switches the page noodle."
    },
    {
      "name": "data-mn-target=\"#id\"",
      "description": "On any control: act on that element only, without persisting."
    },
    {
      "name": "[data-mn-set-noodle] · [data-mn-set-mode] · [data-mn-toggle-mode]",
      "description": "Buttons."
    },
    {
      "name": "Mian.setNoodle(id) · Mian.setMode(mode)",
      "description": "Script API; persisted in localStorage."
    },
    {
      "name": "mn-swatch",
      "description": "A 4-colour preview of the current noodle."
    }
  ],
  "accessibility": [
    "Buttons report aria-pressed. Without data-mode the page follows the operating system."
  ],
  "examples": [
    {
      "title": "Scoped controls",
      "description": "data-mn-target points the controls at one element, so only it changes. Leave data-mn-target out to switch the whole page (and remember the choice).",
      "html": "<div class=\"mn-row\">\n  <select class=\"mn-select mn-input-sm\" data-mn-noodle-select data-mn-target=\"#demo-area\" aria-label=\"Noodle for the demo area\" style=\"width:auto\">\n  <option value=\"mian\">Mian</option>\n  <option value=\"yangchun\">Yangchun</option>\n  <option value=\"linguine\">Linguine</option>\n  <option value=\"penne\">Penne</option>\n  <option value=\"chitarra\">Chitarra</option>\n  <option value=\"shio\">Shio</option>\n  <option value=\"spaghetti\">Spaghetti</option>\n  <option value=\"bucatini\">Bucatini</option>\n  <option value=\"hofun\">Hofun</option>\n  <option value=\"hiyamugi\">Hiyamugi</option>\n  <option value=\"ramyeon\">Ramyeon</option>\n  <option value=\"fettuccine\">Fettuccine</option>\n  <option value=\"lasagne\">Lasagne</option>\n  <option value=\"pappardelle\">Pappardelle</option>\n  <option value=\"paccheri\">Paccheri</option>\n  <option value=\"farfalle\">Farfalle</option>\n  <option value=\"yinzhen\">Yinzhen</option>\n  <option value=\"quadretti\">Quadretti</option>\n  <option value=\"acini\">Acini di pepe</option>\n  <option value=\"kalguksu\">Kalguksu</option>\n  <option value=\"sanuki\">Sanuki</option>\n  <option value=\"hakata\">Hakata</option>\n  <option value=\"ditalini\">Ditalini</option>\n  <option value=\"juksing\">Juksing</option>\n  <option value=\"kishimen\">Kishimen</option>\n  <option value=\"lamian\">LaMian</option>\n  <option value=\"somen\">Somen</option>\n  <option value=\"capellini\">Capellini</option>\n  <option value=\"longxu\">Longxu</option>\n  <option value=\"misua\">Misua</option>\n  <option value=\"bun\">Bún</option>\n  <option value=\"kuzukiri\">Kuzukiri</option>\n  <option value=\"sotanghon\">Sotanghon</option>\n  <option value=\"harusame\">Harusame</option>\n  <option value=\"fensi\">Fensi</option>\n  <option value=\"tokoroten\">Tokoroten</option>\n  <option value=\"nagashi\">Nagashi</option>\n  <option value=\"liangfen\">Liangfen</option>\n  <option value=\"dangmyeon\">Dangmyeon</option>\n  <option value=\"woonsen\">Woonsen</option>\n  <option value=\"shirataki\">Shirataki</option>\n  <option value=\"chasoba\">Cha Soba</option>\n  <option value=\"daoxiao\">Daoxiao</option>\n  <option value=\"pici\">Pici</option>\n  <option value=\"naengmyeon\">Naengmyeon</option>\n  <option value=\"hoto\">Hōtō</option>\n  <option value=\"ramen\">Ramen</option>\n  <option value=\"biangbiang\">Biangbiang</option>\n  <option value=\"maccheroni\">Maccheroni</option>\n  <option value=\"rigatoni\">Rigatoni</option>\n  <option value=\"fusilli\">Fusilli</option>\n  <option value=\"jjajang\">Jjajang</option>\n  <option value=\"laksa\">Laksa</option>\n  <option value=\"soba\">Soba</option>\n  <option value=\"tagliatelle\">Tagliatelle</option>\n  <option value=\"tajarin\">Tajarin</option>\n  <option value=\"mafaldine\">Mafaldine</option>\n  <option value=\"corzetti\">Corzetti</option>\n  <option value=\"inaniwa\">Inaniwa</option>\n  <option value=\"yifu\">Yifu</option>\n  <option value=\"udon\">Udon</option>\n  <option value=\"gnocchi\">Gnocchi</option>\n  <option value=\"cavatelli\">Cavatelli</option>\n  <option value=\"orecchiette\">Orecchiette</option>\n  <option value=\"mee\">Mee</option>\n  <option value=\"mixian\">Mixian</option>\n  <option value=\"pho\">Pho</option>\n  </select>\n  <div class=\"mn-toggle-group\" aria-label=\"Mode for the demo area\">\n    <button data-mn-set-mode=\"light\" data-mn-target=\"#demo-area\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"4\"/><path d=\"M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4\"/></svg> Light</button>\n    <button data-mn-set-mode=\"dark\" data-mn-target=\"#demo-area\"><svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><path d=\"M20 14.5A8 8 0 0 1 9.5 4a8 8 0 1 0 10.5 10.5Z\"/></svg> Dark</button>\n    <button data-mn-set-mode=\"auto\" data-mn-target=\"#demo-area\">Auto</button>\n  </div>\n</div>\n<div id=\"demo-area\" data-noodle=\"mian\" class=\"mn-card\" style=\"margin-top:1rem;padding:1.25rem;background:var(--mn-bg)\">\n  <div class=\"mn-row\"><button class=\"mn-btn\">Primary</button><button class=\"mn-btn mn-btn-outline\">Outline</button><input class=\"mn-switch\" type=\"checkbox\" role=\"switch\" checked aria-label=\"Example switch\"><span class=\"mn-badge mn-badge-soft\">New</span></div>\n</div>"
    },
    {
      "title": "Scoped noodles",
      "description": "Any element can carry its own noodle and mode.",
      "html": "<div class=\"mn-row\" style=\"align-items:stretch\">\n  <div data-noodle=\"udon\" class=\"mn-card\" style=\"padding:1rem;gap:.75rem;min-width:9rem;background:var(--mn-bg)\">\n    <span class=\"mn-swatch\"><i></i><i></i><i></i><i></i></span><strong style=\"text-transform:capitalize\">udon</strong><button class=\"mn-btn mn-btn-sm\">Button</button>\n  </div>\n  <div data-noodle=\"fensi\" data-mode=\"dark\" class=\"mn-card\" style=\"padding:1rem;gap:.75rem;min-width:9rem;background:var(--mn-bg)\">\n    <span class=\"mn-swatch\"><i></i><i></i><i></i><i></i></span><strong style=\"text-transform:capitalize\">fensi</strong><button class=\"mn-btn mn-btn-sm\">Button</button>\n  </div>\n  <div data-noodle=\"yinzhen\" class=\"mn-card\" style=\"padding:1rem;gap:.75rem;min-width:9rem;background:var(--mn-bg)\">\n    <span class=\"mn-swatch\"><i></i><i></i><i></i><i></i></span><strong style=\"text-transform:capitalize\">yinzhen</strong><button class=\"mn-btn mn-btn-sm\">Button</button>\n  </div>\n  <div data-noodle=\"mee\" data-mode=\"dark\" class=\"mn-card\" style=\"padding:1rem;gap:.75rem;min-width:9rem;background:var(--mn-bg)\">\n    <span class=\"mn-swatch\"><i></i><i></i><i></i><i></i></span><strong style=\"text-transform:capitalize\">mee</strong><button class=\"mn-btn mn-btn-sm\">Button</button>\n  </div>\n</div>"
    }
  ],
  "css": ".mn-swatch {\n  display: inline-grid; grid-template-columns: repeat(2, .55rem); gap: 2px; padding: 3px;\n  border-radius: 5px; background: var(--mn-bg); box-shadow: 0 0 0 1px var(--mn-border);\n}\n.mn-swatch > i { width: .55rem; height: .55rem; border-radius: 2px; }\n.mn-swatch > i:nth-child(1) { background: var(--mn-primary); }\n.mn-swatch > i:nth-child(2) { background: var(--mn-accent); }\n.mn-swatch > i:nth-child(3) { background: var(--mn-fg); }\n.mn-swatch > i:nth-child(4) { background: var(--mn-surface-2); }",
  "js": "/* Mian.setNoodle('udon'), Mian.setMode('dark' | 'light' | 'auto'). Persisted.\n   Any control can instead act on one element: add data-mn-target=\"#id\" and\n   the noodle or mode is set on that element only (not persisted). */\nconst KEY_N = 'mian:noodle', KEY_M = 'mian:mode';\nconst store = (k, v) => { try { v == null ? localStorage.removeItem(k) : localStorage.setItem(k, v); } catch (_) {} };\nconst targetOf = (el) => { const sel = el.getAttribute('data-mn-target'); return sel ? document.querySelector(sel) : null; };\nconst modeOf = (el) => el.getAttribute('data-mode') || (el === document.documentElement ? null : el.closest('[data-mode]')?.getAttribute('data-mode')) || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');\nMian.setNoodle = (n, target) => {\n  const el = target || document.documentElement;\n  if (!target) store(KEY_N, n);\n  // The change cross-fades (see Mian.transition) instead of snapping.\n  const run = () => { el.setAttribute('data-noodle', n); syncTheme(); emit(el, 'theme', { noodle: n, scoped: Boolean(target) }); };\n  Mian.transition ? Mian.transition(run) : run();\n};\nMian.setMode = (m, target) => {\n  const el = target || document.documentElement;\n  if (!target) store(KEY_M, m === 'auto' ? null : m);\n  const run = () => { if (m === 'auto') el.removeAttribute('data-mode'); else el.setAttribute('data-mode', m); syncTheme(); emit(el, 'theme', { mode: m, scoped: Boolean(target) }); };\n  Mian.transition ? Mian.transition(run) : run();\n};\nMian.resolvedMode = (target) => modeOf(target || document.documentElement);\nfunction syncTheme() {\n  const root = document.documentElement;\n  const noodleOf = (c) => (targetOf(c) || root).getAttribute('data-noodle') || 'mian';\n  const setModeOf = (c) => (targetOf(c) || root).getAttribute('data-mode') || 'auto';\n  document.querySelectorAll('[data-mn-noodle-select]').forEach((s) => { s.value = noodleOf(s); });\n  document.querySelectorAll('[data-mn-set-noodle]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.mnSetNoodle === noodleOf(b))));\n  document.querySelectorAll('[data-mn-set-mode]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.mnSetMode === setModeOf(b))));\n  document.querySelectorAll('[data-mn-toggle-mode]').forEach((b) => b.setAttribute('aria-pressed', String(Mian.resolvedMode(targetOf(b)) === 'dark')));\n}\ndocument.addEventListener('click', (e) => {\n  const n = e.target.closest('[data-mn-set-noodle]'); if (n) Mian.setNoodle(n.dataset.mnSetNoodle, targetOf(n));\n  const m = e.target.closest('[data-mn-set-mode]'); if (m) Mian.setMode(m.dataset.mnSetMode, targetOf(m));\n  const t = e.target.closest('[data-mn-toggle-mode]'); if (t) Mian.setMode(Mian.resolvedMode(targetOf(t)) === 'dark' ? 'light' : 'dark', targetOf(t));\n});\ndocument.addEventListener('change', (e) => { if (e.target.matches('[data-mn-noodle-select]')) Mian.setNoodle(e.target.value, targetOf(e.target)); });\nregister('theme', () => syncTheme());",
  "install": {
    "css": "https://mian.ro/dist/mian.css",
    "js": "https://mian.ro/dist/mian.js",
    "registryItem": "https://mian.ro/r/theme-controller.json"
  }
}
