@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.
- package/LICENSE +201 -0
- package/README.md +101 -2
- package/dist/extension/config.json +3 -0
- package/dist/extension/content.js +874 -0
- package/dist/extension/core.iife.js +1230 -0
- package/dist/extension/manifest.json +16 -0
- package/dist/extension/popup.html +73 -0
- package/dist/extension/popup.js +71 -0
- package/dist/extension/sw.js +349 -0
- package/dist/index.js +2161 -0
- package/package.json +38 -3
|
@@ -0,0 +1,874 @@
|
|
|
1
|
+
"use strict";
|
|
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
|
+
|
|
71
|
+
// packages/extension/src/observer.ts
|
|
72
|
+
var DEFAULT_DEBOUNCE = 150;
|
|
73
|
+
function observeChanges(options) {
|
|
74
|
+
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE;
|
|
75
|
+
let timer;
|
|
76
|
+
const schedule = () => {
|
|
77
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
78
|
+
timer = setTimeout(() => {
|
|
79
|
+
timer = void 0;
|
|
80
|
+
options.onChange();
|
|
81
|
+
}, debounceMs);
|
|
82
|
+
};
|
|
83
|
+
const mutation = new MutationObserver(schedule);
|
|
84
|
+
mutation.observe(options.root, {
|
|
85
|
+
childList: true,
|
|
86
|
+
subtree: true,
|
|
87
|
+
attributes: true,
|
|
88
|
+
attributeFilter: ["class", "style", "hidden"]
|
|
89
|
+
});
|
|
90
|
+
const resize = new ResizeObserver(schedule);
|
|
91
|
+
resize.observe(options.root);
|
|
92
|
+
const onResize = () => schedule();
|
|
93
|
+
const onNavigate = () => schedule();
|
|
94
|
+
window.addEventListener("resize", onResize);
|
|
95
|
+
window.addEventListener("popstate", onNavigate);
|
|
96
|
+
window.addEventListener("hashchange", onNavigate);
|
|
97
|
+
return {
|
|
98
|
+
stop() {
|
|
99
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
100
|
+
mutation.disconnect();
|
|
101
|
+
resize.disconnect();
|
|
102
|
+
window.removeEventListener("resize", onResize);
|
|
103
|
+
window.removeEventListener("popstate", onNavigate);
|
|
104
|
+
window.removeEventListener("hashchange", onNavigate);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// packages/extension/src/types.ts
|
|
110
|
+
var LAYER_NAMES = {
|
|
111
|
+
grid: true,
|
|
112
|
+
guide: true,
|
|
113
|
+
gap: true,
|
|
114
|
+
issue: true,
|
|
115
|
+
dim: true,
|
|
116
|
+
focus: true,
|
|
117
|
+
hover: true
|
|
118
|
+
};
|
|
119
|
+
var LAYERS = Object.keys(LAYER_NAMES);
|
|
120
|
+
var TOGGLE_LAYERS = [
|
|
121
|
+
"grid",
|
|
122
|
+
"guide",
|
|
123
|
+
"gap",
|
|
124
|
+
"issue",
|
|
125
|
+
"dim",
|
|
126
|
+
"focus"
|
|
127
|
+
];
|
|
128
|
+
var GRID_STEPS = [8, 12, 16, 24, 32, 48, 64, 96, 128];
|
|
129
|
+
|
|
130
|
+
// packages/extension/src/store.ts
|
|
131
|
+
var STORAGE_KEY = "litedesign";
|
|
132
|
+
var DEFAULT_LAYERS = [...LAYERS];
|
|
133
|
+
var DEFAULT_GRID = { step: 8, axis: "both" };
|
|
134
|
+
var AXES = ["vertical", "horizontal", "both"];
|
|
135
|
+
function defaultLayers() {
|
|
136
|
+
return [...DEFAULT_LAYERS];
|
|
137
|
+
}
|
|
138
|
+
async function loadState() {
|
|
139
|
+
if (!hasStorage2())
|
|
140
|
+
return { enabled: false, layers: defaultLayers(), focus: null, grid: DEFAULT_GRID };
|
|
141
|
+
try {
|
|
142
|
+
const stored = await chrome.storage.local.get(STORAGE_KEY);
|
|
143
|
+
return parseState(stored[STORAGE_KEY]);
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.warn("[litedesign] could not read preferences", error);
|
|
146
|
+
return { enabled: false, layers: defaultLayers(), focus: null, grid: DEFAULT_GRID };
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
async function saveState(state) {
|
|
150
|
+
if (!hasStorage2()) return;
|
|
151
|
+
try {
|
|
152
|
+
await chrome.storage.local.set({ [STORAGE_KEY]: state });
|
|
153
|
+
} catch (error) {
|
|
154
|
+
console.warn("[litedesign] could not save preferences", error);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function hasStorage2() {
|
|
158
|
+
return typeof chrome !== "undefined" && chrome.storage?.local !== void 0;
|
|
159
|
+
}
|
|
160
|
+
function parseState(value) {
|
|
161
|
+
if (typeof value !== "object" || value === null) {
|
|
162
|
+
return { enabled: false, layers: defaultLayers(), focus: null, grid: DEFAULT_GRID };
|
|
163
|
+
}
|
|
164
|
+
const record = value;
|
|
165
|
+
return {
|
|
166
|
+
// A value written before this flag existed means "not known to be on".
|
|
167
|
+
enabled: record.enabled === true,
|
|
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
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function parseLayers(value) {
|
|
182
|
+
if (!Array.isArray(value)) return defaultLayers();
|
|
183
|
+
const valid = [...new Set(value.filter(isLayerName))];
|
|
184
|
+
return valid.includes("hover") ? valid : [...valid, "hover"];
|
|
185
|
+
}
|
|
186
|
+
function isLayerName(value) {
|
|
187
|
+
return typeof value === "string" && LAYERS.includes(value);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// packages/extension/src/toolbar.ts
|
|
191
|
+
var STYLE = `
|
|
192
|
+
.bar {
|
|
193
|
+
position: absolute;
|
|
194
|
+
top: 12px;
|
|
195
|
+
right: 12px;
|
|
196
|
+
display: flex;
|
|
197
|
+
align-items: center;
|
|
198
|
+
gap: 6px;
|
|
199
|
+
padding: 6px 8px;
|
|
200
|
+
border-radius: 10px;
|
|
201
|
+
background: rgba(17, 17, 17, 0.94);
|
|
202
|
+
box-shadow: 0 6px 20px rgba(0, 0, 0, 0.28);
|
|
203
|
+
color: #e5e5e5;
|
|
204
|
+
font: 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
205
|
+
pointer-events: auto;
|
|
206
|
+
user-select: none;
|
|
207
|
+
}
|
|
208
|
+
.brand {
|
|
209
|
+
padding: 0 2px;
|
|
210
|
+
color: #ffffff;
|
|
211
|
+
font-weight: 600;
|
|
212
|
+
letter-spacing: 0.02em;
|
|
213
|
+
}
|
|
214
|
+
.stats {
|
|
215
|
+
padding: 0 4px;
|
|
216
|
+
color: #a3a3a3;
|
|
217
|
+
white-space: nowrap;
|
|
218
|
+
}
|
|
219
|
+
button {
|
|
220
|
+
padding: 5px 8px;
|
|
221
|
+
border: 0;
|
|
222
|
+
border-radius: 6px;
|
|
223
|
+
background: #262626;
|
|
224
|
+
color: #d4d4d4;
|
|
225
|
+
font: inherit;
|
|
226
|
+
cursor: pointer;
|
|
227
|
+
}
|
|
228
|
+
button:hover {
|
|
229
|
+
background: #404040;
|
|
230
|
+
}
|
|
231
|
+
button.on {
|
|
232
|
+
background: #3b82f6;
|
|
233
|
+
color: #ffffff;
|
|
234
|
+
}
|
|
235
|
+
button[data-action='close'] {
|
|
236
|
+
padding: 5px 7px;
|
|
237
|
+
background: transparent;
|
|
238
|
+
color: #a3a3a3;
|
|
239
|
+
font-size: 14px;
|
|
240
|
+
}
|
|
241
|
+
button[data-action='close']:hover {
|
|
242
|
+
background: #404040;
|
|
243
|
+
color: #ffffff;
|
|
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
|
+
}
|
|
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
|
+
}
|
|
318
|
+
function createToolbar(options) {
|
|
319
|
+
const root2 = document.createElement("div");
|
|
320
|
+
root2.dataset.litedesignUi = "";
|
|
321
|
+
const style = document.createElement("style");
|
|
322
|
+
style.textContent = STYLE;
|
|
323
|
+
const bar = document.createElement("div");
|
|
324
|
+
bar.className = "bar";
|
|
325
|
+
const brand = document.createElement("span");
|
|
326
|
+
brand.className = "brand";
|
|
327
|
+
brand.textContent = "litedesign";
|
|
328
|
+
const stats = document.createElement("span");
|
|
329
|
+
stats.className = "stats";
|
|
330
|
+
stats.textContent = "measuring\u2026";
|
|
331
|
+
let enabled = new Set(options.layers);
|
|
332
|
+
const isEnabled = (layer) => enabled.has(layer);
|
|
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
|
+
});
|
|
376
|
+
const buttons = /* @__PURE__ */ new Map();
|
|
377
|
+
for (const layer of TOGGLE_LAYERS) {
|
|
378
|
+
const button = document.createElement("button");
|
|
379
|
+
button.type = "button";
|
|
380
|
+
button.dataset.layer = layer;
|
|
381
|
+
button.textContent = layer;
|
|
382
|
+
button.title = `Toggle ${layer} layer`;
|
|
383
|
+
button.addEventListener("click", () => options.onToggle(layer, !isEnabled(layer)));
|
|
384
|
+
buttons.set(layer, button);
|
|
385
|
+
bar.append(button);
|
|
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);
|
|
441
|
+
const close = document.createElement("button");
|
|
442
|
+
close.type = "button";
|
|
443
|
+
close.dataset.action = "close";
|
|
444
|
+
close.textContent = "\xD7";
|
|
445
|
+
close.title = "Close litedesign";
|
|
446
|
+
close.setAttribute("aria-label", "Close litedesign");
|
|
447
|
+
close.addEventListener("click", () => options.onClose());
|
|
448
|
+
bar.append(close);
|
|
449
|
+
popup.append(input, removeButton);
|
|
450
|
+
root2.append(style, bar, popup);
|
|
451
|
+
const setLayers = (layers) => {
|
|
452
|
+
enabled = new Set(layers);
|
|
453
|
+
for (const [layer, button] of buttons) {
|
|
454
|
+
const on = enabled.has(layer);
|
|
455
|
+
button.setAttribute("aria-pressed", String(on));
|
|
456
|
+
button.classList.toggle("on", on);
|
|
457
|
+
}
|
|
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
|
+
};
|
|
468
|
+
setLayers(options.layers);
|
|
469
|
+
setGrid(grid);
|
|
470
|
+
return {
|
|
471
|
+
element: root2,
|
|
472
|
+
setStats(nodes, issues, truncated) {
|
|
473
|
+
stats.textContent = `${nodes} nodes \xB7 ${issues} issues${truncated ? " \xB7 truncated" : ""}`;
|
|
474
|
+
},
|
|
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
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// packages/extension/src/boot.ts
|
|
500
|
+
var mountedHost;
|
|
501
|
+
function overlayHost() {
|
|
502
|
+
return mountedHost;
|
|
503
|
+
}
|
|
504
|
+
async function boot(core2) {
|
|
505
|
+
if (window.__litedesign_state__ === "active") {
|
|
506
|
+
window.__litedesign_teardown__?.();
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
if (window.__litedesign_state__ === "starting") return;
|
|
510
|
+
window.__litedesign_state__ = "starting";
|
|
511
|
+
const state = await loadState();
|
|
512
|
+
if (window.__litedesign_state__ !== "starting") return;
|
|
513
|
+
try {
|
|
514
|
+
mount(core2, state.layers, state.grid);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
window.__litedesign_state__ = void 0;
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
async function applyOverlay(core2, options) {
|
|
521
|
+
if (!options.enabled) {
|
|
522
|
+
window.__litedesign_teardown__?.();
|
|
523
|
+
return { enabled: false, layers: [], focus: null };
|
|
524
|
+
}
|
|
525
|
+
const stored = await loadState();
|
|
526
|
+
const layers = options.layers ?? stored.layers;
|
|
527
|
+
if (window.__litedesign_state__ === "active") {
|
|
528
|
+
window.__litedesign_layers__?.(layers);
|
|
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
|
+
}
|
|
537
|
+
}
|
|
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
|
+
};
|
|
544
|
+
}
|
|
545
|
+
function mount(core2, initial, initialGrid) {
|
|
546
|
+
const root2 = document.body ?? document.documentElement;
|
|
547
|
+
let layers = initial;
|
|
548
|
+
let grid = initialGrid;
|
|
549
|
+
let graph = core2.buildGraph(root2);
|
|
550
|
+
let handle;
|
|
551
|
+
let observer;
|
|
552
|
+
let focusSelector = null;
|
|
553
|
+
let focusId = null;
|
|
554
|
+
let labels = [];
|
|
555
|
+
let labelMode = false;
|
|
556
|
+
let toolbar;
|
|
557
|
+
const teardown = () => {
|
|
558
|
+
observer?.stop();
|
|
559
|
+
handle?.destroy();
|
|
560
|
+
handle = void 0;
|
|
561
|
+
observer = void 0;
|
|
562
|
+
mountedHost = void 0;
|
|
563
|
+
focusSelector = null;
|
|
564
|
+
focusId = null;
|
|
565
|
+
if (labelMode) document.removeEventListener("click", onLabelClick, true);
|
|
566
|
+
labelMode = false;
|
|
567
|
+
window.__litedesign_teardown__ = void 0;
|
|
568
|
+
window.__litedesign_layers__ = void 0;
|
|
569
|
+
window.__litedesign_set_focus__ = void 0;
|
|
570
|
+
window.__litedesign_focus_selector__ = void 0;
|
|
571
|
+
window.__litedesign_state__ = void 0;
|
|
572
|
+
void saveState({ enabled: false, layers, focus: null, grid });
|
|
573
|
+
};
|
|
574
|
+
const refresh = () => {
|
|
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
|
+
}
|
|
581
|
+
handle?.update(graph);
|
|
582
|
+
toolbar.setStats(graph.nodes.length, graph.issues.length, graph.truncated);
|
|
583
|
+
};
|
|
584
|
+
const setLayers = (next) => {
|
|
585
|
+
layers = next;
|
|
586
|
+
handle?.setLayers(layers);
|
|
587
|
+
toolbar.setLayers(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
|
+
}
|
|
673
|
+
};
|
|
674
|
+
toolbar = createToolbar({
|
|
675
|
+
layers,
|
|
676
|
+
grid,
|
|
677
|
+
onToggle(layer, enabled) {
|
|
678
|
+
setLayers(
|
|
679
|
+
enabled ? [.../* @__PURE__ */ new Set([...layers, layer])] : layers.filter((name) => name !== layer)
|
|
680
|
+
);
|
|
681
|
+
},
|
|
682
|
+
onLabelToggle: toggleLabelMode,
|
|
683
|
+
onGrid: setGrid,
|
|
684
|
+
onClose: teardown
|
|
685
|
+
});
|
|
686
|
+
toolbar.setStats(graph.nodes.length, graph.issues.length, graph.truncated);
|
|
687
|
+
handle = core2.render(graph, { layers, toolbar: toolbar.element, grid });
|
|
688
|
+
observer = observeChanges({ root: root2, onChange: refresh });
|
|
689
|
+
void loadLabels().then((next) => {
|
|
690
|
+
labels = next;
|
|
691
|
+
applyMarks();
|
|
692
|
+
});
|
|
693
|
+
mountedHost = handle.host;
|
|
694
|
+
window.__litedesign_layers__ = setLayers;
|
|
695
|
+
window.__litedesign_set_focus__ = setFocus;
|
|
696
|
+
window.__litedesign_focus_selector__ = () => focusSelector;
|
|
697
|
+
window.__litedesign_teardown__ = teardown;
|
|
698
|
+
window.__litedesign_state__ = "active";
|
|
699
|
+
void saveState({ enabled: true, layers, focus: focusSelector, grid });
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// packages/extension/src/bridge.ts
|
|
703
|
+
var TEMP_LAYERS = ["grid", "guide", "gap", "issue"];
|
|
704
|
+
var FULL_PAGE = "full-page screenshots need the Playwright driver. The extension can only capture the visible viewport.";
|
|
705
|
+
var suspended;
|
|
706
|
+
function installBridge(core2) {
|
|
707
|
+
window.__litedesign_toggle__ = () => void boot(core2);
|
|
708
|
+
if (window.__litedesign_bridge__ === true) return;
|
|
709
|
+
window.__litedesign_bridge__ = true;
|
|
710
|
+
if (typeof chrome === "undefined" || chrome.runtime?.onMessage === void 0) return;
|
|
711
|
+
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
712
|
+
void respond(core2, message).then(sendResponse, (error) => {
|
|
713
|
+
sendResponse({ ok: false, error: messageOf(error) });
|
|
714
|
+
});
|
|
715
|
+
return true;
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
async function respond(core2, raw) {
|
|
719
|
+
if (typeof raw !== "object" || raw === null) throw new Error("invalid message");
|
|
720
|
+
const message = raw;
|
|
721
|
+
const action = typeof message.action === "string" ? message.action : "";
|
|
722
|
+
const payload = message.payload ?? {};
|
|
723
|
+
switch (action) {
|
|
724
|
+
case "ping":
|
|
725
|
+
return { ok: true, result: true };
|
|
726
|
+
case "toggle":
|
|
727
|
+
await boot(core2);
|
|
728
|
+
return { ok: true };
|
|
729
|
+
case "overlay":
|
|
730
|
+
return {
|
|
731
|
+
ok: true,
|
|
732
|
+
result: await applyOverlay(core2, {
|
|
733
|
+
enabled: payload.enabled !== false,
|
|
734
|
+
layers: payload.layers,
|
|
735
|
+
focus: payload.focus
|
|
736
|
+
})
|
|
737
|
+
};
|
|
738
|
+
case "labels":
|
|
739
|
+
return { ok: true, result: { labels: await loadLabels() } };
|
|
740
|
+
case "graph":
|
|
741
|
+
return { ok: true, result: core2.buildGraph(root()) };
|
|
742
|
+
case "measure":
|
|
743
|
+
return { ok: true, result: measure(core2, payload.selector) };
|
|
744
|
+
case "screenshot_prepare":
|
|
745
|
+
return { ok: true, result: await prepareScreenshot(core2, payload) };
|
|
746
|
+
case "annotate_prepare":
|
|
747
|
+
return { ok: true, result: await prepareAnnotation(core2, payload) };
|
|
748
|
+
case "capture_cleanup":
|
|
749
|
+
await cleanup();
|
|
750
|
+
return { ok: true };
|
|
751
|
+
default:
|
|
752
|
+
throw new Error(`unknown action: ${action || "(none)"}`);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
function measure(core2, selector) {
|
|
756
|
+
if (selector === void 0 || selector === "") throw new Error("measure needs a selector");
|
|
757
|
+
const graph = core2.buildGraph(root());
|
|
758
|
+
const node = core2.resolveNode(graph, selector);
|
|
759
|
+
if (node === null) throw new Error(`selector "${selector}" did not match any element on the page`);
|
|
760
|
+
const gaps = graph.gaps.filter((gap) => gap.a === node.id || gap.b === node.id).map((gap) => ({
|
|
761
|
+
to: gap.a === node.id ? gap.b : gap.a,
|
|
762
|
+
axis: gap.axis,
|
|
763
|
+
distance: gap.distance,
|
|
764
|
+
kind: gap.kind
|
|
765
|
+
}));
|
|
766
|
+
return {
|
|
767
|
+
id: node.id,
|
|
768
|
+
selector: node.selector,
|
|
769
|
+
tag: node.tag,
|
|
770
|
+
rect: node.rect,
|
|
771
|
+
styles: node.styles,
|
|
772
|
+
gaps
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
async function prepareScreenshot(core2, payload) {
|
|
776
|
+
if (payload.fullPage === true) throw new Error(FULL_PAGE);
|
|
777
|
+
const dpr = window.devicePixelRatio || 1;
|
|
778
|
+
const rect = await prepareElement(core2, payload.selector);
|
|
779
|
+
suspend();
|
|
780
|
+
return { rect, dpr };
|
|
781
|
+
}
|
|
782
|
+
async function prepareAnnotation(core2, payload) {
|
|
783
|
+
if (payload.fullPage === true) throw new Error(FULL_PAGE);
|
|
784
|
+
const dpr = window.devicePixelRatio || 1;
|
|
785
|
+
const rect = await prepareElement(core2, payload.selector);
|
|
786
|
+
let graph = core2.buildGraph(root());
|
|
787
|
+
if (payload.rules !== void 0 && payload.rules.length > 0) {
|
|
788
|
+
const wanted = new Set(payload.rules);
|
|
789
|
+
graph = { ...graph, issues: graph.issues.filter((issue) => wanted.has(issue.rule)) };
|
|
790
|
+
}
|
|
791
|
+
const handle = core2.render(graph, { layers: payload.layers ?? TEMP_LAYERS });
|
|
792
|
+
suspend(handle);
|
|
793
|
+
await frames(2);
|
|
794
|
+
return { rect, dpr };
|
|
795
|
+
}
|
|
796
|
+
async function cleanup() {
|
|
797
|
+
const current = suspended;
|
|
798
|
+
if (current === void 0) return;
|
|
799
|
+
suspended = void 0;
|
|
800
|
+
current.handle?.destroy();
|
|
801
|
+
showUserOverlay(current.hidden);
|
|
802
|
+
}
|
|
803
|
+
function suspend(handle) {
|
|
804
|
+
const previous = suspended;
|
|
805
|
+
previous?.handle?.destroy();
|
|
806
|
+
showUserOverlay(previous?.hidden);
|
|
807
|
+
suspended = handle === void 0 ? { hidden: hideUserOverlay() } : { handle, hidden: hideUserOverlay() };
|
|
808
|
+
}
|
|
809
|
+
function hideUserOverlay() {
|
|
810
|
+
const element = overlayHost();
|
|
811
|
+
if (element === void 0) return void 0;
|
|
812
|
+
const display = element.style.display;
|
|
813
|
+
element.style.display = "none";
|
|
814
|
+
return { element, display };
|
|
815
|
+
}
|
|
816
|
+
function showUserOverlay(hidden) {
|
|
817
|
+
if (hidden !== void 0) hidden.element.style.display = hidden.display;
|
|
818
|
+
}
|
|
819
|
+
async function prepareElement(core2, selector) {
|
|
820
|
+
if (selector === void 0 || selector === "") return void 0;
|
|
821
|
+
const graph = core2.buildGraph(root());
|
|
822
|
+
const node = core2.resolveNode(graph, selector);
|
|
823
|
+
if (node === null) throw new Error(`selector "${selector}" did not match any element on the page`);
|
|
824
|
+
let element = null;
|
|
825
|
+
try {
|
|
826
|
+
element = document.querySelector(node.selector);
|
|
827
|
+
} catch {
|
|
828
|
+
element = null;
|
|
829
|
+
}
|
|
830
|
+
if (element === null) {
|
|
831
|
+
throw new Error(
|
|
832
|
+
`selector "${selector}" is inside a shadow root. Measure it, but capture the whole viewport instead.`
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
element.scrollIntoView({ block: "center", inline: "center" });
|
|
836
|
+
await frames(1);
|
|
837
|
+
const rect = element.getBoundingClientRect();
|
|
838
|
+
return { x: rect.left, y: rect.top, w: rect.width, h: rect.height };
|
|
839
|
+
}
|
|
840
|
+
function root() {
|
|
841
|
+
return document.body ?? document.documentElement;
|
|
842
|
+
}
|
|
843
|
+
function frames(count) {
|
|
844
|
+
return new Promise((resolve) => {
|
|
845
|
+
let left = count;
|
|
846
|
+
const tick = () => {
|
|
847
|
+
left -= 1;
|
|
848
|
+
if (left <= 0) resolve();
|
|
849
|
+
else requestAnimationFrame(tick);
|
|
850
|
+
};
|
|
851
|
+
requestAnimationFrame(tick);
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
function messageOf(error) {
|
|
855
|
+
if (error instanceof Error && error.message !== "") return error.message;
|
|
856
|
+
if (typeof error === "string" && error !== "") return error;
|
|
857
|
+
return "unexpected error";
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// packages/extension/src/content.ts
|
|
861
|
+
var core = window.__litedesign;
|
|
862
|
+
if (core === void 0) {
|
|
863
|
+
console.warn("[litedesign] core not found; core.iife.js must be injected first");
|
|
864
|
+
} else {
|
|
865
|
+
installBridge(core);
|
|
866
|
+
void restore(core);
|
|
867
|
+
}
|
|
868
|
+
async function restore(core2) {
|
|
869
|
+
const state = await loadState();
|
|
870
|
+
if (!state.enabled) return;
|
|
871
|
+
await applyOverlay(core2, { enabled: true, layers: state.layers, focus: state.focus });
|
|
872
|
+
}
|
|
873
|
+
})();
|
|
874
|
+
//# sourceMappingURL=content.js.map
|