Skip to content

Variables & Styles Extractor v2.1.0 — Simple-mode redesign, heavy-load engine, Tokens Studio export, brand refresh#23

Merged
tknatwork merged 20 commits into
mainfrom
claude/nostalgic-euclid-d86723
Jun 10, 2026
Merged

Variables & Styles Extractor v2.1.0 — Simple-mode redesign, heavy-load engine, Tokens Studio export, brand refresh#23
tknatwork merged 20 commits into
mainfrom
claude/nostalgic-euclid-d86723

Conversation

@tknatwork

Copy link
Copy Markdown
Owner

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

  • Removed ~850KB of tracked dead weight (backup/, releases/, duplicate PNGs), dead code paths, and two runtime bug traps (showToast ReferenceError, dead collection_details handler)
  • Dropped the unused currentuser manifest permission (least privilege)
  • Removed the Google Fonts <link> that was CSP-blocked under networkAccess: ["none"]; consolidated ~280 lines of duplicate/conflicting CSS

Simple-mode redesign (Phase C)

  • Genuine 3-section layout per tab with collection→name-prefix-group selection; styles by type→group
  • Compact 905×628 window in Simple, full 1200×628 in Advanced (Advanced is pixel-identical to v2.0.0)
  • All new rendering uses escapeHtml + data-* + delegated listeners (XSS-safe); external-library dependency flagged with a one-click path to Advanced

Heavy-load engine (Phase D)

  • Batched processing (runBatched/runBatchedAsync/runSequentialAsync) that yields between batches, with a throttled operation_progress protocol rendered in 4 progress hosts and a cooperative Cancel
  • Single cache scan per import (was up to 4 full rescans); snapshot unification; restoreFromSnapshot validates 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 iframe

Tokens Studio export (Phase E)

  • Additive third format 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 before

Brand + docs (Phase F + brand)

  • New "Chip Lift" logo + header lockup, clean v2.1.0 title (coffee emoji retired from the title; tip button keeps its cup)
  • READMEs refreshed; root README trimmed to the single active project; START_HERE.md boot doc; manifest carries the Community plugin id for update publishing

Compliance

Verified against Figma's plugin guidelines (networkAccess governs UI iframe loads; least-privilege permissions; async dynamic-page APIs; commitUndo undo grouping; cooperative cancellation). Community page shows zero network access.

Notes for review

  • code.js is the committed compiled artifact (no CI builds it) — rebuilt via pnpm build (tsc + terser) with each src/code.ts change
  • Marketing/brand media (promo video, carousel) is intentionally not in the repo; only the canonical assets/logo.svg + assets/icon-128.png are tracked
  • Verified extensively in a headless browser preview; manually tested in Figma Desktop before publishing

🤖 Generated with Claude Code

tknatwork and others added 14 commits June 10, 2026 14:34
…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>
tknatwork and others added 2 commits June 10, 2026 22:33
…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>
tknatwork and others added 4 commits June 10, 2026 22:55
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>
@tknatwork tknatwork merged commit a5ee31a into main Jun 10, 2026
2 checks passed
@tknatwork tknatwork deleted the claude/nostalgic-euclid-d86723 branch June 11, 2026 01:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant