@rahmantech/litedesign 0.2.0 → 0.2.2
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 +5 -0
- package/dist/extension/content.js +105 -24
- package/dist/extension/core.iife.js +30 -6
- package/dist/extension/manifest.json +1 -1
- package/dist/extension/popup.js +32 -7
- package/dist/extension/sw.js +33 -0
- package/dist/index.js +88 -12
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -74,6 +74,11 @@ scrolled viewport.
|
|
|
74
74
|
toolbar. When the human refers to a part by name, look it up there instead of
|
|
75
75
|
guessing a selector. Labels are read-only for the agent.
|
|
76
76
|
|
|
77
|
+
The server also serves its workflow as the MCP resource
|
|
78
|
+
`skill://litedesign/workflow` — an agent that supports resources can read the
|
|
79
|
+
flow (inspect first, resolve names through labels, spotlight edits with focus)
|
|
80
|
+
without a file in the repository.
|
|
81
|
+
|
|
77
82
|
## Drivers
|
|
78
83
|
|
|
79
84
|
The server prefers Playwright when `playwright-core` is installed. It launches
|
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
(() => {
|
|
3
|
+
// packages/extension/src/diff.ts
|
|
4
|
+
function signatureOf(node) {
|
|
5
|
+
return `${node.tag}|${node.text}`;
|
|
6
|
+
}
|
|
7
|
+
function nodeSignatures(graph) {
|
|
8
|
+
return graph.nodes.map(signatureOf);
|
|
9
|
+
}
|
|
10
|
+
function changedNodes(before, after) {
|
|
11
|
+
const counts = /* @__PURE__ */ new Map();
|
|
12
|
+
for (const signature of before) counts.set(signature, (counts.get(signature) ?? 0) + 1);
|
|
13
|
+
const changed = [];
|
|
14
|
+
for (const node of after.nodes) {
|
|
15
|
+
const signature = signatureOf(node);
|
|
16
|
+
const seen = counts.get(signature) ?? 0;
|
|
17
|
+
if (seen > 0) counts.set(signature, seen - 1);
|
|
18
|
+
else changed.push(node);
|
|
19
|
+
}
|
|
20
|
+
return changed;
|
|
21
|
+
}
|
|
22
|
+
|
|
3
23
|
// packages/extension/src/labels.ts
|
|
4
24
|
var STORE_KEY = "litedesign-labels";
|
|
5
25
|
function pageKey() {
|
|
@@ -129,6 +149,7 @@
|
|
|
129
149
|
|
|
130
150
|
// packages/extension/src/store.ts
|
|
131
151
|
var STORAGE_KEY = "litedesign";
|
|
152
|
+
var SNAPSHOT_KEY = "litedesign-snapshot";
|
|
132
153
|
var DEFAULT_LAYERS = [...LAYERS];
|
|
133
154
|
var DEFAULT_GRID = { step: 8, axis: "both" };
|
|
134
155
|
var AXES = ["vertical", "horizontal", "both"];
|
|
@@ -186,6 +207,30 @@
|
|
|
186
207
|
function isLayerName(value) {
|
|
187
208
|
return typeof value === "string" && LAYERS.includes(value);
|
|
188
209
|
}
|
|
210
|
+
function loadSnapshot() {
|
|
211
|
+
try {
|
|
212
|
+
const raw = sessionStorage.getItem(SNAPSHOT_KEY);
|
|
213
|
+
if (raw === null) return null;
|
|
214
|
+
const parsed = JSON.parse(raw);
|
|
215
|
+
if (!Array.isArray(parsed)) return null;
|
|
216
|
+
const signatures = parsed.filter((entry) => typeof entry === "string");
|
|
217
|
+
return signatures.length === parsed.length ? signatures : null;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function saveSnapshot(graph) {
|
|
223
|
+
try {
|
|
224
|
+
sessionStorage.setItem(SNAPSHOT_KEY, JSON.stringify(nodeSignatures(graph)));
|
|
225
|
+
} catch {
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
function clearSnapshot() {
|
|
229
|
+
try {
|
|
230
|
+
sessionStorage.removeItem(SNAPSHOT_KEY);
|
|
231
|
+
} catch {
|
|
232
|
+
}
|
|
233
|
+
}
|
|
189
234
|
|
|
190
235
|
// packages/extension/src/toolbar.ts
|
|
191
236
|
var STYLE = `
|
|
@@ -524,18 +569,21 @@
|
|
|
524
569
|
}
|
|
525
570
|
const stored = await loadState();
|
|
526
571
|
const layers = options.layers ?? stored.layers;
|
|
572
|
+
let autoFocus = null;
|
|
527
573
|
if (window.__litedesign_state__ === "active") {
|
|
528
574
|
window.__litedesign_layers__?.(layers);
|
|
529
575
|
} else {
|
|
530
576
|
window.__litedesign_state__ = "starting";
|
|
531
577
|
try {
|
|
532
|
-
mount(core2, layers, stored.grid);
|
|
578
|
+
autoFocus = mount(core2, layers, stored.grid);
|
|
533
579
|
} catch (error) {
|
|
534
580
|
window.__litedesign_state__ = void 0;
|
|
535
581
|
throw error;
|
|
536
582
|
}
|
|
537
583
|
}
|
|
538
|
-
if (options.focus !== void 0)
|
|
584
|
+
if (options.focus !== void 0 && autoFocus === null) {
|
|
585
|
+
window.__litedesign_set_focus__?.(options.focus);
|
|
586
|
+
}
|
|
539
587
|
return {
|
|
540
588
|
enabled: overlayHost() !== void 0,
|
|
541
589
|
layers,
|
|
@@ -553,6 +601,7 @@
|
|
|
553
601
|
let focusId = null;
|
|
554
602
|
let labels = [];
|
|
555
603
|
let labelMode = false;
|
|
604
|
+
let marksInteractive = false;
|
|
556
605
|
let toolbar;
|
|
557
606
|
const teardown = () => {
|
|
558
607
|
observer?.stop();
|
|
@@ -569,17 +618,22 @@
|
|
|
569
618
|
window.__litedesign_set_focus__ = void 0;
|
|
570
619
|
window.__litedesign_focus_selector__ = void 0;
|
|
571
620
|
window.__litedesign_state__ = void 0;
|
|
621
|
+
clearSnapshot();
|
|
572
622
|
void saveState({ enabled: false, layers, focus: null, grid });
|
|
573
623
|
};
|
|
574
624
|
const refresh = () => {
|
|
625
|
+
const before2 = nodeSignatures(graph);
|
|
575
626
|
graph = core2.buildGraph(root2);
|
|
576
627
|
if (focusId !== null && !graph.nodes.some((node) => node.id === focusId)) {
|
|
577
628
|
focusSelector = null;
|
|
578
629
|
focusId = null;
|
|
579
630
|
handle?.setFocus(null);
|
|
580
631
|
}
|
|
632
|
+
const [firstChanged2] = changedNodes(before2, graph);
|
|
633
|
+
if (firstChanged2 !== void 0) setFocus(firstChanged2.selector);
|
|
581
634
|
handle?.update(graph);
|
|
582
635
|
toolbar.setStats(graph.nodes.length, graph.issues.length, graph.truncated);
|
|
636
|
+
saveSnapshot(graph);
|
|
583
637
|
};
|
|
584
638
|
const setLayers = (next) => {
|
|
585
639
|
layers = next;
|
|
@@ -622,7 +676,30 @@
|
|
|
622
676
|
void saveState({ enabled: true, layers, focus: focusSelector, grid });
|
|
623
677
|
};
|
|
624
678
|
const applyMarks = () => {
|
|
625
|
-
handle?.setMarks(
|
|
679
|
+
handle?.setMarks(
|
|
680
|
+
labels.map((label) => ({ selector: label.selector, text: label.text })),
|
|
681
|
+
marksInteractive
|
|
682
|
+
);
|
|
683
|
+
};
|
|
684
|
+
const editLabel = (existing, rect) => {
|
|
685
|
+
toolbar.askLabel(
|
|
686
|
+
rect,
|
|
687
|
+
existing.text,
|
|
688
|
+
(text) => {
|
|
689
|
+
const trimmed = text.trim();
|
|
690
|
+
if (trimmed === "" || trimmed === existing.text) return;
|
|
691
|
+
void renameLabel(existing.id, trimmed).then((next) => {
|
|
692
|
+
labels = next;
|
|
693
|
+
applyMarks();
|
|
694
|
+
});
|
|
695
|
+
},
|
|
696
|
+
() => {
|
|
697
|
+
void removeLabel(existing.id).then((next) => {
|
|
698
|
+
labels = next;
|
|
699
|
+
applyMarks();
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
);
|
|
626
703
|
};
|
|
627
704
|
const onLabelClick = (event) => {
|
|
628
705
|
const target = event.target;
|
|
@@ -633,24 +710,7 @@
|
|
|
633
710
|
const selector = core2.pathOf(target);
|
|
634
711
|
const existing = labels.find((label) => label.selector === selector);
|
|
635
712
|
if (existing !== void 0) {
|
|
636
|
-
|
|
637
|
-
target.getBoundingClientRect(),
|
|
638
|
-
existing.text,
|
|
639
|
-
(text) => {
|
|
640
|
-
const trimmed = text.trim();
|
|
641
|
-
if (trimmed === "" || trimmed === existing.text) return;
|
|
642
|
-
void renameLabel(existing.id, trimmed).then((next) => {
|
|
643
|
-
labels = next;
|
|
644
|
-
applyMarks();
|
|
645
|
-
});
|
|
646
|
-
},
|
|
647
|
-
() => {
|
|
648
|
-
void removeLabel(existing.id).then((next) => {
|
|
649
|
-
labels = next;
|
|
650
|
-
applyMarks();
|
|
651
|
-
});
|
|
652
|
-
}
|
|
653
|
-
);
|
|
713
|
+
editLabel(existing, target.getBoundingClientRect());
|
|
654
714
|
return;
|
|
655
715
|
}
|
|
656
716
|
toolbar.askLabel(target.getBoundingClientRect(), "", (text) => {
|
|
@@ -670,6 +730,8 @@
|
|
|
670
730
|
} else {
|
|
671
731
|
document.removeEventListener("click", onLabelClick, true);
|
|
672
732
|
}
|
|
733
|
+
marksInteractive = labelMode;
|
|
734
|
+
applyMarks();
|
|
673
735
|
};
|
|
674
736
|
toolbar = createToolbar({
|
|
675
737
|
layers,
|
|
@@ -684,12 +746,29 @@
|
|
|
684
746
|
onClose: teardown
|
|
685
747
|
});
|
|
686
748
|
toolbar.setStats(graph.nodes.length, graph.issues.length, graph.truncated);
|
|
687
|
-
handle = core2.render(graph, {
|
|
749
|
+
handle = core2.render(graph, {
|
|
750
|
+
layers,
|
|
751
|
+
toolbar: toolbar.element,
|
|
752
|
+
grid,
|
|
753
|
+
onMarkClick: (mark, anchor) => {
|
|
754
|
+
const existing = labels.find((label) => label.selector === mark.selector);
|
|
755
|
+
if (existing !== void 0) editLabel(existing, anchor);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
688
758
|
observer = observeChanges({ root: root2, onChange: refresh });
|
|
689
759
|
void loadLabels().then((next) => {
|
|
690
760
|
labels = next;
|
|
691
761
|
applyMarks();
|
|
692
762
|
});
|
|
763
|
+
const before = loadSnapshot();
|
|
764
|
+
const changed = before !== null ? changedNodes(before, graph) : [];
|
|
765
|
+
let autoFocus = null;
|
|
766
|
+
const [firstChanged] = changed;
|
|
767
|
+
if (firstChanged !== void 0) {
|
|
768
|
+
autoFocus = firstChanged.selector;
|
|
769
|
+
setFocus(firstChanged.selector);
|
|
770
|
+
}
|
|
771
|
+
saveSnapshot(graph);
|
|
693
772
|
mountedHost = handle.host;
|
|
694
773
|
window.__litedesign_layers__ = setLayers;
|
|
695
774
|
window.__litedesign_set_focus__ = setFocus;
|
|
@@ -697,6 +776,7 @@
|
|
|
697
776
|
window.__litedesign_teardown__ = teardown;
|
|
698
777
|
window.__litedesign_state__ = "active";
|
|
699
778
|
void saveState({ enabled: true, layers, focus: focusSelector, grid });
|
|
779
|
+
return autoFocus;
|
|
700
780
|
}
|
|
701
781
|
|
|
702
782
|
// packages/extension/src/bridge.ts
|
|
@@ -723,9 +803,10 @@
|
|
|
723
803
|
switch (action) {
|
|
724
804
|
case "ping":
|
|
725
805
|
return { ok: true, result: true };
|
|
726
|
-
case "toggle":
|
|
806
|
+
case "toggle": {
|
|
727
807
|
await boot(core2);
|
|
728
|
-
return { ok: true };
|
|
808
|
+
return { ok: true, result: { enabled: window.__litedesign_state__ === "active" } };
|
|
809
|
+
}
|
|
729
810
|
case "overlay":
|
|
730
811
|
return {
|
|
731
812
|
ok: true,
|
|
@@ -980,9 +980,10 @@ var __litedesign = (() => {
|
|
|
980
980
|
return layer;
|
|
981
981
|
}
|
|
982
982
|
var MARK_COLOR = "rgba(217, 119, 6, 0.95)";
|
|
983
|
-
function drawMarks(layer, marks, bySelector) {
|
|
983
|
+
function drawMarks(layer, marks, bySelector, interactive = false) {
|
|
984
984
|
layer.replaceChildren();
|
|
985
|
-
|
|
985
|
+
const badges = /* @__PURE__ */ new Map();
|
|
986
|
+
if (marks.length === 0) return badges;
|
|
986
987
|
const scrollX = window.scrollX;
|
|
987
988
|
const scrollY = window.scrollY;
|
|
988
989
|
for (const mark of marks) {
|
|
@@ -990,10 +991,12 @@ var __litedesign = (() => {
|
|
|
990
991
|
if (node === void 0) continue;
|
|
991
992
|
const badge = box(
|
|
992
993
|
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
|
+
`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" : "")
|
|
994
995
|
);
|
|
995
996
|
badge.textContent = mark.text;
|
|
997
|
+
badges.set(badge, mark);
|
|
996
998
|
}
|
|
999
|
+
return badges;
|
|
997
1000
|
}
|
|
998
1001
|
function drawRadius(layer, node) {
|
|
999
1002
|
layer.replaceChildren();
|
|
@@ -1117,6 +1120,19 @@ var __litedesign = (() => {
|
|
|
1117
1120
|
const layers = createLayers(shadow);
|
|
1118
1121
|
const radiusLayer = createSubLayer("radius", layers.hover);
|
|
1119
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
|
+
}
|
|
1120
1136
|
if (options.toolbar !== void 0) shadow.append(options.toolbar);
|
|
1121
1137
|
document.documentElement.append(host);
|
|
1122
1138
|
let current = graph;
|
|
@@ -1127,6 +1143,8 @@ var __litedesign = (() => {
|
|
|
1127
1143
|
let focusId = null;
|
|
1128
1144
|
let visible = new Set(options.layers ?? LAYERS);
|
|
1129
1145
|
let marks = options.marks ?? [];
|
|
1146
|
+
let marksInteractive = false;
|
|
1147
|
+
let badgeMarks = /* @__PURE__ */ new Map();
|
|
1130
1148
|
let grid = options.grid ?? { step: GRID_STEP, axis: "both" };
|
|
1131
1149
|
let frame = 0;
|
|
1132
1150
|
const draw = () => {
|
|
@@ -1144,7 +1162,12 @@ var __litedesign = (() => {
|
|
|
1144
1162
|
drawDim(layers.dim, dimNode);
|
|
1145
1163
|
drawFocus(layers.focus, outlineNode);
|
|
1146
1164
|
drawRadius(radiusLayer, outlineNode);
|
|
1147
|
-
|
|
1165
|
+
badgeMarks = drawMarks(
|
|
1166
|
+
marksLayer,
|
|
1167
|
+
marks,
|
|
1168
|
+
new Map(current.nodes.map((node) => [node.selector, node])),
|
|
1169
|
+
marksInteractive
|
|
1170
|
+
);
|
|
1148
1171
|
drawHover(layers.hover, hovered === null ? null : byId.get(hovered) ?? null);
|
|
1149
1172
|
};
|
|
1150
1173
|
const schedule = () => {
|
|
@@ -1200,8 +1223,9 @@ var __litedesign = (() => {
|
|
|
1200
1223
|
focusId = id;
|
|
1201
1224
|
draw();
|
|
1202
1225
|
},
|
|
1203
|
-
setMarks(next) {
|
|
1226
|
+
setMarks(next, interactive = false) {
|
|
1204
1227
|
marks = next;
|
|
1228
|
+
marksInteractive = interactive;
|
|
1205
1229
|
draw();
|
|
1206
1230
|
},
|
|
1207
1231
|
setGrid(next) {
|
|
@@ -1224,7 +1248,7 @@ var __litedesign = (() => {
|
|
|
1224
1248
|
}
|
|
1225
1249
|
|
|
1226
1250
|
// packages/core/src/index.ts
|
|
1227
|
-
var VERSION = "0.2.
|
|
1251
|
+
var VERSION = "0.2.2";
|
|
1228
1252
|
return __toCommonJS(index_exports);
|
|
1229
1253
|
})();
|
|
1230
1254
|
//# sourceMappingURL=core.iife.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 3,
|
|
3
3
|
"name": "litedesign",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.2.2",
|
|
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/popup.js
CHANGED
|
@@ -6,9 +6,12 @@
|
|
|
6
6
|
var watchButton = element("watch");
|
|
7
7
|
var overlayButton = element("overlay");
|
|
8
8
|
var tab;
|
|
9
|
-
watchButton.addEventListener("click", () => void
|
|
9
|
+
watchButton.addEventListener("click", () => void toggleWatch());
|
|
10
10
|
overlayButton.addEventListener("click", () => void toggleOverlay());
|
|
11
11
|
void init();
|
|
12
|
+
document.addEventListener("visibilitychange", () => {
|
|
13
|
+
if (!document.hidden) void init();
|
|
14
|
+
});
|
|
12
15
|
async function init() {
|
|
13
16
|
const [active] = await chrome.tabs.query({ active: true, currentWindow: true });
|
|
14
17
|
tab = active;
|
|
@@ -17,14 +20,22 @@
|
|
|
17
20
|
overlayButton.disabled = active?.id === void 0;
|
|
18
21
|
await refresh();
|
|
19
22
|
}
|
|
20
|
-
async function
|
|
23
|
+
async function toggleWatch() {
|
|
21
24
|
if (tab?.id === void 0) return;
|
|
22
|
-
const
|
|
25
|
+
const status = await ask({ type: "status" });
|
|
26
|
+
if (!status.ok) {
|
|
27
|
+
setStatus(status.error, true);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const watching = status.result.page?.tabId === tab.id;
|
|
31
|
+
const reply = await ask(
|
|
32
|
+
watching ? { type: "unwatch", tabId: tab.id } : { type: "watch", tabId: tab.id }
|
|
33
|
+
);
|
|
23
34
|
if (!reply.ok) {
|
|
24
35
|
setStatus(reply.error, true);
|
|
25
36
|
return;
|
|
26
37
|
}
|
|
27
|
-
pageLine.textContent = reply.result.url;
|
|
38
|
+
if (!watching && reply.result !== null) pageLine.textContent = reply.result.url;
|
|
28
39
|
await refresh();
|
|
29
40
|
}
|
|
30
41
|
async function toggleOverlay() {
|
|
@@ -34,7 +45,12 @@
|
|
|
34
45
|
setStatus(reply.error, true);
|
|
35
46
|
return;
|
|
36
47
|
}
|
|
37
|
-
|
|
48
|
+
await refresh();
|
|
49
|
+
if (reply.result.enabled) {
|
|
50
|
+
window.close();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
setStatus("overlay off");
|
|
38
54
|
}
|
|
39
55
|
async function refresh() {
|
|
40
56
|
const reply = await ask({ type: "status" });
|
|
@@ -43,14 +59,23 @@
|
|
|
43
59
|
return;
|
|
44
60
|
}
|
|
45
61
|
const status = reply.result;
|
|
62
|
+
if (!status.bridge) scheduleRecheck();
|
|
46
63
|
const watching = tab?.id !== void 0 && status.page?.tabId === tab.id;
|
|
47
|
-
watchButton.textContent = watching ? "
|
|
48
|
-
watchButton.disabled = tab?.id === void 0
|
|
64
|
+
watchButton.textContent = watching ? "Unwatch this tab" : "Watch this tab";
|
|
65
|
+
watchButton.disabled = tab?.id === void 0;
|
|
49
66
|
setStatus(
|
|
50
67
|
status.bridge ? `bridge online \xB7 port ${status.port}` : "bridge offline \xB7 run: litedesign ws start",
|
|
51
68
|
!status.bridge
|
|
52
69
|
);
|
|
53
70
|
}
|
|
71
|
+
function scheduleRecheck() {
|
|
72
|
+
if (recheckTimer !== void 0) return;
|
|
73
|
+
recheckTimer = setTimeout(() => {
|
|
74
|
+
recheckTimer = void 0;
|
|
75
|
+
void refresh();
|
|
76
|
+
}, 1e3);
|
|
77
|
+
}
|
|
78
|
+
var recheckTimer;
|
|
54
79
|
function setStatus(text, bad = false) {
|
|
55
80
|
statusLine.textContent = text;
|
|
56
81
|
statusLine.classList.toggle("bad", bad);
|
package/dist/extension/sw.js
CHANGED
|
@@ -46,6 +46,9 @@ async function answer(message) {
|
|
|
46
46
|
case "watch":
|
|
47
47
|
if (typeof request.tabId !== "number") throw new Error("watch needs a tab id");
|
|
48
48
|
return { ok: true, result: await watch(request.tabId) };
|
|
49
|
+
case "unwatch":
|
|
50
|
+
if (typeof request.tabId !== "number") throw new Error("unwatch needs a tab id");
|
|
51
|
+
return { ok: true, result: await unwatch(request.tabId) };
|
|
49
52
|
case "overlay":
|
|
50
53
|
if (typeof request.tabId !== "number") throw new Error("overlay needs a tab id");
|
|
51
54
|
return { ok: true, result: await toggleOverlay(request.tabId) };
|
|
@@ -74,6 +77,18 @@ async function watch(tabId) {
|
|
|
74
77
|
await applyStatus(tabId);
|
|
75
78
|
return page;
|
|
76
79
|
}
|
|
80
|
+
async function unwatch(tabId) {
|
|
81
|
+
if (current?.tabId !== tabId) return null;
|
|
82
|
+
try {
|
|
83
|
+
await chrome.tabs.sendMessage(tabId, { action: "overlay", payload: { enabled: false } });
|
|
84
|
+
} catch {
|
|
85
|
+
}
|
|
86
|
+
current = void 0;
|
|
87
|
+
await savePage(void 0);
|
|
88
|
+
send({ type: "bye" });
|
|
89
|
+
await applyStatus();
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
77
92
|
async function toggleOverlay(tabId) {
|
|
78
93
|
if (current?.tabId !== tabId) await watch(tabId);
|
|
79
94
|
return toTab("toggle");
|
|
@@ -87,7 +102,25 @@ chrome.tabs.onRemoved.addListener((tabId) => {
|
|
|
87
102
|
chrome.tabs.onUpdated.addListener((tabId, info) => {
|
|
88
103
|
if (info.status !== "complete" || current?.tabId !== tabId) return;
|
|
89
104
|
void ensureContent(tabId).catch(() => void 0);
|
|
105
|
+
void refreshWatchedPage(tabId).catch(() => void 0);
|
|
90
106
|
});
|
|
107
|
+
async function refreshWatchedPage(tabId) {
|
|
108
|
+
const tab = await chrome.tabs.get(tabId);
|
|
109
|
+
if (current?.tabId !== tabId) return;
|
|
110
|
+
if (tab.url === current.url) return;
|
|
111
|
+
if (!isSupported(tab.url)) {
|
|
112
|
+
current = void 0;
|
|
113
|
+
await savePage(void 0);
|
|
114
|
+
send({ type: "bye" });
|
|
115
|
+
await applyStatus();
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const page = { tabId, url: tab.url ?? "", title: tab.title ?? "" };
|
|
119
|
+
current = page;
|
|
120
|
+
await savePage(page);
|
|
121
|
+
send({ type: "hello", ...page, version: chrome.runtime.getManifest().version });
|
|
122
|
+
await applyStatus();
|
|
123
|
+
}
|
|
91
124
|
function connect() {
|
|
92
125
|
if (socket !== void 0 && socket.readyState <= WebSocket.OPEN) return;
|
|
93
126
|
const next = new WebSocket(`ws://127.0.0.1:${port}`);
|
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.
|
|
47
|
+
var VERSION = "0.2.2";
|
|
48
48
|
|
|
49
49
|
// packages/cli/src/ws.ts
|
|
50
50
|
import { spawn } from "node:child_process";
|
|
@@ -1089,6 +1089,7 @@ function contrastRatio(a, b) {
|
|
|
1089
1089
|
return (lighter + 0.05) / (darker + 0.05);
|
|
1090
1090
|
}
|
|
1091
1091
|
var SPACING_SCALE = 4;
|
|
1092
|
+
var OVERLAY_HOST_ID = "__litedesign_overlay__";
|
|
1092
1093
|
function defineRule(id, run2) {
|
|
1093
1094
|
return Object.assign(run2, { id });
|
|
1094
1095
|
}
|
|
@@ -1329,7 +1330,14 @@ var ExtensionDriver = class {
|
|
|
1329
1330
|
}
|
|
1330
1331
|
async open(url) {
|
|
1331
1332
|
this.#target = url;
|
|
1332
|
-
|
|
1333
|
+
const cached = this.#hub.find(url);
|
|
1334
|
+
if (cached !== void 0) {
|
|
1335
|
+
try {
|
|
1336
|
+
await this.#hub.request(cached.tabId, "ping", void 0);
|
|
1337
|
+
return;
|
|
1338
|
+
} catch {
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1333
1341
|
if (!this.#hub.hasClient()) throw new AwaitingUserError(url);
|
|
1334
1342
|
const page = await this.#hub.requestAny("attach");
|
|
1335
1343
|
if (!sameUrl(page.url, url)) throw new MismatchError(url, page.url);
|
|
@@ -1413,6 +1421,10 @@ import { existsSync as existsSync5 } from "node:fs";
|
|
|
1413
1421
|
import path4 from "node:path";
|
|
1414
1422
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
1415
1423
|
var DEFAULT_VIEWPORT = { width: 1280, height: 800 };
|
|
1424
|
+
var POLL_MS = 150;
|
|
1425
|
+
function sleep(ms) {
|
|
1426
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
1427
|
+
}
|
|
1416
1428
|
var PlaywrightDriver = class {
|
|
1417
1429
|
#playwright;
|
|
1418
1430
|
#corePath;
|
|
@@ -1431,6 +1443,25 @@ var PlaywrightDriver = class {
|
|
|
1431
1443
|
await page.goto(url, { waitUntil: "load" });
|
|
1432
1444
|
await this.#inject(page);
|
|
1433
1445
|
}
|
|
1446
|
+
/**
|
|
1447
|
+
* "Ready" means the page was read, not that `open` once succeeded: the tab
|
|
1448
|
+
* can be closed or navigated away after that. The loop checks liveness and
|
|
1449
|
+
* the URL the session asked for.
|
|
1450
|
+
*/
|
|
1451
|
+
async waitForPage(url, timeoutMs) {
|
|
1452
|
+
for (let waited = 0; waited < timeoutMs; waited += POLL_MS) {
|
|
1453
|
+
const page = this.#page;
|
|
1454
|
+
if (page !== void 0 && !page.isClosed() && sameUrl(page.url(), url)) {
|
|
1455
|
+
return { status: "ready", url: page.url() };
|
|
1456
|
+
}
|
|
1457
|
+
const current2 = this.#page;
|
|
1458
|
+
if (current2 !== void 0 && !current2.isClosed()) {
|
|
1459
|
+
return { status: "mismatch", url: current2.url() };
|
|
1460
|
+
}
|
|
1461
|
+
await sleep(POLL_MS);
|
|
1462
|
+
}
|
|
1463
|
+
return { status: "timeout" };
|
|
1464
|
+
}
|
|
1434
1465
|
async graph() {
|
|
1435
1466
|
const page = await this.#requirePage();
|
|
1436
1467
|
await this.#inject(page);
|
|
@@ -1520,10 +1551,11 @@ var PlaywrightDriver = class {
|
|
|
1520
1551
|
}
|
|
1521
1552
|
/**
|
|
1522
1553
|
* Labels are marks the human makes through the toolbar; a headless page has
|
|
1523
|
-
* no human in it, so
|
|
1554
|
+
* no human in it. `null` says "cannot know", so `inspect` can report the
|
|
1555
|
+
* truth instead of a `count: 0` that reads as "no labels pinned".
|
|
1524
1556
|
*/
|
|
1525
1557
|
async labels() {
|
|
1526
|
-
return
|
|
1558
|
+
return null;
|
|
1527
1559
|
}
|
|
1528
1560
|
async overlay(options) {
|
|
1529
1561
|
const page = await this.#requirePage();
|
|
@@ -1536,7 +1568,7 @@ var PlaywrightDriver = class {
|
|
|
1536
1568
|
target.__litedesign_handle__ = void 0;
|
|
1537
1569
|
if (!input.enabled) {
|
|
1538
1570
|
target.__litedesign_focus__ = null;
|
|
1539
|
-
return {
|
|
1571
|
+
return { applied: false, focus: null };
|
|
1540
1572
|
}
|
|
1541
1573
|
const graph = target.__litedesign.buildGraph(document.body);
|
|
1542
1574
|
const handle = target.__litedesign.render(graph, { layers: input.layers });
|
|
@@ -1553,11 +1585,19 @@ var PlaywrightDriver = class {
|
|
|
1553
1585
|
element?.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
|
|
1554
1586
|
}
|
|
1555
1587
|
}
|
|
1556
|
-
return {
|
|
1588
|
+
return { applied: true, focus };
|
|
1557
1589
|
},
|
|
1558
1590
|
{ enabled: options.enabled, layers, focus: options.focus }
|
|
1559
1591
|
);
|
|
1560
|
-
|
|
1592
|
+
const attached = await page.evaluate(
|
|
1593
|
+
(hostId) => document.getElementById(hostId) !== null,
|
|
1594
|
+
OVERLAY_HOST_ID
|
|
1595
|
+
);
|
|
1596
|
+
return {
|
|
1597
|
+
enabled: attached,
|
|
1598
|
+
layers: attached ? layers : [],
|
|
1599
|
+
focus: attached ? result.focus : null
|
|
1600
|
+
};
|
|
1561
1601
|
}
|
|
1562
1602
|
async close() {
|
|
1563
1603
|
await this.#context?.close().catch(() => void 0);
|
|
@@ -1723,9 +1763,6 @@ async function waitForPage(timeoutMs) {
|
|
|
1723
1763
|
}
|
|
1724
1764
|
const expected = session.url;
|
|
1725
1765
|
const driver = session.driver;
|
|
1726
|
-
if (driver.waitForPage === void 0) {
|
|
1727
|
-
return { status: "ready", url: expected, expected };
|
|
1728
|
-
}
|
|
1729
1766
|
const result = await driver.waitForPage(expected, timeoutMs);
|
|
1730
1767
|
if (result.status === "ready") {
|
|
1731
1768
|
session.ready = true;
|
|
@@ -1790,6 +1827,14 @@ function formatTree(graph, maxLines = MAX_TREE_LINES) {
|
|
|
1790
1827
|
}
|
|
1791
1828
|
return lines.join("\n");
|
|
1792
1829
|
}
|
|
1830
|
+
function labelsSummary(marks) {
|
|
1831
|
+
if (marks === null) return null;
|
|
1832
|
+
const names = [];
|
|
1833
|
+
for (const mark of marks) {
|
|
1834
|
+
if (!names.includes(mark.label)) names.push(mark.label);
|
|
1835
|
+
}
|
|
1836
|
+
return { count: marks.length, names };
|
|
1837
|
+
}
|
|
1793
1838
|
function issuePayload(issues, limit) {
|
|
1794
1839
|
return {
|
|
1795
1840
|
issues: issues.slice(0, limit).map((issue2) => ({
|
|
@@ -1867,6 +1912,7 @@ async function inspectTool(input) {
|
|
|
1867
1912
|
throw error;
|
|
1868
1913
|
}
|
|
1869
1914
|
const graph = await driver.graph();
|
|
1915
|
+
const labels = labelsSummary(await driver.labels());
|
|
1870
1916
|
const payload = {
|
|
1871
1917
|
status: "ready",
|
|
1872
1918
|
url: graph.url,
|
|
@@ -1874,7 +1920,8 @@ async function inspectTool(input) {
|
|
|
1874
1920
|
summary: {
|
|
1875
1921
|
nodes: graph.nodes.length,
|
|
1876
1922
|
issues: graph.issues.length,
|
|
1877
|
-
truncated: graph.truncated
|
|
1923
|
+
truncated: graph.truncated,
|
|
1924
|
+
labels: labels ?? "unknown: this driver cannot read the toolbar"
|
|
1878
1925
|
},
|
|
1879
1926
|
tree: formatTree(graph)
|
|
1880
1927
|
};
|
|
@@ -1887,6 +1934,12 @@ async function inspectTool(input) {
|
|
|
1887
1934
|
async function labelsTool() {
|
|
1888
1935
|
const driver = requirePage();
|
|
1889
1936
|
const labels = await driver.labels();
|
|
1937
|
+
if (labels === null) {
|
|
1938
|
+
return textResult({
|
|
1939
|
+
labels: null,
|
|
1940
|
+
message: "the active driver cannot see the toolbar (headless page). Open the page in your own Chrome to read labels."
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1890
1943
|
return textResult({
|
|
1891
1944
|
labels,
|
|
1892
1945
|
count: labels.length
|
|
@@ -1937,6 +1990,17 @@ async function screenshotAnnotatedTool(input) {
|
|
|
1937
1990
|
return imageResult({ ...pngSize(image), bytes: image.length }, image);
|
|
1938
1991
|
}
|
|
1939
1992
|
|
|
1993
|
+
// packages/cli/src/mcp/workflow.ts
|
|
1994
|
+
var WORKFLOW_URI = "skill://litedesign/workflow";
|
|
1995
|
+
var WORKFLOW_TEXT = `# litedesign workflow
|
|
1996
|
+
|
|
1997
|
+
- Call \`inspect\` first, always. Its summary includes the labels the human pinned on the page.
|
|
1998
|
+
- 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.
|
|
1999
|
+
- Labels are read-only: you read them; the human creates, renames, and removes them.
|
|
2000
|
+
- 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.
|
|
2001
|
+
- 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.
|
|
2002
|
+
`;
|
|
2003
|
+
|
|
1940
2004
|
// packages/cli/src/mcp/server.ts
|
|
1941
2005
|
async function guard(run2) {
|
|
1942
2006
|
try {
|
|
@@ -1960,6 +2024,18 @@ async function runMcpServer() {
|
|
|
1960
2024
|
console.error(`[litedesign] extension bridge disabled: ${reason}`);
|
|
1961
2025
|
}
|
|
1962
2026
|
const server = new McpServer({ name: "litedesign", version: VERSION });
|
|
2027
|
+
server.registerResource(
|
|
2028
|
+
"workflow",
|
|
2029
|
+
WORKFLOW_URI,
|
|
2030
|
+
{
|
|
2031
|
+
title: "Workflow",
|
|
2032
|
+
description: "How the agent should work with this server: inspect first, resolve the human's names through labels, spotlight edits with focus.",
|
|
2033
|
+
mimeType: "text/markdown"
|
|
2034
|
+
},
|
|
2035
|
+
async () => ({
|
|
2036
|
+
contents: [{ uri: WORKFLOW_URI, text: WORKFLOW_TEXT, mimeType: "text/markdown" }]
|
|
2037
|
+
})
|
|
2038
|
+
);
|
|
1963
2039
|
const viewport = z.object({
|
|
1964
2040
|
w: z.number().int().positive().describe("Viewport width in CSS pixels"),
|
|
1965
2041
|
h: z.number().int().positive().describe("Viewport height in CSS pixels")
|
|
@@ -1992,7 +2068,7 @@ async function runMcpServer() {
|
|
|
1992
2068
|
"inspect",
|
|
1993
2069
|
{
|
|
1994
2070
|
title: "Inspect page",
|
|
1995
|
-
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.',
|
|
2071
|
+
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.',
|
|
1996
2072
|
inputSchema: {
|
|
1997
2073
|
url: z.string().url(),
|
|
1998
2074
|
viewport,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rahmantech/litedesign",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"zod": "^4.6.7"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@litedesign/core": "0.2.
|
|
25
|
+
"@litedesign/core": "0.2.2",
|
|
26
26
|
"@types/ws": "^8.18.2"
|
|
27
27
|
},
|
|
28
28
|
"publishConfig": {
|