@dev-lines/core 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.
@@ -0,0 +1,869 @@
1
+ // src/toolbar.ts
2
+ var EASE = "cubic-bezier(0.215, 0.61, 0.355, 1)";
3
+ var STYLE = `
4
+ .dvl-toolbar { position: fixed; bottom: 22px; left: 50%; transform: translateX(-50%); z-index: 2147483601;
5
+ display: flex; align-items: center; gap: 2px; background: #fff; border: 1px solid #ececec;
6
+ border-radius: 999px; padding: 5px; box-shadow: 0 6px 22px rgb(0 0 0 / .08);
7
+ font-family: "Geist", system-ui, sans-serif; font-size: 15px; line-height: 1.5; letter-spacing: -0.01em; color: #0a0a0a; }
8
+ .dvl-toolbar, .dvl-toolbar * { box-sizing: border-box; }
9
+ .dvl-btn { all: unset; cursor: pointer; width: 28px; height: 28px; border-radius: 999px; position: relative;
10
+ display: grid; place-items: center; color: #9ca3af; transition: background .12s, color .12s, transform .12s ${EASE}; }
11
+ .dvl-btn:hover { background: #fafafa; color: #6b7280; }
12
+ .dvl-btn:active { transform: scale(.92); }
13
+ .dvl-btn.active { background: rgb(113 113 122 / .12); color: rgb(113 113 122); }
14
+ .dvl-btn.dvl-power.active { background: rgb(113 113 122); color: #fff; }
15
+ .dvl-btn.dim { opacity: .35; pointer-events: none; }
16
+ .dvl-btn .dvl-dot { position: absolute; bottom: 3px; right: 4px; width: 4px; height: 4px; border-radius: 50%; background: rgb(113 113 122); display: none; }
17
+ .dvl-btn.active .dvl-dot { display: block; }
18
+ .dvl-sep { width: 1px; height: 18px; background: #ececec; margin: 0 3px; }
19
+ .dvl-menu { position: absolute; bottom: calc(100% + 7px); top: auto; left: 0; width: 222px; background: #fff;
20
+ border: 1px solid #ececec; border-radius: 12px; box-shadow: 0 10px 28px rgb(0 0 0 / .12); padding: 12px;
21
+ transform-origin: bottom left; animation: dvl-menu-in .18s ${EASE}; }
22
+ .dvl-menu[hidden] { display: none; }
23
+ @keyframes dvl-menu-in { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }
24
+ .dvl-row { display: flex; align-items: center; justify-content: space-between; padding: 7px 0; font-size: 12px; color: #6b7280; }
25
+ .dvl-faint { color: #9ca3af; }
26
+ .dvl-seg { display: inline-flex; gap: 2px; background: #fafafa; border: 1px solid #ececec; border-radius: 7px; padding: 2px; }
27
+ .dvl-seg button { all: unset; cursor: pointer; font: 11px "Geist Mono", ui-monospace, monospace; padding: 2px 7px; border-radius: 5px; color: #9ca3af; }
28
+ .dvl-seg button.on { background: #fff; color: #0a0a0a; box-shadow: 0 1px 2px rgb(0 0 0 / .08); }
29
+ .dvl-sw { display: flex; gap: 5px; }
30
+ .dvl-sw input[type=color] { -webkit-appearance: none; appearance: none; width: 16px; height: 16px;
31
+ border: none; border-radius: 4px; padding: 0; cursor: pointer; background: none; box-shadow: inset 0 0 0 1px rgb(0 0 0 / .12); }
32
+ .dvl-sw input[type=color]::-webkit-color-swatch-wrapper { padding: 0; }
33
+ .dvl-sw input[type=color]::-webkit-color-swatch { border: none; border-radius: 4px; }
34
+ .dvl-depth { flex-wrap: wrap; justify-content: flex-end; max-width: 156px; }
35
+ .dvl-keys { margin-top: 8px; padding-top: 9px; border-top: 1px solid #ececec; font: 11px/1.8 "Geist Mono", ui-monospace, monospace; color: #9ca3af; }
36
+ .dvl-keys b { color: #6b7280; font-weight: 600; }
37
+ `;
38
+ var ICONS = {
39
+ power: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"><path d="M8 2v6"/><path d="M4.4 4.4a5 5 0 1 0 7.2 0"/></svg>',
40
+ guides: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 1v14M1 8h14"/></svg>',
41
+ outlines: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="2.5" y="2.5" width="11" height="11" rx="2"/></svg>',
42
+ labels: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M2.6 6.4 7 2h6.5v6.5L9 13z"/><circle cx="10.4" cy="5.6" r="1" fill="currentColor" stroke="none"/></svg>',
43
+ copy: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5"/><path d="M3 10.5V3.5a1 1 0 0 1 1-1h7"/></svg>',
44
+ more: '<svg width="15" height="15" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6l4 4 4-4"/></svg>'
45
+ };
46
+ var toTriplet = (hex) => {
47
+ const n = parseInt(hex.slice(1), 16);
48
+ return `${n >> 16 & 255} ${n >> 8 & 255} ${n & 255}`;
49
+ };
50
+ var toHex = (triplet) => "#" + triplet.trim().split(/\s+/).map((x) => (+x).toString(16).padStart(2, "0")).join("");
51
+ function mountToolbar(api) {
52
+ const style = document.createElement("style");
53
+ style.setAttribute("data-devlines-toolbar", "");
54
+ style.textContent = STYLE;
55
+ document.head.appendChild(style);
56
+ const el = document.createElement("div");
57
+ el.className = "dvl-toolbar";
58
+ el.setAttribute("data-devlines", "ignore");
59
+ el.innerHTML = `
60
+ <button class="dvl-btn dvl-power" title="Toggle dev-lines (\u2318/Ctrl+Shift+L)">${ICONS.power}</button>
61
+ <span class="dvl-sep"></span>
62
+ <button class="dvl-btn dvl-guides" title="Guides (G)">${ICONS.guides}</button>
63
+ <button class="dvl-btn dvl-outlines" title="Outlines (O)">${ICONS.outlines}</button>
64
+ <button class="dvl-btn dvl-labels" title="Labels (L)">${ICONS.labels}<span class="dvl-dot"></span></button>
65
+ <button class="dvl-btn dvl-copy" title="Copy handle (C)">${ICONS.copy}</button>
66
+ <span class="dvl-sep"></span>
67
+ <button class="dvl-btn dvl-more" title="Settings">${ICONS.more}</button>
68
+ <div class="dvl-menu" hidden>
69
+ <div class="dvl-row"><span>Labels</span>
70
+ <span class="dvl-seg">
71
+ <button data-mode="off">off</button><button data-mode="hover">hover</button><button data-mode="all">all</button>
72
+ </span>
73
+ </div>
74
+ <div class="dvl-row"><span>Distances</span><span class="dvl-faint">hold Alt</span></div>
75
+ <div class="dvl-row"><span>Guide colors</span><span class="dvl-sw">
76
+ <input type="color" class="dvl-c-line" title="Center &amp; width" />
77
+ <input type="color" class="dvl-c-pad" title="Padding" />
78
+ </span></div>
79
+ <div class="dvl-row"><span>Depth palette</span><span class="dvl-sw dvl-depth"></span></div>
80
+ <div class="dvl-keys"><b>O</b> outlines \xB7 <b>G</b> guides \xB7 <b>L</b> labels \xB7 <b>I</b> inspect \xB7 <b>B</b> boxes \xB7 <b>C</b> copy \xB7 <b>Alt</b> dist \xB7 <b>Esc</b> off</div>
81
+ </div>`;
82
+ document.body.appendChild(el);
83
+ const q = (sel) => el.querySelector(sel);
84
+ const power = q(".dvl-power");
85
+ const guides = q(".dvl-guides");
86
+ const outlines = q(".dvl-outlines");
87
+ const labels = q(".dvl-labels");
88
+ const copy = q(".dvl-copy");
89
+ const more = q(".dvl-more");
90
+ const menu = q(".dvl-menu");
91
+ const seg = q(".dvl-seg");
92
+ const cLine = q(".dvl-c-line");
93
+ const cPad = q(".dvl-c-pad");
94
+ const cDepth = q(".dvl-depth");
95
+ const ensureOn = () => {
96
+ if (!api.getState().enabled) api.enable();
97
+ };
98
+ function sync() {
99
+ const s = api.getState();
100
+ power.classList.toggle("active", s.enabled);
101
+ guides.classList.toggle("active", s.enabled && s.guides);
102
+ outlines.classList.toggle("active", s.enabled && s.outlines);
103
+ labels.classList.toggle("active", s.enabled && s.labels !== "off");
104
+ labels.title = "Labels: " + s.labels + " (L)";
105
+ for (const b of [guides, outlines, labels, copy]) b.classList.toggle("dim", !s.enabled);
106
+ seg.querySelectorAll("button").forEach((b) => b.classList.toggle("on", b.dataset.mode === s.labels));
107
+ }
108
+ power.addEventListener("click", () => api.toggle());
109
+ guides.addEventListener("click", () => {
110
+ ensureOn();
111
+ api.toggleGuides();
112
+ });
113
+ outlines.addEventListener("click", () => {
114
+ ensureOn();
115
+ api.toggleOutlines();
116
+ });
117
+ labels.addEventListener("click", () => {
118
+ ensureOn();
119
+ api.cycleLabels();
120
+ });
121
+ copy.addEventListener("click", () => {
122
+ ensureOn();
123
+ api.copy();
124
+ });
125
+ more.addEventListener("click", () => {
126
+ menu.hidden = !menu.hidden;
127
+ });
128
+ seg.querySelectorAll("button").forEach((b) => b.addEventListener("click", () => {
129
+ ensureOn();
130
+ api.cycleLabels(b.dataset.mode);
131
+ }));
132
+ const palette = api.colors();
133
+ cLine.value = toHex(palette.line);
134
+ cPad.value = toHex(palette.padding);
135
+ for (const t of palette.depth) {
136
+ const input = document.createElement("input");
137
+ input.type = "color";
138
+ input.value = toHex(t);
139
+ cDepth.appendChild(input);
140
+ }
141
+ cLine.addEventListener("input", () => {
142
+ ensureOn();
143
+ api.update({ lineColor: toTriplet(cLine.value) });
144
+ });
145
+ cPad.addEventListener("input", () => {
146
+ ensureOn();
147
+ api.update({ paddingColor: toTriplet(cPad.value) });
148
+ });
149
+ cDepth.addEventListener("input", () => {
150
+ ensureOn();
151
+ api.update({ depthColors: Array.from(cDepth.querySelectorAll("input"), (i) => toTriplet(i.value)) });
152
+ });
153
+ const onDocClick = (e) => {
154
+ if (!el.contains(e.target)) menu.hidden = true;
155
+ };
156
+ document.addEventListener("click", onDocClick);
157
+ sync();
158
+ return {
159
+ el,
160
+ sync,
161
+ destroy() {
162
+ document.removeEventListener("click", onDocClick);
163
+ el.remove();
164
+ style.remove();
165
+ }
166
+ };
167
+ }
168
+
169
+ // src/index.ts
170
+ var STORAGE_KEY = "dev-lines:enabled";
171
+ var Z = 2147483600;
172
+ var DEFAULT_DEPTH_COLORS = [
173
+ "29 29 29",
174
+ // #1d1d1d neutral (graphite)
175
+ "155 164 183",
176
+ // #9ba4b7 neutral (gray)
177
+ "167 125 255",
178
+ // #a77dff purple
179
+ "245 104 104",
180
+ // #f56868 coral
181
+ "237 139 0",
182
+ // #ed8b00 orange
183
+ "241 223 56",
184
+ // #f1df38 yellow
185
+ "138 224 108"
186
+ // #8ae06c green
187
+ ];
188
+ var DEFAULTS = {
189
+ lineColor: "245 104 104",
190
+ // #f56868 coral
191
+ paddingColor: "167 125 255",
192
+ // #a77dff purple
193
+ measureColor: "237 139 0",
194
+ // #ed8b00 orange
195
+ gapColor: "138 224 108",
196
+ // #8ae06c green
197
+ sections: true,
198
+ shortcut: "mod+shift+l",
199
+ labelKey: "l",
200
+ copyKey: "c",
201
+ inspectKey: "i",
202
+ boxModel: true,
203
+ boxModelKey: "b",
204
+ toolbar: false,
205
+ nameAttr: "data-devlines-name",
206
+ autoName: true,
207
+ persist: true,
208
+ start: false
209
+ };
210
+ var DEFAULT_LABELS = {
211
+ mode: "hover",
212
+ show: ["name", "tag", "size", "display"],
213
+ guides: true
214
+ };
215
+ var isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
216
+ function createDevLines(options = {}) {
217
+ if (!isBrowser) return noopController();
218
+ const opts = { ...DEFAULTS, ...options };
219
+ let depthColors = options.depthColors ?? DEFAULT_DEPTH_COLORS;
220
+ const labels = { ...DEFAULT_LABELS, ...options.labels };
221
+ let enabled = false;
222
+ let root = null;
223
+ let sectionsLayer = null;
224
+ let boxLayer = null;
225
+ let guidesLayer = null;
226
+ let labelsLayer = null;
227
+ let measureLayer = null;
228
+ let scheduled = false;
229
+ let ro = null;
230
+ let mo = null;
231
+ let boxes = [];
232
+ let pointer = null;
233
+ let hoverBox = null;
234
+ let lastHoverBox = null;
235
+ let lockedEl = null;
236
+ let lockedBox = null;
237
+ let tb = null;
238
+ let outlinesOn = opts.sections;
239
+ let guidesOn = true;
240
+ let boxModelOn = opts.boxModel;
241
+ let altDown = false;
242
+ const rgba = (t, a) => `rgb(${t} / ${a})`;
243
+ const colorForDepth = (d) => depthColors[Math.min(d, depthColors.length - 1)];
244
+ const notify = () => tb?.sync();
245
+ const focusBox = () => lockedBox ?? hoverBox;
246
+ function line(css) {
247
+ const el = document.createElement("div");
248
+ Object.assign(el.style, { position: "absolute", ...css });
249
+ return el;
250
+ }
251
+ function chip(text, colorTriplet, left, top) {
252
+ const el = document.createElement("div");
253
+ el.textContent = text;
254
+ Object.assign(el.style, {
255
+ position: "absolute",
256
+ left: `${Math.max(0, left)}px`,
257
+ top: `${Math.max(0, top)}px`,
258
+ font: '500 10px/1.4 "Geist Mono", ui-monospace, monospace',
259
+ letterSpacing: "0.01em",
260
+ color: "#fff",
261
+ background: rgba(colorTriplet, 0.92),
262
+ padding: "1px 5px",
263
+ borderRadius: "4px",
264
+ whiteSpace: "nowrap",
265
+ pointerEvents: "none",
266
+ transform: "translateZ(0)"
267
+ });
268
+ return el;
269
+ }
270
+ function buildGuides() {
271
+ const layer = document.createElement("div");
272
+ Object.assign(layer.style, { position: "absolute", inset: "0" });
273
+ const c = rgba(opts.lineColor, 0.75);
274
+ layer.appendChild(line({ top: "0", bottom: "0", left: "50%", width: "1px", transform: "translateX(-50%)", background: c }));
275
+ layer.appendChild(line({ left: "0", right: "0", top: "50%", height: "1px", transform: "translateY(-50%)", background: c }));
276
+ if (opts.contentWidth && opts.contentWidth > 0) {
277
+ const half = opts.contentWidth / 2;
278
+ layer.appendChild(line({ top: "0", bottom: "0", left: "50%", width: "1px", transform: `translateX(${-half}px)`, background: c }));
279
+ layer.appendChild(line({ top: "0", bottom: "0", left: "50%", width: "1px", transform: `translateX(${half}px)`, background: c }));
280
+ }
281
+ if (opts.paddingX && opts.paddingX > 0) {
282
+ const p = rgba(opts.paddingColor, 0.75);
283
+ layer.appendChild(line({ top: "0", bottom: "0", left: `${opts.paddingX}px`, width: "1px", background: p }));
284
+ layer.appendChild(line({ top: "0", bottom: "0", right: `${opts.paddingX}px`, width: "1px", background: p }));
285
+ }
286
+ return layer;
287
+ }
288
+ function isContainer(el) {
289
+ if (el.getAttribute("data-devlines") === "outline") return true;
290
+ const display = getComputedStyle(el).display;
291
+ return display.includes("flex") || display.includes("grid");
292
+ }
293
+ function collect() {
294
+ const out = [];
295
+ const walk = (el, depth) => {
296
+ if (el === root || el === tb?.el || el.getAttribute("data-devlines") === "ignore") return;
297
+ const hit = isContainer(el);
298
+ if (hit) out.push({ el, depth });
299
+ const next = hit ? depth + 1 : depth;
300
+ for (const child of el.children) walk(child, next);
301
+ };
302
+ if (opts.sectionSelector) {
303
+ document.querySelectorAll(opts.sectionSelector).forEach((el) => out.push({ el, depth: 0 }));
304
+ } else {
305
+ for (const child of document.body.children) walk(child, 0);
306
+ }
307
+ return out;
308
+ }
309
+ function drawSections() {
310
+ if (!sectionsLayer) return;
311
+ sectionsLayer.replaceChildren();
312
+ boxes = [];
313
+ if (outlinesOn) {
314
+ for (const { el, depth } of collect()) {
315
+ const rect = el.getBoundingClientRect();
316
+ if (rect.width === 0 || rect.height === 0) continue;
317
+ boxes.push({ el, depth, rect });
318
+ }
319
+ }
320
+ lockedBox = lockedEl ? boxes.find((b) => b.el === lockedEl) ?? null : null;
321
+ if (lockedEl && !lockedBox) lockedEl = null;
322
+ for (const b of boxes) {
323
+ const color = colorForDepth(b.depth);
324
+ const box = document.createElement("div");
325
+ const locked = b === lockedBox;
326
+ Object.assign(box.style, {
327
+ position: "absolute",
328
+ left: `${b.rect.left}px`,
329
+ top: `${b.rect.top}px`,
330
+ width: `${b.rect.width}px`,
331
+ height: `${b.rect.height}px`,
332
+ boxShadow: locked ? `inset 0 0 0 1.5px ${rgba(color, 1)}, 0 0 0 1px ${rgba(color, 0.3)}` : `inset 0 0 0 1px ${rgba(color, 0.6)}`
333
+ });
334
+ sectionsLayer.appendChild(box);
335
+ }
336
+ hoverBox = pointer ? boxAt(pointer.x, pointer.y) : null;
337
+ drawBoxModel();
338
+ drawLabels();
339
+ drawMeasure();
340
+ }
341
+ function nameOf(el) {
342
+ const explicit = el.getAttribute(opts.nameAttr);
343
+ if (explicit) return explicit;
344
+ if (!opts.autoName) return null;
345
+ if (el.id) return el.id;
346
+ const aria = el.getAttribute("aria-label");
347
+ if (aria) return aria;
348
+ const comp = el.getAttribute("data-component") || el.getAttribute("data-testid");
349
+ if (comp) return comp;
350
+ const h = el.querySelector("h1,h2,h3,h4,h5,h6");
351
+ const t = h?.textContent?.trim();
352
+ if (t) return t.length > 24 ? t.slice(0, 24) + "\u2026" : t;
353
+ return null;
354
+ }
355
+ function selectorFor(el) {
356
+ if (el.id) return `#${CSS.escape(el.id)}`;
357
+ const parts = [];
358
+ let node = el;
359
+ while (node && node !== document.body) {
360
+ const cur = node;
361
+ if (cur.id) {
362
+ parts.unshift(`#${CSS.escape(cur.id)}`);
363
+ break;
364
+ }
365
+ let sel = cur.tagName.toLowerCase();
366
+ const parent = cur.parentElement;
367
+ if (parent) {
368
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
369
+ if (sameTag.length > 1) sel += `:nth-of-type(${sameTag.indexOf(cur) + 1})`;
370
+ }
371
+ parts.unshift(sel);
372
+ node = parent;
373
+ }
374
+ return parts.join(" > ") || el.tagName.toLowerCase();
375
+ }
376
+ function displayOf(el) {
377
+ const d = getComputedStyle(el).display;
378
+ return d.includes("grid") ? "grid" : d.includes("flex") ? "flex" : d;
379
+ }
380
+ function labelText(el, depth, rect) {
381
+ const parts = [];
382
+ for (const f of labels.show) {
383
+ if (f === "name") {
384
+ const nm = nameOf(el);
385
+ if (nm) parts.push(nm);
386
+ } else if (f === "tag") parts.push(`<${el.tagName.toLowerCase()}>`);
387
+ else if (f === "size") parts.push(`${Math.round(rect.width)}\xD7${Math.round(rect.height)}`);
388
+ else if (f === "display") parts.push(displayOf(el));
389
+ else if (f === "depth") parts.push(`d${depth}`);
390
+ }
391
+ return parts.join(" \xB7 ");
392
+ }
393
+ function boxAt(x, y) {
394
+ let best = null;
395
+ for (const b of boxes) {
396
+ const r = b.rect;
397
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
398
+ if (!best || b.depth > best.depth) best = b;
399
+ }
400
+ }
401
+ return best;
402
+ }
403
+ function ancestorOf(target) {
404
+ let best = null;
405
+ for (const b of boxes) {
406
+ if (b === target || b.depth >= target.depth) continue;
407
+ const r = b.rect, t = target.rect;
408
+ if (r.left <= t.left && r.top <= t.top && r.right >= t.right && r.bottom >= t.bottom) {
409
+ if (!best || b.depth > best.depth) best = b;
410
+ }
411
+ }
412
+ return best;
413
+ }
414
+ function parentBoxOf(target) {
415
+ let best = null;
416
+ for (const b of boxes) {
417
+ if (b === target || !b.el.contains(target.el)) continue;
418
+ if (!best || best.el.contains(b.el)) best = b;
419
+ }
420
+ return best;
421
+ }
422
+ function childBoxesOf(target) {
423
+ return boxes.filter((b) => b !== target && target.el.contains(b.el) && parentBoxOf(b) === target);
424
+ }
425
+ function navigate(key) {
426
+ const cur = lockedBox;
427
+ if (!cur) return;
428
+ let next = null;
429
+ if (key === "arrowup") next = parentBoxOf(cur);
430
+ else if (key === "arrowdown") next = childBoxesOf(cur)[0];
431
+ else {
432
+ const parent = parentBoxOf(cur);
433
+ const sibs = parent ? childBoxesOf(parent) : boxes.filter((b) => !parentBoxOf(b));
434
+ const i = sibs.indexOf(cur);
435
+ if (i !== -1 && sibs.length > 1) {
436
+ next = sibs[(i + (key === "arrowright" ? 1 : -1) + sibs.length) % sibs.length];
437
+ }
438
+ }
439
+ if (next) inspect(next.el);
440
+ }
441
+ function inspect(el) {
442
+ if (el === void 0) {
443
+ lockedEl = lockedEl ? null : (hoverBox ?? lastHoverBox)?.el ?? null;
444
+ } else {
445
+ lockedEl = el;
446
+ }
447
+ if (enabled) drawSections();
448
+ notify();
449
+ }
450
+ function drawLabels() {
451
+ if (!labelsLayer) return;
452
+ labelsLayer.replaceChildren();
453
+ if (labels.guides && guidesOn) {
454
+ const cx = window.innerWidth / 2;
455
+ labelsLayer.appendChild(chip(`${Math.round(cx)}`, opts.lineColor, cx + 4, 4));
456
+ if (opts.contentWidth && opts.contentWidth > 0) {
457
+ const half = opts.contentWidth / 2;
458
+ labelsLayer.appendChild(chip(`\u2212${Math.round(half)}`, opts.lineColor, cx - half + 4, 22));
459
+ labelsLayer.appendChild(chip(`+${Math.round(half)}`, opts.lineColor, cx + half + 4, 22));
460
+ }
461
+ if (opts.paddingX && opts.paddingX > 0) {
462
+ labelsLayer.appendChild(chip(`${Math.round(opts.paddingX)}`, opts.paddingColor, opts.paddingX + 4, 4));
463
+ labelsLayer.appendChild(chip(`${Math.round(opts.paddingX)}`, opts.paddingColor, window.innerWidth - opts.paddingX + 4, 4));
464
+ }
465
+ }
466
+ if (!outlinesOn) return;
467
+ if (labels.mode === "all") {
468
+ for (const b of boxes) labelsLayer.appendChild(chip(labelText(b.el, b.depth, b.rect), colorForDepth(b.depth), b.rect.left, b.rect.top));
469
+ } else {
470
+ if (lockedBox) labelsLayer.appendChild(chip(labelText(lockedBox.el, lockedBox.depth, lockedBox.rect), colorForDepth(lockedBox.depth), lockedBox.rect.left, lockedBox.rect.top));
471
+ if (labels.mode === "hover" && hoverBox && hoverBox !== lockedBox) {
472
+ labelsLayer.appendChild(chip(labelText(hoverBox.el, hoverBox.depth, hoverBox.rect), colorForDepth(hoverBox.depth), hoverBox.rect.left, hoverBox.rect.top));
473
+ }
474
+ }
475
+ }
476
+ function drawBoxModel() {
477
+ if (!boxLayer) return;
478
+ boxLayer.replaceChildren();
479
+ const b = focusBox();
480
+ if (!boxModelOn || !outlinesOn || !b) return;
481
+ const r = b.rect;
482
+ const cs = getComputedStyle(b.el);
483
+ const band = (x, y, w, h, color, alpha) => {
484
+ if (w < 0.5 || h < 0.5) return;
485
+ boxLayer.appendChild(line({ left: `${x}px`, top: `${y}px`, width: `${w}px`, height: `${h}px`, background: rgba(color, alpha) }));
486
+ };
487
+ const pt = parseFloat(cs.paddingTop) || 0, pr = parseFloat(cs.paddingRight) || 0, pb = parseFloat(cs.paddingBottom) || 0, pl = parseFloat(cs.paddingLeft) || 0;
488
+ band(r.left, r.top, r.width, pt, opts.paddingColor, 0.16);
489
+ band(r.left, r.bottom - pb, r.width, pb, opts.paddingColor, 0.16);
490
+ band(r.left, r.top + pt, pl, r.height - pt - pb, opts.paddingColor, 0.16);
491
+ band(r.right - pr, r.top + pt, pr, r.height - pt - pb, opts.paddingColor, 0.16);
492
+ if (!cs.display.includes("flex") && !cs.display.includes("grid")) return;
493
+ const rowGap = parseFloat(cs.rowGap) || 0;
494
+ const colGap = parseFloat(cs.columnGap) || 0;
495
+ if (rowGap < 1 && colGap < 1) return;
496
+ const kids = [];
497
+ for (const k of b.el.children) {
498
+ if (k.getAttribute("data-devlines") === "ignore") continue;
499
+ const kr = k.getBoundingClientRect();
500
+ if (kr.width > 0 && kr.height > 0) kids.push(kr);
501
+ }
502
+ if (colGap >= 1) {
503
+ const byX = [...kids].sort((a, b2) => a.left - b2.left);
504
+ for (let i = 0; i < byX.length; i++) {
505
+ for (let j = i + 1; j < byX.length; j++) {
506
+ const a = byX[i], c = byX[j];
507
+ const w = c.left - a.right;
508
+ if (w < 1 || w > colGap + 1) continue;
509
+ const y1 = Math.max(a.top, c.top), y2 = Math.min(a.bottom, c.bottom);
510
+ if (y2 - y1 >= 1) band(a.right, y1, w, y2 - y1, opts.gapColor, 0.22);
511
+ }
512
+ }
513
+ }
514
+ if (rowGap >= 1) {
515
+ const byY = [...kids].sort((a, b2) => a.top - b2.top);
516
+ for (let i = 0; i < byY.length; i++) {
517
+ for (let j = i + 1; j < byY.length; j++) {
518
+ const a = byY[i], c = byY[j];
519
+ const h = c.top - a.bottom;
520
+ if (h < 1 || h > rowGap + 1) continue;
521
+ const x1 = Math.max(a.left, c.left), x2 = Math.min(a.right, c.right);
522
+ if (x2 - x1 >= 1) band(x1, a.bottom, x2 - x1, h, opts.gapColor, 0.22);
523
+ }
524
+ }
525
+ }
526
+ }
527
+ function measureSeg(x1, y1, x2, y2, value) {
528
+ if (!measureLayer || value < 1) return;
529
+ const horizontal = y1 === y2;
530
+ measureLayer.appendChild(line({
531
+ left: `${Math.min(x1, x2)}px`,
532
+ top: `${Math.min(y1, y2)}px`,
533
+ width: horizontal ? `${Math.abs(x2 - x1)}px` : "1px",
534
+ height: horizontal ? "1px" : `${Math.abs(y2 - y1)}px`,
535
+ background: rgba(opts.measureColor, 0.9)
536
+ }));
537
+ const mx = horizontal ? (x1 + x2) / 2 : x1 + 3;
538
+ const my = horizontal ? y1 + 3 : (y1 + y2) / 2;
539
+ measureLayer.appendChild(chip(`${Math.round(value)}`, opts.measureColor, mx, my));
540
+ }
541
+ function drawMeasure() {
542
+ if (!measureLayer) return;
543
+ measureLayer.replaceChildren();
544
+ const target = focusBox();
545
+ if (!altDown || !target || !outlinesOn) return;
546
+ const t = target.rect;
547
+ const anc = ancestorOf(target);
548
+ const c = anc ? anc.rect : new DOMRect(0, 0, window.innerWidth, window.innerHeight);
549
+ const midX = (t.left + t.right) / 2;
550
+ const midY = (t.top + t.bottom) / 2;
551
+ measureSeg(midX, c.top, midX, t.top, t.top - c.top);
552
+ measureSeg(midX, t.bottom, midX, c.bottom, c.bottom - t.bottom);
553
+ measureSeg(c.left, midY, t.left, midY, t.left - c.left);
554
+ measureSeg(t.right, midY, c.right, midY, c.right - t.right);
555
+ }
556
+ function flashCopied(rect) {
557
+ if (!root) return;
558
+ const el = chip("copied \u2713", "34 197 94", rect.left, rect.top - 2);
559
+ root.appendChild(el);
560
+ setTimeout(() => el.remove(), 900);
561
+ }
562
+ function paddingShorthand(cs) {
563
+ const v = [cs.paddingTop, cs.paddingRight, cs.paddingBottom, cs.paddingLeft].map((s) => `${Math.round(parseFloat(s) || 0)}px`);
564
+ if (v.every((s) => s === "0px")) return null;
565
+ if (v[0] === v[1] && v[1] === v[2] && v[2] === v[3]) return v[0];
566
+ if (v[0] === v[2] && v[1] === v[3]) return `${v[0]} ${v[1]}`;
567
+ return v.join(" ");
568
+ }
569
+ function gapShorthand(cs) {
570
+ if (!cs.display.includes("flex") && !cs.display.includes("grid")) return null;
571
+ const rg = Math.round(parseFloat(cs.rowGap) || 0);
572
+ const cg = Math.round(parseFloat(cs.columnGap) || 0);
573
+ if (!rg && !cg) return null;
574
+ return rg === cg ? `${rg}px` : `${rg}px ${cg}px`;
575
+ }
576
+ function copyHandle() {
577
+ const target = focusBox() ?? lastHoverBox;
578
+ if (!target) return;
579
+ const el = target.el;
580
+ const name = nameOf(el);
581
+ const cs = getComputedStyle(el);
582
+ const facts = [`${Math.round(target.rect.width)}\xD7${Math.round(target.rect.height)}`, displayOf(el)];
583
+ const pad = paddingShorthand(cs);
584
+ if (pad) facts.push(`padding ${pad}`);
585
+ const gap = gapShorthand(cs);
586
+ if (gap) facts.push(`gap ${gap}`);
587
+ const text = `${name ? `"${name}" \u2014 ` : ""}${selectorFor(el)} (<${el.tagName.toLowerCase()}>)
588
+ ${facts.join(" \xB7 ")}`;
589
+ navigator.clipboard?.writeText(text).then(() => flashCopied(target.rect)).catch(() => {
590
+ });
591
+ }
592
+ function schedule() {
593
+ if (scheduled) return;
594
+ scheduled = true;
595
+ requestAnimationFrame(() => {
596
+ scheduled = false;
597
+ drawSections();
598
+ });
599
+ }
600
+ function onPointerMove(e) {
601
+ pointer = { x: e.clientX, y: e.clientY };
602
+ hoverBox = boxAt(e.clientX, e.clientY);
603
+ if (hoverBox) lastHoverBox = hoverBox;
604
+ drawBoxModel();
605
+ drawLabels();
606
+ drawMeasure();
607
+ }
608
+ function ownNode(n) {
609
+ if (!n) return false;
610
+ if (root && (n === root || root.contains(n))) return true;
611
+ if (tb && (n === tb.el || tb.el.contains(n))) return true;
612
+ return false;
613
+ }
614
+ function enable() {
615
+ if (enabled) return;
616
+ enabled = true;
617
+ root = document.createElement("div");
618
+ root.setAttribute("data-devlines", "ignore");
619
+ Object.assign(root.style, { position: "fixed", inset: "0", zIndex: String(Z), pointerEvents: "none" });
620
+ sectionsLayer = document.createElement("div");
621
+ boxLayer = document.createElement("div");
622
+ guidesLayer = buildGuides();
623
+ labelsLayer = document.createElement("div");
624
+ measureLayer = document.createElement("div");
625
+ for (const l of [sectionsLayer, boxLayer, labelsLayer, measureLayer]) Object.assign(l.style, { position: "absolute", inset: "0" });
626
+ guidesLayer.style.display = guidesOn ? "" : "none";
627
+ root.append(sectionsLayer, boxLayer, guidesLayer, labelsLayer, measureLayer);
628
+ document.body.appendChild(root);
629
+ window.addEventListener("scroll", schedule, true);
630
+ window.addEventListener("resize", schedule);
631
+ window.addEventListener("pointermove", onPointerMove, true);
632
+ ro = new ResizeObserver(schedule);
633
+ ro.observe(document.documentElement);
634
+ mo = new MutationObserver((records) => {
635
+ for (const rec of records) {
636
+ if (ownNode(rec.target)) continue;
637
+ if (rec.type === "childList") {
638
+ const nodes = [...rec.addedNodes, ...rec.removedNodes];
639
+ if (nodes.length && nodes.every((n) => ownNode(n))) continue;
640
+ }
641
+ schedule();
642
+ return;
643
+ }
644
+ });
645
+ mo.observe(document.body, { childList: true, subtree: true, attributes: true });
646
+ drawSections();
647
+ if (opts.persist) try {
648
+ localStorage.setItem(STORAGE_KEY, "1");
649
+ } catch {
650
+ }
651
+ notify();
652
+ }
653
+ function disable() {
654
+ if (!enabled) return;
655
+ enabled = false;
656
+ altDown = false;
657
+ window.removeEventListener("scroll", schedule, true);
658
+ window.removeEventListener("resize", schedule);
659
+ window.removeEventListener("pointermove", onPointerMove, true);
660
+ ro?.disconnect();
661
+ mo?.disconnect();
662
+ ro = mo = null;
663
+ pointer = hoverBox = lastHoverBox = null;
664
+ lockedEl = null;
665
+ lockedBox = null;
666
+ root?.remove();
667
+ root = sectionsLayer = boxLayer = guidesLayer = labelsLayer = measureLayer = null;
668
+ boxes = [];
669
+ if (opts.persist) try {
670
+ localStorage.setItem(STORAGE_KEY, "0");
671
+ } catch {
672
+ }
673
+ notify();
674
+ }
675
+ function toggle() {
676
+ enabled ? disable() : enable();
677
+ }
678
+ function cycleLabels(mode) {
679
+ const order = ["off", "hover", "all"];
680
+ labels.mode = mode ?? order[(order.indexOf(labels.mode) + 1) % order.length];
681
+ drawLabels();
682
+ notify();
683
+ }
684
+ function toggleOutlines(on) {
685
+ outlinesOn = on ?? !outlinesOn;
686
+ drawSections();
687
+ notify();
688
+ }
689
+ function toggleGuides(on) {
690
+ guidesOn = on ?? !guidesOn;
691
+ if (guidesLayer) guidesLayer.style.display = guidesOn ? "" : "none";
692
+ drawLabels();
693
+ notify();
694
+ }
695
+ function toggleBoxModel(on) {
696
+ boxModelOn = on ?? !boxModelOn;
697
+ drawBoxModel();
698
+ notify();
699
+ }
700
+ function update(next) {
701
+ if (next.lineColor !== void 0) opts.lineColor = next.lineColor;
702
+ if (next.paddingColor !== void 0) opts.paddingColor = next.paddingColor;
703
+ if (next.measureColor !== void 0) opts.measureColor = next.measureColor;
704
+ if (next.gapColor !== void 0) opts.gapColor = next.gapColor;
705
+ if (next.depthColors !== void 0) depthColors = next.depthColors;
706
+ if (next.contentWidth !== void 0) opts.contentWidth = next.contentWidth;
707
+ if (next.paddingX !== void 0) opts.paddingX = next.paddingX;
708
+ if (next.sectionSelector !== void 0) opts.sectionSelector = next.sectionSelector;
709
+ if (next.nameAttr !== void 0) opts.nameAttr = next.nameAttr;
710
+ if (next.autoName !== void 0) opts.autoName = next.autoName;
711
+ if (next.labels !== void 0) Object.assign(labels, next.labels);
712
+ if (next.boxModel !== void 0) boxModelOn = next.boxModel;
713
+ if (root && guidesLayer) {
714
+ const fresh = buildGuides();
715
+ fresh.style.display = guidesOn ? "" : "none";
716
+ root.replaceChild(fresh, guidesLayer);
717
+ guidesLayer = fresh;
718
+ }
719
+ drawSections();
720
+ notify();
721
+ }
722
+ function typingInField() {
723
+ const t = document.activeElement;
724
+ return !!t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable);
725
+ }
726
+ function matchesShortcut(e, combo) {
727
+ const parts = combo.toLowerCase().split("+");
728
+ const key = parts[parts.length - 1];
729
+ const mod = e.metaKey || e.ctrlKey;
730
+ return e.key.toLowerCase() === key && parts.includes("mod") === mod && parts.includes("shift") === e.shiftKey && parts.includes("alt") === e.altKey;
731
+ }
732
+ function onKey(e) {
733
+ if (typingInField()) return;
734
+ if (opts.shortcut && matchesShortcut(e, opts.shortcut)) {
735
+ e.preventDefault();
736
+ toggle();
737
+ return;
738
+ }
739
+ if (!enabled) return;
740
+ if (e.key === "Alt" && !altDown) {
741
+ altDown = true;
742
+ drawMeasure();
743
+ return;
744
+ }
745
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
746
+ const k = e.key.toLowerCase();
747
+ if (k === "escape") {
748
+ e.preventDefault();
749
+ if (lockedEl) inspect(null);
750
+ else disable();
751
+ } else if (k === "o") {
752
+ e.preventDefault();
753
+ toggleOutlines();
754
+ } else if (k === "g") {
755
+ e.preventDefault();
756
+ toggleGuides();
757
+ } else if (opts.labelKey && k === opts.labelKey.toLowerCase()) {
758
+ e.preventDefault();
759
+ cycleLabels();
760
+ } else if (opts.copyKey && k === opts.copyKey.toLowerCase()) {
761
+ e.preventDefault();
762
+ copyHandle();
763
+ } else if (opts.inspectKey && k === opts.inspectKey.toLowerCase()) {
764
+ e.preventDefault();
765
+ inspect();
766
+ } else if (opts.boxModelKey && k === opts.boxModelKey.toLowerCase()) {
767
+ e.preventDefault();
768
+ toggleBoxModel();
769
+ } else if (lockedBox && (k === "arrowup" || k === "arrowdown" || k === "arrowleft" || k === "arrowright")) {
770
+ e.preventDefault();
771
+ navigate(k);
772
+ }
773
+ }
774
+ function onKeyUp(e) {
775
+ if (e.key === "Alt" && altDown) {
776
+ altDown = false;
777
+ drawMeasure();
778
+ }
779
+ }
780
+ function onBlur() {
781
+ if (altDown) {
782
+ altDown = false;
783
+ drawMeasure();
784
+ }
785
+ }
786
+ window.addEventListener("keydown", onKey);
787
+ window.addEventListener("keyup", onKeyUp);
788
+ window.addEventListener("blur", onBlur);
789
+ if (opts.toolbar) {
790
+ tb = mountToolbar({
791
+ toggle,
792
+ enable,
793
+ toggleGuides,
794
+ toggleOutlines,
795
+ cycleLabels,
796
+ copy: copyHandle,
797
+ update,
798
+ getState: () => ({ enabled, outlines: outlinesOn, guides: guidesOn, labels: labels.mode }),
799
+ colors: () => ({ line: opts.lineColor, padding: opts.paddingColor, depth: depthColors })
800
+ });
801
+ }
802
+ let startOn = opts.start;
803
+ if (opts.persist) {
804
+ try {
805
+ const saved = localStorage.getItem(STORAGE_KEY);
806
+ if (saved === "1") startOn = true;
807
+ else if (saved === "0") startOn = false;
808
+ } catch {
809
+ }
810
+ }
811
+ if (startOn) enable();
812
+ return {
813
+ enable,
814
+ disable,
815
+ toggle,
816
+ isEnabled: () => enabled,
817
+ cycleLabels,
818
+ toggleOutlines,
819
+ toggleGuides,
820
+ toggleBoxModel,
821
+ inspect,
822
+ update,
823
+ copy: copyHandle,
824
+ getState: () => ({ enabled, outlines: outlinesOn, guides: guidesOn, labels: labels.mode, boxModel: boxModelOn, inspected: lockedEl }),
825
+ refresh: drawSections,
826
+ destroy() {
827
+ disable();
828
+ window.removeEventListener("keydown", onKey);
829
+ window.removeEventListener("keyup", onKeyUp);
830
+ window.removeEventListener("blur", onBlur);
831
+ tb?.destroy();
832
+ tb = null;
833
+ }
834
+ };
835
+ }
836
+ function noopController() {
837
+ return {
838
+ enable() {
839
+ },
840
+ disable() {
841
+ },
842
+ toggle() {
843
+ },
844
+ isEnabled: () => false,
845
+ cycleLabels() {
846
+ },
847
+ toggleOutlines() {
848
+ },
849
+ toggleGuides() {
850
+ },
851
+ toggleBoxModel() {
852
+ },
853
+ inspect() {
854
+ },
855
+ update() {
856
+ },
857
+ copy() {
858
+ },
859
+ getState: () => ({ enabled: false, outlines: false, guides: false, labels: "off", boxModel: false, inspected: null }),
860
+ refresh() {
861
+ },
862
+ destroy() {
863
+ }
864
+ };
865
+ }
866
+
867
+ export {
868
+ createDevLines
869
+ };