@adobe-commerce/elsie 1.4.1-alpha009 → 1.4.1-alpha101

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,24 +77,20 @@ 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,
80
81
  demandOption: true,
81
82
  })
82
- .option('excluded', {
83
- alias: 'x',
84
- describe: 'Paths to exclude from validation',
85
- type: 'array',
86
- demandOption: false,
87
- })
88
83
  .option('endpoints', {
89
84
  alias: 'e',
90
85
  describe: 'Path to GraphQL endpoints',
91
86
  type: 'array',
87
+ string: true,
92
88
  demandOption: true,
93
89
  });
94
90
  },
95
91
  async (argv) => {
96
- const { source, excluded, endpoints } = argv;
97
- await validate(source, endpoints, excluded);
92
+ const { source, endpoints } = argv;
93
+ await validate(source, endpoints);
98
94
  },
99
95
  )
100
96
  .demandCommand(1, 1, 'choose a command: types, mocks or validate');
@@ -5,20 +5,17 @@ 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, excludedPaths = [], collected = []) {
9
- if (excludedPaths.includes(dir)) return collected;
10
-
8
+ async function walk(dir, collected = []) {
11
9
  const dirents = await fsPromises.readdir(dir, { withFileTypes: true });
12
10
 
13
11
  for (const d of dirents) {
14
12
  const full = path.resolve(dir, d.name);
15
13
 
16
14
  if (d.isDirectory()) {
17
- if (excludedPaths.includes(full)) continue;
15
+ // skip node_modules and “hidden” folders such as .git
18
16
  if (d.name === 'node_modules' || d.name.startsWith('.')) continue;
19
- await walk(full, excludedPaths, collected);
17
+ await walk(full, collected);
20
18
  } else if (/\.(c?m?js|ts|tsx)$/.test(d.name)) {
21
- if (excludedPaths.includes(full)) continue;
22
19
  collected.push(full);
23
20
  }
24
21
  }
@@ -100,10 +97,10 @@ async function validateGqlOperations(endpoint, operation) {
100
97
  }
101
98
  }
102
99
 
103
- async function getAllOperations(directories, excludedPaths = []) {
100
+ async function getAllOperations(directories) {
104
101
  let fullContent = '';
105
102
  for (const directory of directories) {
106
- const files = await walk(path.resolve(directory), excludedPaths.map(p => path.resolve(p)));
103
+ const files = await walk(path.resolve(directory));
107
104
  for (const f of files) {
108
105
  const code = await fsPromises.readFile(f, 'utf8');
109
106
 
@@ -123,9 +120,11 @@ async function getAllOperations(directories, excludedPaths = []) {
123
120
  return fullContent;
124
121
  }
125
122
 
126
- module.exports = async function main(sources, endpoints, excluded) {
123
+
124
+
125
+ module.exports = async function main(sources, endpoints) {
127
126
  for (const endpoint of endpoints) {
128
- const operations = await getAllOperations(sources, excluded);
127
+ const operations = await getAllOperations(sources);
129
128
  if (!operations) {
130
129
  console.error('No GraphQL operations found in the specified directories.');
131
130
  process.exitCode = 0;
@@ -133,4 +132,4 @@ module.exports = async function main(sources, endpoints, excluded) {
133
132
  }
134
133
  await validateGqlOperations(endpoint, operations);
135
134
  }
136
- }
135
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.4.1-alpha009",
3
+ "version": "1.4.1-alpha101",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -26,7 +26,7 @@
26
26
  "cleanup": "rimraf dist"
27
27
  },
28
28
  "devDependencies": {
29
- "@adobe-commerce/event-bus": "~1.0.0",
29
+ "@adobe-commerce/event-bus": "~1.0.1",
30
30
  "@adobe-commerce/fetch-graphql": "~1.1.0",
31
31
  "@adobe-commerce/recaptcha": "~1.0.1",
32
32
  "@adobe-commerce/storefront-design": "~1.0.0",
@@ -36,6 +36,7 @@ export interface CartItemProps
36
36
  totalExcludingTax?: VNode;
37
37
  sku?: VNode;
38
38
  quantity?: number;
39
+ quantityContent?: VNode;
39
40
  description?: VNode;
40
41
  attributes?: VNode;
41
42
  footer?: VNode;
@@ -45,6 +46,7 @@ export interface CartItemProps
45
46
  discount?: VNode;
46
47
  savings?: VNode;
47
48
  actions?: VNode;
49
+ removeContent?: VNode;
48
50
  loading?: boolean;
49
51
  updating?: boolean;
50
52
  onRemove?: () => void;
@@ -71,7 +73,9 @@ export const CartItem: FunctionComponent<CartItemProps> = ({
71
73
  discount,
72
74
  savings,
73
75
  actions,
76
+ removeContent,
74
77
  quantity,
78
+ quantityContent,
75
79
  description,
76
80
  attributes,
77
81
  footer,
@@ -304,18 +308,20 @@ export const CartItem: FunctionComponent<CartItemProps> = ({
304
308
  ['dropin-cart-item__quantity--edit', !!onQuantity],
305
309
  ])}
306
310
  >
307
- {onQuantity
308
- ? quantityComponent
309
- : quantity && (
310
- <span
311
- className={classes(['dropin-cart-item__quantity__value'])}
312
- >
313
- {labels.quantity}:{' '}
314
- <strong className="dropin-cart-item__quantity__number">
315
- {Number(quantity).toLocaleString(locale)}
316
- </strong>
317
- </span>
318
- )}
311
+ {quantityContent ? (
312
+ <VComponent node={quantityContent} />
313
+ ) : onQuantity ? (
314
+ quantityComponent
315
+ ) : (
316
+ quantity && (
317
+ <span className={classes(['dropin-cart-item__quantity__value'])}>
318
+ {labels.quantity}:{' '}
319
+ <strong className="dropin-cart-item__quantity__number">
320
+ {Number(quantity).toLocaleString(locale)}
321
+ </strong>
322
+ </span>
323
+ )
324
+ )}
319
325
 
320
326
  {/* Warning */}
321
327
  {warning && (
@@ -432,12 +438,17 @@ export const CartItem: FunctionComponent<CartItemProps> = ({
432
438
 
433
439
  {/* Footer */}
434
440
  {footer && (
435
- <VComponent node={footer} className={classes(['dropin-cart-item__footer'])} />
441
+ <VComponent
442
+ node={footer}
443
+ className={classes(['dropin-cart-item__footer'])}
444
+ />
436
445
  )}
437
446
  </div>
438
447
 
439
448
  {/* Remove Item */}
440
- {onRemove && (
449
+ {removeContent ? (
450
+ <VComponent node={removeContent} />
451
+ ) : onRemove ? (
441
452
  <Button
442
453
  data-testid="cart-item-remove-button"
443
454
  className={classes(['dropin-cart-item__remove'])}
@@ -459,7 +470,7 @@ export const CartItem: FunctionComponent<CartItemProps> = ({
459
470
  }
460
471
  disabled={updating}
461
472
  />
462
- )}
473
+ ) : null}
463
474
  </div>
464
475
  );
465
476
  };
@@ -8,12 +8,13 @@
8
8
  *******************************************************************/
9
9
 
10
10
  import { Icon } from '@adobe-commerce/elsie/components';
11
- import '@adobe-commerce/elsie/components/Picker/Picker.css';
12
11
  import { ChevronDown } from '@adobe-commerce/elsie/icons';
13
12
  import { classes } from '@adobe-commerce/elsie/lib';
14
13
  import { FunctionComponent, VNode } from 'preact';
15
14
  import { HTMLAttributes, useEffect, useState } from 'preact/compat';
16
15
 
16
+ import '@adobe-commerce/elsie/components/Picker/Picker.css';
17
+
17
18
  type PickerValue = string | null;
18
19
 
19
20
  export interface PickerOption {
@@ -68,9 +69,10 @@ export const Picker: FunctionComponent<PickerProps> = ({
68
69
  defaultOption,
69
70
  icon,
70
71
  className,
72
+ id,
71
73
  ...props
72
74
  }) => {
73
- const id = props?.id || name || `dropin-picker-${Math.random().toString(36)}`;
75
+ const uniqueId = id ?? name ?? `dropin-picker-${Math.random().toString(36)}`;
74
76
  const isRequired = !!props?.required;
75
77
 
76
78
  // find the first option that is not disabled
@@ -154,7 +156,7 @@ export const Picker: FunctionComponent<PickerProps> = ({
154
156
  )}
155
157
 
156
158
  <select
157
- id={id}
159
+ id={uniqueId}
158
160
  className={classes([
159
161
  'dropin-picker__select',
160
162
  `dropin-picker__select--${variant}`,
@@ -9,23 +9,10 @@
9
9
 
10
10
  /* https://cssguidelin.es/#bem-like-naming */
11
11
 
12
- .dropin-product-item-card__skeleton {
13
- gap: var(--spacing-small);
14
- }
15
-
16
12
  .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
13
  width: 100%;
26
- height: auto;
14
+ height: 370px;
27
15
  }
28
- */
29
16
 
30
17
  /* Medium (portrait tablets and large phones, 768px and up) */
31
18
  /* @media only screen and (min-width: 768px) { } */
@@ -19,10 +19,11 @@ export const ProductItemCardSkeleton: FunctionComponent = () => {
19
19
  fullWidth={true}
20
20
  className="dropin-product-item-card__skeleton__image"
21
21
  />
22
- <div className="dropin-product-item-card__content dropin-product-item-card__skeleton__content">
22
+ </Skeleton>
23
+ <Skeleton className="dropin-product-item-card__content dropin-product-item-card__skeleton__content">
23
24
  <SkeletonRow
24
25
  fullWidth={true}
25
- size="large"
26
+ size="xsmall"
26
27
  className="dropin-product-item-card__skeleton__item"
27
28
  />
28
29
  <SkeletonRow
@@ -32,11 +33,10 @@ export const ProductItemCardSkeleton: FunctionComponent = () => {
32
33
  />
33
34
  <SkeletonRow
34
35
  fullWidth={true}
35
- size="small"
36
+ size="xsmall"
36
37
  className="dropin-product-item-card__skeleton__item"
37
38
  />
38
- </div>{' '}
39
- </Skeleton>
39
+ </Skeleton>
40
40
  </div>
41
41
  );
42
42
  };
@@ -14,6 +14,7 @@ import {
14
14
  useEffect,
15
15
  useRef,
16
16
  useCallback,
17
+ useMemo,
17
18
  } from 'preact/compat';
18
19
  import { classes } from '@adobe-commerce/elsie/lib';
19
20
  import '@adobe-commerce/elsie/components/TextSwatch/TextSwatch.css';
@@ -93,6 +94,8 @@ export const TextSwatch: FunctionComponent<TextSwatchProps> = ({
93
94
  }
94
95
  }, [label]);
95
96
 
97
+ const uniqueId = useMemo(() => id ?? `${name}_${id}_${Math.random().toString(36)}`, [name, id]);
98
+
96
99
  return (
97
100
  <div
98
101
  className="dropin-text-swatch__container"
@@ -101,7 +104,7 @@ export const TextSwatch: FunctionComponent<TextSwatchProps> = ({
101
104
  <input
102
105
  type={multi ? 'checkbox' : 'radio'}
103
106
  name={name}
104
- id={id}
107
+ id={uniqueId}
105
108
  value={value}
106
109
  aria-label={handleAriaLabel()}
107
110
  checked={selected}
@@ -116,7 +119,7 @@ export const TextSwatch: FunctionComponent<TextSwatchProps> = ({
116
119
  ])}
117
120
  />
118
121
  <label
119
- htmlFor={id}
122
+ htmlFor={uniqueId}
120
123
  ref={spanRef}
121
124
  className={classes([
122
125
  'dropin-text-swatch__label',
@@ -5,48 +5,265 @@ import { Meta, Unstyled } from '@storybook/blocks';
5
5
 
6
6
  # Event Bus
7
7
 
8
- ## Usage
8
+ The Event Bus provides a communication system for different parts of your application to exchange messages and stay synchronized. It enables event-driven architecture for drop-ins, allowing Containers to react to changes from other Containers and communicate data changes to the storefront.
9
+
10
+ ## Import
11
+
12
+ From drop-in project using the SDK
9
13
 
10
14
  ```ts
11
- // from drop-in project (SDK)
12
15
  import { events } from '@adobe-commerce/elsie/lib';
16
+ ```
17
+
13
18
 
14
- // from host site
19
+ From integration project (storefront)
20
+
21
+ ```js
15
22
  import { events } from '@dropins/tools/event-bus.js';
16
23
  ```
17
24
 
18
- ## Methods
25
+ ## Core Methods
26
+
27
+ ### Subscribe to Events
28
+
29
+ Subscribe to events and receive notifications when they occur.
30
+
31
+ ```ts
32
+ const eventListener = events.on('<event>', (payload) => {
33
+ // Handle the event payload
34
+ console.log('Event received:', payload);
35
+ });
36
+
37
+ // Stop listening to the event
38
+ eventListener.off();
39
+ ```
19
40
 
20
- ### Listener
41
+ **Example:**
21
42
  ```ts
22
- const onEvent = events.on('<event>', (payload) => {
23
- //...handle payload
43
+ // Listen for cart updates
44
+ const cartListener = events.on('cart/data', (cartData) => {
45
+ if (cartData) {
46
+ console.log(`Cart has ${cartData.totalQuantity} items`);
47
+ updateCartUI(cartData);
48
+ } else {
49
+ console.log('Cart is empty');
50
+ showEmptyCart();
51
+ }
24
52
  });
25
53
 
26
- // Stop listening to event
27
- onEvent.off();
54
+ // Later, when you want to stop listening
55
+ cartListener.off();
56
+ ```
57
+
58
+ ### Emit Events
59
+
60
+ Broadcast events to all listeners across your application.
61
+
62
+ ```ts
63
+ events.emit('<event>', payload);
64
+ ```
65
+
66
+ **Examples:**
67
+ ```ts
68
+ // Emit cart data
69
+ const cartData = {
70
+ id: 'cart-123',
71
+ totalQuantity: 2,
72
+ items: [
73
+ { uid: 'item-1', quantity: 1, sku: 'PROD-001', name: 'Product Name' }
74
+ ]
75
+ };
76
+
77
+ events.emit('cart/data', cartData);
28
78
  ```
29
79
 
30
- ### Emit
80
+ ### Get Last Event Payload
81
+
82
+ Retrieve the most recent payload for a specific event.
83
+
84
+ ```ts
85
+ const lastPayload = events.lastPayload('<event>');
86
+ ```
31
87
 
88
+ **Example:**
32
89
  ```ts
33
- events.emit('<event>', <payload>);
90
+ // Get the current cart state without waiting for an event
91
+ const currentCart = events.lastPayload('cart/data');
92
+
93
+ if (currentCart) {
94
+ console.log('Current cart total:', currentCart.totalQuantity);
95
+ }
34
96
  ```
35
97
 
36
- ### Logging
98
+ ### Enable Debug Logging
99
+
100
+ Turn on console logging to debug event flow.
37
101
 
38
102
  ```ts
39
- // Enable logging
103
+ // Enable logging to see all events in console
40
104
  events.enableLogger(true);
105
+ ```
41
106
 
42
- // Disable logging
43
- events.enableLogger(false);
107
+ ## Advanced Features
108
+
109
+ ### Eager Loading
110
+
111
+ Execute the event handler immediately with the last known payload when subscribing. This is useful for getting the current state without waiting for the next event.
112
+
113
+ ```ts
114
+ // Handler will execute immediately if there's a previous payload
115
+ const listener = events.on('cart/data', (cartData) => {
116
+ console.log('Cart data received:', cartData);
117
+ }, { eager: true });
44
118
  ```
45
119
 
46
- ### Get Latest Payload
120
+ **Use Cases:**
121
+ - Initialize UI components with current state
122
+ - Avoid waiting for the first event emission
123
+ - Ensure components have the latest data on mount
124
+
125
+ ### Event Scoping
126
+
127
+ Create namespaced events to avoid conflicts between different parts of your application.
128
+
129
+ ```ts
130
+ // Subscribe to a scoped event
131
+ const scopedListener = events.on('data/update', (data) => {
132
+ console.log('Scoped data received:', data);
133
+ }, { scope: 'feature-a' });
134
+
135
+ // Emit a scoped event
136
+ events.emit('data/update', payload, { scope: 'feature-a' });
137
+
138
+ // Get last payload for a scoped event
139
+ const lastScopedData = events.lastPayload('data/update', { scope: 'feature-a' });
140
+ ```
141
+
142
+ **Scoped Event Names:**
143
+ When using scopes, the actual event name becomes `scope/event`. For example:
144
+ - `'feature-a/data/update'` instead of `'data/update'`
145
+ - `'module-b/user/action'` instead of `'user/action'`
146
+
147
+ **Use Cases:**
148
+ - Separate different features or modules
149
+ - Different contexts within the same application
150
+ - Component-specific event handling
151
+
152
+ ### Combining Options
153
+
154
+ Use both eager loading and scoping together for powerful event handling.
47
155
 
48
156
  ```ts
49
- events.lastPayload('<event>'): EventPayload | undefined;
157
+ // Subscribe to a scoped event with eager loading
158
+ const listener = events.on('locale', (locale) => {
159
+ console.log('Current locale:', locale);
160
+ }, {
161
+ eager: true,
162
+ scope: 'user-preferences'
163
+ });
50
164
  ```
51
165
 
166
+ ## Event-Driven Drop-ins
167
+
168
+ The Event Bus enables drop-ins to be truly event-driven, allowing for loose coupling between components and seamless communication across the application.
169
+
170
+ ### Container-to-Container Communication
171
+
172
+ Containers can react to changes from other Containers, enabling complex interactions without direct dependencies.
173
+
174
+ ```ts
175
+ // Product Container: Emits when a product is added to cart
176
+ function ProductContainer() {
177
+ const handleAddToCart = (product) => {
178
+ // Add to cart logic...
179
+
180
+ // Notify other containers about the cart change
181
+ events.emit('cart/data', updatedCartData);
182
+ };
183
+
184
+ return (
185
+ <button onClick={() => handleAddToCart(product)}>
186
+ Add to Cart
187
+ </button>
188
+ );
189
+ }
190
+
191
+ // Cart Container: Reacts to cart changes from any source
192
+ function CartContainer() {
193
+ useEffect(() => {
194
+ const cartListener = events.on('cart/data', (cartData) => {
195
+ updateCartDisplay(cartData);
196
+ updateCartBadge(cartData.totalQuantity);
197
+ }, { eager: true });
198
+
199
+ return () => cartListener.off();
200
+ }, []);
201
+
202
+ return <CartDisplay />;
203
+ }
204
+
205
+ // Mini Cart Container: Also reacts to the same cart changes
206
+ function MiniCartContainer() {
207
+ useEffect(() => {
208
+ const cartListener = events.on('cart/data', (cartData) => {
209
+ updateMiniCart(cartData);
210
+ }, { eager: true });
211
+
212
+ return () => cartListener.off();
213
+ }, []);
214
+
215
+ return <MiniCart />;
216
+ }
217
+ ```
218
+
219
+ ### Storefront Communication
220
+
221
+ Drop-ins can communicate data changes to the storefront, enabling seamless integration with the host application.
222
+
223
+ ```ts
224
+ // Authentication Container: Notifies storefront of login/logout
225
+ function AuthContainer() {
226
+ const handleLogin = (userData) => {
227
+ // Login logic...
228
+
229
+ // Notify storefront of authentication change
230
+ events.emit('authenticated', true);
231
+ };
232
+
233
+ const handleLogout = () => {
234
+ // Logout logic...
235
+
236
+ // Notify storefront of authentication change
237
+ events.emit('authenticated', false);
238
+ };
239
+
240
+ return <AuthForm onLogin={handleLogin} onLogout={handleLogout} />;
241
+ }
242
+
243
+ // Storefront can listen for authentication changes
244
+ // This would be in the host application
245
+ const authListener = events.on('authenticated', (isAuthenticated) => {
246
+ if (isAuthenticated) {
247
+ showUserMenu();
248
+ enableCheckout();
249
+ } else {
250
+ hideUserMenu();
251
+ disableCheckout();
252
+ }
253
+ }, { eager: true });
254
+ ```
255
+
256
+
257
+
258
+ ## Best Practices
259
+
260
+ 1. **Always unsubscribe** from events when components unmount to prevent memory leaks
261
+ 2. **Use scopes** to organize events by feature or component
262
+ 3. **Enable eager loading** when you need immediate access to current state
263
+ 4. **Use descriptive event names** that clearly indicate what data they contain
264
+ 5. **Handle null/undefined payloads** gracefully in your event handlers
265
+ 6. **Enable logging during development** to debug event flow
266
+ 7. **Keep event payloads lightweight** to avoid performance issues
267
+ 8. **Document your event contracts** so other developers know what to expect
268
+
52
269
  </Unstyled>