@mp3wizard/figma-console-mcp 1.17.3 → 1.19.2
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 +13 -12
- package/dist/cloudflare/core/annotation-tools.js +230 -0
- package/dist/cloudflare/core/cloud-websocket-connector.js +93 -0
- package/dist/cloudflare/core/deep-component-tools.js +128 -0
- package/dist/cloudflare/core/design-code-tools.js +65 -7
- package/dist/cloudflare/core/enrichment/enrichment-service.js +108 -12
- package/dist/cloudflare/core/figjam-tools.js +485 -0
- package/dist/cloudflare/core/figma-api.js +7 -4
- package/dist/cloudflare/core/figma-desktop-connector.js +108 -0
- package/dist/cloudflare/core/figma-tools.js +445 -55
- package/dist/cloudflare/core/port-discovery.js +88 -0
- package/dist/cloudflare/core/resolve-package-root.js +11 -0
- package/dist/cloudflare/core/slides-tools.js +607 -0
- package/dist/cloudflare/core/websocket-connector.js +93 -0
- package/dist/cloudflare/core/websocket-server.js +18 -9
- package/dist/cloudflare/index.js +164 -41
- package/dist/core/annotation-tools.d.ts +14 -0
- package/dist/core/annotation-tools.d.ts.map +1 -0
- package/dist/core/annotation-tools.js +231 -0
- package/dist/core/annotation-tools.js.map +1 -0
- package/dist/core/deep-component-tools.d.ts +14 -0
- package/dist/core/deep-component-tools.d.ts.map +1 -0
- package/dist/core/deep-component-tools.js +129 -0
- package/dist/core/deep-component-tools.js.map +1 -0
- package/dist/core/design-code-tools.d.ts.map +1 -1
- package/dist/core/design-code-tools.js +65 -7
- package/dist/core/design-code-tools.js.map +1 -1
- package/dist/core/enrichment/enrichment-service.d.ts.map +1 -1
- package/dist/core/enrichment/enrichment-service.js +108 -12
- package/dist/core/enrichment/enrichment-service.js.map +1 -1
- package/dist/core/figma-api.d.ts +1 -1
- package/dist/core/figma-api.d.ts.map +1 -1
- package/dist/core/figma-api.js +7 -4
- package/dist/core/figma-api.js.map +1 -1
- package/dist/core/figma-connector.d.ts +5 -0
- package/dist/core/figma-connector.d.ts.map +1 -1
- package/dist/core/figma-desktop-connector.d.ts +20 -0
- package/dist/core/figma-desktop-connector.d.ts.map +1 -1
- package/dist/core/figma-desktop-connector.js +83 -0
- package/dist/core/figma-desktop-connector.js.map +1 -1
- package/dist/core/figma-tools.d.ts.map +1 -1
- package/dist/core/figma-tools.js +355 -26
- package/dist/core/figma-tools.js.map +1 -1
- package/dist/core/port-discovery.d.ts +21 -0
- package/dist/core/port-discovery.d.ts.map +1 -1
- package/dist/core/port-discovery.js +88 -0
- package/dist/core/port-discovery.js.map +1 -1
- package/dist/core/resolve-package-root.d.ts +2 -0
- package/dist/core/resolve-package-root.d.ts.map +1 -0
- package/dist/core/resolve-package-root.js +12 -0
- package/dist/core/resolve-package-root.js.map +1 -0
- package/dist/core/types/design-code.d.ts +1 -0
- package/dist/core/types/design-code.d.ts.map +1 -1
- package/dist/core/websocket-connector.d.ts +5 -0
- package/dist/core/websocket-connector.d.ts.map +1 -1
- package/dist/core/websocket-connector.js +18 -0
- package/dist/core/websocket-connector.js.map +1 -1
- package/dist/core/websocket-server.d.ts.map +1 -1
- package/dist/core/websocket-server.js +7 -9
- package/dist/core/websocket-server.js.map +1 -1
- package/dist/local.d.ts +6 -0
- package/dist/local.d.ts.map +1 -1
- package/dist/local.js +58 -1
- package/dist/local.js.map +1 -1
- package/figma-desktop-bridge/code.js +906 -4
- package/figma-desktop-bridge/ui-full.html +80 -0
- package/figma-desktop-bridge/ui.html +82 -0
- package/package.json +1 -1
|
@@ -894,6 +894,566 @@ figma.ui.onmessage = async (msg) => {
|
|
|
894
894
|
}
|
|
895
895
|
}
|
|
896
896
|
|
|
897
|
+
// ============================================================================
|
|
898
|
+
// ANALYZE_COMPONENT_SET - Variant state machine + cross-variant diff
|
|
899
|
+
// For COMPONENT_SET nodes: maps variant states to CSS pseudo-classes,
|
|
900
|
+
// computes visual diffs between default and other variants, and extracts
|
|
901
|
+
// the component API surface (props, slots, conditional elements).
|
|
902
|
+
// ============================================================================
|
|
903
|
+
else if (msg.type === 'ANALYZE_COMPONENT_SET') {
|
|
904
|
+
try {
|
|
905
|
+
console.log('🌉 [Desktop Bridge] Analyzing component set: ' + msg.nodeId);
|
|
906
|
+
|
|
907
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
908
|
+
if (!node) throw new Error('Node not found: ' + msg.nodeId);
|
|
909
|
+
if (node.type !== 'COMPONENT_SET') throw new Error('Node is not a COMPONENT_SET. Type: ' + node.type);
|
|
910
|
+
|
|
911
|
+
// Build variable name lookup
|
|
912
|
+
var varNameMap = {};
|
|
913
|
+
try {
|
|
914
|
+
var allVars = await figma.variables.getLocalVariablesAsync();
|
|
915
|
+
var allColls = await figma.variables.getLocalVariableCollectionsAsync();
|
|
916
|
+
var collMap = {};
|
|
917
|
+
for (var ci = 0; ci < allColls.length; ci++) collMap[allColls[ci].id] = allColls[ci].name;
|
|
918
|
+
for (var vi = 0; vi < allVars.length; vi++) {
|
|
919
|
+
var v = allVars[vi];
|
|
920
|
+
varNameMap[v.id] = { name: v.name, collection: collMap[v.variableCollectionId] || null, type: v.resolvedType };
|
|
921
|
+
}
|
|
922
|
+
} catch(e) {}
|
|
923
|
+
|
|
924
|
+
function resolveVarId(id) {
|
|
925
|
+
return varNameMap[id] ? varNameMap[id].name : id;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function resolveBoundColor(bv) {
|
|
929
|
+
if (!bv) return null;
|
|
930
|
+
// Handle array format (fills/strokes)
|
|
931
|
+
if (Array.isArray(bv)) {
|
|
932
|
+
return bv.length > 0 && bv[0].id ? resolveVarId(bv[0].id) : null;
|
|
933
|
+
}
|
|
934
|
+
// Handle object format
|
|
935
|
+
if (bv.id) return resolveVarId(bv.id);
|
|
936
|
+
if (bv.color && bv.color.id) return resolveVarId(bv.color.id);
|
|
937
|
+
return null;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Extract visual signature from a variant for diffing
|
|
941
|
+
function extractSignature(variant) {
|
|
942
|
+
var sig = {};
|
|
943
|
+
|
|
944
|
+
// Find the main interactive element — prioritize by: has strokes (input fields),
|
|
945
|
+
// is a FRAME with fills and strokes, or is the largest visible non-text child
|
|
946
|
+
var mainChild = null;
|
|
947
|
+
if (variant.children) {
|
|
948
|
+
// First pass: find child with strokes (likely the input/interactive element)
|
|
949
|
+
for (var i = 0; i < variant.children.length; i++) {
|
|
950
|
+
var child = variant.children[i];
|
|
951
|
+
try {
|
|
952
|
+
if (child.visible !== false && child.type !== 'TEXT' && child.strokes && child.strokes.length > 0) {
|
|
953
|
+
mainChild = child;
|
|
954
|
+
break;
|
|
955
|
+
}
|
|
956
|
+
} catch(e) {}
|
|
957
|
+
}
|
|
958
|
+
// Fallback: first visible FRAME child
|
|
959
|
+
if (!mainChild) {
|
|
960
|
+
for (var i2 = 0; i2 < variant.children.length; i2++) {
|
|
961
|
+
var c2 = variant.children[i2];
|
|
962
|
+
try {
|
|
963
|
+
if (c2.visible !== false && c2.type === 'FRAME') {
|
|
964
|
+
mainChild = c2;
|
|
965
|
+
break;
|
|
966
|
+
}
|
|
967
|
+
} catch(e) {}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
if (mainChild) {
|
|
973
|
+
var child = mainChild;
|
|
974
|
+
// This is the main interactive frame
|
|
975
|
+
try {
|
|
976
|
+
var bv = child.boundVariables || {};
|
|
977
|
+
sig.fillToken = resolveBoundColor(bv.fills);
|
|
978
|
+
sig.strokeToken = resolveBoundColor(bv.strokes);
|
|
979
|
+
sig.strokeWeight = child.strokeWeight;
|
|
980
|
+
|
|
981
|
+
// Raw colors as fallback
|
|
982
|
+
if (!sig.fillToken && child.fills && child.fills.length > 0 && child.fills[0].color) {
|
|
983
|
+
var fc = child.fills[0].color;
|
|
984
|
+
sig.fillHex = '#' + Math.round(fc.r*255).toString(16).padStart(2,'0') + Math.round(fc.g*255).toString(16).padStart(2,'0') + Math.round(fc.b*255).toString(16).padStart(2,'0');
|
|
985
|
+
}
|
|
986
|
+
if (!sig.strokeToken && child.strokes && child.strokes.length > 0 && child.strokes[0].color) {
|
|
987
|
+
var sc = child.strokes[0].color;
|
|
988
|
+
sig.strokeHex = '#' + Math.round(sc.r*255).toString(16).padStart(2,'0') + Math.round(sc.g*255).toString(16).padStart(2,'0') + Math.round(sc.b*255).toString(16).padStart(2,'0');
|
|
989
|
+
}
|
|
990
|
+
sig.effects = child.effects && child.effects.length > 0 ? child.effects : null;
|
|
991
|
+
sig.opacity = child.opacity < 1 ? child.opacity : null;
|
|
992
|
+
} catch(e) {}
|
|
993
|
+
|
|
994
|
+
// Check text children for color changes
|
|
995
|
+
if (child.children) {
|
|
996
|
+
for (var t = 0; t < child.children.length; t++) {
|
|
997
|
+
var textChild = child.children[t];
|
|
998
|
+
if (textChild.type === 'TEXT') {
|
|
999
|
+
try {
|
|
1000
|
+
var tbv = textChild.boundVariables || {};
|
|
1001
|
+
sig.textToken = resolveBoundColor(tbv.fills);
|
|
1002
|
+
if (!sig.textToken && textChild.fills && textChild.fills.length > 0 && textChild.fills[0].color) {
|
|
1003
|
+
var tc = textChild.fills[0].color;
|
|
1004
|
+
sig.textHex = '#' + Math.round(tc.r*255).toString(16).padStart(2,'0') + Math.round(tc.g*255).toString(16).padStart(2,'0') + Math.round(tc.b*255).toString(16).padStart(2,'0');
|
|
1005
|
+
}
|
|
1006
|
+
} catch(e) {}
|
|
1007
|
+
break; // First text node is enough
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// Check element visibility changes
|
|
1014
|
+
sig.visibilityChanges = {};
|
|
1015
|
+
if (variant.children) {
|
|
1016
|
+
for (var j = 0; j < variant.children.length; j++) {
|
|
1017
|
+
var ch = variant.children[j];
|
|
1018
|
+
try {
|
|
1019
|
+
if (!ch.visible) sig.visibilityChanges[ch.name] = false;
|
|
1020
|
+
} catch(e) {}
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
return sig;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Parse variant property definitions
|
|
1028
|
+
var propDefs = node.componentPropertyDefinitions || {};
|
|
1029
|
+
var variantAxes = {};
|
|
1030
|
+
var componentProps = {};
|
|
1031
|
+
var propKeys = Object.keys(propDefs);
|
|
1032
|
+
for (var pk = 0; pk < propKeys.length; pk++) {
|
|
1033
|
+
var propKey = propKeys[pk];
|
|
1034
|
+
var propDef = propDefs[propKey];
|
|
1035
|
+
if (propDef.type === 'VARIANT') {
|
|
1036
|
+
variantAxes[propKey] = propDef.variantOptions || [];
|
|
1037
|
+
} else {
|
|
1038
|
+
componentProps[propKey] = {
|
|
1039
|
+
type: propDef.type,
|
|
1040
|
+
defaultValue: propDef.defaultValue
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
// CSS pseudo-class mapping for state variants
|
|
1046
|
+
var stateMapping = {
|
|
1047
|
+
'default': null,
|
|
1048
|
+
'hover': ':hover',
|
|
1049
|
+
'focus': ':focus-visible',
|
|
1050
|
+
'focus-visible': ':focus-visible',
|
|
1051
|
+
'focused': ':focus-visible',
|
|
1052
|
+
'active': ':active',
|
|
1053
|
+
'pressed': ':active',
|
|
1054
|
+
'disabled': ':disabled, [aria-disabled="true"]',
|
|
1055
|
+
'error': '[aria-invalid="true"]',
|
|
1056
|
+
'invalid': '[aria-invalid="true"]',
|
|
1057
|
+
'filled': '.has-value',
|
|
1058
|
+
'selected': '[aria-selected="true"]',
|
|
1059
|
+
'checked': ':checked',
|
|
1060
|
+
'loading': '[aria-busy="true"]',
|
|
1061
|
+
'readonly': '[readonly]',
|
|
1062
|
+
'open': '[aria-expanded="true"]',
|
|
1063
|
+
'closed': '[aria-expanded="false"]'
|
|
1064
|
+
};
|
|
1065
|
+
|
|
1066
|
+
// Analyze variants grouped by axes
|
|
1067
|
+
var variants = node.children || [];
|
|
1068
|
+
var defaultVariant = null;
|
|
1069
|
+
var stateAxis = null;
|
|
1070
|
+
var sizeAxis = null;
|
|
1071
|
+
|
|
1072
|
+
// Detect which axis is "state" and which is "size"
|
|
1073
|
+
var axisKeys = Object.keys(variantAxes);
|
|
1074
|
+
for (var ak = 0; ak < axisKeys.length; ak++) {
|
|
1075
|
+
var axisName = axisKeys[ak].toLowerCase();
|
|
1076
|
+
var axisValues = variantAxes[axisKeys[ak]];
|
|
1077
|
+
if (axisName === 'state' || axisName === 'status' || axisName === 'interaction') {
|
|
1078
|
+
stateAxis = axisKeys[ak];
|
|
1079
|
+
} else if (axisName === 'size' || axisName === 'scale') {
|
|
1080
|
+
sizeAxis = axisKeys[ak];
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// Build state machine from variants
|
|
1085
|
+
var stateMachine = { states: {}, defaultState: null, cssMapping: {} };
|
|
1086
|
+
var defaultSig = null;
|
|
1087
|
+
|
|
1088
|
+
// Find the default variant (first size, default state)
|
|
1089
|
+
for (var di = 0; di < variants.length; di++) {
|
|
1090
|
+
var vName = variants[di].name;
|
|
1091
|
+
if (vName.indexOf('state=default') !== -1 && (sizeAxis ? vName.indexOf(sizeAxis + '=') !== -1 : true)) {
|
|
1092
|
+
// Pick the first default state variant
|
|
1093
|
+
if (!defaultVariant || vName.indexOf('large') !== -1) {
|
|
1094
|
+
defaultVariant = variants[di];
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (defaultVariant) {
|
|
1099
|
+
defaultSig = extractSignature(defaultVariant);
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
// Process each variant and compute diff from default
|
|
1103
|
+
var variantDiffs = [];
|
|
1104
|
+
for (var vdi = 0; vdi < variants.length; vdi++) {
|
|
1105
|
+
var variant = variants[vdi];
|
|
1106
|
+
var sig = extractSignature(variant);
|
|
1107
|
+
|
|
1108
|
+
// Parse variant name to extract axis values
|
|
1109
|
+
var axisParts = variant.name.split(', ');
|
|
1110
|
+
var axisValues = {};
|
|
1111
|
+
for (var ap = 0; ap < axisParts.length; ap++) {
|
|
1112
|
+
var parts = axisParts[ap].split('=');
|
|
1113
|
+
if (parts.length === 2) axisValues[parts[0].trim()] = parts[1].trim();
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
var stateValue = stateAxis ? (axisValues[stateAxis] || 'default') : 'default';
|
|
1117
|
+
var cssSelector = stateMapping[stateValue.toLowerCase()] || null;
|
|
1118
|
+
|
|
1119
|
+
// Compute diff from default
|
|
1120
|
+
var diff = {};
|
|
1121
|
+
if (defaultSig && variant.id !== (defaultVariant ? defaultVariant.id : null)) {
|
|
1122
|
+
if (sig.fillToken !== defaultSig.fillToken) diff.fillToken = sig.fillToken || sig.fillHex;
|
|
1123
|
+
if (sig.strokeToken !== defaultSig.strokeToken) diff.strokeToken = sig.strokeToken || sig.strokeHex;
|
|
1124
|
+
if (sig.strokeWeight !== defaultSig.strokeWeight) diff.strokeWeight = sig.strokeWeight;
|
|
1125
|
+
if (sig.textToken !== defaultSig.textToken) diff.textToken = sig.textToken || sig.textHex;
|
|
1126
|
+
if (sig.opacity !== defaultSig.opacity) diff.opacity = sig.opacity;
|
|
1127
|
+
if (JSON.stringify(sig.effects) !== JSON.stringify(defaultSig.effects)) diff.effects = sig.effects;
|
|
1128
|
+
// Visibility changes not in default
|
|
1129
|
+
var dvKeys = Object.keys(defaultSig.visibilityChanges);
|
|
1130
|
+
var svKeys = Object.keys(sig.visibilityChanges);
|
|
1131
|
+
for (var sk = 0; sk < svKeys.length; sk++) {
|
|
1132
|
+
if (!defaultSig.visibilityChanges[svKeys[sk]]) {
|
|
1133
|
+
if (!diff.visibilityChanges) diff.visibilityChanges = {};
|
|
1134
|
+
diff.visibilityChanges[svKeys[sk]] = sig.visibilityChanges[svKeys[sk]];
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
variantDiffs.push({
|
|
1140
|
+
name: variant.name,
|
|
1141
|
+
id: variant.id,
|
|
1142
|
+
axes: axisValues,
|
|
1143
|
+
state: stateValue,
|
|
1144
|
+
cssSelector: cssSelector,
|
|
1145
|
+
diffFromDefault: Object.keys(diff).length > 0 ? diff : null,
|
|
1146
|
+
signature: sig
|
|
1147
|
+
});
|
|
1148
|
+
|
|
1149
|
+
// Build CSS mapping
|
|
1150
|
+
if (cssSelector) {
|
|
1151
|
+
stateMachine.cssMapping[stateValue] = cssSelector;
|
|
1152
|
+
}
|
|
1153
|
+
if (!stateMachine.states[stateValue]) {
|
|
1154
|
+
stateMachine.states[stateValue] = [];
|
|
1155
|
+
}
|
|
1156
|
+
stateMachine.states[stateValue].push(variant.id);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
if (defaultVariant) {
|
|
1160
|
+
stateMachine.defaultState = 'default';
|
|
1161
|
+
stateMachine.defaultSignature = defaultSig;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
var result = {
|
|
1165
|
+
nodeId: node.id,
|
|
1166
|
+
nodeName: node.name,
|
|
1167
|
+
variantCount: variants.length,
|
|
1168
|
+
variantAxes: variantAxes,
|
|
1169
|
+
componentProps: componentProps,
|
|
1170
|
+
stateMachine: stateMachine,
|
|
1171
|
+
variants: variantDiffs,
|
|
1172
|
+
ai_instruction: 'Use cssMapping to implement interaction states. diffFromDefault shows only what changes per state — apply these as CSS pseudo-class or attribute overrides. componentProps maps to React/Vue component props (BOOLEAN → boolean prop, TEXT → string prop, INSTANCE_SWAP → ReactNode/slot prop).'
|
|
1173
|
+
};
|
|
1174
|
+
|
|
1175
|
+
console.log('🌉 [Desktop Bridge] Component set analysis complete. ' + variants.length + ' variants, ' + Object.keys(stateMachine.cssMapping).length + ' CSS mappings');
|
|
1176
|
+
|
|
1177
|
+
figma.ui.postMessage({
|
|
1178
|
+
type: 'ANALYZE_COMPONENT_SET_RESULT',
|
|
1179
|
+
requestId: msg.requestId,
|
|
1180
|
+
success: true,
|
|
1181
|
+
data: result
|
|
1182
|
+
});
|
|
1183
|
+
|
|
1184
|
+
} catch (error) {
|
|
1185
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1186
|
+
console.error('🌉 [Desktop Bridge] Analyze component set error:', errorMsg);
|
|
1187
|
+
figma.ui.postMessage({
|
|
1188
|
+
type: 'ANALYZE_COMPONENT_SET_RESULT',
|
|
1189
|
+
requestId: msg.requestId,
|
|
1190
|
+
success: false,
|
|
1191
|
+
error: errorMsg
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// ============================================================================
|
|
1197
|
+
// DEEP_GET_COMPONENT - Full recursive component extraction for code generation
|
|
1198
|
+
// Extracts visual properties, boundVariables, reactions, instance references,
|
|
1199
|
+
// and annotations at every level of the component tree.
|
|
1200
|
+
// ============================================================================
|
|
1201
|
+
else if (msg.type === 'DEEP_GET_COMPONENT') {
|
|
1202
|
+
try {
|
|
1203
|
+
var maxDepth = msg.depth || 10;
|
|
1204
|
+
console.log('🌉 [Desktop Bridge] Deep component fetch: ' + msg.nodeId + ' (depth: ' + maxDepth + ')');
|
|
1205
|
+
|
|
1206
|
+
var rootNode = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1207
|
+
if (!rootNode) {
|
|
1208
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
// Build a variable name lookup map for resolving boundVariables
|
|
1212
|
+
var varNameMap = {};
|
|
1213
|
+
try {
|
|
1214
|
+
var allVars = await figma.variables.getLocalVariablesAsync();
|
|
1215
|
+
var allCollections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
1216
|
+
var collectionMap = {};
|
|
1217
|
+
for (var ci = 0; ci < allCollections.length; ci++) {
|
|
1218
|
+
collectionMap[allCollections[ci].id] = allCollections[ci].name;
|
|
1219
|
+
}
|
|
1220
|
+
for (var vi = 0; vi < allVars.length; vi++) {
|
|
1221
|
+
var v = allVars[vi];
|
|
1222
|
+
varNameMap[v.id] = {
|
|
1223
|
+
name: v.name,
|
|
1224
|
+
resolvedType: v.resolvedType,
|
|
1225
|
+
collection: collectionMap[v.variableCollectionId] || null,
|
|
1226
|
+
scopes: v.scopes || [],
|
|
1227
|
+
codeSyntax: v.codeSyntax || {}
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
} catch (e) {
|
|
1231
|
+
console.log('🌉 [Desktop Bridge] Could not build variable map: ' + e.message);
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
// Resolve boundVariables to token names
|
|
1235
|
+
function resolveBoundVars(bv) {
|
|
1236
|
+
if (!bv) return null;
|
|
1237
|
+
var resolved = {};
|
|
1238
|
+
var keys = Object.keys(bv);
|
|
1239
|
+
for (var k = 0; k < keys.length; k++) {
|
|
1240
|
+
var prop = keys[k];
|
|
1241
|
+
var binding = bv[prop];
|
|
1242
|
+
if (Array.isArray(binding)) {
|
|
1243
|
+
resolved[prop] = [];
|
|
1244
|
+
for (var bi = 0; bi < binding.length; bi++) {
|
|
1245
|
+
var b = binding[bi];
|
|
1246
|
+
if (b && b.id) {
|
|
1247
|
+
var info = varNameMap[b.id];
|
|
1248
|
+
resolved[prop].push(info ? { id: b.id, name: info.name, collection: info.collection, resolvedType: info.resolvedType, codeSyntax: info.codeSyntax } : { id: b.id });
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
} else if (binding && binding.id) {
|
|
1252
|
+
var info = varNameMap[binding.id];
|
|
1253
|
+
resolved[prop] = info ? { id: binding.id, name: info.name, collection: info.collection, resolvedType: info.resolvedType, codeSyntax: info.codeSyntax } : { id: binding.id };
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return Object.keys(resolved).length > 0 ? resolved : null;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Extract visual properties from a node
|
|
1260
|
+
function extractNodeProps(n) {
|
|
1261
|
+
var props = {};
|
|
1262
|
+
|
|
1263
|
+
// Layout
|
|
1264
|
+
if (n.layoutMode) props.layoutMode = n.layoutMode;
|
|
1265
|
+
if (n.primaryAxisSizingMode) props.primaryAxisSizingMode = n.primaryAxisSizingMode;
|
|
1266
|
+
if (n.counterAxisSizingMode) props.counterAxisSizingMode = n.counterAxisSizingMode;
|
|
1267
|
+
if (n.layoutSizingHorizontal) props.layoutSizingHorizontal = n.layoutSizingHorizontal;
|
|
1268
|
+
if (n.layoutSizingVertical) props.layoutSizingVertical = n.layoutSizingVertical;
|
|
1269
|
+
if (n.primaryAxisAlignItems) props.primaryAxisAlignItems = n.primaryAxisAlignItems;
|
|
1270
|
+
if (n.counterAxisAlignItems) props.counterAxisAlignItems = n.counterAxisAlignItems;
|
|
1271
|
+
if (n.paddingLeft !== undefined && n.paddingLeft !== 0) props.paddingLeft = n.paddingLeft;
|
|
1272
|
+
if (n.paddingRight !== undefined && n.paddingRight !== 0) props.paddingRight = n.paddingRight;
|
|
1273
|
+
if (n.paddingTop !== undefined && n.paddingTop !== 0) props.paddingTop = n.paddingTop;
|
|
1274
|
+
if (n.paddingBottom !== undefined && n.paddingBottom !== 0) props.paddingBottom = n.paddingBottom;
|
|
1275
|
+
if (n.itemSpacing !== undefined && n.itemSpacing !== 0) props.itemSpacing = n.itemSpacing;
|
|
1276
|
+
if (n.counterAxisSpacing !== undefined && n.counterAxisSpacing !== 0) props.counterAxisSpacing = n.counterAxisSpacing;
|
|
1277
|
+
if (n.layoutWrap && n.layoutWrap !== 'NO_WRAP') props.layoutWrap = n.layoutWrap;
|
|
1278
|
+
if (n.minWidth !== undefined) props.minWidth = n.minWidth;
|
|
1279
|
+
if (n.maxWidth !== undefined) props.maxWidth = n.maxWidth;
|
|
1280
|
+
if (n.minHeight !== undefined) props.minHeight = n.minHeight;
|
|
1281
|
+
if (n.maxHeight !== undefined) props.maxHeight = n.maxHeight;
|
|
1282
|
+
if (n.clipsContent) props.clipsContent = true;
|
|
1283
|
+
|
|
1284
|
+
// Visual
|
|
1285
|
+
try {
|
|
1286
|
+
if (n.fills && n.fills !== figma.mixed && n.fills.length > 0) props.fills = n.fills;
|
|
1287
|
+
} catch (e) { /* mixed fills */ }
|
|
1288
|
+
try {
|
|
1289
|
+
if (n.strokes && n.strokes.length > 0) props.strokes = n.strokes;
|
|
1290
|
+
} catch (e) {}
|
|
1291
|
+
if (n.strokeWeight !== undefined && n.strokeWeight !== 0 && n.strokeWeight !== figma.mixed) props.strokeWeight = n.strokeWeight;
|
|
1292
|
+
if (n.cornerRadius !== undefined && n.cornerRadius !== 0 && n.cornerRadius !== figma.mixed) props.cornerRadius = n.cornerRadius;
|
|
1293
|
+
try {
|
|
1294
|
+
if (n.effects && n.effects.length > 0) props.effects = n.effects;
|
|
1295
|
+
} catch (e) {}
|
|
1296
|
+
if (n.opacity !== undefined && n.opacity < 1) props.opacity = n.opacity;
|
|
1297
|
+
|
|
1298
|
+
// Typography
|
|
1299
|
+
if (n.type === 'TEXT') {
|
|
1300
|
+
try { props.characters = n.characters; } catch (e) {}
|
|
1301
|
+
try { if (n.fontSize !== figma.mixed) props.fontSize = n.fontSize; } catch (e) {}
|
|
1302
|
+
try { if (n.fontName !== figma.mixed) props.fontFamily = n.fontName.family; props.fontStyle = n.fontName.style; } catch (e) {}
|
|
1303
|
+
try { if (n.fontWeight !== figma.mixed) props.fontWeight = n.fontWeight; } catch (e) {}
|
|
1304
|
+
try { if (n.lineHeight !== figma.mixed) props.lineHeight = n.lineHeight; } catch (e) {}
|
|
1305
|
+
try { if (n.letterSpacing !== figma.mixed) props.letterSpacing = n.letterSpacing; } catch (e) {}
|
|
1306
|
+
try { if (n.textAlignHorizontal) props.textAlignHorizontal = n.textAlignHorizontal; } catch (e) {}
|
|
1307
|
+
try { if (n.textAlignVertical) props.textAlignVertical = n.textAlignVertical; } catch (e) {}
|
|
1308
|
+
try { if (n.textAutoResize && n.textAutoResize !== 'NONE') props.textAutoResize = n.textAutoResize; } catch (e) {}
|
|
1309
|
+
try { if (n.textTruncation && n.textTruncation !== 'DISABLED') props.textTruncation = n.textTruncation; } catch (e) {}
|
|
1310
|
+
try { if (n.textCase && n.textCase !== 'ORIGINAL') props.textCase = n.textCase; } catch (e) {}
|
|
1311
|
+
try { if (n.textDecoration && n.textDecoration !== 'NONE') props.textDecoration = n.textDecoration; } catch (e) {}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Design tokens (resolved to names)
|
|
1315
|
+
try {
|
|
1316
|
+
var resolved = resolveBoundVars(n.boundVariables);
|
|
1317
|
+
if (resolved) props.boundVariables = resolved;
|
|
1318
|
+
} catch (e) {}
|
|
1319
|
+
|
|
1320
|
+
// Prototype interactions
|
|
1321
|
+
try {
|
|
1322
|
+
if (n.reactions && n.reactions.length > 0) {
|
|
1323
|
+
props.reactions = n.reactions.map(function(r) {
|
|
1324
|
+
var reaction = { trigger: r.trigger };
|
|
1325
|
+
if (r.action) {
|
|
1326
|
+
reaction.action = { type: r.action.type };
|
|
1327
|
+
if (r.action.navigation) reaction.action.navigation = r.action.navigation;
|
|
1328
|
+
if (r.action.transition) reaction.action.transition = r.action.transition;
|
|
1329
|
+
if (r.action.destinationId) reaction.action.destinationId = r.action.destinationId;
|
|
1330
|
+
}
|
|
1331
|
+
return reaction;
|
|
1332
|
+
});
|
|
1333
|
+
}
|
|
1334
|
+
} catch (e) {}
|
|
1335
|
+
|
|
1336
|
+
// Annotations
|
|
1337
|
+
try {
|
|
1338
|
+
if (n.annotations && n.annotations.length > 0) {
|
|
1339
|
+
props.annotations = n.annotations.map(function(a) {
|
|
1340
|
+
var ann = {};
|
|
1341
|
+
if (a.labelMarkdown) ann.labelMarkdown = a.labelMarkdown;
|
|
1342
|
+
else if (a.label) ann.label = a.label;
|
|
1343
|
+
if (a.properties) ann.properties = a.properties;
|
|
1344
|
+
if (a.categoryId) ann.categoryId = a.categoryId;
|
|
1345
|
+
return ann;
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
} catch (e) {}
|
|
1349
|
+
|
|
1350
|
+
// Component instance reference
|
|
1351
|
+
if (n.type === 'INSTANCE') {
|
|
1352
|
+
try {
|
|
1353
|
+
if (n.mainComponent) {
|
|
1354
|
+
props.mainComponent = {
|
|
1355
|
+
id: n.mainComponent.id,
|
|
1356
|
+
name: n.mainComponent.name,
|
|
1357
|
+
key: n.mainComponent.key || null,
|
|
1358
|
+
isVariant: n.mainComponent.parent && n.mainComponent.parent.type === 'COMPONENT_SET'
|
|
1359
|
+
};
|
|
1360
|
+
if (props.mainComponent.isVariant && n.mainComponent.parent) {
|
|
1361
|
+
props.mainComponent.componentSetName = n.mainComponent.parent.name;
|
|
1362
|
+
props.mainComponent.componentSetId = n.mainComponent.parent.id;
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
} catch (e) {}
|
|
1366
|
+
try {
|
|
1367
|
+
if (n.componentProperties) props.componentProperties = n.componentProperties;
|
|
1368
|
+
} catch (e) {}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
// Component definitions (for COMPONENT and COMPONENT_SET)
|
|
1372
|
+
if (n.type === 'COMPONENT_SET' || n.type === 'COMPONENT') {
|
|
1373
|
+
try {
|
|
1374
|
+
if (n.componentPropertyDefinitions) props.componentPropertyDefinitions = n.componentPropertyDefinitions;
|
|
1375
|
+
} catch (e) {}
|
|
1376
|
+
if (n.type === 'COMPONENT' && n.variantProperties) {
|
|
1377
|
+
props.variantProperties = n.variantProperties;
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
// Dimensions
|
|
1382
|
+
try {
|
|
1383
|
+
props.width = Math.round(n.width);
|
|
1384
|
+
props.height = Math.round(n.height);
|
|
1385
|
+
} catch (e) {}
|
|
1386
|
+
|
|
1387
|
+
return props;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
// Recursive tree walker
|
|
1391
|
+
function walkNode(n, currentDepth) {
|
|
1392
|
+
var nodeData = {
|
|
1393
|
+
id: n.id,
|
|
1394
|
+
name: n.name,
|
|
1395
|
+
type: n.type,
|
|
1396
|
+
visible: n.visible
|
|
1397
|
+
};
|
|
1398
|
+
|
|
1399
|
+
// Skip invisible nodes (unless they're component set variants)
|
|
1400
|
+
if (!n.visible && n.type !== 'COMPONENT') {
|
|
1401
|
+
nodeData._hidden = true;
|
|
1402
|
+
return nodeData;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Extract all properties
|
|
1406
|
+
var props = extractNodeProps(n);
|
|
1407
|
+
var propKeys = Object.keys(props);
|
|
1408
|
+
for (var pk = 0; pk < propKeys.length; pk++) {
|
|
1409
|
+
nodeData[propKeys[pk]] = props[propKeys[pk]];
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// Recurse into children
|
|
1413
|
+
if (n.children && currentDepth < maxDepth) {
|
|
1414
|
+
nodeData.children = [];
|
|
1415
|
+
for (var i = 0; i < n.children.length; i++) {
|
|
1416
|
+
try {
|
|
1417
|
+
nodeData.children.push(walkNode(n.children[i], currentDepth + 1));
|
|
1418
|
+
} catch (e) {
|
|
1419
|
+
// Skip inaccessible slot sublayers
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
} else if (n.children) {
|
|
1423
|
+
// At max depth, include lightweight child summary
|
|
1424
|
+
nodeData.childCount = n.children.length;
|
|
1425
|
+
nodeData._depthLimitReached = true;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
return nodeData;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
var result = walkNode(rootNode, 0);
|
|
1432
|
+
result._variableMapSize = Object.keys(varNameMap).length;
|
|
1433
|
+
result._maxDepthUsed = maxDepth;
|
|
1434
|
+
|
|
1435
|
+
var resultJson = JSON.stringify(result);
|
|
1436
|
+
console.log('🌉 [Desktop Bridge] Deep component data: ' + Math.round(resultJson.length / 1024) + 'KB, vars resolved: ' + Object.keys(varNameMap).length);
|
|
1437
|
+
|
|
1438
|
+
figma.ui.postMessage({
|
|
1439
|
+
type: 'DEEP_GET_COMPONENT_RESULT',
|
|
1440
|
+
requestId: msg.requestId,
|
|
1441
|
+
success: true,
|
|
1442
|
+
data: result
|
|
1443
|
+
});
|
|
1444
|
+
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
1447
|
+
console.error('🌉 [Desktop Bridge] Deep component error:', errorMsg);
|
|
1448
|
+
figma.ui.postMessage({
|
|
1449
|
+
type: 'DEEP_GET_COMPONENT_RESULT',
|
|
1450
|
+
requestId: msg.requestId,
|
|
1451
|
+
success: false,
|
|
1452
|
+
error: errorMsg
|
|
1453
|
+
});
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
897
1457
|
// ============================================================================
|
|
898
1458
|
// GET_LOCAL_COMPONENTS - Get all local components for design system manifest
|
|
899
1459
|
// ============================================================================
|
|
@@ -1342,6 +1902,249 @@ figma.ui.onmessage = async (msg) => {
|
|
|
1342
1902
|
}
|
|
1343
1903
|
}
|
|
1344
1904
|
|
|
1905
|
+
// ============================================================================
|
|
1906
|
+
// GET_ANNOTATIONS - Read annotations from a node (and optionally children)
|
|
1907
|
+
// ============================================================================
|
|
1908
|
+
else if (msg.type === 'GET_ANNOTATIONS') {
|
|
1909
|
+
try {
|
|
1910
|
+
console.log('🌉 [Desktop Bridge] Getting annotations for node:', msg.nodeId);
|
|
1911
|
+
|
|
1912
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
1913
|
+
if (!node) {
|
|
1914
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
1915
|
+
}
|
|
1916
|
+
|
|
1917
|
+
// Get annotation categories for name resolution
|
|
1918
|
+
var categories = [];
|
|
1919
|
+
try {
|
|
1920
|
+
categories = await figma.annotations.getAnnotationCategoriesAsync();
|
|
1921
|
+
} catch (e) {
|
|
1922
|
+
console.log('🌉 [Desktop Bridge] Could not fetch annotation categories:', e.message);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// Build category lookup map
|
|
1926
|
+
var categoryMap = {};
|
|
1927
|
+
for (var ci = 0; ci < categories.length; ci++) {
|
|
1928
|
+
categoryMap[categories[ci].id] = categories[ci].name;
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
// Helper to extract annotations from a single node
|
|
1932
|
+
function extractAnnotations(n) {
|
|
1933
|
+
var anns = n.annotations || [];
|
|
1934
|
+
var result = [];
|
|
1935
|
+
for (var ai = 0; ai < anns.length; ai++) {
|
|
1936
|
+
var ann = anns[ai];
|
|
1937
|
+
var props = [];
|
|
1938
|
+
if (ann.properties) {
|
|
1939
|
+
for (var pi = 0; pi < ann.properties.length; pi++) {
|
|
1940
|
+
props.push({ type: ann.properties[pi].type });
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
result.push({
|
|
1944
|
+
label: ann.label || null,
|
|
1945
|
+
labelMarkdown: ann.labelMarkdown || null,
|
|
1946
|
+
properties: props.length > 0 ? props : null,
|
|
1947
|
+
categoryId: ann.categoryId || null,
|
|
1948
|
+
categoryName: ann.categoryId && categoryMap[ann.categoryId] ? categoryMap[ann.categoryId] : null
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
return result;
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
// Collect annotations from the node itself
|
|
1955
|
+
var nodeAnnotations = extractAnnotations(node);
|
|
1956
|
+
var childAnnotations = [];
|
|
1957
|
+
|
|
1958
|
+
// Optionally walk children
|
|
1959
|
+
var includeChildren = msg.includeChildren || false;
|
|
1960
|
+
var maxDepth = msg.depth || 1;
|
|
1961
|
+
|
|
1962
|
+
if (includeChildren && 'children' in node && node.children) {
|
|
1963
|
+
function walkChildren(parent, currentDepth) {
|
|
1964
|
+
if (currentDepth > maxDepth) return;
|
|
1965
|
+
for (var i = 0; i < parent.children.length; i++) {
|
|
1966
|
+
var child = parent.children[i];
|
|
1967
|
+
try {
|
|
1968
|
+
var anns = extractAnnotations(child);
|
|
1969
|
+
if (anns.length > 0) {
|
|
1970
|
+
childAnnotations.push({
|
|
1971
|
+
nodeId: child.id,
|
|
1972
|
+
nodeName: child.name,
|
|
1973
|
+
nodeType: child.type,
|
|
1974
|
+
annotations: anns
|
|
1975
|
+
});
|
|
1976
|
+
}
|
|
1977
|
+
if ('children' in child && child.children) {
|
|
1978
|
+
walkChildren(child, currentDepth + 1);
|
|
1979
|
+
}
|
|
1980
|
+
} catch (e) {
|
|
1981
|
+
// Skip inaccessible children (slot sublayers, etc.)
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
walkChildren(node, 1);
|
|
1986
|
+
}
|
|
1987
|
+
|
|
1988
|
+
var result = {
|
|
1989
|
+
nodeId: node.id,
|
|
1990
|
+
nodeName: node.name,
|
|
1991
|
+
nodeType: node.type,
|
|
1992
|
+
annotations: nodeAnnotations,
|
|
1993
|
+
annotationCount: nodeAnnotations.length,
|
|
1994
|
+
children: includeChildren ? childAnnotations : undefined,
|
|
1995
|
+
childAnnotationCount: includeChildren ? childAnnotations.reduce(function(sum, c) { return sum + c.annotations.length; }, 0) : undefined,
|
|
1996
|
+
availableCategories: categories.map(function(c) { return { id: c.id, name: c.name }; })
|
|
1997
|
+
};
|
|
1998
|
+
|
|
1999
|
+
console.log('🌉 [Desktop Bridge] Annotations retrieved. Node: ' + nodeAnnotations.length + ', Children: ' + (childAnnotations.length || 0));
|
|
2000
|
+
|
|
2001
|
+
figma.ui.postMessage({
|
|
2002
|
+
type: 'GET_ANNOTATIONS_RESULT',
|
|
2003
|
+
requestId: msg.requestId,
|
|
2004
|
+
success: true,
|
|
2005
|
+
data: result
|
|
2006
|
+
});
|
|
2007
|
+
|
|
2008
|
+
} catch (error) {
|
|
2009
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
2010
|
+
console.error('🌉 [Desktop Bridge] Get annotations error:', errorMsg);
|
|
2011
|
+
figma.ui.postMessage({
|
|
2012
|
+
type: 'GET_ANNOTATIONS_RESULT',
|
|
2013
|
+
requestId: msg.requestId,
|
|
2014
|
+
success: false,
|
|
2015
|
+
error: errorMsg
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
// ============================================================================
|
|
2021
|
+
// SET_ANNOTATIONS - Write annotations to a node
|
|
2022
|
+
// ============================================================================
|
|
2023
|
+
else if (msg.type === 'SET_ANNOTATIONS') {
|
|
2024
|
+
try {
|
|
2025
|
+
console.log('🌉 [Desktop Bridge] Setting annotations on node:', msg.nodeId);
|
|
2026
|
+
|
|
2027
|
+
var node = await figma.getNodeByIdAsync(msg.nodeId);
|
|
2028
|
+
if (!node) {
|
|
2029
|
+
throw new Error('Node not found: ' + msg.nodeId);
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
// Verify node supports annotations
|
|
2033
|
+
if (!('annotations' in node)) {
|
|
2034
|
+
throw new Error('Node type ' + node.type + ' does not support annotations');
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
// Build the annotations array
|
|
2038
|
+
var newAnnotations = [];
|
|
2039
|
+
var inputAnnotations = msg.annotations || [];
|
|
2040
|
+
|
|
2041
|
+
for (var i = 0; i < inputAnnotations.length; i++) {
|
|
2042
|
+
var input = inputAnnotations[i];
|
|
2043
|
+
var ann = {};
|
|
2044
|
+
|
|
2045
|
+
if (input.label) {
|
|
2046
|
+
ann.label = input.label;
|
|
2047
|
+
}
|
|
2048
|
+
if (input.labelMarkdown) {
|
|
2049
|
+
ann.labelMarkdown = input.labelMarkdown;
|
|
2050
|
+
}
|
|
2051
|
+
if (input.properties && input.properties.length > 0) {
|
|
2052
|
+
ann.properties = [];
|
|
2053
|
+
for (var p = 0; p < input.properties.length; p++) {
|
|
2054
|
+
ann.properties.push({ type: input.properties[p].type });
|
|
2055
|
+
}
|
|
2056
|
+
}
|
|
2057
|
+
if (input.categoryId) {
|
|
2058
|
+
ann.categoryId = input.categoryId;
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
newAnnotations.push(ann);
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
// Optionally append to existing annotations instead of replacing
|
|
2065
|
+
if (msg.mode === 'append') {
|
|
2066
|
+
var existing = node.annotations || [];
|
|
2067
|
+
var merged = [];
|
|
2068
|
+
for (var e = 0; e < existing.length; e++) {
|
|
2069
|
+
var ex = existing[e];
|
|
2070
|
+
var copy = {};
|
|
2071
|
+
// Figma auto-populates both label and labelMarkdown on read,
|
|
2072
|
+
// but rejects writing both — prefer labelMarkdown when both exist
|
|
2073
|
+
if (ex.labelMarkdown) {
|
|
2074
|
+
copy.labelMarkdown = ex.labelMarkdown;
|
|
2075
|
+
} else if (ex.label) {
|
|
2076
|
+
copy.label = ex.label;
|
|
2077
|
+
}
|
|
2078
|
+
if (ex.properties) copy.properties = ex.properties;
|
|
2079
|
+
if (ex.categoryId) copy.categoryId = ex.categoryId;
|
|
2080
|
+
merged.push(copy);
|
|
2081
|
+
}
|
|
2082
|
+
for (var n = 0; n < newAnnotations.length; n++) {
|
|
2083
|
+
merged.push(newAnnotations[n]);
|
|
2084
|
+
}
|
|
2085
|
+
newAnnotations = merged;
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
// Set annotations on the node
|
|
2089
|
+
node.annotations = newAnnotations;
|
|
2090
|
+
|
|
2091
|
+
console.log('🌉 [Desktop Bridge] Annotations set successfully. Count: ' + newAnnotations.length);
|
|
2092
|
+
|
|
2093
|
+
figma.ui.postMessage({
|
|
2094
|
+
type: 'SET_ANNOTATIONS_RESULT',
|
|
2095
|
+
requestId: msg.requestId,
|
|
2096
|
+
success: true,
|
|
2097
|
+
data: {
|
|
2098
|
+
nodeId: node.id,
|
|
2099
|
+
nodeName: node.name,
|
|
2100
|
+
annotationCount: newAnnotations.length,
|
|
2101
|
+
mode: msg.mode || 'replace'
|
|
2102
|
+
}
|
|
2103
|
+
});
|
|
2104
|
+
|
|
2105
|
+
} catch (error) {
|
|
2106
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
2107
|
+
console.error('🌉 [Desktop Bridge] Set annotations error:', errorMsg);
|
|
2108
|
+
figma.ui.postMessage({
|
|
2109
|
+
type: 'SET_ANNOTATIONS_RESULT',
|
|
2110
|
+
requestId: msg.requestId,
|
|
2111
|
+
success: false,
|
|
2112
|
+
error: errorMsg
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// ============================================================================
|
|
2118
|
+
// GET_ANNOTATION_CATEGORIES - List available annotation categories
|
|
2119
|
+
// ============================================================================
|
|
2120
|
+
else if (msg.type === 'GET_ANNOTATION_CATEGORIES') {
|
|
2121
|
+
try {
|
|
2122
|
+
console.log('🌉 [Desktop Bridge] Fetching annotation categories');
|
|
2123
|
+
|
|
2124
|
+
var categories = await figma.annotations.getAnnotationCategoriesAsync();
|
|
2125
|
+
var result = categories.map(function(c) { return { id: c.id, name: c.name }; });
|
|
2126
|
+
|
|
2127
|
+
console.log('🌉 [Desktop Bridge] Found ' + result.length + ' annotation categories');
|
|
2128
|
+
|
|
2129
|
+
figma.ui.postMessage({
|
|
2130
|
+
type: 'GET_ANNOTATION_CATEGORIES_RESULT',
|
|
2131
|
+
requestId: msg.requestId,
|
|
2132
|
+
success: true,
|
|
2133
|
+
data: { categories: result }
|
|
2134
|
+
});
|
|
2135
|
+
|
|
2136
|
+
} catch (error) {
|
|
2137
|
+
var errorMsg = error && error.message ? error.message : String(error);
|
|
2138
|
+
console.error('🌉 [Desktop Bridge] Get annotation categories error:', errorMsg);
|
|
2139
|
+
figma.ui.postMessage({
|
|
2140
|
+
type: 'GET_ANNOTATION_CATEGORIES_RESULT',
|
|
2141
|
+
requestId: msg.requestId,
|
|
2142
|
+
success: false,
|
|
2143
|
+
error: errorMsg
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
|
|
1345
2148
|
// ============================================================================
|
|
1346
2149
|
// ADD_COMPONENT_PROPERTY - Add property to component
|
|
1347
2150
|
// ============================================================================
|
|
@@ -2085,9 +2888,107 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2085
2888
|
throw new Error('Node type ' + node.type + ' does not support export');
|
|
2086
2889
|
}
|
|
2087
2890
|
|
|
2088
|
-
// Configure export settings
|
|
2891
|
+
// Configure export settings — AI-optimized defaults (PNG 1x)
|
|
2089
2892
|
var format = msg.format || 'PNG';
|
|
2090
|
-
var scale = msg.scale ||
|
|
2893
|
+
var scale = msg.scale || 1;
|
|
2894
|
+
|
|
2895
|
+
// AI vision cap: Claude API resizes images to 1568px on the longest side
|
|
2896
|
+
// before processing, so exporting larger just wastes bandwidth and tokens.
|
|
2897
|
+
var AI_MAX_DIMENSION = 1568;
|
|
2898
|
+
var nodeWidth = 0;
|
|
2899
|
+
var nodeHeight = 0;
|
|
2900
|
+
|
|
2901
|
+
if (node.type === 'PAGE') {
|
|
2902
|
+
// Pages don't have fixed dimensions — calculate from visible children
|
|
2903
|
+
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
2904
|
+
for (var i = 0; i < node.children.length; i++) {
|
|
2905
|
+
var child = node.children[i];
|
|
2906
|
+
if (child.visible !== false && 'absoluteBoundingBox' in child && child.absoluteBoundingBox) {
|
|
2907
|
+
var bb = child.absoluteBoundingBox;
|
|
2908
|
+
minX = Math.min(minX, bb.x);
|
|
2909
|
+
minY = Math.min(minY, bb.y);
|
|
2910
|
+
maxX = Math.max(maxX, bb.x + bb.width);
|
|
2911
|
+
maxY = Math.max(maxY, bb.y + bb.height);
|
|
2912
|
+
}
|
|
2913
|
+
}
|
|
2914
|
+
if (minX !== Infinity) {
|
|
2915
|
+
nodeWidth = maxX - minX;
|
|
2916
|
+
nodeHeight = maxY - minY;
|
|
2917
|
+
}
|
|
2918
|
+
} else if ('width' in node && 'height' in node) {
|
|
2919
|
+
nodeWidth = node.width;
|
|
2920
|
+
nodeHeight = node.height;
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
// Cap scale so the longest exported side doesn't exceed the AI processing ceiling
|
|
2924
|
+
if (nodeWidth > 0 && nodeHeight > 0) {
|
|
2925
|
+
var longestSide = Math.max(nodeWidth, nodeHeight);
|
|
2926
|
+
var exportedLongest = longestSide * scale;
|
|
2927
|
+
if (exportedLongest > AI_MAX_DIMENSION) {
|
|
2928
|
+
var cappedScale = AI_MAX_DIMENSION / longestSide;
|
|
2929
|
+
console.log('🌉 [Desktop Bridge] Capping scale from', scale, 'to', cappedScale.toFixed(3),
|
|
2930
|
+
'(node ' + Math.round(longestSide) + 'px, cap ' + AI_MAX_DIMENSION + 'px)');
|
|
2931
|
+
scale = cappedScale;
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
// Analyze node content to recommend optimal format
|
|
2936
|
+
var imageCount = 0;
|
|
2937
|
+
var gradientCount = 0;
|
|
2938
|
+
var textCount = 0;
|
|
2939
|
+
var vectorCount = 0;
|
|
2940
|
+
var maxDepth = 3; // Don't recurse too deep — top-level composition is enough
|
|
2941
|
+
|
|
2942
|
+
function analyzeContent(n, depth) {
|
|
2943
|
+
if (depth > maxDepth) return;
|
|
2944
|
+
if (n.type === 'TEXT') { textCount++; return; }
|
|
2945
|
+
if (n.type === 'VECTOR' || n.type === 'LINE' || n.type === 'STAR' ||
|
|
2946
|
+
n.type === 'POLYGON' || n.type === 'ELLIPSE' || n.type === 'BOOLEAN_OPERATION') {
|
|
2947
|
+
vectorCount++; return;
|
|
2948
|
+
}
|
|
2949
|
+
// Check fills for images and gradients
|
|
2950
|
+
if ('fills' in n && Array.isArray(n.fills)) {
|
|
2951
|
+
for (var f = 0; f < n.fills.length; f++) {
|
|
2952
|
+
var fill = n.fills[f];
|
|
2953
|
+
if (fill.visible === false) continue;
|
|
2954
|
+
if (fill.type === 'IMAGE') imageCount++;
|
|
2955
|
+
if (fill.type === 'GRADIENT_LINEAR' || fill.type === 'GRADIENT_RADIAL' ||
|
|
2956
|
+
fill.type === 'GRADIENT_ANGULAR' || fill.type === 'GRADIENT_DIAMOND') gradientCount++;
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
// Recurse into children
|
|
2960
|
+
if ('children' in n) {
|
|
2961
|
+
for (var c = 0; c < n.children.length; c++) {
|
|
2962
|
+
if (n.children[c].visible !== false) {
|
|
2963
|
+
analyzeContent(n.children[c], depth + 1);
|
|
2964
|
+
}
|
|
2965
|
+
}
|
|
2966
|
+
}
|
|
2967
|
+
}
|
|
2968
|
+
analyzeContent(node, 0);
|
|
2969
|
+
|
|
2970
|
+
var totalElements = imageCount + gradientCount + textCount + vectorCount;
|
|
2971
|
+
var photoHeavy = totalElements > 0 && (imageCount + gradientCount) / totalElements > 0.5;
|
|
2972
|
+
var adviceParts = [];
|
|
2973
|
+
|
|
2974
|
+
// Format advice
|
|
2975
|
+
if (photoHeavy) {
|
|
2976
|
+
adviceParts.push('Image/gradient-heavy content — try format: "JPG" for smaller file.');
|
|
2977
|
+
}
|
|
2978
|
+
|
|
2979
|
+
// Scale capping advice
|
|
2980
|
+
if (scale < (msg.scale || 1)) {
|
|
2981
|
+
adviceParts.push('Scale capped from ' + (msg.scale || 1) + 'x to ' +
|
|
2982
|
+
scale.toFixed(2) + 'x (AI vision max: 1568px).');
|
|
2983
|
+
}
|
|
2984
|
+
|
|
2985
|
+
// Scope advice for full-page captures with heavy downscaling
|
|
2986
|
+
if (node.type === 'PAGE' && scale < 0.5) {
|
|
2987
|
+
adviceParts.push('Full-page capture at ' + scale.toFixed(2) +
|
|
2988
|
+
'x — text may be unreadable. Pass a nodeId to target a specific frame or component.');
|
|
2989
|
+
}
|
|
2990
|
+
|
|
2991
|
+
var formatAdvice = adviceParts.join(' ');
|
|
2091
2992
|
|
|
2092
2993
|
var exportSettings = {
|
|
2093
2994
|
format: format,
|
|
@@ -2106,7 +3007,7 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2106
3007
|
bounds = node.absoluteBoundingBox;
|
|
2107
3008
|
}
|
|
2108
3009
|
|
|
2109
|
-
console.log('🌉 [Desktop Bridge] Screenshot captured:', bytes.length, 'bytes');
|
|
3010
|
+
console.log('🌉 [Desktop Bridge] Screenshot captured:', bytes.length, 'bytes (' + format + ' @ ' + scale.toFixed(2) + 'x)');
|
|
2110
3011
|
|
|
2111
3012
|
figma.ui.postMessage({
|
|
2112
3013
|
type: 'CAPTURE_SCREENSHOT_RESULT',
|
|
@@ -2122,7 +3023,8 @@ figma.ui.onmessage = async (msg) => {
|
|
|
2122
3023
|
name: node.name,
|
|
2123
3024
|
type: node.type
|
|
2124
3025
|
},
|
|
2125
|
-
bounds: bounds
|
|
3026
|
+
bounds: bounds,
|
|
3027
|
+
formatAdvice: formatAdvice
|
|
2126
3028
|
}
|
|
2127
3029
|
});
|
|
2128
3030
|
|