@parity-app/cli 0.4.0 → 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-DgI96EpD.mjs");
291
- const { registryDir, registryEntries } = await import("./src-CJUvccJO.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-DaCYZuwb.mjs");
420
- const { covers, registryDir, registryEntries } = await import("./src-CJUvccJO.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-DaCYZuwb.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,53 @@ 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
+ ];
257
304
  const containerKinds = [
258
305
  "screen",
259
306
  "component",
@@ -350,16 +397,97 @@ function parseCanvasDocument(value) {
350
397
  "containers",
351
398
  "notes",
352
399
  "modes",
353
- "detection"
400
+ "detection",
401
+ "objects",
402
+ "links",
403
+ "sources"
354
404
  ], "");
355
405
  return compact({
356
406
  version: 3,
357
407
  containers: expectArray(document.containers, "/containers").map((item, index) => parseContainer(item, `/containers/${index}`)),
358
408
  modes: optional(document.modes, (value) => parseModeSettings(value)),
359
409
  notes: optional(document.notes, (value) => expectArray(value, "/notes").map((item, index) => parseNote(item, `/notes/${index}`))),
360
- 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
+ };
444
+ });
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
+ };
361
482
  });
362
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
+ }
363
491
  function parseDetection(value, path) {
364
492
  const detection = expectObject(value, path);
365
493
  expectKnownKeys(detection, ["include", "exclude"], path);
@@ -1386,6 +1514,7 @@ function librarySource(library, name, component, path) {
1386
1514
  library,
1387
1515
  ...component.group ? { group: component.group } : {},
1388
1516
  ...component.parents ? { parents: component.parents.map((parent) => `${library}/${parent}`) } : {},
1517
+ ...component.parents && component.parentsFrom ? { parentsFrom: component.parentsFrom } : {},
1389
1518
  ...component.internal ? { internal: true } : {},
1390
1519
  ...component.visual === false ? { visual: false } : {},
1391
1520
  ...component.optionalParents ? { optionalParents: component.optionalParents.map((parent) => `${library}/${parent}`) } : {},
@@ -1425,4 +1554,4 @@ function sampleIds(sample, library) {
1425
1554
  ...sample.children ? { children: sample.children.map(child) } : {}
1426
1555
  };
1427
1556
  }
1428
- export { viewportHeightProperty as C, lengthToPixels as E, collectCustomProperties as S, deviceKinds as T, paramTypes as _, applyLibraryEdits as a, generateId as b, isSampleElement as c, parentSignals as d, parseLibraryEdits as f, imageFits as g, containerKinds 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, parseCanvasDocument as v, viewportWidthProperty as w, parseModeSettings as x, styleKeys 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-y3hdgbK2.mjs";
1
+ export { t as covers, n as registryDir, r as registryEntries } from "./src-D85OVBQL.mjs";
@@ -1,5 +1,5 @@
1
- import { E as lengthToPixels, S as collectCustomProperties, a as applyLibraryEdits, c as isSampleElement, f as parseLibraryEdits, l as isSampleRender, m as renderSlotOf, p as parseLibraryManifest, s as isMadeValue, u as libraryManifestExtension } from "./src-CYEKdXm_.mjs";
2
- import "./src-y3hdgbK2.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-CYEKdXm_.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.4.0",
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
 
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
@@ -91,6 +105,9 @@ 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
@@ -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
 
@@ -235,6 +309,25 @@ removes the keys as `null` does.
235
309
  its design; to move it to another design, `Copy` it there and `Delete` the original. `Copy` gives the copy fresh ids and
236
310
  returns the new id. A design's root can't be deleted; use `Replace`.
237
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
+
238
331
  ### Responsive
239
332
 
240
333
  Patches a node in one viewport: `Responsive("dashboard/Stats", "mobile", { style: { columns: 1 } })`. The viewport is a
@@ -320,8 +413,29 @@ Frames draw as the canvas does: instances of a library's component designs (from
320
413
  token values not yet saved show, the person's edits and this call's own `SetTokens` alike, so
321
414
  `SetTokens({ primary: "red" }); await Screenshot(id)` shows the new value in the same call.
322
415
 
323
- `Measure` lays out one frame and returns its size, whether it overflows sideways, and every node's box (`clipped` when a
324
- 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
+
325
439
  `Accessibility` returns axe-core's findings per node and frame, with the rule, its impact and what's wrong.
326
440
 
327
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, b as generateId, n as librarySource, f as parseLibraryEdits, i as withLibraryContext } from "./src-CYEKdXm_.mjs";
3
- import "./src-DaCYZuwb.mjs";