@mp3wizard/figma-console-mcp 1.32.2 → 1.34.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/README.md +26 -17
- package/dist/cloudflare/core/cloud-websocket-connector.js +18 -0
- package/dist/cloudflare/core/design-code-tools.js +60 -17
- package/dist/cloudflare/core/design-system-manifest.js +19 -14
- package/dist/cloudflare/core/design-system-tools.js +43 -34
- package/dist/cloudflare/core/diagnose-tool.js +4 -0
- package/dist/cloudflare/core/enrichment/enrichment-service.js +11 -5
- package/dist/cloudflare/core/enrichment/style-resolver.js +38 -18
- package/dist/cloudflare/core/figma-api.js +118 -54
- package/dist/cloudflare/core/figma-tools.js +179 -63
- package/dist/cloudflare/core/port-discovery.js +404 -31
- package/dist/cloudflare/core/tokens/alias-resolver.js +75 -5
- package/dist/cloudflare/core/tokens/config.js +10 -0
- package/dist/cloudflare/core/tokens/dialect.js +232 -0
- package/dist/cloudflare/core/tokens/figma-converter.js +144 -16
- package/dist/cloudflare/core/tokens/formatters/css-vars.js +21 -12
- package/dist/cloudflare/core/tokens/formatters/dtcg.js +106 -30
- package/dist/cloudflare/core/tokens/formatters/json.js +28 -10
- package/dist/cloudflare/core/tokens/formatters/scss.js +19 -13
- package/dist/cloudflare/core/tokens/formatters/style-dictionary-v3.js +15 -9
- package/dist/cloudflare/core/tokens/formatters/tailwind-v4.js +14 -9
- package/dist/cloudflare/core/tokens/formatters/tokens-studio.js +11 -5
- package/dist/cloudflare/core/tokens/index.js +2 -1
- package/dist/cloudflare/core/tokens/parsers/dtcg.js +32 -5
- package/dist/cloudflare/core/tokens/schemas.js +4 -0
- package/dist/cloudflare/core/tokens-tools.js +1017 -88
- package/dist/cloudflare/core/version-tools.js +44 -3
- package/dist/cloudflare/core/websocket-connector.js +42 -0
- package/dist/cloudflare/core/websocket-server.js +99 -8
- package/dist/cloudflare/core/write-tools.js +355 -86
- package/dist/cloudflare/index.js +7 -7
- package/dist/core/design-code-tools.d.ts.map +1 -1
- package/dist/core/design-code-tools.js +60 -17
- package/dist/core/design-code-tools.js.map +1 -1
- package/dist/core/design-system-manifest.d.ts +1 -0
- package/dist/core/design-system-manifest.d.ts.map +1 -1
- package/dist/core/design-system-manifest.js +19 -14
- package/dist/core/design-system-manifest.js.map +1 -1
- package/dist/core/design-system-tools.d.ts.map +1 -1
- package/dist/core/design-system-tools.js +43 -34
- package/dist/core/design-system-tools.js.map +1 -1
- package/dist/core/diagnose-tool.d.ts +8 -0
- package/dist/core/diagnose-tool.d.ts.map +1 -1
- package/dist/core/diagnose-tool.js +4 -0
- package/dist/core/diagnose-tool.js.map +1 -1
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -1
- package/dist/core/enrichment/enrichment-service.js +11 -5
- package/dist/core/enrichment/enrichment-service.js.map +1 -1
- package/dist/core/enrichment/style-resolver.d.ts +7 -2
- package/dist/core/enrichment/style-resolver.d.ts.map +1 -1
- package/dist/core/enrichment/style-resolver.js +38 -18
- package/dist/core/enrichment/style-resolver.js.map +1 -1
- package/dist/core/figma-api.d.ts +18 -9
- package/dist/core/figma-api.d.ts.map +1 -1
- package/dist/core/figma-api.js +118 -54
- package/dist/core/figma-api.js.map +1 -1
- package/dist/core/figma-connector.d.ts +12 -0
- package/dist/core/figma-connector.d.ts.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +179 -63
- package/dist/core/figma-tools.js.map +1 -1
- package/dist/core/port-discovery.d.ts +40 -0
- package/dist/core/port-discovery.d.ts.map +1 -1
- package/dist/core/port-discovery.js +404 -31
- package/dist/core/port-discovery.js.map +1 -1
- package/dist/core/tokens/alias-resolver.d.ts +45 -3
- package/dist/core/tokens/alias-resolver.d.ts.map +1 -1
- package/dist/core/tokens/alias-resolver.js +75 -5
- package/dist/core/tokens/alias-resolver.js.map +1 -1
- package/dist/core/tokens/config.d.ts +28 -0
- package/dist/core/tokens/config.d.ts.map +1 -1
- package/dist/core/tokens/config.js +10 -0
- package/dist/core/tokens/config.js.map +1 -1
- package/dist/core/tokens/dialect.d.ts +107 -0
- package/dist/core/tokens/dialect.d.ts.map +1 -0
- package/dist/core/tokens/dialect.js +233 -0
- package/dist/core/tokens/dialect.js.map +1 -0
- package/dist/core/tokens/figma-converter.d.ts +23 -2
- package/dist/core/tokens/figma-converter.d.ts.map +1 -1
- package/dist/core/tokens/figma-converter.js +144 -16
- package/dist/core/tokens/figma-converter.js.map +1 -1
- package/dist/core/tokens/formatters/css-vars.d.ts.map +1 -1
- package/dist/core/tokens/formatters/css-vars.js +21 -12
- package/dist/core/tokens/formatters/css-vars.js.map +1 -1
- package/dist/core/tokens/formatters/dtcg.d.ts +2 -2
- package/dist/core/tokens/formatters/dtcg.d.ts.map +1 -1
- package/dist/core/tokens/formatters/dtcg.js +106 -30
- package/dist/core/tokens/formatters/dtcg.js.map +1 -1
- package/dist/core/tokens/formatters/json.d.ts.map +1 -1
- package/dist/core/tokens/formatters/json.js +28 -10
- package/dist/core/tokens/formatters/json.js.map +1 -1
- package/dist/core/tokens/formatters/scss.d.ts.map +1 -1
- package/dist/core/tokens/formatters/scss.js +19 -13
- package/dist/core/tokens/formatters/scss.js.map +1 -1
- package/dist/core/tokens/formatters/style-dictionary-v3.d.ts.map +1 -1
- package/dist/core/tokens/formatters/style-dictionary-v3.js +15 -9
- package/dist/core/tokens/formatters/style-dictionary-v3.js.map +1 -1
- package/dist/core/tokens/formatters/tailwind-v4.d.ts.map +1 -1
- package/dist/core/tokens/formatters/tailwind-v4.js +14 -9
- package/dist/core/tokens/formatters/tailwind-v4.js.map +1 -1
- package/dist/core/tokens/formatters/tokens-studio.d.ts.map +1 -1
- package/dist/core/tokens/formatters/tokens-studio.js +11 -5
- package/dist/core/tokens/formatters/tokens-studio.js.map +1 -1
- package/dist/core/tokens/index.d.ts +2 -1
- package/dist/core/tokens/index.d.ts.map +1 -1
- package/dist/core/tokens/index.js +2 -1
- package/dist/core/tokens/index.js.map +1 -1
- package/dist/core/tokens/parsers/dtcg.js +32 -5
- package/dist/core/tokens/parsers/dtcg.js.map +1 -1
- package/dist/core/tokens/schemas.d.ts +3 -0
- package/dist/core/tokens/schemas.d.ts.map +1 -1
- package/dist/core/tokens/schemas.js +4 -0
- package/dist/core/tokens/schemas.js.map +1 -1
- package/dist/core/tokens/types.d.ts +57 -1
- package/dist/core/tokens/types.d.ts.map +1 -1
- package/dist/core/tokens/types.js.map +1 -1
- package/dist/core/tokens-tools.d.ts +250 -7
- package/dist/core/tokens-tools.d.ts.map +1 -1
- package/dist/core/tokens-tools.js +1017 -88
- package/dist/core/tokens-tools.js.map +1 -1
- package/dist/core/version-tools.d.ts.map +1 -1
- package/dist/core/version-tools.js +44 -3
- package/dist/core/version-tools.js.map +1 -1
- package/dist/core/websocket-connector.d.ts +38 -0
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +42 -0
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/websocket-server.d.ts +23 -0
- package/dist/core/websocket-server.d.ts.map +1 -1
- package/dist/core/websocket-server.js +99 -8
- package/dist/core/websocket-server.js.map +1 -1
- package/dist/core/write-tools.d.ts.map +1 -1
- package/dist/core/write-tools.js +355 -86
- package/dist/core/write-tools.js.map +1 -1
- package/dist/local.d.ts +0 -1
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +253 -63
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +382 -28
- package/figma-desktop-bridge/ui.html +578 -292
- package/package.json +2 -2
|
@@ -5,8 +5,11 @@
|
|
|
5
5
|
// which then forwards messages to the MCP server over the WebSocket bridge.
|
|
6
6
|
|
|
7
7
|
// Plugin version — sent in FILE_INFO for server-side version compatibility checks.
|
|
8
|
-
// The server compares this against
|
|
9
|
-
|
|
8
|
+
// The server compares this against the version of the plugin files IT ships to
|
|
9
|
+
// detect stale cached plugins. Bumped by scripts/release.sh ONLY when plugin files
|
|
10
|
+
// change (see issue #62); server-only releases leave it alone, so it may lag
|
|
11
|
+
// package.json — that's intentional, not drift.
|
|
12
|
+
var PLUGIN_VERSION = '1.34.0'; // Last release in which plugin files changed.
|
|
10
13
|
|
|
11
14
|
console.log('🌉 [Desktop Bridge] Plugin loaded (v' + PLUGIN_VERSION + ')');
|
|
12
15
|
|
|
@@ -148,6 +151,23 @@ var __stickyColors = {
|
|
|
148
151
|
}
|
|
149
152
|
})();
|
|
150
153
|
|
|
154
|
+
// Restore persisted cloud pairing config (stored via STORE_CLOUD_CONFIG) and
|
|
155
|
+
// push it to the UI so cloud users don't lose their pairing on plugin reopen.
|
|
156
|
+
// Fire-and-forget: never blocks the FILE_INFO/VARIABLES_DATA pushes above.
|
|
157
|
+
(function() {
|
|
158
|
+
figma.clientStorage.getAsync('cloudConfig')
|
|
159
|
+
.then(function(stored) {
|
|
160
|
+
// Skip if nothing stored or the shape is unusable (e.g., cleared config)
|
|
161
|
+
if (!stored || !stored.code) return;
|
|
162
|
+
figma.ui.postMessage({ type: 'CLOUD_CONFIG_RESTORED', config: stored });
|
|
163
|
+
console.log('🌉 [Desktop Bridge] Restored cloud config from clientStorage');
|
|
164
|
+
})
|
|
165
|
+
.catch(function(error) {
|
|
166
|
+
// clientStorage can throw — non-critical, just log
|
|
167
|
+
console.warn('🌉 [Desktop Bridge] Could not restore cloud config:', error && error.message ? error.message : String(error));
|
|
168
|
+
});
|
|
169
|
+
})();
|
|
170
|
+
|
|
151
171
|
// Helper function to serialize a variable for response
|
|
152
172
|
function serializeVariable(v) {
|
|
153
173
|
return {
|
|
@@ -356,6 +376,32 @@ async function loadFontsForNode(node) {
|
|
|
356
376
|
}
|
|
357
377
|
}
|
|
358
378
|
|
|
379
|
+
// Resolve a caller-supplied component property name against the instance's
|
|
380
|
+
// actual property keys. TEXT/BOOLEAN/INSTANCE_SWAP property names carry a
|
|
381
|
+
// #nodeId suffix (e.g. "title#2605:17"), so a bare "title" from the caller
|
|
382
|
+
// must be mapped to the suffixed key. Resolution tiers:
|
|
383
|
+
// 1. Exact match (covers VARIANT props and fully-suffixed names)
|
|
384
|
+
// 2. Base name before '#' matches exactly
|
|
385
|
+
// 3. Case-insensitive base-name match
|
|
386
|
+
// Returns the resolved key, or null if nothing matches.
|
|
387
|
+
function resolvePropertyName(currentProps, name) {
|
|
388
|
+
if (!currentProps || !name) return null;
|
|
389
|
+
if (currentProps[name] !== undefined) return name;
|
|
390
|
+
var existingProp;
|
|
391
|
+
for (existingProp in currentProps) {
|
|
392
|
+
if (currentProps.hasOwnProperty(existingProp) && existingProp.split('#')[0] === name) {
|
|
393
|
+
return existingProp;
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
var lowerName = String(name).toLowerCase();
|
|
397
|
+
for (existingProp in currentProps) {
|
|
398
|
+
if (currentProps.hasOwnProperty(existingProp) && existingProp.split('#')[0].toLowerCase() === lowerName) {
|
|
399
|
+
return existingProp;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
|
|
359
405
|
// Listen for requests from UI (e.g., component data requests, write operations)
|
|
360
406
|
figma.ui.onmessage = async (msg) => {
|
|
361
407
|
|
|
@@ -1904,12 +1950,19 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1904
1950
|
// Track failures so they surface in the result instead of failing silently.
|
|
1905
1951
|
var overrideWarnings = [];
|
|
1906
1952
|
|
|
1907
|
-
// Apply property overrides
|
|
1953
|
+
// Apply property overrides. Property keys on the instance carry #nodeId
|
|
1954
|
+
// suffixes ("title#2605:17"), so resolve the caller's bare name first —
|
|
1955
|
+
// otherwise setProperties throws "Could not find a component property".
|
|
1908
1956
|
if (msg.overrides) {
|
|
1957
|
+
var instanceProps = {};
|
|
1958
|
+
try { instanceProps = instance.componentProperties || {}; } catch (e) { instanceProps = {}; }
|
|
1909
1959
|
for (var propName in msg.overrides) {
|
|
1910
1960
|
if (msg.overrides.hasOwnProperty(propName)) {
|
|
1961
|
+
var resolvedName = resolvePropertyName(instanceProps, propName);
|
|
1911
1962
|
try {
|
|
1912
|
-
|
|
1963
|
+
var propsObj = {};
|
|
1964
|
+
propsObj[resolvedName || propName] = msg.overrides[propName];
|
|
1965
|
+
instance.setProperties(propsObj);
|
|
1913
1966
|
} catch (propError) {
|
|
1914
1967
|
var pMsg = propError && propError.message ? propError.message : String(propError);
|
|
1915
1968
|
console.warn('🌉 [Desktop Bridge] Could not set property ' + propName + ':', pMsg);
|
|
@@ -1935,6 +1988,8 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1935
1988
|
var parent = await figma.getNodeByIdAsync(msg.parentId);
|
|
1936
1989
|
if (parent && 'appendChild' in parent) {
|
|
1937
1990
|
parent.appendChild(instance);
|
|
1991
|
+
} else {
|
|
1992
|
+
overrideWarnings.push('parentId "' + msg.parentId + '" not found or cannot accept children; instance placed on current page');
|
|
1938
1993
|
}
|
|
1939
1994
|
}
|
|
1940
1995
|
|
|
@@ -1967,6 +2022,293 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1967
2022
|
}
|
|
1968
2023
|
}
|
|
1969
2024
|
|
|
2025
|
+
// ============================================================================
|
|
2026
|
+
// CREATE_COMPONENT_SET - Build a component set with variants in one call
|
|
2027
|
+
// Two modes:
|
|
2028
|
+
// A) baseComponentId + properties: clone the base for every combination of
|
|
2029
|
+
// the property axes ({State:[default,hover], Size:[sm,lg]} → 4 variants),
|
|
2030
|
+
// rename each to "Prop=Value, Prop=Value", combineAsVariants.
|
|
2031
|
+
// The base component itself BECOMES the first variant (same node ID),
|
|
2032
|
+
// so existing instances of the base survive as instances of that variant.
|
|
2033
|
+
// B) componentIds (+ optional variantProperties): combine existing local
|
|
2034
|
+
// COMPONENT nodes as variants, optionally renaming them first.
|
|
2035
|
+
// Variant properties are derived by Figma from the "Prop=Value" names, and
|
|
2036
|
+
// componentPropertyDefinitions live on the resulting SET (not the variants).
|
|
2037
|
+
// No text is written here, so no font loading is needed (clone() copies text
|
|
2038
|
+
// without touching characters).
|
|
2039
|
+
// ============================================================================
|
|
2040
|
+
else if (msg.type === 'CREATE_COMPONENT_SET') {
|
|
2041
|
+
try {
|
|
2042
|
+
console.log('🌉 [Desktop Bridge] Creating component set:', msg.name || msg.baseComponentId || (msg.componentIds && msg.componentIds.length + ' components'));
|
|
2043
|
+
|
|
2044
|
+
var MAX_VARIANTS = 100;
|
|
2045
|
+
var setWarnings = [];
|
|
2046
|
+
// Rollback state — if anything throws before combineAsVariants succeeds,
|
|
2047
|
+
// remove Mode A clones, restore the base's name, AND restore every
|
|
2048
|
+
// Mode B component the handler renamed, so no partial artifacts remain.
|
|
2049
|
+
// (Previously only Mode A was rolled back: a throw from the
|
|
2050
|
+
// duplicate-combo check or combineAsVariants left the user's standalone
|
|
2051
|
+
// components permanently renamed with no set created.)
|
|
2052
|
+
var createdClones = [];
|
|
2053
|
+
var renamedBase = null;
|
|
2054
|
+
var originalBaseName = null;
|
|
2055
|
+
var renamedNodes = []; // Mode B: [{ node, originalName }]
|
|
2056
|
+
var combineDone = false;
|
|
2057
|
+
|
|
2058
|
+
function rollbackComponentSetWork() {
|
|
2059
|
+
for (var rc = 0; rc < createdClones.length; rc++) {
|
|
2060
|
+
try { createdClones[rc].remove(); } catch (e) {}
|
|
2061
|
+
}
|
|
2062
|
+
createdClones = [];
|
|
2063
|
+
if (renamedBase && originalBaseName !== null) {
|
|
2064
|
+
try { renamedBase.name = originalBaseName; } catch (e) {}
|
|
2065
|
+
renamedBase = null;
|
|
2066
|
+
}
|
|
2067
|
+
for (var rn = 0; rn < renamedNodes.length; rn++) {
|
|
2068
|
+
try { renamedNodes[rn].node.name = renamedNodes[rn].originalName; } catch (e) {}
|
|
2069
|
+
}
|
|
2070
|
+
renamedNodes = [];
|
|
2071
|
+
}
|
|
2072
|
+
|
|
2073
|
+
// Build "Prop=Value, Prop=Value" from an axis-ordered property map
|
|
2074
|
+
function comboName(combo, axisOrder) {
|
|
2075
|
+
var parts = [];
|
|
2076
|
+
for (var a = 0; a < axisOrder.length; a++) {
|
|
2077
|
+
parts.push(axisOrder[a] + '=' + combo[axisOrder[a]]);
|
|
2078
|
+
}
|
|
2079
|
+
return parts.join(', ');
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
var variantNodes = [];
|
|
2083
|
+
|
|
2084
|
+
if (msg.baseComponentId) {
|
|
2085
|
+
// ---- Mode A: generate variants from a base component ----
|
|
2086
|
+
if (!msg.properties || Object.keys(msg.properties).length === 0) {
|
|
2087
|
+
throw new Error('properties is required with baseComponentId. Example: { "State": ["default", "hover"], "Size": ["sm", "lg"] }');
|
|
2088
|
+
}
|
|
2089
|
+
|
|
2090
|
+
var base = await figma.getNodeByIdAsync(msg.baseComponentId);
|
|
2091
|
+
if (!base) throw new Error('Base component not found: ' + msg.baseComponentId + '. NodeIds are session-specific — re-search components to get fresh IDs.');
|
|
2092
|
+
if (base.type !== 'COMPONENT') {
|
|
2093
|
+
throw new Error('baseComponentId must be a COMPONENT node, got ' + base.type + '. To convert a frame, use figma_execute with figma.createComponentFromNode(frame) first.');
|
|
2094
|
+
}
|
|
2095
|
+
if (base.parent && base.parent.type === 'COMPONENT_SET') {
|
|
2096
|
+
throw new Error('Base component is already a variant inside component set "' + base.parent.name + '". Use a standalone component.');
|
|
2097
|
+
}
|
|
2098
|
+
|
|
2099
|
+
// Validate axes and build the cartesian product (last axis varies fastest,
|
|
2100
|
+
// matching Figma's convention of State as the final property)
|
|
2101
|
+
var axisOrder = [];
|
|
2102
|
+
var axisValues = {};
|
|
2103
|
+
for (var axis in msg.properties) {
|
|
2104
|
+
if (msg.properties.hasOwnProperty(axis)) {
|
|
2105
|
+
var vals = msg.properties[axis];
|
|
2106
|
+
if (!vals || !vals.length) throw new Error('Property axis "' + axis + '" has no values');
|
|
2107
|
+
var uniq = {};
|
|
2108
|
+
for (var vv = 0; vv < vals.length; vv++) {
|
|
2109
|
+
var sval = String(vals[vv]);
|
|
2110
|
+
if (sval.indexOf('=') !== -1 || sval.indexOf(',') !== -1) {
|
|
2111
|
+
throw new Error('Variant value "' + sval + '" (axis "' + axis + '") must not contain "=" or "," — Figma parses variant names on those characters');
|
|
2112
|
+
}
|
|
2113
|
+
if (uniq[sval]) throw new Error('Duplicate value "' + sval + '" in axis "' + axis + '"');
|
|
2114
|
+
uniq[sval] = true;
|
|
2115
|
+
}
|
|
2116
|
+
if (axis.indexOf('=') !== -1 || axis.indexOf(',') !== -1) {
|
|
2117
|
+
throw new Error('Property name "' + axis + '" must not contain "=" or ","');
|
|
2118
|
+
}
|
|
2119
|
+
axisOrder.push(axis);
|
|
2120
|
+
axisValues[axis] = vals.map(String);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
var combos = [{}];
|
|
2125
|
+
for (var ai = 0; ai < axisOrder.length; ai++) {
|
|
2126
|
+
var nextCombos = [];
|
|
2127
|
+
var axisName = axisOrder[ai];
|
|
2128
|
+
for (var ci = 0; ci < combos.length; ci++) {
|
|
2129
|
+
for (var vi2 = 0; vi2 < axisValues[axisName].length; vi2++) {
|
|
2130
|
+
var extended = {};
|
|
2131
|
+
for (var k in combos[ci]) { if (combos[ci].hasOwnProperty(k)) extended[k] = combos[ci][k]; }
|
|
2132
|
+
extended[axisName] = axisValues[axisName][vi2];
|
|
2133
|
+
nextCombos.push(extended);
|
|
2134
|
+
}
|
|
2135
|
+
}
|
|
2136
|
+
combos = nextCombos;
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
if (combos.length > MAX_VARIANTS) {
|
|
2140
|
+
throw new Error('Requested ' + combos.length + ' variants — capped at ' + MAX_VARIANTS + '. Split into multiple sets or reduce axis values.');
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
// Base becomes the first combination; clones fill in the rest.
|
|
2144
|
+
// Stack clones vertically so nothing overlaps even without autoArrange.
|
|
2145
|
+
var baseX = base.x;
|
|
2146
|
+
var baseY = base.y;
|
|
2147
|
+
var stepY = Math.ceil(base.height) + 40;
|
|
2148
|
+
originalBaseName = base.name;
|
|
2149
|
+
renamedBase = base;
|
|
2150
|
+
base.name = comboName(combos[0], axisOrder);
|
|
2151
|
+
variantNodes.push(base);
|
|
2152
|
+
for (var ci2 = 1; ci2 < combos.length; ci2++) {
|
|
2153
|
+
var clone = base.clone();
|
|
2154
|
+
clone.name = comboName(combos[ci2], axisOrder);
|
|
2155
|
+
clone.x = baseX;
|
|
2156
|
+
clone.y = baseY + ci2 * stepY;
|
|
2157
|
+
createdClones.push(clone);
|
|
2158
|
+
variantNodes.push(clone);
|
|
2159
|
+
}
|
|
2160
|
+
} else if (msg.componentIds && msg.componentIds.length) {
|
|
2161
|
+
// ---- Mode B: combine existing components ----
|
|
2162
|
+
if (msg.componentIds.length > MAX_VARIANTS) {
|
|
2163
|
+
throw new Error('Requested ' + msg.componentIds.length + ' variants — capped at ' + MAX_VARIANTS + '.');
|
|
2164
|
+
}
|
|
2165
|
+
if (msg.variantProperties && msg.variantProperties.length !== msg.componentIds.length) {
|
|
2166
|
+
throw new Error('variantProperties length (' + msg.variantProperties.length + ') must match componentIds length (' + msg.componentIds.length + ')');
|
|
2167
|
+
}
|
|
2168
|
+
|
|
2169
|
+
for (var ni = 0; ni < msg.componentIds.length; ni++) {
|
|
2170
|
+
var node = await figma.getNodeByIdAsync(msg.componentIds[ni]);
|
|
2171
|
+
if (!node) throw new Error('Component not found: ' + msg.componentIds[ni] + '. NodeIds are session-specific — re-search components to get fresh IDs.');
|
|
2172
|
+
if (node.type !== 'COMPONENT') {
|
|
2173
|
+
throw new Error('Node ' + msg.componentIds[ni] + ' is a ' + node.type + ', not a COMPONENT. All componentIds must reference COMPONENT nodes.');
|
|
2174
|
+
}
|
|
2175
|
+
if (node.parent && node.parent.type === 'COMPONENT_SET') {
|
|
2176
|
+
throw new Error('Component "' + node.name + '" (' + node.id + ') is already a variant in set "' + node.parent.name + '". Components can only belong to one set.');
|
|
2177
|
+
}
|
|
2178
|
+
if (msg.variantProperties) {
|
|
2179
|
+
var props = msg.variantProperties[ni];
|
|
2180
|
+
var propKeys = Object.keys(props);
|
|
2181
|
+
if (!propKeys.length) throw new Error('variantProperties[' + ni + '] is empty — each entry needs at least one property (e.g. { "State": "hover" })');
|
|
2182
|
+
// Same '='/',' validation as Mode A — a value like "hover,Size=lg"
|
|
2183
|
+
// would silently create a bogus axis when Figma parses the name.
|
|
2184
|
+
for (var pk = 0; pk < propKeys.length; pk++) {
|
|
2185
|
+
var propName = propKeys[pk];
|
|
2186
|
+
var propValue = String(props[propName]);
|
|
2187
|
+
if (propName.indexOf('=') !== -1 || propName.indexOf(',') !== -1) {
|
|
2188
|
+
throw new Error('Property name "' + propName + '" (variantProperties[' + ni + ']) must not contain "=" or "," — Figma parses variant names on those characters');
|
|
2189
|
+
}
|
|
2190
|
+
if (propValue.indexOf('=') !== -1 || propValue.indexOf(',') !== -1) {
|
|
2191
|
+
throw new Error('Variant value "' + propValue + '" (property "' + propName + '", variantProperties[' + ni + ']) must not contain "=" or "," — Figma parses variant names on those characters');
|
|
2192
|
+
}
|
|
2193
|
+
}
|
|
2194
|
+
// Record the rename so the rollback path can restore it.
|
|
2195
|
+
renamedNodes.push({ node: node, originalName: node.name });
|
|
2196
|
+
variantNodes.push(node);
|
|
2197
|
+
node.name = comboName(props, propKeys);
|
|
2198
|
+
} else {
|
|
2199
|
+
variantNodes.push(node);
|
|
2200
|
+
if (node.name.indexOf('=') === -1) {
|
|
2201
|
+
setWarnings.push('Component "' + node.name + '" is not named "Prop=Value" — Figma will file it under "Property 1=' + node.name + '". Pass variantProperties to control naming.');
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
}
|
|
2205
|
+
|
|
2206
|
+
// Duplicate variant names create an invalid (conflicting) set — fail early
|
|
2207
|
+
var nameSeen = {};
|
|
2208
|
+
for (var dn = 0; dn < variantNodes.length; dn++) {
|
|
2209
|
+
if (nameSeen[variantNodes[dn].name]) {
|
|
2210
|
+
throw new Error('Two variants would have identical properties: "' + variantNodes[dn].name + '". Each variant needs a unique property combination.');
|
|
2211
|
+
}
|
|
2212
|
+
nameSeen[variantNodes[dn].name] = true;
|
|
2213
|
+
}
|
|
2214
|
+
} else {
|
|
2215
|
+
throw new Error('Provide either baseComponentId + properties (generate variants) or componentIds (combine existing components).');
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2218
|
+
// Resolve the parent — combineAsVariants REQUIRES a parent, and the set is
|
|
2219
|
+
// created inside it. Fall back to the current page.
|
|
2220
|
+
var setParent = figma.currentPage;
|
|
2221
|
+
if (msg.parentId) {
|
|
2222
|
+
var requestedParent = await figma.getNodeByIdAsync(msg.parentId);
|
|
2223
|
+
if (requestedParent && typeof requestedParent.appendChild === 'function' && requestedParent.type !== 'COMPONENT' && requestedParent.type !== 'COMPONENT_SET' && requestedParent.type !== 'INSTANCE') {
|
|
2224
|
+
setParent = requestedParent;
|
|
2225
|
+
} else {
|
|
2226
|
+
setWarnings.push('parentId "' + msg.parentId + '" not found or cannot contain a component set; created on the current page instead');
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
|
|
2230
|
+
// Housekeeping on failure happens in the outer catch (gated on
|
|
2231
|
+
// !combineDone) — it restores Mode A clones/base AND Mode B renames.
|
|
2232
|
+
var componentSet = figma.combineAsVariants(variantNodes, setParent);
|
|
2233
|
+
combineDone = true;
|
|
2234
|
+
if (msg.name) componentSet.name = msg.name;
|
|
2235
|
+
if (msg.position) {
|
|
2236
|
+
componentSet.x = msg.position.x || 0;
|
|
2237
|
+
componentSet.y = msg.position.y || 0;
|
|
2238
|
+
}
|
|
2239
|
+
|
|
2240
|
+
// componentPropertyDefinitions live on the SET (not the variants) —
|
|
2241
|
+
// read them back so callers see the axes Figma actually derived.
|
|
2242
|
+
var propertyDefinitions = {};
|
|
2243
|
+
try {
|
|
2244
|
+
var defs = componentSet.componentPropertyDefinitions;
|
|
2245
|
+
for (var defName in defs) {
|
|
2246
|
+
if (defs.hasOwnProperty(defName)) {
|
|
2247
|
+
propertyDefinitions[defName] = {
|
|
2248
|
+
type: defs[defName].type,
|
|
2249
|
+
defaultValue: defs[defName].defaultValue,
|
|
2250
|
+
variantOptions: defs[defName].variantOptions || []
|
|
2251
|
+
};
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
} catch (defError) {
|
|
2255
|
+
setWarnings.push('Could not read componentPropertyDefinitions: ' + (defError && defError.message ? defError.message : String(defError)));
|
|
2256
|
+
}
|
|
2257
|
+
|
|
2258
|
+
var variantSummaries = [];
|
|
2259
|
+
for (var vs = 0; vs < componentSet.children.length; vs++) {
|
|
2260
|
+
var child = componentSet.children[vs];
|
|
2261
|
+
if (child.type === 'COMPONENT') {
|
|
2262
|
+
// Include each variant's key — instantiation uses VARIANT keys, not the set key
|
|
2263
|
+
variantSummaries.push({ id: child.id, name: child.name, key: child.key });
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
|
|
2267
|
+
figma.currentPage.selection = [componentSet];
|
|
2268
|
+
figma.viewport.scrollAndZoomIntoView([componentSet]);
|
|
2269
|
+
|
|
2270
|
+
console.log('🌉 [Desktop Bridge] Component set created:', componentSet.id, 'with', variantSummaries.length, 'variants');
|
|
2271
|
+
|
|
2272
|
+
// Everything rides in `data` — ui.html's handleResult only forwards
|
|
2273
|
+
// whitelisted top-level fields, and msg.data is on that whitelist.
|
|
2274
|
+
figma.ui.postMessage({
|
|
2275
|
+
type: 'CREATE_COMPONENT_SET_RESULT',
|
|
2276
|
+
requestId: msg.requestId,
|
|
2277
|
+
success: true,
|
|
2278
|
+
data: {
|
|
2279
|
+
componentSet: {
|
|
2280
|
+
id: componentSet.id,
|
|
2281
|
+
name: componentSet.name,
|
|
2282
|
+
key: componentSet.key,
|
|
2283
|
+
x: componentSet.x,
|
|
2284
|
+
y: componentSet.y,
|
|
2285
|
+
width: componentSet.width,
|
|
2286
|
+
height: componentSet.height,
|
|
2287
|
+
parentId: componentSet.parent ? componentSet.parent.id : null
|
|
2288
|
+
},
|
|
2289
|
+
variantCount: variantSummaries.length,
|
|
2290
|
+
variants: variantSummaries,
|
|
2291
|
+
propertyDefinitions: propertyDefinitions,
|
|
2292
|
+
warnings: setWarnings.length ? setWarnings : undefined
|
|
2293
|
+
}
|
|
2294
|
+
});
|
|
2295
|
+
|
|
2296
|
+
} catch (error) {
|
|
2297
|
+
var csErrorMsg = error && error.message ? error.message : String(error);
|
|
2298
|
+
console.error('🌉 [Desktop Bridge] Create component set error:', csErrorMsg);
|
|
2299
|
+
// Undo partial work unless the set was actually created (after
|
|
2300
|
+
// combineAsVariants succeeds the clones/renames live inside the set —
|
|
2301
|
+
// never touch them then).
|
|
2302
|
+
try { if (!combineDone) rollbackComponentSetWork(); } catch (e) {}
|
|
2303
|
+
figma.ui.postMessage({
|
|
2304
|
+
type: 'CREATE_COMPONENT_SET_RESULT',
|
|
2305
|
+
requestId: msg.requestId,
|
|
2306
|
+
success: false,
|
|
2307
|
+
error: csErrorMsg
|
|
2308
|
+
});
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
|
|
1970
2312
|
// ============================================================================
|
|
1971
2313
|
// SET_NODE_DESCRIPTION - Set description on component/style
|
|
1972
2314
|
// ============================================================================
|
|
@@ -2556,16 +2898,25 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2556
2898
|
var nodeIds = msg.nodeIds || (msg.nodeId ? [msg.nodeId] : []);
|
|
2557
2899
|
var updatedCount = 0;
|
|
2558
2900
|
var updatedNodes = [];
|
|
2901
|
+
var skipWarnings = [];
|
|
2559
2902
|
|
|
2560
2903
|
for (var i = 0; i < nodeIds.length; i++) {
|
|
2561
2904
|
var node = await figma.getNodeByIdAsync(nodeIds[i]);
|
|
2562
|
-
if (node
|
|
2905
|
+
if (!node) {
|
|
2906
|
+
skipWarnings.push('node "' + nodeIds[i] + '" not found');
|
|
2907
|
+
} else if (!('fills' in node)) {
|
|
2908
|
+
skipWarnings.push('node "' + nodeIds[i] + '" (' + node.type + ') does not support fills');
|
|
2909
|
+
} else {
|
|
2563
2910
|
node.fills = [fill];
|
|
2564
2911
|
updatedCount++;
|
|
2565
2912
|
updatedNodes.push({ id: node.id, name: node.name });
|
|
2566
2913
|
}
|
|
2567
2914
|
}
|
|
2568
2915
|
|
|
2916
|
+
if (updatedCount === 0 && nodeIds.length > 0) {
|
|
2917
|
+
throw new Error('Image fill applied to 0 node(s): ' + skipWarnings.join('; '));
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2569
2920
|
console.log('🌉 [Desktop Bridge] Image fill applied to', updatedCount, 'node(s), hash:', imageHash);
|
|
2570
2921
|
|
|
2571
2922
|
figma.ui.postMessage({
|
|
@@ -2574,7 +2925,8 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2574
2925
|
success: true,
|
|
2575
2926
|
imageHash: imageHash,
|
|
2576
2927
|
updatedCount: updatedCount,
|
|
2577
|
-
nodes: updatedNodes
|
|
2928
|
+
nodes: updatedNodes,
|
|
2929
|
+
warnings: skipWarnings.length ? skipWarnings : undefined
|
|
2578
2930
|
});
|
|
2579
2931
|
|
|
2580
2932
|
} catch (error) {
|
|
@@ -2968,8 +3320,10 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2968
3320
|
if (props.name) newNode.name = props.name;
|
|
2969
3321
|
if (props.x !== undefined) newNode.x = props.x;
|
|
2970
3322
|
if (props.y !== undefined) newNode.y = props.y;
|
|
2971
|
-
if (props.width !== undefined
|
|
2972
|
-
|
|
3323
|
+
if (props.width !== undefined || props.height !== undefined) {
|
|
3324
|
+
var resizeWidth = props.width !== undefined ? props.width : newNode.width;
|
|
3325
|
+
var resizeHeight = props.height !== undefined ? props.height : newNode.height;
|
|
3326
|
+
newNode.resize(resizeWidth, resizeHeight);
|
|
2973
3327
|
}
|
|
2974
3328
|
|
|
2975
3329
|
// Apply fills if specified
|
|
@@ -3293,30 +3647,23 @@ figma.ui.onmessage = async (msg) => {
|
|
|
3293
3647
|
// VARIANT properties use just "PropertyName"
|
|
3294
3648
|
var propsToSet = {};
|
|
3295
3649
|
var propUpdates = msg.properties || {};
|
|
3650
|
+
var propWarnings = [];
|
|
3651
|
+
var hasTextProp = false;
|
|
3296
3652
|
|
|
3297
3653
|
for (var propName in propUpdates) {
|
|
3654
|
+
if (!propUpdates.hasOwnProperty(propName)) continue;
|
|
3298
3655
|
var newValue = propUpdates[propName];
|
|
3656
|
+
var resolvedName = resolvePropertyName(currentProps, propName);
|
|
3299
3657
|
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
} else {
|
|
3305
|
-
// Try to find a matching property with a suffix (for TEXT/BOOLEAN/INSTANCE_SWAP)
|
|
3306
|
-
var foundMatch = false;
|
|
3307
|
-
for (var existingProp in currentProps) {
|
|
3308
|
-
// Check if this is the base property name with a node ID suffix
|
|
3309
|
-
if (existingProp.startsWith(propName + '#')) {
|
|
3310
|
-
propsToSet[existingProp] = newValue;
|
|
3311
|
-
console.log('🌉 [Desktop Bridge] Found suffixed property:', existingProp, '=', newValue);
|
|
3312
|
-
foundMatch = true;
|
|
3313
|
-
break;
|
|
3314
|
-
}
|
|
3315
|
-
}
|
|
3316
|
-
|
|
3317
|
-
if (!foundMatch) {
|
|
3318
|
-
console.warn('🌉 [Desktop Bridge] Property not found:', propName, '- Available:', Object.keys(currentProps).join(', '));
|
|
3658
|
+
if (resolvedName !== null) {
|
|
3659
|
+
propsToSet[resolvedName] = newValue;
|
|
3660
|
+
if (currentProps[resolvedName] && currentProps[resolvedName].type === 'TEXT') {
|
|
3661
|
+
hasTextProp = true;
|
|
3319
3662
|
}
|
|
3663
|
+
console.log('🌉 [Desktop Bridge] Setting property:', resolvedName, '=', newValue);
|
|
3664
|
+
} else {
|
|
3665
|
+
console.warn('🌉 [Desktop Bridge] Property not found:', propName, '- Available:', Object.keys(currentProps).join(', '));
|
|
3666
|
+
propWarnings.push('property "' + propName + '" not found; available: ' + Object.keys(currentProps).join(', '));
|
|
3320
3667
|
}
|
|
3321
3668
|
}
|
|
3322
3669
|
|
|
@@ -3324,6 +3671,12 @@ figma.ui.onmessage = async (msg) => {
|
|
|
3324
3671
|
throw new Error('No valid properties to set. Available properties: ' + Object.keys(currentProps).join(', '));
|
|
3325
3672
|
}
|
|
3326
3673
|
|
|
3674
|
+
// Pre-load fonts before applying TEXT-property values. Mutating text
|
|
3675
|
+
// content in dynamic-page mode throws unless fonts are already loaded.
|
|
3676
|
+
if (hasTextProp) {
|
|
3677
|
+
await loadFontsForNode(node);
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3327
3680
|
// Apply the properties
|
|
3328
3681
|
node.setProperties(propsToSet);
|
|
3329
3682
|
|
|
@@ -3348,7 +3701,8 @@ figma.ui.onmessage = async (msg) => {
|
|
|
3348
3701
|
};
|
|
3349
3702
|
return acc;
|
|
3350
3703
|
}, {})
|
|
3351
|
-
}
|
|
3704
|
+
},
|
|
3705
|
+
warnings: propWarnings.length ? propWarnings : undefined
|
|
3352
3706
|
});
|
|
3353
3707
|
|
|
3354
3708
|
} catch (error) {
|