Files
Manavarya09 48b92c794b feat(v8): credibility + distribution release
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.
2026-04-20 03:14:04 +04:00

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 };
}