@ogulcancelik/pi-sketch 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Can Celik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,57 @@
1
+ # pi-sketch
2
+
3
+ Quick sketch pad for [pi](https://github.com/badlogic/pi-mono) - draw in browser, send to Claude.
4
+
5
+ ![demo](https://raw.githubusercontent.com/ogulcancelik/pi-sketch/main/assets/demo.gif)
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pi install npm:@ogulcancelik/pi-sketch
11
+ ```
12
+
13
+ Or try without installing:
14
+
15
+ ```bash
16
+ pi -e npm:@ogulcancelik/pi-sketch
17
+ ```
18
+
19
+ ## Usage
20
+
21
+ ```
22
+ /sketch
23
+ ```
24
+
25
+ Opens a canvas in your browser. Draw your sketch, press Enter to send.
26
+
27
+ ## Features
28
+
29
+ - **Quick sketches** - draw arrows, boxes, diagrams
30
+ - **Clipboard paste** - `Ctrl+V` to paste screenshots, annotate on top
31
+ - **Colors** - black, red, green, blue, white (eraser)
32
+ - **Brush sizes** - 1, 2, 3 keys
33
+ - **Undo** - Z key
34
+ - **Cancel from pi** - Escape key in terminal
35
+
36
+ ## Workflow
37
+
38
+ 1. `/paint` - opens browser canvas (URL shown as fallback)
39
+ 2. Draw your sketch
40
+ 3. `Ctrl+V` to paste and annotate screenshots
41
+ 4. Press Enter in browser to save
42
+ 5. `Sketch: /path` appears in your editor - add context and send
43
+
44
+ ## Keyboard Shortcuts (in browser)
45
+
46
+ | Key | Action |
47
+ |-----|--------|
48
+ | `Enter` | Send sketch |
49
+ | `Escape` | Cancel |
50
+ | `C` | Clear canvas |
51
+ | `Z` | Undo |
52
+ | `1-3` | Brush size |
53
+ | `Ctrl+V` | Paste image |
54
+
55
+ ## License
56
+
57
+ MIT
Binary file
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Sketch extension - quick sketch pad that opens in browser
3
+ * /sketch → opens browser canvas → draw → Enter sends to Claude
4
+ */
5
+
6
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
7
+ import { createServer, type Server } from "node:http";
8
+ import { exec } from "node:child_process";
9
+ import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
10
+ import { join, dirname } from "node:path";
11
+ import { tmpdir } from "node:os";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ // Load HTML from file
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const SKETCH_HTML = readFileSync(join(__dirname, "sketch.html"), "utf-8");
17
+
18
+ function openBrowser(url: string): void {
19
+ const platform = process.platform;
20
+ let cmd: string;
21
+
22
+ if (platform === "darwin") {
23
+ cmd = `open "${url}"`;
24
+ } else if (platform === "win32") {
25
+ cmd = `start "" "${url}"`;
26
+ } else {
27
+ cmd = `xdg-open "${url}" 2>/dev/null || sensible-browser "${url}" 2>/dev/null || x-www-browser "${url}" 2>/dev/null`;
28
+ }
29
+
30
+ exec(cmd);
31
+ }
32
+
33
+ interface PaintServer {
34
+ url: string;
35
+ waitForResult: () => Promise<string | null>;
36
+ close: () => void;
37
+ }
38
+
39
+ function launchPaintServer(): PaintServer {
40
+ let resolved = false;
41
+ let resolvePromise: (value: string | null) => void;
42
+
43
+ const resultPromise = new Promise<string | null>((resolve) => {
44
+ resolvePromise = resolve;
45
+ });
46
+
47
+ const server: Server = createServer((req, res) => {
48
+ // CORS headers for local dev
49
+ res.setHeader("Access-Control-Allow-Origin", "*");
50
+ res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
51
+ res.setHeader("Access-Control-Allow-Headers", "Content-Type");
52
+
53
+ if (req.method === "OPTIONS") {
54
+ res.writeHead(204);
55
+ res.end();
56
+ return;
57
+ }
58
+
59
+ if (req.method === "GET" && (req.url === "/" || req.url === "/sketch")) {
60
+ res.writeHead(200, { "Content-Type": "text/html" });
61
+ res.end(SKETCH_HTML);
62
+ return;
63
+ }
64
+
65
+ if (req.method === "POST" && req.url === "/submit") {
66
+ let body = "";
67
+ req.on("data", (chunk) => (body += chunk));
68
+ req.on("end", () => {
69
+ res.writeHead(200, { "Content-Type": "text/plain" });
70
+ res.end("OK");
71
+
72
+ if (!resolved) {
73
+ resolved = true;
74
+ server.close();
75
+ resolvePromise(body); // base64 PNG data
76
+ }
77
+ });
78
+ return;
79
+ }
80
+
81
+ if (req.method === "POST" && req.url === "/cancel") {
82
+ res.writeHead(200, { "Content-Type": "text/plain" });
83
+ res.end("OK");
84
+
85
+ if (!resolved) {
86
+ resolved = true;
87
+ server.close();
88
+ resolvePromise(null);
89
+ }
90
+ return;
91
+ }
92
+
93
+ // 404 for anything else
94
+ res.writeHead(404);
95
+ res.end("Not found");
96
+ });
97
+
98
+ // Handle server errors
99
+ server.on("error", (err) => {
100
+ console.error("Paint server error:", err);
101
+ if (!resolved) {
102
+ resolved = true;
103
+ resolvePromise(null);
104
+ }
105
+ });
106
+
107
+ // Get URL synchronously after listen
108
+ let url = "";
109
+ server.listen(0, "127.0.0.1", () => {
110
+ const addr = server.address();
111
+ if (addr && typeof addr === "object") {
112
+ url = `http://127.0.0.1:${addr.port}/sketch`;
113
+ }
114
+ });
115
+
116
+ // Timeout after 10 minutes
117
+ const timeout = setTimeout(() => {
118
+ if (!resolved) {
119
+ resolved = true;
120
+ server.close();
121
+ resolvePromise(null);
122
+ }
123
+ }, 10 * 60 * 1000);
124
+
125
+ return {
126
+ get url() {
127
+ return url;
128
+ },
129
+ waitForResult: () => resultPromise,
130
+ close: () => {
131
+ clearTimeout(timeout);
132
+ if (!resolved) {
133
+ resolved = true;
134
+ server.close();
135
+ resolvePromise(null);
136
+ }
137
+ },
138
+ };
139
+ }
140
+
141
+ export default function (pi: ExtensionAPI) {
142
+ pi.registerCommand("sketch", {
143
+ description: "Open a sketch pad in browser to draw something for Claude",
144
+
145
+ handler: async (_args, ctx) => {
146
+ if (!ctx.hasUI) {
147
+ ctx.ui.notify("Sketch requires interactive mode", "error");
148
+ return;
149
+ }
150
+
151
+ const paintServer = launchPaintServer();
152
+
153
+ // Wait a tick for the server to get its port
154
+ await new Promise((resolve) => setTimeout(resolve, 50));
155
+
156
+ // Auto-open browser
157
+ openBrowser(paintServer.url);
158
+
159
+ // Use custom UI to show status and handle Escape to cancel
160
+ const imageBase64 = await ctx.ui.custom<string | null>((tui, theme, _kb, done) => {
161
+ // Race between browser result and user pressing Escape
162
+ paintServer.waitForResult().then(done);
163
+
164
+ // Simple component that shows status and handles Escape
165
+ return {
166
+ render(width: number): string[] {
167
+ const line1 = theme.fg("success", "Sketch opened in browser");
168
+ const line2 = theme.fg("muted", paintServer.url);
169
+ const line3 = theme.fg("dim", "Press Escape to cancel");
170
+ return [line1, line2, "", line3];
171
+ },
172
+ handleInput(data: string) {
173
+ // Check for Escape
174
+ if (data === "\x1b" || data === "\x1b\x1b") {
175
+ paintServer.close();
176
+ done(null);
177
+ }
178
+ },
179
+ };
180
+ });
181
+
182
+ try {
183
+ if (imageBase64) {
184
+ // Save to temp file (same flow as @ image attachments)
185
+ const sketchDir = join(tmpdir(), "pi-sketches");
186
+ mkdirSync(sketchDir, { recursive: true });
187
+
188
+ const timestamp = Date.now();
189
+ const sketchPath = join(sketchDir, `sketch-${timestamp}.png`);
190
+
191
+ // Decode base64 and write to file
192
+ const buffer = Buffer.from(imageBase64, "base64");
193
+ writeFileSync(sketchPath, buffer);
194
+
195
+ // Append to editor instead of auto-sending - user can add more text
196
+ const currentText = ctx.ui.getEditorText?.() || "";
197
+ const prefix = currentText ? currentText + "\n" : "";
198
+ ctx.ui.setEditorText(`${prefix}Sketch: ${sketchPath}`);
199
+ } else {
200
+ ctx.ui.notify("Sketch cancelled", "info");
201
+ }
202
+ } catch (error) {
203
+ paintServer.close();
204
+ ctx.ui.notify(`Sketch error: ${error instanceof Error ? error.message : String(error)}`, "error");
205
+ }
206
+ },
207
+ });
208
+ }
@@ -0,0 +1,411 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>pi sketch</title>
6
+ <style>
7
+ * { margin: 0; padding: 0; box-sizing: border-box; }
8
+ body {
9
+ font-family: system-ui, -apple-system, sans-serif;
10
+ background: #1a1a1a;
11
+ height: 100vh;
12
+ display: flex;
13
+ flex-direction: column;
14
+ align-items: center;
15
+ justify-content: center;
16
+ gap: 16px;
17
+ padding: 20px;
18
+ }
19
+ .toolbar {
20
+ display: flex;
21
+ gap: 12px;
22
+ align-items: center;
23
+ color: #888;
24
+ font-size: 14px;
25
+ }
26
+ .toolbar button {
27
+ background: #333;
28
+ color: #fff;
29
+ border: none;
30
+ padding: 8px 16px;
31
+ border-radius: 6px;
32
+ cursor: pointer;
33
+ font-size: 14px;
34
+ }
35
+ .toolbar button:hover { background: #444; }
36
+ .toolbar button.primary { background: #2563eb; }
37
+ .toolbar button.primary:hover { background: #1d4ed8; }
38
+ .brush-sizes {
39
+ display: flex;
40
+ gap: 6px;
41
+ }
42
+ .brush-size {
43
+ width: 28px;
44
+ height: 28px;
45
+ border-radius: 50%;
46
+ background: #333;
47
+ display: flex;
48
+ align-items: center;
49
+ justify-content: center;
50
+ cursor: pointer;
51
+ border: 2px solid transparent;
52
+ }
53
+ .brush-size:hover { background: #444; }
54
+ .brush-size.active { border-color: #2563eb; }
55
+ .brush-size .dot {
56
+ background: #fff;
57
+ border-radius: 50%;
58
+ }
59
+ .colors {
60
+ display: flex;
61
+ gap: 4px;
62
+ }
63
+ .color-btn {
64
+ width: 24px;
65
+ height: 24px;
66
+ border-radius: 50%;
67
+ cursor: pointer;
68
+ border: 2px solid transparent;
69
+ }
70
+ .color-btn:hover { opacity: 0.8; }
71
+ .color-btn.active { border-color: #fff; }
72
+ .canvas-container {
73
+ position: relative;
74
+ display: inline-block;
75
+ }
76
+ canvas {
77
+ background: #fff;
78
+ border-radius: 8px;
79
+ cursor: crosshair;
80
+ box-shadow: 0 4px 24px rgba(0,0,0,0.3);
81
+ }
82
+ .resize-handle {
83
+ position: absolute;
84
+ bottom: 4px;
85
+ right: 4px;
86
+ width: 16px;
87
+ height: 16px;
88
+ cursor: nwse-resize;
89
+ background: linear-gradient(135deg, transparent 50%, #666 50%);
90
+ border-radius: 0 0 6px 0;
91
+ opacity: 0.7;
92
+ }
93
+ .resize-handle:hover {
94
+ opacity: 1;
95
+ }
96
+ .hint {
97
+ color: #666;
98
+ font-size: 13px;
99
+ }
100
+ kbd {
101
+ background: #333;
102
+ padding: 2px 6px;
103
+ border-radius: 4px;
104
+ font-family: inherit;
105
+ }
106
+ </style>
107
+ </head>
108
+ <body>
109
+ <div class="toolbar">
110
+ <div class="colors">
111
+ <div class="color-btn active" data-color="#000000" style="background: #000;" title="Black"></div>
112
+ <div class="color-btn" data-color="#ef4444" style="background: #ef4444;" title="Red"></div>
113
+ <div class="color-btn" data-color="#22c55e" style="background: #22c55e;" title="Green"></div>
114
+ <div class="color-btn" data-color="#3b82f6" style="background: #3b82f6;" title="Blue"></div>
115
+ <div class="color-btn" data-color="#ffffff" style="background: #fff;" title="White (eraser)"></div>
116
+ </div>
117
+ <div class="brush-sizes">
118
+ <div class="brush-size" data-size="2" title="Small">
119
+ <div class="dot" style="width: 4px; height: 4px;"></div>
120
+ </div>
121
+ <div class="brush-size active" data-size="4" title="Medium">
122
+ <div class="dot" style="width: 8px; height: 8px;"></div>
123
+ </div>
124
+ <div class="brush-size" data-size="8" title="Large">
125
+ <div class="dot" style="width: 14px; height: 14px;"></div>
126
+ </div>
127
+ </div>
128
+ <button onclick="clearCanvas()">Clear</button>
129
+ <button onclick="undo()">Undo</button>
130
+ <button class="primary" onclick="send()">Send</button>
131
+ <button onclick="cancel()">Cancel</button>
132
+ </div>
133
+
134
+ <div class="canvas-container">
135
+ <canvas id="canvas" width="1000" height="600"></canvas>
136
+ <div class="resize-handle" id="resizeHandle"></div>
137
+ </div>
138
+
139
+ <div class="hint">
140
+ <kbd>Ctrl+V</kbd> paste image · <kbd>Enter</kbd> send · <kbd>Esc</kbd> cancel · <kbd>C</kbd> clear · <kbd>Z</kbd> undo · <kbd>1-3</kbd> brush size
141
+ </div>
142
+
143
+ <script>
144
+ const canvas = document.getElementById('canvas');
145
+ const ctx = canvas.getContext('2d');
146
+
147
+ let drawing = false;
148
+ let brushSize = 4;
149
+ let brushColor = '#000000';
150
+ let history = [];
151
+ let lastX, lastY;
152
+
153
+ // Save initial blank state
154
+ saveState();
155
+
156
+ function saveState() {
157
+ history.push(canvas.toDataURL());
158
+ if (history.length > 50) history.shift(); // Limit history
159
+ }
160
+
161
+ function undo() {
162
+ if (history.length > 1) {
163
+ history.pop(); // Remove current state
164
+ const img = new Image();
165
+ img.onload = () => {
166
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
167
+ ctx.drawImage(img, 0, 0);
168
+ };
169
+ img.src = history[history.length - 1];
170
+ }
171
+ }
172
+
173
+ const DEFAULT_WIDTH = 1000;
174
+ const DEFAULT_HEIGHT = 600;
175
+
176
+ function clearCanvas() {
177
+ canvas.width = DEFAULT_WIDTH;
178
+ canvas.height = DEFAULT_HEIGHT;
179
+ ctx.fillStyle = '#fff';
180
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
181
+ saveState();
182
+ }
183
+
184
+ function getPos(e) {
185
+ const rect = canvas.getBoundingClientRect();
186
+ const scaleX = canvas.width / rect.width;
187
+ const scaleY = canvas.height / rect.height;
188
+ return {
189
+ x: (e.clientX - rect.left) * scaleX,
190
+ y: (e.clientY - rect.top) * scaleY
191
+ };
192
+ }
193
+
194
+ canvas.addEventListener('mousedown', (e) => {
195
+ drawing = true;
196
+ const pos = getPos(e);
197
+ lastX = pos.x;
198
+ lastY = pos.y;
199
+
200
+ // Draw a dot for single clicks
201
+ ctx.beginPath();
202
+ ctx.arc(pos.x, pos.y, brushSize / 2, 0, Math.PI * 2);
203
+ ctx.fillStyle = brushColor;
204
+ ctx.fill();
205
+ });
206
+
207
+ canvas.addEventListener('mousemove', (e) => {
208
+ if (!drawing) return;
209
+ const pos = getPos(e);
210
+
211
+ ctx.beginPath();
212
+ ctx.moveTo(lastX, lastY);
213
+ ctx.lineTo(pos.x, pos.y);
214
+ ctx.strokeStyle = brushColor;
215
+ ctx.lineWidth = brushSize;
216
+ ctx.lineCap = 'round';
217
+ ctx.lineJoin = 'round';
218
+ ctx.stroke();
219
+
220
+ lastX = pos.x;
221
+ lastY = pos.y;
222
+ });
223
+
224
+ canvas.addEventListener('mouseup', () => {
225
+ if (drawing) {
226
+ drawing = false;
227
+ saveState();
228
+ }
229
+ });
230
+
231
+ canvas.addEventListener('mouseleave', () => {
232
+ if (drawing) {
233
+ drawing = false;
234
+ saveState();
235
+ }
236
+ });
237
+
238
+ // Brush size selection
239
+ document.querySelectorAll('.brush-size').forEach(el => {
240
+ el.addEventListener('click', () => {
241
+ document.querySelectorAll('.brush-size').forEach(b => b.classList.remove('active'));
242
+ el.classList.add('active');
243
+ brushSize = parseInt(el.dataset.size);
244
+ });
245
+ });
246
+
247
+ // Color selection
248
+ document.querySelectorAll('.color-btn').forEach(el => {
249
+ el.addEventListener('click', () => {
250
+ document.querySelectorAll('.color-btn').forEach(b => b.classList.remove('active'));
251
+ el.classList.add('active');
252
+ brushColor = el.dataset.color;
253
+ });
254
+ });
255
+
256
+ // Keyboard shortcuts
257
+ document.addEventListener('keydown', (e) => {
258
+ if (e.key === 'Enter') {
259
+ e.preventDefault();
260
+ send();
261
+ } else if (e.key === 'Escape') {
262
+ cancel();
263
+ } else if (e.key.toLowerCase() === 'c' && !e.ctrlKey && !e.metaKey) {
264
+ clearCanvas();
265
+ } else if (e.key.toLowerCase() === 'z' && !e.ctrlKey && !e.metaKey) {
266
+ undo();
267
+ } else if (e.key === '1') {
268
+ document.querySelector('[data-size="2"]').click();
269
+ } else if (e.key === '2') {
270
+ document.querySelector('[data-size="4"]').click();
271
+ } else if (e.key === '3') {
272
+ document.querySelector('[data-size="8"]').click();
273
+ }
274
+ });
275
+
276
+ // Clipboard paste - paste image and draw on top
277
+ document.addEventListener('paste', async (e) => {
278
+ const items = e.clipboardData?.items;
279
+ if (!items) return;
280
+
281
+ for (const item of items) {
282
+ if (item.type.startsWith('image/')) {
283
+ e.preventDefault();
284
+ const blob = item.getAsFile();
285
+ if (!blob) continue;
286
+
287
+ const img = new Image();
288
+ img.onload = () => {
289
+ // Max canvas bounds - keep it reasonable so toolbar stays visible
290
+ const maxWidth = 1000;
291
+ const maxHeight = 600;
292
+
293
+ let newWidth = img.width;
294
+ let newHeight = img.height;
295
+
296
+ // Scale down preserving aspect ratio
297
+ const widthRatio = maxWidth / newWidth;
298
+ const heightRatio = maxHeight / newHeight;
299
+ const scale = Math.min(widthRatio, heightRatio, 1); // Don't upscale
300
+
301
+ newWidth = Math.round(newWidth * scale);
302
+ newHeight = Math.round(newHeight * scale);
303
+
304
+ // Resize canvas
305
+ canvas.width = newWidth;
306
+ canvas.height = newHeight;
307
+
308
+ // Draw pasted image
309
+ ctx.fillStyle = '#fff';
310
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
311
+ ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
312
+
313
+ saveState();
314
+ URL.revokeObjectURL(img.src);
315
+ };
316
+ img.src = URL.createObjectURL(blob);
317
+ break;
318
+ }
319
+ }
320
+ });
321
+
322
+ // Track if we've already sent a result
323
+ let submitted = false;
324
+
325
+ async function send() {
326
+ if (submitted) return;
327
+ submitted = true;
328
+
329
+ // Get PNG data (strip the data:image/png;base64, prefix)
330
+ const dataUrl = canvas.toDataURL('image/png');
331
+ const base64 = dataUrl.split(',')[1];
332
+
333
+ await fetch('/submit', {
334
+ method: 'POST',
335
+ headers: { 'Content-Type': 'text/plain' },
336
+ body: base64
337
+ });
338
+
339
+ window.close();
340
+ }
341
+
342
+ async function cancel() {
343
+ if (submitted) return;
344
+ submitted = true;
345
+
346
+ await fetch('/cancel', { method: 'POST' });
347
+ window.close();
348
+ }
349
+
350
+ // Resize handle functionality
351
+ const resizeHandle = document.getElementById('resizeHandle');
352
+ let resizing = false;
353
+ let resizeStartX, resizeStartY, startWidth, startHeight;
354
+
355
+ resizeHandle.addEventListener('mousedown', (e) => {
356
+ e.preventDefault();
357
+ resizing = true;
358
+ resizeStartX = e.clientX;
359
+ resizeStartY = e.clientY;
360
+ startWidth = canvas.width;
361
+ startHeight = canvas.height;
362
+
363
+ // Save current canvas content
364
+ const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
365
+
366
+ const onMouseMove = (e) => {
367
+ if (!resizing) return;
368
+
369
+ const dx = e.clientX - resizeStartX;
370
+ const dy = e.clientY - resizeStartY;
371
+
372
+ const newWidth = Math.max(200, startWidth + dx);
373
+ const newHeight = Math.max(150, startHeight + dy);
374
+
375
+ // Resize canvas
376
+ canvas.width = newWidth;
377
+ canvas.height = newHeight;
378
+
379
+ // Fill with white first
380
+ ctx.fillStyle = '#fff';
381
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
382
+
383
+ // Restore previous content
384
+ ctx.putImageData(imageData, 0, 0);
385
+ };
386
+
387
+ const onMouseUp = () => {
388
+ if (resizing) {
389
+ resizing = false;
390
+ saveState();
391
+ }
392
+ document.removeEventListener('mousemove', onMouseMove);
393
+ document.removeEventListener('mouseup', onMouseUp);
394
+ };
395
+
396
+ document.addEventListener('mousemove', onMouseMove);
397
+ document.addEventListener('mouseup', onMouseUp);
398
+ });
399
+
400
+ // Initialize with white background
401
+ clearCanvas();
402
+ history = []; // Reset history after initial clear
403
+ saveState();
404
+
405
+ // Send cancel if tab is closed without explicit action
406
+ window.addEventListener('beforeunload', () => {
407
+ navigator.sendBeacon('/cancel');
408
+ });
409
+ </script>
410
+ </body>
411
+ </html>
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@ogulcancelik/pi-sketch",
3
+ "version": "0.1.0",
4
+ "description": "Quick sketch pad for pi - draw in browser, send to models",
5
+ "keywords": [
6
+ "pi-package",
7
+ "paint",
8
+ "sketch",
9
+ "drawing",
10
+ "visual"
11
+ ],
12
+ "author": "Can Celik",
13
+ "license": "MIT",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/ogulcancelik/pi-sketch.git"
17
+ },
18
+ "homepage": "https://github.com/ogulcancelik/pi-sketch",
19
+ "bugs": {
20
+ "url": "https://github.com/ogulcancelik/pi-sketch/issues"
21
+ },
22
+ "files": [
23
+ "extensions",
24
+ "assets",
25
+ "README.md",
26
+ "LICENSE"
27
+ ],
28
+ "peerDependencies": {
29
+ "@mariozechner/pi-coding-agent": "*"
30
+ },
31
+ "pi": {
32
+ "extensions": [
33
+ "./extensions"
34
+ ]
35
+ }
36
+ }