create-brainerce-store 1.51.0 → 1.52.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 (170) hide show
  1. package/README.md +72 -0
  2. package/dist/index.js +9 -68
  3. package/messages/en.json +32 -0
  4. package/messages/he.json +32 -0
  5. package/package.json +1 -1
  6. package/templates/nextjs/base/.claude/commands/design.md +9 -0
  7. package/templates/nextjs/base/AI-GUIDE.md +52 -3
  8. package/templates/nextjs/base/CLAUDE.md +10 -0
  9. package/templates/nextjs/base/package.json.ejs +52 -39
  10. package/templates/nextjs/base/src/app/globals.css +36 -30
  11. package/templates/nextjs/base/src/components/checkout/payment-step.tsx +14 -10
  12. package/templates/nextjs/base/src/components/ui/accordion.tsx +53 -0
  13. package/templates/nextjs/base/src/components/ui/badge.tsx +32 -0
  14. package/templates/nextjs/base/src/components/ui/button.tsx +49 -0
  15. package/templates/nextjs/base/src/components/ui/card.tsx +55 -0
  16. package/templates/nextjs/base/src/components/ui/checkbox.tsx +28 -0
  17. package/templates/nextjs/base/src/components/ui/dialog.tsx +107 -0
  18. package/templates/nextjs/base/src/components/ui/input.tsx +22 -0
  19. package/templates/nextjs/base/src/components/ui/label.tsx +21 -0
  20. package/templates/nextjs/base/src/components/ui/radio-group.tsx +38 -0
  21. package/templates/nextjs/base/src/components/ui/select.tsx +153 -0
  22. package/templates/nextjs/base/src/components/ui/separator.tsx +26 -0
  23. package/templates/nextjs/base/src/components/ui/sheet.tsx +125 -0
  24. package/templates/nextjs/base/src/components/ui/skeleton.tsx +7 -0
  25. package/templates/nextjs/base/src/components/ui/tabs.tsx +55 -0
  26. package/templates/nextjs/base/src/components/ui/textarea.tsx +21 -0
  27. package/templates/nextjs/base/src/components/ui/tooltip.tsx +32 -0
  28. package/templates/nextjs/base/src/ui/cart/cart-bundle-offer.tsx +17 -11
  29. package/templates/nextjs/base/src/ui/cart/cart-item.tsx +2 -8
  30. package/templates/nextjs/base/src/ui/cart/cart-nudges.tsx +28 -39
  31. package/templates/nextjs/base/src/ui/cart/cart-summary.tsx +3 -1
  32. package/templates/nextjs/base/src/ui/cart/cart-upgrade-banner.tsx +8 -23
  33. package/templates/nextjs/base/src/ui/cart/cart-view.tsx +18 -27
  34. package/templates/nextjs/base/src/ui/cart/coupon-input.tsx +10 -21
  35. package/templates/nextjs/base/src/ui/cart/free-shipping-bar.tsx +2 -3
  36. package/templates/nextjs/base/src/ui/cart/reservation-countdown.tsx +95 -103
  37. package/templates/nextjs/base/src/ui/home/hero-section.tsx +4 -6
  38. package/templates/nextjs/base/src/ui/layout/announcement-bar.tsx.ejs +116 -125
  39. package/templates/nextjs/base/src/ui/layout/faq-section.tsx.ejs +90 -103
  40. package/templates/nextjs/base/src/ui/layout/header-account.tsx +2 -17
  41. package/templates/nextjs/base/src/ui/layout/language-switcher.tsx.ejs +80 -70
  42. package/templates/nextjs/base/src/ui/layout/site-footer.tsx.ejs +144 -164
  43. package/templates/nextjs/base/src/ui/layout/site-header.tsx.ejs +152 -182
  44. package/templates/nextjs/base/src/ui/product/customization-fields.tsx +456 -444
  45. package/templates/nextjs/base/src/ui/product/discount-badge.tsx +18 -22
  46. package/templates/nextjs/base/src/ui/product/frequently-bought-together.tsx +15 -27
  47. package/templates/nextjs/base/src/ui/product/modifier-group-selector.tsx +206 -175
  48. package/templates/nextjs/base/src/ui/product/price-display.tsx +3 -2
  49. package/templates/nextjs/base/src/ui/product/product-card.tsx +238 -271
  50. package/templates/nextjs/base/src/ui/product/product-client-section.tsx +455 -470
  51. package/templates/nextjs/base/src/ui/product/product-listing.tsx +96 -79
  52. package/templates/nextjs/base/src/ui/product/recommendation-section.tsx +102 -107
  53. package/templates/nextjs/base/src/ui/product/review-form.tsx +44 -43
  54. package/templates/nextjs/{designs/passepartout → base/src}/ui/product/reviews-section.tsx.ejs +108 -96
  55. package/templates/nextjs/base/src/ui/product/stock-badge.tsx +61 -63
  56. package/templates/nextjs/base/tailwind.config.ts +69 -42
  57. package/templates/nextjs/designs/{passepartout → atelier}/app-overlay/layout.tsx.ejs +23 -25
  58. package/templates/nextjs/designs/atelier/globals.css +396 -0
  59. package/templates/nextjs/designs/atelier/messages-patch/en.json +99 -0
  60. package/templates/nextjs/designs/atelier/messages-patch/he.json +99 -0
  61. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-bundle-offer.tsx +110 -111
  62. package/templates/nextjs/designs/atelier/ui/cart/cart-drawer.tsx +163 -0
  63. package/templates/nextjs/designs/atelier/ui/cart/cart-item.tsx +140 -0
  64. package/templates/nextjs/designs/atelier/ui/cart/cart-nudges.tsx +34 -0
  65. package/templates/nextjs/designs/atelier/ui/cart/cart-summary.tsx +107 -0
  66. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/cart-upgrade-banner.tsx +123 -141
  67. package/templates/nextjs/designs/atelier/ui/cart/cart-view.tsx +147 -0
  68. package/templates/nextjs/designs/atelier/ui/cart/coupon-input.tsx +142 -0
  69. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/free-shipping-bar.tsx +77 -62
  70. package/templates/nextjs/designs/{passepartout → atelier}/ui/cart/reservation-countdown.tsx +89 -103
  71. package/templates/nextjs/designs/atelier/ui/home/benefits-band.tsx +34 -0
  72. package/templates/nextjs/designs/atelier/ui/home/category-tiles.tsx +148 -0
  73. package/templates/nextjs/designs/atelier/ui/home/discount-banner-strip.tsx +27 -0
  74. package/templates/nextjs/designs/atelier/ui/home/editorial-band.tsx +73 -0
  75. package/templates/nextjs/designs/atelier/ui/home/featured-products-section.tsx +36 -0
  76. package/templates/nextjs/designs/atelier/ui/home/hero-section.tsx +148 -0
  77. package/templates/nextjs/designs/atelier/ui/home/home-client.tsx +54 -0
  78. package/templates/nextjs/designs/atelier/ui/home/newsletter-section.tsx +78 -0
  79. package/templates/nextjs/designs/atelier/ui/home/testimonials-band.tsx +46 -0
  80. package/templates/nextjs/designs/atelier/ui/layout/announcement-bar.tsx +118 -0
  81. package/templates/nextjs/designs/atelier/ui/layout/faq-section.tsx +96 -0
  82. package/templates/nextjs/designs/atelier/ui/layout/header-account.tsx +36 -0
  83. package/templates/nextjs/designs/atelier/ui/layout/header-cart.tsx +36 -0
  84. package/templates/nextjs/designs/atelier/ui/layout/language-switcher.tsx.ejs +137 -0
  85. package/templates/nextjs/designs/atelier/ui/layout/nav-url.ts +14 -0
  86. package/templates/nextjs/designs/atelier/ui/layout/rich-text-block.tsx +34 -0
  87. package/templates/nextjs/designs/atelier/ui/layout/site-footer.tsx.ejs +141 -0
  88. package/templates/nextjs/designs/atelier/ui/layout/site-header.tsx.ejs +138 -0
  89. package/templates/nextjs/designs/atelier/ui/product/customization-fields.tsx +466 -0
  90. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/discount-badge.tsx +17 -22
  91. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/frequently-bought-together.tsx +196 -205
  92. package/templates/nextjs/designs/atelier/ui/product/modifier-group-selector.tsx +193 -0
  93. package/templates/nextjs/designs/atelier/ui/product/price-display.tsx +69 -0
  94. package/templates/nextjs/designs/atelier/ui/product/product-card.tsx +260 -0
  95. package/templates/nextjs/designs/atelier/ui/product/product-client-section.tsx +471 -0
  96. package/templates/nextjs/designs/atelier/ui/product/product-grid.tsx +47 -0
  97. package/templates/nextjs/designs/atelier/ui/product/product-listing.tsx +260 -0
  98. package/templates/nextjs/designs/atelier/ui/product/recommendation-section.tsx +95 -0
  99. package/templates/nextjs/designs/{passepartout → atelier}/ui/product/review-form.tsx +267 -294
  100. package/templates/nextjs/designs/atelier/ui/product/reviews-section.tsx.ejs +148 -0
  101. package/templates/nextjs/designs/atelier/ui/product/stock-badge.tsx +66 -0
  102. package/templates/nextjs/designs/atelier/ui/product/variant-selector.tsx +206 -0
  103. package/templates/nextjs/designs/atelier/ui/shared/cdn-image.tsx +29 -0
  104. package/templates/nextjs/designs/{passepartout → atelier}/ui/shared/fly-to-cart.ts +86 -85
  105. package/templates/nextjs/designs/atelier/ui/shared/icons.tsx +269 -0
  106. package/templates/nextjs/designs/atelier/ui/shared/loading-spinner.tsx +64 -0
  107. package/templates/nextjs/designs/atelier/ui/shared/reveal.tsx +71 -0
  108. package/templates/nextjs/designs/atelier/ui/shared/select-menu.tsx +125 -0
  109. package/templates/nextjs/themes/luxury/globals.css +405 -399
  110. package/templates/nextjs/themes/minimal/globals.css +36 -30
  111. package/templates/nextjs/themes/playful/globals.css +406 -400
  112. package/templates/nextjs/ui-canvas/cart/cart-bundle-offer.tsx +2 -0
  113. package/templates/nextjs/ui-canvas/cart/cart-item.tsx +2 -0
  114. package/templates/nextjs/ui-canvas/cart/cart-nudges.tsx +2 -0
  115. package/templates/nextjs/ui-canvas/cart/cart-summary.tsx +2 -0
  116. package/templates/nextjs/ui-canvas/cart/cart-upgrade-banner.tsx +2 -0
  117. package/templates/nextjs/ui-canvas/cart/cart-view.tsx +2 -2
  118. package/templates/nextjs/ui-canvas/cart/coupon-input.tsx +2 -0
  119. package/templates/nextjs/ui-canvas/cart/free-shipping-bar.tsx +2 -0
  120. package/templates/nextjs/ui-canvas/cart/reservation-countdown.tsx +2 -0
  121. package/templates/nextjs/ui-canvas/home/discount-banner-strip.tsx +1 -1
  122. package/templates/nextjs/ui-canvas/home/featured-products-section.tsx +1 -1
  123. package/templates/nextjs/ui-canvas/home/hero-section.tsx +1 -1
  124. package/templates/nextjs/ui-canvas/home/home-client.tsx +1 -1
  125. package/templates/nextjs/ui-canvas/layout/announcement-bar.tsx.ejs +1 -1
  126. package/templates/nextjs/ui-canvas/layout/faq-section.tsx.ejs +2 -2
  127. package/templates/nextjs/ui-canvas/layout/header-account.tsx +1 -1
  128. package/templates/nextjs/ui-canvas/layout/language-switcher.tsx.ejs +2 -0
  129. package/templates/nextjs/ui-canvas/layout/rich-text-block.tsx.ejs +2 -0
  130. package/templates/nextjs/ui-canvas/layout/site-footer.tsx.ejs +2 -2
  131. package/templates/nextjs/ui-canvas/layout/site-header.tsx.ejs +2 -2
  132. package/templates/nextjs/ui-canvas/product/customization-fields.tsx +2 -0
  133. package/templates/nextjs/ui-canvas/product/discount-badge.tsx +2 -0
  134. package/templates/nextjs/ui-canvas/product/frequently-bought-together.tsx +2 -0
  135. package/templates/nextjs/ui-canvas/product/modifier-group-selector.tsx +2 -0
  136. package/templates/nextjs/ui-canvas/product/price-display.tsx +2 -0
  137. package/templates/nextjs/ui-canvas/product/product-card.tsx +1 -1
  138. package/templates/nextjs/ui-canvas/product/product-client-section.tsx +1 -1
  139. package/templates/nextjs/ui-canvas/product/product-grid.tsx +1 -1
  140. package/templates/nextjs/ui-canvas/product/product-listing.tsx +1 -1
  141. package/templates/nextjs/ui-canvas/product/recommendation-section.tsx +4 -0
  142. package/templates/nextjs/ui-canvas/product/review-form.tsx +29 -32
  143. package/templates/nextjs/ui-canvas/product/{reviews-section.tsx → reviews-section.tsx.ejs} +26 -8
  144. package/templates/nextjs/ui-canvas/product/stock-badge.tsx +2 -0
  145. package/templates/nextjs/ui-canvas/product/variant-selector.tsx +2 -0
  146. package/templates/nextjs/ui-canvas/shared/cdn-image.tsx +2 -0
  147. package/templates/nextjs/ui-canvas/shared/loading-spinner.tsx +2 -0
  148. package/templates/nextjs/base/src/ui/product/reviews-section.tsx +0 -85
  149. package/templates/nextjs/designs/passepartout/globals.css +0 -412
  150. package/templates/nextjs/designs/passepartout/messages-patch/en.json +0 -87
  151. package/templates/nextjs/designs/passepartout/messages-patch/he.json +0 -87
  152. package/templates/nextjs/designs/passepartout/ui/cart/cart-nudges.tsx +0 -39
  153. package/templates/nextjs/designs/passepartout/ui/cart/cart-view.tsx +0 -149
  154. package/templates/nextjs/designs/passepartout/ui/home/home-client.tsx +0 -401
  155. package/templates/nextjs/designs/passepartout/ui/layout/announcement-bar.tsx +0 -139
  156. package/templates/nextjs/designs/passepartout/ui/layout/cart-link.tsx +0 -45
  157. package/templates/nextjs/designs/passepartout/ui/layout/faq-section.tsx +0 -103
  158. package/templates/nextjs/designs/passepartout/ui/layout/site-footer.tsx +0 -159
  159. package/templates/nextjs/designs/passepartout/ui/layout/site-header.tsx.ejs +0 -155
  160. package/templates/nextjs/designs/passepartout/ui/layout/trust-icons.tsx +0 -60
  161. package/templates/nextjs/designs/passepartout/ui/layout/whatsapp-button.tsx +0 -34
  162. package/templates/nextjs/designs/passepartout/ui/product/price-display.tsx +0 -111
  163. package/templates/nextjs/designs/passepartout/ui/product/product-card.tsx +0 -265
  164. package/templates/nextjs/designs/passepartout/ui/product/product-client-section.tsx +0 -667
  165. package/templates/nextjs/designs/passepartout/ui/product/product-grid.tsx +0 -35
  166. package/templates/nextjs/designs/passepartout/ui/product/product-listing.tsx +0 -427
  167. package/templates/nextjs/designs/passepartout/ui/product/recommendation-section.tsx +0 -113
  168. package/templates/nextjs/designs/passepartout/ui/product/stock-badge.tsx +0 -52
  169. package/templates/nextjs/designs/passepartout/ui/product/variant-selector.tsx +0 -307
  170. package/templates/nextjs/designs/passepartout/ui/shared/loading-spinner.tsx +0 -33
@@ -1,307 +0,0 @@
1
- 'use client';
2
-
3
- import { useMemo } from 'react';
4
- import type { Product, ProductVariant } from 'brainerce';
5
- import { getVariantOptions, getProductSwatches, formatPrice } from 'brainerce';
6
- import type { InventoryInfo } from 'brainerce';
7
- import { useCurrency } from '@/core/lib/use-currency';
8
- import { useTranslations } from '@/core/lib/translations';
9
- import { cn } from '@/core/lib/utils';
10
-
11
- interface VariantSelectorProps {
12
- product: Product;
13
- selectedVariant: ProductVariant | null;
14
- onVariantChange: (variant: ProductVariant) => void;
15
- className?: string;
16
- }
17
-
18
- interface AttributeGroup {
19
- name: string;
20
- displayType: string;
21
- values: Array<{
22
- value: string;
23
- swatchColor?: string | null;
24
- swatchColor2?: string | null;
25
- swatchImageUrl?: string | null;
26
- variants: ProductVariant[];
27
- }>;
28
- }
29
-
30
- export function VariantSelector({
31
- product,
32
- selectedVariant,
33
- onVariantChange,
34
- className,
35
- }: VariantSelectorProps) {
36
- const t = useTranslations('productDetail');
37
- const currency = useCurrency();
38
- const variants = useMemo(() => product.variants || [], [product.variants]);
39
-
40
- // Get swatch metadata from product attribute options
41
- const swatchData = useMemo(() => getProductSwatches(product), [product]);
42
- const swatchMap = useMemo(() => {
43
- const map = new Map<
44
- string,
45
- {
46
- displayType: string;
47
- options: Map<
48
- string,
49
- {
50
- swatchColor?: string | null;
51
- swatchColor2?: string | null;
52
- swatchImageUrl?: string | null;
53
- }
54
- >;
55
- }
56
- >();
57
- for (const attr of swatchData) {
58
- const optMap = new Map<
59
- string,
60
- {
61
- swatchColor?: string | null;
62
- swatchColor2?: string | null;
63
- swatchImageUrl?: string | null;
64
- }
65
- >();
66
- for (const opt of attr.options) {
67
- optMap.set(opt.name, {
68
- swatchColor: opt.swatchColor,
69
- swatchColor2: opt.swatchColor2,
70
- swatchImageUrl: opt.swatchImageUrl,
71
- });
72
- }
73
- map.set(attr.attributeName, { displayType: attr.displayType, options: optMap });
74
- }
75
- return map;
76
- }, [swatchData]);
77
-
78
- // Build attribute groups from variant data, enriched with swatch info
79
- const attributeGroups = useMemo<AttributeGroup[]>(() => {
80
- const groups = new Map<string, Map<string, ProductVariant[]>>();
81
-
82
- for (const variant of variants) {
83
- const options = getVariantOptions(variant);
84
- for (const { name, value } of options) {
85
- if (!groups.has(name)) {
86
- groups.set(name, new Map());
87
- }
88
- const valuesMap = groups.get(name)!;
89
- if (!valuesMap.has(value)) {
90
- valuesMap.set(value, []);
91
- }
92
- valuesMap.get(value)!.push(variant);
93
- }
94
- }
95
-
96
- return Array.from(groups.entries()).map(([name, valuesMap]) => {
97
- const attrSwatch = swatchMap.get(name);
98
- return {
99
- name,
100
- displayType: attrSwatch?.displayType || 'DROPDOWN',
101
- values: Array.from(valuesMap.entries()).map(([value, variantList]) => {
102
- const optSwatch = attrSwatch?.options.get(value);
103
- return {
104
- value,
105
- swatchColor: optSwatch?.swatchColor,
106
- swatchColor2: optSwatch?.swatchColor2,
107
- swatchImageUrl: optSwatch?.swatchImageUrl,
108
- variants: variantList,
109
- };
110
- }),
111
- };
112
- });
113
- }, [variants, swatchMap]);
114
-
115
- // Get currently selected attribute values
116
- const selectedOptions = useMemo(() => {
117
- if (!selectedVariant) return new Map<string, string>();
118
- const opts = getVariantOptions(selectedVariant);
119
- return new Map(opts.map(({ name, value }) => [name, value]));
120
- }, [selectedVariant]);
121
-
122
- // Find the variant that matches all selected attributes
123
- function findMatchingVariant(
124
- attributeName: string,
125
- newValue: string
126
- ): ProductVariant | undefined {
127
- const nextSelection = new Map(selectedOptions);
128
- nextSelection.set(attributeName, newValue);
129
-
130
- return variants.find((v) => {
131
- const opts = getVariantOptions(v);
132
- return Array.from(nextSelection.entries()).every(([name, value]) =>
133
- opts.some((o) => o.name === name && o.value === value)
134
- );
135
- });
136
- }
137
-
138
- if (attributeGroups.length === 0) return null;
139
-
140
- return (
141
- <div className={cn('space-y-4', className)}>
142
- {attributeGroups.map((group) => (
143
- <div key={group.name}>
144
- <label className="placard mb-3 block font-sans">
145
- {group.name}
146
- {selectedOptions.get(group.name) && (
147
- <span className="text-foreground ms-1">— {selectedOptions.get(group.name)}</span>
148
- )}
149
- </label>
150
- <div className="flex flex-wrap gap-x-2 gap-y-3">
151
- {group.values.map(
152
- ({
153
- value,
154
- swatchColor,
155
- swatchColor2,
156
- swatchImageUrl,
157
- variants: matchingVariants,
158
- }) => {
159
- const isSelected = selectedOptions.get(group.name) === value;
160
- const matchedVariant = findMatchingVariant(group.name, value);
161
- const isAvailable = matchedVariant?.inventory?.canPurchase !== false;
162
-
163
- // Color swatch rendering
164
- if (group.displayType === 'COLOR_SWATCH' && swatchColor) {
165
- return (
166
- <button
167
- key={value}
168
- type="button"
169
- disabled={!isAvailable}
170
- title={value}
171
- onClick={() => {
172
- const variant = matchedVariant || matchingVariants[0];
173
- if (variant) onVariantChange(variant);
174
- }}
175
- className={cn(
176
- 'relative h-9 w-9 border transition-colors',
177
- isSelected
178
- ? 'border-foreground'
179
- : isAvailable
180
- ? 'border-border hover:border-foreground'
181
- : 'cursor-not-allowed opacity-40'
182
- )}
183
- style={{
184
- background: swatchColor2
185
- ? `linear-gradient(135deg, ${swatchColor} 50%, ${swatchColor2} 50%)`
186
- : swatchColor,
187
- }}
188
- >
189
- {isSelected && (
190
- <span
191
- aria-hidden="true"
192
- className="bg-accent absolute -bottom-2 start-1/2 h-1 w-1 -translate-x-1/2 rounded-full rtl:translate-x-1/2"
193
- />
194
- )}
195
- {!isAvailable && (
196
- <span
197
- className="bg-muted-foreground block h-full w-full opacity-50"
198
- style={{
199
- backgroundImage:
200
- 'linear-gradient(135deg, transparent 45%, currentColor 45%, currentColor 55%, transparent 55%)',
201
- }}
202
- />
203
- )}
204
- </button>
205
- );
206
- }
207
-
208
- // Image swatch rendering
209
- if (group.displayType === 'IMAGE_SWATCH' && swatchImageUrl) {
210
- return (
211
- <button
212
- key={value}
213
- type="button"
214
- disabled={!isAvailable}
215
- title={value}
216
- onClick={() => {
217
- const variant = matchedVariant || matchingVariants[0];
218
- if (variant) onVariantChange(variant);
219
- }}
220
- className={cn(
221
- 'relative h-10 w-10 overflow-visible border transition-colors',
222
- isSelected
223
- ? 'border-foreground'
224
- : isAvailable
225
- ? 'border-border hover:border-foreground'
226
- : 'cursor-not-allowed opacity-40'
227
- )}
228
- >
229
- <img
230
- src={swatchImageUrl}
231
- alt={value}
232
- className="h-full w-full object-cover"
233
- />
234
- {isSelected && (
235
- <span
236
- aria-hidden="true"
237
- className="bg-accent absolute -bottom-2 start-1/2 h-1 w-1 -translate-x-1/2 rounded-full rtl:translate-x-1/2"
238
- />
239
- )}
240
- </button>
241
- );
242
- }
243
-
244
- // Default button rendering (BUTTON, DROPDOWN, or fallback)
245
- return (
246
- <button
247
- key={value}
248
- type="button"
249
- disabled={!isAvailable}
250
- onClick={() => {
251
- const variant = matchedVariant || matchingVariants[0];
252
- if (variant) onVariantChange(variant);
253
- }}
254
- className={cn(
255
- 'relative min-h-11 border px-4 py-2.5 text-[0.8125rem] transition-colors',
256
- isSelected
257
- ? 'border-foreground text-foreground'
258
- : isAvailable
259
- ? 'border-border bg-surface text-foreground hover:border-foreground'
260
- : 'border-border text-muted-foreground cursor-not-allowed line-through opacity-40'
261
- )}
262
- >
263
- {value}
264
- {isSelected && (
265
- <span
266
- aria-hidden="true"
267
- className="bg-accent absolute -bottom-2 start-1/2 h-1 w-1 -translate-x-1/2 rounded-full rtl:translate-x-1/2"
268
- />
269
- )}
270
- </button>
271
- );
272
- }
273
- )}
274
- </div>
275
- </div>
276
- ))}
277
-
278
- {/* Variant-specific info */}
279
- {selectedVariant && (
280
- <div className="text-muted-foreground tabular flex items-center gap-3 pt-1 text-[0.6875rem] tracking-[0.08em]">
281
- {selectedVariant.price && (
282
- <span>
283
- {
284
- formatPrice(selectedVariant.salePrice || selectedVariant.price, {
285
- currency,
286
- }) as string
287
- }
288
- </span>
289
- )}
290
- <span>{getTranslatedStockStatus(selectedVariant.inventory, t)}</span>
291
- </div>
292
- )}
293
- </div>
294
- );
295
- }
296
-
297
- function getTranslatedStockStatus(
298
- inventory: InventoryInfo | null | undefined,
299
- t: (key: string) => string
300
- ): string {
301
- if (!inventory) return t('outOfStock');
302
- const { trackingMode, inStock, available } = inventory;
303
- if (trackingMode === 'DISABLED') return t('unavailable');
304
- if (!inStock) return t('outOfStock');
305
- if (trackingMode === 'UNLIMITED') return t('inStock');
306
- return t('availableInStock').replace('{available}', String(available));
307
- }
@@ -1,33 +0,0 @@
1
- 'use client';
2
-
3
- import { useTranslations } from '@/core/lib/translations';
4
- import { cn } from '@/core/lib/utils';
5
-
6
- interface LoadingSpinnerProps {
7
- size?: 'sm' | 'md' | 'lg';
8
- className?: string;
9
- }
10
-
11
- const sizeClasses = {
12
- sm: 'h-1.5 w-1.5',
13
- md: 'h-2 w-2',
14
- lg: 'h-2.5 w-2.5',
15
- };
16
-
17
- /**
18
- * The breathing dot — a single brass dot pulsing opacity 0.3 → 1.
19
- * A spinner, however styled, still spins; the dot is the only loading
20
- * state consistent with a site where motion confirms touch.
21
- */
22
- export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
23
- const t = useTranslations('common');
24
- return (
25
- <div
26
- className={cn('breathing-dot', sizeClasses[size], className)}
27
- role="status"
28
- aria-label={t('loading')}
29
- >
30
- <span className="sr-only">{t('loading')}</span>
31
- </div>
32
- );
33
- }