@parity-app/cli 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,34 @@
1
- import "./schema-zRNxotYl.mjs";
1
+ import { a as parseData, n as finiteNumber, o as plainObject, t as fileObject } from "./schema-DcQLoUPP.mjs";
2
2
  import { z } from "zod";
3
3
  // ../core/src/adapters.ts
4
4
  const packageVersions = z.record(z.string(), z.string());
5
- z.looseObject({
5
+ const exportsTarget = z.lazy(() => z.union([
6
+ z.string(),
7
+ z.null(),
8
+ z.array(exportsTarget),
9
+ z.record(z.string(), exportsTarget)
10
+ ]));
11
+ /**
12
+ * The parts of a package.json Parity reads: the dependencies adapters detect a stack by, and where an installed
13
+ * package's entry points and a workspace's packages are (PAR-573). It's someone else's file, so the fields Parity
14
+ * doesn't read pass through unchecked.
15
+ */
16
+ const packageManifest = z.looseObject({
6
17
  name: z.string().optional(),
18
+ version: z.string().optional(),
7
19
  dependencies: packageVersions.optional(),
8
20
  devDependencies: packageVersions.optional(),
9
- scripts: z.record(z.string(), z.string()).optional()
21
+ scripts: z.record(z.string(), z.string()).optional(),
22
+ /** Entry points by subpath (`.`, `./tailwind.css`), or where the package's one entry point leads. */
23
+ exports: exportsTarget.optional(),
24
+ types: z.string().optional(),
25
+ typings: z.string().optional(),
26
+ module: z.string().optional(),
27
+ main: z.string().optional(),
28
+ /** The stylesheet a project imports to use the package, as some packages name it. */
29
+ style: z.string().optional(),
30
+ /** A workspace root's package folders, as npm, Yarn and Bun write them. */
31
+ workspaces: z.union([z.array(z.string()), z.looseObject({ packages: z.array(z.string()).optional() })]).optional()
10
32
  });
11
33
  // ../core/src/devices.ts
12
34
  const deviceKinds = [
@@ -33,6 +55,11 @@ function lengthToPixels(value) {
33
55
  */
34
56
  const viewportWidthProperty = "--canvas-vw";
35
57
  const viewportHeightProperty = "--canvas-vh";
58
+ /**
59
+ * The custom property `rem` is rewritten to: in a frame, the project's root font size, not Parity's. A frame sets it
60
+ * from `rootFontSizeProperty`, a length registered so that `1em` on the frame's root becomes that root's font size.
61
+ */
62
+ const remProperty = "--canvas-rem";
36
63
  /** Every custom property declaration in a stylesheet, in source order. */
37
64
  function collectCustomProperties(css) {
38
65
  const declarations = [];
@@ -129,42 +156,7 @@ function findStringEnd(css, start) {
129
156
  else index++;
130
157
  return css.length;
131
158
  }
132
- new RegExp(String.raw`"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|url\([^)]*\)|var\((?:${viewportWidthProperty}|${viewportHeightProperty}),[^)]*\)`, "g");
133
- // ../core/src/validate.ts
134
- function expectObject(value, path) {
135
- if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${path || "/"}: expected an object`);
136
- return value;
137
- }
138
- function expectString(value, path) {
139
- if (typeof value !== "string") throw new TypeError(`${path}: expected a string`);
140
- return value;
141
- }
142
- function expectNumber(value, path) {
143
- if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${path}: expected a number`);
144
- return value;
145
- }
146
- function expectBoolean(value, path) {
147
- if (typeof value !== "boolean") throw new TypeError(`${path}: expected true or false`);
148
- return value;
149
- }
150
- function expectArray(value, path) {
151
- if (!Array.isArray(value)) throw new TypeError(`${path}: expected an array`);
152
- return value;
153
- }
154
- function expectOneOf(value, options, path) {
155
- if (typeof value !== "string" || !options.includes(value)) throw new TypeError(`${path}: expected one of ${options.map((option) => `"${option}"`).join(", ")}`);
156
- return value;
157
- }
158
- function optional(value, parse) {
159
- return value === void 0 ? void 0 : parse(value);
160
- }
161
- /** Drops `undefined` entries, so parsed patches can be spread over defaults without erasing them. */
162
- function compact(value) {
163
- return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
164
- }
165
- function expectKnownKeys(value, keys, path) {
166
- for (const key of Object.keys(value)) if (!keys.includes(key)) throw new TypeError(`${path}/${key}: unknown property`);
167
- }
159
+ new RegExp(String.raw`"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|url\([^)]*\)|var\((?:${viewportWidthProperty}|${viewportHeightProperty}|${remProperty}),[^)]*\)`, "g");
168
160
  // ../core/src/modes.ts
169
161
  const variableKinds = [
170
162
  "viewport",
@@ -172,87 +164,69 @@ const variableKinds = [
172
164
  "locale",
173
165
  "custom"
174
166
  ];
175
- function parseModeSettings(value) {
176
- const settings = expectObject(value, "");
177
- expectKnownKeys(settings, [
178
- "$schema",
179
- "version",
180
- "variables",
181
- "modes"
182
- ], "");
183
- if (settings.version !== 1) throw new TypeError("/version: expected 1");
184
- const variables = expectArray(settings.variables, "/variables").map((item, index) => {
185
- const at = `/variables/${index}`;
186
- const variable = expectObject(item, at);
187
- expectKnownKeys(variable, [
188
- "id",
189
- "name",
190
- "kind",
191
- "options"
192
- ], at);
193
- const options = expectArray(variable.options, `${at}/options`).map((entry, optionIndex) => {
194
- const optionAt = `${at}/options/${optionIndex}`;
195
- const option = expectObject(entry, optionAt);
196
- expectKnownKeys(option, [
197
- "id",
198
- "name",
199
- "width",
200
- "height",
201
- "device",
202
- "className",
203
- "attributes"
204
- ], optionAt);
205
- return compact({
206
- id: expectString(option.id, `${optionAt}/id`),
207
- name: expectString(option.name, `${optionAt}/name`),
208
- width: optional(option.width, (v) => expectNumber(v, `${optionAt}/width`)),
209
- height: optional(option.height, (v) => expectNumber(v, `${optionAt}/height`)),
210
- device: optional(option.device, (v) => expectOneOf(v, deviceKinds, `${optionAt}/device`)),
211
- className: optional(option.className, (v) => expectString(v, `${optionAt}/className`)),
212
- attributes: optional(option.attributes, (v) => {
213
- const attributes = expectObject(v, `${optionAt}/attributes`);
214
- for (const [name, attribute] of Object.entries(attributes)) expectString(attribute, `${optionAt}/attributes/${name}`);
215
- return attributes;
216
- })
217
- });
167
+ /** What choosing this option does to a rendered frame. */
168
+ const variableOption = fileObject({
169
+ id: z.string(),
170
+ name: z.string(),
171
+ /** Frame width in CSS pixels (viewport options). */
172
+ width: finiteNumber.optional(),
173
+ /** The screen's height, for viewports taken from a device. Frames still grow with their content. */
174
+ height: finiteNumber.optional(),
175
+ /** What the viewport is, for its icon. Otherwise judged by its width. */
176
+ device: z.enum(deviceKinds).optional(),
177
+ /** Class added to the frame body, e.g. `dark` for shadcn themes. */
178
+ className: z.string().optional(),
179
+ /** Attributes set on the frame body, e.g. `lang` and `dir` for locales. */
180
+ attributes: z.record(z.string(), z.string()).optional()
181
+ });
182
+ const modeVariable = fileObject({
183
+ id: z.string(),
184
+ name: z.string(),
185
+ kind: z.enum(variableKinds),
186
+ options: z.array(variableOption).min(1, "expected at least one option")
187
+ });
188
+ const modeSpec = fileObject({
189
+ id: z.string(),
190
+ name: z.string(),
191
+ /** Option id per variable id. A variable that isn't listed uses its first option. */
192
+ values: z.record(z.string(), z.string())
193
+ });
194
+ /**
195
+ * Project-wide modes, like Chromatic modes in Storybook. A variable is one axis of the environment (viewport, theme,
196
+ * locale…); a mode is a named combination of one option per variable. Every design can show any mode, and each design
197
+ * chooses which ones it shows. The first mode is the primary one: the main frame of every design.
198
+ */
199
+ const modeSettings = fileObject({
200
+ $schema: z.string().optional(),
201
+ version: z.literal(1),
202
+ variables: z.array(modeVariable),
203
+ modes: z.array(modeSpec).min(1, "expected at least one mode")
204
+ }).superRefine((settings, context) => {
205
+ for (const [index, mode] of settings.modes.entries()) for (const [variableId, optionId] of Object.entries(mode.values)) {
206
+ const path = [
207
+ "modes",
208
+ index,
209
+ "values",
210
+ variableId
211
+ ];
212
+ const variable = settings.variables.find((candidate) => candidate.id === variableId);
213
+ if (!variable) context.addIssue({
214
+ code: "custom",
215
+ message: "unknown variable",
216
+ path,
217
+ input: variableId
218
218
  });
219
- if (options.length === 0) throw new TypeError(`${at}/options: expected at least one option`);
220
- return {
221
- id: expectString(variable.id, `${at}/id`),
222
- name: expectString(variable.name, `${at}/name`),
223
- kind: expectOneOf(variable.kind, variableKinds, `${at}/kind`),
224
- options
225
- };
226
- });
227
- const modes = expectArray(settings.modes, "/modes").map((item, index) => {
228
- const at = `/modes/${index}`;
229
- const mode = expectObject(item, at);
230
- expectKnownKeys(mode, [
231
- "id",
232
- "name",
233
- "values"
234
- ], at);
235
- const values = {};
236
- for (const [variableId, optionId] of Object.entries(expectObject(mode.values, `${at}/values`))) {
237
- const variable = variables.find((candidate) => candidate.id === variableId);
238
- if (!variable) throw new TypeError(`${at}/values/${variableId}: unknown variable`);
239
- const option = expectString(optionId, `${at}/values/${variableId}`);
240
- if (!variable.options.some((candidate) => candidate.id === option)) throw new TypeError(`${at}/values/${variableId}: "${option}" is not an option of ${variable.name}`);
241
- values[variableId] = option;
219
+ else if (!variable.options.some((option) => option.id === optionId)) {
220
+ const message = `"${optionId}" is not an option of ${variable.name}`;
221
+ context.addIssue({
222
+ code: "custom",
223
+ message,
224
+ path,
225
+ input: optionId
226
+ });
242
227
  }
243
- return {
244
- id: expectString(mode.id, `${at}/id`),
245
- name: expectString(mode.name, `${at}/name`),
246
- values
247
- };
248
- });
249
- if (modes.length === 0) throw new TypeError("/modes: expected at least one mode");
250
- return {
251
- version: 1,
252
- variables,
253
- modes
254
- };
255
- }
228
+ }
229
+ });
256
230
  // ../core/src/edit.ts
257
231
  const idAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
258
232
  /** A short random id that isn't in `taken`, for nodes and designs created in the editor or by agents. */
@@ -264,16 +238,45 @@ function generateId(taken, length = 5) {
264
238
  }
265
239
  }
266
240
  // ../core/src/document.ts
241
+ const position = fileObject({
242
+ x: finiteNumber,
243
+ y: finiteNumber
244
+ });
267
245
  const canvasObjectKinds = [
268
246
  "table",
269
247
  "operation",
270
248
  "route"
271
249
  ];
250
+ const canvasObjectSpec = fileObject({
251
+ kind: z.enum(canvasObjectKinds),
252
+ /** A table as `schema.table`, an operation by its id, a route by its URL pattern. */
253
+ ref: z.string(),
254
+ position
255
+ });
256
+ /** The canvas objects, each table, operation and route at most once. */
257
+ const canvasObjects = z.array(canvasObjectSpec).superRefine((objects, context) => {
258
+ const placed = /* @__PURE__ */ new Set();
259
+ for (const [index, { kind, ref }] of objects.entries()) {
260
+ const key = JSON.stringify([kind, ref]);
261
+ if (placed.has(key)) problem(context, [index], `the ${kind} "${ref}" is already on the canvas`);
262
+ placed.add(key);
263
+ }
264
+ });
272
265
  const linkEndKinds = [
273
266
  "screen",
274
267
  "component",
275
268
  ...canvasObjectKinds
276
269
  ];
270
+ const linkEnd = fileObject({
271
+ kind: z.enum(linkEndKinds),
272
+ ref: z.string()
273
+ });
274
+ const linkType = z.enum([
275
+ "shows",
276
+ "calls",
277
+ "reads",
278
+ "writes"
279
+ ]);
277
280
  /** Each type of link, and the kinds it may join, from and to. */
278
281
  const linkTypes = {
279
282
  /** A screen shown at a route's URL. */
@@ -306,30 +309,301 @@ function linkAllowed(type, from, to) {
306
309
  const allowed = linkTypes[type];
307
310
  return allowed.from.includes(from) && allowed.to.includes(to);
308
311
  }
309
- const lenses = [
310
- "data",
311
- "api",
312
- "routes"
312
+ const linkSpec = fileObject({
313
+ id: z.string(),
314
+ type: linkType,
315
+ from: linkEnd,
316
+ to: linkEnd
317
+ }).superRefine((link, context) => {
318
+ if (linkAllowed(link.type, link.from.kind, link.to.kind)) return;
319
+ const { from, to } = linkTypes[link.type];
320
+ const joined = `${from.join(" or ")} to ${to.join(" or ")}, not ${link.from.kind} to ${link.to.kind}`;
321
+ problem(context, [], `a ${link.type} link goes from ${joined}`);
322
+ });
323
+ /** The links the person drew, each id once. */
324
+ const linkSpecs = z.array(linkSpec).superRefine((links, context) => {
325
+ const ids = /* @__PURE__ */ new Set();
326
+ for (const [index, { id }] of links.entries()) {
327
+ if (ids.has(id)) problem(context, [index, "id"], `duplicate link id "${id}"`);
328
+ ids.add(id);
329
+ }
330
+ });
331
+ const lensSources = fileObject({
332
+ data: z.string().optional(),
333
+ api: z.string().optional(),
334
+ routes: z.string().optional()
335
+ });
336
+ const componentRef = fileObject({
337
+ path: z.string(),
338
+ export: z.string()
339
+ });
340
+ const detectionCorrections = fileObject({
341
+ include: z.array(componentRef).optional(),
342
+ exclude: z.unknown().optional()
343
+ }).transform(({ exclude: _dropped, ...corrections }) => corrections);
344
+ const noteSpec = fileObject({
345
+ id: z.string(),
346
+ text: z.string(),
347
+ position,
348
+ /** Wraps at this width; unset notes grow with their text. */
349
+ width: finiteNumber.optional()
350
+ });
351
+ /** Steps of the spacing scale (`4` is `calc(var(--spacing) * 4)`), or the name of a length token. */
352
+ const length = z.union([finiteNumber, z.string()]);
353
+ const sides = "expected a length, [vertical, horizontal] or [top, right, bottom, left]";
354
+ const padding = z.union([
355
+ z.tuple([length, length], sides),
356
+ z.tuple([
357
+ length,
358
+ length,
359
+ length,
360
+ length
361
+ ], sides),
362
+ length
363
+ ]);
364
+ const alignment = z.enum([
365
+ "start",
366
+ "center",
367
+ "end",
368
+ "stretch"
369
+ ]);
370
+ const nodeStyle = fileObject({
371
+ /** Auto layout (flex, the default) or a grid. */
372
+ display: z.enum(["flex", "grid"]).optional(),
373
+ direction: z.enum(["row", "column"]).optional(),
374
+ /** A grid's columns, all the same width; or rows, all the same height. */
375
+ columns: finiteNumber.optional(),
376
+ rows: finiteNumber.optional(),
377
+ gap: length.optional(),
378
+ /** In a grid or wrapping layout, the gap between rows when it differs from `gap`. */
379
+ rowGap: length.optional(),
380
+ /** All sides, `[vertical, horizontal]`, or `[top, right, bottom, left]`. */
381
+ padding: padding.optional(),
382
+ /** Space outside the node, in the same forms as padding. */
383
+ margin: padding.optional(),
384
+ /** Children on the cross axis. */
385
+ align: alignment.optional(),
386
+ /** Children on the main axis. */
387
+ justify: z.enum([
388
+ "start",
389
+ "center",
390
+ "end",
391
+ "between",
392
+ "around",
393
+ "evenly"
394
+ ]).optional(),
395
+ wrap: z.boolean().optional(),
396
+ width: length.optional(),
397
+ height: length.optional(),
398
+ minWidth: length.optional(),
399
+ maxWidth: length.optional(),
400
+ minHeight: length.optional(),
401
+ maxHeight: length.optional(),
402
+ /** Grid cells the node spans, in a grid. */
403
+ colSpan: finiteNumber.optional(),
404
+ rowSpan: finiteNumber.optional(),
405
+ /** Fill along the parent's direction. Same as `fill` for the parent's main axis. */
406
+ grow: z.boolean().optional(),
407
+ /** Where this node sits on its parent's cross axis, overriding the parent's `align`. */
408
+ self: alignment.optional(),
409
+ position: z.enum(["absolute"]).optional(),
410
+ top: length.optional(),
411
+ right: length.optional(),
412
+ bottom: length.optional(),
413
+ left: length.optional(),
414
+ background: z.string().optional(),
415
+ foreground: z.string().optional(),
416
+ /** Border color. */
417
+ border: z.string().optional(),
418
+ /** Border width in pixels, as in Tailwind: 0, 1, 2, 4 or 8. */
419
+ borderWidth: finiteNumber.optional(),
420
+ borderStyle: z.enum([
421
+ "solid",
422
+ "dashed",
423
+ "dotted"
424
+ ]).optional(),
425
+ radius: z.string().optional(),
426
+ shadow: z.string().optional(),
427
+ /** Percent, 0 to 100. */
428
+ opacity: finiteNumber.optional(),
429
+ overflow: z.enum([
430
+ "visible",
431
+ "hidden",
432
+ "scroll"
433
+ ]).optional(),
434
+ font: z.string().optional(),
435
+ size: z.string().optional(),
436
+ weight: finiteNumber.optional(),
437
+ leading: z.string().optional(),
438
+ tracking: z.string().optional(),
439
+ textAlign: z.enum([
440
+ "start",
441
+ "center",
442
+ "end",
443
+ "justify"
444
+ ]).optional(),
445
+ transform: z.enum([
446
+ "uppercase",
447
+ "lowercase",
448
+ "capitalize"
449
+ ]).optional(),
450
+ decoration: z.enum(["underline", "line-through"]).optional(),
451
+ italic: z.boolean().optional(),
452
+ /** Lines of text before it's cut off with an ellipsis. */
453
+ lines: finiteNumber.optional()
454
+ });
455
+ const styleKey = nodeStyle.keyof();
456
+ styleKey.options;
457
+ const patchUnset = fileObject({
458
+ props: z.array(z.string()).optional(),
459
+ style: z.array(styleKey).optional()
460
+ });
461
+ const nodePatch = fileObject({
462
+ hidden: z.boolean().optional(),
463
+ text: z.string().optional(),
464
+ props: plainObject.optional(),
465
+ style: nodeStyle.optional(),
466
+ /** What the patch takes away from what's under it, so a state can turn off a prop or style the design sets. */
467
+ unset: patchUnset.optional()
468
+ });
469
+ /** Patches keyed by node id, or by `instance/inner` for a node inside an instance. */
470
+ const nodePatches = z.record(z.string(), nodePatch);
471
+ const nodeBinding = fileObject({
472
+ text: z.string().optional(),
473
+ props: z.record(z.string(), z.string()).optional(),
474
+ visible: z.string().optional()
475
+ });
476
+ const nodeBase = fileObject({
477
+ id: z.string(),
478
+ name: z.string().optional(),
479
+ hidden: z.boolean().optional(),
480
+ style: nodeStyle.optional(),
481
+ /**
482
+ * Patches applied in one viewport, keyed by the viewport option id from the project's modes. Layout changes per
483
+ * viewport live here, not in copies.
484
+ */
485
+ responsive: nodePatches.optional(),
486
+ /**
487
+ * Inside a component design: which of this node's values come from the component's params, so each instance can
488
+ * show its own. `text` and `props` name a param; `visible` names a boolean param that shows or hides the node.
489
+ */
490
+ bind: nodeBinding.optional()
491
+ });
492
+ const layoutNode = nodeBase.extend({
493
+ type: z.literal("layout"),
494
+ get children() {
495
+ return z.array(designNode);
496
+ }
497
+ });
498
+ const componentNode$1 = nodeBase.extend({
499
+ type: z.literal("component"),
500
+ component: z.string(),
501
+ props: plainObject.optional(),
502
+ /** Child nodes by slot. `children` is the default slot; other names become node-valued props. */
503
+ get slots() {
504
+ return z.record(z.string(), z.array(designNode)).optional();
505
+ }
506
+ });
507
+ const textNode = nodeBase.extend({
508
+ type: z.literal("text"),
509
+ text: z.string(),
510
+ tag: z.enum([
511
+ "p",
512
+ "span",
513
+ "h1",
514
+ "h2",
515
+ "h3",
516
+ "h4",
517
+ "strong"
518
+ ]).optional()
519
+ });
520
+ const instanceNode = nodeBase.extend({
521
+ type: z.literal("instance"),
522
+ /** The component design's id. */
523
+ design: z.string(),
524
+ args: plainObject.optional(),
525
+ /** Which of the component's states this copy shows. */
526
+ state: z.string().optional(),
527
+ /**
528
+ * What this copy changes inside the component, keyed by the inner node's id, or `nested/inner` for a node inside a
529
+ * nested instance. The copy stays linked: everything it doesn't change follows the component.
530
+ */
531
+ overrides: nodePatches.optional()
532
+ });
533
+ const imageFits = [
534
+ "cover",
535
+ "contain",
536
+ "fill"
313
537
  ];
538
+ const imageCredit = fileObject({
539
+ name: z.string(),
540
+ url: z.string(),
541
+ /** The library it came from. */
542
+ source: z.string().optional()
543
+ });
544
+ const imageNode = nodeBase.extend({
545
+ type: z.literal("image"),
546
+ /** A URL, or a path the project's dev server serves, like `/images/photo.jpg`. */
547
+ src: z.string(),
548
+ alt: z.string().optional(),
549
+ /** How it fills its box: cropped to cover it (the default), shown whole, or stretched. */
550
+ fit: z.enum(imageFits).optional(),
551
+ /** Who made it and where it came from, for photos that ask to be credited. */
552
+ credit: imageCredit.optional()
553
+ });
554
+ const designNode = z.discriminatedUnion("type", [
555
+ layoutNode,
556
+ componentNode$1,
557
+ textNode,
558
+ instanceNode,
559
+ imageNode
560
+ ]);
561
+ /** A node tree as a design or an insertion holds it, each node with an id of its own. */
562
+ const nodeTree = designNode.superRefine((root, context) => {
563
+ const ids = /* @__PURE__ */ new Set();
564
+ const visit = (node, path) => {
565
+ if (ids.has(node.id)) problem(context, [...path, "id"], `duplicate node id "${node.id}"`);
566
+ ids.add(node.id);
567
+ if (node.type === "layout") for (const [index, child] of node.children.entries()) visit(child, [
568
+ ...path,
569
+ "children",
570
+ index
571
+ ]);
572
+ else if (node.type === "component") for (const [slot, children] of Object.entries(node.slots ?? {})) for (const [index, child] of children.entries()) visit(child, [
573
+ ...path,
574
+ "slots",
575
+ slot,
576
+ index
577
+ ]);
578
+ };
579
+ visit(root, []);
580
+ });
581
+ /** Calls `visit` for every node in a tree, depth first, including nodes in slots. */
582
+ function walkNodes(node, visit, depth = 0) {
583
+ visit(node, depth);
584
+ let children = [];
585
+ if (node.type === "layout") children = node.children;
586
+ else if (node.type === "component") children = Object.values(node.slots ?? {}).flat();
587
+ for (const child of children) walkNodes(child, visit, depth + 1);
588
+ }
314
589
  const containerKinds = [
315
590
  "screen",
316
591
  "component",
317
592
  "frame"
318
593
  ];
319
- const textTags = [
320
- "p",
321
- "span",
322
- "h1",
323
- "h2",
324
- "h3",
325
- "h4",
326
- "strong"
327
- ];
328
- const imageFits = [
329
- "cover",
330
- "contain",
331
- "fill"
332
- ];
594
+ const frameSize = fileObject({
595
+ width: finiteNumber.optional(),
596
+ height: finiteNumber.optional()
597
+ });
598
+ const variantOption = fileObject({
599
+ id: z.string(),
600
+ name: z.string(),
601
+ /**
602
+ * Patches keyed by node id, or by `instance/inner` for a node inside an instance. They apply over the design and
603
+ * under its states.
604
+ */
605
+ overrides: nodePatches.optional()
606
+ });
333
607
  const paramTypes = [
334
608
  "text",
335
609
  "boolean",
@@ -337,596 +611,178 @@ const paramTypes = [
337
611
  "value",
338
612
  "variant"
339
613
  ];
340
- const alignments = [
341
- "start",
342
- "center",
343
- "end",
344
- "stretch"
345
- ];
346
- const justifications = [
347
- "start",
348
- "center",
349
- "end",
350
- "between",
351
- "around",
352
- "evenly"
353
- ];
354
- /** Every style key, in the order panels show them. */
355
- const styleKeys = [
356
- "display",
357
- "direction",
358
- "columns",
359
- "rows",
360
- "gap",
361
- "rowGap",
362
- "padding",
363
- "margin",
364
- "align",
365
- "justify",
366
- "wrap",
367
- "width",
368
- "height",
369
- "minWidth",
370
- "maxWidth",
371
- "minHeight",
372
- "maxHeight",
373
- "colSpan",
374
- "rowSpan",
375
- "grow",
376
- "self",
377
- "position",
378
- "top",
379
- "right",
380
- "bottom",
381
- "left",
382
- "background",
383
- "foreground",
384
- "border",
385
- "borderWidth",
386
- "borderStyle",
387
- "radius",
388
- "shadow",
389
- "opacity",
390
- "overflow",
391
- "font",
392
- "size",
393
- "weight",
394
- "leading",
395
- "tracking",
396
- "textAlign",
397
- "transform",
398
- "decoration",
399
- "italic",
400
- "lines"
401
- ];
402
- function parseCanvasDocument(value) {
403
- const document = expectObject(value, "");
404
- if (document.version !== 3) throw new TypeError("/version: expected 3");
405
- expectKnownKeys(document, [
406
- "version",
407
- "containers",
408
- "notes",
409
- "modes",
410
- "detection",
411
- "objects",
412
- "links",
413
- "sources"
414
- ], "");
415
- return compact({
416
- version: 3,
417
- containers: expectArray(document.containers, "/containers").map((item, index) => parseContainer(item, `/containers/${index}`)),
418
- modes: optional(document.modes, (value) => parseModeSettings(value)),
419
- notes: optional(document.notes, (value) => expectArray(value, "/notes").map((item, index) => parseNote(item, `/notes/${index}`))),
420
- detection: optional(document.detection, (value) => parseDetection(value, "/detection")),
421
- objects: optional(document.objects, (value) => parseObjects(value, "/objects")),
422
- links: optional(document.links, (value) => parseLinks(value, "/links")),
423
- sources: optional(document.sources, (value) => parseSources(value, "/sources"))
424
- });
425
- }
426
- function parsePosition(value, path) {
427
- const position = expectObject(value, path);
428
- expectKnownKeys(position, ["x", "y"], path);
429
- return {
430
- x: expectNumber(position.x, `${path}/x`),
431
- y: expectNumber(position.y, `${path}/y`)
432
- };
433
- }
434
- function parseObjects(value, path) {
435
- const placed = /* @__PURE__ */ new Set();
436
- return expectArray(value, path).map((item, index) => {
437
- const at = `${path}/${index}`;
438
- const object = expectObject(item, at);
439
- expectKnownKeys(object, [
440
- "kind",
441
- "ref",
442
- "position"
443
- ], at);
444
- const kind = expectOneOf(object.kind, canvasObjectKinds, `${at}/kind`);
445
- const ref = expectString(object.ref, `${at}/ref`);
446
- const key = JSON.stringify([kind, ref]);
447
- if (placed.has(key)) throw new TypeError(`${at}: the ${kind} "${ref}" is already on the canvas`);
448
- placed.add(key);
449
- return {
450
- kind,
451
- ref,
452
- position: parsePosition(object.position, `${at}/position`)
453
- };
454
- });
455
- }
456
- function parseLinkEnd(value, path) {
457
- const end = expectObject(value, path);
458
- expectKnownKeys(end, ["kind", "ref"], path);
459
- return {
460
- kind: expectOneOf(end.kind, linkEndKinds, `${path}/kind`),
461
- ref: expectString(end.ref, `${path}/ref`)
462
- };
463
- }
464
- function parseLinks(value, path) {
614
+ const paramSpec = fileObject({
615
+ type: z.enum(paramTypes),
616
+ /** A variant's default is the id of one of its options; unset means the first. */
617
+ default: z.unknown().optional(),
618
+ description: z.string().optional(),
619
+ /** A variant's choices: size small, medium or large. Each option patches the design, the way a state does. */
620
+ options: z.array(variantOption).optional()
621
+ }).superRefine((param, context) => {
465
622
  const ids = /* @__PURE__ */ new Set();
466
- const types = Object.keys(linkTypes);
467
- return expectArray(value, path).map((item, index) => {
468
- const at = `${path}/${index}`;
469
- const link = expectObject(item, at);
470
- expectKnownKeys(link, [
471
- "id",
472
- "type",
473
- "from",
474
- "to"
475
- ], at);
476
- const id = expectString(link.id, `${at}/id`);
477
- if (ids.has(id)) throw new TypeError(`${at}/id: duplicate link id "${id}"`);
623
+ for (const [index, { id }] of (param.options ?? []).entries()) {
624
+ if (ids.has(id)) problem(context, [
625
+ "options",
626
+ index,
627
+ "id"
628
+ ], `duplicate option id "${id}"`);
478
629
  ids.add(id);
479
- const type = expectOneOf(link.type, types, `${at}/type`);
480
- const from = parseLinkEnd(link.from, `${at}/from`);
481
- const to = parseLinkEnd(link.to, `${at}/to`);
482
- if (!linkAllowed(type, from.kind, to.kind)) {
483
- const { from: froms, to: tos } = linkTypes[type];
484
- throw new TypeError(`${at}: a ${type} link goes from ${froms.join(" or ")} to ${tos.join(" or ")}, not ${from.kind} to ${to.kind}`);
485
- }
486
- return {
487
- id,
488
- type,
489
- from,
490
- to
491
- };
492
- });
493
- }
494
- function parseSources(value, path) {
495
- const sources = expectObject(value, path);
496
- expectKnownKeys(sources, lenses, path);
497
- const chosen = {};
498
- for (const lens of lenses) if (sources[lens] !== void 0) chosen[lens] = expectString(sources[lens], `${path}/${lens}`);
499
- return chosen;
500
- }
501
- function parseDetection(value, path) {
502
- const detection = expectObject(value, path);
503
- expectKnownKeys(detection, ["include", "exclude"], path);
504
- const refs = (list, at) => expectArray(list, at).map((item, index) => {
505
- const ref = expectObject(item, `${at}/${index}`);
506
- expectKnownKeys(ref, ["path", "export"], `${at}/${index}`);
507
- return {
508
- path: expectString(ref.path, `${at}/${index}/path`),
509
- export: expectString(ref.export, `${at}/${index}/export`)
510
- };
511
- });
512
- return compact({ include: optional(detection.include, (list) => refs(list, `${path}/include`)) });
513
- }
514
- function parseNote(value, path) {
515
- const note = expectObject(value, path);
516
- expectKnownKeys(note, [
517
- "id",
518
- "text",
519
- "position",
520
- "width"
521
- ], path);
522
- const position = expectObject(note.position, `${path}/position`);
523
- return compact({
524
- id: expectString(note.id, `${path}/id`),
525
- text: expectString(note.text, `${path}/text`),
526
- position: {
527
- x: expectNumber(position.x, `${path}/position/x`),
528
- y: expectNumber(position.y, `${path}/position/y`)
529
- },
530
- width: optional(note.width, (v) => expectNumber(v, `${path}/width`))
531
- });
532
- }
533
- function parseContainer(value, path) {
534
- const container = expectObject(value, path);
535
- expectKnownKeys(container, [
536
- "id",
537
- "name",
538
- "kind",
539
- "position",
540
- "root",
630
+ }
631
+ if (param.type === "variant" && !param.options?.length) problem(context, ["options"], "a variant needs options");
632
+ if (param.type !== "variant" && param.options) problem(context, ["options"], "only variants have options");
633
+ });
634
+ const containerViewSettings = fileObject({
635
+ /** The mode of the main frame. Defaults to the project's primary mode. */
636
+ mode: z.string().optional(),
637
+ /** Other modes shown beside the main frame. */
638
+ modes: z.array(z.string()).optional(),
639
+ /** The state the canvas shows. Defaults to the first. */
640
+ state: z.string().optional(),
641
+ /** Show every state, one row each, instead of only the active one. */
642
+ allStates: z.boolean().optional(),
643
+ /** The option the board shows for each variant param, by option id. Unset ones show their default. */
644
+ variants: z.record(z.string(), z.string()).optional()
645
+ });
646
+ const stateSpec = fileObject({
647
+ id: z.string(),
648
+ name: z.string(),
649
+ /**
650
+ * Patches keyed by node id, or by `instance/inner` for a node inside an instance. A state wins over a viewport's
651
+ * responsive patch and over the design's variants.
652
+ */
653
+ overrides: nodePatches.optional()
654
+ });
655
+ const containerSpec = fileObject({
656
+ id: z.string(),
657
+ name: z.string(),
658
+ /** Defaults to `screen`. */
659
+ kind: z.enum(containerKinds).optional(),
660
+ position,
661
+ root: nodeTree,
662
+ states: z.array(stateSpec).min(1, "expected at least one state"),
663
+ view: containerViewSettings.optional(),
664
+ /** A component's inputs. Instances pass values for them; nodes inside bind to them. */
665
+ params: z.record(z.string(), paramSpec).optional(),
666
+ /**
667
+ * Components this design sits inside when shown on its own board, outermost first, like a Storybook decorator: a
668
+ * table row needs a table around it to render properly.
669
+ */
670
+ context: z.array(fileObject({
671
+ component: z.string(),
672
+ props: plainObject.optional()
673
+ })).optional(),
674
+ /**
675
+ * A screen's or component's frame sizes in CSS pixels, keyed by viewport option id. Without one, a frame is as wide
676
+ * as its mode's viewport and as tall as its content.
677
+ */
678
+ frames: z.record(z.string(), frameSize).optional(),
679
+ /**
680
+ * A frame's own size in CSS pixels, whatever the project's viewports are. Without a width it's as wide as the primary
681
+ * mode's viewport, and without a height as tall as its content. Only a frame has one.
682
+ */
683
+ size: frameSize.optional()
684
+ }).superRefine((container, context) => {
685
+ const ids = /* @__PURE__ */ new Set();
686
+ walkNodes(container.root, (node) => ids.add(node.id));
687
+ for (const [index, state] of container.states.entries()) checkPatchKeys(state.overrides, ids, context, [
541
688
  "states",
542
- "view",
689
+ index,
690
+ "overrides"
691
+ ]);
692
+ if (container.kind === "frame") checkFrame(container, context);
693
+ else if (container.size !== void 0) problem(context, ["size"], `only a frame has a size of its own; size a ${container.kind ?? "screen"} in frames`);
694
+ for (const [name, param] of Object.entries(container.params ?? {})) for (const [index, option] of (param.options ?? []).entries()) checkPatchKeys(option.overrides, ids, context, [
543
695
  "params",
544
- "context",
545
- "frames",
546
- "size"
547
- ], path);
548
- const position = expectObject(container.position, `${path}/position`);
549
- const view = optional(container.view, (v) => {
550
- const settings = expectObject(v, `${path}/view`);
551
- expectKnownKeys(settings, [
552
- "mode",
553
- "modes",
554
- "state",
555
- "allStates",
556
- "variants"
557
- ], `${path}/view`);
558
- return compact({
559
- mode: optional(settings.mode, (mode) => expectString(mode, `${path}/view/mode`)),
560
- modes: optional(settings.modes, (list) => expectArray(list, `${path}/view/modes`).map((mode, index) => expectString(mode, `${path}/view/modes/${index}`))),
561
- state: optional(settings.state, (state) => expectString(state, `${path}/view/state`)),
562
- allStates: optional(settings.allStates, (all) => expectBoolean(all, `${path}/view/allStates`)),
563
- variants: optional(settings.variants, (choices) => {
564
- const variants = {};
565
- for (const [param, option] of Object.entries(expectObject(choices, `${path}/view/variants`))) variants[param] = expectString(option, `${path}/view/variants/${param}`);
566
- return variants;
567
- })
568
- });
569
- });
570
- const ids = /* @__PURE__ */ new Set();
571
- const root = parseNode(container.root, `${path}/root`, ids);
572
- const states = expectArray(container.states, `${path}/states`).map((item, index) => {
573
- const at = `${path}/states/${index}`;
574
- const state = expectObject(item, at);
575
- expectKnownKeys(state, [
576
- "id",
577
- "name",
578
- "overrides"
579
- ], at);
580
- return {
581
- id: expectString(state.id, `${at}/id`),
582
- name: expectString(state.name, `${at}/name`),
583
- overrides: optional(state.overrides, (v) => parsePatches(v, `${at}/overrides`, ids))
584
- };
585
- });
586
- if (states.length === 0) throw new TypeError(`${path}/states: expected at least one state`);
587
- const kind = optional(container.kind, (v) => expectOneOf(v, containerKinds, `${path}/kind`));
588
- if (kind === "frame") checkFrame(container, root, states, path);
589
- 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`);
590
- return compact({
591
- id: expectString(container.id, `${path}/id`),
592
- name: expectString(container.name, `${path}/name`),
593
- kind,
594
- context: optional(container.context, (v) => expectArray(v, `${path}/context`).map((item, index) => {
595
- const at = `${path}/context/${index}`;
596
- const wrapper = expectObject(item, at);
597
- expectKnownKeys(wrapper, ["component", "props"], at);
598
- return compact({
599
- component: expectString(wrapper.component, `${at}/component`),
600
- props: optional(wrapper.props, (p) => expectObject(p, `${at}/props`))
601
- });
602
- })),
603
- params: optional(container.params, (v) => {
604
- const params = {};
605
- for (const [name, entry] of Object.entries(expectObject(v, `${path}/params`))) {
606
- const at = `${path}/params/${name}`;
607
- const param = expectObject(entry, at);
608
- expectKnownKeys(param, [
609
- "type",
610
- "default",
611
- "description",
612
- "options"
613
- ], at);
614
- const type = expectOneOf(param.type, paramTypes, `${at}/type`);
615
- const options = optional(param.options, (list) => {
616
- const seen = /* @__PURE__ */ new Set();
617
- return expectArray(list, `${at}/options`).map((item, index) => {
618
- const where = `${at}/options/${index}`;
619
- const option = expectObject(item, where);
620
- expectKnownKeys(option, [
621
- "id",
622
- "name",
623
- "overrides"
624
- ], where);
625
- const id = expectString(option.id, `${where}/id`);
626
- if (seen.has(id)) throw new TypeError(`${where}/id: duplicate option id "${id}"`);
627
- seen.add(id);
628
- return compact({
629
- id,
630
- name: expectString(option.name, `${where}/name`),
631
- overrides: optional(option.overrides, (v) => parsePatches(v, `${where}/overrides`, ids))
632
- });
633
- });
634
- });
635
- if (type === "variant" && !options?.length) throw new TypeError(`${at}/options: a variant needs options`);
636
- if (type !== "variant" && options) throw new TypeError(`${at}/options: only variants have options`);
637
- params[name] = compact({
638
- type,
639
- default: param.default,
640
- description: optional(param.description, (d) => expectString(d, `${at}/description`)),
641
- options
642
- });
643
- }
644
- return params;
645
- }),
646
- frames: optional(container.frames, (v) => {
647
- const frames = {};
648
- for (const [viewport, entry] of Object.entries(expectObject(v, `${path}/frames`))) frames[viewport] = parseFrameSize(entry, `${path}/frames/${viewport}`);
649
- return frames;
650
- }),
651
- size: optional(container.size, (v) => parseFrameSize(v, `${path}/size`)),
652
- position: {
653
- x: expectNumber(position.x, `${path}/position/x`),
654
- y: expectNumber(position.y, `${path}/position/y`)
655
- },
656
- root,
657
- states,
658
- view
659
- });
660
- }
661
- function parseFrameSize(value, path) {
662
- const size = expectObject(value, path);
663
- expectKnownKeys(size, ["width", "height"], path);
664
- return compact({
665
- width: optional(size.width, (w) => expectNumber(w, `${path}/width`)),
666
- height: optional(size.height, (h) => expectNumber(h, `${path}/height`))
667
- });
696
+ name,
697
+ "options",
698
+ index,
699
+ "overrides"
700
+ ]);
701
+ });
702
+ /**
703
+ * A design's patches keyed by its own nodes. A key names a node of the design, or starts with one: `instance/inner`
704
+ * reaches inside an instance, which can't be checked without the component.
705
+ */
706
+ function checkPatchKeys(patches, ids, context, path) {
707
+ for (const key of Object.keys(patches ?? {})) {
708
+ const [node] = key.split("/");
709
+ if (!ids.has(node)) problem(context, [...path, key], "no node has this id");
710
+ }
668
711
  }
669
712
  /**
670
713
  * What a frame may hold: a layout at its root and one state, drawn as its layers are, with no params, no view (it
671
714
  * shows only the primary mode and that state) and no sizes per viewport, since it has one of its own.
672
715
  */
673
- function checkFrame(container, root, states, path) {
674
- if (root.type !== "layout") throw new TypeError(`${path}/root: a frame's root is a layout`);
675
- if (states.length > 1) throw new TypeError(`${path}/states: a frame has one state`);
676
- if (Object.keys(states[0].overrides ?? {}).length > 0) throw new TypeError(`${path}/states/0/overrides: a frame is drawn as its layers are`);
677
- if (container.params !== void 0) throw new TypeError(`${path}/params: a frame has no params`);
678
- if (container.view !== void 0) throw new TypeError(`${path}/view: a frame shows only the primary mode and its one state`);
679
- if (container.frames !== void 0) throw new TypeError(`${path}/frames: a frame's size is in size, not per viewport`);
716
+ function checkFrame(frame, context) {
717
+ if (frame.root.type !== "layout") problem(context, ["root"], "a frame's root is a layout");
718
+ if (frame.states.length > 1) problem(context, ["states"], "a frame has one state");
719
+ if (Object.keys(frame.states[0]?.overrides ?? {}).length > 0) problem(context, [
720
+ "states",
721
+ 0,
722
+ "overrides"
723
+ ], "a frame is drawn as its layers are");
724
+ if (frame.params !== void 0) problem(context, ["params"], "a frame has no params");
725
+ if (frame.view !== void 0) problem(context, ["view"], "a frame shows only the primary mode and its one state");
726
+ if (frame.frames !== void 0) problem(context, ["frames"], "a frame's size is in size, not per viewport");
680
727
  }
681
- function parseNode(value, path, ids) {
682
- const node = expectObject(value, path);
683
- const type = expectOneOf(node.type, [
684
- "layout",
685
- "component",
686
- "text",
687
- "instance",
688
- "image"
689
- ], `${path}/type`);
690
- const id = expectString(node.id, `${path}/id`);
691
- if (ids.has(id)) throw new TypeError(`${path}/id: duplicate node id "${id}"`);
692
- ids.add(id);
693
- const base = compact({
694
- id,
695
- name: optional(node.name, (v) => expectString(v, `${path}/name`)),
696
- hidden: optional(node.hidden, (v) => expectBoolean(v, `${path}/hidden`)),
697
- style: optional(node.style, (v) => parseStyle(v, `${path}/style`)),
698
- responsive: optional(node.responsive, (v) => parsePatches(v, `${path}/responsive`)),
699
- bind: optional(node.bind, (v) => parseBinding(v, `${path}/bind`))
728
+ /** A rule's problem with the value it checks, at `path` inside that value. */
729
+ function problem(context, path, message) {
730
+ context.addIssue({
731
+ code: "custom",
732
+ message,
733
+ path
700
734
  });
701
- const common = [
702
- "type",
703
- "id",
704
- "name",
705
- "hidden",
706
- "style",
707
- "responsive",
708
- "bind"
709
- ];
710
- switch (type) {
711
- case "layout":
712
- expectKnownKeys(node, [...common, "children"], path);
713
- return {
714
- ...base,
715
- type,
716
- children: expectArray(node.children, `${path}/children`).map((child, index) => parseNode(child, `${path}/children/${index}`, ids))
717
- };
718
- case "component": {
719
- expectKnownKeys(node, [
720
- ...common,
721
- "component",
722
- "props",
723
- "slots"
724
- ], path);
725
- const slots = optional(node.slots, (v) => {
726
- const result = {};
727
- for (const [slot, children] of Object.entries(expectObject(v, `${path}/slots`))) result[slot] = expectArray(children, `${path}/slots/${slot}`).map((child, index) => parseNode(child, `${path}/slots/${slot}/${index}`, ids));
728
- return result;
729
- });
730
- return {
731
- ...base,
732
- type,
733
- component: expectString(node.component, `${path}/component`),
734
- props: optional(node.props, (v) => expectObject(v, `${path}/props`)),
735
- slots
736
- };
737
- }
738
- case "instance":
739
- expectKnownKeys(node, [
740
- ...common,
741
- "design",
742
- "args",
743
- "state",
744
- "overrides"
745
- ], path);
746
- return compact({
747
- ...base,
748
- type,
749
- design: expectString(node.design, `${path}/design`),
750
- args: optional(node.args, (v) => expectObject(v, `${path}/args`)),
751
- state: optional(node.state, (v) => expectString(v, `${path}/state`)),
752
- overrides: optional(node.overrides, (v) => parsePatches(v, `${path}/overrides`))
753
- });
754
- case "text":
755
- expectKnownKeys(node, [
756
- ...common,
757
- "text",
758
- "tag"
759
- ], path);
760
- return {
761
- ...base,
762
- type,
763
- text: expectString(node.text, `${path}/text`),
764
- tag: optional(node.tag, (v) => expectOneOf(v, textTags, `${path}/tag`))
765
- };
766
- case "image":
767
- expectKnownKeys(node, [
768
- ...common,
769
- "src",
770
- "alt",
771
- "fit",
772
- "credit"
773
- ], path);
774
- return compact({
775
- ...base,
776
- type,
777
- src: expectString(node.src, `${path}/src`),
778
- alt: optional(node.alt, (v) => expectString(v, `${path}/alt`)),
779
- fit: optional(node.fit, (v) => expectOneOf(v, imageFits, `${path}/fit`)),
780
- credit: optional(node.credit, (v) => {
781
- const credit = expectObject(v, `${path}/credit`);
782
- expectKnownKeys(credit, [
783
- "name",
784
- "url",
785
- "source"
786
- ], `${path}/credit`);
787
- return compact({
788
- name: expectString(credit.name, `${path}/credit/name`),
789
- url: expectString(credit.url, `${path}/credit/url`),
790
- source: optional(credit.source, (s) => expectString(s, `${path}/credit/source`))
791
- });
792
- })
793
- });
794
- }
795
735
  }
796
- function parseBinding(value, path) {
797
- const binding = expectObject(value, path);
798
- expectKnownKeys(binding, [
799
- "text",
800
- "props",
801
- "visible"
802
- ], path);
803
- return compact({
804
- text: optional(binding.text, (v) => expectString(v, `${path}/text`)),
805
- visible: optional(binding.visible, (v) => expectString(v, `${path}/visible`)),
806
- props: optional(binding.props, (v) => {
807
- const props = {};
808
- for (const [prop, param] of Object.entries(expectObject(v, `${path}/props`))) props[prop] = expectString(param, `${path}/props/${prop}`);
809
- return props;
810
- })
811
- });
736
+ const canvasDocument = fileObject({
737
+ version: z.literal(3),
738
+ containers: z.array(containerSpec),
739
+ /** The project's modes. Unset means the defaults. */
740
+ modes: modeSettings.optional(),
741
+ /** Text placed on the canvas itself, outside any design: annotations, flow labels, explanations. */
742
+ notes: z.array(noteSpec).optional(),
743
+ /** Where the person corrected which exports are components. Unset while detection gets them right. */
744
+ detection: detectionCorrections.optional(),
745
+ /** Where the person put tables, operations and routes on the canvas. The models are read from the project. */
746
+ objects: canvasObjects.optional(),
747
+ /** Links the person drew by hand. Links found in code or traces are read each time and never saved. */
748
+ links: linkSpecs.optional(),
749
+ /** The source each lens reads, where the person switched from the one Parity picked. */
750
+ sources: lensSources.optional()
751
+ });
752
+ /** Reads a project file, or throws `TypeError` with the first problem as `/json/pointer: message`. */
753
+ function parseCanvasDocument(value) {
754
+ return parseData(canvasDocument, value);
812
755
  }
813
- /**
814
- * Patches keyed by node id. With `ids`, each key must name a node of the design, or start with one: `instance/inner`
815
- * reaches inside an instance, which can't be checked without the component.
816
- */
817
- function parsePatches(value, path, ids) {
818
- const patches = {};
819
- for (const [key, entry] of Object.entries(expectObject(value, path))) {
820
- if (ids && !ids.has(key.split("/")[0])) throw new TypeError(`${path}/${key}: no node has this id`);
821
- patches[key] = parsePatch(entry, `${path}/${key}`);
822
- }
823
- return patches;
756
+ // ../core/src/validate.ts
757
+ function expectObject(value, path) {
758
+ if (typeof value !== "object" || value === null || Array.isArray(value)) throw new TypeError(`${path || "/"}: expected an object`);
759
+ return value;
824
760
  }
825
- function parsePatch(value, path) {
826
- const patch = expectObject(value, path);
827
- expectKnownKeys(patch, [
828
- "hidden",
829
- "text",
830
- "props",
831
- "style",
832
- "unset"
833
- ], path);
834
- return compact({
835
- hidden: optional(patch.hidden, (v) => expectBoolean(v, `${path}/hidden`)),
836
- text: optional(patch.text, (v) => expectString(v, `${path}/text`)),
837
- props: optional(patch.props, (v) => expectObject(v, `${path}/props`)),
838
- style: optional(patch.style, (v) => parseStyle(v, `${path}/style`)),
839
- unset: optional(patch.unset, (v) => parseUnset(v, `${path}/unset`))
840
- });
761
+ function expectString(value, path) {
762
+ if (typeof value !== "string") throw new TypeError(`${path}: expected a string`);
763
+ return value;
841
764
  }
842
- function parseUnset(value, path) {
843
- const unset = expectObject(value, path);
844
- expectKnownKeys(unset, ["props", "style"], path);
845
- const keys = (key, item) => optional(unset[key], (v) => expectArray(v, `${path}/${key}`).map((entry, index) => item(entry, `${path}/${key}/${index}`)));
846
- return compact({
847
- props: keys("props", expectString),
848
- style: keys("style", (v, where) => expectOneOf(v, styleKeys, where))
849
- });
765
+ function expectNumber(value, path) {
766
+ if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${path}: expected a number`);
767
+ return value;
850
768
  }
851
- function parseStyle(value, path) {
852
- const style = expectObject(value, path);
853
- const at = (key) => `${path}/${key}`;
854
- const token = (key) => optional(style[key], (v) => expectString(v, at(key)));
855
- const number = (key) => optional(style[key], (v) => expectNumber(v, at(key)));
856
- const flag = (key) => optional(style[key], (v) => expectBoolean(v, at(key)));
857
- const lengthAt = (v, where) => typeof v === "number" ? v : expectString(v, where);
858
- const length = (key) => optional(style[key], (v) => lengthAt(v, at(key)));
859
- const size = (key) => optional(style[key], (v) => lengthAt(v, at(key)));
860
- const sides = (key) => optional(style[key], (v) => {
861
- if (!Array.isArray(v)) return lengthAt(v, at(key));
862
- if (v.length !== 2 && v.length !== 4) throw new TypeError(`${at(key)}: expected a length, [vertical, horizontal] or [top, right, bottom, left]`);
863
- return v.map((side, index) => lengthAt(side, `${at(key)}/${index}`));
864
- });
865
- expectKnownKeys(style, styleKeys, path);
866
- return compact({
867
- display: optional(style.display, (v) => expectOneOf(v, ["flex", "grid"], at("display"))),
868
- direction: optional(style.direction, (v) => expectOneOf(v, ["row", "column"], at("direction"))),
869
- columns: number("columns"),
870
- rows: number("rows"),
871
- gap: length("gap"),
872
- rowGap: length("rowGap"),
873
- padding: sides("padding"),
874
- margin: sides("margin"),
875
- align: optional(style.align, (v) => expectOneOf(v, alignments, at("align"))),
876
- justify: optional(style.justify, (v) => expectOneOf(v, justifications, at("justify"))),
877
- wrap: flag("wrap"),
878
- width: size("width"),
879
- height: size("height"),
880
- minWidth: length("minWidth"),
881
- maxWidth: length("maxWidth"),
882
- minHeight: length("minHeight"),
883
- maxHeight: length("maxHeight"),
884
- colSpan: number("colSpan"),
885
- rowSpan: number("rowSpan"),
886
- grow: flag("grow"),
887
- self: optional(style.self, (v) => expectOneOf(v, alignments, at("self"))),
888
- position: optional(style.position, (v) => expectOneOf(v, ["absolute"], at("position"))),
889
- top: length("top"),
890
- right: length("right"),
891
- bottom: length("bottom"),
892
- left: length("left"),
893
- background: token("background"),
894
- foreground: token("foreground"),
895
- border: token("border"),
896
- borderWidth: number("borderWidth"),
897
- borderStyle: optional(style.borderStyle, (v) => expectOneOf(v, [
898
- "solid",
899
- "dashed",
900
- "dotted"
901
- ], at("borderStyle"))),
902
- radius: token("radius"),
903
- shadow: token("shadow"),
904
- opacity: number("opacity"),
905
- overflow: optional(style.overflow, (v) => expectOneOf(v, [
906
- "visible",
907
- "hidden",
908
- "scroll"
909
- ], at("overflow"))),
910
- font: token("font"),
911
- size: token("size"),
912
- weight: number("weight"),
913
- leading: token("leading"),
914
- tracking: token("tracking"),
915
- textAlign: optional(style.textAlign, (v) => expectOneOf(v, [
916
- "start",
917
- "center",
918
- "end",
919
- "justify"
920
- ], at("textAlign"))),
921
- transform: optional(style.transform, (v) => expectOneOf(v, [
922
- "uppercase",
923
- "lowercase",
924
- "capitalize"
925
- ], at("transform"))),
926
- decoration: optional(style.decoration, (v) => expectOneOf(v, ["underline", "line-through"], at("decoration"))),
927
- italic: flag("italic"),
928
- lines: number("lines")
929
- });
769
+ function expectArray(value, path) {
770
+ if (!Array.isArray(value)) throw new TypeError(`${path}: expected an array`);
771
+ return value;
772
+ }
773
+ function expectOneOf(value, options, path) {
774
+ if (typeof value !== "string" || !options.includes(value)) throw new TypeError(`${path}: expected one of ${options.map((option) => `"${option}"`).join(", ")}`);
775
+ return value;
776
+ }
777
+ function optional(value, parse) {
778
+ return value === void 0 ? void 0 : parse(value);
779
+ }
780
+ /** Drops `undefined` entries, so parsed patches can be spread over defaults without erasing them. */
781
+ function compact(value) {
782
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== void 0));
783
+ }
784
+ function expectKnownKeys(value, keys, path) {
785
+ for (const key of Object.keys(value)) if (!keys.includes(key)) throw new TypeError(`${path}/${key}: unknown property`);
930
786
  }
931
787
  // ../core/src/library.ts
932
788
  /** How a component was recognised, which is also why it's believed to be one. */
@@ -1341,7 +1197,8 @@ function expectStringRecord(value, path) {
1341
1197
  const named = /* @__PURE__ */ new WeakSet();
1342
1198
  /**
1343
1199
  * A new node for a component, ready to show something: required props get their default or first option, and a
1344
- * component that takes children gets its own name as text, so it isn't an empty box, unless `given` has its own.
1200
+ * component that takes children gets its own name as text, so it isn't an empty box, unless `given` has its own or the
1201
+ * component sets its own size (`fixedSize`), where the name would be cut off ("Avata").
1345
1202
  */
1346
1203
  function componentNode(component, info, taken, given = {}) {
1347
1204
  const props = { ...given };
@@ -1353,7 +1210,7 @@ function componentNode(component, info, taken, given = {}) {
1353
1210
  else if (prop.kind === "number") props[prop.name] = 0;
1354
1211
  else if (prop.kind === "string") props[prop.name] = prop.name;
1355
1212
  }
1356
- const labelled = !("children" in given) && info?.props.some((prop) => prop.name === "children" && prop.kind === "node");
1213
+ const labelled = !("children" in given) && !info?.fixedSize && info?.props.some((prop) => prop.name === "children" && prop.kind === "node");
1357
1214
  if (labelled) props.children = component.split("/").at(-1) ?? component;
1358
1215
  const node = {
1359
1216
  type: "component",
@@ -1434,10 +1291,10 @@ function withLibraryContext(node, sources, around, taken) {
1434
1291
  };
1435
1292
  };
1436
1293
  let placed = node;
1437
- const sample = source.sample;
1294
+ const sample = source.sample ?? (named.has(node) ? usedIn(node.component, sources) : void 0);
1438
1295
  if (sample) {
1439
1296
  const given = { ...node.props };
1440
- if (sample.props && "children" in sample.props && named.has(node)) delete given.children;
1297
+ if (named.has(node)) delete given.children;
1441
1298
  const { props, slots } = held({
1442
1299
  ...sample.props,
1443
1300
  ...given
@@ -1514,6 +1371,39 @@ function withLibraryContext(node, sources, around, taken) {
1514
1371
  }
1515
1372
  }
1516
1373
  /**
1374
+ * How a part with no sample of its own is used in its library's other samples: with the children the first use gives it,
1375
+ * or none when it's used without any (`<MarkerIcon />`) or with elements the sample can't show (an icon, `[]`).
1376
+ * Undefined when no sample uses it.
1377
+ */
1378
+ function usedIn(component, sources) {
1379
+ const find = (children) => {
1380
+ for (const child of children ?? []) {
1381
+ if (typeof child === "string") continue;
1382
+ if (isSampleRender(child)) {
1383
+ const found = find(child.$render);
1384
+ if (found) return found;
1385
+ continue;
1386
+ }
1387
+ if (child.component === component) return child;
1388
+ const found = find(child.children) ?? find(elementProps(child.props));
1389
+ if (found) return found;
1390
+ }
1391
+ };
1392
+ const library = sources[component]?.library;
1393
+ for (const source of Object.values(sources)) {
1394
+ if (source.library !== library || !source.sample) continue;
1395
+ const found = find(source.sample.children) ?? find(elementProps(source.sample.props));
1396
+ if (found) return { children: found.children ?? [] };
1397
+ }
1398
+ }
1399
+ /** The elements and render functions a sample gives as props' values, as children are. */
1400
+ function elementProps(props) {
1401
+ return Object.values(props ?? {}).flatMap((value) => {
1402
+ if (isSampleRender(value)) return [value];
1403
+ return isSampleElement(value) ? value.$element : [];
1404
+ });
1405
+ }
1406
+ /**
1517
1407
  * A library file's component as the canvas inserts it, with its parents and sample named by component id.
1518
1408
  * Discovery has already rewritten module imports for the name the project installed the package under.
1519
1409
  */
@@ -1564,4 +1454,4 @@ function sampleIds(sample, library) {
1564
1454
  ...sample.children ? { children: sample.children.map(child) } : {}
1565
1455
  };
1566
1456
  }
1567
- 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 };
1457
+ export { deviceKinds as A, walkNodes as C, remProperty as D, collectCustomProperties as E, packageManifest as M, viewportHeightProperty as O, parseCanvasDocument as S, modeSettings as T, imageFits as _, applyLibraryEdits as a, linkTypes 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, linkAllowed as v, generateId as w, paramTypes as x, linkEndKinds as y };