@bridge-ds/visual 0.2.3 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capture.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +283 -4
- package/dist/references.d.ts +22 -0
- package/package.json +2 -2
package/dist/capture.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ export interface LaunchOptions {
|
|
|
7
7
|
channel?: string;
|
|
8
8
|
}
|
|
9
9
|
export declare function launch(o?: LaunchOptions): Promise<Browser>;
|
|
10
|
-
export declare function newContext(browser: Browser, viewport: Viewport): Promise<BrowserContext>;
|
|
10
|
+
export declare function newContext(browser: Browser, viewport: Viewport, deviceScaleFactor?: number): Promise<BrowserContext>;
|
|
11
11
|
export declare function newPage(ctx: BrowserContext): Promise<Page>;
|
|
12
12
|
/** Waits for fonts, images, two frames, and a DOM whose markup stops changing (bounded). */
|
|
13
13
|
export declare function settle(page: Page, o?: {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,5 +3,6 @@ export { selectImpact, storiesFromSource, storyId, storyComponent, type SelectIn
|
|
|
3
3
|
export { diffPngs, boxesFromDiff, boxesFromCells, padTo, decodePng, encodePng, type DiffResult } from './diff.js';
|
|
4
4
|
export { COMMENT_MARKER, hasVisualChange, rankShots, renderComment, summarize, summaryLine, type CommentOptions } from './manifest.js';
|
|
5
5
|
export { DEFAULT_ROUTES, DEFAULT_TENANTS, routeUrl, storyUrl, shotKey } from './routes.js';
|
|
6
|
+
export { captureFigmaReferences, type ReferencesOptions, type ReferencesResult } from './references.js';
|
|
6
7
|
export { linkNodeModules } from './workspace.js';
|
|
7
8
|
export type * from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -75,6 +75,47 @@ function visualImpactProblems(raw) {
|
|
|
75
75
|
return out;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
// ../core/src/products.ts
|
|
79
|
+
var DEFAULT_PRODUCTS = [
|
|
80
|
+
{ key: "admin", name: "Admin demo", path: "apps/admin", framework: "react" },
|
|
81
|
+
{ key: "library", name: "Library", default: true }
|
|
82
|
+
];
|
|
83
|
+
var KEY = /^[a-z0-9][a-z0-9-]*$/;
|
|
84
|
+
var REPO = /^[\w.-]+\/[\w.-]+$/;
|
|
85
|
+
var FIELDS = ["key", "name", "repo", "path", "preview", "framework", "default"];
|
|
86
|
+
function productProblems(raw) {
|
|
87
|
+
if (raw === void 0) return [];
|
|
88
|
+
if (!Array.isArray(raw)) return ['"products" must be a list of { "key", "name" } objects'];
|
|
89
|
+
if (!raw.length) return ['"products" must list at least one product, or be left out to use the defaults'];
|
|
90
|
+
const problems = [];
|
|
91
|
+
const seen = /* @__PURE__ */ new Set();
|
|
92
|
+
let defaults = 0;
|
|
93
|
+
raw.forEach((p, i) => {
|
|
94
|
+
const at = `"products[${i}]`;
|
|
95
|
+
if (!p || typeof p !== "object" || Array.isArray(p)) {
|
|
96
|
+
problems.push(`${at}" must be an object with a "key" and a "name"`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const o = p;
|
|
100
|
+
for (const k of Object.keys(o)) if (!FIELDS.includes(k)) problems.push(`${at}.${k}" is not a known field (${FIELDS.join(", ")})`);
|
|
101
|
+
if (typeof o.key !== "string" || !KEY.test(o.key)) problems.push(`${at}.key" must be lowercase letters, digits and dashes, such as "admin"`);
|
|
102
|
+
else if (seen.has(o.key)) problems.push(`${at}.key" ("${o.key}") is used twice: give each product its own key`);
|
|
103
|
+
else seen.add(o.key);
|
|
104
|
+
if (typeof o.name !== "string" || !o.name.trim()) problems.push(`${at}.name" must be the name people read, such as "Admin demo"`);
|
|
105
|
+
if (o.repo !== void 0 && (typeof o.repo !== "string" || !REPO.test(o.repo))) problems.push(`${at}.repo" must be "owner/name"`);
|
|
106
|
+
if (o.path !== void 0) {
|
|
107
|
+
const bad = typeof o.path !== "string" || !o.path.trim() || o.path.startsWith("/") || o.path.split("/").includes("..");
|
|
108
|
+
if (bad) problems.push(`${at}.path" must be a folder relative to the repository root, without a leading "/" or "..", such as "apps/admin"`);
|
|
109
|
+
}
|
|
110
|
+
if (o.preview !== void 0 && (typeof o.preview !== "string" || !/^https?:\/\//.test(o.preview))) problems.push(`${at}.preview" must be an http(s) URL template, such as "https://app-git-{branch}.vercel.app"`);
|
|
111
|
+
if (o.framework !== void 0 && (typeof o.framework !== "string" || !o.framework.trim())) problems.push(`${at}.framework" must be a name such as "react", "vue" or "other"`);
|
|
112
|
+
if (o.default !== void 0 && typeof o.default !== "boolean") problems.push(`${at}.default" must be true or false`);
|
|
113
|
+
if (o.default === true) defaults++;
|
|
114
|
+
});
|
|
115
|
+
if (defaults > 1) problems.push('"products" marks more than one product as default: keep "default": true on the library only');
|
|
116
|
+
return problems;
|
|
117
|
+
}
|
|
118
|
+
|
|
78
119
|
// ../core/src/config.ts
|
|
79
120
|
var CONFIG_FILE = "bridge.config.json";
|
|
80
121
|
var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
|
|
@@ -215,6 +256,7 @@ function defineConfig(input) {
|
|
|
215
256
|
} : {},
|
|
216
257
|
...g.rights ? { rights: normalizeRights(g.rights) } : {}
|
|
217
258
|
},
|
|
259
|
+
products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
|
|
218
260
|
...input.audit ? { audit: { ...input.audit } } : {},
|
|
219
261
|
...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
|
|
220
262
|
...input.integrations ? { integrations: { ...input.integrations } } : {},
|
|
@@ -337,6 +379,7 @@ function loadConfig(source) {
|
|
|
337
379
|
}
|
|
338
380
|
}
|
|
339
381
|
problems.push(...visualImpactProblems(d.visualImpact));
|
|
382
|
+
problems.push(...productProblems(d.products));
|
|
340
383
|
const audit = d.audit;
|
|
341
384
|
if (audit !== void 0) {
|
|
342
385
|
if (!isObj(audit)) problems.push('"audit" must be an object');
|
|
@@ -793,6 +836,148 @@ function impactedModules(index, input) {
|
|
|
793
836
|
return { modules, globalHits };
|
|
794
837
|
}
|
|
795
838
|
|
|
839
|
+
// ../core/src/component-spec.ts
|
|
840
|
+
var COMPONENT_SPECS_VERSION = 1;
|
|
841
|
+
var variantKeyOf = (props) => Object.keys(props).sort().map((k) => `${k}=${props[k]}`).join(", ") || "Default";
|
|
842
|
+
function parseComponentSpecs(text) {
|
|
843
|
+
let data;
|
|
844
|
+
try {
|
|
845
|
+
data = JSON.parse(text);
|
|
846
|
+
} catch {
|
|
847
|
+
throw new Error("component-specs.json is not valid JSON.");
|
|
848
|
+
}
|
|
849
|
+
const f = data;
|
|
850
|
+
if (!f || typeof f !== "object" || !Array.isArray(f.components)) throw new Error('component-specs.json has no "components" list.');
|
|
851
|
+
if (f.version !== COMPONENT_SPECS_VERSION) throw new Error(`component-specs.json is version ${String(f.version)}; this tool reads version ${COMPONENT_SPECS_VERSION}.`);
|
|
852
|
+
return f;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// ../core/src/fidelity-references.ts
|
|
856
|
+
var FIGMA_REFERENCES_DIR = "figma-references";
|
|
857
|
+
var FIGMA_REFERENCES_MANIFEST = `${FIGMA_REFERENCES_DIR}/manifest.json`;
|
|
858
|
+
var FIGMA_REFERENCE_STORY = "internal-figma-reference--variant";
|
|
859
|
+
var FIGMA_FILL_WIDTH_TOKEN = "size.80";
|
|
860
|
+
var STATE_PSEUDO = {
|
|
861
|
+
default: "rest",
|
|
862
|
+
rest: "rest",
|
|
863
|
+
enabled: "rest",
|
|
864
|
+
hover: "hover",
|
|
865
|
+
hovered: "hover",
|
|
866
|
+
pressed: "active",
|
|
867
|
+
active: "active",
|
|
868
|
+
focus: "focus-visible",
|
|
869
|
+
focused: "focus-visible",
|
|
870
|
+
"focus visible": "focus-visible",
|
|
871
|
+
disabled: "disabled"
|
|
872
|
+
};
|
|
873
|
+
var iconName = (component) => component.replace(/^Icon\//, "");
|
|
874
|
+
var exportName = (name) => name.replace(/[^A-Za-z0-9]+/g, "");
|
|
875
|
+
function planReference(spec, variant, file) {
|
|
876
|
+
if (spec.view === "popup") return { ok: false, reason: "drawn open in Figma; the code image shows it closed" };
|
|
877
|
+
const props = { ...spec.fixed ?? {} };
|
|
878
|
+
let pseudo;
|
|
879
|
+
for (const axis of spec.axes) {
|
|
880
|
+
const value = variant.props[axis.name];
|
|
881
|
+
if (value === void 0) return { ok: false, reason: `the variant has no ${axis.name} value` };
|
|
882
|
+
if (axis.prop) {
|
|
883
|
+
props[axis.prop] = value === "true" ? true : value === "false" ? false : value;
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (axis.name === "Checked") {
|
|
887
|
+
props.checked = value === "true";
|
|
888
|
+
if (value === "mixed") props.indeterminate = true;
|
|
889
|
+
continue;
|
|
890
|
+
}
|
|
891
|
+
if (axis.name.toLowerCase() !== "state") return { ok: false, reason: `${axis.name} has no matching option in code` };
|
|
892
|
+
const s = STATE_PSEUDO[value.toLowerCase()];
|
|
893
|
+
if (!s) return { ok: false, reason: `the ${value} state has no code equivalent to render` };
|
|
894
|
+
if (s === "disabled") props.disabled = true;
|
|
895
|
+
else if (s !== "rest") pseudo = s;
|
|
896
|
+
}
|
|
897
|
+
const root = spec.parts.find((p) => !p.parent);
|
|
898
|
+
if (!root) return { ok: false, reason: "the spec has no root layer" };
|
|
899
|
+
const children = (id) => spec.parts.filter((p) => p.parent === id);
|
|
900
|
+
const textOf = (part) => {
|
|
901
|
+
const swapTo = variant.slots?.[part.id] ?? part.swap?.default;
|
|
902
|
+
if (part.swap && swapTo) {
|
|
903
|
+
const slot = file?.slots?.find((s) => s.name === swapTo);
|
|
904
|
+
if (!slot) return { reason: `${swapTo} is not in the specs` };
|
|
905
|
+
if (slot.kind !== "text" || !slot.text) return { reason: `Figma shows a placeholder (${swapTo}) that code has no example of` };
|
|
906
|
+
return slot.text.default;
|
|
907
|
+
}
|
|
908
|
+
const prop = spec.properties.find((p) => p.name === part.text && p.type === "TEXT");
|
|
909
|
+
return typeof prop?.default === "string" ? prop.default : part.name;
|
|
910
|
+
};
|
|
911
|
+
let text;
|
|
912
|
+
const icons = {};
|
|
913
|
+
for (const prop of spec.properties) {
|
|
914
|
+
if (prop.type === "TEXT") {
|
|
915
|
+
const hidden = spec.properties.some((b) => b.type === "BOOLEAN" && b.prop === prop.prop && b.default === false);
|
|
916
|
+
if (hidden && prop.prop) continue;
|
|
917
|
+
if (prop.prop === "children") text = String(prop.default);
|
|
918
|
+
else if (prop.prop) props[prop.prop] = String(prop.default);
|
|
919
|
+
continue;
|
|
920
|
+
}
|
|
921
|
+
const iconPart = spec.parts.find((p) => p.kind === "icon" && p.visible === prop.name);
|
|
922
|
+
if (prop.default === true && !iconPart && prop.prop && props[prop.prop] === void 0 && spec.example?.p?.[prop.prop] === void 0) props[prop.prop] = true;
|
|
923
|
+
}
|
|
924
|
+
for (const part of spec.parts) {
|
|
925
|
+
if (part.kind !== "icon" || !part.swap) continue;
|
|
926
|
+
const shown = part.visible ? spec.properties.find((p) => p.name === part.visible)?.default === true : true;
|
|
927
|
+
if (!shown) continue;
|
|
928
|
+
if (!part.swap.prop) return { ok: false, reason: `the ${part.name} icon has no matching option in code` };
|
|
929
|
+
icons[part.swap.prop] = iconName(variant.slots?.[part.id] ?? part.swap.default);
|
|
930
|
+
}
|
|
931
|
+
let parts;
|
|
932
|
+
const loose = spec.properties.filter((p) => p.type === "TEXT" && !p.prop);
|
|
933
|
+
const frames = children(root.id).filter((p) => p.kind === "frame");
|
|
934
|
+
const compound = text === void 0 && frames.length > 0 && frames.every((f) => spec.parts.some((p) => p.parent === f.id && p.kind === "text" && (p.swap || p.text === f.name)));
|
|
935
|
+
if (compound) {
|
|
936
|
+
parts = [];
|
|
937
|
+
for (const f of frames) {
|
|
938
|
+
const t = spec.parts.find((p) => p.parent === f.id && p.kind === "text");
|
|
939
|
+
const value = textOf(t);
|
|
940
|
+
if (typeof value !== "string") return { ok: false, reason: value.reason };
|
|
941
|
+
parts.push({ component: `${exportName(spec.name)}${exportName(f.name)}`, text: value });
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
for (const p of loose) {
|
|
945
|
+
if (p.name === "Value") continue;
|
|
946
|
+
const part = spec.parts.find((x) => x.text === p.name);
|
|
947
|
+
if (compound && part && part.parent !== root.id) continue;
|
|
948
|
+
if (part && /placeholder$/.test(part.id)) {
|
|
949
|
+
props.placeholder = String(p.default);
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
return { ok: false, reason: `the ${p.name} text has no matching option in code` };
|
|
953
|
+
}
|
|
954
|
+
const example = spec.example;
|
|
955
|
+
const exampleChildren = example?.children?.length ? example.children : void 0;
|
|
956
|
+
return {
|
|
957
|
+
ok: true,
|
|
958
|
+
render: {
|
|
959
|
+
component: exportName(spec.view || spec.fixed || example ? example?.c ?? spec.component ?? spec.name : spec.name),
|
|
960
|
+
props: { ...example?.p ?? {}, ...props },
|
|
961
|
+
...exampleChildren ? { children: exampleChildren } : {},
|
|
962
|
+
...text !== void 0 && !exampleChildren ? { text } : {},
|
|
963
|
+
...parts && !exampleChildren ? { parts } : {},
|
|
964
|
+
...Object.keys(icons).length ? { icons } : {},
|
|
965
|
+
...pseudo ? { pseudo } : {},
|
|
966
|
+
...root.fill ? { width: FIGMA_FILL_WIDTH_TOKEN } : {}
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
}
|
|
970
|
+
function referenceImagePath(component, key) {
|
|
971
|
+
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
972
|
+
const file = key.split(",").map((kv) => slug(kv)).filter(Boolean).join("__");
|
|
973
|
+
return `${slug(component)}/${file || "default"}.png`;
|
|
974
|
+
}
|
|
975
|
+
function planReferences(file) {
|
|
976
|
+
return file.components.flatMap(
|
|
977
|
+
(spec) => spec.variants.map((v) => ({ component: spec.name, specHash: spec.hash, key: variantKeyOf(v.props), plan: planReference(spec, v, file) }))
|
|
978
|
+
);
|
|
979
|
+
}
|
|
980
|
+
|
|
796
981
|
// ../core/src/node/git-local.ts
|
|
797
982
|
import { execFile } from "node:child_process";
|
|
798
983
|
function git(root, args, opts = {}) {
|
|
@@ -838,8 +1023,8 @@ async function launch(o = {}) {
|
|
|
838
1023
|
args: ["--lang=en-US", "--disable-gpu", "--font-render-hinting=none", "--disable-lcd-text", "--force-color-profile=srgb", "--hide-scrollbars", "--disable-skia-runtime-opts", "--disable-renderer-backgrounding"]
|
|
839
1024
|
});
|
|
840
1025
|
}
|
|
841
|
-
async function newContext(browser, viewport) {
|
|
842
|
-
const ctx = await browser.newContext({ viewport, deviceScaleFactor
|
|
1026
|
+
async function newContext(browser, viewport, deviceScaleFactor = 1) {
|
|
1027
|
+
const ctx = await browser.newContext({ viewport, deviceScaleFactor, reducedMotion: "reduce", colorScheme: "light", locale: "en-US", timezoneId: "UTC", serviceWorkers: "block" });
|
|
843
1028
|
await ctx.addInitScript(INIT_SCRIPT);
|
|
844
1029
|
await ctx.route(
|
|
845
1030
|
/^https?:\/\/(?!127\.0\.0\.1[:/]|localhost[:/])/,
|
|
@@ -1272,9 +1457,9 @@ function selectImpact(input) {
|
|
|
1272
1457
|
app
|
|
1273
1458
|
};
|
|
1274
1459
|
}
|
|
1275
|
-
function storyId(title,
|
|
1460
|
+
function storyId(title, exportName2) {
|
|
1276
1461
|
const sanitize = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/-+/g, "-").replace(/^-+/, "").replace(/-+$/, "");
|
|
1277
|
-
const startCase =
|
|
1462
|
+
const startCase = exportName2.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/([A-Z])([A-Z][a-z])/g, "$1 $2").replace(/[_-]+/g, " ").trim();
|
|
1278
1463
|
return `${sanitize(title)}--${sanitize(startCase)}`;
|
|
1279
1464
|
}
|
|
1280
1465
|
function storiesFromSource(files) {
|
|
@@ -1788,12 +1973,106 @@ async function runImpact(o) {
|
|
|
1788
1973
|
log(`${result.summary} Manifest: ${relative2(process.cwd(), result.manifestPath) || result.manifestPath}`);
|
|
1789
1974
|
return result;
|
|
1790
1975
|
}
|
|
1976
|
+
|
|
1977
|
+
// src/references.ts
|
|
1978
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
1979
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
1980
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
1981
|
+
async function captureFigmaReferences(o) {
|
|
1982
|
+
const log = o.log ?? (() => void 0);
|
|
1983
|
+
const scale = o.scale ?? 2;
|
|
1984
|
+
const brand = o.brand ?? "bridge";
|
|
1985
|
+
const theme = o.theme ?? "light";
|
|
1986
|
+
if (!existsSync4(join4(o.storybook, "index.json"))) throw new Error(`No built Storybook in ${o.storybook}: run npm run build-storybook first.`);
|
|
1987
|
+
const index = JSON.parse(await readFile4(join4(o.storybook, "index.json"), "utf8"));
|
|
1988
|
+
if (!index.entries[FIGMA_REFERENCE_STORY]) throw new Error(`The Storybook build has no ${FIGMA_REFERENCE_STORY} story (packages/react/src/stories/FigmaReference.stories.tsx).`);
|
|
1989
|
+
const specs = parseComponentSpecs(await readFile4(o.specs, "utf8"));
|
|
1990
|
+
const jobs = planReferences(specs).filter((j) => !o.only?.length || o.only.includes(j.component));
|
|
1991
|
+
const manifest = {
|
|
1992
|
+
version: 1,
|
|
1993
|
+
tool: "@bridge-ds/visual",
|
|
1994
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1995
|
+
sha: o.sha,
|
|
1996
|
+
scale,
|
|
1997
|
+
mode: { brand, theme, density: "comfortable" },
|
|
1998
|
+
storyId: FIGMA_REFERENCE_STORY,
|
|
1999
|
+
components: {}
|
|
2000
|
+
};
|
|
2001
|
+
for (const j of jobs) manifest.components[j.component] ??= { specHash: j.specHash, variants: {} };
|
|
2002
|
+
let captured = 0;
|
|
2003
|
+
let skipped = 0;
|
|
2004
|
+
const served = await serveDir(o.storybook);
|
|
2005
|
+
const browser = await launch();
|
|
2006
|
+
try {
|
|
2007
|
+
const ctx = await newContext(browser, { width: 1200, height: 800 }, scale);
|
|
2008
|
+
const page = await newPage(ctx);
|
|
2009
|
+
await page.goto(storyUrl(served.url, FIGMA_REFERENCE_STORY, brand, theme), { waitUntil: "load", timeout: 3e4 });
|
|
2010
|
+
await page.waitForSelector("[data-fidelity-root], [data-fidelity-error]", { state: "attached", timeout: 2e4 });
|
|
2011
|
+
await settle(page, { busy: false });
|
|
2012
|
+
log(`Capturing ${jobs.length} variants at ${scale}x (${brand} ${theme})`);
|
|
2013
|
+
for (const j of jobs) {
|
|
2014
|
+
const variants = manifest.components[j.component].variants;
|
|
2015
|
+
if (!j.plan.ok) {
|
|
2016
|
+
variants[j.key] = { skipped: j.plan.reason };
|
|
2017
|
+
skipped++;
|
|
2018
|
+
continue;
|
|
2019
|
+
}
|
|
2020
|
+
const plan = JSON.stringify(j.plan.render);
|
|
2021
|
+
await page.evaluate(
|
|
2022
|
+
({ id, plan: plan2 }) => new Promise((resolve2) => {
|
|
2023
|
+
const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
2024
|
+
const done = () => {
|
|
2025
|
+
clearTimeout(timer);
|
|
2026
|
+
ch.off("storyRendered", done);
|
|
2027
|
+
resolve2();
|
|
2028
|
+
};
|
|
2029
|
+
const timer = setTimeout(done, 1e4);
|
|
2030
|
+
ch.on("storyRendered", done);
|
|
2031
|
+
ch.emit("updateStoryArgs", { storyId: id, updatedArgs: { plan: plan2 } });
|
|
2032
|
+
}),
|
|
2033
|
+
{ id: FIGMA_REFERENCE_STORY, plan }
|
|
2034
|
+
);
|
|
2035
|
+
await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
|
|
2036
|
+
const error = await page.locator("[data-fidelity-error]").first().textContent({ timeout: 200 }).catch(() => null);
|
|
2037
|
+
let target = page.locator("[data-fidelity-root] > *").first();
|
|
2038
|
+
if (!error && !await target.count()) {
|
|
2039
|
+
const marked = page.locator("[data-fidelity-target]").first();
|
|
2040
|
+
await marked.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
|
|
2041
|
+
await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
|
|
2042
|
+
target = marked;
|
|
2043
|
+
}
|
|
2044
|
+
if (error || !await target.count()) {
|
|
2045
|
+
variants[j.key] = { skipped: error ?? "the code example did not render" };
|
|
2046
|
+
skipped++;
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
const file = referenceImagePath(j.component, j.key);
|
|
2050
|
+
const path = join4(o.out, file);
|
|
2051
|
+
await mkdir4(dirname3(path), { recursive: true });
|
|
2052
|
+
await target.screenshot({ path, omitBackground: true, animations: "disabled", caret: "hide", scale: "device" });
|
|
2053
|
+
const box = await target.boundingBox();
|
|
2054
|
+
variants[j.key] = { file, width: Math.round(box?.width ?? 0), height: Math.round(box?.height ?? 0) };
|
|
2055
|
+
captured++;
|
|
2056
|
+
}
|
|
2057
|
+
await ctx.close();
|
|
2058
|
+
} finally {
|
|
2059
|
+
await browser.close().catch(() => void 0);
|
|
2060
|
+
await served.close();
|
|
2061
|
+
}
|
|
2062
|
+
await mkdir4(o.out, { recursive: true });
|
|
2063
|
+
const manifestPath = join4(o.out, "manifest.json");
|
|
2064
|
+
await writeFile4(manifestPath, `${JSON.stringify(manifest, null, 2)}
|
|
2065
|
+
`);
|
|
2066
|
+
log(`${captured} images, ${skipped} variants skipped. ${manifestPath}`);
|
|
2067
|
+
return { manifest, manifestPath, captured, skipped };
|
|
2068
|
+
}
|
|
1791
2069
|
export {
|
|
1792
2070
|
COMMENT_MARKER,
|
|
1793
2071
|
DEFAULT_ROUTES,
|
|
1794
2072
|
DEFAULT_TENANTS,
|
|
1795
2073
|
boxesFromCells,
|
|
1796
2074
|
boxesFromDiff,
|
|
2075
|
+
captureFigmaReferences,
|
|
1797
2076
|
changedTokens,
|
|
1798
2077
|
decodePng,
|
|
1799
2078
|
diffPngs,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type FigmaReferenceManifest } from '@bridge-ds/core';
|
|
2
|
+
export interface ReferencesOptions {
|
|
3
|
+
/** Built Storybook folder (storybook-static). */
|
|
4
|
+
storybook: string;
|
|
5
|
+
/** component-specs.json path. */
|
|
6
|
+
specs: string;
|
|
7
|
+
out: string;
|
|
8
|
+
sha: string;
|
|
9
|
+
scale?: number;
|
|
10
|
+
brand?: string;
|
|
11
|
+
theme?: string;
|
|
12
|
+
/** Only these components (names). */
|
|
13
|
+
only?: string[];
|
|
14
|
+
log?: (line: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface ReferencesResult {
|
|
17
|
+
manifest: FigmaReferenceManifest;
|
|
18
|
+
manifestPath: string;
|
|
19
|
+
captured: number;
|
|
20
|
+
skipped: number;
|
|
21
|
+
}
|
|
22
|
+
export declare function captureFigmaReferences(o: ReferencesOptions): Promise<ReferencesResult>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge-ds/visual",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Bridge visual impact analysis: screenshots the stories and app screens a design system change touches, at the base and head refs, and pixel-diffs them into before/after/diff images and a JSON manifest.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"build:package": "node ../../scripts/build-packages.mjs visual"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@bridge-ds/core": "^0.
|
|
50
|
+
"@bridge-ds/core": "^0.7.0",
|
|
51
51
|
"pixelmatch": "7.1.0",
|
|
52
52
|
"playwright-core": "1.63.0",
|
|
53
53
|
"pngjs": "7.0.0"
|