reflet-sdk 0.6.1 → 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,23 @@ 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
+
19
+ ## [0.6.2] - 2026-10-04
20
+
21
+ ### Fixed
22
+
23
+ - The devtools Board tab no longer dead-ends when nothing points at the current page: switch between "This page" and "Whole board", or click "Show the whole board" from the empty state, so feedback filed on other routes or other tenants' URLs stays reachable.
24
+
8
25
  ## [0.6.1] - 2026-10-04
9
26
 
10
27
  ### 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
@@ -203,8 +205,9 @@ turn it off.
203
205
  - **Send to board** — notes become internal feedback: members see them with an
204
206
  "Internal" badge, the public board never does, and an admin can make one
205
207
  public from the dashboard.
206
- - **Board** — the board's feedback for the current route, with "Show on page",
207
- the reporter's source location, and a code search for the element's text.
208
+ - **Board** — the board's feedback for the current route, or the whole board,
209
+ with "Show on page", the reporter's source location, and a code search for
210
+ the element's text.
208
211
 
209
212
  Code view and the board go through one dev-only route that reads files from
210
213
  your repository and holds the board credential server-side.
@@ -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-C_apeUEq.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";
@@ -79,12 +79,13 @@ function callReflet(apiPath, init = {}) {
79
79
  query: init.query
80
80
  });
81
81
  }
82
- async function fetchBoardFeedbackForPage(pagePath) {
83
- return (await callReflet("/api/v1/feedback/list", { query: {
82
+ function fetchBoardFeedback(pagePath) {
83
+ const query = {
84
84
  limit: String(INBOX_PAGE_SIZE),
85
- pagePath,
86
85
  sortBy: "newest"
87
- } })).items;
86
+ };
87
+ if (pagePath !== null) query.pagePath = pagePath;
88
+ return callReflet("/api/v1/feedback/list", { query });
88
89
  }
89
90
  //#endregion
90
91
  //#region src/devtools/client/copy-button.tsx
@@ -141,16 +142,23 @@ function selectionLines(selection) {
141
142
  if (selection.html) lines.push("HTML:", ...fenced("html", selection.html));
142
143
  return lines;
143
144
  }
145
+ function elementContext(note) {
146
+ return [`Page: ${note.capturedContext.url ?? "unknown"}`, ...selectionLines(note.selection)];
147
+ }
144
148
  function notesToPrompt(notes) {
145
149
  const sections = notes.map((note, index) => [
146
150
  `## Note ${index + 1}`,
147
151
  note.note || "(no comment)",
148
152
  "",
149
- `Page: ${note.capturedContext.url ?? "unknown"}`,
150
- ...selectionLines(note.selection)
153
+ ...elementContext(note)
151
154
  ].join("\n"));
152
155
  return [NOTES_BRIEF, ...sections].join("\n\n");
153
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
+ }
154
162
  function boardFeedbackToPrompt(item) {
155
163
  const selections = item.context?.selections ?? [];
156
164
  const comments = selections.map((selection) => selection.comment).filter(Boolean);
@@ -352,17 +360,26 @@ function FeedbackCard({ item, onOpenCode, onShowSelector }) {
352
360
  }
353
361
  //#endregion
354
362
  //#region src/devtools/client/inbox/inbox-panel.tsx
355
- function useBoardFeedbackForPage() {
363
+ const INBOX_SCOPES = ["page", "board"];
364
+ const SCOPE_LABELS = {
365
+ board: "Whole board",
366
+ page: "This page"
367
+ };
368
+ const EMPTY_SCOPE_MESSAGES = {
369
+ board: "The board has no feedback yet.",
370
+ page: "Nothing on the board points at this page yet."
371
+ };
372
+ function useBoardFeedback(scope) {
356
373
  const [state, setState] = useState({ kind: "loading" });
357
374
  const load = useCallback(async () => {
358
- const pagePath = window.location.pathname;
375
+ const pagePath = scope === "page" ? window.location.pathname : null;
359
376
  setState({ kind: "loading" });
360
377
  try {
361
- const items = await fetchBoardFeedbackForPage(pagePath);
378
+ const page = await fetchBoardFeedback(pagePath);
362
379
  setState({
363
- items,
364
380
  kind: "ready",
365
- pagePath
381
+ page,
382
+ place: pagePath ?? "the board"
366
383
  });
367
384
  } catch (error) {
368
385
  setState({
@@ -370,7 +387,7 @@ function useBoardFeedbackForPage() {
370
387
  message: error instanceof Error ? error.message : "Could not reach the board."
371
388
  });
372
389
  }
373
- }, []);
390
+ }, [scope]);
374
391
  useEffect(() => {
375
392
  load();
376
393
  }, [load]);
@@ -379,14 +396,14 @@ function useBoardFeedbackForPage() {
379
396
  state
380
397
  };
381
398
  }
382
- function InboxPanel({ onOpenCode, onShowSelector }) {
383
- const { load, state } = useBoardFeedbackForPage();
399
+ function BoardFeedbackList({ onOpenCode, onShowSelector, onShowWholeBoard, scope }) {
400
+ const { load, state } = useBoardFeedback(scope);
384
401
  return /* @__PURE__ */ jsxs(Fragment, { children: [
385
402
  /* @__PURE__ */ jsxs("div", {
386
403
  className: "dt-card-head",
387
404
  children: [/* @__PURE__ */ jsx("p", {
388
405
  className: "dt-status",
389
- children: state.kind === "ready" ? `${state.items.length} on ${state.pagePath}` : "Board feedback on this page"
406
+ children: state.kind === "ready" ? `${state.page.total} on ${state.place}` : "Board feedback"
390
407
  }), /* @__PURE__ */ jsx("button", {
391
408
  className: "dt-btn",
392
409
  disabled: state.kind === "loading",
@@ -403,19 +420,43 @@ function InboxPanel({ onOpenCode, onShowSelector }) {
403
420
  className: "dt-error",
404
421
  children: state.message
405
422
  }),
406
- state.kind === "ready" && state.items.length === 0 && /* @__PURE__ */ jsx("div", {
423
+ state.kind === "ready" && state.page.items.length === 0 && /* @__PURE__ */ jsxs("div", {
407
424
  className: "dt-empty",
408
- children: /* @__PURE__ */ jsx("p", { children: "Nothing on the board points at this page yet." })
425
+ children: [/* @__PURE__ */ jsx("p", { children: EMPTY_SCOPE_MESSAGES[scope] }), scope === "page" && /* @__PURE__ */ jsx("button", {
426
+ className: "dt-btn",
427
+ onClick: onShowWholeBoard,
428
+ type: "button",
429
+ children: "Show the whole board"
430
+ })]
409
431
  }),
410
- state.kind === "ready" && state.items.map((item) => /* @__PURE__ */ jsx(FeedbackCard, {
432
+ state.kind === "ready" && state.page.items.map((item) => /* @__PURE__ */ jsx(FeedbackCard, {
411
433
  item,
412
434
  onOpenCode,
413
435
  onShowSelector
414
436
  }, item.id))
415
437
  ] });
416
438
  }
439
+ function InboxPanel({ onOpenCode, onShowSelector }) {
440
+ const [scope, setScope] = useState("page");
441
+ return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
442
+ className: "dt-tabs",
443
+ children: INBOX_SCOPES.map((option) => /* @__PURE__ */ jsx("button", {
444
+ "aria-pressed": option === scope,
445
+ className: "dt-tab",
446
+ onClick: () => setScope(option),
447
+ type: "button",
448
+ children: SCOPE_LABELS[option]
449
+ }, option))
450
+ }), /* @__PURE__ */ jsx(BoardFeedbackList, {
451
+ onOpenCode,
452
+ onShowSelector,
453
+ onShowWholeBoard: () => setScope("board"),
454
+ scope
455
+ }, scope)] });
456
+ }
417
457
  //#endregion
418
458
  //#region src/devtools/client/notices.tsx
459
+ const STATUS_VISIBLE_MS = 2400;
419
460
  let notices = [];
420
461
  let nextNoticeId = 1;
421
462
  const listeners = /* @__PURE__ */ new Set();
@@ -431,10 +472,20 @@ function reportFailure(action, error, retry) {
431
472
  const message = `${action}: ${error instanceof Error ? error.message : "Unknown error."}`;
432
473
  publish([...notices.filter((notice) => notice.message !== message), {
433
474
  id: nextNoticeId++,
475
+ kind: "failure",
434
476
  message,
435
477
  retry
436
478
  }]);
437
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
+ }
438
489
  function subscribe(listener) {
439
490
  listeners.add(listener);
440
491
  return () => listeners.delete(listener);
@@ -446,7 +497,8 @@ function Notices() {
446
497
  className: "dt-notices",
447
498
  children: current.map((notice) => /* @__PURE__ */ jsxs("div", {
448
499
  className: "dt-notice glass",
449
- role: "alert",
500
+ "data-kind": notice.kind,
501
+ role: notice.kind === "failure" ? "alert" : "status",
450
502
  children: [
451
503
  /* @__PURE__ */ jsx("p", { children: notice.message }),
452
504
  notice.retry && /* @__PURE__ */ jsx("button", {
@@ -601,6 +653,18 @@ function BoardTab({ canConnect, onOpenCode, onShowSelector, publicKey, refresh,
601
653
  }), inbox] });
602
654
  }
603
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
604
668
  //#region src/devtools/client/code/editor-url.ts
605
669
  const fileSchemeLink = (scheme) => (absolutePath, line, column) => `${scheme}://file${encodeURI(absolutePath)}:${line}:${column}`;
606
670
  const EDITOR_LINKS = {
@@ -725,6 +789,7 @@ function InboxIcon() {
725
789
  //#endregion
726
790
  //#region src/devtools/client/devtools-bar.tsx
727
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";
728
793
  function BarButton({ children, label, onClick, pressed }) {
729
794
  return /* @__PURE__ */ jsx("button", {
730
795
  "aria-label": label,
@@ -761,7 +826,7 @@ function DevtoolsBar({ edge, noteCount, onPick, onToggleSheet, openTab, side })
761
826
  children: /* @__PURE__ */ jsx(GripIcon, {})
762
827
  }),
763
828
  /* @__PURE__ */ jsx(BarButton, {
764
- label: "Pick an element: note it, or Shift+click to open its code",
829
+ label: PICK_BUTTON_LABEL,
765
830
  onClick: onPick,
766
831
  pressed: false,
767
832
  children: /* @__PURE__ */ jsx(TargetIcon, {})
@@ -776,7 +841,7 @@ function DevtoolsBar({ edge, noteCount, onPick, onToggleSheet, openTab, side })
776
841
  })]
777
842
  }),
778
843
  /* @__PURE__ */ jsx(BarButton, {
779
- label: "Board feedback on this page",
844
+ label: "Board feedback",
780
845
  onClick: () => onToggleSheet("inbox"),
781
846
  pressed: openTab === "inbox",
782
847
  children: /* @__PURE__ */ jsx(InboxIcon, {})
@@ -967,7 +1032,7 @@ function DevtoolsSheet({ children, onClose, onPick, onSelectTab, side, tab, tabs
967
1032
  ...floating.handleProps
968
1033
  }),
969
1034
  /* @__PURE__ */ jsx("button", {
970
- "aria-label": "Pick an element: note it, or Shift+click to open its code",
1035
+ "aria-label": PICK_BUTTON_LABEL,
971
1036
  className: "icon-btn",
972
1037
  onClick: onPick,
973
1038
  title: "Pick an element",
@@ -1043,6 +1108,7 @@ const DEVTOOLS_STYLES = `
1043
1108
  display: flex; align-items: center; gap: 6px; padding: 6px 6px 6px 14px; border-radius: 12px;
1044
1109
  color: var(--rf-danger); font-size: 12.5px; animation: dt-notice-in 220ms var(--rf-ease);
1045
1110
  }
1111
+ .dt-notice[data-kind="status"] { color: var(--rf-text); }
1046
1112
  .dt-notice p { flex: 1; margin: 0; overflow-wrap: anywhere; }
1047
1113
  .dt-notice .icon-btn { width: 30px; height: 30px; }
1048
1114
  .dt-bar {
@@ -1105,7 +1171,7 @@ const DEVTOOLS_STYLES = `
1105
1171
  .dt-tabs { display: flex; gap: 2px; margin-right: auto; }
1106
1172
  .dt-tab { height: 30px; padding: 0 11px; border-radius: var(--rf-pill); color: var(--rf-text-muted); font-weight: 500; }
1107
1173
  .dt-tab:hover { color: var(--rf-text); }
1108
- .dt-tab[aria-selected="true"] { background: var(--rf-bg-hover); color: var(--rf-text); }
1174
+ .dt-tab[aria-selected="true"], .dt-tab[aria-pressed="true"] { background: var(--rf-bg-hover); color: var(--rf-text); }
1109
1175
  .dt-sheet-body { flex: 1; min-height: 0; overflow: auto; display: flex; flex-direction: column; gap: 8px; padding: 10px; }
1110
1176
  .dt-sheet-foot {
1111
1177
  position: sticky; bottom: -10px; display: flex; flex-wrap: wrap; align-items: center; gap: 6px;
@@ -3268,6 +3334,24 @@ async function describeSource(source, canReadSource) {
3268
3334
  }
3269
3335
  return withPosition(shortenSourcePath(source.fileName), source);
3270
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
+ }
3271
3355
  /** `crypto.randomUUID` only exists in secure contexts; a phone on the LAN IP is not one. */
3272
3356
  function newNoteId() {
3273
3357
  const randomBytes = crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(8));
@@ -3289,6 +3373,12 @@ function draftNote(element, note) {
3289
3373
  source: null
3290
3374
  };
3291
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
+ }
3292
3382
  const locateAttempts = /* @__PURE__ */ new Set();
3293
3383
  /** Once per note per page load: a note whose source cannot be found is not retried in a loop. */
3294
3384
  function hasTriedToLocate(noteId) {
@@ -3303,17 +3393,9 @@ async function enrichNote(noteId, element, canReadSource) {
3303
3393
  noteStore.setActivity(noteId, "locating");
3304
3394
  let closeUp = null;
3305
3395
  try {
3306
- 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)]);
3307
3397
  closeUp = snapshot;
3308
- const sourceLocation = source ? await describeSource(source, canReadSource) : null;
3309
- if (source && sourceLocation) await noteStore.patch(noteId, (note) => ({
3310
- ...note,
3311
- selection: {
3312
- ...note.selection,
3313
- sourceLocation
3314
- },
3315
- source
3316
- }));
3398
+ if (located) await noteStore.patch(noteId, (note) => withSource(note, located));
3317
3399
  if (closeUp) await noteStore.attachCloseUp(noteId, {
3318
3400
  blob: closeUp.blob,
3319
3401
  height: closeUp.height,
@@ -3910,7 +3992,7 @@ const PICKER_LABELS = {
3910
3992
  attachElement: "Save note",
3911
3993
  elementNote: "Dev note",
3912
3994
  elementNotePlaceholder: "What should change here?",
3913
- pickElementHint: "Pick an element to note it"
3995
+ pickElementHint: "Pick an element to note or copy it"
3914
3996
  };
3915
3997
  function titleFor(element) {
3916
3998
  const owner = resolveComponentStack(getFiberFromNode(element))[0];
@@ -4029,6 +4111,14 @@ function DevtoolsLayer({ isWidgetOpen, position, publicKey }) {
4029
4111
  setIsPicking(false);
4030
4112
  saveNote(draftNote(element, text), element, canReadSource);
4031
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]);
4032
4122
  return /* @__PURE__ */ jsxs("div", {
4033
4123
  className: "dt",
4034
4124
  "data-recessed": isRecessed,
@@ -4083,6 +4173,7 @@ function DevtoolsLayer({ isWidgetOpen, position, publicKey }) {
4083
4173
  openTab && /* @__PURE__ */ jsx(SelectionOutline, { node: highlighted }),
4084
4174
  /* @__PURE__ */ jsx(Notices, {}),
4085
4175
  isPicking && /* @__PURE__ */ jsx(ElementPicker, {
4176
+ copy,
4086
4177
  inspect,
4087
4178
  labels: PICKER_LABELS,
4088
4179
  onCancel: cancelPick,
@@ -4094,4 +4185,4 @@ function DevtoolsLayer({ isWidgetOpen, position, publicKey }) {
4094
4185
  //#endregion
4095
4186
  export { DevtoolsLayer };
4096
4187
 
4097
- //# sourceMappingURL=devtools-layer-Cry67xS8.js.map
4188
+ //# sourceMappingURL=devtools-layer-DtO89RlJ.js.map