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. */ Each component gets its own section. */
@layer components { @layer components {
/* Phase 2: product cards, grid, badges, hero, categories */ /* Shared heading treatment
/* Phase 2: PDP, variant selector, gallery, accordion */ font-family + weight + tracking + colour used across
/* Phase 3: layout components (header, footer, nav, search) */ hero titles, section headings, collection headers, PDP, etc. */
/* Phase 3: cart components (drawer, items, summary) */
/* Phase 4: content components (contact, reviews, newsletter) */ .t-heading {
/* Phase 4: page templates (checkout success, etc.) */ 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 %> ) do %>
<link rel="preload" href={preload.href} as="font" type="font/woff2" crossorigin /> <link rel="preload" href={preload.href} as="font" type="font/woff2" crossorigin />
<% end %> <% 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/app-shop.css"} />
<link phx-track-static rel="stylesheet" href={~p"/assets/css/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"}> <script defer phx-track-static src={~p"/assets/js/app.js"}>

View File

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