d2d-feedbackkit 0.1.0 → 0.1.2
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 +6 -0
- package/dist/vite-plugin.js +319 -0
- package/package.json +5 -4
- package/src/solid.tsx +75 -59
- package/src/types.ts +10 -0
package/README.md
CHANGED
|
@@ -64,6 +64,12 @@ const locator = createFeedbackLocator({
|
|
|
64
64
|
appKey: "host-app",
|
|
65
65
|
appName: "Host App",
|
|
66
66
|
showFloatingButton: false,
|
|
67
|
+
enableVideoRecording: false,
|
|
68
|
+
theme: {
|
|
69
|
+
primary: "45 96% 53%",
|
|
70
|
+
primaryForeground: "30 70% 8%",
|
|
71
|
+
ring: "45 96% 53%",
|
|
72
|
+
},
|
|
67
73
|
submitFeedback: async (input) => {
|
|
68
74
|
const response = await fetch("https://feedbackkit.d2d.cloud/api/submissions", {
|
|
69
75
|
method: "POST",
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
// src/babel.ts
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseExpression } from "@babel/parser";
|
|
4
|
+
function feedbackLocatorBabelPlugin(babel) {
|
|
5
|
+
const t = babel.types;
|
|
6
|
+
function ensureState(state) {
|
|
7
|
+
if (!state.feedbackLocator) {
|
|
8
|
+
throw new Error("Feedback Locator Babel state is not initialized");
|
|
9
|
+
}
|
|
10
|
+
return state.feedbackLocator;
|
|
11
|
+
}
|
|
12
|
+
function shouldSkip(state) {
|
|
13
|
+
return !state.filename || state.filename.includes("node_modules");
|
|
14
|
+
}
|
|
15
|
+
function enterComponent(state, name, loc) {
|
|
16
|
+
if (!name || !loc || !isComponentName(name)) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const transformState = ensureState(state);
|
|
20
|
+
if (state.opts?.ignoreComponentNames?.includes(name)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const id = transformState.components.length;
|
|
24
|
+
transformState.components.push({ name, loc });
|
|
25
|
+
transformState.componentStack.push(id);
|
|
26
|
+
return id;
|
|
27
|
+
}
|
|
28
|
+
function exitComponent(state, id) {
|
|
29
|
+
if (id === null) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const transformState = ensureState(state);
|
|
33
|
+
const last = transformState.componentStack.at(-1);
|
|
34
|
+
if (last === id) {
|
|
35
|
+
transformState.componentStack.pop();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
name: "feedbackkit-source-locator-babel",
|
|
40
|
+
visitor: {
|
|
41
|
+
Program: {
|
|
42
|
+
enter(_programPath, state) {
|
|
43
|
+
if (shouldSkip(state)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const projectRoot = normalizePath(state.opts?.projectRoot ?? state.cwd ?? process.cwd());
|
|
47
|
+
const filename = normalizePath(state.filename);
|
|
48
|
+
const filePath = toRepoRelativePath(filename, projectRoot);
|
|
49
|
+
const fileKey = filePath;
|
|
50
|
+
state.feedbackLocator = {
|
|
51
|
+
projectRoot,
|
|
52
|
+
filePath,
|
|
53
|
+
fileKey,
|
|
54
|
+
components: [],
|
|
55
|
+
expressions: [],
|
|
56
|
+
componentStack: []
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
exit(programPath, state) {
|
|
60
|
+
if (shouldSkip(state) || !state.feedbackLocator) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const transformState = state.feedbackLocator;
|
|
64
|
+
if (transformState.expressions.length === 0) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const data = {
|
|
68
|
+
filePath: transformState.filePath,
|
|
69
|
+
projectPath: transformState.projectRoot,
|
|
70
|
+
expressions: transformState.expressions,
|
|
71
|
+
components: transformState.components
|
|
72
|
+
};
|
|
73
|
+
const serializedData = JSON.stringify(data);
|
|
74
|
+
const serializedFileKey = JSON.stringify(transformState.fileKey);
|
|
75
|
+
const registerCode = `(() => {
|
|
76
|
+
if (typeof window !== "undefined") {
|
|
77
|
+
window.__FEEDBACK_LOCATOR_DATA__ = window.__FEEDBACK_LOCATOR_DATA__ || {};
|
|
78
|
+
window.__FEEDBACK_LOCATOR_DATA__[${serializedFileKey}] = ${serializedData};
|
|
79
|
+
}
|
|
80
|
+
})()`;
|
|
81
|
+
programPath.pushContainer("body", t.expressionStatement(parseExpression(registerCode)));
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
FunctionDeclaration: {
|
|
85
|
+
enter(pathNode, state) {
|
|
86
|
+
if (shouldSkip(state) || !state.feedbackLocator) {
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
pathNode.setData("feedbackLocatorComponentId", enterComponent(state, pathNode.node.id?.name, pathNode.node.loc));
|
|
90
|
+
},
|
|
91
|
+
exit(pathNode, state) {
|
|
92
|
+
if (shouldSkip(state) || !state.feedbackLocator) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
exitComponent(state, pathNode.getData("feedbackLocatorComponentId") ?? null);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
VariableDeclarator: {
|
|
99
|
+
enter(pathNode, state) {
|
|
100
|
+
if (shouldSkip(state) || !state.feedbackLocator) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const initType = pathNode.node.init?.type;
|
|
104
|
+
const isFunction = initType === "ArrowFunctionExpression" || initType === "FunctionExpression";
|
|
105
|
+
const name = pathNode.node.id?.type === "Identifier" ? pathNode.node.id.name : null;
|
|
106
|
+
pathNode.setData("feedbackLocatorComponentId", isFunction ? enterComponent(state, name, pathNode.node.loc) : null);
|
|
107
|
+
},
|
|
108
|
+
exit(pathNode, state) {
|
|
109
|
+
if (shouldSkip(state) || !state.feedbackLocator) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
exitComponent(state, pathNode.getData("feedbackLocatorComponentId") ?? null);
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
JSXElement(pathNode, state) {
|
|
116
|
+
if (shouldSkip(state) || !state.feedbackLocator || !pathNode.node.loc) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const openingElement = pathNode.node.openingElement;
|
|
120
|
+
const name = getJsxName(openingElement.name);
|
|
121
|
+
if (!isDomJsxName(openingElement.name)) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (openingElement.attributes.some((attribute) => attribute.type === "JSXAttribute" && attribute.name?.name === "data-feedback-locator-id")) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const transformState = state.feedbackLocator;
|
|
128
|
+
const expressionId = transformState.expressions.length;
|
|
129
|
+
const componentId = transformState.componentStack.at(-1) ?? null;
|
|
130
|
+
const locatorId = `${transformState.fileKey}::${expressionId}`;
|
|
131
|
+
transformState.expressions.push({
|
|
132
|
+
name,
|
|
133
|
+
loc: pathNode.node.loc,
|
|
134
|
+
componentId
|
|
135
|
+
});
|
|
136
|
+
openingElement.attributes.push(t.jsxAttribute(t.jsxIdentifier("data-feedback-locator-id"), t.stringLiteral(locatorId)));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function getJsxName(node) {
|
|
142
|
+
if (node.type === "JSXIdentifier") {
|
|
143
|
+
return node.name;
|
|
144
|
+
}
|
|
145
|
+
if (node.type === "JSXMemberExpression") {
|
|
146
|
+
return `${getJsxName(node.object)}.${node.property.name}`;
|
|
147
|
+
}
|
|
148
|
+
if (node.type === "JSXNamespacedName") {
|
|
149
|
+
return `${node.namespace.name}.${node.name.name}`;
|
|
150
|
+
}
|
|
151
|
+
return "unknown";
|
|
152
|
+
}
|
|
153
|
+
function isDomJsxName(node) {
|
|
154
|
+
if (node.type !== "JSXIdentifier") {
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
return /^[a-z]/.test(node.name);
|
|
158
|
+
}
|
|
159
|
+
function isComponentName(name) {
|
|
160
|
+
return /^[A-Z]/.test(name);
|
|
161
|
+
}
|
|
162
|
+
function normalizePath(value) {
|
|
163
|
+
return value.split(path.sep).join("/");
|
|
164
|
+
}
|
|
165
|
+
function toRepoRelativePath(filename, projectRoot) {
|
|
166
|
+
const relative = normalizePath(path.relative(projectRoot, filename));
|
|
167
|
+
return relative.startsWith(".") ? filename : relative;
|
|
168
|
+
}
|
|
169
|
+
// src/source.ts
|
|
170
|
+
var feedbackLocatorDataAttribute = "data-feedback-locator-id";
|
|
171
|
+
var feedbackLocatorDataKey = "__FEEDBACK_LOCATOR_DATA__";
|
|
172
|
+
var locatorJsDataAttribute = "data-locatorjs-id";
|
|
173
|
+
function parseFeedbackLocatorId(locatorId) {
|
|
174
|
+
const separatorIndex = locatorId.lastIndexOf("::");
|
|
175
|
+
if (separatorIndex === -1) {
|
|
176
|
+
throw new Error("feedback locator id is malformed");
|
|
177
|
+
}
|
|
178
|
+
const fileKey = locatorId.slice(0, separatorIndex);
|
|
179
|
+
const expressionId = locatorId.slice(separatorIndex + 2);
|
|
180
|
+
if (!fileKey || !expressionId) {
|
|
181
|
+
throw new Error("feedback locator id is malformed");
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
fileKey,
|
|
185
|
+
expressionIndex: Number(expressionId)
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
function getFeedbackLocatorSourceForElement(target) {
|
|
189
|
+
const found = target.closest(`[${feedbackLocatorDataAttribute}]`);
|
|
190
|
+
if (!(found instanceof HTMLElement)) {
|
|
191
|
+
return getLocatorJsSourceForElement(target);
|
|
192
|
+
}
|
|
193
|
+
const locatorId = found.dataset.feedbackLocatorId;
|
|
194
|
+
if (!locatorId || typeof window === "undefined") {
|
|
195
|
+
return getLocatorJsSourceForElement(target);
|
|
196
|
+
}
|
|
197
|
+
const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorId);
|
|
198
|
+
const fileData = window.__FEEDBACK_LOCATOR_DATA__?.[fileKey];
|
|
199
|
+
const expression = fileData?.expressions[expressionIndex];
|
|
200
|
+
if (!fileData || !expression) {
|
|
201
|
+
return getLocatorJsSourceForElement(found);
|
|
202
|
+
}
|
|
203
|
+
const component = getFeedbackLocatorComponent(fileData, expression.componentId);
|
|
204
|
+
const locatorJsComponent = component ? null : getLocatorJsComponentForElement(found);
|
|
205
|
+
const ancestorComponent = component || locatorJsComponent ? null : getAncestorComponentForElement(found);
|
|
206
|
+
const resolvedComponent = component ?? locatorJsComponent ?? ancestorComponent;
|
|
207
|
+
return {
|
|
208
|
+
locatorId,
|
|
209
|
+
filePath: fileData.filePath,
|
|
210
|
+
elementName: expression.name,
|
|
211
|
+
componentName: resolvedComponent?.name ?? null,
|
|
212
|
+
line: expression.loc.start.line,
|
|
213
|
+
column: expression.loc.start.column + 1,
|
|
214
|
+
componentLine: resolvedComponent?.line ?? null,
|
|
215
|
+
componentColumn: resolvedComponent?.column ?? null
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function getLocatorJsSourceForElement(target) {
|
|
219
|
+
const found = target.closest(`[${locatorJsDataAttribute}]`);
|
|
220
|
+
if (!(found instanceof HTMLElement) || typeof window === "undefined") {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const locatorId = found.getAttribute(locatorJsDataAttribute);
|
|
224
|
+
if (!locatorId) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorId);
|
|
228
|
+
const fileData = window.__LOCATOR_DATA__?.[fileKey];
|
|
229
|
+
const expression = fileData?.expressions?.[expressionIndex];
|
|
230
|
+
if (!fileData || !expression) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
const component = getLocatorJsComponentForElement(found);
|
|
234
|
+
return {
|
|
235
|
+
locatorId,
|
|
236
|
+
filePath: toLocatorJsFilePath(fileData, fileKey),
|
|
237
|
+
elementName: expression.name,
|
|
238
|
+
componentName: component?.name ?? null,
|
|
239
|
+
line: expression.loc.start.line,
|
|
240
|
+
column: expression.loc.start.column + 1,
|
|
241
|
+
componentLine: component?.line ?? null,
|
|
242
|
+
componentColumn: component?.column ?? null
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
function toLocatorJsFilePath(fileData, fileKey) {
|
|
246
|
+
const filePath = fileData.filePath ?? fileKey;
|
|
247
|
+
const projectPath = fileData.projectPath;
|
|
248
|
+
if (projectPath && filePath.startsWith(projectPath)) {
|
|
249
|
+
return filePath.slice(projectPath.length).replace(/^\//, "");
|
|
250
|
+
}
|
|
251
|
+
return filePath.replace(/^\//, "");
|
|
252
|
+
}
|
|
253
|
+
function getFeedbackLocatorComponent(fileData, componentId) {
|
|
254
|
+
if (componentId === null) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
const component = fileData.components[componentId];
|
|
258
|
+
if (!component) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
return {
|
|
262
|
+
name: component.name,
|
|
263
|
+
line: component.loc.start.line,
|
|
264
|
+
column: component.loc.start.column + 1
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function getAncestorComponentForElement(element) {
|
|
268
|
+
let parent = element.parentElement;
|
|
269
|
+
while (parent) {
|
|
270
|
+
const locatorId = parent.getAttribute(feedbackLocatorDataAttribute);
|
|
271
|
+
if (locatorId) {
|
|
272
|
+
const component = getFeedbackLocatorComponentForLocatorId(locatorId);
|
|
273
|
+
if (component) {
|
|
274
|
+
return component;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
parent = parent.parentElement;
|
|
278
|
+
}
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
function getFeedbackLocatorComponentForLocatorId(locatorId) {
|
|
282
|
+
const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorId);
|
|
283
|
+
const fileData = window.__FEEDBACK_LOCATOR_DATA__?.[fileKey];
|
|
284
|
+
const expression = fileData?.expressions[expressionIndex];
|
|
285
|
+
if (!fileData || !expression) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
return getFeedbackLocatorComponent(fileData, expression.componentId);
|
|
289
|
+
}
|
|
290
|
+
function getLocatorJsComponentForElement(element) {
|
|
291
|
+
const locatorJsId = element.getAttribute(locatorJsDataAttribute);
|
|
292
|
+
if (!locatorJsId) {
|
|
293
|
+
return null;
|
|
294
|
+
}
|
|
295
|
+
const { fileKey, expressionIndex } = parseFeedbackLocatorId(locatorJsId);
|
|
296
|
+
const fileData = window.__LOCATOR_DATA__?.[fileKey];
|
|
297
|
+
const expression = fileData?.expressions?.[expressionIndex];
|
|
298
|
+
const componentId = expression?.wrappingComponentId;
|
|
299
|
+
if (componentId === null || componentId === undefined) {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
const component = fileData?.components?.[componentId];
|
|
303
|
+
if (!component) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
name: component.name,
|
|
308
|
+
line: component.loc?.start.line ?? null,
|
|
309
|
+
column: component.loc?.start.column !== undefined ? component.loc.start.column + 1 : null
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
export {
|
|
313
|
+
parseFeedbackLocatorId,
|
|
314
|
+
getFeedbackLocatorSourceForElement,
|
|
315
|
+
feedbackLocatorDataKey,
|
|
316
|
+
feedbackLocatorDataAttribute,
|
|
317
|
+
feedbackLocatorBabelPlugin as feedbackKitBabelPlugin,
|
|
318
|
+
feedbackLocatorBabelPlugin as default
|
|
319
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "d2d-feedbackkit",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SolidJS feedback widget, source locator, and integration helpers.",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"./vite-plugin": {
|
|
26
26
|
"types": "./src/vite-plugin.ts",
|
|
27
|
-
"default": "./
|
|
27
|
+
"default": "./dist/vite-plugin.js"
|
|
28
28
|
},
|
|
29
29
|
"./client": {
|
|
30
30
|
"types": "./src/client.ts",
|
|
@@ -61,10 +61,11 @@
|
|
|
61
61
|
"src/types.ts",
|
|
62
62
|
"src/vite-plugin.ts",
|
|
63
63
|
"src/web-component.tsx",
|
|
64
|
-
"src/widget.tsx"
|
|
64
|
+
"src/widget.tsx",
|
|
65
|
+
"dist/vite-plugin.js"
|
|
65
66
|
],
|
|
66
67
|
"scripts": {
|
|
67
|
-
"build": "bun run type-check",
|
|
68
|
+
"build": "bun run type-check && bun build src/vite-plugin.ts --outfile dist/vite-plugin.js --target node --format esm --packages external",
|
|
68
69
|
"clean": "rm -rf node_modules dist",
|
|
69
70
|
"pack:dry-run": "npm pack --dry-run",
|
|
70
71
|
"publish:public": "npm publish --access public",
|
package/src/solid.tsx
CHANGED
|
@@ -37,10 +37,11 @@ import type {
|
|
|
37
37
|
FeedbackLocatorHotkey,
|
|
38
38
|
FeedbackLocatorPoint,
|
|
39
39
|
FeedbackLocatorSourceMetadata,
|
|
40
|
+
FeedbackLocatorTheme,
|
|
40
41
|
} from "./types";
|
|
41
42
|
import { createShortcutResolver } from "./shortcuts";
|
|
42
43
|
|
|
43
|
-
type FeedbackLocatorRootProps = {
|
|
44
|
+
export type FeedbackLocatorRootProps = {
|
|
44
45
|
locator: FeedbackLocator;
|
|
45
46
|
open?: boolean;
|
|
46
47
|
defaultOpen?: boolean;
|
|
@@ -48,6 +49,8 @@ type FeedbackLocatorRootProps = {
|
|
|
48
49
|
onClose?: (reason: FeedbackLocatorCloseReason) => void;
|
|
49
50
|
onSubmitted?: (result: FeedbackLocatorHostedSubmitResult) => void;
|
|
50
51
|
showFloatingButton?: boolean;
|
|
52
|
+
enableVideoRecording?: boolean;
|
|
53
|
+
theme?: FeedbackLocatorTheme;
|
|
51
54
|
};
|
|
52
55
|
|
|
53
56
|
type FeedbackLocatorMode = "idle" | "selecting" | "annotating" | "submitting";
|
|
@@ -1309,6 +1312,17 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1309
1312
|
const snapshot = () => selected();
|
|
1310
1313
|
const selectionRect = () => hoverRect();
|
|
1311
1314
|
const currentTextDraft = () => textDraft();
|
|
1315
|
+
const isVideoRecordingEnabled = () =>
|
|
1316
|
+
(props.enableVideoRecording ?? props.locator.config.enableVideoRecording) === true;
|
|
1317
|
+
const themeStyle = (): JSX.CSSProperties => {
|
|
1318
|
+
const theme = props.theme ?? props.locator.config.theme;
|
|
1319
|
+
|
|
1320
|
+
return {
|
|
1321
|
+
...(theme?.primary ? { "--primary": theme.primary } : {}),
|
|
1322
|
+
...(theme?.primaryForeground ? { "--primary-foreground": theme.primaryForeground } : {}),
|
|
1323
|
+
...(theme?.ring ? { "--ring": theme.ring } : {}),
|
|
1324
|
+
};
|
|
1325
|
+
};
|
|
1312
1326
|
const canvasCursor = () => {
|
|
1313
1327
|
if (tool() === "select") {
|
|
1314
1328
|
return isMovingAnnotation() ? "grabbing" : "grab";
|
|
@@ -1322,7 +1336,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1322
1336
|
};
|
|
1323
1337
|
|
|
1324
1338
|
return (
|
|
1325
|
-
<div data-feedback-locator-ui="true">
|
|
1339
|
+
<div data-feedback-locator-ui="true" style={themeStyle()}>
|
|
1326
1340
|
<Show
|
|
1327
1341
|
when={
|
|
1328
1342
|
mode() === "idle" &&
|
|
@@ -1615,70 +1629,72 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
|
|
|
1615
1629
|
</Show>
|
|
1616
1630
|
</div>
|
|
1617
1631
|
|
|
1618
|
-
<
|
|
1619
|
-
<
|
|
1620
|
-
|
|
1621
|
-
<div class="
|
|
1622
|
-
<
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
type="button"
|
|
1627
|
-
class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted disabled:opacity-60"
|
|
1628
|
-
disabled={mode() === "submitting" || !supportsScreenRecording()}
|
|
1629
|
-
onClick={startRecording}
|
|
1630
|
-
>
|
|
1631
|
-
<Video class="h-4 w-4" />
|
|
1632
|
-
Neem video op
|
|
1633
|
-
</button>
|
|
1634
|
-
}
|
|
1635
|
-
>
|
|
1636
|
-
<button
|
|
1637
|
-
type="button"
|
|
1638
|
-
class="inline-flex h-9 items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 text-sm font-medium transition hover:bg-destructive/15"
|
|
1639
|
-
disabled={mode() === "submitting"}
|
|
1640
|
-
onClick={stopRecording}
|
|
1641
|
-
>
|
|
1642
|
-
<StopCircle class="h-4 w-4" />
|
|
1643
|
-
Stop opname
|
|
1644
|
-
</button>
|
|
1645
|
-
</Show>
|
|
1646
|
-
|
|
1647
|
-
<Show when={recordingBlob()}>
|
|
1648
|
-
{(blob) => (
|
|
1649
|
-
<>
|
|
1650
|
-
<a
|
|
1651
|
-
class="inline-flex h-9 max-w-full items-center gap-2 truncate rounded-md border px-3 text-sm underline-offset-4 hover:bg-muted hover:underline"
|
|
1652
|
-
href={recordingUrl() ?? undefined}
|
|
1653
|
-
target="_blank"
|
|
1654
|
-
rel="noreferrer"
|
|
1655
|
-
>
|
|
1656
|
-
<Video class="h-4 w-4 shrink-0" />
|
|
1657
|
-
<span class="truncate">
|
|
1658
|
-
screen-recording.webm ({formatBytes(blob().size)},{" "}
|
|
1659
|
-
{formatDuration(recordingDurationMs())})
|
|
1660
|
-
</span>
|
|
1661
|
-
</a>
|
|
1632
|
+
<Show when={isVideoRecordingEnabled()}>
|
|
1633
|
+
<div>
|
|
1634
|
+
<label class="mb-2 block text-sm font-medium">Video-opname</label>
|
|
1635
|
+
<div class="space-y-2 rounded-md border bg-background p-3">
|
|
1636
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
1637
|
+
<Show
|
|
1638
|
+
when={recordingState() === "recording"}
|
|
1639
|
+
fallback={
|
|
1662
1640
|
<button
|
|
1663
1641
|
type="button"
|
|
1664
|
-
class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted"
|
|
1665
|
-
disabled={mode() === "submitting"}
|
|
1666
|
-
onClick={
|
|
1642
|
+
class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted disabled:opacity-60"
|
|
1643
|
+
disabled={mode() === "submitting" || !supportsScreenRecording()}
|
|
1644
|
+
onClick={startRecording}
|
|
1667
1645
|
>
|
|
1668
|
-
<
|
|
1669
|
-
|
|
1646
|
+
<Video class="h-4 w-4" />
|
|
1647
|
+
Neem video op
|
|
1670
1648
|
</button>
|
|
1671
|
-
|
|
1672
|
-
|
|
1649
|
+
}
|
|
1650
|
+
>
|
|
1651
|
+
<button
|
|
1652
|
+
type="button"
|
|
1653
|
+
class="inline-flex h-9 items-center gap-2 rounded-md border border-destructive/40 bg-destructive/10 px-3 text-sm font-medium transition hover:bg-destructive/15"
|
|
1654
|
+
disabled={mode() === "submitting"}
|
|
1655
|
+
onClick={stopRecording}
|
|
1656
|
+
>
|
|
1657
|
+
<StopCircle class="h-4 w-4" />
|
|
1658
|
+
Stop opname
|
|
1659
|
+
</button>
|
|
1660
|
+
</Show>
|
|
1661
|
+
|
|
1662
|
+
<Show when={recordingBlob()}>
|
|
1663
|
+
{(blob) => (
|
|
1664
|
+
<>
|
|
1665
|
+
<a
|
|
1666
|
+
class="inline-flex h-9 max-w-full items-center gap-2 truncate rounded-md border px-3 text-sm underline-offset-4 hover:bg-muted hover:underline"
|
|
1667
|
+
href={recordingUrl() ?? undefined}
|
|
1668
|
+
target="_blank"
|
|
1669
|
+
rel="noreferrer"
|
|
1670
|
+
>
|
|
1671
|
+
<Video class="h-4 w-4 shrink-0" />
|
|
1672
|
+
<span class="truncate">
|
|
1673
|
+
screen-recording.webm ({formatBytes(blob().size)},{" "}
|
|
1674
|
+
{formatDuration(recordingDurationMs())})
|
|
1675
|
+
</span>
|
|
1676
|
+
</a>
|
|
1677
|
+
<button
|
|
1678
|
+
type="button"
|
|
1679
|
+
class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted"
|
|
1680
|
+
disabled={mode() === "submitting"}
|
|
1681
|
+
onClick={clearRecording}
|
|
1682
|
+
>
|
|
1683
|
+
<Trash2 class="h-4 w-4" />
|
|
1684
|
+
Wissen
|
|
1685
|
+
</button>
|
|
1686
|
+
</>
|
|
1687
|
+
)}
|
|
1688
|
+
</Show>
|
|
1689
|
+
</div>
|
|
1690
|
+
<Show when={!supportsScreenRecording()}>
|
|
1691
|
+
<div class="text-xs text-muted-foreground">
|
|
1692
|
+
Video-opname is niet beschikbaar in deze browser.
|
|
1693
|
+
</div>
|
|
1673
1694
|
</Show>
|
|
1674
1695
|
</div>
|
|
1675
|
-
<Show when={!supportsScreenRecording()}>
|
|
1676
|
-
<div class="text-xs text-muted-foreground">
|
|
1677
|
-
Video-opname is niet beschikbaar in deze browser.
|
|
1678
|
-
</div>
|
|
1679
|
-
</Show>
|
|
1680
1696
|
</div>
|
|
1681
|
-
</
|
|
1697
|
+
</Show>
|
|
1682
1698
|
|
|
1683
1699
|
<Accordion title="Broncontext" defaultOpen>
|
|
1684
1700
|
<div class="space-y-1">
|
package/src/types.ts
CHANGED
|
@@ -191,6 +191,12 @@ export type FeedbackLocatorSourceCollector = (
|
|
|
191
191
|
target: HTMLElement,
|
|
192
192
|
) => FeedbackLocatorSourceMetadata | null;
|
|
193
193
|
|
|
194
|
+
export type FeedbackLocatorTheme = {
|
|
195
|
+
primary?: string;
|
|
196
|
+
primaryForeground?: string;
|
|
197
|
+
ring?: string;
|
|
198
|
+
};
|
|
199
|
+
|
|
194
200
|
export type FeedbackLocatorConfig = {
|
|
195
201
|
appKey: string;
|
|
196
202
|
appName: string;
|
|
@@ -209,6 +215,8 @@ export type FeedbackLocatorConfig = {
|
|
|
209
215
|
contextProviders?: FeedbackLocatorContextProvider[];
|
|
210
216
|
hotkey?: FeedbackLocatorHotkey;
|
|
211
217
|
showFloatingButton?: boolean;
|
|
218
|
+
enableVideoRecording?: boolean;
|
|
219
|
+
theme?: FeedbackLocatorTheme;
|
|
212
220
|
sourceCollector?: FeedbackLocatorSourceCollector;
|
|
213
221
|
};
|
|
214
222
|
|
|
@@ -219,6 +227,8 @@ export type FeedbackLocator = {
|
|
|
219
227
|
| "contextProviders"
|
|
220
228
|
| "hotkey"
|
|
221
229
|
| "showFloatingButton"
|
|
230
|
+
| "enableVideoRecording"
|
|
231
|
+
| "theme"
|
|
222
232
|
| "sourceCollector"
|
|
223
233
|
| "startAgentSession"
|
|
224
234
|
| "startCodexThread"
|