diff --git a/assets/css/admin/components.css b/assets/css/admin/components.css index e6cf94b..21419a9 100644 --- a/assets/css/admin/components.css +++ b/assets/css/admin/components.css @@ -1062,4 +1062,260 @@ border-top: 1px solid var(--t-surface-sunken, #e5e5e5); } +/* ── Page editor ── */ + +.page-list { + display: flex; + flex-direction: column; + gap: 1.5rem; + margin-top: 1.5rem; +} + +.page-group-title { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: color-mix(in oklch, var(--t-text-primary) 50%, transparent); + margin-bottom: 0.5rem; +} + +.page-group-cards { + display: flex; + flex-direction: column; + gap: 1px; + border: 1px solid var(--t-border-default); + border-radius: 0.5rem; + overflow: hidden; +} + +.page-card { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + background: var(--t-surface-base); + text-decoration: none; + color: var(--t-text-primary); + transition: background 100ms; + + &:not(:last-child) { + border-bottom: 1px solid var(--t-border-default); + } + + @media (hover: hover) { + &:hover { + background: var(--t-surface-sunken); + } + } +} + +.page-card-icon { + display: flex; + align-items: center; + justify-content: center; + width: 2rem; + height: 2rem; + border-radius: 0.375rem; + background: var(--t-surface-sunken); + flex-shrink: 0; + color: color-mix(in oklch, var(--t-text-primary) 60%, transparent); +} + +.page-card-info { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; +} + +.page-card-title { + font-size: 0.875rem; + font-weight: 500; +} + +.page-card-meta { + font-size: 0.75rem; + color: color-mix(in oklch, var(--t-text-primary) 50%, transparent); +} + +.page-card-arrow { + flex-shrink: 0; + color: color-mix(in oklch, var(--t-text-primary) 30%, transparent); +} + +/* Block list in editor */ + +.block-list { + display: flex; + flex-direction: column; + gap: 0.5rem; + margin-top: 1.5rem; +} + +.block-list-empty { + text-align: center; + padding: 2rem; + color: color-mix(in oklch, var(--t-text-primary) 50%, transparent); + font-size: 0.875rem; + border: 1px dashed var(--t-border-default); + border-radius: 0.5rem; +} + +.block-card { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 0.75rem; + border: 1px solid var(--t-border-default); + border-radius: 0.5rem; + 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); + } +} + +.block-card-position { + display: flex; + align-items: center; + justify-content: center; + width: 1.5rem; + height: 1.5rem; + border-radius: 0.25rem; + font-size: 0.75rem; + font-weight: 600; + color: color-mix(in oklch, var(--t-text-primary) 50%, transparent); + flex-shrink: 0; +} + +.block-card-icon { + display: flex; + align-items: center; + color: color-mix(in oklch, var(--t-text-primary) 60%, transparent); + flex-shrink: 0; +} + +.block-card-name { + flex: 1; + font-size: 0.875rem; + font-weight: 500; + min-width: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.block-card-controls { + display: flex; + align-items: center; + gap: 0.125rem; + flex-shrink: 0; +} + +.block-remove-btn { + color: color-mix(in oklch, var(--t-status-error) 70%, transparent); + + @media (hover: hover) { + &:hover { + color: var(--t-status-error); + } + } +} + +/* Add block button */ + +.block-actions { + margin-top: 1rem; + display: flex; + justify-content: center; +} + +.block-add-btn { + border-style: dashed; +} + +/* Block picker */ + +.block-picker-overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.3); + z-index: 50; + display: flex; + align-items: flex-end; + justify-content: center; + + @media (min-width: 40em) { + align-items: center; + } +} + +.block-picker { + background: var(--t-surface-base); + border-radius: 0.75rem 0.75rem 0 0; + width: 100%; + max-height: 80vh; + overflow-y: auto; + padding: 1rem; + + @media (min-width: 40em) { + border-radius: 0.75rem; + max-width: 28rem; + } +} + +.block-picker-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.75rem; + + & h3 { + font-size: 1rem; + font-weight: 600; + } +} + +.block-picker-search { + width: 100%; + margin-bottom: 0.75rem; +} + +.block-picker-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 0.5rem; +} + +.block-picker-item { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 0.75rem; + border: 1px solid var(--t-border-default); + border-radius: 0.375rem; + background: none; + font-size: 0.8125rem; + cursor: pointer; + text-align: left; + color: inherit; + transition: background 100ms; + + @media (hover: hover) { + &:hover { + background: var(--t-surface-sunken); + } + } +} + +.block-picker-empty { + grid-column: 1 / -1; + text-align: center; + padding: 1rem; + color: color-mix(in oklch, var(--t-text-primary) 50%, transparent); + font-size: 0.8125rem; +} + } /* @layer admin */ diff --git a/assets/css/admin/icons.css b/assets/css/admin/icons.css index 9651e17..fba621f 100644 --- a/assets/css/admin/icons.css +++ b/assets/css/admin/icons.css @@ -74,6 +74,18 @@ height: 1.25rem; } +.hero-arrow-uturn-left { + --hero-arrow-uturn-left: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-arrow-uturn-left); + mask: var(--hero-arrow-uturn-left); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-arrow-uturn-left-mini { --hero-arrow-uturn-left-mini: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-arrow-uturn-left-mini); @@ -86,6 +98,18 @@ height: 1.25rem; } +.hero-arrow-uturn-right { + --hero-arrow-uturn-right: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-arrow-uturn-right); + mask: var(--hero-arrow-uturn-right); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-banknotes { --hero-banknotes: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-banknotes); @@ -122,6 +146,18 @@ height: 1.5rem; } +.hero-calculator { + --hero-calculator: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-calculator); + mask: var(--hero-calculator); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-chart-bar { --hero-chart-bar: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-chart-bar); @@ -134,6 +170,18 @@ height: 1.5rem; } +.hero-chat-bubble-left-right { + --hero-chat-bubble-left-right: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-chat-bubble-left-right); + mask: var(--hero-chat-bubble-left-right); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-check-badge { --hero-check-badge: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-check-badge); @@ -194,6 +242,18 @@ height: 1.25rem; } +.hero-chevron-right { + --hero-chevron-right: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-chevron-right); + mask: var(--hero-chevron-right); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-chevron-right-mini { --hero-chevron-right-mini: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-chevron-right-mini); @@ -218,6 +278,30 @@ height: 1.25rem; } +.hero-clipboard-document { + --hero-clipboard-document: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-clipboard-document); + mask: var(--hero-clipboard-document); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + +.hero-clipboard-document-list { + --hero-clipboard-document-list: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-clipboard-document-list); + mask: var(--hero-clipboard-document-list); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-clock { --hero-clock: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-clock); @@ -290,6 +374,54 @@ height: 1.5rem; } +.hero-document { + --hero-document: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-document); + mask: var(--hero-document); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + +.hero-document-duplicate-mini { + --hero-document-duplicate-mini: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-document-duplicate-mini); + mask: var(--hero-document-duplicate-mini); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.25rem; + height: 1.25rem; +} + +.hero-document-text { + --hero-document-text: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-document-text); + mask: var(--hero-document-text); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + +.hero-envelope { + --hero-envelope: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-envelope); + mask: var(--hero-envelope); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-exclamation-circle { --hero-exclamation-circle: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-exclamation-circle); @@ -374,6 +506,18 @@ height: 1.25rem; } +.hero-funnel { + --hero-funnel: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-funnel); + mask: var(--hero-funnel); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-home { --hero-home: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-home); @@ -410,6 +554,54 @@ height: 1.5rem; } +.hero-link { + --hero-link: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-link); + mask: var(--hero-link); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + +.hero-lock-closed { + --hero-lock-closed: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-lock-closed); + mask: var(--hero-lock-closed); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + +.hero-magnifying-glass { + --hero-magnifying-glass: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-magnifying-glass); + mask: var(--hero-magnifying-glass); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + +.hero-megaphone { + --hero-megaphone: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-megaphone); + mask: var(--hero-megaphone); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-minus-circle-mini { --hero-minus-circle-mini: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-minus-circle-mini); @@ -470,6 +662,18 @@ height: 1.25rem; } +.hero-paper-airplane { + --hero-paper-airplane: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-paper-airplane); + mask: var(--hero-paper-airplane); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-paper-airplane-mini { --hero-paper-airplane-mini: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-paper-airplane-mini); @@ -518,6 +722,18 @@ height: 1.25rem; } +.hero-puzzle-piece { + --hero-puzzle-piece: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-puzzle-piece); + mask: var(--hero-puzzle-piece); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-question-mark-circle-mini { --hero-question-mark-circle-mini: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-question-mark-circle-mini); @@ -530,10 +746,34 @@ height: 1.25rem; } -.hero-rocket-launch { - --hero-rocket-launch: url('data:image/svg+xml;utf8,'); - -webkit-mask: var(--hero-rocket-launch); - mask: var(--hero-rocket-launch); +.hero-rocket-launch-mini { + --hero-rocket-launch-mini: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-rocket-launch-mini); + mask: var(--hero-rocket-launch-mini); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.25rem; + height: 1.25rem; +} + +.hero-share { + --hero-share: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-share); + mask: var(--hero-share); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + +.hero-shield-check { + --hero-shield-check: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-shield-check); + mask: var(--hero-shield-check); mask-repeat: no-repeat; background-color: currentColor; vertical-align: middle; @@ -554,6 +794,18 @@ height: 1.5rem; } +.hero-shopping-cart { + --hero-shopping-cart: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-shopping-cart); + mask: var(--hero-shopping-cart); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-signal { --hero-signal: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-signal); @@ -566,6 +818,42 @@ height: 1.5rem; } +.hero-squares-2x2 { + --hero-squares-2x2: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-squares-2x2); + mask: var(--hero-squares-2x2); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + +.hero-squares-plus { + --hero-squares-plus: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-squares-plus); + mask: var(--hero-squares-plus); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + +.hero-star { + --hero-star: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-star); + mask: var(--hero-star); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-sun-micro { --hero-sun-micro: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-sun-micro); @@ -578,6 +866,42 @@ height: 1rem; } +.hero-tag { + --hero-tag: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-tag); + mask: var(--hero-tag); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + +.hero-trash-mini { + --hero-trash-mini: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-trash-mini); + mask: var(--hero-trash-mini); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.25rem; + height: 1.25rem; +} + +.hero-truck { + --hero-truck: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-truck); + mask: var(--hero-truck); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-truck-mini { --hero-truck-mini: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-truck-mini); @@ -590,6 +914,30 @@ height: 1.25rem; } +.hero-user { + --hero-user: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-user); + mask: var(--hero-user); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + +.hero-users { + --hero-users: url('data:image/svg+xml;utf8,'); + -webkit-mask: var(--hero-users); + mask: var(--hero-users); + mask-repeat: no-repeat; + background-color: currentColor; + vertical-align: middle; + display: inline-block; + width: 1.5rem; + height: 1.5rem; +} + .hero-x-circle { --hero-x-circle: url('data:image/svg+xml;utf8,'); -webkit-mask: var(--hero-x-circle); diff --git a/assets/js/app.js b/assets/js/app.js index 5bb269e..1850ad6 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -634,10 +634,27 @@ const ChartTooltip = { } } +// Warns before navigating away from pages with unsaved changes +const DirtyGuard = { + mounted() { + this._beforeUnload = (e) => { + if (this.el.dataset.dirty === "true") { + e.preventDefault() + e.returnValue = "" + } + } + window.addEventListener("beforeunload", this._beforeUnload) + }, + + destroyed() { + window.removeEventListener("beforeunload", this._beforeUnload) + } +} + 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, CardRadioScroll, AnalyticsInit, AnalyticsExport, ChartTooltip}, + hooks: {...colocatedHooks, ColorSync, Lightbox, CartPersist, CartDrawer, ProductImageScroll, SearchModal, CollectionFilters, CardRadioScroll, AnalyticsInit, AnalyticsExport, ChartTooltip, DirtyGuard}, }) // Show progress bar on live navigation and form submits diff --git a/lib/berrypod_web/components/layouts/admin.html.heex b/lib/berrypod_web/components/layouts/admin.html.heex index 39bf749..15f58f9 100644 --- a/lib/berrypod_web/components/layouts/admin.html.heex +++ b/lib/berrypod_web/components/layouts/admin.html.heex @@ -94,6 +94,14 @@ <.icon name="hero-link" class="size-5" /> Providers +
+ Unsaved changes +
+ + <%!-- Block list --%> +No blocks on this page yet.
+