From 7114047896618fc262131f37584560dcf673a96e Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:34:41 +0530 Subject: [PATCH 01/20] cleanup: remove ~850KB dead weight, dead code paths, and two runtime bug traps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Files removed (recoverable from git history): - backup/ (252KB stale copies), releases/ (580KB frozen snapshots, v2.0.0 copy was already 84 lines stale vs shipped code.js), both byte-identical unreferenced bmc-button PNGs Dead code removed: - code.ts: unused Result machinery; dead get_variables/'variables' round-trip (no UI sender/handler); dead 'close' case - ui.html: never-called processInChunks/batchDOMUpdate; shadowed first selectAllImport definition; dead collection_details handler (called an undefined function); unused tempDiv + orphan DocumentFragment; duplicate non-!important .advanced-only CSS block Bug fix: - showToast was called at two error paths but never defined (ReferenceError); added shim routing to the activity log Compliance (Figma plugin guidelines): - manifest.json: dropped unused "currentuser" permission (figma.currentUser appears nowhere in source or shipped artifact) — least privilege code.js rebuilt via the documented pnpm build pipeline (tsc + terser); previous checked-in artifact was unminified tsc output. Co-Authored-By: Claude Fable 5 --- README.md | 4 +- docs/AI_CONTEXT.md | 8 +- variables-styles-extractor/AGENTS.md | 23 +- .../backup/code.backup.js | 1420 --- .../backup/ui.html.backup | 3127 ------ .../backup/ui.html.v1-backup | 2565 ----- variables-styles-extractor/bmc-button-lg.png | Bin 4811 -> 0 bytes variables-styles-extractor/bmc-button.png | Bin 4811 -> 0 bytes variables-styles-extractor/code.js | 2756 +----- variables-styles-extractor/manifest.json | 2 +- .../releases/v1.6.0/KNOWN_ISSUES.md | 44 - .../releases/v1.6.0/LICENSE | 21 - .../releases/v1.6.0/NOTES.md | 24 - .../releases/v1.6.0/code.js | 11 - .../releases/v1.6.0/manifest.json | 15 - .../releases/v1.6.0/ui.html | 2565 ----- .../releases/v2.0.0/KNOWN_ISSUES.md | 139 - .../releases/v2.0.0/LICENSE | 21 - .../releases/v2.0.0/NOTES.md | 54 - .../releases/v2.0.0/code.js | 2712 ----- .../releases/v2.0.0/manifest.json | 15 - .../releases/v2.0.0/ui.html | 8815 ----------------- variables-styles-extractor/src/code.ts | 50 +- variables-styles-extractor/ui.html | 66 +- 24 files changed, 24 insertions(+), 24433 deletions(-) delete mode 100644 variables-styles-extractor/backup/code.backup.js delete mode 100644 variables-styles-extractor/backup/ui.html.backup delete mode 100644 variables-styles-extractor/backup/ui.html.v1-backup delete mode 100644 variables-styles-extractor/bmc-button-lg.png delete mode 100644 variables-styles-extractor/bmc-button.png delete mode 100644 variables-styles-extractor/releases/v1.6.0/KNOWN_ISSUES.md delete mode 100644 variables-styles-extractor/releases/v1.6.0/LICENSE delete mode 100644 variables-styles-extractor/releases/v1.6.0/NOTES.md delete mode 100644 variables-styles-extractor/releases/v1.6.0/code.js delete mode 100644 variables-styles-extractor/releases/v1.6.0/manifest.json delete mode 100644 variables-styles-extractor/releases/v1.6.0/ui.html delete mode 100644 variables-styles-extractor/releases/v2.0.0/KNOWN_ISSUES.md delete mode 100644 variables-styles-extractor/releases/v2.0.0/LICENSE delete mode 100644 variables-styles-extractor/releases/v2.0.0/NOTES.md delete mode 100644 variables-styles-extractor/releases/v2.0.0/code.js delete mode 100644 variables-styles-extractor/releases/v2.0.0/manifest.json delete mode 100644 variables-styles-extractor/releases/v2.0.0/ui.html diff --git a/README.md b/README.md index c254314..4aac85f 100644 --- a/README.md +++ b/README.md @@ -54,8 +54,7 @@ side-kicks/ (this repo: tknatwork/side-kicks │ ├── src/code.ts ← Backend source (Figma QuickJS VM) │ ├── .gcc/ ← Project session memory + build log │ ├── .github/copilot-instructions.md -│ ├── docs/ ← AI_CONTEXT, CHANGELOG, CODING_STANDARDS, FIGMA_PLUGIN_DEVELOPMENT, etc. -│ └── releases/ +│ └── docs/ ← AI_CONTEXT, CHANGELOG, CODING_STANDARDS, FIGMA_PLUGIN_DEVELOPMENT, etc. ├── nectar-design-toolkit/ ← Project: Design system toolkit (in development) └── Design System Builder/ ← Project: Design system toolkit (in development) ``` @@ -127,7 +126,6 @@ cd "Side-Kicks/" # Optional: # docs/ — additional documentation (CODING_STANDARDS, KNOWN_ISSUES, etc.) # src/ — source code -# releases/ — version archives ``` Then update: diff --git a/docs/AI_CONTEXT.md b/docs/AI_CONTEXT.md index 6b884e4..93e0ba7 100644 --- a/docs/AI_CONTEXT.md +++ b/docs/AI_CONTEXT.md @@ -60,8 +60,8 @@ Side-Kicks/ │ │ ├── copilot-instructions.md ← Project AI rules (PROTECTED) │ │ └── workflows/ ← CI/CD pipelines │ ├── docs/ ← Additional documentation -│ ├── src/ ← Source code -│ └── releases/ ← Version archives +│ └── src/ ← Source code +│ (version archives live in git history — `git log` / tags) ├── [new-project]/ ← TEMPLATE for future projects │ ├── AI_CONTEXT.md ← Project context (PROTECTED) │ ├── CHANGELOG.md ← Project history (PROTECTED) @@ -90,8 +90,8 @@ Each tool/plugin project lives in its **own subfolder** with a consistent struct │ ├── copilot-instructions.md ← Project AI rules (PROTECTED) │ └── workflows/ ← CI/CD pipelines ├── docs/ ← Additional documentation -├── src/ ← Source code -└── releases/ ← Version archives (optional) +└── src/ ← Source code + (version history: git log / tags — no releases/ folder needed) ``` ### Why This Structure? diff --git a/variables-styles-extractor/AGENTS.md b/variables-styles-extractor/AGENTS.md index 850292c..a05281c 100644 --- a/variables-styles-extractor/AGENTS.md +++ b/variables-styles-extractor/AGENTS.md @@ -101,18 +101,17 @@ variables-styles-extractor/ │ └── changelog.md ├── .github/ │ └── copilot-instructions.md ← Project-specific Copilot rules (protected) -├── docs/ -│ ├── AI_CONTEXT.md ← Legacy context (protected, kept for tooling) -│ ├── AGENTS.md ← Redirect to ../AGENTS.md -│ ├── CLAUDE.md ← Redirect to ../CLAUDE.md -│ ├── CHANGELOG.md ← Version history (protected) -│ ├── CODING_STANDARDS.md ← Mandatory rules -│ ├── FIGMA_PLUGIN_DEVELOPMENT.md ← Figma-specific guide -│ ├── GEMINI.md ← Gemini-specific notes -│ ├── JSON_FORMAT.md ← Import/export JSON schema -│ ├── KNOWN_ISSUES.md -│ └── TASKS.md -└── releases/ ← Version archives +└── docs/ + ├── AI_CONTEXT.md ← Legacy context (protected, kept for tooling) + ├── AGENTS.md ← Redirect to ../AGENTS.md + ├── CLAUDE.md ← Redirect to ../CLAUDE.md + ├── CHANGELOG.md ← Version history (protected) + ├── CODING_STANDARDS.md ← Mandatory rules + ├── FIGMA_PLUGIN_DEVELOPMENT.md ← Figma-specific guide + ├── GEMINI.md ← Gemini-specific notes + ├── JSON_FORMAT.md ← Import/export JSON schema + ├── KNOWN_ISSUES.md + └── TASKS.md ``` --- diff --git a/variables-styles-extractor/backup/code.backup.js b/variables-styles-extractor/backup/code.backup.js deleted file mode 100644 index dd24ca8..0000000 --- a/variables-styles-extractor/backup/code.backup.js +++ /dev/null @@ -1,1420 +0,0 @@ -"use strict"; -// Variables and Styles Extractor - Import/Export Figma Variables and Styles -// JSF-AV Compliant Architecture v1.5.4 -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -// Increased UI size for better performance with large design systems (Material Design, etc.) -// themeColor improves OS integration, title helps identify the plugin -figma.showUI(__html__, { - width: 480, - height: 820, - themeColors: true, - title: 'Variables & Styles Extractor' -}); -const Result = { - ok: (value) => ({ ok: true, value }), - err: (error) => ({ ok: false, error }), -}; -// ============================================================================ -// SECTION 3: UTILITY FUNCTIONS (JSF Rule 4.15 - DRY) -// ============================================================================ -const Logger = { - log(message, data) { - console.log(`[Variables Extractor] ${message}`, data || ''); - figma.ui.postMessage({ type: 'log', message, data }); - }, - send(type, data) { - figma.ui.postMessage({ type, data }); - } -}; -// Plan limits by Figma subscription tier (verified from Figma documentation) -const PLAN_LIMITS = { - starter: { - maxModesPerCollection: 1, - canPublishLibraries: false, - hasVariableRestApi: false - }, - professional: { - maxModesPerCollection: 10, - canPublishLibraries: true, - hasVariableRestApi: false - }, - organization: { - maxModesPerCollection: 20, - canPublishLibraries: true, - hasVariableRestApi: false - }, - enterprise: { - maxModesPerCollection: Infinity, - canPublishLibraries: true, - hasVariableRestApi: true - } -}; -// Maximum variables per collection (all plans) -const MAX_VARIABLES_PER_COLLECTION = 5000; -// Plan detection: Figma API doesn't expose plan directly, so we infer from existing modes -function detectCurrentPlan() { - return __awaiter(this, void 0, void 0, function* () { - const collections = yield figma.variables.getLocalVariableCollectionsAsync(); - let maxModesFound = 1; - for (const collection of collections) { - if (collection.modes.length > maxModesFound) { - maxModesFound = collection.modes.length; - } - } - // Infer plan based on highest mode count found - let inferredPlan; - if (maxModesFound > 20) { - inferredPlan = 'enterprise'; - } - else if (maxModesFound > 10) { - inferredPlan = 'organization'; - } - else if (maxModesFound > 1) { - inferredPlan = 'professional'; - } - else { - // Can't distinguish starter from others with 1 mode, assume professional - // User can override in UI - inferredPlan = 'professional'; - } - return Object.assign({ plan: inferredPlan }, PLAN_LIMITS[inferredPlan]); - }); -} -// Validate import data against plan limits -function validateImportAgainstPlan(importData, planOverride) { - return __awaiter(this, void 0, void 0, function* () { - const currentPlan = planOverride - ? Object.assign({ plan: planOverride }, PLAN_LIMITS[planOverride]) : yield detectCurrentPlan(); - const collections = yield figma.variables.getLocalVariableCollectionsAsync(); - const existingMaxModes = collections.reduce((max, col) => Math.max(max, col.modes.length), 0); - const existingTotalVars = (yield figma.variables.getLocalVariablesAsync()).length; - // Analyze import data - it's an array of collection exports and possibly _styles - const importCollections = []; - for (const item of importData) { - // Skip _styles entries - if ('_styles' in item) - continue; - importCollections.push(item); - } - let importingMaxModes = 0; - let importingTotalVars = 0; - const collectionsExceedingModeLimit = []; - for (const colExport of importCollections) { - // Each collection export is { "CollectionName": { modes: {...} } } - const colName = Object.keys(colExport)[0]; - const colData = colExport[colName]; - if (!colData || !colData.modes) - continue; - const modeCount = Object.keys(colData.modes).length; - if (modeCount > importingMaxModes) { - importingMaxModes = modeCount; - } - if (modeCount > currentPlan.maxModesPerCollection) { - collectionsExceedingModeLimit.push(`"${colName}" (${modeCount} modes, limit: ${currentPlan.maxModesPerCollection === Infinity ? '∞' : currentPlan.maxModesPerCollection})`); - } - // Count variables in first mode (they're the same across modes) - const firstMode = Object.values(colData.modes)[0]; - if (firstMode) { - importingTotalVars += countNestedVariables(firstMode); - } - } - // Generate warnings and errors - const warnings = []; - const errors = []; - // Mode limits - not a hard error, UI will show mode selection - // Only warn, don't block - user can select which modes to import - if (collectionsExceedingModeLimit.length > 0) { - // This is handled by the UI with mode selection - // Don't add to errors, just track in collectionsExceedingModeLimit - } - // Check variable count per collection - this IS a hard limit - for (const colExport of importCollections) { - const colName = Object.keys(colExport)[0]; - const colData = colExport[colName]; - if (!colData || !colData.modes) - continue; - const firstMode = Object.values(colData.modes)[0]; - const varCount = firstMode ? countNestedVariables(firstMode) : 0; - if (varCount > MAX_VARIABLES_PER_COLLECTION) { - errors.push(`Collection "${colName}" has ${varCount} variables, exceeds limit of ${MAX_VARIABLES_PER_COLLECTION}`); - } - } - // Warnings for large imports - if (importingTotalVars > 1000) { - warnings.push(`Large import: ${importingTotalVars} variables. This may take a moment.`); - } - if (importCollections.length > 10) { - warnings.push(`Importing ${importCollections.length} collections. Consider importing in batches.`); - } - // canImport is true if no hard errors (variable count) - // Mode limit exceedance is handled by UI with mode selection - return { - currentPlan, - existing: { - collections: collections.length, - maxModesInAnyCollection: existingMaxModes, - totalVariables: existingTotalVars - }, - importing: { - collections: importCollections.length, - maxModesInAnyCollection: importingMaxModes, - totalVariables: importingTotalVars, - collectionsExceedingModeLimit - }, - warnings, - errors, - canImport: errors.length === 0 - }; - }); -} -// Helper to count nested variables in a mode object -function countNestedVariables(obj, count = 0) { - for (const [, value] of Object.entries(obj)) { - if (value && typeof value === 'object') { - if ('$type' in value && '$value' in value) { - // This is a variable - count++; - } - else { - // Nested group - count = countNestedVariables(value, count); - } - } - } - return count; -} -const MathUtils = { - round2(value) { - return Math.round(value * 100) / 100; - }, - clamp(value, min, max) { - return Math.max(min, Math.min(max, value)); - }, - toHexByte(value) { - return Math.round(value * 255).toString(16).padStart(2, '0'); - }, - fromHexByte(hex) { - return parseInt(hex, 16) / 255; - } -}; -// ============================================================================ -// SECTION 4: COLOR CONVERSION MODULE (JSF Rule 4.7 - Single Responsibility) -// ============================================================================ -// Shared hue calculation - eliminates duplication between HSL/HSB -function calculateHue(r, g, b, max, min) { - if (max === min) - return 0; - const d = max - min; - let h = 0; - switch (max) { - case r: - h = ((g - b) / d + (g < b ? 6 : 0)) / 6; - break; - case g: - h = ((b - r) / d + 2) / 6; - break; - case b: - h = ((r - g) / d + 4) / 6; - break; - } - return Math.round(h * 360); -} -const ColorConverter = { - // Figma RGB (0-1) → Hex - toHex(color) { - const hex = '#' + - MathUtils.toHexByte(color.r) + - MathUtils.toHexByte(color.g) + - MathUtils.toHexByte(color.b); - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return hex + MathUtils.toHexByte(alpha); - } - return hex; - }, - // Figma RGB (0-1) → RGB (0-255) - toRgb255(color) { - const result = { - r: Math.round(color.r * 255), - g: Math.round(color.g * 255), - b: Math.round(color.b * 255) - }; - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return Object.assign(Object.assign({}, result), { a: MathUtils.round2(alpha) }); - } - return result; - }, - // Figma RGB (0-1) → CSS string - toCss(color) { - const r = Math.round(color.r * 255); - const g = Math.round(color.g * 255); - const b = Math.round(color.b * 255); - const alpha = color.a; - const a = alpha !== undefined ? MathUtils.round2(alpha) : 1; - return a < 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`; - }, - // Figma RGB (0-1) → HSL - toHsl(color) { - const { r, g, b } = color; - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const l = (max + min) / 2; - let s = 0; - if (max !== min) { - const d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - } - const result = { - h: calculateHue(r, g, b, max, min), - s: Math.round(s * 100), - l: Math.round(l * 100) - }; - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return Object.assign(Object.assign({}, result), { a: MathUtils.round2(alpha) }); - } - return result; - }, - // Figma RGB (0-1) → HSB/HSV - toHsb(color) { - const { r, g, b } = color; - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const s = max === 0 ? 0 : (max - min) / max; - const result = { - h: calculateHue(r, g, b, max, min), - s: Math.round(s * 100), - b: Math.round(max * 100) - }; - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return Object.assign(Object.assign({}, result), { a: MathUtils.round2(alpha) }); - } - return result; - }, - // Master export function - all formats - toAllFormats(color) { - return { - hex: this.toHex(color), - rgb: this.toRgb255(color), - css: this.toCss(color), - hsl: this.toHsl(color), - hsb: this.toHsb(color) - }; - } -}; -// ============================================================================ -// SECTION 5: COLOR PARSING MODULE (JSF Rule 4.7) -// ============================================================================ -const HEX_REGEX_8 = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; -const HEX_REGEX_6 = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; -const RGBA_REGEX = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/i; -const HSLA_REGEX = /hsla?\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*(?:,\s*([\d.]+))?\s*\)/i; -const ColorParser = { - // Hex → Figma RGBA - fromHex(hex) { - const match8 = HEX_REGEX_8.exec(hex); - if (match8) { - return { - r: MathUtils.fromHexByte(match8[1]), - g: MathUtils.fromHexByte(match8[2]), - b: MathUtils.fromHexByte(match8[3]), - a: MathUtils.fromHexByte(match8[4]) - }; - } - const match6 = HEX_REGEX_6.exec(hex); - if (match6) { - return { - r: MathUtils.fromHexByte(match6[1]), - g: MathUtils.fromHexByte(match6[2]), - b: MathUtils.fromHexByte(match6[3]), - a: 1 - }; - } - return { r: 0, g: 0, b: 0, a: 1 }; - }, - // RGB (0-255) → Figma RGBA - fromRgb255(rgb) { - var _a; - return { - r: rgb.r / 255, - g: rgb.g / 255, - b: rgb.b / 255, - a: (_a = rgb.a) !== null && _a !== void 0 ? _a : 1 - }; - }, - // CSS string → Figma RGBA - fromCss(css) { - const rgbaMatch = RGBA_REGEX.exec(css); - if (rgbaMatch) { - return { - r: parseInt(rgbaMatch[1], 10) / 255, - g: parseInt(rgbaMatch[2], 10) / 255, - b: parseInt(rgbaMatch[3], 10) / 255, - a: rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1 - }; - } - const hslaMatch = HSLA_REGEX.exec(css); - if (hslaMatch) { - return this.fromHsl({ - h: parseInt(hslaMatch[1], 10), - s: parseInt(hslaMatch[2], 10), - l: parseInt(hslaMatch[3], 10), - a: hslaMatch[4] !== undefined ? parseFloat(hslaMatch[4]) : 1 - }); - } - return { r: 0, g: 0, b: 0, a: 1 }; - }, - // HSL → Figma RGBA - fromHsl(hsl) { - var _a, _b; - const h = hsl.h / 360; - const s = hsl.s / 100; - const l = hsl.l / 100; - if (s === 0) { - return { r: l, g: l, b: l, a: (_a = hsl.a) !== null && _a !== void 0 ? _a : 1 }; - } - const hue2rgb = (p, q, t) => { - const tt = t < 0 ? t + 1 : t > 1 ? t - 1 : t; - if (tt < 1 / 6) - return p + (q - p) * 6 * tt; - if (tt < 1 / 2) - return q; - if (tt < 2 / 3) - return p + (q - p) * (2 / 3 - tt) * 6; - return p; - }; - const q = l < 0.5 ? l * (1 + s) : l + s - l * s; - const p = 2 * l - q; - return { - r: hue2rgb(p, q, h + 1 / 3), - g: hue2rgb(p, q, h), - b: hue2rgb(p, q, h - 1 / 3), - a: (_b = hsl.a) !== null && _b !== void 0 ? _b : 1 - }; - }, - // HSB → Figma RGBA - fromHsb(hsb) { - var _a; - const h = hsb.h / 360; - const s = hsb.s / 100; - const v = hsb.b / 100; - const i = Math.floor(h * 6); - const f = h * 6 - i; - const p = v * (1 - s); - const q = v * (1 - f * s); - const t = v * (1 - (1 - f) * s); - const rgbMap = [ - [v, t, p], [q, v, p], [p, v, t], - [p, q, v], [t, p, v], [v, p, q] - ]; - const [r, g, b] = rgbMap[i % 6]; - return { r, g, b, a: (_a = hsb.a) !== null && _a !== void 0 ? _a : 1 }; - }, - // Universal parser - accepts any format - parse(color) { - var _a; - // ExportColorValue object - if (typeof color === 'object' && color !== null && 'hex' in color && 'rgb' in color) { - return this.fromHex(color.hex); - } - // RGB object - if (typeof color === 'object' && color !== null && 'r' in color && 'g' in color && 'b' in color) { - const rgb = color; - // Check if Figma native (0-1) or standard (0-255) - if (rgb.r <= 1 && rgb.g <= 1 && rgb.b <= 1) { - return { r: rgb.r, g: rgb.g, b: rgb.b, a: (_a = rgb.a) !== null && _a !== void 0 ? _a : 1 }; - } - return this.fromRgb255(rgb); - } - // HSL object - if (typeof color === 'object' && color !== null && 'h' in color && 's' in color && 'l' in color) { - return this.fromHsl(color); - } - // HSB object - if (typeof color === 'object' && color !== null && 'h' in color && 's' in color && 'b' in color) { - return this.fromHsb(color); - } - // String formats - if (typeof color === 'string') { - if (color.startsWith('rgb') || color.startsWith('hsl')) { - return this.fromCss(color); - } - return this.fromHex(color); - } - return { r: 0, g: 0, b: 0, a: 1 }; - } -}; -// ============================================================================ -// SECTION 6: VARIABLE CACHE (JSF Rule 4.18 - Resource Management) -// ============================================================================ -class VariableCache { - constructor() { - this.collectionMap = new Map(); - this.variableMap = new Map(); - this.initialized = false; - } - initialize() { - return __awaiter(this, void 0, void 0, function* () { - if (this.initialized) - return; - yield this.rebuild(); - this.initialized = true; - }); - } - rebuild() { - return __awaiter(this, void 0, void 0, function* () { - this.collectionMap.clear(); - this.variableMap.clear(); - for (const col of yield figma.variables.getLocalVariableCollectionsAsync()) { - this.collectionMap.set(col.name, col); - for (const varId of col.variableIds) { - const v = yield figma.variables.getVariableByIdAsync(varId); - if (v) { - this.variableMap.set(`${col.name}/${v.name}`, v); - } - } - } - }); - } - getCollection(name) { - return this.collectionMap.get(name); - } - getVariable(key) { - return this.variableMap.get(key); - } - setVariable(key, variable) { - this.variableMap.set(key, variable); - } - setCollection(name, collection) { - this.collectionMap.set(name, collection); - } - get size() { - return this.variableMap.size; - } - get collections() { - return this.collectionMap.values(); - } - getVariableKeys() { - return Array.from(this.variableMap.keys()); - } -} -const variableCache = new VariableCache(); -// ============================================================================ -// SECTION 7: TYPE GUARDS & MAPPERS (JSF Rule 4.9) -// ============================================================================ -function isExportVariableValue(obj) { - return typeof obj === 'object' && obj !== null && '$type' in obj; -} -function isVariableAlias(value) { - return typeof value === 'object' && value !== null && - value.type === 'VARIABLE_ALIAS'; -} -const TypeMapper = { - toExportType(type) { - var _a; - const map = { - 'COLOR': 'color', - 'FLOAT': 'float', - 'STRING': 'string', - 'BOOLEAN': 'boolean' - }; - return (_a = map[type]) !== null && _a !== void 0 ? _a : 'string'; - }, - toFigmaType(type) { - var _a; - const map = { - 'color': 'COLOR', - 'float': 'FLOAT', - 'string': 'STRING', - 'boolean': 'BOOLEAN' - }; - return (_a = map[type]) !== null && _a !== void 0 ? _a : 'STRING'; - }, - scopesToArray(scopes) { - if (scopes.length === 0 || scopes.includes('ALL_SCOPES')) { - return ['ALL_SCOPES']; - } - return [...scopes]; - }, - arrayToScopes(arr) { - if (arr.includes('ALL_SCOPES')) { - return ['ALL_SCOPES']; - } - return arr; - } -}; -// ============================================================================ -// SECTION 8: BINDING UTILITIES -// ============================================================================ -function getVariableBindingInfo(boundVariables, key) { - return __awaiter(this, void 0, void 0, function* () { - if (!(boundVariables === null || boundVariables === void 0 ? void 0 : boundVariables[key])) - return {}; - const alias = boundVariables[key]; - if (!alias) - return {}; - const variable = yield figma.variables.getVariableByIdAsync(alias.id); - if (!variable) - return { id: alias.id }; - const collection = yield figma.variables.getVariableCollectionByIdAsync(variable.variableCollectionId); - return { - id: alias.id, - name: variable.name, - collection: collection === null || collection === void 0 ? void 0 : collection.name - }; - }); -} -function extractBindings(boundVariables, keys) { - return __awaiter(this, void 0, void 0, function* () { - if (!boundVariables) - return undefined; - const bindings = {}; - for (const key of keys) { - const binding = yield getVariableBindingInfo(boundVariables, key); - if (binding.name) { - bindings[key] = binding; - } - } - return Object.keys(bindings).length > 0 ? bindings : undefined; - }); -} -function flattenVariables(obj, prefix) { - const results = []; - for (const key of Object.keys(obj)) { - const val = obj[key]; - const path = prefix ? `${prefix}/${key}` : key; - if (isExportVariableValue(val)) { - results.push({ path, value: val }); - } - else { - results.push(...flattenVariables(val, path)); - } - } - return results; -} -function getValueAtPath(obj, path) { - const parts = path.split('/'); - let current = obj; - for (const part of parts) { - if (typeof current !== 'object' || current === null) - return null; - if (isExportVariableValue(current)) - return null; - current = current[part]; - } - return isExportVariableValue(current) ? current : null; -} -// Color Style Processor -const ColorStyleProcessor = { - export() { - return __awaiter(this, void 0, void 0, function* () { - var _a; - const styles = []; - for (const style of yield figma.getLocalPaintStylesAsync()) { - if (style.paints.length === 0) - continue; - const paint = style.paints[0]; - if (paint.type !== 'SOLID') - continue; - const colorAsRgba = paint.color; - let effectiveOpacity = (_a = paint.opacity) !== null && _a !== void 0 ? _a : 1; - if (colorAsRgba.a !== undefined && colorAsRgba.a < 1 && effectiveOpacity === 1) { - effectiveOpacity = colorAsRgba.a; - } - const colorWithAlpha = { - r: paint.color.r, - g: paint.color.g, - b: paint.color.b, - a: effectiveOpacity - }; - const colorStyle = Object.assign(Object.assign({ name: style.name, color: ColorConverter.toAllFormats(colorWithAlpha), opacity: MathUtils.round2(effectiveOpacity) }, (style.description && { description: style.description })), { boundVariables: yield extractBindings(paint.boundVariables, ['color']) }); - styles.push(colorStyle); - } - return styles; - }); - }, - importStyles(styles, cache) { - return __awaiter(this, void 0, void 0, function* () { - var _a; - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of yield figma.getLocalPaintStylesAsync()) { - existing.set(s.name, s); - } - for (const colorStyle of styles) { - let style; - if (existing.has(colorStyle.name)) { - style = existing.get(colorStyle.name); - updated++; - } - else { - style = figma.createPaintStyle(); - style.name = colorStyle.name; - created++; - } - if (colorStyle.description) { - style.description = colorStyle.description; - } - const colorRgba = ColorParser.parse(colorStyle.color); - let finalOpacity = (_a = colorStyle.opacity) !== null && _a !== void 0 ? _a : 1; - if (colorRgba.a < 1 && colorStyle.opacity === undefined) { - finalOpacity = MathUtils.round2(colorRgba.a); - } - let paint = { - type: 'SOLID', - color: { r: colorRgba.r, g: colorRgba.g, b: colorRgba.b }, - opacity: MathUtils.round2(finalOpacity) - }; - if (colorStyle.boundVariables) { - for (const [key, binding] of Object.entries(colorStyle.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - paint = figma.variables.setBoundVariableForPaint(paint, key, targetVar); - } - catch (e) { - Logger.log(`⚠️ Could not bind ${key}: ${e}`); - } - } - } - } - } - style.paints = [paint]; - } - return { created, updated }; - }); - } -}; -// Text Style Processor -const TextStyleProcessor = { - export() { - return __awaiter(this, void 0, void 0, function* () { - const styles = []; - for (const style of yield figma.getLocalTextStylesAsync()) { - const textStyle = Object.assign(Object.assign({ name: style.name, fontFamily: style.fontName.family, fontStyle: style.fontName.style, fontSize: style.fontSize, lineHeight: style.lineHeight, letterSpacing: style.letterSpacing, textCase: style.textCase, textDecoration: style.textDecoration }, (style.description && { description: style.description })), { boundVariables: yield extractBindings(style.boundVariables, ['fontSize', 'lineHeight', 'letterSpacing', 'paragraphSpacing', 'paragraphIndent']) }); - styles.push(textStyle); - } - return styles; - }); - }, - importStyles(styles, cache) { - return __awaiter(this, void 0, void 0, function* () { - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of yield figma.getLocalTextStylesAsync()) { - existing.set(s.name, s); - } - for (const textStyle of styles) { - let style; - if (existing.has(textStyle.name)) { - style = existing.get(textStyle.name); - updated++; - } - else { - style = figma.createTextStyle(); - style.name = textStyle.name; - created++; - } - if (textStyle.description) { - style.description = textStyle.description; - } - try { - yield figma.loadFontAsync({ family: textStyle.fontFamily, style: textStyle.fontStyle }); - style.fontName = { family: textStyle.fontFamily, style: textStyle.fontStyle }; - style.fontSize = textStyle.fontSize; - style.lineHeight = textStyle.lineHeight; - style.letterSpacing = textStyle.letterSpacing; - if (textStyle.textCase) - style.textCase = textStyle.textCase; - if (textStyle.textDecoration) - style.textDecoration = textStyle.textDecoration; - if (textStyle.boundVariables) { - for (const [key, binding] of Object.entries(textStyle.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - style.setBoundVariable(key, targetVar); - } - catch ( /* Skip */_a) { /* Skip */ } - } - } - } - } - } - catch (e) { - Logger.log(`⚠️ Could not load font for ${textStyle.name}: ${e}`); - } - } - return { created, updated }; - }); - } -}; -// Effect Style Processor -const EffectStyleProcessor = { - export() { - return __awaiter(this, void 0, void 0, function* () { - const styles = []; - for (const style of yield figma.getLocalEffectStylesAsync()) { - const effects = []; - for (const effect of style.effects) { - const effectData = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ type: effect.type, visible: effect.visible }, ('radius' in effect && { radius: effect.radius })), ('spread' in effect && { spread: effect.spread })), ('offset' in effect && { offset: effect.offset })), ('color' in effect && { color: ColorConverter.toAllFormats(effect.color) })), ('blendMode' in effect && { blendMode: effect.blendMode })), ('showShadowBehindNode' in effect && { showShadowBehindNode: effect.showShadowBehindNode })), { boundVariables: yield extractBindings(effect.boundVariables, ['color', 'radius', 'spread', 'offsetX', 'offsetY']) }); - effects.push(effectData); - } - const effectStyle = Object.assign(Object.assign({ name: style.name }, (style.description && { description: style.description })), { effects }); - styles.push(effectStyle); - } - return styles; - }); - }, - importStyles(styles, cache) { - return __awaiter(this, void 0, void 0, function* () { - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of yield figma.getLocalEffectStylesAsync()) { - existing.set(s.name, s); - } - for (const effectStyle of styles) { - let style; - if (existing.has(effectStyle.name)) { - style = existing.get(effectStyle.name); - updated++; - } - else { - style = figma.createEffectStyle(); - style.name = effectStyle.name; - created++; - } - if (effectStyle.description) { - style.description = effectStyle.description; - } - const newEffects = effectStyle.effects.map(effect => { - var _a; - const e = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ type: effect.type, visible: (_a = effect.visible) !== null && _a !== void 0 ? _a : true }, ((effect.radius !== undefined) && { radius: effect.radius })), ((effect.spread !== undefined) && { spread: effect.spread })), ((effect.offset !== undefined) && { offset: effect.offset })), ((effect.color !== undefined) && { - color: (() => { - const c = ColorParser.parse(effect.color); - return { r: c.r, g: c.g, b: c.b, a: MathUtils.round2(c.a) }; - })() - })), ((effect.blendMode !== undefined) && { blendMode: effect.blendMode })), ((effect.showShadowBehindNode !== undefined) && { showShadowBehindNode: effect.showShadowBehindNode })); - return e; - }); - style.effects = newEffects; - // Bind variables - for (let i = 0; i < effectStyle.effects.length; i++) { - const effectData = effectStyle.effects[i]; - if (effectData.boundVariables) { - for (const [key, binding] of Object.entries(effectData.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - const effects = [...style.effects]; - effects[i] = figma.variables.setBoundVariableForEffect(effects[i], key, targetVar); - style.effects = effects; - } - catch ( /* Skip */_a) { /* Skip */ } - } - } - } - } - } - } - return { created, updated }; - }); - } -}; -// Grid Style Processor -const GridStyleProcessor = { - export() { - return __awaiter(this, void 0, void 0, function* () { - const styles = []; - for (const style of yield figma.getLocalGridStylesAsync()) { - const layoutGrids = []; - for (const grid of style.layoutGrids) { - const gridColor = grid.color; - const gridData = Object.assign(Object.assign(Object.assign({ pattern: grid.pattern, visible: grid.visible, color: ColorConverter.toAllFormats(gridColor) }, (grid.pattern === 'GRID' && { sectionSize: grid.sectionSize })), (grid.pattern !== 'GRID' && Object.assign({ alignment: grid.alignment, gutterSize: grid.gutterSize, count: grid.count, offset: grid.offset }, (grid.sectionSize !== undefined && { sectionSize: grid.sectionSize })))), { boundVariables: yield extractBindings(grid.boundVariables, ['gutterSize', 'count', 'offset', 'sectionSize']) }); - layoutGrids.push(gridData); - } - const gridStyle = Object.assign(Object.assign({ name: style.name }, (style.description && { description: style.description })), { layoutGrids }); - styles.push(gridStyle); - } - return styles; - }); - }, - importStyles(styles, cache) { - return __awaiter(this, void 0, void 0, function* () { - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of yield figma.getLocalGridStylesAsync()) { - existing.set(s.name, s); - } - for (const gridStyle of styles) { - let style; - if (existing.has(gridStyle.name)) { - style = existing.get(gridStyle.name); - updated++; - } - else { - style = figma.createGridStyle(); - style.name = gridStyle.name; - created++; - } - if (gridStyle.description) { - style.description = gridStyle.description; - } - const newLayoutGrids = gridStyle.layoutGrids.map((grid) => { - var _a, _b, _c, _d, _e, _f, _g; - const gridColor = grid.color - ? ColorParser.parse(grid.color) - : { r: 1, g: 0, b: 0, a: 0.1 }; - const color = { - r: gridColor.r, - g: gridColor.g, - b: gridColor.b, - a: MathUtils.round2(gridColor.a) - }; - if (grid.pattern === 'GRID') { - return { - pattern: 'GRID', - sectionSize: (_a = grid.sectionSize) !== null && _a !== void 0 ? _a : 10, - visible: grid.visible !== false, - color - }; - } - const alignment = (_b = grid.alignment) !== null && _b !== void 0 ? _b : 'STRETCH'; - const base = { - pattern: grid.pattern, - gutterSize: (_c = grid.gutterSize) !== null && _c !== void 0 ? _c : 10, - count: (_d = grid.count) !== null && _d !== void 0 ? _d : 5, - visible: grid.visible !== false, - color - }; - if (alignment === 'STRETCH') { - return Object.assign(Object.assign({}, base), { alignment: 'STRETCH', offset: (_e = grid.offset) !== null && _e !== void 0 ? _e : 0 }); - } - else if (alignment === 'CENTER') { - return Object.assign(Object.assign({}, base), { alignment: 'CENTER', sectionSize: (_f = grid.sectionSize) !== null && _f !== void 0 ? _f : 100 }); - } - else { - const result = Object.assign(Object.assign({}, base), { alignment: alignment, offset: (_g = grid.offset) !== null && _g !== void 0 ? _g : 0 }); - if (grid.sectionSize !== undefined) { - result.sectionSize = grid.sectionSize; - } - return result; - } - }); - style.layoutGrids = newLayoutGrids; - // Bind variables - for (let i = 0; i < gridStyle.layoutGrids.length; i++) { - const gridData = gridStyle.layoutGrids[i]; - if (gridData.boundVariables) { - for (const [key, binding] of Object.entries(gridData.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - const grids = [...style.layoutGrids]; - grids[i] = figma.variables.setBoundVariableForLayoutGrid(grids[i], key, targetVar); - style.layoutGrids = grids; - } - catch ( /* Skip */_a) { /* Skip */ } - } - } - } - } - } - } - return { created, updated }; - }); - } -}; -// ============================================================================ -// SECTION 11: EXPORT ORCHESTRATOR -// ============================================================================ -function exportVariables(selectedCollections, styleOptions) { - return __awaiter(this, void 0, void 0, function* () { - var _a, _b, _c, _d, _e, _f, _g, _h, _j; - Logger.log('📤 Starting export...'); - try { - let collections = yield figma.variables.getLocalVariableCollectionsAsync(); - if (selectedCollections === null || selectedCollections === void 0 ? void 0 : selectedCollections.length) { - collections = collections.filter(c => selectedCollections.includes(c.name)); - Logger.log(`Filtering to ${collections.length} selected collections`); - } - const exportData = []; - let totalVariables = 0; - for (const collection of collections) { - Logger.log(`Processing collection: ${collection.name}`); - const collectionExport = { - [collection.name]: { modes: {} } - }; - // Initialize modes - for (const mode of collection.modes) { - collectionExport[collection.name].modes[mode.name] = {}; - } - // Process variables - for (const variableId of collection.variableIds) { - const variable = yield figma.variables.getVariableByIdAsync(variableId); - if (!variable) - continue; - totalVariables++; - const nameParts = variable.name.split('/'); - for (const mode of collection.modes) { - const modeValues = collectionExport[collection.name].modes[mode.name]; - const value = variable.valuesByMode[mode.modeId]; - // Navigate/create nested structure - let current = modeValues; - for (let i = 0; i < nameParts.length - 1; i++) { - const part = nameParts[i]; - if (!current[part] || isExportVariableValue(current[part])) { - current[part] = {}; - } - current = current[part]; - } - const leafName = nameParts[nameParts.length - 1]; - // Convert value - let exportValue; - let isAlias = false; - let aliasCollection = ''; - if (isVariableAlias(value)) { - const aliasVar = yield figma.variables.getVariableByIdAsync(value.id); - if (aliasVar) { - const aliasCol = yield figma.variables.getVariableCollectionByIdAsync(aliasVar.variableCollectionId); - isAlias = true; - aliasCollection = (_a = aliasCol === null || aliasCol === void 0 ? void 0 : aliasCol.name) !== null && _a !== void 0 ? _a : ''; - exportValue = `{${aliasVar.name.replace(/\//g, '.')}}`; - } - else { - exportValue = ''; - } - } - else if (typeof value === 'object' && value !== null && 'r' in value) { - exportValue = ColorConverter.toAllFormats(value); - } - else { - exportValue = value; - } - const varExport = Object.assign(Object.assign({ $scopes: TypeMapper.scopesToArray(variable.scopes), $type: TypeMapper.toExportType(variable.resolvedType), $value: exportValue }, (variable.description && { $description: variable.description })), (isAlias && { $libraryName: '', $collectionName: aliasCollection })); - current[leafName] = varExport; - } - } - exportData.push(collectionExport); - } - // Export styles - let stylesExported = null; - if (styleOptions) { - stylesExported = {}; - if (styleOptions.colorStyles) - stylesExported.colorStyles = yield ColorStyleProcessor.export(); - if (styleOptions.textStyles) - stylesExported.textStyles = yield TextStyleProcessor.export(); - if (styleOptions.effectStyles) - stylesExported.effectStyles = yield EffectStyleProcessor.export(); - if (styleOptions.gridStyles) - stylesExported.gridStyles = yield GridStyleProcessor.export(); - if (Object.keys(stylesExported).length > 0) { - exportData.push({ _styles: stylesExported }); - } - else { - stylesExported = null; - } - } - const stats = { - collections: collections.length, - variables: totalVariables, - styles: stylesExported ? { - color: (_c = (_b = stylesExported.colorStyles) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0, - text: (_e = (_d = stylesExported.textStyles) === null || _d === void 0 ? void 0 : _d.length) !== null && _e !== void 0 ? _e : 0, - effect: (_g = (_f = stylesExported.effectStyles) === null || _f === void 0 ? void 0 : _f.length) !== null && _g !== void 0 ? _g : 0, - grid: (_j = (_h = stylesExported.gridStyles) === null || _h === void 0 ? void 0 : _h.length) !== null && _j !== void 0 ? _j : 0 - } : null - }; - Logger.log(`✅ Export complete: ${stats.collections} collections, ${stats.variables} variables`); - Logger.send('export_complete', { - data: JSON.stringify(exportData, null, 2), - stats - }); - } - catch (e) { - Logger.log(`❌ Export error: ${e}`); - Logger.send('error', { message: `Export failed: ${e}` }); - } - }); -} -// ============================================================================ -// SECTION 12: IMPORT ORCHESTRATOR -// ============================================================================ -function importVariables(jsonData, options) { - return __awaiter(this, void 0, void 0, function* () { - Logger.log('📥 Starting import...'); - try { - const importData = JSON.parse(jsonData); - yield variableCache.initialize(); - let createdCollections = 0; - let createdVariables = 0; - let updatedVariables = 0; - let skippedVariables = 0; - let stylesCreated = 0; - let stylesUpdated = 0; - // Separate styles from collections - let stylesData = null; - const collectionData = []; - for (const item of importData) { - const keys = Object.keys(item); - if (keys.length === 1 && keys[0] === '_styles') { - stylesData = item._styles; - } - else { - collectionData.push(item); - } - } - // Process collections - for (const collectionObj of collectionData) { - const collectionName = Object.keys(collectionObj)[0]; - const collectionContent = collectionObj[collectionName]; - Logger.log(`Processing collection: ${collectionName}`); - let collection; - const existingCollection = variableCache.getCollection(collectionName); - if (existingCollection) { - if (!options.merge) { - Logger.log(` Skipping existing collection: ${collectionName}`); - continue; - } - collection = existingCollection; - Logger.log(` Merging into existing collection`); - } - else { - collection = figma.variables.createVariableCollection(collectionName); - variableCache.setCollection(collectionName, collection); - createdCollections++; - Logger.log(` Created new collection`); - } - // Setup modes - const modeNames = Object.keys(collectionContent.modes); - const modeMap = new Map(); - for (const mode of collection.modes) { - modeMap.set(mode.name, mode.modeId); - } - if (collection.modes.length === 1 && !modeMap.has(modeNames[0])) { - collection.renameMode(collection.modes[0].modeId, modeNames[0]); - modeMap.set(modeNames[0], collection.modes[0].modeId); - } - for (const modeName of modeNames) { - if (!modeMap.has(modeName)) { - try { - const newModeId = collection.addMode(modeName); - modeMap.set(modeName, newModeId); - } - catch (e) { - Logger.log(` ⚠️ Could not create mode ${modeName}: ${e}`); - } - } - } - // Process variables - const firstModeVars = collectionContent.modes[modeNames[0]]; - const variablePaths = flattenVariables(firstModeVars, ''); - for (const { path, value } of variablePaths) { - const fullPath = `${collectionName}/${path}`; - let variable; - const existingVar = variableCache.getVariable(fullPath); - if (existingVar) { - if (!options.overwrite) { - skippedVariables++; - continue; - } - variable = existingVar; - updatedVariables++; - } - else { - try { - variable = figma.variables.createVariable(path, collection, TypeMapper.toFigmaType(value.$type)); - createdVariables++; - } - catch (e) { - Logger.log(` ⚠️ Could not create variable ${path}: ${e}`); - continue; - } - } - if (value.$description) { - variable.description = value.$description; - } - try { - variable.scopes = TypeMapper.arrayToScopes(value.$scopes); - } - catch ( /* Skip */_a) { /* Skip */ } - // Set values for each mode - for (const modeName of modeNames) { - const modeId = modeMap.get(modeName); - if (!modeId) - continue; - const modeValue = getValueAtPath(collectionContent.modes[modeName], path); - if (!modeValue) - continue; - if (typeof modeValue.$value === 'string' && modeValue.$value.startsWith('{')) { - const aliasPath = modeValue.$value.slice(1, -1).replace(/\./g, '/'); - const aliasCollection = modeValue.$collectionName || collectionName; - const targetVar = variableCache.getVariable(`${aliasCollection}/${aliasPath}`); - if (targetVar) { - try { - variable.setValueForMode(modeId, { type: 'VARIABLE_ALIAS', id: targetVar.id }); - } - catch (_b) { - setRawValue(variable, modeId, modeValue); - } - } - else { - setRawValue(variable, modeId, modeValue); - } - } - else { - setRawValue(variable, modeId, modeValue); - } - } - variableCache.setVariable(fullPath, variable); - } - } - // Import styles - if (stylesData && options.importStyles) { - Logger.log('📦 Importing styles...'); - yield variableCache.rebuild(); - if (stylesData.colorStyles) { - const r = yield ColorStyleProcessor.importStyles(stylesData.colorStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - if (stylesData.textStyles) { - const r = yield TextStyleProcessor.importStyles(stylesData.textStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - if (stylesData.effectStyles) { - const r = yield EffectStyleProcessor.importStyles(stylesData.effectStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - if (stylesData.gridStyles) { - const r = yield GridStyleProcessor.importStyles(stylesData.gridStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - } - const stats = { - collectionsCreated: createdCollections, - variablesCreated: createdVariables, - variablesUpdated: updatedVariables, - variablesSkipped: skippedVariables, - stylesCreated, - stylesUpdated - }; - Logger.log(`✅ Import complete!`); - Logger.send('import_complete', { stats }); - } - catch (e) { - Logger.log(`❌ Import error: ${e}`); - Logger.send('error', { message: `Import failed: ${e}` }); - } - }); -} -function setRawValue(variable, modeId, value) { - try { - if (value.$type === 'color') { - const rgba = ColorParser.parse(value.$value); - const finalRgba = rgba.a < 1 - ? Object.assign(Object.assign({}, rgba), { a: MathUtils.round2(rgba.a) }) : rgba; - variable.setValueForMode(modeId, finalRgba); - } - else { - variable.setValueForMode(modeId, value.$value); - } - } - catch (e) { - console.error(`Could not set value: ${e}`); - } -} -// ============================================================================ -// SECTION 13: COLLECTION INFO -// ============================================================================ -function getCollections() { - return __awaiter(this, void 0, void 0, function* () { - const collections = yield figma.variables.getLocalVariableCollectionsAsync(); - const data = yield Promise.all(collections.map((c) => __awaiter(this, void 0, void 0, function* () { - const types = { color: 0, float: 0, boolean: 0, string: 0 }; - for (const varId of c.variableIds) { - const variable = yield figma.variables.getVariableByIdAsync(varId); - if (variable) { - const typeStr = TypeMapper.toExportType(variable.resolvedType); - types[typeStr]++; - } - } - return { - id: c.id, - name: c.name, - modes: c.modes.map(m => m.name), - variableCount: c.variableIds.length, - types - }; - }))); - const styles = { - colorStyles: (yield figma.getLocalPaintStylesAsync()).length, - textStyles: (yield figma.getLocalTextStylesAsync()).length, - effectStyles: (yield figma.getLocalEffectStylesAsync()).length, - gridStyles: (yield figma.getLocalGridStylesAsync()).length - }; - Logger.send('collections', { collections: data, styles }); - }); -} -function getVariablesForCollection(collectionName) { - return __awaiter(this, void 0, void 0, function* () { - const allCollections = yield figma.variables.getLocalVariableCollectionsAsync(); - const collection = allCollections.find(c => c.name === collectionName); - if (!collection) { - Logger.send('variables', { variables: [] }); - return; - } - const variables = (yield Promise.all(collection.variableIds - .map((id) => __awaiter(this, void 0, void 0, function* () { - const v = yield figma.variables.getVariableByIdAsync(id); - return v ? { name: v.name, type: v.resolvedType } : null; - })))) - .filter(Boolean); - Logger.send('variables', { variables }); - }); -} -// ============================================================================ -// SECTION 14: CLEAR FUNCTIONS -// ============================================================================ -function clearVariables() { - return __awaiter(this, void 0, void 0, function* () { - Logger.log('🗑️ Clearing all variables...'); - try { - let deletedCollections = 0; - let deletedVariables = 0; - for (const collection of yield figma.variables.getLocalVariableCollectionsAsync()) { - for (const varId of collection.variableIds) { - const variable = yield figma.variables.getVariableByIdAsync(varId); - if (variable) { - variable.remove(); - deletedVariables++; - } - } - collection.remove(); - deletedCollections++; - } - Logger.log(`✅ Cleared ${deletedCollections} collections, ${deletedVariables} variables`); - Logger.send('clear_complete', { message: `${deletedCollections} collections, ${deletedVariables} variables` }); - } - catch (e) { - Logger.log(`❌ Clear variables error: ${e}`); - Logger.send('error', { message: `Failed to clear variables: ${e}` }); - } - }); -} -function clearStyles() { - return __awaiter(this, void 0, void 0, function* () { - Logger.log('🗑️ Clearing all styles...'); - try { - let deletedStyles = 0; - for (const style of yield figma.getLocalPaintStylesAsync()) { - style.remove(); - deletedStyles++; - } - for (const style of yield figma.getLocalTextStylesAsync()) { - style.remove(); - deletedStyles++; - } - for (const style of yield figma.getLocalEffectStylesAsync()) { - style.remove(); - deletedStyles++; - } - for (const style of yield figma.getLocalGridStylesAsync()) { - style.remove(); - deletedStyles++; - } - Logger.log(`✅ Cleared ${deletedStyles} styles`); - Logger.send('clear_complete', { message: `${deletedStyles} styles` }); - } - catch (e) { - Logger.log(`❌ Clear styles error: ${e}`); - Logger.send('error', { message: `Failed to clear styles: ${e}` }); - } - }); -} -function clearAll() { - return __awaiter(this, void 0, void 0, function* () { - Logger.log('🗑️ Clearing everything...'); - try { - yield clearVariables(); - yield clearStyles(); - } - catch (e) { - Logger.log(`❌ Clear all error: ${e}`); - Logger.send('error', { message: `Failed to clear: ${e}` }); - } - }); -} -// ============================================================================ -// SECTION 15: MESSAGE HANDLER -// ============================================================================ -figma.ui.onmessage = (msg) => __awaiter(void 0, void 0, void 0, function* () { - switch (msg.type) { - case 'export': - yield exportVariables(msg.collections, msg.styleOptions); - break; - case 'import': - yield importVariables(msg.data, msg.options); - break; - case 'validate_import': - // Pre-import validation to check plan limits - try { - const importData = JSON.parse(msg.data); - const planOverride = msg.plan; - const validation = yield validateImportAgainstPlan(importData, planOverride); - Logger.send('validation_result', validation); - } - catch (e) { - Logger.send('validation_result', { - errors: [`Invalid JSON: ${e instanceof Error ? e.message : 'Parse error'}`], - canImport: false - }); - } - break; - case 'detect_plan': - // Detect current plan based on existing collections - const detectedPlan = yield detectCurrentPlan(); - Logger.send('plan_detected', detectedPlan); - break; - case 'clear_variables': - yield clearVariables(); - break; - case 'clear_styles': - yield clearStyles(); - break; - case 'clear_all': - yield clearAll(); - break; - case 'get_collections': - yield getCollections(); - break; - case 'get_variables': - yield getVariablesForCollection(msg.collection); - break; - case 'close': - figma.closePlugin(); - break; - } -}); diff --git a/variables-styles-extractor/backup/ui.html.backup b/variables-styles-extractor/backup/ui.html.backup deleted file mode 100644 index d6a78a0..0000000 --- a/variables-styles-extractor/backup/ui.html.backup +++ /dev/null @@ -1,3127 +0,0 @@ - - - - - - - Variables and Styles Extractor - - - - -
- - -
- - -
-
-
- Select Collections to Export - -
-
-
-
- Loading collections... -
-
-
- - -
- - - - - - -
- - -
-
Include Styles (Optional)
-
- - - - - -
-

Styles connected to variables will preserve their bindings

- -
- - - - -
- -

Exports variables and selected styles to JSON format

-
- - -
- - -
-
-
Import JSON
-
-
📁
-
Drop JSON file here or click to browse
- - -
-
Or paste JSON
- -
- - - - - - - -
-
Clear Before Import (Optional)
-

⚠️ Warning: These actions cannot be undone!

-
- - -
- -
- -
-
Import Options
- - - -
- -
- -

Paste or drop a JSON file to preview

-
-
- - -
-
Activity Log
-
-
- --:--:-- - Variables Extractor initialized -
-
-
- - - - - - - - - - diff --git a/variables-styles-extractor/backup/ui.html.v1-backup b/variables-styles-extractor/backup/ui.html.v1-backup deleted file mode 100644 index 407d654..0000000 --- a/variables-styles-extractor/backup/ui.html.v1-backup +++ /dev/null @@ -1,2565 +0,0 @@ - - - - - - - Variables and Styles Extractor - - - - -
- - -
- - -
-
-
- Select Collections to Export - -
-
-
-
- Loading collections... -
-
-
- - -
-
- - -
-
Include Styles (Optional)
-
- - - - -
-

Styles connected to variables will preserve their bindings

-
- - - - -
- -

Exports variables and selected styles to JSON format

-
- - -
- - -
-
-
Import JSON
-
-
📁
-
Drop JSON file here or click to browse
- - -
-
Or paste JSON
- -
- - - - - - - -
-
Clear Before Import (Optional)
-

⚠️ Warning: These actions cannot be undone!

-
- - -
- -
- -
-
Import Options
- - - -
- -
- -

Paste or drop a JSON file to preview

-
-
- - -
-
Activity Log
-
-
- --:--:-- - Variables Extractor initialized -
-
-
- - - - - - - - - - diff --git a/variables-styles-extractor/bmc-button-lg.png b/variables-styles-extractor/bmc-button-lg.png deleted file mode 100644 index 21ed36b7efabacc50ffb03203ceee96a570a43ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4811 zcma)=`8O1d_s1ve3}MD(4VeYW^4KD>&XBS1lqIrn*|J1g8iTRK*w<_k#!hwze)}5A(^PS-E6x71)3Z1h)v_p)c<8*Ri&rNrKg4J`68m1eN zuC-(D4z!l@bf98PO>}lz!m#R^J1t^JhWaUK0$2gbzy{Jnqb8zMU`2%BvO?i?juRfx zyx&=Hph4~xsayU$HEDju|G>7q&nEqMvcGI@^KlOFd`K&6w}&BG5$odt>g*{Mn|tAq zuUDAK%T2xoJTM8F(+dt#+#oN*-4A>$@^ja_A6erjB7WZ{qzc3$Yc)xnwJYJA6fK_e zr;rTM=;5sPhffWf^Ke6%vj$=m4TShw`TfSyGQ>*pP zT@f-Hw4^2cE#`uqO1iWO@SyiR6 z@vpU930}hLXPqoh{sq{;upYgBBXJDf8P-u6%5wRO`Qai+IcjCa>~Hy*Gs^l3f-N)M z(U^Q&QwIMh2!(0OmcRsY2@Jkbx6mV2AT|4F-l&X0$qx-vk?Ii!J*P{#S<;+Ve2sz3 z-;bd|7<7)i6U-InlRL~_4;Pocp2=|R5v2j!k*ybU^=!Q(FexCv(EZpjPu2`;bae3} z2$!O-_`d&)Z$%<$U`t*w@^0N$DF#zOxFcO*>#SSam$jCxt>~~ zUV}nixc#FWAO0$V>~WVSRY5Rrn6;_01nTYg4;LT4?J-uT1oho8>mQNFBNN7p&je7e zc9i#u$k#;siL3DJ%%pJjHk)siBle-aha#?X zxUQO#IxgN4nOJgkqT}_}y4G-H_}PyjK-nf_!pF}b`{!a)l>)$-71R?$h>wfI5 zL}j8pbnBIxFC{Jm;oo$`pAhstxn{V)dLpoLHhjSBL4o)a$mw*7%&Vu14j(Rei7|YF zPs3=xVv$yR1wu+UcKInhoIzkc|1`MkK_)nTD-+YV#_($-ojP@o9)T-Pe%WItZ?YVp z-hMb(R`pt-qPufZd3W13KQ8YuL}wB(DvyS_+M2)U^Vk$dwoY;`G!FO+uUCgQuXm#H zR}Bp!!+k(!vrZ?LHX4=eBH{r%Q$!~QJKXd^@SLRE=PAcLs>$O7>0%WbnYn4M0YyEo zA$_yk45xQ#rmwQV3uyA$)S#P}lD}PuGekL&pdhzTZ+Zr~KL$5=oK^h6PC=Q&cveVj zwOGxCdgt`x98HCfPW6J7CbRHN=R9Cikk0}TaDnr- zeYY>|{}8@vbVG0VOQACNd+&mEBVDTrLLX3#x3J6p=HuDz{ki7!^_1(VQMkRNePQ;! zL(qerX9AxLk?s|Gj&ftKk_o9z0ko#y#~ff(S_Zm(EV2h635ID3PY)#eC$=^ivqXj` zKAd^mc3c%f;y{F(H>%t%>~RAg1Z7mWR}K7rLVw+**ch*md+rUCBx)G!=UAdo3%VMh zO@Cn~DEyjq_@h6}do9s={3r~Bb2OI^IB)i#H7=EINY(;msBu)np=lOl4Tv7MMWDa< zcs)OC=ICosIx;QA=2kBYXZW((-1iX^4U8P^6h-a|Qf#2`qHpit3@&L*`MEAm25nZZ zmA}EY8Lj#xE4Nv-HHEyun+Z@h9OMkLDTNF&o{4&8%L5r<)!;;ak=v5D&o0yyebppI zCs*h@e9F4Q%C0OhMNq^Nx?`49mwO$usG$$Ke*Y_@Lw0(d@)8nsU{U(W(6jalshmK~ z_g+TG9s*Y2+SBs=dvd5o6NXOit

S71G_&7jPUgJ>e|F+uB1dk>&X);VcIhO>(Bf z7}KL8sqi#mm8XQkHKpR5?kB2J-cd<=a2X#XwB>=9b)^6i|b+4{%j9hkf#mMaMn6SWlJ)5kn9* z??*GgdaY6x3jOwgAH{^<*H(r#Fn3Fx#H(I_J*hJhIt6y+gp7Z26%Tu(b2mIambdUW zpYO(PO!xNrU0cAppsmUgZA+!~p<}}N73r-7qvbvqWj|H2TUG)}t4G4RqjBmyw_1fb*if3|F+um7x|J_zba_iiwLoh%CyC{Tx%!?SsUpGsz`Q+$ zfMZ#n*%MYQJn$t1S!GFD13?y(qfl5V(rUxH_H|3qR_4a*Z_08T3UN9J*;YL!;;>m>f3)CP_|6URIvP(NL)v5HWyg_pXZP5@IQF9BiZk409 z6Nn>weav-p)a{?U#`V_W%4Gng73^NhMvr`$zjT1s%tNq;zZ?sIYraS(0RPhSLQ@34 z)X4z#G-HzMPoKMyryxNy68X=r*6<@TkMo&{I%iDJIR8GH@ga_hbmW#oyi5#d!l~UOyV^8742^f}gm6ryAT$ z5|9=}Jzqhj*8KtcD%g2_{-H~Lx*cMJ`y2wLgw{FbESNZPGR8JSGJM9o9vAq6EE?He z>An3rCf7Ge0yiQ|^NNq&xVKz2L{a}areA9pD}69+oLe+n5fNxkDn3*=cG5L1VrbX^ zo)tKti?M{t&rPJ=Ov@@rw|d*ctk zHrKe>8LHWmp&&8({`Zy|dK_l+1`S&ig76UkC%yo2ty1v3C)jF`%GttuqNzmYI~<(5 z@g7acuDvWuJ+bHh)ilW-8Q-}yotGa%iFxdC?|$E+potY`5{+>vV_orAxBYrO z8xyI4bPDSSUOTzQuO*P-OeRzzaL0P?x(38Mi&^sbOH{uz;)1GK`qy+vWPCBX#GD`U zwGwK9b*D`x3e}?O&>%w1^Gt>m$@))RY4NwDy`zY8o;IH?viS!}Ct3D{D3@^dY*4G2 zGKJVaLVmZ$X9K}M+6&Va)BT_u5-Ea(w2gn@9ftEonQyGxJq5xBHS3bPQ`I1;xBuOA zv4(Io6b=NWz1vpDw&Vxx$d7jie;|knYsbas8f)&S=2$KiB4%9DBg((#r)?usn+(2k8`Afj zT#{PsY01wC0nmX#T)#$JqXK$|SafCL15S0U#%?;19gTr}`sDPguG|9Xx2OgoGjc{w zLIQCOqPOw*;T>k=*Zm}w9jM#1z`HTVao%>Plsg5YA@4s|bZ?P>_PW(Tu&0U02M4D7 z;he-cUo$3_dwz{}=y+8&SnXbj7>whcKGE&W9I`}S!CPc!O5;Vd(^~j19h{}+dLUFe z1H?Wmpo=#j5b6eY(U?$jl>}U0A_1+apV(FPa119Ik0+{CF+7e-w`~^+*PfaObAF=L zLgn_oy0?IO2@tt5n;Oo{KW5B`A_&Y*47LbL4ZEU?QPGs^$#S5Ba&msMd%o2v-#eB8 z{+kJe4FZ4j9+Eu7Zd*z)`C?^Mufy!*Iev`ifoBpPu4s|hn#J5wJ2tMuRhB@%9Sbe4 zr=@J+@U7#P33#FsqVITjT{T-NP4q82%F>)`AfcahDCVk$Cu}juTl)_umN*W3I%4~e z&`H=mZAXAi2H`@+{M32ND07jiqVGq8eM>gaAQICX4d$4KGCGx1aomA4>P4=lk2D$Q zI7pcZ4+Bq|7?RT}jh7O~3&1~zs6IR}n-vr=tk{Q2KdN5xNkV6;gjPnakaX}%!_ z$spz=UZJrSmd%^LeH)f)bPKO9dv{FFd{onaYn-NEFuhCtbSe|O7Y;3FnLlM$uGltr zF#gxftdnDwwwjM`cg_IyIZ-WuTPh!Rog?(PRC@;67gu62&&+F>1`l?AU(v&eLV`gu<$w`iN@x_f=>1Uh{E9r#D`&7~=^m_!_V6FRKQ3X9hE+ z6zr$UlpCF(nhUDkb$;+2 zCxYAyn)i6_kRr*G7d!Hd(9&to=%?<-NO^+89D*RMODPF+lN-iBeokyWuCxpW^ThlY zE9R5Z`!*R~vOf|t`5%xR|DgvFc8<`~9z@ThV0znH(k|$=8&kbY`aHBnOR2PN{*9jr znZ1_prQ+##P4d$92)cSHTo!J3d6LW1_)!jS*VIPm!ThN)gp#szvEY7m*mYQsZZLC! z*YV}2ge3_T2DXKthsI(I5UmIF=FpZ$L-q_tAGG#EMPWT2b)Nr8AEAn{o~4`dkeBGx zlagC;#^il=#)mKW&T7XPkggqzs!sC#y?bnwdk}M`j0CX}%*LHp_W#pX{_;2h2EWyh UHOa%p&V?R;o{q701Ih{ee}McV9RL6T diff --git a/variables-styles-extractor/bmc-button.png b/variables-styles-extractor/bmc-button.png deleted file mode 100644 index 21ed36b7efabacc50ffb03203ceee96a570a43ad..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4811 zcma)=`8O1d_s1ve3}MD(4VeYW^4KD>&XBS1lqIrn*|J1g8iTRK*w<_k#!hwze)}5A(^PS-E6x71)3Z1h)v_p)c<8*Ri&rNrKg4J`68m1eN zuC-(D4z!l@bf98PO>}lz!m#R^J1t^JhWaUK0$2gbzy{Jnqb8zMU`2%BvO?i?juRfx zyx&=Hph4~xsayU$HEDju|G>7q&nEqMvcGI@^KlOFd`K&6w}&BG5$odt>g*{Mn|tAq zuUDAK%T2xoJTM8F(+dt#+#oN*-4A>$@^ja_A6erjB7WZ{qzc3$Yc)xnwJYJA6fK_e zr;rTM=;5sPhffWf^Ke6%vj$=m4TShw`TfSyGQ>*pP zT@f-Hw4^2cE#`uqO1iWO@SyiR6 z@vpU930}hLXPqoh{sq{;upYgBBXJDf8P-u6%5wRO`Qai+IcjCa>~Hy*Gs^l3f-N)M z(U^Q&QwIMh2!(0OmcRsY2@Jkbx6mV2AT|4F-l&X0$qx-vk?Ii!J*P{#S<;+Ve2sz3 z-;bd|7<7)i6U-InlRL~_4;Pocp2=|R5v2j!k*ybU^=!Q(FexCv(EZpjPu2`;bae3} z2$!O-_`d&)Z$%<$U`t*w@^0N$DF#zOxFcO*>#SSam$jCxt>~~ zUV}nixc#FWAO0$V>~WVSRY5Rrn6;_01nTYg4;LT4?J-uT1oho8>mQNFBNN7p&je7e zc9i#u$k#;siL3DJ%%pJjHk)siBle-aha#?X zxUQO#IxgN4nOJgkqT}_}y4G-H_}PyjK-nf_!pF}b`{!a)l>)$-71R?$h>wfI5 zL}j8pbnBIxFC{Jm;oo$`pAhstxn{V)dLpoLHhjSBL4o)a$mw*7%&Vu14j(Rei7|YF zPs3=xVv$yR1wu+UcKInhoIzkc|1`MkK_)nTD-+YV#_($-ojP@o9)T-Pe%WItZ?YVp z-hMb(R`pt-qPufZd3W13KQ8YuL}wB(DvyS_+M2)U^Vk$dwoY;`G!FO+uUCgQuXm#H zR}Bp!!+k(!vrZ?LHX4=eBH{r%Q$!~QJKXd^@SLRE=PAcLs>$O7>0%WbnYn4M0YyEo zA$_yk45xQ#rmwQV3uyA$)S#P}lD}PuGekL&pdhzTZ+Zr~KL$5=oK^h6PC=Q&cveVj zwOGxCdgt`x98HCfPW6J7CbRHN=R9Cikk0}TaDnr- zeYY>|{}8@vbVG0VOQACNd+&mEBVDTrLLX3#x3J6p=HuDz{ki7!^_1(VQMkRNePQ;! zL(qerX9AxLk?s|Gj&ftKk_o9z0ko#y#~ff(S_Zm(EV2h635ID3PY)#eC$=^ivqXj` zKAd^mc3c%f;y{F(H>%t%>~RAg1Z7mWR}K7rLVw+**ch*md+rUCBx)G!=UAdo3%VMh zO@Cn~DEyjq_@h6}do9s={3r~Bb2OI^IB)i#H7=EINY(;msBu)np=lOl4Tv7MMWDa< zcs)OC=ICosIx;QA=2kBYXZW((-1iX^4U8P^6h-a|Qf#2`qHpit3@&L*`MEAm25nZZ zmA}EY8Lj#xE4Nv-HHEyun+Z@h9OMkLDTNF&o{4&8%L5r<)!;;ak=v5D&o0yyebppI zCs*h@e9F4Q%C0OhMNq^Nx?`49mwO$usG$$Ke*Y_@Lw0(d@)8nsU{U(W(6jalshmK~ z_g+TG9s*Y2+SBs=dvd5o6NXOit

S71G_&7jPUgJ>e|F+uB1dk>&X);VcIhO>(Bf z7}KL8sqi#mm8XQkHKpR5?kB2J-cd<=a2X#XwB>=9b)^6i|b+4{%j9hkf#mMaMn6SWlJ)5kn9* z??*GgdaY6x3jOwgAH{^<*H(r#Fn3Fx#H(I_J*hJhIt6y+gp7Z26%Tu(b2mIambdUW zpYO(PO!xNrU0cAppsmUgZA+!~p<}}N73r-7qvbvqWj|H2TUG)}t4G4RqjBmyw_1fb*if3|F+um7x|J_zba_iiwLoh%CyC{Tx%!?SsUpGsz`Q+$ zfMZ#n*%MYQJn$t1S!GFD13?y(qfl5V(rUxH_H|3qR_4a*Z_08T3UN9J*;YL!;;>m>f3)CP_|6URIvP(NL)v5HWyg_pXZP5@IQF9BiZk409 z6Nn>weav-p)a{?U#`V_W%4Gng73^NhMvr`$zjT1s%tNq;zZ?sIYraS(0RPhSLQ@34 z)X4z#G-HzMPoKMyryxNy68X=r*6<@TkMo&{I%iDJIR8GH@ga_hbmW#oyi5#d!l~UOyV^8742^f}gm6ryAT$ z5|9=}Jzqhj*8KtcD%g2_{-H~Lx*cMJ`y2wLgw{FbESNZPGR8JSGJM9o9vAq6EE?He z>An3rCf7Ge0yiQ|^NNq&xVKz2L{a}areA9pD}69+oLe+n5fNxkDn3*=cG5L1VrbX^ zo)tKti?M{t&rPJ=Ov@@rw|d*ctk zHrKe>8LHWmp&&8({`Zy|dK_l+1`S&ig76UkC%yo2ty1v3C)jF`%GttuqNzmYI~<(5 z@g7acuDvWuJ+bHh)ilW-8Q-}yotGa%iFxdC?|$E+potY`5{+>vV_orAxBYrO z8xyI4bPDSSUOTzQuO*P-OeRzzaL0P?x(38Mi&^sbOH{uz;)1GK`qy+vWPCBX#GD`U zwGwK9b*D`x3e}?O&>%w1^Gt>m$@))RY4NwDy`zY8o;IH?viS!}Ct3D{D3@^dY*4G2 zGKJVaLVmZ$X9K}M+6&Va)BT_u5-Ea(w2gn@9ftEonQyGxJq5xBHS3bPQ`I1;xBuOA zv4(Io6b=NWz1vpDw&Vxx$d7jie;|knYsbas8f)&S=2$KiB4%9DBg((#r)?usn+(2k8`Afj zT#{PsY01wC0nmX#T)#$JqXK$|SafCL15S0U#%?;19gTr}`sDPguG|9Xx2OgoGjc{w zLIQCOqPOw*;T>k=*Zm}w9jM#1z`HTVao%>Plsg5YA@4s|bZ?P>_PW(Tu&0U02M4D7 z;he-cUo$3_dwz{}=y+8&SnXbj7>whcKGE&W9I`}S!CPc!O5;Vd(^~j19h{}+dLUFe z1H?Wmpo=#j5b6eY(U?$jl>}U0A_1+apV(FPa119Ik0+{CF+7e-w`~^+*PfaObAF=L zLgn_oy0?IO2@tt5n;Oo{KW5B`A_&Y*47LbL4ZEU?QPGs^$#S5Ba&msMd%o2v-#eB8 z{+kJe4FZ4j9+Eu7Zd*z)`C?^Mufy!*Iev`ifoBpPu4s|hn#J5wJ2tMuRhB@%9Sbe4 zr=@J+@U7#P33#FsqVITjT{T-NP4q82%F>)`AfcahDCVk$Cu}juTl)_umN*W3I%4~e z&`H=mZAXAi2H`@+{M32ND07jiqVGq8eM>gaAQICX4d$4KGCGx1aomA4>P4=lk2D$Q zI7pcZ4+Bq|7?RT}jh7O~3&1~zs6IR}n-vr=tk{Q2KdN5xNkV6;gjPnakaX}%!_ z$spz=UZJrSmd%^LeH)f)bPKO9dv{FFd{onaYn-NEFuhCtbSe|O7Y;3FnLlM$uGltr zF#gxftdnDwwwjM`cg_IyIZ-WuTPh!Rog?(PRC@;67gu62&&+F>1`l?AU(v&eLV`gu<$w`iN@x_f=>1Uh{E9r#D`&7~=^m_!_V6FRKQ3X9hE+ z6zr$UlpCF(nhUDkb$;+2 zCxYAyn)i6_kRr*G7d!Hd(9&to=%?<-NO^+89D*RMODPF+lN-iBeokyWuCxpW^ThlY zE9R5Z`!*R~vOf|t`5%xR|DgvFc8<`~9z@ThV0znH(k|$=8&kbY`aHBnOR2PN{*9jr znZ1_prQ+##P4d$92)cSHTo!J3d6LW1_)!jS*VIPm!ThN)gp#szvEY7m*mYQsZZLC! z*YV}2ge3_T2DXKthsI(I5UmIF=FpZ$L-q_tAGG#EMPWT2b)Nr8AEAn{o~4`dkeBGx zlagC;#^il=#)mKW&T7XPkggqzs!sC#y?bnwdk}M`j0CX}%*LHp_W#pX{_;2h2EWyh UHOa%p&V?R;o{q701Ih{ee}McV9RL6T diff --git a/variables-styles-extractor/code.js b/variables-styles-extractor/code.js index 6f73acb..063cbf5 100644 --- a/variables-styles-extractor/code.js +++ b/variables-styles-extractor/code.js @@ -8,2758 +8,4 @@ * @version 2.0.0 * @author Tushar Kant Naik * @website https://tusharkantnaik.com - */ -// JSF-AV Compliant Architecture -// v2.0.0: Wide 4-column layout (1200x628px content area, 680px with Figma title bar) -figma.showUI(__html__, { - width: 1200, - height: 628, - themeColors: true, - title: '☕️ Variables & Styles Extractor v2.0.0' -}); -const Result = { - ok: (value) => ({ ok: true, value }), - err: (error) => ({ ok: false, error }), -}; -// ============================================================================ -// SECTION 3: UTILITY FUNCTIONS (JSF Rule 4.15 - DRY) -// ============================================================================ -const Logger = { - log(message, data) { - console.log(`[Variables Extractor] ${message}`, data || ''); - figma.ui.postMessage({ type: 'log', message, data }); - }, - send(type, data) { - figma.ui.postMessage({ type, data }); - } -}; -// Plan limits by Figma subscription tier (verified from Figma documentation) -const PLAN_LIMITS = { - starter: { - maxModesPerCollection: 1, - canPublishLibraries: false, - hasVariableRestApi: false - }, - professional: { - maxModesPerCollection: 10, - canPublishLibraries: true, - hasVariableRestApi: false - }, - organization: { - maxModesPerCollection: 20, - canPublishLibraries: true, - hasVariableRestApi: false - }, - enterprise: { - maxModesPerCollection: Infinity, - canPublishLibraries: true, - hasVariableRestApi: true - } -}; -// Maximum variables per collection (all plans) -const MAX_VARIABLES_PER_COLLECTION = 5000; -// Plan detection: Figma API doesn't expose plan directly, so we infer from existing modes -async function detectCurrentPlan() { - const collections = await figma.variables.getLocalVariableCollectionsAsync(); - let maxModesFound = 1; - for (const collection of collections) { - if (collection.modes.length > maxModesFound) { - maxModesFound = collection.modes.length; - } - } - // Infer plan based on highest mode count found - let inferredPlan; - if (maxModesFound > 20) { - inferredPlan = 'enterprise'; - } - else if (maxModesFound > 10) { - inferredPlan = 'organization'; - } - else if (maxModesFound > 1) { - inferredPlan = 'professional'; - } - else { - // Can't distinguish starter from others with 1 mode, assume professional - // User can override in UI - inferredPlan = 'professional'; - } - return Object.assign({ plan: inferredPlan }, PLAN_LIMITS[inferredPlan]); -} -// Validate import data against plan limits -async function validateImportAgainstPlan(importData, planOverride) { - const currentPlan = planOverride - ? Object.assign({ plan: planOverride }, PLAN_LIMITS[planOverride]) : await detectCurrentPlan(); - const collections = await figma.variables.getLocalVariableCollectionsAsync(); - const existingMaxModes = collections.reduce((max, col) => Math.max(max, col.modes.length), 0); - const existingTotalVars = (await figma.variables.getLocalVariablesAsync()).length; - // Analyze import data - it's an array of collection exports and possibly _styles - const importCollections = []; - for (const item of importData) { - // Skip _styles entries - if ('_styles' in item) - continue; - importCollections.push(item); - } - let importingMaxModes = 0; - let importingTotalVars = 0; - const collectionsExceedingModeLimit = []; - for (const colExport of importCollections) { - // Each collection export is { "CollectionName": { modes: {...} } } - const colName = Object.keys(colExport)[0]; - const colData = colExport[colName]; - if (!colData || !colData.modes) - continue; - const modeCount = Object.keys(colData.modes).length; - if (modeCount > importingMaxModes) { - importingMaxModes = modeCount; - } - if (modeCount > currentPlan.maxModesPerCollection) { - collectionsExceedingModeLimit.push(`"${colName}" (${modeCount} modes, limit: ${currentPlan.maxModesPerCollection === Infinity ? '∞' : currentPlan.maxModesPerCollection})`); - } - // Count variables in first mode (they're the same across modes) - const firstMode = Object.values(colData.modes)[0]; - if (firstMode) { - importingTotalVars += countNestedVariables(firstMode); - } - } - // Generate warnings and errors - const warnings = []; - const errors = []; - // Mode limits - not a hard error, UI will show mode selection - // Only warn, don't block - user can select which modes to import - if (collectionsExceedingModeLimit.length > 0) { - // This is handled by the UI with mode selection - // Don't add to errors, just track in collectionsExceedingModeLimit - } - // Check variable count per collection - this IS a hard limit - for (const colExport of importCollections) { - const colName = Object.keys(colExport)[0]; - const colData = colExport[colName]; - if (!colData || !colData.modes) - continue; - const firstMode = Object.values(colData.modes)[0]; - const varCount = firstMode ? countNestedVariables(firstMode) : 0; - if (varCount > MAX_VARIABLES_PER_COLLECTION) { - errors.push(`Collection "${colName}" has ${varCount} variables, exceeds limit of ${MAX_VARIABLES_PER_COLLECTION}`); - } - } - // Warnings for large imports - if (importingTotalVars > 1000) { - warnings.push(`Large import: ${importingTotalVars} variables. This may take a moment.`); - } - if (importCollections.length > 10) { - warnings.push(`Importing ${importCollections.length} collections. Consider importing in batches.`); - } - // Detect library dependencies (variables that reference external collections) - const libraryCollections = new Set(); - let libraryVarCount = 0; - for (const colExport of importCollections) { - const colName = Object.keys(colExport)[0]; - const colData = colExport[colName]; - if (!colData || !colData.modes) - continue; - for (const modeName of Object.keys(colData.modes)) { - const modeData = colData.modes[modeName]; - const variables = flattenVariables(modeData, ''); - for (const { value } of variables) { - if (value.$libraryRef && value.$collectionName) { - libraryCollections.add(value.$collectionName); - libraryVarCount++; - } - } - } - } - // Detect font dependencies from text styles - const fontDeps = []; - let fontStyleCount = 0; - // Check for _styles in import data - for (const item of importData) { - if ('_styles' in item) { - const stylesData = item._styles; - if (stylesData.textStyles) { - for (const textStyle of stylesData.textStyles) { - fontStyleCount++; - const fontKey = `${textStyle.fontFamily}|${textStyle.fontStyle}`; - if (!fontDeps.some(f => `${f.family}|${f.style}` === fontKey)) { - fontDeps.push({ family: textStyle.fontFamily, style: textStyle.fontStyle }); - } - } - } - } - } - // canImport is true if no hard errors (variable count) - // Mode limit exceedance is handled by UI with mode selection - return Object.assign(Object.assign({ currentPlan, existing: { - collections: collections.length, - maxModesInAnyCollection: existingMaxModes, - totalVariables: existingTotalVars - }, importing: { - collections: importCollections.length, - maxModesInAnyCollection: importingMaxModes, - totalVariables: importingTotalVars, - collectionsExceedingModeLimit - }, warnings, - errors, canImport: errors.length === 0 }, (libraryCollections.size > 0 && { - libraryDependencies: { - variableCount: libraryVarCount, - collections: Array.from(libraryCollections) - } - })), (fontDeps.length > 0 && { - fontDependencies: { - styleCount: fontStyleCount, - fonts: fontDeps - } - })); -} -// Helper to count nested variables in a mode object -function countNestedVariables(obj, count = 0) { - for (const [, value] of Object.entries(obj)) { - if (value && typeof value === 'object') { - if ('$type' in value && '$value' in value) { - // This is a variable - count++; - } - else { - // Nested group - count = countNestedVariables(value, count); - } - } - } - return count; -} -const MathUtils = { - round2(value) { - return Math.round(value * 100) / 100; - }, - clamp(value, min, max) { - return Math.max(min, Math.min(max, value)); - }, - toHexByte(value) { - return Math.round(value * 255).toString(16).padStart(2, '0'); - }, - fromHexByte(hex) { - return parseInt(hex, 16) / 255; - } -}; -// ============================================================================ -// SECTION 4: COLOR CONVERSION MODULE (JSF Rule 4.7 - Single Responsibility) -// ============================================================================ -// Shared hue calculation - eliminates duplication between HSL/HSB -function calculateHue(r, g, b, max, min) { - if (max === min) - return 0; - const d = max - min; - let h = 0; - switch (max) { - case r: - h = ((g - b) / d + (g < b ? 6 : 0)) / 6; - break; - case g: - h = ((b - r) / d + 2) / 6; - break; - case b: - h = ((r - g) / d + 4) / 6; - break; - } - return Math.round(h * 360); -} -const ColorConverter = { - // Figma RGB (0-1) → Hex - toHex(color) { - const hex = '#' + - MathUtils.toHexByte(color.r) + - MathUtils.toHexByte(color.g) + - MathUtils.toHexByte(color.b); - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return hex + MathUtils.toHexByte(alpha); - } - return hex; - }, - // Figma RGB (0-1) → RGB (0-255) - toRgb255(color) { - const result = { - r: Math.round(color.r * 255), - g: Math.round(color.g * 255), - b: Math.round(color.b * 255) - }; - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return Object.assign(Object.assign({}, result), { a: MathUtils.round2(alpha) }); - } - return result; - }, - // Figma RGB (0-1) → CSS string - toCss(color) { - const r = Math.round(color.r * 255); - const g = Math.round(color.g * 255); - const b = Math.round(color.b * 255); - const alpha = color.a; - const a = alpha !== undefined ? MathUtils.round2(alpha) : 1; - return a < 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`; - }, - // Figma RGB (0-1) → HSL - toHsl(color) { - const { r, g, b } = color; - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const l = (max + min) / 2; - let s = 0; - if (max !== min) { - const d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - } - const result = { - h: calculateHue(r, g, b, max, min), - s: Math.round(s * 100), - l: Math.round(l * 100) - }; - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return Object.assign(Object.assign({}, result), { a: MathUtils.round2(alpha) }); - } - return result; - }, - // Figma RGB (0-1) → HSB/HSV - toHsb(color) { - const { r, g, b } = color; - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const s = max === 0 ? 0 : (max - min) / max; - const result = { - h: calculateHue(r, g, b, max, min), - s: Math.round(s * 100), - b: Math.round(max * 100) - }; - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return Object.assign(Object.assign({}, result), { a: MathUtils.round2(alpha) }); - } - return result; - }, - // Master export function - all formats - toAllFormats(color) { - return { - hex: this.toHex(color), - rgb: this.toRgb255(color), - css: this.toCss(color), - hsl: this.toHsl(color), - hsb: this.toHsb(color) - }; - } -}; -// ============================================================================ -// SECTION 4B: NAMING CONVENTION CONVERTER -// ============================================================================ -const NamingConverter = { - // Convert name to specified convention - convert(name, convention) { - if (convention === 'original') - return name; - // Split by common separators (space, /, -, _) - const words = name - .replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase - .split(/[\s\/\-_]+/) - .filter(w => w.length > 0) - .map(w => w.toLowerCase()); - if (words.length === 0) - return name; - switch (convention) { - case 'camelCase': - return words[0] + words.slice(1).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''); - case 'kebab-case': - return words.join('-'); - case 'snake_case': - return words.join('_'); - default: - return name; - } - }, - // Convert a variable path (e.g., "Colors/Primary/Base" → "colors/primary/base" or "colors.primary.base") - convertPath(path, convention) { - if (convention === 'original') - return path; - return path - .split('/') - .map(part => this.convert(part, convention)) - .join('/'); - }, - // Convert collection name - convertCollectionName(name, convention) { - return this.convert(name, convention); - }, - // Convert mode name - convertModeName(name, convention) { - return this.convert(name, convention); - }, - // Store original names for round-trip - adds $originalName field - addOriginalName(name, convention) { - if (convention === 'original') { - return { converted: name }; - } - const converted = this.convert(name, convention); - if (converted === name) { - return { converted: name }; - } - return { converted, original: name }; - } -}; -// Helper function to resolve alias value recursively -async function resolveAliasValue(variable, preferredModeId, maxDepth = 10) { - if (maxDepth <= 0) { - Logger.log(`⚠️ Max alias resolution depth reached for ${variable.name}`); - return ''; - } - // Try to get value for preferred mode, fallback to first available mode - let value = variable.valuesByMode[preferredModeId]; - if (value === undefined) { - const modeIds = Object.keys(variable.valuesByMode); - if (modeIds.length > 0) { - value = variable.valuesByMode[modeIds[0]]; - } - } - if (value === undefined) { - return ''; - } - // If it's another alias, resolve recursively - if (isVariableAlias(value)) { - const nextVar = await figma.variables.getVariableByIdAsync(value.id); - if (nextVar) { - return resolveAliasValue(nextVar, preferredModeId, maxDepth - 1); - } - return ''; - } - // Return the raw value - return value; -} -// ============================================================================ -// SECTION 4C: W3C DESIGN TOKENS CONVERTER -// ============================================================================ -// W3C Design Tokens type mapping -// https://design-tokens.github.io/community-group/format/ -const W3C_TYPE_MAP = { - 'color': 'color', - 'float': 'number', - 'string': 'string', - 'boolean': 'boolean' -}; -const W3CConverter = { - // Convert Figma color to W3C format (hex with alpha) - colorToW3C(color) { - // W3C uses hex format, including alpha - return color.hex; - }, - // Convert Figma type to W3C type - typeToW3C(figmaType) { - return W3C_TYPE_MAP[figmaType] || 'string'; - }, - // Convert export value to W3C format - valueToW3C(value, isAlias = false) { - const token = { - $value: '', - $type: this.typeToW3C(value.$type) - }; - // Handle alias references - W3C uses {path.to.token} format - if (isAlias && typeof value.$value === 'string' && value.$value.startsWith('{')) { - token.$value = value.$value; - } - else if (value.$type === 'color' && typeof value.$value === 'object') { - // Color value - use hex - token.$value = value.$value.hex; - } - else { - token.$value = value.$value; - } - // Add description if present - if (value.$description) { - token.$description = value.$description; - } - // Add Figma-specific metadata in extensions - if (value.$scopes && value.$scopes.length > 0 && !value.$scopes.includes('ALL_SCOPES')) { - token.$extensions = { - 'com.figma': { - scopes: value.$scopes - } - }; - } - return token; - }, - // Convert collection export to W3C format - collectionToW3C(collectionName, modes, namingConvention, originalName) { - const group = {}; - // Add metadata as $description - if (originalName && originalName !== collectionName) { - group.$description = `Figma collection: ${originalName}`; - } - // For W3C, we typically flatten modes or use first mode - // If multiple modes, create mode groups - const modeNames = Object.keys(modes); - if (modeNames.length === 1) { - // Single mode - flatten directly - this.addTokensToGroup(group, modes[modeNames[0]], namingConvention); - } - else { - // Multiple modes - create mode subgroups - for (const modeName of modeNames) { - const convertedModeName = NamingConverter.convertModeName(modeName, namingConvention); - group[convertedModeName] = {}; - this.addTokensToGroup(group[convertedModeName], modes[modeName], namingConvention); - } - } - return group; - }, - // Recursively add tokens to a group - addTokensToGroup(group, variables, namingConvention) { - for (const [key, value] of Object.entries(variables)) { - const convertedKey = NamingConverter.convert(key, namingConvention); - if (isExportVariableValue(value)) { - // It's a token value - const isAlias = typeof value.$value === 'string' && value.$value.startsWith('{'); - group[convertedKey] = this.valueToW3C(value, isAlias); - } - else { - // It's a nested group - group[convertedKey] = {}; - this.addTokensToGroup(group[convertedKey], value, namingConvention); - } - } - }, - // Parse W3C token to Figma-compatible format - parseW3CToken(token) { - var _a, _b; - const figmaType = this.w3cTypeToFigma(token.$type); - const scopes = ((_b = (_a = token.$extensions) === null || _a === void 0 ? void 0 : _a['com.figma']) === null || _b === void 0 ? void 0 : _b.scopes) || ['ALL_SCOPES']; - // Handle color values - convert hex to full color object - let finalValue; - if (figmaType === 'color' && typeof token.$value === 'string') { - const rgba = ColorParser.parse(token.$value); - finalValue = ColorConverter.toAllFormats(rgba); - } - else if (typeof token.$value === 'string' || typeof token.$value === 'number' || typeof token.$value === 'boolean') { - finalValue = token.$value; - } - else { - // For complex objects, stringify them - finalValue = JSON.stringify(token.$value); - } - // Build result object with all properties at once (readonly-friendly) - const result = token.$description - ? { - $type: figmaType, - $value: finalValue, - $scopes: scopes, - $description: token.$description - } - : { - $type: figmaType, - $value: finalValue, - $scopes: scopes - }; - return result; - }, - // Convert W3C type back to Figma type - w3cTypeToFigma(w3cType) { - const map = { - 'color': 'color', - 'number': 'float', - 'dimension': 'float', - 'string': 'string', - 'boolean': 'boolean', - 'fontFamily': 'string', - 'fontWeight': 'float', - 'duration': 'string', - 'cubicBezier': 'string' - }; - return map[w3cType] || 'string'; - }, - // Detect if JSON is W3C format - isW3CFormat(data) { - if (typeof data !== 'object' || data === null) - return false; - // Check for W3C indicators: - // 1. Root level $type or $value - // 2. Nested objects with $value and $type - const obj = data; - // Check if any top-level key has $value (W3C token) - for (const key of Object.keys(obj)) { - const value = obj[key]; - if (typeof value === 'object' && value !== null) { - if ('$value' in value && '$type' in value) { - return true; - } - // Check one level deeper - for (const subKey of Object.keys(value)) { - const subValue = value[subKey]; - if (typeof subValue === 'object' && subValue !== null && '$value' in subValue) { - return true; - } - } - } - } - // Check if it's our Figma format (array with collection objects) - if (Array.isArray(data)) { - return false; // Figma format is array - } - return false; - }, - // Convert W3C format to Figma format for import - w3cToFigmaFormat(w3cData) { - const result = []; - for (const [collectionName, collectionGroup] of Object.entries(w3cData)) { - // Skip $ prefixed metadata keys - if (collectionName.startsWith('$')) - continue; - const collectionExport = { - [collectionName]: { - modes: { - 'Default': this.w3cGroupToNestedVars(collectionGroup) - } - } - }; - result.push(collectionExport); - } - return result; - }, - // Convert W3C group to nested variables - w3cGroupToNestedVars(group) { - const result = {}; - for (const [key, value] of Object.entries(group)) { - // Skip $ prefixed metadata - if (key.startsWith('$')) - continue; - if (this.isW3CToken(value)) { - // It's a token - result[key] = this.parseW3CToken(value); - } - else if (typeof value === 'object' && value !== null) { - // It's a group - result[key] = this.w3cGroupToNestedVars(value); - } - } - return result; - }, - // Check if object is a W3C token - isW3CToken(obj) { - return typeof obj === 'object' && obj !== null && '$value' in obj; - } -}; -// ============================================================================ -// SECTION 5: COLOR PARSING MODULE (JSF Rule 4.7) -// ============================================================================ -const HEX_REGEX_8 = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; -const HEX_REGEX_6 = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; -const RGBA_REGEX = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/i; -const HSLA_REGEX = /hsla?\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*(?:,\s*([\d.]+))?\s*\)/i; -const ColorParser = { - // Hex → Figma RGBA - fromHex(hex) { - const match8 = HEX_REGEX_8.exec(hex); - if (match8) { - return { - r: MathUtils.fromHexByte(match8[1]), - g: MathUtils.fromHexByte(match8[2]), - b: MathUtils.fromHexByte(match8[3]), - a: MathUtils.fromHexByte(match8[4]) - }; - } - const match6 = HEX_REGEX_6.exec(hex); - if (match6) { - return { - r: MathUtils.fromHexByte(match6[1]), - g: MathUtils.fromHexByte(match6[2]), - b: MathUtils.fromHexByte(match6[3]), - a: 1 - }; - } - return { r: 0, g: 0, b: 0, a: 1 }; - }, - // RGB (0-255) → Figma RGBA - fromRgb255(rgb) { - var _a; - return { - r: rgb.r / 255, - g: rgb.g / 255, - b: rgb.b / 255, - a: (_a = rgb.a) !== null && _a !== void 0 ? _a : 1 - }; - }, - // CSS string → Figma RGBA - fromCss(css) { - const rgbaMatch = RGBA_REGEX.exec(css); - if (rgbaMatch) { - return { - r: parseInt(rgbaMatch[1], 10) / 255, - g: parseInt(rgbaMatch[2], 10) / 255, - b: parseInt(rgbaMatch[3], 10) / 255, - a: rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1 - }; - } - const hslaMatch = HSLA_REGEX.exec(css); - if (hslaMatch) { - return this.fromHsl({ - h: parseInt(hslaMatch[1], 10), - s: parseInt(hslaMatch[2], 10), - l: parseInt(hslaMatch[3], 10), - a: hslaMatch[4] !== undefined ? parseFloat(hslaMatch[4]) : 1 - }); - } - return { r: 0, g: 0, b: 0, a: 1 }; - }, - // HSL → Figma RGBA - fromHsl(hsl) { - var _a, _b; - const h = hsl.h / 360; - const s = hsl.s / 100; - const l = hsl.l / 100; - if (s === 0) { - return { r: l, g: l, b: l, a: (_a = hsl.a) !== null && _a !== void 0 ? _a : 1 }; - } - const hue2rgb = (p, q, t) => { - const tt = t < 0 ? t + 1 : t > 1 ? t - 1 : t; - if (tt < 1 / 6) - return p + (q - p) * 6 * tt; - if (tt < 1 / 2) - return q; - if (tt < 2 / 3) - return p + (q - p) * (2 / 3 - tt) * 6; - return p; - }; - const q = l < 0.5 ? l * (1 + s) : l + s - l * s; - const p = 2 * l - q; - return { - r: hue2rgb(p, q, h + 1 / 3), - g: hue2rgb(p, q, h), - b: hue2rgb(p, q, h - 1 / 3), - a: (_b = hsl.a) !== null && _b !== void 0 ? _b : 1 - }; - }, - // HSB → Figma RGBA - fromHsb(hsb) { - var _a; - const h = hsb.h / 360; - const s = hsb.s / 100; - const v = hsb.b / 100; - const i = Math.floor(h * 6); - const f = h * 6 - i; - const p = v * (1 - s); - const q = v * (1 - f * s); - const t = v * (1 - (1 - f) * s); - const rgbMap = [ - [v, t, p], [q, v, p], [p, v, t], - [p, q, v], [t, p, v], [v, p, q] - ]; - const [r, g, b] = rgbMap[i % 6]; - return { r, g, b, a: (_a = hsb.a) !== null && _a !== void 0 ? _a : 1 }; - }, - // Universal parser - accepts any format - parse(color) { - var _a; - // ExportColorValue object - if (typeof color === 'object' && color !== null && 'hex' in color && 'rgb' in color) { - return this.fromHex(color.hex); - } - // RGB object - if (typeof color === 'object' && color !== null && 'r' in color && 'g' in color && 'b' in color) { - const rgb = color; - // Check if Figma native (0-1) or standard (0-255) - if (rgb.r <= 1 && rgb.g <= 1 && rgb.b <= 1) { - return { r: rgb.r, g: rgb.g, b: rgb.b, a: (_a = rgb.a) !== null && _a !== void 0 ? _a : 1 }; - } - return this.fromRgb255(rgb); - } - // HSL object - if (typeof color === 'object' && color !== null && 'h' in color && 's' in color && 'l' in color) { - return this.fromHsl(color); - } - // HSB object - if (typeof color === 'object' && color !== null && 'h' in color && 's' in color && 'b' in color) { - return this.fromHsb(color); - } - // String formats - if (typeof color === 'string') { - if (color.startsWith('rgb') || color.startsWith('hsl')) { - return this.fromCss(color); - } - return this.fromHex(color); - } - return { r: 0, g: 0, b: 0, a: 1 }; - } -}; -// ============================================================================ -// SECTION 6: VARIABLE CACHE (JSF Rule 4.18 - Resource Management) -// ============================================================================ -class VariableCache { - constructor() { - this.collectionMap = new Map(); - this.variableMap = new Map(); - this.libraryVariableMap = new Map(); // Library/remote variables - this.libraryCollectionNames = new Set(); // Names of connected library collections - this.initialized = false; - } - async initialize() { - if (this.initialized) - return; - await this.rebuild(); - this.initialized = true; - } - async rebuild() { - this.collectionMap.clear(); - this.variableMap.clear(); - this.libraryVariableMap.clear(); - this.libraryCollectionNames.clear(); - // Index local collections and variables - for (const col of await figma.variables.getLocalVariableCollectionsAsync()) { - this.collectionMap.set(col.name, col); - for (const varId of col.variableIds) { - const v = await figma.variables.getVariableByIdAsync(varId); - if (v) { - this.variableMap.set(`${col.name}/${v.name}`, v); - } - } - } - // Also index library/remote variables that are available in this file - await this.indexLibraryVariables(); - } - // Index library variables from connected team libraries - async indexLibraryVariables() { - try { - // Get all library variable collections available to this file - const libraryCollections = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync(); - for (const libCol of libraryCollections) { - this.libraryCollectionNames.add(libCol.name); - // Get variables in this library collection - try { - const libraryVars = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(libCol.key); - for (const libVar of libraryVars) { - // Import the variable so we can reference it by ID - try { - const importedVar = await figma.variables.importVariableByKeyAsync(libVar.key); - if (importedVar) { - this.libraryVariableMap.set(`${libCol.name}/${importedVar.name}`, importedVar); - } - } - catch (importErr) { - // Individual variable import failure - skip - } - } - } - catch (e) { - Logger.log(` ⚠️ Could not index library collection "${libCol.name}": ${e}`); - } - } - if (this.libraryCollectionNames.size > 0) { - Logger.log(`📚 Indexed ${this.libraryVariableMap.size} library variables from ${this.libraryCollectionNames.size} connected libraries`); - } - } - catch (e) { - Logger.log(`⚠️ Could not access team library: ${e}`); - } - } - getCollection(name) { - return this.collectionMap.get(name); - } - getVariable(key) { - // Check local variables first, then library variables - return this.variableMap.get(key) || this.libraryVariableMap.get(key); - } - setVariable(key, variable) { - this.variableMap.set(key, variable); - } - setCollection(name, collection) { - this.collectionMap.set(name, collection); - } - removeCollection(name) { - // Remove collection from map - this.collectionMap.delete(name); - // Remove all variables belonging to this collection - const keysToRemove = []; - for (const key of this.variableMap.keys()) { - if (key.startsWith(`${name}/`)) { - keysToRemove.push(key); - } - } - for (const key of keysToRemove) { - this.variableMap.delete(key); - } - } - // Check if a collection is available (local or library) - isCollectionAvailable(name) { - return this.collectionMap.has(name) || this.libraryCollectionNames.has(name); - } - // Get all connected library collection names - getLibraryCollectionNames() { - return Array.from(this.libraryCollectionNames); - } - get size() { - return this.variableMap.size; - } - get collections() { - return this.collectionMap.values(); - } - getVariableKeys() { - return Array.from(this.variableMap.keys()); - } -} -const variableCache = new VariableCache(); -// ============================================================================ -// SECTION 7: TYPE GUARDS & MAPPERS (JSF Rule 4.9) -// ============================================================================ -function isExportVariableValue(obj) { - return typeof obj === 'object' && obj !== null && '$type' in obj; -} -function isVariableAlias(value) { - return typeof value === 'object' && value !== null && - value.type === 'VARIABLE_ALIAS'; -} -const TypeMapper = { - toExportType(type) { - var _a; - const map = { - 'COLOR': 'color', - 'FLOAT': 'float', - 'STRING': 'string', - 'BOOLEAN': 'boolean' - }; - return (_a = map[type]) !== null && _a !== void 0 ? _a : 'string'; - }, - toFigmaType(type) { - var _a; - const map = { - 'color': 'COLOR', - 'float': 'FLOAT', - 'string': 'STRING', - 'boolean': 'BOOLEAN' - }; - return (_a = map[type]) !== null && _a !== void 0 ? _a : 'STRING'; - }, - scopesToArray(scopes) { - if (scopes.length === 0 || scopes.includes('ALL_SCOPES')) { - return ['ALL_SCOPES']; - } - return [...scopes]; - }, - arrayToScopes(arr) { - if (arr.includes('ALL_SCOPES')) { - return ['ALL_SCOPES']; - } - return arr; - } -}; -// ============================================================================ -// SECTION 8: BINDING UTILITIES -// ============================================================================ -async function getVariableBindingInfo(boundVariables, key) { - if (!(boundVariables === null || boundVariables === void 0 ? void 0 : boundVariables[key])) - return {}; - const alias = boundVariables[key]; - if (!alias) - return {}; - const variable = await figma.variables.getVariableByIdAsync(alias.id); - if (!variable) - return { id: alias.id }; - const collection = await figma.variables.getVariableCollectionByIdAsync(variable.variableCollectionId); - return { - id: alias.id, - name: variable.name, - collection: collection === null || collection === void 0 ? void 0 : collection.name - }; -} -async function extractBindings(boundVariables, keys) { - if (!boundVariables) - return undefined; - const bindings = {}; - for (const key of keys) { - const binding = await getVariableBindingInfo(boundVariables, key); - if (binding.name) { - bindings[key] = binding; - } - } - return Object.keys(bindings).length > 0 ? bindings : undefined; -} -function flattenVariables(obj, prefix) { - const results = []; - for (const key of Object.keys(obj)) { - const val = obj[key]; - const path = prefix ? `${prefix}/${key}` : key; - if (isExportVariableValue(val)) { - results.push({ path, value: val }); - } - else { - results.push(...flattenVariables(val, path)); - } - } - return results; -} -function getValueAtPath(obj, path) { - const parts = path.split('/'); - let current = obj; - for (const part of parts) { - if (typeof current !== 'object' || current === null) - return null; - if (isExportVariableValue(current)) - return null; - current = current[part]; - } - return isExportVariableValue(current) ? current : null; -} -// Color Style Processor - supports SOLID, GRADIENT, and IMAGE paint styles -const ColorStyleProcessor = { - async export(options) { - var _a, _b, _c, _d; - const includeImages = (_a = options === null || options === void 0 ? void 0 : options.includeImages) !== null && _a !== void 0 ? _a : false; - const styles = []; - for (const style of await figma.getLocalPaintStylesAsync()) { - if (style.paints.length === 0) - continue; - const exportPaints = []; - let primaryColor; - let primaryOpacity; - let boundVars; - for (const paint of style.paints) { - if (paint.type === 'SOLID') { - const colorAsRgba = paint.color; - let effectiveOpacity = (_b = paint.opacity) !== null && _b !== void 0 ? _b : 1; - if (colorAsRgba.a !== undefined && colorAsRgba.a < 1 && effectiveOpacity === 1) { - effectiveOpacity = colorAsRgba.a; - } - const colorWithAlpha = { - r: paint.color.r, - g: paint.color.g, - b: paint.color.b, - a: effectiveOpacity - }; - const solidPaint = { - type: 'SOLID', - color: ColorConverter.toAllFormats(colorWithAlpha), - opacity: MathUtils.round2(effectiveOpacity) - }; - exportPaints.push(solidPaint); - // Store first solid color for backward compatibility - if (!primaryColor) { - primaryColor = solidPaint.color; - primaryOpacity = solidPaint.opacity; - boundVars = await extractBindings(paint.boundVariables, ['color']); - } - } - else if (paint.type === 'GRADIENT_LINEAR' || paint.type === 'GRADIENT_RADIAL' || - paint.type === 'GRADIENT_ANGULAR' || paint.type === 'GRADIENT_DIAMOND') { - const gradientStops = paint.gradientStops.map(stop => { - var _a; - return ({ - position: MathUtils.round2(stop.position), - color: ColorConverter.toAllFormats({ - r: stop.color.r, - g: stop.color.g, - b: stop.color.b, - a: (_a = stop.color.a) !== null && _a !== void 0 ? _a : 1 - }) - }); - }); - const gradientPaint = Object.assign(Object.assign({ type: paint.type, gradientStops }, (paint.gradientTransform && { - gradientTransform: paint.gradientTransform - })), { opacity: MathUtils.round2((_c = paint.opacity) !== null && _c !== void 0 ? _c : 1) }); - exportPaints.push(gradientPaint); - } - else if (paint.type === 'IMAGE') { - const imagePaint = Object.assign(Object.assign(Object.assign(Object.assign({ type: 'IMAGE', scaleMode: paint.scaleMode }, (paint.imageHash && { imageHash: paint.imageHash })), { opacity: MathUtils.round2((_d = paint.opacity) !== null && _d !== void 0 ? _d : 1) }), (paint.rotation !== undefined && { rotation: paint.rotation })), (paint.filters && { - filters: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (paint.filters.exposure !== undefined && { exposure: paint.filters.exposure })), (paint.filters.contrast !== undefined && { contrast: paint.filters.contrast })), (paint.filters.saturation !== undefined && { saturation: paint.filters.saturation })), (paint.filters.temperature !== undefined && { temperature: paint.filters.temperature })), (paint.filters.tint !== undefined && { tint: paint.filters.tint })), (paint.filters.highlights !== undefined && { highlights: paint.filters.highlights })), (paint.filters.shadows !== undefined && { shadows: paint.filters.shadows })) - })); - // Try to get image bytes if includeImages is enabled - if (includeImages && paint.imageHash) { - try { - const image = figma.getImageByHash(paint.imageHash); - if (image) { - const imageBytes = await image.getBytesAsync(); - if (imageBytes) { - // Convert to base64 - const base64 = figma.base64Encode(imageBytes); - imagePaint.imageBase64 = base64; - } - } - } - catch (e) { - Logger.log(`⚠️ Could not export image data for style "${style.name}": ${e}`); - } - } - exportPaints.push(imagePaint); - } - } - if (exportPaints.length === 0) - continue; - const colorStyle = Object.assign(Object.assign(Object.assign(Object.assign({ name: style.name, paints: exportPaints }, (primaryColor && { color: primaryColor })), (primaryOpacity !== undefined && { opacity: primaryOpacity })), (style.description && { description: style.description })), (boundVars && Object.keys(boundVars).length > 0 && { boundVariables: boundVars })); - styles.push(colorStyle); - } - return styles; - }, - async importStyles(styles, cache) { - var _a, _b, _c, _d; - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of await figma.getLocalPaintStylesAsync()) { - existing.set(s.name, s); - } - for (const colorStyle of styles) { - let style; - if (existing.has(colorStyle.name)) { - style = existing.get(colorStyle.name); - updated++; - } - else { - style = figma.createPaintStyle(); - style.name = colorStyle.name; - created++; - } - if (colorStyle.description) { - style.description = colorStyle.description; - } - const paints = []; - // Use new paints array if available, otherwise fall back to legacy color field - if (colorStyle.paints && colorStyle.paints.length > 0) { - for (const exportPaint of colorStyle.paints) { - if (exportPaint.type === 'SOLID') { - const colorRgba = ColorParser.parse(exportPaint.color); - let finalOpacity = (_a = exportPaint.opacity) !== null && _a !== void 0 ? _a : 1; - if (colorRgba.a < 1 && exportPaint.opacity === undefined) { - finalOpacity = MathUtils.round2(colorRgba.a); - } - let paint = { - type: 'SOLID', - color: { r: colorRgba.r, g: colorRgba.g, b: colorRgba.b }, - opacity: MathUtils.round2(finalOpacity) - }; - // Apply variable bindings for first solid paint - if (colorStyle.boundVariables && paints.length === 0) { - for (const [key, binding] of Object.entries(colorStyle.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - paint = figma.variables.setBoundVariableForPaint(paint, key, targetVar); - } - catch (e) { - Logger.log(`⚠️ Could not bind ${key}: ${e}`); - } - } - } - } - } - paints.push(paint); - } - else if (exportPaint.type === 'GRADIENT_LINEAR' || exportPaint.type === 'GRADIENT_RADIAL' || - exportPaint.type === 'GRADIENT_ANGULAR' || exportPaint.type === 'GRADIENT_DIAMOND') { - const gradientStops = exportPaint.gradientStops.map(stop => { - const stopColor = ColorParser.parse(stop.color); - return { - position: stop.position, - color: { r: stopColor.r, g: stopColor.g, b: stopColor.b, a: stopColor.a } - }; - }); - // Convert readonly transform to mutable Transform type - const transform = exportPaint.gradientTransform - ? [[exportPaint.gradientTransform[0][0], exportPaint.gradientTransform[0][1], exportPaint.gradientTransform[0][2]], - [exportPaint.gradientTransform[1][0], exportPaint.gradientTransform[1][1], exportPaint.gradientTransform[1][2]]] - : [[1, 0, 0], [0, 1, 0]]; - const gradientPaint = { - type: exportPaint.type, - gradientStops, - gradientTransform: transform, - opacity: (_b = exportPaint.opacity) !== null && _b !== void 0 ? _b : 1 - }; - paints.push(gradientPaint); - } - else if (exportPaint.type === 'IMAGE') { - // Create image paint - let imageHash = null; - // First, try to create image from base64 data if available - // This takes priority because imageHash from another file won't work - if (exportPaint.imageBase64) { - try { - const bytes = figma.base64Decode(exportPaint.imageBase64); - const image = figma.createImage(bytes); - imageHash = image.hash; - Logger.log(`✅ Created image from base64 data for style "${colorStyle.name}"`); - } - catch (e) { - Logger.log(`⚠️ Could not import image from base64 for style "${colorStyle.name}": ${e}`); - } - } - // If no base64 or base64 failed, try using the existing hash (might work if image exists in file) - if (!imageHash && exportPaint.imageHash) { - // Check if the image with this hash exists in the file - const existingImage = figma.getImageByHash(exportPaint.imageHash); - if (existingImage) { - imageHash = exportPaint.imageHash; - Logger.log(`✅ Found existing image with hash for style "${colorStyle.name}"`); - } - else { - Logger.log(`⚠️ Image hash not found in file for style "${colorStyle.name}", skipping image paint (imageHash cannot be null)`); - } - } - // Only add image paint if we have a valid imageHash - Figma API rejects null imageHash - if (imageHash) { - const imagePaint = Object.assign(Object.assign({ type: 'IMAGE', scaleMode: exportPaint.scaleMode, imageHash: imageHash, opacity: (_c = exportPaint.opacity) !== null && _c !== void 0 ? _c : 1 }, (exportPaint.rotation !== undefined && { rotation: exportPaint.rotation })), (exportPaint.filters && { filters: exportPaint.filters })); - paints.push(imagePaint); - } - } - } - } - else if (colorStyle.color) { - // Legacy format: single color field - const colorRgba = ColorParser.parse(colorStyle.color); - let finalOpacity = (_d = colorStyle.opacity) !== null && _d !== void 0 ? _d : 1; - if (colorRgba.a < 1 && colorStyle.opacity === undefined) { - finalOpacity = MathUtils.round2(colorRgba.a); - } - let paint = { - type: 'SOLID', - color: { r: colorRgba.r, g: colorRgba.g, b: colorRgba.b }, - opacity: MathUtils.round2(finalOpacity) - }; - if (colorStyle.boundVariables) { - for (const [key, binding] of Object.entries(colorStyle.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - paint = figma.variables.setBoundVariableForPaint(paint, key, targetVar); - } - catch (e) { - Logger.log(`⚠️ Could not bind ${key}: ${e}`); - } - } - } - } - } - paints.push(paint); - } - if (paints.length > 0) { - style.paints = paints; - } - } - return { created, updated }; - } -}; -// Text Style Processor -const TextStyleProcessor = { - async export(_options) { - const styles = []; - for (const style of await figma.getLocalTextStylesAsync()) { - const textStyle = Object.assign(Object.assign({ name: style.name, fontFamily: style.fontName.family, fontStyle: style.fontName.style, fontSize: style.fontSize, lineHeight: style.lineHeight, letterSpacing: style.letterSpacing, textCase: style.textCase, textDecoration: style.textDecoration }, (style.description && { description: style.description })), { boundVariables: await extractBindings(style.boundVariables, ['fontSize', 'lineHeight', 'letterSpacing', 'paragraphSpacing', 'paragraphIndent']) }); - styles.push(textStyle); - } - return styles; - }, - async importStyles(styles, cache) { - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of await figma.getLocalTextStylesAsync()) { - existing.set(s.name, s); - } - for (const textStyle of styles) { - let style; - if (existing.has(textStyle.name)) { - style = existing.get(textStyle.name); - updated++; - } - else { - style = figma.createTextStyle(); - style.name = textStyle.name; - created++; - } - if (textStyle.description) { - style.description = textStyle.description; - } - try { - await figma.loadFontAsync({ family: textStyle.fontFamily, style: textStyle.fontStyle }); - style.fontName = { family: textStyle.fontFamily, style: textStyle.fontStyle }; - style.fontSize = textStyle.fontSize; - style.lineHeight = textStyle.lineHeight; - style.letterSpacing = textStyle.letterSpacing; - if (textStyle.textCase) - style.textCase = textStyle.textCase; - if (textStyle.textDecoration) - style.textDecoration = textStyle.textDecoration; - if (textStyle.boundVariables) { - for (const [key, binding] of Object.entries(textStyle.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - style.setBoundVariable(key, targetVar); - } - catch ( /* Skip */_a) { /* Skip */ } - } - } - } - } - } - catch (e) { - Logger.log(`⚠️ Could not load font for ${textStyle.name}: ${e}`); - } - } - return { created, updated }; - } -}; -// Effect Style Processor -const EffectStyleProcessor = { - async export(_options) { - const styles = []; - for (const style of await figma.getLocalEffectStylesAsync()) { - const effects = []; - for (const effect of style.effects) { - const effectData = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ type: effect.type, visible: effect.visible }, ('radius' in effect && { radius: effect.radius })), ('spread' in effect && { spread: effect.spread })), ('offset' in effect && { offset: effect.offset })), ('color' in effect && { color: ColorConverter.toAllFormats(effect.color) })), ('blendMode' in effect && { blendMode: effect.blendMode })), ('showShadowBehindNode' in effect && { showShadowBehindNode: effect.showShadowBehindNode })), { boundVariables: await extractBindings(effect.boundVariables, ['color', 'radius', 'spread', 'offsetX', 'offsetY']) }); - effects.push(effectData); - } - const effectStyle = Object.assign(Object.assign({ name: style.name }, (style.description && { description: style.description })), { effects }); - styles.push(effectStyle); - } - return styles; - }, - async importStyles(styles, cache) { - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of await figma.getLocalEffectStylesAsync()) { - existing.set(s.name, s); - } - for (const effectStyle of styles) { - let style; - if (existing.has(effectStyle.name)) { - style = existing.get(effectStyle.name); - updated++; - } - else { - style = figma.createEffectStyle(); - style.name = effectStyle.name; - created++; - } - if (effectStyle.description) { - style.description = effectStyle.description; - } - const newEffects = effectStyle.effects.map(effect => { - var _a; - const e = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ type: effect.type, visible: (_a = effect.visible) !== null && _a !== void 0 ? _a : true }, ((effect.radius !== undefined) && { radius: effect.radius })), ((effect.spread !== undefined) && { spread: effect.spread })), ((effect.offset !== undefined) && { offset: effect.offset })), ((effect.color !== undefined) && { - color: (() => { - const c = ColorParser.parse(effect.color); - return { r: c.r, g: c.g, b: c.b, a: MathUtils.round2(c.a) }; - })() - })), ((effect.blendMode !== undefined) && { blendMode: effect.blendMode })), ((effect.showShadowBehindNode !== undefined) && { showShadowBehindNode: effect.showShadowBehindNode })); - return e; - }); - style.effects = newEffects; - // Bind variables - for (let i = 0; i < effectStyle.effects.length; i++) { - const effectData = effectStyle.effects[i]; - if (effectData.boundVariables) { - for (const [key, binding] of Object.entries(effectData.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - const effects = [...style.effects]; - effects[i] = figma.variables.setBoundVariableForEffect(effects[i], key, targetVar); - style.effects = effects; - } - catch ( /* Skip */_a) { /* Skip */ } - } - } - } - } - } - } - return { created, updated }; - } -}; -// Grid Style Processor -const GridStyleProcessor = { - async export(_options) { - const styles = []; - for (const style of await figma.getLocalGridStylesAsync()) { - const layoutGrids = []; - for (const grid of style.layoutGrids) { - const gridColor = grid.color; - const gridData = Object.assign(Object.assign(Object.assign({ pattern: grid.pattern, visible: grid.visible, color: ColorConverter.toAllFormats(gridColor) }, (grid.pattern === 'GRID' && { sectionSize: grid.sectionSize })), (grid.pattern !== 'GRID' && Object.assign({ alignment: grid.alignment, gutterSize: grid.gutterSize, count: grid.count, offset: grid.offset }, (grid.sectionSize !== undefined && { sectionSize: grid.sectionSize })))), { boundVariables: await extractBindings(grid.boundVariables, ['gutterSize', 'count', 'offset', 'sectionSize']) }); - layoutGrids.push(gridData); - } - const gridStyle = Object.assign(Object.assign({ name: style.name }, (style.description && { description: style.description })), { layoutGrids }); - styles.push(gridStyle); - } - return styles; - }, - async importStyles(styles, cache) { - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of await figma.getLocalGridStylesAsync()) { - existing.set(s.name, s); - } - for (const gridStyle of styles) { - let style; - if (existing.has(gridStyle.name)) { - style = existing.get(gridStyle.name); - updated++; - } - else { - style = figma.createGridStyle(); - style.name = gridStyle.name; - created++; - } - if (gridStyle.description) { - style.description = gridStyle.description; - } - const newLayoutGrids = gridStyle.layoutGrids.map((grid) => { - var _a, _b, _c, _d, _e, _f, _g; - const gridColor = grid.color - ? ColorParser.parse(grid.color) - : { r: 1, g: 0, b: 0, a: 0.1 }; - const color = { - r: gridColor.r, - g: gridColor.g, - b: gridColor.b, - a: MathUtils.round2(gridColor.a) - }; - if (grid.pattern === 'GRID') { - return { - pattern: 'GRID', - sectionSize: (_a = grid.sectionSize) !== null && _a !== void 0 ? _a : 10, - visible: grid.visible !== false, - color - }; - } - const alignment = (_b = grid.alignment) !== null && _b !== void 0 ? _b : 'STRETCH'; - const base = { - pattern: grid.pattern, - gutterSize: (_c = grid.gutterSize) !== null && _c !== void 0 ? _c : 10, - count: (_d = grid.count) !== null && _d !== void 0 ? _d : 5, - visible: grid.visible !== false, - color - }; - if (alignment === 'STRETCH') { - return Object.assign(Object.assign({}, base), { alignment: 'STRETCH', offset: (_e = grid.offset) !== null && _e !== void 0 ? _e : 0 }); - } - else if (alignment === 'CENTER') { - return Object.assign(Object.assign({}, base), { alignment: 'CENTER', sectionSize: (_f = grid.sectionSize) !== null && _f !== void 0 ? _f : 100 }); - } - else { - const result = Object.assign(Object.assign({}, base), { alignment: alignment, offset: (_g = grid.offset) !== null && _g !== void 0 ? _g : 0 }); - if (grid.sectionSize !== undefined) { - result.sectionSize = grid.sectionSize; - } - return result; - } - }); - style.layoutGrids = newLayoutGrids; - // Bind variables - for (let i = 0; i < gridStyle.layoutGrids.length; i++) { - const gridData = gridStyle.layoutGrids[i]; - if (gridData.boundVariables) { - for (const [key, binding] of Object.entries(gridData.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - const grids = [...style.layoutGrids]; - grids[i] = figma.variables.setBoundVariableForLayoutGrid(grids[i], key, targetVar); - style.layoutGrids = grids; - } - catch ( /* Skip */_a) { /* Skip */ } - } - } - } - } - } - } - return { created, updated }; - } -}; -async function computeImportDiff(importData) { - await variableCache.initialize(); - const result = { - newCollections: [], - modifiedCollections: [], - unchangedCollections: [], - newVariables: [], - modifiedVariables: [], - unchangedVariables: 0, - newStyles: [], - modifiedStyles: [], - summary: { - collectionsNew: 0, - collectionsModified: 0, - collectionsUnchanged: 0, - variablesNew: 0, - variablesModified: 0, - variablesUnchanged: 0, - stylesNew: 0, - stylesModified: 0 - } - }; - // Process collections from import data - for (const item of importData) { - const keys = Object.keys(item); - if (keys.length === 1 && keys[0] === '_styles') { - // Handle styles diff - const stylesData = item._styles; - await computeStylesDiff(stylesData, result); - continue; - } - // Handle collection - const collectionObj = item; - const jsonCollectionName = Object.keys(collectionObj)[0]; - const collectionContent = collectionObj[jsonCollectionName]; - const collectionName = collectionContent.$originalName || jsonCollectionName; - const existingCollection = variableCache.getCollection(collectionName); - if (!existingCollection) { - // New collection - result.newCollections.push(collectionName); - result.summary.collectionsNew++; - // Count all variables as new - const varCount = countVariablesInCollection(collectionContent.modes); - result.summary.variablesNew += varCount; - continue; - } - // Existing collection - check for modifications - let hasModifications = false; - for (const [modeName, modeData] of Object.entries(collectionContent.modes)) { - const mode = existingCollection.modes.find(m => m.name === modeName); - if (!mode) { - hasModifications = true; - continue; - } - // Check each variable - await checkVariablesDiff(existingCollection, mode.modeId, modeData, collectionName, '', result); - } - if (result.modifiedVariables.some(v => v.collection === collectionName) || - result.newVariables.some(v => v.collection === collectionName)) { - result.modifiedCollections.push(collectionName); - result.summary.collectionsModified++; - } - else { - result.unchangedCollections.push(collectionName); - result.summary.collectionsUnchanged++; - } - } - return result; -} -function countVariablesInCollection(modes) { - let count = 0; - const firstMode = Object.values(modes)[0]; - if (firstMode) { - count = countVarsInNestedObj(firstMode); - } - return count; -} -function countVarsInNestedObj(obj) { - let count = 0; - for (const value of Object.values(obj)) { - if (isExportVariableValue(value)) { - count++; - } - else { - count += countVarsInNestedObj(value); - } - } - return count; -} -async function checkVariablesDiff(collection, modeId, importData, collectionName, path, result) { - for (const [key, value] of Object.entries(importData)) { - const currentPath = path ? `${path}/${key}` : key; - if (isExportVariableValue(value)) { - // This is a variable value - const existingVar = variableCache.getVariable(`${collectionName}/${currentPath}`); - if (!existingVar) { - result.newVariables.push({ collection: collectionName, path: currentPath }); - result.summary.variablesNew++; - } - else { - // Check if value changed - const existingValue = existingVar.valuesByMode[modeId]; - const importValue = value.$value; - if (valuesAreDifferent(existingValue, importValue)) { - result.modifiedVariables.push({ - collection: collectionName, - path: currentPath, - oldValue: formatValueForDisplay(existingValue), - newValue: formatValueForDisplay(importValue) - }); - result.summary.variablesModified++; - } - else { - result.unchangedVariables++; - result.summary.variablesUnchanged++; - } - } - } - else { - // Nested object, recurse - await checkVariablesDiff(collection, modeId, value, collectionName, currentPath, result); - } - } -} -function valuesAreDifferent(existing, imported) { - if (existing === undefined) - return true; - // Handle alias references - if (isVariableAlias(existing)) { - // Both are alias refs, compare the string value - if (typeof imported === 'string' && imported.startsWith('{')) { - return true; // Can't easily compare alias refs, assume different - } - return true; - } - // Handle colors - if (typeof existing === 'object' && existing !== null && 'r' in existing) { - if (typeof imported === 'object' && imported !== null && 'hex' in imported) { - const existingHex = ColorConverter.toAllFormats(existing).hex; - return existingHex.toLowerCase() !== imported.hex.toLowerCase(); - } - return true; - } - // Handle primitives - return existing !== imported; -} -function formatValueForDisplay(value) { - if (value === undefined) - return 'undefined'; - if (typeof value === 'object' && value !== null) { - if ('hex' in value) - return value.hex; - if ('r' in value) - return ColorConverter.toAllFormats(value).hex; - if ('id' in value) - return '{alias}'; - } - return String(value); -} -async function computeStylesDiff(stylesData, result) { - // Check color styles - if (stylesData.colorStyles) { - const existingColorStyles = await figma.getLocalPaintStylesAsync(); - const existingNames = new Set(existingColorStyles.map(s => s.name)); - for (const style of stylesData.colorStyles) { - if (existingNames.has(style.name)) { - result.modifiedStyles.push({ type: 'color', name: style.name }); - result.summary.stylesModified++; - } - else { - result.newStyles.push({ type: 'color', name: style.name }); - result.summary.stylesNew++; - } - } - } - // Check text styles - if (stylesData.textStyles) { - const existingTextStyles = await figma.getLocalTextStylesAsync(); - const existingNames = new Set(existingTextStyles.map(s => s.name)); - for (const style of stylesData.textStyles) { - if (existingNames.has(style.name)) { - result.modifiedStyles.push({ type: 'text', name: style.name }); - result.summary.stylesModified++; - } - else { - result.newStyles.push({ type: 'text', name: style.name }); - result.summary.stylesNew++; - } - } - } - // Check effect styles - if (stylesData.effectStyles) { - const existingEffectStyles = await figma.getLocalEffectStylesAsync(); - const existingNames = new Set(existingEffectStyles.map(s => s.name)); - for (const style of stylesData.effectStyles) { - if (existingNames.has(style.name)) { - result.modifiedStyles.push({ type: 'effect', name: style.name }); - result.summary.stylesModified++; - } - else { - result.newStyles.push({ type: 'effect', name: style.name }); - result.summary.stylesNew++; - } - } - } - // Check grid styles - if (stylesData.gridStyles) { - const existingGridStyles = await figma.getLocalGridStylesAsync(); - const existingNames = new Set(existingGridStyles.map(s => s.name)); - for (const style of stylesData.gridStyles) { - if (existingNames.has(style.name)) { - result.modifiedStyles.push({ type: 'grid', name: style.name }); - result.summary.stylesModified++; - } - else { - result.newStyles.push({ type: 'grid', name: style.name }); - result.summary.stylesNew++; - } - } - } -} -async function exportVariables(selectedCollections, styleOptions, preserveLibraryRefs, includeImages, namingConvention = 'original', exportFormat = 'figma', selectedModes, resolveAliases = false) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; - Logger.log('📤 Starting export...'); - Logger.log(` preserveLibraryRefs: ${preserveLibraryRefs}`); - Logger.log(` includeImages: ${includeImages}`); - Logger.log(` namingConvention: ${namingConvention}`); - Logger.log(` exportFormat: ${exportFormat}`); - Logger.log(` resolveAliases: ${resolveAliases}`); - if (selectedModes) { - Logger.log(` selectedModes: ${JSON.stringify(selectedModes)}`); - } - try { - let collections = await figma.variables.getLocalVariableCollectionsAsync(); - if (selectedCollections === null || selectedCollections === void 0 ? void 0 : selectedCollections.length) { - collections = collections.filter(c => selectedCollections.includes(c.name)); - Logger.log(`Filtering to ${collections.length} selected collections`); - } - const exportData = []; - const w3cExportData = {}; - let totalVariables = 0; - for (const collection of collections) { - Logger.log(`Processing collection: ${collection.name}`); - // Filter modes if selectedModes specified for this collection - let modesToExport = collection.modes; - if (selectedModes && selectedModes[collection.name]) { - const allowedModes = selectedModes[collection.name]; - modesToExport = collection.modes.filter(m => allowedModes.includes(m.name)); - Logger.log(` Filtering to ${modesToExport.length} modes: ${modesToExport.map(m => m.name).join(', ')}`); - } - // Convert collection name based on naming convention - const exportCollectionName = NamingConverter.convertCollectionName(collection.name, namingConvention); - const collectionExport = { - [exportCollectionName]: Object.assign({ modes: {} }, (exportCollectionName !== collection.name && { $originalName: collection.name })) - }; - // Initialize modes with converted names (only selected modes) - for (const mode of modesToExport) { - const exportModeName = NamingConverter.convertModeName(mode.name, namingConvention); - collectionExport[exportCollectionName].modes[exportModeName] = {}; - // We'll handle original mode names in metadata if needed - } - // Process variables - for (const variableId of collection.variableIds) { - const variable = await figma.variables.getVariableByIdAsync(variableId); - if (!variable) - continue; - totalVariables++; - // Convert variable path parts based on naming convention - const originalParts = variable.name.split('/'); - const nameParts = originalParts.map(part => NamingConverter.convert(part, namingConvention)); - // Only process selected modes - for (const mode of modesToExport) { - const exportModeName = NamingConverter.convertModeName(mode.name, namingConvention); - const modeValues = collectionExport[exportCollectionName].modes[exportModeName]; - const value = variable.valuesByMode[mode.modeId]; - // Navigate/create nested structure - let current = modeValues; - for (let i = 0; i < nameParts.length - 1; i++) { - const part = nameParts[i]; - if (!current[part] || isExportVariableValue(current[part])) { - current[part] = {}; - } - current = current[part]; - } - const leafName = nameParts[nameParts.length - 1]; - // Convert value - let exportValue; - let isAlias = false; - let aliasCollection = ''; - let isLibraryAlias = false; - let aliasRef = ''; - let localValue = undefined; - if (isVariableAlias(value)) { - const aliasVar = await figma.variables.getVariableByIdAsync(value.id); - if (aliasVar) { - const aliasCol = await figma.variables.getVariableCollectionByIdAsync(aliasVar.variableCollectionId); - isAlias = true; - aliasCollection = (_a = aliasCol === null || aliasCol === void 0 ? void 0 : aliasCol.name) !== null && _a !== void 0 ? _a : ''; - isLibraryAlias = (_b = aliasCol === null || aliasCol === void 0 ? void 0 : aliasCol.remote) !== null && _b !== void 0 ? _b : false; - // If resolveAliases is true, resolve to the actual value - if (resolveAliases) { - // Resolve the alias to its actual value - const resolvedValue = await resolveAliasValue(aliasVar, mode.modeId); - if (typeof resolvedValue === 'object' && resolvedValue !== null && 'r' in resolvedValue) { - exportValue = ColorConverter.toAllFormats(resolvedValue); - } - else { - exportValue = resolvedValue; - } - // Don't mark as alias since we resolved it - isAlias = false; - } - else { - // Keep as alias reference - // Convert alias reference to match naming convention - const aliasPath = aliasVar.name.split('/').map(p => NamingConverter.convert(p, namingConvention)).join('.'); - aliasRef = `{${aliasPath}}`; - exportValue = aliasRef; - // Get the resolved local value for library aliases - if (isLibraryAlias) { - // Get the resolved value from the alias - const resolvedValue = aliasVar.valuesByMode[Object.keys(aliasVar.valuesByMode)[0]]; - if (typeof resolvedValue === 'object' && resolvedValue !== null && 'r' in resolvedValue) { - localValue = ColorConverter.toAllFormats(resolvedValue); - } - else if (!isVariableAlias(resolvedValue)) { - localValue = resolvedValue; - } - } - } - } - else { - exportValue = ''; - } - } - else if (typeof value === 'object' && value !== null && 'r' in value) { - exportValue = ColorConverter.toAllFormats(value); - } - else { - exportValue = value; - } - const varExport = Object.assign(Object.assign(Object.assign({ $scopes: TypeMapper.scopesToArray(variable.scopes), $type: TypeMapper.toExportType(variable.resolvedType), $value: exportValue }, (variable.description && { $description: variable.description })), (isAlias && aliasCollection && { $collectionName: aliasCollection })), (isAlias && isLibraryAlias && Object.assign({ $libraryRef: aliasRef }, (localValue !== undefined && { $localValue: localValue })))); - current[leafName] = varExport; - } - } - exportData.push(collectionExport); - // Also build W3C format if needed - if (exportFormat === 'w3c') { - w3cExportData[exportCollectionName] = W3CConverter.collectionToW3C(exportCollectionName, collectionExport[exportCollectionName].modes, namingConvention, collectionExport[exportCollectionName].$originalName); - } - } - // Export styles - let stylesExported = null; - if (styleOptions) { - stylesExported = {}; - if (styleOptions.colorStyles) - stylesExported.colorStyles = await ColorStyleProcessor.export({ includeImages }); - if (styleOptions.textStyles) - stylesExported.textStyles = await TextStyleProcessor.export(); - if (styleOptions.effectStyles) - stylesExported.effectStyles = await EffectStyleProcessor.export(); - if (styleOptions.gridStyles) - stylesExported.gridStyles = await GridStyleProcessor.export(); - if (Object.keys(stylesExported).length > 0) { - exportData.push({ _styles: stylesExported }); - } - else { - stylesExported = null; - } - } - const stats = { - collections: collections.length, - variables: totalVariables, - styles: stylesExported ? { - color: (_d = (_c = stylesExported.colorStyles) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0, - text: (_f = (_e = stylesExported.textStyles) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0, - effect: (_h = (_g = stylesExported.effectStyles) === null || _g === void 0 ? void 0 : _g.length) !== null && _h !== void 0 ? _h : 0, - grid: (_k = (_j = stylesExported.gridStyles) === null || _j === void 0 ? void 0 : _j.length) !== null && _k !== void 0 ? _k : 0 - } : null - }; - // Choose output format - let outputData; - if (exportFormat === 'w3c') { - // W3C Design Tokens format - // Note: Styles are not part of W3C spec, so we add them in extensions - if (stylesExported && Object.keys(stylesExported).length > 0) { - w3cExportData['$extensions'] = { - 'com.figma': { - styles: stylesExported - } - }; - } - outputData = JSON.stringify(w3cExportData, null, 2); - Logger.log(`✅ Export complete (W3C format): ${stats.collections} collections, ${stats.variables} variables`); - } - else { - // Figma JSON format - outputData = JSON.stringify(exportData, null, 2); - Logger.log(`✅ Export complete: ${stats.collections} collections, ${stats.variables} variables`); - } - Logger.send('export_complete', { - data: outputData, - stats, - format: exportFormat - }); - } - catch (e) { - Logger.log(`❌ Export error: ${e}`); - Logger.send('error', { message: `Export failed: ${e}` }); - } -} -// ============================================================================ -// SECTION 12: IMPORT ORCHESTRATOR -// ============================================================================ -async function importVariables(jsonData, options) { - var _a, _b; - Logger.log('📥 Starting import...'); - Logger.log(`📋 Import options: merge=${options.merge}, clearFirst=${options.clearFirst}, importStyles=${options.importStyles}`); - // Create a snapshot BEFORE making any changes for automatic rollback on error - Logger.log('📸 Creating pre-import snapshot for automatic rollback...'); - let preImportSnapshot = null; - try { - preImportSnapshot = await createUndoSnapshot(); - Logger.log('✅ Pre-import snapshot created'); - } - catch (snapshotError) { - Logger.log(`⚠️ Could not create pre-import snapshot: ${snapshotError}`); - // Continue without snapshot - user will be warned if import fails - } - try { - let parsedData = JSON.parse(jsonData); - // Detect format and convert if W3C - let importData; - let detectedFormat = 'figma'; - if (!Array.isArray(parsedData) && W3CConverter.isW3CFormat(parsedData)) { - Logger.log('📄 Detected W3C Design Tokens format, converting...'); - detectedFormat = 'w3c'; - // Extract styles from extensions if present - const w3cData = parsedData; - let stylesFromW3C = null; - if (w3cData['$extensions'] && w3cData['$extensions']['com.figma']) { - const figmaExtensions = w3cData['$extensions']['com.figma']; - if (figmaExtensions.styles) { - stylesFromW3C = figmaExtensions.styles; - } - // Remove extensions from token data - delete w3cData['$extensions']; - } - // Convert W3C to Figma format - const converted = W3CConverter.w3cToFigmaFormat(w3cData); - importData = converted; - // Add styles if present - if (stylesFromW3C) { - importData.push({ _styles: stylesFromW3C }); - } - } - else { - importData = parsedData; - } - // Handle Clean Import: clear everything first - if (options.clearFirst) { - Logger.log('🧹 Clean Import: Clearing existing variables and styles...'); - await clearAll(); - Logger.log('✅ Clean Import: Clearing complete, rebuilding cache...'); - // Reinitialize cache after clearing - await variableCache.rebuild(); - Logger.log('✅ Clean Import: Cache rebuilt, proceeding with import...'); - } - // Handle Custom Merge: selectively clear variables and/or styles - if (options.customMerge) { - const { clearVariables: shouldClearVars, clearStyles: shouldClearStyles } = options.customMerge; - if (shouldClearVars && shouldClearStyles) { - Logger.log('🎯 Custom Merge: Clearing both variables and styles...'); - await clearAll(); - } - else if (shouldClearVars) { - Logger.log('🎯 Custom Merge: Clearing variables only...'); - await clearVariables(); - } - else if (shouldClearStyles) { - Logger.log('🎯 Custom Merge: Clearing styles only...'); - await clearStyles(); - } - Logger.log('✅ Custom Merge: Clearing complete, rebuilding cache...'); - await variableCache.rebuild(); - } - await variableCache.initialize(); - let createdCollections = 0; - let createdVariables = 0; - let updatedVariables = 0; - let skippedVariables = 0; - let stylesCreated = 0; - let stylesUpdated = 0; - // Separate styles from collections - let stylesData = null; - const collectionData = []; - for (const item of importData) { - const keys = Object.keys(item); - if (keys.length === 1 && keys[0] === '_styles') { - stylesData = item._styles; - } - else { - collectionData.push(item); - } - } - // Collect all pending aliases across all collections for pass 2 - const allPendingAliases = []; - // Process collections - PASS 1: Create variables with raw values - Logger.log(`📥 Pass 1: Processing ${collectionData.length} collections...`); - for (const collectionObj of collectionData) { - const jsonCollectionName = Object.keys(collectionObj)[0]; - const collectionContent = collectionObj[jsonCollectionName]; - // Use $originalName if present (for round-trip with code-friendly naming) - // This restores original Figma names when importing JSON that was exported with naming conventions - const collectionName = collectionContent.$originalName || jsonCollectionName; - Logger.log(`Processing collection: ${jsonCollectionName}${collectionContent.$originalName ? ` (original: ${collectionName})` : ''}`); - // Get per-collection behavior (default to merge in simple mode) - // Check both JSON name and original name for behavior lookup - const collectionBehavior = ((_a = options.collectionBehaviors) === null || _a === void 0 ? void 0 : _a[jsonCollectionName]) || - ((_b = options.collectionBehaviors) === null || _b === void 0 ? void 0 : _b[collectionName]) || 'merge'; - let collection; - const existingCollection = variableCache.getCollection(collectionName); - if (existingCollection) { - // Handle per-collection behavior (Advanced mode) - if (collectionBehavior === 'replace') { - // Replace mode: delete existing collection and create fresh - Logger.log(` Replacing collection: ${collectionName}`); - try { - existingCollection.remove(); - variableCache.removeCollection(collectionName); - collection = figma.variables.createVariableCollection(collectionName); - variableCache.setCollection(collectionName, collection); - createdCollections++; - Logger.log(` Created fresh collection (replaced)`); - } - catch (e) { - Logger.log(` ⚠️ Could not replace collection: ${e}`); - continue; - } - } - else if (!options.merge) { - Logger.log(` Skipping existing collection: ${collectionName}`); - continue; - } - else { - collection = existingCollection; - Logger.log(` Merging into existing collection`); - } - } - else { - collection = figma.variables.createVariableCollection(collectionName); - variableCache.setCollection(collectionName, collection); - createdCollections++; - Logger.log(` Created new collection`); - } - // Setup modes - const modeNames = Object.keys(collectionContent.modes); - const modeMap = new Map(); - for (const mode of collection.modes) { - modeMap.set(mode.name, mode.modeId); - } - if (collection.modes.length === 1 && !modeMap.has(modeNames[0])) { - collection.renameMode(collection.modes[0].modeId, modeNames[0]); - modeMap.set(modeNames[0], collection.modes[0].modeId); - } - for (const modeName of modeNames) { - if (!modeMap.has(modeName)) { - try { - const newModeId = collection.addMode(modeName); - modeMap.set(modeName, newModeId); - } - catch (e) { - Logger.log(` ⚠️ Could not create mode ${modeName}: ${e}`); - } - } - } - // Process variables - TWO PASS APPROACH - // Pass 1: Create all variables and set RAW values only (skip aliases) - // Pass 2: Set ALIAS values (now all target variables exist) - const firstModeVars = collectionContent.modes[modeNames[0]]; - const variablePaths = flattenVariables(firstModeVars, ''); - // Store pending alias assignments for pass 2 - const pendingAliases = []; - // PASS 1: Create variables and set raw values - Logger.log(` Pass 1: Creating variables with raw values...`); - for (const { path, value } of variablePaths) { - const fullPath = `${collectionName}/${path}`; - let variable; - const existingVar = variableCache.getVariable(fullPath); - if (existingVar) { - if (!options.overwrite) { - skippedVariables++; - continue; - } - variable = existingVar; - updatedVariables++; - } - else { - try { - variable = figma.variables.createVariable(path, collection, TypeMapper.toFigmaType(value.$type)); - createdVariables++; - } - catch (e) { - Logger.log(` ⚠️ Could not create variable ${path}: ${e}`); - continue; - } - } - if (value.$description) { - variable.description = value.$description; - } - try { - variable.scopes = TypeMapper.arrayToScopes(value.$scopes); - } - catch ( /* Skip */_c) { /* Skip */ } - // Set values for each mode - raw values only in pass 1, queue aliases for pass 2 - for (const modeName of modeNames) { - const modeId = modeMap.get(modeName); - if (!modeId) - continue; - const modeValue = getValueAtPath(collectionContent.modes[modeName], path); - if (!modeValue) - continue; - if (typeof modeValue.$value === 'string' && modeValue.$value.startsWith('{')) { - // This is an alias - queue for pass 2 - const aliasPath = modeValue.$value.slice(1, -1).replace(/\./g, '/'); - const aliasCollection = modeValue.$collectionName || collectionName; - pendingAliases.push({ - variable, - modeId, - aliasPath, - aliasCollection, - fallbackValue: modeValue - }); - // Set a temporary raw value in case alias resolution fails - setRawValue(variable, modeId, modeValue); - } - else { - // Raw value - set immediately - setRawValue(variable, modeId, modeValue); - } - } - variableCache.setVariable(fullPath, variable); - } - // Store pending aliases for this collection (will be processed after all collections) - allPendingAliases.push(...pendingAliases); - } - // PASS 2: Resolve all aliases (now all variables from all collections exist) - Logger.log(`📥 Pass 2: Resolving ${allPendingAliases.length} alias references...`); - await variableCache.rebuild(); // Ensure cache has all newly created variables - let aliasesResolved = 0; - let aliasesFailed = 0; - for (const pending of allPendingAliases) { - const targetVar = variableCache.getVariable(`${pending.aliasCollection}/${pending.aliasPath}`); - if (targetVar) { - try { - pending.variable.setValueForMode(pending.modeId, { type: 'VARIABLE_ALIAS', id: targetVar.id }); - aliasesResolved++; - } - catch (e) { - // Alias failed, raw value was already set as fallback - aliasesFailed++; - Logger.log(` ⚠️ Could not set alias for ${pending.variable.name}: ${e}`); - } - } - else { - // Target not found, raw value was already set as fallback - aliasesFailed++; - Logger.log(` ⚠️ Alias target not found: ${pending.aliasCollection}/${pending.aliasPath}`); - } - } - if (allPendingAliases.length > 0) { - Logger.log(` ✅ Aliases: ${aliasesResolved} resolved, ${aliasesFailed} used fallback values`); - } - // Import styles - if (stylesData && options.importStyles) { - Logger.log('📦 Importing styles...'); - await variableCache.rebuild(); - if (stylesData.colorStyles) { - const r = await ColorStyleProcessor.importStyles(stylesData.colorStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - if (stylesData.textStyles) { - const r = await TextStyleProcessor.importStyles(stylesData.textStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - if (stylesData.effectStyles) { - const r = await EffectStyleProcessor.importStyles(stylesData.effectStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - if (stylesData.gridStyles) { - const r = await GridStyleProcessor.importStyles(stylesData.gridStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - } - const stats = { - collectionsCreated: createdCollections, - variablesCreated: createdVariables, - variablesUpdated: updatedVariables, - variablesSkipped: skippedVariables, - stylesCreated, - stylesUpdated - }; - Logger.log(`✅ Import complete!`); - Logger.send('import_complete', { stats }); - } - catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - Logger.log(`❌ Import error: ${errorMessage}`); - // Automatic rollback if we have a pre-import snapshot - if (preImportSnapshot) { - Logger.log('🔄 Attempting automatic rollback to pre-import state...'); - Logger.send('import_rolling_back', { error: errorMessage }); - try { - await restoreFromSnapshot(preImportSnapshot); - Logger.log('✅ Automatic rollback successful - file restored to pre-import state'); - Logger.send('import_rollback_complete', { - error: errorMessage, - message: 'Import failed but your file has been automatically restored to its previous state.' - }); - } - catch (rollbackError) { - const rollbackErrorMsg = rollbackError instanceof Error ? rollbackError.message : String(rollbackError); - Logger.log(`❌ Rollback failed: ${rollbackErrorMsg}`); - Logger.send('import_rollback_failed', { - error: errorMessage, - rollbackError: rollbackErrorMsg, - message: 'Import failed and automatic rollback also failed. Please use Ctrl+Z (Cmd+Z) to undo manually.' - }); - } - } - else { - // No snapshot available - just report the error - Logger.send('error', { - message: `Import failed: ${errorMessage}. Use Ctrl+Z (Cmd+Z) to undo changes.` - }); - } - } -} -function setRawValue(variable, modeId, value) { - try { - if (value.$type === 'color') { - const rgba = ColorParser.parse(value.$value); - const finalRgba = rgba.a < 1 - ? Object.assign(Object.assign({}, rgba), { a: MathUtils.round2(rgba.a) }) : rgba; - variable.setValueForMode(modeId, finalRgba); - } - else { - variable.setValueForMode(modeId, value.$value); - } - } - catch (e) { - console.error(`Could not set value: ${e}`); - } -} -// ============================================================================ -// SECTION 13: COLLECTION INFO -// ============================================================================ -async function getCollections() { - const collections = await figma.variables.getLocalVariableCollectionsAsync(); - // Log the raw order from Figma API - Logger.log(`📋 Figma API returned ${collections.length} collections in this order:`); - collections.forEach((c, i) => { - Logger.log(` ${i + 1}. "${c.name}" (id: ${c.id})`); - }); - // Track library dependencies and aliases - const libraryDependencies = new Set(); - let totalAliases = 0; - let localAliases = 0; - let libraryAliases = 0; - // Process sequentially to preserve exact order - const data = []; - for (let index = 0; index < collections.length; index++) { - const c = collections[index]; - const types = { color: 0, float: 0, boolean: 0, string: 0 }; - for (const varId of c.variableIds) { - const variable = await figma.variables.getVariableByIdAsync(varId); - if (variable) { - const typeStr = TypeMapper.toExportType(variable.resolvedType); - types[typeStr]++; - // Check for aliases in all modes - for (const modeId of Object.keys(variable.valuesByMode)) { - const value = variable.valuesByMode[modeId]; - if (typeof value === 'object' && value !== null && 'type' in value && value.type === 'VARIABLE_ALIAS') { - totalAliases++; - const aliasedVar = await figma.variables.getVariableByIdAsync(value.id); - if (aliasedVar) { - const aliasedCollection = await figma.variables.getVariableCollectionByIdAsync(aliasedVar.variableCollectionId); - if (aliasedCollection) { - // Check if it's from a remote/library collection - if (aliasedCollection.remote) { - libraryDependencies.add(aliasedCollection.name); - libraryAliases++; - } - else { - localAliases++; - } - } - } - } - } - } - } - data.push({ - id: c.id, - name: c.name, - modes: c.modes.map(m => m.name), - variableCount: c.variableIds.length, - types - }); - } - // Sort alphabetically since Figma API doesn't preserve Variables panel order - data.sort((a, b) => a.name.localeCompare(b.name)); - // Get styles and font info - const paintStyles = await figma.getLocalPaintStylesAsync(); - const textStyles = await figma.getLocalTextStylesAsync(); - const effectStyles = await figma.getLocalEffectStylesAsync(); - const gridStyles = await figma.getLocalGridStylesAsync(); - // Count only exportable paint styles (those with SOLID, GRADIENT, or IMAGE paints) - let exportablePaintStylesCount = 0; - for (const style of paintStyles) { - if (style.paints.length === 0) - continue; - const hasExportablePaint = style.paints.some(p => p.type === 'SOLID' || - p.type === 'GRADIENT_LINEAR' || - p.type === 'GRADIENT_RADIAL' || - p.type === 'GRADIENT_ANGULAR' || - p.type === 'GRADIENT_DIAMOND' || - p.type === 'IMAGE'); - if (hasExportablePaint) - exportablePaintStylesCount++; - } - const styles = { - colorStyles: exportablePaintStylesCount, - textStyles: textStyles.length, - effectStyles: effectStyles.length, - gridStyles: gridStyles.length - }; - // Extract font info from text styles - const fontsUsed = new Map(); - for (const style of textStyles) { - const family = style.fontName.family; - const fontStyle = style.fontName.style; - if (!fontsUsed.has(family)) { - fontsUsed.set(family, new Set()); - } - fontsUsed.get(family).add(fontStyle); - } - const fontsList = Array.from(fontsUsed.entries()).map(([family, styles]) => ({ - family, - styles: Array.from(styles) - })); - // Count variable bindings in paint styles - let styleBindingsCount = 0; - for (const style of paintStyles) { - if (style.boundVariables && Object.keys(style.boundVariables).length > 0) { - styleBindingsCount++; - } - } - Logger.send('collections', { - collections: data, - styles, - libraryDependencies: Array.from(libraryDependencies), - fontsUsed: fontsList, - stats: { - totalVariables: data.reduce((sum, c) => sum + c.variableCount, 0), - totalAliases, - localAliases, - libraryAliases, - styleBindings: styleBindingsCount - } - }); -} -async function getVariablesForCollection(collectionName) { - const allCollections = await figma.variables.getLocalVariableCollectionsAsync(); - const collection = allCollections.find(c => c.name === collectionName); - if (!collection) { - Logger.send('variables', { variables: [] }); - return; - } - const variables = (await Promise.all(collection.variableIds - .map(async (id) => { - const v = await figma.variables.getVariableByIdAsync(id); - return v ? { name: v.name, type: v.resolvedType } : null; - }))) - .filter(Boolean); - Logger.send('variables', { variables }); -} -// ============================================================================ -// SECTION 14: CLEAR FUNCTIONS -// ============================================================================ -async function clearVariables() { - Logger.log('🗑️ Clearing all variables...'); - try { - let deletedCollections = 0; - let deletedVariables = 0; - for (const collection of await figma.variables.getLocalVariableCollectionsAsync()) { - for (const varId of collection.variableIds) { - const variable = await figma.variables.getVariableByIdAsync(varId); - if (variable) { - variable.remove(); - deletedVariables++; - } - } - collection.remove(); - deletedCollections++; - } - Logger.log(`✅ Cleared ${deletedCollections} collections, ${deletedVariables} variables`); - Logger.send('clear_complete', { message: `${deletedCollections} collections, ${deletedVariables} variables` }); - } - catch (e) { - Logger.log(`❌ Clear variables error: ${e}`); - Logger.send('error', { message: `Failed to clear variables: ${e}` }); - } -} -async function clearStyles() { - Logger.log('🗑️ Clearing all styles...'); - try { - let deletedStyles = 0; - for (const style of await figma.getLocalPaintStylesAsync()) { - style.remove(); - deletedStyles++; - } - for (const style of await figma.getLocalTextStylesAsync()) { - style.remove(); - deletedStyles++; - } - for (const style of await figma.getLocalEffectStylesAsync()) { - style.remove(); - deletedStyles++; - } - for (const style of await figma.getLocalGridStylesAsync()) { - style.remove(); - deletedStyles++; - } - Logger.log(`✅ Cleared ${deletedStyles} styles`); - Logger.send('clear_complete', { message: `${deletedStyles} styles` }); - } - catch (e) { - Logger.log(`❌ Clear styles error: ${e}`); - Logger.send('error', { message: `Failed to clear styles: ${e}` }); - } -} -async function clearAll() { - Logger.log('🗑️ Clearing everything...'); - try { - await clearVariables(); - await clearStyles(); - } - catch (e) { - Logger.log(`❌ Clear all error: ${e}`); - Logger.send('error', { message: `Failed to clear: ${e}` }); - } -} -// Create a snapshot of current variables and styles for undo -async function createUndoSnapshot() { - var _a; - Logger.log('📸 Creating snapshot of current file state...'); - // Export all collections using simplified internal format - const collections = await figma.variables.getLocalVariableCollectionsAsync(); - const snapshotCollections = []; - for (const collection of collections) { - const collectionSnapshot = { - name: collection.name, - modes: collection.modes.map(m => ({ id: m.modeId, name: m.name })), - variables: [] - }; - // Process variables - for (const variableId of collection.variableIds) { - const variable = await figma.variables.getVariableByIdAsync(variableId); - if (!variable) - continue; - const varSnapshot = { - name: variable.name, - type: variable.resolvedType, - scopes: [...variable.scopes], - values: {} - }; - for (const mode of collection.modes) { - const value = variable.valuesByMode[mode.modeId]; - if (typeof value === 'object' && value !== null && 'type' in value && value.type === 'VARIABLE_ALIAS') { - // Handle alias - const aliasId = value.id; - const aliasVariable = await figma.variables.getVariableByIdAsync(aliasId); - if (aliasVariable) { - const aliasCollection = await figma.variables.getVariableCollectionByIdAsync(aliasVariable.variableCollectionId); - varSnapshot.values[mode.name] = { - isAlias: true, - aliasName: aliasVariable.name, - aliasCollection: (aliasCollection === null || aliasCollection === void 0 ? void 0 : aliasCollection.name) || '' - }; - } - } - else { - // Handle raw values - if (variable.resolvedType === 'COLOR') { - const rgba = value; - varSnapshot.values[mode.name] = { - isAlias: false, - value: ColorConverter.toHex(rgba) - }; - } - else { - varSnapshot.values[mode.name] = { - isAlias: false, - value: value - }; - } - } - } - collectionSnapshot.variables.push(varSnapshot); - } - snapshotCollections.push(collectionSnapshot); - } - // Export all styles - const stylesExport = { - colorStyles: await ColorStyleProcessor.export({ includeImages: true }), - textStyles: await TextStyleProcessor.export(), - effectStyles: await EffectStyleProcessor.export(), - gridStyles: await GridStyleProcessor.export() - }; - const colorCount = ((_a = stylesExport.colorStyles) === null || _a === void 0 ? void 0 : _a.length) || 0; - Logger.log(`📸 Snapshot captured: ${collections.length} collections, ${colorCount} color styles`); - return { - timestamp: Date.now(), - collections: JSON.stringify(snapshotCollections), - styles: JSON.stringify(stylesExport) - }; -} -// Restore file state from a snapshot (undo) -async function restoreFromSnapshot(snapshot) { - Logger.log('↩️ Restoring file from snapshot...'); - // Step 1: Clear everything - Logger.log(' Step 1: Clearing current state...'); - await clearAll(); - await variableCache.rebuild(); - // Step 2: Restore collections and variables - const snapshotCollections = JSON.parse(snapshot.collections); - Logger.log(` Step 2: Restoring ${snapshotCollections.length} collections...`); - // First pass: Create collections and variables with raw values - const pendingAliases = []; - for (const collSnapshot of snapshotCollections) { - // Create collection - const newCollection = figma.variables.createVariableCollection(collSnapshot.name); - // Setup modes - if (collSnapshot.modes.length > 0) { - // Rename first mode - newCollection.renameMode(newCollection.modes[0].modeId, collSnapshot.modes[0].name); - // Add additional modes - for (let i = 1; i < collSnapshot.modes.length; i++) { - newCollection.addMode(collSnapshot.modes[i].name); - } - } - // Get mode mapping - const modeMap = {}; - for (const mode of newCollection.modes) { - modeMap[mode.name] = mode.modeId; - } - // Process variables - for (const varSnapshot of collSnapshot.variables) { - // Create variable - pass collection node, not ID (required for incremental mode) - const newVar = figma.variables.createVariable(varSnapshot.name, newCollection, varSnapshot.type); - // Set scopes if available - if (varSnapshot.scopes && varSnapshot.scopes.length > 0) { - newVar.scopes = varSnapshot.scopes; - } - // Set values for each mode - for (const modeSnapshot of collSnapshot.modes) { - const modeId = modeMap[modeSnapshot.name]; - const modeValue = varSnapshot.values[modeSnapshot.name]; - if (!modeValue) - continue; - if (modeValue.isAlias && modeValue.aliasName) { - // Queue alias for second pass - pendingAliases.push({ - variable: newVar, - modeId, - aliasPath: modeValue.aliasName, - aliasCollection: modeValue.aliasCollection || collSnapshot.name - }); - } - else if (modeValue.value !== undefined) { - // Set raw value - let rawValue; - if (varSnapshot.type === 'COLOR' && typeof modeValue.value === 'string') { - rawValue = ColorParser.parse(modeValue.value); - } - else { - rawValue = modeValue.value; - } - newVar.setValueForMode(modeId, rawValue); - } - } - } - } - // Second pass: Resolve aliases - Logger.log(` Step 3: Resolving ${pendingAliases.length} aliases...`); - await variableCache.rebuild(); - for (const alias of pendingAliases) { - const targetKey = `${alias.aliasCollection}/${alias.aliasPath}`; - const targetVar = variableCache.getVariable(targetKey); - if (targetVar) { - alias.variable.setValueForMode(alias.modeId, { type: 'VARIABLE_ALIAS', id: targetVar.id }); - } - } - // Step 4: Restore styles - const stylesData = JSON.parse(snapshot.styles); - Logger.log(' Step 4: Restoring styles...'); - if (stylesData.colorStyles && stylesData.colorStyles.length > 0) { - await ColorStyleProcessor.importStyles(stylesData.colorStyles, variableCache); - } - if (stylesData.textStyles && stylesData.textStyles.length > 0) { - await TextStyleProcessor.importStyles(stylesData.textStyles, variableCache); - } - if (stylesData.effectStyles && stylesData.effectStyles.length > 0) { - await EffectStyleProcessor.importStyles(stylesData.effectStyles, variableCache); - } - if (stylesData.gridStyles && stylesData.gridStyles.length > 0) { - await GridStyleProcessor.importStyles(stylesData.gridStyles, variableCache); - } - Logger.log('✅ File restored from snapshot'); -} -// ============================================================================ -// SECTION 15: MESSAGE HANDLER -// ============================================================================ -figma.ui.onmessage = async (msg) => { - switch (msg.type) { - case 'export': - await exportVariables(msg.collections, msg.styleOptions, msg.preserveLibraryRefs, msg.includeImages, msg.namingConvention || 'original', msg.exportFormat || 'figma', msg.selectedModes, msg.resolveAliases || false); - break; - case 'import': - await importVariables(msg.data, msg.options); - break; - case 'validate_import': - // Pre-import validation to check plan limits - try { - const importData = JSON.parse(msg.data); - const planOverride = msg.plan; - const validation = await validateImportAgainstPlan(importData, planOverride); - Logger.send('validation_result', validation); - } - catch (e) { - Logger.send('validation_result', { - errors: [`Invalid JSON: ${e instanceof Error ? e.message : 'Parse error'}`], - canImport: false - }); - } - break; - case 'compute_import_diff': - // Compute what will change before importing - try { - const diffData = JSON.parse(msg.data); - const diff = await computeImportDiff(diffData); - Logger.send('import_diff_result', diff); - } - catch (e) { - Logger.send('import_diff_result', { - error: `Failed to compute diff: ${e instanceof Error ? e.message : 'Unknown error'}` - }); - } - break; - case 'detect_plan': - // Detect current plan based on existing collections - const detectedPlan = await detectCurrentPlan(); - Logger.send('plan_detected', detectedPlan); - break; - case 'clear_variables': - await clearVariables(); - break; - case 'clear_styles': - await clearStyles(); - break; - case 'clear_all': - await clearAll(); - break; - case 'get_collections': - await getCollections(); - break; - case 'get_variables': - await getVariablesForCollection(msg.collection); - break; - case 'check_libraries': - // Check if required library collections are available - try { - const requiredCollections = msg.collections; - // Initialize cache to index both local and library collections - await variableCache.rebuild(); - const availableCollections = []; - const missingCollections = []; - for (const collectionName of requiredCollections) { - if (variableCache.isCollectionAvailable(collectionName)) { - availableCollections.push(collectionName); - } - else { - missingCollections.push(collectionName); - } - } - Logger.log(`📚 Library check: ${availableCollections.length} available, ${missingCollections.length} missing`); - if (availableCollections.length > 0) { - Logger.log(` ✅ Available: ${availableCollections.join(', ')}`); - } - if (missingCollections.length > 0) { - Logger.log(` ❌ Missing: ${missingCollections.join(', ')}`); - } - Logger.send('library_check_result', { - allAvailable: missingCollections.length === 0, - availableCollections, - missingCollections, - requiredCollections - }); - } - catch (e) { - Logger.send('library_check_result', { - allAvailable: false, - availableCollections: [], - missingCollections: msg.collections || [], - requiredCollections: msg.collections || [], - error: e instanceof Error ? e.message : 'Library check failed' - }); - } - break; - case 'check_fonts': - // Check if required fonts are available - try { - const requiredFonts = msg.fonts; - const availableFonts = []; - const missingFonts = []; - // Check each font by attempting to load it - for (const font of requiredFonts) { - try { - await figma.loadFontAsync({ family: font.family, style: font.style }); - availableFonts.push(font); - } - catch (_a) { - missingFonts.push(font); - } - } - Logger.send('font_check_result', { - allAvailable: missingFonts.length === 0, - availableFonts, - missingFonts, - requiredFonts - }); - } - catch (e) { - Logger.send('font_check_result', { - allAvailable: false, - availableFonts: [], - missingFonts: msg.fonts || [], - requiredFonts: msg.fonts || [], - error: e instanceof Error ? e.message : 'Font check failed' - }); - } - break; - case 'create_undo_snapshot': - // Create a snapshot of current variables and styles for undo capability - try { - Logger.log('📸 Creating undo snapshot...'); - const snapshot = await createUndoSnapshot(); - Logger.send('snapshot_created', { snapshot }); - Logger.log('✅ Undo snapshot created successfully'); - } - catch (e) { - Logger.log(`❌ Failed to create snapshot: ${e instanceof Error ? e.message : 'Unknown error'}`); - Logger.send('snapshot_error', { error: e instanceof Error ? e.message : 'Failed to create snapshot' }); - } - break; - case 'undo_import': - // Restore file to pre-import state using snapshot - try { - Logger.log('↩️ Undoing import using snapshot...'); - const snapshotData = msg.snapshot; - await restoreFromSnapshot(snapshotData); - Logger.send('undo_complete', {}); - Logger.log('✅ Import undone successfully'); - } - catch (e) { - Logger.log(`❌ Undo failed: ${e instanceof Error ? e.message : 'Unknown error'}`); - Logger.send('undo_error', { error: e instanceof Error ? e.message : 'Undo failed' }); - } - break; - case 'close': - figma.closePlugin(); - break; - } -}; + */figma.showUI(__html__,{width:1200,height:628,themeColors:!0,title:"☕️ Variables & Styles Extractor v2.0.0"});const Logger={log(e,t){console.log(`[Variables Extractor] ${e}`,t||""),figma.ui.postMessage({type:"log",message:e,data:t})},send(e,t){figma.ui.postMessage({type:e,data:t})}},PLAN_LIMITS={starter:{maxModesPerCollection:1,canPublishLibraries:!1,hasVariableRestApi:!1},professional:{maxModesPerCollection:10,canPublishLibraries:!0,hasVariableRestApi:!1},organization:{maxModesPerCollection:20,canPublishLibraries:!0,hasVariableRestApi:!1},enterprise:{maxModesPerCollection:1/0,canPublishLibraries:!0,hasVariableRestApi:!0}},MAX_VARIABLES_PER_COLLECTION=5e3;async function detectCurrentPlan(){const e=await figma.variables.getLocalVariableCollectionsAsync();let t,o=1;for(const t of e)t.modes.length>o&&(o=t.modes.length);return t=o>20?"enterprise":o>10?"organization":"professional",Object.assign({plan:t},PLAN_LIMITS[t])}async function validateImportAgainstPlan(e,t){const o=t?Object.assign({plan:t},PLAN_LIMITS[t]):await detectCurrentPlan(),a=await figma.variables.getLocalVariableCollectionsAsync(),s=a.reduce((e,t)=>Math.max(e,t.modes.length),0),i=(await figma.variables.getLocalVariablesAsync()).length,r=[];for(const t of e)"_styles"in t||r.push(t);let l=0,n=0;const c=[];for(const e of r){const t=Object.keys(e)[0],a=e[t];if(!a||!a.modes)continue;const s=Object.keys(a.modes).length;s>l&&(l=s),s>o.maxModesPerCollection&&c.push(`"${t}" (${s} modes, limit: ${o.maxModesPerCollection===1/0?"∞":o.maxModesPerCollection})`);const i=Object.values(a.modes)[0];i&&(n+=countNestedVariables(i))}const g=[],f=[];c.length;for(const e of r){const t=Object.keys(e)[0],o=e[t];if(!o||!o.modes)continue;const a=Object.values(o.modes)[0],s=a?countNestedVariables(a):0;s>5e3&&f.push(`Collection "${t}" has ${s} variables, exceeds limit of 5000`)}n>1e3&&g.push(`Large import: ${n} variables. This may take a moment.`),r.length>10&&g.push(`Importing ${r.length} collections. Consider importing in batches.`);const d=new Set;let m=0;for(const e of r){const t=e[Object.keys(e)[0]];if(t&&t.modes)for(const e of Object.keys(t.modes)){const o=flattenVariables(t.modes[e],"");for(const{value:e}of o)e.$libraryRef&&e.$collectionName&&(d.add(e.$collectionName),m++)}}const u=[];let y=0;for(const t of e)if("_styles"in t){const e=t._styles;if(e.textStyles)for(const t of e.textStyles){y++;const e=`${t.fontFamily}|${t.fontStyle}`;u.some(t=>`${t.family}|${t.style}`===e)||u.push({family:t.fontFamily,style:t.fontStyle})}}return Object.assign(Object.assign({currentPlan:o,existing:{collections:a.length,maxModesInAnyCollection:s,totalVariables:i},importing:{collections:r.length,maxModesInAnyCollection:l,totalVariables:n,collectionsExceedingModeLimit:c},warnings:g,errors:f,canImport:0===f.length},d.size>0&&{libraryDependencies:{variableCount:m,collections:Array.from(d)}}),u.length>0&&{fontDependencies:{styleCount:y,fonts:u}})}function countNestedVariables(e,t=0){for(const[,o]of Object.entries(e))o&&"object"==typeof o&&("$type"in o&&"$value"in o?t++:t=countNestedVariables(o,t));return t}const MathUtils={round2:e=>Math.round(100*e)/100,clamp:(e,t,o)=>Math.max(t,Math.min(o,e)),toHexByte:e=>Math.round(255*e).toString(16).padStart(2,"0"),fromHexByte:e=>parseInt(e,16)/255};function calculateHue(e,t,o,a,s){if(a===s)return 0;const i=a-s;let r=0;switch(a){case e:r=((t-o)/i+(t.5?e/(2-s-i):e/(s+i)}const n={h:calculateHue(t,o,a,s,i),s:Math.round(100*l),l:Math.round(100*r)},c=e.a;return void 0!==c&&c<1?Object.assign(Object.assign({},n),{a:MathUtils.round2(c)}):n},toHsb(e){const{r:t,g:o,b:a}=e,s=Math.max(t,o,a),i=Math.min(t,o,a),r=0===s?0:(s-i)/s,l={h:calculateHue(t,o,a,s,i),s:Math.round(100*r),b:Math.round(100*s)},n=e.a;return void 0!==n&&n<1?Object.assign(Object.assign({},l),{a:MathUtils.round2(n)}):l},toAllFormats(e){return{hex:this.toHex(e),rgb:this.toRgb255(e),css:this.toCss(e),hsl:this.toHsl(e),hsb:this.toHsb(e)}}},NamingConverter={convert(e,t){if("original"===t)return e;const o=e.replace(/([a-z])([A-Z])/g,"$1 $2").split(/[\s\/\-_]+/).filter(e=>e.length>0).map(e=>e.toLowerCase());if(0===o.length)return e;switch(t){case"camelCase":return o[0]+o.slice(1).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("");case"kebab-case":return o.join("-");case"snake_case":return o.join("_");default:return e}},convertPath(e,t){return"original"===t?e:e.split("/").map(e=>this.convert(e,t)).join("/")},convertCollectionName(e,t){return this.convert(e,t)},convertModeName(e,t){return this.convert(e,t)},addOriginalName(e,t){if("original"===t)return{converted:e};const o=this.convert(e,t);return o===e?{converted:e}:{converted:o,original:e}}};async function resolveAliasValue(e,t,o=10){if(o<=0)return Logger.log(`⚠️ Max alias resolution depth reached for ${e.name}`),"";let a=e.valuesByMode[t];if(void 0===a){const t=Object.keys(e.valuesByMode);t.length>0&&(a=e.valuesByMode[t[0]])}if(void 0===a)return"";if(isVariableAlias(a)){const e=await figma.variables.getVariableByIdAsync(a.id);return e?resolveAliasValue(e,t,o-1):""}return a}const W3C_TYPE_MAP={color:"color",float:"number",string:"string",boolean:"boolean"},W3CConverter={colorToW3C:e=>e.hex,typeToW3C:e=>W3C_TYPE_MAP[e]||"string",valueToW3C(e,t=!1){const o={$value:"",$type:this.typeToW3C(e.$type)};return t&&"string"==typeof e.$value&&e.$value.startsWith("{")?o.$value=e.$value:"color"===e.$type&&"object"==typeof e.$value?o.$value=e.$value.hex:o.$value=e.$value,e.$description&&(o.$description=e.$description),e.$scopes&&e.$scopes.length>0&&!e.$scopes.includes("ALL_SCOPES")&&(o.$extensions={"com.figma":{scopes:e.$scopes}}),o},collectionToW3C(e,t,o,a){const s={};a&&a!==e&&(s.$description=`Figma collection: ${a}`);const i=Object.keys(t);if(1===i.length)this.addTokensToGroup(s,t[i[0]],o);else for(const e of i){const a=NamingConverter.convertModeName(e,o);s[a]={},this.addTokensToGroup(s[a],t[e],o)}return s},addTokensToGroup(e,t,o){for(const[a,s]of Object.entries(t)){const t=NamingConverter.convert(a,o);if(isExportVariableValue(s)){const o="string"==typeof s.$value&&s.$value.startsWith("{");e[t]=this.valueToW3C(s,o)}else e[t]={},this.addTokensToGroup(e[t],s,o)}},parseW3CToken(e){var t,o;const a=this.w3cTypeToFigma(e.$type),s=(null===(o=null===(t=e.$extensions)||void 0===t?void 0:t["com.figma"])||void 0===o?void 0:o.scopes)||["ALL_SCOPES"];let i;if("color"===a&&"string"==typeof e.$value){const t=ColorParser.parse(e.$value);i=ColorConverter.toAllFormats(t)}else i="string"==typeof e.$value||"number"==typeof e.$value||"boolean"==typeof e.$value?e.$value:JSON.stringify(e.$value);return e.$description?{$type:a,$value:i,$scopes:s,$description:e.$description}:{$type:a,$value:i,$scopes:s}},w3cTypeToFigma:e=>({color:"color",number:"float",dimension:"float",string:"string",boolean:"boolean",fontFamily:"string",fontWeight:"float",duration:"string",cubicBezier:"string"}[e]||"string"),isW3CFormat(e){if("object"!=typeof e||null===e)return!1;const t=e;for(const e of Object.keys(t)){const o=t[e];if("object"==typeof o&&null!==o){if("$value"in o&&"$type"in o)return!0;for(const e of Object.keys(o)){const t=o[e];if("object"==typeof t&&null!==t&&"$value"in t)return!0}}}return Array.isArray(e),!1},w3cToFigmaFormat(e){const t=[];for(const[o,a]of Object.entries(e)){if(o.startsWith("$"))continue;const e={[o]:{modes:{Default:this.w3cGroupToNestedVars(a)}}};t.push(e)}return t},w3cGroupToNestedVars(e){const t={};for(const[o,a]of Object.entries(e))o.startsWith("$")||(this.isW3CToken(a)?t[o]=this.parseW3CToken(a):"object"==typeof a&&null!==a&&(t[o]=this.w3cGroupToNestedVars(a)));return t},isW3CToken:e=>"object"==typeof e&&null!==e&&"$value"in e},HEX_REGEX_8=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,HEX_REGEX_6=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,RGBA_REGEX=/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/i,HSLA_REGEX=/hsla?\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*(?:,\s*([\d.]+))?\s*\)/i,ColorParser={fromHex(e){const t=HEX_REGEX_8.exec(e);if(t)return{r:MathUtils.fromHexByte(t[1]),g:MathUtils.fromHexByte(t[2]),b:MathUtils.fromHexByte(t[3]),a:MathUtils.fromHexByte(t[4])};const o=HEX_REGEX_6.exec(e);return o?{r:MathUtils.fromHexByte(o[1]),g:MathUtils.fromHexByte(o[2]),b:MathUtils.fromHexByte(o[3]),a:1}:{r:0,g:0,b:0,a:1}},fromRgb255(e){var t;return{r:e.r/255,g:e.g/255,b:e.b/255,a:null!==(t=e.a)&&void 0!==t?t:1}},fromCss(e){const t=RGBA_REGEX.exec(e);if(t)return{r:parseInt(t[1],10)/255,g:parseInt(t[2],10)/255,b:parseInt(t[3],10)/255,a:void 0!==t[4]?parseFloat(t[4]):1};const o=HSLA_REGEX.exec(e);return o?this.fromHsl({h:parseInt(o[1],10),s:parseInt(o[2],10),l:parseInt(o[3],10),a:void 0!==o[4]?parseFloat(o[4]):1}):{r:0,g:0,b:0,a:1}},fromHsl(e){var t,o;const a=e.h/360,s=e.s/100,i=e.l/100;if(0===s)return{r:i,g:i,b:i,a:null!==(t=e.a)&&void 0!==t?t:1};const hue2rgb=(e,t,o)=>{const a=o<0?o+1:o>1?o-1:o;return a<1/6?e+6*(t-e)*a:a<.5?t:a<2/3?e+(t-e)*(2/3-a)*6:e},r=i<.5?i*(1+s):i+s-i*s,l=2*i-r;return{r:hue2rgb(l,r,a+1/3),g:hue2rgb(l,r,a),b:hue2rgb(l,r,a-1/3),a:null!==(o=e.a)&&void 0!==o?o:1}},fromHsb(e){var t;const o=e.h/360,a=e.s/100,s=e.b/100,i=Math.floor(6*o),r=6*o-i,l=s*(1-a),n=s*(1-r*a),c=s*(1-(1-r)*a),g=[[s,c,l],[n,s,l],[l,s,c],[l,n,s],[c,l,s],[s,l,n]],[f,d,m]=g[i%6];return{r:f,g:d,b:m,a:null!==(t=e.a)&&void 0!==t?t:1}},parse(e){var t;if("object"==typeof e&&null!==e&&"hex"in e&&"rgb"in e)return this.fromHex(e.hex);if("object"==typeof e&&null!==e&&"r"in e&&"g"in e&&"b"in e){const o=e;return o.r<=1&&o.g<=1&&o.b<=1?{r:o.r,g:o.g,b:o.b,a:null!==(t=o.a)&&void 0!==t?t:1}:this.fromRgb255(o)}return"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"l"in e?this.fromHsl(e):"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"b"in e?this.fromHsb(e):"string"==typeof e?e.startsWith("rgb")||e.startsWith("hsl")?this.fromCss(e):this.fromHex(e):{r:0,g:0,b:0,a:1}}};class VariableCache{constructor(){this.collectionMap=new Map,this.variableMap=new Map,this.libraryVariableMap=new Map,this.libraryCollectionNames=new Set,this.initialized=!1}async initialize(){this.initialized||(await this.rebuild(),this.initialized=!0)}async rebuild(){this.collectionMap.clear(),this.variableMap.clear(),this.libraryVariableMap.clear(),this.libraryCollectionNames.clear();for(const e of await figma.variables.getLocalVariableCollectionsAsync()){this.collectionMap.set(e.name,e);for(const t of e.variableIds){const o=await figma.variables.getVariableByIdAsync(t);o&&this.variableMap.set(`${e.name}/${o.name}`,o)}}await this.indexLibraryVariables()}async indexLibraryVariables(){try{const e=await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();for(const t of e){this.libraryCollectionNames.add(t.name);try{const e=await figma.teamLibrary.getVariablesInLibraryCollectionAsync(t.key);for(const o of e)try{const e=await figma.variables.importVariableByKeyAsync(o.key);e&&this.libraryVariableMap.set(`${t.name}/${e.name}`,e)}catch(e){}}catch(e){Logger.log(` ⚠️ Could not index library collection "${t.name}": ${e}`)}}this.libraryCollectionNames.size>0&&Logger.log(`📚 Indexed ${this.libraryVariableMap.size} library variables from ${this.libraryCollectionNames.size} connected libraries`)}catch(e){Logger.log(`⚠️ Could not access team library: ${e}`)}}getCollection(e){return this.collectionMap.get(e)}getVariable(e){return this.variableMap.get(e)||this.libraryVariableMap.get(e)}setVariable(e,t){this.variableMap.set(e,t)}setCollection(e,t){this.collectionMap.set(e,t)}removeCollection(e){this.collectionMap.delete(e);const t=[];for(const o of this.variableMap.keys())o.startsWith(`${e}/`)&&t.push(o);for(const e of t)this.variableMap.delete(e)}isCollectionAvailable(e){return this.collectionMap.has(e)||this.libraryCollectionNames.has(e)}getLibraryCollectionNames(){return Array.from(this.libraryCollectionNames)}get size(){return this.variableMap.size}get collections(){return this.collectionMap.values()}getVariableKeys(){return Array.from(this.variableMap.keys())}}const variableCache=new VariableCache;function isExportVariableValue(e){return"object"==typeof e&&null!==e&&"$type"in e}function isVariableAlias(e){return"object"==typeof e&&null!==e&&"VARIABLE_ALIAS"===e.type}const TypeMapper={toExportType(e){var t;return null!==(t={COLOR:"color",FLOAT:"float",STRING:"string",BOOLEAN:"boolean"}[e])&&void 0!==t?t:"string"},toFigmaType(e){var t;return null!==(t={color:"COLOR",float:"FLOAT",string:"STRING",boolean:"BOOLEAN"}[e])&&void 0!==t?t:"STRING"},scopesToArray:e=>0===e.length||e.includes("ALL_SCOPES")?["ALL_SCOPES"]:[...e],arrayToScopes:e=>e.includes("ALL_SCOPES")?["ALL_SCOPES"]:e};async function getVariableBindingInfo(e,t){if(!(null==e?void 0:e[t]))return{};const o=e[t];if(!o)return{};const a=await figma.variables.getVariableByIdAsync(o.id);if(!a)return{id:o.id};const s=await figma.variables.getVariableCollectionByIdAsync(a.variableCollectionId);return{id:o.id,name:a.name,collection:null==s?void 0:s.name}}async function extractBindings(e,t){if(!e)return;const o={};for(const a of t){const t=await getVariableBindingInfo(e,a);t.name&&(o[a]=t)}return Object.keys(o).length>0?o:void 0}function flattenVariables(e,t){const o=[];for(const a of Object.keys(e)){const s=e[a],i=t?`${t}/${a}`:a;isExportVariableValue(s)?o.push({path:i,value:s}):o.push(...flattenVariables(s,i))}return o}function getValueAtPath(e,t){const o=t.split("/");let a=e;for(const e of o){if("object"!=typeof a||null===a)return null;if(isExportVariableValue(a))return null;a=a[e]}return isExportVariableValue(a)?a:null}const ColorStyleProcessor={async export(e){var t,o,a,s;const i=null!==(t=null==e?void 0:e.includeImages)&&void 0!==t&&t,r=[];for(const e of await figma.getLocalPaintStylesAsync()){if(0===e.paints.length)continue;const t=[];let l,n,c;for(const r of e.paints)if("SOLID"===r.type){const e=r.color;let a=null!==(o=r.opacity)&&void 0!==o?o:1;void 0!==e.a&&e.a<1&&1===a&&(a=e.a);const s={r:r.color.r,g:r.color.g,b:r.color.b,a:a},i={type:"SOLID",color:ColorConverter.toAllFormats(s),opacity:MathUtils.round2(a)};t.push(i),l||(l=i.color,n=i.opacity,c=await extractBindings(r.boundVariables,["color"]))}else if("GRADIENT_LINEAR"===r.type||"GRADIENT_RADIAL"===r.type||"GRADIENT_ANGULAR"===r.type||"GRADIENT_DIAMOND"===r.type){const e=r.gradientStops.map(e=>{var t;return{position:MathUtils.round2(e.position),color:ColorConverter.toAllFormats({r:e.color.r,g:e.color.g,b:e.color.b,a:null!==(t=e.color.a)&&void 0!==t?t:1})}}),o=Object.assign(Object.assign({type:r.type,gradientStops:e},r.gradientTransform&&{gradientTransform:r.gradientTransform}),{opacity:MathUtils.round2(null!==(a=r.opacity)&&void 0!==a?a:1)});t.push(o)}else if("IMAGE"===r.type){const o=Object.assign(Object.assign(Object.assign(Object.assign({type:"IMAGE",scaleMode:r.scaleMode},r.imageHash&&{imageHash:r.imageHash}),{opacity:MathUtils.round2(null!==(s=r.opacity)&&void 0!==s?s:1)}),void 0!==r.rotation&&{rotation:r.rotation}),r.filters&&{filters:Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},void 0!==r.filters.exposure&&{exposure:r.filters.exposure}),void 0!==r.filters.contrast&&{contrast:r.filters.contrast}),void 0!==r.filters.saturation&&{saturation:r.filters.saturation}),void 0!==r.filters.temperature&&{temperature:r.filters.temperature}),void 0!==r.filters.tint&&{tint:r.filters.tint}),void 0!==r.filters.highlights&&{highlights:r.filters.highlights}),void 0!==r.filters.shadows&&{shadows:r.filters.shadows})});if(i&&r.imageHash)try{const e=figma.getImageByHash(r.imageHash);if(e){const t=await e.getBytesAsync();if(t){const e=figma.base64Encode(t);o.imageBase64=e}}}catch(t){Logger.log(`⚠️ Could not export image data for style "${e.name}": ${t}`)}t.push(o)}if(0===t.length)continue;const g=Object.assign(Object.assign(Object.assign(Object.assign({name:e.name,paints:t},l&&{color:l}),void 0!==n&&{opacity:n}),e.description&&{description:e.description}),c&&Object.keys(c).length>0&&{boundVariables:c});r.push(g)}return r},async importStyles(e,t){var o,a,s,i;let r=0,l=0;const n=new Map;for(const e of await figma.getLocalPaintStylesAsync())n.set(e.name,e);for(const c of e){let e;n.has(c.name)?(e=n.get(c.name),l++):(e=figma.createPaintStyle(),e.name=c.name,r++),c.description&&(e.description=c.description);const g=[];if(c.paints&&c.paints.length>0){for(const e of c.paints)if("SOLID"===e.type){const a=ColorParser.parse(e.color);let s=null!==(o=e.opacity)&&void 0!==o?o:1;a.a<1&&void 0===e.opacity&&(s=MathUtils.round2(a.a));let i={type:"SOLID",color:{r:a.r,g:a.g,b:a.b},opacity:MathUtils.round2(s)};if(c.boundVariables&&0===g.length)for(const[e,o]of Object.entries(c.boundVariables))if(o.name&&o.collection){const a=t.getVariable(`${o.collection}/${o.name}`);if(a)try{i=figma.variables.setBoundVariableForPaint(i,e,a)}catch(t){Logger.log(`⚠️ Could not bind ${e}: ${t}`)}}g.push(i)}else if("GRADIENT_LINEAR"===e.type||"GRADIENT_RADIAL"===e.type||"GRADIENT_ANGULAR"===e.type||"GRADIENT_DIAMOND"===e.type){const t=e.gradientStops.map(e=>{const t=ColorParser.parse(e.color);return{position:e.position,color:{r:t.r,g:t.g,b:t.b,a:t.a}}}),o=e.gradientTransform?[[e.gradientTransform[0][0],e.gradientTransform[0][1],e.gradientTransform[0][2]],[e.gradientTransform[1][0],e.gradientTransform[1][1],e.gradientTransform[1][2]]]:[[1,0,0],[0,1,0]],s={type:e.type,gradientStops:t,gradientTransform:o,opacity:null!==(a=e.opacity)&&void 0!==a?a:1};g.push(s)}else if("IMAGE"===e.type){let t=null;if(e.imageBase64)try{const o=figma.base64Decode(e.imageBase64);t=figma.createImage(o).hash,Logger.log(`✅ Created image from base64 data for style "${c.name}"`)}catch(e){Logger.log(`⚠️ Could not import image from base64 for style "${c.name}": ${e}`)}if(!t&&e.imageHash){figma.getImageByHash(e.imageHash)?(t=e.imageHash,Logger.log(`✅ Found existing image with hash for style "${c.name}"`)):Logger.log(`⚠️ Image hash not found in file for style "${c.name}", skipping image paint (imageHash cannot be null)`)}if(t){const o=Object.assign(Object.assign({type:"IMAGE",scaleMode:e.scaleMode,imageHash:t,opacity:null!==(s=e.opacity)&&void 0!==s?s:1},void 0!==e.rotation&&{rotation:e.rotation}),e.filters&&{filters:e.filters});g.push(o)}}}else if(c.color){const e=ColorParser.parse(c.color);let o=null!==(i=c.opacity)&&void 0!==i?i:1;e.a<1&&void 0===c.opacity&&(o=MathUtils.round2(e.a));let a={type:"SOLID",color:{r:e.r,g:e.g,b:e.b},opacity:MathUtils.round2(o)};if(c.boundVariables)for(const[e,o]of Object.entries(c.boundVariables))if(o.name&&o.collection){const s=t.getVariable(`${o.collection}/${o.name}`);if(s)try{a=figma.variables.setBoundVariableForPaint(a,e,s)}catch(t){Logger.log(`⚠️ Could not bind ${e}: ${t}`)}}g.push(a)}g.length>0&&(e.paints=g)}return{created:r,updated:l}}},TextStyleProcessor={async export(e){const t=[];for(const e of await figma.getLocalTextStylesAsync()){const o=Object.assign(Object.assign({name:e.name,fontFamily:e.fontName.family,fontStyle:e.fontName.style,fontSize:e.fontSize,lineHeight:e.lineHeight,letterSpacing:e.letterSpacing,textCase:e.textCase,textDecoration:e.textDecoration},e.description&&{description:e.description}),{boundVariables:await extractBindings(e.boundVariables,["fontSize","lineHeight","letterSpacing","paragraphSpacing","paragraphIndent"])});t.push(o)}return t},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalTextStylesAsync())s.set(e.name,e);for(const i of e){let e;s.has(i.name)?(e=s.get(i.name),a++):(e=figma.createTextStyle(),e.name=i.name,o++),i.description&&(e.description=i.description);try{if(await figma.loadFontAsync({family:i.fontFamily,style:i.fontStyle}),e.fontName={family:i.fontFamily,style:i.fontStyle},e.fontSize=i.fontSize,e.lineHeight=i.lineHeight,e.letterSpacing=i.letterSpacing,i.textCase&&(e.textCase=i.textCase),i.textDecoration&&(e.textDecoration=i.textDecoration),i.boundVariables)for(const[o,a]of Object.entries(i.boundVariables))if(a.name&&a.collection){const s=t.getVariable(`${a.collection}/${a.name}`);if(s)try{e.setBoundVariable(o,s)}catch(e){}}}catch(e){Logger.log(`⚠️ Could not load font for ${i.name}: ${e}`)}}return{created:o,updated:a}}},EffectStyleProcessor={async export(e){const t=[];for(const e of await figma.getLocalEffectStylesAsync()){const o=[];for(const t of e.effects){const e=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:t.type,visible:t.visible},"radius"in t&&{radius:t.radius}),"spread"in t&&{spread:t.spread}),"offset"in t&&{offset:t.offset}),"color"in t&&{color:ColorConverter.toAllFormats(t.color)}),"blendMode"in t&&{blendMode:t.blendMode}),"showShadowBehindNode"in t&&{showShadowBehindNode:t.showShadowBehindNode}),{boundVariables:await extractBindings(t.boundVariables,["color","radius","spread","offsetX","offsetY"])});o.push(e)}const a=Object.assign(Object.assign({name:e.name},e.description&&{description:e.description}),{effects:o});t.push(a)}return t},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalEffectStylesAsync())s.set(e.name,e);for(const i of e){let e;s.has(i.name)?(e=s.get(i.name),a++):(e=figma.createEffectStyle(),e.name=i.name,o++),i.description&&(e.description=i.description);const r=i.effects.map(e=>{var t;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:e.type,visible:null===(t=e.visible)||void 0===t||t},void 0!==e.radius&&{radius:e.radius}),void 0!==e.spread&&{spread:e.spread}),void 0!==e.offset&&{offset:e.offset}),void 0!==e.color&&{color:(()=>{const t=ColorParser.parse(e.color);return{r:t.r,g:t.g,b:t.b,a:MathUtils.round2(t.a)}})()}),void 0!==e.blendMode&&{blendMode:e.blendMode}),void 0!==e.showShadowBehindNode&&{showShadowBehindNode:e.showShadowBehindNode})});e.effects=r;for(let o=0;o{var t,o,a,s,i,r,l;const n=e.color?ColorParser.parse(e.color):{r:1,g:0,b:0,a:.1},c={r:n.r,g:n.g,b:n.b,a:MathUtils.round2(n.a)};if("GRID"===e.pattern)return{pattern:"GRID",sectionSize:null!==(t=e.sectionSize)&&void 0!==t?t:10,visible:!1!==e.visible,color:c};const g=null!==(o=e.alignment)&&void 0!==o?o:"STRETCH",f={pattern:e.pattern,gutterSize:null!==(a=e.gutterSize)&&void 0!==a?a:10,count:null!==(s=e.count)&&void 0!==s?s:5,visible:!1!==e.visible,color:c};if("STRETCH"===g)return Object.assign(Object.assign({},f),{alignment:"STRETCH",offset:null!==(i=e.offset)&&void 0!==i?i:0});if("CENTER"===g)return Object.assign(Object.assign({},f),{alignment:"CENTER",sectionSize:null!==(r=e.sectionSize)&&void 0!==r?r:100});{const t=Object.assign(Object.assign({},f),{alignment:g,offset:null!==(l=e.offset)&&void 0!==l?l:0});return void 0!==e.sectionSize&&(t.sectionSize=e.sectionSize),t}});e.layoutGrids=r;for(let o=0;ot.name===e);a?await checkVariablesDiff(l,a.modeId,o,r,"",t):n=!0}t.modifiedVariables.some(e=>e.collection===r)||t.newVariables.some(e=>e.collection===r)?(t.modifiedCollections.push(r),t.summary.collectionsModified++):(t.unchangedCollections.push(r),t.summary.collectionsUnchanged++)}return t}function countVariablesInCollection(e){let t=0;const o=Object.values(e)[0];return o&&(t=countVarsInNestedObj(o)),t}function countVarsInNestedObj(e){let t=0;for(const o of Object.values(e))isExportVariableValue(o)?t++:t+=countVarsInNestedObj(o);return t}async function checkVariablesDiff(e,t,o,a,s,i){for(const[r,l]of Object.entries(o)){const o=s?`${s}/${r}`:r;if(isExportVariableValue(l)){const e=variableCache.getVariable(`${a}/${o}`);if(e){const s=e.valuesByMode[t],r=l.$value;valuesAreDifferent(s,r)?(i.modifiedVariables.push({collection:a,path:o,oldValue:formatValueForDisplay(s),newValue:formatValueForDisplay(r)}),i.summary.variablesModified++):(i.unchangedVariables++,i.summary.variablesUnchanged++)}else i.newVariables.push({collection:a,path:o}),i.summary.variablesNew++}else await checkVariablesDiff(e,t,l,a,o,i)}}function valuesAreDifferent(e,t){if(void 0===e)return!0;if(isVariableAlias(e))return"string"==typeof t&&t.startsWith("{"),!0;if("object"==typeof e&&null!==e&&"r"in e){if("object"==typeof t&&null!==t&&"hex"in t){return ColorConverter.toAllFormats(e).hex.toLowerCase()!==t.hex.toLowerCase()}return!0}return e!==t}function formatValueForDisplay(e){if(void 0===e)return"undefined";if("object"==typeof e&&null!==e){if("hex"in e)return e.hex;if("r"in e)return ColorConverter.toAllFormats(e).hex;if("id"in e)return"{alias}"}return String(e)}async function computeStylesDiff(e,t){if(e.colorStyles){const o=await figma.getLocalPaintStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.colorStyles)a.has(o.name)?(t.modifiedStyles.push({type:"color",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"color",name:o.name}),t.summary.stylesNew++)}if(e.textStyles){const o=await figma.getLocalTextStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.textStyles)a.has(o.name)?(t.modifiedStyles.push({type:"text",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"text",name:o.name}),t.summary.stylesNew++)}if(e.effectStyles){const o=await figma.getLocalEffectStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.effectStyles)a.has(o.name)?(t.modifiedStyles.push({type:"effect",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"effect",name:o.name}),t.summary.stylesNew++)}if(e.gridStyles){const o=await figma.getLocalGridStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.gridStyles)a.has(o.name)?(t.modifiedStyles.push({type:"grid",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"grid",name:o.name}),t.summary.stylesNew++)}}async function exportVariables(e,t,o,a,s="original",i="figma",r,l=!1){var n,c,g,f,d,m,u,y,p,b;Logger.log("📤 Starting export..."),Logger.log(` preserveLibraryRefs: ${o}`),Logger.log(` includeImages: ${a}`),Logger.log(` namingConvention: ${s}`),Logger.log(` exportFormat: ${i}`),Logger.log(` resolveAliases: ${l}`),r&&Logger.log(` selectedModes: ${JSON.stringify(r)}`);try{let o=await figma.variables.getLocalVariableCollectionsAsync();(null==e?void 0:e.length)&&(o=o.filter(t=>e.includes(t.name)),Logger.log(`Filtering to ${o.length} selected collections`));const h=[],v={};let C=0;for(const e of o){Logger.log(`Processing collection: ${e.name}`);let t=e.modes;if(r&&r[e.name]){const o=r[e.name];t=e.modes.filter(e=>o.includes(e.name)),Logger.log(` Filtering to ${t.length} modes: ${t.map(e=>e.name).join(", ")}`)}const o=NamingConverter.convertCollectionName(e.name,s),a={[o]:Object.assign({modes:{}},o!==e.name&&{$originalName:e.name})};for(const e of t){const t=NamingConverter.convertModeName(e.name,s);a[o].modes[t]={}}for(const i of e.variableIds){const e=await figma.variables.getVariableByIdAsync(i);if(!e)continue;C++;const r=e.name.split("/").map(e=>NamingConverter.convert(e,s));for(const i of t){const t=NamingConverter.convertModeName(i.name,s),g=a[o].modes[t],f=e.valuesByMode[i.modeId];let d=g;for(let e=0;eNamingConverter.convert(e,s)).join(".")}}`,u=v,h){const t=e.valuesByMode[Object.keys(e.valuesByMode)[0]];"object"==typeof t&&null!==t&&"r"in t?y=ColorConverter.toAllFormats(t):isVariableAlias(t)||(y=t)}}}else u=""}else u="object"==typeof f&&null!==f&&"r"in f?ColorConverter.toAllFormats(f):f;const C=Object.assign(Object.assign(Object.assign({$scopes:TypeMapper.scopesToArray(e.scopes),$type:TypeMapper.toExportType(e.resolvedType),$value:u},e.description&&{$description:e.description}),p&&b&&{$collectionName:b}),p&&h&&Object.assign({$libraryRef:v},void 0!==y&&{$localValue:y}));d[m]=C}}h.push(a),"w3c"===i&&(v[o]=W3CConverter.collectionToW3C(o,a[o].modes,s,a[o].$originalName))}let S=null;t&&(S={},t.colorStyles&&(S.colorStyles=await ColorStyleProcessor.export({includeImages:a})),t.textStyles&&(S.textStyles=await TextStyleProcessor.export()),t.effectStyles&&(S.effectStyles=await EffectStyleProcessor.export()),t.gridStyles&&(S.gridStyles=await GridStyleProcessor.export()),Object.keys(S).length>0?h.push({_styles:S}):S=null);const $={collections:o.length,variables:C,styles:S?{color:null!==(f=null===(g=S.colorStyles)||void 0===g?void 0:g.length)&&void 0!==f?f:0,text:null!==(m=null===(d=S.textStyles)||void 0===d?void 0:d.length)&&void 0!==m?m:0,effect:null!==(y=null===(u=S.effectStyles)||void 0===u?void 0:u.length)&&void 0!==y?y:0,grid:null!==(b=null===(p=S.gridStyles)||void 0===p?void 0:p.length)&&void 0!==b?b:0}:null};let L;"w3c"===i?(S&&Object.keys(S).length>0&&(v.$extensions={"com.figma":{styles:S}}),L=JSON.stringify(v,null,2),Logger.log(`✅ Export complete (W3C format): ${$.collections} collections, ${$.variables} variables`)):(L=JSON.stringify(h,null,2),Logger.log(`✅ Export complete: ${$.collections} collections, ${$.variables} variables`)),Logger.send("export_complete",{data:L,stats:$,format:i})}catch(e){Logger.log(`❌ Export error: ${e}`),Logger.send("error",{message:`Export failed: ${e}`})}}async function importVariables(e,t){var o,a;Logger.log("📥 Starting import..."),Logger.log(`📋 Import options: merge=${t.merge}, clearFirst=${t.clearFirst}, importStyles=${t.importStyles}`),Logger.log("📸 Creating pre-import snapshot for automatic rollback...");let s=null;try{s=await createUndoSnapshot(),Logger.log("✅ Pre-import snapshot created")}catch(e){Logger.log(`⚠️ Could not create pre-import snapshot: ${e}`)}try{let s,i=JSON.parse(e),r="figma";if(!Array.isArray(i)&&W3CConverter.isW3CFormat(i)){Logger.log("📄 Detected W3C Design Tokens format, converting..."),r="w3c";const e=i;let t=null;if(e.$extensions&&e.$extensions["com.figma"]){const o=e.$extensions["com.figma"];o.styles&&(t=o.styles),delete e.$extensions}s=W3CConverter.w3cToFigmaFormat(e),t&&s.push({_styles:t})}else s=i;if(t.clearFirst&&(Logger.log("🧹 Clean Import: Clearing existing variables and styles..."),await clearAll(),Logger.log("✅ Clean Import: Clearing complete, rebuilding cache..."),await variableCache.rebuild(),Logger.log("✅ Clean Import: Cache rebuilt, proceeding with import...")),t.customMerge){const{clearVariables:e,clearStyles:o}=t.customMerge;e&&o?(Logger.log("🎯 Custom Merge: Clearing both variables and styles..."),await clearAll()):e?(Logger.log("🎯 Custom Merge: Clearing variables only..."),await clearVariables()):o&&(Logger.log("🎯 Custom Merge: Clearing styles only..."),await clearStyles()),Logger.log("✅ Custom Merge: Clearing complete, rebuilding cache..."),await variableCache.rebuild()}await variableCache.initialize();let l=0,n=0,c=0,g=0,f=0,d=0,m=null;const u=[];for(const e of s){const t=Object.keys(e);1===t.length&&"_styles"===t[0]?m=e._styles:u.push(e)}const y=[];Logger.log(`📥 Pass 1: Processing ${u.length} collections...`);for(const e of u){const s=Object.keys(e)[0],i=e[s],r=i.$originalName||s;Logger.log(`Processing collection: ${s}${i.$originalName?` (original: ${r})`:""}`);const f=(null===(o=t.collectionBehaviors)||void 0===o?void 0:o[s])||(null===(a=t.collectionBehaviors)||void 0===a?void 0:a[r])||"merge";let d;const m=variableCache.getCollection(r);if(m)if("replace"===f){Logger.log(` Replacing collection: ${r}`);try{m.remove(),variableCache.removeCollection(r),d=figma.variables.createVariableCollection(r),variableCache.setCollection(r,d),l++,Logger.log(" Created fresh collection (replaced)")}catch(e){Logger.log(` ⚠️ Could not replace collection: ${e}`);continue}}else{if(!t.merge){Logger.log(` Skipping existing collection: ${r}`);continue}d=m,Logger.log(" Merging into existing collection")}else d=figma.variables.createVariableCollection(r),variableCache.setCollection(r,d),l++,Logger.log(" Created new collection");const u=Object.keys(i.modes),p=new Map;for(const e of d.modes)p.set(e.name,e.modeId);1!==d.modes.length||p.has(u[0])||(d.renameMode(d.modes[0].modeId,u[0]),p.set(u[0],d.modes[0].modeId));for(const e of u)if(!p.has(e))try{const t=d.addMode(e);p.set(e,t)}catch(t){Logger.log(` ⚠️ Could not create mode ${e}: ${t}`)}const b=flattenVariables(i.modes[u[0]],""),h=[];Logger.log(" Pass 1: Creating variables with raw values...");for(const{path:e,value:o}of b){const a=`${r}/${e}`;let s;const l=variableCache.getVariable(a);if(l){if(!t.overwrite){g++;continue}s=l,c++}else try{s=figma.variables.createVariable(e,d,TypeMapper.toFigmaType(o.$type)),n++}catch(t){Logger.log(` ⚠️ Could not create variable ${e}: ${t}`);continue}o.$description&&(s.description=o.$description);try{s.scopes=TypeMapper.arrayToScopes(o.$scopes)}catch(e){}for(const t of u){const o=p.get(t);if(!o)continue;const a=getValueAtPath(i.modes[t],e);if(a)if("string"==typeof a.$value&&a.$value.startsWith("{")){const e=a.$value.slice(1,-1).replace(/\./g,"/"),t=a.$collectionName||r;h.push({variable:s,modeId:o,aliasPath:e,aliasCollection:t,fallbackValue:a}),setRawValue(s,o,a)}else setRawValue(s,o,a)}variableCache.setVariable(a,s)}y.push(...h)}Logger.log(`📥 Pass 2: Resolving ${y.length} alias references...`),await variableCache.rebuild();let p=0,b=0;for(const e of y){const t=variableCache.getVariable(`${e.aliasCollection}/${e.aliasPath}`);if(t)try{e.variable.setValueForMode(e.modeId,{type:"VARIABLE_ALIAS",id:t.id}),p++}catch(t){b++,Logger.log(` ⚠️ Could not set alias for ${e.variable.name}: ${t}`)}else b++,Logger.log(` ⚠️ Alias target not found: ${e.aliasCollection}/${e.aliasPath}`)}if(y.length>0&&Logger.log(` ✅ Aliases: ${p} resolved, ${b} used fallback values`),m&&t.importStyles){if(Logger.log("📦 Importing styles..."),await variableCache.rebuild(),m.colorStyles){const e=await ColorStyleProcessor.importStyles(m.colorStyles,variableCache);f+=e.created,d+=e.updated}if(m.textStyles){const e=await TextStyleProcessor.importStyles(m.textStyles,variableCache);f+=e.created,d+=e.updated}if(m.effectStyles){const e=await EffectStyleProcessor.importStyles(m.effectStyles,variableCache);f+=e.created,d+=e.updated}if(m.gridStyles){const e=await GridStyleProcessor.importStyles(m.gridStyles,variableCache);f+=e.created,d+=e.updated}}const h={collectionsCreated:l,variablesCreated:n,variablesUpdated:c,variablesSkipped:g,stylesCreated:f,stylesUpdated:d};Logger.log("✅ Import complete!"),Logger.send("import_complete",{stats:h})}catch(e){const t=e instanceof Error?e.message:String(e);if(Logger.log(`❌ Import error: ${t}`),s){Logger.log("🔄 Attempting automatic rollback to pre-import state..."),Logger.send("import_rolling_back",{error:t});try{await restoreFromSnapshot(s),Logger.log("✅ Automatic rollback successful - file restored to pre-import state"),Logger.send("import_rollback_complete",{error:t,message:"Import failed but your file has been automatically restored to its previous state."})}catch(e){const o=e instanceof Error?e.message:String(e);Logger.log(`❌ Rollback failed: ${o}`),Logger.send("import_rollback_failed",{error:t,rollbackError:o,message:"Import failed and automatic rollback also failed. Please use Ctrl+Z (Cmd+Z) to undo manually."})}}else Logger.send("error",{message:`Import failed: ${t}. Use Ctrl+Z (Cmd+Z) to undo changes.`})}}function setRawValue(e,t,o){try{if("color"===o.$type){const a=ColorParser.parse(o.$value),s=a.a<1?Object.assign(Object.assign({},a),{a:MathUtils.round2(a.a)}):a;e.setValueForMode(t,s)}else e.setValueForMode(t,o.$value)}catch(e){console.error(`Could not set value: ${e}`)}}async function getCollections(){const e=await figma.variables.getLocalVariableCollectionsAsync();Logger.log(`📋 Figma API returned ${e.length} collections in this order:`),e.forEach((e,t)=>{Logger.log(` ${t+1}. "${e.name}" (id: ${e.id})`)});const t=new Set;let o=0,a=0,s=0;const i=[];for(let r=0;re.name),variableCount:l.variableIds.length,types:n})}i.sort((e,t)=>e.name.localeCompare(t.name));const r=await figma.getLocalPaintStylesAsync(),l=await figma.getLocalTextStylesAsync(),n=await figma.getLocalEffectStylesAsync(),c=await figma.getLocalGridStylesAsync();let g=0;for(const e of r){if(0===e.paints.length)continue;e.paints.some(e=>"SOLID"===e.type||"GRADIENT_LINEAR"===e.type||"GRADIENT_RADIAL"===e.type||"GRADIENT_ANGULAR"===e.type||"GRADIENT_DIAMOND"===e.type||"IMAGE"===e.type)&&g++}const f={colorStyles:g,textStyles:l.length,effectStyles:n.length,gridStyles:c.length},d=new Map;for(const e of l){const t=e.fontName.family,o=e.fontName.style;d.has(t)||d.set(t,new Set),d.get(t).add(o)}const m=Array.from(d.entries()).map(([e,t])=>({family:e,styles:Array.from(t)}));let u=0;for(const e of r)e.boundVariables&&Object.keys(e.boundVariables).length>0&&u++;Logger.send("collections",{collections:i,styles:f,libraryDependencies:Array.from(t),fontsUsed:m,stats:{totalVariables:i.reduce((e,t)=>e+t.variableCount,0),totalAliases:o,localAliases:a,libraryAliases:s,styleBindings:u}})}async function clearVariables(){Logger.log("🗑️ Clearing all variables...");try{let e=0,t=0;for(const o of await figma.variables.getLocalVariableCollectionsAsync()){for(const e of o.variableIds){const o=await figma.variables.getVariableByIdAsync(e);o&&(o.remove(),t++)}o.remove(),e++}Logger.log(`✅ Cleared ${e} collections, ${t} variables`),Logger.send("clear_complete",{message:`${e} collections, ${t} variables`})}catch(e){Logger.log(`❌ Clear variables error: ${e}`),Logger.send("error",{message:`Failed to clear variables: ${e}`})}}async function clearStyles(){Logger.log("🗑️ Clearing all styles...");try{let e=0;for(const t of await figma.getLocalPaintStylesAsync())t.remove(),e++;for(const t of await figma.getLocalTextStylesAsync())t.remove(),e++;for(const t of await figma.getLocalEffectStylesAsync())t.remove(),e++;for(const t of await figma.getLocalGridStylesAsync())t.remove(),e++;Logger.log(`✅ Cleared ${e} styles`),Logger.send("clear_complete",{message:`${e} styles`})}catch(e){Logger.log(`❌ Clear styles error: ${e}`),Logger.send("error",{message:`Failed to clear styles: ${e}`})}}async function clearAll(){Logger.log("🗑️ Clearing everything...");try{await clearVariables(),await clearStyles()}catch(e){Logger.log(`❌ Clear all error: ${e}`),Logger.send("error",{message:`Failed to clear: ${e}`})}}async function createUndoSnapshot(){var e;Logger.log("📸 Creating snapshot of current file state...");const t=await figma.variables.getLocalVariableCollectionsAsync(),o=[];for(const e of t){const t={name:e.name,modes:e.modes.map(e=>({id:e.modeId,name:e.name})),variables:[]};for(const o of e.variableIds){const a=await figma.variables.getVariableByIdAsync(o);if(!a)continue;const s={name:a.name,type:a.resolvedType,scopes:[...a.scopes],values:{}};for(const t of e.modes){const e=a.valuesByMode[t.modeId];if("object"==typeof e&&null!==e&&"type"in e&&"VARIABLE_ALIAS"===e.type){const o=e.id,a=await figma.variables.getVariableByIdAsync(o);if(a){const e=await figma.variables.getVariableCollectionByIdAsync(a.variableCollectionId);s.values[t.name]={isAlias:!0,aliasName:a.name,aliasCollection:(null==e?void 0:e.name)||""}}}else if("COLOR"===a.resolvedType){const o=e;s.values[t.name]={isAlias:!1,value:ColorConverter.toHex(o)}}else s.values[t.name]={isAlias:!1,value:e}}t.variables.push(s)}o.push(t)}const a={colorStyles:await ColorStyleProcessor.export({includeImages:!0}),textStyles:await TextStyleProcessor.export(),effectStyles:await EffectStyleProcessor.export(),gridStyles:await GridStyleProcessor.export()},s=(null===(e=a.colorStyles)||void 0===e?void 0:e.length)||0;return Logger.log(`📸 Snapshot captured: ${t.length} collections, ${s} color styles`),{timestamp:Date.now(),collections:JSON.stringify(o),styles:JSON.stringify(a)}}async function restoreFromSnapshot(e){Logger.log("↩️ Restoring file from snapshot..."),Logger.log(" Step 1: Clearing current state..."),await clearAll(),await variableCache.rebuild();const t=JSON.parse(e.collections);Logger.log(` Step 2: Restoring ${t.length} collections...`);const o=[];for(const e of t){const t=figma.variables.createVariableCollection(e.name);if(e.modes.length>0){t.renameMode(t.modes[0].modeId,e.modes[0].name);for(let o=1;o0&&(i.scopes=s.scopes);for(const t of e.modes){const r=a[t.name],l=s.values[t.name];if(l)if(l.isAlias&&l.aliasName)o.push({variable:i,modeId:r,aliasPath:l.aliasName,aliasCollection:l.aliasCollection||e.name});else if(void 0!==l.value){let e;e="COLOR"===s.type&&"string"==typeof l.value?ColorParser.parse(l.value):l.value,i.setValueForMode(r,e)}}}}Logger.log(` Step 3: Resolving ${o.length} aliases...`),await variableCache.rebuild();for(const e of o){const t=`${e.aliasCollection}/${e.aliasPath}`,o=variableCache.getVariable(t);o&&e.variable.setValueForMode(e.modeId,{type:"VARIABLE_ALIAS",id:o.id})}const a=JSON.parse(e.styles);Logger.log(" Step 4: Restoring styles..."),a.colorStyles&&a.colorStyles.length>0&&await ColorStyleProcessor.importStyles(a.colorStyles,variableCache),a.textStyles&&a.textStyles.length>0&&await TextStyleProcessor.importStyles(a.textStyles,variableCache),a.effectStyles&&a.effectStyles.length>0&&await EffectStyleProcessor.importStyles(a.effectStyles,variableCache),a.gridStyles&&a.gridStyles.length>0&&await GridStyleProcessor.importStyles(a.gridStyles,variableCache),Logger.log("✅ File restored from snapshot")}figma.ui.onmessage=async e=>{switch(e.type){case"export":await exportVariables(e.collections,e.styleOptions,e.preserveLibraryRefs,e.includeImages,e.namingConvention||"original",e.exportFormat||"figma",e.selectedModes,e.resolveAliases||!1);break;case"import":await importVariables(e.data,e.options);break;case"validate_import":try{const t=JSON.parse(e.data),o=e.plan,a=await validateImportAgainstPlan(t,o);Logger.send("validation_result",a)}catch(e){Logger.send("validation_result",{errors:[`Invalid JSON: ${e instanceof Error?e.message:"Parse error"}`],canImport:!1})}break;case"compute_import_diff":try{const t=JSON.parse(e.data),o=await computeImportDiff(t);Logger.send("import_diff_result",o)}catch(e){Logger.send("import_diff_result",{error:`Failed to compute diff: ${e instanceof Error?e.message:"Unknown error"}`})}break;case"detect_plan":const t=await detectCurrentPlan();Logger.send("plan_detected",t);break;case"clear_variables":await clearVariables();break;case"clear_styles":await clearStyles();break;case"clear_all":await clearAll();break;case"get_collections":await getCollections();break;case"check_libraries":try{const t=e.collections;await variableCache.rebuild();const o=[],a=[];for(const e of t)variableCache.isCollectionAvailable(e)?o.push(e):a.push(e);Logger.log(`📚 Library check: ${o.length} available, ${a.length} missing`),o.length>0&&Logger.log(` ✅ Available: ${o.join(", ")}`),a.length>0&&Logger.log(` ❌ Missing: ${a.join(", ")}`),Logger.send("library_check_result",{allAvailable:0===a.length,availableCollections:o,missingCollections:a,requiredCollections:t})}catch(t){Logger.send("library_check_result",{allAvailable:!1,availableCollections:[],missingCollections:e.collections||[],requiredCollections:e.collections||[],error:t instanceof Error?t.message:"Library check failed"})}break;case"check_fonts":try{const t=e.fonts,o=[],a=[];for(const e of t)try{await figma.loadFontAsync({family:e.family,style:e.style}),o.push(e)}catch(t){a.push(e)}Logger.send("font_check_result",{allAvailable:0===a.length,availableFonts:o,missingFonts:a,requiredFonts:t})}catch(t){Logger.send("font_check_result",{allAvailable:!1,availableFonts:[],missingFonts:e.fonts||[],requiredFonts:e.fonts||[],error:t instanceof Error?t.message:"Font check failed"})}break;case"create_undo_snapshot":try{Logger.log("📸 Creating undo snapshot...");const e=await createUndoSnapshot();Logger.send("snapshot_created",{snapshot:e}),Logger.log("✅ Undo snapshot created successfully")}catch(e){Logger.log(`❌ Failed to create snapshot: ${e instanceof Error?e.message:"Unknown error"}`),Logger.send("snapshot_error",{error:e instanceof Error?e.message:"Failed to create snapshot"})}break;case"undo_import":try{Logger.log("↩️ Undoing import using snapshot...");const t=e.snapshot;await restoreFromSnapshot(t),Logger.send("undo_complete",{}),Logger.log("✅ Import undone successfully")}catch(e){Logger.log(`❌ Undo failed: ${e instanceof Error?e.message:"Unknown error"}`),Logger.send("undo_error",{error:e instanceof Error?e.message:"Undo failed"})}}}; \ No newline at end of file diff --git a/variables-styles-extractor/manifest.json b/variables-styles-extractor/manifest.json index 31722a3..2b72728 100644 --- a/variables-styles-extractor/manifest.json +++ b/variables-styles-extractor/manifest.json @@ -11,5 +11,5 @@ "networkAccess": { "allowedDomains": ["none"] }, - "permissions": ["currentuser"] + "permissions": [] } diff --git a/variables-styles-extractor/releases/v1.6.0/KNOWN_ISSUES.md b/variables-styles-extractor/releases/v1.6.0/KNOWN_ISSUES.md deleted file mode 100644 index 26861a4..0000000 --- a/variables-styles-extractor/releases/v1.6.0/KNOWN_ISSUES.md +++ /dev/null @@ -1,44 +0,0 @@ -# Known Issues - v1.6.0 - -**Version**: 1.6.0 -**Status**: Published to Figma Community - ---- - -## Current Issues - -No known issues at this time. - ---- - -## Reporting Issues - -If you encounter a problem, please report it: - -### Via GitHub -1. Go to [GitHub Issues](https://github.com/tknatwork/side-kicks/issues) -2. Click "New Issue" -3. Select "Bug Report" template -4. Fill in all details - -### Information to Include -- Figma version -- Plugin version (shown in window title) -- Operating system -- Steps to reproduce -- Expected vs. actual behavior -- Export JSON if relevant (remove sensitive data) - ---- - -## Resolved Issues (Previous Versions) - -### Grid Import Validation (Fixed in v1.2.0) -Grid styles failed to import with "layoutGrids validation error". Fixed by using conditional property structure per alignment type. - -### Stack Underflow Error (Fixed in v1.5.5) -Plugin crashed with "stack underflow" on large files. Fixed by changing TypeScript target from ES2020 to ES2017. - ---- - -**Last Updated:** 2025-12-24 diff --git a/variables-styles-extractor/releases/v1.6.0/LICENSE b/variables-styles-extractor/releases/v1.6.0/LICENSE deleted file mode 100644 index a2f135d..0000000 --- a/variables-styles-extractor/releases/v1.6.0/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Tushar Kant Naik - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/variables-styles-extractor/releases/v1.6.0/NOTES.md b/variables-styles-extractor/releases/v1.6.0/NOTES.md deleted file mode 100644 index 3cdb308..0000000 --- a/variables-styles-extractor/releases/v1.6.0/NOTES.md +++ /dev/null @@ -1,24 +0,0 @@ -# v1.6.0 - 2025-12-22 - -## 🎉 First Public Release - -**Status:** Published to Figma Community - -### What's Included -- Full variable export/import with mode support -- All 4 style types (Color, Text, Effect, Grid) -- Variable aliases preserved -- Style-to-variable bindings preserved -- Plan-based mode limits validation -- Universal color parser - -### Files -- `code.js` - Minified plugin code (Terser) -- `ui.html` - Plugin UI -- `manifest.json` - Figma plugin manifest -- `LICENSE` - MIT License - -### Build Info -- TypeScript target: ES2017 -- Minification: Terser (--compress --mangle --keep-fnames) -- Source lines: ~1900 → Minified: 10 lines diff --git a/variables-styles-extractor/releases/v1.6.0/code.js b/variables-styles-extractor/releases/v1.6.0/code.js deleted file mode 100644 index ea82d27..0000000 --- a/variables-styles-extractor/releases/v1.6.0/code.js +++ /dev/null @@ -1,11 +0,0 @@ -"use strict"; -/** - * ☕️ Variables & Styles Extractor - Figma Plugin - * Export and import Figma variables and styles with full fidelity - * - * @copyright 2025 Tushar Kant Naik / The Keep Collective - * @license MIT - See LICENSE file - * @version 1.6.0 - * @author Tushar Kant Naik - * @website https://tusharkantnaik.com - */figma.showUI(__html__,{width:480,height:760,themeColors:!0,title:"☕️ Variables & Styles Extractor v1.6.0"});const Result={ok:e=>({ok:!0,value:e}),err:e=>({ok:!1,error:e})},Logger={log(e,t){console.log(`[Variables Extractor] ${e}`,t||""),figma.ui.postMessage({type:"log",message:e,data:t})},send(e,t){figma.ui.postMessage({type:e,data:t})}},PLAN_LIMITS={starter:{maxModesPerCollection:1,canPublishLibraries:!1,hasVariableRestApi:!1},professional:{maxModesPerCollection:10,canPublishLibraries:!0,hasVariableRestApi:!1},organization:{maxModesPerCollection:20,canPublishLibraries:!0,hasVariableRestApi:!1},enterprise:{maxModesPerCollection:1/0,canPublishLibraries:!0,hasVariableRestApi:!0}},MAX_VARIABLES_PER_COLLECTION=5e3;async function detectCurrentPlan(){const e=await figma.variables.getLocalVariableCollectionsAsync();let t,a=1;for(const t of e)t.modes.length>a&&(a=t.modes.length);return t=a>20?"enterprise":a>10?"organization":"professional",Object.assign({plan:t},PLAN_LIMITS[t])}async function validateImportAgainstPlan(e,t){const a=t?Object.assign({plan:t},PLAN_LIMITS[t]):await detectCurrentPlan(),o=await figma.variables.getLocalVariableCollectionsAsync(),s=o.reduce((e,t)=>Math.max(e,t.modes.length),0),i=(await figma.variables.getLocalVariablesAsync()).length,r=[];for(const t of e)"_styles"in t||r.push(t);let n=0,l=0;const c=[];for(const e of r){const t=Object.keys(e)[0],o=e[t];if(!o||!o.modes)continue;const s=Object.keys(o.modes).length;s>n&&(n=s),s>a.maxModesPerCollection&&c.push(`"${t}" (${s} modes, limit: ${a.maxModesPerCollection===1/0?"∞":a.maxModesPerCollection})`);const i=Object.values(o.modes)[0];i&&(l+=countNestedVariables(i))}const g=[],d=[];c.length;for(const e of r){const t=Object.keys(e)[0],a=e[t];if(!a||!a.modes)continue;const o=Object.values(a.modes)[0],s=o?countNestedVariables(o):0;s>5e3&&d.push(`Collection "${t}" has ${s} variables, exceeds limit of 5000`)}return l>1e3&&g.push(`Large import: ${l} variables. This may take a moment.`),r.length>10&&g.push(`Importing ${r.length} collections. Consider importing in batches.`),{currentPlan:a,existing:{collections:o.length,maxModesInAnyCollection:s,totalVariables:i},importing:{collections:r.length,maxModesInAnyCollection:n,totalVariables:l,collectionsExceedingModeLimit:c},warnings:g,errors:d,canImport:0===d.length}}function countNestedVariables(e,t=0){for(const[,a]of Object.entries(e))a&&"object"==typeof a&&("$type"in a&&"$value"in a?t++:t=countNestedVariables(a,t));return t}const MathUtils={round2:e=>Math.round(100*e)/100,clamp:(e,t,a)=>Math.max(t,Math.min(a,e)),toHexByte:e=>Math.round(255*e).toString(16).padStart(2,"0"),fromHexByte:e=>parseInt(e,16)/255};function calculateHue(e,t,a,o,s){if(o===s)return 0;const i=o-s;let r=0;switch(o){case e:r=((t-a)/i+(t.5?e/(2-s-i):e/(s+i)}const l={h:calculateHue(t,a,o,s,i),s:Math.round(100*n),l:Math.round(100*r)},c=e.a;return void 0!==c&&c<1?Object.assign(Object.assign({},l),{a:MathUtils.round2(c)}):l},toHsb(e){const{r:t,g:a,b:o}=e,s=Math.max(t,a,o),i=Math.min(t,a,o),r=0===s?0:(s-i)/s,n={h:calculateHue(t,a,o,s,i),s:Math.round(100*r),b:Math.round(100*s)},l=e.a;return void 0!==l&&l<1?Object.assign(Object.assign({},n),{a:MathUtils.round2(l)}):n},toAllFormats(e){return{hex:this.toHex(e),rgb:this.toRgb255(e),css:this.toCss(e),hsl:this.toHsl(e),hsb:this.toHsb(e)}}},HEX_REGEX_8=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,HEX_REGEX_6=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,RGBA_REGEX=/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/i,HSLA_REGEX=/hsla?\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*(?:,\s*([\d.]+))?\s*\)/i,ColorParser={fromHex(e){const t=HEX_REGEX_8.exec(e);if(t)return{r:MathUtils.fromHexByte(t[1]),g:MathUtils.fromHexByte(t[2]),b:MathUtils.fromHexByte(t[3]),a:MathUtils.fromHexByte(t[4])};const a=HEX_REGEX_6.exec(e);return a?{r:MathUtils.fromHexByte(a[1]),g:MathUtils.fromHexByte(a[2]),b:MathUtils.fromHexByte(a[3]),a:1}:{r:0,g:0,b:0,a:1}},fromRgb255(e){var t;return{r:e.r/255,g:e.g/255,b:e.b/255,a:null!==(t=e.a)&&void 0!==t?t:1}},fromCss(e){const t=RGBA_REGEX.exec(e);if(t)return{r:parseInt(t[1],10)/255,g:parseInt(t[2],10)/255,b:parseInt(t[3],10)/255,a:void 0!==t[4]?parseFloat(t[4]):1};const a=HSLA_REGEX.exec(e);return a?this.fromHsl({h:parseInt(a[1],10),s:parseInt(a[2],10),l:parseInt(a[3],10),a:void 0!==a[4]?parseFloat(a[4]):1}):{r:0,g:0,b:0,a:1}},fromHsl(e){var t,a;const o=e.h/360,s=e.s/100,i=e.l/100;if(0===s)return{r:i,g:i,b:i,a:null!==(t=e.a)&&void 0!==t?t:1};const hue2rgb=(e,t,a)=>{const o=a<0?a+1:a>1?a-1:a;return o<1/6?e+6*(t-e)*o:o<.5?t:o<2/3?e+(t-e)*(2/3-o)*6:e},r=i<.5?i*(1+s):i+s-i*s,n=2*i-r;return{r:hue2rgb(n,r,o+1/3),g:hue2rgb(n,r,o),b:hue2rgb(n,r,o-1/3),a:null!==(a=e.a)&&void 0!==a?a:1}},fromHsb(e){var t;const a=e.h/360,o=e.s/100,s=e.b/100,i=Math.floor(6*a),r=6*a-i,n=s*(1-o),l=s*(1-r*o),c=s*(1-(1-r)*o),g=[[s,c,n],[l,s,n],[n,s,c],[n,l,s],[c,n,s],[s,n,l]],[d,f,b]=g[i%6];return{r:d,g:f,b:b,a:null!==(t=e.a)&&void 0!==t?t:1}},parse(e){var t;if("object"==typeof e&&null!==e&&"hex"in e&&"rgb"in e)return this.fromHex(e.hex);if("object"==typeof e&&null!==e&&"r"in e&&"g"in e&&"b"in e){const a=e;return a.r<=1&&a.g<=1&&a.b<=1?{r:a.r,g:a.g,b:a.b,a:null!==(t=a.a)&&void 0!==t?t:1}:this.fromRgb255(a)}return"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"l"in e?this.fromHsl(e):"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"b"in e?this.fromHsb(e):"string"==typeof e?e.startsWith("rgb")||e.startsWith("hsl")?this.fromCss(e):this.fromHex(e):{r:0,g:0,b:0,a:1}}};class VariableCache{constructor(){this.collectionMap=new Map,this.variableMap=new Map,this.initialized=!1}async initialize(){this.initialized||(await this.rebuild(),this.initialized=!0)}async rebuild(){this.collectionMap.clear(),this.variableMap.clear();for(const e of await figma.variables.getLocalVariableCollectionsAsync()){this.collectionMap.set(e.name,e);for(const t of e.variableIds){const a=await figma.variables.getVariableByIdAsync(t);a&&this.variableMap.set(`${e.name}/${a.name}`,a)}}}getCollection(e){return this.collectionMap.get(e)}getVariable(e){return this.variableMap.get(e)}setVariable(e,t){this.variableMap.set(e,t)}setCollection(e,t){this.collectionMap.set(e,t)}get size(){return this.variableMap.size}get collections(){return this.collectionMap.values()}getVariableKeys(){return Array.from(this.variableMap.keys())}}const variableCache=new VariableCache;function isExportVariableValue(e){return"object"==typeof e&&null!==e&&"$type"in e}function isVariableAlias(e){return"object"==typeof e&&null!==e&&"VARIABLE_ALIAS"===e.type}const TypeMapper={toExportType(e){var t;return null!==(t={COLOR:"color",FLOAT:"float",STRING:"string",BOOLEAN:"boolean"}[e])&&void 0!==t?t:"string"},toFigmaType(e){var t;return null!==(t={color:"COLOR",float:"FLOAT",string:"STRING",boolean:"BOOLEAN"}[e])&&void 0!==t?t:"STRING"},scopesToArray:e=>0===e.length||e.includes("ALL_SCOPES")?["ALL_SCOPES"]:[...e],arrayToScopes:e=>e.includes("ALL_SCOPES")?["ALL_SCOPES"]:e};async function getVariableBindingInfo(e,t){if(!(null==e?void 0:e[t]))return{};const a=e[t];if(!a)return{};const o=await figma.variables.getVariableByIdAsync(a.id);if(!o)return{id:a.id};const s=await figma.variables.getVariableCollectionByIdAsync(o.variableCollectionId);return{id:a.id,name:o.name,collection:null==s?void 0:s.name}}async function extractBindings(e,t){if(!e)return;const a={};for(const o of t){const t=await getVariableBindingInfo(e,o);t.name&&(a[o]=t)}return Object.keys(a).length>0?a:void 0}function flattenVariables(e,t){const a=[];for(const o of Object.keys(e)){const s=e[o],i=t?`${t}/${o}`:o;isExportVariableValue(s)?a.push({path:i,value:s}):a.push(...flattenVariables(s,i))}return a}function getValueAtPath(e,t){const a=t.split("/");let o=e;for(const e of a){if("object"!=typeof o||null===o)return null;if(isExportVariableValue(o))return null;o=o[e]}return isExportVariableValue(o)?o:null}const ColorStyleProcessor={async export(){var e;const t=[];for(const a of await figma.getLocalPaintStylesAsync()){if(0===a.paints.length)continue;const o=a.paints[0];if("SOLID"!==o.type)continue;const s=o.color;let i=null!==(e=o.opacity)&&void 0!==e?e:1;void 0!==s.a&&s.a<1&&1===i&&(i=s.a);const r={r:o.color.r,g:o.color.g,b:o.color.b,a:i},n=Object.assign(Object.assign({name:a.name,color:ColorConverter.toAllFormats(r),opacity:MathUtils.round2(i)},a.description&&{description:a.description}),{boundVariables:await extractBindings(o.boundVariables,["color"])});t.push(n)}return t},async importStyles(e,t){var a;let o=0,s=0;const i=new Map;for(const e of await figma.getLocalPaintStylesAsync())i.set(e.name,e);for(const r of e){let e;i.has(r.name)?(e=i.get(r.name),s++):(e=figma.createPaintStyle(),e.name=r.name,o++),r.description&&(e.description=r.description);const n=ColorParser.parse(r.color);let l=null!==(a=r.opacity)&&void 0!==a?a:1;n.a<1&&void 0===r.opacity&&(l=MathUtils.round2(n.a));let c={type:"SOLID",color:{r:n.r,g:n.g,b:n.b},opacity:MathUtils.round2(l)};if(r.boundVariables)for(const[e,a]of Object.entries(r.boundVariables))if(a.name&&a.collection){const o=t.getVariable(`${a.collection}/${a.name}`);if(o)try{c=figma.variables.setBoundVariableForPaint(c,e,o)}catch(t){Logger.log(`⚠️ Could not bind ${e}: ${t}`)}}e.paints=[c]}return{created:o,updated:s}}},TextStyleProcessor={async export(){const e=[];for(const t of await figma.getLocalTextStylesAsync()){const a=Object.assign(Object.assign({name:t.name,fontFamily:t.fontName.family,fontStyle:t.fontName.style,fontSize:t.fontSize,lineHeight:t.lineHeight,letterSpacing:t.letterSpacing,textCase:t.textCase,textDecoration:t.textDecoration},t.description&&{description:t.description}),{boundVariables:await extractBindings(t.boundVariables,["fontSize","lineHeight","letterSpacing","paragraphSpacing","paragraphIndent"])});e.push(a)}return e},async importStyles(e,t){let a=0,o=0;const s=new Map;for(const e of await figma.getLocalTextStylesAsync())s.set(e.name,e);for(const i of e){let e;s.has(i.name)?(e=s.get(i.name),o++):(e=figma.createTextStyle(),e.name=i.name,a++),i.description&&(e.description=i.description);try{if(await figma.loadFontAsync({family:i.fontFamily,style:i.fontStyle}),e.fontName={family:i.fontFamily,style:i.fontStyle},e.fontSize=i.fontSize,e.lineHeight=i.lineHeight,e.letterSpacing=i.letterSpacing,i.textCase&&(e.textCase=i.textCase),i.textDecoration&&(e.textDecoration=i.textDecoration),i.boundVariables)for(const[a,o]of Object.entries(i.boundVariables))if(o.name&&o.collection){const s=t.getVariable(`${o.collection}/${o.name}`);if(s)try{e.setBoundVariable(a,s)}catch(e){}}}catch(e){Logger.log(`⚠️ Could not load font for ${i.name}: ${e}`)}}return{created:a,updated:o}}},EffectStyleProcessor={async export(){const e=[];for(const t of await figma.getLocalEffectStylesAsync()){const a=[];for(const e of t.effects){const t=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:e.type,visible:e.visible},"radius"in e&&{radius:e.radius}),"spread"in e&&{spread:e.spread}),"offset"in e&&{offset:e.offset}),"color"in e&&{color:ColorConverter.toAllFormats(e.color)}),"blendMode"in e&&{blendMode:e.blendMode}),"showShadowBehindNode"in e&&{showShadowBehindNode:e.showShadowBehindNode}),{boundVariables:await extractBindings(e.boundVariables,["color","radius","spread","offsetX","offsetY"])});a.push(t)}const o=Object.assign(Object.assign({name:t.name},t.description&&{description:t.description}),{effects:a});e.push(o)}return e},async importStyles(e,t){let a=0,o=0;const s=new Map;for(const e of await figma.getLocalEffectStylesAsync())s.set(e.name,e);for(const i of e){let e;s.has(i.name)?(e=s.get(i.name),o++):(e=figma.createEffectStyle(),e.name=i.name,a++),i.description&&(e.description=i.description);const r=i.effects.map(e=>{var t;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:e.type,visible:null===(t=e.visible)||void 0===t||t},void 0!==e.radius&&{radius:e.radius}),void 0!==e.spread&&{spread:e.spread}),void 0!==e.offset&&{offset:e.offset}),void 0!==e.color&&{color:(()=>{const t=ColorParser.parse(e.color);return{r:t.r,g:t.g,b:t.b,a:MathUtils.round2(t.a)}})()}),void 0!==e.blendMode&&{blendMode:e.blendMode}),void 0!==e.showShadowBehindNode&&{showShadowBehindNode:e.showShadowBehindNode})});e.effects=r;for(let a=0;a{var t,a,o,s,i,r,n;const l=e.color?ColorParser.parse(e.color):{r:1,g:0,b:0,a:.1},c={r:l.r,g:l.g,b:l.b,a:MathUtils.round2(l.a)};if("GRID"===e.pattern)return{pattern:"GRID",sectionSize:null!==(t=e.sectionSize)&&void 0!==t?t:10,visible:!1!==e.visible,color:c};const g=null!==(a=e.alignment)&&void 0!==a?a:"STRETCH",d={pattern:e.pattern,gutterSize:null!==(o=e.gutterSize)&&void 0!==o?o:10,count:null!==(s=e.count)&&void 0!==s?s:5,visible:!1!==e.visible,color:c};if("STRETCH"===g)return Object.assign(Object.assign({},d),{alignment:"STRETCH",offset:null!==(i=e.offset)&&void 0!==i?i:0});if("CENTER"===g)return Object.assign(Object.assign({},d),{alignment:"CENTER",sectionSize:null!==(r=e.sectionSize)&&void 0!==r?r:100});{const t=Object.assign(Object.assign({},d),{alignment:g,offset:null!==(n=e.offset)&&void 0!==n?n:0});return void 0!==e.sectionSize&&(t.sectionSize=e.sectionSize),t}});e.layoutGrids=r;for(let a=0;ae.includes(t.name)),Logger.log(`Filtering to ${d.length} selected collections`));const f=[];let b=0;for(const e of d){Logger.log(`Processing collection: ${e.name}`);const t={[e.name]:{modes:{}}};for(const a of e.modes)t[e.name].modes[a.name]={};for(const o of e.variableIds){const s=await figma.variables.getVariableByIdAsync(o);if(!s)continue;b++;const i=s.name.split("/");for(const o of e.modes){const r=t[e.name].modes[o.name],n=s.valuesByMode[o.modeId];let l=r;for(let e=0;e0?f.push({_styles:u}):u=null);const p={collections:d.length,variables:b,styles:u?{color:null!==(s=null===(o=u.colorStyles)||void 0===o?void 0:o.length)&&void 0!==s?s:0,text:null!==(r=null===(i=u.textStyles)||void 0===i?void 0:i.length)&&void 0!==r?r:0,effect:null!==(l=null===(n=u.effectStyles)||void 0===n?void 0:n.length)&&void 0!==l?l:0,grid:null!==(g=null===(c=u.gridStyles)||void 0===c?void 0:c.length)&&void 0!==g?g:0}:null};Logger.log(`✅ Export complete: ${p.collections} collections, ${p.variables} variables`),Logger.send("export_complete",{data:JSON.stringify(f,null,2),stats:p})}catch(e){Logger.log(`❌ Export error: ${e}`),Logger.send("error",{message:`Export failed: ${e}`})}}async function importVariables(e,t){Logger.log("📥 Starting import...");try{const a=JSON.parse(e);await variableCache.initialize();let o=0,s=0,i=0,r=0,n=0,l=0,c=null;const g=[];for(const e of a){const t=Object.keys(e);1===t.length&&"_styles"===t[0]?c=e._styles:g.push(e)}for(const e of g){const a=Object.keys(e)[0],n=e[a];let l;Logger.log(`Processing collection: ${a}`);const c=variableCache.getCollection(a);if(c){if(!t.merge){Logger.log(` Skipping existing collection: ${a}`);continue}l=c,Logger.log(" Merging into existing collection")}else l=figma.variables.createVariableCollection(a),variableCache.setCollection(a,l),o++,Logger.log(" Created new collection");const g=Object.keys(n.modes),d=new Map;for(const e of l.modes)d.set(e.name,e.modeId);1!==l.modes.length||d.has(g[0])||(l.renameMode(l.modes[0].modeId,g[0]),d.set(g[0],l.modes[0].modeId));for(const e of g)if(!d.has(e))try{const t=l.addMode(e);d.set(e,t)}catch(t){Logger.log(` ⚠️ Could not create mode ${e}: ${t}`)}const f=flattenVariables(n.modes[g[0]],"");for(const{path:e,value:o}of f){const c=`${a}/${e}`;let f;const b=variableCache.getVariable(c);if(b){if(!t.overwrite){r++;continue}f=b,i++}else try{f=figma.variables.createVariable(e,l,TypeMapper.toFigmaType(o.$type)),s++}catch(t){Logger.log(` ⚠️ Could not create variable ${e}: ${t}`);continue}o.$description&&(f.description=o.$description);try{f.scopes=TypeMapper.arrayToScopes(o.$scopes)}catch(e){}for(const t of g){const o=d.get(t);if(!o)continue;const s=getValueAtPath(n.modes[t],e);if(s)if("string"==typeof s.$value&&s.$value.startsWith("{")){const e=s.$value.slice(1,-1).replace(/\./g,"/"),t=s.$collectionName||a,i=variableCache.getVariable(`${t}/${e}`);if(i)try{f.setValueForMode(o,{type:"VARIABLE_ALIAS",id:i.id})}catch(e){setRawValue(f,o,s)}else setRawValue(f,o,s)}else setRawValue(f,o,s)}variableCache.setVariable(c,f)}}if(c&&t.importStyles){if(Logger.log("📦 Importing styles..."),await variableCache.rebuild(),c.colorStyles){const e=await ColorStyleProcessor.importStyles(c.colorStyles,variableCache);n+=e.created,l+=e.updated}if(c.textStyles){const e=await TextStyleProcessor.importStyles(c.textStyles,variableCache);n+=e.created,l+=e.updated}if(c.effectStyles){const e=await EffectStyleProcessor.importStyles(c.effectStyles,variableCache);n+=e.created,l+=e.updated}if(c.gridStyles){const e=await GridStyleProcessor.importStyles(c.gridStyles,variableCache);n+=e.created,l+=e.updated}}const d={collectionsCreated:o,variablesCreated:s,variablesUpdated:i,variablesSkipped:r,stylesCreated:n,stylesUpdated:l};Logger.log("✅ Import complete!"),Logger.send("import_complete",{stats:d})}catch(e){Logger.log(`❌ Import error: ${e}`),Logger.send("error",{message:`Import failed: ${e}`})}}function setRawValue(e,t,a){try{if("color"===a.$type){const o=ColorParser.parse(a.$value),s=o.a<1?Object.assign(Object.assign({},o),{a:MathUtils.round2(o.a)}):o;e.setValueForMode(t,s)}else e.setValueForMode(t,a.$value)}catch(e){console.error(`Could not set value: ${e}`)}}async function getCollections(){const e=await figma.variables.getLocalVariableCollectionsAsync(),t=await Promise.all(e.map(async e=>{const t={color:0,float:0,boolean:0,string:0};for(const a of e.variableIds){const e=await figma.variables.getVariableByIdAsync(a);if(e){t[TypeMapper.toExportType(e.resolvedType)]++}}return{id:e.id,name:e.name,modes:e.modes.map(e=>e.name),variableCount:e.variableIds.length,types:t}})),a={colorStyles:(await figma.getLocalPaintStylesAsync()).length,textStyles:(await figma.getLocalTextStylesAsync()).length,effectStyles:(await figma.getLocalEffectStylesAsync()).length,gridStyles:(await figma.getLocalGridStylesAsync()).length};Logger.send("collections",{collections:t,styles:a})}async function getVariablesForCollection(e){const t=(await figma.variables.getLocalVariableCollectionsAsync()).find(t=>t.name===e);if(!t)return void Logger.send("variables",{variables:[]});const a=(await Promise.all(t.variableIds.map(async e=>{const t=await figma.variables.getVariableByIdAsync(e);return t?{name:t.name,type:t.resolvedType}:null}))).filter(Boolean);Logger.send("variables",{variables:a})}async function clearVariables(){Logger.log("🗑️ Clearing all variables...");try{let e=0,t=0;for(const a of await figma.variables.getLocalVariableCollectionsAsync()){for(const e of a.variableIds){const a=await figma.variables.getVariableByIdAsync(e);a&&(a.remove(),t++)}a.remove(),e++}Logger.log(`✅ Cleared ${e} collections, ${t} variables`),Logger.send("clear_complete",{message:`${e} collections, ${t} variables`})}catch(e){Logger.log(`❌ Clear variables error: ${e}`),Logger.send("error",{message:`Failed to clear variables: ${e}`})}}async function clearStyles(){Logger.log("🗑️ Clearing all styles...");try{let e=0;for(const t of await figma.getLocalPaintStylesAsync())t.remove(),e++;for(const t of await figma.getLocalTextStylesAsync())t.remove(),e++;for(const t of await figma.getLocalEffectStylesAsync())t.remove(),e++;for(const t of await figma.getLocalGridStylesAsync())t.remove(),e++;Logger.log(`✅ Cleared ${e} styles`),Logger.send("clear_complete",{message:`${e} styles`})}catch(e){Logger.log(`❌ Clear styles error: ${e}`),Logger.send("error",{message:`Failed to clear styles: ${e}`})}}async function clearAll(){Logger.log("🗑️ Clearing everything...");try{await clearVariables(),await clearStyles()}catch(e){Logger.log(`❌ Clear all error: ${e}`),Logger.send("error",{message:`Failed to clear: ${e}`})}}figma.ui.onmessage=async e=>{switch(e.type){case"export":await exportVariables(e.collections,e.styleOptions);break;case"import":await importVariables(e.data,e.options);break;case"validate_import":try{const t=JSON.parse(e.data),a=e.plan,o=await validateImportAgainstPlan(t,a);Logger.send("validation_result",o)}catch(e){Logger.send("validation_result",{errors:[`Invalid JSON: ${e instanceof Error?e.message:"Parse error"}`],canImport:!1})}break;case"detect_plan":const t=await detectCurrentPlan();Logger.send("plan_detected",t);break;case"clear_variables":await clearVariables();break;case"clear_styles":await clearStyles();break;case"clear_all":await clearAll();break;case"get_collections":await getCollections();break;case"get_variables":await getVariablesForCollection(e.collection);break;case"close":figma.closePlugin()}}; \ No newline at end of file diff --git a/variables-styles-extractor/releases/v1.6.0/manifest.json b/variables-styles-extractor/releases/v1.6.0/manifest.json deleted file mode 100644 index 31722a3..0000000 --- a/variables-styles-extractor/releases/v1.6.0/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Variables and Styles Extractor", - "id": "", - "api": "1.0.0", - "main": "code.js", - "ui": "ui.html", - "documentAccess": "dynamic-page", - "capabilities": [], - "enableProposedApi": false, - "editorType": ["figma"], - "networkAccess": { - "allowedDomains": ["none"] - }, - "permissions": ["currentuser"] -} diff --git a/variables-styles-extractor/releases/v1.6.0/ui.html b/variables-styles-extractor/releases/v1.6.0/ui.html deleted file mode 100644 index 407d654..0000000 --- a/variables-styles-extractor/releases/v1.6.0/ui.html +++ /dev/null @@ -1,2565 +0,0 @@ - - - - - - - Variables and Styles Extractor - - - - -

- - -
- - -
-
-
- Select Collections to Export - -
-
-
-
- Loading collections... -
-
-
- - -
-
- - -
-
Include Styles (Optional)
-
- - - - -
-

Styles connected to variables will preserve their bindings

-
- - - - -
- -

Exports variables and selected styles to JSON format

-
- - -
- - -
-
-
Import JSON
-
-
📁
-
Drop JSON file here or click to browse
- - -
-
Or paste JSON
- -
- - - - - - - -
-
Clear Before Import (Optional)
-

⚠️ Warning: These actions cannot be undone!

-
- - -
- -
- -
-
Import Options
- - - -
- -
- -

Paste or drop a JSON file to preview

-
-
- - -
-
Activity Log
-
-
- --:--:-- - Variables Extractor initialized -
-
-
- - - - - - - - - - diff --git a/variables-styles-extractor/releases/v2.0.0/KNOWN_ISSUES.md b/variables-styles-extractor/releases/v2.0.0/KNOWN_ISSUES.md deleted file mode 100644 index 25d4248..0000000 --- a/variables-styles-extractor/releases/v2.0.0/KNOWN_ISSUES.md +++ /dev/null @@ -1,139 +0,0 @@ -# Known Issues - Variables & Styles Extractor - -**Plugin**: Variables & Styles Extractor -**Current Version**: 1.6.0 (2.0.0 in development) -**Status**: Published to Figma Community - ---- - -## Current Issues - -*No known issues at this time.* - ---- - -## Resolved Issues - -### KI-008: External Collection References Not Resolved (v1.7.0) -**Status:** ✅ Fixed in v1.7.0 - -**Symptom:** During import, warnings appear stating "Alias target not found" for variables that reference collections not included in the export. - -**Cause:** Variables can reference (alias) variables from other collections. These external collections may be: -- **Library-linked collections:** Variables from published team/shared libraries (e.g., shape tokens, theme systems from external design systems) -- **Non-exported local collections:** Other local collections that weren't selected for export - -The Figma Plugin API only provides access to local collections via `getLocalVariableCollectionsAsync()`. Library-linked collections cannot be exported—they exist only as references within the current file. - -**Solution:** v1.7.0 now handles external dependencies transparently: -- **Export phase:** Detects which external collections are referenced but not included in the export -- **Metadata:** Records external collection names in the `externalCollections` array of the export JSON -- **Import phase:** Displays a clear warning listing missing external dependencies - -**User Action:** -- For **local collections**: Ensure all dependent collections are included in the export -- For **library collections**: These cannot be exported. Either: - - Ensure the target file is connected to the same team library, OR - - Accept that these aliases will remain unresolved and fall back to their raw values - ---- - -### KI-007: Collection Name Matching Fails Due to Whitespace/Unicode Variations (v1.7.0) -**Status:** ✅ Fixed in v1.7.0 - -**Symptom:** During import, "Alias target not found" warnings appear for variables that should exist, particularly when collection names contain emoji characters or special Unicode sequences. - -**Cause:** Collection names with emoji or special characters can have inconsistent whitespace representations (e.g., multiple spaces vs. single space) due to Unicode rendering differences. This caused exact string matching to fail even when the collections were semantically identical. - -**Solution:** v1.7.0 implements name normalization for robust matching: -- Applies Unicode NFC normalization -- Collapses consecutive whitespace into single spaces -- Trims leading and trailing whitespace -- Maintains secondary lookup maps for fuzzy matching while preserving original names - ---- - -### KI-006: Alias Resolution Fails During Import (v1.7.0) -**Status:** ✅ Fixed in v1.7.0 - -**Symptom:** During import, warnings appear stating "Alias target not found" followed by "Setting raw value" for variables that should reference other variables. - -**Cause:** Variables were imported in file order without considering dependencies. When a variable with an alias was imported before its target variable existed, the alias reference could not be resolved. - -**Solution:** v1.7.0 implements a multi-pass import strategy: -- **Pass 1:** Create all variables with raw (non-alias) values only -- **Pass 2:** Set all alias relationships after all target variables exist -- **Pass 3:** Import styles with full variable binding support - -This ensures all seed variables exist before dependent variables attempt to reference them. - ---- - -### KI-005: Variable Alias Type Mismatch Crash (v1.6.0) -**Status:** ✅ Fixed in v1.7.0 - -**Symptom:** Import fails with "Mismatched variable resolved type for mode X:Y" - -**Cause:** When importing aliases (e.g., `{Tailwind/slate/50}`), the target variable may have a different type than expected. For example, a COLOR variable trying to alias a FLOAT variable, or an unresolved alias string being set on a FLOAT variable. - -**Solution:** v1.7.0 now: -- Validates type compatibility before setting aliases -- Compares `resolvedType` of source and target variables -- Only sets alias if types match (COLOR→COLOR, FLOAT→FLOAT, etc.) -- Skips setting unresolved alias references as raw values (they would fail for non-STRING types) -- Falls back gracefully without crashing - ---- - -### KI-004: Image/Video Paint Import Crash (v1.7.0-dev) -**Status:** ✅ Fixed in v1.7.0 - -**Symptom:** Import fails with "Property paints failed validation: Required value missing at [0].imageHash" - -**Cause:** IMAGE and VIDEO paint fills have file-specific `imageHash` references that cannot be transferred between Figma files. - -**Solution:** v1.7.0 now supports full image transfer: -- **Export:** Check "Include image data" to embed images as base64 -- **Import:** Images are recreated with `figma.createImage()` and get new file-specific hashes -- **Without image data:** Image paints are gracefully skipped with a warning - ---- - -### KI-003: Color Styles Only Export Solid Paints (v1.6.0) -**Status:** ✅ Fixed in v1.7.0 - -**Symptom:** Export shows X color styles detected, but JSON file has `colorStyles: []` or fewer than expected. - -**Cause:** v1.6.0 only exports SOLID paint styles. Gradients, images, and empty styles were skipped. - -**Solution:** v1.7.0 adds full support for all paint types including gradients and images. - ---- - -### Grid Import Validation (Fixed in v1.2.0) -Grid styles failed to import with "layoutGrids validation error". Fixed by using conditional property structure per alignment type. - -### Stack Underflow Error (Fixed in v1.5.5) -Plugin crashed with "stack underflow" on large files. Fixed by changing TypeScript target from ES2020 to ES2017. - ---- - -## Reporting Issues - -### Via GitHub (Recommended) -1. Go to [GitHub Issues](https://github.com/tknatwork/side-kicks/issues) -2. Click "New Issue" -3. Select "Bug Report" template -4. Fill in all details - -### Information to Include -- Figma version -- Plugin version (shown in window title bar) -- Operating system -- Steps to reproduce -- Expected vs. actual behavior -- Export JSON if relevant (remove sensitive data first) - ---- - -**Last Updated:** 2026-01-05 diff --git a/variables-styles-extractor/releases/v2.0.0/LICENSE b/variables-styles-extractor/releases/v2.0.0/LICENSE deleted file mode 100644 index a2f135d..0000000 --- a/variables-styles-extractor/releases/v2.0.0/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2025 Tushar Kant Naik - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/variables-styles-extractor/releases/v2.0.0/NOTES.md b/variables-styles-extractor/releases/v2.0.0/NOTES.md deleted file mode 100644 index 1f21371..0000000 --- a/variables-styles-extractor/releases/v2.0.0/NOTES.md +++ /dev/null @@ -1,54 +0,0 @@ -# Variables & Styles Extractor v2.0.0 - -**Release Date:** January 2025 - -## 🎉 Major Release - Complete UI Overhaul - -### ✨ New Features - -- **Library Support** - Full support for library variables with detection and linking -- **Automatic Rollback** - Import failures automatically restore previous state -- **Smart Search** - Real-time search across all item types -- **Inline Edit Mode** - Edit JSON directly in the preview panel -- **Import All / Export All** - One-click operations for all item types - -### 🎨 UI Changes - -- Complete redesign with Export/Import two-panel layout -- Sticky navigation and search header -- Unified button bar with consistent styling -- Color-coded item type badges -- Improved visual feedback with animations - -### ⚡ Performance Improvements - -- **Web Worker** - JSON parsing offloaded to background thread -- **Result Caching** - LRU cache (10 entries, 60s TTL) for repeated operations -- **Throttled Validation** - 300ms debounce for smooth typing -- **Async Operations** - Non-blocking JSON stringify/parse - -### 🛡️ Reliability - -- Import button disables during processing (prevents double-clicks) -- Clear button flashes red after import (visual cue for next action) -- Comprehensive error handling with user-friendly messages -- Automatic state preservation during failures - -### 📁 Files Included - -- `code.js` - Compiled plugin backend -- `ui.html` - Plugin UI -- `manifest.json` - Figma plugin configuration -- `LICENSE` - MIT License - -### 📝 Installation - -1. Download all files in this folder -2. In Figma Desktop: Plugins → Development → Import plugin from manifest... -3. Select the `manifest.json` file - -### 🔗 Links - -- [Figma Community](https://www.figma.com/community/plugin/variables-styles-extractor) -- [GitHub Repository](https://github.com/tknatwork/side-kicks) -- [Documentation](../docs/) diff --git a/variables-styles-extractor/releases/v2.0.0/code.js b/variables-styles-extractor/releases/v2.0.0/code.js deleted file mode 100644 index 7d7a0d4..0000000 --- a/variables-styles-extractor/releases/v2.0.0/code.js +++ /dev/null @@ -1,2712 +0,0 @@ -"use strict"; -/** - * ☕️ Variables & Styles Extractor - Figma Plugin - * Export and import Figma variables and styles with full fidelity - * - * @copyright 2025 Tushar Kant Naik / The Keep Collective - * @license MIT - See LICENSE file - * @version 2.0.0 - * @author Tushar Kant Naik - * @website https://tusharkantnaik.com - */ -// JSF-AV Compliant Architecture -// v2.0.0: Wide 4-column layout (1200x628px content area, 680px with Figma title bar) -figma.showUI(__html__, { - width: 1200, - height: 628, - themeColors: true, - title: '☕️ Variables & Styles Extractor v2.0.0' -}); -const Result = { - ok: (value) => ({ ok: true, value }), - err: (error) => ({ ok: false, error }), -}; -// ============================================================================ -// SECTION 3: UTILITY FUNCTIONS (JSF Rule 4.15 - DRY) -// ============================================================================ -const Logger = { - log(message, data) { - console.log(`[Variables Extractor] ${message}`, data || ''); - figma.ui.postMessage({ type: 'log', message, data }); - }, - send(type, data) { - figma.ui.postMessage({ type, data }); - } -}; -// Plan limits by Figma subscription tier (verified from Figma documentation) -const PLAN_LIMITS = { - starter: { - maxModesPerCollection: 1, - canPublishLibraries: false, - hasVariableRestApi: false - }, - professional: { - maxModesPerCollection: 10, - canPublishLibraries: true, - hasVariableRestApi: false - }, - organization: { - maxModesPerCollection: 20, - canPublishLibraries: true, - hasVariableRestApi: false - }, - enterprise: { - maxModesPerCollection: Infinity, - canPublishLibraries: true, - hasVariableRestApi: true - } -}; -// Maximum variables per collection (all plans) -const MAX_VARIABLES_PER_COLLECTION = 5000; -// Plan detection: Figma API doesn't expose plan directly, so we infer from existing modes -async function detectCurrentPlan() { - const collections = await figma.variables.getLocalVariableCollectionsAsync(); - let maxModesFound = 1; - for (const collection of collections) { - if (collection.modes.length > maxModesFound) { - maxModesFound = collection.modes.length; - } - } - // Infer plan based on highest mode count found - let inferredPlan; - if (maxModesFound > 20) { - inferredPlan = 'enterprise'; - } - else if (maxModesFound > 10) { - inferredPlan = 'organization'; - } - else if (maxModesFound > 1) { - inferredPlan = 'professional'; - } - else { - // Can't distinguish starter from others with 1 mode, assume professional - // User can override in UI - inferredPlan = 'professional'; - } - return Object.assign({ plan: inferredPlan }, PLAN_LIMITS[inferredPlan]); -} -// Validate import data against plan limits -async function validateImportAgainstPlan(importData, planOverride) { - const currentPlan = planOverride - ? Object.assign({ plan: planOverride }, PLAN_LIMITS[planOverride]) : await detectCurrentPlan(); - const collections = await figma.variables.getLocalVariableCollectionsAsync(); - const existingMaxModes = collections.reduce((max, col) => Math.max(max, col.modes.length), 0); - const existingTotalVars = (await figma.variables.getLocalVariablesAsync()).length; - // Analyze import data - it's an array of collection exports and possibly _styles - const importCollections = []; - for (const item of importData) { - // Skip _styles entries - if ('_styles' in item) - continue; - importCollections.push(item); - } - let importingMaxModes = 0; - let importingTotalVars = 0; - const collectionsExceedingModeLimit = []; - for (const colExport of importCollections) { - // Each collection export is { "CollectionName": { modes: {...} } } - const colName = Object.keys(colExport)[0]; - const colData = colExport[colName]; - if (!colData || !colData.modes) - continue; - const modeCount = Object.keys(colData.modes).length; - if (modeCount > importingMaxModes) { - importingMaxModes = modeCount; - } - if (modeCount > currentPlan.maxModesPerCollection) { - collectionsExceedingModeLimit.push(`"${colName}" (${modeCount} modes, limit: ${currentPlan.maxModesPerCollection === Infinity ? '∞' : currentPlan.maxModesPerCollection})`); - } - // Count variables in first mode (they're the same across modes) - const firstMode = Object.values(colData.modes)[0]; - if (firstMode) { - importingTotalVars += countNestedVariables(firstMode); - } - } - // Generate warnings and errors - const warnings = []; - const errors = []; - // Mode limits - not a hard error, UI will show mode selection - // Only warn, don't block - user can select which modes to import - if (collectionsExceedingModeLimit.length > 0) { - // This is handled by the UI with mode selection - // Don't add to errors, just track in collectionsExceedingModeLimit - } - // Check variable count per collection - this IS a hard limit - for (const colExport of importCollections) { - const colName = Object.keys(colExport)[0]; - const colData = colExport[colName]; - if (!colData || !colData.modes) - continue; - const firstMode = Object.values(colData.modes)[0]; - const varCount = firstMode ? countNestedVariables(firstMode) : 0; - if (varCount > MAX_VARIABLES_PER_COLLECTION) { - errors.push(`Collection "${colName}" has ${varCount} variables, exceeds limit of ${MAX_VARIABLES_PER_COLLECTION}`); - } - } - // Warnings for large imports - if (importingTotalVars > 1000) { - warnings.push(`Large import: ${importingTotalVars} variables. This may take a moment.`); - } - if (importCollections.length > 10) { - warnings.push(`Importing ${importCollections.length} collections. Consider importing in batches.`); - } - // Detect library dependencies (variables that reference external collections) - const libraryCollections = new Set(); - let libraryVarCount = 0; - for (const colExport of importCollections) { - const colName = Object.keys(colExport)[0]; - const colData = colExport[colName]; - if (!colData || !colData.modes) - continue; - for (const modeName of Object.keys(colData.modes)) { - const modeData = colData.modes[modeName]; - const variables = flattenVariables(modeData, ''); - for (const { value } of variables) { - if (value.$libraryRef && value.$collectionName) { - libraryCollections.add(value.$collectionName); - libraryVarCount++; - } - } - } - } - // Detect font dependencies from text styles - const fontDeps = []; - let fontStyleCount = 0; - // Check for _styles in import data - for (const item of importData) { - if ('_styles' in item) { - const stylesData = item._styles; - if (stylesData.textStyles) { - for (const textStyle of stylesData.textStyles) { - fontStyleCount++; - const fontKey = `${textStyle.fontFamily}|${textStyle.fontStyle}`; - if (!fontDeps.some(f => `${f.family}|${f.style}` === fontKey)) { - fontDeps.push({ family: textStyle.fontFamily, style: textStyle.fontStyle }); - } - } - } - } - } - // canImport is true if no hard errors (variable count) - // Mode limit exceedance is handled by UI with mode selection - return Object.assign(Object.assign({ currentPlan, existing: { - collections: collections.length, - maxModesInAnyCollection: existingMaxModes, - totalVariables: existingTotalVars - }, importing: { - collections: importCollections.length, - maxModesInAnyCollection: importingMaxModes, - totalVariables: importingTotalVars, - collectionsExceedingModeLimit - }, warnings, - errors, canImport: errors.length === 0 }, (libraryCollections.size > 0 && { - libraryDependencies: { - variableCount: libraryVarCount, - collections: Array.from(libraryCollections) - } - })), (fontDeps.length > 0 && { - fontDependencies: { - styleCount: fontStyleCount, - fonts: fontDeps - } - })); -} -// Helper to count nested variables in a mode object -function countNestedVariables(obj, count = 0) { - for (const [, value] of Object.entries(obj)) { - if (value && typeof value === 'object') { - if ('$type' in value && '$value' in value) { - // This is a variable - count++; - } - else { - // Nested group - count = countNestedVariables(value, count); - } - } - } - return count; -} -const MathUtils = { - round2(value) { - return Math.round(value * 100) / 100; - }, - clamp(value, min, max) { - return Math.max(min, Math.min(max, value)); - }, - toHexByte(value) { - return Math.round(value * 255).toString(16).padStart(2, '0'); - }, - fromHexByte(hex) { - return parseInt(hex, 16) / 255; - } -}; -// ============================================================================ -// SECTION 4: COLOR CONVERSION MODULE (JSF Rule 4.7 - Single Responsibility) -// ============================================================================ -// Shared hue calculation - eliminates duplication between HSL/HSB -function calculateHue(r, g, b, max, min) { - if (max === min) - return 0; - const d = max - min; - let h = 0; - switch (max) { - case r: - h = ((g - b) / d + (g < b ? 6 : 0)) / 6; - break; - case g: - h = ((b - r) / d + 2) / 6; - break; - case b: - h = ((r - g) / d + 4) / 6; - break; - } - return Math.round(h * 360); -} -const ColorConverter = { - // Figma RGB (0-1) → Hex - toHex(color) { - const hex = '#' + - MathUtils.toHexByte(color.r) + - MathUtils.toHexByte(color.g) + - MathUtils.toHexByte(color.b); - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return hex + MathUtils.toHexByte(alpha); - } - return hex; - }, - // Figma RGB (0-1) → RGB (0-255) - toRgb255(color) { - const result = { - r: Math.round(color.r * 255), - g: Math.round(color.g * 255), - b: Math.round(color.b * 255) - }; - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return Object.assign(Object.assign({}, result), { a: MathUtils.round2(alpha) }); - } - return result; - }, - // Figma RGB (0-1) → CSS string - toCss(color) { - const r = Math.round(color.r * 255); - const g = Math.round(color.g * 255); - const b = Math.round(color.b * 255); - const alpha = color.a; - const a = alpha !== undefined ? MathUtils.round2(alpha) : 1; - return a < 1 ? `rgba(${r}, ${g}, ${b}, ${a})` : `rgb(${r}, ${g}, ${b})`; - }, - // Figma RGB (0-1) → HSL - toHsl(color) { - const { r, g, b } = color; - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const l = (max + min) / 2; - let s = 0; - if (max !== min) { - const d = max - min; - s = l > 0.5 ? d / (2 - max - min) : d / (max + min); - } - const result = { - h: calculateHue(r, g, b, max, min), - s: Math.round(s * 100), - l: Math.round(l * 100) - }; - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return Object.assign(Object.assign({}, result), { a: MathUtils.round2(alpha) }); - } - return result; - }, - // Figma RGB (0-1) → HSB/HSV - toHsb(color) { - const { r, g, b } = color; - const max = Math.max(r, g, b); - const min = Math.min(r, g, b); - const s = max === 0 ? 0 : (max - min) / max; - const result = { - h: calculateHue(r, g, b, max, min), - s: Math.round(s * 100), - b: Math.round(max * 100) - }; - const alpha = color.a; - if (alpha !== undefined && alpha < 1) { - return Object.assign(Object.assign({}, result), { a: MathUtils.round2(alpha) }); - } - return result; - }, - // Master export function - all formats - toAllFormats(color) { - return { - hex: this.toHex(color), - rgb: this.toRgb255(color), - css: this.toCss(color), - hsl: this.toHsl(color), - hsb: this.toHsb(color) - }; - } -}; -// ============================================================================ -// SECTION 4B: NAMING CONVENTION CONVERTER -// ============================================================================ -const NamingConverter = { - // Convert name to specified convention - convert(name, convention) { - if (convention === 'original') - return name; - // Split by common separators (space, /, -, _) - const words = name - .replace(/([a-z])([A-Z])/g, '$1 $2') // Split camelCase - .split(/[\s\/\-_]+/) - .filter(w => w.length > 0) - .map(w => w.toLowerCase()); - if (words.length === 0) - return name; - switch (convention) { - case 'camelCase': - return words[0] + words.slice(1).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''); - case 'kebab-case': - return words.join('-'); - case 'snake_case': - return words.join('_'); - default: - return name; - } - }, - // Convert a variable path (e.g., "Colors/Primary/Base" → "colors/primary/base" or "colors.primary.base") - convertPath(path, convention) { - if (convention === 'original') - return path; - return path - .split('/') - .map(part => this.convert(part, convention)) - .join('/'); - }, - // Convert collection name - convertCollectionName(name, convention) { - return this.convert(name, convention); - }, - // Convert mode name - convertModeName(name, convention) { - return this.convert(name, convention); - }, - // Store original names for round-trip - adds $originalName field - addOriginalName(name, convention) { - if (convention === 'original') { - return { converted: name }; - } - const converted = this.convert(name, convention); - if (converted === name) { - return { converted: name }; - } - return { converted, original: name }; - } -}; -// Helper function to resolve alias value recursively -async function resolveAliasValue(variable, preferredModeId, maxDepth = 10) { - if (maxDepth <= 0) { - Logger.log(`⚠️ Max alias resolution depth reached for ${variable.name}`); - return ''; - } - // Try to get value for preferred mode, fallback to first available mode - let value = variable.valuesByMode[preferredModeId]; - if (value === undefined) { - const modeIds = Object.keys(variable.valuesByMode); - if (modeIds.length > 0) { - value = variable.valuesByMode[modeIds[0]]; - } - } - if (value === undefined) { - return ''; - } - // If it's another alias, resolve recursively - if (isVariableAlias(value)) { - const nextVar = await figma.variables.getVariableByIdAsync(value.id); - if (nextVar) { - return resolveAliasValue(nextVar, preferredModeId, maxDepth - 1); - } - return ''; - } - // Return the raw value - return value; -} -// ============================================================================ -// SECTION 4C: W3C DESIGN TOKENS CONVERTER -// ============================================================================ -// W3C Design Tokens type mapping -// https://design-tokens.github.io/community-group/format/ -const W3C_TYPE_MAP = { - 'color': 'color', - 'float': 'number', - 'string': 'string', - 'boolean': 'boolean' -}; -const W3CConverter = { - // Convert Figma color to W3C format (hex with alpha) - colorToW3C(color) { - // W3C uses hex format, including alpha - return color.hex; - }, - // Convert Figma type to W3C type - typeToW3C(figmaType) { - return W3C_TYPE_MAP[figmaType] || 'string'; - }, - // Convert export value to W3C format - valueToW3C(value, isAlias = false) { - const token = { - $value: '', - $type: this.typeToW3C(value.$type) - }; - // Handle alias references - W3C uses {path.to.token} format - if (isAlias && typeof value.$value === 'string' && value.$value.startsWith('{')) { - token.$value = value.$value; - } - else if (value.$type === 'color' && typeof value.$value === 'object') { - // Color value - use hex - token.$value = value.$value.hex; - } - else { - token.$value = value.$value; - } - // Add description if present - if (value.$description) { - token.$description = value.$description; - } - // Add Figma-specific metadata in extensions - if (value.$scopes && value.$scopes.length > 0 && !value.$scopes.includes('ALL_SCOPES')) { - token.$extensions = { - 'com.figma': { - scopes: value.$scopes - } - }; - } - return token; - }, - // Convert collection export to W3C format - collectionToW3C(collectionName, modes, namingConvention, originalName) { - const group = {}; - // Add metadata as $description - if (originalName && originalName !== collectionName) { - group.$description = `Figma collection: ${originalName}`; - } - // For W3C, we typically flatten modes or use first mode - // If multiple modes, create mode groups - const modeNames = Object.keys(modes); - if (modeNames.length === 1) { - // Single mode - flatten directly - this.addTokensToGroup(group, modes[modeNames[0]], namingConvention); - } - else { - // Multiple modes - create mode subgroups - for (const modeName of modeNames) { - const convertedModeName = NamingConverter.convertModeName(modeName, namingConvention); - group[convertedModeName] = {}; - this.addTokensToGroup(group[convertedModeName], modes[modeName], namingConvention); - } - } - return group; - }, - // Recursively add tokens to a group - addTokensToGroup(group, variables, namingConvention) { - for (const [key, value] of Object.entries(variables)) { - const convertedKey = NamingConverter.convert(key, namingConvention); - if (isExportVariableValue(value)) { - // It's a token value - const isAlias = typeof value.$value === 'string' && value.$value.startsWith('{'); - group[convertedKey] = this.valueToW3C(value, isAlias); - } - else { - // It's a nested group - group[convertedKey] = {}; - this.addTokensToGroup(group[convertedKey], value, namingConvention); - } - } - }, - // Parse W3C token to Figma-compatible format - parseW3CToken(token) { - var _a, _b; - const figmaType = this.w3cTypeToFigma(token.$type); - const scopes = ((_b = (_a = token.$extensions) === null || _a === void 0 ? void 0 : _a['com.figma']) === null || _b === void 0 ? void 0 : _b.scopes) || ['ALL_SCOPES']; - // Handle color values - convert hex to full color object - let finalValue; - if (figmaType === 'color' && typeof token.$value === 'string') { - const rgba = ColorParser.parse(token.$value); - finalValue = ColorConverter.toAllFormats(rgba); - } - else if (typeof token.$value === 'string' || typeof token.$value === 'number' || typeof token.$value === 'boolean') { - finalValue = token.$value; - } - else { - // For complex objects, stringify them - finalValue = JSON.stringify(token.$value); - } - // Build result object with all properties at once (readonly-friendly) - const result = token.$description - ? { - $type: figmaType, - $value: finalValue, - $scopes: scopes, - $description: token.$description - } - : { - $type: figmaType, - $value: finalValue, - $scopes: scopes - }; - return result; - }, - // Convert W3C type back to Figma type - w3cTypeToFigma(w3cType) { - const map = { - 'color': 'color', - 'number': 'float', - 'dimension': 'float', - 'string': 'string', - 'boolean': 'boolean', - 'fontFamily': 'string', - 'fontWeight': 'float', - 'duration': 'string', - 'cubicBezier': 'string' - }; - return map[w3cType] || 'string'; - }, - // Detect if JSON is W3C format - isW3CFormat(data) { - if (typeof data !== 'object' || data === null) - return false; - // Check for W3C indicators: - // 1. Root level $type or $value - // 2. Nested objects with $value and $type - const obj = data; - // Check if any top-level key has $value (W3C token) - for (const key of Object.keys(obj)) { - const value = obj[key]; - if (typeof value === 'object' && value !== null) { - if ('$value' in value && '$type' in value) { - return true; - } - // Check one level deeper - for (const subKey of Object.keys(value)) { - const subValue = value[subKey]; - if (typeof subValue === 'object' && subValue !== null && '$value' in subValue) { - return true; - } - } - } - } - // Check if it's our Figma format (array with collection objects) - if (Array.isArray(data)) { - return false; // Figma format is array - } - return false; - }, - // Convert W3C format to Figma format for import - w3cToFigmaFormat(w3cData) { - const result = []; - for (const [collectionName, collectionGroup] of Object.entries(w3cData)) { - // Skip $ prefixed metadata keys - if (collectionName.startsWith('$')) - continue; - const collectionExport = { - [collectionName]: { - modes: { - 'Default': this.w3cGroupToNestedVars(collectionGroup) - } - } - }; - result.push(collectionExport); - } - return result; - }, - // Convert W3C group to nested variables - w3cGroupToNestedVars(group) { - const result = {}; - for (const [key, value] of Object.entries(group)) { - // Skip $ prefixed metadata - if (key.startsWith('$')) - continue; - if (this.isW3CToken(value)) { - // It's a token - result[key] = this.parseW3CToken(value); - } - else if (typeof value === 'object' && value !== null) { - // It's a group - result[key] = this.w3cGroupToNestedVars(value); - } - } - return result; - }, - // Check if object is a W3C token - isW3CToken(obj) { - return typeof obj === 'object' && obj !== null && '$value' in obj; - } -}; -// ============================================================================ -// SECTION 5: COLOR PARSING MODULE (JSF Rule 4.7) -// ============================================================================ -const HEX_REGEX_8 = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; -const HEX_REGEX_6 = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i; -const RGBA_REGEX = /rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/i; -const HSLA_REGEX = /hsla?\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*(?:,\s*([\d.]+))?\s*\)/i; -const ColorParser = { - // Hex → Figma RGBA - fromHex(hex) { - const match8 = HEX_REGEX_8.exec(hex); - if (match8) { - return { - r: MathUtils.fromHexByte(match8[1]), - g: MathUtils.fromHexByte(match8[2]), - b: MathUtils.fromHexByte(match8[3]), - a: MathUtils.fromHexByte(match8[4]) - }; - } - const match6 = HEX_REGEX_6.exec(hex); - if (match6) { - return { - r: MathUtils.fromHexByte(match6[1]), - g: MathUtils.fromHexByte(match6[2]), - b: MathUtils.fromHexByte(match6[3]), - a: 1 - }; - } - return { r: 0, g: 0, b: 0, a: 1 }; - }, - // RGB (0-255) → Figma RGBA - fromRgb255(rgb) { - var _a; - return { - r: rgb.r / 255, - g: rgb.g / 255, - b: rgb.b / 255, - a: (_a = rgb.a) !== null && _a !== void 0 ? _a : 1 - }; - }, - // CSS string → Figma RGBA - fromCss(css) { - const rgbaMatch = RGBA_REGEX.exec(css); - if (rgbaMatch) { - return { - r: parseInt(rgbaMatch[1], 10) / 255, - g: parseInt(rgbaMatch[2], 10) / 255, - b: parseInt(rgbaMatch[3], 10) / 255, - a: rgbaMatch[4] !== undefined ? parseFloat(rgbaMatch[4]) : 1 - }; - } - const hslaMatch = HSLA_REGEX.exec(css); - if (hslaMatch) { - return this.fromHsl({ - h: parseInt(hslaMatch[1], 10), - s: parseInt(hslaMatch[2], 10), - l: parseInt(hslaMatch[3], 10), - a: hslaMatch[4] !== undefined ? parseFloat(hslaMatch[4]) : 1 - }); - } - return { r: 0, g: 0, b: 0, a: 1 }; - }, - // HSL → Figma RGBA - fromHsl(hsl) { - var _a, _b; - const h = hsl.h / 360; - const s = hsl.s / 100; - const l = hsl.l / 100; - if (s === 0) { - return { r: l, g: l, b: l, a: (_a = hsl.a) !== null && _a !== void 0 ? _a : 1 }; - } - const hue2rgb = (p, q, t) => { - const tt = t < 0 ? t + 1 : t > 1 ? t - 1 : t; - if (tt < 1 / 6) - return p + (q - p) * 6 * tt; - if (tt < 1 / 2) - return q; - if (tt < 2 / 3) - return p + (q - p) * (2 / 3 - tt) * 6; - return p; - }; - const q = l < 0.5 ? l * (1 + s) : l + s - l * s; - const p = 2 * l - q; - return { - r: hue2rgb(p, q, h + 1 / 3), - g: hue2rgb(p, q, h), - b: hue2rgb(p, q, h - 1 / 3), - a: (_b = hsl.a) !== null && _b !== void 0 ? _b : 1 - }; - }, - // HSB → Figma RGBA - fromHsb(hsb) { - var _a; - const h = hsb.h / 360; - const s = hsb.s / 100; - const v = hsb.b / 100; - const i = Math.floor(h * 6); - const f = h * 6 - i; - const p = v * (1 - s); - const q = v * (1 - f * s); - const t = v * (1 - (1 - f) * s); - const rgbMap = [ - [v, t, p], [q, v, p], [p, v, t], - [p, q, v], [t, p, v], [v, p, q] - ]; - const [r, g, b] = rgbMap[i % 6]; - return { r, g, b, a: (_a = hsb.a) !== null && _a !== void 0 ? _a : 1 }; - }, - // Universal parser - accepts any format - parse(color) { - var _a; - // ExportColorValue object - if (typeof color === 'object' && color !== null && 'hex' in color && 'rgb' in color) { - return this.fromHex(color.hex); - } - // RGB object - if (typeof color === 'object' && color !== null && 'r' in color && 'g' in color && 'b' in color) { - const rgb = color; - // Check if Figma native (0-1) or standard (0-255) - if (rgb.r <= 1 && rgb.g <= 1 && rgb.b <= 1) { - return { r: rgb.r, g: rgb.g, b: rgb.b, a: (_a = rgb.a) !== null && _a !== void 0 ? _a : 1 }; - } - return this.fromRgb255(rgb); - } - // HSL object - if (typeof color === 'object' && color !== null && 'h' in color && 's' in color && 'l' in color) { - return this.fromHsl(color); - } - // HSB object - if (typeof color === 'object' && color !== null && 'h' in color && 's' in color && 'b' in color) { - return this.fromHsb(color); - } - // String formats - if (typeof color === 'string') { - if (color.startsWith('rgb') || color.startsWith('hsl')) { - return this.fromCss(color); - } - return this.fromHex(color); - } - return { r: 0, g: 0, b: 0, a: 1 }; - } -}; -// ============================================================================ -// SECTION 6: VARIABLE CACHE (JSF Rule 4.18 - Resource Management) -// ============================================================================ -class VariableCache { - constructor() { - this.collectionMap = new Map(); - this.variableMap = new Map(); - this.initialized = false; - } - async initialize() { - if (this.initialized) - return; - await this.rebuild(); - this.initialized = true; - } - async rebuild() { - this.collectionMap.clear(); - this.variableMap.clear(); - for (const col of await figma.variables.getLocalVariableCollectionsAsync()) { - this.collectionMap.set(col.name, col); - for (const varId of col.variableIds) { - const v = await figma.variables.getVariableByIdAsync(varId); - if (v) { - this.variableMap.set(`${col.name}/${v.name}`, v); - } - } - } - } - getCollection(name) { - return this.collectionMap.get(name); - } - getVariable(key) { - return this.variableMap.get(key); - } - setVariable(key, variable) { - this.variableMap.set(key, variable); - } - setCollection(name, collection) { - this.collectionMap.set(name, collection); - } - removeCollection(name) { - // Remove collection from map - this.collectionMap.delete(name); - // Remove all variables belonging to this collection - const keysToRemove = []; - for (const key of this.variableMap.keys()) { - if (key.startsWith(`${name}/`)) { - keysToRemove.push(key); - } - } - for (const key of keysToRemove) { - this.variableMap.delete(key); - } - } - get size() { - return this.variableMap.size; - } - get collections() { - return this.collectionMap.values(); - } - getVariableKeys() { - return Array.from(this.variableMap.keys()); - } -} -const variableCache = new VariableCache(); -// ============================================================================ -// SECTION 7: TYPE GUARDS & MAPPERS (JSF Rule 4.9) -// ============================================================================ -function isExportVariableValue(obj) { - return typeof obj === 'object' && obj !== null && '$type' in obj; -} -function isVariableAlias(value) { - return typeof value === 'object' && value !== null && - value.type === 'VARIABLE_ALIAS'; -} -const TypeMapper = { - toExportType(type) { - var _a; - const map = { - 'COLOR': 'color', - 'FLOAT': 'float', - 'STRING': 'string', - 'BOOLEAN': 'boolean' - }; - return (_a = map[type]) !== null && _a !== void 0 ? _a : 'string'; - }, - toFigmaType(type) { - var _a; - const map = { - 'color': 'COLOR', - 'float': 'FLOAT', - 'string': 'STRING', - 'boolean': 'BOOLEAN' - }; - return (_a = map[type]) !== null && _a !== void 0 ? _a : 'STRING'; - }, - scopesToArray(scopes) { - if (scopes.length === 0 || scopes.includes('ALL_SCOPES')) { - return ['ALL_SCOPES']; - } - return [...scopes]; - }, - arrayToScopes(arr) { - if (arr.includes('ALL_SCOPES')) { - return ['ALL_SCOPES']; - } - return arr; - } -}; -// ============================================================================ -// SECTION 8: BINDING UTILITIES -// ============================================================================ -async function getVariableBindingInfo(boundVariables, key) { - if (!(boundVariables === null || boundVariables === void 0 ? void 0 : boundVariables[key])) - return {}; - const alias = boundVariables[key]; - if (!alias) - return {}; - const variable = await figma.variables.getVariableByIdAsync(alias.id); - if (!variable) - return { id: alias.id }; - const collection = await figma.variables.getVariableCollectionByIdAsync(variable.variableCollectionId); - return { - id: alias.id, - name: variable.name, - collection: collection === null || collection === void 0 ? void 0 : collection.name - }; -} -async function extractBindings(boundVariables, keys) { - if (!boundVariables) - return undefined; - const bindings = {}; - for (const key of keys) { - const binding = await getVariableBindingInfo(boundVariables, key); - if (binding.name) { - bindings[key] = binding; - } - } - return Object.keys(bindings).length > 0 ? bindings : undefined; -} -function flattenVariables(obj, prefix) { - const results = []; - for (const key of Object.keys(obj)) { - const val = obj[key]; - const path = prefix ? `${prefix}/${key}` : key; - if (isExportVariableValue(val)) { - results.push({ path, value: val }); - } - else { - results.push(...flattenVariables(val, path)); - } - } - return results; -} -function getValueAtPath(obj, path) { - const parts = path.split('/'); - let current = obj; - for (const part of parts) { - if (typeof current !== 'object' || current === null) - return null; - if (isExportVariableValue(current)) - return null; - current = current[part]; - } - return isExportVariableValue(current) ? current : null; -} -// Color Style Processor - supports SOLID, GRADIENT, and IMAGE paint styles -const ColorStyleProcessor = { - async export(options) { - var _a, _b, _c, _d; - const includeImages = (_a = options === null || options === void 0 ? void 0 : options.includeImages) !== null && _a !== void 0 ? _a : false; - const styles = []; - for (const style of await figma.getLocalPaintStylesAsync()) { - if (style.paints.length === 0) - continue; - const exportPaints = []; - let primaryColor; - let primaryOpacity; - let boundVars; - for (const paint of style.paints) { - if (paint.type === 'SOLID') { - const colorAsRgba = paint.color; - let effectiveOpacity = (_b = paint.opacity) !== null && _b !== void 0 ? _b : 1; - if (colorAsRgba.a !== undefined && colorAsRgba.a < 1 && effectiveOpacity === 1) { - effectiveOpacity = colorAsRgba.a; - } - const colorWithAlpha = { - r: paint.color.r, - g: paint.color.g, - b: paint.color.b, - a: effectiveOpacity - }; - const solidPaint = { - type: 'SOLID', - color: ColorConverter.toAllFormats(colorWithAlpha), - opacity: MathUtils.round2(effectiveOpacity) - }; - exportPaints.push(solidPaint); - // Store first solid color for backward compatibility - if (!primaryColor) { - primaryColor = solidPaint.color; - primaryOpacity = solidPaint.opacity; - boundVars = await extractBindings(paint.boundVariables, ['color']); - } - } - else if (paint.type === 'GRADIENT_LINEAR' || paint.type === 'GRADIENT_RADIAL' || - paint.type === 'GRADIENT_ANGULAR' || paint.type === 'GRADIENT_DIAMOND') { - const gradientStops = paint.gradientStops.map(stop => { - var _a; - return ({ - position: MathUtils.round2(stop.position), - color: ColorConverter.toAllFormats({ - r: stop.color.r, - g: stop.color.g, - b: stop.color.b, - a: (_a = stop.color.a) !== null && _a !== void 0 ? _a : 1 - }) - }); - }); - const gradientPaint = Object.assign(Object.assign({ type: paint.type, gradientStops }, (paint.gradientTransform && { - gradientTransform: paint.gradientTransform - })), { opacity: MathUtils.round2((_c = paint.opacity) !== null && _c !== void 0 ? _c : 1) }); - exportPaints.push(gradientPaint); - } - else if (paint.type === 'IMAGE') { - const imagePaint = Object.assign(Object.assign(Object.assign(Object.assign({ type: 'IMAGE', scaleMode: paint.scaleMode }, (paint.imageHash && { imageHash: paint.imageHash })), { opacity: MathUtils.round2((_d = paint.opacity) !== null && _d !== void 0 ? _d : 1) }), (paint.rotation !== undefined && { rotation: paint.rotation })), (paint.filters && { - filters: Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({}, (paint.filters.exposure !== undefined && { exposure: paint.filters.exposure })), (paint.filters.contrast !== undefined && { contrast: paint.filters.contrast })), (paint.filters.saturation !== undefined && { saturation: paint.filters.saturation })), (paint.filters.temperature !== undefined && { temperature: paint.filters.temperature })), (paint.filters.tint !== undefined && { tint: paint.filters.tint })), (paint.filters.highlights !== undefined && { highlights: paint.filters.highlights })), (paint.filters.shadows !== undefined && { shadows: paint.filters.shadows })) - })); - // Try to get image bytes if includeImages is enabled - if (includeImages && paint.imageHash) { - try { - const image = figma.getImageByHash(paint.imageHash); - if (image) { - const imageBytes = await image.getBytesAsync(); - if (imageBytes) { - // Convert to base64 - const base64 = figma.base64Encode(imageBytes); - imagePaint.imageBase64 = base64; - } - } - } - catch (e) { - Logger.log(`⚠️ Could not export image data for style "${style.name}": ${e}`); - } - } - exportPaints.push(imagePaint); - } - } - if (exportPaints.length === 0) - continue; - const colorStyle = Object.assign(Object.assign(Object.assign(Object.assign({ name: style.name, paints: exportPaints }, (primaryColor && { color: primaryColor })), (primaryOpacity !== undefined && { opacity: primaryOpacity })), (style.description && { description: style.description })), (boundVars && Object.keys(boundVars).length > 0 && { boundVariables: boundVars })); - styles.push(colorStyle); - } - return styles; - }, - async importStyles(styles, cache) { - var _a, _b, _c, _d; - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of await figma.getLocalPaintStylesAsync()) { - existing.set(s.name, s); - } - for (const colorStyle of styles) { - let style; - if (existing.has(colorStyle.name)) { - style = existing.get(colorStyle.name); - updated++; - } - else { - style = figma.createPaintStyle(); - style.name = colorStyle.name; - created++; - } - if (colorStyle.description) { - style.description = colorStyle.description; - } - const paints = []; - // Use new paints array if available, otherwise fall back to legacy color field - if (colorStyle.paints && colorStyle.paints.length > 0) { - for (const exportPaint of colorStyle.paints) { - if (exportPaint.type === 'SOLID') { - const colorRgba = ColorParser.parse(exportPaint.color); - let finalOpacity = (_a = exportPaint.opacity) !== null && _a !== void 0 ? _a : 1; - if (colorRgba.a < 1 && exportPaint.opacity === undefined) { - finalOpacity = MathUtils.round2(colorRgba.a); - } - let paint = { - type: 'SOLID', - color: { r: colorRgba.r, g: colorRgba.g, b: colorRgba.b }, - opacity: MathUtils.round2(finalOpacity) - }; - // Apply variable bindings for first solid paint - if (colorStyle.boundVariables && paints.length === 0) { - for (const [key, binding] of Object.entries(colorStyle.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - paint = figma.variables.setBoundVariableForPaint(paint, key, targetVar); - } - catch (e) { - Logger.log(`⚠️ Could not bind ${key}: ${e}`); - } - } - } - } - } - paints.push(paint); - } - else if (exportPaint.type === 'GRADIENT_LINEAR' || exportPaint.type === 'GRADIENT_RADIAL' || - exportPaint.type === 'GRADIENT_ANGULAR' || exportPaint.type === 'GRADIENT_DIAMOND') { - const gradientStops = exportPaint.gradientStops.map(stop => { - const stopColor = ColorParser.parse(stop.color); - return { - position: stop.position, - color: { r: stopColor.r, g: stopColor.g, b: stopColor.b, a: stopColor.a } - }; - }); - // Convert readonly transform to mutable Transform type - const transform = exportPaint.gradientTransform - ? [[exportPaint.gradientTransform[0][0], exportPaint.gradientTransform[0][1], exportPaint.gradientTransform[0][2]], - [exportPaint.gradientTransform[1][0], exportPaint.gradientTransform[1][1], exportPaint.gradientTransform[1][2]]] - : [[1, 0, 0], [0, 1, 0]]; - const gradientPaint = { - type: exportPaint.type, - gradientStops, - gradientTransform: transform, - opacity: (_b = exportPaint.opacity) !== null && _b !== void 0 ? _b : 1 - }; - paints.push(gradientPaint); - } - else if (exportPaint.type === 'IMAGE') { - // Create image paint - let imageHash = null; - // First, try to create image from base64 data if available - // This takes priority because imageHash from another file won't work - if (exportPaint.imageBase64) { - try { - const bytes = figma.base64Decode(exportPaint.imageBase64); - const image = figma.createImage(bytes); - imageHash = image.hash; - Logger.log(`✅ Created image from base64 data for style "${colorStyle.name}"`); - } - catch (e) { - Logger.log(`⚠️ Could not import image from base64 for style "${colorStyle.name}": ${e}`); - } - } - // If no base64 or base64 failed, try using the existing hash (might work if image exists in file) - if (!imageHash && exportPaint.imageHash) { - // Check if the image with this hash exists in the file - const existingImage = figma.getImageByHash(exportPaint.imageHash); - if (existingImage) { - imageHash = exportPaint.imageHash; - Logger.log(`✅ Found existing image with hash for style "${colorStyle.name}"`); - } - else { - Logger.log(`⚠️ Image hash not found in file for style "${colorStyle.name}", skipping image paint (imageHash cannot be null)`); - } - } - // Only add image paint if we have a valid imageHash - Figma API rejects null imageHash - if (imageHash) { - const imagePaint = Object.assign(Object.assign({ type: 'IMAGE', scaleMode: exportPaint.scaleMode, imageHash: imageHash, opacity: (_c = exportPaint.opacity) !== null && _c !== void 0 ? _c : 1 }, (exportPaint.rotation !== undefined && { rotation: exportPaint.rotation })), (exportPaint.filters && { filters: exportPaint.filters })); - paints.push(imagePaint); - } - } - } - } - else if (colorStyle.color) { - // Legacy format: single color field - const colorRgba = ColorParser.parse(colorStyle.color); - let finalOpacity = (_d = colorStyle.opacity) !== null && _d !== void 0 ? _d : 1; - if (colorRgba.a < 1 && colorStyle.opacity === undefined) { - finalOpacity = MathUtils.round2(colorRgba.a); - } - let paint = { - type: 'SOLID', - color: { r: colorRgba.r, g: colorRgba.g, b: colorRgba.b }, - opacity: MathUtils.round2(finalOpacity) - }; - if (colorStyle.boundVariables) { - for (const [key, binding] of Object.entries(colorStyle.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - paint = figma.variables.setBoundVariableForPaint(paint, key, targetVar); - } - catch (e) { - Logger.log(`⚠️ Could not bind ${key}: ${e}`); - } - } - } - } - } - paints.push(paint); - } - if (paints.length > 0) { - style.paints = paints; - } - } - return { created, updated }; - } -}; -// Text Style Processor -const TextStyleProcessor = { - async export(_options) { - const styles = []; - for (const style of await figma.getLocalTextStylesAsync()) { - const textStyle = Object.assign(Object.assign({ name: style.name, fontFamily: style.fontName.family, fontStyle: style.fontName.style, fontSize: style.fontSize, lineHeight: style.lineHeight, letterSpacing: style.letterSpacing, textCase: style.textCase, textDecoration: style.textDecoration }, (style.description && { description: style.description })), { boundVariables: await extractBindings(style.boundVariables, ['fontSize', 'lineHeight', 'letterSpacing', 'paragraphSpacing', 'paragraphIndent']) }); - styles.push(textStyle); - } - return styles; - }, - async importStyles(styles, cache) { - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of await figma.getLocalTextStylesAsync()) { - existing.set(s.name, s); - } - for (const textStyle of styles) { - let style; - if (existing.has(textStyle.name)) { - style = existing.get(textStyle.name); - updated++; - } - else { - style = figma.createTextStyle(); - style.name = textStyle.name; - created++; - } - if (textStyle.description) { - style.description = textStyle.description; - } - try { - await figma.loadFontAsync({ family: textStyle.fontFamily, style: textStyle.fontStyle }); - style.fontName = { family: textStyle.fontFamily, style: textStyle.fontStyle }; - style.fontSize = textStyle.fontSize; - style.lineHeight = textStyle.lineHeight; - style.letterSpacing = textStyle.letterSpacing; - if (textStyle.textCase) - style.textCase = textStyle.textCase; - if (textStyle.textDecoration) - style.textDecoration = textStyle.textDecoration; - if (textStyle.boundVariables) { - for (const [key, binding] of Object.entries(textStyle.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - style.setBoundVariable(key, targetVar); - } - catch ( /* Skip */_a) { /* Skip */ } - } - } - } - } - } - catch (e) { - Logger.log(`⚠️ Could not load font for ${textStyle.name}: ${e}`); - } - } - return { created, updated }; - } -}; -// Effect Style Processor -const EffectStyleProcessor = { - async export(_options) { - const styles = []; - for (const style of await figma.getLocalEffectStylesAsync()) { - const effects = []; - for (const effect of style.effects) { - const effectData = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ type: effect.type, visible: effect.visible }, ('radius' in effect && { radius: effect.radius })), ('spread' in effect && { spread: effect.spread })), ('offset' in effect && { offset: effect.offset })), ('color' in effect && { color: ColorConverter.toAllFormats(effect.color) })), ('blendMode' in effect && { blendMode: effect.blendMode })), ('showShadowBehindNode' in effect && { showShadowBehindNode: effect.showShadowBehindNode })), { boundVariables: await extractBindings(effect.boundVariables, ['color', 'radius', 'spread', 'offsetX', 'offsetY']) }); - effects.push(effectData); - } - const effectStyle = Object.assign(Object.assign({ name: style.name }, (style.description && { description: style.description })), { effects }); - styles.push(effectStyle); - } - return styles; - }, - async importStyles(styles, cache) { - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of await figma.getLocalEffectStylesAsync()) { - existing.set(s.name, s); - } - for (const effectStyle of styles) { - let style; - if (existing.has(effectStyle.name)) { - style = existing.get(effectStyle.name); - updated++; - } - else { - style = figma.createEffectStyle(); - style.name = effectStyle.name; - created++; - } - if (effectStyle.description) { - style.description = effectStyle.description; - } - const newEffects = effectStyle.effects.map(effect => { - var _a; - const e = Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({ type: effect.type, visible: (_a = effect.visible) !== null && _a !== void 0 ? _a : true }, ((effect.radius !== undefined) && { radius: effect.radius })), ((effect.spread !== undefined) && { spread: effect.spread })), ((effect.offset !== undefined) && { offset: effect.offset })), ((effect.color !== undefined) && { - color: (() => { - const c = ColorParser.parse(effect.color); - return { r: c.r, g: c.g, b: c.b, a: MathUtils.round2(c.a) }; - })() - })), ((effect.blendMode !== undefined) && { blendMode: effect.blendMode })), ((effect.showShadowBehindNode !== undefined) && { showShadowBehindNode: effect.showShadowBehindNode })); - return e; - }); - style.effects = newEffects; - // Bind variables - for (let i = 0; i < effectStyle.effects.length; i++) { - const effectData = effectStyle.effects[i]; - if (effectData.boundVariables) { - for (const [key, binding] of Object.entries(effectData.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - const effects = [...style.effects]; - effects[i] = figma.variables.setBoundVariableForEffect(effects[i], key, targetVar); - style.effects = effects; - } - catch ( /* Skip */_a) { /* Skip */ } - } - } - } - } - } - } - return { created, updated }; - } -}; -// Grid Style Processor -const GridStyleProcessor = { - async export(_options) { - const styles = []; - for (const style of await figma.getLocalGridStylesAsync()) { - const layoutGrids = []; - for (const grid of style.layoutGrids) { - const gridColor = grid.color; - const gridData = Object.assign(Object.assign(Object.assign({ pattern: grid.pattern, visible: grid.visible, color: ColorConverter.toAllFormats(gridColor) }, (grid.pattern === 'GRID' && { sectionSize: grid.sectionSize })), (grid.pattern !== 'GRID' && Object.assign({ alignment: grid.alignment, gutterSize: grid.gutterSize, count: grid.count, offset: grid.offset }, (grid.sectionSize !== undefined && { sectionSize: grid.sectionSize })))), { boundVariables: await extractBindings(grid.boundVariables, ['gutterSize', 'count', 'offset', 'sectionSize']) }); - layoutGrids.push(gridData); - } - const gridStyle = Object.assign(Object.assign({ name: style.name }, (style.description && { description: style.description })), { layoutGrids }); - styles.push(gridStyle); - } - return styles; - }, - async importStyles(styles, cache) { - let created = 0; - let updated = 0; - const existing = new Map(); - for (const s of await figma.getLocalGridStylesAsync()) { - existing.set(s.name, s); - } - for (const gridStyle of styles) { - let style; - if (existing.has(gridStyle.name)) { - style = existing.get(gridStyle.name); - updated++; - } - else { - style = figma.createGridStyle(); - style.name = gridStyle.name; - created++; - } - if (gridStyle.description) { - style.description = gridStyle.description; - } - const newLayoutGrids = gridStyle.layoutGrids.map((grid) => { - var _a, _b, _c, _d, _e, _f, _g; - const gridColor = grid.color - ? ColorParser.parse(grid.color) - : { r: 1, g: 0, b: 0, a: 0.1 }; - const color = { - r: gridColor.r, - g: gridColor.g, - b: gridColor.b, - a: MathUtils.round2(gridColor.a) - }; - if (grid.pattern === 'GRID') { - return { - pattern: 'GRID', - sectionSize: (_a = grid.sectionSize) !== null && _a !== void 0 ? _a : 10, - visible: grid.visible !== false, - color - }; - } - const alignment = (_b = grid.alignment) !== null && _b !== void 0 ? _b : 'STRETCH'; - const base = { - pattern: grid.pattern, - gutterSize: (_c = grid.gutterSize) !== null && _c !== void 0 ? _c : 10, - count: (_d = grid.count) !== null && _d !== void 0 ? _d : 5, - visible: grid.visible !== false, - color - }; - if (alignment === 'STRETCH') { - return Object.assign(Object.assign({}, base), { alignment: 'STRETCH', offset: (_e = grid.offset) !== null && _e !== void 0 ? _e : 0 }); - } - else if (alignment === 'CENTER') { - return Object.assign(Object.assign({}, base), { alignment: 'CENTER', sectionSize: (_f = grid.sectionSize) !== null && _f !== void 0 ? _f : 100 }); - } - else { - const result = Object.assign(Object.assign({}, base), { alignment: alignment, offset: (_g = grid.offset) !== null && _g !== void 0 ? _g : 0 }); - if (grid.sectionSize !== undefined) { - result.sectionSize = grid.sectionSize; - } - return result; - } - }); - style.layoutGrids = newLayoutGrids; - // Bind variables - for (let i = 0; i < gridStyle.layoutGrids.length; i++) { - const gridData = gridStyle.layoutGrids[i]; - if (gridData.boundVariables) { - for (const [key, binding] of Object.entries(gridData.boundVariables)) { - if (binding.name && binding.collection) { - const targetVar = cache.getVariable(`${binding.collection}/${binding.name}`); - if (targetVar) { - try { - const grids = [...style.layoutGrids]; - grids[i] = figma.variables.setBoundVariableForLayoutGrid(grids[i], key, targetVar); - style.layoutGrids = grids; - } - catch ( /* Skip */_a) { /* Skip */ } - } - } - } - } - } - } - return { created, updated }; - } -}; -async function computeImportDiff(importData) { - await variableCache.initialize(); - const result = { - newCollections: [], - modifiedCollections: [], - unchangedCollections: [], - newVariables: [], - modifiedVariables: [], - unchangedVariables: 0, - newStyles: [], - modifiedStyles: [], - summary: { - collectionsNew: 0, - collectionsModified: 0, - collectionsUnchanged: 0, - variablesNew: 0, - variablesModified: 0, - variablesUnchanged: 0, - stylesNew: 0, - stylesModified: 0 - } - }; - // Process collections from import data - for (const item of importData) { - const keys = Object.keys(item); - if (keys.length === 1 && keys[0] === '_styles') { - // Handle styles diff - const stylesData = item._styles; - await computeStylesDiff(stylesData, result); - continue; - } - // Handle collection - const collectionObj = item; - const jsonCollectionName = Object.keys(collectionObj)[0]; - const collectionContent = collectionObj[jsonCollectionName]; - const collectionName = collectionContent.$originalName || jsonCollectionName; - const existingCollection = variableCache.getCollection(collectionName); - if (!existingCollection) { - // New collection - result.newCollections.push(collectionName); - result.summary.collectionsNew++; - // Count all variables as new - const varCount = countVariablesInCollection(collectionContent.modes); - result.summary.variablesNew += varCount; - continue; - } - // Existing collection - check for modifications - let hasModifications = false; - for (const [modeName, modeData] of Object.entries(collectionContent.modes)) { - const mode = existingCollection.modes.find(m => m.name === modeName); - if (!mode) { - hasModifications = true; - continue; - } - // Check each variable - await checkVariablesDiff(existingCollection, mode.modeId, modeData, collectionName, '', result); - } - if (result.modifiedVariables.some(v => v.collection === collectionName) || - result.newVariables.some(v => v.collection === collectionName)) { - result.modifiedCollections.push(collectionName); - result.summary.collectionsModified++; - } - else { - result.unchangedCollections.push(collectionName); - result.summary.collectionsUnchanged++; - } - } - return result; -} -function countVariablesInCollection(modes) { - let count = 0; - const firstMode = Object.values(modes)[0]; - if (firstMode) { - count = countVarsInNestedObj(firstMode); - } - return count; -} -function countVarsInNestedObj(obj) { - let count = 0; - for (const value of Object.values(obj)) { - if (isExportVariableValue(value)) { - count++; - } - else { - count += countVarsInNestedObj(value); - } - } - return count; -} -async function checkVariablesDiff(collection, modeId, importData, collectionName, path, result) { - for (const [key, value] of Object.entries(importData)) { - const currentPath = path ? `${path}/${key}` : key; - if (isExportVariableValue(value)) { - // This is a variable value - const existingVar = variableCache.getVariable(`${collectionName}/${currentPath}`); - if (!existingVar) { - result.newVariables.push({ collection: collectionName, path: currentPath }); - result.summary.variablesNew++; - } - else { - // Check if value changed - const existingValue = existingVar.valuesByMode[modeId]; - const importValue = value.$value; - if (valuesAreDifferent(existingValue, importValue)) { - result.modifiedVariables.push({ - collection: collectionName, - path: currentPath, - oldValue: formatValueForDisplay(existingValue), - newValue: formatValueForDisplay(importValue) - }); - result.summary.variablesModified++; - } - else { - result.unchangedVariables++; - result.summary.variablesUnchanged++; - } - } - } - else { - // Nested object, recurse - await checkVariablesDiff(collection, modeId, value, collectionName, currentPath, result); - } - } -} -function valuesAreDifferent(existing, imported) { - if (existing === undefined) - return true; - // Handle alias references - if (isVariableAlias(existing)) { - // Both are alias refs, compare the string value - if (typeof imported === 'string' && imported.startsWith('{')) { - return true; // Can't easily compare alias refs, assume different - } - return true; - } - // Handle colors - if (typeof existing === 'object' && existing !== null && 'r' in existing) { - if (typeof imported === 'object' && imported !== null && 'hex' in imported) { - const existingHex = ColorConverter.toAllFormats(existing).hex; - return existingHex.toLowerCase() !== imported.hex.toLowerCase(); - } - return true; - } - // Handle primitives - return existing !== imported; -} -function formatValueForDisplay(value) { - if (value === undefined) - return 'undefined'; - if (typeof value === 'object' && value !== null) { - if ('hex' in value) - return value.hex; - if ('r' in value) - return ColorConverter.toAllFormats(value).hex; - if ('id' in value) - return '{alias}'; - } - return String(value); -} -async function computeStylesDiff(stylesData, result) { - // Check color styles - if (stylesData.colorStyles) { - const existingColorStyles = await figma.getLocalPaintStylesAsync(); - const existingNames = new Set(existingColorStyles.map(s => s.name)); - for (const style of stylesData.colorStyles) { - if (existingNames.has(style.name)) { - result.modifiedStyles.push({ type: 'color', name: style.name }); - result.summary.stylesModified++; - } - else { - result.newStyles.push({ type: 'color', name: style.name }); - result.summary.stylesNew++; - } - } - } - // Check text styles - if (stylesData.textStyles) { - const existingTextStyles = await figma.getLocalTextStylesAsync(); - const existingNames = new Set(existingTextStyles.map(s => s.name)); - for (const style of stylesData.textStyles) { - if (existingNames.has(style.name)) { - result.modifiedStyles.push({ type: 'text', name: style.name }); - result.summary.stylesModified++; - } - else { - result.newStyles.push({ type: 'text', name: style.name }); - result.summary.stylesNew++; - } - } - } - // Check effect styles - if (stylesData.effectStyles) { - const existingEffectStyles = await figma.getLocalEffectStylesAsync(); - const existingNames = new Set(existingEffectStyles.map(s => s.name)); - for (const style of stylesData.effectStyles) { - if (existingNames.has(style.name)) { - result.modifiedStyles.push({ type: 'effect', name: style.name }); - result.summary.stylesModified++; - } - else { - result.newStyles.push({ type: 'effect', name: style.name }); - result.summary.stylesNew++; - } - } - } - // Check grid styles - if (stylesData.gridStyles) { - const existingGridStyles = await figma.getLocalGridStylesAsync(); - const existingNames = new Set(existingGridStyles.map(s => s.name)); - for (const style of stylesData.gridStyles) { - if (existingNames.has(style.name)) { - result.modifiedStyles.push({ type: 'grid', name: style.name }); - result.summary.stylesModified++; - } - else { - result.newStyles.push({ type: 'grid', name: style.name }); - result.summary.stylesNew++; - } - } - } -} -async function exportVariables(selectedCollections, styleOptions, preserveLibraryRefs, includeImages, namingConvention = 'original', exportFormat = 'figma', selectedModes, resolveAliases = false) { - var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k; - Logger.log('📤 Starting export...'); - Logger.log(` preserveLibraryRefs: ${preserveLibraryRefs}`); - Logger.log(` includeImages: ${includeImages}`); - Logger.log(` namingConvention: ${namingConvention}`); - Logger.log(` exportFormat: ${exportFormat}`); - Logger.log(` resolveAliases: ${resolveAliases}`); - if (selectedModes) { - Logger.log(` selectedModes: ${JSON.stringify(selectedModes)}`); - } - try { - let collections = await figma.variables.getLocalVariableCollectionsAsync(); - if (selectedCollections === null || selectedCollections === void 0 ? void 0 : selectedCollections.length) { - collections = collections.filter(c => selectedCollections.includes(c.name)); - Logger.log(`Filtering to ${collections.length} selected collections`); - } - const exportData = []; - const w3cExportData = {}; - let totalVariables = 0; - for (const collection of collections) { - Logger.log(`Processing collection: ${collection.name}`); - // Filter modes if selectedModes specified for this collection - let modesToExport = collection.modes; - if (selectedModes && selectedModes[collection.name]) { - const allowedModes = selectedModes[collection.name]; - modesToExport = collection.modes.filter(m => allowedModes.includes(m.name)); - Logger.log(` Filtering to ${modesToExport.length} modes: ${modesToExport.map(m => m.name).join(', ')}`); - } - // Convert collection name based on naming convention - const exportCollectionName = NamingConverter.convertCollectionName(collection.name, namingConvention); - const collectionExport = { - [exportCollectionName]: Object.assign({ modes: {} }, (exportCollectionName !== collection.name && { $originalName: collection.name })) - }; - // Initialize modes with converted names (only selected modes) - for (const mode of modesToExport) { - const exportModeName = NamingConverter.convertModeName(mode.name, namingConvention); - collectionExport[exportCollectionName].modes[exportModeName] = {}; - // We'll handle original mode names in metadata if needed - } - // Process variables - for (const variableId of collection.variableIds) { - const variable = await figma.variables.getVariableByIdAsync(variableId); - if (!variable) - continue; - totalVariables++; - // Convert variable path parts based on naming convention - const originalParts = variable.name.split('/'); - const nameParts = originalParts.map(part => NamingConverter.convert(part, namingConvention)); - // Only process selected modes - for (const mode of modesToExport) { - const exportModeName = NamingConverter.convertModeName(mode.name, namingConvention); - const modeValues = collectionExport[exportCollectionName].modes[exportModeName]; - const value = variable.valuesByMode[mode.modeId]; - // Navigate/create nested structure - let current = modeValues; - for (let i = 0; i < nameParts.length - 1; i++) { - const part = nameParts[i]; - if (!current[part] || isExportVariableValue(current[part])) { - current[part] = {}; - } - current = current[part]; - } - const leafName = nameParts[nameParts.length - 1]; - // Convert value - let exportValue; - let isAlias = false; - let aliasCollection = ''; - let isLibraryAlias = false; - let aliasRef = ''; - let localValue = undefined; - if (isVariableAlias(value)) { - const aliasVar = await figma.variables.getVariableByIdAsync(value.id); - if (aliasVar) { - const aliasCol = await figma.variables.getVariableCollectionByIdAsync(aliasVar.variableCollectionId); - isAlias = true; - aliasCollection = (_a = aliasCol === null || aliasCol === void 0 ? void 0 : aliasCol.name) !== null && _a !== void 0 ? _a : ''; - isLibraryAlias = (_b = aliasCol === null || aliasCol === void 0 ? void 0 : aliasCol.remote) !== null && _b !== void 0 ? _b : false; - // If resolveAliases is true, resolve to the actual value - if (resolveAliases) { - // Resolve the alias to its actual value - const resolvedValue = await resolveAliasValue(aliasVar, mode.modeId); - if (typeof resolvedValue === 'object' && resolvedValue !== null && 'r' in resolvedValue) { - exportValue = ColorConverter.toAllFormats(resolvedValue); - } - else { - exportValue = resolvedValue; - } - // Don't mark as alias since we resolved it - isAlias = false; - } - else { - // Keep as alias reference - // Convert alias reference to match naming convention - const aliasPath = aliasVar.name.split('/').map(p => NamingConverter.convert(p, namingConvention)).join('.'); - aliasRef = `{${aliasPath}}`; - exportValue = aliasRef; - // Get the resolved local value for library aliases - if (isLibraryAlias) { - // Get the resolved value from the alias - const resolvedValue = aliasVar.valuesByMode[Object.keys(aliasVar.valuesByMode)[0]]; - if (typeof resolvedValue === 'object' && resolvedValue !== null && 'r' in resolvedValue) { - localValue = ColorConverter.toAllFormats(resolvedValue); - } - else if (!isVariableAlias(resolvedValue)) { - localValue = resolvedValue; - } - } - } - } - else { - exportValue = ''; - } - } - else if (typeof value === 'object' && value !== null && 'r' in value) { - exportValue = ColorConverter.toAllFormats(value); - } - else { - exportValue = value; - } - const varExport = Object.assign(Object.assign(Object.assign({ $scopes: TypeMapper.scopesToArray(variable.scopes), $type: TypeMapper.toExportType(variable.resolvedType), $value: exportValue }, (variable.description && { $description: variable.description })), (isAlias && aliasCollection && { $collectionName: aliasCollection })), (isAlias && isLibraryAlias && Object.assign({ $libraryRef: aliasRef }, (localValue !== undefined && { $localValue: localValue })))); - current[leafName] = varExport; - } - } - exportData.push(collectionExport); - // Also build W3C format if needed - if (exportFormat === 'w3c') { - w3cExportData[exportCollectionName] = W3CConverter.collectionToW3C(exportCollectionName, collectionExport[exportCollectionName].modes, namingConvention, collectionExport[exportCollectionName].$originalName); - } - } - // Export styles - let stylesExported = null; - if (styleOptions) { - stylesExported = {}; - if (styleOptions.colorStyles) - stylesExported.colorStyles = await ColorStyleProcessor.export({ includeImages }); - if (styleOptions.textStyles) - stylesExported.textStyles = await TextStyleProcessor.export(); - if (styleOptions.effectStyles) - stylesExported.effectStyles = await EffectStyleProcessor.export(); - if (styleOptions.gridStyles) - stylesExported.gridStyles = await GridStyleProcessor.export(); - if (Object.keys(stylesExported).length > 0) { - exportData.push({ _styles: stylesExported }); - } - else { - stylesExported = null; - } - } - const stats = { - collections: collections.length, - variables: totalVariables, - styles: stylesExported ? { - color: (_d = (_c = stylesExported.colorStyles) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0, - text: (_f = (_e = stylesExported.textStyles) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0, - effect: (_h = (_g = stylesExported.effectStyles) === null || _g === void 0 ? void 0 : _g.length) !== null && _h !== void 0 ? _h : 0, - grid: (_k = (_j = stylesExported.gridStyles) === null || _j === void 0 ? void 0 : _j.length) !== null && _k !== void 0 ? _k : 0 - } : null - }; - // Choose output format - let outputData; - if (exportFormat === 'w3c') { - // W3C Design Tokens format - // Note: Styles are not part of W3C spec, so we add them in extensions - if (stylesExported && Object.keys(stylesExported).length > 0) { - w3cExportData['$extensions'] = { - 'com.figma': { - styles: stylesExported - } - }; - } - outputData = JSON.stringify(w3cExportData, null, 2); - Logger.log(`✅ Export complete (W3C format): ${stats.collections} collections, ${stats.variables} variables`); - } - else { - // Figma JSON format - outputData = JSON.stringify(exportData, null, 2); - Logger.log(`✅ Export complete: ${stats.collections} collections, ${stats.variables} variables`); - } - Logger.send('export_complete', { - data: outputData, - stats, - format: exportFormat - }); - } - catch (e) { - Logger.log(`❌ Export error: ${e}`); - Logger.send('error', { message: `Export failed: ${e}` }); - } -} -// ============================================================================ -// SECTION 12: IMPORT ORCHESTRATOR -// ============================================================================ -async function importVariables(jsonData, options) { - var _a, _b; - Logger.log('📥 Starting import...'); - Logger.log(`📋 Import options: merge=${options.merge}, clearFirst=${options.clearFirst}, importStyles=${options.importStyles}`); - // Create a snapshot BEFORE making any changes for automatic rollback on error - Logger.log('📸 Creating pre-import snapshot for automatic rollback...'); - let preImportSnapshot = null; - try { - preImportSnapshot = await createUndoSnapshot(); - Logger.log('✅ Pre-import snapshot created'); - } - catch (snapshotError) { - Logger.log(`⚠️ Could not create pre-import snapshot: ${snapshotError}`); - // Continue without snapshot - user will be warned if import fails - } - try { - let parsedData = JSON.parse(jsonData); - // Detect format and convert if W3C - let importData; - let detectedFormat = 'figma'; - if (!Array.isArray(parsedData) && W3CConverter.isW3CFormat(parsedData)) { - Logger.log('📄 Detected W3C Design Tokens format, converting...'); - detectedFormat = 'w3c'; - // Extract styles from extensions if present - const w3cData = parsedData; - let stylesFromW3C = null; - if (w3cData['$extensions'] && w3cData['$extensions']['com.figma']) { - const figmaExtensions = w3cData['$extensions']['com.figma']; - if (figmaExtensions.styles) { - stylesFromW3C = figmaExtensions.styles; - } - // Remove extensions from token data - delete w3cData['$extensions']; - } - // Convert W3C to Figma format - const converted = W3CConverter.w3cToFigmaFormat(w3cData); - importData = converted; - // Add styles if present - if (stylesFromW3C) { - importData.push({ _styles: stylesFromW3C }); - } - } - else { - importData = parsedData; - } - // Handle Clean Import: clear everything first - if (options.clearFirst) { - Logger.log('🧹 Clean Import: Clearing existing variables and styles...'); - await clearAll(); - Logger.log('✅ Clean Import: Clearing complete, rebuilding cache...'); - // Reinitialize cache after clearing - await variableCache.rebuild(); - Logger.log('✅ Clean Import: Cache rebuilt, proceeding with import...'); - } - // Handle Custom Merge: selectively clear variables and/or styles - if (options.customMerge) { - const { clearVariables: shouldClearVars, clearStyles: shouldClearStyles } = options.customMerge; - if (shouldClearVars && shouldClearStyles) { - Logger.log('🎯 Custom Merge: Clearing both variables and styles...'); - await clearAll(); - } - else if (shouldClearVars) { - Logger.log('🎯 Custom Merge: Clearing variables only...'); - await clearVariables(); - } - else if (shouldClearStyles) { - Logger.log('🎯 Custom Merge: Clearing styles only...'); - await clearStyles(); - } - Logger.log('✅ Custom Merge: Clearing complete, rebuilding cache...'); - await variableCache.rebuild(); - } - await variableCache.initialize(); - let createdCollections = 0; - let createdVariables = 0; - let updatedVariables = 0; - let skippedVariables = 0; - let stylesCreated = 0; - let stylesUpdated = 0; - // Separate styles from collections - let stylesData = null; - const collectionData = []; - for (const item of importData) { - const keys = Object.keys(item); - if (keys.length === 1 && keys[0] === '_styles') { - stylesData = item._styles; - } - else { - collectionData.push(item); - } - } - // Collect all pending aliases across all collections for pass 2 - const allPendingAliases = []; - // Process collections - PASS 1: Create variables with raw values - Logger.log(`📥 Pass 1: Processing ${collectionData.length} collections...`); - for (const collectionObj of collectionData) { - const jsonCollectionName = Object.keys(collectionObj)[0]; - const collectionContent = collectionObj[jsonCollectionName]; - // Use $originalName if present (for round-trip with code-friendly naming) - // This restores original Figma names when importing JSON that was exported with naming conventions - const collectionName = collectionContent.$originalName || jsonCollectionName; - Logger.log(`Processing collection: ${jsonCollectionName}${collectionContent.$originalName ? ` (original: ${collectionName})` : ''}`); - // Get per-collection behavior (default to merge in simple mode) - // Check both JSON name and original name for behavior lookup - const collectionBehavior = ((_a = options.collectionBehaviors) === null || _a === void 0 ? void 0 : _a[jsonCollectionName]) || - ((_b = options.collectionBehaviors) === null || _b === void 0 ? void 0 : _b[collectionName]) || 'merge'; - let collection; - const existingCollection = variableCache.getCollection(collectionName); - if (existingCollection) { - // Handle per-collection behavior (Advanced mode) - if (collectionBehavior === 'replace') { - // Replace mode: delete existing collection and create fresh - Logger.log(` Replacing collection: ${collectionName}`); - try { - existingCollection.remove(); - variableCache.removeCollection(collectionName); - collection = figma.variables.createVariableCollection(collectionName); - variableCache.setCollection(collectionName, collection); - createdCollections++; - Logger.log(` Created fresh collection (replaced)`); - } - catch (e) { - Logger.log(` ⚠️ Could not replace collection: ${e}`); - continue; - } - } - else if (!options.merge) { - Logger.log(` Skipping existing collection: ${collectionName}`); - continue; - } - else { - collection = existingCollection; - Logger.log(` Merging into existing collection`); - } - } - else { - collection = figma.variables.createVariableCollection(collectionName); - variableCache.setCollection(collectionName, collection); - createdCollections++; - Logger.log(` Created new collection`); - } - // Setup modes - const modeNames = Object.keys(collectionContent.modes); - const modeMap = new Map(); - for (const mode of collection.modes) { - modeMap.set(mode.name, mode.modeId); - } - if (collection.modes.length === 1 && !modeMap.has(modeNames[0])) { - collection.renameMode(collection.modes[0].modeId, modeNames[0]); - modeMap.set(modeNames[0], collection.modes[0].modeId); - } - for (const modeName of modeNames) { - if (!modeMap.has(modeName)) { - try { - const newModeId = collection.addMode(modeName); - modeMap.set(modeName, newModeId); - } - catch (e) { - Logger.log(` ⚠️ Could not create mode ${modeName}: ${e}`); - } - } - } - // Process variables - TWO PASS APPROACH - // Pass 1: Create all variables and set RAW values only (skip aliases) - // Pass 2: Set ALIAS values (now all target variables exist) - const firstModeVars = collectionContent.modes[modeNames[0]]; - const variablePaths = flattenVariables(firstModeVars, ''); - // Store pending alias assignments for pass 2 - const pendingAliases = []; - // PASS 1: Create variables and set raw values - Logger.log(` Pass 1: Creating variables with raw values...`); - for (const { path, value } of variablePaths) { - const fullPath = `${collectionName}/${path}`; - let variable; - const existingVar = variableCache.getVariable(fullPath); - if (existingVar) { - if (!options.overwrite) { - skippedVariables++; - continue; - } - variable = existingVar; - updatedVariables++; - } - else { - try { - variable = figma.variables.createVariable(path, collection, TypeMapper.toFigmaType(value.$type)); - createdVariables++; - } - catch (e) { - Logger.log(` ⚠️ Could not create variable ${path}: ${e}`); - continue; - } - } - if (value.$description) { - variable.description = value.$description; - } - try { - variable.scopes = TypeMapper.arrayToScopes(value.$scopes); - } - catch ( /* Skip */_c) { /* Skip */ } - // Set values for each mode - raw values only in pass 1, queue aliases for pass 2 - for (const modeName of modeNames) { - const modeId = modeMap.get(modeName); - if (!modeId) - continue; - const modeValue = getValueAtPath(collectionContent.modes[modeName], path); - if (!modeValue) - continue; - if (typeof modeValue.$value === 'string' && modeValue.$value.startsWith('{')) { - // This is an alias - queue for pass 2 - const aliasPath = modeValue.$value.slice(1, -1).replace(/\./g, '/'); - const aliasCollection = modeValue.$collectionName || collectionName; - pendingAliases.push({ - variable, - modeId, - aliasPath, - aliasCollection, - fallbackValue: modeValue - }); - // Set a temporary raw value in case alias resolution fails - setRawValue(variable, modeId, modeValue); - } - else { - // Raw value - set immediately - setRawValue(variable, modeId, modeValue); - } - } - variableCache.setVariable(fullPath, variable); - } - // Store pending aliases for this collection (will be processed after all collections) - allPendingAliases.push(...pendingAliases); - } - // PASS 2: Resolve all aliases (now all variables from all collections exist) - Logger.log(`📥 Pass 2: Resolving ${allPendingAliases.length} alias references...`); - await variableCache.rebuild(); // Ensure cache has all newly created variables - let aliasesResolved = 0; - let aliasesFailed = 0; - for (const pending of allPendingAliases) { - const targetVar = variableCache.getVariable(`${pending.aliasCollection}/${pending.aliasPath}`); - if (targetVar) { - try { - pending.variable.setValueForMode(pending.modeId, { type: 'VARIABLE_ALIAS', id: targetVar.id }); - aliasesResolved++; - } - catch (e) { - // Alias failed, raw value was already set as fallback - aliasesFailed++; - Logger.log(` ⚠️ Could not set alias for ${pending.variable.name}: ${e}`); - } - } - else { - // Target not found, raw value was already set as fallback - aliasesFailed++; - Logger.log(` ⚠️ Alias target not found: ${pending.aliasCollection}/${pending.aliasPath}`); - } - } - if (allPendingAliases.length > 0) { - Logger.log(` ✅ Aliases: ${aliasesResolved} resolved, ${aliasesFailed} used fallback values`); - } - // Import styles - if (stylesData && options.importStyles) { - Logger.log('📦 Importing styles...'); - await variableCache.rebuild(); - if (stylesData.colorStyles) { - const r = await ColorStyleProcessor.importStyles(stylesData.colorStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - if (stylesData.textStyles) { - const r = await TextStyleProcessor.importStyles(stylesData.textStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - if (stylesData.effectStyles) { - const r = await EffectStyleProcessor.importStyles(stylesData.effectStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - if (stylesData.gridStyles) { - const r = await GridStyleProcessor.importStyles(stylesData.gridStyles, variableCache); - stylesCreated += r.created; - stylesUpdated += r.updated; - } - } - const stats = { - collectionsCreated: createdCollections, - variablesCreated: createdVariables, - variablesUpdated: updatedVariables, - variablesSkipped: skippedVariables, - stylesCreated, - stylesUpdated - }; - Logger.log(`✅ Import complete!`); - Logger.send('import_complete', { stats }); - } - catch (e) { - const errorMessage = e instanceof Error ? e.message : String(e); - Logger.log(`❌ Import error: ${errorMessage}`); - // Automatic rollback if we have a pre-import snapshot - if (preImportSnapshot) { - Logger.log('🔄 Attempting automatic rollback to pre-import state...'); - Logger.send('import_rolling_back', { error: errorMessage }); - try { - await restoreFromSnapshot(preImportSnapshot); - Logger.log('✅ Automatic rollback successful - file restored to pre-import state'); - Logger.send('import_rollback_complete', { - error: errorMessage, - message: 'Import failed but your file has been automatically restored to its previous state.' - }); - } - catch (rollbackError) { - const rollbackErrorMsg = rollbackError instanceof Error ? rollbackError.message : String(rollbackError); - Logger.log(`❌ Rollback failed: ${rollbackErrorMsg}`); - Logger.send('import_rollback_failed', { - error: errorMessage, - rollbackError: rollbackErrorMsg, - message: 'Import failed and automatic rollback also failed. Please use Ctrl+Z (Cmd+Z) to undo manually.' - }); - } - } - else { - // No snapshot available - just report the error - Logger.send('error', { - message: `Import failed: ${errorMessage}. Use Ctrl+Z (Cmd+Z) to undo changes.` - }); - } - } -} -function setRawValue(variable, modeId, value) { - try { - if (value.$type === 'color') { - const rgba = ColorParser.parse(value.$value); - const finalRgba = rgba.a < 1 - ? Object.assign(Object.assign({}, rgba), { a: MathUtils.round2(rgba.a) }) : rgba; - variable.setValueForMode(modeId, finalRgba); - } - else { - variable.setValueForMode(modeId, value.$value); - } - } - catch (e) { - console.error(`Could not set value: ${e}`); - } -} -// ============================================================================ -// SECTION 13: COLLECTION INFO -// ============================================================================ -async function getCollections() { - const collections = await figma.variables.getLocalVariableCollectionsAsync(); - // Log the raw order from Figma API - Logger.log(`📋 Figma API returned ${collections.length} collections in this order:`); - collections.forEach((c, i) => { - Logger.log(` ${i + 1}. "${c.name}" (id: ${c.id})`); - }); - // Track library dependencies and aliases - const libraryDependencies = new Set(); - let totalAliases = 0; - let localAliases = 0; - let libraryAliases = 0; - // Process sequentially to preserve exact order - const data = []; - for (let index = 0; index < collections.length; index++) { - const c = collections[index]; - const types = { color: 0, float: 0, boolean: 0, string: 0 }; - for (const varId of c.variableIds) { - const variable = await figma.variables.getVariableByIdAsync(varId); - if (variable) { - const typeStr = TypeMapper.toExportType(variable.resolvedType); - types[typeStr]++; - // Check for aliases in all modes - for (const modeId of Object.keys(variable.valuesByMode)) { - const value = variable.valuesByMode[modeId]; - if (typeof value === 'object' && value !== null && 'type' in value && value.type === 'VARIABLE_ALIAS') { - totalAliases++; - const aliasedVar = await figma.variables.getVariableByIdAsync(value.id); - if (aliasedVar) { - const aliasedCollection = await figma.variables.getVariableCollectionByIdAsync(aliasedVar.variableCollectionId); - if (aliasedCollection) { - // Check if it's from a remote/library collection - if (aliasedCollection.remote) { - libraryDependencies.add(aliasedCollection.name); - libraryAliases++; - } - else { - localAliases++; - } - } - } - } - } - } - } - data.push({ - id: c.id, - name: c.name, - modes: c.modes.map(m => m.name), - variableCount: c.variableIds.length, - types - }); - } - // Sort alphabetically since Figma API doesn't preserve Variables panel order - data.sort((a, b) => a.name.localeCompare(b.name)); - // Get styles and font info - const paintStyles = await figma.getLocalPaintStylesAsync(); - const textStyles = await figma.getLocalTextStylesAsync(); - const effectStyles = await figma.getLocalEffectStylesAsync(); - const gridStyles = await figma.getLocalGridStylesAsync(); - // Count only exportable paint styles (those with SOLID, GRADIENT, or IMAGE paints) - let exportablePaintStylesCount = 0; - for (const style of paintStyles) { - if (style.paints.length === 0) - continue; - const hasExportablePaint = style.paints.some(p => p.type === 'SOLID' || - p.type === 'GRADIENT_LINEAR' || - p.type === 'GRADIENT_RADIAL' || - p.type === 'GRADIENT_ANGULAR' || - p.type === 'GRADIENT_DIAMOND' || - p.type === 'IMAGE'); - if (hasExportablePaint) - exportablePaintStylesCount++; - } - const styles = { - colorStyles: exportablePaintStylesCount, - textStyles: textStyles.length, - effectStyles: effectStyles.length, - gridStyles: gridStyles.length - }; - // Extract font info from text styles - const fontsUsed = new Map(); - for (const style of textStyles) { - const family = style.fontName.family; - const fontStyle = style.fontName.style; - if (!fontsUsed.has(family)) { - fontsUsed.set(family, new Set()); - } - fontsUsed.get(family).add(fontStyle); - } - const fontsList = Array.from(fontsUsed.entries()).map(([family, styles]) => ({ - family, - styles: Array.from(styles) - })); - // Count variable bindings in paint styles - let styleBindingsCount = 0; - for (const style of paintStyles) { - if (style.boundVariables && Object.keys(style.boundVariables).length > 0) { - styleBindingsCount++; - } - } - Logger.send('collections', { - collections: data, - styles, - libraryDependencies: Array.from(libraryDependencies), - fontsUsed: fontsList, - stats: { - totalVariables: data.reduce((sum, c) => sum + c.variableCount, 0), - totalAliases, - localAliases, - libraryAliases, - styleBindings: styleBindingsCount - } - }); -} -async function getVariablesForCollection(collectionName) { - const allCollections = await figma.variables.getLocalVariableCollectionsAsync(); - const collection = allCollections.find(c => c.name === collectionName); - if (!collection) { - Logger.send('variables', { variables: [] }); - return; - } - const variables = (await Promise.all(collection.variableIds - .map(async (id) => { - const v = await figma.variables.getVariableByIdAsync(id); - return v ? { name: v.name, type: v.resolvedType } : null; - }))) - .filter(Boolean); - Logger.send('variables', { variables }); -} -// ============================================================================ -// SECTION 14: CLEAR FUNCTIONS -// ============================================================================ -async function clearVariables() { - Logger.log('🗑️ Clearing all variables...'); - try { - let deletedCollections = 0; - let deletedVariables = 0; - for (const collection of await figma.variables.getLocalVariableCollectionsAsync()) { - for (const varId of collection.variableIds) { - const variable = await figma.variables.getVariableByIdAsync(varId); - if (variable) { - variable.remove(); - deletedVariables++; - } - } - collection.remove(); - deletedCollections++; - } - Logger.log(`✅ Cleared ${deletedCollections} collections, ${deletedVariables} variables`); - Logger.send('clear_complete', { message: `${deletedCollections} collections, ${deletedVariables} variables` }); - } - catch (e) { - Logger.log(`❌ Clear variables error: ${e}`); - Logger.send('error', { message: `Failed to clear variables: ${e}` }); - } -} -async function clearStyles() { - Logger.log('🗑️ Clearing all styles...'); - try { - let deletedStyles = 0; - for (const style of await figma.getLocalPaintStylesAsync()) { - style.remove(); - deletedStyles++; - } - for (const style of await figma.getLocalTextStylesAsync()) { - style.remove(); - deletedStyles++; - } - for (const style of await figma.getLocalEffectStylesAsync()) { - style.remove(); - deletedStyles++; - } - for (const style of await figma.getLocalGridStylesAsync()) { - style.remove(); - deletedStyles++; - } - Logger.log(`✅ Cleared ${deletedStyles} styles`); - Logger.send('clear_complete', { message: `${deletedStyles} styles` }); - } - catch (e) { - Logger.log(`❌ Clear styles error: ${e}`); - Logger.send('error', { message: `Failed to clear styles: ${e}` }); - } -} -async function clearAll() { - Logger.log('🗑️ Clearing everything...'); - try { - await clearVariables(); - await clearStyles(); - } - catch (e) { - Logger.log(`❌ Clear all error: ${e}`); - Logger.send('error', { message: `Failed to clear: ${e}` }); - } -} -// Create a snapshot of current variables and styles for undo -async function createUndoSnapshot() { - var _a; - Logger.log('📸 Creating snapshot of current file state...'); - // Export all collections using simplified internal format - const collections = await figma.variables.getLocalVariableCollectionsAsync(); - const snapshotCollections = []; - for (const collection of collections) { - const collectionSnapshot = { - name: collection.name, - modes: collection.modes.map(m => ({ id: m.modeId, name: m.name })), - variables: [] - }; - // Process variables - for (const variableId of collection.variableIds) { - const variable = await figma.variables.getVariableByIdAsync(variableId); - if (!variable) - continue; - const varSnapshot = { - name: variable.name, - type: variable.resolvedType, - scopes: [...variable.scopes], - values: {} - }; - for (const mode of collection.modes) { - const value = variable.valuesByMode[mode.modeId]; - if (typeof value === 'object' && value !== null && 'type' in value && value.type === 'VARIABLE_ALIAS') { - // Handle alias - const aliasId = value.id; - const aliasVariable = await figma.variables.getVariableByIdAsync(aliasId); - if (aliasVariable) { - const aliasCollection = await figma.variables.getVariableCollectionByIdAsync(aliasVariable.variableCollectionId); - varSnapshot.values[mode.name] = { - isAlias: true, - aliasName: aliasVariable.name, - aliasCollection: (aliasCollection === null || aliasCollection === void 0 ? void 0 : aliasCollection.name) || '' - }; - } - } - else { - // Handle raw values - if (variable.resolvedType === 'COLOR') { - const rgba = value; - varSnapshot.values[mode.name] = { - isAlias: false, - value: ColorConverter.toHex(rgba) - }; - } - else { - varSnapshot.values[mode.name] = { - isAlias: false, - value: value - }; - } - } - } - collectionSnapshot.variables.push(varSnapshot); - } - snapshotCollections.push(collectionSnapshot); - } - // Export all styles - const stylesExport = { - colorStyles: await ColorStyleProcessor.export({ includeImages: true }), - textStyles: await TextStyleProcessor.export(), - effectStyles: await EffectStyleProcessor.export(), - gridStyles: await GridStyleProcessor.export() - }; - const colorCount = ((_a = stylesExport.colorStyles) === null || _a === void 0 ? void 0 : _a.length) || 0; - Logger.log(`📸 Snapshot captured: ${collections.length} collections, ${colorCount} color styles`); - return { - timestamp: Date.now(), - collections: JSON.stringify(snapshotCollections), - styles: JSON.stringify(stylesExport) - }; -} -// Restore file state from a snapshot (undo) -async function restoreFromSnapshot(snapshot) { - Logger.log('↩️ Restoring file from snapshot...'); - // Step 1: Clear everything - Logger.log(' Step 1: Clearing current state...'); - await clearAll(); - await variableCache.rebuild(); - // Step 2: Restore collections and variables - const snapshotCollections = JSON.parse(snapshot.collections); - Logger.log(` Step 2: Restoring ${snapshotCollections.length} collections...`); - // First pass: Create collections and variables with raw values - const pendingAliases = []; - for (const collSnapshot of snapshotCollections) { - // Create collection - const newCollection = figma.variables.createVariableCollection(collSnapshot.name); - // Setup modes - if (collSnapshot.modes.length > 0) { - // Rename first mode - newCollection.renameMode(newCollection.modes[0].modeId, collSnapshot.modes[0].name); - // Add additional modes - for (let i = 1; i < collSnapshot.modes.length; i++) { - newCollection.addMode(collSnapshot.modes[i].name); - } - } - // Get mode mapping - const modeMap = {}; - for (const mode of newCollection.modes) { - modeMap[mode.name] = mode.modeId; - } - // Process variables - for (const varSnapshot of collSnapshot.variables) { - // Create variable - pass collection node, not ID (required for incremental mode) - const newVar = figma.variables.createVariable(varSnapshot.name, newCollection, varSnapshot.type); - // Set scopes if available - if (varSnapshot.scopes && varSnapshot.scopes.length > 0) { - newVar.scopes = varSnapshot.scopes; - } - // Set values for each mode - for (const modeSnapshot of collSnapshot.modes) { - const modeId = modeMap[modeSnapshot.name]; - const modeValue = varSnapshot.values[modeSnapshot.name]; - if (!modeValue) - continue; - if (modeValue.isAlias && modeValue.aliasName) { - // Queue alias for second pass - pendingAliases.push({ - variable: newVar, - modeId, - aliasPath: modeValue.aliasName, - aliasCollection: modeValue.aliasCollection || collSnapshot.name - }); - } - else if (modeValue.value !== undefined) { - // Set raw value - let rawValue; - if (varSnapshot.type === 'COLOR' && typeof modeValue.value === 'string') { - rawValue = ColorParser.parse(modeValue.value); - } - else { - rawValue = modeValue.value; - } - newVar.setValueForMode(modeId, rawValue); - } - } - } - } - // Second pass: Resolve aliases - Logger.log(` Step 3: Resolving ${pendingAliases.length} aliases...`); - await variableCache.rebuild(); - for (const alias of pendingAliases) { - const targetKey = `${alias.aliasCollection}/${alias.aliasPath}`; - const targetVar = variableCache.getVariable(targetKey); - if (targetVar) { - alias.variable.setValueForMode(alias.modeId, { type: 'VARIABLE_ALIAS', id: targetVar.id }); - } - } - // Step 4: Restore styles - const stylesData = JSON.parse(snapshot.styles); - Logger.log(' Step 4: Restoring styles...'); - if (stylesData.colorStyles && stylesData.colorStyles.length > 0) { - await ColorStyleProcessor.importStyles(stylesData.colorStyles, variableCache); - } - if (stylesData.textStyles && stylesData.textStyles.length > 0) { - await TextStyleProcessor.importStyles(stylesData.textStyles, variableCache); - } - if (stylesData.effectStyles && stylesData.effectStyles.length > 0) { - await EffectStyleProcessor.importStyles(stylesData.effectStyles, variableCache); - } - if (stylesData.gridStyles && stylesData.gridStyles.length > 0) { - await GridStyleProcessor.importStyles(stylesData.gridStyles, variableCache); - } - Logger.log('✅ File restored from snapshot'); -} -// ============================================================================ -// SECTION 15: MESSAGE HANDLER -// ============================================================================ -figma.ui.onmessage = async (msg) => { - switch (msg.type) { - case 'export': - await exportVariables(msg.collections, msg.styleOptions, msg.preserveLibraryRefs, msg.includeImages, msg.namingConvention || 'original', msg.exportFormat || 'figma', msg.selectedModes, msg.resolveAliases || false); - break; - case 'import': - await importVariables(msg.data, msg.options); - break; - case 'validate_import': - // Pre-import validation to check plan limits - try { - const importData = JSON.parse(msg.data); - const planOverride = msg.plan; - const validation = await validateImportAgainstPlan(importData, planOverride); - Logger.send('validation_result', validation); - } - catch (e) { - Logger.send('validation_result', { - errors: [`Invalid JSON: ${e instanceof Error ? e.message : 'Parse error'}`], - canImport: false - }); - } - break; - case 'compute_import_diff': - // Compute what will change before importing - try { - const diffData = JSON.parse(msg.data); - const diff = await computeImportDiff(diffData); - Logger.send('import_diff_result', diff); - } - catch (e) { - Logger.send('import_diff_result', { - error: `Failed to compute diff: ${e instanceof Error ? e.message : 'Unknown error'}` - }); - } - break; - case 'detect_plan': - // Detect current plan based on existing collections - const detectedPlan = await detectCurrentPlan(); - Logger.send('plan_detected', detectedPlan); - break; - case 'clear_variables': - await clearVariables(); - break; - case 'clear_styles': - await clearStyles(); - break; - case 'clear_all': - await clearAll(); - break; - case 'get_collections': - await getCollections(); - break; - case 'get_variables': - await getVariablesForCollection(msg.collection); - break; - case 'check_libraries': - // Check if required library collections are available - try { - const requiredCollections = msg.collections; - const localCollections = await figma.variables.getLocalVariableCollectionsAsync(); - const localCollectionNames = localCollections.map(c => c.name); - // Check for external library collections (remote) - // Note: Figma API doesn't provide direct access to team library collections - // We can only check if variables referencing those libraries can be resolved - const availableCollections = []; - const missingCollections = []; - for (const collectionName of requiredCollections) { - if (localCollectionNames.includes(collectionName)) { - availableCollections.push(collectionName); - } - else { - // Try to find in team libraries (this is a best-effort check) - // Team library collections might still be available for referencing - missingCollections.push(collectionName); - } - } - Logger.send('library_check_result', { - allAvailable: missingCollections.length === 0, - availableCollections, - missingCollections, - requiredCollections - }); - } - catch (e) { - Logger.send('library_check_result', { - allAvailable: false, - availableCollections: [], - missingCollections: msg.collections || [], - requiredCollections: msg.collections || [], - error: e instanceof Error ? e.message : 'Library check failed' - }); - } - break; - case 'check_fonts': - // Check if required fonts are available - try { - const requiredFonts = msg.fonts; - const availableFonts = []; - const missingFonts = []; - // Check each font by attempting to load it - for (const font of requiredFonts) { - try { - await figma.loadFontAsync({ family: font.family, style: font.style }); - availableFonts.push(font); - } - catch (_a) { - missingFonts.push(font); - } - } - Logger.send('font_check_result', { - allAvailable: missingFonts.length === 0, - availableFonts, - missingFonts, - requiredFonts - }); - } - catch (e) { - Logger.send('font_check_result', { - allAvailable: false, - availableFonts: [], - missingFonts: msg.fonts || [], - requiredFonts: msg.fonts || [], - error: e instanceof Error ? e.message : 'Font check failed' - }); - } - break; - case 'create_undo_snapshot': - // Create a snapshot of current variables and styles for undo capability - try { - Logger.log('📸 Creating undo snapshot...'); - const snapshot = await createUndoSnapshot(); - Logger.send('snapshot_created', { snapshot }); - Logger.log('✅ Undo snapshot created successfully'); - } - catch (e) { - Logger.log(`❌ Failed to create snapshot: ${e instanceof Error ? e.message : 'Unknown error'}`); - Logger.send('snapshot_error', { error: e instanceof Error ? e.message : 'Failed to create snapshot' }); - } - break; - case 'undo_import': - // Restore file to pre-import state using snapshot - try { - Logger.log('↩️ Undoing import using snapshot...'); - const snapshotData = msg.snapshot; - await restoreFromSnapshot(snapshotData); - Logger.send('undo_complete', {}); - Logger.log('✅ Import undone successfully'); - } - catch (e) { - Logger.log(`❌ Undo failed: ${e instanceof Error ? e.message : 'Unknown error'}`); - Logger.send('undo_error', { error: e instanceof Error ? e.message : 'Undo failed' }); - } - break; - case 'close': - figma.closePlugin(); - break; - } -}; diff --git a/variables-styles-extractor/releases/v2.0.0/manifest.json b/variables-styles-extractor/releases/v2.0.0/manifest.json deleted file mode 100644 index 31722a3..0000000 --- a/variables-styles-extractor/releases/v2.0.0/manifest.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "name": "Variables and Styles Extractor", - "id": "", - "api": "1.0.0", - "main": "code.js", - "ui": "ui.html", - "documentAccess": "dynamic-page", - "capabilities": [], - "enableProposedApi": false, - "editorType": ["figma"], - "networkAccess": { - "allowedDomains": ["none"] - }, - "permissions": ["currentuser"] -} diff --git a/variables-styles-extractor/releases/v2.0.0/ui.html b/variables-styles-extractor/releases/v2.0.0/ui.html deleted file mode 100644 index 35ffd71..0000000 --- a/variables-styles-extractor/releases/v2.0.0/ui.html +++ /dev/null @@ -1,8815 +0,0 @@ - - - - - - - - Variables and Styles Extractor v2.0.0 - - - - -
- -
-
- - -
- -
- - - - -
-
- - -
- - -
- - -
-
-
-
- 🧩 Selection - -
-
- -
-
- 🗂️ Variables Collections -
-
- - -
-
-
-
-
- Loading collections... -
-
-
-
- - -
-
- 🎨 Select Styles -
-
- - -
-
- - - - -
- - -
-
-
- - -
-
-
-
- 📋 Status Check -
-
-
- - - - - - - - - - - - - - - - - - - - - - - -
-
- - Loading structure... -
-
-
- - -
-
- 📁 Preview -
- - -
-
- - -
- -
-
-
-
-
-
-
-
-
- 📊 - Select collections to see stats -
-
- - -
- - -
- - -
-
- 📁 Activity Log -
- - -
-
-
-
-
- --:--:-- - Variables Extractor v2.0 ready -
-
- - -
-
- -
- - -
- - -
-
- 🧩 Input & Select - -
-
-
-
-
- -
- -
-
- 📥 Load JSON Data -
-
- - -
- -
- -
- - -
-
- - - -
-
- - - - -
- - -
-
-
- 📋 Check and Validate -
-
-
-
- - - -
- - -
- -
-
🎯
-
-
Figma File Plan
-
Detecting...
-
-
- -
-
-
-
- 🌱 Starter - 1 mode per collection -
-
- 💼 Professional - Up to 10 modes -
-
- 🏢 Organization - Up to 20 modes -
-
- 🏛️ Enterprise - Unlimited modes -
-
-

💡 Your Figma file plan determines which modes you can import.

-
-
- - - - - - -
- - -
- - Load JSON to validate -
-
-
- - -
- - -
-
- 📁 Preview -
- - -
-
- - -
- - - - -
- -
- 📊 - Load JSON to see stats -
-
- - -
- - -
- - -
-
- 📁 Activity Log -
- - -
-
-
-
-
- --:--:-- - Ready to import -
-
- - - - -
-
- -
-
- - - - - - - - - - - - - - - - - - - - diff --git a/variables-styles-extractor/src/code.ts b/variables-styles-extractor/src/code.ts index 62860ce..f9db231 100644 --- a/variables-styles-extractor/src/code.ts +++ b/variables-styles-extractor/src/code.ts @@ -18,27 +18,6 @@ figma.showUI(__html__, { title: '☕️ Variables & Styles Extractor v2.0.0' }); -// ============================================================================ -// SECTION 1: RESULT TYPE PATTERN (JSF Rule 4.13 - Explicit Error Handling) -// ============================================================================ - -interface Success { - readonly ok: true; - readonly value: T; -} - -interface Failure { - readonly ok: false; - readonly error: E; -} - -type Result = Success | Failure; - -const Result = { - ok: (value: T): Success => ({ ok: true, value }), - err: (error: E): Failure => ({ ok: false, error }), -} as const; - // ============================================================================ // SECTION 2: TYPE DEFINITIONS (JSF Rule 4.9 - Strong Typing) // ============================================================================ @@ -3176,25 +3155,6 @@ async function getCollections(): Promise { }); } -async function getVariablesForCollection(collectionName: string): Promise { - const allCollections = await figma.variables.getLocalVariableCollectionsAsync(); - const collection = allCollections.find(c => c.name === collectionName); - - if (!collection) { - Logger.send('variables', { variables: [] }); - return; - } - - const variables = (await Promise.all(collection.variableIds - .map(async id => { - const v = await figma.variables.getVariableByIdAsync(id); - return v ? { name: v.name, type: v.resolvedType } : null; - }))) - .filter(Boolean); - - Logger.send('variables', { variables }); -} - // ============================================================================ // SECTION 14: CLEAR FUNCTIONS // ============================================================================ @@ -3541,11 +3501,7 @@ figma.ui.onmessage = async (msg: { type: string; [key: string]: unknown }) => { case 'get_collections': await getCollections(); break; - - case 'get_variables': - await getVariablesForCollection(msg.collection as string); - break; - + case 'check_libraries': // Check if required library collections are available try { @@ -3650,9 +3606,5 @@ figma.ui.onmessage = async (msg: { type: string; [key: string]: unknown }) => { Logger.send('undo_error', { error: e instanceof Error ? e.message : 'Undo failed' }); } break; - - case 'close': - figma.closePlugin(); - break; } }; diff --git a/variables-styles-extractor/ui.html b/variables-styles-extractor/ui.html index d59a64c..8367305 100644 --- a/variables-styles-extractor/ui.html +++ b/variables-styles-extractor/ui.html @@ -701,15 +701,6 @@ border-color: var(--color-primary); } - /* Advanced mode indicator */ - .advanced-only { - display: none; - } - - body.advanced-mode .advanced-only { - display: block; - } - .collection-item-row { display: flex; align-items: center; @@ -4477,29 +4468,6 @@ }); } - // Batch DOM updates using DocumentFragment - function batchDOMUpdate(container, htmlContent) { - rafUpdate(() => { - container.innerHTML = htmlContent; - }); - } - - // Process array in chunks to avoid blocking UI - async function processInChunks(array, processFn, chunkSize = 50) { - const results = []; - for (let i = 0; i < array.length; i += chunkSize) { - const chunk = array.slice(i, i + chunkSize); - const chunkResults = chunk.map(processFn); - results.push(...chunkResults); - - // Yield to browser between chunks - if (i + chunkSize < array.length) { - await new Promise(resolve => rafUpdate(resolve)); - } - } - return results; - } - // Efficient object flattening with early termination option function flattenObjectFast(obj, prefix = '', maxDepth = 20) { const results = []; @@ -4986,27 +4954,6 @@ addLog('🗑️ Reset to input - cleared all', 'info', 'import'); } - // Select all import collections - function selectAllImport(select) { - const collectionsList = document.getElementById('import-collections-list'); - if (!collectionsList) return; - - const checkboxes = collectionsList.querySelectorAll('.collection-checkbox'); - checkboxes.forEach(checkbox => { - checkbox.checked = select; - const colName = checkbox.closest('.collection-item')?.querySelector('.collection-name')?.textContent; - if (colName) { - if (select) { - selectedImportCollections.add(colName); - } else { - selectedImportCollections.delete(colName); - } - } - }); - - updateImportButtonState(); - } - // Select all import styles function selectAllImportStyles(select) { const styleCheckboxes = document.querySelectorAll('#import-styles-options input[type="checkbox"]'); @@ -6579,9 +6526,6 @@ updateStatusCheckVisibility(); break; - case 'collection_details': - updateCollectionDetails(msg.data); - break; case 'export_complete': exportData = msg.data.data; document.getElementById('export-output').value = exportData; @@ -6842,6 +6786,10 @@ }); } + function showToast(message, type) { + addLog(message, type === 'error' ? 'error' : 'info'); + } + // ========== EXPORT ========== // Track selected export modes per collection @@ -7907,11 +7855,7 @@ const treeEmptyPreview = document.getElementById('import-empty-preview-tree'); if (!treeContainer) return; - - // Use DocumentFragment for better performance - const fragment = document.createDocumentFragment(); - const tempDiv = document.createElement('div'); - + let treeHtml = ''; // Build tree for variable collections (same structure as export) From fd39565c79e4201960ae70b6228b01a48a908f76 Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 15:26:46 +0530 Subject: [PATCH 02/20] css: consolidate duplicate/conflicting styles, remove blocked font fetch (-279 net lines) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cascade-preserving cleanup of the single style block (every consolidation keeps the declarations the pre-change cascade computed; verified by an adversarial declaration-by-declaration review + before/after rendering of both tabs, both modes, and the tip modal): - One .scrollable utility replaces 6 byte-identical per-container scrollbar groups (8 containers re-classed; the 2 genuinely-different dark/slim scrollbar groups kept standalone) - One shared scroll-fade pattern replaces 5 copies (per-site overrides retain only real differences) - Conflicting duplicate selector pairs resolved to their computed winners: .plugin-footer group, .styles-options (grid wins), .hidden x3 -> one rule (drops the inert content-visibility:hidden, BP-001 family), .empty-state, .loading/.spinner, .log-*, back-to-back .collection-item pair merged Compliance (Figma plugin guidelines): - Removed the Google Fonts — manifest declares networkAccess.allowedDomains ["none"], so the request was CSP-blocked at runtime anyway, and the 'Cookie' family was referenced by zero font-family declarations - contain: content -> contain: layout style at all 3 sites (same containment family as the documented KI-001 invisible-elements bug) - rel="noopener noreferrer" added to all 4 target="_blank" anchors Co-Authored-By: Claude Fable 5 --- variables-styles-extractor/ui.html | 461 ++++++----------------------- 1 file changed, 91 insertions(+), 370 deletions(-) diff --git a/variables-styles-extractor/ui.html b/variables-styles-extractor/ui.html index 8367305..f1e730b 100644 --- a/variables-styles-extractor/ui.html +++ b/variables-styles-extractor/ui.html @@ -16,7 +16,6 @@ - Variables and Styles Extractor v2.0.0 @@ -3438,7 +3561,70 @@ - + + +
+ + +
+
+ 🗂️ Variables + +
+
+
+
+ Variables Collections +
+
+ + +
+
+
+
+
+ + +
+
+ 🎨 Styles +
+
+
+
+ Styles +
+
+ + +
+
+
+
+
+ + +
+
+ 📁 Activity Log +
+ + +
+
+
+ +
+
+ + +
+
+
+ +
+ @@ -3895,7 +4081,74 @@ - + + +
+ + +
+
+ 📥 Load JSON +
+
+
+
+ Load JSON Data +
+
+ + +
+ + + +
+
+
+ + +
+
+ 🧩 Import Contents +
+
+
+
+ Import Contents +
+
+ + +
+
+
+
+
+ + +
+
+ 📁 Activity Log +
+ + +
+
+
+ +
+
+ +
+
+
+
+ +
+ @@ -3957,10 +4210,34 @@ : (cb) => setTimeout(cb, 1); // Cancel idle callback - const cancelIdle = window.cancelIdleCallback + const cancelIdle = window.cancelIdleCallback ? (id) => window.cancelIdleCallback(id) : (id) => clearTimeout(id); - + + // ========== SIMPLE-MODE SHARED HELPERS ========== + // Escape any interpolated value before inserting into HTML (XSS-safe rendering) + function escapeHtml(value) { + return String(value).replace(/[&<>"']/g, function (ch) { + return { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[ch]; + }); + } + + // Group key = substring before first '/' in the item name; '' = ungrouped + function getGroupKeyUI(name) { + const idx = String(name).indexOf('/'); + return idx === -1 ? '' : String(name).substring(0, idx); + } + + const UNGROUPED_LABEL = '(ungrouped)'; + + // Style type registry for Simple-mode group lists + const STYLE_TYPES = [ + { type: 'color', label: '🎨 Colour Styles', exportKey: 'colorStyles' }, + { type: 'text', label: '✏️ Text Styles', exportKey: 'textStyles' }, + { type: 'effect', label: '✨ Effect Styles', exportKey: 'effectStyles' }, + { type: 'grid', label: '📐 Layout Guides', exportKey: 'gridStyles' } + ]; + // ========== WEB WORKER FOR JSON PARSING ========== // Create inline Web Worker for heavy JSON operations (parsing & stringifying) const jsonWorkerBlob = new Blob([` @@ -4228,6 +4505,12 @@ if (fileLoadedIndicator) { fileLoadedIndicator.classList.add('hidden'); } + + // Simple mode: keep the mirrored indicator in sync (stage 3 wiring) + const simpleFileLoadedIndicator = document.getElementById('simple-import-file-loaded'); + if (simpleFileLoadedIndicator) { + simpleFileLoadedIndicator.classList.add('hidden'); + } // Hide the import result preview section const resultPreview = document.getElementById('import-result-preview-section'); @@ -4314,6 +4597,10 @@ parsedImportCollections = []; parsedImportStyles = null; selectedImportCollections.clear(); + + // Simple mode: clear mirrored input + group selection (stage 3 wiring) + clearSimpleImportInputUi(); + clearSimpleImportSelectionState(); // Reset import images option const importImagesOption = document.getElementById('import-images-option'); @@ -4686,6 +4973,7 @@ // Update import style selection state function updateImportStyleSelection() { + syncSimpleImportStyleGroupsFromAdvanced(); // write-through (simple mode) updateImportButtonState(); updateImportPreviewStats(); } @@ -5019,6 +5307,20 @@ let importPreviewReviewed = false; // Must review import preview before importing let lastImportSnapshot = null; // Snapshot of file state before import for undo + // ===== Simple-mode group-level selection ===== + // selectedExportCollections / selectedImportCollections become PROJECTIONS of + // these maps: a collection is in the set iff its group Set is non-empty. + let selectedExportGroups = new Map(); // collectionName -> Set(groupKey) + let selectedExportStyleGroups = { color: new Set(), text: new Set(), effect: new Set(), grid: new Set() }; + let stylesGroupsInfo = { color: [], text: [], effect: [], grid: [] }; // GroupSummary[] from backend + let selectedImportGroups = new Map(); // collectionName -> Set(groupKey) + let selectedImportStyleGroups = { color: new Set(), text: new Set(), effect: new Set(), grid: new Set() }; + let importGroupsIndex = new Map(); // collectionName -> GroupSummary[] (computed UI-side) + let importStyleGroupsIndex = { color: [], text: [], effect: [], grid: [] }; + let expandedSimpleExport = new Set(); // keys 'col:' / 'style:' + let expandedSimpleImport = new Set(); + let simpleExportPendingAction = null; // 'download' | 'copy' | null + // Plan limits (mirror of backend constants) const PLAN_LIMITS = { starter: { maxModes: 1, name: 'Starter', detail: '1 mode only (no multi-mode)' }, @@ -6028,6 +6330,9 @@ if (exportLibraryOption && window.detectedLibraryDeps?.length > 0) { exportLibraryOption.classList.remove('hidden'); } + // Simple -> Advanced: restore shared nodes + project Simple state into Advanced DOM (stage 3 wiring) + moveSharedNodesForMode(true); + syncAdvancedDomFromSimpleState(); } else { document.body.classList.remove('advanced-mode'); // Hide library refs option in simple mode @@ -6035,6 +6340,15 @@ if (exportLibraryOption) { exportLibraryOption.classList.add('hidden'); } + // Advanced -> Simple: adopt shared nodes + rebuild group state from Advanced DOM (stage 3 wiring) + moveSharedNodesForMode(false); + rebuildSimpleStateFromAdvanced(); + const si = document.getElementById('simple-import-input'); + const ai = document.getElementById('import-input'); + if (si && ai) si.value = ai.value; + renderSimpleExportVariables(); + renderSimpleExportStyles(); + renderSimpleImportContents(); } // Re-render import collections if on selection face const selectionFace = document.getElementById('import-selection-face'); @@ -6245,6 +6559,17 @@ } updateStatusCheckVisibility(); + + // Simple mode: seed group-level selection + render (stage 3 wiring) + stylesGroupsInfo = msg.data.styleGroups || { color: [], text: [], effect: [], grid: [] }; + initSimpleExportSelectionFromCollections(); + // Seed the Simple style state from the checkboxes updateStyleCheckboxLabels() + // just auto-checked, so the pre-existing Advanced default (styles included + // when counts > 0) is preserved; reconcile then drops stale group keys. + syncSimpleExportStyleGroupsFromAdvanced(); + reconcileSimpleStyleSelection(); + renderSimpleExportVariables(); + renderSimpleExportStyles(); break; case 'export_complete': @@ -6295,6 +6620,18 @@ fontBanner.classList.add('hidden'); updateStatusCheckVisibility(); } + + // Simple mode: complete the pending one-click action (stage 3 wiring) + if (simpleExportPendingAction) { + const simpleExportAct = simpleExportPendingAction; + simpleExportPendingAction = null; + setSimpleExportButtonsDisabled(false); + if (simpleExportAct === 'copy') { + copyExport(); + } else { + downloadExport(); + } + } break; case 'import_complete': const s = msg.data.stats; @@ -6309,6 +6646,9 @@ loadCollections(); document.getElementById('import-input').value = ''; document.getElementById('file-input').value = ''; // Reset file input for re-selection + // Simple mode: keep the mirrored textarea in sync (stage 3 wiring) + const simpleImportInputAfterDone = document.getElementById('simple-import-input'); + if (simpleImportInputAfterDone) simpleImportInputAfterDone.value = ''; const importBtnComplete = document.getElementById('import-btn'); importBtnComplete.disabled = true; importBtnComplete.textContent = '📥 Import Selected'; // Reset button text @@ -6344,6 +6684,12 @@ clearBtnFlash.classList.remove('btn-flash-attention'); }, 2500); } + + // Simple mode: the input was consumed, so clear the Simple selection + // state + contents list, leaving the Import button disabled — mirrors + // Advanced, which disables #import-btn above (stage 3 wiring) + resetSimpleImportButton(); + clearSimpleImportSelectionState(); break; case 'snapshot_created': // Snapshot created, now proceed with actual import @@ -6402,6 +6748,9 @@ importBtnRollback.disabled = false; importBtnRollback.textContent = '📥 Import Selected'; // Reset button text loadCollections(); + // Simple mode: re-enable the Simple import button (stage 3 wiring) + resetSimpleImportButton(); + updateSimpleImportButtonState(); break; case 'import_rollback_failed': // Automatic rollback failed @@ -6411,6 +6760,9 @@ const importBtnFailed = document.getElementById('import-btn'); importBtnFailed.disabled = false; importBtnFailed.textContent = '📥 Import Selected'; // Reset button text + // Simple mode: re-enable the Simple import button (stage 3 wiring) + resetSimpleImportButton(); + updateSimpleImportButtonState(); break; case 'clear_complete': addLog(`✅ Cleared: ${msg.data.message}`, 'success'); @@ -6465,6 +6817,10 @@ break; case 'error': addLog('❌ ' + msg.data.message, 'error'); + // Simple mode: release the action buttons on backend failure (stage 3 wiring) + simpleExportPendingAction = null; + setSimpleExportButtonsDisabled(false); + resetSimpleImportButton(); break; } }; @@ -6688,6 +7044,7 @@ if (stylesInfo.colorStyles > 0) document.getElementById('export-color-styles').checked = select; if (stylesInfo.effectStyles > 0) document.getElementById('export-effect-styles').checked = select; if (stylesInfo.gridStyles > 0) document.getElementById('export-grid-styles').checked = select; + syncSimpleExportStyleGroupsFromAdvanced(); // write-through (simple mode; programmatic .checked fires no change event) updateExportPreview(); } @@ -6712,6 +7069,8 @@ } else { selectedExportCollections.delete(name); } + setSelectedGroupsForCollection(name, selected); // write-through (simple mode) + renderSimpleExportVariables(); // write-through (simple mode) updateExportPreview(); } @@ -6727,6 +7086,8 @@ } } }); + collections.forEach(c => setSelectedGroupsForCollection(c.name, select)); // write-through (simple mode) + renderSimpleExportVariables(); // write-through (simple mode) updateExportPreview(); } @@ -6953,12 +7314,20 @@ return; } - const styleOptions = { - colorStyles: document.getElementById('export-color-styles').checked, - textStyles: document.getElementById('export-text-styles').checked, - effectStyles: document.getElementById('export-effect-styles').checked, - gridStyles: document.getElementById('export-grid-styles').checked - }; + // Advanced: read the style checkboxes; Simple: derive from group-level state + const styleOptions = isAdvancedMode() + ? { + colorStyles: document.getElementById('export-color-styles').checked, + textStyles: document.getElementById('export-text-styles').checked, + effectStyles: document.getElementById('export-effect-styles').checked, + gridStyles: document.getElementById('export-grid-styles').checked + } + : { + colorStyles: selectedExportStyleGroups.color.size > 0, + textStyles: selectedExportStyleGroups.text.size > 0, + effectStyles: selectedExportStyleGroups.effect.size > 0, + gridStyles: selectedExportStyleGroups.grid.size > 0 + }; // In Simple mode, always include images when color styles are selected // In Advanced mode, use the checkbox value @@ -6991,7 +7360,15 @@ } }); } - + + // Simple mode: group-level payloads (null = no partial selections to send) + let selectedGroupsPayload = null; + let selectedStyleGroupsPayload = null; + if (!isAdvancedMode()) { + selectedGroupsPayload = buildSelectedGroupsPayload(); + selectedStyleGroupsPayload = buildSelectedStyleGroupsPayload(); + } + if (includeImages) { addLog(`📤 Exporting ${selectedExportCollections.size} collections with image data...`); } else { @@ -7024,8 +7401,10 @@ namingConvention: namingConvention, exportFormat: exportFormat, selectedModes: selectedModesMap, - resolveAliases: resolveAliases - } + resolveAliases: resolveAliases, + selectedGroups: selectedGroupsPayload, + selectedStyleGroups: selectedStyleGroupsPayload + } }, '*'); } @@ -7124,6 +7503,9 @@ document.getElementById('validation-results').innerHTML = ''; const clearBtn = document.getElementById('import-clear-btn'); if (clearBtn) clearBtn.disabled = true; + + // Simple mode: emptied input -> drop stale group selection/index (stage 3 wiring) + clearSimpleImportSelectionState(); } async function parseImportPreviewAsync() { @@ -7491,6 +7873,11 @@ // Also detect Figma file plan from existing collections parent.postMessage({ pluginMessage: { type: 'detect_plan' } }, '*'); + // Simple mode: rebuild group index + default-select everything (stage 3 wiring) + computeImportGroupsIndex(); + initSimpleImportSelectionAll(); + renderSimpleImportContents(); + } catch (e) { // Hide skeletons on error if (statusSkeleton) statusSkeleton.classList.add('hidden'); @@ -7512,6 +7899,12 @@ if (statusEl) statusEl.textContent = '❌ Invalid JSON format'; importData = null; addLog('❌ Invalid JSON: ' + e.message, 'error', 'import'); + + // Simple mode: drop stale group selection/index (stage 3 wiring). + // The textarea mirror is intentionally NOT cleared here: #import-input + // keeps its content on parse failure (mid-edit JSON), so the Simple + // textarea must keep it too or live typing would be wiped. + clearSimpleImportSelectionState(); } } @@ -7675,6 +8068,8 @@ } else { selectedImportCollections.delete(name); } + setSelectedImportGroupsForCollection(name, selected); // write-through (simple mode) + renderSimpleImportContents(); // write-through (simple mode) updateImportStatus(); // Update preview stats to reflect selection change @@ -7704,6 +8099,11 @@ } } }); + importData.forEach(colObj => { // write-through (simple mode) + const colName = Object.keys(colObj)[0]; + if (colName && colName !== '_styles') setSelectedImportGroupsForCollection(colName, select); + }); + renderSimpleImportContents(); // write-through (simple mode) updateImportStatus(); // Update preview stats to reflect selection change @@ -7769,7 +8169,12 @@ const colName = Object.keys(colObj)[0]; return colName === '_styles' || selectedImportCollections.has(colName); }); - + + // Simple mode: prune the payload down to the group-level selection (stage 3 wiring) + if (!isAdvancedMode()) { + filteredData = pruneImportDataForSimpleSelection(filteredData); + } + // If mode selection is active, filter modes in each collection const planInfo = PLAN_LIMITS[selectedPlan]; if (Object.keys(selectedModes).length > 0 && planInfo.maxModes !== Infinity) { @@ -7799,9 +8204,16 @@ addLog(`📥 Importing ${selectedImportCollections.size} collections...`); } - // Remove styles if none selected in the styles section - const styleCheckboxes = document.querySelectorAll('#import-styles-options input[type="checkbox"]:checked'); - const includeStyles = styleCheckboxes.length > 0; + // Remove styles if none selected in the styles section. + // Advanced: read the selection-face checkboxes (populated by proceedToImportSelection). + // Simple: that face never runs, so derive from the group-level state instead. + let includeStyles; + if (isAdvancedMode()) { + const styleCheckboxes = document.querySelectorAll('#import-styles-options input[type="checkbox"]:checked'); + includeStyles = styleCheckboxes.length > 0; + } else { + includeStyles = STYLE_TYPES.some(st => selectedImportStyleGroups[st.type].size > 0); + } let finalData = includeStyles ? filteredData : filteredData.filter(item => Object.keys(item)[0] !== '_styles'); // Check if images should be included: @@ -7893,7 +8305,62 @@ return { '_styles': strippedStyles }; }); } - + + // Simple mode: prune the (already collection-filtered) import payload down to + // the group-level selection. Builds NEW container objects only — leaf values + // keep referencing the original parsed data, which is never mutated. + function pruneImportDataForSimpleSelection(data) { + return data.map(colObj => { + const colName = Object.keys(colObj)[0]; + + if (colName === '_styles') { + const stylesData = colObj['_styles']; + if (!stylesData) return colObj; + // Deep clone (matches stripImageDataFromStyles' clone strategy) + let cloned; + if (typeof structuredClone === 'function') { + cloned = structuredClone(stylesData); + } else { + cloned = JSON.parse(JSON.stringify(stylesData)); + } + STYLE_TYPES.forEach(st => { + if (!Array.isArray(cloned[st.exportKey])) return; + const sel = selectedImportStyleGroups[st.type]; + cloned[st.exportKey] = cloned[st.exportKey].filter(s => sel.has(getGroupKeyUI((s && s.name) || ''))); + if (cloned[st.exportKey].length === 0) delete cloned[st.exportKey]; + }); + // Drop the whole entry when nothing survives + if (Object.keys(cloned).length === 0) return null; + return { '_styles': cloned }; + } + + const sel = selectedImportGroups.get(colName); + if (!sel) return colObj; // no group entry -> collection passes through unchanged + + const colData = colObj[colName] || {}; + const newCol = {}; + if ('$originalName' in colData) newCol.$originalName = colData.$originalName; + const newModes = {}; + const modesObj = colData.modes || {}; + Object.keys(modesObj).forEach(modeName => { + const modeVars = modesObj[modeName]; + const newMode = {}; + if (modeVars && typeof modeVars === 'object') { + Object.keys(modeVars).forEach(k => { + const v = modeVars[k]; + // Leaf test matches flattenObject/computeImportGroupsIndex: + // top-level leaves live under the '' (ungrouped) key + const isLeaf = v && typeof v === 'object' && ('$value' in v || '$type' in v || '$alias' in v); + if (sel.has(isLeaf ? '' : k)) newMode[k] = v; + }); + } + newModes[modeName] = newMode; + }); + newCol.modes = newModes; + return { [colName]: newCol }; + }).filter(entry => entry !== null); + } + function detectLibraryRefsInImportData(data) { const libraryRefs = new Set(); @@ -8082,12 +8549,900 @@ fileNameSpan.textContent = file.name; fileIndicator.classList.remove('hidden'); } - + + // Simple mode: mirror the loaded file into the Simple input + indicator (stage 3 wiring) + const simpleInputMirror = document.getElementById('simple-import-input'); + if (simpleInputMirror) simpleInputMirror.value = e.target.result; + const simpleFileIndicator = document.getElementById('simple-import-file-loaded'); + const simpleFileNameSpan = document.getElementById('simple-import-file-name'); + if (simpleFileIndicator && simpleFileNameSpan) { + simpleFileNameSpan.textContent = file.name; + simpleFileIndicator.classList.remove('hidden'); + } + parseImportPreview(); }; reader.readAsText(file); } + // ========== SIMPLE MODE: RENDERING ========== + // Render + mutation layer for the Simple-mode 3-column group lists. + // XSS-safety: ALL interpolated values pass through escapeHtml() (values AND + // data-* attribute values); identifiers travel via data-* attributes and + // delegated addEventListener (no inline handlers, no string-interpolated JS). + + // --- Row template helpers (private to this section) --- + + // Build ` data-k="v"` pairs; attribute NAMES are internal constants, values escaped. + function simpleDataAttrsHtml(dataAttrs) { + let out = ''; + for (const key in dataAttrs) { + out += ' data-' + key + '="' + escapeHtml(dataAttrs[key]) + '"'; + } + return out; + } + + // opts: { expandKey, expanded, name, meta, dataAttrs (for the checkbox) } + function simpleParentRowHtml(opts) { + return '
' + + '' + (opts.expanded ? '▾' : '▸') + '' + + '' + + '
' + + '
' + escapeHtml(opts.name) + '
' + + '
' + escapeHtml(opts.meta) + '
' + + '
' + + '
'; + } + + function simpleGroupRowHtml(groupKey, count, dataAttrs, checked) { + const isUngrouped = groupKey === ''; + const label = isUngrouped ? UNGROUPED_LABEL : groupKey; + return ''; + } + + function simpleSectionHeaderHtml(label) { + return '
' + escapeHtml(label) + '
'; + } + + // Ungrouped ('') sorts last; everything else alphabetical + function sortGroupSummaries(groups) { + return groups.slice().sort(function (a, b) { + if (a.name === b.name) return 0; + if (a.name === '') return 1; + if (b.name === '') return -1; + return a.name.localeCompare(b.name); + }); + } + + // --- Group accessors (single source for "what groups does X have") --- + + function getExportCollectionGroups(name) { + const c = collections.find(col => col.name === name); + if (!c) return []; + return c.groups || [{ name: '', count: c.variableCount }]; + } + + function getExportStyleGroups(type) { + const fromBackend = stylesGroupsInfo[type] || []; + if (fromBackend.length > 0) return fromBackend; + const st = STYLE_TYPES.find(s => s.type === type); + const count = st ? (stylesInfo[st.exportKey] || 0) : 0; + return count > 0 ? [{ name: '', count: count }] : []; + } + + function getImportCollectionGroups(name) { + return importGroupsIndex.get(name) || []; + } + + function getImportStyleGroups(type) { + return importStyleGroupsIndex[type] || []; + } + + // --- Renderers (full re-render only on data arrival / mode switch) --- + + function renderSimpleExportVariables() { + const listEl = document.getElementById('simple-export-variables-list'); + if (!listEl) return; + if (!collections || collections.length === 0) { + listEl.innerHTML = '
No variable collections found
'; + return; + } + let html = ''; + collections.forEach(c => { + const groups = sortGroupSummaries(c.groups || [{ name: '', count: c.variableCount }]); + const expandKey = 'col:' + c.name; + const expanded = expandedSimpleExport.has(expandKey); + const selected = selectedExportGroups.get(c.name); + html += simpleParentRowHtml({ + expandKey: expandKey, + expanded: expanded, + name: c.name, + meta: c.variableCount + ' variables • Modes: ' + (c.modes || []).join(', '), + dataAttrs: { action: 'export-col-all', collection: c.name } + }); + html += '
'; + groups.forEach(g => { + html += simpleGroupRowHtml(g.name, g.count, + { action: 'export-group', collection: c.name, group: g.name }, + !!(selected && selected.has(g.name))); + }); + html += '
'; + }); + listEl.innerHTML = html; + // Parent tri-state pass + collections.forEach(c => { + const selected = selectedExportGroups.get(c.name); + setSimpleParentTriState('simple-export-variables-list', 'export-col-all', 'collection', c.name, + selected ? selected.size : 0, getExportCollectionGroups(c.name).length); + }); + } + + function renderSimpleExportStyles() { + const listEl = document.getElementById('simple-export-styles-list'); + if (!listEl) return; + const totalStyles = STYLE_TYPES.reduce((acc, st) => acc + (stylesInfo[st.exportKey] || 0), 0); + if (totalStyles === 0) { + listEl.innerHTML = '
No local styles found
'; + return; + } + let html = ''; + STYLE_TYPES.forEach(st => { + const count = stylesInfo[st.exportKey] || 0; + if (count === 0) return; + const groups = sortGroupSummaries(getExportStyleGroups(st.type)); + const expandKey = 'style:' + st.type; + const expanded = expandedSimpleExport.has(expandKey); + const selected = selectedExportStyleGroups[st.type]; + html += simpleParentRowHtml({ + expandKey: expandKey, + expanded: expanded, + name: st.label, + meta: count + (count === 1 ? ' style' : ' styles'), + dataAttrs: { action: 'export-style-all', type: st.type } + }); + html += '
'; + groups.forEach(g => { + html += simpleGroupRowHtml(g.name, g.count, + { action: 'export-style-group', type: st.type, group: g.name }, + selected.has(g.name)); + }); + html += '
'; + }); + listEl.innerHTML = html; + // Parent tri-state pass + STYLE_TYPES.forEach(st => { + if ((stylesInfo[st.exportKey] || 0) === 0) return; + setSimpleParentTriState('simple-export-styles-list', 'export-style-all', 'type', st.type, + selectedExportStyleGroups[st.type].size, getExportStyleGroups(st.type).length); + }); + } + + // Meta line for an import collection parent row + function importCollectionMetaText(name, groups) { + let totalVars = 0; + groups.forEach(g => { totalVars += g.count; }); + let modes = []; + for (let i = 0; i < parsedImportCollections.length; i++) { + const colObj = parsedImportCollections[i]; + if (Object.keys(colObj)[0] === name) { + modes = Object.keys((colObj[name] && colObj[name].modes) || {}); + break; + } + } + return '~' + totalVars + ' variables • Modes: ' + modes.join(', '); + } + + function renderSimpleImportContents() { + const listEl = document.getElementById('simple-import-list'); + if (!listEl) { updateSimpleImportButtonState(); return; } + const hasStyleGroups = STYLE_TYPES.some(st => (importStyleGroupsIndex[st.type] || []).length > 0); + if (importGroupsIndex.size === 0 && !hasStyleGroups) { + listEl.innerHTML = '
Paste or upload JSON to see contents
'; + updateSimpleImportButtonState(); + return; + } + let html = ''; + if (importGroupsIndex.size > 0) { + html += simpleSectionHeaderHtml('📦 Variables'); + importGroupsIndex.forEach((groups, name) => { + const expandKey = 'col:' + name; + const expanded = expandedSimpleImport.has(expandKey); + const selected = selectedImportGroups.get(name); + html += simpleParentRowHtml({ + expandKey: expandKey, + expanded: expanded, + name: name, + meta: importCollectionMetaText(name, groups), + dataAttrs: { action: 'import-col-all', collection: name } + }); + html += '
'; + groups.forEach(g => { + html += simpleGroupRowHtml(g.name, g.count, + { action: 'import-group', collection: name, group: g.name }, + !!(selected && selected.has(g.name))); + }); + html += '
'; + }); + } + if (hasStyleGroups) { + html += simpleSectionHeaderHtml('🎨 Styles'); + STYLE_TYPES.forEach(st => { + const groups = importStyleGroupsIndex[st.type] || []; + if (groups.length === 0) return; + const count = groups.reduce((acc, g) => acc + g.count, 0); + const expandKey = 'style:' + st.type; + const expanded = expandedSimpleImport.has(expandKey); + const selected = selectedImportStyleGroups[st.type]; + html += simpleParentRowHtml({ + expandKey: expandKey, + expanded: expanded, + name: st.label, + meta: count + (count === 1 ? ' style' : ' styles'), + dataAttrs: { action: 'import-style-all', type: st.type } + }); + html += '
'; + groups.forEach(g => { + html += simpleGroupRowHtml(g.name, g.count, + { action: 'import-style-group', type: st.type, group: g.name }, + selected.has(g.name)); + }); + html += '
'; + }); + } + listEl.innerHTML = html; + // Parent tri-state pass + importGroupsIndex.forEach((groups, name) => { + const selected = selectedImportGroups.get(name); + setSimpleParentTriState('simple-import-list', 'import-col-all', 'collection', name, + selected ? selected.size : 0, groups.length); + }); + STYLE_TYPES.forEach(st => { + const groups = importStyleGroupsIndex[st.type] || []; + if (groups.length === 0) return; + setSimpleParentTriState('simple-import-list', 'import-style-all', 'type', st.type, + selectedImportStyleGroups[st.type].size, groups.length); + }); + updateSimpleImportButtonState(); + } + + // Build importGroupsIndex / importStyleGroupsIndex from the parsed import JSON + // (parsedImportCollections: [{name: {modes: {...}}}], parsedImportStyles: _styles entry). + function computeImportGroupsIndex() { + importGroupsIndex = new Map(); + importStyleGroupsIndex = { color: [], text: [], effect: [], grid: [] }; + + (parsedImportCollections || []).forEach(colObj => { + const colName = Object.keys(colObj)[0]; + if (!colName || colName === '_styles') return; + const col = colObj[colName]; + const modesObj = (col && col.modes) || {}; + const groupMax = new Map(); // groupKey -> max leaf count across modes + Object.keys(modesObj).forEach(modeName => { + const modeVars = modesObj[modeName]; + if (!modeVars || typeof modeVars !== 'object') return; + const perMode = new Map(); + Object.keys(modeVars).forEach(k => { + const v = modeVars[k]; + if (!v || typeof v !== 'object') return; + if ('$value' in v || '$type' in v || '$alias' in v) { + // Top-level leaf => ungrouped (key '') — same leaf test as flattenObject + perMode.set('', (perMode.get('') || 0) + 1); + } else { + const leafCount = flattenObject(v).length; + if (leafCount > 0) perMode.set(k, (perMode.get(k) || 0) + leafCount); + } + }); + perMode.forEach((count, key) => { + if (count > (groupMax.get(key) || 0)) groupMax.set(key, count); + }); + }); + const groups = []; + groupMax.forEach((count, key) => { groups.push({ name: key, count: count }); }); + importGroupsIndex.set(colName, sortGroupSummaries(groups)); + }); + + if (parsedImportStyles) { + STYLE_TYPES.forEach(st => { + const styles = parsedImportStyles[st.exportKey]; + if (!styles || styles.length === 0) return; + const counts = new Map(); + styles.forEach(s => { + const key = getGroupKeyUI((s && s.name) || ''); + counts.set(key, (counts.get(key) || 0) + 1); + }); + const arr = []; + counts.forEach((count, key) => { arr.push({ name: key, count: count }); }); + importStyleGroupsIndex[st.type] = sortGroupSummaries(arr); + }); + } + } + + // --- Targeted DOM sync utilities (mutations touch only affected checkboxes) --- + + function eachSimpleInput(listId, action, fn) { + const listEl = document.getElementById(listId); + if (!listEl) return; + const inputs = listEl.querySelectorAll('input[data-action="' + action + '"]'); + for (let i = 0; i < inputs.length; i++) fn(inputs[i]); + } + + function setSimpleGroupChecks(listId, action, attrName, attrValue, selectedSet) { + eachSimpleInput(listId, action, input => { + if (input.dataset[attrName] !== attrValue) return; + input.checked = !!(selectedSet && selectedSet.has(input.dataset.group)); + }); + } + + function setSimpleParentTriState(listId, action, attrName, attrValue, selectedSize, totalGroups) { + eachSimpleInput(listId, action, input => { + if (input.dataset[attrName] !== attrValue) return; + input.checked = totalGroups > 0 && selectedSize >= totalGroups; + input.indeterminate = selectedSize > 0 && selectedSize < totalGroups; + }); + } + + // --- Export mutations --- + + function setExportGroup(collection, groupKey, on) { + let set = selectedExportGroups.get(collection); + if (!set) { set = new Set(); selectedExportGroups.set(collection, set); } + if (on) set.add(groupKey); else set.delete(groupKey); + if (set.size === 0) selectedExportGroups.delete(collection); + setSimpleParentTriState('simple-export-variables-list', 'export-col-all', 'collection', collection, + set.size, getExportCollectionGroups(collection).length); + syncExportCollectionProjection(collection); + } + + function setExportCollectionAll(collection, on) { + setSelectedGroupsForCollection(collection, on); + const set = selectedExportGroups.get(collection); + setSimpleGroupChecks('simple-export-variables-list', 'export-group', 'collection', collection, set); + setSimpleParentTriState('simple-export-variables-list', 'export-col-all', 'collection', collection, + set ? set.size : 0, getExportCollectionGroups(collection).length); + syncExportCollectionProjection(collection); + } + + function setExportStyleGroup(type, groupKey, on) { + const set = selectedExportStyleGroups[type]; + if (!set) return; + if (on) set.add(groupKey); else set.delete(groupKey); + setSimpleParentTriState('simple-export-styles-list', 'export-style-all', 'type', type, + set.size, getExportStyleGroups(type).length); + syncAdvancedStyleCheckboxFromState(type); + } + + function setExportStyleTypeAll(type, on) { + const set = selectedExportStyleGroups[type]; + if (!set) return; + set.clear(); + if (on) getExportStyleGroups(type).forEach(g => set.add(g.name)); + setSimpleGroupChecks('simple-export-styles-list', 'export-style-group', 'type', type, set); + setSimpleParentTriState('simple-export-styles-list', 'export-style-all', 'type', type, + set.size, getExportStyleGroups(type).length); + syncAdvancedStyleCheckboxFromState(type); + } + + // Projection: selectedExportCollections mirrors "group Set non-empty" + function syncExportCollectionProjection(name) { + const set = selectedExportGroups.get(name); + if (set && set.size > 0) { + selectedExportCollections.add(name); + } else { + selectedExportCollections.delete(name); + } + const cb = document.getElementById('export-' + name); + if (cb) cb.checked = !!(set && set.size); + updateExportPreview(); + } + + function syncAdvancedStyleCheckboxFromState(type) { + const cb = document.getElementById('export-' + type + '-styles'); + if (cb) cb.checked = selectedExportStyleGroups[type].size > 0; + updateExportPreview(); + } + + // --- Import mutations --- + + function setImportGroup(collection, groupKey, on) { + let set = selectedImportGroups.get(collection); + if (!set) { set = new Set(); selectedImportGroups.set(collection, set); } + if (on) set.add(groupKey); else set.delete(groupKey); + if (set.size === 0) selectedImportGroups.delete(collection); + setSimpleParentTriState('simple-import-list', 'import-col-all', 'collection', collection, + set.size, getImportCollectionGroups(collection).length); + syncImportCollectionProjection(collection); + } + + function setImportCollectionAll(collection, on) { + setSelectedImportGroupsForCollection(collection, on); + const set = selectedImportGroups.get(collection); + setSimpleGroupChecks('simple-import-list', 'import-group', 'collection', collection, set); + setSimpleParentTriState('simple-import-list', 'import-col-all', 'collection', collection, + set ? set.size : 0, getImportCollectionGroups(collection).length); + syncImportCollectionProjection(collection); + } + + function setImportStyleGroup(type, groupKey, on) { + const set = selectedImportStyleGroups[type]; + if (!set) return; + if (on) set.add(groupKey); else set.delete(groupKey); + setSimpleParentTriState('simple-import-list', 'import-style-all', 'type', type, + set.size, getImportStyleGroups(type).length); + syncImportStyleProjection(type); + } + + function setImportStyleTypeAll(type, on) { + const set = selectedImportStyleGroups[type]; + if (!set) return; + set.clear(); + if (on) getImportStyleGroups(type).forEach(g => set.add(g.name)); + setSimpleGroupChecks('simple-import-list', 'import-style-group', 'type', type, set); + setSimpleParentTriState('simple-import-list', 'import-style-all', 'type', type, + set.size, getImportStyleGroups(type).length); + syncImportStyleProjection(type); + } + + function syncImportCollectionProjection(name) { + const set = selectedImportGroups.get(name); + if (set && set.size > 0) { + selectedImportCollections.add(name); + } else { + selectedImportCollections.delete(name); + } + // Mirror into the Advanced checkbox (indexed by position in parsedImportCollections) + for (let i = 0; i < parsedImportCollections.length; i++) { + if (Object.keys(parsedImportCollections[i])[0] === name) { + const cb = document.getElementById('import-col-' + i); + if (cb) cb.checked = !!(set && set.size); + break; + } + } + updateSimpleImportButtonState(); + } + + function syncImportStyleProjection(type) { + const cb = document.getElementById('import-' + type + '-styles-sel'); + if (cb) cb.checked = selectedImportStyleGroups[type].size > 0; + updateSimpleImportButtonState(); + } + + // --- Simple Import button state --- + + function updateSimpleImportButtonState() { + const btn = document.getElementById('simple-import-btn'); + if (!btn) return; + const hasData = !!(parsedImportCollections && parsedImportCollections.length > 0) || !!parsedImportStyles; + // Match the import action's real precondition: importVariables (and the + // runSimpleImport guard) require at least one selected variable + // collection, so style-group selections alone must not enable the button. + let hasSelection = false; + selectedImportGroups.forEach(set => { if (set.size > 0) hasSelection = true; }); + btn.disabled = !(hasData && hasSelection); + } + + function resetSimpleImportButton() { + const btn = document.getElementById('simple-import-btn'); + if (!btn) return; + btn.textContent = '📥 Import'; + updateSimpleImportButtonState(); + } + + // --- Write-through helpers (Advanced -> Simple state; no projection sync) --- + + function setSelectedGroupsForCollection(name, selected) { + if (selected) { + const groups = getExportCollectionGroups(name); + const keys = groups.length > 0 ? groups.map(g => g.name) : ['']; + selectedExportGroups.set(name, new Set(keys)); + } else { + selectedExportGroups.delete(name); + } + } + + function setSelectedImportGroupsForCollection(name, selected) { + if (selected) { + const groups = getImportCollectionGroups(name); + const keys = groups.length > 0 ? groups.map(g => g.name) : ['']; + selectedImportGroups.set(name, new Set(keys)); + } else { + selectedImportGroups.delete(name); + } + } + + // Read the four Advanced export style checkboxes -> all-or-none group state + function syncSimpleExportStyleGroupsFromAdvanced() { + STYLE_TYPES.forEach(st => { + const cb = document.getElementById('export-' + st.type + '-styles'); + const set = selectedExportStyleGroups[st.type]; + set.clear(); + if (cb && cb.checked) getExportStyleGroups(st.type).forEach(g => set.add(g.name)); + }); + renderSimpleExportStyles(); + } + + // Read the four Advanced import style checkboxes -> all-or-none group state + function syncSimpleImportStyleGroupsFromAdvanced() { + STYLE_TYPES.forEach(st => { + const cb = document.getElementById('import-' + st.type + '-styles-sel'); + const set = selectedImportStyleGroups[st.type]; + set.clear(); + if (cb && cb.checked) getImportStyleGroups(st.type).forEach(g => set.add(g.name)); + }); + renderSimpleImportContents(); + } + + // --- Delegated listeners (stage 3 calls attachSimpleModeListeners() from init) --- + + function makeSimpleExpandClickHandler(expandedSet) { + return function (event) { + const target = event.target; + if (!target || target.tagName === 'INPUT') return; + const row = target.closest ? target.closest('[data-action="toggle-expand"]') : null; + if (!row) return; + const key = row.dataset.expandKey; + const nowExpanded = !expandedSet.has(key); + if (nowExpanded) expandedSet.add(key); else expandedSet.delete(key); + const chevron = row.querySelector('.simple-chevron'); + if (chevron) chevron.textContent = nowExpanded ? '▾' : '▸'; + const rowsEl = row.nextElementSibling; + if (rowsEl && rowsEl.classList.contains('simple-group-rows')) { + if (nowExpanded) { + rowsEl.removeAttribute('hidden'); + } else { + rowsEl.setAttribute('hidden', ''); + } + } + }; + } + + function handleSimpleExportVarsChange(event) { + const input = event.target; + if (!input || input.tagName !== 'INPUT' || !input.dataset.action) return; + if (input.dataset.action === 'export-col-all') { + setExportCollectionAll(input.dataset.collection, input.checked); + } else if (input.dataset.action === 'export-group') { + setExportGroup(input.dataset.collection, input.dataset.group, input.checked); + } + } + + function handleSimpleExportStylesChange(event) { + const input = event.target; + if (!input || input.tagName !== 'INPUT' || !input.dataset.action) return; + if (input.dataset.action === 'export-style-all') { + setExportStyleTypeAll(input.dataset.type, input.checked); + } else if (input.dataset.action === 'export-style-group') { + setExportStyleGroup(input.dataset.type, input.dataset.group, input.checked); + } + } + + function handleSimpleImportChange(event) { + const input = event.target; + if (!input || input.tagName !== 'INPUT' || !input.dataset.action) return; + if (input.dataset.action === 'import-col-all') { + setImportCollectionAll(input.dataset.collection, input.checked); + } else if (input.dataset.action === 'import-group') { + setImportGroup(input.dataset.collection, input.dataset.group, input.checked); + } else if (input.dataset.action === 'import-style-all') { + setImportStyleTypeAll(input.dataset.type, input.checked); + } else if (input.dataset.action === 'import-style-group') { + setImportStyleGroup(input.dataset.type, input.dataset.group, input.checked); + } + } + + function attachSimpleModeListeners() { + const exportVarsList = document.getElementById('simple-export-variables-list'); + if (exportVarsList) { + exportVarsList.addEventListener('click', makeSimpleExpandClickHandler(expandedSimpleExport)); + exportVarsList.addEventListener('change', handleSimpleExportVarsChange); + } + const exportStylesList = document.getElementById('simple-export-styles-list'); + if (exportStylesList) { + exportStylesList.addEventListener('click', makeSimpleExpandClickHandler(expandedSimpleExport)); + exportStylesList.addEventListener('change', handleSimpleExportStylesChange); + } + const importList = document.getElementById('simple-import-list'); + if (importList) { + importList.addEventListener('click', makeSimpleExpandClickHandler(expandedSimpleImport)); + importList.addEventListener('change', handleSimpleImportChange); + } + // Write-through: Advanced export style checkboxes -> Simple group state. + // (Extra listener; the existing inline onchange="updateExportPreview()" stays untouched.) + STYLE_TYPES.forEach(st => { + const cb = document.getElementById('export-' + st.type + '-styles'); + if (cb) { + cb.addEventListener('change', syncSimpleExportStyleGroupsFromAdvanced); + } + }); + } + + // ========== SIMPLE MODE: PIPELINE WIRING (stage 3) ========== + // Data-arrival seeding, export/import actions, mode-toggle node moves, + // and init-time listener attachment for the Simple 3-column layout. + + // --- Data-arrival seeding (export side; called from the 'collections' message) --- + + // Mirrors the Advanced handler (renderExportCollections re-selects everything): + // every group of every collection starts selected. + function initSimpleExportSelectionFromCollections() { + selectedExportGroups = new Map(); + collections.forEach(c => setSelectedGroupsForCollection(c.name, true)); + } + + // Drop selected style-group keys that no longer exist in the refreshed data, + // then project the result into the Advanced style checkboxes so both faces + // agree. The 'collections' handler seeds the sets from the auto-checked + // Advanced checkboxes first (syncSimpleExportStyleGroupsFromAdvanced), so + // the pre-existing Advanced default (styles included when counts > 0) holds. + function reconcileSimpleStyleSelection() { + STYLE_TYPES.forEach(st => { + const valid = new Set(); + getExportStyleGroups(st.type).forEach(g => valid.add(g.name)); + const set = selectedExportStyleGroups[st.type]; + const keep = []; + set.forEach(name => { if (valid.has(name)) keep.push(name); }); + set.clear(); + keep.forEach(name => set.add(name)); + syncAdvancedStyleCheckboxFromState(st.type); + }); + } + + // --- Data-arrival seeding (import side; called after parseImportPreviewAsync succeeds) --- + + // Default after a successful parse: every collection/group + every style + // type/group selected. Also seeds the selectedImportCollections projection. + function initSimpleImportSelectionAll() { + selectedImportGroups = new Map(); + selectedImportCollections = new Set(); + importGroupsIndex.forEach((groups, name) => { + const keys = new Set(); + groups.forEach(g => keys.add(g.name)); + if (keys.size > 0) { + selectedImportGroups.set(name, keys); + selectedImportCollections.add(name); + } + }); + STYLE_TYPES.forEach(st => { + const set = selectedImportStyleGroups[st.type]; + set.clear(); + (importStyleGroupsIndex[st.type] || []).forEach(g => set.add(g.name)); + }); + } + + // Clear the Simple import selection state + index and repaint the list. + // Used by the parse failure path, resetImportState and clearImportInput. + function clearSimpleImportSelectionState() { + selectedImportGroups = new Map(); + importGroupsIndex = new Map(); + selectedImportStyleGroups = { color: new Set(), text: new Set(), effect: new Set(), grid: new Set() }; + importStyleGroupsIndex = { color: [], text: [], effect: [], grid: [] }; + renderSimpleImportContents(); + updateSimpleImportButtonState(); + } + + // Clear the Simple input mirror (textarea + loaded-file indicator). + // NOT called from the parse catch path: #import-input keeps its content + // there, so the mirror must keep it too. + function clearSimpleImportInputUi() { + const simpleInput = document.getElementById('simple-import-input'); + if (simpleInput) simpleInput.value = ''; + const fileLoaded = document.getElementById('simple-import-file-loaded'); + if (fileLoaded) fileLoaded.classList.add('hidden'); + } + + // --- Export action (Simple buttons share the one-shot pending-action latch) --- + + function setSimpleExportButtonsDisabled(disabled) { + const exportBtn = document.getElementById('simple-export-btn'); + if (exportBtn) exportBtn.disabled = disabled; + const copyBtn = document.getElementById('simple-copy-json-btn'); + if (copyBtn) copyBtn.disabled = disabled; + } + + function runSimpleExport(action) { + if (simpleExportPendingAction) return; + if (selectedExportCollections.size === 0) { + addLog('❌ Please select at least one variable collection', 'error'); + return; + } + simpleExportPendingAction = action; + setSimpleExportButtonsDisabled(true); + exportVariables(); + } + + // --- Import action --- + + function runSimpleImport() { + // Mirror importVariables' own guard BEFORE latching the button, so a + // styles-only selection can't leave the button stuck on 'Processing'. + if (!importData || selectedImportCollections.size === 0) { + addLog('❌ Please select at least one variable collection to import', 'error', 'import'); + return; + } + const btn = document.getElementById('simple-import-btn'); + if (btn) { + btn.disabled = true; + btn.textContent = '⏳ Processing...'; + } + importVariables(); + } + + // --- Export payload builders (Simple mode only; null = nothing partial) --- + + function buildSelectedGroupsPayload() { + const payload = {}; + let hasPartial = false; + selectedExportGroups.forEach((set, name) => { + if (!selectedExportCollections.has(name)) return; + const total = getExportCollectionGroups(name).length; + if (set.size > 0 && total > 0 && set.size < total) { + payload[name] = Array.from(set); + hasPartial = true; + } + }); + return hasPartial ? payload : null; + } + + function buildSelectedStyleGroupsPayload() { + const payload = {}; + let hasPartial = false; + STYLE_TYPES.forEach(st => { + const set = selectedExportStyleGroups[st.type]; + const total = getExportStyleGroups(st.type).length; + if (set.size > 0 && total > 0 && set.size < total) { + payload[st.type] = Array.from(set); + hasPartial = true; + } + }); + return hasPartial ? payload : null; + } + + // --- Mode toggle: shared node reparenting + state/DOM reconciliation --- + + // #log-area-export / #log-area-import / #import-undo-section are single DOM + // nodes shared by both layouts. Advanced original positions: + // export log -> before #export-json-preview-section (same .column-body) + // import log -> before #import-result-preview-section (same .column-body) + // undo section-> last child of that same .column-body + function moveSharedNodesForMode(toAdvanced) { + const logExport = document.getElementById('log-area-export'); + const logImport = document.getElementById('log-area-import'); + const undoSection = document.getElementById('import-undo-section'); + if (toAdvanced) { + const exportPreviewSection = document.getElementById('export-json-preview-section'); + if (logExport && exportPreviewSection && exportPreviewSection.parentNode) { + exportPreviewSection.parentNode.insertBefore(logExport, exportPreviewSection); + } + const importResultSection = document.getElementById('import-result-preview-section'); + if (logImport && importResultSection && importResultSection.parentNode) { + importResultSection.parentNode.insertBefore(logImport, importResultSection); + } + if (undoSection && importResultSection && importResultSection.parentNode) { + importResultSection.parentNode.appendChild(undoSection); + } + } else { + const exportSlot = document.getElementById('simple-export-log-slot'); + if (exportSlot && logExport) exportSlot.appendChild(logExport); + const importSlot = document.getElementById('simple-import-log-slot'); + if (importSlot && logImport) importSlot.appendChild(logImport); + const undoSlot = document.getElementById('simple-import-undo-slot'); + if (undoSlot && undoSection) undoSlot.appendChild(undoSection); + } + } + + // Advanced -> Simple: group state derives from the Advanced selections. + function rebuildSimpleStateFromAdvanced() { + // Export collections: selected in Advanced ? ALL groups : none + selectedExportGroups = new Map(); + collections.forEach(c => { + setSelectedGroupsForCollection(c.name, selectedExportCollections.has(c.name)); + }); + // Export styles: Advanced checkbox checked ? all groups : none + syncSimpleExportStyleGroupsFromAdvanced(); + // Import analogs only if the Advanced selection face was populated; + // otherwise keep the parse-time defaults. + const selectionFace = document.getElementById('import-selection-face'); + if (selectionFace && selectionFace.style.display !== 'none') { + selectedImportGroups = new Map(); + importGroupsIndex.forEach((groups, name) => { + setSelectedImportGroupsForCollection(name, selectedImportCollections.has(name)); + }); + syncSimpleImportStyleGroupsFromAdvanced(); + } + } + + // Simple -> Advanced: project the Simple state into the Advanced controls. + function syncAdvancedDomFromSimpleState() { + collections.forEach(c => { + const cb = document.getElementById('export-' + c.name); + if (cb) cb.checked = selectedExportCollections.has(c.name); + }); + STYLE_TYPES.forEach(st => syncAdvancedStyleCheckboxFromState(st.type)); + const selectionFace = document.getElementById('import-selection-face'); + if (selectionFace && selectionFace.style.display !== 'none') { + renderImportCollectionsList(); + } + } + + // --- Init-time wiring (called once, next to the other init calls) --- + + function wireSimpleSelectButton(action, handler) { + const btn = document.querySelector('button[data-action="' + action + '"]'); + if (btn) btn.addEventListener('click', handler); + } + + function initSimpleMode() { + attachSimpleModeListeners(); + + // Action buttons + const simpleExportActionBtn = document.getElementById('simple-export-btn'); + if (simpleExportActionBtn) simpleExportActionBtn.addEventListener('click', () => runSimpleExport('download')); + const simpleCopyJsonActionBtn = document.getElementById('simple-copy-json-btn'); + if (simpleCopyJsonActionBtn) simpleCopyJsonActionBtn.addEventListener('click', () => runSimpleExport('copy')); + const simpleImportActionBtn = document.getElementById('simple-import-btn'); + if (simpleImportActionBtn) simpleImportActionBtn.addEventListener('click', runSimpleImport); + + // Column-header actions + const simpleRefreshBtn = document.getElementById('simple-export-refresh-btn'); + if (simpleRefreshBtn) simpleRefreshBtn.addEventListener('click', refreshCollections); + const simpleExportCopyLogBtn = document.getElementById('simple-export-copylog-btn'); + if (simpleExportCopyLogBtn) simpleExportCopyLogBtn.addEventListener('click', () => copyLog('export')); + const simpleExportClearLogBtn = document.getElementById('simple-export-clearlog-btn'); + if (simpleExportClearLogBtn) simpleExportClearLogBtn.addEventListener('click', () => clearLog('export')); + const simpleImportCopyLogBtn = document.getElementById('simple-import-copylog-btn'); + if (simpleImportCopyLogBtn) simpleImportCopyLogBtn.addEventListener('click', () => copyLog('import')); + const simpleImportClearLogBtn = document.getElementById('simple-import-clearlog-btn'); + if (simpleImportClearLogBtn) simpleImportClearLogBtn.addEventListener('click', () => clearLog('import')); + + // Load JSON column + const simpleUploadBtn = document.getElementById('simple-import-upload-btn'); + if (simpleUploadBtn) { + simpleUploadBtn.addEventListener('click', () => { + const fi = document.getElementById('file-input'); + if (fi) fi.click(); + }); + } + const simpleImportClearInputBtn = document.getElementById('simple-import-clear-btn'); + if (simpleImportClearInputBtn) simpleImportClearInputBtn.addEventListener('click', clearImportInput); + const simpleImportInputEl = document.getElementById('simple-import-input'); + if (simpleImportInputEl) { + simpleImportInputEl.addEventListener('input', () => { + // Mirror into the Advanced textarea (the single parse source), then + // reuse the exact debounced parse entry the Advanced textarea uses. + const advancedInput = document.getElementById('import-input'); + if (advancedInput) advancedInput.value = simpleImportInputEl.value; + debouncedParseImportPreview(); + }); + } + + // Select All / None card actions + wireSimpleSelectButton('vars-select-all', () => collections.forEach(c => setExportCollectionAll(c.name, true))); + wireSimpleSelectButton('vars-select-none', () => collections.forEach(c => setExportCollectionAll(c.name, false))); + wireSimpleSelectButton('styles-select-all', () => STYLE_TYPES.forEach(st => setExportStyleTypeAll(st.type, true))); + wireSimpleSelectButton('styles-select-none', () => STYLE_TYPES.forEach(st => setExportStyleTypeAll(st.type, false))); + wireSimpleSelectButton('import-select-all', () => { + importGroupsIndex.forEach((groups, name) => setImportCollectionAll(name, true)); + STYLE_TYPES.forEach(st => setImportStyleTypeAll(st.type, true)); + }); + wireSimpleSelectButton('import-select-none', () => { + importGroupsIndex.forEach((groups, name) => setImportCollectionAll(name, false)); + STYLE_TYPES.forEach(st => setImportStyleTypeAll(st.type, false)); + }); + + // Body starts in Simple mode (no saved-prefs restore exists; only the + // radio change listener ever toggles .advanced-mode). + moveSharedNodesForMode(false); + + // Initial paint (empty states until data arrives) + renderSimpleExportVariables(); + renderSimpleExportStyles(); + renderSimpleImportContents(); + updateSimpleImportButtonState(); + } + // ========== INIT ========== function loadCollections() { @@ -8112,6 +9467,12 @@ if (totalHeader) { totalHeader.classList.add('hidden'); } + + // Simple mode: reset group selection + pending action (stage 3 wiring) + selectedExportGroups = new Map(); + STYLE_TYPES.forEach(st => selectedExportStyleGroups[st.type].clear()); + simpleExportPendingAction = null; + setSimpleExportButtonsDisabled(false); } // Reset import state - clears stale data from previous files @@ -8153,6 +9514,10 @@ const fontSection = document.getElementById('font-status-section'); if (libSection) libSection.classList.add('hidden'); if (fontSection) fontSection.classList.add('hidden'); + + // Simple mode: clear mirrored input + group selection (stage 3 wiring) + clearSimpleImportInputUi(); + clearSimpleImportSelectionState(); } // Switch between Order and Tree tabs in Preview column (Export) @@ -8386,6 +9751,9 @@ // Initialize column scroll fade for Import Preview panel initColumnScrollFade('import-preview-stats-scroll-content', 'import-preview-stats-fade-top', 'import-preview-stats-fade-bottom'); + // Initialize Simple-mode wiring (listeners, shared-node placement, first paint) + initSimpleMode(); + // ========== TIP MODAL ========== function openTipModal() { document.getElementById('tipModal').classList.add('visible'); From 79fc9c8fd62f0ed0237e492cb1777448d91ad69b Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 17:45:14 +0530 Subject: [PATCH 04/20] perf: chunked heavy-load handling with progress, cancel, and safe undo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The backend (Figma QuickJS VM) previously ran every heavy loop to completion with zero yielding — large design systems froze Figma during export/import. Now every heavy path is batched via three QuickJS-safe runners (runBatched / runBatchedAsync / runSequentialAsync) that yield to the host between batches, post throttled operation_progress messages (>=250ms, phase changes always), and check a cooperative cancel flag. - Progress UI: one component in all four hosts (Simple Section 3 + above the Advanced action buttons) — spinner, phase label, formatted counts, bar, Cancel. RAF-coalesced; only phase transitions reach the activity log. - Cancellation: 'cancel_operation' handled first in the dispatch, fully synchronous. Export cancel = clean abandon; import cancel = snapshot rollback ("file restored"); pre-mutation cancel = no changes; standalone clear cancel = partial counts + Cmd+Z hint. Rollback/undo-restore are non-cancellable (half-rollback = data loss) but still batched so the UI stays alive. Sentinel-property cancel errors (terser-safe). - Operation lock: one operation at a time (operation_denied otherwise); all action buttons in both modes disable while in flight. - VariableCache: split into rebuildLocal / ensureLibraryIndex (once per session) / clearLocal. An import now does exactly ONE local scan instead of up to 4 full rescans, and library indexing only runs when the payload actually references library tokens. - Snapshot unification: the UI no longer pre-sends create_undo_snapshot (double-snapshot freeze + hang risk gone); the backend snapshot rides back inside import_complete. - Undo order fix (critical): restoreFromSnapshot now parses and shape-validates the snapshot BEFORE clearing — a corrupt snapshot can no longer wipe the file. - figma.commitUndo() brackets imports and standalone clears, so each is one atomic native Cmd+Z step (Figma plugin guidelines). - Chunked export delivery: export JSON streams to the UI in 256KB export_chunk messages (surrogate-pair-safe splits) + export_done with count/length integrity checks; replaces the single unbounded postMessage. - UI perf: stringify worker routing now keyed on real payload size (node-count walk, not array length); worker timeout terminates the stale worker instead of double-parsing; file loads >100MB refused, >20MB confirmed. Verified in preview: progress rendering in both modes, cancel round-trip with rollback messaging, chunked reassembly with emoji (surrogate) payloads, lock/button recovery on all terminal paths. tsc clean; code.js rebuilt (tsc + terser). Co-Authored-By: Claude Fable 5 --- variables-styles-extractor/code.js | 2 +- variables-styles-extractor/src/code.ts | 1485 ++++++++++++++++++------ variables-styles-extractor/ui.html | 447 +++++-- 3 files changed, 1512 insertions(+), 422 deletions(-) diff --git a/variables-styles-extractor/code.js b/variables-styles-extractor/code.js index 4d99820..aafd1c2 100644 --- a/variables-styles-extractor/code.js +++ b/variables-styles-extractor/code.js @@ -8,4 +8,4 @@ * @version 2.0.0 * @author Tushar Kant Naik * @website https://tusharkantnaik.com - */figma.showUI(__html__,{width:1200,height:628,themeColors:!0,title:"☕️ Variables & Styles Extractor v2.0.0"});const Logger={log(e,t){console.log(`[Variables Extractor] ${e}`,t||""),figma.ui.postMessage({type:"log",message:e,data:t})},send(e,t){figma.ui.postMessage({type:e,data:t})}},PLAN_LIMITS={starter:{maxModesPerCollection:1,canPublishLibraries:!1,hasVariableRestApi:!1},professional:{maxModesPerCollection:10,canPublishLibraries:!0,hasVariableRestApi:!1},organization:{maxModesPerCollection:20,canPublishLibraries:!0,hasVariableRestApi:!1},enterprise:{maxModesPerCollection:1/0,canPublishLibraries:!0,hasVariableRestApi:!0}},MAX_VARIABLES_PER_COLLECTION=5e3;async function detectCurrentPlan(){const e=await figma.variables.getLocalVariableCollectionsAsync();let t,o=1;for(const t of e)t.modes.length>o&&(o=t.modes.length);return t=o>20?"enterprise":o>10?"organization":"professional",Object.assign({plan:t},PLAN_LIMITS[t])}async function validateImportAgainstPlan(e,t){const o=t?Object.assign({plan:t},PLAN_LIMITS[t]):await detectCurrentPlan(),a=await figma.variables.getLocalVariableCollectionsAsync(),s=a.reduce((e,t)=>Math.max(e,t.modes.length),0),r=(await figma.variables.getLocalVariablesAsync()).length,i=[];for(const t of e)"_styles"in t||i.push(t);let l=0,n=0;const c=[];for(const e of i){const t=Object.keys(e)[0],a=e[t];if(!a||!a.modes)continue;const s=Object.keys(a.modes).length;s>l&&(l=s),s>o.maxModesPerCollection&&c.push(`"${t}" (${s} modes, limit: ${o.maxModesPerCollection===1/0?"∞":o.maxModesPerCollection})`);const r=Object.values(a.modes)[0];r&&(n+=countNestedVariables(r))}const g=[],f=[];c.length;for(const e of i){const t=Object.keys(e)[0],o=e[t];if(!o||!o.modes)continue;const a=Object.values(o.modes)[0],s=a?countNestedVariables(a):0;s>5e3&&f.push(`Collection "${t}" has ${s} variables, exceeds limit of 5000`)}n>1e3&&g.push(`Large import: ${n} variables. This may take a moment.`),i.length>10&&g.push(`Importing ${i.length} collections. Consider importing in batches.`);const d=new Set;let m=0;for(const e of i){const t=e[Object.keys(e)[0]];if(t&&t.modes)for(const e of Object.keys(t.modes)){const o=flattenVariables(t.modes[e],"");for(const{value:e}of o)e.$libraryRef&&e.$collectionName&&(d.add(e.$collectionName),m++)}}const u=[];let y=0;for(const t of e)if("_styles"in t){const e=t._styles;if(e.textStyles)for(const t of e.textStyles){y++;const e=`${t.fontFamily}|${t.fontStyle}`;u.some(t=>`${t.family}|${t.style}`===e)||u.push({family:t.fontFamily,style:t.fontStyle})}}return Object.assign(Object.assign({currentPlan:o,existing:{collections:a.length,maxModesInAnyCollection:s,totalVariables:r},importing:{collections:i.length,maxModesInAnyCollection:l,totalVariables:n,collectionsExceedingModeLimit:c},warnings:g,errors:f,canImport:0===f.length},d.size>0&&{libraryDependencies:{variableCount:m,collections:Array.from(d)}}),u.length>0&&{fontDependencies:{styleCount:y,fonts:u}})}function countNestedVariables(e,t=0){for(const[,o]of Object.entries(e))o&&"object"==typeof o&&("$type"in o&&"$value"in o?t++:t=countNestedVariables(o,t));return t}const MathUtils={round2:e=>Math.round(100*e)/100,clamp:(e,t,o)=>Math.max(t,Math.min(o,e)),toHexByte:e=>Math.round(255*e).toString(16).padStart(2,"0"),fromHexByte:e=>parseInt(e,16)/255};function calculateHue(e,t,o,a,s){if(a===s)return 0;const r=a-s;let i=0;switch(a){case e:i=((t-o)/r+(t.5?e/(2-s-r):e/(s+r)}const n={h:calculateHue(t,o,a,s,r),s:Math.round(100*l),l:Math.round(100*i)},c=e.a;return void 0!==c&&c<1?Object.assign(Object.assign({},n),{a:MathUtils.round2(c)}):n},toHsb(e){const{r:t,g:o,b:a}=e,s=Math.max(t,o,a),r=Math.min(t,o,a),i=0===s?0:(s-r)/s,l={h:calculateHue(t,o,a,s,r),s:Math.round(100*i),b:Math.round(100*s)},n=e.a;return void 0!==n&&n<1?Object.assign(Object.assign({},l),{a:MathUtils.round2(n)}):l},toAllFormats(e){return{hex:this.toHex(e),rgb:this.toRgb255(e),css:this.toCss(e),hsl:this.toHsl(e),hsb:this.toHsb(e)}}},NamingConverter={convert(e,t){if("original"===t)return e;const o=e.replace(/([a-z])([A-Z])/g,"$1 $2").split(/[\s\/\-_]+/).filter(e=>e.length>0).map(e=>e.toLowerCase());if(0===o.length)return e;switch(t){case"camelCase":return o[0]+o.slice(1).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("");case"kebab-case":return o.join("-");case"snake_case":return o.join("_");default:return e}},convertPath(e,t){return"original"===t?e:e.split("/").map(e=>this.convert(e,t)).join("/")},convertCollectionName(e,t){return this.convert(e,t)},convertModeName(e,t){return this.convert(e,t)},addOriginalName(e,t){if("original"===t)return{converted:e};const o=this.convert(e,t);return o===e?{converted:e}:{converted:o,original:e}}};async function resolveAliasValue(e,t,o=10){if(o<=0)return Logger.log(`⚠️ Max alias resolution depth reached for ${e.name}`),"";let a=e.valuesByMode[t];if(void 0===a){const t=Object.keys(e.valuesByMode);t.length>0&&(a=e.valuesByMode[t[0]])}if(void 0===a)return"";if(isVariableAlias(a)){const e=await figma.variables.getVariableByIdAsync(a.id);return e?resolveAliasValue(e,t,o-1):""}return a}const W3C_TYPE_MAP={color:"color",float:"number",string:"string",boolean:"boolean"},W3CConverter={colorToW3C:e=>e.hex,typeToW3C:e=>W3C_TYPE_MAP[e]||"string",valueToW3C(e,t=!1){const o={$value:"",$type:this.typeToW3C(e.$type)};return t&&"string"==typeof e.$value&&e.$value.startsWith("{")?o.$value=e.$value:"color"===e.$type&&"object"==typeof e.$value?o.$value=e.$value.hex:o.$value=e.$value,e.$description&&(o.$description=e.$description),e.$scopes&&e.$scopes.length>0&&!e.$scopes.includes("ALL_SCOPES")&&(o.$extensions={"com.figma":{scopes:e.$scopes}}),o},collectionToW3C(e,t,o,a){const s={};a&&a!==e&&(s.$description=`Figma collection: ${a}`);const r=Object.keys(t);if(1===r.length)this.addTokensToGroup(s,t[r[0]],o);else for(const e of r){const a=NamingConverter.convertModeName(e,o);s[a]={},this.addTokensToGroup(s[a],t[e],o)}return s},addTokensToGroup(e,t,o){for(const[a,s]of Object.entries(t)){const t=NamingConverter.convert(a,o);if(isExportVariableValue(s)){const o="string"==typeof s.$value&&s.$value.startsWith("{");e[t]=this.valueToW3C(s,o)}else e[t]={},this.addTokensToGroup(e[t],s,o)}},parseW3CToken(e){var t,o;const a=this.w3cTypeToFigma(e.$type),s=(null===(o=null===(t=e.$extensions)||void 0===t?void 0:t["com.figma"])||void 0===o?void 0:o.scopes)||["ALL_SCOPES"];let r;if("color"===a&&"string"==typeof e.$value){const t=ColorParser.parse(e.$value);r=ColorConverter.toAllFormats(t)}else r="string"==typeof e.$value||"number"==typeof e.$value||"boolean"==typeof e.$value?e.$value:JSON.stringify(e.$value);return e.$description?{$type:a,$value:r,$scopes:s,$description:e.$description}:{$type:a,$value:r,$scopes:s}},w3cTypeToFigma:e=>({color:"color",number:"float",dimension:"float",string:"string",boolean:"boolean",fontFamily:"string",fontWeight:"float",duration:"string",cubicBezier:"string"}[e]||"string"),isW3CFormat(e){if("object"!=typeof e||null===e)return!1;const t=e;for(const e of Object.keys(t)){const o=t[e];if("object"==typeof o&&null!==o){if("$value"in o&&"$type"in o)return!0;for(const e of Object.keys(o)){const t=o[e];if("object"==typeof t&&null!==t&&"$value"in t)return!0}}}return Array.isArray(e),!1},w3cToFigmaFormat(e){const t=[];for(const[o,a]of Object.entries(e)){if(o.startsWith("$"))continue;const e={[o]:{modes:{Default:this.w3cGroupToNestedVars(a)}}};t.push(e)}return t},w3cGroupToNestedVars(e){const t={};for(const[o,a]of Object.entries(e))o.startsWith("$")||(this.isW3CToken(a)?t[o]=this.parseW3CToken(a):"object"==typeof a&&null!==a&&(t[o]=this.w3cGroupToNestedVars(a)));return t},isW3CToken:e=>"object"==typeof e&&null!==e&&"$value"in e},HEX_REGEX_8=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,HEX_REGEX_6=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,RGBA_REGEX=/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/i,HSLA_REGEX=/hsla?\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*(?:,\s*([\d.]+))?\s*\)/i,ColorParser={fromHex(e){const t=HEX_REGEX_8.exec(e);if(t)return{r:MathUtils.fromHexByte(t[1]),g:MathUtils.fromHexByte(t[2]),b:MathUtils.fromHexByte(t[3]),a:MathUtils.fromHexByte(t[4])};const o=HEX_REGEX_6.exec(e);return o?{r:MathUtils.fromHexByte(o[1]),g:MathUtils.fromHexByte(o[2]),b:MathUtils.fromHexByte(o[3]),a:1}:{r:0,g:0,b:0,a:1}},fromRgb255(e){var t;return{r:e.r/255,g:e.g/255,b:e.b/255,a:null!==(t=e.a)&&void 0!==t?t:1}},fromCss(e){const t=RGBA_REGEX.exec(e);if(t)return{r:parseInt(t[1],10)/255,g:parseInt(t[2],10)/255,b:parseInt(t[3],10)/255,a:void 0!==t[4]?parseFloat(t[4]):1};const o=HSLA_REGEX.exec(e);return o?this.fromHsl({h:parseInt(o[1],10),s:parseInt(o[2],10),l:parseInt(o[3],10),a:void 0!==o[4]?parseFloat(o[4]):1}):{r:0,g:0,b:0,a:1}},fromHsl(e){var t,o;const a=e.h/360,s=e.s/100,r=e.l/100;if(0===s)return{r:r,g:r,b:r,a:null!==(t=e.a)&&void 0!==t?t:1};const hue2rgb=(e,t,o)=>{const a=o<0?o+1:o>1?o-1:o;return a<1/6?e+6*(t-e)*a:a<.5?t:a<2/3?e+(t-e)*(2/3-a)*6:e},i=r<.5?r*(1+s):r+s-r*s,l=2*r-i;return{r:hue2rgb(l,i,a+1/3),g:hue2rgb(l,i,a),b:hue2rgb(l,i,a-1/3),a:null!==(o=e.a)&&void 0!==o?o:1}},fromHsb(e){var t;const o=e.h/360,a=e.s/100,s=e.b/100,r=Math.floor(6*o),i=6*o-r,l=s*(1-a),n=s*(1-i*a),c=s*(1-(1-i)*a),g=[[s,c,l],[n,s,l],[l,s,c],[l,n,s],[c,l,s],[s,l,n]],[f,d,m]=g[r%6];return{r:f,g:d,b:m,a:null!==(t=e.a)&&void 0!==t?t:1}},parse(e){var t;if("object"==typeof e&&null!==e&&"hex"in e&&"rgb"in e)return this.fromHex(e.hex);if("object"==typeof e&&null!==e&&"r"in e&&"g"in e&&"b"in e){const o=e;return o.r<=1&&o.g<=1&&o.b<=1?{r:o.r,g:o.g,b:o.b,a:null!==(t=o.a)&&void 0!==t?t:1}:this.fromRgb255(o)}return"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"l"in e?this.fromHsl(e):"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"b"in e?this.fromHsb(e):"string"==typeof e?e.startsWith("rgb")||e.startsWith("hsl")?this.fromCss(e):this.fromHex(e):{r:0,g:0,b:0,a:1}}};class VariableCache{constructor(){this.collectionMap=new Map,this.variableMap=new Map,this.libraryVariableMap=new Map,this.libraryCollectionNames=new Set,this.initialized=!1}async initialize(){this.initialized||(await this.rebuild(),this.initialized=!0)}async rebuild(){this.collectionMap.clear(),this.variableMap.clear(),this.libraryVariableMap.clear(),this.libraryCollectionNames.clear();for(const e of await figma.variables.getLocalVariableCollectionsAsync()){this.collectionMap.set(e.name,e);for(const t of e.variableIds){const o=await figma.variables.getVariableByIdAsync(t);o&&this.variableMap.set(`${e.name}/${o.name}`,o)}}await this.indexLibraryVariables()}async indexLibraryVariables(){try{const e=await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync();for(const t of e){this.libraryCollectionNames.add(t.name);try{const e=await figma.teamLibrary.getVariablesInLibraryCollectionAsync(t.key);for(const o of e)try{const e=await figma.variables.importVariableByKeyAsync(o.key);e&&this.libraryVariableMap.set(`${t.name}/${e.name}`,e)}catch(e){}}catch(e){Logger.log(` ⚠️ Could not index library collection "${t.name}": ${e}`)}}this.libraryCollectionNames.size>0&&Logger.log(`📚 Indexed ${this.libraryVariableMap.size} library variables from ${this.libraryCollectionNames.size} connected libraries`)}catch(e){Logger.log(`⚠️ Could not access team library: ${e}`)}}getCollection(e){return this.collectionMap.get(e)}getVariable(e){return this.variableMap.get(e)||this.libraryVariableMap.get(e)}setVariable(e,t){this.variableMap.set(e,t)}setCollection(e,t){this.collectionMap.set(e,t)}removeCollection(e){this.collectionMap.delete(e);const t=[];for(const o of this.variableMap.keys())o.startsWith(`${e}/`)&&t.push(o);for(const e of t)this.variableMap.delete(e)}isCollectionAvailable(e){return this.collectionMap.has(e)||this.libraryCollectionNames.has(e)}getLibraryCollectionNames(){return Array.from(this.libraryCollectionNames)}get size(){return this.variableMap.size}get collections(){return this.collectionMap.values()}getVariableKeys(){return Array.from(this.variableMap.keys())}}const variableCache=new VariableCache;function isExportVariableValue(e){return"object"==typeof e&&null!==e&&"$type"in e}function isVariableAlias(e){return"object"==typeof e&&null!==e&&"VARIABLE_ALIAS"===e.type}const TypeMapper={toExportType(e){var t;return null!==(t={COLOR:"color",FLOAT:"float",STRING:"string",BOOLEAN:"boolean"}[e])&&void 0!==t?t:"string"},toFigmaType(e){var t;return null!==(t={color:"COLOR",float:"FLOAT",string:"STRING",boolean:"BOOLEAN"}[e])&&void 0!==t?t:"STRING"},scopesToArray:e=>0===e.length||e.includes("ALL_SCOPES")?["ALL_SCOPES"]:[...e],arrayToScopes:e=>e.includes("ALL_SCOPES")?["ALL_SCOPES"]:e};async function getVariableBindingInfo(e,t){if(!(null==e?void 0:e[t]))return{};const o=e[t];if(!o)return{};const a=await figma.variables.getVariableByIdAsync(o.id);if(!a)return{id:o.id};const s=await figma.variables.getVariableCollectionByIdAsync(a.variableCollectionId);return{id:o.id,name:a.name,collection:null==s?void 0:s.name}}async function extractBindings(e,t){if(!e)return;const o={};for(const a of t){const t=await getVariableBindingInfo(e,a);t.name&&(o[a]=t)}return Object.keys(o).length>0?o:void 0}function flattenVariables(e,t){const o=[];for(const a of Object.keys(e)){const s=e[a],r=t?`${t}/${a}`:a;isExportVariableValue(s)?o.push({path:r,value:s}):o.push(...flattenVariables(s,r))}return o}function getValueAtPath(e,t){const o=t.split("/");let a=e;for(const e of o){if("object"!=typeof a||null===a)return null;if(isExportVariableValue(a))return null;a=a[e]}return isExportVariableValue(a)?a:null}const ColorStyleProcessor={async export(e){var t,o,a,s;const r=null!==(t=null==e?void 0:e.includeImages)&&void 0!==t&&t,i=[];for(const e of await figma.getLocalPaintStylesAsync()){if(0===e.paints.length)continue;const t=[];let l,n,c;for(const i of e.paints)if("SOLID"===i.type){const e=i.color;let a=null!==(o=i.opacity)&&void 0!==o?o:1;void 0!==e.a&&e.a<1&&1===a&&(a=e.a);const s={r:i.color.r,g:i.color.g,b:i.color.b,a:a},r={type:"SOLID",color:ColorConverter.toAllFormats(s),opacity:MathUtils.round2(a)};t.push(r),l||(l=r.color,n=r.opacity,c=await extractBindings(i.boundVariables,["color"]))}else if("GRADIENT_LINEAR"===i.type||"GRADIENT_RADIAL"===i.type||"GRADIENT_ANGULAR"===i.type||"GRADIENT_DIAMOND"===i.type){const e=i.gradientStops.map(e=>{var t;return{position:MathUtils.round2(e.position),color:ColorConverter.toAllFormats({r:e.color.r,g:e.color.g,b:e.color.b,a:null!==(t=e.color.a)&&void 0!==t?t:1})}}),o=Object.assign(Object.assign({type:i.type,gradientStops:e},i.gradientTransform&&{gradientTransform:i.gradientTransform}),{opacity:MathUtils.round2(null!==(a=i.opacity)&&void 0!==a?a:1)});t.push(o)}else if("IMAGE"===i.type){const o=Object.assign(Object.assign(Object.assign(Object.assign({type:"IMAGE",scaleMode:i.scaleMode},i.imageHash&&{imageHash:i.imageHash}),{opacity:MathUtils.round2(null!==(s=i.opacity)&&void 0!==s?s:1)}),void 0!==i.rotation&&{rotation:i.rotation}),i.filters&&{filters:Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},void 0!==i.filters.exposure&&{exposure:i.filters.exposure}),void 0!==i.filters.contrast&&{contrast:i.filters.contrast}),void 0!==i.filters.saturation&&{saturation:i.filters.saturation}),void 0!==i.filters.temperature&&{temperature:i.filters.temperature}),void 0!==i.filters.tint&&{tint:i.filters.tint}),void 0!==i.filters.highlights&&{highlights:i.filters.highlights}),void 0!==i.filters.shadows&&{shadows:i.filters.shadows})});if(r&&i.imageHash)try{const e=figma.getImageByHash(i.imageHash);if(e){const t=await e.getBytesAsync();if(t){const e=figma.base64Encode(t);o.imageBase64=e}}}catch(t){Logger.log(`⚠️ Could not export image data for style "${e.name}": ${t}`)}t.push(o)}if(0===t.length)continue;const g=Object.assign(Object.assign(Object.assign(Object.assign({name:e.name,paints:t},l&&{color:l}),void 0!==n&&{opacity:n}),e.description&&{description:e.description}),c&&Object.keys(c).length>0&&{boundVariables:c});i.push(g)}return i},async importStyles(e,t){var o,a,s,r;let i=0,l=0;const n=new Map;for(const e of await figma.getLocalPaintStylesAsync())n.set(e.name,e);for(const c of e){let e;n.has(c.name)?(e=n.get(c.name),l++):(e=figma.createPaintStyle(),e.name=c.name,i++),c.description&&(e.description=c.description);const g=[];if(c.paints&&c.paints.length>0){for(const e of c.paints)if("SOLID"===e.type){const a=ColorParser.parse(e.color);let s=null!==(o=e.opacity)&&void 0!==o?o:1;a.a<1&&void 0===e.opacity&&(s=MathUtils.round2(a.a));let r={type:"SOLID",color:{r:a.r,g:a.g,b:a.b},opacity:MathUtils.round2(s)};if(c.boundVariables&&0===g.length)for(const[e,o]of Object.entries(c.boundVariables))if(o.name&&o.collection){const a=t.getVariable(`${o.collection}/${o.name}`);if(a)try{r=figma.variables.setBoundVariableForPaint(r,e,a)}catch(t){Logger.log(`⚠️ Could not bind ${e}: ${t}`)}}g.push(r)}else if("GRADIENT_LINEAR"===e.type||"GRADIENT_RADIAL"===e.type||"GRADIENT_ANGULAR"===e.type||"GRADIENT_DIAMOND"===e.type){const t=e.gradientStops.map(e=>{const t=ColorParser.parse(e.color);return{position:e.position,color:{r:t.r,g:t.g,b:t.b,a:t.a}}}),o=e.gradientTransform?[[e.gradientTransform[0][0],e.gradientTransform[0][1],e.gradientTransform[0][2]],[e.gradientTransform[1][0],e.gradientTransform[1][1],e.gradientTransform[1][2]]]:[[1,0,0],[0,1,0]],s={type:e.type,gradientStops:t,gradientTransform:o,opacity:null!==(a=e.opacity)&&void 0!==a?a:1};g.push(s)}else if("IMAGE"===e.type){let t=null;if(e.imageBase64)try{const o=figma.base64Decode(e.imageBase64);t=figma.createImage(o).hash,Logger.log(`✅ Created image from base64 data for style "${c.name}"`)}catch(e){Logger.log(`⚠️ Could not import image from base64 for style "${c.name}": ${e}`)}if(!t&&e.imageHash){figma.getImageByHash(e.imageHash)?(t=e.imageHash,Logger.log(`✅ Found existing image with hash for style "${c.name}"`)):Logger.log(`⚠️ Image hash not found in file for style "${c.name}", skipping image paint (imageHash cannot be null)`)}if(t){const o=Object.assign(Object.assign({type:"IMAGE",scaleMode:e.scaleMode,imageHash:t,opacity:null!==(s=e.opacity)&&void 0!==s?s:1},void 0!==e.rotation&&{rotation:e.rotation}),e.filters&&{filters:e.filters});g.push(o)}}}else if(c.color){const e=ColorParser.parse(c.color);let o=null!==(r=c.opacity)&&void 0!==r?r:1;e.a<1&&void 0===c.opacity&&(o=MathUtils.round2(e.a));let a={type:"SOLID",color:{r:e.r,g:e.g,b:e.b},opacity:MathUtils.round2(o)};if(c.boundVariables)for(const[e,o]of Object.entries(c.boundVariables))if(o.name&&o.collection){const s=t.getVariable(`${o.collection}/${o.name}`);if(s)try{a=figma.variables.setBoundVariableForPaint(a,e,s)}catch(t){Logger.log(`⚠️ Could not bind ${e}: ${t}`)}}g.push(a)}g.length>0&&(e.paints=g)}return{created:i,updated:l}}},TextStyleProcessor={async export(e){const t=[];for(const e of await figma.getLocalTextStylesAsync()){const o=Object.assign(Object.assign({name:e.name,fontFamily:e.fontName.family,fontStyle:e.fontName.style,fontSize:e.fontSize,lineHeight:e.lineHeight,letterSpacing:e.letterSpacing,textCase:e.textCase,textDecoration:e.textDecoration},e.description&&{description:e.description}),{boundVariables:await extractBindings(e.boundVariables,["fontSize","lineHeight","letterSpacing","paragraphSpacing","paragraphIndent"])});t.push(o)}return t},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalTextStylesAsync())s.set(e.name,e);for(const r of e){let e;s.has(r.name)?(e=s.get(r.name),a++):(e=figma.createTextStyle(),e.name=r.name,o++),r.description&&(e.description=r.description);try{if(await figma.loadFontAsync({family:r.fontFamily,style:r.fontStyle}),e.fontName={family:r.fontFamily,style:r.fontStyle},e.fontSize=r.fontSize,e.lineHeight=r.lineHeight,e.letterSpacing=r.letterSpacing,r.textCase&&(e.textCase=r.textCase),r.textDecoration&&(e.textDecoration=r.textDecoration),r.boundVariables)for(const[o,a]of Object.entries(r.boundVariables))if(a.name&&a.collection){const s=t.getVariable(`${a.collection}/${a.name}`);if(s)try{e.setBoundVariable(o,s)}catch(e){}}}catch(e){Logger.log(`⚠️ Could not load font for ${r.name}: ${e}`)}}return{created:o,updated:a}}},EffectStyleProcessor={async export(e){const t=[];for(const e of await figma.getLocalEffectStylesAsync()){const o=[];for(const t of e.effects){const e=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:t.type,visible:t.visible},"radius"in t&&{radius:t.radius}),"spread"in t&&{spread:t.spread}),"offset"in t&&{offset:t.offset}),"color"in t&&{color:ColorConverter.toAllFormats(t.color)}),"blendMode"in t&&{blendMode:t.blendMode}),"showShadowBehindNode"in t&&{showShadowBehindNode:t.showShadowBehindNode}),{boundVariables:await extractBindings(t.boundVariables,["color","radius","spread","offsetX","offsetY"])});o.push(e)}const a=Object.assign(Object.assign({name:e.name},e.description&&{description:e.description}),{effects:o});t.push(a)}return t},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalEffectStylesAsync())s.set(e.name,e);for(const r of e){let e;s.has(r.name)?(e=s.get(r.name),a++):(e=figma.createEffectStyle(),e.name=r.name,o++),r.description&&(e.description=r.description);const i=r.effects.map(e=>{var t;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:e.type,visible:null===(t=e.visible)||void 0===t||t},void 0!==e.radius&&{radius:e.radius}),void 0!==e.spread&&{spread:e.spread}),void 0!==e.offset&&{offset:e.offset}),void 0!==e.color&&{color:(()=>{const t=ColorParser.parse(e.color);return{r:t.r,g:t.g,b:t.b,a:MathUtils.round2(t.a)}})()}),void 0!==e.blendMode&&{blendMode:e.blendMode}),void 0!==e.showShadowBehindNode&&{showShadowBehindNode:e.showShadowBehindNode})});e.effects=i;for(let o=0;o{var t,o,a,s,r,i,l;const n=e.color?ColorParser.parse(e.color):{r:1,g:0,b:0,a:.1},c={r:n.r,g:n.g,b:n.b,a:MathUtils.round2(n.a)};if("GRID"===e.pattern)return{pattern:"GRID",sectionSize:null!==(t=e.sectionSize)&&void 0!==t?t:10,visible:!1!==e.visible,color:c};const g=null!==(o=e.alignment)&&void 0!==o?o:"STRETCH",f={pattern:e.pattern,gutterSize:null!==(a=e.gutterSize)&&void 0!==a?a:10,count:null!==(s=e.count)&&void 0!==s?s:5,visible:!1!==e.visible,color:c};if("STRETCH"===g)return Object.assign(Object.assign({},f),{alignment:"STRETCH",offset:null!==(r=e.offset)&&void 0!==r?r:0});if("CENTER"===g)return Object.assign(Object.assign({},f),{alignment:"CENTER",sectionSize:null!==(i=e.sectionSize)&&void 0!==i?i:100});{const t=Object.assign(Object.assign({},f),{alignment:g,offset:null!==(l=e.offset)&&void 0!==l?l:0});return void 0!==e.sectionSize&&(t.sectionSize=e.sectionSize),t}});e.layoutGrids=i;for(let o=0;ot.name===e);a?await checkVariablesDiff(l,a.modeId,o,i,"",t):n=!0}t.modifiedVariables.some(e=>e.collection===i)||t.newVariables.some(e=>e.collection===i)?(t.modifiedCollections.push(i),t.summary.collectionsModified++):(t.unchangedCollections.push(i),t.summary.collectionsUnchanged++)}return t}function countVariablesInCollection(e){let t=0;const o=Object.values(e)[0];return o&&(t=countVarsInNestedObj(o)),t}function countVarsInNestedObj(e){let t=0;for(const o of Object.values(e))isExportVariableValue(o)?t++:t+=countVarsInNestedObj(o);return t}async function checkVariablesDiff(e,t,o,a,s,r){for(const[i,l]of Object.entries(o)){const o=s?`${s}/${i}`:i;if(isExportVariableValue(l)){const e=variableCache.getVariable(`${a}/${o}`);if(e){const s=e.valuesByMode[t],i=l.$value;valuesAreDifferent(s,i)?(r.modifiedVariables.push({collection:a,path:o,oldValue:formatValueForDisplay(s),newValue:formatValueForDisplay(i)}),r.summary.variablesModified++):(r.unchangedVariables++,r.summary.variablesUnchanged++)}else r.newVariables.push({collection:a,path:o}),r.summary.variablesNew++}else await checkVariablesDiff(e,t,l,a,o,r)}}function valuesAreDifferent(e,t){if(void 0===e)return!0;if(isVariableAlias(e))return"string"==typeof t&&t.startsWith("{"),!0;if("object"==typeof e&&null!==e&&"r"in e){if("object"==typeof t&&null!==t&&"hex"in t){return ColorConverter.toAllFormats(e).hex.toLowerCase()!==t.hex.toLowerCase()}return!0}return e!==t}function formatValueForDisplay(e){if(void 0===e)return"undefined";if("object"==typeof e&&null!==e){if("hex"in e)return e.hex;if("r"in e)return ColorConverter.toAllFormats(e).hex;if("id"in e)return"{alias}"}return String(e)}async function computeStylesDiff(e,t){if(e.colorStyles){const o=await figma.getLocalPaintStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.colorStyles)a.has(o.name)?(t.modifiedStyles.push({type:"color",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"color",name:o.name}),t.summary.stylesNew++)}if(e.textStyles){const o=await figma.getLocalTextStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.textStyles)a.has(o.name)?(t.modifiedStyles.push({type:"text",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"text",name:o.name}),t.summary.stylesNew++)}if(e.effectStyles){const o=await figma.getLocalEffectStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.effectStyles)a.has(o.name)?(t.modifiedStyles.push({type:"effect",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"effect",name:o.name}),t.summary.stylesNew++)}if(e.gridStyles){const o=await figma.getLocalGridStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.gridStyles)a.has(o.name)?(t.modifiedStyles.push({type:"grid",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"grid",name:o.name}),t.summary.stylesNew++)}}function filterStylesByGroup(e,t){return t?e.filter(function(e){return-1!==t.indexOf(getGroupKey(e.name))}):e}async function exportVariables(e,t,o,a,s="original",r="figma",i,l=!1,n,c){var g,f,d,m,u,y,p,b,h,v;Logger.log("📤 Starting export..."),Logger.log(` preserveLibraryRefs: ${o}`),Logger.log(` includeImages: ${a}`),Logger.log(` namingConvention: ${s}`),Logger.log(` exportFormat: ${r}`),Logger.log(` resolveAliases: ${l}`),i&&Logger.log(` selectedModes: ${JSON.stringify(i)}`);try{let o=await figma.variables.getLocalVariableCollectionsAsync();(null==e?void 0:e.length)&&(o=o.filter(t=>e.includes(t.name)),Logger.log(`Filtering to ${o.length} selected collections`));const C=[],S={};let $=0;for(const e of o){Logger.log(`Processing collection: ${e.name}`);let t=e.modes;if(i&&i[e.name]){const o=i[e.name];t=e.modes.filter(e=>o.includes(e.name)),Logger.log(` Filtering to ${t.length} modes: ${t.map(e=>e.name).join(", ")}`)}const o=NamingConverter.convertCollectionName(e.name,s),a={[o]:Object.assign({modes:{}},o!==e.name&&{$originalName:e.name})};for(const e of t){const t=NamingConverter.convertModeName(e.name,s);a[o].modes[t]={}}for(const r of e.variableIds){const i=await figma.variables.getVariableByIdAsync(r);if(!i)continue;if(n&&n[e.name]&&-1===n[e.name].indexOf(getGroupKey(i.name)))continue;$++;const c=i.name.split("/").map(e=>NamingConverter.convert(e,s));for(const e of t){const t=NamingConverter.convertModeName(e.name,s),r=a[o].modes[t],n=i.valuesByMode[e.modeId];let d=r;for(let e=0;eNamingConverter.convert(e,s)).join(".")}}`,u=v,h){const e=t.valuesByMode[Object.keys(t.valuesByMode)[0]];"object"==typeof e&&null!==e&&"r"in e?y=ColorConverter.toAllFormats(e):isVariableAlias(e)||(y=e)}}}else u=""}else u="object"==typeof n&&null!==n&&"r"in n?ColorConverter.toAllFormats(n):n;const C=Object.assign(Object.assign(Object.assign({$scopes:TypeMapper.scopesToArray(i.scopes),$type:TypeMapper.toExportType(i.resolvedType),$value:u},i.description&&{$description:i.description}),p&&b&&{$collectionName:b}),p&&h&&Object.assign({$libraryRef:v},void 0!==y&&{$localValue:y}));d[m]=C}}C.push(a),"w3c"===r&&(S[o]=W3CConverter.collectionToW3C(o,a[o].modes,s,a[o].$originalName))}let L=null;if(t){if(L={},t.colorStyles){const e=c?c.color:void 0;L.colorStyles=filterStylesByGroup(await ColorStyleProcessor.export({includeImages:a}),e),e&&0===L.colorStyles.length&&delete L.colorStyles}if(t.textStyles){const e=c?c.text:void 0;L.textStyles=filterStylesByGroup(await TextStyleProcessor.export(),e),e&&0===L.textStyles.length&&delete L.textStyles}if(t.effectStyles){const e=c?c.effect:void 0;L.effectStyles=filterStylesByGroup(await EffectStyleProcessor.export(),e),e&&0===L.effectStyles.length&&delete L.effectStyles}if(t.gridStyles){const e=c?c.grid:void 0;L.gridStyles=filterStylesByGroup(await GridStyleProcessor.export(),e),e&&0===L.gridStyles.length&&delete L.gridStyles}Object.keys(L).length>0?C.push({_styles:L}):L=null}const A={collections:o.length,variables:$,styles:L?{color:null!==(m=null===(d=L.colorStyles)||void 0===d?void 0:d.length)&&void 0!==m?m:0,text:null!==(y=null===(u=L.textStyles)||void 0===u?void 0:u.length)&&void 0!==y?y:0,effect:null!==(b=null===(p=L.effectStyles)||void 0===p?void 0:p.length)&&void 0!==b?b:0,grid:null!==(v=null===(h=L.gridStyles)||void 0===h?void 0:h.length)&&void 0!==v?v:0}:null};let w;"w3c"===r?(L&&Object.keys(L).length>0&&(S.$extensions={"com.figma":{styles:L}}),w=JSON.stringify(S,null,2),Logger.log(`✅ Export complete (W3C format): ${A.collections} collections, ${A.variables} variables`)):(w=JSON.stringify(C,null,2),Logger.log(`✅ Export complete: ${A.collections} collections, ${A.variables} variables`)),Logger.send("export_complete",{data:w,stats:A,format:r})}catch(e){Logger.log(`❌ Export error: ${e}`),Logger.send("error",{message:`Export failed: ${e}`})}}async function importVariables(e,t){var o,a;Logger.log("📥 Starting import..."),Logger.log(`📋 Import options: merge=${t.merge}, clearFirst=${t.clearFirst}, importStyles=${t.importStyles}`),Logger.log("📸 Creating pre-import snapshot for automatic rollback...");let s=null;try{s=await createUndoSnapshot(),Logger.log("✅ Pre-import snapshot created")}catch(e){Logger.log(`⚠️ Could not create pre-import snapshot: ${e}`)}try{let s,r=JSON.parse(e),i="figma";if(!Array.isArray(r)&&W3CConverter.isW3CFormat(r)){Logger.log("📄 Detected W3C Design Tokens format, converting..."),i="w3c";const e=r;let t=null;if(e.$extensions&&e.$extensions["com.figma"]){const o=e.$extensions["com.figma"];o.styles&&(t=o.styles),delete e.$extensions}s=W3CConverter.w3cToFigmaFormat(e),t&&s.push({_styles:t})}else s=r;if(t.clearFirst&&(Logger.log("🧹 Clean Import: Clearing existing variables and styles..."),await clearAll(),Logger.log("✅ Clean Import: Clearing complete, rebuilding cache..."),await variableCache.rebuild(),Logger.log("✅ Clean Import: Cache rebuilt, proceeding with import...")),t.customMerge){const{clearVariables:e,clearStyles:o}=t.customMerge;e&&o?(Logger.log("🎯 Custom Merge: Clearing both variables and styles..."),await clearAll()):e?(Logger.log("🎯 Custom Merge: Clearing variables only..."),await clearVariables()):o&&(Logger.log("🎯 Custom Merge: Clearing styles only..."),await clearStyles()),Logger.log("✅ Custom Merge: Clearing complete, rebuilding cache..."),await variableCache.rebuild()}await variableCache.initialize();let l=0,n=0,c=0,g=0,f=0,d=0,m=null;const u=[];for(const e of s){const t=Object.keys(e);1===t.length&&"_styles"===t[0]?m=e._styles:u.push(e)}const y=[];Logger.log(`📥 Pass 1: Processing ${u.length} collections...`);for(const e of u){const s=Object.keys(e)[0],r=e[s],i=r.$originalName||s;Logger.log(`Processing collection: ${s}${r.$originalName?` (original: ${i})`:""}`);const f=(null===(o=t.collectionBehaviors)||void 0===o?void 0:o[s])||(null===(a=t.collectionBehaviors)||void 0===a?void 0:a[i])||"merge";let d;const m=variableCache.getCollection(i);if(m)if("replace"===f){Logger.log(` Replacing collection: ${i}`);try{m.remove(),variableCache.removeCollection(i),d=figma.variables.createVariableCollection(i),variableCache.setCollection(i,d),l++,Logger.log(" Created fresh collection (replaced)")}catch(e){Logger.log(` ⚠️ Could not replace collection: ${e}`);continue}}else{if(!t.merge){Logger.log(` Skipping existing collection: ${i}`);continue}d=m,Logger.log(" Merging into existing collection")}else d=figma.variables.createVariableCollection(i),variableCache.setCollection(i,d),l++,Logger.log(" Created new collection");const u=Object.keys(r.modes),p=new Map;for(const e of d.modes)p.set(e.name,e.modeId);1!==d.modes.length||p.has(u[0])||(d.renameMode(d.modes[0].modeId,u[0]),p.set(u[0],d.modes[0].modeId));for(const e of u)if(!p.has(e))try{const t=d.addMode(e);p.set(e,t)}catch(t){Logger.log(` ⚠️ Could not create mode ${e}: ${t}`)}const b=flattenVariables(r.modes[u[0]],""),h=[];Logger.log(" Pass 1: Creating variables with raw values...");for(const{path:e,value:o}of b){const a=`${i}/${e}`;let s;const l=variableCache.getVariable(a);if(l){if(!t.overwrite){g++;continue}s=l,c++}else try{s=figma.variables.createVariable(e,d,TypeMapper.toFigmaType(o.$type)),n++}catch(t){Logger.log(` ⚠️ Could not create variable ${e}: ${t}`);continue}o.$description&&(s.description=o.$description);try{s.scopes=TypeMapper.arrayToScopes(o.$scopes)}catch(e){}for(const t of u){const o=p.get(t);if(!o)continue;const a=getValueAtPath(r.modes[t],e);if(a)if("string"==typeof a.$value&&a.$value.startsWith("{")){const e=a.$value.slice(1,-1).replace(/\./g,"/"),t=a.$collectionName||i;h.push({variable:s,modeId:o,aliasPath:e,aliasCollection:t,fallbackValue:a}),setRawValue(s,o,a)}else setRawValue(s,o,a)}variableCache.setVariable(a,s)}y.push(...h)}Logger.log(`📥 Pass 2: Resolving ${y.length} alias references...`),await variableCache.rebuild();let p=0,b=0;for(const e of y){const t=variableCache.getVariable(`${e.aliasCollection}/${e.aliasPath}`);if(t)try{e.variable.setValueForMode(e.modeId,{type:"VARIABLE_ALIAS",id:t.id}),p++}catch(t){b++,Logger.log(` ⚠️ Could not set alias for ${e.variable.name}: ${t}`)}else b++,Logger.log(` ⚠️ Alias target not found: ${e.aliasCollection}/${e.aliasPath}`)}if(y.length>0&&Logger.log(` ✅ Aliases: ${p} resolved, ${b} used fallback values`),m&&t.importStyles){if(Logger.log("📦 Importing styles..."),await variableCache.rebuild(),m.colorStyles){const e=await ColorStyleProcessor.importStyles(m.colorStyles,variableCache);f+=e.created,d+=e.updated}if(m.textStyles){const e=await TextStyleProcessor.importStyles(m.textStyles,variableCache);f+=e.created,d+=e.updated}if(m.effectStyles){const e=await EffectStyleProcessor.importStyles(m.effectStyles,variableCache);f+=e.created,d+=e.updated}if(m.gridStyles){const e=await GridStyleProcessor.importStyles(m.gridStyles,variableCache);f+=e.created,d+=e.updated}}const h={collectionsCreated:l,variablesCreated:n,variablesUpdated:c,variablesSkipped:g,stylesCreated:f,stylesUpdated:d};Logger.log("✅ Import complete!"),Logger.send("import_complete",{stats:h})}catch(e){const t=e instanceof Error?e.message:String(e);if(Logger.log(`❌ Import error: ${t}`),s){Logger.log("🔄 Attempting automatic rollback to pre-import state..."),Logger.send("import_rolling_back",{error:t});try{await restoreFromSnapshot(s),Logger.log("✅ Automatic rollback successful - file restored to pre-import state"),Logger.send("import_rollback_complete",{error:t,message:"Import failed but your file has been automatically restored to its previous state."})}catch(e){const o=e instanceof Error?e.message:String(e);Logger.log(`❌ Rollback failed: ${o}`),Logger.send("import_rollback_failed",{error:t,rollbackError:o,message:"Import failed and automatic rollback also failed. Please use Ctrl+Z (Cmd+Z) to undo manually."})}}else Logger.send("error",{message:`Import failed: ${t}. Use Ctrl+Z (Cmd+Z) to undo changes.`})}}function setRawValue(e,t,o){try{if("color"===o.$type){const a=ColorParser.parse(o.$value),s=a.a<1?Object.assign(Object.assign({},a),{a:MathUtils.round2(a.a)}):a;e.setValueForMode(t,s)}else e.setValueForMode(t,o.$value)}catch(e){console.error(`Could not set value: ${e}`)}}function getGroupKey(e){const t=e.indexOf("/");return-1===t?"":e.substring(0,t)}function summarizeGroups(e){const t=Object.create(null),o=[];for(let a=0;a{Logger.log(` ${t+1}. "${e.name}" (id: ${e.id})`)});const t=new Set;let o=0,a=0,s=0;const r=[];for(let i=0;ie.name),variableCount:l.variableIds.length,types:n,groups:summarizeGroups(c)})}r.sort((e,t)=>e.name.localeCompare(t.name));const i=await figma.getLocalPaintStylesAsync(),l=await figma.getLocalTextStylesAsync(),n=await figma.getLocalEffectStylesAsync(),c=await figma.getLocalGridStylesAsync();let g=0;const f=[];for(let e=0;e({family:e,styles:Array.from(t)}));let v=0;for(const e of i)e.boundVariables&&Object.keys(e.boundVariables).length>0&&v++;Logger.send("collections",{collections:r,styles:d,styleGroups:p,libraryDependencies:Array.from(t),fontsUsed:h,stats:{totalVariables:r.reduce((e,t)=>e+t.variableCount,0),totalAliases:o,localAliases:a,libraryAliases:s,styleBindings:v}})}async function clearVariables(){Logger.log("🗑️ Clearing all variables...");try{let e=0,t=0;for(const o of await figma.variables.getLocalVariableCollectionsAsync()){for(const e of o.variableIds){const o=await figma.variables.getVariableByIdAsync(e);o&&(o.remove(),t++)}o.remove(),e++}Logger.log(`✅ Cleared ${e} collections, ${t} variables`),Logger.send("clear_complete",{message:`${e} collections, ${t} variables`})}catch(e){Logger.log(`❌ Clear variables error: ${e}`),Logger.send("error",{message:`Failed to clear variables: ${e}`})}}async function clearStyles(){Logger.log("🗑️ Clearing all styles...");try{let e=0;for(const t of await figma.getLocalPaintStylesAsync())t.remove(),e++;for(const t of await figma.getLocalTextStylesAsync())t.remove(),e++;for(const t of await figma.getLocalEffectStylesAsync())t.remove(),e++;for(const t of await figma.getLocalGridStylesAsync())t.remove(),e++;Logger.log(`✅ Cleared ${e} styles`),Logger.send("clear_complete",{message:`${e} styles`})}catch(e){Logger.log(`❌ Clear styles error: ${e}`),Logger.send("error",{message:`Failed to clear styles: ${e}`})}}async function clearAll(){Logger.log("🗑️ Clearing everything...");try{await clearVariables(),await clearStyles()}catch(e){Logger.log(`❌ Clear all error: ${e}`),Logger.send("error",{message:`Failed to clear: ${e}`})}}async function createUndoSnapshot(){var e;Logger.log("📸 Creating snapshot of current file state...");const t=await figma.variables.getLocalVariableCollectionsAsync(),o=[];for(const e of t){const t={name:e.name,modes:e.modes.map(e=>({id:e.modeId,name:e.name})),variables:[]};for(const o of e.variableIds){const a=await figma.variables.getVariableByIdAsync(o);if(!a)continue;const s={name:a.name,type:a.resolvedType,scopes:[...a.scopes],values:{}};for(const t of e.modes){const e=a.valuesByMode[t.modeId];if("object"==typeof e&&null!==e&&"type"in e&&"VARIABLE_ALIAS"===e.type){const o=e.id,a=await figma.variables.getVariableByIdAsync(o);if(a){const e=await figma.variables.getVariableCollectionByIdAsync(a.variableCollectionId);s.values[t.name]={isAlias:!0,aliasName:a.name,aliasCollection:(null==e?void 0:e.name)||""}}}else if("COLOR"===a.resolvedType){const o=e;s.values[t.name]={isAlias:!1,value:ColorConverter.toHex(o)}}else s.values[t.name]={isAlias:!1,value:e}}t.variables.push(s)}o.push(t)}const a={colorStyles:await ColorStyleProcessor.export({includeImages:!0}),textStyles:await TextStyleProcessor.export(),effectStyles:await EffectStyleProcessor.export(),gridStyles:await GridStyleProcessor.export()},s=(null===(e=a.colorStyles)||void 0===e?void 0:e.length)||0;return Logger.log(`📸 Snapshot captured: ${t.length} collections, ${s} color styles`),{timestamp:Date.now(),collections:JSON.stringify(o),styles:JSON.stringify(a)}}async function restoreFromSnapshot(e){Logger.log("↩️ Restoring file from snapshot..."),Logger.log(" Step 1: Clearing current state..."),await clearAll(),await variableCache.rebuild();const t=JSON.parse(e.collections);Logger.log(` Step 2: Restoring ${t.length} collections...`);const o=[];for(const e of t){const t=figma.variables.createVariableCollection(e.name);if(e.modes.length>0){t.renameMode(t.modes[0].modeId,e.modes[0].name);for(let o=1;o0&&(r.scopes=s.scopes);for(const t of e.modes){const i=a[t.name],l=s.values[t.name];if(l)if(l.isAlias&&l.aliasName)o.push({variable:r,modeId:i,aliasPath:l.aliasName,aliasCollection:l.aliasCollection||e.name});else if(void 0!==l.value){let e;e="COLOR"===s.type&&"string"==typeof l.value?ColorParser.parse(l.value):l.value,r.setValueForMode(i,e)}}}}Logger.log(` Step 3: Resolving ${o.length} aliases...`),await variableCache.rebuild();for(const e of o){const t=`${e.aliasCollection}/${e.aliasPath}`,o=variableCache.getVariable(t);o&&e.variable.setValueForMode(e.modeId,{type:"VARIABLE_ALIAS",id:o.id})}const a=JSON.parse(e.styles);Logger.log(" Step 4: Restoring styles..."),a.colorStyles&&a.colorStyles.length>0&&await ColorStyleProcessor.importStyles(a.colorStyles,variableCache),a.textStyles&&a.textStyles.length>0&&await TextStyleProcessor.importStyles(a.textStyles,variableCache),a.effectStyles&&a.effectStyles.length>0&&await EffectStyleProcessor.importStyles(a.effectStyles,variableCache),a.gridStyles&&a.gridStyles.length>0&&await GridStyleProcessor.importStyles(a.gridStyles,variableCache),Logger.log("✅ File restored from snapshot")}figma.ui.onmessage=async e=>{switch(e.type){case"export":await exportVariables(e.collections,e.styleOptions,e.preserveLibraryRefs,e.includeImages,e.namingConvention||"original",e.exportFormat||"figma",e.selectedModes,e.resolveAliases||!1,e.selectedGroups,e.selectedStyleGroups);break;case"import":await importVariables(e.data,e.options);break;case"validate_import":try{const t=JSON.parse(e.data),o=e.plan,a=await validateImportAgainstPlan(t,o);Logger.send("validation_result",a)}catch(e){Logger.send("validation_result",{errors:[`Invalid JSON: ${e instanceof Error?e.message:"Parse error"}`],canImport:!1})}break;case"compute_import_diff":try{const t=JSON.parse(e.data),o=await computeImportDiff(t);Logger.send("import_diff_result",o)}catch(e){Logger.send("import_diff_result",{error:`Failed to compute diff: ${e instanceof Error?e.message:"Unknown error"}`})}break;case"detect_plan":const t=await detectCurrentPlan();Logger.send("plan_detected",t);break;case"clear_variables":await clearVariables();break;case"clear_styles":await clearStyles();break;case"clear_all":await clearAll();break;case"get_collections":await getCollections();break;case"check_libraries":try{const t=e.collections;await variableCache.rebuild();const o=[],a=[];for(const e of t)variableCache.isCollectionAvailable(e)?o.push(e):a.push(e);Logger.log(`📚 Library check: ${o.length} available, ${a.length} missing`),o.length>0&&Logger.log(` ✅ Available: ${o.join(", ")}`),a.length>0&&Logger.log(` ❌ Missing: ${a.join(", ")}`),Logger.send("library_check_result",{allAvailable:0===a.length,availableCollections:o,missingCollections:a,requiredCollections:t})}catch(t){Logger.send("library_check_result",{allAvailable:!1,availableCollections:[],missingCollections:e.collections||[],requiredCollections:e.collections||[],error:t instanceof Error?t.message:"Library check failed"})}break;case"check_fonts":try{const t=e.fonts,o=[],a=[];for(const e of t)try{await figma.loadFontAsync({family:e.family,style:e.style}),o.push(e)}catch(t){a.push(e)}Logger.send("font_check_result",{allAvailable:0===a.length,availableFonts:o,missingFonts:a,requiredFonts:t})}catch(t){Logger.send("font_check_result",{allAvailable:!1,availableFonts:[],missingFonts:e.fonts||[],requiredFonts:e.fonts||[],error:t instanceof Error?t.message:"Font check failed"})}break;case"create_undo_snapshot":try{Logger.log("📸 Creating undo snapshot...");const e=await createUndoSnapshot();Logger.send("snapshot_created",{snapshot:e}),Logger.log("✅ Undo snapshot created successfully")}catch(e){Logger.log(`❌ Failed to create snapshot: ${e instanceof Error?e.message:"Unknown error"}`),Logger.send("snapshot_error",{error:e instanceof Error?e.message:"Failed to create snapshot"})}break;case"undo_import":try{Logger.log("↩️ Undoing import using snapshot...");const t=e.snapshot;await restoreFromSnapshot(t),Logger.send("undo_complete",{}),Logger.log("✅ Import undone successfully")}catch(e){Logger.log(`❌ Undo failed: ${e instanceof Error?e.message:"Unknown error"}`),Logger.send("undo_error",{error:e instanceof Error?e.message:"Undo failed"})}}}; \ No newline at end of file + */figma.showUI(__html__,{width:1200,height:628,themeColors:!0,title:"☕️ Variables & Styles Extractor v2.0.0"});const Logger={log(e,t){console.log(`[Variables Extractor] ${e}`,t||""),figma.ui.postMessage({type:"log",message:e,data:t})},send(e,t){figma.ui.postMessage({type:e,data:t})}};function yieldToHost(){return new Promise(function(e){setTimeout(e,0)})}function makeCancelError(){const e=new Error("Operation cancelled");return e.isOperationCancelled=!0,e}function isCancelError(e){return"object"==typeof e&&null!==e&&!0===e.isOperationCancelled}const currentOperation={type:null,cancelRequested:!1,cancellable:!0};function beginOperation(e){return null!==currentOperation.type?(figma.ui.postMessage({type:"operation_denied",requested:e,running:currentOperation.type}),!1):(currentOperation.type=e,currentOperation.cancelRequested=!1,currentOperation.cancellable=!0,!0)}function endOperation(){currentOperation.type=null,currentOperation.cancelRequested=!1,currentOperation.cancellable=!0}function checkCancelled(){if(currentOperation.cancelRequested&¤tOperation.cancellable)throw makeCancelError()}async function withOperation(e,t){if(beginOperation(e))try{await t()}finally{endOperation()}}async function runBatched(e,t,a,o){const r=e.length;for(let s=0;s0&&s>=n)&&l-aa&&(a=t.modes.length);return t=a>20?"enterprise":a>10?"organization":"professional",Object.assign({plan:t},PLAN_LIMITS[t])}async function validateImportAgainstPlan(e,t){const a=t?Object.assign({plan:t},PLAN_LIMITS[t]):await detectCurrentPlan(),o=await figma.variables.getLocalVariableCollectionsAsync(),r=o.reduce((e,t)=>Math.max(e,t.modes.length),0),s=(await figma.variables.getLocalVariablesAsync()).length,n=[];for(const t of e)"_styles"in t||n.push(t);let i=0,l=0;const c=[];for(const e of n){const t=Object.keys(e)[0],o=e[t];if(!o||!o.modes)continue;const r=Object.keys(o.modes).length;r>i&&(i=r),r>a.maxModesPerCollection&&c.push(`"${t}" (${r} modes, limit: ${a.maxModesPerCollection===1/0?"∞":a.maxModesPerCollection})`);const s=Object.values(o.modes)[0];s&&(l+=countNestedVariables(s))}const g=[],d=[];c.length;for(const e of n){const t=Object.keys(e)[0],a=e[t];if(!a||!a.modes)continue;const o=Object.values(a.modes)[0],r=o?countNestedVariables(o):0;r>5e3&&d.push(`Collection "${t}" has ${r} variables, exceeds limit of 5000`)}l>1e3&&g.push(`Large import: ${l} variables. This may take a moment.`),n.length>10&&g.push(`Importing ${n.length} collections. Consider importing in batches.`);const f=new Set;let u=0;for(const e of n){const t=e[Object.keys(e)[0]];if(t&&t.modes)for(const e of Object.keys(t.modes)){const a=flattenVariables(t.modes[e],"");for(const{value:e}of a)e.$libraryRef&&e.$collectionName&&(f.add(e.$collectionName),u++)}}const m=[];let p=0;for(const t of e)if("_styles"in t){const e=t._styles;if(e.textStyles)for(const t of e.textStyles){p++;const e=`${t.fontFamily}|${t.fontStyle}`;m.some(t=>`${t.family}|${t.style}`===e)||m.push({family:t.fontFamily,style:t.fontStyle})}}return Object.assign(Object.assign({currentPlan:a,existing:{collections:o.length,maxModesInAnyCollection:r,totalVariables:s},importing:{collections:n.length,maxModesInAnyCollection:i,totalVariables:l,collectionsExceedingModeLimit:c},warnings:g,errors:d,canImport:0===d.length},f.size>0&&{libraryDependencies:{variableCount:u,collections:Array.from(f)}}),m.length>0&&{fontDependencies:{styleCount:p,fonts:m}})}function countNestedVariables(e,t=0){for(const[,a]of Object.entries(e))a&&"object"==typeof a&&("$type"in a&&"$value"in a?t++:t=countNestedVariables(a,t));return t}const MathUtils={round2:e=>Math.round(100*e)/100,clamp:(e,t,a)=>Math.max(t,Math.min(a,e)),toHexByte:e=>Math.round(255*e).toString(16).padStart(2,"0"),fromHexByte:e=>parseInt(e,16)/255};function calculateHue(e,t,a,o,r){if(o===r)return 0;const s=o-r;let n=0;switch(o){case e:n=((t-a)/s+(t.5?e/(2-r-s):e/(r+s)}const l={h:calculateHue(t,a,o,r,s),s:Math.round(100*i),l:Math.round(100*n)},c=e.a;return void 0!==c&&c<1?Object.assign(Object.assign({},l),{a:MathUtils.round2(c)}):l},toHsb(e){const{r:t,g:a,b:o}=e,r=Math.max(t,a,o),s=Math.min(t,a,o),n=0===r?0:(r-s)/r,i={h:calculateHue(t,a,o,r,s),s:Math.round(100*n),b:Math.round(100*r)},l=e.a;return void 0!==l&&l<1?Object.assign(Object.assign({},i),{a:MathUtils.round2(l)}):i},toAllFormats(e){return{hex:this.toHex(e),rgb:this.toRgb255(e),css:this.toCss(e),hsl:this.toHsl(e),hsb:this.toHsb(e)}}},NamingConverter={convert(e,t){if("original"===t)return e;const a=e.replace(/([a-z])([A-Z])/g,"$1 $2").split(/[\s\/\-_]+/).filter(e=>e.length>0).map(e=>e.toLowerCase());if(0===a.length)return e;switch(t){case"camelCase":return a[0]+a.slice(1).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("");case"kebab-case":return a.join("-");case"snake_case":return a.join("_");default:return e}},convertPath(e,t){return"original"===t?e:e.split("/").map(e=>this.convert(e,t)).join("/")},convertCollectionName(e,t){return this.convert(e,t)},convertModeName(e,t){return this.convert(e,t)},addOriginalName(e,t){if("original"===t)return{converted:e};const a=this.convert(e,t);return a===e?{converted:e}:{converted:a,original:e}}};async function resolveAliasValue(e,t,a=10){if(a<=0)return Logger.log(`⚠️ Max alias resolution depth reached for ${e.name}`),"";let o=e.valuesByMode[t];if(void 0===o){const t=Object.keys(e.valuesByMode);t.length>0&&(o=e.valuesByMode[t[0]])}if(void 0===o)return"";if(isVariableAlias(o)){const e=await figma.variables.getVariableByIdAsync(o.id);return e?resolveAliasValue(e,t,a-1):""}return o}const W3C_TYPE_MAP={color:"color",float:"number",string:"string",boolean:"boolean"},W3CConverter={colorToW3C:e=>e.hex,typeToW3C:e=>W3C_TYPE_MAP[e]||"string",valueToW3C(e,t=!1){const a={$value:"",$type:this.typeToW3C(e.$type)};return t&&"string"==typeof e.$value&&e.$value.startsWith("{")?a.$value=e.$value:"color"===e.$type&&"object"==typeof e.$value?a.$value=e.$value.hex:a.$value=e.$value,e.$description&&(a.$description=e.$description),e.$scopes&&e.$scopes.length>0&&!e.$scopes.includes("ALL_SCOPES")&&(a.$extensions={"com.figma":{scopes:e.$scopes}}),a},collectionToW3C(e,t,a,o){const r={};o&&o!==e&&(r.$description=`Figma collection: ${o}`);const s=Object.keys(t);if(1===s.length)this.addTokensToGroup(r,t[s[0]],a);else for(const e of s){const o=NamingConverter.convertModeName(e,a);r[o]={},this.addTokensToGroup(r[o],t[e],a)}return r},addTokensToGroup(e,t,a){for(const[o,r]of Object.entries(t)){const t=NamingConverter.convert(o,a);if(isExportVariableValue(r)){const a="string"==typeof r.$value&&r.$value.startsWith("{");e[t]=this.valueToW3C(r,a)}else e[t]={},this.addTokensToGroup(e[t],r,a)}},parseW3CToken(e){var t,a;const o=this.w3cTypeToFigma(e.$type),r=(null===(a=null===(t=e.$extensions)||void 0===t?void 0:t["com.figma"])||void 0===a?void 0:a.scopes)||["ALL_SCOPES"];let s;if("color"===o&&"string"==typeof e.$value){const t=ColorParser.parse(e.$value);s=ColorConverter.toAllFormats(t)}else s="string"==typeof e.$value||"number"==typeof e.$value||"boolean"==typeof e.$value?e.$value:JSON.stringify(e.$value);return e.$description?{$type:o,$value:s,$scopes:r,$description:e.$description}:{$type:o,$value:s,$scopes:r}},w3cTypeToFigma:e=>({color:"color",number:"float",dimension:"float",string:"string",boolean:"boolean",fontFamily:"string",fontWeight:"float",duration:"string",cubicBezier:"string"}[e]||"string"),isW3CFormat(e){if("object"!=typeof e||null===e)return!1;const t=e;for(const e of Object.keys(t)){const a=t[e];if("object"==typeof a&&null!==a){if("$value"in a&&"$type"in a)return!0;for(const e of Object.keys(a)){const t=a[e];if("object"==typeof t&&null!==t&&"$value"in t)return!0}}}return Array.isArray(e),!1},w3cToFigmaFormat(e){const t=[];for(const[a,o]of Object.entries(e)){if(a.startsWith("$"))continue;const e={[a]:{modes:{Default:this.w3cGroupToNestedVars(o)}}};t.push(e)}return t},w3cGroupToNestedVars(e){const t={};for(const[a,o]of Object.entries(e))a.startsWith("$")||(this.isW3CToken(o)?t[a]=this.parseW3CToken(o):"object"==typeof o&&null!==o&&(t[a]=this.w3cGroupToNestedVars(o)));return t},isW3CToken:e=>"object"==typeof e&&null!==e&&"$value"in e},HEX_REGEX_8=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,HEX_REGEX_6=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,RGBA_REGEX=/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/i,HSLA_REGEX=/hsla?\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*(?:,\s*([\d.]+))?\s*\)/i,ColorParser={fromHex(e){const t=HEX_REGEX_8.exec(e);if(t)return{r:MathUtils.fromHexByte(t[1]),g:MathUtils.fromHexByte(t[2]),b:MathUtils.fromHexByte(t[3]),a:MathUtils.fromHexByte(t[4])};const a=HEX_REGEX_6.exec(e);return a?{r:MathUtils.fromHexByte(a[1]),g:MathUtils.fromHexByte(a[2]),b:MathUtils.fromHexByte(a[3]),a:1}:{r:0,g:0,b:0,a:1}},fromRgb255(e){var t;return{r:e.r/255,g:e.g/255,b:e.b/255,a:null!==(t=e.a)&&void 0!==t?t:1}},fromCss(e){const t=RGBA_REGEX.exec(e);if(t)return{r:parseInt(t[1],10)/255,g:parseInt(t[2],10)/255,b:parseInt(t[3],10)/255,a:void 0!==t[4]?parseFloat(t[4]):1};const a=HSLA_REGEX.exec(e);return a?this.fromHsl({h:parseInt(a[1],10),s:parseInt(a[2],10),l:parseInt(a[3],10),a:void 0!==a[4]?parseFloat(a[4]):1}):{r:0,g:0,b:0,a:1}},fromHsl(e){var t,a;const o=e.h/360,r=e.s/100,s=e.l/100;if(0===r)return{r:s,g:s,b:s,a:null!==(t=e.a)&&void 0!==t?t:1};const hue2rgb=(e,t,a)=>{const o=a<0?a+1:a>1?a-1:a;return o<1/6?e+6*(t-e)*o:o<.5?t:o<2/3?e+(t-e)*(2/3-o)*6:e},n=s<.5?s*(1+r):s+r-s*r,i=2*s-n;return{r:hue2rgb(i,n,o+1/3),g:hue2rgb(i,n,o),b:hue2rgb(i,n,o-1/3),a:null!==(a=e.a)&&void 0!==a?a:1}},fromHsb(e){var t;const a=e.h/360,o=e.s/100,r=e.b/100,s=Math.floor(6*a),n=6*a-s,i=r*(1-o),l=r*(1-n*o),c=r*(1-(1-n)*o),g=[[r,c,i],[l,r,i],[i,r,c],[i,l,r],[c,i,r],[r,i,l]],[d,f,u]=g[s%6];return{r:d,g:f,b:u,a:null!==(t=e.a)&&void 0!==t?t:1}},parse(e){var t;if("object"==typeof e&&null!==e&&"hex"in e&&"rgb"in e)return this.fromHex(e.hex);if("object"==typeof e&&null!==e&&"r"in e&&"g"in e&&"b"in e){const a=e;return a.r<=1&&a.g<=1&&a.b<=1?{r:a.r,g:a.g,b:a.b,a:null!==(t=a.a)&&void 0!==t?t:1}:this.fromRgb255(a)}return"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"l"in e?this.fromHsl(e):"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"b"in e?this.fromHsb(e):"string"==typeof e?e.startsWith("rgb")||e.startsWith("hsl")?this.fromCss(e):this.fromHex(e):{r:0,g:0,b:0,a:1}}};class VariableCache{constructor(){this.collectionMap=new Map,this.variableMap=new Map,this.libraryVariableMap=new Map,this.libraryCollectionNames=new Set,this.initialized=!1,this.libraryIndexed=!1}async ensureReady(){this.initialized||(await this.rebuildLocal(),this.initialized=!0)}async initialize(){await this.ensureReady()}async rebuild(){await this.rebuildLocal(),await this.ensureLibraryIndex(),this.initialized=!0}async rebuildLocal(){this.clearLocal();const e=await figma.variables.getLocalVariableCollectionsAsync();for(let t=0;t{try{const a=await figma.variables.importVariableByKeyAsync(e.key);a&&this.libraryVariableMap.set(`${t}/${a.name}`,a)}catch(e){}})}catch(e){Logger.log(` ⚠️ Could not index library collection "${a.name}": ${e}`)}}this.libraryCollectionNames.size>0&&Logger.log(`📚 Indexed ${this.libraryVariableMap.size} library variables from ${this.libraryCollectionNames.size} connected libraries`)}catch(e){Logger.log(`⚠️ Could not access team library: ${e}`)}}getCollection(e){return this.collectionMap.get(e)}getVariable(e){return this.variableMap.get(e)||this.libraryVariableMap.get(e)}setVariable(e,t){this.variableMap.set(e,t)}setCollection(e,t){this.collectionMap.set(e,t)}removeCollection(e){this.collectionMap.delete(e);const t=[];for(const a of this.variableMap.keys())a.startsWith(`${e}/`)&&t.push(a);for(const e of t)this.variableMap.delete(e)}isCollectionAvailable(e){return this.collectionMap.has(e)||this.libraryCollectionNames.has(e)}getLibraryCollectionNames(){return Array.from(this.libraryCollectionNames)}get size(){return this.variableMap.size}get collections(){return this.collectionMap.values()}getVariableKeys(){return Array.from(this.variableMap.keys())}}const variableCache=new VariableCache;function isExportVariableValue(e){return"object"==typeof e&&null!==e&&"$type"in e}function isVariableAlias(e){return"object"==typeof e&&null!==e&&"VARIABLE_ALIAS"===e.type}const TypeMapper={toExportType(e){var t;return null!==(t={COLOR:"color",FLOAT:"float",STRING:"string",BOOLEAN:"boolean"}[e])&&void 0!==t?t:"string"},toFigmaType(e){var t;return null!==(t={color:"COLOR",float:"FLOAT",string:"STRING",boolean:"BOOLEAN"}[e])&&void 0!==t?t:"STRING"},scopesToArray:e=>0===e.length||e.includes("ALL_SCOPES")?["ALL_SCOPES"]:[...e],arrayToScopes:e=>e.includes("ALL_SCOPES")?["ALL_SCOPES"]:e};async function getVariableBindingInfo(e,t){if(!(null==e?void 0:e[t]))return{};const a=e[t];if(!a)return{};const o=await figma.variables.getVariableByIdAsync(a.id);if(!o)return{id:a.id};const r=await figma.variables.getVariableCollectionByIdAsync(o.variableCollectionId);return{id:a.id,name:o.name,collection:null==r?void 0:r.name}}async function extractBindings(e,t){if(!e)return;const a={};for(const o of t){const t=await getVariableBindingInfo(e,o);t.name&&(a[o]=t)}return Object.keys(a).length>0?a:void 0}function flattenVariables(e,t){const a=[];for(const o of Object.keys(e)){const r=e[o],s=t?`${t}/${o}`:o;isExportVariableValue(r)?a.push({path:s,value:r}):a.push(...flattenVariables(r,s))}return a}function getValueAtPath(e,t){const a=t.split("/");let o=e;for(const e of a){if("object"!=typeof o||null===o)return null;if(isExportVariableValue(o))return null;o=o[e]}return isExportVariableValue(o)?o:null}const ColorStyleProcessor={async export(e){var t;const a=null!==(t=null==e?void 0:e.includeImages)&&void 0!==t&&t,o=[],r=await figma.getLocalPaintStylesAsync();return await runSequentialAsync(r,20,async function(e){var t,r,s;if(0===e.paints.length)return;const n=[];let i,l,c;for(const o of e.paints)if("SOLID"===o.type){const e=o.color;let a=null!==(t=o.opacity)&&void 0!==t?t:1;void 0!==e.a&&e.a<1&&1===a&&(a=e.a);const r={r:o.color.r,g:o.color.g,b:o.color.b,a:a},s={type:"SOLID",color:ColorConverter.toAllFormats(r),opacity:MathUtils.round2(a)};n.push(s),i||(i=s.color,l=s.opacity,c=await extractBindings(o.boundVariables,["color"]))}else if("GRADIENT_LINEAR"===o.type||"GRADIENT_RADIAL"===o.type||"GRADIENT_ANGULAR"===o.type||"GRADIENT_DIAMOND"===o.type){const e=o.gradientStops.map(e=>{var t;return{position:MathUtils.round2(e.position),color:ColorConverter.toAllFormats({r:e.color.r,g:e.color.g,b:e.color.b,a:null!==(t=e.color.a)&&void 0!==t?t:1})}}),t=Object.assign(Object.assign({type:o.type,gradientStops:e},o.gradientTransform&&{gradientTransform:o.gradientTransform}),{opacity:MathUtils.round2(null!==(r=o.opacity)&&void 0!==r?r:1)});n.push(t)}else if("IMAGE"===o.type){const t=Object.assign(Object.assign(Object.assign(Object.assign({type:"IMAGE",scaleMode:o.scaleMode},o.imageHash&&{imageHash:o.imageHash}),{opacity:MathUtils.round2(null!==(s=o.opacity)&&void 0!==s?s:1)}),void 0!==o.rotation&&{rotation:o.rotation}),o.filters&&{filters:Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},void 0!==o.filters.exposure&&{exposure:o.filters.exposure}),void 0!==o.filters.contrast&&{contrast:o.filters.contrast}),void 0!==o.filters.saturation&&{saturation:o.filters.saturation}),void 0!==o.filters.temperature&&{temperature:o.filters.temperature}),void 0!==o.filters.tint&&{tint:o.filters.tint}),void 0!==o.filters.highlights&&{highlights:o.filters.highlights}),void 0!==o.filters.shadows&&{shadows:o.filters.shadows})});if(a&&o.imageHash)try{const e=figma.getImageByHash(o.imageHash);if(e){const a=await e.getBytesAsync();if(a){const e=figma.base64Encode(a);t.imageBase64=e}}}catch(t){Logger.log(`⚠️ Could not export image data for style "${e.name}": ${t}`)}n.push(t)}if(0===n.length)return;const g=Object.assign(Object.assign(Object.assign(Object.assign({name:e.name,paints:n},i&&{color:i}),void 0!==l&&{opacity:l}),e.description&&{description:e.description}),c&&Object.keys(c).length>0&&{boundVariables:c});o.push(g)}),o},async importStyles(e,t){let a=0,o=0;const r=new Map;for(const e of await figma.getLocalPaintStylesAsync())r.set(e.name,e);return await runSequentialAsync(e,20,async function(e){var s,n,i,l;let c;r.has(e.name)?(c=r.get(e.name),o++):(c=figma.createPaintStyle(),c.name=e.name,a++),e.description&&(c.description=e.description);const g=[];if(e.paints&&e.paints.length>0){for(const a of e.paints)if("SOLID"===a.type){const o=ColorParser.parse(a.color);let r=null!==(s=a.opacity)&&void 0!==s?s:1;o.a<1&&void 0===a.opacity&&(r=MathUtils.round2(o.a));let n={type:"SOLID",color:{r:o.r,g:o.g,b:o.b},opacity:MathUtils.round2(r)};if(e.boundVariables&&0===g.length)for(const[a,o]of Object.entries(e.boundVariables))if(o.name&&o.collection){const e=t.getVariable(`${o.collection}/${o.name}`);if(e)try{n=figma.variables.setBoundVariableForPaint(n,a,e)}catch(e){Logger.log(`⚠️ Could not bind ${a}: ${e}`)}}g.push(n)}else if("GRADIENT_LINEAR"===a.type||"GRADIENT_RADIAL"===a.type||"GRADIENT_ANGULAR"===a.type||"GRADIENT_DIAMOND"===a.type){const e=a.gradientStops.map(e=>{const t=ColorParser.parse(e.color);return{position:e.position,color:{r:t.r,g:t.g,b:t.b,a:t.a}}}),t=a.gradientTransform?[[a.gradientTransform[0][0],a.gradientTransform[0][1],a.gradientTransform[0][2]],[a.gradientTransform[1][0],a.gradientTransform[1][1],a.gradientTransform[1][2]]]:[[1,0,0],[0,1,0]],o={type:a.type,gradientStops:e,gradientTransform:t,opacity:null!==(n=a.opacity)&&void 0!==n?n:1};g.push(o)}else if("IMAGE"===a.type){let t=null;if(a.imageBase64)try{const o=figma.base64Decode(a.imageBase64);t=figma.createImage(o).hash,Logger.log(`✅ Created image from base64 data for style "${e.name}"`)}catch(t){Logger.log(`⚠️ Could not import image from base64 for style "${e.name}": ${t}`)}if(!t&&a.imageHash){figma.getImageByHash(a.imageHash)?(t=a.imageHash,Logger.log(`✅ Found existing image with hash for style "${e.name}"`)):Logger.log(`⚠️ Image hash not found in file for style "${e.name}", skipping image paint (imageHash cannot be null)`)}if(t){const e=Object.assign(Object.assign({type:"IMAGE",scaleMode:a.scaleMode,imageHash:t,opacity:null!==(i=a.opacity)&&void 0!==i?i:1},void 0!==a.rotation&&{rotation:a.rotation}),a.filters&&{filters:a.filters});g.push(e)}}}else if(e.color){const a=ColorParser.parse(e.color);let o=null!==(l=e.opacity)&&void 0!==l?l:1;a.a<1&&void 0===e.opacity&&(o=MathUtils.round2(a.a));let r={type:"SOLID",color:{r:a.r,g:a.g,b:a.b},opacity:MathUtils.round2(o)};if(e.boundVariables)for(const[a,o]of Object.entries(e.boundVariables))if(o.name&&o.collection){const e=t.getVariable(`${o.collection}/${o.name}`);if(e)try{r=figma.variables.setBoundVariableForPaint(r,a,e)}catch(e){Logger.log(`⚠️ Could not bind ${a}: ${e}`)}}g.push(r)}g.length>0&&(c.paints=g)}),{created:a,updated:o}}},TextStyleProcessor={async export(e){const t=[],a=await figma.getLocalTextStylesAsync();return await runSequentialAsync(a,20,async function(e){const a=Object.assign(Object.assign({name:e.name,fontFamily:e.fontName.family,fontStyle:e.fontName.style,fontSize:e.fontSize,lineHeight:e.lineHeight,letterSpacing:e.letterSpacing,textCase:e.textCase,textDecoration:e.textDecoration},e.description&&{description:e.description}),{boundVariables:await extractBindings(e.boundVariables,["fontSize","lineHeight","letterSpacing","paragraphSpacing","paragraphIndent"])});t.push(a)}),t},async importStyles(e,t){let a=0,o=0;const r=new Map;for(const e of await figma.getLocalTextStylesAsync())r.set(e.name,e);return await runSequentialAsync(e,20,async function(e){let s;r.has(e.name)?(s=r.get(e.name),o++):(s=figma.createTextStyle(),s.name=e.name,a++),e.description&&(s.description=e.description);try{if(await figma.loadFontAsync({family:e.fontFamily,style:e.fontStyle}),s.fontName={family:e.fontFamily,style:e.fontStyle},s.fontSize=e.fontSize,s.lineHeight=e.lineHeight,s.letterSpacing=e.letterSpacing,e.textCase&&(s.textCase=e.textCase),e.textDecoration&&(s.textDecoration=e.textDecoration),e.boundVariables)for(const[a,o]of Object.entries(e.boundVariables))if(o.name&&o.collection){const e=t.getVariable(`${o.collection}/${o.name}`);if(e)try{s.setBoundVariable(a,e)}catch(e){}}}catch(t){Logger.log(`⚠️ Could not load font for ${e.name}: ${t}`)}}),{created:a,updated:o}}},EffectStyleProcessor={async export(e){const t=[],a=await figma.getLocalEffectStylesAsync();return await runSequentialAsync(a,20,async function(e){const a=[];for(const t of e.effects){const e=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:t.type,visible:t.visible},"radius"in t&&{radius:t.radius}),"spread"in t&&{spread:t.spread}),"offset"in t&&{offset:t.offset}),"color"in t&&{color:ColorConverter.toAllFormats(t.color)}),"blendMode"in t&&{blendMode:t.blendMode}),"showShadowBehindNode"in t&&{showShadowBehindNode:t.showShadowBehindNode}),{boundVariables:await extractBindings(t.boundVariables,["color","radius","spread","offsetX","offsetY"])});a.push(e)}const o=Object.assign(Object.assign({name:e.name},e.description&&{description:e.description}),{effects:a});t.push(o)}),t},async importStyles(e,t){let a=0,o=0;const r=new Map;for(const e of await figma.getLocalEffectStylesAsync())r.set(e.name,e);return await runSequentialAsync(e,20,async function(e){let s;r.has(e.name)?(s=r.get(e.name),o++):(s=figma.createEffectStyle(),s.name=e.name,a++),e.description&&(s.description=e.description);const n=e.effects.map(e=>{var t;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:e.type,visible:null===(t=e.visible)||void 0===t||t},void 0!==e.radius&&{radius:e.radius}),void 0!==e.spread&&{spread:e.spread}),void 0!==e.offset&&{offset:e.offset}),void 0!==e.color&&{color:(()=>{const t=ColorParser.parse(e.color);return{r:t.r,g:t.g,b:t.b,a:MathUtils.round2(t.a)}})()}),void 0!==e.blendMode&&{blendMode:e.blendMode}),void 0!==e.showShadowBehindNode&&{showShadowBehindNode:e.showShadowBehindNode})});s.effects=n;for(let a=0;a{var t,a,o,r,s,n,i;const l=e.color?ColorParser.parse(e.color):{r:1,g:0,b:0,a:.1},c={r:l.r,g:l.g,b:l.b,a:MathUtils.round2(l.a)};if("GRID"===e.pattern)return{pattern:"GRID",sectionSize:null!==(t=e.sectionSize)&&void 0!==t?t:10,visible:!1!==e.visible,color:c};const g=null!==(a=e.alignment)&&void 0!==a?a:"STRETCH",d={pattern:e.pattern,gutterSize:null!==(o=e.gutterSize)&&void 0!==o?o:10,count:null!==(r=e.count)&&void 0!==r?r:5,visible:!1!==e.visible,color:c};if("STRETCH"===g)return Object.assign(Object.assign({},d),{alignment:"STRETCH",offset:null!==(s=e.offset)&&void 0!==s?s:0});if("CENTER"===g)return Object.assign(Object.assign({},d),{alignment:"CENTER",sectionSize:null!==(n=e.sectionSize)&&void 0!==n?n:100});{const t=Object.assign(Object.assign({},d),{alignment:g,offset:null!==(i=e.offset)&&void 0!==i?i:0});return void 0!==e.sectionSize&&(t.sectionSize=e.sectionSize),t}});s.layoutGrids=n;for(let a=0;at.name===e);o?await checkVariablesDiff(i,o.modeId,a,n,"",t):l=!0}t.modifiedVariables.some(e=>e.collection===n)||t.newVariables.some(e=>e.collection===n)?(t.modifiedCollections.push(n),t.summary.collectionsModified++):(t.unchangedCollections.push(n),t.summary.collectionsUnchanged++)}return t}function countVariablesInCollection(e){let t=0;const a=Object.values(e)[0];return a&&(t=countVarsInNestedObj(a)),t}function countVarsInNestedObj(e){let t=0;for(const a of Object.values(e))isExportVariableValue(a)?t++:t+=countVarsInNestedObj(a);return t}async function checkVariablesDiff(e,t,a,o,r,s){for(const[n,i]of Object.entries(a)){const a=r?`${r}/${n}`:n;if(isExportVariableValue(i)){const e=variableCache.getVariable(`${o}/${a}`);if(e){const r=e.valuesByMode[t],n=i.$value;valuesAreDifferent(r,n)?(s.modifiedVariables.push({collection:o,path:a,oldValue:formatValueForDisplay(r),newValue:formatValueForDisplay(n)}),s.summary.variablesModified++):(s.unchangedVariables++,s.summary.variablesUnchanged++)}else s.newVariables.push({collection:o,path:a}),s.summary.variablesNew++}else await checkVariablesDiff(e,t,i,o,a,s)}}function valuesAreDifferent(e,t){if(void 0===e)return!0;if(isVariableAlias(e))return"string"==typeof t&&t.startsWith("{"),!0;if("object"==typeof e&&null!==e&&"r"in e){if("object"==typeof t&&null!==t&&"hex"in t){return ColorConverter.toAllFormats(e).hex.toLowerCase()!==t.hex.toLowerCase()}return!0}return e!==t}function formatValueForDisplay(e){if(void 0===e)return"undefined";if("object"==typeof e&&null!==e){if("hex"in e)return e.hex;if("r"in e)return ColorConverter.toAllFormats(e).hex;if("id"in e)return"{alias}"}return String(e)}async function computeStylesDiff(e,t){if(e.colorStyles){const a=await figma.getLocalPaintStylesAsync(),o=new Set(a.map(e=>e.name));for(const a of e.colorStyles)o.has(a.name)?(t.modifiedStyles.push({type:"color",name:a.name}),t.summary.stylesModified++):(t.newStyles.push({type:"color",name:a.name}),t.summary.stylesNew++)}if(e.textStyles){const a=await figma.getLocalTextStylesAsync(),o=new Set(a.map(e=>e.name));for(const a of e.textStyles)o.has(a.name)?(t.modifiedStyles.push({type:"text",name:a.name}),t.summary.stylesModified++):(t.newStyles.push({type:"text",name:a.name}),t.summary.stylesNew++)}if(e.effectStyles){const a=await figma.getLocalEffectStylesAsync(),o=new Set(a.map(e=>e.name));for(const a of e.effectStyles)o.has(a.name)?(t.modifiedStyles.push({type:"effect",name:a.name}),t.summary.stylesModified++):(t.newStyles.push({type:"effect",name:a.name}),t.summary.stylesNew++)}if(e.gridStyles){const a=await figma.getLocalGridStylesAsync(),o=new Set(a.map(e=>e.name));for(const a of e.gridStyles)o.has(a.name)?(t.modifiedStyles.push({type:"grid",name:a.name}),t.summary.stylesModified++):(t.newStyles.push({type:"grid",name:a.name}),t.summary.stylesNew++)}}function filterStylesByGroup(e,t){return t?e.filter(function(e){return-1!==t.indexOf(getGroupKey(e.name))}):e}async function exportVariables(e,t,a,o,r="original",s="figma",n,i=!1,l,c){var g,d,f,u,m,p,y,b;Logger.log("📤 Starting export..."),Logger.log(` preserveLibraryRefs: ${a}`),Logger.log(` includeImages: ${o}`),Logger.log(` namingConvention: ${r}`),Logger.log(` exportFormat: ${s}`),Logger.log(` resolveAliases: ${i}`),n&&Logger.log(` selectedModes: ${JSON.stringify(n)}`);try{let a=await figma.variables.getLocalVariableCollectionsAsync();(null==e?void 0:e.length)&&(a=a.filter(t=>e.includes(t.name)),Logger.log(`Filtering to ${a.length} selected collections`));const h=[],v={};let C=0;const S=createProgress("export");let A=0;for(let e=0;ea.includes(e.name)),Logger.log(` Filtering to ${t.length} modes: ${t.map(e=>e.name).join(", ")}`)}const a=NamingConverter.convertCollectionName(e.name,r),o={[a]:Object.assign({modes:{}},a!==e.name&&{$originalName:e.name})};for(const e of t){const t=NamingConverter.convertModeName(e.name,r);o[a].modes[t]={}}await runSequentialAsync(e.variableIds,BATCH.SEQ_EXPORT,async function(s){var n,c;L++;const g=await figma.variables.getVariableByIdAsync(s);if(!g)return;if(l&&l[e.name]&&-1===l[e.name].indexOf(getGroupKey(g.name)))return;C++;const d=g.name.split("/").map(e=>NamingConverter.convert(e,r));for(const e of t){const t=NamingConverter.convertModeName(e.name,r),s=o[a].modes[t],l=g.valuesByMode[e.modeId];let f=s;for(let e=0;eNamingConverter.convert(e,r)).join(".")}}`,m=v,h){const e=t.valuesByMode[Object.keys(t.valuesByMode)[0]];"object"==typeof e&&null!==e&&"r"in e?p=ColorConverter.toAllFormats(e):isVariableAlias(e)||(p=e)}}}else m=""}else m="object"==typeof l&&null!==l&&"r"in l?ColorConverter.toAllFormats(l):l;const C=Object.assign(Object.assign(Object.assign({$scopes:TypeMapper.scopesToArray(g.scopes),$type:TypeMapper.toExportType(g.resolvedType),$value:m},g.description&&{$description:g.description}),y&&b&&{$collectionName:b}),y&&h&&Object.assign({$libraryRef:v},void 0!==p&&{$localValue:p}));f[u]=C}},function(){S.report("export_variables","Exporting variables",L,A)}),h.push(o),"w3c"===s&&(v[a]=W3CConverter.collectionToW3C(a,o[a].modes,r,o[a].$originalName))}let w=null;if(t){if(w={},t.colorStyles){S.report("export_styles","Exporting styles",0,0,!0);const e=c?c.color:void 0;w.colorStyles=filterStylesByGroup(await ColorStyleProcessor.export({includeImages:o}),e),e&&0===w.colorStyles.length&&delete w.colorStyles}if(t.textStyles){S.report("export_styles","Exporting styles",0,0,!0);const e=c?c.text:void 0;w.textStyles=filterStylesByGroup(await TextStyleProcessor.export(),e),e&&0===w.textStyles.length&&delete w.textStyles}if(t.effectStyles){S.report("export_styles","Exporting styles",0,0,!0);const e=c?c.effect:void 0;w.effectStyles=filterStylesByGroup(await EffectStyleProcessor.export(),e),e&&0===w.effectStyles.length&&delete w.effectStyles}if(t.gridStyles){S.report("export_styles","Exporting styles",0,0,!0);const e=c?c.grid:void 0;w.gridStyles=filterStylesByGroup(await GridStyleProcessor.export(),e),e&&0===w.gridStyles.length&&delete w.gridStyles}Object.keys(w).length>0?h.push({_styles:w}):w=null}const $={collections:a.length,variables:C,styles:w?{color:null!==(d=null===(g=w.colorStyles)||void 0===g?void 0:g.length)&&void 0!==d?d:0,text:null!==(u=null===(f=w.textStyles)||void 0===f?void 0:f.length)&&void 0!==u?u:0,effect:null!==(p=null===(m=w.effectStyles)||void 0===m?void 0:m.length)&&void 0!==p?p:0,grid:null!==(b=null===(y=w.gridStyles)||void 0===y?void 0:y.length)&&void 0!==b?b:0}:null};let O;"w3c"===s?(w&&Object.keys(w).length>0&&(v.$extensions={"com.figma":{styles:w}}),O=JSON.stringify(v,null,2),Logger.log(`✅ Export complete (W3C format): ${$.collections} collections, ${$.variables} variables`)):(O=JSON.stringify(h,null,2),Logger.log(`✅ Export complete: ${$.collections} collections, ${$.variables} variables`)),await sendExportInChunks(O,$,s)}catch(e){if(isCancelError(e))return Logger.log("🛑 Export cancelled"),void figma.ui.postMessage({type:"operation_cancelled",operation:"export",phase:"export",rolledBack:!1,message:"Export cancelled. Nothing was changed."});Logger.log(`❌ Export error: ${e}`),Logger.send("error",{message:`Export failed: ${e}`})}}async function sendExportInChunks(e,t,a){const o=e.length,r=BATCH.EXPORT_CHUNK_BYTES,s=Math.max(1,Math.ceil(o/r));let n=0,i=0;for(;i=55296&&a<=56319&&(t+=1)}const a=e.slice(i,t);figma.ui.postMessage({type:"export_chunk",seq:n,total:s,data:a}),n++,i=t,n%BATCH.EXPORT_YIELD_EVERY===0&&i0?o.modes[r[0]]:{},"");h.push({collectionObj:t,flatPaths:s}),v+=s.length;for(let e=0;e0&&Logger.log(` ✅ Aliases: ${L} resolved, ${w} used fallback values`),y&&t.importStyles){if(Logger.log("📦 Importing styles..."),s.report("import_styles","Importing styles",0,0,!0),y.colorStyles){const e=await ColorStyleProcessor.importStyles(y.colorStyles,variableCache);m+=e.created,p+=e.updated}if(y.textStyles){const e=await TextStyleProcessor.importStyles(y.textStyles,variableCache);m+=e.created,p+=e.updated}if(y.effectStyles){const e=await EffectStyleProcessor.importStyles(y.effectStyles,variableCache);m+=e.created,p+=e.updated}if(y.gridStyles){const e=await GridStyleProcessor.importStyles(y.gridStyles,variableCache);m+=e.created,p+=e.updated}}const $={collectionsCreated:g,variablesCreated:d,variablesUpdated:f,variablesSkipped:u,stylesCreated:m,stylesUpdated:p};figma.commitUndo(),Logger.log("✅ Import complete!"),Logger.send("import_complete",{stats:$,snapshot:r})}catch(e){const t=isCancelError(e);if(t&&!n)return Logger.log("🛑 Import cancelled before any changes were made"),void figma.ui.postMessage({type:"operation_cancelled",operation:"import",phase:"snapshot",rolledBack:!1,message:"Import cancelled. No changes were made."});const a=e instanceof Error?e.message:String(e);if(t?Logger.log("🛑 Import cancelled after mutation started — rolling back..."):Logger.log(`❌ Import error: ${a}`),r){Logger.log("🔄 Attempting automatic rollback to pre-import state..."),Logger.send("import_rolling_back",{error:a});try{await restoreFromSnapshot(r),Logger.log("✅ Automatic rollback successful - file restored to pre-import state"),t?figma.ui.postMessage({type:"operation_cancelled",operation:"import",phase:"rollback",rolledBack:!0,message:"Import cancelled — your file was restored to its previous state."}):Logger.send("import_rollback_complete",{error:a,message:"Import failed but your file has been automatically restored to its previous state."})}catch(e){const t=e instanceof Error?e.message:String(e);Logger.log(`❌ Rollback failed: ${t}`),Logger.send("import_rollback_failed",{error:a,rollbackError:t,message:"Import failed and automatic rollback also failed. Please use Ctrl+Z (Cmd+Z) to undo manually."})}}else Logger.send("error",{message:`Import failed: ${a}. Use Ctrl+Z (Cmd+Z) to undo changes.`})}}function setRawValue(e,t,a){try{if("color"===a.$type){const o=ColorParser.parse(a.$value),r=o.a<1?Object.assign(Object.assign({},o),{a:MathUtils.round2(o.a)}):o;e.setValueForMode(t,r)}else e.setValueForMode(t,a.$value)}catch(e){console.error(`Could not set value: ${e}`)}}function getGroupKey(e){const t=e.indexOf("/");return-1===t?"":e.substring(0,t)}function summarizeGroups(e){const t=Object.create(null),a=[];for(let o=0;o{Logger.log(` ${t+1}. "${e.name}" (id: ${e.id})`)});const t=new Set;let a=0,o=0,r=0;const s=new Map,n=[];for(let i=0;ie.name),variableCount:l.variableIds.length,types:c,groups:summarizeGroups(g)})}n.sort((e,t)=>e.name.localeCompare(t.name));const i=await figma.getLocalPaintStylesAsync(),l=await figma.getLocalTextStylesAsync(),c=await figma.getLocalEffectStylesAsync(),g=await figma.getLocalGridStylesAsync();let d=0;const f=[];for(let e=0;e({family:e,styles:Array.from(t)}));let C=0;for(const e of i)e.boundVariables&&Object.keys(e.boundVariables).length>0&&C++;Logger.send("collections",{collections:n,styles:u,styleGroups:b,libraryDependencies:Array.from(t),fontsUsed:v,stats:{totalVariables:n.reduce((e,t)=>e+t.variableCount,0),totalAliases:a,localAliases:o,libraryAliases:r,styleBindings:C}})}async function clearVariables(e=!1){Logger.log("🗑️ Clearing all variables...");const t=e?null:createProgress("clear");let a=0,o=0,r=!1;try{const s=await figma.variables.getLocalVariableCollectionsAsync(),n=[];let i=0;for(let e=0;e0&&(figma.commitUndo(),r=!0);let l=0;for(let e=0;e0&&(figma.commitUndo(),o=!0);let c=0;const removeStyle=function(e){e.remove(),a++},onBatch=function(e){t&&t.report("clear","Deleting styles",c+e,l)};await runBatched(r,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=r.length,await runBatched(s,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=s.length,await runBatched(n,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=n.length,await runBatched(i,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=i.length,!e&&o&&figma.commitUndo(),Logger.log(`✅ Cleared ${a} styles`),e||Logger.send("clear_complete",{message:`${a} styles`})}catch(t){if(isCancelError(t)){if(e)throw t;return Logger.log(`🛑 Clear styles cancelled after ${a} styles`),void figma.ui.postMessage({type:"operation_cancelled",operation:"clear",phase:"clear",rolledBack:!1,partial:{collectionsDeleted:0,variablesDeleted:a},message:`Clear cancelled — ${a} styles were already deleted. Remaining items were not touched. Use Cmd+Z to restore deleted items.`})}if(Logger.log(`❌ Clear styles error: ${t}`),e)throw t;Logger.send("error",{message:`Failed to clear styles: ${t}`})}}async function clearAll(e=!1){if(Logger.log("🗑️ Clearing everything..."),e)return await clearVariables(!0),void await clearStyles(!0);let t=!1;try{figma.commitUndo(),t=!0,await clearVariables(!0),await clearStyles(!0),figma.commitUndo(),Logger.send("clear_complete",{message:"all variables and styles"})}catch(e){if(t&&figma.commitUndo(),isCancelError(e))return Logger.log("🛑 Clear all cancelled"),void figma.ui.postMessage({type:"operation_cancelled",operation:"clear",phase:"clear",rolledBack:!1,partial:{collectionsDeleted:0,variablesDeleted:0},message:"Clear cancelled — some items may already have been deleted. Use Cmd+Z to restore deleted items."});Logger.log(`❌ Clear all error: ${e}`),Logger.send("error",{message:`Failed to clear: ${e}`})}}async function createUndoSnapshot(e){var t;Logger.log("📸 Creating snapshot of current file state...");const a=await figma.variables.getLocalVariableCollectionsAsync(),o=[],r=new Map;let s=0;for(let e=0;e({id:e.modeId,name:e.name})),variables:[]},i=await runBatchedAsync(t.variableIds,BATCH.ASYNC_LOOKUP,function(e){return figma.variables.getVariableByIdAsync(e)},function(t){e&&e.report("snapshot","Preparing snapshot (undo safety)",n+t,s)});n+=t.variableIds.length;for(let e=0;e0){t.renameMode(t.modes[0].modeId,e.modes[0].name);for(let a=1;a0&&(r.scopes=o.scopes);for(const t of e.modes){const n=a[t.name],i=o.values[t.name];if(i)if(i.isAlias&&i.aliasName)s.push({variable:r,modeId:n,aliasPath:i.aliasName,aliasCollection:i.aliasCollection||e.name});else if(void 0!==i.value){let e;e="COLOR"===o.type&&"string"==typeof i.value?ColorParser.parse(i.value):i.value,r.setValueForMode(n,e)}}}},function(e,a){t.report("undo_restore","Restoring variables",e,a)}),Logger.log(` Resolving ${s.length} aliases...`),await runBatched(s,BATCH.SYNC_LIGHT,function(e){const t=`${e.aliasCollection}/${e.aliasPath}`,a=variableCache.getVariable(t);a&&e.variable.setValueForMode(e.modeId,{type:"VARIABLE_ALIAS",id:a.id})},function(e,a){t.report("undo_aliases","Restoring aliases",e,a)}),Logger.log(" Restoring styles..."),t.report("undo_styles","Restoring styles",0,0,!0),o.colorStyles&&o.colorStyles.length>0&&await ColorStyleProcessor.importStyles(o.colorStyles,variableCache),o.textStyles&&o.textStyles.length>0&&await TextStyleProcessor.importStyles(o.textStyles,variableCache),o.effectStyles&&o.effectStyles.length>0&&await EffectStyleProcessor.importStyles(o.effectStyles,variableCache),o.gridStyles&&o.gridStyles.length>0&&await GridStyleProcessor.importStyles(o.gridStyles,variableCache),Logger.log("✅ File restored from snapshot")}figma.ui.onmessage=async e=>{switch(e.type){case"cancel_operation":null!==currentOperation.type&&(currentOperation.cancellable?(currentOperation.cancelRequested=!0,Logger.log("🛑 Cancellation requested — finishing current batch…")):Logger.log("⚠️ Rollback in progress — cannot cancel"));break;case"export":await withOperation("export",function(){return exportVariables(e.collections,e.styleOptions,e.preserveLibraryRefs,e.includeImages,e.namingConvention||"original",e.exportFormat||"figma",e.selectedModes,e.resolveAliases||!1,e.selectedGroups,e.selectedStyleGroups)});break;case"import":await withOperation("import",function(){return importVariables(e.data,e.options)});break;case"validate_import":try{const t=JSON.parse(e.data),a=e.plan,o=await validateImportAgainstPlan(t,a);Logger.send("validation_result",o)}catch(e){Logger.send("validation_result",{errors:[`Invalid JSON: ${e instanceof Error?e.message:"Parse error"}`],canImport:!1})}break;case"compute_import_diff":try{const t=JSON.parse(e.data),a=await computeImportDiff(t);Logger.send("import_diff_result",a)}catch(e){Logger.send("import_diff_result",{error:`Failed to compute diff: ${e instanceof Error?e.message:"Unknown error"}`})}break;case"detect_plan":const t=await detectCurrentPlan();Logger.send("plan_detected",t);break;case"clear_variables":await withOperation("clear",function(){return clearVariables(!1)});break;case"clear_styles":await withOperation("clear",function(){return clearStyles(!1)});break;case"clear_all":await withOperation("clear",function(){return clearAll(!1)});break;case"get_collections":await withOperation("scan",getCollections);break;case"check_libraries":try{const t=e.collections;await variableCache.rebuild();const a=[],o=[];for(const e of t)variableCache.isCollectionAvailable(e)?a.push(e):o.push(e);Logger.log(`📚 Library check: ${a.length} available, ${o.length} missing`),a.length>0&&Logger.log(` ✅ Available: ${a.join(", ")}`),o.length>0&&Logger.log(` ❌ Missing: ${o.join(", ")}`),Logger.send("library_check_result",{allAvailable:0===o.length,availableCollections:a,missingCollections:o,requiredCollections:t})}catch(t){Logger.send("library_check_result",{allAvailable:!1,availableCollections:[],missingCollections:e.collections||[],requiredCollections:e.collections||[],error:t instanceof Error?t.message:"Library check failed"})}break;case"check_fonts":try{const t=e.fonts,a=[],o=[],r=await runBatchedAsync(t,BATCH.ASYNC_FONT,function(e){return figma.loadFontAsync({family:e.family,style:e.style}).then(function(){return{font:e,available:!0}}).catch(function(){return{font:e,available:!1}})});for(let e=0;e UI messages for heavy-load operations (progress, chunked export, +// cancellation, operation denial). +interface ProgressMessage { + readonly type: 'operation_progress'; + readonly operation: string; + readonly phase: string; + readonly label: string; + readonly current: number; + readonly total: number; + readonly indeterminate: boolean; +} + +interface ExportChunkMessage { + readonly type: 'export_chunk'; + readonly seq: number; + readonly total: number; + readonly data: string; +} + +interface ExportDoneMessage { + readonly type: 'export_done'; + readonly stats: ExportStats; + readonly format: string; + readonly chunkCount: number; + readonly totalLength: number; +} + +interface CancelledMessage { + readonly type: 'operation_cancelled'; + readonly operation: string; + readonly phase: string; + readonly rolledBack: boolean; + readonly partial?: boolean; + readonly message: string; +} + +interface DeniedMessage { + readonly type: 'operation_denied'; + readonly requested: string; + readonly running: string; +} + // ============================================================================ // SECTION 3: UTILITY FUNCTIONS (JSF Rule 4.15 - DRY) // ============================================================================ @@ -322,6 +364,199 @@ const Logger = { } } as const; +// ============================================================================ +// SECTION 3a: HEAVY-LOAD UTILITIES — yield, cancellation, operation lock, +// batch runners, progress throttling (QuickJS-safe) +// ============================================================================ + +// Yield control back to the host event loop so the UI can repaint. +function yieldToHost(): Promise { + return new Promise(function (resolve) { + setTimeout(resolve, 0); + }); +} + +// Cancellation sentinel: a plain Error tagged with isOperationCancelled. +interface CancelError extends Error { + isOperationCancelled: true; +} + +function makeCancelError(): CancelError { + const err = new Error('Operation cancelled') as CancelError; + err.isOperationCancelled = true; + return err; +} + +function isCancelError(e: unknown): boolean { + return typeof e === 'object' && e !== null && + (e as Record).isOperationCancelled === true; +} + +// Single global operation lock. Only one long operation runs at a time. +interface OperationState { + type: string | null; + cancelRequested: boolean; + cancellable: boolean; +} + +const currentOperation: OperationState = { + type: null, + cancelRequested: false, + cancellable: true +}; + +function beginOperation(type: string): boolean { + if (currentOperation.type !== null) { + figma.ui.postMessage({ + type: 'operation_denied', + requested: type, + running: currentOperation.type + }); + return false; + } + currentOperation.type = type; + currentOperation.cancelRequested = false; + currentOperation.cancellable = true; + return true; +} + +function endOperation(): void { + currentOperation.type = null; + currentOperation.cancelRequested = false; + currentOperation.cancellable = true; +} + +function checkCancelled(): void { + if (currentOperation.cancelRequested && currentOperation.cancellable) { + throw makeCancelError(); + } +} + +async function withOperation(type: string, fn: () => Promise): Promise { + if (!beginOperation(type)) return; + try { + await fn(); + } finally { + endOperation(); + } +} + +// Run a synchronous fn over items in batches, yielding between batches. +async function runBatched( + items: T[], + batchSize: number, + fn: (item: T, index: number) => void, + onBatch?: (done: number, total: number) => void +): Promise { + const total = items.length; + for (let start = 0; start < total; start += batchSize) { + const end = Math.min(start + batchSize, total); + for (let i = start; i < end; i++) { + fn(items[i], i); + } + if (onBatch) onBatch(end, total); + checkCancelled(); + if (end < total) { + await yieldToHost(); + } + } +} + +// Run an async fn over items in chunks via Promise.all, yielding between chunks. +async function runBatchedAsync( + items: T[], + chunkSize: number, + fn: (item: T, index: number) => Promise, + onBatch?: (done: number, total: number) => void +): Promise { + const total = items.length; + const results: R[] = []; + for (let start = 0; start < total; start += chunkSize) { + const end = Math.min(start + chunkSize, total); + const promises: Promise[] = []; + for (let i = start; i < end; i++) { + promises.push(fn(items[i], i)); + } + const chunkResults = await Promise.all(promises); + for (let j = 0; j < chunkResults.length; j++) { + results.push(chunkResults[j]); + } + if (onBatch) onBatch(end, total); + checkCancelled(); + if (end < total) { + await yieldToHost(); + } + } + return results; +} + +// Run an async fn over items strictly in order, yielding every batchSize items. +async function runSequentialAsync( + items: T[], + batchSize: number, + fn: (item: T, index: number) => Promise, + onBatch?: (done: number, total: number) => void +): Promise { + const total = items.length; + const results: R[] = []; + for (let i = 0; i < total; i++) { + results.push(await fn(items[i], i)); + const done = i + 1; + if (done % batchSize === 0 || done === total) { + if (onBatch) onBatch(done, total); + checkCancelled(); + if (done < total) { + await yieldToHost(); + } + } + } + return results; +} + +// Progress reporter with time-based throttling. Always posts on phase change +// or final tick; otherwise skips updates that arrive too soon. +interface ProgressReporter { + report( + phase: string, + label: string, + current: number, + total: number, + indeterminate?: boolean + ): void; +} + +function createProgress(operation: string): ProgressReporter { + let lastPhase: string | null = null; + let lastPost = 0; + return { + report( + phase: string, + label: string, + current: number, + total: number, + indeterminate?: boolean + ): void { + const now = Date.now(); + const phaseChanged = phase !== lastPhase; + const isFinal = total > 0 && current >= total; + if (!phaseChanged && !isFinal) { + if (now - lastPost < BATCH.PROGRESS_MIN_MS) return; + } + lastPhase = phase; + lastPost = now; + figma.ui.postMessage({ + type: 'operation_progress', + operation, + phase, + label, + current, + total, + indeterminate: indeterminate === true + }); + } + }; +} + // Plan limits by Figma subscription tier (verified from Figma documentation) const PLAN_LIMITS: Record> = { starter: { @@ -349,6 +584,19 @@ const PLAN_LIMITS: Record> = { // Maximum variables per collection (all plans) const MAX_VARIABLES_PER_COLLECTION = 5000; +// Batch sizing + throttling config for heavy-load handling (QuickJS sandbox) +const BATCH = { + SYNC_CREATE: 50, + SYNC_LIGHT: 200, + ASYNC_LOOKUP: 50, + ASYNC_LIBRARY: 10, + ASYNC_FONT: 5, + SEQ_EXPORT: 25, + PROGRESS_MIN_MS: 250, + EXPORT_CHUNK_BYTES: 262144, + EXPORT_YIELD_EVERY: 8 +} as const; + // Plan detection: Figma API doesn't expose plan directly, so we infer from existing modes async function detectCurrentPlan(): Promise { const collections = await figma.variables.getLocalVariableCollectionsAsync(); @@ -1237,62 +1485,104 @@ class VariableCache { private libraryVariableMap = new Map(); // Library/remote variables private libraryCollectionNames = new Set(); // Names of connected library collections private initialized = false; + private libraryIndexed = false; - async initialize(): Promise { + // Lazy readiness: build the local index once if not already done. + async ensureReady(): Promise { if (this.initialized) return; - await this.rebuild(); + await this.rebuildLocal(); this.initialized = true; } + // Back-compat: external callers still call initialize(). Delegates to ensureReady(). + async initialize(): Promise { + await this.ensureReady(); + } + + // Full rebuild for callers that need both local and library indexes. async rebuild(): Promise { - this.collectionMap.clear(); - this.variableMap.clear(); - this.libraryVariableMap.clear(); - this.libraryCollectionNames.clear(); - - // Index local collections and variables - for (const col of await figma.variables.getLocalVariableCollectionsAsync()) { + await this.rebuildLocal(); + await this.ensureLibraryIndex(); + this.initialized = true; + } + + // Rebuild ONLY the local collection + variable index. No library indexing. + async rebuildLocal(): Promise { + this.clearLocal(); + + const collections = await figma.variables.getLocalVariableCollectionsAsync(); + for (let c = 0; c < collections.length; c++) { + const col = collections[c]; this.collectionMap.set(col.name, col); - for (const varId of col.variableIds) { - const v = await figma.variables.getVariableByIdAsync(varId); + + // Batch the per-variable async lookups instead of awaiting one at a time. + const ids = col.variableIds; + const resolved = await runBatchedAsync( + ids, + BATCH.ASYNC_LOOKUP, + function (varId: string): Promise { + return figma.variables.getVariableByIdAsync(varId); + } + ); + for (let i = 0; i < resolved.length; i++) { + const v = resolved[i]; if (v) { this.variableMap.set(`${col.name}/${v.name}`, v); } } } - - // Also index library/remote variables that are available in this file + } + + // Build the library/remote variable index once. Idempotent. + async ensureLibraryIndex(): Promise { + if (this.libraryIndexed) return; await this.indexLibraryVariables(); + this.libraryIndexed = true; + } + + // Synchronously clear only the local collection + variable maps. + clearLocal(): void { + this.collectionMap.clear(); + this.variableMap.clear(); } // Index library variables from connected team libraries private async indexLibraryVariables(): Promise { + this.libraryVariableMap.clear(); + this.libraryCollectionNames.clear(); try { // Get all library variable collections available to this file const libraryCollections = await figma.teamLibrary.getAvailableLibraryVariableCollectionsAsync(); - - for (const libCol of libraryCollections) { + + for (let c = 0; c < libraryCollections.length; c++) { + const libCol = libraryCollections[c]; this.libraryCollectionNames.add(libCol.name); - + // Get variables in this library collection try { const libraryVars = await figma.teamLibrary.getVariablesInLibraryCollectionAsync(libCol.key); - for (const libVar of libraryVars) { - // Import the variable so we can reference it by ID - try { - const importedVar = await figma.variables.importVariableByKeyAsync(libVar.key); - if (importedVar) { - this.libraryVariableMap.set(`${libCol.name}/${importedVar.name}`, importedVar); + // Import variables in batches so a large library does not block the host. + const libColName = libCol.name; + await runBatchedAsync( + libraryVars, + BATCH.ASYNC_LIBRARY, + async (libVar): Promise => { + // Import the variable so we can reference it by ID + try { + const importedVar = await figma.variables.importVariableByKeyAsync(libVar.key); + if (importedVar) { + this.libraryVariableMap.set(`${libColName}/${importedVar.name}`, importedVar); + } + } catch (importErr) { + // Individual variable import failure - skip } - } catch (importErr) { - // Individual variable import failure - skip } - } + ); } catch (e) { Logger.log(` ⚠️ Could not index library collection "${libCol.name}": ${e}`); } } - + if (this.libraryCollectionNames.size > 0) { Logger.log(`📚 Indexed ${this.libraryVariableMap.size} library variables from ${this.libraryCollectionNames.size} connected libraries`); } @@ -1501,10 +1791,11 @@ const ColorStyleProcessor: StyleProcessor = { async export(options?: { includeImages?: boolean }): Promise { const includeImages = options?.includeImages ?? false; const styles: ExportColorStyle[] = []; - - for (const style of await figma.getLocalPaintStylesAsync()) { - if (style.paints.length === 0) continue; - + + const localPaintStyles = await figma.getLocalPaintStylesAsync(); + await runSequentialAsync(localPaintStyles, 20, async function (style: PaintStyle): Promise { + if (style.paints.length === 0) return; + const exportPaints: ExportPaintData[] = []; let primaryColor: ExportColorValue | undefined; let primaryOpacity: number | undefined; @@ -1605,8 +1896,8 @@ const ColorStyleProcessor: StyleProcessor = { } } - if (exportPaints.length === 0) continue; - + if (exportPaints.length === 0) return; + const colorStyle: ExportColorStyle = { name: style.name, paints: exportPaints, @@ -1616,10 +1907,10 @@ const ColorStyleProcessor: StyleProcessor = { ...(style.description && { description: style.description }), ...(boundVars && Object.keys(boundVars).length > 0 && { boundVariables: boundVars }) }; - + styles.push(colorStyle); - } - + }); + return styles; }, @@ -1631,10 +1922,10 @@ const ColorStyleProcessor: StyleProcessor = { for (const s of await figma.getLocalPaintStylesAsync()) { existing.set(s.name, s); } - - for (const colorStyle of styles) { + + await runSequentialAsync(styles, 20, async function (colorStyle: ExportColorStyle): Promise { let style: PaintStyle; - + if (existing.has(colorStyle.name)) { style = existing.get(colorStyle.name)!; updated++; @@ -1790,8 +2081,8 @@ const ColorStyleProcessor: StyleProcessor = { if (paints.length > 0) { style.paints = paints; } - } - + }); + return { created, updated }; } }; @@ -1800,8 +2091,9 @@ const ColorStyleProcessor: StyleProcessor = { const TextStyleProcessor: StyleProcessor = { async export(_options?: { includeImages?: boolean }): Promise { const styles: ExportTextStyle[] = []; - - for (const style of await figma.getLocalTextStylesAsync()) { + + const localTextStyles = await figma.getLocalTextStylesAsync(); + await runSequentialAsync(localTextStyles, 20, async function (style: TextStyle): Promise { const textStyle: ExportTextStyle = { name: style.name, fontFamily: style.fontName.family, @@ -1814,10 +2106,10 @@ const TextStyleProcessor: StyleProcessor = { ...(style.description && { description: style.description }), boundVariables: await extractBindings(style.boundVariables as Record, ['fontSize', 'lineHeight', 'letterSpacing', 'paragraphSpacing', 'paragraphIndent']) }; - + styles.push(textStyle); - } - + }); + return styles; }, @@ -1829,10 +2121,10 @@ const TextStyleProcessor: StyleProcessor = { for (const s of await figma.getLocalTextStylesAsync()) { existing.set(s.name, s); } - - for (const textStyle of styles) { + + await runSequentialAsync(styles, 20, async function (textStyle: ExportTextStyle): Promise { let style: TextStyle; - + if (existing.has(textStyle.name)) { style = existing.get(textStyle.name)!; updated++; @@ -1870,8 +2162,8 @@ const TextStyleProcessor: StyleProcessor = { } catch (e) { Logger.log(`⚠️ Could not load font for ${textStyle.name}: ${e}`); } - } - + }); + return { created, updated }; } }; @@ -1880,8 +2172,9 @@ const TextStyleProcessor: StyleProcessor = { const EffectStyleProcessor: StyleProcessor = { async export(_options?: { includeImages?: boolean }): Promise { const styles: ExportEffectStyle[] = []; - - for (const style of await figma.getLocalEffectStylesAsync()) { + + const localEffectStyles = await figma.getLocalEffectStylesAsync(); + await runSequentialAsync(localEffectStyles, 20, async function (style: EffectStyle): Promise { const effects: ExportEffectData[] = []; for (const effect of style.effects) { const effectData: ExportEffectData = { @@ -1903,10 +2196,10 @@ const EffectStyleProcessor: StyleProcessor = { ...(style.description && { description: style.description }), effects }; - + styles.push(effectStyle); - } - + }); + return styles; }, @@ -1918,10 +2211,10 @@ const EffectStyleProcessor: StyleProcessor = { for (const s of await figma.getLocalEffectStylesAsync()) { existing.set(s.name, s); } - - for (const effectStyle of styles) { + + await runSequentialAsync(styles, 20, async function (effectStyle: ExportEffectStyle): Promise { let style: EffectStyle; - + if (existing.has(effectStyle.name)) { style = existing.get(effectStyle.name)!; updated++; @@ -1976,8 +2269,8 @@ const EffectStyleProcessor: StyleProcessor = { } } } - } - + }); + return { created, updated }; } }; @@ -1986,8 +2279,9 @@ const EffectStyleProcessor: StyleProcessor = { const GridStyleProcessor: StyleProcessor = { async export(_options?: { includeImages?: boolean }): Promise { const styles: ExportGridStyle[] = []; - - for (const style of await figma.getLocalGridStylesAsync()) { + + const localGridStyles = await figma.getLocalGridStylesAsync(); + await runSequentialAsync(localGridStyles, 20, async function (style: GridStyle): Promise { const layoutGrids: ExportGridData[] = []; for (const grid of style.layoutGrids) { const gridColor = grid.color as RGBA; @@ -2013,10 +2307,10 @@ const GridStyleProcessor: StyleProcessor = { ...(style.description && { description: style.description }), layoutGrids }; - + styles.push(gridStyle); - } - + }); + return styles; }, @@ -2028,10 +2322,10 @@ const GridStyleProcessor: StyleProcessor = { for (const s of await figma.getLocalGridStylesAsync()) { existing.set(s.name, s); } - - for (const gridStyle of styles) { + + await runSequentialAsync(styles, 20, async function (gridStyle: ExportGridStyle): Promise { let style: GridStyle; - + if (existing.has(gridStyle.name)) { style = existing.get(gridStyle.name)!; updated++; @@ -2112,8 +2406,8 @@ const GridStyleProcessor: StyleProcessor = { } } } - } - + }); + return { created, updated }; } }; @@ -2456,11 +2750,21 @@ async function exportVariables( collections = collections.filter(c => selectedCollections.includes(c.name)); Logger.log(`Filtering to ${collections.length} selected collections`); } - + const exportData: ExportFormat = []; const w3cExportData: Record = {}; let totalVariables = 0; - + + // Progress + heavy-load handling: compute grand total of variables to export + // (sum of filtered collection variable counts) and keep a running count so + // the UI can show determinate progress across collections. + const progress = createProgress('export'); + let grandTotal = 0; + for (let gc = 0; gc < collections.length; gc++) { + grandTotal += collections[gc].variableIds.length; + } + let processedVars = 0; + for (const collection of collections) { Logger.log(`Processing collection: ${collection.name}`); @@ -2490,15 +2794,20 @@ async function exportVariables( // We'll handle original mode names in metadata if needed } - // Process variables - for (const variableId of collection.variableIds) { + // Process variables sequentially (shared nested structure mutation), batched + // with periodic yields so the UI can repaint on large collections. + await runSequentialAsync( + collection.variableIds, + BATCH.SEQ_EXPORT, + async function (variableId: string): Promise { + processedVars++; const variable = await figma.variables.getVariableByIdAsync(variableId); - if (!variable) continue; + if (!variable) return; // Group filtering (Simple mode): key absent for a collection => export ALL its variables if (selectedGroups && selectedGroups[collection.name]) { if (selectedGroups[collection.name].indexOf(getGroupKey(variable.name)) === -1) { - continue; + return; } } @@ -2595,8 +2904,12 @@ async function exportVariables( current[leafName] = varExport; } - } - + }, + function (): void { + progress.report('export_variables', 'Exporting variables', processedVars, grandTotal); + } + ); + exportData.push(collectionExport); // Also build W3C format if needed @@ -2615,6 +2928,7 @@ async function exportVariables( if (styleOptions) { stylesExported = {}; if (styleOptions.colorStyles) { + progress.report('export_styles', 'Exporting styles', 0, 0, true); const colorAllowed = selectedStyleGroups ? selectedStyleGroups.color : undefined; stylesExported.colorStyles = filterStylesByGroup(await ColorStyleProcessor.export({ includeImages }), colorAllowed); if (colorAllowed && stylesExported.colorStyles.length === 0) { @@ -2622,6 +2936,7 @@ async function exportVariables( } } if (styleOptions.textStyles) { + progress.report('export_styles', 'Exporting styles', 0, 0, true); const textAllowed = selectedStyleGroups ? selectedStyleGroups.text : undefined; stylesExported.textStyles = filterStylesByGroup(await TextStyleProcessor.export(), textAllowed); if (textAllowed && stylesExported.textStyles.length === 0) { @@ -2629,6 +2944,7 @@ async function exportVariables( } } if (styleOptions.effectStyles) { + progress.report('export_styles', 'Exporting styles', 0, 0, true); const effectAllowed = selectedStyleGroups ? selectedStyleGroups.effect : undefined; stylesExported.effectStyles = filterStylesByGroup(await EffectStyleProcessor.export(), effectAllowed); if (effectAllowed && stylesExported.effectStyles.length === 0) { @@ -2636,6 +2952,7 @@ async function exportVariables( } } if (styleOptions.gridStyles) { + progress.report('export_styles', 'Exporting styles', 0, 0, true); const gridAllowed = selectedStyleGroups ? selectedStyleGroups.grid : undefined; stylesExported.gridStyles = filterStylesByGroup(await GridStyleProcessor.export(), gridAllowed); if (gridAllowed && stylesExported.gridStyles.length === 0) { @@ -2681,18 +2998,75 @@ async function exportVariables( Logger.log(`✅ Export complete: ${stats.collections} collections, ${stats.variables} variables`); } - Logger.send('export_complete', { - data: outputData, - stats, - format: exportFormat - }); - + await sendExportInChunks(outputData, stats, exportFormat); + } catch (e) { + if (isCancelError(e)) { + Logger.log('🛑 Export cancelled'); + figma.ui.postMessage({ + type: 'operation_cancelled', + operation: 'export', + phase: 'export', + rolledBack: false, + message: 'Export cancelled. Nothing was changed.' + }); + return; + } Logger.log(`❌ Export error: ${e}`); Logger.send('error', { message: `Export failed: ${e}` }); } } +// Deliver a large export payload to the UI in size-bounded chunks so a single +// postMessage never has to serialize a multi-megabyte string at once. Splits on +// byte budget (BATCH.EXPORT_CHUNK_BYTES) but never mid surrogate pair, yields to +// the host every BATCH.EXPORT_YIELD_EVERY chunks, then posts a final summary. +async function sendExportInChunks( + outputData: string, + stats: ExportStats, + format: string +): Promise { + const len = outputData.length; + const size = BATCH.EXPORT_CHUNK_BYTES; + const total = Math.max(1, Math.ceil(len / size)); + let seq = 0; + let start = 0; + while (start < len) { + let end = Math.min(start + size, len); + // Don't split in the middle of a surrogate pair: if the last code unit in + // this slice is a high surrogate, extend the boundary by one. + if (end < len) { + const lastCode = outputData.charCodeAt(end - 1); + if (lastCode >= 0xD800 && lastCode <= 0xDBFF) { + end += 1; + } + } + const piece = outputData.slice(start, end); + figma.ui.postMessage({ + type: 'export_chunk', + seq: seq, + total: total, + data: piece + }); + seq++; + start = end; + if (seq % BATCH.EXPORT_YIELD_EVERY === 0 && start < len) { + await yieldToHost(); + } + } + figma.ui.postMessage({ + type: 'export_done', + stats: stats, + format: format, + // Report the ACTUAL number of chunks emitted, not the precomputed estimate. + // Surrogate-boundary extension can make the real count differ from + // ceil(len/size); the UI integrity check (chunks.length === chunkCount) + // must compare against what was actually sent. + chunkCount: seq, + totalLength: len + }); +} + // ============================================================================ // SECTION 12: IMPORT ORCHESTRATOR // ============================================================================ @@ -2711,7 +3085,12 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis Logger.log(`⚠️ Could not create pre-import snapshot: ${snapshotError}`); // Continue without snapshot - user will be warned if import fails } - + + // Heavy-load handling: throttled progress + a mutation-started flag so the + // cancel/rollback paths know whether the file was actually touched yet. + const progress = createProgress('import'); + let mutationStarted = false; + try { let parsedData = JSON.parse(jsonData); @@ -2748,46 +3127,17 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis importData = parsedData as ExportFormat; } - // Handle Clean Import: clear everything first - if (options.clearFirst) { - Logger.log('🧹 Clean Import: Clearing existing variables and styles...'); - await clearAll(); - Logger.log('✅ Clean Import: Clearing complete, rebuilding cache...'); - // Reinitialize cache after clearing - await variableCache.rebuild(); - Logger.log('✅ Clean Import: Cache rebuilt, proceeding with import...'); - } - - // Handle Custom Merge: selectively clear variables and/or styles - if (options.customMerge) { - const { clearVariables: shouldClearVars, clearStyles: shouldClearStyles } = options.customMerge; - if (shouldClearVars && shouldClearStyles) { - Logger.log('🎯 Custom Merge: Clearing both variables and styles...'); - await clearAll(); - } else if (shouldClearVars) { - Logger.log('🎯 Custom Merge: Clearing variables only...'); - await clearVariables(); - } else if (shouldClearStyles) { - Logger.log('🎯 Custom Merge: Clearing styles only...'); - await clearStyles(); - } - Logger.log('✅ Custom Merge: Clearing complete, rebuilding cache...'); - await variableCache.rebuild(); - } - - await variableCache.initialize(); - let createdCollections = 0; let createdVariables = 0; let updatedVariables = 0; let skippedVariables = 0; let stylesCreated = 0; let stylesUpdated = 0; - + // Separate styles from collections let stylesData: StylesExport | null = null; const collectionData: CollectionExport[] = []; - + for (const item of importData) { const keys = Object.keys(item); if (keys.length === 1 && keys[0] === '_styles') { @@ -2796,7 +3146,77 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis collectionData.push(item as CollectionExport); } } - + + // PRE-PASS: flatten each collection's first mode exactly once (reused in pass 1), + // sum the grand total for determinate progress, and detect whether any value + // references the team library so we only index it when actually needed. + const preflattened: Array<{ + collectionObj: CollectionExport; + flatPaths: FlatVariable[]; + }> = []; + let importGrandTotal = 0; + let needsLibraryIndex = false; + for (let pc = 0; pc < collectionData.length; pc++) { + const collectionObj = collectionData[pc]; + const jsonName = Object.keys(collectionObj)[0]; + const content = collectionObj[jsonName]; + const modeKeys = Object.keys(content.modes); + const firstMode = modeKeys.length > 0 ? content.modes[modeKeys[0]] : {}; + const flatPaths = flattenVariables(firstMode, ''); + preflattened.push({ collectionObj: collectionObj, flatPaths: flatPaths }); + importGrandTotal += flatPaths.length; + + // Scan for library needs: a $libraryRef, or an alias pointing at a + // collection name that is not one of the collections in this import file. + for (let fp = 0; fp < flatPaths.length; fp++) { + const v = flatPaths[fp].value; + if (v.$libraryRef) { + needsLibraryIndex = true; + } else if (v.$collectionName && v.$collectionName !== (content.$originalName || jsonName)) { + needsLibraryIndex = true; + } + } + } + + // FIRST MUTATION BOUNDARY: everything above is read-only. Commit an undo + // checkpoint immediately before the first mutating step so a single Figma + // undo reverts the whole import as one unit. + mutationStarted = true; + figma.commitUndo(); + + // Handle Clean Import: clear everything first (silent — internal step of the + // import's own undo boundary; cancellation rethrows into the rollback path). + if (options.clearFirst) { + Logger.log('🧹 Clean Import: Clearing existing variables and styles...'); + await clearAll(true); + Logger.log('✅ Clean Import: Clearing complete...'); + } + + // Handle Custom Merge: selectively clear variables and/or styles (silent). + if (options.customMerge) { + const { clearVariables: shouldClearVars, clearStyles: shouldClearStyles } = options.customMerge; + if (shouldClearVars && shouldClearStyles) { + Logger.log('🎯 Custom Merge: Clearing both variables and styles...'); + await clearAll(true); + } else if (shouldClearVars) { + Logger.log('🎯 Custom Merge: Clearing variables only...'); + await clearVariables(true); + } else if (shouldClearStyles) { + Logger.log('🎯 Custom Merge: Clearing styles only...'); + await clearStyles(true); + } + Logger.log('✅ Custom Merge: Clearing complete...'); + } + + // Build the local cache index once (picks up any state after clearing). + // Index the team library only when the pre-pass found library refs or the + // caller explicitly opted in via useLibraryRefs. + progress.report('cache_scan', 'Scanning existing variables', 0, 0, true); + await variableCache.rebuildLocal(); + if (needsLibraryIndex || options.useLibraryRefs) { + await variableCache.ensureLibraryIndex(); + } + // Collect all pending aliases across all collections for pass 2 const allPendingAliases: Array<{ variable: Variable; @@ -2805,13 +3225,17 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis aliasCollection: string; fallbackValue: ExportVariableValue; }> = []; - + + let processedTotal = 0; + // Process collections - PASS 1: Create variables with raw values Logger.log(`📥 Pass 1: Processing ${collectionData.length} collections...`); - for (const collectionObj of collectionData) { + for (let pidx = 0; pidx < preflattened.length; pidx++) { + const collectionObj = preflattened[pidx].collectionObj; + const variablePaths = preflattened[pidx].flatPaths; const jsonCollectionName = Object.keys(collectionObj)[0]; const collectionContent = collectionObj[jsonCollectionName]; - + // Use $originalName if present (for round-trip with code-friendly naming) // This restores original Figma names when importing JSON that was exported with naming conventions const collectionName = collectionContent.$originalName || jsonCollectionName; @@ -2883,9 +3307,8 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis // Process variables - TWO PASS APPROACH // Pass 1: Create all variables and set RAW values only (skip aliases) // Pass 2: Set ALIAS values (now all target variables exist) - const firstModeVars = collectionContent.modes[modeNames[0]]; - const variablePaths = flattenVariables(firstModeVars, ''); - + // variablePaths reuses the pre-pass flatten — no redundant re-flatten here. + // Store pending alias assignments for pass 2 const pendingAliases: Array<{ variable: Variable; @@ -2894,19 +3317,24 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis aliasCollection: string; fallbackValue: ExportVariableValue; }> = []; - - // PASS 1: Create variables and set raw values + + // PASS 1: Create variables and set raw values (batched + yielded) Logger.log(` Pass 1: Creating variables with raw values...`); - for (const { path, value } of variablePaths) { + await runBatched( + variablePaths, + BATCH.SYNC_CREATE, + function (entry: FlatVariable): void { + const path = entry.path; + const value = entry.value; const fullPath = `${collectionName}/${path}`; - + let variable: Variable; const existingVar = variableCache.getVariable(fullPath); - + if (existingVar) { if (!options.overwrite) { skippedVariables++; - continue; + return; } variable = existingVar; updatedVariables++; @@ -2920,26 +3348,26 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis createdVariables++; } catch (e) { Logger.log(` ⚠️ Could not create variable ${path}: ${e}`); - continue; + return; } } - + if (value.$description) { variable.description = value.$description; } - + try { variable.scopes = TypeMapper.arrayToScopes(value.$scopes as string[]); } catch { /* Skip */ } - + // Set values for each mode - raw values only in pass 1, queue aliases for pass 2 for (const modeName of modeNames) { const modeId = modeMap.get(modeName); if (!modeId) continue; - + const modeValue = getValueAtPath(collectionContent.modes[modeName], path); if (!modeValue) continue; - + if (typeof modeValue.$value === 'string' && modeValue.$value.startsWith('{')) { // This is an alias - queue for pass 2 const aliasPath = modeValue.$value.slice(1, -1).replace(/\./g, '/'); @@ -2958,24 +3386,41 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis setRawValue(variable, modeId, modeValue); } } - + variableCache.setVariable(fullPath, variable); - } - + }, + function (done: number): void { + progress.report('import_create', 'Importing variables', processedTotal + done, importGrandTotal); + } + ); + processedTotal += variablePaths.length; + // Store pending aliases for this collection (will be processed after all collections) allPendingAliases.push(...pendingAliases); } - - // PASS 2: Resolve all aliases (now all variables from all collections exist) + + // PASS 2: Resolve all aliases (now all variables from all collections exist). + // Aliases run strictly AFTER all raw values are in place. No cache rebuild + // needed: the initial cache_scan indexed pre-existing variables and Pass 1 + // registered every created variable/collection under its JSON-path key — + // exactly the key space the alias lookups below use. Logger.log(`📥 Pass 2: Resolving ${allPendingAliases.length} alias references...`); - await variableCache.rebuild(); // Ensure cache has all newly created variables - + let aliasesResolved = 0; let aliasesFailed = 0; - - for (const pending of allPendingAliases) { + + await runBatched( + allPendingAliases, + BATCH.SYNC_LIGHT, + function (pending: { + variable: Variable; + modeId: string; + aliasPath: string; + aliasCollection: string; + fallbackValue: ExportVariableValue; + }): void { const targetVar = variableCache.getVariable(`${pending.aliasCollection}/${pending.aliasPath}`); - + if (targetVar) { try { pending.variable.setValueForMode(pending.modeId, { type: 'VARIABLE_ALIAS', id: targetVar.id }); @@ -2990,17 +3435,21 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis aliasesFailed++; Logger.log(` ⚠️ Alias target not found: ${pending.aliasCollection}/${pending.aliasPath}`); } - } - + }, + function (done: number, total: number): void { + progress.report('import_aliases', 'Linking aliases', done, total); + } + ); + if (allPendingAliases.length > 0) { Logger.log(` ✅ Aliases: ${aliasesResolved} resolved, ${aliasesFailed} used fallback values`); } - + // Import styles if (stylesData && options.importStyles) { Logger.log('📦 Importing styles...'); - await variableCache.rebuild(); - + progress.report('import_styles', 'Importing styles', 0, 0, true); + if (stylesData.colorStyles) { const r = await ColorStyleProcessor.importStyles(stylesData.colorStyles, variableCache); stylesCreated += r.created; @@ -3022,7 +3471,7 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis stylesUpdated += r.updated; } } - + const stats: ImportStats = { collectionsCreated: createdCollections, variablesCreated: createdVariables, @@ -3031,30 +3480,63 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis stylesCreated, stylesUpdated }; - + + // Close the undo boundary AFTER the import fully completes so the whole + // import collapses into a single undo step. + figma.commitUndo(); + Logger.log(`✅ Import complete!`); - Logger.send('import_complete', { stats }); - + Logger.send('import_complete', { stats, snapshot: preImportSnapshot }); + } catch (e) { + const cancelled = isCancelError(e); + + // Cancelled before any mutation: nothing to roll back. + if (cancelled && !mutationStarted) { + Logger.log('🛑 Import cancelled before any changes were made'); + figma.ui.postMessage({ + type: 'operation_cancelled', + operation: 'import', + phase: 'snapshot', + rolledBack: false, + message: 'Import cancelled. No changes were made.' + }); + return; + } + const errorMessage = e instanceof Error ? e.message : String(e); - Logger.log(`❌ Import error: ${errorMessage}`); - + if (!cancelled) { + Logger.log(`❌ Import error: ${errorMessage}`); + } else { + Logger.log('🛑 Import cancelled after mutation started — rolling back...'); + } + // Automatic rollback if we have a pre-import snapshot if (preImportSnapshot) { Logger.log('🔄 Attempting automatic rollback to pre-import state...'); Logger.send('import_rolling_back', { error: errorMessage }); - + try { await restoreFromSnapshot(preImportSnapshot); Logger.log('✅ Automatic rollback successful - file restored to pre-import state'); - Logger.send('import_rollback_complete', { - error: errorMessage, - message: 'Import failed but your file has been automatically restored to its previous state.' - }); + if (cancelled) { + figma.ui.postMessage({ + type: 'operation_cancelled', + operation: 'import', + phase: 'rollback', + rolledBack: true, + message: 'Import cancelled — your file was restored to its previous state.' + }); + } else { + Logger.send('import_rollback_complete', { + error: errorMessage, + message: 'Import failed but your file has been automatically restored to its previous state.' + }); + } } catch (rollbackError) { const rollbackErrorMsg = rollbackError instanceof Error ? rollbackError.message : String(rollbackError); Logger.log(`❌ Rollback failed: ${rollbackErrorMsg}`); - Logger.send('import_rollback_failed', { + Logger.send('import_rollback_failed', { error: errorMessage, rollbackError: rollbackErrorMsg, message: 'Import failed and automatic rollback also failed. Please use Ctrl+Z (Cmd+Z) to undo manually.' @@ -3062,7 +3544,7 @@ async function importVariables(jsonData: string, options: ImportOptions): Promis } } else { // No snapshot available - just report the error - Logger.send('error', { + Logger.send('error', { message: `Import failed: ${errorMessage}. Use Ctrl+Z (Cmd+Z) to undo changes.` }); } @@ -3161,43 +3643,76 @@ async function getCollections(): Promise { let localAliases = 0; let libraryAliases = 0; - // Process sequentially to preserve exact order + // Memoize aliased-target collections so we never re-fetch the same collection + // once per alias. Maps variableCollectionId -> { name, remote }. + const aliasCollectionMemo = new Map(); + + // Process sequentially to preserve exact order. Per collection we prefetch the + // variable objects in async chunks (yielding between chunks) instead of a + // serial getVariableByIdAsync per id. const data = []; for (let index = 0; index < collections.length; index++) { const c = collections[index]; const types = { color: 0, float: 0, boolean: 0, string: 0 }; const variableNames: string[] = []; - for (const varId of c.variableIds) { - const variable = await figma.variables.getVariableByIdAsync(varId); - if (variable) { - variableNames.push(variable.name); - const typeStr = TypeMapper.toExportType(variable.resolvedType); - types[typeStr as keyof typeof types]++; - - // Check for aliases in all modes - for (const modeId of Object.keys(variable.valuesByMode)) { - const value = variable.valuesByMode[modeId]; - if (typeof value === 'object' && value !== null && 'type' in value && value.type === 'VARIABLE_ALIAS') { - totalAliases++; - const aliasedVar = await figma.variables.getVariableByIdAsync(value.id); - if (aliasedVar) { - const aliasedCollection = await figma.variables.getVariableCollectionByIdAsync(aliasedVar.variableCollectionId); - if (aliasedCollection) { - // Check if it's from a remote/library collection - if (aliasedCollection.remote) { - libraryDependencies.add(aliasedCollection.name); - libraryAliases++; - } else { - localAliases++; - } - } - } - } + // Batch-resolve all variables in this collection. + const variables = await runBatchedAsync( + c.variableIds, + BATCH.ASYNC_LOOKUP, + function (varId: string): Promise { + return figma.variables.getVariableByIdAsync(varId); + } + ); + + // First pass over resolved variables: counts, names, and collect alias + // target ids for a second batched lookup. + const aliasTargetIds: string[] = []; + for (let vi = 0; vi < variables.length; vi++) { + const variable = variables[vi]; + if (!variable) continue; + variableNames.push(variable.name); + const typeStr = TypeMapper.toExportType(variable.resolvedType); + types[typeStr as keyof typeof types]++; + + const modeKeys = Object.keys(variable.valuesByMode); + for (let mk = 0; mk < modeKeys.length; mk++) { + const value = variable.valuesByMode[modeKeys[mk]]; + if (typeof value === 'object' && value !== null && 'type' in value && value.type === 'VARIABLE_ALIAS') { + totalAliases++; + aliasTargetIds.push((value as VariableAlias).id); } } } - + + // Batch-resolve the aliased variables, then deref each one's collection via + // the memo (single fetch per unseen collection id). + const aliasedVars = await runBatchedAsync( + aliasTargetIds, + BATCH.ASYNC_LOOKUP, + function (id: string): Promise { + return figma.variables.getVariableByIdAsync(id); + } + ); + for (let av = 0; av < aliasedVars.length; av++) { + const aliasedVar = aliasedVars[av]; + if (!aliasedVar) continue; + const collId = aliasedVar.variableCollectionId; + let memo = aliasCollectionMemo.get(collId); + if (!memo) { + const aliasedCollection = await figma.variables.getVariableCollectionByIdAsync(collId); + if (!aliasedCollection) continue; + memo = { name: aliasedCollection.name, remote: aliasedCollection.remote }; + aliasCollectionMemo.set(collId, memo); + } + if (memo.remote) { + libraryDependencies.add(memo.name); + libraryAliases++; + } else { + localAliases++; + } + } + data.push({ id: c.id, name: c.name, @@ -3300,104 +3815,299 @@ async function getCollections(): Promise { // SECTION 14: CLEAR FUNCTIONS // ============================================================================ -async function clearVariables(): Promise { +// silent: when true, suppress the user-facing clear_complete/error postMessages +// (used by restore, where the clear is an internal step of a larger operation). +async function clearVariables(silent: boolean = false): Promise { Logger.log('🗑️ Clearing all variables...'); - + + // Progress only surfaces for standalone (non-silent) clears; internal callers + // (restore, clean import) drive their own progress reporters. + const progress = silent ? null : createProgress('clear'); + let deletedCollections = 0; + let deletedVariables = 0; + let committed = false; + try { - let deletedCollections = 0; - let deletedVariables = 0; - - for (const collection of await figma.variables.getLocalVariableCollectionsAsync()) { - for (const varId of collection.variableIds) { - const variable = await figma.variables.getVariableByIdAsync(varId); - if (variable) { - variable.remove(); - deletedVariables++; + // Gather all collections + their variable ids up front so we have a grand + // total for determinate progress. + const collections = await figma.variables.getLocalVariableCollectionsAsync(); + const gathered: Array<{ collection: VariableCollection; ids: string[] }> = []; + let total = 0; + for (let i = 0; i < collections.length; i++) { + const ids = collections[i].variableIds; + gathered.push({ collection: collections[i], ids: ids }); + total += ids.length; + } + + // Standalone clears get a Figma undo checkpoint before the first deletion so + // a single Cmd+Z reverts the whole clear. Internal/silent clears participate + // in their caller's undo boundary instead. + if (!silent && total > 0) { + figma.commitUndo(); + committed = true; + } + + let done = 0; + for (let g = 0; g < gathered.length; g++) { + const entry = gathered[g]; + // Resolve the variable objects for this collection in async chunks. + const resolved = await runBatchedAsync( + entry.ids, + BATCH.ASYNC_LOOKUP, + function (id: string): Promise { + return figma.variables.getVariableByIdAsync(id); } - } - collection.remove(); + ); + // Remove the resolved variables in light synchronous batches. + await runBatched( + resolved, + BATCH.SYNC_LIGHT, + function (variable: Variable | null): void { + if (variable) { + variable.remove(); + deletedVariables++; + } + }, + function (batchDone: number): void { + if (progress) { + progress.report('clear', 'Deleting variables', done + batchDone, total); + } + } + ); + done += entry.ids.length; + entry.collection.remove(); deletedCollections++; } - + + variableCache.clearLocal(); + + if (!silent && committed) { + figma.commitUndo(); + } + Logger.log(`✅ Cleared ${deletedCollections} collections, ${deletedVariables} variables`); - Logger.send('clear_complete', { message: `${deletedCollections} collections, ${deletedVariables} variables` }); + if (!silent) { + Logger.send('clear_complete', { message: `${deletedCollections} collections, ${deletedVariables} variables` }); + } } catch (e) { + // Internal/silent clears must rethrow cancellation so the caller's rollback + // path runs; only standalone clears translate it into a user message. + if (isCancelError(e)) { + if (silent) { + throw e; + } + Logger.log(`🛑 Clear cancelled after ${deletedVariables} variables in ${deletedCollections} collections`); + figma.ui.postMessage({ + type: 'operation_cancelled', + operation: 'clear', + phase: 'clear', + rolledBack: false, + partial: { collectionsDeleted: deletedCollections, variablesDeleted: deletedVariables }, + message: `Clear cancelled — ${deletedVariables} variables in ${deletedCollections} collections were already deleted. Remaining items were not touched. Use Cmd+Z to restore deleted items.` + }); + return; + } Logger.log(`❌ Clear variables error: ${e}`); - Logger.send('error', { message: `Failed to clear variables: ${e}` }); + if (!silent) { + Logger.send('error', { message: `Failed to clear variables: ${e}` }); + } else { + throw e; + } } } -async function clearStyles(): Promise { +async function clearStyles(silent: boolean = false): Promise { Logger.log('🗑️ Clearing all styles...'); - + + const progress = silent ? null : createProgress('clear'); + let deletedStyles = 0; + let committed = false; + try { - let deletedStyles = 0; - - for (const style of await figma.getLocalPaintStylesAsync()) { style.remove(); deletedStyles++; } - for (const style of await figma.getLocalTextStylesAsync()) { style.remove(); deletedStyles++; } - for (const style of await figma.getLocalEffectStylesAsync()) { style.remove(); deletedStyles++; } - for (const style of await figma.getLocalGridStylesAsync()) { style.remove(); deletedStyles++; } - + const paintStyles = await figma.getLocalPaintStylesAsync(); + const textStyles = await figma.getLocalTextStylesAsync(); + const effectStyles = await figma.getLocalEffectStylesAsync(); + const gridStyles = await figma.getLocalGridStylesAsync(); + const total = paintStyles.length + textStyles.length + effectStyles.length + gridStyles.length; + + if (!silent && total > 0) { + figma.commitUndo(); + committed = true; + } + + let done = 0; + const removeStyle = function (style: BaseStyle): void { + style.remove(); + deletedStyles++; + }; + const onBatch = function (batchDone: number): void { + if (progress) { + progress.report('clear', 'Deleting styles', done + batchDone, total); + } + }; + + await runBatched(paintStyles, BATCH.SYNC_LIGHT, removeStyle, onBatch); + done += paintStyles.length; + await runBatched(textStyles, BATCH.SYNC_LIGHT, removeStyle, onBatch); + done += textStyles.length; + await runBatched(effectStyles, BATCH.SYNC_LIGHT, removeStyle, onBatch); + done += effectStyles.length; + await runBatched(gridStyles, BATCH.SYNC_LIGHT, removeStyle, onBatch); + done += gridStyles.length; + + if (!silent && committed) { + figma.commitUndo(); + } + Logger.log(`✅ Cleared ${deletedStyles} styles`); - Logger.send('clear_complete', { message: `${deletedStyles} styles` }); + if (!silent) { + Logger.send('clear_complete', { message: `${deletedStyles} styles` }); + } } catch (e) { + if (isCancelError(e)) { + if (silent) { + throw e; + } + Logger.log(`🛑 Clear styles cancelled after ${deletedStyles} styles`); + figma.ui.postMessage({ + type: 'operation_cancelled', + operation: 'clear', + phase: 'clear', + rolledBack: false, + partial: { collectionsDeleted: 0, variablesDeleted: deletedStyles }, + message: `Clear cancelled — ${deletedStyles} styles were already deleted. Remaining items were not touched. Use Cmd+Z to restore deleted items.` + }); + return; + } Logger.log(`❌ Clear styles error: ${e}`); - Logger.send('error', { message: `Failed to clear styles: ${e}` }); + if (!silent) { + Logger.send('error', { message: `Failed to clear styles: ${e}` }); + } else { + throw e; + } } } -async function clearAll(): Promise { +async function clearAll(silent: boolean = false): Promise { Logger.log('🗑️ Clearing everything...'); - + + // Internal/silent callers: just delegate and let cancellation/errors bubble + // so the caller's rollback path runs. The children participate in the + // caller's own undo boundary (no per-child commitUndo). + if (silent) { + await clearVariables(true); + await clearStyles(true); + return; + } + + // Standalone clearAll: bracket BOTH children in a single Figma undo unit and + // emit exactly ONE terminal message. The children run silent so they don't + // each open their own commitUndo boundary (which would split the operation + // into two separate Cmd+Z steps) and don't post their own clear_complete / + // operation_cancelled. Running silent also makes them rethrow CancelError, so + // a cancellation during the variables phase short-circuits before styles and + // produces a single operation_cancelled message instead of two. + let committed = false; try { - await clearVariables(); - await clearStyles(); + figma.commitUndo(); + committed = true; + await clearVariables(true); + await clearStyles(true); + figma.commitUndo(); + Logger.send('clear_complete', { message: 'all variables and styles' }); } catch (e) { + if (committed) { + figma.commitUndo(); + } + if (isCancelError(e)) { + Logger.log('🛑 Clear all cancelled'); + figma.ui.postMessage({ + type: 'operation_cancelled', + operation: 'clear', + phase: 'clear', + rolledBack: false, + partial: { collectionsDeleted: 0, variablesDeleted: 0 }, + message: 'Clear cancelled — some items may already have been deleted. Use Cmd+Z to restore deleted items.' + }); + return; + } Logger.log(`❌ Clear all error: ${e}`); Logger.send('error', { message: `Failed to clear: ${e}` }); } } -// Create a snapshot of current variables and styles for undo -async function createUndoSnapshot(): Promise { +// Create a snapshot of current variables and styles for undo. Read-only, so it +// is cancellable. An optional progress reporter surfaces determinate progress. +async function createUndoSnapshot(reporter?: ProgressReporter): Promise { Logger.log('📸 Creating snapshot of current file state...'); - + // Export all collections using simplified internal format const collections = await figma.variables.getLocalVariableCollectionsAsync(); const snapshotCollections: unknown[] = []; - + + // Memoize variableCollectionId -> collection name for alias dereferencing so + // we don't re-fetch the same collection once per alias. + const collectionNameById = new Map(); + + // Grand total for determinate progress. + let totalVars = 0; + for (let tc = 0; tc < collections.length; tc++) { + totalVars += collections[tc].variableIds.length; + } + let processedVars = 0; + for (const collection of collections) { + collectionNameById.set(collection.id, collection.name); const collectionSnapshot: Record = { name: collection.name, modes: collection.modes.map(m => ({ id: m.modeId, name: m.name })), variables: [] as unknown[] }; - - // Process variables - for (const variableId of collection.variableIds) { - const variable = await figma.variables.getVariableByIdAsync(variableId); + + // Batch the per-variable async lookups instead of awaiting one at a time. + const resolvedVars = await runBatchedAsync( + collection.variableIds, + BATCH.ASYNC_LOOKUP, + function (variableId: string): Promise { + return figma.variables.getVariableByIdAsync(variableId); + }, + function (done: number): void { + if (reporter) { + reporter.report('snapshot', 'Preparing snapshot (undo safety)', processedVars + done, totalVars); + } + } + ); + processedVars += collection.variableIds.length; + + for (let rv = 0; rv < resolvedVars.length; rv++) { + const variable = resolvedVars[rv]; if (!variable) continue; - + const varSnapshot: Record = { name: variable.name, type: variable.resolvedType, scopes: [...variable.scopes], values: {} as Record }; - + for (const mode of collection.modes) { const value = variable.valuesByMode[mode.modeId]; - + if (typeof value === 'object' && value !== null && 'type' in value && value.type === 'VARIABLE_ALIAS') { // Handle alias const aliasId = (value as VariableAlias).id; const aliasVariable = await figma.variables.getVariableByIdAsync(aliasId); if (aliasVariable) { - const aliasCollection = await figma.variables.getVariableCollectionByIdAsync(aliasVariable.variableCollectionId); + let aliasCollectionName = collectionNameById.get(aliasVariable.variableCollectionId); + if (aliasCollectionName === undefined) { + const aliasCollection = await figma.variables.getVariableCollectionByIdAsync(aliasVariable.variableCollectionId); + aliasCollectionName = aliasCollection ? aliasCollection.name : ''; + collectionNameById.set(aliasVariable.variableCollectionId, aliasCollectionName); + } (varSnapshot.values as Record)[mode.name] = { isAlias: true, aliasName: aliasVariable.name, - aliasCollection: aliasCollection?.name || '' + aliasCollection: aliasCollectionName }; } } else { @@ -3416,13 +4126,13 @@ async function createUndoSnapshot(): Promise { } } } - + (collectionSnapshot.variables as unknown[]).push(varSnapshot); } - + snapshotCollections.push(collectionSnapshot); } - + // Export all styles const stylesExport: StylesExport = { colorStyles: await ColorStyleProcessor.export({ includeImages: true }), @@ -3430,10 +4140,10 @@ async function createUndoSnapshot(): Promise { effectStyles: await EffectStyleProcessor.export(), gridStyles: await GridStyleProcessor.export() }; - + const colorCount = stylesExport.colorStyles?.length || 0; Logger.log(`📸 Snapshot captured: ${collections.length} collections, ${colorCount} color styles`); - + return { timestamp: Date.now(), collections: JSON.stringify(snapshotCollections), @@ -3441,75 +4151,116 @@ async function createUndoSnapshot(): Promise { }; } -// Restore file state from a snapshot (undo) -async function restoreFromSnapshot(snapshot: UndoSnapshot): Promise { - Logger.log('↩️ Restoring file from snapshot...'); - - // Step 1: Clear everything - Logger.log(' Step 1: Clearing current state...'); - await clearAll(); - await variableCache.rebuild(); - - // Step 2: Restore collections and variables - const snapshotCollections = JSON.parse(snapshot.collections) as Array<{ +// Snapshot collection shape used by restore (kept in one place for validation). +interface SnapshotCollection { + name: string; + modes: Array<{ id: string; name: string }>; + variables: Array<{ name: string; - modes: Array<{ id: string; name: string }>; - variables: Array<{ - name: string; - type: VariableResolvedDataType; - scopes: string[]; - values: Record; - }>; + type: VariableResolvedDataType; + scopes: string[]; + values: Record; }>; - - Logger.log(` Step 2: Restoring ${snapshotCollections.length} collections...`); - - // First pass: Create collections and variables with raw values +} + +// Validate a parsed snapshot-collections payload before we touch the file. This +// is what makes restore safe: we never clear the current state until we know the +// snapshot parses and has the expected shape. +function isValidSnapshotCollections(parsed: unknown): parsed is SnapshotCollection[] { + if (!Array.isArray(parsed)) return false; + for (let i = 0; i < parsed.length; i++) { + const c = parsed[i] as Record; + if (c === null || typeof c !== 'object') return false; + if (typeof c.name !== 'string') return false; + if (!Array.isArray(c.modes)) return false; + if (!Array.isArray(c.variables)) return false; + } + return true; +} + +// Restore file state from a snapshot (undo). +// +// CRITICAL ORDERING (fixes the prior data-loss bug where the file was cleared +// before the snapshot was parsed): we PARSE + VALIDATE everything first, and +// only after that passes do we mark the operation non-cancellable and clear. +async function restoreFromSnapshot(snapshot: UndoSnapshot): Promise { + Logger.log('↩️ Restoring file from snapshot...'); + + // Per-call progress reporter (phases: undo_restore, undo_aliases, undo_styles). + const restoreProgress = createProgress('restore'); + + // STEP 1 (read-only): parse + shape-check BEFORE any mutation. + const parsedCollections = JSON.parse(snapshot.collections); + const stylesData = JSON.parse(snapshot.styles) as StylesExport; + if (!isValidSnapshotCollections(parsedCollections)) { + throw new Error('Snapshot is malformed (collections payload failed validation) — refusing to clear the file.'); + } + const snapshotCollections = parsedCollections; + + // STEP 2: point of no return. The restore itself rebuilds the prior state, so + // a half-applied restore is worse than completing it — make it uncancellable. + currentOperation.cancellable = false; + + // STEP 3: clear current state (silent — this is an internal step of restore). + // clearVariables already empties the local cache via clearLocal(); the restore + // pass below registers every collection/variable it creates, so no rescan needed. + Logger.log(' Clearing current state...'); + await clearVariables(true); + await clearStyles(true); + + Logger.log(` Restoring ${snapshotCollections.length} collections...`); + + // First pass: Create collections and variables with raw values (batched). const pendingAliases: Array<{ variable: Variable; modeId: string; aliasPath: string; aliasCollection: string; }> = []; - - for (const collSnapshot of snapshotCollections) { + + await runBatched( + snapshotCollections, + BATCH.SYNC_CREATE, + function (collSnapshot: SnapshotCollection): void { // Create collection const newCollection = figma.variables.createVariableCollection(collSnapshot.name); - + variableCache.setCollection(collSnapshot.name, newCollection); + // Setup modes if (collSnapshot.modes.length > 0) { // Rename first mode newCollection.renameMode(newCollection.modes[0].modeId, collSnapshot.modes[0].name); - + // Add additional modes for (let i = 1; i < collSnapshot.modes.length; i++) { newCollection.addMode(collSnapshot.modes[i].name); } } - + // Get mode mapping const modeMap: Record = {}; for (const mode of newCollection.modes) { modeMap[mode.name] = mode.modeId; } - + // Process variables for (const varSnapshot of collSnapshot.variables) { // Create variable - pass collection node, not ID (required for incremental mode) const newVar = figma.variables.createVariable(varSnapshot.name, newCollection, varSnapshot.type); - + variableCache.setVariable(`${collSnapshot.name}/${varSnapshot.name}`, newVar); + // Set scopes if available if (varSnapshot.scopes && varSnapshot.scopes.length > 0) { newVar.scopes = varSnapshot.scopes as VariableScope[]; } - + // Set values for each mode for (const modeSnapshot of collSnapshot.modes) { const modeId = modeMap[modeSnapshot.name]; const modeValue = varSnapshot.values[modeSnapshot.name]; - + if (!modeValue) continue; - + if (modeValue.isAlias && modeValue.aliasName) { // Queue alias for second pass pendingAliases.push({ @@ -3521,36 +4272,46 @@ async function restoreFromSnapshot(snapshot: UndoSnapshot): Promise { } else if (modeValue.value !== undefined) { // Set raw value let rawValue: VariableValue; - + if (varSnapshot.type === 'COLOR' && typeof modeValue.value === 'string') { rawValue = ColorParser.parse(modeValue.value); } else { rawValue = modeValue.value as VariableValue; } - + newVar.setValueForMode(modeId, rawValue); } } } - } - - // Second pass: Resolve aliases - Logger.log(` Step 3: Resolving ${pendingAliases.length} aliases...`); - await variableCache.rebuild(); - - for (const alias of pendingAliases) { - const targetKey = `${alias.aliasCollection}/${alias.aliasPath}`; - const targetVar = variableCache.getVariable(targetKey); - - if (targetVar) { - alias.variable.setValueForMode(alias.modeId, { type: 'VARIABLE_ALIAS', id: targetVar.id }); + }, + function (done: number, total: number): void { + restoreProgress.report('undo_restore', 'Restoring variables', done, total); } - } - - // Step 4: Restore styles - const stylesData = JSON.parse(snapshot.styles) as StylesExport; - Logger.log(' Step 4: Restoring styles...'); - + ); + + // Second pass: Resolve aliases (batched, light). The first pass registered + // every created collection/variable into the cache, so no rescan is needed. + Logger.log(` Resolving ${pendingAliases.length} aliases...`); + + await runBatched( + pendingAliases, + BATCH.SYNC_LIGHT, + function (alias: { variable: Variable; modeId: string; aliasPath: string; aliasCollection: string }): void { + const targetKey = `${alias.aliasCollection}/${alias.aliasPath}`; + const targetVar = variableCache.getVariable(targetKey); + if (targetVar) { + alias.variable.setValueForMode(alias.modeId, { type: 'VARIABLE_ALIAS', id: targetVar.id }); + } + }, + function (done: number, total: number): void { + restoreProgress.report('undo_aliases', 'Restoring aliases', done, total); + } + ); + + // Restore styles + Logger.log(' Restoring styles...'); + restoreProgress.report('undo_styles', 'Restoring styles', 0, 0, true); + if (stylesData.colorStyles && stylesData.colorStyles.length > 0) { await ColorStyleProcessor.importStyles(stylesData.colorStyles, variableCache); } @@ -3563,7 +4324,7 @@ async function restoreFromSnapshot(snapshot: UndoSnapshot): Promise { if (stylesData.gridStyles && stylesData.gridStyles.length > 0) { await GridStyleProcessor.importStyles(stylesData.gridStyles, variableCache); } - + Logger.log('✅ File restored from snapshot'); } @@ -3573,26 +4334,43 @@ async function restoreFromSnapshot(snapshot: UndoSnapshot): Promise { figma.ui.onmessage = async (msg: { type: string; [key: string]: unknown }) => { switch (msg.type) { + case 'cancel_operation': + // Synchronous cancel request. Only flips a flag the running operation polls + // between batches; the uncancellable rollback window ignores it. + if (currentOperation.type !== null) { + if (currentOperation.cancellable) { + currentOperation.cancelRequested = true; + Logger.log('🛑 Cancellation requested — finishing current batch…'); + } else { + Logger.log('⚠️ Rollback in progress — cannot cancel'); + } + } + break; + case 'export': - await exportVariables( - msg.collections as string[] | undefined, - msg.styleOptions as StyleOptions | undefined, - msg.preserveLibraryRefs as boolean | undefined, - msg.includeImages as boolean | undefined, - (msg.namingConvention as NamingConvention) || 'original', - (msg.exportFormat as ExportFormatType) || 'figma', - msg.selectedModes as Record | undefined, - (msg.resolveAliases as boolean) || false, - msg.selectedGroups as Record | undefined, - msg.selectedStyleGroups as SelectedStyleGroups | undefined - ); + await withOperation('export', function (): Promise { + return exportVariables( + msg.collections as string[] | undefined, + msg.styleOptions as StyleOptions | undefined, + msg.preserveLibraryRefs as boolean | undefined, + msg.includeImages as boolean | undefined, + (msg.namingConvention as NamingConvention) || 'original', + (msg.exportFormat as ExportFormatType) || 'figma', + msg.selectedModes as Record | undefined, + (msg.resolveAliases as boolean) || false, + msg.selectedGroups as Record | undefined, + msg.selectedStyleGroups as SelectedStyleGroups | undefined + ); + }); break; - + case 'import': - await importVariables( - msg.data as string, - msg.options as ImportOptions - ); + await withOperation('import', function (): Promise { + return importVariables( + msg.data as string, + msg.options as ImportOptions + ); + }); break; case 'validate_import': @@ -3630,19 +4408,25 @@ figma.ui.onmessage = async (msg: { type: string; [key: string]: unknown }) => { break; case 'clear_variables': - await clearVariables(); + await withOperation('clear', function (): Promise { + return clearVariables(false); + }); break; - + case 'clear_styles': - await clearStyles(); + await withOperation('clear', function (): Promise { + return clearStyles(false); + }); break; - + case 'clear_all': - await clearAll(); + await withOperation('clear', function (): Promise { + return clearAll(false); + }); break; - + case 'get_collections': - await getCollections(); + await withOperation('scan', getCollections); break; case 'check_libraries': @@ -3695,17 +4479,31 @@ figma.ui.onmessage = async (msg: { type: string; [key: string]: unknown }) => { const requiredFonts = msg.fonts as Array<{ family: string; style: string }>; const availableFonts: Array<{ family: string; style: string }> = []; const missingFonts: Array<{ family: string; style: string }> = []; - - // Check each font by attempting to load it - for (const font of requiredFonts) { - try { - await figma.loadFontAsync({ family: font.family, style: font.style }); - availableFonts.push(font); - } catch { - missingFonts.push(font); + + // Probe fonts in async chunks (no progress UI, no operation lock). Each + // probe resolves to {font, available} and never rejects, so a single + // unavailable font does not abort the whole batch. + const probes = await runBatchedAsync( + requiredFonts, + BATCH.ASYNC_FONT, + function (font: { family: string; style: string }): Promise<{ font: { family: string; style: string }; available: boolean }> { + return figma.loadFontAsync({ family: font.family, style: font.style }) + .then(function (): { font: { family: string; style: string }; available: boolean } { + return { font: font, available: true }; + }) + .catch(function (): { font: { family: string; style: string }; available: boolean } { + return { font: font, available: false }; + }); + } + ); + for (let i = 0; i < probes.length; i++) { + if (probes[i].available) { + availableFonts.push(probes[i].font); + } else { + missingFonts.push(probes[i].font); } } - + Logger.send('font_check_result', { allAvailable: missingFonts.length === 0, availableFonts, @@ -3723,31 +4521,20 @@ figma.ui.onmessage = async (msg: { type: string; [key: string]: unknown }) => { } break; - case 'create_undo_snapshot': - // Create a snapshot of current variables and styles for undo capability - try { - Logger.log('📸 Creating undo snapshot...'); - const snapshot = await createUndoSnapshot(); - Logger.send('snapshot_created', { snapshot }); - Logger.log('✅ Undo snapshot created successfully'); - } catch (e) { - Logger.log(`❌ Failed to create snapshot: ${e instanceof Error ? e.message : 'Unknown error'}`); - Logger.send('snapshot_error', { error: e instanceof Error ? e.message : 'Failed to create snapshot' }); - } - break; - case 'undo_import': // Restore file to pre-import state using snapshot - try { - Logger.log('↩️ Undoing import using snapshot...'); - const snapshotData = msg.snapshot as UndoSnapshot; - await restoreFromSnapshot(snapshotData); - Logger.send('undo_complete', {}); - Logger.log('✅ Import undone successfully'); - } catch (e) { - Logger.log(`❌ Undo failed: ${e instanceof Error ? e.message : 'Unknown error'}`); - Logger.send('undo_error', { error: e instanceof Error ? e.message : 'Undo failed' }); - } + await withOperation('undo', async function (): Promise { + try { + Logger.log('↩️ Undoing import using snapshot...'); + const snapshotData = msg.snapshot as UndoSnapshot; + await restoreFromSnapshot(snapshotData); + Logger.send('undo_complete', {}); + Logger.log('✅ Import undone successfully'); + } catch (e) { + Logger.log(`❌ Undo failed: ${e instanceof Error ? e.message : 'Unknown error'}`); + Logger.send('undo_error', { error: e instanceof Error ? e.message : 'Undo failed' }); + } + }); break; } }; diff --git a/variables-styles-extractor/ui.html b/variables-styles-extractor/ui.html index 501ee43..7430970 100644 --- a/variables-styles-extractor/ui.html +++ b/variables-styles-extractor/ui.html @@ -3237,6 +3237,59 @@ flex: 1; min-height: 0; } + + /* ========== HEAVY-LOAD OPERATION PROGRESS (Phase D) ========== */ + .op-progress { + display: flex; + flex-direction: column; + gap: 6px; + padding: 8px; + border: 1px solid var(--color-border); + border-radius: 6px; + background: var(--color-bg); + } + .op-progress.hidden { display: none; } + .op-progress-header { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + } + .op-progress-count { + margin-left: auto; + font-family: var(--font-mono); + } + .op-progress-track { + height: 6px; + border-radius: 3px; + background: var(--color-border); + overflow: hidden; + } + .op-progress-fill { + height: 100%; + width: 0%; + background: #4CAF50; + transition: width 120ms linear; + } + .op-progress.indeterminate .op-progress-fill { + width: 30%; + animation: op-indet 1.1s ease-in-out infinite; + } + .op-progress.indeterminate .op-progress-count { visibility: hidden; } + .op-progress-spinner { + width: 10px; + height: 10px; + border: 2px solid var(--color-border); + border-top-color: #4CAF50; + border-radius: 50%; + animation: op-spin 0.8s linear infinite; + } + .op-progress.cancel-hidden .op-progress-cancel { display: none; } + @keyframes op-spin { to { transform: rotate(360deg); } } + @keyframes op-indet { + 0% { transform: translateX(-120%); } + 100% { transform: translateX(420%); } + } @@ -3524,7 +3577,12 @@ - + + @@ -3614,11 +3672,15 @@
- +
- - + +
@@ -3648,7 +3710,7 @@ 📥 Load JSON Data
- +
@@ -4038,7 +4100,12 @@ Restore library links (if connected) - + +

Load a JSON file to begin

@@ -4074,7 +4141,7 @@
- +
- +
@@ -4203,7 +4274,95 @@ callback(); }); } - + + // ========== HEAVY-LOAD OPERATION PROGRESS (Phase D) ========== + // Single-operation gate: only one export/import/clear/undo runs at a time. + // Progress messages from the plugin are coalesced to one RAF (latest-wins). + let operationInFlight = null; + let pendingProgress = null; + let progressRafScheduled = false; + let lastProgressPhase = null; + + // Toggle the in-flight operation. Disables/restores [data-op-disable] controls + // and shows/hides every [data-progress-host]. Pass null to clear. + function setOperationInFlight(op) { + operationInFlight = op || null; + const disableEls = document.querySelectorAll('[data-op-disable]'); + disableEls.forEach(el => { + if (op) { + // Remember the prior enabled state once, then disable. + if (el.dataset.opWasEnabled === undefined) { + el.dataset.opWasEnabled = el.disabled ? '0' : '1'; + } + el.disabled = true; + } else { + // Restore prior enabled state. + if (el.dataset.opWasEnabled !== undefined) { + el.disabled = el.dataset.opWasEnabled === '0'; + delete el.dataset.opWasEnabled; + } + } + }); + + const hosts = document.querySelectorAll('[data-progress-host]'); + hosts.forEach(host => { + host.classList.toggle('hidden', !op); + host.classList.remove('cancel-hidden'); + }); + + if (!op) { + lastProgressPhase = null; + pendingProgress = null; + document.querySelectorAll('.op-progress-fill').forEach(fill => { + fill.style.width = '0%'; + }); + } + } + + // Paint the latest progress snapshot into every progress host (latest-wins). + function renderProgressFrame() { + const p = pendingProgress; + if (!p) return; + const total = Number(p.total) || 0; + const current = Number(p.current) || 0; + const indeterminate = !total; + const pct = total ? Math.max(0, Math.min(100, (current / total) * 100)) : 0; + const countText = total + ? current.toLocaleString() + ' / ' + total.toLocaleString() + : ''; + const labelText = p.label || 'Working…'; + + document.querySelectorAll('[data-progress-host]').forEach(host => { + host.classList.toggle('indeterminate', indeterminate); + const labelEl = host.querySelector('.op-progress-label'); + if (labelEl) labelEl.textContent = labelText; + const countEl = host.querySelector('.op-progress-count'); + if (countEl) countEl.textContent = countText; + const fillEl = host.querySelector('.op-progress-fill'); + if (fillEl) fillEl.style.width = pct + '%'; + }); + } + + // Request cancellation of the current operation from the plugin side. + function cancelCurrentOperation() { + if (!operationInFlight) return; + parent.postMessage({ pluginMessage: { type: 'cancel_operation' } }, '*'); + document.querySelectorAll('.op-progress-cancel').forEach(btn => { + btn.disabled = true; + btn.textContent = 'Cancelling…'; + }); + } + + // Schedule a coalesced progress repaint (latest-wins, one RAF max). + function scheduleProgressFrame() { + if (progressRafScheduled) return; + progressRafScheduled = true; + requestAnimationFrame(() => { + progressRafScheduled = false; + renderProgressFrame(); + }); + } + // Schedule work during idle time (or fallback to setTimeout) const scheduleIdle = window.requestIdleCallback ? (cb, opts) => window.requestIdleCallback(cb, opts) @@ -4387,6 +4546,11 @@ // Set timeout for worker response setTimeout(() => { if (jsonWorkerCallbacks.has(id)) { + // Kill the stalled worker so its eventual reply can't double-parse. + if (jsonWorker) { + jsonWorker.terminate(); + jsonWorker = null; + } jsonWorkerCallbacks.delete(id); // Fallback to main thread scheduleIdle(() => { @@ -4400,7 +4564,7 @@ }, { timeout: 200 }); } }, 5000); - + worker.postMessage({ id, type: 'parse', data: jsonString }); } else { // Fallback: use idle callback @@ -4417,12 +4581,44 @@ }); } + // Iterative (stack-based) node-count walk to estimate payload weight without + // recursion blowing the call stack on deep/large structures. Counts object + // keys + array items, and adds (long-string length / 100) so a few huge + // strings also tip a payload into "large". Early-exits once the threshold is hit. + function isLargePayload(data) { + const THRESHOLD = 1000; + let count = 0; + const stack = [data]; + while (stack.length > 0) { + const node = stack.pop(); + if (node === null || node === undefined) continue; + const t = typeof node; + if (t === 'string') { + if (node.length > 100) count += Math.floor(node.length / 100); + if (count > THRESHOLD) return true; + continue; + } + if (t !== 'object') continue; + if (Array.isArray(node)) { + count += node.length; + if (count > THRESHOLD) return true; + for (let i = 0; i < node.length; i++) stack.push(node[i]); + } else { + const keys = Object.keys(node); + count += keys.length; + if (count > THRESHOLD) return true; + for (let i = 0; i < keys.length; i++) stack.push(node[keys[i]]); + } + } + return count > THRESHOLD; + } + // Async JSON stringify using Web Worker for large data async function stringifyJSONAsync(data) { return new Promise((resolve, reject) => { - // Quick size estimation - const isLarge = Array.isArray(data) && data.length > 100; - + // Quick size estimation (iterative node-count walk) + const isLarge = isLargePayload(data); + // For small data, stringify directly if (!isLarge) { try { @@ -4442,6 +4638,11 @@ // Set timeout for worker response setTimeout(() => { if (jsonWorkerCallbacks.has(id)) { + // Kill the stalled worker so its eventual reply can't double-stringify. + if (jsonWorker) { + jsonWorker.terminate(); + jsonWorker = null; + } jsonWorkerCallbacks.delete(id); // Fallback to main thread try { @@ -4451,7 +4652,7 @@ } } }, 5000); - + worker.postMessage({ id, type: 'stringify', data }); } else { // Fallback: stringify on main thread with idle callback @@ -6572,23 +6773,82 @@ renderSimpleExportStyles(); break; - case 'export_complete': - exportData = msg.data.data; - document.getElementById('export-output').value = exportData; - + case 'operation_progress': { + // Per-phase log line (only when phase changes) + coalesced progress paint. + if (msg.phase !== lastProgressPhase) { + lastProgressPhase = msg.phase; + addLog('⏳ ' + msg.label + '…', 'info', msg.operation === 'export' ? 'export' : 'import'); + } + pendingProgress = msg; + scheduleProgressFrame(); + break; + } + case 'export_chunk': { + // Accumulate streamed export chunks (init on first chunk / seq 0). + if (msg.seq === 0 || !window._exportChunks) { + window._exportChunks = []; + } + window._exportChunks[msg.seq] = msg.data; + pendingProgress = { + operation: 'export', + phase: 'export_deliver', + label: 'Receiving export data', + current: msg.seq + 1, + total: msg.total + }; + scheduleProgressFrame(); + break; + } + case 'export_done': { + const chunks = window._exportChunks || []; + window._exportChunks = null; + + // Validate the reassembled stream before consuming it. + const chunksValid = Array.isArray(chunks) + && chunks.length === msg.chunkCount + && chunks.every(c => typeof c === 'string'); + + // Clear the operation gate FIRST so controls re-enable regardless of outcome. + setOperationInFlight(null); + + if (!chunksValid) { + addLog('❌ Export failed: incomplete or corrupt data stream', 'error'); + simpleExportPendingAction = null; + setSimpleExportButtonsDisabled(false); + break; + } + + exportData = chunks.join(''); + + if (typeof msg.totalLength === 'number' && exportData.length !== msg.totalLength) { + addLog('❌ Export failed: data length mismatch', 'error'); + exportData = ''; + simpleExportPendingAction = null; + setSimpleExportButtonsDisabled(false); + break; + } + + // Avoid pushing very large payloads into the textarea (>2MB) to keep the UI responsive. + if (exportData.length <= 2 * 1024 * 1024) { + document.getElementById('export-output').value = exportData; + } else { + document.getElementById('export-output').value = ''; + } + + // ----- Moved from the former export_complete case ----- // Show JSON preview in Activity Log column const jsonPreviewSection = document.getElementById('export-json-preview-section'); const jsonPreviewContent = document.getElementById('export-json-preview'); if (jsonPreviewSection && jsonPreviewContent) { jsonPreviewSection.classList.remove('hidden'); // Show truncated preview (first 2000 chars with ellipsis) - const previewText = exportData.length > 2000 + const previewText = exportData.length > 2000 ? exportData.substring(0, 2000) + '\n\n... (truncated - use Copy/Download for full data)' : exportData; jsonPreviewContent.textContent = previewText; } - - const stats = msg.data.stats; + + const stats = msg.stats; let logMsg = `✅ Export: ${stats.collections} collections, ${stats.variables} vars`; if (stats.styles) { const styleTotal = (stats.styles.color || 0) + (stats.styles.text || 0) + (stats.styles.effect || 0) + (stats.styles.grid || 0); @@ -6597,21 +6857,21 @@ } } addLog(logMsg, 'success'); - + // Show external collections warning if present - const extColls = msg.data.externalCollections; + const extColls = msg.externalCollections; if (extColls && extColls.length > 0) { addLog(`📚 ${extColls.length} library collection(s) referenced but not exported: ${extColls.join(', ')}`, 'warning'); addLog(`⚠️ Aliases to library tokens will use raw values on import`, 'warning'); } - + // Show font requirements in Status Check - const reqFonts = msg.data.requiredFonts; + const reqFonts = msg.requiredFonts; const fontBanner = document.getElementById('font-requirements-banner'); const fontList = document.getElementById('font-requirements-list'); if (reqFonts && reqFonts.length > 0) { fontBanner.classList.remove('hidden'); - fontList.innerHTML = reqFonts.map(f => + fontList.innerHTML = reqFonts.map(f => `🔤 ${f.family} ${f.style}` ).join(''); addLog(`🔤 Required fonts for import: ${reqFonts.map(f => `${f.family} ${f.style}`).join(', ')}`, 'info'); @@ -6633,7 +6893,36 @@ } } break; + } + case 'operation_cancelled': + setOperationInFlight(null); + addLog('🛑 ' + msg.message, 'warning', msg.operation === 'export' ? 'export' : 'import'); + if (msg.operation === 'clear' || (msg.operation === 'import' && msg.rolledBack)) { + loadCollections(); + } + if (msg.operation === 'import') { + const importBtnCancel = document.getElementById('import-btn'); + if (importBtnCancel) importBtnCancel.textContent = '📥 Import Selected'; + } + // Release Simple-mode export action latch + re-enable its buttons. + simpleExportPendingAction = null; + setSimpleExportButtonsDisabled(false); + resetSimpleImportButton(); + break; + case 'operation_denied': + console.warn('Operation denied:', msg.requested, 'while', msg.running); + // Defensive self-heal: the backend rejected a request because another + // operation holds the lock. The UI normally gates on operationInFlight + // before posting, so this is unreachable today — but if a sender ever + // sets the flag before a denial, clear it so controls don't stay + // permanently gated. Harmless no-op when already idle. + setOperationInFlight(null); + break; case 'import_complete': + setOperationInFlight(null); + // Snapshot unification: the plugin now returns the pre-import snapshot + // alongside the completion message (no separate create_undo_snapshot round-trip). + lastImportSnapshot = msg.data.snapshot || null; const s = msg.data.stats; let importMsg = `✅ Import: ${s.collectionsCreated} collections, ${s.variablesCreated} vars created`; if (s.variablesUpdated > 0) { @@ -6691,28 +6980,8 @@ resetSimpleImportButton(); clearSimpleImportSelectionState(); break; - case 'snapshot_created': - // Snapshot created, now proceed with actual import - lastImportSnapshot = msg.data.snapshot; - addLog('✅ Snapshot saved, proceeding with import...', 'success', 'import'); - - if (window._pendingImportAfterSnapshot) { - const { data, options } = window._pendingImportAfterSnapshot; - window._pendingImportAfterSnapshot = null; - executeImportAfterSnapshot(data, options); - } - break; - case 'snapshot_error': - addLog('⚠️ Could not create snapshot, proceeding anyway...', 'warning', 'import'); - lastImportSnapshot = null; - - if (window._pendingImportAfterSnapshot) { - const { data, options } = window._pendingImportAfterSnapshot; - window._pendingImportAfterSnapshot = null; - executeImportAfterSnapshot(data, options); - } - break; case 'undo_complete': + setOperationInFlight(null); addLog('✅ Import undone successfully! File restored to previous state.', 'success', 'import'); hideUndoSection(); loadCollections(); @@ -6725,6 +6994,7 @@ } break; case 'undo_error': + setOperationInFlight(null); addLog(`❌ Undo failed: ${msg.data.error}`, 'error', 'import'); // Reset undo button @@ -6735,12 +7005,16 @@ } break; case 'import_rolling_back': - // Import failed, automatic rollback in progress + // Import failed, automatic rollback in progress. + // The operation is still in flight (rollback running) but cancellation + // is no longer possible, so hide the cancel control on every host. + document.querySelectorAll('[data-progress-host]').forEach(h => h.classList.add('cancel-hidden')); addLog(`⚠️ Import failed: ${msg.data.error}`, 'error', 'import'); addLog('🔄 Automatic rollback in progress...', 'info', 'import'); document.getElementById('import-status').textContent = 'Rolling back changes...'; break; case 'import_rollback_complete': + setOperationInFlight(null); // Automatic rollback succeeded addLog('✅ ' + msg.data.message, 'success', 'import'); document.getElementById('import-status').textContent = 'Import failed - file restored'; @@ -6753,6 +7027,7 @@ updateSimpleImportButtonState(); break; case 'import_rollback_failed': + setOperationInFlight(null); // Automatic rollback failed addLog(`❌ Rollback failed: ${msg.data.rollbackError}`, 'error', 'import'); addLog('⚠️ ' + msg.data.message, 'warning', 'import'); @@ -6765,6 +7040,7 @@ updateSimpleImportButtonState(); break; case 'clear_complete': + setOperationInFlight(null); addLog(`✅ Cleared: ${msg.data.message}`, 'success'); loadCollections(); break; @@ -6816,6 +7092,7 @@ displayImportDiff(msg.data); break; case 'error': + setOperationInFlight(null); addLog('❌ ' + msg.data.message, 'error'); // Simple mode: release the action buttons on backend failure (stage 3 wiring) simpleExportPendingAction = null; @@ -7309,11 +7586,12 @@ } function exportVariables() { + if (operationInFlight) return; // single-operation gate (Phase D) if (selectedExportCollections.size === 0) { addLog('❌ Please select at least one collection', 'error'); return; } - + // Advanced: read the style checkboxes; Simple: derive from group-level state const styleOptions = isAdvancedMode() ? { @@ -7390,9 +7668,10 @@ if (resolveAliases) { addLog(`🔓 Resolving aliases to raw values`, 'info'); } - - parent.postMessage({ - pluginMessage: { + + setOperationInFlight('export'); + parent.postMessage({ + pluginMessage: { type: 'export', collections: Array.from(selectedExportCollections), styleOptions: styleOptions, @@ -8124,26 +8403,32 @@ } function clearAllVariables() { + if (operationInFlight) return; // single-operation gate (Phase D) if (!confirm('⚠️ This will delete ALL variable collections and variables in this file. This cannot be undone. Continue?')) { return; } addLog('🗑️ Clearing all variables...'); + setOperationInFlight('clear'); parent.postMessage({ pluginMessage: { type: 'clear_variables' } }, '*'); } function clearAllStyles() { + if (operationInFlight) return; // single-operation gate (Phase D) if (!confirm('⚠️ This will delete ALL local styles (color, text, effect, grid) in this file. This cannot be undone. Continue?')) { return; } addLog('🗑️ Clearing all styles...'); + setOperationInFlight('clear'); parent.postMessage({ pluginMessage: { type: 'clear_styles' } }, '*'); } function clearAll() { + if (operationInFlight) return; // single-operation gate (Phase D) if (!confirm('⚠️ This will delete ALL variables AND styles in this file. This cannot be undone. Continue?')) { return; } addLog('🗑️ Clearing everything...'); + setOperationInFlight('clear'); parent.postMessage({ pluginMessage: { type: 'clear_all' } }, '*'); } @@ -8152,11 +8437,12 @@ let pendingImportOptions = null; function importVariables() { + if (operationInFlight) return; // single-operation gate (Phase D) if (!importData || selectedImportCollections.size === 0) { addLog('❌ Please select collections to import', 'error'); return; } - + // Disable import button immediately to prevent double-clicks const importBtn = document.getElementById('import-btn'); if (importBtn) { @@ -8431,24 +8717,18 @@ function executeImport(data, options) { // Hide undo section from previous import since we're starting a new one hideUndoSection(); - - // First, take a snapshot of current state for undo capability - addLog('📸 Capturing current state for undo...', 'info', 'import'); - - // Store import data and options for after snapshot - window._pendingImportAfterSnapshot = { data, options }; - - parent.postMessage({ - pluginMessage: { - type: 'create_undo_snapshot' - } - }, '*'); + + // Snapshot unification (Phase D): the plugin now captures the undo snapshot + // as part of the import op and returns it on import_complete. No separate + // create_undo_snapshot pre-send round-trip is needed — send 'import' directly. + executeImportAfterSnapshot(data, options); } - - // Actually execute import after snapshot is captured (async for large data) + + // Stringify + post the import message (async for large data) async function executeImportAfterSnapshot(data, options) { try { const dataString = await stringifyJSONAsync(data); + setOperationInFlight('import'); parent.postMessage({ pluginMessage: { type: 'import', @@ -8465,24 +8745,26 @@ // Undo last import - restores file to pre-import state function undoLastImport() { + if (operationInFlight) return; // single-operation gate (Phase D) if (!lastImportSnapshot) { showToast('No import to undo', 'error'); return; } - + if (!confirm('⚠️ This will undo the last import, restoring the file to its previous state. This cannot be undone. Continue?')) { return; } - + addLog('↩️ Undoing last import...', 'info', 'import'); - + // Disable undo button during operation const undoBtn = document.getElementById('undo-import-btn'); if (undoBtn) { undoBtn.disabled = true; undoBtn.innerHTML = ' Undoing...'; } - + + setOperationInFlight('undo'); parent.postMessage({ pluginMessage: { type: 'undo_import', @@ -8537,6 +8819,18 @@ } function handleFile(file) { + // Heavy-load guard (Phase D): hard-refuse absurd files, confirm very large ones. + const MB = 1024 * 1024; + if (file && file.size > 100 * MB) { + addLog(`❌ File too large (${(file.size / MB).toFixed(1)} MB). Maximum supported size is 100 MB.`, 'error', 'import'); + showToast('File too large (max 100 MB)', 'error'); + return; + } + if (file && file.size > 20 * MB) { + if (!confirm(`⚠️ This file is ${(file.size / MB).toFixed(1)} MB and may take a while to load. Continue?`)) { + return; + } + } const reader = new FileReader(); reader.onload = (e) => { document.getElementById('import-input').value = e.target.result; @@ -9754,6 +10048,15 @@ // Initialize Simple-mode wiring (listeners, shared-node placement, first paint) initSimpleMode(); + // ========== HEAVY-LOAD HANDLING INIT (Phase D) ========== + // Wire every progress-host cancel button to the shared cancel handler. + function initHeavyLoadHandling() { + document.querySelectorAll('.op-progress-cancel').forEach(btn => { + btn.addEventListener('click', cancelCurrentOperation); + }); + } + initHeavyLoadHandling(); + // ========== TIP MODAL ========== function openTipModal() { document.getElementById('tipModal').classList.add('visible'); From b2b2d421b06a1c3b7e8ef41d06609f1c99ba9d2d Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:15:42 +0530 Subject: [PATCH 05/20] feat: Tokens Studio-compatible export format (additive, both tabs) New optional export format emitting JSON the Tokens Studio plugin for Figma imports directly (verified against tokens-studio/figma-plugin's Zod import schemas + docs.tokens.studio; the compiled converter was executed against worked examples during review): - Single-file "shape A" container: one token set per Collection/Mode, $themes (one per collection x mode; own set enabled, other collections + style sets as source) and $metadata.tokenSetOrder - DTCG keys throughout ($value/$type/$description), explicit $type on every token, Tokens Studio canonical type names; FLOAT/STRING variables refined by scope (borderRadius/spacing/sizing/opacity/ fontSizes/... when unambiguous); BOOLEAN as "true"/"false" strings - Colors as #rrggbb / rgba(); aliases as {dot.path} name references (no set prefix); library aliases fall back to their resolved local value or are skipped with one aggregated note - Styles ride in dedicated styles/color, styles/typography (singular composite sub-keys), styles/effects (boxShadow x/y keys, multi-layer arrays) sets; grid styles and image paints skipped with log notes - Collision guards: collections named $themes/$metadata/styles-set names get suffixed; token-path segments strip {}$ and reject __proto__/constructor/prototype Additive guarantee held: the figma (default) and w3c branches diff to exactly one changed line (the ExportFormatType union). UI: third option in the Advanced format dropdown + a compact Format select in Simple Section 3 (default Figma JSON); downloads name the file tokens.json for this format. tsc clean; code.js rebuilt. Co-Authored-By: Claude Fable 5 --- variables-styles-extractor/code.js | 2 +- variables-styles-extractor/src/code.ts | 559 ++++++++++++++++++++++++- variables-styles-extractor/ui.html | 42 +- 3 files changed, 598 insertions(+), 5 deletions(-) diff --git a/variables-styles-extractor/code.js b/variables-styles-extractor/code.js index aafd1c2..5e7f7c6 100644 --- a/variables-styles-extractor/code.js +++ b/variables-styles-extractor/code.js @@ -8,4 +8,4 @@ * @version 2.0.0 * @author Tushar Kant Naik * @website https://tusharkantnaik.com - */figma.showUI(__html__,{width:1200,height:628,themeColors:!0,title:"☕️ Variables & Styles Extractor v2.0.0"});const Logger={log(e,t){console.log(`[Variables Extractor] ${e}`,t||""),figma.ui.postMessage({type:"log",message:e,data:t})},send(e,t){figma.ui.postMessage({type:e,data:t})}};function yieldToHost(){return new Promise(function(e){setTimeout(e,0)})}function makeCancelError(){const e=new Error("Operation cancelled");return e.isOperationCancelled=!0,e}function isCancelError(e){return"object"==typeof e&&null!==e&&!0===e.isOperationCancelled}const currentOperation={type:null,cancelRequested:!1,cancellable:!0};function beginOperation(e){return null!==currentOperation.type?(figma.ui.postMessage({type:"operation_denied",requested:e,running:currentOperation.type}),!1):(currentOperation.type=e,currentOperation.cancelRequested=!1,currentOperation.cancellable=!0,!0)}function endOperation(){currentOperation.type=null,currentOperation.cancelRequested=!1,currentOperation.cancellable=!0}function checkCancelled(){if(currentOperation.cancelRequested&¤tOperation.cancellable)throw makeCancelError()}async function withOperation(e,t){if(beginOperation(e))try{await t()}finally{endOperation()}}async function runBatched(e,t,a,o){const r=e.length;for(let s=0;s0&&s>=n)&&l-aa&&(a=t.modes.length);return t=a>20?"enterprise":a>10?"organization":"professional",Object.assign({plan:t},PLAN_LIMITS[t])}async function validateImportAgainstPlan(e,t){const a=t?Object.assign({plan:t},PLAN_LIMITS[t]):await detectCurrentPlan(),o=await figma.variables.getLocalVariableCollectionsAsync(),r=o.reduce((e,t)=>Math.max(e,t.modes.length),0),s=(await figma.variables.getLocalVariablesAsync()).length,n=[];for(const t of e)"_styles"in t||n.push(t);let i=0,l=0;const c=[];for(const e of n){const t=Object.keys(e)[0],o=e[t];if(!o||!o.modes)continue;const r=Object.keys(o.modes).length;r>i&&(i=r),r>a.maxModesPerCollection&&c.push(`"${t}" (${r} modes, limit: ${a.maxModesPerCollection===1/0?"∞":a.maxModesPerCollection})`);const s=Object.values(o.modes)[0];s&&(l+=countNestedVariables(s))}const g=[],d=[];c.length;for(const e of n){const t=Object.keys(e)[0],a=e[t];if(!a||!a.modes)continue;const o=Object.values(a.modes)[0],r=o?countNestedVariables(o):0;r>5e3&&d.push(`Collection "${t}" has ${r} variables, exceeds limit of 5000`)}l>1e3&&g.push(`Large import: ${l} variables. This may take a moment.`),n.length>10&&g.push(`Importing ${n.length} collections. Consider importing in batches.`);const f=new Set;let u=0;for(const e of n){const t=e[Object.keys(e)[0]];if(t&&t.modes)for(const e of Object.keys(t.modes)){const a=flattenVariables(t.modes[e],"");for(const{value:e}of a)e.$libraryRef&&e.$collectionName&&(f.add(e.$collectionName),u++)}}const m=[];let p=0;for(const t of e)if("_styles"in t){const e=t._styles;if(e.textStyles)for(const t of e.textStyles){p++;const e=`${t.fontFamily}|${t.fontStyle}`;m.some(t=>`${t.family}|${t.style}`===e)||m.push({family:t.fontFamily,style:t.fontStyle})}}return Object.assign(Object.assign({currentPlan:a,existing:{collections:o.length,maxModesInAnyCollection:r,totalVariables:s},importing:{collections:n.length,maxModesInAnyCollection:i,totalVariables:l,collectionsExceedingModeLimit:c},warnings:g,errors:d,canImport:0===d.length},f.size>0&&{libraryDependencies:{variableCount:u,collections:Array.from(f)}}),m.length>0&&{fontDependencies:{styleCount:p,fonts:m}})}function countNestedVariables(e,t=0){for(const[,a]of Object.entries(e))a&&"object"==typeof a&&("$type"in a&&"$value"in a?t++:t=countNestedVariables(a,t));return t}const MathUtils={round2:e=>Math.round(100*e)/100,clamp:(e,t,a)=>Math.max(t,Math.min(a,e)),toHexByte:e=>Math.round(255*e).toString(16).padStart(2,"0"),fromHexByte:e=>parseInt(e,16)/255};function calculateHue(e,t,a,o,r){if(o===r)return 0;const s=o-r;let n=0;switch(o){case e:n=((t-a)/s+(t.5?e/(2-r-s):e/(r+s)}const l={h:calculateHue(t,a,o,r,s),s:Math.round(100*i),l:Math.round(100*n)},c=e.a;return void 0!==c&&c<1?Object.assign(Object.assign({},l),{a:MathUtils.round2(c)}):l},toHsb(e){const{r:t,g:a,b:o}=e,r=Math.max(t,a,o),s=Math.min(t,a,o),n=0===r?0:(r-s)/r,i={h:calculateHue(t,a,o,r,s),s:Math.round(100*n),b:Math.round(100*r)},l=e.a;return void 0!==l&&l<1?Object.assign(Object.assign({},i),{a:MathUtils.round2(l)}):i},toAllFormats(e){return{hex:this.toHex(e),rgb:this.toRgb255(e),css:this.toCss(e),hsl:this.toHsl(e),hsb:this.toHsb(e)}}},NamingConverter={convert(e,t){if("original"===t)return e;const a=e.replace(/([a-z])([A-Z])/g,"$1 $2").split(/[\s\/\-_]+/).filter(e=>e.length>0).map(e=>e.toLowerCase());if(0===a.length)return e;switch(t){case"camelCase":return a[0]+a.slice(1).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("");case"kebab-case":return a.join("-");case"snake_case":return a.join("_");default:return e}},convertPath(e,t){return"original"===t?e:e.split("/").map(e=>this.convert(e,t)).join("/")},convertCollectionName(e,t){return this.convert(e,t)},convertModeName(e,t){return this.convert(e,t)},addOriginalName(e,t){if("original"===t)return{converted:e};const a=this.convert(e,t);return a===e?{converted:e}:{converted:a,original:e}}};async function resolveAliasValue(e,t,a=10){if(a<=0)return Logger.log(`⚠️ Max alias resolution depth reached for ${e.name}`),"";let o=e.valuesByMode[t];if(void 0===o){const t=Object.keys(e.valuesByMode);t.length>0&&(o=e.valuesByMode[t[0]])}if(void 0===o)return"";if(isVariableAlias(o)){const e=await figma.variables.getVariableByIdAsync(o.id);return e?resolveAliasValue(e,t,a-1):""}return o}const W3C_TYPE_MAP={color:"color",float:"number",string:"string",boolean:"boolean"},W3CConverter={colorToW3C:e=>e.hex,typeToW3C:e=>W3C_TYPE_MAP[e]||"string",valueToW3C(e,t=!1){const a={$value:"",$type:this.typeToW3C(e.$type)};return t&&"string"==typeof e.$value&&e.$value.startsWith("{")?a.$value=e.$value:"color"===e.$type&&"object"==typeof e.$value?a.$value=e.$value.hex:a.$value=e.$value,e.$description&&(a.$description=e.$description),e.$scopes&&e.$scopes.length>0&&!e.$scopes.includes("ALL_SCOPES")&&(a.$extensions={"com.figma":{scopes:e.$scopes}}),a},collectionToW3C(e,t,a,o){const r={};o&&o!==e&&(r.$description=`Figma collection: ${o}`);const s=Object.keys(t);if(1===s.length)this.addTokensToGroup(r,t[s[0]],a);else for(const e of s){const o=NamingConverter.convertModeName(e,a);r[o]={},this.addTokensToGroup(r[o],t[e],a)}return r},addTokensToGroup(e,t,a){for(const[o,r]of Object.entries(t)){const t=NamingConverter.convert(o,a);if(isExportVariableValue(r)){const a="string"==typeof r.$value&&r.$value.startsWith("{");e[t]=this.valueToW3C(r,a)}else e[t]={},this.addTokensToGroup(e[t],r,a)}},parseW3CToken(e){var t,a;const o=this.w3cTypeToFigma(e.$type),r=(null===(a=null===(t=e.$extensions)||void 0===t?void 0:t["com.figma"])||void 0===a?void 0:a.scopes)||["ALL_SCOPES"];let s;if("color"===o&&"string"==typeof e.$value){const t=ColorParser.parse(e.$value);s=ColorConverter.toAllFormats(t)}else s="string"==typeof e.$value||"number"==typeof e.$value||"boolean"==typeof e.$value?e.$value:JSON.stringify(e.$value);return e.$description?{$type:o,$value:s,$scopes:r,$description:e.$description}:{$type:o,$value:s,$scopes:r}},w3cTypeToFigma:e=>({color:"color",number:"float",dimension:"float",string:"string",boolean:"boolean",fontFamily:"string",fontWeight:"float",duration:"string",cubicBezier:"string"}[e]||"string"),isW3CFormat(e){if("object"!=typeof e||null===e)return!1;const t=e;for(const e of Object.keys(t)){const a=t[e];if("object"==typeof a&&null!==a){if("$value"in a&&"$type"in a)return!0;for(const e of Object.keys(a)){const t=a[e];if("object"==typeof t&&null!==t&&"$value"in t)return!0}}}return Array.isArray(e),!1},w3cToFigmaFormat(e){const t=[];for(const[a,o]of Object.entries(e)){if(a.startsWith("$"))continue;const e={[a]:{modes:{Default:this.w3cGroupToNestedVars(o)}}};t.push(e)}return t},w3cGroupToNestedVars(e){const t={};for(const[a,o]of Object.entries(e))a.startsWith("$")||(this.isW3CToken(o)?t[a]=this.parseW3CToken(o):"object"==typeof o&&null!==o&&(t[a]=this.w3cGroupToNestedVars(o)));return t},isW3CToken:e=>"object"==typeof e&&null!==e&&"$value"in e},HEX_REGEX_8=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,HEX_REGEX_6=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i,RGBA_REGEX=/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+))?\s*\)/i,HSLA_REGEX=/hsla?\(\s*(\d+)\s*,\s*(\d+)%?\s*,\s*(\d+)%?\s*(?:,\s*([\d.]+))?\s*\)/i,ColorParser={fromHex(e){const t=HEX_REGEX_8.exec(e);if(t)return{r:MathUtils.fromHexByte(t[1]),g:MathUtils.fromHexByte(t[2]),b:MathUtils.fromHexByte(t[3]),a:MathUtils.fromHexByte(t[4])};const a=HEX_REGEX_6.exec(e);return a?{r:MathUtils.fromHexByte(a[1]),g:MathUtils.fromHexByte(a[2]),b:MathUtils.fromHexByte(a[3]),a:1}:{r:0,g:0,b:0,a:1}},fromRgb255(e){var t;return{r:e.r/255,g:e.g/255,b:e.b/255,a:null!==(t=e.a)&&void 0!==t?t:1}},fromCss(e){const t=RGBA_REGEX.exec(e);if(t)return{r:parseInt(t[1],10)/255,g:parseInt(t[2],10)/255,b:parseInt(t[3],10)/255,a:void 0!==t[4]?parseFloat(t[4]):1};const a=HSLA_REGEX.exec(e);return a?this.fromHsl({h:parseInt(a[1],10),s:parseInt(a[2],10),l:parseInt(a[3],10),a:void 0!==a[4]?parseFloat(a[4]):1}):{r:0,g:0,b:0,a:1}},fromHsl(e){var t,a;const o=e.h/360,r=e.s/100,s=e.l/100;if(0===r)return{r:s,g:s,b:s,a:null!==(t=e.a)&&void 0!==t?t:1};const hue2rgb=(e,t,a)=>{const o=a<0?a+1:a>1?a-1:a;return o<1/6?e+6*(t-e)*o:o<.5?t:o<2/3?e+(t-e)*(2/3-o)*6:e},n=s<.5?s*(1+r):s+r-s*r,i=2*s-n;return{r:hue2rgb(i,n,o+1/3),g:hue2rgb(i,n,o),b:hue2rgb(i,n,o-1/3),a:null!==(a=e.a)&&void 0!==a?a:1}},fromHsb(e){var t;const a=e.h/360,o=e.s/100,r=e.b/100,s=Math.floor(6*a),n=6*a-s,i=r*(1-o),l=r*(1-n*o),c=r*(1-(1-n)*o),g=[[r,c,i],[l,r,i],[i,r,c],[i,l,r],[c,i,r],[r,i,l]],[d,f,u]=g[s%6];return{r:d,g:f,b:u,a:null!==(t=e.a)&&void 0!==t?t:1}},parse(e){var t;if("object"==typeof e&&null!==e&&"hex"in e&&"rgb"in e)return this.fromHex(e.hex);if("object"==typeof e&&null!==e&&"r"in e&&"g"in e&&"b"in e){const a=e;return a.r<=1&&a.g<=1&&a.b<=1?{r:a.r,g:a.g,b:a.b,a:null!==(t=a.a)&&void 0!==t?t:1}:this.fromRgb255(a)}return"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"l"in e?this.fromHsl(e):"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"b"in e?this.fromHsb(e):"string"==typeof e?e.startsWith("rgb")||e.startsWith("hsl")?this.fromCss(e):this.fromHex(e):{r:0,g:0,b:0,a:1}}};class VariableCache{constructor(){this.collectionMap=new Map,this.variableMap=new Map,this.libraryVariableMap=new Map,this.libraryCollectionNames=new Set,this.initialized=!1,this.libraryIndexed=!1}async ensureReady(){this.initialized||(await this.rebuildLocal(),this.initialized=!0)}async initialize(){await this.ensureReady()}async rebuild(){await this.rebuildLocal(),await this.ensureLibraryIndex(),this.initialized=!0}async rebuildLocal(){this.clearLocal();const e=await figma.variables.getLocalVariableCollectionsAsync();for(let t=0;t{try{const a=await figma.variables.importVariableByKeyAsync(e.key);a&&this.libraryVariableMap.set(`${t}/${a.name}`,a)}catch(e){}})}catch(e){Logger.log(` ⚠️ Could not index library collection "${a.name}": ${e}`)}}this.libraryCollectionNames.size>0&&Logger.log(`📚 Indexed ${this.libraryVariableMap.size} library variables from ${this.libraryCollectionNames.size} connected libraries`)}catch(e){Logger.log(`⚠️ Could not access team library: ${e}`)}}getCollection(e){return this.collectionMap.get(e)}getVariable(e){return this.variableMap.get(e)||this.libraryVariableMap.get(e)}setVariable(e,t){this.variableMap.set(e,t)}setCollection(e,t){this.collectionMap.set(e,t)}removeCollection(e){this.collectionMap.delete(e);const t=[];for(const a of this.variableMap.keys())a.startsWith(`${e}/`)&&t.push(a);for(const e of t)this.variableMap.delete(e)}isCollectionAvailable(e){return this.collectionMap.has(e)||this.libraryCollectionNames.has(e)}getLibraryCollectionNames(){return Array.from(this.libraryCollectionNames)}get size(){return this.variableMap.size}get collections(){return this.collectionMap.values()}getVariableKeys(){return Array.from(this.variableMap.keys())}}const variableCache=new VariableCache;function isExportVariableValue(e){return"object"==typeof e&&null!==e&&"$type"in e}function isVariableAlias(e){return"object"==typeof e&&null!==e&&"VARIABLE_ALIAS"===e.type}const TypeMapper={toExportType(e){var t;return null!==(t={COLOR:"color",FLOAT:"float",STRING:"string",BOOLEAN:"boolean"}[e])&&void 0!==t?t:"string"},toFigmaType(e){var t;return null!==(t={color:"COLOR",float:"FLOAT",string:"STRING",boolean:"BOOLEAN"}[e])&&void 0!==t?t:"STRING"},scopesToArray:e=>0===e.length||e.includes("ALL_SCOPES")?["ALL_SCOPES"]:[...e],arrayToScopes:e=>e.includes("ALL_SCOPES")?["ALL_SCOPES"]:e};async function getVariableBindingInfo(e,t){if(!(null==e?void 0:e[t]))return{};const a=e[t];if(!a)return{};const o=await figma.variables.getVariableByIdAsync(a.id);if(!o)return{id:a.id};const r=await figma.variables.getVariableCollectionByIdAsync(o.variableCollectionId);return{id:a.id,name:o.name,collection:null==r?void 0:r.name}}async function extractBindings(e,t){if(!e)return;const a={};for(const o of t){const t=await getVariableBindingInfo(e,o);t.name&&(a[o]=t)}return Object.keys(a).length>0?a:void 0}function flattenVariables(e,t){const a=[];for(const o of Object.keys(e)){const r=e[o],s=t?`${t}/${o}`:o;isExportVariableValue(r)?a.push({path:s,value:r}):a.push(...flattenVariables(r,s))}return a}function getValueAtPath(e,t){const a=t.split("/");let o=e;for(const e of a){if("object"!=typeof o||null===o)return null;if(isExportVariableValue(o))return null;o=o[e]}return isExportVariableValue(o)?o:null}const ColorStyleProcessor={async export(e){var t;const a=null!==(t=null==e?void 0:e.includeImages)&&void 0!==t&&t,o=[],r=await figma.getLocalPaintStylesAsync();return await runSequentialAsync(r,20,async function(e){var t,r,s;if(0===e.paints.length)return;const n=[];let i,l,c;for(const o of e.paints)if("SOLID"===o.type){const e=o.color;let a=null!==(t=o.opacity)&&void 0!==t?t:1;void 0!==e.a&&e.a<1&&1===a&&(a=e.a);const r={r:o.color.r,g:o.color.g,b:o.color.b,a:a},s={type:"SOLID",color:ColorConverter.toAllFormats(r),opacity:MathUtils.round2(a)};n.push(s),i||(i=s.color,l=s.opacity,c=await extractBindings(o.boundVariables,["color"]))}else if("GRADIENT_LINEAR"===o.type||"GRADIENT_RADIAL"===o.type||"GRADIENT_ANGULAR"===o.type||"GRADIENT_DIAMOND"===o.type){const e=o.gradientStops.map(e=>{var t;return{position:MathUtils.round2(e.position),color:ColorConverter.toAllFormats({r:e.color.r,g:e.color.g,b:e.color.b,a:null!==(t=e.color.a)&&void 0!==t?t:1})}}),t=Object.assign(Object.assign({type:o.type,gradientStops:e},o.gradientTransform&&{gradientTransform:o.gradientTransform}),{opacity:MathUtils.round2(null!==(r=o.opacity)&&void 0!==r?r:1)});n.push(t)}else if("IMAGE"===o.type){const t=Object.assign(Object.assign(Object.assign(Object.assign({type:"IMAGE",scaleMode:o.scaleMode},o.imageHash&&{imageHash:o.imageHash}),{opacity:MathUtils.round2(null!==(s=o.opacity)&&void 0!==s?s:1)}),void 0!==o.rotation&&{rotation:o.rotation}),o.filters&&{filters:Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},void 0!==o.filters.exposure&&{exposure:o.filters.exposure}),void 0!==o.filters.contrast&&{contrast:o.filters.contrast}),void 0!==o.filters.saturation&&{saturation:o.filters.saturation}),void 0!==o.filters.temperature&&{temperature:o.filters.temperature}),void 0!==o.filters.tint&&{tint:o.filters.tint}),void 0!==o.filters.highlights&&{highlights:o.filters.highlights}),void 0!==o.filters.shadows&&{shadows:o.filters.shadows})});if(a&&o.imageHash)try{const e=figma.getImageByHash(o.imageHash);if(e){const a=await e.getBytesAsync();if(a){const e=figma.base64Encode(a);t.imageBase64=e}}}catch(t){Logger.log(`⚠️ Could not export image data for style "${e.name}": ${t}`)}n.push(t)}if(0===n.length)return;const g=Object.assign(Object.assign(Object.assign(Object.assign({name:e.name,paints:n},i&&{color:i}),void 0!==l&&{opacity:l}),e.description&&{description:e.description}),c&&Object.keys(c).length>0&&{boundVariables:c});o.push(g)}),o},async importStyles(e,t){let a=0,o=0;const r=new Map;for(const e of await figma.getLocalPaintStylesAsync())r.set(e.name,e);return await runSequentialAsync(e,20,async function(e){var s,n,i,l;let c;r.has(e.name)?(c=r.get(e.name),o++):(c=figma.createPaintStyle(),c.name=e.name,a++),e.description&&(c.description=e.description);const g=[];if(e.paints&&e.paints.length>0){for(const a of e.paints)if("SOLID"===a.type){const o=ColorParser.parse(a.color);let r=null!==(s=a.opacity)&&void 0!==s?s:1;o.a<1&&void 0===a.opacity&&(r=MathUtils.round2(o.a));let n={type:"SOLID",color:{r:o.r,g:o.g,b:o.b},opacity:MathUtils.round2(r)};if(e.boundVariables&&0===g.length)for(const[a,o]of Object.entries(e.boundVariables))if(o.name&&o.collection){const e=t.getVariable(`${o.collection}/${o.name}`);if(e)try{n=figma.variables.setBoundVariableForPaint(n,a,e)}catch(e){Logger.log(`⚠️ Could not bind ${a}: ${e}`)}}g.push(n)}else if("GRADIENT_LINEAR"===a.type||"GRADIENT_RADIAL"===a.type||"GRADIENT_ANGULAR"===a.type||"GRADIENT_DIAMOND"===a.type){const e=a.gradientStops.map(e=>{const t=ColorParser.parse(e.color);return{position:e.position,color:{r:t.r,g:t.g,b:t.b,a:t.a}}}),t=a.gradientTransform?[[a.gradientTransform[0][0],a.gradientTransform[0][1],a.gradientTransform[0][2]],[a.gradientTransform[1][0],a.gradientTransform[1][1],a.gradientTransform[1][2]]]:[[1,0,0],[0,1,0]],o={type:a.type,gradientStops:e,gradientTransform:t,opacity:null!==(n=a.opacity)&&void 0!==n?n:1};g.push(o)}else if("IMAGE"===a.type){let t=null;if(a.imageBase64)try{const o=figma.base64Decode(a.imageBase64);t=figma.createImage(o).hash,Logger.log(`✅ Created image from base64 data for style "${e.name}"`)}catch(t){Logger.log(`⚠️ Could not import image from base64 for style "${e.name}": ${t}`)}if(!t&&a.imageHash){figma.getImageByHash(a.imageHash)?(t=a.imageHash,Logger.log(`✅ Found existing image with hash for style "${e.name}"`)):Logger.log(`⚠️ Image hash not found in file for style "${e.name}", skipping image paint (imageHash cannot be null)`)}if(t){const e=Object.assign(Object.assign({type:"IMAGE",scaleMode:a.scaleMode,imageHash:t,opacity:null!==(i=a.opacity)&&void 0!==i?i:1},void 0!==a.rotation&&{rotation:a.rotation}),a.filters&&{filters:a.filters});g.push(e)}}}else if(e.color){const a=ColorParser.parse(e.color);let o=null!==(l=e.opacity)&&void 0!==l?l:1;a.a<1&&void 0===e.opacity&&(o=MathUtils.round2(a.a));let r={type:"SOLID",color:{r:a.r,g:a.g,b:a.b},opacity:MathUtils.round2(o)};if(e.boundVariables)for(const[a,o]of Object.entries(e.boundVariables))if(o.name&&o.collection){const e=t.getVariable(`${o.collection}/${o.name}`);if(e)try{r=figma.variables.setBoundVariableForPaint(r,a,e)}catch(e){Logger.log(`⚠️ Could not bind ${a}: ${e}`)}}g.push(r)}g.length>0&&(c.paints=g)}),{created:a,updated:o}}},TextStyleProcessor={async export(e){const t=[],a=await figma.getLocalTextStylesAsync();return await runSequentialAsync(a,20,async function(e){const a=Object.assign(Object.assign({name:e.name,fontFamily:e.fontName.family,fontStyle:e.fontName.style,fontSize:e.fontSize,lineHeight:e.lineHeight,letterSpacing:e.letterSpacing,textCase:e.textCase,textDecoration:e.textDecoration},e.description&&{description:e.description}),{boundVariables:await extractBindings(e.boundVariables,["fontSize","lineHeight","letterSpacing","paragraphSpacing","paragraphIndent"])});t.push(a)}),t},async importStyles(e,t){let a=0,o=0;const r=new Map;for(const e of await figma.getLocalTextStylesAsync())r.set(e.name,e);return await runSequentialAsync(e,20,async function(e){let s;r.has(e.name)?(s=r.get(e.name),o++):(s=figma.createTextStyle(),s.name=e.name,a++),e.description&&(s.description=e.description);try{if(await figma.loadFontAsync({family:e.fontFamily,style:e.fontStyle}),s.fontName={family:e.fontFamily,style:e.fontStyle},s.fontSize=e.fontSize,s.lineHeight=e.lineHeight,s.letterSpacing=e.letterSpacing,e.textCase&&(s.textCase=e.textCase),e.textDecoration&&(s.textDecoration=e.textDecoration),e.boundVariables)for(const[a,o]of Object.entries(e.boundVariables))if(o.name&&o.collection){const e=t.getVariable(`${o.collection}/${o.name}`);if(e)try{s.setBoundVariable(a,e)}catch(e){}}}catch(t){Logger.log(`⚠️ Could not load font for ${e.name}: ${t}`)}}),{created:a,updated:o}}},EffectStyleProcessor={async export(e){const t=[],a=await figma.getLocalEffectStylesAsync();return await runSequentialAsync(a,20,async function(e){const a=[];for(const t of e.effects){const e=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:t.type,visible:t.visible},"radius"in t&&{radius:t.radius}),"spread"in t&&{spread:t.spread}),"offset"in t&&{offset:t.offset}),"color"in t&&{color:ColorConverter.toAllFormats(t.color)}),"blendMode"in t&&{blendMode:t.blendMode}),"showShadowBehindNode"in t&&{showShadowBehindNode:t.showShadowBehindNode}),{boundVariables:await extractBindings(t.boundVariables,["color","radius","spread","offsetX","offsetY"])});a.push(e)}const o=Object.assign(Object.assign({name:e.name},e.description&&{description:e.description}),{effects:a});t.push(o)}),t},async importStyles(e,t){let a=0,o=0;const r=new Map;for(const e of await figma.getLocalEffectStylesAsync())r.set(e.name,e);return await runSequentialAsync(e,20,async function(e){let s;r.has(e.name)?(s=r.get(e.name),o++):(s=figma.createEffectStyle(),s.name=e.name,a++),e.description&&(s.description=e.description);const n=e.effects.map(e=>{var t;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:e.type,visible:null===(t=e.visible)||void 0===t||t},void 0!==e.radius&&{radius:e.radius}),void 0!==e.spread&&{spread:e.spread}),void 0!==e.offset&&{offset:e.offset}),void 0!==e.color&&{color:(()=>{const t=ColorParser.parse(e.color);return{r:t.r,g:t.g,b:t.b,a:MathUtils.round2(t.a)}})()}),void 0!==e.blendMode&&{blendMode:e.blendMode}),void 0!==e.showShadowBehindNode&&{showShadowBehindNode:e.showShadowBehindNode})});s.effects=n;for(let a=0;a{var t,a,o,r,s,n,i;const l=e.color?ColorParser.parse(e.color):{r:1,g:0,b:0,a:.1},c={r:l.r,g:l.g,b:l.b,a:MathUtils.round2(l.a)};if("GRID"===e.pattern)return{pattern:"GRID",sectionSize:null!==(t=e.sectionSize)&&void 0!==t?t:10,visible:!1!==e.visible,color:c};const g=null!==(a=e.alignment)&&void 0!==a?a:"STRETCH",d={pattern:e.pattern,gutterSize:null!==(o=e.gutterSize)&&void 0!==o?o:10,count:null!==(r=e.count)&&void 0!==r?r:5,visible:!1!==e.visible,color:c};if("STRETCH"===g)return Object.assign(Object.assign({},d),{alignment:"STRETCH",offset:null!==(s=e.offset)&&void 0!==s?s:0});if("CENTER"===g)return Object.assign(Object.assign({},d),{alignment:"CENTER",sectionSize:null!==(n=e.sectionSize)&&void 0!==n?n:100});{const t=Object.assign(Object.assign({},d),{alignment:g,offset:null!==(i=e.offset)&&void 0!==i?i:0});return void 0!==e.sectionSize&&(t.sectionSize=e.sectionSize),t}});s.layoutGrids=n;for(let a=0;at.name===e);o?await checkVariablesDiff(i,o.modeId,a,n,"",t):l=!0}t.modifiedVariables.some(e=>e.collection===n)||t.newVariables.some(e=>e.collection===n)?(t.modifiedCollections.push(n),t.summary.collectionsModified++):(t.unchangedCollections.push(n),t.summary.collectionsUnchanged++)}return t}function countVariablesInCollection(e){let t=0;const a=Object.values(e)[0];return a&&(t=countVarsInNestedObj(a)),t}function countVarsInNestedObj(e){let t=0;for(const a of Object.values(e))isExportVariableValue(a)?t++:t+=countVarsInNestedObj(a);return t}async function checkVariablesDiff(e,t,a,o,r,s){for(const[n,i]of Object.entries(a)){const a=r?`${r}/${n}`:n;if(isExportVariableValue(i)){const e=variableCache.getVariable(`${o}/${a}`);if(e){const r=e.valuesByMode[t],n=i.$value;valuesAreDifferent(r,n)?(s.modifiedVariables.push({collection:o,path:a,oldValue:formatValueForDisplay(r),newValue:formatValueForDisplay(n)}),s.summary.variablesModified++):(s.unchangedVariables++,s.summary.variablesUnchanged++)}else s.newVariables.push({collection:o,path:a}),s.summary.variablesNew++}else await checkVariablesDiff(e,t,i,o,a,s)}}function valuesAreDifferent(e,t){if(void 0===e)return!0;if(isVariableAlias(e))return"string"==typeof t&&t.startsWith("{"),!0;if("object"==typeof e&&null!==e&&"r"in e){if("object"==typeof t&&null!==t&&"hex"in t){return ColorConverter.toAllFormats(e).hex.toLowerCase()!==t.hex.toLowerCase()}return!0}return e!==t}function formatValueForDisplay(e){if(void 0===e)return"undefined";if("object"==typeof e&&null!==e){if("hex"in e)return e.hex;if("r"in e)return ColorConverter.toAllFormats(e).hex;if("id"in e)return"{alias}"}return String(e)}async function computeStylesDiff(e,t){if(e.colorStyles){const a=await figma.getLocalPaintStylesAsync(),o=new Set(a.map(e=>e.name));for(const a of e.colorStyles)o.has(a.name)?(t.modifiedStyles.push({type:"color",name:a.name}),t.summary.stylesModified++):(t.newStyles.push({type:"color",name:a.name}),t.summary.stylesNew++)}if(e.textStyles){const a=await figma.getLocalTextStylesAsync(),o=new Set(a.map(e=>e.name));for(const a of e.textStyles)o.has(a.name)?(t.modifiedStyles.push({type:"text",name:a.name}),t.summary.stylesModified++):(t.newStyles.push({type:"text",name:a.name}),t.summary.stylesNew++)}if(e.effectStyles){const a=await figma.getLocalEffectStylesAsync(),o=new Set(a.map(e=>e.name));for(const a of e.effectStyles)o.has(a.name)?(t.modifiedStyles.push({type:"effect",name:a.name}),t.summary.stylesModified++):(t.newStyles.push({type:"effect",name:a.name}),t.summary.stylesNew++)}if(e.gridStyles){const a=await figma.getLocalGridStylesAsync(),o=new Set(a.map(e=>e.name));for(const a of e.gridStyles)o.has(a.name)?(t.modifiedStyles.push({type:"grid",name:a.name}),t.summary.stylesModified++):(t.newStyles.push({type:"grid",name:a.name}),t.summary.stylesNew++)}}function filterStylesByGroup(e,t){return t?e.filter(function(e){return-1!==t.indexOf(getGroupKey(e.name))}):e}async function exportVariables(e,t,a,o,r="original",s="figma",n,i=!1,l,c){var g,d,f,u,m,p,y,b;Logger.log("📤 Starting export..."),Logger.log(` preserveLibraryRefs: ${a}`),Logger.log(` includeImages: ${o}`),Logger.log(` namingConvention: ${r}`),Logger.log(` exportFormat: ${s}`),Logger.log(` resolveAliases: ${i}`),n&&Logger.log(` selectedModes: ${JSON.stringify(n)}`);try{let a=await figma.variables.getLocalVariableCollectionsAsync();(null==e?void 0:e.length)&&(a=a.filter(t=>e.includes(t.name)),Logger.log(`Filtering to ${a.length} selected collections`));const h=[],v={};let C=0;const S=createProgress("export");let A=0;for(let e=0;ea.includes(e.name)),Logger.log(` Filtering to ${t.length} modes: ${t.map(e=>e.name).join(", ")}`)}const a=NamingConverter.convertCollectionName(e.name,r),o={[a]:Object.assign({modes:{}},a!==e.name&&{$originalName:e.name})};for(const e of t){const t=NamingConverter.convertModeName(e.name,r);o[a].modes[t]={}}await runSequentialAsync(e.variableIds,BATCH.SEQ_EXPORT,async function(s){var n,c;L++;const g=await figma.variables.getVariableByIdAsync(s);if(!g)return;if(l&&l[e.name]&&-1===l[e.name].indexOf(getGroupKey(g.name)))return;C++;const d=g.name.split("/").map(e=>NamingConverter.convert(e,r));for(const e of t){const t=NamingConverter.convertModeName(e.name,r),s=o[a].modes[t],l=g.valuesByMode[e.modeId];let f=s;for(let e=0;eNamingConverter.convert(e,r)).join(".")}}`,m=v,h){const e=t.valuesByMode[Object.keys(t.valuesByMode)[0]];"object"==typeof e&&null!==e&&"r"in e?p=ColorConverter.toAllFormats(e):isVariableAlias(e)||(p=e)}}}else m=""}else m="object"==typeof l&&null!==l&&"r"in l?ColorConverter.toAllFormats(l):l;const C=Object.assign(Object.assign(Object.assign({$scopes:TypeMapper.scopesToArray(g.scopes),$type:TypeMapper.toExportType(g.resolvedType),$value:m},g.description&&{$description:g.description}),y&&b&&{$collectionName:b}),y&&h&&Object.assign({$libraryRef:v},void 0!==p&&{$localValue:p}));f[u]=C}},function(){S.report("export_variables","Exporting variables",L,A)}),h.push(o),"w3c"===s&&(v[a]=W3CConverter.collectionToW3C(a,o[a].modes,r,o[a].$originalName))}let w=null;if(t){if(w={},t.colorStyles){S.report("export_styles","Exporting styles",0,0,!0);const e=c?c.color:void 0;w.colorStyles=filterStylesByGroup(await ColorStyleProcessor.export({includeImages:o}),e),e&&0===w.colorStyles.length&&delete w.colorStyles}if(t.textStyles){S.report("export_styles","Exporting styles",0,0,!0);const e=c?c.text:void 0;w.textStyles=filterStylesByGroup(await TextStyleProcessor.export(),e),e&&0===w.textStyles.length&&delete w.textStyles}if(t.effectStyles){S.report("export_styles","Exporting styles",0,0,!0);const e=c?c.effect:void 0;w.effectStyles=filterStylesByGroup(await EffectStyleProcessor.export(),e),e&&0===w.effectStyles.length&&delete w.effectStyles}if(t.gridStyles){S.report("export_styles","Exporting styles",0,0,!0);const e=c?c.grid:void 0;w.gridStyles=filterStylesByGroup(await GridStyleProcessor.export(),e),e&&0===w.gridStyles.length&&delete w.gridStyles}Object.keys(w).length>0?h.push({_styles:w}):w=null}const $={collections:a.length,variables:C,styles:w?{color:null!==(d=null===(g=w.colorStyles)||void 0===g?void 0:g.length)&&void 0!==d?d:0,text:null!==(u=null===(f=w.textStyles)||void 0===f?void 0:f.length)&&void 0!==u?u:0,effect:null!==(p=null===(m=w.effectStyles)||void 0===m?void 0:m.length)&&void 0!==p?p:0,grid:null!==(b=null===(y=w.gridStyles)||void 0===y?void 0:y.length)&&void 0!==b?b:0}:null};let O;"w3c"===s?(w&&Object.keys(w).length>0&&(v.$extensions={"com.figma":{styles:w}}),O=JSON.stringify(v,null,2),Logger.log(`✅ Export complete (W3C format): ${$.collections} collections, ${$.variables} variables`)):(O=JSON.stringify(h,null,2),Logger.log(`✅ Export complete: ${$.collections} collections, ${$.variables} variables`)),await sendExportInChunks(O,$,s)}catch(e){if(isCancelError(e))return Logger.log("🛑 Export cancelled"),void figma.ui.postMessage({type:"operation_cancelled",operation:"export",phase:"export",rolledBack:!1,message:"Export cancelled. Nothing was changed."});Logger.log(`❌ Export error: ${e}`),Logger.send("error",{message:`Export failed: ${e}`})}}async function sendExportInChunks(e,t,a){const o=e.length,r=BATCH.EXPORT_CHUNK_BYTES,s=Math.max(1,Math.ceil(o/r));let n=0,i=0;for(;i=55296&&a<=56319&&(t+=1)}const a=e.slice(i,t);figma.ui.postMessage({type:"export_chunk",seq:n,total:s,data:a}),n++,i=t,n%BATCH.EXPORT_YIELD_EVERY===0&&i0?o.modes[r[0]]:{},"");h.push({collectionObj:t,flatPaths:s}),v+=s.length;for(let e=0;e0&&Logger.log(` ✅ Aliases: ${L} resolved, ${w} used fallback values`),y&&t.importStyles){if(Logger.log("📦 Importing styles..."),s.report("import_styles","Importing styles",0,0,!0),y.colorStyles){const e=await ColorStyleProcessor.importStyles(y.colorStyles,variableCache);m+=e.created,p+=e.updated}if(y.textStyles){const e=await TextStyleProcessor.importStyles(y.textStyles,variableCache);m+=e.created,p+=e.updated}if(y.effectStyles){const e=await EffectStyleProcessor.importStyles(y.effectStyles,variableCache);m+=e.created,p+=e.updated}if(y.gridStyles){const e=await GridStyleProcessor.importStyles(y.gridStyles,variableCache);m+=e.created,p+=e.updated}}const $={collectionsCreated:g,variablesCreated:d,variablesUpdated:f,variablesSkipped:u,stylesCreated:m,stylesUpdated:p};figma.commitUndo(),Logger.log("✅ Import complete!"),Logger.send("import_complete",{stats:$,snapshot:r})}catch(e){const t=isCancelError(e);if(t&&!n)return Logger.log("🛑 Import cancelled before any changes were made"),void figma.ui.postMessage({type:"operation_cancelled",operation:"import",phase:"snapshot",rolledBack:!1,message:"Import cancelled. No changes were made."});const a=e instanceof Error?e.message:String(e);if(t?Logger.log("🛑 Import cancelled after mutation started — rolling back..."):Logger.log(`❌ Import error: ${a}`),r){Logger.log("🔄 Attempting automatic rollback to pre-import state..."),Logger.send("import_rolling_back",{error:a});try{await restoreFromSnapshot(r),Logger.log("✅ Automatic rollback successful - file restored to pre-import state"),t?figma.ui.postMessage({type:"operation_cancelled",operation:"import",phase:"rollback",rolledBack:!0,message:"Import cancelled — your file was restored to its previous state."}):Logger.send("import_rollback_complete",{error:a,message:"Import failed but your file has been automatically restored to its previous state."})}catch(e){const t=e instanceof Error?e.message:String(e);Logger.log(`❌ Rollback failed: ${t}`),Logger.send("import_rollback_failed",{error:a,rollbackError:t,message:"Import failed and automatic rollback also failed. Please use Ctrl+Z (Cmd+Z) to undo manually."})}}else Logger.send("error",{message:`Import failed: ${a}. Use Ctrl+Z (Cmd+Z) to undo changes.`})}}function setRawValue(e,t,a){try{if("color"===a.$type){const o=ColorParser.parse(a.$value),r=o.a<1?Object.assign(Object.assign({},o),{a:MathUtils.round2(o.a)}):o;e.setValueForMode(t,r)}else e.setValueForMode(t,a.$value)}catch(e){console.error(`Could not set value: ${e}`)}}function getGroupKey(e){const t=e.indexOf("/");return-1===t?"":e.substring(0,t)}function summarizeGroups(e){const t=Object.create(null),a=[];for(let o=0;o{Logger.log(` ${t+1}. "${e.name}" (id: ${e.id})`)});const t=new Set;let a=0,o=0,r=0;const s=new Map,n=[];for(let i=0;ie.name),variableCount:l.variableIds.length,types:c,groups:summarizeGroups(g)})}n.sort((e,t)=>e.name.localeCompare(t.name));const i=await figma.getLocalPaintStylesAsync(),l=await figma.getLocalTextStylesAsync(),c=await figma.getLocalEffectStylesAsync(),g=await figma.getLocalGridStylesAsync();let d=0;const f=[];for(let e=0;e({family:e,styles:Array.from(t)}));let C=0;for(const e of i)e.boundVariables&&Object.keys(e.boundVariables).length>0&&C++;Logger.send("collections",{collections:n,styles:u,styleGroups:b,libraryDependencies:Array.from(t),fontsUsed:v,stats:{totalVariables:n.reduce((e,t)=>e+t.variableCount,0),totalAliases:a,localAliases:o,libraryAliases:r,styleBindings:C}})}async function clearVariables(e=!1){Logger.log("🗑️ Clearing all variables...");const t=e?null:createProgress("clear");let a=0,o=0,r=!1;try{const s=await figma.variables.getLocalVariableCollectionsAsync(),n=[];let i=0;for(let e=0;e0&&(figma.commitUndo(),r=!0);let l=0;for(let e=0;e0&&(figma.commitUndo(),o=!0);let c=0;const removeStyle=function(e){e.remove(),a++},onBatch=function(e){t&&t.report("clear","Deleting styles",c+e,l)};await runBatched(r,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=r.length,await runBatched(s,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=s.length,await runBatched(n,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=n.length,await runBatched(i,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=i.length,!e&&o&&figma.commitUndo(),Logger.log(`✅ Cleared ${a} styles`),e||Logger.send("clear_complete",{message:`${a} styles`})}catch(t){if(isCancelError(t)){if(e)throw t;return Logger.log(`🛑 Clear styles cancelled after ${a} styles`),void figma.ui.postMessage({type:"operation_cancelled",operation:"clear",phase:"clear",rolledBack:!1,partial:{collectionsDeleted:0,variablesDeleted:a},message:`Clear cancelled — ${a} styles were already deleted. Remaining items were not touched. Use Cmd+Z to restore deleted items.`})}if(Logger.log(`❌ Clear styles error: ${t}`),e)throw t;Logger.send("error",{message:`Failed to clear styles: ${t}`})}}async function clearAll(e=!1){if(Logger.log("🗑️ Clearing everything..."),e)return await clearVariables(!0),void await clearStyles(!0);let t=!1;try{figma.commitUndo(),t=!0,await clearVariables(!0),await clearStyles(!0),figma.commitUndo(),Logger.send("clear_complete",{message:"all variables and styles"})}catch(e){if(t&&figma.commitUndo(),isCancelError(e))return Logger.log("🛑 Clear all cancelled"),void figma.ui.postMessage({type:"operation_cancelled",operation:"clear",phase:"clear",rolledBack:!1,partial:{collectionsDeleted:0,variablesDeleted:0},message:"Clear cancelled — some items may already have been deleted. Use Cmd+Z to restore deleted items."});Logger.log(`❌ Clear all error: ${e}`),Logger.send("error",{message:`Failed to clear: ${e}`})}}async function createUndoSnapshot(e){var t;Logger.log("📸 Creating snapshot of current file state...");const a=await figma.variables.getLocalVariableCollectionsAsync(),o=[],r=new Map;let s=0;for(let e=0;e({id:e.modeId,name:e.name})),variables:[]},i=await runBatchedAsync(t.variableIds,BATCH.ASYNC_LOOKUP,function(e){return figma.variables.getVariableByIdAsync(e)},function(t){e&&e.report("snapshot","Preparing snapshot (undo safety)",n+t,s)});n+=t.variableIds.length;for(let e=0;e0){t.renameMode(t.modes[0].modeId,e.modes[0].name);for(let a=1;a0&&(r.scopes=o.scopes);for(const t of e.modes){const n=a[t.name],i=o.values[t.name];if(i)if(i.isAlias&&i.aliasName)s.push({variable:r,modeId:n,aliasPath:i.aliasName,aliasCollection:i.aliasCollection||e.name});else if(void 0!==i.value){let e;e="COLOR"===o.type&&"string"==typeof i.value?ColorParser.parse(i.value):i.value,r.setValueForMode(n,e)}}}},function(e,a){t.report("undo_restore","Restoring variables",e,a)}),Logger.log(` Resolving ${s.length} aliases...`),await runBatched(s,BATCH.SYNC_LIGHT,function(e){const t=`${e.aliasCollection}/${e.aliasPath}`,a=variableCache.getVariable(t);a&&e.variable.setValueForMode(e.modeId,{type:"VARIABLE_ALIAS",id:a.id})},function(e,a){t.report("undo_aliases","Restoring aliases",e,a)}),Logger.log(" Restoring styles..."),t.report("undo_styles","Restoring styles",0,0,!0),o.colorStyles&&o.colorStyles.length>0&&await ColorStyleProcessor.importStyles(o.colorStyles,variableCache),o.textStyles&&o.textStyles.length>0&&await TextStyleProcessor.importStyles(o.textStyles,variableCache),o.effectStyles&&o.effectStyles.length>0&&await EffectStyleProcessor.importStyles(o.effectStyles,variableCache),o.gridStyles&&o.gridStyles.length>0&&await GridStyleProcessor.importStyles(o.gridStyles,variableCache),Logger.log("✅ File restored from snapshot")}figma.ui.onmessage=async e=>{switch(e.type){case"cancel_operation":null!==currentOperation.type&&(currentOperation.cancellable?(currentOperation.cancelRequested=!0,Logger.log("🛑 Cancellation requested — finishing current batch…")):Logger.log("⚠️ Rollback in progress — cannot cancel"));break;case"export":await withOperation("export",function(){return exportVariables(e.collections,e.styleOptions,e.preserveLibraryRefs,e.includeImages,e.namingConvention||"original",e.exportFormat||"figma",e.selectedModes,e.resolveAliases||!1,e.selectedGroups,e.selectedStyleGroups)});break;case"import":await withOperation("import",function(){return importVariables(e.data,e.options)});break;case"validate_import":try{const t=JSON.parse(e.data),a=e.plan,o=await validateImportAgainstPlan(t,a);Logger.send("validation_result",o)}catch(e){Logger.send("validation_result",{errors:[`Invalid JSON: ${e instanceof Error?e.message:"Parse error"}`],canImport:!1})}break;case"compute_import_diff":try{const t=JSON.parse(e.data),a=await computeImportDiff(t);Logger.send("import_diff_result",a)}catch(e){Logger.send("import_diff_result",{error:`Failed to compute diff: ${e instanceof Error?e.message:"Unknown error"}`})}break;case"detect_plan":const t=await detectCurrentPlan();Logger.send("plan_detected",t);break;case"clear_variables":await withOperation("clear",function(){return clearVariables(!1)});break;case"clear_styles":await withOperation("clear",function(){return clearStyles(!1)});break;case"clear_all":await withOperation("clear",function(){return clearAll(!1)});break;case"get_collections":await withOperation("scan",getCollections);break;case"check_libraries":try{const t=e.collections;await variableCache.rebuild();const a=[],o=[];for(const e of t)variableCache.isCollectionAvailable(e)?a.push(e):o.push(e);Logger.log(`📚 Library check: ${a.length} available, ${o.length} missing`),a.length>0&&Logger.log(` ✅ Available: ${a.join(", ")}`),o.length>0&&Logger.log(` ❌ Missing: ${o.join(", ")}`),Logger.send("library_check_result",{allAvailable:0===o.length,availableCollections:a,missingCollections:o,requiredCollections:t})}catch(t){Logger.send("library_check_result",{allAvailable:!1,availableCollections:[],missingCollections:e.collections||[],requiredCollections:e.collections||[],error:t instanceof Error?t.message:"Library check failed"})}break;case"check_fonts":try{const t=e.fonts,a=[],o=[],r=await runBatchedAsync(t,BATCH.ASYNC_FONT,function(e){return figma.loadFontAsync({family:e.family,style:e.style}).then(function(){return{font:e,available:!0}}).catch(function(){return{font:e,available:!1}})});for(let e=0;e0&&n>=r)&&l-oo&&(o=t.modes.length);return t=o>20?"enterprise":o>10?"organization":"professional",Object.assign({plan:t},PLAN_LIMITS[t])}async function validateImportAgainstPlan(e,t){const o=t?Object.assign({plan:t},PLAN_LIMITS[t]):await detectCurrentPlan(),a=await figma.variables.getLocalVariableCollectionsAsync(),s=a.reduce((e,t)=>Math.max(e,t.modes.length),0),n=(await figma.variables.getLocalVariablesAsync()).length,r=[];for(const t of e)"_styles"in t||r.push(t);let i=0,l=0;const c=[];for(const e of r){const t=Object.keys(e)[0],a=e[t];if(!a||!a.modes)continue;const s=Object.keys(a.modes).length;s>i&&(i=s),s>o.maxModesPerCollection&&c.push(`"${t}" (${s} modes, limit: ${o.maxModesPerCollection===1/0?"∞":o.maxModesPerCollection})`);const n=Object.values(a.modes)[0];n&&(l+=countNestedVariables(n))}const g=[],d=[];c.length;for(const e of r){const t=Object.keys(e)[0],o=e[t];if(!o||!o.modes)continue;const a=Object.values(o.modes)[0],s=a?countNestedVariables(a):0;s>5e3&&d.push(`Collection "${t}" has ${s} variables, exceeds limit of 5000`)}l>1e3&&g.push(`Large import: ${l} variables. This may take a moment.`),r.length>10&&g.push(`Importing ${r.length} collections. Consider importing in batches.`);const f=new Set;let u=0;for(const e of r){const t=e[Object.keys(e)[0]];if(t&&t.modes)for(const e of Object.keys(t.modes)){const o=flattenVariables(t.modes[e],"");for(const{value:e}of o)e.$libraryRef&&e.$collectionName&&(f.add(e.$collectionName),u++)}}const p=[];let m=0;for(const t of e)if("_styles"in t){const e=t._styles;if(e.textStyles)for(const t of e.textStyles){m++;const e=`${t.fontFamily}|${t.fontStyle}`;p.some(t=>`${t.family}|${t.style}`===e)||p.push({family:t.fontFamily,style:t.fontStyle})}}return Object.assign(Object.assign({currentPlan:o,existing:{collections:a.length,maxModesInAnyCollection:s,totalVariables:n},importing:{collections:r.length,maxModesInAnyCollection:i,totalVariables:l,collectionsExceedingModeLimit:c},warnings:g,errors:d,canImport:0===d.length},f.size>0&&{libraryDependencies:{variableCount:u,collections:Array.from(f)}}),p.length>0&&{fontDependencies:{styleCount:m,fonts:p}})}function countNestedVariables(e,t=0){for(const[,o]of Object.entries(e))o&&"object"==typeof o&&("$type"in o&&"$value"in o?t++:t=countNestedVariables(o,t));return t}const MathUtils={round2:e=>Math.round(100*e)/100,clamp:(e,t,o)=>Math.max(t,Math.min(o,e)),toHexByte:e=>Math.round(255*e).toString(16).padStart(2,"0"),fromHexByte:e=>parseInt(e,16)/255};function calculateHue(e,t,o,a,s){if(a===s)return 0;const n=a-s;let r=0;switch(a){case e:r=((t-o)/n+(t.5?e/(2-s-n):e/(s+n)}const l={h:calculateHue(t,o,a,s,n),s:Math.round(100*i),l:Math.round(100*r)},c=e.a;return void 0!==c&&c<1?Object.assign(Object.assign({},l),{a:MathUtils.round2(c)}):l},toHsb(e){const{r:t,g:o,b:a}=e,s=Math.max(t,o,a),n=Math.min(t,o,a),r=0===s?0:(s-n)/s,i={h:calculateHue(t,o,a,s,n),s:Math.round(100*r),b:Math.round(100*s)},l=e.a;return void 0!==l&&l<1?Object.assign(Object.assign({},i),{a:MathUtils.round2(l)}):i},toAllFormats(e){return{hex:this.toHex(e),rgb:this.toRgb255(e),css:this.toCss(e),hsl:this.toHsl(e),hsb:this.toHsb(e)}}},NamingConverter={convert(e,t){if("original"===t)return e;const o=e.replace(/([a-z])([A-Z])/g,"$1 $2").split(/[\s\/\-_]+/).filter(e=>e.length>0).map(e=>e.toLowerCase());if(0===o.length)return e;switch(t){case"camelCase":return o[0]+o.slice(1).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("");case"kebab-case":return o.join("-");case"snake_case":return o.join("_");default:return e}},convertPath(e,t){return"original"===t?e:e.split("/").map(e=>this.convert(e,t)).join("/")},convertCollectionName(e,t){return this.convert(e,t)},convertModeName(e,t){return this.convert(e,t)},addOriginalName(e,t){if("original"===t)return{converted:e};const o=this.convert(e,t);return o===e?{converted:e}:{converted:o,original:e}}};async function resolveAliasValue(e,t,o=10){if(o<=0)return Logger.log(`⚠️ Max alias resolution depth reached for ${e.name}`),"";let a=e.valuesByMode[t];if(void 0===a){const t=Object.keys(e.valuesByMode);t.length>0&&(a=e.valuesByMode[t[0]])}if(void 0===a)return"";if(isVariableAlias(a)){const e=await figma.variables.getVariableByIdAsync(a.id);return e?resolveAliasValue(e,t,o-1):""}return a}const W3C_TYPE_MAP={color:"color",float:"number",string:"string",boolean:"boolean"},W3CConverter={colorToW3C:e=>e.hex,typeToW3C:e=>W3C_TYPE_MAP[e]||"string",valueToW3C(e,t=!1){const o={$value:"",$type:this.typeToW3C(e.$type)};return t&&"string"==typeof e.$value&&e.$value.startsWith("{")?o.$value=e.$value:"color"===e.$type&&"object"==typeof e.$value?o.$value=e.$value.hex:o.$value=e.$value,e.$description&&(o.$description=e.$description),e.$scopes&&e.$scopes.length>0&&!e.$scopes.includes("ALL_SCOPES")&&(o.$extensions={"com.figma":{scopes:e.$scopes}}),o},collectionToW3C(e,t,o,a){const s={};a&&a!==e&&(s.$description=`Figma collection: ${a}`);const n=Object.keys(t);if(1===n.length)this.addTokensToGroup(s,t[n[0]],o);else for(const e of n){const a=NamingConverter.convertModeName(e,o);s[a]={},this.addTokensToGroup(s[a],t[e],o)}return s},addTokensToGroup(e,t,o){for(const[a,s]of Object.entries(t)){const t=NamingConverter.convert(a,o);if(isExportVariableValue(s)){const o="string"==typeof s.$value&&s.$value.startsWith("{");e[t]=this.valueToW3C(s,o)}else e[t]={},this.addTokensToGroup(e[t],s,o)}},parseW3CToken(e){var t,o;const a=this.w3cTypeToFigma(e.$type),s=(null===(o=null===(t=e.$extensions)||void 0===t?void 0:t["com.figma"])||void 0===o?void 0:o.scopes)||["ALL_SCOPES"];let n;if("color"===a&&"string"==typeof e.$value){const t=ColorParser.parse(e.$value);n=ColorConverter.toAllFormats(t)}else n="string"==typeof e.$value||"number"==typeof e.$value||"boolean"==typeof e.$value?e.$value:JSON.stringify(e.$value);return e.$description?{$type:a,$value:n,$scopes:s,$description:e.$description}:{$type:a,$value:n,$scopes:s}},w3cTypeToFigma:e=>({color:"color",number:"float",dimension:"float",string:"string",boolean:"boolean",fontFamily:"string",fontWeight:"float",duration:"string",cubicBezier:"string"}[e]||"string"),isW3CFormat(e){if("object"!=typeof e||null===e)return!1;const t=e;for(const e of Object.keys(t)){const o=t[e];if("object"==typeof o&&null!==o){if("$value"in o&&"$type"in o)return!0;for(const e of Object.keys(o)){const t=o[e];if("object"==typeof t&&null!==t&&"$value"in t)return!0}}}return Array.isArray(e),!1},w3cToFigmaFormat(e){const t=[];for(const[o,a]of Object.entries(e)){if(o.startsWith("$"))continue;const e={[o]:{modes:{Default:this.w3cGroupToNestedVars(a)}}};t.push(e)}return t},w3cGroupToNestedVars(e){const t={};for(const[o,a]of Object.entries(e))o.startsWith("$")||(this.isW3CToken(a)?t[o]=this.parseW3CToken(a):"object"==typeof a&&null!==a&&(t[o]=this.w3cGroupToNestedVars(a)));return t},isW3CToken:e=>"object"==typeof e&&null!==e&&"$value"in e},TS_FLOAT_SCOPE_MAP={CORNER_RADIUS:"borderRadius",STROKE_FLOAT:"borderWidth",GAP:"spacing",WIDTH_HEIGHT:"sizing",OPACITY:"opacity",FONT_SIZE:"fontSizes",LINE_HEIGHT:"lineHeights",LETTER_SPACING:"letterSpacing",PARAGRAPH_SPACING:"paragraphSpacing"},TS_STRING_SCOPE_MAP={FONT_FAMILY:"fontFamilies",FONT_STYLE:"fontWeights"},TS_TEXT_CASE_MAP={ORIGINAL:"none",UPPER:"uppercase",LOWER:"lowercase",TITLE:"capitalize"},TS_TEXT_DECORATION_MAP={NONE:"none",UNDERLINE:"underline",STRIKETHROUGH:"line-through"},TokensStudioConverter={sanitizeSegment(e){let t=e.replace(/[{}$]/g,"");return 0===t.length&&(t="_"),"__proto__"!==t&&"constructor"!==t&&"prototype"!==t||(t="_"+t),t},sanitizeAliasRef(e){const t=e.substring(1,e.length-1).split("."),o=[];for(let e=0;e"string"==typeof e&&"{"===e.charAt(0)&&"}"===e.charAt(e.length-1),roundNumber:e=>Math.round(1e3*e)/1e3,colorToTS(e){const t=void 0!==e.rgb?e.rgb.a:void 0;return void 0!==t&&t<1?e.css:void 0!==e.rgb&&e.hex.length>7?e.hex.substring(0,7):e.hex},floatTypeFromScopes(e){if(!e)return"number";let t="",o=0;for(let a=0;ae.toLowerCase().replace(/\s+/g,"-")};function convertToTokensStudio(e){const t=TokensStudioConverter,o={},a=[],s={libraryRefsSkipped:0,imagePaintsSkipped:0,blurEffectsSkipped:0},n=[];let r=null;for(let t=0;t0){const e={};for(let o=0;o0){const t=styleSetKey("styles/color");o[t]=e,a.push(t),i.push(t)}}if(void 0!==r.textStyles&&r.textStyles.length>0){const e={};for(let o=0;o0){const t=styleSetKey("styles/typography");o[t]=e,a.push(t),i.push(t)}}if(void 0!==r.effectStyles&&r.effectStyles.length>0){const e={};for(let o=0;o0){const t=styleSetKey("styles/effects");o[t]=e,a.push(t),i.push(t)}}void 0!==r.gridStyles&&r.gridStyles.length>0&&Logger.log("Tokens Studio export: grid styles skipped (no Tokens Studio representation)")}s.libraryRefsSkipped>0&&Logger.log("Tokens Studio export: skipped "+s.libraryRefsSkipped+" library-alias token(s) with no resolvable local value"),s.imagePaintsSkipped>0&&Logger.log("Tokens Studio export: skipped "+s.imagePaintsSkipped+" image paint(s) in color styles (no Tokens Studio representation)"),s.blurEffectsSkipped>0&&Logger.log("Tokens Studio export: skipped "+s.blurEffectsSkipped+" blur effect(s) in effect styles (boxShadow tokens carry shadows only)");const l=[];for(let e=0;e{const a=o<0?o+1:o>1?o-1:o;return a<1/6?e+6*(t-e)*a:a<.5?t:a<2/3?e+(t-e)*(2/3-a)*6:e},r=n<.5?n*(1+s):n+s-n*s,i=2*n-r;return{r:hue2rgb(i,r,a+1/3),g:hue2rgb(i,r,a),b:hue2rgb(i,r,a-1/3),a:null!==(o=e.a)&&void 0!==o?o:1}},fromHsb(e){var t;const o=e.h/360,a=e.s/100,s=e.b/100,n=Math.floor(6*o),r=6*o-n,i=s*(1-a),l=s*(1-r*a),c=s*(1-(1-r)*a),g=[[s,c,i],[l,s,i],[i,s,c],[i,l,s],[c,i,s],[s,i,l]],[d,f,u]=g[n%6];return{r:d,g:f,b:u,a:null!==(t=e.a)&&void 0!==t?t:1}},parse(e){var t;if("object"==typeof e&&null!==e&&"hex"in e&&"rgb"in e)return this.fromHex(e.hex);if("object"==typeof e&&null!==e&&"r"in e&&"g"in e&&"b"in e){const o=e;return o.r<=1&&o.g<=1&&o.b<=1?{r:o.r,g:o.g,b:o.b,a:null!==(t=o.a)&&void 0!==t?t:1}:this.fromRgb255(o)}return"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"l"in e?this.fromHsl(e):"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"b"in e?this.fromHsb(e):"string"==typeof e?e.startsWith("rgb")||e.startsWith("hsl")?this.fromCss(e):this.fromHex(e):{r:0,g:0,b:0,a:1}}};class VariableCache{constructor(){this.collectionMap=new Map,this.variableMap=new Map,this.libraryVariableMap=new Map,this.libraryCollectionNames=new Set,this.initialized=!1,this.libraryIndexed=!1}async ensureReady(){this.initialized||(await this.rebuildLocal(),this.initialized=!0)}async initialize(){await this.ensureReady()}async rebuild(){await this.rebuildLocal(),await this.ensureLibraryIndex(),this.initialized=!0}async rebuildLocal(){this.clearLocal();const e=await figma.variables.getLocalVariableCollectionsAsync();for(let t=0;t{try{const o=await figma.variables.importVariableByKeyAsync(e.key);o&&this.libraryVariableMap.set(`${t}/${o.name}`,o)}catch(e){}})}catch(e){Logger.log(` ⚠️ Could not index library collection "${o.name}": ${e}`)}}this.libraryCollectionNames.size>0&&Logger.log(`📚 Indexed ${this.libraryVariableMap.size} library variables from ${this.libraryCollectionNames.size} connected libraries`)}catch(e){Logger.log(`⚠️ Could not access team library: ${e}`)}}getCollection(e){return this.collectionMap.get(e)}getVariable(e){return this.variableMap.get(e)||this.libraryVariableMap.get(e)}setVariable(e,t){this.variableMap.set(e,t)}setCollection(e,t){this.collectionMap.set(e,t)}removeCollection(e){this.collectionMap.delete(e);const t=[];for(const o of this.variableMap.keys())o.startsWith(`${e}/`)&&t.push(o);for(const e of t)this.variableMap.delete(e)}isCollectionAvailable(e){return this.collectionMap.has(e)||this.libraryCollectionNames.has(e)}getLibraryCollectionNames(){return Array.from(this.libraryCollectionNames)}get size(){return this.variableMap.size}get collections(){return this.collectionMap.values()}getVariableKeys(){return Array.from(this.variableMap.keys())}}const variableCache=new VariableCache;function isExportVariableValue(e){return"object"==typeof e&&null!==e&&"$type"in e}function isVariableAlias(e){return"object"==typeof e&&null!==e&&"VARIABLE_ALIAS"===e.type}const TypeMapper={toExportType(e){var t;return null!==(t={COLOR:"color",FLOAT:"float",STRING:"string",BOOLEAN:"boolean"}[e])&&void 0!==t?t:"string"},toFigmaType(e){var t;return null!==(t={color:"COLOR",float:"FLOAT",string:"STRING",boolean:"BOOLEAN"}[e])&&void 0!==t?t:"STRING"},scopesToArray:e=>0===e.length||e.includes("ALL_SCOPES")?["ALL_SCOPES"]:[...e],arrayToScopes:e=>e.includes("ALL_SCOPES")?["ALL_SCOPES"]:e};async function getVariableBindingInfo(e,t){if(!(null==e?void 0:e[t]))return{};const o=e[t];if(!o)return{};const a=await figma.variables.getVariableByIdAsync(o.id);if(!a)return{id:o.id};const s=await figma.variables.getVariableCollectionByIdAsync(a.variableCollectionId);return{id:o.id,name:a.name,collection:null==s?void 0:s.name}}async function extractBindings(e,t){if(!e)return;const o={};for(const a of t){const t=await getVariableBindingInfo(e,a);t.name&&(o[a]=t)}return Object.keys(o).length>0?o:void 0}function flattenVariables(e,t){const o=[];for(const a of Object.keys(e)){const s=e[a],n=t?`${t}/${a}`:a;isExportVariableValue(s)?o.push({path:n,value:s}):o.push(...flattenVariables(s,n))}return o}function getValueAtPath(e,t){const o=t.split("/");let a=e;for(const e of o){if("object"!=typeof a||null===a)return null;if(isExportVariableValue(a))return null;a=a[e]}return isExportVariableValue(a)?a:null}const ColorStyleProcessor={async export(e){var t;const o=null!==(t=null==e?void 0:e.includeImages)&&void 0!==t&&t,a=[],s=await figma.getLocalPaintStylesAsync();return await runSequentialAsync(s,20,async function(e){var t,s,n;if(0===e.paints.length)return;const r=[];let i,l,c;for(const a of e.paints)if("SOLID"===a.type){const e=a.color;let o=null!==(t=a.opacity)&&void 0!==t?t:1;void 0!==e.a&&e.a<1&&1===o&&(o=e.a);const s={r:a.color.r,g:a.color.g,b:a.color.b,a:o},n={type:"SOLID",color:ColorConverter.toAllFormats(s),opacity:MathUtils.round2(o)};r.push(n),i||(i=n.color,l=n.opacity,c=await extractBindings(a.boundVariables,["color"]))}else if("GRADIENT_LINEAR"===a.type||"GRADIENT_RADIAL"===a.type||"GRADIENT_ANGULAR"===a.type||"GRADIENT_DIAMOND"===a.type){const e=a.gradientStops.map(e=>{var t;return{position:MathUtils.round2(e.position),color:ColorConverter.toAllFormats({r:e.color.r,g:e.color.g,b:e.color.b,a:null!==(t=e.color.a)&&void 0!==t?t:1})}}),t=Object.assign(Object.assign({type:a.type,gradientStops:e},a.gradientTransform&&{gradientTransform:a.gradientTransform}),{opacity:MathUtils.round2(null!==(s=a.opacity)&&void 0!==s?s:1)});r.push(t)}else if("IMAGE"===a.type){const t=Object.assign(Object.assign(Object.assign(Object.assign({type:"IMAGE",scaleMode:a.scaleMode},a.imageHash&&{imageHash:a.imageHash}),{opacity:MathUtils.round2(null!==(n=a.opacity)&&void 0!==n?n:1)}),void 0!==a.rotation&&{rotation:a.rotation}),a.filters&&{filters:Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},void 0!==a.filters.exposure&&{exposure:a.filters.exposure}),void 0!==a.filters.contrast&&{contrast:a.filters.contrast}),void 0!==a.filters.saturation&&{saturation:a.filters.saturation}),void 0!==a.filters.temperature&&{temperature:a.filters.temperature}),void 0!==a.filters.tint&&{tint:a.filters.tint}),void 0!==a.filters.highlights&&{highlights:a.filters.highlights}),void 0!==a.filters.shadows&&{shadows:a.filters.shadows})});if(o&&a.imageHash)try{const e=figma.getImageByHash(a.imageHash);if(e){const o=await e.getBytesAsync();if(o){const e=figma.base64Encode(o);t.imageBase64=e}}}catch(t){Logger.log(`⚠️ Could not export image data for style "${e.name}": ${t}`)}r.push(t)}if(0===r.length)return;const g=Object.assign(Object.assign(Object.assign(Object.assign({name:e.name,paints:r},i&&{color:i}),void 0!==l&&{opacity:l}),e.description&&{description:e.description}),c&&Object.keys(c).length>0&&{boundVariables:c});a.push(g)}),a},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalPaintStylesAsync())s.set(e.name,e);return await runSequentialAsync(e,20,async function(e){var n,r,i,l;let c;s.has(e.name)?(c=s.get(e.name),a++):(c=figma.createPaintStyle(),c.name=e.name,o++),e.description&&(c.description=e.description);const g=[];if(e.paints&&e.paints.length>0){for(const o of e.paints)if("SOLID"===o.type){const a=ColorParser.parse(o.color);let s=null!==(n=o.opacity)&&void 0!==n?n:1;a.a<1&&void 0===o.opacity&&(s=MathUtils.round2(a.a));let r={type:"SOLID",color:{r:a.r,g:a.g,b:a.b},opacity:MathUtils.round2(s)};if(e.boundVariables&&0===g.length)for(const[o,a]of Object.entries(e.boundVariables))if(a.name&&a.collection){const e=t.getVariable(`${a.collection}/${a.name}`);if(e)try{r=figma.variables.setBoundVariableForPaint(r,o,e)}catch(e){Logger.log(`⚠️ Could not bind ${o}: ${e}`)}}g.push(r)}else if("GRADIENT_LINEAR"===o.type||"GRADIENT_RADIAL"===o.type||"GRADIENT_ANGULAR"===o.type||"GRADIENT_DIAMOND"===o.type){const e=o.gradientStops.map(e=>{const t=ColorParser.parse(e.color);return{position:e.position,color:{r:t.r,g:t.g,b:t.b,a:t.a}}}),t=o.gradientTransform?[[o.gradientTransform[0][0],o.gradientTransform[0][1],o.gradientTransform[0][2]],[o.gradientTransform[1][0],o.gradientTransform[1][1],o.gradientTransform[1][2]]]:[[1,0,0],[0,1,0]],a={type:o.type,gradientStops:e,gradientTransform:t,opacity:null!==(r=o.opacity)&&void 0!==r?r:1};g.push(a)}else if("IMAGE"===o.type){let t=null;if(o.imageBase64)try{const a=figma.base64Decode(o.imageBase64);t=figma.createImage(a).hash,Logger.log(`✅ Created image from base64 data for style "${e.name}"`)}catch(t){Logger.log(`⚠️ Could not import image from base64 for style "${e.name}": ${t}`)}if(!t&&o.imageHash){figma.getImageByHash(o.imageHash)?(t=o.imageHash,Logger.log(`✅ Found existing image with hash for style "${e.name}"`)):Logger.log(`⚠️ Image hash not found in file for style "${e.name}", skipping image paint (imageHash cannot be null)`)}if(t){const e=Object.assign(Object.assign({type:"IMAGE",scaleMode:o.scaleMode,imageHash:t,opacity:null!==(i=o.opacity)&&void 0!==i?i:1},void 0!==o.rotation&&{rotation:o.rotation}),o.filters&&{filters:o.filters});g.push(e)}}}else if(e.color){const o=ColorParser.parse(e.color);let a=null!==(l=e.opacity)&&void 0!==l?l:1;o.a<1&&void 0===e.opacity&&(a=MathUtils.round2(o.a));let s={type:"SOLID",color:{r:o.r,g:o.g,b:o.b},opacity:MathUtils.round2(a)};if(e.boundVariables)for(const[o,a]of Object.entries(e.boundVariables))if(a.name&&a.collection){const e=t.getVariable(`${a.collection}/${a.name}`);if(e)try{s=figma.variables.setBoundVariableForPaint(s,o,e)}catch(e){Logger.log(`⚠️ Could not bind ${o}: ${e}`)}}g.push(s)}g.length>0&&(c.paints=g)}),{created:o,updated:a}}},TextStyleProcessor={async export(e){const t=[],o=await figma.getLocalTextStylesAsync();return await runSequentialAsync(o,20,async function(e){const o=Object.assign(Object.assign({name:e.name,fontFamily:e.fontName.family,fontStyle:e.fontName.style,fontSize:e.fontSize,lineHeight:e.lineHeight,letterSpacing:e.letterSpacing,textCase:e.textCase,textDecoration:e.textDecoration},e.description&&{description:e.description}),{boundVariables:await extractBindings(e.boundVariables,["fontSize","lineHeight","letterSpacing","paragraphSpacing","paragraphIndent"])});t.push(o)}),t},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalTextStylesAsync())s.set(e.name,e);return await runSequentialAsync(e,20,async function(e){let n;s.has(e.name)?(n=s.get(e.name),a++):(n=figma.createTextStyle(),n.name=e.name,o++),e.description&&(n.description=e.description);try{if(await figma.loadFontAsync({family:e.fontFamily,style:e.fontStyle}),n.fontName={family:e.fontFamily,style:e.fontStyle},n.fontSize=e.fontSize,n.lineHeight=e.lineHeight,n.letterSpacing=e.letterSpacing,e.textCase&&(n.textCase=e.textCase),e.textDecoration&&(n.textDecoration=e.textDecoration),e.boundVariables)for(const[o,a]of Object.entries(e.boundVariables))if(a.name&&a.collection){const e=t.getVariable(`${a.collection}/${a.name}`);if(e)try{n.setBoundVariable(o,e)}catch(e){}}}catch(t){Logger.log(`⚠️ Could not load font for ${e.name}: ${t}`)}}),{created:o,updated:a}}},EffectStyleProcessor={async export(e){const t=[],o=await figma.getLocalEffectStylesAsync();return await runSequentialAsync(o,20,async function(e){const o=[];for(const t of e.effects){const e=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:t.type,visible:t.visible},"radius"in t&&{radius:t.radius}),"spread"in t&&{spread:t.spread}),"offset"in t&&{offset:t.offset}),"color"in t&&{color:ColorConverter.toAllFormats(t.color)}),"blendMode"in t&&{blendMode:t.blendMode}),"showShadowBehindNode"in t&&{showShadowBehindNode:t.showShadowBehindNode}),{boundVariables:await extractBindings(t.boundVariables,["color","radius","spread","offsetX","offsetY"])});o.push(e)}const a=Object.assign(Object.assign({name:e.name},e.description&&{description:e.description}),{effects:o});t.push(a)}),t},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalEffectStylesAsync())s.set(e.name,e);return await runSequentialAsync(e,20,async function(e){let n;s.has(e.name)?(n=s.get(e.name),a++):(n=figma.createEffectStyle(),n.name=e.name,o++),e.description&&(n.description=e.description);const r=e.effects.map(e=>{var t;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:e.type,visible:null===(t=e.visible)||void 0===t||t},void 0!==e.radius&&{radius:e.radius}),void 0!==e.spread&&{spread:e.spread}),void 0!==e.offset&&{offset:e.offset}),void 0!==e.color&&{color:(()=>{const t=ColorParser.parse(e.color);return{r:t.r,g:t.g,b:t.b,a:MathUtils.round2(t.a)}})()}),void 0!==e.blendMode&&{blendMode:e.blendMode}),void 0!==e.showShadowBehindNode&&{showShadowBehindNode:e.showShadowBehindNode})});n.effects=r;for(let o=0;o{var t,o,a,s,n,r,i;const l=e.color?ColorParser.parse(e.color):{r:1,g:0,b:0,a:.1},c={r:l.r,g:l.g,b:l.b,a:MathUtils.round2(l.a)};if("GRID"===e.pattern)return{pattern:"GRID",sectionSize:null!==(t=e.sectionSize)&&void 0!==t?t:10,visible:!1!==e.visible,color:c};const g=null!==(o=e.alignment)&&void 0!==o?o:"STRETCH",d={pattern:e.pattern,gutterSize:null!==(a=e.gutterSize)&&void 0!==a?a:10,count:null!==(s=e.count)&&void 0!==s?s:5,visible:!1!==e.visible,color:c};if("STRETCH"===g)return Object.assign(Object.assign({},d),{alignment:"STRETCH",offset:null!==(n=e.offset)&&void 0!==n?n:0});if("CENTER"===g)return Object.assign(Object.assign({},d),{alignment:"CENTER",sectionSize:null!==(r=e.sectionSize)&&void 0!==r?r:100});{const t=Object.assign(Object.assign({},d),{alignment:g,offset:null!==(i=e.offset)&&void 0!==i?i:0});return void 0!==e.sectionSize&&(t.sectionSize=e.sectionSize),t}});n.layoutGrids=r;for(let o=0;ot.name===e);a?await checkVariablesDiff(i,a.modeId,o,r,"",t):l=!0}t.modifiedVariables.some(e=>e.collection===r)||t.newVariables.some(e=>e.collection===r)?(t.modifiedCollections.push(r),t.summary.collectionsModified++):(t.unchangedCollections.push(r),t.summary.collectionsUnchanged++)}return t}function countVariablesInCollection(e){let t=0;const o=Object.values(e)[0];return o&&(t=countVarsInNestedObj(o)),t}function countVarsInNestedObj(e){let t=0;for(const o of Object.values(e))isExportVariableValue(o)?t++:t+=countVarsInNestedObj(o);return t}async function checkVariablesDiff(e,t,o,a,s,n){for(const[r,i]of Object.entries(o)){const o=s?`${s}/${r}`:r;if(isExportVariableValue(i)){const e=variableCache.getVariable(`${a}/${o}`);if(e){const s=e.valuesByMode[t],r=i.$value;valuesAreDifferent(s,r)?(n.modifiedVariables.push({collection:a,path:o,oldValue:formatValueForDisplay(s),newValue:formatValueForDisplay(r)}),n.summary.variablesModified++):(n.unchangedVariables++,n.summary.variablesUnchanged++)}else n.newVariables.push({collection:a,path:o}),n.summary.variablesNew++}else await checkVariablesDiff(e,t,i,a,o,n)}}function valuesAreDifferent(e,t){if(void 0===e)return!0;if(isVariableAlias(e))return"string"==typeof t&&t.startsWith("{"),!0;if("object"==typeof e&&null!==e&&"r"in e){if("object"==typeof t&&null!==t&&"hex"in t){return ColorConverter.toAllFormats(e).hex.toLowerCase()!==t.hex.toLowerCase()}return!0}return e!==t}function formatValueForDisplay(e){if(void 0===e)return"undefined";if("object"==typeof e&&null!==e){if("hex"in e)return e.hex;if("r"in e)return ColorConverter.toAllFormats(e).hex;if("id"in e)return"{alias}"}return String(e)}async function computeStylesDiff(e,t){if(e.colorStyles){const o=await figma.getLocalPaintStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.colorStyles)a.has(o.name)?(t.modifiedStyles.push({type:"color",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"color",name:o.name}),t.summary.stylesNew++)}if(e.textStyles){const o=await figma.getLocalTextStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.textStyles)a.has(o.name)?(t.modifiedStyles.push({type:"text",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"text",name:o.name}),t.summary.stylesNew++)}if(e.effectStyles){const o=await figma.getLocalEffectStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.effectStyles)a.has(o.name)?(t.modifiedStyles.push({type:"effect",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"effect",name:o.name}),t.summary.stylesNew++)}if(e.gridStyles){const o=await figma.getLocalGridStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.gridStyles)a.has(o.name)?(t.modifiedStyles.push({type:"grid",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"grid",name:o.name}),t.summary.stylesNew++)}}function filterStylesByGroup(e,t){return t?e.filter(function(e){return-1!==t.indexOf(getGroupKey(e.name))}):e}async function exportVariables(e,t,o,a,s="original",n="figma",r,i=!1,l,c){var g,d,f,u,p,m,y,h;Logger.log("📤 Starting export..."),Logger.log(` preserveLibraryRefs: ${o}`),Logger.log(` includeImages: ${a}`),Logger.log(` namingConvention: ${s}`),Logger.log(` exportFormat: ${n}`),Logger.log(` resolveAliases: ${i}`),r&&Logger.log(` selectedModes: ${JSON.stringify(r)}`);try{let o=await figma.variables.getLocalVariableCollectionsAsync();(null==e?void 0:e.length)&&(o=o.filter(t=>e.includes(t.name)),Logger.log(`Filtering to ${o.length} selected collections`));const b=[],v={};let S=0;const C=createProgress("export");let A=0;for(let e=0;eo.includes(e.name)),Logger.log(` Filtering to ${t.length} modes: ${t.map(e=>e.name).join(", ")}`)}const o=NamingConverter.convertCollectionName(e.name,s),a={[o]:Object.assign({modes:{}},o!==e.name&&{$originalName:e.name})};for(const e of t){const t=NamingConverter.convertModeName(e.name,s);a[o].modes[t]={}}await runSequentialAsync(e.variableIds,BATCH.SEQ_EXPORT,async function(n){var r,c;L++;const g=await figma.variables.getVariableByIdAsync(n);if(!g)return;if(l&&l[e.name]&&-1===l[e.name].indexOf(getGroupKey(g.name)))return;S++;const d=g.name.split("/").map(e=>NamingConverter.convert(e,s));for(const e of t){const t=NamingConverter.convertModeName(e.name,s),n=a[o].modes[t],l=g.valuesByMode[e.modeId];let f=n;for(let e=0;eNamingConverter.convert(e,s)).join(".")}}`,p=v,b){const e=t.valuesByMode[Object.keys(t.valuesByMode)[0]];"object"==typeof e&&null!==e&&"r"in e?m=ColorConverter.toAllFormats(e):isVariableAlias(e)||(m=e)}}}else p=""}else p="object"==typeof l&&null!==l&&"r"in l?ColorConverter.toAllFormats(l):l;const S=Object.assign(Object.assign(Object.assign({$scopes:TypeMapper.scopesToArray(g.scopes),$type:TypeMapper.toExportType(g.resolvedType),$value:p},g.description&&{$description:g.description}),y&&h&&{$collectionName:h}),y&&b&&Object.assign({$libraryRef:v},void 0!==m&&{$localValue:m}));f[u]=S}},function(){C.report("export_variables","Exporting variables",L,A)}),b.push(a),"w3c"===n&&(v[o]=W3CConverter.collectionToW3C(o,a[o].modes,s,a[o].$originalName))}let $=null;if(t){if($={},t.colorStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.color:void 0;$.colorStyles=filterStylesByGroup(await ColorStyleProcessor.export({includeImages:a}),e),e&&0===$.colorStyles.length&&delete $.colorStyles}if(t.textStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.text:void 0;$.textStyles=filterStylesByGroup(await TextStyleProcessor.export(),e),e&&0===$.textStyles.length&&delete $.textStyles}if(t.effectStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.effect:void 0;$.effectStyles=filterStylesByGroup(await EffectStyleProcessor.export(),e),e&&0===$.effectStyles.length&&delete $.effectStyles}if(t.gridStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.grid:void 0;$.gridStyles=filterStylesByGroup(await GridStyleProcessor.export(),e),e&&0===$.gridStyles.length&&delete $.gridStyles}Object.keys($).length>0?b.push({_styles:$}):$=null}const w={collections:o.length,variables:S,styles:$?{color:null!==(d=null===(g=$.colorStyles)||void 0===g?void 0:g.length)&&void 0!==d?d:0,text:null!==(u=null===(f=$.textStyles)||void 0===f?void 0:f.length)&&void 0!==u?u:0,effect:null!==(m=null===(p=$.effectStyles)||void 0===p?void 0:p.length)&&void 0!==m?m:0,grid:null!==(h=null===(y=$.gridStyles)||void 0===y?void 0:y.length)&&void 0!==h?h:0}:null};let O;"w3c"===n?($&&Object.keys($).length>0&&(v.$extensions={"com.figma":{styles:$}}),O=JSON.stringify(v,null,2),Logger.log(`✅ Export complete (W3C format): ${w.collections} collections, ${w.variables} variables`)):"tokens-studio"===n?(O=JSON.stringify(convertToTokensStudio(b),null,2),Logger.log(`✅ Export complete (Tokens Studio format): ${w.collections} collections, ${w.variables} variables`)):(O=JSON.stringify(b,null,2),Logger.log(`✅ Export complete: ${w.collections} collections, ${w.variables} variables`)),await sendExportInChunks(O,w,n)}catch(e){if(isCancelError(e))return Logger.log("🛑 Export cancelled"),void figma.ui.postMessage({type:"operation_cancelled",operation:"export",phase:"export",rolledBack:!1,message:"Export cancelled. Nothing was changed."});Logger.log(`❌ Export error: ${e}`),Logger.send("error",{message:`Export failed: ${e}`})}}async function sendExportInChunks(e,t,o){const a=e.length,s=BATCH.EXPORT_CHUNK_BYTES,n=Math.max(1,Math.ceil(a/s));let r=0,i=0;for(;i=55296&&o<=56319&&(t+=1)}const o=e.slice(i,t);figma.ui.postMessage({type:"export_chunk",seq:r,total:n,data:o}),r++,i=t,r%BATCH.EXPORT_YIELD_EVERY===0&&i0?a.modes[s[0]]:{},"");b.push({collectionObj:t,flatPaths:n}),v+=n.length;for(let e=0;e0&&Logger.log(` ✅ Aliases: ${L} resolved, ${$} used fallback values`),y&&t.importStyles){if(Logger.log("📦 Importing styles..."),n.report("import_styles","Importing styles",0,0,!0),y.colorStyles){const e=await ColorStyleProcessor.importStyles(y.colorStyles,variableCache);p+=e.created,m+=e.updated}if(y.textStyles){const e=await TextStyleProcessor.importStyles(y.textStyles,variableCache);p+=e.created,m+=e.updated}if(y.effectStyles){const e=await EffectStyleProcessor.importStyles(y.effectStyles,variableCache);p+=e.created,m+=e.updated}if(y.gridStyles){const e=await GridStyleProcessor.importStyles(y.gridStyles,variableCache);p+=e.created,m+=e.updated}}const w={collectionsCreated:g,variablesCreated:d,variablesUpdated:f,variablesSkipped:u,stylesCreated:p,stylesUpdated:m};figma.commitUndo(),Logger.log("✅ Import complete!"),Logger.send("import_complete",{stats:w,snapshot:s})}catch(e){const t=isCancelError(e);if(t&&!r)return Logger.log("🛑 Import cancelled before any changes were made"),void figma.ui.postMessage({type:"operation_cancelled",operation:"import",phase:"snapshot",rolledBack:!1,message:"Import cancelled. No changes were made."});const o=e instanceof Error?e.message:String(e);if(t?Logger.log("🛑 Import cancelled after mutation started — rolling back..."):Logger.log(`❌ Import error: ${o}`),s){Logger.log("🔄 Attempting automatic rollback to pre-import state..."),Logger.send("import_rolling_back",{error:o});try{await restoreFromSnapshot(s),Logger.log("✅ Automatic rollback successful - file restored to pre-import state"),t?figma.ui.postMessage({type:"operation_cancelled",operation:"import",phase:"rollback",rolledBack:!0,message:"Import cancelled — your file was restored to its previous state."}):Logger.send("import_rollback_complete",{error:o,message:"Import failed but your file has been automatically restored to its previous state."})}catch(e){const t=e instanceof Error?e.message:String(e);Logger.log(`❌ Rollback failed: ${t}`),Logger.send("import_rollback_failed",{error:o,rollbackError:t,message:"Import failed and automatic rollback also failed. Please use Ctrl+Z (Cmd+Z) to undo manually."})}}else Logger.send("error",{message:`Import failed: ${o}. Use Ctrl+Z (Cmd+Z) to undo changes.`})}}function setRawValue(e,t,o){try{if("color"===o.$type){const a=ColorParser.parse(o.$value),s=a.a<1?Object.assign(Object.assign({},a),{a:MathUtils.round2(a.a)}):a;e.setValueForMode(t,s)}else e.setValueForMode(t,o.$value)}catch(e){console.error(`Could not set value: ${e}`)}}function getGroupKey(e){const t=e.indexOf("/");return-1===t?"":e.substring(0,t)}function summarizeGroups(e){const t=Object.create(null),o=[];for(let a=0;a{Logger.log(` ${t+1}. "${e.name}" (id: ${e.id})`)});const t=new Set;let o=0,a=0,s=0;const n=new Map,r=[];for(let i=0;ie.name),variableCount:l.variableIds.length,types:c,groups:summarizeGroups(g)})}r.sort((e,t)=>e.name.localeCompare(t.name));const i=await figma.getLocalPaintStylesAsync(),l=await figma.getLocalTextStylesAsync(),c=await figma.getLocalEffectStylesAsync(),g=await figma.getLocalGridStylesAsync();let d=0;const f=[];for(let e=0;e({family:e,styles:Array.from(t)}));let S=0;for(const e of i)e.boundVariables&&Object.keys(e.boundVariables).length>0&&S++;Logger.send("collections",{collections:r,styles:u,styleGroups:h,libraryDependencies:Array.from(t),fontsUsed:v,stats:{totalVariables:r.reduce((e,t)=>e+t.variableCount,0),totalAliases:o,localAliases:a,libraryAliases:s,styleBindings:S}})}async function clearVariables(e=!1){Logger.log("🗑️ Clearing all variables...");const t=e?null:createProgress("clear");let o=0,a=0,s=!1;try{const n=await figma.variables.getLocalVariableCollectionsAsync(),r=[];let i=0;for(let e=0;e0&&(figma.commitUndo(),s=!0);let l=0;for(let e=0;e0&&(figma.commitUndo(),a=!0);let c=0;const removeStyle=function(e){e.remove(),o++},onBatch=function(e){t&&t.report("clear","Deleting styles",c+e,l)};await runBatched(s,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=s.length,await runBatched(n,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=n.length,await runBatched(r,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=r.length,await runBatched(i,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=i.length,!e&&a&&figma.commitUndo(),Logger.log(`✅ Cleared ${o} styles`),e||Logger.send("clear_complete",{message:`${o} styles`})}catch(t){if(isCancelError(t)){if(e)throw t;return Logger.log(`🛑 Clear styles cancelled after ${o} styles`),void figma.ui.postMessage({type:"operation_cancelled",operation:"clear",phase:"clear",rolledBack:!1,partial:{collectionsDeleted:0,variablesDeleted:o},message:`Clear cancelled — ${o} styles were already deleted. Remaining items were not touched. Use Cmd+Z to restore deleted items.`})}if(Logger.log(`❌ Clear styles error: ${t}`),e)throw t;Logger.send("error",{message:`Failed to clear styles: ${t}`})}}async function clearAll(e=!1){if(Logger.log("🗑️ Clearing everything..."),e)return await clearVariables(!0),void await clearStyles(!0);let t=!1;try{figma.commitUndo(),t=!0,await clearVariables(!0),await clearStyles(!0),figma.commitUndo(),Logger.send("clear_complete",{message:"all variables and styles"})}catch(e){if(t&&figma.commitUndo(),isCancelError(e))return Logger.log("🛑 Clear all cancelled"),void figma.ui.postMessage({type:"operation_cancelled",operation:"clear",phase:"clear",rolledBack:!1,partial:{collectionsDeleted:0,variablesDeleted:0},message:"Clear cancelled — some items may already have been deleted. Use Cmd+Z to restore deleted items."});Logger.log(`❌ Clear all error: ${e}`),Logger.send("error",{message:`Failed to clear: ${e}`})}}async function createUndoSnapshot(e){var t;Logger.log("📸 Creating snapshot of current file state...");const o=await figma.variables.getLocalVariableCollectionsAsync(),a=[],s=new Map;let n=0;for(let e=0;e({id:e.modeId,name:e.name})),variables:[]},i=await runBatchedAsync(t.variableIds,BATCH.ASYNC_LOOKUP,function(e){return figma.variables.getVariableByIdAsync(e)},function(t){e&&e.report("snapshot","Preparing snapshot (undo safety)",r+t,n)});r+=t.variableIds.length;for(let e=0;e0){t.renameMode(t.modes[0].modeId,e.modes[0].name);for(let o=1;o0&&(s.scopes=a.scopes);for(const t of e.modes){const r=o[t.name],i=a.values[t.name];if(i)if(i.isAlias&&i.aliasName)n.push({variable:s,modeId:r,aliasPath:i.aliasName,aliasCollection:i.aliasCollection||e.name});else if(void 0!==i.value){let e;e="COLOR"===a.type&&"string"==typeof i.value?ColorParser.parse(i.value):i.value,s.setValueForMode(r,e)}}}},function(e,o){t.report("undo_restore","Restoring variables",e,o)}),Logger.log(` Resolving ${n.length} aliases...`),await runBatched(n,BATCH.SYNC_LIGHT,function(e){const t=`${e.aliasCollection}/${e.aliasPath}`,o=variableCache.getVariable(t);o&&e.variable.setValueForMode(e.modeId,{type:"VARIABLE_ALIAS",id:o.id})},function(e,o){t.report("undo_aliases","Restoring aliases",e,o)}),Logger.log(" Restoring styles..."),t.report("undo_styles","Restoring styles",0,0,!0),a.colorStyles&&a.colorStyles.length>0&&await ColorStyleProcessor.importStyles(a.colorStyles,variableCache),a.textStyles&&a.textStyles.length>0&&await TextStyleProcessor.importStyles(a.textStyles,variableCache),a.effectStyles&&a.effectStyles.length>0&&await EffectStyleProcessor.importStyles(a.effectStyles,variableCache),a.gridStyles&&a.gridStyles.length>0&&await GridStyleProcessor.importStyles(a.gridStyles,variableCache),Logger.log("✅ File restored from snapshot")}figma.ui.onmessage=async e=>{switch(e.type){case"cancel_operation":null!==currentOperation.type&&(currentOperation.cancellable?(currentOperation.cancelRequested=!0,Logger.log("🛑 Cancellation requested — finishing current batch…")):Logger.log("⚠️ Rollback in progress — cannot cancel"));break;case"export":await withOperation("export",function(){return exportVariables(e.collections,e.styleOptions,e.preserveLibraryRefs,e.includeImages,e.namingConvention||"original",e.exportFormat||"figma",e.selectedModes,e.resolveAliases||!1,e.selectedGroups,e.selectedStyleGroups)});break;case"import":await withOperation("import",function(){return importVariables(e.data,e.options)});break;case"validate_import":try{const t=JSON.parse(e.data),o=e.plan,a=await validateImportAgainstPlan(t,o);Logger.send("validation_result",a)}catch(e){Logger.send("validation_result",{errors:[`Invalid JSON: ${e instanceof Error?e.message:"Parse error"}`],canImport:!1})}break;case"compute_import_diff":try{const t=JSON.parse(e.data),o=await computeImportDiff(t);Logger.send("import_diff_result",o)}catch(e){Logger.send("import_diff_result",{error:`Failed to compute diff: ${e instanceof Error?e.message:"Unknown error"}`})}break;case"detect_plan":const t=await detectCurrentPlan();Logger.send("plan_detected",t);break;case"clear_variables":await withOperation("clear",function(){return clearVariables(!1)});break;case"clear_styles":await withOperation("clear",function(){return clearStyles(!1)});break;case"clear_all":await withOperation("clear",function(){return clearAll(!1)});break;case"get_collections":await withOperation("scan",getCollections);break;case"check_libraries":try{const t=e.collections;await variableCache.rebuild();const o=[],a=[];for(const e of t)variableCache.isCollectionAvailable(e)?o.push(e):a.push(e);Logger.log(`📚 Library check: ${o.length} available, ${a.length} missing`),o.length>0&&Logger.log(` ✅ Available: ${o.join(", ")}`),a.length>0&&Logger.log(` ❌ Missing: ${a.join(", ")}`),Logger.send("library_check_result",{allAvailable:0===a.length,availableCollections:o,missingCollections:a,requiredCollections:t})}catch(t){Logger.send("library_check_result",{allAvailable:!1,availableCollections:[],missingCollections:e.collections||[],requiredCollections:e.collections||[],error:t instanceof Error?t.message:"Library check failed"})}break;case"check_fonts":try{const t=e.fonts,o=[],a=[],s=await runBatchedAsync(t,BATCH.ASYNC_FONT,function(e){return figma.loadFontAsync({family:e.family,style:e.style}).then(function(){return{font:e,available:!0}}).catch(function(){return{font:e,available:!1}})});for(let e=0;e/", DTCG keys only ($type/$value/ +// $description), "$themes" + "$metadata.tokenSetOrder" always present, plus +// dedicated "styles/color" / "styles/typography" / "styles/effects" sets. +// Pure transform over the already-built export data — no Figma API calls. +// QuickJS-safe: no spread, no optional chaining, no nullish coalescing. + +// FLOAT scope -> Tokens Studio type refinement (applied only when the variable +// carries exactly one relevant scope and no ALL_SCOPES). +const TS_FLOAT_SCOPE_MAP: Record = { + 'CORNER_RADIUS': 'borderRadius', + 'STROKE_FLOAT': 'borderWidth', + 'GAP': 'spacing', + 'WIDTH_HEIGHT': 'sizing', + 'OPACITY': 'opacity', + 'FONT_SIZE': 'fontSizes', + 'LINE_HEIGHT': 'lineHeights', + 'LETTER_SPACING': 'letterSpacing', + 'PARAGRAPH_SPACING': 'paragraphSpacing' +}; + +// STRING scope -> Tokens Studio type refinement +const TS_STRING_SCOPE_MAP: Record = { + 'FONT_FAMILY': 'fontFamilies', + 'FONT_STYLE': 'fontWeights' +}; + +// Figma textCase -> Tokens Studio typography textCase (SMALL_CAPS* omitted — +// no Tokens Studio representation) +const TS_TEXT_CASE_MAP: Record = { + 'ORIGINAL': 'none', + 'UPPER': 'uppercase', + 'LOWER': 'lowercase', + 'TITLE': 'capitalize' +}; + +// Figma textDecoration -> Tokens Studio typography textDecoration +const TS_TEXT_DECORATION_MAP: Record = { + 'NONE': 'none', + 'UNDERLINE': 'underline', + 'STRIKETHROUGH': 'line-through' +}; + +type TokensStudioCompositeValue = Record; + +interface TokensStudioToken { + $type: string; + $value: string | number | TokensStudioCompositeValue | TokensStudioCompositeValue[]; + $description?: string; +} + +interface TokensStudioGroup { + [key: string]: TokensStudioToken | TokensStudioGroup; +} + +interface TokensStudioTheme { + id: string; + name: string; + group: string; + selectedTokenSets: Record; +} + +// Aggregated skip counters so each category logs ONE note, not one per item. +interface TokensStudioSkipState { + libraryRefsSkipped: number; + imagePaintsSkipped: number; + blurEffectsSkipped: number; +} + +const TokensStudioConverter = { + // Token path segments must not contain { } $ — and never resolve to + // prototype-polluting keys when used as dynamic object properties. + sanitizeSegment(segment: string): string { + let cleaned = segment.replace(/[{}$]/g, ''); + if (cleaned.length === 0) { + cleaned = '_'; + } + if (cleaned === '__proto__' || cleaned === 'constructor' || cleaned === 'prototype') { + cleaned = '_' + cleaned; + } + return cleaned; + }, + + // "{path.to.token}" -> same ref with every dot-segment passed through + // sanitizeSegment, so refs always match sanitized token paths (incl. the + // empty-after-stripping and __proto__/constructor/prototype renames). + // exportData alias refs are already collection-prefix-free name paths + // (built from the target variable's name with "/" -> "."), which is exactly + // the Tokens Studio reference shape; normal refs pass through unchanged. + sanitizeAliasRef(ref: string): string { + const parts = ref.substring(1, ref.length - 1).split('.'); + const cleaned: string[] = []; + for (let i = 0; i < parts.length; i++) { + cleaned.push(this.sanitizeSegment(parts[i])); + } + return '{' + cleaned.join('.') + '}'; + }, + + isAliasRef(value: unknown): value is string { + return typeof value === 'string' && value.charAt(0) === '{' && + value.charAt(value.length - 1) === '}'; + }, + + // Plain JSON numbers, <= 3 decimals + roundNumber(n: number): number { + return Math.round(n * 1000) / 1000; + }, + + // "#rrggbb" for opaque colors; CSS "rgba(r, g, b, a)" when alpha < 1 + // (ColorConverter only sets rgb.a / emits rgba css when alpha < 1). + colorToTS(color: ExportColorValue): string { + const alpha = color.rgb !== undefined ? color.rgb.a : undefined; + if (alpha !== undefined && alpha < 1) { + return color.css; + } + // Near-opaque rounding edge: raw alpha in (0.995, 1) rounds to rgb.a = 1 + // here, but ColorConverter.toHex (which checks the RAW alpha) already + // appended an alpha byte. Strip it so opaque emission is always #rrggbb. + if (color.rgb !== undefined && color.hex.length > 7) { + return color.hex.substring(0, 7); + } + return color.hex; + }, + + floatTypeFromScopes(scopes: readonly string[] | undefined): string { + if (!scopes) { + return 'number'; + } + let mapped = ''; + let relevant = 0; + for (let i = 0; i < scopes.length; i++) { + if (scopes[i] === 'ALL_SCOPES') { + return 'number'; + } + const candidate = TS_FLOAT_SCOPE_MAP[scopes[i]]; + if (candidate !== undefined) { + relevant++; + mapped = candidate; + } + } + return relevant === 1 ? mapped : 'number'; + }, + + stringTypeFromScopes(scopes: readonly string[] | undefined): string { + if (!scopes) { + return 'text'; + } + let mapped = ''; + let relevant = 0; + for (let i = 0; i < scopes.length; i++) { + if (scopes[i] === 'ALL_SCOPES') { + return 'text'; + } + const candidate = TS_STRING_SCOPE_MAP[scopes[i]]; + if (candidate !== undefined) { + relevant++; + mapped = candidate; + } + } + return relevant === 1 ? mapped : 'text'; + }, + + // Convert one export leaf to a Tokens Studio token; null = skip (library + // alias with no resolvable local value — its "{ref}" target is not in the + // export, so the reference would dangle). + convertVariableLeaf(leaf: ExportVariableValue, state: TokensStudioSkipState): TokensStudioToken | null { + let rawValue: string | number | boolean | ExportColorValue = leaf.$value; + if (leaf.$libraryRef !== undefined) { + if (leaf.$localValue !== undefined) { + rawValue = leaf.$localValue; + } else { + state.libraryRefsSkipped++; + return null; + } + } + + let tsType: string; + let tsValue: string | number; + const aliasRef = this.isAliasRef(rawValue); + + if (leaf.$type === 'color') { + tsType = 'color'; + if (aliasRef) { + tsValue = this.sanitizeAliasRef(rawValue as string); + } else if (typeof rawValue === 'object' && rawValue !== null) { + tsValue = this.colorToTS(rawValue as ExportColorValue); + } else { + tsValue = String(rawValue); + } + } else if (leaf.$type === 'float') { + tsType = this.floatTypeFromScopes(leaf.$scopes); + if (aliasRef) { + tsValue = this.sanitizeAliasRef(rawValue as string); + } else if (typeof rawValue === 'number') { + tsValue = this.roundNumber(rawValue); + } else { + tsValue = String(rawValue); + } + } else if (leaf.$type === 'boolean') { + // Tokens Studio boolean tokens carry string values "true" / "false" + tsType = 'boolean'; + tsValue = aliasRef ? this.sanitizeAliasRef(rawValue as string) : String(rawValue); + } else { + tsType = this.stringTypeFromScopes(leaf.$scopes); + tsValue = aliasRef ? this.sanitizeAliasRef(rawValue as string) : String(rawValue); + } + + // Key order is deliberate: $type before $value (matches the plugin output) + const token: TokensStudioToken = { $type: tsType, $value: tsValue }; + if (leaf.$description) { + token.$description = leaf.$description; + } + return token; + }, + + // Insert a token at a "/"-separated path, creating nested groups as needed. + insertTokenAtPath(root: TokensStudioGroup, path: string, token: TokensStudioToken): void { + const parts = path.split('/'); + let current: TokensStudioGroup = root; + for (let i = 0; i < parts.length - 1; i++) { + const seg = this.sanitizeSegment(parts[i]); + const existing = current[seg]; + if (existing !== undefined && typeof existing === 'object' && !('$value' in existing)) { + current = existing as TokensStudioGroup; + } else { + const created: TokensStudioGroup = {}; + current[seg] = created; + current = created; + } + } + current[this.sanitizeSegment(parts[parts.length - 1])] = token; + }, + + // Best-effort CSS gradient string: angle derived from the transform's + // x-axis (fallback 180deg); stop order and positions preserved. + gradientToCss(paint: ExportGradientPaint): string { + let angle = 180; + const t = paint.gradientTransform; + if (t !== undefined) { + let deg = Math.atan2(t[0][1], t[0][0]) * 180 / Math.PI + 90; + deg = ((deg % 360) + 360) % 360; + angle = Math.round(deg * 100) / 100; + } + const stops: string[] = []; + for (let i = 0; i < paint.gradientStops.length; i++) { + const stop = paint.gradientStops[i]; + const pos = Math.round(stop.position * 10000) / 100; + stops.push(this.colorToTS(stop.color) + ' ' + pos + '%'); + } + return 'linear-gradient(' + angle + 'deg, ' + stops.join(', ') + ')'; + }, + + // First exportable paint wins: SOLID -> hex/rgba, gradients -> CSS string; + // IMAGE paints are counted and skipped. + colorStyleToTSValue(style: ExportColorStyle, state: TokensStudioSkipState): string | null { + const paints: readonly ExportPaintData[] = style.paints !== undefined ? style.paints : []; + for (let i = 0; i < paints.length; i++) { + const paint = paints[i]; + if (paint.type === 'SOLID') { + return this.colorToTS(paint.color); + } + if (paint.type === 'GRADIENT_LINEAR' || paint.type === 'GRADIENT_RADIAL' || + paint.type === 'GRADIENT_ANGULAR' || paint.type === 'GRADIENT_DIAMOND') { + return this.gradientToCss(paint); + } + if (paint.type === 'IMAGE') { + state.imagePaintsSkipped++; + } + } + // Legacy single-color field fallback + if (style.color !== undefined) { + return this.colorToTS(style.color); + } + return null; + }, + + // Typography composite: singular sub-keys; sub-keys we cannot derive are + // omitted (ExportTextStyle does not capture paragraphSpacing). + textStyleToTSValue(style: ExportTextStyle): TokensStudioCompositeValue { + const value: TokensStudioCompositeValue = {}; + value.fontFamily = style.fontFamily; + value.fontWeight = style.fontStyle; + if (typeof style.fontSize === 'number') { + value.fontSize = this.roundNumber(style.fontSize); + } + const lineHeight = style.lineHeight as { unit?: string; value?: number }; + if (lineHeight !== undefined && lineHeight !== null) { + if (lineHeight.unit === 'AUTO') { + value.lineHeight = 'AUTO'; + } else if (lineHeight.unit === 'PERCENT' && typeof lineHeight.value === 'number') { + value.lineHeight = this.roundNumber(lineHeight.value) + '%'; + } else if (lineHeight.unit === 'PIXELS' && typeof lineHeight.value === 'number') { + value.lineHeight = this.roundNumber(lineHeight.value); + } + } + const letterSpacing = style.letterSpacing as { unit?: string; value?: number }; + if (letterSpacing !== undefined && letterSpacing !== null) { + if (letterSpacing.unit === 'PERCENT' && typeof letterSpacing.value === 'number') { + value.letterSpacing = this.roundNumber(letterSpacing.value) + '%'; + } else if (letterSpacing.unit === 'PIXELS' && typeof letterSpacing.value === 'number') { + value.letterSpacing = this.roundNumber(letterSpacing.value); + } + } + if (style.textCase !== undefined) { + const textCase = TS_TEXT_CASE_MAP[style.textCase]; + if (textCase !== undefined) { + value.textCase = textCase; + } + } + if (style.textDecoration !== undefined) { + const textDecoration = TS_TEXT_DECORATION_MAP[style.textDecoration]; + if (textDecoration !== undefined) { + value.textDecoration = textDecoration; + } + } + return value; + }, + + // Shadow layers only; blur effects are counted and skipped. Single layer -> + // object, multiple layers -> array (layer order preserved). + effectStyleToTSValue( + style: ExportEffectStyle, + state: TokensStudioSkipState + ): TokensStudioCompositeValue | TokensStudioCompositeValue[] | null { + const layers: TokensStudioCompositeValue[] = []; + const effects = style.effects !== undefined ? style.effects : []; + for (let i = 0; i < effects.length; i++) { + const effect = effects[i]; + if (effect.type === 'DROP_SHADOW' || effect.type === 'INNER_SHADOW') { + const layer: TokensStudioCompositeValue = { + color: effect.color !== undefined ? this.colorToTS(effect.color) : '#000000', + type: effect.type === 'DROP_SHADOW' ? 'dropShadow' : 'innerShadow', + x: effect.offset !== undefined ? this.roundNumber(effect.offset.x) : 0, + y: effect.offset !== undefined ? this.roundNumber(effect.offset.y) : 0, + blur: typeof effect.radius === 'number' ? this.roundNumber(effect.radius) : 0, + spread: typeof effect.spread === 'number' ? this.roundNumber(effect.spread) : 0 + }; + layers.push(layer); + } else { + state.blurEffectsSkipped++; + } + } + if (layers.length === 0) { + return null; + } + return layers.length === 1 ? layers[0] : layers; + }, + + // Theme ids: lowercase, spaces -> "-" + themeIdSegment(name: string): string { + return name.toLowerCase().replace(/\s+/g, '-'); + } +} as const; + +// Transform the already-built (post-group-filter) export data into the Tokens +// Studio single-file container ("shape A"). +function convertToTokensStudio(exportData: ExportFormat): Record { + const conv = TokensStudioConverter; + const result: Record = {}; + const tokenSetOrder: string[] = []; + const state: TokensStudioSkipState = { + libraryRefsSkipped: 0, + imagePaintsSkipped: 0, + blurEffectsSkipped: 0 + }; + + // Split collection wrappers from the trailing _styles wrapper + interface TSCollectionInfo { + name: string; + modeNames: string[]; + modes: ModeVariables; + } + const collections: TSCollectionInfo[] = []; + let stylesData: StylesExport | null = null; + + for (let i = 0; i < exportData.length; i++) { + const item = exportData[i]; + const maybeStyles = (item as { _styles?: StylesExport })._styles; + if (maybeStyles !== undefined) { + stylesData = maybeStyles; + continue; + } + const wrapper = item as CollectionExport; + const wrapperKeys = Object.keys(wrapper); + for (let k = 0; k < wrapperKeys.length; k++) { + const rawName = wrapperKeys[k]; + const entry = wrapper[rawName]; + if (!entry || typeof entry !== 'object' || entry.modes === undefined) { + continue; + } + // Reserved container keys: a collection literally named "$themes" or + // "$metadata" gets a "-set" suffix as its set-name base. + const setBaseName = (rawName === '$themes' || rawName === '$metadata') ? rawName + '-set' : rawName; + collections.push({ + name: setBaseName, + modeNames: Object.keys(entry.modes), + modes: entry.modes + }); + } + } + + // Variable token sets: one per Collection/Mode (even single-mode + // collections), collections in order, modes in collection order. + for (let c = 0; c < collections.length; c++) { + const col = collections[c]; + for (let m = 0; m < col.modeNames.length; m++) { + const modeName = col.modeNames[m]; + const setName = col.name + '/' + modeName; + const group: TokensStudioGroup = {}; + const flat = flattenVariables(col.modes[modeName], ''); + for (let f = 0; f < flat.length; f++) { + const token = conv.convertVariableLeaf(flat[f].value, state); + if (token !== null) { + conv.insertTokenAtPath(group, flat[f].path, token); + } + } + result[setName] = group; + tokenSetOrder.push(setName); + } + } + + // Style sets (only emitted when they have content), appended after the + // variable sets in tokenSetOrder. + const styleSetNames: string[] = []; + // A collection literally named "styles" with a mode named color/typography/ + // effects would produce a variable set with the same key as a style set; + // suffix the style set instead of silently overwriting the variable set. + function styleSetKey(base: string): string { + return result[base] !== undefined ? base + '-set' : base; + } + if (stylesData !== null) { + if (stylesData.colorStyles !== undefined && stylesData.colorStyles.length > 0) { + const group: TokensStudioGroup = {}; + for (let i = 0; i < stylesData.colorStyles.length; i++) { + const style = stylesData.colorStyles[i]; + const tsValue = conv.colorStyleToTSValue(style, state); + if (tsValue === null) { + continue; + } + const token: TokensStudioToken = { $type: 'color', $value: tsValue }; + if (style.description) { + token.$description = style.description; + } + conv.insertTokenAtPath(group, style.name, token); + } + if (Object.keys(group).length > 0) { + const colorSetKey = styleSetKey('styles/color'); + result[colorSetKey] = group; + tokenSetOrder.push(colorSetKey); + styleSetNames.push(colorSetKey); + } + } + if (stylesData.textStyles !== undefined && stylesData.textStyles.length > 0) { + const group: TokensStudioGroup = {}; + for (let i = 0; i < stylesData.textStyles.length; i++) { + const style = stylesData.textStyles[i]; + const token: TokensStudioToken = { $type: 'typography', $value: conv.textStyleToTSValue(style) }; + if (style.description) { + token.$description = style.description; + } + conv.insertTokenAtPath(group, style.name, token); + } + if (Object.keys(group).length > 0) { + const typographySetKey = styleSetKey('styles/typography'); + result[typographySetKey] = group; + tokenSetOrder.push(typographySetKey); + styleSetNames.push(typographySetKey); + } + } + if (stylesData.effectStyles !== undefined && stylesData.effectStyles.length > 0) { + const group: TokensStudioGroup = {}; + for (let i = 0; i < stylesData.effectStyles.length; i++) { + const style = stylesData.effectStyles[i]; + const tsValue = conv.effectStyleToTSValue(style, state); + if (tsValue === null) { + continue; + } + const token: TokensStudioToken = { $type: 'boxShadow', $value: tsValue }; + if (style.description) { + token.$description = style.description; + } + conv.insertTokenAtPath(group, style.name, token); + } + if (Object.keys(group).length > 0) { + const effectsSetKey = styleSetKey('styles/effects'); + result[effectsSetKey] = group; + tokenSetOrder.push(effectsSetKey); + styleSetNames.push(effectsSetKey); + } + } + if (stylesData.gridStyles !== undefined && stylesData.gridStyles.length > 0) { + Logger.log('Tokens Studio export: grid styles skipped (no Tokens Studio representation)'); + } + } + + // One aggregated note per skipped category (JSF: no per-item log spam) + if (state.libraryRefsSkipped > 0) { + Logger.log('Tokens Studio export: skipped ' + state.libraryRefsSkipped + ' library-alias token(s) with no resolvable local value'); + } + if (state.imagePaintsSkipped > 0) { + Logger.log('Tokens Studio export: skipped ' + state.imagePaintsSkipped + ' image paint(s) in color styles (no Tokens Studio representation)'); + } + if (state.blurEffectsSkipped > 0) { + Logger.log('Tokens Studio export: skipped ' + state.blurEffectsSkipped + ' blur effect(s) in effect styles (boxShadow tokens carry shadows only)'); + } + + // $themes: one entry per collection x mode. Own set "enabled"; every other + // collection contributes its same-named mode set when it exists (else its + // first mode set) as "source"; style sets are "source" in every theme. + const themes: TokensStudioTheme[] = []; + for (let c = 0; c < collections.length; c++) { + const col = collections[c]; + for (let m = 0; m < col.modeNames.length; m++) { + const modeName = col.modeNames[m]; + const selected: Record = {}; + selected[col.name + '/' + modeName] = 'enabled'; + for (let o = 0; o < collections.length; o++) { + if (o === c) { + continue; + } + const other = collections[o]; + if (other.modeNames.length === 0) { + continue; + } + let pick = other.modeNames[0]; + for (let mm = 0; mm < other.modeNames.length; mm++) { + if (other.modeNames[mm] === modeName) { + pick = modeName; + break; + } + } + selected[other.name + '/' + pick] = 'source'; + } + for (let s = 0; s < styleSetNames.length; s++) { + selected[styleSetNames[s]] = 'source'; + } + themes.push({ + id: conv.themeIdSegment(col.name) + '-' + conv.themeIdSegment(modeName), + name: modeName, + group: col.name, + selectedTokenSets: selected + }); + } + } + + result['$themes'] = themes; + result['$metadata'] = { tokenSetOrder: tokenSetOrder }; + return result; +} + // ============================================================================ // SECTION 5: COLOR PARSING MODULE (JSF Rule 4.7) // ============================================================================ @@ -2992,6 +3545,10 @@ async function exportVariables( } outputData = JSON.stringify(w3cExportData, null, 2); Logger.log(`✅ Export complete (W3C format): ${stats.collections} collections, ${stats.variables} variables`); + } else if (exportFormat === 'tokens-studio') { + // Tokens Studio (tokens-studio/figma-plugin) single-file container + outputData = JSON.stringify(convertToTokensStudio(exportData), null, 2); + Logger.log(`✅ Export complete (Tokens Studio format): ${stats.collections} collections, ${stats.variables} variables`); } else { // Figma JSON format outputData = JSON.stringify(exportData, null, 2); diff --git a/variables-styles-extractor/ui.html b/variables-styles-extractor/ui.html index 7430970..0ce5ca6 100644 --- a/variables-styles-extractor/ui.html +++ b/variables-styles-extractor/ui.html @@ -3220,6 +3220,25 @@ flex-shrink: 0; } + .simple-export-format-row { + display: flex; + align-items: center; + gap: 8px; + flex-shrink: 0; + } + + .simple-export-format-label { + font-size: 11px; + font-weight: 600; + flex-shrink: 0; + } + + .simple-export-format-row .form-select { + flex: 1; + font-size: 11px; + padding: 4px 6px; + } + .simple-log-slot { display: flex; flex-direction: column; @@ -3552,6 +3571,7 @@ @@ -3678,6 +3698,14 @@
+
+ + +
@@ -5495,6 +5523,7 @@ let stylesInfo = { colorStyles: 0, textStyles: 0, effectStyles: 0, gridStyles: 0 }; let importData = null; let exportData = ''; + let lastExportFormat = 'figma'; // Format of the last completed export (drives download filename) let selectedExportCollections = new Set(); let selectedImportCollections = new Set(); let selectedPlan = 'professional'; // Default plan @@ -6819,6 +6848,7 @@ } exportData = chunks.join(''); + lastExportFormat = typeof msg.format === 'string' ? msg.format : 'figma'; if (typeof msg.totalLength === 'number' && exportData.length !== msg.totalLength) { addLog('❌ Export failed: data length mismatch', 'error'); @@ -7622,7 +7652,7 @@ : 'original'; // Always original in Simple mode const exportFormat = isAdvancedMode() ? (document.getElementById('export-format')?.value || 'figma') - : 'figma'; // Always Figma JSON in Simple mode + : (document.getElementById('simple-export-format')?.value || 'figma'); // Simple mode: compact format select const resolveAliases = isAdvancedMode() ? (document.getElementById('export-resolve-aliases')?.checked || false) : false; // Keep aliases in Simple mode @@ -7663,6 +7693,8 @@ if (exportFormat === 'w3c') { addLog(`📄 Using W3C Design Tokens format`, 'info'); + } else if (exportFormat === 'tokens-studio') { + addLog(`📄 Using Tokens Studio format (DTCG)`, 'info'); } if (resolveAliases) { @@ -7694,6 +7726,8 @@ if (helpEl) { if (format === 'w3c') { helpEl.textContent = 'Export to W3C Design Tokens (Style Dictionary compatible)'; + } else if (format === 'tokens-studio') { + helpEl.textContent = 'Tokens Studio–compatible format (DTCG)'; } else { helpEl.textContent = 'Export to Figma JSON Format'; } @@ -7739,10 +7773,12 @@ const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = 'export.json'; + // Tokens Studio convention: the plugin expects a file named tokens.json + const filename = lastExportFormat === 'tokens-studio' ? 'tokens.json' : 'export.json'; + a.download = filename; a.click(); URL.revokeObjectURL(url); - addLog('💾 Downloaded export.json', 'success'); + addLog('💾 Downloaded ' + filename, 'success'); } // ========== IMPORT ========== From 02d4a8b14011a05a49897e7938f1140697426cc8 Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:44:02 +0530 Subject: [PATCH 06/20] =?UTF-8?q?feat:=20compact=20Simple-mode=20window=20?= =?UTF-8?q?=E2=80=94=20sections=20match=20Advanced=20column=20widths?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Simple-mode sections resize from 378-379px to the Advanced column widths (279/280/280), and the plugin window follows the mode: 905x628 in Simple (the default on open), 1200x628 in Advanced. The UI posts 'resize_ui' with the mode on toggle; the backend accepts only the two known sizes (never arbitrary dimensions from the iframe) via a UI_SIZE constant shared with the initial showUI call. 2px slack retained, mirroring Advanced. Verified in preview at both sizes: no horizontal scroll, correct round-trip messages, body width tracks mode. Co-Authored-By: Claude Fable 5 --- variables-styles-extractor/code.js | 2 +- variables-styles-extractor/src/code.ts | 24 ++++++++++++++++++++---- variables-styles-extractor/ui.html | 15 ++++++++++++--- 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/variables-styles-extractor/code.js b/variables-styles-extractor/code.js index 5e7f7c6..4afdbbd 100644 --- a/variables-styles-extractor/code.js +++ b/variables-styles-extractor/code.js @@ -8,4 +8,4 @@ * @version 2.0.0 * @author Tushar Kant Naik * @website https://tusharkantnaik.com - */figma.showUI(__html__,{width:1200,height:628,themeColors:!0,title:"☕️ Variables & Styles Extractor v2.0.0"});const Logger={log(e,t){console.log(`[Variables Extractor] ${e}`,t||""),figma.ui.postMessage({type:"log",message:e,data:t})},send(e,t){figma.ui.postMessage({type:e,data:t})}};function yieldToHost(){return new Promise(function(e){setTimeout(e,0)})}function makeCancelError(){const e=new Error("Operation cancelled");return e.isOperationCancelled=!0,e}function isCancelError(e){return"object"==typeof e&&null!==e&&!0===e.isOperationCancelled}const currentOperation={type:null,cancelRequested:!1,cancellable:!0};function beginOperation(e){return null!==currentOperation.type?(figma.ui.postMessage({type:"operation_denied",requested:e,running:currentOperation.type}),!1):(currentOperation.type=e,currentOperation.cancelRequested=!1,currentOperation.cancellable=!0,!0)}function endOperation(){currentOperation.type=null,currentOperation.cancelRequested=!1,currentOperation.cancellable=!0}function checkCancelled(){if(currentOperation.cancelRequested&¤tOperation.cancellable)throw makeCancelError()}async function withOperation(e,t){if(beginOperation(e))try{await t()}finally{endOperation()}}async function runBatched(e,t,o,a){const s=e.length;for(let n=0;n0&&n>=r)&&l-oo&&(o=t.modes.length);return t=o>20?"enterprise":o>10?"organization":"professional",Object.assign({plan:t},PLAN_LIMITS[t])}async function validateImportAgainstPlan(e,t){const o=t?Object.assign({plan:t},PLAN_LIMITS[t]):await detectCurrentPlan(),a=await figma.variables.getLocalVariableCollectionsAsync(),s=a.reduce((e,t)=>Math.max(e,t.modes.length),0),n=(await figma.variables.getLocalVariablesAsync()).length,r=[];for(const t of e)"_styles"in t||r.push(t);let i=0,l=0;const c=[];for(const e of r){const t=Object.keys(e)[0],a=e[t];if(!a||!a.modes)continue;const s=Object.keys(a.modes).length;s>i&&(i=s),s>o.maxModesPerCollection&&c.push(`"${t}" (${s} modes, limit: ${o.maxModesPerCollection===1/0?"∞":o.maxModesPerCollection})`);const n=Object.values(a.modes)[0];n&&(l+=countNestedVariables(n))}const g=[],d=[];c.length;for(const e of r){const t=Object.keys(e)[0],o=e[t];if(!o||!o.modes)continue;const a=Object.values(o.modes)[0],s=a?countNestedVariables(a):0;s>5e3&&d.push(`Collection "${t}" has ${s} variables, exceeds limit of 5000`)}l>1e3&&g.push(`Large import: ${l} variables. This may take a moment.`),r.length>10&&g.push(`Importing ${r.length} collections. Consider importing in batches.`);const f=new Set;let u=0;for(const e of r){const t=e[Object.keys(e)[0]];if(t&&t.modes)for(const e of Object.keys(t.modes)){const o=flattenVariables(t.modes[e],"");for(const{value:e}of o)e.$libraryRef&&e.$collectionName&&(f.add(e.$collectionName),u++)}}const p=[];let m=0;for(const t of e)if("_styles"in t){const e=t._styles;if(e.textStyles)for(const t of e.textStyles){m++;const e=`${t.fontFamily}|${t.fontStyle}`;p.some(t=>`${t.family}|${t.style}`===e)||p.push({family:t.fontFamily,style:t.fontStyle})}}return Object.assign(Object.assign({currentPlan:o,existing:{collections:a.length,maxModesInAnyCollection:s,totalVariables:n},importing:{collections:r.length,maxModesInAnyCollection:i,totalVariables:l,collectionsExceedingModeLimit:c},warnings:g,errors:d,canImport:0===d.length},f.size>0&&{libraryDependencies:{variableCount:u,collections:Array.from(f)}}),p.length>0&&{fontDependencies:{styleCount:m,fonts:p}})}function countNestedVariables(e,t=0){for(const[,o]of Object.entries(e))o&&"object"==typeof o&&("$type"in o&&"$value"in o?t++:t=countNestedVariables(o,t));return t}const MathUtils={round2:e=>Math.round(100*e)/100,clamp:(e,t,o)=>Math.max(t,Math.min(o,e)),toHexByte:e=>Math.round(255*e).toString(16).padStart(2,"0"),fromHexByte:e=>parseInt(e,16)/255};function calculateHue(e,t,o,a,s){if(a===s)return 0;const n=a-s;let r=0;switch(a){case e:r=((t-o)/n+(t.5?e/(2-s-n):e/(s+n)}const l={h:calculateHue(t,o,a,s,n),s:Math.round(100*i),l:Math.round(100*r)},c=e.a;return void 0!==c&&c<1?Object.assign(Object.assign({},l),{a:MathUtils.round2(c)}):l},toHsb(e){const{r:t,g:o,b:a}=e,s=Math.max(t,o,a),n=Math.min(t,o,a),r=0===s?0:(s-n)/s,i={h:calculateHue(t,o,a,s,n),s:Math.round(100*r),b:Math.round(100*s)},l=e.a;return void 0!==l&&l<1?Object.assign(Object.assign({},i),{a:MathUtils.round2(l)}):i},toAllFormats(e){return{hex:this.toHex(e),rgb:this.toRgb255(e),css:this.toCss(e),hsl:this.toHsl(e),hsb:this.toHsb(e)}}},NamingConverter={convert(e,t){if("original"===t)return e;const o=e.replace(/([a-z])([A-Z])/g,"$1 $2").split(/[\s\/\-_]+/).filter(e=>e.length>0).map(e=>e.toLowerCase());if(0===o.length)return e;switch(t){case"camelCase":return o[0]+o.slice(1).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("");case"kebab-case":return o.join("-");case"snake_case":return o.join("_");default:return e}},convertPath(e,t){return"original"===t?e:e.split("/").map(e=>this.convert(e,t)).join("/")},convertCollectionName(e,t){return this.convert(e,t)},convertModeName(e,t){return this.convert(e,t)},addOriginalName(e,t){if("original"===t)return{converted:e};const o=this.convert(e,t);return o===e?{converted:e}:{converted:o,original:e}}};async function resolveAliasValue(e,t,o=10){if(o<=0)return Logger.log(`⚠️ Max alias resolution depth reached for ${e.name}`),"";let a=e.valuesByMode[t];if(void 0===a){const t=Object.keys(e.valuesByMode);t.length>0&&(a=e.valuesByMode[t[0]])}if(void 0===a)return"";if(isVariableAlias(a)){const e=await figma.variables.getVariableByIdAsync(a.id);return e?resolveAliasValue(e,t,o-1):""}return a}const W3C_TYPE_MAP={color:"color",float:"number",string:"string",boolean:"boolean"},W3CConverter={colorToW3C:e=>e.hex,typeToW3C:e=>W3C_TYPE_MAP[e]||"string",valueToW3C(e,t=!1){const o={$value:"",$type:this.typeToW3C(e.$type)};return t&&"string"==typeof e.$value&&e.$value.startsWith("{")?o.$value=e.$value:"color"===e.$type&&"object"==typeof e.$value?o.$value=e.$value.hex:o.$value=e.$value,e.$description&&(o.$description=e.$description),e.$scopes&&e.$scopes.length>0&&!e.$scopes.includes("ALL_SCOPES")&&(o.$extensions={"com.figma":{scopes:e.$scopes}}),o},collectionToW3C(e,t,o,a){const s={};a&&a!==e&&(s.$description=`Figma collection: ${a}`);const n=Object.keys(t);if(1===n.length)this.addTokensToGroup(s,t[n[0]],o);else for(const e of n){const a=NamingConverter.convertModeName(e,o);s[a]={},this.addTokensToGroup(s[a],t[e],o)}return s},addTokensToGroup(e,t,o){for(const[a,s]of Object.entries(t)){const t=NamingConverter.convert(a,o);if(isExportVariableValue(s)){const o="string"==typeof s.$value&&s.$value.startsWith("{");e[t]=this.valueToW3C(s,o)}else e[t]={},this.addTokensToGroup(e[t],s,o)}},parseW3CToken(e){var t,o;const a=this.w3cTypeToFigma(e.$type),s=(null===(o=null===(t=e.$extensions)||void 0===t?void 0:t["com.figma"])||void 0===o?void 0:o.scopes)||["ALL_SCOPES"];let n;if("color"===a&&"string"==typeof e.$value){const t=ColorParser.parse(e.$value);n=ColorConverter.toAllFormats(t)}else n="string"==typeof e.$value||"number"==typeof e.$value||"boolean"==typeof e.$value?e.$value:JSON.stringify(e.$value);return e.$description?{$type:a,$value:n,$scopes:s,$description:e.$description}:{$type:a,$value:n,$scopes:s}},w3cTypeToFigma:e=>({color:"color",number:"float",dimension:"float",string:"string",boolean:"boolean",fontFamily:"string",fontWeight:"float",duration:"string",cubicBezier:"string"}[e]||"string"),isW3CFormat(e){if("object"!=typeof e||null===e)return!1;const t=e;for(const e of Object.keys(t)){const o=t[e];if("object"==typeof o&&null!==o){if("$value"in o&&"$type"in o)return!0;for(const e of Object.keys(o)){const t=o[e];if("object"==typeof t&&null!==t&&"$value"in t)return!0}}}return Array.isArray(e),!1},w3cToFigmaFormat(e){const t=[];for(const[o,a]of Object.entries(e)){if(o.startsWith("$"))continue;const e={[o]:{modes:{Default:this.w3cGroupToNestedVars(a)}}};t.push(e)}return t},w3cGroupToNestedVars(e){const t={};for(const[o,a]of Object.entries(e))o.startsWith("$")||(this.isW3CToken(a)?t[o]=this.parseW3CToken(a):"object"==typeof a&&null!==a&&(t[o]=this.w3cGroupToNestedVars(a)));return t},isW3CToken:e=>"object"==typeof e&&null!==e&&"$value"in e},TS_FLOAT_SCOPE_MAP={CORNER_RADIUS:"borderRadius",STROKE_FLOAT:"borderWidth",GAP:"spacing",WIDTH_HEIGHT:"sizing",OPACITY:"opacity",FONT_SIZE:"fontSizes",LINE_HEIGHT:"lineHeights",LETTER_SPACING:"letterSpacing",PARAGRAPH_SPACING:"paragraphSpacing"},TS_STRING_SCOPE_MAP={FONT_FAMILY:"fontFamilies",FONT_STYLE:"fontWeights"},TS_TEXT_CASE_MAP={ORIGINAL:"none",UPPER:"uppercase",LOWER:"lowercase",TITLE:"capitalize"},TS_TEXT_DECORATION_MAP={NONE:"none",UNDERLINE:"underline",STRIKETHROUGH:"line-through"},TokensStudioConverter={sanitizeSegment(e){let t=e.replace(/[{}$]/g,"");return 0===t.length&&(t="_"),"__proto__"!==t&&"constructor"!==t&&"prototype"!==t||(t="_"+t),t},sanitizeAliasRef(e){const t=e.substring(1,e.length-1).split("."),o=[];for(let e=0;e"string"==typeof e&&"{"===e.charAt(0)&&"}"===e.charAt(e.length-1),roundNumber:e=>Math.round(1e3*e)/1e3,colorToTS(e){const t=void 0!==e.rgb?e.rgb.a:void 0;return void 0!==t&&t<1?e.css:void 0!==e.rgb&&e.hex.length>7?e.hex.substring(0,7):e.hex},floatTypeFromScopes(e){if(!e)return"number";let t="",o=0;for(let a=0;ae.toLowerCase().replace(/\s+/g,"-")};function convertToTokensStudio(e){const t=TokensStudioConverter,o={},a=[],s={libraryRefsSkipped:0,imagePaintsSkipped:0,blurEffectsSkipped:0},n=[];let r=null;for(let t=0;t0){const e={};for(let o=0;o0){const t=styleSetKey("styles/color");o[t]=e,a.push(t),i.push(t)}}if(void 0!==r.textStyles&&r.textStyles.length>0){const e={};for(let o=0;o0){const t=styleSetKey("styles/typography");o[t]=e,a.push(t),i.push(t)}}if(void 0!==r.effectStyles&&r.effectStyles.length>0){const e={};for(let o=0;o0){const t=styleSetKey("styles/effects");o[t]=e,a.push(t),i.push(t)}}void 0!==r.gridStyles&&r.gridStyles.length>0&&Logger.log("Tokens Studio export: grid styles skipped (no Tokens Studio representation)")}s.libraryRefsSkipped>0&&Logger.log("Tokens Studio export: skipped "+s.libraryRefsSkipped+" library-alias token(s) with no resolvable local value"),s.imagePaintsSkipped>0&&Logger.log("Tokens Studio export: skipped "+s.imagePaintsSkipped+" image paint(s) in color styles (no Tokens Studio representation)"),s.blurEffectsSkipped>0&&Logger.log("Tokens Studio export: skipped "+s.blurEffectsSkipped+" blur effect(s) in effect styles (boxShadow tokens carry shadows only)");const l=[];for(let e=0;e{const a=o<0?o+1:o>1?o-1:o;return a<1/6?e+6*(t-e)*a:a<.5?t:a<2/3?e+(t-e)*(2/3-a)*6:e},r=n<.5?n*(1+s):n+s-n*s,i=2*n-r;return{r:hue2rgb(i,r,a+1/3),g:hue2rgb(i,r,a),b:hue2rgb(i,r,a-1/3),a:null!==(o=e.a)&&void 0!==o?o:1}},fromHsb(e){var t;const o=e.h/360,a=e.s/100,s=e.b/100,n=Math.floor(6*o),r=6*o-n,i=s*(1-a),l=s*(1-r*a),c=s*(1-(1-r)*a),g=[[s,c,i],[l,s,i],[i,s,c],[i,l,s],[c,i,s],[s,i,l]],[d,f,u]=g[n%6];return{r:d,g:f,b:u,a:null!==(t=e.a)&&void 0!==t?t:1}},parse(e){var t;if("object"==typeof e&&null!==e&&"hex"in e&&"rgb"in e)return this.fromHex(e.hex);if("object"==typeof e&&null!==e&&"r"in e&&"g"in e&&"b"in e){const o=e;return o.r<=1&&o.g<=1&&o.b<=1?{r:o.r,g:o.g,b:o.b,a:null!==(t=o.a)&&void 0!==t?t:1}:this.fromRgb255(o)}return"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"l"in e?this.fromHsl(e):"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"b"in e?this.fromHsb(e):"string"==typeof e?e.startsWith("rgb")||e.startsWith("hsl")?this.fromCss(e):this.fromHex(e):{r:0,g:0,b:0,a:1}}};class VariableCache{constructor(){this.collectionMap=new Map,this.variableMap=new Map,this.libraryVariableMap=new Map,this.libraryCollectionNames=new Set,this.initialized=!1,this.libraryIndexed=!1}async ensureReady(){this.initialized||(await this.rebuildLocal(),this.initialized=!0)}async initialize(){await this.ensureReady()}async rebuild(){await this.rebuildLocal(),await this.ensureLibraryIndex(),this.initialized=!0}async rebuildLocal(){this.clearLocal();const e=await figma.variables.getLocalVariableCollectionsAsync();for(let t=0;t{try{const o=await figma.variables.importVariableByKeyAsync(e.key);o&&this.libraryVariableMap.set(`${t}/${o.name}`,o)}catch(e){}})}catch(e){Logger.log(` ⚠️ Could not index library collection "${o.name}": ${e}`)}}this.libraryCollectionNames.size>0&&Logger.log(`📚 Indexed ${this.libraryVariableMap.size} library variables from ${this.libraryCollectionNames.size} connected libraries`)}catch(e){Logger.log(`⚠️ Could not access team library: ${e}`)}}getCollection(e){return this.collectionMap.get(e)}getVariable(e){return this.variableMap.get(e)||this.libraryVariableMap.get(e)}setVariable(e,t){this.variableMap.set(e,t)}setCollection(e,t){this.collectionMap.set(e,t)}removeCollection(e){this.collectionMap.delete(e);const t=[];for(const o of this.variableMap.keys())o.startsWith(`${e}/`)&&t.push(o);for(const e of t)this.variableMap.delete(e)}isCollectionAvailable(e){return this.collectionMap.has(e)||this.libraryCollectionNames.has(e)}getLibraryCollectionNames(){return Array.from(this.libraryCollectionNames)}get size(){return this.variableMap.size}get collections(){return this.collectionMap.values()}getVariableKeys(){return Array.from(this.variableMap.keys())}}const variableCache=new VariableCache;function isExportVariableValue(e){return"object"==typeof e&&null!==e&&"$type"in e}function isVariableAlias(e){return"object"==typeof e&&null!==e&&"VARIABLE_ALIAS"===e.type}const TypeMapper={toExportType(e){var t;return null!==(t={COLOR:"color",FLOAT:"float",STRING:"string",BOOLEAN:"boolean"}[e])&&void 0!==t?t:"string"},toFigmaType(e){var t;return null!==(t={color:"COLOR",float:"FLOAT",string:"STRING",boolean:"BOOLEAN"}[e])&&void 0!==t?t:"STRING"},scopesToArray:e=>0===e.length||e.includes("ALL_SCOPES")?["ALL_SCOPES"]:[...e],arrayToScopes:e=>e.includes("ALL_SCOPES")?["ALL_SCOPES"]:e};async function getVariableBindingInfo(e,t){if(!(null==e?void 0:e[t]))return{};const o=e[t];if(!o)return{};const a=await figma.variables.getVariableByIdAsync(o.id);if(!a)return{id:o.id};const s=await figma.variables.getVariableCollectionByIdAsync(a.variableCollectionId);return{id:o.id,name:a.name,collection:null==s?void 0:s.name}}async function extractBindings(e,t){if(!e)return;const o={};for(const a of t){const t=await getVariableBindingInfo(e,a);t.name&&(o[a]=t)}return Object.keys(o).length>0?o:void 0}function flattenVariables(e,t){const o=[];for(const a of Object.keys(e)){const s=e[a],n=t?`${t}/${a}`:a;isExportVariableValue(s)?o.push({path:n,value:s}):o.push(...flattenVariables(s,n))}return o}function getValueAtPath(e,t){const o=t.split("/");let a=e;for(const e of o){if("object"!=typeof a||null===a)return null;if(isExportVariableValue(a))return null;a=a[e]}return isExportVariableValue(a)?a:null}const ColorStyleProcessor={async export(e){var t;const o=null!==(t=null==e?void 0:e.includeImages)&&void 0!==t&&t,a=[],s=await figma.getLocalPaintStylesAsync();return await runSequentialAsync(s,20,async function(e){var t,s,n;if(0===e.paints.length)return;const r=[];let i,l,c;for(const a of e.paints)if("SOLID"===a.type){const e=a.color;let o=null!==(t=a.opacity)&&void 0!==t?t:1;void 0!==e.a&&e.a<1&&1===o&&(o=e.a);const s={r:a.color.r,g:a.color.g,b:a.color.b,a:o},n={type:"SOLID",color:ColorConverter.toAllFormats(s),opacity:MathUtils.round2(o)};r.push(n),i||(i=n.color,l=n.opacity,c=await extractBindings(a.boundVariables,["color"]))}else if("GRADIENT_LINEAR"===a.type||"GRADIENT_RADIAL"===a.type||"GRADIENT_ANGULAR"===a.type||"GRADIENT_DIAMOND"===a.type){const e=a.gradientStops.map(e=>{var t;return{position:MathUtils.round2(e.position),color:ColorConverter.toAllFormats({r:e.color.r,g:e.color.g,b:e.color.b,a:null!==(t=e.color.a)&&void 0!==t?t:1})}}),t=Object.assign(Object.assign({type:a.type,gradientStops:e},a.gradientTransform&&{gradientTransform:a.gradientTransform}),{opacity:MathUtils.round2(null!==(s=a.opacity)&&void 0!==s?s:1)});r.push(t)}else if("IMAGE"===a.type){const t=Object.assign(Object.assign(Object.assign(Object.assign({type:"IMAGE",scaleMode:a.scaleMode},a.imageHash&&{imageHash:a.imageHash}),{opacity:MathUtils.round2(null!==(n=a.opacity)&&void 0!==n?n:1)}),void 0!==a.rotation&&{rotation:a.rotation}),a.filters&&{filters:Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},void 0!==a.filters.exposure&&{exposure:a.filters.exposure}),void 0!==a.filters.contrast&&{contrast:a.filters.contrast}),void 0!==a.filters.saturation&&{saturation:a.filters.saturation}),void 0!==a.filters.temperature&&{temperature:a.filters.temperature}),void 0!==a.filters.tint&&{tint:a.filters.tint}),void 0!==a.filters.highlights&&{highlights:a.filters.highlights}),void 0!==a.filters.shadows&&{shadows:a.filters.shadows})});if(o&&a.imageHash)try{const e=figma.getImageByHash(a.imageHash);if(e){const o=await e.getBytesAsync();if(o){const e=figma.base64Encode(o);t.imageBase64=e}}}catch(t){Logger.log(`⚠️ Could not export image data for style "${e.name}": ${t}`)}r.push(t)}if(0===r.length)return;const g=Object.assign(Object.assign(Object.assign(Object.assign({name:e.name,paints:r},i&&{color:i}),void 0!==l&&{opacity:l}),e.description&&{description:e.description}),c&&Object.keys(c).length>0&&{boundVariables:c});a.push(g)}),a},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalPaintStylesAsync())s.set(e.name,e);return await runSequentialAsync(e,20,async function(e){var n,r,i,l;let c;s.has(e.name)?(c=s.get(e.name),a++):(c=figma.createPaintStyle(),c.name=e.name,o++),e.description&&(c.description=e.description);const g=[];if(e.paints&&e.paints.length>0){for(const o of e.paints)if("SOLID"===o.type){const a=ColorParser.parse(o.color);let s=null!==(n=o.opacity)&&void 0!==n?n:1;a.a<1&&void 0===o.opacity&&(s=MathUtils.round2(a.a));let r={type:"SOLID",color:{r:a.r,g:a.g,b:a.b},opacity:MathUtils.round2(s)};if(e.boundVariables&&0===g.length)for(const[o,a]of Object.entries(e.boundVariables))if(a.name&&a.collection){const e=t.getVariable(`${a.collection}/${a.name}`);if(e)try{r=figma.variables.setBoundVariableForPaint(r,o,e)}catch(e){Logger.log(`⚠️ Could not bind ${o}: ${e}`)}}g.push(r)}else if("GRADIENT_LINEAR"===o.type||"GRADIENT_RADIAL"===o.type||"GRADIENT_ANGULAR"===o.type||"GRADIENT_DIAMOND"===o.type){const e=o.gradientStops.map(e=>{const t=ColorParser.parse(e.color);return{position:e.position,color:{r:t.r,g:t.g,b:t.b,a:t.a}}}),t=o.gradientTransform?[[o.gradientTransform[0][0],o.gradientTransform[0][1],o.gradientTransform[0][2]],[o.gradientTransform[1][0],o.gradientTransform[1][1],o.gradientTransform[1][2]]]:[[1,0,0],[0,1,0]],a={type:o.type,gradientStops:e,gradientTransform:t,opacity:null!==(r=o.opacity)&&void 0!==r?r:1};g.push(a)}else if("IMAGE"===o.type){let t=null;if(o.imageBase64)try{const a=figma.base64Decode(o.imageBase64);t=figma.createImage(a).hash,Logger.log(`✅ Created image from base64 data for style "${e.name}"`)}catch(t){Logger.log(`⚠️ Could not import image from base64 for style "${e.name}": ${t}`)}if(!t&&o.imageHash){figma.getImageByHash(o.imageHash)?(t=o.imageHash,Logger.log(`✅ Found existing image with hash for style "${e.name}"`)):Logger.log(`⚠️ Image hash not found in file for style "${e.name}", skipping image paint (imageHash cannot be null)`)}if(t){const e=Object.assign(Object.assign({type:"IMAGE",scaleMode:o.scaleMode,imageHash:t,opacity:null!==(i=o.opacity)&&void 0!==i?i:1},void 0!==o.rotation&&{rotation:o.rotation}),o.filters&&{filters:o.filters});g.push(e)}}}else if(e.color){const o=ColorParser.parse(e.color);let a=null!==(l=e.opacity)&&void 0!==l?l:1;o.a<1&&void 0===e.opacity&&(a=MathUtils.round2(o.a));let s={type:"SOLID",color:{r:o.r,g:o.g,b:o.b},opacity:MathUtils.round2(a)};if(e.boundVariables)for(const[o,a]of Object.entries(e.boundVariables))if(a.name&&a.collection){const e=t.getVariable(`${a.collection}/${a.name}`);if(e)try{s=figma.variables.setBoundVariableForPaint(s,o,e)}catch(e){Logger.log(`⚠️ Could not bind ${o}: ${e}`)}}g.push(s)}g.length>0&&(c.paints=g)}),{created:o,updated:a}}},TextStyleProcessor={async export(e){const t=[],o=await figma.getLocalTextStylesAsync();return await runSequentialAsync(o,20,async function(e){const o=Object.assign(Object.assign({name:e.name,fontFamily:e.fontName.family,fontStyle:e.fontName.style,fontSize:e.fontSize,lineHeight:e.lineHeight,letterSpacing:e.letterSpacing,textCase:e.textCase,textDecoration:e.textDecoration},e.description&&{description:e.description}),{boundVariables:await extractBindings(e.boundVariables,["fontSize","lineHeight","letterSpacing","paragraphSpacing","paragraphIndent"])});t.push(o)}),t},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalTextStylesAsync())s.set(e.name,e);return await runSequentialAsync(e,20,async function(e){let n;s.has(e.name)?(n=s.get(e.name),a++):(n=figma.createTextStyle(),n.name=e.name,o++),e.description&&(n.description=e.description);try{if(await figma.loadFontAsync({family:e.fontFamily,style:e.fontStyle}),n.fontName={family:e.fontFamily,style:e.fontStyle},n.fontSize=e.fontSize,n.lineHeight=e.lineHeight,n.letterSpacing=e.letterSpacing,e.textCase&&(n.textCase=e.textCase),e.textDecoration&&(n.textDecoration=e.textDecoration),e.boundVariables)for(const[o,a]of Object.entries(e.boundVariables))if(a.name&&a.collection){const e=t.getVariable(`${a.collection}/${a.name}`);if(e)try{n.setBoundVariable(o,e)}catch(e){}}}catch(t){Logger.log(`⚠️ Could not load font for ${e.name}: ${t}`)}}),{created:o,updated:a}}},EffectStyleProcessor={async export(e){const t=[],o=await figma.getLocalEffectStylesAsync();return await runSequentialAsync(o,20,async function(e){const o=[];for(const t of e.effects){const e=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:t.type,visible:t.visible},"radius"in t&&{radius:t.radius}),"spread"in t&&{spread:t.spread}),"offset"in t&&{offset:t.offset}),"color"in t&&{color:ColorConverter.toAllFormats(t.color)}),"blendMode"in t&&{blendMode:t.blendMode}),"showShadowBehindNode"in t&&{showShadowBehindNode:t.showShadowBehindNode}),{boundVariables:await extractBindings(t.boundVariables,["color","radius","spread","offsetX","offsetY"])});o.push(e)}const a=Object.assign(Object.assign({name:e.name},e.description&&{description:e.description}),{effects:o});t.push(a)}),t},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalEffectStylesAsync())s.set(e.name,e);return await runSequentialAsync(e,20,async function(e){let n;s.has(e.name)?(n=s.get(e.name),a++):(n=figma.createEffectStyle(),n.name=e.name,o++),e.description&&(n.description=e.description);const r=e.effects.map(e=>{var t;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:e.type,visible:null===(t=e.visible)||void 0===t||t},void 0!==e.radius&&{radius:e.radius}),void 0!==e.spread&&{spread:e.spread}),void 0!==e.offset&&{offset:e.offset}),void 0!==e.color&&{color:(()=>{const t=ColorParser.parse(e.color);return{r:t.r,g:t.g,b:t.b,a:MathUtils.round2(t.a)}})()}),void 0!==e.blendMode&&{blendMode:e.blendMode}),void 0!==e.showShadowBehindNode&&{showShadowBehindNode:e.showShadowBehindNode})});n.effects=r;for(let o=0;o{var t,o,a,s,n,r,i;const l=e.color?ColorParser.parse(e.color):{r:1,g:0,b:0,a:.1},c={r:l.r,g:l.g,b:l.b,a:MathUtils.round2(l.a)};if("GRID"===e.pattern)return{pattern:"GRID",sectionSize:null!==(t=e.sectionSize)&&void 0!==t?t:10,visible:!1!==e.visible,color:c};const g=null!==(o=e.alignment)&&void 0!==o?o:"STRETCH",d={pattern:e.pattern,gutterSize:null!==(a=e.gutterSize)&&void 0!==a?a:10,count:null!==(s=e.count)&&void 0!==s?s:5,visible:!1!==e.visible,color:c};if("STRETCH"===g)return Object.assign(Object.assign({},d),{alignment:"STRETCH",offset:null!==(n=e.offset)&&void 0!==n?n:0});if("CENTER"===g)return Object.assign(Object.assign({},d),{alignment:"CENTER",sectionSize:null!==(r=e.sectionSize)&&void 0!==r?r:100});{const t=Object.assign(Object.assign({},d),{alignment:g,offset:null!==(i=e.offset)&&void 0!==i?i:0});return void 0!==e.sectionSize&&(t.sectionSize=e.sectionSize),t}});n.layoutGrids=r;for(let o=0;ot.name===e);a?await checkVariablesDiff(i,a.modeId,o,r,"",t):l=!0}t.modifiedVariables.some(e=>e.collection===r)||t.newVariables.some(e=>e.collection===r)?(t.modifiedCollections.push(r),t.summary.collectionsModified++):(t.unchangedCollections.push(r),t.summary.collectionsUnchanged++)}return t}function countVariablesInCollection(e){let t=0;const o=Object.values(e)[0];return o&&(t=countVarsInNestedObj(o)),t}function countVarsInNestedObj(e){let t=0;for(const o of Object.values(e))isExportVariableValue(o)?t++:t+=countVarsInNestedObj(o);return t}async function checkVariablesDiff(e,t,o,a,s,n){for(const[r,i]of Object.entries(o)){const o=s?`${s}/${r}`:r;if(isExportVariableValue(i)){const e=variableCache.getVariable(`${a}/${o}`);if(e){const s=e.valuesByMode[t],r=i.$value;valuesAreDifferent(s,r)?(n.modifiedVariables.push({collection:a,path:o,oldValue:formatValueForDisplay(s),newValue:formatValueForDisplay(r)}),n.summary.variablesModified++):(n.unchangedVariables++,n.summary.variablesUnchanged++)}else n.newVariables.push({collection:a,path:o}),n.summary.variablesNew++}else await checkVariablesDiff(e,t,i,a,o,n)}}function valuesAreDifferent(e,t){if(void 0===e)return!0;if(isVariableAlias(e))return"string"==typeof t&&t.startsWith("{"),!0;if("object"==typeof e&&null!==e&&"r"in e){if("object"==typeof t&&null!==t&&"hex"in t){return ColorConverter.toAllFormats(e).hex.toLowerCase()!==t.hex.toLowerCase()}return!0}return e!==t}function formatValueForDisplay(e){if(void 0===e)return"undefined";if("object"==typeof e&&null!==e){if("hex"in e)return e.hex;if("r"in e)return ColorConverter.toAllFormats(e).hex;if("id"in e)return"{alias}"}return String(e)}async function computeStylesDiff(e,t){if(e.colorStyles){const o=await figma.getLocalPaintStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.colorStyles)a.has(o.name)?(t.modifiedStyles.push({type:"color",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"color",name:o.name}),t.summary.stylesNew++)}if(e.textStyles){const o=await figma.getLocalTextStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.textStyles)a.has(o.name)?(t.modifiedStyles.push({type:"text",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"text",name:o.name}),t.summary.stylesNew++)}if(e.effectStyles){const o=await figma.getLocalEffectStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.effectStyles)a.has(o.name)?(t.modifiedStyles.push({type:"effect",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"effect",name:o.name}),t.summary.stylesNew++)}if(e.gridStyles){const o=await figma.getLocalGridStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.gridStyles)a.has(o.name)?(t.modifiedStyles.push({type:"grid",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"grid",name:o.name}),t.summary.stylesNew++)}}function filterStylesByGroup(e,t){return t?e.filter(function(e){return-1!==t.indexOf(getGroupKey(e.name))}):e}async function exportVariables(e,t,o,a,s="original",n="figma",r,i=!1,l,c){var g,d,f,u,p,m,y,h;Logger.log("📤 Starting export..."),Logger.log(` preserveLibraryRefs: ${o}`),Logger.log(` includeImages: ${a}`),Logger.log(` namingConvention: ${s}`),Logger.log(` exportFormat: ${n}`),Logger.log(` resolveAliases: ${i}`),r&&Logger.log(` selectedModes: ${JSON.stringify(r)}`);try{let o=await figma.variables.getLocalVariableCollectionsAsync();(null==e?void 0:e.length)&&(o=o.filter(t=>e.includes(t.name)),Logger.log(`Filtering to ${o.length} selected collections`));const b=[],v={};let S=0;const C=createProgress("export");let A=0;for(let e=0;eo.includes(e.name)),Logger.log(` Filtering to ${t.length} modes: ${t.map(e=>e.name).join(", ")}`)}const o=NamingConverter.convertCollectionName(e.name,s),a={[o]:Object.assign({modes:{}},o!==e.name&&{$originalName:e.name})};for(const e of t){const t=NamingConverter.convertModeName(e.name,s);a[o].modes[t]={}}await runSequentialAsync(e.variableIds,BATCH.SEQ_EXPORT,async function(n){var r,c;L++;const g=await figma.variables.getVariableByIdAsync(n);if(!g)return;if(l&&l[e.name]&&-1===l[e.name].indexOf(getGroupKey(g.name)))return;S++;const d=g.name.split("/").map(e=>NamingConverter.convert(e,s));for(const e of t){const t=NamingConverter.convertModeName(e.name,s),n=a[o].modes[t],l=g.valuesByMode[e.modeId];let f=n;for(let e=0;eNamingConverter.convert(e,s)).join(".")}}`,p=v,b){const e=t.valuesByMode[Object.keys(t.valuesByMode)[0]];"object"==typeof e&&null!==e&&"r"in e?m=ColorConverter.toAllFormats(e):isVariableAlias(e)||(m=e)}}}else p=""}else p="object"==typeof l&&null!==l&&"r"in l?ColorConverter.toAllFormats(l):l;const S=Object.assign(Object.assign(Object.assign({$scopes:TypeMapper.scopesToArray(g.scopes),$type:TypeMapper.toExportType(g.resolvedType),$value:p},g.description&&{$description:g.description}),y&&h&&{$collectionName:h}),y&&b&&Object.assign({$libraryRef:v},void 0!==m&&{$localValue:m}));f[u]=S}},function(){C.report("export_variables","Exporting variables",L,A)}),b.push(a),"w3c"===n&&(v[o]=W3CConverter.collectionToW3C(o,a[o].modes,s,a[o].$originalName))}let $=null;if(t){if($={},t.colorStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.color:void 0;$.colorStyles=filterStylesByGroup(await ColorStyleProcessor.export({includeImages:a}),e),e&&0===$.colorStyles.length&&delete $.colorStyles}if(t.textStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.text:void 0;$.textStyles=filterStylesByGroup(await TextStyleProcessor.export(),e),e&&0===$.textStyles.length&&delete $.textStyles}if(t.effectStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.effect:void 0;$.effectStyles=filterStylesByGroup(await EffectStyleProcessor.export(),e),e&&0===$.effectStyles.length&&delete $.effectStyles}if(t.gridStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.grid:void 0;$.gridStyles=filterStylesByGroup(await GridStyleProcessor.export(),e),e&&0===$.gridStyles.length&&delete $.gridStyles}Object.keys($).length>0?b.push({_styles:$}):$=null}const w={collections:o.length,variables:S,styles:$?{color:null!==(d=null===(g=$.colorStyles)||void 0===g?void 0:g.length)&&void 0!==d?d:0,text:null!==(u=null===(f=$.textStyles)||void 0===f?void 0:f.length)&&void 0!==u?u:0,effect:null!==(m=null===(p=$.effectStyles)||void 0===p?void 0:p.length)&&void 0!==m?m:0,grid:null!==(h=null===(y=$.gridStyles)||void 0===y?void 0:y.length)&&void 0!==h?h:0}:null};let O;"w3c"===n?($&&Object.keys($).length>0&&(v.$extensions={"com.figma":{styles:$}}),O=JSON.stringify(v,null,2),Logger.log(`✅ Export complete (W3C format): ${w.collections} collections, ${w.variables} variables`)):"tokens-studio"===n?(O=JSON.stringify(convertToTokensStudio(b),null,2),Logger.log(`✅ Export complete (Tokens Studio format): ${w.collections} collections, ${w.variables} variables`)):(O=JSON.stringify(b,null,2),Logger.log(`✅ Export complete: ${w.collections} collections, ${w.variables} variables`)),await sendExportInChunks(O,w,n)}catch(e){if(isCancelError(e))return Logger.log("🛑 Export cancelled"),void figma.ui.postMessage({type:"operation_cancelled",operation:"export",phase:"export",rolledBack:!1,message:"Export cancelled. Nothing was changed."});Logger.log(`❌ Export error: ${e}`),Logger.send("error",{message:`Export failed: ${e}`})}}async function sendExportInChunks(e,t,o){const a=e.length,s=BATCH.EXPORT_CHUNK_BYTES,n=Math.max(1,Math.ceil(a/s));let r=0,i=0;for(;i=55296&&o<=56319&&(t+=1)}const o=e.slice(i,t);figma.ui.postMessage({type:"export_chunk",seq:r,total:n,data:o}),r++,i=t,r%BATCH.EXPORT_YIELD_EVERY===0&&i0?a.modes[s[0]]:{},"");b.push({collectionObj:t,flatPaths:n}),v+=n.length;for(let e=0;e0&&Logger.log(` ✅ Aliases: ${L} resolved, ${$} used fallback values`),y&&t.importStyles){if(Logger.log("📦 Importing styles..."),n.report("import_styles","Importing styles",0,0,!0),y.colorStyles){const e=await ColorStyleProcessor.importStyles(y.colorStyles,variableCache);p+=e.created,m+=e.updated}if(y.textStyles){const e=await TextStyleProcessor.importStyles(y.textStyles,variableCache);p+=e.created,m+=e.updated}if(y.effectStyles){const e=await EffectStyleProcessor.importStyles(y.effectStyles,variableCache);p+=e.created,m+=e.updated}if(y.gridStyles){const e=await GridStyleProcessor.importStyles(y.gridStyles,variableCache);p+=e.created,m+=e.updated}}const w={collectionsCreated:g,variablesCreated:d,variablesUpdated:f,variablesSkipped:u,stylesCreated:p,stylesUpdated:m};figma.commitUndo(),Logger.log("✅ Import complete!"),Logger.send("import_complete",{stats:w,snapshot:s})}catch(e){const t=isCancelError(e);if(t&&!r)return Logger.log("🛑 Import cancelled before any changes were made"),void figma.ui.postMessage({type:"operation_cancelled",operation:"import",phase:"snapshot",rolledBack:!1,message:"Import cancelled. No changes were made."});const o=e instanceof Error?e.message:String(e);if(t?Logger.log("🛑 Import cancelled after mutation started — rolling back..."):Logger.log(`❌ Import error: ${o}`),s){Logger.log("🔄 Attempting automatic rollback to pre-import state..."),Logger.send("import_rolling_back",{error:o});try{await restoreFromSnapshot(s),Logger.log("✅ Automatic rollback successful - file restored to pre-import state"),t?figma.ui.postMessage({type:"operation_cancelled",operation:"import",phase:"rollback",rolledBack:!0,message:"Import cancelled — your file was restored to its previous state."}):Logger.send("import_rollback_complete",{error:o,message:"Import failed but your file has been automatically restored to its previous state."})}catch(e){const t=e instanceof Error?e.message:String(e);Logger.log(`❌ Rollback failed: ${t}`),Logger.send("import_rollback_failed",{error:o,rollbackError:t,message:"Import failed and automatic rollback also failed. Please use Ctrl+Z (Cmd+Z) to undo manually."})}}else Logger.send("error",{message:`Import failed: ${o}. Use Ctrl+Z (Cmd+Z) to undo changes.`})}}function setRawValue(e,t,o){try{if("color"===o.$type){const a=ColorParser.parse(o.$value),s=a.a<1?Object.assign(Object.assign({},a),{a:MathUtils.round2(a.a)}):a;e.setValueForMode(t,s)}else e.setValueForMode(t,o.$value)}catch(e){console.error(`Could not set value: ${e}`)}}function getGroupKey(e){const t=e.indexOf("/");return-1===t?"":e.substring(0,t)}function summarizeGroups(e){const t=Object.create(null),o=[];for(let a=0;a{Logger.log(` ${t+1}. "${e.name}" (id: ${e.id})`)});const t=new Set;let o=0,a=0,s=0;const n=new Map,r=[];for(let i=0;ie.name),variableCount:l.variableIds.length,types:c,groups:summarizeGroups(g)})}r.sort((e,t)=>e.name.localeCompare(t.name));const i=await figma.getLocalPaintStylesAsync(),l=await figma.getLocalTextStylesAsync(),c=await figma.getLocalEffectStylesAsync(),g=await figma.getLocalGridStylesAsync();let d=0;const f=[];for(let e=0;e({family:e,styles:Array.from(t)}));let S=0;for(const e of i)e.boundVariables&&Object.keys(e.boundVariables).length>0&&S++;Logger.send("collections",{collections:r,styles:u,styleGroups:h,libraryDependencies:Array.from(t),fontsUsed:v,stats:{totalVariables:r.reduce((e,t)=>e+t.variableCount,0),totalAliases:o,localAliases:a,libraryAliases:s,styleBindings:S}})}async function clearVariables(e=!1){Logger.log("🗑️ Clearing all variables...");const t=e?null:createProgress("clear");let o=0,a=0,s=!1;try{const n=await figma.variables.getLocalVariableCollectionsAsync(),r=[];let i=0;for(let e=0;e0&&(figma.commitUndo(),s=!0);let l=0;for(let e=0;e0&&(figma.commitUndo(),a=!0);let c=0;const removeStyle=function(e){e.remove(),o++},onBatch=function(e){t&&t.report("clear","Deleting styles",c+e,l)};await runBatched(s,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=s.length,await runBatched(n,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=n.length,await runBatched(r,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=r.length,await runBatched(i,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=i.length,!e&&a&&figma.commitUndo(),Logger.log(`✅ Cleared ${o} styles`),e||Logger.send("clear_complete",{message:`${o} styles`})}catch(t){if(isCancelError(t)){if(e)throw t;return Logger.log(`🛑 Clear styles cancelled after ${o} styles`),void figma.ui.postMessage({type:"operation_cancelled",operation:"clear",phase:"clear",rolledBack:!1,partial:{collectionsDeleted:0,variablesDeleted:o},message:`Clear cancelled — ${o} styles were already deleted. Remaining items were not touched. Use Cmd+Z to restore deleted items.`})}if(Logger.log(`❌ Clear styles error: ${t}`),e)throw t;Logger.send("error",{message:`Failed to clear styles: ${t}`})}}async function clearAll(e=!1){if(Logger.log("🗑️ Clearing everything..."),e)return await clearVariables(!0),void await clearStyles(!0);let t=!1;try{figma.commitUndo(),t=!0,await clearVariables(!0),await clearStyles(!0),figma.commitUndo(),Logger.send("clear_complete",{message:"all variables and styles"})}catch(e){if(t&&figma.commitUndo(),isCancelError(e))return Logger.log("🛑 Clear all cancelled"),void figma.ui.postMessage({type:"operation_cancelled",operation:"clear",phase:"clear",rolledBack:!1,partial:{collectionsDeleted:0,variablesDeleted:0},message:"Clear cancelled — some items may already have been deleted. Use Cmd+Z to restore deleted items."});Logger.log(`❌ Clear all error: ${e}`),Logger.send("error",{message:`Failed to clear: ${e}`})}}async function createUndoSnapshot(e){var t;Logger.log("📸 Creating snapshot of current file state...");const o=await figma.variables.getLocalVariableCollectionsAsync(),a=[],s=new Map;let n=0;for(let e=0;e({id:e.modeId,name:e.name})),variables:[]},i=await runBatchedAsync(t.variableIds,BATCH.ASYNC_LOOKUP,function(e){return figma.variables.getVariableByIdAsync(e)},function(t){e&&e.report("snapshot","Preparing snapshot (undo safety)",r+t,n)});r+=t.variableIds.length;for(let e=0;e0){t.renameMode(t.modes[0].modeId,e.modes[0].name);for(let o=1;o0&&(s.scopes=a.scopes);for(const t of e.modes){const r=o[t.name],i=a.values[t.name];if(i)if(i.isAlias&&i.aliasName)n.push({variable:s,modeId:r,aliasPath:i.aliasName,aliasCollection:i.aliasCollection||e.name});else if(void 0!==i.value){let e;e="COLOR"===a.type&&"string"==typeof i.value?ColorParser.parse(i.value):i.value,s.setValueForMode(r,e)}}}},function(e,o){t.report("undo_restore","Restoring variables",e,o)}),Logger.log(` Resolving ${n.length} aliases...`),await runBatched(n,BATCH.SYNC_LIGHT,function(e){const t=`${e.aliasCollection}/${e.aliasPath}`,o=variableCache.getVariable(t);o&&e.variable.setValueForMode(e.modeId,{type:"VARIABLE_ALIAS",id:o.id})},function(e,o){t.report("undo_aliases","Restoring aliases",e,o)}),Logger.log(" Restoring styles..."),t.report("undo_styles","Restoring styles",0,0,!0),a.colorStyles&&a.colorStyles.length>0&&await ColorStyleProcessor.importStyles(a.colorStyles,variableCache),a.textStyles&&a.textStyles.length>0&&await TextStyleProcessor.importStyles(a.textStyles,variableCache),a.effectStyles&&a.effectStyles.length>0&&await EffectStyleProcessor.importStyles(a.effectStyles,variableCache),a.gridStyles&&a.gridStyles.length>0&&await GridStyleProcessor.importStyles(a.gridStyles,variableCache),Logger.log("✅ File restored from snapshot")}figma.ui.onmessage=async e=>{switch(e.type){case"cancel_operation":null!==currentOperation.type&&(currentOperation.cancellable?(currentOperation.cancelRequested=!0,Logger.log("🛑 Cancellation requested — finishing current batch…")):Logger.log("⚠️ Rollback in progress — cannot cancel"));break;case"export":await withOperation("export",function(){return exportVariables(e.collections,e.styleOptions,e.preserveLibraryRefs,e.includeImages,e.namingConvention||"original",e.exportFormat||"figma",e.selectedModes,e.resolveAliases||!1,e.selectedGroups,e.selectedStyleGroups)});break;case"import":await withOperation("import",function(){return importVariables(e.data,e.options)});break;case"validate_import":try{const t=JSON.parse(e.data),o=e.plan,a=await validateImportAgainstPlan(t,o);Logger.send("validation_result",a)}catch(e){Logger.send("validation_result",{errors:[`Invalid JSON: ${e instanceof Error?e.message:"Parse error"}`],canImport:!1})}break;case"compute_import_diff":try{const t=JSON.parse(e.data),o=await computeImportDiff(t);Logger.send("import_diff_result",o)}catch(e){Logger.send("import_diff_result",{error:`Failed to compute diff: ${e instanceof Error?e.message:"Unknown error"}`})}break;case"detect_plan":const t=await detectCurrentPlan();Logger.send("plan_detected",t);break;case"clear_variables":await withOperation("clear",function(){return clearVariables(!1)});break;case"clear_styles":await withOperation("clear",function(){return clearStyles(!1)});break;case"clear_all":await withOperation("clear",function(){return clearAll(!1)});break;case"get_collections":await withOperation("scan",getCollections);break;case"check_libraries":try{const t=e.collections;await variableCache.rebuild();const o=[],a=[];for(const e of t)variableCache.isCollectionAvailable(e)?o.push(e):a.push(e);Logger.log(`📚 Library check: ${o.length} available, ${a.length} missing`),o.length>0&&Logger.log(` ✅ Available: ${o.join(", ")}`),a.length>0&&Logger.log(` ❌ Missing: ${a.join(", ")}`),Logger.send("library_check_result",{allAvailable:0===a.length,availableCollections:o,missingCollections:a,requiredCollections:t})}catch(t){Logger.send("library_check_result",{allAvailable:!1,availableCollections:[],missingCollections:e.collections||[],requiredCollections:e.collections||[],error:t instanceof Error?t.message:"Library check failed"})}break;case"check_fonts":try{const t=e.fonts,o=[],a=[],s=await runBatchedAsync(t,BATCH.ASYNC_FONT,function(e){return figma.loadFontAsync({family:e.family,style:e.style}).then(function(){return{font:e,available:!0}}).catch(function(){return{font:e,available:!1}})});for(let e=0;e0&&n>=r)&&l-oo&&(o=t.modes.length);return t=o>20?"enterprise":o>10?"organization":"professional",Object.assign({plan:t},PLAN_LIMITS[t])}async function validateImportAgainstPlan(e,t){const o=t?Object.assign({plan:t},PLAN_LIMITS[t]):await detectCurrentPlan(),a=await figma.variables.getLocalVariableCollectionsAsync(),s=a.reduce((e,t)=>Math.max(e,t.modes.length),0),n=(await figma.variables.getLocalVariablesAsync()).length,r=[];for(const t of e)"_styles"in t||r.push(t);let i=0,l=0;const c=[];for(const e of r){const t=Object.keys(e)[0],a=e[t];if(!a||!a.modes)continue;const s=Object.keys(a.modes).length;s>i&&(i=s),s>o.maxModesPerCollection&&c.push(`"${t}" (${s} modes, limit: ${o.maxModesPerCollection===1/0?"∞":o.maxModesPerCollection})`);const n=Object.values(a.modes)[0];n&&(l+=countNestedVariables(n))}const g=[],d=[];c.length;for(const e of r){const t=Object.keys(e)[0],o=e[t];if(!o||!o.modes)continue;const a=Object.values(o.modes)[0],s=a?countNestedVariables(a):0;s>5e3&&d.push(`Collection "${t}" has ${s} variables, exceeds limit of 5000`)}l>1e3&&g.push(`Large import: ${l} variables. This may take a moment.`),r.length>10&&g.push(`Importing ${r.length} collections. Consider importing in batches.`);const f=new Set;let u=0;for(const e of r){const t=e[Object.keys(e)[0]];if(t&&t.modes)for(const e of Object.keys(t.modes)){const o=flattenVariables(t.modes[e],"");for(const{value:e}of o)e.$libraryRef&&e.$collectionName&&(f.add(e.$collectionName),u++)}}const p=[];let m=0;for(const t of e)if("_styles"in t){const e=t._styles;if(e.textStyles)for(const t of e.textStyles){m++;const e=`${t.fontFamily}|${t.fontStyle}`;p.some(t=>`${t.family}|${t.style}`===e)||p.push({family:t.fontFamily,style:t.fontStyle})}}return Object.assign(Object.assign({currentPlan:o,existing:{collections:a.length,maxModesInAnyCollection:s,totalVariables:n},importing:{collections:r.length,maxModesInAnyCollection:i,totalVariables:l,collectionsExceedingModeLimit:c},warnings:g,errors:d,canImport:0===d.length},f.size>0&&{libraryDependencies:{variableCount:u,collections:Array.from(f)}}),p.length>0&&{fontDependencies:{styleCount:m,fonts:p}})}function countNestedVariables(e,t=0){for(const[,o]of Object.entries(e))o&&"object"==typeof o&&("$type"in o&&"$value"in o?t++:t=countNestedVariables(o,t));return t}const MathUtils={round2:e=>Math.round(100*e)/100,clamp:(e,t,o)=>Math.max(t,Math.min(o,e)),toHexByte:e=>Math.round(255*e).toString(16).padStart(2,"0"),fromHexByte:e=>parseInt(e,16)/255};function calculateHue(e,t,o,a,s){if(a===s)return 0;const n=a-s;let r=0;switch(a){case e:r=((t-o)/n+(t.5?e/(2-s-n):e/(s+n)}const l={h:calculateHue(t,o,a,s,n),s:Math.round(100*i),l:Math.round(100*r)},c=e.a;return void 0!==c&&c<1?Object.assign(Object.assign({},l),{a:MathUtils.round2(c)}):l},toHsb(e){const{r:t,g:o,b:a}=e,s=Math.max(t,o,a),n=Math.min(t,o,a),r=0===s?0:(s-n)/s,i={h:calculateHue(t,o,a,s,n),s:Math.round(100*r),b:Math.round(100*s)},l=e.a;return void 0!==l&&l<1?Object.assign(Object.assign({},i),{a:MathUtils.round2(l)}):i},toAllFormats(e){return{hex:this.toHex(e),rgb:this.toRgb255(e),css:this.toCss(e),hsl:this.toHsl(e),hsb:this.toHsb(e)}}},NamingConverter={convert(e,t){if("original"===t)return e;const o=e.replace(/([a-z])([A-Z])/g,"$1 $2").split(/[\s\/\-_]+/).filter(e=>e.length>0).map(e=>e.toLowerCase());if(0===o.length)return e;switch(t){case"camelCase":return o[0]+o.slice(1).map(e=>e.charAt(0).toUpperCase()+e.slice(1)).join("");case"kebab-case":return o.join("-");case"snake_case":return o.join("_");default:return e}},convertPath(e,t){return"original"===t?e:e.split("/").map(e=>this.convert(e,t)).join("/")},convertCollectionName(e,t){return this.convert(e,t)},convertModeName(e,t){return this.convert(e,t)},addOriginalName(e,t){if("original"===t)return{converted:e};const o=this.convert(e,t);return o===e?{converted:e}:{converted:o,original:e}}};async function resolveAliasValue(e,t,o=10){if(o<=0)return Logger.log(`⚠️ Max alias resolution depth reached for ${e.name}`),"";let a=e.valuesByMode[t];if(void 0===a){const t=Object.keys(e.valuesByMode);t.length>0&&(a=e.valuesByMode[t[0]])}if(void 0===a)return"";if(isVariableAlias(a)){const e=await figma.variables.getVariableByIdAsync(a.id);return e?resolveAliasValue(e,t,o-1):""}return a}const W3C_TYPE_MAP={color:"color",float:"number",string:"string",boolean:"boolean"},W3CConverter={colorToW3C:e=>e.hex,typeToW3C:e=>W3C_TYPE_MAP[e]||"string",valueToW3C(e,t=!1){const o={$value:"",$type:this.typeToW3C(e.$type)};return t&&"string"==typeof e.$value&&e.$value.startsWith("{")?o.$value=e.$value:"color"===e.$type&&"object"==typeof e.$value?o.$value=e.$value.hex:o.$value=e.$value,e.$description&&(o.$description=e.$description),e.$scopes&&e.$scopes.length>0&&!e.$scopes.includes("ALL_SCOPES")&&(o.$extensions={"com.figma":{scopes:e.$scopes}}),o},collectionToW3C(e,t,o,a){const s={};a&&a!==e&&(s.$description=`Figma collection: ${a}`);const n=Object.keys(t);if(1===n.length)this.addTokensToGroup(s,t[n[0]],o);else for(const e of n){const a=NamingConverter.convertModeName(e,o);s[a]={},this.addTokensToGroup(s[a],t[e],o)}return s},addTokensToGroup(e,t,o){for(const[a,s]of Object.entries(t)){const t=NamingConverter.convert(a,o);if(isExportVariableValue(s)){const o="string"==typeof s.$value&&s.$value.startsWith("{");e[t]=this.valueToW3C(s,o)}else e[t]={},this.addTokensToGroup(e[t],s,o)}},parseW3CToken(e){var t,o;const a=this.w3cTypeToFigma(e.$type),s=(null===(o=null===(t=e.$extensions)||void 0===t?void 0:t["com.figma"])||void 0===o?void 0:o.scopes)||["ALL_SCOPES"];let n;if("color"===a&&"string"==typeof e.$value){const t=ColorParser.parse(e.$value);n=ColorConverter.toAllFormats(t)}else n="string"==typeof e.$value||"number"==typeof e.$value||"boolean"==typeof e.$value?e.$value:JSON.stringify(e.$value);return e.$description?{$type:a,$value:n,$scopes:s,$description:e.$description}:{$type:a,$value:n,$scopes:s}},w3cTypeToFigma:e=>({color:"color",number:"float",dimension:"float",string:"string",boolean:"boolean",fontFamily:"string",fontWeight:"float",duration:"string",cubicBezier:"string"}[e]||"string"),isW3CFormat(e){if("object"!=typeof e||null===e)return!1;const t=e;for(const e of Object.keys(t)){const o=t[e];if("object"==typeof o&&null!==o){if("$value"in o&&"$type"in o)return!0;for(const e of Object.keys(o)){const t=o[e];if("object"==typeof t&&null!==t&&"$value"in t)return!0}}}return Array.isArray(e),!1},w3cToFigmaFormat(e){const t=[];for(const[o,a]of Object.entries(e)){if(o.startsWith("$"))continue;const e={[o]:{modes:{Default:this.w3cGroupToNestedVars(a)}}};t.push(e)}return t},w3cGroupToNestedVars(e){const t={};for(const[o,a]of Object.entries(e))o.startsWith("$")||(this.isW3CToken(a)?t[o]=this.parseW3CToken(a):"object"==typeof a&&null!==a&&(t[o]=this.w3cGroupToNestedVars(a)));return t},isW3CToken:e=>"object"==typeof e&&null!==e&&"$value"in e},TS_FLOAT_SCOPE_MAP={CORNER_RADIUS:"borderRadius",STROKE_FLOAT:"borderWidth",GAP:"spacing",WIDTH_HEIGHT:"sizing",OPACITY:"opacity",FONT_SIZE:"fontSizes",LINE_HEIGHT:"lineHeights",LETTER_SPACING:"letterSpacing",PARAGRAPH_SPACING:"paragraphSpacing"},TS_STRING_SCOPE_MAP={FONT_FAMILY:"fontFamilies",FONT_STYLE:"fontWeights"},TS_TEXT_CASE_MAP={ORIGINAL:"none",UPPER:"uppercase",LOWER:"lowercase",TITLE:"capitalize"},TS_TEXT_DECORATION_MAP={NONE:"none",UNDERLINE:"underline",STRIKETHROUGH:"line-through"},TokensStudioConverter={sanitizeSegment(e){let t=e.replace(/[{}$]/g,"");return 0===t.length&&(t="_"),"__proto__"!==t&&"constructor"!==t&&"prototype"!==t||(t="_"+t),t},sanitizeAliasRef(e){const t=e.substring(1,e.length-1).split("."),o=[];for(let e=0;e"string"==typeof e&&"{"===e.charAt(0)&&"}"===e.charAt(e.length-1),roundNumber:e=>Math.round(1e3*e)/1e3,colorToTS(e){const t=void 0!==e.rgb?e.rgb.a:void 0;return void 0!==t&&t<1?e.css:void 0!==e.rgb&&e.hex.length>7?e.hex.substring(0,7):e.hex},floatTypeFromScopes(e){if(!e)return"number";let t="",o=0;for(let a=0;ae.toLowerCase().replace(/\s+/g,"-")};function convertToTokensStudio(e){const t=TokensStudioConverter,o={},a=[],s={libraryRefsSkipped:0,imagePaintsSkipped:0,blurEffectsSkipped:0},n=[];let r=null;for(let t=0;t0){const e={};for(let o=0;o0){const t=styleSetKey("styles/color");o[t]=e,a.push(t),i.push(t)}}if(void 0!==r.textStyles&&r.textStyles.length>0){const e={};for(let o=0;o0){const t=styleSetKey("styles/typography");o[t]=e,a.push(t),i.push(t)}}if(void 0!==r.effectStyles&&r.effectStyles.length>0){const e={};for(let o=0;o0){const t=styleSetKey("styles/effects");o[t]=e,a.push(t),i.push(t)}}void 0!==r.gridStyles&&r.gridStyles.length>0&&Logger.log("Tokens Studio export: grid styles skipped (no Tokens Studio representation)")}s.libraryRefsSkipped>0&&Logger.log("Tokens Studio export: skipped "+s.libraryRefsSkipped+" library-alias token(s) with no resolvable local value"),s.imagePaintsSkipped>0&&Logger.log("Tokens Studio export: skipped "+s.imagePaintsSkipped+" image paint(s) in color styles (no Tokens Studio representation)"),s.blurEffectsSkipped>0&&Logger.log("Tokens Studio export: skipped "+s.blurEffectsSkipped+" blur effect(s) in effect styles (boxShadow tokens carry shadows only)");const l=[];for(let e=0;e{const a=o<0?o+1:o>1?o-1:o;return a<1/6?e+6*(t-e)*a:a<.5?t:a<2/3?e+(t-e)*(2/3-a)*6:e},r=n<.5?n*(1+s):n+s-n*s,i=2*n-r;return{r:hue2rgb(i,r,a+1/3),g:hue2rgb(i,r,a),b:hue2rgb(i,r,a-1/3),a:null!==(o=e.a)&&void 0!==o?o:1}},fromHsb(e){var t;const o=e.h/360,a=e.s/100,s=e.b/100,n=Math.floor(6*o),r=6*o-n,i=s*(1-a),l=s*(1-r*a),c=s*(1-(1-r)*a),g=[[s,c,i],[l,s,i],[i,s,c],[i,l,s],[c,i,s],[s,i,l]],[d,f,u]=g[n%6];return{r:d,g:f,b:u,a:null!==(t=e.a)&&void 0!==t?t:1}},parse(e){var t;if("object"==typeof e&&null!==e&&"hex"in e&&"rgb"in e)return this.fromHex(e.hex);if("object"==typeof e&&null!==e&&"r"in e&&"g"in e&&"b"in e){const o=e;return o.r<=1&&o.g<=1&&o.b<=1?{r:o.r,g:o.g,b:o.b,a:null!==(t=o.a)&&void 0!==t?t:1}:this.fromRgb255(o)}return"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"l"in e?this.fromHsl(e):"object"==typeof e&&null!==e&&"h"in e&&"s"in e&&"b"in e?this.fromHsb(e):"string"==typeof e?e.startsWith("rgb")||e.startsWith("hsl")?this.fromCss(e):this.fromHex(e):{r:0,g:0,b:0,a:1}}};class VariableCache{constructor(){this.collectionMap=new Map,this.variableMap=new Map,this.libraryVariableMap=new Map,this.libraryCollectionNames=new Set,this.initialized=!1,this.libraryIndexed=!1}async ensureReady(){this.initialized||(await this.rebuildLocal(),this.initialized=!0)}async initialize(){await this.ensureReady()}async rebuild(){await this.rebuildLocal(),await this.ensureLibraryIndex(),this.initialized=!0}async rebuildLocal(){this.clearLocal();const e=await figma.variables.getLocalVariableCollectionsAsync();for(let t=0;t{try{const o=await figma.variables.importVariableByKeyAsync(e.key);o&&this.libraryVariableMap.set(`${t}/${o.name}`,o)}catch(e){}})}catch(e){Logger.log(` ⚠️ Could not index library collection "${o.name}": ${e}`)}}this.libraryCollectionNames.size>0&&Logger.log(`📚 Indexed ${this.libraryVariableMap.size} library variables from ${this.libraryCollectionNames.size} connected libraries`)}catch(e){Logger.log(`⚠️ Could not access team library: ${e}`)}}getCollection(e){return this.collectionMap.get(e)}getVariable(e){return this.variableMap.get(e)||this.libraryVariableMap.get(e)}setVariable(e,t){this.variableMap.set(e,t)}setCollection(e,t){this.collectionMap.set(e,t)}removeCollection(e){this.collectionMap.delete(e);const t=[];for(const o of this.variableMap.keys())o.startsWith(`${e}/`)&&t.push(o);for(const e of t)this.variableMap.delete(e)}isCollectionAvailable(e){return this.collectionMap.has(e)||this.libraryCollectionNames.has(e)}getLibraryCollectionNames(){return Array.from(this.libraryCollectionNames)}get size(){return this.variableMap.size}get collections(){return this.collectionMap.values()}getVariableKeys(){return Array.from(this.variableMap.keys())}}const variableCache=new VariableCache;function isExportVariableValue(e){return"object"==typeof e&&null!==e&&"$type"in e}function isVariableAlias(e){return"object"==typeof e&&null!==e&&"VARIABLE_ALIAS"===e.type}const TypeMapper={toExportType(e){var t;return null!==(t={COLOR:"color",FLOAT:"float",STRING:"string",BOOLEAN:"boolean"}[e])&&void 0!==t?t:"string"},toFigmaType(e){var t;return null!==(t={color:"COLOR",float:"FLOAT",string:"STRING",boolean:"BOOLEAN"}[e])&&void 0!==t?t:"STRING"},scopesToArray:e=>0===e.length||e.includes("ALL_SCOPES")?["ALL_SCOPES"]:[...e],arrayToScopes:e=>e.includes("ALL_SCOPES")?["ALL_SCOPES"]:e};async function getVariableBindingInfo(e,t){if(!(null==e?void 0:e[t]))return{};const o=e[t];if(!o)return{};const a=await figma.variables.getVariableByIdAsync(o.id);if(!a)return{id:o.id};const s=await figma.variables.getVariableCollectionByIdAsync(a.variableCollectionId);return{id:o.id,name:a.name,collection:null==s?void 0:s.name}}async function extractBindings(e,t){if(!e)return;const o={};for(const a of t){const t=await getVariableBindingInfo(e,a);t.name&&(o[a]=t)}return Object.keys(o).length>0?o:void 0}function flattenVariables(e,t){const o=[];for(const a of Object.keys(e)){const s=e[a],n=t?`${t}/${a}`:a;isExportVariableValue(s)?o.push({path:n,value:s}):o.push(...flattenVariables(s,n))}return o}function getValueAtPath(e,t){const o=t.split("/");let a=e;for(const e of o){if("object"!=typeof a||null===a)return null;if(isExportVariableValue(a))return null;a=a[e]}return isExportVariableValue(a)?a:null}const ColorStyleProcessor={async export(e){var t;const o=null!==(t=null==e?void 0:e.includeImages)&&void 0!==t&&t,a=[],s=await figma.getLocalPaintStylesAsync();return await runSequentialAsync(s,20,async function(e){var t,s,n;if(0===e.paints.length)return;const r=[];let i,l,c;for(const a of e.paints)if("SOLID"===a.type){const e=a.color;let o=null!==(t=a.opacity)&&void 0!==t?t:1;void 0!==e.a&&e.a<1&&1===o&&(o=e.a);const s={r:a.color.r,g:a.color.g,b:a.color.b,a:o},n={type:"SOLID",color:ColorConverter.toAllFormats(s),opacity:MathUtils.round2(o)};r.push(n),i||(i=n.color,l=n.opacity,c=await extractBindings(a.boundVariables,["color"]))}else if("GRADIENT_LINEAR"===a.type||"GRADIENT_RADIAL"===a.type||"GRADIENT_ANGULAR"===a.type||"GRADIENT_DIAMOND"===a.type){const e=a.gradientStops.map(e=>{var t;return{position:MathUtils.round2(e.position),color:ColorConverter.toAllFormats({r:e.color.r,g:e.color.g,b:e.color.b,a:null!==(t=e.color.a)&&void 0!==t?t:1})}}),t=Object.assign(Object.assign({type:a.type,gradientStops:e},a.gradientTransform&&{gradientTransform:a.gradientTransform}),{opacity:MathUtils.round2(null!==(s=a.opacity)&&void 0!==s?s:1)});r.push(t)}else if("IMAGE"===a.type){const t=Object.assign(Object.assign(Object.assign(Object.assign({type:"IMAGE",scaleMode:a.scaleMode},a.imageHash&&{imageHash:a.imageHash}),{opacity:MathUtils.round2(null!==(n=a.opacity)&&void 0!==n?n:1)}),void 0!==a.rotation&&{rotation:a.rotation}),a.filters&&{filters:Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},void 0!==a.filters.exposure&&{exposure:a.filters.exposure}),void 0!==a.filters.contrast&&{contrast:a.filters.contrast}),void 0!==a.filters.saturation&&{saturation:a.filters.saturation}),void 0!==a.filters.temperature&&{temperature:a.filters.temperature}),void 0!==a.filters.tint&&{tint:a.filters.tint}),void 0!==a.filters.highlights&&{highlights:a.filters.highlights}),void 0!==a.filters.shadows&&{shadows:a.filters.shadows})});if(o&&a.imageHash)try{const e=figma.getImageByHash(a.imageHash);if(e){const o=await e.getBytesAsync();if(o){const e=figma.base64Encode(o);t.imageBase64=e}}}catch(t){Logger.log(`⚠️ Could not export image data for style "${e.name}": ${t}`)}r.push(t)}if(0===r.length)return;const g=Object.assign(Object.assign(Object.assign(Object.assign({name:e.name,paints:r},i&&{color:i}),void 0!==l&&{opacity:l}),e.description&&{description:e.description}),c&&Object.keys(c).length>0&&{boundVariables:c});a.push(g)}),a},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalPaintStylesAsync())s.set(e.name,e);return await runSequentialAsync(e,20,async function(e){var n,r,i,l;let c;s.has(e.name)?(c=s.get(e.name),a++):(c=figma.createPaintStyle(),c.name=e.name,o++),e.description&&(c.description=e.description);const g=[];if(e.paints&&e.paints.length>0){for(const o of e.paints)if("SOLID"===o.type){const a=ColorParser.parse(o.color);let s=null!==(n=o.opacity)&&void 0!==n?n:1;a.a<1&&void 0===o.opacity&&(s=MathUtils.round2(a.a));let r={type:"SOLID",color:{r:a.r,g:a.g,b:a.b},opacity:MathUtils.round2(s)};if(e.boundVariables&&0===g.length)for(const[o,a]of Object.entries(e.boundVariables))if(a.name&&a.collection){const e=t.getVariable(`${a.collection}/${a.name}`);if(e)try{r=figma.variables.setBoundVariableForPaint(r,o,e)}catch(e){Logger.log(`⚠️ Could not bind ${o}: ${e}`)}}g.push(r)}else if("GRADIENT_LINEAR"===o.type||"GRADIENT_RADIAL"===o.type||"GRADIENT_ANGULAR"===o.type||"GRADIENT_DIAMOND"===o.type){const e=o.gradientStops.map(e=>{const t=ColorParser.parse(e.color);return{position:e.position,color:{r:t.r,g:t.g,b:t.b,a:t.a}}}),t=o.gradientTransform?[[o.gradientTransform[0][0],o.gradientTransform[0][1],o.gradientTransform[0][2]],[o.gradientTransform[1][0],o.gradientTransform[1][1],o.gradientTransform[1][2]]]:[[1,0,0],[0,1,0]],a={type:o.type,gradientStops:e,gradientTransform:t,opacity:null!==(r=o.opacity)&&void 0!==r?r:1};g.push(a)}else if("IMAGE"===o.type){let t=null;if(o.imageBase64)try{const a=figma.base64Decode(o.imageBase64);t=figma.createImage(a).hash,Logger.log(`✅ Created image from base64 data for style "${e.name}"`)}catch(t){Logger.log(`⚠️ Could not import image from base64 for style "${e.name}": ${t}`)}if(!t&&o.imageHash){figma.getImageByHash(o.imageHash)?(t=o.imageHash,Logger.log(`✅ Found existing image with hash for style "${e.name}"`)):Logger.log(`⚠️ Image hash not found in file for style "${e.name}", skipping image paint (imageHash cannot be null)`)}if(t){const e=Object.assign(Object.assign({type:"IMAGE",scaleMode:o.scaleMode,imageHash:t,opacity:null!==(i=o.opacity)&&void 0!==i?i:1},void 0!==o.rotation&&{rotation:o.rotation}),o.filters&&{filters:o.filters});g.push(e)}}}else if(e.color){const o=ColorParser.parse(e.color);let a=null!==(l=e.opacity)&&void 0!==l?l:1;o.a<1&&void 0===e.opacity&&(a=MathUtils.round2(o.a));let s={type:"SOLID",color:{r:o.r,g:o.g,b:o.b},opacity:MathUtils.round2(a)};if(e.boundVariables)for(const[o,a]of Object.entries(e.boundVariables))if(a.name&&a.collection){const e=t.getVariable(`${a.collection}/${a.name}`);if(e)try{s=figma.variables.setBoundVariableForPaint(s,o,e)}catch(e){Logger.log(`⚠️ Could not bind ${o}: ${e}`)}}g.push(s)}g.length>0&&(c.paints=g)}),{created:o,updated:a}}},TextStyleProcessor={async export(e){const t=[],o=await figma.getLocalTextStylesAsync();return await runSequentialAsync(o,20,async function(e){const o=Object.assign(Object.assign({name:e.name,fontFamily:e.fontName.family,fontStyle:e.fontName.style,fontSize:e.fontSize,lineHeight:e.lineHeight,letterSpacing:e.letterSpacing,textCase:e.textCase,textDecoration:e.textDecoration},e.description&&{description:e.description}),{boundVariables:await extractBindings(e.boundVariables,["fontSize","lineHeight","letterSpacing","paragraphSpacing","paragraphIndent"])});t.push(o)}),t},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalTextStylesAsync())s.set(e.name,e);return await runSequentialAsync(e,20,async function(e){let n;s.has(e.name)?(n=s.get(e.name),a++):(n=figma.createTextStyle(),n.name=e.name,o++),e.description&&(n.description=e.description);try{if(await figma.loadFontAsync({family:e.fontFamily,style:e.fontStyle}),n.fontName={family:e.fontFamily,style:e.fontStyle},n.fontSize=e.fontSize,n.lineHeight=e.lineHeight,n.letterSpacing=e.letterSpacing,e.textCase&&(n.textCase=e.textCase),e.textDecoration&&(n.textDecoration=e.textDecoration),e.boundVariables)for(const[o,a]of Object.entries(e.boundVariables))if(a.name&&a.collection){const e=t.getVariable(`${a.collection}/${a.name}`);if(e)try{n.setBoundVariable(o,e)}catch(e){}}}catch(t){Logger.log(`⚠️ Could not load font for ${e.name}: ${t}`)}}),{created:o,updated:a}}},EffectStyleProcessor={async export(e){const t=[],o=await figma.getLocalEffectStylesAsync();return await runSequentialAsync(o,20,async function(e){const o=[];for(const t of e.effects){const e=Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:t.type,visible:t.visible},"radius"in t&&{radius:t.radius}),"spread"in t&&{spread:t.spread}),"offset"in t&&{offset:t.offset}),"color"in t&&{color:ColorConverter.toAllFormats(t.color)}),"blendMode"in t&&{blendMode:t.blendMode}),"showShadowBehindNode"in t&&{showShadowBehindNode:t.showShadowBehindNode}),{boundVariables:await extractBindings(t.boundVariables,["color","radius","spread","offsetX","offsetY"])});o.push(e)}const a=Object.assign(Object.assign({name:e.name},e.description&&{description:e.description}),{effects:o});t.push(a)}),t},async importStyles(e,t){let o=0,a=0;const s=new Map;for(const e of await figma.getLocalEffectStylesAsync())s.set(e.name,e);return await runSequentialAsync(e,20,async function(e){let n;s.has(e.name)?(n=s.get(e.name),a++):(n=figma.createEffectStyle(),n.name=e.name,o++),e.description&&(n.description=e.description);const r=e.effects.map(e=>{var t;return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({type:e.type,visible:null===(t=e.visible)||void 0===t||t},void 0!==e.radius&&{radius:e.radius}),void 0!==e.spread&&{spread:e.spread}),void 0!==e.offset&&{offset:e.offset}),void 0!==e.color&&{color:(()=>{const t=ColorParser.parse(e.color);return{r:t.r,g:t.g,b:t.b,a:MathUtils.round2(t.a)}})()}),void 0!==e.blendMode&&{blendMode:e.blendMode}),void 0!==e.showShadowBehindNode&&{showShadowBehindNode:e.showShadowBehindNode})});n.effects=r;for(let o=0;o{var t,o,a,s,n,r,i;const l=e.color?ColorParser.parse(e.color):{r:1,g:0,b:0,a:.1},c={r:l.r,g:l.g,b:l.b,a:MathUtils.round2(l.a)};if("GRID"===e.pattern)return{pattern:"GRID",sectionSize:null!==(t=e.sectionSize)&&void 0!==t?t:10,visible:!1!==e.visible,color:c};const g=null!==(o=e.alignment)&&void 0!==o?o:"STRETCH",d={pattern:e.pattern,gutterSize:null!==(a=e.gutterSize)&&void 0!==a?a:10,count:null!==(s=e.count)&&void 0!==s?s:5,visible:!1!==e.visible,color:c};if("STRETCH"===g)return Object.assign(Object.assign({},d),{alignment:"STRETCH",offset:null!==(n=e.offset)&&void 0!==n?n:0});if("CENTER"===g)return Object.assign(Object.assign({},d),{alignment:"CENTER",sectionSize:null!==(r=e.sectionSize)&&void 0!==r?r:100});{const t=Object.assign(Object.assign({},d),{alignment:g,offset:null!==(i=e.offset)&&void 0!==i?i:0});return void 0!==e.sectionSize&&(t.sectionSize=e.sectionSize),t}});n.layoutGrids=r;for(let o=0;ot.name===e);a?await checkVariablesDiff(i,a.modeId,o,r,"",t):l=!0}t.modifiedVariables.some(e=>e.collection===r)||t.newVariables.some(e=>e.collection===r)?(t.modifiedCollections.push(r),t.summary.collectionsModified++):(t.unchangedCollections.push(r),t.summary.collectionsUnchanged++)}return t}function countVariablesInCollection(e){let t=0;const o=Object.values(e)[0];return o&&(t=countVarsInNestedObj(o)),t}function countVarsInNestedObj(e){let t=0;for(const o of Object.values(e))isExportVariableValue(o)?t++:t+=countVarsInNestedObj(o);return t}async function checkVariablesDiff(e,t,o,a,s,n){for(const[r,i]of Object.entries(o)){const o=s?`${s}/${r}`:r;if(isExportVariableValue(i)){const e=variableCache.getVariable(`${a}/${o}`);if(e){const s=e.valuesByMode[t],r=i.$value;valuesAreDifferent(s,r)?(n.modifiedVariables.push({collection:a,path:o,oldValue:formatValueForDisplay(s),newValue:formatValueForDisplay(r)}),n.summary.variablesModified++):(n.unchangedVariables++,n.summary.variablesUnchanged++)}else n.newVariables.push({collection:a,path:o}),n.summary.variablesNew++}else await checkVariablesDiff(e,t,i,a,o,n)}}function valuesAreDifferent(e,t){if(void 0===e)return!0;if(isVariableAlias(e))return"string"==typeof t&&t.startsWith("{"),!0;if("object"==typeof e&&null!==e&&"r"in e){if("object"==typeof t&&null!==t&&"hex"in t){return ColorConverter.toAllFormats(e).hex.toLowerCase()!==t.hex.toLowerCase()}return!0}return e!==t}function formatValueForDisplay(e){if(void 0===e)return"undefined";if("object"==typeof e&&null!==e){if("hex"in e)return e.hex;if("r"in e)return ColorConverter.toAllFormats(e).hex;if("id"in e)return"{alias}"}return String(e)}async function computeStylesDiff(e,t){if(e.colorStyles){const o=await figma.getLocalPaintStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.colorStyles)a.has(o.name)?(t.modifiedStyles.push({type:"color",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"color",name:o.name}),t.summary.stylesNew++)}if(e.textStyles){const o=await figma.getLocalTextStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.textStyles)a.has(o.name)?(t.modifiedStyles.push({type:"text",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"text",name:o.name}),t.summary.stylesNew++)}if(e.effectStyles){const o=await figma.getLocalEffectStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.effectStyles)a.has(o.name)?(t.modifiedStyles.push({type:"effect",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"effect",name:o.name}),t.summary.stylesNew++)}if(e.gridStyles){const o=await figma.getLocalGridStylesAsync(),a=new Set(o.map(e=>e.name));for(const o of e.gridStyles)a.has(o.name)?(t.modifiedStyles.push({type:"grid",name:o.name}),t.summary.stylesModified++):(t.newStyles.push({type:"grid",name:o.name}),t.summary.stylesNew++)}}function filterStylesByGroup(e,t){return t?e.filter(function(e){return-1!==t.indexOf(getGroupKey(e.name))}):e}async function exportVariables(e,t,o,a,s="original",n="figma",r,i=!1,l,c){var g,d,f,u,p,m,y,h;Logger.log("📤 Starting export..."),Logger.log(` preserveLibraryRefs: ${o}`),Logger.log(` includeImages: ${a}`),Logger.log(` namingConvention: ${s}`),Logger.log(` exportFormat: ${n}`),Logger.log(` resolveAliases: ${i}`),r&&Logger.log(` selectedModes: ${JSON.stringify(r)}`);try{let o=await figma.variables.getLocalVariableCollectionsAsync();(null==e?void 0:e.length)&&(o=o.filter(t=>e.includes(t.name)),Logger.log(`Filtering to ${o.length} selected collections`));const b=[],v={};let S=0;const C=createProgress("export");let A=0;for(let e=0;eo.includes(e.name)),Logger.log(` Filtering to ${t.length} modes: ${t.map(e=>e.name).join(", ")}`)}const o=NamingConverter.convertCollectionName(e.name,s),a={[o]:Object.assign({modes:{}},o!==e.name&&{$originalName:e.name})};for(const e of t){const t=NamingConverter.convertModeName(e.name,s);a[o].modes[t]={}}await runSequentialAsync(e.variableIds,BATCH.SEQ_EXPORT,async function(n){var r,c;L++;const g=await figma.variables.getVariableByIdAsync(n);if(!g)return;if(l&&l[e.name]&&-1===l[e.name].indexOf(getGroupKey(g.name)))return;S++;const d=g.name.split("/").map(e=>NamingConverter.convert(e,s));for(const e of t){const t=NamingConverter.convertModeName(e.name,s),n=a[o].modes[t],l=g.valuesByMode[e.modeId];let f=n;for(let e=0;eNamingConverter.convert(e,s)).join(".")}}`,p=v,b){const e=t.valuesByMode[Object.keys(t.valuesByMode)[0]];"object"==typeof e&&null!==e&&"r"in e?m=ColorConverter.toAllFormats(e):isVariableAlias(e)||(m=e)}}}else p=""}else p="object"==typeof l&&null!==l&&"r"in l?ColorConverter.toAllFormats(l):l;const S=Object.assign(Object.assign(Object.assign({$scopes:TypeMapper.scopesToArray(g.scopes),$type:TypeMapper.toExportType(g.resolvedType),$value:p},g.description&&{$description:g.description}),y&&h&&{$collectionName:h}),y&&b&&Object.assign({$libraryRef:v},void 0!==m&&{$localValue:m}));f[u]=S}},function(){C.report("export_variables","Exporting variables",L,A)}),b.push(a),"w3c"===n&&(v[o]=W3CConverter.collectionToW3C(o,a[o].modes,s,a[o].$originalName))}let $=null;if(t){if($={},t.colorStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.color:void 0;$.colorStyles=filterStylesByGroup(await ColorStyleProcessor.export({includeImages:a}),e),e&&0===$.colorStyles.length&&delete $.colorStyles}if(t.textStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.text:void 0;$.textStyles=filterStylesByGroup(await TextStyleProcessor.export(),e),e&&0===$.textStyles.length&&delete $.textStyles}if(t.effectStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.effect:void 0;$.effectStyles=filterStylesByGroup(await EffectStyleProcessor.export(),e),e&&0===$.effectStyles.length&&delete $.effectStyles}if(t.gridStyles){C.report("export_styles","Exporting styles",0,0,!0);const e=c?c.grid:void 0;$.gridStyles=filterStylesByGroup(await GridStyleProcessor.export(),e),e&&0===$.gridStyles.length&&delete $.gridStyles}Object.keys($).length>0?b.push({_styles:$}):$=null}const w={collections:o.length,variables:S,styles:$?{color:null!==(d=null===(g=$.colorStyles)||void 0===g?void 0:g.length)&&void 0!==d?d:0,text:null!==(u=null===(f=$.textStyles)||void 0===f?void 0:f.length)&&void 0!==u?u:0,effect:null!==(m=null===(p=$.effectStyles)||void 0===p?void 0:p.length)&&void 0!==m?m:0,grid:null!==(h=null===(y=$.gridStyles)||void 0===y?void 0:y.length)&&void 0!==h?h:0}:null};let O;"w3c"===n?($&&Object.keys($).length>0&&(v.$extensions={"com.figma":{styles:$}}),O=JSON.stringify(v,null,2),Logger.log(`✅ Export complete (W3C format): ${w.collections} collections, ${w.variables} variables`)):"tokens-studio"===n?(O=JSON.stringify(convertToTokensStudio(b),null,2),Logger.log(`✅ Export complete (Tokens Studio format): ${w.collections} collections, ${w.variables} variables`)):(O=JSON.stringify(b,null,2),Logger.log(`✅ Export complete: ${w.collections} collections, ${w.variables} variables`)),await sendExportInChunks(O,w,n)}catch(e){if(isCancelError(e))return Logger.log("🛑 Export cancelled"),void figma.ui.postMessage({type:"operation_cancelled",operation:"export",phase:"export",rolledBack:!1,message:"Export cancelled. Nothing was changed."});Logger.log(`❌ Export error: ${e}`),Logger.send("error",{message:`Export failed: ${e}`})}}async function sendExportInChunks(e,t,o){const a=e.length,s=BATCH.EXPORT_CHUNK_BYTES,n=Math.max(1,Math.ceil(a/s));let r=0,i=0;for(;i=55296&&o<=56319&&(t+=1)}const o=e.slice(i,t);figma.ui.postMessage({type:"export_chunk",seq:r,total:n,data:o}),r++,i=t,r%BATCH.EXPORT_YIELD_EVERY===0&&i0?a.modes[s[0]]:{},"");b.push({collectionObj:t,flatPaths:n}),v+=n.length;for(let e=0;e0&&Logger.log(` ✅ Aliases: ${L} resolved, ${$} used fallback values`),y&&t.importStyles){if(Logger.log("📦 Importing styles..."),n.report("import_styles","Importing styles",0,0,!0),y.colorStyles){const e=await ColorStyleProcessor.importStyles(y.colorStyles,variableCache);p+=e.created,m+=e.updated}if(y.textStyles){const e=await TextStyleProcessor.importStyles(y.textStyles,variableCache);p+=e.created,m+=e.updated}if(y.effectStyles){const e=await EffectStyleProcessor.importStyles(y.effectStyles,variableCache);p+=e.created,m+=e.updated}if(y.gridStyles){const e=await GridStyleProcessor.importStyles(y.gridStyles,variableCache);p+=e.created,m+=e.updated}}const w={collectionsCreated:g,variablesCreated:d,variablesUpdated:f,variablesSkipped:u,stylesCreated:p,stylesUpdated:m};figma.commitUndo(),Logger.log("✅ Import complete!"),Logger.send("import_complete",{stats:w,snapshot:s})}catch(e){const t=isCancelError(e);if(t&&!r)return Logger.log("🛑 Import cancelled before any changes were made"),void figma.ui.postMessage({type:"operation_cancelled",operation:"import",phase:"snapshot",rolledBack:!1,message:"Import cancelled. No changes were made."});const o=e instanceof Error?e.message:String(e);if(t?Logger.log("🛑 Import cancelled after mutation started — rolling back..."):Logger.log(`❌ Import error: ${o}`),s){Logger.log("🔄 Attempting automatic rollback to pre-import state..."),Logger.send("import_rolling_back",{error:o});try{await restoreFromSnapshot(s),Logger.log("✅ Automatic rollback successful - file restored to pre-import state"),t?figma.ui.postMessage({type:"operation_cancelled",operation:"import",phase:"rollback",rolledBack:!0,message:"Import cancelled — your file was restored to its previous state."}):Logger.send("import_rollback_complete",{error:o,message:"Import failed but your file has been automatically restored to its previous state."})}catch(e){const t=e instanceof Error?e.message:String(e);Logger.log(`❌ Rollback failed: ${t}`),Logger.send("import_rollback_failed",{error:o,rollbackError:t,message:"Import failed and automatic rollback also failed. Please use Ctrl+Z (Cmd+Z) to undo manually."})}}else Logger.send("error",{message:`Import failed: ${o}. Use Ctrl+Z (Cmd+Z) to undo changes.`})}}function setRawValue(e,t,o){try{if("color"===o.$type){const a=ColorParser.parse(o.$value),s=a.a<1?Object.assign(Object.assign({},a),{a:MathUtils.round2(a.a)}):a;e.setValueForMode(t,s)}else e.setValueForMode(t,o.$value)}catch(e){console.error(`Could not set value: ${e}`)}}function getGroupKey(e){const t=e.indexOf("/");return-1===t?"":e.substring(0,t)}function summarizeGroups(e){const t=Object.create(null),o=[];for(let a=0;a{Logger.log(` ${t+1}. "${e.name}" (id: ${e.id})`)});const t=new Set;let o=0,a=0,s=0;const n=new Map,r=[];for(let i=0;ie.name),variableCount:l.variableIds.length,types:c,groups:summarizeGroups(g)})}r.sort((e,t)=>e.name.localeCompare(t.name));const i=await figma.getLocalPaintStylesAsync(),l=await figma.getLocalTextStylesAsync(),c=await figma.getLocalEffectStylesAsync(),g=await figma.getLocalGridStylesAsync();let d=0;const f=[];for(let e=0;e({family:e,styles:Array.from(t)}));let S=0;for(const e of i)e.boundVariables&&Object.keys(e.boundVariables).length>0&&S++;Logger.send("collections",{collections:r,styles:u,styleGroups:h,libraryDependencies:Array.from(t),fontsUsed:v,stats:{totalVariables:r.reduce((e,t)=>e+t.variableCount,0),totalAliases:o,localAliases:a,libraryAliases:s,styleBindings:S}})}async function clearVariables(e=!1){Logger.log("🗑️ Clearing all variables...");const t=e?null:createProgress("clear");let o=0,a=0,s=!1;try{const n=await figma.variables.getLocalVariableCollectionsAsync(),r=[];let i=0;for(let e=0;e0&&(figma.commitUndo(),s=!0);let l=0;for(let e=0;e0&&(figma.commitUndo(),a=!0);let c=0;const removeStyle=function(e){e.remove(),o++},onBatch=function(e){t&&t.report("clear","Deleting styles",c+e,l)};await runBatched(s,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=s.length,await runBatched(n,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=n.length,await runBatched(r,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=r.length,await runBatched(i,BATCH.SYNC_LIGHT,removeStyle,onBatch),c+=i.length,!e&&a&&figma.commitUndo(),Logger.log(`✅ Cleared ${o} styles`),e||Logger.send("clear_complete",{message:`${o} styles`})}catch(t){if(isCancelError(t)){if(e)throw t;return Logger.log(`🛑 Clear styles cancelled after ${o} styles`),void figma.ui.postMessage({type:"operation_cancelled",operation:"clear",phase:"clear",rolledBack:!1,partial:{collectionsDeleted:0,variablesDeleted:o},message:`Clear cancelled — ${o} styles were already deleted. Remaining items were not touched. Use Cmd+Z to restore deleted items.`})}if(Logger.log(`❌ Clear styles error: ${t}`),e)throw t;Logger.send("error",{message:`Failed to clear styles: ${t}`})}}async function clearAll(e=!1){if(Logger.log("🗑️ Clearing everything..."),e)return await clearVariables(!0),void await clearStyles(!0);let t=!1;try{figma.commitUndo(),t=!0,await clearVariables(!0),await clearStyles(!0),figma.commitUndo(),Logger.send("clear_complete",{message:"all variables and styles"})}catch(e){if(t&&figma.commitUndo(),isCancelError(e))return Logger.log("🛑 Clear all cancelled"),void figma.ui.postMessage({type:"operation_cancelled",operation:"clear",phase:"clear",rolledBack:!1,partial:{collectionsDeleted:0,variablesDeleted:0},message:"Clear cancelled — some items may already have been deleted. Use Cmd+Z to restore deleted items."});Logger.log(`❌ Clear all error: ${e}`),Logger.send("error",{message:`Failed to clear: ${e}`})}}async function createUndoSnapshot(e){var t;Logger.log("📸 Creating snapshot of current file state...");const o=await figma.variables.getLocalVariableCollectionsAsync(),a=[],s=new Map;let n=0;for(let e=0;e({id:e.modeId,name:e.name})),variables:[]},i=await runBatchedAsync(t.variableIds,BATCH.ASYNC_LOOKUP,function(e){return figma.variables.getVariableByIdAsync(e)},function(t){e&&e.report("snapshot","Preparing snapshot (undo safety)",r+t,n)});r+=t.variableIds.length;for(let e=0;e0){t.renameMode(t.modes[0].modeId,e.modes[0].name);for(let o=1;o0&&(s.scopes=a.scopes);for(const t of e.modes){const r=o[t.name],i=a.values[t.name];if(i)if(i.isAlias&&i.aliasName)n.push({variable:s,modeId:r,aliasPath:i.aliasName,aliasCollection:i.aliasCollection||e.name});else if(void 0!==i.value){let e;e="COLOR"===a.type&&"string"==typeof i.value?ColorParser.parse(i.value):i.value,s.setValueForMode(r,e)}}}},function(e,o){t.report("undo_restore","Restoring variables",e,o)}),Logger.log(` Resolving ${n.length} aliases...`),await runBatched(n,BATCH.SYNC_LIGHT,function(e){const t=`${e.aliasCollection}/${e.aliasPath}`,o=variableCache.getVariable(t);o&&e.variable.setValueForMode(e.modeId,{type:"VARIABLE_ALIAS",id:o.id})},function(e,o){t.report("undo_aliases","Restoring aliases",e,o)}),Logger.log(" Restoring styles..."),t.report("undo_styles","Restoring styles",0,0,!0),a.colorStyles&&a.colorStyles.length>0&&await ColorStyleProcessor.importStyles(a.colorStyles,variableCache),a.textStyles&&a.textStyles.length>0&&await TextStyleProcessor.importStyles(a.textStyles,variableCache),a.effectStyles&&a.effectStyles.length>0&&await EffectStyleProcessor.importStyles(a.effectStyles,variableCache),a.gridStyles&&a.gridStyles.length>0&&await GridStyleProcessor.importStyles(a.gridStyles,variableCache),Logger.log("✅ File restored from snapshot")}figma.ui.onmessage=async e=>{switch(e.type){case"cancel_operation":null!==currentOperation.type&&(currentOperation.cancellable?(currentOperation.cancelRequested=!0,Logger.log("🛑 Cancellation requested — finishing current batch…")):Logger.log("⚠️ Rollback in progress — cannot cancel"));break;case"resize_ui":"advanced"===e.mode?figma.ui.resize(UI_SIZE.advanced.width,UI_SIZE.advanced.height):figma.ui.resize(UI_SIZE.simple.width,UI_SIZE.simple.height);break;case"export":await withOperation("export",function(){return exportVariables(e.collections,e.styleOptions,e.preserveLibraryRefs,e.includeImages,e.namingConvention||"original",e.exportFormat||"figma",e.selectedModes,e.resolveAliases||!1,e.selectedGroups,e.selectedStyleGroups)});break;case"import":await withOperation("import",function(){return importVariables(e.data,e.options)});break;case"validate_import":try{const t=JSON.parse(e.data),o=e.plan,a=await validateImportAgainstPlan(t,o);Logger.send("validation_result",a)}catch(e){Logger.send("validation_result",{errors:[`Invalid JSON: ${e instanceof Error?e.message:"Parse error"}`],canImport:!1})}break;case"compute_import_diff":try{const t=JSON.parse(e.data),o=await computeImportDiff(t);Logger.send("import_diff_result",o)}catch(e){Logger.send("import_diff_result",{error:`Failed to compute diff: ${e instanceof Error?e.message:"Unknown error"}`})}break;case"detect_plan":const t=await detectCurrentPlan();Logger.send("plan_detected",t);break;case"clear_variables":await withOperation("clear",function(){return clearVariables(!1)});break;case"clear_styles":await withOperation("clear",function(){return clearStyles(!1)});break;case"clear_all":await withOperation("clear",function(){return clearAll(!1)});break;case"get_collections":await withOperation("scan",getCollections);break;case"check_libraries":try{const t=e.collections;await variableCache.rebuild();const o=[],a=[];for(const e of t)variableCache.isCollectionAvailable(e)?o.push(e):a.push(e);Logger.log(`📚 Library check: ${o.length} available, ${a.length} missing`),o.length>0&&Logger.log(` ✅ Available: ${o.join(", ")}`),a.length>0&&Logger.log(` ❌ Missing: ${a.join(", ")}`),Logger.send("library_check_result",{allAvailable:0===a.length,availableCollections:o,missingCollections:a,requiredCollections:t})}catch(t){Logger.send("library_check_result",{allAvailable:!1,availableCollections:[],missingCollections:e.collections||[],requiredCollections:e.collections||[],error:t instanceof Error?t.message:"Library check failed"})}break;case"check_fonts":try{const t=e.fonts,o=[],a=[],s=await runBatchedAsync(t,BATCH.ASYNC_FONT,function(e){return figma.loadFontAsync({family:e.family,style:e.style}).then(function(){return{font:e,available:!0}}).catch(function(){return{font:e,available:!1}})});for(let e=0;e { } break; + case 'resize_ui': + // Window follows the UI mode. Only the two known sizes are accepted — + // never arbitrary dimensions from the iframe. + if (msg.mode === 'advanced') { + figma.ui.resize(UI_SIZE.advanced.width, UI_SIZE.advanced.height); + } else { + figma.ui.resize(UI_SIZE.simple.width, UI_SIZE.simple.height); + } + break; + case 'export': await withOperation('export', function (): Promise { return exportVariables( diff --git a/variables-styles-extractor/ui.html b/variables-styles-extractor/ui.html index 0ce5ca6..03cdcdb 100644 --- a/variables-styles-extractor/ui.html +++ b/variables-styles-extractor/ui.html @@ -3122,9 +3122,16 @@ (the legacy .column:nth-child width rules are position-dependent). ========================================================================== */ :root { - --simple-col1-width: 378px; - --simple-col2-width: 379px; - --simple-col3-width: 379px; + /* Sections match the Advanced column widths; the plugin window itself + shrinks to --simple-plugin-width in Simple mode (backend resize_ui). */ + --simple-col1-width: 279px; + --simple-col2-width: 280px; + --simple-col3-width: 280px; + --simple-plugin-width: 905px; /* 16 + 279 + 16 + 280 + 16 + 280 + 16 = 903 (+2px slack, mirroring Advanced) */ + } + + body:not(.advanced-mode) { + width: var(--simple-plugin-width); } .simple-layout { @@ -6553,6 +6560,8 @@ document.querySelectorAll('input[name="user-mode"]').forEach(radio => { radio.addEventListener('change', (e) => { currentUserMode = e.target.value; + // Window follows the mode: Simple is compact (905px), Advanced full (1200px) + parent.postMessage({ pluginMessage: { type: 'resize_ui', mode: currentUserMode } }, '*'); if (currentUserMode === 'advanced') { document.body.classList.add('advanced-mode'); // Show library refs option if deps detected From 4ce3d613f186d16f267e7611ca08970aff5547e0 Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:44:02 +0530 Subject: [PATCH 07/20] docs: START_HERE boot doc, refreshed protocol tables, v2.1.0 changelog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Context-harness phase (tracked half — untracked GCC state files were refreshed at the real .gcc paths per the workspace untracked policy): - New START_HERE.md: 60-second boot checklist, hard-constraint recap (QuickJS / CSS sandbox / networkAccess none), build + hot-reload steps, architecture one-pager with the current message protocol, heavy-load model, export formats, and danger zones — with a SYSTEM PAIRING header - AGENTS.md: START_HERE.md added to read order + structure tree; Communication-flow message table replaced with the current protocol (incl. resize_ui, chunked export, progress/cancel, operation lock) and a history note listing the removed 2026-06 messages - docs/CHANGELOG.md: [2.1.0] Unreleased entry (Simple-mode redesign + compact window, progress/cancel, safer undo, Tokens Studio export, perf + cleanup) - docs/KNOWN_ISSUES.md: stale version header fixed; issues resolved by this work marked resolved-in-2.1.0 - docs/TASKS.md: overhaul phases logged; open items = Figma Desktop manual test matrix, version bump, Community publish Co-Authored-By: Claude Fable 5 --- variables-styles-extractor/AGENTS.md | 39 ++++++-- variables-styles-extractor/START_HERE.md | 92 +++++++++++++++++++ variables-styles-extractor/docs/CHANGELOG.md | 47 +++++++++- .../docs/KNOWN_ISSUES.md | 4 +- variables-styles-extractor/docs/TASKS.md | 20 +++- 5 files changed, 191 insertions(+), 11 deletions(-) create mode 100644 variables-styles-extractor/START_HERE.md diff --git a/variables-styles-extractor/AGENTS.md b/variables-styles-extractor/AGENTS.md index a05281c..6311a0b 100644 --- a/variables-styles-extractor/AGENTS.md +++ b/variables-styles-extractor/AGENTS.md @@ -4,7 +4,7 @@ Updated by: manual, on architectural or convention changes Pairs with: CLAUDE.md (pointer), docs/AI_CONTEXT.md (legacy context, protected), docs/CODING_STANDARDS.md, docs/FIGMA_PLUGIN_DEVELOPMENT.md Update trigger: change to plugin architecture, Figma constraints discovered, message protocol added -Last verified: 2026-05-22 (promoted from docs/AGENTS.md to project root) +Last verified: 2026-06-10 (message protocol overhaul absorbed; START_HERE.md boot doc added) Index: docs/AI_CONTEXT.md === END PAIRING === --> @@ -24,6 +24,7 @@ Index: docs/AI_CONTEXT.md ## Quick start for AI agents ### Step 1: Read in this order +0. **[`START_HERE.md`](START_HERE.md)** — 60-second boot check (constraints recap, build commands, danger zones). 1. **This file** (`AGENTS.md`) — plugin architecture + constraints + conventions. 2. **`.gcc/session-memory.md`** — warm-start state from the last session. 3. **[`docs/CODING_STANDARDS.md`](docs/CODING_STANDARDS.md)** — mandatory coding rules for this plugin. @@ -81,6 +82,7 @@ treat any NEW violation as a regression and fix it inline. ``` variables-styles-extractor/ +├── START_HERE.md ← Boot check (read first — constraints recap, build commands, danger zones) ├── AGENTS.md ← This file (canonical AI rules) ├── CLAUDE.md ← Pointer to AGENTS.md (legacy Claude Code path) ├── README.md ← Public-facing @@ -142,19 +144,30 @@ ui.html ─────postMessage─────► code.ts (Figma VM) │ │ │◄────figma.ui.postMessage─────┘ -Export: UI sends 'export' → code.ts processes → 'export_complete' +Export: UI sends 'export' → code.ts processes → 'export_chunk' × N → 'export_done' Import: UI sends 'validate_import' → code.ts validates → 'validation_result' - UI sends 'import' → code.ts imports → 'import_complete' + UI sends 'import' → code.ts imports → 'import_complete' (carries undo snapshot) +Progress: long ops stream 'operation_progress' (throttled ≥250ms); UI may send 'cancel_operation' ``` | UI → Backend | Backend → UI | |--------------|--------------| -| `export` | `export_complete` | -| `import` | `import_complete` | +| `export` (with `selectedGroups` / `selectedStyleGroups` / `exportFormat` incl. `tokens-studio`) | `export_chunk` (256KB chunks) / `export_done` | +| `import` | `import_complete` (carries undo snapshot) / `import_rolling_back` / `import_rollback_complete` / `import_rollback_failed` | | `validate_import` | `validation_result` | +| `compute_import_diff` | `import_diff_result` | +| `detect_plan` | `plan_detected` | +| `clear_variables` / `clear_styles` / `clear_all` | `clear_complete` | +| `get_collections` | `collections` (with `groups` + `styleGroups`) | | `check_libraries` | `library_check_result` | | `check_fonts` | `font_check_result` | -| `get_collections` | `collections` | +| `undo_import` | `undo_complete` / `undo_error` | +| `cancel_operation` | `operation_cancelled` | +| `resize_ui` (`mode: 'simple' \| 'advanced'`) | — (window resizes: Simple 905×628, Advanced 1200×628) | +| — (any op while another runs) | `operation_denied` | +| — (anytime) | `log`, `operation_progress`, `error` | + +> **History:** the 2026-06 protocol overhaul REMOVED `export_complete`, `get_variables`/`variables`, `collection_details`, `close`, and `create_undo_snapshot`/`snapshot_created`/`snapshot_error` — do not reintroduce them. --- @@ -188,6 +201,18 @@ interface PlanValidation { } ``` +**Export options (2026-06):** the `export` message now carries `selectedGroups` and +`selectedStyleGroups` (name-prefix group filters — absent key = export all) and +`exportFormat: 'figma' | 'w3c' | 'tokens-studio'` (the Tokens Studio format is +additive — it must never change the `figma`/`w3c` output shapes). + +**Heavy-load utilities (2026-06):** `code.ts` ships a `BATCH` config with +`runBatched` / `runBatchedAsync` / `runSequentialAsync` (QuickJS-safe, no +generators) that yield between batches, plus throttled `operation_progress` +emission, cooperative cancellation, and a single operation lock +(`operation_denied` on concurrent ops). Route any new loop over many +variables/styles through these — never block the VM with a raw loop. + --- ## Code-change checklist @@ -276,4 +301,4 @@ Never delete — rewrite if the content becomes wrong: --- -*Last updated: 2026-05-22 (Portfolio-style structure adopted; content promoted from `docs/AGENTS.md` to project root)* +*Last updated: 2026-06-10 (message protocol replaced with the 2026-06 overhaul version — chunked export, progress/cancel, snapshot-in-import_complete; new export options + heavy-load utilities documented; START_HERE.md added as read-order item 0)* diff --git a/variables-styles-extractor/START_HERE.md b/variables-styles-extractor/START_HERE.md new file mode 100644 index 0000000..5b4b86c --- /dev/null +++ b/variables-styles-extractor/START_HERE.md @@ -0,0 +1,92 @@ + + +# START_HERE.md — Variables & Styles Extractor + +> Boot check for any AI session opening this Figma plugin folder. +> Run the 60-second checklist, internalise the constraints, then work. + +--- + +## 1. 60-second boot checklist + +1. **Read [`AGENTS.md`](AGENTS.md)** — canonical AI-builder rules: architecture, conventions, protocol, file-protection rules. +2. **Read `.gcc/session-memory.md`** — warm-start state from the prior session. + **Real path:** `Side-Kicks/variables-styles-extractor/.gcc/session-memory.md` at the **repo-root main checkout** (the `.gcc/` folder is untracked — it is NOT inside this plugin subfolder and does NOT exist in git worktrees; reach over to the main checkout to read it). +3. **If you will write code:** read [`docs/CODING_STANDARDS.md`](docs/CODING_STANDARDS.md) before touching `src/code.ts` or `ui.html`. + +--- + +## 2. Hard constraints recap + +| Surface | Constraint | +|---------|-----------| +| `src/code.ts` (QuickJS VM) | **No spread (`{...obj}`), no generators, no optional chaining (`?.`) in NEW code.** Use `Object.assign({}, obj)`, plain loops, and `if (obj && obj.prop)`. Pre-existing violations in old code are history; any NEW one is a regression. | +| `ui.html` CSS | **No `contain: strict`, no `content-visibility: auto`** — both break rendering in the Figma iframe sandbox. Use `contain: layout style`. | +| Artifacts | **Single-file artifacts.** `ui.html` is one self-contained file (all CSS + JS inline); `code.js` is one compiled file. No bundlers, no external assets. | +| Network | **`networkAccess: ["none"]` in `manifest.json`** — the iframe cannot load ANY external resource. No CDN scripts, no Google Fonts ``, no remote images in `ui.html`. Everything ships inline. | + +--- + +## 3. Build & test commands + +```bash +pnpm install --frozen-lockfile # deps (pnpm only — never npm/npx) +pnpm build:dev # tsc only → readable code.js (debugging) +pnpm build # tsc + terser → minified production code.js +``` + +**Commit `code.js` with `src/code.ts` changes — no CI builds it.** The compiled file ships from this repo. + +### Figma Desktop hot-reload (manual test loop) + +1. Figma Desktop → **Plugins → Development → Import plugin from manifest…** → pick this folder's `manifest.json` (first time only). +2. After each `pnpm build:dev`: **Plugins → Development → Hot reload** (or close/reopen the plugin). +3. Test in a real file. Once working, run `pnpm build` and commit the minified `code.js`. + +--- + +## 4. Architecture one-pager + +**Three source files:** + +| File | Runtime | Role | +|------|---------|------| +| `src/code.ts` | Figma QuickJS VM (ES2017) | Backend — all Figma API access, export/import/clear/undo logic | +| `ui.html` | Browser iframe | Frontend — single-file UI, Simple + Advanced modes | +| `manifest.json` | Figma | Plugin config (`networkAccess: ["none"]`) | + +`code.js` is the compiled backend output — checked in, never hand-edited. + +**Two UI modes:** **Simple** (3-section Export/Import tabs driven by name-prefix groups; `selectedExportGroups`/`selectedImportGroups` Maps are the source of truth, collection-level Sets are projections) and **Advanced** (full collection/mode-level control — kept pixel-identical; Simple-mode work must not touch it). + +**Message protocol (current):** + +- **UI → backend:** `cancel_operation`, `resize_ui` (window follows mode: Simple 905×628 / Advanced 1200×628), `export` (with `selectedGroups` / `selectedStyleGroups` / `exportFormat` incl. `tokens-studio`), `import`, `validate_import`, `compute_import_diff`, `detect_plan`, `clear_variables` / `clear_styles` / `clear_all`, `get_collections`, `check_libraries`, `check_fonts`, `undo_import` +- **Backend → UI:** `log`, `collections` (with `groups` + `styleGroups`), `operation_progress`, `export_chunk`, `export_done`, `import_complete` (carries the undo snapshot), `import_rolling_back` / `import_rollback_complete` / `import_rollback_failed`, `validation_result`, `import_diff_result`, `plan_detected`, `clear_complete`, `library_check_result`, `font_check_result`, `undo_complete` / `undo_error`, `operation_cancelled`, `operation_denied`, `error` +- **Removed (2026-06 overhaul — do not reintroduce):** `export_complete`, `get_variables`/`variables`, `collection_details`, `close`, `create_undo_snapshot`/`snapshot_created`/`snapshot_error` + +**Heavy-load model:** `BATCH` config + `runBatched` / `runBatchedAsync` / `runSequentialAsync` (QuickJS-safe, no generators) yield between batches; `operation_progress` is throttled (≥250ms) and renders in `[data-progress-host]` components in both modes with a Cancel button; cancellation is cooperative (sentinel-property errors, `cancel_operation` handled first in dispatch); a single **operation lock** rejects concurrent ops with `operation_denied`; exports stream as 256KB `export_chunk` messages (surrogate-safe) finished by `export_done`. + +**Export formats:** `figma` (default) | `w3c` | `tokens-studio` (additive third format: shape-A single file with Tokens Studio sets/$themes/$metadata, DTCG keys, `{dot.path}` aliases). + +--- + +## 5. Danger zones + +- **Snapshot / rollback invariants:** `restoreFromSnapshot` **validates the snapshot BEFORE clearing anything** (never wipe-first — that was a real bug). Rollback is **non-cancellable** once started. The undo snapshot rides inside `import_complete`; the UI never pre-creates snapshots. `figma.commitUndo()` brackets imports and standalone clears so native Cmd+Z stays atomic. +- **Additive-format guarantee:** export formats are strictly additive — adding/changing `tokens-studio` (or any new format) must never alter the `figma` or `w3c` output shapes. Existing exported files must keep importing cleanly. +- **Advanced-untouched rule:** any Simple-mode work must leave Advanced mode pixel-identical and behaviorally unchanged. Advanced mutators write through to the Simple-mode group Maps — keep that direction intact. + +--- + +*Last verified: 2026-06-10* diff --git a/variables-styles-extractor/docs/CHANGELOG.md b/variables-styles-extractor/docs/CHANGELOG.md index 45e092f..a8a420f 100644 --- a/variables-styles-extractor/docs/CHANGELOG.md +++ b/variables-styles-extractor/docs/CHANGELOG.md @@ -10,6 +10,51 @@ All notable changes to this Figma plugin are documented here. --- +## [2.1.0] - Unreleased + +### ✨ Simple Mode Redesign, Heavy-File Performance & Tokens Studio Export + +**Status:** In development — pending final manual testing in Figma Desktop before release + +### New Features + +#### Simple Mode — Redesigned 3-Section Layout +- **🧩 Simple mode rebuilt as a clean 3-section layout** on both tabs + - **Export tab:** Variables (collections → groups) → Styles (types → groups) → Log + Export + Copy JSON + - **Import tab:** Paste/Upload → parsed contents (grouped) → Log + Import + Undo +- **🎯 Collection- and group-level selection**: pick exactly which variable groups (by name prefix, e.g. `color/...`) and style groups to export or import — no more all-or-nothing +- **📐 Compact window in Simple mode**: sections match the Advanced column width and the plugin window shrinks to 905×628 in Simple mode (back to 1200×628 in Advanced) +- **🖥️ Advanced mode unchanged**: pixel-identical to v2.0.0 + +#### Progress Bars + Cancel for Long Operations +- **📊 Live progress bars** for long-running exports, imports, and clears — shown in both Simple and Advanced modes +- **🛑 Cancel button**: safely stop a long operation mid-flight +- **🧊 No more freezes**: large files no longer lock up the plugin while it works + +#### Safer Import Undo +- **🛡️ Snapshot validated before any clearing**: a corrupt or incomplete undo snapshot can no longer wipe your file — undo now refuses to clear anything until the snapshot checks out +- **↩️ One-step native undo**: imports (and standalone clears) are wrapped so a single Cmd+Z in Figma reverts the entire operation + +#### Tokens Studio-Compatible Export (New Optional Format) +- **🎨 New third export format**: "Tokens Studio" — a single `tokens.json` with token sets per Collection/Mode, `$themes`, and `$metadata.tokenSetOrder` +- **🔗 DTCG-style keys and Tokens Studio-canonical types**, with aliases exported as `{dot.path}` references +- **🖌️ Color, typography, and effect styles** included as token sets; grid/image/blur styles are skipped with explanatory notes +- Available from the Advanced format dropdown and the Simple mode Format select + +### Performance +- **⚡ Faster imports**: a single local cache scan per import (previously up to 4 full rescans); library indexing now runs only when actually needed +- **📦 Chunked export delivery**: huge exports are streamed to the UI in chunks, so very large files export reliably without hitting message-size limits + +### Fixed +- **🔧 showToast crash**: fixed a `ReferenceError` that could crash the plugin when showing notifications + +### Removed +- **🧹 Dead weight removed**: ~850KB of unused files and dead code stripped from the plugin +- **🔐 Unused `currentuser` permission dropped**: the plugin no longer requests access it never used +- **🚫 Blocked Google Fonts request removed**: the UI no longer attempts an external font request that was always blocked by the plugin's no-network policy + +--- + ## [2.0.0] - 2026-01-16 (UNRELEASED) ### 🎨 Major UI Overhaul - Wide 4-Column Layout @@ -343,4 +388,4 @@ Major feature release adding support for all paint types in color styles, includ --- -**Last Updated:** 2026-01-15 +**Last Updated:** 2026-06-10 diff --git a/variables-styles-extractor/docs/KNOWN_ISSUES.md b/variables-styles-extractor/docs/KNOWN_ISSUES.md index 25d4248..ba04c5d 100644 --- a/variables-styles-extractor/docs/KNOWN_ISSUES.md +++ b/variables-styles-extractor/docs/KNOWN_ISSUES.md @@ -1,7 +1,7 @@ # Known Issues - Variables & Styles Extractor **Plugin**: Variables & Styles Extractor -**Current Version**: 1.6.0 (2.0.0 in development) +**Current Version**: 2.0.0 (published) — 2.1.0 unreleased in development **Status**: Published to Figma Community --- @@ -136,4 +136,4 @@ Plugin crashed with "stack underflow" on large files. Fixed by changing TypeScri --- -**Last Updated:** 2026-01-05 +**Last Updated:** 2026-06-10 diff --git a/variables-styles-extractor/docs/TASKS.md b/variables-styles-extractor/docs/TASKS.md index 8117929..ea06e7a 100644 --- a/variables-styles-extractor/docs/TASKS.md +++ b/variables-styles-extractor/docs/TASKS.md @@ -154,4 +154,22 @@ Use this file for comprehensive testing. --- -*Last updated: 2026-01-16* +## 🔁 v2.1.0 Overhaul — Session 2026-06-10 + +Five-phase overhaul completed on branch `claude/nostalgic-euclid-d86723` (worktree; not yet pushed/merged). + +### ✅ Completed Phases +- [x] **Phase A — Cleanup** (`7114047`): removed ~850KB dead weight (backup/, releases/, duplicate PNGs) and dead code; fixed showToast ReferenceError; dropped unused `currentuser` manifest permission +- [x] **Phase B — CSS consolidation** (`fd39565`): −279 net lines; `.scrollable` utility + shared scroll-fade pattern; 7 conflicting selector pairs resolved; removed CSP-blocked Google Fonts link; `rel=noopener` on external links +- [x] **Phase C — Simple-mode 3-section UI** (`cca5893`): Export/Import tabs rebuilt with collection → name-prefix group selection; group-selection Maps as source of truth; XSS-verified rendering; Advanced mode pixel-identical +- [x] **Phase D — Heavy-load handling** (`79fc9c8`): batched processing with progress bars + Cancel (both modes); cooperative cancellation + operation lock; single cache scan per import; snapshot validated before clearing; native Cmd+Z bracketing via `figma.commitUndo()`; chunked export delivery +- [x] **Phase E — Tokens Studio export** (`b2b2d42`): additive third export format — DTCG keys, token sets per Collection/Mode, `$themes` + `$metadata.tokenSetOrder`, `{dot.path}` aliases, `tokens.json` filename + +### 🔜 Open Items +- [ ] Run the Figma Desktop manual test matrix (hot-reload dev plugin; both modes; large-file import/export with progress + cancel; corrupt-snapshot undo refuses to clear; Tokens Studio plugin imports the exported tokens.json) +- [ ] Bump plugin version 2.0.0 → 2.1.0 (package.json, ui.html footer, code.ts showUI title) + set CHANGELOG.md release date; push branch + PR to tknatwork/side-kicks main +- [ ] Publish to Figma Community + +--- + +*Last updated: 2026-06-10* From 4cbfa87e69653aae06151afc4f79d632e7d79239 Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 18:58:00 +0530 Subject: [PATCH 08/20] ui: expand Simple-mode cards to full sections, flag external library deps - The column headers already name each Simple section, so the redundant inner card headers ("Variables Collections", "Styles", "Load JSON Data", "Import Contents") are gone; the yellow cards now expand to the full section space (tighter section padding) so group lists and the Select All / None buttons get the room - External-dependency flag: when the file's variables reference team- library tokens (export side, from the collections payload) or pasted JSON contains $libraryRef tokens (import side), Simple mode shows a warning banner with a one-click "Open Advanced" button that performs the full mode switch (radio + window resize + state projection); banners clear with their data Verified in preview: cards fill sections (zero inner headers in the simple layout), banners appear/disappear with their triggers, Open Advanced round-trips, Advanced mode untouched. Co-Authored-By: Claude Fable 5 --- variables-styles-extractor/ui.html | 90 +++++++++++++++++++++++++----- 1 file changed, 77 insertions(+), 13 deletions(-) diff --git a/variables-styles-extractor/ui.html b/variables-styles-extractor/ui.html index 03cdcdb..c3b733c 100644 --- a/variables-styles-extractor/ui.html +++ b/variables-styles-extractor/ui.html @@ -3264,6 +3264,40 @@ min-height: 0; } + /* The column header already names each Simple section, so the yellow cards + carry no inner header and expand to the full section space. */ + .simple-layout .column-body { + padding: 8px; + } + .simple-layout .simple-fill-card { + width: 100%; + } + + /* External-library dependency flag (Simple mode) with a path to Advanced */ + .simple-dep-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius); + background: #FFF3CD; + font-size: 11px; + flex-shrink: 0; + margin-bottom: 8px; + } + .simple-dep-banner.hidden { + display: none; + } + .simple-dep-banner-text { + flex: 1; + min-width: 0; + } + .simple-dep-banner .btn { + flex-shrink: 0; + white-space: nowrap; + } + /* ========== HEAVY-LOAD OPERATION PROGRESS (Phase D) ========== */ .op-progress { display: flex; @@ -3657,10 +3691,11 @@
+
-
- Variables Collections -
@@ -3677,9 +3712,6 @@
-
- Styles -
@@ -4194,9 +4226,6 @@
-
- Load JSON Data -
@@ -4217,10 +4246,11 @@ 🧩 Import Contents
+
-
- Import Contents -
@@ -5557,6 +5587,7 @@ let expandedSimpleExport = new Set(); // keys 'col:' / 'style:' let expandedSimpleImport = new Set(); let simpleExportPendingAction = null; // 'download' | 'copy' | null + let simpleImportHasLibraryRefs = false; // pasted JSON contains $libraryRef tokens // Plan limits (mirror of backend constants) const PLAN_LIMITS = { @@ -6739,7 +6770,8 @@ // Store library deps globally for export window.detectedLibraryDeps = libraryDeps; - + updateSimpleDepBanners(); + if (libraryDeps.length > 0) { warningBanner.classList.remove('hidden'); depsList.innerHTML = libraryDeps.map(name => `📚 ${name}`).join(''); @@ -8198,9 +8230,11 @@ parent.postMessage({ pluginMessage: { type: 'detect_plan' } }, '*'); // Simple mode: rebuild group index + default-select everything (stage 3 wiring) + simpleImportHasLibraryRefs = data.indexOf('"$libraryRef"') !== -1; computeImportGroupsIndex(); initSimpleImportSelectionAll(); renderSimpleImportContents(); + updateSimpleDepBanners(); } catch (e) { // Hide skeletons on error @@ -9558,8 +9592,33 @@ importGroupsIndex = new Map(); selectedImportStyleGroups = { color: new Set(), text: new Set(), effect: new Set(), grid: new Set() }; importStyleGroupsIndex = { color: [], text: [], effect: [], grid: [] }; + simpleImportHasLibraryRefs = false; renderSimpleImportContents(); updateSimpleImportButtonState(); + updateSimpleDepBanners(); + } + + // External-library dependency flags (Simple mode): the column headers name the + // sections, but library-aware handling (refs option, mapping review) lives in + // Advanced — flag deps and offer the switch. + function updateSimpleDepBanners() { + const exportBanner = document.getElementById('simple-export-dep-banner'); + if (exportBanner) { + const hasDeps = !!(window.detectedLibraryDeps && window.detectedLibraryDeps.length > 0); + exportBanner.classList.toggle('hidden', !hasDeps); + } + const importBanner = document.getElementById('simple-import-dep-banner'); + if (importBanner) { + importBanner.classList.toggle('hidden', !simpleImportHasLibraryRefs); + } + } + + function switchToAdvancedMode() { + const radio = document.getElementById('mode-advanced'); + if (radio && !radio.checked) { + radio.checked = true; + radio.dispatchEvent(new Event('change', { bubbles: true })); + } } // Clear the Simple input mirror (textarea + loaded-file indicator). @@ -9771,6 +9830,11 @@ STYLE_TYPES.forEach(st => setImportStyleTypeAll(st.type, false)); }); + // Library-dependency banners: one-click switch to the Advanced tab + document.querySelectorAll('[data-action="switch-advanced"]').forEach(btn => { + btn.addEventListener('click', switchToAdvancedMode); + }); + // Body starts in Simple mode (no saved-prefs restore exists; only the // radio change listener ever toggles .advanced-mode). moveSharedNodesForMode(false); From d3daa8805eee22221e2918d7b93074bdfe06bafa Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:44:00 +0530 Subject: [PATCH 09/20] perf/ui: instant paste feedback, lag-free mode switch with large JSON, Simple-mode edge fades MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes from real Figma Desktop testing with large design-system files: 1. Processing spinner on paste/upload (Simple import): the Import Contents section shows a spinner the moment data is pasted, typed, or uploaded, replaced by the parsed contents when ready. 2. Large-payload load balancing — the root cause of the paste lag AND the Simple->Advanced switch freeze was multi-megabyte JSON living in BOTH import textareas (every reflow, including the 905->1200 window resize, re-laid-out megabytes of text): - New raw-text store: payloads >300KB never enter the textarea DOM. A paste interceptor stores the clipboard text directly (preventDefault — 0ms handler measured vs multi-second insert) and both textareas show a short read-only placeholder ("Large JSON loaded (N MB)...") until cleared. - All parse/skeleton readers now go through getImportRawText(); clear/import-complete/reset paths reset the store + read-only state. - The mode-switch DOM sync is deferred one tick so the toggle and window resize paint first (0.3ms click handler measured with a large system loaded; double-toggle guarded). 3. Edge shadow fades: the Simple-mode group lists (export variables, export styles, import contents) now have the same scroll edge fades as the Advanced columns, via the shared scroll-fade-container pattern + initScrollFade (MutationObserver keeps them current as lists re-render). Fixed the flex min-height chain so the inner list (not the whole card) stays the scroll container. Co-Authored-By: Claude Fable 5 --- variables-styles-extractor/ui.html | 192 +++++++++++++++++++++-------- 1 file changed, 138 insertions(+), 54 deletions(-) diff --git a/variables-styles-extractor/ui.html b/variables-styles-extractor/ui.html index c3b733c..6177443 100644 --- a/variables-styles-extractor/ui.html +++ b/variables-styles-extractor/ui.html @@ -3272,6 +3272,11 @@ .simple-layout .simple-fill-card { width: 100%; } + /* The scroll-fade wrappers must shrink with the card so the inner list + (not the whole card) is the scroll container. */ + .simple-layout .scroll-fade-container { + min-height: 0; + } /* External-library dependency flag (Simple mode) with a path to Advanced */ .simple-dep-banner { @@ -3700,7 +3705,9 @@
-
+
+
+
@@ -3716,7 +3723,9 @@
-
+
+
+
@@ -3782,7 +3791,7 @@
- +
-
+
+
+
@@ -4793,8 +4804,7 @@ // Clear import input area function clearImportInput() { - const importInput = document.getElementById('import-input'); - if (importInput) importInput.value = ''; + setImportRawText(''); importData = null; libraryStatus = null; fontStatus = null; @@ -5524,7 +5534,7 @@ // Show loading skeletons immediately for instant UI feedback function showImportSkeletons() { - const data = document.getElementById('import-input').value.trim(); + const data = getImportRawText().trim(); const statusSkeleton = document.getElementById('import-status-skeleton'); const previewSkeleton = document.getElementById('import-preview-skeleton'); const orderEmptyPreview = document.getElementById('import-empty-preview-order'); @@ -5588,6 +5598,10 @@ let expandedSimpleImport = new Set(); let simpleExportPendingAction = null; // 'download' | 'copy' | null let simpleImportHasLibraryRefs = false; // pasted JSON contains $libraryRef tokens + // Raw import JSON lives here, NOT only in the textareas: multi-MB textarea + // values make every reflow (mode switch, window resize) freeze the iframe. + let importRawText = ''; + let importDisplayTruncated = false; // textareas show a short placeholder instead // Plan limits (mirror of backend constants) const PLAN_LIMITS = { @@ -6593,38 +6607,38 @@ currentUserMode = e.target.value; // Window follows the mode: Simple is compact (905px), Advanced full (1200px) parent.postMessage({ pluginMessage: { type: 'resize_ui', mode: currentUserMode } }, '*'); - if (currentUserMode === 'advanced') { - document.body.classList.add('advanced-mode'); - // Show library refs option if deps detected + const toAdvanced = currentUserMode === 'advanced'; + document.body.classList.toggle('advanced-mode', toAdvanced); + // Defer the DOM-heavy sync one tick so the toggle highlight and the + // window resize land first — keeps the switch feeling instant even + // with a large design system loaded (load balancing). + setTimeout(() => { + if ((currentUserMode === 'advanced') !== toAdvanced) return; // re-toggled meanwhile const exportLibraryOption = document.getElementById('export-library-option'); - if (exportLibraryOption && window.detectedLibraryDeps?.length > 0) { - exportLibraryOption.classList.remove('hidden'); + if (toAdvanced) { + if (exportLibraryOption && window.detectedLibraryDeps?.length > 0) { + exportLibraryOption.classList.remove('hidden'); + } + // Simple -> Advanced: restore shared nodes + project Simple state into Advanced DOM + moveSharedNodesForMode(true); + syncAdvancedDomFromSimpleState(); + } else { + if (exportLibraryOption) { + exportLibraryOption.classList.add('hidden'); + } + // Advanced -> Simple: adopt shared nodes + rebuild group state from Advanced DOM + moveSharedNodesForMode(false); + rebuildSimpleStateFromAdvanced(); + renderSimpleExportVariables(); + renderSimpleExportStyles(); + renderSimpleImportContents(); } - // Simple -> Advanced: restore shared nodes + project Simple state into Advanced DOM (stage 3 wiring) - moveSharedNodesForMode(true); - syncAdvancedDomFromSimpleState(); - } else { - document.body.classList.remove('advanced-mode'); - // Hide library refs option in simple mode - const exportLibraryOption = document.getElementById('export-library-option'); - if (exportLibraryOption) { - exportLibraryOption.classList.add('hidden'); + // Re-render import collections if on selection face + const selectionFace = document.getElementById('import-selection-face'); + if (selectionFace && selectionFace.style.display !== 'none') { + renderImportCollectionsList(); } - // Advanced -> Simple: adopt shared nodes + rebuild group state from Advanced DOM (stage 3 wiring) - moveSharedNodesForMode(false); - rebuildSimpleStateFromAdvanced(); - const si = document.getElementById('simple-import-input'); - const ai = document.getElementById('import-input'); - if (si && ai) si.value = ai.value; - renderSimpleExportVariables(); - renderSimpleExportStyles(); - renderSimpleImportContents(); - } - // Re-render import collections if on selection face - const selectionFace = document.getElementById('import-selection-face'); - if (selectionFace && selectionFace.style.display !== 'none') { - renderImportCollectionsList(); - } + }, 0); }); }); @@ -7004,11 +7018,8 @@ } addLog(importMsg, 'success', 'import'); loadCollections(); - document.getElementById('import-input').value = ''; + setImportRawText(''); document.getElementById('file-input').value = ''; // Reset file input for re-selection - // Simple mode: keep the mirrored textarea in sync (stage 3 wiring) - const simpleImportInputAfterDone = document.getElementById('simple-import-input'); - if (simpleImportInputAfterDone) simpleImportInputAfterDone.value = ''; const importBtnComplete = document.getElementById('import-btn'); importBtnComplete.disabled = true; importBtnComplete.textContent = '📥 Import Selected'; // Reset button text @@ -7824,10 +7835,18 @@ // ========== IMPORT ========== + // Advanced textarea input: sync the raw-text store (caret preserved), then parse + function onAdvancedImportInput(ta) { + if (importDisplayTruncated) return; // placeholder is read-only + showSimpleImportProcessing(); + setImportRawText(ta.value, ta); + debouncedParseImportPreview(); + } + // Simplified parseImportPreview - reuses showImportSkeletons and parseImportPreviewAsync function parseImportPreview() { showImportSkeletons(); - const data = document.getElementById('import-input').value.trim(); + const data = getImportRawText().trim(); if (data) { parseImportPreviewAsync(); } @@ -7865,7 +7884,7 @@ } async function parseImportPreviewAsync() { - const data = document.getElementById('import-input').value.trim(); + const data = getImportRawText().trim(); if (!data) return; const orderEmptyPreview = document.getElementById('import-empty-preview-order'); @@ -8885,13 +8904,18 @@ const importTextarea = document.getElementById('import-input'); if (importTextarea) { importTextarea.addEventListener('paste', (e) => { + // Large payloads bypass the textarea DOM entirely (handleImportPaste + // calls preventDefault) — inserting multi-MB text is what freezes paste. + handleImportPaste(e); // Show loading feedback immediately showImportSkeletons(); + showSimpleImportProcessing(); clearFileLoadedIndicator(); - - // Let the paste happen, then process with a slight delay - // to ensure textarea value is updated + if (e.defaultPrevented) return; // raw text already stored + parse scheduled + + // Small paste: let it land in the textarea, then sync + process setTimeout(() => { + setImportRawText(importTextarea.value, importTextarea); parseImportPreviewAsync(); }, 0); }); @@ -8912,7 +8936,8 @@ } const reader = new FileReader(); reader.onload = (e) => { - document.getElementById('import-input').value = e.target.result; + showSimpleImportProcessing(); + setImportRawText(e.target.result); addLog('📁 Loaded: ' + file.name, 'success', 'import'); // Show the loaded file indicator @@ -8923,9 +8948,7 @@ fileIndicator.classList.remove('hidden'); } - // Simple mode: mirror the loaded file into the Simple input + indicator (stage 3 wiring) - const simpleInputMirror = document.getElementById('simple-import-input'); - if (simpleInputMirror) simpleInputMirror.value = e.target.result; + // Simple mode: textarea already synced by setImportRawText; mirror the indicator const simpleFileIndicator = document.getElementById('simple-import-file-loaded'); const simpleFileNameSpan = document.getElementById('simple-import-file-name'); if (simpleFileIndicator && simpleFileNameSpan) { @@ -9631,6 +9654,63 @@ if (fileLoaded) fileLoaded.classList.add('hidden'); } + // ===== Raw import text store (large-payload performance) ===== + // Payloads above this size never live in the textareas — a placeholder is + // shown instead, so reflows (mode switch, window resize) stay cheap. + const IMPORT_DISPLAY_LIMIT = 300 * 1024; + + function getImportRawText() { + if (importDisplayTruncated) return importRawText; + const ta = document.getElementById('import-input'); + return ta ? ta.value : ''; + } + + function importPlaceholderText() { + const mb = (importRawText.length / (1024 * 1024)).toFixed(1); + return '📄 Large JSON loaded (' + mb + ' MB) — content is held in memory and the preview is hidden to keep the plugin fast.\n\nUse 🗑️ Clear to remove it.'; + } + + // Single write path for the import JSON. sourceEl (optional) is the textarea + // the user is typing in — left untouched for small payloads to preserve the caret. + function setImportRawText(text, sourceEl) { + importRawText = text || ''; + importDisplayTruncated = importRawText.length > IMPORT_DISPLAY_LIMIT; + const display = importDisplayTruncated ? importPlaceholderText() : importRawText; + [document.getElementById('import-input'), document.getElementById('simple-import-input')].forEach(ta => { + if (!ta) return; + if (ta === sourceEl && !importDisplayTruncated) { + ta.readOnly = false; + return; + } + ta.value = display; + ta.readOnly = importDisplayTruncated; + }); + } + + // Immediate feedback while a pasted/uploaded payload is being parsed. + function showSimpleImportProcessing() { + const list = document.getElementById('simple-import-list'); + if (list) { + list.innerHTML = '
Processing JSON…
'; + } + const btn = document.getElementById('simple-import-btn'); + if (btn) btn.disabled = true; + } + + // Paste interceptor: very large clipboard payloads bypass the textarea DOM + // entirely (inserting multi-MB text is what makes paste feel frozen). + function handleImportPaste(e) { + const clip = e.clipboardData || window.clipboardData; + if (!clip) return; + const text = clip.getData('text'); + if (text && text.length > IMPORT_DISPLAY_LIMIT) { + e.preventDefault(); + showSimpleImportProcessing(); + setImportRawText(text); + debouncedParseImportPreview(); + } + } + // --- Export action (Simple buttons share the one-shot pending-action latch) --- function setSimpleExportButtonsDisabled(disabled) { @@ -9808,12 +9888,12 @@ const simpleImportInputEl = document.getElementById('simple-import-input'); if (simpleImportInputEl) { simpleImportInputEl.addEventListener('input', () => { - // Mirror into the Advanced textarea (the single parse source), then - // reuse the exact debounced parse entry the Advanced textarea uses. - const advancedInput = document.getElementById('import-input'); - if (advancedInput) advancedInput.value = simpleImportInputEl.value; + if (importDisplayTruncated) return; // placeholder is read-only + showSimpleImportProcessing(); + setImportRawText(simpleImportInputEl.value, simpleImportInputEl); debouncedParseImportPreview(); }); + simpleImportInputEl.addEventListener('paste', handleImportPaste); } // Select All / None card actions @@ -9888,7 +9968,7 @@ libraryStatus = null; fontStatus = null; importWithLibraryLinks = true; - document.getElementById('import-input').value = ''; + setImportRawText(''); document.getElementById('file-input').value = ''; document.getElementById('import-btn').disabled = true; document.getElementById('import-status').textContent = 'Load a JSON file to begin'; @@ -10095,6 +10175,10 @@ // Initialize scroll fade for export collections initScrollFade('export-collections-container', 'export-collections'); + // Simple-mode lists get the same edge fades as the Advanced columns + initScrollFade('simple-export-vars-fade', 'simple-export-variables-list'); + initScrollFade('simple-export-styles-fade', 'simple-export-styles-list'); + initScrollFade('simple-import-fade', 'simple-import-list'); // ========== COLUMN BODY SCROLL FADE ========== function initColumnScrollFade(columnBodyId, fadeTopId, fadeBottomId) { From 6e718f73c3a8b54c1c29f96022f9fb6f598ec608 Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:53:50 +0530 Subject: [PATCH 10/20] ui: collapse footer accordion under progress, dropdowns always open downward MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Advanced export spill fix: when an operation starts, any open accordion sharing a .column-footer with a progress host collapses automatically (the Export Options accordion + progress + button no longer overflow the column boundary; verified 14px inside). 2. Custom dropdown panels everywhere: native popup so options + always appear BELOW the control (never clipped by column overflow: + the panel is fixed-position and attached to ). */ + .dd-panel { + position: fixed; + z-index: 10000; + background: var(--color-bg); + border: var(--border-width) solid var(--color-border); + border-radius: var(--border-radius); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25); + overflow-y: auto; + font-size: 11px; + } + .dd-option { + display: flex; + align-items: center; + gap: 6px; + padding: 6px 10px; + cursor: pointer; + white-space: nowrap; + } + .dd-option:hover { + background: var(--color-bg-secondary); + } + .dd-option.selected { + font-weight: 700; + } + .dd-check { + width: 12px; + flex-shrink: 0; + } + /* External-library dependency flag (Simple mode) with a path to Advanced */ .simple-dep-banner { display: flex; @@ -4363,6 +4395,17 @@ // and shows/hides every [data-progress-host]. Pass null to clear. function setOperationInFlight(op) { operationInFlight = op || null; + if (op) { + // Collapse any open accordion sharing a footer with a progress host — + // the progress component needs that vertical space, otherwise the + // footer content spills past the column boundary. + document.querySelectorAll('[data-progress-host]').forEach(h => { + const footer = h.closest('.column-footer'); + if (footer) { + footer.querySelectorAll('details[open]').forEach(d => { d.open = false; }); + } + }); + } const disableEls = document.querySelectorAll('[data-op-disable]'); disableEls.forEach(el => { if (op) { @@ -9697,6 +9740,87 @@ if (btn) btn.disabled = true; } + // ===== Custom dropdown panels ===== + // Native - ${mode} + + ${escapeHtml(mode)} `).join('')} ` : ''; - + return ` -
- +
+
-
${c.name}
-
${c.variableCount} variables • Modes: ${c.modes.join(', ')}
-
+
${cn}
+
${escapeHtml(c.variableCount)} variables • Modes: ${escapeHtml(c.modes.join(', '))}
+
${renderTypeBadges(c.types || {})}
${modeCheckboxes} @@ -7351,6 +7352,25 @@ `; }).join(''); + // Delegated handlers (attached once) — replaces inline onclick/onchange so + // user-supplied collection/mode names never enter an executable context. + if (!container.dataset.exportDelegated) { + container.dataset.exportDelegated = '1'; + container.addEventListener('click', function (e) { + if (e.target.closest('input')) return; // checkbox manages its own state + const item = e.target.closest('.collection-item[data-action="toggle-export-collection"]'); + if (item) toggleExportCollection(item.dataset.collection); + }); + container.addEventListener('change', function (e) { + const t = e.target; + if (t.classList && t.classList.contains('collection-checkbox')) { + updateExportSelection(t.dataset.collection, t.checked); + } else if (t.classList && t.classList.contains('export-mode-checkbox')) { + toggleExportMode(t.dataset.collection, t.dataset.mode, t.checked); + } + }); + } + // Detect and display source file plan updateExportDetectedPlan(); @@ -7642,31 +7662,31 @@
📦 Variables
- ${totalColors} + ${escapeHtml(totalColors)} Colors
- ${totalNumbers} + ${escapeHtml(totalNumbers)} Numbers
- ${totalStrings} + ${escapeHtml(totalStrings)} Strings
- ${totalBooleans} + ${escapeHtml(totalBooleans)} Booleans
- ${selectedExportCollections.size} + ${escapeHtml(selectedExportCollections.size)} Collections
- ${totalVars} + ${escapeHtml(totalVars)} Total
@@ -7681,25 +7701,25 @@
🎨 Styles
- ${selectedTextStyles} + ${escapeHtml(selectedTextStyles)} Text
- ${selectedColorStyles} + ${escapeHtml(selectedColorStyles)} Color
- ${selectedEffectStyles} + ${escapeHtml(selectedEffectStyles)} Effect
- ${selectedGridStyles} + ${escapeHtml(selectedGridStyles)} Layout Guide
- ${totalStyles} + ${escapeHtml(totalStyles)} Total
@@ -7712,27 +7732,27 @@ // Build tree preview let tree = ''; selectedCols.forEach(c => { - tree += `
📁 ${c.name}`; + tree += `
📁 ${escapeHtml(c.name)}`; c.modes.forEach(m => { - tree += `
📂 ${m}
`; + tree += `
📂 ${escapeHtml(m)}
`; }); - tree += `
... ${c.variableCount} variables
`; + tree += `
... ${escapeHtml(c.variableCount)} variables
`; }); // Add styles to tree if selected if (hasStylesSelected && totalStyles > 0) { tree += `
🎨 Styles`; if (selectedColorStyles > 0) { - tree += `
🎨 ${selectedColorStyles} Color Styles
`; + tree += `
🎨 ${escapeHtml(selectedColorStyles)} Color Styles
`; } if (selectedTextStyles > 0) { - tree += `
📝 ${selectedTextStyles} Text Styles
`; + tree += `
📝 ${escapeHtml(selectedTextStyles)} Text Styles
`; } if (selectedEffectStyles > 0) { - tree += `
✨ ${selectedEffectStyles} Effect Styles
`; + tree += `
✨ ${escapeHtml(selectedEffectStyles)} Effect Styles
`; } if (selectedGridStyles > 0) { - tree += `
📐 ${selectedGridStyles} Grid Styles
`; + tree += `
📐 ${escapeHtml(selectedGridStyles)} Grid Styles
`; } tree += `
`; } @@ -8024,7 +8044,7 @@ const flatVars = flattenObject(modeVars); varCount = Math.max(varCount, flatVars.length); flatVars.forEach(v => { - if (v.$type) types[v.$type]++; + if (v.$type && Object.prototype.hasOwnProperty.call(types, v.$type)) types[v.$type]++; }); }); @@ -8059,31 +8079,31 @@
📦 Variables
- ${totalColors} + ${escapeHtml(totalColors)} Colors
- ${totalNumbers} + ${escapeHtml(totalNumbers)} Numbers
- ${totalStrings} + ${escapeHtml(totalStrings)} Strings
- ${totalBooleans} + ${escapeHtml(totalBooleans)} Booleans
- ${totalCollections} + ${escapeHtml(totalCollections)} Collections
- ${totalVariables} + ${escapeHtml(totalVariables)} Total
@@ -8098,25 +8118,25 @@
🎨 Styles
- ${totalTextStyles} + ${escapeHtml(totalTextStyles)} Text
- ${totalColorStyles} + ${escapeHtml(totalColorStyles)} Color
- ${totalEffectStyles} + ${escapeHtml(totalEffectStyles)} Effect
- ${totalGridStyles} + ${escapeHtml(totalGridStyles)} Layout Guide
- ${totalStyles} + ${escapeHtml(totalStyles)} Total
@@ -8808,10 +8828,10 @@ // Leaf test matches flattenObject/computeImportGroupsIndex: // top-level leaves live under the '' (ungrouped) key const isLeaf = v && typeof v === 'object' && ('$value' in v || '$type' in v || '$alias' in v); - if (sel.has(isLeaf ? '' : k)) newMode[k] = v; + if (k !== '__proto__' && k !== 'constructor' && k !== 'prototype' && sel.has(isLeaf ? '' : k)) newMode[k] = v; }); } - newModes[modeName] = newMode; + if (modeName !== '__proto__' && modeName !== 'constructor' && modeName !== 'prototype') newModes[modeName] = newMode; }); newCol.modes = newModes; return { [colName]: newCol }; @@ -8854,7 +8874,7 @@ const listEl = document.getElementById('library-mapping-list'); listEl.innerHTML = libraryRefs.map(name => - `📚 ${name}` + `📚 ${escapeHtml(name)}` ).join(''); modal.classList.remove('hidden'); From d9295f4e58d678ac3940a8e4a6d7917c3ddabb6d Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:55:51 +0530 Subject: [PATCH 17/20] security: clear remaining CodeQL alerts (type badges + prune objects) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first pass cleared 11 of 24; this clears the last high-severity clusters CodeQL still traced: - renderTypeBadges interpolated the variable-type counts (types.color/float/boolean/string, from the collections message) into innerHTML unescaped — now escapeHtml-wrapped in both the export- and import-side badge renderers. This was the real source CodeQL attributed to the renderExportCollections sink. - pruneImportDataForSimpleSelection now builds its per-mode objects with Object.create(null) (CodeQL-recognized safe target for property injection from imported keys), in addition to the existing explicit __proto__/constructor/prototype key guards. Verified in preview: a malicious "__proto__" mode/key in pasted JSON is dropped, the prototype is not polluted, the pruned payload still JSON-serializes, and valid modes survive; no console errors. Remaining: one MEDIUM js/missing-origin-check on window.onmessage — a Figma plugin iframe cannot meaningfully compare event.origin (messages arrive from the Figma host); the handler already gates on the pluginMessage envelope. To be dismissed with rationale. Co-Authored-By: Claude Fable 5 --- variables-styles-extractor/ui.html | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/variables-styles-extractor/ui.html b/variables-styles-extractor/ui.html index 7e7b90e..a1dc71f 100644 --- a/variables-styles-extractor/ui.html +++ b/variables-styles-extractor/ui.html @@ -5076,10 +5076,10 @@
${colName} ${newBadge}
~${varCount} variables • Modes: ${modes.join(', ')}
- ${types.color > 0 ? `${types.color} colors` : ''} - ${types.float > 0 ? `${types.float} numbers` : ''} - ${types.boolean > 0 ? `${types.boolean} booleans` : ''} - ${types.string > 0 ? `${types.string} strings` : ''} + ${types.color > 0 ? `${escapeHtml(types.color)} colors` : ''} + ${types.float > 0 ? `${escapeHtml(types.float)} numbers` : ''} + ${types.boolean > 0 ? `${escapeHtml(types.boolean)} booleans` : ''} + ${types.string > 0 ? `${escapeHtml(types.string)} strings` : ''}
${advancedControls} @@ -7502,10 +7502,10 @@ function renderTypeBadges(types) { const badges = []; - if (types.color) badges.push(`${types.color} colors`); - if (types.float) badges.push(`${types.float} numbers`); - if (types.boolean) badges.push(`${types.boolean} booleans`); - if (types.string) badges.push(`${types.string} strings`); + if (types.color) badges.push(`${escapeHtml(types.color)} colors`); + if (types.float) badges.push(`${escapeHtml(types.float)} numbers`); + if (types.boolean) badges.push(`${escapeHtml(types.boolean)} booleans`); + if (types.string) badges.push(`${escapeHtml(types.string)} strings`); return badges.join(''); } @@ -8817,11 +8817,11 @@ const colData = colObj[colName] || {}; const newCol = {}; if ('$originalName' in colData) newCol.$originalName = colData.$originalName; - const newModes = {}; + const newModes = Object.create(null); const modesObj = colData.modes || {}; Object.keys(modesObj).forEach(modeName => { const modeVars = modesObj[modeName]; - const newMode = {}; + const newMode = Object.create(null); if (modeVars && typeof modeVars === 'object') { Object.keys(modeVars).forEach(k => { const v = modeVars[k]; From f5214fff8e18792f611ae6961fc13536884a93c1 Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:01:41 +0530 Subject: [PATCH 18/20] security: escape remaining numeric sinks + eliminate dynamic type-key writes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 3 — clears the remaining high-severity CodeQL clusters by making the mitigations recognizable to static analysis: - Font-status and validation cards: escape the last unescaped leaf interpolations (font/style counts, plan name, importing collection/ variable/mode counts) so no tainted path reaches innerHTML - Variable-type counters: replaced the dynamic `types[v.$type]++` (x3) with an explicit color/float/boolean/string if-chain — no user-controlled property name is written at all - export_chunk accumulator: index coerced with `>>> 0` to a non-negative int32 array index Remaining expected alerts are the prune writes to Object.create(null) targets (guarded against __proto__/constructor/prototype) and the window.onmessage origin check — both safe at runtime but not recognizable by CodeQL; these will be dismissed with rationale. Co-Authored-By: Claude Fable 5 --- variables-styles-extractor/ui.html | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/variables-styles-extractor/ui.html b/variables-styles-extractor/ui.html index a1dc71f..c09d0a1 100644 --- a/variables-styles-extractor/ui.html +++ b/variables-styles-extractor/ui.html @@ -5037,7 +5037,7 @@ const flatVars = flattenObject(modeVars); varCount = Math.max(varCount, flatVars.length); flatVars.forEach(v => { - if (v.$type && Object.prototype.hasOwnProperty.call(types, v.$type)) types[v.$type]++; + if (v.$type === 'color') types.color++; else if (v.$type === 'float') types.float++; else if (v.$type === 'boolean') types.boolean++; else if (v.$type === 'string') types.string++; }); }); @@ -5357,7 +5357,7 @@ const flatVars = flattenObject(modeVars); varCount = Math.max(varCount, flatVars.length); flatVars.forEach(v => { - if (v.$type && Object.prototype.hasOwnProperty.call(types, v.$type)) types[v.$type]++; + if (v.$type === 'color') types.color++; else if (v.$type === 'float') types.float++; else if (v.$type === 'boolean') types.boolean++; else if (v.$type === 'string') types.string++; }); }); @@ -6299,7 +6299,7 @@ All Fonts Available
- All ${requiredFonts.length} font(s) used by ${styleCount} text style(s) are installed and ready for import. + All ${escapeHtml(requiredFonts.length)} font(s) used by ${escapeHtml(styleCount)} text style(s) are installed and ready for import.
${availableFonts.map(f => ` @@ -6320,7 +6320,7 @@ Some Fonts Missing
- ${styleCount} text style(s) require ${requiredFonts.length} font(s). Some are available, but ${missingFonts.length} are missing. + ${escapeHtml(styleCount)} text style(s) require ${escapeHtml(requiredFonts.length)} font(s). Some are available, but ${escapeHtml(missingFonts.length)} are missing. Text styles with missing fonts may not import correctly.
@@ -6351,7 +6351,7 @@ Fonts Not Found
- ${styleCount} text style(s) require ${requiredFonts.length} font(s) that are not installed. + ${escapeHtml(styleCount)} text style(s) require ${escapeHtml(requiredFonts.length)} font(s) that are not installed. Text styles may fail to import or appear incorrectly.
@@ -6428,12 +6428,12 @@ html += `
✅ Ready to Import
-

Import is compatible with ${planInfo.name} plan (${planInfo.maxModes === Infinity ? 'unlimited' : planInfo.maxModes} modes/collection)

+

Import is compatible with ${escapeHtml(planInfo.name)} plan (${planInfo.maxModes === Infinity ? 'unlimited' : escapeHtml(planInfo.maxModes)} modes/collection)

${validation.importing ? `

- ${validation.importing.collections} collections • - ${validation.importing.totalVariables} variables • - Max ${validation.importing.maxModesInAnyCollection} modes in any collection + ${escapeHtml(validation.importing.collections)} collections • + ${escapeHtml(validation.importing.totalVariables)} variables • + Max ${escapeHtml(validation.importing.maxModesInAnyCollection)} modes in any collection

` : ''}
@@ -6945,7 +6945,7 @@ if (msg.seq === 0 || !window._exportChunks) { window._exportChunks = []; } - if (typeof msg.seq === 'number' && msg.seq >= 0) { window._exportChunks[msg.seq] = msg.data; } + if (typeof msg.seq === 'number' && msg.seq >= 0) { window._exportChunks[msg.seq >>> 0] = msg.data; } pendingProgress = { operation: 'export', phase: 'export_deliver', @@ -8044,7 +8044,7 @@ const flatVars = flattenObject(modeVars); varCount = Math.max(varCount, flatVars.length); flatVars.forEach(v => { - if (v.$type && Object.prototype.hasOwnProperty.call(types, v.$type)) types[v.$type]++; + if (v.$type === 'color') types.color++; else if (v.$type === 'float') types.float++; else if (v.$type === 'boolean') types.boolean++; else if (v.$type === 'string') types.string++; }); }); From ac3e58ca7d87351ee9614b59c0c766712413a69f Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:04:39 +0530 Subject: [PATCH 19/20] security: escape the last library-status-card count (clears final XSS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The library status card interpolated ${varCount} (variable count from the collections message) into innerHTML unescaped across its three branches — now escapeHtml-wrapped. This clears the last js/xss alerts (6076/6083). Co-Authored-By: Claude Fable 5 --- variables-styles-extractor/ui.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/variables-styles-extractor/ui.html b/variables-styles-extractor/ui.html index c09d0a1..49824b3 100644 --- a/variables-styles-extractor/ui.html +++ b/variables-styles-extractor/ui.html @@ -5988,7 +5988,7 @@ Asset Sources Detected!
- ${varCount} variables reference team library collections that are connected to this file. + ${escapeHtml(varCount)} variables reference team library collections that are connected to this file. Library links will be restored during import.
@@ -6012,7 +6012,7 @@ Partial Asset Sources
- ${varCount} variables reference team library collections. Some libraries are connected, but others are missing. + ${escapeHtml(varCount)} variables reference team library collections. Some libraries are connected, but others are missing.
${availableCollections.map(col => ` @@ -6048,7 +6048,7 @@ Asset Sources Not Connected
- ${varCount} variables reference team library collections that are not connected to this file. + ${escapeHtml(varCount)} variables reference team library collections that are not connected to this file. Variables will be imported with their fallback resolved values.
From a8fb8947aa4f893dcf9610bf666f9110dd38b500 Mon Sep 17 00:00:00 2001 From: Tushar Kant Naik <61114548+tknatwork@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:07:11 +0530 Subject: [PATCH 20/20] security: restructure prune key-guards as dominating early-return barriers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The inline (k !== '__proto__' && ...) guards on the two prune writes weren't recognised by CodeQL's RemotePropertyInjection barrier-guard analysis. Hoist them to early-return barriers (if (k === '__proto__' || ...) return;) that dominate the writes — the standard recognised form. Targets remain Object.create(null) (the write is a no-op for pollution regardless). Behaviour unchanged. Co-Authored-By: Claude Fable 5 --- variables-styles-extractor/ui.html | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/variables-styles-extractor/ui.html b/variables-styles-extractor/ui.html index 49824b3..ecdd181 100644 --- a/variables-styles-extractor/ui.html +++ b/variables-styles-extractor/ui.html @@ -8820,18 +8820,22 @@ const newModes = Object.create(null); const modesObj = colData.modes || {}; Object.keys(modesObj).forEach(modeName => { + // Barrier guard: never write a prototype-polluting key (also a no-op + // for the null-prototype target below, but recognised by static analysis). + if (modeName === '__proto__' || modeName === 'constructor' || modeName === 'prototype') return; const modeVars = modesObj[modeName]; const newMode = Object.create(null); if (modeVars && typeof modeVars === 'object') { Object.keys(modeVars).forEach(k => { + if (k === '__proto__' || k === 'constructor' || k === 'prototype') return; const v = modeVars[k]; // Leaf test matches flattenObject/computeImportGroupsIndex: // top-level leaves live under the '' (ungrouped) key const isLeaf = v && typeof v === 'object' && ('$value' in v || '$type' in v || '$alias' in v); - if (k !== '__proto__' && k !== 'constructor' && k !== 'prototype' && sel.has(isLeaf ? '' : k)) newMode[k] = v; + if (sel.has(isLeaf ? '' : k)) newMode[k] = v; }); } - if (modeName !== '__proto__' && modeName !== 'constructor' && modeName !== 'prototype') newModes[modeName] = newMode; + newModes[modeName] = newMode; }); newCol.modes = newModes; return { [colName]: newCol };