makethisbetter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (168) hide show
  1. package/LICENSE +62 -0
  2. package/README.md +113 -0
  3. package/dist/annotate/draw.d.ts +21 -0
  4. package/dist/annotate/draw.d.ts.map +1 -0
  5. package/dist/annotate/draw.test.d.ts +2 -0
  6. package/dist/annotate/draw.test.d.ts.map +1 -0
  7. package/dist/annotate/highlighter.d.ts +11 -0
  8. package/dist/annotate/highlighter.d.ts.map +1 -0
  9. package/dist/annotate/highlighter.test.d.ts +2 -0
  10. package/dist/annotate/highlighter.test.d.ts.map +1 -0
  11. package/dist/annotate/overlay.d.ts +17 -0
  12. package/dist/annotate/overlay.d.ts.map +1 -0
  13. package/dist/annotate/overlay.test.d.ts +2 -0
  14. package/dist/annotate/overlay.test.d.ts.map +1 -0
  15. package/dist/annotate/pin.d.ts +10 -0
  16. package/dist/annotate/pin.d.ts.map +1 -0
  17. package/dist/annotate/pin.test.d.ts +2 -0
  18. package/dist/annotate/pin.test.d.ts.map +1 -0
  19. package/dist/annotate/session.d.ts +20 -0
  20. package/dist/annotate/session.d.ts.map +1 -0
  21. package/dist/annotate/session.test.d.ts +2 -0
  22. package/dist/annotate/session.test.d.ts.map +1 -0
  23. package/dist/annotate/toolbar.d.ts +17 -0
  24. package/dist/annotate/toolbar.d.ts.map +1 -0
  25. package/dist/annotate/toolbar.test.d.ts +2 -0
  26. package/dist/annotate/toolbar.test.d.ts.map +1 -0
  27. package/dist/api/client.d.ts +10 -0
  28. package/dist/api/client.d.ts.map +1 -0
  29. package/dist/api/client.test.d.ts +2 -0
  30. package/dist/api/client.test.d.ts.map +1 -0
  31. package/dist/context/collector.d.ts +10 -0
  32. package/dist/context/collector.d.ts.map +1 -0
  33. package/dist/context/collector.test.d.ts +2 -0
  34. package/dist/context/collector.test.d.ts.map +1 -0
  35. package/dist/context/console.d.ts +10 -0
  36. package/dist/context/console.d.ts.map +1 -0
  37. package/dist/context/console.test.d.ts +2 -0
  38. package/dist/context/console.test.d.ts.map +1 -0
  39. package/dist/context/error-source.d.ts +21 -0
  40. package/dist/context/error-source.d.ts.map +1 -0
  41. package/dist/context/error-source.test.d.ts +2 -0
  42. package/dist/context/error-source.test.d.ts.map +1 -0
  43. package/dist/context/frustration.d.ts +27 -0
  44. package/dist/context/frustration.d.ts.map +1 -0
  45. package/dist/context/frustration.test.d.ts +2 -0
  46. package/dist/context/frustration.test.d.ts.map +1 -0
  47. package/dist/global.d.ts +3 -0
  48. package/dist/global.d.ts.map +1 -0
  49. package/dist/i18n/en.d.ts +47 -0
  50. package/dist/i18n/en.d.ts.map +1 -0
  51. package/dist/i18n/index.d.ts +4 -0
  52. package/dist/i18n/index.d.ts.map +1 -0
  53. package/dist/i18n/index.test.d.ts +2 -0
  54. package/dist/i18n/index.test.d.ts.map +1 -0
  55. package/dist/i18n/zh-CN.d.ts +47 -0
  56. package/dist/i18n/zh-CN.d.ts.map +1 -0
  57. package/dist/index.d.mts +8 -0
  58. package/dist/index.d.ts +8 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/makethisbetter.cjs.js +84 -0
  61. package/dist/makethisbetter.cjs.js.map +1 -0
  62. package/dist/makethisbetter.esm.js +1433 -0
  63. package/dist/makethisbetter.esm.js.map +1 -0
  64. package/dist/makethisbetter.js +84 -0
  65. package/dist/makethisbetter.js.map +1 -0
  66. package/dist/popup/comment.d.ts +31 -0
  67. package/dist/popup/comment.d.ts.map +1 -0
  68. package/dist/popup/comment.test.d.ts +2 -0
  69. package/dist/popup/comment.test.d.ts.map +1 -0
  70. package/dist/popup/success.d.ts +8 -0
  71. package/dist/popup/success.d.ts.map +1 -0
  72. package/dist/popup/success.test.d.ts +2 -0
  73. package/dist/popup/success.test.d.ts.map +1 -0
  74. package/dist/record/control-bar.d.ts +12 -0
  75. package/dist/record/control-bar.d.ts.map +1 -0
  76. package/dist/record/control-bar.test.d.ts +2 -0
  77. package/dist/record/control-bar.test.d.ts.map +1 -0
  78. package/dist/record/session.d.ts +26 -0
  79. package/dist/record/session.d.ts.map +1 -0
  80. package/dist/record/session.test.d.ts +2 -0
  81. package/dist/record/session.test.d.ts.map +1 -0
  82. package/dist/screenshot/capture.d.ts +3 -0
  83. package/dist/screenshot/capture.d.ts.map +1 -0
  84. package/dist/screenshot/capture.test.d.ts +2 -0
  85. package/dist/screenshot/capture.test.d.ts.map +1 -0
  86. package/dist/styles.d.ts +9 -0
  87. package/dist/styles.d.ts.map +1 -0
  88. package/dist/types.d.ts +45 -0
  89. package/dist/types.d.ts.map +1 -0
  90. package/dist/widget/controller.d.ts +37 -0
  91. package/dist/widget/controller.d.ts.map +1 -0
  92. package/dist/widget/controller.test.d.ts +2 -0
  93. package/dist/widget/controller.test.d.ts.map +1 -0
  94. package/dist/widget/frustration-prompt.d.ts +11 -0
  95. package/dist/widget/frustration-prompt.d.ts.map +1 -0
  96. package/dist/widget/frustration-prompt.test.d.ts +2 -0
  97. package/dist/widget/frustration-prompt.test.d.ts.map +1 -0
  98. package/dist/widget/payload.d.ts +5 -0
  99. package/dist/widget/payload.d.ts.map +1 -0
  100. package/dist/widget/payload.test.d.ts +2 -0
  101. package/dist/widget/payload.test.d.ts.map +1 -0
  102. package/dist/widget/pet.d.ts +12 -0
  103. package/dist/widget/pet.d.ts.map +1 -0
  104. package/dist/widget/pet.test.d.ts +2 -0
  105. package/dist/widget/pet.test.d.ts.map +1 -0
  106. package/dist/widget/shadow.d.ts +11 -0
  107. package/dist/widget/shadow.d.ts.map +1 -0
  108. package/dist/widget/shadow.test.d.ts +2 -0
  109. package/dist/widget/shadow.test.d.ts.map +1 -0
  110. package/dist/widget/tab.d.ts +10 -0
  111. package/dist/widget/tab.d.ts.map +1 -0
  112. package/dist/widget/tab.test.d.ts +2 -0
  113. package/dist/widget/tab.test.d.ts.map +1 -0
  114. package/package.json +64 -0
  115. package/src/annotate/draw.test.ts +168 -0
  116. package/src/annotate/draw.ts +108 -0
  117. package/src/annotate/highlighter.test.ts +47 -0
  118. package/src/annotate/highlighter.ts +84 -0
  119. package/src/annotate/overlay.test.ts +91 -0
  120. package/src/annotate/overlay.ts +44 -0
  121. package/src/annotate/pin.test.ts +87 -0
  122. package/src/annotate/pin.ts +28 -0
  123. package/src/annotate/session.test.ts +219 -0
  124. package/src/annotate/session.ts +104 -0
  125. package/src/annotate/toolbar.test.ts +132 -0
  126. package/src/annotate/toolbar.ts +81 -0
  127. package/src/api/client.test.ts +75 -0
  128. package/src/api/client.ts +77 -0
  129. package/src/context/collector.test.ts +14 -0
  130. package/src/context/collector.ts +41 -0
  131. package/src/context/console.test.ts +44 -0
  132. package/src/context/console.ts +34 -0
  133. package/src/context/error-source.test.ts +115 -0
  134. package/src/context/error-source.ts +62 -0
  135. package/src/context/frustration.test.ts +134 -0
  136. package/src/context/frustration.ts +145 -0
  137. package/src/global.ts +3 -0
  138. package/src/i18n/en.ts +47 -0
  139. package/src/i18n/index.test.ts +24 -0
  140. package/src/i18n/index.ts +16 -0
  141. package/src/i18n/zh-CN.ts +47 -0
  142. package/src/index.ts +32 -0
  143. package/src/popup/comment.test.ts +73 -0
  144. package/src/popup/comment.ts +177 -0
  145. package/src/popup/success.test.ts +75 -0
  146. package/src/popup/success.ts +33 -0
  147. package/src/record/control-bar.test.ts +110 -0
  148. package/src/record/control-bar.ts +46 -0
  149. package/src/record/session.test.ts +153 -0
  150. package/src/record/session.ts +139 -0
  151. package/src/screenshot/capture.test.ts +68 -0
  152. package/src/screenshot/capture.ts +96 -0
  153. package/src/styles/widget.css +847 -0
  154. package/src/styles.ts +9 -0
  155. package/src/types.ts +46 -0
  156. package/src/vite-env.d.ts +6 -0
  157. package/src/widget/controller.test.ts +62 -0
  158. package/src/widget/controller.ts +249 -0
  159. package/src/widget/frustration-prompt.test.ts +86 -0
  160. package/src/widget/frustration-prompt.ts +34 -0
  161. package/src/widget/payload.test.ts +167 -0
  162. package/src/widget/payload.ts +31 -0
  163. package/src/widget/pet.test.ts +63 -0
  164. package/src/widget/pet.ts +67 -0
  165. package/src/widget/shadow.test.ts +25 -0
  166. package/src/widget/shadow.ts +41 -0
  167. package/src/widget/tab.test.ts +92 -0
  168. package/src/widget/tab.ts +31 -0
@@ -0,0 +1,37 @@
1
+ import type { MakeThisBetterConfig } from '../types';
2
+ export declare class WidgetController {
3
+ private config;
4
+ private shadow;
5
+ private tab;
6
+ private pet;
7
+ private toolbar;
8
+ private session;
9
+ private popup;
10
+ private success;
11
+ private frustrationPrompt;
12
+ private apiClient;
13
+ private consoleCollector;
14
+ private frustrationDetector;
15
+ private messages;
16
+ private mode;
17
+ private currentAnnotation;
18
+ private recordSession;
19
+ private recordBar;
20
+ private currentRecording;
21
+ constructor(config: MakeThisBetterConfig);
22
+ private buildUI;
23
+ private handleTabClick;
24
+ private handleFrustration;
25
+ private dismissFrustrationPrompt;
26
+ private enterAnnotationMode;
27
+ private handleToolbarModeChange;
28
+ private enterRecordingMode;
29
+ private stopRecording;
30
+ private exitRecordingMode;
31
+ private showPopup;
32
+ private handleSubmit;
33
+ private showSuccess;
34
+ private exitAll;
35
+ destroy(): void;
36
+ }
37
+ //# sourceMappingURL=controller.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../../src/widget/controller.ts"],"names":[],"mappings":"AAmBA,OAAO,KAAK,EAAE,oBAAoB,EAAc,MAAM,UAAU,CAAA;AAIhE,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,MAAM,CAAsB;IACpC,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,GAAG,CAA2B;IACtC,OAAO,CAAC,GAAG,CAAwB;IACnC,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,OAAO,CAAiC;IAChD,OAAO,CAAC,KAAK,CAA4B;IACzC,OAAO,CAAC,OAAO,CAA2B;IAC1C,OAAO,CAAC,iBAAiB,CAAqC;IAC9D,OAAO,CAAC,SAAS,CAAW;IAC5B,OAAO,CAAC,gBAAgB,CAAuB;IAC/C,OAAO,CAAC,mBAAmB,CAAmC;IAC9D,OAAO,CAAC,QAAQ,CAAgC;IAChD,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,iBAAiB,CAA0B;IACnD,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,SAAS,CAAgC;IACjD,OAAO,CAAC,gBAAgB,CAA+B;gBAE3C,MAAM,EAAE,oBAAoB;IAcxC,OAAO,CAAC,OAAO;IAef,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,iBAAiB;IAYzB,OAAO,CAAC,wBAAwB;IAKhC,OAAO,CAAC,mBAAmB;IAiB3B,OAAO,CAAC,uBAAuB;YAejB,kBAAkB;IAgBhC,OAAO,CAAC,aAAa;IAWrB,OAAO,CAAC,iBAAiB;IAOzB,OAAO,CAAC,SAAS;YAcH,YAAY;IAkC1B,OAAO,CAAC,WAAW;IAUnB,OAAO,CAAC,OAAO;IAef,OAAO,IAAI,IAAI;CAWhB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=controller.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"controller.test.d.ts","sourceRoot":"","sources":["../../src/widget/controller.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ import type { ShadowContainer } from './shadow';
2
+ import type { I18nMessages } from '../i18n';
3
+ export declare class FrustrationPromptCard {
4
+ private el;
5
+ constructor(shadow: ShadowContainer, messages: I18nMessages, handlers: {
6
+ onTell: () => void;
7
+ onDismiss: () => void;
8
+ });
9
+ destroy(): void;
10
+ }
11
+ //# sourceMappingURL=frustration-prompt.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frustration-prompt.d.ts","sourceRoot":"","sources":["../../src/widget/frustration-prompt.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAI3C,qBAAa,qBAAqB;IAChC,OAAO,CAAC,EAAE,CAAgB;gBAGxB,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE;QAAE,MAAM,EAAE,MAAM,IAAI,CAAC;QAAC,SAAS,EAAE,MAAM,IAAI,CAAA;KAAE;IAmBzD,OAAO,IAAI,IAAI;CAGhB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=frustration-prompt.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frustration-prompt.test.d.ts","sourceRoot":"","sources":["../../src/widget/frustration-prompt.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,5 @@
1
+ import type { PageContext } from '../context/collector';
2
+ import type { Annotation, FeedbackPayload, MakeThisBetterConfig } from '../types';
3
+ import type { RecordingResult } from '../record/session';
4
+ export declare function buildPayload(description: string, pageContext: PageContext, consoleErrors: string[], annotation: Annotation | null, user: MakeThisBetterConfig['user'], recording?: RecordingResult): FeedbackPayload;
5
+ //# sourceMappingURL=payload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payload.d.ts","sourceRoot":"","sources":["../../src/widget/payload.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAA;AACvD,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AACjF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAExD,wBAAgB,YAAY,CAC1B,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,WAAW,EACxB,aAAa,EAAE,MAAM,EAAE,EACvB,UAAU,EAAE,UAAU,GAAG,IAAI,EAC7B,IAAI,EAAE,oBAAoB,CAAC,MAAM,CAAC,EAClC,SAAS,CAAC,EAAE,eAAe,GAC1B,eAAe,CAmBjB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=payload.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"payload.test.d.ts","sourceRoot":"","sources":["../../src/widget/payload.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,12 @@
1
+ import type { ShadowContainer } from './shadow';
2
+ import type { I18nMessages } from '../i18n';
3
+ export declare class PetEntry {
4
+ private el;
5
+ private bubble;
6
+ private onClick;
7
+ constructor(shadow: ShadowContainer, messages: I18nMessages, onClick: () => void);
8
+ private buildSVG;
9
+ setActive(active: boolean): void;
10
+ destroy(): void;
11
+ }
12
+ //# sourceMappingURL=pet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pet.d.ts","sourceRoot":"","sources":["../../src/widget/pet.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAG3C,qBAAa,QAAQ;IACnB,OAAO,CAAC,EAAE,CAAgB;IAC1B,OAAO,CAAC,MAAM,CAAgB;IAC9B,OAAO,CAAC,OAAO,CAAY;gBAGzB,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,YAAY,EACtB,OAAO,EAAE,MAAM,IAAI;IAyBrB,OAAO,CAAC,QAAQ;IAkBhB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAOhC,OAAO,IAAI,IAAI;CAIhB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=pet.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pet.test.d.ts","sourceRoot":"","sources":["../../src/widget/pet.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,11 @@
1
+ import type { MakeThisBetterConfig } from '../types';
2
+ export declare class ShadowContainer {
3
+ private host;
4
+ readonly root: ShadowRoot;
5
+ constructor(theme?: MakeThisBetterConfig['theme']);
6
+ el<T extends HTMLElement = HTMLElement>(tag: string, className?: string): T;
7
+ append(...nodes: Node[]): void;
8
+ remove(...nodes: Node[]): void;
9
+ destroy(): void;
10
+ }
11
+ //# sourceMappingURL=shadow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shadow.d.ts","sourceRoot":"","sources":["../../src/widget/shadow.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAA;AAEpD,qBAAa,eAAe;IAC1B,OAAO,CAAC,IAAI,CAAa;IACzB,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;gBAEb,KAAK,GAAE,oBAAoB,CAAC,OAAO,CAAU;IAczD,EAAE,CAAC,CAAC,SAAS,WAAW,GAAG,WAAW,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,CAAC;IAM3E,MAAM,CAAC,GAAG,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI;IAI9B,MAAM,CAAC,GAAG,KAAK,EAAE,IAAI,EAAE,GAAG,IAAI;IAM9B,OAAO,IAAI,IAAI;CAGhB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=shadow.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shadow.test.d.ts","sourceRoot":"","sources":["../../src/widget/shadow.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,10 @@
1
+ import type { ShadowContainer } from './shadow';
2
+ import type { I18nMessages } from '../i18n';
3
+ export declare class FeedbackTab {
4
+ private el;
5
+ private onClick;
6
+ constructor(shadow: ShadowContainer, messages: I18nMessages, position: 'left' | 'right', onClick: () => void);
7
+ setActive(active: boolean): void;
8
+ destroy(): void;
9
+ }
10
+ //# sourceMappingURL=tab.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab.d.ts","sourceRoot":"","sources":["../../src/widget/tab.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,SAAS,CAAA;AAE3C,qBAAa,WAAW;IACtB,OAAO,CAAC,EAAE,CAAmB;IAC7B,OAAO,CAAC,OAAO,CAAY;gBAGzB,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,YAAY,EACtB,QAAQ,EAAE,MAAM,GAAG,OAAO,EAC1B,OAAO,EAAE,MAAM,IAAI;IAWrB,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAIhC,OAAO,IAAI,IAAI;CAIhB"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=tab.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tab.test.d.ts","sourceRoot":"","sources":["../../src/widget/tab.test.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,64 @@
1
+ {
2
+ "name": "makethisbetter",
3
+ "version": "1.0.0",
4
+ "description": "Make This Better JS Widget SDK — embed user feedback collection in any website",
5
+ "type": "module",
6
+ "main": "./dist/makethisbetter.cjs.js",
7
+ "module": "./dist/makethisbetter.esm.js",
8
+ "types": "./dist/index.d.ts",
9
+ "unpkg": "./dist/makethisbetter.js",
10
+ "jsdelivr": "./dist/makethisbetter.js",
11
+ "sideEffects": false,
12
+ "exports": {
13
+ ".": {
14
+ "import": {
15
+ "types": "./dist/index.d.mts",
16
+ "default": "./dist/makethisbetter.esm.js"
17
+ },
18
+ "require": {
19
+ "types": "./dist/index.d.ts",
20
+ "default": "./dist/makethisbetter.cjs.js"
21
+ }
22
+ },
23
+ "./package.json": "./package.json"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "src"
28
+ ],
29
+ "scripts": {
30
+ "dev": "vite serve demo",
31
+ "build": "vite build && vite build --config vite.iife.config.ts && tsc --emitDeclarationOnly --declaration --declarationDir dist && cp dist/index.d.ts dist/index.d.mts",
32
+ "test": "vitest run",
33
+ "test:watch": "vitest",
34
+ "type-check": "tsc --noEmit",
35
+ "lint": "tsc --noEmit"
36
+ },
37
+ "devDependencies": {
38
+ "@vitest/coverage-v8": "^1.5.0",
39
+ "jsdom": "^24.0.0",
40
+ "rrweb": "^2.1.0",
41
+ "typescript": "^5.4.0",
42
+ "vite": "^5.2.0",
43
+ "vitest": "^1.5.0"
44
+ },
45
+ "dependencies": {
46
+ "html-to-image": "^1.11.11"
47
+ },
48
+ "peerDependencies": {
49
+ "rrweb": "^2.1.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "rrweb": {
53
+ "optional": true
54
+ }
55
+ },
56
+ "keywords": [
57
+ "feedback",
58
+ "widget",
59
+ "user-feedback",
60
+ "bug-reporting",
61
+ "annotation"
62
+ ],
63
+ "license": "BSL-1.1"
64
+ }
@@ -0,0 +1,168 @@
1
+ import { afterEach, describe, it, expect, vi } from 'vitest'
2
+ import { ShadowContainer } from '../widget/shadow'
3
+ import { DrawLayer } from './draw'
4
+
5
+ const SVG_SEL = 'svg[class="mtb-draw-svg"]'
6
+
7
+ describe('DrawLayer', () => {
8
+ let shadow: ShadowContainer
9
+ let onDrawComplete: ReturnType<typeof vi.fn>
10
+ let draw: DrawLayer
11
+
12
+ function setup() {
13
+ shadow = new ShadowContainer()
14
+ onDrawComplete = vi.fn()
15
+ draw = new DrawLayer(shadow, onDrawComplete)
16
+ }
17
+
18
+ afterEach(() => {
19
+ draw.destroy()
20
+ shadow.destroy()
21
+ document.body.innerHTML = ''
22
+ })
23
+
24
+ describe('constructor', () => {
25
+ it('appends an SVG element to the shadow root', () => {
26
+ setup()
27
+ const svg = shadow.root.querySelector(SVG_SEL)
28
+ expect(svg).toBeTruthy()
29
+ expect(svg!.tagName).toBe('svg')
30
+ })
31
+ })
32
+
33
+ describe('startDraw', () => {
34
+ it('creates a path element in the SVG', () => {
35
+ setup()
36
+ draw.startDraw(10, 20)
37
+ const path = shadow.root.querySelector(`${SVG_SEL} path`)
38
+ expect(path).toBeTruthy()
39
+ expect(path!.tagName).toBe('path')
40
+ })
41
+
42
+ it('sets isCurrentlyDrawing to true', () => {
43
+ setup()
44
+ expect(draw.isCurrentlyDrawing()).toBe(false)
45
+ draw.startDraw(10, 20)
46
+ expect(draw.isCurrentlyDrawing()).toBe(true)
47
+ })
48
+
49
+ it('initializes point count to 1', () => {
50
+ setup()
51
+ draw.startDraw(10, 20)
52
+ expect(draw.getPointCount()).toBe(1)
53
+ })
54
+ })
55
+
56
+ describe('continueDraw', () => {
57
+ it('adds points and updates the path d attribute', () => {
58
+ setup()
59
+ draw.startDraw(10, 20)
60
+ draw.continueDraw(30, 40)
61
+ const path = shadow.root.querySelector(`${SVG_SEL} path`)!
62
+ expect(path.getAttribute('d')).toContain('M 10 20')
63
+ expect(path.getAttribute('d')).toContain('L 30 40')
64
+ expect(draw.getPointCount()).toBe(2)
65
+ })
66
+
67
+ it('does nothing when not drawing', () => {
68
+ setup()
69
+ draw.continueDraw(30, 40)
70
+ expect(draw.getPointCount()).toBe(0)
71
+ })
72
+ })
73
+
74
+ describe('endDraw', () => {
75
+ it('calls onDrawComplete with a draw annotation when enough points', () => {
76
+ setup()
77
+ draw.startDraw(0, 0)
78
+ draw.continueDraw(10, 10)
79
+ draw.endDraw()
80
+
81
+ expect(onDrawComplete).toHaveBeenCalledOnce()
82
+ const annotation = onDrawComplete.mock.calls[0][0]
83
+ expect(annotation.type).toBe('draw')
84
+ expect(annotation.drawPath).toContain('M 0 0')
85
+ expect(annotation.drawPath).toContain('L 10 10')
86
+ expect(annotation.x).toBe(5)
87
+ expect(annotation.y).toBe(5)
88
+ })
89
+
90
+ it('removes the path and does not call callback with fewer than 2 points', () => {
91
+ setup()
92
+ draw.startDraw(10, 20)
93
+ draw.endDraw()
94
+
95
+ expect(onDrawComplete).not.toHaveBeenCalled()
96
+ expect(draw.isCurrentlyDrawing()).toBe(false)
97
+ })
98
+
99
+ it('does nothing when not currently drawing', () => {
100
+ setup()
101
+ draw.endDraw()
102
+ expect(onDrawComplete).not.toHaveBeenCalled()
103
+ })
104
+
105
+ it('sets isCurrentlyDrawing to false after ending', () => {
106
+ setup()
107
+ draw.startDraw(0, 0)
108
+ draw.continueDraw(10, 10)
109
+ draw.endDraw()
110
+ expect(draw.isCurrentlyDrawing()).toBe(false)
111
+ })
112
+ })
113
+
114
+ describe('clearAll', () => {
115
+ it('removes all SVG children and resets state', () => {
116
+ setup()
117
+ draw.startDraw(0, 0)
118
+ draw.continueDraw(10, 10)
119
+ draw.clearAll()
120
+
121
+ expect(shadow.root.querySelector(SVG_SEL)!.childElementCount).toBe(0)
122
+ expect(draw.isCurrentlyDrawing()).toBe(false)
123
+ expect(draw.getPointCount()).toBe(0)
124
+ })
125
+ })
126
+
127
+ describe('cancelCurrentDraw', () => {
128
+ it('removes the current path and resets drawing state', () => {
129
+ setup()
130
+ draw.startDraw(0, 0)
131
+ draw.continueDraw(10, 10)
132
+ draw.cancelCurrentDraw()
133
+
134
+ expect(draw.isCurrentlyDrawing()).toBe(false)
135
+ expect(draw.getPointCount()).toBe(0)
136
+ const svg = shadow.root.querySelector(SVG_SEL)!
137
+ expect(svg.querySelector('path')).toBeNull()
138
+ })
139
+
140
+ it('is safe to call when not drawing', () => {
141
+ setup()
142
+ expect(() => draw.cancelCurrentDraw()).not.toThrow()
143
+ })
144
+ })
145
+
146
+ describe('destroy', () => {
147
+ it('removes the SVG element from the shadow root', () => {
148
+ setup()
149
+ draw.destroy()
150
+ expect(shadow.root.querySelector(SVG_SEL)).toBeNull()
151
+ })
152
+ })
153
+
154
+ describe('getCenter calculation', () => {
155
+ it('computes the center of the bounding box of all points', () => {
156
+ setup()
157
+ draw.startDraw(0, 0)
158
+ draw.continueDraw(100, 0)
159
+ draw.continueDraw(100, 100)
160
+ draw.continueDraw(0, 100)
161
+ draw.endDraw()
162
+
163
+ const annotation = onDrawComplete.mock.calls[0][0]
164
+ expect(annotation.x).toBe(50)
165
+ expect(annotation.y).toBe(50)
166
+ })
167
+ })
168
+ })
@@ -0,0 +1,108 @@
1
+ import type { ShadowContainer } from '../widget/shadow'
2
+ import type { Annotation } from '../types'
3
+ import { DRAW_STROKE } from '../styles'
4
+
5
+ export class DrawLayer {
6
+ private svg: SVGSVGElement
7
+ private path: SVGPathElement | null = null
8
+ private points: [number, number][] = []
9
+ private isDrawing = false
10
+ private onDrawComplete: (annotation: Annotation) => void
11
+
12
+ constructor(shadow: ShadowContainer, onDrawComplete: (annotation: Annotation) => void) {
13
+ this.onDrawComplete = onDrawComplete
14
+
15
+ this.svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
16
+ this.svg.setAttribute('class', 'mtb-draw-svg')
17
+ shadow.append(this.svg)
18
+ }
19
+
20
+ startDraw(x: number, y: number): void {
21
+ this.isDrawing = true
22
+ this.points = [[x, y]]
23
+
24
+ this.path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
25
+ this.path.setAttribute('fill', 'none')
26
+ this.path.setAttribute('stroke', DRAW_STROKE.color)
27
+ this.path.setAttribute('stroke-width', DRAW_STROKE.width)
28
+ this.path.setAttribute('stroke-linecap', DRAW_STROKE.linecap)
29
+ this.path.setAttribute('stroke-linejoin', DRAW_STROKE.linejoin)
30
+ this.path.setAttribute('style', `filter:${DRAW_STROKE.filter}`)
31
+ this.svg.appendChild(this.path)
32
+ }
33
+
34
+ continueDraw(x: number, y: number): void {
35
+ if (!this.isDrawing || !this.path) return
36
+ this.points.push([x, y])
37
+ this.path.setAttribute('d', this.buildPathD())
38
+ }
39
+
40
+ endDraw(): void {
41
+ if (!this.isDrawing || this.points.length < 2) {
42
+ this.isDrawing = false
43
+ if (this.path) {
44
+ this.path.remove()
45
+ this.path = null
46
+ }
47
+ return
48
+ }
49
+ this.isDrawing = false
50
+
51
+ const center = this.getCenter()
52
+ const pathD = this.buildPathD()
53
+
54
+ const annotation: Annotation = {
55
+ type: 'draw',
56
+ x: center[0],
57
+ y: center[1],
58
+ drawPath: pathD,
59
+ }
60
+
61
+ this.onDrawComplete(annotation)
62
+ }
63
+
64
+ private buildPathD(): string {
65
+ if (this.points.length === 0) return ''
66
+ const [fx, fy] = this.points[0]
67
+ let d = `M ${fx} ${fy}`
68
+ for (let i = 1; i < this.points.length; i++) {
69
+ const [x, y] = this.points[i]
70
+ d += ` L ${x} ${y}`
71
+ }
72
+ return d
73
+ }
74
+
75
+ private getCenter(): [number, number] {
76
+ const xs = this.points.map(p => p[0])
77
+ const ys = this.points.map(p => p[1])
78
+ const cx = (Math.min(...xs) + Math.max(...xs)) / 2
79
+ const cy = (Math.min(...ys) + Math.max(...ys)) / 2
80
+ return [cx, cy]
81
+ }
82
+
83
+ clearAll(): void {
84
+ while (this.svg.firstChild) this.svg.removeChild(this.svg.firstChild)
85
+ this.path = null
86
+ this.isDrawing = false
87
+ this.points = []
88
+ }
89
+
90
+ cancelCurrentDraw(): void {
91
+ this.path?.remove()
92
+ this.path = null
93
+ this.isDrawing = false
94
+ this.points = []
95
+ }
96
+
97
+ isCurrentlyDrawing(): boolean {
98
+ return this.isDrawing
99
+ }
100
+
101
+ getPointCount(): number {
102
+ return this.points.length
103
+ }
104
+
105
+ destroy(): void {
106
+ this.svg.remove()
107
+ }
108
+ }
@@ -0,0 +1,47 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getElementName, getCssSelector } from './highlighter'
3
+
4
+ describe('getElementName', () => {
5
+ it('returns aria-label when present', () => {
6
+ const el = document.createElement('button')
7
+ el.setAttribute('aria-label', 'Close dialog')
8
+ expect(getElementName(el)).toBe('Close dialog')
9
+ })
10
+
11
+ it('returns text content when no label', () => {
12
+ const el = document.createElement('button')
13
+ el.textContent = 'Export PDF'
14
+ expect(getElementName(el)).toBe('Export PDF')
15
+ })
16
+
17
+ it('returns tag name as fallback', () => {
18
+ const el = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
19
+ expect(getElementName(el)).toBe('svg')
20
+ })
21
+
22
+ it('truncates long text content', () => {
23
+ const el = document.createElement('p')
24
+ el.textContent = 'a'.repeat(100)
25
+ expect(getElementName(el).length).toBeLessThanOrEqual(40)
26
+ })
27
+ })
28
+
29
+ describe('getCssSelector', () => {
30
+ it('returns id selector when element has id', () => {
31
+ const el = document.createElement('div')
32
+ el.id = 'main'
33
+ document.body.appendChild(el)
34
+ expect(getCssSelector(el)).toBe('#main')
35
+ document.body.removeChild(el)
36
+ })
37
+
38
+ it('returns tag selector for unknown element', () => {
39
+ const el = document.createElement('span')
40
+ const parent = document.createElement('div')
41
+ parent.appendChild(el)
42
+ document.body.appendChild(parent)
43
+ const selector = getCssSelector(el)
44
+ expect(selector).toContain('span')
45
+ document.body.removeChild(parent)
46
+ })
47
+ })
@@ -0,0 +1,84 @@
1
+ import type { ShadowContainer } from '../widget/shadow'
2
+
3
+ export class ElementHighlighter {
4
+ private highlightEl: HTMLDivElement
5
+
6
+ constructor(shadow: ShadowContainer) {
7
+ this.highlightEl = shadow.el<HTMLDivElement>('div', 'mtb-highlight')
8
+ this.hide()
9
+ shadow.append(this.highlightEl)
10
+ }
11
+
12
+ highlight(el: Element): void {
13
+ const rect = el.getBoundingClientRect()
14
+ this.highlightEl.style.top = `${rect.top - 2}px`
15
+ this.highlightEl.style.left = `${rect.left - 2}px`
16
+ this.highlightEl.style.width = `${rect.width + 4}px`
17
+ this.highlightEl.style.height = `${rect.height + 4}px`
18
+ this.highlightEl.style.display = 'block'
19
+ }
20
+
21
+ hide(): void {
22
+ this.highlightEl.style.display = 'none'
23
+ }
24
+
25
+ destroy(): void {
26
+ this.highlightEl.remove()
27
+ }
28
+ }
29
+
30
+ export function getElementName(el: Element): string {
31
+ if (el instanceof HTMLElement) {
32
+ const label = el.getAttribute('aria-label')
33
+ if (label) return label
34
+
35
+ const title = el.getAttribute('title')
36
+ if (title) return title
37
+
38
+ const text = el.textContent?.trim().slice(0, 40)
39
+ if (text) return text
40
+
41
+ const placeholder = (el as HTMLInputElement).placeholder
42
+ if (placeholder) return placeholder
43
+
44
+ const alt = (el as HTMLImageElement).alt
45
+ if (alt) return alt
46
+
47
+ const dataMark = el.getAttribute('data-mark')
48
+ if (dataMark) return dataMark
49
+ }
50
+ return el.tagName.toLowerCase()
51
+ }
52
+
53
+ export function getCssSelector(el: Element): string {
54
+ const parts: string[] = []
55
+ let current: Element | null = el
56
+ while (current && current !== document.body && parts.length < 5) {
57
+ let selector = current.tagName.toLowerCase()
58
+ if (current.id) {
59
+ selector = `#${current.id}`
60
+ parts.unshift(selector)
61
+ break
62
+ }
63
+ const classNames = Array.from(current.classList)
64
+ .filter(c => !c.startsWith('mtb-'))
65
+ .slice(0, 2)
66
+ .join('.')
67
+ if (classNames) selector += `.${classNames}`
68
+
69
+ const parent = current.parentElement
70
+ if (parent) {
71
+ const siblings = Array.from(parent.children).filter(
72
+ c => c.tagName === current!.tagName,
73
+ )
74
+ if (siblings.length > 1) {
75
+ const idx = siblings.indexOf(current) + 1
76
+ selector += `:nth-child(${idx})`
77
+ }
78
+ }
79
+
80
+ parts.unshift(selector)
81
+ current = current.parentElement
82
+ }
83
+ return parts.join(' > ')
84
+ }