reflet-sdk 0.6.2 → 0.6.3

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,17 @@ 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.3] - 2026-10-04
9
+
10
+ ### Added
11
+
12
+ - 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.
13
+
14
+ ### Fixed
15
+
16
+ - 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.
17
+ - 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.
18
+
8
19
  ## [0.6.2] - 2026-10-04
9
20
 
10
21
  ### 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 { 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-Rh-hsxyX.js";
3
+ import { t as REFLET_Z_INDEX } from "./z-index-CIb4-3Fy.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,6 +653,18 @@ 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
669
  const fileSchemeLink = (scheme) => (absolutePath, line, column) => `${scheme}://file${encodeURI(absolutePath)}:${line}:${column}`;
639
670
  const EDITOR_LINKS = {
@@ -758,6 +789,7 @@ function InboxIcon() {
758
789
  //#endregion
759
790
  //#region src/devtools/client/devtools-bar.tsx
760
791
  const BAR_POSITION_KEY = "reflet-devtools-bar-position";
792
+ const PICK_BUTTON_LABEL = "Pick an element: note it, copy it, or Shift+click to open its code";
761
793
  function BarButton({ children, label, onClick, pressed }) {
762
794
  return /* @__PURE__ */ jsx("button", {
763
795
  "aria-label": label,
@@ -794,7 +826,7 @@ function DevtoolsBar({ edge, noteCount, onPick, onToggleSheet, openTab, side })
794
826
  children: /* @__PURE__ */ jsx(GripIcon, {})
795
827
  }),
796
828
  /* @__PURE__ */ jsx(BarButton, {
797
- label: "Pick an element: note it, or Shift+click to open its code",
829
+ label: PICK_BUTTON_LABEL,
798
830
  onClick: onPick,
799
831
  pressed: false,
800
832
  children: /* @__PURE__ */ jsx(TargetIcon, {})
@@ -1000,7 +1032,7 @@ function DevtoolsSheet({ children, onClose, onPick, onSelectTab, side, tab, tabs
1000
1032
  ...floating.handleProps
1001
1033
  }),
1002
1034
  /* @__PURE__ */ jsx("button", {
1003
- "aria-label": "Pick an element: note it, or Shift+click to open its code",
1035
+ "aria-label": PICK_BUTTON_LABEL,
1004
1036
  className: "icon-btn",
1005
1037
  onClick: onPick,
1006
1038
  title: "Pick an element",
@@ -1076,6 +1108,7 @@ const DEVTOOLS_STYLES = `
1076
1108
  display: flex; align-items: center; gap: 6px; padding: 6px 6px 6px 14px; border-radius: 12px;
1077
1109
  color: var(--rf-danger); font-size: 12.5px; animation: dt-notice-in 220ms var(--rf-ease);
1078
1110
  }
1111
+ .dt-notice[data-kind="status"] { color: var(--rf-text); }
1079
1112
  .dt-notice p { flex: 1; margin: 0; overflow-wrap: anywhere; }
1080
1113
  .dt-notice .icon-btn { width: 30px; height: 30px; }
1081
1114
  .dt-bar {
@@ -3301,6 +3334,24 @@ async function describeSource(source, canReadSource) {
3301
3334
  }
3302
3335
  return withPosition(shortenSourcePath(source.fileName), source);
3303
3336
  }
3337
+ async function locateSource(element, canReadSource) {
3338
+ const source = await locateElementSource(element).catch(() => null);
3339
+ if (!source) return null;
3340
+ return {
3341
+ source,
3342
+ sourceLocation: await describeSource(source, canReadSource)
3343
+ };
3344
+ }
3345
+ function withSource(note, located) {
3346
+ return {
3347
+ ...note,
3348
+ selection: {
3349
+ ...note.selection,
3350
+ sourceLocation: located.sourceLocation
3351
+ },
3352
+ source: located.source
3353
+ };
3354
+ }
3304
3355
  /** `crypto.randomUUID` only exists in secure contexts; a phone on the LAN IP is not one. */
3305
3356
  function newNoteId() {
3306
3357
  const randomBytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(8));
@@ -3322,6 +3373,12 @@ function draftNote(element, note) {
3322
3373
  source: null
3323
3374
  };
3324
3375
  }
3376
+ /** The whole note in one go, source included: a copy has no stored note to fill in later. */
3377
+ async function draftLocatedNote(element, note, canReadSource) {
3378
+ const draft = draftNote(element, note);
3379
+ const located = await locateSource(element, canReadSource);
3380
+ return located ? withSource(draft, located) : draft;
3381
+ }
3325
3382
  const locateAttempts = /* @__PURE__ */ new Set();
3326
3383
  /** Once per note per page load: a note whose source cannot be found is not retried in a loop. */
3327
3384
  function hasTriedToLocate(noteId) {
@@ -3336,17 +3393,9 @@ async function enrichNote(noteId, element, canReadSource) {
3336
3393
  noteStore.setActivity(noteId, "locating");
3337
3394
  let closeUp = null;
3338
3395
  try {
3339
- const [source, snapshot] = await Promise.all([locateElementSource(element).catch(() => null), captureElementSnapshot(element).catch(() => null)]);
3396
+ const [located, snapshot] = await Promise.all([locateSource(element, canReadSource), captureElementSnapshot(element).catch(() => null)]);
3340
3397
  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
- }));
3398
+ if (located) await noteStore.patch(noteId, (note) => withSource(note, located));
3350
3399
  if (closeUp) await noteStore.attachCloseUp(noteId, {
3351
3400
  blob: closeUp.blob,
3352
3401
  height: closeUp.height,
@@ -3943,7 +3992,7 @@ const PICKER_LABELS = {
3943
3992
  attachElement: "Save note",
3944
3993
  elementNote: "Dev note",
3945
3994
  elementNotePlaceholder: "What should change here?",
3946
- pickElementHint: "Pick an element to note it"
3995
+ pickElementHint: "Pick an element to note or copy it"
3947
3996
  };
3948
3997
  function titleFor(element) {
3949
3998
  const owner = resolveComponentStack(getFiberFromNode(element))[0];
@@ -4062,6 +4111,14 @@ function DevtoolsLayer({ isWidgetOpen, position, publicKey }) {
4062
4111
  setIsPicking(false);
4063
4112
  saveNote(draftNote(element, text), element, canReadSource);
4064
4113
  }, [canReadSource]);
4114
+ const copy = useMemo(() => ({
4115
+ label: "Copy",
4116
+ onCopy: (element, text) => {
4117
+ setIsPicking(false);
4118
+ const title = titleFor(element);
4119
+ copyToClipboard(draftLocatedNote(element, text, canReadSource).then(noteAsContext)).then(() => announce(`Copied ${title}`)).catch((error) => reportFailure(`${title} was not copied`, error));
4120
+ }
4121
+ }), [canReadSource]);
4065
4122
  return /* @__PURE__ */ jsxs("div", {
4066
4123
  className: "dt",
4067
4124
  "data-recessed": isRecessed,
@@ -4116,6 +4173,7 @@ function DevtoolsLayer({ isWidgetOpen, position, publicKey }) {
4116
4173
  openTab && /* @__PURE__ */ jsx(SelectionOutline, { node: highlighted }),
4117
4174
  /* @__PURE__ */ jsx(Notices, {}),
4118
4175
  isPicking && /* @__PURE__ */ jsx(ElementPicker, {
4176
+ copy,
4119
4177
  inspect,
4120
4178
  labels: PICKER_LABELS,
4121
4179
  onCancel: cancelPick,
@@ -4127,4 +4185,4 @@ function DevtoolsLayer({ isWidgetOpen, position, publicKey }) {
4127
4185
  //#endregion
4128
4186
  export { DevtoolsLayer };
4129
4187
 
4130
- //# sourceMappingURL=devtools-layer-DOECMj0S.js.map
4188
+ //# sourceMappingURL=devtools-layer-DtO89RlJ.js.map