astro-component-kit 0.3.1 → 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 (249) hide show
  1. package/README.md +133 -36
  2. package/package.json +17 -4
  3. package/src/components/index.ts +18 -202
  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/buttons/NeonBorderButton.astro +162 -0
  21. package/src/components/lib/buttons/RippleButton.astro +199 -0
  22. package/src/components/lib/buttons/SlideRevealButton.astro +159 -0
  23. package/src/components/lib/buttons/index.ts +4 -0
  24. package/src/components/lib/cards/GithubRepoCard.astro +176 -0
  25. package/src/components/lib/cards/PricingCompareMiniCard.astro +196 -0
  26. package/src/components/lib/cards/StatsTrendCard.astro +149 -0
  27. package/src/components/lib/cards/index.ts +3 -0
  28. package/src/components/lib/charts/ActivityRings.astro +105 -0
  29. package/src/components/lib/charts/AreaChart.astro +4 -4
  30. package/src/components/lib/charts/BarChart.astro +3 -3
  31. package/src/components/lib/charts/BoxPlotChart.astro +109 -0
  32. package/src/components/lib/charts/BubbleChart.astro +101 -0
  33. package/src/components/lib/charts/BulletChart.astro +160 -0
  34. package/src/components/lib/charts/BumpChart.astro +92 -0
  35. package/src/components/lib/charts/CandlestickChart.astro +105 -0
  36. package/src/components/lib/charts/ChartFrame.astro +373 -0
  37. package/src/components/lib/charts/DivergingBarChart.astro +98 -0
  38. package/src/components/lib/charts/DonutChart.astro +2 -2
  39. package/src/components/lib/charts/DumbbellChart.astro +92 -0
  40. package/src/components/lib/charts/ForecastLineChart.astro +125 -0
  41. package/src/components/lib/charts/FunnelChart.astro +122 -0
  42. package/src/components/lib/charts/GaugeChart.astro +111 -0
  43. package/src/components/lib/charts/GoalProgressChart.astro +97 -0
  44. package/src/components/lib/charts/GroupedBarChart.astro +125 -0
  45. package/src/components/lib/charts/HeatmapChart.astro +131 -0
  46. package/src/components/lib/charts/HistogramChart.astro +111 -0
  47. package/src/components/lib/charts/HorizontalBarChart.astro +155 -0
  48. package/src/components/lib/charts/KpiStatTile.astro +116 -0
  49. package/src/components/lib/charts/KpiTrendGrid.astro +60 -0
  50. package/src/components/lib/charts/LineChart.astro +160 -0
  51. package/src/components/lib/charts/LinearProgressGauge.astro +136 -0
  52. package/src/components/lib/charts/LollipopChart.astro +100 -0
  53. package/src/components/lib/charts/MeterGauge.astro +122 -0
  54. package/src/components/lib/charts/MetricDeltaTable.astro +125 -0
  55. package/src/components/lib/charts/PieChart.astro +135 -0
  56. package/src/components/lib/charts/PolarAreaChart.astro +85 -0
  57. package/src/components/lib/charts/PopulationPyramid.astro +122 -0
  58. package/src/components/lib/charts/PunchCardChart.astro +89 -0
  59. package/src/components/lib/charts/RadarChart.astro +94 -0
  60. package/src/components/lib/charts/RadarMetricChart.astro +168 -0
  61. package/src/components/lib/charts/RadialBarChart.astro +124 -0
  62. package/src/components/lib/charts/RangeAreaChart.astro +113 -0
  63. package/src/components/lib/charts/RetentionCohortTable.astro +126 -0
  64. package/src/components/lib/charts/SankeyChart.astro +128 -0
  65. package/src/components/lib/charts/ScatterPlot.astro +139 -0
  66. package/src/components/lib/charts/SegmentedBarChart.astro +119 -0
  67. package/src/components/lib/charts/SlopeChart.astro +105 -0
  68. package/src/components/lib/charts/SparklineChart.astro +87 -0
  69. package/src/components/lib/charts/StackedAreaChart.astro +130 -0
  70. package/src/components/lib/charts/StackedBarChart.astro +136 -0
  71. package/src/components/lib/charts/SunburstChart.astro +103 -0
  72. package/src/components/lib/charts/TreemapChart.astro +94 -0
  73. package/src/components/lib/charts/WaffleChart.astro +121 -0
  74. package/src/components/lib/charts/WaterfallChart.astro +118 -0
  75. package/src/components/lib/charts/chart-tooltip.ts +75 -0
  76. package/src/components/lib/charts/chart-utils.ts +271 -0
  77. package/src/components/lib/charts/index.ts +46 -0
  78. package/src/components/lib/data/ActivityHeatmapGrid.astro +157 -0
  79. package/src/components/lib/data/MetricComparisonRow.astro +109 -0
  80. package/src/components/lib/data/StatusTimelineFeed.astro +168 -0
  81. package/src/components/lib/data/index.ts +3 -0
  82. package/src/components/lib/ecommerce/CartDrawer.astro +483 -0
  83. package/src/components/lib/ecommerce/CartPageList.astro +290 -0
  84. package/src/components/lib/ecommerce/CartToast.astro +276 -0
  85. package/src/components/lib/ecommerce/CategoryTiles.astro +143 -0
  86. package/src/components/lib/ecommerce/CheckoutForm.astro +423 -0
  87. package/src/components/lib/ecommerce/CountdownBanner.astro +230 -0
  88. package/src/components/lib/ecommerce/FilterBar.astro +278 -0
  89. package/src/components/lib/ecommerce/FilterSidebar.astro +405 -0
  90. package/src/components/lib/ecommerce/LiveActivityToast.astro +213 -0
  91. package/src/components/lib/ecommerce/OrderSummary.astro +295 -0
  92. package/src/components/lib/ecommerce/ProductBuyBox.astro +247 -0
  93. package/src/components/lib/ecommerce/ProductCardMinimal.astro +229 -0
  94. package/src/components/lib/ecommerce/ProductGallery.astro +145 -0
  95. package/src/components/lib/ecommerce/ProductGrid.astro +118 -0
  96. package/src/components/lib/ecommerce/ProductMedia.astro +113 -0
  97. package/src/components/lib/ecommerce/ProductRating.astro +78 -0
  98. package/src/components/lib/ecommerce/ProductToolbar.astro +93 -0
  99. package/src/components/lib/ecommerce/PromoBar.astro +145 -0
  100. package/src/components/lib/ecommerce/StockIndicator.astro +127 -0
  101. package/src/components/lib/ecommerce/StoreHeader.astro +446 -0
  102. package/src/components/lib/ecommerce/StoreHero.astro +260 -0
  103. package/src/components/lib/ecommerce/StoreProductCard.astro +307 -0
  104. package/src/components/lib/ecommerce/TrustBadges.astro +101 -0
  105. package/src/components/lib/ecommerce/VariantPicker.astro +145 -0
  106. package/src/components/lib/ecommerce/cart-store.ts +151 -0
  107. package/src/components/lib/ecommerce/catalog-filter.ts +62 -0
  108. package/src/components/lib/ecommerce/index.ts +34 -0
  109. package/src/components/lib/ecommerce/product-icons.ts +28 -0
  110. package/src/components/lib/ecommerce/types.ts +70 -0
  111. package/src/components/lib/feedback/ProgressRingCounter.astro +118 -0
  112. package/src/components/lib/feedback/ToastNotificationGroup.astro +173 -0
  113. package/src/components/lib/feedback/index.ts +2 -0
  114. package/src/components/lib/forms/AnimatedInput.astro +4 -2
  115. package/src/components/lib/forms/CustomRadioGroup.astro +3 -1
  116. package/src/components/lib/forms/GlassInlineToggleGroup.astro +161 -0
  117. package/src/components/lib/forms/PasswordStrengthInput.astro +242 -0
  118. package/src/components/lib/forms/PhoneCountryInput.astro +145 -0
  119. package/src/components/lib/forms/TagsAutocompleteInput.astro +249 -0
  120. package/src/components/lib/forms/index.ts +5 -0
  121. package/src/components/lib/index.ts +2 -0
  122. package/src/components/lib/layout/BentoGrid3.astro +6 -4
  123. package/src/components/lib/layout/CenterFixedBox.astro +2 -2
  124. package/src/components/lib/layout/CenteredContainer.astro +4 -2
  125. package/src/components/lib/layout/ClusterHorizontal.astro +2 -2
  126. package/src/components/lib/layout/DiagonalSectionDivider.astro +69 -0
  127. package/src/components/lib/layout/FeatureHero.astro +4 -2
  128. package/src/components/lib/layout/FooterCenteredMinimal.astro +4 -2
  129. package/src/components/lib/layout/FullBleedSection.astro +2 -2
  130. package/src/components/lib/layout/GalleryGrid4.astro +2 -2
  131. package/src/components/lib/layout/HolyGrailGrid.astro +127 -0
  132. package/src/components/lib/layout/LandingSectionSplit.astro +5 -3
  133. package/src/components/lib/layout/MasonryGrid.astro +2 -2
  134. package/src/components/lib/layout/MinimalFooterGrid.astro +1 -1
  135. package/src/components/lib/layout/ScrollSnapSection.astro +1 -1
  136. package/src/components/lib/layout/SidebarLayoutWrapper.astro +4 -2
  137. package/src/components/lib/layout/SplitScreenLayout.astro +3 -3
  138. package/src/components/lib/layout/StepLayoutVertical.astro +1 -1
  139. package/src/components/lib/layout/StickySidebarWrapper.astro +4 -2
  140. package/src/components/lib/layout/index.ts +3 -1
  141. package/src/components/lib/navigation/QuickJumpDock.astro +125 -0
  142. package/src/components/lib/navigation/StepBreadcrumbBar.astro +151 -0
  143. package/src/components/lib/navigation/SubNavbarPills.astro +102 -0
  144. package/src/components/lib/navigation/index.ts +3 -0
  145. package/src/components/lib/overlays/CookiePreferencesDrawer.astro +226 -0
  146. package/src/components/lib/overlays/FloatingFeedbackWidget.astro +203 -0
  147. package/src/components/lib/overlays/ImageCompareSlider.astro +172 -0
  148. package/src/components/lib/overlays/index.ts +3 -0
  149. package/src/components/lib/sections/AgencyContactBrief.astro +194 -0
  150. package/src/components/lib/sections/AgencyFooterCta.astro +94 -0
  151. package/src/components/lib/sections/AgencyHeader.astro +161 -0
  152. package/src/components/lib/sections/AgencyHeroKinetic.astro +172 -0
  153. package/src/components/lib/sections/AgencyServicesList.astro +113 -0
  154. package/src/components/lib/sections/AgencyTeamGrid.astro +111 -0
  155. package/src/components/lib/sections/AgencyWorkGrid.astro +147 -0
  156. package/src/components/lib/sections/BlogGridSection.astro +133 -0
  157. package/src/components/lib/sections/ButtonsShowcase.astro +20 -18
  158. package/src/components/lib/sections/CardsShowcase.astro +28 -26
  159. package/src/components/lib/sections/CareersOpenings.astro +141 -0
  160. package/src/components/lib/sections/ChangelogSection.astro +151 -0
  161. package/src/components/lib/sections/ContactGlassForm.astro +10 -8
  162. package/src/components/lib/sections/ContactSplitMap.astro +141 -0
  163. package/src/components/lib/sections/CtaAppDownload.astro +13 -11
  164. package/src/components/lib/sections/CtaGlowBanner.astro +12 -10
  165. package/src/components/lib/sections/CtaNewsletterSection.astro +165 -0
  166. package/src/components/lib/sections/CtaSplitImage.astro +149 -0
  167. package/src/components/lib/sections/DataShowcase.astro +23 -21
  168. package/src/components/lib/sections/FaqAccordionSection.astro +8 -8
  169. package/src/components/lib/sections/FaqTwoColumn.astro +121 -0
  170. package/src/components/lib/sections/FeatureAlternatingRows.astro +192 -0
  171. package/src/components/lib/sections/FeatureBentoGrid.astro +13 -11
  172. package/src/components/lib/sections/FeatureIconGrid.astro +155 -0
  173. package/src/components/lib/sections/FeatureTabsShowcase.astro +182 -0
  174. package/src/components/lib/sections/FeatureTimelineFlow.astro +8 -6
  175. package/src/components/lib/sections/FeedbackShowcase.astro +15 -13
  176. package/src/components/lib/sections/FooterMegaSection.astro +157 -0
  177. package/src/components/lib/sections/FormsShowcase.astro +22 -20
  178. package/src/components/lib/sections/HeroDashboardPreview.astro +182 -0
  179. package/src/components/lib/sections/HeroGlass.astro +22 -20
  180. package/src/components/lib/sections/HeroGradient.astro +15 -13
  181. package/src/components/lib/sections/HeroMinimalCentered.astro +175 -0
  182. package/src/components/lib/sections/HeroShowcaseSplit.astro +306 -0
  183. package/src/components/lib/sections/HeroSplit.astro +13 -11
  184. package/src/components/lib/sections/HeroTerminal.astro +253 -0
  185. package/src/components/lib/sections/HeroWaitlist.astro +231 -0
  186. package/src/components/lib/sections/HowItWorksSteps.astro +180 -0
  187. package/src/components/lib/sections/IntegrationsGrid.astro +167 -0
  188. package/src/components/lib/sections/LayoutShowcase.astro +21 -19
  189. package/src/components/lib/sections/LogoCloudSection.astro +96 -0
  190. package/src/components/lib/sections/LogoWallGrid.astro +93 -0
  191. package/src/components/lib/sections/NavigationShowcase.astro +15 -13
  192. package/src/components/lib/sections/OverlaysShowcase.astro +21 -19
  193. package/src/components/lib/sections/PricingComparisonMatrix.astro +15 -13
  194. package/src/components/lib/sections/PricingSinglePlan.astro +159 -0
  195. package/src/components/lib/sections/PricingTierCards.astro +11 -9
  196. package/src/components/lib/sections/PricingToggleCards.astro +171 -0
  197. package/src/components/lib/sections/ShopCategoryHero.astro +103 -0
  198. package/src/components/lib/sections/ShopCollectionMosaic.astro +131 -0
  199. package/src/components/lib/sections/ShopFooterWordmark.astro +100 -0
  200. package/src/components/lib/sections/ShopHeaderEditorial.astro +396 -0
  201. package/src/components/lib/sections/ShopHeroEditorial.astro +151 -0
  202. package/src/components/lib/sections/ShopLookbook.astro +250 -0
  203. package/src/components/lib/sections/ShopNewsletterEditorial.astro +128 -0
  204. package/src/components/lib/sections/ShopProductRail.astro +150 -0
  205. package/src/components/lib/sections/ShopReviewsEditorial.astro +133 -0
  206. package/src/components/lib/sections/ShopValuesMarquee.astro +91 -0
  207. package/src/components/lib/sections/StatsCounterBand.astro +124 -0
  208. package/src/components/lib/sections/StatsDashboardSection.astro +85 -0
  209. package/src/components/lib/sections/StatsMetricShowcase.astro +9 -7
  210. package/src/components/lib/sections/TeamGridSection.astro +12 -10
  211. package/src/components/lib/sections/TestimonialCarousel.astro +167 -0
  212. package/src/components/lib/sections/TestimonialFeatured.astro +122 -0
  213. package/src/components/lib/sections/TestimonialGridSection.astro +188 -0
  214. package/src/components/lib/sections/TestimonialWallGlass.astro +11 -9
  215. package/src/components/lib/sections/TypographyShowcase.astro +10 -8
  216. package/src/components/lib/sections/index.ts +39 -0
  217. package/src/components/lib/templates/ecommerce/StoreCategoryPage.astro +160 -0
  218. package/src/components/lib/templates/ecommerce/StoreCheckoutPage.astro +98 -0
  219. package/src/components/lib/templates/ecommerce/StoreHomePage.astro +191 -0
  220. package/src/components/lib/templates/ecommerce/StoreLayout.astro +169 -0
  221. package/src/components/lib/templates/ecommerce/StoreProductPage.astro +165 -0
  222. package/src/components/lib/templates/ecommerce/data.ts +187 -0
  223. package/src/components/lib/templates/ecommerce/index.ts +14 -0
  224. package/src/components/lib/templates/ecommerce-editorial/EditorialBagPage.astro +52 -0
  225. package/src/components/lib/templates/ecommerce-editorial/EditorialCheckoutPage.astro +123 -0
  226. package/src/components/lib/templates/ecommerce-editorial/EditorialHomePage.astro +102 -0
  227. package/src/components/lib/templates/ecommerce-editorial/EditorialLayout.astro +216 -0
  228. package/src/components/lib/templates/ecommerce-editorial/EditorialProductPage.astro +205 -0
  229. package/src/components/lib/templates/ecommerce-editorial/EditorialShopPage.astro +112 -0
  230. package/src/components/lib/templates/ecommerce-editorial/data.ts +125 -0
  231. package/src/components/lib/templates/ecommerce-editorial/index.ts +16 -0
  232. package/src/components/lib/templates/saas-hotel/AppLayout.astro +83 -0
  233. package/src/components/lib/templates/saas-hotel/DashboardPage.astro +66 -0
  234. package/src/components/lib/templates/saas-hotel/GuestsPage.astro +65 -0
  235. package/src/components/lib/templates/saas-hotel/LandingPage.astro +213 -0
  236. package/src/components/lib/templates/saas-hotel/ReportsPage.astro +63 -0
  237. package/src/components/lib/templates/saas-hotel/ReservationsPage.astro +49 -0
  238. package/src/components/lib/templates/saas-hotel/RoomsPage.astro +48 -0
  239. package/src/components/lib/templates/saas-hotel/SettingsPage.astro +115 -0
  240. package/src/components/lib/templates/saas-hotel/SignInPage.astro +50 -0
  241. package/src/components/lib/templates/saas-hotel/data.ts +229 -0
  242. package/src/components/lib/templates/saas-hotel/index.ts +19 -0
  243. package/src/components/lib/templates/saas-hotel/lobby-theme.css +90 -0
  244. package/src/components/lib/typography/AnimatedCounterText.astro +102 -0
  245. package/src/components/lib/typography/CurvedPathHeading.astro +86 -0
  246. package/src/components/lib/typography/GradientTextMarquee.astro +105 -0
  247. package/src/components/lib/typography/HackerDecodeText.astro +85 -0
  248. package/src/components/lib/typography/index.ts +4 -0
  249. package/src/styles/global.css +113 -1
@@ -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
+ }
@@ -0,0 +1,118 @@
1
+ ---
2
+ /**
3
+ * ProgressRingCounter — Circular SVG progress ring with centered countdown or metric value.
4
+ *
5
+ * @param {number} value - Current counter or progress value. Default is 75.
6
+ * @param {number} max - Maximum value threshold. Default is 100.
7
+ * @param {number} size - Pixel diameter of the ring. Default is 96.
8
+ * @param {number} strokeWidth - Thickness of the ring stroke. Default is 6.
9
+ * @param {string} color - Primary gauge fill color. Default is '#d4a017'.
10
+ * @param {string} label - Subtitle label beneath figure. Default is 'COMPLETED'.
11
+ */
12
+ interface Props {
13
+ value?: number;
14
+ max?: number;
15
+ size?: number;
16
+ strokeWidth?: number;
17
+ color?: string;
18
+ label?: string;
19
+ }
20
+
21
+ const {
22
+ value = 75,
23
+ max = 100,
24
+ size = 96,
25
+ strokeWidth = 6,
26
+ color = '#d4a017',
27
+ label = 'COMPLETED',
28
+ } = Astro.props;
29
+
30
+ const radius = (size - strokeWidth) / 2;
31
+ const circumference = 2 * Math.PI * radius;
32
+ const percent = Math.min(100, Math.max(0, (value / max) * 100));
33
+ const strokeDashoffset = circumference - (percent / 100) * circumference;
34
+ ---
35
+
36
+ <div class="ring-counter" style={`--ring-size: ${size}px; --ring-color: ${color};`}>
37
+ <svg width={size} height={size} class="ring-counter__svg">
38
+ <circle
39
+ class="ring-counter__bg"
40
+ cx={size / 2}
41
+ cy={size / 2}
42
+ r={radius}
43
+ stroke-width={strokeWidth}
44
+ />
45
+ <circle
46
+ class="ring-counter__meter"
47
+ cx={size / 2}
48
+ cy={size / 2}
49
+ r={radius}
50
+ stroke-width={strokeWidth}
51
+ stroke-dasharray={circumference}
52
+ stroke-dashoffset={strokeDashoffset}
53
+ />
54
+ </svg>
55
+
56
+ <div class="ring-counter__center">
57
+ <span class="val">{Math.round(percent)}%</span>
58
+ {label && <span class="lbl">{label}</span>}
59
+ </div>
60
+ </div>
61
+
62
+ <style>
63
+ .ring-counter {
64
+ position: relative;
65
+ width: var(--ring-size);
66
+ height: var(--ring-size);
67
+ display: inline-flex;
68
+ align-items: center;
69
+ justify-content: center;
70
+ font-family: var(--font-sans, system-ui, sans-serif);
71
+ user-select: none;
72
+ }
73
+
74
+ .ring-counter__svg {
75
+ transform: rotate(-90deg);
76
+ width: 100%;
77
+ height: 100%;
78
+ }
79
+
80
+ .ring-counter__bg {
81
+ fill: none;
82
+ stroke: rgba(255, 255, 255, 0.08);
83
+ }
84
+
85
+ .ring-counter__meter {
86
+ fill: none;
87
+ stroke: var(--ring-color);
88
+ stroke-linecap: round;
89
+ transition: stroke-dashoffset 0.8s cubic-bezier(0.16, 1, 0.3, 1);
90
+ filter: drop-shadow(0 0 6px rgba(212, 160, 23, 0.35));
91
+ }
92
+
93
+ .ring-counter__center {
94
+ position: absolute;
95
+ inset: 0;
96
+ display: flex;
97
+ flex-direction: column;
98
+ align-items: center;
99
+ justify-content: center;
100
+ text-align: center;
101
+ }
102
+
103
+ .ring-counter__center .val {
104
+ font-size: 1.25rem;
105
+ font-weight: 800;
106
+ color: var(--c-text-1, #ffffff);
107
+ line-height: 1;
108
+ }
109
+
110
+ .ring-counter__center .lbl {
111
+ font-size: 0.55rem;
112
+ font-weight: 700;
113
+ letter-spacing: 0.08em;
114
+ color: var(--c-text-3, #737373);
115
+ margin-top: 4px;
116
+ text-transform: uppercase;
117
+ }
118
+ </style>