affora 0.1.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.
@@ -0,0 +1,551 @@
1
+ import type React from 'react';
2
+ import { useId, useState } from 'react';
3
+
4
+ /**
5
+ * ProductCard — a hero commerce product tile.
6
+ *
7
+ * Authored once and rendered under every Affora theme. Every stylistic value is
8
+ * a design token (var(--x)) so the same markup reads as tight-dark, warm-rounded,
9
+ * serif-editorial, structured-enterprise, or bold-expressive without edits.
10
+ *
11
+ * Agent-legible substrate (never compromised):
12
+ * - <article> with a real <h3> name, price as <data>, a text rating line,
13
+ * a <dl>/<dt>/<dd> spec list (Battery/Driver/Weight, all text — never an image),
14
+ * a native radio group for size (each <input type=radio> visible-labelled with
15
+ * its SKU text), and a real <button> "Add to cart".
16
+ * - onCommit(sku) fires when a size is chosen and again on add-to-cart.
17
+ */
18
+
19
+ type Size = { code: 'S' | 'M' | 'L'; label: string; sku: string; inStock: boolean };
20
+
21
+ const SIZES: Size[] = [
22
+ { code: 'S', label: 'Small', sku: 'SKU-S', inStock: true },
23
+ { code: 'M', label: 'Medium', sku: 'SKU-M', inStock: true },
24
+ { code: 'L', label: 'Large', sku: 'SKU-L', inStock: true },
25
+ ];
26
+
27
+ /**
28
+ * Real-catalogue content. When `window.__affora_product` is set (arm.html reads
29
+ * `?product=<asin>` from public/real/products.json, a WebShop / Amazon-derived
30
+ * item), the card carries that item's real name, brand, price, rating, photo and
31
+ * size options. When it is unset — every measured run — the card renders the
32
+ * reference content below, byte for byte. The substrate does not change between
33
+ * the two: same elements, same roles, same fieldset; only the facts do.
34
+ */
35
+ export type RealProduct = {
36
+ asin: string; brand: string; name: string; price: number; was: number | null;
37
+ rating: number | string; reviews: number | string; category?: string;
38
+ options: Record<string, string[]>; image: string; source: string;
39
+ };
40
+ declare global { interface Window { __affora_product?: RealProduct } }
41
+ const REAL: RealProduct | undefined = typeof window !== 'undefined' ? window.__affora_product : undefined;
42
+
43
+ const SPECS: { term: string; value: string }[] = [
44
+ { term: 'Battery', value: '38 h ANC' },
45
+ { term: 'Driver', value: '40 mm Be' },
46
+ { term: 'Weight', value: '249 g' },
47
+ ];
48
+
49
+ const realSizes = (r: RealProduct): Size[] => {
50
+ const group = r.options.Size ?? Object.values(r.options)[0] ?? [];
51
+ return group.slice(0, 3).map((label) => ({
52
+ code: label.replace(/^(X{0,2}[SML])[a-z-]*$/i, '$1').slice(0, 4) as Size['code'],
53
+ // The second line names the scale, not the value: a numeric size is US,
54
+ // a lettered one is a garment size, and repeating "S" under "S" says nothing.
55
+ label: /^\d/.test(label) ? 'US' : 'Size',
56
+ sku: `${r.asin.slice(-4)}-${label.replace(/\s+/g, '')}`, inStock: true,
57
+ }));
58
+ };
59
+ const savePct = (price: number, was: number | null) => (was && was > price ? Math.round(100 * (1 - price / was)) : 0);
60
+
61
+ export const ProductCard: React.FC<{ onCommit?: (v: string) => void }> = ({
62
+ onCommit,
63
+ }) => {
64
+ const groupName = useId();
65
+ const sizes = REAL ? realSizes(REAL) : SIZES;
66
+ const [sku, setSku] = useState<string | null>(null);
67
+ const [added, setAdded] = useState(false);
68
+
69
+ const chooseSize = (next: string) => {
70
+ setSku(next);
71
+ setAdded(false);
72
+ onCommit?.(next);
73
+ };
74
+
75
+ const addToCart = () => {
76
+ if (!sku) return;
77
+ setAdded(true);
78
+ onCommit?.(sku);
79
+ };
80
+
81
+ return (
82
+ <article className="pcf">
83
+ <style>{CSS}</style>
84
+
85
+ {/* ── Media / hero ─────────────────────────────────────────── */}
86
+ <div className="pcf__media">
87
+ {!REAL && <span className="pcf__badge">New</span>}
88
+ {/* Decorative product illustration — drawn from tokens, not task data. */}
89
+ {REAL ? (
90
+ <img className="pcf__art pcf__photo" src={REAL.image} alt={REAL.name} />
91
+ ) : (
92
+ <svg
93
+ className="pcf__art"
94
+ viewBox="0 0 120 120"
95
+ role="img"
96
+ aria-label="Aurora One wireless headphones"
97
+ >
98
+ <path
99
+ d="M22 66V58a38 38 0 0 1 76 0v8"
100
+ fill="none"
101
+ stroke="currentColor"
102
+ strokeWidth="5"
103
+ strokeLinecap="round"
104
+ />
105
+ <rect x="14" y="62" width="20" height="34" rx="9" className="pcf__art-cup" />
106
+ <rect x="86" y="62" width="20" height="34" rx="9" className="pcf__art-cup" />
107
+ </svg>
108
+ )}
109
+ </div>
110
+
111
+ {/* ── Identity ─────────────────────────────────────────────── */}
112
+ <div className="pcf__head">
113
+ <p className="pcf__brand">{REAL ? REAL.brand : 'Aurora Audio'}</p>
114
+ <h3 className="pcf__name">{REAL ? REAL.name : 'Aurora One Wireless Headphones'}</h3>
115
+ <p className="pcf__rating">
116
+ <svg className="pcf__star" viewBox="0 0 20 20" aria-hidden="true">
117
+ <path
118
+ d="M10 1.6l2.47 5 5.53.8-4 3.9.94 5.5L10 14.2 5.06 16.8 6 11.3l-4-3.9 5.53-.8z"
119
+ fill="currentColor"
120
+ />
121
+ </svg>
122
+ <span className="pcf__rating-num">{REAL ? String(REAL.rating) : '4.6'}</span>
123
+ <span className="pcf__dot" aria-hidden="true">
124
+ ·
125
+ </span>
126
+ <span className="pcf__reviews">{REAL ? `${REAL.reviews} reviews` : '128 reviews'}</span>
127
+ </p>
128
+ </div>
129
+
130
+ {/* ── Price ────────────────────────────────────────────────── */}
131
+ <p className="pcf__price-row">
132
+ <data className="pcf__price" value={REAL ? REAL.price.toFixed(2) : '129.00'}>
133
+ ${REAL ? REAL.price.toFixed(2) : '129.00'}
134
+ </data>
135
+ {(!REAL || REAL.was) && (
136
+ <data className="pcf__was" value={REAL && REAL.was ? REAL.was.toFixed(2) : '169.00'}>
137
+ ${REAL && REAL.was ? REAL.was.toFixed(2) : '169.00'}
138
+ </data>
139
+ )}
140
+ {(!REAL || savePct(REAL.price, REAL.was) > 0) && (
141
+ <span className="pcf__save">Save {REAL ? savePct(REAL.price, REAL.was) : 24}%</span>
142
+ )}
143
+ </p>
144
+
145
+ {/* ── Specs (text only, never an image) ────────────────────── */}
146
+ <dl className="pcf__specs">
147
+ {(REAL ? [{ term: 'Brand', value: REAL.brand.split(' ')[0] }, { term: 'Sizes', value: `${(REAL.options.Size ?? []).length || Object.values(REAL.options)[0]?.length || 0} listed` }, { term: 'Colors', value: `${(REAL.options.Color ?? []).length || 1} listed` }] : SPECS).map((s) => (
148
+ <div className="pcf__spec" key={s.term}>
149
+ <dt className="pcf__spec-term">{s.term}</dt>
150
+ <dd className="pcf__spec-val">{s.value}</dd>
151
+ </div>
152
+ ))}
153
+ </dl>
154
+
155
+ {/* ── Size selector (real radio group) ─────────────────────── */}
156
+ <fieldset className="pcf__sizes">
157
+ <legend className="pcf__sizes-legend">
158
+ Fit
159
+ <span className="pcf__sizes-hint">{sku ? `SKU ${sku}` : 'Select a fit'}</span>
160
+ </legend>
161
+ <div className="pcf__seg" role="radiogroup" aria-label="Fit">
162
+ {sizes.map((s) => (
163
+ <label
164
+ key={s.sku}
165
+ className="pcf__opt"
166
+ data-active={sku === s.sku || undefined}
167
+ data-oos={!s.inStock || undefined}
168
+ >
169
+ <input
170
+ className="pcf__radio"
171
+ type="radio"
172
+ name={groupName}
173
+ value={s.sku}
174
+ aria-label={`${s.inStock ? s.label : `${s.label} (sold out)`} — ${s.sku}`}
175
+ checked={sku === s.sku}
176
+ disabled={!s.inStock}
177
+ onChange={() => chooseSize(s.sku)}
178
+ />
179
+ <span className="pcf__opt-code">{s.code}</span>
180
+ <span className="pcf__opt-label">{s.inStock ? s.label : 'Sold out'}</span>
181
+ <span className="pcf__opt-sku">{s.sku}</span>
182
+ </label>
183
+ ))}
184
+ </div>
185
+ </fieldset>
186
+
187
+ {/* ── Commit ───────────────────────────────────────────────── */}
188
+ <button
189
+ type="button"
190
+ className="pcf__cta"
191
+ onClick={addToCart}
192
+ disabled={!sku}
193
+ aria-disabled={!sku}
194
+ data-added={added || undefined}
195
+ >
196
+ <svg className="pcf__cart" viewBox="0 0 20 20" aria-hidden="true">
197
+ {added ? (
198
+ <path
199
+ d="M4 10.5l3.5 3.5L16 6"
200
+ fill="none"
201
+ stroke="currentColor"
202
+ strokeWidth="2.2"
203
+ strokeLinecap="round"
204
+ strokeLinejoin="round"
205
+ />
206
+ ) : (
207
+ <path
208
+ d="M3 3h2l1.2 9.2a1.6 1.6 0 0 0 1.6 1.4h6.6a1.6 1.6 0 0 0 1.6-1.3L17.5 6H6M8 17.5a1 1 0 1 0 0 .01M15 17.5a1 1 0 1 0 0 .01"
209
+ fill="none"
210
+ stroke="currentColor"
211
+ strokeWidth="1.6"
212
+ strokeLinecap="round"
213
+ strokeLinejoin="round"
214
+ />
215
+ )}
216
+ </svg>
217
+ <span>{added ? 'Added to cart' : sku ? 'Add to cart' : 'Choose a fit'}</span>
218
+ </button>
219
+ </article>
220
+ );
221
+ };
222
+
223
+ export const CSS = `
224
+ /* A photograph arrives on its own white ground, which reads as a white box inside
225
+ a tinted media pane; multiplying it into the pane drops that ground. A drawn
226
+ SVG has no ground to drop, so it is left alone. */
227
+ .pcf__photo { width: 100%; height: 100%; object-fit: contain; display: block;
228
+ mix-blend-mode: multiply; padding: 0; }
229
+ .pcf__photo[src$=".svg"] { mix-blend-mode: normal; width: 72%; height: auto; margin: 0 auto; }
230
+ .pcf {
231
+ box-sizing: border-box;
232
+ width: 100%;
233
+ max-width: var(--lay-width, 360px);
234
+ display: flex;
235
+ flex-direction: column;
236
+ gap: calc(var(--gap) * var(--lay-density, 1));
237
+ padding: calc(var(--pad-loose) * var(--lay-density, 1));
238
+ background: var(--glass-bg, var(--surface));
239
+ backdrop-filter: var(--glass-blur, none);
240
+ -webkit-backdrop-filter: var(--glass-blur, none);
241
+ color: var(--fg);
242
+ border: 1px solid var(--glass-border, var(--border));
243
+ border-radius: var(--radius-lg);
244
+ box-shadow: var(--shadow);
245
+ font-family: var(--font);
246
+ font-size: var(--text-base);
247
+ font-weight: var(--weight-normal);
248
+ line-height: var(--leading);
249
+ letter-spacing: var(--tracking-tight);
250
+ transition: box-shadow var(--dur) var(--ease), transform var(--dur) var(--ease),
251
+ border-color var(--dur) var(--ease);
252
+ }
253
+ /* commerce layout: media pane beside the buying controls. Achieved with
254
+ flex-wrap over the SAME children in the SAME DOM order — no reordering. */
255
+ [data-layout="commerce"] .pcf {
256
+ flex-direction: row;
257
+ flex-wrap: wrap;
258
+ align-items: flex-start;
259
+ }
260
+ [data-layout="commerce"] .pcf__media {
261
+ flex: 0 0 44%;
262
+ align-self: stretch;
263
+ }
264
+ [data-layout="commerce"] .pcf__head,
265
+ [data-layout="commerce"] .pcf__price-row,
266
+ [data-layout="commerce"] .pcf__specs,
267
+ [data-layout="commerce"] .pcf__sizes,
268
+ [data-layout="commerce"] .pcf__cta { flex: 1 1 46%; }
269
+
270
+ .pcf *,
271
+ .pcf *::before,
272
+ .pcf *::after { box-sizing: border-box; }
273
+ .pcf:hover {
274
+ box-shadow: var(--shadow-lg);
275
+ border-color: var(--border-strong);
276
+ }
277
+
278
+ /* ── Media ── */
279
+ .pcf__media {
280
+ position: relative;
281
+ display: var(--lay-media, grid);
282
+ place-items: center;
283
+ aspect-ratio: 16 / 10;
284
+ border-radius: var(--radius);
285
+ color: var(--accent);
286
+ background:
287
+ radial-gradient(120% 120% at 50% 0%, var(--accent-weak), transparent 70%),
288
+ var(--surface-2);
289
+ border: 1px solid var(--border);
290
+ overflow: hidden;
291
+ }
292
+ .pcf__art {
293
+ width: 62%;
294
+ height: auto;
295
+ filter: drop-shadow(var(--shadow-sm));
296
+ transition: transform var(--dur) var(--ease-emphasis);
297
+ }
298
+ .pcf:hover .pcf__art { transform: translateY(-3px) scale(1.03); }
299
+ .pcf__art-cup { fill: var(--accent); }
300
+ .pcf__badge {
301
+ position: absolute;
302
+ top: var(--pad-tight);
303
+ left: var(--pad-tight);
304
+ padding: 2px var(--pad-tight);
305
+ font-family: var(--font-mono);
306
+ font-size: calc(var(--text-base) * 0.7);
307
+ font-weight: var(--weight-medium);
308
+ letter-spacing: 0.04em;
309
+ text-transform: uppercase;
310
+ color: var(--accent-fg);
311
+ background: var(--accent);
312
+ border-radius: var(--radius-full);
313
+ }
314
+
315
+ /* ── Identity ── */
316
+ .pcf__head { display: flex; flex-direction: column; gap: calc(var(--gap) * 0.25); }
317
+ .pcf__brand {
318
+ margin: 0;
319
+ font-size: calc(var(--text-base) * 0.78);
320
+ font-weight: var(--weight-medium);
321
+ letter-spacing: 0.08em;
322
+ text-transform: uppercase;
323
+ color: var(--fg-subtle);
324
+ }
325
+ .pcf__name {
326
+ margin: 0;
327
+ font-family: var(--font-display);
328
+ font-size: calc(var(--text-base) * var(--scale-ratio) * var(--scale-ratio));
329
+ font-weight: var(--weight-display);
330
+ line-height: 1.15;
331
+ letter-spacing: var(--tracking-tight);
332
+ color: var(--fg);
333
+ text-wrap: balance;
334
+ }
335
+ .pcf__rating {
336
+ display: flex;
337
+ align-items: center;
338
+ gap: calc(var(--gap) * 0.4);
339
+ margin: calc(var(--gap) * 0.25) 0 0;
340
+ font-size: calc(var(--text-base) * 0.9);
341
+ color: var(--fg-muted);
342
+ }
343
+ .pcf__star { width: 1em; height: 1em; color: var(--warning); flex: none; }
344
+ .pcf__rating-num {
345
+ font-weight: var(--weight-bold);
346
+ color: var(--fg);
347
+ font-variant-numeric: tabular-nums;
348
+ }
349
+ .pcf__dot { color: var(--fg-subtle); }
350
+ .pcf__reviews { font-variant-numeric: tabular-nums; }
351
+
352
+ /* ── Price ── */
353
+ .pcf__price-row {
354
+ display: flex;
355
+ align-items: baseline;
356
+ flex-wrap: wrap;
357
+ gap: calc(var(--gap) * 0.6);
358
+ margin: 0;
359
+ }
360
+ .pcf__price {
361
+ font-family: var(--font-display);
362
+ font-size: calc(var(--text-base) * 1.7);
363
+ font-weight: var(--weight-bold);
364
+ color: var(--fg);
365
+ font-variant-numeric: tabular-nums;
366
+ letter-spacing: var(--tracking-tight);
367
+ }
368
+ .pcf__was {
369
+ font-size: calc(var(--text-base) * 0.95);
370
+ color: var(--fg-subtle);
371
+ text-decoration: line-through;
372
+ font-variant-numeric: tabular-nums;
373
+ }
374
+ .pcf__save {
375
+ padding: 1px var(--pad-tight);
376
+ font-size: calc(var(--text-base) * 0.72);
377
+ font-weight: var(--weight-bold);
378
+ letter-spacing: 0.02em;
379
+ color: var(--success);
380
+ background: color-mix(in srgb, var(--success) 14%, transparent);
381
+ border-radius: var(--radius-full);
382
+ font-variant-numeric: tabular-nums;
383
+ }
384
+
385
+ /* ── Specs ── */
386
+ .pcf__specs {
387
+ display: grid;
388
+ grid-template-columns: repeat(var(--lay-cols, 3), minmax(0, 1fr));
389
+ gap: var(--gap);
390
+ margin: 0;
391
+ padding: var(--pad);
392
+ background: var(--surface-2);
393
+ border: 1px solid var(--border);
394
+ border-radius: var(--radius);
395
+ }
396
+ .pcf__spec { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
397
+ .pcf__spec-term {
398
+ font-size: min(calc(var(--text-base) * 0.72), 11px);
399
+ font-weight: var(--weight-medium);
400
+ letter-spacing: 0.05em;
401
+ text-transform: uppercase;
402
+ color: var(--fg-subtle);
403
+ }
404
+ .pcf__spec-val {
405
+ margin: 0;
406
+ font-family: var(--font-mono);
407
+ font-size: min(calc(var(--text-base) * 0.9), 13px);
408
+ font-weight: var(--weight-medium);
409
+ color: var(--fg);
410
+ font-variant-numeric: tabular-nums;
411
+ white-space: nowrap;
412
+ }
413
+
414
+ /* ── Size selector ── */
415
+ .pcf__sizes { margin: 0; padding: 0; border: 0; min-width: 0; }
416
+ .pcf__sizes-legend {
417
+ display: flex;
418
+ align-items: baseline;
419
+ justify-content: space-between;
420
+ gap: var(--gap);
421
+ width: 100%;
422
+ padding: 0;
423
+ margin-bottom: calc(var(--gap) * 0.6);
424
+ font-size: calc(var(--text-base) * 0.78);
425
+ font-weight: var(--weight-bold);
426
+ letter-spacing: 0.06em;
427
+ text-transform: uppercase;
428
+ color: var(--fg);
429
+ }
430
+ .pcf__sizes-hint {
431
+ font-family: var(--font-mono);
432
+ font-size: calc(var(--text-base) * 0.72);
433
+ font-weight: var(--weight-normal);
434
+ letter-spacing: 0;
435
+ text-transform: none;
436
+ color: var(--fg-subtle);
437
+ }
438
+ .pcf__seg { display: grid; grid-template-columns: repeat(var(--lay-cols, 3), minmax(0, 1fr)); gap: calc(var(--gap) * 0.5); }
439
+ .pcf__opt {
440
+ position: relative;
441
+ min-width: 0;
442
+ display: flex;
443
+ flex-direction: column;
444
+ align-items: center;
445
+ gap: 1px;
446
+ padding: var(--pad-tight);
447
+ text-align: center;
448
+ cursor: pointer;
449
+ color: var(--fg-muted);
450
+ background: var(--surface);
451
+ border: 1px solid var(--border-strong);
452
+ border-radius: var(--radius);
453
+ transition: border-color var(--dur-fast) var(--ease),
454
+ background var(--dur-fast) var(--ease), color var(--dur-fast) var(--ease),
455
+ transform var(--dur-fast) var(--ease);
456
+ }
457
+ .pcf__opt:hover:not([data-oos]) { border-color: var(--accent); color: var(--fg); }
458
+ .pcf__opt[data-active] {
459
+ color: var(--accent-fg);
460
+ background: var(--accent);
461
+ border-color: var(--accent);
462
+ }
463
+ .pcf__opt[data-active] .pcf__opt-label,
464
+ .pcf__opt[data-active] .pcf__opt-sku { color: var(--accent-fg); opacity: 0.85; }
465
+ .pcf__opt[data-oos] {
466
+ cursor: not-allowed;
467
+ opacity: 0.55;
468
+ background: var(--surface-2);
469
+ border-style: dashed;
470
+ }
471
+ .pcf__opt:has(.pcf__radio:focus-visible) {
472
+ outline: 2px solid var(--ring);
473
+ outline-offset: 2px;
474
+ }
475
+ .pcf__radio {
476
+ /* Cover the whole chip so the operable target is where it visibly is:
477
+ the mark/badge lands on the chip center and a pixel click hits the input
478
+ directly — not the 1px hidden-control anti-pattern (cf. React Aria, §4c). */
479
+ position: absolute;
480
+ inset: 0;
481
+ width: 100%; height: 100%;
482
+ opacity: 0;
483
+ margin: 0;
484
+ cursor: pointer;
485
+ }
486
+ .pcf__opt[data-oos] .pcf__radio { cursor: not-allowed; }
487
+ .pcf__opt-code {
488
+ font-family: var(--font-display);
489
+ font-size: calc(var(--text-base) * 1.05);
490
+ font-weight: var(--weight-bold);
491
+ line-height: 1.1;
492
+ color: inherit;
493
+ }
494
+ .pcf__opt-label {
495
+ font-size: calc(var(--text-base) * 0.72);
496
+ font-weight: var(--weight-medium);
497
+ color: var(--fg-muted);
498
+ }
499
+ .pcf__opt-sku {
500
+ font-family: var(--font-mono);
501
+ font-size: calc(var(--text-base) * 0.6);
502
+ letter-spacing: -0.02em;
503
+ color: var(--fg-subtle);
504
+ max-width: 100%;
505
+ overflow: hidden;
506
+ text-overflow: ellipsis;
507
+ white-space: nowrap;
508
+ }
509
+
510
+ /* ── CTA ── */
511
+ .pcf__cta {
512
+ display: inline-flex;
513
+ align-items: center;
514
+ justify-content: center;
515
+ gap: calc(var(--gap) * 0.5);
516
+ width: 100%;
517
+ padding: var(--pad) var(--pad-loose);
518
+ font-family: var(--font);
519
+ font-size: calc(var(--text-base) * 1.02);
520
+ font-weight: var(--weight-bold);
521
+ letter-spacing: var(--tracking-tight);
522
+ color: var(--accent-fg);
523
+ background: var(--accent);
524
+ border: 1px solid var(--accent);
525
+ border-radius: var(--radius);
526
+ box-shadow: var(--shadow-sm);
527
+ cursor: pointer;
528
+ transition: transform var(--dur-fast) var(--ease-emphasis),
529
+ box-shadow var(--dur) var(--ease), background var(--dur) var(--ease),
530
+ opacity var(--dur) var(--ease);
531
+ }
532
+ .pcf__cta:hover:not(:disabled) { box-shadow: var(--shadow); transform: translateY(-1px); }
533
+ .pcf__cta:active:not(:disabled) { transform: translateY(0); box-shadow: var(--shadow-sm); }
534
+ .pcf__cta:focus-visible { outline: 2px solid var(--ring); outline-offset: 2px; }
535
+ .pcf__cta:disabled {
536
+ cursor: not-allowed;
537
+ opacity: 0.55;
538
+ color: var(--fg-subtle);
539
+ background: var(--surface-2);
540
+ border-color: var(--border-strong);
541
+ box-shadow: none;
542
+ }
543
+ .pcf__cta[data-added] { background: var(--success); border-color: var(--success); color: #fff; }
544
+ .pcf__cart { width: 1.15em; height: 1.15em; flex: none; }
545
+
546
+ @media (prefers-reduced-motion: reduce) {
547
+ .pcf, .pcf *, .pcf *::before, .pcf *::after { transition: none !important; }
548
+ }
549
+ `;
550
+
551
+ export default ProductCard;