diff --git a/.cdp/cdp-check.cjs b/.cdp/cdp-check.cjs
new file mode 100644
index 0000000..894f171
--- /dev/null
+++ b/.cdp/cdp-check.cjs
@@ -0,0 +1,59 @@
+/* Verify computed styles for a given theme class after runtime swap. */
+const { spawn } = require("node:child_process");
+const fs = require("node:fs");
+const http = require("node:http");
+const EDGE = "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe";
+const PORT = 9337;
+function httpJson(path) {
+ return new Promise((resolve, reject) => {
+ const req = http.request({ host: "127.0.0.1", port: PORT, path }, (res) => {
+ let d = "";
+ res.on("data", (c) => (d += c));
+ res.on("end", () => { try { resolve(JSON.parse(d)); } catch { resolve(d); } });
+ });
+ req.on("error", reject); req.end();
+ });
+}
+(async () => {
+ const profile = fs.mkdtempSync("/tmp/edge-prof-");
+ const proc = spawn(EDGE, ["--headless=new", `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`, "--no-first-run", "--no-default-browser-check", "--disable-gpu", "about:blank"], { stdio: "ignore" });
+ let targets = null;
+ for (let i = 0; i < 40; i++) { await new Promise((r) => setTimeout(r, 500)); try { targets = await httpJson("/json/list"); break; } catch {} }
+ if (!targets) { console.error("no targets"); process.exit(1); }
+ const page = targets.find((t) => t.type === "page");
+ const ws = new WebSocket(page.webSocketDebuggerUrl);
+ await new Promise((r) => (ws.onopen = r));
+ let id = 0;
+ const send = (method, params = {}) => new Promise((resolve) => {
+ const myId = ++id;
+ ws.send(JSON.stringify({ id: myId, method, params }));
+ const t = setInterval(() => { const m = ws._buf.find((b) => b.id === myId); if (m) { clearInterval(t); resolve(m.result ?? m.error); } }, 10);
+ });
+ ws._buf = [];
+ ws.onmessage = (m) => { const msg = JSON.parse(m.data); if (msg.id) { ws._buf.push(msg); return; }
+ if (msg.method === "Runtime.consoleAPICalled") console.log("[c]", msg.params.args.map((a) => a.value ?? a.description ?? a.type).join(" ").slice(0, 160)); };
+ await send("Runtime.enable");
+ await send("Network.enable");
+ const cookieFile = process.argv[2];
+ if (cookieFile) {
+ const line = fs.readFileSync(cookieFile, "utf8").split("\n").map((l) => l.trim()).find((l) => l.includes("authjs.session-token"));
+ if (line) await send("Network.setCookies", { cookies: [{ name: "authjs.session-token", value: line.split("\t").pop(), domain: "localhost", path: "/", httpOnly: true, sameSite: "Lax" }] });
+ }
+ await send("Page.enable");
+ await send("Page.navigate", { url: "http://localhost:3210/" });
+ await new Promise((r) => setTimeout(r, 4000));
+ const theme = process.argv[3] || "dark";
+ await send("Runtime.evaluate", { expression: `document.documentElement.className = ${JSON.stringify(theme)}` });
+ await new Promise((r) => setTimeout(r, 1500));
+ const info = await send("Runtime.evaluate", { expression: `(() => {
+ const card = document.querySelector('[aria-label^="Drag to move"]')?.closest('[class*="rounded-2xl"]');
+ const checkbox = card ? card.querySelector('button[class*="size-4"]') : null;
+ return JSON.stringify({
+ htmlClass: document.documentElement.className.split(" ").slice(-1)[0],
+ checkboxBorder: checkbox ? getComputedStyle(checkbox).borderColor : null,
+ cardBg: card ? getComputedStyle(card).backgroundColor : null,
+ });
+ })()`, returnByValue: true });
+ console.log(info?.result?.value);
+ proc.kill(); process.exit(0);
+})().catch((e) => { console.error(e.message); process.exit(1); });
diff --git a/.cdp/cdp-console.cjs b/.cdp/cdp-console.cjs
new file mode 100644
index 0000000..8cf3a49
--- /dev/null
+++ b/.cdp/cdp-console.cjs
@@ -0,0 +1,81 @@
+/* Print console messages + page errors for one URL in headless Edge. */
+const { spawn } = require("node:child_process");
+const fs = require("node:fs");
+const http = require("node:http");
+
+const EDGE = "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe";
+const PORT = 9334;
+const url = process.argv[2] || "http://localhost:3210/";
+const cookieFile = process.argv[3];
+
+function httpJson(path) {
+ return new Promise((resolve, reject) => {
+ const req = http.request({ host: "127.0.0.1", port: PORT, path }, (res) => {
+ let d = "";
+ res.on("data", (c) => (d += c));
+ res.on("end", () => { try { resolve(JSON.parse(d)); } catch { resolve(d); } });
+ });
+ req.on("error", reject);
+ req.end();
+ });
+}
+
+(async () => {
+ const profile = fs.mkdtempSync("/tmp/edge-prof-");
+ const proc = spawn(EDGE, [
+ "--headless=new", `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`,
+ "--no-first-run", "--no-default-browser-check", "--disable-gpu", "about:blank",
+ ], { stdio: "ignore" });
+
+ let targets = null;
+ for (let i = 0; i < 40; i++) {
+ await new Promise((r) => setTimeout(r, 500));
+ try { targets = await httpJson("/json/list"); break; } catch {}
+ }
+ if (!targets) { console.error("edge did not start"); process.exit(1); }
+ const page = targets.find((t) => t.type === "page");
+
+ const ws = new WebSocket(page.webSocketDebuggerUrl);
+ await new Promise((r) => (ws.onopen = r));
+ let id = 0;
+ const send = (method, params = {}) =>
+ new Promise((resolve) => {
+ const myId = ++id;
+ ws.send(JSON.stringify({ id: myId, method, params }));
+ const t = setInterval(() => {
+ const m = ws._buf.find((b) => b.id === myId);
+ if (m) { clearInterval(t); resolve(m.result ?? m.error); }
+ }, 10);
+ });
+ ws._buf = [];
+ ws.onmessage = (m) => {
+ const msg = JSON.parse(m.data);
+ if (msg.id) { ws._buf.push(msg); return; }
+ if (msg.method === "Runtime.consoleAPICalled") {
+ const text = msg.params.args.map((a) => a.value ?? a.description ?? a.type).join(" ");
+ console.log(`[${msg.params.type}] ${text}`.slice(0, 3000));
+ }
+ if (msg.method === "Runtime.exceptionThrown") {
+ console.log("[EXCEPTION]", JSON.stringify(msg.params.exceptionDetails.exception?.description ?? msg.params.exceptionDetails.text).slice(0, 3000));
+ }
+ if (msg.method === "Log.entryAdded") {
+ const e = msg.params.entry;
+ console.log(`[log:${e.level}] ${e.text}`.slice(0, 3000));
+ }
+ };
+
+ await send("Runtime.enable");
+ await send("Log.enable");
+ await send("Network.enable");
+ if (cookieFile) {
+ const line = fs.readFileSync(cookieFile, "utf8").split("\n").map((l) => l.trim()).find((l) => l.includes("authjs.session-token"));
+ if (line) await send("Network.setCookies", { cookies: [{ name: "authjs.session-token", value: line.split("\t").pop(), domain: "localhost", path: "/", httpOnly: true, sameSite: "Lax" }] });
+ }
+ await send("Page.enable");
+ await send("Page.navigate", { url });
+ await new Promise((r) => setTimeout(r, 4500));
+ const cls = await send("Runtime.evaluate", { expression: "document.documentElement.className" });
+ console.log("ROOT CLASS:", cls?.result?.value);
+ proc.kill();
+ process.exit(0);
+})().catch((e) => { console.error(e.message); process.exit(1); });
diff --git a/.cdp/cdp-shots.cjs b/.cdp/cdp-shots.cjs
new file mode 100644
index 0000000..7e6a71e
--- /dev/null
+++ b/.cdp/cdp-shots.cjs
@@ -0,0 +1,82 @@
+/* Faithful theme test: sets next-themes' localStorage.theme (the theme NAME)
+ and reloads -- exactly what the in-app theme toggle does -- then screenshots. */
+const { spawn } = require("node:child_process");
+const fs = require("node:fs");
+const http = require("node:http");
+const path = require("node:path");
+
+const EDGE = "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe";
+const PORT = 9341;
+const BASE = "http://localhost:3210";
+const OUT = path.join(__dirname, "..", "shots");
+
+function httpJson(p) {
+ return new Promise((resolve, reject) => {
+ const req = http.request({ host: "127.0.0.1", port: PORT, path: p }, (res) => {
+ let d = ""; res.on("data", (c) => (d += c));
+ res.on("end", () => { try { resolve(JSON.parse(d)); } catch { resolve(d); } });
+ });
+ req.on("error", reject); req.end();
+ });
+}
+
+(async () => {
+ const profile = fs.mkdtempSync("/tmp/edge-prof-");
+ const proc = spawn(EDGE, ["--headless=new", `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`, "--no-first-run", "--no-default-browser-check", "--disable-gpu", "about:blank"], { stdio: "ignore" });
+ let targets = null;
+ for (let i = 0; i < 40; i++) { await new Promise((r) => setTimeout(r, 500)); try { targets = await httpJson("/json/list"); break; } catch {} }
+ if (!targets) { console.error("no targets"); process.exit(1); }
+ const page = targets.find((t) => t.type === "page");
+ const ws = new WebSocket(page.webSocketDebuggerUrl);
+ await new Promise((r) => (ws.onopen = r));
+ let id = 0;
+ const send = (method, params = {}) => new Promise((resolve) => {
+ const myId = ++id; ws.send(JSON.stringify({ id: myId, method, params }));
+ const t = setInterval(() => { const m = ws._buf.find((b) => b.id === myId); if (m) { clearInterval(t); resolve(m.result ?? m.error); } }, 10);
+ });
+ ws._buf = [];
+ ws.onmessage = (m) => { const msg = JSON.parse(m.data); if (msg.id) { ws._buf.push(msg); return; }
+ if (msg.method === "Runtime.consoleAPICalled" && msg.params.type === "error") {
+ const txt = msg.params.args.map((a) => a.value ?? a.description ?? a.type).join(" ");
+ if (!/hydrated|React DevTools|HMR/.test(txt)) console.log("[err]", txt.slice(0, 140));
+ } };
+
+ await send("Runtime.enable"); await send("Page.enable"); await send("Network.enable");
+ const cookieFile = process.argv[2];
+ const cookieValue = cookieFile ? (fs.readFileSync(cookieFile, "utf8").split("\n").map((l) => l.trim()).find((l) => l.includes("authjs.session-token")) || "").split("\t").pop() : null;
+ // Cookie is applied per-job below (login pages need a logged-out state).
+
+ const jobs = [
+ { name: "01-login-default", url: `${BASE}/login`, theme: "default", cookie: false, w: 1280, h: 800 },
+ { name: "02-home-default", url: `${BASE}/`, theme: "default", cookie: true, w: 1600, h: 1000 },
+ { name: "03-home-sunset", url: `${BASE}/`, theme: "sunset", cookie: true, w: 1600, h: 1000 },
+ { name: "04-home-dark", url: `${BASE}/`, theme: "dark", cookie: true, w: 1600, h: 1000 },
+ { name: "05-home-ocean", url: `${BASE}/`, theme: "ocean", cookie: true, w: 1600, h: 1000 },
+ { name: "06-projects-dark", url: `${BASE}/projects`, theme: "dark", cookie: true, w: 1600, h: 1000 },
+ { name: "07-chat-ocean", url: `${BASE}/chat`, theme: "ocean", cookie: true, w: 1600, h: 1000 },
+ { name: "08-admin-default", url: `${BASE}/admin`, theme: "default", cookie: true, w: 1600, h: 1000 },
+ { name: "09-login-sunset", url: `${BASE}/login`, theme: "sunset", cookie: false, w: 1280, h: 800 },
+ { name: "10-home-default-mobile", url: `${BASE}/`, theme: "default", cookie: true, w: 430, h: 932 },
+ ];
+
+ for (const job of jobs) {
+ if (job.cookie && cookieValue) {
+ await send("Network.setCookies", { cookies: [{ name: "authjs.session-token", value: cookieValue, domain: "localhost", path: "/", httpOnly: true, sameSite: "Lax" }] });
+ } else {
+ await send("Network.deleteCookies", { name: "authjs.session-token", domain: "localhost" });
+ }
+ await send("Emulation.setDeviceMetricsOverride", { width: job.w, height: job.h, deviceScaleFactor: 1, mobile: job.w < 500 });
+ await send("Page.navigate", { url: job.url });
+ await new Promise((r) => setTimeout(r, 3500));
+ // Switch theme the way the app does: write the theme NAME to next-themes'
+ // storage key, then reload so it applies on a fresh (SSR+hydration) render.
+ await send("Runtime.evaluate", { expression: `localStorage.setItem('theme', ${JSON.stringify(job.theme)}); 'ok'` });
+ await send("Page.reload");
+ await new Promise((r) => setTimeout(r, 3800));
+ const htmlClass = (await send("Runtime.evaluate", { expression: `document.documentElement.className`, returnByValue: true }))?.result?.value || "";
+ const shot = await send("Page.captureScreenshot", { format: "png", captureBeyondViewport: false });
+ fs.writeFileSync(path.join(OUT, `${job.name}.png`), Buffer.from(shot.data, "base64"));
+ console.log(`saved ${job.name} (html=...${htmlClass.split(" ").filter(Boolean).slice(-1)[0]})`);
+ }
+ proc.kill(); process.exit(0);
+})().catch((e) => { console.error(e); process.exit(1); });
diff --git a/.cdp/debug-menu.cjs b/.cdp/debug-menu.cjs
new file mode 100644
index 0000000..7ba9baa
--- /dev/null
+++ b/.cdp/debug-menu.cjs
@@ -0,0 +1,53 @@
+/* Debug: step through the theme menu with full state dumps between clicks. */
+const { spawn } = require("node:child_process");
+const fs = require("node:fs");
+const http = require("node:http");
+const EDGE = "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe";
+const PORT = 9356;
+function httpJson(p){return new Promise((res,rej)=>{const r=http.request({host:"127.0.0.1",port:PORT,path:p},(x)=>{let d="";x.on("data",c=>d+=c);x.on("end",()=>{try{res(JSON.parse(d))}catch{res(d)}})});r.on("error",rej);r.end()})}
+(async()=>{
+ const prof=fs.mkdtempSync(require("node:os").tmpdir()+"/edge-prof-");
+ const proc=spawn(EDGE,["--headless=new","--remote-debugging-port="+PORT,"--user-data-dir="+prof,"--no-first-run","--no-default-browser-check","--disable-gpu","about:blank"],{stdio:"ignore"});
+ let tg=null;for(let i=0;i<40;i++){await new Promise(r=>setTimeout(r,500));try{tg=await httpJson("/json/list");break}catch{}}
+ const page=tg.find(t=>t.type==="page");
+ const ws=new WebSocket(page.webSocketDebuggerUrl);
+ await new Promise(r=>ws.onopen=r);let id=0;
+ const send=(m,p={})=>new Promise(res=>{const i2=++id;ws.send(JSON.stringify({id:i2,method:m,params:p}));const t=setInterval(()=>{const f=ws._buf.find(b=>b.id===i2);if(f){clearInterval(t);res(f.result??f.error)}},10)});
+ ws._buf=[];const errs=[];
+ ws.onmessage=(m)=>{const j=JSON.parse(m.data);if(j.id){ws._buf.push(j);return;}
+ if(j.method==="Runtime.consoleAPICalled"&&j.params.type==="error"){errs.push(j.params.args.map(a=>(a.value??a.description??"")).join(" ").slice(0,160));}
+ if(j.method==="Runtime.exceptionThrown"){errs.push("EXC: "+(j.params.exceptionDetails?.exception?.description||"").slice(0,120));}};
+ await send("Runtime.enable");await send("Page.enable");await send("Network.enable");
+ const cv=fs.readFileSync(process.argv[2],"utf8").split("\n").map(l=>l.trim()).find(l=>l.includes("authjs.session-token")).split("\t").pop();
+ await send("Network.setCookies",{cookies:[{name:"authjs.session-token",value:cv,domain:"localhost",path:"/",httpOnly:true,sameSite:"Lax"}]});
+ await send("Emulation.setDeviceMetricsOverride",{width:1440,height:900,deviceScaleFactor:1});
+
+ const ev=(expr)=>send("Runtime.evaluate",{expression:expr,returnByValue:true,awaitPromise:true}).then(r=>r.result?.value ?? r.exceptionDetails?.exception?.description);
+ const dump=async(label)=>console.log(label, JSON.stringify(await ev(`(()=>{
+ const btns=Array.from(document.querySelectorAll("button")).map(b=>(b.textContent||"").trim()).filter(t=>["Theme","Default","Sunset","Dark","Ocean","Match system"].includes(t));
+ return {html:document.documentElement.className.split(" ").filter(Boolean).slice(-1).join(","), ls:localStorage.getItem("theme"), btns, menuOpen:!!document.querySelector('[data-slot="dropdown-menu-content"]')};
+ })()`)));
+ const click=async(x,y)=>{await send("Input.dispatchMouseEvent",{type:"mousePressed",x,y,button:"left",clickCount:1});await send("Input.dispatchMouseEvent",{type:"mouseReleased",x,y,button:"left",clickCount:1});};
+ const center=async(name)=>{const v=await ev(`(()=>{const b=Array.from(document.querySelectorAll("button")).find(x=>(x.textContent||"").trim()==="${name}");if(!b)return null;const r=b.getBoundingClientRect();return JSON.stringify({x:Math.round(r.x+12),y:Math.round(r.y+r.height/2)})})()`);return v?JSON.parse(v):null;};
+
+ await send("Page.navigate",{url:"http://localhost:3210/"});
+ await new Promise(r=>setTimeout(r,9000));
+ console.log("PAGE", JSON.stringify(await ev("JSON.stringify({title:document.title,bodyLen:document.body.innerText.length,btnCount:document.querySelectorAll('button').length})")));
+
+ await dump("S0 initial ");
+ let p=await center("Theme"); if(!p){console.log("no Theme btn");proc.kill();process.exit(1);}
+ await click(p.x,p.y); await new Promise(r=>setTimeout(r,600));
+ await dump("S1 after open ");
+ p=await center("Default"); console.log(" default btn at", JSON.stringify(p));
+ await click(p.x,p.y); await new Promise(r=>setTimeout(r,900));
+ await dump("S2 after Default");
+ p=await center("Theme"); if(!p){console.log("no Theme btn #2");proc.kill();process.exit(1);}
+ await click(p.x,p.y); await new Promise(r=>setTimeout(r,600));
+ await dump("S3 after open2 ");
+ p=await center("Ocean"); console.log(" ocean btn at", JSON.stringify(p));
+ await click(p.x,p.y); await new Promise(r=>setTimeout(r,900));
+ await dump("S4 after Ocean ");
+ const real=errs.filter(e=>!/favicon|manifest|icon|404/i.test(e));
+ console.log("ERRORS:", real.length); real.slice(0,6).forEach(e=>console.log(" -",e));
+ proc.kill();process.exit(0);
+})().catch(e=>{console.error("FAIL",e.message);process.exit(1)});
diff --git a/.cdp/diag.cjs b/.cdp/diag.cjs
new file mode 100644
index 0000000..148b693
--- /dev/null
+++ b/.cdp/diag.cjs
@@ -0,0 +1,53 @@
+const { spawn } = require("node:child_process");
+const fs = require("node:fs");
+const http = require("node:http");
+const EDGE = "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe";
+const PORT = 9339;
+function httpJson(path) {
+ return new Promise((resolve, reject) => {
+ const req = http.request({ host: "127.0.0.1", port: PORT, path }, (res) => {
+ let d = ""; res.on("data", (c) => (d += c));
+ res.on("end", () => { try { resolve(JSON.parse(d)); } catch { resolve(d); } });
+ });
+ req.on("error", reject); req.end();
+ });
+}
+(async () => {
+ const profile = fs.mkdtempSync("/tmp/edge-prof-");
+ const proc = spawn(EDGE, ["--headless=new", `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`, "--no-first-run", "--no-default-browser-check", "--disable-gpu", "about:blank"], { stdio: "ignore" });
+ let targets = null;
+ for (let i = 0; i < 40; i++) { await new Promise((r) => setTimeout(r, 500)); try { targets = await httpJson("/json/list"); break; } catch {} }
+ if (!targets) { console.error("no targets"); process.exit(1); }
+ const page = targets.find((t) => t.type === "page");
+ const ws = new WebSocket(page.webSocketDebuggerUrl);
+ await new Promise((r) => (ws.onopen = r));
+ let id = 0;
+ const send = (method, params = {}) => new Promise((resolve) => {
+ const myId = ++id; ws.send(JSON.stringify({ id: myId, method, params }));
+ const t = setInterval(() => { const m = ws._buf.find((b) => b.id === myId); if (m) { clearInterval(t); resolve(m.result ?? m.error); } }, 10);
+ });
+ ws._buf = [];
+ ws.onmessage = (m) => { const msg = JSON.parse(m.data); if (msg.id) { ws._buf.push(msg); return; }
+ if (msg.method === "Runtime.consoleAPICalled") console.log("[c]", msg.params.args.map((a) => a.value ?? a.description ?? a.type).join(" ").slice(0, 120)); };
+ await send("Runtime.enable"); await send("Network.enable");
+ const cookieFile = process.argv[2];
+ if (cookieFile) { const line = fs.readFileSync(cookieFile, "utf8").split("\n").map((l) => l.trim()).find((l) => l.includes("authjs.session-token"));
+ if (line) await send("Network.setCookies", { cookies: [{ name: "authjs.session-token", value: line.split("\t").pop(), domain: "localhost", path: "/", httpOnly: true, sameSite: "Lax" }] }); }
+ await send("Page.enable");
+ await send("Page.navigate", { url: "http://localhost:3210/" });
+ await new Promise((r) => setTimeout(r, 4000));
+ const theme = process.argv[3] || "dark";
+ await send("Runtime.evaluate", { expression: `document.documentElement.className = ${JSON.stringify(theme)}` });
+ await new Promise((r) => setTimeout(r, 1600));
+ const res = await send("Runtime.evaluate", { expression: `(() => {
+ const card = document.querySelector('[aria-label^="Drag to move"]')?.closest('[class*="rounded-2xl"]');
+ return JSON.stringify({
+ htmlClassTail: document.documentElement.className.split(" ").filter(Boolean).slice(-2),
+ inlineStyle: card ? card.getAttribute("style") : null,
+ computedBg: card ? getComputedStyle(card).backgroundColor : null,
+ varCard: getComputedStyle(document.documentElement).getPropertyValue('--card').trim(),
+ });
+ })()`, returnByValue: true });
+ console.log(res?.result?.value);
+ proc.kill(); process.exit(0);
+})().catch((e) => { console.error(e.message); process.exit(1); });
diff --git a/.cdp/final-check.cjs b/.cdp/final-check.cjs
new file mode 100644
index 0000000..72849aa
--- /dev/null
+++ b/.cdp/final-check.cjs
@@ -0,0 +1,173 @@
+/* FINAL CHECK: login via UI, verify Home renders, cycle all 4 themes live,
+ * confirm card colors change instantly, zero console errors, screenshots. */
+const { spawn } = require("node:child_process");
+const fs = require("node:fs");
+const http = require("node:http");
+const EDGE = "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe";
+const PORT = 9380;
+function httpJson(p) {
+ return new Promise((res, rej) => {
+ const r = http.request({ host: "127.0.0.1", port: PORT, path: p }, (x) => {
+ let d = ""; x.on("data", (c) => { d += c; });
+ x.on("end", () => { try { res(JSON.parse(d)); } catch (e) { res(d); } });
+ });
+ r.on("error", rej); r.end();
+ });
+}
+(async () => {
+ const prof = fs.mkdtempSync(require("node:os").tmpdir() + "/edge-prof-");
+ const proc = spawn(EDGE, ["--headless=new", `--remote-debugging-port=${PORT}`, `--user-data-dir=${prof}`, "--no-first-run", "--no-default-browser-check", "--disable-gpu", "about:blank"], { stdio: "ignore" });
+ let tg = null;
+ for (let i = 0; i < 40; i++) { await new Promise(r => setTimeout(r, 500)); try { tg = await httpJson("/json/list"); break; } catch (e) {} }
+ const page = tg.find(t => t.type === "page");
+ const ws = new WebSocket(page.webSocketDebuggerUrl);
+ await new Promise(r => (ws.onopen = r));
+ let id = 0;
+ const send = (m, p = {}) => new Promise(res => {
+ const i2 = ++id; ws.send(JSON.stringify({ id: i2, method: m, params: p }));
+ const t = setInterval(() => { const f = ws._buf.find(b => b.id === i2); if (f) { clearInterval(t); res(f.result ?? f.error); } }, 10);
+ });
+ ws._buf = [];
+ const errors = [];
+ ws.onmessage = (m) => {
+ const j = JSON.parse(m.data);
+ if (j.id) { ws._buf.push(j); return; }
+ if (j.method === "Runtime.consoleAPICalled" && j.params.type === "error") {
+ errors.push(j.params.args.map(a => (a.value ?? a.description ?? "")).join(" ").slice(0, 140));
+ }
+ if (j.method === "Runtime.exceptionThrown") {
+ errors.push("EXC: " + (j.params.exceptionDetails?.exception?.description || j.params.exceptionDetails?.text || "").slice(0, 140));
+ }
+ };
+ await send("Runtime.enable"); await send("Page.enable"); await send("Network.enable");
+ await send("Emulation.setDeviceMetricsOverride", { width: 1440, height: 900, deviceScaleFactor: 1 });
+
+ const ev = (e) => send("Runtime.evaluate", { expression: e, returnByValue: true }).then(r => (r.result ? r.result.value : "ERR"));
+ const click = async (x, y) => {
+ await send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
+ await send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
+ };
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+ const shot = async (name) => { const s = await send("Page.captureScreenshot", { format: "png" }); fs.writeFileSync("shots/" + name, Buffer.from(s.data, "base64")); };
+
+ // --- 1. login -----------------------------------------------------------
+ await send("Page.navigate", { url: "http://localhost:3210/" });
+ await sleep(4000);
+ const fill = (sel, v) => ev(`(() => { const el = document.querySelector(${JSON.stringify(sel)}); if (!el) return false; const set = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set; set.call(el, ${JSON.stringify(v)}); el.dispatchEvent(new Event("input", { bubbles: true })); return true; })()`);
+ console.log("1. login form found:", await ev("!!document.querySelector('input[type=email]') && !!document.querySelector('input[type=password]')"));
+ await fill("input[type=email]", "dev@example.com");
+ await fill("input[type=password]", "password123");
+ const sub = JSON.parse(await ev(`(() => { const b = document.querySelector("form button[type=submit]") || document.querySelector("form button"); const r = b.getBoundingClientRect(); return JSON.stringify({ x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }); })()`));
+ await click(sub.x, sub.y);
+ let home = false;
+ for (let i = 0; i < 20; i++) {
+ await sleep(1000);
+ const url = await ev("location.href");
+ const btns = await ev("document.querySelectorAll('button').length");
+ if (url.endsWith("3210/") && btns > 8) { home = true; break; }
+ }
+ console.log("2. logged in & on Home:", home, "| URL:", await ev("location.href"), "| buttons:", await ev("document.querySelectorAll('button').length"));
+ console.log("3. board rendered:", await ev(`(() => { const lanes = document.querySelectorAll('[data-slot="dropdown-menu-trigger"], [class*="lane"]').length; const groups = Array.from(document.querySelectorAll("h3")).length; return "headings=" + groups; })()`));
+
+ // --- 2. helpers ---------------------------------------------------------
+ const cardState = () => ev(`(() => {
+ const cs = Array.from(document.querySelectorAll('[class*=rounded-2xl]')).filter(c => /background-color/.test(c.getAttribute('style') || ''));
+ if (!cs.length) return { cards: 0 };
+ const s = cs[0].getAttribute('style');
+ return {
+ cards: cs.length,
+ fill: (s.match(/background-color:([^;]+)/) || [])[1],
+ stroke: (s.match(/border-color:([^;]+)/) || [])[1],
+ html: document.documentElement.className.split(' ').filter(Boolean).slice(-1).join(','),
+ bodyBg: getComputedStyle(document.body).backgroundColor,
+ };
+ })()`);
+
+ const selectTheme = async (name) => {
+ for (let attempt = 1; attempt <= 3; attempt++) {
+ const v = await ev(`(() => { const b = Array.from(document.querySelectorAll('button')).find(x => (x.textContent || '').trim() === 'Theme'); if (!b) return null; const r = b.getBoundingClientRect(); return JSON.stringify({ x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }); })()`);
+ if (!v) { await sleep(400); continue; }
+ const tp = JSON.parse(v); await click(tp.x, tp.y);
+ let labels = null;
+ for (let i = 0; i < 20; i++) {
+ await sleep(150);
+ labels = await ev(`(() => { const m = document.querySelector('[data-slot="dropdown-menu-content"]'); if (!m || m.querySelectorAll('button').length < 4) return null; return JSON.stringify(Array.from(m.querySelectorAll('button')).map(b => (b.textContent || '').trim())); })()`);
+ if (labels) break;
+ }
+ if (!labels) { await sleep(500); continue; }
+ const idx = JSON.parse(labels).indexOf(name);
+ if (idx === -1) return "option missing";
+ const p = JSON.parse(await ev(`(() => { const m = document.querySelector('[data-slot="dropdown-menu-content"]'); const b = m.querySelectorAll('button')[${idx}]; const r = b.getBoundingClientRect(); return JSON.stringify({ x: Math.round(r.x + 12), y: Math.round(r.y + r.height / 2) }); })()`));
+ await click(p.x, p.y); await sleep(900);
+ return "ok";
+ }
+ return "failed after retries";
+ };
+
+ // --- 3. cycle themes: card fill must match light/dark variant live ---
+ const seq = ["Sunset", "Default", "Dark", "Ocean", "Sunset"];
+ const expectedFill = (theme) =>
+ theme === "Dark" || theme === "Ocean" ? "#3a2320" /* dark tint (softDark) */ : "#fae4e1"; /* light tint (soft) */;
+ let prev = await cardState();
+ let allOk = true;
+ for (const name of seq) {
+ const r = await selectTheme(name);
+ await sleep(300);
+ const now = await cardState();
+ const correct = r === "ok" && now.fill && now.fill.includes(expectedFill(name)) && prev.fill !== undefined;
+ // a live switch = the html class changed and the fill is the right variant for the new theme
+ if (!correct) allOk = false;
+ console.log(`4. ${name.padEnd(7)}:`, r, "| fill correct for theme:", now.fill.includes(expectedFill(name)), "| fill:", now.fill && now.fill.trim().slice(0, 40), "| html:", now.html);
+ prev = now;
+ }
+ console.log("5. ALL theme switches correct & live:", allOk);
+
+ // --- 4. other pages smoke test ------------------------------------------
+ const pages = ["/projects", "/chat", "/admin"];
+ let adminBadgeOk = null;
+ for (const p of pages) {
+ const before = errors.length;
+ await ev(`location.href = ${JSON.stringify("http://localhost:3210" + p)}; true`);
+ await sleep(3500);
+ const ok = await ev(`document.body.innerText.length > 50 && document.querySelectorAll('button').length > 3`);
+ const newErr = errors.length - before;
+ console.log(`7. page ${p}: renders=${!!ok}, new console errors=${newErr}`);
+ if (p === "/admin") {
+ // role badge must be visible & its menu must open on a trusted click
+ await ev(`(() => { const b = Array.from(document.querySelectorAll('button')).find(x => /Change role/.test(x.getAttribute('aria-label') || '')); if (b) b.scrollIntoView({ block: 'center' }); return true; })()`);
+ await sleep(400);
+ const badgePos = await ev(`(() => { const b = Array.from(document.querySelectorAll('button')).find(x => /Change role/.test(x.getAttribute('aria-label') || '')); if (!b) return null; const r = b.getBoundingClientRect(); return JSON.stringify({ x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) }); })()`);
+ if (badgePos) {
+ const bp = JSON.parse(badgePos);
+ await click(bp.x, bp.y);
+ let opened = false;
+ for (let i = 0; i < 10; i++) {
+ await sleep(150);
+ opened = !!(await ev(`document.querySelector('[data-slot="dropdown-menu-content"]')`));
+ if (opened) break;
+ }
+ adminBadgeOk = opened;
+ console.log("7b. admin role-badge menu opens:", opened);
+ await ev(`document.body.click(); true`);
+ await sleep(300);
+ } else {
+ adminBadgeOk = false;
+ console.log("7b. admin role badge not found");
+ }
+ }
+ }
+
+ // --- 5. final state + errors --------------------------------------------
+ await ev(`location.href = "http://localhost:3210/"; true`);
+ await sleep(4000);
+ await shot("final-home.png");
+ const realErrors = errors.filter(e => !/favicon|manifest|icon|404|DevTools|HMR/i.test(e));
+ console.log("8. CONSOLE ERRORS (filtered):", realErrors.length);
+ realErrors.slice(0, 8).forEach(e => console.log(" -", e));
+ console.log("9. hydration warnings:", errors.filter(e => /hydrat/i.test(e)).length);
+ console.log("10. Base UI crashes:", errors.filter(e => /Base UI/i.test(e)).length);
+ proc.kill();
+ const pass = allOk && realErrors.length === 0 && home && adminBadgeOk === true;
+ console.log(pass ? "\nFINAL RESULT: ALL PASS ✔" : "\nFINAL RESULT: FAIL");
+ process.exit(pass ? 0 : 2);
+})().catch(e => { console.error("FAIL", e.message); process.exit(1); });
diff --git a/.cdp/live-switch.cjs b/.cdp/live-switch.cjs
new file mode 100644
index 0000000..78b464f
--- /dev/null
+++ b/.cdp/live-switch.cjs
@@ -0,0 +1,117 @@
+/* Live-switch test with waits + evidence screenshots. */
+const { spawn } = require("node:child_process");
+const fs = require("node:fs");
+const http = require("node:http");
+const EDGE = "C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe";
+const PORT = 9368;
+function httpJson(p){
+ return new Promise((res, rej) => {
+ const r = http.request({ host: "127.0.0.1", port: PORT, path: p }, (x) => {
+ let d = "";
+ x.on("data", (c) => { d += c; });
+ x.on("end", () => { try { res(JSON.parse(d)); } catch (e) { res(d); } });
+ });
+ r.on("error", rej);
+ r.end();
+ });
+}
+(async () => {
+ const prof = fs.mkdtempSync(require("node:os").tmpdir() + "/edge-prof-");
+ const proc = spawn(EDGE, ["--headless=new", `--remote-debugging-port=${PORT}`, `--user-data-dir=${prof}`, "--no-first-run", "--no-default-browser-check", "--disable-gpu", "about:blank"], { stdio: "ignore" });
+ let tg = null;
+ for (let i = 0; i < 40; i++) { await new Promise(r => setTimeout(r, 500)); try { tg = await httpJson("/json/list"); break; } catch (e) {} }
+ const page = tg.find(t => t.type === "page");
+ const ws = new WebSocket(page.webSocketDebuggerUrl);
+ await new Promise(r => (ws.onopen = r));
+ let id = 0;
+ const send = (m, p = {}) => new Promise(res => {
+ const i2 = ++id;
+ ws.send(JSON.stringify({ id: i2, method: m, params: p }));
+ const t = setInterval(() => { const f = ws._buf.find(b => b.id === i2); if (f) { clearInterval(t); res(f.result ?? f.error); } }, 10);
+ });
+ ws._buf = [];
+ const errs = [];
+ ws.onmessage = (m) => {
+ const j = JSON.parse(m.data);
+ if (j.id) { ws._buf.push(j); return; }
+ if (j.method === "Runtime.consoleAPICalled" && j.params.type === "error") errs.push(j.params.args.map(a => (a.value ?? a.description ?? "")).join(" ").slice(0, 120));
+ if (j.method === "Runtime.exceptionThrown") errs.push("EXC: " + (j.params.exceptionDetails?.exception?.description || "").slice(0, 120));
+ };
+ await send("Runtime.enable");
+ await send("Page.enable");
+ await send("Network.enable");
+ const cv = fs.readFileSync(process.argv[1], "utf8").split("\n").map(l => l.trim()).find(l => l.includes("authjs.session-token")).split("\t").pop();
+ await send("Network.setCookies", { cookies: [{ name: "authjs.session-token", value: cv, domain: "localhost", path: "/", httpOnly: true, sameSite: "Lax" }] });
+ await send("Emulation.setDeviceMetricsOverride", { width: 1440, height: 900, deviceScaleFactor: 1 });
+ await send("Page.navigate", { url: "http://localhost:3210/" });
+ let ready = false;
+ for (let i = 0; i < 45; i++) {
+ await new Promise(r => setTimeout(r, 1000));
+ ready = await send("Runtime.evaluate", { expression: "document.querySelectorAll('button').length > 5", returnByValue: true }).then(r => r.result?.value);
+ if (ready) break;
+ }
+ console.log("page ready:", ready);
+
+ const ev = (expr) => send("Runtime.evaluate", { expression: expr, returnByValue: true }).then(r => r.result?.value);
+ const click = async (x, y) => {
+ await send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button: "left", clickCount: 1 });
+ await send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button: "left", clickCount: 1 });
+ };
+ const sleep = (ms) => new Promise(r => setTimeout(r, ms));
+ const cardStyle = async () => ev(`(() => {
+ const c = Array.from(document.querySelectorAll('[class*=rounded-2xl]')).find(x => /background-color/.test(x.getAttribute('style') || ''));
+ return c ? c.getAttribute('style').slice(0, 150) : 'no-card';
+ })()`);
+ const shot = async (name) => {
+ const s = await send("Page.captureScreenshot", { format: "png" });
+ fs.writeFileSync("shots/" + name, Buffer.from(s.data, "base64"));
+ };
+
+ const selectTheme = async (name) => {
+ const v = await ev(`(() => {
+ const b = Array.from(document.querySelectorAll('button')).find(x => (x.textContent || '').trim() === 'Theme');
+ if (!b) return null;
+ const r = b.getBoundingClientRect();
+ return JSON.stringify({ x: Math.round(r.x + r.width / 2), y: Math.round(r.y + r.height / 2) });
+ })()`);
+ if (!v) return "no theme button";
+ const tp = JSON.parse(v);
+ await click(tp.x, tp.y);
+ let labels = null;
+ for (let i = 0; i < 16; i++) {
+ await sleep(150);
+ labels = await ev(`(() => {
+ const m = document.querySelector('[data-slot="dropdown-menu-content"]');
+ if (!m || m.querySelectorAll('button').length < 4) return null;
+ return JSON.stringify(Array.from(m.querySelectorAll('button')).map(b => (b.textContent || '').trim()));
+ })()`);
+ if (labels) break;
+ }
+ if (!labels) return "menu never populated";
+ const idx = JSON.parse(labels).indexOf(name);
+ if (idx === -1) return "option missing: " + labels;
+ const p = JSON.parse(await ev(`(() => {
+ const m = document.querySelector('[data-slot="dropdown-menu-content"]');
+ const b = m.querySelectorAll('button')[${idx}];
+ const r = b.getBoundingClientRect();
+ return JSON.stringify({ x: Math.round(r.x + 12), y: Math.round(r.y + r.height / 2) });
+ })()`));
+ await click(p.x, p.y);
+ await sleep(900);
+ return "ok";
+ };
+
+ const A = await cardStyle(); console.log("A dark :", A); await shot("12-live-dark.png");
+ console.log("-> Sunset :", await selectTheme("Sunset"));
+ const B = await cardStyle(); console.log("B sunset :", B); await shot("13-live-sunset.png");
+ console.log("-> Ocean :", await selectTheme("Ocean"));
+ const C = await cardStyle(); console.log("C ocean :", C); await shot("14-live-ocean.png");
+ console.log("-> Default:", await selectTheme("Default"));
+ const D = await cardStyle(); console.log("D default :", D); await shot("15-live-default.png");
+ console.log("CHANGED A!=B:", A !== B, " B!=C:", B !== C, " C!=D:", C !== D);
+ const real = errs.filter(e => !/favicon|manifest|icon|404/i.test(e));
+ console.log("CONSOLE ERRORS:", real.length);
+ real.slice(0, 4).forEach(e => console.log(" -", e));
+ proc.kill();
+ process.exit(0);
+})().catch(e => { console.error("FAIL", e.message); process.exit(1); });
diff --git a/.cdp/nt-beta/next-themes-1.0.0-beta.0.tgz b/.cdp/nt-beta/next-themes-1.0.0-beta.0.tgz
new file mode 100644
index 0000000..2613bc9
Binary files /dev/null and b/.cdp/nt-beta/next-themes-1.0.0-beta.0.tgz differ
diff --git a/.cdp/nt-beta/package/README.md b/.cdp/nt-beta/package/README.md
new file mode 100644
index 0000000..c1da8b4
--- /dev/null
+++ b/.cdp/nt-beta/package/README.md
@@ -0,0 +1,502 @@
+# next-themes  
+
+An abstraction for themes in your Next.js app.
+
+- ✅ Perfect dark mode in 2 lines of code
+- ✅ System setting with prefers-color-scheme
+- ✅ Themed browser UI with color-scheme
+- ✅ No flash on load (both SSR and SSG)
+- ✅ Sync theme across tabs and windows
+- ✅ Disable flashing when changing themes
+- ✅ Force pages to specific themes
+- ✅ Class or data attribute selector
+- ✅ `useTheme` hook
+
+Check out the [Live Example](https://next-themes-example.vercel.app/) to try it for yourself.
+
+## Install
+
+```bash
+$ npm install next-themes
+# or
+$ yarn add next-themes
+```
+
+## Use
+
+You'll need a [Custom `App`](https://nextjs.org/docs/advanced-features/custom-app) to use next-themes. The simplest `_app` looks like this:
+
+```js
+// pages/_app.js
+
+function MyApp({ Component, pageProps }) {
+ return