@adobe-commerce/elsie 1.2.1-alpha1 → 1.2.2-alpha1
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.
- package/config/vite.mjs +43 -2
- package/package.json +1 -1
- package/src/components/ImageSwatch/ImageSwatch.css +6 -0
- package/src/components/ProductItemCard/ProductItemCard.css +87 -0
- package/src/components/ProductItemCard/ProductItemCard.stories.tsx +371 -0
- package/src/components/ProductItemCard/ProductItemCard.tsx +94 -0
- package/src/components/ProductItemCard/ProductItemCardSkeleton.css +40 -0
- package/src/components/ProductItemCard/ProductItemCardSkeleton.tsx +42 -0
- package/src/components/ProductItemCard/index.ts +11 -0
- package/src/components/index.ts +1 -0
- package/src/docs/Utilities/links.mdx +58 -0
package/config/vite.mjs
CHANGED
|
@@ -29,6 +29,15 @@ const elsieConfig = await import(
|
|
|
29
29
|
path.resolve(process.cwd(), './.elsie.js')
|
|
30
30
|
).then((m) => m.default);
|
|
31
31
|
|
|
32
|
+
const packageJSON = await import(
|
|
33
|
+
path.resolve(process.cwd(), './package.json'),
|
|
34
|
+
{
|
|
35
|
+
assert: {
|
|
36
|
+
type: 'json',
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
).then((m) => m.default);
|
|
40
|
+
|
|
32
41
|
// Paths
|
|
33
42
|
const paths = {
|
|
34
43
|
api: elsieConfig.api?.root
|
|
@@ -116,7 +125,6 @@ export default {
|
|
|
116
125
|
entryFileNames: '[name].js',
|
|
117
126
|
assetFileNames: '[name].[ext]',
|
|
118
127
|
chunkFileNames: 'chunks/[name].js',
|
|
119
|
-
|
|
120
128
|
manualChunks: (id) => {
|
|
121
129
|
if (id.includes(paths.fragments)) {
|
|
122
130
|
// Fragments file does not accept chunking
|
|
@@ -176,7 +184,7 @@ export default {
|
|
|
176
184
|
modulePreload: false,
|
|
177
185
|
commonjsOptions: { transformMixedEsModules: true },
|
|
178
186
|
minify: !!isProd,
|
|
179
|
-
sourcemap:
|
|
187
|
+
sourcemap: true,
|
|
180
188
|
},
|
|
181
189
|
|
|
182
190
|
optimizeDeps: {},
|
|
@@ -287,6 +295,39 @@ export default {
|
|
|
287
295
|
},
|
|
288
296
|
}),
|
|
289
297
|
|
|
298
|
+
{
|
|
299
|
+
name: 'rewrite-sourcemap-sources',
|
|
300
|
+
generateBundle(options, bundle) {
|
|
301
|
+
for (const fileName in bundle) {
|
|
302
|
+
const chunk = bundle[fileName];
|
|
303
|
+
|
|
304
|
+
// Process both .map files and JS/TS files with sourcemaps
|
|
305
|
+
if ((chunk.type === 'asset' && fileName.endsWith('.map')) ||
|
|
306
|
+
(chunk.type === 'chunk' && chunk.map)) {
|
|
307
|
+
try {
|
|
308
|
+
// Get the sourcemap object - either from the asset source or the chunk's map
|
|
309
|
+
const map = chunk.type === 'asset' ? JSON.parse(chunk.source) : chunk.map;
|
|
310
|
+
|
|
311
|
+
if (map.sources) {
|
|
312
|
+
map.sources = map.sources.map((input) => {
|
|
313
|
+
return input.replace(/(?:\.\.?\/)+src\//, `/${packageJSON.name}/src/`);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
// Update the sourcemap in the appropriate place
|
|
317
|
+
if (chunk.type === 'asset') {
|
|
318
|
+
chunk.source = JSON.stringify(map);
|
|
319
|
+
} else {
|
|
320
|
+
chunk.map = map;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} catch (e) {
|
|
324
|
+
console.error('Error transforming sourcemap:', e);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
},
|
|
330
|
+
|
|
290
331
|
process.env.ANALYZE
|
|
291
332
|
? visualizer({
|
|
292
333
|
title: `${elsieConfig.name} Dropin Bundle Analysis`,
|
package/package.json
CHANGED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/********************************************************************
|
|
2
|
+
* Copyright 2025 Adobe
|
|
3
|
+
* All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* NOTICE: Adobe permits you to use, modify, and distribute this
|
|
6
|
+
* file in accordance with the terms of the Adobe license agreement
|
|
7
|
+
* accompanying it.
|
|
8
|
+
*******************************************************************/
|
|
9
|
+
|
|
10
|
+
/* https://cssguidelin.es/#bem-like-naming */
|
|
11
|
+
|
|
12
|
+
.dropin-product-item-card {
|
|
13
|
+
display: grid;
|
|
14
|
+
position: relative;
|
|
15
|
+
grid-auto-flow: row;
|
|
16
|
+
background: var(--color-neutral-50);
|
|
17
|
+
border: 1px solid var(--color-neutral-400);
|
|
18
|
+
color: var(--color-neutral-800);
|
|
19
|
+
font: var(--type-body-1-default-font);
|
|
20
|
+
letter-spacing: var(--type-body-1-default-letter-spacing);
|
|
21
|
+
margin: var(--spacing-small);
|
|
22
|
+
width: 300px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.dropin-product-item-card__image-container {
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
width: 100%;
|
|
28
|
+
height: auto;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.dropin-product-item-card__image img {
|
|
32
|
+
width: 100%;
|
|
33
|
+
max-height: 375px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.dropin-product-item-card__content {
|
|
37
|
+
display: grid;
|
|
38
|
+
grid-template-columns: 1fr 1fr;
|
|
39
|
+
padding: var(--spacing-small);
|
|
40
|
+
gap: var(--spacing-xxsmall);
|
|
41
|
+
align-items: center;
|
|
42
|
+
color: var(--color-neutral-800);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.dropin-product-item-card__title,
|
|
46
|
+
.dropin-product-item-card__sku,
|
|
47
|
+
.dropin-product-item-card__price,
|
|
48
|
+
.dropin-product-item-card__swatches,
|
|
49
|
+
.dropin-product-item-card__action {
|
|
50
|
+
grid-column: 1/3;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.dropin-product-item-card__title {
|
|
54
|
+
font: var(--type-body-1-strong-font);
|
|
55
|
+
letter-spacing: var(--type-body-1-strong-letter-spacing);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.dropin-product-item-card__sku {
|
|
59
|
+
font: var(--type-body-1-default-font);
|
|
60
|
+
letter-spacing: var(--type-body-1-default-letter-spacing);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.dropin-product-item-card__price {
|
|
64
|
+
font: var(--type-body-1-default-font);
|
|
65
|
+
letter-spacing: var(--type-body-1-default-letter-spacing);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.dropin-product-item-card__swatches {
|
|
69
|
+
margin-top: var(--spacing-xsmall);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.dropin-product-item-card__action {
|
|
73
|
+
margin-top: var(--spacing-xsmall);
|
|
74
|
+
width: 100%;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Medium (portrait tablets and large phones, 768px and up) */
|
|
78
|
+
/* @media only screen and (min-width: 768px) { } */
|
|
79
|
+
|
|
80
|
+
/* Large (landscape tablets, 1024px and up) */
|
|
81
|
+
/* @media only screen and (min-width: 1024px) { } */
|
|
82
|
+
|
|
83
|
+
/* XLarge (laptops/desktops, 1366px and up) */
|
|
84
|
+
/* @media only screen and (min-width: 1366px) { } */
|
|
85
|
+
|
|
86
|
+
/* XXlarge (large laptops and desktops, 1920px and up) */
|
|
87
|
+
/* @media only screen and (min-width: 1920px) { } */
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/********************************************************************
|
|
2
|
+
* ADOBE CONFIDENTIAL
|
|
3
|
+
* __________________
|
|
4
|
+
*
|
|
5
|
+
* Copyright 2025 Adobe
|
|
6
|
+
* All Rights Reserved.
|
|
7
|
+
*
|
|
8
|
+
* NOTICE: All information contained herein is, and remains
|
|
9
|
+
* the property of Adobe and its suppliers, if any. The intellectual
|
|
10
|
+
* and technical concepts contained herein are proprietary to Adobe
|
|
11
|
+
* and its suppliers and are protected by all applicable intellectual
|
|
12
|
+
* property laws, including trade secret and copyright laws.
|
|
13
|
+
* Dissemination of this information or reproduction of this material
|
|
14
|
+
* is strictly forbidden unless prior written permission is obtained
|
|
15
|
+
* from Adobe.
|
|
16
|
+
*******************************************************************/
|
|
17
|
+
|
|
18
|
+
// https://storybook.js.org/docs/7.0/preact/writing-stories/introduction
|
|
19
|
+
import type { Meta, StoryObj } from '@storybook/preact';
|
|
20
|
+
import {
|
|
21
|
+
Button,
|
|
22
|
+
Icon,
|
|
23
|
+
Image,
|
|
24
|
+
Price,
|
|
25
|
+
ProductItemCard as component,
|
|
26
|
+
ProductItemCardProps,
|
|
27
|
+
ColorSwatch,
|
|
28
|
+
} from '@adobe-commerce/elsie/components';
|
|
29
|
+
import { Cart } from '@adobe-commerce/elsie/icons';
|
|
30
|
+
/**
|
|
31
|
+
* Use ProductItemCard to display product recommendations with image, title, price, SKU, and action button.
|
|
32
|
+
*/
|
|
33
|
+
const meta: Meta<ProductItemCardProps> = {
|
|
34
|
+
title: 'Components/ProductItemCard',
|
|
35
|
+
component,
|
|
36
|
+
|
|
37
|
+
argTypes: {
|
|
38
|
+
image: {
|
|
39
|
+
control: {
|
|
40
|
+
type: 'select',
|
|
41
|
+
labels: {
|
|
42
|
+
DefaultImage: 'Default Image',
|
|
43
|
+
Empty: 'No Image',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
description: 'Product image node.',
|
|
47
|
+
options: ['DefaultImage', 'Empty'],
|
|
48
|
+
mapping: {
|
|
49
|
+
DefaultImage: (
|
|
50
|
+
<Image
|
|
51
|
+
src="https://picsum.photos/300/375"
|
|
52
|
+
width="300"
|
|
53
|
+
height="375"
|
|
54
|
+
alt="Product Image"
|
|
55
|
+
loading="lazy"
|
|
56
|
+
/>
|
|
57
|
+
),
|
|
58
|
+
Empty: null,
|
|
59
|
+
},
|
|
60
|
+
table: { defaultValue: { summary: 'null' } },
|
|
61
|
+
},
|
|
62
|
+
titleNode: {
|
|
63
|
+
control: {
|
|
64
|
+
type: 'select',
|
|
65
|
+
labels: {
|
|
66
|
+
DefaultTitle: 'Default Title',
|
|
67
|
+
LongTitle: 'Long Title',
|
|
68
|
+
Empty: 'No Title',
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
description: 'Product title node.',
|
|
72
|
+
options: ['DefaultTitle', 'LongTitle', 'Empty'],
|
|
73
|
+
mapping: {
|
|
74
|
+
DefaultTitle: <div>Hollister Backyard Sweatshirt</div>,
|
|
75
|
+
LongTitle: (
|
|
76
|
+
<div>
|
|
77
|
+
Hollister Backyard Sweatshirt with Extra Long Product Name That
|
|
78
|
+
Might Wrap
|
|
79
|
+
</div>
|
|
80
|
+
),
|
|
81
|
+
Empty: null,
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
price: {
|
|
85
|
+
control: {
|
|
86
|
+
type: 'select',
|
|
87
|
+
labels: {
|
|
88
|
+
DefaultPrice: 'Default Price',
|
|
89
|
+
SalePrice: 'Sale Price',
|
|
90
|
+
Empty: 'No Price',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
description: 'Product price node.',
|
|
94
|
+
options: ['DefaultPrice', 'SalePrice', 'Empty'],
|
|
95
|
+
mapping: {
|
|
96
|
+
DefaultPrice: (
|
|
97
|
+
<>
|
|
98
|
+
<Price amount={49.99} />
|
|
99
|
+
</>
|
|
100
|
+
),
|
|
101
|
+
SalePrice: (
|
|
102
|
+
<>
|
|
103
|
+
<Price amount={69.99} variant="strikethrough" />
|
|
104
|
+
<Price amount={49.99} sale />
|
|
105
|
+
</>
|
|
106
|
+
),
|
|
107
|
+
Empty: null,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
sku: {
|
|
111
|
+
control: {
|
|
112
|
+
type: 'select',
|
|
113
|
+
labels: {
|
|
114
|
+
DefaultSku: 'Default SKU',
|
|
115
|
+
Empty: 'No SKU',
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
description: 'Product SKU node.',
|
|
119
|
+
options: ['DefaultSku', 'Empty'],
|
|
120
|
+
mapping: {
|
|
121
|
+
DefaultSku: <div>SKU: 123456789</div>,
|
|
122
|
+
Empty: null,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
swatches: {
|
|
126
|
+
control: {
|
|
127
|
+
type: 'select',
|
|
128
|
+
labels: {
|
|
129
|
+
DefaultSwatches: 'Default Swatches',
|
|
130
|
+
SelectedSwatches: 'Selected Swatches',
|
|
131
|
+
OutOfStockSwatches: 'Out of Stock Swatches',
|
|
132
|
+
Empty: 'No Swatches',
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
description: 'Product swatches node.',
|
|
136
|
+
options: [
|
|
137
|
+
'DefaultSwatches',
|
|
138
|
+
'SelectedSwatches',
|
|
139
|
+
'OutOfStockSwatches',
|
|
140
|
+
'Empty',
|
|
141
|
+
],
|
|
142
|
+
mapping: {
|
|
143
|
+
DefaultSwatches: (
|
|
144
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
145
|
+
<ColorSwatch
|
|
146
|
+
color="red"
|
|
147
|
+
label="Red"
|
|
148
|
+
groupAriaLabel="Color options"
|
|
149
|
+
value="red"
|
|
150
|
+
/>
|
|
151
|
+
<ColorSwatch
|
|
152
|
+
color="blue"
|
|
153
|
+
label="Blue"
|
|
154
|
+
groupAriaLabel="Color options"
|
|
155
|
+
value="blue"
|
|
156
|
+
/>
|
|
157
|
+
<ColorSwatch
|
|
158
|
+
color="green"
|
|
159
|
+
label="Green"
|
|
160
|
+
groupAriaLabel="Color options"
|
|
161
|
+
value="green"
|
|
162
|
+
/>
|
|
163
|
+
</div>
|
|
164
|
+
),
|
|
165
|
+
SelectedSwatches: (
|
|
166
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
167
|
+
<ColorSwatch
|
|
168
|
+
color="red"
|
|
169
|
+
label="Red"
|
|
170
|
+
groupAriaLabel="Color options"
|
|
171
|
+
value="red"
|
|
172
|
+
selected
|
|
173
|
+
/>
|
|
174
|
+
<ColorSwatch
|
|
175
|
+
color="blue"
|
|
176
|
+
label="Blue"
|
|
177
|
+
groupAriaLabel="Color options"
|
|
178
|
+
value="blue"
|
|
179
|
+
/>
|
|
180
|
+
<ColorSwatch
|
|
181
|
+
color="green"
|
|
182
|
+
label="Green"
|
|
183
|
+
groupAriaLabel="Color options"
|
|
184
|
+
value="green"
|
|
185
|
+
/>
|
|
186
|
+
</div>
|
|
187
|
+
),
|
|
188
|
+
OutOfStockSwatches: (
|
|
189
|
+
<div style={{ display: 'flex', gap: '8px' }}>
|
|
190
|
+
<ColorSwatch
|
|
191
|
+
color="red"
|
|
192
|
+
label="Red"
|
|
193
|
+
groupAriaLabel="Color options"
|
|
194
|
+
value="red"
|
|
195
|
+
outOfStock
|
|
196
|
+
/>
|
|
197
|
+
<ColorSwatch
|
|
198
|
+
color="blue"
|
|
199
|
+
label="Blue"
|
|
200
|
+
groupAriaLabel="Color options"
|
|
201
|
+
value="blue"
|
|
202
|
+
/>
|
|
203
|
+
<ColorSwatch
|
|
204
|
+
color="green"
|
|
205
|
+
label="Green"
|
|
206
|
+
groupAriaLabel="Color options"
|
|
207
|
+
value="green"
|
|
208
|
+
/>
|
|
209
|
+
</div>
|
|
210
|
+
),
|
|
211
|
+
Empty: null,
|
|
212
|
+
},
|
|
213
|
+
table: { defaultValue: { summary: 'null' } },
|
|
214
|
+
},
|
|
215
|
+
actionButton: {
|
|
216
|
+
control: {
|
|
217
|
+
type: 'select',
|
|
218
|
+
labels: {
|
|
219
|
+
DefaultButton: 'Default Button',
|
|
220
|
+
CustomButton: 'Custom Button',
|
|
221
|
+
Empty: 'No Button',
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
description: 'Action button node.',
|
|
225
|
+
options: ['DefaultButton', 'CustomButton', 'Empty'],
|
|
226
|
+
mapping: {
|
|
227
|
+
DefaultButton: <Button>Select Options</Button>,
|
|
228
|
+
CustomButton: (
|
|
229
|
+
<Button icon={<Icon source={Cart} size="24" />} variant="primary">
|
|
230
|
+
Add to Cart
|
|
231
|
+
</Button>
|
|
232
|
+
),
|
|
233
|
+
Empty: null,
|
|
234
|
+
},
|
|
235
|
+
table: { defaultValue: { summary: 'null' } },
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
export default meta;
|
|
241
|
+
|
|
242
|
+
type Story = StoryObj<ProductItemCardProps>;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Default ProductItemCard with all elements
|
|
246
|
+
*/
|
|
247
|
+
export const Default: Story = {
|
|
248
|
+
args: {
|
|
249
|
+
initialized: true,
|
|
250
|
+
image: 'DefaultImage' as any,
|
|
251
|
+
titleNode: 'DefaultTitle' as any,
|
|
252
|
+
price: 'DefaultPrice' as any,
|
|
253
|
+
sku: 'DefaultSku' as any,
|
|
254
|
+
swatches: 'DefaultSwatches' as any,
|
|
255
|
+
actionButton: 'DefaultButton' as any,
|
|
256
|
+
},
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* ProductItemCard with long title
|
|
261
|
+
*/
|
|
262
|
+
export const LongTitle: Story = {
|
|
263
|
+
args: {
|
|
264
|
+
initialized: true,
|
|
265
|
+
image: 'DefaultImage' as any,
|
|
266
|
+
titleNode: 'LongTitle' as any,
|
|
267
|
+
price: 'DefaultPrice' as any,
|
|
268
|
+
sku: 'DefaultSku' as any,
|
|
269
|
+
swatches: 'DefaultSwatches' as any,
|
|
270
|
+
actionButton: 'DefaultButton' as any,
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* ProductItemCard with sale price
|
|
276
|
+
*/
|
|
277
|
+
export const SalePrice: Story = {
|
|
278
|
+
args: {
|
|
279
|
+
initialized: true,
|
|
280
|
+
image: 'DefaultImage' as any,
|
|
281
|
+
titleNode: 'DefaultTitle' as any,
|
|
282
|
+
price: 'SalePrice' as any,
|
|
283
|
+
sku: 'DefaultSku' as any,
|
|
284
|
+
swatches: 'DefaultSwatches' as any,
|
|
285
|
+
actionButton: 'DefaultButton' as any,
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* ProductItemCard without action button
|
|
291
|
+
*/
|
|
292
|
+
export const NoButton: Story = {
|
|
293
|
+
args: {
|
|
294
|
+
initialized: true,
|
|
295
|
+
image: 'DefaultImage' as any,
|
|
296
|
+
titleNode: 'DefaultTitle' as any,
|
|
297
|
+
price: 'DefaultPrice' as any,
|
|
298
|
+
sku: 'DefaultSku' as any,
|
|
299
|
+
swatches: 'DefaultSwatches' as any,
|
|
300
|
+
actionButton: 'Empty' as any,
|
|
301
|
+
},
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* ProductItemCard without image
|
|
306
|
+
*/
|
|
307
|
+
export const NoImage: Story = {
|
|
308
|
+
args: {
|
|
309
|
+
initialized: true,
|
|
310
|
+
image: 'Empty' as any,
|
|
311
|
+
titleNode: 'DefaultTitle' as any,
|
|
312
|
+
price: 'DefaultPrice' as any,
|
|
313
|
+
sku: 'DefaultSku' as any,
|
|
314
|
+
swatches: 'DefaultSwatches' as any,
|
|
315
|
+
actionButton: 'DefaultButton' as any,
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* ProductItemCard with minimal content
|
|
321
|
+
*/
|
|
322
|
+
export const Minimal: Story = {
|
|
323
|
+
args: {
|
|
324
|
+
initialized: true,
|
|
325
|
+
image: 'DefaultImage' as any,
|
|
326
|
+
titleNode: 'DefaultTitle' as any,
|
|
327
|
+
price: 'Empty' as any,
|
|
328
|
+
sku: 'Empty' as any,
|
|
329
|
+
swatches: 'Empty' as any,
|
|
330
|
+
actionButton: 'Empty' as any,
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* ProductItemCard with selected swatch
|
|
336
|
+
*/
|
|
337
|
+
export const SelectedSwatch: Story = {
|
|
338
|
+
args: {
|
|
339
|
+
initialized: true,
|
|
340
|
+
image: 'DefaultImage' as any,
|
|
341
|
+
titleNode: 'DefaultTitle' as any,
|
|
342
|
+
price: 'DefaultPrice' as any,
|
|
343
|
+
sku: 'DefaultSku' as any,
|
|
344
|
+
swatches: 'SelectedSwatches' as any,
|
|
345
|
+
actionButton: 'DefaultButton' as any,
|
|
346
|
+
},
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* ProductItemCard with out of stock swatch
|
|
351
|
+
*/
|
|
352
|
+
export const OutOfStockSwatch: Story = {
|
|
353
|
+
args: {
|
|
354
|
+
initialized: true,
|
|
355
|
+
image: 'DefaultImage' as any,
|
|
356
|
+
titleNode: 'DefaultTitle' as any,
|
|
357
|
+
price: 'DefaultPrice' as any,
|
|
358
|
+
sku: 'DefaultSku' as any,
|
|
359
|
+
swatches: 'OutOfStockSwatches' as any,
|
|
360
|
+
actionButton: 'DefaultButton' as any,
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* ProductItemCard in skeleton loading state
|
|
366
|
+
*/
|
|
367
|
+
export const Skeleton: Story = {
|
|
368
|
+
args: {
|
|
369
|
+
initialized: false,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/********************************************************************
|
|
2
|
+
* Copyright 2025 Adobe
|
|
3
|
+
* All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* NOTICE: Adobe permits you to use, modify, and distribute this
|
|
6
|
+
* file in accordance with the terms of the Adobe license agreement
|
|
7
|
+
* accompanying it.
|
|
8
|
+
*******************************************************************/
|
|
9
|
+
|
|
10
|
+
import { FunctionComponent, VNode } from 'preact';
|
|
11
|
+
import { HTMLAttributes } from 'preact/compat';
|
|
12
|
+
import { VComponent, classes } from '@adobe-commerce/elsie/lib';
|
|
13
|
+
import { ProductItemCardSkeleton } from '@adobe-commerce/elsie/components/ProductItemCard/ProductItemCardSkeleton';
|
|
14
|
+
import '@adobe-commerce/elsie/components/ProductItemCard/ProductItemCard.css';
|
|
15
|
+
|
|
16
|
+
export interface ProductItemCardProps
|
|
17
|
+
extends Omit<HTMLAttributes<HTMLDivElement>, 'loading'> {
|
|
18
|
+
image?: VNode;
|
|
19
|
+
titleNode?: VNode;
|
|
20
|
+
price?: VNode;
|
|
21
|
+
sku?: VNode;
|
|
22
|
+
actionButton?: VNode;
|
|
23
|
+
swatches?: VNode;
|
|
24
|
+
initialized?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const ProductItemCard: FunctionComponent<ProductItemCardProps> = ({
|
|
28
|
+
className,
|
|
29
|
+
image,
|
|
30
|
+
titleNode,
|
|
31
|
+
price,
|
|
32
|
+
sku,
|
|
33
|
+
actionButton,
|
|
34
|
+
swatches,
|
|
35
|
+
initialized = false,
|
|
36
|
+
...props
|
|
37
|
+
}) => {
|
|
38
|
+
if (!initialized) {
|
|
39
|
+
return <ProductItemCardSkeleton />;
|
|
40
|
+
}
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
{...props}
|
|
44
|
+
className={classes(['dropin-product-item-card', className])}
|
|
45
|
+
>
|
|
46
|
+
<div className="dropin-product-item-card__image-container">
|
|
47
|
+
{image && (
|
|
48
|
+
<VComponent
|
|
49
|
+
node={image}
|
|
50
|
+
className={classes(['dropin-product-item-card__image'])}
|
|
51
|
+
/>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
<div className="dropin-product-item-card__content">
|
|
55
|
+
{titleNode && (
|
|
56
|
+
<VComponent
|
|
57
|
+
node={titleNode}
|
|
58
|
+
className={classes(['dropin-product-item-card__title'])}
|
|
59
|
+
/>
|
|
60
|
+
)}
|
|
61
|
+
{sku && (
|
|
62
|
+
<VComponent
|
|
63
|
+
node={sku}
|
|
64
|
+
className={classes(['dropin-product-item-card__sku'])}
|
|
65
|
+
/>
|
|
66
|
+
)}
|
|
67
|
+
{price && (
|
|
68
|
+
<div className="dropin-product-item-card__price">
|
|
69
|
+
<VComponent
|
|
70
|
+
node={price}
|
|
71
|
+
className={classes(['dropin-product-item-card__price'])}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
)}
|
|
75
|
+
{swatches && (
|
|
76
|
+
<div className="dropin-product-item-card__swatches">
|
|
77
|
+
<VComponent
|
|
78
|
+
node={swatches}
|
|
79
|
+
className={classes(['dropin-product-item-card__swatches'])}
|
|
80
|
+
/>
|
|
81
|
+
</div>
|
|
82
|
+
)}
|
|
83
|
+
{actionButton && (
|
|
84
|
+
<div className="dropin-product-item-card__action">
|
|
85
|
+
<VComponent
|
|
86
|
+
node={actionButton}
|
|
87
|
+
className={classes(['dropin-product-item-card__action'])}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/********************************************************************
|
|
2
|
+
* Copyright 2025 Adobe
|
|
3
|
+
* All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* NOTICE: Adobe permits you to use, modify, and distribute this
|
|
6
|
+
* file in accordance with the terms of the Adobe license agreement
|
|
7
|
+
* accompanying it.
|
|
8
|
+
*******************************************************************/
|
|
9
|
+
|
|
10
|
+
/* https://cssguidelin.es/#bem-like-naming */
|
|
11
|
+
|
|
12
|
+
.dropin-product-item-card__skeleton {
|
|
13
|
+
gap: var(--spacing-small);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.dropin-product-item-card__skeleton__image {
|
|
17
|
+
height: 375px;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.dropin-product-item-card__skeleton__content {
|
|
21
|
+
grid-column: 1/-1;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* .dropin-product-item-card__skeleton__image {
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: auto;
|
|
27
|
+
}
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/* Medium (portrait tablets and large phones, 768px and up) */
|
|
31
|
+
/* @media only screen and (min-width: 768px) { } */
|
|
32
|
+
|
|
33
|
+
/* Large (landscape tablets, 1024px and up) */
|
|
34
|
+
/* @media only screen and (min-width: 1024px) { } */
|
|
35
|
+
|
|
36
|
+
/* XLarge (laptops/desktops, 1366px and up) */
|
|
37
|
+
/* @media only screen and (min-width: 1366px) { } */
|
|
38
|
+
|
|
39
|
+
/* XXlarge (large laptops and desktops, 1920px and up) */
|
|
40
|
+
/* @media only screen and (min-width: 1920px) { } */
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/********************************************************************
|
|
2
|
+
* Copyright 2025 Adobe
|
|
3
|
+
* All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* NOTICE: Adobe permits you to use, modify, and distribute this
|
|
6
|
+
* file in accordance with the terms of the Adobe license agreement
|
|
7
|
+
* accompanying it.
|
|
8
|
+
*******************************************************************/
|
|
9
|
+
|
|
10
|
+
import { FunctionComponent } from 'preact';
|
|
11
|
+
import { Skeleton, SkeletonRow } from '@adobe-commerce/elsie/components';
|
|
12
|
+
import '@adobe-commerce/elsie/components/ProductItemCard/ProductItemCardSkeleton.css';
|
|
13
|
+
|
|
14
|
+
export const ProductItemCardSkeleton: FunctionComponent = () => {
|
|
15
|
+
return (
|
|
16
|
+
<div className="dropin-product-item-card dropin-product-item-card-skeleton">
|
|
17
|
+
<Skeleton className="dropin-product-item-card__skeleton dropin-product-item-card__image-container">
|
|
18
|
+
<SkeletonRow
|
|
19
|
+
fullWidth={true}
|
|
20
|
+
className="dropin-product-item-card__skeleton__image"
|
|
21
|
+
/>
|
|
22
|
+
<div className="dropin-product-item-card__content dropin-product-item-card__skeleton__content">
|
|
23
|
+
<SkeletonRow
|
|
24
|
+
fullWidth={true}
|
|
25
|
+
size="large"
|
|
26
|
+
className="dropin-product-item-card__skeleton__item"
|
|
27
|
+
/>
|
|
28
|
+
<SkeletonRow
|
|
29
|
+
fullWidth={true}
|
|
30
|
+
size="xsmall"
|
|
31
|
+
className="dropin-product-item-card__skeleton__item"
|
|
32
|
+
/>
|
|
33
|
+
<SkeletonRow
|
|
34
|
+
fullWidth={true}
|
|
35
|
+
size="small"
|
|
36
|
+
className="dropin-product-item-card__skeleton__item"
|
|
37
|
+
/>
|
|
38
|
+
</div>{' '}
|
|
39
|
+
</Skeleton>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/********************************************************************
|
|
2
|
+
* Copyright 2025 Adobe
|
|
3
|
+
* All Rights Reserved.
|
|
4
|
+
*
|
|
5
|
+
* NOTICE: Adobe permits you to use, modify, and distribute this
|
|
6
|
+
* file in accordance with the terms of the Adobe license agreement
|
|
7
|
+
* accompanying it.
|
|
8
|
+
*******************************************************************/
|
|
9
|
+
|
|
10
|
+
export * from '@adobe-commerce/elsie/components/ProductItemCard/ProductItemCard';
|
|
11
|
+
export { ProductItemCard as default } from '@adobe-commerce/elsie/components/ProductItemCard/ProductItemCard';
|
package/src/components/index.ts
CHANGED
|
@@ -47,3 +47,4 @@ export * from '@adobe-commerce/elsie/components/Header';
|
|
|
47
47
|
export * from '@adobe-commerce/elsie/components/Tag';
|
|
48
48
|
export * from '@adobe-commerce/elsie/components/ContentGrid';
|
|
49
49
|
export * from '@adobe-commerce/elsie/components/Pagination';
|
|
50
|
+
export * from '@adobe-commerce/elsie/components/ProductItemCard';
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Meta, Unstyled } from '@storybook/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Utilities/Links" />
|
|
4
|
+
<Unstyled>
|
|
5
|
+
|
|
6
|
+
# Adding Links using the route pattern
|
|
7
|
+
|
|
8
|
+
Whenever possible, avoid placing `onClick` handlers directly on anchor elements (`<a>`) in drop-in components, such as product or category pages, as this results in accessibility issues and broken browser behavior. Problems include:
|
|
9
|
+
|
|
10
|
+
- Right-click > Open in New Tab results in blank pages.
|
|
11
|
+
- Middle-click (open in background tab) won't work as expected.
|
|
12
|
+
- Keyboard navigation and screen readers may not trigger the link correctly.
|
|
13
|
+
|
|
14
|
+
Instead, follow the route pattern to provide composable and accessible navigation.
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
Components accept a `routeX` function as a prop. The function receives a data model (a product, for example) and returns a URL. Internally, it's used like this:
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
<a href={routeProduct?.(product) ?? '#'}>...</a>
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This lets developers customize routing logic per storefront while preserving link semantics.
|
|
25
|
+
|
|
26
|
+
## Example — Component-Side
|
|
27
|
+
|
|
28
|
+
In your component (a PLP item, for example):
|
|
29
|
+
|
|
30
|
+
The `routeProduct` prop must be optional and default to a # or an non-functional element like a `div` if not provided.
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
type Props = {
|
|
34
|
+
routeProduct?: (product: ProductModel) => string;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function ProductCard({ product, routeProduct }: Props) {
|
|
38
|
+
return (
|
|
39
|
+
<a href={routeProduct?.(product) ?? '#'}>
|
|
40
|
+
<div>{product.name}</div>
|
|
41
|
+
</a>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Example — Storefront-Side
|
|
47
|
+
|
|
48
|
+
In the storefront integration (`commerce-cart.js` or `commerce-plp.js`, for example):
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
import { rootLink } from '@adobe/commerce-url-utils';
|
|
52
|
+
|
|
53
|
+
provider.render(ProductList, {
|
|
54
|
+
routeProduct: (product) => rootLink(`/products/${product.url.urlKey}/${product.topLevelSku}`),
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
</Unstyled>
|