replace Tailwind utilities in layout + page templates with CSS (Phase 5a)

Absorb ~100 Tailwind utility classes from layout.ex and all page
templates into semantic CSS rules in components.css. Uses theme font-size
vars (--t-text-small, --t-text-caption) instead of rem to respect the
theme's em-based scaling system.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-02-17 09:03:35 +00:00
parent 84de1c37c5
commit fc9c33ab0c
10 changed files with 474 additions and 124 deletions

View File

@ -65,7 +65,7 @@ Plans: [admin-redesign.md](docs/plans/admin-redesign.md) | [setup-wizard.md](doc
| ~~33~~ | ~~Phase 1: Layout primitives + reset~~ | 32 | 1.5h | done | | ~~33~~ | ~~Phase 1: Layout primitives + reset~~ | 32 | 1.5h | done |
| ~~34~~ | ~~Phase 2: Extract product inline styles~~ | 33 | 3h | done | | ~~34~~ | ~~Phase 2: Extract product inline styles~~ | 33 | 3h | done |
| ~~35~~ | ~~Phase 3: Extract layout + cart inline styles~~ | 33 | 3h | done | | ~~35~~ | ~~Phase 3: Extract layout + cart inline styles~~ | 33 | 3h | done |
| 36 | Phase 4: Extract content + template inline styles | 33 | 2.5h | | | ~~36~~ | ~~Phase 4: Extract content + template inline styles~~ | 33 | 2.5h | done |
| 37 | Phase 5: Remove Tailwind from shop | 34-36 | 3h | | | 37 | Phase 5: Remove Tailwind from shop | 34-36 | 3h | |
| 38 | Phase 6: Replace DaisyUI (admin) | 37 | 3h | | | 38 | Phase 6: Replace DaisyUI (admin) | 37 | 3h | |
| 39 | Phase 7: Remove Tailwind entirely | 38 | 1.5h | | | 39 | Phase 7: Remove Tailwind entirely | 38 | 1.5h | |

View File

@ -335,12 +335,25 @@
font-size: var(--t-text-small); font-size: var(--t-text-small);
} }
/* ── Page container (shared responsive centering) ── */
.page-container {
max-width: 80rem;
margin-inline: auto;
padding: 2rem 1rem;
}
/* ── Shop container (body-level defaults) ── */ /* ── Shop container (body-level defaults) ── */
.shop-container { .shop-container {
background-color: var(--t-surface-base); background-color: var(--t-surface-base);
font-family: var(--t-font-body); font-family: var(--t-font-body);
color: var(--t-text-primary); color: var(--t-text-primary);
min-height: 100vh;
}
.shop-container[data-bottom-nav] {
padding-bottom: 5rem;
} }
/* ── Shop header ── */ /* ── Shop header ── */
@ -350,11 +363,13 @@
border-bottom: 1px solid var(--t-border-default); border-bottom: 1px solid var(--t-border-default);
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.5rem;
} }
.shop-logo { .shop-logo {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
@ -362,6 +377,7 @@
.shop-logo-link { .shop-logo-link {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 0.5rem;
text-decoration: none; text-decoration: none;
} }
@ -377,24 +393,39 @@
} }
.shop-nav { .shop-nav {
display: none;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.shop-actions { .shop-actions {
display: flex;
align-items: center;
position: relative; position: relative;
z-index: 1; z-index: 1;
} }
.header-icon-btn { .header-icon-btn {
width: 2.25rem;
height: 2.25rem;
display: flex;
align-items: center;
justify-content: center;
position: relative;
color: var(--t-text-secondary); color: var(--t-text-secondary);
border-radius: var(--t-radius-button); border-radius: var(--t-radius-button);
transition: all 0.2s ease;
&:where(button) { &:where(button) {
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
} }
& svg {
width: 1.25rem;
height: 1.25rem;
}
} }
.cart-badge { .cart-badge {
@ -441,22 +472,50 @@
padding-bottom: env(safe-area-inset-bottom, 0px); padding-bottom: env(safe-area-inset-bottom, 0px);
& ul { & ul {
display: flex;
justify-content: space-around;
align-items: center;
height: 4rem;
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style: none; list-style: none;
} }
& li {
flex: 1;
}
} }
.mobile-nav-link { .mobile-nav-link {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 0.25rem;
padding-block: 0.5rem;
margin-inline: 0.25rem;
min-height: 56px;
border-radius: var(--t-radius-card, 0.5rem);
font-size: var(--t-text-caption);
color: var(--t-text-secondary); color: var(--t-text-secondary);
text-decoration: none; text-decoration: none;
font-weight: 500; font-weight: 500;
background-color: transparent; background-color: transparent;
& svg {
width: 1.25rem;
height: 1.25rem;
}
&[aria-current="page"] { &[aria-current="page"] {
color: hsl(var(--t-accent-h) var(--t-accent-s) calc(var(--t-accent-l) - 15%)); color: hsl(var(--t-accent-h) var(--t-accent-s) calc(var(--t-accent-l) - 15%));
font-weight: 600; font-weight: 600;
background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.1); background-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l) / 0.1);
& svg {
width: 1.5rem;
height: 1.5rem;
}
} }
} }
@ -473,6 +532,9 @@
} }
.search-panel { .search-panel {
width: 100%;
max-width: 36rem;
margin-inline: 1rem;
background: var(--t-surface-raised); background: var(--t-surface-raised);
border-radius: var(--t-radius-card); border-radius: var(--t-radius-card);
overflow: hidden; overflow: hidden;
@ -480,39 +542,79 @@
} }
.search-bar { .search-bar {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 1rem;
border-bottom: 1px solid var(--t-border-default); border-bottom: 1px solid var(--t-border-default);
} }
.search-icon { .search-icon {
width: 1.25rem;
height: 1.25rem;
flex-shrink: 0;
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
} }
.search-input { .search-input {
flex: 1;
font-size: var(--t-text-large, 1.125rem);
font-family: var(--t-font-body); font-family: var(--t-font-body);
color: var(--t-text-primary); color: var(--t-text-primary);
background: transparent;
border: none;
outline: none;
} }
.search-kbd { .search-kbd {
display: none;
align-items: center;
gap: 0.25rem;
font-size: var(--t-text-caption);
padding: 0.125rem 0.375rem;
border-radius: var(--t-radius-button, 0.25rem);
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
border: 1px solid var(--t-border-default); border: 1px solid var(--t-border-default);
} }
.search-close { .search-close {
width: 2rem;
height: 2rem;
display: flex;
align-items: center;
justify-content: center;
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
background: none; background: none;
border: none; border: none;
cursor: pointer; cursor: pointer;
border-radius: var(--t-radius-button); border-radius: var(--t-radius-button);
transition: all 0.2s ease;
& svg {
width: 1.25rem;
height: 1.25rem;
}
} }
.search-results { .search-results {
max-height: 60vh; max-height: 60vh;
overflow-y: auto; overflow-y: auto;
& ul {
padding-block: 0.5rem;
margin: 0;
list-style: none;
}
} }
.search-result { .search-result {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
transition: background-color 0.15s ease;
&:hover { &:hover {
background: var(--t-surface-sunken); background: var(--t-surface-sunken);
@ -520,19 +622,47 @@
} }
.search-result-thumb { .search-result-thumb {
width: 3rem;
height: 3rem;
flex-shrink: 0;
border-radius: var(--t-radius-card, 0.25rem);
overflow: hidden;
background: var(--t-surface-sunken); background: var(--t-surface-sunken);
& img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.search-result-details {
flex: 1;
min-width: 0;
} }
.search-result-title { .search-result-title {
font-size: var(--t-text-small);
font-weight: 500;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--t-text-primary); color: var(--t-text-primary);
} }
.search-result-meta { .search-result-meta {
font-size: var(--t-text-caption);
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
& span {
margin-left: 0.5rem;
}
} }
.search-hint { .search-hint {
padding: 1.5rem;
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
font-size: var(--t-text-small);
} }
/* ── Shop footer ── */ /* ── Shop footer ── */
@ -542,21 +672,65 @@
border-top: 1px solid var(--t-border-default); border-top: 1px solid var(--t-border-default);
} }
.shop-footer-inner {
max-width: 80rem;
margin-inline: auto;
padding: 3rem 1rem;
}
.footer-grid {
display: grid;
grid-template-columns: 1fr;
gap: 3rem;
}
.footer-links {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 2rem;
}
.footer-heading { .footer-heading {
font-family: var(--t-font-heading); font-family: var(--t-font-heading);
font-weight: 600;
font-size: var(--t-text-small);
margin-bottom: 1rem;
color: var(--t-text-primary); color: var(--t-text-primary);
} }
.footer-nav {
display: flex;
flex-direction: column;
gap: 0.5rem;
font-size: var(--t-text-small);
list-style: none;
margin: 0;
padding: 0;
}
.footer-link { .footer-link {
color: var(--t-text-secondary); color: var(--t-text-secondary);
cursor: pointer; cursor: pointer;
transition: opacity 0.15s ease;
&:hover {
opacity: 0.8;
}
} }
.footer-bottom { .footer-bottom {
margin-top: 3rem;
padding-top: 2rem;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 1rem;
border-top: 1px solid var(--t-border-subtle); border-top: 1px solid var(--t-border-subtle);
} }
.footer-copyright { .footer-copyright {
font-size: var(--t-text-caption);
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
} }
@ -1081,7 +1255,24 @@
/* ── Checkout success ── */ /* ── Checkout success ── */
.checkout-main {
max-width: 48rem;
padding-block: 4rem;
}
.checkout-header {
text-align: center;
margin-bottom: 3rem;
}
.checkout-icon { .checkout-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
border-radius: 9999px;
margin-bottom: 1.5rem;
background-color: var(--t-accent); background-color: var(--t-accent);
color: var(--t-accent-contrast); color: var(--t-accent-contrast);
} }
@ -1089,52 +1280,120 @@
.checkout-heading { .checkout-heading {
font-family: var(--t-font-heading); font-family: var(--t-font-heading);
color: var(--t-text-primary); color: var(--t-text-primary);
&:where(h1) {
font-size: var(--t-text-3xl, 1.875rem);
font-weight: 700;
margin-bottom: 0.75rem;
}
&:where(h2) {
font-size: var(--t-text-large, 1.125rem);
font-weight: 600;
margin-bottom: 1rem;
}
} }
.checkout-meta { .checkout-meta {
font-size: var(--t-text-large, 1.125rem);
margin-bottom: 0.5rem;
color: var(--t-text-secondary); color: var(--t-text-secondary);
& strong { & strong {
color: var(--t-text-primary); color: var(--t-text-primary);
} }
&:last-child {
font-size: var(--t-text-base, 1rem);
margin-bottom: 0;
}
}
.checkout-card {
padding: 1.5rem;
margin-bottom: 2rem;
} }
.checkout-items { .checkout-items {
display: flex;
flex-direction: column;
gap: 1rem;
margin-bottom: 1.5rem;
list-style: none; list-style: none;
margin: 0; margin-top: 0;
padding: 0; padding: 0;
} }
.checkout-item { .checkout-item {
border-color: var(--t-border-default); display: flex;
justify-content: space-between;
align-items: flex-start;
padding-bottom: 1rem;
border-bottom: 1px solid var(--t-border-default);
&:last-child {
border-bottom: none;
padding-bottom: 0;
}
} }
.checkout-item-name { .checkout-item-name {
font-weight: 500;
color: var(--t-text-primary); color: var(--t-text-primary);
} }
.checkout-item-detail { .checkout-item-detail {
font-size: var(--t-text-small, 0.875rem);
color: var(--t-text-secondary); color: var(--t-text-secondary);
} }
.checkout-item-price { .checkout-item-price {
font-weight: 500;
color: var(--t-text-primary); color: var(--t-text-primary);
} }
.checkout-total-border { .checkout-total-border {
border-color: var(--t-border-default); border-top: 1px solid var(--t-border-default);
padding-top: 1rem;
} }
.checkout-total { .checkout-total {
display: flex;
justify-content: space-between;
font-size: var(--t-text-large, 1.125rem);
color: var(--t-text-primary); color: var(--t-text-primary);
} }
.checkout-total-label {
font-weight: 600;
}
.checkout-total-amount {
font-weight: 700;
}
.checkout-shipping-address { .checkout-shipping-address {
color: var(--t-text-secondary); color: var(--t-text-secondary);
} }
.checkout-actions {
text-align: center;
}
.checkout-cta {
padding: 0.75rem 2rem;
}
.checkout-pending-icon { .checkout-pending-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 4rem;
height: 4rem;
border-radius: 9999px;
margin-bottom: 1.5rem;
background-color: var(--t-surface-sunken); background-color: var(--t-surface-sunken);
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
} }
.checkout-pending-spinner { .checkout-pending-spinner {
@ -1142,34 +1401,140 @@
} }
.checkout-pending-text { .checkout-pending-text {
font-size: var(--t-text-large, 1.125rem);
margin-bottom: 2rem;
color: var(--t-text-secondary); color: var(--t-text-secondary);
} }
.checkout-pending-hint { .checkout-pending-hint {
font-size: var(--t-text-small, 0.875rem);
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
} }
.checkout-contact-link { .checkout-contact-link {
text-decoration: underline;
color: var(--t-accent); color: var(--t-accent);
} }
@keyframes pulse {
50% { opacity: 0.5; }
}
/* ── Error page ── */ /* ── Error page ── */
.error-main { .error-main {
display: flex;
align-items: center;
justify-content: center;
min-height: calc(100vh - 4rem); min-height: calc(100vh - 4rem);
} }
/* ── PDP variant fallback ── */ .error-container {
max-width: 42rem;
padding-block: 4rem;
}
/* ── PDP page ── */
.pdp-grid {
display: grid;
grid-template-columns: 1fr;
gap: 3rem;
margin-bottom: 4rem;
}
.pdp-variant-fallback { .pdp-variant-fallback {
margin-bottom: 1.5rem;
font-size: var(--t-text-small, 0.875rem);
color: var(--t-text-secondary); color: var(--t-text-secondary);
} }
/* ── Cart page list ── */ /* ── Cart page ── */
.cart-grid {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
.cart-page-card {
padding: 1rem;
}
.cart-page-list { .cart-page-list {
display: flex;
flex-direction: column;
gap: 1rem;
list-style: none; list-style: none;
margin: 0; margin: 0;
padding: 0; padding: 0;
} }
/* ── Contact page ── */
.contact-main {
max-width: 56rem;
padding-top: 0;
padding-bottom: 4rem;
}
.contact-grid {
display: grid;
gap: 2rem;
margin-bottom: 3rem;
}
.contact-sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* ── Content page image ── */
.content-hero-image {
width: 100%;
height: 300px;
object-fit: cover;
}
/* ── Screen reader only ── */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
/* ── Responsive breakpoints ── */
@media (min-width: 640px) {
.page-container { padding-inline: 1.5rem; }
.shop-header { padding: 0.75rem 1rem; }
.shop-footer-inner { padding-inline: 1.5rem; }
.search-kbd { display: flex; }
}
@media (min-width: 768px) {
.shop-container[data-bottom-nav] { padding-bottom: 0; }
.shop-header { padding: 1rem 2rem; }
.shop-nav { display: flex; gap: 1.5rem; }
.mobile-bottom-nav { display: none; }
.footer-grid { grid-template-columns: repeat(2, 1fr); }
.footer-bottom { flex-direction: row; }
.pdp-grid { grid-template-columns: repeat(2, 1fr); }
.contact-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (min-width: 1024px) {
.page-container { padding-inline: 2rem; }
.shop-footer-inner { padding-inline: 2rem; }
.cart-grid { grid-template-columns: 2fr 1fr; }
}
} }

View File

@ -1,20 +1,20 @@
<.shop_layout {layout_assigns(assigns)} active_page="cart"> <.shop_layout {layout_assigns(assigns)} active_page="cart">
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main id="main-content" class="page-container">
<.page_title text="Your basket" /> <.page_title text="Your basket" />
<%= if @cart_items == [] do %> <%= if @cart_items == [] do %>
<.cart_empty_state mode={@mode} /> <.cart_empty_state mode={@mode} />
<% else %> <% else %>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div class="cart-grid">
<div class="lg:col-span-2"> <div>
<ul <ul
role="list" role="list"
aria-label="Cart items" aria-label="Cart items"
class="cart-page-list flex flex-col gap-4" class="cart-page-list"
> >
<%= for item <- @cart_items do %> <%= for item <- @cart_items do %>
<li> <li>
<.shop_card class="p-4"> <.shop_card class="cart-page-card">
<.cart_item_row item={item} size={:default} show_quantity_controls mode={@mode} /> <.cart_item_row item={item} size={:default} show_quantity_controls mode={@mode} />
</.shop_card> </.shop_card>
</li> </li>

View File

@ -1,10 +1,11 @@
<.shop_layout {layout_assigns(assigns)} active_page="checkout"> <.shop_layout {layout_assigns(assigns)} active_page="checkout">
<main id="main-content" class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> <main id="main-content" class="page-container checkout-main">
<%= if @order && @order.payment_status == "paid" do %> <%= if @order && @order.payment_status == "paid" do %>
<div class="text-center mb-12"> <div class="checkout-header">
<div class="checkout-icon inline-flex items-center justify-center w-16 h-16 rounded-full mb-6"> <div class="checkout-icon">
<svg <svg
class="w-8 h-8" width="32"
height="32"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="2.5" stroke-width="2.5"
@ -14,11 +15,11 @@
</svg> </svg>
</div> </div>
<h1 class="checkout-heading text-3xl font-bold mb-3"> <h1 class="checkout-heading">
Thank you for your order Thank you for your order
</h1> </h1>
<p class="checkout-meta text-lg mb-2"> <p class="checkout-meta">
Order <strong>{@order.order_number}</strong> Order <strong>{@order.order_number}</strong>
</p> </p>
@ -29,38 +30,38 @@
<% end %> <% end %>
</div> </div>
<.shop_card class="p-6 mb-8"> <.shop_card class="checkout-card">
<h2 class="checkout-heading text-lg font-semibold mb-4"> <h2 class="checkout-heading">
Order details Order details
</h2> </h2>
<ul class="checkout-items flex flex-col gap-4 mb-6"> <ul class="checkout-items">
<%= for item <- @order.items do %> <%= for item <- @order.items do %>
<li class="checkout-item flex justify-between items-start pb-4 border-b last:border-b-0 last:pb-0"> <li class="checkout-item">
<div> <div>
<p class="checkout-item-name font-medium"> <p class="checkout-item-name">
{item.product_name} {item.product_name}
</p> </p>
<%= if item.variant_title do %> <%= if item.variant_title do %>
<p class="checkout-item-detail text-sm"> <p class="checkout-item-detail">
{item.variant_title} {item.variant_title}
</p> </p>
<% end %> <% end %>
<p class="checkout-item-detail text-sm"> <p class="checkout-item-detail">
Qty: {item.quantity} Qty: {item.quantity}
</p> </p>
</div> </div>
<span class="checkout-item-price font-medium"> <span class="checkout-item-price">
{SimpleshopTheme.Cart.format_price(item.unit_price * item.quantity)} {SimpleshopTheme.Cart.format_price(item.unit_price * item.quantity)}
</span> </span>
</li> </li>
<% end %> <% end %>
</ul> </ul>
<div class="checkout-total-border border-t pt-4"> <div class="checkout-total-border">
<div class="checkout-total flex justify-between text-lg"> <div class="checkout-total">
<span class="font-semibold">Total</span> <span class="checkout-total-label">Total</span>
<span class="font-bold"> <span class="checkout-total-amount">
{SimpleshopTheme.Cart.format_price(@order.total)} {SimpleshopTheme.Cart.format_price(@order.total)}
</span> </span>
</div> </div>
@ -68,8 +69,8 @@
</.shop_card> </.shop_card>
<%= if @order.shipping_address != %{} do %> <%= if @order.shipping_address != %{} do %>
<.shop_card class="p-6 mb-8"> <.shop_card class="checkout-card">
<h2 class="checkout-heading text-lg font-semibold mb-3"> <h2 class="checkout-heading">
Shipping to Shipping to
</h2> </h2>
<div class="checkout-shipping-address"> <div class="checkout-shipping-address">
@ -86,18 +87,19 @@
</.shop_card> </.shop_card>
<% end %> <% end %>
<div class="text-center"> <div class="checkout-actions">
<.shop_link_button href="/collections/all" class="px-8 py-3"> <.shop_link_button href="/collections/all" class="checkout-cta">
Continue shopping Continue shopping
</.shop_link_button> </.shop_link_button>
</div> </div>
<% else %> <% else %>
<%!-- Payment pending or order not found --%> <%!-- Payment pending or order not found --%>
<div class="text-center py-16"> <div class="checkout-header">
<div class="checkout-pending-icon inline-flex items-center justify-center w-16 h-16 rounded-full mb-6 animate-pulse"> <div class="checkout-pending-icon">
<span class="checkout-pending-spinner"> <span class="checkout-pending-spinner">
<svg <svg
class="w-8 h-8" width="32"
height="32"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" stroke-width="1.5"
@ -112,18 +114,18 @@
</span> </span>
</div> </div>
<h1 class="checkout-heading text-3xl font-bold mb-3"> <h1 class="checkout-heading">
Processing your payment Processing your payment
</h1> </h1>
<p class="checkout-pending-text text-lg mb-8"> <p class="checkout-pending-text">
Please wait while we confirm your payment. This usually takes a few seconds. Please wait while we confirm your payment. This usually takes a few seconds.
</p> </p>
<p class="checkout-pending-hint text-sm"> <p class="checkout-pending-hint">
If this page doesn't update, please <.link If this page doesn't update, please <.link
navigate="/contact" navigate="/contact"
class="checkout-contact-link underline" class="checkout-contact-link"
>contact us</.link>. >contact us</.link>.
</p> </p>
</div> </div>

View File

@ -2,7 +2,7 @@
<main id="main-content"> <main id="main-content">
<.collection_header title="All Products" product_count={length(assigns[:products] || [])} /> <.collection_header title="All Products" product_count={length(assigns[:products] || [])} />
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="page-container">
<.filter_bar categories={assigns[:categories] || []} /> <.filter_bar categories={assigns[:categories] || []} />
<.product_grid theme_settings={@theme_settings}> <.product_grid theme_settings={@theme_settings}>

View File

@ -1,15 +1,15 @@
<.shop_layout {layout_assigns(assigns)} active_page="contact"> <.shop_layout {layout_assigns(assigns)} active_page="contact">
<main id="main-content" class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 pb-16"> <main id="main-content" class="page-container contact-main">
<.hero_section <.hero_section
variant={:page} variant={:page}
title="Get in touch" title="Get in touch"
description="Sample contact page for the demo store. Add your own message here something friendly about how customers can reach you." description="Sample contact page for the demo store. Add your own message here something friendly about how customers can reach you."
/> />
<div class="grid gap-8 md:grid-cols-2 mb-12"> <div class="contact-grid">
<.contact_form email="hello@example.com" /> <.contact_form email="hello@example.com" />
<div class="flex flex-col gap-6"> <div class="contact-sidebar">
<.order_tracking_card /> <.order_tracking_card />
<.info_card <.info_card

View File

@ -22,7 +22,7 @@
source_width={1200} source_width={1200}
alt={@image_alt} alt={@image_alt}
sizes="(max-width: 800px) 100vw, 800px" sizes="(max-width: 800px) 100vw, 800px"
class="w-full h-[300px] object-cover" class="content-hero-image"
/> />
</div> </div>
<% end %> <% end %>

View File

@ -1,9 +1,9 @@
<.shop_layout {layout_assigns(assigns)} active_page="error" error_page> <.shop_layout {layout_assigns(assigns)} active_page="error" error_page>
<main <main
id="main-content" id="main-content"
class="error-main flex items-center justify-center" class="error-main"
> >
<div class="max-w-2xl mx-auto px-4 sm:px-6 lg:px-8 py-16"> <div class="page-container error-container">
<.hero_section <.hero_section
variant={:error} variant={:error}
pre_title={@error_code} pre_title={@error_code}

View File

@ -1,5 +1,5 @@
<.shop_layout {layout_assigns(assigns)} active_page="pdp"> <.shop_layout {layout_assigns(assigns)} active_page="pdp">
<main id="main-content" class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <main id="main-content" class="page-container">
<.breadcrumb <.breadcrumb
items={ items={
if @product.category do if @product.category do
@ -19,7 +19,7 @@
mode={@mode} mode={@mode}
/> />
<div class="grid grid-cols-1 md:grid-cols-2 gap-12 mb-16"> <div class="pdp-grid">
<.product_gallery images={@gallery_images} product_name={@product.title} /> <.product_gallery images={@gallery_images} product_name={@product.title} />
<div> <div>
@ -38,7 +38,7 @@
<%!-- Fallback for products with no variant options --%> <%!-- Fallback for products with no variant options --%>
<div <div
:if={@option_types == []} :if={@option_types == []}
class="pdp-variant-fallback mb-6 text-sm" class="pdp-variant-fallback"
> >
One size One size
</div> </div>

View File

@ -100,7 +100,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
<div <div
id={unless @error_page, do: "shop-container"} id={unless @error_page, do: "shop-container"}
phx-hook={unless @error_page, do: "CartPersist"} phx-hook={unless @error_page, do: "CartPersist"}
class={["shop-container min-h-screen", !@error_page && "pb-20 md:pb-0"]} class="shop-container"
data-bottom-nav={!@error_page || nil}
> >
<.skip_link /> <.skip_link />
@ -177,10 +178,10 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
def mobile_bottom_nav(assigns) do def mobile_bottom_nav(assigns) do
~H""" ~H"""
<nav <nav
class="mobile-bottom-nav md:hidden" class="mobile-bottom-nav"
aria-label="Main navigation" aria-label="Main navigation"
> >
<ul class="flex justify-around items-center h-16"> <ul>
<.mobile_nav_item <.mobile_nav_item
icon={:home} icon={:home}
label="Home" label="Home"
@ -233,26 +234,26 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
assigns = assign(assigns, :is_current, is_current) assigns = assign(assigns, :is_current, is_current)
~H""" ~H"""
<li class="flex-1"> <li>
<%= if @mode == :preview do %> <%= if @mode == :preview do %>
<a <a
href="#" href="#"
phx-click="change_preview_page" phx-click="change_preview_page"
phx-value-page={@page} phx-value-page={@page}
class="mobile-nav-link flex flex-col items-center justify-center gap-1 py-2 mx-1 rounded-lg min-h-[56px]" class="mobile-nav-link"
aria-current={if @is_current, do: "page", else: nil} aria-current={if @is_current, do: "page", else: nil}
> >
<.nav_icon icon={@icon} size={if @is_current, do: "w-6 h-6", else: "w-5 h-5"} /> <.nav_icon icon={@icon} />
<span class="text-xs">{@label}</span> <span>{@label}</span>
</a> </a>
<% else %> <% else %>
<.link <.link
navigate={@href} navigate={@href}
class="mobile-nav-link flex flex-col items-center justify-center gap-1 py-2 mx-1 rounded-lg min-h-[56px]" class="mobile-nav-link"
aria-current={if @is_current, do: "page", else: nil} aria-current={if @is_current, do: "page", else: nil}
> >
<.nav_icon icon={@icon} size={if @is_current, do: "w-6 h-6", else: "w-5 h-5"} /> <.nav_icon icon={@icon} />
<span class="text-xs">{@label}</span> <span>{@label}</span>
</.link> </.link>
<% end %> <% end %>
</li> </li>
@ -260,11 +261,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
end end
defp nav_icon(%{icon: :home} = assigns) do defp nav_icon(%{icon: :home} = assigns) do
assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end)
~H""" ~H"""
<svg <svg
class={@size}
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -281,11 +279,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
end end
defp nav_icon(%{icon: :shop} = assigns) do defp nav_icon(%{icon: :shop} = assigns) do
assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end)
~H""" ~H"""
<svg <svg
class={@size}
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -304,11 +299,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
end end
defp nav_icon(%{icon: :about} = assigns) do defp nav_icon(%{icon: :about} = assigns) do
assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end)
~H""" ~H"""
<svg <svg
class={@size}
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -326,11 +318,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
end end
defp nav_icon(%{icon: :contact} = assigns) do defp nav_icon(%{icon: :contact} = assigns) do
assigns = assign_new(assigns, :size, fn -> "w-5 h-5" end)
~H""" ~H"""
<svg <svg
class={@size}
width="24" width="24"
height="24" height="24"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -385,12 +374,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
phx-click={Phoenix.LiveView.JS.dispatch("close-search", to: "#search-modal")} phx-click={Phoenix.LiveView.JS.dispatch("close-search", to: "#search-modal")}
> >
<div <div
class="search-panel w-full max-w-xl mx-4" class="search-panel"
onclick="event.stopPropagation()" onclick="event.stopPropagation()"
> >
<div class="search-bar flex items-center gap-3 p-4"> <div class="search-bar">
<svg <svg
class="search-icon w-5 h-5 flex-shrink-0" class="search-icon"
width="20" width="20"
height="20" height="20"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -405,7 +394,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
type="text" type="text"
id="search-input" id="search-input"
name="query" name="query"
class="search-input flex-1 text-lg bg-transparent border-none outline-none" class="search-input"
placeholder="Search products..." placeholder="Search products..."
value={@search_query} value={@search_query}
phx-keyup="search" phx-keyup="search"
@ -417,19 +406,18 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
aria-autocomplete="list" aria-autocomplete="list"
/> />
<div <div
class="search-kbd hidden sm:flex items-center gap-1 text-xs px-1.5 py-0.5 rounded" class="search-kbd"
aria-hidden="true" aria-hidden="true"
> >
<kbd></kbd><kbd>K</kbd> <kbd></kbd><kbd>K</kbd>
</div> </div>
<button <button
type="button" type="button"
class="search-close w-8 h-8 flex items-center justify-center transition-all" class="search-close"
phx-click={Phoenix.LiveView.JS.dispatch("close-search", to: "#search-modal")} phx-click={Phoenix.LiveView.JS.dispatch("close-search", to: "#search-modal")}
aria-label="Close search" aria-label="Close search"
> >
<svg <svg
class="w-5 h-5"
width="20" width="20"
height="20" height="20"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -446,7 +434,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
<div class="search-results"> <div class="search-results">
<%= cond do %> <%= cond do %>
<% @search_results != [] -> %> <% @search_results != [] -> %>
<ul id="search-results-list" class="py-2" role="listbox" aria-label="Search results"> <ul id="search-results-list" role="listbox" aria-label="Search results">
<li <li
:for={item <- @results_with_images} :for={item <- @results_with_images}
id={"search-result-#{item.idx}"} id={"search-result-#{item.idx}"}
@ -455,27 +443,26 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
> >
<.link <.link
navigate={"/products/#{item.product.slug || item.product.id}"} navigate={"/products/#{item.product.slug || item.product.id}"}
class="search-result flex items-center gap-3 px-4 py-3 transition-colors" class="search-result"
phx-click={Phoenix.LiveView.JS.dispatch("close-search", to: "#search-modal")} phx-click={Phoenix.LiveView.JS.dispatch("close-search", to: "#search-modal")}
> >
<div <div
:if={item.image_url} :if={item.image_url}
class="search-result-thumb w-12 h-12 flex-shrink-0 rounded overflow-hidden" class="search-result-thumb"
> >
<img <img
src={item.image_url} src={item.image_url}
alt={item.product.title} alt={item.product.title}
class="w-full h-full object-cover"
loading="lazy" loading="lazy"
/> />
</div> </div>
<div class="flex-1 min-w-0"> <div class="search-result-details">
<p class="search-result-title text-sm font-medium truncate"> <p class="search-result-title">
{item.product.title} {item.product.title}
</p> </p>
<p class="search-result-meta text-xs"> <p class="search-result-meta">
{item.product.category} {item.product.category}
<span class="ml-2"> <span>
{Cart.format_price(item.product.cheapest_price)} {Cart.format_price(item.product.cheapest_price)}
</span> </span>
</p> </p>
@ -484,12 +471,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
</li> </li>
</ul> </ul>
<% String.length(@search_query) >= 2 -> %> <% String.length(@search_query) >= 2 -> %>
<div class="search-hint p-6"> <div class="search-hint">
<p class="text-sm">No products found for "{@search_query}"</p> <p>No products found for "{@search_query}"</p>
</div> </div>
<% @hint_text != nil -> %> <% @hint_text != nil -> %>
<div class="search-hint p-6"> <div class="search-hint">
<p class="text-sm">{@hint_text}</p> <p>{@hint_text}</p>
</div> </div>
<% true -> %> <% true -> %>
<% end %> <% end %>
@ -522,24 +509,23 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
~H""" ~H"""
<footer class="shop-footer"> <footer class="shop-footer">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12"> <div class="shop-footer-inner">
<div class="grid grid-cols-1 md:grid-cols-2 gap-12"> <div class="footer-grid">
<.newsletter_card variant={:inline} /> <.newsletter_card variant={:inline} />
<!-- Links --> <div class="footer-links">
<div class="grid grid-cols-2 gap-8">
<div> <div>
<h4 class="footer-heading font-semibold mb-4 text-sm"> <h4 class="footer-heading">
Shop Shop
</h4> </h4>
<ul class="flex flex-col gap-2 text-sm"> <ul class="footer-nav">
<%= if @mode == :preview do %> <%= if @mode == :preview do %>
<li> <li>
<a <a
href="#" href="#"
phx-click="change_preview_page" phx-click="change_preview_page"
phx-value-page="collection" phx-value-page="collection"
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
All products All products
</a> </a>
@ -550,7 +536,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
href="#" href="#"
phx-click="change_preview_page" phx-click="change_preview_page"
phx-value-page="collection" phx-value-page="collection"
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
{category.name} {category.name}
</a> </a>
@ -560,7 +546,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
<li> <li>
<.link <.link
navigate="/collections/all" navigate="/collections/all"
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
All products All products
</.link> </.link>
@ -569,7 +555,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
<li> <li>
<.link <.link
navigate={"/collections/#{category.slug}"} navigate={"/collections/#{category.slug}"}
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
{category.name} {category.name}
</.link> </.link>
@ -579,17 +565,17 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
</ul> </ul>
</div> </div>
<div> <div>
<h4 class="footer-heading font-semibold mb-4 text-sm"> <h4 class="footer-heading">
Help Help
</h4> </h4>
<ul class="flex flex-col gap-2 text-sm"> <ul class="footer-nav">
<%= if @mode == :preview do %> <%= if @mode == :preview do %>
<li> <li>
<a <a
href="#" href="#"
phx-click="change_preview_page" phx-click="change_preview_page"
phx-value-page="delivery" phx-value-page="delivery"
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
Delivery & returns Delivery & returns
</a> </a>
@ -599,7 +585,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
href="#" href="#"
phx-click="change_preview_page" phx-click="change_preview_page"
phx-value-page="privacy" phx-value-page="privacy"
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
Privacy policy Privacy policy
</a> </a>
@ -609,7 +595,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
href="#" href="#"
phx-click="change_preview_page" phx-click="change_preview_page"
phx-value-page="terms" phx-value-page="terms"
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
Terms of service Terms of service
</a> </a>
@ -619,7 +605,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
href="#" href="#"
phx-click="change_preview_page" phx-click="change_preview_page"
phx-value-page="contact" phx-value-page="contact"
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
Contact Contact
</a> </a>
@ -628,7 +614,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
<li> <li>
<.link <.link
navigate="/delivery" navigate="/delivery"
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
Delivery & returns Delivery & returns
</.link> </.link>
@ -636,7 +622,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
<li> <li>
<.link <.link
navigate="/privacy" navigate="/privacy"
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
Privacy policy Privacy policy
</.link> </.link>
@ -644,7 +630,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
<li> <li>
<.link <.link
navigate="/terms" navigate="/terms"
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
Terms of service Terms of service
</.link> </.link>
@ -652,7 +638,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
<li> <li>
<.link <.link
navigate="/contact" navigate="/contact"
class="footer-link transition-colors hover:opacity-80" class="footer-link"
> >
Contact Contact
</.link> </.link>
@ -664,8 +650,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
</div> </div>
<!-- Bottom Bar --> <!-- Bottom Bar -->
<div class="footer-bottom mt-12 pt-8 flex flex-col md:flex-row justify-between items-center gap-4"> <div class="footer-bottom">
<p class="footer-copyright text-xs"> <p class="footer-copyright">
© {@current_year} {@theme_settings.site_name} © {@current_year} {@theme_settings.site_name}
</p> </p>
<.social_links /> <.social_links />
@ -702,7 +688,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
def shop_header(assigns) do def shop_header(assigns) do
~H""" ~H"""
<header class="shop-header px-2 py-2 sm:px-4 sm:py-3 md:px-8 md:py-4"> <header class="shop-header">
<%= if @theme_settings.header_background_enabled && @header_image do %> <%= if @theme_settings.header_background_enabled && @header_image do %>
<div style={header_background_style(@theme_settings, @header_image)} /> <div style={header_background_style(@theme_settings, @header_image)} />
<% end %> <% end %>
@ -716,7 +702,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
/> />
</div> </div>
<nav class="shop-nav hidden md:flex md:gap-6"> <nav class="shop-nav">
<%= if @mode == :preview do %> <%= if @mode == :preview do %>
<.nav_item label="Home" page="home" active_page={@active_page} mode={:preview} /> <.nav_item label="Home" page="home" active_page={@active_page} mode={:preview} />
<.nav_item <.nav_item
@ -742,15 +728,14 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
<% end %> <% end %>
</nav> </nav>
<div class="shop-actions flex items-center"> <div class="shop-actions">
<.link <.link
:if={@is_admin} :if={@is_admin}
href="/admin" href="/admin"
class="header-icon-btn w-9 h-9 flex items-center justify-center transition-all" class="header-icon-btn"
aria-label="Admin" aria-label="Admin"
> >
<svg <svg
class="w-5 h-5"
width="20" width="20"
height="20" height="20"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -772,12 +757,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
</.link> </.link>
<button <button
type="button" type="button"
class="header-icon-btn w-9 h-9 flex items-center justify-center transition-all" class="header-icon-btn"
phx-click={Phoenix.LiveView.JS.dispatch("open-search", to: "#search-modal")} phx-click={Phoenix.LiveView.JS.dispatch("open-search", to: "#search-modal")}
aria-label="Search" aria-label="Search"
> >
<svg <svg
class="w-5 h-5"
width="20" width="20"
height="20" height="20"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -791,12 +775,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
</button> </button>
<button <button
type="button" type="button"
class="header-icon-btn w-9 h-9 flex items-center justify-center transition-all relative" class="header-icon-btn"
phx-click={open_cart_drawer_js()} phx-click={open_cart_drawer_js()}
aria-label="Cart" aria-label="Cart"
> >
<svg <svg
class="w-5 h-5"
width="20" width="20"
height="20" height="20"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -875,7 +858,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Layout do
<img <img
src={logo_url(@logo_image, @theme_settings)} src={logo_url(@logo_image, @theme_settings)}
alt={@theme_settings.site_name} alt={@theme_settings.site_name}
class="shop-logo-img mr-2" class="shop-logo-img"
style={"height: #{@theme_settings.logo_size}px;"} style={"height: #{@theme_settings.logo_size}px;"}
/> />
<% end %> <% end %>