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,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Icon } from '@/components/icons';
|
|
4
|
+
import type { AssetFile } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface AssetGridProps {
|
|
7
|
+
files: AssetFile[];
|
|
8
|
+
selectedFiles: string[];
|
|
9
|
+
onSelect: (path: string, multi: boolean) => void;
|
|
10
|
+
onDelete: (path: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatSize(bytes: number): string {
|
|
14
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
15
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
16
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function AssetGrid({ files, selectedFiles, onSelect, onDelete }: AssetGridProps) {
|
|
20
|
+
if (files.length === 0) {
|
|
21
|
+
return (
|
|
22
|
+
<div className="text-center py-8 text-black/60 text-xs">
|
|
23
|
+
No files in this folder
|
|
24
|
+
</div>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="grid grid-cols-3 gap-2">
|
|
30
|
+
{files.map((file) => (
|
|
31
|
+
<div
|
|
32
|
+
key={file.path}
|
|
33
|
+
onClick={(e) => onSelect(file.path, e.shiftKey || e.metaKey)}
|
|
34
|
+
className={`relative group rounded-lg border overflow-hidden cursor-pointer transition-colors ${
|
|
35
|
+
selectedFiles.includes(file.path)
|
|
36
|
+
? 'border-focus ring-2 ring-focus/20'
|
|
37
|
+
: 'border-border hover:border-focus'
|
|
38
|
+
}`}
|
|
39
|
+
>
|
|
40
|
+
{/* Preview */}
|
|
41
|
+
<div className="aspect-square bg-sun-yellow/20 flex items-center justify-center">
|
|
42
|
+
{file.type === 'image' ? (
|
|
43
|
+
<img
|
|
44
|
+
src={file.path}
|
|
45
|
+
alt={file.name}
|
|
46
|
+
className="max-w-full max-h-full object-contain"
|
|
47
|
+
/>
|
|
48
|
+
) : (
|
|
49
|
+
<span className="text-2xl">
|
|
50
|
+
{file.type === 'video' ? '🎬' : '📄'}
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
{/* Info */}
|
|
56
|
+
<div className="p-2 bg-warm-cloud">
|
|
57
|
+
<p className="text-xs font-medium text-black truncate">{file.name}</p>
|
|
58
|
+
<p className="text-xs text-black/60">{formatSize(file.size)}</p>
|
|
59
|
+
</div>
|
|
60
|
+
|
|
61
|
+
{/* Delete button */}
|
|
62
|
+
<button
|
|
63
|
+
onClick={(e) => {
|
|
64
|
+
e.stopPropagation();
|
|
65
|
+
onDelete(file.path);
|
|
66
|
+
}}
|
|
67
|
+
className="absolute top-1 right-1 w-6 h-6 bg-error-red text-cream rounded-md opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center"
|
|
68
|
+
>
|
|
69
|
+
<Icon name="close" size={12} className="text-cream" />
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
))}
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import type { AssetFolder, AssetFile } from '../../types';
|
|
5
|
+
|
|
6
|
+
interface FolderTreeProps {
|
|
7
|
+
folder: AssetFolder;
|
|
8
|
+
selectedFolder: string | null;
|
|
9
|
+
onSelectFolder: (path: string) => void;
|
|
10
|
+
depth?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function FolderTree({ folder, selectedFolder, onSelectFolder, depth = 0 }: FolderTreeProps) {
|
|
14
|
+
const [expanded, setExpanded] = useState(depth < 2);
|
|
15
|
+
|
|
16
|
+
const subfolders = folder.children.filter((c): c is AssetFolder => 'children' in c);
|
|
17
|
+
const files = folder.children.filter((c): c is AssetFile => !('children' in c));
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div style={{ paddingLeft: depth > 0 ? '12px' : '0' }}>
|
|
21
|
+
<button
|
|
22
|
+
onClick={() => {
|
|
23
|
+
setExpanded(!expanded);
|
|
24
|
+
onSelectFolder(folder.path);
|
|
25
|
+
}}
|
|
26
|
+
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-md text-xs text-left transition-colors ${
|
|
27
|
+
selectedFolder === folder.path
|
|
28
|
+
? 'bg-sun-yellow text-black'
|
|
29
|
+
: 'hover:bg-sun-yellow/20 text-black'
|
|
30
|
+
}`}
|
|
31
|
+
>
|
|
32
|
+
<span>{expanded ? '📂' : '📁'}</span>
|
|
33
|
+
<span className="flex-1">{folder.name}</span>
|
|
34
|
+
<span className="text-black/60">{files.length}</span>
|
|
35
|
+
</button>
|
|
36
|
+
|
|
37
|
+
{expanded && subfolders.length > 0 && (
|
|
38
|
+
<div className="mt-1">
|
|
39
|
+
{subfolders.map((subfolder) => (
|
|
40
|
+
<FolderTree
|
|
41
|
+
key={subfolder.path}
|
|
42
|
+
folder={subfolder}
|
|
43
|
+
selectedFolder={selectedFolder}
|
|
44
|
+
onSelectFolder={onSelectFolder}
|
|
45
|
+
depth={depth + 1}
|
|
46
|
+
/>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
interface UploadDropzoneProps {
|
|
6
|
+
onUpload: (files: File[]) => void;
|
|
7
|
+
isUploading: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function UploadDropzone({ onUpload, isUploading }: UploadDropzoneProps) {
|
|
11
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
12
|
+
|
|
13
|
+
const handleDragOver = useCallback((e: React.DragEvent) => {
|
|
14
|
+
e.preventDefault();
|
|
15
|
+
setIsDragging(true);
|
|
16
|
+
}, []);
|
|
17
|
+
|
|
18
|
+
const handleDragLeave = useCallback((e: React.DragEvent) => {
|
|
19
|
+
e.preventDefault();
|
|
20
|
+
setIsDragging(false);
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
const handleDrop = useCallback((e: React.DragEvent) => {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
setIsDragging(false);
|
|
26
|
+
|
|
27
|
+
const files = Array.from(e.dataTransfer.files);
|
|
28
|
+
if (files.length > 0) {
|
|
29
|
+
onUpload(files);
|
|
30
|
+
}
|
|
31
|
+
}, [onUpload]);
|
|
32
|
+
|
|
33
|
+
const handleFileSelect = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
|
34
|
+
const files = Array.from(e.target.files || []);
|
|
35
|
+
if (files.length > 0) {
|
|
36
|
+
onUpload(files);
|
|
37
|
+
}
|
|
38
|
+
e.target.value = '';
|
|
39
|
+
}, [onUpload]);
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
onDragOver={handleDragOver}
|
|
44
|
+
onDragLeave={handleDragLeave}
|
|
45
|
+
onDrop={handleDrop}
|
|
46
|
+
className={`border-2 border-dashed rounded-lg p-4 text-center transition-colors ${
|
|
47
|
+
isDragging
|
|
48
|
+
? 'border-focus bg-focus/10'
|
|
49
|
+
: 'border-border hover:border-focus'
|
|
50
|
+
}`}
|
|
51
|
+
>
|
|
52
|
+
<input
|
|
53
|
+
type="file"
|
|
54
|
+
id="file-upload"
|
|
55
|
+
multiple
|
|
56
|
+
onChange={handleFileSelect}
|
|
57
|
+
className="hidden"
|
|
58
|
+
accept="image/*,video/*"
|
|
59
|
+
/>
|
|
60
|
+
|
|
61
|
+
<label
|
|
62
|
+
htmlFor="file-upload"
|
|
63
|
+
className="cursor-pointer block"
|
|
64
|
+
>
|
|
65
|
+
<div className="text-2xl mb-2">📤</div>
|
|
66
|
+
<p className="text-xs text-black mb-1">
|
|
67
|
+
{isUploading ? 'Uploading...' : 'Drop files here or click to upload'}
|
|
68
|
+
</p>
|
|
69
|
+
<p className="text-xs text-black/60">
|
|
70
|
+
Images & videos only
|
|
71
|
+
</p>
|
|
72
|
+
</label>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useRef } from 'react';
|
|
4
|
+
import { useDevToolsStore } from '../../store';
|
|
5
|
+
import { FolderTree } from './FolderTree';
|
|
6
|
+
import { AssetGrid } from './AssetGrid';
|
|
7
|
+
import { UploadDropzone } from './UploadDropzone';
|
|
8
|
+
import type { AssetFile, AssetFolder } from '../../types';
|
|
9
|
+
import { Button } from '@/components/ui/Button';
|
|
10
|
+
|
|
11
|
+
export function AssetsTab() {
|
|
12
|
+
const {
|
|
13
|
+
assets,
|
|
14
|
+
selectedFolder,
|
|
15
|
+
setSelectedFolder,
|
|
16
|
+
refreshAssets,
|
|
17
|
+
uploadAsset,
|
|
18
|
+
deleteAsset,
|
|
19
|
+
optimizeAssets,
|
|
20
|
+
isLoading
|
|
21
|
+
} = useDevToolsStore();
|
|
22
|
+
|
|
23
|
+
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
|
|
24
|
+
const [isUploading, setIsUploading] = useState(false);
|
|
25
|
+
const [isOptimizing, setIsOptimizing] = useState(false);
|
|
26
|
+
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
|
|
27
|
+
const messageTimerRef = useRef<NodeJS.Timeout>(undefined);
|
|
28
|
+
|
|
29
|
+
// Cleanup timeout on unmount
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
return () => {
|
|
32
|
+
if (messageTimerRef.current) clearTimeout(messageTimerRef.current);
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
refreshAssets();
|
|
38
|
+
}, []);
|
|
39
|
+
|
|
40
|
+
// Get files in selected folder
|
|
41
|
+
const getFilesInFolder = (folder: AssetFolder | null, targetPath: string | null): AssetFile[] => {
|
|
42
|
+
if (!folder || !targetPath) return [];
|
|
43
|
+
|
|
44
|
+
if (folder.path === targetPath) {
|
|
45
|
+
return folder.children.filter((c): c is AssetFile => !('children' in c));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
for (const child of folder.children) {
|
|
49
|
+
if ('children' in child) {
|
|
50
|
+
const result = getFilesInFolder(child, targetPath);
|
|
51
|
+
if (result.length > 0 || child.path === targetPath) {
|
|
52
|
+
return result.length > 0 ? result : child.children.filter((c): c is AssetFile => !('children' in c));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return [];
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const currentFiles = assets ? getFilesInFolder(assets, selectedFolder || assets.path) : [];
|
|
61
|
+
|
|
62
|
+
const handleUpload = async (files: File[]) => {
|
|
63
|
+
setIsUploading(true);
|
|
64
|
+
try {
|
|
65
|
+
for (const file of files) {
|
|
66
|
+
const folder = selectedFolder?.replace('/assets/', '') || '';
|
|
67
|
+
await uploadAsset(file, folder);
|
|
68
|
+
}
|
|
69
|
+
setMessage({ type: 'success', text: `Uploaded ${files.length} file(s)` });
|
|
70
|
+
} catch (error) {
|
|
71
|
+
setMessage({ type: 'error', text: 'Upload failed' });
|
|
72
|
+
} finally {
|
|
73
|
+
setIsUploading(false);
|
|
74
|
+
if (messageTimerRef.current) clearTimeout(messageTimerRef.current);
|
|
75
|
+
messageTimerRef.current = setTimeout(() => setMessage(null), 3000);
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const handleDelete = async (path: string) => {
|
|
80
|
+
if (!confirm('Delete this file?')) return;
|
|
81
|
+
await deleteAsset(path);
|
|
82
|
+
setSelectedFiles((prev) => prev.filter((p) => p !== path));
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const handleOptimize = async () => {
|
|
86
|
+
if (selectedFiles.length === 0) return;
|
|
87
|
+
setIsOptimizing(true);
|
|
88
|
+
try {
|
|
89
|
+
await optimizeAssets(selectedFiles);
|
|
90
|
+
setMessage({ type: 'success', text: 'Optimization complete!' });
|
|
91
|
+
} catch {
|
|
92
|
+
setMessage({ type: 'error', text: 'Optimization failed' });
|
|
93
|
+
} finally {
|
|
94
|
+
setIsOptimizing(false);
|
|
95
|
+
if (messageTimerRef.current) clearTimeout(messageTimerRef.current);
|
|
96
|
+
messageTimerRef.current = setTimeout(() => setMessage(null), 3000);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const handleSelect = (path: string, multi: boolean) => {
|
|
101
|
+
setSelectedFiles((prev) => {
|
|
102
|
+
if (multi) {
|
|
103
|
+
return prev.includes(path)
|
|
104
|
+
? prev.filter((p) => p !== path)
|
|
105
|
+
: [...prev, path];
|
|
106
|
+
}
|
|
107
|
+
return prev.includes(path) && prev.length === 1 ? [] : [path];
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<div className="flex flex-col h-full overflow-auto pt-4 pb-4 pl-4 pr-2 bg-[var(--color-white)] border border-black rounded space-y-4">
|
|
113
|
+
{/* Header */}
|
|
114
|
+
<div className="flex items-center justify-between">
|
|
115
|
+
<h2 className="font-joystix text-sm uppercase text-black">Assets</h2>
|
|
116
|
+
<div className="flex gap-2">
|
|
117
|
+
{selectedFiles.length > 0 && (
|
|
118
|
+
<Button
|
|
119
|
+
variant="primary"
|
|
120
|
+
size="md"
|
|
121
|
+
iconName="lightning"
|
|
122
|
+
onClick={handleOptimize}
|
|
123
|
+
disabled={isOptimizing}
|
|
124
|
+
>
|
|
125
|
+
{isOptimizing ? 'Optimizing...' : `Optimize (${selectedFiles.length})`}
|
|
126
|
+
</Button>
|
|
127
|
+
)}
|
|
128
|
+
<Button
|
|
129
|
+
variant="outline"
|
|
130
|
+
size="md"
|
|
131
|
+
iconName="refresh"
|
|
132
|
+
onClick={() => refreshAssets()}
|
|
133
|
+
disabled={isLoading}
|
|
134
|
+
>
|
|
135
|
+
{isLoading ? 'Loading...' : 'Refresh'}
|
|
136
|
+
</Button>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
|
|
140
|
+
{/* Message */}
|
|
141
|
+
{message && (
|
|
142
|
+
<div
|
|
143
|
+
className={`px-3 py-2 font-mondwest text-base rounded-sm ${
|
|
144
|
+
message.type === 'success'
|
|
145
|
+
? 'bg-success-green text-black'
|
|
146
|
+
: 'bg-error-red text-cream'
|
|
147
|
+
}`}
|
|
148
|
+
>
|
|
149
|
+
{message.text}
|
|
150
|
+
</div>
|
|
151
|
+
)}
|
|
152
|
+
|
|
153
|
+
{/* Upload Dropzone */}
|
|
154
|
+
<UploadDropzone onUpload={handleUpload} isUploading={isUploading} />
|
|
155
|
+
|
|
156
|
+
{/* Content */}
|
|
157
|
+
<div className="flex gap-4">
|
|
158
|
+
{/* Folder Tree */}
|
|
159
|
+
{assets && (
|
|
160
|
+
<div className="w-1/3 border border-black rounded-md p-2 max-h-[300px] overflow-y-auto">
|
|
161
|
+
<FolderTree
|
|
162
|
+
folder={assets}
|
|
163
|
+
selectedFolder={selectedFolder || assets.path}
|
|
164
|
+
onSelectFolder={setSelectedFolder}
|
|
165
|
+
/>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
|
|
169
|
+
{/* Asset Grid */}
|
|
170
|
+
<div className="flex-1 border border-black rounded-md p-2 max-h-[300px] overflow-y-auto">
|
|
171
|
+
<AssetGrid
|
|
172
|
+
files={currentFiles}
|
|
173
|
+
selectedFiles={selectedFiles}
|
|
174
|
+
onSelect={handleSelect}
|
|
175
|
+
onDelete={handleDelete}
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
</div>
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { Button } from '@/components/ui';
|
|
5
|
+
|
|
6
|
+
interface AddTabButtonProps {
|
|
7
|
+
onAdd: (folderName: string) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function AddTabButton({ onAdd }: AddTabButtonProps) {
|
|
11
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
12
|
+
const [folderName, setFolderName] = useState('');
|
|
13
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
if (isEditing && inputRef.current) {
|
|
17
|
+
inputRef.current.focus();
|
|
18
|
+
}
|
|
19
|
+
}, [isEditing]);
|
|
20
|
+
|
|
21
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
22
|
+
if (e.key === 'Enter' && folderName.trim()) {
|
|
23
|
+
onAdd(folderName.trim());
|
|
24
|
+
setFolderName('');
|
|
25
|
+
setIsEditing(false);
|
|
26
|
+
} else if (e.key === 'Escape') {
|
|
27
|
+
setFolderName('');
|
|
28
|
+
setIsEditing(false);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (isEditing) {
|
|
33
|
+
return (
|
|
34
|
+
<input
|
|
35
|
+
ref={inputRef}
|
|
36
|
+
type="text"
|
|
37
|
+
value={folderName}
|
|
38
|
+
onChange={(e) => setFolderName(e.target.value)}
|
|
39
|
+
onKeyDown={handleKeyDown}
|
|
40
|
+
onBlur={() => {
|
|
41
|
+
// Only close if empty, otherwise keep editing
|
|
42
|
+
if (!folderName.trim()) {
|
|
43
|
+
setIsEditing(false);
|
|
44
|
+
}
|
|
45
|
+
}}
|
|
46
|
+
placeholder="Folder name..."
|
|
47
|
+
className="flex items-center justify-center px-4 py-2 font-joystix text-xs uppercase cursor-text select-none text-black transition-all duration-200 ease-out relative border border-black rounded-sm bg-warm-cloud focus:outline-none focus:ring-2 focus:ring-sun-yellow"
|
|
48
|
+
style={{ minWidth: '120px' }}
|
|
49
|
+
/>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<Button
|
|
55
|
+
variant="primary"
|
|
56
|
+
size="md"
|
|
57
|
+
iconOnly={true}
|
|
58
|
+
iconName="plus"
|
|
59
|
+
onClick={() => setIsEditing(true)}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from 'react';
|
|
4
|
+
import { Input, Button } from '@/components/ui';
|
|
5
|
+
import type { DiscoveredComponent } from '../../types';
|
|
6
|
+
|
|
7
|
+
interface ComponentListProps {
|
|
8
|
+
components: DiscoveredComponent[];
|
|
9
|
+
folderName: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Section component matching DesignSystemTab style
|
|
13
|
+
function Section({ title, children }: { title: string; children: React.ReactNode }) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="mb-6">
|
|
16
|
+
<h3 className="font-joystix text-xs uppercase text-black mb-3 border-b border-black/20 pb-2">
|
|
17
|
+
{title}
|
|
18
|
+
</h3>
|
|
19
|
+
<div className="space-y-3">
|
|
20
|
+
{children}
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Props display component matching DesignSystemTab style
|
|
27
|
+
function PropsDisplay({ props }: { props: string }) {
|
|
28
|
+
return (
|
|
29
|
+
<code className="font-mono text-xs text-black/60 bg-black/5 px-2 py-1 rounded-sm block mt-2">
|
|
30
|
+
{props}
|
|
31
|
+
</code>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Component row display
|
|
36
|
+
function ComponentRow({ component, folderName }: { component: DiscoveredComponent; folderName: string }) {
|
|
37
|
+
const [copied, setCopied] = useState(false);
|
|
38
|
+
const copiedTimerRef = useRef<NodeJS.Timeout>(undefined);
|
|
39
|
+
|
|
40
|
+
// Cleanup timeout on unmount
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
return () => {
|
|
43
|
+
if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
|
|
44
|
+
};
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const handleCopyCursorCommand = () => {
|
|
48
|
+
const propsList = component.props.length > 0
|
|
49
|
+
? component.props
|
|
50
|
+
.map((prop) => {
|
|
51
|
+
const required = prop.required ? 'required' : 'optional';
|
|
52
|
+
const defaultValue = prop.defaultValue ? `, default: ${prop.defaultValue}` : '';
|
|
53
|
+
return `- ${prop.name}: ${prop.type} (${required}${defaultValue})`;
|
|
54
|
+
})
|
|
55
|
+
.join('\n')
|
|
56
|
+
: 'No props defined';
|
|
57
|
+
|
|
58
|
+
const command = `Create a preview section for the ${component.name} component located at ${component.path}.
|
|
59
|
+
|
|
60
|
+
Props:
|
|
61
|
+
${propsList}
|
|
62
|
+
|
|
63
|
+
Add it to devtools/tabs/ComponentsTab/previews/${folderName}.tsx following the Section/Row/PropsDisplay pattern from DesignSystemTab.tsx.
|
|
64
|
+
Include example usage for each variant/prop combination.
|
|
65
|
+
If the preview file doesn't exist, create it with the necessary imports.`;
|
|
66
|
+
|
|
67
|
+
navigator.clipboard.writeText(command).then(() => {
|
|
68
|
+
setCopied(true);
|
|
69
|
+
if (copiedTimerRef.current) clearTimeout(copiedTimerRef.current);
|
|
70
|
+
copiedTimerRef.current = setTimeout(() => setCopied(false), 2000);
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const propsString = component.props.length > 0
|
|
75
|
+
? component.props
|
|
76
|
+
.map((prop) => {
|
|
77
|
+
const optional = prop.required ? '' : '?';
|
|
78
|
+
const defaultValue = prop.defaultValue ? ` = ${prop.defaultValue}` : '';
|
|
79
|
+
return `${prop.name}${optional}: ${prop.type}${defaultValue}`;
|
|
80
|
+
})
|
|
81
|
+
.join(', ')
|
|
82
|
+
: 'No props';
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div className="border border-black bg-warm-cloud -mt-px first:mt-0">
|
|
86
|
+
<div className="overflow-hidden transition-[height] duration-200 ease-out">
|
|
87
|
+
<div className="px-4 pb-4">
|
|
88
|
+
<div className="space-y-6">
|
|
89
|
+
<div className="mb-6">
|
|
90
|
+
<div className="flex items-center justify-between mb-3">
|
|
91
|
+
<h4 className="font-joystix text-xs uppercase text-black">{component.name}</h4>
|
|
92
|
+
<Button
|
|
93
|
+
variant="outline"
|
|
94
|
+
size="sm"
|
|
95
|
+
iconName={copied ? "checkmark-filled" : undefined}
|
|
96
|
+
onClick={handleCopyCursorCommand}
|
|
97
|
+
>
|
|
98
|
+
{copied ? 'Copied' : 'Copy Cursor Command'}
|
|
99
|
+
</Button>
|
|
100
|
+
</div>
|
|
101
|
+
<div className="space-y-2">
|
|
102
|
+
<div className="font-mondwest text-sm text-black/60 font-mono">
|
|
103
|
+
{component.path}
|
|
104
|
+
</div>
|
|
105
|
+
{component.props.length > 0 && (
|
|
106
|
+
<PropsDisplay props={propsString} />
|
|
107
|
+
)}
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function ComponentList({ components, folderName }: ComponentListProps) {
|
|
118
|
+
const [search, setSearch] = useState('');
|
|
119
|
+
|
|
120
|
+
const filtered = components.filter((c) =>
|
|
121
|
+
c.name.toLowerCase().includes(search.toLowerCase()) ||
|
|
122
|
+
c.path.toLowerCase().includes(search.toLowerCase())
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<div className="space-y-4">
|
|
127
|
+
{/* Search */}
|
|
128
|
+
<Input
|
|
129
|
+
type="text"
|
|
130
|
+
value={search}
|
|
131
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
132
|
+
placeholder="Search components..."
|
|
133
|
+
fullWidth={true}
|
|
134
|
+
size="md"
|
|
135
|
+
iconName="search"
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
{/* Component List */}
|
|
139
|
+
<div className="space-y-0">
|
|
140
|
+
{filtered.length > 0 ? (
|
|
141
|
+
filtered.map((component) => (
|
|
142
|
+
<ComponentRow key={component.path} component={component} folderName={folderName} />
|
|
143
|
+
))
|
|
144
|
+
) : (
|
|
145
|
+
<div className="text-center py-8 text-black/60 font-mondwest text-base">
|
|
146
|
+
{search ? `No components match "${search}"` : 'No components found'}
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|