@adobe-commerce/elsie 1.2.0 → 1.2.1-alpha56
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 +45 -4
- package/package.json +1 -1
- package/src/components/CartItem/CartItem.tsx +1 -3
- 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/VComponent.mdx +92 -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: {},
|
|
@@ -274,12 +282,12 @@ export default {
|
|
|
274
282
|
|
|
275
283
|
// Normalize elsie imports to point to the correct paths
|
|
276
284
|
content = content.replace(
|
|
277
|
-
/'(.*@adobe\/elsie\/src\/)/g,
|
|
285
|
+
/'(.*@adobe-commerce\/elsie\/src\/)/g,
|
|
278
286
|
"'@dropins/tools/types/elsie/src/"
|
|
279
287
|
);
|
|
280
288
|
|
|
281
289
|
content = content.replace(
|
|
282
|
-
/'(@adobe\/elsie\/icons)'/g,
|
|
290
|
+
/'(@adobe-commerce\/elsie\/icons)'/g,
|
|
283
291
|
"'@dropins/tools/types/elsie/src/icons'"
|
|
284
292
|
);
|
|
285
293
|
|
|
@@ -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
|
@@ -432,9 +432,7 @@ export const CartItem: FunctionComponent<CartItemProps> = ({
|
|
|
432
432
|
|
|
433
433
|
{/* Footer */}
|
|
434
434
|
{footer && (
|
|
435
|
-
|
|
436
|
-
<VComponent node={footer} />
|
|
437
|
-
</div>
|
|
435
|
+
<VComponent node={footer} className={classes(['dropin-cart-item__footer'])} />
|
|
438
436
|
)}
|
|
439
437
|
</div>
|
|
440
438
|
|
|
@@ -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,92 @@
|
|
|
1
|
+
import { Meta, Unstyled } from '@storybook/blocks';
|
|
2
|
+
|
|
3
|
+
<Meta title="Utilities/VComponent" />
|
|
4
|
+
<Unstyled>
|
|
5
|
+
|
|
6
|
+
# VComponent: A VNode wrapper for dynamic rendering
|
|
7
|
+
|
|
8
|
+
In modern Preact-based architectures, composability and flexibility are essential for building reusable UI components. `VComponent` is a utility provided by the SDK that enables rendering of virtual nodes (`VNode`) passed as props—empowering consumers to inject arbitrary content while maintaining a clean separation of concerns.
|
|
9
|
+
|
|
10
|
+
## Why use VComponent?
|
|
11
|
+
|
|
12
|
+
By default, Preact allows children to be passed as virtual nodes, enabling dynamic rendering:
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
<MyComponent>
|
|
16
|
+
<h1>Hello</h1>
|
|
17
|
+
</MyComponent>
|
|
18
|
+
```
|
|
19
|
+
However, flexibility increases when we extend this pattern to named props like `header`, `footer`, or `image`. Instead of hardcoding internal markup, we delegate the responsibility of rendering to the consumer.
|
|
20
|
+
|
|
21
|
+
## Traditional approach (tightly coupled)
|
|
22
|
+
|
|
23
|
+
The standard approach to rendering a component is to pass values as props directly to the component.
|
|
24
|
+
|
|
25
|
+
**Implementation:**
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
const Card = ({ imageProps }) => {
|
|
29
|
+
return <img {...imageProps} />;
|
|
30
|
+
};
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
**Usage:**
|
|
34
|
+
```tsx
|
|
35
|
+
<Card imageProps={{ src: 'logo.png', alt: 'Logo' }} />
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This implementation tightly couples the component to a specific HTML element (`<img>`), which limits its flexibility and reuse.
|
|
39
|
+
|
|
40
|
+
## Composable approach with VComponent
|
|
41
|
+
|
|
42
|
+
The composable approach with `VComponent` allows consumers to pass arbitrary DOM nodes through props.
|
|
43
|
+
|
|
44
|
+
**Implementation:**
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import { VComponent } from '@adobe-commerce/elsie/lib';
|
|
48
|
+
|
|
49
|
+
interface Props {
|
|
50
|
+
image: VNode;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const Card = ({ image }: Props) => {
|
|
54
|
+
return <VComponent node={image} className="dropin-header-image" />;
|
|
55
|
+
};
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Usage:**
|
|
59
|
+
|
|
60
|
+
```tsx
|
|
61
|
+
<Card image={<img src="logo.png" alt="Logo" />} />
|
|
62
|
+
// or with a custom slot/component
|
|
63
|
+
<Card image={<Slot name="brand-image" />} />
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
This decouples the component from a specific element. Instead, it renders whatever VNode is passed in. Consumers now have full control over what gets displayed.
|
|
67
|
+
|
|
68
|
+
## How it works
|
|
69
|
+
|
|
70
|
+
`VComponent` is a thin wrapper around a virtual node (`VNode`). It renders the node it receives as-is, while optionally applying extra props like `className`.
|
|
71
|
+
|
|
72
|
+
This makes it ideal for rendering content passed through slots or injected from a higher-order component.
|
|
73
|
+
|
|
74
|
+
```tsx
|
|
75
|
+
<VComponent node={header} className="my-header" />
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## When to use it
|
|
79
|
+
|
|
80
|
+
Use `VComponent` when:
|
|
81
|
+
|
|
82
|
+
- You want to allow injected custom DOM nodes (icons, slots, rich content)
|
|
83
|
+
- You're designing reusable components meant to be extended or implemented by different consumers (Containers, Slots, etc.)
|
|
84
|
+
|
|
85
|
+
## Benefits
|
|
86
|
+
|
|
87
|
+
- Promotes reusability and composability
|
|
88
|
+
- Supports custom rendering logic with no assumptions
|
|
89
|
+
- Reduces internal complexity by offloading rendering decisions
|
|
90
|
+
- Ideal for BYO-UI and dynamic layout strategies
|
|
91
|
+
|
|
92
|
+
</Unstyled>
|
|
@@ -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>
|