@shohojdhara/atomix 0.3.12 → 0.3.14
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/CHANGELOG.md +19 -0
- package/README.md +2 -0
- package/dist/atomix.css +101 -88
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +5 -15258
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.d.ts +1 -1
- package/dist/charts.js +17 -19
- package/dist/charts.js.map +1 -1
- package/dist/core.d.ts +41 -11
- package/dist/core.js +55 -41
- package/dist/core.js.map +1 -1
- package/dist/forms.d.ts +28 -11
- package/dist/forms.js +25 -24
- package/dist/forms.js.map +1 -1
- package/dist/heavy.d.ts +1 -1
- package/dist/heavy.js +32 -25
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +122 -46
- package/dist/index.esm.js +865 -200
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +870 -204
- package/dist/index.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/theme.d.ts +27 -2
- package/dist/theme.js +721 -108
- package/dist/theme.js.map +1 -1
- package/package.json +1 -1
- package/scripts/atomix-cli.js +610 -1111
- package/scripts/cli/component-generator.js +610 -0
- package/scripts/cli/documentation-sync.js +542 -0
- package/scripts/cli/interactive-init.js +84 -288
- package/scripts/cli/mappings.js +211 -0
- package/scripts/cli/migration-tools.js +95 -288
- package/scripts/cli/template-manager.js +107 -0
- package/scripts/cli/templates/README.md +123 -0
- package/scripts/cli/templates/composable-templates.js +149 -0
- package/scripts/cli/templates/config-templates.js +126 -0
- package/scripts/cli/templates/index.js +95 -0
- package/scripts/cli/templates/project-templates.js +214 -0
- package/scripts/cli/templates/react-templates.js +261 -0
- package/scripts/cli/templates/scss-templates.js +156 -0
- package/scripts/cli/templates/storybook-templates.js +236 -0
- package/scripts/cli/templates/testing-templates.js +45 -0
- package/scripts/cli/templates/token-templates.js +447 -0
- package/scripts/cli/templates/types-templates.js +133 -0
- package/scripts/cli/templates-original-backup.js +1655 -0
- package/scripts/cli/templates.js +35 -0
- package/scripts/cli/templates_backup.js +684 -0
- package/scripts/cli/theme-bridge.js +20 -14
- package/scripts/cli/token-manager.js +150 -77
- package/scripts/cli/utils.js +37 -25
- package/src/components/Accordion/Accordion.stories.tsx +5 -5
- package/src/components/Accordion/Accordion.test.tsx +57 -0
- package/src/components/Accordion/Accordion.tsx +4 -0
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +41 -44
- package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +1 -1
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +37 -37
- package/src/components/AtomixGlass/stories/Modes.stories.tsx +1 -2
- package/src/components/AtomixGlass/stories/Playground.stories.tsx +50 -51
- package/src/components/Avatar/Avatar.stories.tsx +26 -26
- package/src/components/Badge/Badge.stories.tsx +31 -31
- package/src/components/Badge/Badge.test.tsx +51 -0
- package/src/components/Badge/Badge.tsx +20 -1
- package/src/components/Block/Block.stories.tsx +5 -5
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +1 -1
- package/src/components/Breadcrumb/Breadcrumb.tsx +2 -2
- package/src/components/Button/Button.stories.tsx +13 -13
- package/src/components/Button/Button.tsx +4 -4
- package/src/components/Button/ButtonGroup.stories.tsx +2 -2
- package/src/components/Button/README.md +5 -0
- package/src/components/Callout/Callout.stories.tsx +11 -11
- package/src/components/Callout/Callout.test.tsx +10 -10
- package/src/components/Callout/Callout.tsx +7 -7
- package/src/components/Callout/README.md +9 -8
- package/src/components/Card/Card.tsx +2 -2
- package/src/components/Chart/Chart.stories.tsx +6 -6
- package/src/components/Chart/Chart.tsx +1 -1
- package/src/components/ColorModeToggle/ColorModeToggle.stories.tsx +1 -1
- package/src/components/DataTable/DataTable.tsx +14 -12
- package/src/components/DatePicker/DatePicker.stories.tsx +6 -6
- package/src/components/Dropdown/Dropdown.stories.tsx +4 -4
- package/src/components/Form/Checkbox.stories.tsx +3 -3
- package/src/components/Form/Checkbox.tsx +4 -2
- package/src/components/Form/Form.stories.tsx +3 -3
- package/src/components/Form/FormGroup.stories.tsx +1 -1
- package/src/components/Form/Input.stories.tsx +28 -16
- package/src/components/Form/Input.test.tsx +59 -0
- package/src/components/Form/Input.tsx +97 -95
- package/src/components/Form/Radio.stories.tsx +94 -94
- package/src/components/Form/Radio.tsx +2 -2
- package/src/components/Form/Select.stories.tsx +4 -4
- package/src/components/Form/Select.tsx +2 -2
- package/src/components/Form/Textarea.stories.tsx +22 -7
- package/src/components/Form/Textarea.test.tsx +45 -0
- package/src/components/Form/Textarea.tsx +88 -86
- package/src/components/List/List.stories.tsx +2 -2
- package/src/components/Modal/Modal.stories.tsx +4 -4
- package/src/components/Navigation/Navbar/Navbar.stories.tsx +5 -5
- package/src/components/Navigation/Navbar/Navbar.tsx +1 -1
- package/src/components/Navigation/README.md +1 -1
- package/src/components/Pagination/Pagination.stories.tsx +5 -2
- package/src/components/Pagination/Pagination.tsx +1 -1
- package/src/components/PhotoViewer/PhotoViewer.stories.tsx +10 -10
- package/src/components/Popover/Popover.stories.tsx +1 -1
- package/src/components/ProductReview/ProductReview.tsx +1 -1
- package/src/components/Progress/Progress.tsx +46 -46
- package/src/components/Rating/Rating.stories.tsx +4 -4
- package/src/components/Rating/Rating.tsx +8 -8
- package/src/components/Slider/Slider.stories.tsx +63 -63
- package/src/components/Spinner/Spinner.stories.tsx +2 -2
- package/src/components/Spinner/Spinner.test.tsx +35 -0
- package/src/components/Spinner/Spinner.tsx +9 -2
- package/src/components/Testimonial/Testimonial.stories.tsx +1 -1
- package/src/components/Toggle/Toggle.stories.tsx +32 -9
- package/src/components/Toggle/Toggle.test.tsx +91 -0
- package/src/components/Toggle/Toggle.tsx +44 -27
- package/src/components/Tooltip/Tooltip.tsx +1 -1
- package/src/layouts/Grid/Grid.stories.tsx +49 -49
- package/src/layouts/MasonryGrid/MasonryGrid.stories.tsx +2 -2
- package/src/lib/composables/useAccordion.ts +12 -3
- package/src/lib/composables/useBreadcrumb.ts +2 -2
- package/src/lib/composables/useCallout.ts +7 -7
- package/src/lib/composables/useNavbar.ts +1 -1
- package/src/lib/constants/components.ts +1 -1
- package/src/lib/storybook/InteractiveDemo.tsx +113 -0
- package/src/lib/storybook/PreviewContainer.tsx +36 -0
- package/src/lib/storybook/VariantsGrid.tsx +21 -0
- package/src/lib/storybook/index.ts +3 -0
- package/src/lib/theme/core/createThemeObject.ts +9 -5
- package/src/lib/theme/devtools/CLI.ts +155 -0
- package/src/lib/theme/devtools/DesignTokensCustomizer.stories.tsx +213 -0
- package/src/lib/theme/devtools/DesignTokensCustomizer.tsx +566 -0
- package/src/lib/theme/devtools/LiveEditor.tsx +2 -1
- package/src/lib/theme/devtools/index.ts +3 -0
- package/src/lib/theme/errors/errors.ts +8 -0
- package/src/lib/theme/runtime/ThemeProvider.tsx +117 -57
- package/src/lib/theme/runtime/__tests__/ThemeProvider.integration.test.tsx +305 -0
- package/src/lib/theme/runtime/__tests__/ThemeProvider.test.tsx +588 -0
- package/src/lib/theme/utils/__tests__/themeValidation.test.ts +264 -0
- package/src/lib/theme/utils/index.ts +1 -0
- package/src/lib/theme/utils/themeValidation.ts +501 -0
- package/src/lib/theme-tools.ts +32 -3
- package/src/lib/types/components.ts +81 -26
- package/src/lib/utils/themeNaming.ts +1 -1
- package/src/styles/06-components/_components.atomix-glass.scss +14 -15
- package/src/styles/06-components/_components.callout.scss +29 -33
- package/src/styles/06-components/_index.scss +1 -1
- package/src/styles/99-utilities/_utilities.display.scss +14 -3
- package/src/styles/99-utilities/_utilities.flex.scss +10 -10
- package/src/styles/99-utilities/_utilities.text.scss +28 -8
- package/scripts/cli/__tests__/cli-commands.test.js +0 -204
- package/scripts/cli/__tests__/utils.test.js +0 -201
- package/scripts/cli/__tests__/vitest.config.js +0 -26
package/scripts/cli/utils.js
CHANGED
|
@@ -7,6 +7,18 @@ import { resolve, relative, isAbsolute, normalize } from 'path';
|
|
|
7
7
|
import { existsSync } from 'fs';
|
|
8
8
|
import { access } from 'fs/promises';
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Enhanced Error Class for CLI
|
|
12
|
+
*/
|
|
13
|
+
export class AtomixCLIError extends Error {
|
|
14
|
+
constructor(message, code, suggestions = []) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = 'AtomixCLIError';
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.suggestions = suggestions;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
10
22
|
/**
|
|
11
23
|
* Validates and sanitizes file paths to prevent directory traversal attacks
|
|
12
24
|
* @param {string} inputPath - The path to validate
|
|
@@ -17,13 +29,13 @@ export function validatePath(inputPath, basePath = process.cwd()) {
|
|
|
17
29
|
try {
|
|
18
30
|
// Normalize the paths to remove any '..' or '.' segments
|
|
19
31
|
const normalizedBase = normalize(resolve(basePath));
|
|
20
|
-
const normalizedInput = normalize(isAbsolute(inputPath)
|
|
21
|
-
? inputPath
|
|
32
|
+
const normalizedInput = normalize(isAbsolute(inputPath)
|
|
33
|
+
? inputPath
|
|
22
34
|
: resolve(basePath, inputPath));
|
|
23
|
-
|
|
35
|
+
|
|
24
36
|
// Check if the resolved path is within the base directory
|
|
25
37
|
const relativePath = relative(normalizedBase, normalizedInput);
|
|
26
|
-
|
|
38
|
+
|
|
27
39
|
// If the relative path starts with '..', it's outside the base directory
|
|
28
40
|
if (relativePath.startsWith('..')) {
|
|
29
41
|
return {
|
|
@@ -32,7 +44,7 @@ export function validatePath(inputPath, basePath = process.cwd()) {
|
|
|
32
44
|
error: 'Path is outside the project directory'
|
|
33
45
|
};
|
|
34
46
|
}
|
|
35
|
-
|
|
47
|
+
|
|
36
48
|
// Additional checks for sensitive paths
|
|
37
49
|
const sensitivePatterns = [
|
|
38
50
|
/^\.git/,
|
|
@@ -43,7 +55,7 @@ export function validatePath(inputPath, basePath = process.cwd()) {
|
|
|
43
55
|
/private/i,
|
|
44
56
|
/secret/i
|
|
45
57
|
];
|
|
46
|
-
|
|
58
|
+
|
|
47
59
|
for (const pattern of sensitivePatterns) {
|
|
48
60
|
if (pattern.test(relativePath)) {
|
|
49
61
|
return {
|
|
@@ -53,7 +65,7 @@ export function validatePath(inputPath, basePath = process.cwd()) {
|
|
|
53
65
|
};
|
|
54
66
|
}
|
|
55
67
|
}
|
|
56
|
-
|
|
68
|
+
|
|
57
69
|
return {
|
|
58
70
|
isValid: true,
|
|
59
71
|
safePath: normalizedInput,
|
|
@@ -80,7 +92,7 @@ export function validateComponentName(name) {
|
|
|
80
92
|
error: 'Component name must be a non-empty string'
|
|
81
93
|
};
|
|
82
94
|
}
|
|
83
|
-
|
|
95
|
+
|
|
84
96
|
// Check PascalCase: starts with uppercase, only contains letters and numbers
|
|
85
97
|
if (!/^[A-Z][a-zA-Z0-9]*$/.test(name)) {
|
|
86
98
|
return {
|
|
@@ -88,20 +100,20 @@ export function validateComponentName(name) {
|
|
|
88
100
|
error: 'Component name must be in PascalCase (e.g., Button, CardHeader)'
|
|
89
101
|
};
|
|
90
102
|
}
|
|
91
|
-
|
|
103
|
+
|
|
92
104
|
// Check for reserved words
|
|
93
105
|
const reservedWords = [
|
|
94
106
|
'Component', 'React', 'Fragment', 'Suspense', 'StrictMode',
|
|
95
107
|
'Error', 'Loading', 'App', 'Root', 'Document', 'Html'
|
|
96
108
|
];
|
|
97
|
-
|
|
109
|
+
|
|
98
110
|
if (reservedWords.includes(name)) {
|
|
99
111
|
return {
|
|
100
112
|
isValid: false,
|
|
101
113
|
error: `"${name}" is a reserved word. Please choose a different name.`
|
|
102
114
|
};
|
|
103
115
|
}
|
|
104
|
-
|
|
116
|
+
|
|
105
117
|
// Check minimum length
|
|
106
118
|
if (name.length < 2) {
|
|
107
119
|
return {
|
|
@@ -109,7 +121,7 @@ export function validateComponentName(name) {
|
|
|
109
121
|
error: 'Component name must be at least 2 characters long'
|
|
110
122
|
};
|
|
111
123
|
}
|
|
112
|
-
|
|
124
|
+
|
|
113
125
|
return { isValid: true };
|
|
114
126
|
}
|
|
115
127
|
|
|
@@ -125,7 +137,7 @@ export function validateThemeName(name) {
|
|
|
125
137
|
error: 'Theme name must be a non-empty string'
|
|
126
138
|
};
|
|
127
139
|
}
|
|
128
|
-
|
|
140
|
+
|
|
129
141
|
// Check kebab-case: lowercase letters, numbers, and hyphens
|
|
130
142
|
if (!/^[a-z][a-z0-9-]*$/.test(name)) {
|
|
131
143
|
return {
|
|
@@ -133,7 +145,7 @@ export function validateThemeName(name) {
|
|
|
133
145
|
error: 'Theme name must be lowercase and use hyphens (e.g., dark-theme)'
|
|
134
146
|
};
|
|
135
147
|
}
|
|
136
|
-
|
|
148
|
+
|
|
137
149
|
// Check for consecutive hyphens
|
|
138
150
|
if (/--/.test(name)) {
|
|
139
151
|
return {
|
|
@@ -141,7 +153,7 @@ export function validateThemeName(name) {
|
|
|
141
153
|
error: 'Theme name cannot contain consecutive hyphens'
|
|
142
154
|
};
|
|
143
155
|
}
|
|
144
|
-
|
|
156
|
+
|
|
145
157
|
// Check for trailing hyphen
|
|
146
158
|
if (name.endsWith('-')) {
|
|
147
159
|
return {
|
|
@@ -149,7 +161,7 @@ export function validateThemeName(name) {
|
|
|
149
161
|
error: 'Theme name cannot end with a hyphen'
|
|
150
162
|
};
|
|
151
163
|
}
|
|
152
|
-
|
|
164
|
+
|
|
153
165
|
return { isValid: true };
|
|
154
166
|
}
|
|
155
167
|
|
|
@@ -162,7 +174,7 @@ export function sanitizeInput(input) {
|
|
|
162
174
|
if (typeof input !== 'string') {
|
|
163
175
|
return String(input);
|
|
164
176
|
}
|
|
165
|
-
|
|
177
|
+
|
|
166
178
|
// Remove any shell metacharacters that could be dangerous
|
|
167
179
|
return input
|
|
168
180
|
.replace(/[;&|`$<>\\]/g, '')
|
|
@@ -190,7 +202,7 @@ export async function fileExists(filePath) {
|
|
|
190
202
|
*/
|
|
191
203
|
export function isCI() {
|
|
192
204
|
return !!(
|
|
193
|
-
process.env.CI ||
|
|
205
|
+
process.env.CI ||
|
|
194
206
|
process.env.CONTINUOUS_INTEGRATION ||
|
|
195
207
|
process.env.GITHUB_ACTIONS ||
|
|
196
208
|
process.env.GITLAB_CI ||
|
|
@@ -205,9 +217,9 @@ export function isCI() {
|
|
|
205
217
|
* @returns {boolean}
|
|
206
218
|
*/
|
|
207
219
|
export function isDebug() {
|
|
208
|
-
return process.env.ATOMIX_DEBUG === 'true' ||
|
|
209
|
-
|
|
210
|
-
|
|
220
|
+
return process.env.ATOMIX_DEBUG === 'true' ||
|
|
221
|
+
process.argv.includes('--debug') ||
|
|
222
|
+
process.argv.includes('-d');
|
|
211
223
|
}
|
|
212
224
|
|
|
213
225
|
/**
|
|
@@ -271,7 +283,7 @@ export function isValidColor(color) {
|
|
|
271
283
|
/^hsla\(/i, // hsla()
|
|
272
284
|
/^var\(--/ // CSS custom property
|
|
273
285
|
];
|
|
274
|
-
|
|
286
|
+
|
|
275
287
|
return patterns.some(pattern => pattern.test(color));
|
|
276
288
|
}
|
|
277
289
|
|
|
@@ -284,7 +296,7 @@ export function isValidColor(color) {
|
|
|
284
296
|
export function validateNpmScripts(packageJson, requiredScripts = []) {
|
|
285
297
|
const scripts = packageJson.scripts || {};
|
|
286
298
|
const missing = requiredScripts.filter(script => !scripts[script]);
|
|
287
|
-
|
|
299
|
+
|
|
288
300
|
return {
|
|
289
301
|
valid: missing.length === 0,
|
|
290
302
|
missing
|
|
@@ -311,7 +323,7 @@ export function checkNodeVersion(requiredVersion = '18.0.0') {
|
|
|
311
323
|
const currentVersion = process.version.substring(1); // Remove 'v' prefix
|
|
312
324
|
const current = currentVersion.split('.').map(Number);
|
|
313
325
|
const required = requiredVersion.split('.').map(Number);
|
|
314
|
-
|
|
326
|
+
|
|
315
327
|
let compatible = true;
|
|
316
328
|
for (let i = 0; i < required.length; i++) {
|
|
317
329
|
if (current[i] < required[i]) {
|
|
@@ -321,7 +333,7 @@ export function checkNodeVersion(requiredVersion = '18.0.0') {
|
|
|
321
333
|
break;
|
|
322
334
|
}
|
|
323
335
|
}
|
|
324
|
-
|
|
336
|
+
|
|
325
337
|
return {
|
|
326
338
|
compatible,
|
|
327
339
|
current: currentVersion,
|
|
@@ -145,7 +145,7 @@ export const AccordionGroup: Story = {
|
|
|
145
145
|
render: () => (
|
|
146
146
|
<div>
|
|
147
147
|
<h2>Accordion Group</h2>
|
|
148
|
-
<div className="u-
|
|
148
|
+
<div className="u-flex u-flex-column u-gap-3" style={{ width: '500px' }}>
|
|
149
149
|
<Accordion title="First Accordion" defaultOpen={true}>
|
|
150
150
|
<p>Content of the first accordion.</p>
|
|
151
151
|
</Accordion>
|
|
@@ -186,7 +186,7 @@ export const AllVariants: Story = {
|
|
|
186
186
|
render: () => (
|
|
187
187
|
<div>
|
|
188
188
|
<h2>All Accordion Variants</h2>
|
|
189
|
-
<div className="u-
|
|
189
|
+
<div className="u-flex u-flex-column u-gap-5">
|
|
190
190
|
<div>
|
|
191
191
|
<h3>Default</h3>
|
|
192
192
|
<Accordion title="Default Accordion">
|
|
@@ -445,7 +445,7 @@ export const GlassGroup: Story = {
|
|
|
445
445
|
Glass Accordion Group
|
|
446
446
|
</h2>
|
|
447
447
|
<div
|
|
448
|
-
className="u-
|
|
448
|
+
className="u-flex u-flex-column u-gap-3"
|
|
449
449
|
style={{ width: '100%', maxWidth: '600px', margin: '0 auto' }}
|
|
450
450
|
>
|
|
451
451
|
<Accordion title="First Glass Accordion" defaultOpen={true} glass>
|
|
@@ -835,7 +835,7 @@ export const GlassInteractiveShowcase: Story = {
|
|
|
835
835
|
Interactive Glass Accordion Showcase
|
|
836
836
|
</h2>
|
|
837
837
|
|
|
838
|
-
<div className="u-
|
|
838
|
+
<div className="u-flex u-flex-column u-gap-3">
|
|
839
839
|
<Accordion
|
|
840
840
|
title="Features & Benefits"
|
|
841
841
|
isOpen={openIndex === 0}
|
|
@@ -975,7 +975,7 @@ export const GlassRichContent: Story = {
|
|
|
975
975
|
Glass Accordion with Rich Content
|
|
976
976
|
</h2>
|
|
977
977
|
|
|
978
|
-
<div className="u-
|
|
978
|
+
<div className="u-flex u-flex-column u-gap-3">
|
|
979
979
|
<Accordion title="Design Philosophy" defaultOpen={true} glass={true}>
|
|
980
980
|
<div>
|
|
981
981
|
<p style={{ marginBottom: '1rem' }}>
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
2
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
3
|
+
import { Accordion } from './Accordion';
|
|
4
|
+
import React from 'react';
|
|
5
|
+
|
|
6
|
+
describe('Accordion Component', () => {
|
|
7
|
+
it('renders correctly with title', () => {
|
|
8
|
+
render(<Accordion title="Test Accordion">Content</Accordion>);
|
|
9
|
+
expect(screen.getByText('Test Accordion')).toBeInTheDocument();
|
|
10
|
+
expect(screen.getByText('Content')).toBeInTheDocument();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it('toggles when clicked', () => {
|
|
14
|
+
const onOpenChange = vi.fn();
|
|
15
|
+
render(<Accordion title="Test" onOpenChange={onOpenChange}>Content</Accordion>);
|
|
16
|
+
const button = screen.getByRole('button');
|
|
17
|
+
|
|
18
|
+
fireEvent.click(button);
|
|
19
|
+
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
20
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
21
|
+
|
|
22
|
+
fireEvent.click(button);
|
|
23
|
+
expect(onOpenChange).toHaveBeenCalledWith(false);
|
|
24
|
+
expect(button).toHaveAttribute('aria-expanded', 'false');
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('calls legacy onOpen/onClose handlers', () => {
|
|
28
|
+
const onOpen = vi.fn();
|
|
29
|
+
const onClose = vi.fn();
|
|
30
|
+
render(<Accordion title="Test" onOpen={onOpen} onClose={onClose}>Content</Accordion>);
|
|
31
|
+
const button = screen.getByRole('button');
|
|
32
|
+
|
|
33
|
+
fireEvent.click(button);
|
|
34
|
+
expect(onOpen).toHaveBeenCalled();
|
|
35
|
+
|
|
36
|
+
fireEvent.click(button);
|
|
37
|
+
expect(onClose).toHaveBeenCalled();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('handles controlled state', () => {
|
|
41
|
+
const onOpenChange = vi.fn();
|
|
42
|
+
const { rerender } = render(<Accordion title="Test" isOpen={false} onOpenChange={onOpenChange}>Content</Accordion>);
|
|
43
|
+
const button = screen.getByRole('button');
|
|
44
|
+
|
|
45
|
+
fireEvent.click(button);
|
|
46
|
+
expect(onOpenChange).toHaveBeenCalledWith(true);
|
|
47
|
+
expect(button).toHaveAttribute('aria-expanded', 'false'); // Should not change internally
|
|
48
|
+
|
|
49
|
+
rerender(<Accordion title="Test" isOpen={true} onOpenChange={onOpenChange}>Content</Accordion>);
|
|
50
|
+
expect(button).toHaveAttribute('aria-expanded', 'true');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('supports glass effect', () => {
|
|
54
|
+
const { container } = render(<Accordion title="Test" glass>Content</Accordion>);
|
|
55
|
+
expect(container.querySelector('.c-accordion--glass')).toBeInTheDocument();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -12,6 +12,8 @@ export const Accordion: React.FC<AccordionProps> = memo(({
|
|
|
12
12
|
defaultOpen = false,
|
|
13
13
|
isOpen: controlledOpen,
|
|
14
14
|
onOpenChange,
|
|
15
|
+
onOpen,
|
|
16
|
+
onClose,
|
|
15
17
|
disabled = false,
|
|
16
18
|
iconPosition = 'right',
|
|
17
19
|
icon,
|
|
@@ -39,6 +41,8 @@ export const Accordion: React.FC<AccordionProps> = memo(({
|
|
|
39
41
|
iconPosition,
|
|
40
42
|
isOpen: controlledOpen,
|
|
41
43
|
onOpenChange,
|
|
44
|
+
onOpen,
|
|
45
|
+
onClose,
|
|
42
46
|
});
|
|
43
47
|
|
|
44
48
|
// Default icon
|
|
@@ -14,6 +14,9 @@ import { ATOMIX_GLASS } from '../../lib/constants/components';
|
|
|
14
14
|
|
|
15
15
|
const { CONSTANTS } = ATOMIX_GLASS;
|
|
16
16
|
|
|
17
|
+
// Module-level counter for deterministic ID generation
|
|
18
|
+
let idCounter = 0;
|
|
19
|
+
|
|
17
20
|
// Module-level shared shader cache with LRU eviction
|
|
18
21
|
const MAX_CACHE_SIZE = 15;
|
|
19
22
|
interface ShaderCacheEntry {
|
|
@@ -131,16 +134,10 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
131
134
|
ref
|
|
132
135
|
) => {
|
|
133
136
|
// Generate a stable, deterministic ID for SSR compatibility
|
|
134
|
-
// Use a
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
// Server-side: use a predictable pattern
|
|
139
|
-
return `atomix-glass-filter-ssr-${Math.random().toString(36).substring(2, 11)}`;
|
|
140
|
-
}
|
|
141
|
-
// Client-side: use timestamp + random for uniqueness
|
|
142
|
-
return `atomix-glass-filter-${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
143
|
-
});
|
|
137
|
+
// Use a module-level counter that's consistent across server and client
|
|
138
|
+
const filterId = useMemo(() => {
|
|
139
|
+
return `atomix-glass-filter-${++idCounter}`;
|
|
140
|
+
}, []);
|
|
144
141
|
|
|
145
142
|
const [shaderMapUrl, setShaderMapUrl] = useState<string>('');
|
|
146
143
|
const shaderGeneratorRef = useRef<any>(null);
|
|
@@ -207,27 +204,20 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
207
204
|
fragment: selectedShader,
|
|
208
205
|
});
|
|
209
206
|
|
|
210
|
-
//
|
|
211
|
-
|
|
207
|
+
// Defer shader generation with longer delay to avoid blocking
|
|
208
|
+
setTimeout(() => {
|
|
212
209
|
const url = shaderGeneratorRef.current?.updateShader() || '';
|
|
213
210
|
setCachedShader(cacheKey, url);
|
|
214
211
|
setShaderMapUrl(url);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
if (typeof requestIdleCallback !== 'undefined') {
|
|
218
|
-
requestIdleCallback(generate, { timeout: 1000 });
|
|
219
|
-
} else {
|
|
220
|
-
// Fallback to setTimeout for browsers without requestIdleCallback
|
|
221
|
-
setTimeout(generate, 0);
|
|
222
|
-
}
|
|
212
|
+
}, 100);
|
|
223
213
|
} catch (error) {
|
|
224
214
|
console.warn('AtomixGlassContainer: Error generating shader map', error);
|
|
225
215
|
setShaderMapUrl(''); // Fallback to empty string
|
|
226
216
|
}
|
|
227
217
|
};
|
|
228
218
|
|
|
229
|
-
// Debounce with
|
|
230
|
-
shaderDebounceTimeoutRef.current = setTimeout(generateShader,
|
|
219
|
+
// Debounce with 500ms delay to reduce frequency
|
|
220
|
+
shaderDebounceTimeoutRef.current = setTimeout(generateShader, 500);
|
|
231
221
|
} else {
|
|
232
222
|
// Not in shader mode, clear URL
|
|
233
223
|
setShaderMapUrl('');
|
|
@@ -406,7 +396,6 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
406
396
|
};
|
|
407
397
|
}
|
|
408
398
|
}, [
|
|
409
|
-
filterId,
|
|
410
399
|
liquidBlur,
|
|
411
400
|
saturation,
|
|
412
401
|
blurAmount,
|
|
@@ -484,10 +473,12 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
484
473
|
>
|
|
485
474
|
<div
|
|
486
475
|
className={ATOMIX_GLASS.INNER_CLASS}
|
|
487
|
-
style={
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
476
|
+
style={
|
|
477
|
+
{
|
|
478
|
+
padding: `var(--atomix-glass-container-padding)`,
|
|
479
|
+
boxShadow: `var(--atomix-glass-container-box-shadow)`,
|
|
480
|
+
} as CSSProperties
|
|
481
|
+
}
|
|
491
482
|
onMouseEnter={onMouseEnter}
|
|
492
483
|
onMouseLeave={onMouseLeave}
|
|
493
484
|
onMouseDown={onMouseDown}
|
|
@@ -513,31 +504,37 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
513
504
|
{/* Enhanced Apple Liquid Glass Inner Shadow Layer */}
|
|
514
505
|
<div
|
|
515
506
|
className={ATOMIX_GLASS.FILTER_OVERLAY_CLASS}
|
|
516
|
-
style={
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
507
|
+
style={
|
|
508
|
+
{
|
|
509
|
+
filter: `url(#${filterId})`,
|
|
510
|
+
backdropFilter: `var(--atomix-glass-container-backdrop)`,
|
|
511
|
+
borderRadius: `var(--atomix-glass-container-radius)`,
|
|
512
|
+
} as CSSProperties
|
|
513
|
+
}
|
|
521
514
|
/>
|
|
522
515
|
<div
|
|
523
516
|
className={ATOMIX_GLASS.FILTER_SHADOW_CLASS}
|
|
524
|
-
style={
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
517
|
+
style={
|
|
518
|
+
{
|
|
519
|
+
boxShadow: `var(--atomix-glass-container-shadow)`,
|
|
520
|
+
opacity: `var(--atomix-glass-container-shadow-opacity)`,
|
|
521
|
+
background: `var(--atomix-glass-container-bg)`,
|
|
522
|
+
borderRadius: `var(--atomix-glass-container-radius)`,
|
|
523
|
+
} as CSSProperties
|
|
524
|
+
}
|
|
530
525
|
/>
|
|
531
526
|
</div>
|
|
532
527
|
|
|
533
528
|
<div
|
|
534
529
|
ref={contentRef}
|
|
535
530
|
className={ATOMIX_GLASS.CONTENT_CLASS}
|
|
536
|
-
style={
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
531
|
+
style={
|
|
532
|
+
{
|
|
533
|
+
position: 'relative',
|
|
534
|
+
textShadow: `var(--atomix-glass-container-text-shadow)`,
|
|
535
|
+
...(elasticity > 0 ? { zIndex: 100 } : {}),
|
|
536
|
+
} as CSSProperties
|
|
537
|
+
}
|
|
541
538
|
>
|
|
542
539
|
{children}
|
|
543
540
|
</div>
|
|
@@ -547,4 +544,4 @@ export const AtomixGlassContainer = forwardRef<HTMLDivElement, AtomixGlassContai
|
|
|
547
544
|
}
|
|
548
545
|
);
|
|
549
546
|
|
|
550
|
-
AtomixGlassContainer.displayName = 'AtomixGlassContainer';
|
|
547
|
+
AtomixGlassContainer.displayName = 'AtomixGlassContainer';
|
|
@@ -946,7 +946,7 @@ export const PerformanceOptimization: Story = {
|
|
|
946
946
|
<div style={{ margin: '0 auto', width: '100%' }}>
|
|
947
947
|
{/* Header Section */}
|
|
948
948
|
<div style={{ textAlign: 'center', marginBottom: '60px' }}>
|
|
949
|
-
<Badge variant="primary" label="Performance Guide" glass={{className: 'u-
|
|
949
|
+
<Badge variant="primary" label="Performance Guide" glass={{className: 'u-inline-block', children:<></>}} />
|
|
950
950
|
<h1
|
|
951
951
|
style={{
|
|
952
952
|
color: '#fff',
|