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.
Files changed (4) hide show
  1. package/README.md +110 -0
  2. package/bin/cli.js +258 -0
  3. package/package.json +12 -0
  4. 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
+ }