@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.
@@ -1,4 +1,10 @@
1
- import type { InternalDocument, InternalPatchDocument } from "./types";
1
+ import type { InternalDocument, InternalPatchDocument, JsonObject, PatchSetValues } from "./types";
2
2
  export declare function normalizeDocument(value: unknown): InternalDocument;
3
+ /**
4
+ * Normalize a patch `set` without knowing the target. Only authored keys are
5
+ * produced; nothing is defaulted (no Arial, no TEXT fill). Unknown, deferred
6
+ * and immutable keys throw. Coordinates stay parent-relative.
7
+ */
8
+ export declare function normalizePatchSet(set: JsonObject, path?: string): PatchSetValues;
3
9
  export declare function isPatchDocument(value: unknown): boolean;
4
10
  export declare function normalizePatchDocument(value: unknown): InternalPatchDocument;
@@ -0,0 +1,41 @@
1
+ import type { ComponentPropertyPatchEntry, DesignProperties, InternalNode } from "./types";
2
+ export type AxisRename = {
3
+ from: string;
4
+ to: string;
5
+ };
6
+ export type OptionRename = {
7
+ axis: string;
8
+ from: string;
9
+ to: string;
10
+ };
11
+ /** Explicit variantAxes patch entry: array sets options (never renames); object renames explicitly. */
12
+ export type VariantAxisPatchEntry = null | string[] | {
13
+ rename?: string;
14
+ options?: string[];
15
+ renameOptions?: Record<string, string>;
16
+ };
17
+ export type VariantAxesPatch = Record<string, VariantAxisPatchEntry>;
18
+ /**
19
+ * Apply a variantAxes shallow-merge with **explicit** renames only.
20
+ * - `axis: string[]` — replace that axis's options. Never inferred as a rename.
21
+ * - `axis: { rename?, options?, renameOptions? }` — explicit axis/option renames.
22
+ * - `axis: null` — error (Figma cannot delete VARIANT properties).
23
+ * Unmentioned axes are kept.
24
+ */
25
+ export declare function detectVariantRenames(current: Record<string, string[]>, patch: VariantAxesPatch): {
26
+ axes: Record<string, string[]>;
27
+ axisRenames: AxisRename[];
28
+ optionRenames: OptionRename[];
29
+ errors: string[];
30
+ };
31
+ /** After renames, ensure removed options are not still sitting on children without renameOptions. */
32
+ export declare function variantAxesChildConflicts(set: InternalNode, previousAxes: Record<string, string[]>, nextAxes: Record<string, string[]>, optionRenames: OptionRename[], axisRenames: AxisRename[]): string[];
33
+ /** Every declared option must appear on at least one COMPONENT child (Figma derives options from names). */
34
+ export declare function variantAxesUncarriedOptions(set: InternalNode, axes: Record<string, string[]>): string[];
35
+ export declare function applyVariantRenamesInForest(roots: InternalNode[], setId: string, axisRenames: AxisRename[], optionRenames: OptionRename[], axes: Record<string, string[]>, index?: Map<string, {
36
+ node: InternalNode;
37
+ parent: InternalNode | null;
38
+ }>): void;
39
+ export declare function applyComponentPropertiesPatch(props: DesignProperties, patch: Record<string, ComponentPropertyPatchEntry | null> | null, path: string): string[];
40
+ export declare function applyVariantPatchOnComponent(node: InternalNode, parent: InternalNode | null, patch: Record<string, string | null> | null, path: string): string[];
41
+ export declare function rewriteVariantChildNames(set: InternalNode): void;
@@ -0,0 +1,34 @@
1
+ import type { InternalNode, JsonObject } from "./types";
2
+ /** Collect every id in a subtree (root first, depth-first). */
3
+ export declare function collectSubtreeIds(node: InternalNode): string[];
4
+ export declare function subtreeContainsType(node: InternalNode, types: Set<string>): boolean;
5
+ /**
6
+ * Build the deterministic id map for a duplicate.
7
+ * Default: `<sourceId><idSuffix>` for every id in the subtree.
8
+ * `ids` overrides specific source→new mappings; every key must be in the subtree.
9
+ */
10
+ export declare function buildDuplicateIdMap(source: InternalNode, idSuffix: string, ids: Record<string, string> | undefined, path: string): {
11
+ map: Map<string, string>;
12
+ errors: string[];
13
+ };
14
+ /** Same as buildDuplicateIdMap but when only the id set is known (e.g. live Figma walk). */
15
+ export declare function buildDuplicateIdMapFromIds(subtreeIds: Iterable<string>, idSuffix: string, ids: Record<string, string> | undefined, path: string): {
16
+ map: Map<string, string>;
17
+ errors: string[];
18
+ };
19
+ /** Deep-clone a node tree applying an id map (and re-pointing internal prototype destinations). */
20
+ export declare function cloneSubtreeWithIds(node: InternalNode, idMap: Map<string, string>): InternalNode;
21
+ /** Re-point prototype destinations that land inside the duplicated subtree. */
22
+ export declare function rewritePrototypeDestinations(properties: JsonObject, idMap: Map<string, string>): void;
23
+ /**
24
+ * Replace only delimited whole ids in text (quoted `'id'` / `"id"`, or path
25
+ * segments bounded by non-id characters). Longest source ids first so
26
+ * `card-title` is not corrupted by a shorter `card` mapping.
27
+ */
28
+ export declare function remapDelimitedIds(text: string, idMap: Map<string, string>): string;
29
+ /**
30
+ * Remap issue identity keys through a source→copy id map so duplicated
31
+ * pre-existing issues on the copy are exempt (with multiplicity).
32
+ * Owner is remapped exactly (`node:${id}`); path/message only via delimited ids.
33
+ */
34
+ export declare function remapIssueKeyThroughDuplicate(key: string, idMap: Map<string, string>): string | null;
@@ -0,0 +1,61 @@
1
+ import type { JsonObject } from "./types";
2
+ /**
3
+ * Single source of truth for `set` in patch operations. Every key a node may
4
+ * carry in the schema is either settable here or explicitly excluded. Core,
5
+ * the JSON schema test-suite, and the Figma adapter all read this list.
6
+ */
7
+ export declare const PATCH_SET_EXCLUDED_NODE_KEYS: readonly ["id", "type", "children", "coordinateMode"];
8
+ export declare const PATCH_SET_KEYS: readonly ["name", "x", "y", "w", "h", "rotation", "fill", "fills", "stroke", "strokes", "strokeWeight", "strokeTopWeight", "strokeRightWeight", "strokeBottomWeight", "strokeLeftWeight", "strokeAlign", "strokeCap", "strokeJoin", "dashPattern", "cornerRadius", "cornerRadii", "opacity", "blendMode", "visible", "locked", "isMask", "clipsContent", "effects", "elevation", "shadow", "layout", "constraints", "layoutSizingHorizontal", "layoutSizingVertical", "layoutAlign", "layoutGrow", "layoutPositioning", "minWidth", "maxWidth", "minHeight", "maxHeight", "layoutGrids", "text", "font", "lineHeight", "letterSpacing", "align", "verticalAlignment", "textDecoration", "textCase", "paragraphSpacing", "paragraphIndent", "listSpacing", "hangingPunctuation", "hangingList", "textAutoResize", "textTruncation", "maxLines", "runs", "pointCount", "innerRadius", "startingAngle", "endingAngle", "innerRadiusRatio", "svg", "vectorPaths", "componentId", "componentProperties", "instanceProperties", "componentPropertyReferences", "variantAxes", "variant", "operation", "prototype", "overflowDirection", "numberOfFixedChildren", "styleRefs", "bindings", "variableModes"];
9
+ export type PatchSetKey = typeof PATCH_SET_KEYS[number];
10
+ /**
11
+ * - `scalar`: the value replaces the previous value.
12
+ * - `merge`: objects deep-merge into the current value (unmentioned fields are kept).
13
+ * - `replace`: arrays (paint, effect, run, grid, path lists) replace the whole list.
14
+ * - `deferred`: recognised but not patchable yet; both engines reject it with PATCH_SET_UNSUPPORTED.
15
+ * - object merge keys (`bindings`, `styleRefs`, `variableModes`, `instanceProperties`, `componentPropertyReferences`, `componentProperties`, `variantAxes`, `variant`): shallow-merge entries; a field set to `null` clears that entry (except variantAxes: deleting an axis is an error).
16
+ * - `immutable`: can never be patched in place; remove + insert instead.
17
+ */
18
+ export type PatchSetSemantics = "scalar" | "merge" | "replace" | "deferred" | "immutable";
19
+ export declare const PATCH_SET_SEMANTICS: Readonly<Record<PatchSetKey, PatchSetSemantics>>;
20
+ export declare const PATCH_SET_DEFERRED_KEYS: readonly PatchSetKey[];
21
+ /** Compact node types each key applies to. `null` means every type. */
22
+ export declare const PATCH_SET_APPLIES_TO: Readonly<Record<PatchSetKey, readonly string[] | null>>;
23
+ /** Keys that style a TEXT node and are owned by a linked text style. */
24
+ export declare const PATCH_TEXT_STYLE_KEYS: readonly PatchSetKey[];
25
+ export declare function isPatchSetKey(key: string): key is PatchSetKey;
26
+ /**
27
+ * The state of a patch target as seen by an engine (core document tree or the
28
+ * Figma scene). Both engines build this and run the same rules, so a set that
29
+ * is rejected in one is rejected in the other.
30
+ */
31
+ export interface PatchTargetContext {
32
+ /** Compact node type of the target (Figma maps its native type to the compact one). */
33
+ type: string;
34
+ /** Compact type of the parent, or null for a root canvas. */
35
+ parentType: string | null;
36
+ /** Parent currently has Auto Layout. */
37
+ parentAutoLayout: boolean;
38
+ /** Target currently has Auto Layout (before this set). */
39
+ autoLayout: boolean;
40
+ /** Current layoutPositioning of the target. */
41
+ layoutPositioning?: string;
42
+ /** TEXT with per-range styling (core: a run carrying font/fill/letterSpacing/textDecoration/link; Figma: some text property is "mixed" in Figma). */
43
+ mixedText: boolean;
44
+ /** TEXT whose fonts differ per range (core: a run with font.family/style; Figma: fontName is "mixed" in Figma). */
45
+ mixedFontName: boolean;
46
+ /** TEXT linked to a text style (core: styleRefs.text; Figma: textStyleId). */
47
+ textStyle: boolean;
48
+ /** Variable-bound fields on the target (compact binding field names). */
49
+ boundFields: readonly string[];
50
+ /** Number of direct children. */
51
+ childCount: number;
52
+ }
53
+ /** Variable-binding field names a set would overwrite (fill/stroke excluded: those detach — see docs). */
54
+ export declare function patchSetBindingFields(set: JsonObject): string[];
55
+ /** Shape checks that need no target. Returns `key: message` strings. */
56
+ export declare function patchSetShapeIssues(set: JsonObject): string[];
57
+ /**
58
+ * Target-dependent rules. Both engines call this with their own view of the
59
+ * target before applying a set. Returns `key: message` strings.
60
+ */
61
+ export declare function patchSetTargetIssues(set: JsonObject, context: PatchTargetContext): string[];
@@ -0,0 +1,39 @@
1
+ import type { InternalDocument, InternalNode, JsonObject } from "./types";
2
+ /** Walk every destination-bearing action in a prototype array. */
3
+ export declare function forEachPrototypeDestination(prototype: unknown, visit: (action: JsonObject, type: string, destination: string, actionPath: string) => void, basePath?: string): void;
4
+ export declare function isAfterTimeoutReaction(reaction: JsonObject): boolean;
5
+ /** True when `nodeId` is a document.nodes root (top-level canvas frame). */
6
+ export declare function isTopLevelNodeId(roots: InternalNode[], nodeId: string): boolean;
7
+ export type PrototypeLookup = {
8
+ has: (id: string) => boolean;
9
+ typeOf?: (id: string) => string | undefined;
10
+ /** Canvas root id for a node id (document.nodes root). */
11
+ canvasOf?: (id: string) => string | undefined;
12
+ /** Whether an id is a top-level canvas frame. */
13
+ isTopLevel?: (id: string) => boolean;
14
+ };
15
+ /**
16
+ * Shared destination checks (missing / CHANGE_TO / SCROLL_TO).
17
+ * Used by validateDocument. Does not require NAVIGATE destinations to be top-level
18
+ * (import stays lenient).
19
+ */
20
+ export declare function assertPrototypeSetDestinations(prototype: unknown, lookup: PrototypeLookup, path: string, sourceId?: string): string[];
21
+ /**
22
+ * Strict rules for patch `set.prototype` only: NAVIGATE/SWAP/OVERLAY destinations
23
+ * must be top-level frames; AFTER_TIMEOUT only on a top-level target.
24
+ */
25
+ export declare function assertPrototypePatchRules(prototype: unknown, lookup: PrototypeLookup, path: string, sourceId: string | undefined, sourceIsTopLevel: boolean): string[];
26
+ /** Top-level canvas id for a node (the document.nodes root containing it). */
27
+ export declare function canvasRootId(roots: InternalNode[], nodeId: string): string | undefined;
28
+ /**
29
+ * Document-wide prototype destination errors (missing / CHANGE_TO / SCROLL_TO).
30
+ * Used by validateDocument so end-of-patch remove of a destination fails.
31
+ * Import-lenient: nested NAVIGATE destinations are allowed here.
32
+ */
33
+ export declare function prototypeDestinationErrors(document: InternalDocument): string[];
34
+ /**
35
+ * Strict patch rules across the whole document (top-level NAVIGATE dest +
36
+ * AFTER_TIMEOUT only on top-level hosts). Used at end of patch with multiset
37
+ * exemption against the pre-patch document.
38
+ */
39
+ export declare function prototypePatchStrictErrors(document: InternalDocument): string[];
package/dist/patch.d.ts CHANGED
@@ -1,6 +1,30 @@
1
- import type { InternalDocument, InternalPatchDocument } from "./types";
1
+ import { type RepairIssue } from "./lint";
2
+ import { type PatchTargetContext } from "./patch-keys";
3
+ import type { InternalDocument, InternalNode, InternalPatchDocument } from "./types";
2
4
  export interface PatchResult {
3
5
  document: InternalDocument;
4
6
  affectedIds: string[];
7
+ warnings: string[];
5
8
  }
9
+ /** Thrown by applyPatch. `issues` carries every problem as structured repair output. */
10
+ export declare class PatchError extends Error {
11
+ readonly issues: RepairIssue[];
12
+ constructor(issues: RepairIssue[]);
13
+ }
14
+ /** Core's view of a target, mirrored by the Figma adapter's figmaPatchContext. */
15
+ export declare function corePatchContext(node: InternalNode, parent: InternalNode | null): PatchTargetContext;
16
+ /**
17
+ * Apply a normalized patch to a canonical document. The input is never
18
+ * mutated. Throws PatchError (with structured issues) when an operation is
19
+ * rejected or when the patched document fails validateDocument.
20
+ */
6
21
  export declare function applyDocumentPatch(document: InternalDocument, patch: InternalPatchDocument): PatchResult;
22
+ /**
23
+ * Issues present in `after` but not in `before`. Issues already in the input
24
+ * document never block a patch, even on nodes the patch touches; only issues
25
+ * the patch introduces (directly or through a move/insert/remove) are
26
+ * reported. Identity is code + owner (node id / variable / document) + the
27
+ * property path inside the owner + message, never an array index, and the
28
+ * comparison is a multiset so a second copy of an existing issue counts as new.
29
+ */
30
+ export declare function newDocumentIssues(before: InternalDocument, after: InternalDocument, duplicateMaps?: Array<Map<string, string>>): RepairIssue[];
package/dist/types.d.ts CHANGED
@@ -22,13 +22,20 @@ export interface Constraints {
22
22
  horizontal: "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE";
23
23
  vertical: "MIN" | "CENTER" | "MAX" | "STRETCH" | "SCALE";
24
24
  }
25
- export type ComponentPropertyType = "BOOLEAN" | "TEXT" | "INSTANCE_SWAP" | "VARIANT";
25
+ /** Authorable component property types. VARIANT axes use variantAxes/variant on COMPONENT_SET instead; Figma SLOT is out of scope. */
26
+ export type ComponentPropertyType = "BOOLEAN" | "TEXT" | "INSTANCE_SWAP";
26
27
  export interface ComponentPropertyOptions {
27
28
  preferredValues?: Array<{
28
29
  type: "COMPONENT" | "COMPONENT_SET";
29
30
  key: string;
30
31
  }>;
31
32
  }
33
+ /** Child-layer links to the nearest ancestor COMPONENT's componentProperties (Figma componentPropertyReferences). Values are authored property names. */
34
+ export interface ComponentPropertyReferences {
35
+ characters?: string;
36
+ visible?: string;
37
+ mainComponent?: string;
38
+ }
32
39
  export type OverflowDirection = "NONE" | "HORIZONTAL" | "VERTICAL" | "BOTH";
33
40
  export type BlendMode = "PASS_THROUGH" | "NORMAL" | "DARKEN" | "MULTIPLY" | "LINEAR_BURN" | "COLOR_BURN" | "LIGHTEN" | "SCREEN" | "LINEAR_DODGE" | "COLOR_DODGE" | "OVERLAY" | "SOFT_LIGHT" | "HARD_LIGHT" | "DIFFERENCE" | "EXCLUSION" | "HUE" | "SATURATION" | "COLOR" | "LUMINOSITY";
34
41
  export type StrokeCap = "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";
@@ -156,6 +163,7 @@ export interface DesignProperties {
156
163
  options?: ComponentPropertyOptions;
157
164
  }>;
158
165
  instanceProperties?: Record<string, string | boolean | VariableAlias>;
166
+ componentPropertyReferences?: ComponentPropertyReferences;
159
167
  operation?: "UNION" | "SUBTRACT" | "INTERSECT" | "EXCLUDE";
160
168
  overflowDirection?: OverflowDirection;
161
169
  numberOfFixedChildren?: number;
@@ -262,13 +270,71 @@ export interface InternalDocument {
262
270
  variables: VariableCollectionDefinition[];
263
271
  }
264
272
  export type ImportMode = "CREATE" | "REPLACE" | "UPDATE";
273
+ /**
274
+ * Normalized values of a patch `set`, independent of the target type. Only
275
+ * keys present in the authored set appear here: no defaults are added.
276
+ * Coordinates are parent-relative (the authoring convention).
277
+ */
278
+ export interface ComponentPropertyPatchEntry {
279
+ type?: ComponentPropertyType;
280
+ defaultValue?: string | boolean;
281
+ options?: ComponentPropertyOptions;
282
+ }
283
+ /** variantAxes patch value: string[] replaces options (never renames); object form renames explicitly. */
284
+ export type VariantAxisPatchEntry = null | string[] | {
285
+ rename?: string;
286
+ options?: string[];
287
+ renameOptions?: Record<string, string>;
288
+ };
289
+ export interface PatchSetValues extends Omit<Partial<DesignProperties>, "position" | "size" | "styles" | "font" | "constraints" | "layout" | "bindings" | "styleRefs" | "variableModes" | "instanceProperties" | "componentPropertyReferences" | "componentProperties" | "variantAxes" | "variant"> {
290
+ name?: string;
291
+ position?: {
292
+ x?: number;
293
+ y?: number;
294
+ };
295
+ size?: {
296
+ width?: number;
297
+ height?: number;
298
+ };
299
+ styles?: {
300
+ fills?: DesignPaint[];
301
+ strokes?: DesignPaint[];
302
+ effects?: DesignEffect[];
303
+ };
304
+ font?: Partial<DesignFont>;
305
+ constraints?: Partial<Constraints>;
306
+ layout?: DesignLayout;
307
+ /** Shallow-merge; field `null` clears that entry; whole-key `null` clears the map. */
308
+ bindings?: Record<string, string | null> | null;
309
+ styleRefs?: Record<string, string | null> | null;
310
+ variableModes?: Record<string, string | null> | null;
311
+ instanceProperties?: Record<string, string | boolean | VariableAlias | null> | null;
312
+ componentPropertyReferences?: {
313
+ characters?: string | null;
314
+ visible?: string | null;
315
+ mainComponent?: string | null;
316
+ } | null;
317
+ /** Name-keyed upsert of COMPONENT property definitions; `null` deletes a TEXT/BOOLEAN/INSTANCE_SWAP def. */
318
+ componentProperties?: Record<string, ComponentPropertyPatchEntry | null> | null;
319
+ /** Shallow-merge axes; array replaces options (never renames); object form `{ rename, options, renameOptions }` is explicit. Null axis deletes are rejected. */
320
+ variantAxes?: Record<string, VariantAxisPatchEntry> | null;
321
+ /** Merge variant selection on a COMPONENT inside a set. Whole-key or per-axis null is rejected. */
322
+ variant?: Record<string, string | null> | null;
323
+ }
265
324
  export interface PatchOperation {
266
- op: "SET" | "REMOVE" | "APPEND";
325
+ op: "SET" | "REMOVE" | "APPEND" | "INSERT" | "MOVE" | "DUPLICATE";
267
326
  id?: string;
268
327
  parent?: string;
328
+ index?: number;
329
+ /** Authored set object (keys from PATCH_SET_KEYS). */
269
330
  set?: JsonObject;
270
- normalized?: DesignProperties;
331
+ /** Type-agnostic normalized set values; target-type rules run at apply time. */
332
+ normalized?: PatchSetValues;
271
333
  node?: InternalNode;
334
+ /** Required for DUPLICATE: non-empty suffix appended to every subtree id (unless overridden). */
335
+ idSuffix?: string;
336
+ /** Optional DUPLICATE overrides: sourceId → newId (keys must be in the source subtree). */
337
+ ids?: Record<string, string>;
272
338
  }
273
339
  export interface InternalPatchDocument {
274
340
  patch: {
@@ -1,2 +1,16 @@
1
1
  import type { InternalDocument } from "./types";
2
- export declare function validateDocument(document: InternalDocument): string[];
2
+ /** Validate a canonical document. Reports at most `limit` errors (default 30; pass Infinity for all). */
3
+ export declare function validateDocument(document: InternalDocument, limit?: number): string[];
4
+ /**
5
+ * A stable identity for a validateDocument issue that survives index shifts.
6
+ * Node issues are keyed on the owning node's id plus the property path inside
7
+ * that node (e.g. `node:card .bindings.fill`); variable issues on collection
8
+ * and variable names; anything else on its path. Array indexes in node paths
9
+ * (nodes[i].children[j]) never appear in the key, so an insert, move or remove
10
+ * elsewhere does not make an old issue look new.
11
+ */
12
+ export declare function documentIssueOwners(document: InternalDocument): (path: string) => {
13
+ owner: string;
14
+ nodeId?: string;
15
+ rest: string;
16
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@compact-design/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Reusable parser, normalizer, validator, patch engine, and linter for Compact Design JSON.",
5
5
  "license": "MPL-2.0",
6
6
  "type": "module",