semantic-inspector 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 semantic-inspector contributors
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,113 @@
1
+ # semantic-inspector
2
+
3
+ A dev-only React inspector for vibe-coding. Hit a hotkey to enter inspect mode:
4
+ hovering highlights the element under the cursor and shows its component name +
5
+ `file:line`. **Click** copies that text identifier to the clipboard;
6
+ **Shift+click** copies a PNG screenshot of just that element. Built for pasting
7
+ precise UI context into an AI chat in seconds.
8
+
9
+ Stack: Vite + `@vitejs/plugin-react` + React 18/19. Zero runtime cost in
10
+ production — you gate where it mounts.
11
+
12
+ ## Install
13
+
14
+ ```sh
15
+ npm i -D semantic-inspector
16
+ ```
17
+
18
+ `react` / `react-dom` are peer deps (>=18). `vite` is an optional peer — only
19
+ needed if you use the Vite plugin entry point.
20
+
21
+ ## How it works
22
+
23
+ Source locations come from a **build-time stamp**, not React internals. A Babel
24
+ pass adds `data-loc="<path>:<line>"` and `data-comp="<Component>"` to JSX host
25
+ elements (`div`, `section`, …). The runtime reads those DOM attributes, so it
26
+ stays robust across React versions. If a node isn't stamped (prod build, foreign
27
+ node), it degrades gracefully: fiber `displayName` → filename → tag name.
28
+
29
+ ## Three entry points
30
+
31
+ | Import | What it is |
32
+ | ------------------------------- | ---------------------------------------------------------------------- |
33
+ | `semantic-inspector` | `<SemanticInspector/>` — the overlay + hotkey + clipboard runtime. |
34
+ | `semantic-inspector/vite` | `stampLocVite()` — Vite plugin that stamps `data-loc` / `data-comp`. |
35
+ | `semantic-inspector/babel` | Raw Babel plugin, for the babel variant of `@vitejs/plugin-react`. |
36
+
37
+ ## Usage
38
+
39
+ ### 1. Stamp source locations (Vite plugin)
40
+
41
+ `@vitejs/plugin-react` **v6** transpiles via oxc (no Babel hook), so stamp with a
42
+ separate `pre` plugin:
43
+
44
+ ```ts
45
+ import react from '@vitejs/plugin-react';
46
+ import { stampLocVite } from 'semantic-inspector/vite';
47
+ import { defineConfig } from 'vite';
48
+
49
+ export default defineConfig({
50
+ // stampLocVite first (enforce: 'pre'), then react()
51
+ plugins: [stampLocVite({ rootDir: process.cwd() }), react()]
52
+ });
53
+ ```
54
+
55
+ `rootDir` is the base for the relative path written into `data-loc`.
56
+
57
+ On the **Babel variant** of plugin-react you can skip the separate pre-pass:
58
+
59
+ ```ts
60
+ import react from '@vitejs/plugin-react';
61
+ import stampLoc from 'semantic-inspector/babel';
62
+
63
+ react({ babel: { plugins: [[stampLoc, { rootDir: process.cwd() }]] } });
64
+ ```
65
+
66
+ ### 2. Mount it (behind your own dev flag, ideally lazy)
67
+
68
+ ```tsx
69
+ import { lazy, Suspense } from 'react';
70
+
71
+ const SemanticInspector = lazy(() =>
72
+ import('semantic-inspector').then((m) => ({ default: m.SemanticInspector }))
73
+ );
74
+
75
+ {
76
+ import.meta.env.DEV && (
77
+ <Suspense fallback={null}>
78
+ <SemanticInspector onCopy={(kind) => toast(`${kind} copied`)} />
79
+ </Suspense>
80
+ );
81
+ }
82
+ ```
83
+
84
+ ## Props
85
+
86
+ | prop | default | purpose |
87
+ | ------------ | ------------------------ | ---------------------------------------- |
88
+ | `hotkey` | `'Alt+Shift+S'` | toggle inspect mode (Esc exits) |
89
+ | `formatText` | `` `${comp} — ${loc}` `` | format of the text copied on click |
90
+ | `onCopy` | — | callback after a copy (telemetry/toasts) |
91
+ | `onError` | — | callback on clipboard/screenshot failure |
92
+
93
+ ## Notes
94
+
95
+ - `navigator.clipboard` requires a secure context (localhost / https) and a
96
+ user gesture — that's why copy happens on **click**, not hover.
97
+ - Screenshots use `modern-screenshot` (DOM→canvas): cross-origin `<img>` without
98
+ CORS and some exotic CSS may not render.
99
+ - On a prod React build without `data-loc`, the name falls back to the fiber
100
+ (minified) or tag — degraded mode. Full mode needs the build-time stamp.
101
+
102
+ ## Development
103
+
104
+ ```sh
105
+ npm install
106
+ npm test # vitest
107
+ npm run typecheck
108
+ npm run build # tsup -> dist/ (esm + cjs + d.ts)
109
+ ```
110
+
111
+ ## License
112
+
113
+ MIT
package/dist/babel.cjs ADDED
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ // src/stampLocBabel.ts
4
+ function isHostElement(name) {
5
+ return name.type === "JSXIdentifier" && /^[a-z]/.test(name.name);
6
+ }
7
+ function hasAttr(el, attrName) {
8
+ return el.attributes.some(
9
+ (a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === attrName
10
+ );
11
+ }
12
+ function nearestComponentName(path) {
13
+ let p = path;
14
+ while (p) {
15
+ const node = p.node;
16
+ if (node.type === "FunctionDeclaration" && node.id && /^[A-Z]/.test(node.id.name)) {
17
+ return node.id.name;
18
+ }
19
+ if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
20
+ const parent = p.parentPath?.node;
21
+ if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier" && /^[A-Z]/.test(parent.id.name)) {
22
+ return parent.id.name;
23
+ }
24
+ }
25
+ p = p.parentPath;
26
+ }
27
+ return null;
28
+ }
29
+ function stampLocBabel(babel, opts = {}) {
30
+ const t = babel.types;
31
+ const attrLoc = opts.attrLoc ?? "data-loc";
32
+ const attrComp = opts.attrComp ?? "data-comp";
33
+ const rootDir = opts.rootDir ?? process.cwd();
34
+ const attr = (name, value) => t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value));
35
+ const toRel = (file) => {
36
+ let root = rootDir;
37
+ while (root.endsWith("/")) root = root.slice(0, -1);
38
+ const rel = file.startsWith(root + "/") ? file.slice(root.length + 1) : file;
39
+ return rel.split("\\").join("/");
40
+ };
41
+ return {
42
+ name: "stamp-loc",
43
+ visitor: {
44
+ JSXOpeningElement(path, state) {
45
+ const node = path.node;
46
+ if (!isHostElement(node.name)) return;
47
+ const filename = state.file.opts.filename;
48
+ const loc = node.loc;
49
+ if (!filename || !loc) return;
50
+ if (!hasAttr(node, attrLoc)) {
51
+ node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}`));
52
+ }
53
+ if (!hasAttr(node, attrComp)) {
54
+ const comp = nearestComponentName(path);
55
+ if (comp) node.attributes.push(attr(attrComp, comp));
56
+ }
57
+ }
58
+ }
59
+ };
60
+ }
61
+
62
+ module.exports = stampLocBabel;
63
+ //# sourceMappingURL=babel.cjs.map
64
+ //# sourceMappingURL=babel.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stampLocBabel.ts"],"names":[],"mappings":"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","file":"babel.cjs","sourcesContent":["import type { NodePath, PluginObj, types as BabelTypes } from '@babel/core';\n\nexport interface StampLocOptions {\n /** Имя атрибута пути. Default 'data-loc'. */\n attrLoc?: string;\n /** Имя атрибута компонента. Default 'data-comp'. */\n attrComp?: string;\n /** База для относительного пути в data-loc. Default process.cwd(). */\n rootDir?: string;\n}\n\nfunction isHostElement(name: BabelTypes.JSXOpeningElement['name']): boolean {\n return name.type === 'JSXIdentifier' && /^[a-z]/.test(name.name);\n}\n\nfunction hasAttr(el: BabelTypes.JSXOpeningElement, attrName: string): boolean {\n return el.attributes.some(\n (a) => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === attrName\n );\n}\n\n// Ближайшая функция-компонент с PascalCase-именем вверх по дереву:\n// function Foo() {} | const Foo = () => {} | const Foo = function () {}\nfunction nearestComponentName(path: NodePath): string | null {\n let p: NodePath | null = path;\n while (p) {\n const node = p.node;\n if (node.type === 'FunctionDeclaration' && node.id && /^[A-Z]/.test(node.id.name)) {\n return node.id.name;\n }\n if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {\n const parent = p.parentPath?.node;\n if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && /^[A-Z]/.test(parent.id.name)) {\n return parent.id.name;\n }\n }\n p = p.parentPath;\n }\n return null;\n}\n\n/**\n * Babel-плагин: вешает data-loc=\"<path>:<line>\" и data-comp=\"<Component>\" на\n * JSX host-элементы (div, section, ...). Рантайм-инспектор читает эти DOM-атрибуты\n * (не React-internals), поэтому устойчив к версии React. Компонентные теги\n * (PascalCase) пропускаем — они не дают собственного DOM-узла.\n */\nexport default function stampLocBabel(babel: { types: typeof BabelTypes }, opts: StampLocOptions = {}): PluginObj {\n const t = babel.types;\n const attrLoc = opts.attrLoc ?? 'data-loc';\n const attrComp = opts.attrComp ?? 'data-comp';\n const rootDir = opts.rootDir ?? process.cwd();\n\n const attr = (name: string, value: string) => t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value));\n\n // path.relative без node:path, чтобы плагин не тянул узловые модули в чужих средах.\n const toRel = (file: string): string => {\n let root = rootDir;\n while (root.endsWith('/')) root = root.slice(0, -1);\n const rel = file.startsWith(root + '/') ? file.slice(root.length + 1) : file;\n return rel.split('\\\\').join('/');\n };\n\n return {\n name: 'stamp-loc',\n visitor: {\n JSXOpeningElement(path, state) {\n const node = path.node;\n if (!isHostElement(node.name)) return;\n\n const filename = state.file.opts.filename;\n const loc = node.loc;\n if (!filename || !loc) return;\n\n if (!hasAttr(node, attrLoc)) {\n node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}`));\n }\n if (!hasAttr(node, attrComp)) {\n const comp = nearestComponentName(path);\n if (comp) node.attributes.push(attr(attrComp, comp));\n }\n }\n }\n };\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import { types, PluginObj } from '@babel/core';
2
+
3
+ interface StampLocOptions {
4
+ /** Имя атрибута пути. Default 'data-loc'. */
5
+ attrLoc?: string;
6
+ /** Имя атрибута компонента. Default 'data-comp'. */
7
+ attrComp?: string;
8
+ /** База для относительного пути в data-loc. Default process.cwd(). */
9
+ rootDir?: string;
10
+ }
11
+ /**
12
+ * Babel-плагин: вешает data-loc="<path>:<line>" и data-comp="<Component>" на
13
+ * JSX host-элементы (div, section, ...). Рантайм-инспектор читает эти DOM-атрибуты
14
+ * (не React-internals), поэтому устойчив к версии React. Компонентные теги
15
+ * (PascalCase) пропускаем — они не дают собственного DOM-узла.
16
+ */
17
+ declare function stampLocBabel(babel: {
18
+ types: typeof types;
19
+ }, opts?: StampLocOptions): PluginObj;
20
+
21
+ export { type StampLocOptions, stampLocBabel as default };
@@ -0,0 +1,21 @@
1
+ import { types, PluginObj } from '@babel/core';
2
+
3
+ interface StampLocOptions {
4
+ /** Имя атрибута пути. Default 'data-loc'. */
5
+ attrLoc?: string;
6
+ /** Имя атрибута компонента. Default 'data-comp'. */
7
+ attrComp?: string;
8
+ /** База для относительного пути в data-loc. Default process.cwd(). */
9
+ rootDir?: string;
10
+ }
11
+ /**
12
+ * Babel-плагин: вешает data-loc="<path>:<line>" и data-comp="<Component>" на
13
+ * JSX host-элементы (div, section, ...). Рантайм-инспектор читает эти DOM-атрибуты
14
+ * (не React-internals), поэтому устойчив к версии React. Компонентные теги
15
+ * (PascalCase) пропускаем — они не дают собственного DOM-узла.
16
+ */
17
+ declare function stampLocBabel(babel: {
18
+ types: typeof types;
19
+ }, opts?: StampLocOptions): PluginObj;
20
+
21
+ export { type StampLocOptions, stampLocBabel as default };
package/dist/babel.js ADDED
@@ -0,0 +1,3 @@
1
+ export { stampLocBabel as default } from './chunk-AAPCI2HO.js';
2
+ //# sourceMappingURL=babel.js.map
3
+ //# sourceMappingURL=babel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"babel.js"}
@@ -0,0 +1,62 @@
1
+ // src/stampLocBabel.ts
2
+ function isHostElement(name) {
3
+ return name.type === "JSXIdentifier" && /^[a-z]/.test(name.name);
4
+ }
5
+ function hasAttr(el, attrName) {
6
+ return el.attributes.some(
7
+ (a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === attrName
8
+ );
9
+ }
10
+ function nearestComponentName(path) {
11
+ let p = path;
12
+ while (p) {
13
+ const node = p.node;
14
+ if (node.type === "FunctionDeclaration" && node.id && /^[A-Z]/.test(node.id.name)) {
15
+ return node.id.name;
16
+ }
17
+ if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
18
+ const parent = p.parentPath?.node;
19
+ if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier" && /^[A-Z]/.test(parent.id.name)) {
20
+ return parent.id.name;
21
+ }
22
+ }
23
+ p = p.parentPath;
24
+ }
25
+ return null;
26
+ }
27
+ function stampLocBabel(babel, opts = {}) {
28
+ const t = babel.types;
29
+ const attrLoc = opts.attrLoc ?? "data-loc";
30
+ const attrComp = opts.attrComp ?? "data-comp";
31
+ const rootDir = opts.rootDir ?? process.cwd();
32
+ const attr = (name, value) => t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value));
33
+ const toRel = (file) => {
34
+ let root = rootDir;
35
+ while (root.endsWith("/")) root = root.slice(0, -1);
36
+ const rel = file.startsWith(root + "/") ? file.slice(root.length + 1) : file;
37
+ return rel.split("\\").join("/");
38
+ };
39
+ return {
40
+ name: "stamp-loc",
41
+ visitor: {
42
+ JSXOpeningElement(path, state) {
43
+ const node = path.node;
44
+ if (!isHostElement(node.name)) return;
45
+ const filename = state.file.opts.filename;
46
+ const loc = node.loc;
47
+ if (!filename || !loc) return;
48
+ if (!hasAttr(node, attrLoc)) {
49
+ node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}`));
50
+ }
51
+ if (!hasAttr(node, attrComp)) {
52
+ const comp = nearestComponentName(path);
53
+ if (comp) node.attributes.push(attr(attrComp, comp));
54
+ }
55
+ }
56
+ }
57
+ };
58
+ }
59
+
60
+ export { stampLocBabel };
61
+ //# sourceMappingURL=chunk-AAPCI2HO.js.map
62
+ //# sourceMappingURL=chunk-AAPCI2HO.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stampLocBabel.ts"],"names":[],"mappings":"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","file":"chunk-AAPCI2HO.js","sourcesContent":["import type { NodePath, PluginObj, types as BabelTypes } from '@babel/core';\n\nexport interface StampLocOptions {\n /** Имя атрибута пути. Default 'data-loc'. */\n attrLoc?: string;\n /** Имя атрибута компонента. Default 'data-comp'. */\n attrComp?: string;\n /** База для относительного пути в data-loc. Default process.cwd(). */\n rootDir?: string;\n}\n\nfunction isHostElement(name: BabelTypes.JSXOpeningElement['name']): boolean {\n return name.type === 'JSXIdentifier' && /^[a-z]/.test(name.name);\n}\n\nfunction hasAttr(el: BabelTypes.JSXOpeningElement, attrName: string): boolean {\n return el.attributes.some(\n (a) => a.type === 'JSXAttribute' && a.name.type === 'JSXIdentifier' && a.name.name === attrName\n );\n}\n\n// Ближайшая функция-компонент с PascalCase-именем вверх по дереву:\n// function Foo() {} | const Foo = () => {} | const Foo = function () {}\nfunction nearestComponentName(path: NodePath): string | null {\n let p: NodePath | null = path;\n while (p) {\n const node = p.node;\n if (node.type === 'FunctionDeclaration' && node.id && /^[A-Z]/.test(node.id.name)) {\n return node.id.name;\n }\n if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {\n const parent = p.parentPath?.node;\n if (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && /^[A-Z]/.test(parent.id.name)) {\n return parent.id.name;\n }\n }\n p = p.parentPath;\n }\n return null;\n}\n\n/**\n * Babel-плагин: вешает data-loc=\"<path>:<line>\" и data-comp=\"<Component>\" на\n * JSX host-элементы (div, section, ...). Рантайм-инспектор читает эти DOM-атрибуты\n * (не React-internals), поэтому устойчив к версии React. Компонентные теги\n * (PascalCase) пропускаем — они не дают собственного DOM-узла.\n */\nexport default function stampLocBabel(babel: { types: typeof BabelTypes }, opts: StampLocOptions = {}): PluginObj {\n const t = babel.types;\n const attrLoc = opts.attrLoc ?? 'data-loc';\n const attrComp = opts.attrComp ?? 'data-comp';\n const rootDir = opts.rootDir ?? process.cwd();\n\n const attr = (name: string, value: string) => t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value));\n\n // path.relative без node:path, чтобы плагин не тянул узловые модули в чужих средах.\n const toRel = (file: string): string => {\n let root = rootDir;\n while (root.endsWith('/')) root = root.slice(0, -1);\n const rel = file.startsWith(root + '/') ? file.slice(root.length + 1) : file;\n return rel.split('\\\\').join('/');\n };\n\n return {\n name: 'stamp-loc',\n visitor: {\n JSXOpeningElement(path, state) {\n const node = path.node;\n if (!isHostElement(node.name)) return;\n\n const filename = state.file.opts.filename;\n const loc = node.loc;\n if (!filename || !loc) return;\n\n if (!hasAttr(node, attrLoc)) {\n node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}`));\n }\n if (!hasAttr(node, attrComp)) {\n const comp = nearestComponentName(path);\n if (comp) node.attributes.push(attr(attrComp, comp));\n }\n }\n }\n };\n}\n"]}
package/dist/index.cjs ADDED
@@ -0,0 +1,244 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var jsxRuntime = require('react/jsx-runtime');
5
+ var modernScreenshot = require('modern-screenshot');
6
+
7
+ // src/SemanticInspector.tsx
8
+ var Z = 2147483600;
9
+ var badge = {
10
+ position: "fixed",
11
+ bottom: 12,
12
+ left: 12,
13
+ zIndex: Z + 2,
14
+ padding: "6px 10px",
15
+ borderRadius: 6,
16
+ font: "12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace",
17
+ background: "rgba(17,17,17,0.92)",
18
+ color: "#fff",
19
+ pointerEvents: "none",
20
+ boxShadow: "0 2px 8px rgba(0,0,0,0.3)"
21
+ };
22
+ var toastStyle = {
23
+ position: "fixed",
24
+ bottom: 12,
25
+ right: 12,
26
+ zIndex: Z + 2,
27
+ padding: "6px 10px",
28
+ borderRadius: 6,
29
+ font: "12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace",
30
+ background: "rgba(22,101,52,0.95)",
31
+ color: "#fff",
32
+ pointerEvents: "none",
33
+ maxWidth: "60vw",
34
+ whiteSpace: "nowrap",
35
+ overflow: "hidden",
36
+ textOverflow: "ellipsis"
37
+ };
38
+ function boxStyle(r) {
39
+ return {
40
+ position: "fixed",
41
+ left: r.left,
42
+ top: r.top,
43
+ width: r.width,
44
+ height: r.height,
45
+ zIndex: Z,
46
+ outline: "2px solid #6366f1",
47
+ background: "rgba(99,102,241,0.12)",
48
+ pointerEvents: "none",
49
+ transition: "all 60ms ease-out"
50
+ };
51
+ }
52
+ function tipStyle(r) {
53
+ const top = r.top > 26 ? r.top - 24 : r.bottom + 4;
54
+ return {
55
+ position: "fixed",
56
+ left: r.left,
57
+ top,
58
+ zIndex: Z + 1,
59
+ padding: "2px 6px",
60
+ borderRadius: 4,
61
+ font: "11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
62
+ background: "#6366f1",
63
+ color: "#fff",
64
+ pointerEvents: "none",
65
+ whiteSpace: "nowrap"
66
+ };
67
+ }
68
+ function Overlay({ target, toast }) {
69
+ return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
70
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: badge, children: "\u2316 inspect \xB7 click=name \xB7 \u21E7click=shot \xB7 Esc=exit" }),
71
+ target && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
72
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: boxStyle(target.rect) }),
73
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: tipStyle(target.rect), children: [
74
+ target.comp,
75
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { style: { opacity: 0.75 }, children: [
76
+ " \xB7 ",
77
+ target.loc ?? "no source"
78
+ ] })
79
+ ] })
80
+ ] }),
81
+ toast && /* @__PURE__ */ jsxRuntime.jsx("div", { style: toastStyle, children: toast })
82
+ ] });
83
+ }
84
+ async function copyText(text) {
85
+ await navigator.clipboard.writeText(text);
86
+ }
87
+ async function copyElementShot(el) {
88
+ const blob = await modernScreenshot.domToBlob(el);
89
+ if (!blob) throw new Error("screenshot produced empty blob");
90
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
91
+ }
92
+
93
+ // src/resolveTarget.ts
94
+ var LOC_ATTR = "data-loc";
95
+ var COMP_ATTR = "data-comp";
96
+ function resolveTarget(el) {
97
+ if (!el) return null;
98
+ const target = el.closest(`[${LOC_ATTR}]`) ?? el;
99
+ const loc = target.getAttribute(LOC_ATTR);
100
+ const comp = target.getAttribute(COMP_ATTR) ?? fiberName(target) ?? fallbackName(target, loc);
101
+ return { comp, loc, el: target, rect: target.getBoundingClientRect() };
102
+ }
103
+ function fallbackName(el, loc) {
104
+ if (loc) {
105
+ const base = loc.split(":")[0].split("/").pop()?.replace(/\.[jt]sx?$/, "");
106
+ if (base) return base;
107
+ }
108
+ return el.tagName.toLowerCase();
109
+ }
110
+ function fiberName(el) {
111
+ const key = Object.keys(el).find((k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$"));
112
+ if (!key) return null;
113
+ let fiber = el[key] ?? null;
114
+ while (fiber) {
115
+ const t = fiber.type;
116
+ const name = t && typeof t !== "string" ? t.displayName ?? t.name : void 0;
117
+ if (name && /^[A-Z]/.test(name)) return name;
118
+ fiber = fiber.return;
119
+ }
120
+ return null;
121
+ }
122
+
123
+ // src/useInspector.ts
124
+ var DEFAULT_HOTKEY = "Alt+Shift+S";
125
+ function defaultFormat(t) {
126
+ return t.loc ? `${t.comp} \u2014 ${t.loc}` : t.comp;
127
+ }
128
+ function matchHotkey(e, hotkey) {
129
+ const parts = hotkey.split("+").map((p) => p.trim().toLowerCase());
130
+ const key = parts[parts.length - 1];
131
+ const want = (m, alt) => parts.includes(m) || (alt ? parts.includes(alt) : false);
132
+ if (e.altKey !== want("alt")) return false;
133
+ if (e.shiftKey !== want("shift")) return false;
134
+ if (e.ctrlKey !== want("ctrl", "control")) return false;
135
+ if (e.metaKey !== want("meta", "cmd")) return false;
136
+ return e.key.toLowerCase() === key || e.code.toLowerCase() === `key${key}`;
137
+ }
138
+ function useInspector(opts = {}) {
139
+ const { hotkey = DEFAULT_HOTKEY } = opts;
140
+ const [active, setActive] = react.useState(false);
141
+ const [target, setTarget] = react.useState(null);
142
+ const cbRef = react.useRef(opts);
143
+ cbRef.current = opts;
144
+ react.useEffect(() => {
145
+ function onKey(e) {
146
+ if (matchHotkey(e, hotkey)) {
147
+ e.preventDefault();
148
+ setActive((a) => !a);
149
+ } else if (e.key === "Escape") {
150
+ setActive(false);
151
+ }
152
+ }
153
+ window.addEventListener("keydown", onKey);
154
+ return () => {
155
+ window.removeEventListener("keydown", onKey);
156
+ };
157
+ }, [hotkey]);
158
+ react.useEffect(() => {
159
+ if (!active) {
160
+ setTarget(null);
161
+ return;
162
+ }
163
+ function onMove(e) {
164
+ setTarget(resolveTarget(document.elementFromPoint(e.clientX, e.clientY)));
165
+ }
166
+ function onClick(e) {
167
+ const t = resolveTarget(document.elementFromPoint(e.clientX, e.clientY));
168
+ if (!t) return;
169
+ e.preventDefault();
170
+ e.stopPropagation();
171
+ const { formatText = defaultFormat, onCopy, onError } = cbRef.current;
172
+ const done = (kind, payload) => {
173
+ onCopy?.(kind, payload);
174
+ };
175
+ const fail = (kind, err) => {
176
+ onError?.(kind, err);
177
+ };
178
+ if (e.shiftKey) {
179
+ copyElementShot(t.el).then(
180
+ () => {
181
+ done("screenshot", t.comp);
182
+ },
183
+ (err) => {
184
+ fail("screenshot", err);
185
+ }
186
+ );
187
+ } else {
188
+ const text = formatText({ comp: t.comp, loc: t.loc });
189
+ copyText(text).then(
190
+ () => {
191
+ done("text", text);
192
+ },
193
+ (err) => {
194
+ fail("text", err);
195
+ }
196
+ );
197
+ }
198
+ }
199
+ window.addEventListener("mousemove", onMove, true);
200
+ window.addEventListener("click", onClick, true);
201
+ const prevCursor = document.body.style.cursor;
202
+ document.body.style.cursor = "crosshair";
203
+ return () => {
204
+ window.removeEventListener("mousemove", onMove, true);
205
+ window.removeEventListener("click", onClick, true);
206
+ document.body.style.cursor = prevCursor;
207
+ };
208
+ }, [active]);
209
+ return { active, target };
210
+ }
211
+ var TOAST_MS = 1400;
212
+ function SemanticInspector(props) {
213
+ const [toast, setToast] = react.useState(null);
214
+ const timer = react.useRef(void 0);
215
+ const flash = (msg) => {
216
+ setToast(msg);
217
+ clearTimeout(timer.current);
218
+ timer.current = setTimeout(() => {
219
+ setToast(null);
220
+ }, TOAST_MS);
221
+ };
222
+ const { active, target } = useInspector({
223
+ hotkey: props.hotkey,
224
+ formatText: props.formatText,
225
+ onCopy: (kind, payload) => {
226
+ flash(kind === "text" ? `\u2713 ${payload}` : "\u2713 screenshot copied");
227
+ props.onCopy?.(kind, payload);
228
+ },
229
+ onError: (kind, err) => {
230
+ flash(`\u2717 ${kind} failed`);
231
+ props.onError?.(kind, err);
232
+ }
233
+ });
234
+ if (!active && !toast) return null;
235
+ return /* @__PURE__ */ jsxRuntime.jsx(Overlay, { target: active ? target : null, toast });
236
+ }
237
+
238
+ exports.SemanticInspector = SemanticInspector;
239
+ exports.copyElementShot = copyElementShot;
240
+ exports.copyText = copyText;
241
+ exports.resolveTarget = resolveTarget;
242
+ exports.useInspector = useInspector;
243
+ //# sourceMappingURL=index.cjs.map
244
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/Overlay.tsx","../src/clipboard.ts","../src/resolveTarget.ts","../src/useInspector.ts","../src/SemanticInspector.tsx"],"names":["jsxs","Fragment","jsx","domToBlob","useState","useRef","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import type { CSSProperties } from 'react';\nimport type { InspectTarget } from './types';\n\nconst Z = 2147483600;\n\nconst badge: CSSProperties = {\n position: 'fixed',\n bottom: 12,\n left: 12,\n zIndex: Z + 2,\n padding: '6px 10px',\n borderRadius: 6,\n font: '12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace',\n background: 'rgba(17,17,17,0.92)',\n color: '#fff',\n pointerEvents: 'none',\n boxShadow: '0 2px 8px rgba(0,0,0,0.3)'\n};\n\nconst toastStyle: CSSProperties = {\n position: 'fixed',\n bottom: 12,\n right: 12,\n zIndex: Z + 2,\n padding: '6px 10px',\n borderRadius: 6,\n font: '12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace',\n background: 'rgba(22,101,52,0.95)',\n color: '#fff',\n pointerEvents: 'none',\n maxWidth: '60vw',\n whiteSpace: 'nowrap',\n overflow: 'hidden',\n textOverflow: 'ellipsis'\n};\n\nfunction boxStyle(r: DOMRect): CSSProperties {\n return {\n position: 'fixed',\n left: r.left,\n top: r.top,\n width: r.width,\n height: r.height,\n zIndex: Z,\n outline: '2px solid #6366f1',\n background: 'rgba(99,102,241,0.12)',\n pointerEvents: 'none',\n transition: 'all 60ms ease-out'\n };\n}\n\nfunction tipStyle(r: DOMRect): CSSProperties {\n const top = r.top > 26 ? r.top - 24 : r.bottom + 4;\n return {\n position: 'fixed',\n left: r.left,\n top,\n zIndex: Z + 1,\n padding: '2px 6px',\n borderRadius: 4,\n font: '11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace',\n background: '#6366f1',\n color: '#fff',\n pointerEvents: 'none',\n whiteSpace: 'nowrap'\n };\n}\n\nexport function Overlay({ target, toast }: { target: InspectTarget | null; toast: string | null }) {\n return (\n <>\n <div style={badge}>⌖ inspect · click=name · ⇧click=shot · Esc=exit</div>\n {target && (\n <>\n <div style={boxStyle(target.rect)} />\n <div style={tipStyle(target.rect)}>\n {target.comp}\n <span style={{ opacity: 0.75 }}> · {target.loc ?? 'no source'}</span>\n </div>\n </>\n )}\n {toast && <div style={toastStyle}>{toast}</div>}\n </>\n );\n}\n","import { domToBlob } from 'modern-screenshot';\n\n/** Текст в буфер. */\nexport async function copyText(text: string): Promise<void> {\n await navigator.clipboard.writeText(text);\n}\n\n/**\n * PNG-скриншот ТОЛЬКО переданного элемента в буфер (image/png).\n * Должен вызываться из user-gesture (клик), иначе браузер блокит image-копию.\n */\nexport async function copyElementShot(el: Element): Promise<void> {\n const blob = await domToBlob(el as HTMLElement);\n if (!blob) throw new Error('screenshot produced empty blob');\n await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);\n}\n","import type { InspectTarget } from './types';\n\nconst LOC_ATTR = 'data-loc';\nconst COMP_ATTR = 'data-comp';\n\n/**\n * DOM-элемент под курсором → цель инспекции.\n * Идём к ближайшему предку с data-loc (заштампован babel-плагином). Если его нет\n * (prod-билд без штампов / сторонний узел) — best-effort: имя из React fiber,\n * затем имя файла из data-loc, затем имя тега.\n */\nexport function resolveTarget(el: Element | null): InspectTarget | null {\n if (!el) return null;\n const target = el.closest(`[${LOC_ATTR}]`) ?? el;\n const loc = target.getAttribute(LOC_ATTR);\n const comp = target.getAttribute(COMP_ATTR) ?? fiberName(target) ?? fallbackName(target, loc);\n return { comp, loc, el: target, rect: target.getBoundingClientRect() };\n}\n\nfunction fallbackName(el: Element, loc: string | null): string {\n if (loc) {\n const base = loc\n .split(':')[0]\n .split('/')\n .pop()\n ?.replace(/\\.[jt]sx?$/, '');\n if (base) return base;\n }\n return el.tagName.toLowerCase();\n}\n\n// React-internals fallback. _debugSource убран в React 19, но имя компонента из\n// fiber.type всё ещё доступно (в dev-билде не минифицировано).\ninterface FiberLike {\n type: { displayName?: string; name?: string } | string | null | undefined;\n return: FiberLike | null;\n}\n\nfunction fiberName(el: Element): string | null {\n const key = Object.keys(el).find((k) => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'));\n if (!key) return null;\n let fiber = (el as unknown as Record<string, FiberLike | undefined>)[key] ?? null;\n while (fiber) {\n const t = fiber.type;\n const name = t && typeof t !== 'string' ? (t.displayName ?? t.name) : undefined;\n if (name && /^[A-Z]/.test(name)) return name;\n fiber = fiber.return;\n }\n return null;\n}\n","import { useEffect, useRef, useState } from 'react';\nimport { copyElementShot, copyText } from './clipboard';\nimport { resolveTarget } from './resolveTarget';\nimport type { CopyKind, InspectTarget, SemanticInspectorProps } from './types';\n\nconst DEFAULT_HOTKEY = 'Alt+Shift+S';\n\nfunction defaultFormat(t: { comp: string; loc: string | null }): string {\n return t.loc ? `${t.comp} — ${t.loc}` : t.comp;\n}\n\n/** 'Alt+Shift+S' → совпадает ли событие keydown. Последний токен — клавиша. */\nfunction matchHotkey(e: KeyboardEvent, hotkey: string): boolean {\n const parts = hotkey.split('+').map((p) => p.trim().toLowerCase());\n const key = parts[parts.length - 1];\n const want = (m: string, alt?: string) => parts.includes(m) || (alt ? parts.includes(alt) : false);\n if (e.altKey !== want('alt')) return false;\n if (e.shiftKey !== want('shift')) return false;\n if (e.ctrlKey !== want('ctrl', 'control')) return false;\n if (e.metaKey !== want('meta', 'cmd')) return false;\n return e.key.toLowerCase() === key || e.code.toLowerCase() === `key${key}`;\n}\n\n/**\n * Состояние режима инспекции + слушатели.\n * - keydown: хоткей переключает active, Esc выключает.\n * - active: mousemove обновляет target; click (capture, preventDefault) копирует\n * текст, Shift+click — скриншот элемента.\n */\nexport function useInspector(opts: SemanticInspectorProps = {}) {\n const { hotkey = DEFAULT_HOTKEY } = opts;\n const [active, setActive] = useState(false);\n const [target, setTarget] = useState<InspectTarget | null>(null);\n\n // Свежие колбэки без переподписки слушателей.\n const cbRef = useRef<SemanticInspectorProps>(opts);\n cbRef.current = opts;\n\n useEffect(() => {\n function onKey(e: KeyboardEvent) {\n if (matchHotkey(e, hotkey)) {\n e.preventDefault();\n setActive((a) => !a);\n } else if (e.key === 'Escape') {\n setActive(false);\n }\n }\n window.addEventListener('keydown', onKey);\n return () => {\n window.removeEventListener('keydown', onKey);\n };\n }, [hotkey]);\n\n useEffect(() => {\n if (!active) {\n setTarget(null);\n return;\n }\n\n function onMove(e: MouseEvent) {\n setTarget(resolveTarget(document.elementFromPoint(e.clientX, e.clientY)));\n }\n\n function onClick(e: MouseEvent) {\n const t = resolveTarget(document.elementFromPoint(e.clientX, e.clientY));\n if (!t) return;\n e.preventDefault();\n e.stopPropagation();\n const { formatText = defaultFormat, onCopy, onError } = cbRef.current;\n const done = (kind: CopyKind, payload: string) => {\n onCopy?.(kind, payload);\n };\n const fail = (kind: CopyKind, err: unknown) => {\n onError?.(kind, err);\n };\n if (e.shiftKey) {\n copyElementShot(t.el).then(\n () => {\n done('screenshot', t.comp);\n },\n (err: unknown) => {\n fail('screenshot', err);\n }\n );\n } else {\n const text = formatText({ comp: t.comp, loc: t.loc });\n copyText(text).then(\n () => {\n done('text', text);\n },\n (err: unknown) => {\n fail('text', err);\n }\n );\n }\n }\n\n window.addEventListener('mousemove', onMove, true);\n window.addEventListener('click', onClick, true);\n const prevCursor = document.body.style.cursor;\n document.body.style.cursor = 'crosshair';\n return () => {\n window.removeEventListener('mousemove', onMove, true);\n window.removeEventListener('click', onClick, true);\n document.body.style.cursor = prevCursor;\n };\n }, [active]);\n\n return { active, target };\n}\n","import { useRef, useState } from 'react';\nimport { Overlay } from './Overlay';\nimport type { CopyKind, SemanticInspectorProps } from './types';\nimport { useInspector } from './useInspector';\n\nconst TOAST_MS = 1400;\n\n/**\n * Семантический инспектор. Сам по себе ничего не показывает, пока не включён\n * хоткеем. Гейтинг (где монтировать) — забота консьюмера: монтируй под своим\n * dev-флагом и желательно через React.lazy, чтобы не тянуть в prod-бандл.\n */\nexport function SemanticInspector(props: SemanticInspectorProps) {\n const [toast, setToast] = useState<string | null>(null);\n const timer = useRef<ReturnType<typeof setTimeout>>(undefined);\n\n const flash = (msg: string) => {\n setToast(msg);\n clearTimeout(timer.current);\n timer.current = setTimeout(() => {\n setToast(null);\n }, TOAST_MS);\n };\n\n const { active, target } = useInspector({\n hotkey: props.hotkey,\n formatText: props.formatText,\n onCopy: (kind: CopyKind, payload: string) => {\n flash(kind === 'text' ? `✓ ${payload}` : '✓ screenshot copied');\n props.onCopy?.(kind, payload);\n },\n onError: (kind: CopyKind, err: unknown) => {\n flash(`✗ ${kind} failed`);\n props.onError?.(kind, err);\n }\n });\n\n if (!active && !toast) return null;\n return <Overlay target={active ? target : null} toast={toast} />;\n}\n"]}