diff --git a/.github/codeql/codeql-config.yml b/.github/codeql/codeql-config.yml
new file mode 100644
index 0000000..3f35d0e
--- /dev/null
+++ b/.github/codeql/codeql-config.yml
@@ -0,0 +1,21 @@
+# CodeQL configuration for Side-Kicks
+#
+# The repository ships a single-file Figma plugin: the analyzable source is
+# src/code.ts (TypeScript) and the inline script in ui.html. code.js is the
+# MINIFIED, checked-in build artifact (terser output) — CodeQL classifies it as
+# generated and skips it, which previously left "no source code seen" and failed
+# the run. Scope analysis to the real source and ignore build/output artifacts.
+
+name: "Side-Kicks CodeQL config"
+
+paths:
+ - variables-styles-extractor/src
+ - variables-styles-extractor/ui.html
+
+paths-ignore:
+ - "**/code.js"
+ - "**/*.min.js"
+ - "**/releases/**"
+ - "**/backup/**"
+ - "**/marketing-assets/**"
+ - "**/node_modules/**"
diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md
index f9f3d37..acbef68 100644
--- a/.github/copilot-instructions.md
+++ b/.github/copilot-instructions.md
@@ -1,135 +1,19 @@
-# Copilot Instructions: Side-Kicks
+
-> ⚠️ PROTECTED FILE - DO NOT DELETE
-> This file must NEVER be deleted during cleanup or overhaul operations.
-> Instead, rewrite its contents to reflect the new direction.
+# Copilot Instructions — Side-Kicks
----
+This file is a thin redirect. The canonical AI-builder rules live elsewhere.
-## Folder Purpose
+**Read these, in order:**
-**Side-Kicks** is a multi-project workspace for Figma plugins and design tools. Each project is isolated in its own subfolder.
+1. **[`AGENTS.md`](../AGENTS.md)** — canonical workspace AI rules (what this repo is, project scope, security posture).
+2. When working inside the active project, read **[`variables-styles-extractor/AGENTS.md`](../variables-styles-extractor/AGENTS.md)** and its **`START_HERE.md`** boot check.
----
-
-## 🗑️ Bin Folder (Fail-Safe)
-
-Before deleting any file, move it to `../bin/` first:
-```bash
-# Instead of: rm file.md
-# Do: mv file.md ../../bin/
-```
-
----
-
-## Folder Structure
-
-```
-Side-Kicks/
-├── .github/
-│ ├── copilot-instructions.md ← THIS FILE (folder rules)
-│ └── ISSUE_TEMPLATE/ ← Shared GitHub templates
-├── docs/
-│ ├── AI_CONTEXT.md ← Folder context (PROTECTED)
-│ └── CHANGELOG.md ← Structure changes (PROTECTED)
-├── variables-styles-extractor/ ← PROJECT: Figma Plugin
-│ ├── AI_CONTEXT.md ← Project context (PROTECTED)
-│ ├── CHANGELOG.md ← Project history (PROTECTED)
-│ ├── TASKS.md ← Task tracking (PROTECTED)
-│ ├── .github/copilot-instructions.md
-│ ├── docs/
-│ └── src/
-└── README.md
-```
-
----
-
-## ⚠️ CRITICAL RULES
-
-### Protected Files
-
-**Folder-Level** (never delete, rewrite instead):
-- `.github/copilot-instructions.md` (this file)
-- `docs/AI_CONTEXT.md`
-- `docs/CHANGELOG.md`
-
-**Project-Level** (each project has its own):
-- `AI_CONTEXT.md`
-- `CHANGELOG.md`
-- `TASKS.md`
-- `.github/copilot-instructions.md`
-
-### Changelog Scope
-| Level | Tracks |
-|-------|--------|
-| `docs/CHANGELOG.md` | New projects, folder structure changes |
-| `[project]/CHANGELOG.md` | Code changes, releases within project |
-
-### Project Isolation
-- **Each project** lives in its own subfolder
-- **NEVER mix** files from different projects
-- **Stay scoped** - Identify which project before making changes
-
----
-
-## Current Projects
-
-### variables-styles-extractor/
-| Property | Value |
-|----------|-------|
-| **Purpose** | Figma plugin to export/import variables & styles |
-| **Status** | Active - Published on Figma Community |
-| **Context** | `variables-styles-extractor/AI_CONTEXT.md` |
-| **Tasks** | `variables-styles-extractor/TASKS.md` |
-
----
-
-## Project Template
-
-When creating a new project:
-
-```
-[project-name]/
-├── AI_CONTEXT.md ← Project context (PROTECTED)
-├── CHANGELOG.md ← Project history (PROTECTED)
-├── TASKS.md ← Task tracking (PROTECTED)
-├── README.md ← Public documentation
-├── .github/
-│ ├── copilot-instructions.md
-│ └── workflows/
-├── docs/ ← Additional documentation
-└── src/ ← Source code
-```
-
----
-
-## Guidelines for AI Assistants
-
-### DO:
-1. ✅ Identify which project you're working in first
-2. ✅ Read project's `AI_CONTEXT.md` before making changes
-3. ✅ Keep changes within that project's folder
-4. ✅ Update project's CHANGELOG when making changes
-5. ✅ Move files to `bin/` before deleting
-
-### DON'T:
-1. ❌ Delete protected files (rewrite instead)
-2. ❌ Mix files from different projects
-3. ❌ Touch `Portfolio/` folder (different workspace section)
-4. ❌ Delete files directly (use bin/ fail-safe)
-
----
-
-## Adding a New Project
-
-1. Create folder structure (see template)
-2. Create `AI_CONTEXT.md` with project context
-3. Create `CHANGELOG.md` with initial entry
-4. Create `TASKS.md` for task tracking
-5. Create `.github/copilot-instructions.md` with project rules
-6. Update folder's `docs/AI_CONTEXT.md` to list the project
-7. Log in folder's `docs/CHANGELOG.md`
-
----
-
-*Last Updated: 27 December 2025*
+This repo has one active project: **variables-styles-extractor** (a Figma plugin). The former nectar-design-toolkit and Design System Builder projects were removed.
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 84b29a8..6504302 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -4,10 +4,14 @@
# Updates land as PRs targeting `main` with the `dependencies` label.
# Branch protection still requires owner approval before merge — Dependabot
# cannot self-merge unless explicitly granted, which is not configured here.
+#
+# The repository contains a single active project (variables-styles-extractor);
+# the former Design System Builder and nectar-design-toolkit projects were
+# removed, so their ecosystems are no longer configured here.
version: 2
updates:
- # ── npm / pnpm package ecosystems ──────────────────────────────────────────
+ # ── npm / pnpm ─────────────────────────────────────────────────────────────
# variables-styles-extractor — published Figma plugin
- package-ecosystem: "npm"
@@ -27,85 +31,6 @@ updates:
dev-dependencies:
dependency-type: "development"
- # Design System Builder — pnpm workspace (root lockfile covers all packages)
- - package-ecosystem: "npm"
- directory: "/Design System Builder"
- schedule:
- interval: "weekly"
- day: "monday"
- open-pull-requests-limit: 5
- labels:
- - "dependencies"
- - "npm"
- - "design-system-builder"
- commit-message:
- prefix: "chore(deps)"
- include: "scope"
- groups:
- dev-dependencies:
- dependency-type: "development"
-
- # nectar-design-toolkit — each subproject ships its own package-lock.json
- - package-ecosystem: "npm"
- directory: "/nectar-design-toolkit/bridge-server"
- schedule: { interval: "weekly", day: "monday" }
- open-pull-requests-limit: 5
- labels: ["dependencies", "npm", "nectar-design-toolkit"]
- commit-message: { prefix: "chore(deps)", include: "scope" }
- groups:
- dev-dependencies:
- dependency-type: "development"
-
- - package-ecosystem: "npm"
- directory: "/nectar-design-toolkit/figma-plugin"
- schedule: { interval: "weekly", day: "monday" }
- open-pull-requests-limit: 5
- labels: ["dependencies", "npm", "nectar-design-toolkit"]
- commit-message: { prefix: "chore(deps)", include: "scope" }
- groups:
- dev-dependencies:
- dependency-type: "development"
-
- - package-ecosystem: "npm"
- directory: "/nectar-design-toolkit/mcp-server"
- schedule: { interval: "weekly", day: "monday" }
- open-pull-requests-limit: 5
- labels: ["dependencies", "npm", "nectar-design-toolkit"]
- commit-message: { prefix: "chore(deps)", include: "scope" }
- groups:
- dev-dependencies:
- dependency-type: "development"
-
- - package-ecosystem: "npm"
- directory: "/nectar-design-toolkit/nds-builder"
- schedule: { interval: "weekly", day: "monday" }
- open-pull-requests-limit: 5
- labels: ["dependencies", "npm", "nectar-design-toolkit"]
- commit-message: { prefix: "chore(deps)", include: "scope" }
- groups:
- dev-dependencies:
- dependency-type: "development"
-
- - package-ecosystem: "npm"
- directory: "/nectar-design-toolkit/nectar-style-generator"
- schedule: { interval: "weekly", day: "monday" }
- open-pull-requests-limit: 5
- labels: ["dependencies", "npm", "nectar-design-toolkit"]
- commit-message: { prefix: "chore(deps)", include: "scope" }
- groups:
- dev-dependencies:
- dependency-type: "development"
-
- - package-ecosystem: "npm"
- directory: "/nectar-design-toolkit/orchestration-server"
- schedule: { interval: "weekly", day: "monday" }
- open-pull-requests-limit: 5
- labels: ["dependencies", "npm", "nectar-design-toolkit"]
- commit-message: { prefix: "chore(deps)", include: "scope" }
- groups:
- dev-dependencies:
- dependency-type: "development"
-
# ── GitHub Actions ─────────────────────────────────────────────────────────
- package-ecosystem: "github-actions"
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
index 42cb134..21269bc 100644
--- a/.github/workflows/codeql.yml
+++ b/.github/workflows/codeql.yml
@@ -47,6 +47,9 @@ jobs:
languages: ${{ matrix.language }}
# Use security-extended for stronger ruleset (default is "security")
queries: security-extended
+ # Scope analysis to real source (src/ + ui.html inline JS); the
+ # checked-in code.js is minified build output and is skipped.
+ config-file: ./.github/codeql/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
diff --git a/AGENTS.md b/AGENTS.md
index 25c3f1c..8b00c2e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,7 +1,7 @@
-> ⚠️ PROTECTED FILE - DO NOT DELETE
-> This file must NEVER be deleted during cleanup or overhaul operations.
-> Instead, rewrite its contents to reflect the new direction.
+# Variables & Styles Extractor — Copilot Instructions
-## ⚠️ WORKSPACE SCOPE RESTRICTION
+> ⚠️ PROTECTED FILE — DO NOT DELETE. Rewrite if the direction changes.
-**ONLY work within:** `Side-Kicks/variables-styles-extractor/`
-**NEVER touch:** `My Portfolio/`, `Content Files/`, `Research Study/`
+This is a thin redirect. The canonical AI-builder rules are not duplicated here.
----
+**AI builders, read in this order before touching code:**
-## Protected Files
+1. [`START_HERE.md`](../START_HERE.md) — 60-second boot check (constraints recap, build commands, danger zones)
+2. [`AGENTS.md`](../AGENTS.md) — canonical plugin AI rules (architecture, constraints, conventions)
+3. [`docs/CODING_STANDARDS.md`](../docs/CODING_STANDARDS.md) + [`docs/FIGMA_PLUGIN_DEVELOPMENT.md`](../docs/FIGMA_PLUGIN_DEVELOPMENT.md) — mandatory coding rules and Figma sandbox patterns
-| File | Purpose |
-|------|---------|
-| `docs/AI_CONTEXT.md` | Project context |
-| `docs/CHANGELOG.md` | Project history |
-| `docs/TASKS.md` | Task tracking |
-| `.github/copilot-instructions.md` | This file |
-| `docs/AGENTS.md` | Universal AI instructions |
-
----
-
-## Project Overview
-
-This is a Figma plugin for exporting and importing variables and styles between Figma files.
-
-- **Repository:** https://github.com/tknatwork/side-kicks
-- **Version:** 2.0.0 (in development - UI overhaul)
-- **UI Size:** 1000x540 pixels (4-column layout)
-
-## Critical Files to Read First
-
-**MANDATORY:** Read these files before ANY code changes or new feature development.
-
-| Priority | File | Purpose |
-|----------|------|---------|
-| 1 | `docs/CODING_STANDARDS.md` | Coding standards, patterns & conventions |
-| 2 | `docs/AI_CONTEXT.md` | Project context and architecture |
-| 3 | `docs/CHANGELOG.md` | Version history and recent changes |
-| 4 | `docs/TASKS.md` | Current tasks and backlog |
-
----
-
-## New Feature Development Protocol
-
-When working on a new kind of build or feature that isn't covered in the critical files:
-
-### Step 1: Build First, Document After
-1. **Check critical files** for relevant instructions/patterns
-2. **If no guidance exists** for this type of feature:
- - Follow existing coding standards and patterns
- - Build the feature to be functional first
- - Test thoroughly to ensure it works
-
-### Step 2: Update Documentation
-Once the feature is working:
-1. **Update `docs/CODING_STANDARDS.md`** with any new patterns discovered
-2. **Update `docs/AI_CONTEXT.md`** if architecture changed
-3. **Update `docs/CHANGELOG.md`** with the new feature
-4. **Update this file** (`copilot-instructions.md`) if new critical workflows emerged
-
-### Why This Matters
-- Critical files must reflect the actual working codebase
-- New patterns become documented standards for future development
-- AI agents learn from accumulated best practices
-
----
-
-## Tech Stack
-
-- TypeScript (ES2017 target - required for Figma VM)
-- Single-file UI (`ui.html` - HTML/CSS/JS combined)
-- Figma Plugin API
-
-## Key Constraints
-
-### Figma VM Limitations
-- NO spread operators (`{...obj}`)
-- NO generators
-- Use `Object.assign()` instead of spread
-- Use async Figma APIs (`*Async()` versions)
-
-### CSS in Plugin Iframe
-- NO `contain: strict` (use `contain: layout style`)
-- NO `content-visibility: auto`
-- GPU acceleration OK (`transform: translateZ(0)`)
-
-## Build Commands
-
-```bash
-cd Side-Kicks/variables-styles-extractor
-pnpm install
-pnpm build # tsc + terser minification
-```
-
-## After Making Changes
-
-1. Update `docs/CHANGELOG.md` with changes
-2. Add to Best Practices if you learned something new
-3. Update Known Issues if you found/fixed a bug
-4. Run `pnpm build` and test in Figma
+Scope: work only within `Side-Kicks/variables-styles-extractor/`.
diff --git a/variables-styles-extractor/AGENTS.md b/variables-styles-extractor/AGENTS.md
index 850292c..3149a5c 100644
--- a/variables-styles-extractor/AGENTS.md
+++ b/variables-styles-extractor/AGENTS.md
@@ -1,11 +1,11 @@
# AGENTS.md — Variables & Styles Extractor
@@ -24,11 +24,11 @@ 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.
4. **[`docs/FIGMA_PLUGIN_DEVELOPMENT.md`](docs/FIGMA_PLUGIN_DEVELOPMENT.md)** — Figma sandbox constraints and patterns.
-5. **[`docs/AI_CONTEXT.md`](docs/AI_CONTEXT.md)** — legacy project context (protected, kept for tooling).
### Step 2: Understand what you can break
@@ -81,6 +81,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
@@ -101,18 +102,16 @@ 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/
+ ├── 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
```
---
@@ -143,19 +142,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.
---
@@ -189,6 +199,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
@@ -240,7 +262,6 @@ Never delete — rewrite if the content becomes wrong:
| `AGENTS.md` (this file) | Canonical AI rules |
| `CLAUDE.md` | Pointer for legacy Claude Code path |
| `LICENSE` | MIT + CFRL notice |
-| `docs/AI_CONTEXT.md` | Legacy context (protected) |
| `docs/AGENTS.md` | Redirect (kept for tooling) |
| `docs/CLAUDE.md` | Redirect (kept for tooling) |
| `docs/CHANGELOG.md` | Version history (protected) |
@@ -266,7 +287,6 @@ Never delete — rewrite if the content becomes wrong:
|------|---------|--------------|
| `AGENTS.md` (this file) | Canonical AI rules | Every session |
| `CLAUDE.md` | Pointer | Auto-loaded by legacy paths |
-| `docs/AI_CONTEXT.md` | Legacy context (protected) | First time on project |
| `docs/CODING_STANDARDS.md` | Mandatory rules | Before every coding session |
| `docs/FIGMA_PLUGIN_DEVELOPMENT.md` | Figma sandbox guide | Before writing code touching Figma APIs |
| `docs/CHANGELOG.md` | Version history | Understanding prior changes |
@@ -277,4 +297,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/README.md b/variables-styles-extractor/README.md
index ff54151..81acb06 100644
--- a/variables-styles-extractor/README.md
+++ b/variables-styles-extractor/README.md
@@ -1,21 +1,33 @@
-# ☕️ Variables & Styles Extractor
+# Variables & Styles Extractor
[](https://www.figma.com/community/plugin/1584331992332668732/variables-and-styles-extractor)
[](./LICENSE)
[](https://www.figma.com/community-free-resource-license/)
-[](./package.json)
+[](./package.json)
-**Export and import Figma variables and styles with full fidelity.**
+**Move your design system anywhere. Export and import Figma variables and styles — selectively, safely, and in Tokens Studio–compatible JSON.**
-> 🔍 **Status:** v2.0.0 published to Figma Community (17 January 2026)
+> 🔍 **Status:** v2.1.0 published to Figma Community · v2.0.0 first published 17 January 2026
+
+Variables & Styles Extractor moves complete design systems between Figma files — every variable collection, mode, alias, and style — as clean, re-importable JSON. It runs **100% locally** (zero network access) and stays responsive on large design systems thanks to a batched processing engine with live progress and a real Cancel button.
+
+## Highlights (v2.1)
+
+- 🧩 **Simple mode, redesigned** — a clean three-section layout: pick variables, pick styles, export. Collections expand into name-prefix groups (just like Figma's Variables panel), so you ship exactly the groups you choose.
+- 🎯 **Group-level selection** — export or import only the variable/style groups you tick, not all-or-nothing.
+- 📦 **Built for large design systems** — batched processing keeps Figma responsive on thousands of variables, with live progress bars and a Cancel button that safely rolls everything back.
+- 🛡️ **Safe import undo** — every import takes a validated snapshot first; undo restores in one click, and a single `Cmd/Ctrl+Z` reverts the whole import natively. A corrupt snapshot can never wipe your file.
+- 🎨 **Three export formats** — Figma JSON (perfect round-trips), W3C Design Tokens, and a Tokens Studio–compatible format (token sets per Collection/Mode, `$themes`, DTCG keys, `{dot.path}` aliases).
+- ⚙️ **Advanced mode** — per-mode selection, merge strategies, pre-import diff review, plan checks, library mapping, and font validation, one toggle away.
+- 🔒 **Private by design** — `networkAccess: none`; your file never leaves Figma.
## Features
### Variables
- ✅ Color, Number, String, Boolean variables
-- ✅ Variable collections with multiple modes (up to 20+)
-- ✅ Variable aliases and references
-- ✅ Library-linked variable detection
+- ✅ Variable collections with multiple modes
+- ✅ Variable aliases and references (resolved or preserved)
+- ✅ Library-linked variable detection + dependency flagging
### Styles
- ✅ Color styles (solid, gradient, image fills)
@@ -24,65 +36,82 @@
- ✅ Grid styles (rows, columns, grid)
- ✅ Multi-paint color styles
+### Export formats
+- ✅ **Figma JSON** — full-fidelity round-trip (default)
+- ✅ **Tokens Studio** — single-file shape with token sets, `$themes`, `$metadata`, DTCG keys
+- ✅ **W3C Design Tokens** — the open DTCG draft standard
+
### Import safety
-- ✅ Automatic rollback on failure
-- ✅ One-click undo for last import
-- ✅ Pre-import snapshots
+- ✅ Validated pre-import snapshot (checked **before** any clearing)
+- ✅ Automatic rollback on failure, with progress
+- ✅ One-click undo + single-step native `Cmd/Ctrl+Z`
- ✅ Smart merge / clean import / custom merge
+### Heavy-load handling
+- ✅ Batched processing engine — stays responsive on large systems
+- ✅ Live progress for export, import, and clear operations
+- ✅ Cooperative Cancel that rolls back safely
+- ✅ Chunked export delivery for very large payloads
+- ✅ Instant paste / lag-free mode switching with multi-MB JSON
+
### Validation
- ✅ Font availability checking
-- ✅ Library connection status
+- ✅ Library connection status + external dependency detection
- ✅ Plan compatibility (Starter/Pro/Org/Enterprise)
-- ✅ External dependency detection
-### Performance
-- ✅ Web Worker JSON parsing (handles 1MB+ files)
-- ✅ Result caching for repeated operations
-- ✅ Skeleton loaders during load
-- ✅ 4-column layout (1200×628 px)
+### Interface
+- ✅ Simple mode (compact 905×628) and Advanced mode (1200×628)
+- ✅ Edge-fade scroll indicators and skeleton loaders
- ✅ Activity log with copy/clear controls
## Installation
### From Figma Community (Recommended)
1. Visit the [plugin page](https://www.figma.com/community/plugin/1584331992332668732/variables-and-styles-extractor)
-2. Click "Try it out" or "Save"
+2. Click "Open in…" / "Save"
3. Open any Figma file
-4. Run: Plugins → Variables & Styles Extractor
+4. Run: Plugins → Variables and Styles Extractor
### From Source
1. Clone this repository
-2. In Figma: Plugins → Development → Import plugin from manifest
-3. Select the `manifest.json` file
+2. In Figma: Plugins → Development → Import plugin from manifest…
+3. Select `variables-styles-extractor/manifest.json`
## Usage
### Export
-1. Open a Figma file with variables/styles
-2. Run the plugin
-3. Go to Export tab
-4. Select collections to export
-5. Click "Export Selected"
+1. Open a Figma file with variables/styles and run the plugin (opens in Simple mode)
+2. Tick the variable collections/groups and style types you want
+3. Pick a format (Figma JSON · Tokens Studio · W3C) and click **Export** — or **Copy JSON**
### Import
-1. Open the target Figma file
-2. Run the plugin
-3. Go to Import tab
-4. Drop JSON file or paste contents
-5. Click "Import"
+1. Open the target Figma file and run the plugin → Import tab
+2. Paste JSON or upload a `.json` file
+3. Tick what to import, then click **Import** (use **Undo Import** to revert)
## Privacy
-- 🔒 No network access - fully local
-- 🔒 No data collection
-- 🔒 Open source
+- 🔒 **No network access** — `networkAccess: none` in the manifest; fully local
+- 🔒 No data collection — your file never leaves Figma
+- 🔒 Open source (MIT)
+
+## Build
+
+```bash
+cd variables-styles-extractor
+pnpm install --frozen-lockfile
+pnpm build:dev # tsc only (readable code.js)
+pnpm build # tsc + terser (minified, shipped) — commit code.js
+```
+
+`code.js` is the checked-in compiled artifact; there is no CI build step, so commit it alongside `src/code.ts` changes. See [`AGENTS.md`](./AGENTS.md) and [`docs/CODING_STANDARDS.md`](./docs/CODING_STANDARDS.md) for the Figma QuickJS/iframe constraints.
## Support
- [Report a Bug](https://github.com/tknatwork/side-kicks/issues/new?template=bug_report.md)
- [Request a Feature](https://github.com/tknatwork/side-kicks/issues/new?template=feature_request.md)
- [Known Issues](./docs/KNOWN_ISSUES.md)
+- [Changelog](./docs/CHANGELOG.md)
## License
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/assets/icon-128.png b/variables-styles-extractor/assets/icon-128.png
new file mode 100644
index 0000000..6d71699
Binary files /dev/null and b/variables-styles-extractor/assets/icon-128.png differ
diff --git a/variables-styles-extractor/assets/logo.svg b/variables-styles-extractor/assets/logo.svg
new file mode 100644
index 0000000..e9d4e88
--- /dev/null
+++ b/variables-styles-extractor/assets/logo.svg
@@ -0,0 +1,9 @@
+
\ No newline at end of file
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...
-
-
-
-
-
-
-
-
-
-
- 📚
- Library Dependencies Detected
-
-
- Some variables reference collections from linked libraries that cannot be exported:
-
-
-
- ⚠️ Important: Only local variables & styles will be exported. Variables that alias library tokens will export their raw values. To preserve library references, ensure the destination file is connected to the same library.
-
-
-
-
-
-
- 🔤
- Font Requirements
-
-
- The exported text styles require these fonts to be installed in the destination file:
-
-
-
- 💡 Tip: Ensure these fonts are installed (via Google Fonts, Adobe Fonts, or local installation) in the system before importing to the destination file.
-
-
-
-
-
-
-
Include Styles (Optional)
-
-
-
-
-
-
-
-
Styles connected to variables will preserve their bindings
-
⚠️ Including images will increase file size significantly
-
-
-
-
-
-
- 📊 Export Preview
-
-
-
-
-
-
-
-
-
Exports variables and selected styles to JSON format
-
-
-
-
Export Result
-
-
-
-
-
-
-
-
-
-
-
-
Import JSON
-
-
📁
-
Drop JSON file here or click to browse
-
-
-
-
Or paste JSON
-
-
-
-
-
-
-
- 📋 Detected Structure
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
📊
-
-
Detected File Plan
-
Professional
-
Up to 10 modes per collection
-
-
-
-
Override Plan (if detection is wrong)
-
- The plugin detected your plan based on existing collections. Select a different plan if incorrect.
-
-
-
- Starter
- 1 mode
-
-
- Pro
- 10 modes
-
-
- Org
- 20 modes
-
-
- Enterprise
- Unlimited
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
☕ Support My Work
-
-
-
-
Hey! I'm an individual designer who builds tools to make life simpler.
-
If this plugin saved you time, consider buying me a coffee. Every tip helps me keep building useful stuff!
@@ -5302,7 +5607,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');
@@ -5338,6 +5643,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
@@ -5351,6 +5657,25 @@
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
+ 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 = {
starter: { maxModes: 1, name: 'Starter', detail: '1 mode only (no multi-mode)' },
@@ -5663,14 +5988,14 @@
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.
${availableCollections.map(col => `
✅
- ${col}
+ ${escapeHtml(col)}
`).join('')}
@@ -5687,19 +6012,19 @@
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.
❌
- ${col} (not connected - will use fallback values)
+ ${escapeHtml(col)} (not connected - will use fallback values)
`).join('')}
@@ -5723,14 +6048,14 @@
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.
${missingCollections.map(col => `
❌
- ${col}
+ ${escapeHtml(col)}
`).join('')}
@@ -5974,13 +6299,13 @@
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.
- ${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.
- ${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.
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
` : ''}
@@ -6353,26 +6678,40 @@
document.querySelectorAll('input[name="user-mode"]').forEach(radio => {
radio.addEventListener('change', (e) => {
currentUserMode = e.target.value;
- if (currentUserMode === 'advanced') {
- document.body.classList.add('advanced-mode');
- // Show library refs option if deps detected
+ // Window follows the mode: Simple is compact (905px), Advanced full (1200px)
+ parent.postMessage({ pluginMessage: { type: 'resize_ui', mode: currentUserMode } }, '*');
+ 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();
}
- } 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();
}
- }
- // Re-render import collections if on selection face
- const selectionFace = document.getElementById('import-selection-face');
- if (selectionFace && selectionFace.style.display !== 'none') {
- renderImportCollectionsList();
- }
+ }, 0);
});
});
@@ -6455,15 +6794,15 @@
let sourceHtml = '';
// Variables info
if (structureStats.totalVariables > 0) {
- sourceHtml += `✅ ${structureStats.totalVariables} Local Variables`;
+ sourceHtml += `✅ ${escapeHtml(structureStats.totalVariables)} Local Variables`;
}
if (structureStats.libraryAliases > 0) {
- sourceHtml += `📚 ${structureStats.libraryAliases} Library References`;
+ sourceHtml += `📚 ${escapeHtml(structureStats.libraryAliases)} Library References`;
}
// Styles info
const totalStyles = (stylesInfo.colorStyles || 0) + (stylesInfo.textStyles || 0) + (stylesInfo.effectStyles || 0) + (stylesInfo.gridStyles || 0);
if (totalStyles > 0) {
- sourceHtml += `✅ ${totalStyles} Local Styles`;
+ sourceHtml += `✅ ${escapeHtml(totalStyles)} Local Styles`;
}
if (sourceHtml) {
assetSourceBanner.classList.remove('hidden');
@@ -6479,13 +6818,13 @@
if (bindingsInfoBanner && bindingsInfoList) {
let bindingsHtml = '';
if (structureStats.totalAliases > 0) {
- bindingsHtml += `🔗 ${structureStats.totalAliases} Variable Aliases`;
+ bindingsHtml += `🔗 ${escapeHtml(structureStats.totalAliases)} Variable Aliases`;
if (structureStats.localAliases > 0) {
- bindingsHtml += `✅ ${structureStats.localAliases} Local`;
+ bindingsHtml += `✅ ${escapeHtml(structureStats.localAliases)} Local`;
}
}
if (structureStats.styleBindings > 0) {
- bindingsHtml += `🎨 ${structureStats.styleBindings} Styles → Variables`;
+ bindingsHtml += `🎨 ${escapeHtml(structureStats.styleBindings)} Styles → Variables`;
}
if (bindingsHtml) {
bindingsInfoBanner.classList.remove('hidden');
@@ -6503,7 +6842,7 @@
if (fontsUsed.length > 0) {
fontsUsedBanner.classList.remove('hidden');
fontsUsedList.innerHTML = fontsUsed.map(f =>
- `🔤 ${f.family} (${f.styles.join(', ')})`
+ `🔤 ${escapeHtml(f.family)} (${escapeHtml(f.styles.join(', '))})`
).join('');
} else {
fontsUsedBanner.classList.add('hidden');
@@ -6518,10 +6857,11 @@
// 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('');
+ depsList.innerHTML = libraryDeps.map(name => `📚 ${escapeHtml(name)}`).join('');
addLog(`📚 Detected library dependencies: ${libraryDeps.join(', ')}`, 'warning');
// Show the export library option checkbox (only in Advanced mode)
@@ -6577,28 +6917,96 @@
}
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 'collection_details':
- updateCollectionDetails(msg.data);
+ 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_complete':
- exportData = msg.data.data;
- document.getElementById('export-output').value = exportData;
-
+ }
+ case 'export_chunk': {
+ // Accumulate streamed export chunks (init on first chunk / seq 0).
+ if (msg.seq === 0 || !window._exportChunks) {
+ window._exportChunks = [];
+ }
+ if (typeof msg.seq === 'number' && msg.seq >= 0) { window._exportChunks[msg.seq >>> 0] = 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('');
+ lastExportFormat = typeof msg.format === 'string' ? msg.format : 'figma';
+
+ 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);
@@ -6607,22 +7015,22 @@
}
}
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 =>
- `🔤 ${f.family} ${f.style}`
+ fontList.innerHTML = reqFonts.map(f =>
+ `🔤 ${escapeHtml(f.family)} ${escapeHtml(f.style)}`
).join('');
addLog(`🔤 Required fonts for import: ${reqFonts.map(f => `${f.family} ${f.style}`).join(', ')}`, 'info');
updateStatusCheckVisibility();
@@ -6630,8 +7038,49 @@
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 '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) {
@@ -6642,7 +7091,7 @@
}
addLog(importMsg, 'success', 'import');
loadCollections();
- document.getElementById('import-input').value = '';
+ setImportRawText('');
document.getElementById('file-input').value = ''; // Reset file input for re-selection
const importBtnComplete = document.getElementById('import-btn');
importBtnComplete.disabled = true;
@@ -6679,29 +7128,15 @@
clearBtnFlash.classList.remove('btn-flash-attention');
}, 2500);
}
- 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);
- }
+
+ // 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 'undo_complete':
+ setOperationInFlight(null);
addLog('✅ Import undone successfully! File restored to previous state.', 'success', 'import');
hideUndoSection();
loadCollections();
@@ -6714,6 +7149,7 @@
}
break;
case 'undo_error':
+ setOperationInFlight(null);
addLog(`❌ Undo failed: ${msg.data.error}`, 'error', 'import');
// Reset undo button
@@ -6724,12 +7160,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';
@@ -6737,8 +7177,12 @@
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':
+ setOperationInFlight(null);
// Automatic rollback failed
addLog(`❌ Rollback failed: ${msg.data.rollbackError}`, 'error', 'import');
addLog('⚠️ ' + msg.data.message, 'warning', 'import');
@@ -6746,8 +7190,12 @@
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':
+ setOperationInFlight(null);
addLog(`✅ Cleared: ${msg.data.message}`, 'success');
loadCollections();
break;
@@ -6799,7 +7247,12 @@
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;
+ setSimpleExportButtonsDisabled(false);
+ resetSimpleImportButton();
break;
}
};
@@ -6827,7 +7280,7 @@
const time = new Date().toLocaleTimeString();
const entry = document.createElement('div');
entry.className = 'log-entry';
- entry.innerHTML = `${time}${message}`;
+ entry.innerHTML = `${escapeHtml(time)}${escapeHtml(message)}`;
// Use requestAnimationFrame for smooth DOM updates
rafUpdate(() => {
@@ -6842,6 +7295,10 @@
});
}
+ function showToast(message, type) {
+ addLog(message, type === 'error' ? 'error' : 'info');
+ }
+
// ========== EXPORT ==========
// Track selected export modes per collection
@@ -6864,28 +7321,29 @@
selectedExportCollections.add(c.name); // Select all by default
// Initialize all modes as selected for this collection
selectedExportModes.set(c.name, new Set(c.modes));
-
+ const cn = escapeHtml(c.name); // names are user-controlled (file/import data)
+
const modeCheckboxes = c.modes.length > 1 ? `