Variables & Styles Extractor v2.1.0 — Simple-mode redesign, heavy-load engine, Tokens Studio export, brand refresh#23
Merged
Conversation
…bug traps Files removed (recoverable from git history): - backup/ (252KB stale copies), releases/ (580KB frozen snapshots, v2.0.0 copy was already 84 lines stale vs shipped code.js), both byte-identical unreferenced bmc-button PNGs Dead code removed: - code.ts: unused Result<T,E> machinery; dead get_variables/'variables' round-trip (no UI sender/handler); dead 'close' case - ui.html: never-called processInChunks/batchDOMUpdate; shadowed first selectAllImport definition; dead collection_details handler (called an undefined function); unused tempDiv + orphan DocumentFragment; duplicate non-!important .advanced-only CSS block Bug fix: - showToast was called at two error paths but never defined (ReferenceError); added shim routing to the activity log Compliance (Figma plugin guidelines): - manifest.json: dropped unused "currentuser" permission (figma.currentUser appears nowhere in source or shipped artifact) — least privilege code.js rebuilt via the documented pnpm build pipeline (tsc + terser); previous checked-in artifact was unminified tsc output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tch (-279 net lines) Cascade-preserving cleanup of the single style block (every consolidation keeps the declarations the pre-change cascade computed; verified by an adversarial declaration-by-declaration review + before/after rendering of both tabs, both modes, and the tip modal): - One .scrollable utility replaces 6 byte-identical per-container scrollbar groups (8 containers re-classed; the 2 genuinely-different dark/slim scrollbar groups kept standalone) - One shared scroll-fade pattern replaces 5 copies (per-site overrides retain only real differences) - Conflicting duplicate selector pairs resolved to their computed winners: .plugin-footer group, .styles-options (grid wins), .hidden x3 -> one rule (drops the inert content-visibility:hidden, BP-001 family), .empty-state, .loading/.spinner, .log-*, back-to-back .collection-item pair merged Compliance (Figma plugin guidelines): - Removed the Google Fonts <link> — manifest declares networkAccess.allowedDomains ["none"], so the request was CSP-blocked at runtime anyway, and the 'Cookie' family was referenced by zero font-family declarations - contain: content -> contain: layout style at all 3 sites (same containment family as the documented KI-001 invisible-elements bug) - rel="noopener noreferrer" added to all 4 target="_blank" anchors Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simple mode is now a genuine 3-column layout per tab (Advanced is
unchanged — verified pixel-identical, columns reparent both ways):
Export tab: [Variables → name-prefix groups] [Styles → type → groups]
[Activity log + Export + Copy JSON]
Import tab: [Paste/upload JSON] [parsed Variables & Styles → groups]
[Activity log + Import + Undo]
Selection model:
- Collections expand to name-prefix groups (split on first "/"),
no-slash items shown as "(ungrouped)", sorted last — mirrors Figma's
native Variables panel
- selectedExportGroups / selectedImportGroups Maps are the source of
truth; the existing collection-level Sets become projections kept in
sync by write-throughs on the Advanced mutators (one shared state,
both modes agree)
- Tri-state parent checkboxes; selection feeds both Export (download)
and Copy JSON
Backend (code.ts):
- getCollections now emits per-collection `groups` + a sibling
`styleGroups` summary (name+count, zero new API calls)
- exportVariables gains optional selectedGroups / selectedStyleGroups
filters; absent key = export-all, so Advanced (sends null) is
byte-identical
- Import group filtering is 100% UI-side payload pruning; backend
import untouched
Safety: all new rendering routes every interpolated value through a
single escapeHtml() + data-* attributes + delegated listeners — no
onclick string interpolation (verified XSS-safe with a payload-named
collection in both modes). Fixed an Advanced default-style-export
regression surfaced in review. tsc clean; code.js rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The backend (Figma QuickJS VM) previously ran every heavy loop to
completion with zero yielding — large design systems froze Figma during
export/import. Now every heavy path is batched via three QuickJS-safe
runners (runBatched / runBatchedAsync / runSequentialAsync) that yield
to the host between batches, post throttled operation_progress messages
(>=250ms, phase changes always), and check a cooperative cancel flag.
- Progress UI: one component in all four hosts (Simple Section 3 +
above the Advanced action buttons) — spinner, phase label, formatted
counts, bar, Cancel. RAF-coalesced; only phase transitions reach the
activity log.
- Cancellation: 'cancel_operation' handled first in the dispatch,
fully synchronous. Export cancel = clean abandon; import cancel =
snapshot rollback ("file restored"); pre-mutation cancel = no
changes; standalone clear cancel = partial counts + Cmd+Z hint.
Rollback/undo-restore are non-cancellable (half-rollback = data
loss) but still batched so the UI stays alive. Sentinel-property
cancel errors (terser-safe).
- Operation lock: one operation at a time (operation_denied otherwise);
all action buttons in both modes disable while in flight.
- VariableCache: split into rebuildLocal / ensureLibraryIndex (once
per session) / clearLocal. An import now does exactly ONE local scan
instead of up to 4 full rescans, and library indexing only runs when
the payload actually references library tokens.
- Snapshot unification: the UI no longer pre-sends create_undo_snapshot
(double-snapshot freeze + hang risk gone); the backend snapshot rides
back inside import_complete.
- Undo order fix (critical): restoreFromSnapshot now parses and
shape-validates the snapshot BEFORE clearing — a corrupt snapshot
can no longer wipe the file.
- figma.commitUndo() brackets imports and standalone clears, so each
is one atomic native Cmd+Z step (Figma plugin guidelines).
- Chunked export delivery: export JSON streams to the UI in 256KB
export_chunk messages (surrogate-pair-safe splits) + export_done
with count/length integrity checks; replaces the single unbounded
postMessage.
- UI perf: stringify worker routing now keyed on real payload size
(node-count walk, not array length); worker timeout terminates the
stale worker instead of double-parsing; file loads >100MB refused,
>20MB confirmed.
Verified in preview: progress rendering in both modes, cancel
round-trip with rollback messaging, chunked reassembly with emoji
(surrogate) payloads, lock/button recovery on all terminal paths.
tsc clean; code.js rebuilt (tsc + terser).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
New optional export format emitting JSON the Tokens Studio plugin for
Figma imports directly (verified against tokens-studio/figma-plugin's
Zod import schemas + docs.tokens.studio; the compiled converter was
executed against worked examples during review):
- Single-file "shape A" container: one token set per Collection/Mode,
$themes (one per collection x mode; own set enabled, other
collections + style sets as source) and $metadata.tokenSetOrder
- DTCG keys throughout ($value/$type/$description), explicit $type on
every token, Tokens Studio canonical type names; FLOAT/STRING
variables refined by scope (borderRadius/spacing/sizing/opacity/
fontSizes/... when unambiguous); BOOLEAN as "true"/"false" strings
- Colors as #rrggbb / rgba(); aliases as {dot.path} name references
(no set prefix); library aliases fall back to their resolved local
value or are skipped with one aggregated note
- Styles ride in dedicated styles/color, styles/typography (singular
composite sub-keys), styles/effects (boxShadow x/y keys, multi-layer
arrays) sets; grid styles and image paints skipped with log notes
- Collision guards: collections named $themes/$metadata/styles-set
names get suffixed; token-path segments strip {}$ and reject
__proto__/constructor/prototype
Additive guarantee held: the figma (default) and w3c branches diff to
exactly one changed line (the ExportFormatType union). UI: third
option in the Advanced format dropdown + a compact Format select in
Simple Section 3 (default Figma JSON); downloads name the file
tokens.json for this format. tsc clean; code.js rebuilt.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simple-mode sections resize from 378-379px to the Advanced column widths (279/280/280), and the plugin window follows the mode: 905x628 in Simple (the default on open), 1200x628 in Advanced. The UI posts 'resize_ui' with the mode on toggle; the backend accepts only the two known sizes (never arbitrary dimensions from the iframe) via a UI_SIZE constant shared with the initial showUI call. 2px slack retained, mirroring Advanced. Verified in preview at both sizes: no horizontal scroll, correct round-trip messages, body width tracks mode. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Context-harness phase (tracked half — untracked GCC state files were refreshed at the real .gcc paths per the workspace untracked policy): - New START_HERE.md: 60-second boot checklist, hard-constraint recap (QuickJS / CSS sandbox / networkAccess none), build + hot-reload steps, architecture one-pager with the current message protocol, heavy-load model, export formats, and danger zones — with a SYSTEM PAIRING header - AGENTS.md: START_HERE.md added to read order + structure tree; Communication-flow message table replaced with the current protocol (incl. resize_ui, chunked export, progress/cancel, operation lock) and a history note listing the removed 2026-06 messages - docs/CHANGELOG.md: [2.1.0] Unreleased entry (Simple-mode redesign + compact window, progress/cancel, safer undo, Tokens Studio export, perf + cleanup) - docs/KNOWN_ISSUES.md: stale version header fixed; issues resolved by this work marked resolved-in-2.1.0 - docs/TASKS.md: overhaul phases logged; open items = Figma Desktop manual test matrix, version bump, Community publish Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…deps
- The column headers already name each Simple section, so the redundant
inner card headers ("Variables Collections", "Styles", "Load JSON
Data", "Import Contents") are gone; the yellow cards now expand to
the full section space (tighter section padding) so group lists and
the Select All / None buttons get the room
- External-dependency flag: when the file's variables reference team-
library tokens (export side, from the collections payload) or pasted
JSON contains $libraryRef tokens (import side), Simple mode shows a
warning banner with a one-click "Open Advanced" button that performs
the full mode switch (radio + window resize + state projection);
banners clear with their data
Verified in preview: cards fill sections (zero inner headers in the
simple layout), banners appear/disappear with their triggers, Open
Advanced round-trips, Advanced mode untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…, Simple-mode edge fades
Three fixes from real Figma Desktop testing with large design-system files:
1. Processing spinner on paste/upload (Simple import): the Import
Contents section shows a spinner the moment data is pasted, typed,
or uploaded, replaced by the parsed contents when ready.
2. Large-payload load balancing — the root cause of the paste lag AND
the Simple->Advanced switch freeze was multi-megabyte JSON living in
BOTH import textareas (every reflow, including the 905->1200 window
resize, re-laid-out megabytes of text):
- New raw-text store: payloads >300KB never enter the textarea DOM.
A paste interceptor stores the clipboard text directly
(preventDefault — 0ms handler measured vs multi-second insert) and
both textareas show a short read-only placeholder ("Large JSON
loaded (N MB)...") until cleared.
- All parse/skeleton readers now go through getImportRawText();
clear/import-complete/reset paths reset the store + read-only state.
- The mode-switch DOM sync is deferred one tick so the toggle and
window resize paint first (0.3ms click handler measured with a
large system loaded; double-toggle guarded).
3. Edge shadow fades: the Simple-mode group lists (export variables,
export styles, import contents) now have the same scroll edge fades
as the Advanced columns, via the shared scroll-fade-container
pattern + initScrollFade (MutationObserver keeps them current as
lists re-render). Fixed the flex min-height chain so the inner list
(not the whole card) stays the scroll container.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ownward 1. Advanced export spill fix: when an operation starts, any open accordion sharing a .column-footer with a progress host collapses automatically (the Export Options accordion + progress + button no longer overflow the column boundary; verified 14px inside). 2. Custom dropdown panels everywhere: native <select> popups open upward when the OS decides there's more room above. A document-level delegate now intercepts every select in the plugin (including dynamically rendered ones like the per-collection behavior dropdowns) and renders a fixed-position styled panel attached to <body> — always BELOW the control, never clipped by column overflow, max-height clamped to the viewport with internal scroll. Selection drives the underlying select and dispatches change (existing onchange handlers untouched); closes on outside click / Escape / scroll / resize; option labels rendered via textContent (no HTML injection). Keyboard: Enter/Space/ArrowDown opens, Escape closes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- New logo ("Chip Lift"): color chips extracted from a tray on a yellow
tile — neo-brutalist to match the plugin; chosen over a braces motif
to avoid visual conflict with Tokens Studio's brand. Master SVG +
128px icon checked into assets/.
- In-plugin brand lockup in the header's left slot (same 140px
footprint as the old spacer, tabs stay centered).
- Title retires the coffee emoji: "Variables & Styles Extractor v2.1.0"
(the Buy-me-a-coffee tip button keeps its cup).
- Version bump 2.0.0 -> 2.1.0 (package.json, showUI title, ui.html
header strings, ready-log line); README tagline + status refreshed.
Community listing kit (delivered outside the repo, Desktop
vse-plugin-test/brand-assets/): 9 carousel images 1920x1080, icon
128/512, ~22s promo video, LISTING.md with tagline + description.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Publishing an update to the existing Community listing requires the plugin id Figma assigned at first publish (the number in the listing URL: figma.com/community/plugin/1584331992332668732). The manifest shipped with an empty id, which would have published a NEW plugin instead of updating the existing one. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Plugin README: lead with the v2.1 story (Simple mode + group selection, three export formats incl. Tokens Studio, batched heavy-load engine with progress/cancel, validated-snapshot undo, 100% local) and a build section; drop stale 4-column/Web-Worker framing - Root README: mark Variables & Styles Extractor as Published v2.1.0, remove the two deleted projects (nectar-design-toolkit, Design System Builder) from the table + layout, add START_HERE.md and assets/ to the tree Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CodeQL was failing repo-wide (incl. main's scheduled run) with "no source code seen during build": after the earlier project deletions the only JS/TS left is the MINIFIED, checked-in code.js (terser output, which CodeQL skips as generated), so the analysis found no analyzable source and the required check stayed red. Add a CodeQL config that scopes analysis to the real source — variables-styles-extractor/src (TypeScript) and ui.html (inline JS) — and ignores build/output artifacts (code.js, *.min.js, releases/, backup/, marketing-assets/, node_modules/). This is also correct hygiene: security scanning should run on source, not minified output. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…le project Repo is now a single-project repo (nectar-design-toolkit + Design System Builder removed). Bring the AI-builder docs in line with the myportfolio convention and drop removed-project references. AI-doc optimization (mirror myportfolio): - Retire AI_CONTEXT.md at both levels — context lives in the canonical AGENTS.md, not a stale narrative file (myportfolio has none) - Slim both .github/copilot-instructions.md from ~130/105-line duplicates to thin ~20-line redirects (SYSTEM PAIRING header + read order → START_HERE.md → AGENTS.md), no duplicated trees/rules - Repoint pairing-header Index fields off the deleted AI_CONTEXT.md; remove AI_CONTEXT rows from read-orders, doc-index, structure trees, and file-protection tables; repoint docs/GEMINI.md references to AGENTS.md - Canonical (AGENTS.md) + pointer (CLAUDE.md) + boot (START_HERE.md) chain intact at both levels; only historical CHANGELOG entries still mention AI_CONTEXT Single-project reconciliation: - Workspace AGENTS.md + root README: project table/layout reduced to variables-styles-extractor; docs/CHANGELOG.md gains a removal entry - .github/dependabot.yml: dropped the removed Design System Builder + nectar-design-toolkit ecosystems (was spawning stale update PRs) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CodeQL (now scanning the source) flagged 24 pre-existing alerts — 22 high-severity — in the Advanced-mode rendering and message handling. These are exploitable via a malicious imported design-system JSON (collection/style/library/font names and log/file strings flowed unescaped into innerHTML; imported $type/mode keys flowed into object property writes). Remediated all of them; the new Simple-mode code was already escape-safe. XSS (17 sinks → escapeHtml at the interpolation): - addLog now escapes message/type/time (the central import-driven log vector; also clears xss-through-dom + xss-through-exception) - library + font status cards (collection/font names), asset-source and bindings banners, validation warnings, fonts-used / required-fonts banners, library-deps + library-mapping lists, export preview stats + tree, all stat-value spans - renderExportCollections: replaced inline onclick/onchange handlers (where escapeHtml can't help — entities decode before the JS runs) with data-* attributes + one delegated click/change listener; collection/mode names are escaped in every text and attribute context Prototype pollution (6 sites → key guards): - types[v.$type]++ (x3) gated by Object.prototype.hasOwnProperty - export_chunk seq coerced to a non-negative number index - prune newMode[k] / newModes[modeName] reject __proto__/constructor/ prototype keys Verified in preview: malicious collection/mode/library/font names and log messages render as inert text (no alert, zero injected script/img); the refactored Advanced export rows still toggle correctly (row click + per-mode checkbox); no console errors. Only ui.html changed — code.js (compiled from code.ts) is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
3 tasks
The first pass cleared 11 of 24; this clears the last high-severity clusters CodeQL still traced: - renderTypeBadges interpolated the variable-type counts (types.color/float/boolean/string, from the collections message) into innerHTML unescaped — now escapeHtml-wrapped in both the export- and import-side badge renderers. This was the real source CodeQL attributed to the renderExportCollections sink. - pruneImportDataForSimpleSelection now builds its per-mode objects with Object.create(null) (CodeQL-recognized safe target for property injection from imported keys), in addition to the existing explicit __proto__/constructor/prototype key guards. Verified in preview: a malicious "__proto__" mode/key in pasted JSON is dropped, the prototype is not polluted, the pruned payload still JSON-serializes, and valid modes survive; no console errors. Remaining: one MEDIUM js/missing-origin-check on window.onmessage — a Figma plugin iframe cannot meaningfully compare event.origin (messages arrive from the Figma host); the handler already gates on the pluginMessage envelope. To be dismissed with rationale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… writes Round 3 — clears the remaining high-severity CodeQL clusters by making the mitigations recognizable to static analysis: - Font-status and validation cards: escape the last unescaped leaf interpolations (font/style counts, plan name, importing collection/ variable/mode counts) so no tainted path reaches innerHTML - Variable-type counters: replaced the dynamic `types[v.$type]++` (x3) with an explicit color/float/boolean/string if-chain — no user-controlled property name is written at all - export_chunk accumulator: index coerced with `>>> 0` to a non-negative int32 array index Remaining expected alerts are the prune writes to Object.create(null) targets (guarded against __proto__/constructor/prototype) and the window.onmessage origin check — both safe at runtime but not recognizable by CodeQL; these will be dismissed with rationale. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The library status card interpolated ${varCount} (variable count from
the collections message) into innerHTML unescaped across its three
branches — now escapeHtml-wrapped. This clears the last js/xss alerts
(6076/6083).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…riers The inline (k !== '__proto__' && ...) guards on the two prune writes weren't recognised by CodeQL's RemotePropertyInjection barrier-guard analysis. Hoist them to early-return barriers (if (k === '__proto__' || ...) return;) that dominate the writes — the standard recognised form. Targets remain Object.create(null) (the write is a no-op for pollution regardless). Behaviour unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ships v2.1.0 of the Variables & Styles Extractor (already published to the Figma Community listing). 13 commits; both shipped artifacts stay single-file (
code.js,ui.html) and all backend code respects the Figma QuickJS constraints.What's in it
Cleanup & safety
backup/,releases/, duplicate PNGs), dead code paths, and two runtime bug traps (showToastReferenceError, deadcollection_detailshandler)currentusermanifest permission (least privilege)<link>that was CSP-blocked undernetworkAccess: ["none"]; consolidated ~280 lines of duplicate/conflicting CSSSimple-mode redesign (Phase C)
escapeHtml+data-*+ delegated listeners (XSS-safe); external-library dependency flagged with a one-click path to AdvancedHeavy-load engine (Phase D)
runBatched/runBatchedAsync/runSequentialAsync) that yields between batches, with a throttledoperation_progressprotocol rendered in 4 progress hosts and a cooperative CancelrestoreFromSnapshotvalidates before clearing (fixes a wipe-first data-loss bug);figma.commitUndo()brackets for one-step native undo; chunked export delivery; large-payload paste/mode-switch no longer freezes the iframeTokens Studio export (Phase E)
tokens-studio(DTCG profile: token sets per Collection/Mode,$themes,$metadata,{dot.path}aliases) in both tabs — Figma JSON and W3C outputs are byte-identical to beforeBrand + docs (Phase F + brand)
v2.1.0title (coffee emoji retired from the title; tip button keeps its cup)Compliance
Verified against Figma's plugin guidelines (networkAccess governs UI iframe loads; least-privilege permissions; async dynamic-page APIs;
commitUndoundo grouping; cooperative cancellation). Community page shows zero network access.Notes for review
code.jsis the committed compiled artifact (no CI builds it) — rebuilt viapnpm build(tsc + terser) with eachsrc/code.tschangeassets/logo.svg+assets/icon-128.pngare tracked🤖 Generated with Claude Code