@antigenic-oss/paint 0.2.8 → 0.2.9
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 +42 -15
- package/package.json +10 -8
- package/src/app/api/proxy/[[...path]]/route.ts +12 -1
- package/src/app/docs/page.tsx +22 -2
- package/src/app/layout.tsx +48 -2
- package/src/components/right-panel/changes/ChangesPanel.tsx +7 -1
- package/src/hooks/useChangeTracker.ts +34 -26
- package/src/hooks/usePostMessage.ts +20 -0
- package/src/types/messages.ts +6 -0
package/README.md
CHANGED
|
@@ -1,28 +1,35 @@
|
|
|
1
1
|
# pAInt
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
<p>
|
|
4
|
+
<a href="https://www.npmjs.com/package/@antigenic-oss/paint"><img alt="npm version" src="https://img.shields.io/npm/v/@antigenic-oss/paint" /></a>
|
|
5
|
+
<a href="https://www.npmjs.com/package/@antigenic-oss/paint"><img alt="npm downloads" src="https://img.shields.io/npm/dm/@antigenic-oss/paint" /></a>
|
|
6
|
+
<a href="https://github.com/Antigenic-OSS/pAInt/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/Antigenic-OSS/pAInt" /></a>
|
|
7
|
+
<a href="https://github.com/Antigenic-OSS/pAInt/issues"><img alt="Issues" src="https://img.shields.io/github/issues/Antigenic-OSS/pAInt" /></a>
|
|
8
|
+
<br />
|
|
9
|
+
<a href="https://github.com/Antigenic-OSS/pAInt/stargazers"><img alt="Stars" src="https://img.shields.io/github/stars/Antigenic-OSS/pAInt?style=social" /></a>
|
|
10
|
+
<a href="https://bun.sh"><img alt="Bun" src="https://img.shields.io/badge/local%20dev-Bun-000000" /></a>
|
|
11
|
+
<a href="https://nodejs.org"><img alt="Node.js" src="https://img.shields.io/badge/runtime-Node.js-5FA04E" /></a>
|
|
12
|
+
<a href="https://nextjs.org"><img alt="Next.js" src="https://img.shields.io/badge/framework-Next.js-000000" /></a>
|
|
13
|
+
<a href="https://www.typescriptlang.org"><img alt="TypeScript" src="https://img.shields.io/badge/language-TypeScript-3178C6" /></a>
|
|
14
|
+
</p>
|
|
12
15
|
|
|
13
16
|
pAInt is a visual editor for localhost web projects. It helps you inspect elements, edit styles, manage CSS variables, and export changelogs for [Claude Code](https://claude.ai/claude-code).
|
|
14
17
|
|
|
18
|
+
Built by [Antigenic](https://antigenic.org).
|
|
19
|
+
|
|
15
20
|
## Table of Contents
|
|
16
21
|
|
|
17
22
|
- [Project Status](#project-status)
|
|
18
23
|
- [Global CLI](#global-cli)
|
|
19
24
|
- [What You Can Do](#what-you-can-do)
|
|
25
|
+
- [Why pAInt Saves AI Tokens](#why-paint-saves-ai-tokens)
|
|
20
26
|
- [Prerequisites](#prerequisites)
|
|
21
27
|
- [Quick Start](#quick-start)
|
|
22
28
|
- [Connection Modes](#connection-modes)
|
|
23
29
|
- [Core Workflow](#core-workflow)
|
|
24
30
|
- [Interface Layout](#interface-layout)
|
|
25
31
|
- [Commands](#commands)
|
|
32
|
+
- [Bridge vs Server vs Terminal](#bridge-vs-server-vs-terminal)
|
|
26
33
|
- [Architecture Summary](#architecture-summary)
|
|
27
34
|
- [Security Notes](#security-notes)
|
|
28
35
|
- [Documentation](#documentation)
|
|
@@ -101,6 +108,15 @@ Terminal WS (when started): `ws://localhost:4001/ws`
|
|
|
101
108
|
- Track every change and export a structured changelog
|
|
102
109
|
- Send changelogs to Claude Code for source-file application
|
|
103
110
|
|
|
111
|
+
## Why pAInt Saves AI Tokens
|
|
112
|
+
|
|
113
|
+
pAInt helps you use fewer AI tokens by turning broad prompts into precise, structured change data:
|
|
114
|
+
|
|
115
|
+
- You edit visually first, so you do not need long back-and-forth prompt iterations
|
|
116
|
+
- Exported changelogs include exact selectors, properties, and before/after values
|
|
117
|
+
- Claude Code receives focused instructions, reducing token-heavy ambiguity
|
|
118
|
+
- You review only real diffs instead of asking the model to rediscover page context each time
|
|
119
|
+
|
|
104
120
|
## Prerequisites
|
|
105
121
|
|
|
106
122
|
- Local repository development: Bun `>=1.3`
|
|
@@ -172,6 +188,20 @@ bun run start # Production server (port 4000)
|
|
|
172
188
|
bun run lint # Biome check
|
|
173
189
|
```
|
|
174
190
|
|
|
191
|
+
## Bridge vs Server vs Terminal
|
|
192
|
+
|
|
193
|
+
These three pieces have different jobs:
|
|
194
|
+
|
|
195
|
+
- `pAInt server` (`paint start`): runs the main pAInt web app UI (default `http://127.0.0.1:4000`)
|
|
196
|
+
- `bridge server` (`paint bridge start`): local HTTP bridge used when pAInt UI is hosted remotely (for example Vercel) but still needs safe local machine access (`http://127.0.0.1:4002` by default)
|
|
197
|
+
- `terminal server` (`paint terminal start`): local WebSocket PTY service for the in-app Terminal tab (`ws://localhost:4001/ws` by default)
|
|
198
|
+
|
|
199
|
+
When to run each:
|
|
200
|
+
|
|
201
|
+
- Local-only workflow: run `paint start` (and optionally `paint terminal start` if you want in-app terminal streaming)
|
|
202
|
+
- Hosted UI + local project workflow: run `paint bridge start` so the hosted UI can reach local-only capabilities
|
|
203
|
+
- Claude/CLI visibility workflow: run `paint terminal start` to see command output and progress inside pAInt
|
|
204
|
+
|
|
175
205
|
## Architecture Summary
|
|
176
206
|
|
|
177
207
|
- Next.js App Router frontend (develop locally with Bun, run globally via Node.js CLI)
|
|
@@ -201,15 +231,12 @@ See `CONTRIBUTING.md` for setup, workflow, and pull request expectations.
|
|
|
201
231
|
|
|
202
232
|
## Release Automation
|
|
203
233
|
|
|
204
|
-
- Versioning
|
|
234
|
+
- Versioning is manual (update `package.json` before merging to `main`).
|
|
205
235
|
- CI workflow: `.github/workflows/ci.yml`
|
|
206
236
|
- Release workflow: `.github/workflows/release.yml`
|
|
207
237
|
- Publishing uses npm Trusted Publishing (OIDC) from the `release` GitHub Environment.
|
|
208
|
-
-
|
|
209
|
-
|
|
210
|
-
```bash
|
|
211
|
-
bun run changeset
|
|
212
|
-
```
|
|
238
|
+
- On every push to `main`, the release workflow runs build/lint/smoke checks.
|
|
239
|
+
- Publish happens automatically only when `package.json` version differs from the currently published npm version.
|
|
213
240
|
|
|
214
241
|
## License
|
|
215
242
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antigenic-oss/paint",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "Visual editor for localhost web projects with a global paint CLI",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://github.com/Antigenic-OSS/pAInt",
|
|
@@ -13,10 +13,16 @@
|
|
|
13
13
|
},
|
|
14
14
|
"keywords": [
|
|
15
15
|
"paint",
|
|
16
|
+
"pAInt",
|
|
16
17
|
"visual-editor",
|
|
17
18
|
"nextjs",
|
|
18
19
|
"css",
|
|
19
|
-
"localhost"
|
|
20
|
+
"localhost",
|
|
21
|
+
"bridge-server",
|
|
22
|
+
"terminal",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"ai-coding",
|
|
25
|
+
"developer-tools"
|
|
20
26
|
],
|
|
21
27
|
"bin": {
|
|
22
28
|
"paint": "bin/paint.js"
|
|
@@ -53,10 +59,7 @@
|
|
|
53
59
|
"lint:fix": "biome lint --write .",
|
|
54
60
|
"format": "biome format --write .",
|
|
55
61
|
"smoke:packed-global": "node ./bin/smoke-packed-global.js",
|
|
56
|
-
"prepublishOnly": "npm run smoke:packed-global"
|
|
57
|
-
"changeset": "changeset",
|
|
58
|
-
"version-packages": "changeset version",
|
|
59
|
-
"release": "changeset publish"
|
|
62
|
+
"prepublishOnly": "npm run smoke:packed-global"
|
|
60
63
|
},
|
|
61
64
|
"dependencies": {
|
|
62
65
|
"@tailwindcss/postcss": "4.2.1",
|
|
@@ -77,7 +80,6 @@
|
|
|
77
80
|
"zustand": "^5.0.11"
|
|
78
81
|
},
|
|
79
82
|
"devDependencies": {
|
|
80
|
-
"@biomejs/biome": "2.4.5"
|
|
81
|
-
"@changesets/cli": "^2.29.7"
|
|
83
|
+
"@biomejs/biome": "2.4.5"
|
|
82
84
|
}
|
|
83
85
|
}
|
|
@@ -1330,6 +1330,11 @@ function getInspectorCode(): string {
|
|
|
1330
1330
|
}
|
|
1331
1331
|
var meNewSelector = generateSelectorPath(meEl);
|
|
1332
1332
|
var meActualIndex = Array.from(meNewParent.children).indexOf(meEl);
|
|
1333
|
+
var meAttrs = {};
|
|
1334
|
+
for (var mai = 0; mai < meEl.attributes.length; mai++) {
|
|
1335
|
+
var ma = meEl.attributes[mai];
|
|
1336
|
+
meAttrs[ma.name] = ma.value;
|
|
1337
|
+
}
|
|
1333
1338
|
send({
|
|
1334
1339
|
type: 'ELEMENT_MOVED',
|
|
1335
1340
|
payload: {
|
|
@@ -1338,7 +1343,13 @@ function getInspectorCode(): string {
|
|
|
1338
1343
|
oldParentSelectorPath: meOldParentSelector,
|
|
1339
1344
|
newParentSelectorPath: msg.payload.newParentSelectorPath,
|
|
1340
1345
|
oldIndex: meOldIndex,
|
|
1341
|
-
newIndex: meActualIndex
|
|
1346
|
+
newIndex: meActualIndex,
|
|
1347
|
+
tagName: meEl.tagName.toLowerCase(),
|
|
1348
|
+
className: meEl.className && typeof meEl.className === 'string' ? meEl.className : null,
|
|
1349
|
+
elementId: meEl.id || null,
|
|
1350
|
+
innerText: (meEl.innerText || '').substring(0, 500) || null,
|
|
1351
|
+
attributes: meAttrs,
|
|
1352
|
+
computedStyles: getComputedStylesForElement(meEl)
|
|
1342
1353
|
}
|
|
1343
1354
|
});
|
|
1344
1355
|
if (selectedElement === meEl) {
|
package/src/app/docs/page.tsx
CHANGED
|
@@ -10,9 +10,29 @@ import {
|
|
|
10
10
|
} from './DocsClient'
|
|
11
11
|
|
|
12
12
|
export const metadata: Metadata = {
|
|
13
|
-
title: '
|
|
13
|
+
title: 'Setup Guide',
|
|
14
14
|
description:
|
|
15
|
-
'Framework-specific setup instructions for connecting pAInt to
|
|
15
|
+
'Framework-specific setup instructions for connecting pAInt to localhost projects, including proxy, bridge, and terminal-assisted workflows.',
|
|
16
|
+
keywords: [
|
|
17
|
+
'pAInt setup',
|
|
18
|
+
'localhost visual editor setup',
|
|
19
|
+
'bridge server setup',
|
|
20
|
+
'terminal server setup',
|
|
21
|
+
'Next.js inspector script',
|
|
22
|
+
'Claude Code changelog workflow',
|
|
23
|
+
],
|
|
24
|
+
openGraph: {
|
|
25
|
+
title: 'pAInt Setup Guide',
|
|
26
|
+
description:
|
|
27
|
+
'Connect pAInt to your localhost app with framework-specific instructions and efficient AI handoff workflows.',
|
|
28
|
+
type: 'article',
|
|
29
|
+
},
|
|
30
|
+
twitter: {
|
|
31
|
+
card: 'summary',
|
|
32
|
+
title: 'pAInt Setup Guide',
|
|
33
|
+
description:
|
|
34
|
+
'Configure pAInt quickly for localhost projects and ship cleaner AI-assisted edits.',
|
|
35
|
+
},
|
|
16
36
|
}
|
|
17
37
|
|
|
18
38
|
const SCRIPT_TAG =
|
package/src/app/layout.tsx
CHANGED
|
@@ -2,8 +2,54 @@ import type { Metadata } from 'next'
|
|
|
2
2
|
import './globals.css'
|
|
3
3
|
|
|
4
4
|
export const metadata: Metadata = {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
metadataBase: new URL('https://dev-editor-flow.vercel.app'),
|
|
6
|
+
title: {
|
|
7
|
+
default: 'pAInt | Visual Editor for Localhost Projects',
|
|
8
|
+
template: '%s | pAInt',
|
|
9
|
+
},
|
|
10
|
+
description:
|
|
11
|
+
'pAInt is a visual editor for localhost apps with bridge, server, and terminal workflows. Edit first, export precise changelogs, and save AI tokens with focused Claude Code handoff.',
|
|
12
|
+
keywords: [
|
|
13
|
+
'pAInt',
|
|
14
|
+
'visual editor',
|
|
15
|
+
'localhost web editor',
|
|
16
|
+
'bridge server',
|
|
17
|
+
'terminal server',
|
|
18
|
+
'Claude Code',
|
|
19
|
+
'AI token efficiency',
|
|
20
|
+
'CSS visual editing',
|
|
21
|
+
'Next.js visual editor',
|
|
22
|
+
'developer tooling',
|
|
23
|
+
],
|
|
24
|
+
alternates: {
|
|
25
|
+
canonical: '/',
|
|
26
|
+
},
|
|
27
|
+
openGraph: {
|
|
28
|
+
type: 'website',
|
|
29
|
+
url: '/',
|
|
30
|
+
siteName: 'pAInt',
|
|
31
|
+
title: 'pAInt | Visual Editor for Localhost Projects',
|
|
32
|
+
description:
|
|
33
|
+
'Inspect, edit, and export structured UI changes. Use bridge/server/terminal modes to ship faster and reduce AI token usage.',
|
|
34
|
+
},
|
|
35
|
+
twitter: {
|
|
36
|
+
card: 'summary_large_image',
|
|
37
|
+
title: 'pAInt | Visual Editor for Localhost Projects',
|
|
38
|
+
description:
|
|
39
|
+
'Visual-first editing plus focused changelogs for Claude Code means fewer tokens and faster delivery.',
|
|
40
|
+
},
|
|
41
|
+
robots: {
|
|
42
|
+
index: true,
|
|
43
|
+
follow: true,
|
|
44
|
+
googleBot: {
|
|
45
|
+
index: true,
|
|
46
|
+
follow: true,
|
|
47
|
+
'max-image-preview': 'large',
|
|
48
|
+
'max-snippet': -1,
|
|
49
|
+
'max-video-preview': -1,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
category: 'developer tools',
|
|
7
53
|
}
|
|
8
54
|
|
|
9
55
|
export default function RootLayout({
|
|
@@ -1198,7 +1198,12 @@ export function ChangesPanel() {
|
|
|
1198
1198
|
])
|
|
1199
1199
|
|
|
1200
1200
|
const handleAiScan = useCallback(() => {
|
|
1201
|
-
if (!targetUrl ||
|
|
1201
|
+
if (!targetUrl || breakpointChanges.length === 0) return
|
|
1202
|
+
if (!projectRoot) {
|
|
1203
|
+
showToast('error', 'Set a project folder first — click the folder icon in the Claude tab')
|
|
1204
|
+
setActiveRightTab('claude')
|
|
1205
|
+
return
|
|
1206
|
+
}
|
|
1202
1207
|
|
|
1203
1208
|
setAiScanStatus('scanning')
|
|
1204
1209
|
setAiScanError(null)
|
|
@@ -1251,6 +1256,7 @@ export function ChangesPanel() {
|
|
|
1251
1256
|
setAiScanResult,
|
|
1252
1257
|
showToast,
|
|
1253
1258
|
setActiveLeftTab,
|
|
1259
|
+
setActiveRightTab,
|
|
1254
1260
|
])
|
|
1255
1261
|
|
|
1256
1262
|
const handleSendToClaudeCode = useCallback(
|
|
@@ -261,6 +261,32 @@ export function performRevertAll() {
|
|
|
261
261
|
}
|
|
262
262
|
}
|
|
263
263
|
|
|
264
|
+
// Module-level singletons for auto-persist and load — prevents duplicate
|
|
265
|
+
// subscriptions and stale-data overwrites when multiple components call
|
|
266
|
+
// useChangeTracker() and some of them remount (e.g. LayerNode after tree updates).
|
|
267
|
+
let persistSubscribed = false
|
|
268
|
+
let persistTimeout: ReturnType<typeof setTimeout> | null = null
|
|
269
|
+
let prevChangesRef: unknown = null
|
|
270
|
+
let lastLoadedUrl: string | null = null
|
|
271
|
+
|
|
272
|
+
function ensurePersistSubscription() {
|
|
273
|
+
if (persistSubscribed) return
|
|
274
|
+
persistSubscribed = true
|
|
275
|
+
useEditorStore.subscribe((state) => {
|
|
276
|
+
const ref = state.styleChanges
|
|
277
|
+
if (ref === prevChangesRef) return
|
|
278
|
+
prevChangesRef = ref
|
|
279
|
+
|
|
280
|
+
const url = state.targetUrl
|
|
281
|
+
if (!url) return
|
|
282
|
+
|
|
283
|
+
if (persistTimeout) clearTimeout(persistTimeout)
|
|
284
|
+
persistTimeout = setTimeout(() => {
|
|
285
|
+
useEditorStore.getState().persistChanges(url)
|
|
286
|
+
}, 300)
|
|
287
|
+
})
|
|
288
|
+
}
|
|
289
|
+
|
|
264
290
|
/**
|
|
265
291
|
* Hook that tracks style changes, sends PREVIEW_CHANGE to inspector,
|
|
266
292
|
* and auto-persists changes to localStorage.
|
|
@@ -273,36 +299,18 @@ export function useChangeTracker() {
|
|
|
273
299
|
const pushUndo = useEditorStore((s) => s.pushUndo)
|
|
274
300
|
const { sendToInspector } = usePostMessage()
|
|
275
301
|
|
|
276
|
-
//
|
|
277
|
-
const persistTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
278
|
-
const prevChangesRef = useRef<unknown>(null)
|
|
279
|
-
|
|
302
|
+
// Set up singleton auto-persist subscription (once globally, not per component)
|
|
280
303
|
useEffect(() => {
|
|
281
|
-
|
|
282
|
-
// Trigger on any styleChanges or elementSnapshots reference change
|
|
283
|
-
const ref = state.styleChanges
|
|
284
|
-
if (ref === prevChangesRef.current) return
|
|
285
|
-
prevChangesRef.current = ref
|
|
286
|
-
|
|
287
|
-
const url = state.targetUrl
|
|
288
|
-
if (!url) return
|
|
289
|
-
|
|
290
|
-
// Debounce persistence
|
|
291
|
-
if (persistTimeoutRef.current) clearTimeout(persistTimeoutRef.current)
|
|
292
|
-
persistTimeoutRef.current = setTimeout(() => {
|
|
293
|
-
useEditorStore.getState().persistChanges(url)
|
|
294
|
-
}, 300)
|
|
295
|
-
})
|
|
296
|
-
|
|
297
|
-
return () => {
|
|
298
|
-
unsubscribe()
|
|
299
|
-
if (persistTimeoutRef.current) clearTimeout(persistTimeoutRef.current)
|
|
300
|
-
}
|
|
304
|
+
ensurePersistSubscription()
|
|
301
305
|
}, [])
|
|
302
306
|
|
|
303
|
-
// Load persisted changes when target URL changes
|
|
307
|
+
// Load persisted changes only when target URL actually changes,
|
|
308
|
+
// not on every component mount. This prevents remounting components
|
|
309
|
+
// (e.g. LayerNode after tree updates) from overwriting in-memory
|
|
310
|
+
// changes with stale localStorage data.
|
|
304
311
|
useEffect(() => {
|
|
305
|
-
if (targetUrl) {
|
|
312
|
+
if (targetUrl && targetUrl !== lastLoadedUrl) {
|
|
313
|
+
lastLoadedUrl = targetUrl
|
|
306
314
|
useEditorStore.getState().loadPersistedChanges(targetUrl)
|
|
307
315
|
}
|
|
308
316
|
}, [targetUrl])
|
|
@@ -450,6 +450,12 @@ function handleMessage(event: MessageEvent) {
|
|
|
450
450
|
newParentSelectorPath: mvNewParent,
|
|
451
451
|
oldIndex: mvOldIndex,
|
|
452
452
|
newIndex: mvNewIndex,
|
|
453
|
+
tagName: mvTag,
|
|
454
|
+
className: mvClass,
|
|
455
|
+
elementId: mvId,
|
|
456
|
+
innerText: mvText,
|
|
457
|
+
attributes: mvAttrs,
|
|
458
|
+
computedStyles: mvStyles,
|
|
453
459
|
} = msg.payload
|
|
454
460
|
const mvProperty = '__element_moved__'
|
|
455
461
|
|
|
@@ -464,6 +470,20 @@ function handleMessage(event: MessageEvent) {
|
|
|
464
470
|
changeScope: store.changeScope,
|
|
465
471
|
})
|
|
466
472
|
|
|
473
|
+
// Save element snapshot so move appears in Changes panel
|
|
474
|
+
store.saveElementSnapshot({
|
|
475
|
+
selectorPath: mvNewSelector,
|
|
476
|
+
tagName: mvTag || 'unknown',
|
|
477
|
+
className: mvClass ?? null,
|
|
478
|
+
elementId: mvId ?? null,
|
|
479
|
+
attributes: mvAttrs || {},
|
|
480
|
+
innerText: mvText,
|
|
481
|
+
computedStyles: mvStyles ? { ...mvStyles } : {},
|
|
482
|
+
pagePath: store.currentPagePath,
|
|
483
|
+
changeScope: store.changeScope,
|
|
484
|
+
sourceInfo: null,
|
|
485
|
+
})
|
|
486
|
+
|
|
467
487
|
// Track the move
|
|
468
488
|
store.addStyleChange({
|
|
469
489
|
id: generateId(),
|
package/src/types/messages.ts
CHANGED
|
@@ -337,6 +337,12 @@ export interface ElementMovedMessage {
|
|
|
337
337
|
newParentSelectorPath: string
|
|
338
338
|
oldIndex: number
|
|
339
339
|
newIndex: number
|
|
340
|
+
tagName: string
|
|
341
|
+
className: string | null
|
|
342
|
+
elementId: string | null
|
|
343
|
+
innerText: string | null
|
|
344
|
+
attributes: Record<string, string>
|
|
345
|
+
computedStyles: Record<string, string>
|
|
340
346
|
}
|
|
341
347
|
}
|
|
342
348
|
|