From 37653e5e7aa6ea79188fc22b94a016c3e3b24aba Mon Sep 17 00:00:00 2001 From: Jamey Greenwood Date: Fri, 2 Jan 2026 13:48:03 +0000 Subject: [PATCH] feat: redesign contact page for POD sellers & add theme enhancements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Contact page redesign: - Replace retail-style contact info (phone, address, hours) with POD-appropriate layout - Add order tracking card with email input - Add "Handy to know" section with printing/delivery/returns info - Add email contact with response time promise - Add social links (Instagram, Pinterest) - Update intro text to be warmer and more personal Collection page improvements: - Replace sidebar filters with horizontal category pills - Add filter pill CSS with theme token integration PDP enhancements: - Add image lightbox with keyboard navigation - Add thumbnail gallery with active state - Add reviews section (toggleable) - Add related products section (toggleable) - Add trust badges section (toggleable) Theme system additions: - Add button_style setting (filled/outline/soft) - Add product_text_align setting (left/center) - Add image_aspect_ratio setting (square/portrait/landscape) - Add responsive form layouts with flex-wrap 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- assets/css/theme-layer2-attributes.css | 342 +++++++++++++++++- assets/css/theme-semantic.css | 22 +- assets/js/app.js | 103 +++++- .../settings/theme_settings.ex | 7 +- lib/simpleshop_theme/theme/css_generator.ex | 82 +++++ .../live/theme_live/index.ex | 8 +- .../live/theme_live/index.html.heex | 234 +++++++++++- .../live/theme_live/preview_pages.ex | 35 +- .../theme_live/preview_pages/about.html.heex | 4 +- .../preview_pages/collection.html.heex | 142 ++------ .../preview_pages/contact.html.heex | 92 ++++- .../theme_live/preview_pages/error.html.heex | 21 +- .../theme_live/preview_pages/home.html.heex | 28 +- .../theme_live/preview_pages/pdp.html.heex | 294 ++++++++++++--- 14 files changed, 1178 insertions(+), 236 deletions(-) diff --git a/assets/css/theme-layer2-attributes.css b/assets/css/theme-layer2-attributes.css index 09eb997..0982ed6 100644 --- a/assets/css/theme-layer2-attributes.css +++ b/assets/css/theme-layer2-attributes.css @@ -252,6 +252,89 @@ box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -4px rgba(0, 0, 0, 0.1); } +/* ============================================= + Dynamic Theme Settings (consume CSS variables) + ============================================= */ + +/* Density - apply to product grids */ +.preview-frame .product-grid { + gap: var(--space-lg, 1.5rem); +} + +.preview-frame[data-density="spacious"] .product-grid { + gap: calc(var(--space-lg, 1.5rem) * 1.25); +} + +.preview-frame[data-density="compact"] .product-grid { + gap: calc(var(--space-lg, 1.5rem) * 0.75); +} + +/* Density also affects card padding */ +.preview-frame .product-card > div:last-child { + padding: var(--space-md, 1rem); +} + +.preview-frame[data-density="spacious"] .product-card > div:last-child { + padding: calc(var(--space-md, 1rem) * 1.25); +} + +.preview-frame[data-density="compact"] .product-card > div:last-child { + padding: calc(var(--space-md, 1rem) * 0.75); +} + +/* Product Text Alignment - targets the product info area inside cards */ +.preview-frame .product-card > div:last-child { + text-align: var(--t-product-text-align, left); +} + +/* Image Aspect Ratio - targets the image container inside product cards */ +.preview-frame .product-card .product-image-container { + aspect-ratio: var(--t-image-aspect-ratio, 1 / 1); +} + +/* Font Size Scale - applied to base font (16px is accessible minimum) */ +.preview-frame { + font-size: calc(16px * var(--t-font-size-scale, 1)); +} + +/* Heading Weight Override - takes precedence over typography preset */ +.preview-frame h1, +.preview-frame h2, +.preview-frame h3, +.preview-frame h4, +.preview-frame h5, +.preview-frame h6 { + font-weight: var(--t-heading-weight-override, var(--t-heading-weight, 600)) !important; +} + +/* Layout Max Width - applied via data attribute for better specificity */ +.preview-frame[data-layout="contained"] .max-w-7xl { + max-width: var(--t-layout-max-width, 1100px); +} + +.preview-frame[data-layout="wide"] .max-w-7xl { + max-width: var(--t-layout-max-width, 1400px); +} + +.preview-frame[data-layout="full"] .max-w-7xl { + max-width: var(--t-layout-max-width, 100%); +} + +/* Button Style - using data attribute approach */ +/* Outline button style */ +.preview-frame[data-button-style="outline"] button[style*="background-color: hsl(var(--t-accent"] { + background-color: transparent !important; + color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)) !important; + border: 2px solid hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)) !important; +} + +/* Soft button style */ +.preview-frame[data-button-style="soft"] button[style*="background-color: hsl(var(--t-accent"] { + background-color: hsl(var(--t-accent-h) var(--t-accent-s) 90%) !important; + color: hsl(var(--t-accent-h) var(--t-accent-s) 30%) !important; + border: 2px solid transparent !important; +} + /* Product Badges */ .product-badge { position: absolute; @@ -276,6 +359,11 @@ color: var(--t-text-inverse); } +.badge-sold-out { + background-color: var(--t-text-tertiary, #737373); + color: #ffffff; +} + /* Product Hover Image */ .product-image-container { position: relative; @@ -292,20 +380,46 @@ opacity: 1; } -.product-card:hover .product-image-primary { +/* Only hide primary image on hover when a hover image sibling exists */ +.product-card:hover .product-image-primary:has(+ .product-image-hover) { opacity: 0; } +/* Secondary Accent (Hover Colour) Usage */ +/* Applied to interactive elements on hover for visual feedback */ + +/* Links in body text */ +.preview-frame a:not([class*="btn"]):not([class*="button"]):not(.product-card):not(.nav-link):hover { + color: var(--t-secondary-accent, var(--t-text-primary)); +} + +/* Product card hover effect - subtle accent border */ +.preview-frame .product-card:hover { + border-color: var(--t-secondary-accent, var(--t-border-default)) !important; +} + +/* Button hover states - darken or use secondary accent */ +.preview-frame button:hover, +.preview-frame [role="button"]:hover { + filter: brightness(0.95); +} + +/* Nav links hover */ +.preview-frame .nav-link:hover, +.preview-frame nav a:hover { + color: var(--t-secondary-accent, var(--t-text-primary)); +} + /* Social Links */ .social-link:hover { background-color: var(--t-surface-sunken); - color: var(--t-text-primary); + color: var(--t-secondary-accent, var(--t-text-primary)); } /* Header Icon Buttons */ .header-icon-btn:hover { background-color: var(--t-surface-sunken); - color: var(--t-text-primary); + color: var(--t-secondary-accent, var(--t-text-primary)); } /* Search Modal Animation */ @@ -318,3 +432,225 @@ transform: translateY(0); transition: transform 0.2s ease; } + +/* Lightbox - using native dialog */ +.lightbox { + position: fixed; + inset: 0; + width: 100%; + height: 100%; + max-width: 100%; + max-height: 100%; + border: none; + padding: 0; + margin: 0; + background: transparent; + display: flex; + align-items: center; + justify-content: center; +} + +.lightbox::backdrop { + background: rgba(0, 0, 0, 0.95); +} + +.lightbox:not([open]) { + display: none; +} + +.lightbox-content { + position: relative; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.lightbox-close { + position: absolute; + top: var(--space-md, 1rem); + right: var(--space-md, 1rem); + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.05); + border: none; + border-radius: 50%; + color: white; + cursor: pointer; + transition: background 0.15s ease; + z-index: 1; +} + +.lightbox-close:hover { + background: rgba(255, 255, 255, 0.1); +} + +.lightbox-close svg { + width: 24px; + height: 24px; +} + +.lightbox-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + background: rgba(255, 255, 255, 0.1); + border: none; + border-radius: 50%; + color: white; + cursor: pointer; + transition: background 0.15s ease; +} + +.lightbox-nav:hover { + background: rgba(255, 255, 255, 0.2); +} + +.lightbox-nav svg { + width: 24px; + height: 24px; +} + +.lightbox-prev { + left: var(--space-md, 1rem); +} + +.lightbox-next { + right: var(--space-md, 1rem); +} + +.lightbox-image-container { + max-width: 90vw; + max-height: 75vh; + display: flex; + align-items: center; + justify-content: center; +} + +.lightbox-image { + max-width: 100%; + max-height: 75vh; + object-fit: contain; + border-radius: var(--t-radius-image, 8px); +} + +.lightbox-figure { + margin: 0; + display: flex; + flex-direction: column; + align-items: center; + gap: var(--space-md, 1rem); +} + +.lightbox-caption { + color: rgba(255, 255, 255, 0.8); + font-family: var(--t-font-body); + font-size: 0.875rem; + text-align: center; + max-width: 600px; + line-height: 1.5; +} + +.lightbox-counter { + position: absolute; + bottom: var(--space-md, 1rem); + left: 50%; + transform: translateX(-50%); + color: rgba(255, 255, 255, 0.7); + font-size: 0.875rem; + font-family: var(--t-font-body); +} + +/* PDP Main Image zoom cursor */ +.pdp-main-image-container { + cursor: zoom-in; +} + +/* ============================================= + Type Scale Utility Classes + These scale with the font-size setting + ============================================= */ + +/* Body text sizes */ +.preview-frame .t-caption { font-size: var(--t-text-caption); } +.preview-frame .t-small { font-size: var(--t-text-small); } +.preview-frame .t-base { font-size: var(--t-text-base); } +.preview-frame .t-large { font-size: var(--t-text-large); } +.preview-frame .t-xl { font-size: var(--t-text-xl); } +.preview-frame .t-2xl { font-size: var(--t-text-2xl); } + +/* Heading sizes */ +.preview-frame .t-heading-sm { font-size: var(--t-heading-sm); } +.preview-frame .t-heading-md { font-size: var(--t-heading-md); } +.preview-frame .t-heading-lg { font-size: var(--t-heading-lg); } +.preview-frame .t-heading-xl { font-size: var(--t-heading-xl); } +.preview-frame .t-heading-display { font-size: var(--t-heading-display); } + +/* Override Tailwind text-* classes within preview to use our scale */ +.preview-frame .text-xs { font-size: var(--t-text-caption) !important; } +.preview-frame .text-sm { font-size: var(--t-text-small) !important; } +.preview-frame .text-base { font-size: var(--t-text-base) !important; } +.preview-frame .text-lg { font-size: var(--t-text-large) !important; } +.preview-frame .text-xl { font-size: var(--t-text-xl) !important; } +.preview-frame .text-2xl { font-size: var(--t-text-2xl) !important; } + +/* Map larger Tailwind sizes to our heading scale */ +.preview-frame .text-3xl { font-size: var(--t-heading-lg) !important; } +.preview-frame .text-4xl { font-size: var(--t-heading-xl) !important; } +.preview-frame .text-5xl, +.preview-frame .text-6xl, +.preview-frame .text-7xl, +.preview-frame .text-8xl, +.preview-frame .text-9xl { font-size: var(--t-heading-display) !important; } + +/* ============================================= + Filter Pills (Collection Page) + ============================================= */ + +.preview-frame .filter-pills-container { + scrollbar-width: none; + -webkit-overflow-scrolling: touch; +} + +.preview-frame .filter-pills-container::-webkit-scrollbar { + display: none; +} + +.preview-frame .filter-pill { + flex-shrink: 0; + padding: 0.5rem 1rem; + font-size: var(--t-text-small); + font-weight: 500; + border-radius: var(--t-radius-button); + border: 1px solid var(--t-border-default); + background-color: var(--t-surface-base); + color: var(--t-text-secondary); + cursor: pointer; + transition: all 0.15s ease; + white-space: nowrap; +} + +.preview-frame .filter-pill:hover { + background-color: var(--t-surface-sunken); + color: var(--t-text-primary); +} + +.preview-frame .filter-pill-active { + background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); + color: var(--t-text-inverse); + border-color: transparent; +} + +.preview-frame .filter-pill-active:hover { + background-color: hsl(var(--t-accent-h) var(--t-accent-s) calc(var(--t-accent-l) - 5%)); + color: var(--t-text-inverse); +} diff --git a/assets/css/theme-semantic.css b/assets/css/theme-semantic.css index 973eeeb..20c798c 100644 --- a/assets/css/theme-semantic.css +++ b/assets/css/theme-semantic.css @@ -18,10 +18,30 @@ --t-secondary-accent: #ea580c; --t-sale-color: #dc2626; - /* Font size scale */ + /* Font size scale - all sizes use em so they scale with --t-font-size-scale */ --t-font-size-scale: 1; --t-heading-weight: 600; + /* + * Type Scale - Limited to 6 body sizes for design consistency + * These use em units so they scale with the base font size setting + * Body: caption, small, base, large + * Display: xl, 2xl (for headings and hero text) + */ + --t-text-caption: 0.75em; /* ~12px at 16px base, ~14px at 18px base */ + --t-text-small: 0.875em; /* ~14px at 16px base, ~16px at 18px base */ + --t-text-base: 1em; /* matches base font size setting */ + --t-text-large: 1.125em; /* ~18px at 16px base, ~20px at 18px base */ + --t-text-xl: 1.25em; /* ~20px at 16px base, ~22px at 18px base */ + --t-text-2xl: 1.5em; /* ~24px at 16px base, ~27px at 18px base */ + + /* Heading sizes - separate scale for headings */ + --t-heading-sm: 1.25em; /* h4, h5, h6 */ + --t-heading-md: 1.5em; /* h3 */ + --t-heading-lg: 2em; /* h2 */ + --t-heading-xl: 2.5em; /* h1 */ + --t-heading-display: 3em; /* hero/display text */ + /* Layout */ --t-layout-max-width: 1400px; --t-button-style: filled; diff --git a/assets/js/app.js b/assets/js/app.js index 152b6f5..2208907 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -43,11 +43,104 @@ const ColorSync = { } } +// Hook for PDP image lightbox +const Lightbox = { + mounted() { + const dialog = this.el + const lightboxImage = dialog.querySelector('#lightbox-image') + const lightboxCounter = dialog.querySelector('#lightbox-counter') + + // Get images from data attribute + const getImages = () => { + try { + return JSON.parse(dialog.dataset.images || '[]') + } catch { + return [] + } + } + + const getCurrentIndex = () => parseInt(dialog.dataset.currentIndex || '0', 10) + const setCurrentIndex = (idx) => { dialog.dataset.currentIndex = idx.toString() } + + const updateImage = () => { + const images = getImages() + const idx = getCurrentIndex() + if (images.length > 0 && lightboxImage) { + lightboxImage.src = images[idx] + if (lightboxCounter) { + lightboxCounter.textContent = `${idx + 1} / ${images.length}` + } + } + } + + const nextImage = () => { + const images = getImages() + const newIdx = (getCurrentIndex() + 1) % images.length + setCurrentIndex(newIdx) + updateImage() + } + + const prevImage = () => { + const images = getImages() + const newIdx = (getCurrentIndex() - 1 + images.length) % images.length + setCurrentIndex(newIdx) + updateImage() + } + + const openLightbox = () => { + updateImage() + dialog.showModal() + } + + const closeLightbox = () => { + dialog.close() + } + + // Event listeners for custom events dispatched from LiveView.JS + dialog.addEventListener('pdp:open-lightbox', openLightbox) + dialog.addEventListener('pdp:close-lightbox', closeLightbox) + dialog.addEventListener('pdp:next-image', nextImage) + dialog.addEventListener('pdp:prev-image', prevImage) + + // Close on clicking backdrop + dialog.addEventListener('click', (e) => { + if (e.target === dialog) { + closeLightbox() + } + }) + + // Keyboard navigation + dialog.addEventListener('keydown', (e) => { + if (e.key === 'ArrowRight') { + nextImage() + } else if (e.key === 'ArrowLeft') { + prevImage() + } else if (e.key === 'Escape') { + closeLightbox() + } + }) + + // Store cleanup function + this.cleanup = () => { + dialog.removeEventListener('pdp:open-lightbox', openLightbox) + dialog.removeEventListener('pdp:close-lightbox', closeLightbox) + dialog.removeEventListener('pdp:next-image', nextImage) + dialog.removeEventListener('pdp:prev-image', prevImage) + } + }, + + destroyed() { + if (this.cleanup) { + this.cleanup() + } + } +} + const csrfToken = document.querySelector("meta[name='csrf-token']").getAttribute("content") const liveSocket = new LiveSocket("/live", Socket, { longPollFallbackMs: 2500, params: {_csrf_token: csrfToken}, - hooks: {...colocatedHooks, ColorSync}, + hooks: {...colocatedHooks, ColorSync, Lightbox}, }) // Show progress bar on live navigation and form submits @@ -55,6 +148,14 @@ topbar.config({barColors: {0: "#29d"}, shadowColor: "rgba(0, 0, 0, .3)"}) window.addEventListener("phx:page-loading-start", _info => topbar.show(300)) window.addEventListener("phx:page-loading-stop", _info => topbar.hide()) +// Scroll preview frame to top when changing pages +window.addEventListener("phx:scroll-preview-top", (e) => { + const previewFrame = document.querySelector('.preview-frame') + if (previewFrame) { + previewFrame.scrollTop = 0 + } +}) + // connect if there are any LiveViews on the page liveSocket.connect() diff --git a/lib/simpleshop_theme/settings/theme_settings.ex b/lib/simpleshop_theme/settings/theme_settings.ex index ebc5086..1f3fa7f 100644 --- a/lib/simpleshop_theme/settings/theme_settings.ex +++ b/lib/simpleshop_theme/settings/theme_settings.ex @@ -44,7 +44,6 @@ defmodule SimpleshopTheme.Settings.ThemeSettings do field :announcement_bar, :boolean, default: true field :sticky_header, :boolean, default: false field :hover_image, :boolean, default: true - field :quick_add, :boolean, default: true field :show_prices, :boolean, default: true field :pdp_trust_badges, :boolean, default: true field :pdp_reviews, :boolean, default: true @@ -85,7 +84,6 @@ defmodule SimpleshopTheme.Settings.ThemeSettings do :announcement_bar, :sticky_header, :hover_image, - :quick_add, :show_prices, :pdp_trust_badges, :pdp_reviews, @@ -105,5 +103,10 @@ defmodule SimpleshopTheme.Settings.ThemeSettings do |> validate_number(:header_position_y, greater_than_or_equal_to: 0, less_than_or_equal_to: 100) |> validate_inclusion(:layout_width, ~w(contained wide full)) |> validate_inclusion(:card_shadow, ~w(none sm md lg)) + |> validate_inclusion(:font_size, ~w(small medium large)) + |> validate_inclusion(:heading_weight, ~w(regular medium bold)) + |> validate_inclusion(:button_style, ~w(filled outline soft)) + |> validate_inclusion(:product_text_align, ~w(left center)) + |> validate_inclusion(:image_aspect_ratio, ~w(square portrait landscape)) end end diff --git a/lib/simpleshop_theme/theme/css_generator.ex b/lib/simpleshop_theme/theme/css_generator.ex index 0c09641..c5a4540 100644 --- a/lib/simpleshop_theme/theme/css_generator.ex +++ b/lib/simpleshop_theme/theme/css_generator.ex @@ -19,6 +19,12 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do .preview-frame, .shop-root { #{generate_accent(settings.accent_color)} #{generate_secondary_colors(settings)} + #{generate_font_size(settings.font_size)} + #{generate_heading_weight(settings.heading_weight)} + #{generate_layout_width(settings.layout_width)} + #{generate_button_style(settings.button_style)} + #{generate_product_text_align(settings.product_text_align)} + #{generate_image_aspect_ratio(settings.image_aspect_ratio)} } """ |> String.trim() @@ -225,6 +231,82 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do """ end + # Font size variations + # Using 18px as base for better accessibility (WCAG recommends 18px+) + # Small: 18px, Medium: 19px, Large: 20px + defp generate_font_size("small") do + "--t-font-size-scale: 1.125;" + end + + defp generate_font_size("medium") do + "--t-font-size-scale: 1.1875;" + end + + defp generate_font_size("large") do + "--t-font-size-scale: 1.25;" + end + + # Heading weight (override typography default) + defp generate_heading_weight("regular") do + "--t-heading-weight-override: 400;" + end + + defp generate_heading_weight("medium") do + "--t-heading-weight-override: 500;" + end + + defp generate_heading_weight("bold") do + "--t-heading-weight-override: 700;" + end + + # Layout width + defp generate_layout_width("contained") do + "--t-layout-max-width: 1100px;" + end + + defp generate_layout_width("wide") do + "--t-layout-max-width: 1400px;" + end + + defp generate_layout_width("full") do + "--t-layout-max-width: 100%;" + end + + # Button style + defp generate_button_style("filled") do + "--t-button-style: filled;" + end + + defp generate_button_style("outline") do + "--t-button-style: outline;" + end + + defp generate_button_style("soft") do + "--t-button-style: soft;" + end + + # Product text alignment + defp generate_product_text_align("left") do + "--t-product-text-align: left;" + end + + defp generate_product_text_align("center") do + "--t-product-text-align: center;" + end + + # Image aspect ratio + defp generate_image_aspect_ratio("square") do + "--t-image-aspect-ratio: 1 / 1;" + end + + defp generate_image_aspect_ratio("portrait") do + "--t-image-aspect-ratio: 3 / 4;" + end + + defp generate_image_aspect_ratio("landscape") do + "--t-image-aspect-ratio: 4 / 3;" + end + # Convert hex color to HSL defp hex_to_hsl("#" <> hex), do: hex_to_hsl(hex) diff --git a/lib/simpleshop_theme_web/live/theme_live/index.ex b/lib/simpleshop_theme_web/live/theme_live/index.ex index 740afec..79ac447 100644 --- a/lib/simpleshop_theme_web/live/theme_live/index.ex +++ b/lib/simpleshop_theme_web/live/theme_live/index.ex @@ -122,7 +122,13 @@ defmodule SimpleshopThemeWeb.ThemeLive.Index do @impl true def handle_event("change_preview_page", %{"page" => page_name}, socket) do page_atom = String.to_existing_atom(page_name) - {:noreply, assign(socket, :preview_page, page_atom)} + + socket = + socket + |> assign(:preview_page, page_atom) + |> push_event("scroll-preview-top", %{}) + + {:noreply, socket} end @impl true diff --git a/lib/simpleshop_theme_web/live/theme_live/index.html.heex b/lib/simpleshop_theme_web/live/theme_live/index.html.heex index 071dd3d..42eeb69 100644 --- a/lib/simpleshop_theme_web/live/theme_live/index.html.heex +++ b/lib/simpleshop_theme_web/live/theme_live/index.html.heex @@ -310,7 +310,7 @@ - +
@@ -327,6 +327,38 @@
+
+ +
+
+ + <%= @theme_settings.secondary_accent_color %> +
+
+
+ +
+ +
+
+ + <%= @theme_settings.sale_color %> +
+
+
+
@@ -370,6 +402,52 @@ <% end %> + +
+ +
+ <%= for {value, label} <- [{"small", "Small"}, {"medium", "Medium"}, {"large", "Large"}] do %> + + <% end %> +
+
+ +
+ +
+ <%= for {value, label} <- [{"regular", "Regular"}, {"medium", "Medium"}, {"bold", "Bold"}] do %> + + <% end %> +
+
@@ -567,10 +645,33 @@ <% end %> + +
+ +
+ <%= for {value, label} <- [{"filled", "Filled"}, {"outline", "Outline"}, {"soft", "Soft"}] do %> + + <% end %> +
+
- -
+ +
@@ -578,7 +679,7 @@ - Layout + Products
@@ -603,6 +704,128 @@ <% end %>
+ +
+ +
+ <%= for {value, label} <- [{"square", "Square"}, {"portrait", "Portrait"}, {"landscape", "Landscape"}] do %> + + <% end %> +
+
+ +
+ +
+ <%= for {value, label} <- [{"left", "Left"}, {"center", "Centre"}] do %> + + <% end %> +
+
+ +
+ +
+ +
+ +
+
+ + +
+
+ + + + + Product page +
+ +
+ +
+ +
+ +
+ +
+ +
@@ -674,7 +897,8 @@ data-header={@theme_settings.header_layout} data-sticky={to_string(@theme_settings.sticky_header)} data-layout={@theme_settings.layout_width} - data-shadow={@theme_settings.card_shadow}> + data-shadow={@theme_settings.card_shadow} + data-button-style={@theme_settings.button_style}> diff --git a/lib/simpleshop_theme_web/live/theme_live/preview_pages.ex b/lib/simpleshop_theme_web/live/theme_live/preview_pages.ex index 4edf84a..629fb87 100644 --- a/lib/simpleshop_theme_web/live/theme_live/preview_pages.ex +++ b/lib/simpleshop_theme_web/live/theme_live/preview_pages.ex @@ -12,7 +12,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do ~H"""

Free delivery on orders over £40

@@ -39,7 +39,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do @@ -147,16 +147,16 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do

Get 10% off your first order and be the first to know about new prints.

-
+ Phoenix.LiveView.JS.remove_class("open", to: "#cart-drawer-overlay") |> Phoenix.LiveView.JS.push("change_preview_page", value: %{page: "cart"})} class="cart-drawer-link" - style="display: block; text-align: center; font-family: var(--t-font-body); font-size: var(--p-text-sm); color: var(--t-text-secondary); text-decoration: underline; cursor: pointer;" + style="display: block; text-align: center; font-family: var(--t-font-body); font-size: var(--t-text-small); color: var(--t-text-secondary); text-decoration: underline; cursor: pointer;" > View basket diff --git a/lib/simpleshop_theme_web/live/theme_live/preview_pages/about.html.heex b/lib/simpleshop_theme_web/live/theme_live/preview_pages/about.html.heex index 67879bd..b0fe962 100644 --- a/lib/simpleshop_theme_web/live/theme_live/preview_pages/about.html.heex +++ b/lib/simpleshop_theme_web/live/theme_live/preview_pages/about.html.heex @@ -41,14 +41,14 @@ Based in the Yorkshire countryside, I work from a small garden studio surrounded by the very nature that inspires each piece. Every print is checked by hand before being carefully packaged and sent on its way.

-

+

Sustainability

I believe beautiful art shouldn't cost the earth. All prints are produced on FSC-certified paper, shipped in plastic-free packaging, and printed locally to reduce transport emissions.

-

+

The process

diff --git a/lib/simpleshop_theme_web/live/theme_live/preview_pages/collection.html.heex b/lib/simpleshop_theme_web/live/theme_live/preview_pages/collection.html.heex index 97cd3b6..5a99a1c 100644 --- a/lib/simpleshop_theme_web/live/theme_live/preview_pages/collection.html.heex +++ b/lib/simpleshop_theme_web/live/theme_live/preview_pages/collection.html.heex @@ -20,91 +20,36 @@

-
- -
-
-

- Filter by Category -

-
- <%= for category <- @preview_data.categories do %> - - <% end %> -
- -
-

- Price Range -

-
- - - - -
-
-
+ +
+ +
+ + <%= for category <- @preview_data.categories do %> + + <% end %>
- -
-
-

- Showing all <%= length(@preview_data.products) %> products -

- -
+ + +
-
+
"lg:grid-cols-2" "3" -> "lg:grid-cols-3" @@ -119,15 +64,14 @@ class="product-card group overflow-hidden transition-all" style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card); cursor: pointer;" > -
+
- <%= if product.on_sale do %> - Sale - <% end %> - <%= if not product.in_stock do %> -
- Out of Stock -
+ <%= cond do %> + <% not product.in_stock -> %> + Sold out + <% product.on_sale -> %> + Sale + <% true -> %> <% end %> - <%= if product[:hover_image_url] do %> + <%= if @theme_settings.hover_image && product[:hover_image_url] do %> {product.name} <% end %>
-
+

<%= product.category %>

<%= product.name %>

-
+ <%= if @theme_settings.show_prices do %>
<%= if product.on_sale do %> @@ -166,21 +110,11 @@ <% end %>
- <%= if product.in_stock do %> - - <% end %> -
+ <% end %>
<% end %>
-
-
diff --git a/lib/simpleshop_theme_web/live/theme_live/preview_pages/contact.html.heex b/lib/simpleshop_theme_web/live/theme_live/preview_pages/contact.html.heex index 7eb2391..fdbee5d 100644 --- a/lib/simpleshop_theme_web/live/theme_live/preview_pages/contact.html.heex +++ b/lib/simpleshop_theme_web/live/theme_live/preview_pages/contact.html.heex @@ -13,7 +13,7 @@

- Have a question or comment? We'd love to hear from you. Send us a message and we'll respond as soon as possible. + Questions about your order or just want to say hello? Drop us a message and we'll get back to you as soon as we can.

@@ -87,44 +87,96 @@
+
-

Email

-

hello@example.com

+

Track your order

+

+ Enter your email and we'll send you a link to check your order status. +

+
+ + +
+
-

Phone

-

(555) 123-4567

+

Handy to know

+
    +
  • + • + Printing: 2-5 business days +
  • +
  • + • + Delivery: 3-7 business days after printing +
  • +
  • + • + Returns: Happy to help with faulty or damaged items +
  • +
+
-

Address

-

- 123 Main Street
- San Francisco, CA 94102
- United States +

Get in touch

+ + + + + hello@example.com + +

+ We typically respond within 24 hours

-
-

Hours

-

- Monday - Friday: 9am - 6pm
- Saturday: 10am - 4pm
- Sunday: Closed -

+ +
diff --git a/lib/simpleshop_theme_web/live/theme_live/preview_pages/error.html.heex b/lib/simpleshop_theme_web/live/theme_live/preview_pages/error.html.heex index abec1a9..2ae1336 100644 --- a/lib/simpleshop_theme_web/live/theme_live/preview_pages/error.html.heex +++ b/lib/simpleshop_theme_web/live/theme_live/preview_pages/error.html.heex @@ -37,26 +37,35 @@
-
+
<%= for product <- Enum.take(@preview_data.products, 4) do %>
-
+
{product.name} + <%= if @theme_settings.hover_image && product[:hover_image_url] do %> + {product.name} + <% end %>

<%= product.name %>

-

- £<%= product.price / 100 %> -

+ <%= if @theme_settings.show_prices do %> +

+ £<%= product.price / 100 %> +

+ <% end %>
<% end %> diff --git a/lib/simpleshop_theme_web/live/theme_live/preview_pages/home.html.heex b/lib/simpleshop_theme_web/live/theme_live/preview_pages/home.html.heex index b2e9fec..d79f41d 100644 --- a/lib/simpleshop_theme_web/live/theme_live/preview_pages/home.html.heex +++ b/lib/simpleshop_theme_web/live/theme_live/preview_pages/home.html.heex @@ -49,7 +49,7 @@
"lg:grid-cols-2" "3" -> "lg:grid-cols-3" @@ -64,7 +64,7 @@ class="product-card group overflow-hidden transition-all hover:-translate-y-1" style="background-color: var(--t-surface-raised); border-radius: var(--t-radius-card); cursor: pointer;" > -
+
<%= if product[:is_new] do %> New <% end %> @@ -76,30 +76,26 @@ alt={product.name} class="product-image-primary w-full h-full object-cover transition-opacity duration-300" /> - <%= if product[:hover_image_url] do %> + <%= if @theme_settings.hover_image && product[:hover_image_url] do %> {product.name} <% end %> -
-
+

<%= product.name %>

-

- <%= if product.on_sale do %> - £<%= product.compare_at_price / 100 %> - <% end %> - £<%= product.price / 100 %> -

+ <%= if @theme_settings.show_prices do %> +

+ <%= if product.on_sale do %> + £<%= product.compare_at_price / 100 %> + <% end %> + £<%= product.price / 100 %> +

+ <% end %>
<% end %> diff --git a/lib/simpleshop_theme_web/live/theme_live/preview_pages/pdp.html.heex b/lib/simpleshop_theme_web/live/theme_live/preview_pages/pdp.html.heex index 525f404..6701a00 100644 --- a/lib/simpleshop_theme_web/live/theme_live/preview_pages/pdp.html.heex +++ b/lib/simpleshop_theme_web/live/theme_live/preview_pages/pdp.html.heex @@ -12,35 +12,125 @@
-
- Home +
+
-
+ <% gallery_images = [product.image_url, product.hover_image_url, product.image_url, product.hover_image_url] %> +
{product.name} + +
+
+ + + +
+
- <%= for img_url <- [product.image_url, product.hover_image_url, product.image_url, product.hover_image_url] do %> -
+ <%= for {img_url, idx} <- Enum.with_index(gallery_images) do %> +
+ <% end %>
+ + + + + +
@@ -118,61 +208,151 @@ Add to basket - -
-
- - - -
-

Free Delivery

-

On orders over £40

+ + <%= if @theme_settings.pdp_trust_badges do %> +
+
+ + + +
+

Free Delivery

+

On orders over £40

+
-
-
- - - -
-

Easy Returns

-

30-day return policy

-
-
-
-
-
- - -
-

- You might also like -

- -
- <%= for related_product <- Enum.slice(@preview_data.products, 1, 4) do %> -
-
- {related_product.name} -
-
-

- <%= related_product.name %> -

-

- $<%= related_product.price / 100 %> -

+
+ + + +
+

Easy Returns

+

30-day return policy

+
<% end %>
+ + + <%= if @theme_settings.pdp_reviews do %> +
+
+

+ Customer reviews +

+
+
+ <%= for _i <- 1..5 do %> + + + + <% end %> +
+ Based on 24 reviews +
+
+ +
+ +
+
+
+ <%= for _i <- 1..5 do %> + + + + <% end %> +
+ 2 weeks ago +
+

Absolutely beautiful

+

+ The quality exceeded my expectations. The colours are vibrant and the paper feels premium. It's now pride of place in my living room. +

+
+ Sarah M. + Verified purchase +
+
+ + +
+
+
+ <%= for i <- 1..5 do %> + + + + <% end %> +
+ 1 month ago +
+

Great gift

+

+ Bought this as a gift and it arrived beautifully packaged. Fast shipping too. Would definitely order again. +

+
+ James T. + Verified purchase +
+
+
+ + +
+ <% end %> + + + <%= if @theme_settings.pdp_related_products do %> + + <% end %>