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,159 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { readdir, stat, unlink, mkdir, writeFile } from 'fs/promises';
|
|
3
|
+
import { join, relative, extname } from 'path';
|
|
4
|
+
|
|
5
|
+
const ASSETS_DIR = join(process.cwd(), 'public', 'assets');
|
|
6
|
+
|
|
7
|
+
interface AssetFile {
|
|
8
|
+
name: string;
|
|
9
|
+
path: string;
|
|
10
|
+
type: 'image' | 'video' | 'other';
|
|
11
|
+
size: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface AssetFolder {
|
|
15
|
+
name: string;
|
|
16
|
+
path: string;
|
|
17
|
+
children: (AssetFolder | AssetFile)[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// GET - List all assets
|
|
21
|
+
export async function GET() {
|
|
22
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{ error: 'Dev tools API not available in production' },
|
|
25
|
+
{ status: 403 }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Ensure assets directory exists
|
|
31
|
+
await mkdir(ASSETS_DIR, { recursive: true });
|
|
32
|
+
|
|
33
|
+
// Also ensure subdirectories exist
|
|
34
|
+
const subdirs = ['icons', 'images', 'logos', 'backgrounds'];
|
|
35
|
+
for (const dir of subdirs) {
|
|
36
|
+
await mkdir(join(ASSETS_DIR, dir), { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const assets = await scanAssets(ASSETS_DIR);
|
|
40
|
+
return NextResponse.json({ assets });
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return NextResponse.json(
|
|
43
|
+
{ error: 'Failed to list assets', details: String(error) },
|
|
44
|
+
{ status: 500 }
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// POST - Upload asset
|
|
50
|
+
export async function POST(req: NextRequest) {
|
|
51
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
52
|
+
return NextResponse.json(
|
|
53
|
+
{ error: 'Dev tools API not available in production' },
|
|
54
|
+
{ status: 403 }
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const formData = await req.formData();
|
|
60
|
+
const file = formData.get('file') as File | null;
|
|
61
|
+
const folder = formData.get('folder') as string || '';
|
|
62
|
+
|
|
63
|
+
if (!file) {
|
|
64
|
+
return NextResponse.json({ error: 'No file provided' }, { status: 400 });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Validate folder path to prevent directory traversal
|
|
68
|
+
const safePath = folder.replace(/\.\./g, '').replace(/^\/+/, '');
|
|
69
|
+
const targetDir = join(ASSETS_DIR, safePath);
|
|
70
|
+
|
|
71
|
+
// Ensure target directory exists
|
|
72
|
+
await mkdir(targetDir, { recursive: true });
|
|
73
|
+
|
|
74
|
+
// Write file
|
|
75
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
76
|
+
const filePath = join(targetDir, file.name);
|
|
77
|
+
await writeFile(filePath, buffer);
|
|
78
|
+
|
|
79
|
+
return NextResponse.json({
|
|
80
|
+
success: true,
|
|
81
|
+
path: '/assets/' + safePath + '/' + file.name
|
|
82
|
+
});
|
|
83
|
+
} catch (error) {
|
|
84
|
+
return NextResponse.json(
|
|
85
|
+
{ error: 'Failed to upload file', details: String(error) },
|
|
86
|
+
{ status: 500 }
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// DELETE - Delete asset
|
|
92
|
+
export async function DELETE(req: NextRequest) {
|
|
93
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
94
|
+
return NextResponse.json(
|
|
95
|
+
{ error: 'Dev tools API not available in production' },
|
|
96
|
+
{ status: 403 }
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const { path: filePath } = await req.json();
|
|
102
|
+
|
|
103
|
+
// Validate path to prevent directory traversal
|
|
104
|
+
const safePath = filePath.replace(/\.\./g, '').replace(/^\/+/, '');
|
|
105
|
+
const fullPath = join(process.cwd(), 'public', safePath);
|
|
106
|
+
|
|
107
|
+
// Ensure the path is within the assets directory
|
|
108
|
+
if (!fullPath.startsWith(ASSETS_DIR)) {
|
|
109
|
+
return NextResponse.json({ error: 'Invalid path' }, { status: 400 });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
await unlink(fullPath);
|
|
113
|
+
return NextResponse.json({ success: true });
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return NextResponse.json(
|
|
116
|
+
{ error: 'Failed to delete file', details: String(error) },
|
|
117
|
+
{ status: 500 }
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function scanAssets(dir: string): Promise<AssetFolder> {
|
|
123
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
124
|
+
const children: (AssetFolder | AssetFile)[] = [];
|
|
125
|
+
|
|
126
|
+
for (const entry of entries) {
|
|
127
|
+
const fullPath = join(dir, entry.name);
|
|
128
|
+
const relativePath = '/assets/' + relative(ASSETS_DIR, fullPath);
|
|
129
|
+
|
|
130
|
+
if (entry.isDirectory()) {
|
|
131
|
+
const folder = await scanAssets(fullPath);
|
|
132
|
+
children.push(folder);
|
|
133
|
+
} else {
|
|
134
|
+
const stats = await stat(fullPath);
|
|
135
|
+
const ext = extname(entry.name).toLowerCase();
|
|
136
|
+
|
|
137
|
+
let type: 'image' | 'video' | 'other' = 'other';
|
|
138
|
+
if (['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg', '.ico'].includes(ext)) {
|
|
139
|
+
type = 'image';
|
|
140
|
+
} else if (['.mp4', '.webm', '.mov', '.avi'].includes(ext)) {
|
|
141
|
+
type = 'video';
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
children.push({
|
|
145
|
+
name: entry.name,
|
|
146
|
+
path: relativePath,
|
|
147
|
+
type,
|
|
148
|
+
size: stats.size,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
name: dir === ASSETS_DIR ? 'assets' : relative(join(dir, '..'), dir),
|
|
155
|
+
path: '/assets/' + relative(ASSETS_DIR, dir),
|
|
156
|
+
children,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { mkdir } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const COMPONENTS_DIR = join(process.cwd(), 'components');
|
|
6
|
+
|
|
7
|
+
export async function POST(request: Request) {
|
|
8
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
9
|
+
return NextResponse.json(
|
|
10
|
+
{ error: 'Dev tools API not available in production' },
|
|
11
|
+
{ status: 403 }
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const { folderName } = await request.json();
|
|
17
|
+
|
|
18
|
+
if (!folderName || typeof folderName !== 'string') {
|
|
19
|
+
return NextResponse.json(
|
|
20
|
+
{ error: 'Folder name is required' },
|
|
21
|
+
{ status: 400 }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Validate folder name (alphanumeric, underscore, hyphen only)
|
|
26
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(folderName)) {
|
|
27
|
+
return NextResponse.json(
|
|
28
|
+
{ error: 'Folder name must contain only letters, numbers, underscores, or hyphens' },
|
|
29
|
+
{ status: 400 }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const folderPath = join(COMPONENTS_DIR, folderName);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
await mkdir(folderPath, { recursive: false });
|
|
37
|
+
return NextResponse.json({ success: true, folderPath });
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const err = error as NodeJS.ErrnoException;
|
|
40
|
+
if (err.code === 'EEXIST') {
|
|
41
|
+
return NextResponse.json(
|
|
42
|
+
{ error: 'Folder already exists' },
|
|
43
|
+
{ status: 409 }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return NextResponse.json(
|
|
50
|
+
{ error: 'Failed to create folder', details: String(error) },
|
|
51
|
+
{ status: 500 }
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { readdir, readFile } from 'fs/promises';
|
|
3
|
+
import { join, relative } from 'path';
|
|
4
|
+
|
|
5
|
+
const COMPONENTS_DIR = join(process.cwd(), 'components');
|
|
6
|
+
|
|
7
|
+
interface DiscoveredComponent {
|
|
8
|
+
name: string;
|
|
9
|
+
path: string;
|
|
10
|
+
props: PropDefinition[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface PropDefinition {
|
|
14
|
+
name: string;
|
|
15
|
+
type: string;
|
|
16
|
+
required: boolean;
|
|
17
|
+
defaultValue?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function GET(request: Request) {
|
|
21
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
22
|
+
return NextResponse.json(
|
|
23
|
+
{ error: 'Dev tools API not available in production' },
|
|
24
|
+
{ status: 403 }
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const { searchParams } = new URL(request.url);
|
|
30
|
+
const folder = searchParams.get('folder');
|
|
31
|
+
|
|
32
|
+
// If folder is specified, scan only that folder
|
|
33
|
+
const scanDir = folder
|
|
34
|
+
? join(COMPONENTS_DIR, folder)
|
|
35
|
+
: COMPONENTS_DIR;
|
|
36
|
+
|
|
37
|
+
const components = await scanComponents(scanDir);
|
|
38
|
+
return NextResponse.json({ components });
|
|
39
|
+
} catch (error) {
|
|
40
|
+
// If components directory doesn't exist, return empty array
|
|
41
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
42
|
+
return NextResponse.json({ components: [] });
|
|
43
|
+
}
|
|
44
|
+
return NextResponse.json(
|
|
45
|
+
{ error: 'Failed to scan components', details: String(error) },
|
|
46
|
+
{ status: 500 }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function scanComponents(dir: string): Promise<DiscoveredComponent[]> {
|
|
52
|
+
const components: DiscoveredComponent[] = [];
|
|
53
|
+
|
|
54
|
+
async function scan(currentDir: string) {
|
|
55
|
+
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
56
|
+
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
const fullPath = join(currentDir, entry.name);
|
|
59
|
+
|
|
60
|
+
if (entry.isDirectory()) {
|
|
61
|
+
await scan(fullPath);
|
|
62
|
+
} else if (entry.name.endsWith('.tsx') || entry.name.endsWith('.ts')) {
|
|
63
|
+
// Skip test files and stories
|
|
64
|
+
if (entry.name.includes('.test.') || entry.name.includes('.stories.')) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
70
|
+
const component = parseComponent(content, '/' + relative(process.cwd(), fullPath));
|
|
71
|
+
if (component) {
|
|
72
|
+
components.push(component);
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
// Failed to parse component - skip
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
await scan(dir);
|
|
82
|
+
return components;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function parseComponent(content: string, filePath: string): DiscoveredComponent | null {
|
|
86
|
+
// Check for default export
|
|
87
|
+
const hasDefaultExport = /export\s+default\s+function\s+(\w+)/.test(content) ||
|
|
88
|
+
/export\s+default\s+(\w+)/.test(content);
|
|
89
|
+
|
|
90
|
+
if (!hasDefaultExport) return null;
|
|
91
|
+
|
|
92
|
+
// Extract component name
|
|
93
|
+
const nameMatch = content.match(/export\s+default\s+function\s+(\w+)/);
|
|
94
|
+
const name = nameMatch?.[1] || 'Unknown';
|
|
95
|
+
|
|
96
|
+
// Extract props interface
|
|
97
|
+
const propsMatch = content.match(/interface\s+(\w+Props)\s*\{([^}]+)\}/);
|
|
98
|
+
const props: PropDefinition[] = [];
|
|
99
|
+
|
|
100
|
+
if (propsMatch) {
|
|
101
|
+
const propsBody = propsMatch[2];
|
|
102
|
+
const propLines = propsBody.split('\n').filter((l) => l.trim());
|
|
103
|
+
|
|
104
|
+
for (const line of propLines) {
|
|
105
|
+
const propMatch = line.match(/(\w+)(\?)?:\s*([^;]+)/);
|
|
106
|
+
if (propMatch) {
|
|
107
|
+
props.push({
|
|
108
|
+
name: propMatch[1],
|
|
109
|
+
type: propMatch[3].trim(),
|
|
110
|
+
required: !propMatch[2],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Try inline type annotation if no interface found
|
|
117
|
+
if (props.length === 0) {
|
|
118
|
+
const inlineMatch = content.match(/\{\s*([^}]+)\s*\}\s*:\s*\{([^}]+)\}/);
|
|
119
|
+
if (inlineMatch) {
|
|
120
|
+
const propsBody = inlineMatch[2];
|
|
121
|
+
const propLines = propsBody.split(/[,;]/).filter((l) => l.trim());
|
|
122
|
+
|
|
123
|
+
for (const line of propLines) {
|
|
124
|
+
const propMatch = line.trim().match(/(\w+)(\?)?:\s*(.+)/);
|
|
125
|
+
if (propMatch) {
|
|
126
|
+
props.push({
|
|
127
|
+
name: propMatch[1],
|
|
128
|
+
type: propMatch[3].trim(),
|
|
129
|
+
required: !propMatch[2],
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Extract default values from destructuring
|
|
137
|
+
const destructureMatch = content.match(/\{\s*([^}]+)\s*\}\s*:\s*(?:\w+Props|\{[^}]+\})/);
|
|
138
|
+
if (destructureMatch) {
|
|
139
|
+
const destructureBody = destructureMatch[1];
|
|
140
|
+
for (const prop of props) {
|
|
141
|
+
const defaultMatch = destructureBody.match(
|
|
142
|
+
new RegExp(`${prop.name}\\s*=\\s*(['"\`]?[^,}]+['"\`]?)`)
|
|
143
|
+
);
|
|
144
|
+
if (defaultMatch) {
|
|
145
|
+
prop.defaultValue = defaultMatch[1].trim();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
name,
|
|
152
|
+
path: filePath,
|
|
153
|
+
props,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { readdir } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
const FONTS_DIR = join(process.cwd(), 'public', 'fonts');
|
|
7
|
+
|
|
8
|
+
export async function GET() {
|
|
9
|
+
// Security: Block in production
|
|
10
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: 'Dev tools API not available in production' },
|
|
13
|
+
{ status: 403 }
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
// Check if fonts directory exists
|
|
19
|
+
if (!existsSync(FONTS_DIR)) {
|
|
20
|
+
return NextResponse.json({ fonts: [] });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Read all files in fonts directory
|
|
24
|
+
const files = await readdir(FONTS_DIR);
|
|
25
|
+
|
|
26
|
+
// Filter to only font files
|
|
27
|
+
const fontFiles = files.filter(file => {
|
|
28
|
+
const ext = file.split('.').pop()?.toLowerCase();
|
|
29
|
+
return ['woff2', 'woff', 'ttf', 'otf'].includes(ext || '');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// Group fonts by family name
|
|
33
|
+
const fontMap = new Map<string, Array<{ filename: string; path: string; format: string }>>();
|
|
34
|
+
|
|
35
|
+
for (const file of fontFiles) {
|
|
36
|
+
// Extract font family name from filename
|
|
37
|
+
// Examples: "Mondwest-Regular.woff2" -> "Mondwest"
|
|
38
|
+
// "Pixeloid-Sans-Bold.woff2" -> "Pixeloid Sans"
|
|
39
|
+
// "joystix_monospace.ttf" -> "Joystix Monospace"
|
|
40
|
+
const nameWithoutExt = file.replace(/\.[^.]+$/, '');
|
|
41
|
+
|
|
42
|
+
// Handle both dash and underscore separators
|
|
43
|
+
const parts = nameWithoutExt.split(/[-_]/);
|
|
44
|
+
|
|
45
|
+
// Handle special cases:
|
|
46
|
+
// - "Pixeloid-Sans" or "Pixeloid-Mono" -> "Pixeloid Sans" / "Pixeloid Mono"
|
|
47
|
+
// - "joystix_monospace" -> "Joystix Monospace"
|
|
48
|
+
// - "Mondwest-Regular" -> "Mondwest"
|
|
49
|
+
let familyName = parts[0];
|
|
50
|
+
|
|
51
|
+
if (parts.length > 1) {
|
|
52
|
+
const secondPart = parts[1];
|
|
53
|
+
// Check if second part is a type (Sans, Mono, Monospace) or a weight/style
|
|
54
|
+
if (secondPart === 'Sans' || secondPart === 'Mono') {
|
|
55
|
+
familyName = `${parts[0]} ${secondPart}`;
|
|
56
|
+
} else if (secondPart.toLowerCase() === 'monospace') {
|
|
57
|
+
familyName = `${parts[0]} Monospace`;
|
|
58
|
+
}
|
|
59
|
+
// Otherwise, it's likely a weight/style (Regular, Bold, etc.), so just use the first part
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Capitalize first letter of each word
|
|
63
|
+
familyName = familyName
|
|
64
|
+
.split(' ')
|
|
65
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
66
|
+
.join(' ');
|
|
67
|
+
|
|
68
|
+
const ext = file.split('.').pop()?.toLowerCase() || '';
|
|
69
|
+
const path = `/fonts/${file}`;
|
|
70
|
+
|
|
71
|
+
if (!fontMap.has(familyName)) {
|
|
72
|
+
fontMap.set(familyName, []);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
fontMap.get(familyName)!.push({
|
|
76
|
+
filename: file,
|
|
77
|
+
path,
|
|
78
|
+
format: ext,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Convert to array format
|
|
83
|
+
const fonts = Array.from(fontMap.entries()).map(([family, files]) => ({
|
|
84
|
+
family,
|
|
85
|
+
files,
|
|
86
|
+
}));
|
|
87
|
+
|
|
88
|
+
return NextResponse.json({ fonts });
|
|
89
|
+
} catch (error) {
|
|
90
|
+
return NextResponse.json(
|
|
91
|
+
{ error: 'Failed to list fonts', details: String(error) },
|
|
92
|
+
{ status: 500 }
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { writeFile, mkdir } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { existsSync } from 'fs';
|
|
5
|
+
|
|
6
|
+
const FONTS_DIR = join(process.cwd(), 'public', 'fonts');
|
|
7
|
+
|
|
8
|
+
export async function POST(req: Request) {
|
|
9
|
+
// Security: Block in production
|
|
10
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: 'Dev tools API not available in production' },
|
|
13
|
+
{ status: 403 }
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const formData = await req.formData();
|
|
19
|
+
const file = formData.get('file') as File;
|
|
20
|
+
const name = formData.get('name') as string;
|
|
21
|
+
const family = formData.get('family') as string;
|
|
22
|
+
const weight = formData.get('weight') as string;
|
|
23
|
+
const style = formData.get('style') as string;
|
|
24
|
+
const format = formData.get('format') as string;
|
|
25
|
+
|
|
26
|
+
if (!file) {
|
|
27
|
+
return NextResponse.json(
|
|
28
|
+
{ error: 'No file provided' },
|
|
29
|
+
{ status: 400 }
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Ensure fonts directory exists
|
|
34
|
+
if (!existsSync(FONTS_DIR)) {
|
|
35
|
+
await mkdir(FONTS_DIR, { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Generate filename
|
|
39
|
+
const safeFamilyName = family.replace(/\s+/g, '-');
|
|
40
|
+
const weightLabel = getWeightLabel(parseInt(weight, 10));
|
|
41
|
+
const styleLabel = style === 'italic' ? '-Italic' : '';
|
|
42
|
+
const filename = `${safeFamilyName}-${weightLabel}${styleLabel}.${format}`;
|
|
43
|
+
|
|
44
|
+
// Write file
|
|
45
|
+
const filePath = join(FONTS_DIR, filename);
|
|
46
|
+
const buffer = Buffer.from(await file.arrayBuffer());
|
|
47
|
+
await writeFile(filePath, buffer);
|
|
48
|
+
|
|
49
|
+
// Return the public path
|
|
50
|
+
const publicPath = `/fonts/${filename}`;
|
|
51
|
+
|
|
52
|
+
return NextResponse.json({
|
|
53
|
+
success: true,
|
|
54
|
+
path: publicPath,
|
|
55
|
+
filename,
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
return NextResponse.json(
|
|
59
|
+
{ error: 'Failed to upload font', details: String(error) },
|
|
60
|
+
{ status: 500 }
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function getWeightLabel(weight: number): string {
|
|
66
|
+
const labels: Record<number, string> = {
|
|
67
|
+
100: 'Thin',
|
|
68
|
+
200: 'ExtraLight',
|
|
69
|
+
300: 'Light',
|
|
70
|
+
400: 'Regular',
|
|
71
|
+
500: 'Medium',
|
|
72
|
+
600: 'SemiBold',
|
|
73
|
+
700: 'Bold',
|
|
74
|
+
800: 'ExtraBold',
|
|
75
|
+
900: 'Black',
|
|
76
|
+
};
|
|
77
|
+
return labels[weight] || 'Regular';
|
|
78
|
+
}
|
|
79
|
+
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { readFile } from 'fs/promises';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
|
|
5
|
+
const GLOBALS_PATH = join(process.cwd(), 'app', 'globals.css');
|
|
6
|
+
|
|
7
|
+
export async function GET() {
|
|
8
|
+
if (process.env.NODE_ENV !== 'development') {
|
|
9
|
+
return NextResponse.json(
|
|
10
|
+
{ error: 'Dev tools API not available in production' },
|
|
11
|
+
{ status: 403 }
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const css = await readFile(GLOBALS_PATH, 'utf-8');
|
|
17
|
+
return new NextResponse(css, {
|
|
18
|
+
headers: {
|
|
19
|
+
'Content-Type': 'text/css',
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return NextResponse.json(
|
|
24
|
+
{ error: 'Failed to read CSS', details: String(error) },
|
|
25
|
+
{ status: 500 }
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|