d2d-feedbackkit 0.0.0 → 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/README.md CHANGED
@@ -11,3 +11,80 @@ 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
+ submitFeedback: async (input) => {
68
+ const response = await fetch("https://feedbackkit.d2d.cloud/api/submissions", {
69
+ method: "POST",
70
+ headers: { "x-api-key": "<feedbackkit-api-key>" },
71
+ body: input.formData,
72
+ });
73
+ return response.json();
74
+ },
75
+ });
76
+
77
+ const widget = document.querySelector("d2d-feedbackkit");
78
+ widget.locator = locator;
79
+ widget.showFloatingButton = false;
80
+
81
+ document.querySelector("#feedback-button").addEventListener("click", () => {
82
+ widget.open = true;
83
+ });
84
+
85
+ widget.addEventListener("feedbackkit-submitted", () => {
86
+ showToast("Feedback verstuurd");
87
+ });
88
+ ```
89
+
90
+ 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.0",
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,28 @@ 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,
44
40
  } from "./types";
45
41
  import { createShortcutResolver } from "./shortcuts";
46
42
 
47
43
  type FeedbackLocatorRootProps = {
48
44
  locator: FeedbackLocator;
45
+ open?: boolean;
46
+ defaultOpen?: boolean;
47
+ onOpenChange?: (open: boolean) => void;
48
+ onClose?: (reason: FeedbackLocatorCloseReason) => void;
49
+ onSubmitted?: (result: FeedbackLocatorHostedSubmitResult) => void;
50
+ showFloatingButton?: boolean;
49
51
  };
50
52
 
51
53
  type FeedbackLocatorMode = "idle" | "selecting" | "annotating" | "submitting";
52
- type FeedbackLocatorSubmitTarget = "linear" | "agent" | "codex" | "opencode";
54
+ type FeedbackLocatorSubmitTarget = "feedback";
53
55
  type AnnotationTool = "select" | "pen" | "rect" | "arrow" | "text";
54
56
 
55
57
  type PenAnnotation = {
@@ -194,6 +196,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
194
196
  let annotationDragState: AnnotationDragState | null = null;
195
197
 
196
198
  const [mode, setMode] = createSignal<FeedbackLocatorMode>("idle");
199
+ const [uncontrolledOpen, setUncontrolledOpen] = createSignal(Boolean(props.defaultOpen));
197
200
  const [hoverRect, setHoverRect] = createSignal<FeedbackLocatorBounds | null>(null);
198
201
  const [selected, setSelected] = createSignal<ScreenshotState | null>(null);
199
202
  const [annotations, setAnnotations] = createSignal<Annotation[]>([]);
@@ -218,14 +221,8 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
218
221
  const [recordingUrl, setRecordingUrl] = createSignal<string | null>(null);
219
222
  const [recordingDurationMs, setRecordingDurationMs] = createSignal<number | null>(null);
220
223
  const [error, setError] = createSignal<string | null>(null);
224
+ const [successToast, setSuccessToast] = createSignal<string | null>(null);
221
225
  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
226
  let mediaRecorder: MediaRecorder | undefined;
230
227
  let recordingStream: MediaStream | undefined;
231
228
  let recordingStartedAt = 0;
@@ -240,7 +237,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
240
237
 
241
238
  if (matchesHotkey(event, props.locator.config.hotkey!)) {
242
239
  event.preventDefault();
243
- startSelection();
240
+ openWidget();
244
241
  }
245
242
  };
246
243
 
@@ -252,6 +249,18 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
252
249
  });
253
250
  });
254
251
 
252
+ createEffect(() => {
253
+ const shouldBeOpen = isWidgetOpen();
254
+
255
+ if (shouldBeOpen && mode() === "idle") {
256
+ beginSelection();
257
+ }
258
+
259
+ if (!shouldBeOpen && mode() !== "idle") {
260
+ resetState();
261
+ }
262
+ });
263
+
255
264
  createEffect(() => {
256
265
  if (mode() !== "selecting") {
257
266
  return;
@@ -280,7 +289,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
280
289
  const handleKeydown = (event: KeyboardEvent) => {
281
290
  if (event.key === "Escape") {
282
291
  event.preventDefault();
283
- reset();
292
+ requestClose("escape");
284
293
  }
285
294
  };
286
295
 
@@ -387,7 +396,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
387
396
  }
388
397
 
389
398
  if (action === "close") {
390
- reset();
399
+ requestClose("escape");
391
400
  }
392
401
  };
393
402
 
@@ -395,18 +404,38 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
395
404
  onCleanup(() => document.removeEventListener("keydown", handleKeydown));
396
405
  });
397
406
 
398
- function startSelection() {
407
+ function isWidgetOpen() {
408
+ return props.open ?? uncontrolledOpen();
409
+ }
410
+
411
+ function setWidgetOpen(open: boolean) {
412
+ if (props.open === undefined) {
413
+ setUncontrolledOpen(open);
414
+ }
415
+ props.onOpenChange?.(open);
416
+ }
417
+
418
+ function openWidget() {
419
+ setWidgetOpen(true);
420
+ if (mode() === "idle") {
421
+ beginSelection();
422
+ }
423
+ }
424
+
425
+ function requestClose(reason: FeedbackLocatorCloseReason) {
426
+ resetState();
427
+ setWidgetOpen(false);
428
+ props.onClose?.(reason);
429
+ }
430
+
431
+ function beginSelection() {
399
432
  setError(null);
400
433
  setSubmitTarget(null);
401
- setLinearResult(null);
402
- setAgentSession(null);
403
- setCodexThread(null);
404
434
  setSelected(null);
405
435
  setAnnotations([]);
406
436
  setAnnotationHistory([]);
407
437
  setRedoStack([]);
408
438
  setDraft(null);
409
- setTextDraft(null);
410
439
  setSelectedAnnotationIndex(null);
411
440
  setContextPreview(null);
412
441
  annotationDragState = null;
@@ -417,7 +446,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
417
446
  setMode("selecting");
418
447
  }
419
448
 
420
- function reset() {
449
+ function resetState() {
421
450
  const current = selected();
422
451
  if (current) {
423
452
  URL.revokeObjectURL(current.originalUrl);
@@ -440,8 +469,6 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
440
469
  clearRecording();
441
470
  setError(null);
442
471
  setSubmitTarget(null);
443
- setLinearResult(null);
444
- setAgentSession(null);
445
472
  }
446
473
 
447
474
  async function captureElement(element: HTMLElement) {
@@ -630,68 +657,12 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
630
657
  }
631
658
 
632
659
  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);
660
+ await runSubmission("feedback", async (prepared) => {
661
+ const submitResult = await props.locator.config.submitFeedback(prepared);
662
+ props.onSubmitted?.(submitResult);
663
+ setSuccessToast("Feedback verstuurd");
664
+ window.setTimeout(() => setSuccessToast(null), 4000);
665
+ requestClose("submitted");
695
666
  });
696
667
  }
697
668
 
@@ -711,8 +682,10 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
711
682
  }
712
683
 
713
684
  await submit(prepared);
714
- applySubmissionAnnotations(prepared);
715
- setMode("annotating");
685
+ if (mode() === "submitting") {
686
+ applySubmissionAnnotations(prepared);
687
+ setMode("annotating");
688
+ }
716
689
  } catch (submitError) {
717
690
  setMode("annotating");
718
691
  setError(
@@ -1350,11 +1323,16 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1350
1323
 
1351
1324
  return (
1352
1325
  <div data-feedback-locator-ui="true">
1353
- <Show when={mode() === "idle"}>
1326
+ <Show
1327
+ when={
1328
+ mode() === "idle" &&
1329
+ (props.showFloatingButton ?? props.locator.config.showFloatingButton) !== false
1330
+ }
1331
+ >
1354
1332
  <button
1355
1333
  type="button"
1356
1334
  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}
1335
+ onClick={openWidget}
1358
1336
  title="Feedback toevoegen (Ctrl+Shift+F)"
1359
1337
  >
1360
1338
  <MessageSquarePlus class="h-4 w-4" />
@@ -1368,6 +1346,14 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1368
1346
  </div>
1369
1347
  </Show>
1370
1348
 
1349
+ <Show when={successToast()}>
1350
+ {(message) => (
1351
+ <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">
1352
+ {message()}
1353
+ </div>
1354
+ )}
1355
+ </Show>
1356
+
1371
1357
  <Show when={mode() === "selecting"}>
1372
1358
  <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
1359
  <MousePointer2 class="h-4 w-4 text-primary" />
@@ -1375,7 +1361,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1375
1361
  <button
1376
1362
  type="button"
1377
1363
  class="ml-2 rounded-md p-1 text-muted-foreground hover:bg-muted hover:text-foreground"
1378
- onClick={reset}
1364
+ onClick={() => requestClose("user")}
1379
1365
  title="Annuleren"
1380
1366
  >
1381
1367
  <X class="h-4 w-4" />
@@ -1410,7 +1396,7 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1410
1396
  <button
1411
1397
  type="button"
1412
1398
  class="rounded-md p-2 text-muted-foreground hover:bg-muted hover:text-foreground"
1413
- onClick={reset}
1399
+ onClick={() => requestClose("user")}
1414
1400
  title="Sluiten"
1415
1401
  >
1416
1402
  <X class="h-5 w-5" />
@@ -1592,109 +1578,6 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1592
1578
 
1593
1579
  <aside class="flex min-h-0 flex-col bg-background">
1594
1580
  <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
1581
  <div>
1699
1582
  <label class="mb-2 block text-sm font-medium">Feedback</label>
1700
1583
  <textarea
@@ -1842,11 +1725,11 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1842
1725
  onClick={submitFeedback}
1843
1726
  >
1844
1727
  <Show
1845
- when={submitTarget() === "linear"}
1728
+ when={submitTarget() === "feedback"}
1846
1729
  fallback={
1847
1730
  <>
1848
1731
  <Send class="h-4 w-4" />
1849
- Verstuur naar Linear
1732
+ Verstuur feedback
1850
1733
  </>
1851
1734
  }
1852
1735
  >
@@ -1854,92 +1737,6 @@ export const FeedbackLocatorRoot: Component<FeedbackLocatorRootProps> = (props)
1854
1737
  Versturen...
1855
1738
  </Show>
1856
1739
  </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
1740
  </div>
1944
1741
  </aside>
1945
1742
  </div>
@@ -2111,83 +1908,6 @@ function formatBytes(size: number) {
2111
1908
  return `${(size / (1024 * 1024)).toFixed(1)} MB`;
2112
1909
  }
2113
1910
 
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
1911
  function formatDuration(durationMs: number | null) {
2192
1912
  if (!durationMs) {
2193
1913
  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;
@@ -191,6 +208,7 @@ export type FeedbackLocatorConfig = {
191
208
  ) => Promise<FeedbackLocatorOpenCodeThreadResult>;
192
209
  contextProviders?: FeedbackLocatorContextProvider[];
193
210
  hotkey?: FeedbackLocatorHotkey;
211
+ showFloatingButton?: boolean;
194
212
  sourceCollector?: FeedbackLocatorSourceCollector;
195
213
  };
196
214
 
@@ -200,6 +218,7 @@ export type FeedbackLocator = {
200
218
  FeedbackLocatorConfig,
201
219
  | "contextProviders"
202
220
  | "hotkey"
221
+ | "showFloatingButton"
203
222
  | "sourceCollector"
204
223
  | "startAgentSession"
205
224
  | "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);