radtools 0.1.0 → 0.1.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.
- package/package.json +8 -2
- package/templates/components/icons/Icon.tsx +38 -170
- package/templates/components/icons.tsx +6 -104
- package/templates/devtools/store/index.ts +2 -28
- package/templates/devtools/store/slices/componentsSlice.ts +18 -30
- package/templates/devtools/store/slices/panelSlice.ts +20 -3
package/package.json
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "radtools",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Visual dev tools for Next.js + Tailwind v4 projects",
|
|
5
|
-
"keywords": [
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nextjs",
|
|
7
|
+
"tailwind",
|
|
8
|
+
"devtools",
|
|
9
|
+
"design-system",
|
|
10
|
+
"visual-editor"
|
|
11
|
+
],
|
|
6
12
|
"author": "kemos4be",
|
|
7
13
|
"license": "MIT",
|
|
8
14
|
"repository": {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { memo, useEffect, useState } from 'react';
|
|
4
4
|
|
|
5
5
|
interface IconProps {
|
|
6
|
-
/** Icon filename without .svg extension
|
|
6
|
+
/** Icon name (filename without .svg extension) */
|
|
7
7
|
name: string;
|
|
8
8
|
/** Icon size in pixels (applies to both width and height) */
|
|
9
9
|
size?: number;
|
|
@@ -14,23 +14,22 @@ interface IconProps {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Icon component using SVG files from /assets/icons.
|
|
18
18
|
*
|
|
19
|
-
* Icons
|
|
20
|
-
* processed to use currentColor for fills, inheriting the parent's
|
|
21
|
-
* text color.
|
|
19
|
+
* Icons automatically use currentColor, inheriting the parent's text color.
|
|
22
20
|
*
|
|
23
21
|
* @example
|
|
24
22
|
* ```tsx
|
|
25
23
|
* // Inherits parent text color
|
|
26
24
|
* <div className="text-blue-500">
|
|
27
|
-
* <Icon name="
|
|
25
|
+
* <Icon name="copy" size={24} />
|
|
28
26
|
* </div>
|
|
29
27
|
*
|
|
30
28
|
* // Override color with className
|
|
31
|
-
* <Icon name="
|
|
29
|
+
* <Icon name="checkmark" size={20} className="text-green-500" />
|
|
32
30
|
* ```
|
|
33
31
|
*/
|
|
32
|
+
|
|
34
33
|
function IconComponent({
|
|
35
34
|
name,
|
|
36
35
|
size = 24,
|
|
@@ -38,187 +37,56 @@ function IconComponent({
|
|
|
38
37
|
'aria-label': ariaLabel,
|
|
39
38
|
}: IconProps) {
|
|
40
39
|
const [svgContent, setSvgContent] = useState<string | null>(null);
|
|
41
|
-
const
|
|
40
|
+
const iconPath = `/assets/icons/${name}.svg`;
|
|
42
41
|
|
|
43
42
|
useEffect(() => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// Process SVG to use currentColor for theming
|
|
59
|
-
let processed = svgText;
|
|
60
|
-
|
|
61
|
-
// Check if SVG uses CSS classes (like .st0 with fill: currentColor)
|
|
62
|
-
const usesCssClasses = processed.includes('<style>') && processed.includes('currentColor');
|
|
63
|
-
|
|
64
|
-
if (!usesCssClasses) {
|
|
65
|
-
// Only process fill/stroke attributes if not using CSS classes
|
|
66
|
-
// Remove existing fill attributes (except none)
|
|
67
|
-
processed = processed.replace(/fill="(?!none)[^"]*"/g, 'fill="currentColor"');
|
|
68
|
-
// Remove existing stroke attributes (except none)
|
|
69
|
-
processed = processed.replace(/stroke="(?!none)[^"]*"/g, 'stroke="currentColor"');
|
|
70
|
-
|
|
71
|
-
// Add fill="currentColor" to root svg if no fill exists
|
|
72
|
-
if (!processed.includes('fill=')) {
|
|
73
|
-
processed = processed.replace(/<svg([^>]*)>/, `<svg$1 fill="currentColor">`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Set dimensions (always do this, preserving viewBox)
|
|
78
|
-
// Parse the SVG tag more carefully to avoid breaking structure
|
|
79
|
-
const svgTagMatch = processed.match(/<svg([^>]*)>/);
|
|
80
|
-
if (svgTagMatch) {
|
|
81
|
-
let svgAttrs = svgTagMatch[1];
|
|
82
|
-
|
|
83
|
-
// Extract and preserve viewBox (critical for CSS-based fills to work correctly)
|
|
84
|
-
const viewBoxMatch = svgAttrs.match(/viewBox="[^"]*"/);
|
|
85
|
-
const viewBox = viewBoxMatch ? viewBoxMatch[0] : null;
|
|
86
|
-
|
|
87
|
-
// Remove existing width/height attributes
|
|
88
|
-
svgAttrs = svgAttrs.replace(/\s*width="[^"]*"/g, '');
|
|
89
|
-
svgAttrs = svgAttrs.replace(/\s*height="[^"]*"/g, '');
|
|
90
|
-
|
|
91
|
-
// Clean up extra spaces
|
|
92
|
-
svgAttrs = svgAttrs.trim().replace(/\s+/g, ' ');
|
|
93
|
-
|
|
94
|
-
// Build new attributes: preserve viewBox first, then add width/height
|
|
95
|
-
const newAttrs = [];
|
|
96
|
-
if (viewBox) {
|
|
97
|
-
newAttrs.push(viewBox);
|
|
98
|
-
}
|
|
99
|
-
newAttrs.push(`width="${size}"`, `height="${size}"`);
|
|
100
|
-
|
|
101
|
-
// Add any remaining attributes
|
|
102
|
-
const remainingAttrs = svgAttrs
|
|
103
|
-
.replace(/viewBox="[^"]*"/g, '')
|
|
104
|
-
.trim();
|
|
105
|
-
if (remainingAttrs) {
|
|
106
|
-
newAttrs.push(remainingAttrs);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Reconstruct the SVG tag with proper spacing
|
|
110
|
-
processed = processed.replace(/<svg[^>]*>/, `<svg ${newAttrs.join(' ')}>`);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
setSvgContent(processed);
|
|
114
|
-
setError(false);
|
|
115
|
-
} catch (e) {
|
|
116
|
-
if (mounted) {
|
|
117
|
-
setError(true);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
loadIcon();
|
|
123
|
-
|
|
124
|
-
return () => {
|
|
125
|
-
mounted = false;
|
|
126
|
-
};
|
|
127
|
-
}, [name, size]);
|
|
128
|
-
|
|
129
|
-
// Mapping of icon names to PixelCode fallback characters
|
|
130
|
-
const ICON_FALLBACKS: Record<string, string> = {
|
|
131
|
-
'close': '×', // Multiplication sign
|
|
132
|
-
'check': '✓', // Check mark
|
|
133
|
-
'checkmark': '✓',
|
|
134
|
-
'checkmark-filled': '✓',
|
|
135
|
-
'arrow-left': '←', // Left arrow
|
|
136
|
-
'arrow-right': '→', // Right arrow
|
|
137
|
-
'chevron-down': '▼', // Down triangle
|
|
138
|
-
'plus': '+',
|
|
139
|
-
'minus': '−',
|
|
140
|
-
'search': '○', // Circle
|
|
141
|
-
'settings': '⚙', // Gear symbol
|
|
142
|
-
'home': '⌂', // Home symbol
|
|
143
|
-
'home-outline': '⌂',
|
|
144
|
-
'folder-closed': '▷', // Right-pointing triangle
|
|
145
|
-
'folder-open': '▼', // Down triangle
|
|
146
|
-
'file-blank': '□', // Empty square
|
|
147
|
-
'file-image': '▣', // Square with pattern
|
|
148
|
-
'file-written': '▤', // Square with lines
|
|
149
|
-
'trash': '×',
|
|
150
|
-
'trash-open': '×',
|
|
151
|
-
'trash-full': '×',
|
|
152
|
-
'power': '⚡', // Lightning
|
|
153
|
-
'power-thin': '⚡',
|
|
154
|
-
'locked': '🔒', // Lock
|
|
155
|
-
'unlocked': '🔓', // Unlock
|
|
156
|
-
'save': '💾', // Floppy disk
|
|
157
|
-
'download': '↓', // Down arrow
|
|
158
|
-
'copy': '⧉', // Copy symbol
|
|
159
|
-
'refresh': '↻', // Refresh arrow
|
|
160
|
-
'refresh-block': '↻',
|
|
161
|
-
'information': 'ℹ', // Information
|
|
162
|
-
'information-circle': 'ℹ',
|
|
163
|
-
'warning': '⚠', // Warning
|
|
164
|
-
'warning-triangle-filled': '⚠',
|
|
165
|
-
'warning-triangle-filled-2': '⚠',
|
|
166
|
-
'warning-triangle-lines': '⚠',
|
|
167
|
-
'waring-triangle-filled': '⚠',
|
|
168
|
-
'question': '?',
|
|
169
|
-
'question-block': '?',
|
|
170
|
-
'expand': '▶',
|
|
171
|
-
'hamburger': '☰', // Hamburger menu
|
|
172
|
-
'avatar': '○',
|
|
173
|
-
'lightning': '⚡',
|
|
174
|
-
'not-allowed': '⊘',
|
|
175
|
-
'hourglass': '⏳',
|
|
176
|
-
'wrench': '🔧',
|
|
177
|
-
};
|
|
43
|
+
fetch(iconPath)
|
|
44
|
+
.then((res) => res.text())
|
|
45
|
+
.then((text) => {
|
|
46
|
+
// Inject width and height into SVG for proper scaling
|
|
47
|
+
const svgWithSize = text.replace(
|
|
48
|
+
/<svg([^>]*)>/,
|
|
49
|
+
`<svg$1 width="${size}" height="${size}">`
|
|
50
|
+
);
|
|
51
|
+
setSvgContent(svgWithSize);
|
|
52
|
+
})
|
|
53
|
+
.catch((err) => {
|
|
54
|
+
console.error(`Failed to load icon: ${name}`, err);
|
|
55
|
+
});
|
|
56
|
+
}, [name, iconPath, size]);
|
|
178
57
|
|
|
179
|
-
if (
|
|
180
|
-
// Get fallback character, or use a default
|
|
181
|
-
const fallbackChar = ICON_FALLBACKS[name] || '?';
|
|
182
|
-
|
|
58
|
+
if (!svgContent) {
|
|
183
59
|
return (
|
|
184
60
|
<span
|
|
185
|
-
className={
|
|
186
|
-
style={{
|
|
187
|
-
display: 'inline-flex',
|
|
188
|
-
width: size,
|
|
189
|
-
height: size,
|
|
190
|
-
alignItems: 'center',
|
|
191
|
-
justifyContent: 'center',
|
|
192
|
-
fontSize: size * 0.8, // Slightly smaller than container for padding
|
|
193
|
-
lineHeight: 1,
|
|
194
|
-
}}
|
|
195
|
-
role="img"
|
|
61
|
+
className={className}
|
|
196
62
|
aria-label={ariaLabel}
|
|
197
63
|
aria-hidden={!ariaLabel}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
64
|
+
style={{
|
|
65
|
+
width: size,
|
|
66
|
+
height: size,
|
|
67
|
+
display: 'inline-block',
|
|
68
|
+
}}
|
|
69
|
+
/>
|
|
201
70
|
);
|
|
202
71
|
}
|
|
203
72
|
|
|
204
73
|
return (
|
|
205
74
|
<span
|
|
206
75
|
className={className}
|
|
207
|
-
style={{
|
|
208
|
-
display: 'inline-flex',
|
|
209
|
-
width: size,
|
|
210
|
-
height: size,
|
|
211
|
-
alignItems: 'center',
|
|
212
|
-
justifyContent: 'center',
|
|
213
|
-
}}
|
|
214
|
-
role="img"
|
|
215
76
|
aria-label={ariaLabel}
|
|
216
77
|
aria-hidden={!ariaLabel}
|
|
78
|
+
style={{
|
|
79
|
+
width: size,
|
|
80
|
+
height: size,
|
|
81
|
+
display: 'inline-block',
|
|
82
|
+
verticalAlign: 'middle',
|
|
83
|
+
lineHeight: 0,
|
|
84
|
+
}}
|
|
217
85
|
dangerouslySetInnerHTML={{ __html: svgContent }}
|
|
218
86
|
/>
|
|
219
87
|
);
|
|
220
88
|
}
|
|
221
89
|
|
|
222
|
-
// Memoize to prevent unnecessary re-renders
|
|
90
|
+
// Memoize to prevent unnecessary re-renders
|
|
223
91
|
export const Icon = memo(IconComponent);
|
|
224
92
|
|
|
@@ -29,125 +29,27 @@ interface IconProps {
|
|
|
29
29
|
// ============================================================================
|
|
30
30
|
|
|
31
31
|
export function CloseIcon({ className = '', size = 10 }: IconProps) {
|
|
32
|
-
return (
|
|
33
|
-
<svg
|
|
34
|
-
width={size}
|
|
35
|
-
height={size}
|
|
36
|
-
viewBox="0 0 10 11"
|
|
37
|
-
fill="none"
|
|
38
|
-
className={className}
|
|
39
|
-
>
|
|
40
|
-
<path
|
|
41
|
-
fillRule="evenodd"
|
|
42
|
-
clipRule="evenodd"
|
|
43
|
-
d="M1.11111 0.5H0V1.61111H1.11111V2.72222H2.22222V3.83333H3.33333V4.94444H4.44444V6.05556H3.33333V7.16667H2.22222V8.27778H1.11111V9.38889H0V10.5H1.11111H2.22222V9.38889H3.33333V8.27778H4.44444V7.16667H5.55556V8.27778H6.66667V9.38889H7.77778V10.5H8.88889H10V9.38889H8.88889V8.27778H7.77778V7.16667H6.66667V6.05556H5.55556V4.94444H6.66667V3.83333H7.77778V2.72222H8.88889V1.61111H10V0.5H8.88889H7.77778V1.61111H6.66667V2.72222H5.55556V3.83333H4.44444V2.72222H3.33333V1.61111H2.22222V0.5H1.11111Z"
|
|
44
|
-
fill="currentColor"
|
|
45
|
-
/>
|
|
46
|
-
</svg>
|
|
47
|
-
);
|
|
32
|
+
return <Icon name="close" size={typeof size === 'number' ? size : parseInt(String(size))} className={className} />;
|
|
48
33
|
}
|
|
49
34
|
|
|
50
35
|
export function CopyIcon({ className = '', size = 18 }: IconProps) {
|
|
51
|
-
return (
|
|
52
|
-
<svg
|
|
53
|
-
width={size}
|
|
54
|
-
height={size}
|
|
55
|
-
viewBox="0 0 28 28"
|
|
56
|
-
fill="none"
|
|
57
|
-
className={className}
|
|
58
|
-
>
|
|
59
|
-
<path
|
|
60
|
-
fillRule="evenodd"
|
|
61
|
-
clipRule="evenodd"
|
|
62
|
-
d="M8.71111 0H28V19.2889H19.2889V28H0V8.71111H8.71111V0ZM11 8.71111H19.2889V17H26V2.5H11V8.71111ZM17 11.5H2.5V25.5H17V11.5Z"
|
|
63
|
-
fill="currentColor"
|
|
64
|
-
/>
|
|
65
|
-
</svg>
|
|
66
|
-
);
|
|
36
|
+
return <Icon name="copy" size={typeof size === 'number' ? size : parseInt(String(size))} className={className} />;
|
|
67
37
|
}
|
|
68
38
|
|
|
69
39
|
export function CopiedIcon({ className = '', size = 18 }: IconProps) {
|
|
70
|
-
return (
|
|
71
|
-
<svg
|
|
72
|
-
width={size}
|
|
73
|
-
height={typeof size === 'number' ? size * (20 / 26) : size}
|
|
74
|
-
viewBox="0 0 26 20"
|
|
75
|
-
fill="none"
|
|
76
|
-
className={className}
|
|
77
|
-
>
|
|
78
|
-
<path
|
|
79
|
-
d="M11.4187 11.4375H8.56875V8.5875H5.7V5.71875H2.85V8.5875H0V11.4375H2.85V14.2875H5.7V17.1563H8.56875V20.0062H11.4187V17.1563H14.2875V14.2875H17.1375V11.4375H19.9875V8.5875H22.8562V5.71875H25.7062V2.86875H22.8562V0H19.9875V2.86875H17.1375V5.71875H14.2875V8.5875H11.4187V11.4375Z"
|
|
80
|
-
fill="currentColor"
|
|
81
|
-
/>
|
|
82
|
-
</svg>
|
|
83
|
-
);
|
|
40
|
+
return <Icon name="checkmark-filled" size={typeof size === 'number' ? size : parseInt(String(size))} className={className} />;
|
|
84
41
|
}
|
|
85
42
|
|
|
86
43
|
export function HelpIcon({ className = '', size = 16 }: IconProps) {
|
|
87
|
-
return (
|
|
88
|
-
<svg
|
|
89
|
-
width={size}
|
|
90
|
-
height={size}
|
|
91
|
-
viewBox="0 0 16 16"
|
|
92
|
-
fill="none"
|
|
93
|
-
className={className}
|
|
94
|
-
>
|
|
95
|
-
{/* Outer circle */}
|
|
96
|
-
<path d="M5 0H11V2H5V0Z" fill="currentColor" />
|
|
97
|
-
<path d="M3 2H5V4H3V2Z" fill="currentColor" />
|
|
98
|
-
<path d="M11 2H13V4H11V2Z" fill="currentColor" />
|
|
99
|
-
<path d="M2 4H4V6H2V4Z" fill="currentColor" />
|
|
100
|
-
<path d="M12 4H14V6H12V4Z" fill="currentColor" />
|
|
101
|
-
<path d="M0 5H2V11H0V5Z" fill="currentColor" />
|
|
102
|
-
<path d="M14 5H16V11H14V5Z" fill="currentColor" />
|
|
103
|
-
<path d="M2 10H4V12H2V10Z" fill="currentColor" />
|
|
104
|
-
<path d="M12 10H14V12H12V10Z" fill="currentColor" />
|
|
105
|
-
<path d="M3 12H5V14H3V12Z" fill="currentColor" />
|
|
106
|
-
<path d="M11 12H13V14H11V12Z" fill="currentColor" />
|
|
107
|
-
<path d="M5 14H11V16H5V14Z" fill="currentColor" />
|
|
108
|
-
{/* Question mark */}
|
|
109
|
-
<path d="M6 4H10V5H6V4Z" fill="currentColor" />
|
|
110
|
-
<path d="M10 5H11V7H10V5Z" fill="currentColor" />
|
|
111
|
-
<path d="M8 7H10V8H8V7Z" fill="currentColor" />
|
|
112
|
-
<path d="M7 8H9V10H7V8Z" fill="currentColor" />
|
|
113
|
-
<path d="M7 11H9V13H7V11Z" fill="currentColor" />
|
|
114
|
-
</svg>
|
|
115
|
-
);
|
|
44
|
+
return <Icon name="question" size={typeof size === 'number' ? size : parseInt(String(size))} className={className} />;
|
|
116
45
|
}
|
|
117
46
|
|
|
118
47
|
export function ComponentsIcon({ className = '', size = 16 }: IconProps) {
|
|
119
|
-
return (
|
|
120
|
-
<svg
|
|
121
|
-
width={size}
|
|
122
|
-
height={size}
|
|
123
|
-
viewBox="0 0 24 24"
|
|
124
|
-
fill="currentColor"
|
|
125
|
-
className={className}
|
|
126
|
-
>
|
|
127
|
-
{/* 2x2 grid of squares representing components */}
|
|
128
|
-
<rect x="2" y="2" width="9" height="9" />
|
|
129
|
-
<rect x="13" y="2" width="9" height="9" />
|
|
130
|
-
<rect x="2" y="13" width="9" height="9" />
|
|
131
|
-
<rect x="13" y="13" width="9" height="9" />
|
|
132
|
-
</svg>
|
|
133
|
-
);
|
|
48
|
+
return <Icon name="wrench" size={typeof size === 'number' ? size : parseInt(String(size))} className={className} />;
|
|
134
49
|
}
|
|
135
50
|
|
|
136
51
|
export function ChevronIcon({ className = '', size = 8 }: IconProps) {
|
|
137
|
-
return (
|
|
138
|
-
<svg
|
|
139
|
-
width={size}
|
|
140
|
-
height={size}
|
|
141
|
-
viewBox="0 0 8 10"
|
|
142
|
-
fill="none"
|
|
143
|
-
className={className}
|
|
144
|
-
>
|
|
145
|
-
<path
|
|
146
|
-
d="M7.21826 5.64286L5.91799 5.64286L5.91799 6.92857L4.64346 6.92857L4.64346 8.21429L3.35605 8.21429L3.35605 9.5L0.781248 9.5L0.781249 8.21429L2.06865 8.21429L2.06865 6.92857L3.35605 6.92857L3.35605 5.64286L4.64346 5.64286L4.64346 4.35714L3.35605 4.35714L3.35605 3.07143L2.06865 3.07143L2.06865 1.78572L0.78125 1.78571L0.78125 0.5L3.35606 0.5L3.35606 1.78572L4.64346 1.78572L4.64346 3.07143L5.91799 3.07143L5.91799 4.35714L7.21826 4.35714L7.21826 5.64286Z"
|
|
147
|
-
fill="currentColor"
|
|
148
|
-
/>
|
|
149
|
-
</svg>
|
|
150
|
-
);
|
|
52
|
+
return <Icon name="chevron-down" size={typeof size === 'number' ? size : parseInt(String(size))} className={className} />;
|
|
151
53
|
}
|
|
152
54
|
|
|
153
55
|
export default {
|
|
@@ -1,26 +1,13 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
2
|
import { devtools, persist } from 'zustand/middleware';
|
|
3
|
+
import { PanelSlice, createPanelSlice } from './slices/panelSlice';
|
|
3
4
|
import { VariablesSlice, createVariablesSlice } from './slices/variablesSlice';
|
|
4
5
|
import { TypographySlice, createTypographySlice } from './slices/typographySlice';
|
|
5
6
|
import { ComponentsSlice, createComponentsSlice } from './slices/componentsSlice';
|
|
6
7
|
import { AssetsSlice, createAssetsSlice } from './slices/assetsSlice';
|
|
7
8
|
import { MockStatesSlice, createMockStatesSlice } from './slices/mockStatesSlice';
|
|
8
|
-
import { PanelSlice, createPanelSlice } from './slices/panelSlice';
|
|
9
|
-
import type { Tab } from '../types';
|
|
10
|
-
|
|
11
|
-
interface PanelState {
|
|
12
|
-
isOpen: boolean;
|
|
13
|
-
activeTab: Tab;
|
|
14
|
-
panelPosition: { x: number; y: number };
|
|
15
|
-
panelSize: { width: number; height: number };
|
|
16
|
-
togglePanel: () => void;
|
|
17
|
-
setActiveTab: (tab: Tab) => void;
|
|
18
|
-
setPanelPosition: (position: { x: number; y: number }) => void;
|
|
19
|
-
setPanelSize: (size: { width: number; height: number }) => void;
|
|
20
|
-
}
|
|
21
9
|
|
|
22
|
-
type DevToolsState =
|
|
23
|
-
PanelSlice &
|
|
10
|
+
type DevToolsState = PanelSlice &
|
|
24
11
|
VariablesSlice &
|
|
25
12
|
TypographySlice &
|
|
26
13
|
ComponentsSlice &
|
|
@@ -31,17 +18,6 @@ export const useDevToolsStore = create<DevToolsState>()(
|
|
|
31
18
|
devtools(
|
|
32
19
|
persist(
|
|
33
20
|
(set, get, api) => ({
|
|
34
|
-
// Panel state
|
|
35
|
-
isOpen: false,
|
|
36
|
-
activeTab: 'variables' as Tab,
|
|
37
|
-
panelPosition: { x: 20, y: 20 },
|
|
38
|
-
panelSize: { width: 420, height: 600 },
|
|
39
|
-
togglePanel: () => set((state) => ({ isOpen: !state.isOpen })),
|
|
40
|
-
setActiveTab: (tab) => set({ activeTab: tab }),
|
|
41
|
-
setPanelPosition: (position) => set({ panelPosition: position }),
|
|
42
|
-
setPanelSize: (size) => set({ panelSize: size }),
|
|
43
|
-
|
|
44
|
-
// Slices
|
|
45
21
|
...createPanelSlice(set, get, api),
|
|
46
22
|
...createVariablesSlice(set, get, api),
|
|
47
23
|
...createTypographySlice(set, get, api),
|
|
@@ -52,7 +28,6 @@ export const useDevToolsStore = create<DevToolsState>()(
|
|
|
52
28
|
{
|
|
53
29
|
name: 'devtools-storage',
|
|
54
30
|
partialize: (state) => ({
|
|
55
|
-
// Only persist these fields
|
|
56
31
|
panelPosition: state.panelPosition,
|
|
57
32
|
panelSize: state.panelSize,
|
|
58
33
|
activeTab: state.activeTab,
|
|
@@ -63,4 +38,3 @@ export const useDevToolsStore = create<DevToolsState>()(
|
|
|
63
38
|
{ name: 'RadTools DevTools' }
|
|
64
39
|
)
|
|
65
40
|
);
|
|
66
|
-
|
|
@@ -14,19 +14,8 @@ export interface ComponentsSlice {
|
|
|
14
14
|
refreshComponents: () => Promise<void>;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export const createComponentsSlice: StateCreator<ComponentsSlice, [], [], ComponentsSlice> = (set) =>
|
|
18
|
-
|
|
19
|
-
isLoading: false,
|
|
20
|
-
lastScanned: null,
|
|
21
|
-
|
|
22
|
-
setComponents: (components) => set({
|
|
23
|
-
components,
|
|
24
|
-
lastScanned: new Date().toISOString()
|
|
25
|
-
}),
|
|
26
|
-
|
|
27
|
-
setIsLoading: (isLoading) => set({ isLoading }),
|
|
28
|
-
|
|
29
|
-
scanComponents: async () => {
|
|
17
|
+
export const createComponentsSlice: StateCreator<ComponentsSlice, [], [], ComponentsSlice> = (set, get) => {
|
|
18
|
+
const scan = async () => {
|
|
30
19
|
set({ isLoading: true });
|
|
31
20
|
try {
|
|
32
21
|
const res = await fetch('/api/devtools/components');
|
|
@@ -36,24 +25,23 @@ export const createComponentsSlice: StateCreator<ComponentsSlice, [], [], Compon
|
|
|
36
25
|
lastScanned: new Date().toISOString(),
|
|
37
26
|
isLoading: false
|
|
38
27
|
});
|
|
39
|
-
} catch
|
|
28
|
+
} catch {
|
|
40
29
|
set({ isLoading: false });
|
|
41
30
|
}
|
|
42
|
-
}
|
|
31
|
+
};
|
|
43
32
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const data = await res.json();
|
|
49
|
-
set({
|
|
50
|
-
components: data.components || [],
|
|
51
|
-
lastScanned: new Date().toISOString(),
|
|
52
|
-
isLoading: false
|
|
53
|
-
});
|
|
54
|
-
} catch (error) {
|
|
55
|
-
set({ isLoading: false });
|
|
56
|
-
}
|
|
57
|
-
},
|
|
58
|
-
});
|
|
33
|
+
return {
|
|
34
|
+
components: [],
|
|
35
|
+
isLoading: false,
|
|
36
|
+
lastScanned: null,
|
|
59
37
|
|
|
38
|
+
setComponents: (components) => set({
|
|
39
|
+
components,
|
|
40
|
+
lastScanned: new Date().toISOString()
|
|
41
|
+
}),
|
|
42
|
+
|
|
43
|
+
setIsLoading: (isLoading) => set({ isLoading }),
|
|
44
|
+
scanComponents: scan,
|
|
45
|
+
refreshComponents: scan,
|
|
46
|
+
};
|
|
47
|
+
};
|
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import { StateCreator } from 'zustand';
|
|
2
|
+
import type { Tab } from '../../types';
|
|
2
3
|
|
|
3
4
|
export interface PanelSlice {
|
|
4
|
-
//
|
|
5
|
+
// Panel state
|
|
6
|
+
isOpen: boolean;
|
|
7
|
+
activeTab: Tab;
|
|
8
|
+
panelPosition: { x: number; y: number };
|
|
9
|
+
panelSize: { width: number; height: number };
|
|
5
10
|
isFullscreen: boolean;
|
|
11
|
+
|
|
12
|
+
// Actions
|
|
13
|
+
togglePanel: () => void;
|
|
14
|
+
setActiveTab: (tab: Tab) => void;
|
|
15
|
+
setPanelPosition: (position: { x: number; y: number }) => void;
|
|
16
|
+
setPanelSize: (size: { width: number; height: number }) => void;
|
|
6
17
|
toggleFullscreen: () => void;
|
|
7
18
|
setFullscreen: (value: boolean) => void;
|
|
8
19
|
}
|
|
9
20
|
|
|
10
21
|
export const createPanelSlice: StateCreator<PanelSlice, [], [], PanelSlice> = (set) => ({
|
|
22
|
+
isOpen: false,
|
|
23
|
+
activeTab: 'variables' as Tab,
|
|
24
|
+
panelPosition: { x: 20, y: 20 },
|
|
25
|
+
panelSize: { width: 420, height: 600 },
|
|
11
26
|
isFullscreen: false,
|
|
12
27
|
|
|
28
|
+
togglePanel: () => set((state) => ({ isOpen: !state.isOpen })),
|
|
29
|
+
setActiveTab: (tab) => set({ activeTab: tab }),
|
|
30
|
+
setPanelPosition: (position) => set({ panelPosition: position }),
|
|
31
|
+
setPanelSize: (size) => set({ panelSize: size }),
|
|
13
32
|
toggleFullscreen: () => set((state) => ({ isFullscreen: !state.isFullscreen })),
|
|
14
|
-
|
|
15
33
|
setFullscreen: (value) => set({ isFullscreen: value }),
|
|
16
34
|
});
|
|
17
|
-
|