@compact-design/core 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -7254,6 +7254,371 @@ function validationIssues(errors) {
7254
7254
  });
7255
7255
  }
7256
7256
 
7257
+ // src/patch-keys.ts
7258
+ var PATCH_SET_EXCLUDED_NODE_KEYS = ["id", "type", "children", "coordinateMode"];
7259
+ var PATCH_SET_KEYS = [
7260
+ "name",
7261
+ "x",
7262
+ "y",
7263
+ "w",
7264
+ "h",
7265
+ "rotation",
7266
+ "fill",
7267
+ "fills",
7268
+ "stroke",
7269
+ "strokes",
7270
+ "strokeWeight",
7271
+ "strokeTopWeight",
7272
+ "strokeRightWeight",
7273
+ "strokeBottomWeight",
7274
+ "strokeLeftWeight",
7275
+ "strokeAlign",
7276
+ "strokeCap",
7277
+ "strokeJoin",
7278
+ "dashPattern",
7279
+ "cornerRadius",
7280
+ "cornerRadii",
7281
+ "opacity",
7282
+ "blendMode",
7283
+ "visible",
7284
+ "locked",
7285
+ "isMask",
7286
+ "clipsContent",
7287
+ "effects",
7288
+ "elevation",
7289
+ "shadow",
7290
+ "layout",
7291
+ "constraints",
7292
+ "layoutSizingHorizontal",
7293
+ "layoutSizingVertical",
7294
+ "layoutAlign",
7295
+ "layoutGrow",
7296
+ "layoutPositioning",
7297
+ "minWidth",
7298
+ "maxWidth",
7299
+ "minHeight",
7300
+ "maxHeight",
7301
+ "layoutGrids",
7302
+ "text",
7303
+ "font",
7304
+ "lineHeight",
7305
+ "letterSpacing",
7306
+ "align",
7307
+ "verticalAlignment",
7308
+ "textDecoration",
7309
+ "textCase",
7310
+ "paragraphSpacing",
7311
+ "paragraphIndent",
7312
+ "listSpacing",
7313
+ "hangingPunctuation",
7314
+ "hangingList",
7315
+ "textAutoResize",
7316
+ "textTruncation",
7317
+ "maxLines",
7318
+ "runs",
7319
+ "pointCount",
7320
+ "innerRadius",
7321
+ "startingAngle",
7322
+ "endingAngle",
7323
+ "innerRadiusRatio",
7324
+ "svg",
7325
+ "vectorPaths",
7326
+ "componentId",
7327
+ "componentProperties",
7328
+ "instanceProperties",
7329
+ "componentPropertyReferences",
7330
+ "variantAxes",
7331
+ "variant",
7332
+ "operation",
7333
+ "prototype",
7334
+ "overflowDirection",
7335
+ "numberOfFixedChildren",
7336
+ "styleRefs",
7337
+ "bindings",
7338
+ "variableModes"
7339
+ ];
7340
+ var PATCH_SET_SEMANTICS = {
7341
+ name: "scalar",
7342
+ x: "scalar",
7343
+ y: "scalar",
7344
+ w: "scalar",
7345
+ h: "scalar",
7346
+ rotation: "scalar",
7347
+ fill: "replace",
7348
+ fills: "replace",
7349
+ stroke: "replace",
7350
+ strokes: "replace",
7351
+ strokeWeight: "scalar",
7352
+ strokeTopWeight: "scalar",
7353
+ strokeRightWeight: "scalar",
7354
+ strokeBottomWeight: "scalar",
7355
+ strokeLeftWeight: "scalar",
7356
+ strokeAlign: "scalar",
7357
+ strokeCap: "scalar",
7358
+ strokeJoin: "scalar",
7359
+ dashPattern: "replace",
7360
+ cornerRadius: "scalar",
7361
+ cornerRadii: "replace",
7362
+ opacity: "scalar",
7363
+ blendMode: "scalar",
7364
+ visible: "scalar",
7365
+ locked: "scalar",
7366
+ isMask: "scalar",
7367
+ clipsContent: "scalar",
7368
+ effects: "replace",
7369
+ elevation: "replace",
7370
+ shadow: "replace",
7371
+ layout: "merge",
7372
+ constraints: "merge",
7373
+ layoutSizingHorizontal: "scalar",
7374
+ layoutSizingVertical: "scalar",
7375
+ layoutAlign: "scalar",
7376
+ layoutGrow: "scalar",
7377
+ layoutPositioning: "scalar",
7378
+ minWidth: "scalar",
7379
+ maxWidth: "scalar",
7380
+ minHeight: "scalar",
7381
+ maxHeight: "scalar",
7382
+ layoutGrids: "replace",
7383
+ text: "scalar",
7384
+ font: "merge",
7385
+ lineHeight: "scalar",
7386
+ letterSpacing: "scalar",
7387
+ align: "scalar",
7388
+ verticalAlignment: "scalar",
7389
+ textDecoration: "scalar",
7390
+ textCase: "scalar",
7391
+ paragraphSpacing: "scalar",
7392
+ paragraphIndent: "scalar",
7393
+ listSpacing: "scalar",
7394
+ hangingPunctuation: "scalar",
7395
+ hangingList: "scalar",
7396
+ textAutoResize: "scalar",
7397
+ textTruncation: "scalar",
7398
+ maxLines: "scalar",
7399
+ runs: "replace",
7400
+ pointCount: "scalar",
7401
+ innerRadius: "scalar",
7402
+ startingAngle: "scalar",
7403
+ endingAngle: "scalar",
7404
+ innerRadiusRatio: "scalar",
7405
+ svg: "immutable",
7406
+ vectorPaths: "replace",
7407
+ componentId: "deferred",
7408
+ componentProperties: "merge",
7409
+ instanceProperties: "merge",
7410
+ componentPropertyReferences: "merge",
7411
+ variantAxes: "merge",
7412
+ variant: "merge",
7413
+ operation: "scalar",
7414
+ prototype: "replace",
7415
+ overflowDirection: "scalar",
7416
+ numberOfFixedChildren: "scalar",
7417
+ styleRefs: "merge",
7418
+ bindings: "merge",
7419
+ variableModes: "merge"
7420
+ };
7421
+ var PATCH_SET_DEFERRED_KEYS = PATCH_SET_KEYS.filter((key) => PATCH_SET_SEMANTICS[key] === "deferred");
7422
+ var ALL = null;
7423
+ var FRAME_LIKE = ["FRAME", "COMPONENT", "COMPONENT_SET", "INSTANCE"];
7424
+ var AUTO_LAYOUT_CAPABLE = ["FRAME", "COMPONENT"];
7425
+ var STROKABLE = ["FRAME", "COMPONENT", "COMPONENT_SET", "INSTANCE", "RECTANGLE", "ELLIPSE", "ARC", "LINE", "POLYGON", "STAR", "VECTOR", "TEXT", "BOOLEAN_OPERATION"];
7426
+ var FILLABLE = [...STROKABLE, "SECTION"];
7427
+ var BLENDABLE = [...STROKABLE, "GROUP"];
7428
+ var LAYOUT_CHILD = [...STROKABLE, "GROUP"];
7429
+ var TEXT_ONLY = ["TEXT"];
7430
+ var PATCH_SET_APPLIES_TO = {
7431
+ name: ALL,
7432
+ x: ALL,
7433
+ y: ALL,
7434
+ rotation: [...BLENDABLE],
7435
+ w: [...STROKABLE, "SECTION", "SLICE"],
7436
+ h: [...STROKABLE, "SECTION", "SLICE"],
7437
+ fill: FILLABLE,
7438
+ fills: FILLABLE,
7439
+ stroke: STROKABLE,
7440
+ strokes: STROKABLE,
7441
+ strokeWeight: STROKABLE,
7442
+ strokeTopWeight: FRAME_LIKE.concat("RECTANGLE"),
7443
+ strokeRightWeight: FRAME_LIKE.concat("RECTANGLE"),
7444
+ strokeBottomWeight: FRAME_LIKE.concat("RECTANGLE"),
7445
+ strokeLeftWeight: FRAME_LIKE.concat("RECTANGLE"),
7446
+ strokeAlign: STROKABLE,
7447
+ strokeCap: ["LINE", "VECTOR", "ELLIPSE", "ARC"],
7448
+ strokeJoin: STROKABLE,
7449
+ dashPattern: STROKABLE,
7450
+ cornerRadius: [...FRAME_LIKE, "RECTANGLE", "POLYGON", "STAR", "VECTOR"],
7451
+ cornerRadii: [...FRAME_LIKE, "RECTANGLE"],
7452
+ opacity: BLENDABLE,
7453
+ blendMode: BLENDABLE,
7454
+ visible: ALL,
7455
+ locked: ALL,
7456
+ isMask: BLENDABLE,
7457
+ clipsContent: FRAME_LIKE,
7458
+ effects: BLENDABLE,
7459
+ elevation: BLENDABLE,
7460
+ shadow: BLENDABLE,
7461
+ layout: AUTO_LAYOUT_CAPABLE,
7462
+ constraints: STROKABLE,
7463
+ layoutSizingHorizontal: LAYOUT_CHILD,
7464
+ layoutSizingVertical: LAYOUT_CHILD,
7465
+ layoutAlign: LAYOUT_CHILD,
7466
+ layoutGrow: LAYOUT_CHILD,
7467
+ layoutPositioning: LAYOUT_CHILD,
7468
+ minWidth: LAYOUT_CHILD,
7469
+ maxWidth: LAYOUT_CHILD,
7470
+ minHeight: LAYOUT_CHILD,
7471
+ maxHeight: LAYOUT_CHILD,
7472
+ layoutGrids: AUTO_LAYOUT_CAPABLE,
7473
+ text: TEXT_ONLY,
7474
+ font: TEXT_ONLY,
7475
+ lineHeight: TEXT_ONLY,
7476
+ letterSpacing: TEXT_ONLY,
7477
+ align: TEXT_ONLY,
7478
+ verticalAlignment: TEXT_ONLY,
7479
+ textDecoration: TEXT_ONLY,
7480
+ textCase: TEXT_ONLY,
7481
+ paragraphSpacing: TEXT_ONLY,
7482
+ paragraphIndent: TEXT_ONLY,
7483
+ listSpacing: TEXT_ONLY,
7484
+ hangingPunctuation: TEXT_ONLY,
7485
+ hangingList: TEXT_ONLY,
7486
+ textAutoResize: TEXT_ONLY,
7487
+ textTruncation: TEXT_ONLY,
7488
+ maxLines: TEXT_ONLY,
7489
+ runs: TEXT_ONLY,
7490
+ pointCount: ["POLYGON", "STAR"],
7491
+ innerRadius: ["STAR"],
7492
+ startingAngle: ["ELLIPSE", "ARC"],
7493
+ endingAngle: ["ELLIPSE", "ARC"],
7494
+ innerRadiusRatio: ["ELLIPSE", "ARC"],
7495
+ svg: ALL,
7496
+ vectorPaths: ["VECTOR"],
7497
+ componentId: ALL,
7498
+ componentProperties: ["COMPONENT"],
7499
+ instanceProperties: ["INSTANCE"],
7500
+ componentPropertyReferences: ALL,
7501
+ variantAxes: ["COMPONENT_SET"],
7502
+ variant: ["COMPONENT"],
7503
+ operation: ["BOOLEAN_OPERATION"],
7504
+ prototype: ALL,
7505
+ overflowDirection: AUTO_LAYOUT_CAPABLE,
7506
+ numberOfFixedChildren: AUTO_LAYOUT_CAPABLE,
7507
+ styleRefs: ALL,
7508
+ bindings: ALL,
7509
+ variableModes: ALL
7510
+ };
7511
+ var PATCH_TEXT_STYLE_KEYS = ["font", "lineHeight", "letterSpacing", "paragraphSpacing", "paragraphIndent", "listSpacing", "textCase", "textDecoration", "hangingPunctuation", "hangingList"];
7512
+ var PATCH_SET_KEY_SET = new Set(PATCH_SET_KEYS);
7513
+ function isPatchSetKey(key) {
7514
+ return PATCH_SET_KEY_SET.has(key);
7515
+ }
7516
+ var isObject = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
7517
+ var GROUP_TYPES = /* @__PURE__ */ new Set(["GROUP", "BOOLEAN_OPERATION"]);
7518
+ var PADDING_FIELDS = { left: "paddingLeft", top: "paddingTop", right: "paddingRight", bottom: "paddingBottom" };
7519
+ function patchSetBindingFields(set) {
7520
+ const fields = [];
7521
+ const direct = {
7522
+ w: ["width"],
7523
+ h: ["height"],
7524
+ opacity: ["opacity"],
7525
+ visible: ["visible"],
7526
+ text: ["characters"],
7527
+ cornerRadius: ["cornerRadius", "topLeftRadius", "topRightRadius", "bottomLeftRadius", "bottomRightRadius"],
7528
+ cornerRadii: ["topLeftRadius", "topRightRadius", "bottomLeftRadius", "bottomRightRadius"],
7529
+ strokeWeight: ["strokeWeight"],
7530
+ strokeTopWeight: ["strokeTopWeight"],
7531
+ strokeRightWeight: ["strokeRightWeight"],
7532
+ strokeBottomWeight: ["strokeBottomWeight"],
7533
+ strokeLeftWeight: ["strokeLeftWeight"],
7534
+ minWidth: ["minWidth"],
7535
+ maxWidth: ["maxWidth"],
7536
+ minHeight: ["minHeight"],
7537
+ maxHeight: ["maxHeight"],
7538
+ paragraphSpacing: ["paragraphSpacing"],
7539
+ paragraphIndent: ["paragraphIndent"],
7540
+ letterSpacing: ["letterSpacing"],
7541
+ lineHeight: ["lineHeight"]
7542
+ };
7543
+ for (const [key, mapped] of Object.entries(direct)) if (key in set) fields.push(...mapped);
7544
+ if (isObject(set.layout)) {
7545
+ if ("itemSpacing" in set.layout) fields.push("itemSpacing");
7546
+ if ("counterAxisSpacing" in set.layout) fields.push("counterAxisSpacing");
7547
+ if (isObject(set.layout.padding)) {
7548
+ for (const side of Object.keys(set.layout.padding)) if (PADDING_FIELDS[side]) fields.push(PADDING_FIELDS[side]);
7549
+ }
7550
+ }
7551
+ if (isObject(set.font)) {
7552
+ if ("size" in set.font) fields.push("fontSize");
7553
+ if ("family" in set.font) fields.push("fontFamily");
7554
+ if ("style" in set.font) fields.push("fontStyle");
7555
+ }
7556
+ return fields;
7557
+ }
7558
+ function patchSetShapeIssues(set) {
7559
+ const issues = [];
7560
+ for (const key of Object.keys(set)) {
7561
+ if (!isPatchSetKey(key)) {
7562
+ issues.push(`${key}: unknown set key '${key}'. Settable keys: ${PATCH_SET_KEYS.join(", ")}.`);
7563
+ continue;
7564
+ }
7565
+ const semantics = PATCH_SET_SEMANTICS[key];
7566
+ if (semantics === "deferred") issues.push(`${key}: '${key}' cannot be patched yet (planned follow-up). Re-import the node, or remove and insert it.`);
7567
+ if (semantics === "immutable") issues.push(`${key}: '${key}' cannot be patched in place. Remove the node and insert a new one.`);
7568
+ }
7569
+ for (const key of ["font", "layout", "constraints"]) {
7570
+ if (key in set && (!isObject(set[key]) || !Object.keys(set[key]).length)) issues.push(`${key}: '${key}' must be a non-empty object.`);
7571
+ }
7572
+ if (isObject(set.layout) && "padding" in set.layout && (!isObject(set.layout.padding) || !Object.keys(set.layout.padding).length)) issues.push("layout.padding: padding must be a non-empty object.");
7573
+ if ("fill" in set && "fills" in set) issues.push("fills: set either fill or fills, not both.");
7574
+ if ("stroke" in set && "strokes" in set) issues.push("strokes: set either stroke or strokes, not both.");
7575
+ if (isObject(set.layout) && set.layout.counterAxisAlignItems === "STRETCH") issues.push("layout.counterAxisAlignItems: STRETCH cannot be patched yet (Figma applies it per child at import). Set layoutAlign: STRETCH on the children instead.");
7576
+ if (Array.isArray(set.runs) && set.text === void 0 && set.runs.some((run) => !isObject(run) || typeof run.text !== "string")) issues.push("runs: without text, every run must carry its own text so the content is defined.");
7577
+ return issues;
7578
+ }
7579
+ function patchSetTargetIssues(set, context) {
7580
+ const issues = patchSetShapeIssues(set);
7581
+ for (const key of Object.keys(set)) {
7582
+ if (!isPatchSetKey(key)) continue;
7583
+ const types = PATCH_SET_APPLIES_TO[key];
7584
+ if (types && !types.includes(context.type)) issues.push(`${key}: '${key}' does not apply to ${context.type} nodes.`);
7585
+ }
7586
+ const layout = isObject(set.layout) ? set.layout : null;
7587
+ const autoLayoutAfter = context.autoLayout || Boolean(layout && typeof layout.direction === "string");
7588
+ const positioningAfter = typeof set.layoutPositioning === "string" ? set.layoutPositioning : context.layoutPositioning;
7589
+ if (("x" in set || "y" in set) && context.parentType && GROUP_TYPES.has(context.parentType)) {
7590
+ issues.push(`${"x" in set ? "x" : "y"}: children of a ${context.parentType} cannot be positioned by patch (the group derives its bounds from its children). Set x/y on the ${context.parentType} instead, or move the node out first.`);
7591
+ }
7592
+ if (("x" in set || "y" in set) && context.parentAutoLayout && positioningAfter !== "ABSOLUTE") {
7593
+ issues.push(`${"x" in set ? "x" : "y"}: the parent uses Auto Layout, which positions this node. Also set layoutPositioning: "ABSOLUTE" to place it by x/y.`);
7594
+ }
7595
+ if (layout && !context.autoLayout && typeof layout.direction !== "string" && PATCH_SET_APPLIES_TO.layout?.includes(context.type)) {
7596
+ issues.push("layout.direction: the target has no Auto Layout yet; a partial layout must include direction to enable it.");
7597
+ }
7598
+ for (const key of ["layoutAlign", "layoutGrow", "layoutPositioning"]) {
7599
+ if (key in set && !context.parentAutoLayout) issues.push(`${key}: '${key}' only applies to children of an Auto Layout parent.`);
7600
+ }
7601
+ for (const key of ["layoutSizingHorizontal", "layoutSizingVertical"]) {
7602
+ if (set[key] === "FILL" && !context.parentAutoLayout) issues.push(`${key}: FILL requires an Auto Layout parent.`);
7603
+ if (set[key] === "HUG" && context.type !== "TEXT" && !autoLayoutAfter) issues.push(`${key}: HUG requires a TEXT node or an Auto Layout frame.`);
7604
+ }
7605
+ for (const key of ["minWidth", "maxWidth", "minHeight", "maxHeight"]) {
7606
+ if (key in set && !autoLayoutAfter && !context.parentAutoLayout) issues.push(`${key}: '${key}' only applies to Auto Layout frames or their children.`);
7607
+ }
7608
+ if (typeof set.numberOfFixedChildren === "number" && set.numberOfFixedChildren > context.childCount) {
7609
+ issues.push(`numberOfFixedChildren: ${set.numberOfFixedChildren} exceeds the node's ${context.childCount} children.`);
7610
+ }
7611
+ if (context.type === "TEXT") {
7612
+ if (("text" in set || "runs" in set) && context.mixedText) {
7613
+ issues.push(`${"runs" in set ? "runs" : "text"}: the node has per-range text styling; replacing its content by patch is not supported yet. Remove and insert the TEXT node instead.`);
7614
+ }
7615
+ if (isObject(set.font) && context.mixedFontName && "family" in set.font !== "style" in set.font) {
7616
+ issues.push("font: the node mixes fonts across ranges; set font.family and font.style together (font.size alone is fine).");
7617
+ }
7618
+ }
7619
+ return issues;
7620
+ }
7621
+
7257
7622
  // src/normalize.ts
7258
7623
  function hexColor(value) {
7259
7624
  if (typeof value !== "string" || !/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(value)) return null;
@@ -7374,6 +7739,18 @@ function effect(value) {
7374
7739
  visible
7375
7740
  };
7376
7741
  }
7742
+ var OVERFLOW_DIRECTIONS = {
7743
+ NONE: "NONE",
7744
+ HORIZONTAL: "HORIZONTAL",
7745
+ VERTICAL: "VERTICAL",
7746
+ BOTH: "BOTH",
7747
+ HORIZONTAL_SCROLLING: "HORIZONTAL",
7748
+ VERTICAL_SCROLLING: "VERTICAL",
7749
+ HORIZONTAL_AND_VERTICAL_SCROLLING: "BOTH"
7750
+ };
7751
+ function lineHeightValue(value) {
7752
+ return typeof value === "number" ? { unit: "PERCENT", value: value <= 3 ? value * 100 : value } : value;
7753
+ }
7377
7754
  var passthrough = [
7378
7755
  "cornerRadius",
7379
7756
  "cornerRadii",
@@ -7417,6 +7794,7 @@ var passthrough = [
7417
7794
  "innerRadiusRatio",
7418
7795
  "componentProperties",
7419
7796
  "instanceProperties",
7797
+ "componentPropertyReferences",
7420
7798
  "prototype",
7421
7799
  "variantAxes",
7422
7800
  "variant",
@@ -7445,23 +7823,12 @@ function normalizeNode(node, parent, path) {
7445
7823
  if (type === "TEXT" && raw.alignment !== void 0) throw new Error(`${path}.alignment is not part of the current text API; use align.`);
7446
7824
  if (type === "TEXT" && raw.fontSize !== void 0) throw new Error(`${path}.fontSize is not part of the current text API; use font.size.`);
7447
7825
  Object.assign(props, Object.fromEntries(passthrough.filter((key) => raw[key] !== void 0).map((key) => [key, raw[key]])));
7448
- if (typeof raw.overflowDirection === "string") {
7449
- const overflowDirections = {
7450
- NONE: "NONE",
7451
- HORIZONTAL: "HORIZONTAL",
7452
- VERTICAL: "VERTICAL",
7453
- BOTH: "BOTH",
7454
- HORIZONTAL_SCROLLING: "HORIZONTAL",
7455
- VERTICAL_SCROLLING: "VERTICAL",
7456
- HORIZONTAL_AND_VERTICAL_SCROLLING: "BOTH"
7457
- };
7458
- props.overflowDirection = overflowDirections[raw.overflowDirection];
7459
- }
7826
+ if (typeof raw.overflowDirection === "string") props.overflowDirection = OVERFLOW_DIRECTIONS[raw.overflowDirection];
7460
7827
  if (type === "TEXT") {
7461
7828
  const font = raw.font || {};
7462
7829
  props.text = raw.text !== void 0 ? raw.text : (raw.runs || []).map((run) => run.text || "").join("");
7463
7830
  props.font = { family: font.family || "Arial", style: font.style || "Regular", size: font.size || 16 };
7464
- props.lineHeight = typeof raw.lineHeight === "number" ? { unit: "PERCENT", value: raw.lineHeight <= 3 ? raw.lineHeight * 100 : raw.lineHeight } : raw.lineHeight || { unit: "AUTO" };
7831
+ props.lineHeight = raw.lineHeight !== void 0 ? lineHeightValue(raw.lineHeight) : { unit: "AUTO" };
7465
7832
  props.alignment = raw.align || "LEFT";
7466
7833
  for (const key of ["letterSpacing", "textDecoration", "paragraphSpacing", "paragraphIndent", "listSpacing", "hangingPunctuation", "hangingList", "textCase", "verticalAlignment", "textAutoResize", "textTruncation", "maxLines", "runs"]) {
7467
7834
  if (raw[key] !== void 0) Object.assign(props, { [key]: raw[key] });
@@ -7590,6 +7957,37 @@ function normalizeDocument(value) {
7590
7957
  applyStructuredVariants(result.nodes);
7591
7958
  return result;
7592
7959
  }
7960
+ var SET_VALUE_RENAMES = { align: "alignment" };
7961
+ var SET_STRUCTURED_KEYS = /* @__PURE__ */ new Set(["name", "x", "y", "w", "h", "fill", "fills", "stroke", "strokes", "effects", "shadow", "elevation", "lineHeight", "overflowDirection", "font", "constraints", "layout"]);
7962
+ function normalizePatchSet(set, path = "set") {
7963
+ const issues = patchSetShapeIssues(set);
7964
+ if (issues.length) throw new Error(`${path}.${issues[0]}`);
7965
+ const raw = set;
7966
+ const values = {};
7967
+ if (typeof raw.name === "string") values.name = raw.name;
7968
+ if ("x" in raw || "y" in raw) values.position = { ..."x" in raw ? { x: raw.x } : {}, ..."y" in raw ? { y: raw.y } : {} };
7969
+ if ("w" in raw || "h" in raw) values.size = { ..."w" in raw ? { width: raw.w } : {}, ..."h" in raw ? { height: raw.h } : {} };
7970
+ const styles = {};
7971
+ if (raw.fills !== void 0) styles.fills = raw.fills.map(paint);
7972
+ else if (raw.fill !== void 0) styles.fills = [paint(raw.fill)];
7973
+ if (raw.strokes !== void 0) styles.strokes = raw.strokes.map(paint);
7974
+ else if (raw.stroke !== void 0) styles.strokes = [paint(raw.stroke)];
7975
+ if (raw.effects !== void 0 || raw.shadow !== void 0 || raw.elevation !== void 0) styles.effects = effects(raw);
7976
+ if (Object.keys(styles).length) values.styles = styles;
7977
+ if (raw.lineHeight !== void 0) values.lineHeight = lineHeightValue(raw.lineHeight);
7978
+ if (typeof raw.overflowDirection === "string") values.overflowDirection = OVERFLOW_DIRECTIONS[raw.overflowDirection];
7979
+ if (raw.font !== void 0) values.font = { ...raw.font };
7980
+ if (raw.constraints !== void 0) values.constraints = { ...raw.constraints };
7981
+ if (raw.layout !== void 0) {
7982
+ const layout = raw.layout;
7983
+ values.layout = { ...layout, ...layout.padding && typeof layout.padding === "object" ? { padding: { ...layout.padding } } : {} };
7984
+ }
7985
+ for (const key of PATCH_SET_KEYS) {
7986
+ if (SET_STRUCTURED_KEYS.has(key) || raw[key] === void 0) continue;
7987
+ Object.assign(values, { [SET_VALUE_RENAMES[key] || key]: raw[key] });
7988
+ }
7989
+ return values;
7990
+ }
7593
7991
  function isPatchDocument(value) {
7594
7992
  return Boolean(value && typeof value === "object" && !Array.isArray(value) && "patch" in value);
7595
7993
  }
@@ -7599,194 +7997,1544 @@ function normalizePatchDocument(value) {
7599
7997
  if (!Array.isArray(source.patch?.operations) || !source.patch.operations.length) throw new Error("patch.operations must be a non-empty array.");
7600
7998
  const operations = source.patch.operations.map((operation, index) => {
7601
7999
  const op = String(operation.op || "").toUpperCase();
7602
- if (!["SET", "REMOVE", "APPEND"].includes(op)) throw new Error(`patch.operations[${index}].op must be set, remove, or append.`);
7603
- if ((op === "SET" || op === "REMOVE") && typeof operation.id !== "string") throw new Error(`patch.operations[${index}].id is required.`);
8000
+ if (!["SET", "REMOVE", "APPEND", "INSERT", "MOVE", "DUPLICATE"].includes(op)) throw new Error(`patch.operations[${index}].op must be set, remove, append, insert, move, or duplicate.`);
8001
+ if ((op === "SET" || op === "REMOVE" || op === "MOVE" || op === "DUPLICATE") && typeof operation.id !== "string") throw new Error(`patch.operations[${index}].id is required.`);
7604
8002
  if (op === "SET" && (!operation.set || typeof operation.set !== "object" || Array.isArray(operation.set))) throw new Error(`patch.operations[${index}].set is required.`);
7605
8003
  if (op === "APPEND" && (typeof operation.parent !== "string" || !operation.node)) throw new Error(`patch.operations[${index}] requires parent and node.`);
7606
- const result = { op, id: typeof operation.id === "string" ? operation.id : void 0, parent: typeof operation.parent === "string" ? operation.parent : void 0, set: operation.set };
8004
+ if (op === "INSERT" && (typeof operation.parent !== "string" || !operation.node)) throw new Error(`patch.operations[${index}] requires parent and node.`);
8005
+ if (op === "MOVE" && typeof operation.parent !== "string") throw new Error(`patch.operations[${index}].parent is required.`);
8006
+ if ((op === "INSERT" || op === "MOVE") && (typeof operation.index !== "number" || !Number.isInteger(operation.index) || operation.index < 0)) {
8007
+ throw new Error(`patch.operations[${index}].index must be a non-negative integer.`);
8008
+ }
8009
+ if (op === "DUPLICATE") {
8010
+ if (typeof operation.idSuffix !== "string" || !operation.idSuffix) {
8011
+ throw new Error(`patch.operations[${index}].idSuffix must be a non-empty string.`);
8012
+ }
8013
+ if (operation.index !== void 0 && (typeof operation.index !== "number" || !Number.isInteger(operation.index) || operation.index < 0)) {
8014
+ throw new Error(`patch.operations[${index}].index must be a non-negative integer.`);
8015
+ }
8016
+ if (operation.ids !== void 0) {
8017
+ if (typeof operation.ids !== "object" || operation.ids === null || Array.isArray(operation.ids)) {
8018
+ throw new Error(`patch.operations[${index}].ids must be an object.`);
8019
+ }
8020
+ for (const [key, value2] of Object.entries(operation.ids)) {
8021
+ if (typeof value2 !== "string" || !value2) {
8022
+ throw new Error(`patch.operations[${index}].ids.${key} must be a non-empty string.`);
8023
+ }
8024
+ }
8025
+ }
8026
+ }
8027
+ const result = {
8028
+ op,
8029
+ id: typeof operation.id === "string" ? operation.id : void 0,
8030
+ parent: typeof operation.parent === "string" ? operation.parent : void 0,
8031
+ index: typeof operation.index === "number" ? operation.index : void 0,
8032
+ set: operation.set,
8033
+ idSuffix: typeof operation.idSuffix === "string" ? operation.idSuffix : void 0,
8034
+ ids: operation.ids && typeof operation.ids === "object" && !Array.isArray(operation.ids) ? { ...operation.ids } : void 0
8035
+ };
7607
8036
  if (op === "SET") {
7608
- const textKeys = ["text", "font", "lineHeight", "letterSpacing", "align", "runs", "textDecoration", "paragraphSpacing", "textAutoResize"];
7609
8037
  const set = operation.set;
7610
- const syntheticType = textKeys.some((key) => key in set) ? "TEXT" : "FRAME";
7611
- result.normalized = normalizeNode({ ...set, type: syntheticType, w: typeof set.w === "number" ? set.w : 1, h: typeof set.h === "number" ? set.h : 1 }, { x: 0, y: 0 }, `patch-${index}`).properties;
8038
+ if (!Object.keys(set).length) throw new Error(`patch.operations[${index}].set must contain at least one key.`);
8039
+ result.normalized = normalizePatchSet(set, `patch.operations[${index}].set`);
7612
8040
  }
7613
8041
  if (op === "APPEND") result.node = normalizeNode(operation.node, { x: 0, y: 0 }, `patch-${index}-append`);
8042
+ if (op === "INSERT") result.node = normalizeNode(operation.node, { x: 0, y: 0 }, `patch-${index}-insert`);
7614
8043
  return result;
7615
8044
  });
7616
8045
  return { patch: { operations } };
7617
8046
  }
7618
8047
 
7619
- // src/patch.ts
7620
- function cloneValue(value) {
7621
- if (value instanceof Uint8Array) return new Uint8Array(value);
7622
- if (Array.isArray(value)) return value.map(cloneValue);
7623
- if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneValue(item)]));
7624
- return value;
7625
- }
7626
- function cloneNode(node) {
7627
- return { ...node, properties: cloneValue(node.properties), children: node.children.map(cloneNode) };
8048
+ // src/patch-definitions.ts
8049
+ function isPlainObject(value) {
8050
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
7628
8051
  }
7629
- function mergeSet(node, operation) {
7630
- const set = operation.set || {};
7631
- const normalized = operation.normalized;
7632
- if (!normalized) throw new Error(`Patch SET '${operation.id}' was not normalized.`);
7633
- if (typeof set.name === "string") node.name = set.name;
7634
- const propertyKeys = Object.keys(set).filter((key) => !["id", "name", "type", "children"].includes(key));
7635
- for (const key of propertyKeys) {
7636
- if (key === "x") node.properties.position.x = normalized.position.x;
7637
- else if (key === "y") node.properties.position.y = normalized.position.y;
7638
- else if (key === "w") node.properties.size.width = normalized.size.width;
7639
- else if (key === "h") node.properties.size.height = normalized.size.height;
7640
- else if (key === "fill" || key === "fills") node.properties.styles.fills = cloneValue(normalized.styles.fills);
7641
- else if (key === "stroke" || key === "strokes") node.properties.styles.strokes = cloneValue(normalized.styles.strokes);
7642
- else if (key === "effects" || key === "shadow" || key === "elevation") node.properties.styles.effects = cloneValue(normalized.styles.effects);
7643
- else Object.assign(node.properties, { [key === "align" ? "alignment" : key]: cloneValue(normalized[key]) });
8052
+ function uniqueStrings(path, options, errors) {
8053
+ if (!options.length || options.some((option) => typeof option !== "string" || !option)) {
8054
+ errors.push(`${path}: must be a non-empty string array`);
8055
+ return null;
8056
+ }
8057
+ if (new Set(options).size !== options.length) {
8058
+ errors.push(`${path}: options must be unique`);
8059
+ return null;
7644
8060
  }
8061
+ return options;
7645
8062
  }
7646
- function applyDocumentPatch(document, patch) {
7647
- const result = { ...document, nodes: document.nodes.map(cloneNode), styles: cloneValue(document.styles), variables: cloneValue(document.variables) };
7648
- const affectedIds = [];
7649
- const index = /* @__PURE__ */ new Map();
7650
- const visit = (nodes, parent) => nodes.forEach((node) => {
7651
- index.set(node.id, { node, parent, siblings: nodes });
7652
- visit(node.children, node);
7653
- });
7654
- visit(result.nodes, null);
7655
- for (const [operationIndex, operation] of patch.patch.operations.entries()) {
7656
- if (operation.op === "APPEND") {
7657
- const target2 = operation.parent ? index.get(operation.parent) : void 0;
7658
- if (!target2 || !operation.node) throw new Error(`patch.operations[${operationIndex}]: parent '${operation.parent || ""}' was not found.`);
7659
- if (index.has(operation.node.id)) throw new Error(`patch.operations[${operationIndex}]: ID '${operation.node.id}' already exists.`);
7660
- const appended = cloneNode(operation.node);
7661
- target2.node.children.push(appended);
7662
- visit([appended], target2.node);
7663
- affectedIds.push(appended.id);
8063
+ function detectVariantRenames(current, patch) {
8064
+ const errors = [];
8065
+ const axes = { ...current };
8066
+ const axisRenames = [];
8067
+ const optionRenames = [];
8068
+ for (const [axis, entry] of Object.entries(patch)) {
8069
+ const path = `variantAxes.${axis}`;
8070
+ if (entry === null) {
8071
+ errors.push(`${path}: deleting a variant axis is not supported (Figma cannot delete VARIANT properties)`);
7664
8072
  continue;
7665
8073
  }
7666
- const target = operation.id ? index.get(operation.id) : void 0;
7667
- if (!target) throw new Error(`patch.operations[${operationIndex}]: node '${operation.id || ""}' was not found.`);
7668
- if (operation.op === "REMOVE") {
7669
- target.siblings.splice(target.siblings.indexOf(target.node), 1);
7670
- index.delete(target.node.id);
7671
- affectedIds.push(target.node.id);
7672
- } else {
7673
- mergeSet(target.node, operation);
7674
- affectedIds.push(target.node.id);
8074
+ if (Array.isArray(entry)) {
8075
+ const options = uniqueStrings(path, entry, errors);
8076
+ if (!options) continue;
8077
+ if (!(axis in current) && !(axis in axes)) {
8078
+ } else if (!(axis in axes) && axisRenames.some((rename) => rename.to === axis)) {
8079
+ }
8080
+ axes[axis] = options;
8081
+ continue;
8082
+ }
8083
+ if (!isPlainObject(entry)) {
8084
+ errors.push(`${path}: must be a string array, null, or { rename?, options?, renameOptions? }`);
8085
+ continue;
8086
+ }
8087
+ const renameTo = entry.rename;
8088
+ const renameOptionsRaw = entry.renameOptions;
8089
+ const optionsRaw = entry.options;
8090
+ if (renameTo !== void 0 && (typeof renameTo !== "string" || !renameTo)) {
8091
+ errors.push(`${path}.rename: must be a non-empty string`);
8092
+ continue;
8093
+ }
8094
+ if (renameTo !== void 0 && renameTo !== axis) {
8095
+ if (!(axis in current) && !(axis in axes)) {
8096
+ errors.push(`${path}: cannot rename unknown axis '${axis}'`);
8097
+ } else if (renameTo in axes && renameTo !== axis) {
8098
+ errors.push(`${path}.rename: axis '${renameTo}' already exists`);
8099
+ } else {
8100
+ axisRenames.push({ from: axis, to: renameTo });
8101
+ const prior = axes[axis] || current[axis] || [];
8102
+ delete axes[axis];
8103
+ axes[renameTo] = [...prior];
8104
+ }
8105
+ }
8106
+ const axisKey = typeof renameTo === "string" && renameTo ? renameTo : axis;
8107
+ const baseOptions = axes[axisKey] || current[axis] || [];
8108
+ const explicitOptionRenames = [];
8109
+ if (renameOptionsRaw !== void 0) {
8110
+ if (!isPlainObject(renameOptionsRaw)) {
8111
+ errors.push(`${path}.renameOptions: must be an object of old\u2192new option names`);
8112
+ } else {
8113
+ for (const [from, to] of Object.entries(renameOptionsRaw)) {
8114
+ if (typeof to !== "string" || !to) {
8115
+ errors.push(`${path}.renameOptions.${from}: must be a non-empty string`);
8116
+ continue;
8117
+ }
8118
+ if (!baseOptions.includes(from)) {
8119
+ errors.push(`${path}.renameOptions.${from}: option '${from}' is not on axis '${axisKey}'`);
8120
+ continue;
8121
+ }
8122
+ explicitOptionRenames.push({ axis: axisKey, from, to });
8123
+ }
8124
+ }
8125
+ }
8126
+ let nextOptions = [...baseOptions];
8127
+ for (const { from, to } of explicitOptionRenames) {
8128
+ nextOptions = nextOptions.map((option) => option === from ? to : option);
8129
+ }
8130
+ if (optionsRaw !== void 0) {
8131
+ if (!Array.isArray(optionsRaw)) {
8132
+ errors.push(`${path}.options: must be a string array`);
8133
+ } else {
8134
+ const options = uniqueStrings(`${path}.options`, optionsRaw, errors);
8135
+ if (options) nextOptions = options;
8136
+ }
8137
+ } else if (explicitOptionRenames.length) {
8138
+ nextOptions = [...new Set(nextOptions)];
8139
+ }
8140
+ if (!nextOptions.length) {
8141
+ errors.push(`${path}: options must be a non-empty string array`);
8142
+ continue;
8143
+ }
8144
+ if (new Set(nextOptions).size !== nextOptions.length) {
8145
+ errors.push(`${path}: options must be unique`);
8146
+ continue;
7675
8147
  }
8148
+ axes[axisKey] = nextOptions;
8149
+ optionRenames.push(...explicitOptionRenames);
7676
8150
  }
7677
- return { document: result, affectedIds };
8151
+ return { axes, axisRenames, optionRenames, errors };
7678
8152
  }
7679
-
7680
- // src/parser.ts
7681
- function parseDocument(source) {
7682
- if (!source.trim()) throw new Error("Paste a Compact Design JSON document first.");
7683
- try {
7684
- return JSON.parse(source);
7685
- } catch (error) {
7686
- throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
8153
+ function variantAxesChildConflicts(set, previousAxes, nextAxes, optionRenames, axisRenames) {
8154
+ const errors = [];
8155
+ const renamedAway = new Set(optionRenames.map((rename) => `${rename.axis}\0${rename.from}`));
8156
+ const axisFrom = new Map(axisRenames.map((rename) => [rename.from, rename.to]));
8157
+ for (const child of set.children) {
8158
+ if (child.type !== "COMPONENT") continue;
8159
+ const variant = { ...child.properties.variant || {} };
8160
+ for (const [from, to] of axisFrom) {
8161
+ if (from in variant) {
8162
+ variant[to] = variant[from];
8163
+ delete variant[from];
8164
+ }
8165
+ }
8166
+ for (const { axis, from, to } of optionRenames) {
8167
+ if (variant[axis] === from) variant[axis] = to;
8168
+ }
8169
+ for (const [axis, value] of Object.entries(variant)) {
8170
+ const options = nextAxes[axis];
8171
+ if (!options) {
8172
+ errors.push(`variantAxes: child '${child.id}' still uses axis '${axis}' which is not declared`);
8173
+ continue;
8174
+ }
8175
+ if (!options.includes(value)) {
8176
+ const priorAxis = [...axisFrom.entries()].find(([, to]) => to === axis)?.[0] || axis;
8177
+ const priorOptions = previousAxes[priorAxis] || previousAxes[axis] || [];
8178
+ if (priorOptions.includes(value) || renamedAway.has(`${axis}\0${value}`)) {
8179
+ errors.push(
8180
+ `variantAxes.${axis}: option '${value}' is used by variant child '${child.id}' but was removed without renameOptions (use renameOptions: { "${value}": "NewName" } or update the child's variant first)`
8181
+ );
8182
+ } else {
8183
+ errors.push(`variantAxes.${axis}: child '${child.id}' has value '${value}' which is not declared`);
8184
+ }
8185
+ }
8186
+ }
7687
8187
  }
8188
+ return errors;
7688
8189
  }
7689
-
7690
- // src/generated-schema.ts
7691
- var generated_schema_default = {
7692
- "$schema": "https://json-schema.org/draft/2020-12/schema",
7693
- "$id": "https://compact-design-json.dev/schema.json",
7694
- "title": "Compact Design JSON",
7695
- "description": "The current public input language for the Compact Design JSON to Figma plugin.",
7696
- "type": "object",
7697
- "additionalProperties": false,
7698
- "properties": {
7699
- "$schema": { "type": "string" },
7700
- "canvas": { "$ref": "#/$defs/canvas" },
7701
- "canvases": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/responsiveCanvas" } },
7702
- "nodes": { "$ref": "#/$defs/nodes" },
7703
- "variables": { "type": "array", "items": { "$ref": "#/$defs/variableCollection" } },
7704
- "styles": { "type": "array", "items": { "$ref": "#/$defs/style" } },
7705
- "patch": { "$ref": "#/$defs/patchRoot" }
7706
- },
7707
- "oneOf": [
7708
- {
7709
- "required": ["canvas"],
7710
- "not": { "anyOf": [{ "required": ["canvases"] }, { "required": ["patch"] }] }
7711
- },
7712
- {
7713
- "required": ["canvases"],
7714
- "not": { "anyOf": [{ "required": ["canvas"] }, { "required": ["patch"] }] }
7715
- },
7716
- {
7717
- "required": ["patch"],
7718
- "not": { "anyOf": [{ "required": ["canvas"] }, { "required": ["canvases"] }] }
8190
+ function variantAxesUncarriedOptions(set, axes) {
8191
+ const errors = [];
8192
+ const carried = {};
8193
+ for (const axis of Object.keys(axes)) carried[axis] = /* @__PURE__ */ new Set();
8194
+ for (const child of set.children) {
8195
+ if (child.type !== "COMPONENT") continue;
8196
+ for (const [axis, value] of Object.entries(child.properties.variant || {})) {
8197
+ if (carried[axis]) carried[axis].add(value);
7719
8198
  }
7720
- ],
7721
- "$defs": {
7722
- "positiveNumber": { "type": "number", "exclusiveMinimum": 0 },
7723
- "nonNegativeNumber": { "type": "number", "minimum": 0 },
7724
- "nullablePositiveNumber": { "type": ["number", "null"], "exclusiveMinimum": 0 },
7725
- "hexColor": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$" },
7726
- "color": {
7727
- "type": "object",
7728
- "additionalProperties": false,
7729
- "required": ["r", "g", "b"],
7730
- "properties": {
7731
- "r": { "type": "number", "minimum": 0, "maximum": 255 },
7732
- "g": { "type": "number", "minimum": 0, "maximum": 255 },
7733
- "b": { "type": "number", "minimum": 0, "maximum": 255 },
7734
- "a": { "type": "number", "minimum": 0, "maximum": 1 }
8199
+ }
8200
+ for (const [axis, options] of Object.entries(axes)) {
8201
+ for (const option of options) {
8202
+ if (!carried[axis]?.has(option)) {
8203
+ errors.push(
8204
+ `variantAxes.${axis}: option '${option}' is not carried by any variant child (add or set a COMPONENT child's variant to '${option}', or use renameOptions)`
8205
+ );
7735
8206
  }
7736
- },
7737
- "vector": {
7738
- "type": "object",
7739
- "additionalProperties": false,
7740
- "required": ["x", "y"],
7741
- "properties": { "x": { "type": "number" }, "y": { "type": "number" } }
7742
- },
7743
- "transform": {
7744
- "type": "array",
7745
- "minItems": 2,
7746
- "maxItems": 2,
7747
- "items": { "type": "array", "minItems": 3, "maxItems": 3, "items": { "type": "number" } }
7748
- },
7749
- "gradientStop": {
7750
- "type": "object",
7751
- "additionalProperties": false,
7752
- "required": ["at", "color"],
7753
- "properties": {
7754
- "at": { "type": "number", "minimum": 0, "maximum": 1 },
7755
- "color": { "oneOf": [{ "$ref": "#/$defs/hexColor" }, { "$ref": "#/$defs/color" }] }
8207
+ }
8208
+ }
8209
+ return errors;
8210
+ }
8211
+ function applyVariantRenamesInForest(roots, setId, axisRenames, optionRenames, axes, index) {
8212
+ const childIds = /* @__PURE__ */ new Set();
8213
+ const collectChildren = (node, inside) => {
8214
+ const inSet = inside || node.type === "COMPONENT_SET" && node.id === setId;
8215
+ if (node.type === "COMPONENT" && inSet && node.id !== setId) childIds.add(node.id);
8216
+ const childInside = node.type === "COMPONENT_SET" ? node.id === setId : inSet;
8217
+ for (const child of node.children) collectChildren(child, childInside);
8218
+ };
8219
+ for (const root of roots) collectChildren(root, false);
8220
+ const visit = (node, insideTargetSet) => {
8221
+ const inSet = insideTargetSet || node.type === "COMPONENT_SET" && node.id === setId;
8222
+ if (node.type === "COMPONENT" && inSet && node.id !== setId) {
8223
+ const variant = { ...node.properties.variant || {} };
8224
+ for (const { from, to } of axisRenames) {
8225
+ if (from in variant) {
8226
+ variant[to] = variant[from];
8227
+ delete variant[from];
8228
+ }
7756
8229
  }
7757
- },
7758
- "imagePaint": {
7759
- "type": "object",
7760
- "additionalProperties": false,
7761
- "required": ["image"],
7762
- "properties": {
7763
- "image": {
7764
- "type": "string",
7765
- "anyOf": [
7766
- { "pattern": "^https://" },
7767
- { "pattern": "^file:[^/].+" },
7768
- { "pattern": "^data:image/(png|jpeg|gif|webp);base64," }
7769
- ]
7770
- },
7771
- "fit": { "enum": ["COVER", "CONTAIN", "CROP", "TILE", "FILL", "FIT"] },
7772
- "opacity": { "type": "number", "minimum": 0, "maximum": 1 },
7773
- "transform": { "$ref": "#/$defs/transform" }
8230
+ for (const { axis, from, to } of optionRenames) {
8231
+ if (variant[axis] === from) variant[axis] = to;
7774
8232
  }
7775
- },
7776
- "gradientPaint": {
7777
- "type": "object",
7778
- "additionalProperties": false,
7779
- "required": ["gradient", "stops"],
7780
- "properties": {
7781
- "gradient": { "enum": ["LINEAR", "RADIAL", "ANGULAR", "DIAMOND"] },
7782
- "angle": { "type": "number" },
7783
- "transform": { "$ref": "#/$defs/transform" },
7784
- "stops": { "type": "array", "minItems": 2, "items": { "$ref": "#/$defs/gradientStop" } },
7785
- "opacity": { "type": "number", "minimum": 0, "maximum": 1 }
8233
+ node.properties.variant = Object.keys(variant).length ? variant : void 0;
8234
+ if (Object.keys(axes).length) {
8235
+ node.name = Object.keys(axes).map((axis) => `${axis}=${variant[axis] ?? axes[axis][0]}`).join(", ");
7786
8236
  }
7787
- },
7788
- "solidPaint": {
7789
- "type": "object",
8237
+ }
8238
+ if (node.type === "INSTANCE" && node.properties.instanceProperties) {
8239
+ const mainId = node.properties.componentId;
8240
+ if (!mainId || !childIds.has(mainId)) {
8241
+ } else {
8242
+ const next = {};
8243
+ for (const [key, value] of Object.entries(node.properties.instanceProperties)) {
8244
+ let newKey = key;
8245
+ for (const { from, to } of axisRenames) if (key === from) newKey = to;
8246
+ let newValue = value;
8247
+ if (typeof value === "string") {
8248
+ for (const { axis, from, to } of optionRenames) {
8249
+ if (newKey === axis && value === from) newValue = to;
8250
+ }
8251
+ }
8252
+ next[newKey] = newValue;
8253
+ }
8254
+ node.properties.instanceProperties = next;
8255
+ }
8256
+ }
8257
+ const childInside = node.type === "COMPONENT_SET" ? node.id === setId : inSet;
8258
+ for (const child of node.children) visit(child, childInside);
8259
+ };
8260
+ for (const root of roots) visit(root, false);
8261
+ void index;
8262
+ }
8263
+ function applyComponentPropertiesPatch(props, patch, path) {
8264
+ const errors = [];
8265
+ if (patch === null) {
8266
+ delete props.componentProperties;
8267
+ return errors;
8268
+ }
8269
+ const list = [...props.componentProperties || []];
8270
+ const indexByName = () => {
8271
+ const map = /* @__PURE__ */ new Map();
8272
+ list.forEach((property, index) => map.set(property.name, index));
8273
+ return map;
8274
+ };
8275
+ let byName = indexByName();
8276
+ for (const [name, entry] of Object.entries(patch)) {
8277
+ if (entry === null) {
8278
+ const index = byName.get(name);
8279
+ if (index === void 0) continue;
8280
+ list.splice(index, 1);
8281
+ byName = indexByName();
8282
+ continue;
8283
+ }
8284
+ const existingIndex = byName.get(name);
8285
+ if (existingIndex === void 0) {
8286
+ if (!entry.type) {
8287
+ errors.push(`${path}.componentProperties.${name}: type is required when creating a property`);
8288
+ continue;
8289
+ }
8290
+ if (entry.defaultValue === void 0) {
8291
+ errors.push(`${path}.componentProperties.${name}: defaultValue is required when creating a property`);
8292
+ continue;
8293
+ }
8294
+ list.push({
8295
+ name,
8296
+ type: entry.type,
8297
+ defaultValue: entry.defaultValue,
8298
+ ...entry.options ? { options: entry.options } : {}
8299
+ });
8300
+ byName = indexByName();
8301
+ continue;
8302
+ }
8303
+ const existing = list[existingIndex];
8304
+ if (entry.type !== void 0 && entry.type !== existing.type) {
8305
+ errors.push(`${path}.componentProperties.${name}: type is immutable after create (was ${existing.type}, got ${entry.type})`);
8306
+ continue;
8307
+ }
8308
+ list[existingIndex] = {
8309
+ ...existing,
8310
+ ...entry.defaultValue !== void 0 ? { defaultValue: entry.defaultValue } : {},
8311
+ ...entry.options !== void 0 ? { options: entry.options } : {}
8312
+ };
8313
+ }
8314
+ if (list.length) props.componentProperties = list;
8315
+ else delete props.componentProperties;
8316
+ return errors;
8317
+ }
8318
+ function applyVariantPatchOnComponent(node, parent, patch, path) {
8319
+ const errors = [];
8320
+ if (!parent || parent.type !== "COMPONENT_SET") {
8321
+ errors.push(`${path}.variant: only applies to COMPONENT children of a COMPONENT_SET`);
8322
+ return errors;
8323
+ }
8324
+ if (patch === null) {
8325
+ errors.push(`${path}.variant: clearing variant on a COMPONENT inside a COMPONENT_SET is not supported (variant components require a selection)`);
8326
+ return errors;
8327
+ }
8328
+ const axes = parent.properties.variantAxes || {};
8329
+ const variant = { ...node.properties.variant || {} };
8330
+ for (const [axis, value] of Object.entries(patch)) {
8331
+ if (value === null) {
8332
+ errors.push(`${path}.variant.${axis}: clearing an axis on a variant COMPONENT is not supported`);
8333
+ continue;
8334
+ }
8335
+ if (!axes[axis]) {
8336
+ errors.push(`${path}.variant.${axis}: axis '${axis}' is not declared on the parent COMPONENT_SET`);
8337
+ continue;
8338
+ }
8339
+ if (!axes[axis].includes(value)) {
8340
+ errors.push(`${path}.variant.${axis}: value '${value}' is not declared in variantAxes`);
8341
+ continue;
8342
+ }
8343
+ variant[axis] = value;
8344
+ }
8345
+ node.properties.variant = Object.keys(variant).length ? variant : void 0;
8346
+ if (Object.keys(axes).length) {
8347
+ node.name = Object.keys(axes).map((axis) => `${axis}=${variant[axis] ?? axes[axis][0]}`).join(", ");
8348
+ }
8349
+ return errors;
8350
+ }
8351
+ function rewriteVariantChildNames(set) {
8352
+ const axes = set.properties.variantAxes || {};
8353
+ for (const child of set.children) {
8354
+ if (child.type !== "COMPONENT") continue;
8355
+ const selected = child.properties.variant || {};
8356
+ child.name = Object.keys(axes).map((axis) => `${axis}=${selected[axis] ?? axes[axis][0]}`).join(", ");
8357
+ }
8358
+ }
8359
+
8360
+ // src/patch-duplicate.ts
8361
+ function collectSubtreeIds(node) {
8362
+ const ids = [node.id];
8363
+ for (const child of node.children) ids.push(...collectSubtreeIds(child));
8364
+ return ids;
8365
+ }
8366
+ function subtreeContainsType(node, types) {
8367
+ if (types.has(node.type)) return true;
8368
+ return node.children.some((child) => subtreeContainsType(child, types));
8369
+ }
8370
+ function buildDuplicateIdMap(source, idSuffix, ids, path) {
8371
+ return buildDuplicateIdMapFromIds(collectSubtreeIds(source), idSuffix, ids, path);
8372
+ }
8373
+ function buildDuplicateIdMapFromIds(subtreeIds, idSuffix, ids, path) {
8374
+ const errors = [];
8375
+ if (typeof idSuffix !== "string" || !idSuffix) {
8376
+ errors.push(`${path}.idSuffix: must be a non-empty string`);
8377
+ return { map: /* @__PURE__ */ new Map(), errors };
8378
+ }
8379
+ const subtree = new Set(subtreeIds);
8380
+ const map = /* @__PURE__ */ new Map();
8381
+ for (const id of subtree) map.set(id, `${id}${idSuffix}`);
8382
+ if (ids !== void 0) {
8383
+ if (!ids || typeof ids !== "object" || Array.isArray(ids)) {
8384
+ errors.push(`${path}.ids: must be an object of sourceId\u2192newId`);
8385
+ } else {
8386
+ for (const [from, to] of Object.entries(ids)) {
8387
+ if (!subtree.has(from)) {
8388
+ errors.push(`${path}.ids.${from}: is not an id in the source subtree`);
8389
+ continue;
8390
+ }
8391
+ if (typeof to !== "string" || !to) {
8392
+ errors.push(`${path}.ids.${from}: new id must be a non-empty string`);
8393
+ continue;
8394
+ }
8395
+ map.set(from, to);
8396
+ }
8397
+ }
8398
+ }
8399
+ const seen = /* @__PURE__ */ new Map();
8400
+ for (const [from, to] of map) {
8401
+ const prior = seen.get(to);
8402
+ if (prior !== void 0) {
8403
+ errors.push(`${path}: duplicate maps both '${prior}' and '${from}' to '${to}'`);
8404
+ } else {
8405
+ seen.set(to, from);
8406
+ }
8407
+ }
8408
+ return { map, errors };
8409
+ }
8410
+ function cloneSubtreeWithIds(node, idMap) {
8411
+ const mappedId = idMap.get(node.id) || node.id;
8412
+ const properties = cloneValue(node.properties);
8413
+ rewritePrototypeDestinations(properties, idMap);
8414
+ return {
8415
+ ...node,
8416
+ id: mappedId,
8417
+ properties,
8418
+ children: node.children.map((child) => cloneSubtreeWithIds(child, idMap))
8419
+ };
8420
+ }
8421
+ function cloneValue(value) {
8422
+ if (Array.isArray(value)) return value.map(cloneValue);
8423
+ if (value && typeof value === "object") {
8424
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneValue(item)]));
8425
+ }
8426
+ return value;
8427
+ }
8428
+ function rewritePrototypeDestinations(properties, idMap) {
8429
+ const prototype = properties.prototype;
8430
+ if (!Array.isArray(prototype)) return;
8431
+ for (const reaction of prototype) {
8432
+ if (!reaction || typeof reaction !== "object" || Array.isArray(reaction)) continue;
8433
+ const actions = reaction.actions;
8434
+ if (!Array.isArray(actions)) continue;
8435
+ for (const action of actions) {
8436
+ if (!action || typeof action !== "object" || Array.isArray(action)) continue;
8437
+ const dest = action.destination;
8438
+ if (typeof dest === "string" && idMap.has(dest)) {
8439
+ action.destination = idMap.get(dest);
8440
+ }
8441
+ }
8442
+ }
8443
+ }
8444
+ function remapDelimitedIds(text, idMap) {
8445
+ if (!text || idMap.size === 0) return text;
8446
+ const entries = [...idMap.entries()].sort((a, b) => b[0].length - a[0].length);
8447
+ let result = text;
8448
+ for (const [from, to] of entries) {
8449
+ result = result.split(`'${from}'`).join(`'${to}'`);
8450
+ result = result.split(`"${from}"`).join(`"${to}"`);
8451
+ }
8452
+ return result;
8453
+ }
8454
+ function remapIssueKeyThroughDuplicate(key, idMap) {
8455
+ const parts = key.split("|");
8456
+ if (parts.length < 4) return null;
8457
+ const [code, owner, rest, ...messageParts] = parts;
8458
+ const message = messageParts.join("|");
8459
+ let nextOwner = owner;
8460
+ let changed = false;
8461
+ if (owner.startsWith("node:")) {
8462
+ const id = owner.slice("node:".length);
8463
+ const mapped = idMap.get(id);
8464
+ if (mapped) {
8465
+ nextOwner = `node:${mapped}`;
8466
+ changed = true;
8467
+ }
8468
+ }
8469
+ const nextRest = remapDelimitedIds(rest, idMap);
8470
+ const nextMessage = remapDelimitedIds(message, idMap);
8471
+ if (nextRest !== rest || nextMessage !== message) changed = true;
8472
+ return changed ? `${code}|${nextOwner}|${nextRest}|${nextMessage}` : null;
8473
+ }
8474
+
8475
+ // src/patch-prototype.ts
8476
+ var DESTINATION_ACTIONS = /* @__PURE__ */ new Set(["NAVIGATE", "SWAP", "OVERLAY", "SCROLL_TO", "CHANGE_TO"]);
8477
+ var TOP_LEVEL_NAV_ACTIONS = /* @__PURE__ */ new Set(["NAVIGATE", "SWAP", "OVERLAY"]);
8478
+ function triggerType(reaction) {
8479
+ const trigger = reaction.trigger;
8480
+ if (typeof trigger === "string") return trigger;
8481
+ if (trigger && typeof trigger === "object" && !Array.isArray(trigger) && typeof trigger.type === "string") {
8482
+ return String(trigger.type);
8483
+ }
8484
+ return "";
8485
+ }
8486
+ function reactionActions(reaction) {
8487
+ if (Array.isArray(reaction.actions)) {
8488
+ return reaction.actions.filter((item) => Boolean(item) && typeof item === "object" && !Array.isArray(item));
8489
+ }
8490
+ if (reaction.action) return [{ ...reaction, type: reaction.action }];
8491
+ return [];
8492
+ }
8493
+ function forEachPrototypeDestination(prototype, visit, basePath = "prototype") {
8494
+ if (!Array.isArray(prototype)) return;
8495
+ prototype.forEach((reaction, index) => {
8496
+ if (!reaction || typeof reaction !== "object" || Array.isArray(reaction)) return;
8497
+ reactionActions(reaction).forEach((action, actionIndex) => {
8498
+ const type = String(action.type || "").toUpperCase();
8499
+ if (!DESTINATION_ACTIONS.has(type)) return;
8500
+ const destination = action.destination;
8501
+ if (typeof destination !== "string") return;
8502
+ visit(action, type, destination, `${basePath}[${index}].actions[${actionIndex}]`);
8503
+ });
8504
+ });
8505
+ }
8506
+ function isAfterTimeoutReaction(reaction) {
8507
+ return triggerType(reaction) === "AFTER_TIMEOUT";
8508
+ }
8509
+ function isTopLevelNodeId(roots, nodeId) {
8510
+ return roots.some((root) => root.id === nodeId);
8511
+ }
8512
+ function assertPrototypeSetDestinations(prototype, lookup, path, sourceId) {
8513
+ const errors = [];
8514
+ if (!Array.isArray(prototype)) {
8515
+ errors.push(`${path}: prototype must be an array`);
8516
+ return errors;
8517
+ }
8518
+ forEachPrototypeDestination(prototype, (_action, type, destination, actionPath) => {
8519
+ if (!lookup.has(destination)) {
8520
+ errors.push(`${actionPath}.destination: Prototype destination '${destination}' was not found`);
8521
+ return;
8522
+ }
8523
+ if (type === "CHANGE_TO") {
8524
+ const destType = lookup.typeOf?.(destination);
8525
+ if (destType && destType !== "COMPONENT") {
8526
+ errors.push(`${actionPath}.destination: CHANGE_TO destination '${destination}' must be a component variant, received ${destType}`);
8527
+ }
8528
+ }
8529
+ if (type === "SCROLL_TO" && sourceId && lookup.canvasOf) {
8530
+ const sourceCanvas = lookup.canvasOf(sourceId);
8531
+ const destCanvas = lookup.canvasOf(destination);
8532
+ if (sourceCanvas && destCanvas && sourceCanvas !== destCanvas) {
8533
+ errors.push(`${actionPath}.destination: SCROLL_TO destination '${destination}' must be inside the same top-level canvas as its source`);
8534
+ }
8535
+ }
8536
+ }, path);
8537
+ return errors;
8538
+ }
8539
+ function assertPrototypePatchRules(prototype, lookup, path, sourceId, sourceIsTopLevel) {
8540
+ const errors = assertPrototypeSetDestinations(prototype, lookup, path, sourceId);
8541
+ if (!Array.isArray(prototype)) return errors;
8542
+ if (!sourceIsTopLevel) {
8543
+ prototype.forEach((reaction, index) => {
8544
+ if (!reaction || typeof reaction !== "object" || Array.isArray(reaction)) return;
8545
+ if (isAfterTimeoutReaction(reaction)) {
8546
+ errors.push(`${path}[${index}].trigger: AFTER_TIMEOUT in set.prototype requires a top-level target node`);
8547
+ }
8548
+ });
8549
+ }
8550
+ forEachPrototypeDestination(prototype, (_action, type, destination, actionPath) => {
8551
+ if (!TOP_LEVEL_NAV_ACTIONS.has(type)) return;
8552
+ if (!lookup.has(destination)) return;
8553
+ if (lookup.isTopLevel && !lookup.isTopLevel(destination)) {
8554
+ errors.push(`${actionPath}.destination: ${type} destination must be a top-level frame; use '${lookup.canvasOf?.(destination) || destination}'`);
8555
+ }
8556
+ }, path);
8557
+ return errors;
8558
+ }
8559
+ function canvasRootId(roots, nodeId) {
8560
+ const find = (nodes, rootId) => {
8561
+ for (const node of nodes) {
8562
+ if (node.id === nodeId) return rootId;
8563
+ const nested = find(node.children, rootId);
8564
+ if (nested) return nested;
8565
+ }
8566
+ return void 0;
8567
+ };
8568
+ for (const root of roots) {
8569
+ const hit = find([root], root.id);
8570
+ if (hit) return hit;
8571
+ }
8572
+ return void 0;
8573
+ }
8574
+ function documentLookup(document) {
8575
+ const types = /* @__PURE__ */ new Map();
8576
+ const collect = (node) => {
8577
+ types.set(node.id, node.type);
8578
+ node.children.forEach(collect);
8579
+ };
8580
+ document.nodes.forEach(collect);
8581
+ return {
8582
+ has: (id) => types.has(id),
8583
+ typeOf: (id) => types.get(id),
8584
+ canvasOf: (id) => canvasRootId(document.nodes, id),
8585
+ isTopLevel: (id) => isTopLevelNodeId(document.nodes, id)
8586
+ };
8587
+ }
8588
+ function prototypeDestinationErrors(document) {
8589
+ const lookup = documentLookup(document);
8590
+ const errors = [];
8591
+ const walk = (node, path) => {
8592
+ if (node.properties.prototype) {
8593
+ errors.push(...assertPrototypeSetDestinations(node.properties.prototype, lookup, `${path}.prototype`, node.id));
8594
+ }
8595
+ node.children.forEach((child, index) => walk(child, `${path}.children[${index}]`));
8596
+ };
8597
+ document.nodes.forEach((node, index) => walk(node, `nodes[${index}]`));
8598
+ return errors;
8599
+ }
8600
+ function prototypePatchStrictErrors(document) {
8601
+ const lookup = documentLookup(document);
8602
+ const errors = [];
8603
+ const walk = (node, path) => {
8604
+ if (node.properties.prototype) {
8605
+ errors.push(...assertPrototypePatchRules(
8606
+ node.properties.prototype,
8607
+ lookup,
8608
+ `${path}.prototype`,
8609
+ node.id,
8610
+ isTopLevelNodeId(document.nodes, node.id)
8611
+ ));
8612
+ }
8613
+ node.children.forEach((child, index) => walk(child, `${path}.children[${index}]`));
8614
+ };
8615
+ document.nodes.forEach((node, index) => walk(node, `nodes[${index}]`));
8616
+ return errors;
8617
+ }
8618
+
8619
+ // src/constants.ts
8620
+ var SUPPORTED_NODE_TYPES = /* @__PURE__ */ new Set([
8621
+ "FRAME",
8622
+ "GROUP",
8623
+ "RECTANGLE",
8624
+ "ELLIPSE",
8625
+ "LINE",
8626
+ "TEXT",
8627
+ "VECTOR",
8628
+ "SVG",
8629
+ "COMPONENT",
8630
+ "COMPONENT_SET",
8631
+ "INSTANCE",
8632
+ "BOOLEAN_OPERATION",
8633
+ "POLYGON",
8634
+ "STAR",
8635
+ "SECTION",
8636
+ "SLICE",
8637
+ "ARC"
8638
+ ]);
8639
+ var SUPPORTED_PAINT_TYPES = /* @__PURE__ */ new Set([
8640
+ "SOLID",
8641
+ "GRADIENT_LINEAR",
8642
+ "GRADIENT_RADIAL",
8643
+ "GRADIENT_ANGULAR",
8644
+ "GRADIENT_DIAMOND",
8645
+ "IMAGE"
8646
+ ]);
8647
+
8648
+ // src/validate.ts
8649
+ function validateDocument(document, limit = 30) {
8650
+ const errors = [];
8651
+ const ids = /* @__PURE__ */ new Set();
8652
+ const components = /* @__PURE__ */ new Set();
8653
+ const nodeTypes = /* @__PURE__ */ new Map();
8654
+ const add = (path, message) => {
8655
+ if (errors.length < limit) errors.push(`${path}: ${message}`);
8656
+ };
8657
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
8658
+ const triggers = /* @__PURE__ */ new Set(["ON_CLICK", "ON_HOVER", "ON_PRESS", "ON_DRAG", "AFTER_TIMEOUT", "MOUSE_UP", "MOUSE_DOWN", "MOUSE_ENTER", "MOUSE_LEAVE", "ON_KEY_DOWN", "ON_MEDIA_HIT", "ON_MEDIA_END"]);
8659
+ const actions = /* @__PURE__ */ new Set(["NAVIGATE", "SWAP", "OVERLAY", "SCROLL_TO", "CHANGE_TO", "BACK", "CLOSE", "URL", "UPDATE_MEDIA_RUNTIME", "SET_VARIABLE", "SET_VARIABLE_MODE", "CONDITIONAL"]);
8660
+ function validateAction(value, path) {
8661
+ if (!value || typeof value !== "object") return add(path, "action must be an object");
8662
+ const action = value;
8663
+ const type = String(action.type || "");
8664
+ if (!actions.has(type)) add(`${path}.type`, `unsupported prototype action '${type}'`);
8665
+ if (["NAVIGATE", "SWAP", "OVERLAY", "SCROLL_TO", "CHANGE_TO"].includes(type) && typeof action.destination !== "string") add(`${path}.destination`, `${type} requires a destination ID`);
8666
+ if (type === "URL" && typeof action.url !== "string") add(`${path}.url`, "URL action requires url");
8667
+ if (type === "SET_VARIABLE" && typeof (action.variable || action.variableId) !== "string") add(`${path}.variable`, "SET_VARIABLE requires a variable reference");
8668
+ if (type === "SET_VARIABLE_MODE" && (typeof (action.collection || action.variableCollectionId) !== "string" || typeof (action.mode || action.variableModeId) !== "string")) add(path, "SET_VARIABLE_MODE requires collection and mode references");
8669
+ if (type === "UPDATE_MEDIA_RUNTIME" && typeof action.mediaAction !== "string") add(`${path}.mediaAction`, "UPDATE_MEDIA_RUNTIME requires mediaAction");
8670
+ if (type === "CONDITIONAL") {
8671
+ const blocks = action.blocks || action.conditionalBlocks;
8672
+ if (!Array.isArray(blocks) || !blocks.length) add(`${path}.blocks`, "CONDITIONAL requires at least one block");
8673
+ else blocks.forEach((block, index) => {
8674
+ if (block && typeof block === "object" && !Array.isArray(block)) {
8675
+ const nested = block.actions;
8676
+ if (Array.isArray(nested)) nested.forEach((item, actionIndex) => validateAction(item, `${path}.blocks[${index}].actions[${actionIndex}]`));
8677
+ }
8678
+ });
8679
+ }
8680
+ }
8681
+ function validatePrototype(values, path) {
8682
+ values.forEach((reaction, index) => {
8683
+ const reactionPath = `${path}[${index}]`;
8684
+ const triggerValue = reaction.trigger;
8685
+ const trigger = typeof triggerValue === "string" ? triggerValue : triggerValue && typeof triggerValue === "object" && !Array.isArray(triggerValue) && typeof triggerValue.type === "string" ? triggerValue.type : "";
8686
+ if (!triggers.has(trigger)) add(`${reactionPath}.trigger`, `unsupported trigger '${trigger}'`);
8687
+ const reactionActions2 = Array.isArray(reaction.actions) ? reaction.actions : reaction.action ? [{ ...reaction, type: reaction.action }] : [];
8688
+ if (!reactionActions2.length) add(`${reactionPath}.actions`, "reaction requires at least one action");
8689
+ reactionActions2.forEach((action, actionIndex) => validateAction(action, `${reactionPath}.actions[${actionIndex}]`));
8690
+ });
8691
+ }
8692
+ function validatePaint(value, path) {
8693
+ if (!value || typeof value !== "object") return add(path, "paint must be an object or valid hex color");
8694
+ if (!SUPPORTED_PAINT_TYPES.has(value.type)) add(`${path}.type`, `unsupported paint type '${value.type}'`);
8695
+ if (value.type === "IMAGE") {
8696
+ if (typeof value.src !== "string" || !/^https:\/\//i.test(value.src) && !/^file:[^/].+/i.test(value.src) && !/^data:image\/(png|jpeg|gif|webp);base64,/i.test(value.src)) add(`${path}.src`, "IMAGE requires HTTPS, file:filename, or an embedded image data URL");
8697
+ if (!value.scaleMode || !["FILL", "FIT", "CROP", "TILE"].includes(value.scaleMode)) add(`${path}.scaleMode`, "must be COVER/FILL, CONTAIN/FIT, CROP, or TILE");
8698
+ if (value.imageTransform !== void 0 && (!Array.isArray(value.imageTransform) || value.imageTransform.length !== 2)) add(`${path}.imageTransform`, "must be a 2\xD73 transform matrix");
8699
+ }
8700
+ if (value.type?.startsWith("GRADIENT_") && (!Array.isArray(value.gradientStops) || value.gradientStops.length < 2)) add(`${path}.gradientStops`, "gradient requires at least two stops");
8701
+ if (value.opacity !== void 0 && (!finite(value.opacity) || value.opacity < 0 || value.opacity > 1)) add(`${path}.opacity`, "must be 0\u20131");
8702
+ }
8703
+ const componentPropsById = /* @__PURE__ */ new Map();
8704
+ function owningComponentProperties(ancestors) {
8705
+ for (let index = ancestors.length - 1; index >= 0; index -= 1) {
8706
+ const ancestor = ancestors[index];
8707
+ if (ancestor.type === "INSTANCE") return null;
8708
+ if (ancestor.type === "COMPONENT") return ancestor.componentProperties || [];
8709
+ }
8710
+ return null;
8711
+ }
8712
+ function validateComponentPropertyReferences(node, path, ancestors) {
8713
+ const refs = node.properties.componentPropertyReferences;
8714
+ if (refs === void 0) return;
8715
+ if (!refs || typeof refs !== "object" || Array.isArray(refs)) return add(`${path}.componentPropertyReferences`, "must be an object");
8716
+ const owning = owningComponentProperties(ancestors);
8717
+ if (owning === null) return add(`${path}.componentPropertyReferences`, "must be on a descendant of a COMPONENT and must not cross a nested INSTANCE");
8718
+ const byName = new Map(owning.map((property) => [property.name, property]));
8719
+ const entries = refs;
8720
+ for (const field of Object.keys(entries)) {
8721
+ if (!["characters", "visible", "mainComponent"].includes(field)) add(`${path}.componentPropertyReferences.${field}`, "supported fields are characters, visible, and mainComponent");
8722
+ }
8723
+ const characters = entries.characters;
8724
+ if (characters !== void 0) {
8725
+ if (typeof characters !== "string" || !characters) add(`${path}.componentPropertyReferences.characters`, "must be a non-empty property name");
8726
+ else if (node.type !== "TEXT") add(`${path}.componentPropertyReferences.characters`, "characters may only be set on TEXT nodes");
8727
+ else {
8728
+ const property = byName.get(characters);
8729
+ if (!property) add(`${path}.componentPropertyReferences.characters`, `property '${characters}' is not defined on the owning COMPONENT`);
8730
+ else if (property.type !== "TEXT") add(`${path}.componentPropertyReferences.characters`, `property '${characters}' has type ${property.type} but characters requires TEXT`);
8731
+ }
8732
+ }
8733
+ const visible = entries.visible;
8734
+ if (visible !== void 0) {
8735
+ if (typeof visible !== "string" || !visible) add(`${path}.componentPropertyReferences.visible`, "must be a non-empty property name");
8736
+ else {
8737
+ const property = byName.get(visible);
8738
+ if (!property) add(`${path}.componentPropertyReferences.visible`, `property '${visible}' is not defined on the owning COMPONENT`);
8739
+ else if (property.type !== "BOOLEAN") add(`${path}.componentPropertyReferences.visible`, `property '${visible}' has type ${property.type} but visible requires BOOLEAN`);
8740
+ }
8741
+ }
8742
+ const mainComponent = entries.mainComponent;
8743
+ if (mainComponent !== void 0) {
8744
+ if (typeof mainComponent !== "string" || !mainComponent) add(`${path}.componentPropertyReferences.mainComponent`, "must be a non-empty property name");
8745
+ else if (node.type !== "INSTANCE") add(`${path}.componentPropertyReferences.mainComponent`, "mainComponent may only be set on INSTANCE nodes");
8746
+ else {
8747
+ const property = byName.get(mainComponent);
8748
+ if (!property) add(`${path}.componentPropertyReferences.mainComponent`, `property '${mainComponent}' is not defined on the owning COMPONENT`);
8749
+ else if (property.type !== "INSTANCE_SWAP") add(`${path}.componentPropertyReferences.mainComponent`, `property '${mainComponent}' has type ${property.type} but mainComponent requires INSTANCE_SWAP`);
8750
+ }
8751
+ }
8752
+ }
8753
+ function walk(node, path, ancestors = []) {
8754
+ if (ids.has(node.id)) add(`${path}.id`, `duplicate ID '${node.id}'`);
8755
+ else ids.add(node.id);
8756
+ if (!SUPPORTED_NODE_TYPES.has(node.type)) add(`${path}.type`, `unsupported node type '${node.type}'`);
8757
+ nodeTypes.set(node.id, node.type);
8758
+ if (node.type === "COMPONENT") {
8759
+ components.add(node.id);
8760
+ if (node.properties.componentProperties) componentPropsById.set(node.id, node.properties.componentProperties);
8761
+ }
8762
+ const props = node.properties;
8763
+ if (!finite(props.position?.x) || !finite(props.position?.y)) add(`${path}.position`, "x and y must be finite numbers");
8764
+ if (!finite(props.size?.width) || props.size.width <= 0 || !finite(props.size?.height) || props.size.height <= 0) add(`${path}.size`, "w and h must be positive finite numbers");
8765
+ if (props.cornerRadius !== void 0 && (!finite(props.cornerRadius) || props.cornerRadius < 0)) add(`${path}.cornerRadius`, "must be non-negative");
8766
+ if (props.cornerRadii !== void 0 && (!Array.isArray(props.cornerRadii) || props.cornerRadii.length !== 4 || props.cornerRadii.some((n) => !finite(n) || n < 0))) add(`${path}.cornerRadii`, "must contain four non-negative numbers");
8767
+ if (props.layout) {
8768
+ if (!props.layout.direction || !["HORIZONTAL", "VERTICAL", "GRID"].includes(props.layout.direction)) add(`${path}.layout.direction`, "must be HORIZONTAL, VERTICAL, or GRID");
8769
+ for (const key of ["primaryAxisSizingMode", "counterAxisSizingMode"]) {
8770
+ const value = props.layout[key];
8771
+ if (value !== void 0 && !["FIXED", "HUG", "AUTO"].includes(value)) add(`${path}.layout.${key}`, "must be FIXED or HUG");
8772
+ }
8773
+ if (props.layout.primaryAxisAlignItems !== void 0 && !["MIN", "MAX", "CENTER", "SPACE_BETWEEN"].includes(props.layout.primaryAxisAlignItems)) add(`${path}.layout.primaryAxisAlignItems`, "must be MIN, MAX, CENTER, or SPACE_BETWEEN");
8774
+ if (props.layout.counterAxisAlignItems !== void 0 && !["MIN", "MAX", "CENTER", "BASELINE", "STRETCH"].includes(props.layout.counterAxisAlignItems)) add(`${path}.layout.counterAxisAlignItems`, "must be MIN, MAX, CENTER, BASELINE, or STRETCH");
8775
+ }
8776
+ for (const key of ["fills", "strokes"]) props.styles[key].forEach((item, index) => validatePaint(item, `${path}.${key}[${index}]`));
8777
+ if (node.type === "TEXT" && typeof props.text !== "string") add(`${path}.text`, "TEXT requires text or runs");
8778
+ if (node.type === "SVG" && typeof props.svg !== "string") add(`${path}.svg`, "SVG requires markup");
8779
+ if (node.type === "VECTOR" && !Array.isArray(props.vectorPaths)) add(`${path}.vectorPaths`, "VECTOR requires vectorPaths");
8780
+ if (node.type === "INSTANCE" && typeof props.componentId !== "string") add(`${path}.componentId`, "INSTANCE requires componentId");
8781
+ if (node.type === "BOOLEAN_OPERATION" && (typeof props.operation !== "string" || !["UNION", "SUBTRACT", "INTERSECT", "EXCLUDE"].includes(props.operation))) add(`${path}.operation`, "BOOLEAN_OPERATION requires operation UNION, SUBTRACT, INTERSECT, or EXCLUDE");
8782
+ if (node.type === "COMPONENT_SET" && (!node.children.length || node.children.some((child) => child.type !== "COMPONENT"))) add(`${path}.children`, "COMPONENT_SET requires COMPONENT children");
8783
+ if (node.type === "COMPONENT_SET" && props.variantAxes) {
8784
+ for (const [axis, options] of Object.entries(props.variantAxes)) if (!axis || !Array.isArray(options) || !options.length || options.some((option) => typeof option !== "string")) add(`${path}.variantAxes.${axis}`, "must be a non-empty string array");
8785
+ const variantAxes = props.variantAxes;
8786
+ node.children.forEach((child, index) => Object.entries(child.properties.variant || {}).forEach(([axis, value]) => {
8787
+ if (!variantAxes[axis]?.includes(value)) add(`${path}.children[${index}].variant.${axis}`, `value '${value}' is not declared in variantAxes`);
8788
+ }));
8789
+ for (const message of variantAxesUncarriedOptions(node, variantAxes)) {
8790
+ const match = /^variantAxes\.([^:]+):\s*(.*)$/.exec(message);
8791
+ if (match) add(`${path}.variantAxes.${match[1]}`, match[2]);
8792
+ else add(path, message);
8793
+ }
8794
+ }
8795
+ if (props.prototype !== void 0) Array.isArray(props.prototype) ? validatePrototype(props.prototype, `${path}.prototype`) : add(`${path}.prototype`, "must be an array of reactions");
8796
+ if (["GROUP", "BOOLEAN_OPERATION"].includes(node.type) && !node.children.length) add(`${path}.children`, `${node.type} requires children`);
8797
+ validateComponentPropertyReferences(node, path, ancestors);
8798
+ const nextAncestors = [...ancestors, { type: node.type, id: node.id, componentProperties: props.componentProperties }];
8799
+ node.children.forEach((child, index) => walk(child, `${path}.children[${index}]`, nextAncestors));
8800
+ }
8801
+ document.nodes.forEach((node, index) => walk(node, `nodes[${index}]`));
8802
+ function requireComponentId(targetId, issuePath) {
8803
+ const targetType = nodeTypes.get(targetId);
8804
+ if (!targetType) add(issuePath, `component '${targetId}' is not defined in this import`);
8805
+ else if (targetType !== "COMPONENT") add(issuePath, `'${targetId}' is a ${targetType} but INSTANCE_SWAP requires a COMPONENT`);
8806
+ }
8807
+ function references(node, path) {
8808
+ if (node.type === "INSTANCE" && (!node.properties.componentId || !components.has(node.properties.componentId))) add(`${path}.componentId`, `component '${node.properties.componentId || ""}' is not defined in this import`);
8809
+ if (node.type === "COMPONENT" && node.properties.componentProperties) {
8810
+ node.properties.componentProperties.forEach((property, index) => {
8811
+ if (property.type !== "INSTANCE_SWAP") return;
8812
+ if (typeof property.defaultValue !== "string" || !property.defaultValue) {
8813
+ add(`${path}.componentProperties[${index}].defaultValue`, "INSTANCE_SWAP requires a component id string");
8814
+ return;
8815
+ }
8816
+ requireComponentId(property.defaultValue, `${path}.componentProperties[${index}].defaultValue`);
8817
+ });
8818
+ }
8819
+ const overrides = node.properties.instanceProperties;
8820
+ if (node.type === "INSTANCE" && overrides && typeof node.properties.componentId === "string") {
8821
+ const declared = componentPropsById.get(node.properties.componentId);
8822
+ if (declared) {
8823
+ const byName = new Map(declared.map((property) => [property.name, property]));
8824
+ for (const [key, value] of Object.entries(overrides)) {
8825
+ const property = byName.get(key);
8826
+ if (!property) {
8827
+ add(`${path}.instanceProperties.${key}`, `property '${key}' is not defined on component '${node.properties.componentId}'`);
8828
+ continue;
8829
+ }
8830
+ if (property.type === "BOOLEAN" && typeof value !== "boolean") add(`${path}.instanceProperties.${key}`, `property '${key}' expects a boolean`);
8831
+ else if ((property.type === "TEXT" || property.type === "INSTANCE_SWAP") && typeof value !== "string" && !(value && typeof value === "object" && value.type === "VARIABLE_ALIAS")) add(`${path}.instanceProperties.${key}`, `property '${key}' expects a string`);
8832
+ else if (property.type === "INSTANCE_SWAP" && typeof value === "string") requireComponentId(value, `${path}.instanceProperties.${key}`);
8833
+ }
8834
+ }
8835
+ }
8836
+ node.children.forEach((child, index) => references(child, `${path}.children[${index}]`));
8837
+ }
8838
+ document.nodes.forEach((node, index) => references(node, `nodes[${index}]`));
8839
+ const styleKeys = /* @__PURE__ */ new Map();
8840
+ for (const style of document.styles || []) {
8841
+ if (typeof style.id === "string" && style.id) styleKeys.set(style.id, style.type);
8842
+ if (typeof style.name === "string" && style.name) styleKeys.set(style.name, style.type);
8843
+ }
8844
+ const variableKeys = /* @__PURE__ */ new Set();
8845
+ const collectionNames = /* @__PURE__ */ new Set();
8846
+ const modesByCollection = /* @__PURE__ */ new Map();
8847
+ for (const [collectionIndex, collection] of (document.variables || []).entries()) {
8848
+ const modes = Array.isArray(collection.modes) && collection.modes.length ? collection.modes : [];
8849
+ if (collection.modes !== void 0 && (!modes.length || modes.some((mode) => typeof mode !== "string"))) add(`variables[${collectionIndex}].modes`, "must be a non-empty array of mode names");
8850
+ if (typeof collection.name === "string" && collection.name) {
8851
+ collectionNames.add(collection.name);
8852
+ if (collection.modes !== void 0 && modes.length && modes.every((mode) => typeof mode === "string")) {
8853
+ modesByCollection.set(collection.name, modes);
8854
+ }
8855
+ }
8856
+ for (const [variableIndex, variable] of (collection.items || []).entries()) {
8857
+ const path = `variables[${collectionIndex}].items[${variableIndex}]`;
8858
+ if (!variable.name || typeof variable.name !== "string") add(`${path}.name`, "must be a non-empty string");
8859
+ if (!["COLOR", "FLOAT", "STRING", "BOOLEAN"].includes(variable.type)) add(`${path}.type`, "must be COLOR, FLOAT, STRING, or BOOLEAN");
8860
+ if (typeof variable.id === "string" && variable.id) variableKeys.add(variable.id);
8861
+ if (typeof variable.name === "string" && variable.name) variableKeys.add(variable.name);
8862
+ const values = variable.values || (variable.value !== void 0 ? { default: variable.value } : {});
8863
+ if (!Object.keys(values).length) add(`${path}`, "requires value or values");
8864
+ if (variable.values && modes.length) {
8865
+ for (const mode of modes) if (!(mode in variable.values)) add(`${path}.values.${mode}`, "missing value for declared mode");
8866
+ }
8867
+ for (const [mode, value] of Object.entries(values)) {
8868
+ if (variable.type === "FLOAT" && !finite(value)) add(`${path}.${mode}`, "FLOAT value must be a finite number");
8869
+ if (variable.type === "STRING" && typeof value !== "string") add(`${path}.${mode}`, "STRING value must be a string");
8870
+ if (variable.type === "BOOLEAN" && typeof value !== "boolean") add(`${path}.${mode}`, "BOOLEAN value must be true or false");
8871
+ const color = value && typeof value === "object" && !Array.isArray(value) ? value : null;
8872
+ if (variable.type === "COLOR" && (!color || !finite(color.r) || !finite(color.g) || !finite(color.b))) add(`${path}.${mode}`, "COLOR value requires numeric r, g, and b channels");
8873
+ }
8874
+ }
8875
+ }
8876
+ function tokenReferences(node, path) {
8877
+ const bindings = node.properties.bindings;
8878
+ if (bindings) {
8879
+ for (const [field, key] of Object.entries(bindings)) {
8880
+ if (typeof key !== "string" || !key) {
8881
+ add(`${path}.bindings.${field}`, "must be a non-empty variable id or name");
8882
+ continue;
8883
+ }
8884
+ if (!variableKeys.has(key)) add(`${path}.bindings.${field}`, `variable '${key}' is not defined in this document`);
8885
+ }
8886
+ }
8887
+ const variableModes = node.properties.variableModes;
8888
+ if (variableModes) {
8889
+ for (const [collectionName, modeName] of Object.entries(variableModes)) {
8890
+ if (!collectionNames.has(collectionName)) {
8891
+ add(`${path}.variableModes.${collectionName}`, `variable collection '${collectionName}' is not defined in this document`);
8892
+ continue;
8893
+ }
8894
+ if (typeof modeName !== "string" || !modeName) {
8895
+ add(`${path}.variableModes.${collectionName}`, "must be a non-empty mode name");
8896
+ continue;
8897
+ }
8898
+ const declared = modesByCollection.get(collectionName);
8899
+ if (declared && !declared.includes(modeName)) add(`${path}.variableModes.${collectionName}`, `mode '${modeName}' is not declared on collection '${collectionName}'`);
8900
+ }
8901
+ }
8902
+ const styleRefs = node.properties.styleRefs;
8903
+ if (styleRefs) {
8904
+ for (const [field, key] of Object.entries(styleRefs)) {
8905
+ if (typeof key !== "string" || !key) {
8906
+ add(`${path}.styleRefs.${field}`, "must be a non-empty style id or name");
8907
+ continue;
8908
+ }
8909
+ const styleType = styleKeys.get(key);
8910
+ if (!styleType) {
8911
+ add(`${path}.styleRefs.${field}`, `style '${key}' is not defined in this document`);
8912
+ continue;
8913
+ }
8914
+ if ((field === "fill" || field === "stroke") && styleType !== "PAINT") {
8915
+ add(`${path}.styleRefs.${field}`, `style '${key}' has type ${styleType} but ${field} requires PAINT`);
8916
+ } else if (field === "text" && styleType !== "TEXT") {
8917
+ add(`${path}.styleRefs.${field}`, `style '${key}' has type ${styleType} but text requires TEXT`);
8918
+ }
8919
+ }
8920
+ }
8921
+ node.children.forEach((child, index) => tokenReferences(child, `${path}.children[${index}]`));
8922
+ }
8923
+ document.nodes.forEach((node, index) => tokenReferences(node, `nodes[${index}]`));
8924
+ for (const message of prototypeDestinationErrors(document)) {
8925
+ const split = message.indexOf(": ");
8926
+ add(split > 0 ? message.slice(0, split) : "$", split > 0 ? message.slice(split + 2) : message);
8927
+ }
8928
+ return errors;
8929
+ }
8930
+ function documentIssueOwners(document) {
8931
+ const nodePaths = /* @__PURE__ */ new Map();
8932
+ const visit = (node, path) => {
8933
+ nodePaths.set(path, node.id);
8934
+ node.children.forEach((child, index) => visit(child, `${path}.children[${index}]`));
8935
+ };
8936
+ document.nodes.forEach((node, index) => visit(node, `nodes[${index}]`));
8937
+ const variablePaths = /* @__PURE__ */ new Map();
8938
+ (document.variables || []).forEach((collection, collectionIndex) => {
8939
+ const collectionKey = typeof collection.name === "string" && collection.name ? collection.name : `#${collectionIndex}`;
8940
+ variablePaths.set(`variables[${collectionIndex}]`, `collection:${collectionKey}`);
8941
+ (collection.items || []).forEach((variable, variableIndex) => {
8942
+ const variableKey = typeof variable.id === "string" && variable.id ? variable.id : typeof variable.name === "string" && variable.name ? variable.name : `#${variableIndex}`;
8943
+ variablePaths.set(`variables[${collectionIndex}].items[${variableIndex}]`, `variable:${collectionKey}/${variableKey}`);
8944
+ });
8945
+ });
8946
+ const longest = (paths, path) => {
8947
+ let best;
8948
+ for (const [prefix, owner] of paths) if ((path === prefix || path.startsWith(`${prefix}.`)) && (!best || prefix.length > best[0].length)) best = [prefix, owner];
8949
+ return best;
8950
+ };
8951
+ return (path) => {
8952
+ const node = longest(nodePaths, path);
8953
+ if (node) return { owner: `node:${node[1]}`, nodeId: node[1], rest: path.slice(node[0].length) };
8954
+ const variable = longest(variablePaths, path);
8955
+ if (variable) return { owner: variable[1], rest: path.slice(variable[0].length) };
8956
+ return { owner: "document", rest: path };
8957
+ };
8958
+ }
8959
+
8960
+ // src/patch.ts
8961
+ var PatchError = class extends Error {
8962
+ issues;
8963
+ constructor(issues) {
8964
+ super(issues.map((issue2) => `${issue2.path}: ${issue2.message}`).join("\n"));
8965
+ this.name = "PatchError";
8966
+ this.issues = issues;
8967
+ }
8968
+ };
8969
+ var CONTAINER_TYPES = /* @__PURE__ */ new Set(["FRAME", "GROUP", "COMPONENT", "COMPONENT_SET", "SECTION", "BOOLEAN_OPERATION"]);
8970
+ var RUN_STYLE_KEYS = ["font", "fill", "letterSpacing", "textDecoration", "link"];
8971
+ function cloneValue2(value) {
8972
+ if (value instanceof Uint8Array) return new Uint8Array(value);
8973
+ if (Array.isArray(value)) return value.map(cloneValue2);
8974
+ if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, cloneValue2(item)]));
8975
+ return value;
8976
+ }
8977
+ function cloneNode(node) {
8978
+ return { ...node, properties: cloneValue2(node.properties), children: node.children.map(cloneNode) };
8979
+ }
8980
+ function translate(node, dx, dy) {
8981
+ if (!dx && !dy) return;
8982
+ node.properties.position = { x: node.properties.position.x + dx, y: node.properties.position.y + dy };
8983
+ for (const child of node.children) translate(child, dx, dy);
8984
+ }
8985
+ function origin(parent) {
8986
+ return parent ? { x: parent.properties.position.x, y: parent.properties.position.y } : { x: 0, y: 0 };
8987
+ }
8988
+ var isObject2 = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
8989
+ function corePatchContext(node, parent) {
8990
+ const props = node.properties;
8991
+ const runs = Array.isArray(props.runs) ? props.runs : [];
8992
+ return {
8993
+ type: node.type,
8994
+ parentType: parent ? parent.type : null,
8995
+ parentAutoLayout: Boolean(parent?.properties.layout?.direction),
8996
+ autoLayout: Boolean(props.layout?.direction),
8997
+ layoutPositioning: props.layoutPositioning,
8998
+ mixedText: node.type === "TEXT" && runs.some((run) => RUN_STYLE_KEYS.some((key) => run[key] !== void 0)),
8999
+ mixedFontName: node.type === "TEXT" && runs.some((run) => isObject2(run.font) && (run.font.family !== void 0 || run.font.style !== void 0)),
9000
+ textStyle: Boolean(props.styleRefs?.text),
9001
+ boundFields: Object.keys(props.bindings || {}),
9002
+ childCount: node.children.length
9003
+ };
9004
+ }
9005
+ function detachField(props, mapKey, field) {
9006
+ const map = props[mapKey];
9007
+ if (!map || map[field] === void 0) return false;
9008
+ const next = { ...map };
9009
+ delete next[field];
9010
+ if (Object.keys(next).length) props[mapKey] = next;
9011
+ else delete props[mapKey];
9012
+ return true;
9013
+ }
9014
+ function detach(props, field) {
9015
+ const warnings = [];
9016
+ if (detachField(props, "bindings", field)) warnings.push(`detached bindings.${field}`);
9017
+ if (detachField(props, "styleRefs", field)) warnings.push(`detached styleRefs.${field}`);
9018
+ return warnings;
9019
+ }
9020
+ function mergeObjectMap(current, patch, assign) {
9021
+ if (patch === void 0) return;
9022
+ if (patch === null) {
9023
+ assign(void 0);
9024
+ return;
9025
+ }
9026
+ const next = { ...current || {} };
9027
+ for (const [key, value] of Object.entries(patch)) {
9028
+ if (value === null) delete next[key];
9029
+ else next[key] = value;
9030
+ }
9031
+ assign(Object.keys(next).length ? next : void 0);
9032
+ }
9033
+ function stripRuns(props, keys) {
9034
+ if (!Array.isArray(props.runs)) return;
9035
+ props.runs = props.runs.map((run) => {
9036
+ const next = { ...run };
9037
+ for (const key of keys) {
9038
+ if (key.startsWith("font.")) {
9039
+ if (isObject2(next.font)) {
9040
+ const font = { ...next.font };
9041
+ delete font[key.slice(5)];
9042
+ if (Object.keys(font).length) next.font = font;
9043
+ else delete next.font;
9044
+ }
9045
+ } else delete next[key];
9046
+ }
9047
+ return next;
9048
+ });
9049
+ }
9050
+ function mergeSet(ref, values, set, warnings) {
9051
+ const { node, parent } = ref;
9052
+ const props = node.properties;
9053
+ const path = `node '${node.id}'`;
9054
+ if (values.name !== void 0) node.name = values.name;
9055
+ if (values.position) {
9056
+ const base = origin(parent);
9057
+ const x = values.position.x !== void 0 ? base.x + values.position.x : props.position.x;
9058
+ const y = values.position.y !== void 0 ? base.y + values.position.y : props.position.y;
9059
+ translate(node, x - props.position.x, y - props.position.y);
9060
+ }
9061
+ if (values.size) props.size = { width: values.size.width ?? props.size.width, height: values.size.height ?? props.size.height };
9062
+ const styleKeys = [];
9063
+ if (values.styles?.fills) {
9064
+ props.styles.fills = cloneValue2(values.styles.fills);
9065
+ for (const message of detach(props, "fill")) warnings.push(`${path}: ${message} because fill was set`);
9066
+ styleKeys.push("fill");
9067
+ }
9068
+ if (values.styles?.strokes) {
9069
+ props.styles.strokes = cloneValue2(values.styles.strokes);
9070
+ for (const message of detach(props, "stroke")) warnings.push(`${path}: ${message} because stroke was set`);
9071
+ }
9072
+ if (values.styles?.effects) props.styles.effects = cloneValue2(values.styles.effects);
9073
+ if (values.layout) {
9074
+ const current = props.layout || {};
9075
+ const padding = values.layout.padding ? { ...current.padding || {}, ...values.layout.padding } : current.padding;
9076
+ props.layout = { ...current, ...cloneValue2(values.layout), ...padding ? { padding } : {} };
9077
+ }
9078
+ if (values.constraints) props.constraints = { horizontal: "MIN", vertical: "MIN", ...props.constraints || {}, ...values.constraints };
9079
+ if (node.type === "TEXT") {
9080
+ if (values.font || setTouchesTextStyle(set)) {
9081
+ if (props.styleRefs?.text) {
9082
+ detachField(props, "styleRefs", "text");
9083
+ warnings.push(`${path}: detached styleRefs.text because typography was set`);
9084
+ }
9085
+ }
9086
+ if (values.font) {
9087
+ stripRuns(props, Object.keys(values.font).map((key) => `font.${key}`));
9088
+ props.font = { ...props.font || {}, ...values.font };
9089
+ }
9090
+ if (values.letterSpacing !== void 0) styleKeys.push("letterSpacing");
9091
+ if (values.textDecoration !== void 0) styleKeys.push("textDecoration");
9092
+ stripRuns(props, styleKeys);
9093
+ if (values.runs !== void 0) {
9094
+ props.runs = cloneValue2(values.runs);
9095
+ props.text = values.text !== void 0 ? values.text : values.runs.map((run) => typeof run.text === "string" ? run.text : "").join("");
9096
+ } else if (values.text !== void 0) {
9097
+ props.text = values.text;
9098
+ delete props.runs;
9099
+ }
9100
+ }
9101
+ const overwritten = patchSetBindingFields(set).filter((field) => props.bindings && props.bindings[field] !== void 0);
9102
+ for (const field of [...new Set(overwritten)]) {
9103
+ detachField(props, "bindings", field);
9104
+ warnings.push(`${path}: detached bindings.${field} because the set overwrites it`);
9105
+ }
9106
+ mergeObjectMap(props.bindings, values.bindings, (next) => {
9107
+ if (next) props.bindings = next;
9108
+ else delete props.bindings;
9109
+ });
9110
+ mergeObjectMap(props.styleRefs, values.styleRefs, (next) => {
9111
+ if (next) props.styleRefs = next;
9112
+ else delete props.styleRefs;
9113
+ });
9114
+ mergeObjectMap(props.variableModes, values.variableModes, (next) => {
9115
+ if (next) props.variableModes = next;
9116
+ else delete props.variableModes;
9117
+ });
9118
+ mergeObjectMap(props.instanceProperties, values.instanceProperties, (next) => {
9119
+ if (next) props.instanceProperties = next;
9120
+ else delete props.instanceProperties;
9121
+ });
9122
+ mergeObjectMap(props.componentPropertyReferences, values.componentPropertyReferences, (next) => {
9123
+ if (next) props.componentPropertyReferences = next;
9124
+ else delete props.componentPropertyReferences;
9125
+ });
9126
+ const handled = /* @__PURE__ */ new Set(["name", "position", "size", "styles", "layout", "constraints", "font", "runs", "text", "bindings", "styleRefs", "variableModes", "instanceProperties", "componentPropertyReferences", "componentProperties", "variantAxes", "variant"]);
9127
+ for (const [key, value] of Object.entries(values)) {
9128
+ if (handled.has(key)) continue;
9129
+ Object.assign(props, { [key]: cloneValue2(value) });
9130
+ }
9131
+ }
9132
+ function setTouchesTextStyle(set) {
9133
+ return ["lineHeight", "letterSpacing", "paragraphSpacing", "paragraphIndent", "listSpacing", "textCase", "textDecoration", "hangingPunctuation", "hangingList"].some((key) => key in set);
9134
+ }
9135
+ function clampIndex(index, length) {
9136
+ return Math.max(0, Math.min(index, length));
9137
+ }
9138
+ function isDescendantOf(ancestor, candidate) {
9139
+ for (const child of ancestor.children) {
9140
+ if (child === candidate || isDescendantOf(child, candidate)) return true;
9141
+ }
9142
+ return false;
9143
+ }
9144
+ function hasInstanceAncestor(node, index) {
9145
+ let current = node;
9146
+ while (current) {
9147
+ if (current.type === "INSTANCE") return true;
9148
+ const ref = index.get(current.id);
9149
+ current = ref?.parent ?? null;
9150
+ }
9151
+ return false;
9152
+ }
9153
+ function assertContainerParent(parent, operationIndex, parentId, index) {
9154
+ if (parent.type === "INSTANCE" || hasInstanceAncestor(parent, index)) {
9155
+ throw new Error(`patch.operations[${operationIndex}]: parent '${parentId}' is an INSTANCE (or inside one) and cannot accept structural edits.`);
9156
+ }
9157
+ if (!CONTAINER_TYPES.has(parent.type)) {
9158
+ throw new Error(`patch.operations[${operationIndex}]: parent '${parentId}' cannot contain children.`);
9159
+ }
9160
+ }
9161
+ function assertNotInsideInstance(parent, operationIndex, id, index) {
9162
+ if (parent && hasInstanceAncestor(parent, index)) {
9163
+ throw new Error(`patch.operations[${operationIndex}]: node '${id}' is inside an INSTANCE and cannot be moved.`);
9164
+ }
9165
+ }
9166
+ function patchIssue(code, path, message, suggestion) {
9167
+ return { severity: "ERROR", code, path, message, suggestion };
9168
+ }
9169
+ function applyOperation(operation, operationIndex, index, visit, affectedIds, warnings, roots, duplicateMaps) {
9170
+ if (operation.op === "APPEND" || operation.op === "INSERT") {
9171
+ const target2 = operation.parent ? index.get(operation.parent) : void 0;
9172
+ if (!target2 || !operation.node) throw new Error(`patch.operations[${operationIndex}]: parent '${operation.parent || ""}' was not found.`);
9173
+ assertContainerParent(target2.node, operationIndex, operation.parent || "", index);
9174
+ if (index.has(operation.node.id)) throw new Error(`patch.operations[${operationIndex}]: ID '${operation.node.id}' already exists.`);
9175
+ const child = cloneNode(operation.node);
9176
+ const base = origin(target2.node);
9177
+ translate(child, base.x, base.y);
9178
+ if (target2.node.type === "COMPONENT_SET" && child.type === "COMPONENT") {
9179
+ const axes = target2.node.properties.variantAxes || {};
9180
+ const selected = child.properties.variant || {};
9181
+ if (Object.keys(axes).length) {
9182
+ child.name = Object.keys(axes).map((axis) => `${axis}=${selected[axis] ?? axes[axis][0]}`).join(", ");
9183
+ } else if (Object.keys(selected).length) {
9184
+ child.name = Object.entries(selected).map(([axis, value]) => `${axis}=${value}`).join(", ");
9185
+ }
9186
+ }
9187
+ if (operation.op === "APPEND") {
9188
+ target2.node.children.push(child);
9189
+ } else {
9190
+ const at = clampIndex(operation.index ?? 0, target2.node.children.length);
9191
+ target2.node.children.splice(at, 0, child);
9192
+ }
9193
+ visit([child], target2.node);
9194
+ affectedIds.push(child.id);
9195
+ return;
9196
+ }
9197
+ if (operation.op === "DUPLICATE") {
9198
+ const sourceRef = operation.id ? index.get(operation.id) : void 0;
9199
+ if (!sourceRef) throw new Error(`patch.operations[${operationIndex}]: node '${operation.id || ""}' was not found.`);
9200
+ assertNotInsideInstance(sourceRef.parent, operationIndex, operation.id || "", index);
9201
+ if (subtreeContainsType(sourceRef.node, /* @__PURE__ */ new Set(["COMPONENT", "COMPONENT_SET"]))) {
9202
+ throw new Error(`patch.operations[${operationIndex}]: cannot duplicate a COMPONENT or COMPONENT_SET (or a subtree that contains one)`);
9203
+ }
9204
+ const path = `patch.operations[${operationIndex}]`;
9205
+ const { map, errors } = buildDuplicateIdMap(sourceRef.node, operation.idSuffix || "", operation.ids, path);
9206
+ if (errors.length) throw new Error(errors[0]);
9207
+ for (const [, newId] of map) {
9208
+ if (index.has(newId)) throw new Error(`${path}: new id '${newId}' already exists`);
9209
+ }
9210
+ let destParent;
9211
+ let destSiblings;
9212
+ if (operation.parent !== void 0) {
9213
+ const dest = index.get(operation.parent);
9214
+ if (!dest) throw new Error(`${path}: parent '${operation.parent}' was not found.`);
9215
+ assertContainerParent(dest.node, operationIndex, operation.parent, index);
9216
+ destParent = dest.node;
9217
+ destSiblings = dest.node.children;
9218
+ } else if (sourceRef.parent) {
9219
+ destParent = sourceRef.parent;
9220
+ destSiblings = sourceRef.siblings;
9221
+ } else {
9222
+ destParent = null;
9223
+ destSiblings = roots;
9224
+ }
9225
+ if (destParent) assertContainerParent(destParent, operationIndex, destParent.id, index);
9226
+ const copy = cloneSubtreeWithIds(sourceRef.node, map);
9227
+ const fromOrigin = origin(sourceRef.parent);
9228
+ const toOrigin = origin(destParent);
9229
+ translate(copy, toOrigin.x - fromOrigin.x, toOrigin.y - fromOrigin.y);
9230
+ let at;
9231
+ if (operation.index !== void 0) {
9232
+ at = clampIndex(operation.index, destSiblings.length);
9233
+ } else if (destParent === sourceRef.parent || destParent === null && sourceRef.parent === null) {
9234
+ const sourceIndex = sourceRef.siblings.indexOf(sourceRef.node);
9235
+ at = clampIndex(sourceIndex + 1, destSiblings.length);
9236
+ } else {
9237
+ at = destSiblings.length;
9238
+ }
9239
+ destSiblings.splice(at, 0, copy);
9240
+ visit([copy], destParent);
9241
+ duplicateMaps.push(map);
9242
+ affectedIds.push(copy.id);
9243
+ return;
9244
+ }
9245
+ if (operation.op === "MOVE") {
9246
+ const moving = operation.id ? index.get(operation.id) : void 0;
9247
+ if (!moving) throw new Error(`patch.operations[${operationIndex}]: node '${operation.id || ""}' was not found.`);
9248
+ if (!moving.parent) throw new Error(`patch.operations[${operationIndex}]: cannot move the document root.`);
9249
+ assertNotInsideInstance(moving.parent, operationIndex, operation.id || "", index);
9250
+ const destination = operation.parent ? index.get(operation.parent) : void 0;
9251
+ if (!destination) throw new Error(`patch.operations[${operationIndex}]: parent '${operation.parent || ""}' was not found.`);
9252
+ assertContainerParent(destination.node, operationIndex, operation.parent || "", index);
9253
+ if (destination.node === moving.node || isDescendantOf(moving.node, destination.node)) {
9254
+ throw new Error(`patch.operations[${operationIndex}]: cannot move a node under itself or its descendants.`);
9255
+ }
9256
+ const from = origin(moving.parent);
9257
+ const to = origin(destination.node);
9258
+ translate(moving.node, to.x - from.x, to.y - from.y);
9259
+ const fromIndex = moving.siblings.indexOf(moving.node);
9260
+ moving.siblings.splice(fromIndex, 1);
9261
+ const at = clampIndex(operation.index ?? 0, destination.node.children.length);
9262
+ destination.node.children.splice(at, 0, moving.node);
9263
+ index.set(moving.node.id, { node: moving.node, parent: destination.node, siblings: destination.node.children });
9264
+ visit(moving.node.children, moving.node);
9265
+ affectedIds.push(moving.node.id);
9266
+ return;
9267
+ }
9268
+ const target = operation.id ? index.get(operation.id) : void 0;
9269
+ if (!target) throw new Error(`patch.operations[${operationIndex}]: node '${operation.id || ""}' was not found.`);
9270
+ if (operation.op === "REMOVE") {
9271
+ const removeAll = (node) => {
9272
+ index.delete(node.id);
9273
+ node.children.forEach(removeAll);
9274
+ };
9275
+ target.siblings.splice(target.siblings.indexOf(target.node), 1);
9276
+ removeAll(target.node);
9277
+ affectedIds.push(target.node.id);
9278
+ return;
9279
+ }
9280
+ const set = operation.set || {};
9281
+ const values = operation.normalized;
9282
+ if (!values) throw new Error(`patch.operations[${operationIndex}]: SET '${operation.id}' was not normalized.`);
9283
+ const issues = patchSetTargetIssues(set, corePatchContext(target.node, target.parent));
9284
+ if (issues.length) {
9285
+ throw new PatchError(issues.map((message) => {
9286
+ const split = message.indexOf(":");
9287
+ return patchIssue("PATCH_SET_INVALID", `patch.operations[${operationIndex}].set.${message.slice(0, split)}`, message.slice(split + 1).trim(), "Use a key that applies to this node, or see the patch rules in DESIGN-LANGUAGE.md.");
9288
+ }));
9289
+ }
9290
+ if (values.prototype !== void 0) {
9291
+ const protoErrors = assertPrototypePatchRules(
9292
+ values.prototype,
9293
+ {
9294
+ has: (id) => index.has(id),
9295
+ typeOf: (id) => index.get(id)?.node.type,
9296
+ canvasOf: (id) => canvasRootId(roots, id),
9297
+ isTopLevel: (id) => isTopLevelNodeId(roots, id)
9298
+ },
9299
+ `patch.operations[${operationIndex}].set.prototype`,
9300
+ target.node.id,
9301
+ isTopLevelNodeId(roots, target.node.id)
9302
+ );
9303
+ if (protoErrors.length) {
9304
+ throw new PatchError(protoErrors.map((message) => {
9305
+ const split = message.indexOf(": ");
9306
+ return patchIssue("PATCH_OPERATION", split > 0 ? message.slice(0, split) : `patch.operations[${operationIndex}].set.prototype`, split > 0 ? message.slice(split + 2) : message, "Use a top-level frame destination; AFTER_TIMEOUT only on a top-level node.");
9307
+ }));
9308
+ }
9309
+ }
9310
+ mergeSet(target, values, set, warnings);
9311
+ const defErrors = [];
9312
+ if (values.componentProperties !== void 0) {
9313
+ defErrors.push(...applyComponentPropertiesPatch(target.node.properties, values.componentProperties, `patch.operations[${operationIndex}].set`));
9314
+ }
9315
+ if (values.variant !== void 0) {
9316
+ defErrors.push(...applyVariantPatchOnComponent(target.node, target.parent, values.variant, `patch.operations[${operationIndex}].set`));
9317
+ }
9318
+ if (values.variantAxes !== void 0) {
9319
+ if (target.node.type !== "COMPONENT_SET") {
9320
+ defErrors.push(`patch.operations[${operationIndex}].set.variantAxes: only applies to COMPONENT_SET`);
9321
+ } else if (values.variantAxes === null) {
9322
+ defErrors.push(`patch.operations[${operationIndex}].set.variantAxes: clearing all axes is not supported (Figma cannot delete VARIANT properties)`);
9323
+ } else {
9324
+ const current = target.node.properties.variantAxes || {};
9325
+ const detected = detectVariantRenames(current, values.variantAxes);
9326
+ const axisErrors = [
9327
+ ...detected.errors,
9328
+ ...!detected.errors.length ? variantAxesChildConflicts(target.node, current, detected.axes, detected.optionRenames, detected.axisRenames) : []
9329
+ ].map((message) => `patch.operations[${operationIndex}].set.${message}`);
9330
+ defErrors.push(...axisErrors);
9331
+ if (!axisErrors.length) {
9332
+ target.node.properties.variantAxes = detected.axes;
9333
+ applyVariantRenamesInForest(roots, target.node.id, detected.axisRenames, detected.optionRenames, detected.axes);
9334
+ rewriteVariantChildNames(target.node);
9335
+ }
9336
+ }
9337
+ }
9338
+ if (defErrors.length) {
9339
+ throw new PatchError(defErrors.map((message) => {
9340
+ const split = message.indexOf(": ");
9341
+ return patchIssue("PATCH_SET_INVALID", split > 0 ? message.slice(0, split) : `patch.operations[${operationIndex}].set`, split > 0 ? message.slice(split + 2) : message, "See component/variant patch rules in DESIGN-LANGUAGE.md.");
9342
+ }));
9343
+ }
9344
+ affectedIds.push(target.node.id);
9345
+ }
9346
+ function applyDocumentPatch(document, patch) {
9347
+ const result = { ...document, nodes: document.nodes.map(cloneNode), styles: cloneValue2(document.styles), variables: cloneValue2(document.variables) };
9348
+ const affectedIds = [];
9349
+ const warnings = [];
9350
+ const duplicateMaps = [];
9351
+ const index = /* @__PURE__ */ new Map();
9352
+ const visit = (nodes, parent) => nodes.forEach((node) => {
9353
+ index.set(node.id, { node, parent, siblings: nodes });
9354
+ visit(node.children, node);
9355
+ });
9356
+ visit(result.nodes, null);
9357
+ for (const [operationIndex, operation] of patch.patch.operations.entries()) {
9358
+ try {
9359
+ applyOperation(operation, operationIndex, index, visit, affectedIds, warnings, result.nodes, duplicateMaps);
9360
+ } catch (error) {
9361
+ if (error instanceof PatchError) throw error;
9362
+ const message = error instanceof Error ? error.message : String(error);
9363
+ const split = message.indexOf(": ");
9364
+ throw new PatchError([patchIssue("PATCH_OPERATION", split > 0 ? message.slice(0, split) : `patch.operations[${operationIndex}]`, split > 0 ? message.slice(split + 2) : message, "Target nodes that exist and containers that can accept children.")]);
9365
+ }
9366
+ }
9367
+ const introduced = newDocumentIssues(document, result, duplicateMaps);
9368
+ const strictKeyed = (doc) => {
9369
+ const owners = documentIssueOwners(doc);
9370
+ return prototypePatchStrictErrors(doc).map((message) => {
9371
+ const split = message.indexOf(": ");
9372
+ const path = split > 0 ? message.slice(0, split) : "$";
9373
+ const body = split > 0 ? message.slice(split + 2) : message;
9374
+ const owner = owners(path);
9375
+ return { path, body, key: `STRICT|${owner.owner}|${owner.rest}|${body}` };
9376
+ });
9377
+ };
9378
+ const strictExisting = /* @__PURE__ */ new Map();
9379
+ const beforeStrict = strictKeyed(document);
9380
+ for (const { key } of beforeStrict) strictExisting.set(key, (strictExisting.get(key) || 0) + 1);
9381
+ for (const idMap of duplicateMaps) {
9382
+ for (const { key } of beforeStrict) {
9383
+ const remapped = remapIssueKeyThroughDuplicate(key, idMap);
9384
+ if (remapped) strictExisting.set(remapped, (strictExisting.get(remapped) || 0) + 1);
9385
+ }
9386
+ }
9387
+ for (const { path, body, key } of strictKeyed(result)) {
9388
+ const count = strictExisting.get(key) || 0;
9389
+ if (count > 0) {
9390
+ strictExisting.set(key, count - 1);
9391
+ continue;
9392
+ }
9393
+ introduced.push(patchIssue("PATCH_RESULT_INVALID", path, `after patch: ${body}`, "NAVIGATE/SWAP/OVERLAY destinations must be top-level frames; AFTER_TIMEOUT only on top-level nodes."));
9394
+ }
9395
+ if (introduced.length) throw new PatchError(introduced);
9396
+ return { document: result, affectedIds, warnings };
9397
+ }
9398
+ function newDocumentIssues(before, after, duplicateMaps = []) {
9399
+ const keyed = (document) => {
9400
+ const owners = documentIssueOwners(document);
9401
+ return validationIssues(validateDocument(document, Infinity)).map((issue2) => {
9402
+ const owner = owners(issue2.path);
9403
+ return { issue: issue2, owner, key: `${issue2.code}|${owner.owner}|${owner.rest}|${issue2.message}` };
9404
+ });
9405
+ };
9406
+ const existing = /* @__PURE__ */ new Map();
9407
+ const beforeKeyed = keyed(before);
9408
+ for (const { key } of beforeKeyed) existing.set(key, (existing.get(key) || 0) + 1);
9409
+ for (const idMap of duplicateMaps) {
9410
+ for (const { key } of beforeKeyed) {
9411
+ const remapped = remapIssueKeyThroughDuplicate(key, idMap);
9412
+ if (remapped) existing.set(remapped, (existing.get(remapped) || 0) + 1);
9413
+ }
9414
+ }
9415
+ const introduced = [];
9416
+ for (const { issue: issue2, owner, key } of keyed(after)) {
9417
+ const count = existing.get(key) || 0;
9418
+ if (count > 0) {
9419
+ existing.set(key, count - 1);
9420
+ continue;
9421
+ }
9422
+ const where = owner.nodeId !== void 0 ? ` (node '${owner.nodeId}')` : "";
9423
+ introduced.push({ ...issue2, code: "PATCH_RESULT_INVALID", message: `after patch${where}: ${issue2.message}`, suggestion: "This issue is introduced by the patch; issues already in the input document are not reported." });
9424
+ }
9425
+ return introduced;
9426
+ }
9427
+
9428
+ // src/parser.ts
9429
+ function parseDocument(source) {
9430
+ if (!source.trim()) throw new Error("Paste a Compact Design JSON document first.");
9431
+ try {
9432
+ return JSON.parse(source);
9433
+ } catch (error) {
9434
+ throw new Error(`Invalid JSON: ${error instanceof Error ? error.message : String(error)}`);
9435
+ }
9436
+ }
9437
+
9438
+ // src/generated-schema.ts
9439
+ var generated_schema_default = {
9440
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
9441
+ "$id": "https://compact-design-json.dev/schema.json",
9442
+ "title": "Compact Design JSON",
9443
+ "description": "The current public input language for the Compact Design JSON to Figma plugin.",
9444
+ "type": "object",
9445
+ "additionalProperties": false,
9446
+ "properties": {
9447
+ "$schema": { "type": "string" },
9448
+ "canvas": { "$ref": "#/$defs/canvas" },
9449
+ "canvases": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/responsiveCanvas" } },
9450
+ "nodes": { "$ref": "#/$defs/nodes" },
9451
+ "variables": { "type": "array", "items": { "$ref": "#/$defs/variableCollection" } },
9452
+ "styles": { "type": "array", "items": { "$ref": "#/$defs/style" } },
9453
+ "patch": { "$ref": "#/$defs/patchRoot" }
9454
+ },
9455
+ "oneOf": [
9456
+ {
9457
+ "required": ["canvas"],
9458
+ "not": { "anyOf": [{ "required": ["canvases"] }, { "required": ["patch"] }] }
9459
+ },
9460
+ {
9461
+ "required": ["canvases"],
9462
+ "not": { "anyOf": [{ "required": ["canvas"] }, { "required": ["patch"] }] }
9463
+ },
9464
+ {
9465
+ "required": ["patch"],
9466
+ "not": { "anyOf": [{ "required": ["canvas"] }, { "required": ["canvases"] }] }
9467
+ }
9468
+ ],
9469
+ "$defs": {
9470
+ "positiveNumber": { "type": "number", "exclusiveMinimum": 0 },
9471
+ "nonNegativeNumber": { "type": "number", "minimum": 0 },
9472
+ "nullablePositiveNumber": { "type": ["number", "null"], "exclusiveMinimum": 0 },
9473
+ "hexColor": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$" },
9474
+ "color": {
9475
+ "type": "object",
9476
+ "additionalProperties": false,
9477
+ "required": ["r", "g", "b"],
9478
+ "properties": {
9479
+ "r": { "type": "number", "minimum": 0, "maximum": 255 },
9480
+ "g": { "type": "number", "minimum": 0, "maximum": 255 },
9481
+ "b": { "type": "number", "minimum": 0, "maximum": 255 },
9482
+ "a": { "type": "number", "minimum": 0, "maximum": 1 }
9483
+ }
9484
+ },
9485
+ "vector": {
9486
+ "type": "object",
9487
+ "additionalProperties": false,
9488
+ "required": ["x", "y"],
9489
+ "properties": { "x": { "type": "number" }, "y": { "type": "number" } }
9490
+ },
9491
+ "transform": {
9492
+ "type": "array",
9493
+ "minItems": 2,
9494
+ "maxItems": 2,
9495
+ "items": { "type": "array", "minItems": 3, "maxItems": 3, "items": { "type": "number" } }
9496
+ },
9497
+ "gradientStop": {
9498
+ "type": "object",
9499
+ "additionalProperties": false,
9500
+ "required": ["at", "color"],
9501
+ "properties": {
9502
+ "at": { "type": "number", "minimum": 0, "maximum": 1 },
9503
+ "color": { "oneOf": [{ "$ref": "#/$defs/hexColor" }, { "$ref": "#/$defs/color" }] }
9504
+ }
9505
+ },
9506
+ "imagePaint": {
9507
+ "type": "object",
9508
+ "additionalProperties": false,
9509
+ "required": ["image"],
9510
+ "properties": {
9511
+ "image": {
9512
+ "type": "string",
9513
+ "anyOf": [
9514
+ { "pattern": "^https://" },
9515
+ { "pattern": "^file:[^/].+" },
9516
+ { "pattern": "^data:image/(png|jpeg|gif|webp);base64," }
9517
+ ]
9518
+ },
9519
+ "fit": { "enum": ["COVER", "CONTAIN", "CROP", "TILE", "FILL", "FIT"] },
9520
+ "opacity": { "type": "number", "minimum": 0, "maximum": 1 },
9521
+ "transform": { "$ref": "#/$defs/transform" }
9522
+ }
9523
+ },
9524
+ "gradientPaint": {
9525
+ "type": "object",
9526
+ "additionalProperties": false,
9527
+ "required": ["gradient", "stops"],
9528
+ "properties": {
9529
+ "gradient": { "enum": ["LINEAR", "RADIAL", "ANGULAR", "DIAMOND"] },
9530
+ "angle": { "type": "number" },
9531
+ "transform": { "$ref": "#/$defs/transform" },
9532
+ "stops": { "type": "array", "minItems": 2, "items": { "$ref": "#/$defs/gradientStop" } },
9533
+ "opacity": { "type": "number", "minimum": 0, "maximum": 1 }
9534
+ }
9535
+ },
9536
+ "solidPaint": {
9537
+ "type": "object",
7790
9538
  "additionalProperties": false,
7791
9539
  "required": ["type", "color"],
7792
9540
  "properties": {
@@ -8257,6 +10005,26 @@ var generated_schema_default = {
8257
10005
  "options": { "type": "object" }
8258
10006
  }
8259
10007
  },
10008
+ "componentPropertyReferences": {
10009
+ "type": "object",
10010
+ "description": "Links this child layer to component properties on its nearest ancestor COMPONENT (Figma componentPropertyReferences). Values are the authored property names from that COMPONENT's componentProperties. characters \u2192 TEXT (TEXT nodes only); visible \u2192 BOOLEAN (any node); mainComponent \u2192 INSTANCE_SWAP (INSTANCE nodes only).",
10011
+ "additionalProperties": false,
10012
+ "properties": {
10013
+ "characters": { "type": "string", "minLength": 1 },
10014
+ "visible": { "type": "string", "minLength": 1 },
10015
+ "mainComponent": { "type": "string", "minLength": 1 }
10016
+ }
10017
+ },
10018
+ "componentPropertyReferencesPatch": {
10019
+ "type": "object",
10020
+ "description": "Patch form of componentPropertyReferences: null field clears that link.",
10021
+ "additionalProperties": false,
10022
+ "properties": {
10023
+ "characters": { "type": ["string", "null"], "minLength": 1 },
10024
+ "visible": { "type": ["string", "null"], "minLength": 1 },
10025
+ "mainComponent": { "type": ["string", "null"], "minLength": 1 }
10026
+ }
10027
+ },
8260
10028
  "vectorPath": {
8261
10029
  "type": "object",
8262
10030
  "additionalProperties": false,
@@ -8405,6 +10173,7 @@ var generated_schema_default = {
8405
10173
  "items": { "$ref": "#/$defs/componentProperty" }
8406
10174
  },
8407
10175
  "instanceProperties": { "type": "object" },
10176
+ "componentPropertyReferences": { "$ref": "#/$defs/componentPropertyReferences" },
8408
10177
  "variantAxes": {
8409
10178
  "type": "object",
8410
10179
  "additionalProperties": {
@@ -8601,15 +10370,191 @@ var generated_schema_default = {
8601
10370
  "font": { "$ref": "#/$defs/font" },
8602
10371
  "lineHeight": {
8603
10372
  "oneOf": [
8604
- { "type": "number", "exclusiveMinimum": 0 },
8605
- { "$ref": "#/$defs/lineHeightObject" }
10373
+ { "type": "number", "exclusiveMinimum": 0 },
10374
+ { "$ref": "#/$defs/lineHeightObject" }
10375
+ ]
10376
+ },
10377
+ "letterSpacing": { "$ref": "#/$defs/letterSpacing" },
10378
+ "paragraphSpacing": { "$ref": "#/$defs/nonNegativeNumber" }
10379
+ }
10380
+ },
10381
+ "style": { "oneOf": [{ "$ref": "#/$defs/paintStyle" }, { "$ref": "#/$defs/textStyle" }] },
10382
+ "fontPatch": {
10383
+ "description": "Partial font for patch set. Merges into the target's font: unmentioned family/style/size are kept.",
10384
+ "allOf": [{ "$ref": "#/$defs/font" }],
10385
+ "minProperties": 1
10386
+ },
10387
+ "paddingPatch": {
10388
+ "description": "Partial padding for patch set. Merges per side into the target's padding.",
10389
+ "allOf": [{ "$ref": "#/$defs/padding" }],
10390
+ "minProperties": 1
10391
+ },
10392
+ "layoutPatch": {
10393
+ "type": "object",
10394
+ "description": "Partial Auto Layout for patch set. Deep-merges into the target's layout (padding merges per side). direction is required only when the target has no Auto Layout yet.",
10395
+ "additionalProperties": false,
10396
+ "minProperties": 1,
10397
+ "properties": {
10398
+ "direction": { "enum": ["HORIZONTAL", "VERTICAL", "GRID"] },
10399
+ "itemSpacing": { "type": "number" },
10400
+ "counterAxisSpacing": { "type": "number" },
10401
+ "padding": { "$ref": "#/$defs/paddingPatch" },
10402
+ "primaryAxisAlignItems": { "enum": ["MIN", "MAX", "CENTER", "SPACE_BETWEEN"] },
10403
+ "counterAxisAlignItems": { "enum": ["MIN", "MAX", "CENTER", "BASELINE"], "description": "STRETCH is not patchable yet; set layoutAlign: STRETCH on children." },
10404
+ "primaryAxisSizingMode": { "enum": ["FIXED", "HUG", "AUTO"] },
10405
+ "counterAxisSizingMode": { "enum": ["FIXED", "HUG", "AUTO"] },
10406
+ "wrap": { "type": "boolean" }
10407
+ }
10408
+ },
10409
+ "constraintsPatch": {
10410
+ "type": "object",
10411
+ "description": "Partial constraints for patch set. Merges into the target's constraints (missing axes default to MIN, Figma's default).",
10412
+ "additionalProperties": false,
10413
+ "minProperties": 1,
10414
+ "properties": {
10415
+ "horizontal": { "enum": ["MIN", "CENTER", "MAX", "STRETCH", "SCALE"] },
10416
+ "vertical": { "enum": ["MIN", "CENTER", "MAX", "STRETCH", "SCALE"] }
10417
+ }
10418
+ },
10419
+ "patchSet": {
10420
+ "type": "object",
10421
+ "description": "Properties for a patch set operation. Derived from node properties minus id, type, children and coordinateMode. Unknown keys are rejected. x/y are parent-relative. Objects (layout, layout.padding, font, constraints) deep-merge; arrays (fills, strokes, effects, runs, layoutGrids, vectorPaths, dashPattern, cornerRadii) replace. font is partial (its fields merge).",
10422
+ "additionalProperties": false,
10423
+ "minProperties": 1,
10424
+ "properties": {
10425
+ "name": { "type": "string" },
10426
+ "x": { "type": "number" },
10427
+ "y": { "type": "number" },
10428
+ "w": { "$ref": "#/$defs/positiveNumber" },
10429
+ "h": { "$ref": "#/$defs/positiveNumber" },
10430
+ "rotation": { "type": "number" },
10431
+ "fill": { "$ref": "#/$defs/paint" },
10432
+ "fills": { "type": "array", "items": { "$ref": "#/$defs/paint" } },
10433
+ "stroke": { "$ref": "#/$defs/paint" },
10434
+ "strokes": { "type": "array", "items": { "$ref": "#/$defs/paint" } },
10435
+ "strokeWeight": { "$ref": "#/$defs/nonNegativeNumber" },
10436
+ "strokeTopWeight": { "$ref": "#/$defs/nonNegativeNumber" },
10437
+ "strokeRightWeight": { "$ref": "#/$defs/nonNegativeNumber" },
10438
+ "strokeBottomWeight": { "$ref": "#/$defs/nonNegativeNumber" },
10439
+ "strokeLeftWeight": { "$ref": "#/$defs/nonNegativeNumber" },
10440
+ "strokeAlign": { "enum": ["CENTER", "INSIDE", "OUTSIDE"] },
10441
+ "strokeCap": { "enum": ["NONE", "ROUND", "SQUARE", "LINE_ARROW", "TRIANGLE_ARROW", "DIAMOND_FILLED", "CIRCLE_FILLED", "TRIANGLE_FILLED", "WASHI_TAPE_1", "WASHI_TAPE_2", "WASHI_TAPE_3", "WASHI_TAPE_4", "WASHI_TAPE_5", "WASHI_TAPE_6"] },
10442
+ "strokeJoin": { "enum": ["MITER", "BEVEL", "ROUND"] },
10443
+ "dashPattern": { "type": "array", "items": { "$ref": "#/$defs/nonNegativeNumber" } },
10444
+ "cornerRadius": { "$ref": "#/$defs/nonNegativeNumber" },
10445
+ "cornerRadii": { "type": "array", "minItems": 4, "maxItems": 4, "items": { "$ref": "#/$defs/nonNegativeNumber" } },
10446
+ "opacity": { "type": "number", "minimum": 0, "maximum": 1 },
10447
+ "blendMode": { "type": "string" },
10448
+ "visible": { "type": "boolean" },
10449
+ "locked": { "type": "boolean" },
10450
+ "isMask": { "type": "boolean" },
10451
+ "clipsContent": { "type": "boolean" },
10452
+ "effects": { "type": "array", "items": { "$ref": "#/$defs/effect" } },
10453
+ "elevation": { "enum": ["NONE", "LOW", "MEDIUM", "HIGH", "FLOATING"] },
10454
+ "shadow": { "oneOf": [{ "$ref": "#/$defs/shadow" }, { "type": "array", "items": { "$ref": "#/$defs/shadow" } }] },
10455
+ "layout": { "$ref": "#/$defs/layoutPatch" },
10456
+ "constraints": { "$ref": "#/$defs/constraintsPatch" },
10457
+ "layoutSizingHorizontal": { "enum": ["FIXED", "HUG", "FILL"] },
10458
+ "layoutSizingVertical": { "enum": ["FIXED", "HUG", "FILL"] },
10459
+ "layoutAlign": { "enum": ["INHERIT", "STRETCH"] },
10460
+ "layoutGrow": { "type": "number", "minimum": 0 },
10461
+ "layoutPositioning": { "enum": ["AUTO", "ABSOLUTE"] },
10462
+ "minWidth": { "$ref": "#/$defs/nullablePositiveNumber" },
10463
+ "maxWidth": { "$ref": "#/$defs/nullablePositiveNumber" },
10464
+ "minHeight": { "$ref": "#/$defs/nullablePositiveNumber" },
10465
+ "maxHeight": { "$ref": "#/$defs/nullablePositiveNumber" },
10466
+ "layoutGrids": { "type": "array", "items": { "$ref": "#/$defs/grid" } },
10467
+ "text": { "type": "string" },
10468
+ "font": { "$ref": "#/$defs/fontPatch" },
10469
+ "lineHeight": { "oneOf": [{ "type": "number", "exclusiveMinimum": 0 }, { "$ref": "#/$defs/lineHeightObject" }] },
10470
+ "letterSpacing": { "$ref": "#/$defs/letterSpacing" },
10471
+ "align": { "enum": ["LEFT", "CENTER", "RIGHT", "JUSTIFIED"] },
10472
+ "verticalAlignment": { "enum": ["TOP", "CENTER", "BOTTOM"] },
10473
+ "textDecoration": { "enum": ["NONE", "UNDERLINE", "STRIKETHROUGH"] },
10474
+ "textCase": { "enum": ["ORIGINAL", "UPPER", "LOWER", "TITLE", "SMALL_CAPS", "SMALL_CAPS_FORCED"] },
10475
+ "paragraphSpacing": { "$ref": "#/$defs/nonNegativeNumber" },
10476
+ "paragraphIndent": { "$ref": "#/$defs/nonNegativeNumber" },
10477
+ "listSpacing": { "$ref": "#/$defs/nonNegativeNumber" },
10478
+ "hangingPunctuation": { "type": "boolean" },
10479
+ "hangingList": { "type": "boolean" },
10480
+ "textAutoResize": { "enum": ["NONE", "WIDTH_AND_HEIGHT", "HEIGHT", "TRUNCATE"] },
10481
+ "textTruncation": { "enum": ["DISABLED", "ENDING"] },
10482
+ "maxLines": { "type": ["integer", "null"], "minimum": 1 },
10483
+ "runs": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/textRun" } },
10484
+ "pointCount": { "type": "integer", "minimum": 3 },
10485
+ "innerRadius": { "type": "number", "minimum": 0, "maximum": 1 },
10486
+ "startingAngle": { "type": "number" },
10487
+ "endingAngle": { "type": "number" },
10488
+ "innerRadiusRatio": { "type": "number", "minimum": 0, "maximum": 1 },
10489
+ "svg": { "type": "string", "minLength": 1, "description": "SVG markup cannot be patched in place: core and the Figma plugin reject it. Remove and insert a new SVG node." },
10490
+ "vectorPaths": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/vectorPath" } },
10491
+ "componentId": { "type": "string", "minLength": 1, "description": "Recognised but not patchable yet: core and the Figma plugin reject it with PATCH_SET_UNSUPPORTED. Re-import, or remove and insert the node." },
10492
+ "componentProperties": {
10493
+ "description": "Name-keyed upsert of COMPONENT property definitions (not a full wipe). Field null deletes a TEXT/BOOLEAN/INSTANCE_SWAP property. type is immutable after create. INSTANCE_SWAP defaultValue must be a COMPONENT id.",
10494
+ "oneOf": [
10495
+ { "type": "null" },
10496
+ {
10497
+ "type": "object",
10498
+ "additionalProperties": {
10499
+ "oneOf": [
10500
+ { "type": "null" },
10501
+ {
10502
+ "type": "object",
10503
+ "additionalProperties": false,
10504
+ "properties": {
10505
+ "type": { "enum": ["TEXT", "BOOLEAN", "INSTANCE_SWAP"] },
10506
+ "defaultValue": {},
10507
+ "options": { "type": "object" }
10508
+ }
10509
+ }
10510
+ ]
10511
+ }
10512
+ }
10513
+ ]
10514
+ },
10515
+ "instanceProperties": { "description": "Shallow-merge instance overrides (authored names). Field null clears that override. Whole-key null clears all. INSTANCE_SWAP values must be COMPONENT ids.", "oneOf": [{ "type": "null" }, { "type": "object" }] },
10516
+ "componentPropertyReferences": { "description": "Shallow-merge onto the node. Field null clears that link. Whole-key null clears all refs. Values are authored property names on the owning COMPONENT.", "oneOf": [{ "type": "null" }, { "$ref": "#/$defs/componentPropertyReferencesPatch" }] },
10517
+ "variantAxes": {
10518
+ "description": "Shallow-merge on COMPONENT_SET. A string array replaces that axis's options and NEVER renames. Object form { rename?, options?, renameOptions? } renames explicitly. Axis null is an error (Figma cannot delete VARIANT properties). Every declared option must be carried by a COMPONENT child after the patch (Figma derives options from child names).",
10519
+ "type": "object",
10520
+ "additionalProperties": {
10521
+ "oneOf": [
10522
+ { "type": "null" },
10523
+ { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "uniqueItems": true },
10524
+ {
10525
+ "type": "object",
10526
+ "additionalProperties": false,
10527
+ "properties": {
10528
+ "rename": { "type": "string", "minLength": 1, "description": "Rename this axis to the given name." },
10529
+ "options": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 }, "uniqueItems": true, "description": "Final option list after renameOptions are applied." },
10530
+ "renameOptions": { "type": "object", "additionalProperties": { "type": "string", "minLength": 1 }, "description": "Explicit old\u2192new option renames on this axis (after axis rename, if any)." }
10531
+ }
10532
+ }
10533
+ ]
10534
+ }
10535
+ },
10536
+ "variant": {
10537
+ "description": "Merge variant selection on a COMPONENT inside a COMPONENT_SET. Every axis/value must be declared on the parent variantAxes. Rewrites the component name. Whole-key null and per-axis null are rejected (variant components require a selection).",
10538
+ "oneOf": [
10539
+ { "type": "null" },
10540
+ { "type": "object", "additionalProperties": { "type": ["string", "null"], "minLength": 1 } }
8606
10541
  ]
8607
10542
  },
8608
- "letterSpacing": { "$ref": "#/$defs/letterSpacing" },
8609
- "paragraphSpacing": { "$ref": "#/$defs/nonNegativeNumber" }
10543
+ "operation": { "enum": ["UNION", "SUBTRACT", "INTERSECT", "EXCLUDE"] },
10544
+ "prototype": { "type": "array", "items": { "$ref": "#/$defs/prototype" }, "description": "Replace the full reaction list (like runs/effects). [] clears. Destinations must exist (including ids created earlier in the same patch). NAVIGATE/SWAP/OVERLAY destinations must be top-level frames; AFTER_TIMEOUT only on a top-level target. No whole-key null." },
10545
+ "overflowDirection": { "enum": ["NONE", "HORIZONTAL", "VERTICAL", "BOTH", "HORIZONTAL_SCROLLING", "VERTICAL_SCROLLING", "HORIZONTAL_AND_VERTICAL_SCROLLING"] },
10546
+ "numberOfFixedChildren": { "type": "integer", "minimum": 0 },
10547
+ "styleRefs": { "description": "Shallow-merge style refs. Field null clears that ref. Whole-key null clears all styleRefs.", "oneOf": [{ "type": "null" }, { "type": "object", "additionalProperties": { "type": ["string", "null"] } }] },
10548
+ "bindings": { "description": "Shallow-merge variable bindings. Field null clears that binding. Whole-key null clears all bindings.", "oneOf": [{ "type": "null" }, { "type": "object", "additionalProperties": { "type": ["string", "null"] } }] },
10549
+ "variableModes": { "description": "Shallow-merge collection\u2192mode. Field null clears that collection mode. Whole-key null clears all.", "oneOf": [{ "type": "null" }, { "type": "object", "additionalProperties": { "type": ["string", "null"] } }] }
10550
+ },
10551
+ "not": {
10552
+ "anyOf": [
10553
+ { "required": ["fill", "fills"] },
10554
+ { "required": ["stroke", "strokes"] }
10555
+ ]
8610
10556
  }
8611
10557
  },
8612
- "style": { "oneOf": [{ "$ref": "#/$defs/paintStyle" }, { "$ref": "#/$defs/textStyle" }] },
8613
10558
  "patchOperation": {
8614
10559
  "oneOf": [
8615
10560
  {
@@ -8619,7 +10564,7 @@ var generated_schema_default = {
8619
10564
  "properties": {
8620
10565
  "op": { "const": "set" },
8621
10566
  "id": { "type": "string" },
8622
- "set": { "type": "object", "minProperties": 1 }
10567
+ "set": { "$ref": "#/$defs/patchSet" }
8623
10568
  }
8624
10569
  },
8625
10570
  {
@@ -8637,6 +10582,41 @@ var generated_schema_default = {
8637
10582
  "parent": { "type": "string" },
8638
10583
  "node": { "$ref": "#/$defs/node" }
8639
10584
  }
10585
+ },
10586
+ {
10587
+ "type": "object",
10588
+ "additionalProperties": false,
10589
+ "required": ["op", "parent", "index", "node"],
10590
+ "properties": {
10591
+ "op": { "const": "insert" },
10592
+ "parent": { "type": "string" },
10593
+ "index": { "type": "integer", "minimum": 0 },
10594
+ "node": { "$ref": "#/$defs/node" }
10595
+ }
10596
+ },
10597
+ {
10598
+ "type": "object",
10599
+ "additionalProperties": false,
10600
+ "required": ["op", "id", "parent", "index"],
10601
+ "properties": {
10602
+ "op": { "const": "move" },
10603
+ "id": { "type": "string" },
10604
+ "parent": { "type": "string" },
10605
+ "index": { "type": "integer", "minimum": 0 }
10606
+ }
10607
+ },
10608
+ {
10609
+ "type": "object",
10610
+ "additionalProperties": false,
10611
+ "required": ["op", "id", "idSuffix"],
10612
+ "properties": {
10613
+ "op": { "const": "duplicate" },
10614
+ "id": { "type": "string", "minLength": 1, "description": "Source node to duplicate." },
10615
+ "idSuffix": { "type": "string", "minLength": 1, "description": "Appended to every subtree id unless overridden in ids." },
10616
+ "parent": { "type": "string", "minLength": 1, "description": "Optional new parent; omit to keep the source parent (or page-level for a top-level screen)." },
10617
+ "index": { "type": "integer", "minimum": 0, "description": "Optional insert index; default is right after the source when same parent, else end." },
10618
+ "ids": { "type": "object", "additionalProperties": { "type": "string", "minLength": 1 }, "description": "Optional sourceId\u2192newId overrides. Keys must be ids within the source subtree." }
10619
+ }
8640
10620
  }
8641
10621
  ]
8642
10622
  },
@@ -8680,220 +10660,6 @@ function validateSchema(value) {
8680
10660
  return validate2(value) ? [] : (validate2.errors || []).map(issue);
8681
10661
  }
8682
10662
 
8683
- // src/constants.ts
8684
- var SUPPORTED_NODE_TYPES = /* @__PURE__ */ new Set([
8685
- "FRAME",
8686
- "GROUP",
8687
- "RECTANGLE",
8688
- "ELLIPSE",
8689
- "LINE",
8690
- "TEXT",
8691
- "VECTOR",
8692
- "SVG",
8693
- "COMPONENT",
8694
- "COMPONENT_SET",
8695
- "INSTANCE",
8696
- "BOOLEAN_OPERATION",
8697
- "POLYGON",
8698
- "STAR",
8699
- "SECTION",
8700
- "SLICE",
8701
- "ARC"
8702
- ]);
8703
- var SUPPORTED_PAINT_TYPES = /* @__PURE__ */ new Set([
8704
- "SOLID",
8705
- "GRADIENT_LINEAR",
8706
- "GRADIENT_RADIAL",
8707
- "GRADIENT_ANGULAR",
8708
- "GRADIENT_DIAMOND",
8709
- "IMAGE"
8710
- ]);
8711
-
8712
- // src/validate.ts
8713
- function validateDocument(document) {
8714
- const errors = [];
8715
- const ids = /* @__PURE__ */ new Set();
8716
- const components = /* @__PURE__ */ new Set();
8717
- const add = (path, message) => {
8718
- if (errors.length < 30) errors.push(`${path}: ${message}`);
8719
- };
8720
- const finite = (value) => typeof value === "number" && Number.isFinite(value);
8721
- const triggers = /* @__PURE__ */ new Set(["ON_CLICK", "ON_HOVER", "ON_PRESS", "ON_DRAG", "AFTER_TIMEOUT", "MOUSE_UP", "MOUSE_DOWN", "MOUSE_ENTER", "MOUSE_LEAVE", "ON_KEY_DOWN", "ON_MEDIA_HIT", "ON_MEDIA_END"]);
8722
- const actions = /* @__PURE__ */ new Set(["NAVIGATE", "SWAP", "OVERLAY", "SCROLL_TO", "CHANGE_TO", "BACK", "CLOSE", "URL", "UPDATE_MEDIA_RUNTIME", "SET_VARIABLE", "SET_VARIABLE_MODE", "CONDITIONAL"]);
8723
- function validateAction(value, path) {
8724
- if (!value || typeof value !== "object") return add(path, "action must be an object");
8725
- const action = value;
8726
- const type = String(action.type || "");
8727
- if (!actions.has(type)) add(`${path}.type`, `unsupported prototype action '${type}'`);
8728
- if (["NAVIGATE", "SWAP", "OVERLAY", "SCROLL_TO", "CHANGE_TO"].includes(type) && typeof action.destination !== "string") add(`${path}.destination`, `${type} requires a destination ID`);
8729
- if (type === "URL" && typeof action.url !== "string") add(`${path}.url`, "URL action requires url");
8730
- if (type === "SET_VARIABLE" && typeof (action.variable || action.variableId) !== "string") add(`${path}.variable`, "SET_VARIABLE requires a variable reference");
8731
- if (type === "SET_VARIABLE_MODE" && (typeof (action.collection || action.variableCollectionId) !== "string" || typeof (action.mode || action.variableModeId) !== "string")) add(path, "SET_VARIABLE_MODE requires collection and mode references");
8732
- if (type === "UPDATE_MEDIA_RUNTIME" && typeof action.mediaAction !== "string") add(`${path}.mediaAction`, "UPDATE_MEDIA_RUNTIME requires mediaAction");
8733
- if (type === "CONDITIONAL") {
8734
- const blocks = action.blocks || action.conditionalBlocks;
8735
- if (!Array.isArray(blocks) || !blocks.length) add(`${path}.blocks`, "CONDITIONAL requires at least one block");
8736
- else blocks.forEach((block, index) => {
8737
- if (block && typeof block === "object" && !Array.isArray(block)) {
8738
- const nested = block.actions;
8739
- if (Array.isArray(nested)) nested.forEach((item, actionIndex) => validateAction(item, `${path}.blocks[${index}].actions[${actionIndex}]`));
8740
- }
8741
- });
8742
- }
8743
- }
8744
- function validatePrototype(values, path) {
8745
- values.forEach((reaction, index) => {
8746
- const reactionPath = `${path}[${index}]`;
8747
- const triggerValue = reaction.trigger;
8748
- const trigger = typeof triggerValue === "string" ? triggerValue : triggerValue && typeof triggerValue === "object" && !Array.isArray(triggerValue) && typeof triggerValue.type === "string" ? triggerValue.type : "";
8749
- if (!triggers.has(trigger)) add(`${reactionPath}.trigger`, `unsupported trigger '${trigger}'`);
8750
- const reactionActions = Array.isArray(reaction.actions) ? reaction.actions : reaction.action ? [{ ...reaction, type: reaction.action }] : [];
8751
- if (!reactionActions.length) add(`${reactionPath}.actions`, "reaction requires at least one action");
8752
- reactionActions.forEach((action, actionIndex) => validateAction(action, `${reactionPath}.actions[${actionIndex}]`));
8753
- });
8754
- }
8755
- function validatePaint(value, path) {
8756
- if (!value || typeof value !== "object") return add(path, "paint must be an object or valid hex color");
8757
- if (!SUPPORTED_PAINT_TYPES.has(value.type)) add(`${path}.type`, `unsupported paint type '${value.type}'`);
8758
- if (value.type === "IMAGE") {
8759
- if (typeof value.src !== "string" || !/^https:\/\//i.test(value.src) && !/^file:[^/].+/i.test(value.src) && !/^data:image\/(png|jpeg|gif|webp);base64,/i.test(value.src)) add(`${path}.src`, "IMAGE requires HTTPS, file:filename, or an embedded image data URL");
8760
- if (!value.scaleMode || !["FILL", "FIT", "CROP", "TILE"].includes(value.scaleMode)) add(`${path}.scaleMode`, "must be COVER/FILL, CONTAIN/FIT, CROP, or TILE");
8761
- if (value.imageTransform !== void 0 && (!Array.isArray(value.imageTransform) || value.imageTransform.length !== 2)) add(`${path}.imageTransform`, "must be a 2\xD73 transform matrix");
8762
- }
8763
- if (value.type?.startsWith("GRADIENT_") && (!Array.isArray(value.gradientStops) || value.gradientStops.length < 2)) add(`${path}.gradientStops`, "gradient requires at least two stops");
8764
- if (value.opacity !== void 0 && (!finite(value.opacity) || value.opacity < 0 || value.opacity > 1)) add(`${path}.opacity`, "must be 0\u20131");
8765
- }
8766
- function walk(node, path) {
8767
- if (ids.has(node.id)) add(`${path}.id`, `duplicate ID '${node.id}'`);
8768
- else ids.add(node.id);
8769
- if (!SUPPORTED_NODE_TYPES.has(node.type)) add(`${path}.type`, `unsupported node type '${node.type}'`);
8770
- if (node.type === "COMPONENT") components.add(node.id);
8771
- const props = node.properties;
8772
- if (!finite(props.position?.x) || !finite(props.position?.y)) add(`${path}.position`, "x and y must be finite numbers");
8773
- if (!finite(props.size?.width) || props.size.width <= 0 || !finite(props.size?.height) || props.size.height <= 0) add(`${path}.size`, "w and h must be positive finite numbers");
8774
- if (props.cornerRadius !== void 0 && (!finite(props.cornerRadius) || props.cornerRadius < 0)) add(`${path}.cornerRadius`, "must be non-negative");
8775
- if (props.cornerRadii !== void 0 && (!Array.isArray(props.cornerRadii) || props.cornerRadii.length !== 4 || props.cornerRadii.some((n) => !finite(n) || n < 0))) add(`${path}.cornerRadii`, "must contain four non-negative numbers");
8776
- if (props.layout) {
8777
- if (!props.layout.direction || !["HORIZONTAL", "VERTICAL", "GRID"].includes(props.layout.direction)) add(`${path}.layout.direction`, "must be HORIZONTAL, VERTICAL, or GRID");
8778
- for (const key of ["primaryAxisSizingMode", "counterAxisSizingMode"]) {
8779
- const value = props.layout[key];
8780
- if (value !== void 0 && !["FIXED", "HUG", "AUTO"].includes(value)) add(`${path}.layout.${key}`, "must be FIXED or HUG");
8781
- }
8782
- if (props.layout.primaryAxisAlignItems !== void 0 && !["MIN", "MAX", "CENTER", "SPACE_BETWEEN"].includes(props.layout.primaryAxisAlignItems)) add(`${path}.layout.primaryAxisAlignItems`, "must be MIN, MAX, CENTER, or SPACE_BETWEEN");
8783
- if (props.layout.counterAxisAlignItems !== void 0 && !["MIN", "MAX", "CENTER", "BASELINE", "STRETCH"].includes(props.layout.counterAxisAlignItems)) add(`${path}.layout.counterAxisAlignItems`, "must be MIN, MAX, CENTER, BASELINE, or STRETCH");
8784
- }
8785
- for (const key of ["fills", "strokes"]) props.styles[key].forEach((item, index) => validatePaint(item, `${path}.${key}[${index}]`));
8786
- if (node.type === "TEXT" && typeof props.text !== "string") add(`${path}.text`, "TEXT requires text or runs");
8787
- if (node.type === "SVG" && typeof props.svg !== "string") add(`${path}.svg`, "SVG requires markup");
8788
- if (node.type === "VECTOR" && !Array.isArray(props.vectorPaths)) add(`${path}.vectorPaths`, "VECTOR requires vectorPaths");
8789
- if (node.type === "INSTANCE" && typeof props.componentId !== "string") add(`${path}.componentId`, "INSTANCE requires componentId");
8790
- if (node.type === "BOOLEAN_OPERATION" && (typeof props.operation !== "string" || !["UNION", "SUBTRACT", "INTERSECT", "EXCLUDE"].includes(props.operation))) add(`${path}.operation`, "BOOLEAN_OPERATION requires operation UNION, SUBTRACT, INTERSECT, or EXCLUDE");
8791
- if (node.type === "COMPONENT_SET" && (!node.children.length || node.children.some((child) => child.type !== "COMPONENT"))) add(`${path}.children`, "COMPONENT_SET requires COMPONENT children");
8792
- if (node.type === "COMPONENT_SET" && props.variantAxes) {
8793
- for (const [axis, options] of Object.entries(props.variantAxes)) if (!axis || !Array.isArray(options) || !options.length || options.some((option) => typeof option !== "string")) add(`${path}.variantAxes.${axis}`, "must be a non-empty string array");
8794
- const variantAxes = props.variantAxes;
8795
- node.children.forEach((child, index) => Object.entries(child.properties.variant || {}).forEach(([axis, value]) => {
8796
- if (!variantAxes[axis]?.includes(value)) add(`${path}.children[${index}].variant.${axis}`, `value '${value}' is not declared in variantAxes`);
8797
- }));
8798
- }
8799
- if (props.prototype !== void 0) Array.isArray(props.prototype) ? validatePrototype(props.prototype, `${path}.prototype`) : add(`${path}.prototype`, "must be an array of reactions");
8800
- if (["GROUP", "BOOLEAN_OPERATION"].includes(node.type) && !node.children.length) add(`${path}.children`, `${node.type} requires children`);
8801
- node.children.forEach((child, index) => walk(child, `${path}.children[${index}]`));
8802
- }
8803
- document.nodes.forEach((node, index) => walk(node, `nodes[${index}]`));
8804
- function references(node, path) {
8805
- if (node.type === "INSTANCE" && (!node.properties.componentId || !components.has(node.properties.componentId))) add(`${path}.componentId`, `component '${node.properties.componentId || ""}' is not defined in this import`);
8806
- node.children.forEach((child, index) => references(child, `${path}.children[${index}]`));
8807
- }
8808
- document.nodes.forEach((node, index) => references(node, `nodes[${index}]`));
8809
- const styleKeys = /* @__PURE__ */ new Map();
8810
- for (const style of document.styles || []) {
8811
- if (typeof style.id === "string" && style.id) styleKeys.set(style.id, style.type);
8812
- if (typeof style.name === "string" && style.name) styleKeys.set(style.name, style.type);
8813
- }
8814
- const variableKeys = /* @__PURE__ */ new Set();
8815
- const collectionNames = /* @__PURE__ */ new Set();
8816
- const modesByCollection = /* @__PURE__ */ new Map();
8817
- for (const [collectionIndex, collection] of (document.variables || []).entries()) {
8818
- const modes = Array.isArray(collection.modes) && collection.modes.length ? collection.modes : [];
8819
- if (collection.modes !== void 0 && (!modes.length || modes.some((mode) => typeof mode !== "string"))) add(`variables[${collectionIndex}].modes`, "must be a non-empty array of mode names");
8820
- if (typeof collection.name === "string" && collection.name) {
8821
- collectionNames.add(collection.name);
8822
- if (collection.modes !== void 0 && modes.length && modes.every((mode) => typeof mode === "string")) {
8823
- modesByCollection.set(collection.name, modes);
8824
- }
8825
- }
8826
- for (const [variableIndex, variable] of (collection.items || []).entries()) {
8827
- const path = `variables[${collectionIndex}].items[${variableIndex}]`;
8828
- if (!variable.name || typeof variable.name !== "string") add(`${path}.name`, "must be a non-empty string");
8829
- if (!["COLOR", "FLOAT", "STRING", "BOOLEAN"].includes(variable.type)) add(`${path}.type`, "must be COLOR, FLOAT, STRING, or BOOLEAN");
8830
- if (typeof variable.id === "string" && variable.id) variableKeys.add(variable.id);
8831
- if (typeof variable.name === "string" && variable.name) variableKeys.add(variable.name);
8832
- const values = variable.values || (variable.value !== void 0 ? { default: variable.value } : {});
8833
- if (!Object.keys(values).length) add(`${path}`, "requires value or values");
8834
- if (variable.values && modes.length) {
8835
- for (const mode of modes) if (!(mode in variable.values)) add(`${path}.values.${mode}`, "missing value for declared mode");
8836
- }
8837
- for (const [mode, value] of Object.entries(values)) {
8838
- if (variable.type === "FLOAT" && !finite(value)) add(`${path}.${mode}`, "FLOAT value must be a finite number");
8839
- if (variable.type === "STRING" && typeof value !== "string") add(`${path}.${mode}`, "STRING value must be a string");
8840
- if (variable.type === "BOOLEAN" && typeof value !== "boolean") add(`${path}.${mode}`, "BOOLEAN value must be true or false");
8841
- const color = value && typeof value === "object" && !Array.isArray(value) ? value : null;
8842
- if (variable.type === "COLOR" && (!color || !finite(color.r) || !finite(color.g) || !finite(color.b))) add(`${path}.${mode}`, "COLOR value requires numeric r, g, and b channels");
8843
- }
8844
- }
8845
- }
8846
- function tokenReferences(node, path) {
8847
- const bindings = node.properties.bindings;
8848
- if (bindings) {
8849
- for (const [field, key] of Object.entries(bindings)) {
8850
- if (typeof key !== "string" || !key) {
8851
- add(`${path}.bindings.${field}`, "must be a non-empty variable id or name");
8852
- continue;
8853
- }
8854
- if (!variableKeys.has(key)) add(`${path}.bindings.${field}`, `variable '${key}' is not defined in this document`);
8855
- }
8856
- }
8857
- const variableModes = node.properties.variableModes;
8858
- if (variableModes) {
8859
- for (const [collectionName, modeName] of Object.entries(variableModes)) {
8860
- if (!collectionNames.has(collectionName)) {
8861
- add(`${path}.variableModes.${collectionName}`, `variable collection '${collectionName}' is not defined in this document`);
8862
- continue;
8863
- }
8864
- if (typeof modeName !== "string" || !modeName) {
8865
- add(`${path}.variableModes.${collectionName}`, "must be a non-empty mode name");
8866
- continue;
8867
- }
8868
- const declared = modesByCollection.get(collectionName);
8869
- if (declared && !declared.includes(modeName)) add(`${path}.variableModes.${collectionName}`, `mode '${modeName}' is not declared on collection '${collectionName}'`);
8870
- }
8871
- }
8872
- const styleRefs = node.properties.styleRefs;
8873
- if (styleRefs) {
8874
- for (const [field, key] of Object.entries(styleRefs)) {
8875
- if (typeof key !== "string" || !key) {
8876
- add(`${path}.styleRefs.${field}`, "must be a non-empty style id or name");
8877
- continue;
8878
- }
8879
- const styleType = styleKeys.get(key);
8880
- if (!styleType) {
8881
- add(`${path}.styleRefs.${field}`, `style '${key}' is not defined in this document`);
8882
- continue;
8883
- }
8884
- if ((field === "fill" || field === "stroke") && styleType !== "PAINT") {
8885
- add(`${path}.styleRefs.${field}`, `style '${key}' has type ${styleType} but ${field} requires PAINT`);
8886
- } else if (field === "text" && styleType !== "TEXT") {
8887
- add(`${path}.styleRefs.${field}`, `style '${key}' has type ${styleType} but text requires TEXT`);
8888
- }
8889
- }
8890
- }
8891
- node.children.forEach((child, index) => tokenReferences(child, `${path}.children[${index}]`));
8892
- }
8893
- document.nodes.forEach((node, index) => tokenReferences(node, `nodes[${index}]`));
8894
- return errors;
8895
- }
8896
-
8897
10663
  // src/references.ts
8898
10664
  function indexDocument(document) {
8899
10665
  const nodes = /* @__PURE__ */ new Map();
@@ -8909,6 +10675,11 @@ function indexDocument(document) {
8909
10675
  }
8910
10676
 
8911
10677
  // src/index.ts
10678
+ function parseIssue(error, code = "PARSE_OR_NORMALIZE") {
10679
+ const message = error instanceof Error ? error.message : String(error);
10680
+ const match = /^(patch\.operations\[\d+\](?:\.[^:\s]+)?): (.*)$/s.exec(message);
10681
+ return { severity: "ERROR", code, path: match ? match[1] : "$", message: match ? match[2] : message, suggestion: "Correct the document using the Compact Design schema." };
10682
+ }
8912
10683
  function parse(source) {
8913
10684
  return parseDocument(source);
8914
10685
  }
@@ -8921,13 +10692,46 @@ function normalizePatch(value) {
8921
10692
  function validate(value) {
8922
10693
  try {
8923
10694
  const schemaIssues = validateSchema(value);
8924
- if (schemaIssues.length) return { valid: false, issues: schemaIssues };
8925
- if (isPatchDocument(value)) return { valid: true, issues: [], patch: normalizePatchDocument(value) };
10695
+ if (schemaIssues.length) return { valid: false, issues: isPatchDocument(value) ? [...patchKeyIssues(value), ...schemaIssues] : schemaIssues };
10696
+ if (isPatchDocument(value)) {
10697
+ try {
10698
+ return { valid: true, issues: [], patch: normalizePatchDocument(value) };
10699
+ } catch (error) {
10700
+ return { valid: false, issues: [parseIssue(error, "PATCH_SET_UNSUPPORTED")] };
10701
+ }
10702
+ }
8926
10703
  const document = normalizeDocument(value);
8927
10704
  const issues = validationIssues(validateDocument(document));
8928
10705
  return { valid: issues.length === 0, issues, document };
8929
10706
  } catch (error) {
8930
- return { valid: false, issues: [{ severity: "ERROR", code: "PARSE_OR_NORMALIZE", path: "$", message: error instanceof Error ? error.message : String(error), suggestion: "Correct the document using the Compact Design schema." }] };
10707
+ return { valid: false, issues: [parseIssue(error)] };
10708
+ }
10709
+ }
10710
+ function patchKeyIssues(value) {
10711
+ const operations = value.patch?.operations;
10712
+ if (!Array.isArray(operations)) return [];
10713
+ const issues = [];
10714
+ operations.forEach((operation, index) => {
10715
+ if (!operation || typeof operation !== "object") return;
10716
+ const { op, set } = operation;
10717
+ if (op !== "set" || !set || typeof set !== "object" || Array.isArray(set)) return;
10718
+ for (const issue2 of patchSetShapeIssues(set)) {
10719
+ const split = issue2.indexOf(": ");
10720
+ const key = split > 0 ? issue2.slice(0, split) : "";
10721
+ issues.push({ severity: "ERROR", code: "PATCH_SET_UNSUPPORTED", path: `patch.operations[${index}].set${key ? `.${key}` : ""}`, message: split > 0 ? issue2.slice(split + 2) : issue2, suggestion: "Use only the settable keys listed in PATCH_SET_KEYS / DESIGN-LANGUAGE.md (Patch set semantics)." });
10722
+ }
10723
+ });
10724
+ return issues;
10725
+ }
10726
+ function validatePatch(document, patch) {
10727
+ const checked = validate(patch);
10728
+ if (!checked.valid) return { valid: false, issues: checked.issues };
10729
+ if (!checked.patch) return { valid: false, issues: [{ severity: "ERROR", code: "PATCH_EXPECTED", path: "$", message: "Expected a patch document with a top-level patch.operations array.", suggestion: 'Wrap operations in { "patch": { "operations": [...] } }.' }] };
10730
+ try {
10731
+ const result = applyDocumentPatch(document, checked.patch);
10732
+ return { valid: true, issues: [], patch: checked.patch, document: result.document, affectedIds: result.affectedIds, warnings: result.warnings };
10733
+ } catch (error) {
10734
+ return { valid: false, issues: error instanceof PatchError ? error.issues : [parseIssue(error, "PATCH_OPERATION")], patch: checked.patch };
8931
10735
  }
8932
10736
  }
8933
10737
  function lint(value) {
@@ -8937,13 +10741,44 @@ function applyPatch(document, patch) {
8937
10741
  return applyDocumentPatch(document, patch);
8938
10742
  }
8939
10743
  export {
10744
+ PATCH_SET_APPLIES_TO,
10745
+ PATCH_SET_DEFERRED_KEYS,
10746
+ PATCH_SET_EXCLUDED_NODE_KEYS,
10747
+ PATCH_SET_KEYS,
10748
+ PATCH_SET_SEMANTICS,
10749
+ PATCH_TEXT_STYLE_KEYS,
10750
+ PatchError,
8940
10751
  applyPatch,
10752
+ assertPrototypePatchRules,
10753
+ assertPrototypeSetDestinations,
10754
+ buildDuplicateIdMap,
10755
+ buildDuplicateIdMapFromIds,
10756
+ cloneSubtreeWithIds,
10757
+ collectSubtreeIds,
10758
+ corePatchContext,
10759
+ detectVariantRenames,
10760
+ forEachPrototypeDestination,
8941
10761
  indexDocument,
10762
+ isAfterTimeoutReaction,
8942
10763
  isPatchDocument,
10764
+ isPatchSetKey,
10765
+ isTopLevelNodeId,
8943
10766
  lint,
10767
+ newDocumentIssues,
8944
10768
  normalize,
8945
10769
  normalizePatch,
10770
+ normalizePatchSet,
8946
10771
  parse,
10772
+ patchSetBindingFields,
10773
+ patchSetShapeIssues,
10774
+ patchSetTargetIssues,
10775
+ prototypeDestinationErrors,
10776
+ prototypePatchStrictErrors,
10777
+ remapDelimitedIds,
10778
+ remapIssueKeyThroughDuplicate,
8947
10779
  schema,
8948
- validate
10780
+ subtreeContainsType,
10781
+ validate,
10782
+ validatePatch,
10783
+ variantAxesUncarriedOptions
8949
10784
  };