replace Tailwind utilities in product + cart components with CSS (Phase 5b)

Remove ~140 Tailwind utility classes from product.ex and cart.ex, replacing
with semantic CSS classes in components.css. Delete helper functions that
generated Tailwind class strings (card_classes, image_container_classes,
content_padding_class, title_classes, hero_cta_classes, grid_classes).
Use data-* attributes for variant styling, grid columns, and sticky
positioning. Update theme-layer2 selectors for renamed classes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jamey 2026-02-17 10:32:48 +00:00
parent fc9c33ab0c
commit 04b6ee3f37
5 changed files with 775 additions and 239 deletions

View File

@ -18,6 +18,8 @@
.product-card { .product-card {
background-color: var(--t-surface-raised); background-color: var(--t-surface-raised);
border-radius: var(--t-radius-card); border-radius: var(--t-radius-card);
overflow: hidden;
transition: all 0.2s ease;
&[data-variant="default"], &[data-variant="default"],
&[data-variant="compact"] { &[data-variant="compact"] {
@ -33,6 +35,10 @@
cursor: pointer; cursor: pointer;
} }
&[data-variant="featured"]:hover {
transform: translateY(-0.25rem);
}
&[data-clickable] { &[data-clickable] {
position: relative; position: relative;
} }
@ -45,10 +51,32 @@
.product-card-image-wrap { .product-card-image-wrap {
z-index: 1; z-index: 1;
overflow: hidden;
position: relative;
background-color: #e5e7eb;
}
.product-card-image-wrap img {
width: 100%;
height: 100%;
object-fit: cover;
}
.product-image-primary {
transition: opacity 0.3s ease;
} }
.product-card-placeholder { .product-card-placeholder {
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
display: flex;
align-items: center;
justify-content: center;
}
.placeholder-icon {
width: 3rem;
height: 3rem;
opacity: 0.4;
} }
.product-card-category { .product-card-category {
@ -56,10 +84,19 @@
text-decoration: none; text-decoration: none;
position: relative; position: relative;
z-index: 1; z-index: 1;
font-size: var(--t-text-caption);
margin-bottom: 0.25rem;
display: block;
}
.product-card-category:where(a):hover {
text-decoration: underline;
} }
.product-card-title { .product-card-title {
color: var(--t-text-primary); color: var(--t-text-primary);
font-weight: 600;
margin-bottom: 0.5rem;
} }
.product-card[data-variant="default"] .product-card-title, .product-card[data-variant="default"] .product-card-title,
@ -67,8 +104,28 @@
font-family: var(--t-font-heading); font-family: var(--t-font-heading);
} }
.product-card[data-variant="featured"] .product-card-title {
font-size: var(--t-text-small);
font-weight: 500;
margin-bottom: 0.25rem;
}
.product-card[data-variant="compact"] .product-card-title {
font-size: var(--t-text-small);
margin-bottom: 0.25rem;
}
.product-card[data-variant="minimal"] .product-card-title {
font-size: var(--t-text-caption);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.product-card-delivery { .product-card-delivery {
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
font-size: var(--t-text-caption);
margin-top: 0.25rem;
} }
/* ── Product prices (shared between cards and PDP) ── */ /* ── Product prices (shared between cards and PDP) ── */
@ -79,6 +136,7 @@
.product-price--compare { .product-price--compare {
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
text-decoration: line-through;
} }
.product-price--regular { .product-price--regular {
@ -89,14 +147,66 @@
color: var(--t-text-secondary); color: var(--t-text-secondary);
} }
/* Card-variant price sizing */
.product-card[data-variant="default"] .product-price--sale,
.product-card[data-variant="default"] .product-price--regular {
font-size: var(--t-text-large);
font-weight: 700;
}
.product-card[data-variant="default"] .product-price--compare {
font-size: var(--t-text-small);
margin-left: 0.5rem;
}
.product-card[data-variant="featured"] .product-price--secondary {
font-size: var(--t-text-small);
}
.product-card[data-variant="featured"] .product-price--compare {
margin-right: 0.25rem;
}
.product-card[data-variant="compact"] .product-price--regular {
font-weight: 700;
}
.product-card[data-variant="minimal"] .product-price--secondary {
font-size: var(--t-text-caption);
}
/* PDP price sizing */
.pdp-price-row {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1.5rem;
}
.pdp-price-row .product-price--sale,
.pdp-price-row .product-price--regular {
font-size: var(--t-heading-lg);
font-weight: 700;
}
.pdp-price-row .product-price--compare {
font-size: var(--t-text-xl);
}
.sale-badge { .sale-badge {
background-color: var(--t-sale-color); background-color: var(--t-sale-color);
padding: 0.25rem 0.5rem;
font-size: var(--t-text-small);
font-weight: 700;
color: #fff;
border-radius: var(--t-radius-sm, 4px);
} }
/* ── Hero section ── */ /* ── Hero section ── */
.hero-section { .hero-section {
padding: var(--space-2xl) var(--space-lg); padding: var(--space-2xl) var(--space-lg);
text-align: center;
&[data-background="base"] { &[data-background="base"] {
background-color: var(--t-surface-base); background-color: var(--t-surface-base);
@ -105,21 +215,73 @@
&[data-background="sunken"] { &[data-background="sunken"] {
background-color: var(--t-surface-sunken); background-color: var(--t-surface-sunken);
} }
& .t-heading {
font-size: var(--t-heading-lg);
margin-bottom: 1rem;
}
} }
.hero-section--page { .hero-section--page {
padding-top: var(--space-2xl); padding-top: var(--space-2xl);
text-align: center;
& .t-heading {
font-size: var(--t-heading-xl);
margin-bottom: 1.5rem;
}
& .hero-description {
margin-bottom: 3rem;
max-width: 42rem;
}
}
.hero-error {
text-align: center;
& .t-heading {
font-size: var(--t-heading-lg);
margin-bottom: 1.5rem;
}
& .hero-description {
margin-bottom: 2rem;
max-width: 28rem;
}
} }
.hero-pre-title { .hero-pre-title {
font-family: var(--t-font-heading); font-family: var(--t-font-heading);
font-weight: var(--t-heading-weight); font-weight: var(--t-heading-weight);
color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
font-size: var(--t-heading-display);
margin-bottom: 1rem;
} }
.hero-description { .hero-description {
color: var(--t-text-secondary); color: var(--t-text-secondary);
line-height: 1.6; line-height: 1.6;
font-size: var(--t-text-large);
max-width: 32rem;
margin-inline: auto;
margin-bottom: 2rem;
}
.hero-cta-group {
display: flex;
flex-direction: column;
gap: 1rem;
justify-content: center;
}
.hero-cta {
padding: 0.75rem 2rem;
font-weight: 600;
transition: all 0.2s ease;
text-decoration: none;
display: inline-block;
cursor: pointer;
} }
/* ── Category nav ── */ /* ── Category nav ── */
@ -129,14 +291,49 @@
background-color: var(--t-surface-base); background-color: var(--t-surface-base);
} }
.category-nav {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
max-width: 48rem;
margin-inline: auto;
}
.category-card { .category-card {
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
padding: 1rem;
border-radius: 0.5rem;
transition: background-color 0.15s ease;
}
.category-card:hover {
background-color: rgba(0, 0, 0, 0.05);
}
.category-image {
width: 6rem;
height: 6rem;
border-radius: 9999px;
background-color: #e5e7eb;
background-size: cover;
background-position: center;
transition: transform 0.15s ease;
}
.category-card:hover .category-image {
transform: scale(1.05);
} }
.category-name { .category-name {
font-family: var(--t-font-body); font-family: var(--t-font-body);
color: var(--t-text-primary); color: var(--t-text-primary);
font-size: var(--t-text-small);
font-weight: 500;
} }
/* ── Featured products section ── */ /* ── Featured products section ── */
@ -144,6 +341,16 @@
.featured-section { .featured-section {
padding: var(--space-xl) var(--space-lg); padding: var(--space-xl) var(--space-lg);
background-color: var(--t-surface-sunken); background-color: var(--t-surface-sunken);
& .t-heading {
font-size: var(--t-text-2xl);
margin-bottom: 1.5rem;
}
}
.featured-cta-wrap {
text-align: center;
margin-top: 2rem;
} }
.outline-button { .outline-button {
@ -153,6 +360,10 @@
border-radius: var(--t-radius-button); border-radius: var(--t-radius-button);
cursor: pointer; cursor: pointer;
text-decoration: none; text-decoration: none;
padding: 0.75rem 1.5rem;
font-weight: 500;
transition: all 0.2s ease;
display: inline-block;
} }
/* ── Image + text section ── */ /* ── Image + text section ── */
@ -160,21 +371,38 @@
.image-text-section { .image-text-section {
padding: var(--space-2xl) var(--space-lg); padding: var(--space-2xl) var(--space-lg);
background-color: var(--t-surface-base); background-color: var(--t-surface-base);
display: grid;
grid-template-columns: 1fr;
gap: 3rem;
align-items: center;
& .t-heading {
font-size: var(--t-text-2xl);
margin-bottom: 1rem;
}
} }
.image-text-image { .image-text-image {
border-radius: var(--t-radius-image); border-radius: var(--t-radius-image);
height: 18rem;
background-size: cover;
background-position: center;
} }
.image-text-body { .image-text-body {
color: var(--t-text-secondary); color: var(--t-text-secondary);
line-height: 1.7; line-height: 1.7;
font-size: var(--t-text-base);
margin-bottom: 1rem;
} }
.accent-link { .accent-link {
color: var(--t-accent-text, hsl(var(--t-accent-h) var(--t-accent-s) 38%)); color: var(--t-accent-text, hsl(var(--t-accent-h) var(--t-accent-s) 38%));
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
font-size: var(--t-text-small);
font-weight: 500;
transition: color 0.15s ease;
} }
/* ── Collection header ── */ /* ── Collection header ── */
@ -182,12 +410,41 @@
.collection-header-wrap { .collection-header-wrap {
background-color: var(--t-surface-raised); background-color: var(--t-surface-raised);
border-color: var(--t-border-default); border-color: var(--t-border-default);
border-bottom: 1px solid var(--t-border-default);
& .t-heading {
font-size: var(--t-heading-lg);
margin-bottom: 0.5rem;
}
}
.collection-header-inner {
max-width: 80rem;
margin-inline: auto;
padding: 2rem 1rem;
} }
.collection-header-meta { .collection-header-meta {
color: var(--t-text-secondary); color: var(--t-text-secondary);
} }
/* ── Filter bar ── */
.filter-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.5rem;
}
.filter-pills-container {
display: flex;
gap: 0.5rem;
overflow-x: auto;
}
/* ── Breadcrumb ── */ /* ── Breadcrumb ── */
.breadcrumb { .breadcrumb {
@ -196,12 +453,22 @@
& [aria-current="page"] { & [aria-current="page"] {
color: var(--t-text-primary); color: var(--t-text-primary);
} }
& a:hover {
text-decoration: underline;
}
} }
/* ── Related products ── */ /* ── Related products ── */
.related-section { .related-section {
border-top: 1px solid var(--t-border-default); border-top: 1px solid var(--t-border-default);
padding-block: 3rem;
& .t-heading {
font-size: var(--t-text-2xl);
margin-bottom: 1.5rem;
}
} }
/* ── PDP gallery ── */ /* ── PDP gallery ── */
@ -209,16 +476,42 @@
.pdp-gallery-frame { .pdp-gallery-frame {
border-radius: var(--t-radius-image); border-radius: var(--t-radius-image);
overflow: hidden; overflow: hidden;
position: relative;
}
.pdp-gallery-single img {
width: 100%;
height: 100%;
object-fit: cover;
} }
.pdp-thumbnail { .pdp-thumbnail {
border-radius: var(--t-radius-image); border-radius: var(--t-radius-image);
aspect-ratio: 1 / 1;
background-color: #e5e7eb;
overflow: hidden;
border: none;
padding: 0;
cursor: pointer;
}
.pdp-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
} }
/* ── Variant selector ── */ /* ── Variant selector ── */
.variant-selector {
margin-bottom: 1.5rem;
}
.variant-label { .variant-label {
color: var(--t-text-primary); color: var(--t-text-primary);
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
} }
.variant-label-value { .variant-label-value {
@ -226,8 +519,21 @@
font-weight: normal; font-weight: normal;
} }
.variant-options {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.color-swatch { .color-swatch {
border-color: var(--t-border-default); width: 2.5rem;
height: 2.5rem;
border-radius: 9999px;
border: 2px solid var(--t-border-default);
transition: all 0.2s ease;
position: relative;
cursor: pointer;
padding: 0;
&[aria-pressed="true"] { &[aria-pressed="true"] {
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
@ -240,6 +546,10 @@
border-radius: var(--t-radius-button); border-radius: var(--t-radius-button);
color: var(--t-text-primary); color: var(--t-text-primary);
background: transparent; background: transparent;
padding: 0.5rem 1rem;
font-weight: 500;
transition: all 0.2s ease;
cursor: pointer;
&[aria-pressed="true"] { &[aria-pressed="true"] {
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
@ -249,30 +559,60 @@
/* ── Quantity selector ── */ /* ── Quantity selector ── */
.quantity-selector {
margin-bottom: 2rem;
}
.qty-label { .qty-label {
color: var(--t-text-primary); color: var(--t-text-primary);
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
}
.qty-row {
display: flex;
align-items: center;
gap: 1rem;
} }
.qty-group { .qty-group {
border: 2px solid var(--t-border-default); border: 2px solid var(--t-border-default);
border-radius: var(--t-radius-input); border-radius: var(--t-radius-input);
display: flex;
align-items: center;
} }
.qty-btn { .qty-btn {
color: var(--t-text-primary); color: var(--t-text-primary);
padding: 0.5rem 1rem;
background: none;
border: none;
cursor: pointer;
&:disabled {
opacity: 0.3;
}
} }
.qty-display { .qty-display {
border-color: var(--t-border-default);
color: var(--t-text-primary); color: var(--t-text-primary);
padding: 0.5rem 1rem;
border-inline: 2px solid var(--t-border-default);
min-width: 3rem;
text-align: center;
font-variant-numeric: tabular-nums;
} }
.stock-in { .stock-in {
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
font-size: var(--t-text-small);
} }
.stock-out { .stock-out {
color: var(--t-sale-color); color: var(--t-sale-color);
font-size: var(--t-text-small);
font-weight: 600;
} }
/* ── Add to cart ── */ /* ── Add to cart ── */
@ -280,6 +620,17 @@
.atc-wrap { .atc-wrap {
background-color: var(--t-surface-base); background-color: var(--t-surface-base);
border-color: var(--t-border-subtle); border-color: var(--t-border-subtle);
margin-bottom: 1rem;
&[data-sticky="true"] {
position: sticky;
bottom: 0;
z-index: 10;
padding-block: 0.75rem;
margin-inline: -1rem;
padding-inline: 1rem;
border-top: 1px solid var(--t-border-subtle);
}
} }
.atc-btn { .atc-btn {
@ -288,6 +639,11 @@
border-radius: var(--t-radius-button); border-radius: var(--t-radius-button);
border: none; border: none;
cursor: pointer; cursor: pointer;
width: 100%;
padding: 0.75rem 1.5rem;
font-size: var(--t-text-large);
font-weight: 600;
transition: all 0.2s ease;
&:disabled { &:disabled {
background-color: var(--t-border-default); background-color: var(--t-border-default);
@ -299,10 +655,35 @@
.accordion-summary { .accordion-summary {
color: var(--t-text-primary); color: var(--t-text-primary);
display: flex;
justify-content: space-between;
align-items: center;
padding-block: 1rem;
cursor: pointer;
list-style: none;
&::-webkit-details-marker {
display: none;
}
}
.accordion-title {
font-weight: 600;
}
.accordion-icon {
width: 1.25rem;
height: 1.25rem;
transition: transform 0.2s ease;
}
details[open] > .accordion-summary .accordion-icon {
transform: rotate(180deg);
} }
.accordion-body { .accordion-body {
color: var(--t-text-secondary); color: var(--t-text-secondary);
padding-bottom: 1rem;
} }
/* ── Product details ── */ /* ── Product details ── */
@ -311,6 +692,20 @@
border-top: 1px solid var(--t-border-subtle); border-top: 1px solid var(--t-border-subtle);
border-bottom: 1px solid var(--t-border-subtle); border-bottom: 1px solid var(--t-border-subtle);
border-color: var(--t-border-subtle); border-color: var(--t-border-subtle);
margin-top: 2rem;
& > * + * {
border-top: 1px solid var(--t-border-subtle);
}
}
.details-description {
line-height: 1.625;
}
.details-table {
width: 100%;
font-size: var(--t-text-small);
} }
.details-table-row { .details-table-row {
@ -319,10 +714,29 @@
.details-th { .details-th {
color: var(--t-text-primary); color: var(--t-text-primary);
text-align: left;
padding-block: 0.5rem;
font-weight: 600;
}
.details-td {
padding-block: 0.5rem;
} }
.details-subheading { .details-subheading {
color: var(--t-text-primary); color: var(--t-text-primary);
font-weight: 600;
margin-bottom: 0.25rem;
}
.details-shipping {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.details-shipping-text {
font-size: var(--t-text-small);
} }
/* ── Announcement bar ── */ /* ── Announcement bar ── */
@ -784,6 +1198,11 @@
padding: 0.5rem; padding: 0.5rem;
cursor: pointer; cursor: pointer;
color: var(--t-text-secondary); color: var(--t-text-secondary);
& svg {
width: 1.25rem;
height: 1.25rem;
}
} }
.cart-drawer-items { .cart-drawer-items {
@ -829,6 +1248,7 @@
border: none; border: none;
cursor: pointer; cursor: pointer;
font-family: var(--t-font-body); font-family: var(--t-font-body);
margin-bottom: 0.5rem;
} }
/* ── Cart item row ── */ /* ── Cart item row ── */
@ -910,6 +1330,8 @@
.cart-qty-group { .cart-qty-group {
border: 1px solid var(--t-border-default); border: 1px solid var(--t-border-default);
border-radius: var(--t-radius-input); border-radius: var(--t-radius-input);
display: flex;
align-items: center;
} }
.cart-qty-btn { .cart-qty-btn {
@ -917,13 +1339,15 @@
border: none; border: none;
cursor: pointer; cursor: pointer;
color: var(--t-text-primary); color: var(--t-text-primary);
padding: 0.25rem 0.75rem;
} }
.cart-qty-display { .cart-qty-display {
border-color: var(--t-border-default);
color: var(--t-text-primary); color: var(--t-text-primary);
min-width: 2rem; min-width: 2rem;
text-align: center; text-align: center;
padding: 0.25rem 0.75rem;
border-inline: 1px solid var(--t-border-default);
} }
.cart-qty-text { .cart-qty-text {
@ -933,6 +1357,7 @@
.cart-item-price-col { .cart-item-price-col {
flex-shrink: 0; flex-shrink: 0;
text-align: right;
} }
.cart-item-price { .cart-item-price {
@ -961,10 +1386,20 @@
.cart-empty { .cart-empty {
color: var(--t-text-secondary); color: var(--t-text-secondary);
text-align: center;
padding-block: 2rem;
} }
.cart-empty-icon { .cart-empty-icon {
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
width: 4rem;
height: 4rem;
margin-inline: auto;
margin-bottom: 1rem;
}
.cart-empty p {
margin-bottom: 1rem;
} }
.cart-continue-link { .cart-continue-link {
@ -980,25 +1415,66 @@
/* ── Cart page item (full size) ── */ /* ── Cart page item (full size) ── */
.cart-page-item {
display: flex;
gap: 1rem;
padding: 1rem;
}
.cart-page-image { .cart-page-image {
border-radius: var(--t-radius-image); border-radius: var(--t-radius-image);
width: 6rem;
height: 6rem;
flex-shrink: 0;
background-color: #e5e7eb;
overflow: hidden;
& img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.cart-page-item-info {
flex: 1;
} }
.cart-page-item-name { .cart-page-item-name {
font-family: var(--t-font-heading); font-family: var(--t-font-heading);
color: var(--t-text-primary); color: var(--t-text-primary);
font-weight: 600;
margin-bottom: 0.25rem;
} }
.cart-page-item-variant { .cart-page-item-variant {
color: var(--t-text-secondary); color: var(--t-text-secondary);
font-size: var(--t-text-small);
margin-bottom: 0.5rem;
}
.cart-page-item-actions {
display: flex;
align-items: center;
gap: 1rem;
} }
.cart-page-item-remove { .cart-page-item-remove {
color: var(--t-text-tertiary); color: var(--t-text-tertiary);
font-size: var(--t-text-small);
background: none;
border: none;
cursor: pointer;
}
.cart-page-item-price-col {
text-align: right;
} }
.cart-page-item-price { .cart-page-item-price {
color: var(--t-text-primary); color: var(--t-text-primary);
font-weight: 700;
font-size: var(--t-text-large);
} }
/* ── Delivery line ── */ /* ── Delivery line ── */
@ -1007,12 +1483,21 @@
font-family: var(--t-font-body); font-family: var(--t-font-body);
font-size: var(--t-text-small); font-size: var(--t-text-small);
color: var(--t-text-secondary); color: var(--t-text-secondary);
display: flex;
justify-content: space-between;
align-items: center;
& form { & form {
display: inline; display: inline;
} }
} }
.delivery-line-label {
display: flex;
align-items: center;
gap: 0.25rem;
}
.delivery-select { .delivery-select {
appearance: auto; appearance: auto;
background: transparent; background: transparent;
@ -1027,9 +1512,30 @@
/* ── Order summary ── */ /* ── Order summary ── */
.order-summary-card {
padding: 1.5rem;
position: sticky;
top: 1rem;
}
.order-summary-heading { .order-summary-heading {
font-family: var(--t-font-heading); font-family: var(--t-font-heading);
color: var(--t-text-primary); color: var(--t-text-primary);
font-size: var(--t-text-xl);
font-weight: 700;
margin-bottom: 1.5rem;
}
.order-summary-lines {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1.5rem;
}
.order-summary-line {
display: flex;
justify-content: space-between;
} }
.order-summary-label { .order-summary-label {
@ -1041,7 +1547,34 @@
} }
.order-summary-divider { .order-summary-divider {
border-color: var(--t-border-default); border-top: 1px solid var(--t-border-default);
padding-top: 0.75rem;
}
.order-summary-total {
display: flex;
justify-content: space-between;
font-size: var(--t-text-large);
}
.order-summary-checkout {
width: 100%;
padding: 0.75rem 1.5rem;
font-weight: 600;
transition: all 0.2s ease;
}
.order-summary-checkout-form {
margin-bottom: 0.75rem;
}
.order-summary-continue {
display: block;
width: 100%;
padding: 0.75rem 1.5rem;
font-weight: 600;
transition: all 0.2s ease;
text-align: center;
} }
/* ── Content body ── */ /* ── Content body ── */
@ -1498,6 +2031,42 @@
object-fit: cover; object-fit: cover;
} }
/* ── Product grid ── */
.product-grid {
display: grid;
}
.product-grid[data-columns="fixed-4"] {
grid-template-columns: repeat(2, 1fr);
}
.product-grid:not([data-columns="fixed-4"]) {
grid-template-columns: 1fr;
}
/* ── Cart layout (cart page) ── */
.cart-layout {
display: grid;
grid-template-columns: 1fr;
gap: 2rem;
}
.cart-items-stack {
display: flex;
flex-direction: column;
gap: 1rem;
}
/* ── Error page product grid ── */
.error-container .product-grid {
margin-top: 3rem;
max-width: 36rem;
margin-inline: auto;
}
/* ── Screen reader only ── */ /* ── Screen reader only ── */
.sr-only { .sr-only {
@ -1516,9 +2085,14 @@
@media (min-width: 640px) { @media (min-width: 640px) {
.page-container { padding-inline: 1.5rem; } .page-container { padding-inline: 1.5rem; }
.collection-header-inner { padding-inline: 1.5rem; }
.shop-header { padding: 0.75rem 1rem; } .shop-header { padding: 0.75rem 1rem; }
.shop-footer-inner { padding-inline: 1.5rem; } .shop-footer-inner { padding-inline: 1.5rem; }
.search-kbd { display: flex; } .search-kbd { display: flex; }
.product-grid:not([data-columns="fixed-4"]) {
grid-template-columns: repeat(2, 1fr);
}
.hero-cta-group { flex-direction: row; }
} }
@media (min-width: 768px) { @media (min-width: 768px) {
@ -1530,11 +2104,33 @@
.footer-bottom { flex-direction: row; } .footer-bottom { flex-direction: row; }
.pdp-grid { grid-template-columns: repeat(2, 1fr); } .pdp-grid { grid-template-columns: repeat(2, 1fr); }
.contact-grid { grid-template-columns: repeat(2, 1fr); } .contact-grid { grid-template-columns: repeat(2, 1fr); }
.product-grid[data-columns="fixed-4"] {
grid-template-columns: repeat(4, 1fr);
}
.image-text-section { grid-template-columns: repeat(2, 1fr); }
.collection-header-wrap .t-heading { font-size: var(--t-heading-xl); }
.hero-section .t-heading { font-size: var(--t-heading-xl); }
.hero-section--page .t-heading { font-size: var(--t-heading-display); }
.hero-error .t-heading { font-size: var(--t-heading-xl); }
.pdp-price-row .product-price--sale,
.pdp-price-row .product-price--regular { font-size: var(--t-heading-xl); }
.atc-wrap[data-sticky="true"] {
position: relative;
padding-block: 0;
margin-inline: 0;
padding-inline: 0;
border-top: none;
}
} }
@media (min-width: 1024px) { @media (min-width: 1024px) {
.page-container { padding-inline: 2rem; } .page-container { padding-inline: 2rem; }
.collection-header-inner { padding-inline: 2rem; }
.shop-footer-inner { padding-inline: 2rem; } .shop-footer-inner { padding-inline: 2rem; }
.cart-grid { grid-template-columns: 2fr 1fr; } .cart-grid { grid-template-columns: 2fr 1fr; }
.cart-layout { grid-template-columns: 2fr 1fr; }
.product-grid[data-columns="2"] { grid-template-columns: repeat(2, 1fr); }
.product-grid[data-columns="3"] { grid-template-columns: repeat(3, 1fr); }
.product-grid[data-columns="4"] { grid-template-columns: repeat(4, 1fr); }
} }
} }

View File

@ -85,15 +85,15 @@
} }
/* Layout Width */ /* Layout Width */
&[data-layout="contained"] .max-w-7xl { &[data-layout="contained"] :is(.max-w-7xl, .page-container, .collection-header-inner) {
max-width: var(--t-layout-max-width, 1100px); max-width: var(--t-layout-max-width, 1100px);
} }
&[data-layout="wide"] .max-w-7xl { &[data-layout="wide"] :is(.max-w-7xl, .page-container, .collection-header-inner) {
max-width: var(--t-layout-max-width, 1400px); max-width: var(--t-layout-max-width, 1400px);
} }
&[data-layout="full"] .max-w-7xl { &[data-layout="full"] :is(.max-w-7xl, .page-container, .collection-header-inner) {
max-width: var(--t-layout-max-width, 100%); max-width: var(--t-layout-max-width, 100%);
padding-left: 2rem; padding-left: 2rem;
padding-right: 2rem; padding-right: 2rem;
@ -175,7 +175,7 @@
} }
/* Image Aspect Ratio */ /* Image Aspect Ratio */
& .product-card .product-image-container { & .product-card .product-card-image-wrap {
aspect-ratio: var(--t-image-aspect-ratio, 1 / 1); aspect-ratio: var(--t-image-aspect-ratio, 1 / 1);
} }
@ -330,7 +330,7 @@
} }
/* Product Card Images — mobile: swipe, desktop: hover crossfade */ /* Product Card Images — mobile: swipe, desktop: hover crossfade */
.product-image-container { .product-card-image-wrap {
position: relative; position: relative;
} }

View File

@ -18,7 +18,7 @@
mode={@mode} mode={@mode}
/> />
<.product_grid columns={:fixed_4} gap="gap-4" class="mt-12 max-w-xl mx-auto"> <.product_grid columns={:fixed_4}>
<%= for product <- Enum.take(assigns[:products] || [], 4) do %> <%= for product <- Enum.take(assigns[:products] || [], 4) do %>
<.product_card <.product_card
product={product} product={product}

View File

@ -92,7 +92,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
aria-label="Close cart" aria-label="Close cart"
> >
<svg <svg
class="w-5 h-5" class="cart-drawer-close-icon"
width="20" width="20"
height="20" height="20"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -134,7 +134,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
<%= if @mode == :preview do %> <%= if @mode == :preview do %>
<button <button
type="button" type="button"
class="cart-drawer-checkout w-full mb-2" class="cart-drawer-checkout"
> >
Checkout Checkout
</button> </button>
@ -143,7 +143,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} /> <input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
<button <button
type="submit" type="submit"
class="cart-drawer-checkout w-full mb-2" class="cart-drawer-checkout"
> >
Checkout Checkout
</button> </button>
@ -213,24 +213,24 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
<div class="cart-item-actions"> <div class="cart-item-actions">
<%= if @show_quantity_controls do %> <%= if @show_quantity_controls do %>
<div class="cart-qty-group flex items-center"> <div class="cart-qty-group">
<button <button
type="button" type="button"
phx-click="decrement" phx-click="decrement"
phx-value-id={@item.variant_id} phx-value-id={@item.variant_id}
class="cart-qty-btn px-3 py-1" class="cart-qty-btn"
aria-label={"Decrease quantity of #{@item.name}"} aria-label={"Decrease quantity of #{@item.name}"}
> >
</button> </button>
<span class="cart-qty-display px-3 py-1 border-x"> <span class="cart-qty-display">
{@item.quantity} {@item.quantity}
</span> </span>
<button <button
type="button" type="button"
phx-click="increment" phx-click="increment"
phx-value-id={@item.variant_id} phx-value-id={@item.variant_id}
class="cart-qty-btn px-3 py-1" class="cart-qty-btn"
aria-label={"Increase quantity of #{@item.name}"} aria-label={"Increase quantity of #{@item.name}"}
> >
+ +
@ -246,7 +246,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
</div> </div>
</div> </div>
<div class="cart-item-price-col text-right"> <div class="cart-item-price-col">
<p class="cart-item-price" data-size={if @size == :compact, do: "compact"}> <p class="cart-item-price" data-size={if @size == :compact, do: "compact"}>
{SimpleshopTheme.Cart.format_price(@item.price * @item.quantity)} {SimpleshopTheme.Cart.format_price(@item.price * @item.quantity)}
</p> </p>
@ -262,9 +262,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
def cart_empty_state(assigns) do def cart_empty_state(assigns) do
~H""" ~H"""
<div class="cart-empty text-center py-8"> <div class="cart-empty">
<svg <svg
class="cart-empty-icon w-16 h-16 mx-auto mb-4" class="cart-empty-icon"
width="64" width="64"
height="64" height="64"
viewBox="0 0 24 24" viewBox="0 0 24 24"
@ -276,7 +276,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
<line x1="3" y1="6" x2="21" y2="6"></line> <line x1="3" y1="6" x2="21" y2="6"></line>
<path d="M16 10a4 4 0 01-8 0"></path> <path d="M16 10a4 4 0 01-8 0"></path>
</svg> </svg>
<p class="mb-4">Your basket is empty</p> <p>Your basket is empty</p>
<%= if @mode == :preview do %> <%= if @mode == :preview do %>
<button <button
type="button" type="button"
@ -334,43 +334,42 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
def cart_item(assigns) do def cart_item(assigns) do
~H""" ~H"""
<.shop_card class="flex gap-4 p-4"> <.shop_card class="cart-page-item">
<div class="cart-page-image w-24 h-24 flex-shrink-0 bg-gray-200 overflow-hidden"> <div class="cart-page-image">
<img <img
src={cart_item_image(@item.product)} src={cart_item_image(@item.product)}
alt={@item.product.title} alt={@item.product.title}
width="96" width="96"
height="96" height="96"
loading="lazy" loading="lazy"
class="w-full h-full object-cover"
/> />
</div> </div>
<div class="flex-1"> <div class="cart-page-item-info">
<h3 class="cart-page-item-name font-semibold mb-1"> <h3 class="cart-page-item-name">
{@item.product.title} {@item.product.title}
</h3> </h3>
<p class="cart-page-item-variant text-sm mb-2"> <p class="cart-page-item-variant">
{@item.variant} {@item.variant}
</p> </p>
<div class="flex items-center gap-4"> <div class="cart-page-item-actions">
<div class="cart-qty-group flex items-center"> <div class="cart-qty-group">
<button class="cart-qty-btn px-3 py-1"></button> <button class="cart-qty-btn"></button>
<span class="cart-qty-display px-3 py-1 border-x"> <span class="cart-qty-display">
{@item.quantity} {@item.quantity}
</span> </span>
<button class="cart-qty-btn px-3 py-1">+</button> <button class="cart-qty-btn">+</button>
</div> </div>
<button class="cart-page-item-remove text-sm"> <button class="cart-page-item-remove">
Remove Remove
</button> </button>
</div> </div>
</div> </div>
<div class="text-right"> <div class="cart-page-item-price-col">
<p class="cart-page-item-price font-bold text-lg"> <p class="cart-page-item-price">
{SimpleshopTheme.Cart.format_price(@item.product.cheapest_price * @item.quantity)} {SimpleshopTheme.Cart.format_price(@item.product.cheapest_price * @item.quantity)}
</p> </p>
</div> </div>
@ -391,8 +390,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
defp delivery_line(assigns) do defp delivery_line(assigns) do
~H""" ~H"""
<div class="delivery-line flex justify-between items-center"> <div class="delivery-line">
<span class="flex items-center gap-1"> <span class="delivery-line-label">
Delivery to Delivery to
<%= if @available_countries != [] and @mode != :preview do %> <%= if @available_countries != [] and @mode != :preview do %>
<form phx-change="change_country"> <form phx-change="change_country">
@ -445,15 +444,15 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
assign(assigns, :estimated_total, assigns.subtotal + (assigns.shipping_estimate || 0)) assign(assigns, :estimated_total, assigns.subtotal + (assigns.shipping_estimate || 0))
~H""" ~H"""
<.shop_card class="p-6 sticky top-4"> <.shop_card class="order-summary-card">
<h2 class="order-summary-heading text-xl font-bold mb-6"> <h2 class="order-summary-heading">
Order summary Order summary
</h2> </h2>
<div class="flex flex-col gap-3 mb-6"> <div class="order-summary-lines">
<div class="flex justify-between"> <div class="order-summary-line">
<span class="order-summary-label">Subtotal</span> <span>Subtotal</span>
<span class="order-summary-value"> <span>
{SimpleshopTheme.Cart.format_price(@subtotal)} {SimpleshopTheme.Cart.format_price(@subtotal)}
</span> </span>
</div> </div>
@ -463,12 +462,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
available_countries={@available_countries} available_countries={@available_countries}
mode={@mode} mode={@mode}
/> />
<div class="order-summary-divider border-t pt-3"> <div class="order-summary-divider">
<div class="flex justify-between text-lg"> <div class="order-summary-total">
<span class="order-summary-value font-semibold"> <span>
{if @shipping_estimate, do: "Estimated total", else: "Subtotal"} {if @shipping_estimate, do: "Estimated total", else: "Subtotal"}
</span> </span>
<span class="order-summary-value font-bold"> <span>
{SimpleshopTheme.Cart.format_price(@estimated_total)} {SimpleshopTheme.Cart.format_price(@estimated_total)}
</span> </span>
</div> </div>
@ -476,26 +475,26 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
</div> </div>
<%= if @mode == :preview do %> <%= if @mode == :preview do %>
<.shop_button class="w-full px-6 py-3 font-semibold transition-all mb-3"> <.shop_button class="order-summary-checkout">
Checkout Checkout
</.shop_button> </.shop_button>
<.shop_button_outline <.shop_button_outline
phx-click="change_preview_page" phx-click="change_preview_page"
phx-value-page="collection" phx-value-page="collection"
class="w-full px-6 py-3 font-semibold transition-all" class="order-summary-continue"
> >
Continue shopping Continue shopping
</.shop_button_outline> </.shop_button_outline>
<% else %> <% else %>
<form action="/checkout" method="post" class="mb-3"> <form action="/checkout" method="post" class="order-summary-checkout-form">
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} /> <input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
<.shop_button type="submit" class="w-full px-6 py-3 font-semibold transition-all"> <.shop_button type="submit" class="order-summary-checkout">
Checkout Checkout
</.shop_button> </.shop_button>
</form> </form>
<.shop_link_outline <.shop_link_outline
href="/collections/all" href="/collections/all"
class="block w-full px-6 py-3 font-semibold transition-all text-center" class="order-summary-continue"
> >
Continue shopping Continue shopping
</.shop_link_outline> </.shop_link_outline>
@ -524,14 +523,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
def cart_layout(assigns) do def cart_layout(assigns) do
~H""" ~H"""
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> <div class="cart-layout">
<div class="lg:col-span-2"> <div class="cart-items-stack">
<div class="flex flex-col gap-4">
<%= for item <- @items do %> <%= for item <- @items do %>
<.cart_item item={item} /> <.cart_item item={item} />
<% end %> <% end %>
</div> </div>
</div>
<div> <div>
<.order_summary subtotal={@subtotal} mode={@mode} /> <.order_summary subtotal={@subtotal} mode={@mode} />

View File

@ -62,7 +62,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
~H""" ~H"""
<article <article
class={card_classes(@variant)} class="product-card"
data-variant={@variant} data-variant={@variant}
data-clickable={@clickable_resolved || nil} data-clickable={@clickable_resolved || nil}
> >
@ -103,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={["product-card-image-wrap", image_container_classes(@variant)]}> <div class="product-card-image-wrap">
<%= if @show_badges do %> <%= if @show_badges do %>
<.product_badge product={@product} /> <.product_badge product={@product} />
<% end %> <% end %>
@ -118,13 +118,13 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
alt={@product.title} alt={@product.title}
variant={@variant} variant={@variant}
priority={@priority} priority={@priority}
class="product-image-primary w-full h-full object-cover transition-opacity duration-300" class="product-image-primary"
/> />
<.product_card_image <.product_card_image
image={@hover_image} image={@hover_image}
alt={@product.title} alt={@product.title}
variant={@variant} variant={@variant}
class="product-image-hover w-full h-full object-cover" class="product-image-hover"
/> />
</div> </div>
<% else %> <% else %>
@ -133,7 +133,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
alt={@product.title} alt={@product.title}
variant={@variant} variant={@variant}
priority={@priority} priority={@priority}
class="product-image-primary w-full h-full object-cover" class="product-image-primary"
/> />
<% end %> <% end %>
<%= if @has_hover_image do %> <%= if @has_hover_image do %>
@ -143,22 +143,22 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
</div> </div>
<% end %> <% end %>
</div> </div>
<div class={content_padding_class(@variant)}> <div class="product-card-content">
<%= 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 class="product-card-category text-xs mb-1"> <p class="product-card-category">
{@product.category} {@product.category}
</p> </p>
<% else %> <% else %>
<.link <.link
navigate={"/collections/#{Slug.slugify(@product.category)}"} navigate={"/collections/#{Slug.slugify(@product.category)}"}
class="product-card-category text-xs mb-1 block hover:underline" class="product-card-category"
> >
{@product.category} {@product.category}
</.link> </.link>
<% end %> <% end %>
<% end %> <% end %>
<h3 class={["product-card-title", title_classes(@variant)]}> <h3 class="product-card-title">
<%= if @clickable do %> <%= if @clickable do %>
<%= if @mode == :preview do %> <%= if @mode == :preview do %>
<a <a
@ -185,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="product-card-delivery text-xs mt-1"> <p class="product-card-delivery">
Made to order Made to order
</p> </p>
<% end %> <% end %>
@ -226,7 +226,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<%= cond do %> <%= cond do %>
<% is_nil(@src) -> %> <% is_nil(@src) -> %>
<div <div
class={[@class, "product-card-placeholder flex items-center justify-center"]} class={[@class, "product-card-placeholder"]}
role="img" role="img"
aria-label={@alt} aria-label={@alt}
> >
@ -238,7 +238,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
stroke="currentColor" stroke="currentColor"
width="48" width="48"
height="48" height="48"
class="size-12 opacity-40" class="placeholder-icon"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@ -297,33 +297,33 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<% :default -> %> <% :default -> %>
<div> <div>
<%= if @product.on_sale do %> <%= if @product.on_sale do %>
<span class="product-price--sale text-lg font-bold"> <span class="product-price--sale">
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)} {SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
</span> </span>
<span class="product-price--compare text-sm line-through ml-2"> <span class="product-price--compare">
{SimpleshopTheme.Cart.format_price(@product.compare_at_price)} {SimpleshopTheme.Cart.format_price(@product.compare_at_price)}
</span> </span>
<% else %> <% else %>
<span class="product-price--regular text-lg font-bold"> <span class="product-price--regular">
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)} {SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
</span> </span>
<% end %> <% end %>
</div> </div>
<% :featured -> %> <% :featured -> %>
<p class="product-price--secondary text-sm"> <p class="product-price--secondary">
<%= if @product.on_sale do %> <%= if @product.on_sale do %>
<span class="product-price--compare line-through mr-1"> <span class="product-price--compare">
{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="product-price--regular font-bold"> <p class="product-price--regular">
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)} {SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
</p> </p>
<% :minimal -> %> <% :minimal -> %>
<p class="product-price--secondary text-xs"> <p class="product-price--secondary">
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)} {SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
</p> </p>
<% end %> <% end %>
@ -342,32 +342,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
defp variant_defaults(:minimal), defp variant_defaults(:minimal),
do: %{show_category: false, show_badges: false, show_delivery_text: false, clickable: false} do: %{show_category: false, show_badges: false, show_delivery_text: false, clickable: false}
defp card_classes(:default), do: "product-card group overflow-hidden transition-all"
defp card_classes(:featured),
do: "product-card group overflow-hidden transition-all hover:-translate-y-1"
defp card_classes(:compact), do: "product-card group overflow-hidden cursor-pointer"
defp card_classes(:minimal), do: "product-card group overflow-hidden"
defp image_container_classes(:compact),
do: "product-image-container aspect-square bg-gray-200 overflow-hidden relative"
defp image_container_classes(:minimal),
do: "product-image-container aspect-square bg-gray-200 overflow-hidden relative"
defp image_container_classes(_),
do: "product-image-container bg-gray-200 overflow-hidden relative"
defp content_padding_class(:compact), do: "p-3"
defp content_padding_class(:minimal), do: "p-2"
defp content_padding_class(_), do: ""
defp title_classes(:default), do: "font-semibold mb-2"
defp title_classes(:featured), do: "text-sm font-medium mb-1"
defp title_classes(:compact), do: "font-semibold text-sm mb-1"
defp title_classes(:minimal), do: "text-xs font-semibold truncate"
@doc """ @doc """
Renders a responsive product grid container. Renders a responsive product grid container.
@ -394,57 +368,33 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<% end %> <% end %>
</.product_grid> </.product_grid>
<.product_grid columns={:fixed_4} gap="gap-6"> <.product_grid columns={:fixed_4}>
... ...
</.product_grid> </.product_grid>
""" """
attr :theme_settings, :map, default: nil attr :theme_settings, :map, default: nil
attr :columns, :atom, default: nil attr :columns, :atom, default: nil
attr :gap, :string, default: nil
attr :class, :string, default: nil attr :class, :string, default: nil
slot :inner_block, required: true slot :inner_block, required: true
def product_grid(assigns) do def product_grid(assigns) do
cols =
cond do
assigns.columns == :fixed_4 -> "fixed-4"
assigns.theme_settings != nil -> assigns.theme_settings.grid_columns || "3"
true -> "3"
end
assigns = assign(assigns, :data_columns, cols)
~H""" ~H"""
<div class={grid_classes(@theme_settings, @columns, @gap, @class)}> <div class={["product-grid", @class]} data-columns={@data_columns}>
{render_slot(@inner_block)} {render_slot(@inner_block)}
</div> </div>
""" """
end end
defp grid_classes(theme_settings, columns, gap, extra_class) do
base = "product-grid grid"
cols =
cond do
columns == :fixed_4 ->
"grid-cols-2 md:grid-cols-4"
theme_settings != nil ->
responsive_cols = "grid-cols-1 sm:grid-cols-2"
lg_cols =
case theme_settings.grid_columns do
"2" -> "lg:grid-cols-2"
"3" -> "lg:grid-cols-3"
"4" -> "lg:grid-cols-4"
_ -> "lg:grid-cols-3"
end
"#{responsive_cols} #{lg_cols}"
true ->
"grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
end
gap_class = gap || ""
[base, cols, gap_class, extra_class]
|> Enum.reject(&(is_nil(&1) or &1 == ""))
|> Enum.join(" ")
end
@doc """ @doc """
Renders a centered hero section with title, description, and optional CTAs. Renders a centered hero section with title, description, and optional CTAs.
@ -509,11 +459,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
~H""" ~H"""
<%= case @variant do %> <%= case @variant do %>
<% :default -> %> <% :default -> %>
<section class="hero-section text-center" data-background={@background}> <section class="hero-section" data-background={@background}>
<h1 class="t-heading text-3xl md:text-4xl mb-4"> <h1 class="t-heading">
{@title} {@title}
</h1> </h1>
<p class="hero-description text-lg max-w-lg mx-auto mb-8"> <p class="hero-description">
{@description} {@description}
</p> </p>
<.hero_cta <.hero_cta
@ -526,29 +476,29 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
/> />
</section> </section>
<% :page -> %> <% :page -> %>
<div class="hero-section--page text-center"> <div class="hero-section--page">
<h1 class="t-heading text-4xl md:text-5xl mb-6"> <h1 class="t-heading">
{@title} {@title}
</h1> </h1>
<p class="hero-description text-lg mb-12 max-w-2xl mx-auto"> <p class="hero-description">
{@description} {@description}
</p> </p>
</div> </div>
<% :error -> %> <% :error -> %>
<div class="text-center"> <div class="hero-error">
<%= if @pre_title do %> <%= if @pre_title do %>
<h1 class="hero-pre-title text-8xl md:text-9xl mb-4"> <h1 class="hero-pre-title">
{@pre_title} {@pre_title}
</h1> </h1>
<% end %> <% end %>
<h2 class="t-heading text-3xl md:text-4xl mb-6"> <h2 class="t-heading">
{@title} {@title}
</h2> </h2>
<p class="hero-description text-lg mb-8 max-w-md mx-auto"> <p class="hero-description">
{@description} {@description}
</p> </p>
<%= if @cta_text || @secondary_cta_text do %> <%= if @cta_text || @secondary_cta_text do %>
<div class="flex flex-col sm:flex-row gap-4 justify-center"> <div class="hero-cta-group">
<.hero_cta <.hero_cta
:if={@cta_text} :if={@cta_text}
text={@cta_text} text={@cta_text}
@ -579,19 +529,27 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
attr :variant, :atom, required: true attr :variant, :atom, required: true
defp hero_cta(assigns) do defp hero_cta(assigns) do
base_class =
case assigns.variant do
:primary -> "themed-button hero-cta"
:secondary -> "themed-button-outline hero-cta"
end
assigns = assign(assigns, :cta_class, base_class)
~H""" ~H"""
<%= if @mode == :preview do %> <%= if @mode == :preview do %>
<button <button
phx-click="change_preview_page" phx-click="change_preview_page"
phx-value-page={@page} phx-value-page={@page}
class={hero_cta_classes(@variant)} class={@cta_class}
> >
{@text} {@text}
</button> </button>
<% else %> <% else %>
<.link <.link
navigate={@href || "/"} navigate={@href || "/"}
class={["inline-block", hero_cta_classes(@variant)]} class={@cta_class}
> >
{@text} {@text}
</.link> </.link>
@ -599,11 +557,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
""" """
end end
defp hero_cta_classes(:primary), do: "themed-button px-8 py-3 font-semibold transition-all"
defp hero_cta_classes(:secondary),
do: "themed-button-outline px-8 py-3 font-semibold transition-all"
@doc """ @doc """
Renders a row of category circles for navigation. Renders a row of category circles for navigation.
@ -626,17 +579,17 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
~H""" ~H"""
<section class="category-nav-section"> <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="category-nav" aria-label="Product categories">
<%= for category <- Enum.take(@categories, @limit) do %> <%= for category <- Enum.take(@categories, @limit) do %>
<%= 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="collection" phx-value-page="collection"
class="category-card flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5" class="category-card"
> >
<div <div
class="w-24 h-24 rounded-full bg-gray-200 bg-cover bg-center transition-transform hover:scale-105" class="category-image"
style={ style={
if(category[:image_url], if(category[:image_url],
do: "background-image: url('#{category.image_url}');", do: "background-image: url('#{category.image_url}');",
@ -645,17 +598,17 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
} }
> >
</div> </div>
<span class="category-name text-sm font-medium"> <span class="category-name">
{category.name} {category.name}
</span> </span>
</a> </a>
<% else %> <% else %>
<.link <.link
navigate={"/collections/#{category.slug}"} navigate={"/collections/#{category.slug}"}
class="category-card flex flex-col items-center gap-3 p-4 rounded-lg transition-colors hover:bg-black/5" class="category-card"
> >
<div <div
class="w-24 h-24 rounded-full bg-gray-200 bg-cover bg-center transition-transform hover:scale-105" class="category-image"
style={ style={
if(category[:image_url], if(category[:image_url],
do: "background-image: url('#{category.image_url}');", do: "background-image: url('#{category.image_url}');",
@ -664,7 +617,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
} }
> >
</div> </div>
<span class="category-name text-sm font-medium"> <span class="category-name">
{category.name} {category.name}
</span> </span>
</.link> </.link>
@ -710,7 +663,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def featured_products_section(assigns) do def featured_products_section(assigns) do
~H""" ~H"""
<section class="featured-section"> <section class="featured-section">
<h2 class="t-heading text-2xl mb-6"> <h2 class="t-heading">
{@title} {@title}
</h2> </h2>
@ -726,19 +679,19 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<% end %> <% end %>
</.product_grid> </.product_grid>
<div class="text-center mt-8"> <div class="featured-cta-wrap">
<%= if @mode == :preview do %> <%= if @mode == :preview do %>
<button <button
phx-click="change_preview_page" phx-click="change_preview_page"
phx-value-page={@cta_page} phx-value-page={@cta_page}
class="outline-button px-6 py-3 font-medium transition-all" class="outline-button"
> >
{@cta_text} {@cta_text}
</button> </button>
<% else %> <% else %>
<.link <.link
navigate={@cta_href} navigate={@cta_href}
class="outline-button inline-block px-6 py-3 font-medium transition-all" class="outline-button"
> >
{@cta_text} {@cta_text}
</.link> </.link>
@ -784,7 +737,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def image_text_section(assigns) do def image_text_section(assigns) do
~H""" ~H"""
<section class="image-text-section grid grid-cols-1 md:grid-cols-2 gap-12 items-center"> <section class="image-text-section">
<%= 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
@ -815,7 +768,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
defp image_text_image(assigns) do defp image_text_image(assigns) do
~H""" ~H"""
<div <div
class="image-text-image h-72 bg-cover bg-center" class="image-text-image"
style={"background-image: url('#{@image_url}');"} style={"background-image: url('#{@image_url}');"}
> >
</div> </div>
@ -832,10 +785,10 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
defp image_text_content(assigns) do defp image_text_content(assigns) do
~H""" ~H"""
<div> <div>
<h2 class="t-heading text-2xl mb-4"> <h2 class="t-heading">
{@title} {@title}
</h2> </h2>
<p class="image-text-body text-base mb-4"> <p class="image-text-body">
{@description} {@description}
</p> </p>
<%= if @link_text do %> <%= if @link_text do %>
@ -844,14 +797,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="accent-link text-sm font-medium transition-colors" class="accent-link"
> >
{@link_text} {@link_text}
</a> </a>
<% else %> <% else %>
<.link <.link
navigate={@link_href || "/"} navigate={@link_href || "/"}
class="accent-link text-sm font-medium transition-colors" class="accent-link"
> >
{@link_text} {@link_text}
</.link> </.link>
@ -880,9 +833,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def collection_header(assigns) do def collection_header(assigns) do
~H""" ~H"""
<div class="collection-header-wrap border-b"> <div class="collection-header-wrap">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8"> <div class="collection-header-inner">
<h1 class="t-heading text-3xl md:text-4xl mb-2"> <h1 class="t-heading">
{@title} {@title}
</h1> </h1>
<%= if @subtitle do %> <%= if @subtitle do %>
@ -924,9 +877,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def filter_bar(assigns) do def filter_bar(assigns) do
~H""" ~H"""
<div class="flex flex-wrap items-center justify-between gap-4 mb-6"> <div class="filter-bar">
<!-- Category Pills --> <!-- Category Pills -->
<div class="filter-pills-container flex gap-2 overflow-x-auto"> <div class="filter-pills-container">
<button class={"filter-pill#{if @active_category == "All", do: " filter-pill-active", else: ""}"}> <button class={"filter-pill#{if @active_category == "All", do: " filter-pill-active", else: ""}"}>
All All
</button> </button>
@ -938,7 +891,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
</div> </div>
<!-- Sort Dropdown --> <!-- Sort Dropdown -->
<.shop_select options={@sort_options} class="px-4 py-2" /> <.shop_select options={@sort_options} />
</div> </div>
""" """
end end
@ -980,12 +933,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
href="#" href="#"
phx-click="change_preview_page" phx-click="change_preview_page"
phx-value-page={item.page} phx-value-page={item.page}
class="hover:underline"
> >
{item.label} {item.label}
</a> </a>
<% else %> <% else %>
<.link navigate={item.href || "/"} class="hover:underline">{item.label}</.link> <.link navigate={item.href || "/"}>{item.label}</.link>
<% end %> <% end %>
</li> </li>
<% end %> <% end %>
@ -1022,12 +974,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def related_products_section(assigns) do def related_products_section(assigns) do
~H""" ~H"""
<div class="related-section py-12"> <div class="related-section">
<h2 class="t-heading text-2xl mb-6"> <h2 class="t-heading">
{@title} {@title}
</h2> </h2>
<.product_grid columns={:fixed_4} gap="gap-6"> <.product_grid columns={:fixed_4}>
<%= for product <- Enum.take(@products, @limit) do %> <%= for product <- Enum.take(@products, @limit) do %>
<.product_card <.product_card
product={product} product={product}
@ -1062,7 +1014,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="pdp-gallery-frame relative"> <div class="pdp-gallery-frame">
<%!-- 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
@ -1129,7 +1081,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="product-card-placeholder w-full h-full flex items-center justify-center" class="product-card-placeholder"
role="img" role="img"
aria-label={@product_name} aria-label={@product_name}
> >
@ -1141,7 +1093,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
stroke="currentColor" stroke="currentColor"
width="48" width="48"
height="48" height="48"
class="size-12 opacity-40" class="placeholder-icon"
> >
<path <path
stroke-linecap="round" stroke-linecap="round"
@ -1156,7 +1108,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
alt={@product_name} alt={@product_name}
width="600" width="600"
height="600" height="600"
class="w-full h-full object-cover"
/> />
<div <div
class="pdp-lightbox-click" class="pdp-lightbox-click"
@ -1184,7 +1135,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<%= for {url, idx} <- Enum.with_index(@images) do %> <%= for {url, idx} <- Enum.with_index(@images) 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={"pdp-thumbnail#{if idx == 0, do: " pdp-thumbnail-active", else: ""}"}
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",
@ -1207,7 +1158,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
width="150" width="150"
height="150" height="150"
loading="lazy" loading="lazy"
class="w-full h-full object-cover"
/> />
</button> </button>
<% end %> <% end %>
@ -1337,23 +1287,23 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
~H""" ~H"""
<div> <div>
<h1 class="t-heading text-3xl md:text-4xl mb-4"> <h1 class="t-heading pdp-title">
{@product.title} {@product.title}
</h1> </h1>
<div class="flex items-center gap-4 mb-6"> <div class="pdp-price-row">
<%= if @product.on_sale do %> <%= if @product.on_sale do %>
<span class="product-price--sale text-3xl font-bold"> <span class="product-price--sale">
{SimpleshopTheme.Cart.format_price(@price)} {SimpleshopTheme.Cart.format_price(@price)}
</span> </span>
<span class="product-price--compare text-xl line-through"> <span class="product-price--compare">
{SimpleshopTheme.Cart.format_price(@product.compare_at_price)} {SimpleshopTheme.Cart.format_price(@product.compare_at_price)}
</span> </span>
<span class="sale-badge px-2 py-1 text-sm font-bold text-white rounded"> <span class="sale-badge">
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="product-price--regular text-3xl font-bold"> <span class="product-price--regular">
{SimpleshopTheme.Cart.format_price(@price)} {SimpleshopTheme.Cart.format_price(@price)}
</span> </span>
<% end %> <% end %>
@ -1390,14 +1340,14 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def variant_selector(assigns) do def variant_selector(assigns) do
~H""" ~H"""
<div class="mb-6"> <div class="variant-selector">
<div class="variant-label block font-semibold mb-2"> <div class="variant-label">
{@option_type.name}<span {@option_type.name}<span
:if={@selected} :if={@selected}
class="variant-label-value" class="variant-label-value"
>: {@selected}</span> >: {@selected}</span>
</div> </div>
<div class="flex flex-wrap gap-2"> <div class="variant-options">
<%= if @option_type.type == :color do %> <%= if @option_type.type == :color do %>
<.color_swatch <.color_swatch
:for={value <- @option_type.values} :for={value <- @option_type.values}
@ -1437,10 +1387,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="color-swatch"
"color-swatch w-10 h-10 rounded-full border-2 transition-all relative",
@selected && "ring-2 ring-offset-2"
]}
style={"background-color: #{@hex};"} style={"background-color: #{@hex};"}
title={@title} title={@title}
aria-label={"Select #{@title}"} aria-label={"Select #{@title}"}
@ -1463,7 +1410,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="size-btn px-4 py-2 font-medium transition-all" class="size-btn"
aria-pressed={to_string(@selected)} aria-pressed={to_string(@selected)}
> >
{@title} {@title}
@ -1493,22 +1440,22 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def quantity_selector(assigns) do def quantity_selector(assigns) do
~H""" ~H"""
<div class="mb-8"> <div class="quantity-selector">
<label class="qty-label block font-semibold mb-2"> <label class="qty-label">
Quantity Quantity
</label> </label>
<div class="flex items-center gap-4"> <div class="qty-row">
<div class="qty-group flex items-center"> <div class="qty-group">
<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="qty-btn px-4 py-2 disabled:opacity-30" class="qty-btn"
> >
</button> </button>
<span class="qty-display px-4 py-2 border-x-2 min-w-12 text-center tabular-nums"> <span class="qty-display">
{@quantity} {@quantity}
</span> </span>
<button <button
@ -1516,15 +1463,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="qty-btn px-4 py-2 disabled:opacity-30" class="qty-btn"
> >
+ +
</button> </button>
</div> </div>
<%= if @in_stock do %> <%= if @in_stock do %>
<span class="stock-in text-sm">In stock</span> <span class="stock-in">In stock</span>
<% else %> <% else %>
<span class="stock-out text-sm font-semibold">Out of stock</span> <span class="stock-out">Out of stock</span>
<% end %> <% end %>
</div> </div>
</div> </div>
@ -1553,16 +1500,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
def add_to_cart_button(assigns) do def add_to_cart_button(assigns) do
~H""" ~H"""
<div class={[ <div class="atc-wrap" data-sticky={to_string(@sticky)}>
"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 <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="atc-btn w-full px-6 py-4 text-lg font-semibold transition-all" class="atc-btn"
> >
{@text} {@text}
</button> </button>
@ -1599,11 +1542,11 @@ 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}>
<summary class="accordion-summary flex justify-between items-center py-4 cursor-pointer list-none [&::-webkit-details-marker]:hidden"> <summary class="accordion-summary">
<span class="font-semibold">{@title}</span> <span class="accordion-title">{@title}</span>
<svg <svg
class="w-5 h-5 transition-transform duration-200 group-open:rotate-180" class="accordion-icon"
width="20" width="20"
height="20" height="20"
fill="none" fill="none"
@ -1613,7 +1556,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="accordion-body pb-4"> <div class="accordion-body">
{render_slot(@inner_block)} {render_slot(@inner_block)}
</div> </div>
</details> </details>
@ -1651,25 +1594,25 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
end) end)
~H""" ~H"""
<div class="details-wrap mt-8 divide-y"> <div class="details-wrap">
<.accordion_item title="Description" open={true}> <.accordion_item title="Description" open={true}>
<p class="leading-relaxed"> <p class="details-description">
{@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.
</p> </p>
</.accordion_item> </.accordion_item>
<%= if @show_size_guide do %> <%= if @show_size_guide do %>
<.accordion_item title="Size Guide"> <.accordion_item title="Size Guide">
<table class="w-full text-sm"> <table class="details-table">
<thead> <thead>
<tr class="details-table-row"> <tr class="details-table-row">
<th class="details-th text-left py-2 font-semibold"> <th class="details-th">
Size Size
</th> </th>
<th class="details-th text-left py-2 font-semibold"> <th class="details-th">
Chest (cm) Chest (cm)
</th> </th>
<th class="details-th text-left py-2 font-semibold"> <th class="details-th">
Length (cm) Length (cm)
</th> </th>
</tr> </tr>
@ -1677,9 +1620,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<tbody> <tbody>
<%= for {size_row, idx} <- Enum.with_index(@sizes) do %> <%= for {size_row, idx} <- Enum.with_index(@sizes) do %>
<tr class={idx < length(@sizes) - 1 && "details-table-row"}> <tr class={idx < length(@sizes) - 1 && "details-table-row"}>
<td class="py-2">{size_row.size}</td> <td class="details-td">{size_row.size}</td>
<td class="py-2">{size_row.chest}</td> <td class="details-td">{size_row.chest}</td>
<td class="py-2">{size_row.length}</td> <td class="details-td">{size_row.length}</td>
</tr> </tr>
<% end %> <% end %>
</tbody> </tbody>
@ -1688,16 +1631,16 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
<% end %> <% end %>
<.accordion_item title="Shipping & Returns"> <.accordion_item title="Shipping & Returns">
<div class="flex flex-col gap-3"> <div class="details-shipping">
<div> <div>
<p class="details-subheading font-semibold mb-1">Delivery</p> <p class="details-subheading">Delivery</p>
<p class="text-sm"> <p class="details-shipping-text">
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="details-subheading font-semibold mb-1">Returns</p> <p class="details-subheading">Returns</p>
<p class="text-sm"> <p class="details-shipping-text">
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>
</div> </div>