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,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
+