@bridge-ds/visual 0.3.12 → 0.3.14
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/LICENSE +110 -0
- package/dist/capture.d.ts +10 -0
- package/dist/diff.d.ts +2 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +122 -22
- package/package.json +3 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, MIT Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-MIT
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Florian Gouloubi
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the MIT license that is effective on the second anniversary of the date we make
|
|
91
|
+
the Software available. On or after that date, you may use the Software under
|
|
92
|
+
the MIT license, in which case the following will apply:
|
|
93
|
+
|
|
94
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
95
|
+
this software and associated documentation files (the "Software"), to deal in
|
|
96
|
+
the Software without restriction, including without limitation the rights to
|
|
97
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
|
98
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
|
99
|
+
so, subject to the following conditions:
|
|
100
|
+
|
|
101
|
+
The above copyright notice and this permission notice shall be included in all
|
|
102
|
+
copies or substantial portions of the Software.
|
|
103
|
+
|
|
104
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
105
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
106
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
107
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
108
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
109
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
110
|
+
SOFTWARE.
|
package/dist/capture.d.ts
CHANGED
|
@@ -38,3 +38,13 @@ export declare function shoot(page: Page, r: ShotRequest): Promise<string | null
|
|
|
38
38
|
* click a story), much faster than a page load per story. Resolves once the story rendered or failed.
|
|
39
39
|
*/
|
|
40
40
|
export declare function switchStory(page: Page, storyId: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* A page for `bridge specs --measure`: an installed Chrome, one page at 1200x800, light scheme, reduced motion. Unlike
|
|
43
|
+
* the capture context it reaches remote hosts (a team's deployed Storybook). Close it when done.
|
|
44
|
+
*/
|
|
45
|
+
export declare function openMeasurePage(o?: LaunchOptions & {
|
|
46
|
+
scale?: number;
|
|
47
|
+
}): Promise<{
|
|
48
|
+
page: Page;
|
|
49
|
+
close: () => Promise<void>;
|
|
50
|
+
}>;
|
package/dist/diff.d.ts
CHANGED
|
@@ -12,6 +12,8 @@ export interface DiffResult {
|
|
|
12
12
|
}
|
|
13
13
|
export declare const decodePng: (buf: Buffer) => PNG;
|
|
14
14
|
export declare const encodePng: (png: PNG) => Buffer;
|
|
15
|
+
/** The `x, y, width, height` region of `src` (px of the image), clipped to it. */
|
|
16
|
+
export declare function cropPng(src: PNG, x: number, y: number, width: number, height: number): PNG;
|
|
15
17
|
/** Copies `src` onto a `width` x `height` canvas filled with an opaque neutral, so sizes can differ. */
|
|
16
18
|
export declare function padTo(src: PNG, width: number, height: number): PNG;
|
|
17
19
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export { runImpact, planImpact, changedTokens, type ImpactOptions, type ImpactResult, type Plan } from './run.js';
|
|
2
2
|
export { selectImpact, storiesFromSource, storyId, storyComponent, type SelectInput } from './select.js';
|
|
3
|
-
export { diffPngs, boxesFromDiff, boxesFromCells, padTo, decodePng, encodePng, type DiffResult } from './diff.js';
|
|
3
|
+
export { diffPngs, boxesFromDiff, boxesFromCells, padTo, cropPng, 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
6
|
export { captureFigmaReferences, type ReferencesOptions, type ReferencesResult } from './references.js';
|
|
7
7
|
export { linkNodeModules } from './workspace.js';
|
|
8
|
+
export { openMeasurePage } from './capture.js';
|
|
8
9
|
export type * from './types.js';
|
package/dist/index.js
CHANGED
|
@@ -121,6 +121,12 @@ var CONFIG_FILE = "bridge.config.json";
|
|
|
121
121
|
var CHANGES_MODES = ["suggest", "direct"];
|
|
122
122
|
var DEFAULT_CHANGES_MODE = "suggest";
|
|
123
123
|
var DEFAULT_OVERRIDE = { allowed: [], requireReason: true, minReasonLength: 20 };
|
|
124
|
+
var TEAM_NAME_MAX = 60;
|
|
125
|
+
function teamName(value) {
|
|
126
|
+
if (typeof value !== "string") return void 0;
|
|
127
|
+
const v = value.trim().replace(/\s+/g, " ");
|
|
128
|
+
return v && v.length <= TEAM_NAME_MAX ? v : void 0;
|
|
129
|
+
}
|
|
124
130
|
var DEFAULT_TOKEN_PATHS = {
|
|
125
131
|
root: "tokens",
|
|
126
132
|
primitives: "primitives",
|
|
@@ -226,6 +232,7 @@ function defineConfig(input) {
|
|
|
226
232
|
const g = input.governance ?? {};
|
|
227
233
|
const contrastFile = contrastPath ?? [root === "." ? "" : root, paths.contrastChecks].filter(Boolean).join("/");
|
|
228
234
|
return {
|
|
235
|
+
...teamName(input.name) ? { name: teamName(input.name) } : {},
|
|
229
236
|
repo: parseRepoInput(input.repo, input.branch),
|
|
230
237
|
tokens: { ...paths, include: input.tokens?.include ?? ["**/*.tokens.json"], exclude: input.tokens?.exclude ?? [], tiers, layers, ...t.usage ? { usage: trimSlashes(t.usage) } : {} },
|
|
231
238
|
axes,
|
|
@@ -258,12 +265,14 @@ function defineConfig(input) {
|
|
|
258
265
|
}
|
|
259
266
|
} : {},
|
|
260
267
|
...g.rights ? { rights: normalizeRights(g.rights) } : {},
|
|
261
|
-
...g.soloOwnerCanApproveBoth === true ? { soloOwnerCanApproveBoth: true } : {}
|
|
268
|
+
...g.soloOwnerCanApproveBoth === true ? { soloOwnerCanApproveBoth: true } : {},
|
|
269
|
+
...g.leadsPublishTier1 === false ? { leadsPublishTier1: false } : {}
|
|
262
270
|
},
|
|
263
271
|
products: input.products?.length ? input.products.map((p) => ({ ...p })) : DEFAULT_PRODUCTS.map((p) => ({ ...p })),
|
|
264
272
|
changes: input.changes === "direct" ? "direct" : DEFAULT_CHANGES_MODE,
|
|
265
273
|
...input.storybook?.url ? { storybook: { url: input.storybook.url.trim().replace(/\/+$/, "") } } : {},
|
|
266
274
|
...input.library?.public === true ? { library: { public: true } } : {},
|
|
275
|
+
...input.demo?.viewer?.trim() ? { demo: { viewer: input.demo.viewer.trim(), ...input.demo.role?.trim() ? { role: input.demo.role.trim() } : {} } } : {},
|
|
267
276
|
...input.audit ? { audit: { ...input.audit } } : {},
|
|
268
277
|
...input.visualImpact ? { visualImpact: { mode: input.visualImpact.mode === "on-demand" ? "on-demand" : "auto" } } : {},
|
|
269
278
|
...input.integrations ? { integrations: { ...input.integrations } } : {},
|
|
@@ -385,9 +394,11 @@ function loadConfig(source) {
|
|
|
385
394
|
}
|
|
386
395
|
problems.push(...rightsProblems(gov.rights));
|
|
387
396
|
if (gov.soloOwnerCanApproveBoth !== void 0 && typeof gov.soloOwnerCanApproveBoth !== "boolean") problems.push('"governance.soloOwnerCanApproveBoth" must be true or false');
|
|
397
|
+
if (gov.leadsPublishTier1 !== void 0 && typeof gov.leadsPublishTier1 !== "boolean") problems.push('"governance.leadsPublishTier1" must be true or false');
|
|
388
398
|
}
|
|
389
399
|
}
|
|
390
400
|
problems.push(...visualImpactProblems(d.visualImpact));
|
|
401
|
+
if (d.name !== void 0 && teamName(d.name) === void 0) problems.push(`"name" must be the team's name, 1 to ${TEAM_NAME_MAX} characters`);
|
|
391
402
|
if (d.changes !== void 0 && !CHANGES_MODES.includes(d.changes)) problems.push(`"changes" must be "suggest" or "direct" (got ${JSON.stringify(d.changes)})`);
|
|
392
403
|
const sb = d.storybook;
|
|
393
404
|
if (sb !== void 0) {
|
|
@@ -403,6 +414,14 @@ function loadConfig(source) {
|
|
|
403
414
|
for (const k of Object.keys(lib)) if (k !== "public") problems.push(`"library.${k}" is not a known key`);
|
|
404
415
|
}
|
|
405
416
|
}
|
|
417
|
+
const demo = d.demo;
|
|
418
|
+
if (demo !== void 0) {
|
|
419
|
+
if (!isObj(demo) || !isStr(demo.viewer) || !demo.viewer.trim()) problems.push('"demo" must be { "viewer" }, for example { "viewer": "Dana", "role": "DS lead" }');
|
|
420
|
+
else {
|
|
421
|
+
if (demo.role !== void 0 && !isStr(demo.role)) problems.push('"demo.role" must be a string, for example "DS lead"');
|
|
422
|
+
for (const k of Object.keys(demo)) if (k !== "viewer" && k !== "role") problems.push(`"demo.${k}" is not a known key`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
406
425
|
problems.push(...productProblems(d.products));
|
|
407
426
|
const audit = d.audit;
|
|
408
427
|
if (audit !== void 0) {
|
|
@@ -897,7 +916,7 @@ var STATE_PSEUDO = {
|
|
|
897
916
|
var iconName = (component) => component.replace(/^Icon\//, "");
|
|
898
917
|
var exportName = (name) => name.replace(/[^A-Za-z0-9]+/g, "");
|
|
899
918
|
function planReference(spec, variant, file) {
|
|
900
|
-
if (spec.view === "popup") return
|
|
919
|
+
if (spec.view === "popup") return planPopup(spec, variant);
|
|
901
920
|
const props = { ...spec.fixed ?? {} };
|
|
902
921
|
let pseudo;
|
|
903
922
|
for (const axis of spec.axes) {
|
|
@@ -926,6 +945,7 @@ function planReference(spec, variant, file) {
|
|
|
926
945
|
if (part.swap && swapTo) {
|
|
927
946
|
const slot = file?.slots?.find((s) => s.name === swapTo);
|
|
928
947
|
if (!slot) return { reason: `${swapTo} is not in the specs` };
|
|
948
|
+
if (slot.kind === "media") return { reason: `Figma shows a media placeholder (${swapTo}); code shows the image it is given, nothing to compare` };
|
|
929
949
|
if (slot.kind !== "text" || !slot.text) return { reason: `Figma shows a placeholder (${swapTo}) that code has no example of` };
|
|
930
950
|
return slot.text.default;
|
|
931
951
|
}
|
|
@@ -977,10 +997,13 @@ function planReference(spec, variant, file) {
|
|
|
977
997
|
}
|
|
978
998
|
const example = spec.example;
|
|
979
999
|
const exampleChildren = example?.children?.length ? example.children : void 0;
|
|
1000
|
+
const component = exportName(example?.c ?? (spec.name.includes("/") ? spec.component ?? spec.name : spec.name));
|
|
1001
|
+
const parent = spec.component ? exportName(spec.component) : void 0;
|
|
980
1002
|
return {
|
|
981
1003
|
ok: true,
|
|
982
1004
|
render: {
|
|
983
|
-
component
|
|
1005
|
+
component,
|
|
1006
|
+
...parent && parent !== component ? { within: parent } : {},
|
|
984
1007
|
props: { ...example?.p ?? {}, ...props },
|
|
985
1008
|
...exampleChildren ? { children: exampleChildren } : {},
|
|
986
1009
|
...text !== void 0 && !exampleChildren ? { text } : {},
|
|
@@ -991,6 +1014,28 @@ function planReference(spec, variant, file) {
|
|
|
991
1014
|
}
|
|
992
1015
|
};
|
|
993
1016
|
}
|
|
1017
|
+
function planPopup(spec, variant) {
|
|
1018
|
+
const root = spec.parts.find((p) => !p.parent);
|
|
1019
|
+
const own = {};
|
|
1020
|
+
for (const axis of spec.axes) {
|
|
1021
|
+
const value = variant.props[axis.name];
|
|
1022
|
+
if (!axis.prop || value === void 0) return { ok: false, reason: `${axis.name} has no matching option in code` };
|
|
1023
|
+
own[axis.prop] = value === "true" ? true : value === "false" ? false : value;
|
|
1024
|
+
}
|
|
1025
|
+
const className = /\.module\.css \.([\w-]+)/.exec(root?.source ?? "")?.[1];
|
|
1026
|
+
const component = spec.component ? exportName(spec.component) : void 0;
|
|
1027
|
+
const example = spec.example;
|
|
1028
|
+
if (!component || !example) return { ok: false, reason: "drawn open from no code example: nothing to open in code" };
|
|
1029
|
+
if (!className) return { ok: false, reason: "the popup has no class to find it by in code" };
|
|
1030
|
+
const popup = { className };
|
|
1031
|
+
if (exportName(example.c) === component) {
|
|
1032
|
+
return { ok: true, render: { component, props: { ...example.p ?? {}, ...own, defaultOpen: true }, ...example.children?.length ? { children: example.children } : {}, popup } };
|
|
1033
|
+
}
|
|
1034
|
+
return {
|
|
1035
|
+
ok: true,
|
|
1036
|
+
render: { component, props: { defaultOpen: true }, children: [{ c: `${component}Trigger`, children: ["Open"] }, { ...example, p: { ...example.p ?? {}, ...own } }], popup }
|
|
1037
|
+
};
|
|
1038
|
+
}
|
|
994
1039
|
function referenceImagePath(component, key) {
|
|
995
1040
|
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
996
1041
|
const file = key.split(",").map((kv) => slug(kv)).filter(Boolean).join("__");
|
|
@@ -1214,6 +1259,17 @@ async function switchStory(page, storyId2) {
|
|
|
1214
1259
|
);
|
|
1215
1260
|
if (!ok) throw new Error("Storybook channel not found");
|
|
1216
1261
|
}
|
|
1262
|
+
async function openMeasurePage(o = {}) {
|
|
1263
|
+
const browser = await launch(o);
|
|
1264
|
+
try {
|
|
1265
|
+
const ctx = await browser.newContext({ viewport: { width: 1200, height: 800 }, deviceScaleFactor: o.scale ?? 1, reducedMotion: "reduce", colorScheme: "light", locale: "en-US", serviceWorkers: "block" });
|
|
1266
|
+
const page = await ctx.newPage();
|
|
1267
|
+
return { page, close: () => browser.close().catch(() => void 0) };
|
|
1268
|
+
} catch (e) {
|
|
1269
|
+
await browser.close().catch(() => void 0);
|
|
1270
|
+
throw e;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1217
1273
|
|
|
1218
1274
|
// src/composite.ts
|
|
1219
1275
|
import { readFile, writeFile as writeFile2 } from "node:fs/promises";
|
|
@@ -1265,6 +1321,15 @@ import pixelmatch from "pixelmatch";
|
|
|
1265
1321
|
import { PNG } from "pngjs";
|
|
1266
1322
|
var decodePng = (buf) => PNG.sync.read(buf);
|
|
1267
1323
|
var encodePng = (png) => PNG.sync.write(png, { colorType: 6 });
|
|
1324
|
+
function cropPng(src, x, y, width, height) {
|
|
1325
|
+
const left = Math.max(0, Math.round(x));
|
|
1326
|
+
const top = Math.max(0, Math.round(y));
|
|
1327
|
+
const w = Math.max(1, Math.min(src.width - left, Math.round(width)));
|
|
1328
|
+
const h = Math.max(1, Math.min(src.height - top, Math.round(height)));
|
|
1329
|
+
const out = new PNG({ width: w, height: h });
|
|
1330
|
+
PNG.bitblt(src, out, left, top, w, h, 0, 0);
|
|
1331
|
+
return out;
|
|
1332
|
+
}
|
|
1268
1333
|
function padTo(src, width, height) {
|
|
1269
1334
|
if (src.width === width && src.height === height) return src;
|
|
1270
1335
|
const out = new PNG({ width, height });
|
|
@@ -2105,9 +2170,12 @@ async function captureFigmaReferences(o) {
|
|
|
2105
2170
|
try {
|
|
2106
2171
|
const ctx = await newContext(browser, { width: 1200, height: 800 }, scale);
|
|
2107
2172
|
const page = await newPage(ctx);
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2173
|
+
const openStory = async () => {
|
|
2174
|
+
await page.goto(storyUrl(served.url, FIGMA_REFERENCE_STORY, brand, theme), { waitUntil: "load", timeout: 3e4 });
|
|
2175
|
+
await page.waitForSelector("[data-fidelity-root], [data-fidelity-error]", { state: "attached", timeout: 2e4 });
|
|
2176
|
+
await settle(page, { busy: false });
|
|
2177
|
+
};
|
|
2178
|
+
await openStory();
|
|
2111
2179
|
log(`Capturing ${jobs.length} variants at ${scale}x (${brand} ${theme})`);
|
|
2112
2180
|
for (const j of jobs) {
|
|
2113
2181
|
const variants = manifest.components[j.component].variants;
|
|
@@ -2116,25 +2184,17 @@ async function captureFigmaReferences(o) {
|
|
|
2116
2184
|
skipped++;
|
|
2117
2185
|
continue;
|
|
2118
2186
|
}
|
|
2119
|
-
const
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
resolve2();
|
|
2127
|
-
};
|
|
2128
|
-
const timer = setTimeout(done, 1e4);
|
|
2129
|
-
ch.on("storyRendered", done);
|
|
2130
|
-
ch.emit("updateStoryArgs", { storyId: id, updatedArgs: { plan: plan2 } });
|
|
2131
|
-
}),
|
|
2132
|
-
{ id: FIGMA_REFERENCE_STORY, plan }
|
|
2133
|
-
);
|
|
2187
|
+
const shown = await renderPlan(page, JSON.stringify(j.plan.render));
|
|
2188
|
+
if (shown.broken) {
|
|
2189
|
+
variants[j.key] = { skipped: `the code example threw: ${shown.error ?? "the story did not finish rendering"}` };
|
|
2190
|
+
skipped++;
|
|
2191
|
+
await openStory();
|
|
2192
|
+
continue;
|
|
2193
|
+
}
|
|
2134
2194
|
await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
|
|
2135
2195
|
const error = await page.locator("[data-fidelity-error]").first().textContent({ timeout: 200 }).catch(() => null);
|
|
2136
2196
|
let target = page.locator("[data-fidelity-root] > *").first();
|
|
2137
|
-
if (!error && !await target.count()) {
|
|
2197
|
+
if (!error && (j.plan.render.within || j.plan.render.popup || !await target.count())) {
|
|
2138
2198
|
const marked = page.locator("[data-fidelity-target]").first();
|
|
2139
2199
|
await marked.waitFor({ state: "visible", timeout: 3e3 }).catch(() => void 0);
|
|
2140
2200
|
await settle(page, { network: false, quiet: 2, interval: 40, busy: false });
|
|
@@ -2145,6 +2205,15 @@ async function captureFigmaReferences(o) {
|
|
|
2145
2205
|
skipped++;
|
|
2146
2206
|
continue;
|
|
2147
2207
|
}
|
|
2208
|
+
const transparent = await target.evaluate((el) => {
|
|
2209
|
+
for (let e = el; e; e = e.parentElement) if (getComputedStyle(e).opacity === "0") return true;
|
|
2210
|
+
return false;
|
|
2211
|
+
});
|
|
2212
|
+
if (transparent) {
|
|
2213
|
+
variants[j.key] = { skipped: "the code example rendered invisible (opacity 0)" };
|
|
2214
|
+
skipped++;
|
|
2215
|
+
continue;
|
|
2216
|
+
}
|
|
2148
2217
|
const file = referenceImagePath(j.component, j.key);
|
|
2149
2218
|
const path = join4(o.out, file);
|
|
2150
2219
|
await mkdir4(dirname3(path), { recursive: true });
|
|
@@ -2165,6 +2234,35 @@ async function captureFigmaReferences(o) {
|
|
|
2165
2234
|
log(`${captured} images, ${skipped} variants skipped. ${manifestPath}`);
|
|
2166
2235
|
return { manifest, manifestPath, captured, skipped };
|
|
2167
2236
|
}
|
|
2237
|
+
async function renderPlan(page, plan) {
|
|
2238
|
+
const r = await page.evaluate(
|
|
2239
|
+
({ id, plan: plan2 }) => new Promise((resolve2) => {
|
|
2240
|
+
const ch = window.__STORYBOOK_ADDONS_CHANNEL__;
|
|
2241
|
+
let error;
|
|
2242
|
+
const failed = (p) => {
|
|
2243
|
+
const e = p;
|
|
2244
|
+
error ??= e?.message ?? e?.title ?? e?.description;
|
|
2245
|
+
};
|
|
2246
|
+
const finish = (rendered) => {
|
|
2247
|
+
clearTimeout(timer);
|
|
2248
|
+
ch.off("storyRendered", done);
|
|
2249
|
+
ch.off("storyThrewException", failed);
|
|
2250
|
+
ch.off("storyErrored", failed);
|
|
2251
|
+
resolve2({ rendered, ...error ? { error } : {} });
|
|
2252
|
+
};
|
|
2253
|
+
const done = () => finish(true);
|
|
2254
|
+
document.querySelectorAll("[data-fidelity-target]").forEach((el) => el.removeAttribute("data-fidelity-target"));
|
|
2255
|
+
const timer = setTimeout(() => finish(false), 1e4);
|
|
2256
|
+
ch.on("storyThrewException", failed);
|
|
2257
|
+
ch.on("storyErrored", failed);
|
|
2258
|
+
ch.on("storyRendered", done);
|
|
2259
|
+
ch.emit("updateStoryArgs", { storyId: id, updatedArgs: { plan: plan2 } });
|
|
2260
|
+
}),
|
|
2261
|
+
{ id: FIGMA_REFERENCE_STORY, plan }
|
|
2262
|
+
);
|
|
2263
|
+
const errorScreen = await page.evaluate(() => document.body.classList.contains("sb-show-errordisplay"));
|
|
2264
|
+
return { broken: !r.rendered || !!r.error || errorScreen, ...r.error ? { error: r.error } : {} };
|
|
2265
|
+
}
|
|
2168
2266
|
export {
|
|
2169
2267
|
COMMENT_MARKER,
|
|
2170
2268
|
DEFAULT_ROUTES,
|
|
@@ -2173,11 +2271,13 @@ export {
|
|
|
2173
2271
|
boxesFromDiff,
|
|
2174
2272
|
captureFigmaReferences,
|
|
2175
2273
|
changedTokens,
|
|
2274
|
+
cropPng,
|
|
2176
2275
|
decodePng,
|
|
2177
2276
|
diffPngs,
|
|
2178
2277
|
encodePng,
|
|
2179
2278
|
hasVisualChange,
|
|
2180
2279
|
linkNodeModules,
|
|
2280
|
+
openMeasurePage,
|
|
2181
2281
|
padTo,
|
|
2182
2282
|
planImpact,
|
|
2183
2283
|
rankShots,
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bridge-ds/visual",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.14",
|
|
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
|
-
"license": "MIT",
|
|
6
|
+
"license": "FSL-1.1-MIT",
|
|
7
7
|
"author": "Florian Gouloubi",
|
|
8
8
|
"homepage": "https://github.com/peterbono/ai-design-system-demo/tree/main/packages/visual#readme",
|
|
9
9
|
"repository": {
|
|
@@ -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.20.0",
|
|
51
51
|
"pixelmatch": "7.1.0",
|
|
52
52
|
"playwright-core": "1.63.0",
|
|
53
53
|
"pngjs": "7.0.0"
|