@adobe-commerce/elsie 1.0.1-alpha04151330 → 1.1.0-alpha2345

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/__mocks__/svg.js +2 -1
  2. package/bin/builders/build/index.js +1 -2
  3. package/bin/builders/generate/api/index.js +9 -0
  4. package/bin/builders/generate/api/templates/function.js +11 -2
  5. package/bin/builders/generate/api/templates/index.js +11 -2
  6. package/bin/builders/generate/api/templates/story.js +11 -2
  7. package/bin/builders/generate/api/templates/unit-test.js +11 -2
  8. package/bin/builders/generate/component/index.js +9 -0
  9. package/bin/builders/generate/component/templates/Component.js +22 -4
  10. package/bin/builders/generate/component/templates/css.js +11 -2
  11. package/bin/builders/generate/component/templates/index.js +11 -2
  12. package/bin/builders/generate/component/templates/stories.js +11 -2
  13. package/bin/builders/generate/component/templates/unit-test.js +11 -2
  14. package/bin/builders/generate/config/index.js +9 -0
  15. package/bin/builders/generate/config/templates/elsie.js +10 -1
  16. package/bin/builders/generate/container/index.js +9 -0
  17. package/bin/builders/generate/container/templates/Component.js +11 -2
  18. package/bin/builders/generate/container/templates/index.js +11 -2
  19. package/bin/builders/generate/container/templates/stories.js +11 -2
  20. package/bin/builders/generate/container/templates/unit-test.js +11 -2
  21. package/bin/builders/generate/index.js +9 -0
  22. package/bin/builders/lint/index.js +1 -1
  23. package/bin/builders/storybook/index.js +1 -1
  24. package/bin/builders/test/index.js +1 -1
  25. package/bin/lib/cli.js +16 -1
  26. package/package.json +2 -2
  27. package/src/components/Header/Header.tsx +2 -2
  28. package/src/components/ImageSwatch/ImageSwatch.css +10 -0
  29. package/src/components/ImageSwatch/ImageSwatch.stories.tsx +53 -4
  30. package/src/components/ImageSwatch/ImageSwatch.tsx +44 -9
  31. package/src/components/Incrementer/Incrementer.tsx +10 -1
  32. package/src/components/Tag/Tag.stories.tsx +1 -0
  33. package/src/docs/slots.mdx +21 -3
  34. package/src/lib/slot.tsx +30 -15
package/__mocks__/svg.js CHANGED
@@ -7,4 +7,5 @@
7
7
  * accompanying it.
8
8
  *******************************************************************/
9
9
 
10
- export * from '../src/icons';
10
+ const SVGComponent = 'svg';
11
+ export default SVGComponent;
@@ -3,8 +3,7 @@ const path = require('path');
3
3
  module.exports = async function generateResourceBuilder({ argv }) {
4
4
  const { build } = await import('vite');
5
5
 
6
- const configFile =
7
- argv?.config ?? path.resolve(__dirname, '../../../config/vite.mjs');
6
+ const configFile = argv?.config ?? path.resolve(__dirname, '../../../config/vite.mjs');
8
7
 
9
8
  const outDir = argv?.outDir ?? 'dist';
10
9
 
@@ -1,3 +1,12 @@
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
+
1
10
  const path = require('path');
2
11
  const fs = require('fs');
3
12
  const writeFile = require('../../../lib/write-file');
@@ -1,9 +1,18 @@
1
1
  const { stripIndent } = require('common-tags');
2
2
 
3
3
  module.exports = ({ basename }) => {
4
- return `${stripIndent`
4
+ return `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${stripIndent`
5
14
  export const ${basename} = () => {
6
15
  return 'Howdy!';
7
16
  }
8
- `}\n`;
17
+ `}\n`;
9
18
  };
@@ -1,7 +1,16 @@
1
1
  const { source } = require('common-tags');
2
2
 
3
3
  module.exports = ({ basename, importPath = '.' }) => {
4
- return `${source`
4
+ return `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${source`
5
14
  export * from '${importPath}/${basename}';
6
- `}\n`;
15
+ `}\n`;
7
16
  };
@@ -1,7 +1,16 @@
1
1
  const { source } = require('common-tags');
2
2
 
3
3
  module.exports = ({ pathname, basename, importPath = '.' }) => {
4
- return `${source`
4
+ return `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${source`
5
14
  import { Meta } from '@storybook/blocks';
6
15
 
7
16
  <Meta title="API/${pathname}" />
@@ -19,5 +28,5 @@ module.exports = ({ pathname, basename, importPath = '.' }) => {
19
28
  \`\`\`ts
20
29
  ${basename}();
21
30
  \`\`\`
22
- `}\n`;
31
+ `}\n`;
23
32
  };
@@ -1,7 +1,16 @@
1
1
  const { source } = require('common-tags');
2
2
 
3
3
  module.exports = ({ name, basename, pathname, importPath = '.' }) => {
4
- return `${source`
4
+ return `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${source`
5
14
  import { ${basename} } from '${importPath}';
6
15
 
7
16
  describe('${name}/api/${pathname}', () => {
@@ -11,5 +20,5 @@ module.exports = ({ name, basename, pathname, importPath = '.' }) => {
11
20
  expect(value).toEqual('Howdy!');
12
21
  });
13
22
  });
14
- `}\n`;
23
+ `}\n`;
15
24
  };
@@ -1,3 +1,12 @@
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
+
1
10
  const path = require('path');
2
11
  const fs = require('fs');
3
12
  const writeFile = require('../../../lib/write-file');
@@ -5,7 +5,16 @@ module.exports = ({ basename, importPath = '.', skipCSS, cssPrefix }) => {
5
5
  const _name = hyphenatedName(basename);
6
6
 
7
7
  if (skipCSS)
8
- return `${stripIndent`
8
+ return `/********************************************************************
9
+ * Copyright 2025 Adobe
10
+ * All Rights Reserved.
11
+ *
12
+ * NOTICE: Adobe permits you to use, modify, and distribute this
13
+ * file in accordance with the terms of the Adobe license agreement
14
+ * accompanying it.
15
+ *******************************************************************/
16
+
17
+ ${stripIndent`
9
18
  import { FunctionComponent } from 'preact';
10
19
  import { HTMLAttributes } from 'preact/compat';
11
20
 
@@ -18,9 +27,18 @@ module.exports = ({ basename, importPath = '.', skipCSS, cssPrefix }) => {
18
27
  </div>
19
28
  );
20
29
  };
21
- `}\n`;
30
+ `}\n`;
22
31
 
23
- return `${stripIndent`
32
+ return `/********************************************************************
33
+ * Copyright 2025 Adobe
34
+ * All Rights Reserved.
35
+ *
36
+ * NOTICE: Adobe permits you to use, modify, and distribute this
37
+ * file in accordance with the terms of the Adobe license agreement
38
+ * accompanying it.
39
+ *******************************************************************/
40
+
41
+ ${stripIndent`
24
42
  import { FunctionComponent } from 'preact';
25
43
  import { HTMLAttributes } from 'preact/compat';
26
44
  import { classes } from '@adobe-commerce/elsie/lib';
@@ -39,5 +57,5 @@ module.exports = ({ basename, importPath = '.', skipCSS, cssPrefix }) => {
39
57
  </div>
40
58
  );
41
59
  };
42
- `}\n`;
60
+ `}\n`;
43
61
  };
@@ -4,7 +4,16 @@ const { hyphenatedName } = require('../../../../lib/string');
4
4
  module.exports = ({ basename, cssPrefix }) => {
5
5
  const _name = hyphenatedName(basename);
6
6
 
7
- return `${source`
7
+ return `/********************************************************************
8
+ * Copyright 2025 Adobe
9
+ * All Rights Reserved.
10
+ *
11
+ * NOTICE: Adobe permits you to use, modify, and distribute this
12
+ * file in accordance with the terms of the Adobe license agreement
13
+ * accompanying it.
14
+ *******************************************************************/
15
+
16
+ ${source`
8
17
  /* https://cssguidelin.es/#bem-like-naming */
9
18
 
10
19
  /* .${cssPrefix}${_name} { } */
@@ -20,5 +29,5 @@ module.exports = ({ basename, cssPrefix }) => {
20
29
 
21
30
  /* XXlarge (large laptops and desktops, 1920px and up) */
22
31
  /* @media only screen and (min-width: 1920px) { } */
23
- `}\n`;
32
+ `}\n`;
24
33
  };
@@ -1,8 +1,17 @@
1
1
  const { source } = require('common-tags');
2
2
 
3
3
  module.exports = ({ basename, importPath = '.' }) => {
4
- return `${source`
4
+ return `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${source`
5
14
  export * from '${importPath}/${basename}';
6
15
  export { ${basename} as default } from '${importPath}/${basename}';
7
- `}\n`;
16
+ `}\n`;
8
17
  };
@@ -1,7 +1,16 @@
1
1
  const { source } = require('common-tags');
2
2
 
3
3
  module.exports = ({ name, pathname, basename, group, importPath = '.' }) => {
4
- return `${source`
4
+ return `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${source`
5
14
  // https://storybook.js.org/docs/7.0/preact/writing-stories/introduction
6
15
  import type { Meta, StoryObj } from '@storybook/preact';
7
16
  import { ${basename} as component, ${basename}Props } from '${importPath}';
@@ -42,5 +51,5 @@ module.exports = ({ name, pathname, basename, group, importPath = '.' }) => {
42
51
  children: "👋 Hello from your new ${basename} story!",
43
52
  },
44
53
  };
45
- `}\n`;
54
+ `}\n`;
46
55
  };
@@ -1,7 +1,16 @@
1
1
  const { source } = require('common-tags');
2
2
 
3
3
  module.exports = ({ name, basename, pathname, group, importPath = '.' }) => {
4
- return `${source`
4
+ return `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${source`
5
14
  /** https://preactjs.com/guide/v10/preact-testing-library/ */
6
15
 
7
16
  import { render } from '@adobe-commerce/elsie/lib/tests';
@@ -15,5 +24,5 @@ module.exports = ({ name, basename, pathname, group, importPath = '.' }) => {
15
24
  expect(!!container).toEqual(true);
16
25
  });
17
26
  });
18
- `}\n`;
27
+ `}\n`;
19
28
  };
@@ -1,3 +1,12 @@
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
+
1
10
  const path = require('path');
2
11
  const fs = require('fs/promises');
3
12
  const m = require('../../../lib/log-message');
@@ -1,7 +1,16 @@
1
1
  const { source } = require('common-tags');
2
2
 
3
3
  module.exports = ({ name, importPath }) =>
4
- `${source`
4
+ `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${source`
5
14
  module.exports = {
6
15
  name: '${name}',
7
16
  api: {
@@ -1,3 +1,12 @@
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
+
1
10
  const path = require('path');
2
11
  const fs = require('fs');
3
12
  const writeFile = require('../../../lib/write-file');
@@ -1,7 +1,16 @@
1
1
  const { stripIndent } = require('common-tags');
2
2
 
3
3
  module.exports = ({ basename }) => {
4
- return `${stripIndent`
4
+ return `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${stripIndent`
5
14
  import { HTMLAttributes } from 'preact/compat';
6
15
  import { Container } from '@adobe-commerce/elsie/lib';
7
16
 
@@ -14,5 +23,5 @@ module.exports = ({ basename }) => {
14
23
  </div>
15
24
  );
16
25
  };
17
- `}\n`;
26
+ `}\n`;
18
27
  };
@@ -1,8 +1,17 @@
1
1
  const { source } = require('common-tags');
2
2
 
3
3
  module.exports = ({ basename, importPath = '.' }) => {
4
- return `${source`
4
+ return `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${source`
5
14
  export * from '${importPath}/${basename}';
6
15
  export { ${basename} as default } from '${importPath}/${basename}';
7
- `}\n`;
16
+ `}\n`;
8
17
  };
@@ -1,7 +1,16 @@
1
1
  const { source } = require('common-tags');
2
2
 
3
3
  module.exports = ({ pathname, basename, importPath = '.' }) => {
4
- return `${source`
4
+ return `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${source`
5
14
  // https://storybook.js.org/docs/7.0/preact/writing-stories/introduction
6
15
  import type { Meta, StoryObj } from '@storybook/preact';
7
16
  import { ${basename} as component, ${basename}Props } from '${importPath}';
@@ -30,5 +39,5 @@ module.exports = ({ pathname, basename, importPath = '.' }) => {
30
39
  children: "👋 Howdy, I'm Howdy!",
31
40
  },
32
41
  };
33
- `}\n`;
42
+ `}\n`;
34
43
  };
@@ -1,7 +1,16 @@
1
1
  const { source } = require('common-tags');
2
2
 
3
3
  module.exports = ({ name, basename, pathname, importPath = '.' }) => {
4
- return `${source`
4
+ return `/********************************************************************
5
+ * Copyright 2025 Adobe
6
+ * All Rights Reserved.
7
+ *
8
+ * NOTICE: Adobe permits you to use, modify, and distribute this
9
+ * file in accordance with the terms of the Adobe license agreement
10
+ * accompanying it.
11
+ *******************************************************************/
12
+
13
+ ${source`
5
14
  /** https://preactjs.com/guide/v10/preact-testing-library/ */
6
15
 
7
16
  import { render } from '@adobe-commerce/elsie/lib/tests';
@@ -15,5 +24,5 @@ module.exports = ({ name, basename, pathname, importPath = '.' }) => {
15
24
  expect(!!container).toEqual(true);
16
25
  });
17
26
  });
18
- `}\n`;
27
+ `}\n`;
19
28
  };
@@ -1,3 +1,12 @@
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
+
1
10
  const path = require('path');
2
11
  const { toPascalCase, toCamelCase } = require('../../lib/string');
3
12
  const m = require('../../lib/log-message');
@@ -1,5 +1,5 @@
1
1
  const cli = require('../../lib/cli');
2
2
 
3
3
  module.exports = function generateResourceBuilder() {
4
- cli('eslint "*/**/*.{ts,tsx}"');
4
+ return cli('eslint "*/**/*.{ts,tsx}"');
5
5
  };
@@ -1,5 +1,5 @@
1
1
  const cli = require('../../lib/cli');
2
2
 
3
3
  module.exports = function generateResourceBuilder() {
4
- cli('storybook dev -h localhost -p 6006 --disable-telemetry --quiet');
4
+ return cli('storybook dev -h localhost -p 6006 --disable-telemetry --quiet');
5
5
  };
@@ -1,5 +1,5 @@
1
1
  const cli = require('../../lib/cli');
2
2
 
3
3
  module.exports = function generateResourceBuilder() {
4
- cli('jest');
4
+ return cli('jest');
5
5
  };
package/bin/lib/cli.js CHANGED
@@ -4,5 +4,20 @@ module.exports = function cli(command) {
4
4
  let cmd = command;
5
5
  const argvs = process.argv.slice(3).join(' ');
6
6
  if (argvs) cmd += ` ${argvs}`;
7
- return spawn(cmd, { shell: true, stdio: 'inherit' });
7
+
8
+ return new Promise((resolve, reject) => {
9
+ const child = spawn(cmd, { shell: true, stdio: 'inherit' });
10
+
11
+ child.on('close', (code) => {
12
+ if (code !== 0) {
13
+ reject(new Error(`Command failed with exit code ${code}`));
14
+ } else {
15
+ resolve(child);
16
+ }
17
+ });
18
+
19
+ child.on('error', (err) => {
20
+ reject(err);
21
+ });
22
+ });
8
23
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.0.1-alpha04151330",
3
+ "version": "1.1.0-alpha2345",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -15,7 +15,7 @@
15
15
  "dev": "concurrently 'yarn storybook' 'yarn serve'",
16
16
  "storybook": "elsie storybook",
17
17
  "serve": "elsie serve --config vite.config.mjs",
18
- "lint": "elsie lint --max-warnings=0",
18
+ "lint": "elsie lint",
19
19
  "test": "elsie test",
20
20
  "test:ci": "jest --config jest.config.js --passWithNoTests --coverage",
21
21
  "build": "elsie build --config vite.config.mjs",
@@ -7,13 +7,13 @@
7
7
  * accompanying it.
8
8
  *******************************************************************/
9
9
 
10
- import { ComponentChildren, FunctionComponent, VNode } from 'preact';
10
+ import { ComponentChildren, FunctionComponent, VNode, JSX } from 'preact';
11
11
  import { HTMLAttributes } from 'preact/compat';
12
12
  import { classes, VComponent } from '@adobe-commerce/elsie/lib';
13
13
  import { Divider } from '@adobe-commerce/elsie/components';
14
14
  import '@adobe-commerce/elsie/components/Header/Header.css';
15
15
 
16
- export interface HeaderProps extends HTMLAttributes<HTMLDivElement> {
16
+ export interface HeaderProps extends Omit<HTMLAttributes<HTMLDivElement>, 'size'> {
17
17
  title: string;
18
18
  size?: 'medium' | 'large';
19
19
  divider?: boolean;
@@ -33,6 +33,7 @@
33
33
  overflow: hidden;
34
34
  }
35
35
 
36
+ .dropin-image-swatch__span img,
36
37
  .dropin-image-swatch__content {
37
38
  width: inherit;
38
39
  position: absolute;
@@ -121,10 +122,19 @@
121
122
  }
122
123
 
123
124
  .dropin-image-swatch__span--out-of-stock > .dropin-image-swatch__content,
125
+ .dropin-image-swatch__span--out-of-stock img,
126
+ .dropin-image-swatch__container
127
+ input[type='radio']:disabled
128
+ ~ .dropin-image-swatch__span
129
+ img,
124
130
  .dropin-image-swatch__container
125
131
  input[type='radio']:disabled
126
132
  ~ .dropin-image-swatch__span
127
133
  > .dropin-image-swatch__content,
134
+ .dropin-image-swatch__container
135
+ input[type='checkbox']:disabled
136
+ ~ .dropin-image-swatch__span
137
+ img,
128
138
  .dropin-image-swatch__container
129
139
  input[type='checkbox']:disabled
130
140
  ~ .dropin-image-swatch__span
@@ -260,11 +260,11 @@ export const MultiImageSwatch: Story = {
260
260
  ),
261
261
  };
262
262
 
263
- export const CustomImageNodeSwatch: Story = {
263
+ export const CustomImageNodeVNodeSwatch: Story = {
264
264
  args: {
265
265
  name: 'customImageSwatch',
266
266
  id: 'customImageSwatch1',
267
- label: 'Custom Image Node Example',
267
+ label: 'Custom Image Node VNode Example',
268
268
  groupAriaLabel: 'Custom Image Swatches',
269
269
  value: 'customImageNode',
270
270
  src: `https://picsum.photos/${defaultWidth}/${defaultHeight}`, // fallback, not used with imageNode
@@ -277,7 +277,7 @@ export const CustomImageNodeSwatch: Story = {
277
277
  <div style="position: relative; width: 100%; height: 100%;">
278
278
  <img
279
279
  src={`https://picsum.photos/${defaultWidth}/${defaultHeight}?grayscale`}
280
- alt="Custom grayscale image"
280
+ alt="Custom grayscale image - VNode"
281
281
  style="width: 100%; height: 100%; object-fit: cover;"
282
282
  />
283
283
  <div style="position: absolute; top: 0; left: 0; background: rgba(255,255,255,0.7); padding: 4px 8px; border-radius: 0 0 8px 0;">
@@ -295,7 +295,56 @@ export const CustomImageNodeSwatch: Story = {
295
295
  'div[style*="position: relative"]'
296
296
  );
297
297
  const customImage = canvasElement.querySelector(
298
- 'img[alt="Custom grayscale image"]'
298
+ 'img[alt="Custom grayscale image - VNode"]'
299
+ );
300
+ const customLabel = canvasElement.querySelector(
301
+ 'span[style*="font-weight: bold"]'
302
+ );
303
+
304
+ expect(imageSwatch).toBeInTheDocument();
305
+ expect(customImageContainer).toBeInTheDocument();
306
+ expect(customImage).toBeInTheDocument();
307
+ expect(customLabel).toBeInTheDocument();
308
+ expect(customLabel?.textContent).toBe('Custom');
309
+ },
310
+ };
311
+
312
+ export const CustomImageNodeRenderFunctionSwatch: Story = {
313
+ args: {
314
+ name: 'customImageSwatch',
315
+ id: 'customImageSwatch2',
316
+ label: 'Custom Image Node Render Function Example',
317
+ groupAriaLabel: 'Custom Image Swatches',
318
+ value: 'customImageNode',
319
+ src: `https://picsum.photos/${defaultWidth}/${defaultHeight}`, // fallback, not used with imageNode
320
+ alt: 'Custom Image Node',
321
+ selected: false,
322
+ disabled: false,
323
+ outOfStock: false,
324
+ onValue: action('onValue'),
325
+ imageNode: () => (
326
+ <div style="position: relative; width: 100%; height: 100%;">
327
+ <img
328
+ src={`https://picsum.photos/${defaultWidth}/${defaultHeight}?grayscale`}
329
+ alt="Custom grayscale image - Render Function"
330
+ style="width: 100%; height: 100%; object-fit: cover;"
331
+ />
332
+ <div style="position: absolute; top: 0; left: 0; background: rgba(255,255,255,0.7); padding: 4px 8px; border-radius: 0 0 8px 0;">
333
+ <span style="font-size: 12px; font-weight: bold; color: #333;">
334
+ Custom
335
+ </span>
336
+ </div>
337
+ </div>
338
+ ),
339
+ },
340
+ play: async ({ canvasElement }) => {
341
+ const canvas = within(canvasElement);
342
+ const imageSwatch = await canvas.findByRole('radio');
343
+ const customImageContainer = canvasElement.querySelector(
344
+ 'div[style*="position: relative"]'
345
+ );
346
+ const customImage = canvasElement.querySelector(
347
+ 'img[alt="Custom grayscale image - Render Function"]'
299
348
  );
300
349
  const customLabel = canvasElement.querySelector(
301
350
  'span[style*="font-weight: bold"]'
@@ -8,11 +8,26 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import { FunctionComponent, VNode } from 'preact';
11
- import { HTMLAttributes, useCallback } from 'preact/compat';
11
+ import { HTMLAttributes, useCallback, JSX, useMemo } from 'preact/compat';
12
12
  import { classes } from '@adobe-commerce/elsie/lib';
13
13
  import '@adobe-commerce/elsie/components/ImageSwatch/ImageSwatch.css';
14
- import { Image } from '@adobe-commerce/elsie/components/Image';
14
+ import { Image, ImageProps } from '@adobe-commerce/elsie/components/Image';
15
15
  import { useText } from '@adobe-commerce/elsie/i18n';
16
+
17
+ export interface ImageNodeRenderProps extends ImageProps {
18
+ imageSwatchContext: {
19
+ disabled?: boolean;
20
+ outOfStock?: boolean;
21
+ multi?: boolean;
22
+ selected?: boolean;
23
+ value?: string;
24
+ label?: string;
25
+ groupAriaLabel?: string;
26
+ name?: string;
27
+ id?: string;
28
+ };
29
+ }
30
+
16
31
  export interface ImageSwatchProps
17
32
  extends Omit<HTMLAttributes<HTMLInputElement>, 'label'> {
18
33
  name?: string;
@@ -26,7 +41,7 @@ export interface ImageSwatchProps
26
41
  selected?: boolean;
27
42
  outOfStock?: boolean;
28
43
  multi?: boolean;
29
- imageNode?: VNode;
44
+ imageNode?: VNode | ((props: ImageNodeRenderProps) => JSX.Element);
30
45
  onValue?: (value: any) => void;
31
46
  onUpdateError?: (error: Error) => void;
32
47
  }
@@ -80,6 +95,16 @@ export const ImageSwatch: FunctionComponent<ImageSwatchProps> = ({
80
95
  return `${groupAriaLabel}: ${label} ${swatchLabel}`;
81
96
  };
82
97
 
98
+ const imageProps: ImageProps = useMemo(() => {
99
+ return {
100
+ src,
101
+ alt,
102
+ loading: 'lazy',
103
+ params: { width: 100, fit: 'bounds', crop: true },
104
+ onError: (e: any) => (e.target.style.display = 'none'),
105
+ };
106
+ }, [src, alt]);
107
+
83
108
  return (
84
109
  <label className={classes(['dropin-image-swatch__container', className])}>
85
110
  <input
@@ -107,14 +132,24 @@ export const ImageSwatch: FunctionComponent<ImageSwatchProps> = ({
107
132
  className,
108
133
  ])}
109
134
  >
110
- {imageNode || (
135
+ {typeof imageNode === 'function' ? (
136
+ imageNode({
137
+ ...imageProps,
138
+ imageSwatchContext: {
139
+ disabled,
140
+ outOfStock,
141
+ selected,
142
+ value,
143
+ label,
144
+ groupAriaLabel,
145
+ name,
146
+ id,
147
+ },
148
+ })
149
+ ) : imageNode || (
111
150
  <Image
112
- src={src}
151
+ {...imageProps}
113
152
  className={classes(['dropin-image-swatch__content'])}
114
- params={{ width: 100, fit: 'bounds', crop: true }}
115
- alt={alt}
116
- loading={'lazy'}
117
- onError={(e: any) => (e.target.style.display = 'none')}
118
153
  />
119
154
  )}
120
155
  </span>
@@ -8,7 +8,7 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import { FunctionComponent } from 'preact';
11
- import { useState, useCallback } from 'preact/hooks';
11
+ import { useState, useCallback, useEffect } from 'preact/hooks';
12
12
  import { HTMLAttributes } from 'preact/compat';
13
13
  import { classes, debounce } from '@adobe-commerce/elsie/lib';
14
14
  import { Add, Minus } from '@adobe-commerce/elsie/icons';
@@ -56,6 +56,15 @@ export const Incrementer: FunctionComponent<IncrementerProps> = ({
56
56
  ? 'Dropin.Incrementer.maxQuantityMessage'
57
57
  : 'Dropin.Incrementer.errorMessage';
58
58
 
59
+ // Add this effect to synchronize internal state with external value prop
60
+ useEffect(() => {
61
+ const propValue = Number(value);
62
+ if (propValue !== currentValue) {
63
+ setCurrentValue(propValue);
64
+ }
65
+ // eslint-disable-next-line react-hooks/exhaustive-deps
66
+ }, [value]);
67
+
59
68
  // eslint-disable-next-line react-hooks/exhaustive-deps
60
69
  const debouncedOnValueHandler = useCallback(
61
70
  debounce(async (newValue: any) => {
@@ -48,6 +48,7 @@ const Template: StoryObj<TagProps> = {
48
48
  <Tag {...args}>
49
49
  {/* This workaround allows children to be edited as plain text in Storybook */}
50
50
  {args.children && typeof args.children === 'string' ? (
51
+ // eslint-disable-next-line react/no-danger
51
52
  <span dangerouslySetInnerHTML={{ __html: args.children }} />
52
53
  ) : undefined}
53
54
  </Tag>
@@ -32,23 +32,41 @@ The `<Slot />` component is used to define a slot in a container. It receives a
32
32
 
33
33
  The name of the slot in _PascalCase_. `string` (required).
34
34
 
35
- ### wrapperTag
35
+ ### slotTag
36
36
 
37
37
  The HTML tag to use for the slot's wrapper element. This allows you to change the wrapper element from the default `div` to any valid HTML tag (e.g., 'span', 'p', 'a', etc.). When using specific tags like 'a', you can also provide their respective HTML attributes (e.g., 'href', 'target', etc.).
38
38
 
39
39
  Example:
40
40
  ```tsx
41
41
  // Render with a span wrapper
42
- <Slot name="MySlot" wrapperTag="span">
42
+ <Slot name="MySlot" slotTag="span">
43
43
  Inline content
44
44
  </Slot>
45
45
 
46
46
  // Render with an anchor wrapper
47
- <Slot name="MySlot" wrapperTag="a" href="https://example.com" target="_blank">
47
+ <Slot name="MySlot" slotTag="a" href="https://example.com" target="_blank">
48
48
  Link content
49
49
  </Slot>
50
50
  ```
51
51
 
52
+ ### contentTag
53
+
54
+ The HTML tag to use for wrapping dynamically inserted content within the slot. This is separate from the slot's wrapper tag and allows you to control how dynamic content is structured. Defaults to 'div'.
55
+
56
+ Example:
57
+ ```tsx
58
+ <Slot
59
+ name="MySlot"
60
+ slotTag="article" // The outer wrapper will be an article
61
+ contentTag="section" // Dynamic content will be wrapped in sections
62
+ slot={(ctx) => {
63
+ const elem = document.createElement('div');
64
+ elem.innerHTML = 'Dynamic content';
65
+ ctx.appendChild(elem); // This will be wrapped in a section tag
66
+ }}
67
+ />
68
+ ```
69
+
52
70
  ### slot (required)
53
71
 
54
72
  - `ctx`: An object representing the context of the slot, including methods for manipulating the slot's content.
package/src/lib/slot.tsx CHANGED
@@ -25,6 +25,7 @@ import '@adobe-commerce/elsie/components/UIProvider/debugger.css';
25
25
 
26
26
  type MutateElement = (elem: HTMLElement) => void;
27
27
 
28
+
28
29
  interface State {
29
30
  get: (key: string) => void;
30
31
  set: (key: string, value: any) => void;
@@ -42,7 +43,8 @@ interface PrivateContext<T> {
42
43
  _registerMethod: (
43
44
  cb: (next: T & DefaultSlotContext<T>, state: State) => void
44
45
  ) => void;
45
- _htmlElementToVNode: (element: HTMLElement) => VNode;
46
+ // eslint-disable-next-line no-undef
47
+ _htmlElementToVNode: (element: HTMLElement, tag: keyof HTMLElementTagNameMap) => VNode;
46
48
  }
47
49
 
48
50
  interface DefaultSlotContext<T> extends PrivateContext<T> {
@@ -75,7 +77,9 @@ export function useSlot<K, V extends HTMLElement>(
75
77
  context: Context<K> = {},
76
78
  callback?: SlotProps<K>,
77
79
  children?: ComponentChildren,
78
- render?: Function
80
+ render?: Function,
81
+ // eslint-disable-next-line no-undef
82
+ contentTag: keyof HTMLElementTagNameMap = 'div'
79
83
  ): [RefObject<V>, Record<string, any>] {
80
84
  const slotsQueue = useContext(SlotQueueContext);
81
85
 
@@ -146,15 +150,17 @@ export function useSlot<K, V extends HTMLElement>(
146
150
  context._registerMethod = _registerMethod;
147
151
 
148
152
  const _htmlElementToVNode = useCallback((elem: HTMLElement) => {
149
- return (
150
- <div
151
- data-slot-html-element={elem.tagName.toLowerCase()}
152
- ref={(refElem) => {
153
+ return createElement(
154
+ contentTag,
155
+ {
156
+ 'data-slot-html-element': elem.tagName.toLowerCase(),
157
+ ref: (refElem: HTMLElement | null): void => {
153
158
  refElem?.appendChild(elem);
154
- }}
155
- />
159
+ }
160
+ },
161
+ null
156
162
  );
157
- }, []);
163
+ }, [contentTag]);
158
164
 
159
165
  // @ts-ignore
160
166
  context._htmlElementToVNode = _htmlElementToVNode;
@@ -316,7 +322,7 @@ export function useSlot<K, V extends HTMLElement>(
316
322
  status.current = 'loading';
317
323
 
318
324
  log(`🟩 "${name}" Slot Initialized`);
319
- await callback(context as K & DefaultSlotContext<K>, elementRef.current);
325
+ await callback(context as K & DefaultSlotContext<K>, elementRef.current as HTMLDivElement | null);
320
326
  } catch (error) {
321
327
  console.error(`Error in "${callback.name}" Slot callback`, error);
322
328
  } finally {
@@ -356,7 +362,10 @@ interface SlotPropsComponent<T>
356
362
  slot?: SlotProps<T>;
357
363
  context?: Context<T>;
358
364
  render?: (props: Record<string, any>) => VNode | VNode[];
359
- wrapperTag?: keyof HTMLElementTagNameMap;
365
+ // eslint-disable-next-line no-undef
366
+ slotTag?: keyof HTMLElementTagNameMap; // The tag for the slot wrapper itself
367
+ // eslint-disable-next-line no-undef
368
+ contentTag?: keyof HTMLElementTagNameMap; // The tag for dynamically inserted content
360
369
  children?: ComponentChildren;
361
370
  }
362
371
 
@@ -366,9 +375,14 @@ export function Slot<T>({
366
375
  slot,
367
376
  children,
368
377
  render,
369
- wrapperTag = 'div',
378
+ slotTag = 'div',
379
+ contentTag = 'div',
370
380
  ...props
371
- }: Readonly<SlotPropsComponent<T>>) {
381
+ }: Readonly<SlotPropsComponent<T>>): VNode<{
382
+ ref: RefObject<HTMLElement>;
383
+ 'data-slot': string;
384
+ [key: string]: any;
385
+ }> {
372
386
  const slotsQueue = useContext(SlotQueueContext);
373
387
 
374
388
  const [elementRef, slotProps] = useSlot<T, HTMLElement>(
@@ -376,7 +390,8 @@ export function Slot<T>({
376
390
  context,
377
391
  slot,
378
392
  children,
379
- render
393
+ render,
394
+ contentTag
380
395
  );
381
396
 
382
397
  useMemo(() => {
@@ -392,7 +407,7 @@ export function Slot<T>({
392
407
  }, [name, slotsQueue]);
393
408
 
394
409
  return createElement(
395
- wrapperTag,
410
+ slotTag,
396
411
  {
397
412
  ...props,
398
413
  ref: elementRef,