Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d0d1aba
feat: export selection as a Design Bundle (JSON + assets)
AvetosDesign Aug 19, 2026
e66cc9b
docs: document Design Bundle export in the README
AvetosDesign Aug 19, 2026
ff500a2
fix: satisfy oxlint on designBundleTree.ts
AvetosDesign Aug 20, 2026
a025a00
docs: remove internal decision-log and pipeline-stage references from…
AvetosDesign Aug 20, 2026
7ce9238
Address CodeRabbit review feedback on PR #263
AvetosDesign Aug 20, 2026
8249755
feat: add WordPress target tab (UI shell)
AvetosDesign Aug 31, 2026
61205b2
feat: remove standalone Design Bundle export button
AvetosDesign Aug 31, 2026
39d592e
fix: WordPress tab styling, empty-selection gate, and stale turbo bui…
AvetosDesign Aug 31, 2026
aaf1cba
feat: mechanical port of wp-figma-gen's WordPress generation logic
AvetosDesign Aug 31, 2026
3734322
feat: restore selection-to-DesignBundle translation layer (D122)
AvetosDesign Aug 31, 2026
956a476
feat: wire WP Theme download to real generation (D123)
AvetosDesign Aug 31, 2026
1e5477a
fix: remove artificial 10MB cap on WP Theme downloads (D124)
AvetosDesign Aug 31, 2026
8ddb1b0
feat: implement Design Bundle download, closing out Phase 9 (D125)
AvetosDesign Aug 31, 2026
df36976
feat: add Theme Name/Bundle Name field to WordPress tab (D126)
AvetosDesign Aug 31, 2026
6af05bd
feat(backend): CSS optimization Phase A (kind-scoped dedup) + Phase B…
AvetosDesign Sep 1, 2026
30b2022
feat(backend): CSS optimization Phase C -- named-style CSS classes (t…
AvetosDesign Sep 1, 2026
8d7d7e9
docs: document the WordPress target in the README
AvetosDesign Sep 1, 2026
dd03ae4
docs: fix stale comment claiming WP output mode wasn't wired (D133)
AvetosDesign Sep 1, 2026
967fad5
docs: scrub internal jargon from WordPress export comments for upstre…
AvetosDesign Sep 1, 2026
cdf941a
refactor(wordpress): dedupe download flow + extract shared F2C helper…
AvetosDesign Sep 1, 2026
9ea74ac
refactor(wordpress): phase (c) -- gap bug fix, more common/ extractio…
AvetosDesign Sep 1, 2026
5cedc37
refactor(protocol): merge WordPress and project download messages int…
AvetosDesign Sep 2, 2026
bb5beeb
docs: truncate WordPress export comments in pre-existing files (XC33-46)
AvetosDesign Sep 2, 2026
fb363d0
fix(ui): remove WordPress tab's hardcoded green-600 special case (D138)
AvetosDesign Sep 2, 2026
e2f1448
docs: drop "see" from XC tag convention -- (see XCnn) -> (XCnn) (D139)
AvetosDesign Sep 2, 2026
8ca2561
fix(wordpress): dedupe a component's master-definition node against i…
AvetosDesign Sep 2, 2026
2e48226
fix(wordpress): address CodeRabbit review findings on PR #264 (D142)
AvetosDesign Sep 2, 2026
b15ff8a
fix(wordpress): address remaining CodeRabbit nitpicks on PR #264 (D143)
AvetosDesign Sep 2, 2026
c618108
fix(wordpress): fix footer pruning for a single-child (footer-only) r…
AvetosDesign Sep 3, 2026
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
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ The generator is deterministic and runs inside Figma's plugin sandbox. It does n
| Tailwind CSS | HTML, React (JSX), Twig; supports Tailwind 3 and Tailwind 4 |
| Flutter | Full app, stateless widget, or snippet |
| SwiftUI | Preview, `View` struct, or snippet |
| WordPress | WP Theme or Design Bundle, zipped for download (see below) |

The plugin can also package generated code and local image assets into downloadable starters:

Expand All @@ -46,6 +47,13 @@ The plugin can also package generated code and local image assets into downloada

These exports are deliberately small and dependency-light. They are starting points, not generated production applications.

### WordPress export

Alongside the four code targets above, the plugin can generate Page Patterns for a **self-hosted WordPress site** running the block editor (Full Site Editing). Pick "WordPress" as the target, then choose one of two output modes:

- **WP Theme** — a complete, installable WordPress block theme: `theme.json`, `style.css`, a page template with header/footer template parts, and one starter Pattern per top-level layer in your selection, packaged as `theme.zip`. Install it via **Appearance → Themes → Add New → Upload Theme** in WordPress, or unzip it into `wp-content/themes/`.
- **Design Bundle** — the same normalized node tree the code targets convert from, exported instead as a `design-bundle.json` manifest plus an `assets/` folder of exported images, zipped. It's a target-neutral snapshot meant for another tool to read, not for pasting into a site directly — think of it as the "Normalize" stage of [How conversion works](#how-conversion-works) written to disk before any target-specific "Generate" step runs.

## What you can tune

Options appear only when they apply to the selected target:
Expand All @@ -57,8 +65,10 @@ Options appear only when they apply to the selected target:
- Embed images as Base64 in HTML.
- Embed supported vector shapes as SVG for web output.
- Switch between framework-specific file, component, and snippet modes.
- Set the theme or bundle name for WordPress.
- Embed fonts in the theme or bundle for WordPress.

The interface also includes a visual preview, light and dark preview backgrounds, conversion warnings, extracted colors and gradients, code copying, and project downloads.
The interface also includes a visual preview, light and dark preview backgrounds, conversion warnings, extracted colors and gradients, code copying, and project downloads. Note: WordPress targets do not display copyable code; they display theme or bundle information (designs, patterns, assets, fonts) instead.

## How to use it

Expand Down Expand Up @@ -160,14 +170,15 @@ pnpm format:check # Check formatting without writing

### Repository structure

| Path | Purpose |
| -------------------- | ---------------------------------------------------------------------------------------- |
| `packages/backend` | Figma node processing, intermediate representation, code generators, and project exports |
| `packages/plugin-ui` | Shared React interface used by the plugin and interactive website demo |
| `packages/types` | Shared settings, message, preview, and output types |
| `packages/tsconfig` | Shared TypeScript configuration |
| `apps/plugin` | Figma controller and UI entry points; builds `code.js` and `index.html` |
| `apps/web` | Public website, interactive preview, privacy page, and comparison guides |
| Path | Purpose |
| -------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `packages/backend` | Figma node processing, intermediate representation, code generators, and project exports |
| `packages/backend/src/wordpress` | WordPress export — generates a block theme or Design Bundle from the normalized node tree instead of code |
| `packages/plugin-ui` | Shared React interface used by the plugin and interactive website demo |
| `packages/types` | Shared settings, message, preview, and output types |
| `packages/tsconfig` | Shared TypeScript configuration |
| `apps/plugin` | Figma controller and UI entry points; builds `code.js` and `index.html` |
| `apps/web` | Public website, interactive preview, privacy page, and comparison guides |

## Contributing and support

Expand Down
182 changes: 139 additions & 43 deletions apps/plugin/plugin-src/code.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
htmlMain,
extractProjectImageNodeIds,
generateProjectZip,
generateWordPressTheme,
generateDesignBundleZip,
postSettingsChanged,
replaceProjectImagePlaceholders,
} from "backend";
Expand All @@ -18,9 +20,10 @@ import { flutterCodeGenTextStyles } from "backend/src/flutter/flutterMain";
import { htmlCodeGenTextStyles } from "backend/src/html/htmlMain";
import { swiftUICodeGenTextStyles } from "backend/src/swiftui/swiftuiMain";
import {
DownloadProjectFormat,
DownloadFormat,
PluginSettings,
SettingWillChangeMessage,
WordPressOutputMode,
} from "types";

let userPluginSettings: PluginSettings;
Expand All @@ -46,6 +49,11 @@ export const defaultPluginSettings: PluginSettings = {
thresholdPercent: 15,
baseFontFamily: "",
fontFamilyCustomConfig: {},
// WordPress tab defaults (XC1)
wpOutputMode: "theme",
wpIncludeFonts: true,
// Placeholder only (XC2)
wpThemeName: "",
};

// A helper type guard to ensure the key belongs to the PluginSettings type
Expand Down Expand Up @@ -78,6 +86,8 @@ const getUserSettings = async () => {
};

userPluginSettings = updatedPluginSrcSettings as PluginSettings;
// Seed the theme name from the current file's name (XC3)
userPluginSettings.wpThemeName = figma.root.name;
console.log("[DEBUG] getUserSettings - Final settings:", userPluginSettings);
return userPluginSettings;
};
Expand All @@ -92,7 +102,7 @@ const initSettings = async () => {

// Used to prevent running from happening again.
let isLoading = false;
let isDownloadingProject = false;
let isDownloading = false;
let rerunAfterDownload = false;
const safeRun = async (settings: PluginSettings) => {
console.log(
Expand All @@ -101,7 +111,7 @@ const safeRun = async (settings: PluginSettings) => {
"selectionCount =",
figma.currentPage.selection.length,
);
if (isDownloadingProject) {
if (isDownloading) {
rerunAfterDownload = true;
return;
}
Expand Down Expand Up @@ -153,7 +163,7 @@ type ExportedProjectImage = {

const allowedFormatsByFramework: Record<
"Flutter" | "HTML" | "SwiftUI" | "Tailwind",
DownloadProjectFormat[]
DownloadFormat[]
> = {
Flutter: ["flutter"],
HTML: ["html", "nextjs", "vite"],
Expand Down Expand Up @@ -265,7 +275,7 @@ const getConvertedSelectionForDownload = async (

const generateDownloadCode = async (
nodes: readonly SceneNode[],
format: DownloadProjectFormat,
format: DownloadFormat,
pluginSettings: PluginSettings,
) => {
const convertedSelection = await getConvertedSelectionForDownload(
Expand Down Expand Up @@ -317,14 +327,16 @@ const generateDownloadCode = async (
return result?.html || "<div>Failed to generate HTML</div>";
};

const downloadProject = async (format: DownloadProjectFormat) => {
const downloadProject = async (format: DownloadFormat) => {
if (!["flutter", "html", "nextjs", "swiftui", "vite"].includes(format)) {
throw new Error(`Invalid download format: ${format}.`);
}

const pluginSettings = { ...userPluginSettings };
if (
pluginSettings.framework === "Compose" ||
// Safety net (XC33)
pluginSettings.framework === "WordPress" ||
!allowedFormatsByFramework[pluginSettings.framework].includes(format)
) {
throw new Error(
Expand Down Expand Up @@ -373,13 +385,104 @@ const downloadProject = async (format: DownloadProjectFormat) => {
);

figma.ui.postMessage({
type: "project-zip",
type: "zip",
zip,
format,
fileName: `${rootName}-${format}.zip`,
});
};

// F2C's own plugin version (XC5)
const PLUGIN_VERSION = "1.0.0";

const downloadWordPressOutput = async (format: DownloadFormat) => {
// format is one of the two wordpress-* DownloadFormat values (XC34)
const outputMode: WordPressOutputMode =
format === "wordpress-design-bundle" ? "designBundle" : "theme";
const pluginSettings = { ...userPluginSettings };
const selection = [...figma.currentPage.selection];
if (selection.length === 0) {
throw new Error(
outputMode === "theme"
? "Please select at least one layer to generate a WordPress theme from."
: "Please select at least one layer to generate a Design Bundle from.",
);
}

// Both outputs share buildBundleFromSelection (XC6)
const result =
outputMode === "theme"
? await generateWordPressTheme(selection, pluginSettings, {
pluginVersion: PLUGIN_VERSION,
themeName: pluginSettings.wpThemeName,
})
: await generateDesignBundleZip(selection, pluginSettings, {
bundleName: pluginSettings.wpThemeName,
});

// No artificial size ceiling here (XC7)
const zip = result.zip.buffer.slice(
result.zip.byteOffset,
result.zip.byteOffset + result.zip.byteLength,
);

figma.ui.postMessage({
type: "zip",
zip,
format,
fileName: result.fileName,
warnings: result.warnings,
summary: result.summary,
});
};

// Shared guard/try-catch/finally shape for the merged download message
// handler below: both the code-target and WordPress download paths need
// the isLoading/isDownloading guards and the same finally-block rerun
// logic, differing only in two bits of user-facing/log text (the error
// type itself is now shared too -- both flows post the one
// "download-error" message).
const runGuardedDownload = async (
itemLabel: string,
logLabel: string,
action: () => Promise<void>,
) => {
if (isLoading) {
figma.ui.postMessage({
type: "download-error",
error: "Please wait for the current conversion to finish.",
});
return;
}

if (isDownloading) {
figma.ui.postMessage({
type: "download-error",
error: "A project download is already in progress.",
});
return;
}

isDownloading = true;
try {
await action();
} catch (error) {
console.error(`${logLabel} failed:`, error);
figma.ui.postMessage({
type: "download-error",
error: `Failed to create ${itemLabel}: ${
error instanceof Error ? error.message : "Unknown error occurred"
}`,
});
} finally {
isDownloading = false;
if (rerunAfterDownload) {
rerunAfterDownload = false;
void safeRun(userPluginSettings);
}
}
};

const standardMode = async () => {
console.log("[DEBUG] standardMode - Starting standard mode initialization");
figma.showUI(__html__, { width: 450, height: 700, themeColors: true });
Expand Down Expand Up @@ -420,46 +523,39 @@ const standardMode = async () => {

if (msg.type === "ui-ready") {
await initializeOnce();
} else if (msg.type === "download-project") {
if (isLoading) {
figma.ui.postMessage({
type: "project-download-error",
error: "Please wait for the current conversion to finish.",
});
return;
}

if (isDownloadingProject) {
figma.ui.postMessage({
type: "project-download-error",
error: "A project download is already in progress.",
});
return;
}

isDownloadingProject = true;
try {
await downloadProject(msg.format as DownloadProjectFormat);
} catch (error) {
console.error("Download project failed:", error);
figma.ui.postMessage({
type: "project-download-error",
error: `Failed to create project: ${
error instanceof Error ? error.message : "Unknown error occurred"
}`,
});
} finally {
isDownloadingProject = false;
if (rerunAfterDownload) {
rerunAfterDownload = false;
void safeRun(userPluginSettings);
}
}
} else if (msg.type === "download") {
const format = msg.format as DownloadFormat;
const isWordPress =
format === "wordpress-theme" || format === "wordpress-design-bundle";
// Both wordpress-* formats used to share the "WordPress theme"
// label here, so a failed Design Bundle generation surfaced the
// misleading error "Failed to create WordPress theme." The format
// is already known at this point -- use it.
const isDesignBundle = format === "wordpress-design-bundle";
await runGuardedDownload(
isDesignBundle
? "Design Bundle"
: isWordPress
? "WordPress theme"
: "project",
isDesignBundle
? "Design Bundle download"
: isWordPress
? "WordPress theme download"
: "Download project",
() =>
isWordPress
? downloadWordPressOutput(format)
: downloadProject(format),
);
} else if (msg.type === "pluginSettingWillChange") {
const { key, value } = msg as SettingWillChangeMessage<unknown>;
console.log(`[DEBUG] Setting changed: ${key} = ${value}`);
(userPluginSettings as any)[key] = value;
figma.clientStorage.setAsync("userPluginSettings", userPluginSettings);
// Do not persist wpThemeName (XC9)
const settingsToPersist = { ...userPluginSettings };
delete (settingsToPersist as any).wpThemeName;
figma.clientStorage.setAsync("userPluginSettings", settingsToPersist);
safeRun(userPluginSettings);
} else if (msg.type === "get-selection-json") {
console.log("[DEBUG] get-selection-json message received");
Expand Down
Loading