mirror of
https://github.com/Manavarya09/design-extract.git
synced 2026-09-19 02:41:14 +08:00
48b92c794b
Reliability fixes that restore trust on real sites: - colors.js: brand-color detection rewritten — ranks chromatic clusters by interactiveBg x100 + saturation x2 + log(usage), requires HSL sat >25 or an interactive-bg hit. Linear primary: #d0d6e0 (gray) -> #e4f222 (lime). Stripe: #533afd. Verified on 5 ground-truth sites. - accessibility.js + crawler.js: crawler now emits hasText per element (direct text-node child with visible chars); WCAG extractor filters decorative glyph spans, transparent overlays, non-text containers. Linear: 25% (171 failing pairs) -> 83% (1 failing pair). - scoring.js: rubric recalibrated against Stripe/Linear/Vercel/GitHub/Apple. Loosened color/shadow/radii/type-scale thresholds; softened spacing no-base penalty when tokenization is present; cssHealth now weighted in the overall. Linear 47->76, Stripe 81->88, Apple 83->86. New CLI flags: - --selector <css>: scope extraction to a DOM subtree. Stripe: 2409 -> 112 elements when scoped to footer. Falls back to full doc if invalid. - --system-chrome: channel: 'chrome' in Playwright launch; skips the bundled Chromium download. - --json: full extraction to stdout (already partial in v7, now first-class and documented). New distribution surfaces (each publishes to its own registry): - vscode-extension/: Extract-from-URL + inject-into-workspace commands. - raycast-extension/: Extract, Score, Copy-CLI commands. - figma-plugin/: URL or paste-JSON -> Figma Variables with multi-mode. - github-action/: "Design regression guard" runs designlang on PRs, diffs tokens vs baseline, comments the delta, optional fail-on-change. - smithery.yaml + smithery.dockerfile + docs/MCP-REGISTRY.md: MCP registry submission checklist. - chrome-extension/PRIVACY.md + STORE_LISTING.md: Chrome/Firefox/Edge store listing prep (extension code already existed in v7.1). - docs/LAUNCH.md: PH + Show HN + Twitter copy + day-of checklist. - docs/demo.tape: VHS tape for the animated README hero. README updated: hero references demo.gif with PNG fallback, adds "Install everywhere" table for the new surfaces, documents the three new flags. .npmignore excludes every companion-surface directory so the npm tarball stays 109KB / 73 files. All 275 tests pass.
117 lines
4.0 KiB
JavaScript
117 lines
4.0 KiB
JavaScript
// designlang Figma plugin — imports a `*-figma-variables.json` payload
|
|
// (produced by `designlang <url>`) into Figma Variables.
|
|
//
|
|
// Payload shape (compatible with src/formatters/figma.js):
|
|
// { collections: [{ name, modes: [{ name, variables: [{ name, type, value }] }] }] }
|
|
// — or any superset. Unknown keys are ignored.
|
|
|
|
figma.showUI(__html__, { width: 420, height: 440, themeColors: true });
|
|
|
|
figma.ui.onmessage = async (msg) => {
|
|
if (msg.type === "cancel") {
|
|
figma.closePlugin();
|
|
return;
|
|
}
|
|
if (msg.type !== "import") return;
|
|
|
|
try {
|
|
const data = msg.data;
|
|
const collections = Array.isArray(data?.collections) ? data.collections : [];
|
|
if (collections.length === 0) throw new Error("No `collections` array found in payload.");
|
|
|
|
let varCount = 0;
|
|
let colCount = 0;
|
|
|
|
for (const c of collections) {
|
|
const collection = figma.variables.createVariableCollection(c.name || "designlang");
|
|
colCount++;
|
|
const modes = Array.isArray(c.modes) && c.modes.length > 0 ? c.modes : [{ name: "Default", variables: c.variables || [] }];
|
|
|
|
// First mode: rename the default one Figma auto-creates.
|
|
const primaryMode = modes[0];
|
|
collection.renameMode(collection.modes[0].modeId, primaryMode.name || "Default");
|
|
|
|
const modeIds = [collection.modes[0].modeId];
|
|
for (let i = 1; i < modes.length; i++) {
|
|
modeIds.push(collection.addMode(modes[i].name || `Mode ${i + 1}`));
|
|
}
|
|
|
|
// Create variables from the first mode, then layer in subsequent modes.
|
|
const byName = new Map();
|
|
for (let m = 0; m < modes.length; m++) {
|
|
const mode = modes[m];
|
|
const vars = Array.isArray(mode.variables) ? mode.variables : [];
|
|
for (const v of vars) {
|
|
if (!v || !v.name) continue;
|
|
let variable = byName.get(v.name);
|
|
if (!variable) {
|
|
const type = figmaVarType(v.type);
|
|
variable = figma.variables.createVariable(v.name, collection, type);
|
|
byName.set(v.name, variable);
|
|
varCount++;
|
|
}
|
|
const value = coerceValue(v.type, v.value);
|
|
if (value !== undefined) variable.setValueForMode(modeIds[m], value);
|
|
}
|
|
}
|
|
}
|
|
|
|
figma.ui.postMessage({ type: "done", variableCount: varCount, collectionCount: colCount });
|
|
figma.notify(`designlang: imported ${varCount} variables across ${colCount} collections.`);
|
|
} catch (err) {
|
|
figma.ui.postMessage({ type: "error", message: err.message });
|
|
}
|
|
};
|
|
|
|
function figmaVarType(t) {
|
|
switch ((t || "").toLowerCase()) {
|
|
case "color": return "COLOR";
|
|
case "number":
|
|
case "float":
|
|
case "size":
|
|
case "spacing":
|
|
case "radius": return "FLOAT";
|
|
case "string":
|
|
case "fontfamily": return "STRING";
|
|
case "boolean": return "BOOLEAN";
|
|
default: return "STRING";
|
|
}
|
|
}
|
|
|
|
function coerceValue(type, v) {
|
|
if (v === undefined || v === null) return undefined;
|
|
const t = (type || "").toLowerCase();
|
|
if (t === "color") {
|
|
if (typeof v === "string") return hexToFigmaColor(v);
|
|
if (typeof v === "object" && "r" in v) {
|
|
return {
|
|
r: normChan(v.r),
|
|
g: normChan(v.g),
|
|
b: normChan(v.b),
|
|
a: typeof v.a === "number" ? (v.a > 1 ? v.a / 255 : v.a) : 1,
|
|
};
|
|
}
|
|
}
|
|
if (t === "number" || t === "float" || t === "size" || t === "spacing" || t === "radius") {
|
|
const n = typeof v === "string" ? parseFloat(v) : v;
|
|
return typeof n === "number" && !Number.isNaN(n) ? n : undefined;
|
|
}
|
|
if (t === "boolean") return Boolean(v);
|
|
return String(v);
|
|
}
|
|
|
|
function normChan(c) {
|
|
if (typeof c !== "number") return 0;
|
|
return c > 1 ? c / 255 : c;
|
|
}
|
|
|
|
function hexToFigmaColor(hex) {
|
|
const h = hex.replace("#", "");
|
|
const full = h.length === 3 ? h.split("").map((x) => x + x).join("") : h;
|
|
const r = parseInt(full.slice(0, 2), 16) / 255;
|
|
const g = parseInt(full.slice(2, 4), 16) / 255;
|
|
const b = parseInt(full.slice(4, 6), 16) / 255;
|
|
const a = full.length === 8 ? parseInt(full.slice(6, 8), 16) / 255 : 1;
|
|
return { r, g, b, a };
|
|
}
|