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.
Files changed (180) hide show
  1. package/README.md +105 -27
  2. package/package.json +16 -3
  3. package/src/components/index.ts +2 -0
  4. package/src/components/lib/Galaxy3D.astro +249 -137
  5. package/src/components/lib/app/AppSidebar.astro +249 -0
  6. package/src/components/lib/app/AppTopbar.astro +284 -0
  7. package/src/components/lib/app/ArrivalsBoard.astro +176 -0
  8. package/src/components/lib/app/AuthCard.astro +213 -0
  9. package/src/components/lib/app/DataTable.astro +346 -0
  10. package/src/components/lib/app/GuestProfileCard.astro +133 -0
  11. package/src/components/lib/app/HousekeepingBoard.astro +189 -0
  12. package/src/components/lib/app/PageHeader.astro +157 -0
  13. package/src/components/lib/app/PlanUsageCard.astro +112 -0
  14. package/src/components/lib/app/RoomRack.astro +207 -0
  15. package/src/components/lib/app/SettingsSection.astro +96 -0
  16. package/src/components/lib/app/SwitchRow.astro +71 -0
  17. package/src/components/lib/app/TeamMembers.astro +190 -0
  18. package/src/components/lib/app/app-icons.ts +52 -0
  19. package/src/components/lib/app/index.ts +19 -0
  20. package/src/components/lib/charts/AreaChart.astro +4 -4
  21. package/src/components/lib/charts/BarChart.astro +3 -3
  22. package/src/components/lib/charts/ChartFrame.astro +10 -10
  23. package/src/components/lib/charts/DonutChart.astro +2 -2
  24. package/src/components/lib/charts/DumbbellChart.astro +1 -1
  25. package/src/components/lib/charts/ForecastLineChart.astro +1 -1
  26. package/src/components/lib/charts/GaugeChart.astro +1 -1
  27. package/src/components/lib/charts/HeatmapChart.astro +1 -1
  28. package/src/components/lib/charts/HorizontalBarChart.astro +2 -2
  29. package/src/components/lib/charts/KpiStatTile.astro +4 -4
  30. package/src/components/lib/charts/LinearProgressGauge.astro +4 -4
  31. package/src/components/lib/charts/MetricDeltaTable.astro +3 -3
  32. package/src/components/lib/charts/RadarMetricChart.astro +4 -4
  33. package/src/components/lib/charts/RadialBarChart.astro +1 -1
  34. package/src/components/lib/charts/RetentionCohortTable.astro +2 -2
  35. package/src/components/lib/charts/TreemapChart.astro +1 -1
  36. package/src/components/lib/ecommerce/CartDrawer.astro +483 -0
  37. package/src/components/lib/ecommerce/CartPageList.astro +290 -0
  38. package/src/components/lib/ecommerce/CartToast.astro +276 -0
  39. package/src/components/lib/ecommerce/CategoryTiles.astro +143 -0
  40. package/src/components/lib/ecommerce/CheckoutForm.astro +423 -0
  41. package/src/components/lib/ecommerce/CountdownBanner.astro +230 -0
  42. package/src/components/lib/ecommerce/FilterBar.astro +278 -0
  43. package/src/components/lib/ecommerce/FilterSidebar.astro +405 -0
  44. package/src/components/lib/ecommerce/LiveActivityToast.astro +213 -0
  45. package/src/components/lib/ecommerce/OrderSummary.astro +295 -0
  46. package/src/components/lib/ecommerce/ProductBuyBox.astro +247 -0
  47. package/src/components/lib/ecommerce/ProductCardMinimal.astro +229 -0
  48. package/src/components/lib/ecommerce/ProductGallery.astro +145 -0
  49. package/src/components/lib/ecommerce/ProductGrid.astro +118 -0
  50. package/src/components/lib/ecommerce/ProductMedia.astro +113 -0
  51. package/src/components/lib/ecommerce/ProductRating.astro +78 -0
  52. package/src/components/lib/ecommerce/ProductToolbar.astro +93 -0
  53. package/src/components/lib/ecommerce/PromoBar.astro +145 -0
  54. package/src/components/lib/ecommerce/StockIndicator.astro +127 -0
  55. package/src/components/lib/ecommerce/StoreHeader.astro +446 -0
  56. package/src/components/lib/ecommerce/StoreHero.astro +260 -0
  57. package/src/components/lib/ecommerce/StoreProductCard.astro +307 -0
  58. package/src/components/lib/ecommerce/TrustBadges.astro +101 -0
  59. package/src/components/lib/ecommerce/VariantPicker.astro +145 -0
  60. package/src/components/lib/ecommerce/cart-store.ts +151 -0
  61. package/src/components/lib/ecommerce/catalog-filter.ts +62 -0
  62. package/src/components/lib/ecommerce/index.ts +34 -0
  63. package/src/components/lib/ecommerce/product-icons.ts +28 -0
  64. package/src/components/lib/ecommerce/types.ts +70 -0
  65. package/src/components/lib/forms/AnimatedInput.astro +4 -2
  66. package/src/components/lib/forms/CustomRadioGroup.astro +3 -1
  67. package/src/components/lib/index.ts +2 -0
  68. package/src/components/lib/layout/BentoGrid3.astro +6 -4
  69. package/src/components/lib/layout/CenterFixedBox.astro +2 -2
  70. package/src/components/lib/layout/CenteredContainer.astro +4 -2
  71. package/src/components/lib/layout/ClusterHorizontal.astro +2 -2
  72. package/src/components/lib/layout/FeatureHero.astro +4 -2
  73. package/src/components/lib/layout/FooterCenteredMinimal.astro +4 -2
  74. package/src/components/lib/layout/FullBleedSection.astro +2 -2
  75. package/src/components/lib/layout/GalleryGrid4.astro +2 -2
  76. package/src/components/lib/layout/HolyGrailGrid.astro +11 -9
  77. package/src/components/lib/layout/LandingSectionSplit.astro +5 -3
  78. package/src/components/lib/layout/MasonryGrid.astro +2 -2
  79. package/src/components/lib/layout/MinimalFooterGrid.astro +1 -1
  80. package/src/components/lib/layout/ScrollSnapSection.astro +1 -1
  81. package/src/components/lib/layout/SidebarLayoutWrapper.astro +4 -2
  82. package/src/components/lib/layout/SplitScreenLayout.astro +3 -3
  83. package/src/components/lib/layout/StepLayoutVertical.astro +1 -1
  84. package/src/components/lib/layout/StickySidebarWrapper.astro +4 -2
  85. package/src/components/lib/sections/AgencyContactBrief.astro +194 -0
  86. package/src/components/lib/sections/AgencyFooterCta.astro +94 -0
  87. package/src/components/lib/sections/AgencyHeader.astro +161 -0
  88. package/src/components/lib/sections/AgencyHeroKinetic.astro +172 -0
  89. package/src/components/lib/sections/AgencyServicesList.astro +113 -0
  90. package/src/components/lib/sections/AgencyTeamGrid.astro +111 -0
  91. package/src/components/lib/sections/AgencyWorkGrid.astro +147 -0
  92. package/src/components/lib/sections/BlogGridSection.astro +9 -7
  93. package/src/components/lib/sections/ButtonsShowcase.astro +20 -18
  94. package/src/components/lib/sections/CardsShowcase.astro +28 -26
  95. package/src/components/lib/sections/CareersOpenings.astro +10 -8
  96. package/src/components/lib/sections/ChangelogSection.astro +14 -7
  97. package/src/components/lib/sections/ContactGlassForm.astro +10 -8
  98. package/src/components/lib/sections/ContactSplitMap.astro +11 -9
  99. package/src/components/lib/sections/CtaAppDownload.astro +13 -11
  100. package/src/components/lib/sections/CtaGlowBanner.astro +12 -10
  101. package/src/components/lib/sections/CtaNewsletterSection.astro +13 -11
  102. package/src/components/lib/sections/CtaSplitImage.astro +13 -11
  103. package/src/components/lib/sections/DataShowcase.astro +23 -21
  104. package/src/components/lib/sections/FaqAccordionSection.astro +8 -8
  105. package/src/components/lib/sections/FaqTwoColumn.astro +9 -7
  106. package/src/components/lib/sections/FeatureAlternatingRows.astro +10 -8
  107. package/src/components/lib/sections/FeatureBentoGrid.astro +13 -11
  108. package/src/components/lib/sections/FeatureIconGrid.astro +6 -6
  109. package/src/components/lib/sections/FeatureTabsShowcase.astro +13 -11
  110. package/src/components/lib/sections/FeatureTimelineFlow.astro +8 -6
  111. package/src/components/lib/sections/FeedbackShowcase.astro +15 -13
  112. package/src/components/lib/sections/FooterMegaSection.astro +13 -11
  113. package/src/components/lib/sections/FormsShowcase.astro +22 -20
  114. package/src/components/lib/sections/HeroDashboardPreview.astro +11 -9
  115. package/src/components/lib/sections/HeroGlass.astro +22 -20
  116. package/src/components/lib/sections/HeroGradient.astro +15 -13
  117. package/src/components/lib/sections/HeroMinimalCentered.astro +10 -10
  118. package/src/components/lib/sections/HeroShowcaseSplit.astro +18 -16
  119. package/src/components/lib/sections/HeroSplit.astro +13 -11
  120. package/src/components/lib/sections/HeroTerminal.astro +14 -12
  121. package/src/components/lib/sections/HeroWaitlist.astro +10 -8
  122. package/src/components/lib/sections/HowItWorksSteps.astro +12 -10
  123. package/src/components/lib/sections/IntegrationsGrid.astro +7 -7
  124. package/src/components/lib/sections/LayoutShowcase.astro +21 -19
  125. package/src/components/lib/sections/LogoCloudSection.astro +4 -4
  126. package/src/components/lib/sections/LogoWallGrid.astro +6 -4
  127. package/src/components/lib/sections/NavigationShowcase.astro +15 -13
  128. package/src/components/lib/sections/OverlaysShowcase.astro +21 -19
  129. package/src/components/lib/sections/PricingComparisonMatrix.astro +15 -13
  130. package/src/components/lib/sections/PricingSinglePlan.astro +13 -11
  131. package/src/components/lib/sections/PricingTierCards.astro +11 -9
  132. package/src/components/lib/sections/PricingToggleCards.astro +13 -13
  133. package/src/components/lib/sections/ShopCategoryHero.astro +103 -0
  134. package/src/components/lib/sections/ShopCollectionMosaic.astro +131 -0
  135. package/src/components/lib/sections/ShopFooterWordmark.astro +100 -0
  136. package/src/components/lib/sections/ShopHeaderEditorial.astro +396 -0
  137. package/src/components/lib/sections/ShopHeroEditorial.astro +151 -0
  138. package/src/components/lib/sections/ShopLookbook.astro +250 -0
  139. package/src/components/lib/sections/ShopNewsletterEditorial.astro +128 -0
  140. package/src/components/lib/sections/ShopProductRail.astro +150 -0
  141. package/src/components/lib/sections/ShopReviewsEditorial.astro +133 -0
  142. package/src/components/lib/sections/ShopValuesMarquee.astro +91 -0
  143. package/src/components/lib/sections/StatsCounterBand.astro +9 -7
  144. package/src/components/lib/sections/StatsDashboardSection.astro +4 -2
  145. package/src/components/lib/sections/StatsMetricShowcase.astro +9 -7
  146. package/src/components/lib/sections/TeamGridSection.astro +12 -10
  147. package/src/components/lib/sections/TestimonialCarousel.astro +10 -8
  148. package/src/components/lib/sections/TestimonialFeatured.astro +7 -5
  149. package/src/components/lib/sections/TestimonialGridSection.astro +7 -7
  150. package/src/components/lib/sections/TestimonialWallGlass.astro +11 -9
  151. package/src/components/lib/sections/TypographyShowcase.astro +10 -8
  152. package/src/components/lib/sections/index.ts +10 -0
  153. package/src/components/lib/templates/ecommerce/StoreCategoryPage.astro +160 -0
  154. package/src/components/lib/templates/ecommerce/StoreCheckoutPage.astro +98 -0
  155. package/src/components/lib/templates/ecommerce/StoreHomePage.astro +191 -0
  156. package/src/components/lib/templates/ecommerce/StoreLayout.astro +169 -0
  157. package/src/components/lib/templates/ecommerce/StoreProductPage.astro +165 -0
  158. package/src/components/lib/templates/ecommerce/data.ts +187 -0
  159. package/src/components/lib/templates/ecommerce/index.ts +14 -0
  160. package/src/components/lib/templates/ecommerce-editorial/EditorialBagPage.astro +52 -0
  161. package/src/components/lib/templates/ecommerce-editorial/EditorialCheckoutPage.astro +123 -0
  162. package/src/components/lib/templates/ecommerce-editorial/EditorialHomePage.astro +102 -0
  163. package/src/components/lib/templates/ecommerce-editorial/EditorialLayout.astro +216 -0
  164. package/src/components/lib/templates/ecommerce-editorial/EditorialProductPage.astro +205 -0
  165. package/src/components/lib/templates/ecommerce-editorial/EditorialShopPage.astro +112 -0
  166. package/src/components/lib/templates/ecommerce-editorial/data.ts +125 -0
  167. package/src/components/lib/templates/ecommerce-editorial/index.ts +16 -0
  168. package/src/components/lib/templates/saas-hotel/AppLayout.astro +83 -0
  169. package/src/components/lib/templates/saas-hotel/DashboardPage.astro +66 -0
  170. package/src/components/lib/templates/saas-hotel/GuestsPage.astro +65 -0
  171. package/src/components/lib/templates/saas-hotel/LandingPage.astro +213 -0
  172. package/src/components/lib/templates/saas-hotel/ReportsPage.astro +63 -0
  173. package/src/components/lib/templates/saas-hotel/ReservationsPage.astro +49 -0
  174. package/src/components/lib/templates/saas-hotel/RoomsPage.astro +48 -0
  175. package/src/components/lib/templates/saas-hotel/SettingsPage.astro +115 -0
  176. package/src/components/lib/templates/saas-hotel/SignInPage.astro +50 -0
  177. package/src/components/lib/templates/saas-hotel/data.ts +229 -0
  178. package/src/components/lib/templates/saas-hotel/index.ts +19 -0
  179. package/src/components/lib/templates/saas-hotel/lobby-theme.css +90 -0
  180. package/src/styles/global.css +95 -0
@@ -0,0 +1,145 @@
1
+ ---
2
+ /**
3
+ * VariantPicker — Colour swatches or size pills as an accessible radio group.
4
+ * Can keep an add-to-cart button's `data-variant` in sync with the selection.
5
+ *
6
+ * @param {string} label - Group legend. Default 'Colour'.
7
+ * @param {string} name - Radio name. Default 'variant'.
8
+ * @param {string} type - Picker style (swatch|pill). Default 'swatch'.
9
+ * @param {Array<{label: string, value?: string, hex?: string, disabled?: boolean}>} options - Choices.
10
+ * @param {number} selected - Index of the pre-selected option. Default 0.
11
+ * @param {string} target - id of an add-to-cart button whose `data-variant` follows the choice.
12
+ * @param {string} size - Control size (sm|md|lg). Default 'md'.
13
+ * @param {string} className - Extra class.
14
+ *
15
+ * Theming: --vp-active-bg, --vp-active-color, --vp-active-border, --vp-ring, --vp-radius.
16
+ *
17
+ * Accessibility: native radios (arrow keys work), visible focus ring, the chosen value is
18
+ * printed next to the legend; disabled options are announced as unavailable.
19
+ */
20
+ interface Option { label: string; value?: string; hex?: string; disabled?: boolean }
21
+ interface Props {
22
+ label?: string;
23
+ name?: string;
24
+ type?: 'swatch' | 'pill';
25
+ options?: Option[];
26
+ selected?: number;
27
+ target?: string;
28
+ size?: 'sm' | 'md' | 'lg';
29
+ className?: string;
30
+ }
31
+
32
+ const {
33
+ label = 'Colour',
34
+ name = 'variant',
35
+ type = 'swatch',
36
+ options = [
37
+ { label: 'Graphite', hex: '#2b2b2f' },
38
+ { label: 'Sand', hex: '#d8c3a5' },
39
+ { label: 'Ocean', hex: '#3d6f9e' },
40
+ { label: 'Clay', hex: '#b5654a', disabled: true },
41
+ ],
42
+ selected = 0,
43
+ target,
44
+ size = 'md',
45
+ className = '',
46
+ } = Astro.props;
47
+
48
+ const uid = `vp-${name}`;
49
+ ---
50
+
51
+ <fieldset class:list={['v-picker', `v-picker--${type}`, `v-picker--${size}`, className]} data-variant-picker data-target={target}>
52
+ <legend class="v-picker__legend">
53
+ {label}: <span class="v-picker__value" data-variant-value>{options[selected]?.label}</span>
54
+ </legend>
55
+ <div class="v-picker__options">
56
+ {options.map((o, i) => (
57
+ <label class:list={['v-picker__opt', { 'is-disabled': o.disabled }]} style={o.hex ? `--sw: ${o.hex}` : ''} title={o.label}>
58
+ <input
59
+ type="radio"
60
+ name={`${uid}`}
61
+ value={o.value ?? o.label}
62
+ data-label={o.label}
63
+ checked={i === selected}
64
+ disabled={o.disabled}
65
+ />
66
+ {type === 'swatch'
67
+ ? <span class="v-picker__dot" aria-hidden="true"></span>
68
+ : <span class="v-picker__pill">{o.label}</span>}
69
+ {type === 'swatch' && <span class="sr-only">{o.label}{o.disabled ? ' (sold out)' : ''}</span>}
70
+ {type === 'pill' && o.disabled && <span class="sr-only"> (sold out)</span>}
71
+ </label>
72
+ ))}
73
+ </div>
74
+ </fieldset>
75
+
76
+ <script>
77
+ document.querySelectorAll<HTMLFieldSetElement>('[data-variant-picker]:not([data-bound])').forEach((fs) => {
78
+ fs.dataset.bound = '';
79
+ const out = fs.querySelector<HTMLElement>('[data-variant-value]');
80
+ const sync = () => {
81
+ const checked = fs.querySelector<HTMLInputElement>('input:checked');
82
+ if (!checked) return;
83
+ if (out) out.textContent = checked.dataset.label || checked.value;
84
+ const btn = fs.dataset.target ? document.getElementById(fs.dataset.target) : null;
85
+ if (btn) {
86
+ // Combine every picker that targets the same button: "Sand / M"
87
+ const parts = Array.from(document.querySelectorAll<HTMLElement>(`[data-variant-picker][data-target="${fs.dataset.target}"]`))
88
+ .map((p) => p.querySelector<HTMLInputElement>('input:checked')?.dataset.label)
89
+ .filter(Boolean);
90
+ btn.dataset.variant = parts.join(' / ');
91
+ }
92
+ };
93
+ fs.addEventListener('change', sync);
94
+ sync();
95
+ });
96
+ </script>
97
+
98
+ <style>
99
+ .v-picker { margin: 0; padding: 0; border: 0; min-width: 0; }
100
+ .v-picker__legend { padding: 0; margin-bottom: var(--sp-3, 0.75rem); font-size: 0.85rem; font-weight: 600; color: var(--c-text-2, #a3a3a3); }
101
+ .v-picker__value { color: var(--c-text-1, #fff); }
102
+ .v-picker__options { display: flex; flex-wrap: wrap; gap: var(--sp-2, 0.5rem); }
103
+ .v-picker__opt { position: relative; cursor: pointer; }
104
+ .v-picker__opt input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; }
105
+ .v-picker__opt.is-disabled { cursor: not-allowed; }
106
+
107
+ .v-picker__dot {
108
+ display: block;
109
+ width: 36px;
110
+ height: 36px;
111
+ margin: 4px;
112
+ border-radius: 50%;
113
+ background: var(--sw);
114
+ box-shadow: 0 0 0 1px rgb(var(--ink-rgb, 255 255 255) / 0.2);
115
+ transition: box-shadow 0.15s ease, transform 0.15s ease;
116
+ }
117
+ .v-picker--sm .v-picker__dot { width: 28px; height: 28px; margin: 8px; }
118
+ .v-picker--lg .v-picker__dot { width: 44px; height: 44px; margin: 2px; }
119
+ .v-picker__opt:hover .v-picker__dot { transform: scale(1.06); }
120
+ .v-picker__opt input:checked + .v-picker__dot { box-shadow: 0 0 0 3px var(--c-bg, #050505), 0 0 0 5px var(--vp-ring, var(--c-primary-light, #d4a017)); }
121
+ .v-picker__opt.is-disabled .v-picker__dot { opacity: 0.35; background-image: linear-gradient(135deg, transparent 46%, rgb(var(--ink-rgb, 255 255 255) / 0.8) 47% 53%, transparent 54%); }
122
+
123
+ .v-picker__pill {
124
+ display: grid;
125
+ place-items: center;
126
+ min-width: 52px;
127
+ min-height: 44px;
128
+ padding: 0 var(--sp-4, 1rem);
129
+ border-radius: var(--vp-radius, var(--r-md, 12px));
130
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.12);
131
+ color: var(--c-text-1, #fff);
132
+ font-size: 0.88rem;
133
+ font-weight: 600;
134
+ transition: border-color 0.15s ease, background 0.15s ease;
135
+ }
136
+ .v-picker--sm .v-picker__pill { min-height: 36px; min-width: 44px; font-size: 0.8rem; }
137
+ .v-picker__opt:hover .v-picker__pill { border-color: rgb(var(--ink-rgb, 255 255 255) / 0.3); }
138
+ .v-picker__opt input:checked + .v-picker__pill { border-color: var(--vp-active-border, var(--c-primary-light, #d4a017)); background: var(--vp-active-bg, rgba(212, 160, 23, 0.12)); color: var(--vp-active-color, var(--c-text-1, #fff)); }
139
+ .v-picker__opt.is-disabled .v-picker__pill { color: var(--c-text-3, #737373); text-decoration: line-through; }
140
+
141
+ .v-picker__opt input:focus-visible + .v-picker__dot,
142
+ .v-picker__opt input:focus-visible + .v-picker__pill { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 3px; }
143
+
144
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
145
+ </style>
@@ -0,0 +1,151 @@
1
+ // src/components/lib/ecommerce/cart-store.ts
2
+ // Tiny framework-free cart store shared by every e-commerce component.
3
+ //
4
+ // - Persists to localStorage (per `storageKey`), falls back to memory when storage is blocked.
5
+ // - Broadcasts `ack:cart` on `document` after every change, and syncs across tabs.
6
+ // - `formatMoney` is pure and also safe to call at build time.
7
+
8
+ import type { CartItem, MoneyFormat } from './types';
9
+
10
+ export const CART_EVENT = 'ack:cart';
11
+ /** Fired after an add-to-cart button adds an item: `{ key, item, openCart }`. */
12
+ export const ADDED_EVENT = 'ack:cart-added';
13
+ /** Fire this to show any notice in CartToast: `{ title, text?, tone?: 'info'|'success'|'warning'|'danger' }`. */
14
+ export const NOTIFY_EVENT = 'ack:notify';
15
+ const DEFAULT_KEY = 'ack-cart';
16
+
17
+ const memory: Record<string, CartItem[]> = {};
18
+
19
+ export function formatMoney(value: number, { currency = 'USD', locale = 'en-US' }: MoneyFormat = {}): string {
20
+ try {
21
+ return new Intl.NumberFormat(locale, { style: 'currency', currency }).format(value);
22
+ } catch {
23
+ return `${value.toFixed(2)} ${currency}`;
24
+ }
25
+ }
26
+
27
+ function read(key: string): CartItem[] {
28
+ try {
29
+ const raw = window.localStorage.getItem(key);
30
+ if (raw) {
31
+ const parsed = JSON.parse(raw);
32
+ if (Array.isArray(parsed)) return parsed.filter((i) => i && typeof i.id === 'string');
33
+ }
34
+ return memory[key] ?? [];
35
+ } catch {
36
+ return memory[key] ?? [];
37
+ }
38
+ }
39
+
40
+ function write(key: string, items: CartItem[]): void {
41
+ memory[key] = items;
42
+ try {
43
+ window.localStorage.setItem(key, JSON.stringify(items));
44
+ } catch {
45
+ /* storage blocked — memory copy is enough for this page view */
46
+ }
47
+ document.dispatchEvent(new CustomEvent(CART_EVENT, { detail: { key, items } }));
48
+ }
49
+
50
+ /** Line id: product id + variant, so "Tee / M" and "Tee / L" are separate lines. */
51
+ export function lineId(id: string, variant?: string): string {
52
+ return variant ? `${id}::${variant}` : id;
53
+ }
54
+
55
+ export function createCart(storageKey: string = DEFAULT_KEY) {
56
+ const api = {
57
+ key: storageKey,
58
+ items: (): CartItem[] => read(storageKey),
59
+ count: (): number => read(storageKey).reduce((n, i) => n + i.qty, 0),
60
+ subtotal: (): number => read(storageKey).reduce((n, i) => n + i.price * i.qty, 0),
61
+ add(item: Omit<CartItem, 'qty'> & { qty?: number }): void {
62
+ const items = read(storageKey);
63
+ const id = lineId(item.id, item.variant);
64
+ const existing = items.find((i) => i.id === id);
65
+ if (existing) existing.qty = Math.min(99, existing.qty + (item.qty ?? 1));
66
+ else items.push({ ...item, id, qty: Math.max(1, item.qty ?? 1) });
67
+ write(storageKey, items);
68
+ },
69
+ setQty(id: string, qty: number): void {
70
+ const items = read(storageKey)
71
+ .map((i) => (i.id === id ? { ...i, qty: Math.min(99, Math.max(0, Math.round(qty))) } : i))
72
+ .filter((i) => i.qty > 0);
73
+ write(storageKey, items);
74
+ },
75
+ remove(id: string): void {
76
+ write(storageKey, read(storageKey).filter((i) => i.id !== id));
77
+ },
78
+ clear(): void {
79
+ write(storageKey, []);
80
+ },
81
+ /** Fills an empty cart once (demo stores). Never overwrites a real cart. */
82
+ seed(items: CartItem[]): void {
83
+ const flag = `${storageKey}:seeded`;
84
+ try {
85
+ if (window.localStorage.getItem(flag)) return;
86
+ window.localStorage.setItem(flag, '1');
87
+ } catch {
88
+ if (memory[flag]) return;
89
+ memory[flag] = [];
90
+ }
91
+ if (read(storageKey).length === 0 && items.length) write(storageKey, items);
92
+ },
93
+ subscribe(fn: (items: CartItem[]) => void): () => void {
94
+ const onLocal = (e: Event) => {
95
+ const detail = (e as CustomEvent).detail;
96
+ if (!detail || detail.key === storageKey) fn(read(storageKey));
97
+ };
98
+ const onStorage = (e: StorageEvent) => {
99
+ if (e.key === storageKey) fn(read(storageKey));
100
+ };
101
+ document.addEventListener(CART_EVENT, onLocal);
102
+ window.addEventListener('storage', onStorage);
103
+ return () => {
104
+ document.removeEventListener(CART_EVENT, onLocal);
105
+ window.removeEventListener('storage', onStorage);
106
+ };
107
+ },
108
+ };
109
+ return api;
110
+ }
111
+
112
+ export type Cart = ReturnType<typeof createCart>;
113
+
114
+ /**
115
+ * Wires every `[data-add-to-cart]` button inside `root` to the cart.
116
+ * The attribute holds the item JSON; an optional `data-cart-key` picks the store.
117
+ * Variant pickers can update `data-variant` on the button before the click.
118
+ */
119
+ export function bindAddToCart(root: ParentNode = document): void {
120
+ root.querySelectorAll<HTMLButtonElement>('[data-add-to-cart]:not([data-cart-bound])').forEach((btn) => {
121
+ btn.dataset.cartBound = '';
122
+ btn.addEventListener('click', (event) => {
123
+ event.preventDefault();
124
+ let item: CartItem;
125
+ try {
126
+ item = JSON.parse(btn.dataset.addToCart || '{}');
127
+ } catch {
128
+ return;
129
+ }
130
+ if (!item.id) return;
131
+ const qtyInput = btn.dataset.qtyFrom ? document.getElementById(btn.dataset.qtyFrom) as HTMLInputElement | null : null;
132
+ const qty = qtyInput ? Number(qtyInput.value) || 1 : 1;
133
+ const added = { ...item, variant: btn.dataset.variant || item.variant, qty };
134
+ createCart(btn.dataset.cartKey || DEFAULT_KEY).add(added);
135
+ document.dispatchEvent(new CustomEvent(ADDED_EVENT, { detail: { key: btn.dataset.cartKey || DEFAULT_KEY, item: added, openCart: btn.dataset.openCart !== 'false' } }));
136
+
137
+ const label = btn.querySelector<HTMLElement>('[data-label]');
138
+ const original = label?.textContent;
139
+ btn.classList.add('is-added');
140
+ if (label && btn.dataset.addedLabel) label.textContent = btn.dataset.addedLabel;
141
+ window.setTimeout(() => {
142
+ btn.classList.remove('is-added');
143
+ if (label && original) label.textContent = original;
144
+ }, 1400);
145
+
146
+ if (btn.dataset.openCart !== 'false') {
147
+ document.dispatchEvent(new CustomEvent('ack:cart-open', { detail: { key: btn.dataset.cartKey || DEFAULT_KEY } }));
148
+ }
149
+ });
150
+ });
151
+ }
@@ -0,0 +1,62 @@
1
+ // src/components/lib/ecommerce/catalog-filter.ts
2
+ // Client-side filter + sort for any grid of `[data-product]` cards
3
+ // (StoreProductCard and ProductCardMinimal both expose the same data attributes).
4
+
5
+ export interface CatalogState {
6
+ categories: string[];
7
+ colors: string[];
8
+ maxPrice: number;
9
+ minRating: number;
10
+ inStockOnly: boolean;
11
+ sort: 'featured' | 'price-asc' | 'price-desc' | 'rating' | 'name' | string;
12
+ }
13
+
14
+ /** Filters and re-orders the cards inside `grid`. Returns the number of visible cards. */
15
+ export function applyCatalogFilter(grid: HTMLElement, state: CatalogState): number {
16
+ const cards = Array.from(grid.querySelectorAll<HTMLElement>('[data-product]'));
17
+ cards.forEach((c, i) => { if (c.dataset.index === undefined) c.dataset.index = String(i); });
18
+ let visible = 0;
19
+
20
+ for (const card of cards) {
21
+ const d = card.dataset;
22
+ const colors = (d.colors || '').split(',').filter(Boolean);
23
+ const ok =
24
+ (!state.categories.length || state.categories.includes(d.category || '')) &&
25
+ Number(d.price) <= state.maxPrice &&
26
+ Number(d.rating || 0) >= state.minRating &&
27
+ (!state.inStockOnly || d.stock === 'in') &&
28
+ (!state.colors.length || state.colors.some((c) => colors.includes(c)));
29
+ // Cards may be wrapped (e.g. in <li>): hide the direct child of the grid.
30
+ const item = card.parentElement === grid ? card : (card.closest(`#${CSS.escape(grid.id)} > *`) as HTMLElement | null) ?? card;
31
+ item.hidden = !ok;
32
+ if (ok) visible++;
33
+ }
34
+
35
+ const items = cards.map((card) => ({ card, item: card.parentElement === grid ? card : ((card.closest(`#${CSS.escape(grid.id)} > *`) as HTMLElement | null) ?? card) }));
36
+ items.sort((a, b) => {
37
+ const A = a.card.dataset, B = b.card.dataset;
38
+ switch (state.sort) {
39
+ case 'price-asc': return Number(A.price) - Number(B.price);
40
+ case 'price-desc': return Number(B.price) - Number(A.price);
41
+ case 'rating': return Number(B.rating) - Number(A.rating);
42
+ case 'name': return (A.name || '').localeCompare(B.name || '');
43
+ default: return Number(A.index) - Number(B.index);
44
+ }
45
+ });
46
+ grid.append(...items.map((i) => i.item));
47
+
48
+ const empty = grid.parentElement?.querySelector<HTMLElement>('[data-product-empty]');
49
+ if (empty) empty.hidden = visible > 0;
50
+ document.querySelectorAll<HTMLElement>(`[data-result-count="${grid.id}"]`).forEach((n) => { n.textContent = String(visible); });
51
+ return visible;
52
+ }
53
+
54
+ /** Reads/writes `?category=` and `?sort=` so filtered views can be shared. */
55
+ export function syncCatalogUrl(state: Pick<CatalogState, 'categories' | 'sort'>): void {
56
+ const url = new URL(window.location.href);
57
+ if (state.categories.length) url.searchParams.set('category', state.categories.join(','));
58
+ else url.searchParams.delete('category');
59
+ if (state.sort && state.sort !== 'featured') url.searchParams.set('sort', state.sort);
60
+ else url.searchParams.delete('sort');
61
+ window.history.replaceState(null, '', url);
62
+ }
@@ -0,0 +1,34 @@
1
+ // src/components/lib/ecommerce/index.ts
2
+ // Barrel export for e-commerce components (shop UI + client cart store).
3
+
4
+ export { default as ProductMedia } from './ProductMedia.astro';
5
+ export { default as ProductRating } from './ProductRating.astro';
6
+ export { default as StoreProductCard } from './StoreProductCard.astro';
7
+ export { default as ProductGrid } from './ProductGrid.astro';
8
+ export { default as PromoBar } from './PromoBar.astro';
9
+ export { default as StoreHeader } from './StoreHeader.astro';
10
+ export { default as CartDrawer } from './CartDrawer.astro';
11
+ export { default as StoreHero } from './StoreHero.astro';
12
+ export { default as CategoryTiles } from './CategoryTiles.astro';
13
+ export { default as TrustBadges } from './TrustBadges.astro';
14
+ export { default as FilterSidebar } from './FilterSidebar.astro';
15
+ export { default as ProductToolbar } from './ProductToolbar.astro';
16
+ export { default as ProductGallery } from './ProductGallery.astro';
17
+ export { default as VariantPicker } from './VariantPicker.astro';
18
+ export { default as ProductBuyBox } from './ProductBuyBox.astro';
19
+ export { default as OrderSummary } from './OrderSummary.astro';
20
+ export { default as CheckoutForm } from './CheckoutForm.astro';
21
+ export { default as CartToast } from './CartToast.astro';
22
+ export { default as LiveActivityToast } from './LiveActivityToast.astro';
23
+ export { default as CountdownBanner } from './CountdownBanner.astro';
24
+ export { default as StockIndicator } from './StockIndicator.astro';
25
+ export { default as ProductCardMinimal } from './ProductCardMinimal.astro';
26
+ export { default as FilterBar } from './FilterBar.astro';
27
+ export { default as CartPageList } from './CartPageList.astro';
28
+
29
+ export { createCart, bindAddToCart, formatMoney, lineId, CART_EVENT, ADDED_EVENT, NOTIFY_EVENT } from './cart-store';
30
+ export { applyCatalogFilter, syncCatalogUrl } from './catalog-filter';
31
+ export type { CatalogState } from './catalog-filter';
32
+ export { productIcons, productIcon } from './product-icons';
33
+ export type { ProductIcon } from './product-icons';
34
+ export type { Product, ProductColor, StoreCategory, CartItem, ShippingRate, MoneyFormat } from './types';
@@ -0,0 +1,28 @@
1
+ // src/components/lib/ecommerce/product-icons.ts
2
+ // Line-art product illustrations (64×64 viewBox, stroke = currentColor).
3
+ // Used by ProductMedia so demo stores look finished without any external images.
4
+
5
+ export const productIcons = {
6
+ headphones: '<path d="M12 38v-6a20 20 0 0 1 40 0v6"/><rect x="8" y="36" width="10" height="16" rx="4"/><rect x="46" y="36" width="10" height="16" rx="4"/><path d="M18 44h-2M46 44h2"/>',
7
+ watch: '<rect x="18" y="18" width="28" height="28" rx="9"/><path d="M24 18l2-10h12l2 10M24 46l2 10h12l2-10"/><path d="M32 25v7l5 4"/><path d="M46 30h3"/>',
8
+ sneaker: '<path d="M6 44c0-6 2-14 6-20l8 4c3 2 7 2 10 0l4-3c6 8 14 11 22 12 2 0 3 2 3 4v3H6z"/><path d="M6 47v3h53v-3"/><path d="M22 30l3 4M27 28l3 4M32 26l2 4"/>',
9
+ backpack: '<rect x="14" y="16" width="36" height="42" rx="10"/><path d="M24 16v-4a8 8 0 0 1 16 0v4"/><rect x="21" y="36" width="22" height="13" rx="4"/><path d="M21 42h22"/>',
10
+ camera: '<rect x="6" y="18" width="52" height="34" rx="7"/><path d="M22 18l4-7h12l4 7"/><circle cx="32" cy="35" r="10"/><circle cx="32" cy="35" r="4"/><path d="M48 25h3"/>',
11
+ lamp: '<path d="M20 10h24l8 20H12z"/><path d="M32 30v20"/><path d="M20 56h24"/><path d="M26 50h12"/>',
12
+ keyboard: '<rect x="4" y="20" width="56" height="26" rx="5"/><path d="M11 28h3M19 28h3M27 28h3M35 28h3M43 28h3M51 28h2M13 35h3M21 35h3M29 35h3M37 35h3M45 35h4M18 41h28"/>',
13
+ sunglasses: '<path d="M4 26h56"/><path d="M8 26c0 9 3 14 10 14s10-5 10-14"/><path d="M36 26c0 9 3 14 10 14s10-5 10-14"/><path d="M28 30c2-2 6-2 8 0"/>',
14
+ bottle: '<path d="M26 6h12v8l4 6v34a4 4 0 0 1-4 4H26a4 4 0 0 1-4-4V20l4-6z"/><path d="M22 28h20M22 44h20"/>',
15
+ speaker: '<rect x="16" y="6" width="32" height="52" rx="8"/><circle cx="32" cy="38" r="10"/><circle cx="32" cy="38" r="3"/><circle cx="32" cy="17" r="4"/>',
16
+ jacket: '<path d="M22 8l10 6 10-6 12 8 4 22-8 2v18H14V40l-8-2 4-22z"/><path d="M32 14v42"/><path d="M22 8l-2 12 6 6M42 8l2 12-6 6"/><path d="M38 34h4M38 42h4"/>',
17
+ mug: '<path d="M12 18h32v28a8 8 0 0 1-8 8H20a8 8 0 0 1-8-8z"/><path d="M44 24h4a6 6 0 0 1 0 12h-4"/><path d="M22 8c-2 3 2 4 0 7M30 8c-2 3 2 4 0 7"/>',
18
+ plant: '<path d="M20 40h24l-3 18H23z"/><path d="M32 40V22"/><path d="M32 30c-8 0-12-6-12-14 8 0 12 6 12 14zM32 26c0-8 4-14 12-14 0 8-4 14-12 14z"/>',
19
+ shirt: '<path d="M22 8l-14 8 5 12 7-3v31h24V25l7 3 5-12-14-8c-1 4-5 6-10 6s-9-2-10-6z"/>',
20
+ bag: '<path d="M12 22h40l-4 34H16z"/><path d="M24 22v-6a8 8 0 0 1 16 0v6"/><path d="M24 30v2M40 30v2"/>',
21
+ phone: '<rect x="18" y="4" width="28" height="56" rx="7"/><path d="M28 10h8"/><circle cx="32" cy="52" r="2"/>',
22
+ } as const;
23
+
24
+ export type ProductIcon = keyof typeof productIcons;
25
+
26
+ export function productIcon(name?: string): string {
27
+ return productIcons[(name as ProductIcon) ?? 'bag'] ?? productIcons.bag;
28
+ }
@@ -0,0 +1,70 @@
1
+ // src/components/lib/ecommerce/types.ts
2
+ // Shared data shapes for the e-commerce components and the store template.
3
+
4
+ import type { ProductIcon } from './product-icons';
5
+
6
+ export interface ProductColor {
7
+ name: string;
8
+ hex: string;
9
+ }
10
+
11
+ export interface Product {
12
+ /** URL-safe id, also used as cart line id. */
13
+ slug: string;
14
+ name: string;
15
+ /** Price in major units (e.g. 129.9). */
16
+ price: number;
17
+ /** Optional "before" price, shown struck through. */
18
+ compareAt?: number;
19
+ /** Category id (matches `StoreCategory.id`). */
20
+ category: string;
21
+ /** 0–5 average rating. */
22
+ rating?: number;
23
+ reviews?: number;
24
+ /** Short highlight such as "New" or "-20%". */
25
+ badge?: string;
26
+ badgeVariant?: 'default' | 'success' | 'warning' | 'danger' | 'info';
27
+ colors?: ProductColor[];
28
+ sizes?: string[];
29
+ /** Built-in illustration used when no `image` is given. */
30
+ icon?: ProductIcon;
31
+ /** Hue (0–360) of the generated product backdrop. */
32
+ hue?: number;
33
+ /** Real product photo; replaces the illustration. */
34
+ image?: string;
35
+ description?: string;
36
+ inStock?: boolean;
37
+ /** Units left; StockIndicator shows urgency at or below its threshold. */
38
+ stock?: number;
39
+ }
40
+
41
+ export interface StoreCategory {
42
+ id: string;
43
+ name: string;
44
+ description?: string;
45
+ icon?: ProductIcon;
46
+ hue?: number;
47
+ }
48
+
49
+ export interface CartItem {
50
+ id: string;
51
+ name: string;
52
+ price: number;
53
+ qty: number;
54
+ variant?: string;
55
+ image?: string;
56
+ icon?: ProductIcon;
57
+ hue?: number;
58
+ }
59
+
60
+ export interface ShippingRate {
61
+ id: string;
62
+ label: string;
63
+ eta: string;
64
+ price: number;
65
+ }
66
+
67
+ export interface MoneyFormat {
68
+ currency?: string;
69
+ locale?: string;
70
+ }
@@ -8,6 +8,7 @@
8
8
  * @param {string} placeholder - Passed placeholder. Default is ' '. (Space is needed for CSS :placeholder-shown to work).
9
9
  * @param {boolean} required - HTML required attribute parameter. Default is false.
10
10
  * @param {string} name - HTML input name binding.
11
+ * @param {string} autocomplete - HTML autocomplete hint (e.g. 'email', 'given-name'). Default is 'off'.
11
12
  */
12
13
  interface Props {
13
14
  label: string;
@@ -16,13 +17,14 @@ interface Props {
16
17
  placeholder?: string;
17
18
  required?: boolean;
18
19
  name?: string;
20
+ autocomplete?: string;
19
21
  }
20
22
 
21
- const { label, id, type = 'text', placeholder = ' ', required = false, name } = Astro.props;
23
+ const { label, id, type = 'text', placeholder = ' ', required = false, name, autocomplete = 'off' } = Astro.props;
22
24
  ---
23
25
 
24
26
  <div class="anim-input-wrap">
25
- <input {id} {name} {type} {placeholder} {required} class="anim-input" autocomplete="off" />
27
+ <input {id} {name} {type} {placeholder} {required} class="anim-input" autocomplete={autocomplete} />
26
28
  <label for={id} class="anim-input-label">{label}</label>
27
29
  </div>
28
30
 
@@ -39,7 +39,9 @@ const { label, name, options = [], horizontal = false } = Astro.props;
39
39
 
40
40
  .radio-item { display: flex; align-items: center; gap: 0.75rem; cursor: pointer; color: var(--c-text-2, #94a3b8); transition: 0.2s; user-select: none; }
41
41
 
42
- .radio-item input { appearance: none; display: none; }
42
+ .radio-item input { position: absolute; opacity: 0; width: 1px; height: 1px; margin: 0; pointer-events: none; }
43
+ .radio-item { position: relative; }
44
+ .radio-item input:focus-visible + .radio-circle { outline: 2px solid var(--c-primary-light, #818cf8); outline-offset: 2px; }
43
45
 
44
46
  .radio-circle {
45
47
  width: 20px; height: 20px;
@@ -3,6 +3,8 @@ export * from './buttons/index';
3
3
  export * from './cards/index';
4
4
  export * from './charts/index';
5
5
  export * from './data/index';
6
+ export * from './ecommerce/index';
7
+ export * from './app/index';
6
8
  export * from './feedback/index';
7
9
  export * from './forms/index';
8
10
  export * from './layout/index';
@@ -9,6 +9,7 @@ interface Props {
9
9
  const { className = '' } = Astro.props;
10
10
  ---
11
11
 
12
+ <div class="cq-bento-grid3-cq">
12
13
  <div class={`bento ${className}`}>
13
14
  <div class="item wide">
14
15
  <slot name="1">
@@ -35,8 +36,9 @@ const { className = '' } = Astro.props;
35
36
  </slot>
36
37
  </div>
37
38
  </div>
38
-
39
+ </div>
39
40
  <style>
41
+ .cq-bento-grid3-cq { container-type: inline-size; }
40
42
  .bento {
41
43
  display: grid;
42
44
  grid-template-columns: repeat(3, 1fr);
@@ -45,8 +47,8 @@ const { className = '' } = Astro.props;
45
47
  width: 100%;
46
48
  }
47
49
  .item {
48
- background: rgba(255, 255, 255, 0.03);
49
- border: 1px solid rgba(255, 255, 255, 0.08);
50
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.03);
51
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.08);
50
52
  border-radius: 16px;
51
53
  padding: 1.25rem;
52
54
  display: flex;
@@ -69,7 +71,7 @@ const { className = '' } = Astro.props;
69
71
  .tall {
70
72
  grid-row: span 2;
71
73
  }
72
- @media (max-width: 768px) {
74
+ @container (max-width: 768px) {
73
75
  .bento {
74
76
  grid-template-columns: 1fr;
75
77
  grid-auto-rows: auto;
@@ -28,7 +28,7 @@ const { className = '' } = Astro.props;
28
28
  background: rgba(0, 0, 0, 0.2);
29
29
  border-radius: var(--r-lg, 12px);
30
30
  overflow: hidden;
31
- border: 1px dashed rgba(255, 255, 255, 0.15);
31
+ border: 1px dashed rgb(var(--ink-rgb, 255 255 255) / 0.15);
32
32
  }
33
33
  .abs-center {
34
34
  position: absolute;
@@ -40,7 +40,7 @@ const { className = '' } = Astro.props;
40
40
  .abs-card {
41
41
  padding: 1.25rem 2rem;
42
42
  background: rgba(20, 20, 20, 0.85);
43
- border: 1px solid rgba(255, 255, 255, 0.1);
43
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1);
44
44
  border-radius: 12px;
45
45
  text-align: center;
46
46
  }
@@ -25,11 +25,13 @@ interface Props {
25
25
  const { maxWidth = '1200px', padding = '1.5rem', className = '' } = Astro.props;
26
26
  ---
27
27
 
28
+ <div class="cq-centered-container-cq">
28
29
  <div class:list={['centered-container', className]} style={`--container-max: ${maxWidth}; --container-pad: ${padding}`}>
29
30
  <slot />
30
31
  </div>
31
-
32
+ </div>
32
33
  <style>
34
+ .cq-centered-container-cq { container-type: inline-size; }
33
35
  .centered-container {
34
36
  width: 100%;
35
37
  max-width: var(--container-max);
@@ -38,7 +40,7 @@ const { maxWidth = '1200px', padding = '1.5rem', className = '' } = Astro.props;
38
40
  padding-right: var(--container-pad);
39
41
  }
40
42
 
41
- @media (max-width: 640px) {
43
+ @container (max-width: 640px) {
42
44
  .centered-container {
43
45
  padding-left: 1rem;
44
46
  padding-right: 1rem;
@@ -31,8 +31,8 @@ const { gap = '0.75rem', align = 'center', className = '' } = Astro.props;
31
31
  }
32
32
  .cluster-tag {
33
33
  padding: 0.4rem 0.8rem;
34
- background: rgba(255, 255, 255, 0.05);
35
- border: 1px solid rgba(255, 255, 255, 0.1);
34
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.05);
35
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1);
36
36
  border-radius: 9999px;
37
37
  font-size: 0.8rem;
38
38
  color: #94a3b8;
@@ -39,6 +39,7 @@ const {
39
39
  } = Astro.props;
40
40
  ---
41
41
 
42
+ <div class="cq-feature-hero-cq">
42
43
  <section class:list={['feature-hero', className]}>
43
44
  <div class="feature-hero__bg" aria-hidden="true">
44
45
  <div class="feature-hero__gradient" style={`--gradient-from: ${gradientFrom}; --gradient-to: ${gradientTo}`}></div>
@@ -52,8 +53,9 @@ const {
52
53
  </div>
53
54
  </div>
54
55
  </section>
55
-
56
+ </div>
56
57
  <style>
58
+ .cq-feature-hero-cq { container-type: inline-size; }
57
59
  .feature-hero {
58
60
  position: relative;
59
61
  padding: var(--sp-16) var(--sp-6);
@@ -142,7 +144,7 @@ const {
142
144
  }
143
145
  }
144
146
 
145
- @media (max-width: 640px) {
147
+ @container (max-width: 640px) {
146
148
  .feature-hero {
147
149
  padding: var(--sp-12) var(--sp-4);
148
150
  }