From 5c21b64a99c679e1b7188523187e2336d6ad5c90 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Mon, 2 Mar 2026 12:45:11 -0800 Subject: [PATCH] feat: add My Account submenu with dark mode + encrypted backup toggles Reorganize user dropdown into expandable "My Account" submenu containing account actions (Add Email, Add Device, Recovery) plus Dark Mode and Encrypted Backup toggle switches. Move theme toggle from toolbar into account settings, default to dark mode. Co-Authored-By: Claude Opus 4.6 --- shared/components/rstack-identity.ts | 101 +++++++++++++++++++++++++-- website/canvas.html | 14 ++-- 2 files changed, 102 insertions(+), 13 deletions(-) diff --git a/shared/components/rstack-identity.ts b/shared/components/rstack-identity.ts index fe3d06c..b2f4c0b 100644 --- a/shared/components/rstack-identity.ts +++ b/shared/components/rstack-identity.ts @@ -412,12 +412,31 @@ export class RStackIdentity extends HTMLElement { ${notifsHTML} + + - - - - @@ -468,6 +487,13 @@ export class RStackIdentity extends HTMLElement { el.addEventListener("click", (e) => { e.stopPropagation(); const action = (el as HTMLElement).dataset.action; + if (action === "toggle-account") { + const submenu = this.#shadow.getElementById("account-submenu")!; + const arrow = (el as HTMLElement).querySelector(".submenu-arrow")!; + submenu.classList.toggle("open"); + arrow.textContent = submenu.classList.contains("open") ? "▾" : "▸"; + return; + } dropdown.classList.remove("open"); if (action === "signout") { clearSession(); @@ -486,6 +512,34 @@ export class RStackIdentity extends HTMLElement { } }); }); + + // Theme toggle + const themeToggle = this.#shadow.getElementById("theme-toggle") as HTMLInputElement; + if (themeToggle) { + const currentTheme = localStorage.getItem("canvas-theme") || "dark"; + themeToggle.checked = currentTheme === "dark"; + themeToggle.addEventListener("change", (e) => { + e.stopPropagation(); + const newTheme = themeToggle.checked ? "dark" : "light"; + localStorage.setItem("canvas-theme", newTheme); + document.body.setAttribute("data-theme", newTheme); + document.querySelectorAll(".rstack-header, .rstack-tab-row").forEach(el => el.setAttribute("data-theme", newTheme)); + this.dispatchEvent(new CustomEvent("theme-change", { bubbles: true, composed: true, detail: { theme: newTheme } })); + this.#render(); + }); + } + + // Backup toggle + const backupToggle = this.#shadow.getElementById("backup-toggle") as HTMLInputElement; + if (backupToggle) { + backupToggle.checked = localStorage.getItem("encryptid_backup_enabled") === "true"; + backupToggle.addEventListener("change", (e) => { + e.stopPropagation(); + const enabled = backupToggle.checked; + localStorage.setItem("encryptid_backup_enabled", enabled ? "true" : "false"); + this.dispatchEvent(new CustomEvent("backup-toggle", { bubbles: true, composed: true, detail: { enabled } })); + }); + } } else { this.#shadow.innerHTML = ` @@ -1268,6 +1322,45 @@ const STYLES = ` .notif-btn--approve:hover:not(:disabled) { opacity: 0.85; } .notif-btn--deny { background: rgba(239,68,68,0.15); color: #ef4444; } .notif-btn--deny:hover:not(:disabled) { background: rgba(239,68,68,0.25); } + +/* Submenu accordion */ +.submenu { + max-height: 0; overflow: hidden; + transition: max-height 0.2s ease-out; +} +.submenu.open { max-height: 300px; } +.submenu-item { padding-left: 32px !important; font-size: 0.825rem; } +.submenu-toggle { position: relative; } +.submenu-arrow { + position: absolute; right: 16px; + font-size: 0.7rem; transition: transform 0.2s; +} + +/* Toggle switch */ +.toggle-row { + display: flex; align-items: center; + justify-content: space-between; cursor: default; +} +.toggle-switch { + position: relative; width: 36px; height: 20px; + display: inline-block; flex-shrink: 0; +} +.toggle-switch input { opacity: 0; width: 0; height: 0; } +.toggle-slider { + position: absolute; inset: 0; border-radius: 10px; + background: rgba(255,255,255,0.15); cursor: pointer; + transition: background 0.2s; +} +.toggle-slider::before { + content: ""; position: absolute; + width: 16px; height: 16px; border-radius: 50%; + left: 2px; bottom: 2px; background: white; + transition: transform 0.2s; +} +.toggle-switch input:checked + .toggle-slider { background: #059669; } +.toggle-switch input:checked + .toggle-slider::before { transform: translateX(16px); } +.user.light .toggle-slider { background: rgba(0,0,0,0.15); } +.user.light .toggle-switch input:checked + .toggle-slider { background: #059669; } `; const MODAL_STYLES = ` diff --git a/website/canvas.html b/website/canvas.html index d49322a..620129d 100644 --- a/website/canvas.html +++ b/website/canvas.html @@ -1140,7 +1140,6 @@ - @@ -1257,24 +1256,21 @@ if (tb) tb.setModules(moduleList); }).catch(() => {}); - // ── Dark mode toggle ── + // ── Dark mode (default dark, toggled from My Account dropdown) ── { - const savedTheme = localStorage.getItem("canvas-theme") || "light"; - const themeBtn = document.getElementById("toggle-theme"); + const savedTheme = localStorage.getItem("canvas-theme") || "dark"; function applyTheme(theme) { document.body.setAttribute("data-theme", theme); document.querySelector(".rstack-header")?.setAttribute("data-theme", theme); document.querySelector(".rstack-tab-row")?.setAttribute("data-theme", theme); - if (themeBtn) themeBtn.textContent = theme === "dark" ? "☀️ Light" : "🌙 Dark"; } applyTheme(savedTheme); - themeBtn?.addEventListener("click", () => { - const next = document.body.getAttribute("data-theme") === "dark" ? "light" : "dark"; - applyTheme(next); - localStorage.setItem("canvas-theme", next); + // Listen for theme changes from rstack-identity component + document.addEventListener("theme-change", (e) => { + applyTheme(e.detail?.theme || "dark"); }); }