@rahmantech/litedesign 0.2.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 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
@@ -553,6 +553,7 @@
553
553
  let focusId = null;
554
554
  let labels = [];
555
555
  let labelMode = false;
556
+ let marksInteractive = false;
556
557
  let toolbar;
557
558
  const teardown = () => {
558
559
  observer?.stop();
@@ -622,7 +623,30 @@
622
623
  void saveState({ enabled: true, layers, focus: focusSelector, grid });
623
624
  };
624
625
  const applyMarks = () => {
625
- handle?.setMarks(labels.map((label) => ({ selector: label.selector, text: label.text })));
626
+ handle?.setMarks(
627
+ labels.map((label) => ({ selector: label.selector, text: label.text })),
628
+ marksInteractive
629
+ );
630
+ };
631
+ const editLabel = (existing, rect) => {
632
+ toolbar.askLabel(
633
+ rect,
634
+ existing.text,
635
+ (text) => {
636
+ const trimmed = text.trim();
637
+ if (trimmed === "" || trimmed === existing.text) return;
638
+ void renameLabel(existing.id, trimmed).then((next) => {
639
+ labels = next;
640
+ applyMarks();
641
+ });
642
+ },
643
+ () => {
644
+ void removeLabel(existing.id).then((next) => {
645
+ labels = next;
646
+ applyMarks();
647
+ });
648
+ }
649
+ );
626
650
  };
627
651
  const onLabelClick = (event) => {
628
652
  const target = event.target;
@@ -633,24 +657,7 @@
633
657
  const selector = core2.pathOf(target);
634
658
  const existing = labels.find((label) => label.selector === selector);
635
659
  if (existing !== void 0) {
636
- toolbar.askLabel(
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
- );
660
+ editLabel(existing, target.getBoundingClientRect());
654
661
  return;
655
662
  }
656
663
  toolbar.askLabel(target.getBoundingClientRect(), "", (text) => {
@@ -670,6 +677,8 @@
670
677
  } else {
671
678
  document.removeEventListener("click", onLabelClick, true);
672
679
  }
680
+ marksInteractive = labelMode;
681
+ applyMarks();
673
682
  };
674
683
  toolbar = createToolbar({
675
684
  layers,
@@ -684,7 +693,15 @@
684
693
  onClose: teardown
685
694
  });
686
695
  toolbar.setStats(graph.nodes.length, graph.issues.length, graph.truncated);
687
- handle = core2.render(graph, { layers, toolbar: toolbar.element, grid });
696
+ handle = core2.render(graph, {
697
+ layers,
698
+ toolbar: toolbar.element,
699
+ grid,
700
+ onMarkClick: (mark, anchor) => {
701
+ const existing = labels.find((label) => label.selector === mark.selector);
702
+ if (existing !== void 0) editLabel(existing, anchor);
703
+ }
704
+ });
688
705
  observer = observeChanges({ root: root2, onChange: refresh });
689
706
  void loadLabels().then((next) => {
690
707
  labels = next;
@@ -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
- if (marks.length === 0) return;
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
- drawMarks(marksLayer, marks, new Map(current.nodes.map((node) => [node.selector, node])));
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.0";
1251
+ var VERSION = "0.2.1";
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.1.0",
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/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.0";
47
+ var VERSION = "0.2.1";
48
48
 
49
49
  // packages/cli/src/ws.ts
50
50
  import { spawn } from "node:child_process";
@@ -1790,6 +1790,13 @@ function formatTree(graph, maxLines = MAX_TREE_LINES) {
1790
1790
  }
1791
1791
  return lines.join("\n");
1792
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
+ }
1793
1800
  function issuePayload(issues, limit) {
1794
1801
  return {
1795
1802
  issues: issues.slice(0, limit).map((issue2) => ({
@@ -1867,6 +1874,7 @@ async function inspectTool(input) {
1867
1874
  throw error;
1868
1875
  }
1869
1876
  const graph = await driver.graph();
1877
+ const labels = labelsSummary(await driver.labels());
1870
1878
  const payload = {
1871
1879
  status: "ready",
1872
1880
  url: graph.url,
@@ -1874,7 +1882,8 @@ async function inspectTool(input) {
1874
1882
  summary: {
1875
1883
  nodes: graph.nodes.length,
1876
1884
  issues: graph.issues.length,
1877
- truncated: graph.truncated
1885
+ truncated: graph.truncated,
1886
+ labels
1878
1887
  },
1879
1888
  tree: formatTree(graph)
1880
1889
  };
@@ -1937,6 +1946,17 @@ async function screenshotAnnotatedTool(input) {
1937
1946
  return imageResult({ ...pngSize(image), bytes: image.length }, image);
1938
1947
  }
1939
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
+
1940
1960
  // packages/cli/src/mcp/server.ts
1941
1961
  async function guard(run2) {
1942
1962
  try {
@@ -1960,6 +1980,18 @@ async function runMcpServer() {
1960
1980
  console.error(`[litedesign] extension bridge disabled: ${reason}`);
1961
1981
  }
1962
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
+ );
1963
1995
  const viewport = z.object({
1964
1996
  w: z.number().int().positive().describe("Viewport width in CSS pixels"),
1965
1997
  h: z.number().int().positive().describe("Viewport height in CSS pixels")
@@ -1992,7 +2024,7 @@ async function runMcpServer() {
1992
2024
  "inspect",
1993
2025
  {
1994
2026
  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.',
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.',
1996
2028
  inputSchema: {
1997
2029
  url: z.string().url(),
1998
2030
  viewport,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rahmantech/litedesign",
3
- "version": "0.2.0",
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",
@@ -22,7 +22,7 @@
22
22
  "zod": "^4.6.7"
23
23
  },
24
24
  "devDependencies": {
25
- "@litedesign/core": "0.2.0",
25
+ "@litedesign/core": "0.2.1",
26
26
  "@types/ws": "^8.18.2"
27
27
  },
28
28
  "publishConfig": {