@rahmantech/litedesign 0.0.0-stage → 0.2.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.
@@ -0,0 +1,1230 @@
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
+ kind: input.kind,
400
+ actual: input.actual,
401
+ expected: input.expected,
402
+ autoFix: input.autoFix
403
+ };
404
+ }
405
+
406
+ // packages/core/src/rules/alignment.ts
407
+ var TOLERANCE2 = 2;
408
+ var WINDOW = 4;
409
+ var alignmentNearMiss = defineRule("alignment-near-miss", (graph) => {
410
+ const issues = [];
411
+ for (const spec of EDGES) {
412
+ const items = graph.nodes.filter((node) => node.parent !== null).map((node) => ({ node, value: spec.value(node.rect) })).sort((a, b) => a.value - b.value);
413
+ let start = 0;
414
+ for (let i = 1; i <= items.length; i++) {
415
+ const previous = items[i - 1];
416
+ const current = items[i];
417
+ const breaks = current === void 0 || previous === void 0 || current.value - previous.value > WINDOW;
418
+ if (!breaks) continue;
419
+ const cluster = items.slice(start, i);
420
+ start = i;
421
+ if (cluster.length < 2) continue;
422
+ const first = cluster[0];
423
+ const last = cluster[cluster.length - 1];
424
+ if (first === void 0 || last === void 0) continue;
425
+ if (last.value - first.value <= TOLERANCE2) continue;
426
+ const target = cluster[Math.floor(cluster.length / 2)];
427
+ if (target === void 0) continue;
428
+ let worst = cluster[0];
429
+ if (worst === void 0) continue;
430
+ for (const item of cluster) {
431
+ if (Math.abs(item.value - target.value) > Math.abs(worst.value - target.value)) worst = item;
432
+ }
433
+ issues.push(
434
+ issue({
435
+ rule: "alignment-near-miss",
436
+ selector: worst.node.id,
437
+ key: spec.edge,
438
+ severity: "P2",
439
+ kind: "candidate",
440
+ actual: `${Math.round(worst.value * 10) / 10}px`,
441
+ expected: `${Math.round(target.value * 10) / 10}px`,
442
+ autoFix: false
443
+ })
444
+ );
445
+ }
446
+ }
447
+ return issues;
448
+ });
449
+
450
+ // packages/core/src/rules/contrast.ts
451
+ var WHITE = { r: 255, g: 255, b: 255, a: 1 };
452
+ var NORMAL_MINIMUM = 4.5;
453
+ var LARGE_MINIMUM = 3;
454
+ var contrastMinimum = defineRule("contrast", (graph) => {
455
+ const byId = new Map(graph.nodes.map((node) => [node.id, node]));
456
+ const issues = [];
457
+ for (const node of graph.nodes) {
458
+ if (node.text === "") continue;
459
+ const foreground = parseColor(node.styles.color);
460
+ if (foreground === null) continue;
461
+ const background = backgroundOf(node, byId);
462
+ const ratio = contrastRatio(blend(foreground, background), background);
463
+ const minimum = isLargeText(node) ? LARGE_MINIMUM : NORMAL_MINIMUM;
464
+ if (ratio >= minimum) continue;
465
+ issues.push(
466
+ issue({
467
+ rule: "contrast",
468
+ selector: node.id,
469
+ severity: "P1",
470
+ kind: "violation",
471
+ actual: `${Math.round(ratio * 100) / 100}:1`,
472
+ expected: `>= ${minimum}:1`,
473
+ autoFix: false
474
+ })
475
+ );
476
+ }
477
+ return issues;
478
+ });
479
+ function isLargeText(node) {
480
+ const size = Number.parseFloat(node.styles.fontSize);
481
+ const weight = Number.parseInt(node.styles.fontWeight, 10);
482
+ if (!Number.isFinite(size)) return false;
483
+ return size >= 24 || size >= 18.66 && weight >= 700;
484
+ }
485
+ function backgroundOf(node, byId) {
486
+ const layers = [];
487
+ let current = node;
488
+ while (current !== void 0) {
489
+ const color = parseColor(current.styles.backgroundColor);
490
+ if (color !== null && color.a > 0) {
491
+ layers.push(color);
492
+ if (color.a === 1) break;
493
+ }
494
+ current = current.parent === null ? void 0 : byId.get(current.parent);
495
+ }
496
+ let background = WHITE;
497
+ for (let i = layers.length - 1; i >= 0; i--) {
498
+ const layer = layers[i];
499
+ if (layer === void 0) continue;
500
+ background = blend(layer, background);
501
+ }
502
+ return background;
503
+ }
504
+
505
+ // packages/core/src/rules/overflow.ts
506
+ var SLACK = 1;
507
+ var overflowClipped = defineRule("overflow", (graph) => {
508
+ const issues = [];
509
+ for (const node of graph.nodes) {
510
+ if (node.styles.display.startsWith("inline")) continue;
511
+ if (node.box.clientW === 0 && node.box.clientH === 0) continue;
512
+ if (node.box.scrollW > node.box.clientW + SLACK) {
513
+ issues.push(
514
+ issue({
515
+ rule: "overflow",
516
+ selector: node.id,
517
+ key: "x",
518
+ severity: "P2",
519
+ kind: "violation",
520
+ actual: `${node.box.scrollW}px content in ${node.box.clientW}px`,
521
+ expected: "content fits horizontally",
522
+ autoFix: false
523
+ })
524
+ );
525
+ }
526
+ if (node.box.scrollH > node.box.clientH + SLACK) {
527
+ issues.push(
528
+ issue({
529
+ rule: "overflow",
530
+ selector: node.id,
531
+ key: "y",
532
+ severity: "P2",
533
+ kind: "violation",
534
+ actual: `${node.box.scrollH}px content in ${node.box.clientH}px`,
535
+ expected: "content fits vertically",
536
+ autoFix: false
537
+ })
538
+ );
539
+ }
540
+ }
541
+ return issues;
542
+ });
543
+
544
+ // packages/core/src/rules/radius.ts
545
+ var MIN_SAMPLES = 4;
546
+ var radiusConsistency = defineRule("radius-consistency", (graph) => {
547
+ const groups = /* @__PURE__ */ new Map();
548
+ for (const node of graph.nodes) {
549
+ const radius = normalizeRadius(node.styles.borderRadius);
550
+ if (radius === null) continue;
551
+ const group = groups.get(radius);
552
+ if (group) group.push(node);
553
+ else groups.set(radius, [node]);
554
+ }
555
+ if (groups.size < 2) return [];
556
+ const total = [...groups.values()].reduce((sum, group) => sum + group.length, 0);
557
+ if (total < MIN_SAMPLES) return [];
558
+ const ranked = [...groups.entries()].sort((a, b) => b[1].length - a[1].length);
559
+ const expected = ranked[0];
560
+ if (expected === void 0) return [];
561
+ const [expectedValue, expectedNodes] = expected;
562
+ const issues = [];
563
+ for (const [value, nodes] of ranked.slice(1)) {
564
+ if (nodes.length >= expectedNodes.length) continue;
565
+ for (const node of nodes) {
566
+ if (isPill(node)) continue;
567
+ issues.push(
568
+ issue({
569
+ rule: "radius-consistency",
570
+ selector: node.id,
571
+ severity: "P3",
572
+ kind: "candidate",
573
+ actual: value,
574
+ expected: expectedValue,
575
+ autoFix: true
576
+ })
577
+ );
578
+ }
579
+ }
580
+ return issues;
581
+ });
582
+ function normalizeRadius(value) {
583
+ const text = value.trim();
584
+ if (text === "" || text === "0px" || text === "0") return null;
585
+ const parts = text.split(/\s+/);
586
+ const unique = [...new Set(parts)];
587
+ if (unique.length === 1) return unique[0] ?? null;
588
+ return parts.join(" ");
589
+ }
590
+ function isPill(node) {
591
+ const text = node.styles.borderRadius;
592
+ if (text.includes("%")) return true;
593
+ const radius = Number.parseFloat(text);
594
+ if (!Number.isFinite(radius)) return false;
595
+ return radius >= Math.min(node.rect.w, node.rect.h) / 2;
596
+ }
597
+
598
+ // packages/core/src/rules/spacing.ts
599
+ var spacingScale = defineRule("spacing-scale", (graph) => {
600
+ const issues = [];
601
+ for (const gap of graph.gaps) {
602
+ if (gap.kind !== "sibling") continue;
603
+ const actual = Math.round(gap.distance);
604
+ if (actual <= 0 || actual % SPACING_SCALE === 0) continue;
605
+ issues.push(
606
+ issue({
607
+ rule: "spacing-scale",
608
+ selector: gap.a,
609
+ key: `${gap.axis}:${gap.b}`,
610
+ severity: "P2",
611
+ kind: "candidate",
612
+ actual: `${actual}px`,
613
+ expected: `${Math.round(actual / SPACING_SCALE) * SPACING_SCALE}px`,
614
+ autoFix: true
615
+ })
616
+ );
617
+ }
618
+ return issues;
619
+ });
620
+
621
+ // packages/core/src/rules/index.ts
622
+ var defaultRules = [
623
+ spacingScale,
624
+ alignmentNearMiss,
625
+ radiusConsistency,
626
+ overflowClipped,
627
+ contrastMinimum
628
+ ];
629
+ var RULE_IDS = defaultRules.map((rule) => rule.id);
630
+ function findIssues(graph, rules = defaultRules) {
631
+ return rules.flatMap((rule) => rule(graph));
632
+ }
633
+
634
+ // packages/core/src/walk.ts
635
+ var SKIP_TAGS = /* @__PURE__ */ new Set([
636
+ "script",
637
+ "style",
638
+ "link",
639
+ "meta",
640
+ "noscript",
641
+ "template",
642
+ "head",
643
+ "title",
644
+ "base"
645
+ ]);
646
+ var SHADOW_MARKER = ":host";
647
+ function pathOf(el) {
648
+ const parts = [];
649
+ let cur = el;
650
+ while (cur !== null) {
651
+ const parent = cur.parentElement;
652
+ if (parent !== null) {
653
+ const index = Array.prototype.indexOf.call(parent.children, cur) + 1;
654
+ parts.unshift(`${cur.tagName.toLowerCase()}:nth-child(${index})`);
655
+ cur = parent;
656
+ continue;
657
+ }
658
+ const host = shadowHostOf(cur);
659
+ if (host === null) break;
660
+ parts.unshift(SHADOW_MARKER);
661
+ cur = host;
662
+ }
663
+ return parts.length > 0 ? parts.join(" > ") : el.tagName.toLowerCase();
664
+ }
665
+ function shadowHostOf(el) {
666
+ const root = el.getRootNode();
667
+ return root.host ?? null;
668
+ }
669
+ function collectElements(root, maxNodes = MAX_NODES) {
670
+ const entries = [];
671
+ const rootPath = pathOf(root);
672
+ const visit = (el, parent, depth, index, boundary, parentPath) => {
673
+ if (entries.length >= maxNodes) return;
674
+ if (el.id === OVERLAY_HOST_ID) return;
675
+ const tag2 = el.tagName.toLowerCase();
676
+ if (SKIP_TAGS.has(tag2)) return;
677
+ if (!isVisible(el)) return;
678
+ const marker = boundary ? ` ${SHADOW_MARKER} > ` : " > ";
679
+ const id = parentPath === null ? rootPath : `${parentPath}${marker}${tag2}:nth-child(${index})`;
680
+ let self = parent;
681
+ const rect = el.getBoundingClientRect();
682
+ if (rect.width > 0 || rect.height > 0) {
683
+ entries.push({ el, id, depth, parent });
684
+ self = entries.length - 1;
685
+ }
686
+ const children = Array.from(el.children);
687
+ for (let i = 0; i < children.length; i++) {
688
+ const child = children[i];
689
+ if (child !== void 0) visit(child, self, depth + 1, i + 1, false, id);
690
+ }
691
+ const shadow = el.shadowRoot;
692
+ if (shadow !== null) {
693
+ const shadowChildren = Array.from(shadow.children);
694
+ for (let i = 0; i < shadowChildren.length; i++) {
695
+ const child = shadowChildren[i];
696
+ if (child !== void 0) visit(child, self, depth + 1, i + 1, true, id);
697
+ }
698
+ }
699
+ };
700
+ visit(root, -1, 0, 0, false, null);
701
+ return entries;
702
+ }
703
+ function isVisible(el) {
704
+ if (typeof el.checkVisibility === "function") return el.checkVisibility();
705
+ return getComputedStyle(el).display !== "none";
706
+ }
707
+
708
+ // packages/core/src/graph.ts
709
+ function buildGraph(root = null) {
710
+ const start = root ?? (typeof document === "undefined" ? null : document.body);
711
+ if (start === null) return emptyGraph();
712
+ const entries = collectElements(start);
713
+ const nodes = [];
714
+ const byId = /* @__PURE__ */ new Map();
715
+ const ids = [];
716
+ for (const entry of entries) {
717
+ const parentId = entry.parent >= 0 ? ids[entry.parent] ?? null : null;
718
+ const node = extractNode(entry.el, entry.id, parentId, entry.depth);
719
+ ids.push(node.id);
720
+ nodes.push(node);
721
+ byId.set(node.id, node);
722
+ if (parentId !== null) byId.get(parentId)?.children.push(node.id);
723
+ }
724
+ const graph = {
725
+ url: typeof location === "undefined" ? "" : location.href,
726
+ viewport: {
727
+ w: typeof innerWidth === "undefined" ? 0 : innerWidth,
728
+ h: typeof innerHeight === "undefined" ? 0 : innerHeight
729
+ },
730
+ nodes,
731
+ gaps: findGaps(nodes),
732
+ guides: findGuides(nodes),
733
+ issues: [],
734
+ truncated: entries.length >= MAX_NODES
735
+ };
736
+ graph.issues = findIssues(graph);
737
+ return graph;
738
+ }
739
+ function resolveNode(graph, selector) {
740
+ const direct = graph.nodes.find((node) => node.id === selector);
741
+ if (direct !== void 0) return direct;
742
+ if (typeof document === "undefined") return null;
743
+ let element = null;
744
+ try {
745
+ element = document.querySelector(selector);
746
+ } catch {
747
+ return null;
748
+ }
749
+ if (element === null) return null;
750
+ const id = pathOf(element);
751
+ return graph.nodes.find((node) => node.id === id) ?? null;
752
+ }
753
+ function emptyGraph() {
754
+ return {
755
+ url: "",
756
+ viewport: { w: 0, h: 0 },
757
+ nodes: [],
758
+ gaps: [],
759
+ guides: [],
760
+ issues: [],
761
+ truncated: false
762
+ };
763
+ }
764
+
765
+ // packages/core/src/render/layers.ts
766
+ var LAYERS = ["grid", "guide", "gap", "issue", "dim", "focus", "hover"];
767
+ var GUIDE_COLOR = {
768
+ left: "rgba(236, 72, 153, 0.55)",
769
+ right: "rgba(236, 72, 153, 0.55)",
770
+ center: "rgba(168, 85, 247, 0.55)",
771
+ top: "rgba(236, 72, 153, 0.55)",
772
+ bottom: "rgba(236, 72, 153, 0.55)"
773
+ };
774
+ var GAP_COLOR = "rgba(14, 165, 233, 0.9)";
775
+ var GUIDE_PADDING = 8;
776
+ var LABEL_LIMIT = 100;
777
+ var SEVERITY_COLOR = {
778
+ P0: "#dc2626",
779
+ P1: "#f97316",
780
+ P2: "#eab308",
781
+ P3: "#3b82f6"
782
+ };
783
+ var BASE = "position:absolute;pointer-events:none";
784
+ function createLayers(shadow) {
785
+ const layers = {};
786
+ for (const name of LAYERS) {
787
+ const layer = document.createElement("div");
788
+ layer.dataset.layer = name;
789
+ layer.style.cssText = `${BASE};inset:0`;
790
+ shadow.append(layer);
791
+ layers[name] = layer;
792
+ }
793
+ return layers;
794
+ }
795
+ function gridBackground(step, axis) {
796
+ const line = "rgba(59, 130, 246, 0.16)";
797
+ const vertical = `repeating-linear-gradient(to right, ${line} 0 1px, transparent 1px ${step}px)`;
798
+ const horizontal = `repeating-linear-gradient(to bottom, ${line} 0 1px, transparent 1px ${step}px)`;
799
+ if (axis === "vertical") return vertical;
800
+ if (axis === "horizontal") return horizontal;
801
+ return `${vertical}, ${horizontal}`;
802
+ }
803
+ function drawGrid(layer, grid) {
804
+ layer.replaceChildren();
805
+ layer.style.backgroundImage = gridBackground(grid.step, grid.axis);
806
+ }
807
+ function focusedGuides(graph, focus) {
808
+ if (focus === null) return graph.guides;
809
+ return graph.guides.filter((guide) => guide.members.includes(focus.id));
810
+ }
811
+ function drawGuides(layer, graph, focus = null) {
812
+ layer.replaceChildren();
813
+ const byId = new Map(graph.nodes.map((node) => [node.id, node]));
814
+ const scrollX = window.scrollX;
815
+ const scrollY = window.scrollY;
816
+ for (const guide of focusedGuides(graph, focus).slice(0, MAX_MARKERS)) {
817
+ const span = spanOf(guide, byId);
818
+ if (span === null) continue;
819
+ const color = GUIDE_COLOR[guide.edge];
820
+ const [start, end] = span;
821
+ if (guide.axis === "x") {
822
+ box(
823
+ layer,
824
+ `left:${guide.value - scrollX}px;top:${start - scrollY}px;width:1px;height:${end - start}px;background:${color}`
825
+ );
826
+ } else {
827
+ box(
828
+ layer,
829
+ `top:${guide.value - scrollY}px;left:${start - scrollX}px;height:1px;width:${end - start}px;background:${color}`
830
+ );
831
+ }
832
+ }
833
+ }
834
+ function spanOf(guide, byId) {
835
+ let start = Number.POSITIVE_INFINITY;
836
+ let end = Number.NEGATIVE_INFINITY;
837
+ for (const id of guide.members) {
838
+ const node = byId.get(id);
839
+ if (node === void 0) continue;
840
+ const from = guide.axis === "x" ? node.rect.y : node.rect.x;
841
+ const to = guide.axis === "x" ? bottom(node.rect) : right(node.rect);
842
+ start = Math.min(start, from);
843
+ end = Math.max(end, to);
844
+ }
845
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
846
+ return [start - GUIDE_PADDING, end + GUIDE_PADDING];
847
+ }
848
+ function drawGaps(layer, graph) {
849
+ layer.replaceChildren();
850
+ const byId = new Map(graph.nodes.map((node) => [node.id, node]));
851
+ const scrollX = window.scrollX;
852
+ const scrollY = window.scrollY;
853
+ const showLabels = graph.gaps.length <= LABEL_LIMIT;
854
+ for (const gap of graph.gaps.slice(0, MAX_MARKERS)) {
855
+ const a = byId.get(gap.a);
856
+ const b = byId.get(gap.b);
857
+ if (a === void 0 || b === void 0) continue;
858
+ if (gap.axis === "x") {
859
+ const start2 = right(a.rect);
860
+ const middle2 = (Math.max(a.rect.y, b.rect.y) + Math.min(bottom(a.rect), bottom(b.rect))) / 2;
861
+ box(
862
+ layer,
863
+ `left:${start2 - scrollX}px;top:${middle2 - scrollY}px;width:${gap.distance}px;height:1px;background:${GAP_COLOR}`
864
+ );
865
+ if (showLabels) {
866
+ label(
867
+ layer,
868
+ `${round(gap.distance, 1)}`,
869
+ start2 + gap.distance / 2 - scrollX,
870
+ middle2 - scrollY
871
+ );
872
+ }
873
+ continue;
874
+ }
875
+ const start = bottom(a.rect);
876
+ const middle = (Math.max(a.rect.x, b.rect.x) + Math.min(right(a.rect), right(b.rect))) / 2;
877
+ box(
878
+ layer,
879
+ `left:${middle - scrollX}px;top:${start - scrollY}px;width:1px;height:${gap.distance}px;background:${GAP_COLOR}`
880
+ );
881
+ if (showLabels) {
882
+ label(
883
+ layer,
884
+ `${round(gap.distance, 1)}`,
885
+ middle - scrollX,
886
+ start + gap.distance / 2 - scrollY
887
+ );
888
+ }
889
+ }
890
+ }
891
+ function drawIssues(layer, graph) {
892
+ layer.replaceChildren();
893
+ const byId = new Map(graph.nodes.map((node) => [node.id, node]));
894
+ const scrollX = window.scrollX;
895
+ const scrollY = window.scrollY;
896
+ const showLabels = graph.issues.length <= LABEL_LIMIT;
897
+ for (const item of graph.issues.slice(0, MAX_MARKERS)) {
898
+ const node = byId.get(item.selector);
899
+ if (node === void 0) continue;
900
+ const color = SEVERITY_COLOR[item.severity];
901
+ const left = node.rect.x - scrollX;
902
+ const top = node.rect.y - scrollY;
903
+ box(
904
+ layer,
905
+ `left:${left}px;top:${top}px;width:${node.rect.w}px;height:${node.rect.h}px;border:1px solid ${color};border-radius:2px`
906
+ );
907
+ if (!showLabels || !onScreen(left, top, node.rect.w, node.rect.h)) continue;
908
+ tag(layer, `${item.severity} \xB7 ${item.actual} \u2192 ${item.expected}`, left + 2, top + 2, color);
909
+ }
910
+ }
911
+ function onScreen(x, y, w, h) {
912
+ return x + w > 0 && y + h > 0 && x < window.innerWidth && y < window.innerHeight;
913
+ }
914
+ function tag(layer, text, x, y, color) {
915
+ const element = box(
916
+ layer,
917
+ `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`
918
+ );
919
+ element.textContent = text;
920
+ }
921
+ var HOVER_BORDER = "rgba(59, 130, 246, 0.9)";
922
+ var HOVER_FILL = "rgba(59, 130, 246, 0.08)";
923
+ var FOCUS_DIM = "rgba(15, 23, 42, 0.55)";
924
+ var FOCUS_BORDER = "#3b82f6";
925
+ var SVG_NS = "http://www.w3.org/2000/svg";
926
+ var MIN_RADIUS = 4;
927
+ var TANGENT_COLOR = "rgba(148, 163, 184, 0.9)";
928
+ function parseRadius(css, rect) {
929
+ const text = css.trim();
930
+ if (text === "" || text.includes("/")) return null;
931
+ const parts = text.split(/\s+/);
932
+ if (parts.length > 4) return null;
933
+ const values = [];
934
+ for (const part of parts) {
935
+ const resolved = part.endsWith("%") ? Math.min(rect.w, rect.h) * (Number.parseFloat(part) / 100) : Number.parseFloat(part);
936
+ if (!Number.isFinite(resolved) || resolved < 0) return null;
937
+ values.push(resolved);
938
+ }
939
+ const [first = 0, second = first, third = first, fourth = second] = values;
940
+ const clamp = (radius) => Math.min(radius, rect.w / 2, rect.h / 2);
941
+ return {
942
+ tl: clamp(first),
943
+ tr: clamp(second),
944
+ br: clamp(third),
945
+ bl: clamp(fourth)
946
+ };
947
+ }
948
+ function radiusGeometry(rect, radii) {
949
+ const specs = [
950
+ { corner: "tl", r: radii.tl, x: rect.x, y: rect.y, sx: 1, sy: 1 },
951
+ { corner: "tr", r: radii.tr, x: rect.x + rect.w, y: rect.y, sx: -1, sy: 1 },
952
+ { corner: "br", r: radii.br, x: rect.x + rect.w, y: rect.y + rect.h, sx: -1, sy: -1 },
953
+ { corner: "bl", r: radii.bl, x: rect.x, y: rect.y + rect.h, sx: 1, sy: -1 }
954
+ ];
955
+ const marks = [];
956
+ for (const spec of specs) {
957
+ if (spec.r < MIN_RADIUS) continue;
958
+ const hx = spec.x + spec.sx * spec.r;
959
+ const hy = spec.y;
960
+ const vx = spec.x;
961
+ const vy = spec.y + spec.sy * spec.r;
962
+ const sweep = spec.sx * spec.sy === 1 ? 0 : 1;
963
+ marks.push({
964
+ corner: spec.corner,
965
+ radius: spec.r,
966
+ arc: `M ${hx} ${hy} A ${spec.r} ${spec.r} 0 0 ${sweep} ${vx} ${vy}`,
967
+ tangents: [
968
+ [spec.x, spec.y, hx, hy],
969
+ [spec.x, spec.y, vx, vy]
970
+ ]
971
+ });
972
+ }
973
+ return marks;
974
+ }
975
+ function createSubLayer(name, before) {
976
+ const layer = document.createElement("div");
977
+ layer.dataset.layer = name;
978
+ layer.style.cssText = `${BASE};inset:0`;
979
+ before.before(layer);
980
+ return layer;
981
+ }
982
+ var MARK_COLOR = "rgba(217, 119, 6, 0.95)";
983
+ function drawMarks(layer, marks, bySelector) {
984
+ layer.replaceChildren();
985
+ if (marks.length === 0) return;
986
+ const scrollX = window.scrollX;
987
+ const scrollY = window.scrollY;
988
+ for (const mark of marks) {
989
+ const node = bySelector.get(mark.selector);
990
+ if (node === void 0) continue;
991
+ const badge = box(
992
+ layer,
993
+ `left:${node.rect.x - scrollX}px;top:${node.rect.y - scrollY}px;transform:translateY(-100%);padding:2px 6px;border-radius:4px;background:${MARK_COLOR};color:#fff;font:10px/1.4 ui-monospace,monospace;white-space:nowrap`
994
+ );
995
+ badge.textContent = mark.text;
996
+ }
997
+ }
998
+ function drawRadius(layer, node) {
999
+ layer.replaceChildren();
1000
+ if (node === null) return;
1001
+ const radii = parseRadius(node.styles.borderRadius, node.rect);
1002
+ if (radii === null) return;
1003
+ const view = {
1004
+ x: node.rect.x - window.scrollX,
1005
+ y: node.rect.y - window.scrollY,
1006
+ w: node.rect.w,
1007
+ h: node.rect.h
1008
+ };
1009
+ const marks = radiusGeometry(view, radii);
1010
+ if (marks.length === 0) return;
1011
+ const svg = document.createElementNS(SVG_NS, "svg");
1012
+ svg.style.cssText = `${BASE};inset:0;width:100%;height:100%`;
1013
+ for (const mark of marks) {
1014
+ const arc = document.createElementNS(SVG_NS, "path");
1015
+ arc.setAttribute("d", mark.arc);
1016
+ arc.setAttribute("fill", "none");
1017
+ arc.setAttribute("stroke", GAP_COLOR);
1018
+ arc.setAttribute("stroke-width", "2");
1019
+ svg.append(arc);
1020
+ for (const [x1, y1, x2, y2] of mark.tangents) {
1021
+ const line = document.createElementNS(SVG_NS, "line");
1022
+ line.setAttribute("x1", String(x1));
1023
+ line.setAttribute("y1", String(y1));
1024
+ line.setAttribute("x2", String(x2));
1025
+ line.setAttribute("y2", String(y2));
1026
+ line.setAttribute("stroke", TANGENT_COLOR);
1027
+ line.setAttribute("stroke-width", "1");
1028
+ line.setAttribute("stroke-dasharray", "3 3");
1029
+ svg.append(line);
1030
+ }
1031
+ }
1032
+ layer.append(svg);
1033
+ const labeled = marks.every((mark) => mark.radius === marks[0]?.radius) ? marks.slice(0, 1) : marks;
1034
+ for (const mark of labeled) {
1035
+ const inset = mark.radius * 0.55;
1036
+ const x = mark.corner === "tr" || mark.corner === "br" ? view.x + view.w - inset : view.x + inset;
1037
+ const y = mark.corner === "br" || mark.corner === "bl" ? view.y + view.h - inset : view.y + inset;
1038
+ label(layer, `${round(mark.radius, 1)}px`, x, y);
1039
+ }
1040
+ }
1041
+ function dimStyle(rect, scroll) {
1042
+ const left = rect.x - scroll.x;
1043
+ const top = rect.y - scroll.y;
1044
+ return `left:${left}px;top:${top}px;width:${rect.w}px;height:${rect.h}px;box-shadow:0 0 0 9999px ${FOCUS_DIM}`;
1045
+ }
1046
+ function outlineStyle(rect, scroll) {
1047
+ const left = rect.x - scroll.x;
1048
+ const top = rect.y - scroll.y;
1049
+ return `left:${left}px;top:${top}px;width:${rect.w}px;height:${rect.h}px;border:2px solid ${FOCUS_BORDER};border-radius:2px`;
1050
+ }
1051
+ function drawDim(layer, node) {
1052
+ layer.replaceChildren();
1053
+ if (node === null) return;
1054
+ box(layer, dimStyle(node.rect, { x: window.scrollX, y: window.scrollY }));
1055
+ }
1056
+ function drawFocus(layer, node) {
1057
+ layer.replaceChildren();
1058
+ if (node === null) return;
1059
+ box(layer, outlineStyle(node.rect, { x: window.scrollX, y: window.scrollY }));
1060
+ }
1061
+ function drawHover(layer, node) {
1062
+ layer.replaceChildren();
1063
+ if (node === null) return;
1064
+ const left = node.rect.x - window.scrollX;
1065
+ const top = node.rect.y - window.scrollY;
1066
+ box(
1067
+ layer,
1068
+ `left:${left}px;top:${top}px;width:${node.rect.w}px;height:${node.rect.h}px;border:1px solid ${HOVER_BORDER};background:${HOVER_FILL}`
1069
+ );
1070
+ const lines = [
1071
+ `${node.tag} ${round(node.rect.w, 0)} \xD7 ${round(node.rect.h, 0)}`,
1072
+ `padding ${node.styles.padding}`,
1073
+ `margin ${node.styles.margin}`
1074
+ ];
1075
+ if (node.styles.gap !== "normal" && node.styles.gap !== "0px") {
1076
+ lines.push(`gap ${node.styles.gap}`);
1077
+ }
1078
+ const above = top > 72;
1079
+ card(
1080
+ layer,
1081
+ lines.join("\n"),
1082
+ left,
1083
+ above ? top - 6 : top + 6,
1084
+ above ? "translateY(-100%)" : "none"
1085
+ );
1086
+ }
1087
+ function card(layer, text, x, y, transform) {
1088
+ const element = box(
1089
+ layer,
1090
+ `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`
1091
+ );
1092
+ element.textContent = text;
1093
+ }
1094
+ function box(layer, css) {
1095
+ const element = document.createElement("div");
1096
+ element.style.cssText = `${BASE};${css}`;
1097
+ layer.append(element);
1098
+ return element;
1099
+ }
1100
+ function label(layer, text, x, y) {
1101
+ const element = box(
1102
+ layer,
1103
+ `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`
1104
+ );
1105
+ element.textContent = text;
1106
+ }
1107
+
1108
+ // packages/core/src/render/overlay.ts
1109
+ function render(graph, options = {}) {
1110
+ const host = document.createElement("div");
1111
+ host.id = OVERLAY_HOST_ID;
1112
+ host.style.cssText = "position:fixed;inset:0;pointer-events:none;z-index:2147483647;contain:strict";
1113
+ const shadow = host.attachShadow({ mode: "closed" });
1114
+ const printStyle = document.createElement("style");
1115
+ printStyle.textContent = "@media print { :host { display: none !important } }";
1116
+ shadow.append(printStyle);
1117
+ const layers = createLayers(shadow);
1118
+ const radiusLayer = createSubLayer("radius", layers.hover);
1119
+ const marksLayer = createSubLayer("marks", layers.hover);
1120
+ if (options.toolbar !== void 0) shadow.append(options.toolbar);
1121
+ document.documentElement.append(host);
1122
+ let current = graph;
1123
+ let byId = indexNodes(graph);
1124
+ let hovered = null;
1125
+ let hoverTarget = null;
1126
+ let hoverFrame = 0;
1127
+ let focusId = null;
1128
+ let visible = new Set(options.layers ?? LAYERS);
1129
+ let marks = options.marks ?? [];
1130
+ let grid = options.grid ?? { step: GRID_STEP, axis: "both" };
1131
+ let frame = 0;
1132
+ const draw = () => {
1133
+ frame = 0;
1134
+ for (const name of LAYERS) {
1135
+ layers[name].style.display = visible.has(name) ? "block" : "none";
1136
+ }
1137
+ drawGrid(layers.grid, grid);
1138
+ drawGaps(layers.gap, current);
1139
+ drawIssues(layers.issue, current);
1140
+ const focused = focusId === null ? null : byId.get(focusId) ?? null;
1141
+ drawGuides(layers.guide, current, focused);
1142
+ const outlineNode = visible.has("focus") ? focused : null;
1143
+ const dimNode = visible.has("dim") ? focused : null;
1144
+ drawDim(layers.dim, dimNode);
1145
+ drawFocus(layers.focus, outlineNode);
1146
+ drawRadius(radiusLayer, outlineNode);
1147
+ drawMarks(marksLayer, marks, new Map(current.nodes.map((node) => [node.selector, node])));
1148
+ drawHover(layers.hover, hovered === null ? null : byId.get(hovered) ?? null);
1149
+ };
1150
+ const schedule = () => {
1151
+ if (frame !== 0) return;
1152
+ frame = requestAnimationFrame(draw);
1153
+ };
1154
+ const onScroll = () => schedule();
1155
+ const onResize = () => schedule();
1156
+ const applyHover = () => {
1157
+ hoverFrame = 0;
1158
+ const target = hoverTarget;
1159
+ if (target === null) {
1160
+ hovered = null;
1161
+ drawHover(layers.hover, null);
1162
+ return;
1163
+ }
1164
+ const id = pathOf(target);
1165
+ const node = byId.get(id) ?? null;
1166
+ hovered = node === null ? null : id;
1167
+ drawHover(layers.hover, node);
1168
+ };
1169
+ const scheduleHover = (target) => {
1170
+ hoverTarget = target;
1171
+ if (hoverFrame !== 0) return;
1172
+ hoverFrame = requestAnimationFrame(applyHover);
1173
+ };
1174
+ const onMouseOver = (event) => {
1175
+ const target = event.target;
1176
+ const outside = target instanceof Element && target.id === OVERLAY_HOST_ID;
1177
+ scheduleHover(target instanceof Element && !outside ? target : null);
1178
+ };
1179
+ const onMouseOut = (event) => {
1180
+ if (event instanceof MouseEvent && event.relatedTarget === null) scheduleHover(null);
1181
+ };
1182
+ window.addEventListener("scroll", onScroll, { passive: true });
1183
+ window.addEventListener("resize", onResize);
1184
+ document.addEventListener("mouseover", onMouseOver, { passive: true });
1185
+ document.addEventListener("mouseout", onMouseOut, { passive: true });
1186
+ draw();
1187
+ return {
1188
+ host,
1189
+ update(next) {
1190
+ current = next;
1191
+ byId = indexNodes(next);
1192
+ if (focusId !== null && !byId.has(focusId)) focusId = null;
1193
+ draw();
1194
+ },
1195
+ setLayers(names) {
1196
+ visible = new Set(names);
1197
+ draw();
1198
+ },
1199
+ setFocus(id) {
1200
+ focusId = id;
1201
+ draw();
1202
+ },
1203
+ setMarks(next) {
1204
+ marks = next;
1205
+ draw();
1206
+ },
1207
+ setGrid(next) {
1208
+ grid = next;
1209
+ draw();
1210
+ },
1211
+ destroy() {
1212
+ if (frame !== 0) cancelAnimationFrame(frame);
1213
+ if (hoverFrame !== 0) cancelAnimationFrame(hoverFrame);
1214
+ window.removeEventListener("scroll", onScroll);
1215
+ window.removeEventListener("resize", onResize);
1216
+ document.removeEventListener("mouseover", onMouseOver);
1217
+ document.removeEventListener("mouseout", onMouseOut);
1218
+ host.remove();
1219
+ }
1220
+ };
1221
+ }
1222
+ function indexNodes(graph) {
1223
+ return new Map(graph.nodes.map((node) => [node.id, node]));
1224
+ }
1225
+
1226
+ // packages/core/src/index.ts
1227
+ var VERSION = "0.2.0";
1228
+ return __toCommonJS(index_exports);
1229
+ })();
1230
+ //# sourceMappingURL=core.iife.js.map