Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Stopped disabled shades and tints from appearing as variables in the Oxygen and Bricks builders. The generated CSS already left out a color's shades or tints once you switched them off, but the builder variable dropdowns and Oxygen Classic's Alt-click variable panel still listed them, so picking one pointed at a custom property that did not exist.
- Fixed the block editor and Site Editor marking a clean post, template or template part as modified ("Review changes") as soon as it opened. The Core Framework class panel wrote an empty class name to every block before loading its saved classes, which counted as an edit. The panel now starts from the block's saved classes and only writes when a class actually changes.
- Restored the Core Framework dark and light preview toggle in the Bricks 2.4 toolbar. Bricks 2.4 replaced its single toolbar with several position-specific ones, so the toggle never appeared. It now sits last in the toolbar, comes back when Bricks rebuilds its toolbars, works from the keyboard, and follows an "auto" theme preference.
- Fixed local Figma projects disappearing when the plugin reopened. The default presets exceeded Figma's 100 kB storage-entry limit, so variables could update while project storage failed. Local projects now use smaller storage entries, preserve the previous save if a write fails, and report success only after the project and variables have been saved. Existing local projects remain readable. Variable synchronization also leaves collections other than Core Framework untouched.

## [2.0.2] - 2026-08-28

Expand Down
14 changes: 8 additions & 6 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"micromatch": "4.0.8",
"nanoid": "3.3.18",
"postcss": "8.5.26",
"proxy-addr": "2.0.8",
"shell-quote": "1.11.0",
"svgo": "3.3.4",
"websocket-driver": "0.7.5",
"yaml": "1.10.3"
Expand Down
2 changes: 2 additions & 0 deletions packages/figma/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Fork maintainers who publish their own Community listing must create their own p

You can create and save a project locally without any network connection. To synchronize with WordPress, create a connection key in **Core Framework → Figma** on the WordPress site and enter it in the Figma plugin. This key authenticates requests to that WordPress site; it is not a product license. Treat it as a secret and revoke it from WordPress when it is no longer needed.

Local projects are saved in the current Figma file. After reopening the plugin in that file, choose **Use local project** to continue editing. A successful local save confirms both project storage and variable synchronization. Large projects are split into storage entries below Figma's size limit; existing projects saved by earlier versions remain readable. Synchronization updates the **Core Framework** variable collection and leaves other collections alone.

The plugin connects directly to the WordPress origin encoded in that key; project data is not routed through Core Framework servers. The bundled Google Fonts catalog is local. Google is contacted only when a user selects, previews, or imports a Google-hosted font. See Google's [Terms of Service](https://policies.google.com/terms) and [Privacy Policy](https://policies.google.com/privacy).

The plugin no longer supports the legacy hosted web-project IDs or `cfweb:` synchronization keys. Export those projects as `.core` files and import them locally instead.
Expand Down
53 changes: 34 additions & 19 deletions packages/figma/main/code.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { SimpleVariable } from "../src/types";
import { readLocalProject, saveLocalProject } from "./localProjectStorage";
import {
ALLOWED_REST_ROUTES,
PRESET_REST_ROUTE,
Expand All @@ -9,7 +10,6 @@ import {
} from "./wordpressConnection";

const PRESET_API_KEY_STORAGE_KEY = "cf_plugin_project_api_key";
const PRESET_LOCAL_STORAGE_KEY = "cf_plugin_project_local";

interface PresetResponse {
success?: boolean;
Expand Down Expand Up @@ -139,12 +139,15 @@ figma.ui.onmessage = async (msg) => {
await handleWordPressRequest(msg);
break;
}
case "save-project-locally":
case "add-variables": {
const isLocalSave = msg.type === "save-project-locally";
try {
const variables = msg?.variables as SimpleVariable[];
const currentVarNames = variables.map((variable) => variable.variable);
if (isLocalSave) saveLocalProject(figma.root, msg.payload?.preset);
const localCollections = [...(await figma.variables.getLocalVariableCollectionsAsync())];
const localVariables = await figma.variables.getLocalVariablesAsync();
const allLocalVariables = await figma.variables.getLocalVariablesAsync();

let coreFrameworkVariableCollection = localCollections.find(
(collection) => collection.name === CORE_FRAMEWORK_COLLECTION_NAME,
Expand All @@ -157,6 +160,9 @@ figma.ui.onmessage = async (msg) => {
}

const defaultModeId = coreFrameworkVariableCollection.modes[0].modeId;
const localVariables = allLocalVariables.filter(
(variable) => variable.variableCollectionId === coreFrameworkVariableCollection.id,
);

for (const { variable, value, type, webSyntax } of variables) {
try {
Expand All @@ -179,19 +185,31 @@ figma.ui.onmessage = async (msg) => {
console.warn("Failed to set variable");
console.warn({ variable, value, type, webSyntax });
console.error(e);
if (isLocalSave)
throw new Error(`Project saved, but failed to update Figma variable "${variable}".`);
}
}

localVariables.forEach((variableNode) => {
currentVarNames.includes(variableNode.name) || variableNode.remove();
});

figma.ui.postMessage({
type: "added-variables",
});
figma.ui.postMessage(
isLocalSave
? { type: "save-project-locally-response", requestId: msg.requestId, success: true }
: { type: "added-variables" },
);
} catch (e) {
console.warn("Failed to add variables");
console.warn(e);
if (isLocalSave) {
figma.ui.postMessage({
type: "save-project-locally-response",
requestId: msg.requestId,
success: false,
error: e instanceof Error ? e.message : "Failed to save the local Figma project",
});
}
}

break;
Expand All @@ -217,22 +235,19 @@ figma.ui.onmessage = async (msg) => {
}
break;
}
case "save-project-locally": {
const preset = msg.payload?.preset;
if (preset) {
figma.root.setPluginData(PRESET_LOCAL_STORAGE_KEY, JSON.stringify(preset));
}
break;
}
case "get-project-locally": {
const serializedPreset = figma.root.getPluginData(PRESET_LOCAL_STORAGE_KEY);
let preset = null;
if (serializedPreset) {
try {
preset = JSON.parse(serializedPreset);
} catch (error) {
console.warn("Failed to read the local Core Framework project", error);
}
try {
preset = readLocalProject(figma.root);
} catch (error) {
console.warn("Failed to read the local Core Framework project", error);
figma.ui.postMessage({
type: "get-project-locally",
preset: null,
error:
"Could not read the saved local project. Try reopening the plugin before starting a new project.",
});
break;
}
figma.ui.postMessage({ type: "get-project-locally", preset });
break;
Expand Down
81 changes: 81 additions & 0 deletions packages/figma/main/localProjectStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const STORAGE_KEY = "cf_plugin_project_local";
const FORMAT = "core-framework-project-chunks";
// Well below Figma's 100 kB per-entry limit, including UTF-8/UTF-16 and key overhead.
// Do not depend on TextEncoder: it is not available in the Figma main sandbox.
const CHUNK_LENGTH = 16_000;

type PluginDataStore = Pick<PluginDataMixin, "getPluginData" | "setPluginData" | "getPluginDataKeys">;
type Manifest = { format: typeof FORMAT; version: 1; bank: "a" | "b"; chunks: number };

function isManifest(value: unknown): value is Manifest {
const manifest = value as Manifest | null;
return Boolean(
manifest &&
manifest.format === FORMAT &&
manifest.version === 1 &&
(manifest.bank === "a" || manifest.bank === "b") &&
Number.isSafeInteger(manifest.chunks) &&
manifest.chunks > 0,
);
}

function chunkKey(bank: Manifest["bank"], index: number) {
return `${STORAGE_KEY}_${bank}_${index}`;
}

export function readLocalProject(store: PluginDataStore): unknown {
const serialized = store.getPluginData(STORAGE_KEY);
if (!serialized) return null;
const value = JSON.parse(serialized);
// Older versions stored the preset directly in this entry.
if (value?.format !== FORMAT) return value;
if (!isManifest(value)) throw new Error("Invalid local project storage metadata");

const chunks: string[] = [];
for (let index = 0; index < value.chunks; index++) {
const chunk = store.getPluginData(chunkKey(value.bank, index));
if (!chunk) throw new Error("The saved local project is incomplete");
chunks.push(chunk);
}
return JSON.parse(chunks.join(""));
}

export function saveLocalProject(store: PluginDataStore, preset: unknown): void {
if (!preset || typeof preset !== "object") throw new Error("No local project to save");
const serialized = JSON.stringify(preset);
const previous = store.getPluginData(STORAGE_KEY);
let previousManifest: unknown;
try {
previousManifest = previous ? JSON.parse(previous) : null;
} catch {
// A new explicit save may replace unreadable legacy data.
}
const bank = isManifest(previousManifest) && previousManifest.bank === "a" ? "b" : "a";
const keys = new Set<string>();
let index = 0;
for (let start = 0; start < serialized.length; index++) {
let end = Math.min(start + CHUNK_LENGTH, serialized.length);
// Keep surrogate pairs together when Figma encodes each entry separately.
const last = serialized.charCodeAt(end - 1);
if (end < serialized.length && last >= 0xd800 && last <= 0xdbff) end--;
const key = chunkKey(bank, index);
store.setPluginData(key, serialized.slice(start, end));
keys.add(key);
start = end;
}
// Publish only after every chunk is written. A failed write leaves the previous
// manifest (or legacy preset) and its data intact, including across plugin restarts.
const manifest: Manifest = { format: FORMAT, version: 1, bank, chunks: index };
store.setPluginData(STORAGE_KEY, JSON.stringify(manifest));

// Cleanup is best effort: failure here must not turn a committed save into an error.
try {
for (const key of store.getPluginDataKeys()) {
if (/^cf_plugin_project_local_[ab]_\d+$/.test(key) && !keys.has(key)) {
store.setPluginData(key, "");
}
}
} catch (error) {
console.warn("Failed to remove old Core Framework project chunks", error);
}
}
31 changes: 26 additions & 5 deletions packages/figma/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
} from "./utils/frameMessaging";

// Sync variables to Figma - must be outside component to avoid stale closures
function syncVariables(presetData: Preset, colorVariables: ColorVariable[]) {
function syncVariables(presetData: Preset, colorVariables: ColorVariable[], localSaveRequestId?: string) {
if (!presetData || !colorVariables) {
console.error("No preset or color variables");
return;
Expand Down Expand Up @@ -109,7 +109,16 @@ function syncVariables(presetData: Preset, colorVariables: ColorVariable[]) {
);
});

postMessageToParent({ type: "add-variables", variables });
postMessageToParent(
localSaveRequestId
? {
type: "save-project-locally",
requestId: localSaveRequestId,
payload: { preset: presetData },
variables,
}
: { type: "add-variables", variables },
);
}

function App() {
Expand Down Expand Up @@ -158,6 +167,9 @@ function App() {
if (pluginMessage?.type === "wordpress-response") {
postMessageToIframe("cf-figma-wordpress-response", pluginMessage);
}
if (pluginMessage?.type === "save-project-locally-response") {
postMessageToIframe("cf-figma-local-save-response", pluginMessage);
}

if (pluginMessage?.type === "import-project") {
const importedPreset = pluginMessage?.preset as Preset | undefined;
Expand Down Expand Up @@ -192,9 +204,18 @@ function App() {

if (event.data.type === "cf-push-local") {
const payload = event.data.payload;
parent.postMessage({ pluginMessage: { type: "save-project-locally", payload } }, "*");
if (payload?.preset && payload?.colorVariables) {
syncVariables(payload.preset, payload.colorVariables);
try {
if (!payload?.preset || !payload?.colorVariables || !event.data.requestId) {
throw new Error("No local project to save");
}
syncVariables(payload.preset, payload.colorVariables, event.data.requestId);
setPreset(payload.preset);
} catch (error) {
postMessageToIframe("cf-figma-local-save-response", {
requestId: event.data.requestId,
success: false,
error: error instanceof Error ? error.message : "Failed to prepare Figma variables",
});
}
}
};
Expand Down
11 changes: 9 additions & 2 deletions packages/figma/src/components/SelectProjectSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const SelectProjectSection = memo<SelectProjectSection>(({ handleLoadedPr
const [error, setError] = useState<string | null>(null);

const [localPreset, setLocalPreset] = useState<Preset | null>(null);
const [localError, setLocalError] = useState<string | null>(null);
const [showConfirmation, setShowConfirmation] = useState(false);

const onImport = (apiKey: string) => {
Expand Down Expand Up @@ -141,7 +142,7 @@ export const SelectProjectSection = memo<SelectProjectSection>(({ handleLoadedPr
};

const loadOnboarding = () => {
if (localPreset?.id) {
if (localPreset?.id || localError) {
setShowConfirmation(true);
} else {
startNewProject();
Expand Down Expand Up @@ -214,6 +215,7 @@ export const SelectProjectSection = memo<SelectProjectSection>(({ handleLoadedPr
}
case "get-project-locally": {
const preset = pluginMessage.preset;
setLocalError(pluginMessage.error || null);
if (preset) {
setLocalPreset(preset);
}
Expand Down Expand Up @@ -276,9 +278,14 @@ export const SelectProjectSection = memo<SelectProjectSection>(({ handleLoadedPr
title="Start a new project"
content={
<>
{localError && <p className="error">{localError}</p>}
{showConfirmation ? (
<div className="confirmation-inline">
<p>You have a local project. What would you like to do?</p>
<p>
{localError
? "Starting from scratch will replace the saved local project."
: "You have a local project. What would you like to do?"}
</p>
<div className="confirmation-buttons">
<button className="btn-primary" onClick={startNewProject}>
Start from scratch
Expand Down
Loading
Loading