semantic-inspector 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  var react = require('react');
4
4
  var jsxRuntime = require('react/jsx-runtime');
5
- var modernScreenshot = require('modern-screenshot');
6
5
 
7
6
  // src/SemanticInspector.tsx
8
7
  var Z = 2147483600;
@@ -65,7 +64,10 @@ function tipStyle(r) {
65
64
  whiteSpace: "nowrap"
66
65
  };
67
66
  }
68
- function Overlay({ target, toast }) {
67
+ var Overlay = react.memo(function Overlay2({
68
+ target,
69
+ toast
70
+ }) {
69
71
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
70
72
  /* @__PURE__ */ jsxRuntime.jsx("div", { style: badge, children: "\u2316 inspect \xB7 click=name \xB7 \u21E7click=shot \xB7 Esc=exit" }),
71
73
  target && /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
@@ -80,12 +82,21 @@ function Overlay({ target, toast }) {
80
82
  ] }),
81
83
  toast && /* @__PURE__ */ jsxRuntime.jsx("div", { style: toastStyle, children: toast })
82
84
  ] });
83
- }
85
+ });
86
+
87
+ // src/clipboard.ts
84
88
  async function copyText(text) {
89
+ if (typeof navigator === "undefined" || !navigator.clipboard) {
90
+ throw new Error("Clipboard API unavailable (needs a secure context: https or localhost)");
91
+ }
85
92
  await navigator.clipboard.writeText(text);
86
93
  }
87
94
  async function copyElementShot(el) {
88
- const blob = await modernScreenshot.domToBlob(el);
95
+ if (typeof navigator === "undefined" || !navigator.clipboard?.write || typeof ClipboardItem === "undefined") {
96
+ throw new Error("Clipboard image write unsupported (needs a secure context: https or localhost)");
97
+ }
98
+ const { domToBlob } = await import('modern-screenshot');
99
+ const blob = await domToBlob(el, { scale: 1 });
89
100
  if (!blob) throw new Error("screenshot produced empty blob");
90
101
  await navigator.clipboard.write([new ClipboardItem({ "image/png": blob })]);
91
102
  }
@@ -108,9 +119,10 @@ function fallbackName(el, loc) {
108
119
  return el.tagName.toLowerCase();
109
120
  }
110
121
  function fiberName(el) {
111
- const key = Object.keys(el).find((k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$"));
122
+ const host = el;
123
+ const key = Object.keys(host).find((k) => k.startsWith("__reactFiber$") || k.startsWith("__reactInternalInstance$"));
112
124
  if (!key) return null;
113
- let fiber = el[key] ?? null;
125
+ let fiber = host[key] ?? null;
114
126
  while (fiber) {
115
127
  const t = fiber.type;
116
128
  const name = t && typeof t !== "string" ? t.displayName ?? t.name : void 0;
@@ -125,15 +137,42 @@ var DEFAULT_HOTKEY = "Alt+Shift+S";
125
137
  function defaultFormat(t) {
126
138
  return t.loc ? `${t.comp} \u2014 ${t.loc}` : t.comp;
127
139
  }
128
- function matchHotkey(e, hotkey) {
140
+ function parseHotkey(hotkey) {
129
141
  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}`;
142
+ const has = (...names) => names.some((n) => parts.includes(n));
143
+ const last = parts[parts.length - 1];
144
+ return {
145
+ alt: has("alt"),
146
+ shift: has("shift"),
147
+ ctrl: has("ctrl", "control"),
148
+ meta: has("meta", "cmd"),
149
+ key: last === "" ? "+" : last
150
+ };
151
+ }
152
+ var CODE_TO_KEY = {
153
+ slash: "/",
154
+ backslash: "\\",
155
+ period: ".",
156
+ comma: ",",
157
+ semicolon: ";",
158
+ quote: "'",
159
+ backquote: "`",
160
+ bracketleft: "[",
161
+ bracketright: "]",
162
+ minus: "-",
163
+ equal: "="
164
+ };
165
+ function matchHotkey(e, hk) {
166
+ if (e.altKey !== hk.alt || e.shiftKey !== hk.shift || e.ctrlKey !== hk.ctrl || e.metaKey !== hk.meta) {
167
+ return false;
168
+ }
169
+ const rawCode = e.code.toLowerCase();
170
+ const code = CODE_TO_KEY[rawCode] ?? rawCode.replace(/^(key|digit)/, "");
171
+ return e.key.toLowerCase() === hk.key || code === hk.key;
172
+ }
173
+ function sameTarget(a, b) {
174
+ if (!a || !b) return a === b;
175
+ return a.el === b.el && a.rect.left === b.rect.left && a.rect.top === b.rect.top && a.rect.width === b.rect.width && a.rect.height === b.rect.height;
137
176
  }
138
177
  function useInspector(opts = {}) {
139
178
  const { hotkey = DEFAULT_HOTKEY } = opts;
@@ -141,68 +180,77 @@ function useInspector(opts = {}) {
141
180
  const [target, setTarget] = react.useState(null);
142
181
  const cbRef = react.useRef(opts);
143
182
  cbRef.current = opts;
183
+ const targetRef = react.useRef(null);
184
+ const hk = react.useMemo(() => parseHotkey(hotkey), [hotkey]);
144
185
  react.useEffect(() => {
145
186
  function onKey(e) {
146
- if (matchHotkey(e, hotkey)) {
187
+ if (matchHotkey(e, hk)) {
147
188
  e.preventDefault();
148
189
  setActive((a) => !a);
149
190
  } else if (e.key === "Escape") {
150
- setActive(false);
191
+ setActive((a) => a ? false : a);
151
192
  }
152
193
  }
153
194
  window.addEventListener("keydown", onKey);
154
- return () => {
155
- window.removeEventListener("keydown", onKey);
156
- };
157
- }, [hotkey]);
195
+ return () => window.removeEventListener("keydown", onKey);
196
+ }, [hk]);
158
197
  react.useEffect(() => {
159
198
  if (!active) {
199
+ targetRef.current = null;
160
200
  setTarget(null);
161
201
  return;
162
202
  }
203
+ let rafId = 0;
204
+ let lastX = 0;
205
+ let lastY = 0;
206
+ let shotInFlight = false;
163
207
  function onMove(e) {
164
- setTarget(resolveTarget(document.elementFromPoint(e.clientX, e.clientY)));
208
+ lastX = e.clientX;
209
+ lastY = e.clientY;
210
+ if (rafId) return;
211
+ rafId = requestAnimationFrame(() => {
212
+ rafId = 0;
213
+ const next = resolveTarget(document.elementFromPoint(lastX, lastY));
214
+ targetRef.current = next;
215
+ setTarget((prev) => sameTarget(prev, next) ? prev : next);
216
+ });
165
217
  }
166
218
  function onClick(e) {
167
- const t = resolveTarget(document.elementFromPoint(e.clientX, e.clientY));
219
+ const t = targetRef.current ?? resolveTarget(document.elementFromPoint(e.clientX, e.clientY));
168
220
  if (!t) return;
169
221
  e.preventDefault();
170
222
  e.stopPropagation();
171
223
  const { formatText = defaultFormat, onCopy, onError } = cbRef.current;
172
- const done = (kind, payload) => {
173
- onCopy?.(kind, payload);
174
- };
224
+ const done = (kind, payload) => onCopy?.(kind, payload);
175
225
  const fail = (kind, err) => {
176
- onError?.(kind, err);
226
+ if (onError) onError(kind, err);
227
+ else console.warn(`[semantic-inspector] ${kind} copy failed:`, err);
177
228
  };
178
229
  if (e.shiftKey) {
230
+ if (shotInFlight) return;
231
+ shotInFlight = true;
179
232
  copyElementShot(t.el).then(
180
- () => {
181
- done("screenshot", t.comp);
182
- },
183
- (err) => {
184
- fail("screenshot", err);
185
- }
186
- );
233
+ () => done("screenshot", t.comp),
234
+ (err) => fail("screenshot", err)
235
+ ).finally(() => {
236
+ shotInFlight = false;
237
+ });
187
238
  } else {
188
239
  const text = formatText({ comp: t.comp, loc: t.loc });
189
240
  copyText(text).then(
190
- () => {
191
- done("text", text);
192
- },
193
- (err) => {
194
- fail("text", err);
195
- }
241
+ () => done("text", text),
242
+ (err) => fail("text", err)
196
243
  );
197
244
  }
198
245
  }
199
- window.addEventListener("mousemove", onMove, true);
246
+ window.addEventListener("mousemove", onMove, { capture: true, passive: true });
200
247
  window.addEventListener("click", onClick, true);
201
248
  const prevCursor = document.body.style.cursor;
202
249
  document.body.style.cursor = "crosshair";
203
250
  return () => {
204
251
  window.removeEventListener("mousemove", onMove, true);
205
252
  window.removeEventListener("click", onClick, true);
253
+ if (rafId) cancelAnimationFrame(rafId);
206
254
  document.body.style.cursor = prevCursor;
207
255
  };
208
256
  }, [active]);
@@ -211,23 +259,20 @@ function useInspector(opts = {}) {
211
259
  var TOAST_MS = 1400;
212
260
  function SemanticInspector(props) {
213
261
  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
- };
262
+ react.useEffect(() => {
263
+ if (toast == null) return;
264
+ const id = setTimeout(() => setToast(null), TOAST_MS);
265
+ return () => clearTimeout(id);
266
+ }, [toast]);
222
267
  const { active, target } = useInspector({
223
268
  hotkey: props.hotkey,
224
269
  formatText: props.formatText,
225
270
  onCopy: (kind, payload) => {
226
- flash(kind === "text" ? `\u2713 ${payload}` : "\u2713 screenshot copied");
271
+ setToast(kind === "text" ? `\u2713 ${payload}` : "\u2713 screenshot copied");
227
272
  props.onCopy?.(kind, payload);
228
273
  },
229
274
  onError: (kind, err) => {
230
- flash(`\u2717 ${kind} failed`);
275
+ setToast(`\u2717 ${kind} failed`);
231
276
  props.onError?.(kind, err);
232
277
  }
233
278
  });
@@ -236,9 +281,6 @@ function SemanticInspector(props) {
236
281
  }
237
282
 
238
283
  exports.SemanticInspector = SemanticInspector;
239
- exports.copyElementShot = copyElementShot;
240
- exports.copyText = copyText;
241
- exports.resolveTarget = resolveTarget;
242
284
  exports.useInspector = useInspector;
243
285
  //# sourceMappingURL=index.cjs.map
244
286
  //# sourceMappingURL=index.cjs.map
@@ -1 +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"]}
1
+ {"version":3,"sources":["../src/Overlay.tsx","../src/clipboard.ts","../src/resolveTarget.ts","../src/useInspector.ts","../src/SemanticInspector.tsx"],"names":["memo","Overlay","jsxs","Fragment","jsx","useState","useRef","useMemo","useEffect"],"mappings":"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","file":"index.cjs","sourcesContent":["import { type CSSProperties, type JSX, type MemoExoticComponent, memo } from 'react';\nimport type { InspectTarget } from './types';\n\n// Just below the 32-bit max so the overlay wins stacking contests, with a small reserved band:\n// Z = highlight box, Z+1 = tip, Z+2 = badge/toast.\nconst Z = 2147483600;\n\nconst badge = {\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} satisfies CSSProperties;\n\nconst toastStyle = {\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} satisfies CSSProperties;\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 } satisfies CSSProperties;\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 } satisfies CSSProperties;\n}\n\ninterface OverlayProps {\n target: InspectTarget | null;\n toast: string | null;\n}\n\nexport const Overlay: MemoExoticComponent<(props: OverlayProps) => JSX.Element> = memo(function Overlay({\n target,\n toast\n}: OverlayProps): JSX.Element {\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","/** Copy text to the clipboard. Requires a secure context (https or localhost). */\nexport async function copyText(text: string): Promise<void> {\n if (typeof navigator === 'undefined' || !navigator.clipboard) {\n throw new Error('Clipboard API unavailable (needs a secure context: https or localhost)');\n }\n await navigator.clipboard.writeText(text);\n}\n\n/**\n * Copy a PNG screenshot of ONLY the given element to the clipboard (image/png).\n * Must be called from a user gesture (click), or the browser blocks the image write.\n *\n * `modern-screenshot` is imported lazily so it is emitted as a separate chunk and never lands\n * in a consumer's production bundle through the runtime entry.\n */\nexport async function copyElementShot(el: Element): Promise<void> {\n if (typeof navigator === 'undefined' || !navigator.clipboard?.write || typeof ClipboardItem === 'undefined') {\n throw new Error('Clipboard image write unsupported (needs a secure context: https or localhost)');\n }\n const { domToBlob } = await import('modern-screenshot');\n // scale: 1 avoids the 4x rasterization cost of devicePixelRatio on Retina displays.\n const blob = await domToBlob(el as HTMLElement, { scale: 1 });\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 element under the cursor → inspection target.\n *\n * Walks to the nearest ancestor carrying data-loc (stamped by the Babel plugin). If none\n * exists (prod build without stamps / foreign node), it falls back best-effort to: the name\n * from the React fiber, then the file name from data-loc, then the tag name.\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 was removed in React 19, but the component name from\n// fiber.type is still available (not minified in dev builds).\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 host = el as Element & Record<string, FiberLike | undefined>;\n const key = Object.keys(host).find((k) => k.startsWith('__reactFiber$') || k.startsWith('__reactInternalInstance$'));\n if (!key) return null;\n let fiber: FiberLike | null = host[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, useMemo, useRef, useState } from 'react';\nimport { copyElementShot, copyText } from './clipboard';\nimport { resolveTarget } from './resolveTarget';\nimport type { CopyKind, InspectTarget, LocInfo, SemanticInspectorProps, UseInspectorResult } from './types';\n\nconst DEFAULT_HOTKEY = 'Alt+Shift+S';\n\nfunction defaultFormat(t: LocInfo): string {\n return t.loc ? `${t.comp} — ${t.loc}` : t.comp;\n}\n\ninterface Hotkey {\n alt: boolean;\n shift: boolean;\n ctrl: boolean;\n meta: boolean;\n key: string;\n}\n\n/** Parse 'Alt+Shift+S' into a descriptor. The final token is the key (empty → literal '+'). */\nfunction parseHotkey(hotkey: string): Hotkey {\n const parts = hotkey.split('+').map((p) => p.trim().toLowerCase());\n const has = (...names: string[]): boolean => names.some((n) => parts.includes(n));\n const last = parts[parts.length - 1];\n return {\n alt: has('alt'),\n shift: has('shift'),\n ctrl: has('ctrl', 'control'),\n meta: has('meta', 'cmd'),\n key: last === '' ? '+' : last\n };\n}\n\n// Physical-key codes whose token differs from the produced character, so a hotkey written with\n// the unshifted glyph still matches when Shift transforms it (e.g. Ctrl+Shift+/ → e.key '?').\nconst CODE_TO_KEY: Record<string, string> = {\n slash: '/',\n backslash: '\\\\',\n period: '.',\n comma: ',',\n semicolon: ';',\n quote: \"'\",\n backquote: '`',\n bracketleft: '[',\n bracketright: ']',\n minus: '-',\n equal: '='\n};\n\n/** Whether a keydown event matches the parsed hotkey. */\nfunction matchHotkey(e: KeyboardEvent, hk: Hotkey): boolean {\n if (e.altKey !== hk.alt || e.shiftKey !== hk.shift || e.ctrlKey !== hk.ctrl || e.metaKey !== hk.meta) {\n return false;\n }\n // Normalize the physical code (KeyA → a, Digit1 → 1, Slash → /) so non-letter keys match too.\n const rawCode = e.code.toLowerCase();\n const code = CODE_TO_KEY[rawCode] ?? rawCode.replace(/^(key|digit)/, '');\n return e.key.toLowerCase() === hk.key || code === hk.key;\n}\n\nfunction sameTarget(a: InspectTarget | null, b: InspectTarget | null): boolean {\n if (!a || !b) return a === b;\n return (\n a.el === b.el &&\n a.rect.left === b.rect.left &&\n a.rect.top === b.rect.top &&\n a.rect.width === b.rect.width &&\n a.rect.height === b.rect.height\n );\n}\n\n/**\n * Inspection-mode state + listeners.\n * - keydown: the hotkey toggles `active`; Esc exits.\n * - active: mousemove (rAF-coalesced) updates `target`; click (capture, preventDefault) copies\n * text, Shift+click copies an element screenshot.\n */\nexport function useInspector(opts: SemanticInspectorProps = {}): UseInspectorResult {\n const { hotkey = DEFAULT_HOTKEY } = opts;\n const [active, setActive] = useState(false);\n const [target, setTarget] = useState<InspectTarget | null>(null);\n\n // Fresh callbacks without re-subscribing listeners.\n const cbRef = useRef<SemanticInspectorProps>(opts);\n cbRef.current = opts;\n\n // Latest hovered target, so the click handler copies exactly what is highlighted.\n const targetRef = useRef<InspectTarget | null>(null);\n\n const hk = useMemo(() => parseHotkey(hotkey), [hotkey]);\n\n useEffect(() => {\n function onKey(e: KeyboardEvent): void {\n if (matchHotkey(e, hk)) {\n e.preventDefault();\n setActive((a) => !a);\n } else if (e.key === 'Escape') {\n setActive((a) => (a ? false : a));\n }\n }\n window.addEventListener('keydown', onKey);\n return () => window.removeEventListener('keydown', onKey);\n }, [hk]);\n\n useEffect(() => {\n if (!active) {\n targetRef.current = null;\n setTarget(null);\n return;\n }\n\n let rafId = 0;\n let lastX = 0;\n let lastY = 0;\n let shotInFlight = false;\n\n function onMove(e: MouseEvent): void {\n lastX = e.clientX;\n lastY = e.clientY;\n if (rafId) return; // one update per frame, regardless of input rate\n rafId = requestAnimationFrame(() => {\n rafId = 0;\n const next = resolveTarget(document.elementFromPoint(lastX, lastY));\n targetRef.current = next;\n setTarget((prev) => (sameTarget(prev, next) ? prev : next));\n });\n }\n\n function onClick(e: MouseEvent): void {\n const t = targetRef.current ?? 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): void => onCopy?.(kind, payload);\n const fail = (kind: CopyKind, err: unknown): void => {\n if (onError) onError(kind, err);\n // Surface failures even without an onError handler (console is the right channel for a dev tool).\n else console.warn(`[semantic-inspector] ${kind} copy failed:`, err);\n };\n\n if (e.shiftKey) {\n if (shotInFlight) return; // ignore overlapping captures\n shotInFlight = true;\n copyElementShot(t.el)\n .then(\n () => done('screenshot', t.comp),\n (err: unknown) => fail('screenshot', err)\n )\n .finally(() => {\n shotInFlight = false;\n });\n } else {\n const text = formatText({ comp: t.comp, loc: t.loc });\n copyText(text).then(\n () => done('text', text),\n (err: unknown) => fail('text', err)\n );\n }\n }\n\n window.addEventListener('mousemove', onMove, { capture: true, passive: 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 if (rafId) cancelAnimationFrame(rafId);\n document.body.style.cursor = prevCursor;\n };\n }, [active]);\n\n return { active, target };\n}\n","import { type JSX, useEffect, useState } from 'react';\nimport { Overlay } from './Overlay';\nimport type { CopyKind, SemanticInspectorProps } from './types';\nimport { useInspector } from './useInspector';\n\n// How long the copy toast stays visible (ms).\nconst TOAST_MS = 1400;\n\n/**\n * Semantic inspector. Renders nothing until toggled by the hotkey. Gating (where to mount) is\n * the consumer's responsibility: mount it under your dev flag, ideally via React.lazy, so it is\n * not pulled into the production bundle.\n */\nexport function SemanticInspector(props: SemanticInspectorProps): JSX.Element | null {\n const [toast, setToast] = useState<string | null>(null);\n\n // Auto-hide the toast; cleanup cancels the pending timer (including on unmount).\n // Note: two identical consecutive toasts share one window — acceptable for a dev tool.\n useEffect(() => {\n if (toast == null) return;\n const id = setTimeout(() => setToast(null), TOAST_MS);\n return () => clearTimeout(id);\n }, [toast]);\n\n const { active, target } = useInspector({\n hotkey: props.hotkey,\n formatText: props.formatText,\n onCopy: (kind: CopyKind, payload: string) => {\n setToast(kind === 'text' ? `✓ ${payload}` : '✓ screenshot copied');\n props.onCopy?.(kind, payload);\n },\n onError: (kind: CopyKind, err: unknown) => {\n setToast(`✗ ${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/index.d.cts CHANGED
@@ -1,62 +1,58 @@
1
- import * as react from 'react';
1
+ import { JSX } from 'react';
2
2
 
3
3
  type CopyKind = 'text' | 'screenshot';
4
- interface InspectTarget {
5
- /** Имя компонента (data-comp) или fallback (fiber displayName / имя файла / тег). */
4
+ /** Component name + source location resolved for an inspected element. */
5
+ interface LocInfo {
6
+ /** Component name (from data-comp) or a fallback (fiber displayName / file name / tag). */
6
7
  comp: string;
7
- /** "<path>:<line>" из data-loc, либо null если элемент не заштампован. */
8
+ /** "<path>:<line>:<col>" from data-loc, or null when the element is not stamped. */
8
9
  loc: string | null;
9
- /** Реальный DOM-элемент (ближайший с data-loc, либо сам). */
10
+ }
11
+ interface InspectTarget extends LocInfo {
12
+ /** The resolved DOM element (nearest ancestor with data-loc, or the element itself). */
10
13
  el: Element;
11
- /** Геометрия для оверлея. */
14
+ /** Geometry for the overlay highlight. */
12
15
  rect: DOMRect;
13
16
  }
14
17
  interface SemanticInspectorProps {
15
- /** Хоткей-toggle. Default 'Alt+Shift+S'. Формат: 'Alt+Shift+S', 'Ctrl+Cmd+I'. */
18
+ /**
19
+ * Toggle hotkey. Default 'Alt+Shift+S'.
20
+ *
21
+ * Format: modifiers (`Alt`, `Shift`, `Ctrl`/`Control`, `Meta`/`Cmd`) joined with `+`
22
+ * followed by a final key, e.g. 'Ctrl+Cmd+I'. Matching is case-insensitive and also
23
+ * accepts the physical `event.code` (so layout-shifted glyphs still work). `Esc` always exits.
24
+ */
16
25
  hotkey?: string;
17
- /** Формат текста для буфера. Default: `${comp} — ${loc}` (или `${comp}` без loc). */
18
- formatText?: (t: {
19
- comp: string;
20
- loc: string | null;
21
- }) => string;
22
- /** Колбэк после успешной копии — для телеметрии/тостов апа. */
26
+ /** Formats the clipboard text. Default: `${comp} — ${loc}` (or `${comp}` when loc is null). */
27
+ formatText?: (t: LocInfo) => string;
28
+ /**
29
+ * Called after a successful copy. For kind `'text'`, `payload` is the copied string; for
30
+ * kind `'screenshot'`, `payload` is the component name (the PNG itself goes to the clipboard,
31
+ * not to this callback).
32
+ */
23
33
  onCopy?: (kind: CopyKind, payload: string) => void;
24
- /** Колбэк при ошибке копии (clipboard reject / screenshot fail). */
34
+ /** Called when a copy fails (clipboard rejection / screenshot failure). */
25
35
  onError?: (kind: CopyKind, err: unknown) => void;
26
36
  }
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): {
37
+ /** Return value of `useInspector`. */
38
+ interface UseInspectorResult {
42
39
  active: boolean;
43
40
  target: InspectTarget | null;
44
- };
41
+ }
45
42
 
46
43
  /**
47
- * DOM-элемент под курсором цель инспекции.
48
- * Идём к ближайшему предку с data-loc (заштампован babel-плагином). Если его нет
49
- * (prod-билд без штампов / сторонний узел) — best-effort: имя из React fiber,
50
- * затем имя файла из data-loc, затем имя тега.
44
+ * Semantic inspector. Renders nothing until toggled by the hotkey. Gating (where to mount) is
45
+ * the consumer's responsibility: mount it under your dev flag, ideally via React.lazy, so it is
46
+ * not pulled into the production bundle.
51
47
  */
52
- declare function resolveTarget(el: Element | null): InspectTarget | null;
48
+ declare function SemanticInspector(props: SemanticInspectorProps): JSX.Element | null;
53
49
 
54
- /** Текст в буфер. */
55
- declare function copyText(text: string): Promise<void>;
56
50
  /**
57
- * PNG-скриншот ТОЛЬКО переданного элемента в буфер (image/png).
58
- * Должен вызываться из user-gesture (клик), иначе браузер блокит image-копию.
51
+ * Inspection-mode state + listeners.
52
+ * - keydown: the hotkey toggles `active`; Esc exits.
53
+ * - active: mousemove (rAF-coalesced) updates `target`; click (capture, preventDefault) copies
54
+ * text, Shift+click copies an element screenshot.
59
55
  */
60
- declare function copyElementShot(el: Element): Promise<void>;
56
+ declare function useInspector(opts?: SemanticInspectorProps): UseInspectorResult;
61
57
 
62
- export { type CopyKind, type InspectTarget, SemanticInspector, type SemanticInspectorProps, copyElementShot, copyText, resolveTarget, useInspector };
58
+ export { type CopyKind, type InspectTarget, type LocInfo, SemanticInspector, type SemanticInspectorProps, type UseInspectorResult, useInspector };
package/dist/index.d.ts CHANGED
@@ -1,62 +1,58 @@
1
- import * as react from 'react';
1
+ import { JSX } from 'react';
2
2
 
3
3
  type CopyKind = 'text' | 'screenshot';
4
- interface InspectTarget {
5
- /** Имя компонента (data-comp) или fallback (fiber displayName / имя файла / тег). */
4
+ /** Component name + source location resolved for an inspected element. */
5
+ interface LocInfo {
6
+ /** Component name (from data-comp) or a fallback (fiber displayName / file name / tag). */
6
7
  comp: string;
7
- /** "<path>:<line>" из data-loc, либо null если элемент не заштампован. */
8
+ /** "<path>:<line>:<col>" from data-loc, or null when the element is not stamped. */
8
9
  loc: string | null;
9
- /** Реальный DOM-элемент (ближайший с data-loc, либо сам). */
10
+ }
11
+ interface InspectTarget extends LocInfo {
12
+ /** The resolved DOM element (nearest ancestor with data-loc, or the element itself). */
10
13
  el: Element;
11
- /** Геометрия для оверлея. */
14
+ /** Geometry for the overlay highlight. */
12
15
  rect: DOMRect;
13
16
  }
14
17
  interface SemanticInspectorProps {
15
- /** Хоткей-toggle. Default 'Alt+Shift+S'. Формат: 'Alt+Shift+S', 'Ctrl+Cmd+I'. */
18
+ /**
19
+ * Toggle hotkey. Default 'Alt+Shift+S'.
20
+ *
21
+ * Format: modifiers (`Alt`, `Shift`, `Ctrl`/`Control`, `Meta`/`Cmd`) joined with `+`
22
+ * followed by a final key, e.g. 'Ctrl+Cmd+I'. Matching is case-insensitive and also
23
+ * accepts the physical `event.code` (so layout-shifted glyphs still work). `Esc` always exits.
24
+ */
16
25
  hotkey?: string;
17
- /** Формат текста для буфера. Default: `${comp} — ${loc}` (или `${comp}` без loc). */
18
- formatText?: (t: {
19
- comp: string;
20
- loc: string | null;
21
- }) => string;
22
- /** Колбэк после успешной копии — для телеметрии/тостов апа. */
26
+ /** Formats the clipboard text. Default: `${comp} — ${loc}` (or `${comp}` when loc is null). */
27
+ formatText?: (t: LocInfo) => string;
28
+ /**
29
+ * Called after a successful copy. For kind `'text'`, `payload` is the copied string; for
30
+ * kind `'screenshot'`, `payload` is the component name (the PNG itself goes to the clipboard,
31
+ * not to this callback).
32
+ */
23
33
  onCopy?: (kind: CopyKind, payload: string) => void;
24
- /** Колбэк при ошибке копии (clipboard reject / screenshot fail). */
34
+ /** Called when a copy fails (clipboard rejection / screenshot failure). */
25
35
  onError?: (kind: CopyKind, err: unknown) => void;
26
36
  }
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): {
37
+ /** Return value of `useInspector`. */
38
+ interface UseInspectorResult {
42
39
  active: boolean;
43
40
  target: InspectTarget | null;
44
- };
41
+ }
45
42
 
46
43
  /**
47
- * DOM-элемент под курсором цель инспекции.
48
- * Идём к ближайшему предку с data-loc (заштампован babel-плагином). Если его нет
49
- * (prod-билд без штампов / сторонний узел) — best-effort: имя из React fiber,
50
- * затем имя файла из data-loc, затем имя тега.
44
+ * Semantic inspector. Renders nothing until toggled by the hotkey. Gating (where to mount) is
45
+ * the consumer's responsibility: mount it under your dev flag, ideally via React.lazy, so it is
46
+ * not pulled into the production bundle.
51
47
  */
52
- declare function resolveTarget(el: Element | null): InspectTarget | null;
48
+ declare function SemanticInspector(props: SemanticInspectorProps): JSX.Element | null;
53
49
 
54
- /** Текст в буфер. */
55
- declare function copyText(text: string): Promise<void>;
56
50
  /**
57
- * PNG-скриншот ТОЛЬКО переданного элемента в буфер (image/png).
58
- * Должен вызываться из user-gesture (клик), иначе браузер блокит image-копию.
51
+ * Inspection-mode state + listeners.
52
+ * - keydown: the hotkey toggles `active`; Esc exits.
53
+ * - active: mousemove (rAF-coalesced) updates `target`; click (capture, preventDefault) copies
54
+ * text, Shift+click copies an element screenshot.
59
55
  */
60
- declare function copyElementShot(el: Element): Promise<void>;
56
+ declare function useInspector(opts?: SemanticInspectorProps): UseInspectorResult;
61
57
 
62
- export { type CopyKind, type InspectTarget, SemanticInspector, type SemanticInspectorProps, copyElementShot, copyText, resolveTarget, useInspector };
58
+ export { type CopyKind, type InspectTarget, type LocInfo, SemanticInspector, type SemanticInspectorProps, type UseInspectorResult, useInspector };