@aparte/plugin-cards 0.0.0-stage → 0.29.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 (103) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -2
  3. package/dist/carousel-CPy-0JGN.js +63 -0
  4. package/dist/carousel-CPy-0JGN.js.map +1 -0
  5. package/dist/controls-BVX2u7sK.js +62 -0
  6. package/dist/controls-BVX2u7sK.js.map +1 -0
  7. package/dist/format-CchcJRum.js +720 -0
  8. package/dist/format-CchcJRum.js.map +1 -0
  9. package/dist/index.d.ts +28 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +35 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/items/icons.d.ts +12 -0
  14. package/dist/items/icons.d.ts.map +1 -0
  15. package/dist/items/index.d.ts +100 -0
  16. package/dist/items/index.d.ts.map +1 -0
  17. package/dist/items.js +698 -0
  18. package/dist/items.js.map +1 -0
  19. package/dist/kit/carousel.d.ts +17 -0
  20. package/dist/kit/carousel.d.ts.map +1 -0
  21. package/dist/kit/controls.d.ts +51 -0
  22. package/dist/kit/controls.d.ts.map +1 -0
  23. package/dist/kit/define.d.ts +194 -0
  24. package/dist/kit/define.d.ts.map +1 -0
  25. package/dist/kit/dom.d.ts +40 -0
  26. package/dist/kit/dom.d.ts.map +1 -0
  27. package/dist/kit/format.d.ts +49 -0
  28. package/dist/kit/format.d.ts.map +1 -0
  29. package/dist/kit/rating.d.ts +19 -0
  30. package/dist/kit/rating.d.ts.map +1 -0
  31. package/dist/kit/sparkline.d.ts +19 -0
  32. package/dist/kit/sparkline.d.ts.map +1 -0
  33. package/dist/kit/strings.d.ts +139 -0
  34. package/dist/kit/strings.d.ts.map +1 -0
  35. package/dist/link/icons.d.ts +16 -0
  36. package/dist/link/icons.d.ts.map +1 -0
  37. package/dist/link/index.d.ts +98 -0
  38. package/dist/link/index.d.ts.map +1 -0
  39. package/dist/link.js +387 -0
  40. package/dist/link.js.map +1 -0
  41. package/dist/media/icons.d.ts +28 -0
  42. package/dist/media/icons.d.ts.map +1 -0
  43. package/dist/media/index.d.ts +107 -0
  44. package/dist/media/index.d.ts.map +1 -0
  45. package/dist/media.js +610 -0
  46. package/dist/media.js.map +1 -0
  47. package/dist/order/icons.d.ts +49 -0
  48. package/dist/order/icons.d.ts.map +1 -0
  49. package/dist/order/index.d.ts +204 -0
  50. package/dist/order/index.d.ts.map +1 -0
  51. package/dist/order.js +1843 -0
  52. package/dist/order.js.map +1 -0
  53. package/dist/place/icons.d.ts +18 -0
  54. package/dist/place/icons.d.ts.map +1 -0
  55. package/dist/place/index.d.ts +207 -0
  56. package/dist/place/index.d.ts.map +1 -0
  57. package/dist/place.js +1456 -0
  58. package/dist/place.js.map +1 -0
  59. package/dist/plan/icons.d.ts +25 -0
  60. package/dist/plan/icons.d.ts.map +1 -0
  61. package/dist/plan/index.d.ts +153 -0
  62. package/dist/plan/index.d.ts.map +1 -0
  63. package/dist/plan.js +1191 -0
  64. package/dist/plan.js.map +1 -0
  65. package/dist/product/icons.d.ts +16 -0
  66. package/dist/product/icons.d.ts.map +1 -0
  67. package/dist/product/index.d.ts +133 -0
  68. package/dist/product/index.d.ts.map +1 -0
  69. package/dist/product.js +1245 -0
  70. package/dist/product.js.map +1 -0
  71. package/dist/rating-B8Mdx_b_.js +35 -0
  72. package/dist/rating-B8Mdx_b_.js.map +1 -0
  73. package/dist/score/icons.d.ts +35 -0
  74. package/dist/score/icons.d.ts.map +1 -0
  75. package/dist/score/index.d.ts +185 -0
  76. package/dist/score/index.d.ts.map +1 -0
  77. package/dist/score.js +1843 -0
  78. package/dist/score.js.map +1 -0
  79. package/dist/sparkline-C-cA9obz.js +56 -0
  80. package/dist/sparkline-C-cA9obz.js.map +1 -0
  81. package/dist/stats/icons.d.ts +19 -0
  82. package/dist/stats/icons.d.ts.map +1 -0
  83. package/dist/stats/index.d.ts +137 -0
  84. package/dist/stats/index.d.ts.map +1 -0
  85. package/dist/stats.js +1085 -0
  86. package/dist/stats.js.map +1 -0
  87. package/dist/stock/chart.d.ts +135 -0
  88. package/dist/stock/chart.d.ts.map +1 -0
  89. package/dist/stock/icons.d.ts +15 -0
  90. package/dist/stock/icons.d.ts.map +1 -0
  91. package/dist/stock/index.d.ts +195 -0
  92. package/dist/stock/index.d.ts.map +1 -0
  93. package/dist/stock.js +1013 -0
  94. package/dist/stock.js.map +1 -0
  95. package/dist/weather/conditions.d.ts +25 -0
  96. package/dist/weather/conditions.d.ts.map +1 -0
  97. package/dist/weather/icons.d.ts +12 -0
  98. package/dist/weather/icons.d.ts.map +1 -0
  99. package/dist/weather/index.d.ts +146 -0
  100. package/dist/weather/index.d.ts.map +1 -0
  101. package/dist/weather.js +1162 -0
  102. package/dist/weather.js.map +1 -0
  103. package/package.json +137 -3
@@ -0,0 +1,1245 @@
1
+ import { d as defineCard, o as str, b as formatNumber, p as word, e as el, k as glyph, h as formatPrice, c as currencyCode, w as webUrl, r as readPriceText, n as num, g as formatPercent } from "./format-CchcJRum.js";
2
+ import { c as carousel } from "./carousel-CPy-0JGN.js";
3
+ import { r as rating } from "./rating-B8Mdx_b_.js";
4
+ const svg = (body) => `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg">${body}</svg>`;
5
+ const BAG = svg('<path d="M5.5 8h13l-1 11.2a1.8 1.8 0 0 1-1.8 1.6H8.3a1.8 1.8 0 0 1-1.8-1.6z"/><path d="M9 10.5V6.8a3 3 0 0 1 6 0v3.7"/>');
6
+ const TRUCK = svg('<path d="M2.8 6.5h10.4v9.3H2.8z"/><path d="M13.2 9.5h4.1l3 3.2v3.1h-7.1"/><circle cx="7" cy="17.3" r="1.7"/><circle cx="17" cy="17.3" r="1.7"/>');
7
+ const SEARCH = svg('<circle cx="10.8" cy="10.8" r="6.3"/><path d="M15.5 15.5l4.7 4.7"/>');
8
+ const TAG = svg('<path d="M3.5 12.6V4.8a1.3 1.3 0 0 1 1.3-1.3h7.8l8 8a1.3 1.3 0 0 1 0 1.8l-7 7a1.3 1.3 0 0 1-1.8 0z"/><circle cx="8.3" cy="8.3" r="1.5"/>');
9
+ const productStyles = `/* =========================================
10
+ The product card — @aparte/plugin-cards/product
11
+ =========================================
12
+ Three variants of one card, chosen by \`data-variant\` on its root:
13
+ card a tile: the photo, the shop, the name, the stars, the price. Several
14
+ products are a row of tiles that scrolls, and the card drops its own
15
+ frame so the tiles are not boxes inside a box.
16
+ compact a row per product — thumbnail, name, stars, price — a list to scan.
17
+ detail one product at length: the photo beside the facts on a wide card, above
18
+ them on a narrow one; the others in a small row under it.
19
+ Every colour is a theme token or derived from one, so light and dark follow the chat.
20
+ */
21
+
22
+ .aparte-product-card {
23
+ --aparte-product-tile-size: 12.5rem;
24
+ --aparte-product-thumb-size: 3.5rem;
25
+ --aparte-product-media-radius: var(--aparte-radius-lg);
26
+ /* A product photo is shot on white. The plate it sits on is near-white in BOTH schemes
27
+ (derived from the ink, so it is a hair off white in light and a hair off cream in
28
+ dark) and the photo is multiplied into it: its white background melts into the plate
29
+ instead of sitting as a white square on a grey one. A photo on a dark plate in dark
30
+ mode keeps its white box whatever we do — the shops' own apps show it light too. */
31
+ --aparte-product-media-bg: color-mix(in oklab, var(--aparte-text) 5%, #fff);
32
+ --aparte-product-image-fit: contain;
33
+ --aparte-product-image-inset: 7%;
34
+ /* Where there is no photo: the theme's own surfaces, warmed by its accent. */
35
+ --aparte-product-placeholder: linear-gradient(
36
+ 145deg,
37
+ color-mix(in oklab, var(--aparte-primary) 16%, var(--aparte-surface-2)) 0%,
38
+ var(--aparte-surface-2) 55%,
39
+ color-mix(in oklab, var(--aparte-text) 5%, var(--aparte-surface-2)) 100%
40
+ );
41
+ --aparte-product-placeholder-ink: color-mix(in oklab, var(--aparte-text) 30%, transparent);
42
+ --aparte-product-price-color: var(--aparte-text);
43
+ --aparte-product-hover-border: color-mix(in oklab, var(--aparte-text) 22%, var(--aparte-border));
44
+ --aparte-product-hover-shadow: 0 8px 18px -12px rgb(0 0 0 / 0.38);
45
+ }
46
+
47
+ /* ── Widths: each variant asks for its own; the bubble's limit caps them all ── */
48
+
49
+ .aparte-product-card[data-variant="card"] { --aparte-rich-card-max-inline-size: 16.5rem; }
50
+ .aparte-product-card[data-variant="compact"] { --aparte-rich-card-max-inline-size: 28rem; }
51
+ .aparte-product-card[data-variant="detail"] { --aparte-rich-card-max-inline-size: 36rem; }
52
+ .aparte-product-card:has(> .aparte-product__collection) { --aparte-rich-card-max-inline-size: 42rem; }
53
+
54
+ /* Several tiles: each is framed, so the card is not. The kit's buttons under it lose
55
+ their band, which would otherwise float as a strip under nothing. */
56
+ .aparte-product-card:has(> .aparte-product__collection) {
57
+ overflow: visible;
58
+ border-color: transparent;
59
+ border-radius: 0;
60
+ background: transparent;
61
+ }
62
+
63
+ .aparte-product-card:has(> .aparte-product__collection) > .aparte-rich-card__actions {
64
+ padding-inline: 0;
65
+ border-block-start: 0;
66
+ background: none;
67
+ }
68
+
69
+ /* ── Headings: the collection's title, the query of a running search ─── */
70
+
71
+ .aparte-product__heading {
72
+ margin-block-end: var(--aparte-space-4);
73
+ font-size: var(--aparte-font-size-md);
74
+ font-weight: var(--aparte-font-weight-semibold);
75
+ overflow-wrap: anywhere;
76
+ }
77
+
78
+ .aparte-product-card[data-variant="compact"] > .aparte-product__heading {
79
+ margin: 0;
80
+ padding: var(--aparte-space-5) var(--aparte-space-6) var(--aparte-space-2);
81
+ }
82
+
83
+ .aparte-product__query {
84
+ display: flex;
85
+ align-items: center;
86
+ gap: var(--aparte-space-3);
87
+ min-inline-size: 0;
88
+ margin-block-end: var(--aparte-space-4);
89
+ color: var(--aparte-text-muted);
90
+ font-size: var(--aparte-font-size-md);
91
+ --aparte-rich-card-glyph-size: 1rem;
92
+ }
93
+
94
+ .aparte-product__query-text {
95
+ overflow: hidden;
96
+ text-overflow: ellipsis;
97
+ white-space: nowrap;
98
+ }
99
+
100
+ .aparte-product-card[data-variant="compact"] > .aparte-product__query,
101
+ .aparte-product-card[data-variant="detail"] > .aparte-product__query {
102
+ margin: 0;
103
+ padding: var(--aparte-space-5) var(--aparte-space-6) 0;
104
+ }
105
+
106
+ /* ── The carousel, in product sizes ──────────────────────────────────── */
107
+
108
+ .aparte-product-card .aparte-rich-card__carousel {
109
+ --aparte-rich-card-slide-size: var(--aparte-product-tile-size);
110
+ }
111
+
112
+ /* ── A product, whatever its shape ───────────────────────────────────── */
113
+
114
+ .aparte-product {
115
+ position: relative;
116
+ min-inline-size: 0;
117
+ color: var(--aparte-text);
118
+ }
119
+
120
+ /* The title's link covers the tile or the row: one link per product, read once. */
121
+ .aparte-product__link {
122
+ color: inherit;
123
+ text-decoration: none;
124
+ }
125
+
126
+ .aparte-product--tile .aparte-product__link,
127
+ .aparte-product--row .aparte-product__link {
128
+ outline: none;
129
+ }
130
+
131
+ .aparte-product--tile .aparte-product__link::after,
132
+ .aparte-product--row .aparte-product__link::after {
133
+ content: "";
134
+ position: absolute;
135
+ inset: 0;
136
+ border-radius: inherit;
137
+ }
138
+
139
+ .aparte-product[data-linked]:hover .aparte-product__title {
140
+ text-decoration: underline;
141
+ text-decoration-thickness: 1px;
142
+ text-underline-offset: 0.18em;
143
+ }
144
+
145
+ /* The ring is the product's, since the link is all of it. */
146
+ .aparte-product--tile:has(.aparte-product__link:focus-visible) {
147
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
148
+ outline-offset: var(--aparte-focus-outline-offset);
149
+ }
150
+
151
+ .aparte-product--row:has(.aparte-product__link:focus-visible) {
152
+ /* A row reaches the card's clipped edges: its ring is drawn inside. */
153
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
154
+ outline-offset: calc(var(--aparte-focus-outline-width) * -1);
155
+ }
156
+
157
+ /* ── The photo ───────────────────────────────────────────────────────── */
158
+
159
+ .aparte-product__media {
160
+ position: relative;
161
+ display: grid;
162
+ place-items: center;
163
+ aspect-ratio: 1;
164
+ overflow: hidden;
165
+ border-radius: var(--aparte-product-media-radius);
166
+ background: var(--aparte-product-media-bg);
167
+ isolation: isolate;
168
+ }
169
+
170
+ .aparte-product__media[data-empty] {
171
+ background: var(--aparte-product-placeholder);
172
+ }
173
+
174
+ .aparte-product__placeholder {
175
+ grid-area: 1 / 1;
176
+ color: var(--aparte-product-placeholder-ink);
177
+ --aparte-rich-card-glyph-size: clamp(1.25rem, 30%, 3.5rem);
178
+ }
179
+
180
+ .aparte-product__media:not([data-empty]) .aparte-product__placeholder { visibility: hidden; }
181
+
182
+ .aparte-product__image {
183
+ grid-area: 1 / 1;
184
+ box-sizing: border-box;
185
+ inline-size: 100%;
186
+ block-size: 100%;
187
+ padding: var(--aparte-product-image-inset);
188
+ object-fit: var(--aparte-product-image-fit);
189
+ mix-blend-mode: multiply;
190
+ transition: scale var(--aparte-duration-slower) ease, opacity var(--aparte-duration-base) ease;
191
+ }
192
+
193
+ .aparte-product__image[hidden] { display: none; }
194
+
195
+ .aparte-product[data-linked]:hover .aparte-product__image,
196
+ .aparte-product__media-link:hover .aparte-product__image {
197
+ scale: 1.04;
198
+ }
199
+
200
+ /* Out of stock: the photo steps back, the line under it says why. */
201
+ .aparte-product[data-availability="out-of-stock"] .aparte-product__image {
202
+ opacity: 0.55;
203
+ filter: grayscale(0.5);
204
+ }
205
+
206
+ .aparte-product__flags {
207
+ position: absolute;
208
+ z-index: 1;
209
+ inset-block-start: var(--aparte-space-3);
210
+ inset-inline-start: var(--aparte-space-3);
211
+ display: flex;
212
+ flex-wrap: wrap;
213
+ gap: var(--aparte-space-2);
214
+ max-inline-size: calc(100% - var(--aparte-space-3) * 2);
215
+ }
216
+
217
+ /* The shop's word: a pill of the card's own surface over the plate, in either scheme. */
218
+ .aparte-product__flags .aparte-product__badge {
219
+ max-inline-size: 100%;
220
+ overflow: hidden;
221
+ text-overflow: ellipsis;
222
+ background: color-mix(in oklab, var(--aparte-surface-1) 90%, transparent);
223
+ color: var(--aparte-text);
224
+ box-shadow: 0 1px 3px rgb(0 0 0 / 0.14);
225
+ backdrop-filter: blur(6px);
226
+ }
227
+
228
+ .aparte-product__discount {
229
+ font-variant-numeric: tabular-nums;
230
+ }
231
+
232
+ /* ── The words beside the photo ──────────────────────────────────────── */
233
+
234
+ .aparte-product__body {
235
+ display: flex;
236
+ flex: 1;
237
+ flex-direction: column;
238
+ gap: var(--aparte-space-2);
239
+ min-inline-size: 0;
240
+ }
241
+
242
+ .aparte-product__merchant {
243
+ display: flex;
244
+ align-items: center;
245
+ gap: var(--aparte-space-2);
246
+ min-inline-size: 0;
247
+ color: var(--aparte-text-muted);
248
+ font-size: var(--aparte-font-size-xs);
249
+ }
250
+
251
+ .aparte-product__merchant-name {
252
+ overflow: hidden;
253
+ text-overflow: ellipsis;
254
+ white-space: nowrap;
255
+ }
256
+
257
+ .aparte-product__logo {
258
+ flex: none;
259
+ inline-size: 1rem;
260
+ block-size: 1rem;
261
+ border-radius: var(--aparte-radius-xs);
262
+ object-fit: cover;
263
+ }
264
+
265
+ .aparte-product__logo[hidden] { display: none; }
266
+
267
+ .aparte-product__title {
268
+ display: -webkit-box;
269
+ overflow: hidden;
270
+ -webkit-box-orient: vertical;
271
+ -webkit-line-clamp: 2;
272
+ line-clamp: 2;
273
+ font-size: var(--aparte-font-size-md);
274
+ font-weight: var(--aparte-font-weight-medium);
275
+ line-height: 1.3;
276
+ overflow-wrap: anywhere;
277
+ }
278
+
279
+ .aparte-product__price {
280
+ display: flex;
281
+ flex-wrap: wrap;
282
+ align-items: baseline;
283
+ gap: var(--aparte-space-1) var(--aparte-space-3);
284
+ font-variant-numeric: tabular-nums;
285
+ }
286
+
287
+ .aparte-product__now {
288
+ color: var(--aparte-product-price-color);
289
+ font-size: var(--aparte-font-size-lg);
290
+ font-weight: var(--aparte-font-weight-bold);
291
+ letter-spacing: -0.01em;
292
+ white-space: nowrap;
293
+ }
294
+
295
+ .aparte-product__was {
296
+ color: var(--aparte-text-muted);
297
+ font-size: var(--aparte-font-size-sm);
298
+ text-decoration-thickness: 1px;
299
+ white-space: nowrap;
300
+ }
301
+
302
+ .aparte-product__price .aparte-product__discount { align-self: center; }
303
+
304
+ /* Stock: a dot in the state's colour, the word in its readable ink. */
305
+ .aparte-product__stock {
306
+ --aparte-product-stock-tone: var(--aparte-neutral);
307
+ --aparte-product-stock-ink: var(--aparte-text-muted);
308
+ display: inline-flex;
309
+ align-items: center;
310
+ gap: var(--aparte-space-2);
311
+ min-inline-size: 0;
312
+ color: var(--aparte-product-stock-ink);
313
+ font-size: var(--aparte-font-size-xs);
314
+ font-weight: var(--aparte-font-weight-medium);
315
+ }
316
+
317
+ .aparte-product__stock[data-availability="in-stock"] { --aparte-product-stock-tone: var(--aparte-success); --aparte-product-stock-ink: var(--aparte-success-ink); }
318
+ .aparte-product__stock[data-availability="low-stock"] { --aparte-product-stock-tone: var(--aparte-warning); --aparte-product-stock-ink: var(--aparte-warning-ink); }
319
+ .aparte-product__stock[data-availability="out-of-stock"] { --aparte-product-stock-tone: var(--aparte-error); --aparte-product-stock-ink: var(--aparte-error-ink); }
320
+ .aparte-product__stock[data-availability="preorder"] { --aparte-product-stock-tone: var(--aparte-info); --aparte-product-stock-ink: var(--aparte-info-ink); }
321
+
322
+ .aparte-product__dot {
323
+ flex: none;
324
+ inline-size: 0.4375rem;
325
+ block-size: 0.4375rem;
326
+ border-radius: var(--aparte-radius-full);
327
+ background: var(--aparte-product-stock-tone);
328
+ box-shadow: 0 0 0 3px color-mix(in oklab, var(--aparte-product-stock-tone) 20%, transparent);
329
+ }
330
+
331
+ .aparte-product__stock-text {
332
+ overflow: hidden;
333
+ text-overflow: ellipsis;
334
+ white-space: nowrap;
335
+ }
336
+
337
+ .aparte-product__shipping {
338
+ display: inline-flex;
339
+ align-items: center;
340
+ gap: var(--aparte-space-2);
341
+ min-inline-size: 0;
342
+ color: var(--aparte-text-muted);
343
+ font-size: var(--aparte-font-size-xs);
344
+ --aparte-rich-card-glyph-size: 1rem;
345
+ }
346
+
347
+ .aparte-product__shipping[data-free] {
348
+ color: var(--aparte-success-ink);
349
+ font-weight: var(--aparte-font-weight-medium);
350
+ }
351
+
352
+ .aparte-product__shipping-icon:dir(rtl) { transform: scaleX(-1); }
353
+
354
+ /* Buttons sit above the link that covers the product. */
355
+ .aparte-product__actions {
356
+ position: relative;
357
+ z-index: 1;
358
+ display: flex;
359
+ flex-wrap: wrap;
360
+ gap: var(--aparte-space-2);
361
+ }
362
+
363
+ /* ── card: the tile ──────────────────────────────────────────────────── */
364
+
365
+ .aparte-product--tile {
366
+ display: flex;
367
+ flex-direction: column;
368
+ gap: var(--aparte-space-4);
369
+ padding: var(--aparte-space-3) var(--aparte-space-3) var(--aparte-space-4);
370
+ border-radius: var(--aparte-card-radius);
371
+ }
372
+
373
+ .aparte-product--tile .aparte-product__body { padding-inline: var(--aparte-space-2); }
374
+
375
+ /* Two lines held even when the name takes one, so the prices in a row line up. */
376
+ .aparte-product--tile .aparte-product__title { min-block-size: 2lh; }
377
+
378
+ /* The price sits at the foot of the tile, level with its neighbours'. */
379
+ .aparte-product--tile .aparte-product__price {
380
+ margin-block-start: auto;
381
+ padding-block-start: var(--aparte-space-1);
382
+ }
383
+
384
+ .aparte-product--tile .aparte-product__actions { padding-inline: var(--aparte-space-2); }
385
+ .aparte-product--tile .aparte-product__action { flex: 1 1 auto; justify-content: center; }
386
+
387
+ .aparte-product__collection .aparte-product--tile,
388
+ .aparte-product__more .aparte-product--tile {
389
+ border: var(--aparte-border-width) solid var(--aparte-border);
390
+ background: var(--aparte-surface-1);
391
+ transition:
392
+ border-color var(--aparte-duration-base) ease,
393
+ box-shadow var(--aparte-duration-base) ease,
394
+ translate var(--aparte-duration-base) ease;
395
+ }
396
+
397
+ .aparte-product__collection .aparte-product--tile[data-linked]:hover,
398
+ .aparte-product__more .aparte-product--tile[data-linked]:hover {
399
+ border-color: var(--aparte-product-hover-border);
400
+ box-shadow: var(--aparte-product-hover-shadow);
401
+ translate: 0 -2px;
402
+ }
403
+
404
+ /* A mini tile: the others under a detail. */
405
+ .aparte-product--mini {
406
+ gap: var(--aparte-space-3);
407
+ padding: var(--aparte-space-2) var(--aparte-space-2) var(--aparte-space-3);
408
+ }
409
+
410
+ .aparte-product--mini .aparte-product__body { gap: var(--aparte-space-1); }
411
+ .aparte-product--mini .aparte-product__title { font-size: var(--aparte-font-size-sm); }
412
+ .aparte-product--mini .aparte-product__now { font-size: var(--aparte-font-size-md); }
413
+ .aparte-product--mini .aparte-product__was { font-size: var(--aparte-font-size-xs); }
414
+
415
+ /* ── compact: the rows ───────────────────────────────────────────────── */
416
+
417
+ /* On the drawn card only: a one-line state shrinks to its words, and a size container
418
+ cannot. */
419
+ .aparte-product-card[data-variant="compact"][data-state="ready"] {
420
+ container: aparte-product-rows / inline-size;
421
+ }
422
+
423
+ .aparte-product__rows {
424
+ margin: 0;
425
+ padding: var(--aparte-space-2) 0;
426
+ list-style: none;
427
+ }
428
+
429
+ /* A divider starting after the thumbnail, the way a settings list draws it. */
430
+ .aparte-product__item { position: relative; }
431
+
432
+ .aparte-product__item + .aparte-product__item::before {
433
+ content: "";
434
+ position: absolute;
435
+ inset-block-start: 0;
436
+ inset-inline: calc(var(--aparte-space-6) + var(--aparte-product-thumb-size) + var(--aparte-space-5)) var(--aparte-space-6);
437
+ border-block-start: var(--aparte-border-width) solid var(--aparte-border);
438
+ }
439
+
440
+ .aparte-product--row {
441
+ display: grid;
442
+ grid-template-columns: var(--aparte-product-thumb-size) minmax(0, 1fr) auto;
443
+ align-items: center;
444
+ column-gap: var(--aparte-space-5);
445
+ padding: var(--aparte-space-4) var(--aparte-space-6);
446
+ transition: background-color var(--aparte-duration-fast) ease;
447
+ }
448
+
449
+ .aparte-product--row:has(> .aparte-product__actions) {
450
+ grid-template-columns: var(--aparte-product-thumb-size) minmax(0, 1fr) auto auto;
451
+ }
452
+
453
+ .aparte-product--row[data-linked]:hover { background: var(--aparte-surface-2); }
454
+
455
+ .aparte-product__thumb {
456
+ inline-size: var(--aparte-product-thumb-size);
457
+ --aparte-product-media-radius: var(--aparte-radius-md);
458
+ --aparte-product-image-inset: 4%;
459
+ }
460
+
461
+ .aparte-product__thumb .aparte-product__placeholder { --aparte-rich-card-glyph-size: 45%; }
462
+
463
+ .aparte-product__main {
464
+ display: flex;
465
+ flex-direction: column;
466
+ gap: var(--aparte-space-1);
467
+ min-inline-size: 0;
468
+ }
469
+
470
+ .aparte-product__meta {
471
+ display: flex;
472
+ flex-wrap: wrap;
473
+ align-items: center;
474
+ gap: var(--aparte-space-1) var(--aparte-space-3);
475
+ min-inline-size: 0;
476
+ color: var(--aparte-text-muted);
477
+ font-size: var(--aparte-font-size-xs);
478
+ }
479
+
480
+ .aparte-product__meta .aparte-rich-card__rating { font-size: var(--aparte-font-size-xs); }
481
+
482
+ .aparte-product--row .aparte-product__price {
483
+ flex-direction: column;
484
+ align-items: flex-end;
485
+ gap: 0;
486
+ text-align: end;
487
+ }
488
+
489
+ .aparte-product--row .aparte-product__now { font-size: var(--aparte-font-size-base); }
490
+ .aparte-product--row .aparte-product__was { font-size: var(--aparte-font-size-xs); }
491
+
492
+ /* On a narrow card a row's button goes under its name rather than squeeze it. */
493
+ @container aparte-product-rows (max-width: 24rem) {
494
+ .aparte-product--row:has(> .aparte-product__actions) {
495
+ grid-template-columns: var(--aparte-product-thumb-size) minmax(0, 1fr) auto;
496
+ }
497
+
498
+ .aparte-product--row > .aparte-product__actions {
499
+ grid-column: 2 / -1;
500
+ margin-block-start: var(--aparte-space-3);
501
+ }
502
+ }
503
+
504
+ /* ── detail: one product at length ───────────────────────────────────── */
505
+
506
+ .aparte-product__detail {
507
+ container: aparte-product-detail / inline-size;
508
+ }
509
+
510
+ .aparte-product--detail {
511
+ display: grid;
512
+ gap: var(--aparte-space-6);
513
+ padding: var(--aparte-space-6);
514
+ }
515
+
516
+ .aparte-product--detail .aparte-product__media,
517
+ .aparte-product--detail .aparte-product__sk-media {
518
+ aspect-ratio: 4 / 3;
519
+ }
520
+
521
+ .aparte-product__media-link {
522
+ display: block;
523
+ border-radius: var(--aparte-product-media-radius);
524
+ }
525
+
526
+ .aparte-product--detail .aparte-product__body { gap: var(--aparte-space-3); }
527
+
528
+ /* The clamp clips what overflows it, the focus ring included: the padding gives the ring
529
+ room inside, and the margin takes that room back from the layout. */
530
+ .aparte-product--detail .aparte-product__title {
531
+ -webkit-line-clamp: 4;
532
+ line-clamp: 4;
533
+ margin: calc(var(--aparte-space-2) * -1);
534
+ padding: var(--aparte-space-2);
535
+ font-size: var(--aparte-font-size-lg);
536
+ font-weight: var(--aparte-font-weight-semibold);
537
+ }
538
+
539
+ .aparte-product--detail .aparte-product__link:hover { text-decoration: underline; text-underline-offset: 0.18em; }
540
+
541
+ .aparte-product--detail .aparte-product__link:focus-visible {
542
+ border-radius: var(--aparte-radius-xs);
543
+ outline: var(--aparte-focus-outline-width) solid var(--aparte-border-focus);
544
+ outline-offset: var(--aparte-focus-outline-offset);
545
+ }
546
+
547
+ .aparte-product--detail .aparte-product__now { font-size: var(--aparte-font-size-2xl); }
548
+ .aparte-product--detail .aparte-product__was { font-size: var(--aparte-font-size-md); }
549
+
550
+ .aparte-product__facts {
551
+ display: flex;
552
+ flex-wrap: wrap;
553
+ align-items: center;
554
+ gap: var(--aparte-space-2) var(--aparte-space-6);
555
+ }
556
+
557
+ .aparte-product__description {
558
+ display: -webkit-box;
559
+ margin: 0;
560
+ overflow: hidden;
561
+ -webkit-box-orient: vertical;
562
+ -webkit-line-clamp: 3;
563
+ line-clamp: 3;
564
+ color: var(--aparte-text-muted);
565
+ font-size: var(--aparte-font-size-sm);
566
+ line-height: 1.5;
567
+ overflow-wrap: anywhere;
568
+ }
569
+
570
+ .aparte-product__options {
571
+ display: flex;
572
+ flex-wrap: wrap;
573
+ gap: var(--aparte-space-2);
574
+ margin: 0;
575
+ padding: 0;
576
+ list-style: none;
577
+ }
578
+
579
+ .aparte-product__option--more { color: var(--aparte-text-muted); }
580
+
581
+ .aparte-product--detail .aparte-product__actions { margin-block-start: var(--aparte-space-2); }
582
+ .aparte-product--detail .aparte-product__action { flex: 1 1 8rem; justify-content: center; }
583
+
584
+ @container aparte-product-detail (min-width: 30rem) {
585
+ .aparte-product--detail {
586
+ grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
587
+ align-items: start;
588
+ gap: var(--aparte-space-7);
589
+ }
590
+
591
+ .aparte-product--detail .aparte-product__media,
592
+ .aparte-product--detail .aparte-product__sk-media {
593
+ aspect-ratio: 1;
594
+ }
595
+ }
596
+
597
+ /* The others, under the detail. */
598
+ .aparte-product__more {
599
+ --aparte-product-tile-size: 9.5rem;
600
+ padding: var(--aparte-space-5) var(--aparte-space-6) var(--aparte-space-6);
601
+ border-block-start: var(--aparte-border-width) solid var(--aparte-border);
602
+ background: color-mix(in oklab, var(--aparte-surface-2) 45%, var(--aparte-surface-1));
603
+ }
604
+
605
+ .aparte-product__more-title {
606
+ margin-block-end: var(--aparte-space-4);
607
+ color: var(--aparte-text-muted);
608
+ font-size: var(--aparte-font-size-sm);
609
+ font-weight: var(--aparte-font-weight-semibold);
610
+ }
611
+
612
+ /* ── Loading ─────────────────────────────────────────────────────────── */
613
+
614
+ .aparte-product__sk-track {
615
+ display: grid;
616
+ grid-auto-flow: column;
617
+ grid-auto-columns: var(--aparte-product-tile-size);
618
+ column-gap: var(--aparte-space-4);
619
+ overflow: hidden;
620
+ }
621
+
622
+ .aparte-product-card .aparte-product__sk-media {
623
+ block-size: auto;
624
+ aspect-ratio: 1;
625
+ border-radius: var(--aparte-product-media-radius);
626
+ }
627
+
628
+ .aparte-product-card .aparte-product__sk-thumb {
629
+ inline-size: var(--aparte-product-thumb-size);
630
+ block-size: auto;
631
+ aspect-ratio: 1;
632
+ border-radius: var(--aparte-radius-md);
633
+ }
634
+
635
+ .aparte-product-card .aparte-product__sk-line--short { inline-size: 60%; }
636
+
637
+ .aparte-product-card .aparte-product__sk-price {
638
+ inline-size: 4.5rem;
639
+ block-size: 1.125rem;
640
+ margin-block-start: auto;
641
+ }
642
+
643
+ .aparte-product--row .aparte-product__sk-price { margin-block-start: 0; }
644
+
645
+ /* ── Small screens, motion ───────────────────────────────────────────── */
646
+
647
+ @media (max-width: 380px) {
648
+ .aparte-product-card { --aparte-product-tile-size: 10.75rem; --aparte-product-thumb-size: 3rem; }
649
+ .aparte-product--row { column-gap: var(--aparte-space-4); padding-inline: var(--aparte-space-5); }
650
+ .aparte-product__item + .aparte-product__item::before {
651
+ inset-inline: calc(var(--aparte-space-5) + var(--aparte-product-thumb-size) + var(--aparte-space-4)) var(--aparte-space-5);
652
+ }
653
+ .aparte-product--detail { padding: var(--aparte-space-5); }
654
+ .aparte-product__more { padding-inline: var(--aparte-space-5); }
655
+ }
656
+
657
+ /* The kit stops every transition under reduced motion; the hover's lift and zoom are
658
+ end states, not transitions, so they are taken out here. */
659
+ @media (prefers-reduced-motion: reduce) {
660
+ .aparte-product__collection .aparte-product--tile[data-linked]:hover,
661
+ .aparte-product__more .aparte-product--tile[data-linked]:hover { translate: none; }
662
+
663
+ .aparte-product[data-linked]:hover .aparte-product__image,
664
+ .aparte-product__media-link:hover .aparte-product__image { scale: none; }
665
+ }
666
+ `;
667
+ const P = {
668
+ cardsProductTitle: { en: "Products", fr: "Produits", de: "Produkte", es: "Productos", it: "Prodotti", ja: "商品", ko: "상품", pt: "Produtos", ru: "Товары", zh: "商品" },
669
+ cardsProductCount: { en: "{n} products", fr: "{n} produits", de: "{n} Produkte", es: "{n} productos", it: "{n} prodotti", ja: "{n}件の商品", ko: "상품 {n}개", pt: "{n} produtos", ru: "Товаров: {n}", zh: "{n} 件商品" },
670
+ cardsProductInStock: { en: "In stock", fr: "En stock", de: "Auf Lager", es: "En stock", it: "Disponibile", ja: "在庫あり", ko: "재고 있음", pt: "Em estoque", ru: "В наличии", zh: "有货" },
671
+ cardsProductLowStock: { en: "Only a few left", fr: "Plus que quelques-uns", de: "Nur noch wenige", es: "Quedan pocas unidades", it: "Ultimi pezzi", ja: "残りわずか", ko: "품절 임박", pt: "Últimas unidades", ru: "Осталось мало", zh: "仅剩少量" },
672
+ cardsProductOutOfStock: { en: "Out of stock", fr: "Rupture de stock", de: "Ausverkauft", es: "Agotado", it: "Esaurito", ja: "在庫切れ", ko: "품절", pt: "Esgotado", ru: "Нет в наличии", zh: "缺货" },
673
+ cardsProductPreorder: { en: "Pre-order", fr: "Précommande", de: "Vorbestellbar", es: "Preventa", it: "Preordine", ja: "予約受付中", ko: "예약 판매", pt: "Pré-venda", ru: "Предзаказ", zh: "预售" },
674
+ cardsProductWas: { en: "Was", fr: "Au lieu de", de: "Statt", es: "Antes", it: "Prima", ja: "通常価格", ko: "정가", pt: "De", ru: "Было", zh: "原价" },
675
+ cardsProductFreeShipping: { en: "Free delivery", fr: "Livraison gratuite", de: "Kostenloser Versand", es: "Envío gratis", it: "Spedizione gratuita", ja: "送料無料", ko: "무료 배송", pt: "Frete grátis", ru: "Бесплатная доставка", zh: "免运费" },
676
+ cardsProductShippingCost: { en: "{price} delivery", fr: "Livraison {price}", de: "Versand {price}", es: "Envío {price}", it: "Spedizione {price}", ja: "送料 {price}", ko: "배송비 {price}", pt: "Frete {price}", ru: "Доставка {price}", zh: "运费 {price}" },
677
+ cardsProductOptions: { en: "Options", fr: "Options", de: "Optionen", es: "Opciones", it: "Opzioni", ja: "オプション", ko: "옵션", pt: "Opções", ru: "Варианты", zh: "可选项" },
678
+ cardsProductMoreOptions: { en: "{n} more", fr: "{n} de plus", de: "{n} weitere", es: "{n} más", it: "altri {n}", ja: "ほか{n}件", ko: "{n}개 더", pt: "mais {n}", ru: "ещё {n}", zh: "还有 {n} 个" },
679
+ cardsProductMore: { en: "More options", fr: "Autres choix", de: "Weitere Angebote", es: "Más opciones", it: "Altre proposte", ja: "その他の候補", ko: "다른 상품", pt: "Mais opções", ru: "Другие варианты", zh: "更多选择" }
680
+ };
681
+ const STOCK_WORD = {
682
+ "in-stock": "cardsProductInStock",
683
+ "low-stock": "cardsProductLowStock",
684
+ "out-of-stock": "cardsProductOutOfStock",
685
+ preorder: "cardsProductPreorder"
686
+ };
687
+ const PRODUCTS_SHOWN = 10;
688
+ const OPTIONS_SHOWN = 6;
689
+ const ACTIONS_PER_PRODUCT = 2;
690
+ const isObject = (value) => !!value && typeof value === "object" && !Array.isArray(value);
691
+ function pick(r, keys) {
692
+ if (!r) return void 0;
693
+ for (const key of keys) if (r[key] !== void 0 && r[key] !== null) return r[key];
694
+ return void 0;
695
+ }
696
+ function listOf(value) {
697
+ if (Array.isArray(value)) return value;
698
+ if (!isObject(value)) return [];
699
+ if (Array.isArray(value["nodes"])) return value["nodes"];
700
+ if (Array.isArray(value["edges"])) return value["edges"].map((e) => isObject(e) ? e["node"] : void 0);
701
+ return [];
702
+ }
703
+ function readMoney(value) {
704
+ if (typeof value === "number") return { amount: Number.isFinite(value) && value >= 0 ? value : void 0 };
705
+ if (typeof value === "string") {
706
+ const { amount, currency } = readPriceText(value);
707
+ return { amount: amount === void 0 ? void 0 : Math.abs(amount), currency };
708
+ }
709
+ if (isObject(value)) {
710
+ return {
711
+ amount: readMoney(pick(value, ["amount", "value", "price"])).amount,
712
+ currency: currencyCode(pick(value, ["currencyCode", "currency_code", "currency"]))
713
+ };
714
+ }
715
+ return {};
716
+ }
717
+ function readImage(value) {
718
+ if (Array.isArray(value)) {
719
+ for (const item of value) {
720
+ const image = readImage(item);
721
+ if (image) return image;
722
+ }
723
+ return void 0;
724
+ }
725
+ if (isObject(value)) {
726
+ const src2 = webUrl(pick(value, ["src", "url", "contentUrl", "href", "large", "medium", "thumbnail"]));
727
+ const alt = str(pick(value, ["alt", "altText", "alt_text"]));
728
+ return src2 ? { src: src2, ...alt ? { alt } : {} } : void 0;
729
+ }
730
+ const src = webUrl(value);
731
+ return src ? { src } : void 0;
732
+ }
733
+ const AVAILABILITY = {
734
+ instock: "in-stock",
735
+ available: "in-stock",
736
+ instoreonly: "in-stock",
737
+ onlineonly: "in-stock",
738
+ lowstock: "low-stock",
739
+ limitedavailability: "low-stock",
740
+ fewleft: "low-stock",
741
+ outofstock: "out-of-stock",
742
+ soldout: "out-of-stock",
743
+ unavailable: "out-of-stock",
744
+ discontinued: "out-of-stock",
745
+ preorder: "preorder",
746
+ presale: "preorder",
747
+ backorder: "preorder"
748
+ };
749
+ function readAvailability(r, offer) {
750
+ const value = pick(r, ["availability", "stockStatus", "stock_status"]) ?? pick(offer, ["availability"]);
751
+ if (typeof value === "string" && value.trim()) {
752
+ const key = value.trim().toLowerCase().replace(/^(?:https?:\/\/schema\.org\/|schema:)/, "").replace(/[^a-z]/g, "");
753
+ const status = AVAILABILITY[key];
754
+ return status ? { status } : { text: value.trim() };
755
+ }
756
+ const flag = typeof value === "boolean" ? value : pick(r, ["inStock", "in_stock", "available"]);
757
+ if (typeof flag === "boolean") return { status: flag ? "in-stock" : "out-of-stock" };
758
+ return void 0;
759
+ }
760
+ function readRating(r) {
761
+ const aggregate = isObject(r["aggregateRating"]) ? r["aggregateRating"] : void 0;
762
+ let value = pick(r, ["rating", "stars", "ratingValue", "rating_value", "averageRating", "average_rating"]) ?? pick(aggregate, ["ratingValue"]);
763
+ let count = pick(r, ["reviewCount", "review_count", "reviews", "reviewsCount", "reviews_count", "ratingCount", "rating_count", "numReviews"]) ?? pick(aggregate, ["reviewCount", "ratingCount"]);
764
+ if (isObject(value)) {
765
+ count ??= pick(value, ["count", "reviews"]);
766
+ value = pick(value, ["rate", "value", "average"]);
767
+ }
768
+ if (Array.isArray(count)) count = count.length;
769
+ const best = num(pick(r, ["bestRating", "best_rating"]) ?? pick(aggregate, ["bestRating"]));
770
+ const given = num(value);
771
+ const stars = given !== void 0 && best !== void 0 && best > 0 && given <= best ? given * 5 / best : given;
772
+ const reviews = num(count);
773
+ return {
774
+ // A value off the five-star scale is another scale, not a five: drawn as none.
775
+ rating: stars !== void 0 && stars >= 0 && stars <= 5 ? stars : void 0,
776
+ reviewCount: reviews !== void 0 && reviews >= 0 ? Math.floor(reviews) : void 0
777
+ };
778
+ }
779
+ const NO_VARIANT = "Default Title";
780
+ function readOptions(r) {
781
+ const out = [];
782
+ const add = (value) => {
783
+ const s = str(value);
784
+ if (s && s !== NO_VARIANT && !out.includes(s)) out.push(s);
785
+ };
786
+ const keys = Array.isArray(r["options"]) ? ["options", "sizes", "colors", "colours"] : ["sizes", "colors", "colours", "variants"];
787
+ for (const key of keys) {
788
+ for (const item of listOf(r[key])) {
789
+ if (!isObject(item)) add(item);
790
+ else if (Array.isArray(item["values"])) item["values"].forEach(add);
791
+ else add(pick(item, ["title", "name", "value", "label"]));
792
+ }
793
+ }
794
+ return out.slice(0, 50);
795
+ }
796
+ function readShipping(r) {
797
+ if (r["freeShipping"] === true || r["free_shipping"] === true) return { cost: 0 };
798
+ let value = pick(r, ["shipping", "delivery", "shippingInfo", "shipping_info"]);
799
+ if (isObject(value)) value = pick(value, ["text", "label", "description"]) ?? pick(value, ["cost", "price", "amount"]);
800
+ if (typeof value === "number") return Number.isFinite(value) && value >= 0 ? { cost: value } : void 0;
801
+ const text = str(value);
802
+ if (!text) return void 0;
803
+ const cost = num(text);
804
+ return cost !== void 0 && cost >= 0 ? { cost } : { text };
805
+ }
806
+ function readActions(value) {
807
+ if (!Array.isArray(value)) return [];
808
+ return value.map((a) => {
809
+ const label = isObject(a) ? str(a["label"]) : void 0;
810
+ if (!label) return null;
811
+ const url = a["url"] !== void 0 ? webUrl(a["url"]) : void 0;
812
+ if (url === null) return null;
813
+ const id = str(a["id"]);
814
+ if (!url && !id) return null;
815
+ return { label, ...url ? { url } : {}, ...id ? { id } : {}, ...a["primary"] === true ? { primary: true } : {} };
816
+ }).filter((a) => a !== null).slice(0, ACTIONS_PER_PRODUCT);
817
+ }
818
+ const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
819
+ function decodeEntities(text) {
820
+ return text.replace(/&(#x[0-9a-f]+|#\d+|[a-z]+);/gi, (whole, name) => {
821
+ if (name.startsWith("#")) {
822
+ const code = /^#x/i.test(name) ? parseInt(name.slice(2), 16) : Number(name.slice(1));
823
+ return code > 0 && code <= 1114111 ? String.fromCodePoint(code) : whole;
824
+ }
825
+ return ENTITIES[name.toLowerCase()] ?? whole;
826
+ });
827
+ }
828
+ function plainText(html) {
829
+ return decodeEntities(html.replace(/<\/?(?:br|p|li|div|h\d)(?=[\s/>])[^>]*>/gi, " ").replace(/<[^>]*>/g, "")).replace(/\s+/g, " ").trim();
830
+ }
831
+ function cheapestVariant(r) {
832
+ let best;
833
+ for (const v of listOf(r["variants"])) {
834
+ if (!isObject(v)) continue;
835
+ const price = readMoney(pick(v, ["price", "amount"]));
836
+ if (price.amount === void 0) continue;
837
+ if (!best || price.amount < best.price.amount) best = { price, compare: readMoney(pick(v, ["compare_at_price", "compareAtPrice"])) };
838
+ }
839
+ return best;
840
+ }
841
+ const lowEnd = (range) => isObject(range) ? range["minVariantPrice"] : void 0;
842
+ function readProduct(raw) {
843
+ if (!isObject(raw)) return null;
844
+ const r = raw;
845
+ const title = str(pick(r, ["title", "name", "productName", "product_title"]));
846
+ if (!title) return null;
847
+ const offers = r["offers"];
848
+ const offer = Array.isArray(offers) ? isObject(offers[0]) ? offers[0] : void 0 : isObject(offers) ? offers : void 0;
849
+ const own = pick(r, ["price", "extracted_price", "salePrice", "sale_price", "currentPrice", "current_price"]) ?? pick(offer, ["price", "lowPrice"]) ?? lowEnd(r["priceRange"]);
850
+ const variant = own === void 0 ? cheapestVariant(r) : void 0;
851
+ const price = variant ? variant.price : readMoney(own);
852
+ const compareOwn = pick(r, [
853
+ "compareAtPrice",
854
+ "compare_at_price",
855
+ "originalPrice",
856
+ "original_price",
857
+ "listPrice",
858
+ "list_price",
859
+ "regularPrice",
860
+ "regular_price",
861
+ "wasPrice",
862
+ "was_price",
863
+ "extracted_old_price",
864
+ "oldPrice",
865
+ "old_price",
866
+ "msrp"
867
+ ]) ?? lowEnd(r["compareAtPriceRange"]);
868
+ const compare = compareOwn === void 0 && variant ? variant.compare : readMoney(compareOwn);
869
+ const currency = currencyCode(pick(r, ["currency", "currency_code", "currencyCode", "priceCurrency"]) ?? pick(offer, ["priceCurrency"])) ?? price.currency ?? compare.currency;
870
+ const merchantRaw = pick(r, ["merchant", "seller", "store", "shop", "source", "retailer", "vendor"]) ?? pick(r, ["brand"]);
871
+ const merchant = isObject(merchantRaw) ? str(pick(merchantRaw, ["name", "title"])) : str(merchantRaw);
872
+ const merchantLogo = webUrl(pick(r, ["merchantLogo", "merchant_logo", "source_icon", "storeLogo", "logo"]) ?? (isObject(merchantRaw) ? pick(merchantRaw, ["logo", "icon", "image"]) : void 0));
873
+ const description = str(pick(r, ["description", "summary", "snippet", "subtitle", "body_html", "descriptionHtml"]));
874
+ const { rating: stars, reviewCount } = readRating(r);
875
+ const availability = readAvailability(r, offer);
876
+ const shipping = readShipping(r);
877
+ const image = readImage(pick(r, ["image", "images", "thumbnail", "image_url", "imageUrl", "featuredImage", "photo"]));
878
+ const url = webUrl(pick(r, ["url", "link", "permalink", "product_link", "productUrl", "product_url", "onlineStoreUrl", "href"]) ?? pick(offer, ["url"]));
879
+ const id = str(pick(r, ["id", "sku", "product_id", "productId"]));
880
+ const badge = str(pick(r, ["badge", "tag", "ribbon"]));
881
+ return {
882
+ title,
883
+ ...id ? { id } : {},
884
+ ...image ? { image } : {},
885
+ ...price.amount !== void 0 ? { price: price.amount } : {},
886
+ ...currency ? { currency } : {},
887
+ ...price.amount !== void 0 && compare.amount !== void 0 && compare.amount > price.amount ? { compareAtPrice: compare.amount } : {},
888
+ ...stars !== void 0 ? { rating: stars } : {},
889
+ ...reviewCount !== void 0 ? { reviewCount } : {},
890
+ ...availability ? { availability } : {},
891
+ ...merchant ? { merchant } : {},
892
+ ...merchant && merchantLogo ? { merchantLogo } : {},
893
+ ...badge ? { badge } : {},
894
+ options: readOptions(r),
895
+ ...shipping ? { shipping } : {},
896
+ // Shown as text, so a tag would show as `<p>`: take the words out of the markup.
897
+ ...description ? { description: plainText(description) } : {},
898
+ ...url ? { url } : {},
899
+ actions: readActions(r["actions"])
900
+ };
901
+ }
902
+ function readProducts(raw) {
903
+ if (!raw || typeof raw !== "object") return null;
904
+ let list;
905
+ let title;
906
+ if (Array.isArray(raw)) {
907
+ list = raw;
908
+ } else {
909
+ const r = raw;
910
+ const many = ["products", "results", "shopping_results", "items"].map((key) => r[key]).find(Array.isArray);
911
+ if (many) {
912
+ list = many;
913
+ title = str(pick(r, ["title", "heading"]));
914
+ } else {
915
+ list = [r];
916
+ }
917
+ }
918
+ const products = list.map(readProduct).filter((p) => p !== null).slice(0, PRODUCTS_SHOWN);
919
+ return products.length ? { ...title ? { title } : {}, products } : null;
920
+ }
921
+ function discountOf(product) {
922
+ const { price, compareAtPrice } = product;
923
+ if (price === void 0 || compareAtPrice === void 0 || compareAtPrice <= price || compareAtPrice <= 0) return void 0;
924
+ const ratio = (compareAtPrice - price) / compareAtPrice;
925
+ return Math.round(ratio * 100) >= 1 ? ratio : void 0;
926
+ }
927
+ const money = (config, product, amount) => formatPrice(config, amount, product.currency);
928
+ const notNull = (value) => value !== null && value !== void 0;
929
+ function media(product, className = "") {
930
+ const frame = el(
931
+ "div",
932
+ { class: `aparte-product__media${className ? ` ${className}` : ""}` },
933
+ glyph(BAG, "aparte-rich-card__glyph aparte-product__placeholder")
934
+ );
935
+ if (!product.image) {
936
+ frame.setAttribute("data-empty", "");
937
+ return frame;
938
+ }
939
+ const image = el("img", {
940
+ class: "aparte-product__image",
941
+ attrs: {
942
+ src: product.image.src,
943
+ alt: product.image.alt ?? product.title,
944
+ loading: "lazy",
945
+ decoding: "async",
946
+ // A shop's CDN learns nothing about the chat page that showed its photo.
947
+ referrerpolicy: "no-referrer"
948
+ }
949
+ });
950
+ image.addEventListener("error", () => {
951
+ image.hidden = true;
952
+ frame.setAttribute("data-empty", "");
953
+ }, { once: true });
954
+ frame.append(image);
955
+ return frame;
956
+ }
957
+ function titleOf(product) {
958
+ const title = el("div", { class: "aparte-product__title" });
959
+ if (product.url) {
960
+ title.append(el("a", {
961
+ class: "aparte-product__link",
962
+ attrs: { href: product.url, target: "_blank", rel: "noopener noreferrer" },
963
+ text: product.title
964
+ }));
965
+ } else {
966
+ title.textContent = product.title;
967
+ }
968
+ return title;
969
+ }
970
+ function merchantOf(product) {
971
+ if (!product.merchant) return null;
972
+ const node = el("div", { class: "aparte-product__merchant" });
973
+ if (product.merchantLogo) {
974
+ const logo = el("img", { class: "aparte-product__logo", attrs: { src: product.merchantLogo, alt: "", loading: "lazy", decoding: "async", referrerpolicy: "no-referrer" } });
975
+ logo.addEventListener("error", () => {
976
+ logo.hidden = true;
977
+ }, { once: true });
978
+ node.append(logo);
979
+ }
980
+ node.append(el("span", { class: "aparte-product__merchant-name", text: product.merchant }));
981
+ return node;
982
+ }
983
+ function discountBadge(ctx, product) {
984
+ const ratio = discountOf(product);
985
+ if (ratio === void 0) return null;
986
+ const badge = el("span", { class: "aparte-badge aparte-badge--danger aparte-badge--solid aparte-badge--sm aparte-product__discount" });
987
+ ctx.bind(badge, () => `−${formatPercent(ctx.config, ratio)}`);
988
+ return badge;
989
+ }
990
+ function priceOf(ctx, product, withDiscount = false) {
991
+ if (product.price === void 0) return null;
992
+ const row2 = el("div", { class: "aparte-product__price" });
993
+ const now = el("span", { class: "aparte-product__now" });
994
+ ctx.bind(now, () => money(ctx.config, product, product.price));
995
+ row2.append(now);
996
+ if (product.compareAtPrice !== void 0) {
997
+ const said = el("span", { class: "aparte-sr-only" });
998
+ ctx.bind(said, () => `${word(ctx.config, "cardsProductWas", P.cardsProductWas)} `);
999
+ const value = el("span", { class: "aparte-product__was-value" });
1000
+ ctx.bind(value, () => money(ctx.config, product, product.compareAtPrice));
1001
+ row2.append(el("del", { class: "aparte-product__was" }, said, value));
1002
+ const discount = withDiscount ? discountBadge(ctx, product) : null;
1003
+ if (discount) row2.append(discount);
1004
+ }
1005
+ return row2;
1006
+ }
1007
+ function availabilityOf(ctx, product) {
1008
+ const availability = product.availability;
1009
+ if (!availability) return null;
1010
+ const status = "status" in availability ? availability.status : void 0;
1011
+ const text = el("span", { class: "aparte-product__stock-text" });
1012
+ if (status) ctx.word(text, STOCK_WORD[status], P[STOCK_WORD[status]]);
1013
+ else text.textContent = availability.text;
1014
+ return el(
1015
+ "div",
1016
+ { class: "aparte-product__stock", attrs: { "data-availability": status ?? "other" } },
1017
+ el("span", { class: "aparte-product__dot", attrs: { "aria-hidden": "true" } }),
1018
+ text
1019
+ );
1020
+ }
1021
+ function shippingOf(ctx, product) {
1022
+ const shipping = product.shipping;
1023
+ if (!shipping) return null;
1024
+ const text = el("span", { class: "aparte-product__shipping-text" });
1025
+ if ("text" in shipping) text.textContent = shipping.text;
1026
+ else if (shipping.cost === 0) ctx.word(text, "cardsProductFreeShipping", P.cardsProductFreeShipping);
1027
+ else ctx.bind(text, () => word(ctx.config, "cardsProductShippingCost", P.cardsProductShippingCost, { price: money(ctx.config, product, shipping.cost) }));
1028
+ return el(
1029
+ "div",
1030
+ { class: "aparte-product__shipping", attrs: { "data-free": "cost" in shipping && shipping.cost === 0 } },
1031
+ glyph(TRUCK, "aparte-rich-card__glyph aparte-product__shipping-icon"),
1032
+ text
1033
+ );
1034
+ }
1035
+ function flags(ctx, product, withDiscount) {
1036
+ const items = [
1037
+ product.badge ? el("span", { class: "aparte-badge aparte-badge--sm aparte-product__badge", text: product.badge }) : null,
1038
+ withDiscount ? discountBadge(ctx, product) : null
1039
+ ].filter(notNull);
1040
+ return items.length ? el("div", { class: "aparte-product__flags" }, ...items) : null;
1041
+ }
1042
+ function optionsOf(ctx, product) {
1043
+ if (!product.options.length) return null;
1044
+ const list = el("ul", { class: "aparte-product__options", attrs: { role: "list" } });
1045
+ ctx.bind(list, () => word(ctx.config, "cardsProductOptions", P.cardsProductOptions), "aria-label");
1046
+ for (const option of product.options.slice(0, OPTIONS_SHOWN)) {
1047
+ list.append(el("li", { class: "aparte-tag aparte-tag--sm aparte-product__option" }, el("span", { class: "aparte-tag__label", text: option })));
1048
+ }
1049
+ const rest = product.options.length - OPTIONS_SHOWN;
1050
+ if (rest > 0) {
1051
+ const shown = el("span", { class: "aparte-tag__label", attrs: { "aria-hidden": "true" } });
1052
+ ctx.bind(shown, () => `+${formatNumber(ctx.config, rest)}`);
1053
+ const said = el("span", { class: "aparte-sr-only" });
1054
+ ctx.bind(said, () => word(ctx.config, "cardsProductMoreOptions", P.cardsProductMoreOptions, { n: formatNumber(ctx.config, rest) }));
1055
+ list.append(el("li", { class: "aparte-tag aparte-tag--sm aparte-product__option aparte-product__option--more" }, shown, said));
1056
+ }
1057
+ return list;
1058
+ }
1059
+ function actionsOf(ctx, product, max) {
1060
+ const buttons = product.actions.slice(0, max).map((action) => ctx.action(action, product, "aparte-product__action")).filter(notNull);
1061
+ return buttons.length ? el("div", { class: "aparte-product__actions" }, ...buttons) : null;
1062
+ }
1063
+ function productRoot(kind, product, extra = "") {
1064
+ return el("div", {
1065
+ class: `aparte-product aparte-product--${kind}${extra ? ` ${extra}` : ""}`,
1066
+ attrs: {
1067
+ "data-availability": product.availability && "status" in product.availability ? product.availability.status : void 0,
1068
+ "data-linked": product.url !== void 0
1069
+ }
1070
+ });
1071
+ }
1072
+ function tile(ctx, product, { actions, mini = false }) {
1073
+ const node = productRoot("tile", product, mini ? "aparte-product--mini" : "");
1074
+ const photo = media(product);
1075
+ const corner = mini ? null : flags(ctx, product, true);
1076
+ if (corner) photo.append(corner);
1077
+ const body = el(
1078
+ "div",
1079
+ { class: "aparte-product__body" },
1080
+ mini ? null : merchantOf(product),
1081
+ titleOf(product),
1082
+ mini ? null : rating(ctx, product.rating, { count: product.reviewCount }),
1083
+ priceOf(ctx, product),
1084
+ mini ? null : availabilityOf(ctx, product)
1085
+ );
1086
+ node.append(photo, body);
1087
+ const buttons = actionsOf(ctx, product, actions);
1088
+ if (buttons) node.append(buttons);
1089
+ return node;
1090
+ }
1091
+ function row(ctx, product) {
1092
+ const node = productRoot("row", product);
1093
+ const meta = el(
1094
+ "div",
1095
+ { class: "aparte-product__meta" },
1096
+ product.merchant ? el("span", { class: "aparte-product__merchant-name", text: product.merchant }) : null,
1097
+ rating(ctx, product.rating, { starsOnly: true }),
1098
+ product.availability && !("status" in product.availability && product.availability.status === "in-stock") ? availabilityOf(ctx, product) : null
1099
+ );
1100
+ const main = el("div", { class: "aparte-product__main" }, titleOf(product), meta.childNodes.length ? meta : null);
1101
+ node.append(media(product, "aparte-product__thumb"), main);
1102
+ const amount = priceOf(ctx, product);
1103
+ if (amount) node.append(amount);
1104
+ const buttons = actionsOf(ctx, product, 1);
1105
+ if (buttons) node.append(buttons);
1106
+ return node;
1107
+ }
1108
+ function detail(ctx, product) {
1109
+ const node = productRoot("detail", product);
1110
+ const photo = media(product);
1111
+ const corner = flags(ctx, product, false);
1112
+ if (corner) photo.append(corner);
1113
+ const shown = product.url ? el("a", { class: "aparte-product__media-link", attrs: { href: product.url, target: "_blank", rel: "noopener noreferrer", tabindex: -1, "aria-hidden": "true" } }, photo) : photo;
1114
+ const facts = el("div", { class: "aparte-product__facts" }, availabilityOf(ctx, product), shippingOf(ctx, product));
1115
+ const body = el(
1116
+ "div",
1117
+ { class: "aparte-product__body" },
1118
+ merchantOf(product),
1119
+ titleOf(product),
1120
+ rating(ctx, product.rating, { count: product.reviewCount }),
1121
+ priceOf(ctx, product, true),
1122
+ facts.childNodes.length ? facts : null,
1123
+ product.description ? el("p", { class: "aparte-product__description", text: product.description }) : null,
1124
+ optionsOf(ctx, product),
1125
+ actionsOf(ctx, product, ACTIONS_PER_PRODUCT)
1126
+ );
1127
+ node.append(shown, body);
1128
+ return el("div", { class: "aparte-product__detail" }, node);
1129
+ }
1130
+ function heading(data) {
1131
+ return data.title ? el("div", { class: "aparte-product__heading", text: data.title }) : null;
1132
+ }
1133
+ function labelOf(data, ctx) {
1134
+ const products = data?.products ?? [];
1135
+ if (products.length === 1) return products[0].title;
1136
+ if (data?.title) return data.title;
1137
+ if (products.length > 1) return word(ctx.config, "cardsProductCount", P.cardsProductCount, { n: formatNumber(ctx.config, products.length) });
1138
+ return word(ctx.config, "cardsProductTitle", P.cardsProductTitle);
1139
+ }
1140
+ function render(data, ctx) {
1141
+ const { products } = data;
1142
+ const first = products[0];
1143
+ if (ctx.variant === "detail") {
1144
+ const out = [detail(ctx, first)];
1145
+ if (products.length > 1) {
1146
+ const title = el("div", { class: "aparte-product__more-title" });
1147
+ ctx.word(title, "cardsProductMore", P.cardsProductMore);
1148
+ const slides2 = products.slice(1).map((p) => tile(ctx, p, { actions: 0, mini: true }));
1149
+ out.push(el(
1150
+ "div",
1151
+ { class: "aparte-product__more" },
1152
+ title,
1153
+ carousel(ctx, slides2, { label: () => word(ctx.config, "cardsProductMore", P.cardsProductMore) })
1154
+ ));
1155
+ }
1156
+ return out;
1157
+ }
1158
+ if (ctx.variant === "compact") {
1159
+ if (products.length === 1) return [row(ctx, first)];
1160
+ const list = el(
1161
+ "ul",
1162
+ { class: "aparte-product__rows", attrs: { role: "list" } },
1163
+ ...products.map((p) => el("li", { class: "aparte-product__item" }, row(ctx, p)))
1164
+ );
1165
+ return [heading(data), list].filter(notNull);
1166
+ }
1167
+ if (products.length === 1) return [tile(ctx, first, { actions: ACTIONS_PER_PRODUCT })];
1168
+ const slides = products.map((p) => tile(ctx, p, { actions: 1 }));
1169
+ return [el(
1170
+ "div",
1171
+ { class: "aparte-product__collection" },
1172
+ heading(data),
1173
+ carousel(ctx, slides, { label: () => labelOf(data, ctx) })
1174
+ )];
1175
+ }
1176
+ function skeleton(partial, ctx) {
1177
+ const block = (cls, kind = "rect") => el("span", { class: `aparte-skeleton aparte-skeleton--${kind} ${cls}`, attrs: { "aria-hidden": "true" } });
1178
+ const lines = () => [block("aparte-product__sk-line", "text"), block("aparte-product__sk-line aparte-product__sk-line--short", "text")];
1179
+ const query = partial?.title ? el("div", { class: "aparte-product__query" }, glyph(SEARCH, "aparte-rich-card__glyph aparte-product__query-icon"), el("span", { class: "aparte-product__query-text", text: partial.title })) : null;
1180
+ if (ctx.variant === "compact") {
1181
+ const rows = [0, 1, 2].map(() => el(
1182
+ "div",
1183
+ { class: "aparte-product__item" },
1184
+ el(
1185
+ "div",
1186
+ { class: "aparte-product aparte-product--row aparte-product--skeleton" },
1187
+ block("aparte-product__sk-thumb"),
1188
+ el("div", { class: "aparte-product__main" }, ...lines()),
1189
+ block("aparte-product__sk-price", "text")
1190
+ )
1191
+ ));
1192
+ return [query, el("div", { class: "aparte-product__rows" }, ...rows)].filter(notNull);
1193
+ }
1194
+ if (ctx.variant === "detail") {
1195
+ return [query, el(
1196
+ "div",
1197
+ { class: "aparte-product__detail" },
1198
+ el(
1199
+ "div",
1200
+ { class: "aparte-product aparte-product--detail aparte-product--skeleton" },
1201
+ block("aparte-product__sk-media"),
1202
+ el("div", { class: "aparte-product__body" }, ...lines(), block("aparte-product__sk-price", "text"), block("aparte-product__sk-line", "text"))
1203
+ )
1204
+ )].filter(notNull);
1205
+ }
1206
+ const tiles = [0, 1, 2].map(() => el(
1207
+ "div",
1208
+ { class: "aparte-product aparte-product--tile aparte-product--skeleton" },
1209
+ block("aparte-product__sk-media"),
1210
+ el("div", { class: "aparte-product__body" }, ...lines(), block("aparte-product__sk-price", "text"))
1211
+ ));
1212
+ return [el("div", { class: "aparte-product__collection" }, query, el("div", { class: "aparte-product__sk-track" }, ...tiles))];
1213
+ }
1214
+ const productCard = defineCard({
1215
+ name: "product",
1216
+ variants: ["card", "compact", "detail"],
1217
+ styles: productStyles,
1218
+ icon: TAG,
1219
+ normalize: readProducts,
1220
+ render,
1221
+ skeleton,
1222
+ label: labelOf,
1223
+ fallback: (data, ctx) => {
1224
+ const lines = data.products.map((p) => {
1225
+ const amount = p.price !== void 0 ? ` — ${money(ctx.config, p, p.price)}` : "";
1226
+ const stars = p.rating !== void 0 ? ` (${formatNumber(ctx.config, p.rating, { maximumFractionDigits: 1 })}/5)` : "";
1227
+ return `${p.title}${amount}${stars}`;
1228
+ });
1229
+ return [data.title, ...lines].filter(notNull).join("\n");
1230
+ },
1231
+ partial: (input) => {
1232
+ const query = str(input["query"] ?? input["q"] ?? input["search"] ?? input["keywords"] ?? input["term"] ?? input["product"]);
1233
+ return query ? { title: query } : null;
1234
+ }
1235
+ });
1236
+ function setupProductCard(options = {}) {
1237
+ return productCard.setup(options);
1238
+ }
1239
+ export {
1240
+ discountOf,
1241
+ productCard,
1242
+ readProducts,
1243
+ setupProductCard
1244
+ };
1245
+ //# sourceMappingURL=product.js.map