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