@rahmantech/litedesign 0.1.0 → 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.
package/README.md CHANGED
@@ -44,21 +44,36 @@ litedesign mcp # MCP server (stdio)
44
44
 
45
45
  ## Tools
46
46
 
47
- | Tool | What it returns |
48
- | ---------------------- | ----------------------------------------------------------- |
49
- | `capabilities` | version, drivers, mode, connected pages, valid rules |
50
- | `inspect` | tree summary; opens the page; no image by default |
51
- | `await_page` | wait for the page from `inspect` to connect |
52
- | `measure` | box model, computed styles, gaps for one selector |
53
- | `find_issues` | rule violations with selector, actual, expected, severity |
54
- | `screenshot` | PNG of the viewport or one element |
55
- | `screenshot_annotated` | PNG with grid, guides, gaps, and issue labels |
56
- | `overlay` | shows or hides the layers; answers with what the page shows |
47
+ | Tool | What it returns |
48
+ | ---------------------- | -------------------------------------------------------- |
49
+ | `capabilities` | version, drivers, mode, connected pages, valid rules |
50
+ | `inspect` | tree summary; opens the page; no image by default |
51
+ | `await_page` | wait for the page from `inspect` to connect |
52
+ | `measure` | box model, computed styles, gaps for one selector |
53
+ | `find_issues` | findings with kind, selector, actual, expected, severity |
54
+ | `screenshot` | PNG of the viewport or one element |
55
+ | `screenshot_annotated` | PNG with grid, guides, gaps, and issue labels |
56
+ | `overlay` | shows or hides layers, spotlights one component |
57
+ | `labels` | names the human pinned in the toolbar, with selectors |
57
58
 
58
59
  Call `inspect` first. It opens the page and keeps it alive for the other tools.
59
60
  It returns no image unless you pass `screenshot: true`; use the `screenshot` or
60
61
  `screenshot_annotated` tools when you actually need to see the page.
61
62
 
63
+ `overflow` and `contrast` findings are measured facts (`kind: "violation"`);
64
+ spacing, alignment, and radius findings are heuristics (`kind: "candidate"`)
65
+ that can be false positives. Verify a candidate with `measure` before changing
66
+ code.
67
+
68
+ `overlay` also takes `focus`: the same selector `measure` returns. The rest of
69
+ the page dims, the target is outlined and scrolled into view; `focus: null`
70
+ clears it. Focusing scrolls the page, so a capture made afterwards records the
71
+ scrolled viewport.
72
+
73
+ `labels` returns the names the human pinned onto parts of the page through the
74
+ toolbar. When the human refers to a part by name, look it up there instead of
75
+ guessing a selector. Labels are read-only for the agent.
76
+
62
77
  ## Drivers
63
78
 
64
79
  The server prefers Playwright when `playwright-core` is installed. It launches
@@ -1,5 +1,73 @@
1
1
  "use strict";
2
2
  (() => {
3
+ // packages/extension/src/labels.ts
4
+ var STORE_KEY = "litedesign-labels";
5
+ function pageKey() {
6
+ return `${location.origin}${location.pathname}`;
7
+ }
8
+ function hasStorage() {
9
+ return typeof chrome !== "undefined" && chrome.storage?.local !== void 0;
10
+ }
11
+ function sanitize(value) {
12
+ if (!Array.isArray(value)) return [];
13
+ const labels = [];
14
+ for (const entry of value) {
15
+ if (typeof entry !== "object" || entry === null) continue;
16
+ const record = entry;
17
+ if (typeof record.id !== "string" || record.id === "") continue;
18
+ if (typeof record.text !== "string" || record.text.trim() === "") continue;
19
+ if (typeof record.selector !== "string" || record.selector === "") continue;
20
+ labels.push({ id: record.id, text: record.text, selector: record.selector });
21
+ }
22
+ return labels;
23
+ }
24
+ async function readStore() {
25
+ if (!hasStorage()) return {};
26
+ try {
27
+ const stored = await chrome.storage.local.get(STORE_KEY);
28
+ const pages = stored[STORE_KEY];
29
+ return typeof pages === "object" && pages !== null ? pages : {};
30
+ } catch (error) {
31
+ console.warn("[litedesign] could not read labels", error);
32
+ return {};
33
+ }
34
+ }
35
+ async function writeStore(store) {
36
+ if (!hasStorage()) return;
37
+ try {
38
+ await chrome.storage.local.set({ [STORE_KEY]: store });
39
+ } catch (error) {
40
+ console.warn("[litedesign] could not save labels", error);
41
+ }
42
+ }
43
+ async function loadLabels() {
44
+ const store = await readStore();
45
+ return sanitize(store[pageKey()]);
46
+ }
47
+ async function replaceOnPage(next) {
48
+ const store = await readStore();
49
+ store[pageKey()] = next;
50
+ await writeStore(store);
51
+ return next;
52
+ }
53
+ async function addLabel(text, selector) {
54
+ const labels = await loadLabels();
55
+ const label = {
56
+ id: crypto.randomUUID(),
57
+ text: text.trim(),
58
+ selector
59
+ };
60
+ return replaceOnPage([...labels, label]);
61
+ }
62
+ async function removeLabel(id) {
63
+ const labels = await loadLabels();
64
+ return replaceOnPage(labels.filter((label) => label.id !== id));
65
+ }
66
+ async function renameLabel(id, text) {
67
+ const labels = await loadLabels();
68
+ return replaceOnPage(labels.map((label) => label.id === id ? { ...label, text } : label));
69
+ }
70
+
3
71
  // packages/extension/src/observer.ts
4
72
  var DEFAULT_DEBOUNCE = 150;
5
73
  function observeChanges(options) {
@@ -44,6 +112,8 @@
44
112
  guide: true,
45
113
  gap: true,
46
114
  issue: true,
115
+ dim: true,
116
+ focus: true,
47
117
  hover: true
48
118
  };
49
119
  var LAYERS = Object.keys(LAYER_NAMES);
@@ -51,45 +121,61 @@
51
121
  "grid",
52
122
  "guide",
53
123
  "gap",
54
- "issue"
124
+ "issue",
125
+ "dim",
126
+ "focus"
55
127
  ];
128
+ var GRID_STEPS = [8, 12, 16, 24, 32, 48, 64, 96, 128];
56
129
 
57
130
  // packages/extension/src/store.ts
58
131
  var STORAGE_KEY = "litedesign";
59
132
  var DEFAULT_LAYERS = [...LAYERS];
133
+ var DEFAULT_GRID = { step: 8, axis: "both" };
134
+ var AXES = ["vertical", "horizontal", "both"];
60
135
  function defaultLayers() {
61
136
  return [...DEFAULT_LAYERS];
62
137
  }
63
138
  async function loadState() {
64
- if (!hasStorage()) return { enabled: false, layers: defaultLayers() };
139
+ if (!hasStorage2())
140
+ return { enabled: false, layers: defaultLayers(), focus: null, grid: DEFAULT_GRID };
65
141
  try {
66
142
  const stored = await chrome.storage.local.get(STORAGE_KEY);
67
143
  return parseState(stored[STORAGE_KEY]);
68
144
  } catch (error) {
69
145
  console.warn("[litedesign] could not read preferences", error);
70
- return { enabled: false, layers: defaultLayers() };
146
+ return { enabled: false, layers: defaultLayers(), focus: null, grid: DEFAULT_GRID };
71
147
  }
72
148
  }
73
149
  async function saveState(state) {
74
- if (!hasStorage()) return;
150
+ if (!hasStorage2()) return;
75
151
  try {
76
152
  await chrome.storage.local.set({ [STORAGE_KEY]: state });
77
153
  } catch (error) {
78
154
  console.warn("[litedesign] could not save preferences", error);
79
155
  }
80
156
  }
81
- function hasStorage() {
157
+ function hasStorage2() {
82
158
  return typeof chrome !== "undefined" && chrome.storage?.local !== void 0;
83
159
  }
84
160
  function parseState(value) {
85
161
  if (typeof value !== "object" || value === null) {
86
- return { enabled: false, layers: defaultLayers() };
162
+ return { enabled: false, layers: defaultLayers(), focus: null, grid: DEFAULT_GRID };
87
163
  }
88
164
  const record = value;
89
165
  return {
90
166
  // A value written before this flag existed means "not known to be on".
91
167
  enabled: record.enabled === true,
92
- layers: parseLayers(record.layers)
168
+ layers: parseLayers(record.layers),
169
+ focus: typeof record.focus === "string" && record.focus !== "" ? record.focus : null,
170
+ grid: parseGrid(record.grid)
171
+ };
172
+ }
173
+ function parseGrid(value) {
174
+ if (typeof value !== "object" || value === null) return DEFAULT_GRID;
175
+ const record = value;
176
+ return {
177
+ step: GRID_STEPS.includes(record.step) ? record.step : DEFAULT_GRID.step,
178
+ axis: AXES.includes(record.axis) ? record.axis : DEFAULT_GRID.axis
93
179
  };
94
180
  }
95
181
  function parseLayers(value) {
@@ -156,7 +242,79 @@
156
242
  background: #404040;
157
243
  color: #ffffff;
158
244
  }
245
+ .grid-group {
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 2px;
249
+ padding-left: 6px;
250
+ margin-left: 2px;
251
+ border-left: 1px solid #404040;
252
+ }
253
+ .grid-group button {
254
+ padding: 4px 5px;
255
+ display: inline-flex;
256
+ align-items: center;
257
+ }
258
+ .grid-group svg {
259
+ display: block;
260
+ }
261
+ .grid-step {
262
+ padding: 0 3px;
263
+ color: #a3a3a3;
264
+ font-variant-numeric: tabular-nums;
265
+ min-width: 22px;
266
+ text-align: center;
267
+ }
268
+ .label-popup {
269
+ position: fixed;
270
+ box-sizing: border-box;
271
+ width: 164px;
272
+ padding: 4px;
273
+ border: 1px solid #d97706;
274
+ border-radius: 8px;
275
+ background: rgba(17, 17, 17, 0.96);
276
+ box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
277
+ pointer-events: auto;
278
+ align-items: center;
279
+ gap: 4px;
280
+ display: none;
281
+ }
282
+ .label-popup input {
283
+ box-sizing: border-box;
284
+ flex: 1;
285
+ min-width: 0;
286
+ padding: 4px 6px;
287
+ border: 0;
288
+ border-radius: 5px;
289
+ background: #111111;
290
+ color: #e5e5e5;
291
+ font: inherit;
292
+ }
293
+ .label-popup button {
294
+ padding: 4px 7px;
295
+ border: 0;
296
+ border-radius: 5px;
297
+ background: #262626;
298
+ color: #d4d4d4;
299
+ font: inherit;
300
+ cursor: pointer;
301
+ }
302
+ .label-popup button:hover {
303
+ background: #7f1d1d;
304
+ color: #ffffff;
305
+ }
159
306
  `;
307
+ var LABEL_INPUT = { width: 164, height: 34 };
308
+ function placeInput(rect, viewport) {
309
+ const margin = 8;
310
+ const gap = 8;
311
+ const left = Math.min(
312
+ Math.max(rect.left, margin),
313
+ Math.max(margin, viewport.width - LABEL_INPUT.width - margin)
314
+ );
315
+ const above = rect.top > LABEL_INPUT.height + gap * 2;
316
+ return { left, top: above ? rect.top - gap : rect.bottom + gap, above };
317
+ }
160
318
  function createToolbar(options) {
161
319
  const root2 = document.createElement("div");
162
320
  root2.dataset.litedesignUi = "";
@@ -173,6 +331,48 @@
173
331
  let enabled = new Set(options.layers);
174
332
  const isEnabled = (layer) => enabled.has(layer);
175
333
  bar.append(brand, stats);
334
+ const labelButton = document.createElement("button");
335
+ labelButton.type = "button";
336
+ labelButton.dataset.action = "label";
337
+ labelButton.textContent = "label";
338
+ labelButton.title = "Mark a part of the page with a name the agent can read";
339
+ labelButton.addEventListener("click", () => options.onLabelToggle());
340
+ const popup = document.createElement("div");
341
+ popup.className = "label-popup";
342
+ const input = document.createElement("input");
343
+ input.type = "text";
344
+ input.placeholder = "label name";
345
+ const removeButton = document.createElement("button");
346
+ removeButton.type = "button";
347
+ removeButton.textContent = "\xD7";
348
+ removeButton.title = "Remove this label";
349
+ removeButton.hidden = true;
350
+ let committing = false;
351
+ let commit;
352
+ let remove;
353
+ const closeInput = () => {
354
+ popup.style.display = "none";
355
+ };
356
+ removeButton.addEventListener("mousedown", (event) => event.preventDefault());
357
+ removeButton.addEventListener("click", () => {
358
+ committing = true;
359
+ closeInput();
360
+ remove?.();
361
+ });
362
+ input.addEventListener("keydown", (event) => {
363
+ if (event.key === "Enter") {
364
+ committing = true;
365
+ const value = input.value;
366
+ closeInput();
367
+ commit?.(value);
368
+ } else if (event.key === "Escape") {
369
+ closeInput();
370
+ }
371
+ });
372
+ input.addEventListener("blur", () => {
373
+ if (!committing) closeInput();
374
+ committing = false;
375
+ });
176
376
  const buttons = /* @__PURE__ */ new Map();
177
377
  for (const layer of TOGGLE_LAYERS) {
178
378
  const button = document.createElement("button");
@@ -184,6 +384,60 @@
184
384
  buttons.set(layer, button);
185
385
  bar.append(button);
186
386
  }
387
+ let grid = options.grid;
388
+ const gridButtons = /* @__PURE__ */ new Map();
389
+ const line = (d) => `<svg width="11" height="11" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.5"><path d="${d}"/></svg>`;
390
+ const axes = [
391
+ ["vertical", "M3 1v10M9 1v10", "Grid: vertical lines only"],
392
+ ["horizontal", "M1 3h10M1 9h10", "Grid: horizontal lines only"],
393
+ ["both", "M3 1v10M9 1v10M1 3h10M1 9h10", "Grid: both directions"]
394
+ ];
395
+ const gridGroup = document.createElement("span");
396
+ gridGroup.className = "grid-group";
397
+ for (const [axis, path, title] of axes) {
398
+ const button = document.createElement("button");
399
+ button.type = "button";
400
+ button.dataset.axis = axis;
401
+ button.title = title;
402
+ button.innerHTML = line(path);
403
+ button.addEventListener("click", () => {
404
+ grid = { ...grid, axis };
405
+ setGrid(grid);
406
+ options.onGrid(grid);
407
+ });
408
+ gridButtons.set(axis, button);
409
+ gridGroup.append(button);
410
+ }
411
+ const stepLabel = document.createElement("span");
412
+ stepLabel.className = "grid-step";
413
+ const stepDown = document.createElement("button");
414
+ stepDown.type = "button";
415
+ stepDown.dataset.axis = "step-down";
416
+ stepDown.textContent = "\u2212";
417
+ stepDown.title = "Smaller grid spacing";
418
+ stepDown.addEventListener("click", () => {
419
+ const index = GRID_STEPS.indexOf(grid.step);
420
+ if (index > 0) {
421
+ grid = { ...grid, step: GRID_STEPS[index - 1] ?? grid.step };
422
+ setGrid(grid);
423
+ options.onGrid(grid);
424
+ }
425
+ });
426
+ const stepUp = document.createElement("button");
427
+ stepUp.type = "button";
428
+ stepUp.dataset.axis = "step-up";
429
+ stepUp.textContent = "+";
430
+ stepUp.title = "Larger grid spacing";
431
+ stepUp.addEventListener("click", () => {
432
+ const index = GRID_STEPS.indexOf(grid.step);
433
+ if (index !== -1 && index < GRID_STEPS.length - 1) {
434
+ grid = { ...grid, step: GRID_STEPS[index + 1] ?? grid.step };
435
+ setGrid(grid);
436
+ options.onGrid(grid);
437
+ }
438
+ });
439
+ gridGroup.append(stepDown, stepLabel, stepUp);
440
+ bar.append(labelButton, gridGroup);
187
441
  const close = document.createElement("button");
188
442
  close.type = "button";
189
443
  close.dataset.action = "close";
@@ -192,7 +446,8 @@
192
446
  close.setAttribute("aria-label", "Close litedesign");
193
447
  close.addEventListener("click", () => options.onClose());
194
448
  bar.append(close);
195
- root2.append(style, bar);
449
+ popup.append(input, removeButton);
450
+ root2.append(style, bar, popup);
196
451
  const setLayers = (layers) => {
197
452
  enabled = new Set(layers);
198
453
  for (const [layer, button] of buttons) {
@@ -201,13 +456,43 @@
201
456
  button.classList.toggle("on", on);
202
457
  }
203
458
  };
459
+ const setGrid = (next) => {
460
+ grid = next;
461
+ for (const [axis, button] of gridButtons) {
462
+ const on = axis === grid.axis;
463
+ button.setAttribute("aria-pressed", String(on));
464
+ button.classList.toggle("on", on);
465
+ }
466
+ stepLabel.textContent = String(grid.step);
467
+ };
204
468
  setLayers(options.layers);
469
+ setGrid(grid);
205
470
  return {
206
471
  element: root2,
207
472
  setStats(nodes, issues, truncated) {
208
473
  stats.textContent = `${nodes} nodes \xB7 ${issues} issues${truncated ? " \xB7 truncated" : ""}`;
209
474
  },
210
- setLayers
475
+ setLayers,
476
+ setGrid,
477
+ setLabelMode(on) {
478
+ labelButton.classList.toggle("on", on);
479
+ labelButton.setAttribute("aria-pressed", String(on));
480
+ if (!on) closeInput();
481
+ },
482
+ askLabel(rect, initial, fn, onRemove) {
483
+ commit = fn;
484
+ remove = onRemove;
485
+ committing = false;
486
+ const placement = placeInput(rect, { width: window.innerWidth, height: window.innerHeight });
487
+ popup.style.left = `${placement.left}px`;
488
+ popup.style.top = `${placement.top}px`;
489
+ popup.style.transform = placement.above ? "translateY(-100%)" : "none";
490
+ removeButton.hidden = onRemove === void 0;
491
+ popup.style.display = "flex";
492
+ input.value = initial;
493
+ input.focus();
494
+ if (initial !== "") input.select();
495
+ }
211
496
  };
212
497
  }
213
498
 
@@ -226,7 +511,7 @@
226
511
  const state = await loadState();
227
512
  if (window.__litedesign_state__ !== "starting") return;
228
513
  try {
229
- mount(core2, state.layers);
514
+ mount(core2, state.layers, state.grid);
230
515
  } catch (error) {
231
516
  window.__litedesign_state__ = void 0;
232
517
  throw error;
@@ -235,28 +520,39 @@
235
520
  async function applyOverlay(core2, options) {
236
521
  if (!options.enabled) {
237
522
  window.__litedesign_teardown__?.();
238
- return { enabled: false, layers: [] };
523
+ return { enabled: false, layers: [], focus: null };
239
524
  }
240
- const layers = options.layers ?? (await loadState()).layers;
525
+ const stored = await loadState();
526
+ const layers = options.layers ?? stored.layers;
241
527
  if (window.__litedesign_state__ === "active") {
242
528
  window.__litedesign_layers__?.(layers);
243
- return { enabled: true, layers };
244
- }
245
- window.__litedesign_state__ = "starting";
246
- try {
247
- mount(core2, layers);
248
- } catch (error) {
249
- window.__litedesign_state__ = void 0;
250
- throw error;
529
+ } else {
530
+ window.__litedesign_state__ = "starting";
531
+ try {
532
+ mount(core2, layers, stored.grid);
533
+ } catch (error) {
534
+ window.__litedesign_state__ = void 0;
535
+ throw error;
536
+ }
251
537
  }
252
- return { enabled: overlayHost() !== void 0, layers };
538
+ if (options.focus !== void 0) window.__litedesign_set_focus__?.(options.focus);
539
+ return {
540
+ enabled: overlayHost() !== void 0,
541
+ layers,
542
+ focus: window.__litedesign_focus_selector__?.() ?? null
543
+ };
253
544
  }
254
- function mount(core2, initial) {
545
+ function mount(core2, initial, initialGrid) {
255
546
  const root2 = document.body ?? document.documentElement;
256
547
  let layers = initial;
548
+ let grid = initialGrid;
257
549
  let graph = core2.buildGraph(root2);
258
550
  let handle;
259
551
  let observer;
552
+ let focusSelector = null;
553
+ let focusId = null;
554
+ let labels = [];
555
+ let labelMode = false;
260
556
  let toolbar;
261
557
  const teardown = () => {
262
558
  observer?.stop();
@@ -264,13 +560,24 @@
264
560
  handle = void 0;
265
561
  observer = void 0;
266
562
  mountedHost = void 0;
563
+ focusSelector = null;
564
+ focusId = null;
565
+ if (labelMode) document.removeEventListener("click", onLabelClick, true);
566
+ labelMode = false;
267
567
  window.__litedesign_teardown__ = void 0;
268
568
  window.__litedesign_layers__ = void 0;
569
+ window.__litedesign_set_focus__ = void 0;
570
+ window.__litedesign_focus_selector__ = void 0;
269
571
  window.__litedesign_state__ = void 0;
270
- void saveState({ enabled: false, layers });
572
+ void saveState({ enabled: false, layers, focus: null, grid });
271
573
  };
272
574
  const refresh = () => {
273
575
  graph = core2.buildGraph(root2);
576
+ if (focusId !== null && !graph.nodes.some((node) => node.id === focusId)) {
577
+ focusSelector = null;
578
+ focusId = null;
579
+ handle?.setFocus(null);
580
+ }
274
581
  handle?.update(graph);
275
582
  toolbar.setStats(graph.nodes.length, graph.issues.length, graph.truncated);
276
583
  };
@@ -278,25 +585,118 @@
278
585
  layers = next;
279
586
  handle?.setLayers(layers);
280
587
  toolbar.setLayers(layers);
281
- void saveState({ enabled: true, layers });
588
+ void saveState({ enabled: true, layers, focus: focusSelector, grid });
589
+ };
590
+ const setGrid = (next) => {
591
+ grid = next;
592
+ handle?.setGrid(grid);
593
+ toolbar.setGrid(grid);
594
+ void saveState({ enabled: true, layers, focus: focusSelector, grid });
595
+ };
596
+ const setFocus = (selector) => {
597
+ if (selector === null) {
598
+ focusSelector = null;
599
+ focusId = null;
600
+ handle?.setFocus(null);
601
+ void saveState({ enabled: true, layers, focus: null, grid });
602
+ return;
603
+ }
604
+ const node = core2.resolveNode(graph, selector);
605
+ if (node === null) {
606
+ focusSelector = null;
607
+ focusId = null;
608
+ handle?.setFocus(null);
609
+ void saveState({ enabled: true, layers, focus: null, grid });
610
+ return;
611
+ }
612
+ focusSelector = node.selector;
613
+ focusId = node.id;
614
+ handle?.setFocus(node.id);
615
+ let element = null;
616
+ try {
617
+ element = document.querySelector(node.selector);
618
+ } catch {
619
+ element = null;
620
+ }
621
+ element?.scrollIntoView({ block: "center", inline: "center", behavior: "instant" });
622
+ void saveState({ enabled: true, layers, focus: focusSelector, grid });
623
+ };
624
+ const applyMarks = () => {
625
+ handle?.setMarks(labels.map((label) => ({ selector: label.selector, text: label.text })));
626
+ };
627
+ const onLabelClick = (event) => {
628
+ const target = event.target;
629
+ if (!(target instanceof Element)) return;
630
+ if (target === handle?.host) return;
631
+ event.preventDefault();
632
+ event.stopPropagation();
633
+ const selector = core2.pathOf(target);
634
+ const existing = labels.find((label) => label.selector === selector);
635
+ 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
+ );
654
+ return;
655
+ }
656
+ toolbar.askLabel(target.getBoundingClientRect(), "", (text) => {
657
+ const trimmed = text.trim();
658
+ if (trimmed === "") return;
659
+ void addLabel(trimmed, selector).then((next) => {
660
+ labels = next;
661
+ applyMarks();
662
+ });
663
+ });
664
+ };
665
+ const toggleLabelMode = () => {
666
+ labelMode = !labelMode;
667
+ toolbar.setLabelMode(labelMode);
668
+ if (labelMode) {
669
+ document.addEventListener("click", onLabelClick, true);
670
+ } else {
671
+ document.removeEventListener("click", onLabelClick, true);
672
+ }
282
673
  };
283
674
  toolbar = createToolbar({
284
675
  layers,
676
+ grid,
285
677
  onToggle(layer, enabled) {
286
678
  setLayers(
287
679
  enabled ? [.../* @__PURE__ */ new Set([...layers, layer])] : layers.filter((name) => name !== layer)
288
680
  );
289
681
  },
682
+ onLabelToggle: toggleLabelMode,
683
+ onGrid: setGrid,
290
684
  onClose: teardown
291
685
  });
292
686
  toolbar.setStats(graph.nodes.length, graph.issues.length, graph.truncated);
293
- handle = core2.render(graph, { layers, toolbar: toolbar.element });
687
+ handle = core2.render(graph, { layers, toolbar: toolbar.element, grid });
294
688
  observer = observeChanges({ root: root2, onChange: refresh });
689
+ void loadLabels().then((next) => {
690
+ labels = next;
691
+ applyMarks();
692
+ });
295
693
  mountedHost = handle.host;
296
694
  window.__litedesign_layers__ = setLayers;
695
+ window.__litedesign_set_focus__ = setFocus;
696
+ window.__litedesign_focus_selector__ = () => focusSelector;
297
697
  window.__litedesign_teardown__ = teardown;
298
698
  window.__litedesign_state__ = "active";
299
- void saveState({ enabled: true, layers });
699
+ void saveState({ enabled: true, layers, focus: focusSelector, grid });
300
700
  }
301
701
 
302
702
  // packages/extension/src/bridge.ts
@@ -331,9 +731,12 @@
331
731
  ok: true,
332
732
  result: await applyOverlay(core2, {
333
733
  enabled: payload.enabled !== false,
334
- layers: payload.layers
734
+ layers: payload.layers,
735
+ focus: payload.focus
335
736
  })
336
737
  };
738
+ case "labels":
739
+ return { ok: true, result: { labels: await loadLabels() } };
337
740
  case "graph":
338
741
  return { ok: true, result: core2.buildGraph(root()) };
339
742
  case "measure":
@@ -465,7 +868,7 @@
465
868
  async function restore(core2) {
466
869
  const state = await loadState();
467
870
  if (!state.enabled) return;
468
- await applyOverlay(core2, { enabled: true, layers: state.layers });
871
+ await applyOverlay(core2, { enabled: true, layers: state.layers, focus: state.focus });
469
872
  }
470
873
  })();
471
874
  //# sourceMappingURL=content.js.map