@toghen/figma 0.0.0-stage → 0.1.1

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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +18 -2
  3. package/cli/bin.mjs +19 -0
  4. package/cli/bridge.ts +243 -0
  5. package/cli/check.ts +62 -0
  6. package/cli/figcraft.ts +764 -0
  7. package/cli/figcraftApply.ts +1067 -0
  8. package/cli/figcraftFile.ts +476 -0
  9. package/cli/figcraftLibrary.ts +147 -0
  10. package/cli/figcraftRecord.ts +121 -0
  11. package/cli/figcraftScene.ts +661 -0
  12. package/cli/main.ts +218 -0
  13. package/cli/members.ts +65 -0
  14. package/cli/run.ts +756 -0
  15. package/component-files.md +665 -0
  16. package/dist/code.js +24161 -0
  17. package/dist/manifest.json +23 -0
  18. package/dist/ui.html +6918 -0
  19. package/guide.md +985 -0
  20. package/main/code.ts +50 -0
  21. package/main/components/apply.ts +704 -0
  22. package/main/components/endOfRun.ts +168 -0
  23. package/main/components/errors.ts +17 -0
  24. package/main/components/fieldWriter.ts +497 -0
  25. package/main/components/file.ts +263 -0
  26. package/main/components/receipt.ts +228 -0
  27. package/main/components/resolve.ts +1193 -0
  28. package/main/components/run.ts +273 -0
  29. package/main/components/snapshot.ts +341 -0
  30. package/main/components/stamp.ts +312 -0
  31. package/main/components/update.ts +465 -0
  32. package/main/dispatcher.ts +33 -0
  33. package/main/engines/batchRunner.ts +200 -0
  34. package/main/engines/ioAdapter.ts +132 -0
  35. package/main/engines/operationExecutor.ts +512 -0
  36. package/main/engines/propertyMapper.ts +221 -0
  37. package/main/engines/queryEngine.ts +45 -0
  38. package/main/engines/styleSyncEngine.ts +444 -0
  39. package/main/engines/variableSyncEngine.ts +561 -0
  40. package/main/handlers/auditSelection.ts +60 -0
  41. package/main/handlers/boundLayers.ts +51 -0
  42. package/main/handlers/cancelOperation.ts +18 -0
  43. package/main/handlers/executeApplyBindings.ts +69 -0
  44. package/main/handlers/executeScaffold.ts +59 -0
  45. package/main/handlers/executeStyleSync.ts +121 -0
  46. package/main/handlers/executeVariableSync.ts +82 -0
  47. package/main/handlers/exportDocument.ts +114 -0
  48. package/main/handlers/getSelection.ts +43 -0
  49. package/main/handlers/index.ts +30 -0
  50. package/main/handlers/inspectCorrespondence.ts +172 -0
  51. package/main/handlers/loadConventions.ts +86 -0
  52. package/main/handlers/loadDocument.ts +253 -0
  53. package/main/handlers/lock.ts +270 -0
  54. package/main/handlers/planApply.ts +157 -0
  55. package/main/handlers/planApplyBindings.ts +79 -0
  56. package/main/handlers/planScaffold.ts +84 -0
  57. package/main/handlers/planStyleSync.ts +49 -0
  58. package/main/handlers/planVariableSync.ts +49 -0
  59. package/main/handlers/readSelectionBindings.ts +160 -0
  60. package/main/handlers/resizeUi.ts +105 -0
  61. package/main/handlers/runComponent.ts +65 -0
  62. package/main/handlers/templates.ts +462 -0
  63. package/main/handlers/tokenGraph.ts +122 -0
  64. package/main/runtimeState.ts +66 -0
  65. package/main/utils/abortController.ts +11 -0
  66. package/main/utils/boundVariables.ts +84 -0
  67. package/main/utils/buildPayloads.ts +85 -0
  68. package/main/utils/correspondence.ts +658 -0
  69. package/main/utils/designSource.ts +122 -0
  70. package/main/utils/figmaCollectionIndex.ts +120 -0
  71. package/main/utils/figmaStyleAdapter.ts +268 -0
  72. package/main/utils/figmaTokenAdapter.ts +255 -0
  73. package/main/utils/flattenDesignSystem.ts +38 -0
  74. package/main/utils/folds.ts +203 -0
  75. package/main/utils/fontStack.ts +57 -0
  76. package/main/utils/lists.ts +4 -0
  77. package/main/utils/lockStore.ts +72 -0
  78. package/main/utils/moves.ts +67 -0
  79. package/main/utils/pageRoots.ts +58 -0
  80. package/main/utils/planSymbols.ts +96 -0
  81. package/main/utils/purpose.ts +351 -0
  82. package/main/utils/recentStore.ts +166 -0
  83. package/main/utils/resolverModes.ts +271 -0
  84. package/main/utils/rootRecord.ts +137 -0
  85. package/main/utils/shardedData.ts +142 -0
  86. package/main/utils/standing.ts +106 -0
  87. package/main/utils/styleTarget.ts +51 -0
  88. package/main/utils/sum.ts +612 -0
  89. package/main/utils/symbolicIdRegistry.ts +26 -0
  90. package/main/utils/syncState.ts +133 -0
  91. package/main/utils/templateStore.ts +73 -0
  92. package/main/utils/tokenPath.ts +28 -0
  93. package/main/utils/tokenStamps.ts +91 -0
  94. package/manifest.json +15 -0
  95. package/package.json +64 -4
  96. package/reference.md +1609 -0
  97. package/shared/components/artboard.ts +126 -0
  98. package/shared/components/axes.ts +420 -0
  99. package/shared/components/bindings.ts +64 -0
  100. package/shared/components/blueprint.ts +248 -0
  101. package/shared/components/check.ts +705 -0
  102. package/shared/components/diff.ts +668 -0
  103. package/shared/components/fields.ts +404 -0
  104. package/shared/components/groups.ts +306 -0
  105. package/shared/components/index.ts +140 -0
  106. package/shared/components/members.ts +280 -0
  107. package/shared/components/model.ts +273 -0
  108. package/shared/components/order.ts +53 -0
  109. package/shared/components/read.ts +480 -0
  110. package/shared/components/stack.ts +124 -0
  111. package/shared/components/steps.ts +186 -0
  112. package/shared/components/validate.ts +565 -0
  113. package/shared/conventions.ts +28 -0
  114. package/shared/inputs.ts +40 -0
  115. package/shared/operations.ts +233 -0
  116. package/shared/pendingNames.ts +39 -0
  117. package/shared/protocol.ts +588 -0
  118. package/shared/tokenTypeMap.ts +21 -0
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Which variables a layer is bound to, property by property, the first fill's
3
+ * and stroke's paints included (those are bound on the paint, not the node).
4
+ */
5
+
6
+ import { FIELDS, type FieldSpec } from '../../shared/components/fields.js';
7
+
8
+ /** Field to what `pick` reads from its row of the field table, for the fields it reads something from. */
9
+ function fromTable<T>(pick: (name: string, spec: FieldSpec) => T | undefined): Record<string, T> {
10
+ const out: Record<string, T> = {};
11
+ for (const [name, spec] of Object.entries(FIELDS)) {
12
+ const value = pick(name, spec);
13
+ if (value !== undefined) out[name] = value;
14
+ }
15
+ return out;
16
+ }
17
+
18
+ /** The property names a paint binding is reported under, as conventions name them: `fills[0].color`, read from the field table. */
19
+ export const PAINT_PROPERTIES: Readonly<Record<string, string>> = fromTable((name, spec) =>
20
+ spec.token?.bind?.call === 'paint' ? `${name}[0].${spec.token.bind.field}` : undefined,
21
+ );
22
+
23
+ /**
24
+ * Fields whose binding Figma reports on their parts, read from the field table. Bound to a variable, `cornerRadius` is
25
+ * reported as its four corners bound to it, with no `cornerRadius` key (measured 20 October 2026), and `strokeWeight`
26
+ * as its four sides.
27
+ */
28
+ export const BOUND_BY_PARTS: Readonly<Record<string, readonly string[]>> = fromTable((_name, spec) => spec.token?.bind?.parts);
29
+
30
+ /**
31
+ * The id of the variable a field is bound to, in a node's `boundVariables`. A field Figma binds by its parts is bound
32
+ * when every part is bound to one variable; `null` when only some are, or to different variables, and `undefined`
33
+ * when the field is not bound at all.
34
+ */
35
+ export function boundIdOf(bound: Record<string, unknown>, field: string): string | null | undefined {
36
+ // A text field is reported as an array of aliases, one per bound range: bound when they all name one variable.
37
+ // ponytail: a field bound on one range only reads as bound too (measured §14: one alias); `getRangeBoundVariable`
38
+ // over the whole text would tell, should a person's partial binding need catching.
39
+ const ranges = bound[field];
40
+ if (Array.isArray(ranges) && FIELDS[field]?.token?.bind?.call === 'text') {
41
+ const ids = new Set(ranges.map(idOf));
42
+ return ids.size === 0 ? undefined : ids.size === 1 ? ([...ids][0] ?? null) : null;
43
+ }
44
+ const own = idOf(bound[field]);
45
+ const parts = BOUND_BY_PARTS[field];
46
+ if (own !== undefined || !parts) return own;
47
+ const ids = new Set(parts.map((part) => idOf(bound[part])));
48
+ return ids.size === 1 ? [...ids][0] : null;
49
+ }
50
+
51
+ /** Property → bound variable id. */
52
+ export function boundVariablesOf(node: SceneNode): Map<string, string> {
53
+ const found = new Map<string, string>();
54
+ const bound = (node as { boundVariables?: Record<string, unknown> }).boundVariables ?? {};
55
+ for (const [property, value] of Object.entries(bound)) {
56
+ const id = idOf(Array.isArray(value) ? value[0] : value);
57
+ if (id && !(property in PAINT_PROPERTIES)) found.set(property, id);
58
+ }
59
+ // A field bound by its parts is named once, as the file and the conventions name it.
60
+ for (const [field, parts] of Object.entries(BOUND_BY_PARTS)) {
61
+ const id = boundIdOf(bound, field);
62
+ if (!id) continue;
63
+ for (const part of parts) found.delete(part);
64
+ found.set(field, id);
65
+ }
66
+ for (const [key, property] of Object.entries(PAINT_PROPERTIES)) {
67
+ const paints = (node as unknown as Record<string, unknown>)[key];
68
+ if (!Array.isArray(paints) || paints.length === 0) continue;
69
+ const id = idOf((paints[0] as { boundVariables?: { color?: unknown } }).boundVariables?.color);
70
+ if (id) found.set(property, id);
71
+ }
72
+ return found;
73
+ }
74
+
75
+ export function boundVariableIds(node: SceneNode): string[] {
76
+ return [...boundVariablesOf(node).values()];
77
+ }
78
+
79
+ function idOf(alias: unknown): string | undefined {
80
+ if (typeof alias === 'string') return alias;
81
+ if (typeof alias === 'object' && alias !== null && typeof (alias as { id?: unknown }).id === 'string')
82
+ return (alias as { id: string }).id;
83
+ return undefined;
84
+ }
@@ -0,0 +1,85 @@
1
+ import type { Resolver } from '@toghen/lib';
2
+ import { figmaCollectionNameFor, pathsInAnyMode, readToken, resolveSumModes } from './resolverModes.js';
3
+ import { buildSum } from './sum.js';
4
+ import { defaultTokenTypeMap, type TokenTypeConversionMap } from '../../shared/tokenTypeMap.js';
5
+ import type { DocumentCollectionSummary, DocumentStatePayload, SelectionPayload } from '../../shared/protocol.js';
6
+
7
+ export function buildDocumentStatePayload(
8
+ resolver: Resolver,
9
+ conversionMap: TokenTypeConversionMap = defaultTokenTypeMap,
10
+ fileName: string | null = null,
11
+ ): DocumentStatePayload {
12
+ const modifiers: Record<string, string[]> = {};
13
+ const modifierDefaults: Record<string, string> = {};
14
+ for (const modifier of resolver.getModifiers()) {
15
+ modifiers[modifier.name] = modifier.contexts;
16
+ if (modifier.default !== undefined) modifierDefaults[modifier.name] = modifier.default;
17
+ }
18
+ // The sum, as `LOAD_DOCUMENT` builds it; never the product, which a large
19
+ // document cannot finish. The count is of distinct token paths: a token is
20
+ // one token however many modes give it a value.
21
+ const resolution = resolveSumModes(resolver);
22
+ const { ds, dsCollectionName, defaultMode, modeNames } = resolution;
23
+ const paths = pathsInAnyMode(resolution);
24
+ const tokenCount = paths.size;
25
+ let styleCount = 0;
26
+ for (const [path, [mode]] of paths) {
27
+ const type = readToken(ds, `${dsCollectionName}.${path}`, mode)?.$type;
28
+ if (type !== undefined && conversionMap[type]?.target === 'style') styleCount += 1;
29
+ }
30
+
31
+ const version = resolver.getDeclaredVersion();
32
+ const collections = sumCollections(resolver, conversionMap);
33
+
34
+ return {
35
+ loaded: true,
36
+ name: resolver.name ?? 'Untitled',
37
+ description: resolver.description,
38
+ tokenCount,
39
+ modifiers,
40
+ modes: modeNames,
41
+ modifierDefaults,
42
+ defaultMode,
43
+ ...(version ? { version } : {}),
44
+ ...(fileName ? { fileName } : {}),
45
+ styleCount,
46
+ ...(collections ? { collections } : {}),
47
+ };
48
+ }
49
+
50
+ /**
51
+ * The collections a variable sync would build, with how many variables each
52
+ * would hold, from the same sum the plan builds from.
53
+ *
54
+ * Undefined when the sum cannot be built: the plan says why, and a table drawn
55
+ * from a guess would contradict it.
56
+ */
57
+ function sumCollections(resolver: Resolver, conversionMap: TokenTypeConversionMap): DocumentCollectionSummary[] | undefined {
58
+ try {
59
+ const sum = buildSum(resolver, figmaCollectionNameFor(resolver), conversionMap);
60
+ const counts = new Map<string, number>();
61
+ for (const token of sum.tokens) {
62
+ for (const variable of token.variables) {
63
+ counts.set(variable.collection, (counts.get(variable.collection) ?? 0) + 1);
64
+ }
65
+ }
66
+ return sum.collections.map((collection) => ({
67
+ name: collection.name,
68
+ ...(collection.modifier ? { modifier: collection.modifier } : {}),
69
+ // A folded part axis's collection says so, or the screen draws it like the base collection (smell #44).
70
+ ...(collection.axis ? { axis: true as const } : {}),
71
+ modes: [...collection.modes],
72
+ variableCount: counts.get(collection.name) ?? 0,
73
+ }));
74
+ } catch {
75
+ return undefined;
76
+ }
77
+ }
78
+
79
+ export function buildSelectionPayload(selection: readonly SceneNode[]): SelectionPayload {
80
+ return {
81
+ ids: selection.map((node) => node.id),
82
+ names: selection.map((node) => node.name),
83
+ count: selection.length,
84
+ };
85
+ }