extract product.ex inline styles to CSS component classes (Phase 2)

Move ~80 inline style= attributes from product.ex into ~40 CSS classes
in @layer components. Only genuinely dynamic values (hex colours,
background-image URLs) remain as inline styles. Pre-declare CSS layer
order in shop_root.html.heex so reset < components in the cascade.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-02-17 00:13:03 +00:00
parent fcd1b1ce80
commit 2af2d782d5
3 changed files with 410 additions and 214 deletions

View File

@ -1,11 +1,327 @@
/* Component styles extracted from inline styles in later phases.
/* Component styles extracted from inline styles in product.ex, layout.ex, etc.
Each component gets its own section. */
@layer components {
/* Phase 2: product cards, grid, badges, hero, categories */
/* Phase 2: PDP, variant selector, gallery, accordion */
/* Phase 3: layout components (header, footer, nav, search) */
/* Phase 3: cart components (drawer, items, summary) */
/* Phase 4: content components (contact, reviews, newsletter) */
/* Phase 4: page templates (checkout success, etc.) */
/* Shared heading treatment
font-family + weight + tracking + colour used across
hero titles, section headings, collection headers, PDP, etc. */
.t-heading {
font-family: var(--t-font-heading);
font-weight: var(--t-heading-weight);
letter-spacing: var(--t-heading-tracking);
color: var(--t-text-primary);
}
/* ── Product card ── */
.product-card {
background-color: var(--t-surface-raised);
border-radius: var(--t-radius-card);
&[data-variant="default"],
&[data-variant="compact"] {
border: 1px solid var(--t-border-default);
}
&[data-variant="minimal"] {
border: 1px solid var(--t-border-subtle);
}
&[data-variant="default"],
&[data-variant="featured"] {
cursor: pointer;
}
&[data-clickable] {
position: relative;
}
& .stretched-link {
color: inherit;
text-decoration: none;
}
}
.product-card-image-wrap {
z-index: 1;
}
.product-card-placeholder {
color: var(--t-text-tertiary);
}
.product-card-category {
color: var(--t-text-tertiary);
text-decoration: none;
position: relative;
z-index: 1;
}
.product-card-title {
color: var(--t-text-primary);
}
.product-card[data-variant="default"] .product-card-title,
.product-card[data-variant="compact"] .product-card-title {
font-family: var(--t-font-heading);
}
.product-card-delivery {
color: var(--t-text-tertiary);
}
/* ── Product prices (shared between cards and PDP) ── */
.product-price--sale {
color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
}
.product-price--compare {
color: var(--t-text-tertiary);
}
.product-price--regular {
color: var(--t-text-primary);
}
.product-price--secondary {
color: var(--t-text-secondary);
}
.sale-badge {
background-color: var(--t-sale-color);
}
/* ── Hero section ── */
.hero-section {
padding: var(--space-2xl) var(--space-lg);
&[data-background="base"] {
background-color: var(--t-surface-base);
}
&[data-background="sunken"] {
background-color: var(--t-surface-sunken);
}
}
.hero-section--page {
padding-top: var(--space-2xl);
}
.hero-pre-title {
font-family: var(--t-font-heading);
font-weight: var(--t-heading-weight);
color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
}
.hero-description {
color: var(--t-text-secondary);
line-height: 1.6;
}
/* ── Category nav ── */
.category-nav-section {
padding: var(--space-xl) var(--space-lg);
background-color: var(--t-surface-base);
}
.category-card {
text-decoration: none;
cursor: pointer;
}
.category-name {
font-family: var(--t-font-body);
color: var(--t-text-primary);
}
/* ── Featured products section ── */
.featured-section {
padding: var(--space-xl) var(--space-lg);
background-color: var(--t-surface-sunken);
}
.outline-button {
background-color: transparent;
color: var(--t-text-primary);
border: 1px solid var(--t-text-primary);
border-radius: var(--t-radius-button);
cursor: pointer;
text-decoration: none;
}
/* ── Image + text section ── */
.image-text-section {
padding: var(--space-2xl) var(--space-lg);
background-color: var(--t-surface-base);
}
.image-text-image {
border-radius: var(--t-radius-image);
}
.image-text-body {
color: var(--t-text-secondary);
line-height: 1.7;
}
.accent-link {
color: var(--t-accent-text, hsl(var(--t-accent-h) var(--t-accent-s) 38%));
text-decoration: none;
cursor: pointer;
}
/* ── Collection header ── */
.collection-header-wrap {
background-color: var(--t-surface-raised);
border-color: var(--t-border-default);
}
.collection-header-meta {
color: var(--t-text-secondary);
}
/* ── Breadcrumb ── */
.breadcrumb {
color: var(--t-text-secondary);
& [aria-current="page"] {
color: var(--t-text-primary);
}
}
/* ── Related products ── */
.related-section {
border-top: 1px solid var(--t-border-default);
}
/* ── PDP gallery ── */
.pdp-gallery-frame {
border-radius: var(--t-radius-image);
overflow: hidden;
}
.pdp-thumbnail {
border-radius: var(--t-radius-image);
}
/* ── Variant selector ── */
.variant-label {
color: var(--t-text-primary);
}
.variant-label-value {
color: var(--t-text-secondary);
font-weight: normal;
}
.color-swatch {
border-color: var(--t-border-default);
&[aria-pressed="true"] {
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
--tw-ring-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
}
}
.size-btn {
border: 2px solid var(--t-border-default);
border-radius: var(--t-radius-button);
color: var(--t-text-primary);
background: transparent;
&[aria-pressed="true"] {
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
background: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.1);
}
}
/* ── Quantity selector ── */
.qty-label {
color: var(--t-text-primary);
}
.qty-group {
border: 2px solid var(--t-border-default);
border-radius: var(--t-radius-input);
}
.qty-btn {
color: var(--t-text-primary);
}
.qty-display {
border-color: var(--t-border-default);
color: var(--t-text-primary);
}
.stock-in {
color: var(--t-text-tertiary);
}
.stock-out {
color: var(--t-sale-color);
}
/* ── Add to cart ── */
.atc-wrap {
background-color: var(--t-surface-base);
border-color: var(--t-border-subtle);
}
.atc-btn {
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);
border: none;
cursor: pointer;
&:disabled {
background-color: var(--t-border-default);
cursor: not-allowed;
}
}
/* ── Accordion ── */
.accordion-summary {
color: var(--t-text-primary);
}
.accordion-body {
color: var(--t-text-secondary);
}
/* ── Product details ── */
.details-wrap {
border-top: 1px solid var(--t-border-subtle);
border-bottom: 1px solid var(--t-border-subtle);
border-color: var(--t-border-subtle);
}
.details-table-row {
border-bottom: 1px solid var(--t-border-subtle);
}
.details-th {
color: var(--t-text-primary);
}
.details-subheading {
color: var(--t-text-primary);
}
}

View File

@ -19,6 +19,10 @@
) do %>
<link rel="preload" href={preload.href} as="font" type="font/woff2" crossorigin />
<% end %>
<!-- Pre-declare layer order so reset < components regardless of load order -->
<style>
@layer properties, reset, primitives, tokens, theme, base, components, layout, utilities, overrides;
</style>
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app-shop.css"} />
<link phx-track-static rel="stylesheet" href={~p"/assets/css/shop.css"} />
<script defer phx-track-static src={~p"/assets/js/app.js"}>

View File

@ -63,7 +63,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
~H"""
<article
class={card_classes(@variant)}
style={card_style(@variant) <> if(@clickable_resolved, do: " position: relative;", else: "")}
data-variant={@variant}
data-clickable={@clickable_resolved || nil}
>
<.product_card_inner
product={@product}
@ -102,7 +103,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|> assign(:has_hover_image, assigns.theme_settings.hover_image && hover_image != nil)
~H"""
<div class={image_container_classes(@variant)} style="z-index: 1;">
<div class={["product-card-image-wrap", image_container_classes(@variant)]}>
<%= if @show_badges do %>
<.product_badge product={@product} />
<% end %>
@ -145,23 +146,19 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<div class={content_padding_class(@variant)}>
<%= if @show_category && Map.get(@product, :category) do %>
<%= if @mode == :preview do %>
<p
class="text-xs mb-1"
style="color: var(--t-text-tertiary); position: relative; z-index: 1;"
>
<p class="product-card-category text-xs mb-1">
{@product.category}
</p>
<% else %>
<.link
navigate={"/collections/#{Slug.slugify(@product.category)}"}
class="text-xs mb-1 block hover:underline"
style="color: var(--t-text-tertiary); text-decoration: none; position: relative; z-index: 1;"
class="product-card-category text-xs mb-1 block hover:underline"
>
{@product.category}
</.link>
<% end %>
<% end %>
<h3 class={title_classes(@variant)} style={title_style(@variant)}>
<h3 class={["product-card-title", title_classes(@variant)]}>
<%= if @clickable do %>
<%= if @mode == :preview do %>
<a
@ -169,7 +166,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
phx-click="change_preview_page"
phx-value-page="pdp"
class="stretched-link"
style="color: inherit; text-decoration: none;"
>
{@product.title}
</a>
@ -177,7 +173,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<.link
navigate={"/products/#{Map.get(@product, :slug) || Map.get(@product, :id)}"}
class="stretched-link"
style="color: inherit; text-decoration: none;"
>
{@product.title}
</.link>
@ -190,7 +185,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<.product_price product={@product} variant={@variant} />
<% end %>
<%= if @show_delivery_text do %>
<p class="text-xs mt-1" style="color: var(--t-text-tertiary);">
<p class="product-card-delivery text-xs mt-1">
Made to order
</p>
<% end %>
@ -231,8 +226,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<%= cond do %>
<% is_nil(@src) -> %>
<div
class={[@class, "flex items-center justify-center"]}
style="color: var(--t-text-tertiary);"
class={[@class, "product-card-placeholder flex items-center justify-center"]}
role="img"
aria-label={@alt}
>
@ -303,36 +297,33 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<% :default -> %>
<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));"
>
<span class="product-price--sale text-lg font-bold">
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
</span>
<span class="text-sm line-through ml-2" style="color: var(--t-text-tertiary);">
<span class="product-price--compare text-sm line-through ml-2">
{SimpleshopTheme.Cart.format_price(@product.compare_at_price)}
</span>
<% else %>
<span class="text-lg font-bold" style="color: var(--t-text-primary);">
<span class="product-price--regular text-lg font-bold">
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
</span>
<% end %>
</div>
<% :featured -> %>
<p class="text-sm" style="color: var(--t-text-secondary);">
<p class="product-price--secondary text-sm">
<%= if @product.on_sale do %>
<span class="line-through mr-1" style="color: var(--t-text-tertiary);">
<span class="product-price--compare line-through mr-1">
{SimpleshopTheme.Cart.format_price(@product.compare_at_price)}
</span>
<% end %>
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
</p>
<% :compact -> %>
<p class="font-bold" style="color: var(--t-text-primary);">
<p class="product-price--regular font-bold">
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
</p>
<% :minimal -> %>
<p class="text-xs" style="color: var(--t-text-secondary);">
<p class="product-price--secondary text-xs">
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
</p>
<% end %>
@ -359,22 +350,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
defp card_classes(:compact), do: "product-card group overflow-hidden cursor-pointer"
defp card_classes(:minimal), do: "product-card group overflow-hidden"
defp card_style(:default),
do:
"background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card); cursor: pointer;"
defp card_style(:featured),
do:
"background-color: var(--t-surface-raised); border-radius: var(--t-radius-card); cursor: pointer;"
defp card_style(:compact),
do:
"background-color: var(--t-surface-raised); border: 1px solid var(--t-border-default); border-radius: var(--t-radius-card);"
defp card_style(:minimal),
do:
"background-color: var(--t-surface-raised); border: 1px solid var(--t-border-subtle); border-radius: var(--t-radius-card);"
defp image_container_classes(:compact),
do: "product-image-container aspect-square bg-gray-200 overflow-hidden relative"
@ -393,16 +368,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
defp title_classes(:compact), do: "font-semibold text-sm mb-1"
defp title_classes(:minimal), do: "text-xs font-semibold truncate"
defp title_style(:default),
do: "font-family: var(--t-font-heading); color: var(--t-text-primary);"
defp title_style(:featured), do: "color: var(--t-text-primary);"
defp title_style(:compact),
do: "font-family: var(--t-font-heading); color: var(--t-text-primary);"
defp title_style(:minimal), do: "color: var(--t-text-primary);"
@doc """
Renders a responsive product grid container.
@ -544,20 +509,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
~H"""
<%= case @variant do %>
<% :default -> %>
<section
class="text-center"
style={"padding: var(--space-2xl) var(--space-lg); background-color: var(--t-surface-#{@background});"}
>
<h1
class="text-3xl md:text-4xl mb-4"
style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking); color: var(--t-text-primary);"
>
<section class="hero-section text-center" data-background={@background}>
<h1 class="t-heading text-3xl md:text-4xl mb-4">
{@title}
</h1>
<p
class="text-lg max-w-lg mx-auto mb-8"
style="color: var(--t-text-secondary); line-height: 1.6;"
>
<p class="hero-description text-lg max-w-lg mx-auto mb-8">
{@description}
</p>
<.hero_cta
@ -570,34 +526,25 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
/>
</section>
<% :page -> %>
<div class="text-center" style="padding-top: var(--space-2xl);">
<h1
class="text-4xl md:text-5xl font-bold mb-6"
style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);"
>
<div class="hero-section--page text-center">
<h1 class="t-heading text-4xl md:text-5xl mb-6">
{@title}
</h1>
<p class="text-lg mb-12 max-w-2xl mx-auto" style="color: var(--t-text-secondary);">
<p class="hero-description text-lg mb-12 max-w-2xl mx-auto">
{@description}
</p>
</div>
<% :error -> %>
<div class="text-center">
<%= if @pre_title do %>
<h1
class="text-8xl md:text-9xl font-bold mb-4"
style="font-family: var(--t-font-heading); color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); font-weight: var(--t-heading-weight);"
>
<h1 class="hero-pre-title text-8xl md:text-9xl mb-4">
{@pre_title}
</h1>
<% end %>
<h2
class="text-3xl md:text-4xl font-bold mb-6"
style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);"
>
<h2 class="t-heading text-3xl md:text-4xl mb-6">
{@title}
</h2>
<p class="text-lg mb-8 max-w-md mx-auto" style="color: var(--t-text-secondary);">
<p class="hero-description text-lg mb-8 max-w-md mx-auto">
{@description}
</p>
<%= if @cta_text || @secondary_cta_text do %>
@ -638,7 +585,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
phx-click="change_preview_page"
phx-value-page={@page}
class={hero_cta_classes(@variant)}
style={hero_cta_style(@variant)}
>
{@text}
</button>
@ -646,7 +592,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<.link
navigate={@href || "/"}
class={["inline-block", hero_cta_classes(@variant)]}
style={hero_cta_style(@variant) <> " text-decoration: none;"}
>
{@text}
</.link>
@ -659,9 +604,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
defp hero_cta_classes(:secondary),
do: "themed-button-outline px-8 py-3 font-semibold transition-all"
defp hero_cta_style(:primary), do: ""
defp hero_cta_style(:secondary), do: ""
@doc """
Renders a row of category circles for navigation.
@ -682,7 +624,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def category_nav(assigns) do
~H"""
<section style="padding: var(--space-xl) var(--space-lg); background-color: var(--t-surface-base);">
<section class="category-nav-section">
<h2 class="sr-only">Shop by Category</h2>
<nav class="grid grid-cols-3 gap-4 max-w-3xl mx-auto" aria-label="Product categories">
<%= for category <- Enum.take(@categories, @limit) do %>
@ -691,8 +633,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
href="#"
phx-click="change_preview_page"
phx-value-page="collection"
class="flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5"
style="text-decoration: none; cursor: pointer;"
class="category-card flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5"
>
<div
class="w-24 h-24 rounded-full bg-gray-200 bg-cover bg-center transition-transform hover:scale-105"
@ -704,18 +645,14 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
}
>
</div>
<span
class="text-sm font-medium"
style="font-family: var(--t-font-body); color: var(--t-text-primary);"
>
<span class="category-name text-sm font-medium">
{category.name}
</span>
</a>
<% else %>
<.link
navigate={"/collections/#{category.slug}"}
class="flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5"
style="text-decoration: none;"
class="category-card flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5"
>
<div
class="w-24 h-24 rounded-full bg-gray-200 bg-cover bg-center transition-transform hover:scale-105"
@ -727,10 +664,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
}
>
</div>
<span
class="text-sm font-medium"
style="font-family: var(--t-font-body); color: var(--t-text-primary);"
>
<span class="category-name text-sm font-medium">
{category.name}
</span>
</.link>
@ -775,11 +709,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def featured_products_section(assigns) do
~H"""
<section style="padding: var(--space-xl) var(--space-lg); background-color: var(--t-surface-sunken);">
<h2
class="text-2xl mb-6"
style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking); color: var(--t-text-primary);"
>
<section class="featured-section">
<h2 class="t-heading text-2xl mb-6">
{@title}
</h2>
@ -800,16 +731,14 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<button
phx-click="change_preview_page"
phx-value-page={@cta_page}
class="px-6 py-3 font-medium transition-all"
style="background-color: transparent; color: var(--t-text-primary); border: 1px solid var(--t-text-primary); border-radius: var(--t-radius-button); cursor: pointer;"
class="outline-button px-6 py-3 font-medium transition-all"
>
{@cta_text}
</button>
<% else %>
<.link
navigate={@cta_href}
class="inline-block px-6 py-3 font-medium transition-all"
style="background-color: transparent; color: var(--t-text-primary); border: 1px solid var(--t-text-primary); border-radius: var(--t-radius-button); text-decoration: none;"
class="outline-button inline-block px-6 py-3 font-medium transition-all"
>
{@cta_text}
</.link>
@ -855,10 +784,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def image_text_section(assigns) do
~H"""
<section
class="grid grid-cols-1 md:grid-cols-2 gap-12 items-center"
style="padding: var(--space-2xl) var(--space-lg); background-color: var(--t-surface-base);"
>
<section class="image-text-section grid grid-cols-1 md:grid-cols-2 gap-12 items-center">
<%= if @image_position == :left do %>
<.image_text_image image_url={@image_url} />
<.image_text_content
@ -889,8 +815,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
defp image_text_image(assigns) do
~H"""
<div
class="h-72 rounded-lg bg-cover bg-center"
style={"background-image: url('#{@image_url}'); border-radius: var(--t-radius-image);"}
class="image-text-image h-72 bg-cover bg-center"
style={"background-image: url('#{@image_url}');"}
>
</div>
"""
@ -906,13 +832,10 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
defp image_text_content(assigns) do
~H"""
<div>
<h2
class="text-2xl mb-4"
style="font-family: var(--t-font-heading); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking); color: var(--t-text-primary);"
>
<h2 class="t-heading text-2xl mb-4">
{@title}
</h2>
<p class="text-base mb-4" style="color: var(--t-text-secondary); line-height: 1.7;">
<p class="image-text-body text-base mb-4">
{@description}
</p>
<%= if @link_text do %>
@ -921,16 +844,14 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
href="#"
phx-click="change_preview_page"
phx-value-page={@link_page}
class="text-sm font-medium transition-colors"
style="color: var(--t-accent-text, hsl(var(--t-accent-h) var(--t-accent-s) 38%)); text-decoration: none; cursor: pointer;"
class="accent-link text-sm font-medium transition-colors"
>
{@link_text}
</a>
<% else %>
<.link
navigate={@link_href || "/"}
class="text-sm font-medium transition-colors"
style="color: var(--t-accent-text, hsl(var(--t-accent-h) var(--t-accent-s) 38%)); text-decoration: none;"
class="accent-link text-sm font-medium transition-colors"
>
{@link_text}
</.link>
@ -959,22 +880,16 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def collection_header(assigns) do
~H"""
<div
class="border-b"
style="background-color: var(--t-surface-raised); border-color: var(--t-border-default);"
>
<div class="collection-header-wrap border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<h1
class="text-3xl md:text-4xl font-bold mb-2"
style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);"
>
<h1 class="t-heading text-3xl md:text-4xl mb-2">
{@title}
</h1>
<%= if @subtitle do %>
<p style="color: var(--t-text-secondary);">{@subtitle}</p>
<p class="collection-header-meta">{@subtitle}</p>
<% end %>
<%= if @product_count do %>
<p style="color: var(--t-text-secondary);">{@product_count} products</p>
<p class="collection-header-meta">{@product_count} products</p>
<% end %>
</div>
</div>
@ -1053,11 +968,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def breadcrumb(assigns) do
~H"""
<nav aria-label="Breadcrumb" class="breadcrumb" style="color: var(--t-text-secondary);">
<nav aria-label="Breadcrumb" class="breadcrumb">
<ol>
<%= for {item, _index} <- Enum.with_index(@items) do %>
<%= if item[:current] do %>
<li aria-current="page" style="color: var(--t-text-primary);">{item.label}</li>
<li aria-current="page">{item.label}</li>
<% else %>
<li>
<%= if @mode == :preview do %>
@ -1107,11 +1022,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def related_products_section(assigns) do
~H"""
<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);"
>
<div class="related-section py-12">
<h2 class="t-heading text-2xl mb-6">
{@title}
</h2>
@ -1150,7 +1062,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
~H"""
<div class="pdp-gallery">
<%!-- Image area (relative container for dots + desktop nav) --%>
<div class="relative" style="border-radius: var(--t-radius-image); overflow: hidden;">
<div class="pdp-gallery-frame relative">
<%!-- Scroll-snap carousel (2+ images) or single image --%>
<%= if length(@images) > 1 do %>
<div
@ -1217,8 +1129,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<div class="pdp-gallery-single">
<%= if @images == [] do %>
<div
class="w-full h-full flex items-center justify-center"
style="color: var(--t-text-tertiary);"
class="product-card-placeholder w-full h-full flex items-center justify-center"
role="img"
aria-label={@product_name}
>
@ -1274,7 +1185,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<button
type="button"
class={"aspect-square bg-gray-200 overflow-hidden pdp-thumbnail#{if idx == 0, do: " pdp-thumbnail-active", else: ""}"}
style="border-radius: var(--t-radius-image);"
aria-label={"View image #{idx + 1} of #{length(@images)}"}
phx-click={
Phoenix.LiveView.JS.dispatch("pdp:scroll-to",
@ -1427,32 +1337,23 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
~H"""
<div>
<h1
class="text-3xl md:text-4xl font-bold mb-4"
style="font-family: var(--t-font-heading); color: var(--t-text-primary); font-weight: var(--t-heading-weight); letter-spacing: var(--t-heading-tracking);"
>
<h1 class="t-heading text-3xl md:text-4xl mb-4">
{@product.title}
</h1>
<div class="flex items-center gap-4 mb-6">
<%= if @product.on_sale do %>
<span
class="text-3xl font-bold"
style="color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"
>
<span class="product-price--sale text-3xl font-bold">
{SimpleshopTheme.Cart.format_price(@price)}
</span>
<span class="text-xl line-through" style="color: var(--t-text-tertiary);">
<span class="product-price--compare text-xl line-through">
{SimpleshopTheme.Cart.format_price(@product.compare_at_price)}
</span>
<span
class="px-2 py-1 text-sm font-bold text-white rounded"
style="background-color: var(--t-sale-color);"
>
<span class="sale-badge px-2 py-1 text-sm font-bold text-white rounded">
SAVE {round((@product.compare_at_price - @price) / @product.compare_at_price * 100)}%
</span>
<% else %>
<span class="text-3xl font-bold" style="color: var(--t-text-primary);">
<span class="product-price--regular text-3xl font-bold">
{SimpleshopTheme.Cart.format_price(@price)}
</span>
<% end %>
@ -1490,10 +1391,10 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def variant_selector(assigns) do
~H"""
<div class="mb-6">
<div class="block font-semibold mb-2" style="color: var(--t-text-primary);">
<div class="variant-label block font-semibold mb-2">
{@option_type.name}<span
:if={@selected}
style="color: var(--t-text-secondary); font-weight: normal;"
class="variant-label-value"
>: {@selected}</span>
</div>
<div class="flex flex-wrap gap-2">
@ -1537,10 +1438,10 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
phx-value-option={@option_name}
phx-value-selected={@title}
class={[
"w-10 h-10 rounded-full border-2 transition-all relative",
"color-swatch w-10 h-10 rounded-full border-2 transition-all relative",
@selected && "ring-2 ring-offset-2"
]}
style={"background-color: #{@hex}; border-color: #{if @selected, do: "hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l))", else: "var(--t-border-default)"}; --tw-ring-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));"}
style={"background-color: #{@hex};"}
title={@title}
aria-label={"Select #{@title}"}
aria-pressed={to_string(@selected)}
@ -1562,10 +1463,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
phx-click={if @mode == :shop, do: "select_option"}
phx-value-option={@option_name}
phx-value-selected={@title}
class={[
"px-4 py-2 font-medium transition-all"
]}
style={"border: 2px solid #{if @selected, do: "hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l))", else: "var(--t-border-default)"}; border-radius: var(--t-radius-button); color: var(--t-text-primary); background: #{if @selected, do: "hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.1)", else: "transparent"};"}
class="size-btn px-4 py-2 font-medium transition-all"
aria-pressed={to_string(@selected)}
>
{@title}
@ -1596,28 +1494,21 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def quantity_selector(assigns) do
~H"""
<div class="mb-8">
<label class="block font-semibold mb-2" style="color: var(--t-text-primary);">
<label class="qty-label block font-semibold mb-2">
Quantity
</label>
<div class="flex items-center gap-4">
<div
class="flex items-center"
style="border: 2px solid var(--t-border-default); border-radius: var(--t-radius-input);"
>
<div class="qty-group flex items-center">
<button
type="button"
phx-click="decrement_quantity"
disabled={@quantity <= @min}
aria-label="Decrease quantity"
class="px-4 py-2 disabled:opacity-30"
style="color: var(--t-text-primary);"
class="qty-btn px-4 py-2 disabled:opacity-30"
>
</button>
<span
class="px-4 py-2 border-x-2 min-w-12 text-center tabular-nums"
style="border-color: var(--t-border-default); color: var(--t-text-primary);"
>
<span class="qty-display px-4 py-2 border-x-2 min-w-12 text-center tabular-nums">
{@quantity}
</span>
<button
@ -1625,16 +1516,15 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
phx-click="increment_quantity"
disabled={@quantity >= @max}
aria-label="Increase quantity"
class="px-4 py-2 disabled:opacity-30"
style="color: var(--t-text-primary);"
class="qty-btn px-4 py-2 disabled:opacity-30"
>
+
</button>
</div>
<%= if @in_stock do %>
<span class="text-sm" style="color: var(--t-text-tertiary);">In stock</span>
<span class="stock-in text-sm">In stock</span>
<% else %>
<span class="text-sm font-semibold" style="color: var(--t-sale-color);">Out of stock</span>
<span class="stock-out text-sm font-semibold">Out of stock</span>
<% end %>
</div>
</div>
@ -1663,20 +1553,16 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def add_to_cart_button(assigns) do
~H"""
<div
class={[
"mb-4",
@sticky &&
"sticky bottom-0 z-10 py-3 md:relative md:py-0 -mx-4 px-4 md:mx-0 md:px-0 border-t md:border-0"
]}
style="background-color: var(--t-surface-base); border-color: var(--t-border-subtle);"
>
<div class={[
"atc-wrap mb-4",
@sticky &&
"sticky bottom-0 z-10 py-3 md:relative md:py-0 -mx-4 px-4 md:mx-0 md:px-0 border-t md:border-0"
]}>
<button
type="button"
phx-click={if @mode == :preview, do: open_cart_drawer_js(), else: "add_to_cart"}
disabled={@disabled}
class="w-full px-6 py-4 text-lg font-semibold transition-all"
style={"background-color: #{if @disabled, do: "var(--t-border-default)", else: "hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l))"}; color: var(--t-text-inverse); border-radius: var(--t-radius-button); cursor: #{if @disabled, do: "not-allowed", else: "pointer"}; border: none;"}
class="atc-btn w-full px-6 py-4 text-lg font-semibold transition-all"
>
{@text}
</button>
@ -1714,10 +1600,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def accordion_item(assigns) do
~H"""
<details open={@open} class="group">
<summary
class="flex justify-between items-center py-4 cursor-pointer list-none [&::-webkit-details-marker]:hidden"
style="color: var(--t-text-primary);"
>
<summary class="accordion-summary flex justify-between items-center py-4 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
<span class="font-semibold">{@title}</span>
<svg
class="w-5 h-5 transition-transform duration-200 group-open:rotate-180"
@ -1730,7 +1613,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
</svg>
</summary>
<div class="pb-4" style="color: var(--t-text-secondary);">
<div class="accordion-body pb-4">
{render_slot(@inner_block)}
</div>
</details>
@ -1768,10 +1651,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
end)
~H"""
<div
class="mt-8 divide-y"
style="border-top: 1px solid var(--t-border-subtle); border-bottom: 1px solid var(--t-border-subtle); border-color: var(--t-border-subtle);"
>
<div class="details-wrap mt-8 divide-y">
<.accordion_item title="Description" open={true}>
<p class="leading-relaxed">
{@product.description}. Crafted with attention to detail and quality materials, this product is designed to last. Perfect for everyday use or special occasions.
@ -1782,25 +1662,21 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<.accordion_item title="Size Guide">
<table class="w-full text-sm">
<thead>
<tr style="border-bottom: 1px solid var(--t-border-subtle);">
<th class="text-left py-2 font-semibold" style="color: var(--t-text-primary);">
<tr class="details-table-row">
<th class="details-th text-left py-2 font-semibold">
Size
</th>
<th class="text-left py-2 font-semibold" style="color: var(--t-text-primary);">
<th class="details-th text-left py-2 font-semibold">
Chest (cm)
</th>
<th class="text-left py-2 font-semibold" style="color: var(--t-text-primary);">
<th class="details-th text-left py-2 font-semibold">
Length (cm)
</th>
</tr>
</thead>
<tbody>
<%= for {size_row, idx} <- Enum.with_index(@sizes) do %>
<tr style={
if idx < length(@sizes) - 1,
do: "border-bottom: 1px solid var(--t-border-subtle);",
else: ""
}>
<tr class={idx < length(@sizes) - 1 && "details-table-row"}>
<td class="py-2">{size_row.size}</td>
<td class="py-2">{size_row.chest}</td>
<td class="py-2">{size_row.length}</td>
@ -1814,13 +1690,13 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<.accordion_item title="Shipping & Returns">
<div class="flex flex-col gap-3">
<div>
<p class="font-semibold mb-1" style="color: var(--t-text-primary);">Delivery</p>
<p class="details-subheading font-semibold mb-1">Delivery</p>
<p class="text-sm">
Free UK delivery on orders over £40. Standard delivery 3-5 working days. Express delivery available at checkout.
</p>
</div>
<div>
<p class="font-semibold mb-1" style="color: var(--t-text-primary);">Returns</p>
<p class="details-subheading font-semibold mb-1">Returns</p>
<p class="text-sm">
We offer a 30-day return policy. Items must be unused and in original packaging. Please contact us to arrange a return.
</p>