@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,112 @@
1
+ import type { BurstFrame, CaptureFrameBurstOptions, ElementTarget } from "@rose42/feedback-capture";
2
+ import type { FeedbackReportType } from "../reportType.js";
3
+ import { type SelectScreenshotRegionOptions, type SelectScreenshotRegionResult } from "../cropScreenshot.js";
4
+ import { type SelectBurstFramesOptions } from "../selectBurstFrames.js";
5
+ /** Reasons plugins mark the shell busy (Esc / outside-click should not close). */
6
+ export type T3FeedbackBusyReason = "picking" | "cropping" | "recording" | string;
7
+ /** Handler run after a successful element pick (e.g. screenshot afterPick). */
8
+ export type T3FeedbackAfterPickHandler = (target: ElementTarget) => void | Promise<void>;
9
+ /** Well-known imperative actions (handle.pick / capture / record) plus custom names. */
10
+ export type T3FeedbackImperativeAction = "pick" | "capture" | "record" | (string & {});
11
+ /**
12
+ * Capture / crop / clipboard / burst helpers bound to the feedback root.
13
+ * Custom plugins use these to build screenshot-class UX without shell `data-act` wiring.
14
+ */
15
+ export type T3FeedbackHostMedia = {
16
+ /** Capture viewport; always hides the feedback root. */
17
+ captureScreenshot: () => Promise<string | undefined>;
18
+ selectCropRegion: (dataUrl: string, options?: Omit<SelectScreenshotRegionOptions, "parent" | "dataUrl">) => Promise<SelectScreenshotRegionResult>;
19
+ readClipboardImage: () => Promise<string | undefined>;
20
+ captureFrameBurst: (options?: Omit<CaptureFrameBurstOptions, "hideElements"> & {
21
+ hideElements?: HTMLElement[];
22
+ }) => Promise<BurstFrame[] | undefined>;
23
+ selectBurstFrames: (options: Omit<SelectBurstFramesOptions, "parent">) => Promise<string[] | null>;
24
+ };
25
+ /**
26
+ * Host services shared by UI contributions and shortcut handlers.
27
+ * Send-action cluster and FAB behavior are intentionally absent.
28
+ */
29
+ export type T3FeedbackHostServices = {
30
+ root: HTMLElement;
31
+ getReportType: () => FeedbackReportType;
32
+ setReportType: (type: FeedbackReportType) => void;
33
+ subscribeReportType: (listener: (type: FeedbackReportType) => void) => () => void;
34
+ isPanelOpen: () => boolean;
35
+ /** Open the compose panel (shell-owned; same as FAB open path without activity). */
36
+ openPanel: () => void;
37
+ setStatus: (message: string, kind?: "" | "ok" | "err") => void;
38
+ persistDraft: (panelOpen?: boolean) => void;
39
+ focusComment: () => void;
40
+ /** Opaque per-plugin draft bag (JSON-serializable values). */
41
+ getDraftExtra: (pluginId: string) => unknown;
42
+ setDraftExtra: (pluginId: string, value: unknown) => void;
43
+ /** While busy, shell skips Esc/outside close. Returns a disposer. */
44
+ registerBusy: (reason: T3FeedbackBusyReason) => () => void;
45
+ isBusy: () => boolean;
46
+ /**
47
+ * Register a handler for successful element picks (e.g. screenshot afterPick).
48
+ * Returns a disposer. Handlers run in registration order via {@link notifyPicked}.
49
+ */
50
+ registerAfterPick: (handler: T3FeedbackAfterPickHandler) => () => void;
51
+ /** Invoke after-pick handlers with the picked target. */
52
+ notifyPicked: (target: ElementTarget) => Promise<void>;
53
+ /**
54
+ * Register an imperative action (first registration wins).
55
+ * Used by `handle.pick` / `capture` / `record` and custom chords.
56
+ */
57
+ registerAction: (name: T3FeedbackImperativeAction, run: () => void) => () => void;
58
+ /** Run a registered action; no-op when missing. Returns whether a handler ran. */
59
+ runAction: (name: T3FeedbackImperativeAction) => boolean;
60
+ /** Register a clear hook for draft discard / successful send. */
61
+ registerDraftClear: (clear: () => void) => () => void;
62
+ /** Viewport capture, crop dialog, clipboard image, and frame-burst helpers. */
63
+ media: T3FeedbackHostMedia;
64
+ };
65
+ /** UI mount context = host services + live panel nodes. */
66
+ export type T3FeedbackUiContext = T3FeedbackHostServices & {
67
+ form: HTMLFormElement;
68
+ backdrop: HTMLElement;
69
+ };
70
+ /** Shortcut run context = host services (panel nodes may be absent). */
71
+ export type T3FeedbackShortcutRunContext = T3FeedbackHostServices;
72
+ type CaptureScreenshotFn = (options?: {
73
+ hideElements?: HTMLElement[];
74
+ }) => Promise<string | undefined>;
75
+ type CaptureFrameBurstFn = (options?: CaptureFrameBurstOptions) => Promise<BurstFrame[] | undefined>;
76
+ /** Bind media helpers to a mount root (hides root during capture; overlays parent to root). */
77
+ export declare function createHostMedia(root: HTMLElement, deps: {
78
+ captureScreenshot: CaptureScreenshotFn;
79
+ captureFrameBurst: CaptureFrameBurstFn;
80
+ }): T3FeedbackHostMedia;
81
+ /** Create a mutable busy registry for the shell. */
82
+ export declare function createBusyRegistry(): {
83
+ registerBusy: (reason: T3FeedbackBusyReason) => () => void;
84
+ isBusy: () => boolean;
85
+ };
86
+ /** Registry for cross-plugin after-pick coordination (element → screenshot, …). */
87
+ export declare function createAfterPickRegistry(): {
88
+ registerAfterPick: (handler: T3FeedbackAfterPickHandler) => () => void;
89
+ notifyPicked: (target: ElementTarget) => Promise<void>;
90
+ clear: () => void;
91
+ };
92
+ /** First-wins imperative action registry (handle.pick / capture / record). */
93
+ export declare function createActionRegistry(): {
94
+ registerAction: (name: T3FeedbackImperativeAction, run: () => void) => () => void;
95
+ runAction: (name: T3FeedbackImperativeAction) => boolean;
96
+ clear: () => void;
97
+ };
98
+ /** Draft-clear hooks so plugins zero live state without shell controller calls. */
99
+ export declare function createDraftClearRegistry(): {
100
+ registerDraftClear: (clear: () => void) => () => void;
101
+ runDraftClears: () => void;
102
+ clear: () => void;
103
+ };
104
+ /** In-memory draft extras store keyed by plugin id. */
105
+ export declare function createDraftExtrasStore(initial?: Record<string, unknown>): {
106
+ get: (pluginId: string) => unknown;
107
+ set: (pluginId: string, value: unknown) => void;
108
+ snapshot: () => Record<string, unknown>;
109
+ replace: (next: Record<string, unknown>) => void;
110
+ clear: () => void;
111
+ };
112
+ export {};
@@ -0,0 +1,132 @@
1
+ import { selectScreenshotRegion, } from "../cropScreenshot.js";
2
+ import { readClipboardImageDataUrl } from "../readClipboardImage.js";
3
+ import { selectBurstFrames } from "../selectBurstFrames.js";
4
+ /** Bind media helpers to a mount root (hides root during capture; overlays parent to root). */
5
+ export function createHostMedia(root, deps) {
6
+ return {
7
+ captureScreenshot: () => deps.captureScreenshot({ hideElements: [root] }),
8
+ selectCropRegion: (dataUrl, options) => selectScreenshotRegion({
9
+ ...options,
10
+ parent: root,
11
+ dataUrl,
12
+ }),
13
+ readClipboardImage: () => readClipboardImageDataUrl(),
14
+ captureFrameBurst: (options) => {
15
+ const extraHide = options?.hideElements ?? [];
16
+ return deps.captureFrameBurst({
17
+ ...options,
18
+ hideElements: [root, ...extraHide],
19
+ });
20
+ },
21
+ selectBurstFrames: (options) => selectBurstFrames({
22
+ ...options,
23
+ parent: root,
24
+ }),
25
+ };
26
+ }
27
+ /** Create a mutable busy registry for the shell. */
28
+ export function createBusyRegistry() {
29
+ const reasons = new Set();
30
+ return {
31
+ registerBusy: (reason) => {
32
+ const key = String(reason);
33
+ reasons.add(key);
34
+ return () => {
35
+ reasons.delete(key);
36
+ };
37
+ },
38
+ isBusy: () => reasons.size > 0,
39
+ };
40
+ }
41
+ /** Registry for cross-plugin after-pick coordination (element → screenshot, …). */
42
+ export function createAfterPickRegistry() {
43
+ const handlers = new Set();
44
+ return {
45
+ registerAfterPick: (handler) => {
46
+ handlers.add(handler);
47
+ return () => {
48
+ handlers.delete(handler);
49
+ };
50
+ },
51
+ notifyPicked: async (target) => {
52
+ for (const handler of [...handlers]) {
53
+ await handler(target);
54
+ }
55
+ },
56
+ clear: () => {
57
+ handlers.clear();
58
+ },
59
+ };
60
+ }
61
+ /** First-wins imperative action registry (handle.pick / capture / record). */
62
+ export function createActionRegistry() {
63
+ const actions = new Map();
64
+ return {
65
+ registerAction: (name, run) => {
66
+ const key = String(name);
67
+ if (!actions.has(key))
68
+ actions.set(key, run);
69
+ return () => {
70
+ if (actions.get(key) === run)
71
+ actions.delete(key);
72
+ };
73
+ },
74
+ runAction: (name) => {
75
+ const run = actions.get(String(name));
76
+ if (!run)
77
+ return false;
78
+ run();
79
+ return true;
80
+ },
81
+ clear: () => {
82
+ actions.clear();
83
+ },
84
+ };
85
+ }
86
+ /** Draft-clear hooks so plugins zero live state without shell controller calls. */
87
+ export function createDraftClearRegistry() {
88
+ const hooks = new Set();
89
+ return {
90
+ registerDraftClear: (clear) => {
91
+ hooks.add(clear);
92
+ return () => {
93
+ hooks.delete(clear);
94
+ };
95
+ },
96
+ runDraftClears: () => {
97
+ for (const clear of [...hooks]) {
98
+ try {
99
+ clear();
100
+ }
101
+ catch {
102
+ // ignore
103
+ }
104
+ }
105
+ },
106
+ clear: () => {
107
+ hooks.clear();
108
+ },
109
+ };
110
+ }
111
+ /** In-memory draft extras store keyed by plugin id. */
112
+ export function createDraftExtrasStore(initial) {
113
+ let extras = { ...(initial ?? {}) };
114
+ return {
115
+ get: (pluginId) => extras[pluginId],
116
+ set: (pluginId, value) => {
117
+ if (value === undefined) {
118
+ delete extras[pluginId];
119
+ }
120
+ else {
121
+ extras[pluginId] = value;
122
+ }
123
+ },
124
+ snapshot: () => ({ ...extras }),
125
+ replace: (next) => {
126
+ extras = { ...next };
127
+ },
128
+ clear: () => {
129
+ extras = {};
130
+ },
131
+ };
132
+ }
@@ -0,0 +1,22 @@
1
+ /** Shared HTML helpers for panel UI contributions. */
2
+ export declare function escapeHtml(value: string): string;
3
+ export declare 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>";
4
+ export declare const PICK_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=\"M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z\"/><path d=\"M13 13l6 6\"/></svg>";
5
+ export declare const CAMERA_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=\"M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z\"/><circle cx=\"12\" cy=\"13\" r=\"3\"/></svg>";
6
+ export declare const RECORD_ICON_SVG = "<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\"><circle cx=\"12\" cy=\"12\" r=\"8\"/><circle cx=\"12\" cy=\"12\" r=\"3\" fill=\"currentColor\"/></svg>";
7
+ export declare 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>";
8
+ export declare const IMAGE_PLUS_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=\"M16 5h6\"/><path d=\"M19 2v6\"/><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h7\"/><circle cx=\"9\" cy=\"10\" r=\"2\"/><path d=\"m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21\"/></svg>";
9
+ export declare function closeButtonHtml(opts: {
10
+ act: string;
11
+ label: string;
12
+ /** Native tooltip; defaults to `label`. */
13
+ titleAttr?: string;
14
+ className?: string;
15
+ }): string;
16
+ export declare function iconButtonHtml(opts: {
17
+ act: string;
18
+ label: string;
19
+ icon: string;
20
+ titleAttr?: string;
21
+ className?: string;
22
+ }): string;
@@ -0,0 +1,26 @@
1
+ /** Shared HTML helpers for panel UI contributions. */
2
+ export function escapeHtml(value) {
3
+ return value
4
+ .replaceAll("&", "&amp;")
5
+ .replaceAll("<", "&lt;")
6
+ .replaceAll(">", "&gt;")
7
+ .replaceAll('"', "&quot;");
8
+ }
9
+ export 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>`;
10
+ export const PICK_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="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/><path d="M13 13l6 6"/></svg>`;
11
+ export const CAMERA_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="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg>`;
12
+ export const RECORD_ICON_SVG = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><circle cx="12" cy="12" r="8"/><circle cx="12" cy="12" r="3" fill="currentColor"/></svg>`;
13
+ export 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>`;
14
+ export const IMAGE_PLUS_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="M16 5h6"/><path d="M19 2v6"/><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h7"/><circle cx="9" cy="10" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/></svg>`;
15
+ export function closeButtonHtml(opts) {
16
+ const cls = opts.className ? `t3f-icon-btn ${opts.className}` : "t3f-icon-btn";
17
+ const title = opts.titleAttr ?? opts.label;
18
+ return `<button type="button" class="${cls}" data-act="${escapeHtml(opts.act)}" aria-label="${escapeHtml(opts.label)}" title="${escapeHtml(title)}">${CLOSE_ICON_SVG}</button>`;
19
+ }
20
+ export function iconButtonHtml(opts) {
21
+ const cls = opts.className ? `t3f-icon-btn ${opts.className}` : "t3f-icon-btn";
22
+ const title = opts.titleAttr
23
+ ? ` title="${escapeHtml(opts.titleAttr)}"`
24
+ : ` title="${escapeHtml(opts.label)}"`;
25
+ return `<button type="button" class="${cls}" data-act="${escapeHtml(opts.act)}" aria-label="${escapeHtml(opts.label)}"${title}>${opts.icon}</button>`;
26
+ }
@@ -0,0 +1,8 @@
1
+ import type { FeedbackCapturePluginWithUi, T3FeedbackUiContribution } from "./slots.js";
2
+ /**
3
+ * Live `label` + `collect()` preview rows for capture plugins that do not
4
+ * provide their own `ui` contribution.
5
+ */
6
+ export declare function createLabeledPluginPreviewContribution(plugins: ReadonlyArray<FeedbackCapturePluginWithUi & {
7
+ label: string;
8
+ }>): T3FeedbackUiContribution | null;
@@ -0,0 +1,71 @@
1
+ import { pluginAppliesToReportType } from "../reportType.js";
2
+ function formatCustomPluginPreview(value) {
3
+ if (value === undefined || value === null)
4
+ return "";
5
+ if (typeof value === "string")
6
+ return value;
7
+ if (typeof value === "number" || typeof value === "boolean")
8
+ return String(value);
9
+ try {
10
+ return JSON.stringify(value);
11
+ }
12
+ catch {
13
+ return String(value);
14
+ }
15
+ }
16
+ /**
17
+ * Live `label` + `collect()` preview rows for capture plugins that do not
18
+ * provide their own `ui` contribution.
19
+ */
20
+ export function createLabeledPluginPreviewContribution(plugins) {
21
+ if (plugins.length === 0)
22
+ return null;
23
+ return {
24
+ id: "labeled-plugins",
25
+ slot: "beforeComment",
26
+ order: 0,
27
+ mount: (container, ctx) => {
28
+ const root = document.createElement("div");
29
+ root.className = "t3f-custom-plugins";
30
+ container.append(root);
31
+ const sync = (reportType) => {
32
+ const visible = plugins.filter((plugin) => pluginAppliesToReportType(plugin, reportType));
33
+ if (visible.length === 0) {
34
+ root.hidden = true;
35
+ root.replaceChildren();
36
+ return;
37
+ }
38
+ root.hidden = false;
39
+ root.replaceChildren();
40
+ for (const plugin of visible) {
41
+ const row = document.createElement("div");
42
+ row.className = "t3f-custom-plugin";
43
+ row.dataset.pluginId = plugin.id;
44
+ const labelEl = document.createElement("div");
45
+ labelEl.className = "t3f-custom-plugin-label";
46
+ labelEl.textContent = plugin.label;
47
+ const valueEl = document.createElement("div");
48
+ valueEl.className = "t3f-custom-plugin-value";
49
+ valueEl.textContent = "…";
50
+ row.append(labelEl, valueEl);
51
+ root.append(row);
52
+ void Promise.resolve()
53
+ .then(() => plugin.collect())
54
+ .then((value) => {
55
+ if (!root.isConnected)
56
+ return;
57
+ const text = formatCustomPluginPreview(value);
58
+ valueEl.textContent = text || "—";
59
+ })
60
+ .catch(() => {
61
+ if (!root.isConnected)
62
+ return;
63
+ valueEl.textContent = "—";
64
+ });
65
+ }
66
+ };
67
+ sync(ctx.getReportType());
68
+ return ctx.subscribeReportType(sync);
69
+ },
70
+ };
71
+ }
@@ -0,0 +1,13 @@
1
+ import type { FeedbackCapturePlugin } from "@rose42/feedback-capture";
2
+ import { type FeedbackCapturePluginWithUi, type T3FeedbackUiContribution } from "./slots.js";
3
+ /**
4
+ * Resolve all UI contributions for a panel open: built-ins (caller-supplied),
5
+ * auto label previews, and explicit `plugin.ui` registrations.
6
+ */
7
+ export declare function resolvePanelUiContributions(options: {
8
+ /** Built-in UI contributions (element, screenshot, …) already gated by activePlugins. */
9
+ builtins?: readonly T3FeedbackUiContribution[];
10
+ plugins?: readonly FeedbackCapturePluginWithUi[];
11
+ }): T3FeedbackUiContribution[];
12
+ /** Narrow capture plugins to the UI-capable shape (structural). */
13
+ export declare function asPluginsWithUi(plugins: readonly FeedbackCapturePlugin[]): FeedbackCapturePluginWithUi[];
@@ -0,0 +1,20 @@
1
+ import { collectPluginUiContributions, pluginsNeedingLabelPreview, } from "./slots.js";
2
+ import { createLabeledPluginPreviewContribution } from "./labeledPluginPreview.js";
3
+ /**
4
+ * Resolve all UI contributions for a panel open: built-ins (caller-supplied),
5
+ * auto label previews, and explicit `plugin.ui` registrations.
6
+ */
7
+ export function resolvePanelUiContributions(options) {
8
+ const plugins = options.plugins ?? [];
9
+ const labeled = pluginsNeedingLabelPreview(plugins);
10
+ const labelPreview = createLabeledPluginPreviewContribution(labeled);
11
+ return [
12
+ ...(options.builtins ?? []),
13
+ ...(labelPreview ? [labelPreview] : []),
14
+ ...collectPluginUiContributions(plugins),
15
+ ];
16
+ }
17
+ /** Narrow capture plugins to the UI-capable shape (structural). */
18
+ export function asPluginsWithUi(plugins) {
19
+ return plugins;
20
+ }
@@ -0,0 +1,39 @@
1
+ import type { T3FeedbackScreenshotConfig, T3FeedbackShortcutConfig } from "../../types.js";
2
+ import type { FeedbackDraftGalleryItem } from "../draft.js";
3
+ import type { T3FeedbackHostServices } from "./host.js";
4
+ import { createScreenshotUiContributions } from "./screenshotUi.js";
5
+ import type { FeedbackCapturePluginWithUi } from "./slots.js";
6
+ import { type BoundScreenshotPanelSession, type ScreenshotAfterPickMode, type ScreenshotPanelSessionCopy } from "./screenshotPanelSession.js";
7
+ export type ScreenshotPanelCopy = ScreenshotPanelSessionCopy & {
8
+ clearScreenshotLabel: string;
9
+ insertClipboardLabel: string;
10
+ dismissClipboardLabel: string;
11
+ };
12
+ export type ScreenshotPanelController = {
13
+ plugin: FeedbackCapturePluginWithUi;
14
+ getUiApi: ReturnType<typeof createScreenshotUiContributions>["getApi"];
15
+ getGallery: () => FeedbackDraftGalleryItem[];
16
+ setGallery: (gallery: FeedbackDraftGalleryItem[]) => void;
17
+ /** Register imperative actions + draft clear once the host exists. */
18
+ installHostHooks: (host: T3FeedbackHostServices) => void;
19
+ requestStartCapture: () => void;
20
+ requestStartRecord: () => void;
21
+ runCapture: () => void;
22
+ runRecord: () => void;
23
+ getBoundSession: () => BoundScreenshotPanelSession | null;
24
+ };
25
+ /**
26
+ * Screenshot panel plugin — gallery state + ui + capture/record shortcuts + collect.
27
+ * Session binds automatically from {@link FeedbackCapturePluginWithUi.ui} mounts.
28
+ */
29
+ export declare function createScreenshotPanelPlugin(options: {
30
+ copy: ScreenshotPanelCopy;
31
+ captureShortcut: T3FeedbackShortcutConfig | null;
32
+ recordShortcut: T3FeedbackShortcutConfig | null;
33
+ captureTitle: string;
34
+ recordTitle: string;
35
+ screenshotConfig: T3FeedbackScreenshotConfig;
36
+ afterPick: ScreenshotAfterPickMode;
37
+ afterPickCropPaddingPx: number;
38
+ getHost: () => T3FeedbackHostServices;
39
+ }): ScreenshotPanelController;
@@ -0,0 +1,212 @@
1
+ import { createScreenshotUiContributions } from "./screenshotUi.js";
2
+ import { bindScreenshotPanelSession, } from "./screenshotPanelSession.js";
3
+ function readGalleryExtra(value) {
4
+ if (!value || typeof value !== "object")
5
+ return [];
6
+ const gallery = value.gallery;
7
+ if (!Array.isArray(gallery))
8
+ return [];
9
+ return gallery.filter((item) => {
10
+ if (!item || typeof item !== "object")
11
+ return false;
12
+ const record = item;
13
+ return (typeof record.dataUrl === "string" &&
14
+ record.dataUrl.startsWith("data:image/") &&
15
+ typeof record.sourceDataUrl === "string" &&
16
+ record.sourceDataUrl.startsWith("data:image/"));
17
+ });
18
+ }
19
+ function readElementTargetExtra(value) {
20
+ if (!value || typeof value !== "object")
21
+ return undefined;
22
+ const record = value;
23
+ if (typeof record.selector !== "string" || typeof record.tagName !== "string") {
24
+ return undefined;
25
+ }
26
+ return value;
27
+ }
28
+ /**
29
+ * Screenshot panel plugin — gallery state + ui + capture/record shortcuts + collect.
30
+ * Session binds automatically from {@link FeedbackCapturePluginWithUi.ui} mounts.
31
+ */
32
+ export function createScreenshotPanelPlugin(options) {
33
+ const { copy, captureShortcut, recordShortcut, captureTitle, recordTitle, screenshotConfig, afterPick, afterPickCropPaddingPx, getHost, } = options;
34
+ let gallery = [];
35
+ let startCapturePending = false;
36
+ let startRecordPending = false;
37
+ let bound = null;
38
+ let hostHooksInstalled = false;
39
+ const uiFactory = createScreenshotUiContributions({
40
+ copy,
41
+ captureTitle,
42
+ recordTitle,
43
+ });
44
+ const applyGallery = (next) => {
45
+ gallery = next;
46
+ try {
47
+ getHost().setDraftExtra("screenshot", { gallery: next });
48
+ }
49
+ catch {
50
+ // Host not ready yet.
51
+ }
52
+ };
53
+ const clearDraft = () => {
54
+ gallery = [];
55
+ try {
56
+ getHost().setDraftExtra("screenshot", undefined);
57
+ }
58
+ catch {
59
+ // ignore
60
+ }
61
+ };
62
+ const requestStartCapture = () => {
63
+ if (bound) {
64
+ void bound.runShotFlow();
65
+ return;
66
+ }
67
+ startCapturePending = true;
68
+ getHost().openPanel();
69
+ };
70
+ const requestStartRecord = () => {
71
+ if (bound) {
72
+ void bound.runRecordFlow();
73
+ return;
74
+ }
75
+ startRecordPending = true;
76
+ getHost().openPanel();
77
+ };
78
+ const installHostHooks = (host) => {
79
+ if (hostHooksInstalled)
80
+ return;
81
+ hostHooksInstalled = true;
82
+ host.registerAction("capture", () => requestStartCapture());
83
+ host.registerAction("record", () => requestStartRecord());
84
+ host.registerDraftClear(clearDraft);
85
+ };
86
+ const bindFromCtx = (ctx) => {
87
+ const ui = uiFactory.getApi();
88
+ if (!ui || bound)
89
+ return;
90
+ if (gallery.length === 0) {
91
+ gallery = readGalleryExtra(ctx.getDraftExtra("screenshot"));
92
+ }
93
+ const startCapture = startCapturePending;
94
+ const startRecord = startRecordPending;
95
+ startCapturePending = false;
96
+ startRecordPending = false;
97
+ bound = bindScreenshotPanelSession({
98
+ form: ctx.form,
99
+ root: ctx.root,
100
+ backdrop: ctx.backdrop,
101
+ ui,
102
+ copy,
103
+ screenshotConfig,
104
+ captureTitle,
105
+ recordTitle,
106
+ getGallery: () => gallery,
107
+ setGallery: applyGallery,
108
+ setStatus: (message, kind) => ctx.setStatus(message, kind),
109
+ persistDraft: () => ctx.persistDraft(true),
110
+ focusComment: () => ctx.focusComment(),
111
+ isOpen: () => ctx.isPanelOpen(),
112
+ getElementTarget: () => readElementTargetExtra(ctx.getDraftExtra("elementTarget")),
113
+ registerBusy: ctx.registerBusy,
114
+ afterPick,
115
+ afterPickCropPaddingPx,
116
+ registerAfterPick: ctx.registerAfterPick,
117
+ startCapture,
118
+ startRecord: startCapture ? false : startRecord,
119
+ });
120
+ const session = bound;
121
+ const previousDispose = session.dispose;
122
+ session.dispose = () => {
123
+ previousDispose();
124
+ if (bound === session)
125
+ bound = null;
126
+ };
127
+ };
128
+ const wrapContributions = (contributions) => contributions.map((contribution) => ({
129
+ ...contribution,
130
+ mount: (container, ctx) => {
131
+ installHostHooks(ctx);
132
+ const result = contribution.mount(container, ctx);
133
+ const boundBefore = bound;
134
+ bindFromCtx(ctx);
135
+ const ownsSession = Boolean(bound && !boundBefore);
136
+ const disposeInner = typeof result === "function"
137
+ ? result
138
+ : result && typeof result === "object"
139
+ ? result.dispose
140
+ : undefined;
141
+ return () => {
142
+ disposeInner?.();
143
+ if (ownsSession && bound) {
144
+ bound.dispose();
145
+ bound = null;
146
+ }
147
+ };
148
+ },
149
+ }));
150
+ const shortcuts = [
151
+ ...(captureShortcut
152
+ ? [
153
+ {
154
+ id: "builtin:capture",
155
+ shortcut: {
156
+ key: captureShortcut.key,
157
+ mod: captureShortcut.mod,
158
+ shift: captureShortcut.shift,
159
+ alt: captureShortcut.alt,
160
+ },
161
+ run: () => requestStartCapture(),
162
+ },
163
+ ]
164
+ : []),
165
+ ...(recordShortcut
166
+ ? [
167
+ {
168
+ id: "builtin:record",
169
+ shortcut: {
170
+ key: recordShortcut.key,
171
+ mod: recordShortcut.mod,
172
+ shift: recordShortcut.shift,
173
+ alt: recordShortcut.alt,
174
+ },
175
+ run: () => requestStartRecord(),
176
+ },
177
+ ]
178
+ : []),
179
+ ];
180
+ const plugin = {
181
+ id: "screenshot",
182
+ collect: () => {
183
+ if (gallery.length === 0)
184
+ return undefined;
185
+ return gallery.map((g) => g.dataUrl);
186
+ },
187
+ ...(shortcuts.length > 0 ? { shortcuts } : {}),
188
+ ui: wrapContributions(uiFactory.contributions),
189
+ };
190
+ return {
191
+ plugin,
192
+ getUiApi: uiFactory.getApi,
193
+ getGallery: () => gallery,
194
+ setGallery: applyGallery,
195
+ installHostHooks,
196
+ requestStartCapture,
197
+ requestStartRecord,
198
+ runCapture: () => {
199
+ if (bound)
200
+ void bound.runShotFlow();
201
+ else
202
+ requestStartCapture();
203
+ },
204
+ runRecord: () => {
205
+ if (bound)
206
+ void bound.runRecordFlow();
207
+ else
208
+ requestStartRecord();
209
+ },
210
+ getBoundSession: () => bound,
211
+ };
212
+ }