@rahmantech/litedesign 0.2.1 → 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.
@@ -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) window.__litedesign_set_focus__?.(options.focus);
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,
@@ -570,17 +618,22 @@
570
618
  window.__litedesign_set_focus__ = void 0;
571
619
  window.__litedesign_focus_selector__ = void 0;
572
620
  window.__litedesign_state__ = void 0;
621
+ clearSnapshot();
573
622
  void saveState({ enabled: false, layers, focus: null, grid });
574
623
  };
575
624
  const refresh = () => {
625
+ const before2 = nodeSignatures(graph);
576
626
  graph = core2.buildGraph(root2);
577
627
  if (focusId !== null && !graph.nodes.some((node) => node.id === focusId)) {
578
628
  focusSelector = null;
579
629
  focusId = null;
580
630
  handle?.setFocus(null);
581
631
  }
632
+ const [firstChanged2] = changedNodes(before2, graph);
633
+ if (firstChanged2 !== void 0) setFocus(firstChanged2.selector);
582
634
  handle?.update(graph);
583
635
  toolbar.setStats(graph.nodes.length, graph.issues.length, graph.truncated);
636
+ saveSnapshot(graph);
584
637
  };
585
638
  const setLayers = (next) => {
586
639
  layers = next;
@@ -707,6 +760,15 @@
707
760
  labels = next;
708
761
  applyMarks();
709
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);
710
772
  mountedHost = handle.host;
711
773
  window.__litedesign_layers__ = setLayers;
712
774
  window.__litedesign_set_focus__ = setFocus;
@@ -714,6 +776,7 @@
714
776
  window.__litedesign_teardown__ = teardown;
715
777
  window.__litedesign_state__ = "active";
716
778
  void saveState({ enabled: true, layers, focus: focusSelector, grid });
779
+ return autoFocus;
717
780
  }
718
781
 
719
782
  // packages/extension/src/bridge.ts
@@ -740,9 +803,10 @@
740
803
  switch (action) {
741
804
  case "ping":
742
805
  return { ok: true, result: true };
743
- case "toggle":
806
+ case "toggle": {
744
807
  await boot(core2);
745
- return { ok: true };
808
+ return { ok: true, result: { enabled: window.__litedesign_state__ === "active" } };
809
+ }
746
810
  case "overlay":
747
811
  return {
748
812
  ok: true,
@@ -1248,7 +1248,7 @@ var __litedesign = (() => {
1248
1248
  }
1249
1249
 
1250
1250
  // packages/core/src/index.ts
1251
- var VERSION = "0.2.1";
1251
+ var VERSION = "0.2.2";
1252
1252
  return __toCommonJS(index_exports);
1253
1253
  })();
1254
1254
  //# sourceMappingURL=core.iife.js.map
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "litedesign",
4
- "version": "0.2.1",
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://*/*"],
@@ -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 watchThisTab());
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 watchThisTab() {
23
+ async function toggleWatch() {
21
24
  if (tab?.id === void 0) return;
22
- const reply = await ask({ type: "watch", tabId: tab.id });
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
- window.close();
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 ? "Watching this tab" : "Watch this tab";
48
- watchButton.disabled = tab?.id === void 0 || watching;
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);
@@ -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.2.1";
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
- if (this.#hub.find(url) !== void 0) return;
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 there is nothing to read.
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 { enabled: false, layers: [], focus: null };
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 { enabled: true, layers: input.layers, focus };
1588
+ return { applied: true, focus };
1557
1589
  },
1558
1590
  { enabled: options.enabled, layers, focus: options.focus }
1559
1591
  );
1560
- return result;
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;
@@ -1791,6 +1828,7 @@ function formatTree(graph, maxLines = MAX_TREE_LINES) {
1791
1828
  return lines.join("\n");
1792
1829
  }
1793
1830
  function labelsSummary(marks) {
1831
+ if (marks === null) return null;
1794
1832
  const names = [];
1795
1833
  for (const mark of marks) {
1796
1834
  if (!names.includes(mark.label)) names.push(mark.label);
@@ -1883,7 +1921,7 @@ async function inspectTool(input) {
1883
1921
  nodes: graph.nodes.length,
1884
1922
  issues: graph.issues.length,
1885
1923
  truncated: graph.truncated,
1886
- labels
1924
+ labels: labels ?? "unknown: this driver cannot read the toolbar"
1887
1925
  },
1888
1926
  tree: formatTree(graph)
1889
1927
  };
@@ -1896,6 +1934,12 @@ async function inspectTool(input) {
1896
1934
  async function labelsTool() {
1897
1935
  const driver = requirePage();
1898
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
+ }
1899
1943
  return textResult({
1900
1944
  labels,
1901
1945
  count: labels.length
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rahmantech/litedesign",
3
- "version": "0.2.1",
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.1",
25
+ "@litedesign/core": "0.2.2",
26
26
  "@types/ws": "^8.18.2"
27
27
  },
28
28
  "publishConfig": {