@paramrig/web 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) 2026 Soheil Saheb-Jamii
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,359 @@
1
+ # @paramrig/web
2
+
3
+ Expose a running development page's design decisions as controls, let a person tune
4
+ them in the ParamRig workbench, and read back what they approved as a file in your
5
+ repository.
6
+
7
+ This document is written for the agent that maintains the project being tuned. It
8
+ is self-contained: everything needed to instrument a project, keep it safe, and
9
+ answer the feedback is here. The workbench's own behaviour — selection, drawing,
10
+ review — is described in `docs/web-workspace.md` in the ParamRig repository.
11
+
12
+ The integration is **development only**. It is framework-neutral, depends on
13
+ nothing, and does nothing at all unless a workbench is framing the page.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ npm install --save-dev @paramrig/web
19
+ ```
20
+
21
+ Releases are published from the ParamRig repository by its release workflow, on a
22
+ `web-v<version>` tag, with provenance. To try an unreleased build, run
23
+ `docker compose run --rm app npm run build:web-sdk` there and install the
24
+ resulting `packages/web-sdk` directory as a local dependency.
25
+
26
+ ## 1. Describe the controls
27
+
28
+ Write `.paramrig/manifest.json` at the root of the project. It is the contract: it
29
+ says what can be tuned, where the page runs, and which source revision it describes.
30
+
31
+ ```json
32
+ {
33
+ "version": 1,
34
+ "id": "fieldnotes",
35
+ "name": "Fieldnotes",
36
+ "revision": "study-1",
37
+ "origin": "http://localhost:3000",
38
+ "pages": [{ "id": "home", "name": "Home", "path": "/" }],
39
+ "groups": [{ "id": "brand", "label": "Brand" }],
40
+ "parameters": [
41
+ { "id": "accent", "kind": "color", "label": "Accent", "group": "brand", "defaultValue": "#df7757" }
42
+ ],
43
+ "bindings": [
44
+ { "paramId": "accent", "scope": "global", "kind": "css-variable", "property": "--accent" }
45
+ ]
46
+ }
47
+ ```
48
+
49
+ | Field | Meaning |
50
+ | --- | --- |
51
+ | `version` | Always `1`. |
52
+ | `id` | Stable identifier, `[A-Za-z0-9][A-Za-z0-9_-]{0,127}`. Never change it: the workbench's library, its saved drafts and every batch on disk are keyed by it. |
53
+ | `name` | What a person sees. |
54
+ | `revision` | The source revision this manifest describes: any string — a counter, a date, a short label — as long as each one is new. **Change it whenever you change the code the controls describe.** The workbench waits rather than applying a batch to code it does not match. |
55
+ | `origin` | The exact HTTP or HTTPS origin the development server answers on — scheme, host and port, nothing else. |
56
+ | `pages` | At least one, at most 100. Each has a unique `id`, a `name` and a `path` starting with `/`. |
57
+ | `groups` | Named sections for the controls. Each has an `id` and a `label`. |
58
+ | `parameters` | The controls themselves, at most 500, with unique `id`s. |
59
+ | `bindings` | What each control writes, and how far it reaches. |
60
+
61
+ `parseManifest` throws on anything it does not accept, and the message names what
62
+ is wrong. Call it yourself so the failure is yours rather than the workbench's.
63
+
64
+ ### Control kinds
65
+
66
+ Every parameter has `id`, `label`, `group`, `kind` and `defaultValue`.
67
+
68
+ `number` · `color` · `select` · `curve` · `switch` · `gradient` · `text` ·
69
+ `vector` · `range` · `palette` · `points` · `radial` · `group` · `list` ·
70
+ `resource` · `gizmo2d` · `gizmo3d` · `camera` · `textureFrame` · `multiselect` ·
71
+ `action` · `preset`
72
+
73
+ Four of them require more:
74
+
75
+ - `number` requires `min`, `max` and a `step` greater than zero. Its `unit` is
76
+ what a person reads beside the control; the binding's `unit`, below, is what is
77
+ written to CSS. They are usually the same string.
78
+ - `select`, `multiselect` and `preset` require `options`, each `{ value, label }`.
79
+ - `group` requires `fields`, an array of parameters, nested at most 8 deep.
80
+ - `list` requires `item`, one parameter describing an element.
81
+
82
+ The others carry optional fields of their own — `text` has `maxLength` and
83
+ `multiline`, for instance — listed in the package's type declarations,
84
+ `dist/rigs/extended-types.d.ts`. All twenty-one exist because the same controls
85
+ serve ParamRig's vector and 3D rigs; on a web page, the useful ones are those
86
+ whose value maps onto a CSS value or onto an adapter you write.
87
+
88
+ Start with `number`, `color`, `select`, `switch` and `text`. They cover most of
89
+ what a person wants to move, and every one of them reads clearly in a review.
90
+
91
+ ### Bindings
92
+
93
+ A binding names a `paramId`, a `scope`, a `kind` and a `property`.
94
+
95
+ **Scope** — how far a change reaches, declared by you and never changed by which
96
+ element a person happens to select:
97
+
98
+ - `global` — the whole application.
99
+ - `page` — one page; also declare `pageId`.
100
+ - `element` — one instrumented family; also declare `target: { id, instance? }`.
101
+ Leaving `instance` out is deliberate: it applies to every instance of that
102
+ family, and the workbench says so in the review.
103
+
104
+ **Kind** — where the value goes:
105
+
106
+ - `css-variable` — writes a CSS custom property. `property` is the property name,
107
+ `--accent`. Prefer this for design tokens: one binding moves everything that
108
+ reads the token. A string is written as it is: `Public Sans` needs no quotes
109
+ to reach `font-family` through `var()`.
110
+ - `style` — writes one declared CSS property on the target. `property` is the CSS
111
+ property, `font-size`. The original inline value and its priority are preserved
112
+ and restored.
113
+ - `adapter` — calls a `read`/`apply`/`restore` adapter you registered in the
114
+ application. `property` is the adapter's key. Use it for anything CSS cannot
115
+ express: copy, state, a canvas, a composite value.
116
+
117
+ `unit` is appended to numeric CSS values — `"unit": "px"`. Without it a number is
118
+ written bare, which is what `line-height` and `opacity` want.
119
+
120
+ `source` is optional: a file path shown beside the control, the same claim as
121
+ `data-paramrig-source` on an element. Nothing reads it as an instruction, and a
122
+ stale one is not an error, only a hint that lies.
123
+
124
+ **How the source value is read.** When the workbench pairs, the SDK reads what
125
+ the page shows rather than trusting the manifest: `read()` for an adapter, the
126
+ computed style for a CSS property or custom property. `defaultValue` is the
127
+ fallback when that read gives nothing. A computed length comes back in pixels
128
+ whatever the stylesheet declared, so the SDK converts it into the binding's
129
+ `unit` when the two are commensurable — `rem`, `em`, `vw`, `pt` and the other
130
+ lengths, `s` and `ms`, `deg` and `turn`. A custom property reads back as you
131
+ declared it, and the same conversion applies when it carries a unit. A
132
+ percentage cannot be read back, nor
133
+ can a bare number the browser reports with a unit, such as a unitless
134
+ `line-height`; there the `defaultValue` stands, so keep it true.
135
+
136
+ Callbacks never travel through a manifest, a batch or a message. Resource controls
137
+ carry metadata, not file bytes; expose a URL your project owns, or an adapter.
138
+
139
+ ## 2. Name the elements
140
+
141
+ An element the workbench can talk about carries `data-paramrig-id`. Everything
142
+ else is optional.
143
+
144
+ ```html
145
+ <article data-paramrig-id="story-card"
146
+ data-paramrig-instance="coast"
147
+ data-paramrig-label="Story card"
148
+ data-paramrig-source="src/StoryCard.tsx">
149
+ <h2 data-paramrig-id="story-title">Following the coastline</h2>
150
+ </article>
151
+ ```
152
+
153
+ - `data-paramrig-id` — a stable semantic identifier. It is what a binding points
154
+ at, and what survives a refactor. Keep it when you move the markup.
155
+ - `data-paramrig-instance` — tells repeated components apart. Descendants inherit
156
+ it, so the title above belongs to `coast` without repeating it.
157
+ - `data-paramrig-label` — the name a person reads. Without it the identifier is
158
+ read as a sentence (`story-card` becomes **Story card**), then the accessible
159
+ name, then the words on screen, then `Unnamed article`.
160
+ - `data-paramrig-source` — a hint about where this comes from, shown beside the
161
+ element. It is your claim, not an inferred source map; keep it honest or omit it.
162
+
163
+ Instrument the elements most likely to receive feedback. Uninstrumented ones can
164
+ still be selected and commented on, but their reference is provisional and has to
165
+ be reattached after a reload.
166
+
167
+ ## 3. Connect, in development only
168
+
169
+ ```ts
170
+ import { connectWeb, parseManifest } from '@paramrig/web'
171
+ import manifestFile from './.paramrig/manifest.json'
172
+
173
+ const connection = connectWeb({
174
+ manifest: parseManifest(manifestFile),
175
+ adapters: {
176
+ headings: {
177
+ read: () => currentHeadingFont,
178
+ apply: value => setHeadingFont(String(value)),
179
+ restore: () => setHeadingFont(initialHeadingFont),
180
+ },
181
+ },
182
+ })
183
+
184
+ connection.dispose()
185
+ ```
186
+
187
+ React:
188
+
189
+ ```tsx
190
+ useEffect(() => {
191
+ if (!import.meta.env.DEV) return
192
+ const connection = connectWeb({ manifest: parseManifest(manifestFile), adapters })
193
+ return () => connection.dispose()
194
+ }, [])
195
+ ```
196
+
197
+ **Always dispose.** On unmount, and on hot-module replacement — `import.meta.hot`
198
+ in Vite, `module.hot` in webpack. A second `connectWeb` without a `dispose` is
199
+ handled (it warns and replaces the first, so two overlays never coexist), but the
200
+ warning is telling you the cleanup is missing.
201
+
202
+ **Keep it out of production.** The example above is enough under Vite: the
203
+ package declares `sideEffects: false`, the guarded call is dead code once
204
+ `import.meta.env.DEV` is `false`, and the bundler drops the import with it. Keep
205
+ `parseManifest` and the manifest inside the guard too, or they stay in the
206
+ bundle. With another bundler, or `process.env.NODE_ENV`, put a dynamic
207
+ `import()` behind the same check.
208
+
209
+ `hostOrigin` is optional and takes one origin or a list. It defaults to
210
+ `['http://localhost:5174', 'http://127.0.0.1:5174']`, the two addresses the
211
+ workbench answers on. Name your own only if you run the workbench elsewhere.
212
+
213
+ ### What it does when there is no workbench
214
+
215
+ `connectWeb` never throws, because a development integration that takes the
216
+ application off the screen is worse than one that is unavailable:
217
+
218
+ - **Not in a frame** — the page opened normally — it installs nothing. No
219
+ listener, no observer, no overlay, and nothing in the console.
220
+ - **The manifest's `origin` is not the page's origin**, or `hostOrigin` is not an
221
+ origin: one `console.warn` naming both, then nothing. A development server
222
+ usually answers to both `localhost` and `127.0.0.1`; opening the page by the
223
+ other name should cost a line, not a blank screen. Fix it by opening the page at
224
+ the manifest's origin, or by writing the origin you actually use.
225
+
226
+ `parseManifest` does throw. An invalid manifest is a programming error, and you
227
+ called it.
228
+
229
+ ### Framing, and HTTPS
230
+
231
+ The workbench opens your page in a cross-origin iframe, so your development server
232
+ has to allow it:
233
+
234
+ ```
235
+ Content-Security-Policy: frame-ancestors 'self' http://localhost:5174 http://127.0.0.1:5174;
236
+ ```
237
+
238
+ and it must not send a conflicting `X-Frame-Options`. Scope that header to the
239
+ development configuration. Never relax it in production, and never remove the
240
+ production header to make a local session work.
241
+
242
+ If your development server is HTTPS, the workbench must be reached over HTTPS as
243
+ well: a browser will not frame an `https:` page from an `http:` document without
244
+ complaint, and mixed content is refused outright.
245
+
246
+ The pairing itself is checked at every step. The SDK announces itself to its
247
+ parent; the workbench answers with `hello`; that reply pins the origin and the
248
+ session that every later message, in both directions, is checked against. Nothing
249
+ is listening before the reply, and the application's own clicks pass through.
250
+
251
+ ## 4. What to commit in `.paramrig`
252
+
253
+ | File | Who writes it | Commit it? |
254
+ | --- | --- | --- |
255
+ | `manifest.json` | You | **Yes.** It is source: it describes your code. |
256
+ | `batches/<id>.json` | ParamRig | Your call. They are immutable and they are the record of what was asked. Committing them lets the next agent read the history; they can be large when they carry markup. |
257
+ | `responses/<id>.json` | You | Your call, and it should match `batches/`. |
258
+ | `draft.json` | ParamRig | **No.** It is rewritten continuously and is not approved instructions. |
259
+ | `captures/*.png` | ParamRig | **No.** Large, and reproducible from the batch. |
260
+ | `README.md` | ParamRig | Your call. It repeats what is here. |
261
+ | `.gitignore` | ParamRig | Yes — it is the two "no" rows above. |
262
+
263
+ ParamRig writes that `.gitignore` when there is none, covering `draft.json` and
264
+ `captures/`, and never writes over it. Everything else is yours to decide.
265
+
266
+ ParamRig writes nothing outside `.paramrig`. It does not start your application,
267
+ install anything, or touch your source.
268
+
269
+ ## 5. The loop
270
+
271
+ A person tunes controls, comments on elements, draws on the page, then approves.
272
+ That writes one immutable file: `.paramrig/batches/<id>.json`. They will hand you
273
+ an instruction naming it.
274
+
275
+ **Read the batch.** It carries `values` (every control's value, not only the ones
276
+ that moved), `changes` (each with `before`, `after` and the bindings it writes),
277
+ and `tickets` (the comments, each with its targets, marks, page, viewport, scroll
278
+ context and captures). A batch restates every difference from your source, so the
279
+ newest one alone is the whole picture, and a control present in `values` but
280
+ absent from `changes` did not move. A ticket need not map to a change either:
281
+ when the approved values already do what it asks — one binding carrying a font
282
+ to every heading — say so in its message rather than look for a second edit.
283
+
284
+ **Apply it in the source.** Move the real values in the real files — the batch is
285
+ a request, not a patch. A value behind an adapter usually lives in three places
286
+ that move together: the initial state, `read` and `restore`. Preserve every
287
+ `data-paramrig-id` and `data-paramrig-instance`. Then, in `manifest.json`, set
288
+ each changed control's `defaultValue` to the value you applied, so the manifest
289
+ tells the truth even without a page to read, and change `revision`, because the
290
+ code the controls describe has changed.
291
+
292
+ **Write one response**, atomically: a temporary file, then a rename.
293
+
294
+ ```json
295
+ {
296
+ "version": 1,
297
+ "id": "response-001",
298
+ "projectId": "fieldnotes",
299
+ "batchId": "the-approved-batch-id",
300
+ "sourceRevision": "study-1",
301
+ "resultRevision": "study-2",
302
+ "createdAt": "2026-09-05T12:00:00.000Z",
303
+ "summary": "Applied the approved palette and adjusted the hero.",
304
+ "tickets": [
305
+ { "id": "the-ticket-id", "status": "implemented", "message": "Check the hero at mobile width." }
306
+ ]
307
+ }
308
+ ```
309
+
310
+ - `id` must be unique, and the file must be named `<id>.json`. Any name the
311
+ identifier pattern accepts will do — letters, digits, `-` and `_`, no dot —
312
+ `response-<uuid>` and a counter alike.
313
+ - `projectId` is the manifest's `id`; `batchId` names the batch you read.
314
+ - `sourceRevision` is the batch's own `sourceRevision`. `resultRevision` is the
315
+ new manifest revision. A response naming another revision is reported, not
316
+ applied.
317
+ - `tickets` answers the tickets of that batch, each its own way, and only tickets
318
+ that belong to it. `status` is `implemented` or `needs-info`.
319
+ - `message` is what a person reads: what changed, and how to check it.
320
+
321
+ Use `needs-info` when you cannot act. Say precisely what you need — a value, a
322
+ choice between two readings, a page you cannot reach. It reopens the ticket for
323
+ another round rather than closing it wrongly.
324
+
325
+ **A response is a claim, not an approval.** Write `summary` and each `message` as
326
+ what you did and how to check it, never as a certification. Only the person
327
+ validates a correction, in the workbench.
328
+
329
+ **One round, end to end.** The batch says `accent` moved from `#bc593d` to
330
+ `#336b72` through `--accent`, and `heading-font` from `Georgia` to `Public Sans`
331
+ through the `headings` adapter; one ticket on the hero asks for the coastal
332
+ palette. You change `--accent` in the stylesheet; you change the initial font,
333
+ `read` and `restore` in the adapter; you set both `defaultValue`s, move
334
+ `revision` from `study-1` to `study-2`, and write `responses/response-001.json`
335
+ naming the batch, `study-1` as `sourceRevision`, `study-2` as `resultRevision`,
336
+ and the ticket as `implemented` with a message saying where to look. The batch
337
+ itself you do not touch.
338
+
339
+ **What never changes.** Do not edit or delete a batch, `draft.json`, or a capture
340
+ file. Batches are immutable, and the workbench refuses a rewrite. A ticket that is
341
+ reopened leaves its batch and drops the answer that batch received, so a later
342
+ response to a batch a ticket has left does not reach it.
343
+
344
+ ## Schemas
345
+
346
+ Machine-readable JSON Schema (draft 2020-12) for the three files, shipped with the
347
+ package and importable as `@paramrig/web/schemas/<name>.schema.json`:
348
+
349
+ - `schemas/manifest.schema.json`
350
+ - `schemas/batch.schema.json`
351
+ - `schemas/response.schema.json`
352
+
353
+ They are a second expression of the guards in the SDK, and a test in the ParamRig
354
+ repository fails the moment the two disagree. Validate against them in your own
355
+ checks; the guards remain the authority at runtime.
356
+
357
+ ## Licence
358
+
359
+ MIT. See `LICENSE` in the ParamRig repository.