replace admin rail with unified bottom sheet editor
All checks were successful
deploy / deploy (push) Successful in 1m30s

- add editor sheet component anchored bottom (mobile) / right (desktop)
- admin cog moves to header, always visible for admins
- remove Done button from editor header, keep only Save
- add editor_at_defaults tracking to disable Reset when at defaults
- sheet collapses on click outside or Escape, stays in edit mode
- dirty indicator + beforeunload warning for unsaved changes
- keyboard shortcuts: Ctrl+Z undo, Ctrl+Shift+Z redo
- WCAG compliant: aria-expanded, live region, focus management

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-03-07 09:30:07 +00:00
parent dbcecc7878
commit f4f036b84b
12 changed files with 1232 additions and 474 deletions

View File

@@ -478,6 +478,56 @@ const SearchModal = {
}
}
// Mobile nav drawer - slides in from the left
const MobileNavDrawer = {
mounted() {
this.el.addEventListener("open-mobile-nav", () => this.open())
this.el.addEventListener("close-mobile-nav", () => this.close())
// Close on Escape key
this._keydown = (e) => {
if (e.key === "Escape" && this.isOpen()) {
e.preventDefault()
this.close()
}
}
document.addEventListener("keydown", this._keydown)
// Prevent hamburger button navigation on no-JS fallback
this._hamburger = document.querySelector('.header-hamburger')
if (this._hamburger) {
this._preventNav = (e) => e.preventDefault()
this._hamburger.addEventListener("click", this._preventNav)
}
},
destroyed() {
document.removeEventListener("keydown", this._keydown)
if (this._hamburger && this._preventNav) {
this._hamburger.removeEventListener("click", this._preventNav)
}
},
isOpen() {
return this.el.classList.contains("is-open")
},
open() {
this.el.classList.add("is-open")
document.body.style.overflow = "hidden"
// Focus the close button for accessibility
const closeBtn = this.el.querySelector(".mobile-nav-close")
if (closeBtn) closeBtn.focus()
},
close() {
this.el.classList.remove("is-open")
document.body.style.overflow = ""
// Return focus to hamburger button
if (this._hamburger) this._hamburger.focus()
}
}
// Flex-wrap base → horizontal scroll enhancement for collection category pills.
// If the pills wrap past 2 rows on mobile, switches to single-row scroll
// and scrolls the active pill into view.
@@ -629,6 +679,53 @@ const DirtyGuard = {
}
}
// EditorSheet: simple open/collapse sheet for page editing
// Positioning is handled by CSS - JS just handles click-outside and Escape
const EditorSheet = {
mounted() {
// Click outside to collapse (works in any mode for preview)
// Use mousedown instead of click to avoid race with LiveView re-renders
this._onDocMousedown = (e) => {
if (!this.el.contains(e.target) && this._getState() !== "collapsed") {
this._setState("collapsed")
}
}
document.addEventListener("mousedown", this._onDocMousedown)
// Escape key to collapse
this._onKeydown = (e) => {
if (e.key === "Escape" && this._getState() !== "collapsed") {
e.preventDefault()
this._setState("collapsed")
}
}
document.addEventListener("keydown", this._onKeydown)
},
destroyed() {
document.removeEventListener("mousedown", this._onDocMousedown)
document.removeEventListener("keydown", this._onKeydown)
},
_getState() {
return this.el.dataset.state || "collapsed"
},
_setState(state) {
this.el.dataset.state = state
this.el.setAttribute("aria-expanded", state !== "collapsed")
this.pushEvent("editor_set_sheet_state", { state })
this._announce(state === "collapsed" ? "Editor collapsed" : "Editor expanded")
},
_announce(message) {
const region = document.getElementById("editor-live-region")
if (region) {
region.textContent = message
}
}
}
// DirtyGuard + Ctrl+Z / Ctrl+Shift+Z undo/redo for page editors
const EditorKeyboard = {
mounted() {
@@ -682,7 +779,7 @@ const EditorKeyboard = {
const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content")
const liveSocket = new LiveSocket("/live", Socket, {
params: {_csrf_token: csrfToken, screen_width: window.innerWidth},
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, CollectionFilters, AnalyticsInit, AnalyticsExport, ChartTooltip, DirtyGuard, EditorKeyboard},
hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, MobileNavDrawer, CollectionFilters, AnalyticsInit, AnalyticsExport, ChartTooltip, DirtyGuard, EditorKeyboard, EditorSheet},
})
// Show progress bar on live navigation and form submits
@@ -701,6 +798,12 @@ window.addEventListener("phx:scroll-preview-top", (e) => {
// connect if there are any LiveViews on the page
liveSocket.connect()
// Suppress connection error banners during intentional navigation (refresh, link click).
// beforeunload fires on navigation but NOT on genuine connection drops.
window.addEventListener("beforeunload", () => {
document.body.classList.add("navigating-away")
})
// expose liveSocket on window for web console debug logs and latency simulation:
// >> liveSocket.enableDebug()
// >> liveSocket.enableLatencySim(1000) // enabled for duration of browser session