@adobe-commerce/elsie 1.4.1 → 1.5.0-alpha100

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.
@@ -77,20 +77,24 @@ module.exports = async function generateResourceBuilder(yargs) {
77
77
  alias: 's',
78
78
  describe: 'Path to the source code containing GraphQL operations',
79
79
  type: 'array',
80
- string: true,
81
80
  demandOption: true,
82
81
  })
82
+ .option('excluded', {
83
+ alias: 'x',
84
+ describe: 'Paths to exclude from validation',
85
+ type: 'array',
86
+ demandOption: false,
87
+ })
83
88
  .option('endpoints', {
84
89
  alias: 'e',
85
90
  describe: 'Path to GraphQL endpoints',
86
91
  type: 'array',
87
- string: true,
88
92
  demandOption: true,
89
93
  });
90
94
  },
91
95
  async (argv) => {
92
- const { source, endpoints } = argv;
93
- await validate(source, endpoints);
96
+ const { source, excluded, endpoints } = argv;
97
+ await validate(source, endpoints, excluded);
94
98
  },
95
99
  )
96
100
  .demandCommand(1, 1, 'choose a command: types, mocks or validate');
@@ -5,17 +5,20 @@ const parser = require('@babel/parser');
5
5
  const traverse = require('@babel/traverse');
6
6
  const { getIntrospectionQuery, buildClientSchema, parse, validate } = require('graphql');
7
7
 
8
- async function walk(dir, collected = []) {
8
+ async function walk(dir, excludedPaths = [], collected = []) {
9
+ if (excludedPaths.includes(dir)) return collected;
10
+
9
11
  const dirents = await fsPromises.readdir(dir, { withFileTypes: true });
10
12
 
11
13
  for (const d of dirents) {
12
14
  const full = path.resolve(dir, d.name);
13
15
 
14
16
  if (d.isDirectory()) {
15
- // skip node_modules and “hidden” folders such as .git
17
+ if (excludedPaths.includes(full)) continue;
16
18
  if (d.name === 'node_modules' || d.name.startsWith('.')) continue;
17
- await walk(full, collected);
19
+ await walk(full, excludedPaths, collected);
18
20
  } else if (/\.(c?m?js|ts|tsx)$/.test(d.name)) {
21
+ if (excludedPaths.includes(full)) continue;
19
22
  collected.push(full);
20
23
  }
21
24
  }
@@ -97,10 +100,10 @@ async function validateGqlOperations(endpoint, operation) {
97
100
  }
98
101
  }
99
102
 
100
- async function getAllOperations(directories) {
103
+ async function getAllOperations(directories, excludedPaths = []) {
101
104
  let fullContent = '';
102
105
  for (const directory of directories) {
103
- const files = await walk(path.resolve(directory));
106
+ const files = await walk(path.resolve(directory), excludedPaths.map(p => path.resolve(p)));
104
107
  for (const f of files) {
105
108
  const code = await fsPromises.readFile(f, 'utf8');
106
109
 
@@ -120,11 +123,9 @@ async function getAllOperations(directories) {
120
123
  return fullContent;
121
124
  }
122
125
 
123
-
124
-
125
- module.exports = async function main(sources, endpoints) {
126
+ module.exports = async function main(sources, endpoints, excluded) {
126
127
  for (const endpoint of endpoints) {
127
- const operations = await getAllOperations(sources);
128
+ const operations = await getAllOperations(sources, excluded);
128
129
  if (!operations) {
129
130
  console.error('No GraphQL operations found in the specified directories.');
130
131
  process.exitCode = 0;
@@ -132,4 +133,4 @@ module.exports = async function main(sources, endpoints) {
132
133
  }
133
134
  await validateGqlOperations(endpoint, operations);
134
135
  }
135
- }
136
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.4.1",
3
+ "version": "1.5.0-alpha100",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -0,0 +1,49 @@
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-input-file__input {
13
+ display: none;
14
+ }
15
+
16
+ .dropin-input-file__label {
17
+ border: 0 none;
18
+ cursor: pointer;
19
+ border-radius: var(--shape-border-radius-3);
20
+ padding: var(--spacing-xsmall) var(--spacing-medium);
21
+ display: flex;
22
+ justify-content: center;
23
+ align-items: center;
24
+ background: var(--color-brand-500);
25
+ color: var(--color-neutral-50);
26
+ font: var(--type-button-2-font);
27
+ letter-spacing: var(--type-button-2-letter-spacing);
28
+ }
29
+
30
+ .dropin-input-file__label:hover {
31
+ background-color: var(--color-button-hover);
32
+ }
33
+
34
+ .dropin-input-file__icon {
35
+ height: 24px;
36
+ margin-right: var(--spacing-xsmall);
37
+ }
38
+
39
+ /* Medium (portrait tablets and large phones, 768px and up) */
40
+ /* @media only screen and (min-width: 768px) { } */
41
+
42
+ /* Large (landscape tablets, 1024px and up) */
43
+ /* @media only screen and (min-width: 1024px) { } */
44
+
45
+ /* XLarge (laptops/desktops, 1366px and up) */
46
+ /* @media only screen and (min-width: 1366px) { } */
47
+
48
+ /* XXlarge (large laptops and desktops, 1920px and up) */
49
+ /* @media only screen and (min-width: 1920px) { } */
@@ -0,0 +1,90 @@
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://storybook.js.org/docs/7.0/preact/writing-stories/introduction
11
+ import type { Meta, StoryObj } from '@storybook/preact';
12
+ import { InputFile, InputFileProps } from '@adobe-commerce/elsie/components/InputFile';
13
+ import { action } from '@storybook/addon-actions';
14
+ import { IconsList } from '@adobe-commerce/elsie/components/Icon/Icon.stories.helpers';
15
+
16
+ /**
17
+ * Use InputFile to upload files.
18
+ */
19
+ const meta: Meta<InputFileProps> = {
20
+ title: 'Components/InputFile',
21
+ component: InputFile,
22
+ argTypes: {
23
+ label: {
24
+ description: 'Label for the input file.',
25
+ type: 'string',
26
+ },
27
+ accept: {
28
+ description: 'Restrict selectable file types',
29
+ type: 'string',
30
+ },
31
+ multiple: {
32
+ description: 'Allow multiple files selection.',
33
+ type: {
34
+ name: 'boolean',
35
+ required: false
36
+ },
37
+ },
38
+ id: {
39
+ description: 'id',
40
+ type: {
41
+ required: false,
42
+ name: 'string'
43
+ },
44
+ control: 'text',
45
+ },
46
+ onChange: {
47
+ description: 'Handler for when the file selection changes.',
48
+ control: false,
49
+ table: {
50
+ type: {
51
+ summary: 'function'
52
+ },
53
+ },
54
+ },
55
+ icon: {
56
+ description: 'Optional icon.',
57
+ table: {
58
+ type: { summary: 'FunctionComponent' },
59
+ },
60
+ options: Object.keys(IconsList),
61
+ mapping: IconsList,
62
+ control: 'select',
63
+ },
64
+ },
65
+ };
66
+
67
+ export default meta;
68
+
69
+ type Story = StoryObj<InputFileProps>;
70
+
71
+ export const Default: Story = {
72
+ args: {
73
+ label: 'Upload File',
74
+ id: 'single-file-input',
75
+ accept: ".pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .txt, .csv, .jpg, .jpeg, .png, .gif, .bmp, .tiff, .ico, .webp",
76
+ onChange: action('onChange'),
77
+ icon: 'none' as any,
78
+ },
79
+ };
80
+
81
+ export const MultipleFiles: Story = {
82
+ args: {
83
+ label: 'Upload Multiple Files',
84
+ id: 'multiple-files-input',
85
+ accept: ".pdf, .doc, .docx, .xls, .xlsx, .ppt, .pptx, .txt, .csv, .jpg, .jpeg, .png, .gif, .bmp, .tiff, .ico, .webp",
86
+ multiple: true,
87
+ onChange: action('onChange'),
88
+ icon: 'none' as any,
89
+ },
90
+ };
@@ -0,0 +1,59 @@
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 { useId } from 'preact/hooks';
12
+ import { HTMLAttributes } from 'preact/compat';
13
+ import { classes } from '@adobe-commerce/elsie/lib';
14
+ import '@adobe-commerce/elsie/components/InputFile/InputFile.css';
15
+
16
+ export interface InputFileProps extends Omit<HTMLAttributes<HTMLInputElement>, 'type' | 'icon'> {
17
+ accept?: string;
18
+ onChange?: (event: Event) => void;
19
+ label?: string;
20
+ multiple?: boolean;
21
+ icon?: VNode<HTMLAttributes<SVGSVGElement>>;
22
+ }
23
+
24
+ export const InputFile: FunctionComponent<InputFileProps> = ({
25
+ accept,
26
+ onChange,
27
+ label = 'Upload Document',
28
+ icon,
29
+ className,
30
+ multiple,
31
+ id: providedId,
32
+ ...props
33
+ }) => {
34
+
35
+ const generatedId = useId();
36
+ const id = providedId || generatedId;
37
+
38
+ const handleChange = (e: Event) => {
39
+ onChange?.(e);
40
+ };
41
+
42
+ return (
43
+ <div className={classes(['dropin-input-file', className])}>
44
+ <label htmlFor={id} className="dropin-input-file__label">
45
+ {icon && <span className="dropin-input-file__icon">{icon}</span>}
46
+ {label}
47
+ </label>
48
+ <input
49
+ id={id}
50
+ type="file"
51
+ accept={accept}
52
+ multiple={multiple}
53
+ onChange={handleChange}
54
+ className="dropin-input-file__input"
55
+ {...props}
56
+ />
57
+ </div>
58
+ );
59
+ };
@@ -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/InputFile/InputFile';
11
+ export { InputFile as default } from '@adobe-commerce/elsie/components/InputFile/InputFile';
@@ -8,13 +8,12 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import { Icon } from '@adobe-commerce/elsie/components';
11
+ import '@adobe-commerce/elsie/components/Picker/Picker.css';
11
12
  import { ChevronDown } from '@adobe-commerce/elsie/icons';
12
13
  import { classes } from '@adobe-commerce/elsie/lib';
13
14
  import { FunctionComponent, VNode } from 'preact';
14
15
  import { HTMLAttributes, useEffect, useState } from 'preact/compat';
15
16
 
16
- import '@adobe-commerce/elsie/components/Picker/Picker.css';
17
-
18
17
  type PickerValue = string | null;
19
18
 
20
19
  export interface PickerOption {
@@ -69,10 +68,9 @@ export const Picker: FunctionComponent<PickerProps> = ({
69
68
  defaultOption,
70
69
  icon,
71
70
  className,
72
- id,
73
71
  ...props
74
72
  }) => {
75
- const uniqueId = id ?? name ?? `dropin-picker-${Math.random().toString(36)}`;
73
+ const id = props?.id || name || `dropin-picker-${Math.random().toString(36)}`;
76
74
  const isRequired = !!props?.required;
77
75
 
78
76
  // find the first option that is not disabled
@@ -156,7 +154,7 @@ export const Picker: FunctionComponent<PickerProps> = ({
156
154
  )}
157
155
 
158
156
  <select
159
- id={uniqueId}
157
+ id={id}
160
158
  className={classes([
161
159
  'dropin-picker__select',
162
160
  `dropin-picker__select--${variant}`,
@@ -14,7 +14,6 @@ import {
14
14
  useEffect,
15
15
  useRef,
16
16
  useCallback,
17
- useMemo,
18
17
  } from 'preact/compat';
19
18
  import { classes } from '@adobe-commerce/elsie/lib';
20
19
  import '@adobe-commerce/elsie/components/TextSwatch/TextSwatch.css';
@@ -94,8 +93,6 @@ export const TextSwatch: FunctionComponent<TextSwatchProps> = ({
94
93
  }
95
94
  }, [label]);
96
95
 
97
- const uniqueId = useMemo(() => id ?? `${name}_${id}_${Math.random().toString(36)}`, [name, id]);
98
-
99
96
  return (
100
97
  <div
101
98
  className="dropin-text-swatch__container"
@@ -104,7 +101,7 @@ export const TextSwatch: FunctionComponent<TextSwatchProps> = ({
104
101
  <input
105
102
  type={multi ? 'checkbox' : 'radio'}
106
103
  name={name}
107
- id={uniqueId}
104
+ id={id}
108
105
  value={value}
109
106
  aria-label={handleAriaLabel()}
110
107
  checked={selected}
@@ -119,7 +116,7 @@ export const TextSwatch: FunctionComponent<TextSwatchProps> = ({
119
116
  ])}
120
117
  />
121
118
  <label
122
- htmlFor={uniqueId}
119
+ htmlFor={id}
123
120
  ref={spanRef}
124
121
  className={classes([
125
122
  'dropin-text-swatch__label',
@@ -48,3 +48,4 @@ 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
50
  export * from '@adobe-commerce/elsie/components/ProductItemCard';
51
+ export * from '@adobe-commerce/elsie/components/InputFile';
@@ -121,21 +121,5 @@ button.addEventListener('click', () => {
121
121
  });
122
122
  ```
123
123
 
124
- ### Unmounting components without instance access
125
-
126
- The `Render.unmount` static method provides a way to unmount components from the DOM when you don't have direct access to the component instance.
127
- This is particularly useful in scenarios where components are rendered inside modals, dialogs, or other temporary containers that need to be cleaned up.
128
-
129
- #### Example
130
-
131
- ```js
132
- // Close the dialog
133
- dialog.close();
134
-
135
- // Unmount any dropin containers rendered in the modal
136
- dialog.querySelectorAll('[data-dropin-container]').forEach(Render.unmount);
137
- ```
138
-
139
- This approach ensures that all dropin components are properly cleaned up when their container elements are removed from the DOM, preventing memory leaks and maintaining application performance.
140
124
  </Unstyled>
141
125
 
@@ -40,7 +40,7 @@ function resetConfig() {
40
40
  * @param {Object} [configObj=config] - The config object.
41
41
  * @returns {string} - The root path.
42
42
  */
43
- function getRootPath(configObj: Config | null = config): string {
43
+ function getRootPath(configObj: Config | null = config, options: { match?: (key: string) => boolean }): string {
44
44
  if (!configObj) {
45
45
  console.warn('No config found. Please call initializeConfig() first.');
46
46
  return '/';
@@ -56,7 +56,7 @@ function getRootPath(configObj: Config | null = config): string {
56
56
  .find(
57
57
  (key) =>
58
58
  window.location.pathname === key ||
59
- window.location.pathname.startsWith(key)
59
+ (options?.match?.(key) ?? window.location.pathname.startsWith(key))
60
60
  );
61
61
 
62
62
  return value ?? '/';
@@ -126,11 +126,14 @@ function applyConfigOverrides(
126
126
 
127
127
  /**
128
128
  * Initializes the configuration system.
129
+ * @param {Object} configObj - The config object.
130
+ * @param {Object} [options] - The options object.
131
+ * @param {Function} [options.match] - The function to match the path to the config.
129
132
  * @returns {Object} The initialized root configuration
130
133
  */
131
- function initializeConfig(configObj: Config): ConfigRoot {
134
+ function initializeConfig(configObj: Config, options?: { match?: (key: string) => boolean }): ConfigRoot {
132
135
  config = configObj;
133
- rootPath = getRootPath(config);
136
+ rootPath = getRootPath(config, { match: options?.match });
134
137
  rootConfig = applyConfigOverrides(config, rootPath);
135
138
  return rootConfig;
136
139
  }
@@ -71,21 +71,17 @@ export class Render {
71
71
  rootElement.innerHTML = '';
72
72
 
73
73
  // clone the root element to initialize rendering on the background
74
- const root = document.createElement('div');
74
+ const tmp = document.createElement('div');
75
75
 
76
76
  // apply base design tokens and global styles to the root element
77
77
  rootElement.classList.add('dropin-design');
78
- rootElement.setAttribute('data-dropin-container', Component.name);
79
78
 
80
- // store the virtual root element
81
- (rootElement as any).__rootElement = root;
82
-
83
- render(<Root next={state} />, root);
79
+ render(<Root next={state} />, tmp);
84
80
 
85
81
  // API object to control the rendered component
86
82
  const API: RenderAPI = {
87
83
  remove: () => {
88
- render(null, root);
84
+ render(null, tmp);
89
85
  },
90
86
  setProps: (cb: (prev: T) => T) => {
91
87
  const next = cb(state.peek());
@@ -101,7 +97,7 @@ export class Render {
101
97
  rootElement.classList.add('dropin-design');
102
98
 
103
99
  // append the rendered component to the DOM only when all slots are resolved
104
- rootElement.appendChild(root.firstChild ?? root);
100
+ rootElement.appendChild(tmp.firstChild ?? tmp);
105
101
 
106
102
  return resolve(API);
107
103
  }
@@ -110,24 +106,14 @@ export class Render {
110
106
  };
111
107
  }
112
108
 
113
- /**
114
- * Unmounts a container from a root element.
115
- * @param rootElement - The root element to unmount the container from.
116
- */
117
- static unmount(rootElement: HTMLElement) {
118
- const root = (rootElement as any)?.__rootElement;
119
- if (!root) throw new Error('Root element is not defined');
120
- render(null, root);
121
- }
122
-
123
109
  /**
124
110
  * UnRenders a component from a root element.
125
111
  * @param rootElement - The root element to unmount the component from.
126
- * @deprecated Use `remove` method from the returned object of the `mount` method instead or `unmount` method from the `Render` class.
112
+ * @deprecated Use `remove` method from the returned object of the `mount` method instead.
127
113
  */
128
114
  unmount(rootElement: HTMLElement) {
129
115
  if (!rootElement) throw new Error('Root element is not defined');
130
- rootElement.firstChild?.remove();
116
+ rootElement.firstChild?.remove();
131
117
  }
132
118
 
133
119
  /**
@@ -149,4 +135,4 @@ export class Render {
149
135
  { ...options }
150
136
  );
151
137
  }
152
- }
138
+ }