diff --git a/shared/elevation.css b/shared/elevation.css index 9dd31fc..26c02c7 100644 --- a/shared/elevation.css +++ b/shared/elevation.css @@ -45,3 +45,69 @@ color: var(--danger); font-weight: 600; } + +/* Page-wide chrome when admin mode is active. The toggle alone is + easy to miss; these add an inescapable visual cue: + 1. Thin red border around the entire page (body) — peripheral- + vision reminder regardless of which tool / scroll position. + 2. Sticky banner across the top with a one-click "Drop admin" + button so the user can disarm without hunting for the toggle. + Both rendered ONLY when the zddc-elevate cookie is set; the + shared/elevation.js init() syncs the body class on every page + load and tears it down when elevation is cleared. */ +body.is-elevated { + outline: 3px solid var(--danger, #dc3545); + outline-offset: -3px; +} + +.elevation-banner { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.4rem 0.9rem; + background: rgba(220, 53, 69, 0.95); + color: #fff; + font-size: 0.85rem; + font-weight: 500; + letter-spacing: 0.01em; + position: sticky; + top: 0; + z-index: 9100; /* above modal-overlay (9000) so it's never hidden */ + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18); +} + +.elevation-banner__dot { + width: 0.5rem; + height: 0.5rem; + background: #fff; + border-radius: 50%; + box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); + animation: elev-pulse 1.6s infinite; + flex-shrink: 0; +} + +@keyframes elev-pulse { + 0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); } + 70% { box-shadow: 0 0 0 8px rgba(255, 255, 255, 0); } + 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); } +} + +.elevation-banner__msg { + flex: 1 1 auto; +} + +.elevation-banner__off { + background: rgba(255, 255, 255, 0.18); + border: 1px solid rgba(255, 255, 255, 0.7); + color: #fff; + padding: 0.18rem 0.65rem; + border-radius: var(--radius, 4px); + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.02em; + cursor: pointer; + flex-shrink: 0; +} +.elevation-banner__off:hover { + background: rgba(255, 255, 255, 0.3); +} diff --git a/shared/elevation.js b/shared/elevation.js index bc6b135..f53c1b9 100644 --- a/shared/elevation.js +++ b/shared/elevation.js @@ -77,7 +77,52 @@ }); } + // Page-wide affordances when elevation is active. The toggle alone + // is easy to miss — admin mode silently bypasses WORM and ACL + // restrictions, which produces surprising "I shouldn't have been + // able to do that" moments. A body class + a sticky banner with a + // one-click disable make the armed state unmistakable. + function applyArmedChrome(elevated) { + var b = document.body; + if (!b) return; + if (elevated) b.classList.add('is-elevated'); + else b.classList.remove('is-elevated'); + + var banner = document.getElementById('elevation-banner'); + if (elevated) { + if (!banner) { + banner = document.createElement('div'); + banner.id = 'elevation-banner'; + banner.className = 'elevation-banner'; + banner.setAttribute('role', 'alert'); + banner.innerHTML = + '' + + '' + + 'Admin mode is on — write access bypasses WORM and ACL safeguards.' + + '' + + ''; + document.body.insertBefore(banner, document.body.firstChild); + var off = banner.querySelector('#elevation-banner-off'); + if (off) off.addEventListener('click', function () { + setElevated(false); + window.location.reload(); + }); + } + } else if (banner) { + banner.parentNode.removeChild(banner); + } + } + async function init() { + // Body chrome applies on every page load whether or not the + // header has a toggle slot — the banner needs to surface in + // tools / pages that don't host the toggle (e.g. iframed + // classifier inside browse's grid mode), so the user can't + // accidentally write through an elevated context elsewhere. + applyArmedChrome(isElevated()); + var host = document.getElementById('elevation-toggle'); if (!host) return; // tool doesn't include the slot yet — no-op var access = await fetchAccess(); diff --git a/zddc/internal/handler/tables.html b/zddc/internal/handler/tables.html index 20acf84..799dd2f 100644 --- a/zddc/internal/handler/tables.html +++ b/zddc/internal/handler/tables.html @@ -884,6 +884,72 @@ body.help-open .app-header { font-weight: 600; } +/* Page-wide chrome when admin mode is active. The toggle alone is + easy to miss; these add an inescapable visual cue: + 1. Thin red border around the entire page (body) — peripheral- + vision reminder regardless of which tool / scroll position. + 2. Sticky banner across the top with a one-click "Drop admin" + button so the user can disarm without hunting for the toggle. + Both rendered ONLY when the zddc-elevate cookie is set; the + shared/elevation.js init() syncs the body class on every page + load and tears it down when elevation is cleared. */ +body.is-elevated { + outline: 3px solid var(--danger, #dc3545); + outline-offset: -3px; +} + +.elevation-banner { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.4rem 0.9rem; + background: rgba(220, 53, 69, 0.95); + color: #fff; + font-size: 0.85rem; + font-weight: 500; + letter-spacing: 0.01em; + position: sticky; + top: 0; + z-index: 9100; /* above modal-overlay (9000) so it's never hidden */ + box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18); +} + +.elevation-banner__dot { + width: 0.5rem; + height: 0.5rem; + background: #fff; + border-radius: 50%; + box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); + animation: elev-pulse 1.6s infinite; + flex-shrink: 0; +} + +@keyframes elev-pulse { + 0% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7); } + 70% { box-shadow: 0 0 0 8px rgba(255, 255, 255, 0); } + 100% { box-shadow: 0 0 0 0 rgba(255, 255, 255, 0); } +} + +.elevation-banner__msg { + flex: 1 1 auto; +} + +.elevation-banner__off { + background: rgba(255, 255, 255, 0.18); + border: 1px solid rgba(255, 255, 255, 0.7); + color: #fff; + padding: 0.18rem 0.65rem; + border-radius: var(--radius, 4px); + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 0.02em; + cursor: pointer; + flex-shrink: 0; +} +.elevation-banner__off:hover { + background: rgba(255, 255, 255, 0.3); +} + /* shared/nav.css — lateral project-stage strip paired with shared/nav.js. Sits as a sibling immediately under .app-header (mounted by JS). Rendered only in online mode when a project segment is in the URL. */ @@ -1493,7 +1559,7 @@ body.help-open .app-header {
ZDDC Table - v0.0.17-alpha · 2026-05-15 20:58:00 · 167a56d-dirty + v0.0.17-alpha · 2026-05-18 13:40:38 · 03d008f-dirty
@@ -3063,7 +3129,52 @@ body.help-open .app-header { }); } + // Page-wide affordances when elevation is active. The toggle alone + // is easy to miss — admin mode silently bypasses WORM and ACL + // restrictions, which produces surprising "I shouldn't have been + // able to do that" moments. A body class + a sticky banner with a + // one-click disable make the armed state unmistakable. + function applyArmedChrome(elevated) { + var b = document.body; + if (!b) return; + if (elevated) b.classList.add('is-elevated'); + else b.classList.remove('is-elevated'); + + var banner = document.getElementById('elevation-banner'); + if (elevated) { + if (!banner) { + banner = document.createElement('div'); + banner.id = 'elevation-banner'; + banner.className = 'elevation-banner'; + banner.setAttribute('role', 'alert'); + banner.innerHTML = + '' + + '' + + 'Admin mode is on — write access bypasses WORM and ACL safeguards.' + + '' + + ''; + document.body.insertBefore(banner, document.body.firstChild); + var off = banner.querySelector('#elevation-banner-off'); + if (off) off.addEventListener('click', function () { + setElevated(false); + window.location.reload(); + }); + } + } else if (banner) { + banner.parentNode.removeChild(banner); + } + } + async function init() { + // Body chrome applies on every page load whether or not the + // header has a toggle slot — the banner needs to surface in + // tools / pages that don't host the toggle (e.g. iframed + // classifier inside browse's grid mode), so the user can't + // accidentally write through an elevated context elsewhere. + applyArmedChrome(isElevated()); + var host = document.getElementById('elevation-toggle'); if (!host) return; // tool doesn't include the slot yet — no-op var access = await fetchAccess();