@dev-lines/core 0.1.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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 wa-de
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,107 @@
1
+ # @dev-lines/core
2
+
3
+ A framework-agnostic **layout debug overlay**. Draws guides, depth-colored section outlines, labels, and spacing distances *on top of* any page โ€” without mutating your DOM. The whole overlay lives in one fixed, `pointer-events: none` layer, so your app is never touched and teardown is total.
4
+
5
+ > See the lines. Before you ship the bug. โ†’ **[lines.wiki](https://lines.wiki)**
6
+
7
+ - ๐ŸŽฏ Viewport center cross + optional content-width and padding guides
8
+ - ๐ŸŒˆ Depth-colored outlines around every flex/grid container (up to 12 depths)
9
+ - ๐Ÿท๏ธ Labels: name ยท tag ยท size ยท layout, per-box or all-at-once
10
+ - ๐Ÿ“ Hold **Alt** for spacing distances from a box to its container
11
+ - ๐Ÿ“‹ Press **C** to copy a box's handle (name ยท selector ยท tag) for handing to an AI agent
12
+ - โšก Zero dependencies. React optional.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ npm i -D @dev-lines/core
18
+ ```
19
+
20
+ ## Usage (vanilla)
21
+
22
+ ```js
23
+ import { createDevLines } from "@dev-lines/core";
24
+
25
+ const dl = createDevLines({ contentWidth: 1280, paddingX: 24 });
26
+ dl.enable();
27
+ ```
28
+
29
+ Toggle anytime with **โŒ˜/Ctrl + Shift + L**. Once enabled:
30
+
31
+ | Key | Action |
32
+ |-----|--------|
33
+ | `O` | toggle outlines |
34
+ | `G` | toggle guides |
35
+ | `L` | cycle labels (off โ†’ hover โ†’ all) |
36
+ | `C` | copy hovered box's handle |
37
+ | `Alt` (hold) | show spacing distances |
38
+ | `Esc` | disable |
39
+
40
+ ## Usage (React)
41
+
42
+ ```tsx
43
+ import { DevLines } from "@dev-lines/core/react";
44
+
45
+ export default function App() {
46
+ return (
47
+ <>
48
+ {process.env.NODE_ENV === "development" && <DevLines contentWidth={1280} paddingX={24} />}
49
+ {/* your app */}
50
+ </>
51
+ );
52
+ }
53
+ ```
54
+
55
+ The overlay mounts for the lifetime of the component and tears down completely on unmount.
56
+
57
+ ## Options
58
+
59
+ All optional. Colors are space-separated RGB triplets (e.g. `"245 104 104"`).
60
+
61
+ | Option | Default | Description |
62
+ |--------|---------|-------------|
63
+ | `contentWidth` | โ€” | Draw content-width edges at center ยฑ width/2 |
64
+ | `paddingX` | โ€” | Draw padding edges this many px from each side |
65
+ | `lineColor` | `"245 104 104"` | Center/width guide color (coral) |
66
+ | `paddingColor` | `"167 125 255"` | Padding guide color (purple) |
67
+ | `measureColor` | `"237 139 0"` | Alt-distance color (orange) |
68
+ | `depthColors` | 12-color palette | Outline colors per nesting depth |
69
+ | `sections` | `true` | Start with outlines on |
70
+ | `sectionSelector` | โ€” | Outline only elements matching this selector |
71
+ | `labels` | `{ mode: "hover", show: ["name","tag","size","display"] }` | Label config |
72
+ | `nameAttr` | `"data-devlines-name"` | Attribute read for a box's explicit name |
73
+ | `autoName` | `true` | Derive names from id / aria-label / component / heading |
74
+ | `copyKey` | `"c"` | Key to copy a box handle (`null` to disable) |
75
+ | `shortcut` | `"mod+shift+l"` | Toggle shortcut (`null` to disable) |
76
+ | `labelKey` | `"l"` | Cycle-labels key (`null` to disable) |
77
+ | `persist` | `true` | Remember on/off across reloads via localStorage |
78
+ | `start` | `false` | Enable immediately on create |
79
+
80
+ ## Controller API
81
+
82
+ `createDevLines(options)` returns:
83
+
84
+ ```ts
85
+ interface DevLinesController {
86
+ enable(): void;
87
+ disable(): void;
88
+ toggle(): void;
89
+ isEnabled(): boolean;
90
+ cycleLabels(mode?: "off" | "hover" | "all"): void;
91
+ toggleOutlines(on?: boolean): void;
92
+ toggleGuides(on?: boolean): void;
93
+ update(options): void; // live-update colors / contentWidth / paddingX and redraw
94
+ copy(): void; // copy hovered box's handle
95
+ getState(): { enabled, outlines, guides, labels };
96
+ refresh(): void;
97
+ destroy(): void;
98
+ }
99
+ ```
100
+
101
+ ## Naming boxes
102
+
103
+ Give any element an explicit label with `data-devlines-name="Hero"`, or force an outline on a non-flex/grid element with `data-devlines="outline"`. Mark elements to skip with `data-devlines="ignore"`.
104
+
105
+ ## License
106
+
107
+ MIT ยฉ [wa-de](https://www.wa-de.org)
@@ -0,0 +1,492 @@
1
+ // src/index.ts
2
+ var STORAGE_KEY = "dev-lines:enabled";
3
+ var Z = 2147483600;
4
+ var DEFAULT_DEPTH_COLORS = [
5
+ "29 29 29",
6
+ // #1d1d1d neutral (graphite)
7
+ "155 164 183",
8
+ // #9ba4b7 neutral (gray)
9
+ "167 125 255",
10
+ // #a77dff purple
11
+ "245 104 104",
12
+ // #f56868 coral
13
+ "237 139 0",
14
+ // #ed8b00 orange
15
+ "241 223 56",
16
+ // #f1df38 yellow
17
+ "138 224 108"
18
+ // #8ae06c green
19
+ ];
20
+ var DEFAULTS = {
21
+ lineColor: "245 104 104",
22
+ // #f56868 coral
23
+ paddingColor: "167 125 255",
24
+ // #a77dff purple
25
+ measureColor: "237 139 0",
26
+ // #ed8b00 orange
27
+ sections: true,
28
+ shortcut: "mod+shift+l",
29
+ labelKey: "l",
30
+ copyKey: "c",
31
+ nameAttr: "data-devlines-name",
32
+ autoName: true,
33
+ persist: true,
34
+ start: false
35
+ };
36
+ var DEFAULT_LABELS = {
37
+ mode: "hover",
38
+ show: ["name", "tag", "size", "display"],
39
+ guides: true
40
+ };
41
+ var isBrowser = typeof window !== "undefined" && typeof document !== "undefined";
42
+ function createDevLines(options = {}) {
43
+ if (!isBrowser) return noopController();
44
+ const opts = { ...DEFAULTS, ...options };
45
+ let depthColors = options.depthColors ?? DEFAULT_DEPTH_COLORS;
46
+ const labels = { ...DEFAULT_LABELS, ...options.labels };
47
+ let enabled = false;
48
+ let root = null;
49
+ let sectionsLayer = null;
50
+ let guidesLayer = null;
51
+ let labelsLayer = null;
52
+ let measureLayer = null;
53
+ let scheduled = false;
54
+ let ro = null;
55
+ let mo = null;
56
+ let boxes = [];
57
+ let pointer = null;
58
+ let hoverBox = null;
59
+ let lastHoverBox = null;
60
+ let outlinesOn = opts.sections;
61
+ let guidesOn = true;
62
+ let altDown = false;
63
+ const rgba = (t, a) => `rgb(${t} / ${a})`;
64
+ const colorForDepth = (d) => depthColors[Math.min(d, depthColors.length - 1)];
65
+ function line(css) {
66
+ const el = document.createElement("div");
67
+ Object.assign(el.style, { position: "absolute", ...css });
68
+ return el;
69
+ }
70
+ function chip(text, colorTriplet, left, top) {
71
+ const el = document.createElement("div");
72
+ el.textContent = text;
73
+ Object.assign(el.style, {
74
+ position: "absolute",
75
+ left: `${Math.max(0, left)}px`,
76
+ top: `${Math.max(0, top)}px`,
77
+ font: '500 10px/1.4 "Geist Mono", ui-monospace, monospace',
78
+ letterSpacing: "0.01em",
79
+ color: "#fff",
80
+ background: rgba(colorTriplet, 0.92),
81
+ padding: "1px 5px",
82
+ borderRadius: "4px",
83
+ whiteSpace: "nowrap",
84
+ pointerEvents: "none",
85
+ transform: "translateZ(0)"
86
+ });
87
+ return el;
88
+ }
89
+ function buildGuides() {
90
+ const layer = document.createElement("div");
91
+ Object.assign(layer.style, { position: "absolute", inset: "0" });
92
+ const c = rgba(opts.lineColor, 0.75);
93
+ layer.appendChild(line({ top: "0", bottom: "0", left: "50%", width: "1px", transform: "translateX(-50%)", background: c }));
94
+ layer.appendChild(line({ left: "0", right: "0", top: "50%", height: "1px", transform: "translateY(-50%)", background: c }));
95
+ if (opts.contentWidth && opts.contentWidth > 0) {
96
+ const half = opts.contentWidth / 2;
97
+ layer.appendChild(line({ top: "0", bottom: "0", left: "50%", width: "1px", transform: `translateX(${-half}px)`, background: c }));
98
+ layer.appendChild(line({ top: "0", bottom: "0", left: "50%", width: "1px", transform: `translateX(${half}px)`, background: c }));
99
+ }
100
+ if (opts.paddingX && opts.paddingX > 0) {
101
+ const p = rgba(opts.paddingColor, 0.75);
102
+ layer.appendChild(line({ top: "0", bottom: "0", left: `${opts.paddingX}px`, width: "1px", background: p }));
103
+ layer.appendChild(line({ top: "0", bottom: "0", right: `${opts.paddingX}px`, width: "1px", background: p }));
104
+ }
105
+ return layer;
106
+ }
107
+ function isContainer(el) {
108
+ if (el.getAttribute("data-devlines") === "outline") return true;
109
+ const display = getComputedStyle(el).display;
110
+ return display.includes("flex") || display.includes("grid");
111
+ }
112
+ function collect() {
113
+ const out = [];
114
+ const walk = (el, depth) => {
115
+ if (el === root || el.getAttribute("data-devlines") === "ignore") return;
116
+ const hit = isContainer(el);
117
+ if (hit) out.push({ el, depth });
118
+ const next = hit ? depth + 1 : depth;
119
+ for (const child of el.children) walk(child, next);
120
+ };
121
+ if (opts.sectionSelector) {
122
+ document.querySelectorAll(opts.sectionSelector).forEach((el) => out.push({ el, depth: 0 }));
123
+ } else {
124
+ for (const child of document.body.children) walk(child, 0);
125
+ }
126
+ return out;
127
+ }
128
+ function drawSections() {
129
+ if (!sectionsLayer) return;
130
+ sectionsLayer.replaceChildren();
131
+ boxes = [];
132
+ if (outlinesOn) {
133
+ for (const { el, depth } of collect()) {
134
+ const rect = el.getBoundingClientRect();
135
+ if (rect.width === 0 || rect.height === 0) continue;
136
+ boxes.push({ el, depth, rect });
137
+ const color = colorForDepth(depth);
138
+ const box = document.createElement("div");
139
+ Object.assign(box.style, {
140
+ position: "absolute",
141
+ left: `${rect.left}px`,
142
+ top: `${rect.top}px`,
143
+ width: `${rect.width}px`,
144
+ height: `${rect.height}px`,
145
+ boxShadow: `inset 0 0 0 1px ${rgba(color, 0.6)}`
146
+ });
147
+ sectionsLayer.appendChild(box);
148
+ }
149
+ }
150
+ hoverBox = pointer ? boxAt(pointer.x, pointer.y) : null;
151
+ drawLabels();
152
+ drawMeasure();
153
+ }
154
+ function nameOf(el) {
155
+ const explicit = el.getAttribute(opts.nameAttr);
156
+ if (explicit) return explicit;
157
+ if (!opts.autoName) return null;
158
+ if (el.id) return el.id;
159
+ const aria = el.getAttribute("aria-label");
160
+ if (aria) return aria;
161
+ const comp = el.getAttribute("data-component") || el.getAttribute("data-testid");
162
+ if (comp) return comp;
163
+ const h = el.querySelector("h1,h2,h3,h4,h5,h6");
164
+ const t = h?.textContent?.trim();
165
+ if (t) return t.length > 24 ? t.slice(0, 24) + "\u2026" : t;
166
+ return null;
167
+ }
168
+ function selectorFor(el) {
169
+ if (el.id) return `#${CSS.escape(el.id)}`;
170
+ const parts = [];
171
+ let node = el;
172
+ while (node && node !== document.body) {
173
+ const cur = node;
174
+ if (cur.id) {
175
+ parts.unshift(`#${CSS.escape(cur.id)}`);
176
+ break;
177
+ }
178
+ let sel = cur.tagName.toLowerCase();
179
+ const parent = cur.parentElement;
180
+ if (parent) {
181
+ const sameTag = Array.from(parent.children).filter((c) => c.tagName === cur.tagName);
182
+ if (sameTag.length > 1) sel += `:nth-of-type(${sameTag.indexOf(cur) + 1})`;
183
+ }
184
+ parts.unshift(sel);
185
+ node = parent;
186
+ }
187
+ return parts.join(" > ") || el.tagName.toLowerCase();
188
+ }
189
+ function labelText(el, depth, rect) {
190
+ const parts = [];
191
+ for (const f of labels.show) {
192
+ if (f === "name") {
193
+ const nm = nameOf(el);
194
+ if (nm) parts.push(nm);
195
+ } else if (f === "tag") parts.push(`<${el.tagName.toLowerCase()}>`);
196
+ else if (f === "size") parts.push(`${Math.round(rect.width)}\xD7${Math.round(rect.height)}`);
197
+ else if (f === "display") {
198
+ const d = getComputedStyle(el).display;
199
+ parts.push(d.includes("grid") ? "grid" : d.includes("flex") ? "flex" : d);
200
+ } else if (f === "depth") parts.push(`d${depth}`);
201
+ }
202
+ return parts.join(" \xB7 ");
203
+ }
204
+ function boxAt(x, y) {
205
+ let best = null;
206
+ for (const b of boxes) {
207
+ const r = b.rect;
208
+ if (x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) {
209
+ if (!best || b.depth > best.depth) best = b;
210
+ }
211
+ }
212
+ return best;
213
+ }
214
+ function ancestorOf(target) {
215
+ let best = null;
216
+ for (const b of boxes) {
217
+ if (b === target || b.depth >= target.depth) continue;
218
+ const r = b.rect, t = target.rect;
219
+ if (r.left <= t.left && r.top <= t.top && r.right >= t.right && r.bottom >= t.bottom) {
220
+ if (!best || b.depth > best.depth) best = b;
221
+ }
222
+ }
223
+ return best;
224
+ }
225
+ function drawLabels() {
226
+ if (!labelsLayer) return;
227
+ labelsLayer.replaceChildren();
228
+ if (labels.guides && guidesOn) {
229
+ const cx = window.innerWidth / 2;
230
+ labelsLayer.appendChild(chip(`${Math.round(cx)}`, opts.lineColor, cx + 4, 4));
231
+ if (opts.contentWidth && opts.contentWidth > 0) {
232
+ const half = opts.contentWidth / 2;
233
+ labelsLayer.appendChild(chip(`\u2212${Math.round(half)}`, opts.lineColor, cx - half + 4, 22));
234
+ labelsLayer.appendChild(chip(`+${Math.round(half)}`, opts.lineColor, cx + half + 4, 22));
235
+ }
236
+ if (opts.paddingX && opts.paddingX > 0) {
237
+ labelsLayer.appendChild(chip(`${Math.round(opts.paddingX)}`, opts.paddingColor, opts.paddingX + 4, 4));
238
+ labelsLayer.appendChild(chip(`${Math.round(opts.paddingX)}`, opts.paddingColor, window.innerWidth - opts.paddingX + 4, 4));
239
+ }
240
+ }
241
+ if (!outlinesOn) return;
242
+ if (labels.mode === "all") {
243
+ for (const b of boxes) labelsLayer.appendChild(chip(labelText(b.el, b.depth, b.rect), colorForDepth(b.depth), b.rect.left, b.rect.top));
244
+ } else if (labels.mode === "hover" && hoverBox) {
245
+ labelsLayer.appendChild(chip(labelText(hoverBox.el, hoverBox.depth, hoverBox.rect), colorForDepth(hoverBox.depth), hoverBox.rect.left, hoverBox.rect.top));
246
+ }
247
+ }
248
+ function measureSeg(x1, y1, x2, y2, value) {
249
+ if (!measureLayer || value < 1) return;
250
+ const horizontal = y1 === y2;
251
+ measureLayer.appendChild(line({
252
+ left: `${Math.min(x1, x2)}px`,
253
+ top: `${Math.min(y1, y2)}px`,
254
+ width: horizontal ? `${Math.abs(x2 - x1)}px` : "1px",
255
+ height: horizontal ? "1px" : `${Math.abs(y2 - y1)}px`,
256
+ background: rgba(opts.measureColor, 0.9)
257
+ }));
258
+ const mx = horizontal ? (x1 + x2) / 2 : x1 + 3;
259
+ const my = horizontal ? y1 + 3 : (y1 + y2) / 2;
260
+ measureLayer.appendChild(chip(`${Math.round(value)}`, opts.measureColor, mx, my));
261
+ }
262
+ function drawMeasure() {
263
+ if (!measureLayer) return;
264
+ measureLayer.replaceChildren();
265
+ if (!altDown || !hoverBox || !outlinesOn) return;
266
+ const t = hoverBox.rect;
267
+ const anc = ancestorOf(hoverBox);
268
+ const c = anc ? anc.rect : new DOMRect(0, 0, window.innerWidth, window.innerHeight);
269
+ const midX = (t.left + t.right) / 2;
270
+ const midY = (t.top + t.bottom) / 2;
271
+ measureSeg(midX, c.top, midX, t.top, t.top - c.top);
272
+ measureSeg(midX, t.bottom, midX, c.bottom, c.bottom - t.bottom);
273
+ measureSeg(c.left, midY, t.left, midY, t.left - c.left);
274
+ measureSeg(t.right, midY, c.right, midY, c.right - t.right);
275
+ }
276
+ function flashCopied(rect) {
277
+ if (!root) return;
278
+ const el = chip("copied \u2713", "34 197 94", rect.left, rect.top - 2);
279
+ root.appendChild(el);
280
+ setTimeout(() => el.remove(), 900);
281
+ }
282
+ function copyHandle() {
283
+ const target = hoverBox ?? lastHoverBox;
284
+ if (!target) return;
285
+ const el = target.el;
286
+ const name = nameOf(el);
287
+ const text = `${name ? `"${name}" \u2014 ` : ""}${selectorFor(el)} (<${el.tagName.toLowerCase()}>)`;
288
+ navigator.clipboard?.writeText(text).then(() => flashCopied(target.rect)).catch(() => {
289
+ });
290
+ }
291
+ function schedule() {
292
+ if (scheduled) return;
293
+ scheduled = true;
294
+ requestAnimationFrame(() => {
295
+ scheduled = false;
296
+ drawSections();
297
+ });
298
+ }
299
+ function onPointerMove(e) {
300
+ pointer = { x: e.clientX, y: e.clientY };
301
+ hoverBox = boxAt(e.clientX, e.clientY);
302
+ if (hoverBox) lastHoverBox = hoverBox;
303
+ drawLabels();
304
+ drawMeasure();
305
+ }
306
+ function enable() {
307
+ if (enabled) return;
308
+ enabled = true;
309
+ root = document.createElement("div");
310
+ root.setAttribute("data-devlines", "ignore");
311
+ Object.assign(root.style, { position: "fixed", inset: "0", zIndex: String(Z), pointerEvents: "none" });
312
+ sectionsLayer = document.createElement("div");
313
+ guidesLayer = buildGuides();
314
+ labelsLayer = document.createElement("div");
315
+ measureLayer = document.createElement("div");
316
+ for (const l of [sectionsLayer, labelsLayer, measureLayer]) Object.assign(l.style, { position: "absolute", inset: "0" });
317
+ guidesLayer.style.display = guidesOn ? "" : "none";
318
+ root.append(sectionsLayer, guidesLayer, labelsLayer, measureLayer);
319
+ document.body.appendChild(root);
320
+ window.addEventListener("scroll", schedule, true);
321
+ window.addEventListener("resize", schedule);
322
+ window.addEventListener("pointermove", onPointerMove, true);
323
+ ro = new ResizeObserver(schedule);
324
+ ro.observe(document.documentElement);
325
+ mo = new MutationObserver(schedule);
326
+ mo.observe(document.body, { childList: true, subtree: true, attributes: true });
327
+ drawSections();
328
+ if (opts.persist) try {
329
+ localStorage.setItem(STORAGE_KEY, "1");
330
+ } catch {
331
+ }
332
+ }
333
+ function disable() {
334
+ if (!enabled) return;
335
+ enabled = false;
336
+ altDown = false;
337
+ window.removeEventListener("scroll", schedule, true);
338
+ window.removeEventListener("resize", schedule);
339
+ window.removeEventListener("pointermove", onPointerMove, true);
340
+ ro?.disconnect();
341
+ mo?.disconnect();
342
+ ro = mo = null;
343
+ pointer = hoverBox = lastHoverBox = null;
344
+ root?.remove();
345
+ root = sectionsLayer = guidesLayer = labelsLayer = measureLayer = null;
346
+ boxes = [];
347
+ if (opts.persist) try {
348
+ localStorage.setItem(STORAGE_KEY, "0");
349
+ } catch {
350
+ }
351
+ }
352
+ function toggle() {
353
+ enabled ? disable() : enable();
354
+ }
355
+ function cycleLabels(mode) {
356
+ const order = ["off", "hover", "all"];
357
+ labels.mode = mode ?? order[(order.indexOf(labels.mode) + 1) % order.length];
358
+ drawLabels();
359
+ }
360
+ function toggleOutlines(on) {
361
+ outlinesOn = on ?? !outlinesOn;
362
+ drawSections();
363
+ }
364
+ function toggleGuides(on) {
365
+ guidesOn = on ?? !guidesOn;
366
+ if (guidesLayer) guidesLayer.style.display = guidesOn ? "" : "none";
367
+ drawLabels();
368
+ }
369
+ function update(next) {
370
+ if (next.lineColor !== void 0) opts.lineColor = next.lineColor;
371
+ if (next.paddingColor !== void 0) opts.paddingColor = next.paddingColor;
372
+ if (next.measureColor !== void 0) opts.measureColor = next.measureColor;
373
+ if (next.depthColors !== void 0) depthColors = next.depthColors;
374
+ if (next.contentWidth !== void 0) opts.contentWidth = next.contentWidth;
375
+ if (next.paddingX !== void 0) opts.paddingX = next.paddingX;
376
+ if (root && guidesLayer) {
377
+ const fresh = buildGuides();
378
+ fresh.style.display = guidesOn ? "" : "none";
379
+ root.replaceChild(fresh, guidesLayer);
380
+ guidesLayer = fresh;
381
+ }
382
+ drawSections();
383
+ }
384
+ function typingInField() {
385
+ const t = document.activeElement;
386
+ return !!t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA" || t.isContentEditable);
387
+ }
388
+ function matchesShortcut(e, combo) {
389
+ const parts = combo.toLowerCase().split("+");
390
+ const key = parts[parts.length - 1];
391
+ const mod = e.metaKey || e.ctrlKey;
392
+ return e.key.toLowerCase() === key && parts.includes("mod") === mod && parts.includes("shift") === e.shiftKey && parts.includes("alt") === e.altKey;
393
+ }
394
+ function onKey(e) {
395
+ if (typingInField()) return;
396
+ if (opts.shortcut && matchesShortcut(e, opts.shortcut)) {
397
+ e.preventDefault();
398
+ toggle();
399
+ return;
400
+ }
401
+ if (!enabled) return;
402
+ if (e.key === "Alt" && !altDown) {
403
+ altDown = true;
404
+ drawMeasure();
405
+ return;
406
+ }
407
+ if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return;
408
+ const k = e.key.toLowerCase();
409
+ if (k === "escape") {
410
+ e.preventDefault();
411
+ disable();
412
+ } else if (k === "o") {
413
+ e.preventDefault();
414
+ toggleOutlines();
415
+ } else if (k === "g") {
416
+ e.preventDefault();
417
+ toggleGuides();
418
+ } else if (opts.labelKey && k === opts.labelKey.toLowerCase()) {
419
+ e.preventDefault();
420
+ cycleLabels();
421
+ } else if (opts.copyKey && k === opts.copyKey.toLowerCase()) {
422
+ e.preventDefault();
423
+ copyHandle();
424
+ }
425
+ }
426
+ function onKeyUp(e) {
427
+ if (e.key === "Alt" && altDown) {
428
+ altDown = false;
429
+ drawMeasure();
430
+ }
431
+ }
432
+ window.addEventListener("keydown", onKey);
433
+ window.addEventListener("keyup", onKeyUp);
434
+ let startOn = opts.start;
435
+ if (opts.persist) {
436
+ try {
437
+ const saved = localStorage.getItem(STORAGE_KEY);
438
+ if (saved === "1") startOn = true;
439
+ else if (saved === "0") startOn = false;
440
+ } catch {
441
+ }
442
+ }
443
+ if (startOn) enable();
444
+ return {
445
+ enable,
446
+ disable,
447
+ toggle,
448
+ isEnabled: () => enabled,
449
+ cycleLabels,
450
+ toggleOutlines,
451
+ toggleGuides,
452
+ update,
453
+ copy: copyHandle,
454
+ getState: () => ({ enabled, outlines: outlinesOn, guides: guidesOn, labels: labels.mode }),
455
+ refresh: drawSections,
456
+ destroy() {
457
+ disable();
458
+ window.removeEventListener("keydown", onKey);
459
+ window.removeEventListener("keyup", onKeyUp);
460
+ }
461
+ };
462
+ }
463
+ function noopController() {
464
+ return {
465
+ enable() {
466
+ },
467
+ disable() {
468
+ },
469
+ toggle() {
470
+ },
471
+ isEnabled: () => false,
472
+ cycleLabels() {
473
+ },
474
+ toggleOutlines() {
475
+ },
476
+ toggleGuides() {
477
+ },
478
+ update() {
479
+ },
480
+ copy() {
481
+ },
482
+ getState: () => ({ enabled: false, outlines: false, guides: false, labels: "off" }),
483
+ refresh() {
484
+ },
485
+ destroy() {
486
+ }
487
+ };
488
+ }
489
+
490
+ export {
491
+ createDevLines
492
+ };
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @dev-lines/core โ€” a framework-agnostic layout debug overlay.
3
+ *
4
+ * Draws, on top of any page, without mutating it:
5
+ * - a viewport center cross (vertical + horizontal)
6
+ * - optional content-width edges (center ยฑ contentWidth/2)
7
+ * - optional padding edges (paddingX in from each side)
8
+ * - depth-colored outlines around layout containers (flex/grid)
9
+ * - labels: per-box chips (tag ยท size ยท layout ยท custom name) and guide readouts
10
+ * - hold Alt: spacing overlay from the hovered box to its containing box
11
+ *
12
+ * Everything lives in one fixed overlay layer with pointer-events: none, so the
13
+ * host app is never touched and teardown is total.
14
+ *
15
+ * Commands (when enabled): O toggle outlines ยท G toggle guides ยท L cycle labels ยท
16
+ * Esc disable ยท hold Alt for distances. Toggle the overlay with mod+shift+L.
17
+ */
18
+ type LabelField = "name" | "tag" | "size" | "display" | "depth";
19
+ type LabelMode = "off" | "hover" | "all";
20
+ interface LabelOptions {
21
+ mode?: LabelMode;
22
+ show?: LabelField[];
23
+ guides?: boolean;
24
+ }
25
+ interface DevLinesOptions {
26
+ contentWidth?: number;
27
+ paddingX?: number;
28
+ lineColor?: string;
29
+ paddingColor?: string;
30
+ measureColor?: string;
31
+ depthColors?: string[];
32
+ sections?: boolean;
33
+ sectionSelector?: string;
34
+ labels?: LabelOptions;
35
+ /** Attribute read for a box's explicit name. Default "data-devlines-name". */
36
+ nameAttr?: string;
37
+ /** Derive a name (id โ†’ aria-label โ†’ data-component/testid โ†’ heading) when none is set. Default true. */
38
+ autoName?: boolean;
39
+ /** Key to copy the hovered box's handle (name ยท selector ยท tag) for handing to an agent. Default "c". */
40
+ copyKey?: string | null;
41
+ shortcut?: string | null;
42
+ labelKey?: string | null;
43
+ persist?: boolean;
44
+ start?: boolean;
45
+ }
46
+ interface DevLinesController {
47
+ enable(): void;
48
+ disable(): void;
49
+ toggle(): void;
50
+ isEnabled(): boolean;
51
+ cycleLabels(mode?: LabelMode): void;
52
+ toggleOutlines(on?: boolean): void;
53
+ toggleGuides(on?: boolean): void;
54
+ /** Live-update visual options (colors, contentWidth, paddingX) and redraw. */
55
+ update(options: Partial<DevLinesOptions>): void;
56
+ /** Copy the hovered (or last-hovered) box's handle for agent handoff. */
57
+ copy(): void;
58
+ getState(): {
59
+ enabled: boolean;
60
+ outlines: boolean;
61
+ guides: boolean;
62
+ labels: LabelMode;
63
+ };
64
+ refresh(): void;
65
+ destroy(): void;
66
+ }
67
+ declare function createDevLines(options?: DevLinesOptions): DevLinesController;
68
+
69
+ export { type DevLinesController, type DevLinesOptions, type LabelField, type LabelMode, type LabelOptions, createDevLines };
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ import {
2
+ createDevLines
3
+ } from "./chunk-MUK3OXWT.js";
4
+ export {
5
+ createDevLines
6
+ };
@@ -0,0 +1,13 @@
1
+ import { DevLinesOptions } from './index.js';
2
+
3
+ interface DevLinesProps extends DevLinesOptions {
4
+ }
5
+ /**
6
+ * Mounts the dev-lines overlay for the lifetime of this component.
7
+ * Render it once, gated behind a dev check:
8
+ *
9
+ * {process.env.NODE_ENV === "development" && <DevLines contentWidth={416} />}
10
+ */
11
+ declare function DevLines(props?: DevLinesProps): null;
12
+
13
+ export { DevLines, type DevLinesProps, DevLines as default };
package/dist/react.js ADDED
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import {
3
+ createDevLines
4
+ } from "./chunk-MUK3OXWT.js";
5
+
6
+ // src/react.tsx
7
+ import { useEffect } from "react";
8
+ function DevLines(props = {}) {
9
+ const key = JSON.stringify(props);
10
+ useEffect(() => {
11
+ const controller = createDevLines(props);
12
+ return () => controller.destroy();
13
+ }, [key]);
14
+ return null;
15
+ }
16
+ var react_default = DevLines;
17
+ export {
18
+ DevLines,
19
+ react_default as default
20
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@dev-lines/core",
3
+ "version": "0.1.0",
4
+ "description": "Framework-agnostic layout debug overlay: center/width/padding guides + depth-colored section outlines, labels, and distances. Zero dependencies.",
5
+ "license": "MIT",
6
+ "author": "wa-de (https://www.wa-de.org)",
7
+ "homepage": "https://lines.wiki",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/wwwwaaaaddddeeee/dev-lines.git",
11
+ "directory": "packages/core"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/wwwwaaaaddddeeee/dev-lines/issues"
15
+ },
16
+ "keywords": [
17
+ "debug",
18
+ "layout",
19
+ "overlay",
20
+ "guides",
21
+ "grid",
22
+ "ruler",
23
+ "devtools",
24
+ "css",
25
+ "outline",
26
+ "react"
27
+ ],
28
+ "type": "module",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "main": "./dist/index.js",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ },
40
+ "./react": {
41
+ "types": "./dist/react.d.ts",
42
+ "import": "./dist/react.js"
43
+ }
44
+ },
45
+ "files": ["dist"],
46
+ "sideEffects": false,
47
+ "scripts": {
48
+ "build": "tsup src/index.ts src/react.tsx --format esm --dts --clean",
49
+ "dev": "tsup src/index.ts src/react.tsx --format esm --dts --watch"
50
+ },
51
+ "peerDependencies": {
52
+ "react": ">=18"
53
+ },
54
+ "peerDependenciesMeta": {
55
+ "react": {
56
+ "optional": true
57
+ }
58
+ },
59
+ "devDependencies": {
60
+ "@types/react": "^19.0.0",
61
+ "react": "^19.0.0",
62
+ "tsup": "^8.3.0",
63
+ "typescript": "^5.7.0"
64
+ }
65
+ }