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
package/README.md ADDED
@@ -0,0 +1,108 @@
1
+ # RadTools
2
+
3
+ A Webflow-like visual editing dev tools system for Next.js + Tailwind v4 projects.
4
+
5
+ ## Features
6
+
7
+ - **Variables Tab** - Manage design tokens (brand colors, semantic tokens, color modes, border radius)
8
+ - **Typography Tab** - Manage fonts and typography styles
9
+ - **Components Tab** - Auto-discover components from `/components/` directory with prop information
10
+ - **Assets Tab** - Upload, organize, and optimize images in `public/assets/`
11
+ - **Mock States Tab** - Simulate auth, wallet, subscription states during development
12
+
13
+ ## Quick Start
14
+
15
+ 1. Press `⇧⌘K` (Mac) or `⇧Ctrl+K` (Windows/Linux) to toggle the dev tools panel
16
+ 2. The panel is draggable - move it wherever you like
17
+ 3. Switch between tabs to access different features
18
+
19
+ ## Keyboard Shortcuts
20
+
21
+ | Shortcut | Action |
22
+ |----------|--------|
23
+ | `⇧⌘K` / `⇧Ctrl+K` | Toggle dev tools panel |
24
+ | `Esc` | Close modals |
25
+
26
+ ## Tab Guide
27
+
28
+ ### Variables Tab
29
+ Edit your design tokens visually:
30
+ - Add/edit/delete brand colors and neutrals
31
+ - Create semantic tokens that reference brand colors
32
+ - Toggle color modes (dark mode preview)
33
+ - Adjust border radius values
34
+ - Click "Save to CSS" to write changes to `app/globals.css`
35
+
36
+ ### Typography Tab
37
+ Manage fonts and typography styles:
38
+ - Upload and manage font files from `public/fonts/`
39
+ - Configure typography styles for HTML elements (h1-h6, p, code, etc.)
40
+ - Visual editor for typography properties
41
+ - Changes automatically persist to `app/globals.css`
42
+
43
+ ### Components Tab
44
+ Discover all components in your `/components/` directory:
45
+ - Auto-scans for default exports
46
+ - Displays prop types, required status, and default values
47
+ - Click "Refresh" to rescan after adding new components
48
+
49
+ ### Assets Tab
50
+ Manage files in `public/assets/`:
51
+ - Drag and drop to upload images
52
+ - Organize into folders (icons, images, logos, backgrounds)
53
+ - Select images and click "Optimize" for Sharp-based compression
54
+ - Delete assets directly from the UI
55
+
56
+ ### Mock States Tab
57
+ Simulate different app states:
58
+ - Pre-configured presets for auth, wallet, and subscription states
59
+ - Create custom mock states with JSON values
60
+ - Use `useMockState('category')` hook in components to consume mock data
61
+ - Only one state per category can be active at a time
62
+
63
+ ## Using Mock States in Components
64
+
65
+ ```tsx
66
+ import { useMockState } from '@/devtools';
67
+
68
+ function UserProfile() {
69
+ const mockAuth = useMockState('auth');
70
+ const realAuth = useRealAuthHook();
71
+
72
+ // In development with mock active: use mock
73
+ // In development without mock: use real
74
+ // In production: mock is undefined, use real
75
+ const auth = mockAuth ?? realAuth;
76
+
77
+ if (!auth?.isAuthenticated) return <LoginPrompt />;
78
+ return <Profile user={auth.user} />;
79
+ }
80
+ ```
81
+
82
+ ## Production Safety
83
+
84
+ - Dev tools are automatically excluded from production builds
85
+ - `NODE_ENV === 'production'` check prevents rendering
86
+ - API routes return 403 in production
87
+ - `useMockState()` returns `undefined` in production
88
+
89
+ ## File Structure
90
+
91
+ ```
92
+ /devtools/
93
+ ├── index.ts # Public exports
94
+ ├── DevToolsProvider.tsx # Main provider
95
+ ├── DevToolsPanel.tsx # Panel with tabs
96
+ ├── store/ # Zustand store
97
+ ├── tabs/ # Tab components
98
+ ├── components/ # Shared UI components
99
+ ├── hooks/ # Custom hooks
100
+ ├── lib/ # Utilities
101
+ └── types/ # TypeScript types
102
+ ```
103
+
104
+ ## Dependencies
105
+
106
+ - `zustand` - State management
107
+ - `react-draggable` - Draggable panel
108
+ - `sharp` - Image optimization (API routes)
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/cli/index.js').catch((error) => {
3
+ console.error('Failed to load RadTools CLI:', error.message);
4
+ process.exit(1);
5
+ });
@@ -0,0 +1,427 @@
1
+ // src/cli/init.ts
2
+ import * as fs4 from "fs";
3
+ import * as path4 from "path";
4
+ import * as readline from "readline";
5
+ import { fileURLToPath } from "url";
6
+
7
+ // src/cli/utils/detect-package-manager.ts
8
+ import * as fs from "fs";
9
+ import * as path from "path";
10
+ function detectPackageManager(cwd) {
11
+ if (fs.existsSync(path.join(cwd, "bun.lockb"))) {
12
+ return "bun";
13
+ }
14
+ if (fs.existsSync(path.join(cwd, "pnpm-lock.yaml"))) {
15
+ return "pnpm";
16
+ }
17
+ if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
18
+ return "yarn";
19
+ }
20
+ return "npm";
21
+ }
22
+
23
+ // src/cli/utils/install-dependencies.ts
24
+ import { spawn } from "child_process";
25
+ var DEPENDENCIES = ["zustand", "react-draggable", "sharp"];
26
+ function installDependencies(packageManager, cwd) {
27
+ return new Promise((resolve2, reject) => {
28
+ let command;
29
+ let args;
30
+ switch (packageManager) {
31
+ case "npm":
32
+ command = "npm";
33
+ args = ["install", ...DEPENDENCIES];
34
+ break;
35
+ case "yarn":
36
+ command = "yarn";
37
+ args = ["add", ...DEPENDENCIES];
38
+ break;
39
+ case "pnpm":
40
+ command = "pnpm";
41
+ args = ["add", ...DEPENDENCIES];
42
+ break;
43
+ case "bun":
44
+ command = "bun";
45
+ args = ["add", ...DEPENDENCIES];
46
+ break;
47
+ }
48
+ const child = spawn(command, args, {
49
+ cwd,
50
+ stdio: "inherit",
51
+ shell: true
52
+ });
53
+ child.on("close", (code) => {
54
+ if (code === 0) {
55
+ resolve2();
56
+ } else {
57
+ reject(new Error(`${command} exited with code ${code}`));
58
+ }
59
+ });
60
+ child.on("error", (err) => {
61
+ reject(err);
62
+ });
63
+ });
64
+ }
65
+
66
+ // src/cli/utils/copy-templates.ts
67
+ import * as fs2 from "fs";
68
+ import * as path2 from "path";
69
+ function copyDir(src, dest) {
70
+ fs2.mkdirSync(dest, { recursive: true });
71
+ const entries = fs2.readdirSync(src, { withFileTypes: true });
72
+ for (const entry of entries) {
73
+ const srcPath = path2.join(src, entry.name);
74
+ const destPath = path2.join(dest, entry.name);
75
+ if (entry.isDirectory()) {
76
+ copyDir(srcPath, destPath);
77
+ } else {
78
+ fs2.copyFileSync(srcPath, destPath);
79
+ }
80
+ }
81
+ }
82
+ async function copyTemplates(templatesDir, targetDir) {
83
+ if (!fs2.existsSync(templatesDir)) {
84
+ throw new Error(`Templates directory not found: ${templatesDir}`);
85
+ }
86
+ const devtoolsSrc = path2.join(templatesDir, "devtools");
87
+ const devtoolsDest = path2.join(targetDir, "devtools");
88
+ if (fs2.existsSync(devtoolsSrc)) {
89
+ copyDir(devtoolsSrc, devtoolsDest);
90
+ }
91
+ const componentsSrc = path2.join(templatesDir, "components");
92
+ const componentsDest = path2.join(targetDir, "components");
93
+ if (fs2.existsSync(componentsSrc)) {
94
+ fs2.mkdirSync(componentsDest, { recursive: true });
95
+ const uiSrc = path2.join(componentsSrc, "ui");
96
+ const uiDest = path2.join(componentsDest, "ui");
97
+ if (fs2.existsSync(uiSrc)) {
98
+ copyDir(uiSrc, uiDest);
99
+ }
100
+ const radOsSrc = path2.join(componentsSrc, "Rad_os");
101
+ const radOsDest = path2.join(componentsDest, "Rad_os");
102
+ if (fs2.existsSync(radOsSrc)) {
103
+ copyDir(radOsSrc, radOsDest);
104
+ }
105
+ const iconsSrc2 = path2.join(componentsSrc, "icons");
106
+ const iconsDest2 = path2.join(componentsDest, "icons");
107
+ if (fs2.existsSync(iconsSrc2)) {
108
+ copyDir(iconsSrc2, iconsDest2);
109
+ }
110
+ const iconsFileSrc = path2.join(componentsSrc, "icons.tsx");
111
+ const iconsFileDest = path2.join(componentsDest, "icons.tsx");
112
+ if (fs2.existsSync(iconsFileSrc)) {
113
+ fs2.copyFileSync(iconsFileSrc, iconsFileDest);
114
+ }
115
+ }
116
+ const hooksSrc = path2.join(templatesDir, "hooks");
117
+ const hooksDest = path2.join(targetDir, "hooks");
118
+ if (fs2.existsSync(hooksSrc)) {
119
+ copyDir(hooksSrc, hooksDest);
120
+ }
121
+ const apiRoutesSrc = path2.join(templatesDir, "api-routes");
122
+ const apiRoutesDest = path2.join(targetDir, "app", "api", "devtools");
123
+ if (fs2.existsSync(apiRoutesSrc)) {
124
+ fs2.mkdirSync(path2.join(targetDir, "app", "api"), { recursive: true });
125
+ copyDir(apiRoutesSrc, apiRoutesDest);
126
+ }
127
+ const iconsSrc = path2.join(templatesDir, "public", "assets", "icons");
128
+ const iconsDest = path2.join(targetDir, "public", "assets", "icons");
129
+ if (fs2.existsSync(iconsSrc)) {
130
+ copyDir(iconsSrc, iconsDest);
131
+ }
132
+ const globalsCssSrc = path2.join(templatesDir, "globals.css");
133
+ const globalsCssDest = path2.join(targetDir, "app", "globals.css");
134
+ if (fs2.existsSync(globalsCssSrc)) {
135
+ fs2.copyFileSync(globalsCssSrc, globalsCssDest);
136
+ }
137
+ }
138
+
139
+ // src/cli/utils/update-layout.ts
140
+ import * as fs3 from "fs";
141
+ import * as path3 from "path";
142
+ async function updateLayout(cwd) {
143
+ const possiblePaths = [
144
+ path3.join(cwd, "app", "layout.tsx"),
145
+ path3.join(cwd, "app", "layout.js"),
146
+ path3.join(cwd, "app", "layout.jsx")
147
+ ];
148
+ let layoutPath = null;
149
+ for (const p of possiblePaths) {
150
+ if (fs3.existsSync(p)) {
151
+ layoutPath = p;
152
+ break;
153
+ }
154
+ }
155
+ if (!layoutPath) {
156
+ throw new Error("Could not find app/layout.tsx or app/layout.js");
157
+ }
158
+ let content = fs3.readFileSync(layoutPath, "utf-8");
159
+ if (content.includes("DevToolsProvider")) {
160
+ return true;
161
+ }
162
+ const devtoolsImport = "import { DevToolsProvider } from '@/devtools';";
163
+ const importRegex = /^import\s+.*?(?:from\s+['"][^'"]+['"])?;?\s*$/gm;
164
+ let lastImportEnd = 0;
165
+ let match;
166
+ while ((match = importRegex.exec(content)) !== null) {
167
+ lastImportEnd = match.index + match[0].length;
168
+ }
169
+ if (lastImportEnd > 0) {
170
+ content = content.slice(0, lastImportEnd) + "\n" + devtoolsImport + content.slice(lastImportEnd);
171
+ } else {
172
+ content = devtoolsImport + "\n" + content;
173
+ }
174
+ const childrenPatterns = [
175
+ /(\{children\})/g,
176
+ /(\{\s*children\s*\})/g
177
+ ];
178
+ let wrapped = false;
179
+ for (const pattern of childrenPatterns) {
180
+ if (pattern.test(content)) {
181
+ content = content.replace(pattern, "<DevToolsProvider>$1</DevToolsProvider>");
182
+ wrapped = true;
183
+ break;
184
+ }
185
+ }
186
+ if (!wrapped) {
187
+ const bodyPattern = /(<body[^>]*>)([\s\S]*?)(\{children\})([\s\S]*?)(<\/body>)/;
188
+ if (bodyPattern.test(content)) {
189
+ content = content.replace(
190
+ bodyPattern,
191
+ "$1$2<DevToolsProvider>$3</DevToolsProvider>$4$5"
192
+ );
193
+ wrapped = true;
194
+ }
195
+ }
196
+ if (!wrapped) {
197
+ throw new Error(
198
+ "Could not find {children} to wrap with DevToolsProvider. Please add manually."
199
+ );
200
+ }
201
+ fs3.writeFileSync(layoutPath, content, "utf-8");
202
+ return true;
203
+ }
204
+
205
+ // src/cli/init.ts
206
+ var __filename = fileURLToPath(import.meta.url);
207
+ var __dirname = path4.dirname(__filename);
208
+ var colors = {
209
+ reset: "\x1B[0m",
210
+ bold: "\x1B[1m",
211
+ dim: "\x1B[2m",
212
+ green: "\x1B[32m",
213
+ yellow: "\x1B[33m",
214
+ blue: "\x1B[34m",
215
+ cyan: "\x1B[36m",
216
+ red: "\x1B[31m"
217
+ };
218
+ function log(message) {
219
+ console.log(message);
220
+ }
221
+ function success(message) {
222
+ console.log(`${colors.green}\u2713${colors.reset} ${message}`);
223
+ }
224
+ function warn(message) {
225
+ console.log(`${colors.yellow}!${colors.reset} ${message}`);
226
+ }
227
+ function error(message) {
228
+ console.log(`${colors.red}\u2717${colors.reset} ${message}`);
229
+ }
230
+ function box(lines) {
231
+ const maxLength = Math.max(...lines.map((l) => l.length));
232
+ const top = `\u256D${"\u2500".repeat(maxLength + 4)}\u256E`;
233
+ const bottom = `\u2570${"\u2500".repeat(maxLength + 4)}\u256F`;
234
+ const middle = lines.map((l) => `\u2502 ${l.padEnd(maxLength)} \u2502`).join("\n");
235
+ return `${top}
236
+ ${middle}
237
+ ${bottom}`;
238
+ }
239
+ async function prompt(question) {
240
+ const rl = readline.createInterface({
241
+ input: process.stdin,
242
+ output: process.stdout
243
+ });
244
+ return new Promise((resolve2) => {
245
+ rl.question(question, (answer) => {
246
+ rl.close();
247
+ resolve2(answer.trim());
248
+ });
249
+ });
250
+ }
251
+ async function confirm(question) {
252
+ const answer = await prompt(`${question} (Y/n) `);
253
+ return answer.toLowerCase() !== "n";
254
+ }
255
+ function checkPrerequisites(cwd) {
256
+ const errors = [];
257
+ const packageJsonPath = path4.join(cwd, "package.json");
258
+ if (!fs4.existsSync(packageJsonPath)) {
259
+ errors.push("No package.json found. Run this command in a Next.js project root.");
260
+ return { valid: false, errors };
261
+ }
262
+ const packageJson = JSON.parse(fs4.readFileSync(packageJsonPath, "utf-8"));
263
+ const hasNext = packageJson.dependencies?.next || packageJson.devDependencies?.next;
264
+ if (!hasNext) {
265
+ errors.push("Next.js not found in dependencies. This tool requires Next.js.");
266
+ }
267
+ const tailwindVersion = packageJson.dependencies?.tailwindcss || packageJson.devDependencies?.tailwindcss || packageJson.dependencies?.["@tailwindcss/postcss"] || packageJson.devDependencies?.["@tailwindcss/postcss"];
268
+ if (!tailwindVersion) {
269
+ errors.push("Tailwind CSS not found in dependencies.");
270
+ } else if (!tailwindVersion.includes("4") && !tailwindVersion.includes("^4")) {
271
+ warn(`Tailwind CSS version ${tailwindVersion} detected. RadTools is optimized for Tailwind v4.`);
272
+ }
273
+ const appDir = path4.join(cwd, "app");
274
+ if (!fs4.existsSync(appDir)) {
275
+ errors.push("No /app directory found. RadTools requires Next.js App Router.");
276
+ }
277
+ return { valid: errors.length === 0, errors };
278
+ }
279
+ function checkExistingInstallation(cwd) {
280
+ const conflicts = [];
281
+ if (fs4.existsSync(path4.join(cwd, "devtools"))) {
282
+ conflicts.push("devtools/ directory already exists");
283
+ }
284
+ if (fs4.existsSync(path4.join(cwd, "app", "api", "devtools"))) {
285
+ conflicts.push("app/api/devtools/ directory already exists");
286
+ }
287
+ return conflicts;
288
+ }
289
+ async function init() {
290
+ const cwd = process.cwd();
291
+ log("");
292
+ log(
293
+ box([
294
+ "",
295
+ `${colors.bold}RadTools${colors.reset} - Visual Dev Tools`,
296
+ "for Next.js + Tailwind v4",
297
+ ""
298
+ ])
299
+ );
300
+ log("");
301
+ log(`${colors.dim}Checking prerequisites...${colors.reset}`);
302
+ const { valid, errors: prereqErrors } = checkPrerequisites(cwd);
303
+ if (!valid) {
304
+ for (const err of prereqErrors) {
305
+ error(err);
306
+ }
307
+ process.exit(1);
308
+ }
309
+ success("Next.js project detected");
310
+ success("Tailwind CSS detected");
311
+ success("App Router detected");
312
+ log("");
313
+ const conflicts = checkExistingInstallation(cwd);
314
+ if (conflicts.length > 0) {
315
+ warn("Existing installation detected:");
316
+ for (const conflict of conflicts) {
317
+ log(` - ${conflict}`);
318
+ }
319
+ log("");
320
+ const proceed = await confirm("Do you want to overwrite existing files?");
321
+ if (!proceed) {
322
+ log("Installation cancelled.");
323
+ process.exit(0);
324
+ }
325
+ log("");
326
+ }
327
+ const packageManager = detectPackageManager(cwd);
328
+ success(`Using ${packageManager}`);
329
+ log("");
330
+ log(`${colors.bold}Installing RadTools...${colors.reset}`);
331
+ log("");
332
+ const templatesDir = path4.resolve(__dirname, "..", "..", "templates");
333
+ try {
334
+ await copyTemplates(templatesDir, cwd);
335
+ success("Created devtools/");
336
+ success("Created components/ui/");
337
+ success("Created app/api/devtools/");
338
+ success("Created public/assets/icons/");
339
+ success("Replaced app/globals.css with RadTools theme");
340
+ } catch (err) {
341
+ error(`Failed to copy templates: ${err.message}`);
342
+ process.exit(1);
343
+ }
344
+ try {
345
+ const updated = await updateLayout(cwd);
346
+ if (updated) {
347
+ success("Updated app/layout.tsx with DevToolsProvider");
348
+ } else {
349
+ warn("Could not auto-update layout.tsx. Please add DevToolsProvider manually.");
350
+ }
351
+ } catch (err) {
352
+ warn(`Could not update layout.tsx: ${err.message}`);
353
+ warn("Please add DevToolsProvider manually.");
354
+ }
355
+ log("");
356
+ log(`${colors.dim}Installing dependencies...${colors.reset}`);
357
+ try {
358
+ await installDependencies(packageManager, cwd);
359
+ success("Installed zustand, react-draggable");
360
+ } catch (err) {
361
+ warn(`Could not auto-install dependencies: ${err.message}`);
362
+ log("");
363
+ log("Please install manually:");
364
+ log(` ${packageManager} ${packageManager === "npm" ? "install" : "add"} zustand react-draggable`);
365
+ }
366
+ log("");
367
+ log(
368
+ box([
369
+ "",
370
+ `${colors.green}RadTools installed successfully!${colors.reset}`,
371
+ "",
372
+ `Press ${colors.cyan}Shift+Cmd+K${colors.reset} to open devtools`,
373
+ `(${colors.dim}Shift+Ctrl+K on Windows/Linux${colors.reset})`,
374
+ ""
375
+ ])
376
+ );
377
+ log("");
378
+ log(`${colors.bold}Next steps:${colors.reset}`);
379
+ log("");
380
+ log(` 1. Start your dev server: ${colors.cyan}${packageManager} run dev${colors.reset}`);
381
+ log(` 2. Press ${colors.cyan}Shift+Cmd+K${colors.reset} to open RadTools`);
382
+ log("");
383
+ }
384
+
385
+ // src/cli/index.ts
386
+ var VERSION = "0.1.0";
387
+ var HELP = `
388
+ RadTools - Visual Dev Tools for Next.js + Tailwind v4
389
+
390
+ Usage:
391
+ radtools <command> [options]
392
+
393
+ Commands:
394
+ init Install RadTools in your Next.js project
395
+
396
+ Options:
397
+ --help, -h Show this help message
398
+ --version, -v Show version number
399
+
400
+ Examples:
401
+ npx radtools init
402
+ `;
403
+ async function main() {
404
+ const args = process.argv.slice(2);
405
+ const command = args[0];
406
+ if (args.includes("--help") || args.includes("-h") || !command) {
407
+ console.log(HELP);
408
+ process.exit(0);
409
+ }
410
+ if (args.includes("--version") || args.includes("-v")) {
411
+ console.log(`radtools v${VERSION}`);
412
+ process.exit(0);
413
+ }
414
+ switch (command) {
415
+ case "init":
416
+ await init();
417
+ break;
418
+ default:
419
+ console.error(`Unknown command: ${command}`);
420
+ console.log(HELP);
421
+ process.exit(1);
422
+ }
423
+ }
424
+ main().catch((error2) => {
425
+ console.error("Error:", error2.message);
426
+ process.exit(1);
427
+ });
package/package.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ "name": "radtools",
3
+ "version": "0.1.0",
4
+ "description": "Visual dev tools for Next.js + Tailwind v4 projects",
5
+ "keywords": ["nextjs", "tailwind", "devtools", "design-system", "visual-editor"],
6
+ "author": "kemos4be",
7
+ "license": "MIT",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/Radiants-DAO/radtools.git"
11
+ },
12
+ "homepage": "https://github.com/Radiants-DAO/radtools#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/Radiants-DAO/radtools/issues"
15
+ },
16
+ "engines": {
17
+ "node": ">=18.0.0"
18
+ },
19
+ "type": "module",
20
+ "bin": {
21
+ "radtools": "./bin/radtools.js"
22
+ },
23
+ "files": [
24
+ "bin",
25
+ "dist",
26
+ "templates"
27
+ ],
28
+ "scripts": {
29
+ "dev": "next dev",
30
+ "build": "next build",
31
+ "build:cli": "tsup src/cli/index.ts --format esm --outDir dist/cli --clean --target node18",
32
+ "prepublishOnly": "npm run build:cli",
33
+ "start": "next start",
34
+ "lint": "eslint"
35
+ },
36
+ "dependencies": {
37
+ "next": "16.0.10",
38
+ "react": "19.2.1",
39
+ "react-dom": "19.2.1",
40
+ "react-draggable": "^4.5.0",
41
+ "sharp": "^0.34.5",
42
+ "zustand": "^5.0.9"
43
+ },
44
+ "devDependencies": {
45
+ "@tailwindcss/postcss": "^4",
46
+ "@types/node": "^20",
47
+ "@types/react": "^19",
48
+ "@types/react-dom": "^19",
49
+ "eslint": "^9",
50
+ "eslint-config-next": "16.0.10",
51
+ "tailwindcss": "^4",
52
+ "tsup": "^8.3.0",
53
+ "typescript": "^5"
54
+ }
55
+ }
@@ -0,0 +1,94 @@
1
+ import { NextRequest, NextResponse } from 'next/server';
2
+ import { readFile, writeFile } from 'fs/promises';
3
+ import { join, extname } from 'path';
4
+ import sharp from 'sharp';
5
+
6
+ const ASSETS_DIR = join(process.cwd(), 'public', 'assets');
7
+
8
+ export async function POST(req: NextRequest) {
9
+ if (process.env.NODE_ENV !== 'development') {
10
+ return NextResponse.json(
11
+ { error: 'Dev tools API not available in production' },
12
+ { status: 403 }
13
+ );
14
+ }
15
+
16
+ try {
17
+ const { files } = await req.json();
18
+
19
+ if (!Array.isArray(files) || files.length === 0) {
20
+ return NextResponse.json({ error: 'No files provided' }, { status: 400 });
21
+ }
22
+
23
+ const results: { path: string; originalSize: number; optimizedSize: number }[] = [];
24
+
25
+ for (const filePath of files) {
26
+ // Validate path to prevent directory traversal
27
+ const safePath = filePath.replace(/\.\./g, '').replace(/^\/+/, '');
28
+ const fullPath = join(process.cwd(), 'public', safePath);
29
+
30
+ // Ensure the path is within the assets directory
31
+ if (!fullPath.startsWith(ASSETS_DIR)) {
32
+ continue;
33
+ }
34
+
35
+ const ext = extname(fullPath).toLowerCase();
36
+
37
+ // Only optimize images
38
+ if (!['.jpg', '.jpeg', '.png', '.webp'].includes(ext)) {
39
+ continue;
40
+ }
41
+
42
+ try {
43
+ const originalBuffer = await readFile(fullPath);
44
+ const originalSize = originalBuffer.length;
45
+
46
+ let optimizedBuffer: Buffer;
47
+
48
+ if (ext === '.png') {
49
+ optimizedBuffer = await sharp(originalBuffer)
50
+ .png({ quality: 80, compressionLevel: 9 })
51
+ .toBuffer();
52
+ } else if (ext === '.webp') {
53
+ optimizedBuffer = await sharp(originalBuffer)
54
+ .webp({ quality: 80 })
55
+ .toBuffer();
56
+ } else {
57
+ optimizedBuffer = await sharp(originalBuffer)
58
+ .jpeg({ quality: 80, progressive: true })
59
+ .toBuffer();
60
+ }
61
+
62
+ // Only save if we actually reduced the size
63
+ if (optimizedBuffer.length < originalSize) {
64
+ await writeFile(fullPath, optimizedBuffer);
65
+ results.push({
66
+ path: safePath,
67
+ originalSize,
68
+ optimizedSize: optimizedBuffer.length,
69
+ });
70
+ } else {
71
+ results.push({
72
+ path: safePath,
73
+ originalSize,
74
+ optimizedSize: originalSize,
75
+ });
76
+ }
77
+ } catch (err) {
78
+ // Failed to optimize file - skip
79
+ }
80
+ }
81
+
82
+ return NextResponse.json({
83
+ success: true,
84
+ results,
85
+ totalSaved: results.reduce((sum, r) => sum + (r.originalSize - r.optimizedSize), 0),
86
+ });
87
+ } catch (error) {
88
+ return NextResponse.json(
89
+ { error: 'Failed to optimize files', details: String(error) },
90
+ { status: 500 }
91
+ );
92
+ }
93
+ }
94
+