@parity-app/cli 0.3.1 → 0.5.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/dist/bin.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { _ as errorDetails, a as describeExec, c as describeProps, f as setCaller, g as errorCodes, h as closest, i as describeConsole, l as describeStatus, m as CodedError, n as describeAccessibility, o as describeModes, p as mcpCallerName, r as describeComponents, s as describeProblems, t as call, u as describeTokens, v as readErrorDetails } from "./client-Cf66-n3R.mjs";
2
+ import { _ as errorDetails, a as describeExec, c as describeProps, f as setCaller, g as errorCodes, h as closest, i as describeConsole, l as describeStatus, m as CodedError, n as describeAccessibility, o as describeModes, p as mcpCallerName, r as describeComponents, s as describeProblems, t as call, u as describeTokens, v as readErrorDetails } from "./client-DgpoM3e7.mjs";
3
3
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
4
4
  import { dirname, join, normalize, relative, resolve, sep } from "node:path";
5
5
  import { parseArgs } from "node:util";
@@ -99,6 +99,120 @@ function structuredExec(result) {
99
99
  screenshots: result.screenshots.map(({ png: _png, ...shot }) => shot)
100
100
  };
101
101
  }
102
+ const screenshotSchema = {
103
+ type: "object",
104
+ properties: {
105
+ path: {
106
+ type: "string",
107
+ description: "Where the PNG was saved."
108
+ },
109
+ design: { type: "string" },
110
+ state: { type: "string" },
111
+ mode: { type: "string" }
112
+ },
113
+ required: [
114
+ "path",
115
+ "design",
116
+ "state",
117
+ "mode"
118
+ ]
119
+ };
120
+ const strings$1 = {
121
+ type: "array",
122
+ items: { type: "string" }
123
+ };
124
+ const discardedSchema = {
125
+ type: "array",
126
+ description: "Patches each Replace threw away with the node it swapped out. Present only when there were some; reapply what's still wanted to the replacement with Responsive, Update with a state, or UpdateState.",
127
+ items: {
128
+ type: "object",
129
+ properties: {
130
+ design: { type: "string" },
131
+ replaced: {
132
+ type: "string",
133
+ description: "The node Replace swapped out."
134
+ },
135
+ replacement: {
136
+ type: "string",
137
+ description: "The node that took its place."
138
+ },
139
+ nodes: {
140
+ type: "array",
141
+ items: {
142
+ type: "object",
143
+ properties: {
144
+ id: {
145
+ type: "string",
146
+ description: "The old node's id, or instance/inner path."
147
+ },
148
+ name: { type: "string" },
149
+ viewports: {
150
+ ...strings$1,
151
+ description: "Viewport ids it had responsive patches for."
152
+ },
153
+ states: {
154
+ ...strings$1,
155
+ description: "State ids it had patches in."
156
+ },
157
+ variants: {
158
+ ...strings$1,
159
+ description: "Variant options it had patches in, as param=option."
160
+ }
161
+ },
162
+ required: ["id"]
163
+ }
164
+ }
165
+ },
166
+ required: [
167
+ "design",
168
+ "replaced",
169
+ "replacement",
170
+ "nodes"
171
+ ]
172
+ }
173
+ };
174
+ const warningGroupsSchema = {
175
+ type: "array",
176
+ description: "The warnings as data, in the same order: each finding once, with how many places and which. Errors come first, then warnings, then suggestions.",
177
+ items: {
178
+ type: "object",
179
+ properties: {
180
+ category: {
181
+ type: "string",
182
+ description: "Stable: render_failed, unknown_component, unknown_component_design, unknown_prop, invalid_option, not_a_token, not_a_length_token, overlap, kind_changed or unnamed_layout."
183
+ },
184
+ severity: {
185
+ type: "string",
186
+ enum: [
187
+ "error",
188
+ "warning",
189
+ "suggestion"
190
+ ],
191
+ description: "error: a render failed, so a check or screenshot is missing. suggestion: worth doing, not wrong."
192
+ },
193
+ message: {
194
+ type: "string",
195
+ description: "What's wrong and what to do about it."
196
+ },
197
+ count: {
198
+ type: "integer",
199
+ minimum: 1,
200
+ description: "How many places it was found."
201
+ },
202
+ at: {
203
+ ...strings$1,
204
+ description: "Every place: a node's design/node path, or a frame. Empty for a whole board."
205
+ }
206
+ },
207
+ required: [
208
+ "category",
209
+ "severity",
210
+ "message",
211
+ "count",
212
+ "at"
213
+ ]
214
+ }
215
+ };
102
216
  /** Execute's and screenshot's structured result. */
103
217
  const execOutputSchema = {
104
218
  type: "object",
@@ -114,34 +228,19 @@ const execOutputSchema = {
114
228
  description: "Lines from Print."
115
229
  },
116
230
  warnings: {
117
- type: "array",
118
- items: { type: "string" }
231
+ ...strings$1,
232
+ description: "One line per warning group, places first."
119
233
  },
234
+ warningGroups: warningGroupsSchema,
120
235
  created: {
121
236
  type: "object",
122
237
  additionalProperties: { type: "string" },
123
238
  description: "Names of designs and nodes created, mapped to their ids."
124
239
  },
240
+ discarded: discardedSchema,
125
241
  screenshots: {
126
242
  type: "array",
127
- items: {
128
- type: "object",
129
- properties: {
130
- path: {
131
- type: "string",
132
- description: "Where the PNG was saved."
133
- },
134
- design: { type: "string" },
135
- state: { type: "string" },
136
- mode: { type: "string" }
137
- },
138
- required: [
139
- "path",
140
- "design",
141
- "state",
142
- "mode"
143
- ]
144
- }
243
+ items: screenshotSchema
145
244
  },
146
245
  error: operationErrorSchema
147
246
  },
@@ -287,8 +386,8 @@ const executeOperation = {
287
386
  */
288
387
  async function runCheck(library, options) {
289
388
  if (!library) throw new Error("parity lib check <library> [--tab t] [--mode m]");
290
- const { applyLibraryEdits, componentNode, generateId, librarySource, parseLibraryEdits, withLibraryContext } = await import("./src-DZKi9702.mjs");
291
- const { registryDir, registryEntries } = await import("./src-BGF7szJh.mjs");
389
+ const { applyLibraryEdits, componentNode, generateId, librarySource, parseLibraryEdits, withLibraryContext } = await import("./src-CqmC2-Em.mjs");
390
+ const { registryDir, registryEntries } = await import("./src-BwTWa2_Y.mjs");
292
391
  const entry = (await registryEntries()).find((item) => item.manifest.id === library);
293
392
  if (!entry) throw new Error(`The registry has no library "${library}"`);
294
393
  let manifest = entry.manifest;
@@ -416,8 +515,8 @@ function describeCheck(report) {
416
515
  * because it reads the project's files and its own copy of TypeScript.
417
516
  */
418
517
  async function runLib(packageName, options) {
419
- const { appManifestPath, generateLibrary, manifestText, mergeManifest, reviseLibrary } = await import("./src-Aoey5Uq0.mjs");
420
- const { covers, registryDir, registryEntries } = await import("./src-BGF7szJh.mjs");
518
+ const { appManifestPath, generateLibrary, manifestText, mergeManifest, reviseLibrary } = await import("./src-CpoDOrn3.mjs");
519
+ const { covers, registryDir, registryEntries } = await import("./src-BwTWa2_Y.mjs");
421
520
  const spec = packageName === void 0 ? void 0 : packageSpec(packageName);
422
521
  let root = resolve(options.root ?? ".");
423
522
  if (spec && (spec.version !== void 0 || options.registry && !options.root)) {
@@ -428,7 +527,7 @@ async function runLib(packageName, options) {
428
527
  console.error(`Installing ${spec.name} ${version} on its own…`);
429
528
  root = await installedVersion(spec.name, version);
430
529
  }
431
- const { parseLibraryEdits } = await import("./src-Aoey5Uq0.mjs");
530
+ const { parseLibraryEdits } = await import("./src-CpoDOrn3.mjs");
432
531
  const trialEdits = options.edits ? parseLibraryEdits(JSON.parse(await readFile(resolve(options.edits), "utf8"))) : void 0;
433
532
  const recordedId = options.registry && spec ? options.id ?? (await registryEntries()).find((entry) => entry.manifest.package?.name === spec.name)?.manifest.id : void 0;
434
533
  const appErrors = recordedId ? await readAppErrors(registryDir(), recordedId) : void 0;
@@ -778,6 +877,13 @@ const frameSchema = {
778
877
  state: string,
779
878
  mode: string
780
879
  };
880
+ const number = { type: "number" };
881
+ const boxSchema = object({
882
+ x: number,
883
+ y: number,
884
+ width: number,
885
+ height: number
886
+ });
781
887
  const designSchema = object({
782
888
  id: string,
783
889
  name: string,
@@ -899,9 +1005,44 @@ const problemsSchema = object({
899
1005
  type: "array",
900
1006
  items: object({
901
1007
  ...frameSchema,
1008
+ kind: {
1009
+ type: "string",
1010
+ enum: [
1011
+ "render-error",
1012
+ "unknown-component",
1013
+ "content-wider",
1014
+ "frame-overflow",
1015
+ "clipped"
1016
+ ]
1017
+ },
902
1018
  problem: string,
903
- node: string
904
- }, ["node"])
1019
+ node: string,
1020
+ name: string,
1021
+ box: boxSchema,
1022
+ container: object({
1023
+ node: string,
1024
+ name: string,
1025
+ box: boxSchema
1026
+ }, ["node", "name"]),
1027
+ overflow: object({
1028
+ left: number,
1029
+ right: number,
1030
+ top: number,
1031
+ bottom: number
1032
+ }, [
1033
+ "left",
1034
+ "right",
1035
+ "top",
1036
+ "bottom"
1037
+ ])
1038
+ }, [
1039
+ "kind",
1040
+ "node",
1041
+ "name",
1042
+ "box",
1043
+ "container",
1044
+ "overflow"
1045
+ ])
905
1046
  },
906
1047
  frames: integer,
907
1048
  warnings: strings
@@ -1002,8 +1143,86 @@ const propsSchema = object({
1002
1143
  "default",
1003
1144
  "required"
1004
1145
  ])
1005
- }
1006
- }, ["path", "import"]);
1146
+ },
1147
+ composition: object({
1148
+ role: {
1149
+ type: "string",
1150
+ enum: [
1151
+ "provider",
1152
+ "wrapper",
1153
+ "component"
1154
+ ],
1155
+ description: "provider draws nothing and gives parts inside it context; wrapper draws and holds other parts."
1156
+ },
1157
+ parents: object({
1158
+ status: {
1159
+ type: "string",
1160
+ enum: [
1161
+ "listed",
1162
+ "none",
1163
+ "unknown"
1164
+ ],
1165
+ description: "listed: insert it inside chain; none: it needs nothing; unknown: nothing says, see reason."
1166
+ },
1167
+ chain: {
1168
+ type: "array",
1169
+ description: "Outermost first.",
1170
+ items: object({
1171
+ component: string,
1172
+ optional: boolean_
1173
+ }, ["optional"])
1174
+ },
1175
+ from: {
1176
+ ...string,
1177
+ description: "What said so, such as the library's examples or the error it throws."
1178
+ },
1179
+ reason: string
1180
+ }, [
1181
+ "chain",
1182
+ "from",
1183
+ "reason"
1184
+ ]),
1185
+ holds: {
1186
+ ...strings,
1187
+ description: "Parts that go directly inside it."
1188
+ },
1189
+ slots: {
1190
+ type: "array",
1191
+ items: object({
1192
+ name: string,
1193
+ key: string,
1194
+ kind: {
1195
+ type: "string",
1196
+ enum: [
1197
+ "children",
1198
+ "element",
1199
+ "render"
1200
+ ]
1201
+ }
1202
+ })
1203
+ },
1204
+ example: object({
1205
+ node: {
1206
+ type: "object",
1207
+ description: "A node Insert takes as it is: the component inside its parents."
1208
+ },
1209
+ status: {
1210
+ type: "string",
1211
+ enum: [
1212
+ "drawn",
1213
+ "failing",
1214
+ "untried"
1215
+ ],
1216
+ description: "Whether the component has drawn on the canvas."
1217
+ },
1218
+ reason: string
1219
+ }, ["reason"])
1220
+ })
1221
+ }, [
1222
+ "path",
1223
+ "import",
1224
+ "composition"
1225
+ ]);
1007
1226
  const viewportSchema = object({
1008
1227
  id: string,
1009
1228
  name: string,
@@ -1334,13 +1553,13 @@ const tools = [
1334
1553
  }
1335
1554
  },
1336
1555
  operationTool("screenshot", "Capture a design in any of its states and modes, rendered offscreen at real widths. state and mode take an id, a list, or \"all\"; mode also takes \"shown\".", operations.screenshot, (result) => [text(describeExec(result)), ...result.screenshots.map((shot) => image(shot.png))]),
1337
- operationTool("find_problems", "Find layout problems in a design, rendered offscreen in every state and the modes it shows unless you narrow it: render errors, content wider than the frame, nodes cut off by a parent that clips, and components that aren't the project's. Run it after changes, with screenshot. state and mode take an id, a list, or \"all\"; mode also takes \"shown\".", operations.problems, (result) => [text(describeProblems(result))]),
1338
- operationTool("check_accessibility", "Check a design's accessibility with axe-core against WCAG 2.2 AA, rendered offscreen in every state and the modes it shows unless you narrow it. Lists each rule broken, worst first, with the layers that break it (ids you can pass to execute), the frames, what's wrong and a link on how to fix it. state and mode take an id, a list, or \"all\"; mode also takes \"shown\". Automated checks find some issues, not all.", operations.accessibility, (result) => [text(describeAccessibility(result))]),
1556
+ operationTool("find_problems", "Find layout problems in a design, rendered offscreen in every state and the modes it shows unless you narrow it: render errors, nodes that stick out of the frame, nodes cut off by an ancestor that clips, and components that aren't the project's. Each layout finding names the node and what it overflows, with both boxes and how far past each edge. Run it after changes, with screenshot. state and mode take an id, a list, or \"all\"; mode also takes \"shown\".", operations.problems, (result) => [text(describeProblems(result))]),
1557
+ operationTool("check_accessibility", "Check a design's accessibility with axe-core against WCAG 2.2 AA, rendered offscreen in every state and the modes it shows unless you narrow it. Lists each rule broken, worst first, with the layers that break it (ids you can pass to execute), the frames, what's wrong and a link on how to fix it. A contrast failure names the color token to set as style.foreground so it passes in every frame checked, when one does. state and mode take an id, a list, or \"all\"; mode also takes \"shown\". Automated checks find some issues, not all.", operations.accessibility, (result) => [text(describeAccessibility(result))]),
1339
1558
  operationTool("get_console", "Read what the project's own code wrote to the console: errors, warnings and logs from its components on the canvas, including while the person interacts with them, and from offscreen renders such as screenshots. Oldest first; the same message several times in a row is counted once. clear: true empties it after reading, so the next read shows only what's new.", operations.console, (result) => [text(describeConsole(result))]),
1340
1559
  operationTool("open_project", "Open a project folder in a new Parity tab, or switch to it if it's already open.", operations.open, json),
1341
1560
  operationTool("get_tokens", "Read tokens filtered by category, source or name, with values in every theme.", operations.tokens, (result) => [text(describeTokens(result))]),
1342
1561
  operationTool("get_components", "List project components, libraries, icon sets and exports that were left out or failed to render.", operations.components, (result) => [text(describeComponents(result))]),
1343
- operationTool("get_props", "Read a component's props from source. all includes attributes it passes through.", operations.props, (result) => [text(describeProps(result))]),
1562
+ operationTool("get_props", "Read a component's props from source, and its composition: the parents or provider it goes inside, the parts it holds, its slots and an example node to insert. all includes attributes it passes through.", operations.props, (result) => [text(describeProps(result))]),
1344
1563
  {
1345
1564
  name: "read_skill",
1346
1565
  description: "Read the Parity design skill or a file it references.",
@@ -1465,7 +1684,8 @@ const usage = `parity: drive the Parity app from a terminal or an agent
1465
1684
  unless --limit says
1466
1685
  parity components [--tab t] the components designs can use, by library
1467
1686
  parity props <Component> [--all] [--tab t]
1468
- a component's props from its source; --all adds the attributes it passes on
1687
+ a component's props from its source, then what it goes inside and holds,
1688
+ with an example to insert; --all adds the attributes it passes on
1469
1689
  parity lib [package] [--root dir] [--out file|-] [--registry] [--id id] [--name n] [--range r] [--json]
1470
1690
  [--only new|all|A,B] [--edits file] [--no-examples] [--no-tags]
1471
1691
  write a .lib.par: a package's components, tokens, breakpoints, icons and
@@ -468,10 +468,45 @@ function describeExec(result) {
468
468
  if (result.output.length > 0) lines.push("", "Output:", ...result.output);
469
469
  const created = Object.entries(result.created);
470
470
  if (created.length > 0) lines.push("", "Created:", ...created.map(([name, id]) => ` ${name} = ${id}`));
471
- if (result.warnings.length > 0) lines.push("", "Warnings (fix these next):", ...result.warnings.map((warning) => ` ${warning}`));
471
+ lines.push(...describeWarnings(result));
472
+ if (result.discarded?.length) {
473
+ lines.push("", "Discarded by Replace (reapply what you still want to the new node):");
474
+ for (const { design, replaced, replacement, nodes } of result.discarded) {
475
+ lines.push(` ${design}/${replaced}, now ${replacement}:`);
476
+ for (const node of nodes) {
477
+ const where = [
478
+ node.viewports && `viewports ${node.viewports.join(", ")}`,
479
+ node.states && `states ${node.states.join(", ")}`,
480
+ node.variants && `variants ${node.variants.join(", ")}`
481
+ ].filter(Boolean);
482
+ lines.push(` ${node.name ? `${node.name} (${node.id})` : node.id}: ${where.join("; ")}`);
483
+ }
484
+ }
485
+ }
472
486
  if (result.screenshots.length > 0) lines.push("", "Screenshots:", ...result.screenshots.map((shot) => ` ${shot.design} / ${shot.state} / ${shot.mode}: ${shot.path}`));
473
487
  return lines.join("\n");
474
488
  }
489
+ const warningHeadings = {
490
+ error: "Render failures (these frames weren't checked):",
491
+ warning: "Warnings (fix these next):",
492
+ suggestion: "Suggestions:"
493
+ };
494
+ /**
495
+ * The warnings under a heading for each severity, so a failed render doesn't read like a naming tip. The lines are
496
+ * the app's, one per group in the groups' order; an app older than PAR-460 sends no groups, so they're all warnings.
497
+ */
498
+ function describeWarnings(result) {
499
+ const lines = [];
500
+ for (const severity of [
501
+ "error",
502
+ "warning",
503
+ "suggestion"
504
+ ]) {
505
+ const matching = result.warnings.filter((_, index) => (result.warningGroups?.[index]?.severity ?? "warning") === severity);
506
+ if (matching.length > 0) lines.push("", warningHeadings[severity], ...matching.map((warning) => ` ${warning}`));
507
+ }
508
+ return lines;
509
+ }
475
510
  /** Where a tab's edits stand: the revision, what's applied and undoable, what's on disk, and what failed to write. */
476
511
  function describeStatus(result) {
477
512
  const { tab, revision, unsaved, undo, redo, save, errors } = result;
@@ -563,6 +598,7 @@ function describeAccessibility(result) {
563
598
  name: finding.name ?? "The design",
564
599
  nodes: finding.node ? [finding.node] : [],
565
600
  detail: finding.detail,
601
+ ...fixLine(finding) ? { fix: fixLine(finding) } : {},
566
602
  frames: []
567
603
  };
568
604
  layer.frames.push({
@@ -586,6 +622,7 @@ function describeAccessibility(result) {
586
622
  const where = layer.frames.length === frames && frames > 1 ? "every frame" : frameList(layer.frames);
587
623
  const ids = layer.nodes.length > 0 ? ` (${layer.nodes.join(", ")})` : "";
588
624
  lines.push(` ${layer.name}${ids}, in ${where}: ${layer.detail}`);
625
+ if (layer.fix) lines.push(` ${layer.fix}`);
589
626
  }
590
627
  lines.push(` How to fix it: ${helpUrl}`);
591
628
  }
@@ -593,12 +630,19 @@ function describeAccessibility(result) {
593
630
  lines.push("", "Automated checks find some issues, not all. Try the design with a keyboard and a screen reader too.");
594
631
  return lines.join("\n");
595
632
  }
633
+ /** A contrast failure's fix in a sentence, naming the style to set so it can be passed to execute. */
634
+ function fixLine(finding) {
635
+ const fix = finding.contrast?.fix;
636
+ if (fix === "none") return "Fix: none of the project's color tokens pass on this background; change the background.";
637
+ return fix && `Fix: set style.foreground to "${fix.token}", which reaches ${fix.ratio}:1 in every frame checked.`;
638
+ }
596
639
  /** A readable account of the layout problems in a design's frames, each once with the frames it's in. */
597
640
  function describeProblems(result) {
598
641
  const { design, problems, frames } = result;
599
642
  const checked = `${design.name} (${design.id}), ${frames === 1 ? "1 frame" : `${frames} frames`}`;
600
643
  const grouped = /* @__PURE__ */ new Map();
601
- for (const { problem, node, state, mode } of problems) {
644
+ for (const { problem, node: id, name, state, mode } of problems) {
645
+ const node = id && name ? `${name} (${id})` : id;
602
646
  const key = `${node ?? ""}\n${problem}`;
603
647
  const entry = grouped.get(key) ?? {
604
648
  problem,
@@ -671,19 +715,47 @@ function describeComponents({ components, iconLibraries = [], leftOut = [], draw
671
715
  }
672
716
  return lines.join("\n").trimEnd();
673
717
  }
674
- /** A component's props, with each variant's options and default. */
718
+ /** A component's props, with each variant's options and default, then what it needs around it and holds. */
675
719
  function describeProps(result) {
676
- if (!result.known) return `${result.component}'s props haven't been read from its source yet. Try again in a moment.`;
677
- const imported = result.import && result.import !== result.path ? `, imported from "${result.import}"` : "";
678
- const lines = [`${result.component}, from ${result.path}${imported}:`];
679
- for (const prop of result.props) {
680
- const options = prop.options ? `: ${prop.options.map((option) => JSON.stringify(option)).join(", ")}` : "";
681
- const fallback = prop.default === void 0 ? "" : `, default ${JSON.stringify(prop.default)}`;
682
- lines.push(` ${prop.name} (${prop.kind}${prop.required ? ", required" : ""})${options}${fallback}`);
683
- }
684
- if (result.props.length === 0) lines.push(" No props of its own. --all lists the attributes it passes on.");
720
+ const lines = [];
721
+ if (!result.known) lines.push(`${result.component}'s props haven't been read from its source yet. Try again in a moment.`);
722
+ else {
723
+ const imported = result.import && result.import !== result.path ? `, imported from "${result.import}"` : "";
724
+ lines.push(`${result.component}, from ${result.path}${imported}:`);
725
+ for (const prop of result.props) {
726
+ const options = prop.options ? `: ${prop.options.map((option) => JSON.stringify(option)).join(", ")}` : "";
727
+ const fallback = prop.default === void 0 ? "" : `, default ${JSON.stringify(prop.default)}`;
728
+ lines.push(` ${prop.name} (${prop.kind}${prop.required ? ", required" : ""})${options}${fallback}`);
729
+ }
730
+ if (result.props.length === 0) lines.push(" No props of its own. --all lists the attributes it passes on.");
731
+ }
732
+ if (result.composition) lines.push("", ...describeComposition(result.composition));
685
733
  return lines.join("\n");
686
734
  }
735
+ const roles = {
736
+ provider: "a provider, which draws nothing and gives the parts inside it their context",
737
+ wrapper: "a wrapper, which other parts go inside",
738
+ component: "a component"
739
+ };
740
+ const exampleStatus = {
741
+ drawn: "drawn on the canvas",
742
+ failing: "failing on the canvas",
743
+ untried: "not drawn yet"
744
+ };
745
+ /** What a component needs around it, what it holds, its slots, and its example as JSON for `Insert`. */
746
+ function describeComposition({ role, parents, holds, slots, example }) {
747
+ const lines = [`It's ${roles[role]}.`];
748
+ if (parents.status === "listed") {
749
+ const chain = parents.chain.map((parent) => parent.optional ? `${parent.component} (optional)` : parent.component);
750
+ lines.push(`Goes inside, outermost first: ${chain.join(" > ")}${parents.from ? ` (from its ${parents.from})` : ""}`);
751
+ } else if (parents.status === "none") lines.push("Needs nothing around it.");
752
+ else lines.push(`What it needs around it is unknown: ${parents.reason}`);
753
+ if (holds.length > 0) lines.push(`Holds: ${holds.join(", ")}`);
754
+ if (slots.length > 0) lines.push(`Slots: ${slots.map((slot) => `${slot.key} (${slot.kind})`).join(", ")}`);
755
+ const reason = example.reason ? `: ${example.reason}` : "";
756
+ lines.push(`Example for Insert, ${exampleStatus[example.status]}${reason}:`, ` ${JSON.stringify(example.node)}`);
757
+ return lines;
758
+ }
687
759
  /** Frames as state and mode ids, naming a mode once when they share it: "default, error in desktop-light". */
688
760
  function frameList(frames) {
689
761
  const modes = [...new Set(frames.map((frame) => frame.mode))];
package/dist/index.d.mts CHANGED
@@ -63,15 +63,42 @@ interface Screenshot {
63
63
  state: string;
64
64
  mode: string;
65
65
  }
66
+ /** What one `Replace` threw away: the patches keyed to the old node and what was inside it (PAR-455). */
67
+ interface Discarded {
68
+ design: string;
69
+ replaced: string;
70
+ replacement: string;
71
+ nodes: {
72
+ id: string;
73
+ name?: string;
74
+ viewports?: string[];
75
+ states?: string[];
76
+ variants?: string[];
77
+ }[];
78
+ }
79
+ /**
80
+ * Findings with the same category and message, once, with every place they were found (PAR-460). The app's
81
+ * `warnings` are these as lines, in the same order.
82
+ */
83
+ interface WarningGroup {
84
+ category: string;
85
+ severity: "error" | "warning" | "suggestion";
86
+ message: string;
87
+ count: number;
88
+ at: string[];
89
+ }
66
90
  /** The canonical result. Structured output is this without the screenshots' image data. */
67
91
  interface ExecResponse {
68
92
  ok: boolean;
69
93
  applied: boolean;
70
94
  output: string[];
71
95
  warnings: string[];
96
+ warningGroups?: WarningGroup[];
72
97
  /** Set when the call failed, with the editId to retry a snippet by. */
73
98
  error?: OperationError;
74
99
  created: Record<string, string>;
100
+ /** Set when a Replace threw away responsive, state or variant patches. */
101
+ discarded?: Discarded[];
75
102
  screenshots: Screenshot[];
76
103
  }
77
104
  /**
@@ -225,6 +252,13 @@ export interface AccessibilityFinding {
225
252
  node?: string;
226
253
  name?: string;
227
254
  detail: string;
255
+ /** For a contrast failure: the token to color the layer's text with, or `"none"` when none of the project's pass. */
256
+ contrast?: {
257
+ fix?: {
258
+ token: string;
259
+ ratio: number;
260
+ } | "none";
261
+ };
228
262
  }
229
263
  export interface AccessibilityResponse {
230
264
  tab: {
@@ -253,15 +287,41 @@ export interface ProblemsResponse {
253
287
  id: string;
254
288
  name: string;
255
289
  };
256
- problems: {
257
- state: string;
258
- mode: string;
259
- problem: string;
260
- node?: string;
261
- }[];
290
+ problems: Problem[];
262
291
  frames: number;
263
292
  warnings: string[];
264
293
  }
294
+ /** A box in a frame, in CSS pixels from its top left corner. */
295
+ export interface FrameBox {
296
+ x: number;
297
+ y: number;
298
+ width: number;
299
+ height: number;
300
+ }
301
+ /**
302
+ * One problem in one frame. Layout problems (`frame-overflow`, `clipped`) name the node, what it overflows (the node
303
+ * that clips it, or the frame, which has none), both boxes, and how far past each edge it reaches.
304
+ */
305
+ export interface Problem {
306
+ state: string;
307
+ mode: string;
308
+ kind?: "render-error" | "unknown-component" | "content-wider" | "frame-overflow" | "clipped";
309
+ problem: string;
310
+ node?: string;
311
+ name?: string;
312
+ box?: FrameBox;
313
+ container?: {
314
+ node?: string;
315
+ name?: string;
316
+ box: FrameBox;
317
+ };
318
+ overflow?: {
319
+ left?: number;
320
+ right?: number;
321
+ top?: number;
322
+ bottom?: number;
323
+ };
324
+ }
265
325
  /** A readable account of the layout problems in a design's frames, each once with the frames it's in. */
266
326
  export declare function describeProblems(result: ProblemsResponse): string;
267
327
  export interface TokenInfo {
@@ -325,8 +385,38 @@ export interface PropsResponse {
325
385
  default?: string | number | boolean | null;
326
386
  required?: boolean;
327
387
  }[];
388
+ /** What it needs around it and holds inside it, read from its library's metadata (PAR-454). */
389
+ composition?: Composition;
390
+ }
391
+ /** A component's composition, as `@parity-app/editor`'s `componentComposition` reads it. */
392
+ export interface Composition {
393
+ role: "provider" | "wrapper" | "component";
394
+ parents: {
395
+ status: "listed";
396
+ chain: {
397
+ component: string;
398
+ optional?: true;
399
+ }[];
400
+ from?: string;
401
+ } | {
402
+ status: "none";
403
+ } | {
404
+ status: "unknown";
405
+ reason: string;
406
+ };
407
+ holds: string[];
408
+ slots: {
409
+ name: string;
410
+ key: string;
411
+ kind: "children" | "element" | "render";
412
+ }[];
413
+ example: {
414
+ node: unknown;
415
+ status: "drawn" | "failing" | "untried";
416
+ reason?: string;
417
+ };
328
418
  }
329
- /** A component's props, with each variant's options and default. */
419
+ /** A component's props, with each variant's options and default, then what it needs around it and holds. */
330
420
  export declare function describeProps(result: PropsResponse): string;
331
421
  export interface ConsoleEntry {
332
422
  level: "info" | "warning" | "error" | "debug";
package/dist/index.mjs CHANGED
@@ -1 +1 @@
1
- export { t as call, n as describeAccessibility, r as describeComponents, i as describeConsole, a as describeExec, o as describeModes, s as describeProblems, c as describeProps, l as describeStatus, u as describeTokens, d as registry, f as setCaller } from "./client-Cf66-n3R.mjs";
1
+ export { t as call, n as describeAccessibility, r as describeComponents, i as describeConsole, a as describeExec, o as describeModes, s as describeProblems, c as describeProps, l as describeStatus, u as describeTokens, d as registry, f as setCaller } from "./client-DgpoM3e7.mjs";
@@ -254,6 +254,58 @@ function generateId(taken, length = 5) {
254
254
  }
255
255
  }
256
256
  // ../core/src/document.ts
257
+ const canvasObjectKinds = [
258
+ "table",
259
+ "operation",
260
+ "route"
261
+ ];
262
+ const linkEndKinds = [
263
+ "screen",
264
+ "component",
265
+ ...canvasObjectKinds
266
+ ];
267
+ /** Each type of link, and the kinds it may join, from and to. */
268
+ const linkTypes = {
269
+ /** A screen shown at a route's URL. */
270
+ shows: {
271
+ from: ["screen"],
272
+ to: ["route"]
273
+ },
274
+ /** A screen, route or component that calls an operation. */
275
+ calls: {
276
+ from: [
277
+ "screen",
278
+ "route",
279
+ "component"
280
+ ],
281
+ to: ["operation"]
282
+ },
283
+ /** An operation that reads a table. */
284
+ reads: {
285
+ from: ["operation"],
286
+ to: ["table"]
287
+ },
288
+ /** An operation that writes to a table. */
289
+ writes: {
290
+ from: ["operation"],
291
+ to: ["table"]
292
+ }
293
+ };
294
+ /** Whether a link of this type may join these two kinds. */
295
+ function linkAllowed(type, from, to) {
296
+ const allowed = linkTypes[type];
297
+ return allowed.from.includes(from) && allowed.to.includes(to);
298
+ }
299
+ const lenses = [
300
+ "data",
301
+ "api",
302
+ "routes"
303
+ ];
304
+ const containerKinds = [
305
+ "screen",
306
+ "component",
307
+ "frame"
308
+ ];
257
309
  const textTags = [
258
310
  "p",
259
311
  "span",
@@ -345,16 +397,97 @@ function parseCanvasDocument(value) {
345
397
  "containers",
346
398
  "notes",
347
399
  "modes",
348
- "detection"
400
+ "detection",
401
+ "objects",
402
+ "links",
403
+ "sources"
349
404
  ], "");
350
405
  return compact({
351
406
  version: 3,
352
407
  containers: expectArray(document.containers, "/containers").map((item, index) => parseContainer(item, `/containers/${index}`)),
353
408
  modes: optional(document.modes, (value) => parseModeSettings(value)),
354
409
  notes: optional(document.notes, (value) => expectArray(value, "/notes").map((item, index) => parseNote(item, `/notes/${index}`))),
355
- detection: optional(document.detection, (value) => parseDetection(value, "/detection"))
410
+ detection: optional(document.detection, (value) => parseDetection(value, "/detection")),
411
+ objects: optional(document.objects, (value) => parseObjects(value, "/objects")),
412
+ links: optional(document.links, (value) => parseLinks(value, "/links")),
413
+ sources: optional(document.sources, (value) => parseSources(value, "/sources"))
414
+ });
415
+ }
416
+ function parsePosition(value, path) {
417
+ const position = expectObject(value, path);
418
+ expectKnownKeys(position, ["x", "y"], path);
419
+ return {
420
+ x: expectNumber(position.x, `${path}/x`),
421
+ y: expectNumber(position.y, `${path}/y`)
422
+ };
423
+ }
424
+ function parseObjects(value, path) {
425
+ const placed = /* @__PURE__ */ new Set();
426
+ return expectArray(value, path).map((item, index) => {
427
+ const at = `${path}/${index}`;
428
+ const object = expectObject(item, at);
429
+ expectKnownKeys(object, [
430
+ "kind",
431
+ "ref",
432
+ "position"
433
+ ], at);
434
+ const kind = expectOneOf(object.kind, canvasObjectKinds, `${at}/kind`);
435
+ const ref = expectString(object.ref, `${at}/ref`);
436
+ const key = JSON.stringify([kind, ref]);
437
+ if (placed.has(key)) throw new TypeError(`${at}: the ${kind} "${ref}" is already on the canvas`);
438
+ placed.add(key);
439
+ return {
440
+ kind,
441
+ ref,
442
+ position: parsePosition(object.position, `${at}/position`)
443
+ };
356
444
  });
357
445
  }
446
+ function parseLinkEnd(value, path) {
447
+ const end = expectObject(value, path);
448
+ expectKnownKeys(end, ["kind", "ref"], path);
449
+ return {
450
+ kind: expectOneOf(end.kind, linkEndKinds, `${path}/kind`),
451
+ ref: expectString(end.ref, `${path}/ref`)
452
+ };
453
+ }
454
+ function parseLinks(value, path) {
455
+ const ids = /* @__PURE__ */ new Set();
456
+ const types = Object.keys(linkTypes);
457
+ return expectArray(value, path).map((item, index) => {
458
+ const at = `${path}/${index}`;
459
+ const link = expectObject(item, at);
460
+ expectKnownKeys(link, [
461
+ "id",
462
+ "type",
463
+ "from",
464
+ "to"
465
+ ], at);
466
+ const id = expectString(link.id, `${at}/id`);
467
+ if (ids.has(id)) throw new TypeError(`${at}/id: duplicate link id "${id}"`);
468
+ ids.add(id);
469
+ const type = expectOneOf(link.type, types, `${at}/type`);
470
+ const from = parseLinkEnd(link.from, `${at}/from`);
471
+ const to = parseLinkEnd(link.to, `${at}/to`);
472
+ if (!linkAllowed(type, from.kind, to.kind)) {
473
+ const { from: froms, to: tos } = linkTypes[type];
474
+ throw new TypeError(`${at}: a ${type} link goes from ${froms.join(" or ")} to ${tos.join(" or ")}, not ${from.kind} to ${to.kind}`);
475
+ }
476
+ return {
477
+ id,
478
+ type,
479
+ from,
480
+ to
481
+ };
482
+ });
483
+ }
484
+ function parseSources(value, path) {
485
+ const sources = expectObject(value, path);
486
+ expectKnownKeys(sources, lenses, path);
487
+ const chosen = {};
488
+ for (const lens of lenses) if (sources[lens] !== void 0) chosen[lens] = expectString(sources[lens], `${path}/${lens}`);
489
+ return chosen;
490
+ }
358
491
  function parseDetection(value, path) {
359
492
  const detection = expectObject(value, path);
360
493
  expectKnownKeys(detection, ["include", "exclude"], path);
@@ -399,7 +532,8 @@ function parseContainer(value, path) {
399
532
  "view",
400
533
  "params",
401
534
  "context",
402
- "frames"
535
+ "frames",
536
+ "size"
403
537
  ], path);
404
538
  const position = expectObject(container.position, `${path}/position`);
405
539
  const view = optional(container.view, (v) => {
@@ -440,10 +574,13 @@ function parseContainer(value, path) {
440
574
  };
441
575
  });
442
576
  if (states.length === 0) throw new TypeError(`${path}/states: expected at least one state`);
577
+ const kind = optional(container.kind, (v) => expectOneOf(v, containerKinds, `${path}/kind`));
578
+ if (kind === "frame") checkFrame(container, root, states, path);
579
+ else if (container.size !== void 0) throw new TypeError(`${path}/size: only a frame has a size of its own; size a ${kind ?? "screen"} in frames`);
443
580
  return compact({
444
581
  id: expectString(container.id, `${path}/id`),
445
582
  name: expectString(container.name, `${path}/name`),
446
- kind: optional(container.kind, (v) => expectOneOf(v, ["screen", "component"], `${path}/kind`)),
583
+ kind,
447
584
  context: optional(container.context, (v) => expectArray(v, `${path}/context`).map((item, index) => {
448
585
  const at = `${path}/context/${index}`;
449
586
  const wrapper = expectObject(item, at);
@@ -498,17 +635,10 @@ function parseContainer(value, path) {
498
635
  }),
499
636
  frames: optional(container.frames, (v) => {
500
637
  const frames = {};
501
- for (const [viewport, entry] of Object.entries(expectObject(v, `${path}/frames`))) {
502
- const at = `${path}/frames/${viewport}`;
503
- const size = expectObject(entry, at);
504
- expectKnownKeys(size, ["width", "height"], at);
505
- frames[viewport] = compact({
506
- width: optional(size.width, (w) => expectNumber(w, `${at}/width`)),
507
- height: optional(size.height, (h) => expectNumber(h, `${at}/height`))
508
- });
509
- }
638
+ for (const [viewport, entry] of Object.entries(expectObject(v, `${path}/frames`))) frames[viewport] = parseFrameSize(entry, `${path}/frames/${viewport}`);
510
639
  return frames;
511
640
  }),
641
+ size: optional(container.size, (v) => parseFrameSize(v, `${path}/size`)),
512
642
  position: {
513
643
  x: expectNumber(position.x, `${path}/position/x`),
514
644
  y: expectNumber(position.y, `${path}/position/y`)
@@ -518,6 +648,26 @@ function parseContainer(value, path) {
518
648
  view
519
649
  });
520
650
  }
651
+ function parseFrameSize(value, path) {
652
+ const size = expectObject(value, path);
653
+ expectKnownKeys(size, ["width", "height"], path);
654
+ return compact({
655
+ width: optional(size.width, (w) => expectNumber(w, `${path}/width`)),
656
+ height: optional(size.height, (h) => expectNumber(h, `${path}/height`))
657
+ });
658
+ }
659
+ /**
660
+ * What a frame may hold: a layout at its root and one state, drawn as its layers are, with no params, no view (it
661
+ * shows only the primary mode and that state) and no sizes per viewport, since it has one of its own.
662
+ */
663
+ function checkFrame(container, root, states, path) {
664
+ if (root.type !== "layout") throw new TypeError(`${path}/root: a frame's root is a layout`);
665
+ if (states.length > 1) throw new TypeError(`${path}/states: a frame has one state`);
666
+ if (Object.keys(states[0].overrides ?? {}).length > 0) throw new TypeError(`${path}/states/0/overrides: a frame is drawn as its layers are`);
667
+ if (container.params !== void 0) throw new TypeError(`${path}/params: a frame has no params`);
668
+ if (container.view !== void 0) throw new TypeError(`${path}/view: a frame shows only the primary mode and its one state`);
669
+ if (container.frames !== void 0) throw new TypeError(`${path}/frames: a frame's size is in size, not per viewport`);
670
+ }
521
671
  function parseNode(value, path, ids) {
522
672
  const node = expectObject(value, path);
523
673
  const type = expectOneOf(node.type, [
@@ -1364,6 +1514,7 @@ function librarySource(library, name, component, path) {
1364
1514
  library,
1365
1515
  ...component.group ? { group: component.group } : {},
1366
1516
  ...component.parents ? { parents: component.parents.map((parent) => `${library}/${parent}`) } : {},
1517
+ ...component.parents && component.parentsFrom ? { parentsFrom: component.parentsFrom } : {},
1367
1518
  ...component.internal ? { internal: true } : {},
1368
1519
  ...component.visual === false ? { visual: false } : {},
1369
1520
  ...component.optionalParents ? { optionalParents: component.optionalParents.map((parent) => `${library}/${parent}`) } : {},
@@ -1403,4 +1554,4 @@ function sampleIds(sample, library) {
1403
1554
  ...sample.children ? { children: sample.children.map(child) } : {}
1404
1555
  };
1405
1556
  }
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 };
1557
+ export { deviceKinds as A, parseCanvasDocument as C, collectCustomProperties as D, parseModeSettings as E, viewportHeightProperty as O, paramTypes as S, generateId as T, imageFits as _, applyLibraryEdits as a, linkEndKinds as b, isSampleElement as c, parentSignals as d, parseLibraryEdits as f, containerKinds as g, canvasObjectKinds as h, withLibraryContext as i, lengthToPixels as j, viewportWidthProperty as k, 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, lenses as v, styleKeys as w, linkTypes as x, linkAllowed as y };
@@ -1 +1 @@
1
- export { t as covers, n as registryDir, r as registryEntries } from "./src-eso2QXtH.mjs";
1
+ export { t as covers, n as registryDir, r as registryEntries } from "./src-D85OVBQL.mjs";
@@ -1,5 +1,5 @@
1
- import { T as lengthToPixels, a as applyLibraryEdits, c as isSampleElement, f as parseLibraryEdits, l as isSampleRender, m as renderSlotOf, p as parseLibraryManifest, s as isMadeValue, u as libraryManifestExtension, x as collectCustomProperties } from "./src-lVLSuOgh.mjs";
2
- import "./src-eso2QXtH.mjs";
1
+ import { D as collectCustomProperties, a as applyLibraryEdits, c as isSampleElement, f as parseLibraryEdits, j as lengthToPixels, l as isSampleRender, m as renderSlotOf, p as parseLibraryManifest, s as isMadeValue, u as libraryManifestExtension } from "./src-BryimTPw.mjs";
2
+ import "./src-D85OVBQL.mjs";
3
3
  import { a as publishedVersions, n as packageFile, o as repositoryFiles, r as publishedSource } from "./published-CdXoUgfD.mjs";
4
4
  import { createRequire } from "node:module";
5
5
  import { mkdir, readFile, readdir, realpath, rm, stat, writeFile } from "node:fs/promises";
@@ -601,11 +601,12 @@ function listTheme(selector) {
601
601
  const [only] = names;
602
602
  return names.size === 1 ? only : void 0;
603
603
  }
604
- const simpleSelector = /^(?:(?<root>html|:root|:host)(?![\w(-])|\.(?<className>[\w-]+)|\[\s*[\w-]+\s*(?:=\s*(?:"(?<double>[^"]*)"|'(?<single>[^']*)'|(?<bare>[\w-]+))\s*(?:[is]\s*)?)?\]|(?<functional>:(?:not|where|is|host)\())/;
604
+ const simpleSelector = /^(?:(?<root>html|:root|:host)(?![\w(-])|\.(?<className>[\w-]+)|\[\s*(?<attribute>[\w-]+)\s*(?:=\s*(?:"(?<double>[^"]*)"|'(?<single>[^']*)'|(?<bare>[\w-]+))\s*(?:[is]\s*)?)?\]|(?<functional>:(?:not|where|is|host)\())/;
605
605
  /** The theme one compound selector (no combinators) names, or undefined when it isn't a theme's. */
606
606
  function compoundTheme(part) {
607
607
  let root = false;
608
608
  const names = [];
609
+ let attribute;
609
610
  let rest = part;
610
611
  while (rest.length > 0) {
611
612
  const match = simpleSelector.exec(rest);
@@ -623,11 +624,14 @@ function compoundTheme(part) {
623
624
  if (!nested) return void 0;
624
625
  root ||= nested.root;
625
626
  if (nested.name !== void 0) names.push(nested.name);
627
+ attribute ??= nested.attribute;
626
628
  continue;
627
629
  }
628
630
  rest = rest.slice(match[0].length);
629
631
  if (groups.root) root = true;
630
- const name = groups.className ?? groups.double ?? groups.single ?? groups.bare;
632
+ const value = groups.double ?? groups.single ?? groups.bare;
633
+ if (value !== void 0) attribute = groups.attribute;
634
+ const name = groups.className ?? value;
631
635
  if (name !== void 0) names.push(name);
632
636
  }
633
637
  if (names.length > 1) return void 0;
@@ -635,7 +639,8 @@ function compoundTheme(part) {
635
639
  if (!root && names.length === 0) return void 0;
636
640
  return {
637
641
  root: root || names[0] === "light",
638
- ...name === void 0 ? {} : { name }
642
+ ...name === void 0 ? {} : { name },
643
+ ...name !== void 0 && attribute !== void 0 ? { attribute } : {}
639
644
  };
640
645
  }
641
646
  function matchingParen(text, open) {
@@ -0,0 +1,3 @@
1
+ import "./client-DgpoM3e7.mjs";
2
+ export { a as applyLibraryEdits, t as componentNode, T as generateId, n as librarySource, f as parseLibraryEdits, i as withLibraryContext } from "./src-BryimTPw.mjs";
3
+ import "./src-CpoDOrn3.mjs";
@@ -1,4 +1,4 @@
1
- import { p as parseLibraryManifest, u as libraryManifestExtension } from "./src-lVLSuOgh.mjs";
1
+ import { p as parseLibraryManifest, u as libraryManifestExtension } from "./src-BryimTPw.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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parity-app/cli",
3
- "version": "0.3.1",
3
+ "version": "0.5.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.",
package/skills/SKILL.md CHANGED
@@ -39,7 +39,7 @@ project, pass `tab: "active"` (`--tab active`) and read `get_app_state` for the
39
39
  | `get_console` | `parity console` | What the project's own code wrote to the console, on the canvas and offscreen. |
40
40
  | `get_tokens` | `parity tokens` | Tokens filtered by category, source or name, with values in every theme. |
41
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. |
42
+ | `get_props` | `parity props <Component>` | Props from source, then what it goes inside and holds, its slots and an example to insert; `all` adds passed-through attributes. |
43
43
  | `get_tabs` | `parity tabs` | The open projects. |
44
44
  | `open_project` | `parity open <folder>` | Opens a project in a new tab. |
45
45
  | `read_skill` | `parity skill [file]` | This skill. |
@@ -97,7 +97,8 @@ keeping the designs there, so ask before running that in someone's project.
97
97
  them with `AddState`, as changes to the base design, not as copies.
98
98
  5. **Check every frame.** Screenshot the design in all its states and the modes it shows (`state: "all"`,
99
99
  `mode: "shown"`). Run `find_problems` and `check_accessibility` too, and read the warnings `execute` returns: they're
100
- usually a token that doesn't exist or a prop the component doesn't take. Fix what you find, then check again.
100
+ usually a token that doesn't exist or a prop the component doesn't take. A render failure comes first and means a
101
+ frame wasn't checked. Fix what you find, then check again.
101
102
  6. **Say what you did.** Tell the person what changed and what you couldn't verify. Ask them to try anything interactive
102
103
  themselves with the Interact tool (I), which hands their mouse and keyboard to the real components.
103
104
 
@@ -5,20 +5,26 @@ change, so you never edit the files by hand. This is the shape `GetDesign` and `
5
5
 
6
6
  ## Designs
7
7
 
8
- A design is a screen or a component, drawn on the canvas as a board of frames: one frame per state and mode it shows.
8
+ A design is a screen, a component or a frame, drawn on the canvas as a board of frames: one frame per state and mode it
9
+ shows.
9
10
 
10
11
  | Field | What it is |
11
12
  | --------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
12
13
  | `id` | Made from the name when the design is created. Stable; use it in paths. |
13
14
  | `name` | What people see. |
14
- | `kind` | `screen` (a page or view) or `component` (a reusable part that screens place as instances). |
15
+ | `kind` | `screen` (a page or view), `component` (a reusable part that screens place as instances) or `frame` (a sketch; see below). |
15
16
  | `root` | The design itself: one node, usually a layout. |
16
17
  | `states` | Situations the whole design can be in, like loading or empty. The first is the base; the others store only what they change. |
17
18
  | `params` | A component design's inputs, each with a `type` (`text`, `boolean`, `number`, `value`, `variant`) and a `default`. See Params and bindings. |
18
- | `frames` | A fixed size per viewport, when the design isn't as wide as the viewport or as tall as its content. |
19
+ | `frames` | A screen's or component's fixed size per viewport, when it isn't as wide as the viewport or as tall as its content. |
20
+ | `size` | A frame's own `width` and `height` in pixels. Without a width it's as wide as the primary mode; without a height, as tall as its content. |
19
21
  | `view` | How the canvas shows it: the main mode, the modes beside it, the state and the variant options shown. Not part of the design. |
20
22
  | `context` | Components a component design is drawn inside on its own board, such as a table around a table row. |
21
23
 
24
+ A frame is the light kind, for sketching: a layout at its root, drawn once in the project's primary mode at its own
25
+ `size`, with one state and no `params`, `view` or `frames`. No code is written from it. Make it a screen or a component
26
+ to give it more.
27
+
22
28
  ## Nodes
23
29
 
24
30
  Every node has an `id` (given when it's inserted), and can have a `name`, `hidden`, `style` and `responsive`. Name the
package/skills/execute.md CHANGED
@@ -29,6 +29,20 @@ held.
29
29
  patches with a nonempty `find`, string `replace` (which can be empty), and optional boolean `all`. Invalid requests
30
30
  are rejected before reaching the app.
31
31
 
32
+ ### Warnings
33
+
34
+ A call that applies can still return warnings. Each finding comes once, with every place it was found. The text puts
35
+ them under "Render failures", "Warnings" and "Suggestions". The structured result's `warningGroups` gives each one's
36
+ `category`, `severity` (`error`, `warning` or `suggestion`), `message`, `count` and `at`, which lists the places as
37
+ `design/node` paths or frames. A render failure means a screenshot or check is missing for that frame, so look into it
38
+ first. `unnamed_layout` is a suggestion for layouts this call added: give each one a name with `Update`.
39
+
40
+ ```js
41
+ // Unnamed rows come back as one suggestion, such as 12 places (team-members/k3x9a, … and 7 more). Name them:
42
+ const rows = Get("team-members", (node, at) => (node.type === "layout" && !node.name ? at.path : undefined))
43
+ rows.forEach((path, index) => Update(path, { name: `Member row ${index + 1}` }))
44
+ ```
45
+
32
46
  ## API
33
47
 
34
48
  ```ts
@@ -47,8 +61,8 @@ GetModes(design?): { settings, variables, viewports, modes, designs } // modes
47
61
  Selection(): { design?, node?, mode? } // what the person has selected
48
62
 
49
63
  // Designs
50
- CreateDesign({ name, kind?, root?, x?, y?, file?, baseState? }): string // returns the new design's id
51
- UpdateDesign(design, { name?, kind?, x?, y?, mode?, showModes?, state?, allStates? }): void
64
+ CreateDesign({ name, kind?, root?, x?, y?, width?, height?, file?, baseState? }): string // the new design's id
65
+ UpdateDesign(design, { name?, kind?, x?, y?, width?, height?, mode?, showModes?, state?, allStates? }): void
52
66
  DeleteDesign(design): void
53
67
 
54
68
  // Nodes
@@ -91,12 +105,15 @@ A design is named by its id or its name. A node is named by `design/node`, where
91
105
  by its id alone when only one design has that id. Names aren't unique, so prefer ids once you have them; `Insert`,
92
106
  `Copy` and `CreateDesign` return them, and nodes you create with a `name` are listed under "Created" in the result.
93
107
 
108
+ A state is named by its id only, as `Designs()` lists it and `AddState` returns it. A state's name in its place fails
109
+ with `unknown_state`, and the error names the id it belongs to.
110
+
94
111
  ## Reading
95
112
 
96
113
  ### Designs
97
114
 
98
- Lists every design with its `id`, `name`, `kind` (`screen` or `component`), `states`, and `showing`: the state on the
99
- canvas, the main mode and the modes beside it. Start here to find ids.
115
+ Lists every design with its `id`, `name`, `kind` (`screen`, `component` or `frame`), `states`, and `showing`: the state
116
+ on the canvas, the main mode and the modes beside it. Start here to find ids.
100
117
 
101
118
  ### Get
102
119
 
@@ -131,8 +148,58 @@ Print(Get((node, at) => (node.type === "component" && node.component === "Button
131
148
  ### Components and Props
132
149
 
133
150
  `Components()` is everything a `component` node can name. `Props("Button")` returns the props found in the component's
134
- source: variants with their options (`variant`, `size`), its own props, and with `{ all: true }` the attributes it
135
- passes to its element. A prop that isn't listed, or a variant value outside its options, comes back as a warning.
151
+ source: variants with their options (`variant`, `size`), its own props, including those a library's component adds to
152
+ its element's (Nova's `isActive`), and with `{ all: true }` the attributes it passes to its element. A prop that isn't
153
+ listed, or a variant value outside its options, comes back as a warning.
154
+
155
+ Its `composition` says what it needs around it and holds, before you insert it:
156
+
157
+ - `role` is `provider` for one that draws nothing and gives the parts inside it their context (`SidebarProvider`),
158
+ `wrapper` for one other parts go inside (`Sidebar`), and `component` otherwise.
159
+ - `parents` is `listed`, with the `chain` it goes inside, outermost first, and where its library learned that; `none`;
160
+ or `unknown`, with the `reason`, such as a project component that hasn't drawn yet or an error asking for a provider.
161
+ An `optional` parent is left out when the ones outside it are already there.
162
+ - `holds` lists the parts that go directly inside it, and `slots` the `key` each slot's layers go under.
163
+ - `example.node` is the component inside its parents with its sample, as the Assets panel inserts it and the Components
164
+ panel draws it. `example.status` is `drawn` once the component has drawn on the canvas, `failing` with a `reason`, or
165
+ `untried`.
166
+
167
+ ```js
168
+ // A sidebar goes inside its provider: insert the example, then fill it in.
169
+ const { composition } = Props("nova/Sidebar")
170
+ Print(composition.parents.chain.map((parent) => parent.component))
171
+ const sidebar = Insert("settings/Content", composition.example.node)
172
+ ```
173
+
174
+ Writing the layers yourself works the same way. A provider draws nothing of its own, so the Components panel leaves it
175
+ out, but `Components()` lists it, since the parts inside it throw without it. A part placed without its provider shows
176
+ in `Problems` as a render error that names it.
177
+
178
+ ```js
179
+ // A Nova sidebar, inside the provider its parts need.
180
+ const part = (component, props, children) => ({ type: "component", component, props, slots: { children } })
181
+ const item = (label, isActive) =>
182
+ part("nova/SidebarMenuItem", {}, [
183
+ { type: "component", component: "nova/SidebarMenuButton", props: { isActive, children: label } }
184
+ ])
185
+ CreateDesign({
186
+ name: "App sidebar",
187
+ root: {
188
+ type: "layout",
189
+ name: "Frame",
190
+ style: { width: 280, height: 400 },
191
+ children: [
192
+ part("nova/SidebarProvider", {}, [
193
+ part("nova/Sidebar", { collapsible: "none" }, [
194
+ part("nova/SidebarContent", {}, [
195
+ part("nova/SidebarMenu", {}, [item("Dashboard", true), item("Members", false)])
196
+ ])
197
+ ])
198
+ ])
199
+ ]
200
+ }
201
+ })
202
+ ```
136
203
 
137
204
  ### Icons
138
205
 
@@ -158,8 +225,15 @@ Insert("dashboard/Toolbar", {
158
225
 
159
226
  Each token has a `name`, a `category` (`color`, `font-size`, `radius`, `shadow`, `spacing`, `font`, `font-weight`,
160
227
  `leading`, `tracking` and others), a `source` (`semantic` for the project's own variables, `theme` for its styling
161
- framework's scale) and its `values` per theme. Filter with `GetTokens({ category: "color", source: "semantic" })` or
162
- `GetTokens({ search: "radius" })`.
228
+ framework's scale, `keyword` for a framework keyword the style fields also take) and its `values` per theme. Filter with
229
+ `GetTokens({ category: "color", source: "semantic" })` or `GetTokens({ search: "radius" })`.
230
+
231
+ For a circle or a pill, use `radius-full`, Tailwind's `rounded-full`. It's listed under `radius` with
232
+ `source: "keyword"`, and frames draw it as round as the box allows, at any size:
233
+
234
+ ```js
235
+ Update("avatar", { style: { width: 10, height: 10, radius: "radius-full" } })
236
+ ```
163
237
 
164
238
  ### Selection
165
239
 
@@ -175,11 +249,27 @@ foreground colours. Without `x` and `y` it goes into empty space. `kind: "compon
175
249
  screens place as instances; most component designs come from `ExtractComponent` instead, because that also gives them
176
250
  params.
177
251
 
252
+ `kind: "frame"` makes a frame, the light kind for sketching an idea before it's a screen or a component: an empty padded
253
+ layout, drawn once at its own `width` and `height` in the project's primary mode. A frame has one state, no params and
254
+ no modes beside it, and no code is written from it, so `AddState`, `showModes` or another `mode` on one fail with
255
+ `invalid_input`. Turn it into a screen or a component when it needs them.
256
+
257
+ ```js
258
+ const sketch = CreateDesign({ name: "Sketch", kind: "frame", width: 480, height: 320 })
259
+ Insert(sketch, { type: "text", tag: "h2", text: "Pricing ideas" })
260
+ await Screenshot(sketch)
261
+ UpdateDesign(sketch, { kind: "screen" }) // keeps its layers, and its size in the primary viewport
262
+ ```
263
+
178
264
  ### UpdateDesign
179
265
 
180
266
  Renames or moves a design, or changes how it's shown: `mode` sets the main frame's mode, `showModes` the modes beside
181
267
  it, `state` the state on the canvas, and `allStates: true` shows every state in its own row. None of these change the
182
- design itself.
268
+ design itself. `width` and `height` resize a frame.
269
+
270
+ `kind` turns a design into another kind. Turned into a frame, a design keeps its layers as its board drew them in the
271
+ base state, with its params' values filled in, and drops its other states, the modes beside it, its params and its
272
+ variants; the call's warnings say what went.
183
273
 
184
274
  ## Nodes
185
275
 
@@ -219,6 +309,25 @@ removes the keys as `null` does.
219
309
  its design; to move it to another design, `Copy` it there and `Delete` the original. `Copy` gives the copy fresh ids and
220
310
  returns the new id. A design's root can't be deleted; use `Replace`.
221
311
 
312
+ The new node and everything in it get fresh ids, so `Replace` throws away the patches on the old node and what was
313
+ inside it: its `Responsive` patches, its changes in every state and its variant options' patches. Replacing a design's
314
+ root clears every state's changes. To keep a node's patches, change it with `Update` instead. The result lists what went
315
+ under "Discarded by Replace" (`discarded` in structured output), with each node's viewports, states and variants:
316
+
317
+ ```text
318
+ Discarded by Replace (reapply what you still want to the new node):
319
+ dashboard/stats, now k3x9a:
320
+ Stats (stats): viewports mobile
321
+ ```
322
+
323
+ Put back the ones you still want on the new ids, in the same call or the next. A state's changes go back with
324
+ `UpdateState(design, state, { changes: { [id]: … } })`.
325
+
326
+ ```js
327
+ const stats = Replace("dashboard/Stats", { type: "layout", name: "Stats", style: { direction: "row", gap: 16 } })
328
+ Responsive(stats, "mobile", { style: { direction: "column" } })
329
+ ```
330
+
222
331
  ### Responsive
223
332
 
224
333
  Patches a node in one viewport: `Responsive("dashboard/Stats", "mobile", { style: { columns: 1 } })`. The viewport is a
@@ -304,8 +413,29 @@ Frames draw as the canvas does: instances of a library's component designs (from
304
413
  token values not yet saved show, the person's edits and this call's own `SetTokens` alike, so
305
414
  `SetTokens({ primary: "red" }); await Screenshot(id)` shows the new value in the same call.
306
415
 
307
- `Measure` lays out one frame and returns its size, whether it overflows sideways, and every node's box (`clipped` when a
308
- parent cuts it off). `Problems` checks frames for render errors, overflow, clipping and unknown components.
416
+ `Measure` lays out one frame and returns its state, mode and size, whether it overflows sideways, and every node's box
417
+ with the node it's drawn inside (`parent`); a node an ancestor cuts off has `clipped` and that ancestor's box in `clip`.
418
+ `Problems` checks frames for render errors, overflow, clipping and unknown components. Each layout problem has its
419
+ `kind`: `frame-overflow` for a node that sticks out of the frame, by its position or its width, or `clipped` for one an
420
+ ancestor that hides overflow cuts off. It names the `node`, its `box`, the `container` it overflows (the clipping node,
421
+ or the frame, which has no `node`) with that box, and the pixels past each edge in `overflow`. A node past a parent that
422
+ doesn't clip, inside the frame, shows, so it isn't a problem; children going no further than a reported parent are left
423
+ out.
424
+
425
+ ```js
426
+ const id = CreateDesign({ name: "Checkout" })
427
+ Insert(id, { type: "component", component: "Button", name: "Pay", props: { children: "Pay" } })
428
+ // Each one, such as { kind: "frame-overflow", node: "pay", name: "Pay", box: { x: 340, y: 100, width: 60, height: 36 },
429
+ // container: { box: { x: 0, y: 0, width: 390, height: 600 } }, overflow: { right: 10 }, problem: "at x 340, …" }
430
+ for (const problem of await Problems(id, { mode: "shown" })) Print(problem.kind, problem.name, problem.overflow)
431
+ const layout = await Measure(id)
432
+ Print(
433
+ layout.state,
434
+ layout.mode,
435
+ layout.nodes.map((node) => `${node.id} in ${node.parent}`)
436
+ )
437
+ ```
438
+
309
439
  `Accessibility` returns axe-core's findings per node and frame, with the rule, its impact and what's wrong.
310
440
 
311
441
  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
@@ -15,8 +15,10 @@ small details.
15
15
 
16
16
  - **Render errors.** A component threw. Usually a prop with the wrong type, or a part used outside its parent (a
17
17
  `TabsTrigger` without `Tabs`). `parity components` lists the components failing anywhere on the canvas, with why.
18
- - **Overflow.** Content wider than the frame, usually a row that needs to wrap or stack on mobile.
19
- - **Clipping.** A node cut off by a parent with `overflow: hidden` or a fixed size.
18
+ - **Overflow.** A node that sticks out of the frame, by its width or where it's placed, usually a row that needs to wrap
19
+ or stack on mobile. The finding gives the node, its box, the frame's, and how many pixels past which edge.
20
+ - **Clipping.** A node cut off by an ancestor with `overflow: hidden` or a fixed size, named in the finding with its
21
+ box. A node that only spills past a parent that doesn't clip still shows, so it isn't listed.
20
22
  - **Unknown components.** A name that isn't in `Components()`; it draws as a placeholder.
21
23
 
22
24
  `Measure` gives every node's box in one frame, for checking alignment or sizes by numbers.
@@ -17,7 +17,9 @@ instance follows.
17
17
  - Read its props with `Props("Badge")`. Variants and sizes come with their options; use those values exactly. A prop the
18
18
  component doesn't declare comes back as a warning.
19
19
  - Compose the parts the way the library intends: `Tabs` holds a `TabsList` of `TabsTrigger`s and one `TabsContent` per
20
- trigger; `Table` holds `TableHeader` and `TableBody`, each with `TableRow`s of cells.
20
+ trigger; `Table` holds `TableHeader` and `TableBody`, each with `TableRow`s of cells. `Props("nova/Sidebar")` says
21
+ what a part goes inside in `composition.parents`, here the `SidebarProvider` it needs, and what goes inside it in
22
+ `composition.holds`. Start from `composition.example.node`, which already has its parents, when they aren't there.
21
23
  - Put children where they go. `slots.children` is what goes between the component's tags. Other slots, when a component
22
24
  takes nodes as props, are named after the prop.
23
25
 
@@ -22,6 +22,20 @@ only its changes, so editing the base updates every state.
22
22
 
23
23
  Name states for the situation ("No members yet", "Invite failed"), not the change ("Table hidden").
24
24
 
25
+ Every call that takes a state takes its id, never its name: `UpdateState`, `DeleteState`, `Update`'s and `Get`'s
26
+ `state`, `UpdateDesign`'s `state` and the checks. `AddState` returns the id, and `Designs()` lists each design's states
27
+ with their ids and names. Names can repeat and change, so an id made from a name stops matching it after a rename. A
28
+ name passed by mistake fails with `unknown_state`, naming the id it belongs to, or every id when states share the name.
29
+
30
+ ```js
31
+ AddState("team-members", { name: "No members yet", changes: { "Members table": { hidden: true } } })
32
+ // Later, find a state's id by its name in Designs(), then pass the id.
33
+ const states = Designs().find((design) => design.id === "team-members").states // [{ id, name }]
34
+ const empty = states.find((state) => state.name === "No members yet").id
35
+ UpdateState("team-members", empty, { name: "No members" })
36
+ await Screenshot("team-members", { state: empty })
37
+ ```
38
+
25
39
  ## Modes
26
40
 
27
41
  Modes are the project's, shared by every design. Each mode picks one option of each variable:
@@ -1,3 +0,0 @@
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";