astro-component-kit 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +105 -27
- package/package.json +16 -3
- package/src/components/index.ts +2 -0
- package/src/components/lib/Galaxy3D.astro +249 -137
- package/src/components/lib/app/AppSidebar.astro +249 -0
- package/src/components/lib/app/AppTopbar.astro +284 -0
- package/src/components/lib/app/ArrivalsBoard.astro +176 -0
- package/src/components/lib/app/AuthCard.astro +213 -0
- package/src/components/lib/app/DataTable.astro +346 -0
- package/src/components/lib/app/GuestProfileCard.astro +133 -0
- package/src/components/lib/app/HousekeepingBoard.astro +189 -0
- package/src/components/lib/app/PageHeader.astro +157 -0
- package/src/components/lib/app/PlanUsageCard.astro +112 -0
- package/src/components/lib/app/RoomRack.astro +207 -0
- package/src/components/lib/app/SettingsSection.astro +96 -0
- package/src/components/lib/app/SwitchRow.astro +71 -0
- package/src/components/lib/app/TeamMembers.astro +190 -0
- package/src/components/lib/app/app-icons.ts +52 -0
- package/src/components/lib/app/index.ts +19 -0
- package/src/components/lib/charts/AreaChart.astro +4 -4
- package/src/components/lib/charts/BarChart.astro +3 -3
- package/src/components/lib/charts/ChartFrame.astro +10 -10
- package/src/components/lib/charts/DonutChart.astro +2 -2
- package/src/components/lib/charts/DumbbellChart.astro +1 -1
- package/src/components/lib/charts/ForecastLineChart.astro +1 -1
- package/src/components/lib/charts/GaugeChart.astro +1 -1
- package/src/components/lib/charts/HeatmapChart.astro +1 -1
- package/src/components/lib/charts/HorizontalBarChart.astro +2 -2
- package/src/components/lib/charts/KpiStatTile.astro +4 -4
- package/src/components/lib/charts/LinearProgressGauge.astro +4 -4
- package/src/components/lib/charts/MetricDeltaTable.astro +3 -3
- package/src/components/lib/charts/RadarMetricChart.astro +4 -4
- package/src/components/lib/charts/RadialBarChart.astro +1 -1
- package/src/components/lib/charts/RetentionCohortTable.astro +2 -2
- package/src/components/lib/charts/TreemapChart.astro +1 -1
- package/src/components/lib/ecommerce/CartDrawer.astro +483 -0
- package/src/components/lib/ecommerce/CartPageList.astro +290 -0
- package/src/components/lib/ecommerce/CartToast.astro +276 -0
- package/src/components/lib/ecommerce/CategoryTiles.astro +143 -0
- package/src/components/lib/ecommerce/CheckoutForm.astro +423 -0
- package/src/components/lib/ecommerce/CountdownBanner.astro +230 -0
- package/src/components/lib/ecommerce/FilterBar.astro +278 -0
- package/src/components/lib/ecommerce/FilterSidebar.astro +405 -0
- package/src/components/lib/ecommerce/LiveActivityToast.astro +213 -0
- package/src/components/lib/ecommerce/OrderSummary.astro +295 -0
- package/src/components/lib/ecommerce/ProductBuyBox.astro +247 -0
- package/src/components/lib/ecommerce/ProductCardMinimal.astro +229 -0
- package/src/components/lib/ecommerce/ProductGallery.astro +145 -0
- package/src/components/lib/ecommerce/ProductGrid.astro +118 -0
- package/src/components/lib/ecommerce/ProductMedia.astro +113 -0
- package/src/components/lib/ecommerce/ProductRating.astro +78 -0
- package/src/components/lib/ecommerce/ProductToolbar.astro +93 -0
- package/src/components/lib/ecommerce/PromoBar.astro +145 -0
- package/src/components/lib/ecommerce/StockIndicator.astro +127 -0
- package/src/components/lib/ecommerce/StoreHeader.astro +446 -0
- package/src/components/lib/ecommerce/StoreHero.astro +260 -0
- package/src/components/lib/ecommerce/StoreProductCard.astro +307 -0
- package/src/components/lib/ecommerce/TrustBadges.astro +101 -0
- package/src/components/lib/ecommerce/VariantPicker.astro +145 -0
- package/src/components/lib/ecommerce/cart-store.ts +151 -0
- package/src/components/lib/ecommerce/catalog-filter.ts +62 -0
- package/src/components/lib/ecommerce/index.ts +34 -0
- package/src/components/lib/ecommerce/product-icons.ts +28 -0
- package/src/components/lib/ecommerce/types.ts +70 -0
- package/src/components/lib/forms/AnimatedInput.astro +4 -2
- package/src/components/lib/forms/CustomRadioGroup.astro +3 -1
- package/src/components/lib/index.ts +2 -0
- package/src/components/lib/layout/BentoGrid3.astro +6 -4
- package/src/components/lib/layout/CenterFixedBox.astro +2 -2
- package/src/components/lib/layout/CenteredContainer.astro +4 -2
- package/src/components/lib/layout/ClusterHorizontal.astro +2 -2
- package/src/components/lib/layout/FeatureHero.astro +4 -2
- package/src/components/lib/layout/FooterCenteredMinimal.astro +4 -2
- package/src/components/lib/layout/FullBleedSection.astro +2 -2
- package/src/components/lib/layout/GalleryGrid4.astro +2 -2
- package/src/components/lib/layout/HolyGrailGrid.astro +11 -9
- package/src/components/lib/layout/LandingSectionSplit.astro +5 -3
- package/src/components/lib/layout/MasonryGrid.astro +2 -2
- package/src/components/lib/layout/MinimalFooterGrid.astro +1 -1
- package/src/components/lib/layout/ScrollSnapSection.astro +1 -1
- package/src/components/lib/layout/SidebarLayoutWrapper.astro +4 -2
- package/src/components/lib/layout/SplitScreenLayout.astro +3 -3
- package/src/components/lib/layout/StepLayoutVertical.astro +1 -1
- package/src/components/lib/layout/StickySidebarWrapper.astro +4 -2
- package/src/components/lib/sections/AgencyContactBrief.astro +194 -0
- package/src/components/lib/sections/AgencyFooterCta.astro +94 -0
- package/src/components/lib/sections/AgencyHeader.astro +161 -0
- package/src/components/lib/sections/AgencyHeroKinetic.astro +172 -0
- package/src/components/lib/sections/AgencyServicesList.astro +113 -0
- package/src/components/lib/sections/AgencyTeamGrid.astro +111 -0
- package/src/components/lib/sections/AgencyWorkGrid.astro +147 -0
- package/src/components/lib/sections/BlogGridSection.astro +9 -7
- package/src/components/lib/sections/ButtonsShowcase.astro +20 -18
- package/src/components/lib/sections/CardsShowcase.astro +28 -26
- package/src/components/lib/sections/CareersOpenings.astro +10 -8
- package/src/components/lib/sections/ChangelogSection.astro +14 -7
- package/src/components/lib/sections/ContactGlassForm.astro +10 -8
- package/src/components/lib/sections/ContactSplitMap.astro +11 -9
- package/src/components/lib/sections/CtaAppDownload.astro +13 -11
- package/src/components/lib/sections/CtaGlowBanner.astro +12 -10
- package/src/components/lib/sections/CtaNewsletterSection.astro +13 -11
- package/src/components/lib/sections/CtaSplitImage.astro +13 -11
- package/src/components/lib/sections/DataShowcase.astro +23 -21
- package/src/components/lib/sections/FaqAccordionSection.astro +8 -8
- package/src/components/lib/sections/FaqTwoColumn.astro +9 -7
- package/src/components/lib/sections/FeatureAlternatingRows.astro +10 -8
- package/src/components/lib/sections/FeatureBentoGrid.astro +13 -11
- package/src/components/lib/sections/FeatureIconGrid.astro +6 -6
- package/src/components/lib/sections/FeatureTabsShowcase.astro +13 -11
- package/src/components/lib/sections/FeatureTimelineFlow.astro +8 -6
- package/src/components/lib/sections/FeedbackShowcase.astro +15 -13
- package/src/components/lib/sections/FooterMegaSection.astro +13 -11
- package/src/components/lib/sections/FormsShowcase.astro +22 -20
- package/src/components/lib/sections/HeroDashboardPreview.astro +11 -9
- package/src/components/lib/sections/HeroGlass.astro +22 -20
- package/src/components/lib/sections/HeroGradient.astro +15 -13
- package/src/components/lib/sections/HeroMinimalCentered.astro +10 -10
- package/src/components/lib/sections/HeroShowcaseSplit.astro +18 -16
- package/src/components/lib/sections/HeroSplit.astro +13 -11
- package/src/components/lib/sections/HeroTerminal.astro +14 -12
- package/src/components/lib/sections/HeroWaitlist.astro +10 -8
- package/src/components/lib/sections/HowItWorksSteps.astro +12 -10
- package/src/components/lib/sections/IntegrationsGrid.astro +7 -7
- package/src/components/lib/sections/LayoutShowcase.astro +21 -19
- package/src/components/lib/sections/LogoCloudSection.astro +4 -4
- package/src/components/lib/sections/LogoWallGrid.astro +6 -4
- package/src/components/lib/sections/NavigationShowcase.astro +15 -13
- package/src/components/lib/sections/OverlaysShowcase.astro +21 -19
- package/src/components/lib/sections/PricingComparisonMatrix.astro +15 -13
- package/src/components/lib/sections/PricingSinglePlan.astro +13 -11
- package/src/components/lib/sections/PricingTierCards.astro +11 -9
- package/src/components/lib/sections/PricingToggleCards.astro +13 -13
- package/src/components/lib/sections/ShopCategoryHero.astro +103 -0
- package/src/components/lib/sections/ShopCollectionMosaic.astro +131 -0
- package/src/components/lib/sections/ShopFooterWordmark.astro +100 -0
- package/src/components/lib/sections/ShopHeaderEditorial.astro +396 -0
- package/src/components/lib/sections/ShopHeroEditorial.astro +151 -0
- package/src/components/lib/sections/ShopLookbook.astro +250 -0
- package/src/components/lib/sections/ShopNewsletterEditorial.astro +128 -0
- package/src/components/lib/sections/ShopProductRail.astro +150 -0
- package/src/components/lib/sections/ShopReviewsEditorial.astro +133 -0
- package/src/components/lib/sections/ShopValuesMarquee.astro +91 -0
- package/src/components/lib/sections/StatsCounterBand.astro +9 -7
- package/src/components/lib/sections/StatsDashboardSection.astro +4 -2
- package/src/components/lib/sections/StatsMetricShowcase.astro +9 -7
- package/src/components/lib/sections/TeamGridSection.astro +12 -10
- package/src/components/lib/sections/TestimonialCarousel.astro +10 -8
- package/src/components/lib/sections/TestimonialFeatured.astro +7 -5
- package/src/components/lib/sections/TestimonialGridSection.astro +7 -7
- package/src/components/lib/sections/TestimonialWallGlass.astro +11 -9
- package/src/components/lib/sections/TypographyShowcase.astro +10 -8
- package/src/components/lib/sections/index.ts +10 -0
- package/src/components/lib/templates/ecommerce/StoreCategoryPage.astro +160 -0
- package/src/components/lib/templates/ecommerce/StoreCheckoutPage.astro +98 -0
- package/src/components/lib/templates/ecommerce/StoreHomePage.astro +191 -0
- package/src/components/lib/templates/ecommerce/StoreLayout.astro +169 -0
- package/src/components/lib/templates/ecommerce/StoreProductPage.astro +165 -0
- package/src/components/lib/templates/ecommerce/data.ts +187 -0
- package/src/components/lib/templates/ecommerce/index.ts +14 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialBagPage.astro +52 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialCheckoutPage.astro +123 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialHomePage.astro +102 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialLayout.astro +216 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialProductPage.astro +205 -0
- package/src/components/lib/templates/ecommerce-editorial/EditorialShopPage.astro +112 -0
- package/src/components/lib/templates/ecommerce-editorial/data.ts +125 -0
- package/src/components/lib/templates/ecommerce-editorial/index.ts +16 -0
- package/src/components/lib/templates/saas-hotel/AppLayout.astro +83 -0
- package/src/components/lib/templates/saas-hotel/DashboardPage.astro +66 -0
- package/src/components/lib/templates/saas-hotel/GuestsPage.astro +65 -0
- package/src/components/lib/templates/saas-hotel/LandingPage.astro +213 -0
- package/src/components/lib/templates/saas-hotel/ReportsPage.astro +63 -0
- package/src/components/lib/templates/saas-hotel/ReservationsPage.astro +49 -0
- package/src/components/lib/templates/saas-hotel/RoomsPage.astro +48 -0
- package/src/components/lib/templates/saas-hotel/SettingsPage.astro +115 -0
- package/src/components/lib/templates/saas-hotel/SignInPage.astro +50 -0
- package/src/components/lib/templates/saas-hotel/data.ts +229 -0
- package/src/components/lib/templates/saas-hotel/index.ts +19 -0
- package/src/components/lib/templates/saas-hotel/lobby-theme.css +90 -0
- package/src/styles/global.css +95 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* EditorialLayout — Full HTML document for the editorial e-commerce template: ShopHeaderEditorial
|
|
4
|
+
* with mega menu, main slot, ShopFooterWordmark and CartToast. Owns the "Forma" look through
|
|
5
|
+
* design tokens: serif display font, square corners, terracotta accent, light + dark palettes.
|
|
6
|
+
*
|
|
7
|
+
* @param {string} title - Page title (store name is appended).
|
|
8
|
+
* @param {string} description - Meta description.
|
|
9
|
+
* @param {EditorialStore} store - Catalogue and settings. Default: Forma demo store.
|
|
10
|
+
* @param {EditorialRoutes} routes - Page URLs.
|
|
11
|
+
* @param {string} currentHref - Highlights the matching menu link.
|
|
12
|
+
* @param {string} theme - Initial theme (light|dark|auto). The header toggle overrides it per visitor. Default 'light'.
|
|
13
|
+
* @param {boolean} seedCart - Pre-fill an empty bag with demo items (previews only). Default false.
|
|
14
|
+
* @param {boolean} noindex - Add robots noindex. Default false.
|
|
15
|
+
* @param {boolean} minimal - Hide the menu and footer columns (checkout focus mode). Default false.
|
|
16
|
+
* @param {string} lang - Document language. Default 'en'.
|
|
17
|
+
*/
|
|
18
|
+
import '../../../../styles/global.css';
|
|
19
|
+
import ShopHeaderEditorial from '../../sections/ShopHeaderEditorial.astro';
|
|
20
|
+
import ShopFooterWordmark from '../../sections/ShopFooterWordmark.astro';
|
|
21
|
+
import CartToast from '../../ecommerce/CartToast.astro';
|
|
22
|
+
import { formaStore, editorialRoutes, editorialCategoryHref, type EditorialStore, type EditorialRoutes } from './data';
|
|
23
|
+
|
|
24
|
+
interface Props {
|
|
25
|
+
title: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
store?: EditorialStore;
|
|
28
|
+
routes?: EditorialRoutes;
|
|
29
|
+
currentHref?: string;
|
|
30
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
31
|
+
seedCart?: boolean;
|
|
32
|
+
noindex?: boolean;
|
|
33
|
+
minimal?: boolean;
|
|
34
|
+
lang?: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const {
|
|
38
|
+
title,
|
|
39
|
+
description,
|
|
40
|
+
store = formaStore,
|
|
41
|
+
routes = editorialRoutes,
|
|
42
|
+
currentHref,
|
|
43
|
+
theme = 'light',
|
|
44
|
+
seedCart = false,
|
|
45
|
+
noindex = false,
|
|
46
|
+
minimal = false,
|
|
47
|
+
lang = 'en',
|
|
48
|
+
} = Astro.props;
|
|
49
|
+
|
|
50
|
+
const fullTitle = title.includes(store.name) ? title : `${title} — ${store.name} Studio`;
|
|
51
|
+
const half = Math.ceil(store.categories.length / 2);
|
|
52
|
+
const links = [
|
|
53
|
+
{
|
|
54
|
+
label: 'Shop',
|
|
55
|
+
href: routes.shop,
|
|
56
|
+
columns: [
|
|
57
|
+
{ title: 'Categories', links: store.categories.slice(0, half).map((c) => ({ label: c.name, href: editorialCategoryHref(routes, c.id) })) },
|
|
58
|
+
{ title: 'More', links: store.categories.slice(half).map((c) => ({ label: c.name, href: editorialCategoryHref(routes, c.id) })) },
|
|
59
|
+
{ title: 'Edits', links: [{ label: 'New in', href: `${routes.shop}?sort=featured` }, { label: 'Under €100', href: `${routes.shop}?sort=price-asc` }, { label: 'Gifts', href: routes.shop }] },
|
|
60
|
+
],
|
|
61
|
+
feature: { title: 'The Autumn edit', text: 'Warm, quiet pieces for the colder months.', href: `${routes.product}wool-overcoat/`, icon: 'jacket', hue: 18 },
|
|
62
|
+
},
|
|
63
|
+
{ label: 'New in', href: `${routes.shop}?sort=featured` },
|
|
64
|
+
{ label: 'Objects', href: editorialCategoryHref(routes, 'objects') },
|
|
65
|
+
];
|
|
66
|
+
const seed = seedCart
|
|
67
|
+
? [
|
|
68
|
+
{ id: 'linen-overshirt::Oat / M', name: 'Linen Overshirt', price: 145, qty: 1, variant: 'Oat / M', icon: 'shirt' as const, hue: 30 },
|
|
69
|
+
{ id: 'kiln-mug::Clay', name: 'Kiln Mug', price: 32, qty: 2, variant: 'Clay', icon: 'mug' as const, hue: 14 },
|
|
70
|
+
]
|
|
71
|
+
: [];
|
|
72
|
+
const footerColumns = [
|
|
73
|
+
{ title: 'Shop', links: store.categories.map((c) => ({ label: c.name, href: editorialCategoryHref(routes, c.id) })) },
|
|
74
|
+
{ title: 'Care', links: [{ label: 'Shipping', href: '#' }, { label: 'Returns', href: '#' }, { label: 'Repairs', href: '#' }, { label: 'Size guide', href: '#' }] },
|
|
75
|
+
{ title: 'Studio', links: [{ label: 'About', href: '#' }, { label: 'Journal', href: '#' }, { label: 'Stockists', href: '#' }] },
|
|
76
|
+
];
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
<!doctype html>
|
|
80
|
+
<html lang={lang} class="theme-forma" data-theme={theme === 'auto' ? 'light' : theme}>
|
|
81
|
+
<head>
|
|
82
|
+
<meta charset="utf-8" />
|
|
83
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
84
|
+
<title>{fullTitle}</title>
|
|
85
|
+
{description && <meta name="description" content={description} />}
|
|
86
|
+
{noindex && <meta name="robots" content="noindex, follow" />}
|
|
87
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
88
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
89
|
+
<link href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300..600;1,9..144,300..600&display=swap" rel="stylesheet" />
|
|
90
|
+
<script is:inline define:vars={{ initial: theme }}>
|
|
91
|
+
(function () {
|
|
92
|
+
var t = initial;
|
|
93
|
+
try { t = localStorage.getItem('ack-theme') || t; } catch (e) {}
|
|
94
|
+
if (t === 'auto') t = matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
95
|
+
document.documentElement.dataset.theme = t;
|
|
96
|
+
})();
|
|
97
|
+
</script>
|
|
98
|
+
<slot name="head" />
|
|
99
|
+
</head>
|
|
100
|
+
<body class="forma">
|
|
101
|
+
<a class="forma__skip" href="#main">Skip to content</a>
|
|
102
|
+
<ShopHeaderEditorial
|
|
103
|
+
brand={store.name}
|
|
104
|
+
brandHref={routes.home}
|
|
105
|
+
announcement={minimal ? '' : store.announcement}
|
|
106
|
+
links={minimal ? [] : links}
|
|
107
|
+
searchHref={minimal ? '' : routes.shop}
|
|
108
|
+
bagHref={routes.bag}
|
|
109
|
+
cartKey={store.cartKey}
|
|
110
|
+
currentHref={currentHref}
|
|
111
|
+
/>
|
|
112
|
+
<main id="main" class="forma__main" tabindex="-1">
|
|
113
|
+
<slot />
|
|
114
|
+
</main>
|
|
115
|
+
<ShopFooterWordmark brand={store.name} brandHref={routes.home} columns={minimal ? [] : footerColumns} contact={minimal ? undefined : store.contact} copyright={`© ${new Date().getFullYear()} ${store.name} Studio · Demo store built with Astro Components Kit`} />
|
|
116
|
+
<CartToast cartKey={store.cartKey} position="bottom-right" actionLabel="View bag" addedTitle="Added to bag" />
|
|
117
|
+
{seed.length > 0 && <div hidden data-forma-seed={JSON.stringify(seed)} data-key={store.cartKey}></div>}
|
|
118
|
+
</body>
|
|
119
|
+
</html>
|
|
120
|
+
|
|
121
|
+
<script>
|
|
122
|
+
import { createCart } from '../../ecommerce/cart-store';
|
|
123
|
+
|
|
124
|
+
// Demo seed (previews) + "View bag" goes to the bag page instead of a drawer.
|
|
125
|
+
const seed = document.querySelector<HTMLElement>('[data-forma-seed]');
|
|
126
|
+
if (seed) { try { createCart(seed.dataset.key).seed(JSON.parse(seed.dataset.formaSeed || '[]')); } catch { /* ignore */ } }
|
|
127
|
+
document.addEventListener('ack:cart-open', () => {
|
|
128
|
+
const bag = document.querySelector<HTMLAnchorElement>('a[data-bag-link]');
|
|
129
|
+
if (bag) window.location.href = bag.href;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Expose the header height so sticky toolbars sit right under it.
|
|
133
|
+
const header = document.querySelector<HTMLElement>('[data-shop-header]');
|
|
134
|
+
const setH = () => { if (header) document.documentElement.style.setProperty('--shop-header-h', `${header.offsetHeight}px`); };
|
|
135
|
+
setH();
|
|
136
|
+
window.addEventListener('resize', setH);
|
|
137
|
+
</script>
|
|
138
|
+
|
|
139
|
+
<style is:global>
|
|
140
|
+
.theme-forma {
|
|
141
|
+
--font-display: 'Fraunces', Georgia, 'Times New Roman', serif;
|
|
142
|
+
--r-sm: 2px;
|
|
143
|
+
--r-md: 3px;
|
|
144
|
+
--r-lg: 4px;
|
|
145
|
+
--r-xl: 6px;
|
|
146
|
+
--shop-header-h: 106px;
|
|
147
|
+
}
|
|
148
|
+
.theme-forma[data-theme='light'] {
|
|
149
|
+
--c-bg: #f3efe8;
|
|
150
|
+
--c-bg-1: #fbf9f5;
|
|
151
|
+
--c-bg-2: #e7e1d6;
|
|
152
|
+
--c-surface: rgba(23, 21, 18, 0.03);
|
|
153
|
+
--c-border: rgba(23, 21, 18, 0.14);
|
|
154
|
+
--c-text-1: #171512;
|
|
155
|
+
--c-text-2: #514b44;
|
|
156
|
+
--c-text-3: #6d665e;
|
|
157
|
+
--c-primary: #9c3d1c;
|
|
158
|
+
--c-primary-light: #9c3d1c;
|
|
159
|
+
--c-primary-glow: rgba(156, 61, 28, 0.14);
|
|
160
|
+
--c-accent: #7a2e14;
|
|
161
|
+
--shop-accent: #9c3d1c;
|
|
162
|
+
--c-on-primary: #ffffff;
|
|
163
|
+
--ink-rgb: 23 21 18;
|
|
164
|
+
--paper-rgb: 251 249 245;
|
|
165
|
+
--c-success: #2f6b3a;
|
|
166
|
+
--c-warning: #8a4b06;
|
|
167
|
+
--c-danger: #a8261b;
|
|
168
|
+
--pm-ink-s: 45%;
|
|
169
|
+
--pm-ink-l: 32%;
|
|
170
|
+
--pm-soft-s: 30%;
|
|
171
|
+
--pm-soft-l: 80%;
|
|
172
|
+
--pm-soft-a: 0.95;
|
|
173
|
+
color-scheme: light;
|
|
174
|
+
}
|
|
175
|
+
.theme-forma[data-theme='dark'] {
|
|
176
|
+
--c-bg: #12110f;
|
|
177
|
+
--c-bg-1: #1a1815;
|
|
178
|
+
--c-bg-2: #24211c;
|
|
179
|
+
--c-surface: rgba(242, 237, 229, 0.03);
|
|
180
|
+
--c-border: rgba(242, 237, 229, 0.14);
|
|
181
|
+
--c-text-1: #f2ede5;
|
|
182
|
+
--c-text-2: #bcb4a8;
|
|
183
|
+
--c-text-3: #948b7f;
|
|
184
|
+
--c-primary: #e2835a;
|
|
185
|
+
--c-primary-light: #e2835a;
|
|
186
|
+
--c-primary-glow: rgba(226, 131, 90, 0.16);
|
|
187
|
+
--c-accent: #f0b796;
|
|
188
|
+
--shop-accent: #e2835a;
|
|
189
|
+
--c-on-primary: #1a0d07;
|
|
190
|
+
--ink-rgb: 242 237 229;
|
|
191
|
+
--paper-rgb: 18 17 15;
|
|
192
|
+
--c-success: #7fc48c;
|
|
193
|
+
--c-warning: #e7b46a;
|
|
194
|
+
--c-danger: #f08a7a;
|
|
195
|
+
--pm-ink-s: 45%;
|
|
196
|
+
--pm-ink-l: 76%;
|
|
197
|
+
--pm-soft-s: 25%;
|
|
198
|
+
--pm-soft-l: 24%;
|
|
199
|
+
--pm-soft-a: 0.8;
|
|
200
|
+
color-scheme: dark;
|
|
201
|
+
}
|
|
202
|
+
body.forma { background: var(--c-bg); color: var(--c-text-2); overflow-x: clip; }
|
|
203
|
+
body.forma::before { display: none; }
|
|
204
|
+
.forma ::selection { background: var(--shop-accent); color: var(--c-on-primary); }
|
|
205
|
+
.forma__main { outline: none; }
|
|
206
|
+
.forma-wrap { width: 100%; max-width: 1440px; margin: 0 auto; padding: 0 clamp(1rem, 3vw, 2.5rem); }
|
|
207
|
+
.forma__skip { position: fixed; top: -60px; left: 12px; z-index: 2000; padding: 0.6rem 1rem; background: var(--c-text-1); color: var(--c-bg); font-weight: 700; transition: top 0.2s ease; }
|
|
208
|
+
.forma__skip:focus { top: 12px; color: var(--c-bg); }
|
|
209
|
+
.forma .section-space { padding-block: clamp(3rem, 7vw, 6rem); }
|
|
210
|
+
|
|
211
|
+
/* Reused library pieces, tuned to the editorial look */
|
|
212
|
+
.forma .badge { border-radius: var(--r-sm); }
|
|
213
|
+
.forma .cart-toast { border-radius: var(--r-sm); }
|
|
214
|
+
.forma .anim-input, .forma .custom-select, .forma .stepper { border-radius: var(--r-sm); background: var(--c-bg-1); border-color: var(--c-border); }
|
|
215
|
+
.theme-forma[data-theme='light'] .anim-input, .theme-forma[data-theme='light'] .custom-select { background: #fff; }
|
|
216
|
+
</style>
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* EditorialProductPage — Editorial product detail: stacked full-bleed images on the left, a sticky
|
|
4
|
+
* info column (VariantPicker colour + size, add to bag, StockIndicator, size guide and care notes),
|
|
5
|
+
* then "Complete the look" (ShopProductRail) and reviews (ShopReviewsEditorial).
|
|
6
|
+
*
|
|
7
|
+
* Usage (src/pages/product/[slug].astro):
|
|
8
|
+
* import { EditorialProductPage, formaStore } from 'astro-component-kit/templates/ecommerce-editorial';
|
|
9
|
+
* export const getStaticPaths = () => formaStore.products.map((p) => ({ params: { slug: p.slug } }));
|
|
10
|
+
* <EditorialProductPage slug={Astro.params.slug} />
|
|
11
|
+
*
|
|
12
|
+
* @param {string} slug - Product slug. Default: first product.
|
|
13
|
+
* @param {EditorialStore} store - Catalogue and settings.
|
|
14
|
+
* @param {EditorialRoutes} routes - Page URLs.
|
|
15
|
+
* @param {string} theme - Initial theme (light|dark|auto). Default 'light'.
|
|
16
|
+
* @param {boolean} seedCart - Pre-fill the bag with demo items. Default false.
|
|
17
|
+
* @param {boolean} noindex - Add robots noindex. Default false.
|
|
18
|
+
*/
|
|
19
|
+
import EditorialLayout from './EditorialLayout.astro';
|
|
20
|
+
import ProductMedia from '../../ecommerce/ProductMedia.astro';
|
|
21
|
+
import ProductRating from '../../ecommerce/ProductRating.astro';
|
|
22
|
+
import VariantPicker from '../../ecommerce/VariantPicker.astro';
|
|
23
|
+
import StockIndicator from '../../ecommerce/StockIndicator.astro';
|
|
24
|
+
import ShopProductRail from '../../sections/ShopProductRail.astro';
|
|
25
|
+
import ShopReviewsEditorial from '../../sections/ShopReviewsEditorial.astro';
|
|
26
|
+
import { formatMoney } from '../../ecommerce/cart-store';
|
|
27
|
+
import type { Product, StoreCategory } from '../../ecommerce/types';
|
|
28
|
+
import { formaStore, editorialRoutes, editorialCategoryHref, type EditorialStore, type EditorialRoutes } from './data';
|
|
29
|
+
|
|
30
|
+
interface Props {
|
|
31
|
+
slug?: string;
|
|
32
|
+
store?: EditorialStore;
|
|
33
|
+
routes?: EditorialRoutes;
|
|
34
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
35
|
+
seedCart?: boolean;
|
|
36
|
+
noindex?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const { slug, store = formaStore, routes = editorialRoutes, theme = 'light', seedCart = false, noindex = false } = Astro.props as Props;
|
|
40
|
+
const catalog: EditorialStore = store;
|
|
41
|
+
|
|
42
|
+
const product: Product = catalog.products.find((p: Product) => p.slug === slug) ?? catalog.products[0];
|
|
43
|
+
const category: StoreCategory | undefined = catalog.categories.find((c: StoreCategory) => c.id === product.category);
|
|
44
|
+
const money = { currency: store.currency, locale: store.locale };
|
|
45
|
+
const inStock = product.inStock !== false;
|
|
46
|
+
const views = [0, 1, 2, 3].map((i) => ({ hue: (product.hue ?? 30) + i * 14, turn: [0, -12, 10, 0][i], zoom: [1, 1.25, 0.9, 1.5][i], variant: (i % 2 ? 'glow' : 'soft') as 'glow' | 'soft' }));
|
|
47
|
+
const related: Product[] = catalog.products.filter((p: Product) => p.slug !== product.slug && p.category !== product.category).slice(0, 6);
|
|
48
|
+
const payload = JSON.stringify({ id: product.slug, name: product.name, price: product.price, icon: product.icon, hue: product.hue, image: product.image });
|
|
49
|
+
const productReviews = catalog.reviews.filter((r: EditorialStore['reviews'][number]) => r.product === product.name);
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
<EditorialLayout title={product.name} description={product.description} store={store} routes={routes} theme={theme} seedCart={seedCart} noindex={noindex}>
|
|
53
|
+
<div class="forma-wrap">
|
|
54
|
+
<nav class="pdp-crumbs" aria-label="Breadcrumb">
|
|
55
|
+
<ol>
|
|
56
|
+
<li><a href={routes.home}>Home</a></li>
|
|
57
|
+
<li><a href={routes.shop}>Shop</a></li>
|
|
58
|
+
{category && <li><a href={editorialCategoryHref(routes, category.id)}>{category.name}</a></li>}
|
|
59
|
+
<li><span aria-current="page">{product.name}</span></li>
|
|
60
|
+
</ol>
|
|
61
|
+
</nav>
|
|
62
|
+
|
|
63
|
+
<div class="pdp">
|
|
64
|
+
<div class="pdp__gallery" aria-label={`${product.name} images`} role="group">
|
|
65
|
+
{product.image ? (
|
|
66
|
+
<ProductMedia image={product.image} alt={product.name} ratio="4 / 5" />
|
|
67
|
+
) : (
|
|
68
|
+
views.map((v, i) => (
|
|
69
|
+
<div class:list={['pdp__view', { 'pdp__view--wide': i === 0 || i === 3 }]} style={`--turn: ${v.turn}deg; --zoom: ${v.zoom}`}>
|
|
70
|
+
<ProductMedia icon={product.icon} hue={v.hue} ratio={i === 0 || i === 3 ? '4 / 5' : '3 / 4'} variant={v.variant} size="lg" />
|
|
71
|
+
</div>
|
|
72
|
+
))
|
|
73
|
+
)}
|
|
74
|
+
</div>
|
|
75
|
+
|
|
76
|
+
<div class="pdp__info">
|
|
77
|
+
<p class="pdp__cat">{category?.name}{product.badge && <span> · {product.badge}</span>}</p>
|
|
78
|
+
<h1 class="pdp__title">{product.name}</h1>
|
|
79
|
+
<p class="pdp__price">
|
|
80
|
+
<span>{formatMoney(product.price, money)}</span>
|
|
81
|
+
{product.compareAt && <s><span class="sr-only">Was </span>{formatMoney(product.compareAt, money)}</s>}
|
|
82
|
+
</p>
|
|
83
|
+
{product.rating !== undefined && (
|
|
84
|
+
<a class="pdp__rating" href="#reviews"><ProductRating value={product.rating} count={product.reviews} /></a>
|
|
85
|
+
)}
|
|
86
|
+
{product.description && <p class="pdp__desc">{product.description}</p>}
|
|
87
|
+
|
|
88
|
+
<div class="pdp__pickers">
|
|
89
|
+
{product.colors && product.colors.length > 0 && (
|
|
90
|
+
<VariantPicker label="Colour" name="pdp-color" type="swatch" size="sm" target="pdp-add" options={product.colors.map((c: { name: string; hex: string }) => ({ label: c.name, hex: c.hex }))} />
|
|
91
|
+
)}
|
|
92
|
+
{product.sizes && product.sizes.length > 0 && (
|
|
93
|
+
<VariantPicker label="Size" name="pdp-size" type="pill" size="sm" target="pdp-add" options={product.sizes.map((s: string) => ({ label: s }))} selected={Math.floor(product.sizes.length / 2)} />
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{product.sizes && product.sizes.length > 0 && (
|
|
98
|
+
<details class="pdp__guide">
|
|
99
|
+
<summary>Size guide</summary>
|
|
100
|
+
<p>Our pieces are cut relaxed. If you are between sizes, choose the smaller one for a closer fit. Model is 183 cm and wears size M.</p>
|
|
101
|
+
</details>
|
|
102
|
+
)}
|
|
103
|
+
|
|
104
|
+
<button
|
|
105
|
+
type="button"
|
|
106
|
+
class="pdp__add"
|
|
107
|
+
id="pdp-add"
|
|
108
|
+
data-add-to-cart={payload}
|
|
109
|
+
data-cart-key={store.cartKey}
|
|
110
|
+
data-open-cart="false"
|
|
111
|
+
data-added-label="Added to bag ✓"
|
|
112
|
+
disabled={!inStock}
|
|
113
|
+
><span data-label>{inStock ? `Add to bag — ${formatMoney(product.price, money)}` : 'Sold out'}</span></button>
|
|
114
|
+
|
|
115
|
+
{inStock && product.stock !== undefined && product.stock <= 5 && (
|
|
116
|
+
<StockIndicator stock={product.stock} viewers={6 + ((product.reviews ?? 0) % 11)} variant="inline" size="sm" />
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
<ul class="pdp__notes">
|
|
120
|
+
<li>Complimentary shipping over {formatMoney(store.freeShippingThreshold, money)}</li>
|
|
121
|
+
<li>Free returns within 30 days</li>
|
|
122
|
+
<li>Free repairs, for life</li>
|
|
123
|
+
</ul>
|
|
124
|
+
|
|
125
|
+
<div class="pdp__acc">
|
|
126
|
+
<details open><summary>Materials & care</summary><p>Natural fibres, traceable to the mill. Wash cold, dry flat, iron while damp. Every piece comes with a care card.</p></details>
|
|
127
|
+
<details><summary>Made by</summary><p>A family workshop we have worked with since day one. Small batches, fair wages and audited conditions.</p></details>
|
|
128
|
+
<details><summary>Shipping & returns</summary><p>Ships in 1–2 days in plastic-free packaging. Return anything within 30 days — we cover the label.</p></details>
|
|
129
|
+
</div>
|
|
130
|
+
</div>
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
{related.length > 0 && (
|
|
135
|
+
<div class="forma-wrap section-space">
|
|
136
|
+
<ShopProductRail eyebrow="Pairs well with" title="Complete the *look*" products={related} productBase={routes.product} link={{ label: 'Shop all', href: routes.shop }} id="pdp-rail" currency={store.currency} locale={store.locale} cartKey={store.cartKey} />
|
|
137
|
+
</div>
|
|
138
|
+
)}
|
|
139
|
+
|
|
140
|
+
<div class="forma-wrap section-space pdp-reviews" id="reviews">
|
|
141
|
+
<ShopReviewsEditorial eyebrow="Reviews" score={product.rating ?? 4.8} count={product.reviews ?? 0} reviews={productReviews.length ? productReviews : store.reviews} />
|
|
142
|
+
</div>
|
|
143
|
+
</EditorialLayout>
|
|
144
|
+
|
|
145
|
+
<style>
|
|
146
|
+
.pdp-crumbs ol { display: flex; flex-wrap: wrap; gap: 0.5rem; margin: 0; padding: var(--sp-5) 0; list-style: none; font-size: 0.72rem; letter-spacing: 0.1em; text-transform: uppercase; color: var(--c-text-2); }
|
|
147
|
+
.pdp-crumbs li + li::before { content: '/'; margin-right: 0.5rem; opacity: 0.5; }
|
|
148
|
+
.pdp-crumbs a { color: inherit; }
|
|
149
|
+
.pdp-crumbs a:hover, .pdp-crumbs [aria-current] { color: var(--c-text-1); }
|
|
150
|
+
|
|
151
|
+
.pdp { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 0.9fr); gap: clamp(1.5rem, 4vw, 4.5rem); align-items: start; }
|
|
152
|
+
.pdp__gallery { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
|
|
153
|
+
.pdp__view--wide { grid-column: 1 / -1; }
|
|
154
|
+
.pdp__view :global(.p-media__art) { transform: rotate(var(--turn)) scale(var(--zoom)); }
|
|
155
|
+
.pdp__view :global(.p-media) { border-radius: var(--r-sm); }
|
|
156
|
+
|
|
157
|
+
.pdp__info { position: sticky; top: calc(var(--shop-header-h) + 1.5rem); display: flex; flex-direction: column; gap: var(--sp-4); padding-bottom: var(--sp-8); }
|
|
158
|
+
.pdp__cat { margin: 0; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--c-text-2); }
|
|
159
|
+
.pdp__cat span { color: var(--shop-accent); }
|
|
160
|
+
.pdp__title { margin: 0; font-family: var(--font-display); font-size: clamp(2.4rem, 4.5vw, 3.8rem); font-weight: 400; letter-spacing: -0.04em; line-height: 0.95; color: var(--c-text-1); }
|
|
161
|
+
.pdp__price { display: flex; gap: var(--sp-3); margin: 0; font-size: 1.2rem; color: var(--c-text-1); font-variant-numeric: tabular-nums; }
|
|
162
|
+
.pdp__price s { color: var(--c-text-2); }
|
|
163
|
+
.pdp__rating { width: fit-content; }
|
|
164
|
+
.pdp__rating :global(.p-rating) { --p-rating-star: var(--shop-accent); }
|
|
165
|
+
.pdp__desc { margin: 0; max-width: 34rem; line-height: 1.7; }
|
|
166
|
+
.pdp__pickers { display: grid; gap: var(--sp-5); padding-top: var(--sp-4); border-top: 1px solid var(--c-border); }
|
|
167
|
+
.pdp__pickers { --vp-radius: var(--r-sm); --vp-active-bg: var(--c-text-1); --vp-active-color: var(--c-bg); --vp-active-border: var(--c-text-1); --vp-ring: var(--c-text-1); }
|
|
168
|
+
.pdp__guide summary, .pdp__acc summary { display: flex; align-items: center; justify-content: space-between; min-height: 48px; color: var(--c-text-1); font-size: 0.9rem; cursor: pointer; list-style: none; }
|
|
169
|
+
.pdp__guide summary { width: fit-content; min-height: 40px; text-decoration: underline; text-underline-offset: 4px; }
|
|
170
|
+
.pdp__guide summary::-webkit-details-marker, .pdp__acc summary::-webkit-details-marker { display: none; }
|
|
171
|
+
.pdp__guide p, .pdp__acc p { margin: 0 0 var(--sp-4); font-size: 0.9rem; line-height: 1.7; }
|
|
172
|
+
.pdp__add {
|
|
173
|
+
width: 100%;
|
|
174
|
+
min-height: 58px;
|
|
175
|
+
border: 0;
|
|
176
|
+
border-radius: var(--r-sm);
|
|
177
|
+
background: var(--c-text-1);
|
|
178
|
+
color: var(--c-bg);
|
|
179
|
+
font: inherit;
|
|
180
|
+
font-size: 0.9rem;
|
|
181
|
+
font-weight: 700;
|
|
182
|
+
letter-spacing: 0.06em;
|
|
183
|
+
text-transform: uppercase;
|
|
184
|
+
cursor: pointer;
|
|
185
|
+
transition: background 0.2s ease, color 0.2s ease;
|
|
186
|
+
}
|
|
187
|
+
.pdp__add:hover:not(:disabled), .pdp__add.is-added { background: var(--shop-accent); color: var(--c-on-primary); }
|
|
188
|
+
.pdp__add:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
189
|
+
.pdp__add:focus-visible, .pdp-crumbs a:focus-visible, .pdp summary:focus-visible, .pdp__rating:focus-visible { outline: 2px solid var(--shop-accent); outline-offset: 3px; }
|
|
190
|
+
.pdp__notes { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; font-size: 0.82rem; }
|
|
191
|
+
.pdp__notes li::before { content: '— '; color: var(--shop-accent); }
|
|
192
|
+
.pdp__acc { border-top: 1px solid var(--c-border); }
|
|
193
|
+
.pdp__acc details { border-bottom: 1px solid var(--c-border); }
|
|
194
|
+
.pdp__acc summary::after { content: '+'; font-size: 1.2rem; transition: transform 0.2s ease; }
|
|
195
|
+
.pdp__acc details[open] summary::after { transform: rotate(45deg); }
|
|
196
|
+
.pdp-reviews { padding-top: 0; }
|
|
197
|
+
.sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
198
|
+
|
|
199
|
+
@media (max-width: 900px) {
|
|
200
|
+
.pdp { grid-template-columns: 1fr; }
|
|
201
|
+
.pdp__info { position: static; }
|
|
202
|
+
.pdp__gallery { grid-auto-flow: column; grid-template-columns: none; grid-auto-columns: 82%; overflow-x: auto; scroll-snap-type: x mandatory; }
|
|
203
|
+
.pdp__view, .pdp__view--wide { grid-column: auto; scroll-snap-align: start; }
|
|
204
|
+
}
|
|
205
|
+
</style>
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
---
|
|
2
|
+
/**
|
|
3
|
+
* EditorialShopPage — Editorial catalogue: ShopCategoryHero, sticky FilterBar (chips, refine
|
|
4
|
+
* panel, sort, live count) and a grid of ProductCardMinimal with quick add.
|
|
5
|
+
*
|
|
6
|
+
* Usage (src/pages/shop.astro):
|
|
7
|
+
* import { EditorialShopPage } from 'astro-component-kit/templates/ecommerce-editorial';
|
|
8
|
+
* <EditorialShopPage />
|
|
9
|
+
* Category links use `?category=<id>`; pass `category` to build one static page per category.
|
|
10
|
+
*
|
|
11
|
+
* @param {EditorialStore} store - Catalogue and settings.
|
|
12
|
+
* @param {EditorialRoutes} routes - Page URLs.
|
|
13
|
+
* @param {string} category - Pre-selected category id.
|
|
14
|
+
* @param {string} theme - Initial theme (light|dark|auto). Default 'light'.
|
|
15
|
+
* @param {boolean} seedCart - Pre-fill the bag with demo items. Default false.
|
|
16
|
+
* @param {boolean} noindex - Add robots noindex. Default false.
|
|
17
|
+
*/
|
|
18
|
+
import EditorialLayout from './EditorialLayout.astro';
|
|
19
|
+
import ShopCategoryHero from '../../sections/ShopCategoryHero.astro';
|
|
20
|
+
import ShopNewsletterEditorial from '../../sections/ShopNewsletterEditorial.astro';
|
|
21
|
+
import FilterBar from '../../ecommerce/FilterBar.astro';
|
|
22
|
+
import ProductCardMinimal from '../../ecommerce/ProductCardMinimal.astro';
|
|
23
|
+
import { formaStore, editorialRoutes, editorialProductHref, editorialCategoryHref, type EditorialStore, type EditorialRoutes } from './data';
|
|
24
|
+
|
|
25
|
+
interface Props {
|
|
26
|
+
store?: EditorialStore;
|
|
27
|
+
routes?: EditorialRoutes;
|
|
28
|
+
category?: string;
|
|
29
|
+
theme?: 'light' | 'dark' | 'auto';
|
|
30
|
+
seedCart?: boolean;
|
|
31
|
+
noindex?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const { store = formaStore, routes = editorialRoutes, category, theme = 'light', seedCart = false, noindex = false } = Astro.props;
|
|
35
|
+
|
|
36
|
+
const active = store.categories.find((c) => c.id === category);
|
|
37
|
+
const maxPrice = Math.ceil(Math.max(...store.products.map((p) => p.price)) / 50) * 50;
|
|
38
|
+
const colors = Array.from(new Map(store.products.flatMap((p) => p.colors ?? []).map((c) => [c.name, c])).values());
|
|
39
|
+
const chips = store.categories.map((c) => ({ id: c.id, name: c.name, count: store.products.filter((p) => p.category === c.id).length }));
|
|
40
|
+
const title = active?.name ?? 'All pieces';
|
|
41
|
+
const text = active?.description ?? 'Everything we make, in one place — clothing, shoes, bags and objects for the home. Filter by category, colour or price.';
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
<EditorialLayout title={title} description={text} store={store} routes={routes} currentHref={routes.shop} theme={theme} seedCart={seedCart} noindex={noindex}>
|
|
45
|
+
<div class="forma-wrap">
|
|
46
|
+
<ShopCategoryHero
|
|
47
|
+
breadcrumb={[{ label: 'Home', href: routes.home }, { label: 'Shop', href: routes.shop }, ...(active ? [{ label: active.name }] : [])]}
|
|
48
|
+
title={title}
|
|
49
|
+
count={active ? chips.find((c) => c.id === active.id)?.count : store.products.length}
|
|
50
|
+
text={text}
|
|
51
|
+
links={[]}
|
|
52
|
+
visuals={[]}
|
|
53
|
+
/>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<div class="forma-wrap">
|
|
57
|
+
<FilterBar
|
|
58
|
+
target="forma-grid"
|
|
59
|
+
categories={chips}
|
|
60
|
+
selected={active ? [active.id] : []}
|
|
61
|
+
maxPrice={maxPrice}
|
|
62
|
+
colors={colors}
|
|
63
|
+
total={store.products.length}
|
|
64
|
+
countLabel="pieces"
|
|
65
|
+
currency={store.currency}
|
|
66
|
+
locale={store.locale}
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div class="forma-wrap shop-grid-wrap">
|
|
71
|
+
<ul class="shop-grid" id="forma-grid">
|
|
72
|
+
{store.products.map((p, i) => (
|
|
73
|
+
<li><ProductCardMinimal product={p} href={editorialProductHref(routes, p.slug)} index={i + 1} currency={store.currency} locale={store.locale} cartKey={store.cartKey} /></li>
|
|
74
|
+
))}
|
|
75
|
+
</ul>
|
|
76
|
+
<div class="shop-empty" data-product-empty hidden role="status">
|
|
77
|
+
<p class="shop-empty__title">Nothing matches — yet.</p>
|
|
78
|
+
<p>Try another colour or <a href={editorialCategoryHref(routes)}>see everything</a>.</p>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<div class="section-space">
|
|
83
|
+
<ShopNewsletterEditorial size="sm" title="New pieces, *first*" />
|
|
84
|
+
</div>
|
|
85
|
+
</EditorialLayout>
|
|
86
|
+
|
|
87
|
+
<style>
|
|
88
|
+
.shop-grid-wrap { padding-top: clamp(1.5rem, 4vw, 2.5rem); }
|
|
89
|
+
.shop-grid {
|
|
90
|
+
display: grid;
|
|
91
|
+
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
92
|
+
gap: clamp(1.5rem, 3vw, 2.5rem) clamp(0.75rem, 1.5vw, 1.25rem);
|
|
93
|
+
margin: 0;
|
|
94
|
+
padding: 0;
|
|
95
|
+
list-style: none;
|
|
96
|
+
}
|
|
97
|
+
.shop-grid > li { min-width: 0; }
|
|
98
|
+
.shop-grid > li:nth-child(7n + 1) { grid-column: span 2; }
|
|
99
|
+
.shop-grid > li:nth-child(7n + 1) :global(.p-media) { aspect-ratio: 3 / 2 !important; }
|
|
100
|
+
.shop-grid > li[hidden] { display: none; }
|
|
101
|
+
.shop-grid > li:nth-child(7n + 1) :global(.pcm__title) { font-size: 1.5rem; }
|
|
102
|
+
.shop-empty { padding: clamp(3rem, 8vw, 6rem) 0; text-align: center; }
|
|
103
|
+
.shop-empty[hidden] { display: none; }
|
|
104
|
+
.shop-empty p { margin: 0; }
|
|
105
|
+
.shop-empty__title { font-family: var(--font-display); font-size: 2rem; color: var(--c-text-1); }
|
|
106
|
+
.shop-empty a { color: var(--c-text-1); text-decoration: underline; }
|
|
107
|
+
@media (max-width: 1100px) { .shop-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
|
108
|
+
@media (max-width: 760px) {
|
|
109
|
+
.shop-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
110
|
+
.shop-grid > li:nth-child(7n + 1) { grid-column: 1 / -1; }
|
|
111
|
+
}
|
|
112
|
+
</style>
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// src/components/lib/templates/ecommerce-editorial/data.ts
|
|
2
|
+
// Demo catalogue and settings for the editorial e-commerce template ("Forma Studio").
|
|
3
|
+
// Copy this file, swap in your products and pass it as `store` to every page.
|
|
4
|
+
|
|
5
|
+
import type { Product, ShippingRate, StoreCategory } from '../../ecommerce/types';
|
|
6
|
+
|
|
7
|
+
export interface EditorialRoutes {
|
|
8
|
+
home: string;
|
|
9
|
+
shop: string;
|
|
10
|
+
/** Product URL prefix — pages live at `${product}${slug}/`. */
|
|
11
|
+
product: string;
|
|
12
|
+
bag: string;
|
|
13
|
+
checkout: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface EditorialStore {
|
|
17
|
+
name: string;
|
|
18
|
+
tagline: string;
|
|
19
|
+
announcement: string;
|
|
20
|
+
currency: string;
|
|
21
|
+
locale: string;
|
|
22
|
+
cartKey: string;
|
|
23
|
+
freeShippingThreshold: number;
|
|
24
|
+
shippingFlat: number;
|
|
25
|
+
taxRate: number;
|
|
26
|
+
promoCodes: Record<string, number>;
|
|
27
|
+
shippingRates: ShippingRate[];
|
|
28
|
+
categories: StoreCategory[];
|
|
29
|
+
products: Product[];
|
|
30
|
+
newIn: string[];
|
|
31
|
+
look: Array<{ slug: string; x: number; y: number }>;
|
|
32
|
+
reviews: Array<{ quote: string; name: string; place?: string; product?: string; rating?: number }>;
|
|
33
|
+
values: Array<{ title: string; text: string }>;
|
|
34
|
+
contact: { title: string; lines: string[] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const editorialRoutes: EditorialRoutes = {
|
|
38
|
+
home: '/',
|
|
39
|
+
shop: '/shop/',
|
|
40
|
+
product: '/product/',
|
|
41
|
+
bag: '/bag/',
|
|
42
|
+
checkout: '/checkout/',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const C = {
|
|
46
|
+
oat: { name: 'Oat', hex: '#d9cbb3' },
|
|
47
|
+
ink: { name: 'Ink', hex: '#22252b' },
|
|
48
|
+
rust: { name: 'Rust', hex: '#9c3d1c' },
|
|
49
|
+
sage: { name: 'Sage', hex: '#8a9a7b' },
|
|
50
|
+
chalk: { name: 'Chalk', hex: '#ece7de' },
|
|
51
|
+
clay: { name: 'Clay', hex: '#b07a5a' },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const formaStore: EditorialStore = {
|
|
55
|
+
name: 'Forma',
|
|
56
|
+
tagline: 'Clothing and objects made slowly, in small runs, by workshops we know by name.',
|
|
57
|
+
announcement: 'Complimentary shipping over €150 · Free returns within 30 days',
|
|
58
|
+
currency: 'EUR',
|
|
59
|
+
locale: 'en-GB',
|
|
60
|
+
cartKey: 'forma-bag',
|
|
61
|
+
freeShippingThreshold: 150,
|
|
62
|
+
shippingFlat: 8,
|
|
63
|
+
taxRate: 0,
|
|
64
|
+
promoCodes: { FORMA10: 10 },
|
|
65
|
+
shippingRates: [
|
|
66
|
+
{ id: 'standard', label: 'Standard', eta: '2–4 business days', price: 8 },
|
|
67
|
+
{ id: 'express', label: 'Express', eta: 'Next business day', price: 18 },
|
|
68
|
+
{ id: 'studio', label: 'Collect at the studio', eta: 'Madrid · ready tomorrow', price: 0 },
|
|
69
|
+
],
|
|
70
|
+
categories: [
|
|
71
|
+
{ id: 'clothing', name: 'Clothing', description: 'Overshirts, knitwear and outerwear in natural fibres.', icon: 'jacket', hue: 22 },
|
|
72
|
+
{ id: 'shoes', name: 'Shoes', description: 'Leather and suede, resoleable.', icon: 'sneaker', hue: 28 },
|
|
73
|
+
{ id: 'bags', name: 'Bags', description: 'Canvas and leather that soften with age.', icon: 'bag', hue: 34 },
|
|
74
|
+
{ id: 'objects', name: 'Objects', description: 'Stoneware, lighting and living things.', icon: 'mug', hue: 14 },
|
|
75
|
+
{ id: 'accessories', name: 'Accessories', description: 'Eyewear and watches, quietly made.', icon: 'sunglasses', hue: 200 },
|
|
76
|
+
],
|
|
77
|
+
products: [
|
|
78
|
+
{ slug: 'linen-overshirt', name: 'Linen Overshirt', price: 145, category: 'clothing', badge: 'New', colors: [C.oat, C.ink, C.rust], sizes: ['XS', 'S', 'M', 'L', 'XL'], icon: 'shirt', hue: 30, rating: 4.8, reviews: 96, stock: 4,
|
|
79
|
+
description: 'A relaxed overshirt in heavyweight Portuguese linen, garment-washed for softness. Corozo buttons, two patch pockets, a boxy cut that layers over knitwear.' },
|
|
80
|
+
{ slug: 'merino-crew', name: 'Merino Crew Knit', price: 160, category: 'clothing', colors: [C.sage, C.oat, C.ink], sizes: ['S', 'M', 'L', 'XL'], icon: 'jacket', hue: 95, rating: 4.9, reviews: 142,
|
|
81
|
+
description: 'Extra-fine Italian merino knitted in a dense, flat gauge. Breathable, itch-free and made to be worn next to the skin all winter.' },
|
|
82
|
+
{ slug: 'wool-overcoat', name: 'Wool Overcoat', price: 320, compareAt: 380, category: 'clothing', badge: 'Archive price', colors: [C.rust, C.ink], sizes: ['S', 'M', 'L'], icon: 'jacket', hue: 18, rating: 4.7, reviews: 54, stock: 2,
|
|
83
|
+
description: 'A single-breasted coat in boiled wool from a mill in Biella. Unlined, light and warm, with a soft shoulder.' },
|
|
84
|
+
{ slug: 'organic-tee', name: 'Organic Heavy Tee', price: 55, category: 'clothing', colors: [C.chalk, C.ink, C.sage, C.clay], sizes: ['XS', 'S', 'M', 'L', 'XL'], icon: 'shirt', hue: 40, rating: 4.6, reviews: 310,
|
|
85
|
+
description: 'A 240 gsm organic cotton tee with a structured collar that keeps its shape wash after wash.' },
|
|
86
|
+
{ slug: 'dune-loafer', name: 'Dune Suede Loafer', price: 210, category: 'shoes', colors: [C.clay, C.ink], sizes: ['39', '40', '41', '42', '43', '44'], icon: 'sneaker', hue: 28, rating: 4.7, reviews: 77, stock: 3,
|
|
87
|
+
description: 'Hand-lasted suede loafers on a Blake-stitched leather sole. Resoleable for years of wear.' },
|
|
88
|
+
{ slug: 'canvas-runner', name: 'Canvas Runner', price: 135, category: 'shoes', badge: 'New', colors: [C.chalk, C.sage], sizes: ['38', '39', '40', '41', '42', '43'], icon: 'sneaker', hue: 90, rating: 4.5, reviews: 41,
|
|
89
|
+
description: 'Organic canvas upper, natural rubber sole and recycled laces. Light enough for everyday.' },
|
|
90
|
+
{ slug: 'field-tote', name: 'Field Tote', price: 120, category: 'bags', colors: [C.rust, C.oat, C.ink], icon: 'bag', hue: 18, rating: 4.9, reviews: 203,
|
|
91
|
+
description: 'Waxed cotton canvas with vegetable-tanned leather handles. Holds a laptop, a lunch and a book.' },
|
|
92
|
+
{ slug: 'day-rucksack', name: 'Day Rucksack', price: 185, category: 'bags', colors: [C.ink, C.sage], icon: 'backpack', hue: 150, rating: 4.6, reviews: 66,
|
|
93
|
+
description: 'A roll-top rucksack in recycled nylon with a padded sleeve and a quiet, magnetic closure.' },
|
|
94
|
+
{ slug: 'kiln-mug', name: 'Kiln Mug', price: 32, category: 'objects', colors: [C.clay, C.chalk], icon: 'mug', hue: 14, rating: 4.9, reviews: 388,
|
|
95
|
+
description: 'Wheel-thrown stoneware from a family workshop in La Bisbal. Each glaze pools a little differently.' },
|
|
96
|
+
{ slug: 'paper-lamp', name: 'Paper Table Lamp', price: 140, category: 'objects', badge: 'New', colors: [C.chalk], icon: 'lamp', hue: 40, rating: 4.7, reviews: 29,
|
|
97
|
+
description: 'Washi paper on an oak base. Throws a soft, warm light that makes small rooms feel bigger.' },
|
|
98
|
+
{ slug: 'olive-planter', name: 'Olive Planter', price: 58, category: 'objects', colors: [C.sage, C.chalk], icon: 'plant', hue: 110, rating: 4.4, reviews: 18, inStock: false,
|
|
99
|
+
description: 'A matte terracotta planter with a saucer, sized for a young olive tree.' },
|
|
100
|
+
{ slug: 'acetate-frames', name: 'Acetate Frames', price: 175, category: 'accessories', colors: [C.ink, C.clay], icon: 'sunglasses', hue: 200, rating: 4.8, reviews: 61,
|
|
101
|
+
description: 'Bio-acetate frames with polarised lenses, made in Cadore. Comes with a linen case.' },
|
|
102
|
+
],
|
|
103
|
+
newIn: ['linen-overshirt', 'canvas-runner', 'paper-lamp', 'merino-crew', 'field-tote', 'acetate-frames'],
|
|
104
|
+
look: [
|
|
105
|
+
{ slug: 'linen-overshirt', x: 40, y: 32 },
|
|
106
|
+
{ slug: 'field-tote', x: 68, y: 60 },
|
|
107
|
+
{ slug: 'kiln-mug', x: 26, y: 74 },
|
|
108
|
+
],
|
|
109
|
+
reviews: [
|
|
110
|
+
{ quote: 'The overshirt has been washed forty times and it only gets softer. It is the first thing I reach for.', name: 'Inés M.', place: 'Valencia', product: 'Linen Overshirt', rating: 5 },
|
|
111
|
+
{ quote: 'Packaging without plastic, a handwritten note, and a mug that feels made for my hands.', name: 'Jonas P.', place: 'Hamburg', product: 'Kiln Mug', rating: 5 },
|
|
112
|
+
{ quote: 'They repaired the strap of my tote for free, two years after I bought it. That is rare.', name: 'Amira S.', place: 'Lyon', product: 'Field Tote', rating: 5 },
|
|
113
|
+
],
|
|
114
|
+
values: [
|
|
115
|
+
{ title: 'Honest materials', text: 'Linen, wool, organic cotton and stoneware — traceable to the mill and the kiln.' },
|
|
116
|
+
{ title: 'Made in small runs', text: 'Each piece is produced in batches of a few hundred, never mass-manufactured.' },
|
|
117
|
+
{ title: 'Kept for years', text: 'Free repairs on everything we make, and a buy-back for pieces you have outgrown.' },
|
|
118
|
+
],
|
|
119
|
+
contact: { title: 'Visit the studio', lines: ['Calle del Pez 14, Madrid', 'Tue–Sat · 11:00–20:00', 'hello@forma.studio'] },
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const editorialProductHref = (routes: EditorialRoutes, slug: string) => `${routes.product}${slug}/`;
|
|
123
|
+
export const editorialCategoryHref = (routes: EditorialRoutes, id?: string) => (id ? `${routes.shop}?category=${encodeURIComponent(id)}` : routes.shop);
|
|
124
|
+
export const pickEditorial = (store: EditorialStore, slugs: string[]) =>
|
|
125
|
+
slugs.map((s) => store.products.find((p) => p.slug === s)).filter((p): p is Product => Boolean(p));
|