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:
parent
fc9c33ab0c
commit
04b6ee3f37
@ -18,6 +18,8 @@
|
||||
.product-card {
|
||||
background-color: var(--t-surface-raised);
|
||||
border-radius: var(--t-radius-card);
|
||||
overflow: hidden;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&[data-variant="default"],
|
||||
&[data-variant="compact"] {
|
||||
@ -33,6 +35,10 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&[data-variant="featured"]:hover {
|
||||
transform: translateY(-0.25rem);
|
||||
}
|
||||
|
||||
&[data-clickable] {
|
||||
position: relative;
|
||||
}
|
||||
@ -45,10 +51,32 @@
|
||||
|
||||
.product-card-image-wrap {
|
||||
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 {
|
||||
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 {
|
||||
@ -56,10 +84,19 @@
|
||||
text-decoration: none;
|
||||
position: relative;
|
||||
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 {
|
||||
color: var(--t-text-primary);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.product-card[data-variant="default"] .product-card-title,
|
||||
@ -67,8 +104,28 @@
|
||||
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 {
|
||||
color: var(--t-text-tertiary);
|
||||
font-size: var(--t-text-caption);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
/* ── Product prices (shared between cards and PDP) ── */
|
||||
@ -79,6 +136,7 @@
|
||||
|
||||
.product-price--compare {
|
||||
color: var(--t-text-tertiary);
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.product-price--regular {
|
||||
@ -89,14 +147,66 @@
|
||||
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 {
|
||||
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 {
|
||||
padding: var(--space-2xl) var(--space-lg);
|
||||
text-align: center;
|
||||
|
||||
&[data-background="base"] {
|
||||
background-color: var(--t-surface-base);
|
||||
@ -105,21 +215,73 @@
|
||||
&[data-background="sunken"] {
|
||||
background-color: var(--t-surface-sunken);
|
||||
}
|
||||
|
||||
& .t-heading {
|
||||
font-size: var(--t-heading-lg);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-section--page {
|
||||
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 {
|
||||
font-family: var(--t-font-heading);
|
||||
font-weight: var(--t-heading-weight);
|
||||
color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
font-size: var(--t-heading-display);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.hero-description {
|
||||
color: var(--t-text-secondary);
|
||||
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 ── */
|
||||
@ -129,14 +291,49 @@
|
||||
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 {
|
||||
text-decoration: none;
|
||||
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 {
|
||||
font-family: var(--t-font-body);
|
||||
color: var(--t-text-primary);
|
||||
font-size: var(--t-text-small);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* ── Featured products section ── */
|
||||
@ -144,6 +341,16 @@
|
||||
.featured-section {
|
||||
padding: var(--space-xl) var(--space-lg);
|
||||
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 {
|
||||
@ -153,6 +360,10 @@
|
||||
border-radius: var(--t-radius-button);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* ── Image + text section ── */
|
||||
@ -160,21 +371,38 @@
|
||||
.image-text-section {
|
||||
padding: var(--space-2xl) var(--space-lg);
|
||||
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 {
|
||||
border-radius: var(--t-radius-image);
|
||||
height: 18rem;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.image-text-body {
|
||||
color: var(--t-text-secondary);
|
||||
line-height: 1.7;
|
||||
font-size: var(--t-text-base);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.accent-link {
|
||||
color: var(--t-accent-text, hsl(var(--t-accent-h) var(--t-accent-s) 38%));
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-size: var(--t-text-small);
|
||||
font-weight: 500;
|
||||
transition: color 0.15s ease;
|
||||
}
|
||||
|
||||
/* ── Collection header ── */
|
||||
@ -182,12 +410,41 @@
|
||||
.collection-header-wrap {
|
||||
background-color: var(--t-surface-raised);
|
||||
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 {
|
||||
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 {
|
||||
@ -196,12 +453,22 @@
|
||||
& [aria-current="page"] {
|
||||
color: var(--t-text-primary);
|
||||
}
|
||||
|
||||
& a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Related products ── */
|
||||
|
||||
.related-section {
|
||||
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 ── */
|
||||
@ -209,16 +476,42 @@
|
||||
.pdp-gallery-frame {
|
||||
border-radius: var(--t-radius-image);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.pdp-gallery-single img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.pdp-thumbnail {
|
||||
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 {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.variant-label {
|
||||
color: var(--t-text-primary);
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.variant-label-value {
|
||||
@ -226,8 +519,21 @@
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.variant-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.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"] {
|
||||
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);
|
||||
color: var(--t-text-primary);
|
||||
background: transparent;
|
||||
padding: 0.5rem 1rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&[aria-pressed="true"] {
|
||||
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
|
||||
@ -249,30 +559,60 @@
|
||||
|
||||
/* ── Quantity selector ── */
|
||||
|
||||
.quantity-selector {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.qty-label {
|
||||
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 {
|
||||
border: 2px solid var(--t-border-default);
|
||||
border-radius: var(--t-radius-input);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.qty-btn {
|
||||
color: var(--t-text-primary);
|
||||
padding: 0.5rem 1rem;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.qty-display {
|
||||
border-color: var(--t-border-default);
|
||||
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 {
|
||||
color: var(--t-text-tertiary);
|
||||
font-size: var(--t-text-small);
|
||||
}
|
||||
|
||||
.stock-out {
|
||||
color: var(--t-sale-color);
|
||||
font-size: var(--t-text-small);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Add to cart ── */
|
||||
@ -280,6 +620,17 @@
|
||||
.atc-wrap {
|
||||
background-color: var(--t-surface-base);
|
||||
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 {
|
||||
@ -288,6 +639,11 @@
|
||||
border-radius: var(--t-radius-button);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1.5rem;
|
||||
font-size: var(--t-text-large);
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:disabled {
|
||||
background-color: var(--t-border-default);
|
||||
@ -299,10 +655,35 @@
|
||||
|
||||
.accordion-summary {
|
||||
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 {
|
||||
color: var(--t-text-secondary);
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* ── Product details ── */
|
||||
@ -311,6 +692,20 @@
|
||||
border-top: 1px solid var(--t-border-subtle);
|
||||
border-bottom: 1px solid 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 {
|
||||
@ -319,10 +714,29 @@
|
||||
|
||||
.details-th {
|
||||
color: var(--t-text-primary);
|
||||
text-align: left;
|
||||
padding-block: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.details-td {
|
||||
padding-block: 0.5rem;
|
||||
}
|
||||
|
||||
.details-subheading {
|
||||
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 ── */
|
||||
@ -784,6 +1198,11 @@
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
color: var(--t-text-secondary);
|
||||
|
||||
& svg {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.cart-drawer-items {
|
||||
@ -829,6 +1248,7 @@
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-family: var(--t-font-body);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
/* ── Cart item row ── */
|
||||
@ -910,6 +1330,8 @@
|
||||
.cart-qty-group {
|
||||
border: 1px solid var(--t-border-default);
|
||||
border-radius: var(--t-radius-input);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.cart-qty-btn {
|
||||
@ -917,13 +1339,15 @@
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--t-text-primary);
|
||||
padding: 0.25rem 0.75rem;
|
||||
}
|
||||
|
||||
.cart-qty-display {
|
||||
border-color: var(--t-border-default);
|
||||
color: var(--t-text-primary);
|
||||
min-width: 2rem;
|
||||
text-align: center;
|
||||
padding: 0.25rem 0.75rem;
|
||||
border-inline: 1px solid var(--t-border-default);
|
||||
}
|
||||
|
||||
.cart-qty-text {
|
||||
@ -933,6 +1357,7 @@
|
||||
|
||||
.cart-item-price-col {
|
||||
flex-shrink: 0;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.cart-item-price {
|
||||
@ -961,10 +1386,20 @@
|
||||
|
||||
.cart-empty {
|
||||
color: var(--t-text-secondary);
|
||||
text-align: center;
|
||||
padding-block: 2rem;
|
||||
}
|
||||
|
||||
.cart-empty-icon {
|
||||
color: var(--t-text-tertiary);
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin-inline: auto;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cart-empty p {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.cart-continue-link {
|
||||
@ -980,25 +1415,66 @@
|
||||
|
||||
/* ── Cart page item (full size) ── */
|
||||
|
||||
.cart-page-item {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.cart-page-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 {
|
||||
font-family: var(--t-font-heading);
|
||||
color: var(--t-text-primary);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.cart-page-item-variant {
|
||||
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 {
|
||||
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 {
|
||||
color: var(--t-text-primary);
|
||||
font-weight: 700;
|
||||
font-size: var(--t-text-large);
|
||||
}
|
||||
|
||||
/* ── Delivery line ── */
|
||||
@ -1007,12 +1483,21 @@
|
||||
font-family: var(--t-font-body);
|
||||
font-size: var(--t-text-small);
|
||||
color: var(--t-text-secondary);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
& form {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
|
||||
.delivery-line-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.delivery-select {
|
||||
appearance: auto;
|
||||
background: transparent;
|
||||
@ -1027,9 +1512,30 @@
|
||||
|
||||
/* ── Order summary ── */
|
||||
|
||||
.order-summary-card {
|
||||
padding: 1.5rem;
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
}
|
||||
|
||||
.order-summary-heading {
|
||||
font-family: var(--t-font-heading);
|
||||
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 {
|
||||
@ -1041,7 +1547,34 @@
|
||||
}
|
||||
|
||||
.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 ── */
|
||||
@ -1498,6 +2031,42 @@
|
||||
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 ── */
|
||||
|
||||
.sr-only {
|
||||
@ -1516,9 +2085,14 @@
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.page-container { padding-inline: 1.5rem; }
|
||||
.collection-header-inner { padding-inline: 1.5rem; }
|
||||
.shop-header { padding: 0.75rem 1rem; }
|
||||
.shop-footer-inner { padding-inline: 1.5rem; }
|
||||
.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) {
|
||||
@ -1530,11 +2104,33 @@
|
||||
.footer-bottom { flex-direction: row; }
|
||||
.pdp-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) {
|
||||
.page-container { padding-inline: 2rem; }
|
||||
.collection-header-inner { padding-inline: 2rem; }
|
||||
.shop-footer-inner { padding-inline: 2rem; }
|
||||
.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); }
|
||||
}
|
||||
}
|
||||
|
||||
@ -85,15 +85,15 @@
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
&[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);
|
||||
}
|
||||
|
||||
&[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%);
|
||||
padding-left: 2rem;
|
||||
padding-right: 2rem;
|
||||
@ -175,7 +175,7 @@
|
||||
}
|
||||
|
||||
/* Image Aspect Ratio */
|
||||
& .product-card .product-image-container {
|
||||
& .product-card .product-card-image-wrap {
|
||||
aspect-ratio: var(--t-image-aspect-ratio, 1 / 1);
|
||||
}
|
||||
|
||||
@ -330,7 +330,7 @@
|
||||
}
|
||||
|
||||
/* Product Card Images — mobile: swipe, desktop: hover crossfade */
|
||||
.product-image-container {
|
||||
.product-card-image-wrap {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
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 %>
|
||||
<.product_card
|
||||
product={product}
|
||||
|
||||
@ -92,7 +92,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
aria-label="Close cart"
|
||||
>
|
||||
<svg
|
||||
class="w-5 h-5"
|
||||
class="cart-drawer-close-icon"
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
@ -134,7 +134,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
<%= if @mode == :preview do %>
|
||||
<button
|
||||
type="button"
|
||||
class="cart-drawer-checkout w-full mb-2"
|
||||
class="cart-drawer-checkout"
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
@ -143,7 +143,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
<input type="hidden" name="_csrf_token" value={Phoenix.Controller.get_csrf_token()} />
|
||||
<button
|
||||
type="submit"
|
||||
class="cart-drawer-checkout w-full mb-2"
|
||||
class="cart-drawer-checkout"
|
||||
>
|
||||
Checkout
|
||||
</button>
|
||||
@ -213,24 +213,24 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
|
||||
<div class="cart-item-actions">
|
||||
<%= if @show_quantity_controls do %>
|
||||
<div class="cart-qty-group flex items-center">
|
||||
<div class="cart-qty-group">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="decrement"
|
||||
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}"}
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span class="cart-qty-display px-3 py-1 border-x">
|
||||
<span class="cart-qty-display">
|
||||
{@item.quantity}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="increment"
|
||||
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}"}
|
||||
>
|
||||
+
|
||||
@ -246,7 +246,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
</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"}>
|
||||
{SimpleshopTheme.Cart.format_price(@item.price * @item.quantity)}
|
||||
</p>
|
||||
@ -262,9 +262,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
|
||||
def cart_empty_state(assigns) do
|
||||
~H"""
|
||||
<div class="cart-empty text-center py-8">
|
||||
<div class="cart-empty">
|
||||
<svg
|
||||
class="cart-empty-icon w-16 h-16 mx-auto mb-4"
|
||||
class="cart-empty-icon"
|
||||
width="64"
|
||||
height="64"
|
||||
viewBox="0 0 24 24"
|
||||
@ -276,7 +276,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
<line x1="3" y1="6" x2="21" y2="6"></line>
|
||||
<path d="M16 10a4 4 0 01-8 0"></path>
|
||||
</svg>
|
||||
<p class="mb-4">Your basket is empty</p>
|
||||
<p>Your basket is empty</p>
|
||||
<%= if @mode == :preview do %>
|
||||
<button
|
||||
type="button"
|
||||
@ -334,43 +334,42 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
|
||||
def cart_item(assigns) do
|
||||
~H"""
|
||||
<.shop_card class="flex gap-4 p-4">
|
||||
<div class="cart-page-image w-24 h-24 flex-shrink-0 bg-gray-200 overflow-hidden">
|
||||
<.shop_card class="cart-page-item">
|
||||
<div class="cart-page-image">
|
||||
<img
|
||||
src={cart_item_image(@item.product)}
|
||||
alt={@item.product.title}
|
||||
width="96"
|
||||
height="96"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex-1">
|
||||
<h3 class="cart-page-item-name font-semibold mb-1">
|
||||
<div class="cart-page-item-info">
|
||||
<h3 class="cart-page-item-name">
|
||||
{@item.product.title}
|
||||
</h3>
|
||||
<p class="cart-page-item-variant text-sm mb-2">
|
||||
<p class="cart-page-item-variant">
|
||||
{@item.variant}
|
||||
</p>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="cart-qty-group flex items-center">
|
||||
<button class="cart-qty-btn px-3 py-1">−</button>
|
||||
<span class="cart-qty-display px-3 py-1 border-x">
|
||||
<div class="cart-page-item-actions">
|
||||
<div class="cart-qty-group">
|
||||
<button class="cart-qty-btn">−</button>
|
||||
<span class="cart-qty-display">
|
||||
{@item.quantity}
|
||||
</span>
|
||||
<button class="cart-qty-btn px-3 py-1">+</button>
|
||||
<button class="cart-qty-btn">+</button>
|
||||
</div>
|
||||
|
||||
<button class="cart-page-item-remove text-sm">
|
||||
<button class="cart-page-item-remove">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right">
|
||||
<p class="cart-page-item-price font-bold text-lg">
|
||||
<div class="cart-page-item-price-col">
|
||||
<p class="cart-page-item-price">
|
||||
{SimpleshopTheme.Cart.format_price(@item.product.cheapest_price * @item.quantity)}
|
||||
</p>
|
||||
</div>
|
||||
@ -391,8 +390,8 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
|
||||
defp delivery_line(assigns) do
|
||||
~H"""
|
||||
<div class="delivery-line flex justify-between items-center">
|
||||
<span class="flex items-center gap-1">
|
||||
<div class="delivery-line">
|
||||
<span class="delivery-line-label">
|
||||
Delivery to
|
||||
<%= if @available_countries != [] and @mode != :preview do %>
|
||||
<form phx-change="change_country">
|
||||
@ -445,15 +444,15 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
assign(assigns, :estimated_total, assigns.subtotal + (assigns.shipping_estimate || 0))
|
||||
|
||||
~H"""
|
||||
<.shop_card class="p-6 sticky top-4">
|
||||
<h2 class="order-summary-heading text-xl font-bold mb-6">
|
||||
<.shop_card class="order-summary-card">
|
||||
<h2 class="order-summary-heading">
|
||||
Order summary
|
||||
</h2>
|
||||
|
||||
<div class="flex flex-col gap-3 mb-6">
|
||||
<div class="flex justify-between">
|
||||
<span class="order-summary-label">Subtotal</span>
|
||||
<span class="order-summary-value">
|
||||
<div class="order-summary-lines">
|
||||
<div class="order-summary-line">
|
||||
<span>Subtotal</span>
|
||||
<span>
|
||||
{SimpleshopTheme.Cart.format_price(@subtotal)}
|
||||
</span>
|
||||
</div>
|
||||
@ -463,12 +462,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
available_countries={@available_countries}
|
||||
mode={@mode}
|
||||
/>
|
||||
<div class="order-summary-divider border-t pt-3">
|
||||
<div class="flex justify-between text-lg">
|
||||
<span class="order-summary-value font-semibold">
|
||||
<div class="order-summary-divider">
|
||||
<div class="order-summary-total">
|
||||
<span>
|
||||
{if @shipping_estimate, do: "Estimated total", else: "Subtotal"}
|
||||
</span>
|
||||
<span class="order-summary-value font-bold">
|
||||
<span>
|
||||
{SimpleshopTheme.Cart.format_price(@estimated_total)}
|
||||
</span>
|
||||
</div>
|
||||
@ -476,26 +475,26 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
</div>
|
||||
|
||||
<%= 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
|
||||
</.shop_button>
|
||||
<.shop_button_outline
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page="collection"
|
||||
class="w-full px-6 py-3 font-semibold transition-all"
|
||||
class="order-summary-continue"
|
||||
>
|
||||
Continue shopping
|
||||
</.shop_button_outline>
|
||||
<% 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()} />
|
||||
<.shop_button type="submit" class="w-full px-6 py-3 font-semibold transition-all">
|
||||
<.shop_button type="submit" class="order-summary-checkout">
|
||||
Checkout
|
||||
</.shop_button>
|
||||
</form>
|
||||
<.shop_link_outline
|
||||
href="/collections/all"
|
||||
class="block w-full px-6 py-3 font-semibold transition-all text-center"
|
||||
class="order-summary-continue"
|
||||
>
|
||||
Continue shopping
|
||||
</.shop_link_outline>
|
||||
@ -524,13 +523,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Cart do
|
||||
|
||||
def cart_layout(assigns) do
|
||||
~H"""
|
||||
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
<div class="lg:col-span-2">
|
||||
<div class="flex flex-col gap-4">
|
||||
<%= for item <- @items do %>
|
||||
<.cart_item item={item} />
|
||||
<% end %>
|
||||
</div>
|
||||
<div class="cart-layout">
|
||||
<div class="cart-items-stack">
|
||||
<%= for item <- @items do %>
|
||||
<.cart_item item={item} />
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
||||
@ -62,7 +62,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
~H"""
|
||||
<article
|
||||
class={card_classes(@variant)}
|
||||
class="product-card"
|
||||
data-variant={@variant}
|
||||
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)
|
||||
|
||||
~H"""
|
||||
<div class={["product-card-image-wrap", image_container_classes(@variant)]}>
|
||||
<div class="product-card-image-wrap">
|
||||
<%= if @show_badges do %>
|
||||
<.product_badge product={@product} />
|
||||
<% end %>
|
||||
@ -118,13 +118,13 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
alt={@product.title}
|
||||
variant={@variant}
|
||||
priority={@priority}
|
||||
class="product-image-primary w-full h-full object-cover transition-opacity duration-300"
|
||||
class="product-image-primary"
|
||||
/>
|
||||
<.product_card_image
|
||||
image={@hover_image}
|
||||
alt={@product.title}
|
||||
variant={@variant}
|
||||
class="product-image-hover w-full h-full object-cover"
|
||||
class="product-image-hover"
|
||||
/>
|
||||
</div>
|
||||
<% else %>
|
||||
@ -133,7 +133,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
alt={@product.title}
|
||||
variant={@variant}
|
||||
priority={@priority}
|
||||
class="product-image-primary w-full h-full object-cover"
|
||||
class="product-image-primary"
|
||||
/>
|
||||
<% end %>
|
||||
<%= if @has_hover_image do %>
|
||||
@ -143,22 +143,22 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<div class={content_padding_class(@variant)}>
|
||||
<div class="product-card-content">
|
||||
<%= if @show_category && Map.get(@product, :category) do %>
|
||||
<%= if @mode == :preview do %>
|
||||
<p class="product-card-category text-xs mb-1">
|
||||
<p class="product-card-category">
|
||||
{@product.category}
|
||||
</p>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={"/collections/#{Slug.slugify(@product.category)}"}
|
||||
class="product-card-category text-xs mb-1 block hover:underline"
|
||||
class="product-card-category"
|
||||
>
|
||||
{@product.category}
|
||||
</.link>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<h3 class={["product-card-title", title_classes(@variant)]}>
|
||||
<h3 class="product-card-title">
|
||||
<%= if @clickable do %>
|
||||
<%= if @mode == :preview do %>
|
||||
<a
|
||||
@ -185,7 +185,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<.product_price product={@product} variant={@variant} />
|
||||
<% end %>
|
||||
<%= if @show_delivery_text do %>
|
||||
<p class="product-card-delivery text-xs mt-1">
|
||||
<p class="product-card-delivery">
|
||||
Made to order
|
||||
</p>
|
||||
<% end %>
|
||||
@ -226,7 +226,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<%= cond do %>
|
||||
<% is_nil(@src) -> %>
|
||||
<div
|
||||
class={[@class, "product-card-placeholder flex items-center justify-center"]}
|
||||
class={[@class, "product-card-placeholder"]}
|
||||
role="img"
|
||||
aria-label={@alt}
|
||||
>
|
||||
@ -238,7 +238,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
stroke="currentColor"
|
||||
width="48"
|
||||
height="48"
|
||||
class="size-12 opacity-40"
|
||||
class="placeholder-icon"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@ -297,33 +297,33 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<% :default -> %>
|
||||
<div>
|
||||
<%= 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)}
|
||||
</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)}
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="product-price--regular text-lg font-bold">
|
||||
<span class="product-price--regular">
|
||||
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% :featured -> %>
|
||||
<p class="product-price--secondary text-sm">
|
||||
<p class="product-price--secondary">
|
||||
<%= 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)}
|
||||
</span>
|
||||
<% end %>
|
||||
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
|
||||
</p>
|
||||
<% :compact -> %>
|
||||
<p class="product-price--regular font-bold">
|
||||
<p class="product-price--regular">
|
||||
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
|
||||
</p>
|
||||
<% :minimal -> %>
|
||||
<p class="product-price--secondary text-xs">
|
||||
<p class="product-price--secondary">
|
||||
{SimpleshopTheme.Cart.format_price(@product.cheapest_price)}
|
||||
</p>
|
||||
<% end %>
|
||||
@ -342,32 +342,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
defp variant_defaults(:minimal),
|
||||
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 """
|
||||
Renders a responsive product grid container.
|
||||
|
||||
@ -394,57 +368,33 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<% end %>
|
||||
</.product_grid>
|
||||
|
||||
<.product_grid columns={:fixed_4} gap="gap-6">
|
||||
<.product_grid columns={:fixed_4}>
|
||||
...
|
||||
</.product_grid>
|
||||
"""
|
||||
attr :theme_settings, :map, default: nil
|
||||
attr :columns, :atom, default: nil
|
||||
attr :gap, :string, default: nil
|
||||
attr :class, :string, default: nil
|
||||
|
||||
slot :inner_block, required: true
|
||||
|
||||
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"""
|
||||
<div class={grid_classes(@theme_settings, @columns, @gap, @class)}>
|
||||
<div class={["product-grid", @class]} data-columns={@data_columns}>
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
"""
|
||||
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 """
|
||||
Renders a centered hero section with title, description, and optional CTAs.
|
||||
|
||||
@ -509,11 +459,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
~H"""
|
||||
<%= case @variant do %>
|
||||
<% :default -> %>
|
||||
<section class="hero-section text-center" data-background={@background}>
|
||||
<h1 class="t-heading text-3xl md:text-4xl mb-4">
|
||||
<section class="hero-section" data-background={@background}>
|
||||
<h1 class="t-heading">
|
||||
{@title}
|
||||
</h1>
|
||||
<p class="hero-description text-lg max-w-lg mx-auto mb-8">
|
||||
<p class="hero-description">
|
||||
{@description}
|
||||
</p>
|
||||
<.hero_cta
|
||||
@ -526,29 +476,29 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
/>
|
||||
</section>
|
||||
<% :page -> %>
|
||||
<div class="hero-section--page text-center">
|
||||
<h1 class="t-heading text-4xl md:text-5xl mb-6">
|
||||
<div class="hero-section--page">
|
||||
<h1 class="t-heading">
|
||||
{@title}
|
||||
</h1>
|
||||
<p class="hero-description text-lg mb-12 max-w-2xl mx-auto">
|
||||
<p class="hero-description">
|
||||
{@description}
|
||||
</p>
|
||||
</div>
|
||||
<% :error -> %>
|
||||
<div class="text-center">
|
||||
<div class="hero-error">
|
||||
<%= if @pre_title do %>
|
||||
<h1 class="hero-pre-title text-8xl md:text-9xl mb-4">
|
||||
<h1 class="hero-pre-title">
|
||||
{@pre_title}
|
||||
</h1>
|
||||
<% end %>
|
||||
<h2 class="t-heading text-3xl md:text-4xl mb-6">
|
||||
<h2 class="t-heading">
|
||||
{@title}
|
||||
</h2>
|
||||
<p class="hero-description text-lg mb-8 max-w-md mx-auto">
|
||||
<p class="hero-description">
|
||||
{@description}
|
||||
</p>
|
||||
<%= 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
|
||||
:if={@cta_text}
|
||||
text={@cta_text}
|
||||
@ -579,19 +529,27 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
attr :variant, :atom, required: true
|
||||
|
||||
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"""
|
||||
<%= if @mode == :preview do %>
|
||||
<button
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page={@page}
|
||||
class={hero_cta_classes(@variant)}
|
||||
class={@cta_class}
|
||||
>
|
||||
{@text}
|
||||
</button>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={@href || "/"}
|
||||
class={["inline-block", hero_cta_classes(@variant)]}
|
||||
class={@cta_class}
|
||||
>
|
||||
{@text}
|
||||
</.link>
|
||||
@ -599,11 +557,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
"""
|
||||
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 """
|
||||
Renders a row of category circles for navigation.
|
||||
|
||||
@ -626,17 +579,17 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
~H"""
|
||||
<section class="category-nav-section">
|
||||
<h2 class="sr-only">Shop by Category</h2>
|
||||
<nav class="grid grid-cols-3 gap-4 max-w-3xl mx-auto" aria-label="Product categories">
|
||||
<nav class="category-nav" aria-label="Product categories">
|
||||
<%= for category <- Enum.take(@categories, @limit) do %>
|
||||
<%= if @mode == :preview do %>
|
||||
<a
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
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
|
||||
class="w-24 h-24 rounded-full bg-gray-200 bg-cover bg-center transition-transform hover:scale-105"
|
||||
class="category-image"
|
||||
style={
|
||||
if(category[:image_url],
|
||||
do: "background-image: url('#{category.image_url}');",
|
||||
@ -645,17 +598,17 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
}
|
||||
>
|
||||
</div>
|
||||
<span class="category-name text-sm font-medium">
|
||||
<span class="category-name">
|
||||
{category.name}
|
||||
</span>
|
||||
</a>
|
||||
<% else %>
|
||||
<.link
|
||||
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
|
||||
class="w-24 h-24 rounded-full bg-gray-200 bg-cover bg-center transition-transform hover:scale-105"
|
||||
class="category-image"
|
||||
style={
|
||||
if(category[:image_url],
|
||||
do: "background-image: url('#{category.image_url}');",
|
||||
@ -664,7 +617,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
}
|
||||
>
|
||||
</div>
|
||||
<span class="category-name text-sm font-medium">
|
||||
<span class="category-name">
|
||||
{category.name}
|
||||
</span>
|
||||
</.link>
|
||||
@ -710,7 +663,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
def featured_products_section(assigns) do
|
||||
~H"""
|
||||
<section class="featured-section">
|
||||
<h2 class="t-heading text-2xl mb-6">
|
||||
<h2 class="t-heading">
|
||||
{@title}
|
||||
</h2>
|
||||
|
||||
@ -726,19 +679,19 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<% end %>
|
||||
</.product_grid>
|
||||
|
||||
<div class="text-center mt-8">
|
||||
<div class="featured-cta-wrap">
|
||||
<%= if @mode == :preview do %>
|
||||
<button
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page={@cta_page}
|
||||
class="outline-button px-6 py-3 font-medium transition-all"
|
||||
class="outline-button"
|
||||
>
|
||||
{@cta_text}
|
||||
</button>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={@cta_href}
|
||||
class="outline-button inline-block px-6 py-3 font-medium transition-all"
|
||||
class="outline-button"
|
||||
>
|
||||
{@cta_text}
|
||||
</.link>
|
||||
@ -784,7 +737,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def image_text_section(assigns) do
|
||||
~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 %>
|
||||
<.image_text_image image_url={@image_url} />
|
||||
<.image_text_content
|
||||
@ -815,7 +768,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
defp image_text_image(assigns) do
|
||||
~H"""
|
||||
<div
|
||||
class="image-text-image h-72 bg-cover bg-center"
|
||||
class="image-text-image"
|
||||
style={"background-image: url('#{@image_url}');"}
|
||||
>
|
||||
</div>
|
||||
@ -832,10 +785,10 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
defp image_text_content(assigns) do
|
||||
~H"""
|
||||
<div>
|
||||
<h2 class="t-heading text-2xl mb-4">
|
||||
<h2 class="t-heading">
|
||||
{@title}
|
||||
</h2>
|
||||
<p class="image-text-body text-base mb-4">
|
||||
<p class="image-text-body">
|
||||
{@description}
|
||||
</p>
|
||||
<%= if @link_text do %>
|
||||
@ -844,14 +797,14 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page={@link_page}
|
||||
class="accent-link text-sm font-medium transition-colors"
|
||||
class="accent-link"
|
||||
>
|
||||
{@link_text}
|
||||
</a>
|
||||
<% else %>
|
||||
<.link
|
||||
navigate={@link_href || "/"}
|
||||
class="accent-link text-sm font-medium transition-colors"
|
||||
class="accent-link"
|
||||
>
|
||||
{@link_text}
|
||||
</.link>
|
||||
@ -880,9 +833,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def collection_header(assigns) do
|
||||
~H"""
|
||||
<div class="collection-header-wrap border-b">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<h1 class="t-heading text-3xl md:text-4xl mb-2">
|
||||
<div class="collection-header-wrap">
|
||||
<div class="collection-header-inner">
|
||||
<h1 class="t-heading">
|
||||
{@title}
|
||||
</h1>
|
||||
<%= if @subtitle do %>
|
||||
@ -924,9 +877,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def filter_bar(assigns) do
|
||||
~H"""
|
||||
<div class="flex flex-wrap items-center justify-between gap-4 mb-6">
|
||||
<div class="filter-bar">
|
||||
<!-- 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: ""}"}>
|
||||
All
|
||||
</button>
|
||||
@ -938,7 +891,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
</div>
|
||||
|
||||
<!-- Sort Dropdown -->
|
||||
<.shop_select options={@sort_options} class="px-4 py-2" />
|
||||
<.shop_select options={@sort_options} />
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
@ -980,12 +933,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
href="#"
|
||||
phx-click="change_preview_page"
|
||||
phx-value-page={item.page}
|
||||
class="hover:underline"
|
||||
>
|
||||
{item.label}
|
||||
</a>
|
||||
<% else %>
|
||||
<.link navigate={item.href || "/"} class="hover:underline">{item.label}</.link>
|
||||
<.link navigate={item.href || "/"}>{item.label}</.link>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
@ -1022,12 +974,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def related_products_section(assigns) do
|
||||
~H"""
|
||||
<div class="related-section py-12">
|
||||
<h2 class="t-heading text-2xl mb-6">
|
||||
<div class="related-section">
|
||||
<h2 class="t-heading">
|
||||
{@title}
|
||||
</h2>
|
||||
|
||||
<.product_grid columns={:fixed_4} gap="gap-6">
|
||||
<.product_grid columns={:fixed_4}>
|
||||
<%= for product <- Enum.take(@products, @limit) do %>
|
||||
<.product_card
|
||||
product={product}
|
||||
@ -1062,7 +1014,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
~H"""
|
||||
<div class="pdp-gallery">
|
||||
<%!-- 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 --%>
|
||||
<%= if length(@images) > 1 do %>
|
||||
<div
|
||||
@ -1129,7 +1081,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<div class="pdp-gallery-single">
|
||||
<%= if @images == [] do %>
|
||||
<div
|
||||
class="product-card-placeholder w-full h-full flex items-center justify-center"
|
||||
class="product-card-placeholder"
|
||||
role="img"
|
||||
aria-label={@product_name}
|
||||
>
|
||||
@ -1141,7 +1093,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
stroke="currentColor"
|
||||
width="48"
|
||||
height="48"
|
||||
class="size-12 opacity-40"
|
||||
class="placeholder-icon"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
@ -1156,7 +1108,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
alt={@product_name}
|
||||
width="600"
|
||||
height="600"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<div
|
||||
class="pdp-lightbox-click"
|
||||
@ -1184,7 +1135,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<%= for {url, idx} <- Enum.with_index(@images) do %>
|
||||
<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)}"}
|
||||
phx-click={
|
||||
Phoenix.LiveView.JS.dispatch("pdp:scroll-to",
|
||||
@ -1207,7 +1158,6 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
width="150"
|
||||
height="150"
|
||||
loading="lazy"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
</button>
|
||||
<% end %>
|
||||
@ -1337,23 +1287,23 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
~H"""
|
||||
<div>
|
||||
<h1 class="t-heading text-3xl md:text-4xl mb-4">
|
||||
<h1 class="t-heading pdp-title">
|
||||
{@product.title}
|
||||
</h1>
|
||||
|
||||
<div class="flex items-center gap-4 mb-6">
|
||||
<div class="pdp-price-row">
|
||||
<%= if @product.on_sale do %>
|
||||
<span class="product-price--sale text-3xl font-bold">
|
||||
<span class="product-price--sale">
|
||||
{SimpleshopTheme.Cart.format_price(@price)}
|
||||
</span>
|
||||
<span class="product-price--compare text-xl line-through">
|
||||
<span class="product-price--compare">
|
||||
{SimpleshopTheme.Cart.format_price(@product.compare_at_price)}
|
||||
</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)}%
|
||||
</span>
|
||||
<% else %>
|
||||
<span class="product-price--regular text-3xl font-bold">
|
||||
<span class="product-price--regular">
|
||||
{SimpleshopTheme.Cart.format_price(@price)}
|
||||
</span>
|
||||
<% end %>
|
||||
@ -1390,14 +1340,14 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def variant_selector(assigns) do
|
||||
~H"""
|
||||
<div class="mb-6">
|
||||
<div class="variant-label block font-semibold mb-2">
|
||||
<div class="variant-selector">
|
||||
<div class="variant-label">
|
||||
{@option_type.name}<span
|
||||
:if={@selected}
|
||||
class="variant-label-value"
|
||||
>: {@selected}</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div class="variant-options">
|
||||
<%= if @option_type.type == :color do %>
|
||||
<.color_swatch
|
||||
:for={value <- @option_type.values}
|
||||
@ -1437,10 +1387,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
phx-click={if @mode == :shop, do: "select_option"}
|
||||
phx-value-option={@option_name}
|
||||
phx-value-selected={@title}
|
||||
class={[
|
||||
"color-swatch w-10 h-10 rounded-full border-2 transition-all relative",
|
||||
@selected && "ring-2 ring-offset-2"
|
||||
]}
|
||||
class="color-swatch"
|
||||
style={"background-color: #{@hex};"}
|
||||
title={@title}
|
||||
aria-label={"Select #{@title}"}
|
||||
@ -1463,7 +1410,7 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
phx-click={if @mode == :shop, do: "select_option"}
|
||||
phx-value-option={@option_name}
|
||||
phx-value-selected={@title}
|
||||
class="size-btn px-4 py-2 font-medium transition-all"
|
||||
class="size-btn"
|
||||
aria-pressed={to_string(@selected)}
|
||||
>
|
||||
{@title}
|
||||
@ -1493,22 +1440,22 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def quantity_selector(assigns) do
|
||||
~H"""
|
||||
<div class="mb-8">
|
||||
<label class="qty-label block font-semibold mb-2">
|
||||
<div class="quantity-selector">
|
||||
<label class="qty-label">
|
||||
Quantity
|
||||
</label>
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="qty-group flex items-center">
|
||||
<div class="qty-row">
|
||||
<div class="qty-group">
|
||||
<button
|
||||
type="button"
|
||||
phx-click="decrement_quantity"
|
||||
disabled={@quantity <= @min}
|
||||
aria-label="Decrease quantity"
|
||||
class="qty-btn px-4 py-2 disabled:opacity-30"
|
||||
class="qty-btn"
|
||||
>
|
||||
−
|
||||
</button>
|
||||
<span class="qty-display px-4 py-2 border-x-2 min-w-12 text-center tabular-nums">
|
||||
<span class="qty-display">
|
||||
{@quantity}
|
||||
</span>
|
||||
<button
|
||||
@ -1516,15 +1463,15 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
phx-click="increment_quantity"
|
||||
disabled={@quantity >= @max}
|
||||
aria-label="Increase quantity"
|
||||
class="qty-btn px-4 py-2 disabled:opacity-30"
|
||||
class="qty-btn"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<%= if @in_stock do %>
|
||||
<span class="stock-in text-sm">In stock</span>
|
||||
<span class="stock-in">In stock</span>
|
||||
<% else %>
|
||||
<span class="stock-out text-sm font-semibold">Out of stock</span>
|
||||
<span class="stock-out">Out of stock</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@ -1553,16 +1500,12 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def add_to_cart_button(assigns) do
|
||||
~H"""
|
||||
<div class={[
|
||||
"atc-wrap mb-4",
|
||||
@sticky &&
|
||||
"sticky bottom-0 z-10 py-3 md:relative md:py-0 -mx-4 px-4 md:mx-0 md:px-0 border-t md:border-0"
|
||||
]}>
|
||||
<div class="atc-wrap" data-sticky={to_string(@sticky)}>
|
||||
<button
|
||||
type="button"
|
||||
phx-click={if @mode == :preview, do: open_cart_drawer_js(), else: "add_to_cart"}
|
||||
disabled={@disabled}
|
||||
class="atc-btn w-full px-6 py-4 text-lg font-semibold transition-all"
|
||||
class="atc-btn"
|
||||
>
|
||||
{@text}
|
||||
</button>
|
||||
@ -1599,11 +1542,11 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
|
||||
def accordion_item(assigns) do
|
||||
~H"""
|
||||
<details open={@open} class="group">
|
||||
<summary class="accordion-summary flex justify-between items-center py-4 cursor-pointer list-none [&::-webkit-details-marker]:hidden">
|
||||
<span class="font-semibold">{@title}</span>
|
||||
<details open={@open}>
|
||||
<summary class="accordion-summary">
|
||||
<span class="accordion-title">{@title}</span>
|
||||
<svg
|
||||
class="w-5 h-5 transition-transform duration-200 group-open:rotate-180"
|
||||
class="accordion-icon"
|
||||
width="20"
|
||||
height="20"
|
||||
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" />
|
||||
</svg>
|
||||
</summary>
|
||||
<div class="accordion-body pb-4">
|
||||
<div class="accordion-body">
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</details>
|
||||
@ -1651,25 +1594,25 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
end)
|
||||
|
||||
~H"""
|
||||
<div class="details-wrap mt-8 divide-y">
|
||||
<div class="details-wrap">
|
||||
<.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.
|
||||
</p>
|
||||
</.accordion_item>
|
||||
|
||||
<%= if @show_size_guide do %>
|
||||
<.accordion_item title="Size Guide">
|
||||
<table class="w-full text-sm">
|
||||
<table class="details-table">
|
||||
<thead>
|
||||
<tr class="details-table-row">
|
||||
<th class="details-th text-left py-2 font-semibold">
|
||||
<th class="details-th">
|
||||
Size
|
||||
</th>
|
||||
<th class="details-th text-left py-2 font-semibold">
|
||||
<th class="details-th">
|
||||
Chest (cm)
|
||||
</th>
|
||||
<th class="details-th text-left py-2 font-semibold">
|
||||
<th class="details-th">
|
||||
Length (cm)
|
||||
</th>
|
||||
</tr>
|
||||
@ -1677,9 +1620,9 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<tbody>
|
||||
<%= for {size_row, idx} <- Enum.with_index(@sizes) do %>
|
||||
<tr class={idx < length(@sizes) - 1 && "details-table-row"}>
|
||||
<td class="py-2">{size_row.size}</td>
|
||||
<td class="py-2">{size_row.chest}</td>
|
||||
<td class="py-2">{size_row.length}</td>
|
||||
<td class="details-td">{size_row.size}</td>
|
||||
<td class="details-td">{size_row.chest}</td>
|
||||
<td class="details-td">{size_row.length}</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
@ -1688,16 +1631,16 @@ defmodule SimpleshopThemeWeb.ShopComponents.Product do
|
||||
<% end %>
|
||||
|
||||
<.accordion_item title="Shipping & Returns">
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="details-shipping">
|
||||
<div>
|
||||
<p class="details-subheading font-semibold mb-1">Delivery</p>
|
||||
<p class="text-sm">
|
||||
<p class="details-subheading">Delivery</p>
|
||||
<p class="details-shipping-text">
|
||||
Free UK delivery on orders over £40. Standard delivery 3-5 working days. Express delivery available at checkout.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="details-subheading font-semibold mb-1">Returns</p>
|
||||
<p class="text-sm">
|
||||
<p class="details-subheading">Returns</p>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user