@shohojdhara/atomix 0.3.5 → 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.
- package/README.md +101 -199
- package/atomix.config.ts +241 -0
- package/dist/atomix.css +260 -179
- package/dist/atomix.css.map +1 -1
- package/dist/atomix.min.css +250 -179
- package/dist/atomix.min.css.map +1 -1
- package/dist/charts.js +61 -66
- package/dist/charts.js.map +1 -1
- package/dist/core.js +47 -31
- package/dist/core.js.map +1 -1
- package/dist/forms.js +47 -31
- package/dist/forms.js.map +1 -1
- package/dist/heavy.js +47 -31
- package/dist/heavy.js.map +1 -1
- package/dist/index.d.ts +1841 -1633
- package/dist/index.esm.js +4975 -4113
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +5151 -4290
- 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 +1572 -1442
- package/dist/theme.js +4816 -4080
- package/dist/theme.js.map +1 -1
- package/package.json +6 -20
- package/src/components/Accordion/Accordion.stories.tsx +50 -17
- package/src/components/AtomixGlass/AtomixGlass.tsx +65 -31
- package/src/components/AtomixGlass/AtomixGlassContainer.tsx +11 -4
- package/src/components/AtomixGlass/stories/AtomixGlass.stories.tsx +1 -32
- package/src/components/AtomixGlass/stories/Examples.stories.tsx +2 -2
- package/src/components/AtomixGlass/stories/shared-components.tsx +0 -31
- package/src/components/Avatar/Avatar.stories.tsx +7 -0
- package/src/components/Badge/Badge.stories.tsx +91 -13
- package/src/components/Block/Block.stories.tsx +7 -23
- package/src/components/Breadcrumb/Breadcrumb.stories.tsx +7 -0
- package/src/components/Button/Button.stories.tsx +141 -22
- package/src/components/Button/ButtonGroup.stories.tsx +315 -0
- package/src/components/Button/ButtonGroup.tsx +67 -0
- package/src/components/Button/index.ts +2 -0
- package/src/components/Callout/Callout.stories.tsx +8 -6
- package/src/components/Card/Card.stories.tsx +82 -28
- package/src/components/Chart/AnimatedChart.tsx +0 -1
- package/src/components/Chart/AreaChart.tsx +0 -1
- package/src/components/Chart/BarChart.tsx +0 -1
- package/src/components/Chart/BubbleChart.tsx +0 -1
- package/src/components/Chart/CandlestickChart.tsx +0 -1
- package/src/components/Chart/Chart.stories.tsx +5 -7
- package/src/components/Chart/Chart.tsx +0 -16
- package/src/components/Chart/ChartRenderer.tsx +1 -1
- package/src/components/Chart/DonutChart.tsx +0 -1
- package/src/components/Chart/FunnelChart.tsx +0 -1
- package/src/components/Chart/GaugeChart.tsx +0 -1
- package/src/components/Chart/HeatmapChart.tsx +0 -1
- package/src/components/Chart/LineChart.tsx +0 -1
- package/src/components/Chart/MultiAxisChart.tsx +0 -1
- package/src/components/Chart/PieChart.tsx +0 -1
- package/src/components/Chart/RadarChart.tsx +0 -1
- package/src/components/Chart/ScatterChart.tsx +0 -1
- package/src/components/Chart/WaterfallChart.tsx +0 -1
- package/src/components/ColorModeToggle/ColorModeToggle.stories.tsx +7 -0
- package/src/components/DataTable/DataTable.stories.tsx +23 -16
- package/src/components/DatePicker/DatePicker.stories.tsx +27 -19
- package/src/components/Dropdown/Dropdown.stories.tsx +11 -19
- package/src/components/EdgePanel/EdgePanel.stories.tsx +1 -0
- package/src/components/Footer/Footer.stories.tsx +8 -6
- package/src/components/Footer/FooterLink.tsx +9 -2
- package/src/components/Form/Checkbox.stories.tsx +7 -0
- package/src/components/Form/Form.stories.tsx +7 -0
- package/src/components/Form/FormGroup.stories.tsx +9 -1
- package/src/components/Form/Input.stories.tsx +69 -16
- package/src/components/Form/Radio.stories.tsx +9 -1
- package/src/components/Form/Select.stories.tsx +9 -1
- package/src/components/Form/Textarea.stories.tsx +10 -2
- package/src/components/Hero/Hero.stories.tsx +7 -0
- package/src/components/List/List.stories.tsx +7 -0
- package/src/components/Messages/Messages.stories.tsx +8 -7
- package/src/components/Modal/Modal.stories.tsx +17 -6
- package/src/components/Navigation/Menu/Menu.stories.tsx +7 -0
- package/src/components/Navigation/Nav/Nav.stories.tsx +7 -0
- package/src/components/Navigation/Navbar/Navbar.stories.tsx +1 -0
- package/src/components/Navigation/SideMenu/SideMenu.stories.tsx +1 -1
- package/src/components/Pagination/Pagination.stories.tsx +188 -111
- package/src/components/Pagination/Pagination.tsx +83 -3
- package/src/components/PhotoViewer/PhotoViewer.stories.tsx +10 -5
- package/src/components/Popover/Popover.stories.tsx +191 -115
- package/src/components/ProductReview/ProductReview.stories.tsx +80 -58
- package/src/components/Progress/Progress.stories.tsx +79 -49
- package/src/components/Rating/Rating.stories.tsx +109 -84
- package/src/components/River/River.stories.tsx +194 -114
- package/src/components/SectionIntro/SectionIntro.stories.tsx +19 -9
- package/src/components/Slider/Slider.stories.tsx +7 -0
- package/src/components/Spinner/Spinner.stories.tsx +15 -11
- package/src/components/Steps/Steps.stories.tsx +132 -98
- package/src/components/Tabs/Tabs.stories.tsx +163 -112
- package/src/components/Testimonial/Testimonial.stories.tsx +114 -68
- package/src/components/Todo/Todo.stories.tsx +38 -12
- package/src/components/Toggle/Toggle.stories.tsx +61 -28
- package/src/components/Tooltip/Tooltip.stories.tsx +318 -200
- package/src/components/Upload/Upload.stories.tsx +122 -84
- package/src/components/VideoPlayer/VideoPlayer.stories.tsx +7 -24
- package/src/components/index.ts +1 -0
- package/src/lib/composables/useAtomixGlass.ts +2 -3
- package/src/lib/composables/useNavbar.ts +0 -10
- package/src/lib/config/loader.ts +2 -1
- package/src/lib/constants/components.ts +10 -0
- package/src/lib/hooks/useComponentCustomization.ts +1 -1
- package/src/lib/theme/README.md +174 -0
- package/src/lib/theme/adapters/index.ts +31 -0
- package/src/lib/theme/adapters/themeAdapter.ts +287 -0
- package/src/lib/theme/config/__tests__/configLoader.test.ts +207 -0
- package/src/lib/theme/config/configLoader.ts +254 -0
- package/src/lib/theme/config/loader.ts +37 -48
- package/src/lib/theme/config/types.ts +2 -2
- package/src/lib/theme/config/validator.ts +15 -91
- package/src/lib/theme/{constants.ts → constants/constants.ts} +0 -18
- package/src/lib/theme/constants/index.ts +8 -0
- package/src/lib/theme/core/ThemeRegistry.ts +19 -6
- package/src/lib/theme/core/__tests__/createTheme.test.ts +132 -0
- package/src/lib/theme/core/composeTheme.ts +155 -0
- package/src/lib/theme/core/createTheme.ts +94 -0
- package/src/lib/theme/{createTheme.ts → core/createThemeObject.ts} +10 -6
- package/src/lib/theme/core/index.ts +5 -19
- package/src/lib/theme/devtools/Comparator.tsx +346 -22
- package/src/lib/theme/devtools/IMPROVEMENTS.md +139 -38
- package/src/lib/theme/devtools/Inspector.tsx +335 -51
- package/src/lib/theme/devtools/LiveEditor.tsx +478 -107
- package/src/lib/theme/devtools/Preview.tsx +471 -221
- package/src/lib/theme/{core → devtools}/ThemeValidator.ts +1 -1
- package/src/lib/theme/devtools/index.ts +14 -4
- package/src/lib/theme/devtools/useHistory.ts +130 -0
- package/src/lib/theme/errors/index.ts +12 -0
- package/src/lib/theme/generators/cssFile.ts +79 -0
- package/src/lib/theme/generators/generateCSS.ts +89 -0
- package/src/lib/theme/{generateCSSVariables.ts → generators/generateCSSVariables.ts} +3 -13
- package/src/lib/theme/generators/index.ts +19 -0
- package/src/lib/theme/i18n/rtl.ts +5 -6
- package/src/lib/theme/index.ts +120 -15
- package/src/lib/theme/runtime/ThemeApplicator.ts +52 -111
- package/src/lib/theme/{ThemeContext.tsx → runtime/ThemeContext.tsx} +1 -1
- package/src/lib/theme/runtime/ThemeErrorBoundary.tsx +1 -1
- package/src/lib/theme/runtime/ThemeProvider.tsx +456 -179
- package/src/lib/theme/runtime/index.ts +1 -2
- package/src/lib/theme/runtime/useTheme.ts +1 -2
- package/src/lib/theme/test/testTheme.ts +385 -0
- package/src/lib/theme/tokens/index.ts +12 -0
- package/src/lib/theme/tokens/tokens.ts +721 -0
- package/src/lib/theme/types.ts +6 -42
- package/src/lib/theme/{utils.ts → utils/domUtils.ts} +2 -2
- package/src/lib/theme/utils/index.ts +11 -0
- package/src/lib/theme/utils/injectCSS.ts +90 -0
- package/src/lib/theme/utils/themeHelpers.ts +78 -0
- package/src/lib/theme/{themeUtils.ts → utils/themeUtils.ts} +1 -1
- package/src/lib/theme-tools.ts +7 -8
- package/src/lib/types/components.ts +40 -130
- package/src/lib/utils/componentUtils.ts +1 -1
- package/src/styles/01-settings/_settings.design-tokens.scss +4 -1
- package/src/styles/02-tools/_tools.button.scss +66 -79
- package/src/styles/06-components/_components.atomix-glass.scss +13 -3
- package/src/styles/06-components/_components.pagination.scss +88 -0
- package/scripts/sync-theme-config.js +0 -309
- package/src/lib/theme/composeTheme.ts +0 -370
- package/src/lib/theme/core/ThemeCache.ts +0 -283
- package/src/lib/theme/core/ThemeEngine.test.ts +0 -146
- package/src/lib/theme/core/ThemeEngine.ts +0 -665
- package/src/lib/theme/createThemeFromConfig.ts +0 -132
- package/src/lib/theme/devtools/CLI.ts +0 -364
- package/src/lib/theme/runtime/ThemeManager.test.ts +0 -192
- package/src/lib/theme/runtime/ThemeManager.ts +0 -446
- package/src/styles/03-generic/_generated-root.css +0 -26
- package/src/themes/README.md +0 -442
- package/src/themes/themes.config.js +0 -68
- /package/src/lib/theme/{cssVariableMapper.ts → adapters/cssVariableMapper.ts} +0 -0
- /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 {
|
|
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,30 +122,98 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
|
|
|
34
122
|
className,
|
|
35
123
|
style,
|
|
36
124
|
}) => {
|
|
37
|
-
const
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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 =
|
|
54
|
-
|
|
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
|
-
}, [
|
|
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
218
|
const handleColorChange = useCallback((path: string, value: string | number) => {
|
|
63
219
|
const newTheme = { ...theme };
|
|
@@ -78,7 +234,7 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
|
|
|
78
234
|
if (lastKey) {
|
|
79
235
|
current[lastKey] = value;
|
|
80
236
|
}
|
|
81
|
-
updateTheme(
|
|
237
|
+
updateTheme(createThemeObject(newTheme));
|
|
82
238
|
}, [theme, updateTheme]);
|
|
83
239
|
|
|
84
240
|
const exportTheme = useCallback(() => {
|
|
@@ -96,114 +252,234 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
|
|
|
96
252
|
navigator.clipboard?.writeText(jsonInput);
|
|
97
253
|
}, [jsonInput]);
|
|
98
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
|
+
|
|
99
322
|
return (
|
|
100
323
|
<div className={`atomix-theme-live-editor ${className || ''}`} style={style}>
|
|
101
324
|
<div className="editor-header">
|
|
102
325
|
<h2>Live Theme Editor</h2>
|
|
103
326
|
<div className="editor-controls">
|
|
104
|
-
<
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
</
|
|
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>
|
|
122
369
|
</div>
|
|
123
370
|
</div>
|
|
124
371
|
|
|
125
|
-
<div className="editor-content">
|
|
126
|
-
<div
|
|
372
|
+
<div className="editor-content" ref={editorRef}>
|
|
373
|
+
<div
|
|
374
|
+
className="editor-panel"
|
|
375
|
+
style={{ width: `${resizerPosition}%` }}
|
|
376
|
+
>
|
|
127
377
|
{editMode === 'visual' ? (
|
|
128
378
|
<div className="visual-editor">
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
value=
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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>
|
|
146
456
|
</div>
|
|
147
457
|
</div>
|
|
148
458
|
|
|
149
|
-
{/*
|
|
150
|
-
<div className="editor-
|
|
151
|
-
<
|
|
152
|
-
<div className="
|
|
153
|
-
<
|
|
154
|
-
type="color"
|
|
155
|
-
value={theme.palette.secondary.main}
|
|
156
|
-
onChange={(e) => handleColorChange('palette.secondary.main', e.target.value)}
|
|
157
|
-
/>
|
|
459
|
+
{/* Typography */}
|
|
460
|
+
<div className="editor-section">
|
|
461
|
+
<h3>Typography</h3>
|
|
462
|
+
<div className="editor-field">
|
|
463
|
+
<label>Font Family</label>
|
|
158
464
|
<input
|
|
159
465
|
type="text"
|
|
160
|
-
value={theme.
|
|
161
|
-
onChange={(e) => handleColorChange('
|
|
162
|
-
placeholder="
|
|
466
|
+
value={theme.typography.fontFamily}
|
|
467
|
+
onChange={(e) => handleColorChange('typography.fontFamily', e.target.value)}
|
|
468
|
+
placeholder="Inter, sans-serif"
|
|
163
469
|
/>
|
|
164
470
|
</div>
|
|
165
|
-
</div>
|
|
166
471
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<div className="editor-field">
|
|
170
|
-
<label>Default Background</label>
|
|
171
|
-
<div className="color-input-group">
|
|
472
|
+
<div className="editor-field">
|
|
473
|
+
<label>Base Font Size (px)</label>
|
|
172
474
|
<input
|
|
173
|
-
type="
|
|
174
|
-
value={theme.
|
|
175
|
-
onChange={(e) => handleColorChange('
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
type="text"
|
|
179
|
-
value={theme.palette.background.default}
|
|
180
|
-
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"
|
|
181
480
|
/>
|
|
182
481
|
</div>
|
|
183
482
|
</div>
|
|
184
|
-
|
|
185
|
-
{/* Typography */}
|
|
186
|
-
<h3>Typography</h3>
|
|
187
|
-
<div className="editor-field">
|
|
188
|
-
<label>Font Family</label>
|
|
189
|
-
<input
|
|
190
|
-
type="text"
|
|
191
|
-
value={theme.typography.fontFamily}
|
|
192
|
-
onChange={(e) => handleColorChange('typography.fontFamily', e.target.value)}
|
|
193
|
-
placeholder="Inter, sans-serif"
|
|
194
|
-
/>
|
|
195
|
-
</div>
|
|
196
|
-
|
|
197
|
-
<div className="editor-field">
|
|
198
|
-
<label>Base Font Size (px)</label>
|
|
199
|
-
<input
|
|
200
|
-
type="number"
|
|
201
|
-
value={theme.typography.fontSize}
|
|
202
|
-
onChange={(e) => handleColorChange('typography.fontSize', parseInt(e.target.value))}
|
|
203
|
-
min="10"
|
|
204
|
-
max="24"
|
|
205
|
-
/>
|
|
206
|
-
</div>
|
|
207
483
|
</div>
|
|
208
484
|
) : (
|
|
209
485
|
<div className="json-editor">
|
|
@@ -215,13 +491,23 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
|
|
|
215
491
|
{error && (
|
|
216
492
|
<div className="error-message">
|
|
217
493
|
❌ {error}
|
|
494
|
+
<button className="error-dismiss" onClick={() => setError(null)}>×</button>
|
|
218
495
|
</div>
|
|
219
496
|
)}
|
|
220
497
|
</div>
|
|
221
498
|
)}
|
|
222
499
|
</div>
|
|
223
500
|
|
|
224
|
-
<div
|
|
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
|
+
>
|
|
225
511
|
<h3>Live Preview</h3>
|
|
226
512
|
<ThemePreview
|
|
227
513
|
theme={theme}
|
|
@@ -257,10 +543,19 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
|
|
|
257
543
|
}
|
|
258
544
|
|
|
259
545
|
.editor-controls {
|
|
546
|
+
display: flex;
|
|
547
|
+
gap: 12px;
|
|
548
|
+
align-items: center;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.history-controls,
|
|
552
|
+
.mode-controls,
|
|
553
|
+
.action-controls {
|
|
260
554
|
display: flex;
|
|
261
555
|
gap: 8px;
|
|
262
556
|
}
|
|
263
557
|
|
|
558
|
+
.history-button,
|
|
264
559
|
.mode-button,
|
|
265
560
|
.export-button,
|
|
266
561
|
.copy-button {
|
|
@@ -273,12 +568,18 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
|
|
|
273
568
|
transition: all 0.2s;
|
|
274
569
|
}
|
|
275
570
|
|
|
571
|
+
.history-button:hover:not(:disabled),
|
|
276
572
|
.mode-button:hover,
|
|
277
573
|
.export-button:hover,
|
|
278
574
|
.copy-button:hover {
|
|
279
575
|
background: #f5f5f5;
|
|
280
576
|
}
|
|
281
577
|
|
|
578
|
+
.history-button:disabled {
|
|
579
|
+
opacity: 0.5;
|
|
580
|
+
cursor: not-allowed;
|
|
581
|
+
}
|
|
582
|
+
|
|
282
583
|
.mode-button.active {
|
|
283
584
|
background: #2196f3;
|
|
284
585
|
color: white;
|
|
@@ -298,16 +599,38 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
|
|
|
298
599
|
}
|
|
299
600
|
|
|
300
601
|
.editor-content {
|
|
301
|
-
display:
|
|
302
|
-
|
|
303
|
-
gap: 24px;
|
|
304
|
-
padding: 24px;
|
|
602
|
+
display: flex;
|
|
603
|
+
position: relative;
|
|
305
604
|
min-height: 600px;
|
|
306
605
|
}
|
|
307
606
|
|
|
308
607
|
.editor-panel,
|
|
309
608
|
.preview-panel {
|
|
310
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;
|
|
311
634
|
}
|
|
312
635
|
|
|
313
636
|
.editor-panel h3,
|
|
@@ -320,11 +643,39 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
|
|
|
320
643
|
}
|
|
321
644
|
|
|
322
645
|
.visual-editor {
|
|
646
|
+
display: flex;
|
|
647
|
+
flex-direction: column;
|
|
648
|
+
gap: 24px;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
.editor-section {
|
|
323
652
|
display: flex;
|
|
324
653
|
flex-direction: column;
|
|
325
654
|
gap: 16px;
|
|
326
655
|
}
|
|
327
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
|
+
|
|
328
679
|
.editor-field {
|
|
329
680
|
display: flex;
|
|
330
681
|
flex-direction: column;
|
|
@@ -386,14 +737,34 @@ export const ThemeLiveEditor: React.FC<ThemeLiveEditorProps> = ({
|
|
|
386
737
|
border-radius: 4px;
|
|
387
738
|
margin-top: 8px;
|
|
388
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%;
|
|
389
762
|
}
|
|
390
763
|
|
|
391
764
|
.preview-panel {
|
|
392
765
|
border-left: 1px solid #e0e0e0;
|
|
393
|
-
padding-left: 24px;
|
|
394
766
|
}
|
|
395
767
|
`}</style>
|
|
396
768
|
</div>
|
|
397
769
|
);
|
|
398
770
|
};
|
|
399
|
-
|