@rahmantech/litedesign 0.0.0-stage → 0.1.0
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 +201 -0
- package/README.md +86 -2
- package/dist/extension/config.json +3 -0
- package/dist/extension/content.js +471 -0
- package/dist/extension/content.js.map +7 -0
- package/dist/extension/core.iife.js +1048 -0
- package/dist/extension/manifest.json +16 -0
- package/dist/extension/popup.html +73 -0
- package/dist/extension/popup.js +71 -0
- package/dist/extension/popup.js.map +7 -0
- package/dist/extension/sw.js +348 -0
- package/dist/extension/sw.js.map +7 -0
- package/dist/index.js +2088 -0
- package/dist/index.js.map +7 -0
- package/package.json +37 -3
|
@@ -0,0 +1,1048 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __litedesign = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// packages/core/src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
EDGES: () => EDGES,
|
|
25
|
+
GRID_STEP: () => GRID_STEP,
|
|
26
|
+
LAYERS: () => LAYERS,
|
|
27
|
+
MAX_MARKERS: () => MAX_MARKERS,
|
|
28
|
+
MAX_NODES: () => MAX_NODES,
|
|
29
|
+
OVERLAY_HOST_ID: () => OVERLAY_HOST_ID,
|
|
30
|
+
RULE_IDS: () => RULE_IDS,
|
|
31
|
+
SPACING_SCALE: () => SPACING_SCALE,
|
|
32
|
+
VERSION: () => VERSION,
|
|
33
|
+
axisGap: () => axisGap,
|
|
34
|
+
blend: () => blend,
|
|
35
|
+
bottom: () => bottom,
|
|
36
|
+
buildGraph: () => buildGraph,
|
|
37
|
+
centerX: () => centerX,
|
|
38
|
+
centerY: () => centerY,
|
|
39
|
+
contrastRatio: () => contrastRatio,
|
|
40
|
+
defaultRules: () => defaultRules,
|
|
41
|
+
findGaps: () => findGaps,
|
|
42
|
+
findGuides: () => findGuides,
|
|
43
|
+
findIssues: () => findIssues,
|
|
44
|
+
findSiblingGaps: () => findSiblingGaps,
|
|
45
|
+
findVisualGaps: () => findVisualGaps,
|
|
46
|
+
isClose: () => isClose,
|
|
47
|
+
luminance: () => luminance,
|
|
48
|
+
overlaps: () => overlaps,
|
|
49
|
+
overlapsOn: () => overlapsOn,
|
|
50
|
+
parseColor: () => parseColor,
|
|
51
|
+
pathOf: () => pathOf,
|
|
52
|
+
projectionOverlap: () => projectionOverlap,
|
|
53
|
+
render: () => render,
|
|
54
|
+
resolveNode: () => resolveNode,
|
|
55
|
+
right: () => right,
|
|
56
|
+
round: () => round
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// packages/core/src/geometry.ts
|
|
60
|
+
function right(rect) {
|
|
61
|
+
return rect.x + rect.w;
|
|
62
|
+
}
|
|
63
|
+
function bottom(rect) {
|
|
64
|
+
return rect.y + rect.h;
|
|
65
|
+
}
|
|
66
|
+
function centerX(rect) {
|
|
67
|
+
return rect.x + rect.w / 2;
|
|
68
|
+
}
|
|
69
|
+
function centerY(rect) {
|
|
70
|
+
return rect.y + rect.h / 2;
|
|
71
|
+
}
|
|
72
|
+
function overlaps(a, b) {
|
|
73
|
+
return a.x < right(b) && b.x < right(a) && a.y < bottom(b) && b.y < bottom(a);
|
|
74
|
+
}
|
|
75
|
+
function projectionOverlap(a, b, axis) {
|
|
76
|
+
return axis === "x" ? Math.max(0, Math.min(right(a), right(b)) - Math.max(a.x, b.x)) : Math.max(0, Math.min(bottom(a), bottom(b)) - Math.max(a.y, b.y));
|
|
77
|
+
}
|
|
78
|
+
function axisGap(a, b, axis) {
|
|
79
|
+
return axis === "x" ? b.x - right(a) : b.y - bottom(a);
|
|
80
|
+
}
|
|
81
|
+
function overlapsOn(a, b, axis, ratio = 0.5) {
|
|
82
|
+
const shared = projectionOverlap(a, b, axis);
|
|
83
|
+
const smallest = axis === "x" ? Math.min(a.w, b.w) : Math.min(a.h, b.h);
|
|
84
|
+
return smallest > 0 && shared / smallest >= ratio;
|
|
85
|
+
}
|
|
86
|
+
function isClose(a, b, tolerance) {
|
|
87
|
+
return Math.abs(a - b) <= tolerance;
|
|
88
|
+
}
|
|
89
|
+
function round(value, precision = 2) {
|
|
90
|
+
const factor = 10 ** precision;
|
|
91
|
+
return Math.round(value * factor) / factor;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// packages/core/src/align.ts
|
|
95
|
+
var TOLERANCE = 2;
|
|
96
|
+
var EDGES = [
|
|
97
|
+
{ axis: "x", edge: "left", value: (rect) => rect.x },
|
|
98
|
+
{ axis: "x", edge: "right", value: right },
|
|
99
|
+
{ axis: "x", edge: "center", value: centerX },
|
|
100
|
+
{ axis: "y", edge: "top", value: (rect) => rect.y },
|
|
101
|
+
{ axis: "y", edge: "bottom", value: bottom },
|
|
102
|
+
{ axis: "y", edge: "center", value: centerY }
|
|
103
|
+
];
|
|
104
|
+
function findGuides(nodes, tolerance = TOLERANCE) {
|
|
105
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
106
|
+
const guides = [];
|
|
107
|
+
for (const spec of EDGES) {
|
|
108
|
+
const items = nodes.map((node) => ({ node, value: spec.value(node.rect) })).sort((a, b) => a.value - b.value);
|
|
109
|
+
let start = 0;
|
|
110
|
+
for (let i = 1; i <= items.length; i++) {
|
|
111
|
+
const previous = items[i - 1];
|
|
112
|
+
const current = items[i];
|
|
113
|
+
const breaks = current === void 0 || previous === void 0 || current.value - previous.value > tolerance;
|
|
114
|
+
if (!breaks) continue;
|
|
115
|
+
const cluster = items.slice(start, i);
|
|
116
|
+
start = i;
|
|
117
|
+
if (cluster.length < 2) continue;
|
|
118
|
+
const members = cluster.map((item) => item.node);
|
|
119
|
+
if (isAncestorChain(members, byId)) continue;
|
|
120
|
+
const total = cluster.reduce((sum, item) => sum + item.value, 0);
|
|
121
|
+
guides.push({
|
|
122
|
+
axis: spec.axis,
|
|
123
|
+
edge: spec.edge,
|
|
124
|
+
value: round(total / cluster.length),
|
|
125
|
+
members: members.map((node) => node.id)
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return guides;
|
|
130
|
+
}
|
|
131
|
+
function isAncestorChain(members, byId) {
|
|
132
|
+
if (members.length < 2) return false;
|
|
133
|
+
const ids = new Set(members.map((node) => node.id));
|
|
134
|
+
let deepest = members[0];
|
|
135
|
+
for (const node of members) {
|
|
136
|
+
if (deepest === void 0 || node.depth > deepest.depth) deepest = node;
|
|
137
|
+
}
|
|
138
|
+
if (deepest === void 0) return false;
|
|
139
|
+
let hits = 0;
|
|
140
|
+
let current = deepest.parent === null ? void 0 : byId.get(deepest.parent);
|
|
141
|
+
while (current !== void 0) {
|
|
142
|
+
if (ids.has(current.id)) hits++;
|
|
143
|
+
current = current.parent === null ? void 0 : byId.get(current.parent);
|
|
144
|
+
}
|
|
145
|
+
return hits === members.length - 1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// packages/core/src/color.ts
|
|
149
|
+
function parseColor(value) {
|
|
150
|
+
const text = value.trim().toLowerCase();
|
|
151
|
+
if (text === "" || text === "transparent") return null;
|
|
152
|
+
if (text.startsWith("#")) return parseHex(text);
|
|
153
|
+
const match = text.match(/^rgba?\(([^)]+)\)$/);
|
|
154
|
+
if (match === null) return null;
|
|
155
|
+
const body = match[1];
|
|
156
|
+
if (body === void 0) return null;
|
|
157
|
+
const parts = body.split(/[,/\s]+/).filter((part) => part !== "");
|
|
158
|
+
if (parts.length < 3) return null;
|
|
159
|
+
const [r, g, b, a] = parts;
|
|
160
|
+
return {
|
|
161
|
+
r: Number(r),
|
|
162
|
+
g: Number(g),
|
|
163
|
+
b: Number(b),
|
|
164
|
+
a: a === void 0 ? 1 : parseAlpha(a)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function parseAlpha(value) {
|
|
168
|
+
if (value.endsWith("%")) return Number(value.slice(0, -1)) / 100;
|
|
169
|
+
return Number(value);
|
|
170
|
+
}
|
|
171
|
+
function parseHex(text) {
|
|
172
|
+
const hex = text.slice(1);
|
|
173
|
+
if (hex.length === 3 || hex.length === 4) {
|
|
174
|
+
const [r, g, b, a] = hex.split("");
|
|
175
|
+
if (r === void 0 || g === void 0 || b === void 0) return null;
|
|
176
|
+
return {
|
|
177
|
+
r: Number.parseInt(r + r, 16),
|
|
178
|
+
g: Number.parseInt(g + g, 16),
|
|
179
|
+
b: Number.parseInt(b + b, 16),
|
|
180
|
+
a: a === void 0 ? 1 : Number.parseInt(a + a, 16) / 255
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (hex.length === 6 || hex.length === 8) {
|
|
184
|
+
return {
|
|
185
|
+
r: Number.parseInt(hex.slice(0, 2), 16),
|
|
186
|
+
g: Number.parseInt(hex.slice(2, 4), 16),
|
|
187
|
+
b: Number.parseInt(hex.slice(4, 6), 16),
|
|
188
|
+
a: hex.length === 8 ? Number.parseInt(hex.slice(6, 8), 16) / 255 : 1
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
function blend(source, destination) {
|
|
194
|
+
const a = source.a + destination.a * (1 - source.a);
|
|
195
|
+
if (a === 0) return { r: 0, g: 0, b: 0, a: 0 };
|
|
196
|
+
const mix = (src, dst) => (src * source.a + dst * destination.a * (1 - source.a)) / a;
|
|
197
|
+
return {
|
|
198
|
+
r: mix(source.r, destination.r),
|
|
199
|
+
g: mix(source.g, destination.g),
|
|
200
|
+
b: mix(source.b, destination.b),
|
|
201
|
+
a
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function luminance(color) {
|
|
205
|
+
const channel = (value) => {
|
|
206
|
+
const s = value / 255;
|
|
207
|
+
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
208
|
+
};
|
|
209
|
+
return 0.2126 * channel(color.r) + 0.7152 * channel(color.g) + 0.0722 * channel(color.b);
|
|
210
|
+
}
|
|
211
|
+
function contrastRatio(a, b) {
|
|
212
|
+
const first = luminance(a);
|
|
213
|
+
const second = luminance(b);
|
|
214
|
+
const lighter = Math.max(first, second);
|
|
215
|
+
const darker = Math.min(first, second);
|
|
216
|
+
return (lighter + 0.05) / (darker + 0.05);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// packages/core/src/constants.ts
|
|
220
|
+
var MAX_NODES = 5e3;
|
|
221
|
+
var GRID_STEP = 8;
|
|
222
|
+
var SPACING_SCALE = 4;
|
|
223
|
+
var OVERLAY_HOST_ID = "__litedesign_overlay__";
|
|
224
|
+
var MAX_MARKERS = 250;
|
|
225
|
+
|
|
226
|
+
// packages/core/src/gaps.ts
|
|
227
|
+
function findSiblingGaps(nodes) {
|
|
228
|
+
const byParent = /* @__PURE__ */ new Map();
|
|
229
|
+
for (const node of nodes) {
|
|
230
|
+
const siblings = byParent.get(node.parent);
|
|
231
|
+
if (siblings) siblings.push(node);
|
|
232
|
+
else byParent.set(node.parent, [node]);
|
|
233
|
+
}
|
|
234
|
+
const gaps = [];
|
|
235
|
+
for (const siblings of byParent.values()) {
|
|
236
|
+
gaps.push(...axisGaps(siblings, "x"), ...axisGaps(siblings, "y"));
|
|
237
|
+
}
|
|
238
|
+
return gaps;
|
|
239
|
+
}
|
|
240
|
+
function axisGaps(siblings, axis) {
|
|
241
|
+
const sorted = [...siblings].sort(
|
|
242
|
+
(a, b) => axis === "x" ? a.rect.x - b.rect.x : a.rect.y - b.rect.y
|
|
243
|
+
);
|
|
244
|
+
const gaps = [];
|
|
245
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
246
|
+
const a = sorted[i];
|
|
247
|
+
const b = sorted[i + 1];
|
|
248
|
+
if (a === void 0 || b === void 0) continue;
|
|
249
|
+
const distance = axisGap(a.rect, b.rect, axis);
|
|
250
|
+
if (distance < 0.5) continue;
|
|
251
|
+
const cross = axis === "x" ? "y" : "x";
|
|
252
|
+
if (!overlapsOn(a.rect, b.rect, cross)) continue;
|
|
253
|
+
gaps.push({ a: a.id, b: b.id, axis, distance: round(distance), kind: "sibling" });
|
|
254
|
+
}
|
|
255
|
+
return gaps;
|
|
256
|
+
}
|
|
257
|
+
function findVisualGaps(nodes) {
|
|
258
|
+
const seen = /* @__PURE__ */ new Set();
|
|
259
|
+
const gaps = [];
|
|
260
|
+
const push = (a, b, axis, distance) => {
|
|
261
|
+
const key = pairKey(a.id, b.id, axis);
|
|
262
|
+
if (seen.has(key)) return;
|
|
263
|
+
seen.add(key);
|
|
264
|
+
gaps.push({ a: a.id, b: b.id, axis, distance: round(distance), kind: "visual" });
|
|
265
|
+
};
|
|
266
|
+
const byX = [...nodes].sort((a, b) => a.rect.x - b.rect.x);
|
|
267
|
+
const byRight = [...nodes].sort((a, b) => right(a.rect) - right(b.rect));
|
|
268
|
+
const byY = [...nodes].sort((a, b) => a.rect.y - b.rect.y);
|
|
269
|
+
const byBottom = [...nodes].sort((a, b) => bottom(a.rect) - bottom(b.rect));
|
|
270
|
+
for (const a of nodes) {
|
|
271
|
+
for (let j = firstAtLeast(byX, right(a.rect) - 0.5, (n) => n.rect.x); j < byX.length; j++) {
|
|
272
|
+
const b = byX[j];
|
|
273
|
+
if (b === void 0) continue;
|
|
274
|
+
if (!overlapsOn(a.rect, b.rect, "y")) continue;
|
|
275
|
+
push(a, b, "x", b.rect.x - right(a.rect));
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
for (let j = lastAtMost(byRight, a.rect.x + 0.5, (n) => right(n.rect)); j >= 0; j--) {
|
|
279
|
+
const b = byRight[j];
|
|
280
|
+
if (b === void 0) continue;
|
|
281
|
+
if (!overlapsOn(a.rect, b.rect, "y")) continue;
|
|
282
|
+
push(b, a, "x", a.rect.x - right(b.rect));
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
for (let j = firstAtLeast(byY, bottom(a.rect) - 0.5, (n) => n.rect.y); j < byY.length; j++) {
|
|
286
|
+
const b = byY[j];
|
|
287
|
+
if (b === void 0) continue;
|
|
288
|
+
if (!overlapsOn(a.rect, b.rect, "x")) continue;
|
|
289
|
+
push(a, b, "y", b.rect.y - bottom(a.rect));
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
for (let j = lastAtMost(byBottom, a.rect.y + 0.5, (n) => bottom(n.rect)); j >= 0; j--) {
|
|
293
|
+
const b = byBottom[j];
|
|
294
|
+
if (b === void 0) continue;
|
|
295
|
+
if (!overlapsOn(a.rect, b.rect, "x")) continue;
|
|
296
|
+
push(b, a, "y", a.rect.y - bottom(b.rect));
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
return gaps;
|
|
301
|
+
}
|
|
302
|
+
function findGaps(nodes) {
|
|
303
|
+
const siblings = findSiblingGaps(nodes);
|
|
304
|
+
const claimed = new Set(siblings.map((gap) => pairKey(gap.a, gap.b, gap.axis)));
|
|
305
|
+
const visual = findVisualGaps(nodes).filter(
|
|
306
|
+
(gap) => !claimed.has(pairKey(gap.a, gap.b, gap.axis))
|
|
307
|
+
);
|
|
308
|
+
return [...siblings, ...visual];
|
|
309
|
+
}
|
|
310
|
+
function pairKey(a, b, axis) {
|
|
311
|
+
return a < b ? `${a}|${b}|${axis}` : `${b}|${a}|${axis}`;
|
|
312
|
+
}
|
|
313
|
+
function firstAtLeast(list, target, value) {
|
|
314
|
+
let low = 0;
|
|
315
|
+
let high = list.length;
|
|
316
|
+
while (low < high) {
|
|
317
|
+
const middle = low + high >> 1;
|
|
318
|
+
const item = list[middle];
|
|
319
|
+
if (item !== void 0 && value(item) < target) low = middle + 1;
|
|
320
|
+
else high = middle;
|
|
321
|
+
}
|
|
322
|
+
return low;
|
|
323
|
+
}
|
|
324
|
+
function lastAtMost(list, target, value) {
|
|
325
|
+
let low = 0;
|
|
326
|
+
let high = list.length;
|
|
327
|
+
while (low < high) {
|
|
328
|
+
const middle = low + high >> 1;
|
|
329
|
+
const item = list[middle];
|
|
330
|
+
if (item !== void 0 && value(item) <= target) low = middle + 1;
|
|
331
|
+
else high = middle;
|
|
332
|
+
}
|
|
333
|
+
return low - 1;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// packages/core/src/extract.ts
|
|
337
|
+
var MAX_TEXT = 80;
|
|
338
|
+
function extractNode(el, id, parentId, depth) {
|
|
339
|
+
const rect = el.getBoundingClientRect();
|
|
340
|
+
return {
|
|
341
|
+
id,
|
|
342
|
+
selector: id,
|
|
343
|
+
tag: el.tagName.toLowerCase(),
|
|
344
|
+
rect: {
|
|
345
|
+
x: rect.left + window.scrollX,
|
|
346
|
+
y: rect.top + window.scrollY,
|
|
347
|
+
w: rect.width,
|
|
348
|
+
h: rect.height
|
|
349
|
+
},
|
|
350
|
+
box: readBox(el),
|
|
351
|
+
text: readText(el),
|
|
352
|
+
depth,
|
|
353
|
+
parent: parentId,
|
|
354
|
+
children: [],
|
|
355
|
+
styles: readStyles(el)
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
function readBox(el) {
|
|
359
|
+
return {
|
|
360
|
+
scrollW: el.scrollWidth,
|
|
361
|
+
scrollH: el.scrollHeight,
|
|
362
|
+
clientW: el.clientWidth,
|
|
363
|
+
clientH: el.clientHeight
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
function readText(el) {
|
|
367
|
+
let text = "";
|
|
368
|
+
for (const node of Array.from(el.childNodes)) {
|
|
369
|
+
if (node.nodeType === 3) text += node.textContent ?? "";
|
|
370
|
+
}
|
|
371
|
+
return text.replace(/\s+/g, " ").trim().slice(0, MAX_TEXT);
|
|
372
|
+
}
|
|
373
|
+
function readStyles(el) {
|
|
374
|
+
const style = getComputedStyle(el);
|
|
375
|
+
return {
|
|
376
|
+
display: style.display,
|
|
377
|
+
borderRadius: style.borderRadius,
|
|
378
|
+
color: style.color,
|
|
379
|
+
backgroundColor: style.backgroundColor,
|
|
380
|
+
fontSize: style.fontSize,
|
|
381
|
+
fontWeight: style.fontWeight,
|
|
382
|
+
padding: style.padding,
|
|
383
|
+
margin: style.margin,
|
|
384
|
+
gap: style.gap
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// packages/core/src/rules/rule.ts
|
|
389
|
+
function defineRule(id, run) {
|
|
390
|
+
return Object.assign(run, { id });
|
|
391
|
+
}
|
|
392
|
+
function issue(input) {
|
|
393
|
+
const suffix = input.key === void 0 ? "" : `:${input.key}`;
|
|
394
|
+
return {
|
|
395
|
+
id: `${input.rule}:${input.selector}${suffix}`,
|
|
396
|
+
selector: input.selector,
|
|
397
|
+
rule: input.rule,
|
|
398
|
+
severity: input.severity,
|
|
399
|
+
actual: input.actual,
|
|
400
|
+
expected: input.expected,
|
|
401
|
+
autoFix: input.autoFix
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// packages/core/src/rules/alignment.ts
|
|
406
|
+
var TOLERANCE2 = 2;
|
|
407
|
+
var WINDOW = 4;
|
|
408
|
+
var alignmentNearMiss = defineRule("alignment-near-miss", (graph) => {
|
|
409
|
+
const issues = [];
|
|
410
|
+
for (const spec of EDGES) {
|
|
411
|
+
const items = graph.nodes.filter((node) => node.parent !== null).map((node) => ({ node, value: spec.value(node.rect) })).sort((a, b) => a.value - b.value);
|
|
412
|
+
let start = 0;
|
|
413
|
+
for (let i = 1; i <= items.length; i++) {
|
|
414
|
+
const previous = items[i - 1];
|
|
415
|
+
const current = items[i];
|
|
416
|
+
const breaks = current === void 0 || previous === void 0 || current.value - previous.value > WINDOW;
|
|
417
|
+
if (!breaks) continue;
|
|
418
|
+
const cluster = items.slice(start, i);
|
|
419
|
+
start = i;
|
|
420
|
+
if (cluster.length < 2) continue;
|
|
421
|
+
const first = cluster[0];
|
|
422
|
+
const last = cluster[cluster.length - 1];
|
|
423
|
+
if (first === void 0 || last === void 0) continue;
|
|
424
|
+
if (last.value - first.value <= TOLERANCE2) continue;
|
|
425
|
+
const target = cluster[Math.floor(cluster.length / 2)];
|
|
426
|
+
if (target === void 0) continue;
|
|
427
|
+
let worst = cluster[0];
|
|
428
|
+
if (worst === void 0) continue;
|
|
429
|
+
for (const item of cluster) {
|
|
430
|
+
if (Math.abs(item.value - target.value) > Math.abs(worst.value - target.value)) worst = item;
|
|
431
|
+
}
|
|
432
|
+
issues.push(
|
|
433
|
+
issue({
|
|
434
|
+
rule: "alignment-near-miss",
|
|
435
|
+
selector: worst.node.id,
|
|
436
|
+
key: spec.edge,
|
|
437
|
+
severity: "P2",
|
|
438
|
+
actual: `${Math.round(worst.value * 10) / 10}px`,
|
|
439
|
+
expected: `${Math.round(target.value * 10) / 10}px`,
|
|
440
|
+
autoFix: false
|
|
441
|
+
})
|
|
442
|
+
);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return issues;
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
// packages/core/src/rules/contrast.ts
|
|
449
|
+
var WHITE = { r: 255, g: 255, b: 255, a: 1 };
|
|
450
|
+
var NORMAL_MINIMUM = 4.5;
|
|
451
|
+
var LARGE_MINIMUM = 3;
|
|
452
|
+
var contrastMinimum = defineRule("contrast", (graph) => {
|
|
453
|
+
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
454
|
+
const issues = [];
|
|
455
|
+
for (const node of graph.nodes) {
|
|
456
|
+
if (node.text === "") continue;
|
|
457
|
+
const foreground = parseColor(node.styles.color);
|
|
458
|
+
if (foreground === null) continue;
|
|
459
|
+
const background = backgroundOf(node, byId);
|
|
460
|
+
const ratio = contrastRatio(blend(foreground, background), background);
|
|
461
|
+
const minimum = isLargeText(node) ? LARGE_MINIMUM : NORMAL_MINIMUM;
|
|
462
|
+
if (ratio >= minimum) continue;
|
|
463
|
+
issues.push(
|
|
464
|
+
issue({
|
|
465
|
+
rule: "contrast",
|
|
466
|
+
selector: node.id,
|
|
467
|
+
severity: "P1",
|
|
468
|
+
actual: `${Math.round(ratio * 100) / 100}:1`,
|
|
469
|
+
expected: `>= ${minimum}:1`,
|
|
470
|
+
autoFix: false
|
|
471
|
+
})
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
return issues;
|
|
475
|
+
});
|
|
476
|
+
function isLargeText(node) {
|
|
477
|
+
const size = Number.parseFloat(node.styles.fontSize);
|
|
478
|
+
const weight = Number.parseInt(node.styles.fontWeight, 10);
|
|
479
|
+
if (!Number.isFinite(size)) return false;
|
|
480
|
+
return size >= 24 || size >= 18.66 && weight >= 700;
|
|
481
|
+
}
|
|
482
|
+
function backgroundOf(node, byId) {
|
|
483
|
+
const layers = [];
|
|
484
|
+
let current = node;
|
|
485
|
+
while (current !== void 0) {
|
|
486
|
+
const color = parseColor(current.styles.backgroundColor);
|
|
487
|
+
if (color !== null && color.a > 0) {
|
|
488
|
+
layers.push(color);
|
|
489
|
+
if (color.a === 1) break;
|
|
490
|
+
}
|
|
491
|
+
current = current.parent === null ? void 0 : byId.get(current.parent);
|
|
492
|
+
}
|
|
493
|
+
let background = WHITE;
|
|
494
|
+
for (let i = layers.length - 1; i >= 0; i--) {
|
|
495
|
+
const layer = layers[i];
|
|
496
|
+
if (layer === void 0) continue;
|
|
497
|
+
background = blend(layer, background);
|
|
498
|
+
}
|
|
499
|
+
return background;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// packages/core/src/rules/overflow.ts
|
|
503
|
+
var SLACK = 1;
|
|
504
|
+
var overflowClipped = defineRule("overflow", (graph) => {
|
|
505
|
+
const issues = [];
|
|
506
|
+
for (const node of graph.nodes) {
|
|
507
|
+
if (node.styles.display.startsWith("inline")) continue;
|
|
508
|
+
if (node.box.clientW === 0 && node.box.clientH === 0) continue;
|
|
509
|
+
if (node.box.scrollW > node.box.clientW + SLACK) {
|
|
510
|
+
issues.push(
|
|
511
|
+
issue({
|
|
512
|
+
rule: "overflow",
|
|
513
|
+
selector: node.id,
|
|
514
|
+
key: "x",
|
|
515
|
+
severity: "P2",
|
|
516
|
+
actual: `${node.box.scrollW}px content in ${node.box.clientW}px`,
|
|
517
|
+
expected: "content fits horizontally",
|
|
518
|
+
autoFix: false
|
|
519
|
+
})
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
if (node.box.scrollH > node.box.clientH + SLACK) {
|
|
523
|
+
issues.push(
|
|
524
|
+
issue({
|
|
525
|
+
rule: "overflow",
|
|
526
|
+
selector: node.id,
|
|
527
|
+
key: "y",
|
|
528
|
+
severity: "P2",
|
|
529
|
+
actual: `${node.box.scrollH}px content in ${node.box.clientH}px`,
|
|
530
|
+
expected: "content fits vertically",
|
|
531
|
+
autoFix: false
|
|
532
|
+
})
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return issues;
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
// packages/core/src/rules/radius.ts
|
|
540
|
+
var MIN_SAMPLES = 4;
|
|
541
|
+
var radiusConsistency = defineRule("radius-consistency", (graph) => {
|
|
542
|
+
const groups = /* @__PURE__ */ new Map();
|
|
543
|
+
for (const node of graph.nodes) {
|
|
544
|
+
const radius = normalizeRadius(node.styles.borderRadius);
|
|
545
|
+
if (radius === null) continue;
|
|
546
|
+
const group = groups.get(radius);
|
|
547
|
+
if (group) group.push(node);
|
|
548
|
+
else groups.set(radius, [node]);
|
|
549
|
+
}
|
|
550
|
+
if (groups.size < 2) return [];
|
|
551
|
+
const total = [...groups.values()].reduce((sum, group) => sum + group.length, 0);
|
|
552
|
+
if (total < MIN_SAMPLES) return [];
|
|
553
|
+
const ranked = [...groups.entries()].sort((a, b) => b[1].length - a[1].length);
|
|
554
|
+
const expected = ranked[0];
|
|
555
|
+
if (expected === void 0) return [];
|
|
556
|
+
const [expectedValue, expectedNodes] = expected;
|
|
557
|
+
const issues = [];
|
|
558
|
+
for (const [value, nodes] of ranked.slice(1)) {
|
|
559
|
+
if (nodes.length >= expectedNodes.length) continue;
|
|
560
|
+
for (const node of nodes) {
|
|
561
|
+
if (isPill(node)) continue;
|
|
562
|
+
issues.push(
|
|
563
|
+
issue({
|
|
564
|
+
rule: "radius-consistency",
|
|
565
|
+
selector: node.id,
|
|
566
|
+
severity: "P3",
|
|
567
|
+
actual: value,
|
|
568
|
+
expected: expectedValue,
|
|
569
|
+
autoFix: true
|
|
570
|
+
})
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return issues;
|
|
575
|
+
});
|
|
576
|
+
function normalizeRadius(value) {
|
|
577
|
+
const text = value.trim();
|
|
578
|
+
if (text === "" || text === "0px" || text === "0") return null;
|
|
579
|
+
const parts = text.split(/\s+/);
|
|
580
|
+
const unique = [...new Set(parts)];
|
|
581
|
+
if (unique.length === 1) return unique[0] ?? null;
|
|
582
|
+
return parts.join(" ");
|
|
583
|
+
}
|
|
584
|
+
function isPill(node) {
|
|
585
|
+
const text = node.styles.borderRadius;
|
|
586
|
+
if (text.includes("%")) return true;
|
|
587
|
+
const radius = Number.parseFloat(text);
|
|
588
|
+
if (!Number.isFinite(radius)) return false;
|
|
589
|
+
return radius >= Math.min(node.rect.w, node.rect.h) / 2;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// packages/core/src/rules/spacing.ts
|
|
593
|
+
var spacingScale = defineRule("spacing-scale", (graph) => {
|
|
594
|
+
const issues = [];
|
|
595
|
+
for (const gap of graph.gaps) {
|
|
596
|
+
if (gap.kind !== "sibling") continue;
|
|
597
|
+
const actual = Math.round(gap.distance);
|
|
598
|
+
if (actual <= 0 || actual % SPACING_SCALE === 0) continue;
|
|
599
|
+
issues.push(
|
|
600
|
+
issue({
|
|
601
|
+
rule: "spacing-scale",
|
|
602
|
+
selector: gap.a,
|
|
603
|
+
key: `${gap.axis}:${gap.b}`,
|
|
604
|
+
severity: "P2",
|
|
605
|
+
actual: `${actual}px`,
|
|
606
|
+
expected: `${Math.round(actual / SPACING_SCALE) * SPACING_SCALE}px`,
|
|
607
|
+
autoFix: true
|
|
608
|
+
})
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
return issues;
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// packages/core/src/rules/index.ts
|
|
615
|
+
var defaultRules = [
|
|
616
|
+
spacingScale,
|
|
617
|
+
alignmentNearMiss,
|
|
618
|
+
radiusConsistency,
|
|
619
|
+
overflowClipped,
|
|
620
|
+
contrastMinimum
|
|
621
|
+
];
|
|
622
|
+
var RULE_IDS = defaultRules.map((rule) => rule.id);
|
|
623
|
+
function findIssues(graph, rules = defaultRules) {
|
|
624
|
+
return rules.flatMap((rule) => rule(graph));
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// packages/core/src/walk.ts
|
|
628
|
+
var SKIP_TAGS = /* @__PURE__ */ new Set([
|
|
629
|
+
"script",
|
|
630
|
+
"style",
|
|
631
|
+
"link",
|
|
632
|
+
"meta",
|
|
633
|
+
"noscript",
|
|
634
|
+
"template",
|
|
635
|
+
"head",
|
|
636
|
+
"title",
|
|
637
|
+
"base"
|
|
638
|
+
]);
|
|
639
|
+
var SHADOW_MARKER = ":host";
|
|
640
|
+
function pathOf(el) {
|
|
641
|
+
const parts = [];
|
|
642
|
+
let cur = el;
|
|
643
|
+
while (cur !== null) {
|
|
644
|
+
const parent = cur.parentElement;
|
|
645
|
+
if (parent !== null) {
|
|
646
|
+
const index = Array.prototype.indexOf.call(parent.children, cur) + 1;
|
|
647
|
+
parts.unshift(`${cur.tagName.toLowerCase()}:nth-child(${index})`);
|
|
648
|
+
cur = parent;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
const host = shadowHostOf(cur);
|
|
652
|
+
if (host === null) break;
|
|
653
|
+
parts.unshift(SHADOW_MARKER);
|
|
654
|
+
cur = host;
|
|
655
|
+
}
|
|
656
|
+
return parts.length > 0 ? parts.join(" > ") : el.tagName.toLowerCase();
|
|
657
|
+
}
|
|
658
|
+
function shadowHostOf(el) {
|
|
659
|
+
const root = el.getRootNode();
|
|
660
|
+
return root.host ?? null;
|
|
661
|
+
}
|
|
662
|
+
function collectElements(root, maxNodes = MAX_NODES) {
|
|
663
|
+
const entries = [];
|
|
664
|
+
const rootPath = pathOf(root);
|
|
665
|
+
const visit = (el, parent, depth, index, boundary, parentPath) => {
|
|
666
|
+
if (entries.length >= maxNodes) return;
|
|
667
|
+
if (el.id === OVERLAY_HOST_ID) return;
|
|
668
|
+
const tag2 = el.tagName.toLowerCase();
|
|
669
|
+
if (SKIP_TAGS.has(tag2)) return;
|
|
670
|
+
if (!isVisible(el)) return;
|
|
671
|
+
const marker = boundary ? ` ${SHADOW_MARKER} > ` : " > ";
|
|
672
|
+
const id = parentPath === null ? rootPath : `${parentPath}${marker}${tag2}:nth-child(${index})`;
|
|
673
|
+
let self = parent;
|
|
674
|
+
const rect = el.getBoundingClientRect();
|
|
675
|
+
if (rect.width > 0 || rect.height > 0) {
|
|
676
|
+
entries.push({ el, id, depth, parent });
|
|
677
|
+
self = entries.length - 1;
|
|
678
|
+
}
|
|
679
|
+
const children = Array.from(el.children);
|
|
680
|
+
for (let i = 0; i < children.length; i++) {
|
|
681
|
+
const child = children[i];
|
|
682
|
+
if (child !== void 0) visit(child, self, depth + 1, i + 1, false, id);
|
|
683
|
+
}
|
|
684
|
+
const shadow = el.shadowRoot;
|
|
685
|
+
if (shadow !== null) {
|
|
686
|
+
const shadowChildren = Array.from(shadow.children);
|
|
687
|
+
for (let i = 0; i < shadowChildren.length; i++) {
|
|
688
|
+
const child = shadowChildren[i];
|
|
689
|
+
if (child !== void 0) visit(child, self, depth + 1, i + 1, true, id);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
};
|
|
693
|
+
visit(root, -1, 0, 0, false, null);
|
|
694
|
+
return entries;
|
|
695
|
+
}
|
|
696
|
+
function isVisible(el) {
|
|
697
|
+
if (typeof el.checkVisibility === "function") return el.checkVisibility();
|
|
698
|
+
return getComputedStyle(el).display !== "none";
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
// packages/core/src/graph.ts
|
|
702
|
+
function buildGraph(root = null) {
|
|
703
|
+
const start = root ?? (typeof document === "undefined" ? null : document.body);
|
|
704
|
+
if (start === null) return emptyGraph();
|
|
705
|
+
const entries = collectElements(start);
|
|
706
|
+
const nodes = [];
|
|
707
|
+
const byId = /* @__PURE__ */ new Map();
|
|
708
|
+
const ids = [];
|
|
709
|
+
for (const entry of entries) {
|
|
710
|
+
const parentId = entry.parent >= 0 ? ids[entry.parent] ?? null : null;
|
|
711
|
+
const node = extractNode(entry.el, entry.id, parentId, entry.depth);
|
|
712
|
+
ids.push(node.id);
|
|
713
|
+
nodes.push(node);
|
|
714
|
+
byId.set(node.id, node);
|
|
715
|
+
if (parentId !== null) byId.get(parentId)?.children.push(node.id);
|
|
716
|
+
}
|
|
717
|
+
const graph = {
|
|
718
|
+
url: typeof location === "undefined" ? "" : location.href,
|
|
719
|
+
viewport: {
|
|
720
|
+
w: typeof innerWidth === "undefined" ? 0 : innerWidth,
|
|
721
|
+
h: typeof innerHeight === "undefined" ? 0 : innerHeight
|
|
722
|
+
},
|
|
723
|
+
nodes,
|
|
724
|
+
gaps: findGaps(nodes),
|
|
725
|
+
guides: findGuides(nodes),
|
|
726
|
+
issues: [],
|
|
727
|
+
truncated: entries.length >= MAX_NODES
|
|
728
|
+
};
|
|
729
|
+
graph.issues = findIssues(graph);
|
|
730
|
+
return graph;
|
|
731
|
+
}
|
|
732
|
+
function resolveNode(graph, selector) {
|
|
733
|
+
const direct = graph.nodes.find((node) => node.id === selector);
|
|
734
|
+
if (direct !== void 0) return direct;
|
|
735
|
+
if (typeof document === "undefined") return null;
|
|
736
|
+
let element = null;
|
|
737
|
+
try {
|
|
738
|
+
element = document.querySelector(selector);
|
|
739
|
+
} catch {
|
|
740
|
+
return null;
|
|
741
|
+
}
|
|
742
|
+
if (element === null) return null;
|
|
743
|
+
const id = pathOf(element);
|
|
744
|
+
return graph.nodes.find((node) => node.id === id) ?? null;
|
|
745
|
+
}
|
|
746
|
+
function emptyGraph() {
|
|
747
|
+
return {
|
|
748
|
+
url: "",
|
|
749
|
+
viewport: { w: 0, h: 0 },
|
|
750
|
+
nodes: [],
|
|
751
|
+
gaps: [],
|
|
752
|
+
guides: [],
|
|
753
|
+
issues: [],
|
|
754
|
+
truncated: false
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// packages/core/src/render/layers.ts
|
|
759
|
+
var LAYERS = ["grid", "guide", "gap", "issue", "hover"];
|
|
760
|
+
var GUIDE_COLOR = {
|
|
761
|
+
left: "rgba(236, 72, 153, 0.55)",
|
|
762
|
+
right: "rgba(236, 72, 153, 0.55)",
|
|
763
|
+
center: "rgba(168, 85, 247, 0.55)",
|
|
764
|
+
top: "rgba(236, 72, 153, 0.55)",
|
|
765
|
+
bottom: "rgba(236, 72, 153, 0.55)"
|
|
766
|
+
};
|
|
767
|
+
var GAP_COLOR = "rgba(14, 165, 233, 0.9)";
|
|
768
|
+
var GUIDE_PADDING = 8;
|
|
769
|
+
var LABEL_LIMIT = 100;
|
|
770
|
+
var SEVERITY_COLOR = {
|
|
771
|
+
P0: "#dc2626",
|
|
772
|
+
P1: "#f97316",
|
|
773
|
+
P2: "#eab308",
|
|
774
|
+
P3: "#3b82f6"
|
|
775
|
+
};
|
|
776
|
+
var BASE = "position:absolute;pointer-events:none";
|
|
777
|
+
function createLayers(shadow) {
|
|
778
|
+
const layers = {};
|
|
779
|
+
for (const name of LAYERS) {
|
|
780
|
+
const layer = document.createElement("div");
|
|
781
|
+
layer.dataset.layer = name;
|
|
782
|
+
layer.style.cssText = `${BASE};inset:0`;
|
|
783
|
+
shadow.append(layer);
|
|
784
|
+
layers[name] = layer;
|
|
785
|
+
}
|
|
786
|
+
return layers;
|
|
787
|
+
}
|
|
788
|
+
function drawGrid(layer) {
|
|
789
|
+
layer.replaceChildren();
|
|
790
|
+
layer.style.backgroundImage = `repeating-linear-gradient(to right, rgba(59, 130, 246, 0.16) 0 1px, transparent 1px ${GRID_STEP}px)`;
|
|
791
|
+
}
|
|
792
|
+
function drawGuides(layer, graph) {
|
|
793
|
+
layer.replaceChildren();
|
|
794
|
+
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
795
|
+
const scrollX = window.scrollX;
|
|
796
|
+
const scrollY = window.scrollY;
|
|
797
|
+
for (const guide of graph.guides.slice(0, MAX_MARKERS)) {
|
|
798
|
+
const span = spanOf(guide, byId);
|
|
799
|
+
if (span === null) continue;
|
|
800
|
+
const color = GUIDE_COLOR[guide.edge];
|
|
801
|
+
const [start, end] = span;
|
|
802
|
+
if (guide.axis === "x") {
|
|
803
|
+
box(
|
|
804
|
+
layer,
|
|
805
|
+
`left:${guide.value - scrollX}px;top:${start - scrollY}px;width:1px;height:${end - start}px;background:${color}`
|
|
806
|
+
);
|
|
807
|
+
} else {
|
|
808
|
+
box(
|
|
809
|
+
layer,
|
|
810
|
+
`top:${guide.value - scrollY}px;left:${start - scrollX}px;height:1px;width:${end - start}px;background:${color}`
|
|
811
|
+
);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
function spanOf(guide, byId) {
|
|
816
|
+
let start = Number.POSITIVE_INFINITY;
|
|
817
|
+
let end = Number.NEGATIVE_INFINITY;
|
|
818
|
+
for (const id of guide.members) {
|
|
819
|
+
const node = byId.get(id);
|
|
820
|
+
if (node === void 0) continue;
|
|
821
|
+
const from = guide.axis === "x" ? node.rect.y : node.rect.x;
|
|
822
|
+
const to = guide.axis === "x" ? bottom(node.rect) : right(node.rect);
|
|
823
|
+
start = Math.min(start, from);
|
|
824
|
+
end = Math.max(end, to);
|
|
825
|
+
}
|
|
826
|
+
if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
|
|
827
|
+
return [start - GUIDE_PADDING, end + GUIDE_PADDING];
|
|
828
|
+
}
|
|
829
|
+
function drawGaps(layer, graph) {
|
|
830
|
+
layer.replaceChildren();
|
|
831
|
+
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
832
|
+
const scrollX = window.scrollX;
|
|
833
|
+
const scrollY = window.scrollY;
|
|
834
|
+
const showLabels = graph.gaps.length <= LABEL_LIMIT;
|
|
835
|
+
for (const gap of graph.gaps.slice(0, MAX_MARKERS)) {
|
|
836
|
+
const a = byId.get(gap.a);
|
|
837
|
+
const b = byId.get(gap.b);
|
|
838
|
+
if (a === void 0 || b === void 0) continue;
|
|
839
|
+
if (gap.axis === "x") {
|
|
840
|
+
const start2 = right(a.rect);
|
|
841
|
+
const middle2 = (Math.max(a.rect.y, b.rect.y) + Math.min(bottom(a.rect), bottom(b.rect))) / 2;
|
|
842
|
+
box(
|
|
843
|
+
layer,
|
|
844
|
+
`left:${start2 - scrollX}px;top:${middle2 - scrollY}px;width:${gap.distance}px;height:1px;background:${GAP_COLOR}`
|
|
845
|
+
);
|
|
846
|
+
if (showLabels) {
|
|
847
|
+
label(
|
|
848
|
+
layer,
|
|
849
|
+
`${round(gap.distance, 1)}`,
|
|
850
|
+
start2 + gap.distance / 2 - scrollX,
|
|
851
|
+
middle2 - scrollY
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const start = bottom(a.rect);
|
|
857
|
+
const middle = (Math.max(a.rect.x, b.rect.x) + Math.min(right(a.rect), right(b.rect))) / 2;
|
|
858
|
+
box(
|
|
859
|
+
layer,
|
|
860
|
+
`left:${middle - scrollX}px;top:${start - scrollY}px;width:1px;height:${gap.distance}px;background:${GAP_COLOR}`
|
|
861
|
+
);
|
|
862
|
+
if (showLabels) {
|
|
863
|
+
label(
|
|
864
|
+
layer,
|
|
865
|
+
`${round(gap.distance, 1)}`,
|
|
866
|
+
middle - scrollX,
|
|
867
|
+
start + gap.distance / 2 - scrollY
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function drawIssues(layer, graph) {
|
|
873
|
+
layer.replaceChildren();
|
|
874
|
+
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
875
|
+
const scrollX = window.scrollX;
|
|
876
|
+
const scrollY = window.scrollY;
|
|
877
|
+
const showLabels = graph.issues.length <= LABEL_LIMIT;
|
|
878
|
+
for (const item of graph.issues.slice(0, MAX_MARKERS)) {
|
|
879
|
+
const node = byId.get(item.selector);
|
|
880
|
+
if (node === void 0) continue;
|
|
881
|
+
const color = SEVERITY_COLOR[item.severity];
|
|
882
|
+
const left = node.rect.x - scrollX;
|
|
883
|
+
const top = node.rect.y - scrollY;
|
|
884
|
+
box(
|
|
885
|
+
layer,
|
|
886
|
+
`left:${left}px;top:${top}px;width:${node.rect.w}px;height:${node.rect.h}px;border:1px solid ${color};border-radius:2px`
|
|
887
|
+
);
|
|
888
|
+
if (!showLabels || !onScreen(left, top, node.rect.w, node.rect.h)) continue;
|
|
889
|
+
tag(layer, `${item.severity} \xB7 ${item.actual} \u2192 ${item.expected}`, left + 2, top + 2, color);
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
function onScreen(x, y, w, h) {
|
|
893
|
+
return x + w > 0 && y + h > 0 && x < window.innerWidth && y < window.innerHeight;
|
|
894
|
+
}
|
|
895
|
+
function tag(layer, text, x, y, color) {
|
|
896
|
+
const element = box(
|
|
897
|
+
layer,
|
|
898
|
+
`left:${x}px;top:${y}px;padding:1px 4px;border-radius:3px;background:${color};color:#fff;font:10px/1.3 ui-monospace,monospace;white-space:nowrap`
|
|
899
|
+
);
|
|
900
|
+
element.textContent = text;
|
|
901
|
+
}
|
|
902
|
+
var HOVER_BORDER = "rgba(59, 130, 246, 0.9)";
|
|
903
|
+
var HOVER_FILL = "rgba(59, 130, 246, 0.08)";
|
|
904
|
+
function drawHover(layer, node) {
|
|
905
|
+
layer.replaceChildren();
|
|
906
|
+
if (node === null) return;
|
|
907
|
+
const left = node.rect.x - window.scrollX;
|
|
908
|
+
const top = node.rect.y - window.scrollY;
|
|
909
|
+
box(
|
|
910
|
+
layer,
|
|
911
|
+
`left:${left}px;top:${top}px;width:${node.rect.w}px;height:${node.rect.h}px;border:1px solid ${HOVER_BORDER};background:${HOVER_FILL}`
|
|
912
|
+
);
|
|
913
|
+
const lines = [
|
|
914
|
+
`${node.tag} ${round(node.rect.w, 0)} \xD7 ${round(node.rect.h, 0)}`,
|
|
915
|
+
`padding ${node.styles.padding}`,
|
|
916
|
+
`margin ${node.styles.margin}`
|
|
917
|
+
];
|
|
918
|
+
if (node.styles.gap !== "normal" && node.styles.gap !== "0px") {
|
|
919
|
+
lines.push(`gap ${node.styles.gap}`);
|
|
920
|
+
}
|
|
921
|
+
const above = top > 72;
|
|
922
|
+
card(
|
|
923
|
+
layer,
|
|
924
|
+
lines.join("\n"),
|
|
925
|
+
left,
|
|
926
|
+
above ? top - 6 : top + 6,
|
|
927
|
+
above ? "translateY(-100%)" : "none"
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
function card(layer, text, x, y, transform) {
|
|
931
|
+
const element = box(
|
|
932
|
+
layer,
|
|
933
|
+
`left:${x}px;top:${y}px;transform:${transform};padding:6px 8px;border-radius:6px;background:rgba(17, 17, 17, 0.92);color:#fff;font:11px/1.5 ui-monospace,monospace;white-space:pre`
|
|
934
|
+
);
|
|
935
|
+
element.textContent = text;
|
|
936
|
+
}
|
|
937
|
+
function box(layer, css) {
|
|
938
|
+
const element = document.createElement("div");
|
|
939
|
+
element.style.cssText = `${BASE};${css}`;
|
|
940
|
+
layer.append(element);
|
|
941
|
+
return element;
|
|
942
|
+
}
|
|
943
|
+
function label(layer, text, x, y) {
|
|
944
|
+
const element = box(
|
|
945
|
+
layer,
|
|
946
|
+
`left:${x}px;top:${y}px;transform:translate(-50%,-50%);padding:1px 3px;border-radius:3px;background:#111;color:#fff;font:10px/1.2 ui-monospace,monospace;white-space:nowrap`
|
|
947
|
+
);
|
|
948
|
+
element.textContent = text;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// packages/core/src/render/overlay.ts
|
|
952
|
+
function render(graph, options = {}) {
|
|
953
|
+
const host = document.createElement("div");
|
|
954
|
+
host.id = OVERLAY_HOST_ID;
|
|
955
|
+
host.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:2147483647;contain:strict";
|
|
956
|
+
const shadow = host.attachShadow({ mode: "closed" });
|
|
957
|
+
const printStyle = document.createElement("style");
|
|
958
|
+
printStyle.textContent = "@media print { :host { display: none !important } }";
|
|
959
|
+
shadow.append(printStyle);
|
|
960
|
+
const layers = createLayers(shadow);
|
|
961
|
+
if (options.toolbar !== void 0) shadow.append(options.toolbar);
|
|
962
|
+
document.documentElement.append(host);
|
|
963
|
+
let current = graph;
|
|
964
|
+
let byId = indexNodes(graph);
|
|
965
|
+
let hovered = null;
|
|
966
|
+
let hoverTarget = null;
|
|
967
|
+
let hoverFrame = 0;
|
|
968
|
+
let visible = new Set(options.layers ?? LAYERS);
|
|
969
|
+
let frame = 0;
|
|
970
|
+
const draw = () => {
|
|
971
|
+
frame = 0;
|
|
972
|
+
for (const name of LAYERS) {
|
|
973
|
+
layers[name].style.display = visible.has(name) ? "block" : "none";
|
|
974
|
+
}
|
|
975
|
+
drawGrid(layers.grid);
|
|
976
|
+
drawGuides(layers.guide, current);
|
|
977
|
+
drawGaps(layers.gap, current);
|
|
978
|
+
drawIssues(layers.issue, current);
|
|
979
|
+
drawHover(layers.hover, hovered === null ? null : byId.get(hovered) ?? null);
|
|
980
|
+
};
|
|
981
|
+
const schedule = () => {
|
|
982
|
+
if (frame !== 0) return;
|
|
983
|
+
frame = requestAnimationFrame(draw);
|
|
984
|
+
};
|
|
985
|
+
const onScroll = () => schedule();
|
|
986
|
+
const onResize = () => schedule();
|
|
987
|
+
const applyHover = () => {
|
|
988
|
+
hoverFrame = 0;
|
|
989
|
+
const target = hoverTarget;
|
|
990
|
+
if (target === null) {
|
|
991
|
+
hovered = null;
|
|
992
|
+
drawHover(layers.hover, null);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
const id = pathOf(target);
|
|
996
|
+
const node = byId.get(id) ?? null;
|
|
997
|
+
hovered = node === null ? null : id;
|
|
998
|
+
drawHover(layers.hover, node);
|
|
999
|
+
};
|
|
1000
|
+
const scheduleHover = (target) => {
|
|
1001
|
+
hoverTarget = target;
|
|
1002
|
+
if (hoverFrame !== 0) return;
|
|
1003
|
+
hoverFrame = requestAnimationFrame(applyHover);
|
|
1004
|
+
};
|
|
1005
|
+
const onMouseOver = (event) => {
|
|
1006
|
+
const target = event.target;
|
|
1007
|
+
const outside = target instanceof Element && target.id === OVERLAY_HOST_ID;
|
|
1008
|
+
scheduleHover(target instanceof Element && !outside ? target : null);
|
|
1009
|
+
};
|
|
1010
|
+
const onMouseOut = (event) => {
|
|
1011
|
+
if (event instanceof MouseEvent && event.relatedTarget === null) scheduleHover(null);
|
|
1012
|
+
};
|
|
1013
|
+
window.addEventListener("scroll", onScroll, { passive: true });
|
|
1014
|
+
window.addEventListener("resize", onResize);
|
|
1015
|
+
document.addEventListener("mouseover", onMouseOver, { passive: true });
|
|
1016
|
+
document.addEventListener("mouseout", onMouseOut, { passive: true });
|
|
1017
|
+
draw();
|
|
1018
|
+
return {
|
|
1019
|
+
host,
|
|
1020
|
+
update(next) {
|
|
1021
|
+
current = next;
|
|
1022
|
+
byId = indexNodes(next);
|
|
1023
|
+
draw();
|
|
1024
|
+
},
|
|
1025
|
+
setLayers(names) {
|
|
1026
|
+
visible = new Set(names);
|
|
1027
|
+
draw();
|
|
1028
|
+
},
|
|
1029
|
+
destroy() {
|
|
1030
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
1031
|
+
if (hoverFrame !== 0) cancelAnimationFrame(hoverFrame);
|
|
1032
|
+
window.removeEventListener("scroll", onScroll);
|
|
1033
|
+
window.removeEventListener("resize", onResize);
|
|
1034
|
+
document.removeEventListener("mouseover", onMouseOver);
|
|
1035
|
+
document.removeEventListener("mouseout", onMouseOut);
|
|
1036
|
+
host.remove();
|
|
1037
|
+
}
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
function indexNodes(graph) {
|
|
1041
|
+
return new Map(graph.nodes.map((node) => [node.id, node]));
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// packages/core/src/index.ts
|
|
1045
|
+
var VERSION = "0.1.0";
|
|
1046
|
+
return __toCommonJS(index_exports);
|
|
1047
|
+
})();
|
|
1048
|
+
//# sourceMappingURL=core.iife.js.map
|