@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 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: !isProd,
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.2.1-alpha1",
3
+ "version": "1.2.2-alpha1",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -33,6 +33,12 @@
33
33
  overflow: hidden;
34
34
  }
35
35
 
36
+ .dropin-image-swatch__span span {
37
+ display: inline-block;
38
+ width: 100%;
39
+ height: 100%;
40
+ }
41
+
36
42
  .dropin-image-swatch__span img,
37
43
  .dropin-image-swatch__content {
38
44
  width: inherit;
@@ -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';
@@ -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>