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.
- package/README.md +108 -0
- package/bin/radtools.js +5 -0
- package/dist/cli/index.js +427 -0
- package/package.json +55 -0
- package/templates/api-routes/assets/optimize/route.ts +94 -0
- package/templates/api-routes/assets/route.ts +159 -0
- package/templates/api-routes/components/create-folder/route.ts +55 -0
- package/templates/api-routes/components/route.ts +156 -0
- package/templates/api-routes/fonts/route.ts +96 -0
- package/templates/api-routes/fonts/upload/route.ts +79 -0
- package/templates/api-routes/read-css/route.ts +29 -0
- package/templates/api-routes/write-css/route.ts +423 -0
- package/templates/components/Rad_os/AppWindow.tsx +423 -0
- package/templates/components/Rad_os/MobileAppModal.tsx +76 -0
- package/templates/components/Rad_os/WindowTitleBar.tsx +290 -0
- package/templates/components/icons/Icon.tsx +224 -0
- package/templates/components/icons/README.md +85 -0
- package/templates/components/icons/index.ts +20 -0
- package/templates/components/icons.tsx +164 -0
- package/templates/components/ui/Accordion.tsx +268 -0
- package/templates/components/ui/Alert.tsx +111 -0
- package/templates/components/ui/Badge.tsx +87 -0
- package/templates/components/ui/Breadcrumbs.tsx +88 -0
- package/templates/components/ui/Button.tsx +249 -0
- package/templates/components/ui/Card.tsx +137 -0
- package/templates/components/ui/Checkbox.tsx +137 -0
- package/templates/components/ui/ContextMenu.tsx +220 -0
- package/templates/components/ui/Dialog.tsx +264 -0
- package/templates/components/ui/Divider.tsx +70 -0
- package/templates/components/ui/DropdownMenu.tsx +301 -0
- package/templates/components/ui/HelpPanel.tsx +119 -0
- package/templates/components/ui/Input.tsx +176 -0
- package/templates/components/ui/Popover.tsx +211 -0
- package/templates/components/ui/Progress.tsx +158 -0
- package/templates/components/ui/Select.tsx +134 -0
- package/templates/components/ui/Sheet.tsx +316 -0
- package/templates/components/ui/Slider.tsx +223 -0
- package/templates/components/ui/Switch.tsx +155 -0
- package/templates/components/ui/Tabs.tsx +253 -0
- package/templates/components/ui/Toast.tsx +192 -0
- package/templates/components/ui/Tooltip.tsx +129 -0
- package/templates/components/ui/hooks/useModalBehavior.ts +66 -0
- package/templates/components/ui/index.ts +84 -0
- package/templates/devtools/DevToolsPanel.tsx +261 -0
- package/templates/devtools/DevToolsProvider.tsx +43 -0
- package/templates/devtools/components/BreakpointIndicator.tsx +49 -0
- package/templates/devtools/components/ColorPicker.tsx +33 -0
- package/templates/devtools/components/ComponentsSecondaryNav.tsx +44 -0
- package/templates/devtools/components/ContextualFooter.tsx +56 -0
- package/templates/devtools/components/DraggablePanel.tsx +43 -0
- package/templates/devtools/components/PrimaryNavigationFooter.tsx +254 -0
- package/templates/devtools/components/SearchableColorDropdown.tsx +253 -0
- package/templates/devtools/components/SecondaryNavigation.tsx +36 -0
- package/templates/devtools/components/TokenDropdown.tsx +47 -0
- package/templates/devtools/components/TypographyFooter.tsx +145 -0
- package/templates/devtools/hooks/useMockState.ts +16 -0
- package/templates/devtools/index.ts +17 -0
- package/templates/devtools/lib/componentScanner.ts +78 -0
- package/templates/devtools/lib/cssParser.ts +465 -0
- package/templates/devtools/lib/searchIndexes.ts +45 -0
- package/templates/devtools/lib/selectorGenerator.ts +86 -0
- package/templates/devtools/store/index.ts +66 -0
- package/templates/devtools/store/slices/assetsSlice.ts +106 -0
- package/templates/devtools/store/slices/componentsSlice.ts +59 -0
- package/templates/devtools/store/slices/mockStatesSlice.ts +77 -0
- package/templates/devtools/store/slices/panelSlice.ts +17 -0
- package/templates/devtools/store/slices/typographySlice.ts +538 -0
- package/templates/devtools/store/slices/variablesSlice.ts +167 -0
- package/templates/devtools/tabs/AssetsTab/AssetGrid.tsx +76 -0
- package/templates/devtools/tabs/AssetsTab/FolderTree.tsx +53 -0
- package/templates/devtools/tabs/AssetsTab/UploadDropzone.tsx +76 -0
- package/templates/devtools/tabs/AssetsTab/index.tsx +182 -0
- package/templates/devtools/tabs/ComponentsTab/AddTabButton.tsx +63 -0
- package/templates/devtools/tabs/ComponentsTab/ComponentList.tsx +153 -0
- package/templates/devtools/tabs/ComponentsTab/DesignSystemTab.tsx +1515 -0
- package/templates/devtools/tabs/ComponentsTab/DynamicFolderTab.tsx +113 -0
- package/templates/devtools/tabs/ComponentsTab/PropDisplay.tsx +55 -0
- package/templates/devtools/tabs/ComponentsTab/index.tsx +167 -0
- package/templates/devtools/tabs/ComponentsTab/previews/.gitkeep +4 -0
- package/templates/devtools/tabs/ComponentsTab/previews/Rad_os.tsx +262 -0
- package/templates/devtools/tabs/ComponentsTab/tabConfig.ts +53 -0
- package/templates/devtools/tabs/MockStatesTab/index.tsx +29 -0
- package/templates/devtools/tabs/TypographyTab/FontManager.tsx +421 -0
- package/templates/devtools/tabs/TypographyTab/TypographyStylesDisplay.tsx +290 -0
- package/templates/devtools/tabs/TypographyTab/index.tsx +98 -0
- package/templates/devtools/tabs/VariablesTab/BaseColorEditor.tsx +267 -0
- package/templates/devtools/tabs/VariablesTab/BorderRadiusEditor.tsx +37 -0
- package/templates/devtools/tabs/VariablesTab/ColorModeSelector.tsx +235 -0
- package/templates/devtools/tabs/VariablesTab/index.tsx +100 -0
- package/templates/devtools/types/index.ts +99 -0
- package/templates/globals.css +574 -0
- package/templates/hooks/index.ts +1 -0
- package/templates/hooks/useWindowManager.ts +212 -0
- package/templates/public/assets/icons/avatar.svg +18 -0
- package/templates/public/assets/icons/checkmark-filled.svg +14 -0
- package/templates/public/assets/icons/checkmark.svg +14 -0
- package/templates/public/assets/icons/chevron-down.svg +14 -0
- package/templates/public/assets/icons/close.svg +14 -0
- package/templates/public/assets/icons/copy.svg +14 -0
- package/templates/public/assets/icons/download.svg +14 -0
- package/templates/public/assets/icons/expand.svg +31 -0
- package/templates/public/assets/icons/file-blank.svg +17 -0
- package/templates/public/assets/icons/file-image.svg +19 -0
- package/templates/public/assets/icons/file-written.svg +17 -0
- package/templates/public/assets/icons/folder-closed.svg +17 -0
- package/templates/public/assets/icons/folder-open.svg +17 -0
- package/templates/public/assets/icons/hamburger.svg +18 -0
- package/templates/public/assets/icons/home-outline.svg +28 -0
- package/templates/public/assets/icons/home.svg +30 -0
- package/templates/public/assets/icons/hourglass.svg +25 -0
- package/templates/public/assets/icons/information-circle.svg +14 -0
- package/templates/public/assets/icons/information.svg +17 -0
- package/templates/public/assets/icons/lightning.svg +14 -0
- package/templates/public/assets/icons/locked.svg +17 -0
- package/templates/public/assets/icons/not-allowed.svg +14 -0
- package/templates/public/assets/icons/plus.svg +5 -0
- package/templates/public/assets/icons/power-thin.svg +17 -0
- package/templates/public/assets/icons/power.svg +17 -0
- package/templates/public/assets/icons/question-block.svg +14 -0
- package/templates/public/assets/icons/question.svg +17 -0
- package/templates/public/assets/icons/refresh-block.svg +14 -0
- package/templates/public/assets/icons/refresh.svg +17 -0
- package/templates/public/assets/icons/save.svg +14 -0
- package/templates/public/assets/icons/search.svg +25 -0
- package/templates/public/assets/icons/settings.svg +14 -0
- package/templates/public/assets/icons/trash-full.svg +21 -0
- package/templates/public/assets/icons/trash-open.svg +23 -0
- package/templates/public/assets/icons/trash.svg +18 -0
- package/templates/public/assets/icons/unlocked.svg +17 -0
- package/templates/public/assets/icons/waring-triangle-filled.svg +17 -0
- package/templates/public/assets/icons/warning-triangle-filled-2.svg +30 -0
- package/templates/public/assets/icons/warning-triangle-lines.svg +29 -0
- 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';
|