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,295 @@
1
+ ---
2
+ /**
3
+ * OrderSummary — Checkout sidebar: cart lines, promo code, subtotal, shipping, tax and total.
4
+ * Reads the cart store live and follows the shipping method chosen in CheckoutForm.
5
+ *
6
+ * @param {string} title - Heading. Default 'Order summary'.
7
+ * @param {string} cartKey - Cart storage key. Default 'ack-cart'.
8
+ * @param {ShippingRate[]} shippingRates - Rates matching CheckoutForm's `shipping` radios.
9
+ * @param {number} freeShippingThreshold - Subtotal that makes the first rate free (0 disables). Default 75.
10
+ * @param {number} taxRate - Tax as a fraction (0.08 = 8%). Default 0.08.
11
+ * @param {Record<string, number>} promoCodes - Code → percent discount. Default { ASTRO10: 10 }.
12
+ * @param {string} editHref - "Edit cart" link (optional).
13
+ * @param {string} currency - ISO currency. Default 'USD'.
14
+ * @param {string} locale - Number format locale. Default 'en-US'.
15
+ * @param {boolean} sticky - Stick while the form scrolls (desktop). Default true.
16
+ * @param {string} variant - Style (glass|plain). Default 'glass'.
17
+ * @param {string} className - Extra class.
18
+ *
19
+ * Emits `ack:totals` on document with `{ subtotal, discount, shipping, tax, total, formatted }`.
20
+ *
21
+ * Accessibility: the totals block is a polite live region; promo feedback is a status message.
22
+ */
23
+ import type { ShippingRate } from './types';
24
+
25
+ interface Props {
26
+ title?: string;
27
+ cartKey?: string;
28
+ shippingRates?: ShippingRate[];
29
+ freeShippingThreshold?: number;
30
+ taxRate?: number;
31
+ promoCodes?: Record<string, number>;
32
+ editHref?: string;
33
+ currency?: string;
34
+ locale?: string;
35
+ sticky?: boolean;
36
+ variant?: 'glass' | 'plain';
37
+ className?: string;
38
+ }
39
+
40
+ const {
41
+ title = 'Order summary',
42
+ cartKey = 'ack-cart',
43
+ shippingRates = [
44
+ { id: 'standard', label: 'Standard', eta: '3–5 business days', price: 6 },
45
+ { id: 'express', label: 'Express', eta: '1–2 business days', price: 14 },
46
+ ],
47
+ freeShippingThreshold = 75,
48
+ taxRate = 0.08,
49
+ promoCodes = { ASTRO10: 10 },
50
+ editHref,
51
+ currency = 'USD',
52
+ locale = 'en-US',
53
+ sticky = true,
54
+ variant = 'glass',
55
+ className = '',
56
+ } = Astro.props;
57
+
58
+ const config = JSON.stringify({ shippingRates, freeShippingThreshold, taxRate, promoCodes, currency, locale });
59
+ ---
60
+
61
+ <aside class:list={['o-summary', `o-summary--${variant}`, { 'o-summary--sticky': sticky }, className]} aria-labelledby="o-summary-title" data-order-summary data-key={cartKey} data-config={config}>
62
+ <div class="o-summary__head">
63
+ <h2 class="o-summary__title" id="o-summary-title">{title}</h2>
64
+ {editHref && <a class="o-summary__edit" href={editHref}>Edit cart</a>}
65
+ </div>
66
+
67
+ <ul class="o-summary__lines" data-lines aria-label="Items in your order"></ul>
68
+ <p class="o-summary__empty" data-empty hidden>Your cart is empty.</p>
69
+
70
+ <form class="o-summary__promo" data-promo novalidate>
71
+ <label class="sr-only" for="o-summary-code">Discount code</label>
72
+ <input id="o-summary-code" name="code" type="text" placeholder="Discount code" autocomplete="off" autocapitalize="characters" spellcheck="false" />
73
+ <button type="submit">Apply</button>
74
+ </form>
75
+ <p class="o-summary__promo-msg" role="status" data-promo-msg></p>
76
+
77
+ <dl class="o-summary__totals" aria-live="polite">
78
+ <div><dt>Subtotal</dt><dd data-t="subtotal">—</dd></div>
79
+ <div data-discount-row hidden><dt>Discount <span data-code></span></dt><dd data-t="discount">—</dd></div>
80
+ <div><dt>Shipping</dt><dd data-t="shipping">—</dd></div>
81
+ <div><dt>Estimated tax</dt><dd data-t="tax">—</dd></div>
82
+ <div class="o-summary__total"><dt>Total</dt><dd data-t="total">—</dd></div>
83
+ </dl>
84
+ </aside>
85
+
86
+ <script>
87
+ import { createCart, formatMoney } from './cart-store';
88
+ import { productIcon } from './product-icons';
89
+ import type { CartItem, ShippingRate } from './types';
90
+
91
+ document.querySelectorAll<HTMLElement>('[data-order-summary]:not([data-bound])').forEach((root) => {
92
+ root.dataset.bound = '';
93
+ const cfg = JSON.parse(root.dataset.config || '{}') as {
94
+ shippingRates: ShippingRate[]; freeShippingThreshold: number; taxRate: number;
95
+ promoCodes: Record<string, number>; currency: string; locale: string;
96
+ };
97
+ const money = { currency: cfg.currency, locale: cfg.locale };
98
+ const cart = createCart(root.dataset.key);
99
+ const list = root.querySelector<HTMLUListElement>('[data-lines]')!;
100
+ const empty = root.querySelector<HTMLElement>('[data-empty]')!;
101
+ const promo = root.querySelector<HTMLFormElement>('[data-promo]')!;
102
+ const promoMsg = root.querySelector<HTMLElement>('[data-promo-msg]')!;
103
+ const discountRow = root.querySelector<HTMLElement>('[data-discount-row]')!;
104
+ const codeOut = root.querySelector<HTMLElement>('[data-code]')!;
105
+ const out = (k: string) => root.querySelector<HTMLElement>(`[data-t="${k}"]`)!;
106
+ let code = '';
107
+ let shippingId = cfg.shippingRates[0]?.id;
108
+
109
+ const line = (i: CartItem) => {
110
+ const li = document.createElement('li');
111
+ li.className = 'o-line';
112
+ const media = document.createElement('span');
113
+ media.className = 'o-line__media';
114
+ media.style.setProperty('--h', String(i.hue ?? 40));
115
+ media.innerHTML = i.image
116
+ ? `<img src="${encodeURI(i.image)}" alt="">`
117
+ : `<svg viewBox="0 0 64 64" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${productIcon(i.icon)}</svg>`;
118
+ const qty = document.createElement('span');
119
+ qty.className = 'o-line__qty';
120
+ qty.textContent = String(i.qty);
121
+ qty.setAttribute('aria-hidden', 'true');
122
+ media.append(qty);
123
+ const info = document.createElement('span');
124
+ info.className = 'o-line__info';
125
+ const name = document.createElement('span');
126
+ name.className = 'o-line__name';
127
+ name.textContent = i.name;
128
+ const meta = document.createElement('span');
129
+ meta.className = 'o-line__meta';
130
+ meta.textContent = [i.variant, `Qty ${i.qty}`].filter(Boolean).join(' · ');
131
+ info.append(name, meta);
132
+ const price = document.createElement('span');
133
+ price.className = 'o-line__price';
134
+ price.textContent = formatMoney(i.price * i.qty, money);
135
+ li.append(media, info, price);
136
+ return li;
137
+ };
138
+
139
+ function render() {
140
+ const items = cart.items();
141
+ list.replaceChildren(...items.map(line));
142
+ empty.hidden = items.length > 0;
143
+ const subtotal = items.reduce((n, i) => n + i.price * i.qty, 0);
144
+ const pct = code ? cfg.promoCodes[code] ?? 0 : 0;
145
+ const discount = Math.round(subtotal * pct) / 100;
146
+ const rate = cfg.shippingRates.find((r) => r.id === shippingId) ?? cfg.shippingRates[0];
147
+ const freeStandard = cfg.freeShippingThreshold > 0 && subtotal - discount >= cfg.freeShippingThreshold && rate === cfg.shippingRates[0];
148
+ const shipping = items.length === 0 || !rate ? 0 : freeStandard ? 0 : rate.price;
149
+ const tax = Math.round((subtotal - discount) * cfg.taxRate * 100) / 100;
150
+ const total = Math.max(0, subtotal - discount + shipping + tax);
151
+
152
+ out('subtotal').textContent = formatMoney(subtotal, money);
153
+ out('discount').textContent = `−${formatMoney(discount, money)}`;
154
+ discountRow.hidden = discount === 0;
155
+ codeOut.textContent = code ? `(${code})` : '';
156
+ out('shipping').textContent = shipping === 0 && items.length ? 'Free' : formatMoney(shipping, money);
157
+ out('tax').textContent = formatMoney(tax, money);
158
+ out('total').textContent = formatMoney(total, money);
159
+
160
+ document.dispatchEvent(new CustomEvent('ack:totals', {
161
+ detail: { subtotal, discount, shipping, tax, total, formatted: formatMoney(total, money), freeStandard, count: items.length },
162
+ }));
163
+ }
164
+
165
+ promo.addEventListener('submit', (e) => {
166
+ e.preventDefault();
167
+ const input = promo.elements.namedItem('code') as HTMLInputElement;
168
+ const value = input.value.trim().toUpperCase();
169
+ if (!value) { promoMsg.textContent = 'Enter a code first.'; promoMsg.dataset.state = 'error'; return; }
170
+ if (cfg.promoCodes[value] !== undefined) {
171
+ code = value;
172
+ promoMsg.textContent = `${value} applied — ${cfg.promoCodes[value]}% off.`;
173
+ promoMsg.dataset.state = 'ok';
174
+ } else {
175
+ promoMsg.textContent = `"${value}" is not a valid code.`;
176
+ promoMsg.dataset.state = 'error';
177
+ }
178
+ render();
179
+ });
180
+
181
+ document.addEventListener('change', (e) => {
182
+ const t = e.target as HTMLInputElement;
183
+ if (t?.name === 'shipping' && t.checked) { shippingId = t.value; render(); }
184
+ });
185
+ document.addEventListener('ack:totals-request', render);
186
+
187
+ render();
188
+ cart.subscribe(render);
189
+ });
190
+ </script>
191
+
192
+ <style>
193
+ .o-summary {
194
+ display: grid;
195
+ gap: var(--sp-4, 1rem);
196
+ align-self: start;
197
+ min-width: 0;
198
+ }
199
+ .o-summary--glass {
200
+ padding: var(--sp-6, 1.5rem);
201
+ border-radius: var(--r-xl, 28px);
202
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.07);
203
+ background: linear-gradient(180deg, rgb(var(--ink-rgb, 255 255 255) / 0.035), rgb(var(--ink-rgb, 255 255 255) / 0.012));
204
+ }
205
+ .o-summary--sticky { position: sticky; top: calc(var(--store-header-h, 68px) + 1.5rem); }
206
+
207
+ .o-summary__head { display: flex; justify-content: space-between; align-items: baseline; }
208
+ .o-summary__title { margin: 0; font-size: 1.15rem; font-weight: 800; color: var(--c-text-1, #fff); }
209
+ .o-summary__edit { font-size: 0.85rem; font-weight: 600; }
210
+
211
+ .o-summary__lines { display: grid; gap: var(--sp-3, 0.75rem); margin: 0; padding: 0; list-style: none; }
212
+ .o-summary__lines :global(.o-line) { display: grid; grid-template-columns: 60px 1fr auto; gap: var(--sp-3, 0.75rem); align-items: center; }
213
+ .o-summary__lines :global(.o-line__media) {
214
+ position: relative;
215
+ display: grid;
216
+ place-items: center;
217
+ width: 60px;
218
+ height: 60px;
219
+ border-radius: var(--r-md, 12px);
220
+ color: hsl(var(--h) var(--pm-ink-s, 80%) var(--pm-ink-l, 80%));
221
+ background: radial-gradient(circle at 50% 40%, hsl(var(--h) 70% 50% / 0.3), transparent 70%), var(--c-bg-2, #111);
222
+ border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05));
223
+ }
224
+ .o-summary__lines :global(.o-line__media svg) { width: 58%; }
225
+ .o-summary__lines :global(.o-line__media img) { width: 100%; height: 100%; object-fit: cover; border-radius: inherit; }
226
+ .o-summary__lines :global(.o-line__qty) {
227
+ position: absolute;
228
+ top: -7px;
229
+ right: -7px;
230
+ min-width: 22px;
231
+ height: 22px;
232
+ padding: 0 6px;
233
+ border-radius: 999px;
234
+ background: var(--c-text-1, #fff);
235
+ color: var(--c-bg, #050505);
236
+ font-size: 0.72rem;
237
+ font-weight: 800;
238
+ line-height: 22px;
239
+ text-align: center;
240
+ }
241
+ .o-summary__lines :global(.o-line__info) { display: grid; gap: 2px; min-width: 0; }
242
+ .o-summary__lines :global(.o-line__name) { font-size: 0.88rem; font-weight: 700; color: var(--c-text-1, #fff); line-height: 1.3; }
243
+ .o-summary__lines :global(.o-line__meta) { font-size: 0.78rem; color: var(--c-text-2, #a3a3a3); }
244
+ .o-summary__lines :global(.o-line__price) { font-size: 0.9rem; font-weight: 700; color: var(--c-text-1, #fff); }
245
+ .o-summary__empty { margin: 0; color: var(--c-text-2, #a3a3a3); }
246
+
247
+ .o-summary__promo { display: flex; gap: var(--sp-2, 0.5rem); padding-top: var(--sp-4, 1rem); border-top: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05)); }
248
+ .o-summary__promo input {
249
+ flex: 1;
250
+ min-width: 0;
251
+ min-height: 46px;
252
+ padding: 0 var(--sp-4, 1rem);
253
+ border-radius: var(--r-md, 12px);
254
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.1);
255
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.03);
256
+ color: var(--c-text-1, #fff);
257
+ font: inherit;
258
+ font-size: 0.9rem;
259
+ text-transform: uppercase;
260
+ }
261
+ .o-summary__promo input::placeholder { text-transform: none; color: var(--c-text-2, #a3a3a3); }
262
+ .o-summary__promo input:focus { outline: none; border-color: var(--c-primary-light, #d4a017); }
263
+ .o-summary__promo button {
264
+ min-height: 46px;
265
+ padding: 0 var(--sp-5, 1.25rem);
266
+ border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.14);
267
+ border-radius: var(--r-md, 12px);
268
+ background: rgb(var(--ink-rgb, 255 255 255) / 0.05);
269
+ color: var(--c-text-1, #fff);
270
+ font: inherit;
271
+ font-weight: 700;
272
+ font-size: 0.88rem;
273
+ cursor: pointer;
274
+ }
275
+ .o-summary__promo button:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.09); }
276
+ .o-summary__promo button:focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
277
+ .o-summary__promo-msg { margin: calc(var(--sp-2, 0.5rem) * -1) 0 0; min-height: 1em; font-size: 0.8rem; }
278
+ .o-summary__promo-msg:empty { display: none; }
279
+ .o-summary__promo-msg[data-state='ok'] { color: var(--c-success, #34d399); }
280
+ .o-summary__promo-msg[data-state='error'] { color: var(--c-danger, #f87171); }
281
+
282
+ .o-summary__totals { display: grid; gap: var(--sp-2, 0.5rem); margin: 0; padding-top: var(--sp-4, 1rem); border-top: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05)); }
283
+ .o-summary__totals div { display: flex; justify-content: space-between; font-size: 0.9rem; }
284
+ .o-summary__totals div[hidden] { display: none; }
285
+ .o-summary__totals dt { color: var(--c-text-2, #a3a3a3); }
286
+ .o-summary__totals dd { margin: 0; color: var(--c-text-1, #fff); font-weight: 600; }
287
+ .o-summary__totals [data-discount-row] dd { color: var(--c-success, #34d399); }
288
+ .o-summary__total { margin-top: var(--sp-2, 0.5rem); padding-top: var(--sp-3, 0.75rem); border-top: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05)); }
289
+ .o-summary__totals .o-summary__total dt { color: var(--c-text-1, #fff); font-weight: 700; font-size: 1rem; }
290
+ .o-summary__totals .o-summary__total dd { font-size: 1.4rem; font-weight: 900; letter-spacing: -0.02em; }
291
+
292
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
293
+
294
+ @media (max-width: 960px) { .o-summary--sticky { position: static; } }
295
+ </style>
@@ -0,0 +1,247 @@
1
+ ---
2
+ /**
3
+ * ProductBuyBox — Product detail purchase panel: title, rating, price, colour/size pickers,
4
+ * quantity (StepperInput), add to cart, buy now, guarantees and expandable details.
5
+ *
6
+ * @param {Product} product - Product data. A demo product is used by default.
7
+ * @param {string} categoryLabel - Eyebrow text (e.g. the category name).
8
+ * @param {string} checkoutHref - Where "Buy now" goes after adding the item. Default '#'.
9
+ * @param {Array<{title: string, body: string}>} details - Expandable sections (materials, shipping…).
10
+ * @param {string} currency - ISO currency. Default 'USD'.
11
+ * @param {string} locale - Number format locale. Default 'en-US'.
12
+ * @param {string} cartKey - Cart storage key. Default 'ack-cart'.
13
+ * @param {string} id - Base id for inner controls. Default 'buy-box'.
14
+ * @param {string} variant - Style (plain|glass). Default 'plain'.
15
+ * @param {string} size - Type scale (md|lg). Default 'md'.
16
+ * @param {string} className - Extra class.
17
+ *
18
+ * Accessibility: one h1 with the product name; pickers are radio groups; details use native
19
+ * <details>/<summary>; sold-out state disables the buttons and says so in text.
20
+ */
21
+ import Badge from '../feedback/Badge.astro';
22
+ import StepperInput from '../forms/StepperInput.astro';
23
+ import ProductRating from './ProductRating.astro';
24
+ import VariantPicker from './VariantPicker.astro';
25
+ import TrustBadges from './TrustBadges.astro';
26
+ import StockIndicator from './StockIndicator.astro';
27
+ import { formatMoney } from './cart-store';
28
+ import type { Product } from './types';
29
+
30
+ interface Props {
31
+ product?: Product;
32
+ categoryLabel?: string;
33
+ checkoutHref?: string;
34
+ details?: Array<{ title: string; body: string }>;
35
+ currency?: string;
36
+ locale?: string;
37
+ cartKey?: string;
38
+ id?: string;
39
+ variant?: 'plain' | 'glass';
40
+ size?: 'md' | 'lg';
41
+ className?: string;
42
+ }
43
+
44
+ const {
45
+ product = {
46
+ slug: 'aurora-headphones',
47
+ name: 'Aurora Wireless Headphones',
48
+ price: 189,
49
+ compareAt: 229,
50
+ category: 'audio',
51
+ rating: 4.8,
52
+ reviews: 312,
53
+ badge: 'Bestseller',
54
+ badgeVariant: 'warning',
55
+ colors: [{ name: 'Graphite', hex: '#2b2b2f' }, { name: 'Sand', hex: '#d8c3a5' }, { name: 'Ocean', hex: '#3d6f9e' }],
56
+ icon: 'headphones',
57
+ hue: 38,
58
+ description: 'Adaptive noise cancelling, 40-hour battery and memory-foam cushions that disappear after five minutes. Tuned in-house for a warm, detailed sound.',
59
+ inStock: true,
60
+ },
61
+ categoryLabel = 'Audio',
62
+ checkoutHref = '#',
63
+ details = [
64
+ { title: 'Features', body: 'Hybrid ANC with transparency mode · Bluetooth 5.4 multipoint · USB-C fast charge (10 min = 5 h) · Foldable aluminium frame.' },
65
+ { title: 'Shipping & returns', body: 'Free standard shipping over $75, delivered in 2–4 business days. Return anything within 30 days for a full refund.' },
66
+ { title: 'Warranty', body: 'Two-year limited warranty covering manufacturing defects, with free repair or replacement.' },
67
+ ],
68
+ currency = 'USD',
69
+ locale = 'en-US',
70
+ cartKey = 'ack-cart',
71
+ id = 'buy-box',
72
+ variant = 'plain',
73
+ size = 'md',
74
+ className = '',
75
+ } = Astro.props;
76
+
77
+ const money = { currency, locale };
78
+ const inStock = product.inStock !== false;
79
+ const saving = product.compareAt ? Math.round((1 - product.price / product.compareAt) * 100) : 0;
80
+ const payload = JSON.stringify({ id: product.slug, name: product.name, price: product.price, icon: product.icon, hue: product.hue, image: product.image });
81
+ const addId = `${id}-add`;
82
+ ---
83
+
84
+ <div class:list={['buy-box', `buy-box--${variant}`, `buy-box--${size}`, className]}>
85
+ <div class="buy-box__top">
86
+ {categoryLabel && <p class="buy-box__cat">{categoryLabel}</p>}
87
+ {product.badge && <Badge variant={product.badgeVariant ?? 'default'}>{product.badge}</Badge>}
88
+ </div>
89
+ <h1 class="buy-box__title">{product.name}</h1>
90
+ {product.rating !== undefined && (
91
+ <a class="buy-box__rating" href="#reviews">
92
+ <ProductRating value={product.rating} count={product.reviews} size="md" />
93
+ </a>
94
+ )}
95
+
96
+ <p class="buy-box__price">
97
+ <span class="buy-box__now">{formatMoney(product.price, money)}</span>
98
+ {product.compareAt && <s class="buy-box__was"><span class="sr-only">Was </span>{formatMoney(product.compareAt, money)}</s>}
99
+ {saving > 0 && <span class="buy-box__save">Save {saving}%</span>}
100
+ </p>
101
+ {product.description && <p class="buy-box__desc">{product.description}</p>}
102
+
103
+ <div class="buy-box__pickers">
104
+ {product.colors && product.colors.length > 0 && (
105
+ <VariantPicker label="Colour" name={`${id}-color`} type="swatch" target={addId} options={product.colors.map((c) => ({ label: c.name, hex: c.hex }))} />
106
+ )}
107
+ {product.sizes && product.sizes.length > 0 && (
108
+ <VariantPicker label="Size" name={`${id}-size`} type="pill" target={addId} options={product.sizes.map((s) => ({ label: s }))} />
109
+ )}
110
+ </div>
111
+
112
+ <div class="buy-box__actions">
113
+ <StepperInput id={`${id}-qty`} name="qty" value={1} min={1} max={10} />
114
+ <button
115
+ class="buy-box__add"
116
+ id={addId}
117
+ type="button"
118
+ data-add-to-cart={payload}
119
+ data-cart-key={cartKey}
120
+ data-qty-from={`${id}-qty`}
121
+ data-added-label="Added to cart"
122
+ disabled={!inStock}
123
+ >
124
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 7h12l-1 13H7z"/><path d="M9 7V6a3 3 0 0 1 6 0v1"/></svg>
125
+ <span data-label>{inStock ? 'Add to cart' : 'Sold out'}</span>
126
+ </button>
127
+ </div>
128
+ {inStock && (
129
+ <a class="buy-box__buy" href={checkoutHref} data-buy-now={addId}>Buy now</a>
130
+ )}
131
+
132
+ {inStock && product.stock !== undefined && product.stock <= 5 ? (
133
+ <StockIndicator stock={product.stock} viewers={8 + (product.reviews ?? 0) % 17} />
134
+ ) : (
135
+ <p class="buy-box__stock" data-in={inStock ? '' : undefined}>
136
+ <span aria-hidden="true"></span>{inStock ? 'In stock — ships in 24 hours' : 'Out of stock — back soon'}
137
+ </p>
138
+ )}
139
+
140
+ <TrustBadges variant="inline" size="sm" items={[
141
+ { icon: 'truck', title: 'Free shipping', text: 'Over $75' },
142
+ { icon: 'return', title: '30-day returns', text: 'Free & easy' },
143
+ ]} />
144
+
145
+ {details.length > 0 && (
146
+ <div class="buy-box__details">
147
+ {details.map((d, i) => (
148
+ <details open={i === 0}>
149
+ <summary>{d.title}</summary>
150
+ <p>{d.body}</p>
151
+ </details>
152
+ ))}
153
+ </div>
154
+ )}
155
+ </div>
156
+
157
+ <script>
158
+ import { bindAddToCart } from './cart-store';
159
+
160
+ bindAddToCart(document);
161
+ document.querySelectorAll<HTMLAnchorElement>('[data-buy-now]').forEach((link) => {
162
+ link.addEventListener('click', (e) => {
163
+ const btn = document.getElementById(link.dataset.buyNow || '') as HTMLButtonElement | null;
164
+ if (!btn || btn.disabled) return;
165
+ e.preventDefault();
166
+ btn.dataset.openCart = 'false';
167
+ btn.click();
168
+ window.location.href = link.href;
169
+ });
170
+ });
171
+ </script>
172
+
173
+ <style>
174
+ .buy-box { display: flex; flex-direction: column; gap: var(--sp-4, 1rem); min-width: 0; }
175
+ .buy-box--glass {
176
+ padding: var(--sp-6, 1.5rem);
177
+ border-radius: var(--r-xl, 28px);
178
+ border: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05));
179
+ background: var(--c-surface, rgb(var(--ink-rgb, 255 255 255) / 0.02));
180
+ }
181
+ .buy-box__top { display: flex; align-items: center; gap: var(--sp-3, 0.75rem); }
182
+ .buy-box__cat { margin: 0; font-size: 0.75rem; font-weight: 800; letter-spacing: 0.1em; text-transform: uppercase; color: var(--c-primary-light, #d4a017); }
183
+ .buy-box__title { margin: 0; font-size: clamp(1.8rem, 3.6vw, 2.6rem); font-weight: 900; line-height: 1.08; letter-spacing: -0.035em; color: var(--c-text-1, #fff); }
184
+ .buy-box--lg .buy-box__title { font-size: clamp(2.2rem, 4.4vw, 3.2rem); }
185
+ .buy-box__rating { width: fit-content; text-decoration: none; border-radius: var(--r-sm, 8px); }
186
+ .buy-box__rating:hover :global(.p-rating__count) { text-decoration: underline; }
187
+
188
+ .buy-box__price { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--sp-3, 0.75rem); margin: 0; }
189
+ .buy-box__now { font-size: 1.9rem; font-weight: 900; color: var(--c-text-1, #fff); letter-spacing: -0.02em; }
190
+ .buy-box__was { font-size: 1.05rem; color: var(--c-text-2, #a3a3a3); }
191
+ .buy-box__save { padding: 0.2rem 0.6rem; border-radius: var(--r-full, 9999px); background: rgba(52, 211, 153, 0.12); color: var(--c-success, #34d399); font-size: 0.78rem; font-weight: 800; }
192
+ .buy-box__desc { margin: 0; line-height: 1.7; color: var(--c-text-2, #a3a3a3); }
193
+
194
+ .buy-box__pickers { display: grid; gap: var(--sp-5, 1.25rem); padding: var(--sp-2, 0.5rem) 0; }
195
+
196
+ .buy-box__actions { display: flex; gap: var(--sp-3, 0.75rem); align-items: stretch; }
197
+ .buy-box__actions :global(.stepper-container) { width: auto; flex-shrink: 0; }
198
+ .buy-box__actions :global(.stepper) { height: 52px; border-radius: var(--r-full, 9999px); border-color: rgb(var(--ink-rgb, 255 255 255) / 0.12); }
199
+ .buy-box__actions :global(.stepper-btn) { width: 48px; height: 100%; background: transparent; color: var(--c-text-1, #fff); }
200
+ .buy-box__actions :global(.stepper-btn:hover) { background: rgb(var(--ink-rgb, 255 255 255) / 0.07); }
201
+ .buy-box__actions :global(.stepper-input) { width: 44px; height: 100%; }
202
+ .buy-box__add,
203
+ .buy-box__buy {
204
+ display: inline-flex;
205
+ align-items: center;
206
+ justify-content: center;
207
+ gap: var(--sp-2, 0.5rem);
208
+ min-height: 52px;
209
+ border-radius: var(--r-full, 9999px);
210
+ font: inherit;
211
+ font-weight: 800;
212
+ font-size: 0.95rem;
213
+ text-decoration: none;
214
+ cursor: pointer;
215
+ transition: filter 0.2s ease, transform 0.2s ease, background 0.2s ease;
216
+ }
217
+ .buy-box__add { flex: 1; border: 0; background: var(--c-primary-light, #d4a017); color: var(--c-on-primary, #050505); }
218
+ .buy-box__add:hover { filter: brightness(1.08); transform: translateY(-1px); }
219
+ .buy-box__add.is-added { background: var(--c-success, #34d399); }
220
+ .buy-box__add:disabled { background: rgb(var(--ink-rgb, 255 255 255) / 0.08); color: var(--c-text-2, #a3a3a3); cursor: not-allowed; transform: none; }
221
+ .buy-box__buy { border: 1px solid rgb(var(--ink-rgb, 255 255 255) / 0.16); color: var(--c-text-1, #fff); }
222
+ .buy-box__buy:hover { background: rgb(var(--ink-rgb, 255 255 255) / 0.05); color: var(--c-text-1, #fff); }
223
+ .buy-box :is(a, button, summary):focus-visible { outline: 2px solid var(--c-primary-light, #d4a017); outline-offset: 2px; }
224
+
225
+ .buy-box__stock { display: flex; align-items: center; gap: var(--sp-2, 0.5rem); margin: 0; font-size: 0.85rem; color: var(--c-danger, #f87171); }
226
+ .buy-box__stock[data-in] { color: var(--c-success, #34d399); }
227
+ .buy-box__stock span { width: 8px; height: 8px; border-radius: 50%; background: currentColor; box-shadow: 0 0 10px currentColor; }
228
+
229
+ .buy-box__details { border-top: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05)); }
230
+ .buy-box__details details { border-bottom: 1px solid var(--c-border, rgb(var(--ink-rgb, 255 255 255) / 0.05)); }
231
+ .buy-box__details summary {
232
+ display: flex;
233
+ align-items: center;
234
+ justify-content: space-between;
235
+ min-height: 52px;
236
+ font-weight: 700;
237
+ color: var(--c-text-1, #fff);
238
+ cursor: pointer;
239
+ list-style: none;
240
+ }
241
+ .buy-box__details summary::-webkit-details-marker { display: none; }
242
+ .buy-box__details summary::after { content: '+'; font-size: 1.3rem; font-weight: 400; color: var(--c-text-2, #a3a3a3); transition: transform 0.2s ease; }
243
+ .buy-box__details details[open] summary::after { transform: rotate(45deg); }
244
+ .buy-box__details p { margin: 0 0 var(--sp-4, 1rem); font-size: 0.9rem; line-height: 1.7; color: var(--c-text-2, #a3a3a3); }
245
+
246
+ .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
247
+ </style>