reflet-sdk 0.6.2 → 0.6.4

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/CHANGELOG.md CHANGED
@@ -5,6 +5,32 @@ All notable changes to `reflet-sdk` will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.6.4] - 2026-10-04
9
+
10
+ ### Added
11
+
12
+ - Click a screenshot thumbnail in the widget to open it full size, the same view the pencil opens.
13
+
14
+ ### Fixed
15
+
16
+ - Picked-element highlights and devtools close-ups land on the element on Windows and Linux. A classic scrollbar used to shift them left, by up to its own width near the right edge.
17
+ - Letter hotkeys work with macOS Option (⌥F types "ƒ") and on non-Latin keyboard layouts, for both the `hotkey` prop and the picker's ⌘C / Ctrl+C.
18
+ - Pressing Enter to confirm Japanese, Chinese or Korean input no longer submits the picker note, the devtools note or a text annotation. Pressing Escape to cancel the input no longer closes the picker note, a text annotation, the devtools panel or a note pin either.
19
+ - Right-click no longer picks an element, and the page's context menu no longer opens while picking.
20
+ - "Open in editor" links work for Windows paths (`vscode://file/C:/…`), and devtools resolves `file:///C:/…`, Vite `/@fs/C:/…` and `/C:/…` source frames.
21
+ - Devtools refuses Server Component chunks on another Windows drive as outside the repository.
22
+
23
+ ## [0.6.3] - 2026-10-04
24
+
25
+ ### Added
26
+
27
+ - Copy an element straight from the devtools picker, React Grab style: aim at it and press ⌘C (Ctrl+C), or press Copy on the note card to include your note. The clipboard gets the element, its source location, owner components, selector and HTML.
28
+
29
+ ### Fixed
30
+
31
+ - The element picker and its hover outline stay above every layer of the page, so elements inside a fixed sidebar or header are outlined instead of hidden under it.
32
+ - The note card and hover label stay on screen when the picked element is taller than the window: the card tries below, above, then beside the element, and overlaps it only when nothing else fits.
33
+
8
34
  ## [0.6.2] - 2026-10-04
9
35
 
10
36
  ### Fixed
package/README.md CHANGED
@@ -190,7 +190,9 @@ turn it off.
190
190
  "View code" button, and Shift+click (or Shift+Enter) skips the note and opens
191
191
  the source file with the JSX element highlighted, ready to jump to in your
192
192
  editor. Locations come from React's debug stacks resolved through the dev
193
- server's source maps, Server Components included.
193
+ server's source maps, Server Components included. Press ⌘C (Ctrl+C) on the
194
+ aimed element, or Copy on the note card, to put the element and its source on
195
+ the clipboard for a coding agent, without saving a note.
194
196
  - **Notes** — a note appears the moment you press Enter, as a numbered pin on
195
197
  its element, Figma-style; click the pin to read, edit or delete it, and find
196
198
  the same number on its card in the panel. Its source location and a
@@ -1,6 +1,6 @@
1
1
  import { a as EDITORS, n as DEVTOOLS_REQUEST_HEADER, r as DEVTOOLS_ROUTE_BASE, t as DEVTOOLS_ENDPOINTS } from "./protocol-ClqTFzwR.js";
2
- import { A as TargetIcon, B as buildElementSelection, C as GripIcon, H as getFiberFromNode, I as DEFAULT_WIDGET_LABELS, J as releaseCapture, K as captureViewport, L as SDK_VERSION, R as collectPageContext, S as CloseIcon, U as resolveComponentStack, V as describeElement, W as shortenSourcePath, a as deriveTitle, c as useFloatingPosition, d as SelectionOutline, k as Svg, l as onViewportChange, m as listenToKeydown, n as isWidgetOwned, p as listenOnPageAndWidget, q as capturedFromCanvas, s as uploadImageBlob, t as ElementPicker, u as visibleViewport } from "./picker-DCoAFX5y.js";
3
- import { t as REFLET_Z_INDEX } from "./z-index-BL0SGZEP.js";
2
+ import { B as collectPageContext, G as resolveComponentStack, H as buildElementSelection, J as captureViewport, K as shortenSourcePath, M as TargetIcon, R as DEFAULT_WIDGET_LABELS, T as GripIcon, U as describeElement, W as getFiberFromNode, X as capturedViewportSize, Y as capturedFromCanvas, Z as releaseCapture, a as uploadImageBlob, c as useFloatingPosition, d as SelectionOutline, g as isSubmitEnter, j as Svg, l as onViewportChange, m as listenToKeydown, o as ElementPicker, p as listenOnPageAndWidget, r as deriveTitle, s as isWidgetOwned, u as visibleViewport, w as CloseIcon, z as SDK_VERSION } from "./submit-Cd-g1Dtg.js";
3
+ import { a as REFLET_Z_INDEX } from "./react-context-Caq1fodf.js";
4
4
  import * as G$1 from "react";
5
5
  import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
@@ -142,16 +142,23 @@ function selectionLines(selection) {
142
142
  if (selection.html) lines.push("HTML:", ...fenced("html", selection.html));
143
143
  return lines;
144
144
  }
145
+ function elementContext(note) {
146
+ return [`Page: ${note.capturedContext.url ?? "unknown"}`, ...selectionLines(note.selection)];
147
+ }
145
148
  function notesToPrompt(notes) {
146
149
  const sections = notes.map((note, index) => [
147
150
  `## Note ${index + 1}`,
148
151
  note.note || "(no comment)",
149
152
  "",
150
- `Page: ${note.capturedContext.url ?? "unknown"}`,
151
- ...selectionLines(note.selection)
153
+ ...elementContext(note)
152
154
  ].join("\n"));
153
155
  return [NOTES_BRIEF, ...sections].join("\n\n");
154
156
  }
157
+ /** No brief: pasted into a prompt the developer writes around it. */
158
+ function noteAsContext(note) {
159
+ const context = elementContext(note).join("\n");
160
+ return note.note ? `${note.note}\n\n${context}` : context;
161
+ }
155
162
  function boardFeedbackToPrompt(item) {
156
163
  const selections = item.context?.selections ?? [];
157
164
  const comments = selections.map((selection) => selection.comment).filter(Boolean);
@@ -449,6 +456,7 @@ function InboxPanel({ onOpenCode, onShowSelector }) {
449
456
  }
450
457
  //#endregion
451
458
  //#region src/devtools/client/notices.tsx
459
+ const STATUS_VISIBLE_MS = 2400;
452
460
  let notices = [];
453
461
  let nextNoticeId = 1;
454
462
  const listeners = /* @__PURE__ */ new Set();
@@ -464,10 +472,20 @@ function reportFailure(action, error, retry) {
464
472
  const message = `${action}: ${error instanceof Error ? error.message : "Unknown error."}`;
465
473
  publish([...notices.filter((notice) => notice.message !== message), {
466
474
  id: nextNoticeId++,
475
+ kind: "failure",
467
476
  message,
468
477
  retry
469
478
  }]);
470
479
  }
480
+ function announce(message) {
481
+ const id = nextNoticeId++;
482
+ publish([...notices.filter((notice) => notice.message !== message), {
483
+ id,
484
+ kind: "status",
485
+ message
486
+ }]);
487
+ setTimeout(() => dismissNotice(id), STATUS_VISIBLE_MS);
488
+ }
471
489
  function subscribe(listener) {
472
490
  listeners.add(listener);
473
491
  return () => listeners.delete(listener);
@@ -479,7 +497,8 @@ function Notices() {
479
497
  className: "dt-notices",
480
498
  children: current.map((notice) => /* @__PURE__ */ jsxs("div", {
481
499
  className: "dt-notice glass",
482
- role: "alert",
500
+ "data-kind": notice.kind,
501
+ role: notice.kind === "failure" ? "alert" : "status",
483
502
  children: [
484
503
  /* @__PURE__ */ jsx("p", { children: notice.message }),
485
504
  notice.retry && /* @__PURE__ */ jsx("button", {
@@ -634,8 +653,25 @@ function BoardTab({ canConnect, onOpenCode, onShowSelector, publicKey, refresh,
634
653
  }), inbox] });
635
654
  }
636
655
  //#endregion
656
+ //#region src/devtools/client/clipboard.ts
657
+ /**
658
+ * Safari only lets a page write the clipboard inside the key press or click
659
+ * itself, while the text waits on a source-map lookup: the write starts now
660
+ * and the text follows.
661
+ */
662
+ async function copyToClipboard(text) {
663
+ if (!window.isSecureContext) throw new Error("The clipboard needs HTTPS or localhost.");
664
+ const blob = text.then((value) => new Blob([value], { type: "text/plain" }));
665
+ await navigator.clipboard.write([new ClipboardItem({ "text/plain": blob })]);
666
+ }
667
+ //#endregion
637
668
  //#region src/devtools/client/code/editor-url.ts
638
- const fileSchemeLink = (scheme) => (absolutePath, line, column) => `${scheme}://file${encodeURI(absolutePath)}:${line}:${column}`;
669
+ /** VS Code-family links want `/c:/dir/file.tsx` on Windows, `/dir/file.tsx` elsewhere. */
670
+ const fileSchemeLink = (scheme) => (absolutePath, line, column) => {
671
+ const slashed = absolutePath.replaceAll("\\", "/");
672
+ const rooted = slashed.startsWith("/") ? slashed : `/${slashed}`;
673
+ return `${scheme}://file${encodeURI(rooted)}:${line}:${column}`;
674
+ };
639
675
  const EDITOR_LINKS = {
640
676
  cursor: fileSchemeLink("cursor"),
641
677
  vscode: fileSchemeLink("vscode"),
@@ -758,6 +794,7 @@ function InboxIcon() {
758
794
  //#endregion
759
795
  //#region src/devtools/client/devtools-bar.tsx
760
796
  const BAR_POSITION_KEY = "reflet-devtools-bar-position";
797
+ const PICK_BUTTON_LABEL = "Pick an element: note it, copy it, or Shift+click to open its code";
761
798
  function BarButton({ children, label, onClick, pressed }) {
762
799
  return /* @__PURE__ */ jsx("button", {
763
800
  "aria-label": label,
@@ -794,7 +831,7 @@ function DevtoolsBar({ edge, noteCount, onPick, onToggleSheet, openTab, side })
794
831
  children: /* @__PURE__ */ jsx(GripIcon, {})
795
832
  }),
796
833
  /* @__PURE__ */ jsx(BarButton, {
797
- label: "Pick an element: note it, or Shift+click to open its code",
834
+ label: PICK_BUTTON_LABEL,
798
835
  onClick: onPick,
799
836
  pressed: false,
800
837
  children: /* @__PURE__ */ jsx(TargetIcon, {})
@@ -953,7 +990,7 @@ function DevtoolsSheet({ children, onClose, onPick, onSelectTab, side, tab, tabs
953
990
  const isMoved = floating.position !== null;
954
991
  const { rootRef } = floating;
955
992
  useEffect(() => listenToKeydown(rootRef.current, (event) => {
956
- if (event.key === "Escape") onClose();
993
+ if (event.key === "Escape" && !event.isComposing) onClose();
957
994
  }), [onClose, rootRef]);
958
995
  return /* @__PURE__ */ jsxs("aside", {
959
996
  "aria-label": "Reflet devtools",
@@ -1000,7 +1037,7 @@ function DevtoolsSheet({ children, onClose, onPick, onSelectTab, side, tab, tabs
1000
1037
  ...floating.handleProps
1001
1038
  }),
1002
1039
  /* @__PURE__ */ jsx("button", {
1003
- "aria-label": "Pick an element: note it, or Shift+click to open its code",
1040
+ "aria-label": PICK_BUTTON_LABEL,
1004
1041
  className: "icon-btn",
1005
1042
  onClick: onPick,
1006
1043
  title: "Pick an element",
@@ -1076,6 +1113,7 @@ const DEVTOOLS_STYLES = `
1076
1113
  display: flex; align-items: center; gap: 6px; padding: 6px 6px 6px 14px; border-radius: 12px;
1077
1114
  color: var(--rf-danger); font-size: 12.5px; animation: dt-notice-in 220ms var(--rf-ease);
1078
1115
  }
1116
+ .dt-notice[data-kind="status"] { color: var(--rf-text); }
1079
1117
  .dt-notice p { flex: 1; margin: 0; overflow-wrap: anywhere; }
1080
1118
  .dt-notice .icon-btn { width: 30px; height: 30px; }
1081
1119
  .dt-bar {
@@ -3029,7 +3067,7 @@ const SNAPSHOT_MAX_WIDTH = 800;
3029
3067
  const OUTLINE_COLOR = "#4f46e5";
3030
3068
  const OUTLINE_WIDTH = 2;
3031
3069
  /** The element plus some room around it, grown to a readable minimum, kept inside the window. */
3032
- function cropAround(rect) {
3070
+ function cropAround(rect, viewport) {
3033
3071
  const grow = (start, length, limit) => {
3034
3072
  const padded = Math.max(length + 64, MIN_CROP_SIZE);
3035
3073
  const from = Math.max(0, start + length / 2 - padded / 2);
@@ -3039,8 +3077,8 @@ function cropAround(rect) {
3039
3077
  to
3040
3078
  };
3041
3079
  };
3042
- const horizontal = grow(rect.left, rect.width, window.innerWidth);
3043
- const vertical = grow(rect.top, rect.height, window.innerHeight);
3080
+ const horizontal = grow(rect.left, rect.width, viewport.width);
3081
+ const vertical = grow(rect.top, rect.height, viewport.height);
3044
3082
  return {
3045
3083
  height: vertical.to - vertical.from,
3046
3084
  width: horizontal.to - horizontal.from,
@@ -3068,8 +3106,9 @@ async function captureElementSnapshot(element) {
3068
3106
  try {
3069
3107
  const rect = box.getBoundingClientRect();
3070
3108
  if (rect.width === 0 || rect.height === 0) return null;
3071
- const scale = page.width / window.innerWidth;
3072
- const crop = cropAround(rect);
3109
+ const viewport = capturedViewportSize();
3110
+ const scale = page.width / viewport.width;
3111
+ const crop = cropAround(rect, viewport);
3073
3112
  const canvas = document.createElement("canvas");
3074
3113
  canvas.width = Math.round(crop.width * scale);
3075
3114
  canvas.height = Math.round(crop.height * scale);
@@ -3301,6 +3340,24 @@ async function describeSource(source, canReadSource) {
3301
3340
  }
3302
3341
  return withPosition(shortenSourcePath(source.fileName), source);
3303
3342
  }
3343
+ async function locateSource(element, canReadSource) {
3344
+ const source = await locateElementSource(element).catch(() => null);
3345
+ if (!source) return null;
3346
+ return {
3347
+ source,
3348
+ sourceLocation: await describeSource(source, canReadSource)
3349
+ };
3350
+ }
3351
+ function withSource(note, located) {
3352
+ return {
3353
+ ...note,
3354
+ selection: {
3355
+ ...note.selection,
3356
+ sourceLocation: located.sourceLocation
3357
+ },
3358
+ source: located.source
3359
+ };
3360
+ }
3304
3361
  /** `crypto.randomUUID` only exists in secure contexts; a phone on the LAN IP is not one. */
3305
3362
  function newNoteId() {
3306
3363
  const randomBytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(8));
@@ -3322,6 +3379,12 @@ function draftNote(element, note) {
3322
3379
  source: null
3323
3380
  };
3324
3381
  }
3382
+ /** The whole note in one go, source included: a copy has no stored note to fill in later. */
3383
+ async function draftLocatedNote(element, note, canReadSource) {
3384
+ const draft = draftNote(element, note);
3385
+ const located = await locateSource(element, canReadSource);
3386
+ return located ? withSource(draft, located) : draft;
3387
+ }
3325
3388
  const locateAttempts = /* @__PURE__ */ new Set();
3326
3389
  /** Once per note per page load: a note whose source cannot be found is not retried in a loop. */
3327
3390
  function hasTriedToLocate(noteId) {
@@ -3336,17 +3399,9 @@ async function enrichNote(noteId, element, canReadSource) {
3336
3399
  noteStore.setActivity(noteId, "locating");
3337
3400
  let closeUp = null;
3338
3401
  try {
3339
- const [source, snapshot] = await Promise.all([locateElementSource(element).catch(() => null), captureElementSnapshot(element).catch(() => null)]);
3402
+ const [located, snapshot] = await Promise.all([locateSource(element, canReadSource), captureElementSnapshot(element).catch(() => null)]);
3340
3403
  closeUp = snapshot;
3341
- const sourceLocation = source ? await describeSource(source, canReadSource) : null;
3342
- if (source && sourceLocation) await noteStore.patch(noteId, (note) => ({
3343
- ...note,
3344
- selection: {
3345
- ...note.selection,
3346
- sourceLocation
3347
- },
3348
- source
3349
- }));
3404
+ if (located) await noteStore.patch(noteId, (note) => withSource(note, located));
3350
3405
  if (closeUp) await noteStore.attachCloseUp(noteId, {
3351
3406
  blob: closeUp.blob,
3352
3407
  height: closeUp.height,
@@ -3408,7 +3463,7 @@ function NoteEditor({ initialText, noteId, onDone }) {
3408
3463
  className: "dt-field",
3409
3464
  onChange: (event) => setText(event.target.value),
3410
3465
  onKeyDown: (event) => {
3411
- if (event.key === "Enter" && !event.shiftKey) {
3466
+ if (isSubmitEnter(event.nativeEvent)) {
3412
3467
  event.preventDefault();
3413
3468
  save();
3414
3469
  }
@@ -3855,7 +3910,7 @@ function NotePopover({ anchor, notes, onClose, onOpenCode }) {
3855
3910
  };
3856
3911
  const stopOutsidePress = listenOnPageAndWidget(popoverRef.current, "pointerdown", closeOnOutsidePress, true);
3857
3912
  const stopKeys = listenToKeydown(popoverRef.current, (event) => {
3858
- if (event.key === "Escape") onClose();
3913
+ if (event.key === "Escape" && !event.isComposing) onClose();
3859
3914
  });
3860
3915
  return () => {
3861
3916
  stopOutsidePress();
@@ -3943,7 +3998,7 @@ const PICKER_LABELS = {
3943
3998
  attachElement: "Save note",
3944
3999
  elementNote: "Dev note",
3945
4000
  elementNotePlaceholder: "What should change here?",
3946
- pickElementHint: "Pick an element to note it"
4001
+ pickElementHint: "Pick an element to note or copy it"
3947
4002
  };
3948
4003
  function titleFor(element) {
3949
4004
  const owner = resolveComponentStack(getFiberFromNode(element))[0];
@@ -4062,6 +4117,14 @@ function DevtoolsLayer({ isWidgetOpen, position, publicKey }) {
4062
4117
  setIsPicking(false);
4063
4118
  saveNote(draftNote(element, text), element, canReadSource);
4064
4119
  }, [canReadSource]);
4120
+ const copy = useMemo(() => ({
4121
+ label: "Copy",
4122
+ onCopy: (element, text) => {
4123
+ setIsPicking(false);
4124
+ const title = titleFor(element);
4125
+ copyToClipboard(draftLocatedNote(element, text, canReadSource).then(noteAsContext)).then(() => announce(`Copied ${title}`)).catch((error) => reportFailure(`${title} was not copied`, error));
4126
+ }
4127
+ }), [canReadSource]);
4065
4128
  return /* @__PURE__ */ jsxs("div", {
4066
4129
  className: "dt",
4067
4130
  "data-recessed": isRecessed,
@@ -4116,6 +4179,7 @@ function DevtoolsLayer({ isWidgetOpen, position, publicKey }) {
4116
4179
  openTab && /* @__PURE__ */ jsx(SelectionOutline, { node: highlighted }),
4117
4180
  /* @__PURE__ */ jsx(Notices, {}),
4118
4181
  isPicking && /* @__PURE__ */ jsx(ElementPicker, {
4182
+ copy,
4119
4183
  inspect,
4120
4184
  labels: PICKER_LABELS,
4121
4185
  onCancel: cancelPick,
@@ -4127,4 +4191,4 @@ function DevtoolsLayer({ isWidgetOpen, position, publicKey }) {
4127
4191
  //#endregion
4128
4192
  export { DevtoolsLayer };
4129
4193
 
4130
- //# sourceMappingURL=devtools-layer-DOECMj0S.js.map
4194
+ //# sourceMappingURL=devtools-layer-DgmM3RhW.js.map