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 CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "radtools",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Visual dev tools for Next.js + Tailwind v4 projects",
5
- "keywords": ["nextjs", "tailwind", "devtools", "design-system", "visual-editor"],
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 { useState, useEffect, memo } from 'react';
3
+ import { memo, useEffect, useState } from 'react';
4
4
 
5
5
  interface IconProps {
6
- /** Icon filename without .svg extension (e.g., "arrow-left") */
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
- * Runtime SVG icon loader with automatic currentColor support.
17
+ * Icon component using SVG files from /assets/icons.
18
18
  *
19
- * Icons are loaded from /assets/icons/{name}.svg and automatically
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="arrow-left" size={24} />
25
+ * <Icon name="copy" size={24} />
28
26
  * </div>
29
27
  *
30
28
  * // Override color with className
31
- * <Icon name="check" size={20} className="text-green-500" />
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 [error, setError] = useState(false);
40
+ const iconPath = `/assets/icons/${name}.svg`;
42
41
 
43
42
  useEffect(() => {
44
- let mounted = true;
45
-
46
- const loadIcon = async () => {
47
- try {
48
- const response = await fetch(`/assets/icons/${name}.svg`);
49
-
50
- if (!response.ok) {
51
- throw new Error(`Icon not found: ${name}`);
52
- }
53
-
54
- const svgText = await response.text();
55
-
56
- if (!mounted) return;
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 (error || !svgContent) {
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={`font-['PixelCode'] ${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
- {fallbackChar}
200
- </span>
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 and re-fetches
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 = PanelState &
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
- components: [],
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 (error) {
28
+ } catch {
40
29
  set({ isLoading: false });
41
30
  }
42
- },
31
+ };
43
32
 
44
- refreshComponents: async () => {
45
- set({ isLoading: true });
46
- try {
47
- const res = await fetch('/api/devtools/components');
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
- // Fullscreen state (NOT persisted)
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
-