Appearance
Factor Logic
A Figma file holds at most one Systema theme at a time (see Single-theme-in-Figma policy). The push flow enforces this and survives structure changes, theme renames, and re-pushes.
Identity: how the plugin knows what's in the file
figma.root.getPluginData("systema_pushed")— a JSON record{ themeId, themeName, pushedAt, schemaHash }written at the end of every successful push. This is the source of truth for "which theme currently lives in this file".collection.getPluginData("systema_managed") === "true"— Systema's own collections. Anything in the file without this tag is foreign user content and is never touched.collection.getPluginData("systema_theme") === {themeId}— secondary per-collection tag, written alongsidesystema_managed. Used as a fallback whensystema_pushedis empty (legacy tester files).variable.getPluginData("systema_tid")— stable semantic identity computed by the token engine (buildTid([...path-parts])). Lets the plugin track a variable across path renames and structure changes without creating a duplicate.collection.getPluginData("systema_hash")— FNV-1a hash of all tokens in that collection (paths + per-mode values + each token'saliasOfTokenId). Including the alias state in the hash means flipping the Alias duplicate tokens setting actually triggers a re-push instead of being skipped — without that bit, raw-COLOR ↔VARIABLE_ALIASswaps left every collection on disk untouched because the per-mode values themselves were unchanged.
Step-by-step: what happens when you click Push Vars
- Save config —
figma.root.setPluginData("systema_config", JSON.stringify(config)). Shared across all users of the file. - Generate tokens —
generateAllTokensWithStats()produces the flat token array. - Check for theme overwrite — read
systema_pushed. If the storedthemeIddiffers from the theme being pushed, open the Replace theme in Figma? dialog:- Same structure (
schemaHashmatches) → three outcomes:Cancel/Change values only/Replace (unbind).Change values onlyrewrites values in the existing variables so node bindings survive;Replace (unbind)sweeps every Systema collection up front and recreates from scratch (bindings become unbound). - Different structure or no recorded
schemaHash(legacy tester case) →Cancel/Replace (unbind)only. There's no reliable way to reuse existing variables when paths might not match. - Cancel → backend posts
push-complete { success: false }and the UI discards the optimistic push snapshot so Push Vars stays active.
- Same structure (
- Legacy-prefix migration — collections named
{prefix}/color/...or{prefix}/meta(produced by old builds) are renamed to flatcolor/.../meta. Variable ids survive, so existing node bindings keep resolving. - Index variables — only collections tagged with
systema_managedANDsystema_theme === {activeThemeId}are indexed. Every other Systema collection's variables are skipped (OOM-safe on files that still carry legacy multi-theme collections). Foreign user variables are always skipped. - Hash-skip collections — for each target collection, compare the computed hash against
systema_hashon the existing collection. If equal, the whole collection is skipped with no Figma API writes at all. - Sync modes — rename mode 0 to the first theme mode, add missing modes, remove stale modes the user deleted. Every theme mode has a stable id; the engine uses
modeIdMapto translate. - Apply tokens — for every token:
- If a variable with the same path exists in the target collection → reuse.
- Else if a variable in any managed collection carries the same
systema_tid→ rename in place. - Else create a new variable via
figma.variables.createVariable. - Set
scopes(derived fromentityTypeviascopesForEntityType()),description,hiddenFromPublishing, andsystema_tidplugin data. New variables skip the read-before-write diff for these properties to halve the proxy boundary traffic on large first pushes. - For each mode, set the value. Existing variables skip the write when the new value is within ε of the current one (fast diff). Duplicate-color tokens are written as
VARIABLE_ALIASreferences to the canonical variable instead of literal hex —totalAliasedin the summary.
- Cross-collection rebind — when
collectionStructurechanges (e.g. balanced → granular), the Figma API forbids moving variables between collections, so the plugin creates the new variables and then walks file nodes rebindingVARIABLE_ALIASreferences from old → new. Old variables are converted to legacy aliases (zero-value +hiddenFromPublishing) so externally-published library consumers continue to resolve until they can rebind themselves. - Orphan resolution — variables that exist in a managed collection but aren't in the incoming token set. Pure-migration orphans (cross-collection tid matches) and orphans during a replace-theme flow are auto-removed without prompting. Other orphans open a single dialog asking to keep or remove.
- Stale collection cleanup — remove any managed collection that isn't in
byCollection.keys(). This is also how foreign-theme collections get swept on the way out after the user pickedChange values only. - Write
systema_pushed— save{ themeId, themeName, pushedAt: Date.now(), schemaHash }onfigma.root. Postpush-complete { success: true, message }to the UI, which promotes the optimistic push snapshot intolastPushedJsons[themeId]and flipsisPushNeededto false.
STRING variables
The meta collection includes a theme-name variable with resolvedType: "STRING" so tools consuming the published library know which theme is in the file. All other variables are resolvedType: "COLOR".
Last-pushed marker
The UI subscribes to last-pushed messages from the backend (emitted on startup and after every successful push) and stores the record in useConfigStore.lastPushed. The theme-selector dropdown renders the Figma logo on the row whose id matches; hovering it shows a tooltip. No indicator is shown when the active theme and the last-pushed theme coincide — the selector trigger already names it.