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,405 @@
1
+ ---
2
+ /**
3
+ * FilterSidebar — Catalog filters (category, max price, colour, rating, stock) that filter
4
+ * and sort a ProductGrid instantly in the browser. Collapses behind a "Filters" button on mobile.
5
+ *
6
+ * @param {string} target - id of the ProductGrid to filter. Default 'product-grid'.
7
+ * @param {string} id - Form id; ProductToolbar's sort select uses `${id}-sort`. Default 'catalog-filters'.
8
+ * @param {Array<{id: string, name: string, count?: number}>} categories - Category checkboxes.
9
+ * @param {string[]} selected - Pre-checked category ids (also read from `?category=` in the URL).
10
+ * @param {number} maxPrice - Upper bound of the price slider. Default 300.
11
+ * @param {Array<{name: string, hex: string}>} colors - Colour swatches.
12
+ * @param {string} currency - ISO currency. Default 'USD'.
13
+ * @param {string} locale - Number format locale. Default 'en-US'.
14
+ * @param {string} variant - Style (glass|plain). Default 'glass'.
15
+ * @param {string} size - Density (sm|md). Default 'md'.
16
+ * @param {string} className - Extra class.
17
+ *
18
+ * Accessibility: native form controls in fieldsets with legends; result count is announced
19
+ * by ProductToolbar's live region; the mobile toggle is a disclosure button.
20
+ */
21
+ import { formatMoney } from './cart-store';
22
+
23
+ interface Props {
24
+ target?: string;
25
+ id?: string;
26
+ categories?: Array<{ id: string; name: string; count?: number }>;
27
+ selected?: string[];
28
+ maxPrice?: number;
29
+ colors?: Array<{ name: string; hex: string }>;
30
+ currency?: string;
31
+ locale?: string;
32
+ variant?: 'glass' | 'plain';
33
+ size?: 'sm' | 'md';
34
+ className?: string;
35
+ }
36
+
37
+ const {
38
+ target = 'product-grid',
39
+ id = 'catalog-filters',
40
+ categories = [
41
+ { id: 'audio', name: 'Audio', count: 3 },
42
+ { id: 'wearables', name: 'Wearables', count: 2 },
43
+ { id: 'bags', name: 'Bags', count: 2 },
44
+ { id: 'home', name: 'Home', count: 3 },
45
+ ],
46
+ selected = [],
47
+ maxPrice = 300,
48
+ colors = [
49
+ { name: 'Graphite', hex: '#2b2b2f' },
50
+ { name: 'Sand', hex: '#d8c3a5' },
51
+ { name: 'Ocean', hex: '#3d6f9e' },
52
+ { name: 'Moss', hex: '#5f7a4e' },
53
+ { name: 'Clay', hex: '#b5654a' },
54
+ ],
55
+ currency = 'USD',
56
+ locale = 'en-US',
57
+ variant = 'glass',
58
+ size = 'md',
59
+ className = '',
60
+ } = Astro.props;
61
+
62
+ const money = { currency, locale };
63
+ ---
64
+
65
+ <div class:list={['filters', `filters--${variant}`, `filters--${size}`, className]} data-filters-root>
66
+ <button class="filters__toggle" type="button" aria-expanded="false" aria-controls={id} data-filters-toggle>
67
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" aria-hidden="true"><path d="M4 6h16M7 12h10M10 18h4"/></svg>
68
+ Filters
69
+ <span class="filters__active" data-active-count hidden></span>
70
+ </button>
71
+
72
+ <form class="filters__form" id={id} data-filters={target} data-currency={currency} data-locale={locale} aria-label="Product filters">
73
+ <fieldset class="filters__group">
74
+ <legend>Category</legend>
75
+ {categories.map((c) => (
76
+ <label class="filters__check">
77
+ <input type="checkbox" name="category" value={c.id} checked={selected.includes(c.id)} />
78
+ <span class="filters__box" aria-hidden="true"></span>
79
+ <span class="filters__label">{c.name}</span>
80
+ {c.count !== undefined && <span class="filters__count">{c.count}</span>}
81
+ </label>
82
+ ))}
83
+ </fieldset>
84
+
85
+ <fieldset class="filters__group">
86
+ <legend>Price</legend>
87
+ <label class="filters__range">
88
+ <span class="filters__range-head">
89
+ <span>Up to</span>
90
+ <output data-price-out>{formatMoney(maxPrice, money)}</output>
91
+ </span>
92
+ <input type="range" name="max" min="0" max={maxPrice} step="5" value={maxPrice} aria-label="Maximum price" />
93
+ <span class="filters__range-scale" aria-hidden="true"><span>{formatMoney(0, money)}</span><span>{formatMoney(maxPrice, money)}</span></span>
94
+ </label>
95
+ </fieldset>
96
+
97
+ {colors.length > 0 && (
98
+ <fieldset class="filters__group">
99
+ <legend>Colour</legend>
100
+ <div class="filters__swatches">
101
+ {colors.map((c) => (
102
+ <label class="filters__swatch" style={`--sw: ${c.hex}`}>
103
+ <input type="checkbox" name="color" value={c.name.toLowerCase()} />
104
+ <span class="filters__swatch-dot" aria-hidden="true"></span>
105
+ <span class="sr-only">{c.name}</span>
106
+ </label>
107
+ ))}
108
+ </div>
109
+ </fieldset>
110
+ )}
111
+
112
+ <fieldset class="filters__group">
113
+ <legend>Rating</legend>
114
+ {[['0', 'Any rating'], ['4.5', '4.5 ★ & up'], ['4', '4 ★ & up']].map(([v, l], i) => (
115
+ <label class="filters__check filters__check--radio">
116
+ <input type="radio" name="rating" value={v} checked={i === 0} />
117
+ <span class="filters__box" aria-hidden="true"></span>
118
+ <span class="filters__label">{l}</span>
119
+ </label>
120
+ ))}
121
+ </fieldset>
122
+
123
+ <fieldset class="filters__group">
124
+ <legend class="sr-only">Availability</legend>
125
+ <label class="filters__switch">
126
+ <span>In stock only</span>
127
+ <input type="checkbox" name="stock" value="in" role="switch" />
128
+ <span class="filters__track" aria-hidden="true"></span>
129
+ </label>
130
+ </fieldset>
131
+
132
+ <button class="filters__reset" type="reset">Clear all filters</button>
133
+ </form>
134
+ </div>
135
+
136
+ <script>
137
+ import { formatMoney } from './cart-store';
138
+
139
+ document.querySelectorAll<HTMLFormElement>('form[data-filters]:not([data-bound])').forEach((form) => {
140
+ form.dataset.bound = '';
141
+ const root = form.closest<HTMLElement>('[data-filters-root]')!;
142
+ const target = document.getElementById(form.dataset.filters || '');
143
+ if (!target) return;
144
+ const grid: HTMLElement = target;
145
+ const wrap = grid.parentElement;
146
+ const empty = wrap?.querySelector<HTMLElement>('[data-product-empty]');
147
+ const cards = Array.from(grid.querySelectorAll<HTMLElement>('[data-product]'));
148
+ cards.forEach((c, i) => { c.dataset.index = String(i); });
149
+ const sort = document.getElementById(`${form.id}-sort`) as HTMLSelectElement | null;
150
+ const priceOut = form.querySelector<HTMLOutputElement>('[data-price-out]');
151
+ const toggle = root.querySelector<HTMLButtonElement>('[data-filters-toggle]');
152
+ const activeBadge = root.querySelector<HTMLElement>('[data-active-count]');
153
+ const money = { currency: form.dataset.currency, locale: form.dataset.locale };
154
+ const priceInput = form.elements.namedItem('max') as HTMLInputElement;
155
+ const ceiling = Number(priceInput.max);
156
+
157
+ // Pre-select categories from ?category=a,b
158
+ const params = new URLSearchParams(window.location.search);
159
+ const fromUrl = (params.get('category') || '').split(',').filter(Boolean);
160
+ if (fromUrl.length) {
161
+ form.querySelectorAll<HTMLInputElement>('input[name="category"]').forEach((i) => { i.checked = fromUrl.includes(i.value); });
162
+ }
163
+ if (sort && params.get('sort')) sort.value = params.get('sort')!;
164
+
165
+ function apply() {
166
+ const data = new FormData(form);
167
+ const cats = data.getAll('category') as string[];
168
+ const colors = data.getAll('color') as string[];
169
+ const max = Number(data.get('max'));
170
+ const minRating = Number(data.get('rating') || 0);
171
+ const stockOnly = data.get('stock') === 'in';
172
+ let visible = 0;
173
+
174
+ cards.forEach((card) => {
175
+ const d = card.dataset;
176
+ const cardColors = (d.colors || '').split(',').filter(Boolean);
177
+ const ok =
178
+ (!cats.length || cats.includes(d.category || '')) &&
179
+ Number(d.price) <= max &&
180
+ Number(d.rating) >= minRating &&
181
+ (!stockOnly || d.stock === 'in') &&
182
+ (!colors.length || colors.some((c) => cardColors.includes(c)));
183
+ card.hidden = !ok;
184
+ if (ok) visible++;
185
+ });
186
+
187
+ const mode = sort?.value || 'featured';
188
+ const sorted = [...cards].sort((a, b) => {
189
+ const A = a.dataset, B = b.dataset;
190
+ if (mode === 'price-asc') return Number(A.price) - Number(B.price);
191
+ if (mode === 'price-desc') return Number(B.price) - Number(A.price);
192
+ if (mode === 'rating') return Number(B.rating) - Number(A.rating);
193
+ if (mode === 'name') return (A.name || '').localeCompare(B.name || '');
194
+ return Number(A.index) - Number(B.index);
195
+ });
196
+ grid.append(...sorted);
197
+
198
+ if (empty) empty.hidden = visible > 0;
199
+ if (priceOut) priceOut.textContent = formatMoney(max, money);
200
+ document.querySelectorAll<HTMLElement>(`[data-result-count="${grid.id}"]`).forEach((n) => { n.textContent = String(visible); });
201
+
202
+ const active = cats.length + colors.length + (max < ceiling ? 1 : 0) + (minRating > 0 ? 1 : 0) + (stockOnly ? 1 : 0);
203
+ if (activeBadge) { activeBadge.hidden = active === 0; activeBadge.textContent = String(active); }
204
+
205
+ const url = new URL(window.location.href);
206
+ if (cats.length) url.searchParams.set('category', cats.join(','));
207
+ else url.searchParams.delete('category');
208
+ if (mode !== 'featured') url.searchParams.set('sort', mode);
209
+ else url.searchParams.delete('sort');
210
+ window.history.replaceState(null, '', url);
211
+ }
212
+
213
+ form.addEventListener('input', apply);
214
+ form.addEventListener('change', apply);
215
+ form.addEventListener('submit', (e) => { e.preventDefault(); apply(); });
216
+ form.addEventListener('reset', () => window.setTimeout(apply, 0));
217
+ sort?.addEventListener('change', apply);
218
+
219
+ toggle?.addEventListener('click', () => {
220
+ const open = toggle.getAttribute('aria-expanded') !== 'true';
221
+ toggle.setAttribute('aria-expanded', String(open));
222
+ root.classList.toggle('is-open', open);
223
+ });
224
+
225
+ apply();
226
+ });
227
+ </script>
228
+
229
+ <style>
230
+ .filters { container-type: inline-size; position: relative; }
231
+ .filters__form {
232
+ display: grid;
233
+ gap: var(--sp-2, 0.5rem);
234
+ }
235
+ .filters--glass .filters__form {
236
+ padding: var(--sp-5, 1.25rem);
237
+ border-radius: var(--r-lg, 20px);
238
+ border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05));
239
+ background: var(--c-surface, rgb(var(--ink-rgb, 255 255 255) / 0.02));
240
+ }
241
+
242
+ .filters__group {
243
+ display: grid;
244
+ gap: 2px;
245
+ margin: 0;
246
+ padding: 0 0 var(--sp-4, 1rem);
247
+ border: 0;
248
+ border-bottom: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05));
249
+ }
250
+ .filters__group:last-of-type { border-bottom: 0; }
251
+ .filters__group legend {
252
+ padding: var(--sp-2, 0.5rem) 0;
253
+ font-size: 0.72rem;
254
+ font-weight: 800;
255
+ letter-spacing: 0.1em;
256
+ text-transform: uppercase;
257
+ color: var(--c-text-1, #fff);
258
+ }
259
+
260
+ .filters__check {
261
+ position: relative;
262
+ display: flex;
263
+ align-items: center;
264
+ gap: var(--sp-3, 0.75rem);
265
+ min-height: 40px;
266
+ padding: 0 var(--sp-2, 0.5rem);
267
+ margin: 0 calc(var(--sp-2, 0.5rem) * -1);
268
+ border-radius: var(--r-sm, 8px);
269
+ cursor: pointer;
270
+ color: var(--c-text-2, #a3a3a3);
271
+ font-size: 0.9rem;
272
+ }
273
+ .filters--sm .filters__check { min-height: 34px; font-size: 0.85rem; }
274
+ .filters__check:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.03); color: var(--c-text-1, #fff); }
275
+ .filters__check input,
276
+ .filters__swatch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
277
+ .filters__box {
278
+ display: grid;
279
+ place-items: center;
280
+ flex-shrink: 0;
281
+ width: 18px;
282
+ height: 18px;
283
+ border-radius: 5px;
284
+ border: 1.5px solid rgb(var(--ink-rgb, 255 255 255) / 0.25);
285
+ transition: background 0.15s ease, border-color 0.15s ease;
286
+ }
287
+ .filters__check--radio .filters__box { border-radius: 50%; }
288
+ .filters__check input:checked + .filters__box {
289
+ border-color: var(--c-primary-light, #d4a017);
290
+ background: var(--c-primary-light, #d4a017);
291
+ box-shadow: inset 0 0 0 3px var(--c-bg-1, #0a0a0a);
292
+ }
293
+ .filters__check input:checked ~ .filters__label { color: var(--c-text-1, #fff); font-weight: 600; }
294
+ .filters__check input:focus-visible + .filters__box,
295
+ .filters__swatch input:focus-visible + .filters__swatch-dot,
296
+ .filters__switch input:focus-visible + .filters__track { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
297
+ .filters__label { flex: 1; }
298
+ .filters__count { font-size: 0.75rem; color: var(--c-text-2, #a3a3a3); }
299
+
300
+ .filters__range { display: grid; gap: var(--sp-2, 0.5rem); }
301
+ .filters__range-head { display: flex; justify-content: space-between; font-size: 0.85rem; }
302
+ .filters__range-head output { color: var(--c-text-1, #fff); font-weight: 700; }
303
+ .filters__range input { width: 100%; accent-color: var(--c-primary-light, #d4a017); height: 24px; cursor: pointer; }
304
+ .filters__range-scale { display: flex; justify-content: space-between; font-size: 0.72rem; color: var(--c-text-2, #a3a3a3); }
305
+
306
+ .filters__swatches { display: flex; flex-wrap: wrap; gap: var(--sp-2, 0.5rem); padding-top: 2px; }
307
+ .filters__swatch { position: relative; display: grid; place-items: center; width: 40px; height: 40px; cursor: pointer; }
308
+ .filters__swatch-dot {
309
+ width: 28px;
310
+ height: 28px;
311
+ border-radius: 50%;
312
+ background: var(--sw);
313
+ box-shadow: 0 0 0 1px rgb(var(--ink-rgb, 255 255 255) / 0.2);
314
+ transition: box-shadow 0.15s ease, transform 0.15s ease;
315
+ }
316
+ .filters__swatch:hover .filters__swatch-dot { transform: scale(1.08); }
317
+ .filters__swatch input:checked + .filters__swatch-dot { box-shadow: 0 0 0 2px var(--c-bg-1, #0a0a0a), 0 0 0 4px var(--c-primary-light, #d4a017); }
318
+
319
+ .filters__switch {
320
+ position: relative;
321
+ display: flex;
322
+ align-items: center;
323
+ justify-content: space-between;
324
+ min-height: 44px;
325
+ font-size: 0.9rem;
326
+ color: var(--c-text-1, #fff);
327
+ cursor: pointer;
328
+ }
329
+ .filters__switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
330
+ .filters__track {
331
+ position: relative;
332
+ width: 42px;
333
+ height: 24px;
334
+ border-radius: 999px;
335
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.12);
336
+ transition: background 0.2s ease;
337
+ }
338
+ .filters__track::after {
339
+ content: '';
340
+ position: absolute;
341
+ top: 3px;
342
+ left: 3px;
343
+ width: 18px;
344
+ height: 18px;
345
+ border-radius: 50%;
346
+ background: var(--c-text-1, #fff);
347
+ transition: transform 0.2s ease;
348
+ }
349
+ .filters__switch input:checked + .filters__track { background: var(--c-primary-light, #d4a017); }
350
+ .filters__switch input:checked + .filters__track::after { transform: translateX(18px); }
351
+
352
+ .filters__reset {
353
+ min-height: 44px;
354
+ margin-top: var(--sp-2, 0.5rem);
355
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.12);
356
+ border-radius: var(--r-full, 9999px);
357
+ background: transparent;
358
+ color: var(--c-text-1, #fff);
359
+ font: inherit;
360
+ font-size: 0.85rem;
361
+ font-weight: 600;
362
+ cursor: pointer;
363
+ }
364
+ .filters__reset:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); }
365
+ .filters__reset:focus-visible, .filters__toggle:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
366
+
367
+ .filters__toggle {
368
+ display: none;
369
+ align-items: center;
370
+ gap: var(--sp-2, 0.5rem);
371
+ min-height: 44px;
372
+ padding: 0 var(--sp-4, 1rem);
373
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.12);
374
+ border-radius: var(--r-full, 9999px);
375
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.03);
376
+ color: var(--c-text-1, #fff);
377
+ font: inherit;
378
+ font-size: 0.9rem;
379
+ font-weight: 600;
380
+ cursor: pointer;
381
+ }
382
+ .filters__active {
383
+ min-width: 20px;
384
+ height: 20px;
385
+ padding: 0 6px;
386
+ border-radius: 999px;
387
+ background: var(--c-primary-light, #d4a017);
388
+ color: var(--c-on-primary, #050505);
389
+ font-size: 0.72rem;
390
+ font-weight: 800;
391
+ line-height: 20px;
392
+ }
393
+ .filters__active[hidden] { display: none; }
394
+
395
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
396
+
397
+ @container (max-width: 900px) {
398
+ .filters__toggle { display: inline-flex; }
399
+ .filters__form { display: none; margin-top: var(--sp-3, 0.75rem); }
400
+ .filters.is-open .filters__form { display: grid; }
401
+ }
402
+ @media (prefers-reduced-motion: reduce) {
403
+ .filters__track, .filters__track::after, .filters__swatch-dot { transition: none; }
404
+ }
405
+ </style>
@@ -0,0 +1,213 @@
1
+ ---
2
+ /**
3
+ * LiveActivityToast — Rotating social-proof notice ("Lucía in Madrid bought Aurora Headphones
4
+ * · 3 min ago") that slides in, waits and hides, cycling through `items`.
5
+ *
6
+ * @param {Array} items - `{ name, city, product, href?, icon?, hue?, ago }` entries to cycle through.
7
+ * @param {string} verb - Action text between person and product. Default 'just bought'.
8
+ * @param {number} interval - Time between notices in ms. Default 9000.
9
+ * @param {number} visibleFor - How long each notice stays in ms. Default 5000.
10
+ * @param {number} delay - Wait before the first notice in ms. Default 4000.
11
+ * @param {string} position - Corner (bottom-left|bottom-right). Default 'bottom-left'.
12
+ * @param {string} storageKey - sessionStorage key that remembers "stop showing". Default 'ack-activity-off'.
13
+ * @param {boolean} demo - Render statically with the first item (docs/previews). Default false.
14
+ * @param {string} variant - Style (glass|solid). Default 'glass'.
15
+ * @param {string} className - Extra class.
16
+ *
17
+ * Accessibility: not announced by screen readers (it is promotional, aria-live="off"), never
18
+ * steals focus, has a close button that stops it for the session, pauses on hover/focus and
19
+ * when the tab is hidden; respects prefers-reduced-motion.
20
+ */
21
+ import { productIcon } from './product-icons';
22
+
23
+ interface Activity { name: string; city: string; product: string; href?: string; icon?: string; hue?: number; ago: string }
24
+ interface Props {
25
+ items?: Activity[];
26
+ verb?: string;
27
+ interval?: number;
28
+ visibleFor?: number;
29
+ delay?: number;
30
+ position?: 'bottom-left' | 'bottom-right';
31
+ storageKey?: string;
32
+ demo?: boolean;
33
+ variant?: 'glass' | 'solid';
34
+ className?: string;
35
+ }
36
+
37
+ const {
38
+ items = [
39
+ { name: 'Lucía', city: 'Madrid', product: 'Aurora Wireless Headphones', icon: 'headphones', hue: 38, ago: '2 min ago' },
40
+ { name: 'Tom', city: 'Berlin', product: 'Trail 24L Backpack', icon: 'backpack', hue: 150, ago: '6 min ago' },
41
+ { name: 'Aiko', city: 'Osaka', product: 'Orbit Smart Watch', icon: 'watch', hue: 205, ago: '11 min ago' },
42
+ ],
43
+ verb = 'just bought',
44
+ interval = 9000,
45
+ visibleFor = 5000,
46
+ delay = 4000,
47
+ position = 'bottom-left',
48
+ storageKey = 'ack-activity-off',
49
+ demo = false,
50
+ variant = 'glass',
51
+ className = '',
52
+ } = Astro.props;
53
+
54
+ const first = items[0];
55
+ ---
56
+
57
+ <div
58
+ class:list={['live-activity', `live-activity--${position}`, `live-activity--${variant}`, { 'live-activity--demo is-visible': demo }, className]}
59
+ aria-live="off"
60
+ data-live-activity={demo ? undefined : JSON.stringify(items)}
61
+ data-verb={verb}
62
+ data-interval={interval}
63
+ data-visible={visibleFor}
64
+ data-delay={delay}
65
+ data-key={storageKey}
66
+ hidden={!demo}
67
+ >
68
+ {first && (
69
+ <>
70
+ <span class="live-activity__media" style={`--h: ${first.hue ?? 40}`} aria-hidden="true" data-media>
71
+ <svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"><Fragment set:html={productIcon(first.icon)} /></svg>
72
+ </span>
73
+ <a class="live-activity__body" href={first.href ?? '#'} data-link>
74
+ <span class="live-activity__line"><strong data-who>{first.name}</strong> <span data-city>in {first.city}</span> {verb}</span>
75
+ <span class="live-activity__product" data-product>{first.product}</span>
76
+ <span class="live-activity__ago"><span class="live-activity__dot" aria-hidden="true"></span><span data-ago>{first.ago}</span> · Verified</span>
77
+ </a>
78
+ <button class="live-activity__close" type="button" aria-label="Stop showing purchase notifications" data-close>×</button>
79
+ </>
80
+ )}
81
+ </div>
82
+
83
+ <script>
84
+ import { productIcon } from './product-icons';
85
+
86
+ document.querySelectorAll<HTMLElement>('[data-live-activity]:not([data-bound])').forEach((el) => {
87
+ el.dataset.bound = '';
88
+ const key = el.dataset.key || 'ack-activity-off';
89
+ try { if (window.sessionStorage.getItem(key)) return; } catch { /* storage blocked */ }
90
+ let items: Array<Record<string, any>> = [];
91
+ try { items = JSON.parse(el.dataset.liveActivity || '[]'); } catch { return; }
92
+ if (!items.length) return;
93
+
94
+ const q = (s: string) => el.querySelector<HTMLElement>(s);
95
+ const media = q('[data-media]');
96
+ const link = q('[data-link]') as HTMLAnchorElement | null;
97
+ const verb = el.dataset.verb || 'just bought';
98
+ const interval = Number(el.dataset.interval) || 9000;
99
+ const visibleFor = Number(el.dataset.visible) || 5000;
100
+ let index = 0;
101
+ let paused = false;
102
+ let stopped = false;
103
+ let hideTimer = 0;
104
+
105
+ function fill(item: Record<string, any>) {
106
+ q('[data-who]')!.textContent = item.name;
107
+ q('[data-city]')!.textContent = `in ${item.city}`;
108
+ q('[data-product]')!.textContent = item.product;
109
+ q('[data-ago]')!.textContent = item.ago;
110
+ if (link) link.href = item.href || '#';
111
+ if (media) {
112
+ media.style.setProperty('--h', String(item.hue ?? 40));
113
+ media.innerHTML = `<svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round">${productIcon(item.icon)}</svg>`;
114
+ }
115
+ const line = q('.live-activity__line');
116
+ if (line && line.lastChild) line.lastChild.textContent = ` ${verb}`;
117
+ }
118
+
119
+ function cycle() {
120
+ if (stopped) return;
121
+ if (paused || document.hidden) { window.setTimeout(cycle, 1500); return; }
122
+ fill(items[index % items.length]);
123
+ index++;
124
+ el.hidden = false;
125
+ requestAnimationFrame(() => el.classList.add('is-visible'));
126
+ hideTimer = window.setTimeout(hide, visibleFor);
127
+ }
128
+ function hide() {
129
+ if (paused) { hideTimer = window.setTimeout(hide, 1000); return; }
130
+ el.classList.remove('is-visible');
131
+ window.setTimeout(() => { el.hidden = true; }, 300);
132
+ window.setTimeout(cycle, Math.max(1000, interval - visibleFor));
133
+ }
134
+
135
+ el.addEventListener('mouseenter', () => { paused = true; });
136
+ el.addEventListener('mouseleave', () => { paused = false; });
137
+ el.addEventListener('focusin', () => { paused = true; });
138
+ el.addEventListener('focusout', () => { paused = false; });
139
+ q('[data-close]')?.addEventListener('click', () => {
140
+ stopped = true;
141
+ window.clearTimeout(hideTimer);
142
+ el.classList.remove('is-visible');
143
+ el.hidden = true;
144
+ try { window.sessionStorage.setItem(key, '1'); } catch { /* ignore */ }
145
+ });
146
+
147
+ window.setTimeout(cycle, Number(el.dataset.delay) || 4000);
148
+ });
149
+ </script>
150
+
151
+ <style>
152
+ .live-activity {
153
+ position: fixed;
154
+ bottom: 1rem;
155
+ z-index: 1050;
156
+ display: grid;
157
+ grid-template-columns: 52px 1fr auto;
158
+ align-items: center;
159
+ gap: var(--sp-3, 0.75rem);
160
+ width: min(360px, calc(100vw - 2rem));
161
+ padding: var(--sp-3, 0.75rem);
162
+ border-radius: var(--r-lg, 20px);
163
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1);
164
+ background: rgb(var(--paper-rgb, 5 5 5) / 0.86);
165
+ backdrop-filter: blur(16px) saturate(1.4);
166
+ box-shadow: 0 24px 60px -24px rgba(0, 0, 0, 0.6);
167
+ opacity: 0;
168
+ transform: translateY(16px);
169
+ transition: opacity 0.3s ease, transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
170
+ }
171
+ .live-activity[hidden] { display: none; }
172
+ .live-activity.is-visible { opacity: 1; transform: none; }
173
+ .live-activity--bottom-left { left: 1rem; }
174
+ .live-activity--bottom-right { right: 1rem; }
175
+ .live-activity--solid { background: var(--c-bg-1, #0a0a0a); backdrop-filter: none; }
176
+ .live-activity--demo { position: relative; inset: auto; margin: 0 auto; }
177
+
178
+ .live-activity__media {
179
+ display: grid;
180
+ place-items: center;
181
+ width: 52px;
182
+ height: 52px;
183
+ border-radius: var(--r-md, 12px);
184
+ color: hsl(var(--h) var(--pm-ink-s, 80%) var(--pm-ink-l, 80%));
185
+ background: radial-gradient(circle at 50% 40%, hsl(var(--h) 70% 50% / 0.3), transparent 70%), var(--c-bg-2, #111);
186
+ }
187
+ .live-activity__media :global(svg) { width: 60%; }
188
+ .live-activity__body { display: grid; gap: 1px; min-width: 0; color: inherit; text-decoration: none; border-radius: var(--r-sm, 8px); }
189
+ .live-activity__body:hover .live-activity__product { text-decoration: underline; }
190
+ .live-activity__body:focus-visible, .live-activity__close:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
191
+ .live-activity__line { font-size: 0.8rem; color: var(--c-text-2, #a3a3a3); }
192
+ .live-activity__line strong { color: var(--c-text-1, #fff); }
193
+ .live-activity__product { font-size: 0.88rem; font-weight: 700; color: var(--c-text-1, #fff); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
194
+ .live-activity__ago { display: inline-flex; align-items: center; gap: 6px; font-size: 0.72rem; color: var(--c-text-2, #a3a3a3); }
195
+ .live-activity__dot { width: 6px; height: 6px; border-radius: 50%; background: var(--c-success, #34d399); box-shadow: 0 0 8px var(--c-success, #34d399); }
196
+ .live-activity__close {
197
+ align-self: start;
198
+ width: 32px;
199
+ height: 32px;
200
+ border: 0;
201
+ border-radius: 50%;
202
+ background: transparent;
203
+ color: var(--c-text-2, #a3a3a3);
204
+ font-size: 1.25rem;
205
+ line-height: 1;
206
+ cursor: pointer;
207
+ }
208
+ .live-activity__close:hover { color: var(--c-text-1, #fff); background: rgb(var(--ink-rgb, 255 255 255) / 0.06); }
209
+
210
+ @media (prefers-reduced-motion: reduce) {
211
+ .live-activity { transition: none; transform: none; }
212
+ }
213
+ </style>