spark-html-devtools 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/README.md ADDED
@@ -0,0 +1,34 @@
1
+ # ⚡ spark-html-devtools
2
+
3
+ A tiny in-page inspector for [spark-html](https://github.com/wilkinnovo/spark)
4
+ apps. Drop it in during development and get a ⚡ panel (bottom-right) showing:
5
+
6
+ - **Stores** — every named store and its live state.
7
+ - **Components** — the component tree (`[name]` hosts) with each component's
8
+ reactive state.
9
+ - **Patches** — a counter, plus a brief amber outline on whichever component just
10
+ re-rendered, so Spark's surgical reactivity is *visible*.
11
+
12
+ It's read-only — it never mutates your app.
13
+
14
+ ```js
15
+ import { devtools } from 'spark-html-devtools';
16
+
17
+ if (import.meta.env?.DEV) devtools(); // dev only
18
+ ```
19
+
20
+ ## Install
21
+
22
+ ```bash
23
+ npm install -D spark-html-devtools
24
+ ```
25
+
26
+ ## API
27
+
28
+ ```js
29
+ const stop = devtools({ open: true }); // start expanded
30
+ // …
31
+ stop(); // remove the panel + restore hooks
32
+ ```
33
+
34
+ Requires `spark-html` ≥ 0.21.4 (uses its `inspectStores()` introspection).
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "spark-html-devtools",
3
+ "version": "0.1.0",
4
+ "description": "A tiny in-page devtools panel for spark-html — live store state, component tree, and patch activity.",
5
+ "type": "module",
6
+ "main": "./src/index.js",
7
+ "types": "./src/index.d.ts",
8
+ "homepage": "https://wilkinnovo.github.io/spark",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./src/index.d.ts",
12
+ "default": "./src/index.js"
13
+ }
14
+ },
15
+ "scripts": {
16
+ "test": "node test/devtools.js"
17
+ },
18
+ "files": [
19
+ "src"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/wilkinnovo/spark.git",
24
+ "directory": "packages/spark-html-devtools"
25
+ },
26
+ "dependencies": {
27
+ "spark-html": "^0.21.4"
28
+ },
29
+ "keywords": [
30
+ "spark-html",
31
+ "devtools",
32
+ "debug",
33
+ "inspector"
34
+ ],
35
+ "license": "MIT"
36
+ }
package/src/index.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * spark-html-devtools — in-page inspector panel for spark-html apps.
3
+ */
4
+
5
+ export interface DevtoolsOptions {
6
+ /** Start with the panel expanded (default false). */
7
+ open?: boolean;
8
+ }
9
+
10
+ /**
11
+ * Mount the devtools panel (a ⚡ button bottom-right). Shows live store state,
12
+ * the component tree with each component's reactive state, a patch counter, and
13
+ * flashes whichever component just re-rendered. Read-only. Returns a teardown
14
+ * function. Intended for development — gate it behind `import.meta.env.DEV`.
15
+ */
16
+ export function devtools(options?: DevtoolsOptions): () => void;
17
+
18
+ declare const _default: { devtools: typeof devtools };
19
+ export default _default;
package/src/index.js ADDED
@@ -0,0 +1,133 @@
1
+ /**
2
+ * spark-html-devtools — a tiny in-page panel for inspecting a Spark app.
3
+ *
4
+ * import { devtools } from 'spark-html-devtools';
5
+ * if (import.meta.env?.DEV) devtools(); // dev only
6
+ *
7
+ * Shows, live:
8
+ * • every named store + its state,
9
+ * • the component tree ([name] hosts) + each component's reactive state,
10
+ * • a patch counter, and a brief amber outline on whichever component just
11
+ * re-rendered — so "surgical reactivity" is visible.
12
+ *
13
+ * Reads from the DOM (component scopes) and `inspectStores()`; it never mutates
14
+ * the app. Toggle with the ⚡ button (bottom-right).
15
+ */
16
+ import { inspectStores } from 'spark-html';
17
+
18
+ const AMBER = '#ffd24a';
19
+ let booted = false;
20
+
21
+ function safe(value) {
22
+ const seen = new WeakSet();
23
+ try {
24
+ return JSON.stringify(value, (_k, v) => {
25
+ if (typeof v === 'function') return undefined;
26
+ if (v && typeof v === 'object') { if (seen.has(v)) return '[circular]'; seen.add(v); }
27
+ return v;
28
+ }, 1);
29
+ } catch { return String(value); }
30
+ }
31
+
32
+ function esc(s) {
33
+ return String(s).replace(/[&<>]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;' }[c]));
34
+ }
35
+
36
+ function readScope(scope) {
37
+ const o = {};
38
+ try {
39
+ for (const k of Object.keys(scope)) {
40
+ try { const v = scope[k]; if (typeof v !== 'function') o[k] = v; } catch { /* getter threw */ }
41
+ }
42
+ } catch { /* not enumerable */ }
43
+ return o;
44
+ }
45
+
46
+ function components() {
47
+ return [...document.querySelectorAll('[name]')]
48
+ .filter((el) => el.__sparkScope)
49
+ .map((el) => ({ name: el.getAttribute('name'), state: readScope(el.__sparkScope), el }));
50
+ }
51
+
52
+ /**
53
+ * Mount the devtools panel. Returns a teardown function.
54
+ * @param {object} [options]
55
+ * @param {boolean} [options.open=false] Start expanded.
56
+ */
57
+ export function devtools(options = {}) {
58
+ if (booted || typeof document === 'undefined') return () => {};
59
+ booted = true;
60
+
61
+ let patches = 0;
62
+ let open = !!options.open;
63
+
64
+ const root = document.createElement('div');
65
+ root.setAttribute('data-spark-devtools', '');
66
+ root.innerHTML = `
67
+ <button class="sdt-toggle" title="Spark devtools">⚡</button>
68
+ <div class="sdt-panel">
69
+ <div class="sdt-head"><b>⚡ spark devtools</b><span class="sdt-meta"></span><button class="sdt-x">×</button></div>
70
+ <div class="sdt-body"></div>
71
+ </div>`;
72
+
73
+ const style = document.createElement('style');
74
+ style.textContent = `
75
+ [data-spark-devtools]{position:fixed;right:14px;bottom:14px;z-index:2147483647;font:12px/1.5 "JetBrains Mono",ui-monospace,monospace}
76
+ [data-spark-devtools] .sdt-toggle{width:40px;height:40px;border-radius:50%;border:1px solid #333;background:#000;color:${AMBER};font-size:20px;cursor:pointer}
77
+ [data-spark-devtools] .sdt-panel{display:none;position:absolute;right:0;bottom:48px;width:340px;max-height:70vh;overflow:auto;background:#0a0a0a;color:#e8e8e8;border:1px solid #333}
78
+ [data-spark-devtools][data-open] .sdt-panel{display:block}
79
+ [data-spark-devtools][data-open] .sdt-toggle{outline:2px solid ${AMBER}}
80
+ [data-spark-devtools] .sdt-head{display:flex;align-items:center;gap:8px;padding:9px 12px;border-bottom:1px solid #1a1a1a;position:sticky;top:0;background:#0a0a0a}
81
+ [data-spark-devtools] .sdt-head b{color:#fff}
82
+ [data-spark-devtools] .sdt-meta{margin-left:auto;color:#888}
83
+ [data-spark-devtools] .sdt-x{background:none;border:none;color:#888;font-size:16px;cursor:pointer}
84
+ [data-spark-devtools] .sdt-body{padding:10px 12px}
85
+ [data-spark-devtools] .sdt-sec{font-size:10px;text-transform:uppercase;letter-spacing:.1em;color:#666;margin:12px 0 6px}
86
+ [data-spark-devtools] .sdt-item{border-left:2px solid #1a1a1a;padding:2px 0 2px 10px;margin:4px 0}
87
+ [data-spark-devtools] .sdt-name{color:${AMBER}}
88
+ [data-spark-devtools] pre{margin:2px 0 0;white-space:pre-wrap;color:#aaa;font-size:11px}
89
+ .sdt-flash{outline:1px solid ${AMBER} !important;outline-offset:-1px;transition:outline .15s}`;
90
+ document.head.appendChild(style);
91
+ document.body.appendChild(root);
92
+
93
+ const panelBody = root.querySelector('.sdt-body');
94
+ const meta = root.querySelector('.sdt-meta');
95
+ const setOpen = (v) => { open = v; if (v) root.setAttribute('data-open', ''); else root.removeAttribute('data-open'); if (v) render(); };
96
+ root.querySelector('.sdt-toggle').addEventListener('click', () => setOpen(!open));
97
+ root.querySelector('.sdt-x').addEventListener('click', () => setOpen(false));
98
+
99
+ function render() {
100
+ const stores = inspectStores();
101
+ const comps = components();
102
+ meta.textContent = `${comps.length} comp · ${patches} patches`;
103
+ const storeRows = Object.keys(stores).map((n) =>
104
+ `<div class="sdt-item"><span class="sdt-name">${esc(n)}</span><pre>${esc(safe(stores[n]))}</pre></div>`).join('') || '<div class="sdt-item">—</div>';
105
+ const compRows = comps.map((c) =>
106
+ `<div class="sdt-item"><span class="sdt-name">${esc(c.name)}</span><pre>${esc(safe(c.state))}</pre></div>`).join('') || '<div class="sdt-item">—</div>';
107
+ panelBody.innerHTML =
108
+ `<div class="sdt-sec">stores</div>${storeRows}<div class="sdt-sec">components</div>${compRows}`;
109
+ }
110
+
111
+ // Patch hook (the runtime calls this per component patch). Chain any existing.
112
+ const prev = globalThis.__sparkTestOnPatch;
113
+ globalThis.__sparkTestOnPatch = (el) => {
114
+ patches++;
115
+ if (typeof prev === 'function') try { prev(el); } catch { /* ignore */ }
116
+ if (el && el.classList) {
117
+ el.classList.add('sdt-flash');
118
+ setTimeout(() => el.classList && el.classList.remove('sdt-flash'), 180);
119
+ }
120
+ };
121
+
122
+ const timer = setInterval(() => { if (open) render(); }, 400);
123
+ if (open) { root.setAttribute('data-open', ''); render(); }
124
+
125
+ return function teardown() {
126
+ clearInterval(timer);
127
+ globalThis.__sparkTestOnPatch = prev;
128
+ root.remove(); style.remove();
129
+ booted = false;
130
+ };
131
+ }
132
+
133
+ export default { devtools };