@shohojdhara/atomix 0.3.4 → 0.3.6

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 (237) hide show
  1. package/README.md +101 -199
  2. package/atomix.config.ts +241 -0
  3. package/dist/atomix.css +269 -189
  4. package/dist/atomix.css.map +1 -0
  5. package/dist/atomix.min.css +15179 -11
  6. package/dist/atomix.min.css.map +1 -0
  7. package/dist/charts.d.ts +1929 -0
  8. package/dist/charts.js +6477 -0
  9. package/dist/charts.js.map +1 -0
  10. package/dist/core.d.ts +1289 -0
  11. package/dist/core.js +3373 -0
  12. package/dist/core.js.map +1 -0
  13. package/dist/forms.d.ts +1085 -0
  14. package/dist/forms.js +2466 -0
  15. package/dist/forms.js.map +1 -0
  16. package/dist/heavy.d.ts +636 -0
  17. package/dist/heavy.js +4566 -0
  18. package/dist/heavy.js.map +1 -0
  19. package/dist/index.d.ts +5171 -4792
  20. package/dist/index.esm.js +6098 -4563
  21. package/dist/index.esm.js.map +1 -1
  22. package/dist/index.js +6291 -4747
  23. package/dist/index.js.map +1 -1
  24. package/dist/index.min.js +1 -1
  25. package/dist/index.min.js.map +1 -1
  26. package/dist/layout.d.ts +300 -0
  27. package/dist/layout.js +336 -0
  28. package/dist/layout.js.map +1 -0
  29. package/dist/theme.d.ts +2122 -0
  30. package/dist/theme.js +6084 -0
  31. package/dist/theme.js.map +1 -0
  32. package/package.json +59 -27
  33. package/scripts/atomix-cli.js +544 -16
  34. package/scripts/cli/__tests__/cli-commands.test.js +204 -0
  35. package/scripts/cli/__tests__/utils.test.js +201 -0
  36. package/scripts/cli/__tests__/vitest.config.js +26 -0
  37. package/scripts/cli/interactive-init.js +1 -1
  38. package/scripts/cli/token-manager.js +32 -7
  39. package/scripts/cli/utils.js +347 -0
  40. package/src/components/Accordion/Accordion.stories.tsx +50 -17
  41. package/src/components/Accordion/Accordion.tsx +5 -54
  42. package/src/components/Accordion/index.ts +1 -1
  43. package/src/components/AtomixGlass/AtomixGlass.tsx +65 -31
  44. package/src/components/AtomixGlass/AtomixGlassContainer.tsx +11 -4
  45. package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +1 -32
  46. package/src/components/AtomixGlass/stories/Examples.stories.tsx +2 -2
  47. package/src/components/AtomixGlass/stories/shared-components.tsx +0 -31
  48. package/src/components/Avatar/Avatar.stories.tsx +7 -0
  49. package/src/components/Avatar/Avatar.tsx +3 -3
  50. package/src/components/Badge/Badge.stories.tsx +91 -13
  51. package/src/components/Badge/Badge.tsx +3 -3
  52. package/src/components/Block/Block.stories.tsx +7 -23
  53. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +7 -0
  54. package/src/components/Breadcrumb/Breadcrumb.tsx +3 -3
  55. package/src/components/Button/Button.stories.tsx +141 -22
  56. package/src/components/Button/ButtonGroup.stories.tsx +315 -0
  57. package/src/components/Button/ButtonGroup.tsx +67 -0
  58. package/src/components/Button/index.ts +2 -0
  59. package/src/components/Callout/Callout.stories.tsx +8 -6
  60. package/src/components/Card/Card.stories.tsx +82 -28
  61. package/src/components/Card/ElevationCard.tsx +1 -1
  62. package/src/components/Chart/AnimatedChart.tsx +19 -18
  63. package/src/components/Chart/AreaChart.tsx +5 -2
  64. package/src/components/Chart/BarChart.tsx +1 -1
  65. package/src/components/Chart/BubbleChart.tsx +6 -6
  66. package/src/components/Chart/CandlestickChart.tsx +0 -1
  67. package/src/components/Chart/Chart.stories.tsx +5 -7
  68. package/src/components/Chart/Chart.tsx +0 -16
  69. package/src/components/Chart/ChartRenderer.tsx +1 -1
  70. package/src/components/Chart/ChartToolbar.tsx +1 -0
  71. package/src/components/Chart/DonutChart.tsx +0 -1
  72. package/src/components/Chart/FunnelChart.tsx +1 -2
  73. package/src/components/Chart/GaugeChart.tsx +0 -1
  74. package/src/components/Chart/HeatmapChart.tsx +0 -1
  75. package/src/components/Chart/LineChart.tsx +0 -1
  76. package/src/components/Chart/MultiAxisChart.tsx +0 -1
  77. package/src/components/Chart/PieChart.tsx +0 -1
  78. package/src/components/Chart/RadarChart.tsx +19 -13
  79. package/src/components/Chart/ScatterChart.tsx +3 -4
  80. package/src/components/Chart/TreemapChart.tsx +2 -1
  81. package/src/components/Chart/WaterfallChart.tsx +0 -2
  82. package/src/components/Chart/types.ts +12 -2
  83. package/src/components/Chart/utils.ts +4 -3
  84. package/src/components/ColorModeToggle/ColorModeToggle.stories.tsx +7 -0
  85. package/src/components/DataTable/DataTable.stories.tsx +23 -16
  86. package/src/components/DataTable/DataTable.tsx +3 -3
  87. package/src/components/DatePicker/DatePicker.stories.tsx +27 -19
  88. package/src/components/Dropdown/Dropdown.stories.tsx +11 -19
  89. package/src/components/Dropdown/Dropdown.tsx +12 -9
  90. package/src/components/EdgePanel/EdgePanel.stories.tsx +1 -0
  91. package/src/components/Footer/Footer.stories.tsx +8 -6
  92. package/src/components/Footer/FooterLink.tsx +9 -2
  93. package/src/components/Footer/FooterSection.tsx +3 -3
  94. package/src/components/Form/Checkbox.stories.tsx +7 -0
  95. package/src/components/Form/Checkbox.tsx +3 -3
  96. package/src/components/Form/Form.stories.tsx +7 -0
  97. package/src/components/Form/FormGroup.stories.tsx +9 -1
  98. package/src/components/Form/Input.stories.tsx +69 -16
  99. package/src/components/Form/Input.tsx +4 -2
  100. package/src/components/Form/Radio.stories.tsx +9 -1
  101. package/src/components/Form/Radio.tsx +3 -3
  102. package/src/components/Form/Select.stories.tsx +9 -1
  103. package/src/components/Form/Select.tsx +3 -3
  104. package/src/components/Form/Textarea.stories.tsx +10 -2
  105. package/src/components/Form/Textarea.tsx +4 -2
  106. package/src/components/Hero/Hero.stories.tsx +7 -0
  107. package/src/components/List/List.stories.tsx +10 -3
  108. package/src/components/List/List.tsx +3 -3
  109. package/src/components/List/ListGroup.tsx +3 -1
  110. package/src/components/Messages/Messages.stories.tsx +8 -7
  111. package/src/components/Modal/Modal.stories.tsx +17 -6
  112. package/src/components/Modal/Modal.tsx +3 -3
  113. package/src/components/Navigation/Menu/MegaMenu.tsx +9 -3
  114. package/src/components/Navigation/Menu/Menu.stories.tsx +7 -0
  115. package/src/components/Navigation/Menu/Menu.tsx +9 -3
  116. package/src/components/Navigation/Nav/Nav.stories.tsx +7 -0
  117. package/src/components/Navigation/Navbar/Navbar.stories.tsx +1 -0
  118. package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +1 -1
  119. package/src/components/Pagination/Pagination.stories.tsx +188 -111
  120. package/src/components/Pagination/Pagination.tsx +88 -7
  121. package/src/components/PhotoViewer/PhotoViewer.stories.tsx +10 -5
  122. package/src/components/PhotoViewer/PhotoViewerImage.tsx +2 -2
  123. package/src/components/Popover/Popover.stories.tsx +191 -115
  124. package/src/components/Popover/Popover.tsx +4 -4
  125. package/src/components/ProductReview/ProductReview.stories.tsx +80 -58
  126. package/src/components/Progress/Progress.stories.tsx +79 -49
  127. package/src/components/Progress/Progress.tsx +6 -2
  128. package/src/components/Rating/Rating.stories.tsx +109 -84
  129. package/src/components/Rating/Rating.tsx +5 -2
  130. package/src/components/River/River.stories.tsx +194 -114
  131. package/src/components/SectionIntro/SectionIntro.stories.tsx +19 -9
  132. package/src/components/Slider/Slider.stories.tsx +7 -0
  133. package/src/components/Slider/Slider.tsx +10 -9
  134. package/src/components/Spinner/Spinner.stories.tsx +15 -11
  135. package/src/components/Spinner/Spinner.tsx +3 -3
  136. package/src/components/Steps/Steps.stories.tsx +132 -98
  137. package/src/components/Tabs/Tabs.stories.tsx +163 -112
  138. package/src/components/Tabs/Tabs.tsx +3 -3
  139. package/src/components/Testimonial/Testimonial.stories.tsx +114 -68
  140. package/src/components/Todo/Todo.stories.tsx +38 -12
  141. package/src/components/Toggle/Toggle.stories.tsx +61 -28
  142. package/src/components/Tooltip/Tooltip.stories.tsx +318 -200
  143. package/src/components/Tooltip/Tooltip.tsx +3 -3
  144. package/src/components/Upload/Upload.stories.tsx +122 -84
  145. package/src/components/VideoPlayer/VideoPlayer.stories.tsx +7 -24
  146. package/src/components/index.ts +6 -2
  147. package/src/layouts/MasonryGrid/MasonryGrid.tsx +2 -2
  148. package/src/lib/composables/useAtomixGlass.ts +2 -3
  149. package/src/lib/composables/useChartPerformance.ts +102 -78
  150. package/src/lib/composables/useChartScale.ts +10 -0
  151. package/src/lib/composables/useHero.ts +9 -2
  152. package/src/lib/composables/useHeroBackgroundSlider.ts +5 -3
  153. package/src/lib/composables/useNavbar.ts +0 -10
  154. package/src/lib/composables/useSideMenu.ts +1 -0
  155. package/src/lib/composables/useVideoPlayer.ts +3 -2
  156. package/src/lib/config/loader.ts +57 -14
  157. package/src/lib/constants/components.ts +10 -0
  158. package/src/lib/hooks/index.ts +0 -1
  159. package/src/lib/hooks/useComponentCustomization.ts +11 -15
  160. package/src/lib/hooks/usePerformanceMonitor.ts +149 -0
  161. package/src/lib/patterns/index.ts +2 -2
  162. package/src/lib/patterns/slots.tsx +2 -2
  163. package/src/lib/theme/README.md +174 -0
  164. package/src/lib/theme/adapters/index.ts +31 -0
  165. package/src/lib/theme/adapters/themeAdapter.ts +287 -0
  166. package/src/lib/theme/config/__tests__/configLoader.test.ts +207 -0
  167. package/src/lib/theme/config/configLoader.ts +254 -0
  168. package/src/lib/theme/config/loader.ts +37 -48
  169. package/src/lib/theme/config/types.ts +2 -2
  170. package/src/lib/theme/config/validator.ts +15 -91
  171. package/src/lib/theme/{constants.ts → constants/constants.ts} +0 -18
  172. package/src/lib/theme/constants/index.ts +8 -0
  173. package/src/lib/theme/core/ThemeRegistry.ts +19 -6
  174. package/src/lib/theme/core/__tests__/createTheme.test.ts +132 -0
  175. package/src/lib/theme/core/composeTheme.ts +155 -0
  176. package/src/lib/theme/core/createTheme.ts +94 -0
  177. package/src/lib/theme/{createTheme.ts → core/createThemeObject.ts} +10 -6
  178. package/src/lib/theme/core/index.ts +5 -19
  179. package/src/lib/theme/devtools/Comparator.tsx +346 -22
  180. package/src/lib/theme/devtools/IMPROVEMENTS.md +139 -38
  181. package/src/lib/theme/devtools/Inspector.tsx +335 -51
  182. package/src/lib/theme/devtools/LiveEditor.tsx +489 -112
  183. package/src/lib/theme/devtools/Preview.tsx +471 -221
  184. package/src/lib/theme/{core → devtools}/ThemeValidator.ts +6 -3
  185. package/src/lib/theme/devtools/index.ts +14 -4
  186. package/src/lib/theme/devtools/useHistory.ts +130 -0
  187. package/src/lib/theme/errors/index.ts +12 -0
  188. package/src/lib/theme/generators/cssFile.ts +79 -0
  189. package/src/lib/theme/generators/generateCSS.ts +89 -0
  190. package/src/lib/theme/{generateCSSVariables.ts → generators/generateCSSVariables.ts} +4 -14
  191. package/src/lib/theme/generators/index.ts +19 -0
  192. package/src/lib/theme/i18n/rtl.ts +7 -7
  193. package/src/lib/theme/index.ts +120 -15
  194. package/src/lib/theme/runtime/ThemeApplicator.ts +53 -95
  195. package/src/lib/theme/{ThemeContext.tsx → runtime/ThemeContext.tsx} +1 -1
  196. package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +4 -4
  197. package/src/lib/theme/runtime/ThemeProvider.tsx +456 -179
  198. package/src/lib/theme/runtime/index.ts +1 -2
  199. package/src/lib/theme/runtime/useTheme.ts +1 -2
  200. package/src/lib/theme/test/testTheme.ts +385 -0
  201. package/src/lib/theme/tokens/index.ts +12 -0
  202. package/src/lib/theme/tokens/tokens.ts +721 -0
  203. package/src/lib/theme/types.ts +6 -42
  204. package/src/lib/theme/{utils.ts → utils/domUtils.ts} +2 -2
  205. package/src/lib/theme/utils/index.ts +11 -0
  206. package/src/lib/theme/utils/injectCSS.ts +90 -0
  207. package/src/lib/theme/utils/themeHelpers.ts +78 -0
  208. package/src/lib/theme/{themeUtils.ts → utils/themeUtils.ts} +1 -1
  209. package/src/lib/theme-tools.ts +8 -9
  210. package/src/lib/types/components.ts +93 -34
  211. package/src/lib/types/partProps.ts +0 -16
  212. package/src/lib/utils/componentUtils.ts +1 -1
  213. package/src/lib/utils/fontPreloader.ts +148 -0
  214. package/src/lib/utils/index.ts +11 -0
  215. package/src/lib/utils/memoryMonitor.ts +189 -0
  216. package/src/styles/01-settings/_settings.design-tokens.scss +4 -1
  217. package/src/styles/01-settings/_settings.fonts.scss +2 -5
  218. package/src/styles/02-tools/_tools.button.scss +66 -79
  219. package/src/styles/06-components/_components.atomix-glass.scss +13 -3
  220. package/src/styles/06-components/_components.navbar.scss +0 -6
  221. package/src/styles/06-components/_components.pagination.scss +88 -0
  222. package/scripts/build-themes.js +0 -208
  223. package/scripts/sync-theme-config.js +0 -309
  224. package/src/components/AtomixGlass/atomixGLass.old.tsx +0 -1263
  225. package/src/lib/theme/composeTheme.ts +0 -370
  226. package/src/lib/theme/core/ThemeCache.ts +0 -283
  227. package/src/lib/theme/core/ThemeEngine.test.ts +0 -146
  228. package/src/lib/theme/core/ThemeEngine.ts +0 -657
  229. package/src/lib/theme/createThemeFromConfig.ts +0 -132
  230. package/src/lib/theme/devtools/CLI.ts +0 -364
  231. package/src/lib/theme/runtime/ThemeManager.test.ts +0 -192
  232. package/src/lib/theme/runtime/ThemeManager.ts +0 -442
  233. package/src/styles/03-generic/_generated-root.css +0 -5
  234. package/src/themes/README.md +0 -442
  235. package/src/themes/themes.config.js +0 -35
  236. /package/src/lib/theme/{cssVariableMapper.ts → adapters/cssVariableMapper.ts} +0 -0
  237. /package/src/lib/theme/{errors.ts → errors/errors.ts} +0 -0
@@ -2,12 +2,14 @@
2
2
  * Theme Live Editor Component
3
3
  *
4
4
  * React component for live editing themes in development
5
+ * Enhanced with undo/redo, keyboard shortcuts, resizable layout, and better color pickers
5
6
  */
6
7
 
7
- import React, { useState, useCallback } from 'react';
8
+ import React, { useState, useCallback, useEffect, useRef } from 'react';
8
9
  import type { Theme } from '../types';
9
- import { createTheme } from '../createTheme';
10
+ import { createThemeObject } from '../core/createThemeObject';
10
11
  import { ThemePreview } from './Preview';
12
+ import { useHistory } from './useHistory';
11
13
 
12
14
  /**
13
15
  * Live editor props
@@ -23,6 +25,92 @@ export interface ThemeLiveEditorProps {
23
25
  style?: React.CSSProperties;
24
26
  }
25
27
 
28
+ /**
29
+ * Color format type
30
+ */
31
+ type ColorFormat = 'hex' | 'rgb' | 'rgba' | 'hsl' | 'hsla';
32
+
33
+ /**
34
+ * Convert color to different formats
35
+ */
36
+ function convertColorFormat(color: string, format: ColorFormat): string {
37
+ // Remove whitespace
38
+ color = color.trim();
39
+
40
+ // If already in target format, return as is
41
+ if (format === 'hex' && color.startsWith('#')) return color;
42
+ if (format === 'rgb' && color.startsWith('rgb(')) return color;
43
+ if (format === 'rgba' && color.startsWith('rgba(')) return color;
44
+ if (format === 'hsl' && color.startsWith('hsl(')) return color;
45
+ if (format === 'hsla' && color.startsWith('hsla(')) return color;
46
+
47
+ // Create a temporary element to parse color
48
+ const temp = document.createElement('div');
49
+ temp.style.color = color;
50
+ document.body.appendChild(temp);
51
+ const computed = window.getComputedStyle(temp).color;
52
+ document.body.removeChild(temp);
53
+
54
+ // Parse rgb values
55
+ const rgbMatch = computed.match(/\d+/g);
56
+ if (!rgbMatch || rgbMatch.length < 3) return color;
57
+
58
+ const r = parseInt(rgbMatch[0], 10);
59
+ const g = parseInt(rgbMatch[1] || '0', 10);
60
+ const b = parseInt(rgbMatch[2] || '0', 10);
61
+ const a = rgbMatch[3] ? parseFloat(rgbMatch[3]) : 1;
62
+
63
+ switch (format) {
64
+ case 'hex':
65
+ return `#${[r, g, b].map(x => x.toString(16).padStart(2, '0')).join('')}`;
66
+ case 'rgb':
67
+ return `rgb(${r}, ${g}, ${b})`;
68
+ case 'rgba':
69
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
70
+ case 'hsl':
71
+ case 'hsla':
72
+ // Convert RGB to HSL (simplified)
73
+ const hsl = rgbToHsl(r, g, b);
74
+ return format === 'hsl'
75
+ ? `hsl(${hsl.h}, ${hsl.s}%, ${hsl.l}%)`
76
+ : `hsla(${hsl.h}, ${hsl.s}%, ${hsl.l}%, ${a})`;
77
+ default:
78
+ return color;
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Convert RGB to HSL
84
+ */
85
+ function rgbToHsl(r: number, g: number, b: number): { h: number; s: number; l: number } {
86
+ r /= 255;
87
+ g /= 255;
88
+ b /= 255;
89
+
90
+ const max = Math.max(r, g, b);
91
+ const min = Math.min(r, g, b);
92
+ let h = 0;
93
+ let s = 0;
94
+ const l = (max + min) / 2;
95
+
96
+ if (max !== min) {
97
+ const d = max - min;
98
+ s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
99
+
100
+ switch (max) {
101
+ case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
102
+ case g: h = ((b - r) / d + 2) / 6; break;
103
+ case b: h = ((r - g) / d + 4) / 6; break;
104
+ }
105
+ }
106
+
107
+ return {
108
+ h: Math.round(h * 360),
109
+ s: Math.round(s * 100),
110
+ l: Math.round(l * 100),
111
+ };
112
+ }
113
+
26
114
  /**
27
115
  * Theme Live Editor Component
28
116
  *
@@ -34,45 +122,119 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
34
122
  className,
35
123
  style,
36
124
  }) => {
37
- const [theme, setTheme] = useState<Theme>(initialTheme);
125
+ const {
126
+ state: theme,
127
+ setState: setThemeHistory,
128
+ undo,
129
+ redo,
130
+ canUndo,
131
+ canRedo,
132
+ } = useHistory<Theme>({
133
+ initialState: initialTheme,
134
+ maxHistorySize: 50,
135
+ });
136
+
38
137
  const [jsonInput, setJsonInput] = useState<string>(JSON.stringify(initialTheme, null, 2));
39
138
  const [error, setError] = useState<string | null>(null);
40
139
  const [editMode, setEditMode] = useState<'visual' | 'json'>('visual');
41
-
42
- const updateTheme = useCallback((newTheme: Theme) => {
43
- setTheme(newTheme);
44
- setJsonInput(JSON.stringify(newTheme, null, 2));
140
+ const [colorFormat, setColorFormat] = useState<ColorFormat>('hex');
141
+ const [resizerPosition, setResizerPosition] = useState<number>(50); // Percentage
142
+ const [isResizing, setIsResizing] = useState(false);
143
+ const editorRef = useRef<HTMLDivElement>(null);
144
+ const previewRef = useRef<HTMLDivElement>(null);
145
+
146
+ // Load saved layout preference
147
+ useEffect(() => {
148
+ const saved = localStorage.getItem('atomix-editor-layout');
149
+ if (saved) {
150
+ const position = parseFloat(saved);
151
+ if (!isNaN(position) && position > 0 && position < 100) {
152
+ setResizerPosition(position);
153
+ }
154
+ }
155
+ }, []);
156
+
157
+ // Save layout preference
158
+ useEffect(() => {
159
+ localStorage.setItem('atomix-editor-layout', resizerPosition.toString());
160
+ }, [resizerPosition]);
161
+
162
+ const updateTheme = useCallback((newTheme: Theme, addToHistory = true) => {
163
+ if (addToHistory) {
164
+ setThemeHistory(newTheme);
165
+ } else {
166
+ // Direct update without history (for JSON editor typing)
167
+ setJsonInput(JSON.stringify(newTheme, null, 2));
168
+ }
45
169
  onChange?.(newTheme);
46
170
  setError(null);
47
- }, [onChange]);
171
+ }, [onChange, setThemeHistory]);
172
+
173
+ // Sync JSON input with theme history
174
+ useEffect(() => {
175
+ setJsonInput(JSON.stringify(theme, null, 2));
176
+ }, [theme]);
48
177
 
49
178
  const handleJsonChange = useCallback((value: string) => {
50
179
  setJsonInput(value);
51
180
  try {
52
181
  const parsed = JSON.parse(value);
53
- const newTheme = createTheme(parsed);
54
- setTheme(newTheme);
55
- onChange?.(newTheme);
56
- setError(null);
182
+ const newTheme = createThemeObject(parsed);
183
+ updateTheme(newTheme, false); // Don't add to history on every keystroke
57
184
  } catch (err) {
58
185
  setError(err instanceof Error ? err.message : 'Invalid JSON');
59
186
  }
60
- }, [onChange]);
187
+ }, [updateTheme]);
188
+
189
+ // Debounced JSON update to history
190
+ const jsonUpdateTimeoutRef = useRef<NodeJS.Timeout>();
191
+ useEffect(() => {
192
+ if (error) return;
193
+
194
+ try {
195
+ const parsed = JSON.parse(jsonInput);
196
+ const newTheme = createThemeObject(parsed);
197
+
198
+ // Clear existing timeout
199
+ if (jsonUpdateTimeoutRef.current) {
200
+ clearTimeout(jsonUpdateTimeoutRef.current);
201
+ }
202
+
203
+ // Add to history after 1 second of no typing
204
+ jsonUpdateTimeoutRef.current = setTimeout(() => {
205
+ setThemeHistory(newTheme);
206
+ }, 1000);
207
+ } catch {
208
+ // Invalid JSON, don't update
209
+ }
210
+
211
+ return () => {
212
+ if (jsonUpdateTimeoutRef.current) {
213
+ clearTimeout(jsonUpdateTimeoutRef.current);
214
+ }
215
+ };
216
+ }, [jsonInput, error, setThemeHistory]);
61
217
 
62
- const handleColorChange = useCallback((path: string, value: string) => {
218
+ const handleColorChange = useCallback((path: string, value: string | number) => {
63
219
  const newTheme = { ...theme };
64
220
  const keys = path.split('.');
65
221
  let current: any = newTheme;
66
222
 
67
223
  for (let i = 0; i < keys.length - 1; i++) {
68
- if (!current[keys[i]]) {
69
- current[keys[i]] = {};
224
+ const key = keys[i];
225
+ if (key && !current[key]) {
226
+ current[key] = {};
227
+ }
228
+ if (key) {
229
+ current = current[key];
70
230
  }
71
- current = current[keys[i]];
72
231
  }
73
232
 
74
- current[keys[keys.length - 1]] = value;
75
- updateTheme(createTheme(newTheme));
233
+ const lastKey = keys[keys.length - 1];
234
+ if (lastKey) {
235
+ current[lastKey] = value;
236
+ }
237
+ updateTheme(createThemeObject(newTheme));
76
238
  }, [theme, updateTheme]);
77
239
 
78
240
  const exportTheme = useCallback(() => {
@@ -90,114 +252,234 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
90
252
  navigator.clipboard?.writeText(jsonInput);
91
253
  }, [jsonInput]);
92
254
 
255
+ // Keyboard shortcuts
256
+ useEffect(() => {
257
+ const handleKeyDown = (e: KeyboardEvent) => {
258
+ const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0;
259
+ const ctrlKey = isMac ? e.metaKey : e.ctrlKey;
260
+
261
+ if (ctrlKey && e.key === 'z' && !e.shiftKey) {
262
+ e.preventDefault();
263
+ if (canUndo) undo();
264
+ } else if (ctrlKey && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
265
+ e.preventDefault();
266
+ if (canRedo) redo();
267
+ } else if (ctrlKey && e.key === 's') {
268
+ e.preventDefault();
269
+ exportTheme();
270
+ } else if (ctrlKey && e.key === '/') {
271
+ e.preventDefault();
272
+ setEditMode(prev => prev === 'visual' ? 'json' : 'visual');
273
+ } else if (e.key === 'Escape') {
274
+ setError(null);
275
+ }
276
+ };
277
+
278
+ window.addEventListener('keydown', handleKeyDown);
279
+ return () => window.removeEventListener('keydown', handleKeyDown);
280
+ }, [canUndo, canRedo, undo, redo, exportTheme]);
281
+
282
+ // Resizer handlers
283
+ const handleResizeStart = useCallback((e: React.MouseEvent) => {
284
+ e.preventDefault();
285
+ setIsResizing(true);
286
+ }, []);
287
+
288
+ useEffect(() => {
289
+ if (!isResizing) return;
290
+
291
+ const handleMouseMove = (e: MouseEvent) => {
292
+ if (!editorRef.current || !previewRef.current) return;
293
+
294
+ const container = editorRef.current.parentElement;
295
+ if (!container) return;
296
+
297
+ const containerRect = container.getBoundingClientRect();
298
+ const newPosition = ((e.clientX - containerRect.left) / containerRect.width) * 100;
299
+
300
+ // Constrain between 20% and 80%
301
+ const constrainedPosition = Math.max(20, Math.min(80, newPosition));
302
+ setResizerPosition(constrainedPosition);
303
+ };
304
+
305
+ const handleMouseUp = () => {
306
+ setIsResizing(false);
307
+ };
308
+
309
+ window.addEventListener('mousemove', handleMouseMove);
310
+ window.addEventListener('mouseup', handleMouseUp);
311
+
312
+ return () => {
313
+ window.removeEventListener('mousemove', handleMouseMove);
314
+ window.removeEventListener('mouseup', handleMouseUp);
315
+ };
316
+ }, [isResizing]);
317
+
318
+ const getColorValue = useCallback((color: string): string => {
319
+ return convertColorFormat(color, colorFormat);
320
+ }, [colorFormat]);
321
+
93
322
  return (
94
323
  <div className={`atomix-theme-live-editor ${className || ''}`} style={style}>
95
324
  <div className="editor-header">
96
325
  <h2>Live Theme Editor</h2>
97
326
  <div className="editor-controls">
98
- <button
99
- className={`mode-button ${editMode === 'visual' ? 'active' : ''}`}
100
- onClick={() => setEditMode('visual')}
101
- >
102
- Visual
103
- </button>
104
- <button
105
- className={`mode-button ${editMode === 'json' ? 'active' : ''}`}
106
- onClick={() => setEditMode('json')}
107
- >
108
- JSON
109
- </button>
110
- <button className="export-button" onClick={exportTheme}>
111
- Export
112
- </button>
113
- <button className="copy-button" onClick={copyToClipboard}>
114
- Copy JSON
115
- </button>
327
+ <div className="history-controls">
328
+ <button
329
+ className="history-button"
330
+ onClick={undo}
331
+ disabled={!canUndo}
332
+ title="Undo (Ctrl+Z)"
333
+ >
334
+ Undo
335
+ </button>
336
+ <button
337
+ className="history-button"
338
+ onClick={redo}
339
+ disabled={!canRedo}
340
+ title="Redo (Ctrl+Shift+Z)"
341
+ >
342
+ Redo
343
+ </button>
344
+ </div>
345
+ <div className="mode-controls">
346
+ <button
347
+ className={`mode-button ${editMode === 'visual' ? 'active' : ''}`}
348
+ onClick={() => setEditMode('visual')}
349
+ title="Visual Editor (Ctrl+/)"
350
+ >
351
+ Visual
352
+ </button>
353
+ <button
354
+ className={`mode-button ${editMode === 'json' ? 'active' : ''}`}
355
+ onClick={() => setEditMode('json')}
356
+ title="JSON Editor (Ctrl+/)"
357
+ >
358
+ JSON
359
+ </button>
360
+ </div>
361
+ <div className="action-controls">
362
+ <button className="export-button" onClick={exportTheme} title="Export (Ctrl+S)">
363
+ Export
364
+ </button>
365
+ <button className="copy-button" onClick={copyToClipboard} title="Copy JSON">
366
+ Copy JSON
367
+ </button>
368
+ </div>
116
369
  </div>
117
370
  </div>
118
371
 
119
- <div className="editor-content">
120
- <div className="editor-panel">
372
+ <div className="editor-content" ref={editorRef}>
373
+ <div
374
+ className="editor-panel"
375
+ style={{ width: `${resizerPosition}%` }}
376
+ >
121
377
  {editMode === 'visual' ? (
122
378
  <div className="visual-editor">
123
- <h3>Colors</h3>
124
-
125
- {/* Primary Color */}
126
- <div className="editor-field">
127
- <label>Primary Color</label>
128
- <div className="color-input-group">
129
- <input
130
- type="color"
131
- value={theme.palette.primary.main}
132
- onChange={(e) => handleColorChange('palette.primary.main', e.target.value)}
133
- />
134
- <input
135
- type="text"
136
- value={theme.palette.primary.main}
137
- onChange={(e) => handleColorChange('palette.primary.main', e.target.value)}
138
- placeholder="#7AFFD7"
139
- />
379
+ <div className="editor-section">
380
+ <h3>Colors</h3>
381
+ <div className="color-format-selector">
382
+ <label>Color Format:</label>
383
+ <select
384
+ value={colorFormat}
385
+ onChange={(e) => setColorFormat(e.target.value as ColorFormat)}
386
+ >
387
+ <option value="hex">HEX</option>
388
+ <option value="rgb">RGB</option>
389
+ <option value="rgba">RGBA</option>
390
+ <option value="hsl">HSL</option>
391
+ <option value="hsla">HSLA</option>
392
+ </select>
393
+ </div>
394
+
395
+ {/* Primary Color */}
396
+ <div className="editor-field">
397
+ <label>Primary Color</label>
398
+ <div className="color-input-group">
399
+ <input
400
+ type="color"
401
+ value={theme.palette.primary.main}
402
+ onChange={(e) => handleColorChange('palette.primary.main', e.target.value)}
403
+ />
404
+ <input
405
+ type="text"
406
+ value={getColorValue(theme.palette.primary.main)}
407
+ onChange={(e) => {
408
+ const converted = convertColorFormat(e.target.value, 'hex');
409
+ handleColorChange('palette.primary.main', converted);
410
+ }}
411
+ placeholder="#7AFFD7"
412
+ />
413
+ </div>
414
+ </div>
415
+
416
+ {/* Secondary Color */}
417
+ <div className="editor-field">
418
+ <label>Secondary Color</label>
419
+ <div className="color-input-group">
420
+ <input
421
+ type="color"
422
+ value={theme.palette.secondary.main}
423
+ onChange={(e) => handleColorChange('palette.secondary.main', e.target.value)}
424
+ />
425
+ <input
426
+ type="text"
427
+ value={getColorValue(theme.palette.secondary.main)}
428
+ onChange={(e) => {
429
+ const converted = convertColorFormat(e.target.value, 'hex');
430
+ handleColorChange('palette.secondary.main', converted);
431
+ }}
432
+ placeholder="#FF5733"
433
+ />
434
+ </div>
435
+ </div>
436
+
437
+ {/* Background Colors */}
438
+ <h3>Background</h3>
439
+ <div className="editor-field">
440
+ <label>Default Background</label>
441
+ <div className="color-input-group">
442
+ <input
443
+ type="color"
444
+ value={theme.palette.background.default}
445
+ onChange={(e) => handleColorChange('palette.background.default', e.target.value)}
446
+ />
447
+ <input
448
+ type="text"
449
+ value={getColorValue(theme.palette.background.default)}
450
+ onChange={(e) => {
451
+ const converted = convertColorFormat(e.target.value, 'hex');
452
+ handleColorChange('palette.background.default', converted);
453
+ }}
454
+ />
455
+ </div>
140
456
  </div>
141
457
  </div>
142
458
 
143
- {/* Secondary Color */}
144
- <div className="editor-field">
145
- <label>Secondary Color</label>
146
- <div className="color-input-group">
147
- <input
148
- type="color"
149
- value={theme.palette.secondary.main}
150
- onChange={(e) => handleColorChange('palette.secondary.main', e.target.value)}
151
- />
459
+ {/* Typography */}
460
+ <div className="editor-section">
461
+ <h3>Typography</h3>
462
+ <div className="editor-field">
463
+ <label>Font Family</label>
152
464
  <input
153
465
  type="text"
154
- value={theme.palette.secondary.main}
155
- onChange={(e) => handleColorChange('palette.secondary.main', e.target.value)}
156
- placeholder="#FF5733"
466
+ value={theme.typography.fontFamily}
467
+ onChange={(e) => handleColorChange('typography.fontFamily', e.target.value)}
468
+ placeholder="Inter, sans-serif"
157
469
  />
158
470
  </div>
159
- </div>
160
471
 
161
- {/* Background Colors */}
162
- <h3>Background</h3>
163
- <div className="editor-field">
164
- <label>Default Background</label>
165
- <div className="color-input-group">
472
+ <div className="editor-field">
473
+ <label>Base Font Size (px)</label>
166
474
  <input
167
- type="color"
168
- value={theme.palette.background.default}
169
- onChange={(e) => handleColorChange('palette.background.default', e.target.value)}
170
- />
171
- <input
172
- type="text"
173
- value={theme.palette.background.default}
174
- onChange={(e) => handleColorChange('palette.background.default', e.target.value)}
475
+ type="number"
476
+ value={theme.typography.fontSize}
477
+ onChange={(e) => handleColorChange('typography.fontSize', parseInt(e.target.value))}
478
+ min="10"
479
+ max="24"
175
480
  />
176
481
  </div>
177
482
  </div>
178
-
179
- {/* Typography */}
180
- <h3>Typography</h3>
181
- <div className="editor-field">
182
- <label>Font Family</label>
183
- <input
184
- type="text"
185
- value={theme.typography.fontFamily}
186
- onChange={(e) => handleColorChange('typography.fontFamily', e.target.value)}
187
- placeholder="Inter, sans-serif"
188
- />
189
- </div>
190
-
191
- <div className="editor-field">
192
- <label>Base Font Size (px)</label>
193
- <input
194
- type="number"
195
- value={theme.typography.fontSize}
196
- onChange={(e) => handleColorChange('typography.fontSize', parseInt(e.target.value))}
197
- min="10"
198
- max="24"
199
- />
200
- </div>
201
483
  </div>
202
484
  ) : (
203
485
  <div className="json-editor">
@@ -209,13 +491,23 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
209
491
  {error && (
210
492
  <div className="error-message">
211
493
  ❌ {error}
494
+ <button className="error-dismiss" onClick={() => setError(null)}>×</button>
212
495
  </div>
213
496
  )}
214
497
  </div>
215
498
  )}
216
499
  </div>
217
500
 
218
- <div className="preview-panel">
501
+ <div
502
+ className={`resizer ${isResizing ? 'resizing' : ''}`}
503
+ onMouseDown={handleResizeStart}
504
+ />
505
+
506
+ <div
507
+ className="preview-panel"
508
+ ref={previewRef}
509
+ style={{ width: `${100 - resizerPosition}%` }}
510
+ >
219
511
  <h3>Live Preview</h3>
220
512
  <ThemePreview
221
513
  theme={theme}
@@ -251,10 +543,19 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
251
543
  }
252
544
 
253
545
  .editor-controls {
546
+ display: flex;
547
+ gap: 12px;
548
+ align-items: center;
549
+ }
550
+
551
+ .history-controls,
552
+ .mode-controls,
553
+ .action-controls {
254
554
  display: flex;
255
555
  gap: 8px;
256
556
  }
257
557
 
558
+ .history-button,
258
559
  .mode-button,
259
560
  .export-button,
260
561
  .copy-button {
@@ -267,12 +568,18 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
267
568
  transition: all 0.2s;
268
569
  }
269
570
 
571
+ .history-button:hover:not(:disabled),
270
572
  .mode-button:hover,
271
573
  .export-button:hover,
272
574
  .copy-button:hover {
273
575
  background: #f5f5f5;
274
576
  }
275
577
 
578
+ .history-button:disabled {
579
+ opacity: 0.5;
580
+ cursor: not-allowed;
581
+ }
582
+
276
583
  .mode-button.active {
277
584
  background: #2196f3;
278
585
  color: white;
@@ -292,16 +599,38 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
292
599
  }
293
600
 
294
601
  .editor-content {
295
- display: grid;
296
- grid-template-columns: 1fr 1fr;
297
- gap: 24px;
298
- padding: 24px;
602
+ display: flex;
603
+ position: relative;
299
604
  min-height: 600px;
300
605
  }
301
606
 
302
607
  .editor-panel,
303
608
  .preview-panel {
304
609
  overflow-y: auto;
610
+ padding: 24px;
611
+ }
612
+
613
+ .resizer {
614
+ width: 4px;
615
+ background: #e0e0e0;
616
+ cursor: col-resize;
617
+ position: relative;
618
+ flex-shrink: 0;
619
+ transition: background 0.2s;
620
+ }
621
+
622
+ .resizer:hover,
623
+ .resizer.resizing {
624
+ background: #2196f3;
625
+ }
626
+
627
+ .resizer::before {
628
+ content: '';
629
+ position: absolute;
630
+ left: -2px;
631
+ right: -2px;
632
+ top: 0;
633
+ bottom: 0;
305
634
  }
306
635
 
307
636
  .editor-panel h3,
@@ -314,11 +643,39 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
314
643
  }
315
644
 
316
645
  .visual-editor {
646
+ display: flex;
647
+ flex-direction: column;
648
+ gap: 24px;
649
+ }
650
+
651
+ .editor-section {
317
652
  display: flex;
318
653
  flex-direction: column;
319
654
  gap: 16px;
320
655
  }
321
656
 
657
+ .color-format-selector {
658
+ display: flex;
659
+ align-items: center;
660
+ gap: 8px;
661
+ margin-bottom: 8px;
662
+ }
663
+
664
+ .color-format-selector label {
665
+ font-size: 14px;
666
+ font-weight: 500;
667
+ color: #666;
668
+ }
669
+
670
+ .color-format-selector select {
671
+ padding: 6px 12px;
672
+ border: 1px solid #e0e0e0;
673
+ border-radius: 4px;
674
+ font-size: 14px;
675
+ background: white;
676
+ cursor: pointer;
677
+ }
678
+
322
679
  .editor-field {
323
680
  display: flex;
324
681
  flex-direction: column;
@@ -380,14 +737,34 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
380
737
  border-radius: 4px;
381
738
  margin-top: 8px;
382
739
  font-size: 14px;
740
+ display: flex;
741
+ justify-content: space-between;
742
+ align-items: center;
743
+ }
744
+
745
+ .error-dismiss {
746
+ background: none;
747
+ border: none;
748
+ color: #d32f2f;
749
+ font-size: 20px;
750
+ cursor: pointer;
751
+ padding: 0;
752
+ width: 24px;
753
+ height: 24px;
754
+ display: flex;
755
+ align-items: center;
756
+ justify-content: center;
757
+ }
758
+
759
+ .error-dismiss:hover {
760
+ background: rgba(211, 47, 47, 0.1);
761
+ border-radius: 50%;
383
762
  }
384
763
 
385
764
  .preview-panel {
386
765
  border-left: 1px solid #e0e0e0;
387
- padding-left: 24px;
388
766
  }
389
767
  `}</style>
390
768
  </div>
391
769
  );
392
770
  };
393
-