@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 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: {},
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.2.0",
3
+ "version": "1.2.1-alpha56",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -432,9 +432,7 @@ export const CartItem: FunctionComponent<CartItemProps> = ({
432
432
 
433
433
  {/* Footer */}
434
434
  {footer && (
435
- <div className={classes(['dropin-cart-item__footer'])}>
436
- <VComponent node={footer} />
437
- </div>
435
+ <VComponent node={footer} className={classes(['dropin-cart-item__footer'])} />
438
436
  )}
439
437
  </div>
440
438
 
@@ -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,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>