@shohojdhara/atomix 0.4.0 → 0.4.1

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.
Files changed (66) hide show
  1. package/dist/atomix.css +9231 -9337
  2. package/dist/atomix.css.map +1 -1
  3. package/dist/atomix.min.css +2 -2
  4. package/dist/atomix.min.css.map +1 -1
  5. package/dist/charts.js +4 -5
  6. package/dist/charts.js.map +1 -1
  7. package/dist/core.d.ts +87 -10
  8. package/dist/core.js +673 -480
  9. package/dist/core.js.map +1 -1
  10. package/dist/forms.d.ts +15 -3
  11. package/dist/forms.js +530 -97
  12. package/dist/forms.js.map +1 -1
  13. package/dist/heavy.js +5 -6
  14. package/dist/heavy.js.map +1 -1
  15. package/dist/index.d.ts +495 -254
  16. package/dist/index.esm.js +1269 -723
  17. package/dist/index.esm.js.map +1 -1
  18. package/dist/index.js +1273 -723
  19. package/dist/index.js.map +1 -1
  20. package/dist/index.min.js +1 -1
  21. package/dist/index.min.js.map +1 -1
  22. package/package.json +2 -2
  23. package/scripts/atomix-cli.js +10 -1
  24. package/scripts/cli/__tests__/utils.test.js +6 -2
  25. package/scripts/cli/migration-tools.js +2 -2
  26. package/scripts/cli/theme-bridge.js +7 -9
  27. package/scripts/cli/utils.js +2 -1
  28. package/src/components/Accordion/Accordion.stories.tsx +40 -0
  29. package/src/components/Accordion/Accordion.tsx +174 -56
  30. package/src/components/Accordion/AccordionCompound.test.tsx +70 -0
  31. package/src/components/Breadcrumb/Breadcrumb.tsx +156 -50
  32. package/src/components/Breadcrumb/BreadcrumbCompound.test.tsx +84 -0
  33. package/src/components/Callout/Callout.stories.tsx +166 -1011
  34. package/src/components/Callout/Callout.tsx +196 -84
  35. package/src/components/Callout/CalloutCompound.test.tsx +72 -0
  36. package/src/components/Dropdown/Dropdown.tsx +133 -20
  37. package/src/components/Dropdown/DropdownCompound.test.tsx +64 -0
  38. package/src/components/EdgePanel/EdgePanel.tsx +164 -112
  39. package/src/components/EdgePanel/EdgePanelCompound.test.tsx +53 -0
  40. package/src/components/Form/Select.stories.tsx +23 -0
  41. package/src/components/Form/Select.test.tsx +99 -0
  42. package/src/components/Form/Select.tsx +144 -93
  43. package/src/components/Form/SelectOption.tsx +88 -0
  44. package/src/components/Hero/Hero.stories.tsx +37 -0
  45. package/src/components/Hero/Hero.test.tsx +142 -0
  46. package/src/components/Hero/Hero.tsx +142 -3
  47. package/src/components/List/List.test.tsx +62 -0
  48. package/src/components/List/List.tsx +16 -5
  49. package/src/components/List/ListItem.tsx +20 -0
  50. package/src/components/Modal/Modal.stories.tsx +65 -1
  51. package/src/components/Modal/Modal.tsx +115 -35
  52. package/src/components/Modal/ModalCompound.test.tsx +94 -0
  53. package/src/components/Steps/Steps.tsx +124 -21
  54. package/src/components/Steps/StepsCompound.test.tsx +81 -0
  55. package/src/components/Tabs/Tabs.tsx +197 -44
  56. package/src/components/Tabs/TabsCompound.test.tsx +64 -0
  57. package/src/lib/composables/index.ts +0 -4
  58. package/src/lib/composables/useAtomixGlass.ts +0 -15
  59. package/src/lib/theme/devtools/CLI.ts +2 -10
  60. package/src/lib/types/components.ts +8 -2
  61. package/src/lib/utils/__tests__/componentUtils.test.ts +57 -2
  62. package/src/lib/utils/__tests__/themeNaming.test.ts +117 -0
  63. package/src/lib/utils/themeNaming.ts +1 -1
  64. package/src/styles/02-tools/_tools.breakpoints.scss +1 -1
  65. package/src/styles/02-tools/_tools.utility-api.scss +6 -6
  66. package/src/styles/99-utilities/_utilities.text.scss +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shohojdhara/atomix",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Atomix Design System - A modern component library for web applications",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -302,4 +302,4 @@
302
302
  "publishConfig": {
303
303
  "access": "public"
304
304
  }
305
- }
305
+ }
@@ -78,6 +78,15 @@ const packageJson = JSON.parse(
78
78
  // CLI Configuration
79
79
  const DEBUG = process.env.ATOMIX_DEBUG === 'true' || process.argv.includes('--debug');
80
80
 
81
+ const SENSITIVE_KEYS = /password|secret|token|api[-_]?key|access[-_]?key|auth[-_]?token|authorization|credential/i;
82
+
83
+ function sensitiveDataReplacer(key, value) {
84
+ if (key && SENSITIVE_KEYS.test(key)) {
85
+ return '***REDACTED***';
86
+ }
87
+ return value;
88
+ }
89
+
81
90
  /**
82
91
  * Debug logger
83
92
  */
@@ -85,7 +94,7 @@ function debug(message, data = null) {
85
94
  if (DEBUG) {
86
95
  console.log(chalk.gray(`[DEBUG] ${message}`));
87
96
  if (data) {
88
- console.log(chalk.gray(JSON.stringify(data, null, 2),));
97
+ console.log(chalk.gray(JSON.stringify(data, sensitiveDataReplacer, 2)));
89
98
  }
90
99
  }
91
100
  }
@@ -115,12 +115,16 @@ describe('CLI Utils', () => {
115
115
  'command | pipe',
116
116
  '`substitution`',
117
117
  '$(command)',
118
- 'input > /dev/null'
118
+ 'input > /dev/null',
119
+ '"quoted"',
120
+ "'singlequoted'",
121
+ '" $(whoami) "',
122
+ "' ; rm -rf / '"
119
123
  ];
120
124
 
121
125
  dangerousInputs.forEach(input => {
122
126
  const sanitized = sanitizeInput(input);
123
- expect(sanitized).not.toMatch(/[;&|`$<>]/);
127
+ expect(sanitized).not.toMatch(/[;&|`$<>\\"']/);
124
128
  });
125
129
  });
126
130
 
@@ -3,7 +3,7 @@
3
3
  * Helps migrate from other design systems and CSS frameworks
4
4
  */
5
5
 
6
- import { readFile, writeFile, readdir, stat } from 'fs/promises';
6
+ import { readFile, writeFile, readdir, lstat } from 'fs/promises';
7
7
  import { join, extname, relative } from 'path';
8
8
  import chalk from 'chalk';
9
9
  import ora from 'ora';
@@ -329,7 +329,7 @@ async function getAllFiles(dir, extensions = []) {
329
329
 
330
330
  for (const entry of entries) {
331
331
  const fullPath = join(currentPath, entry);
332
- const stats = await stat(fullPath);
332
+ const stats = await lstat(fullPath);
333
333
 
334
334
  if (stats.isDirectory()) {
335
335
  // Skip node_modules and hidden directories
@@ -3,11 +3,11 @@
3
3
  *
4
4
  * Bridges the TypeScript theme devtools CLI (src/lib/theme/devtools/CLI.ts) with the main JavaScript CLI.
5
5
  *
6
- * @dependency ts-node
6
+ * @dependency tsx
7
7
  * Theme subcommands (atomix theme validate|list|inspect|compare|export|create) run the TypeScript
8
- * theme CLI via ts-node. Ensure ts-node is installed in the project when using these commands:
9
- * npm install --save-dev ts-node
10
- * If ts-node is missing, theme subcommands will fail; run `atomix doctor` to check availability.
8
+ * theme CLI via tsx. Ensure tsx is installed in the project when using these commands:
9
+ * npm install --save-dev tsx
10
+ * If tsx is missing, theme subcommands will fail; run `atomix doctor` to check availability.
11
11
  */
12
12
 
13
13
  import { spawn } from 'child_process';
@@ -29,13 +29,11 @@ export async function executeThemeCommand(command, args = [], options = {}) {
29
29
  // Path to the theme CLI
30
30
  const themeCliPath = join(__dirname, '../../src/lib/theme/devtools/CLI.ts');
31
31
 
32
- // Use ts-node to execute TypeScript CLI
33
- const tsNodePath = join(__dirname, '../../node_modules/.bin/ts-node');
32
+ // Use tsx to execute TypeScript CLI
33
+ const tsxPath = join(__dirname, '../../node_modules/.bin/tsx');
34
34
 
35
35
  return new Promise((resolve, reject) => {
36
- const child = spawn(tsNodePath, [
37
- '--esm',
38
- '--experimental-specifier-resolution=node',
36
+ const child = spawn(tsxPath, [
39
37
  themeCliPath,
40
38
  command,
41
39
  ...args
@@ -175,8 +175,9 @@ export function sanitizeInput(input) {
175
175
  }
176
176
 
177
177
  // Remove any shell metacharacters that could be dangerous
178
+ // Added single and double quotes to the blacklist to prevent shell injection
178
179
  return input
179
- .replace(/[;&|`$<>\\]/g, '')
180
+ .replace(/[;&|`$<>\\"']/g, '')
180
181
  .replace(/\0/g, '') // Remove null bytes
181
182
  .trim();
182
183
  }
@@ -63,6 +63,7 @@ The Accordion component provides an expandable/collapsible container for content
63
63
  - Glass morphism effect support
64
64
  - Keyboard navigation support
65
65
  - Disabled state handling
66
+ - **Compound Component Pattern** (new)
66
67
 
67
68
  ## Accessibility
68
69
 
@@ -81,6 +82,19 @@ The Accordion component provides an expandable/collapsible container for content
81
82
  </Accordion>
82
83
  \`\`\`
83
84
 
85
+ ### Compound Component Usage
86
+
87
+ \`\`\`tsx
88
+ <Accordion>
89
+ <Accordion.Header>
90
+ <span>Custom Header Content</span>
91
+ </Accordion.Header>
92
+ <Accordion.Body>
93
+ <p>Content goes here</p>
94
+ </Accordion.Body>
95
+ </Accordion>
96
+ \`\`\`
97
+
84
98
  ### With Custom Icon
85
99
 
86
100
  \`\`\`tsx
@@ -266,6 +280,32 @@ export const WithAllProps: Story = {
266
280
  },
267
281
  };
268
282
 
283
+ export const CompoundUsage: Story = {
284
+ render: args => (
285
+ <Accordion {...args}>
286
+ <Accordion.Header>
287
+ <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
288
+ <strong>Custom Header Layout</strong>
289
+ <span style={{ fontSize: '0.8em', color: '#666' }}>(with subtitle)</span>
290
+ </div>
291
+ </Accordion.Header>
292
+ <Accordion.Body>
293
+ <p>
294
+ This accordion uses the Compound Component pattern (Accordion.Header and Accordion.Body).
295
+ This allows for complete control over the header layout and content structure.
296
+ </p>
297
+ </Accordion.Body>
298
+ </Accordion>
299
+ ),
300
+ parameters: {
301
+ docs: {
302
+ description: {
303
+ story: 'Demonstrates the Compound Component usage pattern for custom header layouts.',
304
+ },
305
+ },
306
+ },
307
+ };
308
+
269
309
  // ============================================================================
270
310
  // VARIANTS & STATES STORIES
271
311
  // ============================================================================
@@ -1,4 +1,4 @@
1
- import React, { ReactNode, useId, memo } from 'react';
1
+ import React, { ReactNode, useId, memo, forwardRef } from 'react';
2
2
  import { ACCORDION } from '../../lib/constants/components';
3
3
  import { useAccordion } from '../../lib/composables/useAccordion';
4
4
  import type {
@@ -9,7 +9,105 @@ import { AtomixGlass } from '../AtomixGlass/AtomixGlass';
9
9
 
10
10
  export type AccordionProps = AccordionPropsType;
11
11
 
12
- export const Accordion: React.FC<AccordionProps> = memo(
12
+ // Default icon
13
+ const DefaultIcon = () => (
14
+ <i className="c-accordion__icon" aria-hidden="true">
15
+ <svg
16
+ xmlns="http://www.w3.org/2000/svg"
17
+ width="24"
18
+ height="24"
19
+ viewBox="0 0 24 24"
20
+ fill="none"
21
+ stroke="currentColor"
22
+ strokeWidth="2"
23
+ strokeLinecap="round"
24
+ strokeLinejoin="round"
25
+ aria-hidden="true"
26
+ focusable="false"
27
+ >
28
+ <polyline points="6 9 12 15 18 9"></polyline>
29
+ </svg>
30
+ </i>
31
+ );
32
+
33
+ export interface AccordionHeaderProps extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'title'> {
34
+ title?: ReactNode;
35
+ icon?: ReactNode;
36
+ iconPosition?: 'left' | 'right';
37
+ isOpen?: boolean;
38
+ }
39
+
40
+ export const AccordionHeader = forwardRef<HTMLButtonElement, AccordionHeaderProps>(
41
+ (
42
+ { title, icon, iconPosition = 'right', isOpen, children, className = '', ...props },
43
+ ref
44
+ ) => {
45
+ // Determine icon to render. Explicit check for undefined to allow null/false to hide icon.
46
+ const iconElement = icon === undefined ? <DefaultIcon /> : icon;
47
+
48
+ return (
49
+ <button
50
+ ref={ref}
51
+ type="button"
52
+ className={className} // Parent injects the class names
53
+ {...props}
54
+ >
55
+ {title && <span className="c-accordion__title">{title}</span>}
56
+ {children}
57
+ {iconElement}
58
+ </button>
59
+ );
60
+ }
61
+ );
62
+ AccordionHeader.displayName = 'AccordionHeader';
63
+
64
+ export interface AccordionBodyProps extends React.HTMLAttributes<HTMLDivElement> {
65
+ panelRef?: React.RefObject<HTMLDivElement>;
66
+ contentRef?: React.RefObject<HTMLDivElement>;
67
+ }
68
+
69
+ // Helper to merge refs
70
+ function mergeRefs<T = any>(...refs: (React.MutableRefObject<T> | React.LegacyRef<T> | undefined | null)[]) {
71
+ return (node: T) => {
72
+ refs.forEach((ref) => {
73
+ if (typeof ref === 'function') {
74
+ ref(node);
75
+ } else if (ref != null) {
76
+ (ref as React.MutableRefObject<T | null>).current = node;
77
+ }
78
+ });
79
+ };
80
+ }
81
+
82
+ export const AccordionBody = forwardRef<HTMLDivElement, AccordionBodyProps>(
83
+ ({ children, className = '', panelRef, contentRef, ...props }, ref) => {
84
+ const mergedPanelRef = React.useMemo(() => mergeRefs(ref, panelRef), [ref, panelRef]);
85
+
86
+ return (
87
+ <div
88
+ ref={mergedPanelRef}
89
+ className={className} // Parent injects class names
90
+ role="region"
91
+ {...props}
92
+ >
93
+ <div
94
+ className={ACCORDION.SELECTORS.BODY.replace('.', '')}
95
+ ref={contentRef}
96
+ >
97
+ {children}
98
+ </div>
99
+ </div>
100
+ );
101
+ }
102
+ );
103
+ AccordionBody.displayName = 'AccordionBody';
104
+
105
+ type AccordionComponent = React.FC<AccordionProps> & {
106
+ Header: typeof AccordionHeader;
107
+ Body: typeof AccordionBody;
108
+ };
109
+
110
+ const AccordionImpl = memo(
13
111
  ({
14
112
  title,
15
113
  children,
@@ -24,7 +122,7 @@ export const Accordion: React.FC<AccordionProps> = memo(
24
122
  className = '',
25
123
  style,
26
124
  glass,
27
- }) => {
125
+ }: AccordionProps) => {
28
126
  // Generate unique IDs for accessibility
29
127
  const instanceId = useId();
30
128
  const buttonId = `accordion-header-${instanceId}`;
@@ -49,59 +147,75 @@ export const Accordion: React.FC<AccordionProps> = memo(
49
147
  onClose,
50
148
  });
51
149
 
52
- // Default icon
53
- const defaultIcon = (
54
- <i className="c-accordion__icon" aria-hidden="true">
55
- <svg
56
- xmlns="http://www.w3.org/2000/svg"
57
- width="24"
58
- height="24"
59
- viewBox="0 0 24 24"
60
- fill="none"
61
- stroke="currentColor"
62
- strokeWidth="2"
63
- strokeLinecap="round"
64
- strokeLinejoin="round"
65
- aria-hidden="true"
66
- focusable="false"
67
- >
68
- <polyline points="6 9 12 15 18 9"></polyline>
69
- </svg>
70
- </i>
150
+ const headerClassNames = generateHeaderClassNames();
151
+ const panelClassNames = ACCORDION.SELECTORS.PANEL.replace('.', '');
152
+
153
+ // Check for compound usage
154
+ const hasCompoundComponents = React.Children.toArray(children).some((child) =>
155
+ React.isValidElement(child) &&
156
+ ['AccordionHeader', 'AccordionBody'].includes((child.type as any).displayName)
71
157
  );
72
158
 
73
- const accordionContent = (
159
+ const content = (
74
160
  <div
75
161
  className={generateClassNames(className) + (glass ? ' c-accordion--glass' : '')}
76
162
  style={style}
77
163
  >
78
- <button
79
- id={buttonId}
80
- className={generateHeaderClassNames()}
81
- onClick={toggle}
82
- aria-expanded={state.isOpen}
83
- aria-controls={panelId}
84
- aria-disabled={disabled}
85
- disabled={disabled}
86
- type="button"
87
- >
88
- <span className="c-accordion__title">{title}</span>
89
- {icon || defaultIcon}
90
- </button>
91
- <div
92
- id={panelId}
93
- className={ACCORDION.SELECTORS.PANEL.replace('.', '')}
94
- ref={panelRef as React.RefObject<HTMLDivElement>}
95
- role="region"
96
- aria-labelledby={buttonId}
97
- >
98
- <div
99
- className={ACCORDION.SELECTORS.BODY.replace('.', '')}
100
- ref={contentRef as React.RefObject<HTMLDivElement>}
101
- >
102
- {children}
103
- </div>
104
- </div>
164
+ {hasCompoundComponents ? (
165
+ React.Children.map(children, child => {
166
+ if (React.isValidElement(child)) {
167
+ if ((child.type as any).displayName === 'AccordionHeader') {
168
+ return React.cloneElement(child, {
169
+ id: buttonId,
170
+ className: `${headerClassNames} ${(child.props as any).className || ''}`.trim(),
171
+ onClick: (e: React.MouseEvent) => {
172
+ toggle();
173
+ (child.props as any).onClick?.(e);
174
+ },
175
+ 'aria-expanded': state.isOpen,
176
+ 'aria-controls': panelId,
177
+ 'aria-disabled': disabled,
178
+ disabled: disabled,
179
+ iconPosition: (child.props as any).iconPosition || iconPosition,
180
+ } as any);
181
+ }
182
+ if ((child.type as any).displayName === 'AccordionBody') {
183
+ return React.cloneElement(child, {
184
+ id: panelId,
185
+ className: `${panelClassNames} ${(child.props as any).className || ''}`.trim(),
186
+ 'aria-labelledby': buttonId,
187
+ panelRef: panelRef,
188
+ contentRef: contentRef,
189
+ } as any);
190
+ }
191
+ }
192
+ return child;
193
+ })
194
+ ) : (
195
+ <>
196
+ <AccordionHeader
197
+ id={buttonId}
198
+ className={headerClassNames}
199
+ onClick={toggle}
200
+ aria-expanded={state.isOpen}
201
+ aria-controls={panelId}
202
+ aria-disabled={disabled}
203
+ disabled={disabled}
204
+ title={title}
205
+ icon={icon}
206
+ iconPosition={iconPosition}
207
+ />
208
+ <AccordionBody
209
+ id={panelId}
210
+ className={panelClassNames}
211
+ aria-labelledby={buttonId}
212
+ panelRef={panelRef as React.RefObject<HTMLDivElement>}
213
+ contentRef={contentRef as React.RefObject<HTMLDivElement>}
214
+ >
215
+ {children}
216
+ </AccordionBody>
217
+ </>
218
+ )}
105
219
  </div>
106
220
  );
107
221
 
@@ -114,15 +228,19 @@ export const Accordion: React.FC<AccordionProps> = memo(
114
228
 
115
229
  const glassProps = glass === true ? defaultGlassProps : { ...defaultGlassProps, ...glass };
116
230
 
117
- return <AtomixGlass {...glassProps}>{accordionContent}</AtomixGlass>;
231
+ return <AtomixGlass {...glassProps}>{content}</AtomixGlass>;
118
232
  }
119
233
 
120
- return accordionContent;
234
+ return content;
121
235
  }
122
236
  );
123
237
 
124
- // Set display name for debugging
125
- Accordion.displayName = 'Accordion';
238
+ AccordionImpl.displayName = 'Accordion';
239
+
240
+ // Attach subcomponents
241
+ const AccordionWithSubcomponents = AccordionImpl as unknown as AccordionComponent;
242
+ AccordionWithSubcomponents.Header = AccordionHeader;
243
+ AccordionWithSubcomponents.Body = AccordionBody;
126
244
 
127
- // Export as default
128
- export default Accordion;
245
+ export const Accordion = AccordionWithSubcomponents;
246
+ export default Accordion as unknown as AccordionComponent;
@@ -0,0 +1,70 @@
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 legacy props', () => {
8
+ render(
9
+ <Accordion title="Legacy Title" defaultOpen>
10
+ Legacy Content
11
+ </Accordion>
12
+ );
13
+
14
+ expect(screen.getByText('Legacy Title')).toBeInTheDocument();
15
+ expect(screen.getByText('Legacy Content')).toBeInTheDocument();
16
+ });
17
+
18
+ it('renders correctly with compound components', () => {
19
+ render(
20
+ <Accordion defaultOpen>
21
+ <Accordion.Header>
22
+ <span>Compound Header</span>
23
+ </Accordion.Header>
24
+ <Accordion.Body>
25
+ <p>Compound Body</p>
26
+ </Accordion.Body>
27
+ </Accordion>
28
+ );
29
+
30
+ expect(screen.getByText('Compound Header')).toBeInTheDocument();
31
+ expect(screen.getByText('Compound Body')).toBeInTheDocument();
32
+ });
33
+
34
+ it('toggles visibility in compound mode', () => {
35
+ render(
36
+ <Accordion>
37
+ <Accordion.Header>Header</Accordion.Header>
38
+ <Accordion.Body>Body</Accordion.Body>
39
+ </Accordion>
40
+ );
41
+
42
+ const button = screen.getByRole('button');
43
+ expect(button).toHaveAttribute('aria-expanded', 'false');
44
+
45
+ fireEvent.click(button);
46
+ expect(button).toHaveAttribute('aria-expanded', 'true');
47
+ });
48
+
49
+ it('injects props into compound components', () => {
50
+ // We want to verify that aria-controls and aria-labelledby are correctly linked
51
+ // even in compound mode (since the parent injects IDs)
52
+ render(
53
+ <Accordion>
54
+ <Accordion.Header>Header</Accordion.Header>
55
+ <Accordion.Body>Body</Accordion.Body>
56
+ </Accordion>
57
+ );
58
+
59
+ const button = screen.getByRole('button');
60
+ const region = screen.getByRole('region');
61
+
62
+ const controlsId = button.getAttribute('aria-controls');
63
+ const labelledById = region.getAttribute('aria-labelledby');
64
+ const buttonId = button.getAttribute('id');
65
+ const regionId = region.getAttribute('id');
66
+
67
+ expect(controlsId).toBe(regionId);
68
+ expect(labelledById).toBe(buttonId);
69
+ });
70
+ });