implement unified on-site editor phases 1-2
All checks were successful
deploy / deploy (push) Successful in 1m10s

Add theme editing to the existing PageEditorHook, enabling on-site
theme customisation alongside page editing. The editor panel now has
three tabs (Page, Theme, Settings) and can be collapsed while
keeping editing state intact.

- Add theme editing state and event handlers to PageEditorHook
- Add 3-tab UI with tab switching logic
- Add transparent overlay for click-outside dismiss
- Add mobile drag-to-resize with height persistence
- Fix animation replay on drag release (has-dragged class)
- Preserve panel height across LiveView re-renders
- Default to Page tab on editable pages, Theme otherwise
- Show unsaved changes indicator on FAB when panel collapsed
- Fix handle_event grouping warning in admin theme

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
jamey
2026-03-09 09:01:21 +00:00
parent 74ab6411f7
commit 168b6ce76f
10 changed files with 954 additions and 53 deletions

View File

@@ -679,18 +679,10 @@ const DirtyGuard = {
}
}
// EditorSheet: handles click-outside and Escape for the editor panel
// EditorSheet: handles click-outside, Escape, and mobile drag-to-resize
const EditorSheet = {
mounted() {
this._onDocMousedown = (e) => {
if (this._getState() !== "open") return
const fab = document.querySelector(".editor-fab")
if (this.el.contains(e.target)) return
if (fab && fab.contains(e.target)) return
this._close()
}
document.addEventListener("mousedown", this._onDocMousedown)
// Close on Escape key
this._onKeydown = (e) => {
if (e.key === "Escape" && this._getState() === "open") {
e.preventDefault()
@@ -698,11 +690,29 @@ const EditorSheet = {
}
}
document.addEventListener("keydown", this._onKeydown)
// Restore saved height on mobile and mark as already opened
this._restoreSavedHeight()
this._hasDragged = !!localStorage.getItem("editor-panel-height")
if (this._hasDragged) {
this.el.classList.add("has-dragged")
}
// Set up drag handle for mobile resizing
this._setupDragHandle()
},
updated() {
// Restore state after LiveView re-renders the element
this._restoreSavedHeight()
if (this._hasDragged) {
this.el.classList.add("has-dragged")
}
},
destroyed() {
document.removeEventListener("mousedown", this._onDocMousedown)
document.removeEventListener("keydown", this._onKeydown)
this._cleanupDragHandle()
},
_getState() {
@@ -732,6 +742,103 @@ const EditorSheet = {
if (region) {
region.textContent = message
}
},
// ── Mobile drag-to-resize ──
_isMobile() {
return window.matchMedia("(max-width: 767px)").matches
},
_restoreSavedHeight() {
if (!this._isMobile()) return
const savedHeight = localStorage.getItem("editor-panel-height")
if (savedHeight) {
this.el.style.setProperty("--editor-panel-height", savedHeight)
}
},
_setupDragHandle() {
const handle = this.el.querySelector("[data-drag-handle]")
if (!handle) return
this._dragHandle = handle
this._isDragging = false
this._startY = 0
this._startHeight = 0
// Touch events
this._onTouchStart = (e) => this._handleDragStart(e.touches[0].clientY)
this._onTouchMove = (e) => {
if (!this._isDragging) return
e.preventDefault()
this._handleDragMove(e.touches[0].clientY)
}
this._onTouchEnd = () => this._handleDragEnd()
// Mouse events (for testing on desktop)
this._onMouseDown = (e) => {
e.preventDefault()
this._handleDragStart(e.clientY)
document.addEventListener("mousemove", this._onMouseMove)
document.addEventListener("mouseup", this._onMouseUp)
}
this._onMouseMove = (e) => {
if (!this._isDragging) return
this._handleDragMove(e.clientY)
}
this._onMouseUp = () => {
this._handleDragEnd()
document.removeEventListener("mousemove", this._onMouseMove)
document.removeEventListener("mouseup", this._onMouseUp)
}
handle.addEventListener("touchstart", this._onTouchStart, { passive: true })
document.addEventListener("touchmove", this._onTouchMove, { passive: false })
document.addEventListener("touchend", this._onTouchEnd)
handle.addEventListener("mousedown", this._onMouseDown)
},
_cleanupDragHandle() {
if (!this._dragHandle) return
this._dragHandle.removeEventListener("touchstart", this._onTouchStart)
document.removeEventListener("touchmove", this._onTouchMove)
document.removeEventListener("touchend", this._onTouchEnd)
this._dragHandle.removeEventListener("mousedown", this._onMouseDown)
document.removeEventListener("mousemove", this._onMouseMove)
document.removeEventListener("mouseup", this._onMouseUp)
},
_handleDragStart(clientY) {
if (!this._isMobile()) return
this._isDragging = true
this._startY = clientY
this._startHeight = this.el.offsetHeight
this.el.classList.add("dragging")
},
_handleDragMove(clientY) {
if (!this._isDragging) return
const deltaY = this._startY - clientY
const newHeight = this._startHeight + deltaY
const vh = window.innerHeight
const minHeight = 200
const maxHeight = vh * 0.9
const clampedHeight = Math.max(minHeight, Math.min(newHeight, maxHeight))
this.el.style.setProperty("--editor-panel-height", `${clampedHeight}px`)
},
_handleDragEnd() {
if (!this._isDragging) return
this._isDragging = false
// Save the height preference
const currentHeight = getComputedStyle(this.el).getPropertyValue("--editor-panel-height")
if (currentHeight) {
localStorage.setItem("editor-panel-height", currentHeight.trim())
}
// Mark that we've dragged so the open animation doesn't replay
this._hasDragged = true
this.el.classList.add("has-dragged")
this.el.classList.remove("dragging")
}
}