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
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)
|
package/bin/radtools.js
ADDED
|
@@ -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
|
+
|