implement unified on-site editor phases 1-2
All checks were successful
deploy / deploy (push) Successful in 1m10s
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:
@@ -2525,6 +2525,15 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* ── Editor overlay ── */
|
||||
.editor-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 999;
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* ── Editor panel ── */
|
||||
.editor-panel {
|
||||
position: fixed;
|
||||
@@ -2533,6 +2542,10 @@
|
||||
flex-direction: column;
|
||||
background: var(--t-surface-base);
|
||||
box-shadow: var(--t-shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.15));
|
||||
/* Force GPU compositing to prevent paint flickering during drag */
|
||||
transform: translateZ(0);
|
||||
backface-visibility: hidden;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
/* Hidden when collapsed (but not during close animation) */
|
||||
@@ -2540,6 +2553,34 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* ── Drag handle (mobile only) ── */
|
||||
.editor-panel-drag-handle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.editor-panel-drag-handle {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
cursor: grab;
|
||||
touch-action: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-panel-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.editor-panel-drag-handle-bar {
|
||||
width: 2.5rem;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
background: var(--t-border-default);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Mobile: bottom sheet ── */
|
||||
@media (max-width: 767px) {
|
||||
.editor-panel[data-state="open"],
|
||||
@@ -2547,13 +2588,26 @@
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 15dvh;
|
||||
/* Default 50vh, overridden by JS with saved preference */
|
||||
height: var(--editor-panel-height, 50dvh);
|
||||
max-height: 90dvh;
|
||||
min-height: 200px;
|
||||
border-radius: var(--t-radius-lg, 12px) var(--t-radius-lg, 12px) 0 0;
|
||||
border: 1px solid var(--t-border-default);
|
||||
border-bottom: none;
|
||||
/* Keep layer promoted to prevent flash on resize end */
|
||||
will-change: transform;
|
||||
contain: strict;
|
||||
}
|
||||
|
||||
.editor-panel[data-state="open"] {
|
||||
/* During drag: disable animations and also hint height changes */
|
||||
.editor-panel.dragging {
|
||||
transition: none !important;
|
||||
will-change: transform, height;
|
||||
}
|
||||
|
||||
/* Only animate on initial open, not after drag ends */
|
||||
.editor-panel[data-state="open"]:not(.dragging):not(.has-dragged) {
|
||||
animation: editor-panel-slide-up 0.3s cubic-bezier(0.32, 0.72, 0, 1) both;
|
||||
}
|
||||
|
||||
@@ -2640,10 +2694,30 @@
|
||||
.editor-panel-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-panel-close {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
background: transparent;
|
||||
color: var(--t-text-secondary);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: var(--t-border-default);
|
||||
color: var(--t-text-primary);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Dirty indicator ── */
|
||||
.editor-panel-dirty {
|
||||
display: flex;
|
||||
@@ -2661,12 +2735,50 @@
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
/* ── Tab bar ── */
|
||||
.editor-tabs {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
border-bottom: 1px solid var(--t-border-default);
|
||||
padding: 0 0.75rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-tab {
|
||||
flex: 1;
|
||||
padding: 0.5rem 0.75rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
color: var(--t-text-secondary);
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: color 0.15s, border-color 0.15s;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
color: var(--t-text-primary);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.4;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-tab-active {
|
||||
color: var(--t-text-primary);
|
||||
border-bottom-color: var(--t-accent, oklch(0.6 0.15 250));
|
||||
}
|
||||
|
||||
/* ── Panel content ── */
|
||||
.editor-panel-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
padding: 0.75rem;
|
||||
/* Ensure content is clipped to panel bounds during drag */
|
||||
contain: paint;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 1rem;
|
||||
|
||||
129
assets/js/app.js
129
assets/js/app.js
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user