draply-dev 1.0.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 +110 -0
- package/bin/cli.js +258 -0
- package/package.json +12 -0
- package/src/overlay.js +1569 -0
package/README.md
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Draply 🎯
|
|
2
|
+
|
|
3
|
+
> Visual overlay for any frontend project.
|
|
4
|
+
> Move, resize, recolor, and restyle your **live dev site** — save changes to CSS automatically.
|
|
5
|
+
|
|
6
|
+
No config. No setup. Works with any framework.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Install & Run
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# 1. Start your dev server as usual
|
|
14
|
+
npm run dev # Next.js, Vite, CRA, Nuxt...
|
|
15
|
+
|
|
16
|
+
# 2. In a new terminal, run Draply
|
|
17
|
+
npx draply 5173 # ← port of your dev server
|
|
18
|
+
|
|
19
|
+
# 3. Open the Draply URL (NOT your dev server URL)
|
|
20
|
+
# → http://localhost:4000
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
**Custom ports:**
|
|
24
|
+
```bash
|
|
25
|
+
npx draply 3000 # your app on 3000, draply on 4000
|
|
26
|
+
npx draply 8080 9000 # your app on 8080, draply on 9000
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## How it works
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
Your app (port 5173)
|
|
35
|
+
↓
|
|
36
|
+
Draply proxy (port 4000)
|
|
37
|
+
• Intercepts HTML responses
|
|
38
|
+
• Injects visual overlay into every page
|
|
39
|
+
• Forwards everything else unchanged
|
|
40
|
+
↓
|
|
41
|
+
Browser shows YOUR site + green button in corner
|
|
42
|
+
↓
|
|
43
|
+
Click button → sidebar opens → pick a tool → edit
|
|
44
|
+
↓
|
|
45
|
+
Save → writes draply.css + auto-imports it into your project
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Auto-detection
|
|
49
|
+
|
|
50
|
+
On first save, Draply **auto-detects your framework** and adds the CSS import:
|
|
51
|
+
|
|
52
|
+
| Framework | File modified |
|
|
53
|
+
|-----------|--------------|
|
|
54
|
+
| Next.js (App Router) | `app/layout.tsx` |
|
|
55
|
+
| Next.js (Pages) | `pages/_app.tsx` |
|
|
56
|
+
| Nuxt | `nuxt.config.ts` |
|
|
57
|
+
| Vue (Vite) | `src/main.ts` |
|
|
58
|
+
| React (Vite) | `src/main.tsx` |
|
|
59
|
+
| CRA | `public/index.html` |
|
|
60
|
+
| Plain HTML | `index.html` |
|
|
61
|
+
|
|
62
|
+
No manual setup needed — changes persist even without Draply running.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Tools
|
|
67
|
+
|
|
68
|
+
| Tool | What it does |
|
|
69
|
+
|------|-------------|
|
|
70
|
+
| 🖱️ Select | Click any element to highlight it |
|
|
71
|
+
| ✦ Move | Drag elements to any pixel position |
|
|
72
|
+
| 🔍 Inspect | Hover to see dimensions + CSS info |
|
|
73
|
+
| 📐 Resize | 8 corner/edge handles to resize any element |
|
|
74
|
+
| 🎨 Colors | Change background, text color, border color |
|
|
75
|
+
| 📝 Typography | Font size, weight, line-height, letter-spacing |
|
|
76
|
+
| 🖼️ Assets | Upload PNG/SVG/JPG → place on page → control z-index |
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Keyboard shortcuts
|
|
81
|
+
|
|
82
|
+
| Key | Action |
|
|
83
|
+
|-----|--------|
|
|
84
|
+
| `Esc` | Cancel placing asset / deselect |
|
|
85
|
+
| `Arrow keys` | Nudge selected element 1px |
|
|
86
|
+
| `Shift + Arrow` | Nudge 10px |
|
|
87
|
+
| `Delete` | Remove selected placed asset |
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Works with
|
|
92
|
+
|
|
93
|
+
- ✅ React / Next.js / Vite
|
|
94
|
+
- ✅ Vue / Nuxt
|
|
95
|
+
- ✅ Svelte / Astro
|
|
96
|
+
- ✅ Vanilla HTML/CSS
|
|
97
|
+
- ✅ Any framework that serves HTML on localhost
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Requirements
|
|
102
|
+
|
|
103
|
+
- Node.js 16+
|
|
104
|
+
- Your dev server must be running before starting Draply
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## License
|
|
109
|
+
|
|
110
|
+
MIT
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const zlib = require('zlib');
|
|
8
|
+
|
|
9
|
+
const args = process.argv.slice(2);
|
|
10
|
+
const targetPort = parseInt(args[0]) || 3000;
|
|
11
|
+
const proxyPort = parseInt(args[1]) || 4000;
|
|
12
|
+
const targetHost = args[2] || 'localhost';
|
|
13
|
+
|
|
14
|
+
const OVERLAY_JS = fs.readFileSync(path.join(__dirname, '../src/overlay.js'), 'utf8');
|
|
15
|
+
// Unique marker that does NOT appear inside overlay.js itself
|
|
16
|
+
const MARKER = 'data-ps-done="1"';
|
|
17
|
+
const INJECT_TAG = `\n<link rel="stylesheet" href="/draply.css">\n<script ${MARKER}>\n${OVERLAY_JS}\n</script>`;
|
|
18
|
+
|
|
19
|
+
function injectOverlay(html) {
|
|
20
|
+
if (html.includes(MARKER)) return html;
|
|
21
|
+
if (html.includes('</body>')) return html.replace(/<\/body>/i, INJECT_TAG + '\n</body>');
|
|
22
|
+
if (html.includes('</html>')) return html.replace(/<\/html>/i, INJECT_TAG + '\n</html>');
|
|
23
|
+
return html + INJECT_TAG;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function decode(headers, chunks) {
|
|
27
|
+
const enc = headers['content-encoding'] || '';
|
|
28
|
+
const buf = Buffer.concat(chunks);
|
|
29
|
+
return new Promise((res, rej) => {
|
|
30
|
+
if (enc === 'gzip') return zlib.gunzip(buf, (e,d) => e ? rej(e) : res(d.toString('utf8')));
|
|
31
|
+
if (enc === 'deflate') return zlib.inflate(buf, (e,d) => e ? rej(e) : res(d.toString('utf8')));
|
|
32
|
+
if (enc === 'br') return zlib.brotliDecompress(buf, (e,d) => e ? rej(e) : res(d.toString('utf8')));
|
|
33
|
+
res(buf.toString('utf8'));
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Создаём пустой draply.css при старте если его нет
|
|
38
|
+
const projectRoot = process.cwd();
|
|
39
|
+
const overridesPath = path.join(projectRoot, 'draply.css');
|
|
40
|
+
if (!fs.existsSync(overridesPath)) {
|
|
41
|
+
fs.writeFileSync(overridesPath, '/* draply */\n', 'utf8');
|
|
42
|
+
}
|
|
43
|
+
let cssInjected = false; // флаг: CSS уже подключён к проекту?
|
|
44
|
+
|
|
45
|
+
// ── Авто-определение фреймворка и подключение draply.css ──────────────────
|
|
46
|
+
function autoInjectCSS() {
|
|
47
|
+
if (cssInjected) return;
|
|
48
|
+
cssInjected = true;
|
|
49
|
+
|
|
50
|
+
const has = f => fs.existsSync(path.join(projectRoot, f));
|
|
51
|
+
const read = f => fs.readFileSync(path.join(projectRoot, f), 'utf8');
|
|
52
|
+
const write = (f, c) => fs.writeFileSync(path.join(projectRoot, f), c, 'utf8');
|
|
53
|
+
const already = (content) => content.includes('draply.css');
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
// Next.js App Router
|
|
57
|
+
for (const f of ['app/layout.tsx', 'app/layout.jsx', 'app/layout.js']) {
|
|
58
|
+
if (has(f)) {
|
|
59
|
+
let src = read(f);
|
|
60
|
+
if (already(src)) return;
|
|
61
|
+
src = `import '../draply.css';\n` + src;
|
|
62
|
+
write(f, src);
|
|
63
|
+
console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → ${f}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Next.js Pages Router
|
|
69
|
+
for (const f of ['pages/_app.tsx', 'pages/_app.jsx', 'pages/_app.js']) {
|
|
70
|
+
if (has(f)) {
|
|
71
|
+
let src = read(f);
|
|
72
|
+
if (already(src)) return;
|
|
73
|
+
src = `import '../draply.css';\n` + src;
|
|
74
|
+
write(f, src);
|
|
75
|
+
console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → ${f}`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Nuxt
|
|
81
|
+
for (const f of ['nuxt.config.ts', 'nuxt.config.js']) {
|
|
82
|
+
if (has(f)) {
|
|
83
|
+
let src = read(f);
|
|
84
|
+
if (already(src)) return;
|
|
85
|
+
if (src.includes('css:')) {
|
|
86
|
+
src = src.replace(/css:\s*\[/, "css: ['./draply.css', ");
|
|
87
|
+
} else {
|
|
88
|
+
src = src.replace(/export default defineNuxtConfig\(\{/, "export default defineNuxtConfig({\n css: ['./draply.css'],");
|
|
89
|
+
}
|
|
90
|
+
write(f, src);
|
|
91
|
+
console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → ${f}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Vue (Vite)
|
|
97
|
+
if (has('src/App.vue')) {
|
|
98
|
+
for (const f of ['src/main.ts', 'src/main.js']) {
|
|
99
|
+
if (has(f)) {
|
|
100
|
+
let src = read(f);
|
|
101
|
+
if (already(src)) return;
|
|
102
|
+
src = `import '../draply.css'\n` + src;
|
|
103
|
+
write(f, src);
|
|
104
|
+
console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → ${f}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// React (Vite) — src/main.tsx / src/main.jsx
|
|
111
|
+
for (const f of ['src/main.tsx', 'src/main.jsx', 'src/main.js', 'src/main.ts', 'src/index.tsx', 'src/index.jsx', 'src/index.js']) {
|
|
112
|
+
if (has(f)) {
|
|
113
|
+
let src = read(f);
|
|
114
|
+
if (already(src)) return;
|
|
115
|
+
src = `import '../draply.css'\n` + src;
|
|
116
|
+
write(f, src);
|
|
117
|
+
console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → ${f}`);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// CRA — public/index.html
|
|
123
|
+
if (has('public/index.html')) {
|
|
124
|
+
let src = read('public/index.html');
|
|
125
|
+
if (already(src)) return;
|
|
126
|
+
src = src.replace(/<\/head>/i, ' <link rel="stylesheet" href="%PUBLIC_URL%/draply.css">\n </head>');
|
|
127
|
+
write('public/index.html', src);
|
|
128
|
+
// Копируем draply.css в public/
|
|
129
|
+
fs.copyFileSync(overridesPath, path.join(projectRoot, 'public/draply.css'));
|
|
130
|
+
console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → public/index.html`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Plain HTML / Vite — index.html в корне
|
|
135
|
+
if (has('index.html')) {
|
|
136
|
+
let src = read('index.html');
|
|
137
|
+
if (already(src)) return;
|
|
138
|
+
src = src.replace(/<\/head>/i, ' <link rel="stylesheet" href="./draply.css">\n </head>');
|
|
139
|
+
write('index.html', src);
|
|
140
|
+
console.log(` \x1b[32m✓\x1b[0m Подключил draply.css → index.html`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
console.log(` \x1b[33m⚠\x1b[0m Не удалось определить фреймворк — добавь draply.css вручную`);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
console.log(` \x1b[33m⚠\x1b[0m Ошибка авто-подключения: ${err.message}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const server = http.createServer((req, res) => {
|
|
151
|
+
|
|
152
|
+
// CORS preflight
|
|
153
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
154
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
|
|
155
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
|
|
156
|
+
if (req.method === 'OPTIONS') { res.writeHead(204); res.end(); return; }
|
|
157
|
+
|
|
158
|
+
// ── Draply: Save endpoint ──────────────────────────────────────────────────
|
|
159
|
+
if (req.url === '/draply-save' && req.method === 'POST') {
|
|
160
|
+
let body = '';
|
|
161
|
+
req.on('data', c => body += c);
|
|
162
|
+
req.on('end', () => {
|
|
163
|
+
try {
|
|
164
|
+
const { changes } = JSON.parse(body);
|
|
165
|
+
const lines = [];
|
|
166
|
+
for (const ch of (changes || [])) {
|
|
167
|
+
if (!ch.selector) continue;
|
|
168
|
+
const props = Object.entries(ch.props)
|
|
169
|
+
.map(([k, v]) => ` ${k}: ${v};`)
|
|
170
|
+
.join('\n');
|
|
171
|
+
const label = ch.selector.split('>').pop().trim();
|
|
172
|
+
lines.push(`/* ${label} */
|
|
173
|
+
${ch.selector} {
|
|
174
|
+
${props}
|
|
175
|
+
}`);
|
|
176
|
+
}
|
|
177
|
+
const css = '/* draply — ' + new Date().toLocaleString('ru-RU') + ' */\n\n' + lines.join('\n\n') + '\n';
|
|
178
|
+
fs.writeFileSync(overridesPath, css, 'utf8');
|
|
179
|
+
autoInjectCSS();
|
|
180
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
181
|
+
res.end(JSON.stringify({ ok: true }));
|
|
182
|
+
} catch (e) {
|
|
183
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
184
|
+
res.end(JSON.stringify({ ok: false, error: e.message }));
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// ── Draply: Serve CSS ──────────────────────────────────────────────────────
|
|
191
|
+
if (req.url === '/draply.css') {
|
|
192
|
+
try {
|
|
193
|
+
const css = fs.readFileSync(overridesPath, 'utf8');
|
|
194
|
+
res.writeHead(200, { 'Content-Type': 'text/css', 'cache-control': 'no-store' });
|
|
195
|
+
res.end(css);
|
|
196
|
+
} catch (e) {
|
|
197
|
+
res.writeHead(200, { 'Content-Type': 'text/css' });
|
|
198
|
+
res.end('');
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
// ── Proxy to dev server ────────────────────────────────────────────────────
|
|
206
|
+
const opts = {
|
|
207
|
+
hostname: targetHost,
|
|
208
|
+
port: targetPort,
|
|
209
|
+
path: req.url,
|
|
210
|
+
method: req.method,
|
|
211
|
+
headers: { ...req.headers, host: `${targetHost}:${targetPort}`, 'accept-encoding': 'identity' },
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const pReq = http.request(opts, pRes => {
|
|
215
|
+
const ct = pRes.headers['content-type'] || '';
|
|
216
|
+
if (ct.includes('text/html')) {
|
|
217
|
+
const chunks = [];
|
|
218
|
+
pRes.on('data', c => chunks.push(c));
|
|
219
|
+
pRes.on('end', async () => {
|
|
220
|
+
try {
|
|
221
|
+
const html = await decode(pRes.headers, chunks);
|
|
222
|
+
const out = Buffer.from(injectOverlay(html), 'utf8');
|
|
223
|
+
const headers = { ...pRes.headers };
|
|
224
|
+
delete headers['content-encoding'];
|
|
225
|
+
headers['content-length'] = out.length;
|
|
226
|
+
headers['cache-control'] = 'no-store';
|
|
227
|
+
res.writeHead(pRes.statusCode, headers);
|
|
228
|
+
res.end(out);
|
|
229
|
+
} catch(e) {
|
|
230
|
+
res.writeHead(500); res.end('Draply error: ' + e.message);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
} else {
|
|
234
|
+
res.writeHead(pRes.statusCode, pRes.headers);
|
|
235
|
+
pRes.pipe(res);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
pReq.on('error', () => {
|
|
240
|
+
res.writeHead(502, {'Content-Type':'text/html'});
|
|
241
|
+
res.end(`<!DOCTYPE html><html><body style="background:#0a0a0f;color:#e8e8f0;font-family:monospace;padding:60px;text-align:center">
|
|
242
|
+
<h2 style="color:#ff6b6b">⚠ Не могу достучаться до ${targetHost}:${targetPort}</h2>
|
|
243
|
+
<p style="color:#555;margin-top:16px">Убедись что dev сервер запущен, потом обнови страницу</p>
|
|
244
|
+
<script>setTimeout(()=>location.reload(), 2000)</script>
|
|
245
|
+
</body></html>`);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
req.pipe(pReq);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
server.listen(proxyPort, () => {
|
|
252
|
+
console.log('\n \x1b[32m●\x1b[0m Draply запущен\n');
|
|
253
|
+
console.log(` Твой проект → \x1b[36mhttp://${targetHost}:${targetPort}\x1b[0m`);
|
|
254
|
+
console.log(` Открой это → \x1b[33mhttp://localhost:${proxyPort}\x1b[0m \x1b[32m← вот сюда заходи!\x1b[0m\n`);
|
|
255
|
+
console.log(` \x1b[90mCtrl+C чтобы остановить\x1b[0m\n`);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
process.on('SIGINT', () => { console.log('\n \x1b[90mDraply остановлен\x1b[0m\n'); process.exit(0); });
|
package/package.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "draply-dev",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Visual overlay for any frontend project — move, resize, restyle live in the browser, save to CSS",
|
|
5
|
+
"author": "Arman",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"bin": { "draply": "./bin/cli.js" },
|
|
8
|
+
"files": ["bin/", "src/"],
|
|
9
|
+
"keywords": ["css", "visual-editor", "frontend", "overlay", "design-tool", "no-code", "drag-and-drop"],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"engines": { "node": ">=16.0.0" }
|
|
12
|
+
}
|