feat: redesign contact page for POD sellers & add theme enhancements

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 <noreply@anthropic.com>
This commit is contained in:
Jamey Greenwood 2026-01-02 13:48:03 +00:00
parent 98a9e3b3d4
commit 37653e5e7a
14 changed files with 1178 additions and 236 deletions

View File

@ -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);
}

View File

@ -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;

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -310,7 +310,7 @@
</div>
</div>
<!-- Accent Color (stays in essentials) -->
<!-- Accent Colors (stays in essentials) -->
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Accent colour</label>
<form id="accent-color-form" phx-change="update_color" phx-value-field="accent_color" phx-hook="ColorSync">
@ -327,6 +327,38 @@
</form>
</div>
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Hover colour</label>
<form id="secondary-accent-color-form" phx-change="update_color" phx-value-field="secondary_accent_color" phx-hook="ColorSync">
<div class="flex items-center gap-3">
<input
type="color"
id="secondary-accent-color-picker"
name="value"
value={@theme_settings.secondary_accent_color}
class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0"
/>
<span class="font-mono text-sm text-base-content/70"><%= @theme_settings.secondary_accent_color %></span>
</div>
</form>
</div>
<div class="mb-6">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Sale colour</label>
<form id="sale-color-form" phx-change="update_color" phx-value-field="sale_color" phx-hook="ColorSync">
<div class="flex items-center gap-3">
<input
type="color"
id="sale-color-picker"
name="value"
value={@theme_settings.sale_color}
class="w-12 h-12 rounded-lg cursor-pointer border-0 p-0"
/>
<span class="font-mono text-sm text-base-content/70"><%= @theme_settings.sale_color %></span>
</div>
</form>
</div>
<!-- Customise Section (collapsible accordion using native details/summary) -->
<details class="border-t border-base-300 mt-6 pt-4 group" id="customise-section" open={@customise_open}>
<summary class="flex items-center justify-between w-full py-3 cursor-pointer list-none [&::-webkit-details-marker]:hidden" phx-click="toggle_customise">
@ -370,6 +402,52 @@
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Font size</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"small", "Small"}, {"medium", "Medium"}, {"large", "Large"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="font_size"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.font_size == value,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= label %>
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Heading weight</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"regular", "Regular"}, {"medium", "Medium"}, {"bold", "Bold"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="heading_weight"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.heading_weight == value,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= label %>
</button>
<% end %>
</div>
</div>
</div>
<!-- Colours Group -->
@ -567,10 +645,33 @@
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Button style</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"filled", "Filled"}, {"outline", "Outline"}, {"soft", "Soft"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="button_style"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.button_style == value,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= label %>
</button>
<% end %>
</div>
</div>
</div>
<!-- Layout Group -->
<div class="mb-4">
<!-- Products Group -->
<div class="mb-6 pb-6 border-b border-base-200">
<div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"></rect>
@ -578,7 +679,7 @@
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
<span class="text-sm font-semibold text-base-content">Layout</span>
<span class="text-sm font-semibold text-base-content">Products</span>
</div>
<div class="mb-4">
@ -603,6 +704,128 @@
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Image aspect ratio</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"square", "Square"}, {"portrait", "Portrait"}, {"landscape", "Landscape"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="image_aspect_ratio"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.image_aspect_ratio == value,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= label %>
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="block text-xs font-semibold uppercase tracking-wider text-base-content/60 mb-3">Product text alignment</label>
<div class="flex flex-wrap gap-2">
<%= for {value, label} <- [{"left", "Left"}, {"center", "Centre"}] do %>
<button
type="button"
phx-click="update_setting"
phx-value-field="product_text_align"
phx-value-setting_value={value}
class={[
"px-3 py-2 text-sm rounded-lg border-2 transition-all",
if(@theme_settings.product_text_align == value,
do: "border-base-content bg-base-100 text-base-content",
else: "border-transparent bg-base-200 hover:bg-base-300 text-base-content"
)
]}
>
<%= label %>
</button>
<% end %>
</div>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.hover_image}
phx-click="toggle_setting"
phx-value-field="hover_image"
class="checkbox checkbox-sm"
/>
<span class="text-sm text-base-content">Second image on hover</span>
</label>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.show_prices}
phx-click="toggle_setting"
phx-value-field="show_prices"
class="checkbox checkbox-sm"
/>
<span class="text-sm text-base-content">Show prices</span>
</label>
</div>
</div>
<!-- Product Page Group -->
<div class="mb-4">
<div class="flex items-center gap-2 mb-4">
<svg class="w-4 h-4 text-base-content/50" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"></rect>
<line x1="3" y1="9" x2="21" y2="9"></line>
</svg>
<span class="text-sm font-semibold text-base-content">Product page</span>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.pdp_trust_badges}
phx-click="toggle_setting"
phx-value-field="pdp_trust_badges"
class="checkbox checkbox-sm"
/>
<span class="text-sm text-base-content">Trust badges</span>
</label>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.pdp_reviews}
phx-click="toggle_setting"
phx-value-field="pdp_reviews"
class="checkbox checkbox-sm"
/>
<span class="text-sm text-base-content">Reviews section</span>
</label>
</div>
<div class="mb-4">
<label class="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={@theme_settings.pdp_related_products}
phx-click="toggle_setting"
phx-value-field="pdp_related_products"
class="checkbox checkbox-sm"
/>
<span class="text-sm text-base-content">Related products</span>
</label>
</div>
</div>
</div>
</details>
@ -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}>
<style>
<%= Phoenix.HTML.raw(@generated_css) %>
</style>

View File

@ -12,7 +12,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
~H"""
<div
class="announcement-bar"
style="background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); text-align: center; padding: 0.5rem 1rem; font-size: 0.875rem;"
style="background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); text-align: center; padding: 0.5rem 1rem; font-size: var(--t-text-small);"
>
<p style="margin: 0;">Free delivery on orders over £40</p>
</div>
@ -39,7 +39,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
<div class="shop-logo" style="display: flex; align-items: center; position: relative; z-index: 1;">
<%= case @theme_settings.logo_mode do %>
<% "text-only" -> %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
@ -51,7 +51,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
style={"height: #{@theme_settings.logo_size}px; width: auto; margin-right: 0.5rem;"}
/>
<% end %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
@ -63,13 +63,13 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
style={"height: #{@theme_settings.logo_size}px; width: auto;"}
/>
<% else %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% end %>
<% _ -> %>
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: 1.25rem; font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<span class="shop-logo-text" style="font-family: var(--t-font-heading); font-size: var(--t-text-xl); font-weight: var(--t-heading-weight); color: var(--t-text-primary);">
<%= @theme_settings.site_name %>
</span>
<% end %>
@ -107,7 +107,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
<line x1="3" y1="6" x2="21" y2="6"></line>
<path d="M16 10a4 4 0 01-8 0"></path>
</svg>
<span class="cart-count" style="position: absolute; top: -4px; right: -4px; background: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); font-size: 11px; font-weight: 600; min-width: 18px; height: 18px; border-radius: 9999px; display: flex; align-items: center; justify-content: center; padding: 0 4px;">2</span>
<span class="cart-count" style="position: absolute; top: -4px; right: -4px; background: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); font-size: var(--t-text-caption); font-weight: 600; min-width: 18px; height: 18px; border-radius: 9999px; display: flex; align-items: center; justify-content: center; padding: 0 4px;">2</span>
<span class="sr-only">Cart (2)</span>
</button>
</div>
@ -147,16 +147,16 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
<p class="mb-4 text-sm" style="color: var(--t-text-secondary);">
Get 10% off your first order and be the first to know about new prints.
</p>
<form class="flex gap-2">
<form class="flex flex-wrap gap-2">
<input
type="email"
placeholder="your@email.com"
class="flex-1 px-4 py-2 text-sm"
style="background-color: var(--t-surface-base); color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input);"
class="flex-1 min-w-0 px-4 py-2 text-sm"
style="background-color: var(--t-surface-base); color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input); min-width: 150px;"
/>
<button
type="submit"
class="px-6 py-2 font-medium transition-all text-sm"
class="px-6 py-2 font-medium transition-all text-sm whitespace-nowrap"
style="background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); border-radius: var(--t-radius-button);"
>
Subscribe
@ -240,7 +240,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
style="position: fixed; top: 0; right: -400px; width: 400px; max-width: 90vw; height: 100vh; background: var(--t-surface-raised); z-index: 1001; display: flex; flex-direction: column; transition: right 0.3s ease; box-shadow: -4px 0 20px rgba(0,0,0,0.15);"
>
<div class="cart-drawer-header" style="display: flex; justify-content: space-between; align-items: center; padding: 1rem 1.5rem; border-bottom: 1px solid var(--t-border-default);">
<h2 style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--p-text-lg); color: var(--t-text-primary); margin: 0;">Your basket</h2>
<h2 style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--t-text-large); color: var(--t-text-primary); margin: 0;">Your basket</h2>
<button
type="button"
class="cart-drawer-close"
@ -260,13 +260,13 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
<div class="cart-drawer-item" style="display: flex; gap: 0.75rem; padding: 0.75rem 0; border-bottom: 1px solid var(--t-border-default);">
<div class="cart-drawer-item-image" style={"width: 60px; height: 60px; border-radius: var(--t-radius-card); background-size: cover; background-position: center; background-image: url('#{item.image}'); flex-shrink: 0;"}></div>
<div class="cart-drawer-item-details" style="flex: 1;">
<h3 style="font-family: var(--t-font-body); font-size: var(--p-text-sm); font-weight: 500; color: var(--t-text-primary); margin: 0 0 2px;">
<h3 style="font-family: var(--t-font-body); font-size: var(--t-text-small); font-weight: 500; color: var(--t-text-primary); margin: 0 0 2px;">
<%= item.name %>
</h3>
<p style="font-family: var(--t-font-body); font-size: var(--p-text-xs); color: var(--t-text-tertiary); margin: 0;">
<p style="font-family: var(--t-font-body); font-size: var(--t-text-caption); color: var(--t-text-tertiary); margin: 0;">
<%= item.variant %>
</p>
<p class="cart-drawer-item-price" style="color: var(--t-text-primary); font-weight: 500; margin-top: 4px; font-size: var(--p-text-sm);">
<p class="cart-drawer-item-price" style="color: var(--t-text-primary); font-weight: 500; margin-top: 4px; font-size: var(--t-text-small);">
<%= item.price %>
</p>
</div>
@ -275,7 +275,7 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
</div>
<div class="cart-drawer-footer" style="padding: 1rem 1.5rem; border-top: 1px solid var(--t-border-default); background: var(--t-surface-sunken);">
<div class="cart-drawer-total" style="display: flex; justify-content: space-between; font-family: var(--t-font-body); font-size: var(--p-text-base); font-weight: 600; color: var(--t-text-primary); margin-bottom: 1rem;">
<div class="cart-drawer-total" style="display: flex; justify-content: space-between; font-family: var(--t-font-body); font-size: var(--t-text-base); font-weight: 600; color: var(--t-text-primary); margin-bottom: 1rem;">
<span>Subtotal</span>
<span>£72.00</span>
</div>
@ -288,10 +288,9 @@ defmodule SimpleshopThemeWeb.ThemeLive.PreviewPages do
</button>
<a
href="#"
phx-click="change_preview_page"
phx-value-page="cart"
phx-click={Phoenix.LiveView.JS.remove_class("open", to: "#cart-drawer") |> 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
</a>

View File

@ -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.
</p>
<h2 class="mt-8 mb-3" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--p-text-xl); color: var(--t-text-primary);">
<h2 class="mt-8 mb-3" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--t-text-xl); color: var(--t-text-primary);">
Sustainability
</h2>
<p class="mb-4" style="color: var(--t-text-secondary);">
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.
</p>
<h2 class="mt-8 mb-3" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--p-text-xl); color: var(--t-text-primary);">
<h2 class="mt-8 mb-3" style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); font-size: var(--t-text-xl); color: var(--t-text-primary);">
The process
</h2>
<p class="mb-4" style="color: var(--t-text-secondary);">

View File

@ -20,91 +20,36 @@
</div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex flex-col md:flex-row gap-8">
<!-- Filters Sidebar -->
<div class="w-full md:w-64 flex-shrink-0">
<div class="p-4" style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);">
<h3 class="font-semibold mb-4" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
Filter by Category
</h3>
<div class="space-y-2">
<%= for category <- @preview_data.categories do %>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="rounded"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); border-radius: var(--t-radius-input);"
/>
<span style="color: var(--t-text-primary);"><%= category.name %></span>
<span class="ml-auto text-sm" style="color: var(--t-text-tertiary);">
<%= category.product_count %>
</span>
</label>
<% end %>
</div>
<div class="mt-6 pt-6" style="border-top: 1px solid var(--t-border-subtle);">
<h3 class="font-semibold mb-4" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
Price Range
</h3>
<div class="space-y-2">
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="rounded"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
/>
<span style="color: var(--t-text-primary);">Under $25</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="rounded"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
/>
<span style="color: var(--t-text-primary);">$25 - $50</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="rounded"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
/>
<span style="color: var(--t-text-primary);">$50 - $100</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
class="rounded"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
/>
<span style="color: var(--t-text-primary);">Over $100</span>
</label>
</div>
</div>
</div>
<!-- Filter Bar: Category Pills + Sort -->
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
<!-- Category Pills -->
<div class="filter-pills-container flex gap-2 overflow-x-auto">
<button class="filter-pill filter-pill-active">
All
</button>
<%= for category <- @preview_data.categories do %>
<button class="filter-pill">
<%= category.name %>
</button>
<% end %>
</div>
<!-- Product Grid -->
<div class="flex-1">
<div class="flex items-center justify-between mb-6">
<p style="color: var(--t-text-secondary);">
Showing all <%= length(@preview_data.products) %> products
</p>
<select
class="px-4 py-2"
style="background-color: var(--t-surface-raised); color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input);"
>
<option>Sort by: Featured</option>
<option>Price: Low to High</option>
<option>Price: High to Low</option>
<option>Newest</option>
<option>Best Selling</option>
</select>
</div>
<!-- Sort Dropdown -->
<select
class="px-4 py-2"
style="background-color: var(--t-surface-raised); color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input);"
>
<option>Sort by: Featured</option>
<option>Price: Low to High</option>
<option>Price: High to Low</option>
<option>Newest</option>
<option>Best Selling</option>
</select>
</div>
<div class={[
"grid gap-6 grid-cols-1 sm:grid-cols-2",
<!-- Product Grid -->
<div class={[
"product-grid grid grid-cols-1 sm:grid-cols-2",
case @theme_settings.grid_columns do
"2" -> "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;"
>
<div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative">
<div class="product-image-container bg-gray-200 overflow-hidden relative">
<!-- Product Badge -->
<%= if product.on_sale do %>
<span class="product-badge badge-sale">Sale</span>
<% end %>
<%= if not product.in_stock do %>
<div class="absolute inset-0 bg-black bg-opacity-50 flex items-center justify-center z-20">
<span class="text-white font-semibold">Out of Stock</span>
</div>
<%= cond do %>
<% not product.in_stock -> %>
<span class="product-badge badge-sold-out">Sold out</span>
<% product.on_sale -> %>
<span class="product-badge badge-sale">Sale</span>
<% true -> %>
<% end %>
<!-- Primary Image -->
<img
@ -136,7 +80,7 @@
class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
/>
<!-- Hover Image -->
<%= if product[:hover_image_url] do %>
<%= if @theme_settings.hover_image && product[:hover_image_url] do %>
<img
src={product.hover_image_url}
alt={product.name}
@ -144,14 +88,14 @@
/>
<% end %>
</div>
<div class="p-4">
<div>
<p class="text-xs mb-1" style="color: var(--t-text-tertiary);">
<%= product.category %>
</p>
<h3 class="font-semibold mb-2" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
<%= product.name %>
</h3>
<div class="flex items-center justify-between">
<%= if @theme_settings.show_prices do %>
<div>
<%= if product.on_sale do %>
<span class="text-lg font-bold" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
@ -166,21 +110,11 @@
</span>
<% end %>
</div>
<%= if product.in_stock do %>
<button
class="quick-add-btn px-3 py-1.5 text-sm font-medium transition-all"
style="background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); border-radius: var(--t-radius-button);"
>
Quick add
</button>
<% end %>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
</div>
</div>
</div>
<!-- Footer -->

View File

@ -13,7 +13,7 @@
</h1>
<p class="text-lg mb-12 text-center max-w-2xl mx-auto" style="color: var(--t-text-secondary);">
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.
</p>
<div class="grid gap-8 md:grid-cols-2 mb-12">
@ -87,44 +87,96 @@
<!-- Contact Info -->
<div class="space-y-6">
<!-- Order Tracking -->
<div
class="p-6"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
>
<h3 class="font-bold mb-2" style="color: var(--t-text-primary);">Email</h3>
<p style="color: var(--t-text-secondary);">hello@example.com</p>
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Track your order</h3>
<p class="text-sm mb-3" style="color: var(--t-text-secondary);">
Enter your email and we'll send you a link to check your order status.
</p>
<div class="flex flex-wrap gap-2">
<input
type="email"
placeholder="your@email.com"
class="flex-1 min-w-0 px-3 py-2 text-sm"
style="background-color: var(--t-surface-base); color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-input); min-width: 150px;"
/>
<button
class="px-4 py-2 text-sm font-medium whitespace-nowrap"
style="background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: var(--t-text-inverse); border-radius: var(--t-radius-button);"
>
Send
</button>
</div>
</div>
<!-- Helpful info -->
<div
class="p-6"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
>
<h3 class="font-bold mb-2" style="color: var(--t-text-primary);">Phone</h3>
<p style="color: var(--t-text-secondary);">(555) 123-4567</p>
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Handy to know</h3>
<ul class="space-y-2 text-sm" style="color: var(--t-text-secondary);">
<li class="flex items-start gap-2">
<span style="color: var(--t-text-tertiary);">•</span>
<span><strong style="color: var(--t-text-primary);">Printing:</strong> 2-5 business days</span>
</li>
<li class="flex items-start gap-2">
<span style="color: var(--t-text-tertiary);">•</span>
<span><strong style="color: var(--t-text-primary);">Delivery:</strong> 3-7 business days after printing</span>
</li>
<li class="flex items-start gap-2">
<span style="color: var(--t-text-tertiary);">•</span>
<span><strong style="color: var(--t-text-primary);">Returns:</strong> Happy to help with faulty or damaged items</span>
</li>
</ul>
</div>
<!-- Get in touch -->
<div
class="p-6"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
>
<h3 class="font-bold mb-2" style="color: var(--t-text-primary);">Address</h3>
<p style="color: var(--t-text-secondary);">
123 Main Street<br />
San Francisco, CA 94102<br />
United States
<h3 class="font-bold mb-3" style="color: var(--t-text-primary);">Get in touch</h3>
<a href="mailto:hello@example.com" class="flex items-center gap-2 mb-2" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); text-decoration: none;">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
hello@example.com
</a>
<p class="text-sm" style="color: var(--t-text-secondary);">
We typically respond within 24 hours
</p>
</div>
<div
class="p-6"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
>
<h3 class="font-bold mb-2" style="color: var(--t-text-primary);">Hours</h3>
<p style="color: var(--t-text-secondary);">
Monday - Friday: 9am - 6pm<br />
Saturday: 10am - 4pm<br />
Sunday: Closed
</p>
<!-- Social links -->
<div class="flex gap-4 justify-center">
<a
href="#"
class="social-link w-9 h-9 flex items-center justify-center transition-all"
style="color: var(--t-text-secondary); border-radius: var(--t-radius-button);"
aria-label="Instagram"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="2" y="2" width="20" height="20" rx="5" ry="5"></rect>
<path d="M16 11.37A4 4 0 1 1 12.63 8 4 4 0 0 1 16 11.37z"></path>
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5"></line>
</svg>
</a>
<a
href="#"
class="social-link w-9 h-9 flex items-center justify-center transition-all"
style="color: var(--t-text-secondary); border-radius: var(--t-radius-button);"
aria-label="Pinterest"
>
<svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<path d="M8 12c0-2.2 1.8-4 4-4s4 1.8 4 4-1.8 4-4 4"></path>
<line x1="12" y1="16" x2="9" y2="21"></line>
</svg>
</a>
</div>
</div>
</div>

View File

@ -37,26 +37,35 @@
</button>
</div>
<div class="mt-12 grid gap-4 grid-cols-2 md:grid-cols-4 max-w-xl mx-auto">
<div class="product-grid mt-12 grid gap-4 grid-cols-2 md:grid-cols-4 max-w-xl mx-auto">
<%= for product <- Enum.take(@preview_data.products, 4) do %>
<div
class="product-card group overflow-hidden"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-subtle); border-radius: var(--t-radius-card);"
>
<div class="aspect-square bg-gray-200 overflow-hidden">
<div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative">
<img
src={product.image_url}
alt={product.name}
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
/>
<%= if @theme_settings.hover_image && product[:hover_image_url] do %>
<img
src={product.hover_image_url}
alt={product.name}
class="product-image-hover w-full h-full object-cover"
/>
<% end %>
</div>
<div class="p-2">
<p class="text-xs font-semibold truncate" style="color: var(--t-text-primary);">
<%= product.name %>
</p>
<p class="text-xs" style="color: var(--t-text-secondary);">
£<%= product.price / 100 %>
</p>
<%= if @theme_settings.show_prices do %>
<p class="text-xs" style="color: var(--t-text-secondary);">
£<%= product.price / 100 %>
</p>
<% end %>
</div>
</div>
<% end %>

View File

@ -49,7 +49,7 @@
</h2>
<div class={[
"grid gap-6 grid-cols-1 sm:grid-cols-2",
"product-grid grid grid-cols-1 sm:grid-cols-2",
case @theme_settings.grid_columns do
"2" -> "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;"
>
<div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative">
<div class="product-image-container bg-gray-200 overflow-hidden relative">
<%= if product[:is_new] do %>
<span class="product-badge badge-new">New</span>
<% 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 %>
<img
src={product.hover_image_url}
alt={product.name}
class="product-image-hover w-full h-full object-cover"
/>
<% end %>
<button
class="absolute bottom-2 left-2 right-2 px-3 py-2 text-sm font-medium opacity-0 translate-y-2 group-hover:opacity-100 group-hover:translate-y-0 transition-all"
style="background-color: var(--t-surface-raised); color: var(--t-text-primary); border-radius: var(--t-radius-button);"
>
Quick add
</button>
</div>
<div style="padding: var(--space-md);">
<div>
<h3 class="text-sm font-medium mb-1" style="color: var(--t-text-primary);">
<%= product.name %>
</h3>
<p class="text-sm" style="color: var(--t-text-secondary);">
<%= if product.on_sale do %>
<span class="line-through mr-1" style="color: var(--t-text-tertiary);">£<%= product.compare_at_price / 100 %></span>
<% end %>
£<%= product.price / 100 %>
</p>
<%= if @theme_settings.show_prices do %>
<p class="text-sm" style="color: var(--t-text-secondary);">
<%= if product.on_sale do %>
<span class="line-through mr-1" style="color: var(--t-text-tertiary);">£<%= product.compare_at_price / 100 %></span>
<% end %>
£<%= product.price / 100 %>
</p>
<% end %>
</div>
</div>
<% end %>

View File

@ -12,35 +12,125 @@
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- Breadcrumb -->
<div class="mb-8 flex items-center gap-2 text-sm" style="color: var(--t-text-secondary);">
<span>Home</span>
<nav class="mb-8 flex items-center gap-2 text-sm" style="color: var(--t-text-secondary);">
<a href="#" phx-click="change_preview_page" phx-value-page="home" class="hover:underline">Home</a>
<span>/</span>
<span><%= product.category %></span>
<a href="#" phx-click="change_preview_page" phx-value-page="collection" class="hover:underline"><%= product.category %></a>
<span>/</span>
<span style="color: var(--t-text-primary);"><%= product.name %></span>
</div>
</nav>
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16">
<!-- Product Images -->
<div>
<div class="aspect-square bg-gray-200 mb-4 overflow-hidden" style="border-radius: var(--t-radius-image);">
<% gallery_images = [product.image_url, product.hover_image_url, product.image_url, product.hover_image_url] %>
<div
class="pdp-main-image-container aspect-square bg-gray-200 mb-4 overflow-hidden relative"
style="border-radius: var(--t-radius-image);"
phx-click={Phoenix.LiveView.JS.exec("data-show", to: "#pdp-lightbox")}
>
<img
id="pdp-main-image"
src={product.image_url}
alt={product.name}
class="w-full h-full object-cover"
/>
<!-- Zoom icon overlay -->
<div class="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity bg-black/10">
<div class="w-12 h-12 bg-white/90 rounded-full flex items-center justify-center shadow-lg">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0zM10 7v3m0 0v3m0-3h3m-3 0H7" />
</svg>
</div>
</div>
</div>
<div class="grid grid-cols-4 gap-4">
<%= for img_url <- [product.image_url, product.hover_image_url, product.image_url, product.hover_image_url] do %>
<div class="aspect-square bg-gray-200 cursor-pointer overflow-hidden" style="border: 2px solid var(--t-border-default); border-radius: var(--t-radius-image);">
<%= for {img_url, idx} <- Enum.with_index(gallery_images) do %>
<button
type="button"
class={"aspect-square bg-gray-200 cursor-pointer overflow-hidden pdp-thumbnail#{if idx == 0, do: " pdp-thumbnail-active", else: ""}"}
style="border-radius: var(--t-radius-image);"
data-index={idx}
phx-click={Phoenix.LiveView.JS.set_attribute({"src", img_url}, to: "#pdp-main-image")
|> Phoenix.LiveView.JS.set_attribute({"data-current-index", to_string(idx)}, to: "#pdp-lightbox")
|> Phoenix.LiveView.JS.remove_class("pdp-thumbnail-active", to: ".pdp-thumbnail")
|> Phoenix.LiveView.JS.add_class("pdp-thumbnail-active")}
>
<img
src={img_url}
alt={product.name}
class="w-full h-full object-cover"
/>
</div>
</button>
<% end %>
</div>
<style>
.pdp-thumbnail {
border: 2px solid var(--t-border-default);
transition: border-color 0.15s ease;
}
.pdp-thumbnail-active {
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
}
</style>
<!-- Image Lightbox -->
<dialog
class="lightbox"
id="pdp-lightbox"
aria-label="Product image gallery"
data-current-index="0"
data-images={Jason.encode!(gallery_images)}
data-show={Phoenix.LiveView.JS.exec("phx-show-lightbox", to: "#pdp-lightbox")}
phx-show-lightbox={Phoenix.LiveView.JS.dispatch("pdp:open-lightbox", to: "#pdp-lightbox")}
phx-hook="Lightbox"
>
<div class="lightbox-content">
<button
type="button"
class="lightbox-close"
aria-label="Close gallery"
phx-click={Phoenix.LiveView.JS.dispatch("pdp:close-lightbox", to: "#pdp-lightbox")}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
<button
type="button"
class="lightbox-nav lightbox-prev"
aria-label="Previous image"
phx-click={Phoenix.LiveView.JS.dispatch("pdp:prev-image", to: "#pdp-lightbox")}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<figure class="lightbox-figure">
<div class="lightbox-image-container">
<img
class="lightbox-image"
id="lightbox-image"
src={product.image_url}
alt={product.name}
/>
</div>
<figcaption class="lightbox-caption"><%= product.name %></figcaption>
</figure>
<button
type="button"
class="lightbox-nav lightbox-next"
aria-label="Next image"
phx-click={Phoenix.LiveView.JS.dispatch("pdp:next-image", to: "#pdp-lightbox")}
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
<div class="lightbox-counter" id="lightbox-counter">1 / <%= length(gallery_images) %></div>
</div>
</dialog>
</div>
<!-- Product Info -->
@ -118,61 +208,151 @@
Add to basket
</button>
<!-- Features -->
<div class="p-4 space-y-3" style="background-color: var(--t-surface-sunken); border-radius: var(--t-radius-card);">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<div>
<p class="font-semibold" style="color: var(--t-text-primary);">Free Delivery</p>
<p class="text-sm" style="color: var(--t-text-secondary);">On orders over £40</p>
<!-- Features / Trust Badges -->
<%= if @theme_settings.pdp_trust_badges do %>
<div class="p-4 space-y-3" style="background-color: var(--t-surface-sunken); border-radius: var(--t-radius-card);">
<div class="flex items-start gap-3">
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
</svg>
<div>
<p class="font-semibold" style="color: var(--t-text-primary);">Free Delivery</p>
<p class="text-sm" style="color: var(--t-text-secondary);">On orders over £40</p>
</div>
</div>
</div>
<div class="flex items-start gap-3">
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p class="font-semibold" style="color: var(--t-text-primary);">Easy Returns</p>
<p class="text-sm" style="color: var(--t-text-secondary);">30-day return policy</p>
</div>
</div>
</div>
</div>
</div>
<!-- Related Products -->
<div>
<h2 class="text-2xl font-bold mb-6" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
You might also like
</h2>
<div class="grid gap-6 grid-cols-2 md:grid-cols-4">
<%= for related_product <- Enum.slice(@preview_data.products, 1, 4) do %>
<div
class="product-card group overflow-hidden"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
>
<div class="aspect-square bg-gray-200 overflow-hidden">
<img
src={related_product.image_url}
alt={related_product.name}
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
/>
</div>
<div class="p-3">
<h3 class="font-semibold text-sm mb-1" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
<%= related_product.name %>
</h3>
<p class="font-bold" style="color: var(--t-text-primary);">
$<%= related_product.price / 100 %>
</p>
<div class="flex items-start gap-3">
<svg class="w-5 h-5 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p class="font-semibold" style="color: var(--t-text-primary);">Easy Returns</p>
<p class="text-sm" style="color: var(--t-text-secondary);">30-day return policy</p>
</div>
</div>
</div>
<% end %>
</div>
</div>
<!-- Reviews Section -->
<%= if @theme_settings.pdp_reviews do %>
<div class="pdp-reviews py-12" style="border-top: 1px solid var(--t-border-default);">
<div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4 mb-8">
<h2 class="text-2xl font-bold" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
Customer reviews
</h2>
<div class="flex items-center gap-3">
<div class="flex gap-0.5">
<%= for _i <- 1..5 do %>
<svg class="w-5 h-5" viewBox="0 0 20 20" style="color: #f59e0b;">
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" fill="currentColor"/>
</svg>
<% end %>
</div>
<span class="text-sm" style="color: var(--t-text-secondary);">Based on 24 reviews</span>
</div>
</div>
<div class="space-y-6">
<!-- Review 1 -->
<article class="pb-6" style="border-bottom: 1px solid var(--t-border-subtle);">
<div class="flex items-center justify-between mb-2">
<div class="flex gap-0.5">
<%= for _i <- 1..5 do %>
<svg class="w-4 h-4" viewBox="0 0 20 20" style="color: #f59e0b;">
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" fill="currentColor"/>
</svg>
<% end %>
</div>
<span class="text-xs" style="color: var(--t-text-tertiary);">2 weeks ago</span>
</div>
<h3 class="font-semibold mb-1" style="color: var(--t-text-primary);">Absolutely beautiful</h3>
<p class="text-sm mb-3" style="color: var(--t-text-secondary); line-height: 1.6;">
The quality exceeded my expectations. The colours are vibrant and the paper feels premium. It's now pride of place in my living room.
</p>
<div class="flex items-center gap-2">
<span class="text-sm font-medium" style="color: var(--t-text-primary);">Sarah M.</span>
<span class="text-xs px-2 py-0.5 rounded" style="background-color: var(--t-surface-sunken); color: var(--t-text-tertiary);">Verified purchase</span>
</div>
</article>
<!-- Review 2 -->
<article class="pb-6" style="border-bottom: 1px solid var(--t-border-subtle);">
<div class="flex items-center justify-between mb-2">
<div class="flex gap-0.5">
<%= for i <- 1..5 do %>
<svg class="w-4 h-4" viewBox="0 0 20 20" style={"color: #{if i <= 4, do: "#f59e0b", else: "var(--t-border-default)"};"}>
<path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" fill="currentColor"/>
</svg>
<% end %>
</div>
<span class="text-xs" style="color: var(--t-text-tertiary);">1 month ago</span>
</div>
<h3 class="font-semibold mb-1" style="color: var(--t-text-primary);">Great gift</h3>
<p class="text-sm mb-3" style="color: var(--t-text-secondary); line-height: 1.6;">
Bought this as a gift and it arrived beautifully packaged. Fast shipping too. Would definitely order again.
</p>
<div class="flex items-center gap-2">
<span class="text-sm font-medium" style="color: var(--t-text-primary);">James T.</span>
<span class="text-xs px-2 py-0.5 rounded" style="background-color: var(--t-surface-sunken); color: var(--t-text-tertiary);">Verified purchase</span>
</div>
</article>
</div>
<button
class="mt-6 px-6 py-2 text-sm font-medium transition-all mx-auto block"
style="background-color: transparent; color: var(--t-text-primary); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-button);"
>
Load more reviews
</button>
</div>
<% end %>
<!-- Related Products -->
<%= if @theme_settings.pdp_related_products do %>
<div class="py-12" style="border-top: 1px solid var(--t-border-default);">
<h2 class="text-2xl font-bold mb-6" style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight);">
You might also like
</h2>
<div class="product-grid grid gap-6 grid-cols-2 md:grid-cols-4">
<%= for related_product <- Enum.slice(@preview_data.products, 1, 4) do %>
<a
href="#"
phx-click="change_preview_page"
phx-value-page="pdp"
class="product-card group overflow-hidden cursor-pointer"
style="background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
>
<div class="product-image-container aspect-square bg-gray-200 overflow-hidden relative">
<img
src={related_product.image_url}
alt={related_product.name}
class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
/>
<%= if @theme_settings.hover_image && related_product[:hover_image_url] do %>
<img
src={related_product.hover_image_url}
alt={related_product.name}
class="product-image-hover w-full h-full object-cover"
/>
<% end %>
</div>
<div class="p-3">
<h3 class="font-semibold text-sm mb-1" style="font-family: var(--t-font-heading); color: var(--t-text-primary);">
<%= related_product.name %>
</h3>
<%= if @theme_settings.show_prices do %>
<p class="font-bold" style="color: var(--t-text-primary);">
£<%= related_product.price / 100 %>
</p>
<% end %>
</div>
</a>
<% end %>
</div>
</div>
<% end %>
</div>
<!-- Footer -->