@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.
Files changed (68) hide show
  1. package/README.md +13 -12
  2. package/dist/cloudflare/core/annotation-tools.js +230 -0
  3. package/dist/cloudflare/core/cloud-websocket-connector.js +93 -0
  4. package/dist/cloudflare/core/deep-component-tools.js +128 -0
  5. package/dist/cloudflare/core/design-code-tools.js +65 -7
  6. package/dist/cloudflare/core/enrichment/enrichment-service.js +108 -12
  7. package/dist/cloudflare/core/figjam-tools.js +485 -0
  8. package/dist/cloudflare/core/figma-api.js +7 -4
  9. package/dist/cloudflare/core/figma-desktop-connector.js +108 -0
  10. package/dist/cloudflare/core/figma-tools.js +445 -55
  11. package/dist/cloudflare/core/port-discovery.js +88 -0
  12. package/dist/cloudflare/core/resolve-package-root.js +11 -0
  13. package/dist/cloudflare/core/slides-tools.js +607 -0
  14. package/dist/cloudflare/core/websocket-connector.js +93 -0
  15. package/dist/cloudflare/core/websocket-server.js +18 -9
  16. package/dist/cloudflare/index.js +164 -41
  17. package/dist/core/annotation-tools.d.ts +14 -0
  18. package/dist/core/annotation-tools.d.ts.map +1 -0
  19. package/dist/core/annotation-tools.js +231 -0
  20. package/dist/core/annotation-tools.js.map +1 -0
  21. package/dist/core/deep-component-tools.d.ts +14 -0
  22. package/dist/core/deep-component-tools.d.ts.map +1 -0
  23. package/dist/core/deep-component-tools.js +129 -0
  24. package/dist/core/deep-component-tools.js.map +1 -0
  25. package/dist/core/design-code-tools.d.ts.map +1 -1
  26. package/dist/core/design-code-tools.js +65 -7
  27. package/dist/core/design-code-tools.js.map +1 -1
  28. package/dist/core/enrichment/enrichment-service.d.ts.map +1 -1
  29. package/dist/core/enrichment/enrichment-service.js +108 -12
  30. package/dist/core/enrichment/enrichment-service.js.map +1 -1
  31. package/dist/core/figma-api.d.ts +1 -1
  32. package/dist/core/figma-api.d.ts.map +1 -1
  33. package/dist/core/figma-api.js +7 -4
  34. package/dist/core/figma-api.js.map +1 -1
  35. package/dist/core/figma-connector.d.ts +5 -0
  36. package/dist/core/figma-connector.d.ts.map +1 -1
  37. package/dist/core/figma-desktop-connector.d.ts +20 -0
  38. package/dist/core/figma-desktop-connector.d.ts.map +1 -1
  39. package/dist/core/figma-desktop-connector.js +83 -0
  40. package/dist/core/figma-desktop-connector.js.map +1 -1
  41. package/dist/core/figma-tools.d.ts.map +1 -1
  42. package/dist/core/figma-tools.js +355 -26
  43. package/dist/core/figma-tools.js.map +1 -1
  44. package/dist/core/port-discovery.d.ts +21 -0
  45. package/dist/core/port-discovery.d.ts.map +1 -1
  46. package/dist/core/port-discovery.js +88 -0
  47. package/dist/core/port-discovery.js.map +1 -1
  48. package/dist/core/resolve-package-root.d.ts +2 -0
  49. package/dist/core/resolve-package-root.d.ts.map +1 -0
  50. package/dist/core/resolve-package-root.js +12 -0
  51. package/dist/core/resolve-package-root.js.map +1 -0
  52. package/dist/core/types/design-code.d.ts +1 -0
  53. package/dist/core/types/design-code.d.ts.map +1 -1
  54. package/dist/core/websocket-connector.d.ts +5 -0
  55. package/dist/core/websocket-connector.d.ts.map +1 -1
  56. package/dist/core/websocket-connector.js +18 -0
  57. package/dist/core/websocket-connector.js.map +1 -1
  58. package/dist/core/websocket-server.d.ts.map +1 -1
  59. package/dist/core/websocket-server.js +7 -9
  60. package/dist/core/websocket-server.js.map +1 -1
  61. package/dist/local.d.ts +6 -0
  62. package/dist/local.d.ts.map +1 -1
  63. package/dist/local.js +58 -1
  64. package/dist/local.js.map +1 -1
  65. package/figma-desktop-bridge/code.js +906 -4
  66. package/figma-desktop-bridge/ui-full.html +80 -0
  67. package/figma-desktop-bridge/ui.html +82 -0
  68. 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 || 2;
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