@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.
- package/LICENSE +21 -0
- package/README.md +18 -2
- package/cli/bin.mjs +19 -0
- package/cli/bridge.ts +243 -0
- package/cli/check.ts +62 -0
- package/cli/figcraft.ts +764 -0
- package/cli/figcraftApply.ts +1067 -0
- package/cli/figcraftFile.ts +476 -0
- package/cli/figcraftLibrary.ts +147 -0
- package/cli/figcraftRecord.ts +121 -0
- package/cli/figcraftScene.ts +661 -0
- package/cli/main.ts +218 -0
- package/cli/members.ts +65 -0
- package/cli/run.ts +756 -0
- package/component-files.md +665 -0
- package/dist/code.js +24161 -0
- package/dist/manifest.json +23 -0
- package/dist/ui.html +6918 -0
- package/guide.md +985 -0
- package/main/code.ts +50 -0
- package/main/components/apply.ts +704 -0
- package/main/components/endOfRun.ts +168 -0
- package/main/components/errors.ts +17 -0
- package/main/components/fieldWriter.ts +497 -0
- package/main/components/file.ts +263 -0
- package/main/components/receipt.ts +228 -0
- package/main/components/resolve.ts +1193 -0
- package/main/components/run.ts +273 -0
- package/main/components/snapshot.ts +341 -0
- package/main/components/stamp.ts +312 -0
- package/main/components/update.ts +465 -0
- package/main/dispatcher.ts +33 -0
- package/main/engines/batchRunner.ts +200 -0
- package/main/engines/ioAdapter.ts +132 -0
- package/main/engines/operationExecutor.ts +512 -0
- package/main/engines/propertyMapper.ts +221 -0
- package/main/engines/queryEngine.ts +45 -0
- package/main/engines/styleSyncEngine.ts +444 -0
- package/main/engines/variableSyncEngine.ts +561 -0
- package/main/handlers/auditSelection.ts +60 -0
- package/main/handlers/boundLayers.ts +51 -0
- package/main/handlers/cancelOperation.ts +18 -0
- package/main/handlers/executeApplyBindings.ts +69 -0
- package/main/handlers/executeScaffold.ts +59 -0
- package/main/handlers/executeStyleSync.ts +121 -0
- package/main/handlers/executeVariableSync.ts +82 -0
- package/main/handlers/exportDocument.ts +114 -0
- package/main/handlers/getSelection.ts +43 -0
- package/main/handlers/index.ts +30 -0
- package/main/handlers/inspectCorrespondence.ts +172 -0
- package/main/handlers/loadConventions.ts +86 -0
- package/main/handlers/loadDocument.ts +253 -0
- package/main/handlers/lock.ts +270 -0
- package/main/handlers/planApply.ts +157 -0
- package/main/handlers/planApplyBindings.ts +79 -0
- package/main/handlers/planScaffold.ts +84 -0
- package/main/handlers/planStyleSync.ts +49 -0
- package/main/handlers/planVariableSync.ts +49 -0
- package/main/handlers/readSelectionBindings.ts +160 -0
- package/main/handlers/resizeUi.ts +105 -0
- package/main/handlers/runComponent.ts +65 -0
- package/main/handlers/templates.ts +462 -0
- package/main/handlers/tokenGraph.ts +122 -0
- package/main/runtimeState.ts +66 -0
- package/main/utils/abortController.ts +11 -0
- package/main/utils/boundVariables.ts +84 -0
- package/main/utils/buildPayloads.ts +85 -0
- package/main/utils/correspondence.ts +658 -0
- package/main/utils/designSource.ts +122 -0
- package/main/utils/figmaCollectionIndex.ts +120 -0
- package/main/utils/figmaStyleAdapter.ts +268 -0
- package/main/utils/figmaTokenAdapter.ts +255 -0
- package/main/utils/flattenDesignSystem.ts +38 -0
- package/main/utils/folds.ts +203 -0
- package/main/utils/fontStack.ts +57 -0
- package/main/utils/lists.ts +4 -0
- package/main/utils/lockStore.ts +72 -0
- package/main/utils/moves.ts +67 -0
- package/main/utils/pageRoots.ts +58 -0
- package/main/utils/planSymbols.ts +96 -0
- package/main/utils/purpose.ts +351 -0
- package/main/utils/recentStore.ts +166 -0
- package/main/utils/resolverModes.ts +271 -0
- package/main/utils/rootRecord.ts +137 -0
- package/main/utils/shardedData.ts +142 -0
- package/main/utils/standing.ts +106 -0
- package/main/utils/styleTarget.ts +51 -0
- package/main/utils/sum.ts +612 -0
- package/main/utils/symbolicIdRegistry.ts +26 -0
- package/main/utils/syncState.ts +133 -0
- package/main/utils/templateStore.ts +73 -0
- package/main/utils/tokenPath.ts +28 -0
- package/main/utils/tokenStamps.ts +91 -0
- package/manifest.json +15 -0
- package/package.json +64 -4
- package/reference.md +1609 -0
- package/shared/components/artboard.ts +126 -0
- package/shared/components/axes.ts +420 -0
- package/shared/components/bindings.ts +64 -0
- package/shared/components/blueprint.ts +248 -0
- package/shared/components/check.ts +705 -0
- package/shared/components/diff.ts +668 -0
- package/shared/components/fields.ts +404 -0
- package/shared/components/groups.ts +306 -0
- package/shared/components/index.ts +140 -0
- package/shared/components/members.ts +280 -0
- package/shared/components/model.ts +273 -0
- package/shared/components/order.ts +53 -0
- package/shared/components/read.ts +480 -0
- package/shared/components/stack.ts +124 -0
- package/shared/components/steps.ts +186 -0
- package/shared/components/validate.ts +565 -0
- package/shared/conventions.ts +28 -0
- package/shared/inputs.ts +40 -0
- package/shared/operations.ts +233 -0
- package/shared/pendingNames.ts +39 -0
- package/shared/protocol.ts +588 -0
- package/shared/tokenTypeMap.ts +21 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reads the stamps on the file's variables and styles into something the screen can say.
|
|
3
|
+
*
|
|
4
|
+
* ### Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Each variable and style a sync writes carries a stamp (`tokenStamps.ts`): the
|
|
7
|
+
* token it holds and a receipt of what was written. The dirty check reads them,
|
|
8
|
+
* and without this none of it would reach the UI: a user opening a file they
|
|
9
|
+
* synced last week would have no way to learn that they had.
|
|
10
|
+
*
|
|
11
|
+
* ### What "covered" means here, exactly
|
|
12
|
+
*
|
|
13
|
+
* A token is covered when its variable's stamp holds a receipt for **every mode
|
|
14
|
+
* that token actually has a value in**, not every mode the document declares.
|
|
15
|
+
* A token contributed only by `dark` is complete with one, and measuring it
|
|
16
|
+
* against both modes would report it permanently half-done. This mirrors
|
|
17
|
+
* `VariableSyncEngine`, which builds `staleModes` only from modes where the
|
|
18
|
+
* token resolves; the two have to agree or the status contradicts the plan.
|
|
19
|
+
*
|
|
20
|
+
* Style-backed tokens are the exception: Figma styles have no modes, so a style
|
|
21
|
+
* stamped for the token is complete.
|
|
22
|
+
*
|
|
23
|
+
* ### What it deliberately does not claim
|
|
24
|
+
*
|
|
25
|
+
* Coverage is *presence of a stamp*, not currency. A token whose document value
|
|
26
|
+
* changed since the last sync still counts as covered: the stamp exists, the
|
|
27
|
+
* document has simply moved, and the plan is what says so. Answering "is it up
|
|
28
|
+
* to date?" here would mean keeping a second copy of the dirty check in step
|
|
29
|
+
* with the engine's. One dirty check, in the engine, is the arrangement worth having.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import type { Resolver } from '@toghen/lib';
|
|
33
|
+
import type { SyncStatePayload } from '../../shared/protocol.js';
|
|
34
|
+
import { defaultTokenTypeMap, type TokenTypeConversionMap } from '../../shared/tokenTypeMap.js';
|
|
35
|
+
import { indexFigmaCollections, type FigmaCollectionIndex } from './figmaCollectionIndex.js';
|
|
36
|
+
import { figmaCollectionNameFor } from './resolverModes.js';
|
|
37
|
+
import { buildSum, type SumToken } from './sum.js';
|
|
38
|
+
import { localStyles } from './tokenStamps.js';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Builds the payload describing what this Figma file's stamps say about being synced.
|
|
42
|
+
*
|
|
43
|
+
* @param resolver - The loaded document, or `null`. Without one the file-level
|
|
44
|
+
* facts are still answered and the coverage counts are `null`: there is
|
|
45
|
+
* nothing to measure the stamps against, and reporting `0` would read as
|
|
46
|
+
* "nothing is synced" when the truth is "nothing has been asked".
|
|
47
|
+
*/
|
|
48
|
+
export async function buildSyncStatePayload(
|
|
49
|
+
resolver: Resolver | null,
|
|
50
|
+
conversionMap: TokenTypeConversionMap = defaultTokenTypeMap,
|
|
51
|
+
): Promise<SyncStatePayload> {
|
|
52
|
+
const figmaIndex = await indexFigmaCollections();
|
|
53
|
+
const styled = new Set<string>();
|
|
54
|
+
for (const { stamp } of await localStyles()) if (stamp?.is.token !== undefined) styled.add(stamp.is.token);
|
|
55
|
+
const tokens = [...Object.keys(figmaIndex.variablesByToken), ...styled];
|
|
56
|
+
const recordedCount = Object.values(figmaIndex.variablesByToken).reduce((count, stamped) => count + stamped.length, 0) + styled.size;
|
|
57
|
+
|
|
58
|
+
const base = {
|
|
59
|
+
hasRecord: recordedCount > 0,
|
|
60
|
+
recordedCount,
|
|
61
|
+
recordNames: recordNamesOf(tokens),
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
if (!resolver) {
|
|
65
|
+
return { ...base, documentTokenCount: null, coveredCount: null, partialCount: null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let sumTokens: SumToken[];
|
|
69
|
+
try {
|
|
70
|
+
sumTokens = buildSum(resolver, figmaCollectionNameFor(resolver), conversionMap).tokens;
|
|
71
|
+
} catch {
|
|
72
|
+
// A document the sum cannot be built from has no coverage to count; the plan says why.
|
|
73
|
+
return { ...base, documentTokenCount: null, coveredCount: null, partialCount: null };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let covered = 0;
|
|
77
|
+
let partial = 0;
|
|
78
|
+
for (const token of sumTokens) {
|
|
79
|
+
const coverage = token.routing.kind === 'style' ? (styled.has(token.path) ? 'covered' : 'none') : coverageOfToken(token, figmaIndex);
|
|
80
|
+
if (coverage === 'covered') covered += 1;
|
|
81
|
+
if (coverage === 'partial') partial += 1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
...base,
|
|
86
|
+
documentTokenCount: sumTokens.length,
|
|
87
|
+
coveredCount: covered,
|
|
88
|
+
partialCount: partial,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type Coverage = 'covered' | 'partial' | 'none';
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* A token is the variables it is made of: one, or the links of a chain, each
|
|
96
|
+
* stamped with a key of its own. It is covered when every one of them is.
|
|
97
|
+
*/
|
|
98
|
+
function coverageOfToken(token: SumToken, figmaIndex: FigmaCollectionIndex): Coverage {
|
|
99
|
+
if (token.variables.length === 0) return 'none';
|
|
100
|
+
|
|
101
|
+
const each = token.variables.map((variable): Coverage => {
|
|
102
|
+
const collection = figmaIndex.byName[variable.collection];
|
|
103
|
+
const stamped = figmaIndex.variablesByToken[variable.key]?.find((held) => held.variableCollectionId === collection?.id);
|
|
104
|
+
const modes = [...variable.cells.keys()];
|
|
105
|
+
if (!stamped || !collection || modes.length === 0) return 'none';
|
|
106
|
+
const receipt = stamped.stamp?.receipt ?? {};
|
|
107
|
+
const recorded = modes.filter((mode) => {
|
|
108
|
+
const modeId = collection.modes.find((held) => held.name === mode)?.modeId;
|
|
109
|
+
return modeId !== undefined && receipt[modeId] !== undefined;
|
|
110
|
+
}).length;
|
|
111
|
+
if (recorded === modes.length) return 'covered';
|
|
112
|
+
return recorded > 0 ? 'partial' : 'none';
|
|
113
|
+
});
|
|
114
|
+
if (each.every((coverage) => coverage === 'covered')) return 'covered';
|
|
115
|
+
return each.some((coverage) => coverage !== 'none') ? 'partial' : 'none';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whose stamps they are: the leading segment of every token they name, each
|
|
120
|
+
* once, in the order first met.
|
|
121
|
+
*
|
|
122
|
+
* A lookup path starts with the document's name, which is also the name of the
|
|
123
|
+
* base collection variable sync writes, so this names the documents synced into
|
|
124
|
+
* the file without a document loaded to compare against.
|
|
125
|
+
*/
|
|
126
|
+
function recordNamesOf(tokens: readonly string[]): string[] {
|
|
127
|
+
const names = new Set<string>();
|
|
128
|
+
for (const path of tokens) {
|
|
129
|
+
const dot = path.indexOf('.');
|
|
130
|
+
if (dot > 0) names.add(path.slice(0, dot));
|
|
131
|
+
}
|
|
132
|
+
return [...names];
|
|
133
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component files and blueprints kept in the Figma file, so a set can be planned and built
|
|
3
|
+
* again from the plugin's screen without the command line.
|
|
4
|
+
*
|
|
5
|
+
* Kept in the file rather than per user: a template is
|
|
6
|
+
* part of what the file is built from, and everyone who opens the file should
|
|
7
|
+
* be able to build from it. Sharded (`shardedData.ts`), as older builds kept the sync record,
|
|
8
|
+
* since one file of a few dozen members can pass Figma's 100 kB per entry.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { isPlainObject, readSharded, toAsciiJson, writeSharded } from './shardedData.js';
|
|
12
|
+
|
|
13
|
+
const KEY = 'templates';
|
|
14
|
+
|
|
15
|
+
/** Our own limit: room for a few hundred files, and for other plugins. */
|
|
16
|
+
const MAX_SHARDS = 8;
|
|
17
|
+
|
|
18
|
+
export interface KeptTemplate {
|
|
19
|
+
/** The component's name, or a blueprint's page (`.blueprint.kdl`), which is the key: one template per name. */
|
|
20
|
+
name: string;
|
|
21
|
+
fileName: string;
|
|
22
|
+
text: string;
|
|
23
|
+
/** When it was kept, ISO. */
|
|
24
|
+
kept: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The kept templates, in the order they were first kept. A damaged record reads as none. */
|
|
28
|
+
export function loadTemplates(): KeptTemplate[] {
|
|
29
|
+
let payload: string;
|
|
30
|
+
try {
|
|
31
|
+
payload = readSharded(KEY);
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
if (!payload) return [];
|
|
36
|
+
try {
|
|
37
|
+
const parsed: unknown = JSON.parse(payload);
|
|
38
|
+
if (!Array.isArray(parsed)) return [];
|
|
39
|
+
return parsed.filter(
|
|
40
|
+
(entry): entry is KeptTemplate =>
|
|
41
|
+
isPlainObject(entry) &&
|
|
42
|
+
typeof entry.name === 'string' &&
|
|
43
|
+
typeof entry.fileName === 'string' &&
|
|
44
|
+
typeof entry.text === 'string' &&
|
|
45
|
+
typeof entry.kept === 'string',
|
|
46
|
+
);
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Writes the list. Returns why it could not, rather than throwing. */
|
|
53
|
+
export function saveTemplates(templates: readonly KeptTemplate[]): { ok: true } | { ok: false; reason: string } {
|
|
54
|
+
const written = writeSharded(KEY, toAsciiJson(templates), MAX_SHARDS);
|
|
55
|
+
if (written.ok) return { ok: true };
|
|
56
|
+
if (written.tooBig) {
|
|
57
|
+
return {
|
|
58
|
+
ok: false,
|
|
59
|
+
reason: `The kept templates come to ${Math.round(written.bytes / 1024)} kB, more than this plugin keeps in one file. Forget one first.`,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
return { ok: false, reason: written.reason ?? 'Figma refused the write.' };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Keeps one, replacing the template of the same name and, when named, the one it takes the place of. */
|
|
66
|
+
export function keepTemplate(templates: readonly KeptTemplate[], template: KeptTemplate, replaces?: string): KeptTemplate[] {
|
|
67
|
+
const without = templates.filter((kept) => kept.name !== template.name && kept.name !== replaces);
|
|
68
|
+
const at = templates.findIndex((kept) => kept.name === template.name || kept.name === replaces);
|
|
69
|
+
if (at < 0) return [...without, template];
|
|
70
|
+
// Where it was, so replacing a file does not move its row.
|
|
71
|
+
const before = templates.slice(0, at).filter((kept) => kept.name !== template.name && kept.name !== replaces);
|
|
72
|
+
return [...before, template, ...without.slice(before.length)];
|
|
73
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The functions that spell the Figma name a token path becomes.
|
|
3
|
+
*
|
|
4
|
+
* Variable sync writes a variable, and the component engine looks one up, by this
|
|
5
|
+
* name, so both call it and nothing else. It lives in a module of its own, and
|
|
6
|
+
* `correspondence.ts` re-exports it, so that the component script, which bundles
|
|
7
|
+
* only the engine, does not carry the sync comparison and its dependencies with it.
|
|
8
|
+
*
|
|
9
|
+
* A name has no collection in it: the variable lives in a collection, and a
|
|
10
|
+
* component file writes `{space.md}`, not `{ds.space.md}`. So the path these take
|
|
11
|
+
* is a *reference path*, the token's path without the collection segment, and
|
|
12
|
+
* lib's `referencePath()` turns a lookup path into one. Keys keep their spelling:
|
|
13
|
+
* `button.backgroundColor` is `button/backgroundColor`.
|
|
14
|
+
*/
|
|
15
|
+
export function tokenPathToVariableName(path: string, separator = '/'): string {
|
|
16
|
+
return path.replace(/\./g, separator);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* A chain link's variable name: the token's name, `--`, the modifier.
|
|
21
|
+
*
|
|
22
|
+
* The head of a chain lives in another collection under the bare name, and a
|
|
23
|
+
* name found in two collections stops a component build, so a link needs the
|
|
24
|
+
* suffix. It is the CSS adapter's link rule, so one rule names a link in both targets.
|
|
25
|
+
*/
|
|
26
|
+
export function linkVariableName(path: string, modifier: string): string {
|
|
27
|
+
return `${tokenPathToVariableName(path)}--${modifier}`;
|
|
28
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stamps the variable and style syncs keep on what they write.
|
|
3
|
+
*
|
|
4
|
+
* A stamp (`components/stamp.ts`) lives on the object it describes: the token the object holds, and a receipt, for
|
|
5
|
+
* each field Toghen owns, of what Figma reported right after Toghen wrote it. A variable's fields are its value in each
|
|
6
|
+
* mode, keyed by the mode's id, and `scopes`; a style's are the properties a style plan writes (`reportOf`). What
|
|
7
|
+
* Figma reports now against the receipt says whether anyone changed the object since (drift); where nobody did, the
|
|
8
|
+
* document against what Figma holds says whether the document moved.
|
|
9
|
+
*
|
|
10
|
+
* Kept on the object, a receipt cannot be carried onto another one, which a record keyed by token path on the
|
|
11
|
+
* document root could (decisions, *Ruled: the stamp, one receipt per object, for every target*). Older builds kept
|
|
12
|
+
* that record; `rootRecord.ts` turns it into stamps, once.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getStamp, receiptOf, setStamp, sortOut, type Stamp, type Stamped } from '../components/stamp.js';
|
|
16
|
+
|
|
17
|
+
/** Adds to an object's receipt what Figma reports for the fields just written. A stamp for another token starts again. */
|
|
18
|
+
export function stampWritten(object: Stamped, token: string, reported: Record<string, unknown>): void {
|
|
19
|
+
const held = getStamp(object);
|
|
20
|
+
const receipt = held?.is.token === token ? held.receipt : {};
|
|
21
|
+
setStamp(object, { is: { token }, receipt: { ...receipt, ...receiptOf(reported) } });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Scopes as a receipt hashes them: Figma keeps them in an order of its own, and they compare as a set. */
|
|
25
|
+
export const reportedScopes = (scopes: readonly VariableScope[]): VariableScope[] => [...scopes].sort();
|
|
26
|
+
|
|
27
|
+
/** What Figma reports for the properties a style plan writes: what a style's receipt hashes, field by field. */
|
|
28
|
+
export function reportOf(style: BaseStyle): Record<string, unknown> {
|
|
29
|
+
switch (style.type) {
|
|
30
|
+
case 'TEXT': {
|
|
31
|
+
const text = style as TextStyle;
|
|
32
|
+
return {
|
|
33
|
+
// Figma reads a font back with variation settings nobody set (measured 20 October 2026): a style holds its family and style.
|
|
34
|
+
fontName: { family: text.fontName.family, style: text.fontName.style },
|
|
35
|
+
fontSize: text.fontSize,
|
|
36
|
+
lineHeight: text.lineHeight,
|
|
37
|
+
letterSpacing: text.letterSpacing,
|
|
38
|
+
textCase: text.textCase,
|
|
39
|
+
textDecoration: text.textDecoration,
|
|
40
|
+
boundVariables: text.boundVariables,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
case 'EFFECT':
|
|
44
|
+
return { effects: (style as EffectStyle).effects };
|
|
45
|
+
case 'PAINT':
|
|
46
|
+
return { paints: (style as PaintStyle).paints };
|
|
47
|
+
default:
|
|
48
|
+
return {};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type StyleKind = 'TEXT' | 'EFFECT' | 'PAINT';
|
|
53
|
+
|
|
54
|
+
/** A local style, with its stamp when it is Toghen's. */
|
|
55
|
+
export interface LocalStyle {
|
|
56
|
+
style: BaseStyle;
|
|
57
|
+
styleType: StyleKind;
|
|
58
|
+
/** Absent on a style Toghen did not write, and on a person's copy of one. */
|
|
59
|
+
stamp?: Stamp;
|
|
60
|
+
/** The style moved: its stamp names another id, which the next write replaces with its own. */
|
|
61
|
+
moved?: true;
|
|
62
|
+
/** A person's copy of a style Toghen wrote: the original. It carries the original's stamp, which is not read as its own. */
|
|
63
|
+
copyOf?: BaseStyle;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** The file's local styles, each with its stamp when it is Toghen's, and each copy of one with its original (`sortOut`). */
|
|
67
|
+
export async function localStyles(): Promise<LocalStyle[]> {
|
|
68
|
+
const [text, effect, paint] = await Promise.all([
|
|
69
|
+
figma.getLocalTextStylesAsync(),
|
|
70
|
+
figma.getLocalEffectStylesAsync(),
|
|
71
|
+
figma.getLocalPaintStylesAsync(),
|
|
72
|
+
]);
|
|
73
|
+
const all = [
|
|
74
|
+
...text.map((style) => ({ style, styleType: 'TEXT' as const })),
|
|
75
|
+
...effect.map((style) => ({ style, styleType: 'EFFECT' as const })),
|
|
76
|
+
...paint.map((style) => ({ style, styleType: 'PAINT' as const })),
|
|
77
|
+
];
|
|
78
|
+
const byId = new Map<string, BaseStyle>(all.map(({ style }) => [style.id, style]));
|
|
79
|
+
const sorted = await sortOut(
|
|
80
|
+
all.map(({ style }) => style),
|
|
81
|
+
getStamp,
|
|
82
|
+
(id) => byId.get(id),
|
|
83
|
+
);
|
|
84
|
+
const copyOf = new Map(sorted.copies.map(({ copy, of }) => [copy, of]));
|
|
85
|
+
return all.map(({ style, styleType }) => {
|
|
86
|
+
const of = copyOf.get(style);
|
|
87
|
+
if (of) return { style, styleType, copyOf: of };
|
|
88
|
+
const stamp = getStamp(style);
|
|
89
|
+
return { style, styleType, ...(stamp ? { stamp } : {}), ...(sorted.moved.has(style) ? { moved: true as const } : {}) };
|
|
90
|
+
});
|
|
91
|
+
}
|
package/manifest.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Toghen",
|
|
3
|
+
"id": "com.toghen.figma",
|
|
4
|
+
"api": "1.0.0",
|
|
5
|
+
"main": "dist/code.js",
|
|
6
|
+
"ui": "dist/ui.html",
|
|
7
|
+
"editorType": ["figma"],
|
|
8
|
+
"documentAccess": "dynamic-page",
|
|
9
|
+
"permissions": ["teamlibrary"],
|
|
10
|
+
"enablePrivatePluginApi": true,
|
|
11
|
+
"networkAccess": {
|
|
12
|
+
"allowedDomains": ["none"],
|
|
13
|
+
"devAllowedDomains": ["ws://localhost:3055"]
|
|
14
|
+
}
|
|
15
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,66 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toghen/figma",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"toghen-figma": "cli/bin.mjs"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "node build.mjs",
|
|
10
|
+
"build:dist": "node build.mjs",
|
|
11
|
+
"build:dist:prod": "NODE_ENV=production node build.mjs",
|
|
12
|
+
"build:watch": "node build.mjs --watch",
|
|
13
|
+
"preview": "node scripts/preview.mjs",
|
|
14
|
+
"typecheck": "tsc -p tsconfig.plugin.json && tsc -p tsconfig.test.json",
|
|
15
|
+
"expectations": "tsx scripts/expectation-sheet.ts",
|
|
16
|
+
"components": "node cli/bin.mjs components",
|
|
17
|
+
"toghen-figma": "node cli/bin.mjs",
|
|
18
|
+
"pretest": "pnpm typecheck",
|
|
19
|
+
"test": "node --import tsx/esm --test 'test/**/*.test.ts'",
|
|
20
|
+
"lint": "biome check .",
|
|
21
|
+
"format": "biome check --write .",
|
|
22
|
+
"check": "biome check . && pnpm typecheck && pnpm test && pnpm build && node scripts/check-purity.mjs && node scripts/check-leaks.mjs",
|
|
23
|
+
"prepack": "node build.mjs"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@biomejs/biome": "2.5.15",
|
|
27
|
+
"@figma/plugin-typings": "^1.142.0",
|
|
28
|
+
"@toghen/ui": "^0.1.0",
|
|
29
|
+
"@types/node": "^24.1.0",
|
|
30
|
+
"@types/ws": "^8.18.2",
|
|
31
|
+
"esbuild": "^0.21.0",
|
|
32
|
+
"postcss": "^8.5.8",
|
|
33
|
+
"typescript": "^5.8.3"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@bgotink/kdl": "^0.4.0",
|
|
37
|
+
"@toghen/lib": "^0.1.0",
|
|
38
|
+
"kdlet": "^0.1.0",
|
|
39
|
+
"kdlet-guards": "^0.1.0",
|
|
40
|
+
"tsx": "^4.20.3",
|
|
41
|
+
"ws": "^8.22.0"
|
|
42
|
+
},
|
|
43
|
+
"license": "MIT",
|
|
44
|
+
"author": "Egildo Tagliareni <egildo@tagliareni.it>",
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://codeberg.org/Toghen/figma.git"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=24"
|
|
51
|
+
},
|
|
52
|
+
"packageManager": "pnpm@10.29.3",
|
|
53
|
+
"description": "Toghen's Figma adapter: tokens to Variables and Styles, component files, and the toghen-figma command line",
|
|
54
|
+
"files": [
|
|
55
|
+
"cli/",
|
|
56
|
+
"main/",
|
|
57
|
+
"shared/",
|
|
58
|
+
"dist/",
|
|
59
|
+
"manifest.json",
|
|
60
|
+
"README.md",
|
|
61
|
+
"guide.md",
|
|
62
|
+
"reference.md",
|
|
63
|
+
"component-files.md",
|
|
64
|
+
"LICENSE"
|
|
65
|
+
]
|
|
66
|
+
}
|