@adobe-commerce/elsie 1.4.1-alpha102 → 1.5.0-alpha007

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
+ }
@@ -4,7 +4,9 @@ const path = require('path');
4
4
  module.exports = async function generateResourceBuilder({ argv }) {
5
5
  const { build, preview } = await import('vite');
6
6
 
7
- const configFile = argv?.config ?? path.resolve(__dirname, '../../../config/vite.mjs');
7
+ const configFile =
8
+ argv?.config ??
9
+ path.resolve(...[__dirname, '..', '..', '..', 'config', 'vite.mjs']);
8
10
 
9
11
  let built = false;
10
12
 
package/config/vite.mjs 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
  import { glob } from 'glob';
@@ -25,7 +25,12 @@ import banner from 'vite-plugin-banner';
25
25
  const env = loadEnv('', process.cwd());
26
26
 
27
27
  // Load Elsie Config
28
- const elsieConfig = await import(path.resolve(process.cwd(), './.elsie.js')).then((m) => m.default);
28
+ const elsieConfigPath = path.resolve(process.cwd(), './.elsie.js');
29
+ // Convert Windows paths to file:// URLs for ES module imports
30
+ const elsieConfigUrl = elsieConfigPath.startsWith('file://')
31
+ ? elsieConfigPath
32
+ : `file://${elsieConfigPath.replace(/\\/g, '/')}`;
33
+ const elsieConfig = await import(elsieConfigUrl).then((m) => m.default);
29
34
 
30
35
  // Read package.json using createRequire (compatible with Node 20 and 22)
31
36
  const require = createRequire(import.meta.url);
@@ -293,19 +298,19 @@ export default {
293
298
  generateBundle(options, bundle) {
294
299
  for (const fileName in bundle) {
295
300
  const chunk = bundle[fileName];
296
-
301
+
297
302
  // Process both .map files and JS/TS files with sourcemaps
298
- if ((chunk.type === 'asset' && fileName.endsWith('.map')) ||
303
+ if ((chunk.type === 'asset' && fileName.endsWith('.map')) ||
299
304
  (chunk.type === 'chunk' && chunk.map)) {
300
305
  try {
301
306
  // Get the sourcemap object - either from the asset source or the chunk's map
302
307
  const map = chunk.type === 'asset' ? JSON.parse(chunk.source) : chunk.map;
303
-
308
+
304
309
  if (map.sources) {
305
310
  map.sources = map.sources.map((input) => {
306
311
  return input.replace(/(?:\.\.?\/)+src\//, `/${packageJSON.name}/src/`);
307
312
  });
308
-
313
+
309
314
  // Update the sourcemap in the appropriate place
310
315
  if (chunk.type === 'asset') {
311
316
  chunk.source = JSON.stringify(map);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe-commerce/elsie",
3
- "version": "1.4.1-alpha102",
3
+ "version": "1.5.0-alpha007",
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';
@@ -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://cssguidelin.es/#bem-like-naming */
11
+
12
+ .dropin-table {
13
+ container-type: inline-size;
14
+ overflow-x: auto;
15
+ font: var(--type-body-1-default-font);
16
+ letter-spacing: var(--type-body-1-default-letter-spacing);
17
+ }
18
+
19
+ .dropin-table__table {
20
+ border-collapse: collapse;
21
+ width: 100%;
22
+ }
23
+
24
+ .dropin-table__caption {
25
+ font: var(--type-details-caption-1-font);
26
+ letter-spacing: var(--type-details-caption-1-letter-spacing);
27
+ text-align: left;
28
+ margin-bottom: var(--spacing-small);
29
+ caption-side: top;
30
+ }
31
+
32
+ .dropin-table__header__cell {
33
+ font: var(--type-body-1-strong-font);
34
+ letter-spacing: var(--type-body-1-strong-letter-spacing);
35
+ }
36
+
37
+ .dropin-table__header__cell,
38
+ .dropin-table__body__cell {
39
+ padding: var(--spacing-xsmall);
40
+ text-align: left;
41
+ white-space: nowrap;
42
+ }
43
+
44
+ .dropin-table__header__cell--sortable {
45
+ cursor: pointer;
46
+ }
47
+
48
+ .dropin-table__header__row {
49
+ border-bottom: 2px solid var(--color-neutral-400);
50
+ }
51
+
52
+ .dropin-table__body__row {
53
+ border-bottom: 1px solid var(--color-neutral-400);
54
+ }
55
+
56
+ .dropin-table__header__sort-button {
57
+ margin-left: var(--spacing-xsmall);
58
+ vertical-align: middle;
59
+ }
60
+
61
+ /* Container query for mobile layout */
62
+ @container (max-width: 600px) {
63
+ /* Mobile layout Stacked */
64
+ .dropin-table--mobile-layout-stacked .dropin-table__header {
65
+ display: none;
66
+ }
67
+
68
+ .dropin-table--mobile-layout-stacked .dropin-table__body__cell {
69
+ display: block;
70
+ }
71
+
72
+ .dropin-table--mobile-layout-stacked .dropin-table__body__cell::before {
73
+ content: attr(data-label);
74
+ font-weight: bold;
75
+ display: block;
76
+ margin-bottom: var(--spacing-xxsmall);
77
+ }
78
+ }
79
+
80
+ /* Medium (portrait tablets and large phones, 768px and up) */
81
+ /* @media only screen and (min-width: 768px) { } */
82
+
83
+ /* Large (landscape tablets, 1024px and up) */
84
+ /* @media only screen and (min-width: 1024px) { } */
85
+
86
+ /* XLarge (laptops/desktops, 1366px and up) */
87
+ /* @media only screen and (min-width: 1366px) { } */
88
+
89
+ /* XXlarge (large laptops and desktops, 1920px and up) */
90
+ /* @media only screen and (min-width: 1920px) { } */
@@ -0,0 +1,492 @@
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 { useState } from 'preact/hooks';
13
+ import { Table as TableComponent, TableProps } from '@adobe-commerce/elsie/components/Table';
14
+
15
+ /**
16
+ * Use the `Table` component to render data in a structured table.
17
+ *
18
+ * ## Column Structure
19
+ * Each column in the `columns` array defines a table column with:
20
+ * - **`key`**: Unique identifier that matches the property names in `rowData` objects
21
+ * - **`label`**: Display text shown in the column header
22
+ * - **`sortBy`**: Optional sorting state (`true` for sortable but neutral, `'asc'` for ascending, `'desc'` for descending)
23
+ *
24
+ * ## Row Data Structure
25
+ * Each object in the `rowData` array represents a table row where:
26
+ * - **Keys** must match the `key` values from the `columns` array
27
+ * - **Values** can be:
28
+ * - **Strings**: Plain text content
29
+ * - **Numbers**: Numeric values (automatically converted to strings for display)
30
+ * - **VNode**: Preact `VNode` for complex content (buttons, icons, formatted text, etc.)
31
+ *
32
+ * ## Mobile Layout & Container Queries
33
+ * The table uses **container queries** instead of media queries for responsive behavior:
34
+ * - **`mobileLayout`**: Optional prop that controls mobile behavior
35
+ * - `'none'` (default): No special mobile layout
36
+ * - `'stacked'`: Stacks cells vertically when container width ≤ 600px
37
+ * - **Container Query Breakpoint**: 600px - triggers when the table's container becomes narrow
38
+ * - **`data-label`**: Automatically added to cells for accessibility in stacked layout
39
+ * - **Responsive Behavior**: Table adapts to its container width, not viewport width
40
+ *
41
+ * ## All props
42
+ * The table below shows all the props for the `Table` component.
43
+ */
44
+
45
+ const meta: Meta<TableProps> = {
46
+ title: 'Components/Table',
47
+ component: TableComponent,
48
+ parameters: {
49
+ layout: 'padded',
50
+ },
51
+ argTypes: {
52
+ columns: {
53
+ description: 'Array of column definitions for the table. Each column defines the structure and behavior of a table column.',
54
+ table: {
55
+ type: { summary: 'Column[]' },
56
+ },
57
+ control: 'object',
58
+ },
59
+ rowData: {
60
+ description: 'Array of data objects to display in table rows. Each object represents a table row where keys match column keys and values contain the cell content.',
61
+ table: {
62
+ type: { summary: 'RowData[]' },
63
+ },
64
+ control: 'object',
65
+ },
66
+ mobileLayout: {
67
+ description: 'Controls responsive layout behavior using container queries. When set to "stacked", cells stack vertically when the table container width ≤ 600px. The `data-label` attribute is automatically added to cells for accessibility.',
68
+ table: {
69
+ type: { summary: 'none | stacked' },
70
+ },
71
+ control: 'select',
72
+ options: ['none', 'stacked'],
73
+ mapping: {
74
+ none: 'none',
75
+ stacked: 'stacked',
76
+ },
77
+ },
78
+ caption: {
79
+ description: 'Optional table caption that provides context and description. Displays above the table and is announced by screen readers.',
80
+ table: {
81
+ type: { summary: 'string' },
82
+ },
83
+ control: 'text',
84
+ },
85
+ onSortChange: {
86
+ description: 'Callback function triggered when column sorting changes.',
87
+ table: {
88
+ type: { summary: '(columnKey: string, direction: Sortable) => void' },
89
+ },
90
+ action: 'onSortChange',
91
+ },
92
+ },
93
+ };
94
+
95
+ export default meta;
96
+
97
+ type Story = StoryObj<TableProps>;
98
+
99
+ // Wrapper component to manage sorting state
100
+ const TableWithState = (args: TableProps) => {
101
+ const [columns, setColumns] = useState(args.columns);
102
+
103
+ const handleSortChange = (columnKey: string, direction: 'asc' | 'desc' | true) => {
104
+ // call Action onSortChange
105
+ args.onSortChange?.(columnKey, direction);
106
+
107
+ // Update column sort states
108
+ setColumns(prevColumns =>
109
+ prevColumns.map(col => {
110
+ if (col.key === columnKey) {
111
+ return { ...col, sortBy: direction };
112
+ } else if (col.sortBy === 'asc' || col.sortBy === 'desc') {
113
+ return { ...col, sortBy: true }; // Reset other sorted columns to neutral
114
+ }
115
+ return col;
116
+ })
117
+ );
118
+ };
119
+
120
+ return (
121
+ <TableComponent
122
+ {...args}
123
+ columns={columns}
124
+ onSortChange={handleSortChange}
125
+ />
126
+ );
127
+ };
128
+
129
+ /**
130
+ * Simple table.
131
+ * Demonstrates basic table structure with string and number content types.
132
+ *
133
+ *
134
+ * ```tsx
135
+ * <Table
136
+ * columns={[
137
+ * { key: 'name', label: 'Name' },
138
+ * { key: 'email', label: 'Email' },
139
+ * { key: 'age', label: 'Age' }
140
+ * ]}
141
+ * rowData={[
142
+ * { name: 'John', email: 'john@example.com', age: 20 },
143
+ * { name: 'Jane', email: 'jane@example.com', age: 21 }
144
+ * ]}
145
+ * />
146
+ * ```
147
+ */
148
+ export const Table: Story = {
149
+ args: {
150
+ columns: [
151
+ { key: 'name', label: 'Name' },
152
+ { key: 'email', label: 'Email' },
153
+ { key: 'age', label: 'Age' },
154
+ { key: 'actions', label: '' },
155
+ ],
156
+ rowData: [
157
+ { name: 'John', email: 'john@example.com', age: 20, actions: <button>Edit</button> },
158
+ { name: 'Jane', email: 'jane@example.com', age: 21, actions: <button>Edit</button> },
159
+ { name: 'Jim', email: 'jim@example.com', age: 22, actions: <button>Edit</button> },
160
+ { name: 'Jill', email: 'jill@example.com', age: 23, actions: <button>Edit</button> },
161
+ ],
162
+ },
163
+ };
164
+
165
+ /**
166
+ * Table where all columns are sortable. Demonstrates the three-state sorting cycle: `true` → `'asc'` → `'desc'` → `true`.
167
+ * Shows how multiple columns can be sortable simultaneously, with only one active sort at a time.
168
+ *
169
+ * ```tsx
170
+ * <Table
171
+ * columns={[
172
+ * { key: 'name', label: 'Name', sortBy: true },
173
+ * { key: 'email', label: 'Email', sortBy: true },
174
+ * { key: 'age', label: 'Age', sortBy: true }
175
+ * ]}
176
+ * rowData={[
177
+ * { name: 'John', email: 'john@example.com', age: 20 },
178
+ * { name: 'Jane', email: 'jane@example.com', age: 21 }
179
+ * ]}
180
+ * onSortChange={(columnKey, direction) => handleSort(columnKey, direction)}
181
+ * />
182
+ * ```
183
+ */
184
+ export const AllSortable: Story = {
185
+ render: TableWithState,
186
+ args: {
187
+ columns: [
188
+ { key: 'name', label: 'Name', sortBy: true },
189
+ { key: 'email', label: 'Email', sortBy: true },
190
+ { key: 'age', label: 'Age', sortBy: true },
191
+ { key: 'actions', label: '' },
192
+ ],
193
+ rowData: [
194
+ { name: 'John', email: 'john@example.com', age: 20, actions: <button>Edit</button> },
195
+ { name: 'Jane', email: 'jane@example.com', age: 21, actions: <button>Edit</button> },
196
+ { name: 'Jim', email: 'jim@example.com', age: 22, actions: <button>Edit</button> },
197
+ { name: 'Jill', email: 'jill@example.com', age: 23, actions: <button>Edit</button> },
198
+ { name: 'Jack', email: 'jack@example.com', age: 24, actions: <button>Edit</button> },
199
+ ],
200
+ },
201
+ };
202
+
203
+ /**
204
+ * Wide table with 10 columns to demonstrate horizontal scrolling and container query behavior.
205
+ * This table will show how the container query responds when the table becomes too wide for its container.
206
+ *
207
+ * ```tsx
208
+ * <Table
209
+ * columns={[
210
+ * { key: 'id', label: 'ID' },
211
+ * { key: 'name', label: 'Full Name' },
212
+ * { key: 'email', label: 'Email Address' },
213
+ * { key: 'phone', label: 'Phone Number' },
214
+ * { key: 'department', label: 'Department' },
215
+ * { key: 'position', label: 'Position' },
216
+ * { key: 'salary', label: 'Salary' },
217
+ * { key: 'startDate', label: 'Start Date' },
218
+ * { key: 'status', label: 'Status' },
219
+ * { key: 'actions', label: 'Actions' }
220
+ * ]}
221
+ * rowData={[
222
+ * { 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> }
223
+ * ]}
224
+ * />
225
+ * ```
226
+ */
227
+ export const WideTable: Story = {
228
+ args: {
229
+ columns: [
230
+ { key: 'id', label: 'ID' },
231
+ { key: 'name', label: 'Full Name' },
232
+ { key: 'email', label: 'Email Address' },
233
+ { key: 'phone', label: 'Phone Number' },
234
+ { key: 'department', label: 'Department' },
235
+ { key: 'position', label: 'Position' },
236
+ { key: 'salary', label: 'Salary' },
237
+ { key: 'startDate', label: 'Start Date' },
238
+ { key: 'status', label: 'Status' },
239
+ { key: 'actions', label: 'Actions' },
240
+ ],
241
+ rowData: [
242
+ {
243
+ id: 1,
244
+ name: 'John Doe',
245
+ email: 'john.doe@company.com',
246
+ phone: '+1-555-0123',
247
+ department: 'Engineering',
248
+ position: 'Senior Developer',
249
+ salary: '$95,000',
250
+ startDate: '2022-01-15',
251
+ status: 'Active',
252
+ actions: <button>Edit</button>
253
+ },
254
+ {
255
+ id: 2,
256
+ name: 'Jane Smith',
257
+ email: 'jane.smith@company.com',
258
+ phone: '+1-555-0124',
259
+ department: 'Marketing',
260
+ position: 'Marketing Manager',
261
+ salary: '$78,000',
262
+ startDate: '2021-06-20',
263
+ status: 'Active',
264
+ actions: <button>Edit</button>
265
+ },
266
+ {
267
+ id: 3,
268
+ name: 'Bob Johnson',
269
+ email: 'bob.johnson@company.com',
270
+ phone: '+1-555-0125',
271
+ department: 'Sales',
272
+ position: 'Sales Director',
273
+ salary: '$110,000',
274
+ startDate: '2020-03-10',
275
+ status: 'Active',
276
+ actions: <button>Edit</button>
277
+ },
278
+ {
279
+ id: 4,
280
+ name: 'Alice Brown',
281
+ email: 'alice.brown@company.com',
282
+ phone: '+1-555-0126',
283
+ department: 'HR',
284
+ position: 'HR Specialist',
285
+ salary: '$65,000',
286
+ startDate: '2023-02-28',
287
+ status: 'Pending',
288
+ actions: <button>Edit</button>
289
+ },
290
+ {
291
+ id: 5,
292
+ name: 'Charlie Wilson',
293
+ email: 'charlie.wilson@company.com',
294
+ phone: '+1-555-0127',
295
+ department: 'Finance',
296
+ position: 'Financial Analyst',
297
+ salary: '$72,000',
298
+ startDate: '2022-09-12',
299
+ status: 'Active',
300
+ actions: <button>Edit</button>
301
+ },
302
+ ],
303
+ },
304
+ };
305
+
306
+ /**
307
+ * Table demonstrating complex VNode content in cells with multi-line text and interactive elements.
308
+ * This shows how the table handles rich content including buttons, badges, and formatted text.
309
+ *
310
+ * ```tsx
311
+ * <Table
312
+ * columns={[
313
+ * { key: 'user', label: 'User Info' },
314
+ * { key: 'description', label: 'Description' },
315
+ * { key: 'status', label: 'Status' },
316
+ * { key: 'actions', label: 'Actions' }
317
+ * ]}
318
+ * rowData={[
319
+ * {
320
+ * user: <div><strong>John Doe</strong><br/>john@example.com<br/>Senior Developer</div>,
321
+ * description: <div>Lead developer for the<br/>e-commerce platform<br/>with 5+ years experience</div>,
322
+ * status: <span style="background: green; color: white; padding: 2px 8px; border-radius: 4px;">Active</span>,
323
+ * actions: <div><button>Edit</button><br/><button>Delete</button><br/><button>View</button></div>
324
+ * }
325
+ * ]}
326
+ * />
327
+ * ```
328
+ */
329
+ export const ComplexCells: Story = {
330
+ args: {
331
+ columns: [
332
+ { key: 'user', label: 'User Info' },
333
+ { key: 'description', label: 'Description' },
334
+ { key: 'status', label: 'Status' },
335
+ { key: 'actions', label: 'Actions' },
336
+ ],
337
+ rowData: [
338
+ {
339
+ user: (
340
+ <div>
341
+ <strong>John Doe</strong><br/>
342
+ john.doe@company.com<br/>
343
+ <em>Senior Developer</em>
344
+ </div>
345
+ ),
346
+ description: (
347
+ <div>
348
+ Lead developer for the<br/>
349
+ e-commerce platform<br/>
350
+ <small>with 5+ years experience</small>
351
+ </div>
352
+ ),
353
+ status: (
354
+ <span style={{
355
+ background: '#22c55e',
356
+ color: 'white',
357
+ padding: '2px 8px',
358
+ borderRadius: '4px',
359
+ fontSize: '12px',
360
+ fontWeight: 'bold'
361
+ }}>
362
+ Active
363
+ </span>
364
+ ),
365
+ actions: (
366
+ <div>
367
+ <button style={{ marginBottom: '4px', display: 'block' }}>Edit</button>
368
+ <button style={{ marginBottom: '4px', display: 'block' }}>Delete</button>
369
+ <button style={{ display: 'block' }}>View</button>
370
+ </div>
371
+ ),
372
+ },
373
+ {
374
+ user: (
375
+ <div>
376
+ <strong>Jane Smith</strong><br/>
377
+ jane.smith@company.com<br/>
378
+ <em>Product Manager</em>
379
+ </div>
380
+ ),
381
+ description: (
382
+ <div>
383
+ Manages product roadmap<br/>
384
+ and feature planning<br/>
385
+ <small>3+ years in product</small>
386
+ </div>
387
+ ),
388
+ status: (
389
+ <span style={{
390
+ background: '#f59e0b',
391
+ color: 'white',
392
+ padding: '2px 8px',
393
+ borderRadius: '4px',
394
+ fontSize: '12px',
395
+ fontWeight: 'bold'
396
+ }}>
397
+ Pending
398
+ </span>
399
+ ),
400
+ actions: (
401
+ <div>
402
+ <button style={{ marginBottom: '4px', display: 'block' }}>Edit</button>
403
+ <button style={{ marginBottom: '4px', display: 'block' }}>Approve</button>
404
+ <button style={{ display: 'block' }}>Reject</button>
405
+ </div>
406
+ ),
407
+ },
408
+ {
409
+ user: (
410
+ <div>
411
+ <strong>Bob Johnson</strong><br/>
412
+ bob.johnson@company.com<br/>
413
+ <em>UX Designer</em>
414
+ </div>
415
+ ),
416
+ description: (
417
+ <div>
418
+ Designs user interfaces<br/>
419
+ and user experiences<br/>
420
+ <small>Expert in Figma & Sketch</small>
421
+ </div>
422
+ ),
423
+ status: (
424
+ <span style={{
425
+ background: '#ef4444',
426
+ color: 'white',
427
+ padding: '2px 8px',
428
+ borderRadius: '4px',
429
+ fontSize: '12px',
430
+ fontWeight: 'bold'
431
+ }}>
432
+ Inactive
433
+ </span>
434
+ ),
435
+ actions: (
436
+ <div>
437
+ <button style={{ marginBottom: '4px', display: 'block' }}>Edit</button>
438
+ <button style={{ marginBottom: '4px', display: 'block' }}>Activate</button>
439
+ <button style={{ display: 'block' }}>Archive</button>
440
+ </div>
441
+ ),
442
+ },
443
+ ],
444
+ },
445
+ };
446
+
447
+ /**
448
+ * Table with stacked mobile layout that uses container queries.
449
+ * This demonstrates how the table adapts to its container width rather than viewport width.
450
+ * The table will stack vertically when its container becomes narrow (≤600px).
451
+ *
452
+ * **Container Query Behavior**: Uses `mobileLayout="stacked"` to enable responsive stacking.
453
+ * When the container width ≤ 600px:
454
+ * - Headers are hidden (`display: none`)
455
+ * - Cells stack vertically (`display: block`)
456
+ * - Column labels appear as `data-label` attributes before each cell value
457
+ * - Perfect for mobile views, sidebars, or constrained layouts
458
+ *
459
+ * ```tsx
460
+ * <Table
461
+ * mobileLayout="stacked"
462
+ * columns={[
463
+ * { key: 'name', label: 'Name' },
464
+ * { key: 'email', label: 'Email' },
465
+ * { key: 'age', label: 'Age' }
466
+ * ]}
467
+ * rowData={[
468
+ * { name: 'John', email: 'john@example.com', age: 20 },
469
+ * { name: 'Jane', email: 'jane@example.com', age: 21 }
470
+ * ]}
471
+ * />
472
+ * ```
473
+ */
474
+ export const StackedMobileLayout: Story = {
475
+ args: {
476
+ mobileLayout: 'stacked',
477
+ columns: [
478
+ { key: 'name', label: 'Name' },
479
+ { key: 'email', label: 'Email' },
480
+ { key: 'age', label: 'Age' },
481
+ { key: 'status', label: 'Status' },
482
+ { key: 'actions', label: 'Actions' },
483
+ ],
484
+ rowData: [
485
+ { name: 'John Doe', email: 'john.doe@example.com', age: 28, status: 'Active', actions: <button>Edit</button> },
486
+ { name: 'Jane Smith', email: 'jane.smith@example.com', age: 32, status: 'Inactive', actions: <button>Edit</button> },
487
+ { name: 'Bob Johnson', email: 'bob.johnson@example.com', age: 45, status: 'Active', actions: <button>Edit</button> },
488
+ { name: 'Alice Brown', email: 'alice.brown@example.com', age: 29, status: 'Pending', actions: <button>Edit</button> },
489
+ ],
490
+ },
491
+ };
492
+
@@ -0,0 +1,149 @@
1
+ /********************************************************************
2
+ * Copyright 2025 Adobe
3
+ * All Rights Reserved.
4
+ *
5
+ * NOTICE: Adobe permits you to use, modify, and distribute this
6
+ * file in accordance with the terms of the Adobe license agreement
7
+ * accompanying it.
8
+ *******************************************************************/
9
+
10
+ import { FunctionComponent, VNode } from 'preact';
11
+ import { HTMLAttributes } from 'preact/compat';
12
+ import { classes, VComponent } from '@adobe-commerce/elsie/lib';
13
+ import { Icon, Button } from '@adobe-commerce/elsie/components';
14
+ import { useText } from '@adobe-commerce/elsie/i18n';
15
+
16
+ import '@adobe-commerce/elsie/components/Table/Table.css';
17
+
18
+ type Sortable = 'asc' | 'desc' | true;
19
+
20
+ type Column = {
21
+ key: string;
22
+ label: string;
23
+ sortBy?: Sortable;
24
+ };
25
+
26
+ type RowData = {
27
+ [key: string]: VNode | string | number;
28
+ };
29
+
30
+ export interface TableProps extends HTMLAttributes<HTMLTableElement> {
31
+ columns: Column[];
32
+ rowData: RowData[];
33
+ mobileLayout?: 'stacked' | 'none';
34
+ caption?: string;
35
+ onSortChange?: (columnKey: string, direction: Sortable) => void;
36
+ }
37
+
38
+ export const Table: FunctionComponent<TableProps> = ({
39
+ className,
40
+ children,
41
+ columns = [],
42
+ rowData = [],
43
+ mobileLayout = 'none',
44
+ caption,
45
+ onSortChange,
46
+ ...props
47
+ }) => {
48
+ const translations = useText({
49
+ ariaSortAscending: 'Dropin.Table.ariaSortAscending',
50
+ ariaSortDescending: 'Dropin.Table.ariaSortDescending',
51
+ sortedAscending: 'Dropin.Table.sortedAscending',
52
+ sortedDescending: 'Dropin.Table.sortedDescending',
53
+ sortBy: 'Dropin.Table.sortBy',
54
+ });
55
+
56
+ const handleSort = (column: Column) => {
57
+ if (!onSortChange) return;
58
+
59
+ // Determine next sort direction
60
+ let nextDirection: Sortable;
61
+ if (column.sortBy === true) {
62
+ nextDirection = 'asc';
63
+ } else if (column.sortBy === 'asc') {
64
+ nextDirection = 'desc';
65
+ } else {
66
+ nextDirection = true;
67
+ }
68
+
69
+ onSortChange(column.key, nextDirection);
70
+ };
71
+
72
+ const getSortButton = (column: Column) => {
73
+ if (column.sortBy === undefined) return null;
74
+
75
+ let iconSource: string;
76
+ let ariaLabel: string;
77
+
78
+ if (column.sortBy === 'asc') {
79
+ iconSource = 'ChevronUp';
80
+ ariaLabel = translations.sortedAscending.replace('{label}', column.label);
81
+ } else if (column.sortBy === 'desc') {
82
+ iconSource = 'ChevronDown';
83
+ ariaLabel = translations.sortedDescending.replace('{label}', column.label);
84
+ } else {
85
+ iconSource = 'Sort';
86
+ ariaLabel = translations.sortBy.replace('{label}', column.label);
87
+ }
88
+
89
+ return (
90
+ <Button
91
+ variant="tertiary"
92
+ size="medium"
93
+ className="dropin-table__header__sort-button"
94
+ icon={<Icon source={iconSource} />}
95
+ aria-label={ariaLabel}
96
+ onClick={() => handleSort(column)}
97
+ />
98
+ );
99
+ };
100
+
101
+ return (
102
+ <div className={classes(['dropin-table', `dropin-table--mobile-layout-${mobileLayout}`, className])}>
103
+ <table {...props} className="dropin-table__table">
104
+ {caption && <caption className="dropin-table__caption">{caption}</caption>}
105
+ <thead className="dropin-table__header">
106
+ <tr className="dropin-table__header__row">
107
+ {columns.map((column) => (
108
+ <th
109
+ key={column.key}
110
+ className={classes([
111
+ 'dropin-table__header__cell',
112
+ ['dropin-table__header__cell--sorted', column.sortBy === 'asc' || column.sortBy === 'desc'],
113
+ ['dropin-table__header__cell--sortable', column.sortBy !== undefined]
114
+ ])}
115
+ role="columnheader"
116
+ >
117
+ {column.label}
118
+ {getSortButton(column)}
119
+ </th>
120
+ ))}
121
+ </tr>
122
+ </thead>
123
+ <tbody className="dropin-table__body">
124
+ {rowData.map((row, rowIndex) => (
125
+ <tr key={rowIndex} className="dropin-table__body__row">
126
+ {columns.map((column) => {
127
+ const cell = row[column.key];
128
+
129
+ if (typeof cell === 'string' || typeof cell === 'number') {
130
+ return (
131
+ <td key={column.key} className="dropin-table__body__cell" data-label={column.label}>
132
+ {cell}
133
+ </td>
134
+ );
135
+ }
136
+
137
+ return (
138
+ <td key={column.key} className="dropin-table__body__cell" data-label={column.label}>
139
+ <VComponent node={cell} />
140
+ </td>
141
+ );
142
+ })}
143
+ </tr>
144
+ ))}
145
+ </tbody>
146
+ </table>
147
+ </div>
148
+ );
149
+ };
@@ -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/Table/Table';
11
+ export { Table as default } from '@adobe-commerce/elsie/components/Table/Table';
@@ -48,3 +48,5 @@ 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';
52
+ export * from '@adobe-commerce/elsie/components/Table';
@@ -121,5 +121,21 @@ 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.
124
140
  </Unstyled>
125
141
 
@@ -32,6 +32,12 @@ 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
+ ### lazy
36
+
37
+ Controls whether the slot should be loaded immediately or deferred for later initialization. `boolean` (optional).
38
+
39
+ When `lazy={false}`, the slot is initialized as soon as the container mounts. When `lazy={true}`, the slot can be initialized later on when it is needed. This is useful for performance optimization, especially when the slot's content is not immediately required.
40
+
35
41
  ### slotTag
36
42
 
37
43
  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.).
@@ -71,7 +77,7 @@ Example:
71
77
 
72
78
  - `ctx`: An object representing the context of the slot, including methods for manipulating the slot's content.
73
79
 
74
- The slot property, which is implemented as a promise function, provides developers with the flexibility to dynamically generate and manipulate content within slots.
80
+ The slot property, which is implemented as a promise function, provides developers with the flexibility to dynamically generate and manipulate content within slots.
75
81
  However, it's important to note that this promise is render-blocking, meaning that the component will not render until the promise is resolved.
76
82
 
77
83
  ### context
@@ -141,6 +141,13 @@
141
141
  },
142
142
  "InputDate": {
143
143
  "picker": "Select a date"
144
+ },
145
+ "Table": {
146
+ "ariaSortAscending": "ascending",
147
+ "ariaSortDescending": "descending",
148
+ "sortedAscending": "Sort {label} ascending",
149
+ "sortedDescending": "Sort {label} descending",
150
+ "sortBy": "Sort by {label}"
144
151
  }
145
152
  }
146
153
  }
package/src/lib/slot.tsx CHANGED
@@ -2,30 +2,35 @@
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
- import { cloneElement, ComponentChildren, RefObject, VNode, createElement } from 'preact';
10
+ import { IntlContext, Lang } from '@adobe-commerce/elsie/i18n';
11
+ import {
12
+ cloneElement,
13
+ ComponentChildren,
14
+ createElement,
15
+ RefObject,
16
+ VNode,
17
+ } from 'preact';
18
+ import { HTMLAttributes } from 'preact/compat';
11
19
  import {
12
20
  StateUpdater,
21
+ useCallback,
13
22
  useContext,
14
- useState,
15
- useRef,
16
23
  useEffect,
17
24
  useMemo,
18
- useCallback,
25
+ useRef,
26
+ useState,
19
27
  } from 'preact/hooks';
20
- import { IntlContext, Lang } from '@adobe-commerce/elsie/i18n';
21
- import { HTMLAttributes } from 'preact/compat';
22
28
  import { SlotQueueContext } from './render';
23
29
 
24
30
  import '@adobe-commerce/elsie/components/UIProvider/debugger.css';
25
31
 
26
32
  type MutateElement = (elem: HTMLElement) => void;
27
33
 
28
-
29
34
  interface State {
30
35
  get: (key: string) => void;
31
36
  set: (key: string, value: any) => void;
@@ -149,18 +154,21 @@ export function useSlot<K, V extends HTMLElement>(
149
154
  // @ts-ignore
150
155
  context._registerMethod = _registerMethod;
151
156
 
152
- const _htmlElementToVNode = useCallback((elem: HTMLElement) => {
153
- return createElement(
154
- contentTag,
155
- {
156
- 'data-slot-html-element': elem.tagName.toLowerCase(),
157
- ref: (refElem: HTMLElement | null): void => {
158
- refElem?.appendChild(elem);
159
- }
160
- },
161
- null
162
- );
163
- }, [contentTag]);
157
+ const _htmlElementToVNode = useCallback(
158
+ (elem: HTMLElement) => {
159
+ return createElement(
160
+ contentTag,
161
+ {
162
+ 'data-slot-html-element': elem.tagName.toLowerCase(),
163
+ ref: (refElem: HTMLElement | null): void => {
164
+ refElem?.appendChild(elem);
165
+ },
166
+ },
167
+ null
168
+ );
169
+ },
170
+ [contentTag]
171
+ );
164
172
 
165
173
  // @ts-ignore
166
174
  context._htmlElementToVNode = _htmlElementToVNode;
@@ -322,7 +330,10 @@ export function useSlot<K, V extends HTMLElement>(
322
330
  status.current = 'loading';
323
331
 
324
332
  log(`🟩 "${name}" Slot Initialized`);
325
- await callback(context as K & DefaultSlotContext<K>, elementRef.current as HTMLDivElement | null);
333
+ await callback(
334
+ context as K & DefaultSlotContext<K>,
335
+ elementRef.current as HTMLDivElement | null
336
+ );
326
337
  } catch (error) {
327
338
  console.error(`Error in "${callback.name}" Slot callback`, error);
328
339
  } finally {
@@ -336,7 +347,7 @@ export function useSlot<K, V extends HTMLElement>(
336
347
  // Initialization
337
348
  useEffect(() => {
338
349
  handleLifeCycleInit().finally(() => {
339
- if (slotsQueue) {
350
+ if (slotsQueue && slotsQueue.value.has(name)) {
340
351
  slotsQueue.value.delete(name);
341
352
  slotsQueue.value = new Set(slotsQueue.value);
342
353
  }
@@ -359,6 +370,7 @@ export function useSlot<K, V extends HTMLElement>(
359
370
  interface SlotPropsComponent<T>
360
371
  extends Omit<HTMLAttributes<HTMLElement>, 'slot'> {
361
372
  name: string;
373
+ lazy?: boolean;
362
374
  slot?: SlotProps<T>;
363
375
  context?: Context<T>;
364
376
  render?: (props: Record<string, any>) => VNode | VNode[];
@@ -371,6 +383,7 @@ interface SlotPropsComponent<T>
371
383
 
372
384
  export function Slot<T>({
373
385
  name,
386
+ lazy = false,
374
387
  context,
375
388
  slot,
376
389
  children,
@@ -400,11 +413,11 @@ export function Slot<T>({
400
413
  }
401
414
 
402
415
  // add slot to queue
403
- if (slotsQueue) {
416
+ if (slotsQueue && lazy === false) {
404
417
  slotsQueue.value.add(name);
405
418
  slotsQueue.value = new Set(slotsQueue.value);
406
419
  }
407
- }, [name, slotsQueue]);
420
+ }, [name, lazy, slotsQueue]);
408
421
 
409
422
  return createElement(
410
423
  slotTag,