@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,16 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "litedesign",
4
+ "version": "0.1.0",
5
+ "description": "Grid, gap, and alignment on the real web page.",
6
+ "permissions": ["activeTab", "scripting", "storage"],
7
+ "host_permissions": ["http://*/*", "https://*/*"],
8
+ "background": {
9
+ "service_worker": "sw.js",
10
+ "type": "module"
11
+ },
12
+ "action": {
13
+ "default_title": "litedesign: watch this tab",
14
+ "default_popup": "popup.html"
15
+ }
16
+ }
@@ -0,0 +1,73 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title>litedesign</title>
6
+ <style>
7
+ :root {
8
+ color-scheme: dark;
9
+ }
10
+ body {
11
+ width: 264px;
12
+ margin: 0;
13
+ padding: 12px;
14
+ background: #111111;
15
+ color: #e5e5e5;
16
+ font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
17
+ }
18
+ h1 {
19
+ margin: 0 0 8px;
20
+ font-size: 12px;
21
+ font-weight: 600;
22
+ letter-spacing: 0.02em;
23
+ }
24
+ .page {
25
+ margin: 0 0 10px;
26
+ color: #a3a3a3;
27
+ word-break: break-all;
28
+ }
29
+ button {
30
+ display: block;
31
+ width: 100%;
32
+ margin-bottom: 6px;
33
+ padding: 7px 9px;
34
+ border: 0;
35
+ border-radius: 6px;
36
+ background: #262626;
37
+ color: #d4d4d4;
38
+ font: inherit;
39
+ text-align: left;
40
+ cursor: pointer;
41
+ }
42
+ button:hover:enabled {
43
+ background: #404040;
44
+ }
45
+ button.primary {
46
+ background: #3b82f6;
47
+ color: #ffffff;
48
+ }
49
+ button.primary:hover:enabled {
50
+ background: #2563eb;
51
+ }
52
+ button:disabled {
53
+ opacity: 0.5;
54
+ cursor: default;
55
+ }
56
+ .status {
57
+ margin: 8px 0 0;
58
+ color: #a3a3a3;
59
+ }
60
+ .status.bad {
61
+ color: #f87171;
62
+ }
63
+ </style>
64
+ </head>
65
+ <body>
66
+ <h1>litedesign</h1>
67
+ <p class="page" id="page">reading the active tab…</p>
68
+ <button type="button" id="watch" class="primary">Watch this tab</button>
69
+ <button type="button" id="overlay">Toggle overlay</button>
70
+ <p class="status" id="status">checking the bridge…</p>
71
+ <script src="popup.js"></script>
72
+ </body>
73
+ </html>
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ (() => {
3
+ // packages/extension/src/popup.ts
4
+ var pageLine = element("page");
5
+ var statusLine = element("status");
6
+ var watchButton = element("watch");
7
+ var overlayButton = element("overlay");
8
+ var tab;
9
+ watchButton.addEventListener("click", () => void watchThisTab());
10
+ overlayButton.addEventListener("click", () => void toggleOverlay());
11
+ void init();
12
+ async function init() {
13
+ const [active] = await chrome.tabs.query({ active: true, currentWindow: true });
14
+ tab = active;
15
+ pageLine.textContent = active?.url ?? "no active tab";
16
+ watchButton.disabled = active?.id === void 0;
17
+ overlayButton.disabled = active?.id === void 0;
18
+ await refresh();
19
+ }
20
+ async function watchThisTab() {
21
+ if (tab?.id === void 0) return;
22
+ const reply = await ask({ type: "watch", tabId: tab.id });
23
+ if (!reply.ok) {
24
+ setStatus(reply.error, true);
25
+ return;
26
+ }
27
+ pageLine.textContent = reply.result.url;
28
+ await refresh();
29
+ }
30
+ async function toggleOverlay() {
31
+ if (tab?.id === void 0) return;
32
+ const reply = await ask({ type: "overlay", tabId: tab.id });
33
+ if (!reply.ok) {
34
+ setStatus(reply.error, true);
35
+ return;
36
+ }
37
+ window.close();
38
+ }
39
+ async function refresh() {
40
+ const reply = await ask({ type: "status" });
41
+ if (!reply.ok) {
42
+ setStatus(reply.error, true);
43
+ return;
44
+ }
45
+ const status = reply.result;
46
+ const watching = tab?.id !== void 0 && status.page?.tabId === tab.id;
47
+ watchButton.textContent = watching ? "Watching this tab" : "Watch this tab";
48
+ watchButton.disabled = tab?.id === void 0 || watching;
49
+ setStatus(
50
+ status.bridge ? `bridge online \xB7 port ${status.port}` : "bridge offline \xB7 run: litedesign ws start",
51
+ !status.bridge
52
+ );
53
+ }
54
+ function setStatus(text, bad = false) {
55
+ statusLine.textContent = text;
56
+ statusLine.classList.toggle("bad", bad);
57
+ }
58
+ async function ask(message) {
59
+ try {
60
+ return await chrome.runtime.sendMessage(message);
61
+ } catch (error) {
62
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
63
+ }
64
+ }
65
+ function element(id) {
66
+ const found = document.getElementById(id);
67
+ if (found === null) throw new Error(`missing element: ${id}`);
68
+ return found;
69
+ }
70
+ })();
71
+ //# sourceMappingURL=popup.js.map
@@ -0,0 +1,349 @@
1
+ // packages/extension/src/backoff.ts
2
+ var RECONNECT_MIN = 1e3;
3
+ var RECONNECT_MAX = 3e4;
4
+ var OFFLINE_AFTER = 3;
5
+ function backoffDelay(failures2) {
6
+ const step = Math.max(failures2 - 1, 0);
7
+ return Math.min(RECONNECT_MIN * 2 ** step, RECONNECT_MAX);
8
+ }
9
+
10
+ // packages/extension/src/sw.ts
11
+ var DEFAULT_PORT = 8787;
12
+ var PING_INTERVAL = 2e4;
13
+ var SESSION_KEY = "page";
14
+ var ONLINE_TITLE = "litedesign: watch this tab";
15
+ var socket;
16
+ var current;
17
+ var port = DEFAULT_PORT;
18
+ var failures = 0;
19
+ var offline = false;
20
+ var reconnectTimer;
21
+ var pingTimer;
22
+ var ready = start();
23
+ async function start() {
24
+ port = await readPort();
25
+ current = await readPage();
26
+ connect();
27
+ }
28
+ function wake() {
29
+ retry();
30
+ }
31
+ chrome.tabs.onActivated.addListener(wake);
32
+ chrome.windows.onFocusChanged.addListener(wake);
33
+ chrome.runtime.onMessage.addListener((message, _sender, respond) => {
34
+ void answer(message).then(respond, (error) => {
35
+ respond({ ok: false, error: messageOf(error) });
36
+ });
37
+ return true;
38
+ });
39
+ async function answer(message) {
40
+ await ready;
41
+ if (typeof message !== "object" || message === null) throw new Error("invalid message");
42
+ const request = message;
43
+ switch (request.type) {
44
+ case "status":
45
+ return { ok: true, result: status() };
46
+ case "watch":
47
+ if (typeof request.tabId !== "number") throw new Error("watch needs a tab id");
48
+ return { ok: true, result: await watch(request.tabId) };
49
+ case "overlay":
50
+ if (typeof request.tabId !== "number") throw new Error("overlay needs a tab id");
51
+ return { ok: true, result: await toggleOverlay(request.tabId) };
52
+ default:
53
+ throw new Error(`unknown message: ${String(request.type)}`);
54
+ }
55
+ }
56
+ function status() {
57
+ return {
58
+ bridge: socket?.readyState === WebSocket.OPEN,
59
+ port,
60
+ page: current ?? null
61
+ };
62
+ }
63
+ async function watch(tabId) {
64
+ const tab = await chrome.tabs.get(tabId);
65
+ if (!isSupported(tab.url)) {
66
+ throw new Error("this tab is not an http(s) page. Open the page you want to measure.");
67
+ }
68
+ const page = { tabId, url: tab.url ?? "", title: tab.title ?? "" };
69
+ await ensureContent(tabId);
70
+ current = page;
71
+ await savePage(page);
72
+ send({ type: "hello", ...page, version: chrome.runtime.getManifest().version });
73
+ retry();
74
+ await applyStatus(tabId);
75
+ return page;
76
+ }
77
+ async function toggleOverlay(tabId) {
78
+ if (current?.tabId !== tabId) await watch(tabId);
79
+ return toTab("toggle");
80
+ }
81
+ chrome.tabs.onRemoved.addListener((tabId) => {
82
+ if (current?.tabId !== tabId) return;
83
+ current = void 0;
84
+ void savePage(void 0);
85
+ send({ type: "bye" });
86
+ });
87
+ chrome.tabs.onUpdated.addListener((tabId, info) => {
88
+ if (info.status !== "complete" || current?.tabId !== tabId) return;
89
+ void ensureContent(tabId).catch(() => void 0);
90
+ });
91
+ function connect() {
92
+ if (socket !== void 0 && socket.readyState <= WebSocket.OPEN) return;
93
+ const next = new WebSocket(`ws://127.0.0.1:${port}`);
94
+ socket = next;
95
+ next.addEventListener("open", () => {
96
+ failures = 0;
97
+ void setOnline();
98
+ if (current !== void 0) {
99
+ send({ type: "hello", ...current, version: chrome.runtime.getManifest().version });
100
+ }
101
+ startPing();
102
+ });
103
+ next.addEventListener("message", (event) => {
104
+ void onMessage(event.data);
105
+ });
106
+ next.addEventListener("close", () => {
107
+ if (socket === next) socket = void 0;
108
+ stopPing();
109
+ failures += 1;
110
+ if (current !== void 0 && failures >= OFFLINE_AFTER) void setOffline();
111
+ scheduleReconnect();
112
+ });
113
+ next.addEventListener("error", () => {
114
+ });
115
+ }
116
+ function retry() {
117
+ failures = 0;
118
+ if (reconnectTimer !== void 0) {
119
+ clearTimeout(reconnectTimer);
120
+ reconnectTimer = void 0;
121
+ }
122
+ connect();
123
+ }
124
+ function scheduleReconnect() {
125
+ if (reconnectTimer !== void 0) return;
126
+ const delay = backoffDelay(failures) + Math.round(Math.random() * 250);
127
+ reconnectTimer = setTimeout(() => {
128
+ reconnectTimer = void 0;
129
+ connect();
130
+ }, delay);
131
+ }
132
+ async function setOffline() {
133
+ const first = !offline;
134
+ offline = true;
135
+ if (first) {
136
+ console.info(
137
+ `[litedesign] no MCP server on ws://127.0.0.1:${port} after ${OFFLINE_AFTER} attempts. Start "litedesign mcp"; the bridge reconnects by itself.`
138
+ );
139
+ }
140
+ await applyStatus();
141
+ }
142
+ async function setOnline() {
143
+ if (!offline) return;
144
+ offline = false;
145
+ await applyStatus();
146
+ }
147
+ function offlineTitle() {
148
+ return `litedesign: no MCP server on port ${port} \u2014 start "litedesign mcp"`;
149
+ }
150
+ async function applyStatus(tabId = current?.tabId) {
151
+ const badge = offline ? "!" : "";
152
+ const title = offline ? offlineTitle() : ONLINE_TITLE;
153
+ try {
154
+ await chrome.action.setBadgeText({ text: badge });
155
+ await chrome.action.setBadgeBackgroundColor({ color: "#dc2626" });
156
+ await chrome.action.setTitle({ title });
157
+ if (tabId !== void 0) {
158
+ await chrome.action.setBadgeText({ tabId, text: badge });
159
+ await chrome.action.setTitle({ tabId, title });
160
+ }
161
+ } catch {
162
+ }
163
+ }
164
+ function startPing() {
165
+ stopPing();
166
+ pingTimer = setInterval(() => send({ type: "ping" }), PING_INTERVAL);
167
+ }
168
+ function stopPing() {
169
+ if (pingTimer !== void 0) clearInterval(pingTimer);
170
+ pingTimer = void 0;
171
+ }
172
+ function send(message) {
173
+ if (socket?.readyState !== WebSocket.OPEN) return;
174
+ socket.send(JSON.stringify(message));
175
+ }
176
+ async function onMessage(raw) {
177
+ let message;
178
+ try {
179
+ const parsed = JSON.parse(String(raw));
180
+ if (typeof parsed !== "object" || parsed === null) return;
181
+ message = parsed;
182
+ } catch {
183
+ return;
184
+ }
185
+ if (message.type === "ping") {
186
+ send({ type: "pong" });
187
+ return;
188
+ }
189
+ if (message.type === "request" && typeof message.id === "string" && typeof message.action === "string") {
190
+ await handleRequest(message.id, message.action, message.payload);
191
+ }
192
+ }
193
+ async function handleRequest(id, action, payload) {
194
+ try {
195
+ const result = await route(action, payload);
196
+ send({ type: "response", id, ok: true, result });
197
+ } catch (error) {
198
+ send({ type: "response", id, ok: false, error: messageOf(error) });
199
+ }
200
+ }
201
+ async function route(action, payload) {
202
+ switch (action) {
203
+ case "attach":
204
+ return attach();
205
+ case "graph":
206
+ case "measure":
207
+ case "overlay":
208
+ case "labels":
209
+ return toTab(action, payload);
210
+ case "screenshot":
211
+ return capture("screenshot_prepare", payload);
212
+ case "annotate":
213
+ return capture("annotate_prepare", payload);
214
+ default:
215
+ throw new Error(`unknown action: ${action}`);
216
+ }
217
+ }
218
+ async function attach() {
219
+ const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
220
+ if (tab?.id === void 0 || !isSupported(tab.url)) {
221
+ throw new Error(
222
+ "the active tab is not an http(s) page. Switch to the page you want to measure."
223
+ );
224
+ }
225
+ const page = { tabId: tab.id, url: tab.url ?? "", title: tab.title ?? "" };
226
+ await ensureContent(page.tabId);
227
+ current = page;
228
+ await savePage(page);
229
+ send({ type: "hello", ...page, version: chrome.runtime.getManifest().version });
230
+ return page;
231
+ }
232
+ async function capture(action, payload) {
233
+ const page = requirePage();
234
+ const tab = await chrome.tabs.get(page.tabId);
235
+ if (tab.active !== true) {
236
+ throw new Error("the page is not the active tab. Switch to it and try again.");
237
+ }
238
+ if (tab.windowId === void 0) throw new Error("the page has no window.");
239
+ const prepared = await toTabRaw(action, payload);
240
+ if (!prepared.ok) throw new Error(prepared.error);
241
+ const prep = prepared.result ?? {};
242
+ try {
243
+ const dataUrl = await chrome.tabs.captureVisibleTab(tab.windowId, { format: "png" });
244
+ const base64 = dataUrl.slice(dataUrl.indexOf(",") + 1);
245
+ return {
246
+ data: prep.rect === void 0 ? base64 : await crop(base64, prep.rect, prep.dpr ?? 1)
247
+ };
248
+ } finally {
249
+ await toTabRaw("capture_cleanup").catch(() => void 0);
250
+ }
251
+ }
252
+ async function toTab(action, payload) {
253
+ const response = await toTabRaw(action, payload);
254
+ if (!response.ok) throw new Error(response.error);
255
+ return response.result;
256
+ }
257
+ async function toTabRaw(action, payload) {
258
+ const page = requirePage();
259
+ await ensureContent(page.tabId);
260
+ const response = await chrome.tabs.sendMessage(page.tabId, { action, payload });
261
+ if (response === void 0) throw new Error("the content script did not respond");
262
+ return response;
263
+ }
264
+ async function ensureContent(tabId) {
265
+ try {
266
+ const response = await chrome.tabs.sendMessage(tabId, { action: "ping" });
267
+ if (response?.ok === true) return;
268
+ } catch {
269
+ }
270
+ await chrome.scripting.executeScript({
271
+ target: { tabId },
272
+ files: ["core.iife.js", "content.js"]
273
+ });
274
+ }
275
+ async function crop(base64, rect, dpr) {
276
+ const bitmap = await createImageBitmap(toBlob(base64));
277
+ const x = clamp(Math.round(rect.x * dpr), 0, bitmap.width);
278
+ const y = clamp(Math.round(rect.y * dpr), 0, bitmap.height);
279
+ const w = clamp(Math.round(rect.w * dpr), 1, bitmap.width - x);
280
+ const h = clamp(Math.round(rect.h * dpr), 1, bitmap.height - y);
281
+ if (w <= 0 || h <= 0) throw new Error("the element is outside the visible viewport.");
282
+ const canvas = new OffscreenCanvas(w, h);
283
+ const context = canvas.getContext("2d");
284
+ if (context === null) throw new Error("could not create an offscreen canvas.");
285
+ context.drawImage(bitmap, x, y, w, h, 0, 0, w, h);
286
+ bitmap.close();
287
+ const blob = await canvas.convertToBlob({ type: "png" });
288
+ return toBase64(await blob.arrayBuffer());
289
+ }
290
+ function toBlob(base64) {
291
+ const binary = atob(base64);
292
+ const bytes = new Uint8Array(binary.length);
293
+ for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
294
+ return new Blob([bytes], { type: "image/png" });
295
+ }
296
+ function toBase64(buffer) {
297
+ const bytes = new Uint8Array(buffer);
298
+ const chunk = 32768;
299
+ let binary = "";
300
+ for (let i = 0; i < bytes.length; i += chunk) {
301
+ binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
302
+ }
303
+ return btoa(binary);
304
+ }
305
+ function requirePage() {
306
+ if (current === void 0) {
307
+ throw new Error("no page is attached. Call inspect first so the extension can attach to it.");
308
+ }
309
+ return current;
310
+ }
311
+ function isSupported(url) {
312
+ return url !== void 0 && /^(https?|file):/.test(url);
313
+ }
314
+ function clamp(value, min, max) {
315
+ return Math.min(Math.max(value, min), max);
316
+ }
317
+ function messageOf(error) {
318
+ if (error instanceof Error && error.message !== "") return error.message;
319
+ if (typeof error === "string" && error !== "") return error;
320
+ return "unexpected error";
321
+ }
322
+ async function readPort() {
323
+ try {
324
+ const response = await fetch(chrome.runtime.getURL("config.json"), { cache: "no-store" });
325
+ if (!response.ok) return DEFAULT_PORT;
326
+ const config = await response.json();
327
+ const value = config.port;
328
+ return typeof value === "number" && Number.isInteger(value) && value > 0 && value < 65536 ? value : DEFAULT_PORT;
329
+ } catch {
330
+ return DEFAULT_PORT;
331
+ }
332
+ }
333
+ async function readPage() {
334
+ try {
335
+ const stored = await chrome.storage.session.get(SESSION_KEY);
336
+ const value = stored[SESSION_KEY];
337
+ return value !== void 0 && typeof value.tabId === "number" && typeof value.url === "string" ? value : void 0;
338
+ } catch {
339
+ return void 0;
340
+ }
341
+ }
342
+ async function savePage(page) {
343
+ try {
344
+ if (page === void 0) await chrome.storage.session.remove(SESSION_KEY);
345
+ else await chrome.storage.session.set({ [SESSION_KEY]: page });
346
+ } catch {
347
+ }
348
+ }
349
+ //# sourceMappingURL=sw.js.map