@rose42/t3-feedback 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +516 -0
  3. package/dist/bridge/attachments.d.ts +27 -0
  4. package/dist/bridge/attachments.js +109 -0
  5. package/dist/bridge/format.d.ts +70 -0
  6. package/dist/bridge/format.js +358 -0
  7. package/dist/bridge/http.d.ts +31 -0
  8. package/dist/bridge/http.js +169 -0
  9. package/dist/bridge/index.d.ts +13 -0
  10. package/dist/bridge/index.js +12 -0
  11. package/dist/bridge/parse.d.ts +16 -0
  12. package/dist/bridge/parse.js +82 -0
  13. package/dist/bridge/paths.d.ts +10 -0
  14. package/dist/bridge/paths.js +10 -0
  15. package/dist/bridge/probeThreads.d.ts +13 -0
  16. package/dist/bridge/probeThreads.js +66 -0
  17. package/dist/bridge/readSource.d.ts +8 -0
  18. package/dist/bridge/readSource.js +64 -0
  19. package/dist/bridge/sendFeedback.d.ts +99 -0
  20. package/dist/bridge/sendFeedback.js +417 -0
  21. package/dist/cli/bin.d.ts +2 -0
  22. package/dist/cli/bin.js +40 -0
  23. package/dist/cli/constants.d.ts +4 -0
  24. package/dist/cli/constants.js +4 -0
  25. package/dist/cli/detect.d.ts +17 -0
  26. package/dist/cli/detect.js +118 -0
  27. package/dist/cli/index.d.ts +4 -0
  28. package/dist/cli/index.js +4 -0
  29. package/dist/cli/init.d.ts +18 -0
  30. package/dist/cli/init.js +165 -0
  31. package/dist/cli/pm.d.ts +12 -0
  32. package/dist/cli/pm.js +49 -0
  33. package/dist/cli/prompt.d.ts +2 -0
  34. package/dist/cli/prompt.js +19 -0
  35. package/dist/cli/wire/next.d.ts +8 -0
  36. package/dist/cli/wire/next.js +71 -0
  37. package/dist/cli/wire/node.d.ts +3 -0
  38. package/dist/cli/wire/node.js +76 -0
  39. package/dist/cli/wire/vite.d.ts +25 -0
  40. package/dist/cli/wire/vite.js +71 -0
  41. package/dist/client/activeThreads.d.ts +23 -0
  42. package/dist/client/activeThreads.js +89 -0
  43. package/dist/client/cropScreenshot.d.ts +39 -0
  44. package/dist/client/cropScreenshot.js +212 -0
  45. package/dist/client/customPlugins.d.ts +7 -0
  46. package/dist/client/customPlugins.js +38 -0
  47. package/dist/client/draft.d.ts +33 -0
  48. package/dist/client/draft.js +222 -0
  49. package/dist/client/index.d.ts +24 -0
  50. package/dist/client/index.js +20 -0
  51. package/dist/client/mount.d.ts +210 -0
  52. package/dist/client/mount.js +2408 -0
  53. package/dist/client/openInT3.d.ts +28 -0
  54. package/dist/client/openInT3.js +81 -0
  55. package/dist/client/readClipboardImage.d.ts +2 -0
  56. package/dist/client/readClipboardImage.js +2 -0
  57. package/dist/client/reportType.d.ts +43 -0
  58. package/dist/client/reportType.js +110 -0
  59. package/dist/client/selectBurstFrames.d.ts +21 -0
  60. package/dist/client/selectBurstFrames.js +101 -0
  61. package/dist/client/sessionThread.d.ts +26 -0
  62. package/dist/client/sessionThread.js +93 -0
  63. package/dist/client/threadPicker.d.ts +20 -0
  64. package/dist/client/threadPicker.js +51 -0
  65. package/dist/client/transport.d.ts +11 -0
  66. package/dist/client/transport.js +35 -0
  67. package/dist/client/ui/commentPanelPlugin.d.ts +46 -0
  68. package/dist/client/ui/commentPanelPlugin.js +126 -0
  69. package/dist/client/ui/defaultPlugins.d.ts +19 -0
  70. package/dist/client/ui/defaultPlugins.js +48 -0
  71. package/dist/client/ui/elementTargetChip.d.ts +64 -0
  72. package/dist/client/ui/elementTargetChip.js +185 -0
  73. package/dist/client/ui/elementTargetPanelPlugin.d.ts +43 -0
  74. package/dist/client/ui/elementTargetPanelPlugin.js +167 -0
  75. package/dist/client/ui/elementTargetPanelSession.d.ts +49 -0
  76. package/dist/client/ui/elementTargetPanelSession.js +279 -0
  77. package/dist/client/ui/elementTargetUi.d.ts +36 -0
  78. package/dist/client/ui/elementTargetUi.js +105 -0
  79. package/dist/client/ui/host.d.ts +112 -0
  80. package/dist/client/ui/host.js +132 -0
  81. package/dist/client/ui/html.d.ts +22 -0
  82. package/dist/client/ui/html.js +26 -0
  83. package/dist/client/ui/labeledPluginPreview.d.ts +8 -0
  84. package/dist/client/ui/labeledPluginPreview.js +71 -0
  85. package/dist/client/ui/resolveContributions.d.ts +13 -0
  86. package/dist/client/ui/resolveContributions.js +20 -0
  87. package/dist/client/ui/screenshotPanelPlugin.d.ts +39 -0
  88. package/dist/client/ui/screenshotPanelPlugin.js +212 -0
  89. package/dist/client/ui/screenshotPanelSession.d.ts +84 -0
  90. package/dist/client/ui/screenshotPanelSession.js +445 -0
  91. package/dist/client/ui/screenshotUi.d.ts +31 -0
  92. package/dist/client/ui/screenshotUi.js +130 -0
  93. package/dist/client/ui/shortcuts.d.ts +32 -0
  94. package/dist/client/ui/shortcuts.js +40 -0
  95. package/dist/client/ui/slots.d.ts +73 -0
  96. package/dist/client/ui/slots.js +112 -0
  97. package/dist/controls.d.ts +8 -0
  98. package/dist/controls.js +16 -0
  99. package/dist/index.d.ts +3 -0
  100. package/dist/index.js +2 -0
  101. package/dist/plugin.d.ts +9 -0
  102. package/dist/plugin.js +186 -0
  103. package/dist/types.d.ts +464 -0
  104. package/dist/types.js +283 -0
  105. package/docs/devcontainer.md +160 -0
  106. package/package.json +107 -0
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Fullscreen crop dialog lives here; headless crop geometry + `cropDataUrl` come
3
+ * from `@rose42/feedback-capture`.
4
+ */
5
+ import { clientPointToNatural, containedImageLayout, cropDataUrl, rectFromPoints, } from "@rose42/feedback-capture";
6
+ export { clientPointToNatural, containedImageLayout, cropDataUrl, rectFromPoints };
7
+ const DEFAULT_CROP_COPY = {
8
+ hint: "Drag to select a region. Enter crops; Esc uses the full image.",
9
+ cropLabel: "Crop",
10
+ useFullLabel: "Use full",
11
+ cancelLabel: "Cancel",
12
+ discardLabel: "Discard",
13
+ };
14
+ const MIN_SELECTION_PX = 8;
15
+ /** Abort open crop overlays under `parent` (e.g. on feedback unmount). */
16
+ const cropAbortors = new WeakMap();
17
+ /** Cancel any crop dialogs still open under `parent` and drop their window listeners. */
18
+ export function abortScreenshotRegions(parent) {
19
+ parent.querySelectorAll(".t3f-crop").forEach((el) => {
20
+ cropAbortors.get(el)?.();
21
+ });
22
+ }
23
+ const CLOSE_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" aria-hidden="true"><path d="M18 6 6 18M6 6l12 12" /></svg>`;
24
+ /** Back/exit — distinct from Discard (X). */
25
+ const CANCEL_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>`;
26
+ const CROP_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 2v14a2 2 0 0 0 2 2h14"/><path d="M18 22V8a2 2 0 0 0-2-2H2"/></svg>`;
27
+ const MAXIMIZE_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8 3H5a2 2 0 0 0-2 2v3"/><path d="M21 8V5a2 2 0 0 0-2-2h-3"/><path d="M3 16v3a2 2 0 0 0 2 2h3"/><path d="M16 21h3a2 2 0 0 0 2-2v-3"/></svg>`;
28
+ function escapeHtml(value) {
29
+ return value
30
+ .replaceAll("&", "&amp;")
31
+ .replaceAll("<", "&lt;")
32
+ .replaceAll(">", "&gt;")
33
+ .replaceAll('"', "&quot;");
34
+ }
35
+ function naturalRectToDisplay(layout, rect) {
36
+ return {
37
+ x: layout.offsetX + rect.x * layout.scale,
38
+ y: layout.offsetY + rect.y * layout.scale,
39
+ width: rect.width * layout.scale,
40
+ height: rect.height * layout.scale,
41
+ };
42
+ }
43
+ /**
44
+ * Fullscreen overlay: drag a region on the screenshot.
45
+ * Resolves with an image data URL, cancel (keep prior), or discard (drop the shot).
46
+ */
47
+ export function selectScreenshotRegion(options) {
48
+ const copy = { ...DEFAULT_CROP_COPY, ...options.copy };
49
+ const allowDiscard = options.allowDiscard !== false;
50
+ return new Promise((resolve) => {
51
+ const overlay = document.createElement("div");
52
+ overlay.className = "t3f-crop";
53
+ overlay.setAttribute("role", "dialog");
54
+ overlay.setAttribute("aria-label", "Select screenshot region");
55
+ overlay.innerHTML = `
56
+ <div class="t3f-crop-stage">
57
+ <img class="t3f-crop-img" alt="" draggable="false" />
58
+ <div class="t3f-crop-marquee" hidden></div>
59
+ </div>
60
+ <div class="t3f-crop-bar">
61
+ <div class="t3f-crop-hint">${escapeHtml(copy.hint)}</div>
62
+ <div class="t3f-crop-actions">
63
+ <button type="button" class="t3f-icon-btn" data-crop-act="cancel" aria-label="${escapeHtml(copy.cancelLabel)}" title="${escapeHtml(copy.cancelLabel)}">${CANCEL_ICON_SVG}</button>
64
+ ${allowDiscard
65
+ ? `<button type="button" class="t3f-icon-btn t3f-crop-discard" data-crop-act="discard" aria-label="${escapeHtml(copy.discardLabel)}" title="${escapeHtml(copy.discardLabel)}">${CLOSE_ICON_SVG}</button>`
66
+ : ""}
67
+ <button type="button" class="t3f-btn t3f-btn-tool" data-crop-act="full" aria-label="${escapeHtml(copy.useFullLabel)}" title="${escapeHtml(copy.useFullLabel)}">${MAXIMIZE_ICON_SVG}<span>${escapeHtml(copy.useFullLabel)}</span></button>
68
+ <button type="button" class="t3f-btn t3f-btn-tool primary" data-crop-act="crop" aria-label="${escapeHtml(copy.cropLabel)}" title="${escapeHtml(copy.cropLabel)}">${CROP_ICON_SVG}<span>${escapeHtml(copy.cropLabel)}</span></button>
69
+ </div>
70
+ </div>
71
+ `;
72
+ options.parent.appendChild(overlay);
73
+ const stage = overlay.querySelector(".t3f-crop-stage");
74
+ const img = overlay.querySelector(".t3f-crop-img");
75
+ const marquee = overlay.querySelector(".t3f-crop-marquee");
76
+ const cropBtn = overlay.querySelector('[data-crop-act="crop"]');
77
+ let selection = null;
78
+ let dragStart = null;
79
+ let settled = false;
80
+ const layoutOf = () => containedImageLayout(img.naturalWidth || 1, img.naturalHeight || 1, stage.clientWidth, stage.clientHeight);
81
+ const paintMarquee = (rect) => {
82
+ if (!rect) {
83
+ marquee.hidden = true;
84
+ return;
85
+ }
86
+ const layout = layoutOf();
87
+ const d = naturalRectToDisplay(layout, rect);
88
+ marquee.hidden = false;
89
+ marquee.style.left = `${d.x}px`;
90
+ marquee.style.top = `${d.y}px`;
91
+ marquee.style.width = `${d.width}px`;
92
+ marquee.style.height = `${d.height}px`;
93
+ };
94
+ const applyInitialRect = () => {
95
+ const initial = options.initialRect;
96
+ if (!initial || img.naturalWidth <= 0 || img.naturalHeight <= 0) {
97
+ paintMarquee(null);
98
+ return;
99
+ }
100
+ const width = Math.max(1, Math.min(initial.width, img.naturalWidth - initial.x));
101
+ const height = Math.max(1, Math.min(initial.height, img.naturalHeight - initial.y));
102
+ const x = Math.max(0, Math.min(initial.x, img.naturalWidth - 1));
103
+ const y = Math.max(0, Math.min(initial.y, img.naturalHeight - 1));
104
+ if (width < MIN_SELECTION_PX || height < MIN_SELECTION_PX) {
105
+ selection = null;
106
+ paintMarquee(null);
107
+ return;
108
+ }
109
+ selection = { x, y, width, height };
110
+ paintMarquee(selection);
111
+ };
112
+ const finish = (result) => {
113
+ if (settled)
114
+ return;
115
+ settled = true;
116
+ cropAbortors.delete(overlay);
117
+ window.removeEventListener("keydown", onKeyDown, true);
118
+ window.removeEventListener("pointermove", onPointerMove);
119
+ window.removeEventListener("pointerup", onPointerUp);
120
+ window.removeEventListener("resize", onResize);
121
+ overlay.remove();
122
+ resolve(result);
123
+ };
124
+ cropAbortors.set(overlay, () => finish({ action: "cancel" }));
125
+ const useFull = () => finish({ action: "image", dataUrl: options.dataUrl });
126
+ const applyCrop = async () => {
127
+ if (!selection) {
128
+ useFull();
129
+ return;
130
+ }
131
+ cropBtn.disabled = true;
132
+ try {
133
+ const cropped = await cropDataUrl(options.dataUrl, selection);
134
+ finish({ action: "image", dataUrl: cropped });
135
+ }
136
+ catch {
137
+ useFull();
138
+ }
139
+ };
140
+ const onKeyDown = (event) => {
141
+ if (event.key === "Escape") {
142
+ event.preventDefault();
143
+ event.stopImmediatePropagation();
144
+ useFull();
145
+ return;
146
+ }
147
+ // Plain Enter crops; leave Ctrl/⌘(+Shift)+Enter and Alt+Enter for the
148
+ // feedback panel send shortcuts.
149
+ if (event.key === "Enter" && !event.ctrlKey && !event.metaKey && !event.altKey) {
150
+ event.preventDefault();
151
+ event.stopImmediatePropagation();
152
+ void applyCrop();
153
+ }
154
+ };
155
+ const localPoint = (event) => {
156
+ const box = stage.getBoundingClientRect();
157
+ return { x: event.clientX - box.left, y: event.clientY - box.top };
158
+ };
159
+ const onPointerDown = (event) => {
160
+ if (event.button !== 0)
161
+ return;
162
+ event.preventDefault();
163
+ stage.setPointerCapture(event.pointerId);
164
+ const layout = layoutOf();
165
+ const local = localPoint(event);
166
+ dragStart = clientPointToNatural(layout, local.x, local.y);
167
+ selection = null;
168
+ paintMarquee(null);
169
+ };
170
+ const onPointerMove = (event) => {
171
+ if (!dragStart)
172
+ return;
173
+ const layout = layoutOf();
174
+ const local = localPoint(event);
175
+ const end = clientPointToNatural(layout, local.x, local.y);
176
+ const next = rectFromPoints(dragStart, end, layout.naturalWidth, layout.naturalHeight, 1);
177
+ paintMarquee(next);
178
+ };
179
+ const onPointerUp = (event) => {
180
+ if (!dragStart)
181
+ return;
182
+ const layout = layoutOf();
183
+ const local = localPoint(event);
184
+ const end = clientPointToNatural(layout, local.x, local.y);
185
+ selection = rectFromPoints(dragStart, end, layout.naturalWidth, layout.naturalHeight);
186
+ dragStart = null;
187
+ paintMarquee(selection);
188
+ try {
189
+ stage.releasePointerCapture(event.pointerId);
190
+ }
191
+ catch {
192
+ /* already released */
193
+ }
194
+ };
195
+ const onResize = () => paintMarquee(selection);
196
+ overlay
197
+ .querySelector('[data-crop-act="cancel"]')
198
+ ?.addEventListener("click", () => finish({ action: "cancel" }));
199
+ overlay
200
+ .querySelector('[data-crop-act="discard"]')
201
+ ?.addEventListener("click", () => finish({ action: "discard" }));
202
+ overlay.querySelector('[data-crop-act="full"]')?.addEventListener("click", useFull);
203
+ cropBtn.addEventListener("click", () => void applyCrop());
204
+ stage.addEventListener("pointerdown", onPointerDown);
205
+ window.addEventListener("pointermove", onPointerMove);
206
+ window.addEventListener("pointerup", onPointerUp);
207
+ window.addEventListener("keydown", onKeyDown, true);
208
+ window.addEventListener("resize", onResize);
209
+ img.onload = () => applyInitialRect();
210
+ img.src = options.dataUrl;
211
+ });
212
+ }
@@ -0,0 +1,7 @@
1
+ import type { FeedbackCapturePlugin } from "@rose42/feedback-capture";
2
+ /**
3
+ * Normalize a Vite-imported custom plugin module into plugin instances.
4
+ * Prefers `default`; otherwise collects plugin-shaped exports from the namespace.
5
+ * Accepts a plugin, an array, or a zero-arg factory returning the same.
6
+ */
7
+ export declare function normalizeCustomPluginExport(mod: unknown): FeedbackCapturePlugin[];
@@ -0,0 +1,38 @@
1
+ function isFeedbackCapturePlugin(value) {
2
+ return Boolean(value &&
3
+ typeof value === "object" &&
4
+ typeof value.id === "string" &&
5
+ typeof value.collect === "function");
6
+ }
7
+ /**
8
+ * Normalize a Vite-imported custom plugin module into plugin instances.
9
+ * Prefers `default`; otherwise collects plugin-shaped exports from the namespace.
10
+ * Accepts a plugin, an array, or a zero-arg factory returning the same.
11
+ */
12
+ export function normalizeCustomPluginExport(mod) {
13
+ if (mod == null)
14
+ return [];
15
+ let candidate = mod;
16
+ if (typeof mod === "object" && mod !== null && "default" in mod) {
17
+ const def = mod.default;
18
+ if (def !== undefined)
19
+ candidate = def;
20
+ }
21
+ if (typeof candidate === "function") {
22
+ try {
23
+ candidate = candidate();
24
+ }
25
+ catch {
26
+ return [];
27
+ }
28
+ }
29
+ if (Array.isArray(candidate)) {
30
+ return candidate.filter(isFeedbackCapturePlugin);
31
+ }
32
+ if (isFeedbackCapturePlugin(candidate))
33
+ return [candidate];
34
+ if (candidate && typeof candidate === "object") {
35
+ return Object.values(candidate).filter(isFeedbackCapturePlugin);
36
+ }
37
+ return [];
38
+ }
@@ -0,0 +1,33 @@
1
+ import type { ElementTarget } from "@rose42/feedback-capture";
2
+ export type FeedbackDraftGalleryItem = {
3
+ dataUrl: string;
4
+ sourceDataUrl: string;
5
+ };
6
+ /**
7
+ * In-progress panel state restored across soft/hard reloads.
8
+ * Plugin-owned state lives in {@link FeedbackDraft.extras} keyed by plugin id.
9
+ * Legacy top-level `gallery` / `elementTarget` / `comment` are still read for compat.
10
+ */
11
+ export type FeedbackDraft = {
12
+ v: 1;
13
+ /** @deprecated Prefer extras.comment — kept for older drafts + shell fallback. */
14
+ comment: string;
15
+ /** @deprecated Prefer extras.screenshot */
16
+ gallery: FeedbackDraftGalleryItem[];
17
+ /** @deprecated Prefer extras.elementTarget */
18
+ elementTarget?: ElementTarget;
19
+ /** Opaque per-plugin state (`comment`, `screenshot`, `elementTarget`, customs…). */
20
+ extras?: Record<string, unknown>;
21
+ /** When true, remount reopens the panel (reload mid-compose). */
22
+ panelOpen: boolean;
23
+ };
24
+ declare const DRAFT_STORAGE_KEY = "t3-feedback:draft";
25
+ /**
26
+ * Normalize legacy top-level fields into extras so plugins read one shape.
27
+ */
28
+ export declare function migrateDraftExtras(draft: FeedbackDraft): Record<string, unknown>;
29
+ /** Read a saved in-progress draft from sessionStorage (tab-scoped). */
30
+ export declare function readFeedbackDraft(): FeedbackDraft | undefined;
31
+ export declare function writeFeedbackDraft(draft: FeedbackDraft): void;
32
+ export declare function clearFeedbackDraft(): void;
33
+ export { DRAFT_STORAGE_KEY };
@@ -0,0 +1,222 @@
1
+ const DRAFT_STORAGE_KEY = "t3-feedback:draft";
2
+ function parseDraftGalleryItem(value) {
3
+ if (!value || typeof value !== "object" || Array.isArray(value))
4
+ return undefined;
5
+ const record = value;
6
+ const dataUrl = typeof record.dataUrl === "string" ? record.dataUrl : "";
7
+ const sourceDataUrl = typeof record.sourceDataUrl === "string" ? record.sourceDataUrl : dataUrl;
8
+ if (!dataUrl.startsWith("data:image/"))
9
+ return undefined;
10
+ if (!sourceDataUrl.startsWith("data:image/"))
11
+ return undefined;
12
+ return { dataUrl, sourceDataUrl };
13
+ }
14
+ function parseDraftElementTarget(value) {
15
+ if (!value || typeof value !== "object" || Array.isArray(value))
16
+ return undefined;
17
+ const record = value;
18
+ const selector = typeof record.selector === "string" ? record.selector.trim() : "";
19
+ const tagName = typeof record.tagName === "string" ? record.tagName.trim() : "";
20
+ if (!selector || !tagName)
21
+ return undefined;
22
+ const box = record.boundingBox;
23
+ const viewport = record.viewport;
24
+ if (!box || typeof box !== "object" || Array.isArray(box))
25
+ return undefined;
26
+ if (!viewport || typeof viewport !== "object" || Array.isArray(viewport))
27
+ return undefined;
28
+ const b = box;
29
+ const v = viewport;
30
+ const num = (n) => (typeof n === "number" && Number.isFinite(n) ? n : undefined);
31
+ const x = num(b.x);
32
+ const y = num(b.y);
33
+ const width = num(b.width);
34
+ const height = num(b.height);
35
+ const vw = num(v.width);
36
+ const vh = num(v.height);
37
+ const dpr = num(v.devicePixelRatio);
38
+ if (x === undefined ||
39
+ y === undefined ||
40
+ width === undefined ||
41
+ height === undefined ||
42
+ vw === undefined ||
43
+ vh === undefined ||
44
+ dpr === undefined) {
45
+ return undefined;
46
+ }
47
+ const target = {
48
+ selector,
49
+ tagName,
50
+ boundingBox: { x, y, width, height },
51
+ viewport: { width: vw, height: vh, devicePixelRatio: dpr },
52
+ };
53
+ if (typeof record.id === "string" && record.id)
54
+ target.id = record.id;
55
+ if (typeof record.role === "string" && record.role)
56
+ target.role = record.role;
57
+ if (typeof record.name === "string" && record.name)
58
+ target.name = record.name;
59
+ if (typeof record.textSnippet === "string" && record.textSnippet) {
60
+ target.textSnippet = record.textSnippet;
61
+ }
62
+ if (typeof record.innerHTMLSnippet === "string" && record.innerHTMLSnippet) {
63
+ target.innerHTMLSnippet = record.innerHTMLSnippet;
64
+ }
65
+ if (record.styles && typeof record.styles === "object" && !Array.isArray(record.styles)) {
66
+ const styles = {};
67
+ for (const [key, styleValue] of Object.entries(record.styles)) {
68
+ if (typeof styleValue === "string")
69
+ styles[key] = styleValue;
70
+ }
71
+ if (Object.keys(styles).length > 0)
72
+ target.styles = styles;
73
+ }
74
+ const component = parseComponentLoc(record.component);
75
+ if (component)
76
+ target.component = component;
77
+ const leaf = parseComponentLoc(record.leaf);
78
+ if (leaf)
79
+ target.leaf = leaf;
80
+ if (Array.isArray(record.owners)) {
81
+ const owners = record.owners
82
+ .map((entry) => parseComponentLoc(entry))
83
+ .filter((entry) => Boolean(entry));
84
+ if (owners.length > 0)
85
+ target.owners = owners;
86
+ }
87
+ return target;
88
+ }
89
+ function parseComponentLoc(value) {
90
+ if (!value || typeof value !== "object" || Array.isArray(value))
91
+ return undefined;
92
+ const record = value;
93
+ const fileName = typeof record.fileName === "string" ? record.fileName.trim() : "";
94
+ const line = typeof record.line === "number" && Number.isFinite(record.line) ? record.line : NaN;
95
+ if (!fileName || !Number.isFinite(line))
96
+ return undefined;
97
+ const loc = { fileName, line };
98
+ if (typeof record.name === "string" && record.name)
99
+ loc.name = record.name;
100
+ if (typeof record.column === "number" && Number.isFinite(record.column)) {
101
+ loc.column = record.column;
102
+ }
103
+ return loc;
104
+ }
105
+ function parseExtras(value) {
106
+ if (!value || typeof value !== "object" || Array.isArray(value))
107
+ return {};
108
+ return { ...value };
109
+ }
110
+ /**
111
+ * Normalize legacy top-level fields into extras so plugins read one shape.
112
+ */
113
+ export function migrateDraftExtras(draft) {
114
+ const extras = { ...(draft.extras ?? {}) };
115
+ if (extras.comment === undefined && draft.comment) {
116
+ extras.comment = draft.comment;
117
+ }
118
+ if (extras.screenshot === undefined && draft.gallery.length > 0) {
119
+ extras.screenshot = { gallery: draft.gallery };
120
+ }
121
+ if (extras.elementTarget === undefined && draft.elementTarget) {
122
+ extras.elementTarget = draft.elementTarget;
123
+ }
124
+ return extras;
125
+ }
126
+ /** Read a saved in-progress draft from sessionStorage (tab-scoped). */
127
+ export function readFeedbackDraft() {
128
+ try {
129
+ const raw = sessionStorage.getItem(DRAFT_STORAGE_KEY);
130
+ if (!raw)
131
+ return undefined;
132
+ const parsed = JSON.parse(raw);
133
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
134
+ return undefined;
135
+ const record = parsed;
136
+ if (record.v !== 1)
137
+ return undefined;
138
+ const comment = typeof record.comment === "string" ? record.comment : "";
139
+ const galleryRaw = Array.isArray(record.gallery) ? record.gallery : [];
140
+ const gallery = galleryRaw
141
+ .map(parseDraftGalleryItem)
142
+ .filter((item) => item !== undefined);
143
+ const elementTarget = parseDraftElementTarget(record.elementTarget);
144
+ const extras = parseExtras(record.extras);
145
+ const panelOpen = record.panelOpen === true;
146
+ const migrated = migrateDraftExtras({
147
+ v: 1,
148
+ comment,
149
+ gallery,
150
+ ...(elementTarget ? { elementTarget } : {}),
151
+ extras,
152
+ panelOpen,
153
+ });
154
+ if (!comment.trim() &&
155
+ gallery.length === 0 &&
156
+ !elementTarget &&
157
+ Object.keys(migrated).length === 0 &&
158
+ !panelOpen) {
159
+ return undefined;
160
+ }
161
+ return {
162
+ v: 1,
163
+ comment: typeof migrated.comment === "string" ? migrated.comment : comment,
164
+ gallery: Array.isArray(migrated.screenshot?.gallery)
165
+ ? migrated.screenshot.gallery
166
+ .map(parseDraftGalleryItem)
167
+ .filter((item) => item !== undefined)
168
+ : gallery,
169
+ extras: migrated,
170
+ panelOpen,
171
+ ...((parseDraftElementTarget(migrated.elementTarget) ?? elementTarget)
172
+ ? {
173
+ elementTarget: parseDraftElementTarget(migrated.elementTarget) ?? elementTarget,
174
+ }
175
+ : {}),
176
+ };
177
+ }
178
+ catch {
179
+ return undefined;
180
+ }
181
+ }
182
+ export function writeFeedbackDraft(draft) {
183
+ const extras = migrateDraftExtras(draft);
184
+ const comment = typeof extras.comment === "string" ? extras.comment : draft.comment;
185
+ const shot = extras.screenshot;
186
+ const gallery = Array.isArray(shot?.gallery) ? shot.gallery : draft.gallery;
187
+ const elementTarget = parseDraftElementTarget(extras.elementTarget) ?? draft.elementTarget;
188
+ const payload = {
189
+ v: 1,
190
+ comment,
191
+ gallery,
192
+ panelOpen: draft.panelOpen,
193
+ extras,
194
+ ...(elementTarget ? { elementTarget } : {}),
195
+ };
196
+ try {
197
+ sessionStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify(payload));
198
+ }
199
+ catch {
200
+ try {
201
+ const slimExtras = { ...extras };
202
+ delete slimExtras.screenshot;
203
+ sessionStorage.setItem(DRAFT_STORAGE_KEY, JSON.stringify({
204
+ ...payload,
205
+ gallery: [],
206
+ extras: slimExtras,
207
+ }));
208
+ }
209
+ catch {
210
+ // ignore
211
+ }
212
+ }
213
+ }
214
+ export function clearFeedbackDraft() {
215
+ try {
216
+ sessionStorage.removeItem(DRAFT_STORAGE_KEY);
217
+ }
218
+ catch {
219
+ // ignore
220
+ }
221
+ }
222
+ export { DRAFT_STORAGE_KEY };
@@ -0,0 +1,24 @@
1
+ export { mountT3Feedback, mountT3FeedbackFromConfig, buildOpenInEditorUrl, buildTargetChipHtml, buildReportTypeHint, componentHierarchy, formatComponentLocation, formatElementChip, gatePluginByReportType, openSourceInEditor, pageContextForReportType, pluginAppliesToReportType, readFeedbackDraft, type FeedbackDraft, type MountT3FeedbackOptions, type T3FeedbackHandle, type T3FeedbackMountCopy, } from "./mount.js";
2
+ export type { FeedbackReportType } from "./reportType.js";
3
+ export { normalizeCustomPluginExport } from "./customPlugins.js";
4
+ export { collectPluginUiContributions, mountSlotContributions, pluginsNeedingLabelPreview, sortUiContributions, T3_FEEDBACK_UI_SLOTS, type FeedbackCapturePluginWithUi, type MountedUiContribution, type T3FeedbackUiContribution, type T3FeedbackUiContext, type T3FeedbackUiMountResult, type T3FeedbackUiSlot, } from "./ui/slots.js";
5
+ export { createAfterPickRegistry, createActionRegistry, createBusyRegistry, createDraftClearRegistry, createDraftExtrasStore, createHostMedia, } from "./ui/host.js";
6
+ export type { T3FeedbackAfterPickHandler, T3FeedbackBusyReason, T3FeedbackHostMedia, T3FeedbackHostServices, T3FeedbackImperativeAction, } from "./ui/host.js";
7
+ export { envelopeImageAttachments, isT3FeedbackAttachmentsValue, type T3FeedbackAttachmentsValue, type T3FeedbackImageAttachment, } from "../bridge/attachments.js";
8
+ export { formatCustomPluginSection, type T3FeedbackFormatHeadingBody, type T3FeedbackFormatLabelValue, type T3FeedbackFormatMarkdown, } from "../bridge/format.js";
9
+ export { captureFrameBurst, captureScreenshot } from "@rose42/feedback-capture";
10
+ export type { BurstFrame, CaptureFrameBurstOptions, CaptureScreenshotOptions, } from "@rose42/feedback-capture";
11
+ export { collectPluginShortcuts, matchRegisteredShortcut, resolvePluginShortcuts, type T3FeedbackPluginShortcut, type T3FeedbackRegisteredShortcut, type T3FeedbackShortcutRunContext, } from "./ui/shortcuts.js";
12
+ export { resolvePanelUiContributions } from "./ui/resolveContributions.js";
13
+ export { createLabeledPluginPreviewContribution } from "./ui/labeledPluginPreview.js";
14
+ export { createElementTargetUiContributions, type ElementTargetUiApi, } from "./ui/elementTargetUi.js";
15
+ export { createScreenshotUiContributions, type ScreenshotUiApi } from "./ui/screenshotUi.js";
16
+ export { cropDataUrl, containedImageLayout, clientPointToNatural, rectFromPoints, selectScreenshotRegion, type ContainedImageLayout, type CropRect, type SelectScreenshotRegionOptions, type SelectScreenshotRegionResult, } from "./cropScreenshot.js";
17
+ export { selectBurstFrames, type SelectBurstFramesCopy, type SelectBurstFramesOptions, } from "./selectBurstFrames.js";
18
+ export { readClipboardImageDataUrl } from "./readClipboardImage.js";
19
+ export { ACTIVE_THREAD_POLL_MS, ACTIVE_THREAD_TTL_MS, clearActiveThreads, listActiveThreads, pruneActiveThreads, removeActiveThread, removeActiveThreads, upsertActiveThread, type ActiveThreadRecord, } from "./activeThreads.js";
20
+ export { SESSION_THREAD_TTL_MS, defaultThreadDestination, readSessionThreadId, readSessionThreadRecord, sessionThreadStorageKey, writeSessionThreadId, writeSessionThreadRecord, type SessionThreadRecord, type SessionThreadReportType, type ThreadDestination, } from "./sessionThread.js";
21
+ export { createT3FeedbackHttpTransport, type T3FeedbackHttpTransportOptions } from "./transport.js";
22
+ export { T3_OPEN_WINDOW_NAME, T3_PAIR_THEN_THREAD_DELAY_MS, fetchOpenHandoff, openInT3Seamless, shouldBypassSeamlessOpen, type OpenInT3SeamlessOptions, } from "./openInT3.js";
23
+ export type { FeedbackSendResult, ThreadListItem, ThreadProbeResult, ThreadProbeState, ThreadsListResponse, ThreadsProbeResponse, T3BuiltinPluginId, T3FeedbackAfterPick, T3FeedbackBuiltinPluginOptions, T3FeedbackFabConfig, T3FeedbackFabOptions, T3FeedbackFabPosition, T3FeedbackFabVisibility, T3FeedbackOpenHandoff, T3FeedbackOpenInEditorConfig, T3FeedbackOpenInEditorOptions, T3FeedbackPickConfig, T3FeedbackPickOptions, T3FeedbackRecordConfig, T3FeedbackRecordOptions, T3FeedbackReportTypeGlyph, T3FeedbackReportTypeGlyphCustom, T3FeedbackReportTypeGlyphMap, T3FeedbackReportTypeGlyphResolved, T3FeedbackScreenshotConfig, T3FeedbackScreenshotCrop, T3FeedbackScreenshotOptions, T3FeedbackShortcutConfig, T3FeedbackShortcutOptions, T3FeedbackThreadReuse, } from "../types.js";
24
+ export { DEFAULT_OPEN_IN_EDITOR_ENDPOINT, DEFAULT_PICK_SHORTCUT, DEFAULT_RECORD_SHORTCUT, DEFAULT_REPORT_TYPE_EMOJIS, DEFAULT_REPORT_TYPE_GLYPH, DEFAULT_SCREENSHOT_SHORTCUT, DEFAULT_SHORTCUT, T3_BUILTIN_PLUGIN_IDS, formatShortcutLabel, isEditableKeyboardTarget, matchesBareKeyOverride, matchesShortcut, resolveActivePluginList, resolveActivePlugins, resolveOpenInEditorConfig, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, shortcutRequiresModifier, shouldOpenFromShortcut, } from "../types.js";
@@ -0,0 +1,20 @@
1
+ export { mountT3Feedback, mountT3FeedbackFromConfig, buildOpenInEditorUrl, buildTargetChipHtml, buildReportTypeHint, componentHierarchy, formatComponentLocation, formatElementChip, gatePluginByReportType, openSourceInEditor, pageContextForReportType, pluginAppliesToReportType, readFeedbackDraft, } from "./mount.js";
2
+ export { normalizeCustomPluginExport } from "./customPlugins.js";
3
+ export { collectPluginUiContributions, mountSlotContributions, pluginsNeedingLabelPreview, sortUiContributions, T3_FEEDBACK_UI_SLOTS, } from "./ui/slots.js";
4
+ export { createAfterPickRegistry, createActionRegistry, createBusyRegistry, createDraftClearRegistry, createDraftExtrasStore, createHostMedia, } from "./ui/host.js";
5
+ export { envelopeImageAttachments, isT3FeedbackAttachmentsValue, } from "../bridge/attachments.js";
6
+ export { formatCustomPluginSection, } from "../bridge/format.js";
7
+ export { captureFrameBurst, captureScreenshot } from "@rose42/feedback-capture";
8
+ export { collectPluginShortcuts, matchRegisteredShortcut, resolvePluginShortcuts, } from "./ui/shortcuts.js";
9
+ export { resolvePanelUiContributions } from "./ui/resolveContributions.js";
10
+ export { createLabeledPluginPreviewContribution } from "./ui/labeledPluginPreview.js";
11
+ export { createElementTargetUiContributions, } from "./ui/elementTargetUi.js";
12
+ export { createScreenshotUiContributions } from "./ui/screenshotUi.js";
13
+ export { cropDataUrl, containedImageLayout, clientPointToNatural, rectFromPoints, selectScreenshotRegion, } from "./cropScreenshot.js";
14
+ export { selectBurstFrames, } from "./selectBurstFrames.js";
15
+ export { readClipboardImageDataUrl } from "./readClipboardImage.js";
16
+ export { ACTIVE_THREAD_POLL_MS, ACTIVE_THREAD_TTL_MS, clearActiveThreads, listActiveThreads, pruneActiveThreads, removeActiveThread, removeActiveThreads, upsertActiveThread, } from "./activeThreads.js";
17
+ export { SESSION_THREAD_TTL_MS, defaultThreadDestination, readSessionThreadId, readSessionThreadRecord, sessionThreadStorageKey, writeSessionThreadId, writeSessionThreadRecord, } from "./sessionThread.js";
18
+ export { createT3FeedbackHttpTransport } from "./transport.js";
19
+ export { T3_OPEN_WINDOW_NAME, T3_PAIR_THEN_THREAD_DELAY_MS, fetchOpenHandoff, openInT3Seamless, shouldBypassSeamlessOpen, } from "./openInT3.js";
20
+ export { DEFAULT_OPEN_IN_EDITOR_ENDPOINT, DEFAULT_PICK_SHORTCUT, DEFAULT_RECORD_SHORTCUT, DEFAULT_REPORT_TYPE_EMOJIS, DEFAULT_REPORT_TYPE_GLYPH, DEFAULT_SCREENSHOT_SHORTCUT, DEFAULT_SHORTCUT, T3_BUILTIN_PLUGIN_IDS, formatShortcutLabel, isEditableKeyboardTarget, matchesBareKeyOverride, matchesShortcut, resolveActivePluginList, resolveActivePlugins, resolveOpenInEditorConfig, resolvePickConfig, resolveRecordConfig, resolveReportTypeGlyph, resolveScreenshotConfig, resolveShortcut, shortcutRequiresModifier, shouldOpenFromShortcut, } from "../types.js";