diff --git a/assets/css/admin/components.css b/assets/css/admin/components.css index 27f1e3d..ea4636b 100644 --- a/assets/css/admin/components.css +++ b/assets/css/admin/components.css @@ -631,33 +631,23 @@ /* ── Feedback ── */ -.admin-toast { - position: fixed; - top: 1rem; - inset-inline-end: 1rem; - z-index: 100; +.admin-banner { + /* document flow — pushes content down, no overlay */ } +/* Hide connection error banners during intentional navigation */ +.navigating-away #flash-group { display: none; } + .admin-alert { display: flex; align-items: flex-start; gap: 0.75rem; padding: 0.75rem 1rem; - border-radius: 0.5rem; font-size: 0.875rem; line-height: 1.5; - width: 20rem; - max-width: 20rem; overflow-wrap: break-word; } -@media (min-width: 640px) { - .admin-alert { - width: 24rem; - max-width: 24rem; - } -} - .admin-alert-title { font-weight: 600; } @@ -669,13 +659,13 @@ .admin-alert-info { background-color: color-mix(in oklch, var(--t-status-info) 12%, var(--t-surface-base)); color: var(--t-status-info); - border: 1px solid color-mix(in oklch, var(--t-status-info) 25%, var(--t-surface-base)); + border-bottom: 1px solid color-mix(in oklch, var(--t-status-info) 25%, var(--t-surface-base)); } .admin-alert-error { background-color: color-mix(in oklch, var(--t-status-error) 12%, var(--t-surface-base)); color: var(--t-status-error); - border: 1px solid color-mix(in oklch, var(--t-status-error) 25%, var(--t-surface-base)); + border-bottom: 1px solid color-mix(in oklch, var(--t-status-error) 25%, var(--t-surface-base)); } .admin-alert-close { @@ -686,6 +676,27 @@ &:hover { opacity: 0.7; } } +.admin-inline-feedback { + display: inline-flex; + align-items: center; + gap: 0.375rem; + font-size: 0.875rem; + line-height: 1.5; +} + +.admin-inline-feedback-saving { + color: var(--admin-text-soft); +} + +.admin-inline-feedback-saved { + color: var(--t-status-success, oklch(0.55 0.15 145)); +} + +.admin-inline-feedback-error { + color: var(--t-status-error); + font-weight: 600; +} + .admin-banner-warning { display: flex; align-items: center; @@ -2020,8 +2031,12 @@ .block-list { display: flex; flex-direction: column; - gap: 0.5rem; - margin-top: 1.5rem; + gap: 0.375rem; + margin-top: 0; + + @media (min-width: 768px) { + gap: 0.5rem; + } } .block-list-empty { @@ -2034,35 +2049,50 @@ } .block-card { - padding: 0.5rem 0.75rem; + padding: 0.375rem 0.5rem; border: 1px solid var(--t-border-default); - border-radius: 0.5rem; + border-radius: 0.375rem; background: var(--t-surface-base); transition: box-shadow 150ms; &:focus-within { box-shadow: 0 0 0 2px color-mix(in oklch, var(--t-text-primary) 20%, transparent); } + + @media (min-width: 768px) { + padding: 0.5rem 0.75rem; + border-radius: 0.5rem; + } } .block-card-position { display: flex; align-items: center; justify-content: center; - width: 1.5rem; - height: 1.5rem; + width: 1.25rem; + height: 1.25rem; border-radius: 0.25rem; - font-size: 0.75rem; + font-size: 0.6875rem; font-weight: 600; color: var(--admin-text-faint); flex-shrink: 0; + + @media (min-width: 768px) { + width: 1.5rem; + height: 1.5rem; + font-size: 0.75rem; + } } .block-card-icon { - display: flex; + display: none; align-items: center; color: var(--admin-text-muted); flex-shrink: 0; + + @media (min-width: 768px) { + display: flex; + } } .block-card-info { @@ -2072,20 +2102,28 @@ .block-card-name { display: block; - font-size: 0.875rem; + font-size: 0.8125rem; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + + @media (min-width: 768px) { + font-size: 0.875rem; + } } .block-card-preview { - display: block; + display: none; font-size: 0.75rem; color: var(--admin-text-faint); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + + @media (min-width: 768px) { + display: block; + } } .block-card-controls { @@ -2233,6 +2271,11 @@ gap: 0.25rem 0.5rem; flex-wrap: wrap; width: 100%; + + /* Desktop: single row with truncating text */ + @media (min-width: 768px) { + flex-wrap: nowrap; + } } .block-card-expanded { @@ -2334,115 +2377,275 @@ border-style: dashed; } -/* ── Live editor layout (sidebar on shop pages) ── */ +/* ══════════════════════════════════════════════════════════════════════════ + Editor sheet (unified bottom/right sheet for page editing) + ══════════════════════════════════════════════════════════════════════════ */ -.page-editor-live { - display: flex; - min-height: 100vh; -} - -.page-editor-sidebar { +.editor-sheet { position: fixed; - top: 0; - left: 0; - bottom: 0; - width: 360px; + z-index: 1000; background: var(--t-surface-base); - border-right: 1px solid var(--t-border-default); - overflow-y: auto; - z-index: 40; - box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1); - padding: 1rem; - transition: transform 0.25s ease; - transform: translateX(0); + 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; } -/* Hidden sidebar — slides off-screen */ -[data-sidebar-open="false"] .page-editor-sidebar { - transform: translateX(-100%); - box-shadow: none; + +@media (prefers-reduced-motion: reduce) { + .editor-sheet { + transition: none; + } } -.page-editor-sidebar-header { +/* ── Mobile: bottom-anchored ── */ +@media (max-width: 767px) { + .editor-sheet { + bottom: 0; + left: 0; + right: 0; + height: 85dvh; + border-radius: var(--t-radius-lg, 12px) var(--t-radius-lg, 12px) 0 0; + transform: translateY(calc(100% - 48px)); + } + + .editor-sheet[data-state="open"] { + transform: translateY(0); + } +} + +/* ── Desktop: right-anchored ── */ +@media (min-width: 768px) { + .editor-sheet { + top: 0; + right: 0; + bottom: 0; + border-radius: var(--t-radius-lg, 12px) 0 0 var(--t-radius-lg, 12px); + width: 420px; + max-width: 90vw; + transform: translateX(calc(100% - 48px)); + flex-direction: column; + } + + .editor-sheet[data-state="open"] { + transform: translateX(0); + } +} + +/* ── Edit button in collapsed state ── */ +.editor-sheet-edit-btn { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.375rem 0.75rem; + background: var(--t-accent); + color: #fff; + border: none; + border-radius: 6px; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: filter 0.15s; +} + +.editor-sheet-edit-btn:hover { + filter: brightness(1.1); +} + +.editor-sheet-edit-btn svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +/* Desktop collapsed: icon-only button with tooltip */ +@media (min-width: 768px) { + .editor-sheet[data-state="collapsed"] .editor-sheet-edit-btn { + padding: 0.5rem; + position: relative; + } + + .editor-sheet[data-state="collapsed"] .editor-sheet-edit-btn span { + /* 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; + } + + /* Tooltip on hover - dark tooltip for visibility on any theme */ + .editor-sheet[data-state="collapsed"] .editor-sheet-edit-btn::after { + content: "Edit page"; + position: absolute; + right: calc(100% + 8px); + top: 50%; + transform: translateY(-50%); + padding: 0.375rem 0.5rem; + background: #1a1a1a; + color: #fff; + font-size: 0.75rem; + font-weight: 500; + border-radius: 4px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: opacity 0.15s; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 10; + } + + .editor-sheet[data-state="collapsed"] .editor-sheet-edit-btn:hover::after, + .editor-sheet[data-state="collapsed"] .editor-sheet-edit-btn:focus::after { + opacity: 1; + } +} + +/* ── 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 { + width: 8px; + height: 8px; + border-radius: 9999px; + background: currentColor; +} + +/* Desktop collapsed: hide "Unsaved" text, show only dot */ +@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-sheet[data-state="collapsed"] .editor-sheet-dirty { + position: relative; + } +} + +/* ── Sheet header (when expanded) ── */ +.editor-sheet-header { display: flex; align-items: center; justify-content: space-between; + padding: 0.75rem 1rem; + border-bottom: 1px solid var(--t-border-default); + flex-shrink: 0; gap: 0.5rem; - margin-bottom: 0.75rem; } -.page-editor-sidebar-title { - font-size: 1rem; - font-weight: 600; - flex: 1; +.editor-sheet-header-left { + display: flex; + align-items: center; + gap: 0.75rem; min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } -.page-editor-sidebar-actions { +.editor-sheet-title { + font-size: 0.875rem; + font-weight: 600; + color: var(--t-text-primary); +} + +.editor-sheet-header-actions { display: flex; align-items: center; gap: 0.25rem; flex-shrink: 0; } -.page-editor-sidebar-dirty { - margin-bottom: 0.5rem; +/* ── Sheet content ── */ +.editor-sheet-content { + flex: 1; + overflow-y: auto; + overscroll-behavior: contain; + padding: 0.75rem; + + @media (min-width: 768px) { + padding: 1rem; + } } -/* Picker inside the editor sidebar — grid scrolls within a capped height */ -.page-editor-sidebar .block-picker-overlay { +/* ── 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 { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.5rem; + margin-bottom: 0.5rem; + + @media (min-width: 768px) { + margin-bottom: 0.75rem; + } +} + +.editor-sheet-page-title { + font-size: 0.9375rem; + font-weight: 600; + flex: 1; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + + @media (min-width: 768px) { + font-size: 1rem; + } +} + +.editor-sheet-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 { position: static; background: none; } -.page-editor-sidebar .block-picker { +.editor-sheet .block-picker { border-radius: 0; max-height: none; padding: 0; } -.page-editor-sidebar .block-picker-grid { +.editor-sheet .block-picker-grid { max-height: 45dvh; overflow-y: auto; } -.page-editor-content { - flex: 1; - margin-left: 360px; - min-width: 0; - transition: margin-left 0.25s ease; -} - -/* Content goes full-width when sidebar is hidden */ -[data-sidebar-open="false"] .page-editor-content { - margin-left: 0; -} - -/* Clickable backdrop to dismiss the sidebar */ -.page-editor-backdrop { - position: fixed; - inset: 0; - z-index: 39; - background: rgba(0, 0, 0, 0.15); - cursor: pointer; -} - -/* Mobile: sidebar overlays content, no margin push */ -@media (max-width: 63.99em) { - .page-editor-sidebar { - width: 85%; - max-width: 360px; - padding-bottom: calc(4rem + env(safe-area-inset-bottom, 0px) + 1rem); - } - - .page-editor-content { - margin-left: 0; - } -} - /* ═══════════════════════════════════════════════════════════════════ Image field (block editor) ═══════════════════════════════════════════════════════════════════ */ diff --git a/assets/css/shop/components.css b/assets/css/shop/components.css index 85cc4a3..3b75fea 100644 --- a/assets/css/shop/components.css +++ b/assets/css/shop/components.css @@ -1089,6 +1089,7 @@ align-items: center; position: relative; z-index: 1; + margin-left: auto; } .header-icon-btn { @@ -1145,66 +1146,161 @@ } } - /* ── Mobile bottom nav ── */ + /* ── Hamburger menu button (mobile only) ── */ - .mobile-bottom-nav { - position: fixed; - bottom: 0; - inset-inline: 0; - z-index: 100; - background-color: var(--t-surface-raised); - border-top: 1px solid var(--t-border-default); - box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); - padding-bottom: env(safe-area-inset-bottom, 0px); + .header-hamburger { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + background: none; + border: none; + cursor: pointer; + color: var(--t-text-secondary); + border-radius: var(--t-radius-button); + margin-right: 0.5rem; + flex-shrink: 0; - & ul { - display: flex; - justify-content: space-around; - align-items: center; - height: 4rem; - margin: 0; - padding: 0; - list-style: none; + &:hover { + background: var(--t-surface-sunken); } - & li { - flex: 1; + & svg { + width: 1.5rem; + height: 1.5rem; + } + + @media (min-width: 768px) { + display: none; } } - .mobile-nav-link { + /* ── Mobile nav drawer ── */ + + .mobile-nav-drawer { + position: fixed; + inset: 0; + z-index: 1000; + pointer-events: none; + visibility: hidden; + } + + .mobile-nav-drawer.is-open { + pointer-events: auto; + visibility: visible; + } + + .mobile-nav-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.4); + opacity: 0; + transition: opacity 0.2s ease; + } + + .mobile-nav-drawer.is-open .mobile-nav-backdrop { + opacity: 1; + } + + .mobile-nav-panel { + position: absolute; + top: 0; + left: 0; + bottom: 0; + width: 280px; + max-width: 85vw; + background: var(--t-surface-base); + transform: translateX(-100%); + transition: transform 0.25s ease; display: flex; flex-direction: column; + overflow-y: auto; + } + + .mobile-nav-drawer.is-open .mobile-nav-panel { + transform: translateX(0); + } + + .mobile-nav-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1rem; + border-bottom: 1px solid var(--t-border-default); + } + + .mobile-nav-title { + font-family: var(--t-font-heading); + font-size: var(--t-text-lg); + font-weight: 600; + color: var(--t-text-primary); + } + + .mobile-nav-close { + display: flex; align-items: center; justify-content: center; - gap: 0.25rem; - padding-block: 0.5rem; - margin-inline: 0.25rem; - min-height: 56px; - border-radius: var(--t-radius-card, 0.5rem); - font-size: var(--t-text-caption); + width: 2.5rem; + height: 2.5rem; + background: none; + border: none; + cursor: pointer; color: var(--t-text-secondary); - text-decoration: none; - font-weight: 500; - background-color: transparent; + border-radius: var(--t-radius-button); - & svg { - width: 1.25rem; - height: 1.25rem; + &:hover { + background: var(--t-surface-sunken); + } + } + + .mobile-nav-links { + list-style: none; + margin: 0; + padding: 0.5rem 0; + } + + .mobile-nav-drawer .mobile-nav-link { + display: block; + padding: 0.875rem 1.25rem; + color: var(--t-text-primary); + text-decoration: none; + font-size: var(--t-text-base); + font-weight: 500; + transition: background 0.15s ease; + + &:hover { + background: var(--t-surface-sunken); } &[aria-current="page"] { - color: color-mix(in oklch, var(--t-accent) 80%, black); - font-weight: 600; - background-color: color-mix(in oklch, var(--t-accent) 10%, transparent); - - & svg { - width: 1.5rem; - height: 1.5rem; - } + color: var(--t-accent); + background: color-mix(in oklch, var(--t-accent) 8%, transparent); } } + .mobile-nav-section { + padding-top: 0.5rem; + border-top: 1px solid var(--t-border-default); + margin-top: 0.5rem; + } + + .mobile-nav-section-title { + display: block; + padding: 0.5rem 1.25rem; + font-size: var(--t-text-sm); + font-weight: 600; + color: var(--t-text-secondary); + text-transform: uppercase; + letter-spacing: 0.05em; + } + + /* ── Mobile bottom nav (REMOVED - replaced by hamburger drawer) ── */ + + .mobile-bottom-nav { + display: none; + } + /* ── Search modal ── */ .search-modal { @@ -2356,13 +2452,7 @@ /* ── Flash messages ── */ .shop-flash-group { - position: fixed; - top: 1rem; - inset-inline-end: 1rem; - z-index: 200; - display: flex; - flex-direction: column; - gap: 0.5rem; + /* document flow — pushes content down, no overlay */ } .shop-flash { @@ -2370,10 +2460,6 @@ align-items: center; gap: 0.75rem; padding: 0.75rem 1rem; - border-radius: var(--t-radius-card); - box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); - max-width: 24rem; - animation: flash-in 0.3s ease-out; background-color: var(--t-surface-raised, #fff); color: var(--t-text-primary); @@ -2384,11 +2470,11 @@ } .shop-flash--info { - border: 1px solid var(--t-border-default); + border-bottom: 1px solid var(--t-border-default); } .shop-flash--error { - border: 1px solid hsl(0 70% 50% / 0.3); + border-bottom: 1px solid hsl(0 70% 50% / 0.3); } .shop-flash-icon { @@ -2405,11 +2491,6 @@ color: hsl(0 70% 50%); } - @keyframes flash-in { - from { opacity: 0; transform: translateX(1rem); } - to { opacity: 1; transform: translateX(0); } - } - /* Transition classes for JS.hide flash dismiss */ .fade-out { transition: opacity 200ms ease-out; } .fade-out-from { opacity: 1; } diff --git a/assets/js/app.js b/assets/js/app.js index e30bb93..7bf75d0 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -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 diff --git a/lib/berrypod/pages/defaults.ex b/lib/berrypod/pages/defaults.ex index eb35fd2..3d21762 100644 --- a/lib/berrypod/pages/defaults.ex +++ b/lib/berrypod/pages/defaults.ex @@ -16,6 +16,20 @@ defmodule Berrypod.Pages.Defaults do |> Enum.map(&for_slug/1) end + @doc "Returns true if the given blocks match the defaults for the slug." + def matches_defaults?(slug, current_blocks) when is_list(current_blocks) do + default_blocks = blocks(slug) + + length(current_blocks) == length(default_blocks) and + Enum.zip(current_blocks, default_blocks) + |> Enum.all?(fn {current, default} -> + current["type"] == default["type"] and + current["settings"] == default["settings"] + end) + end + + def matches_defaults?(_slug, _blocks), do: false + # ── Page titles ───────────────────────────────────────────────── defp title("home"), do: "Home page" diff --git a/lib/berrypod_web/components/core_components.ex b/lib/berrypod_web/components/core_components.ex index ea24d01..69513b8 100644 --- a/lib/berrypod_web/components/core_components.ex +++ b/lib/berrypod_web/components/core_components.ex @@ -47,7 +47,7 @@ defmodule BerrypodWeb.CoreComponents do id={@id} phx-click={JS.push("lv:clear-flash", value: %{key: @kind}) |> hide("##{@id}")} role="alert" - class="admin-toast" + class="admin-banner" {@rest} >
+ <.inline_feedback status={@save_status} message={@save_error} /> + """ + attr :status, :atom, values: [:idle, :saving, :saved, :error], default: :idle + attr :message, :string, default: nil + + def inline_feedback(assigns) do + ~H""" + + <.icon :if={@status == :saving} name="hero-arrow-path" class="size-4 motion-safe:animate-spin" /> + <.icon :if={@status == :saved} name="hero-check" class="size-4" /> + <.icon :if={@status == :error} name="hero-exclamation-circle" class="size-4" /> + {feedback_text(@status, @message)} + + """ + end + + defp feedback_text(:saving, _), do: "Saving..." + defp feedback_text(:saved, nil), do: "Saved" + defp feedback_text(:saved, msg), do: msg + defp feedback_text(:error, nil), do: "Something went wrong" + defp feedback_text(:error, msg), do: msg + defp feedback_text(:idle, _), do: nil + @doc """ Renders a button with navigation support. diff --git a/lib/berrypod_web/components/layouts.ex b/lib/berrypod_web/components/layouts.ex index 21b422c..01f5a27 100644 --- a/lib/berrypod_web/components/layouts.ex +++ b/lib/berrypod_web/components/layouts.ex @@ -35,13 +35,13 @@ defmodule BerrypodWeb.Layouts do def app(assigns) do ~H""" + <.flash_group flash={@flash} /> +
{render_slot(@inner_block)}
- - <.flash_group flash={@flash} /> """ end diff --git a/lib/berrypod_web/components/layouts/admin.html.heex b/lib/berrypod_web/components/layouts/admin.html.heex index 80a1fa8..2f2f266 100644 --- a/lib/berrypod_web/components/layouts/admin.html.heex +++ b/lib/berrypod_web/components/layouts/admin.html.heex @@ -18,9 +18,10 @@ + <.flash_group flash={@flash} /> + <%!-- page content --%>
- <.flash_group flash={@flash} />
{@inner_content}
diff --git a/lib/berrypod_web/components/shop_components/layout.ex b/lib/berrypod_web/components/shop_components/layout.ex index 1450b9a..4734a7d 100644 --- a/lib/berrypod_web/components/shop_components/layout.ex +++ b/lib/berrypod_web/components/shop_components/layout.ex @@ -128,9 +128,6 @@ defmodule BerrypodWeb.ShopComponents.Layout do mode={@mode} cart_count={@cart_count} is_admin={@is_admin} - editing={@editing} - editor_current_path={@editor_current_path} - editor_sidebar_open={@editor_sidebar_open} header_nav_items={@header_nav_items} /> @@ -167,11 +164,12 @@ defmodule BerrypodWeb.ShopComponents.Layout do search_open={@search_open} /> - <.mobile_bottom_nav + <.mobile_nav_drawer :if={!@error_page} active_page={@active_page} mode={@mode} items={@header_nav_items} + categories={assigns[:categories] || []} />
""" @@ -513,6 +511,111 @@ defmodule BerrypodWeb.ShopComponents.Layout do """ end + @doc """ + Renders the mobile navigation drawer. + + A slide-out drawer containing the main navigation links for mobile users. + Triggered by the hamburger menu button in the header. + """ + attr :active_page, :string, required: true + attr :mode, :atom, default: :live + attr :items, :list, default: [] + attr :categories, :list, default: [] + + def mobile_nav_drawer(assigns) do + ~H""" +
+
+
+ +
+ """ + end + @doc """ Renders the shop footer with newsletter signup and links. @@ -662,9 +765,6 @@ defmodule BerrypodWeb.ShopComponents.Layout do attr :mode, :atom, default: :live attr :cart_count, :integer, default: 0 attr :is_admin, :boolean, default: false - attr :editing, :boolean, default: false - attr :editor_current_path, :string, default: nil - attr :editor_sidebar_open, :boolean, default: true attr :header_nav_items, :list, default: [] def shop_header(assigns) do @@ -674,6 +774,27 @@ defmodule BerrypodWeb.ShopComponents.Layout do
<% end %> + <%!-- Hamburger menu button (mobile only) --%> + +