@adobe-commerce/elsie 1.6.0-alpha2 → 1.6.0-alpha23

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.6.0-alpha2",
3
+ "version": "1.6.0-alpha23",
4
4
  "license": "SEE LICENSE IN LICENSE.md",
5
5
  "description": "Domain Package SDK",
6
6
  "engines": {
@@ -2,14 +2,14 @@
2
2
  * Copyright 2024 Adobe
3
3
  * All Rights Reserved.
4
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.
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
8
  *******************************************************************/
9
9
 
10
10
  import { FunctionComponent } from 'preact';
11
11
  import { HTMLAttributes, useMemo } from 'preact/compat';
12
- import { classes, getGlobalLocale } from '@adobe-commerce/elsie/lib';
12
+ import { classes, getPriceFormatter } from '@adobe-commerce/elsie/lib';
13
13
  import '@adobe-commerce/elsie/components/Price/Price.css';
14
14
 
15
15
  export interface PriceProps
@@ -39,43 +39,10 @@ export const Price: FunctionComponent<PriceProps> = ({
39
39
  size = 'small',
40
40
  ...props
41
41
  }) => {
42
- // Determine the locale to use: prop locale > global locale > browser locale
43
- const effectiveLocale = useMemo(() => {
44
- if (locale) {
45
- return locale;
46
- }
47
- const globalLocale = getGlobalLocale();
48
- if (globalLocale) {
49
- return globalLocale;
50
- }
51
- // Fallback to browser locale or default
52
- return process.env.LOCALE && process.env.LOCALE !== 'undefined' ? process.env.LOCALE : 'en-US';
53
- }, [locale]);
54
-
55
- const formatter = useMemo(
56
- () => {
57
- const params: Intl.NumberFormatOptions = {
58
- style: 'currency',
59
- currency: currency || 'USD',
60
- // These options are needed to round to whole numbers if that's what you want.
61
- minimumFractionDigits: 2, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
62
- maximumFractionDigits: 2, // (causes 2500.99 to be printed as $2,501)
63
- ...formatOptions,
64
- }
65
- try {
66
- return new Intl.NumberFormat(effectiveLocale, params);
67
- } catch (error) {
68
- console.error(`Error creating Intl.NumberFormat instance for locale ${effectiveLocale}. Falling back to en-US.`, error);
69
- return new Intl.NumberFormat('en-US', params);
70
- }
71
- },
72
- [effectiveLocale, currency, formatOptions]
73
- );
74
-
75
- const formattedAmount = useMemo(
76
- () => formatter.format(amount),
77
- [amount, formatter]
78
- );
42
+ const formattedAmount = useMemo(() => {
43
+ const formatter = getPriceFormatter({ currency, locale, formatOptions });
44
+ return formatter.format(amount);
45
+ }, [amount, currency, locale, formatOptions]);
79
46
 
80
47
  return (
81
48
  <span
@@ -17,6 +17,7 @@
17
17
  }
18
18
 
19
19
  .dropin-table__table {
20
+ border: var(--shape-border-width-1) solid var(--color-neutral-400);
20
21
  border-collapse: collapse;
21
22
  width: 100%;
22
23
  }
@@ -34,9 +35,8 @@
34
35
  letter-spacing: var(--type-body-1-strong-letter-spacing);
35
36
  }
36
37
 
37
- .dropin-table__header__cell,
38
- .dropin-table__body__cell {
39
- padding: var(--spacing-xsmall);
38
+ .dropin-table__header__cell {
39
+ color: var(--color-neutral-800);
40
40
  text-align: left;
41
41
  white-space: nowrap;
42
42
  }
@@ -46,15 +46,26 @@
46
46
  }
47
47
 
48
48
  .dropin-table__header__row {
49
- border-bottom: 2px solid var(--color-neutral-400);
49
+ border-bottom: var(--shape-border-width-1) solid var(--color-neutral-400);
50
+ }
51
+
52
+ .dropin-table__header__row th {
53
+ padding: var(--spacing-small) var(--spacing-medium);
54
+ }
55
+
56
+ .dropin-table__body__row:nth-child(even) {
57
+ background-color: var(--color-neutral-100);
58
+ }
59
+
60
+ .dropin-table__body__row.dropin-table__body__row--expanded {
61
+ background-color: var(--color-neutral-200);
50
62
  }
51
63
 
52
- .dropin-table__body__row {
53
- border-bottom: 1px solid var(--color-neutral-400);
64
+ .dropin-table__body__cell {
65
+ padding: var(--spacing-small) var(--spacing-medium);
54
66
  }
55
67
 
56
68
  .dropin-table__header__sort-button {
57
- margin-left: var(--spacing-xsmall);
58
69
  vertical-align: middle;
59
70
  }
60
71
 
@@ -67,10 +78,29 @@
67
78
  }
68
79
 
69
80
  .dropin-table__row-details__cell {
70
- padding: var(--spacing-small);
71
- background-color: var(--color-neutral-100);
72
- border-top: 1px solid var(--color-neutral-300);
73
- border-bottom: 1px solid var(--color-neutral-400);
81
+ padding: var(--spacing-small) var(--spacing-medium);
82
+ background-color: var(--color-neutral-200);
83
+ }
84
+
85
+ .dropin-table__row-details--expanded .dropin-table__row-details__cell > *:last-child {
86
+ margin: 0;
87
+ }
88
+
89
+ .dropin-table__row-details__cell h1,
90
+ .dropin-table__row-details__cell h2,
91
+ .dropin-table__row-details__cell h3,
92
+ .dropin-table__row-details__cell h4,
93
+ .dropin-table__row-details__cell h5,
94
+ .dropin-table__row-details__cell h6 {
95
+ font: var(--type-body-1-strong-font);
96
+ letter-spacing: var(--type-body-1-strong-letter-spacing);
97
+ margin: 0;
98
+ }
99
+
100
+ .dropin-table__row-details__cell p,
101
+ .dropin-table__row-details__cell span {
102
+ font: var(--type-body-1-default-font);
103
+ letter-spacing: var(--type-body-1-default-letter-spacing);
74
104
  }
75
105
 
76
106
  /* Container query for mobile layout */
@@ -80,20 +110,63 @@
80
110
  display: none;
81
111
  }
82
112
 
113
+ .dropin-table--mobile-layout-stacked .dropin-table__body__row {
114
+ padding: var(--spacing-medium);
115
+ display: block;
116
+ }
117
+
83
118
  .dropin-table--mobile-layout-stacked .dropin-table__body__cell {
84
119
  display: block;
120
+ font: var(--type-body-2-default-font);
121
+ letter-spacing: var(--type-body-2-default-letter-spacing);
122
+ padding: var(--spacing-xsmall) 0;
123
+ }
124
+
125
+ .dropin-table--mobile-layout-stacked .dropin-table__body__cell:first-child {
126
+ padding-top: 0;
127
+ }
128
+
129
+ .dropin-table--mobile-layout-stacked .dropin-table__body__cell:last-child {
130
+ padding-bottom: 0;
85
131
  }
86
132
 
87
133
  .dropin-table--mobile-layout-stacked .dropin-table__body__cell::before {
88
134
  content: attr(data-label);
89
- font-weight: bold;
135
+ font: var(--type-body-2-strong-font);
136
+ letter-spacing: var(--type-body-2-strong-letter-spacing);
90
137
  display: block;
91
138
  margin-bottom: var(--spacing-xxsmall);
92
139
  }
93
140
 
94
141
  .dropin-table--mobile-layout-stacked .dropin-table__row-details__cell {
95
142
  display: block;
96
- padding: var(--spacing-small);
143
+ padding: var(--spacing-medium);
144
+ }
145
+
146
+ .dropin-table--mobile-layout-stacked .dropin-table__row-details__cell > *:first-child {
147
+ border-top: var(--shape-border-width-1) solid var(--color-neutral-400);
148
+ padding-top: var(--spacing-medium);
149
+ margin-top: calc(var(--spacing-medium) * -1);
150
+ }
151
+
152
+ .dropin-table--mobile-layout-stacked .dropin-table__row-details--expanded .dropin-table__row-details__cell > *:last-child {
153
+ margin: 0;
154
+ }
155
+
156
+ .dropin-table--mobile-layout-stacked .dropin-table__row-details__cell h1,
157
+ .dropin-table--mobile-layout-stacked .dropin-table__row-details__cell h2,
158
+ .dropin-table--mobile-layout-stacked .dropin-table__row-details__cell h3,
159
+ .dropin-table--mobile-layout-stacked .dropin-table__row-details__cell h4,
160
+ .dropin-table--mobile-layout-stacked .dropin-table__row-details__cell h5,
161
+ .dropin-table--mobile-layout-stacked .dropin-table__row-details__cell h6 {
162
+ font: var(--type-body-2-strong-font);
163
+ letter-spacing: var(--type-body-2-strong-letter-spacing);
164
+ }
165
+
166
+ .dropin-table--mobile-layout-stacked .dropin-table__row-details__cell p,
167
+ .dropin-table--mobile-layout-stacked .dropin-table__row-details__cell span {
168
+ font: var(--type-body-2-default-font);
169
+ letter-spacing: var(--type-body-2-default-letter-spacing);
97
170
  }
98
171
  }
99
172
 
@@ -11,6 +11,10 @@
11
11
  import type { Meta, StoryObj } from '@storybook/preact';
12
12
  import { useState } from 'preact/hooks';
13
13
  import { Table as TableComponent, TableProps } from '@adobe-commerce/elsie/components/Table';
14
+ import { ActionButton } from '@adobe-commerce/elsie/components/ActionButton';
15
+ import { ActionButtonGroup } from '@adobe-commerce/elsie/components/ActionButtonGroup';
16
+ import { Pagination } from '@adobe-commerce/elsie/components/Pagination';
17
+ import { Picker, type PickerOption } from '@adobe-commerce/elsie/components/Picker';
14
18
 
15
19
  /**
16
20
  * Use the `Table` component to render data in a structured table.
@@ -175,10 +179,10 @@ export const Table: Story = {
175
179
  { key: 'actions', label: 'Actions' },
176
180
  ],
177
181
  rowData: [
178
- { name: 'John', email: 'john@example.com', age: 20, actions: <button>Edit</button> },
179
- { name: 'Jane', email: 'jane@example.com', age: 21, actions: <button>Edit</button> },
180
- { name: 'Jim', email: 'jim@example.com', age: 22, actions: <button>Edit</button> },
181
- { name: 'Jill', email: 'jill@example.com', age: 23, actions: <button>Edit</button> },
182
+ { name: 'John', email: 'john@example.com', age: 20, actions: <ActionButton>Edit</ActionButton> },
183
+ { name: 'Jane', email: 'jane@example.com', age: 21, actions: <ActionButton>Edit</ActionButton> },
184
+ { name: 'Jim', email: 'jim@example.com', age: 22, actions: <ActionButton>Edit</ActionButton> },
185
+ { name: 'Jill', email: 'jill@example.com', age: 23, actions: <ActionButton>Edit</ActionButton> },
182
186
  ],
183
187
  },
184
188
  };
@@ -212,11 +216,11 @@ export const AllSortable: Story = {
212
216
  { key: 'actions', label: 'Actions' },
213
217
  ],
214
218
  rowData: [
215
- { name: 'John', email: 'john@example.com', age: 20, actions: <button>Edit</button> },
216
- { name: 'Jane', email: 'jane@example.com', age: 21, actions: <button>Edit</button> },
217
- { name: 'Jim', email: 'jim@example.com', age: 22, actions: <button>Edit</button> },
218
- { name: 'Jill', email: 'jill@example.com', age: 23, actions: <button>Edit</button> },
219
- { name: 'Jack', email: 'jack@example.com', age: 24, actions: <button>Edit</button> },
219
+ { name: 'John', email: 'john@example.com', age: 20, actions: <ActionButton>Edit</ActionButton> },
220
+ { name: 'Jane', email: 'jane@example.com', age: 21, actions: <ActionButton>Edit</ActionButton> },
221
+ { name: 'Jim', email: 'jim@example.com', age: 22, actions: <ActionButton>Edit</ActionButton> },
222
+ { name: 'Jill', email: 'jill@example.com', age: 23, actions: <ActionButton>Edit</ActionButton> },
223
+ { name: 'Jack', email: 'jack@example.com', age: 24, actions: <ActionButton>Edit</ActionButton> },
220
224
  ],
221
225
  },
222
226
  };
@@ -240,7 +244,7 @@ export const AllSortable: Story = {
240
244
  * { key: 'actions', label: 'Actions' }
241
245
  * ]}
242
246
  * rowData={[
243
- * { id: 1, name: 'John Doe', email: 'john@company.com', phone: '+1-555-0123', department: 'Engineering', position: 'Senior Developer', salary: '$95,000', startDate: '2022-01-15', status: 'Active', actions: <button>Edit</button> }
247
+ * { id: 1, name: 'John Doe', email: 'john@company.com', phone: '+1-555-0123', department: 'Engineering', position: 'Senior Developer', salary: '$95,000', startDate: '2022-01-15', status: 'Active', actions: <ActionButton>Edit</ActionButton> }
244
248
  * ]}
245
249
  * />
246
250
  * ```
@@ -270,7 +274,7 @@ export const WideTable: Story = {
270
274
  salary: '$95,000',
271
275
  startDate: '2022-01-15',
272
276
  status: 'Active',
273
- actions: <button>Edit</button>
277
+ actions: <ActionButton>Edit</ActionButton>
274
278
  },
275
279
  {
276
280
  id: 2,
@@ -282,7 +286,7 @@ export const WideTable: Story = {
282
286
  salary: '$78,000',
283
287
  startDate: '2021-06-20',
284
288
  status: 'Active',
285
- actions: <button>Edit</button>
289
+ actions: <ActionButton>Edit</ActionButton>
286
290
  },
287
291
  {
288
292
  id: 3,
@@ -294,7 +298,7 @@ export const WideTable: Story = {
294
298
  salary: '$110,000',
295
299
  startDate: '2020-03-10',
296
300
  status: 'Active',
297
- actions: <button>Edit</button>
301
+ actions: <ActionButton>Edit</ActionButton>
298
302
  },
299
303
  {
300
304
  id: 4,
@@ -306,7 +310,7 @@ export const WideTable: Story = {
306
310
  salary: '$65,000',
307
311
  startDate: '2023-02-28',
308
312
  status: 'Pending',
309
- actions: <button>Edit</button>
313
+ actions: <ActionButton>Edit</ActionButton>
310
314
  },
311
315
  {
312
316
  id: 5,
@@ -318,7 +322,7 @@ export const WideTable: Story = {
318
322
  salary: '$72,000',
319
323
  startDate: '2022-09-12',
320
324
  status: 'Active',
321
- actions: <button>Edit</button>
325
+ actions: <ActionButton>Edit</ActionButton>
322
326
  },
323
327
  ],
324
328
  },
@@ -341,7 +345,7 @@ export const WideTable: Story = {
341
345
  * user: <div><strong>John Doe</strong><br/>john@example.com<br/>Senior Developer</div>,
342
346
  * description: <div>Lead developer for the<br/>e-commerce platform<br/>with 5+ years experience</div>,
343
347
  * status: <span>Active</span>,
344
- * actions: <div><button>Edit</button><br/><button>Delete</button><br/><button>View</button></div>
348
+ * actions: <div><ActionButton>Edit</ActionButton><br/><ActionButton>Delete</ActionButton><br/><ActionButton>View</ActionButton></div>
345
349
  * }
346
350
  * ]}
347
351
  * />
@@ -375,11 +379,11 @@ export const ComplexCells: Story = {
375
379
  <span>Active</span>
376
380
  ),
377
381
  actions: (
378
- <div>
379
- <button>Edit</button>
380
- <button>Delete</button>
381
- <button>View</button>
382
- </div>
382
+ <ActionButtonGroup>
383
+ <ActionButton value="edit">Edit</ActionButton>
384
+ <ActionButton value="delete">Delete</ActionButton>
385
+ <ActionButton value="view">View</ActionButton>
386
+ </ActionButtonGroup>
383
387
  ),
384
388
  },
385
389
  {
@@ -401,11 +405,11 @@ export const ComplexCells: Story = {
401
405
  <span>Pending</span>
402
406
  ),
403
407
  actions: (
404
- <div>
405
- <button>Edit</button>
406
- <button>Approve</button>
407
- <button>Reject</button>
408
- </div>
408
+ <ActionButtonGroup>
409
+ <ActionButton value="edit">Edit</ActionButton>
410
+ <ActionButton value="approve">Approve</ActionButton>
411
+ <ActionButton value="reject">Reject</ActionButton>
412
+ </ActionButtonGroup>
409
413
  ),
410
414
  },
411
415
  {
@@ -427,11 +431,11 @@ export const ComplexCells: Story = {
427
431
  <span>Inactive</span>
428
432
  ),
429
433
  actions: (
430
- <div>
431
- <button>Edit</button>
432
- <button>Activate</button>
433
- <button>Archive</button>
434
- </div>
434
+ <ActionButtonGroup>
435
+ <ActionButton value="edit">Edit</ActionButton>
436
+ <ActionButton value="activate">Activate</ActionButton>
437
+ <ActionButton value="archive">Archive</ActionButton>
438
+ </ActionButtonGroup>
435
439
  ),
436
440
  },
437
441
  ],
@@ -476,10 +480,10 @@ export const StackedMobileLayout: Story = {
476
480
  { key: 'actions', label: 'Actions' },
477
481
  ],
478
482
  rowData: [
479
- { name: 'John Doe', email: 'john.doe@example.com', age: 28, status: 'Active', actions: <button>Edit</button> },
480
- { name: 'Jane Smith', email: 'jane.smith@example.com', age: 32, status: 'Inactive', actions: <button>Edit</button> },
481
- { name: 'Bob Johnson', email: 'bob.johnson@example.com', age: 45, status: 'Active', actions: <button>Edit</button> },
482
- { name: 'Alice Brown', email: 'alice.brown@example.com', age: 29, status: 'Pending', actions: <button>Edit</button> },
483
+ { name: 'John Doe', email: 'john.doe@example.com', age: 28, status: 'Active', actions: <ActionButton>Edit</ActionButton> },
484
+ { name: 'Jane Smith', email: 'jane.smith@example.com', age: 32, status: 'Inactive', actions: <ActionButton>Edit</ActionButton> },
485
+ { name: 'Bob Johnson', email: 'bob.johnson@example.com', age: 45, status: 'Active', actions: <ActionButton>Edit</ActionButton> },
486
+ { name: 'Alice Brown', email: 'alice.brown@example.com', age: 29, status: 'Pending', actions: <ActionButton>Edit</ActionButton> },
483
487
  ],
484
488
  },
485
489
  };
@@ -520,7 +524,7 @@ export const StackedMobileLayout: Story = {
520
524
  * {
521
525
  * name: 'John',
522
526
  * email: 'john@example.com',
523
- * actions: <button onClick={() => toggleRow(0)}>Toggle Details</button>,
527
+ * actions: <ActionButton onClick={() => toggleRow(0)}>Toggle Details</ActionButton>,
524
528
  * _rowDetails: <div>Additional information...</div>
525
529
  * }
526
530
  * ]}
@@ -550,22 +554,22 @@ export const RowDetails: Story = {
550
554
  email: 'john.doe@company.com',
551
555
  status: 'Active',
552
556
  actions: (
553
- <button onClick={() => toggleRow(0)}>
557
+ <ActionButton onClick={() => toggleRow(0)}>
554
558
  {expandedRows.has(0) ? 'Hide' : 'Show'}
555
- </button>
559
+ </ActionButton>
556
560
  ),
557
561
  _rowDetails: (
558
- <div>
562
+ <>
559
563
  <h3>Employee Details</h3>
560
564
  <p><strong>Department:</strong> Engineering</p>
561
565
  <p><strong>Position:</strong> Senior Developer</p>
562
566
  <p><strong>Start Date:</strong> January 15, 2022</p>
563
567
  <p><strong>Notes:</strong> Excellent performance, leads the frontend team.</p>
564
568
  <div style={{ marginTop: '12px' }}>
565
- <button style={{ marginRight: '8px' }}>Update Details</button>
566
- <button>View Full Profile</button>
569
+ <ActionButton style={{ marginRight: '8px' }}>Update Details</ActionButton>
570
+ <ActionButton>View Full Profile</ActionButton>
567
571
  </div>
568
- </div>
572
+ </>
569
573
  )
570
574
  },
571
575
  {
@@ -573,22 +577,22 @@ export const RowDetails: Story = {
573
577
  email: 'jane.smith@company.com',
574
578
  status: 'Pending',
575
579
  actions: (
576
- <button onClick={() => toggleRow(1)}>
580
+ <ActionButton onClick={() => toggleRow(1)}>
577
581
  {expandedRows.has(1) ? 'Hide' : 'Show'}
578
- </button>
582
+ </ActionButton>
579
583
  ),
580
584
  _rowDetails: (
581
- <div>
585
+ <>
582
586
  <h3>Pending Approval</h3>
583
587
  <p><strong>Department:</strong> Marketing</p>
584
588
  <p><strong>Position:</strong> Marketing Manager</p>
585
589
  <p><strong>Application Date:</strong> December 1, 2024</p>
586
590
  <p><strong>Status:</strong> Awaiting HR approval</p>
587
591
  <div style={{ marginTop: '12px' }}>
588
- <button style={{ marginRight: '8px', backgroundColor: '#22c55e', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '4px' }}>Approve</button>
589
- <button style={{ backgroundColor: '#ef4444', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '4px' }}>Reject</button>
592
+ <ActionButton style={{ marginRight: '8px', backgroundColor: '#22c55e', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '4px' }}>Approve</ActionButton>
593
+ <ActionButton style={{ backgroundColor: '#ef4444', color: 'white', border: 'none', padding: '6px 12px', borderRadius: '4px' }}>Reject</ActionButton>
590
594
  </div>
591
- </div>
595
+ </>
592
596
  )
593
597
  },
594
598
  {
@@ -596,22 +600,22 @@ export const RowDetails: Story = {
596
600
  email: 'bob.johnson@company.com',
597
601
  status: 'Inactive',
598
602
  actions: (
599
- <button onClick={() => toggleRow(2)}>
603
+ <ActionButton onClick={() => toggleRow(2)}>
600
604
  {expandedRows.has(2) ? 'Hide' : 'Show'}
601
- </button>
605
+ </ActionButton>
602
606
  ),
603
607
  _rowDetails: (
604
- <div>
608
+ <>
605
609
  <h3>Account Information</h3>
606
610
  <p><strong>Department:</strong> Sales</p>
607
611
  <p><strong>Position:</strong> Sales Director</p>
608
612
  <p><strong>Last Active:</strong> November 20, 2024</p>
609
613
  <p><strong>Reason:</strong> On extended leave</p>
610
614
  <div style={{ marginTop: '12px' }}>
611
- <button style={{ marginRight: '8px' }}>Reactivate Account</button>
612
- <button>Contact Employee</button>
615
+ <ActionButton style={{ marginRight: '8px' }}>Reactivate Account</ActionButton>
616
+ <ActionButton>Contact Employee</ActionButton>
613
617
  </div>
614
- </div>
618
+ </>
615
619
  )
616
620
  },
617
621
  ];
@@ -704,14 +708,17 @@ export const VNodeLabels: Story = {
704
708
  <strong>👤 User Name</strong>
705
709
  </span>
706
710
  ),
711
+ ariaLabel: 'User Name',
707
712
  },
708
713
  {
709
714
  key: 'email',
710
715
  label: <span style={{ color: '#0066cc' }}>📧 Email Address</span>,
716
+ ariaLabel: 'Email Address',
711
717
  },
712
718
  {
713
719
  key: 'role',
714
720
  label: <em style={{ color: '#666' }}>Role & Department</em>,
721
+ ariaLabel: 'Role & Department',
715
722
  },
716
723
  {
717
724
  key: 'status',
@@ -727,10 +734,12 @@ export const VNodeLabels: Story = {
727
734
  📊 Status
728
735
  </span>
729
736
  ),
737
+ ariaLabel: 'Status',
730
738
  },
731
739
  {
732
740
  key: 'actions',
733
741
  label: <span>⚙️ Actions</span>,
742
+ ariaLabel: 'Actions',
734
743
  },
735
744
  ],
736
745
  rowData: [
@@ -739,23 +748,277 @@ export const VNodeLabels: Story = {
739
748
  email: 'john.doe@company.com',
740
749
  role: 'Senior Developer',
741
750
  status: 'Active',
742
- actions: <button>Edit</button>,
751
+ actions: <ActionButton>Edit</ActionButton>,
743
752
  },
744
753
  {
745
754
  name: 'Jane Smith',
746
755
  email: 'jane.smith@company.com',
747
756
  role: 'Product Manager',
748
757
  status: 'Active',
749
- actions: <button>Edit</button>,
758
+ actions: <ActionButton>Edit</ActionButton>,
750
759
  },
751
760
  {
752
761
  name: 'Bob Johnson',
753
762
  email: 'bob.johnson@company.com',
754
763
  role: 'UX Designer',
755
764
  status: 'Inactive',
756
- actions: <button>Edit</button>,
765
+ actions: <ActionButton>Edit</ActionButton>,
757
766
  },
758
767
  ],
759
768
  },
760
769
  };
761
770
 
771
+ /**
772
+ * Table with pagination to navigate through multiple pages of data.
773
+ * This demonstrates how to integrate the Pagination component with the Table component.
774
+ *
775
+ * **Features**:
776
+ * - Pagination controls below the table
777
+ * - Page state management with useState
778
+ * - Dynamic row data based on current page
779
+ * - Items per page configuration
780
+ *
781
+ * ```tsx
782
+ * const [currentPage, setCurrentPage] = useState(1);
783
+ * const itemsPerPage = 5;
784
+ * const totalItems = 50;
785
+ * const totalPages = Math.ceil(totalItems / itemsPerPage);
786
+ *
787
+ * const paginatedData = allData.slice(
788
+ * (currentPage - 1) * itemsPerPage,
789
+ * currentPage * itemsPerPage
790
+ * );
791
+ *
792
+ * <div>
793
+ * <Table
794
+ * columns={columns}
795
+ * rowData={paginatedData}
796
+ * />
797
+ * <Pagination
798
+ * currentPage={currentPage}
799
+ * totalPages={totalPages}
800
+ * onChange={setCurrentPage}
801
+ * />
802
+ * </div>
803
+ * ```
804
+ */
805
+ export const WithPagination: Story = {
806
+ render: (args) => {
807
+ const [currentPage, setCurrentPage] = useState(1);
808
+ const [pageSize, setPageSize] = useState(5);
809
+
810
+ // Page size options
811
+ const pageSizeOptions: PickerOption[] = [
812
+ { value: '5', text: '5' },
813
+ { value: '10', text: '10' },
814
+ { value: '15', text: '15' },
815
+ ];
816
+
817
+ // Generate sample data (287 items to match screenshot)
818
+ const allData = Array.from({ length: 287 }, (_, index) => ({
819
+ id: index + 1,
820
+ name: `User ${index + 1}`,
821
+ email: `user${index + 1}@example.com`,
822
+ department: ['Engineering', 'Marketing', 'Sales', 'HR', 'Finance'][index % 5],
823
+ status: ['Active', 'Inactive', 'Pending'][index % 3],
824
+ actions: <ActionButton>Edit</ActionButton>,
825
+ }));
826
+
827
+ const totalPages = Math.ceil(allData.length / pageSize);
828
+
829
+ // Calculate item range
830
+ const startItem = (currentPage - 1) * pageSize + 1;
831
+ const endItem = Math.min(currentPage * pageSize, allData.length);
832
+ const totalItems = allData.length;
833
+
834
+ // Get current page data
835
+ const paginatedData = allData.slice(
836
+ (currentPage - 1) * pageSize,
837
+ currentPage * pageSize
838
+ );
839
+
840
+ // Handle page size change
841
+ const handlePageSizeChange = (event: Event) => {
842
+ const target = event.target as HTMLSelectElement;
843
+ const newPageSize = Number(target.value);
844
+ setPageSize(newPageSize);
845
+ setCurrentPage(1); // Reset to first page when page size changes
846
+ };
847
+
848
+ return (
849
+ <div>
850
+ <TableComponent
851
+ {...args}
852
+ columns={[
853
+ { key: 'id', label: 'ID' },
854
+ { key: 'name', label: 'Name' },
855
+ { key: 'email', label: 'Email' },
856
+ { key: 'department', label: 'Department' },
857
+ { key: 'status', label: 'Status' },
858
+ { key: 'actions', label: 'Actions' },
859
+ ]}
860
+ rowData={paginatedData}
861
+ />
862
+ <div style={{ marginTop: 'var(--spacing-small)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
863
+ <span style={{
864
+ font: 'var(--type-body-1-default-font)',
865
+ letterSpacing: 'var(--type-body-1-default-letter-spacing)',
866
+ color: 'var(--color-neutral-800)'
867
+ }}>
868
+ Items {startItem} to {endItem} of {totalItems} total
869
+ </span>
870
+ <Pagination
871
+ currentPage={currentPage}
872
+ totalPages={totalPages}
873
+ onChange={setCurrentPage}
874
+ />
875
+ <div style={{
876
+ display: 'flex',
877
+ alignItems: 'center',
878
+ gap: 'var(--spacing-xsmall)',
879
+ font: 'var(--type-body-1-default-font)',
880
+ letterSpacing: 'var(--type-body-1-default-letter-spacing)',
881
+ color: 'var(--color-neutral-800)'
882
+ }}>
883
+ <span>Show</span>
884
+ <Picker
885
+ variant="primary"
886
+ size="medium"
887
+ value={String(pageSize)}
888
+ options={pageSizeOptions}
889
+ handleSelect={handlePageSizeChange}
890
+ aria-label="Items per page"
891
+ />
892
+ </div>
893
+ </div>
894
+ </div>
895
+ );
896
+ },
897
+ };
898
+
899
+ /**
900
+ * Table with expandable row details and stacked mobile layout.
901
+ * Combines the expandable rows feature with responsive mobile behavior using container queries.
902
+ *
903
+ * **Features**:
904
+ * - Expandable rows with toggle buttons
905
+ * - Stacked mobile layout when container width ≤ 600px
906
+ * - Row details expand in both desktop and mobile views
907
+ * - Mobile view shows labels above each cell value
908
+ *
909
+ * ```tsx
910
+ * const [expandedRows, setExpandedRows] = useState(new Set());
911
+ *
912
+ * const toggleRow = (rowIndex: number) => {
913
+ * setExpandedRows(prev => {
914
+ * const newSet = new Set(prev);
915
+ * if (newSet.has(rowIndex)) {
916
+ * newSet.delete(rowIndex);
917
+ * } else {
918
+ * newSet.add(rowIndex);
919
+ * }
920
+ * return newSet;
921
+ * });
922
+ * };
923
+ *
924
+ * <Table
925
+ * mobileLayout="stacked"
926
+ * columns={columns}
927
+ * rowData={rowDataWithDetails}
928
+ * expandedRows={expandedRows}
929
+ * />
930
+ * ```
931
+ */
932
+ export const ExpandableRowsWithMobileLayout: Story = {
933
+ render: (args) => {
934
+ const [expandedRows, setExpandedRows] = useState(new Set<number>());
935
+
936
+ const toggleRow = (rowIndex: number) => {
937
+ setExpandedRows((prev) => {
938
+ const newSet = new Set(prev);
939
+ if (newSet.has(rowIndex)) {
940
+ newSet.delete(rowIndex);
941
+ } else {
942
+ newSet.add(rowIndex);
943
+ }
944
+ return newSet;
945
+ });
946
+ };
947
+
948
+ const rowData = [
949
+ {
950
+ name: 'John Doe',
951
+ email: 'john.doe@company.com',
952
+ status: 'Active',
953
+ actions: (
954
+ <ActionButton onClick={() => toggleRow(0)}>
955
+ {expandedRows.has(0) ? 'Hide' : 'Show'}
956
+ </ActionButton>
957
+ ),
958
+ _rowDetails: (
959
+ <>
960
+ <h3>Employee Details</h3>
961
+ <p><strong>Department:</strong> Engineering</p>
962
+ <p><strong>Position:</strong> Senior Developer</p>
963
+ <p><strong>Start Date:</strong> January 15, 2022</p>
964
+ <p><strong>Notes:</strong> Excellent performance, leads the frontend team.</p>
965
+ </>
966
+ )
967
+ },
968
+ {
969
+ name: 'Jane Smith',
970
+ email: 'jane.smith@company.com',
971
+ status: 'Pending',
972
+ actions: (
973
+ <ActionButton onClick={() => toggleRow(1)}>
974
+ {expandedRows.has(1) ? 'Hide' : 'Show'}
975
+ </ActionButton>
976
+ ),
977
+ _rowDetails: (
978
+ <>
979
+ <h3>Pending Approval</h3>
980
+ <p><strong>Department:</strong> Marketing</p>
981
+ <p><strong>Position:</strong> Marketing Manager</p>
982
+ <p><strong>Application Date:</strong> December 1, 2024</p>
983
+ <p><strong>Status:</strong> Awaiting HR approval</p>
984
+ </>
985
+ )
986
+ },
987
+ {
988
+ name: 'Bob Johnson',
989
+ email: 'bob.johnson@company.com',
990
+ status: 'Inactive',
991
+ actions: (
992
+ <ActionButton onClick={() => toggleRow(2)}>
993
+ {expandedRows.has(2) ? 'Hide' : 'Show'}
994
+ </ActionButton>
995
+ ),
996
+ _rowDetails: (
997
+ <>
998
+ <h3>Account Information</h3>
999
+ <p><strong>Department:</strong> Sales</p>
1000
+ <p><strong>Position:</strong> Sales Director</p>
1001
+ <p><strong>Last Active:</strong> November 20, 2024</p>
1002
+ <p><strong>Reason:</strong> On extended leave</p>
1003
+ </>
1004
+ )
1005
+ },
1006
+ ];
1007
+
1008
+ return (
1009
+ <TableComponent
1010
+ {...args}
1011
+ mobileLayout="stacked"
1012
+ columns={[
1013
+ { key: 'name', label: 'Name', ariaLabel: 'Name' },
1014
+ { key: 'email', label: 'Email', ariaLabel: 'Email' },
1015
+ { key: 'status', label: 'Status', ariaLabel: 'Status' },
1016
+ { key: 'actions', label: 'Actions', ariaLabel: 'Actions' },
1017
+ ]}
1018
+ rowData={rowData}
1019
+ expandedRows={expandedRows}
1020
+ />
1021
+ );
1022
+ },
1023
+ };
1024
+
@@ -97,7 +97,8 @@ export const Table: FunctionComponent<TableProps> = ({
97
97
  iconSource = 'ChevronDown';
98
98
  ariaLabel = translations.sortedDescending.replace('{label}', label);
99
99
  } else {
100
- iconSource = 'Sort';
100
+ // Show chevron down when sortable but not sorted
101
+ iconSource = 'ChevronDown';
101
102
  ariaLabel = translations.sortBy.replace('{label}', label);
102
103
  }
103
104
 
@@ -138,7 +139,10 @@ export const Table: FunctionComponent<TableProps> = ({
138
139
 
139
140
  return (
140
141
  <Fragment key={rowIndex}>
141
- <tr className="dropin-table__body__row">
142
+ <tr className={classes([
143
+ 'dropin-table__body__row',
144
+ ['dropin-table__body__row--expanded', isExpanded && hasDetails],
145
+ ])}>
142
146
  {columns.map((column) => {
143
147
  const cell = row[column.key];
144
148
  const label = column.ariaLabel ?? column.label;
@@ -0,0 +1,69 @@
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 { getGlobalLocale } from '@adobe-commerce/elsie/lib';
11
+
12
+ export interface PriceFormatterOptions {
13
+ currency?: string | null;
14
+ locale?: string;
15
+ formatOptions?: Intl.NumberFormatOptions;
16
+ }
17
+
18
+ /**
19
+ * Determines the effective locale to use for price formatting
20
+ * Priority: prop locale > global locale > browser locale > default 'en-US'
21
+ */
22
+ export function getEffectiveLocale(locale?: string): string {
23
+ if (locale) {
24
+ return locale;
25
+ }
26
+ const globalLocale = getGlobalLocale();
27
+ if (globalLocale) {
28
+ return globalLocale;
29
+ }
30
+ // Fallback to browser locale or default
31
+ return process.env.LOCALE && process.env.LOCALE !== 'undefined' ? process.env.LOCALE : 'en-US';
32
+ }
33
+
34
+ /**
35
+ * Gets an Intl.NumberFormat instance for price formatting
36
+ * Uses getEffectiveLocale internally to determine the best locale
37
+ *
38
+ * @example
39
+ * // Single price formatting
40
+ * const formatter = getPriceFormatter({ currency: 'USD', locale: 'en-US' });
41
+ * const price = formatter.format(10.99); // "$10.99"
42
+ *
43
+ * @example
44
+ * // Bulk price formatting (more efficient)
45
+ * const formatter = getPriceFormatter({ currency: 'EUR', locale: 'fr-FR' });
46
+ * const prices = [10.99, 25.50, 99.99].map(amount => formatter.format(amount));
47
+ */
48
+ export function getPriceFormatter(
49
+ options: PriceFormatterOptions = {}
50
+ ): Intl.NumberFormat {
51
+ const { currency, locale, formatOptions = {} } = options;
52
+ const effectiveLocale = getEffectiveLocale(locale);
53
+
54
+ const params: Intl.NumberFormatOptions = {
55
+ style: 'currency',
56
+ currency: currency || 'USD',
57
+ // These options are needed to round to whole numbers if that's what you want.
58
+ minimumFractionDigits: 2, // (this suffices for whole numbers, but will print 2500.10 as $2,500.1)
59
+ maximumFractionDigits: 2, // (causes 2500.99 to be printed as $2,501)
60
+ ...formatOptions,
61
+ };
62
+
63
+ try {
64
+ return new Intl.NumberFormat(effectiveLocale, params);
65
+ } catch (error) {
66
+ console.error(`Error creating Intl.NumberFormat instance for locale ${effectiveLocale}. Falling back to en-US.`, error);
67
+ return new Intl.NumberFormat('en-US', params);
68
+ }
69
+ }
package/src/lib/index.ts CHANGED
@@ -2,9 +2,9 @@
2
2
  * Copyright 2024 Adobe
3
3
  * All Rights Reserved.
4
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.
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
8
  *******************************************************************/
9
9
 
10
10
  export * from '@adobe-commerce/elsie/lib/form-values';
@@ -25,3 +25,4 @@ export * from '@adobe-commerce/elsie/lib/is-number';
25
25
  export * from '@adobe-commerce/elsie/lib/deviceUtils';
26
26
  export * from '@adobe-commerce/elsie/lib/get-path-value';
27
27
  export * from '@adobe-commerce/elsie/lib/get-cookie';
28
+ export * from '@adobe-commerce/elsie/lib/get-price-formatter';