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,63 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest'
2
+ import { MakeThisBetter } from '../index'
3
+
4
+ vi.mock('html-to-image', () => ({
5
+ toPng: vi.fn(async () => 'data:image/png;base64,AA=='),
6
+ }))
7
+
8
+ describe('Pet mode', () => {
9
+ afterEach(() => {
10
+ MakeThisBetter.destroy()
11
+ vi.restoreAllMocks()
12
+ document.body.innerHTML = ''
13
+ })
14
+
15
+ it('renders pet entry instead of tab when entryMode is pet', () => {
16
+ MakeThisBetter.init({
17
+ projectKey: 'proj_test',
18
+ entryMode: 'pet',
19
+ apiUrl: 'https://api.example.com/api/v1',
20
+ })
21
+
22
+ const shadow = document.getElementById('mtb-widget-host')!.shadowRoot!
23
+ expect(shadow.querySelector('.mtb-pet')).toBeTruthy()
24
+ expect(shadow.querySelector('.mtb-tab')).toBeNull()
25
+ })
26
+
27
+ it('shows speech bubble on the pet', () => {
28
+ MakeThisBetter.init({
29
+ projectKey: 'proj_test',
30
+ entryMode: 'pet',
31
+ apiUrl: 'https://api.example.com/api/v1',
32
+ })
33
+
34
+ const shadow = document.getElementById('mtb-widget-host')!.shadowRoot!
35
+ const bubble = shadow.querySelector('.mtb-pet-bubble')
36
+ expect(bubble).toBeTruthy()
37
+ expect(bubble!.textContent).toBe('Need help?')
38
+ })
39
+
40
+ it('opens annotation mode on pet click', () => {
41
+ MakeThisBetter.init({
42
+ projectKey: 'proj_test',
43
+ entryMode: 'pet',
44
+ apiUrl: 'https://api.example.com/api/v1',
45
+ })
46
+
47
+ const shadow = document.getElementById('mtb-widget-host')!.shadowRoot!
48
+ shadow.querySelector<HTMLDivElement>('.mtb-pet')!.click()
49
+ expect(shadow.querySelector('.mtb-overlay')).toBeTruthy()
50
+ expect(shadow.querySelector('.mtb-toolbar')).toBeTruthy()
51
+ })
52
+
53
+ it('renders tab by default (no entryMode)', () => {
54
+ MakeThisBetter.init({
55
+ projectKey: 'proj_test',
56
+ apiUrl: 'https://api.example.com/api/v1',
57
+ })
58
+
59
+ const shadow = document.getElementById('mtb-widget-host')!.shadowRoot!
60
+ expect(shadow.querySelector('.mtb-tab')).toBeTruthy()
61
+ expect(shadow.querySelector('.mtb-pet')).toBeNull()
62
+ })
63
+ })
@@ -0,0 +1,67 @@
1
+ import type { ShadowContainer } from './shadow'
2
+ import type { I18nMessages } from '../i18n'
3
+ import { BRAND_COLOR } from '../styles'
4
+
5
+ export class PetEntry {
6
+ private el: HTMLDivElement
7
+ private bubble: HTMLDivElement
8
+ private onClick: () => void
9
+
10
+ constructor(
11
+ shadow: ShadowContainer,
12
+ messages: I18nMessages,
13
+ onClick: () => void,
14
+ ) {
15
+ this.onClick = onClick
16
+
17
+ this.el = shadow.el<HTMLDivElement>('div', 'mtb-pet')
18
+ this.el.innerHTML = this.buildSVG()
19
+ this.el.setAttribute('role', 'button')
20
+ this.el.setAttribute('aria-label', messages.tab)
21
+ this.el.tabIndex = 0
22
+
23
+ this.bubble = shadow.el<HTMLDivElement>('div', 'mtb-pet-bubble')
24
+ this.bubble.textContent = messages.pet.bubble
25
+ this.el.appendChild(this.bubble)
26
+
27
+ this.el.addEventListener('click', this.onClick)
28
+ this.el.addEventListener('keydown', (e: KeyboardEvent) => {
29
+ if (e.key === 'Enter' || e.key === ' ') {
30
+ e.preventDefault()
31
+ this.onClick()
32
+ }
33
+ })
34
+
35
+ shadow.append(this.el)
36
+ }
37
+
38
+ private buildSVG(): string {
39
+ return `<svg class="mtb-pet-svg" width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
40
+ <circle cx="24" cy="28" r="16" fill="${BRAND_COLOR}"/>
41
+ <circle cx="24" cy="28" r="14" fill="#10b981"/>
42
+ <circle cx="19" cy="25" r="3" fill="#fff"/>
43
+ <circle cx="29" cy="25" r="3" fill="#fff"/>
44
+ <circle cx="20" cy="24.5" r="1.5" fill="#064e3b"/>
45
+ <circle cx="30" cy="24.5" r="1.5" fill="#064e3b"/>
46
+ <ellipse cx="19.5" cy="23.5" rx="0.8" ry="0.6" fill="#fff" opacity="0.7"/>
47
+ <ellipse cx="29.5" cy="23.5" rx="0.8" ry="0.6" fill="#fff" opacity="0.7"/>
48
+ <path d="M20 31 Q24 34 28 31" stroke="#064e3b" stroke-width="1.5" stroke-linecap="round" fill="none"/>
49
+ <circle cx="15" cy="30" r="2.5" fill="${BRAND_COLOR}" opacity="0.4"/>
50
+ <circle cx="33" cy="30" r="2.5" fill="${BRAND_COLOR}" opacity="0.4"/>
51
+ <path d="M14 17 Q16 12 20 15" stroke="#10b981" stroke-width="2" stroke-linecap="round" fill="none"/>
52
+ <path d="M34 17 Q32 12 28 15" stroke="#10b981" stroke-width="2" stroke-linecap="round" fill="none"/>
53
+ </svg>`
54
+ }
55
+
56
+ setActive(active: boolean): void {
57
+ this.el.classList.toggle('active', active)
58
+ if (active) {
59
+ this.bubble.classList.add('hidden')
60
+ }
61
+ }
62
+
63
+ destroy(): void {
64
+ this.el.removeEventListener('click', this.onClick)
65
+ this.el.remove()
66
+ }
67
+ }
@@ -0,0 +1,25 @@
1
+ import { afterEach, describe, expect, it } from 'vitest'
2
+ import { ShadowContainer } from './shadow'
3
+
4
+ describe('ShadowContainer', () => {
5
+ afterEach(() => {
6
+ document.body.innerHTML = ''
7
+ })
8
+
9
+ it('mounts widget styles inside a shadow root', () => {
10
+ const shadow = new ShadowContainer()
11
+
12
+ expect(document.getElementById('mtb-widget-host')?.shadowRoot).toBe(shadow.root)
13
+ expect(shadow.root.querySelector('style')).toBeInstanceOf(HTMLStyleElement)
14
+
15
+ shadow.destroy()
16
+ })
17
+
18
+ it('stores configured theme on the host', () => {
19
+ const shadow = new ShadowContainer('dark')
20
+
21
+ expect(document.getElementById('mtb-widget-host')?.getAttribute('data-mtb-theme')).toBe('dark')
22
+
23
+ shadow.destroy()
24
+ })
25
+ })
@@ -0,0 +1,41 @@
1
+ import cssText from '../styles/widget.css?inline'
2
+ import type { MakeThisBetterConfig } from '../types'
3
+
4
+ export class ShadowContainer {
5
+ private host: HTMLElement
6
+ readonly root: ShadowRoot
7
+
8
+ constructor(theme: MakeThisBetterConfig['theme'] = 'auto') {
9
+ this.host = document.createElement('div')
10
+ this.host.id = 'mtb-widget-host'
11
+ this.host.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;overflow:visible;z-index:2147483646;'
12
+ this.host.setAttribute('data-mtb-theme', theme)
13
+ document.body.appendChild(this.host)
14
+
15
+ this.root = this.host.attachShadow({ mode: 'open' })
16
+
17
+ const style = document.createElement('style')
18
+ style.textContent = cssText
19
+ this.root.appendChild(style)
20
+ }
21
+
22
+ el<T extends HTMLElement = HTMLElement>(tag: string, className?: string): T {
23
+ const el = document.createElement(tag) as T
24
+ if (className) el.className = className
25
+ return el
26
+ }
27
+
28
+ append(...nodes: Node[]): void {
29
+ for (const n of nodes) this.root.appendChild(n)
30
+ }
31
+
32
+ remove(...nodes: Node[]): void {
33
+ for (const n of nodes) {
34
+ if (n.parentNode === this.root) this.root.removeChild(n)
35
+ }
36
+ }
37
+
38
+ destroy(): void {
39
+ document.body.removeChild(this.host)
40
+ }
41
+ }
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { FeedbackTab } from './tab'
3
+ import { ShadowContainer } from './shadow'
4
+ import { getMessages } from '../i18n'
5
+
6
+ function setupTab(position: 'left' | 'right' = 'right') {
7
+ const shadow = new ShadowContainer()
8
+ const onClick = vi.fn()
9
+ const messages = getMessages('en')
10
+ const tab = new FeedbackTab(shadow, messages, position, onClick)
11
+
12
+ return { shadow, tab, onClick, messages }
13
+ }
14
+
15
+ describe('FeedbackTab', () => {
16
+ it('renders button with feedback text', () => {
17
+ const { shadow, tab, messages } = setupTab()
18
+
19
+ const btn = shadow.root.querySelector<HTMLButtonElement>('.mtb-tab')
20
+ expect(btn).toBeTruthy()
21
+ expect(btn!.textContent).toBe(messages.tab)
22
+ expect(btn!.getAttribute('aria-label')).toBe(messages.tab)
23
+
24
+ tab.destroy()
25
+ shadow.destroy()
26
+ })
27
+
28
+ it('positions on right by default (no left class)', () => {
29
+ const { shadow, tab } = setupTab('right')
30
+
31
+ const btn = shadow.root.querySelector<HTMLButtonElement>('.mtb-tab')!
32
+ expect(btn.classList.contains('left')).toBe(false)
33
+
34
+ tab.destroy()
35
+ shadow.destroy()
36
+ })
37
+
38
+ it('adds left class when position is left', () => {
39
+ const { shadow, tab } = setupTab('left')
40
+
41
+ const btn = shadow.root.querySelector<HTMLButtonElement>('.mtb-tab')!
42
+ expect(btn.classList.contains('left')).toBe(true)
43
+
44
+ tab.destroy()
45
+ shadow.destroy()
46
+ })
47
+
48
+ it('calls onClick when button is clicked', () => {
49
+ const { shadow, tab, onClick } = setupTab()
50
+
51
+ shadow.root.querySelector<HTMLButtonElement>('.mtb-tab')!.click()
52
+ expect(onClick).toHaveBeenCalledOnce()
53
+
54
+ tab.destroy()
55
+ shadow.destroy()
56
+ })
57
+
58
+ it('setActive toggles active class', () => {
59
+ const { shadow, tab } = setupTab()
60
+ const btn = shadow.root.querySelector<HTMLButtonElement>('.mtb-tab')!
61
+
62
+ tab.setActive(true)
63
+ expect(btn.classList.contains('active')).toBe(true)
64
+
65
+ tab.setActive(false)
66
+ expect(btn.classList.contains('active')).toBe(false)
67
+
68
+ tab.destroy()
69
+ shadow.destroy()
70
+ })
71
+
72
+ it('destroy removes element from shadow root', () => {
73
+ const { shadow, tab } = setupTab()
74
+
75
+ expect(shadow.root.querySelector('.mtb-tab')).toBeTruthy()
76
+ tab.destroy()
77
+ expect(shadow.root.querySelector('.mtb-tab')).toBeNull()
78
+
79
+ shadow.destroy()
80
+ })
81
+
82
+ it('destroy removes click listener', () => {
83
+ const { shadow, tab, onClick } = setupTab()
84
+
85
+ tab.destroy()
86
+ const btn = shadow.root.querySelector('.mtb-tab')
87
+ expect(btn).toBeNull()
88
+ expect(onClick).not.toHaveBeenCalled()
89
+
90
+ shadow.destroy()
91
+ })
92
+ })
@@ -0,0 +1,31 @@
1
+ import type { ShadowContainer } from './shadow'
2
+ import type { I18nMessages } from '../i18n'
3
+
4
+ export class FeedbackTab {
5
+ private el: HTMLButtonElement
6
+ private onClick: () => void
7
+
8
+ constructor(
9
+ shadow: ShadowContainer,
10
+ messages: I18nMessages,
11
+ position: 'left' | 'right',
12
+ onClick: () => void,
13
+ ) {
14
+ this.onClick = onClick
15
+ this.el = shadow.el<HTMLButtonElement>('button', 'mtb-tab')
16
+ if (position === 'left') this.el.classList.add('left')
17
+ this.el.textContent = messages.tab
18
+ this.el.setAttribute('aria-label', messages.tab)
19
+ this.el.addEventListener('click', onClick)
20
+ shadow.append(this.el)
21
+ }
22
+
23
+ setActive(active: boolean): void {
24
+ this.el.classList.toggle('active', active)
25
+ }
26
+
27
+ destroy(): void {
28
+ this.el.removeEventListener('click', this.onClick)
29
+ this.el.remove()
30
+ }
31
+ }