@mp3wizard/figma-console-mcp 1.32.3 → 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 +25 -17
- package/dist/cloudflare/core/cloud-websocket-connector.js +18 -0
- 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-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
package/dist/core/write-tools.js
CHANGED
|
@@ -697,7 +697,7 @@ return {
|
|
|
697
697
|
}
|
|
698
698
|
});
|
|
699
699
|
// Tool: Setup design tokens (collection + modes + variables atomically)
|
|
700
|
-
server.tool("figma_setup_design_tokens", "Create a complete design token structure in one operation: collection, modes, and all variables. Ideal for importing CSS custom properties or design tokens into Figma. Requires Desktop Bridge plugin.", {
|
|
700
|
+
server.tool("figma_setup_design_tokens", "Create a complete design token structure in one operation: collection, modes, and all variables. Ideal for importing CSS custom properties or design tokens into Figma. Values may be literals OR DTCG-style brace references ('{color.blue.600}', or set-qualified '{primitives.color.blue.600}') that resolve to variable ALIASES — first against variables created in this same call, then against existing local variables. Unresolvable references skip that value with a per-item warning (the rest of the batch still applies), so semantic collections referencing primitives no longer need raw figma_execute. Requires Desktop Bridge plugin.", {
|
|
701
701
|
collectionName: z
|
|
702
702
|
.string()
|
|
703
703
|
.describe("Name for the token collection (e.g., 'Brand Tokens')"),
|
|
@@ -720,7 +720,7 @@ return {
|
|
|
720
720
|
.describe("Optional description"),
|
|
721
721
|
values: z
|
|
722
722
|
.record(z.string(), z.union([z.string(), z.number(), z.boolean()]))
|
|
723
|
-
.describe("Values keyed by mode NAME (not ID). Example: { 'Light': '#FFFFFF', 'Dark': '#000000' }"),
|
|
723
|
+
.describe("Values keyed by mode NAME (not ID). Example: { 'Light': '#FFFFFF', 'Dark': '#000000' }. A string value wrapped in braces is an ALIAS reference resolved to another variable: '{color.blue.600}' matches variable name 'color/blue/600' — first among variables created in THIS call, then existing local variables (exact match, then case-insensitive). Set-qualified references ('{primitives.color.blue.600}') strip the leading collection name. Unresolvable references skip that value and surface in the response's warnings[]."),
|
|
724
724
|
}))
|
|
725
725
|
.min(1)
|
|
726
726
|
.max(100)
|
|
@@ -758,31 +758,130 @@ for (let i = 1; i < modeNames.length; i++) {
|
|
|
758
758
|
modeMap[modeNames[i]] = newModeId;
|
|
759
759
|
}
|
|
760
760
|
|
|
761
|
-
// Step 3: Create all variables
|
|
761
|
+
// Step 3: Create all variables FIRST (values apply in a second pass so
|
|
762
|
+
// brace-reference values can alias variables created later in this call).
|
|
762
763
|
const results = [];
|
|
764
|
+
const warnings = [];
|
|
765
|
+
const resultByName = {}; // ONE results entry per token name — value-phase
|
|
766
|
+
// problems attach to it as valueErrors instead of
|
|
767
|
+
// pushing extra entries (which inflated created+failed
|
|
768
|
+
// past the token count).
|
|
769
|
+
const createdByName = {}; // exact name -> variable (this call)
|
|
770
|
+
const createdByLower = {}; // lowercased name -> variable (this call)
|
|
771
|
+
const createdDefs = []; // { def, variable } for the value pass
|
|
763
772
|
for (const t of tokenDefs) {
|
|
764
773
|
try {
|
|
765
774
|
const variable = figma.variables.createVariable(t.name, collection, t.resolvedType);
|
|
766
775
|
if (t.description) variable.description = t.description;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
results.push({ success: true, name: t.name, id: variable.id });
|
|
776
|
+
createdByName[t.name] = variable;
|
|
777
|
+
createdByLower[t.name.toLowerCase()] = variable;
|
|
778
|
+
createdDefs.push({ def: t, variable: variable });
|
|
779
|
+
const entry = { success: true, name: t.name, id: variable.id };
|
|
780
|
+
resultByName[t.name] = entry;
|
|
781
|
+
results.push(entry);
|
|
774
782
|
} catch (err) {
|
|
775
783
|
results.push({ success: false, name: t.name, error: String(err) });
|
|
776
784
|
}
|
|
777
785
|
}
|
|
778
786
|
|
|
787
|
+
// Reference resolution: '{color.blue.600}' -> variable named 'color/blue/600'.
|
|
788
|
+
// Priority: created-in-this-call (exact, then case-insensitive; optionally
|
|
789
|
+
// set-qualified by THIS collection's name), then existing local variables
|
|
790
|
+
// (exact, then case-insensitive; optionally set-qualified by an existing
|
|
791
|
+
// collection's name).
|
|
792
|
+
function isReference(value) {
|
|
793
|
+
return typeof value === 'string' && /^\\{[^{}]+\\}$/.test(value.trim());
|
|
794
|
+
}
|
|
795
|
+
let existingVars = null;
|
|
796
|
+
let existingCollections = null;
|
|
797
|
+
async function ensureExistingLoaded() {
|
|
798
|
+
if (existingVars === null) {
|
|
799
|
+
existingVars = await figma.variables.getLocalVariablesAsync();
|
|
800
|
+
existingCollections = await figma.variables.getLocalVariableCollectionsAsync();
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function findCreated(name) {
|
|
804
|
+
return createdByName[name] || createdByLower[name.toLowerCase()] || null;
|
|
805
|
+
}
|
|
806
|
+
async function resolveReference(raw) {
|
|
807
|
+
const inner = raw.trim().slice(1, -1);
|
|
808
|
+
const segments = inner.split('.').filter(function (s) { return s.length > 0; });
|
|
809
|
+
if (segments.length === 0) return null;
|
|
810
|
+
const fullName = segments.join('/');
|
|
811
|
+
|
|
812
|
+
// 1. Variables created in THIS call.
|
|
813
|
+
let match = findCreated(fullName);
|
|
814
|
+
if (match) return match;
|
|
815
|
+
if (segments.length > 1 && segments[0].toLowerCase() === collectionName.toLowerCase()) {
|
|
816
|
+
match = findCreated(segments.slice(1).join('/'));
|
|
817
|
+
if (match) return match;
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// 2. Existing local variables.
|
|
821
|
+
await ensureExistingLoaded();
|
|
822
|
+
match = existingVars.find(function (v) { return v.name === fullName; });
|
|
823
|
+
if (match) return match;
|
|
824
|
+
const lowerName = fullName.toLowerCase();
|
|
825
|
+
match = existingVars.find(function (v) { return v.name.toLowerCase() === lowerName; });
|
|
826
|
+
if (match) return match;
|
|
827
|
+
// 2b. Set-qualified: first segment names an existing collection.
|
|
828
|
+
if (segments.length > 1) {
|
|
829
|
+
const restName = segments.slice(1).join('/');
|
|
830
|
+
const restLower = restName.toLowerCase();
|
|
831
|
+
const setLower = segments[0].toLowerCase();
|
|
832
|
+
for (const c of existingCollections) {
|
|
833
|
+
if (c.name.toLowerCase() !== setLower) continue;
|
|
834
|
+
let m = existingVars.find(function (v) { return v.variableCollectionId === c.id && v.name === restName; });
|
|
835
|
+
if (!m) m = existingVars.find(function (v) { return v.variableCollectionId === c.id && v.name.toLowerCase() === restLower; });
|
|
836
|
+
if (m) return m;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
return null;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// Step 4: Apply values — literals directly, brace references as aliases.
|
|
843
|
+
// Value-phase problems attach to the token's EXISTING results entry (as
|
|
844
|
+
// valueErrors) + warnings; they never add a second entry for the same name.
|
|
845
|
+
function noteValueProblem(name, message) {
|
|
846
|
+
const entry = resultByName[name];
|
|
847
|
+
if (entry) {
|
|
848
|
+
entry.valueErrors = entry.valueErrors || [];
|
|
849
|
+
entry.valueErrors.push(message);
|
|
850
|
+
}
|
|
851
|
+
warnings.push('Token "' + name + '": ' + message);
|
|
852
|
+
}
|
|
853
|
+
for (const entry of createdDefs) {
|
|
854
|
+
const t = entry.def;
|
|
855
|
+
const variable = entry.variable;
|
|
856
|
+
for (const [modeName, value] of Object.entries(t.values)) {
|
|
857
|
+
const modeId = modeMap[modeName];
|
|
858
|
+
if (!modeId) { noteValueProblem(t.name, 'Unknown mode: ' + modeName + ' — value skipped.'); continue; }
|
|
859
|
+
try {
|
|
860
|
+
if (isReference(value)) {
|
|
861
|
+
const target = await resolveReference(value);
|
|
862
|
+
if (!target) {
|
|
863
|
+
warnings.push('Unresolvable reference ' + value + ' for token "' + t.name + '" (mode "' + modeName + '") — no matching variable in this call or the local file. Value skipped.');
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
variable.setValueForMode(modeId, figma.variables.createVariableAlias(target));
|
|
867
|
+
} else {
|
|
868
|
+
const processed = t.resolvedType === 'COLOR' && typeof value === 'string' ? hexToRgba(value) : value;
|
|
869
|
+
variable.setValueForMode(modeId, processed);
|
|
870
|
+
}
|
|
871
|
+
} catch (err) {
|
|
872
|
+
noteValueProblem(t.name, 'mode "' + modeName + '": ' + String(err));
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
|
|
779
877
|
return {
|
|
780
878
|
collectionId: collection.id,
|
|
781
879
|
collectionName: collectionName,
|
|
782
880
|
modes: modeMap,
|
|
783
881
|
created: results.filter(r => r.success).length,
|
|
784
882
|
failed: results.filter(r => !r.success).length,
|
|
785
|
-
results
|
|
883
|
+
results,
|
|
884
|
+
warnings
|
|
786
885
|
};`;
|
|
787
886
|
const timeout = Math.max(10000, tokens.length * 200 + modes.length * 500);
|
|
788
887
|
const result = await connector.executeCodeViaUI(script, Math.min(timeout, 30000));
|
|
@@ -1601,47 +1700,15 @@ After instantiating components, use figma_take_screenshot to verify the result l
|
|
|
1601
1700
|
// ============================================================================
|
|
1602
1701
|
// Component Set Arrangement Tool
|
|
1603
1702
|
// ============================================================================
|
|
1604
|
-
//
|
|
1605
|
-
//
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
Recreates the set using figma.combineAsVariants() for proper Figma integration, applies purple dashed border styling, and arranges variants in a labeled grid (columns = last property like State, rows = other properties like Type+Size). Creates a white container with title, row/column labels, and the component set.`, {
|
|
1609
|
-
componentSetId: z
|
|
1610
|
-
.string()
|
|
1611
|
-
.optional()
|
|
1612
|
-
.describe("Node ID of the component set to arrange. If not provided, will look for a selected component set."),
|
|
1613
|
-
componentSetName: z
|
|
1614
|
-
.string()
|
|
1615
|
-
.optional()
|
|
1616
|
-
.describe("Name of the component set to find. Used if componentSetId not provided."),
|
|
1617
|
-
options: z
|
|
1618
|
-
.object({
|
|
1619
|
-
gap: z
|
|
1620
|
-
.number()
|
|
1621
|
-
.optional()
|
|
1622
|
-
.default(24)
|
|
1623
|
-
.describe("Gap between grid cells in pixels (default: 24)"),
|
|
1624
|
-
cellPadding: z
|
|
1625
|
-
.number()
|
|
1626
|
-
.optional()
|
|
1627
|
-
.default(20)
|
|
1628
|
-
.describe("Padding inside each cell around the variant (default: 20)"),
|
|
1629
|
-
columnProperty: z
|
|
1630
|
-
.string()
|
|
1631
|
-
.optional()
|
|
1632
|
-
.describe("Property to use for columns (default: auto-detect last property, usually 'State')"),
|
|
1633
|
-
})
|
|
1634
|
-
.optional()
|
|
1635
|
-
.describe("Layout options"),
|
|
1636
|
-
}, async ({ componentSetId, componentSetName, options }) => {
|
|
1637
|
-
try {
|
|
1638
|
-
const connector = await getDesktopConnector();
|
|
1639
|
-
// Build the code to execute in Figma
|
|
1640
|
-
const code = `
|
|
1703
|
+
// Builds the in-place grid arrangement script. Shared by
|
|
1704
|
+
// figma_arrange_component_set and figma_create_component_set (autoArrange).
|
|
1705
|
+
const buildArrangeComponentSetCode = (componentSetId, componentSetName, options) => `
|
|
1641
1706
|
// ============================================================================
|
|
1642
1707
|
// COMPONENT SET ARRANGEMENT WITH PROPER LABELS AND CONTAINER
|
|
1643
1708
|
// Creates: White container frame -> Row labels (left) -> Column headers (top) -> Component set (center)
|
|
1644
1709
|
// Uses auto-layout for proper alignment of labels with grid cells
|
|
1710
|
+
// NON-DESTRUCTIVE: variants are repositioned in place on the existing set, so
|
|
1711
|
+
// the set's node ID is preserved and placed instances are unaffected.
|
|
1645
1712
|
// ============================================================================
|
|
1646
1713
|
|
|
1647
1714
|
// Configuration
|
|
@@ -1679,8 +1746,10 @@ if (!componentSet || componentSet.type !== "COMPONENT_SET") {
|
|
|
1679
1746
|
}
|
|
1680
1747
|
|
|
1681
1748
|
const page = figma.currentPage;
|
|
1682
|
-
|
|
1683
|
-
|
|
1749
|
+
// Absolute page coordinates — the set may be nested inside a previous
|
|
1750
|
+
// arrangement container, so relative x/y would be wrong for placement
|
|
1751
|
+
const csAbsX = componentSet.absoluteTransform[0][2];
|
|
1752
|
+
const csAbsY = componentSet.absoluteTransform[1][2];
|
|
1684
1753
|
const csOriginalName = componentSet.name;
|
|
1685
1754
|
|
|
1686
1755
|
// Get all variant components
|
|
@@ -1766,46 +1835,50 @@ const csHeight = (totalRows * cellHeight) + ((totalRows - 1) * gap) + (edgePaddi
|
|
|
1766
1835
|
// ============================================================================
|
|
1767
1836
|
// STEP 1: Remove old labels and container frames from previous arrangements
|
|
1768
1837
|
// ============================================================================
|
|
1769
|
-
const
|
|
1838
|
+
const isOldArrangementElement = (n) =>
|
|
1770
1839
|
(n.type === "TEXT" && (n.name.startsWith("Row: ") || n.name.startsWith("Col: "))) ||
|
|
1771
|
-
(n.type === "FRAME" && (n.name === "Component Container" || n.name === "Row Labels" || n.name === "Column Headers"))
|
|
1772
|
-
|
|
1840
|
+
(n.type === "FRAME" && (n.name === "Component Container" || n.name === "Row Labels" || n.name === "Column Headers"));
|
|
1841
|
+
|
|
1842
|
+
// If the set lives inside a previous arrangement container (re-run case),
|
|
1843
|
+
// pull it out to the page first so cleanup can't delete it
|
|
1844
|
+
let ancestor = componentSet.parent;
|
|
1845
|
+
while (ancestor && ancestor.type !== "PAGE") {
|
|
1846
|
+
if (isOldArrangementElement(ancestor)) {
|
|
1847
|
+
page.appendChild(componentSet);
|
|
1848
|
+
break;
|
|
1849
|
+
}
|
|
1850
|
+
ancestor = ancestor.parent;
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
const oldElements = page.children.filter(isOldArrangementElement);
|
|
1773
1854
|
for (const el of oldElements) {
|
|
1774
1855
|
el.remove();
|
|
1775
1856
|
}
|
|
1776
1857
|
|
|
1777
1858
|
// ============================================================================
|
|
1778
|
-
// STEP 2:
|
|
1859
|
+
// STEP 2: Prepare the EXISTING component set for manual grid positioning.
|
|
1860
|
+
// No clone/remove/combineAsVariants — the set keeps its node ID, so all
|
|
1861
|
+
// placed instances survive.
|
|
1779
1862
|
// ============================================================================
|
|
1780
|
-
const clonedVariants = [];
|
|
1781
|
-
for (const variant of variants) {
|
|
1782
|
-
const clone = variant.clone();
|
|
1783
|
-
page.appendChild(clone);
|
|
1784
|
-
clonedVariants.push(clone);
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
// Delete the old component set
|
|
1788
|
-
componentSet.remove();
|
|
1789
1863
|
|
|
1790
|
-
//
|
|
1791
|
-
|
|
1792
|
-
|
|
1864
|
+
// Auto-layout would ignore manual x/y on children — disable it if present
|
|
1865
|
+
if ("layoutMode" in componentSet && componentSet.layoutMode !== "NONE") {
|
|
1866
|
+
componentSet.layoutMode = "NONE";
|
|
1867
|
+
}
|
|
1793
1868
|
|
|
1794
1869
|
// Apply purple dashed border (Figma's native component set styling)
|
|
1795
|
-
|
|
1870
|
+
componentSet.strokes = [{
|
|
1796
1871
|
type: 'SOLID',
|
|
1797
1872
|
color: { r: 151/255, g: 71/255, b: 255/255 } // Figma's purple: #9747FF
|
|
1798
1873
|
}];
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1874
|
+
componentSet.dashPattern = [10, 5];
|
|
1875
|
+
componentSet.strokeWeight = 1;
|
|
1876
|
+
componentSet.strokeAlign = "INSIDE";
|
|
1802
1877
|
|
|
1803
1878
|
// ============================================================================
|
|
1804
|
-
// STEP 3: Arrange variants in grid pattern inside component set
|
|
1879
|
+
// STEP 3: Arrange variants in grid pattern inside component set (in place)
|
|
1805
1880
|
// ============================================================================
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
for (const variant of newVariants) {
|
|
1881
|
+
for (const variant of variants) {
|
|
1809
1882
|
const props = parseVariantName(variant.name);
|
|
1810
1883
|
const colValue = props[columnProp];
|
|
1811
1884
|
const colIdx = columnValues.indexOf(colValue);
|
|
@@ -1842,7 +1915,7 @@ for (const variant of newVariants) {
|
|
|
1842
1915
|
}
|
|
1843
1916
|
|
|
1844
1917
|
// Resize component set to fit grid
|
|
1845
|
-
|
|
1918
|
+
componentSet.resize(csWidth, csHeight);
|
|
1846
1919
|
|
|
1847
1920
|
// ============================================================================
|
|
1848
1921
|
// STEP 4: Create white container frame with proper structure
|
|
@@ -2026,10 +2099,11 @@ componentSetWrapper.name = "Component Set Wrapper";
|
|
|
2026
2099
|
componentSetWrapper.fills = [];
|
|
2027
2100
|
componentSetWrapper.resize(csWidth, csHeight);
|
|
2028
2101
|
|
|
2029
|
-
// Move component set inside wrapper (
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2102
|
+
// Move the existing component set inside wrapper (reparenting preserves the
|
|
2103
|
+
// node ID — placed instances are unaffected)
|
|
2104
|
+
componentSetWrapper.appendChild(componentSet);
|
|
2105
|
+
componentSet.x = 0;
|
|
2106
|
+
componentSet.y = 0;
|
|
2033
2107
|
|
|
2034
2108
|
// Append to parent FIRST, then set layoutSizing
|
|
2035
2109
|
gridColumn.appendChild(componentSetWrapper);
|
|
@@ -2039,8 +2113,8 @@ componentSetWrapper.layoutSizingVertical = 'FIXED';
|
|
|
2039
2113
|
contentRow.appendChild(gridColumn);
|
|
2040
2114
|
|
|
2041
2115
|
// Position container at original location
|
|
2042
|
-
containerFrame.x =
|
|
2043
|
-
containerFrame.y =
|
|
2116
|
+
containerFrame.x = csAbsX - CONTAINER_PADDING - 120; // Account for row labels width
|
|
2117
|
+
containerFrame.y = csAbsY - CONTAINER_PADDING - TITLE_FONT_SIZE - 24 - COLUMN_HEADER_HEIGHT - gap;
|
|
2044
2118
|
|
|
2045
2119
|
// Select and zoom to show result
|
|
2046
2120
|
figma.currentPage.selection = [containerFrame];
|
|
@@ -2048,10 +2122,10 @@ figma.viewport.scrollAndZoomIntoView([containerFrame]);
|
|
|
2048
2122
|
|
|
2049
2123
|
return {
|
|
2050
2124
|
success: true,
|
|
2051
|
-
message: "Component set arranged with proper container, labels, and alignment",
|
|
2125
|
+
message: "Component set arranged in place with proper container, labels, and alignment. Set identity preserved — placed instances unaffected.",
|
|
2052
2126
|
containerId: containerFrame.id,
|
|
2053
|
-
componentSetId:
|
|
2054
|
-
componentSetName:
|
|
2127
|
+
componentSetId: componentSet.id,
|
|
2128
|
+
componentSetName: componentSet.name,
|
|
2055
2129
|
grid: {
|
|
2056
2130
|
rows: totalRows,
|
|
2057
2131
|
columns: totalCols,
|
|
@@ -2064,7 +2138,7 @@ return {
|
|
|
2064
2138
|
rowLabels: rowCombinations.map(combo => rowProps.map(p => combo[p]).join(" / "))
|
|
2065
2139
|
},
|
|
2066
2140
|
componentSetSize: { width: csWidth, height: csHeight },
|
|
2067
|
-
variantCount:
|
|
2141
|
+
variantCount: variants.length,
|
|
2068
2142
|
structure: {
|
|
2069
2143
|
container: "White frame with title, row labels, column headers, and component set",
|
|
2070
2144
|
rowLabels: "Vertically aligned with each row's center",
|
|
@@ -2072,6 +2146,43 @@ return {
|
|
|
2072
2146
|
}
|
|
2073
2147
|
};
|
|
2074
2148
|
`;
|
|
2149
|
+
// Tool: Arrange Component Set (Professional Layout with Native Visualization)
|
|
2150
|
+
// Rearranges variants IN PLACE (sets x/y on existing children) so the component
|
|
2151
|
+
// set's identity is preserved and placed instances are unaffected
|
|
2152
|
+
server.tool("figma_arrange_component_set", `Organize a component set with Figma's native purple dashed visualization. Use after creating variants, adding states (hover/disabled/pressed), or when component sets need cleanup.
|
|
2153
|
+
|
|
2154
|
+
Non-destructive: rearranges the existing variants in place (grid positions on the existing set's children), so the component set keeps its node ID and all placed instances remain intact. Arranges variants in a labeled grid (columns = last property like State, rows = other properties like Type+Size) and wraps the set in a white container with title, row/column labels. Safe to run on component sets with placed instances, and safe to re-run.`, {
|
|
2155
|
+
componentSetId: z
|
|
2156
|
+
.string()
|
|
2157
|
+
.optional()
|
|
2158
|
+
.describe("Node ID of the component set to arrange. If not provided, will look for a selected component set."),
|
|
2159
|
+
componentSetName: z
|
|
2160
|
+
.string()
|
|
2161
|
+
.optional()
|
|
2162
|
+
.describe("Name of the component set to find. Used if componentSetId not provided."),
|
|
2163
|
+
options: z
|
|
2164
|
+
.object({
|
|
2165
|
+
gap: z
|
|
2166
|
+
.number()
|
|
2167
|
+
.optional()
|
|
2168
|
+
.default(24)
|
|
2169
|
+
.describe("Gap between grid cells in pixels (default: 24)"),
|
|
2170
|
+
cellPadding: z
|
|
2171
|
+
.number()
|
|
2172
|
+
.optional()
|
|
2173
|
+
.default(20)
|
|
2174
|
+
.describe("Padding inside each cell around the variant (default: 20)"),
|
|
2175
|
+
columnProperty: z
|
|
2176
|
+
.string()
|
|
2177
|
+
.optional()
|
|
2178
|
+
.describe("Property to use for columns (default: auto-detect last property, usually 'State')"),
|
|
2179
|
+
})
|
|
2180
|
+
.optional()
|
|
2181
|
+
.describe("Layout options"),
|
|
2182
|
+
}, async ({ componentSetId, componentSetName, options }) => {
|
|
2183
|
+
try {
|
|
2184
|
+
const connector = await getDesktopConnector();
|
|
2185
|
+
const code = buildArrangeComponentSetCode(componentSetId ?? null, componentSetName ?? null, options);
|
|
2075
2186
|
const result = await connector.executeCodeViaUI(code, 25000);
|
|
2076
2187
|
if (!result.success) {
|
|
2077
2188
|
throw new Error(result.error || "Failed to arrange component set");
|
|
@@ -2083,7 +2194,7 @@ return {
|
|
|
2083
2194
|
text: JSON.stringify({
|
|
2084
2195
|
...result.result,
|
|
2085
2196
|
hint: result.result?.success
|
|
2086
|
-
? "Component set arranged in a white container frame with properly aligned row and column labels. The purple dashed border is visible. Use figma_capture_screenshot to validate the layout."
|
|
2197
|
+
? "Component set arranged in place (same node ID, placed instances unaffected) inside a white container frame with properly aligned row and column labels. The purple dashed border is visible. Use figma_capture_screenshot to validate the layout."
|
|
2087
2198
|
: undefined,
|
|
2088
2199
|
}),
|
|
2089
2200
|
},
|
|
@@ -2106,6 +2217,164 @@ return {
|
|
|
2106
2217
|
};
|
|
2107
2218
|
}
|
|
2108
2219
|
});
|
|
2220
|
+
// Tool: Create Component Set with variants (structured command — no hand-written
|
|
2221
|
+
// figma_execute scripts, no 30s execution-cap juggling for callers)
|
|
2222
|
+
server.tool("figma_create_component_set", `Create a component set with variants in one call — replaces hand-written figma.combineAsVariants scripts.
|
|
2223
|
+
|
|
2224
|
+
Two modes:
|
|
2225
|
+
1. **Generate from a base component**: pass baseComponentId + properties (variant axes). The base is cloned for every combination of the axes ({ State: ['default','hover','disabled'], Size: ['sm','lg'] } → 6 variants), each named 'Prop=Value' comma-joined (e.g. 'State=hover, Size=sm'), then combined into a set. The base component itself becomes the FIRST variant (same node ID), so existing instances of the base survive as instances of that variant.
|
|
2226
|
+
2. **Combine existing components**: pass componentIds, optionally with variantProperties (aligned 1:1) to rename each component to Prop=Value form before combining.
|
|
2227
|
+
|
|
2228
|
+
Figma derives the variant property definitions from the names; they live on the SET (componentPropertyDefinitions), not on individual variants. The result includes each variant's key — instantiate with a VARIANT's key/nodeId via figma_instantiate_component, not the set's key.
|
|
2229
|
+
|
|
2230
|
+
Set autoArrange:true to lay the new set out as a labeled grid inside a white container (same layout as figma_arrange_component_set). Requires Desktop Bridge plugin.
|
|
2231
|
+
|
|
2232
|
+
SIZE GUIDANCE: hard cap 100 variants. The timeout auto-scales with variant count (~1.2s/variant, 30s floor / 2min cap), but above ~40 variants the single-pass clone+combine gets slow and heavy base components may still push the limit — prefer splitting large matrices into multiple sets (e.g. one set per Size).`, {
|
|
2233
|
+
baseComponentId: z
|
|
2234
|
+
.string()
|
|
2235
|
+
.optional()
|
|
2236
|
+
.describe("Node ID of an existing COMPONENT to use as the base. Cloned per property combination; becomes the set's first variant (keeps its node ID, so placed instances survive). Mutually exclusive with componentIds."),
|
|
2237
|
+
properties: z
|
|
2238
|
+
.record(z.string(), z.array(z.string()))
|
|
2239
|
+
.optional()
|
|
2240
|
+
.describe("Variant property axes — required with baseComponentId. Example: { State: ['default','hover','disabled'], Size: ['sm','lg'] } creates 6 variants. Max 100 combinations. Names and values must not contain '=' or ','."),
|
|
2241
|
+
componentIds: z
|
|
2242
|
+
.array(z.string())
|
|
2243
|
+
.optional()
|
|
2244
|
+
.describe("Node IDs of existing COMPONENT nodes to combine as variants. Mutually exclusive with baseComponentId. Components already inside a component set are rejected."),
|
|
2245
|
+
variantProperties: z
|
|
2246
|
+
.array(z.record(z.string()))
|
|
2247
|
+
.optional()
|
|
2248
|
+
.describe("Only with componentIds: one property map per component, aligned by index — e.g. [{ State: 'default' }, { State: 'hover' }]. Each component is renamed to 'Prop=Value, ...' before combining. Without this, existing names are kept (names lacking '=' become 'Property 1=<name>')."),
|
|
2249
|
+
name: z
|
|
2250
|
+
.string()
|
|
2251
|
+
.optional()
|
|
2252
|
+
.describe("Name for the component set (e.g., 'Button'). Defaults to Figma's derived name."),
|
|
2253
|
+
parentId: z
|
|
2254
|
+
.string()
|
|
2255
|
+
.optional()
|
|
2256
|
+
.describe("Node ID of the container (frame/section) to create the set in. Defaults to the current page."),
|
|
2257
|
+
position: z
|
|
2258
|
+
.object({ x: z.number(), y: z.number() })
|
|
2259
|
+
.optional()
|
|
2260
|
+
.describe("Position of the set within its parent."),
|
|
2261
|
+
autoArrange: z
|
|
2262
|
+
.boolean()
|
|
2263
|
+
.optional()
|
|
2264
|
+
.default(false)
|
|
2265
|
+
.describe("If true, arrange the new set in a labeled grid (columns = last property, rows = other properties) inside a white container — same in-place layout as figma_arrange_component_set."),
|
|
2266
|
+
arrangeOptions: z
|
|
2267
|
+
.object({
|
|
2268
|
+
gap: z.number().optional().describe("Gap between grid cells in pixels (default: 24)"),
|
|
2269
|
+
cellPadding: z.number().optional().describe("Padding inside each cell around the variant (default: 20)"),
|
|
2270
|
+
columnProperty: z.string().optional().describe("Property to use for columns (default: last property)"),
|
|
2271
|
+
})
|
|
2272
|
+
.optional()
|
|
2273
|
+
.describe("Grid layout options, used when autoArrange is true."),
|
|
2274
|
+
}, async ({ baseComponentId, properties, componentIds, variantProperties, name, parentId, position, autoArrange, arrangeOptions, }) => {
|
|
2275
|
+
try {
|
|
2276
|
+
if (!baseComponentId && (!componentIds || componentIds.length === 0)) {
|
|
2277
|
+
throw new Error("Provide either baseComponentId + properties (generate variants from a base) or componentIds (combine existing components)");
|
|
2278
|
+
}
|
|
2279
|
+
if (baseComponentId && componentIds && componentIds.length > 0) {
|
|
2280
|
+
throw new Error("baseComponentId and componentIds are mutually exclusive — pick one mode");
|
|
2281
|
+
}
|
|
2282
|
+
if (baseComponentId &&
|
|
2283
|
+
(!properties || Object.keys(properties).length === 0)) {
|
|
2284
|
+
throw new Error("properties is required with baseComponentId. Example: { State: ['default','hover'], Size: ['sm','lg'] }");
|
|
2285
|
+
}
|
|
2286
|
+
// Variant count drives the connector's scaled timeout; above ~40 the
|
|
2287
|
+
// single-pass clone+combine gets slow, so surface a heads-up.
|
|
2288
|
+
let requestedVariantCount = componentIds?.length ?? 1;
|
|
2289
|
+
if (properties && !componentIds?.length) {
|
|
2290
|
+
requestedVariantCount = 1;
|
|
2291
|
+
for (const values of Object.values(properties)) {
|
|
2292
|
+
requestedVariantCount *= Math.max(1, values?.length ?? 1);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
const sizeWarning = requestedVariantCount > 40
|
|
2296
|
+
? `${requestedVariantCount} variants requested — large sets are slow to build (timeout scales automatically, ~1.2s/variant, 2min cap) and heavy base components may still time out. Consider splitting into multiple sets (e.g. one per Size).`
|
|
2297
|
+
: undefined;
|
|
2298
|
+
const connector = await getDesktopConnector();
|
|
2299
|
+
const result = await connector.createComponentSet({
|
|
2300
|
+
baseComponentId,
|
|
2301
|
+
properties,
|
|
2302
|
+
componentIds,
|
|
2303
|
+
variantProperties,
|
|
2304
|
+
name,
|
|
2305
|
+
parentId,
|
|
2306
|
+
position,
|
|
2307
|
+
});
|
|
2308
|
+
if (!result.success) {
|
|
2309
|
+
throw new Error(result.error || "Failed to create component set");
|
|
2310
|
+
}
|
|
2311
|
+
const data = result.data || {};
|
|
2312
|
+
// Optional in-place grid arrangement — reuses the same script as
|
|
2313
|
+
// figma_arrange_component_set. Creation already succeeded, so an
|
|
2314
|
+
// arrange failure is reported as a warning, not an error.
|
|
2315
|
+
let arrange;
|
|
2316
|
+
if (autoArrange && data.componentSet?.id) {
|
|
2317
|
+
try {
|
|
2318
|
+
const arrangeResult = await connector.executeCodeViaUI(buildArrangeComponentSetCode(data.componentSet.id, null, arrangeOptions), 25000);
|
|
2319
|
+
if (arrangeResult.success &&
|
|
2320
|
+
arrangeResult.result &&
|
|
2321
|
+
!arrangeResult.result.error) {
|
|
2322
|
+
arrange = {
|
|
2323
|
+
arranged: true,
|
|
2324
|
+
containerId: arrangeResult.result.containerId,
|
|
2325
|
+
grid: arrangeResult.result.grid,
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
else {
|
|
2329
|
+
arrange = {
|
|
2330
|
+
arranged: false,
|
|
2331
|
+
error: arrangeResult.error ||
|
|
2332
|
+
arrangeResult.result?.error ||
|
|
2333
|
+
"Arrange failed",
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
}
|
|
2337
|
+
catch (arrangeError) {
|
|
2338
|
+
arrange = {
|
|
2339
|
+
arranged: false,
|
|
2340
|
+
error: arrangeError instanceof Error
|
|
2341
|
+
? arrangeError.message
|
|
2342
|
+
: String(arrangeError),
|
|
2343
|
+
};
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
return {
|
|
2347
|
+
content: [
|
|
2348
|
+
{
|
|
2349
|
+
type: "text",
|
|
2350
|
+
text: JSON.stringify({
|
|
2351
|
+
success: true,
|
|
2352
|
+
message: `Created component set "${data.componentSet?.name ?? name ?? ""}" with ${data.variantCount ?? "?"} variants`,
|
|
2353
|
+
...data,
|
|
2354
|
+
...(sizeWarning ? { sizeWarning } : {}),
|
|
2355
|
+
...(arrange ? { arrange } : {}),
|
|
2356
|
+
hint: "To place instances, pass a VARIANT's key/nodeId from variants[] to figma_instantiate_component — not the set's key. Property definitions live on the set (componentPropertyDefinitions). Use figma_capture_screenshot to verify the result.",
|
|
2357
|
+
}),
|
|
2358
|
+
},
|
|
2359
|
+
],
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
catch (error) {
|
|
2363
|
+
logger.error({ error }, "Failed to create component set");
|
|
2364
|
+
return {
|
|
2365
|
+
content: [
|
|
2366
|
+
{
|
|
2367
|
+
type: "text",
|
|
2368
|
+
text: JSON.stringify({
|
|
2369
|
+
error: error instanceof Error ? error.message : String(error),
|
|
2370
|
+
hint: "Make sure the Desktop Bridge plugin is running. baseComponentId/componentIds must reference COMPONENT nodes that are not already inside a component set. Node IDs are session-specific — re-search components if they may be stale.",
|
|
2371
|
+
}),
|
|
2372
|
+
},
|
|
2373
|
+
],
|
|
2374
|
+
isError: true,
|
|
2375
|
+
};
|
|
2376
|
+
}
|
|
2377
|
+
});
|
|
2109
2378
|
// Tool: Lint Design for accessibility and quality issues
|
|
2110
2379
|
server.tool("figma_lint_design", "Run comprehensive accessibility (WCAG 2.2) and design quality checks on the current page or a specific node tree. " +
|
|
2111
2380
|
"WCAG conformance checks (10 rules): color contrast (AA), non-text contrast (1.4.11), color-only differentiation (1.4.1), " +
|