astro-component-kit 0.4.0 → 0.5.0

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.
Files changed (208) hide show
  1. package/README.md +139 -27
  2. package/bin/cli.js +67 -0
  3. package/bin/installer.js +179 -0
  4. package/package.json +35 -5
  5. package/src/components/index.ts +2 -0
  6. package/src/components/lib/Galaxy3D.astro +249 -137
  7. package/src/components/lib/app/AppSidebar.astro +249 -0
  8. package/src/components/lib/app/AppTopbar.astro +284 -0
  9. package/src/components/lib/app/ArrivalsBoard.astro +176 -0
  10. package/src/components/lib/app/AuthCard.astro +213 -0
  11. package/src/components/lib/app/DataTable.astro +346 -0
  12. package/src/components/lib/app/GuestProfileCard.astro +133 -0
  13. package/src/components/lib/app/HousekeepingBoard.astro +189 -0
  14. package/src/components/lib/app/PageHeader.astro +157 -0
  15. package/src/components/lib/app/PlanUsageCard.astro +112 -0
  16. package/src/components/lib/app/RoomRack.astro +207 -0
  17. package/src/components/lib/app/SettingsSection.astro +96 -0
  18. package/src/components/lib/app/SwitchRow.astro +71 -0
  19. package/src/components/lib/app/TeamMembers.astro +190 -0
  20. package/src/components/lib/app/app-icons.ts +52 -0
  21. package/src/components/lib/app/index.ts +19 -0
  22. package/src/components/lib/charts/AreaChart.astro +4 -4
  23. package/src/components/lib/charts/BarChart.astro +3 -3
  24. package/src/components/lib/charts/ChartFrame.astro +10 -10
  25. package/src/components/lib/charts/DonutChart.astro +2 -2
  26. package/src/components/lib/charts/DumbbellChart.astro +1 -1
  27. package/src/components/lib/charts/ForecastLineChart.astro +1 -1
  28. package/src/components/lib/charts/GaugeChart.astro +1 -1
  29. package/src/components/lib/charts/HeatmapChart.astro +1 -1
  30. package/src/components/lib/charts/HorizontalBarChart.astro +2 -2
  31. package/src/components/lib/charts/KpiStatTile.astro +4 -4
  32. package/src/components/lib/charts/LinearProgressGauge.astro +4 -4
  33. package/src/components/lib/charts/MetricDeltaTable.astro +3 -3
  34. package/src/components/lib/charts/RadarMetricChart.astro +4 -4
  35. package/src/components/lib/charts/RadialBarChart.astro +1 -1
  36. package/src/components/lib/charts/RetentionCohortTable.astro +2 -2
  37. package/src/components/lib/charts/TreemapChart.astro +1 -1
  38. package/src/components/lib/ecommerce/CartDrawer.astro +483 -0
  39. package/src/components/lib/ecommerce/CartPageList.astro +290 -0
  40. package/src/components/lib/ecommerce/CartToast.astro +276 -0
  41. package/src/components/lib/ecommerce/CategoryTiles.astro +143 -0
  42. package/src/components/lib/ecommerce/CheckoutForm.astro +423 -0
  43. package/src/components/lib/ecommerce/CountdownBanner.astro +230 -0
  44. package/src/components/lib/ecommerce/FilterBar.astro +278 -0
  45. package/src/components/lib/ecommerce/FilterSidebar.astro +405 -0
  46. package/src/components/lib/ecommerce/LiveActivityToast.astro +213 -0
  47. package/src/components/lib/ecommerce/OrderSummary.astro +295 -0
  48. package/src/components/lib/ecommerce/ProductBuyBox.astro +247 -0
  49. package/src/components/lib/ecommerce/ProductCardMinimal.astro +229 -0
  50. package/src/components/lib/ecommerce/ProductGallery.astro +145 -0
  51. package/src/components/lib/ecommerce/ProductGrid.astro +118 -0
  52. package/src/components/lib/ecommerce/ProductMedia.astro +113 -0
  53. package/src/components/lib/ecommerce/ProductRating.astro +78 -0
  54. package/src/components/lib/ecommerce/ProductToolbar.astro +93 -0
  55. package/src/components/lib/ecommerce/PromoBar.astro +145 -0
  56. package/src/components/lib/ecommerce/StockIndicator.astro +127 -0
  57. package/src/components/lib/ecommerce/StoreHeader.astro +446 -0
  58. package/src/components/lib/ecommerce/StoreHero.astro +260 -0
  59. package/src/components/lib/ecommerce/StoreProductCard.astro +307 -0
  60. package/src/components/lib/ecommerce/TrustBadges.astro +101 -0
  61. package/src/components/lib/ecommerce/VariantPicker.astro +145 -0
  62. package/src/components/lib/ecommerce/cart-store.ts +151 -0
  63. package/src/components/lib/ecommerce/catalog-filter.ts +62 -0
  64. package/src/components/lib/ecommerce/index.ts +34 -0
  65. package/src/components/lib/ecommerce/product-icons.ts +28 -0
  66. package/src/components/lib/ecommerce/types.ts +70 -0
  67. package/src/components/lib/forms/AnimatedInput.astro +4 -2
  68. package/src/components/lib/forms/CustomRadioGroup.astro +3 -1
  69. package/src/components/lib/index.ts +2 -0
  70. package/src/components/lib/layout/BentoGrid3.astro +6 -4
  71. package/src/components/lib/layout/CenterFixedBox.astro +2 -2
  72. package/src/components/lib/layout/CenteredContainer.astro +4 -2
  73. package/src/components/lib/layout/ClusterHorizontal.astro +2 -2
  74. package/src/components/lib/layout/FeatureHero.astro +4 -2
  75. package/src/components/lib/layout/FooterCenteredMinimal.astro +4 -2
  76. package/src/components/lib/layout/FullBleedSection.astro +2 -2
  77. package/src/components/lib/layout/GalleryGrid4.astro +2 -2
  78. package/src/components/lib/layout/HolyGrailGrid.astro +11 -9
  79. package/src/components/lib/layout/LandingSectionSplit.astro +5 -3
  80. package/src/components/lib/layout/MasonryGrid.astro +2 -2
  81. package/src/components/lib/layout/MinimalFooterGrid.astro +1 -1
  82. package/src/components/lib/layout/ScrollSnapSection.astro +1 -1
  83. package/src/components/lib/layout/SidebarLayoutWrapper.astro +4 -2
  84. package/src/components/lib/layout/SplitScreenLayout.astro +3 -3
  85. package/src/components/lib/layout/StepLayoutVertical.astro +1 -1
  86. package/src/components/lib/layout/StickySidebarWrapper.astro +4 -2
  87. package/src/components/lib/sections/AgencyContactBrief.astro +194 -0
  88. package/src/components/lib/sections/AgencyFooterCta.astro +94 -0
  89. package/src/components/lib/sections/AgencyHeader.astro +161 -0
  90. package/src/components/lib/sections/AgencyHeroKinetic.astro +172 -0
  91. package/src/components/lib/sections/AgencyServicesList.astro +113 -0
  92. package/src/components/lib/sections/AgencyTeamGrid.astro +111 -0
  93. package/src/components/lib/sections/AgencyWorkGrid.astro +147 -0
  94. package/src/components/lib/sections/BlogGridSection.astro +9 -7
  95. package/src/components/lib/sections/ButtonsShowcase.astro +20 -18
  96. package/src/components/lib/sections/CardsShowcase.astro +28 -26
  97. package/src/components/lib/sections/CareersOpenings.astro +10 -8
  98. package/src/components/lib/sections/ChangelogSection.astro +20 -7
  99. package/src/components/lib/sections/ContactGlassForm.astro +10 -8
  100. package/src/components/lib/sections/ContactSplitMap.astro +11 -9
  101. package/src/components/lib/sections/CtaAppDownload.astro +13 -11
  102. package/src/components/lib/sections/CtaGlowBanner.astro +12 -10
  103. package/src/components/lib/sections/CtaNewsletterSection.astro +13 -11
  104. package/src/components/lib/sections/CtaSplitImage.astro +13 -11
  105. package/src/components/lib/sections/DataShowcase.astro +23 -21
  106. package/src/components/lib/sections/FaqAccordionSection.astro +8 -8
  107. package/src/components/lib/sections/FaqTwoColumn.astro +9 -7
  108. package/src/components/lib/sections/FeatureAlternatingRows.astro +10 -8
  109. package/src/components/lib/sections/FeatureBentoGrid.astro +13 -11
  110. package/src/components/lib/sections/FeatureIconGrid.astro +6 -6
  111. package/src/components/lib/sections/FeatureTabsShowcase.astro +13 -11
  112. package/src/components/lib/sections/FeatureTimelineFlow.astro +8 -6
  113. package/src/components/lib/sections/FeedbackShowcase.astro +15 -13
  114. package/src/components/lib/sections/FooterMegaSection.astro +13 -11
  115. package/src/components/lib/sections/FormsShowcase.astro +22 -20
  116. package/src/components/lib/sections/HeroDashboardPreview.astro +11 -9
  117. package/src/components/lib/sections/HeroGlass.astro +22 -20
  118. package/src/components/lib/sections/HeroGradient.astro +15 -13
  119. package/src/components/lib/sections/HeroMinimalCentered.astro +10 -10
  120. package/src/components/lib/sections/HeroShowcaseSplit.astro +18 -16
  121. package/src/components/lib/sections/HeroSplit.astro +13 -11
  122. package/src/components/lib/sections/HeroTerminal.astro +14 -12
  123. package/src/components/lib/sections/HeroWaitlist.astro +10 -8
  124. package/src/components/lib/sections/HowItWorksSteps.astro +12 -10
  125. package/src/components/lib/sections/IntegrationsGrid.astro +7 -7
  126. package/src/components/lib/sections/LayoutShowcase.astro +21 -19
  127. package/src/components/lib/sections/LogoCloudSection.astro +4 -4
  128. package/src/components/lib/sections/LogoWallGrid.astro +6 -4
  129. package/src/components/lib/sections/NavigationShowcase.astro +15 -13
  130. package/src/components/lib/sections/OverlaysShowcase.astro +21 -19
  131. package/src/components/lib/sections/PricingComparisonMatrix.astro +15 -13
  132. package/src/components/lib/sections/PricingSinglePlan.astro +13 -11
  133. package/src/components/lib/sections/PricingTierCards.astro +11 -9
  134. package/src/components/lib/sections/PricingToggleCards.astro +13 -13
  135. package/src/components/lib/sections/ShopCategoryHero.astro +103 -0
  136. package/src/components/lib/sections/ShopCollectionMosaic.astro +131 -0
  137. package/src/components/lib/sections/ShopFooterWordmark.astro +100 -0
  138. package/src/components/lib/sections/ShopHeaderEditorial.astro +396 -0
  139. package/src/components/lib/sections/ShopHeroEditorial.astro +151 -0
  140. package/src/components/lib/sections/ShopLookbook.astro +250 -0
  141. package/src/components/lib/sections/ShopNewsletterEditorial.astro +128 -0
  142. package/src/components/lib/sections/ShopProductRail.astro +150 -0
  143. package/src/components/lib/sections/ShopReviewsEditorial.astro +133 -0
  144. package/src/components/lib/sections/ShopValuesMarquee.astro +91 -0
  145. package/src/components/lib/sections/StatsCounterBand.astro +9 -7
  146. package/src/components/lib/sections/StatsDashboardSection.astro +4 -2
  147. package/src/components/lib/sections/StatsMetricShowcase.astro +9 -7
  148. package/src/components/lib/sections/TeamGridSection.astro +12 -10
  149. package/src/components/lib/sections/TestimonialCarousel.astro +10 -8
  150. package/src/components/lib/sections/TestimonialFeatured.astro +7 -5
  151. package/src/components/lib/sections/TestimonialGridSection.astro +7 -7
  152. package/src/components/lib/sections/TestimonialWallGlass.astro +11 -9
  153. package/src/components/lib/sections/TypographyShowcase.astro +10 -8
  154. package/src/components/lib/sections/index.ts +10 -0
  155. package/src/components/lib/templates/ecommerce/StoreCategoryPage.astro +160 -0
  156. package/src/components/lib/templates/ecommerce/StoreCheckoutPage.astro +98 -0
  157. package/src/components/lib/templates/ecommerce/StoreHomePage.astro +191 -0
  158. package/src/components/lib/templates/ecommerce/StoreLayout.astro +169 -0
  159. package/src/components/lib/templates/ecommerce/StoreProductPage.astro +165 -0
  160. package/src/components/lib/templates/ecommerce/data.ts +187 -0
  161. package/src/components/lib/templates/ecommerce/index.ts +14 -0
  162. package/src/components/lib/templates/ecommerce-editorial/EditorialBagPage.astro +52 -0
  163. package/src/components/lib/templates/ecommerce-editorial/EditorialCheckoutPage.astro +123 -0
  164. package/src/components/lib/templates/ecommerce-editorial/EditorialHomePage.astro +102 -0
  165. package/src/components/lib/templates/ecommerce-editorial/EditorialLayout.astro +216 -0
  166. package/src/components/lib/templates/ecommerce-editorial/EditorialProductPage.astro +205 -0
  167. package/src/components/lib/templates/ecommerce-editorial/EditorialShopPage.astro +112 -0
  168. package/src/components/lib/templates/ecommerce-editorial/data.ts +125 -0
  169. package/src/components/lib/templates/ecommerce-editorial/index.ts +16 -0
  170. package/src/components/lib/templates/saas-hotel/AppLayout.astro +83 -0
  171. package/src/components/lib/templates/saas-hotel/DashboardPage.astro +66 -0
  172. package/src/components/lib/templates/saas-hotel/GuestsPage.astro +65 -0
  173. package/src/components/lib/templates/saas-hotel/LandingPage.astro +213 -0
  174. package/src/components/lib/templates/saas-hotel/ReportsPage.astro +63 -0
  175. package/src/components/lib/templates/saas-hotel/ReservationsPage.astro +49 -0
  176. package/src/components/lib/templates/saas-hotel/RoomsPage.astro +48 -0
  177. package/src/components/lib/templates/saas-hotel/SettingsPage.astro +115 -0
  178. package/src/components/lib/templates/saas-hotel/SignInPage.astro +50 -0
  179. package/src/components/lib/templates/saas-hotel/data.ts +229 -0
  180. package/src/components/lib/templates/saas-hotel/index.ts +19 -0
  181. package/src/components/lib/templates/saas-hotel/lobby-theme.css +90 -0
  182. package/src/data/registry/app.ts +93 -0
  183. package/src/data/registry/buttons.ts +3279 -0
  184. package/src/data/registry/cards.ts +1753 -0
  185. package/src/data/registry/charts-svg.ts +557 -0
  186. package/src/data/registry/charts.ts +500 -0
  187. package/src/data/registry/data.ts +986 -0
  188. package/src/data/registry/ecommerce.ts +269 -0
  189. package/src/data/registry/feedback.ts +1824 -0
  190. package/src/data/registry/forms.ts +2513 -0
  191. package/src/data/registry/index.ts +64 -0
  192. package/src/data/registry/layout.ts +206 -0
  193. package/src/data/registry/navigation.ts +936 -0
  194. package/src/data/registry/overlays.ts +1133 -0
  195. package/src/data/registry/sections-extra.ts +397 -0
  196. package/src/data/registry/sections.ts +361 -0
  197. package/src/data/registry/sources.ts +41 -0
  198. package/src/data/registry/templates.ts +414 -0
  199. package/src/data/registry/types.ts +30 -0
  200. package/src/data/registry/typography.ts +777 -0
  201. package/src/mcp/http-server.ts +324 -0
  202. package/src/mcp/index.ts +67 -0
  203. package/src/mcp/installer.ts +178 -0
  204. package/src/mcp/registry-adapter.ts +278 -0
  205. package/src/mcp/stdio-server.ts +11 -0
  206. package/src/mcp/tools.ts +247 -0
  207. package/src/mcp/types.ts +53 -0
  208. package/src/styles/global.css +95 -0
@@ -0,0 +1,151 @@
1
+ ---
2
+ /**
3
+ * ShopHeroEditorial — Magazine-style shop hero: oversized serif headline with an italic accent
4
+ * word, issue/season meta, two CTAs, a tall feature image with caption and a running ticker.
5
+ *
6
+ * @param {string} kicker - Small label above the title. Default 'Autumn / Winter 26'.
7
+ * @param {string} title - Headline (wrap a word in *asterisks* to italicise it). Default 'Quiet *objects* for everyday rituals'.
8
+ * @param {string} text - Supporting paragraph.
9
+ * @param {{label: string, href: string}} primary - Main CTA.
10
+ * @param {{label: string, href: string}} secondary - Secondary CTA (optional).
11
+ * @param {{icon?: string, hue?: number, image?: string, alt?: string, caption?: string, href?: string}} media - Feature visual.
12
+ * @param {string[]} ticker - Words for the running ticker (empty hides it).
13
+ * @param {string} issue - Small index label, e.g. "N° 04".
14
+ * @param {string} variant - Layout (split|stacked). Default 'split'.
15
+ * @param {string} size - Vertical rhythm (sm|md|lg). Default 'md'.
16
+ * @param {string} className - Extra class.
17
+ *
18
+ * Accessibility: one h1; ticker is decorative (aria-hidden) and stops with reduced motion.
19
+ */
20
+ import ProductMedia from '../ecommerce/ProductMedia.astro';
21
+
22
+ interface Cta { label: string; href: string }
23
+ interface Props {
24
+ kicker?: string;
25
+ title?: string;
26
+ text?: string;
27
+ primary?: Cta;
28
+ secondary?: Cta;
29
+ media?: { icon?: string; hue?: number; image?: string; alt?: string; caption?: string; href?: string };
30
+ ticker?: string[];
31
+ issue?: string;
32
+ variant?: 'split' | 'stacked';
33
+ size?: 'sm' | 'md' | 'lg';
34
+ className?: string;
35
+ }
36
+
37
+ const {
38
+ kicker = 'Autumn / Winter 26',
39
+ title = 'Quiet *objects* for everyday rituals',
40
+ text = 'Clothing and homeware made slowly, in small runs, by workshops we know by name. Designed to be used for years — and to look better for it.',
41
+ primary = { label: 'Shop the collection', href: '#' },
42
+ secondary = { label: 'Read the journal', href: '#' },
43
+ media = { icon: 'jacket', hue: 22, caption: 'Wool overcoat, in Rust — €320', href: '#' },
44
+ ticker = ['Made in small runs', 'Natural fibres', 'Repair for life', 'Carbon-neutral shipping', 'Free returns'],
45
+ issue = 'N° 04',
46
+ variant = 'split',
47
+ size = 'md',
48
+ className = '',
49
+ } = Astro.props;
50
+
51
+ const parts = title.split(/\*(.+?)\*/g);
52
+ const MediaTag = media.href ? 'a' : 'div';
53
+ ---
54
+
55
+ <section class:list={['sh-ed', `sh-ed--${variant}`, `sh-ed--${size}`, className]}>
56
+ <div class="sh-ed__inner">
57
+ <div class="sh-ed__copy">
58
+ <p class="sh-ed__meta"><span>{kicker}</span>{issue && <span>{issue}</span>}</p>
59
+ <h1 class="sh-ed__title">{parts.map((p, i) => (i % 2 ? <em>{p}</em> : p))}</h1>
60
+ <div class="sh-ed__foot">
61
+ {text && <p class="sh-ed__text">{text}</p>}
62
+ <div class="sh-ed__ctas">
63
+ {primary && <a class="sh-ed__btn sh-ed__btn--solid" href={primary.href}>{primary.label}<span aria-hidden="true">→</span></a>}
64
+ {secondary && <a class="sh-ed__btn" href={secondary.href}>{secondary.label}</a>}
65
+ </div>
66
+ </div>
67
+ </div>
68
+ <MediaTag class="sh-ed__media" href={media.href}>
69
+ <ProductMedia image={media.image} alt={media.alt ?? ''} icon={media.icon} hue={media.hue} ratio="4 / 5" variant="soft" size="lg" />
70
+ {media.caption && <span class="sh-ed__caption">{media.caption}</span>}
71
+ </MediaTag>
72
+ </div>
73
+ {ticker.length > 0 && (
74
+ <div class="sh-ed__ticker" aria-hidden="true">
75
+ <div class="sh-ed__track">
76
+ {[0, 1].map(() => ticker.map((w) => <span>{w}<i>✦</i></span>))}
77
+ </div>
78
+ </div>
79
+ )}
80
+ </section>
81
+
82
+ <style>
83
+ .sh-ed { container-type: inline-size; --py: clamp(2.5rem, 6vw, 5rem); color: var(--c-text-1, #fff); overflow: hidden; }
84
+ .sh-ed--sm { --py: clamp(1.5rem, 4vw, 3rem); }
85
+ .sh-ed--lg { --py: clamp(3.5rem, 8vw, 7rem); }
86
+ .sh-ed__inner {
87
+ display: grid;
88
+ grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.75fr);
89
+ gap: clamp(1.5rem, 4vw, 4rem);
90
+ align-items: end;
91
+ max-width: 1440px;
92
+ margin: 0 auto;
93
+ padding: var(--py) clamp(1rem, 3vw, 2.5rem);
94
+ }
95
+ .sh-ed--stacked .sh-ed__inner { grid-template-columns: 1fr; }
96
+ .sh-ed__meta { display: flex; justify-content: space-between; gap: var(--sp-4, 1rem); margin: 0 0 var(--sp-6, 1.5rem); padding-bottom: var(--sp-3, 0.75rem); border-bottom: 1px solid currentColor; font-size: 0.72rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; }
97
+ .sh-ed__title {
98
+ margin: 0;
99
+ font-family: var(--font-display, Georgia, 'Times New Roman', serif);
100
+ font-size: clamp(3rem, 8.5vw, 8.5rem);
101
+ font-weight: 400;
102
+ line-height: 0.92;
103
+ letter-spacing: -0.045em;
104
+ text-wrap: balance;
105
+ }
106
+ .sh-ed__title em { font-style: italic; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
107
+ .sh-ed__foot { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: var(--sp-6, 1.5rem); align-items: end; margin-top: var(--sp-8, 2rem); }
108
+ .sh-ed__text { margin: 0; max-width: 30rem; font-size: 1rem; line-height: 1.65; color: var(--c-text-2, #a3a3a3); }
109
+ .sh-ed__ctas { display: flex; flex-wrap: wrap; gap: var(--sp-2, 0.5rem); }
110
+ .sh-ed__btn {
111
+ display: inline-flex;
112
+ align-items: center;
113
+ gap: 0.6rem;
114
+ min-height: 52px;
115
+ padding: 0 1.4rem;
116
+ border: 1px solid currentColor;
117
+ border-radius: var(--r-sm, 2px);
118
+ color: inherit;
119
+ font-size: 0.85rem;
120
+ font-weight: 600;
121
+ letter-spacing: 0.06em;
122
+ text-transform: uppercase;
123
+ text-decoration: none;
124
+ transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease;
125
+ }
126
+ .sh-ed__btn:hover { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); }
127
+ .sh-ed__btn--solid { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); border-color: var(--c-text-1, #fff); }
128
+ .sh-ed__btn--solid:hover { background: var(--shop-accent, var(--c-primary-light, #d4a017)); border-color: var(--shop-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
129
+ .sh-ed__btn:focus-visible, .sh-ed__media:focus-visible { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 3px; }
130
+
131
+ .sh-ed__media { position: relative; display: block; color: inherit; text-decoration: none; border-radius: var(--r-sm, 2px); overflow: hidden; }
132
+ .sh-ed__media :global(.p-media) { transition: transform 1.2s cubic-bezier(0.16, 1, 0.3, 1); }
133
+ .sh-ed__media:hover :global(.p-media) { transform: scale(1.03); }
134
+ .sh-ed__caption { position: absolute; left: 0; right: 0; bottom: 0; padding: var(--sp-3, 0.75rem) var(--sp-4, 1rem); background: linear-gradient(transparent, rgb(var(--paper-rgb, 5 5 5) / 0.85)); font-size: 0.8rem; letter-spacing: 0.02em; }
135
+
136
+ .sh-ed__ticker { border-block: 1px solid var(--c-border, rgba(255, 255, 255, 0.1)); overflow: hidden; }
137
+ .sh-ed__track { display: flex; width: max-content; animation: sh-ed-run 40s linear infinite; }
138
+ .sh-ed__track span { display: inline-flex; align-items: center; gap: 1.5rem; padding: 0.9rem 0 0.9rem 1.5rem; font-family: var(--font-display, Georgia, serif); font-size: 1.15rem; font-style: italic; white-space: nowrap; }
139
+ .sh-ed__track i { font-style: normal; font-size: 0.7rem; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
140
+ @keyframes sh-ed-run { to { transform: translateX(-50%); } }
141
+
142
+ @container (max-width: 900px) {
143
+ .sh-ed__inner { grid-template-columns: 1fr; }
144
+ .sh-ed__foot { grid-template-columns: 1fr; }
145
+ .sh-ed__media { max-width: 520px; }
146
+ }
147
+ @media (prefers-reduced-motion: reduce) {
148
+ .sh-ed__track { animation: none; }
149
+ .sh-ed__media :global(.p-media) { transition: none; }
150
+ }
151
+ </style>
@@ -0,0 +1,250 @@
1
+ ---
2
+ /**
3
+ * ShopLookbook — "Shop the look": a large editorial image with numbered hotspots. Each hotspot
4
+ * opens a small product card (name, price, link and add to bag) and highlights it in the side list.
5
+ *
6
+ * @param {string} eyebrow - Small label. Default 'Lookbook'.
7
+ * @param {string} title - Heading (use *asterisks* for italics). Default 'Shop the *look*'.
8
+ * @param {string} text - Intro copy.
9
+ * @param {{icon?: string, hue?: number, image?: string, alt?: string}} media - Scene visual.
10
+ * @param {Array<{x: number, y: number, product: Product, href?: string}>} spots - Hotspots; x/y in % of the image.
11
+ * @param {string} currency - ISO currency. Default 'EUR'.
12
+ * @param {string} locale - Number format locale. Default 'en-GB'.
13
+ * @param {string} cartKey - Cart storage key. Default 'ack-cart'.
14
+ * @param {string} variant - Layout (image-left|image-right). Default 'image-left'.
15
+ * @param {string} id - Base id. Default 'lookbook'.
16
+ * @param {string} className - Extra class.
17
+ *
18
+ * Accessibility: hotspots are buttons with full labels and aria-expanded; the same products are
19
+ * listed as plain links next to the image, so the look is usable without the hotspots.
20
+ */
21
+ import ProductMedia from '../ecommerce/ProductMedia.astro';
22
+ import { formatMoney } from '../ecommerce/cart-store';
23
+ import type { Product } from '../ecommerce/types';
24
+
25
+ interface Spot { x: number; y: number; product: Product; href?: string }
26
+ interface Props {
27
+ eyebrow?: string;
28
+ title?: string;
29
+ text?: string;
30
+ media?: { icon?: string; hue?: number; image?: string; alt?: string };
31
+ spots?: Spot[];
32
+ currency?: string;
33
+ locale?: string;
34
+ cartKey?: string;
35
+ variant?: 'image-left' | 'image-right';
36
+ id?: string;
37
+ className?: string;
38
+ }
39
+
40
+ const {
41
+ eyebrow = 'Lookbook',
42
+ title = 'Shop the *look*',
43
+ text = 'A Sunday uniform: a soft overshirt, a tote that goes everywhere and the mug that starts the day.',
44
+ media = { icon: 'jacket', hue: 22 },
45
+ spots = [
46
+ { x: 38, y: 34, product: { slug: 'linen-overshirt', name: 'Linen Overshirt', price: 145, category: 'clothing', icon: 'shirt', hue: 30 } },
47
+ { x: 66, y: 62, product: { slug: 'field-tote', name: 'Field Tote', price: 120, category: 'bags', icon: 'bag', hue: 18 } },
48
+ { x: 24, y: 76, product: { slug: 'kiln-mug', name: 'Kiln Mug', price: 32, category: 'objects', icon: 'mug', hue: 14 } },
49
+ ],
50
+ currency = 'EUR',
51
+ locale = 'en-GB',
52
+ cartKey = 'ack-cart',
53
+ variant = 'image-left',
54
+ id = 'lookbook',
55
+ className = '',
56
+ } = Astro.props;
57
+
58
+ const money = { currency, locale };
59
+ const parts = title.split(/\*(.+?)\*/g);
60
+ const total = spots.reduce((n, s) => n + s.product.price, 0);
61
+ ---
62
+
63
+ <div class="slb-cq">
64
+ <section class:list={['slb', `slb--${variant}`, className]} data-lookbook>
65
+ <div class="slb__stage">
66
+ <ProductMedia image={media.image} alt={media.alt ?? ''} icon={media.icon} hue={media.hue} ratio="4 / 5" variant="soft" size="lg" />
67
+ {spots.map((s, i) => (
68
+ <div class="slb__spot" style={`left: ${s.x}%; top: ${s.y}%`}>
69
+ <button
70
+ type="button"
71
+ class="slb__dot"
72
+ aria-expanded="false"
73
+ aria-controls={`${id}-pop-${i}`}
74
+ aria-label={`${i + 1}: ${s.product.name}, ${formatMoney(s.product.price, money)}`}
75
+ data-spot={i}
76
+ >{i + 1}</button>
77
+ <div class:list={['slb__pop', { 'slb__pop--left': s.x > 60 }]} id={`${id}-pop-${i}`} hidden data-pop>
78
+ <p class="slb__pop-name">{s.product.name}</p>
79
+ <p class="slb__pop-price">{formatMoney(s.product.price, money)}</p>
80
+ <div class="slb__pop-actions">
81
+ <a href={s.href ?? '#'}>View</a>
82
+ <button
83
+ type="button"
84
+ data-add-to-cart={JSON.stringify({ id: s.product.slug, name: s.product.name, price: s.product.price, icon: s.product.icon, hue: s.product.hue, variant: s.product.colors?.[0]?.name })}
85
+ data-cart-key={cartKey}
86
+ data-open-cart="false"
87
+ data-added-label="Added"
88
+ ><span data-label>Add to bag</span></button>
89
+ </div>
90
+ </div>
91
+ </div>
92
+ ))}
93
+ </div>
94
+
95
+ <div class="slb__copy">
96
+ {eyebrow && <p class="slb__eyebrow">{eyebrow}</p>}
97
+ <h2 class="slb__title">{parts.map((p, i) => (i % 2 ? <em>{p}</em> : p))}</h2>
98
+ {text && <p class="slb__text">{text}</p>}
99
+ <ol class="slb__list">
100
+ {spots.map((s, i) => (
101
+ <li class="slb__row" data-row={i}>
102
+ <span class="slb__row-num" aria-hidden="true">{String(i + 1).padStart(2, '0')}</span>
103
+ <a class="slb__row-name" href={s.href ?? '#'}>{s.product.name}</a>
104
+ <span class="slb__row-price">{formatMoney(s.product.price, money)}</span>
105
+ </li>
106
+ ))}
107
+ </ol>
108
+ <p class="slb__total"><span>The complete look</span><strong>{formatMoney(total, money)}</strong></p>
109
+ </div>
110
+ </section>
111
+ </div>
112
+
113
+ <script>
114
+ import { bindAddToCart } from '../ecommerce/cart-store';
115
+ bindAddToCart(document);
116
+
117
+ document.querySelectorAll<HTMLElement>('[data-lookbook]:not([data-bound])').forEach((lb) => {
118
+ lb.dataset.bound = '';
119
+ const dots = Array.from(lb.querySelectorAll<HTMLButtonElement>('[data-spot]'));
120
+ const rows = Array.from(lb.querySelectorAll<HTMLElement>('[data-row]'));
121
+ const close = () => {
122
+ dots.forEach((d) => {
123
+ d.setAttribute('aria-expanded', 'false');
124
+ const p = document.getElementById(d.getAttribute('aria-controls') || '');
125
+ if (p) p.hidden = true;
126
+ });
127
+ rows.forEach((r) => r.classList.remove('is-active'));
128
+ };
129
+ dots.forEach((d, i) => {
130
+ d.addEventListener('click', (e) => {
131
+ e.stopPropagation();
132
+ const open = d.getAttribute('aria-expanded') !== 'true';
133
+ close();
134
+ if (!open) return;
135
+ d.setAttribute('aria-expanded', 'true');
136
+ const p = document.getElementById(d.getAttribute('aria-controls') || '');
137
+ if (p) p.hidden = false;
138
+ rows[i]?.classList.add('is-active');
139
+ });
140
+ rows[i]?.addEventListener('mouseenter', () => d.classList.add('is-hint'));
141
+ rows[i]?.addEventListener('mouseleave', () => d.classList.remove('is-hint'));
142
+ });
143
+ document.addEventListener('click', (e) => { if (!lb.contains(e.target as Node)) close(); });
144
+ lb.addEventListener('keydown', (e) => {
145
+ if (e.key !== 'Escape') return;
146
+ const open = dots.find((d) => d.getAttribute('aria-expanded') === 'true');
147
+ close();
148
+ open?.focus();
149
+ });
150
+ });
151
+ </script>
152
+
153
+ <style>
154
+ .slb-cq { container-type: inline-size; }
155
+ .slb {
156
+ display: grid;
157
+ grid-template-columns: minmax(0, 1.1fr) minmax(0, 0.9fr);
158
+ gap: clamp(1.5rem, 5vw, 5rem);
159
+ align-items: center;
160
+ color: var(--c-text-1, #fff);
161
+ }
162
+ .slb--image-right .slb__stage { order: 2; }
163
+ .slb__stage { position: relative; border-radius: var(--r-sm, 2px); }
164
+ .slb__stage > :global(.p-media) { border-radius: var(--r-sm, 2px); }
165
+ .slb__spot { position: absolute; z-index: 2; transform: translate(-50%, -50%); }
166
+ .slb__dot {
167
+ position: relative;
168
+ display: grid;
169
+ place-items: center;
170
+ width: 40px;
171
+ height: 40px;
172
+ border: 0;
173
+ border-radius: 50%;
174
+ background: var(--c-text-1, #fff);
175
+ color: var(--c-bg, #050505);
176
+ font: inherit;
177
+ font-size: 0.8rem;
178
+ font-weight: 700;
179
+ cursor: pointer;
180
+ box-shadow: 0 10px 30px -8px rgba(0, 0, 0, 0.6);
181
+ transition: transform 0.2s ease, background 0.2s ease, color 0.2s ease;
182
+ }
183
+ .slb__dot::before { content: ''; position: absolute; inset: -6px; border-radius: 50%; border: 1px solid var(--c-text-1, #fff); opacity: 0.6; animation: slb-ring 2.4s ease-out infinite; }
184
+ .slb__dot:hover, .slb__dot.is-hint, .slb__dot[aria-expanded='true'] { transform: scale(1.12); background: var(--shop-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
185
+ @keyframes slb-ring { 0% { transform: scale(0.8); opacity: 0.7; } 100% { transform: scale(1.5); opacity: 0; } }
186
+
187
+ .slb__pop {
188
+ position: absolute;
189
+ top: 50%;
190
+ left: calc(100% + 12px);
191
+ width: 220px;
192
+ padding: var(--sp-4, 1rem);
193
+ transform: translateY(-50%);
194
+ background: var(--c-bg-1, #0a0a0a);
195
+ border: 1px solid var(--c-border, rgba(255, 255, 255, 0.12));
196
+ box-shadow: 0 24px 50px -20px rgba(0, 0, 0, 0.6);
197
+ animation: slb-pop 0.2s ease-out;
198
+ }
199
+ .slb__pop--left { left: auto; right: calc(100% + 12px); }
200
+ .slb__pop[hidden] { display: none; }
201
+ @keyframes slb-pop { from { opacity: 0; transform: translateY(-46%); } }
202
+ .slb__pop-name { margin: 0; font-family: var(--font-display, Georgia, serif); font-size: 1.05rem; }
203
+ .slb__pop-price { margin: 2px 0 var(--sp-3, 0.75rem); font-size: 0.85rem; color: var(--c-text-2, #a3a3a3); }
204
+ .slb__pop-actions { display: flex; gap: var(--sp-2, 0.5rem); }
205
+ .slb__pop-actions a, .slb__pop-actions button {
206
+ flex: 1;
207
+ display: inline-grid;
208
+ place-items: center;
209
+ min-height: 40px;
210
+ border: 1px solid var(--c-text-1, #fff);
211
+ border-radius: var(--r-sm, 2px);
212
+ background: transparent;
213
+ color: var(--c-text-1, #fff);
214
+ font: inherit;
215
+ font-size: 0.75rem;
216
+ font-weight: 600;
217
+ letter-spacing: 0.06em;
218
+ text-transform: uppercase;
219
+ text-decoration: none;
220
+ cursor: pointer;
221
+ }
222
+ .slb__pop-actions button { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); }
223
+ .slb__pop-actions button.is-added { background: var(--shop-accent, var(--c-primary-light, #d4a017)); border-color: transparent; color: var(--c-on-primary, #050505); }
224
+ .slb :is(a, button):focus-visible { outline: 2px solid var(--shop-accent, var(--c-primary-light, #d4a017)); outline-offset: 3px; }
225
+
226
+ .slb__eyebrow { margin: 0 0 var(--sp-3, 0.75rem); font-size: 0.72rem; font-weight: 600; letter-spacing: 0.16em; text-transform: uppercase; color: var(--c-text-2, #a3a3a3); }
227
+ .slb__title { margin: 0; font-family: var(--font-display, Georgia, 'Times New Roman', serif); font-size: clamp(2.4rem, 5.5vw, 4.5rem); font-weight: 400; letter-spacing: -0.04em; line-height: 0.95; }
228
+ .slb__title em { font-style: italic; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
229
+ .slb__text { max-width: 28rem; margin: var(--sp-5, 1.25rem) 0 0; line-height: 1.65; color: var(--c-text-2, #a3a3a3); }
230
+ .slb__list { margin: var(--sp-8, 2rem) 0 0; padding: 0; list-style: none; border-top: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); }
231
+ .slb__row { display: grid; grid-template-columns: 40px 1fr auto; align-items: center; gap: var(--sp-3, 0.75rem); min-height: 56px; border-bottom: 1px solid var(--c-border, rgba(255, 255, 255, 0.12)); transition: background 0.2s ease, padding 0.2s ease; }
232
+ .slb__row.is-active { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); padding-inline: var(--sp-2, 0.5rem); }
233
+ .slb__row-num { font-size: 0.75rem; color: var(--c-text-2, #a3a3a3); font-variant-numeric: tabular-nums; }
234
+ .slb__row-name { font-family: var(--font-display, Georgia, serif); font-size: 1.15rem; color: inherit; text-decoration: none; }
235
+ .slb__row-name:hover { color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
236
+ .slb__row-price { font-variant-numeric: tabular-nums; }
237
+ .slb__total { display: flex; justify-content: space-between; margin: var(--sp-4, 1rem) 0 0; font-size: 0.85rem; color: var(--c-text-2, #a3a3a3); }
238
+ .slb__total strong { font-size: 1.1rem; color: var(--c-text-1, #fff); }
239
+
240
+ @container (max-width: 860px) {
241
+ .slb { grid-template-columns: 1fr; }
242
+ .slb--image-right .slb__stage { order: 0; }
243
+ .slb__pop { left: 50%; top: calc(100% + 10px); transform: translateX(-50%); width: 200px; }
244
+ .slb__pop--left { right: auto; left: 50%; }
245
+ @keyframes slb-pop { from { opacity: 0; } }
246
+ }
247
+ @media (prefers-reduced-motion: reduce) {
248
+ .slb__dot::before, .slb__pop { animation: none; }
249
+ }
250
+ </style>
@@ -0,0 +1,128 @@
1
+ ---
2
+ /**
3
+ * ShopNewsletterEditorial — Oversized editorial sign-up: large serif statement on one side,
4
+ * underline-style email field, consent note and inline success/error messages on the other.
5
+ *
6
+ * @param {string} title - Statement (use *asterisks* for italics). Default 'Letters from the *studio*'.
7
+ * @param {string} text - Supporting copy.
8
+ * @param {string} action - Form action. With '#' the form validates and confirms locally. Default '#'.
9
+ * @param {string} buttonLabel - Submit label. Default 'Subscribe'.
10
+ * @param {string} consent - Small print under the field.
11
+ * @param {string} successText - Message after a valid submit.
12
+ * @param {string} variant - Style (plain|inverted|accent). Default 'plain'.
13
+ * @param {string} size - Type scale (sm|md|lg). Default 'md'.
14
+ * @param {string} id - Base id. Default 'shop-news'.
15
+ * @param {string} className - Extra class.
16
+ *
17
+ * Accessibility: labelled email input with autocomplete, aria-invalid + described error,
18
+ * messages in a polite live region.
19
+ */
20
+ interface Props {
21
+ title?: string;
22
+ text?: string;
23
+ action?: string;
24
+ buttonLabel?: string;
25
+ consent?: string;
26
+ successText?: string;
27
+ variant?: 'plain' | 'inverted' | 'accent';
28
+ size?: 'sm' | 'md' | 'lg';
29
+ id?: string;
30
+ className?: string;
31
+ }
32
+
33
+ const {
34
+ title = 'Letters from the *studio*',
35
+ text = 'One email a month: new pieces before anyone else, the people who make them, and 10% off your first order.',
36
+ action = '#',
37
+ buttonLabel = 'Subscribe',
38
+ consent = 'By subscribing you accept our privacy policy. Unsubscribe in one click.',
39
+ successText = 'Thank you — your first letter is on its way.',
40
+ variant = 'plain',
41
+ size = 'md',
42
+ id = 'shop-news',
43
+ className = '',
44
+ } = Astro.props;
45
+
46
+ const parts = title.split(/\*(.+?)\*/g);
47
+ ---
48
+
49
+ <div class="sne-cq">
50
+ <section class:list={['sne', `sne--${variant}`, `sne--${size}`, className]} aria-labelledby={`${id}-title`}>
51
+ <h2 class="sne__title" id={`${id}-title`}>{parts.map((p, i) => (i % 2 ? <em>{p}</em> : p))}</h2>
52
+ <div class="sne__side">
53
+ {text && <p class="sne__text">{text}</p>}
54
+ <form class="sne__form" action={action} method="post" novalidate data-news data-success={successText}>
55
+ <label class="sne__label" for={`${id}-email`}>Email address</label>
56
+ <div class="sne__field">
57
+ <input id={`${id}-email`} name="email" type="email" required autocomplete="email" placeholder="you@example.com" aria-describedby={`${id}-msg`} />
58
+ <button type="submit">{buttonLabel} <span aria-hidden="true">→</span></button>
59
+ </div>
60
+ <p class="sne__msg" id={`${id}-msg`} aria-live="polite" data-msg></p>
61
+ {consent && <p class="sne__consent">{consent}</p>}
62
+ </form>
63
+ </div>
64
+ </section>
65
+ </div>
66
+
67
+ <script>
68
+ document.querySelectorAll<HTMLFormElement>('form[data-news]:not([data-bound])').forEach((form) => {
69
+ form.dataset.bound = '';
70
+ const input = form.querySelector<HTMLInputElement>('input[type="email"]')!;
71
+ const msg = form.querySelector<HTMLElement>('[data-msg]')!;
72
+ form.addEventListener('submit', (e) => {
73
+ const local = form.getAttribute('action') === '#';
74
+ if (!input.checkValidity()) {
75
+ e.preventDefault();
76
+ input.setAttribute('aria-invalid', 'true');
77
+ msg.dataset.state = 'error';
78
+ msg.textContent = 'Please enter a valid email address.';
79
+ input.focus();
80
+ return;
81
+ }
82
+ input.removeAttribute('aria-invalid');
83
+ if (local) {
84
+ e.preventDefault();
85
+ msg.dataset.state = 'ok';
86
+ msg.textContent = form.dataset.success || 'Subscribed.';
87
+ form.reset();
88
+ }
89
+ });
90
+ });
91
+ </script>
92
+
93
+ <style>
94
+ .sne-cq { container-type: inline-size; }
95
+ .sne {
96
+ display: grid;
97
+ grid-template-columns: minmax(0, 1.2fr) minmax(0, 0.8fr);
98
+ gap: clamp(1.5rem, 5vw, 5rem);
99
+ align-items: end;
100
+ padding: clamp(2.5rem, 6vw, 5rem) clamp(1rem, 3vw, 2.5rem);
101
+ color: var(--c-text-1, #fff);
102
+ }
103
+ .sne--inverted { background: var(--c-text-1, #fff); color: var(--c-bg, #050505); }
104
+ .sne--accent { background: var(--shop-accent, var(--c-primary-light, #d4a017)); color: var(--c-on-primary, #050505); }
105
+ .sne__title { margin: 0; font-family: var(--font-display, Georgia, 'Times New Roman', serif); font-size: clamp(2.6rem, 7vw, 6.5rem); font-weight: 400; letter-spacing: -0.045em; line-height: 0.92; text-wrap: balance; }
106
+ .sne--sm .sne__title { font-size: clamp(2rem, 5vw, 4rem); }
107
+ .sne--lg .sne__title { font-size: clamp(3rem, 9vw, 8.5rem); }
108
+ .sne__title em { font-style: italic; color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
109
+ .sne--accent .sne__title em { color: inherit; text-decoration: underline; text-decoration-thickness: 2px; text-underline-offset: 0.12em; }
110
+ .sne__text { margin: 0 0 var(--sp-6, 1.5rem); max-width: 26rem; line-height: 1.65; opacity: 0.8; }
111
+ .sne__label { display: block; margin-bottom: var(--sp-2, 0.5rem); font-size: 0.72rem; font-weight: 600; letter-spacing: 0.14em; text-transform: uppercase; opacity: 0.75; }
112
+ .sne__field { display: flex; align-items: stretch; border-bottom: 1.5px solid currentColor; }
113
+ .sne__field input { flex: 1; min-width: 0; min-height: 56px; padding: 0; border: 0; background: transparent; color: inherit; font: inherit; font-size: 1.15rem; outline: none; }
114
+ .sne__field input::placeholder { color: currentColor; opacity: 0.45; }
115
+ .sne__field input[aria-invalid='true'] { color: var(--c-danger, #f87171); }
116
+ .sne__field button { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0 0 0 var(--sp-4, 1rem); border: 0; background: none; color: inherit; font: inherit; font-size: 0.85rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; cursor: pointer; }
117
+ .sne__field button:hover { color: var(--shop-accent, var(--c-primary-light, #d4a017)); }
118
+ .sne--accent .sne__field button:hover { color: inherit; text-decoration: underline; }
119
+ .sne__field:focus-within { border-bottom-width: 2.5px; }
120
+ .sne__field button:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
121
+ .sne__msg { min-height: 1.4em; margin: var(--sp-2, 0.5rem) 0 0; font-size: 0.85rem; }
122
+ .sne__msg[data-state='error'] { color: var(--c-danger, #f87171); }
123
+ .sne__msg[data-state='ok'] { color: var(--c-success, #34d399); }
124
+ .sne--accent .sne__msg, .sne--inverted .sne__msg { color: inherit; font-weight: 600; }
125
+ .sne__consent { margin: var(--sp-2, 0.5rem) 0 0; font-size: 0.75rem; opacity: 0.6; }
126
+
127
+ @container (max-width: 860px) { .sne { grid-template-columns: 1fr; } }
128
+ </style>