d2d-feedbackkit 0.0.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/README.md +13 -0
- package/package.json +83 -0
- package/src/agent.ts +161 -0
- package/src/babel.ts +285 -0
- package/src/client.ts +1 -0
- package/src/index.ts +358 -0
- package/src/linear.ts +130 -0
- package/src/shortcuts.ts +27 -0
- package/src/solid.tsx +2723 -0
- package/src/source.ts +248 -0
- package/src/types.ts +218 -0
- package/src/vite-plugin.ts +3 -0
- package/src/web-component.tsx +114 -0
- package/src/widget.tsx +1 -0
package/src/source.ts
ADDED
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { FeedbackLocatorSourceMetadata } from "./types";
|
|
2
|
+
|
|
3
|
+
export const feedbackLocatorDataAttribute = "data-feedback-locator-id";
|
|
4
|
+
export const feedbackLocatorDataKey = "__FEEDBACK_LOCATOR_DATA__";
|
|
5
|
+
const locatorJsDataAttribute = "data-locatorjs-id";
|
|
6
|
+
|
|
7
|
+
export type FeedbackLocatorSourceLocation = {
|
|
8
|
+
start: {
|
|
9
|
+
line: number;
|
|
10
|
+
column: number;
|
|
11
|
+
};
|
|
12
|
+
end: {
|
|
13
|
+
line: number;
|
|
14
|
+
column: number;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export type FeedbackLocatorComponentInfo = {
|
|
19
|
+
name: string;
|
|
20
|
+
loc: FeedbackLocatorSourceLocation;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type FeedbackLocatorExpressionInfo = {
|
|
24
|
+
name: string;
|
|
25
|
+
loc: FeedbackLocatorSourceLocation;
|
|
26
|
+
componentId: number | null;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type FeedbackLocatorFileStorage = {
|
|
30
|
+
filePath: string;
|
|
31
|
+
projectPath: string;
|
|
32
|
+
expressions: FeedbackLocatorExpressionInfo[];
|
|
33
|
+
components: FeedbackLocatorComponentInfo[];
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type LocatorJsComponentInfo = {
|
|
37
|
+
name: string;
|
|
38
|
+
loc?: FeedbackLocatorSourceLocation | null;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type LocatorJsExpressionInfo = {
|
|
42
|
+
name: string;
|
|
43
|
+
loc: FeedbackLocatorSourceLocation;
|
|
44
|
+
wrappingComponentId: number | null;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type LocatorJsFileStorage = {
|
|
48
|
+
filePath?: string;
|
|
49
|
+
projectPath?: string;
|
|
50
|
+
expressions?: LocatorJsExpressionInfo[];
|
|
51
|
+
components?: LocatorJsComponentInfo[];
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
declare global {
|
|
55
|
+
interface Window {
|
|
56
|
+
__FEEDBACK_LOCATOR_DATA__?: Record<string, FeedbackLocatorFileStorage>;
|
|
57
|
+
__LOCATOR_DATA__?: Record<string, LocatorJsFileStorage>;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function parseFeedbackLocatorId(locatorId: string) {
|
|
62
|
+
const separatorIndex = locatorId.lastIndexOf("::");
|
|
63
|
+
|
|
64
|
+
if (separatorIndex === -1) {
|
|
65
|
+
throw new Error("feedback locator id is malformed");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const fileKey = locatorId.slice(0, separatorIndex);
|
|
69
|
+
const expressionId = locatorId.slice(separatorIndex + 2);
|
|
70
|
+
|
|
71
|
+
if (!fileKey || !expressionId) {
|
|
72
|
+
throw new Error("feedback locator id is malformed");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
fileKey,
|
|
77
|
+
expressionIndex: Number(expressionId),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function getFeedbackLocatorSourceForElement(
|
|
82
|
+
target: HTMLElement,
|
|
83
|
+
): FeedbackLocatorSourceMetadata | null {
|
|
84
|
+
const found = target.closest(`[${feedbackLocatorDataAttribute}]`);
|
|
85
|
+
|
|
86
|
+
if (!(found instanceof HTMLElement)) {
|
|
87
|
+
return getLocatorJsSourceForElement(target);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const locatorId = found.dataset.feedbackLocatorId;
|
|
91
|
+
if (!locatorId || typeof window === "undefined") {
|
|
92
|
+
return getLocatorJsSourceForElement(target);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorId);
|
|
96
|
+
const fileData = window.__FEEDBACK_LOCATOR_DATA__?.[fileKey];
|
|
97
|
+
const expression = fileData?.expressions[expressionIndex];
|
|
98
|
+
|
|
99
|
+
if (!fileData || !expression) {
|
|
100
|
+
return getLocatorJsSourceForElement(found);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const component = getFeedbackLocatorComponent(fileData, expression.componentId);
|
|
104
|
+
const locatorJsComponent = component
|
|
105
|
+
? null
|
|
106
|
+
: getLocatorJsComponentForElement(found);
|
|
107
|
+
const ancestorComponent =
|
|
108
|
+
component || locatorJsComponent ? null : getAncestorComponentForElement(found);
|
|
109
|
+
const resolvedComponent = component ?? locatorJsComponent ?? ancestorComponent;
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
locatorId,
|
|
113
|
+
filePath: fileData.filePath,
|
|
114
|
+
elementName: expression.name,
|
|
115
|
+
componentName: resolvedComponent?.name ?? null,
|
|
116
|
+
line: expression.loc.start.line,
|
|
117
|
+
column: expression.loc.start.column + 1,
|
|
118
|
+
componentLine: resolvedComponent?.line ?? null,
|
|
119
|
+
componentColumn: resolvedComponent?.column ?? null,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getLocatorJsSourceForElement(
|
|
124
|
+
target: HTMLElement,
|
|
125
|
+
): FeedbackLocatorSourceMetadata | null {
|
|
126
|
+
const found = target.closest(`[${locatorJsDataAttribute}]`);
|
|
127
|
+
|
|
128
|
+
if (!(found instanceof HTMLElement) || typeof window === "undefined") {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const locatorId = found.getAttribute(locatorJsDataAttribute);
|
|
133
|
+
if (!locatorId) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorId);
|
|
138
|
+
const fileData = window.__LOCATOR_DATA__?.[fileKey];
|
|
139
|
+
const expression = fileData?.expressions?.[expressionIndex];
|
|
140
|
+
|
|
141
|
+
if (!fileData || !expression) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const component = getLocatorJsComponentForElement(found);
|
|
146
|
+
|
|
147
|
+
return {
|
|
148
|
+
locatorId,
|
|
149
|
+
filePath: toLocatorJsFilePath(fileData, fileKey),
|
|
150
|
+
elementName: expression.name,
|
|
151
|
+
componentName: component?.name ?? null,
|
|
152
|
+
line: expression.loc.start.line,
|
|
153
|
+
column: expression.loc.start.column + 1,
|
|
154
|
+
componentLine: component?.line ?? null,
|
|
155
|
+
componentColumn: component?.column ?? null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function toLocatorJsFilePath(fileData: LocatorJsFileStorage, fileKey: string) {
|
|
160
|
+
const filePath = fileData.filePath ?? fileKey;
|
|
161
|
+
const projectPath = fileData.projectPath;
|
|
162
|
+
|
|
163
|
+
if (projectPath && filePath.startsWith(projectPath)) {
|
|
164
|
+
return filePath.slice(projectPath.length).replace(/^\//, "");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return filePath.replace(/^\//, "");
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function getFeedbackLocatorComponent(
|
|
171
|
+
fileData: FeedbackLocatorFileStorage,
|
|
172
|
+
componentId: number | null,
|
|
173
|
+
) {
|
|
174
|
+
if (componentId === null) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const component = fileData.components[componentId];
|
|
179
|
+
if (!component) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
name: component.name,
|
|
185
|
+
line: component.loc.start.line,
|
|
186
|
+
column: component.loc.start.column + 1,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function getAncestorComponentForElement(element: HTMLElement) {
|
|
191
|
+
let parent = element.parentElement;
|
|
192
|
+
|
|
193
|
+
while (parent) {
|
|
194
|
+
const locatorId = parent.getAttribute(feedbackLocatorDataAttribute);
|
|
195
|
+
if (locatorId) {
|
|
196
|
+
const component = getFeedbackLocatorComponentForLocatorId(locatorId);
|
|
197
|
+
if (component) {
|
|
198
|
+
return component;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
parent = parent.parentElement;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getFeedbackLocatorComponentForLocatorId(locatorId: string) {
|
|
209
|
+
const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorId);
|
|
210
|
+
const fileData = window.__FEEDBACK_LOCATOR_DATA__?.[fileKey];
|
|
211
|
+
const expression = fileData?.expressions[expressionIndex];
|
|
212
|
+
|
|
213
|
+
if (!fileData || !expression) {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return getFeedbackLocatorComponent(fileData, expression.componentId);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function getLocatorJsComponentForElement(element: HTMLElement) {
|
|
221
|
+
const locatorJsId = element.getAttribute(locatorJsDataAttribute);
|
|
222
|
+
if (!locatorJsId) {
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorJsId);
|
|
227
|
+
const fileData = window.__LOCATOR_DATA__?.[fileKey];
|
|
228
|
+
const expression = fileData?.expressions?.[expressionIndex];
|
|
229
|
+
const componentId = expression?.wrappingComponentId;
|
|
230
|
+
|
|
231
|
+
if (componentId === null || componentId === undefined) {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const component = fileData?.components?.[componentId];
|
|
236
|
+
if (!component) {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
name: component.name,
|
|
242
|
+
line: component.loc?.start.line ?? null,
|
|
243
|
+
column:
|
|
244
|
+
component.loc?.start.column !== undefined
|
|
245
|
+
? component.loc.start.column + 1
|
|
246
|
+
: null,
|
|
247
|
+
};
|
|
248
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
export type FeedbackLocatorHotkey = {
|
|
2
|
+
key: string;
|
|
3
|
+
ctrl?: boolean;
|
|
4
|
+
shift?: boolean;
|
|
5
|
+
alt?: boolean;
|
|
6
|
+
meta?: boolean;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type FeedbackLocatorPoint = {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type FeedbackLocatorBounds = {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
top: number;
|
|
20
|
+
right: number;
|
|
21
|
+
bottom: number;
|
|
22
|
+
left: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type FeedbackLocatorSourceMetadata = {
|
|
26
|
+
locatorId: string;
|
|
27
|
+
filePath: string;
|
|
28
|
+
elementName: string;
|
|
29
|
+
componentName: string | null;
|
|
30
|
+
line: number;
|
|
31
|
+
column: number;
|
|
32
|
+
componentLine: number | null;
|
|
33
|
+
componentColumn: number | null;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type FeedbackLocatorElementMetadata = {
|
|
37
|
+
tagName: string;
|
|
38
|
+
id: string | null;
|
|
39
|
+
className: string | null;
|
|
40
|
+
role: string | null;
|
|
41
|
+
ariaLabel: string | null;
|
|
42
|
+
name: string | null;
|
|
43
|
+
text: string | null;
|
|
44
|
+
bounds: FeedbackLocatorBounds;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export type FeedbackLocatorBrowserMetadata = {
|
|
48
|
+
userAgent: string;
|
|
49
|
+
language: string;
|
|
50
|
+
platform: string;
|
|
51
|
+
timezone: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type FeedbackLocatorViewportMetadata = {
|
|
55
|
+
width: number;
|
|
56
|
+
height: number;
|
|
57
|
+
devicePixelRatio: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type FeedbackLocatorContextEntry = {
|
|
61
|
+
key: string;
|
|
62
|
+
data: unknown;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type FeedbackLocatorContextProviderInput = {
|
|
66
|
+
targetElement: HTMLElement;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type FeedbackLocatorContextProvider = (
|
|
70
|
+
input?: FeedbackLocatorContextProviderInput,
|
|
71
|
+
) =>
|
|
72
|
+
| FeedbackLocatorContextEntry
|
|
73
|
+
| FeedbackLocatorContextEntry[]
|
|
74
|
+
| null
|
|
75
|
+
| undefined
|
|
76
|
+
| Promise<FeedbackLocatorContextEntry | FeedbackLocatorContextEntry[] | null | undefined>;
|
|
77
|
+
|
|
78
|
+
export type FeedbackLocatorScopedContextCollector = (
|
|
79
|
+
input: FeedbackLocatorContextProviderInput,
|
|
80
|
+
) => unknown | Promise<unknown>;
|
|
81
|
+
|
|
82
|
+
export type FeedbackLocatorContextScope =
|
|
83
|
+
| HTMLElement
|
|
84
|
+
| (() => HTMLElement | null | undefined)
|
|
85
|
+
| null
|
|
86
|
+
| undefined;
|
|
87
|
+
|
|
88
|
+
export type FeedbackLocatorContextRegistration =
|
|
89
|
+
| FeedbackLocatorScopedContextCollector
|
|
90
|
+
| {
|
|
91
|
+
scope?: FeedbackLocatorContextScope;
|
|
92
|
+
collect: FeedbackLocatorScopedContextCollector;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
export type FeedbackLocatorContextPreview = {
|
|
96
|
+
context: Record<string, unknown>;
|
|
97
|
+
contextErrors: string[];
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export type FeedbackLocatorAsset = {
|
|
101
|
+
name: string;
|
|
102
|
+
url: string;
|
|
103
|
+
contentType: string;
|
|
104
|
+
size: number;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export type FeedbackLocatorMetadata = {
|
|
108
|
+
id: string;
|
|
109
|
+
appKey: string;
|
|
110
|
+
appName: string;
|
|
111
|
+
prompt: string;
|
|
112
|
+
createdAt: string;
|
|
113
|
+
url: string;
|
|
114
|
+
route: string;
|
|
115
|
+
browser: FeedbackLocatorBrowserMetadata;
|
|
116
|
+
viewport: FeedbackLocatorViewportMetadata;
|
|
117
|
+
element: FeedbackLocatorElementMetadata;
|
|
118
|
+
source: FeedbackLocatorSourceMetadata | null;
|
|
119
|
+
context: Record<string, unknown>;
|
|
120
|
+
contextErrors: string[];
|
|
121
|
+
assets?: {
|
|
122
|
+
screenshotUrl?: string;
|
|
123
|
+
annotatedScreenshotUrl?: string;
|
|
124
|
+
recording?: FeedbackLocatorAsset;
|
|
125
|
+
attachments?: FeedbackLocatorAsset[];
|
|
126
|
+
contextAttachments?: FeedbackLocatorAsset[];
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export type FeedbackLocatorSubmitInput = {
|
|
131
|
+
formData: FormData;
|
|
132
|
+
metadata: FeedbackLocatorMetadata;
|
|
133
|
+
originalScreenshot: Blob;
|
|
134
|
+
annotatedScreenshot?: Blob;
|
|
135
|
+
recording?: File;
|
|
136
|
+
attachments?: File[];
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export type FeedbackLocatorLinearIssueReference = {
|
|
140
|
+
issueId: string;
|
|
141
|
+
issueIdentifier: string;
|
|
142
|
+
issueTitle: string;
|
|
143
|
+
issueUrl: string;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type FeedbackLocatorSubmitResult = FeedbackLocatorLinearIssueReference;
|
|
147
|
+
|
|
148
|
+
export type FeedbackLocatorAgentSessionResult = {
|
|
149
|
+
sessionId: string;
|
|
150
|
+
sessionTitle: string;
|
|
151
|
+
sessionUrl: string;
|
|
152
|
+
prompt: string;
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export type FeedbackLocatorCodexThreadResult = {
|
|
156
|
+
threadId: string;
|
|
157
|
+
threadTitle: string;
|
|
158
|
+
prompt: string;
|
|
159
|
+
status: "started";
|
|
160
|
+
linearIssue?: FeedbackLocatorLinearIssueReference;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
export type FeedbackLocatorOpenCodeThreadResult = {
|
|
164
|
+
sessionId: string;
|
|
165
|
+
sessionTitle: string;
|
|
166
|
+
prompt: string;
|
|
167
|
+
status: "started" | "completed";
|
|
168
|
+
stdout?: string;
|
|
169
|
+
stderr?: string;
|
|
170
|
+
linearIssue?: FeedbackLocatorLinearIssueReference;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export type FeedbackLocatorSourceCollector = (
|
|
174
|
+
target: HTMLElement,
|
|
175
|
+
) => FeedbackLocatorSourceMetadata | null;
|
|
176
|
+
|
|
177
|
+
export type FeedbackLocatorConfig = {
|
|
178
|
+
appKey: string;
|
|
179
|
+
appName: string;
|
|
180
|
+
submitFeedback: (
|
|
181
|
+
input: FeedbackLocatorSubmitInput,
|
|
182
|
+
) => Promise<FeedbackLocatorSubmitResult>;
|
|
183
|
+
startAgentSession?: (
|
|
184
|
+
input: FeedbackLocatorSubmitInput,
|
|
185
|
+
) => Promise<FeedbackLocatorAgentSessionResult>;
|
|
186
|
+
startCodexThread?: (
|
|
187
|
+
input: FeedbackLocatorSubmitInput,
|
|
188
|
+
) => Promise<FeedbackLocatorCodexThreadResult>;
|
|
189
|
+
startOpenCodeThread?: (
|
|
190
|
+
input: FeedbackLocatorSubmitInput,
|
|
191
|
+
) => Promise<FeedbackLocatorOpenCodeThreadResult>;
|
|
192
|
+
contextProviders?: FeedbackLocatorContextProvider[];
|
|
193
|
+
hotkey?: FeedbackLocatorHotkey;
|
|
194
|
+
sourceCollector?: FeedbackLocatorSourceCollector;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
export type FeedbackLocator = {
|
|
198
|
+
config: Required<Pick<FeedbackLocatorConfig, "appKey" | "appName" | "submitFeedback">> &
|
|
199
|
+
Pick<
|
|
200
|
+
FeedbackLocatorConfig,
|
|
201
|
+
| "contextProviders"
|
|
202
|
+
| "hotkey"
|
|
203
|
+
| "sourceCollector"
|
|
204
|
+
| "startAgentSession"
|
|
205
|
+
| "startCodexThread"
|
|
206
|
+
| "startOpenCodeThread"
|
|
207
|
+
>;
|
|
208
|
+
registerContext: (
|
|
209
|
+
key: string,
|
|
210
|
+
registration: FeedbackLocatorContextRegistration,
|
|
211
|
+
) => () => void;
|
|
212
|
+
collectContext: (input: FeedbackLocatorContextProviderInput) => Promise<FeedbackLocatorContextPreview>;
|
|
213
|
+
createMetadata: (input: {
|
|
214
|
+
prompt: string;
|
|
215
|
+
targetElement: HTMLElement;
|
|
216
|
+
source: FeedbackLocatorSourceMetadata | null;
|
|
217
|
+
}) => Promise<FeedbackLocatorMetadata>;
|
|
218
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { render } from "solid-js/web";
|
|
2
|
+
import { FeedbackLocatorRoot } from "./solid";
|
|
3
|
+
import type { FeedbackLocator } from "./types";
|
|
4
|
+
|
|
5
|
+
export type FeedbackLocatorCustomElementOptions = {
|
|
6
|
+
tagName?: string;
|
|
7
|
+
shadowRoot?: boolean;
|
|
8
|
+
styles?: string | string[];
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type FeedbackLocatorCustomElement = HTMLElement & {
|
|
12
|
+
locator: FeedbackLocator | null;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const defaultFeedbackLocatorElementName = "d2d-feedbackkit";
|
|
16
|
+
|
|
17
|
+
export function defineFeedbackLocatorElement(
|
|
18
|
+
options: FeedbackLocatorCustomElementOptions = {},
|
|
19
|
+
) {
|
|
20
|
+
const tagName = options.tagName ?? defaultFeedbackLocatorElementName;
|
|
21
|
+
const existing = customElements.get(tagName);
|
|
22
|
+
|
|
23
|
+
if (existing) {
|
|
24
|
+
return existing as CustomElementConstructor;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class FeedbackKitElement extends HTMLElement {
|
|
28
|
+
#locator: FeedbackLocator | null = null;
|
|
29
|
+
#dispose: (() => void) | undefined;
|
|
30
|
+
#mount: HTMLElement | undefined;
|
|
31
|
+
|
|
32
|
+
get locator() {
|
|
33
|
+
return this.#locator;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
set locator(locator: FeedbackLocator | null) {
|
|
37
|
+
this.#locator = locator;
|
|
38
|
+
this.#render();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
connectedCallback() {
|
|
42
|
+
this.#render();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
disconnectedCallback() {
|
|
46
|
+
this.#dispose?.();
|
|
47
|
+
this.#dispose = undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#render() {
|
|
51
|
+
if (!this.isConnected || !this.#locator) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
this.#dispose?.();
|
|
56
|
+
this.#dispose = undefined;
|
|
57
|
+
this.#mount = this.#mount ?? this.#createMount();
|
|
58
|
+
this.#mount.replaceChildren();
|
|
59
|
+
this.#dispose = render(
|
|
60
|
+
() => <FeedbackLocatorRoot locator={this.#locator!} />,
|
|
61
|
+
this.#mount,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#createMount() {
|
|
66
|
+
if (!options.shadowRoot) {
|
|
67
|
+
return this;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const root = this.shadowRoot ?? this.attachShadow({ mode: "open" });
|
|
71
|
+
const styles = Array.isArray(options.styles)
|
|
72
|
+
? options.styles
|
|
73
|
+
: options.styles
|
|
74
|
+
? [options.styles]
|
|
75
|
+
: [];
|
|
76
|
+
|
|
77
|
+
for (const styleText of styles) {
|
|
78
|
+
const style = document.createElement("style");
|
|
79
|
+
style.textContent = styleText;
|
|
80
|
+
root.append(style);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const mount = document.createElement("div");
|
|
84
|
+
root.append(mount);
|
|
85
|
+
return mount;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
customElements.define(tagName, FeedbackKitElement);
|
|
90
|
+
return FeedbackKitElement;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function mountFeedbackLocatorElement(
|
|
94
|
+
locator: FeedbackLocator,
|
|
95
|
+
options: FeedbackLocatorCustomElementOptions & { target?: HTMLElement } = {},
|
|
96
|
+
) {
|
|
97
|
+
const tagName = options.tagName ?? defaultFeedbackLocatorElementName;
|
|
98
|
+
defineFeedbackLocatorElement(options);
|
|
99
|
+
|
|
100
|
+
const element = document.createElement(tagName) as FeedbackLocatorCustomElement;
|
|
101
|
+
element.locator = locator;
|
|
102
|
+
(options.target ?? document.body).append(element);
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
element,
|
|
106
|
+
dispose: () => element.remove(),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
declare global {
|
|
111
|
+
interface HTMLElementTagNameMap {
|
|
112
|
+
"d2d-feedbackkit": FeedbackLocatorCustomElement;
|
|
113
|
+
}
|
|
114
|
+
}
|
package/src/widget.tsx
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { FeedbackLocatorRoot } from "./solid";
|