@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.
- package/LICENSE +21 -0
- package/README.md +516 -0
- package/dist/bridge/attachments.d.ts +27 -0
- package/dist/bridge/attachments.js +109 -0
- package/dist/bridge/format.d.ts +70 -0
- package/dist/bridge/format.js +358 -0
- package/dist/bridge/http.d.ts +31 -0
- package/dist/bridge/http.js +169 -0
- package/dist/bridge/index.d.ts +13 -0
- package/dist/bridge/index.js +12 -0
- package/dist/bridge/parse.d.ts +16 -0
- package/dist/bridge/parse.js +82 -0
- package/dist/bridge/paths.d.ts +10 -0
- package/dist/bridge/paths.js +10 -0
- package/dist/bridge/probeThreads.d.ts +13 -0
- package/dist/bridge/probeThreads.js +66 -0
- package/dist/bridge/readSource.d.ts +8 -0
- package/dist/bridge/readSource.js +64 -0
- package/dist/bridge/sendFeedback.d.ts +99 -0
- package/dist/bridge/sendFeedback.js +417 -0
- package/dist/cli/bin.d.ts +2 -0
- package/dist/cli/bin.js +40 -0
- package/dist/cli/constants.d.ts +4 -0
- package/dist/cli/constants.js +4 -0
- package/dist/cli/detect.d.ts +17 -0
- package/dist/cli/detect.js +118 -0
- package/dist/cli/index.d.ts +4 -0
- package/dist/cli/index.js +4 -0
- package/dist/cli/init.d.ts +18 -0
- package/dist/cli/init.js +165 -0
- package/dist/cli/pm.d.ts +12 -0
- package/dist/cli/pm.js +49 -0
- package/dist/cli/prompt.d.ts +2 -0
- package/dist/cli/prompt.js +19 -0
- package/dist/cli/wire/next.d.ts +8 -0
- package/dist/cli/wire/next.js +71 -0
- package/dist/cli/wire/node.d.ts +3 -0
- package/dist/cli/wire/node.js +76 -0
- package/dist/cli/wire/vite.d.ts +25 -0
- package/dist/cli/wire/vite.js +71 -0
- package/dist/client/activeThreads.d.ts +23 -0
- package/dist/client/activeThreads.js +89 -0
- package/dist/client/cropScreenshot.d.ts +39 -0
- package/dist/client/cropScreenshot.js +212 -0
- package/dist/client/customPlugins.d.ts +7 -0
- package/dist/client/customPlugins.js +38 -0
- package/dist/client/draft.d.ts +33 -0
- package/dist/client/draft.js +222 -0
- package/dist/client/index.d.ts +24 -0
- package/dist/client/index.js +20 -0
- package/dist/client/mount.d.ts +210 -0
- package/dist/client/mount.js +2408 -0
- package/dist/client/openInT3.d.ts +28 -0
- package/dist/client/openInT3.js +81 -0
- package/dist/client/readClipboardImage.d.ts +2 -0
- package/dist/client/readClipboardImage.js +2 -0
- package/dist/client/reportType.d.ts +43 -0
- package/dist/client/reportType.js +110 -0
- package/dist/client/selectBurstFrames.d.ts +21 -0
- package/dist/client/selectBurstFrames.js +101 -0
- package/dist/client/sessionThread.d.ts +26 -0
- package/dist/client/sessionThread.js +93 -0
- package/dist/client/threadPicker.d.ts +20 -0
- package/dist/client/threadPicker.js +51 -0
- package/dist/client/transport.d.ts +11 -0
- package/dist/client/transport.js +35 -0
- package/dist/client/ui/commentPanelPlugin.d.ts +46 -0
- package/dist/client/ui/commentPanelPlugin.js +126 -0
- package/dist/client/ui/defaultPlugins.d.ts +19 -0
- package/dist/client/ui/defaultPlugins.js +48 -0
- package/dist/client/ui/elementTargetChip.d.ts +64 -0
- package/dist/client/ui/elementTargetChip.js +185 -0
- package/dist/client/ui/elementTargetPanelPlugin.d.ts +43 -0
- package/dist/client/ui/elementTargetPanelPlugin.js +167 -0
- package/dist/client/ui/elementTargetPanelSession.d.ts +49 -0
- package/dist/client/ui/elementTargetPanelSession.js +279 -0
- package/dist/client/ui/elementTargetUi.d.ts +36 -0
- package/dist/client/ui/elementTargetUi.js +105 -0
- package/dist/client/ui/host.d.ts +112 -0
- package/dist/client/ui/host.js +132 -0
- package/dist/client/ui/html.d.ts +22 -0
- package/dist/client/ui/html.js +26 -0
- package/dist/client/ui/labeledPluginPreview.d.ts +8 -0
- package/dist/client/ui/labeledPluginPreview.js +71 -0
- package/dist/client/ui/resolveContributions.d.ts +13 -0
- package/dist/client/ui/resolveContributions.js +20 -0
- package/dist/client/ui/screenshotPanelPlugin.d.ts +39 -0
- package/dist/client/ui/screenshotPanelPlugin.js +212 -0
- package/dist/client/ui/screenshotPanelSession.d.ts +84 -0
- package/dist/client/ui/screenshotPanelSession.js +445 -0
- package/dist/client/ui/screenshotUi.d.ts +31 -0
- package/dist/client/ui/screenshotUi.js +130 -0
- package/dist/client/ui/shortcuts.d.ts +32 -0
- package/dist/client/ui/shortcuts.js +40 -0
- package/dist/client/ui/slots.d.ts +73 -0
- package/dist/client/ui/slots.js +112 -0
- package/dist/controls.d.ts +8 -0
- package/dist/controls.js +16 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +186 -0
- package/dist/types.d.ts +464 -0
- package/dist/types.js +283 -0
- package/docs/devcontainer.md +160 -0
- package/package.json +107 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { createElementTargetPlugin, } from "@rose42/feedback-capture";
|
|
2
|
+
import { createElementTargetUiContributions } from "./elementTargetUi.js";
|
|
3
|
+
import { bindElementTargetPanelSession, readPickDetailsPrefs, } from "./elementTargetPanelSession.js";
|
|
4
|
+
function isElementTarget(value) {
|
|
5
|
+
if (!value || typeof value !== "object")
|
|
6
|
+
return false;
|
|
7
|
+
const record = value;
|
|
8
|
+
return typeof record.selector === "string" && typeof record.tagName === "string";
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Element-target panel plugin — same public surface as custom plugins
|
|
12
|
+
* (`ui` + `shortcuts` + `collect`). Session binds from ui.mount.
|
|
13
|
+
*/
|
|
14
|
+
export function createElementTargetPanelPlugin(options) {
|
|
15
|
+
const { copy, pickConfig, pickShortcut, pickChord, root, afterPickMode, resolveSource, getHost } = options;
|
|
16
|
+
const elementPlugin = createElementTargetPlugin({
|
|
17
|
+
getIgnoreRoots: () => [root],
|
|
18
|
+
includeInnerHTML: pickConfig.includeInnerHTML,
|
|
19
|
+
maxInnerHTMLLength: pickConfig.maxInnerHTMLLength,
|
|
20
|
+
styles: pickConfig.styles,
|
|
21
|
+
...(resolveSource ? { resolveSource } : {}),
|
|
22
|
+
});
|
|
23
|
+
let startPickPending = false;
|
|
24
|
+
let bound = null;
|
|
25
|
+
let hostHooksInstalled = false;
|
|
26
|
+
const uiFactory = createElementTargetUiContributions({
|
|
27
|
+
copy,
|
|
28
|
+
pickChord,
|
|
29
|
+
pickDetails: {
|
|
30
|
+
includeInnerHTML: pickConfig.includeInnerHTML,
|
|
31
|
+
styles: pickConfig.styles,
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const syncDraftTarget = (target) => {
|
|
35
|
+
try {
|
|
36
|
+
getHost().setDraftExtra("elementTarget", target);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
// Host not ready.
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
const clearDraft = () => {
|
|
43
|
+
elementPlugin.clear();
|
|
44
|
+
syncDraftTarget(undefined);
|
|
45
|
+
};
|
|
46
|
+
const requestStartPick = () => {
|
|
47
|
+
if (bound) {
|
|
48
|
+
void bound.runPickFlow();
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
startPickPending = true;
|
|
52
|
+
getHost().openPanel();
|
|
53
|
+
};
|
|
54
|
+
const installHostHooks = (host) => {
|
|
55
|
+
if (hostHooksInstalled)
|
|
56
|
+
return;
|
|
57
|
+
hostHooksInstalled = true;
|
|
58
|
+
host.registerAction("pick", () => requestStartPick());
|
|
59
|
+
host.registerDraftClear(clearDraft);
|
|
60
|
+
};
|
|
61
|
+
const bindFromCtx = (ctx) => {
|
|
62
|
+
const ui = uiFactory.getApi();
|
|
63
|
+
if (!ui || bound)
|
|
64
|
+
return;
|
|
65
|
+
const saved = ctx.getDraftExtra("elementTarget");
|
|
66
|
+
if (isElementTarget(saved) && !elementPlugin.getTarget()) {
|
|
67
|
+
elementPlugin.setTarget(saved);
|
|
68
|
+
}
|
|
69
|
+
const startPick = startPickPending;
|
|
70
|
+
startPickPending = false;
|
|
71
|
+
const pickDetailsSeed = readPickDetailsPrefs({
|
|
72
|
+
includeInnerHTML: pickConfig.includeInnerHTML,
|
|
73
|
+
styles: pickConfig.styles,
|
|
74
|
+
});
|
|
75
|
+
bound = bindElementTargetPanelSession({
|
|
76
|
+
form: ctx.form,
|
|
77
|
+
backdrop: ctx.backdrop,
|
|
78
|
+
ui,
|
|
79
|
+
copy,
|
|
80
|
+
pickConfig,
|
|
81
|
+
pickDetailsSeed,
|
|
82
|
+
elementPlugin,
|
|
83
|
+
...(resolveSource ? { resolveSource } : {}),
|
|
84
|
+
setStatus: (message, kind) => ctx.setStatus(message, kind),
|
|
85
|
+
persistDraft: () => ctx.persistDraft(true),
|
|
86
|
+
focusComment: () => ctx.focusComment(),
|
|
87
|
+
isOpen: () => ctx.isPanelOpen(),
|
|
88
|
+
registerBusy: ctx.registerBusy,
|
|
89
|
+
setDraftTarget: (target) => {
|
|
90
|
+
syncDraftTarget(target);
|
|
91
|
+
},
|
|
92
|
+
onAfterPick: (target) => ctx.notifyPicked(target),
|
|
93
|
+
afterPickMode,
|
|
94
|
+
startPick,
|
|
95
|
+
});
|
|
96
|
+
const session = bound;
|
|
97
|
+
const previousDispose = session.dispose;
|
|
98
|
+
session.dispose = () => {
|
|
99
|
+
previousDispose();
|
|
100
|
+
if (bound === session)
|
|
101
|
+
bound = null;
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
const wrapContributions = (contributions) => contributions.map((contribution) => ({
|
|
105
|
+
...contribution,
|
|
106
|
+
mount: (container, ctx) => {
|
|
107
|
+
installHostHooks(ctx);
|
|
108
|
+
const result = contribution.mount(container, ctx);
|
|
109
|
+
const boundBefore = bound;
|
|
110
|
+
bindFromCtx(ctx);
|
|
111
|
+
const ownsSession = Boolean(bound && !boundBefore);
|
|
112
|
+
const disposeInner = typeof result === "function"
|
|
113
|
+
? result
|
|
114
|
+
: result && typeof result === "object"
|
|
115
|
+
? result.dispose
|
|
116
|
+
: undefined;
|
|
117
|
+
return () => {
|
|
118
|
+
disposeInner?.();
|
|
119
|
+
if (ownsSession && bound) {
|
|
120
|
+
bound.dispose();
|
|
121
|
+
bound = null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
}));
|
|
126
|
+
const plugin = {
|
|
127
|
+
id: "elementTarget",
|
|
128
|
+
collect: () => elementPlugin.collect(),
|
|
129
|
+
...(elementPlugin.setup ? { setup: elementPlugin.setup } : {}),
|
|
130
|
+
...(elementPlugin.dispose ? { dispose: elementPlugin.dispose } : {}),
|
|
131
|
+
...(pickShortcut
|
|
132
|
+
? {
|
|
133
|
+
shortcuts: {
|
|
134
|
+
id: "builtin:pick",
|
|
135
|
+
shortcut: {
|
|
136
|
+
key: pickShortcut.key,
|
|
137
|
+
mod: pickShortcut.mod,
|
|
138
|
+
shift: pickShortcut.shift,
|
|
139
|
+
alt: pickShortcut.alt,
|
|
140
|
+
},
|
|
141
|
+
run: () => requestStartPick(),
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
: {}),
|
|
145
|
+
ui: wrapContributions(uiFactory.contributions),
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
plugin,
|
|
149
|
+
elementPlugin,
|
|
150
|
+
getUiApi: uiFactory.getApi,
|
|
151
|
+
installHostHooks,
|
|
152
|
+
requestStartPick,
|
|
153
|
+
runPick: () => {
|
|
154
|
+
if (bound)
|
|
155
|
+
void bound.runPickFlow();
|
|
156
|
+
else
|
|
157
|
+
requestStartPick();
|
|
158
|
+
},
|
|
159
|
+
getBoundSession: () => bound,
|
|
160
|
+
getTarget: () => elementPlugin.getTarget() ?? undefined,
|
|
161
|
+
clear: () => clearDraft(),
|
|
162
|
+
setTarget: (target) => {
|
|
163
|
+
elementPlugin.setTarget(target);
|
|
164
|
+
syncDraftTarget(target);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { createElementTargetPlugin, type ElementTarget, type ResolveElementSource } from "@rose42/feedback-capture";
|
|
2
|
+
import type { T3FeedbackPickConfig } from "../../types.js";
|
|
3
|
+
import type { ElementTargetUiApi } from "./elementTargetUi.js";
|
|
4
|
+
export type ElementTargetPanelSessionCopy = {
|
|
5
|
+
openInEditorLabel: string;
|
|
6
|
+
pickingStatus: string;
|
|
7
|
+
pickCancelledStatus: string;
|
|
8
|
+
};
|
|
9
|
+
export type PickDetailsPrefs = {
|
|
10
|
+
includeInnerHTML: boolean;
|
|
11
|
+
styles: "none" | "layout" | "computed";
|
|
12
|
+
};
|
|
13
|
+
export declare function readPickDetailsPrefs(fallback: PickDetailsPrefs): PickDetailsPrefs;
|
|
14
|
+
export declare function writePickDetailsPrefs(prefs: PickDetailsPrefs): void;
|
|
15
|
+
export type BoundElementTargetPanelSession = {
|
|
16
|
+
dispose: () => void;
|
|
17
|
+
renderTarget: () => void;
|
|
18
|
+
runPickFlow: () => Promise<void>;
|
|
19
|
+
syncAttachmentTools: () => void;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Wire element-target UI: chip, highlight, pick-details, pick/clear flows.
|
|
23
|
+
*/
|
|
24
|
+
export declare function bindElementTargetPanelSession(options: {
|
|
25
|
+
form: HTMLFormElement;
|
|
26
|
+
backdrop: HTMLElement;
|
|
27
|
+
ui: ElementTargetUiApi;
|
|
28
|
+
copy: ElementTargetPanelSessionCopy;
|
|
29
|
+
pickConfig: T3FeedbackPickConfig;
|
|
30
|
+
pickDetailsSeed: PickDetailsPrefs;
|
|
31
|
+
elementPlugin: ReturnType<typeof createElementTargetPlugin>;
|
|
32
|
+
resolveSource?: ResolveElementSource;
|
|
33
|
+
setStatus: (message: string, kind?: "" | "ok" | "err") => void;
|
|
34
|
+
persistDraft: () => void;
|
|
35
|
+
focusComment: () => void;
|
|
36
|
+
isOpen: () => boolean;
|
|
37
|
+
registerBusy: (reason: string) => () => void;
|
|
38
|
+
/** Persist target into draft extras (and clear). */
|
|
39
|
+
setDraftTarget: (target: ElementTarget | undefined) => void;
|
|
40
|
+
/**
|
|
41
|
+
* Called after a successful pick with the target.
|
|
42
|
+
* Typically `ctx.notifyPicked(target)`.
|
|
43
|
+
*/
|
|
44
|
+
onAfterPick: (target: ElementTarget) => void | Promise<void>;
|
|
45
|
+
/** When `"none"`, focus comment after pick (no screenshot afterPick). */
|
|
46
|
+
afterPickMode: "none" | "capture" | "crop";
|
|
47
|
+
syncSiblingTools?: () => void;
|
|
48
|
+
startPick?: boolean;
|
|
49
|
+
}): BoundElementTargetPanelSession;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { collectElementTargetAsync, createElementTargetPlugin, } from "@rose42/feedback-capture";
|
|
2
|
+
import { buildTargetChipHtml, formatElementChip, openSourceInEditor, selectorAtAncestorDepth, } from "./elementTargetChip.js";
|
|
3
|
+
const PICK_DETAILS_STORAGE_KEY = "t3-feedback:pick-details";
|
|
4
|
+
export function readPickDetailsPrefs(fallback) {
|
|
5
|
+
try {
|
|
6
|
+
const raw = sessionStorage.getItem(PICK_DETAILS_STORAGE_KEY);
|
|
7
|
+
if (!raw)
|
|
8
|
+
return fallback;
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
const styles = parsed.styles === "none" || parsed.styles === "layout" || parsed.styles === "computed"
|
|
11
|
+
? parsed.styles
|
|
12
|
+
: fallback.styles;
|
|
13
|
+
return {
|
|
14
|
+
includeInnerHTML: typeof parsed.includeInnerHTML === "boolean"
|
|
15
|
+
? parsed.includeInnerHTML
|
|
16
|
+
: fallback.includeInnerHTML,
|
|
17
|
+
styles,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function writePickDetailsPrefs(prefs) {
|
|
25
|
+
try {
|
|
26
|
+
sessionStorage.setItem(PICK_DETAILS_STORAGE_KEY, JSON.stringify(prefs));
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
/* ignore quota / private mode */
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Wire element-target UI: chip, highlight, pick-details, pick/clear flows.
|
|
34
|
+
*/
|
|
35
|
+
export function bindElementTargetPanelSession(options) {
|
|
36
|
+
const { form, backdrop, ui, copy, pickConfig, pickDetailsSeed, elementPlugin, resolveSource, setStatus, persistDraft, focusComment, isOpen, registerBusy, setDraftTarget, onAfterPick, afterPickMode, syncSiblingTools, } = options;
|
|
37
|
+
const { targetBlock, targetChip, footerPick, includeInnerHtmlInput, stylesButtons } = ui;
|
|
38
|
+
let pickDetails = { ...pickDetailsSeed };
|
|
39
|
+
includeInnerHtmlInput.checked = pickDetails.includeInnerHTML;
|
|
40
|
+
const syncStylesSegUi = (styles) => {
|
|
41
|
+
for (const btn of stylesButtons) {
|
|
42
|
+
const value = btn.dataset.styles;
|
|
43
|
+
btn.setAttribute("aria-pressed", value === styles ? "true" : "false");
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
syncStylesSegUi(pickDetails.styles);
|
|
47
|
+
const syncAttachmentTools = () => {
|
|
48
|
+
if (footerPick) {
|
|
49
|
+
footerPick.hidden = !targetBlock.hidden;
|
|
50
|
+
}
|
|
51
|
+
syncSiblingTools?.();
|
|
52
|
+
};
|
|
53
|
+
const clearDomHighlight = () => {
|
|
54
|
+
const domHighlight = backdrop.querySelector(".t3f-dom-highlight");
|
|
55
|
+
if (!domHighlight)
|
|
56
|
+
return;
|
|
57
|
+
domHighlight.style.display = "none";
|
|
58
|
+
domHighlight.hidden = true;
|
|
59
|
+
};
|
|
60
|
+
const paintDomHighlight = (el) => {
|
|
61
|
+
const domHighlight = backdrop.querySelector(".t3f-dom-highlight");
|
|
62
|
+
if (!domHighlight)
|
|
63
|
+
return;
|
|
64
|
+
if (!el || !(el instanceof Element)) {
|
|
65
|
+
clearDomHighlight();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const rect = el.getBoundingClientRect();
|
|
69
|
+
if (rect.width <= 0 && rect.height <= 0) {
|
|
70
|
+
clearDomHighlight();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
domHighlight.hidden = false;
|
|
74
|
+
domHighlight.style.display = "block";
|
|
75
|
+
domHighlight.style.left = `${rect.left}px`;
|
|
76
|
+
domHighlight.style.top = `${rect.top}px`;
|
|
77
|
+
domHighlight.style.width = `${Math.max(0, rect.width)}px`;
|
|
78
|
+
domHighlight.style.height = `${Math.max(0, rect.height)}px`;
|
|
79
|
+
};
|
|
80
|
+
const resolveSelectorElement = (selector) => {
|
|
81
|
+
try {
|
|
82
|
+
return document.querySelector(selector);
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
const renderTarget = () => {
|
|
89
|
+
const target = elementPlugin.getTarget();
|
|
90
|
+
if (!target) {
|
|
91
|
+
targetBlock.hidden = true;
|
|
92
|
+
targetChip.textContent = "";
|
|
93
|
+
targetChip.removeAttribute("title");
|
|
94
|
+
clearDomHighlight();
|
|
95
|
+
syncAttachmentTools();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const chip = formatElementChip(target);
|
|
99
|
+
targetChip.innerHTML = buildTargetChipHtml(target, {
|
|
100
|
+
openInEditor: pickConfig.openInEditor,
|
|
101
|
+
openInEditorLabel: copy.openInEditorLabel,
|
|
102
|
+
});
|
|
103
|
+
targetChip.title = chip;
|
|
104
|
+
targetBlock.hidden = false;
|
|
105
|
+
syncAttachmentTools();
|
|
106
|
+
};
|
|
107
|
+
const readPickDetailsFromUi = () => {
|
|
108
|
+
const pressed = stylesButtons.find((btn) => btn.getAttribute("aria-pressed") === "true");
|
|
109
|
+
const stylesRaw = pressed?.dataset.styles;
|
|
110
|
+
const styles = stylesRaw === "layout" || stylesRaw === "computed" ? stylesRaw : "none";
|
|
111
|
+
return {
|
|
112
|
+
includeInnerHTML: includeInnerHtmlInput.checked,
|
|
113
|
+
styles,
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
const persistPickDetailsFromUi = () => {
|
|
117
|
+
pickDetails = readPickDetailsFromUi();
|
|
118
|
+
writePickDetailsPrefs(pickDetails);
|
|
119
|
+
};
|
|
120
|
+
const refreshPickedTargetDetails = () => {
|
|
121
|
+
const current = elementPlugin.getTarget();
|
|
122
|
+
if (!current)
|
|
123
|
+
return;
|
|
124
|
+
const el = resolveSelectorElement(current.selector);
|
|
125
|
+
if (!el)
|
|
126
|
+
return;
|
|
127
|
+
const details = readPickDetailsFromUi();
|
|
128
|
+
void collectElementTargetAsync(el, {
|
|
129
|
+
includeInnerHTML: details.includeInnerHTML,
|
|
130
|
+
maxInnerHTMLLength: pickConfig.maxInnerHTMLLength,
|
|
131
|
+
styles: details.styles,
|
|
132
|
+
...(resolveSource ? { resolveSource } : {}),
|
|
133
|
+
}).then((next) => {
|
|
134
|
+
elementPlugin.setTarget(next);
|
|
135
|
+
setDraftTarget(next);
|
|
136
|
+
renderTarget();
|
|
137
|
+
persistDraft();
|
|
138
|
+
});
|
|
139
|
+
};
|
|
140
|
+
const onPickDetailsChange = () => {
|
|
141
|
+
persistPickDetailsFromUi();
|
|
142
|
+
refreshPickedTargetDetails();
|
|
143
|
+
};
|
|
144
|
+
includeInnerHtmlInput.addEventListener("change", onPickDetailsChange);
|
|
145
|
+
for (const btn of stylesButtons) {
|
|
146
|
+
btn.addEventListener("click", () => {
|
|
147
|
+
const value = btn.dataset.styles;
|
|
148
|
+
if (value !== "none" && value !== "layout" && value !== "computed")
|
|
149
|
+
return;
|
|
150
|
+
syncStylesSegUi(value);
|
|
151
|
+
onPickDetailsChange();
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
targetChip.addEventListener("click", (event) => {
|
|
155
|
+
const hit = event.target?.closest?.(".t3f-target-file");
|
|
156
|
+
if (!hit || !targetChip.contains(hit) || !pickConfig.openInEditor)
|
|
157
|
+
return;
|
|
158
|
+
if (hit.tagName === "A" && hit.getAttribute("href")) {
|
|
159
|
+
event.stopPropagation();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const fileName = hit.dataset.file;
|
|
163
|
+
const line = Number(hit.dataset.line);
|
|
164
|
+
if (!fileName || !Number.isFinite(line))
|
|
165
|
+
return;
|
|
166
|
+
event.preventDefault();
|
|
167
|
+
event.stopPropagation();
|
|
168
|
+
const columnRaw = hit.dataset.column;
|
|
169
|
+
const column = columnRaw !== undefined ? Number(columnRaw) : undefined;
|
|
170
|
+
void openSourceInEditor({
|
|
171
|
+
fileName,
|
|
172
|
+
line,
|
|
173
|
+
...(typeof column === "number" && Number.isFinite(column) ? { column } : {}),
|
|
174
|
+
}, pickConfig.openInEditor);
|
|
175
|
+
});
|
|
176
|
+
targetChip.addEventListener("pointerover", (event) => {
|
|
177
|
+
const target = elementPlugin.getTarget();
|
|
178
|
+
if (!target)
|
|
179
|
+
return;
|
|
180
|
+
const hit = event.target?.closest?.(".t3f-target-seg, .t3f-target-label");
|
|
181
|
+
if (!hit || !targetChip.contains(hit))
|
|
182
|
+
return;
|
|
183
|
+
const depth = Number(hit.dataset.depth ?? "0");
|
|
184
|
+
const selector = Number.isFinite(depth)
|
|
185
|
+
? selectorAtAncestorDepth(target.selector, depth)
|
|
186
|
+
: target.selector;
|
|
187
|
+
paintDomHighlight(resolveSelectorElement(selector));
|
|
188
|
+
});
|
|
189
|
+
targetChip.addEventListener("pointerout", (event) => {
|
|
190
|
+
const next = event.relatedTarget;
|
|
191
|
+
if (next && targetChip.contains(next))
|
|
192
|
+
return;
|
|
193
|
+
clearDomHighlight();
|
|
194
|
+
});
|
|
195
|
+
targetChip.addEventListener("focusin", (event) => {
|
|
196
|
+
const target = elementPlugin.getTarget();
|
|
197
|
+
if (!target)
|
|
198
|
+
return;
|
|
199
|
+
const hit = event.target;
|
|
200
|
+
if (!hit?.matches?.(".t3f-target-seg, .t3f-target-label"))
|
|
201
|
+
return;
|
|
202
|
+
const depth = Number(hit.dataset.depth ?? "0");
|
|
203
|
+
const selector = Number.isFinite(depth)
|
|
204
|
+
? selectorAtAncestorDepth(target.selector, depth)
|
|
205
|
+
: target.selector;
|
|
206
|
+
paintDomHighlight(resolveSelectorElement(selector));
|
|
207
|
+
});
|
|
208
|
+
targetChip.addEventListener("focusout", (event) => {
|
|
209
|
+
const next = event.relatedTarget;
|
|
210
|
+
if (next && targetChip.contains(next))
|
|
211
|
+
return;
|
|
212
|
+
clearDomHighlight();
|
|
213
|
+
});
|
|
214
|
+
const runPickFlow = async () => {
|
|
215
|
+
const releasePicking = registerBusy("picking");
|
|
216
|
+
backdrop.hidden = true;
|
|
217
|
+
setStatus(copy.pickingStatus);
|
|
218
|
+
try {
|
|
219
|
+
const details = readPickDetailsFromUi();
|
|
220
|
+
persistPickDetailsFromUi();
|
|
221
|
+
const picked = await elementPlugin.pick({
|
|
222
|
+
includeInnerHTML: details.includeInnerHTML,
|
|
223
|
+
maxInnerHTMLLength: pickConfig.maxInnerHTMLLength,
|
|
224
|
+
styles: details.styles,
|
|
225
|
+
});
|
|
226
|
+
if (!isOpen())
|
|
227
|
+
return;
|
|
228
|
+
backdrop.hidden = false;
|
|
229
|
+
if (!picked) {
|
|
230
|
+
setStatus(copy.pickCancelledStatus);
|
|
231
|
+
focusComment();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
setDraftTarget(picked);
|
|
235
|
+
renderTarget();
|
|
236
|
+
persistDraft();
|
|
237
|
+
await onAfterPick(picked);
|
|
238
|
+
if (afterPickMode === "none") {
|
|
239
|
+
setStatus("");
|
|
240
|
+
focusComment();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
finally {
|
|
244
|
+
releasePicking();
|
|
245
|
+
if (isOpen())
|
|
246
|
+
backdrop.hidden = false;
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
form.querySelector('[data-act="clear-pick"]')?.addEventListener("click", () => {
|
|
250
|
+
elementPlugin.clear();
|
|
251
|
+
setDraftTarget(undefined);
|
|
252
|
+
renderTarget();
|
|
253
|
+
persistDraft();
|
|
254
|
+
});
|
|
255
|
+
form.querySelectorAll('[data-act="pick"]').forEach((el) => {
|
|
256
|
+
el.addEventListener("click", () => {
|
|
257
|
+
void runPickFlow();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
if (options.startPick) {
|
|
261
|
+
queueMicrotask(() => {
|
|
262
|
+
void runPickFlow();
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
else if (elementPlugin.getTarget()) {
|
|
266
|
+
renderTarget();
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
syncAttachmentTools();
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
dispose: () => {
|
|
273
|
+
clearDomHighlight();
|
|
274
|
+
},
|
|
275
|
+
renderTarget,
|
|
276
|
+
runPickFlow,
|
|
277
|
+
syncAttachmentTools,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { T3FeedbackUiContribution } from "./slots.js";
|
|
2
|
+
export type ElementTargetUiCopy = {
|
|
3
|
+
elementSectionLabel: string;
|
|
4
|
+
pickLabel: string;
|
|
5
|
+
clearPickLabel: string;
|
|
6
|
+
includeInnerHTMLLabel: string;
|
|
7
|
+
stylesLabel: string;
|
|
8
|
+
stylesNoneLabel: string;
|
|
9
|
+
stylesLayoutLabel: string;
|
|
10
|
+
stylesComputedLabel: string;
|
|
11
|
+
};
|
|
12
|
+
export type ElementTargetUiApi = {
|
|
13
|
+
targetBlock: HTMLDivElement;
|
|
14
|
+
targetChip: HTMLSpanElement;
|
|
15
|
+
footerPick: HTMLButtonElement | null;
|
|
16
|
+
includeInnerHtmlInput: HTMLInputElement;
|
|
17
|
+
stylesButtons: HTMLButtonElement[];
|
|
18
|
+
};
|
|
19
|
+
type PickDetailsSeed = {
|
|
20
|
+
includeInnerHTML: boolean;
|
|
21
|
+
styles: "none" | "layout" | "computed";
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Built-in element-target UI: attachment card + footer pick control.
|
|
25
|
+
* Two contributions share a closure so the returned API is filled once both mount.
|
|
26
|
+
*/
|
|
27
|
+
export declare function createElementTargetUiContributions(options: {
|
|
28
|
+
copy: ElementTargetUiCopy;
|
|
29
|
+
pickChord: string | null;
|
|
30
|
+
pickDetails: PickDetailsSeed;
|
|
31
|
+
}): {
|
|
32
|
+
contributions: T3FeedbackUiContribution[];
|
|
33
|
+
/** Populated after contributions mount (same panel open). */
|
|
34
|
+
getApi: () => ElementTargetUiApi | undefined;
|
|
35
|
+
};
|
|
36
|
+
export {};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { closeButtonHtml, escapeHtml, iconButtonHtml, PICK_ICON_SVG } from "./html.js";
|
|
2
|
+
/**
|
|
3
|
+
* Built-in element-target UI: attachment card + footer pick control.
|
|
4
|
+
* Two contributions share a closure so the returned API is filled once both mount.
|
|
5
|
+
*/
|
|
6
|
+
export function createElementTargetUiContributions(options) {
|
|
7
|
+
let api;
|
|
8
|
+
let targetBlock;
|
|
9
|
+
let targetChip;
|
|
10
|
+
let footerPick;
|
|
11
|
+
let includeInnerHtmlInput;
|
|
12
|
+
let stylesButtons;
|
|
13
|
+
const tryPublish = () => {
|
|
14
|
+
if (!targetBlock ||
|
|
15
|
+
!targetChip ||
|
|
16
|
+
includeInnerHtmlInput === undefined ||
|
|
17
|
+
!stylesButtons ||
|
|
18
|
+
footerPick === undefined) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
api = {
|
|
22
|
+
targetBlock,
|
|
23
|
+
targetChip,
|
|
24
|
+
footerPick,
|
|
25
|
+
includeInnerHtmlInput,
|
|
26
|
+
stylesButtons,
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const attachments = {
|
|
30
|
+
id: "builtin:elementTarget",
|
|
31
|
+
slot: "attachments",
|
|
32
|
+
order: 10,
|
|
33
|
+
mount: (container) => {
|
|
34
|
+
const { copy, pickDetails } = options;
|
|
35
|
+
container.innerHTML = `
|
|
36
|
+
<div class="t3f-section t3f-element" hidden>
|
|
37
|
+
<div class="t3f-section-head">
|
|
38
|
+
<div class="t3f-section-label">${escapeHtml(copy.elementSectionLabel)}</div>
|
|
39
|
+
<div class="t3f-section-tools">
|
|
40
|
+
${iconButtonHtml({
|
|
41
|
+
act: "pick",
|
|
42
|
+
label: copy.pickLabel,
|
|
43
|
+
icon: PICK_ICON_SVG,
|
|
44
|
+
...(options.pickChord ? { titleAttr: options.pickChord } : {}),
|
|
45
|
+
})}
|
|
46
|
+
${closeButtonHtml({ act: "clear-pick", label: copy.clearPickLabel })}
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
<div class="t3f-target">
|
|
50
|
+
<span class="t3f-target-chip"></span>
|
|
51
|
+
</div>
|
|
52
|
+
<div class="t3f-pick-details">
|
|
53
|
+
<label>
|
|
54
|
+
<input type="checkbox" data-act="include-inner-html"${pickDetails.includeInnerHTML ? " checked" : ""} />
|
|
55
|
+
${escapeHtml(copy.includeInnerHTMLLabel)}
|
|
56
|
+
</label>
|
|
57
|
+
<div class="t3f-styles-seg" role="group" aria-label="${escapeHtml(copy.stylesLabel)}">
|
|
58
|
+
<button type="button" data-act="styles" data-styles="none" aria-pressed="${pickDetails.styles === "none" ? "true" : "false"}">${escapeHtml(copy.stylesNoneLabel)}</button>
|
|
59
|
+
<button type="button" data-act="styles" data-styles="layout" aria-pressed="${pickDetails.styles === "layout" ? "true" : "false"}">${escapeHtml(copy.stylesLayoutLabel)}</button>
|
|
60
|
+
<button type="button" data-act="styles" data-styles="computed" aria-pressed="${pickDetails.styles === "computed" ? "true" : "false"}">${escapeHtml(copy.stylesComputedLabel)}</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
`;
|
|
65
|
+
targetBlock = container.querySelector(".t3f-element");
|
|
66
|
+
targetChip = container.querySelector(".t3f-target-chip");
|
|
67
|
+
includeInnerHtmlInput = container.querySelector('[data-act="include-inner-html"]');
|
|
68
|
+
stylesButtons = Array.from(container.querySelectorAll('[data-act="styles"]'));
|
|
69
|
+
tryPublish();
|
|
70
|
+
return () => {
|
|
71
|
+
api = undefined;
|
|
72
|
+
targetBlock = undefined;
|
|
73
|
+
targetChip = undefined;
|
|
74
|
+
includeInnerHtmlInput = undefined;
|
|
75
|
+
stylesButtons = undefined;
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
const actionsTools = {
|
|
80
|
+
id: "builtin:elementTarget-tools",
|
|
81
|
+
slot: "actionsTools",
|
|
82
|
+
order: 10,
|
|
83
|
+
mount: (container) => {
|
|
84
|
+
const { copy } = options;
|
|
85
|
+
container.innerHTML = iconButtonHtml({
|
|
86
|
+
act: "pick",
|
|
87
|
+
label: copy.pickLabel,
|
|
88
|
+
icon: PICK_ICON_SVG,
|
|
89
|
+
className: "t3f-footer-pick",
|
|
90
|
+
titleAttr: options.pickChord ? `${copy.pickLabel} (${options.pickChord})` : copy.pickLabel,
|
|
91
|
+
});
|
|
92
|
+
footerPick = container.querySelector(".t3f-footer-pick");
|
|
93
|
+
tryPublish();
|
|
94
|
+
return () => {
|
|
95
|
+
footerPick = undefined;
|
|
96
|
+
if (!targetBlock)
|
|
97
|
+
api = undefined;
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
return {
|
|
102
|
+
contributions: [attachments, actionsTools],
|
|
103
|
+
getApi: () => api,
|
|
104
|
+
};
|
|
105
|
+
}
|