radtools 0.1.0

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 (133) hide show
  1. package/README.md +108 -0
  2. package/bin/radtools.js +5 -0
  3. package/dist/cli/index.js +427 -0
  4. package/package.json +55 -0
  5. package/templates/api-routes/assets/optimize/route.ts +94 -0
  6. package/templates/api-routes/assets/route.ts +159 -0
  7. package/templates/api-routes/components/create-folder/route.ts +55 -0
  8. package/templates/api-routes/components/route.ts +156 -0
  9. package/templates/api-routes/fonts/route.ts +96 -0
  10. package/templates/api-routes/fonts/upload/route.ts +79 -0
  11. package/templates/api-routes/read-css/route.ts +29 -0
  12. package/templates/api-routes/write-css/route.ts +423 -0
  13. package/templates/components/Rad_os/AppWindow.tsx +423 -0
  14. package/templates/components/Rad_os/MobileAppModal.tsx +76 -0
  15. package/templates/components/Rad_os/WindowTitleBar.tsx +290 -0
  16. package/templates/components/icons/Icon.tsx +224 -0
  17. package/templates/components/icons/README.md +85 -0
  18. package/templates/components/icons/index.ts +20 -0
  19. package/templates/components/icons.tsx +164 -0
  20. package/templates/components/ui/Accordion.tsx +268 -0
  21. package/templates/components/ui/Alert.tsx +111 -0
  22. package/templates/components/ui/Badge.tsx +87 -0
  23. package/templates/components/ui/Breadcrumbs.tsx +88 -0
  24. package/templates/components/ui/Button.tsx +249 -0
  25. package/templates/components/ui/Card.tsx +137 -0
  26. package/templates/components/ui/Checkbox.tsx +137 -0
  27. package/templates/components/ui/ContextMenu.tsx +220 -0
  28. package/templates/components/ui/Dialog.tsx +264 -0
  29. package/templates/components/ui/Divider.tsx +70 -0
  30. package/templates/components/ui/DropdownMenu.tsx +301 -0
  31. package/templates/components/ui/HelpPanel.tsx +119 -0
  32. package/templates/components/ui/Input.tsx +176 -0
  33. package/templates/components/ui/Popover.tsx +211 -0
  34. package/templates/components/ui/Progress.tsx +158 -0
  35. package/templates/components/ui/Select.tsx +134 -0
  36. package/templates/components/ui/Sheet.tsx +316 -0
  37. package/templates/components/ui/Slider.tsx +223 -0
  38. package/templates/components/ui/Switch.tsx +155 -0
  39. package/templates/components/ui/Tabs.tsx +253 -0
  40. package/templates/components/ui/Toast.tsx +192 -0
  41. package/templates/components/ui/Tooltip.tsx +129 -0
  42. package/templates/components/ui/hooks/useModalBehavior.ts +66 -0
  43. package/templates/components/ui/index.ts +84 -0
  44. package/templates/devtools/DevToolsPanel.tsx +261 -0
  45. package/templates/devtools/DevToolsProvider.tsx +43 -0
  46. package/templates/devtools/components/BreakpointIndicator.tsx +49 -0
  47. package/templates/devtools/components/ColorPicker.tsx +33 -0
  48. package/templates/devtools/components/ComponentsSecondaryNav.tsx +44 -0
  49. package/templates/devtools/components/ContextualFooter.tsx +56 -0
  50. package/templates/devtools/components/DraggablePanel.tsx +43 -0
  51. package/templates/devtools/components/PrimaryNavigationFooter.tsx +254 -0
  52. package/templates/devtools/components/SearchableColorDropdown.tsx +253 -0
  53. package/templates/devtools/components/SecondaryNavigation.tsx +36 -0
  54. package/templates/devtools/components/TokenDropdown.tsx +47 -0
  55. package/templates/devtools/components/TypographyFooter.tsx +145 -0
  56. package/templates/devtools/hooks/useMockState.ts +16 -0
  57. package/templates/devtools/index.ts +17 -0
  58. package/templates/devtools/lib/componentScanner.ts +78 -0
  59. package/templates/devtools/lib/cssParser.ts +465 -0
  60. package/templates/devtools/lib/searchIndexes.ts +45 -0
  61. package/templates/devtools/lib/selectorGenerator.ts +86 -0
  62. package/templates/devtools/store/index.ts +66 -0
  63. package/templates/devtools/store/slices/assetsSlice.ts +106 -0
  64. package/templates/devtools/store/slices/componentsSlice.ts +59 -0
  65. package/templates/devtools/store/slices/mockStatesSlice.ts +77 -0
  66. package/templates/devtools/store/slices/panelSlice.ts +17 -0
  67. package/templates/devtools/store/slices/typographySlice.ts +538 -0
  68. package/templates/devtools/store/slices/variablesSlice.ts +167 -0
  69. package/templates/devtools/tabs/AssetsTab/AssetGrid.tsx +76 -0
  70. package/templates/devtools/tabs/AssetsTab/FolderTree.tsx +53 -0
  71. package/templates/devtools/tabs/AssetsTab/UploadDropzone.tsx +76 -0
  72. package/templates/devtools/tabs/AssetsTab/index.tsx +182 -0
  73. package/templates/devtools/tabs/ComponentsTab/AddTabButton.tsx +63 -0
  74. package/templates/devtools/tabs/ComponentsTab/ComponentList.tsx +153 -0
  75. package/templates/devtools/tabs/ComponentsTab/DesignSystemTab.tsx +1515 -0
  76. package/templates/devtools/tabs/ComponentsTab/DynamicFolderTab.tsx +113 -0
  77. package/templates/devtools/tabs/ComponentsTab/PropDisplay.tsx +55 -0
  78. package/templates/devtools/tabs/ComponentsTab/index.tsx +167 -0
  79. package/templates/devtools/tabs/ComponentsTab/previews/.gitkeep +4 -0
  80. package/templates/devtools/tabs/ComponentsTab/previews/Rad_os.tsx +262 -0
  81. package/templates/devtools/tabs/ComponentsTab/tabConfig.ts +53 -0
  82. package/templates/devtools/tabs/MockStatesTab/index.tsx +29 -0
  83. package/templates/devtools/tabs/TypographyTab/FontManager.tsx +421 -0
  84. package/templates/devtools/tabs/TypographyTab/TypographyStylesDisplay.tsx +290 -0
  85. package/templates/devtools/tabs/TypographyTab/index.tsx +98 -0
  86. package/templates/devtools/tabs/VariablesTab/BaseColorEditor.tsx +267 -0
  87. package/templates/devtools/tabs/VariablesTab/BorderRadiusEditor.tsx +37 -0
  88. package/templates/devtools/tabs/VariablesTab/ColorModeSelector.tsx +235 -0
  89. package/templates/devtools/tabs/VariablesTab/index.tsx +100 -0
  90. package/templates/devtools/types/index.ts +99 -0
  91. package/templates/globals.css +574 -0
  92. package/templates/hooks/index.ts +1 -0
  93. package/templates/hooks/useWindowManager.ts +212 -0
  94. package/templates/public/assets/icons/avatar.svg +18 -0
  95. package/templates/public/assets/icons/checkmark-filled.svg +14 -0
  96. package/templates/public/assets/icons/checkmark.svg +14 -0
  97. package/templates/public/assets/icons/chevron-down.svg +14 -0
  98. package/templates/public/assets/icons/close.svg +14 -0
  99. package/templates/public/assets/icons/copy.svg +14 -0
  100. package/templates/public/assets/icons/download.svg +14 -0
  101. package/templates/public/assets/icons/expand.svg +31 -0
  102. package/templates/public/assets/icons/file-blank.svg +17 -0
  103. package/templates/public/assets/icons/file-image.svg +19 -0
  104. package/templates/public/assets/icons/file-written.svg +17 -0
  105. package/templates/public/assets/icons/folder-closed.svg +17 -0
  106. package/templates/public/assets/icons/folder-open.svg +17 -0
  107. package/templates/public/assets/icons/hamburger.svg +18 -0
  108. package/templates/public/assets/icons/home-outline.svg +28 -0
  109. package/templates/public/assets/icons/home.svg +30 -0
  110. package/templates/public/assets/icons/hourglass.svg +25 -0
  111. package/templates/public/assets/icons/information-circle.svg +14 -0
  112. package/templates/public/assets/icons/information.svg +17 -0
  113. package/templates/public/assets/icons/lightning.svg +14 -0
  114. package/templates/public/assets/icons/locked.svg +17 -0
  115. package/templates/public/assets/icons/not-allowed.svg +14 -0
  116. package/templates/public/assets/icons/plus.svg +5 -0
  117. package/templates/public/assets/icons/power-thin.svg +17 -0
  118. package/templates/public/assets/icons/power.svg +17 -0
  119. package/templates/public/assets/icons/question-block.svg +14 -0
  120. package/templates/public/assets/icons/question.svg +17 -0
  121. package/templates/public/assets/icons/refresh-block.svg +14 -0
  122. package/templates/public/assets/icons/refresh.svg +17 -0
  123. package/templates/public/assets/icons/save.svg +14 -0
  124. package/templates/public/assets/icons/search.svg +25 -0
  125. package/templates/public/assets/icons/settings.svg +14 -0
  126. package/templates/public/assets/icons/trash-full.svg +21 -0
  127. package/templates/public/assets/icons/trash-open.svg +23 -0
  128. package/templates/public/assets/icons/trash.svg +18 -0
  129. package/templates/public/assets/icons/unlocked.svg +17 -0
  130. package/templates/public/assets/icons/waring-triangle-filled.svg +17 -0
  131. package/templates/public/assets/icons/warning-triangle-filled-2.svg +30 -0
  132. package/templates/public/assets/icons/warning-triangle-lines.svg +29 -0
  133. package/templates/public/assets/icons/wrench.svg +17 -0
@@ -0,0 +1,290 @@
1
+ 'use client';
2
+
3
+ import React, { useState } from 'react';
4
+ import { Icon } from '@/components/icons';
5
+ import { Divider, Button, HelpPanel, Tooltip } from '@/components/ui';
6
+
7
+ // ============================================================================
8
+ // Types
9
+ // ============================================================================
10
+
11
+ interface ActionButtonConfig {
12
+ /** Button text */
13
+ text: string;
14
+ /** Optional icon name (filename without .svg extension) */
15
+ iconName?: string;
16
+ /** Click handler (takes precedence over href) */
17
+ onClick?: () => void;
18
+ /** URL to navigate to */
19
+ href?: string;
20
+ /** Target for href navigation (e.g., '_blank') */
21
+ target?: string;
22
+ }
23
+
24
+ interface WindowTitleBarProps {
25
+ /** Window title text */
26
+ title: string;
27
+ /** Window ID for generating shareable links */
28
+ windowId: string;
29
+ /** Callback when close button is clicked */
30
+ onClose: () => void;
31
+ /** Additional className for styling */
32
+ className?: string;
33
+ /** Icon name (filename without .svg extension) to display before the title */
34
+ iconName?: string;
35
+
36
+ // Visibility controls
37
+ /** Show the window title (default: true) */
38
+ showTitle?: boolean;
39
+ /** Show the copy link button (default: true) */
40
+ showCopyButton?: boolean;
41
+ /** Show the close button (default: true) */
42
+ showCloseButton?: boolean;
43
+ /** Show the help button (default: false) */
44
+ showHelpButton?: boolean;
45
+ /** Show the action button (default: false) */
46
+ showActionButton?: boolean;
47
+ /** Show the fullscreen button (default: false) */
48
+ showFullscreenButton?: boolean;
49
+
50
+ // Help panel configuration
51
+ /** Help content to display in the help panel */
52
+ helpContent?: React.ReactNode;
53
+ /** Title for the help panel */
54
+ helpTitle?: string;
55
+
56
+ // Action button configuration
57
+ /** Configuration for the action button */
58
+ actionButton?: ActionButtonConfig;
59
+
60
+ // Fullscreen configuration
61
+ /** Callback when fullscreen button is clicked */
62
+ onFullscreen?: () => void;
63
+ }
64
+
65
+ // ============================================================================
66
+ // Component
67
+ // ============================================================================
68
+
69
+ /**
70
+ * Window title bar with configurable elements:
71
+ * - Title (window name)
72
+ * - Decorative divider line
73
+ * - Help button (opens contextual help panel)
74
+ * - Action button (customizable CTA - wallet connect, external link, etc.)
75
+ * - Fullscreen button
76
+ * - Copy link button
77
+ * - Close button
78
+ *
79
+ * All elements have visibility controls and sensible defaults.
80
+ *
81
+ * @example
82
+ * // Basic usage (title + copy + close)
83
+ * <WindowTitleBar
84
+ * title="My App"
85
+ * windowId="my-app"
86
+ * onClose={handleClose}
87
+ * />
88
+ *
89
+ * @example
90
+ * // With help panel
91
+ * <WindowTitleBar
92
+ * title="My App"
93
+ * windowId="my-app"
94
+ * onClose={handleClose}
95
+ * showHelpButton
96
+ * helpContent={<p>This is how to use the app...</p>}
97
+ * />
98
+ *
99
+ * @example
100
+ * // With action button (external link)
101
+ * <WindowTitleBar
102
+ * title="My App"
103
+ * windowId="my-app"
104
+ * onClose={handleClose}
105
+ * showActionButton
106
+ * actionButton={{
107
+ * text: "Visit Site",
108
+ * href: "https://example.com",
109
+ * target: "_blank"
110
+ * }}
111
+ * />
112
+ *
113
+ * @example
114
+ * // With wallet connect
115
+ * <WindowTitleBar
116
+ * title="My App"
117
+ * windowId="my-app"
118
+ * onClose={handleClose}
119
+ * showActionButton
120
+ * actionButton={{
121
+ * text: "Connect Wallet",
122
+ * onClick: connectWallet,
123
+ * icon: <WalletIcon />
124
+ * }}
125
+ * />
126
+ */
127
+ export function WindowTitleBar({
128
+ title,
129
+ windowId,
130
+ onClose,
131
+ className = '',
132
+ iconName,
133
+ // Visibility controls with defaults
134
+ showTitle = true,
135
+ showCopyButton = true,
136
+ showCloseButton = true,
137
+ showHelpButton = false,
138
+ showActionButton = false,
139
+ showFullscreenButton = false,
140
+ // Help panel config
141
+ helpContent,
142
+ helpTitle = 'Help',
143
+ // Action button config
144
+ actionButton,
145
+ // Fullscreen config
146
+ onFullscreen,
147
+ }: WindowTitleBarProps) {
148
+ const [copied, setCopied] = useState(false);
149
+ const [helpOpen, setHelpOpen] = useState(false);
150
+
151
+ // Copy link to clipboard
152
+ const handleCopyLink = async () => {
153
+ if (typeof window === 'undefined') return;
154
+
155
+ const url = `${window.location.origin}${window.location.pathname}#${windowId}`;
156
+
157
+ try {
158
+ await navigator.clipboard.writeText(url);
159
+ setCopied(true);
160
+ setTimeout(() => setCopied(false), 2000);
161
+ } catch (err) {
162
+ // Failed to copy link
163
+ }
164
+ };
165
+
166
+ // Handle action button click
167
+ const handleActionClick = () => {
168
+ if (!actionButton) return;
169
+
170
+ if (actionButton.onClick) {
171
+ actionButton.onClick();
172
+ } else if (actionButton.href) {
173
+ window.open(actionButton.href, actionButton.target || '_self');
174
+ }
175
+ };
176
+
177
+ return (
178
+ <>
179
+ <div
180
+ className={`
181
+ flex items-center gap-3 pl-4 pr-1 pt-[4px] pb-1 h-fit
182
+ cursor-move select-none
183
+ ${className}
184
+ `}
185
+ data-drag-handle
186
+ >
187
+ {/* Title */}
188
+ {showTitle && (
189
+ <div className="flex items-center gap-2">
190
+ {iconName && <Icon name={iconName} size={16} />}
191
+ <span className="font-joystix text-sm uppercase tracking-wide text-primary whitespace-nowrap">
192
+ {title}
193
+ </span>
194
+ </div>
195
+ )}
196
+
197
+ {/* Decorative Line */}
198
+ <div className="flex-1">
199
+ <Divider />
200
+ </div>
201
+
202
+ {/* All Buttons */}
203
+ <div className="flex items-center gap-1">
204
+ {/* Action Button */}
205
+ {showActionButton && actionButton && (
206
+ <Button
207
+ variant="outline"
208
+ size="sm"
209
+ onClick={handleActionClick}
210
+ iconName={actionButton.iconName}
211
+ className="shrink-0"
212
+ >
213
+ {actionButton.text}
214
+ </Button>
215
+ )}
216
+
217
+ {/* Help Button */}
218
+ {showHelpButton && (
219
+ <Tooltip content="Help">
220
+ <Button
221
+ variant="ghost"
222
+ size="md"
223
+ iconOnly={true}
224
+ iconName="question"
225
+ onClick={() => setHelpOpen(true)}
226
+ />
227
+ </Tooltip>
228
+ )}
229
+
230
+ {/* Fullscreen Button */}
231
+ {showFullscreenButton && onFullscreen && (
232
+ <Tooltip content="Enter fullscreen">
233
+ <Button
234
+ variant="ghost"
235
+ size="md"
236
+ iconOnly={true}
237
+ iconName="expand"
238
+ onClick={onFullscreen}
239
+ />
240
+ </Tooltip>
241
+ )}
242
+
243
+ {/* Copy Link Button */}
244
+ {showCopyButton && (
245
+ <Tooltip content="Copy link">
246
+ <Button
247
+ variant="ghost"
248
+ size="md"
249
+ iconOnly={true}
250
+ iconName={copied ? "checkmark-filled" : "copy"}
251
+ onClick={handleCopyLink}
252
+ className={copied ? "text-green" : ""}
253
+ />
254
+ </Tooltip>
255
+ )}
256
+
257
+ {/* Close Button */}
258
+ {showCloseButton && (
259
+ <Tooltip content="Close">
260
+ <Button
261
+ variant="ghost"
262
+ size="md"
263
+ iconOnly={true}
264
+ iconName="close"
265
+ onClick={onClose}
266
+ />
267
+ </Tooltip>
268
+ )}
269
+ </div>
270
+ </div>
271
+
272
+ {/* Help Panel (renders as overlay within window) */}
273
+ {showHelpButton && (
274
+ <HelpPanel
275
+ isOpen={helpOpen}
276
+ onClose={() => setHelpOpen(false)}
277
+ title={helpTitle}
278
+ >
279
+ {helpContent || (
280
+ <p className="text-black/60 italic">
281
+ No help content available for this window.
282
+ </p>
283
+ )}
284
+ </HelpPanel>
285
+ )}
286
+ </>
287
+ );
288
+ }
289
+
290
+ export default WindowTitleBar;
@@ -0,0 +1,224 @@
1
+ 'use client';
2
+
3
+ import { useState, useEffect, memo } from 'react';
4
+
5
+ interface IconProps {
6
+ /** Icon filename without .svg extension (e.g., "arrow-left") */
7
+ name: string;
8
+ /** Icon size in pixels (applies to both width and height) */
9
+ size?: number;
10
+ /** Additional CSS classes for styling (use text-* for color) */
11
+ className?: string;
12
+ /** Accessible label for screen readers */
13
+ 'aria-label'?: string;
14
+ }
15
+
16
+ /**
17
+ * Runtime SVG icon loader with automatic currentColor support.
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.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * // Inherits parent text color
26
+ * <div className="text-blue-500">
27
+ * <Icon name="arrow-left" size={24} />
28
+ * </div>
29
+ *
30
+ * // Override color with className
31
+ * <Icon name="check" size={20} className="text-green-500" />
32
+ * ```
33
+ */
34
+ function IconComponent({
35
+ name,
36
+ size = 24,
37
+ className = '',
38
+ 'aria-label': ariaLabel,
39
+ }: IconProps) {
40
+ const [svgContent, setSvgContent] = useState<string | null>(null);
41
+ const [error, setError] = useState(false);
42
+
43
+ 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
+ };
178
+
179
+ if (error || !svgContent) {
180
+ // Get fallback character, or use a default
181
+ const fallbackChar = ICON_FALLBACKS[name] || '?';
182
+
183
+ return (
184
+ <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"
196
+ aria-label={ariaLabel}
197
+ aria-hidden={!ariaLabel}
198
+ >
199
+ {fallbackChar}
200
+ </span>
201
+ );
202
+ }
203
+
204
+ return (
205
+ <span
206
+ 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
+ aria-label={ariaLabel}
216
+ aria-hidden={!ariaLabel}
217
+ dangerouslySetInnerHTML={{ __html: svgContent }}
218
+ />
219
+ );
220
+ }
221
+
222
+ // Memoize to prevent unnecessary re-renders and re-fetches
223
+ export const Icon = memo(IconComponent);
224
+
@@ -0,0 +1,85 @@
1
+ # Icon System
2
+
3
+ Runtime SVG loader with automatic `currentColor` support for easy theming.
4
+
5
+ ## How It Works
6
+
7
+ Icons are loaded at runtime from `public/assets/icons/` and automatically processed to inherit the parent's text color.
8
+
9
+ ## Usage
10
+
11
+ ### 1. Add Icons
12
+
13
+ Upload SVG files via the Assets panel to the `icons` folder, or manually add them to `public/assets/icons/`.
14
+
15
+ ### 2. Use Icons
16
+
17
+ ```tsx
18
+ import { Icon } from '@/components/icons';
19
+
20
+ function MyComponent() {
21
+ return (
22
+ <div className="text-blue-500">
23
+ {/* Inherits parent text color (blue) */}
24
+ <Icon name="arrow-left" size={24} />
25
+
26
+ {/* Override color with className */}
27
+ <Icon name="check" size={20} className="text-green-500" />
28
+
29
+ {/* With accessibility label */}
30
+ <Icon name="close" size={16} aria-label="Close dialog" />
31
+ </div>
32
+ );
33
+ }
34
+ ```
35
+
36
+ ## Props
37
+
38
+ | Prop | Type | Default | Description |
39
+ |------|------|---------|-------------|
40
+ | `name` | `string` | required | Icon filename without `.svg` extension |
41
+ | `size` | `number` | `24` | Icon size in pixels (width & height) |
42
+ | `className` | `string` | `''` | CSS classes for styling (use `text-*` for color) |
43
+ | `aria-label` | `string` | - | Accessible label for screen readers |
44
+
45
+ ## Icon Naming
46
+
47
+ Use the filename without the `.svg` extension:
48
+ - `arrow-left.svg` → `<Icon name="arrow-left" />`
49
+ - `user-profile.svg` → `<Icon name="user-profile" />`
50
+ - `check.svg` → `<Icon name="check" />`
51
+
52
+ ## Color Inheritance
53
+
54
+ Icons automatically use `currentColor`, inheriting the parent's text color:
55
+
56
+ ```tsx
57
+ // Icon will be blue
58
+ <div className="text-blue-500">
59
+ <Icon name="star" />
60
+ </div>
61
+
62
+ // Icon will be red
63
+ <span className="text-red-500">
64
+ <Icon name="heart" />
65
+ </span>
66
+ ```
67
+
68
+ ## SVG Optimization Tips
69
+
70
+ For best results, optimize SVGs before uploading:
71
+
72
+ 1. ✅ Use `fill="currentColor"` or no fill attribute
73
+ 2. ✅ Remove hardcoded colors like `fill="#000000"`
74
+ 3. ✅ Keep the `viewBox` attribute
75
+ 4. ✅ Remove `width` and `height` attributes (the component handles sizing)
76
+
77
+ The loader automatically converts fills to `currentColor`, but pre-optimized SVGs load faster.
78
+
79
+ ## Benefits
80
+
81
+ - **Zero build config** — Works with Turbopack out of the box
82
+ - **Instant updates** — Add icons and use them immediately
83
+ - **Automatic theming** — Icons inherit text color
84
+ - **Memoized** — Prevents unnecessary re-renders
85
+ - **Accessible** — Supports aria-label for screen readers
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Icon System - Runtime SVG loader
3
+ *
4
+ * Icons are loaded at runtime from public/assets/icons/ and automatically
5
+ * processed to use currentColor for easy theming.
6
+ *
7
+ * Usage:
8
+ * import { Icon } from '@/components/icons';
9
+ *
10
+ * <Icon name="arrow-left" size={24} className="text-blue-500" />
11
+ *
12
+ * To add new icons:
13
+ * 1. Upload SVG files via the Assets panel to the icons folder
14
+ * 2. Use them immediately with <Icon name="filename-without-extension" />
15
+ */
16
+
17
+ export { Icon } from './Icon';
18
+
19
+ // Re-export for convenience
20
+ export type { } from './Icon';