d2d-feedbackkit 0.0.0 → 0.1.1

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 CHANGED
@@ -11,3 +11,86 @@ import { createFeedbackLocator } from "d2d-feedbackkit";
11
11
  import { mountFeedbackLocatorElement } from "d2d-feedbackkit/web-component";
12
12
  import feedbackKitBabelPlugin from "d2d-feedbackkit/vite-plugin";
13
13
  ```
14
+
15
+ ## Solid host with your own button
16
+
17
+ ```tsx
18
+ import { createSignal } from "solid-js";
19
+ import { createFeedbackLocator } from "d2d-feedbackkit";
20
+ import { FeedbackLocatorRoot } from "d2d-feedbackkit/widget";
21
+ import "d2d-feedbackkit/styles.css";
22
+
23
+ const locator = createFeedbackLocator({
24
+ appKey: "morgo-app",
25
+ appName: "Morgo App",
26
+ showFloatingButton: false,
27
+ submitFeedback: async (input) => {
28
+ const response = await fetch("/api/feedback", {
29
+ method: "POST",
30
+ body: input.formData,
31
+ });
32
+ return response.json();
33
+ },
34
+ });
35
+
36
+ export function FeedbackButton() {
37
+ const [open, setOpen] = createSignal(false);
38
+
39
+ return (
40
+ <>
41
+ <button type="button" onClick={() => setOpen(true)}>
42
+ Feedback
43
+ </button>
44
+ <FeedbackLocatorRoot
45
+ locator={locator}
46
+ open={open()}
47
+ onOpenChange={setOpen}
48
+ onSubmitted={() => toast.success("Feedback verstuurd")}
49
+ />
50
+ </>
51
+ );
52
+ }
53
+ ```
54
+
55
+ ## Web component host
56
+
57
+ ```ts
58
+ import { createFeedbackLocator } from "d2d-feedbackkit";
59
+ import { defineFeedbackLocatorElement } from "d2d-feedbackkit/web-component";
60
+
61
+ defineFeedbackLocatorElement({ shadowRoot: true });
62
+
63
+ const locator = createFeedbackLocator({
64
+ appKey: "host-app",
65
+ appName: "Host App",
66
+ showFloatingButton: false,
67
+ enableVideoRecording: false,
68
+ theme: {
69
+ primary: "45 96% 53%",
70
+ primaryForeground: "30 70% 8%",
71
+ ring: "45 96% 53%",
72
+ },
73
+ submitFeedback: async (input) => {
74
+ const response = await fetch("https://feedbackkit.d2d.cloud/api/submissions", {
75
+ method: "POST",
76
+ headers: { "x-api-key": "<feedbackkit-api-key>" },
77
+ body: input.formData,
78
+ });
79
+ return response.json();
80
+ },
81
+ });
82
+
83
+ const widget = document.querySelector("d2d-feedbackkit");
84
+ widget.locator = locator;
85
+ widget.showFloatingButton = false;
86
+
87
+ document.querySelector("#feedback-button").addEventListener("click", () => {
88
+ widget.open = true;
89
+ });
90
+
91
+ widget.addEventListener("feedbackkit-submitted", () => {
92
+ showToast("Feedback verstuurd");
93
+ });
94
+ ```
95
+
96
+ Without the `open` property the widget remains backwards compatible and shows its own floating feedback button by default.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "d2d-feedbackkit",
3
- "version": "0.0.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "SolidJS feedback widget, source locator, and integration helpers.",
6
6
  "license": "UNLICENSED",
@@ -37,6 +37,13 @@
37
37
  "./agent": {
38
38
  "types": "./src/agent.ts",
39
39
  "default": "./src/agent.ts"
40
+ },
41
+ "./styles": {
42
+ "types": "./src/styles.ts",
43
+ "default": "./src/styles.ts"
44
+ },
45
+ "./styles.css": {
46
+ "default": "./src/styles.css"
40
47
  }
41
48
  },
42
49
  "files": [
@@ -49,6 +56,8 @@
49
56
  "src/shortcuts.ts",
50
57
  "src/solid.tsx",
51
58
  "src/source.ts",
59
+ "src/styles.css",
60
+ "src/styles.ts",
52
61
  "src/types.ts",
53
62
  "src/vite-plugin.ts",
54
63
  "src/web-component.tsx",
package/src/index.ts CHANGED
@@ -34,6 +34,7 @@ export function createFeedbackLocator(config: FeedbackLocatorConfig): FeedbackLo
34
34
  const resolvedConfig = {
35
35
  ...config,
36
36
  hotkey: config.hotkey ?? defaultFeedbackLocatorHotkey,
37
+ showFloatingButton: config.showFloatingButton ?? true,
37
38
  contextProviders: config.contextProviders ?? [],
38
39
  sourceCollector: config.sourceCollector ?? getFeedbackLocatorSourceForElement,
39
40
  };
package/src/solid.tsx CHANGED
@@ -1,8 +1,6 @@
1
1
  import { toBlob } from "html-to-image";
2
2
  import {
3
3
  ArrowUpRight,
4
- Bot,
5
- CheckCircle2,
6
4
  ChevronDown,
7
5
  FileUp,
8
6
  Maximize2,
@@ -32,24 +30,31 @@ import {
32
30
  } from "solid-js";
33
31
  import type {
34
32
  FeedbackLocator,
35
- FeedbackLocatorAgentSessionResult,
36
33
  FeedbackLocatorBounds,
37
- FeedbackLocatorCodexThreadResult,
34
+ FeedbackLocatorCloseReason,
38
35
  FeedbackLocatorContextPreview,
36
+ FeedbackLocatorHostedSubmitResult,
39
37
  FeedbackLocatorHotkey,
40
- FeedbackLocatorOpenCodeThreadResult,
41
38
  FeedbackLocatorPoint,
42
39
  FeedbackLocatorSourceMetadata,
43
- FeedbackLocatorSubmitResult,
40
+ FeedbackLocatorTheme,
44
41
  } from "./types";
45
42
  import { createShortcutResolver } from "./shortcuts";
46
43
 
47
- type FeedbackLocatorRootProps = {
44
+ export type FeedbackLocatorRootProps = {
48
45
  locator: FeedbackLocator;
46
+ open?: boolean;
47
+ defaultOpen?: boolean;
48
+ onOpenChange?: (open: boolean) => void;
49
+ onClose?: (reason: FeedbackLocatorCloseReason) => void;
50
+ onSubmitted?: (result: FeedbackLocatorHostedSubmitResult) => void;
51
+ showFloatingButton?: boolean;
52
+ enableVideoRecording?: boolean;
53
+ theme?: FeedbackLocatorTheme;
49
54
  };
50
55
 
51
56
  type FeedbackLocatorMode = "idle" | "selecting" | "annotating" | "submitting";
52
- type FeedbackLocatorSubmitTarget = "linear" | "agent" | "codex" | "opencode";
57
+ type FeedbackLocatorSubmitTarget = "feedback";
53
58
  type AnnotationTool = "select" | "pen" | "rect" | "arrow" | "text";
54
59
 
55
60
  type PenAnnotation = {
@@ -194,6 +199,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
194
199
  let annotationDragState: AnnotationDragState | null = null;
195
200
 
196
201
  const [mode, setMode] = createSignal<FeedbackLocatorMode>("idle");
202
+ const [uncontrolledOpen, setUncontrolledOpen] = createSignal(Boolean(props.defaultOpen));
197
203
  const [hoverRect, setHoverRect] = createSignal<FeedbackLocatorBounds | null>(null);
198
204
  const [selected, setSelected] = createSignal<ScreenshotState | null>(null);
199
205
  const [annotations, setAnnotations] = createSignal<Annotation[]>([]);
@@ -218,14 +224,8 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
218
224
  const [recordingUrl, setRecordingUrl] = createSignal<string | null>(null);
219
225
  const [recordingDurationMs, setRecordingDurationMs] = createSignal<number | null>(null);
220
226
  const [error, setError] = createSignal<string | null>(null);
227
+ const [successToast, setSuccessToast] = createSignal<string | null>(null);
221
228
  const [submitTarget, setSubmitTarget] = createSignal<FeedbackLocatorSubmitTarget | null>(null);
222
- const [linearResult, setLinearResult] = createSignal<FeedbackLocatorSubmitResult | null>(null);
223
- const [agentSession, setAgentSession] =
224
- createSignal<FeedbackLocatorAgentSessionResult | null>(null);
225
- const [codexThread, setCodexThread] =
226
- createSignal<FeedbackLocatorCodexThreadResult | null>(null);
227
- const [openCodeThread, setOpenCodeThread] =
228
- createSignal<FeedbackLocatorOpenCodeThreadResult | null>(null);
229
229
  let mediaRecorder: MediaRecorder | undefined;
230
230
  let recordingStream: MediaStream | undefined;
231
231
  let recordingStartedAt = 0;
@@ -240,7 +240,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
240
240
 
241
241
  if (matchesHotkey(event, props.locator.config.hotkey!)) {
242
242
  event.preventDefault();
243
- startSelection();
243
+ openWidget();
244
244
  }
245
245
  };
246
246
 
@@ -252,6 +252,18 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
252
252
  });
253
253
  });
254
254
 
255
+ createEffect(() => {
256
+ const shouldBeOpen = isWidgetOpen();
257
+
258
+ if (shouldBeOpen && mode() === "idle") {
259
+ beginSelection();
260
+ }
261
+
262
+ if (!shouldBeOpen && mode() !== "idle") {
263
+ resetState();
264
+ }
265
+ });
266
+
255
267
  createEffect(() => {
256
268
  if (mode() !== "selecting") {
257
269
  return;
@@ -280,7 +292,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
280
292
  const handleKeydown = (event: KeyboardEvent) => {
281
293
  if (event.key === "Escape") {
282
294
  event.preventDefault();
283
- reset();
295
+ requestClose("escape");
284
296
  }
285
297
  };
286
298
 
@@ -387,7 +399,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
387
399
  }
388
400
 
389
401
  if (action === "close") {
390
- reset();
402
+ requestClose("escape");
391
403
  }
392
404
  };
393
405
 
@@ -395,18 +407,38 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
395
407
  onCleanup(() => document.removeEventListener("keydown", handleKeydown));
396
408
  });
397
409
 
398
- function startSelection() {
410
+ function isWidgetOpen() {
411
+ return props.open ?? uncontrolledOpen();
412
+ }
413
+
414
+ function setWidgetOpen(open: boolean) {
415
+ if (props.open === undefined) {
416
+ setUncontrolledOpen(open);
417
+ }
418
+ props.onOpenChange?.(open);
419
+ }
420
+
421
+ function openWidget() {
422
+ setWidgetOpen(true);
423
+ if (mode() === "idle") {
424
+ beginSelection();
425
+ }
426
+ }
427
+
428
+ function requestClose(reason: FeedbackLocatorCloseReason) {
429
+ resetState();
430
+ setWidgetOpen(false);
431
+ props.onClose?.(reason);
432
+ }
433
+
434
+ function beginSelection() {
399
435
  setError(null);
400
436
  setSubmitTarget(null);
401
- setLinearResult(null);
402
- setAgentSession(null);
403
- setCodexThread(null);
404
437
  setSelected(null);
405
438
  setAnnotations([]);
406
439
  setAnnotationHistory([]);
407
440
  setRedoStack([]);
408
441
  setDraft(null);
409
- setTextDraft(null);
410
442
  setSelectedAnnotationIndex(null);
411
443
  setContextPreview(null);
412
444
  annotationDragState = null;
@@ -417,7 +449,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
417
449
  setMode("selecting");
418
450
  }
419
451
 
420
- function reset() {
452
+ function resetState() {
421
453
  const current = selected();
422
454
  if (current) {
423
455
  URL.revokeObjectURL(current.originalUrl);
@@ -440,8 +472,6 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
440
472
  clearRecording();
441
473
  setError(null);
442
474
  setSubmitTarget(null);
443
- setLinearResult(null);
444
- setAgentSession(null);
445
475
  }
446
476
 
447
477
  async function captureElement(element: HTMLElement) {
@@ -630,68 +660,12 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
630
660
  }
631
661
 
632
662
  async function submitFeedback() {
633
- await runSubmission("linear", async (prepared) => {
634
- const pendingWindow = openPendingExternalWindow(
635
- "Linear issue aanmaken...",
636
- "De feedback wordt verwerkt en de Linear issue wordt geopend.",
637
- );
638
-
639
- try {
640
- const submitResult = await props.locator.config.submitFeedback(prepared);
641
- setLinearResult(submitResult);
642
- openExternalWindow(submitResult.issueUrl, pendingWindow);
643
- } catch (submitError) {
644
- closePendingWindow(pendingWindow);
645
- throw submitError;
646
- }
647
- });
648
- }
649
-
650
- async function startAgentSession() {
651
- const startAgentSessionAction = props.locator.config.startAgentSession;
652
-
653
- if (!startAgentSessionAction) {
654
- setError("Agent sessies zijn nog niet gekoppeld voor deze app.");
655
- return;
656
- }
657
-
658
- await runSubmission("agent", async (prepared) => {
659
- const pendingWindow = openPendingAgentSessionWindow();
660
- const session = await startAgentSessionAction(prepared);
661
- setAgentSession(session);
662
- openAgentSessionWindow(session, pendingWindow);
663
- });
664
- }
665
-
666
- async function startCodexThread() {
667
- const startCodexThreadAction = props.locator.config.startCodexThread;
668
-
669
- if (!startCodexThreadAction) {
670
- setError("Codex threads zijn nog niet gekoppeld voor deze app.");
671
- return;
672
- }
673
-
674
- await runSubmission("codex", async (prepared) => {
675
- const thread = await startCodexThreadAction(prepared);
676
- setCodexThread(thread);
677
-
678
- if (thread.linearIssue) {
679
- openExternalWindow(thread.linearIssue.issueUrl);
680
- }
681
- });
682
- }
683
-
684
- async function startOpenCodeThread() {
685
- const startOpenCodeThreadAction = props.locator.config.startOpenCodeThread;
686
-
687
- if (!startOpenCodeThreadAction) {
688
- setError("OpenCode sessies zijn nog niet gekoppeld voor deze app.");
689
- return;
690
- }
691
-
692
- await runSubmission("opencode", async (prepared) => {
693
- const thread = await startOpenCodeThreadAction(prepared);
694
- setOpenCodeThread(thread);
663
+ await runSubmission("feedback", async (prepared) => {
664
+ const submitResult = await props.locator.config.submitFeedback(prepared);
665
+ props.onSubmitted?.(submitResult);
666
+ setSuccessToast("Feedback verstuurd");
667
+ window.setTimeout(() => setSuccessToast(null), 4000);
668
+ requestClose("submitted");
695
669
  });
696
670
  }
697
671
 
@@ -711,8 +685,10 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
711
685
  }
712
686
 
713
687
  await submit(prepared);
714
- applySubmissionAnnotations(prepared);
715
- setMode("annotating");
688
+ if (mode() === "submitting") {
689
+ applySubmissionAnnotations(prepared);
690
+ setMode("annotating");
691
+ }
716
692
  } catch (submitError) {
717
693
  setMode("annotating");
718
694
  setError(
@@ -1336,6 +1312,17 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1336
1312
  const snapshot = () => selected();
1337
1313
  const selectionRect = () => hoverRect();
1338
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
+ };
1339
1326
  const canvasCursor = () => {
1340
1327
  if (tool() === "select") {
1341
1328
  return isMovingAnnotation() ? "grabbing" : "grab";
@@ -1349,12 +1336,17 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1349
1336
  };
1350
1337
 
1351
1338
  return (
1352
- <div data-feedback-locator-ui="true">
1353
- <Show when={mode() === "idle"}>
1339
+ <div data-feedback-locator-ui="true" style={themeStyle()}>
1340
+ <Show
1341
+ when={
1342
+ mode() === "idle" &&
1343
+ (props.showFloatingButton ?? props.locator.config.showFloatingButton) !== false
1344
+ }
1345
+ >
1354
1346
  <button
1355
1347
  type="button"
1356
1348
  class="fixed bottom-5 right-5 z-[2147483000] inline-flex h-11 items-center gap-2 rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground shadow-md transition hover:bg-primary/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
1357
- onClick={startSelection}
1349
+ onClick={openWidget}
1358
1350
  title="Feedback toevoegen (Ctrl+Shift+F)"
1359
1351
  >
1360
1352
  <MessageSquarePlus class="h-4 w-4" />
@@ -1368,6 +1360,14 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1368
1360
  </div>
1369
1361
  </Show>
1370
1362
 
1363
+ <Show when={successToast()}>
1364
+ {(message) => (
1365
+ <div class="fixed bottom-20 right-5 z-[2147483000] max-w-sm rounded-md border border-primary/30 bg-background px-4 py-3 text-sm font-medium text-foreground shadow-md">
1366
+ {message()}
1367
+ </div>
1368
+ )}
1369
+ </Show>
1370
+
1371
1371
  <Show when={mode() === "selecting"}>
1372
1372
  <div class="fixed left-1/2 top-4 z-[2147483000] flex -translate-x-1/2 items-center gap-2 rounded-md border bg-background px-3 py-2 text-sm text-foreground shadow-md">
1373
1373
  <MousePointer2 class="h-4 w-4 text-primary" />
@@ -1375,7 +1375,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1375
1375
  <button
1376
1376
  type="button"
1377
1377
  class="ml-2 rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
1378
- onClick={reset}
1378
+ onClick={() => requestClose("user")}
1379
1379
  title="Annuleren"
1380
1380
  >
1381
1381
  <X class="h-4 w-4" />
@@ -1410,7 +1410,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1410
1410
  <button
1411
1411
  type="button"
1412
1412
  class="rounded-md p-2 text-muted-foreground hover:bg-muted hover:text-foreground"
1413
- onClick={reset}
1413
+ onClick={() => requestClose("user")}
1414
1414
  title="Sluiten"
1415
1415
  >
1416
1416
  <X class="h-5 w-5" />
@@ -1592,109 +1592,6 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1592
1592
 
1593
1593
  <aside class="flex min-h-0 flex-col bg-background">
1594
1594
  <div class="space-y-4 overflow-auto p-4">
1595
- <Show when={linearResult()}>
1596
- {(issue) => (
1597
- <div class="rounded-md border border-primary/40 bg-primary/10 p-3 text-sm">
1598
- <div class="flex items-center gap-2 font-medium">
1599
- <CheckCircle2 class="h-4 w-4 text-primary" />
1600
- Linear issue aangemaakt
1601
- </div>
1602
- <a
1603
- class="mt-2 inline-flex items-center gap-1 text-primary underline-offset-4 hover:underline"
1604
- href={issue().issueUrl}
1605
- target="_blank"
1606
- rel="noreferrer"
1607
- >
1608
- {issue().issueIdentifier}
1609
- <ArrowUpRight class="h-3.5 w-3.5" />
1610
- </a>
1611
- </div>
1612
- )}
1613
- </Show>
1614
-
1615
- <Show when={agentSession()}>
1616
- {(session) => (
1617
- <div class="rounded-md border border-sky-500/40 bg-sky-500/10 p-3 text-sm">
1618
- <div class="flex items-center gap-2 font-medium">
1619
- <CheckCircle2 class="h-4 w-4 text-sky-500" />
1620
- Agent sessie gestart
1621
- </div>
1622
- <Show
1623
- when={session().sessionUrl}
1624
- fallback={
1625
- <div class="mt-2 text-muted-foreground">{session().sessionId}</div>
1626
- }
1627
- >
1628
- {(url) => (
1629
- <a
1630
- class="mt-2 inline-flex items-center gap-1 text-sky-600 underline-offset-4 hover:underline"
1631
- href={url()}
1632
- target="_blank"
1633
- rel="noreferrer"
1634
- >
1635
- {session().sessionTitle || session().sessionId}
1636
- <ArrowUpRight class="h-3.5 w-3.5" />
1637
- </a>
1638
- )}
1639
- </Show>
1640
- </div>
1641
- )}
1642
- </Show>
1643
-
1644
- <Show when={codexThread()}>
1645
- {(thread) => (
1646
- <div class="rounded-md border border-violet-500/40 bg-violet-500/10 p-3 text-sm">
1647
- <div class="flex items-center gap-2 font-medium">
1648
- <CheckCircle2 class="h-4 w-4 text-violet-500" />
1649
- Codex thread gestart
1650
- </div>
1651
- <div class="mt-2 text-muted-foreground">
1652
- {thread().threadTitle || thread().threadId}
1653
- </div>
1654
- <Show when={thread().linearIssue}>
1655
- {(issue) => (
1656
- <a
1657
- class="mt-2 inline-flex items-center gap-1 text-violet-600 underline-offset-4 hover:underline"
1658
- href={issue().issueUrl}
1659
- target="_blank"
1660
- rel="noreferrer"
1661
- >
1662
- Linear: {issue().issueIdentifier}
1663
- <ArrowUpRight class="h-3.5 w-3.5" />
1664
- </a>
1665
- )}
1666
- </Show>
1667
- </div>
1668
- )}
1669
- </Show>
1670
-
1671
- <Show when={openCodeThread()}>
1672
- {(thread) => (
1673
- <div class="rounded-md border border-emerald-500/40 bg-emerald-500/10 p-3 text-sm">
1674
- <div class="flex items-center gap-2 font-medium">
1675
- <CheckCircle2 class="h-4 w-4 text-emerald-500" />
1676
- OpenCode sessie gestart
1677
- </div>
1678
- <div class="mt-2 text-muted-foreground">
1679
- {thread().sessionTitle || thread().sessionId}
1680
- </div>
1681
- <Show when={thread().linearIssue}>
1682
- {(issue) => (
1683
- <a
1684
- class="mt-2 inline-flex items-center gap-1 text-emerald-600 underline-offset-4 hover:underline"
1685
- href={issue().issueUrl}
1686
- target="_blank"
1687
- rel="noreferrer"
1688
- >
1689
- Linear: {issue().issueIdentifier}
1690
- <ArrowUpRight class="h-3.5 w-3.5" />
1691
- </a>
1692
- )}
1693
- </Show>
1694
- </div>
1695
- )}
1696
- </Show>
1697
-
1698
1595
  <div>
1699
1596
  <label class="mb-2 block text-sm font-medium">Feedback</label>
1700
1597
  <textarea
@@ -1732,70 +1629,72 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1732
1629
  </Show>
1733
1630
  </div>
1734
1631
 
1735
- <div>
1736
- <label class="mb-2 block text-sm font-medium">Video-opname</label>
1737
- <div class="space-y-2 rounded-md border bg-background p-3">
1738
- <div class="flex flex-wrap items-center gap-2">
1739
- <Show
1740
- when={recordingState() === "recording"}
1741
- fallback={
1742
- <button
1743
- type="button"
1744
- 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"
1745
- disabled={mode() === "submitting" || !supportsScreenRecording()}
1746
- onClick={startRecording}
1747
- >
1748
- <Video class="h-4 w-4" />
1749
- Neem video op
1750
- </button>
1751
- }
1752
- >
1753
- <button
1754
- type="button"
1755
- 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"
1756
- disabled={mode() === "submitting"}
1757
- onClick={stopRecording}
1758
- >
1759
- <StopCircle class="h-4 w-4" />
1760
- Stop opname
1761
- </button>
1762
- </Show>
1763
-
1764
- <Show when={recordingBlob()}>
1765
- {(blob) => (
1766
- <>
1767
- <a
1768
- 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"
1769
- href={recordingUrl() ?? undefined}
1770
- target="_blank"
1771
- rel="noreferrer"
1772
- >
1773
- <Video class="h-4 w-4 shrink-0" />
1774
- <span class="truncate">
1775
- screen-recording.webm ({formatBytes(blob().size)},{" "}
1776
- {formatDuration(recordingDurationMs())})
1777
- </span>
1778
- </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={
1779
1640
  <button
1780
1641
  type="button"
1781
- class="inline-flex h-9 items-center gap-2 rounded-md border px-3 text-sm font-medium transition hover:bg-muted"
1782
- disabled={mode() === "submitting"}
1783
- onClick={clearRecording}
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}
1784
1645
  >
1785
- <Trash2 class="h-4 w-4" />
1786
- Wissen
1646
+ <Video class="h-4 w-4" />
1647
+ Neem video op
1787
1648
  </button>
1788
- </>
1789
- )}
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>
1790
1694
  </Show>
1791
1695
  </div>
1792
- <Show when={!supportsScreenRecording()}>
1793
- <div class="text-xs text-muted-foreground">
1794
- Video-opname is niet beschikbaar in deze browser.
1795
- </div>
1796
- </Show>
1797
1696
  </div>
1798
- </div>
1697
+ </Show>
1799
1698
 
1800
1699
  <Accordion title="Broncontext" defaultOpen>
1801
1700
  <div class="space-y-1">
@@ -1842,11 +1741,11 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1842
1741
  onClick={submitFeedback}
1843
1742
  >
1844
1743
  <Show
1845
- when={submitTarget() === "linear"}
1744
+ when={submitTarget() === "feedback"}
1846
1745
  fallback={
1847
1746
  <>
1848
1747
  <Send class="h-4 w-4" />
1849
- Verstuur naar Linear
1748
+ Verstuur feedback
1850
1749
  </>
1851
1750
  }
1852
1751
  >
@@ -1854,92 +1753,6 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1854
1753
  Versturen...
1855
1754
  </Show>
1856
1755
  </button>
1857
- <button
1858
- type="button"
1859
- class="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-background px-4 text-sm font-medium text-foreground transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-60"
1860
- disabled={
1861
- mode() === "submitting" ||
1862
- !prompt().trim() ||
1863
- !props.locator.config.startAgentSession
1864
- }
1865
- onClick={startAgentSession}
1866
- title={
1867
- props.locator.config.startAgentSession
1868
- ? "Start agent met deze feedback"
1869
- : "Agent sessies zijn nog niet gekoppeld"
1870
- }
1871
- >
1872
- <Show
1873
- when={submitTarget() === "agent"}
1874
- fallback={
1875
- <>
1876
- <Bot class="h-4 w-4" />
1877
- Start agent
1878
- </>
1879
- }
1880
- >
1881
- <LoaderCircle class="h-4 w-4 animate-spin" />
1882
- Agent starten...
1883
- </Show>
1884
- </button>
1885
- <div class="grid grid-cols-2 gap-2">
1886
- <button
1887
- type="button"
1888
- class="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-background px-3 text-sm font-medium text-foreground transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-60"
1889
- disabled={
1890
- mode() === "submitting" ||
1891
- !prompt().trim() ||
1892
- !props.locator.config.startCodexThread
1893
- }
1894
- onClick={startCodexThread}
1895
- title={
1896
- props.locator.config.startCodexThread
1897
- ? "Start Codex thread in plan mode"
1898
- : "Codex threads zijn nog niet gekoppeld"
1899
- }
1900
- >
1901
- <Show
1902
- when={submitTarget() === "codex"}
1903
- fallback={
1904
- <>
1905
- <Bot class="h-4 w-4" />
1906
- Codex
1907
- </>
1908
- }
1909
- >
1910
- <LoaderCircle class="h-4 w-4 animate-spin" />
1911
- Codex...
1912
- </Show>
1913
- </button>
1914
- <button
1915
- type="button"
1916
- class="inline-flex h-10 w-full items-center justify-center gap-2 rounded-md border bg-background px-3 text-sm font-medium text-foreground transition hover:bg-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-60"
1917
- disabled={
1918
- mode() === "submitting" ||
1919
- !prompt().trim() ||
1920
- !props.locator.config.startOpenCodeThread
1921
- }
1922
- onClick={startOpenCodeThread}
1923
- title={
1924
- props.locator.config.startOpenCodeThread
1925
- ? "Start OpenCode sessie in plan mode"
1926
- : "OpenCode sessies zijn nog niet gekoppeld"
1927
- }
1928
- >
1929
- <Show
1930
- when={submitTarget() === "opencode"}
1931
- fallback={
1932
- <>
1933
- <Bot class="h-4 w-4" />
1934
- OpenCode
1935
- </>
1936
- }
1937
- >
1938
- <LoaderCircle class="h-4 w-4 animate-spin" />
1939
- OpenCode...
1940
- </Show>
1941
- </button>
1942
- </div>
1943
1756
  </div>
1944
1757
  </aside>
1945
1758
  </div>
@@ -2111,83 +1924,6 @@ function formatBytes(size: number) {
2111
1924
  return `${(size / (1024 * 1024)).toFixed(1)} MB`;
2112
1925
  }
2113
1926
 
2114
- function openPendingAgentSessionWindow() {
2115
- return openPendingExternalWindow(
2116
- "Feedback agent starten...",
2117
- "De context wordt voorbereid en de agent chat wordt geopend.",
2118
- );
2119
- }
2120
-
2121
- function openAgentSessionWindow(
2122
- session: { sessionUrl?: string },
2123
- pendingWindow?: Window | null,
2124
- ) {
2125
- if (typeof window === "undefined" || !session.sessionUrl) {
2126
- return;
2127
- }
2128
-
2129
- if (pendingWindow && !pendingWindow.closed) {
2130
- pendingWindow.location.href = session.sessionUrl;
2131
- return;
2132
- }
2133
-
2134
- window.open(session.sessionUrl, "_blank", "noopener,noreferrer");
2135
- }
2136
-
2137
- function openPendingExternalWindow(title: string, message: string) {
2138
- if (typeof window === "undefined") {
2139
- return null;
2140
- }
2141
-
2142
- const pendingWindow = window.open("about:blank", "_blank");
2143
-
2144
- if (!pendingWindow) {
2145
- return null;
2146
- }
2147
-
2148
- try {
2149
- pendingWindow.opener = null;
2150
- pendingWindow.document.title = title;
2151
- pendingWindow.document.body.innerHTML = `<main style="font-family: system-ui, sans-serif; padding: 24px;"><h1 style="font-size: 18px;">${escapeHtml(title)}</h1><p>${escapeHtml(message)}</p></main>`;
2152
- } catch {
2153
- // Cross-browser popup handling can deny document writes; navigation still works.
2154
- }
2155
-
2156
- return pendingWindow;
2157
- }
2158
-
2159
- function openExternalWindow(url: string | undefined, pendingWindow?: Window | null) {
2160
- if (typeof window === "undefined" || !url) {
2161
- return;
2162
- }
2163
-
2164
- if (pendingWindow && !pendingWindow.closed) {
2165
- pendingWindow.location.href = url;
2166
- return;
2167
- }
2168
-
2169
- window.open(url, "_blank", "noopener,noreferrer");
2170
- }
2171
-
2172
- function closePendingWindow(pendingWindow?: Window | null) {
2173
- try {
2174
- if (pendingWindow && !pendingWindow.closed) {
2175
- pendingWindow.close();
2176
- }
2177
- } catch {
2178
- // Ignore popup cleanup errors.
2179
- }
2180
- }
2181
-
2182
- function escapeHtml(value: string) {
2183
- return value
2184
- .replace(/&/g, "&amp;")
2185
- .replace(/</g, "&lt;")
2186
- .replace(/>/g, "&gt;")
2187
- .replace(/"/g, "&quot;")
2188
- .replace(/'/g, "&#039;");
2189
- }
2190
-
2191
1927
  function formatDuration(durationMs: number | null) {
2192
1928
  if (!durationMs) {
2193
1929
  return "0s";
package/src/styles.css ADDED
@@ -0,0 +1,21 @@
1
+ :where([data-feedback-locator-ui="true"]) {
2
+ --background: 0 0% 3%;
3
+ --foreground: 0 0% 96%;
4
+ --muted: 0 0% 9%;
5
+ --muted-foreground: 0 0% 66%;
6
+ --border: 0 0% 18%;
7
+ --primary: 45 96% 53%;
8
+ --primary-foreground: 30 70% 8%;
9
+ --destructive: 0 84% 60%;
10
+ --ring: 45 96% 53%;
11
+ font-family:
12
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
13
+ }
14
+
15
+ :where([data-feedback-locator-ui="true"], [data-feedback-locator-ui="true"] *) {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ :where([data-feedback-locator-ui="true"] button, [data-feedback-locator-ui="true"] textarea, [data-feedback-locator-ui="true"] input) {
20
+ font: inherit;
21
+ }
package/src/styles.ts ADDED
@@ -0,0 +1,35 @@
1
+ export const defaultFeedbackLocatorStyles = `
2
+ :host {
3
+ color-scheme: dark;
4
+ --background: 0 0% 3%;
5
+ --foreground: 0 0% 96%;
6
+ --muted: 0 0% 9%;
7
+ --muted-foreground: 0 0% 66%;
8
+ --border: 0 0% 18%;
9
+ --primary: 45 96% 53%;
10
+ --primary-foreground: 30 70% 8%;
11
+ --destructive: 0 84% 60%;
12
+ --ring: 45 96% 53%;
13
+ font-family:
14
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
15
+ }
16
+
17
+ [data-feedback-locator-ui="true"],
18
+ [data-feedback-locator-ui="true"] * {
19
+ box-sizing: border-box;
20
+ }
21
+
22
+ [data-feedback-locator-ui="true"] button,
23
+ [data-feedback-locator-ui="true"] textarea,
24
+ [data-feedback-locator-ui="true"] input {
25
+ font: inherit;
26
+ }
27
+
28
+ [data-feedback-locator-ui="true"] button {
29
+ cursor: pointer;
30
+ }
31
+
32
+ [data-feedback-locator-ui="true"] button:disabled {
33
+ cursor: not-allowed;
34
+ }
35
+ `;
package/src/types.ts CHANGED
@@ -143,7 +143,24 @@ export type FeedbackLocatorLinearIssueReference = {
143
143
  issueUrl: string;
144
144
  };
145
145
 
146
- export type FeedbackLocatorSubmitResult = FeedbackLocatorLinearIssueReference;
146
+ export type FeedbackLocatorSubmitResult = Partial<FeedbackLocatorLinearIssueReference> & {
147
+ submissionId?: string;
148
+ appId?: string;
149
+ appKey?: string;
150
+ linearIssue?: FeedbackLocatorLinearIssueReference;
151
+ agentJob?: {
152
+ id: string;
153
+ provider: "codex" | "opencode";
154
+ status: string;
155
+ };
156
+ };
157
+ export type FeedbackLocatorHostedSubmitResult = FeedbackLocatorSubmitResult;
158
+ export type FeedbackLocatorCloseReason =
159
+ | "user"
160
+ | "submitted"
161
+ | "escape"
162
+ | "outside"
163
+ | "error";
147
164
 
148
165
  export type FeedbackLocatorAgentSessionResult = {
149
166
  sessionId: string;
@@ -174,6 +191,12 @@ export type FeedbackLocatorSourceCollector = (
174
191
  target: HTMLElement,
175
192
  ) => FeedbackLocatorSourceMetadata | null;
176
193
 
194
+ export type FeedbackLocatorTheme = {
195
+ primary?: string;
196
+ primaryForeground?: string;
197
+ ring?: string;
198
+ };
199
+
177
200
  export type FeedbackLocatorConfig = {
178
201
  appKey: string;
179
202
  appName: string;
@@ -191,6 +214,9 @@ export type FeedbackLocatorConfig = {
191
214
  ) => Promise<FeedbackLocatorOpenCodeThreadResult>;
192
215
  contextProviders?: FeedbackLocatorContextProvider[];
193
216
  hotkey?: FeedbackLocatorHotkey;
217
+ showFloatingButton?: boolean;
218
+ enableVideoRecording?: boolean;
219
+ theme?: FeedbackLocatorTheme;
194
220
  sourceCollector?: FeedbackLocatorSourceCollector;
195
221
  };
196
222
 
@@ -200,6 +226,9 @@ export type FeedbackLocator = {
200
226
  FeedbackLocatorConfig,
201
227
  | "contextProviders"
202
228
  | "hotkey"
229
+ | "showFloatingButton"
230
+ | "enableVideoRecording"
231
+ | "theme"
203
232
  | "sourceCollector"
204
233
  | "startAgentSession"
205
234
  | "startCodexThread"
@@ -1,6 +1,11 @@
1
1
  import { render } from "solid-js/web";
2
2
  import { FeedbackLocatorRoot } from "./solid";
3
- import type { FeedbackLocator } from "./types";
3
+ import { defaultFeedbackLocatorStyles } from "./styles";
4
+ import type {
5
+ FeedbackLocator,
6
+ FeedbackLocatorCloseReason,
7
+ FeedbackLocatorHostedSubmitResult,
8
+ } from "./types";
4
9
 
5
10
  export type FeedbackLocatorCustomElementOptions = {
6
11
  tagName?: string;
@@ -10,6 +15,8 @@ export type FeedbackLocatorCustomElementOptions = {
10
15
 
11
16
  export type FeedbackLocatorCustomElement = HTMLElement & {
12
17
  locator: FeedbackLocator | null;
18
+ open: boolean;
19
+ showFloatingButton: boolean;
13
20
  };
14
21
 
15
22
  export const defaultFeedbackLocatorElementName = "d2d-feedbackkit";
@@ -26,9 +33,15 @@ export function defineFeedbackLocatorElement(
26
33
 
27
34
  class FeedbackKitElement extends HTMLElement {
28
35
  #locator: FeedbackLocator | null = null;
36
+ #open = false;
37
+ #showFloatingButton = true;
29
38
  #dispose: (() => void) | undefined;
30
39
  #mount: HTMLElement | undefined;
31
40
 
41
+ static get observedAttributes() {
42
+ return ["open", "show-floating-button"];
43
+ }
44
+
32
45
  get locator() {
33
46
  return this.#locator;
34
47
  }
@@ -38,10 +51,51 @@ export function defineFeedbackLocatorElement(
38
51
  this.#render();
39
52
  }
40
53
 
54
+ get open() {
55
+ return this.#open;
56
+ }
57
+
58
+ set open(open: boolean) {
59
+ const nextOpen = Boolean(open);
60
+ if (this.#open === nextOpen) {
61
+ return;
62
+ }
63
+
64
+ this.#open = nextOpen;
65
+ this.#syncBooleanAttribute("open", nextOpen);
66
+ this.#dispatchOpenChange(nextOpen);
67
+ this.#render();
68
+ }
69
+
70
+ get showFloatingButton() {
71
+ return this.#showFloatingButton;
72
+ }
73
+
74
+ set showFloatingButton(showFloatingButton: boolean) {
75
+ const nextShowFloatingButton = Boolean(showFloatingButton);
76
+ if (this.#showFloatingButton === nextShowFloatingButton) {
77
+ return;
78
+ }
79
+
80
+ this.#showFloatingButton = nextShowFloatingButton;
81
+ this.#syncBooleanAttribute("show-floating-button", nextShowFloatingButton);
82
+ this.#render();
83
+ }
84
+
41
85
  connectedCallback() {
42
86
  this.#render();
43
87
  }
44
88
 
89
+ attributeChangedCallback(name: string, _oldValue: string | null, newValue: string | null) {
90
+ if (name === "open") {
91
+ this.open = newValue !== null;
92
+ }
93
+
94
+ if (name === "show-floating-button") {
95
+ this.showFloatingButton = newValue !== null;
96
+ }
97
+ }
98
+
45
99
  disconnectedCallback() {
46
100
  this.#dispose?.();
47
101
  this.#dispose = undefined;
@@ -57,7 +111,20 @@ export function defineFeedbackLocatorElement(
57
111
  this.#mount = this.#mount ?? this.#createMount();
58
112
  this.#mount.replaceChildren();
59
113
  this.#dispose = render(
60
- () => <FeedbackLocatorRoot locator={this.#locator!} />,
114
+ () => (
115
+ <FeedbackLocatorRoot
116
+ locator={this.#locator!}
117
+ open={this.#open}
118
+ showFloatingButton={this.#showFloatingButton}
119
+ onOpenChange={(open) => {
120
+ this.#open = open;
121
+ this.#syncBooleanAttribute("open", open);
122
+ this.#dispatchOpenChange(open);
123
+ }}
124
+ onClose={(reason) => this.#dispatchClose(reason)}
125
+ onSubmitted={(result) => this.#dispatchSubmitted(result)}
126
+ />
127
+ ),
61
128
  this.#mount,
62
129
  );
63
130
  }
@@ -73,8 +140,9 @@ export function defineFeedbackLocatorElement(
73
140
  : options.styles
74
141
  ? [options.styles]
75
142
  : [];
143
+ const allStyles = [defaultFeedbackLocatorStyles, ...styles];
76
144
 
77
- for (const styleText of styles) {
145
+ for (const styleText of allStyles) {
78
146
  const style = document.createElement("style");
79
147
  style.textContent = styleText;
80
148
  root.append(style);
@@ -84,6 +152,46 @@ export function defineFeedbackLocatorElement(
84
152
  root.append(mount);
85
153
  return mount;
86
154
  }
155
+
156
+ #syncBooleanAttribute(name: string, enabled: boolean) {
157
+ if (enabled && !this.hasAttribute(name)) {
158
+ this.setAttribute(name, "");
159
+ }
160
+
161
+ if (!enabled && this.hasAttribute(name)) {
162
+ this.removeAttribute(name);
163
+ }
164
+ }
165
+
166
+ #dispatchOpenChange(open: boolean) {
167
+ this.dispatchEvent(
168
+ new CustomEvent("feedbackkit-open-change", {
169
+ bubbles: true,
170
+ composed: true,
171
+ detail: { open },
172
+ }),
173
+ );
174
+ }
175
+
176
+ #dispatchClose(reason: FeedbackLocatorCloseReason) {
177
+ this.dispatchEvent(
178
+ new CustomEvent("feedbackkit-close", {
179
+ bubbles: true,
180
+ composed: true,
181
+ detail: { reason },
182
+ }),
183
+ );
184
+ }
185
+
186
+ #dispatchSubmitted(result: FeedbackLocatorHostedSubmitResult) {
187
+ this.dispatchEvent(
188
+ new CustomEvent("feedbackkit-submitted", {
189
+ bubbles: true,
190
+ composed: true,
191
+ detail: { result },
192
+ }),
193
+ );
194
+ }
87
195
  }
88
196
 
89
197
  customElements.define(tagName, FeedbackKitElement);