@parity-app/cli 0.1.0 → 0.3.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.
@@ -1 +1 @@
1
- export { t as covers, n as registryDir, r as registryEntries } from "./src-DXZqtm6z.mjs";
1
+ export { t as covers, n as registryDir, r as registryEntries } from "./src-eso2QXtH.mjs";
@@ -1,2 +1,3 @@
1
- export { a as applyLibraryEdits, t as componentNode, y as generateId, n as librarySource, f as parseLibraryEdits, i as withLibraryContext } from "./src-D9EMN48r.mjs";
2
- import "./src-8MhDV_sA.mjs";
1
+ import "./client-Cf66-n3R.mjs";
2
+ export { a as applyLibraryEdits, t as componentNode, y as generateId, n as librarySource, f as parseLibraryEdits, i as withLibraryContext } from "./src-lVLSuOgh.mjs";
3
+ import "./src-Aoey5Uq0.mjs";
@@ -1,4 +1,4 @@
1
- import { p as parseLibraryManifest, u as libraryManifestExtension } from "./src-D9EMN48r.mjs";
1
+ import { p as parseLibraryManifest, u as libraryManifestExtension } from "./src-lVLSuOgh.mjs";
2
2
  import { createRequire } from "node:module";
3
3
  import { readFile, readdir } from "node:fs/promises";
4
4
  import { dirname, join } from "node:path";
@@ -1,11 +1,3 @@
1
- // ../core/src/css.ts
2
- /**
3
- * Custom properties holding 1% of a frame's viewport width and height, which viewport units are rewritten to: in a frame,
4
- * `100vh` is the height of the screen the frame stands for, not of Parity's window.
5
- */
6
- const viewportWidthProperty = "--canvas-vw";
7
- const viewportHeightProperty = "--canvas-vh";
8
- new RegExp(String.raw`"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|url\([^)]*\)|var\((?:${viewportWidthProperty}|${viewportHeightProperty}),[^)]*\)`, "g");
9
1
  // ../core/src/devices.ts
10
2
  const deviceKinds = [
11
3
  "phone",
@@ -24,6 +16,110 @@ function lengthToPixels(value) {
24
16
  if (!Number.isFinite(number)) return void 0;
25
17
  return match[2] === "rem" || match[2] === "em" ? number * 16 : number;
26
18
  }
19
+ // ../core/src/css.ts
20
+ /**
21
+ * Custom properties holding 1% of a frame's viewport width and height, which viewport units are rewritten to: in a frame,
22
+ * `100vh` is the height of the screen the frame stands for, not of Parity's window.
23
+ */
24
+ const viewportWidthProperty = "--canvas-vw";
25
+ const viewportHeightProperty = "--canvas-vh";
26
+ /** Every custom property declaration in a stylesheet, in source order. */
27
+ function collectCustomProperties(css) {
28
+ const declarations = [];
29
+ const visit = (nodes, selector, atRules) => {
30
+ for (const node of nodes) if (node.kind === "block") {
31
+ const prelude = node.prelude.trim();
32
+ const atRule = /^@([\w-]+)\s*([\s\S]*)$/.exec(prelude);
33
+ if (atRule) visit(node.children, selector, [...atRules, {
34
+ name: atRule[1].toLowerCase(),
35
+ prelude: atRule[2].trim()
36
+ }]);
37
+ else visit(node.children, prelude, atRules);
38
+ } else if (selector !== void 0 || atRules.length > 0) for (const match of node.text.matchAll(/--([\w-]+)\s*:\s*([^;]+)/g)) declarations.push({
39
+ selector: selector ?? "",
40
+ atRules,
41
+ name: match[1],
42
+ value: match[2].trim()
43
+ });
44
+ };
45
+ visit(parse(css), void 0, []);
46
+ return declarations;
47
+ }
48
+ function parse(css) {
49
+ let index = 0;
50
+ function parseLevel() {
51
+ const nodes = [];
52
+ let buffer = "";
53
+ let afterLastSemicolon = 0;
54
+ let parens = 0;
55
+ while (index < css.length) {
56
+ const char = css[index];
57
+ if (char === "/" && css[index + 1] === "*") {
58
+ const end = css.indexOf("*/", index + 2);
59
+ index = end === -1 ? css.length : end + 2;
60
+ continue;
61
+ }
62
+ if (char === "\\") {
63
+ buffer += css.slice(index, index + 2);
64
+ index += 2;
65
+ continue;
66
+ }
67
+ if (char === "\"" || char === "'") {
68
+ const end = findStringEnd(css, index);
69
+ buffer += css.slice(index, end);
70
+ index = end;
71
+ continue;
72
+ }
73
+ index++;
74
+ if (char === "(") parens++;
75
+ if (char === ")") parens = Math.max(0, parens - 1);
76
+ if (parens > 0) {
77
+ buffer += char;
78
+ continue;
79
+ }
80
+ if (char === ";") {
81
+ buffer += char;
82
+ afterLastSemicolon = buffer.length;
83
+ } else if (char === "{") {
84
+ const text = buffer.slice(0, afterLastSemicolon);
85
+ if (text) nodes.push({
86
+ kind: "text",
87
+ text
88
+ });
89
+ nodes.push({
90
+ kind: "block",
91
+ prelude: buffer.slice(afterLastSemicolon),
92
+ children: parseLevel()
93
+ });
94
+ buffer = "";
95
+ afterLastSemicolon = 0;
96
+ } else if (char === "}") {
97
+ if (buffer) nodes.push({
98
+ kind: "text",
99
+ text: buffer
100
+ });
101
+ return nodes;
102
+ } else buffer += char;
103
+ }
104
+ if (buffer) nodes.push({
105
+ kind: "text",
106
+ text: buffer
107
+ });
108
+ return nodes;
109
+ }
110
+ const nodes = [];
111
+ while (index < css.length) nodes.push(...parseLevel());
112
+ return nodes;
113
+ }
114
+ function findStringEnd(css, start) {
115
+ const quote = css[start];
116
+ let index = start + 1;
117
+ while (index < css.length) if (css[index] === "\\") index += 2;
118
+ else if (css[index] === quote) return index + 1;
119
+ else index++;
120
+ return css.length;
121
+ }
122
+ new RegExp(String.raw`"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|url\([^)]*\)|var\((?:${viewportWidthProperty}|${viewportHeightProperty}),[^)]*\)`, "g");
27
123
  // ../core/src/validate.ts
28
124
  function expectObject(value, path) {
29
125
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${path || "/"}: expected an object`);
@@ -270,10 +366,7 @@ function parseDetection(value, path) {
270
366
  export: expectString(ref.export, `${at}/${index}/export`)
271
367
  };
272
368
  });
273
- return compact({
274
- include: optional(detection.include, (list) => refs(list, `${path}/include`)),
275
- exclude: optional(detection.exclude, (list) => refs(list, `${path}/exclude`))
276
- });
369
+ return compact({ include: optional(detection.include, (list) => refs(list, `${path}/include`)) });
277
370
  }
278
371
  function parseNote(value, path) {
279
372
  const note = expectObject(value, path);
@@ -575,13 +668,24 @@ function parsePatch(value, path) {
575
668
  "hidden",
576
669
  "text",
577
670
  "props",
578
- "style"
671
+ "style",
672
+ "unset"
579
673
  ], path);
580
674
  return compact({
581
675
  hidden: optional(patch.hidden, (v) => expectBoolean(v, `${path}/hidden`)),
582
676
  text: optional(patch.text, (v) => expectString(v, `${path}/text`)),
583
677
  props: optional(patch.props, (v) => expectObject(v, `${path}/props`)),
584
- style: optional(patch.style, (v) => parseStyle(v, `${path}/style`))
678
+ style: optional(patch.style, (v) => parseStyle(v, `${path}/style`)),
679
+ unset: optional(patch.unset, (v) => parseUnset(v, `${path}/unset`))
680
+ });
681
+ }
682
+ function parseUnset(value, path) {
683
+ const unset = expectObject(value, path);
684
+ expectKnownKeys(unset, ["props", "style"], path);
685
+ const keys = (key, item) => optional(unset[key], (v) => expectArray(v, `${path}/${key}`).map((entry, index) => item(entry, `${path}/${key}/${index}`)));
686
+ return compact({
687
+ props: keys("props", expectString),
688
+ style: keys("style", (v, where) => expectOneOf(v, styleKeys, where))
585
689
  });
586
690
  }
587
691
  function parseStyle(value, path) {
@@ -729,7 +833,7 @@ const decoratorKeys = [
729
833
  const libraryManifestExtension = ".lib.par";
730
834
  function parseLibraryManifest(value) {
731
835
  const manifest = expectObject(value, "");
732
- const keys = [
836
+ expectKnownKeys(manifest, [
733
837
  "$schema",
734
838
  "version",
735
839
  "id",
@@ -738,13 +842,16 @@ function parseLibraryManifest(value) {
738
842
  "styles",
739
843
  "breakpoints",
740
844
  "tokens",
741
- "canvas"
742
- ];
743
- if (manifest.version === 3) keys.push("package", "components", "icons", "decorators", "edits");
744
- expectKnownKeys(manifest, keys, "");
745
- if (manifest.version !== 1 && manifest.version !== 2 && manifest.version !== 3) throw new TypeError("/version: expected 1, 2 or 3");
845
+ "canvas",
846
+ "package",
847
+ "components",
848
+ "icons",
849
+ "decorators",
850
+ "edits"
851
+ ], "");
852
+ if (manifest.version !== 3) throw new TypeError("/version: expected 3");
746
853
  return compact({
747
- version: manifest.version,
854
+ version: 3,
748
855
  id: optional(manifest.id, (v) => expectString(v, "/id")),
749
856
  name: expectString(manifest.name, "/name"),
750
857
  description: optional(manifest.description, (v) => expectString(v, "/description")),
@@ -1074,7 +1181,7 @@ function expectStringRecord(value, path) {
1074
1181
  const named = /* @__PURE__ */ new WeakSet();
1075
1182
  /**
1076
1183
  * A new node for a component, ready to show something: required props get their default or first option, and a
1077
- * component that takes children gets its own name as text, so it isn't an empty box.
1184
+ * component that takes children gets its own name as text, so it isn't an empty box, unless `given` has its own.
1078
1185
  */
1079
1186
  function componentNode(component, info, taken, given = {}) {
1080
1187
  const props = { ...given };
@@ -1086,7 +1193,7 @@ function componentNode(component, info, taken, given = {}) {
1086
1193
  else if (prop.kind === "number") props[prop.name] = 0;
1087
1194
  else if (prop.kind === "string") props[prop.name] = prop.name;
1088
1195
  }
1089
- const labelled = info?.props.some((prop) => prop.name === "children" && prop.kind === "node");
1196
+ const labelled = !("children" in given) && info?.props.some((prop) => prop.name === "children" && prop.kind === "node");
1090
1197
  if (labelled) props.children = component.split("/").at(-1) ?? component;
1091
1198
  const node = {
1092
1199
  type: "component",
@@ -1247,10 +1354,10 @@ function withLibraryContext(node, sources, around, taken) {
1247
1354
  }
1248
1355
  }
1249
1356
  /**
1250
- * A library file's component as the canvas inserts it, but for where it's declared: its import, its parents and sample
1251
- * by component id (`nova/AccordionItem`), and `imported` naming the package the way the project depends on it.
1357
+ * A library file's component as the canvas inserts it, with its parents and sample named by component id.
1358
+ * Discovery has already rewritten module imports for the name the project installed the package under.
1252
1359
  */
1253
- function librarySource(library, name, component, path, imported = (specifier) => specifier) {
1360
+ function librarySource(library, name, component, path) {
1254
1361
  return {
1255
1362
  path,
1256
1363
  export: name,
@@ -1261,14 +1368,13 @@ function librarySource(library, name, component, path, imported = (specifier) =>
1261
1368
  ...component.visual === false ? { visual: false } : {},
1262
1369
  ...component.optionalParents ? { optionalParents: component.optionalParents.map((parent) => `${library}/${parent}`) } : {},
1263
1370
  ...component.parentParts ? { parentParts: true } : {},
1264
- ...component.sample ? { sample: sampleIds(component.sample, library, imported) } : {}
1371
+ ...component.sample ? { sample: sampleIds(component.sample, library) } : {}
1265
1372
  };
1266
1373
  }
1267
1374
  /**
1268
- * A sample with its children's components named by id, as `nova/AccordionItem`; HTML elements keep their tag. A value the
1269
- * package makes (`{ $call: { import: "@chakra-ui/react", ... } }`) imports it by the name the project depends on.
1375
+ * A sample with its children's components named by id, as `nova/AccordionItem`; HTML elements keep their tag.
1270
1376
  */
1271
- function sampleIds(sample, library, imported = (specifier) => specifier) {
1377
+ function sampleIds(sample, library) {
1272
1378
  const child = (node) => {
1273
1379
  if (typeof node === "string") return node;
1274
1380
  if (isSampleRender(node)) return {
@@ -1288,13 +1394,7 @@ function sampleIds(sample, library, imported = (specifier) => specifier) {
1288
1394
  $render: item.$render.map(child)
1289
1395
  };
1290
1396
  if (isSampleElement(item)) return { $element: item.$element.map(child) };
1291
- return isMadeValue(item) ? {
1292
- ...item,
1293
- $call: {
1294
- ...item.$call,
1295
- import: imported(item.$call.import)
1296
- }
1297
- } : item;
1397
+ return item;
1298
1398
  };
1299
1399
  const propIds = (props) => Object.fromEntries(Object.entries(props).map(([key, item]) => [key, propId(item)]));
1300
1400
  return {
@@ -1303,4 +1403,4 @@ function sampleIds(sample, library, imported = (specifier) => specifier) {
1303
1403
  ...sample.children ? { children: sample.children.map(child) } : {}
1304
1404
  };
1305
1405
  }
1306
- export { viewportHeightProperty as C, lengthToPixels as S, parseCanvasDocument as _, applyLibraryEdits as a, parseModeSettings as b, isSampleElement as c, parentSignals as d, parseLibraryEdits as f, paramTypes as g, imageFits as h, withLibraryContext as i, isSampleRender as l, renderSlotOf as m, librarySource as n, componentEvidence as o, parseLibraryManifest as p, sampleIds as r, isMadeValue as s, componentNode as t, libraryManifestExtension as u, styleKeys as v, viewportWidthProperty as w, deviceKinds as x, generateId as y };
1406
+ export { viewportWidthProperty as C, viewportHeightProperty as S, lengthToPixels as T, parseCanvasDocument as _, applyLibraryEdits as a, parseModeSettings as b, isSampleElement as c, parentSignals as d, parseLibraryEdits as f, paramTypes as g, imageFits as h, withLibraryContext as i, isSampleRender as l, renderSlotOf as m, librarySource as n, componentEvidence as o, parseLibraryManifest as p, sampleIds as r, isMadeValue as s, componentNode as t, libraryManifestExtension as u, styleKeys as v, deviceKinds as w, collectCustomProperties as x, generateId as y };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parity-app/cli",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "Drive the Parity app from a terminal or an agent: read state, run edits, take screenshots, serve MCP.",
@@ -34,6 +34,11 @@
34
34
  "dist",
35
35
  "skills"
36
36
  ],
37
+ "scripts": {
38
+ "build": "vp pack",
39
+ "prepack": "vp pack",
40
+ "try-packed": "node scripts/try-packed.ts"
41
+ },
37
42
  "publishConfig": {
38
43
  "access": "public"
39
44
  },
@@ -42,12 +47,9 @@
42
47
  "typescript": "6.0.3"
43
48
  },
44
49
  "devDependencies": {
50
+ "@parity-app/agent": "0.0.0",
45
51
  "@parity-app/core": "0.0.0",
46
52
  "@parity-app/libgen": "0.0.0",
47
53
  "@parity-app/registry": "0.0.0"
48
- },
49
- "scripts": {
50
- "build": "vp pack",
51
- "try-packed": "node scripts/try-packed.ts"
52
54
  }
53
- }
55
+ }
package/skills/SKILL.md CHANGED
@@ -22,25 +22,62 @@ it.
22
22
 
23
23
  ## Tools
24
24
 
25
- The same tools are MCP tools and `parity` commands in a terminal. They act on a tab, which is one open project; the
26
- active tab is the default.
25
+ The same tools are MCP tools and `parity` commands in a terminal. They act on a tab, which is one open project. Without
26
+ `tab`, they act on the open project your working folder is in, and otherwise on `active`: what the person is looking at,
27
+ the active tab of the Parity window they used last. When the person says "this" or "what I've selected" without naming a
28
+ project, pass `tab: "active"` (`--tab active`) and read `get_app_state` for the selection.
27
29
 
28
30
  | MCP | CLI | What it does |
29
31
  | --------------------- | ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
30
32
  | `get_app_state` | `parity state` | Designs, the selection, modes, components and token names, and what Parity detected (builder and config, package manager, installed versions). Start here. |
31
- | `execute` | `parity exec` | Runs a JavaScript snippet against the designs. All changes land as one undo step. |
33
+ | `get_status` | `parity status` | The revision, whether anything is unsaved and which files and tokens, the last save, and undo and redo; `summary` names them. |
34
+ | `get_modes` | `parity modes [design]` | Viewports (the ids `Responsive` takes), modes (the ids renders take) and what each design shows; a design adds its frames. |
35
+ | `execute` | `parity exec` | Runs a JavaScript snippet against the designs. Its changes land as one undo step, merged per design with edits made meanwhile. |
32
36
  | `screenshot` | `parity screenshot <design>` | Renders a design in its states and modes, offscreen, and returns the images. |
33
37
  | `find_problems` | `parity problems <design>` | Render errors, overflow, clipping and unknown components, per frame. |
34
38
  | `check_accessibility` | `parity a11y <design>` | axe-core against WCAG 2.2 AA, per frame, on the layers that break each rule. |
35
39
  | `get_console` | `parity console` | What the project's own code wrote to the console, on the canvas and offscreen. |
40
+ | `get_tokens` | `parity tokens` | Tokens filtered by category, source or name, with values in every theme. |
41
+ | `get_components` | `parity components` | Components, libraries, icon sets and exports left out or failing to render. |
42
+ | `get_props` | `parity props <Component>` | Props from source; `all` includes passed-through attributes. |
36
43
  | `get_tabs` | `parity tabs` | The open projects. |
37
44
  | `open_project` | `parity open <folder>` | Opens a project in a new tab. |
38
45
  | `read_skill` | `parity skill [file]` | This skill. |
39
46
 
40
- In a terminal, `parity tokens`, `parity components` and `parity props <Component>` read what designs can use without
41
- writing a snippet. `parity components` also says which of the app's exports aren't components and why, which components
42
- fail to draw on the canvas and why (often a missing provider, which a decorator adds), and what the person listed or
43
- took off the list by hand.
47
+ The token, component and prop tools read what designs can use without writing a snippet in either transport.
48
+ `parity components` also says which of the app's exports aren't components and why, which components fail to draw on the
49
+ canvas and why (often a missing provider, which a decorator adds), and what the person listed or took off the list by
50
+ hand.
51
+
52
+ CLI commands and MCP tools share validation and defaults. Screenshot scale defaults to 1 and accepts 0.25 to 3. Audits
53
+ check every state and shown mode unless narrowed; a single-element selector list is equivalent to a string. Screenshot
54
+ failures and audit findings exit with 1 in the CLI and set MCP `isError`, with details retained. `get_console` defaults
55
+ `clear` to false; `get_props` defaults `all` to false. MCP project paths must be absolute; the CLI resolves relative
56
+ folders from its working directory. Skill reading is local and needs no running app.
57
+
58
+ Several copies of Parity can run at once, each with its own projects open. Each call goes to the copy that has its
59
+ project open, so naming the project is enough; `get_tabs` lists every copy's projects. A project open in two copies goes
60
+ to the one that has been running longest. A copy kept apart with `PARITY_AGENT_FILE` is reached only through that file:
61
+
62
+ ```sh
63
+ parity tabs # every project open in any copy
64
+ parity state --tab active # what the person is looking at, and their selection
65
+ PARITY_AGENT_FILE=copy/agent.json pnpm dev & # a copy of Parity kept apart
66
+ PARITY_AGENT_FILE=copy/agent.json parity tabs # just that copy
67
+ ```
68
+
69
+ Every tool returns its result as data too: MCP `structuredContent`, or `--json` in the CLI. Read that rather than the
70
+ text. A failure has `error.code` to branch on, `retryable` (send it again unchanged), `path` (the input at fault) and
71
+ `alternatives` (valid ids for one it didn't know), so a mistyped id is fixed in one retry:
72
+
73
+ ```sh
74
+ parity problems button --state loading --json
75
+ # {"error": {"code": "unknown_state", "path": "/state", "alternatives": ["default", "outline", "destructive", "disabled"], ...}}
76
+ parity problems button --state disabled --json
77
+ ```
78
+
79
+ `get_tokens` returns the first 100 tokens and `get_console` the newest 50 messages, with `total` and `truncated`; narrow
80
+ the read or raise `limit` when `truncated` is true.
44
81
 
45
82
  When a library's components, tokens or icons don't show, `parity lib <package> --root <project>` describes the package
46
83
  in a `.lib.par`: its components, tokens and the providers its frames need. It writes `./<id>.lib.par` in the current
@@ -54,7 +91,8 @@ keeping the designs there, so ask before running that in someone's project.
54
91
  2. **Look before you change.** Read the design with `Get("design-id", { depth: 3 })` in `execute`, and take a screenshot
55
92
  of what's there. Find the components you need with `Components()` and their props with `Props()`.
56
93
  3. **Change it in one snippet.** Build or edit the design with `Insert`, `Update`, `Replace`, `Move` and the rest. A
57
- snippet either applies whole, as one step the person can undo, or changes nothing. Name the layouts you make.
94
+ snippet either applies whole, as one step the person can undo, or changes nothing. Name the layouts you make. If it
95
+ fails because the person changed a design it changes, run it again by its `editId`.
58
96
  4. **Cover every state.** A screen that loads has a loading state, a list has an empty one, a form has an error one. Add
59
97
  them with `AddState`, as changes to the base design, not as copies.
60
98
  5. **Check every frame.** Screenshot the design in all its states and the modes it shows (`state: "all"`,
@@ -65,7 +103,15 @@ keeping the designs there, so ask before running that in someone's project.
65
103
 
66
104
  Your changes to designs are on the canvas but not saved: the person saves (Ctrl+S) or undoes them, as with their own
67
105
  edits. Don't say they're saved. Token values from `SetTokens` are the same: they show at once, and go into the project's
68
- stylesheet when the person saves.
106
+ stylesheet when the person saves. `get_status` says what's saved and what isn't, and its `revision` (also in
107
+ `get_app_state`) stays the same until something changes, so compare it before reading the board again:
108
+
109
+ ```sh
110
+ parity status --summary
111
+ # shadcn-vite is at revision 7c612c04:1.
112
+ # 1 file isn't saved. They wait for the person to save; agents can't.
113
+ # shadcn-vite.par: changed button
114
+ ```
69
115
 
70
116
  ## Rules
71
117
 
@@ -86,11 +132,12 @@ stylesheet when the person saves.
86
132
  - **Reuse what repeats.** When a row, card or list item appears more than twice, make it a component design with
87
133
  `ExtractComponent`, so each copy is an instance with its own content.
88
134
  - **Leave the canvas tidy.** New designs go into empty space (`FindEmptySpace`); `Overlaps()` tells you if boards
89
- collide.
135
+ collide, sizing boards the canvas hasn't drawn yet from their frames (`estimated: true`).
90
136
 
91
137
  ## When something fails
92
138
 
93
- `execute` returns an `editId` when a snippet throws, and nothing changes. Fix it with `editId` and `edits` (exact find
94
- and replace on the snippet) instead of sending it all again. The error names the line. Common causes: a node path that
95
- matches two designs (write it as `design/node`), a node property that doesn't exist, and a state change to something
96
- states can't change (only `hidden`, `text`, `props` and `style`).
139
+ `execute` returns an `editId` when a snippet throws, and nothing changes; `error.code` says why, and
140
+ `error.alternatives` names the valid ids when a design, state, mode or component wasn't found. Fix it with `editId` and
141
+ `edits` (exact find and replace on the snippet) instead of sending it all again. The error names the line. Common
142
+ causes: a node path that matches two designs (write it as `design/node`), a node property that doesn't exist, and a
143
+ state change to something states can't change (only `hidden`, `text`, `props` and `style`).
@@ -132,6 +132,15 @@ Type set on a layout applies to the text inside it. A component's own look comes
132
132
  A state's `overrides` map node ids to patches of `hidden`, `text`, `props` and `style`. A node a state doesn't mention
133
133
  looks as it does in the base. States win over responsive patches.
134
134
 
135
+ A patch can also take values away with `unset`, which lists prop names and style keys to drop from what's under it, so a
136
+ state can turn off a prop the base sets or leave a layout without the base's gap:
137
+
138
+ ```json
139
+ { "fields": { "style": { "padding": 4 }, "unset": { "style": ["gap"], "props": ["disabled"] } } }
140
+ ```
141
+
142
+ A key a patch sets isn't also in its `unset`. Taking away an instance's param value gives the param its default.
143
+
135
144
  ## Responsive
136
145
 
137
146
  A node's `responsive` maps a viewport option's id (such as `tablet` or `mobile`) to a patch of the same four things,
@@ -154,7 +163,8 @@ or hides the node. `ExtractComponent` sets all of this up from the occurrences i
154
163
  A **variant** is a param whose `options` each patch the component design, the way a state does: a size or a tone the
155
164
  instance chooses. Its `default` is an option's id, else the first option. An instance picks one in its `args`, and
156
165
  `view.variants` picks which one the component's own board shows. Patches apply in this order: the viewport's, the
157
- variant options', the state's, then the instance's overrides.
166
+ variant options', the state's, then the instance's overrides. `Get` returns the stored node, with the text it was given;
167
+ `Get(path, { state })` returns it as the board shows it, the param's value filled in and `bind` kept (see execute.md).
158
168
 
159
169
  ```json
160
170
  "params": {
package/skills/execute.md CHANGED
@@ -4,12 +4,30 @@
4
4
  `await` works at the top level. When the snippet finishes, every change lands on the canvas as one step the person can
5
5
  undo; when it throws, nothing changes and you get an `editId` to fix it with.
6
6
 
7
- - Pass `description`: 3 to 8 words the person sees while it runs, such as "Add a pricing section".
7
+ The person, and other agents, can change the tab while your snippet runs. Only the designs your snippet changed, created
8
+ or deleted are applied, over what's there by then, so their edits to other designs, even in the same file, stay. If a
9
+ design you changed was changed or deleted in the meantime, or the modes were and you set them, nothing is applied and
10
+ the call fails naming it:
11
+
12
+ ```text
13
+ Failed, nothing changed: Not applied: since this call read the tab, "Pricing" (app.par) was changed in the editor, and
14
+ this call changes it too. Run the call again to work from what's there now.
15
+ ```
16
+
17
+ Run it again with its `editId` and no edits (`parity exec --edit-id <id>`, or `execute` with just `editId`): the snippet
18
+ reads the tab afresh, so it works from their change. Read the design again first if your snippet depends on what it
19
+ held.
20
+
21
+ - Prefer a `description` of 3 to 8 words, such as "Add a pricing section". CLI `exec` calls it `--label`. Both default
22
+ to "Edit designs" if omitted.
8
23
  - `Print(...)` (or `console.log`) is how you see values. Objects print as JSON.
9
24
  - Values assigned without `const` or `let` carry over to your next snippet in the same tab.
10
25
  - A snippet has 5 seconds of computing time. Rendering (screenshots and checks) doesn't count against it.
11
26
  - To retry a failed snippet, send `editId` with `edits`, each an exact `{ find, replace }` on the snippet text (`all` to
12
- replace every match).
27
+ replace every match). Omitted edits default to an empty list. Do not send a new snippet with `editId`.
28
+ - Both CLI and MCP validate the same request: a nonblank snippet or `editId`, nonblank label and tab when supplied, and
29
+ patches with a nonempty `find`, string `replace` (which can be empty), and optional boolean `all`. Invalid requests
30
+ are rejected before reaching the app.
13
31
 
14
32
  ## API
15
33
 
@@ -17,14 +35,15 @@ undo; when it throws, nothing changes and you get an `editId` to fix it with.
17
35
  // Reading
18
36
  Designs(): DesignSummary[] // every design: id, name, kind, states, what it shows, size
19
37
  GetDesign(design): Design // a whole design, as in document.md
20
- Get(path, { depth?, state? }?): Node // a design's root or a node; depth cuts children off with "…"
38
+ Get(path, { depth?, state?, mode? }?): Node // a design's root or a node; depth cuts children off with "…"
21
39
  Get(path?, visit): any[] // walks nodes, collecting what visit returns
40
+ Get(path, visit, { state?, mode? }): any[] // walks nodes as a frame draws them
22
41
  Components(): string[] // the components designs can use
23
42
  Props(component, { all? }?): PropsInfo // a component's props, from its source
24
43
  Icons(): IconSet[] // the project's icon sets, with their styles
25
44
  Icons(set, { search?, style?, limit? }): Icon[] // async: a set's icons, as component nodes name them
26
45
  GetTokens({ category?, source?, search? }?): Token[]
27
- GetModes(): { settings, modes } // the project's modes, and each resolved
46
+ GetModes(design?): { settings, variables, viewports, modes, designs } // modes resolved; what each design shows
28
47
  Selection(): { design?, node?, mode? } // what the person has selected
29
48
 
30
49
  // Designs
@@ -34,7 +53,7 @@ DeleteDesign(design): void
34
53
 
35
54
  // Nodes
36
55
  Insert(parent, node, { index?, slot? }?): string // returns the new node's id
37
- Update(path, change, { state? }?): void // change: name, text, tag, hidden, props, args, style, component
56
+ Update(path, change, { state? }?): void // change: name, text, tag, hidden, props, args, style, unset, component
38
57
  Replace(path, node): string
39
58
  Move(path, parent, { index?, slot? }?): void // within one design
40
59
  Copy(path, parent, { index?, slot?, name? }?): string
@@ -82,11 +101,27 @@ canvas, the main mode and the modes beside it. Start here to find ids.
82
101
  ### Get
83
102
 
84
103
  `Get("dashboard", { depth: 2 })` returns the design's root with two levels of children; deeper ones show as `"…"`. Read
85
- a branch with a node path, as in `Get("dashboard/Stats", { depth: 2 })`. With `state`, the result shows the nodes as
86
- that state changes them.
104
+ a branch with a node path, as in `Get("dashboard/Stats", { depth: 2 })`. Without `state` or `mode`, Get returns the
105
+ nodes as stored, which is what `Update` and the other edits change.
106
+
107
+ With `state`, `mode` or both, Get returns the nodes as that frame of the board draws them: the design's variant options
108
+ (`view.variants`), its params' defaults filled into bound values, its responsive patches for the mode, then the state's
109
+ patches. `mode` is a mode id from `GetModes()`, as Screenshot takes. A state alone is read in the design's main mode,
110
+ and a mode alone in the state the design shows. A bound value comes back filled in, with its `bind` kept so you can tell
111
+ it comes from a param. An instance comes back as its own node with the args it's drawn with; read what's inside it by
112
+ its path, `design/instance/inner`, which only works with `state` or `mode`.
113
+
114
+ ```js
115
+ // A component whose title is bound to its "title" param: stored, then as the board shows it.
116
+ Print(Get("card/title").text) // "Title"
117
+ Print(Get("card/title", { state: "base" }).text) // "Card title", the param's default
118
+ // A copy of the card on a screen, inside the instance, in a state and on mobile.
119
+ Print(Get("profile/person/title", { state: "selected", mode: "mobile-light" }).text) // "Selected"
120
+ ```
87
121
 
88
122
  `Get(path, visit)` walks the nodes under `path`, or every design without one, and returns what `visit` returns for each.
89
- `visit(node, { design, path, depth, index, parent, skipChildren })`. Return `undefined` to collect nothing.
123
+ `visit(node, { design, path, depth, index, parent, skipChildren })`. Return `undefined` to collect nothing. Given a
124
+ path, `Get(path, visit, { state, mode })` visits the nodes as that frame draws them, as above.
90
125
 
91
126
  ```js
92
127
  // Every Button in the project, with where it is.
@@ -169,11 +204,14 @@ const form = Insert("settings/Content", {
169
204
 
170
205
  ### Update
171
206
 
172
- Changes a node. `change` takes `name`, `text`, `tag`, `hidden`, `props`, `args` (an instance's values), `style` and
173
- `component`. `props`, `args` and `style` merge with what's there; set a key to `null` to remove it.
207
+ Changes a node. `change` takes `name`, `text`, `tag`, `hidden`, `props`, `args` (an instance's values), `style`, `unset`
208
+ and `component`. `props`, `args` and `style` merge with what's there; set a key to `null` to remove it.
174
209
 
175
- With `{ state: "loading" }` the change belongs to that state instead of the base design. A state can change only
176
- `hidden`, `text`, `props` and `style`.
210
+ With `{ state: "loading" }` the change belongs to that state instead of the base design, and `null` takes back the
211
+ state's change to that key. A state can change only `hidden`, `text`, `props`, `style` and `unset`. To turn off in one
212
+ state what the base sets, list it in `unset`:
213
+ `Update(button, { unset: { props: ["disabled"], style: ["gap"] } }, { state: "ready" })`. In the base design, `unset`
214
+ removes the keys as `null` does.
177
215
 
178
216
  ### Replace, Move, Copy and Delete
179
217
 
@@ -239,6 +277,18 @@ ExtractComponent("team-members", rows.nodes, { name: "Member row", params: { [ro
239
277
  `SetModes(settings)` replaces the project's modes: take `GetModes().settings`, change it, and pass it back. Modes are
240
278
  shared by every design.
241
279
 
280
+ `GetModes()` also gives `viewports`, the ids `Responsive` takes, each with its width and window height; `modes`, the ids
281
+ `Screenshot` and the checks take, each with its `values` (one option per variable), width, window height, classes and
282
+ attributes; and `designs`, each design's `state` (or `states` when it shows every state), `main` mode and `beside`
283
+ modes. `GetModes("dashboard")` gives just that design, with each frame's width, fixed `height` if it has one, and
284
+ `window`.
285
+
286
+ ```js
287
+ const { viewports, designs } = GetModes("dashboard")
288
+ Print(viewports.map((viewport) => viewport.id)) // ["desktop", "tablet", "mobile"]: for Responsive
289
+ Print(designs[0].frames.map((frame) => `${frame.mode} ${frame.window.width}x${frame.window.height}`))
290
+ ```
291
+
242
292
  `SetTokens` changes token values in the project's stylesheets: `SetTokens({ primary: "oklch(0.55 0.2 260)" })` for every
243
293
  theme, or `SetTokens({ primary: { default: "…", dark: "…" } })` per theme. Every frame shows the new values at once, and
244
294
  saving writes them into the project's code, so do it only when the person asks to change the design system, not to style
@@ -250,15 +300,34 @@ one design. They're undone with the call's other changes.
250
300
  result. `state` and `mode` take an id, a list, or `"all"`; `mode` also takes `"shown"`, the modes the design shows. The
251
301
  defaults are the state on the canvas and the main mode.
252
302
 
303
+ Frames draw as the canvas does: instances of a library's component designs (from its `.lib.par`) draw that design, and
304
+ token values not yet saved show, the person's edits and this call's own `SetTokens` alike, so
305
+ `SetTokens({ primary: "red" }); await Screenshot(id)` shows the new value in the same call.
306
+
253
307
  `Measure` lays out one frame and returns its size, whether it overflows sideways, and every node's box (`clipped` when a
254
308
  parent cuts it off). `Problems` checks frames for render errors, overflow, clipping and unknown components.
255
309
  `Accessibility` returns axe-core's findings per node and frame, with the rule, its impact and what's wrong.
256
310
 
311
+ These frames are drawn in the same window as the canvas's: as wide as the frame, and as tall as the design's fixed
312
+ height for the viewport, else the viewport's screen height, else the device nearest its width. So `100vh` and window
313
+ size checks measure the same in `Measure` and `Screenshot` as on the canvas, even when the frame grows past that height.
314
+
257
315
  ## The canvas
258
316
 
259
317
  `FindEmptySpace({ width, height })` finds room for a new board, to the right of the others unless `direction: "below"`,
260
318
  or next to the design named in `near`. `Overlaps()` lists boards that collide, titles included; move one with
261
- `UpdateDesign(design, { x, y })`.
319
+ `UpdateDesign(design, { x, y })`. A board is as big as the canvas last drew it. One it hasn't drawn yet is sized from
320
+ its frames, laid out as the canvas lays them out: each shown mode's frame size (`frames` on the design, else the mode's
321
+ width), companions shrunk to 640 wide, and a frame without a fixed height as tall as its window. Those overlaps say
322
+ `estimated: true`.
323
+
324
+ ```js
325
+ // With the default modes: a 1280 main frame, the dark one beside it at half size and the 390 phone, 2505 wide in all.
326
+ const dashboard = CreateDesign({ name: "Dashboard", x: 0, y: 0 })
327
+ UpdateDesign(dashboard, { showModes: ["desktop-dark", "mobile-light"] })
328
+ CreateDesign({ name: "Settings", x: 2200, y: 0 })
329
+ Print(Overlaps(dashboard)) // Dashboard and Settings, 305 across, estimated: true
330
+ ```
262
331
 
263
332
  ## Examples
264
333
 
@@ -30,9 +30,11 @@ Modes are the project's, shared by every design. Each mode picks one option of e
30
30
  - **Theme:** a class on the frame, such as `dark`, that switches the project's tokens.
31
31
  - **Locale:** `lang` and `dir` attributes, for languages and right-to-left scripts.
32
32
 
33
- `GetModes()` lists them. A design shows its main mode and, beside it, any others you choose with
34
- `UpdateDesign(design, { showModes: ["desktop-dark", "mobile-light"] })`. Show the modes that matter for the design: a
35
- dashboard in desktop and mobile, a component in light and dark.
33
+ `get_modes` (`parity modes`) lists them, and `get_modes` with a design gives its frames: run it before a responsive
34
+ change, since the ids `Responsive` takes are viewports (`mobile`) and the ids screenshots take are modes
35
+ (`mobile-light`). `GetModes()` gives the same in a snippet. A design shows its main mode and, beside it, any others you
36
+ choose with `UpdateDesign(design, { showModes: ["desktop-dark", "mobile-light"] })`. Show the modes that matter for the
37
+ design: a dashboard in desktop and mobile, a component in light and dark.
36
38
 
37
39
  ## Viewports and responsive patches
38
40