SaaS — Settings Page
A thorough SaaS account settings page with a vertical settings nav covering Profile, Workspace, Notifications, Appearance, and Security. Grouped form fields, accessible toggles, a per-channel notification matrix, a working theme and accent picker, and a guarded danger zone. Edits track a real dirty state with a save and discard action bar, toggles and theme choices persist to local storage, and an unsaved-changes guard protects you before leaving.
MCP
Code
:root {
--accent: 99 102 241;
--bg: #f7f8fb;
--surface: #ffffff;
--surface-2: #fbfbfd;
--ink: #0f1222;
--muted: #646b85;
--brand: rgb(var(--accent));
--brand-d: rgb(var(--accent) / .85);
--brand-soft: rgb(var(--accent) / .1);
--ok: #16a34a;
--warn: #d97706;
--danger: #dc2626;
--danger-soft: rgba(220, 38, 38, .08);
--line: rgba(15, 18, 34, .1);
--line-2: rgba(15, 18, 34, .06);
--shadow: 0 1px 2px rgba(15, 18, 34, .06), 0 8px 24px rgba(15, 18, 34, .05);
--radius: 14px;
--t: 160ms cubic-bezier(.2, .7, .3, 1);
}
[data-theme="dark"] {
--bg: #0c0e16;
--surface: #14172180;
--surface: #151823;
--surface-2: #1a1e2b;
--ink: #eef0f6;
--muted: #9aa1bb;
--line: rgba(255, 255, 255, .1);
--line-2: rgba(255, 255, 255, .06);
--brand-soft: rgb(var(--accent) / .18);
--danger-soft: rgba(220, 38, 38, .16);
--shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 8px 24px rgba(0, 0, 0, .35);
}
[data-reduce-motion="1"] * {
animation-duration: .001ms !important;
transition-duration: .001ms !important;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
background: var(--bg);
color: var(--ink);
line-height: 1.5;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
transition: background var(--t), color var(--t);
}
h1, h2, p { margin: 0; }
button { font: inherit; }
.skip-link {
position: absolute;
left: -999px;
top: 8px;
background: var(--brand);
color: #fff;
padding: 8px 14px;
border-radius: 8px;
z-index: 50;
}
.skip-link:focus { left: 8px; }
:focus-visible {
outline: 2px solid var(--brand);
outline-offset: 2px;
border-radius: 6px;
}
/* Topbar */
.topbar {
position: sticky;
top: 0;
z-index: 20;
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 24px;
background: color-mix(in srgb, var(--surface) 88%, transparent);
backdrop-filter: blur(10px);
border-bottom: 1px solid var(--line);
}
.brand { display: flex; align-items: center; gap: 10px; }
.brand-mark {
display: grid;
place-items: center;
width: 34px;
height: 34px;
border-radius: 10px;
color: #fff;
background: linear-gradient(135deg, var(--brand), var(--brand-d));
box-shadow: 0 4px 12px rgb(var(--accent) / .35);
}
.brand-name { font-weight: 700; letter-spacing: -.01em; }
.brand-tag {
font-size: 12px;
color: var(--muted);
background: var(--line-2);
padding: 2px 8px;
border-radius: 999px;
}
.topbar-right { display: flex; align-items: center; gap: 12px; }
.dirty-pill {
font-size: 12px;
font-weight: 600;
color: var(--warn);
background: rgba(217, 119, 6, .12);
padding: 4px 10px;
border-radius: 999px;
}
.avatar, .big-avatar {
display: grid;
place-items: center;
border-radius: 50%;
color: #fff;
font-weight: 600;
background: linear-gradient(135deg, #6366f1, #8b5cf6);
flex: none;
}
.avatar { width: 34px; height: 34px; font-size: 13px; }
.big-avatar { width: 64px; height: 64px; font-size: 22px; }
/* Shell */
.shell {
max-width: 1080px;
margin: 0 auto;
padding: 24px;
display: grid;
grid-template-columns: 232px 1fr;
gap: 28px;
align-items: start;
}
/* Nav */
.settings-nav {
position: sticky;
top: 78px;
}
.nav-eyebrow {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--muted);
padding: 0 12px 8px;
}
.settings-nav ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 2px; }
.nav-item {
width: 100%;
display: flex;
align-items: center;
gap: 11px;
padding: 10px 12px;
border: none;
background: transparent;
color: var(--muted);
border-radius: 10px;
cursor: pointer;
font-weight: 500;
text-align: left;
transition: background var(--t), color var(--t);
}
.nav-item svg { flex: none; opacity: .9; }
.nav-item:hover { background: var(--line-2); color: var(--ink); }
.nav-item.is-active {
background: var(--brand-soft);
color: var(--brand);
font-weight: 600;
}
/* Panel + sections */
.panel { min-width: 0; padding-bottom: 96px; }
.section { display: none; animation: fade var(--t); }
.section.is-active { display: block; }
@keyframes fade { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }
.section-head { margin-bottom: 18px; }
.section-head h1 { font-size: 22px; letter-spacing: -.02em; }
.section-sub { color: var(--muted); font-size: 14px; margin-top: 4px; }
/* Cards */
.card {
background: var(--surface);
border: 1px solid var(--line);
border-radius: var(--radius);
padding: 22px;
margin-bottom: 16px;
box-shadow: var(--shadow);
}
.card-title { font-size: 15px; font-weight: 600; margin-bottom: 14px; }
.card-title.danger { color: var(--danger); }
.grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 16px 18px; }
/* Fields */
.field { display: flex; flex-direction: column; gap: 6px; }
.field.span-2 { grid-column: 1 / -1; }
.label { font-size: 13px; font-weight: 600; }
.hint { font-size: 12px; color: var(--muted); }
input[type="text"], input[type="email"], input[type="password"], textarea, select {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--line);
background: var(--surface-2);
color: var(--ink);
border-radius: 10px;
font: inherit;
transition: border-color var(--t), box-shadow var(--t);
}
textarea { resize: vertical; }
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--brand);
box-shadow: 0 0 0 3px var(--brand-soft);
}
select {
appearance: none;
background-image: linear-gradient(45deg, transparent 50%, var(--muted) 50%), linear-gradient(135deg, var(--muted) 50%, transparent 50%);
background-position: calc(100% - 18px) center, calc(100% - 13px) center;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 34px;
}
.input-affix {
display: flex;
align-items: stretch;
border: 1px solid var(--line);
border-radius: 10px;
background: var(--surface-2);
overflow: hidden;
}
.input-affix:focus-within { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }
.input-affix .affix {
display: grid;
place-items: center;
padding: 0 11px;
font-size: 13px;
color: var(--muted);
background: var(--line-2);
border-right: 1px solid var(--line);
}
.input-affix input { border: none; background: transparent; border-radius: 0; }
.input-affix input:focus { box-shadow: none; }
.field-row.identity { display: flex; align-items: center; gap: 18px; }
.identity-name { font-weight: 600; font-size: 16px; }
.identity-mail { color: var(--muted); font-size: 13px; margin: 2px 0 10px; }
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 9px 16px;
border-radius: 10px;
border: 1px solid transparent;
font-weight: 600;
font-size: 14px;
cursor: pointer;
transition: background var(--t), border-color var(--t), transform var(--t), color var(--t);
}
.btn:active { transform: translateY(1px); }
.btn.sm { padding: 6px 12px; font-size: 13px; }
.btn.primary { background: var(--brand); color: #fff; }
.btn.primary:hover { background: var(--brand-d); }
.btn.ghost { background: transparent; border-color: var(--line); color: var(--ink); }
.btn.ghost:hover { background: var(--line-2); }
.btn.danger { background: var(--danger); color: #fff; }
.btn.danger:hover { background: #b91c1c; }
.btn.danger:disabled { opacity: .5; cursor: not-allowed; }
/* Toggles */
.toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 14px 0;
border-top: 1px solid var(--line-2);
}
.toggle-row:first-of-type { padding-top: 0; border-top: none; }
.toggle-text { display: flex; flex-direction: column; gap: 2px; }
.toggle-label { font-weight: 600; font-size: 14px; }
.toggle-desc { font-size: 13px; color: var(--muted); }
.switch {
position: relative;
width: 42px;
height: 24px;
border-radius: 999px;
border: none;
background: var(--line);
cursor: pointer;
flex: none;
transition: background var(--t);
}
.switch.sm { width: 38px; height: 22px; }
.switch .knob {
position: absolute;
top: 3px;
left: 3px;
width: 18px;
height: 18px;
border-radius: 50%;
background: #fff;
box-shadow: 0 1px 3px rgba(0, 0, 0, .3);
transition: transform var(--t);
}
.switch.sm .knob { width: 16px; height: 16px; }
.switch[aria-checked="true"] { background: var(--brand); }
.switch[aria-checked="true"] .knob { transform: translateX(18px); }
.switch.sm[aria-checked="true"] .knob { transform: translateX(16px); }
/* Notifications grid */
.notif-grid {
display: grid;
grid-template-columns: 1fr 60px 60px;
align-items: center;
gap: 12px;
padding: 14px 0;
border-top: 1px solid var(--line-2);
}
.notif-grid.notif-head {
padding-top: 0;
padding-bottom: 10px;
border-top: none;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .06em;
color: var(--muted);
}
.notif-head span:not(:first-child) { text-align: center; }
.notif-grid .switch { justify-self: center; }
.notif-text { display: flex; flex-direction: column; gap: 2px; }
/* Theme picker */
.theme-picker { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; }
.theme-card {
display: flex;
flex-direction: column;
gap: 10px;
padding: 10px;
border: 1.5px solid var(--line);
border-radius: 12px;
background: var(--surface-2);
cursor: pointer;
transition: border-color var(--t), transform var(--t);
}
.theme-card:hover { transform: translateY(-2px); }
.theme-card.is-active { border-color: var(--brand); box-shadow: 0 0 0 3px var(--brand-soft); }
.theme-name { font-size: 13px; font-weight: 600; }
.theme-preview {
height: 56px;
border-radius: 8px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 5px;
border: 1px solid var(--line-2);
}
.theme-preview i { display: block; height: 8px; border-radius: 3px; }
.theme-preview.light { background: #f7f8fb; }
.theme-preview.light i { background: #d6d9e6; }
.theme-preview.light i:first-child { background: var(--brand); width: 60%; }
.theme-preview.dark { background: #14172a; }
.theme-preview.dark i { background: #3a3f5c; }
.theme-preview.dark i:first-child { background: var(--brand); width: 60%; }
.theme-preview.system { background: linear-gradient(110deg, #f7f8fb 50%, #14172a 50%); }
.theme-preview.system i { background: linear-gradient(110deg, #d6d9e6 50%, #3a3f5c 50%); }
.theme-preview.system i:first-child { background: var(--brand); width: 60%; }
/* Accent picker */
.accent-picker { display: flex; gap: 12px; flex-wrap: wrap; }
.accent-swatch {
width: 34px;
height: 34px;
border-radius: 50%;
border: 2px solid transparent;
background: rgb(var(--sw));
cursor: pointer;
position: relative;
transition: transform var(--t);
}
.accent-swatch:hover { transform: scale(1.08); }
.accent-swatch.is-active { box-shadow: 0 0 0 2px var(--surface), 0 0 0 4px rgb(var(--sw)); }
.accent-swatch.is-active::after {
content: "";
position: absolute;
inset: 0;
margin: auto;
width: 12px;
height: 7px;
border-left: 2.5px solid #fff;
border-bottom: 2.5px solid #fff;
transform: translateY(-1px) rotate(-45deg);
}
/* Sessions */
.sessions { list-style: none; margin: 0; padding: 0; }
.sessions li {
display: flex;
align-items: center;
gap: 12px;
padding: 13px 0;
border-top: 1px solid var(--line-2);
}
.sessions li:first-child { padding-top: 0; border-top: none; }
.sess-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--muted); flex: none; }
.sess-dot.ok { background: var(--ok); box-shadow: 0 0 0 3px rgba(22, 163, 74, .18); }
.sessions li > div { flex: 1; min-width: 0; }
.sess-name { font-weight: 600; font-size: 14px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.sess-meta { font-size: 13px; color: var(--muted); }
.chip {
font-size: 11px;
font-weight: 600;
color: var(--brand);
background: var(--brand-soft);
padding: 1px 8px;
border-radius: 999px;
}
/* Danger zone */
.danger-zone { border-color: rgba(220, 38, 38, .3); background: var(--danger-soft); }
.danger-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
/* Action bar */
.action-bar {
position: fixed;
left: 50%;
bottom: 22px;
transform: translateX(-50%);
width: min(620px, calc(100% - 32px));
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
padding: 12px 14px 12px 20px;
background: var(--surface);
border: 1px solid var(--line);
border-radius: 14px;
box-shadow: 0 12px 40px rgba(15, 18, 34, .18);
z-index: 30;
animation: rise var(--t);
}
@keyframes rise { from { opacity: 0; transform: translate(-50%, 12px); } to { opacity: 1; transform: translate(-50%, 0); } }
.action-msg { font-size: 14px; font-weight: 500; color: var(--muted); }
.action-btns { display: flex; gap: 10px; }
/* Modal */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(15, 18, 34, .5);
display: grid;
place-items: center;
padding: 20px;
z-index: 60;
animation: fade var(--t);
}
.modal {
background: var(--surface);
border: 1px solid var(--line);
border-radius: 16px;
padding: 24px;
width: min(420px, 100%);
box-shadow: 0 24px 60px rgba(0, 0, 0, .35);
}
.modal h2 { font-size: 18px; margin-bottom: 8px; }
.modal p { font-size: 14px; color: var(--muted); margin-bottom: 14px; }
.modal code { background: var(--line-2); padding: 1px 6px; border-radius: 5px; font-weight: 600; color: var(--ink); }
.modal input { margin-bottom: 18px; }
.modal-btns { display: flex; justify-content: flex-end; gap: 10px; }
/* Toast */
.toast-wrap {
position: fixed;
bottom: 22px;
right: 22px;
display: flex;
flex-direction: column;
gap: 10px;
z-index: 80;
}
.toast {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 16px;
background: var(--ink);
color: var(--bg);
border-radius: 11px;
font-size: 14px;
font-weight: 500;
box-shadow: 0 10px 30px rgba(0, 0, 0, .25);
animation: toastIn var(--t);
}
.toast.is-out { animation: toastOut var(--t) forwards; }
.toast .tick { color: #4ade80; font-weight: 700; }
@keyframes toastIn { from { opacity: 0; transform: translateX(20px); } to { opacity: 1; transform: none; } }
@keyframes toastOut { to { opacity: 0; transform: translateX(20px); } }
/* Responsive */
@media (max-width: 820px) {
.shell { grid-template-columns: 1fr; gap: 16px; }
.settings-nav { position: static; }
.settings-nav ul {
grid-auto-flow: column;
grid-auto-columns: max-content;
overflow-x: auto;
padding-bottom: 4px;
scrollbar-width: none;
}
.settings-nav ul::-webkit-scrollbar { display: none; }
.nav-item span { white-space: nowrap; }
}
@media (max-width: 560px) {
.shell { padding: 16px; }
.card { padding: 18px; }
.grid-2 { grid-template-columns: 1fr; }
.theme-picker { grid-template-columns: 1fr; }
.field-row.identity { flex-direction: column; align-items: flex-start; text-align: left; }
.danger-row, .action-bar { flex-direction: column; align-items: stretch; }
.action-btns { justify-content: stretch; }
.action-btns .btn { flex: 1; justify-content: center; }
.topbar { padding: 12px 16px; }
}(function () {
"use strict";
var root = document.documentElement;
var STORE = "atlas.settings";
/* ---------- Toast helper ---------- */
var toastWrap = document.getElementById("toastWrap");
function toast(msg, ok) {
var t = document.createElement("div");
t.className = "toast";
t.innerHTML = (ok === false ? "" : '<span class="tick">✓</span>') +
"<span>" + msg + "</span>";
toastWrap.appendChild(t);
setTimeout(function () {
t.classList.add("is-out");
t.addEventListener("animationend", function () { t.remove(); });
}, 2600);
}
/* ---------- Persisted prefs ---------- */
function load() {
try { return JSON.parse(localStorage.getItem(STORE)) || {}; }
catch (e) { return {}; }
}
function save(p) {
try { localStorage.setItem(STORE, JSON.stringify(p)); } catch (e) {}
}
var prefs = load();
/* ---------- Theme ---------- */
var mq = window.matchMedia("(prefers-color-scheme: dark)");
function applyTheme(mode) {
var dark = mode === "dark" || (mode === "system" && mq.matches);
root.setAttribute("data-theme", dark ? "dark" : "light");
}
function applyAccent(rgb) { root.style.setProperty("--accent", rgb); }
var themeMode = prefs.theme || "light";
applyTheme(themeMode);
if (prefs.accent) applyAccent(prefs.accent);
if (prefs.reduceMotion) root.setAttribute("data-reduce-motion", "1");
mq.addEventListener("change", function () {
if (themeMode === "system") applyTheme("system");
});
/* ---------- Section switching (tabs) ---------- */
var tabs = Array.prototype.slice.call(document.querySelectorAll(".nav-item"));
function activate(section) {
tabs.forEach(function (tab) {
var on = tab.dataset.section === section;
tab.classList.toggle("is-active", on);
tab.setAttribute("aria-selected", on ? "true" : "false");
tab.tabIndex = on ? 0 : -1;
});
document.querySelectorAll(".section").forEach(function (s) {
var on = s.id === "sec-" + section;
s.classList.toggle("is-active", on);
s.hidden = !on;
});
}
tabs.forEach(function (tab, i) {
tab.addEventListener("click", function () { activate(tab.dataset.section); });
tab.addEventListener("keydown", function (e) {
var dir = e.key === "ArrowDown" || e.key === "ArrowRight" ? 1
: e.key === "ArrowUp" || e.key === "ArrowLeft" ? -1 : 0;
if (!dir) return;
e.preventDefault();
var next = tabs[(i + dir + tabs.length) % tabs.length];
next.focus();
activate(next.dataset.section);
});
});
/* ---------- Dirty-state tracking ---------- */
var dirty = false;
var actionBar = document.getElementById("actionBar");
var dirtyPill = document.getElementById("dirtyPill");
function setDirty(v) {
if (dirty === v) return;
dirty = v;
actionBar.hidden = !v;
dirtyPill.hidden = !v;
}
// snapshot of all form inputs to detect real changes
var inputs = Array.prototype.slice.call(
document.querySelectorAll(".section input, .section textarea, .section select")
).filter(function (el) { return el.type !== "password"; });
var baseline = {};
inputs.forEach(function (el, i) { el.dataset.k = "f" + i; baseline[el.dataset.k] = el.value; });
// toggle baseline
var switches = Array.prototype.slice.call(document.querySelectorAll(".switch"));
var toggleBase = {};
switches.forEach(function (sw) {
toggleBase[sw.dataset.toggle] = sw.getAttribute("aria-checked") === "true";
// restore persisted toggle state
if (prefs.toggles && prefs.toggles[sw.dataset.toggle] != null) {
sw.setAttribute("aria-checked", prefs.toggles[sw.dataset.toggle] ? "true" : "false");
toggleBase[sw.dataset.toggle] = prefs.toggles[sw.dataset.toggle];
}
});
function recompute() {
var changed = false;
inputs.forEach(function (el) { if (el.value !== baseline[el.dataset.k]) changed = true; });
switches.forEach(function (sw) {
if ((sw.getAttribute("aria-checked") === "true") !== toggleBase[sw.dataset.toggle]) changed = true;
});
setDirty(changed);
}
inputs.forEach(function (el) { el.addEventListener("input", recompute); });
/* ---------- Toggles ---------- */
switches.forEach(function (sw) {
sw.addEventListener("click", function () {
var on = sw.getAttribute("aria-checked") !== "true";
sw.setAttribute("aria-checked", on ? "true" : "false");
// live-effect toggles that persist immediately
if (sw.dataset.toggle === "reduceMotion") {
on ? root.setAttribute("data-reduce-motion", "1") : root.removeAttribute("data-reduce-motion");
}
recompute();
});
sw.addEventListener("keydown", function (e) {
if (e.key === " " || e.key === "Enter") { e.preventDefault(); sw.click(); }
});
});
/* ---------- Theme picker ---------- */
var themeCards = Array.prototype.slice.call(document.querySelectorAll(".theme-card"));
function selectRadio(list, el, attr) {
list.forEach(function (x) {
var on = x === el;
x.classList.toggle("is-active", on);
x.setAttribute("aria-checked", on ? "true" : "false");
x.tabIndex = on ? 0 : -1;
});
}
themeCards.forEach(function (card) {
if (card.dataset.theme === themeMode) selectRadio(themeCards, card);
card.addEventListener("click", function () {
selectRadio(themeCards, card);
themeMode = card.dataset.theme;
applyTheme(themeMode);
prefs.theme = themeMode;
save(prefs);
toast("Theme set to " + card.dataset.theme);
});
});
/* ---------- Accent picker ---------- */
var swatches = Array.prototype.slice.call(document.querySelectorAll(".accent-swatch"));
swatches.forEach(function (sw) {
if (prefs.accent && sw.dataset.accent === prefs.accent) selectRadio(swatches, sw);
sw.addEventListener("click", function () {
selectRadio(swatches, sw);
applyAccent(sw.dataset.accent);
prefs.accent = sw.dataset.accent;
save(prefs);
toast("Accent color updated");
});
});
/* ---------- Save / Discard ---------- */
var saveBtn = document.getElementById("saveBtn");
var discardBtn = document.getElementById("discardBtn");
function commit() {
inputs.forEach(function (el) { baseline[el.dataset.k] = el.value; });
prefs.toggles = prefs.toggles || {};
switches.forEach(function (sw) {
var on = sw.getAttribute("aria-checked") === "true";
toggleBase[sw.dataset.toggle] = on;
prefs.toggles[sw.dataset.toggle] = on;
});
save(prefs);
setDirty(false);
}
saveBtn.addEventListener("click", function () {
commit();
toast("Settings saved");
});
discardBtn.addEventListener("click", function () {
inputs.forEach(function (el) { el.value = baseline[el.dataset.k]; });
switches.forEach(function (sw) {
sw.setAttribute("aria-checked", toggleBase[sw.dataset.toggle] ? "true" : "false");
if (sw.dataset.toggle === "reduceMotion") {
toggleBase.reduceMotion ? root.setAttribute("data-reduce-motion", "1") : root.removeAttribute("data-reduce-motion");
}
});
setDirty(false);
toast("Changes discarded", false);
});
/* ---------- Unsaved-changes guard ---------- */
window.addEventListener("beforeunload", function (e) {
if (dirty) { e.preventDefault(); e.returnValue = ""; }
});
/* ---------- Misc buttons ---------- */
var photoBtn = document.getElementById("changePhoto");
if (photoBtn) photoBtn.addEventListener("click", function () { toast("Photo upload is disabled in this demo", false); });
document.querySelectorAll(".sess-revoke").forEach(function (btn) {
btn.addEventListener("click", function () {
var li = btn.closest("li");
li.style.transition = "opacity .2s, transform .2s";
li.style.opacity = "0";
li.style.transform = "translateX(8px)";
setTimeout(function () { li.remove(); }, 200);
toast("Session revoked");
});
});
/* ---------- Delete modal ---------- */
var modal = document.getElementById("modal");
var openDelete = document.getElementById("deleteWs");
var cancelDelete = document.getElementById("cancelDelete");
var confirmDelete = document.getElementById("confirmDelete");
var confirmInput = document.getElementById("confirmInput");
var lastFocus = null;
function openModal() {
lastFocus = document.activeElement;
modal.hidden = false;
confirmInput.value = "";
confirmDelete.disabled = true;
confirmInput.focus();
document.addEventListener("keydown", escClose);
}
function closeModal() {
modal.hidden = true;
document.removeEventListener("keydown", escClose);
if (lastFocus) lastFocus.focus();
}
function escClose(e) { if (e.key === "Escape") closeModal(); }
openDelete.addEventListener("click", openModal);
cancelDelete.addEventListener("click", closeModal);
modal.addEventListener("click", function (e) { if (e.target === modal) closeModal(); });
confirmInput.addEventListener("input", function () {
confirmDelete.disabled = confirmInput.value.trim() !== "DELETE";
});
confirmDelete.addEventListener("click", function () {
closeModal();
toast("Workspace deletion scheduled (demo only)", false);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Atlas — Settings</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#panel">Skip to settings</a>
<header class="topbar" role="banner">
<div class="brand">
<span class="brand-mark" aria-hidden="true">
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l8 4.5v9L12 22 4 15.5v-9z"/><path d="M12 2v20"/><path d="M4 6.5l8 4.5 8-4.5"/></svg>
</span>
<span class="brand-name">Atlas</span>
<span class="brand-tag">Workspace</span>
</div>
<div class="topbar-right">
<span class="dirty-pill" id="dirtyPill" hidden>Unsaved changes</span>
<div class="avatar" aria-hidden="true">JD</div>
</div>
</header>
<main class="shell">
<nav class="settings-nav" aria-label="Settings sections">
<p class="nav-eyebrow">Settings</p>
<ul role="tablist" aria-orientation="vertical">
<li role="presentation">
<button role="tab" id="tab-profile" aria-controls="sec-profile" aria-selected="true" class="nav-item is-active" data-section="profile">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>
<span>Profile</span>
</button>
</li>
<li role="presentation">
<button role="tab" id="tab-workspace" aria-controls="sec-workspace" aria-selected="false" class="nav-item" data-section="workspace" tabindex="-1">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 21h18"/><path d="M5 21V7l8-4v18"/><path d="M19 21V11l-6-4"/></svg>
<span>Workspace</span>
</button>
</li>
<li role="presentation">
<button role="tab" id="tab-notifications" aria-controls="sec-notifications" aria-selected="false" class="nav-item" data-section="notifications" tabindex="-1">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.7 21a2 2 0 0 1-3.4 0"/></svg>
<span>Notifications</span>
</button>
</li>
<li role="presentation">
<button role="tab" id="tab-appearance" aria-controls="sec-appearance" aria-selected="false" class="nav-item" data-section="appearance" tabindex="-1">
<svg viewBox="0 0 24 24" width="18" height="18" 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 20v2M2 12h2M20 12h2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
<span>Appearance</span>
</button>
</li>
<li role="presentation">
<button role="tab" id="tab-security" aria-controls="sec-security" aria-selected="false" class="nav-item" data-section="security" tabindex="-1">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
<span>Security</span>
</button>
</li>
</ul>
</nav>
<div class="panel" id="panel">
<!-- PROFILE -->
<section role="tabpanel" id="sec-profile" aria-labelledby="tab-profile" class="section is-active" data-form>
<div class="section-head">
<div>
<h1>Profile</h1>
<p class="section-sub">How you appear across the Atlas workspace.</p>
</div>
</div>
<div class="card">
<div class="field-row identity">
<div class="big-avatar" aria-hidden="true">JD</div>
<div class="identity-meta">
<p class="identity-name">Jordan Diaz</p>
<p class="identity-mail">jordan@atlas.dev · Owner</p>
<button type="button" class="btn ghost sm" id="changePhoto">Change photo</button>
</div>
</div>
</div>
<div class="card grid-2">
<label class="field">
<span class="label">Full name</span>
<input type="text" name="fullName" value="Jordan Diaz" autocomplete="name" />
</label>
<label class="field">
<span class="label">Display name</span>
<input type="text" name="displayName" value="Jordan" />
</label>
<label class="field">
<span class="label">Email address</span>
<input type="email" name="email" value="jordan@atlas.dev" autocomplete="email" />
</label>
<label class="field">
<span class="label">Role title</span>
<input type="text" name="title" value="Product Lead" />
</label>
<label class="field span-2">
<span class="label">Bio</span>
<textarea name="bio" rows="3" maxlength="160">Building delightful internal tools at Atlas. Coffee, maps, and clean dashboards.</textarea>
<span class="hint">Brief description for your teammates. Max 160 characters.</span>
</label>
<label class="field">
<span class="label">Time zone</span>
<select name="tz">
<option>(GMT-08:00) Pacific Time</option>
<option selected>(GMT-05:00) Eastern Time</option>
<option>(GMT+00:00) London</option>
<option>(GMT+01:00) Berlin</option>
<option>(GMT+05:30) Mumbai</option>
</select>
</label>
<label class="field">
<span class="label">Language</span>
<select name="lang">
<option selected>English (US)</option>
<option>Español</option>
<option>Français</option>
<option>Deutsch</option>
</select>
</label>
</div>
</section>
<!-- WORKSPACE -->
<section role="tabpanel" id="sec-workspace" aria-labelledby="tab-workspace" class="section" data-form hidden>
<div class="section-head">
<div>
<h1>Workspace</h1>
<p class="section-sub">Settings that apply to everyone in <strong>Atlas</strong>.</p>
</div>
</div>
<div class="card grid-2">
<label class="field">
<span class="label">Workspace name</span>
<input type="text" name="wsName" value="Atlas" />
</label>
<label class="field">
<span class="label">Workspace URL</span>
<div class="input-affix">
<span class="affix">atlas.dev/</span>
<input type="text" name="wsSlug" value="atlas-hq" />
</div>
</label>
<label class="field">
<span class="label">Default member role</span>
<select name="wsRole">
<option selected>Member</option>
<option>Editor</option>
<option>Viewer</option>
</select>
</label>
<label class="field">
<span class="label">Billing email</span>
<input type="email" name="wsBilling" value="billing@atlas.dev" autocomplete="email" />
</label>
</div>
<div class="card">
<h2 class="card-title">Membership</h2>
<div class="toggle-row">
<div class="toggle-text">
<span class="toggle-label">Allow open invites</span>
<span class="toggle-desc">Anyone with an @atlas.dev address can join automatically.</span>
</div>
<button class="switch" role="switch" aria-checked="true" data-toggle="openInvites"><span class="knob"></span></button>
</div>
<div class="toggle-row">
<div class="toggle-text">
<span class="toggle-label">Require admin approval</span>
<span class="toggle-desc">New members must be approved before accessing data.</span>
</div>
<button class="switch" role="switch" aria-checked="false" data-toggle="adminApproval"><span class="knob"></span></button>
</div>
</div>
</section>
<!-- NOTIFICATIONS -->
<section role="tabpanel" id="sec-notifications" aria-labelledby="tab-notifications" class="section" data-form hidden>
<div class="section-head">
<div>
<h1>Notifications</h1>
<p class="section-sub">Choose what reaches you and where.</p>
</div>
</div>
<div class="card">
<div class="notif-grid notif-head" aria-hidden="true">
<span>Activity</span><span>Email</span><span>Push</span>
</div>
<div class="notif-grid" data-notif="mentions">
<div class="notif-text"><span class="toggle-label">Mentions & replies</span><span class="toggle-desc">When someone @mentions you.</span></div>
<button class="switch sm" role="switch" aria-checked="true" data-toggle="mentions_email" aria-label="Mentions email"><span class="knob"></span></button>
<button class="switch sm" role="switch" aria-checked="true" data-toggle="mentions_push" aria-label="Mentions push"><span class="knob"></span></button>
</div>
<div class="notif-grid" data-notif="comments">
<div class="notif-text"><span class="toggle-label">Comments</span><span class="toggle-desc">New comments on items you follow.</span></div>
<button class="switch sm" role="switch" aria-checked="true" data-toggle="comments_email" aria-label="Comments email"><span class="knob"></span></button>
<button class="switch sm" role="switch" aria-checked="false" data-toggle="comments_push" aria-label="Comments push"><span class="knob"></span></button>
</div>
<div class="notif-grid" data-notif="weekly">
<div class="notif-text"><span class="toggle-label">Weekly digest</span><span class="toggle-desc">A summary every Monday morning.</span></div>
<button class="switch sm" role="switch" aria-checked="true" data-toggle="weekly_email" aria-label="Weekly digest email"><span class="knob"></span></button>
<button class="switch sm" role="switch" aria-checked="false" data-toggle="weekly_push" aria-label="Weekly digest push"><span class="knob"></span></button>
</div>
<div class="notif-grid" data-notif="product">
<div class="notif-text"><span class="toggle-label">Product updates</span><span class="toggle-desc">News about features and releases.</span></div>
<button class="switch sm" role="switch" aria-checked="false" data-toggle="product_email" aria-label="Product updates email"><span class="knob"></span></button>
<button class="switch sm" role="switch" aria-checked="false" data-toggle="product_push" aria-label="Product updates push"><span class="knob"></span></button>
</div>
</div>
<div class="card">
<h2 class="card-title">Quiet hours</h2>
<div class="toggle-row">
<div class="toggle-text">
<span class="toggle-label">Pause push at night</span>
<span class="toggle-desc">Mute push notifications from 10pm to 7am, your local time.</span>
</div>
<button class="switch" role="switch" aria-checked="true" data-toggle="quietHours"><span class="knob"></span></button>
</div>
</div>
</section>
<!-- APPEARANCE -->
<section role="tabpanel" id="sec-appearance" aria-labelledby="tab-appearance" class="section" data-form hidden>
<div class="section-head">
<div>
<h1>Appearance</h1>
<p class="section-sub">Personalize how Atlas looks for you.</p>
</div>
</div>
<div class="card">
<h2 class="card-title">Theme</h2>
<div class="theme-picker" role="radiogroup" aria-label="Theme">
<button class="theme-card is-active" role="radio" aria-checked="true" data-theme="light">
<span class="theme-preview light" aria-hidden="true"><i></i><i></i></span>
<span class="theme-name">Light</span>
</button>
<button class="theme-card" role="radio" aria-checked="false" data-theme="dark">
<span class="theme-preview dark" aria-hidden="true"><i></i><i></i></span>
<span class="theme-name">Dark</span>
</button>
<button class="theme-card" role="radio" aria-checked="false" data-theme="system">
<span class="theme-preview system" aria-hidden="true"><i></i><i></i></span>
<span class="theme-name">System</span>
</button>
</div>
</div>
<div class="card">
<h2 class="card-title">Accent color</h2>
<div class="accent-picker" role="radiogroup" aria-label="Accent color">
<button class="accent-swatch is-active" role="radio" aria-checked="true" data-accent="99 102 241" style="--sw:99 102 241" aria-label="Indigo"></button>
<button class="accent-swatch" role="radio" aria-checked="false" data-accent="16 163 127" style="--sw:16 163 127" aria-label="Emerald"></button>
<button class="accent-swatch" role="radio" aria-checked="false" data-accent="225 29 72" style="--sw:225 29 72" aria-label="Rose"></button>
<button class="accent-swatch" role="radio" aria-checked="false" data-accent="217 119 6" style="--sw:217 119 6" aria-label="Amber"></button>
<button class="accent-swatch" role="radio" aria-checked="false" data-accent="14 116 144" style="--sw:14 116 144" aria-label="Cyan"></button>
</div>
</div>
<div class="card">
<h2 class="card-title">Interface</h2>
<div class="toggle-row">
<div class="toggle-text">
<span class="toggle-label">Reduce motion</span>
<span class="toggle-desc">Minimize animations and transitions across the app.</span>
</div>
<button class="switch" role="switch" aria-checked="false" data-toggle="reduceMotion"><span class="knob"></span></button>
</div>
<div class="toggle-row">
<div class="toggle-text">
<span class="toggle-label">Compact density</span>
<span class="toggle-desc">Show more rows by tightening spacing.</span>
</div>
<button class="switch" role="switch" aria-checked="false" data-toggle="compact"><span class="knob"></span></button>
</div>
</div>
</section>
<!-- SECURITY -->
<section role="tabpanel" id="sec-security" aria-labelledby="tab-security" class="section" data-form hidden>
<div class="section-head">
<div>
<h1>Security</h1>
<p class="section-sub">Protect your account and review active sessions.</p>
</div>
</div>
<div class="card grid-2">
<label class="field">
<span class="label">Current password</span>
<input type="password" name="curPass" placeholder="••••••••" />
</label>
<label class="field">
<span class="label">New password</span>
<input type="password" name="newPass" placeholder="At least 12 characters" />
</label>
</div>
<div class="card">
<h2 class="card-title">Two-factor authentication</h2>
<div class="toggle-row">
<div class="toggle-text">
<span class="toggle-label">Authenticator app</span>
<span class="toggle-desc">Require a one-time code from your authenticator at sign-in.</span>
</div>
<button class="switch" role="switch" aria-checked="true" data-toggle="twofa"><span class="knob"></span></button>
</div>
</div>
<div class="card">
<h2 class="card-title">Active sessions</h2>
<ul class="sessions">
<li><span class="sess-dot ok" aria-hidden="true"></span><div><p class="sess-name">MacBook Pro · Chrome <span class="chip">This device</span></p><p class="sess-meta">New York, US · Active now</p></div></li>
<li><span class="sess-dot" aria-hidden="true"></span><div><p class="sess-name">iPhone 15 · Atlas app</p><p class="sess-meta">New York, US · 3 hours ago</p></div><button type="button" class="btn ghost sm sess-revoke">Revoke</button></li>
<li><span class="sess-dot" aria-hidden="true"></span><div><p class="sess-name">Windows · Edge</p><p class="sess-meta">Austin, US · Yesterday</p></div><button type="button" class="btn ghost sm sess-revoke">Revoke</button></li>
</ul>
</div>
<div class="card danger-zone">
<h2 class="card-title danger">Danger zone</h2>
<div class="danger-row">
<div>
<p class="toggle-label">Delete workspace</p>
<p class="toggle-desc">Permanently remove Atlas and all of its data. This cannot be undone.</p>
</div>
<button type="button" class="btn danger" id="deleteWs">Delete workspace</button>
</div>
</div>
</section>
<!-- ACTION BAR -->
<div class="action-bar" id="actionBar" hidden>
<span class="action-msg">You have unsaved changes.</span>
<div class="action-btns">
<button type="button" class="btn ghost" id="discardBtn">Discard</button>
<button type="button" class="btn primary" id="saveBtn">Save changes</button>
</div>
</div>
</div>
</main>
<!-- Delete confirm modal -->
<div class="modal-backdrop" id="modal" hidden>
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modalTitle">
<h2 id="modalTitle">Delete workspace?</h2>
<p>This will permanently delete <strong>Atlas</strong> and remove access for all members. Type <code>DELETE</code> to confirm.</p>
<input type="text" id="confirmInput" placeholder="DELETE" aria-label="Type DELETE to confirm" />
<div class="modal-btns">
<button type="button" class="btn ghost" id="cancelDelete">Cancel</button>
<button type="button" class="btn danger" id="confirmDelete" disabled>Delete forever</button>
</div>
</div>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Settings Page
A complete account settings surface for the fictional Atlas workspace. A sticky vertical nav switches between five panels — Profile, Workspace, Notifications, Appearance, and Security — implemented as an accessible tablist with full arrow-key navigation. Each panel groups its controls into clean cards: labelled inputs and selects, an affixed workspace URL field, custom switch toggles, and a per-channel notification matrix for email and push.
Every interaction works. Editing any field or flipping any toggle raises a real dirty state, revealing a floating Save / Discard action bar and an Unsaved changes pill in the top bar. Saving commits a new baseline and persists toggle states; discarding restores the originals. The Appearance panel ships a working light, dark, and system theme picker plus an accent-color picker — both apply live and persist to local storage, with Reduce motion honoring the user’s preference instantly.
The Security panel rounds it out with a password form, two-factor toggle, a revocable active-sessions list, and a danger zone whose Delete workspace action opens a focus-trapped confirmation modal that requires typing DELETE. A beforeunload guard warns before you navigate away with pending edits.
Illustrative SaaS UI only — fictional product, metrics, and billing. No real backend.