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.
@@ -0,0 +1,62 @@
1
+ import * as react from 'react';
2
+
3
+ type CopyKind = 'text' | 'screenshot';
4
+ interface InspectTarget {
5
+ /** Имя компонента (data-comp) или fallback (fiber displayName / имя файла / тег). */
6
+ comp: string;
7
+ /** "<path>:<line>" из data-loc, либо null если элемент не заштампован. */
8
+ loc: string | null;
9
+ /** Реальный DOM-элемент (ближайший с data-loc, либо сам). */
10
+ el: Element;
11
+ /** Геометрия для оверлея. */
12
+ rect: DOMRect;
13
+ }
14
+ interface SemanticInspectorProps {
15
+ /** Хоткей-toggle. Default 'Alt+Shift+S'. Формат: 'Alt+Shift+S', 'Ctrl+Cmd+I'. */
16
+ hotkey?: string;
17
+ /** Формат текста для буфера. Default: `${comp} — ${loc}` (или `${comp}` без loc). */
18
+ formatText?: (t: {
19
+ comp: string;
20
+ loc: string | null;
21
+ }) => string;
22
+ /** Колбэк после успешной копии — для телеметрии/тостов апа. */
23
+ onCopy?: (kind: CopyKind, payload: string) => void;
24
+ /** Колбэк при ошибке копии (clipboard reject / screenshot fail). */
25
+ onError?: (kind: CopyKind, err: unknown) => void;
26
+ }
27
+
28
+ /**
29
+ * Семантический инспектор. Сам по себе ничего не показывает, пока не включён
30
+ * хоткеем. Гейтинг (где монтировать) — забота консьюмера: монтируй под своим
31
+ * dev-флагом и желательно через React.lazy, чтобы не тянуть в prod-бандл.
32
+ */
33
+ declare function SemanticInspector(props: SemanticInspectorProps): react.JSX.Element | null;
34
+
35
+ /**
36
+ * Состояние режима инспекции + слушатели.
37
+ * - keydown: хоткей переключает active, Esc выключает.
38
+ * - active: mousemove обновляет target; click (capture, preventDefault) копирует
39
+ * текст, Shift+click — скриншот элемента.
40
+ */
41
+ declare function useInspector(opts?: SemanticInspectorProps): {
42
+ active: boolean;
43
+ target: InspectTarget | null;
44
+ };
45
+
46
+ /**
47
+ * DOM-элемент под курсором → цель инспекции.
48
+ * Идём к ближайшему предку с data-loc (заштампован babel-плагином). Если его нет
49
+ * (prod-билд без штампов / сторонний узел) — best-effort: имя из React fiber,
50
+ * затем имя файла из data-loc, затем имя тега.
51
+ */
52
+ declare function resolveTarget(el: Element | null): InspectTarget | null;
53
+
54
+ /** Текст в буфер. */
55
+ declare function copyText(text: string): Promise<void>;
56
+ /**
57
+ * PNG-скриншот ТОЛЬКО переданного элемента в буфер (image/png).
58
+ * Должен вызываться из user-gesture (клик), иначе браузер блокит image-копию.
59
+ */
60
+ declare function copyElementShot(el: Element): Promise<void>;
61
+
62
+ export { type CopyKind, type InspectTarget, SemanticInspector, type SemanticInspectorProps, copyElementShot, copyText, resolveTarget, useInspector };
@@ -0,0 +1,62 @@
1
+ import * as react from 'react';
2
+
3
+ type CopyKind = 'text' | 'screenshot';
4
+ interface InspectTarget {
5
+ /** Имя компонента (data-comp) или fallback (fiber displayName / имя файла / тег). */
6
+ comp: string;
7
+ /** "<path>:<line>" из data-loc, либо null если элемент не заштампован. */
8
+ loc: string | null;
9
+ /** Реальный DOM-элемент (ближайший с data-loc, либо сам). */
10
+ el: Element;
11
+ /** Геометрия для оверлея. */
12
+ rect: DOMRect;
13
+ }
14
+ interface SemanticInspectorProps {
15
+ /** Хоткей-toggle. Default 'Alt+Shift+S'. Формат: 'Alt+Shift+S', 'Ctrl+Cmd+I'. */
16
+ hotkey?: string;
17
+ /** Формат текста для буфера. Default: `${comp} — ${loc}` (или `${comp}` без loc). */
18
+ formatText?: (t: {
19
+ comp: string;
20
+ loc: string | null;
21
+ }) => string;
22
+ /** Колбэк после успешной копии — для телеметрии/тостов апа. */
23
+ onCopy?: (kind: CopyKind, payload: string) => void;
24
+ /** Колбэк при ошибке копии (clipboard reject / screenshot fail). */
25
+ onError?: (kind: CopyKind, err: unknown) => void;
26
+ }
27
+
28
+ /**
29
+ * Семантический инспектор. Сам по себе ничего не показывает, пока не включён
30
+ * хоткеем. Гейтинг (где монтировать) — забота консьюмера: монтируй под своим
31
+ * dev-флагом и желательно через React.lazy, чтобы не тянуть в prod-бандл.
32
+ */
33
+ declare function SemanticInspector(props: SemanticInspectorProps): react.JSX.Element | null;
34
+
35
+ /**
36
+ * Состояние режима инспекции + слушатели.
37
+ * - keydown: хоткей переключает active, Esc выключает.
38
+ * - active: mousemove обновляет target; click (capture, preventDefault) копирует
39
+ * текст, Shift+click — скриншот элемента.
40
+ */
41
+ declare function useInspector(opts?: SemanticInspectorProps): {
42
+ active: boolean;
43
+ target: InspectTarget | null;
44
+ };
45
+
46
+ /**
47
+ * DOM-элемент под курсором → цель инспекции.
48
+ * Идём к ближайшему предку с data-loc (заштампован babel-плагином). Если его нет
49
+ * (prod-билд без штампов / сторонний узел) — best-effort: имя из React fiber,
50
+ * затем имя файла из data-loc, затем имя тега.
51
+ */
52
+ declare function resolveTarget(el: Element | null): InspectTarget | null;
53
+
54
+ /** Текст в буфер. */
55
+ declare function copyText(text: string): Promise<void>;
56
+ /**
57
+ * PNG-скриншот ТОЛЬКО переданного элемента в буфер (image/png).
58
+ * Должен вызываться из user-gesture (клик), иначе браузер блокит image-копию.
59
+ */
60
+ declare function copyElementShot(el: Element): Promise<void>;
61
+
62
+ export { type CopyKind, type InspectTarget, SemanticInspector, type SemanticInspectorProps, copyElementShot, copyText, resolveTarget, useInspector };
package/dist/index.js ADDED
@@ -0,0 +1,238 @@
1
+ import { useState, useRef, useEffect } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { domToBlob } from 'modern-screenshot';
4
+
5
+ // src/SemanticInspector.tsx
6
+ var Z = 2147483600;
7
+ var badge = {
8
+ position: "fixed",
9
+ bottom: 12,
10
+ left: 12,
11
+ zIndex: Z + 2,
12
+ padding: "6px 10px",
13
+ borderRadius: 6,
14
+ font: "12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace",
15
+ background: "rgba(17,17,17,0.92)",
16
+ color: "#fff",
17
+ pointerEvents: "none",
18
+ boxShadow: "0 2px 8px rgba(0,0,0,0.3)"
19
+ };
20
+ var toastStyle = {
21
+ position: "fixed",
22
+ bottom: 12,
23
+ right: 12,
24
+ zIndex: Z + 2,
25
+ padding: "6px 10px",
26
+ borderRadius: 6,
27
+ font: "12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace",
28
+ background: "rgba(22,101,52,0.95)",
29
+ color: "#fff",
30
+ pointerEvents: "none",
31
+ maxWidth: "60vw",
32
+ whiteSpace: "nowrap",
33
+ overflow: "hidden",
34
+ textOverflow: "ellipsis"
35
+ };
36
+ function boxStyle(r) {
37
+ return {
38
+ position: "fixed",
39
+ left: r.left,
40
+ top: r.top,
41
+ width: r.width,
42
+ height: r.height,
43
+ zIndex: Z,
44
+ outline: "2px solid #6366f1",
45
+ background: "rgba(99,102,241,0.12)",
46
+ pointerEvents: "none",
47
+ transition: "all 60ms ease-out"
48
+ };
49
+ }
50
+ function tipStyle(r) {
51
+ const top = r.top > 26 ? r.top - 24 : r.bottom + 4;
52
+ return {
53
+ position: "fixed",
54
+ left: r.left,
55
+ top,
56
+ zIndex: Z + 1,
57
+ padding: "2px 6px",
58
+ borderRadius: 4,
59
+ font: "11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
60
+ background: "#6366f1",
61
+ color: "#fff",
62
+ pointerEvents: "none",
63
+ whiteSpace: "nowrap"
64
+ };
65
+ }
66
+ function Overlay({ target, toast }) {
67
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
68
+ /* @__PURE__ */ jsx("div", { style: badge, children: "\u2316 inspect \xB7 click=name \xB7 \u21E7click=shot \xB7 Esc=exit" }),
69
+ target && /* @__PURE__ */ jsxs(Fragment, { children: [
70
+ /* @__PURE__ */ jsx("div", { style: boxStyle(target.rect) }),
71
+ /* @__PURE__ */ jsxs("div", { style: tipStyle(target.rect), children: [
72
+ target.comp,
73
+ /* @__PURE__ */ jsxs("span", { style: { opacity: 0.75 }, children: [
74
+ " \xB7 ",
75
+ target.loc ?? "no source"
76
+ ] })
77
+ ] })
78
+ ] }),
79
+ toast && /* @__PURE__ */ jsx("div", { style: toastStyle, children: toast })
80
+ ] });
81
+ }
82
+ async function copyText(text) {
83
+ await navigator.clipboard.writeText(text);
84
+ }
85
+ async function copyElementShot(el) {
86
+ const blob = await domToBlob(el);
87
+ if (!blob) throw new Error("screenshot produced empty blob");
88
+ await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
89
+ }
90
+
91
+ // src/resolveTarget.ts
92
+ var LOC_ATTR = "data-loc";
93
+ var COMP_ATTR = "data-comp";
94
+ function resolveTarget(el) {
95
+ if (!el) return null;
96
+ const target = el.closest(`[${LOC_ATTR}]`) ?? el;
97
+ const loc = target.getAttribute(LOC_ATTR);
98
+ const comp = target.getAttribute(COMP_ATTR) ?? fiberName(target) ?? fallbackName(target, loc);
99
+ return { comp, loc, el: target, rect: target.getBoundingClientRect() };
100
+ }
101
+ function fallbackName(el, loc) {
102
+ if (loc) {
103
+ const base = loc.split(":")[0].split("/").pop()?.replace(/\.[jt]sx?$/, "");
104
+ if (base) return base;
105
+ }
106
+ return el.tagName.toLowerCase();
107
+ }
108
+ function fiberName(el) {
109
+ const key = Object.keys(el).find((k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$"));
110
+ if (!key) return null;
111
+ let fiber = el[key] ?? null;
112
+ while (fiber) {
113
+ const t = fiber.type;
114
+ const name = t && typeof t !== "string" ? t.displayName ?? t.name : void 0;
115
+ if (name && /^[A-Z]/.test(name)) return name;
116
+ fiber = fiber.return;
117
+ }
118
+ return null;
119
+ }
120
+
121
+ // src/useInspector.ts
122
+ var DEFAULT_HOTKEY = "Alt+Shift+S";
123
+ function defaultFormat(t) {
124
+ return t.loc ? `${t.comp} \u2014 ${t.loc}` : t.comp;
125
+ }
126
+ function matchHotkey(e, hotkey) {
127
+ const parts = hotkey.split("+").map((p) => p.trim().toLowerCase());
128
+ const key = parts[parts.length - 1];
129
+ const want = (m, alt) => parts.includes(m) || (alt ? parts.includes(alt) : false);
130
+ if (e.altKey !== want("alt")) return false;
131
+ if (e.shiftKey !== want("shift")) return false;
132
+ if (e.ctrlKey !== want("ctrl", "control")) return false;
133
+ if (e.metaKey !== want("meta", "cmd")) return false;
134
+ return e.key.toLowerCase() === key || e.code.toLowerCase() === `key${key}`;
135
+ }
136
+ function useInspector(opts = {}) {
137
+ const { hotkey = DEFAULT_HOTKEY } = opts;
138
+ const [active, setActive] = useState(false);
139
+ const [target, setTarget] = useState(null);
140
+ const cbRef = useRef(opts);
141
+ cbRef.current = opts;
142
+ useEffect(() => {
143
+ function onKey(e) {
144
+ if (matchHotkey(e, hotkey)) {
145
+ e.preventDefault();
146
+ setActive((a) => !a);
147
+ } else if (e.key === "Escape") {
148
+ setActive(false);
149
+ }
150
+ }
151
+ window.addEventListener("keydown", onKey);
152
+ return () => {
153
+ window.removeEventListener("keydown", onKey);
154
+ };
155
+ }, [hotkey]);
156
+ useEffect(() => {
157
+ if (!active) {
158
+ setTarget(null);
159
+ return;
160
+ }
161
+ function onMove(e) {
162
+ setTarget(resolveTarget(document.elementFromPoint(e.clientX, e.clientY)));
163
+ }
164
+ function onClick(e) {
165
+ const t = resolveTarget(document.elementFromPoint(e.clientX, e.clientY));
166
+ if (!t) return;
167
+ e.preventDefault();
168
+ e.stopPropagation();
169
+ const { formatText = defaultFormat, onCopy, onError } = cbRef.current;
170
+ const done = (kind, payload) => {
171
+ onCopy?.(kind, payload);
172
+ };
173
+ const fail = (kind, err) => {
174
+ onError?.(kind, err);
175
+ };
176
+ if (e.shiftKey) {
177
+ copyElementShot(t.el).then(
178
+ () => {
179
+ done("screenshot", t.comp);
180
+ },
181
+ (err) => {
182
+ fail("screenshot", err);
183
+ }
184
+ );
185
+ } else {
186
+ const text = formatText({ comp: t.comp, loc: t.loc });
187
+ copyText(text).then(
188
+ () => {
189
+ done("text", text);
190
+ },
191
+ (err) => {
192
+ fail("text", err);
193
+ }
194
+ );
195
+ }
196
+ }
197
+ window.addEventListener("mousemove", onMove, true);
198
+ window.addEventListener("click", onClick, true);
199
+ const prevCursor = document.body.style.cursor;
200
+ document.body.style.cursor = "crosshair";
201
+ return () => {
202
+ window.removeEventListener("mousemove", onMove, true);
203
+ window.removeEventListener("click", onClick, true);
204
+ document.body.style.cursor = prevCursor;
205
+ };
206
+ }, [active]);
207
+ return { active, target };
208
+ }
209
+ var TOAST_MS = 1400;
210
+ function SemanticInspector(props) {
211
+ const [toast, setToast] = useState(null);
212
+ const timer = useRef(void 0);
213
+ const flash = (msg) => {
214
+ setToast(msg);
215
+ clearTimeout(timer.current);
216
+ timer.current = setTimeout(() => {
217
+ setToast(null);
218
+ }, TOAST_MS);
219
+ };
220
+ const { active, target } = useInspector({
221
+ hotkey: props.hotkey,
222
+ formatText: props.formatText,
223
+ onCopy: (kind, payload) => {
224
+ flash(kind === "text" ? `\u2713 ${payload}` : "\u2713 screenshot copied");
225
+ props.onCopy?.(kind, payload);
226
+ },
227
+ onError: (kind, err) => {
228
+ flash(`\u2717 ${kind} failed`);
229
+ props.onError?.(kind, err);
230
+ }
231
+ });
232
+ if (!active && !toast) return null;
233
+ return /* @__PURE__ */ jsx(Overlay, { target: active ? target : null, toast });
234
+ }
235
+
236
+ export { SemanticInspector, copyElementShot, copyText, resolveTarget, useInspector };
237
+ //# sourceMappingURL=index.js.map
238
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/Overlay.tsx","../src/clipboard.ts","../src/resolveTarget.ts","../src/useInspector.ts","../src/SemanticInspector.tsx"],"names":["useState","useRef","jsx"],"mappings":"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","file":"index.js","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"]}
package/dist/vite.cjs ADDED
@@ -0,0 +1,101 @@
1
+ 'use strict';
2
+
3
+ var core = require('@babel/core');
4
+
5
+ // src/vite.ts
6
+
7
+ // src/stampLocBabel.ts
8
+ function isHostElement(name) {
9
+ return name.type === "JSXIdentifier" && /^[a-z]/.test(name.name);
10
+ }
11
+ function hasAttr(el, attrName) {
12
+ return el.attributes.some(
13
+ (a) => a.type === "JSXAttribute" && a.name.type === "JSXIdentifier" && a.name.name === attrName
14
+ );
15
+ }
16
+ function nearestComponentName(path) {
17
+ let p = path;
18
+ while (p) {
19
+ const node = p.node;
20
+ if (node.type === "FunctionDeclaration" && node.id && /^[A-Z]/.test(node.id.name)) {
21
+ return node.id.name;
22
+ }
23
+ if (node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
24
+ const parent = p.parentPath?.node;
25
+ if (parent?.type === "VariableDeclarator" && parent.id.type === "Identifier" && /^[A-Z]/.test(parent.id.name)) {
26
+ return parent.id.name;
27
+ }
28
+ }
29
+ p = p.parentPath;
30
+ }
31
+ return null;
32
+ }
33
+ function stampLocBabel(babel, opts = {}) {
34
+ const t = babel.types;
35
+ const attrLoc = opts.attrLoc ?? "data-loc";
36
+ const attrComp = opts.attrComp ?? "data-comp";
37
+ const rootDir = opts.rootDir ?? process.cwd();
38
+ const attr = (name, value) => t.jsxAttribute(t.jsxIdentifier(name), t.stringLiteral(value));
39
+ const toRel = (file) => {
40
+ let root = rootDir;
41
+ while (root.endsWith("/")) root = root.slice(0, -1);
42
+ const rel = file.startsWith(root + "/") ? file.slice(root.length + 1) : file;
43
+ return rel.split("\\").join("/");
44
+ };
45
+ return {
46
+ name: "stamp-loc",
47
+ visitor: {
48
+ JSXOpeningElement(path, state) {
49
+ const node = path.node;
50
+ if (!isHostElement(node.name)) return;
51
+ const filename = state.file.opts.filename;
52
+ const loc = node.loc;
53
+ if (!filename || !loc) return;
54
+ if (!hasAttr(node, attrLoc)) {
55
+ node.attributes.push(attr(attrLoc, `${toRel(filename)}:${loc.start.line}`));
56
+ }
57
+ if (!hasAttr(node, attrComp)) {
58
+ const comp = nearestComponentName(path);
59
+ if (comp) node.attributes.push(attr(attrComp, comp));
60
+ }
61
+ }
62
+ }
63
+ };
64
+ }
65
+
66
+ // src/vite.ts
67
+ function stampLocVite(opts = {}) {
68
+ const include = opts.include ?? /\.[jt]sx$/;
69
+ const babelOpts = {
70
+ attrLoc: opts.attrLoc,
71
+ attrComp: opts.attrComp,
72
+ rootDir: opts.rootDir
73
+ };
74
+ return {
75
+ name: "semantic-inspector:stamp-loc",
76
+ enforce: "pre",
77
+ async transform(code, id) {
78
+ const file = id.split("?")[0];
79
+ if (!include.test(file) || file.includes("/node_modules/")) return null;
80
+ try {
81
+ const result = await core.transformAsync(code, {
82
+ filename: file,
83
+ babelrc: false,
84
+ configFile: false,
85
+ sourceMaps: true,
86
+ parserOpts: { plugins: ["jsx", "typescript"] },
87
+ plugins: [[stampLocBabel, babelOpts]]
88
+ });
89
+ if (!result?.code) return null;
90
+ return { code: result.code, map: result.map };
91
+ } catch (err) {
92
+ this.warn(`stamp-loc skipped ${file}: ${err instanceof Error ? err.message : String(err)}`);
93
+ return null;
94
+ }
95
+ }
96
+ };
97
+ }
98
+
99
+ exports.stampLocVite = stampLocVite;
100
+ //# sourceMappingURL=vite.cjs.map
101
+ //# sourceMappingURL=vite.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stampLocBabel.ts","../src/vite.ts"],"names":["transformAsync"],"mappings":"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","file":"vite.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","import { transformAsync } from '@babel/core';\nimport type { Plugin } from 'vite';\nimport stampLocBabel, { type StampLocOptions } from './stampLocBabel';\n\nexport interface StampLocViteOptions extends StampLocOptions {\n /** Какие файлы штамповать. Default /\\.[jt]sx$/. */\n include?: RegExp;\n}\n\n/**\n * Vite-плагин: штампует data-loc/data-comp на JSX host-элементы.\n *\n * `@vitejs/plugin-react` v6 транспилит через oxc (без babel-хука), поэтому\n * атрибуты добавляем отдельным pre-проходом babel'а (только парсинг + наш\n * плагин, JSX/TS сохраняются), а oxc уже делает остальное.\n *\n * Парс-ошибка в отдельном файле не валит сборку — файл просто остаётся без\n * штампов (warn в консоль).\n */\nexport function stampLocVite(opts: StampLocViteOptions = {}): Plugin {\n const include = opts.include ?? /\\.[jt]sx$/;\n const babelOpts: StampLocOptions = {\n attrLoc: opts.attrLoc,\n attrComp: opts.attrComp,\n rootDir: opts.rootDir\n };\n\n return {\n name: 'semantic-inspector:stamp-loc',\n enforce: 'pre',\n async transform(code, id) {\n const file = id.split('?')[0];\n if (!include.test(file) || file.includes('/node_modules/')) return null;\n\n try {\n const result = await transformAsync(code, {\n filename: file,\n babelrc: false,\n configFile: false,\n sourceMaps: true,\n parserOpts: { plugins: ['jsx', 'typescript'] },\n plugins: [[stampLocBabel, babelOpts]]\n });\n if (!result?.code) return null;\n return { code: result.code, map: result.map };\n } catch (err) {\n this.warn(`stamp-loc skipped ${file}: ${err instanceof Error ? err.message : String(err)}`);\n return null;\n }\n }\n };\n}\n"]}
@@ -0,0 +1,21 @@
1
+ import { Plugin } from 'vite';
2
+ import { StampLocOptions } from './babel.cjs';
3
+ import '@babel/core';
4
+
5
+ interface StampLocViteOptions extends StampLocOptions {
6
+ /** Какие файлы штамповать. Default /\.[jt]sx$/. */
7
+ include?: RegExp;
8
+ }
9
+ /**
10
+ * Vite-плагин: штампует data-loc/data-comp на JSX host-элементы.
11
+ *
12
+ * `@vitejs/plugin-react` v6 транспилит через oxc (без babel-хука), поэтому
13
+ * атрибуты добавляем отдельным pre-проходом babel'а (только парсинг + наш
14
+ * плагин, JSX/TS сохраняются), а oxc уже делает остальное.
15
+ *
16
+ * Парс-ошибка в отдельном файле не валит сборку — файл просто остаётся без
17
+ * штампов (warn в консоль).
18
+ */
19
+ declare function stampLocVite(opts?: StampLocViteOptions): Plugin;
20
+
21
+ export { type StampLocViteOptions, stampLocVite };
package/dist/vite.d.ts ADDED
@@ -0,0 +1,21 @@
1
+ import { Plugin } from 'vite';
2
+ import { StampLocOptions } from './babel.js';
3
+ import '@babel/core';
4
+
5
+ interface StampLocViteOptions extends StampLocOptions {
6
+ /** Какие файлы штамповать. Default /\.[jt]sx$/. */
7
+ include?: RegExp;
8
+ }
9
+ /**
10
+ * Vite-плагин: штампует data-loc/data-comp на JSX host-элементы.
11
+ *
12
+ * `@vitejs/plugin-react` v6 транспилит через oxc (без babel-хука), поэтому
13
+ * атрибуты добавляем отдельным pre-проходом babel'а (только парсинг + наш
14
+ * плагин, JSX/TS сохраняются), а oxc уже делает остальное.
15
+ *
16
+ * Парс-ошибка в отдельном файле не валит сборку — файл просто остаётся без
17
+ * штампов (warn в консоль).
18
+ */
19
+ declare function stampLocVite(opts?: StampLocViteOptions): Plugin;
20
+
21
+ export { type StampLocViteOptions, stampLocVite };
package/dist/vite.js ADDED
@@ -0,0 +1,38 @@
1
+ import { stampLocBabel } from './chunk-AAPCI2HO.js';
2
+ import { transformAsync } from '@babel/core';
3
+
4
+ function stampLocVite(opts = {}) {
5
+ const include = opts.include ?? /\.[jt]sx$/;
6
+ const babelOpts = {
7
+ attrLoc: opts.attrLoc,
8
+ attrComp: opts.attrComp,
9
+ rootDir: opts.rootDir
10
+ };
11
+ return {
12
+ name: "semantic-inspector:stamp-loc",
13
+ enforce: "pre",
14
+ async transform(code, id) {
15
+ const file = id.split("?")[0];
16
+ if (!include.test(file) || file.includes("/node_modules/")) return null;
17
+ try {
18
+ const result = await transformAsync(code, {
19
+ filename: file,
20
+ babelrc: false,
21
+ configFile: false,
22
+ sourceMaps: true,
23
+ parserOpts: { plugins: ["jsx", "typescript"] },
24
+ plugins: [[stampLocBabel, babelOpts]]
25
+ });
26
+ if (!result?.code) return null;
27
+ return { code: result.code, map: result.map };
28
+ } catch (err) {
29
+ this.warn(`stamp-loc skipped ${file}: ${err instanceof Error ? err.message : String(err)}`);
30
+ return null;
31
+ }
32
+ }
33
+ };
34
+ }
35
+
36
+ export { stampLocVite };
37
+ //# sourceMappingURL=vite.js.map
38
+ //# sourceMappingURL=vite.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/vite.ts"],"names":[],"mappings":";;;AAmBO,SAAS,YAAA,CAAa,IAAA,GAA4B,EAAC,EAAW;AACnE,EAAA,MAAM,OAAA,GAAU,KAAK,OAAA,IAAW,WAAA;AAChC,EAAA,MAAM,SAAA,GAA6B;AAAA,IACjC,SAAS,IAAA,CAAK,OAAA;AAAA,IACd,UAAU,IAAA,CAAK,QAAA;AAAA,IACf,SAAS,IAAA,CAAK;AAAA,GAChB;AAEA,EAAA,OAAO;AAAA,IACL,IAAA,EAAM,8BAAA;AAAA,IACN,OAAA,EAAS,KAAA;AAAA,IACT,MAAM,SAAA,CAAU,IAAA,EAAM,EAAA,EAAI;AACxB,MAAA,MAAM,IAAA,GAAO,EAAA,CAAG,KAAA,CAAM,GAAG,EAAE,CAAC,CAAA;AAC5B,MAAA,IAAI,CAAC,QAAQ,IAAA,CAAK,IAAI,KAAK,IAAA,CAAK,QAAA,CAAS,gBAAgB,CAAA,EAAG,OAAO,IAAA;AAEnE,MAAA,IAAI;AACF,QAAA,MAAM,MAAA,GAAS,MAAM,cAAA,CAAe,IAAA,EAAM;AAAA,UACxC,QAAA,EAAU,IAAA;AAAA,UACV,OAAA,EAAS,KAAA;AAAA,UACT,UAAA,EAAY,KAAA;AAAA,UACZ,UAAA,EAAY,IAAA;AAAA,UACZ,YAAY,EAAE,OAAA,EAAS,CAAC,KAAA,EAAO,YAAY,CAAA,EAAE;AAAA,UAC7C,OAAA,EAAS,CAAC,CAAC,aAAA,EAAe,SAAS,CAAC;AAAA,SACrC,CAAA;AACD,QAAA,IAAI,CAAC,MAAA,EAAQ,IAAA,EAAM,OAAO,IAAA;AAC1B,QAAA,OAAO,EAAE,IAAA,EAAM,MAAA,CAAO,IAAA,EAAM,GAAA,EAAK,OAAO,GAAA,EAAI;AAAA,MAC9C,SAAS,GAAA,EAAK;AACZ,QAAA,IAAA,CAAK,IAAA,CAAK,CAAA,kBAAA,EAAqB,IAAI,CAAA,EAAA,EAAK,GAAA,YAAe,KAAA,GAAQ,GAAA,CAAI,OAAA,GAAU,MAAA,CAAO,GAAG,CAAC,CAAA,CAAE,CAAA;AAC1F,QAAA,OAAO,IAAA;AAAA,MACT;AAAA,IACF;AAAA,GACF;AACF","file":"vite.js","sourcesContent":["import { transformAsync } from '@babel/core';\nimport type { Plugin } from 'vite';\nimport stampLocBabel, { type StampLocOptions } from './stampLocBabel';\n\nexport interface StampLocViteOptions extends StampLocOptions {\n /** Какие файлы штамповать. Default /\\.[jt]sx$/. */\n include?: RegExp;\n}\n\n/**\n * Vite-плагин: штампует data-loc/data-comp на JSX host-элементы.\n *\n * `@vitejs/plugin-react` v6 транспилит через oxc (без babel-хука), поэтому\n * атрибуты добавляем отдельным pre-проходом babel'а (только парсинг + наш\n * плагин, JSX/TS сохраняются), а oxc уже делает остальное.\n *\n * Парс-ошибка в отдельном файле не валит сборку — файл просто остаётся без\n * штампов (warn в консоль).\n */\nexport function stampLocVite(opts: StampLocViteOptions = {}): Plugin {\n const include = opts.include ?? /\\.[jt]sx$/;\n const babelOpts: StampLocOptions = {\n attrLoc: opts.attrLoc,\n attrComp: opts.attrComp,\n rootDir: opts.rootDir\n };\n\n return {\n name: 'semantic-inspector:stamp-loc',\n enforce: 'pre',\n async transform(code, id) {\n const file = id.split('?')[0];\n if (!include.test(file) || file.includes('/node_modules/')) return null;\n\n try {\n const result = await transformAsync(code, {\n filename: file,\n babelrc: false,\n configFile: false,\n sourceMaps: true,\n parserOpts: { plugins: ['jsx', 'typescript'] },\n plugins: [[stampLocBabel, babelOpts]]\n });\n if (!result?.code) return null;\n return { code: result.code, map: result.map };\n } catch (err) {\n this.warn(`stamp-loc skipped ${file}: ${err instanceof Error ? err.message : String(err)}`);\n return null;\n }\n }\n };\n}\n"]}