diff --git a/assets/css/admin/components.css b/assets/css/admin/components.css
index d4b33a4..a865173 100644
--- a/assets/css/admin/components.css
+++ b/assets/css/admin/components.css
@@ -2378,94 +2378,19 @@
}
/* ══════════════════════════════════════════════════════════════════════════
- Editor sheet (unified bottom/right sheet for page editing)
+ Editor FAB + Panel (page editing)
+
+ Two separate elements:
+ - .editor-fab: floating action button, always a pill in the corner
+ - .editor-panel: sliding panel, animates in/out
══════════════════════════════════════════════════════════════════════════ */
-.editor-sheet {
+/* ── Floating action button ── */
+.editor-fab {
position: fixed;
z-index: 1000;
- background: var(--t-surface-base);
- box-shadow: var(--t-shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.15));
- transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1);
- display: flex;
- flex-direction: column;
-}
-
-
-
-@media (prefers-reduced-motion: reduce) {
- .editor-sheet {
- transition: none;
- }
-}
-
-/* ── Mobile: bottom-anchored ── */
-@media (max-width: 767px) {
- /* Collapsed: floating pill button, no panel background */
- .editor-sheet {
- left: auto;
- right: 16px;
- bottom: 16px;
- top: auto;
- width: auto;
- height: auto;
- background: transparent;
- box-shadow: none;
- border: none;
- border-radius: 0;
- }
-
- /* Open: full panel from bottom */
- .editor-sheet[data-state="open"] {
- left: 0;
- right: 0;
- bottom: 0;
- top: 15dvh;
- width: auto;
- height: auto;
- background: var(--t-surface-base);
- box-shadow: var(--t-shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.15));
- border-radius: var(--t-radius-lg, 12px) var(--t-radius-lg, 12px) 0 0;
- border: 1px solid var(--t-border-default);
- border-bottom: none;
- }
-}
-
-/* ── Desktop: right-anchored ── */
-/* ── Desktop: same floating button, side panel when open ── */
-@media (min-width: 768px) {
- /* Collapsed: floating pill button, no panel background (same as mobile) */
- .editor-sheet {
- left: auto;
- right: 16px;
- bottom: 16px;
- top: auto;
- width: auto;
- height: auto;
- background: transparent;
- box-shadow: none;
- border: none;
- border-radius: 0;
- }
-
- /* Open: side panel from right */
- .editor-sheet[data-state="open"] {
- top: 0;
- right: 0;
- bottom: 0;
- width: 420px;
- max-width: 90vw;
- background: var(--t-surface-base);
- box-shadow: var(--t-shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.15));
- border-radius: var(--t-radius-lg, 12px) 0 0 var(--t-radius-lg, 12px);
- border: 1px solid var(--t-border-default);
- border-right: none;
- }
-}
-
-
-/* ── Edit button in collapsed state ── */
-.editor-sheet-edit-btn {
+ right: 16px;
+ bottom: 16px;
display: flex;
align-items: center;
gap: 0.5rem;
@@ -2478,62 +2403,119 @@
font-weight: 500;
cursor: pointer;
transition: filter 0.15s, box-shadow 0.15s;
- /* Soft glow for visibility over any background */
box-shadow:
0 2px 8px rgba(0, 0, 0, 0.2),
0 0 0 3px rgba(255, 255, 255, 0.6);
}
-.editor-sheet-edit-btn:hover {
+.editor-fab:hover {
filter: brightness(1.1);
}
-.editor-sheet-edit-btn svg {
+.editor-fab svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
-
-/* ── Dirty indicator ── */
-.editor-sheet-dirty {
- display: flex;
- align-items: center;
- gap: 0.25rem;
- color: var(--t-status-warning, oklch(0.75 0.18 85));
- font-size: 0.75rem;
- font-weight: 500;
-}
-
-.editor-sheet-dirty-dot {
+.editor-fab-dirty {
width: 8px;
height: 8px;
border-radius: 9999px;
- background: currentColor;
+ background: var(--t-status-warning, oklch(0.75 0.18 85));
+ flex-shrink: 0;
}
-/* Desktop collapsed: hide "Unsaved" text, show only dot */
+/* ── Editor panel ── */
+.editor-panel {
+ position: fixed;
+ z-index: 1000;
+ display: flex;
+ flex-direction: column;
+ background: var(--t-surface-base);
+ box-shadow: var(--t-shadow-lg, 0 8px 24px rgba(0, 0, 0, 0.15));
+}
+
+/* Hidden when collapsed (but not during close animation) */
+.editor-panel[data-state="collapsed"]:not(.closing) {
+ display: none;
+}
+
+/* ── Mobile: bottom sheet ── */
+@media (max-width: 767px) {
+ .editor-panel[data-state="open"],
+ .editor-panel.closing {
+ left: 0;
+ right: 0;
+ bottom: 0;
+ top: 15dvh;
+ border-radius: var(--t-radius-lg, 12px) var(--t-radius-lg, 12px) 0 0;
+ border: 1px solid var(--t-border-default);
+ border-bottom: none;
+ }
+
+ .editor-panel[data-state="open"] {
+ animation: editor-panel-slide-up 0.3s cubic-bezier(0.32, 0.72, 0, 1) both;
+ }
+
+ .editor-panel.closing {
+ animation: editor-panel-slide-down 0.25s cubic-bezier(0.32, 0.72, 0, 1) both;
+ }
+}
+
+@keyframes editor-panel-slide-up {
+ from { opacity: 0; transform: translateY(24px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes editor-panel-slide-down {
+ from { opacity: 1; transform: translateY(0); }
+ to { opacity: 0; transform: translateY(24px); }
+}
+
+/* ── Desktop: side panel ── */
@media (min-width: 768px) {
- .editor-sheet[data-state="collapsed"] .editor-sheet-dirty span:not(.editor-sheet-dirty-dot) {
- /* Visually hidden but accessible to screen readers */
- position: absolute;
- width: 1px;
- height: 1px;
- padding: 0;
- margin: -1px;
- overflow: hidden;
- clip: rect(0, 0, 0, 0);
- white-space: nowrap;
- border: 0;
+ .editor-panel[data-state="open"],
+ .editor-panel.closing {
+ top: 0;
+ right: 0;
+ bottom: 0;
+ width: 420px;
+ max-width: 90vw;
+ border-radius: var(--t-radius-lg, 12px) 0 0 var(--t-radius-lg, 12px);
+ border: 1px solid var(--t-border-default);
+ border-right: none;
}
- .editor-sheet[data-state="collapsed"] .editor-sheet-dirty {
- position: relative;
+ .editor-panel[data-state="open"] {
+ animation: editor-panel-slide-in 0.3s cubic-bezier(0.32, 0.72, 0, 1) both;
+ }
+
+ .editor-panel.closing {
+ animation: editor-panel-slide-out 0.25s cubic-bezier(0.32, 0.72, 0, 1) both;
}
}
-/* ── Sheet header (when expanded) ── */
-.editor-sheet-header {
+@keyframes editor-panel-slide-in {
+ from { opacity: 0; transform: translateX(24px); }
+ to { opacity: 1; transform: translateX(0); }
+}
+
+@keyframes editor-panel-slide-out {
+ from { opacity: 1; transform: translateX(0); }
+ to { opacity: 0; transform: translateX(24px); }
+}
+
+/* Disable animations for users who prefer reduced motion */
+@media (prefers-reduced-motion: reduce) {
+ .editor-panel[data-state="open"],
+ .editor-panel.closing {
+ animation: none;
+ }
+}
+
+/* ── Panel header ── */
+.editor-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
@@ -2543,28 +2525,45 @@
gap: 0.5rem;
}
-.editor-sheet-header-left {
+.editor-panel-header-left {
display: flex;
align-items: center;
gap: 0.75rem;
min-width: 0;
}
-.editor-sheet-title {
+.editor-panel-title {
font-size: 0.875rem;
font-weight: 600;
color: var(--t-text-primary);
}
-.editor-sheet-header-actions {
+.editor-panel-header-actions {
display: flex;
align-items: center;
gap: 0.25rem;
flex-shrink: 0;
}
-/* ── Sheet content ── */
-.editor-sheet-content {
+/* ── Dirty indicator ── */
+.editor-panel-dirty {
+ display: flex;
+ align-items: center;
+ gap: 0.25rem;
+ color: var(--t-status-warning, oklch(0.75 0.18 85));
+ font-size: 0.75rem;
+ font-weight: 500;
+}
+
+.editor-panel-dirty-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 9999px;
+ background: currentColor;
+}
+
+/* ── Panel content ── */
+.editor-panel-content {
flex: 1;
overflow-y: auto;
overscroll-behavior: contain;
@@ -2575,18 +2574,8 @@
}
}
-/* ── Hide content when collapsed ── */
-.editor-sheet[data-state="collapsed"] .editor-sheet-content {
- display: none;
-}
-
-/* Collapsed header doesn't need bottom border */
-.editor-sheet[data-state="collapsed"] .editor-sheet-header {
- border-bottom: none;
-}
-
-/* ── Page header inside sheet ── */
-.editor-sheet-page-header {
+/* ── Page header inside panel ── */
+.editor-panel-page-header {
display: flex;
align-items: center;
justify-content: space-between;
@@ -2598,7 +2587,7 @@
}
}
-.editor-sheet-page-title {
+.editor-panel-page-title {
font-size: 0.9375rem;
font-weight: 600;
flex: 1;
@@ -2612,26 +2601,26 @@
}
}
-.editor-sheet-undo-redo {
+.editor-panel-undo-redo {
display: flex;
align-items: center;
gap: 0.25rem;
flex-shrink: 0;
}
-/* Picker inside the editor sheet — grid scrolls within a capped height */
-.editor-sheet .block-picker-overlay {
+/* Picker inside the editor panel — grid scrolls within a capped height */
+.editor-panel .block-picker-overlay {
position: static;
background: none;
}
-.editor-sheet .block-picker {
+.editor-panel .block-picker {
border-radius: 0;
max-height: none;
padding: 0;
}
-.editor-sheet .block-picker-grid {
+.editor-panel .block-picker-grid {
max-height: 45dvh;
overflow-y: auto;
}
diff --git a/assets/js/app.js b/assets/js/app.js
index 018a03f..e964183 100644
--- a/assets/js/app.js
+++ b/assets/js/app.js
@@ -679,28 +679,25 @@ const DirtyGuard = {
}
}
-// EditorSheet: simple open/collapse sheet for page editing
-// Positioning is handled by CSS - JS just handles click-outside and Escape
+// EditorSheet: handles click-outside and Escape for the editor panel
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")
- }
+ 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)
- // Escape key to collapse
this._onKeydown = (e) => {
- if (e.key === "Escape" && this._getState() !== "collapsed") {
+ if (e.key === "Escape" && this._getState() === "open") {
e.preventDefault()
- this._setState("collapsed")
+ this._close()
}
}
document.addEventListener("keydown", this._onKeydown)
-
},
destroyed() {
@@ -712,11 +709,22 @@ const EditorSheet = {
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")
+ _close() {
+ const prefersReducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
+ if (prefersReducedMotion) {
+ this.el.setAttribute("aria-hidden", "true")
+ this.pushEvent("editor_set_sheet_state", { state: "collapsed" })
+ this._announce("Editor collapsed")
+ return
+ }
+
+ this.el.classList.add("closing")
+ this.el.addEventListener("animationend", () => {
+ this.el.classList.remove("closing")
+ this.el.setAttribute("aria-hidden", "true")
+ this.pushEvent("editor_set_sheet_state", { state: "collapsed" })
+ this._announce("Editor collapsed")
+ }, { once: true })
},
_announce(message) {
diff --git a/lib/berrypod_web/components/shop_components/layout.ex b/lib/berrypod_web/components/shop_components/layout.ex
index 4734a7d..b0646ea 100644
--- a/lib/berrypod_web/components/shop_components/layout.ex
+++ b/lib/berrypod_web/components/shop_components/layout.ex
@@ -1066,77 +1066,61 @@ defmodule BerrypodWeb.ShopComponents.Layout do
def editor_sheet(assigns) do
~H"""
+ <%!-- Floating action button: always visible when panel is closed --%>
+
+
+ <%!-- Editor panel: slides in/out --%>