@rahmantech/litedesign 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -10
- package/dist/extension/content.js +448 -28
- package/dist/extension/core.iife.js +214 -8
- package/dist/extension/manifest.json +1 -1
- package/dist/extension/sw.js +1 -0
- package/dist/index.js +120 -15
- package/package.json +3 -2
- package/dist/extension/content.js.map +0 -7
- package/dist/extension/popup.js.map +0 -7
- package/dist/extension/sw.js.map +0 -7
- package/dist/index.js.map +0 -7
|
@@ -396,6 +396,7 @@ var __litedesign = (() => {
|
|
|
396
396
|
selector: input.selector,
|
|
397
397
|
rule: input.rule,
|
|
398
398
|
severity: input.severity,
|
|
399
|
+
kind: input.kind,
|
|
399
400
|
actual: input.actual,
|
|
400
401
|
expected: input.expected,
|
|
401
402
|
autoFix: input.autoFix
|
|
@@ -435,6 +436,7 @@ var __litedesign = (() => {
|
|
|
435
436
|
selector: worst.node.id,
|
|
436
437
|
key: spec.edge,
|
|
437
438
|
severity: "P2",
|
|
439
|
+
kind: "candidate",
|
|
438
440
|
actual: `${Math.round(worst.value * 10) / 10}px`,
|
|
439
441
|
expected: `${Math.round(target.value * 10) / 10}px`,
|
|
440
442
|
autoFix: false
|
|
@@ -465,6 +467,7 @@ var __litedesign = (() => {
|
|
|
465
467
|
rule: "contrast",
|
|
466
468
|
selector: node.id,
|
|
467
469
|
severity: "P1",
|
|
470
|
+
kind: "violation",
|
|
468
471
|
actual: `${Math.round(ratio * 100) / 100}:1`,
|
|
469
472
|
expected: `>= ${minimum}:1`,
|
|
470
473
|
autoFix: false
|
|
@@ -513,6 +516,7 @@ var __litedesign = (() => {
|
|
|
513
516
|
selector: node.id,
|
|
514
517
|
key: "x",
|
|
515
518
|
severity: "P2",
|
|
519
|
+
kind: "violation",
|
|
516
520
|
actual: `${node.box.scrollW}px content in ${node.box.clientW}px`,
|
|
517
521
|
expected: "content fits horizontally",
|
|
518
522
|
autoFix: false
|
|
@@ -526,6 +530,7 @@ var __litedesign = (() => {
|
|
|
526
530
|
selector: node.id,
|
|
527
531
|
key: "y",
|
|
528
532
|
severity: "P2",
|
|
533
|
+
kind: "violation",
|
|
529
534
|
actual: `${node.box.scrollH}px content in ${node.box.clientH}px`,
|
|
530
535
|
expected: "content fits vertically",
|
|
531
536
|
autoFix: false
|
|
@@ -564,6 +569,7 @@ var __litedesign = (() => {
|
|
|
564
569
|
rule: "radius-consistency",
|
|
565
570
|
selector: node.id,
|
|
566
571
|
severity: "P3",
|
|
572
|
+
kind: "candidate",
|
|
567
573
|
actual: value,
|
|
568
574
|
expected: expectedValue,
|
|
569
575
|
autoFix: true
|
|
@@ -602,6 +608,7 @@ var __litedesign = (() => {
|
|
|
602
608
|
selector: gap.a,
|
|
603
609
|
key: `${gap.axis}:${gap.b}`,
|
|
604
610
|
severity: "P2",
|
|
611
|
+
kind: "candidate",
|
|
605
612
|
actual: `${actual}px`,
|
|
606
613
|
expected: `${Math.round(actual / SPACING_SCALE) * SPACING_SCALE}px`,
|
|
607
614
|
autoFix: true
|
|
@@ -756,7 +763,7 @@ var __litedesign = (() => {
|
|
|
756
763
|
}
|
|
757
764
|
|
|
758
765
|
// packages/core/src/render/layers.ts
|
|
759
|
-
var LAYERS = ["grid", "guide", "gap", "issue", "hover"];
|
|
766
|
+
var LAYERS = ["grid", "guide", "gap", "issue", "dim", "focus", "hover"];
|
|
760
767
|
var GUIDE_COLOR = {
|
|
761
768
|
left: "rgba(236, 72, 153, 0.55)",
|
|
762
769
|
right: "rgba(236, 72, 153, 0.55)",
|
|
@@ -785,16 +792,28 @@ var __litedesign = (() => {
|
|
|
785
792
|
}
|
|
786
793
|
return layers;
|
|
787
794
|
}
|
|
788
|
-
function
|
|
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) {
|
|
789
804
|
layer.replaceChildren();
|
|
790
|
-
layer.style.backgroundImage =
|
|
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));
|
|
791
810
|
}
|
|
792
|
-
function drawGuides(layer, graph) {
|
|
811
|
+
function drawGuides(layer, graph, focus = null) {
|
|
793
812
|
layer.replaceChildren();
|
|
794
813
|
const byId = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
795
814
|
const scrollX = window.scrollX;
|
|
796
815
|
const scrollY = window.scrollY;
|
|
797
|
-
for (const guide of graph.
|
|
816
|
+
for (const guide of focusedGuides(graph, focus).slice(0, MAX_MARKERS)) {
|
|
798
817
|
const span = spanOf(guide, byId);
|
|
799
818
|
if (span === null) continue;
|
|
800
819
|
const color = GUIDE_COLOR[guide.edge];
|
|
@@ -901,6 +920,147 @@ var __litedesign = (() => {
|
|
|
901
920
|
}
|
|
902
921
|
var HOVER_BORDER = "rgba(59, 130, 246, 0.9)";
|
|
903
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, interactive = false) {
|
|
984
|
+
layer.replaceChildren();
|
|
985
|
+
const badges = /* @__PURE__ */ new Map();
|
|
986
|
+
if (marks.length === 0) return badges;
|
|
987
|
+
const scrollX = window.scrollX;
|
|
988
|
+
const scrollY = window.scrollY;
|
|
989
|
+
for (const mark of marks) {
|
|
990
|
+
const node = bySelector.get(mark.selector);
|
|
991
|
+
if (node === void 0) continue;
|
|
992
|
+
const badge = box(
|
|
993
|
+
layer,
|
|
994
|
+
`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` + (interactive ? ";pointer-events:auto;cursor:pointer" : "")
|
|
995
|
+
);
|
|
996
|
+
badge.textContent = mark.text;
|
|
997
|
+
badges.set(badge, mark);
|
|
998
|
+
}
|
|
999
|
+
return badges;
|
|
1000
|
+
}
|
|
1001
|
+
function drawRadius(layer, node) {
|
|
1002
|
+
layer.replaceChildren();
|
|
1003
|
+
if (node === null) return;
|
|
1004
|
+
const radii = parseRadius(node.styles.borderRadius, node.rect);
|
|
1005
|
+
if (radii === null) return;
|
|
1006
|
+
const view = {
|
|
1007
|
+
x: node.rect.x - window.scrollX,
|
|
1008
|
+
y: node.rect.y - window.scrollY,
|
|
1009
|
+
w: node.rect.w,
|
|
1010
|
+
h: node.rect.h
|
|
1011
|
+
};
|
|
1012
|
+
const marks = radiusGeometry(view, radii);
|
|
1013
|
+
if (marks.length === 0) return;
|
|
1014
|
+
const svg = document.createElementNS(SVG_NS, "svg");
|
|
1015
|
+
svg.style.cssText = `${BASE};inset:0;width:100%;height:100%`;
|
|
1016
|
+
for (const mark of marks) {
|
|
1017
|
+
const arc = document.createElementNS(SVG_NS, "path");
|
|
1018
|
+
arc.setAttribute("d", mark.arc);
|
|
1019
|
+
arc.setAttribute("fill", "none");
|
|
1020
|
+
arc.setAttribute("stroke", GAP_COLOR);
|
|
1021
|
+
arc.setAttribute("stroke-width", "2");
|
|
1022
|
+
svg.append(arc);
|
|
1023
|
+
for (const [x1, y1, x2, y2] of mark.tangents) {
|
|
1024
|
+
const line = document.createElementNS(SVG_NS, "line");
|
|
1025
|
+
line.setAttribute("x1", String(x1));
|
|
1026
|
+
line.setAttribute("y1", String(y1));
|
|
1027
|
+
line.setAttribute("x2", String(x2));
|
|
1028
|
+
line.setAttribute("y2", String(y2));
|
|
1029
|
+
line.setAttribute("stroke", TANGENT_COLOR);
|
|
1030
|
+
line.setAttribute("stroke-width", "1");
|
|
1031
|
+
line.setAttribute("stroke-dasharray", "3 3");
|
|
1032
|
+
svg.append(line);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
layer.append(svg);
|
|
1036
|
+
const labeled = marks.every((mark) => mark.radius === marks[0]?.radius) ? marks.slice(0, 1) : marks;
|
|
1037
|
+
for (const mark of labeled) {
|
|
1038
|
+
const inset = mark.radius * 0.55;
|
|
1039
|
+
const x = mark.corner === "tr" || mark.corner === "br" ? view.x + view.w - inset : view.x + inset;
|
|
1040
|
+
const y = mark.corner === "br" || mark.corner === "bl" ? view.y + view.h - inset : view.y + inset;
|
|
1041
|
+
label(layer, `${round(mark.radius, 1)}px`, x, y);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
function dimStyle(rect, scroll) {
|
|
1045
|
+
const left = rect.x - scroll.x;
|
|
1046
|
+
const top = rect.y - scroll.y;
|
|
1047
|
+
return `left:${left}px;top:${top}px;width:${rect.w}px;height:${rect.h}px;box-shadow:0 0 0 9999px ${FOCUS_DIM}`;
|
|
1048
|
+
}
|
|
1049
|
+
function outlineStyle(rect, scroll) {
|
|
1050
|
+
const left = rect.x - scroll.x;
|
|
1051
|
+
const top = rect.y - scroll.y;
|
|
1052
|
+
return `left:${left}px;top:${top}px;width:${rect.w}px;height:${rect.h}px;border:2px solid ${FOCUS_BORDER};border-radius:2px`;
|
|
1053
|
+
}
|
|
1054
|
+
function drawDim(layer, node) {
|
|
1055
|
+
layer.replaceChildren();
|
|
1056
|
+
if (node === null) return;
|
|
1057
|
+
box(layer, dimStyle(node.rect, { x: window.scrollX, y: window.scrollY }));
|
|
1058
|
+
}
|
|
1059
|
+
function drawFocus(layer, node) {
|
|
1060
|
+
layer.replaceChildren();
|
|
1061
|
+
if (node === null) return;
|
|
1062
|
+
box(layer, outlineStyle(node.rect, { x: window.scrollX, y: window.scrollY }));
|
|
1063
|
+
}
|
|
904
1064
|
function drawHover(layer, node) {
|
|
905
1065
|
layer.replaceChildren();
|
|
906
1066
|
if (node === null) return;
|
|
@@ -958,6 +1118,21 @@ var __litedesign = (() => {
|
|
|
958
1118
|
printStyle.textContent = "@media print { :host { display: none !important } }";
|
|
959
1119
|
shadow.append(printStyle);
|
|
960
1120
|
const layers = createLayers(shadow);
|
|
1121
|
+
const radiusLayer = createSubLayer("radius", layers.hover);
|
|
1122
|
+
const marksLayer = createSubLayer("marks", layers.hover);
|
|
1123
|
+
if (options.onMarkClick !== void 0) {
|
|
1124
|
+
const onMarkClick = options.onMarkClick;
|
|
1125
|
+
marksLayer.addEventListener("click", (event) => {
|
|
1126
|
+
if (!marksInteractive) return;
|
|
1127
|
+
const badge = event.target;
|
|
1128
|
+
if (!(badge instanceof HTMLDivElement)) return;
|
|
1129
|
+
const mark = badgeMarks.get(badge);
|
|
1130
|
+
if (mark === void 0) return;
|
|
1131
|
+
event.preventDefault();
|
|
1132
|
+
event.stopPropagation();
|
|
1133
|
+
onMarkClick(mark, badge.getBoundingClientRect());
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
961
1136
|
if (options.toolbar !== void 0) shadow.append(options.toolbar);
|
|
962
1137
|
document.documentElement.append(host);
|
|
963
1138
|
let current = graph;
|
|
@@ -965,17 +1140,34 @@ var __litedesign = (() => {
|
|
|
965
1140
|
let hovered = null;
|
|
966
1141
|
let hoverTarget = null;
|
|
967
1142
|
let hoverFrame = 0;
|
|
1143
|
+
let focusId = null;
|
|
968
1144
|
let visible = new Set(options.layers ?? LAYERS);
|
|
1145
|
+
let marks = options.marks ?? [];
|
|
1146
|
+
let marksInteractive = false;
|
|
1147
|
+
let badgeMarks = /* @__PURE__ */ new Map();
|
|
1148
|
+
let grid = options.grid ?? { step: GRID_STEP, axis: "both" };
|
|
969
1149
|
let frame = 0;
|
|
970
1150
|
const draw = () => {
|
|
971
1151
|
frame = 0;
|
|
972
1152
|
for (const name of LAYERS) {
|
|
973
1153
|
layers[name].style.display = visible.has(name) ? "block" : "none";
|
|
974
1154
|
}
|
|
975
|
-
drawGrid(layers.grid);
|
|
976
|
-
drawGuides(layers.guide, current);
|
|
1155
|
+
drawGrid(layers.grid, grid);
|
|
977
1156
|
drawGaps(layers.gap, current);
|
|
978
1157
|
drawIssues(layers.issue, current);
|
|
1158
|
+
const focused = focusId === null ? null : byId.get(focusId) ?? null;
|
|
1159
|
+
drawGuides(layers.guide, current, focused);
|
|
1160
|
+
const outlineNode = visible.has("focus") ? focused : null;
|
|
1161
|
+
const dimNode = visible.has("dim") ? focused : null;
|
|
1162
|
+
drawDim(layers.dim, dimNode);
|
|
1163
|
+
drawFocus(layers.focus, outlineNode);
|
|
1164
|
+
drawRadius(radiusLayer, outlineNode);
|
|
1165
|
+
badgeMarks = drawMarks(
|
|
1166
|
+
marksLayer,
|
|
1167
|
+
marks,
|
|
1168
|
+
new Map(current.nodes.map((node) => [node.selector, node])),
|
|
1169
|
+
marksInteractive
|
|
1170
|
+
);
|
|
979
1171
|
drawHover(layers.hover, hovered === null ? null : byId.get(hovered) ?? null);
|
|
980
1172
|
};
|
|
981
1173
|
const schedule = () => {
|
|
@@ -1020,12 +1212,26 @@ var __litedesign = (() => {
|
|
|
1020
1212
|
update(next) {
|
|
1021
1213
|
current = next;
|
|
1022
1214
|
byId = indexNodes(next);
|
|
1215
|
+
if (focusId !== null && !byId.has(focusId)) focusId = null;
|
|
1023
1216
|
draw();
|
|
1024
1217
|
},
|
|
1025
1218
|
setLayers(names) {
|
|
1026
1219
|
visible = new Set(names);
|
|
1027
1220
|
draw();
|
|
1028
1221
|
},
|
|
1222
|
+
setFocus(id) {
|
|
1223
|
+
focusId = id;
|
|
1224
|
+
draw();
|
|
1225
|
+
},
|
|
1226
|
+
setMarks(next, interactive = false) {
|
|
1227
|
+
marks = next;
|
|
1228
|
+
marksInteractive = interactive;
|
|
1229
|
+
draw();
|
|
1230
|
+
},
|
|
1231
|
+
setGrid(next) {
|
|
1232
|
+
grid = next;
|
|
1233
|
+
draw();
|
|
1234
|
+
},
|
|
1029
1235
|
destroy() {
|
|
1030
1236
|
if (frame !== 0) cancelAnimationFrame(frame);
|
|
1031
1237
|
if (hoverFrame !== 0) cancelAnimationFrame(hoverFrame);
|
|
@@ -1042,7 +1248,7 @@ var __litedesign = (() => {
|
|
|
1042
1248
|
}
|
|
1043
1249
|
|
|
1044
1250
|
// packages/core/src/index.ts
|
|
1045
|
-
var VERSION = "0.1
|
|
1251
|
+
var VERSION = "0.2.1";
|
|
1046
1252
|
return __toCommonJS(index_exports);
|
|
1047
1253
|
})();
|
|
1048
1254
|
//# sourceMappingURL=core.iife.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "litedesign",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"description": "Grid, gap, and alignment on the real web page.",
|
|
6
6
|
"permissions": ["activeTab", "scripting", "storage"],
|
|
7
7
|
"host_permissions": ["http://*/*", "https://*/*"],
|
package/dist/extension/sw.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -44,7 +44,7 @@ function isMode(value) {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
// packages/cli/src/version.ts
|
|
47
|
-
var VERSION = "0.1
|
|
47
|
+
var VERSION = "0.2.1";
|
|
48
48
|
|
|
49
49
|
// packages/cli/src/ws.ts
|
|
50
50
|
import { spawn } from "node:child_process";
|
|
@@ -371,7 +371,10 @@ var Hub = class _Hub {
|
|
|
371
371
|
const previous = this.#socketTab.get(socket);
|
|
372
372
|
if (previous !== void 0 && previous !== tabId) {
|
|
373
373
|
const old = this.#clients.get(previous);
|
|
374
|
-
if (old?.socket === socket)
|
|
374
|
+
if (old?.socket === socket) {
|
|
375
|
+
this.#clients.delete(previous);
|
|
376
|
+
this.#broadcast({ type: "page-gone", tabId: previous });
|
|
377
|
+
}
|
|
375
378
|
}
|
|
376
379
|
this.#clients.set(tabId, { ...page, socket });
|
|
377
380
|
this.#socketTab.set(socket, tabId);
|
|
@@ -1096,6 +1099,7 @@ function issue(input) {
|
|
|
1096
1099
|
selector: input.selector,
|
|
1097
1100
|
rule: input.rule,
|
|
1098
1101
|
severity: input.severity,
|
|
1102
|
+
kind: input.kind,
|
|
1099
1103
|
actual: input.actual,
|
|
1100
1104
|
expected: input.expected,
|
|
1101
1105
|
autoFix: input.autoFix
|
|
@@ -1133,6 +1137,7 @@ var alignmentNearMiss = defineRule("alignment-near-miss", (graph) => {
|
|
|
1133
1137
|
selector: worst.node.id,
|
|
1134
1138
|
key: spec.edge,
|
|
1135
1139
|
severity: "P2",
|
|
1140
|
+
kind: "candidate",
|
|
1136
1141
|
actual: `${Math.round(worst.value * 10) / 10}px`,
|
|
1137
1142
|
expected: `${Math.round(target.value * 10) / 10}px`,
|
|
1138
1143
|
autoFix: false
|
|
@@ -1161,6 +1166,7 @@ var contrastMinimum = defineRule("contrast", (graph) => {
|
|
|
1161
1166
|
rule: "contrast",
|
|
1162
1167
|
selector: node.id,
|
|
1163
1168
|
severity: "P1",
|
|
1169
|
+
kind: "violation",
|
|
1164
1170
|
actual: `${Math.round(ratio * 100) / 100}:1`,
|
|
1165
1171
|
expected: `>= ${minimum}:1`,
|
|
1166
1172
|
autoFix: false
|
|
@@ -1207,6 +1213,7 @@ var overflowClipped = defineRule("overflow", (graph) => {
|
|
|
1207
1213
|
selector: node.id,
|
|
1208
1214
|
key: "x",
|
|
1209
1215
|
severity: "P2",
|
|
1216
|
+
kind: "violation",
|
|
1210
1217
|
actual: `${node.box.scrollW}px content in ${node.box.clientW}px`,
|
|
1211
1218
|
expected: "content fits horizontally",
|
|
1212
1219
|
autoFix: false
|
|
@@ -1220,6 +1227,7 @@ var overflowClipped = defineRule("overflow", (graph) => {
|
|
|
1220
1227
|
selector: node.id,
|
|
1221
1228
|
key: "y",
|
|
1222
1229
|
severity: "P2",
|
|
1230
|
+
kind: "violation",
|
|
1223
1231
|
actual: `${node.box.scrollH}px content in ${node.box.clientH}px`,
|
|
1224
1232
|
expected: "content fits vertically",
|
|
1225
1233
|
autoFix: false
|
|
@@ -1256,6 +1264,7 @@ var radiusConsistency = defineRule("radius-consistency", (graph) => {
|
|
|
1256
1264
|
rule: "radius-consistency",
|
|
1257
1265
|
selector: node.id,
|
|
1258
1266
|
severity: "P3",
|
|
1267
|
+
kind: "candidate",
|
|
1259
1268
|
actual: value,
|
|
1260
1269
|
expected: expectedValue,
|
|
1261
1270
|
autoFix: true
|
|
@@ -1292,6 +1301,7 @@ var spacingScale = defineRule("spacing-scale", (graph) => {
|
|
|
1292
1301
|
selector: gap.a,
|
|
1293
1302
|
key: `${gap.axis}:${gap.b}`,
|
|
1294
1303
|
severity: "P2",
|
|
1304
|
+
kind: "candidate",
|
|
1295
1305
|
actual: `${actual}px`,
|
|
1296
1306
|
expected: `${Math.round(actual / SPACING_SCALE) * SPACING_SCALE}px`,
|
|
1297
1307
|
autoFix: true
|
|
@@ -1308,7 +1318,7 @@ var defaultRules = [
|
|
|
1308
1318
|
contrastMinimum
|
|
1309
1319
|
];
|
|
1310
1320
|
var RULE_IDS = defaultRules.map((rule) => rule.id);
|
|
1311
|
-
var LAYERS = ["grid", "guide", "gap", "issue", "hover"];
|
|
1321
|
+
var LAYERS = ["grid", "guide", "gap", "issue", "dim", "focus", "hover"];
|
|
1312
1322
|
|
|
1313
1323
|
// packages/cli/src/drivers/extension.ts
|
|
1314
1324
|
var ExtensionDriver = class {
|
|
@@ -1359,6 +1369,21 @@ var ExtensionDriver = class {
|
|
|
1359
1369
|
}
|
|
1360
1370
|
return result;
|
|
1361
1371
|
}
|
|
1372
|
+
async labels() {
|
|
1373
|
+
const result = await this.#request("labels");
|
|
1374
|
+
if (result === void 0) {
|
|
1375
|
+
throw new ToolError(
|
|
1376
|
+
"the extension did not report labels. Reload the litedesign extension at chrome://extensions, then try again."
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
const labels = [];
|
|
1380
|
+
for (const entry of result.labels ?? []) {
|
|
1381
|
+
if (typeof entry.text === "string" && typeof entry.selector === "string") {
|
|
1382
|
+
labels.push({ label: entry.text, selector: entry.selector });
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return labels;
|
|
1386
|
+
}
|
|
1362
1387
|
async close() {
|
|
1363
1388
|
}
|
|
1364
1389
|
#request(action, payload) {
|
|
@@ -1493,22 +1518,46 @@ var PlaywrightDriver = class {
|
|
|
1493
1518
|
);
|
|
1494
1519
|
return page.screenshot({ type: "png" });
|
|
1495
1520
|
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Labels are marks the human makes through the toolbar; a headless page has
|
|
1523
|
+
* no human in it, so there is nothing to read.
|
|
1524
|
+
*/
|
|
1525
|
+
async labels() {
|
|
1526
|
+
return [];
|
|
1527
|
+
}
|
|
1496
1528
|
async overlay(options) {
|
|
1497
1529
|
const page = await this.#requirePage();
|
|
1498
1530
|
await this.#inject(page);
|
|
1499
1531
|
const layers = options.enabled ? options.layers ?? [...LAYERS] : [];
|
|
1500
|
-
await page.evaluate(
|
|
1532
|
+
const result = await page.evaluate(
|
|
1501
1533
|
(input) => {
|
|
1502
1534
|
const target = window;
|
|
1503
1535
|
target.__litedesign_handle__?.destroy();
|
|
1504
1536
|
target.__litedesign_handle__ = void 0;
|
|
1505
|
-
if (!input.enabled)
|
|
1537
|
+
if (!input.enabled) {
|
|
1538
|
+
target.__litedesign_focus__ = null;
|
|
1539
|
+
return { enabled: false, layers: [], focus: null };
|
|
1540
|
+
}
|
|
1506
1541
|
const graph = target.__litedesign.buildGraph(document.body);
|
|
1507
|
-
|
|
1542
|
+
const handle = target.__litedesign.render(graph, { layers: input.layers });
|
|
1543
|
+
target.__litedesign_handle__ = handle;
|
|
1544
|
+
if (input.focus !== void 0) target.__litedesign_focus__ = input.focus;
|
|
1545
|
+
let focus = null;
|
|
1546
|
+
const wanted = input.focus !== void 0 ? input.focus : target.__litedesign_focus__ ?? null;
|
|
1547
|
+
if (wanted !== null) {
|
|
1548
|
+
const node = target.__litedesign.resolveNode(graph, wanted);
|
|
1549
|
+
if (node !== null) {
|
|
1550
|
+
handle.setFocus(node.id);
|
|
1551
|
+
focus = node.selector;
|
|
1552
|
+
const element = document.querySelector(node.selector);
|
|
1553
|
+
element?.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
return { enabled: true, layers: input.layers, focus };
|
|
1508
1557
|
},
|
|
1509
|
-
{ enabled: options.enabled, layers }
|
|
1558
|
+
{ enabled: options.enabled, layers, focus: options.focus }
|
|
1510
1559
|
);
|
|
1511
|
-
return
|
|
1560
|
+
return result;
|
|
1512
1561
|
}
|
|
1513
1562
|
async close() {
|
|
1514
1563
|
await this.#context?.close().catch(() => void 0);
|
|
@@ -1741,12 +1790,20 @@ function formatTree(graph, maxLines = MAX_TREE_LINES) {
|
|
|
1741
1790
|
}
|
|
1742
1791
|
return lines.join("\n");
|
|
1743
1792
|
}
|
|
1793
|
+
function labelsSummary(marks) {
|
|
1794
|
+
const names = [];
|
|
1795
|
+
for (const mark of marks) {
|
|
1796
|
+
if (!names.includes(mark.label)) names.push(mark.label);
|
|
1797
|
+
}
|
|
1798
|
+
return { count: marks.length, names };
|
|
1799
|
+
}
|
|
1744
1800
|
function issuePayload(issues, limit) {
|
|
1745
1801
|
return {
|
|
1746
1802
|
issues: issues.slice(0, limit).map((issue2) => ({
|
|
1747
1803
|
selector: issue2.selector,
|
|
1748
1804
|
rule: issue2.rule,
|
|
1749
1805
|
severity: issue2.severity,
|
|
1806
|
+
kind: issue2.kind,
|
|
1750
1807
|
actual: issue2.actual,
|
|
1751
1808
|
expected: issue2.expected,
|
|
1752
1809
|
autoFix: issue2.autoFix
|
|
@@ -1817,6 +1874,7 @@ async function inspectTool(input) {
|
|
|
1817
1874
|
throw error;
|
|
1818
1875
|
}
|
|
1819
1876
|
const graph = await driver.graph();
|
|
1877
|
+
const labels = labelsSummary(await driver.labels());
|
|
1820
1878
|
const payload = {
|
|
1821
1879
|
status: "ready",
|
|
1822
1880
|
url: graph.url,
|
|
@@ -1824,7 +1882,8 @@ async function inspectTool(input) {
|
|
|
1824
1882
|
summary: {
|
|
1825
1883
|
nodes: graph.nodes.length,
|
|
1826
1884
|
issues: graph.issues.length,
|
|
1827
|
-
truncated: graph.truncated
|
|
1885
|
+
truncated: graph.truncated,
|
|
1886
|
+
labels
|
|
1828
1887
|
},
|
|
1829
1888
|
tree: formatTree(graph)
|
|
1830
1889
|
};
|
|
@@ -1833,6 +1892,16 @@ async function inspectTool(input) {
|
|
|
1833
1892
|
return imageResult(payload, image);
|
|
1834
1893
|
}
|
|
1835
1894
|
|
|
1895
|
+
// packages/cli/src/mcp/tools/labels.ts
|
|
1896
|
+
async function labelsTool() {
|
|
1897
|
+
const driver = requirePage();
|
|
1898
|
+
const labels = await driver.labels();
|
|
1899
|
+
return textResult({
|
|
1900
|
+
labels,
|
|
1901
|
+
count: labels.length
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
|
|
1836
1905
|
// packages/cli/src/mcp/tools/measure.ts
|
|
1837
1906
|
async function measureTool(input) {
|
|
1838
1907
|
const measured = await requirePage().measure(input.selector);
|
|
@@ -1844,10 +1913,11 @@ async function overlayTool(input) {
|
|
|
1844
1913
|
const driver = requirePage();
|
|
1845
1914
|
const enabled = input.enabled !== false;
|
|
1846
1915
|
const layers = parseLayers(input.layers);
|
|
1847
|
-
const result = await driver.overlay({ enabled, layers });
|
|
1916
|
+
const result = await driver.overlay({ enabled, layers, focus: input.focus });
|
|
1848
1917
|
return textResult({
|
|
1849
1918
|
status: result.enabled ? "shown" : "hidden",
|
|
1850
|
-
layers: result.layers
|
|
1919
|
+
layers: result.layers,
|
|
1920
|
+
focus: result.focus
|
|
1851
1921
|
});
|
|
1852
1922
|
}
|
|
1853
1923
|
function parseLayers(requested) {
|
|
@@ -1876,6 +1946,17 @@ async function screenshotAnnotatedTool(input) {
|
|
|
1876
1946
|
return imageResult({ ...pngSize(image), bytes: image.length }, image);
|
|
1877
1947
|
}
|
|
1878
1948
|
|
|
1949
|
+
// packages/cli/src/mcp/workflow.ts
|
|
1950
|
+
var WORKFLOW_URI = "skill://litedesign/workflow";
|
|
1951
|
+
var WORKFLOW_TEXT = `# litedesign workflow
|
|
1952
|
+
|
|
1953
|
+
- Call \`inspect\` first, always. Its summary includes the labels the human pinned on the page.
|
|
1954
|
+
- If the human refers to a part of the page by name ("the login button", "the altcha box"), call \`labels\` and use the pinned selector \u2014 never guess CSS selectors.
|
|
1955
|
+
- Labels are read-only: you read them; the human creates, renames, and removes them.
|
|
1956
|
+
- To show the human what you are editing, call \`overlay\` with \`focus\`. Set \`layers\` once at the start if needed; after that, send \`focus\` only \u2014 the human owns the layer toggles in the toolbar.
|
|
1957
|
+
- Findings carry kind: "violation" is a measured fact; "candidate" is a heuristic that can be a false positive \u2014 verify it with \`measure\` before changing code.
|
|
1958
|
+
`;
|
|
1959
|
+
|
|
1879
1960
|
// packages/cli/src/mcp/server.ts
|
|
1880
1961
|
async function guard(run2) {
|
|
1881
1962
|
try {
|
|
@@ -1899,6 +1980,18 @@ async function runMcpServer() {
|
|
|
1899
1980
|
console.error(`[litedesign] extension bridge disabled: ${reason}`);
|
|
1900
1981
|
}
|
|
1901
1982
|
const server = new McpServer({ name: "litedesign", version: VERSION });
|
|
1983
|
+
server.registerResource(
|
|
1984
|
+
"workflow",
|
|
1985
|
+
WORKFLOW_URI,
|
|
1986
|
+
{
|
|
1987
|
+
title: "Workflow",
|
|
1988
|
+
description: "How the agent should work with this server: inspect first, resolve the human's names through labels, spotlight edits with focus.",
|
|
1989
|
+
mimeType: "text/markdown"
|
|
1990
|
+
},
|
|
1991
|
+
async () => ({
|
|
1992
|
+
contents: [{ uri: WORKFLOW_URI, text: WORKFLOW_TEXT, mimeType: "text/markdown" }]
|
|
1993
|
+
})
|
|
1994
|
+
);
|
|
1902
1995
|
const viewport = z.object({
|
|
1903
1996
|
w: z.number().int().positive().describe("Viewport width in CSS pixels"),
|
|
1904
1997
|
h: z.number().int().positive().describe("Viewport height in CSS pixels")
|
|
@@ -1931,7 +2024,7 @@ async function runMcpServer() {
|
|
|
1931
2024
|
"inspect",
|
|
1932
2025
|
{
|
|
1933
2026
|
title: "Inspect page",
|
|
1934
|
-
description: 'Open a page and return a tree summary. Call this before measure, find_issues, overlay, screenshot, or screenshot_annotated. No image by default: the numbers the agent needs come from the DOM, so set screenshot to true only when you really need to see the page. With the extension driver this attaches to the active tab by itself; returns "awaiting_user" when no extension is connected and "mismatch" when the active tab is a different page.',
|
|
2027
|
+
description: 'Open a page and return a tree summary. Call this before measure, find_issues, overlay, screenshot, or screenshot_annotated. The summary includes labels: the count and unique names the human pinned onto parts of the page \u2014 when the human refers to a part by name, get its selector from the labels tool. The full workflow is available as the resource skill://litedesign/workflow. No image by default: the numbers the agent needs come from the DOM, so set screenshot to true only when you really need to see the page. With the extension driver this attaches to the active tab by itself; returns "awaiting_user" when no extension is connected and "mismatch" when the active tab is a different page.',
|
|
1935
2028
|
inputSchema: {
|
|
1936
2029
|
url: z.string().url(),
|
|
1937
2030
|
viewport,
|
|
@@ -1968,7 +2061,7 @@ async function runMcpServer() {
|
|
|
1968
2061
|
"find_issues",
|
|
1969
2062
|
{
|
|
1970
2063
|
title: "Find issues",
|
|
1971
|
-
description:
|
|
2064
|
+
description: 'Run layout rules on the open page and return findings. Each finding carries kind: "violation" is a measured fact (overflow, contrast below WCAG); "candidate" is a heuristic judgment (spacing scale, near-miss alignment, radius consistency) that can be a false positive \u2014 verify it with measure before changing code. Call inspect first.',
|
|
1972
2065
|
inputSchema: {
|
|
1973
2066
|
rules: z.array(z.string()).optional().describe(`Rules to run. Default: all. Valid: ${RULE_IDS.join(", ")}`),
|
|
1974
2067
|
limit: z.number().int().min(1).max(200).optional().describe("Maximum issues to return (default 50)")
|
|
@@ -1980,14 +2073,26 @@ async function runMcpServer() {
|
|
|
1980
2073
|
"overlay",
|
|
1981
2074
|
{
|
|
1982
2075
|
title: "Overlay",
|
|
1983
|
-
description: "Show or hide the measurement overlay in the page the human is looking at: grid, guides, gap labels, and issue boxes. Use it to
|
|
2076
|
+
description: "Show or hide the measurement overlay in the page the human is looking at: grid, guides, gap labels, and issue boxes. Use focus to spotlight the component you are editing: the rest of the page dims, the target is outlined, and it scrolls to the viewport center \u2014 so the human sees exactly what you are changing. focus takes the same selector measure returns; null clears it. Set layers once at the start if you need specific ones; after that call with focus only \u2014 the human owns the layer toggles in the toolbar, and your calls must not override their choice. Call inspect first.",
|
|
1984
2077
|
inputSchema: {
|
|
1985
2078
|
enabled: z.boolean().optional().describe("false hides the overlay (default true)"),
|
|
1986
|
-
layers: z.array(z.string()).optional().describe(`Layers to draw. Default: all. Valid: ${LAYERS.join(", ")}`)
|
|
2079
|
+
layers: z.array(z.string()).optional().describe(`Layers to draw. Default: all. Valid: ${LAYERS.join(", ")}`),
|
|
2080
|
+
focus: z.string().nullish().describe(
|
|
2081
|
+
"Selector of the component to spotlight (same format as measure). null clears the focus. Omit to leave it unchanged. Focusing scrolls the page, which changes what a later screenshot captures."
|
|
2082
|
+
)
|
|
1987
2083
|
}
|
|
1988
2084
|
},
|
|
1989
2085
|
async (input) => guard(() => overlayTool(input))
|
|
1990
2086
|
);
|
|
2087
|
+
server.registerTool(
|
|
2088
|
+
"labels",
|
|
2089
|
+
{
|
|
2090
|
+
title: "Labels",
|
|
2091
|
+
description: "Read the names the human pinned onto parts of the page through the toolbar label button. Each entry maps a human name to a selector you can pass to measure, find_issues, or overlay focus. When the human refers to a part by name, look it up here instead of guessing a selector. Labels are the human's marks: read-only for you. Returns an empty list when nothing is pinned yet.",
|
|
2092
|
+
inputSchema: {}
|
|
2093
|
+
},
|
|
2094
|
+
async () => guard(() => labelsTool())
|
|
2095
|
+
);
|
|
1991
2096
|
server.registerTool(
|
|
1992
2097
|
"screenshot",
|
|
1993
2098
|
{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rahmantech/litedesign",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Grid, gap, and alignment on the real web page, plus an MCP bridge for agents.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Muhammad Abdurrahman",
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"dist",
|
|
13
|
+
"!dist/**/*.map",
|
|
13
14
|
"README.md"
|
|
14
15
|
],
|
|
15
16
|
"engines": {
|
|
@@ -21,7 +22,7 @@
|
|
|
21
22
|
"zod": "^4.6.7"
|
|
22
23
|
},
|
|
23
24
|
"devDependencies": {
|
|
24
|
-
"@litedesign/core": "0.1
|
|
25
|
+
"@litedesign/core": "0.2.1",
|
|
25
26
|
"@types/ws": "^8.18.2"
|
|
26
27
|
},
|
|
27
28
|
"publishConfig": {
|