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,134 @@
1
+ import { describe, it, expect, vi, afterEach } from 'vitest'
2
+ import { FrustrationDetector, FrustrationEvent } from './frustration'
3
+
4
+ describe('FrustrationDetector', () => {
5
+ let detector: FrustrationDetector
6
+ let events: FrustrationEvent[]
7
+
8
+ afterEach(() => {
9
+ detector?.stop()
10
+ })
11
+
12
+ function makeDetector(): FrustrationDetector {
13
+ events = []
14
+ detector = new FrustrationDetector((e) => events.push(e))
15
+ detector.start()
16
+ return detector
17
+ }
18
+
19
+ it('detects rage clicks (3+ clicks on same element within 1s)', () => {
20
+ makeDetector()
21
+ const div = document.createElement('div')
22
+ div.textContent = 'static text'
23
+ document.body.appendChild(div)
24
+
25
+ for (let i = 0; i < 3; i++) {
26
+ div.dispatchEvent(new MouseEvent('click', { bubbles: true }))
27
+ }
28
+
29
+ const rageEvents = events.filter(e => e.signal === 'rage_click')
30
+ expect(rageEvents.length).toBe(1)
31
+ expect(rageEvents[0].target).toContain('div')
32
+
33
+ document.body.removeChild(div)
34
+ })
35
+
36
+ it('does not trigger dead click from a single click (requires 3+ dead clicks AND console errors)', () => {
37
+ makeDetector()
38
+ const p = document.createElement('p')
39
+ p.textContent = 'just a paragraph'
40
+ document.body.appendChild(p)
41
+
42
+ p.dispatchEvent(new MouseEvent('click', { bubbles: true }))
43
+
44
+ const deadClicks = events.filter(e => e.signal === 'dead_click')
45
+ expect(deadClicks.length).toBe(0)
46
+
47
+ document.body.removeChild(p)
48
+ })
49
+
50
+ it('does not fire dead click for interactive elements', () => {
51
+ makeDetector()
52
+ const btn = document.createElement('button')
53
+ btn.textContent = 'Click me'
54
+ document.body.appendChild(btn)
55
+
56
+ btn.dispatchEvent(new MouseEvent('click', { bubbles: true }))
57
+
58
+ const deadClicks = events.filter(e => e.signal === 'dead_click')
59
+ expect(deadClicks.length).toBe(0)
60
+
61
+ document.body.removeChild(btn)
62
+ })
63
+
64
+ it('does not fire dead click for links', () => {
65
+ makeDetector()
66
+ const link = document.createElement('a')
67
+ link.href = '#'
68
+ link.textContent = 'Link'
69
+ document.body.appendChild(link)
70
+
71
+ link.dispatchEvent(new MouseEvent('click', { bubbles: true }))
72
+
73
+ const deadClicks = events.filter(e => e.signal === 'dead_click')
74
+ expect(deadClicks.length).toBe(0)
75
+
76
+ document.body.removeChild(link)
77
+ })
78
+
79
+ it('rage clicks always trigger (high priority, ignores cooldown)', () => {
80
+ makeDetector()
81
+ const div = document.createElement('div')
82
+ div.textContent = 'static'
83
+ document.body.appendChild(div)
84
+
85
+ for (let i = 0; i < 3; i++) div.dispatchEvent(new MouseEvent('click', { bubbles: true }))
86
+ expect(events.length).toBe(1)
87
+
88
+ for (let i = 0; i < 3; i++) div.dispatchEvent(new MouseEvent('click', { bubbles: true }))
89
+ expect(events.length).toBe(2)
90
+
91
+ document.body.removeChild(div)
92
+ })
93
+
94
+ it('ignores clicks on widget host element', () => {
95
+ makeDetector()
96
+ const host = document.createElement('div')
97
+ host.id = 'mtb-widget-host'
98
+ const inner = document.createElement('span')
99
+ inner.textContent = 'widget element'
100
+ host.appendChild(inner)
101
+ document.body.appendChild(host)
102
+
103
+ inner.dispatchEvent(new MouseEvent('click', { bubbles: true }))
104
+
105
+ expect(events.length).toBe(0)
106
+
107
+ document.body.removeChild(host)
108
+ })
109
+
110
+ it('does not directly emit console errors (only counts them as signal)', () => {
111
+ makeDetector()
112
+
113
+ if (window.onerror) {
114
+ ;(window.onerror as Function)('Test error', 'test.js', 1, 1, new Error('Test error'))
115
+ }
116
+
117
+ const errorEvents = events.filter(e => e.signal === 'console_error')
118
+ expect(errorEvents.length).toBe(0)
119
+ })
120
+
121
+ it('stops listening after stop()', () => {
122
+ makeDetector()
123
+ detector.stop()
124
+
125
+ const p = document.createElement('p')
126
+ p.textContent = 'text'
127
+ document.body.appendChild(p)
128
+ p.dispatchEvent(new MouseEvent('click', { bubbles: true }))
129
+
130
+ expect(events.length).toBe(0)
131
+
132
+ document.body.removeChild(p)
133
+ })
134
+ })
@@ -0,0 +1,145 @@
1
+ import { errorSource } from './error-source'
2
+
3
+ export type FrustrationSignal = 'rage_click' | 'dead_click' | 'console_error'
4
+
5
+ export interface FrustrationEvent {
6
+ signal: FrustrationSignal
7
+ target?: string
8
+ detail?: string
9
+ timestamp: number
10
+ }
11
+
12
+ interface ClickRecord {
13
+ target: EventTarget | null
14
+ time: number
15
+ }
16
+
17
+ const RAGE_CLICK_THRESHOLD = 3
18
+ const RAGE_CLICK_WINDOW_MS = 1000
19
+
20
+ const INTERACTIVE_TAGS = new Set([
21
+ 'A', 'BUTTON', 'INPUT', 'SELECT', 'TEXTAREA', 'LABEL', 'SUMMARY', 'DETAILS',
22
+ ])
23
+
24
+ const INTERACTIVE_ROLES = new Set([
25
+ 'button', 'link', 'checkbox', 'radio', 'tab', 'menuitem', 'option',
26
+ 'switch', 'textbox', 'combobox', 'listbox', 'slider', 'spinbutton',
27
+ ])
28
+
29
+ export class FrustrationDetector {
30
+ private clickHistory: ClickRecord[] = []
31
+ private onFrustration: (event: FrustrationEvent) => void
32
+ private boundClickHandler: (e: MouseEvent) => void
33
+ private unsubscribeErrors: (() => void) | null = null
34
+ private active = false
35
+ private errorCount = 0
36
+ private deadClickCount = 0
37
+ private cooldownUntil = 0
38
+ private static readonly COOLDOWN_MS = 60_000
39
+
40
+ constructor(onFrustration: (event: FrustrationEvent) => void) {
41
+ this.onFrustration = onFrustration
42
+ this.boundClickHandler = (e: MouseEvent) => this.handleClick(e)
43
+ }
44
+
45
+ start(): void {
46
+ if (this.active) return
47
+ this.active = true
48
+ document.addEventListener('click', this.boundClickHandler, true)
49
+ this.unsubscribeErrors = errorSource.subscribe((_e) => {
50
+ this.errorCount++
51
+ })
52
+ }
53
+
54
+ stop(): void {
55
+ if (!this.active) return
56
+ this.active = false
57
+ document.removeEventListener('click', this.boundClickHandler, true)
58
+ this.unsubscribeErrors?.()
59
+ this.unsubscribeErrors = null
60
+ this.clickHistory = []
61
+ }
62
+
63
+ private handleClick(e: MouseEvent): void {
64
+ const target = e.target as Element | null
65
+ if (!target) return
66
+
67
+ if (this.isWidgetElement(target)) return
68
+
69
+ const now = Date.now()
70
+ this.clickHistory.push({ target, time: now })
71
+
72
+ const cutoff = now - RAGE_CLICK_WINDOW_MS
73
+ this.clickHistory = this.clickHistory.filter(r => r.time > cutoff)
74
+
75
+ const sameTargetClicks = this.clickHistory.filter(r => r.target === target)
76
+ if (sameTargetClicks.length >= RAGE_CLICK_THRESHOLD) {
77
+ this.clickHistory = []
78
+ this.emitHighPriority({
79
+ signal: 'rage_click',
80
+ target: describeElement(target),
81
+ timestamp: now,
82
+ })
83
+ return
84
+ }
85
+
86
+ if (!this.isInteractive(target)) {
87
+ this.deadClickCount++
88
+ if (this.deadClickCount >= 3 && this.errorCount > 0) {
89
+ this.deadClickCount = 0
90
+ this.errorCount = 0
91
+ this.emitIfReady({
92
+ signal: 'dead_click',
93
+ target: describeElement(target),
94
+ timestamp: now,
95
+ })
96
+ }
97
+ }
98
+ }
99
+
100
+ private isInteractive(el: Element): boolean {
101
+ if (INTERACTIVE_TAGS.has(el.tagName)) return true
102
+
103
+ const role = el.getAttribute('role')
104
+ if (role && INTERACTIVE_ROLES.has(role)) return true
105
+
106
+ if (el.hasAttribute('onclick') || el.hasAttribute('tabindex')) return true
107
+ if ((el as HTMLElement).contentEditable === 'true') return true
108
+
109
+ const style = window.getComputedStyle(el)
110
+ if (style.cursor === 'pointer') return true
111
+
112
+ const parent = el.closest('a, button, [role="button"], [role="link"], label')
113
+ if (parent) return true
114
+
115
+ return false
116
+ }
117
+
118
+ private isWidgetElement(el: Element): boolean {
119
+ const host = document.getElementById('mtb-widget-host')
120
+ if (!host) return false
121
+ return host === el || host.contains(el)
122
+ }
123
+
124
+ private emitIfReady(event: FrustrationEvent): void {
125
+ const now = Date.now()
126
+ if (now < this.cooldownUntil) return
127
+ this.cooldownUntil = now + FrustrationDetector.COOLDOWN_MS
128
+ this.onFrustration(event)
129
+ }
130
+
131
+ private emitHighPriority(event: FrustrationEvent): void {
132
+ this.cooldownUntil = Date.now() + FrustrationDetector.COOLDOWN_MS
133
+ this.onFrustration(event)
134
+ }
135
+ }
136
+
137
+ function describeElement(el: Element): string {
138
+ const tag = el.tagName.toLowerCase()
139
+ const id = el.id ? `#${el.id}` : ''
140
+ const cls = el.className && typeof el.className === 'string'
141
+ ? '.' + el.className.trim().split(/\s+/).slice(0, 2).join('.')
142
+ : ''
143
+ const text = el.textContent?.trim().slice(0, 30) ?? ''
144
+ return `<${tag}${id}${cls}>${text ? ` "${text}"` : ''}`
145
+ }
package/src/global.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { MakeThisBetter } from './index'
2
+
3
+ export default MakeThisBetter
package/src/i18n/en.ts ADDED
@@ -0,0 +1,47 @@
1
+ export default {
2
+ tab: 'Feedback',
3
+ toolbar: {
4
+ markup: 'Mark up',
5
+ record: 'Record',
6
+ hint: 'Click any element to annotate · drag to draw',
7
+ hintRecord: 'Recording your screen',
8
+ exit: 'Exit',
9
+ },
10
+ popup: {
11
+ about: 'About',
12
+ placeholder: 'Describe the issue or suggestion...',
13
+ submit: 'Submit',
14
+ cancel: 'Cancel',
15
+ quickOptions: [
16
+ { emoji: '🐛', label: 'Something is broken' },
17
+ { emoji: '💡', label: 'I wish this worked differently' },
18
+ { emoji: '🤔', label: "I don't understand this" },
19
+ { emoji: '✨', label: 'I have an idea' },
20
+ ],
21
+ },
22
+ success: {
23
+ title: 'Thanks!',
24
+ message: 'Your feedback has been received.',
25
+ close: 'Close',
26
+ },
27
+ error: {
28
+ submit: 'Failed to submit. Please try again.',
29
+ },
30
+ pet: {
31
+ bubble: 'Need help?',
32
+ },
33
+ frustration: {
34
+ prompt: 'Something not working? Tell us.',
35
+ action: 'Annotate',
36
+ dismiss: 'Dismiss',
37
+ },
38
+ annotation: {
39
+ drawing: 'Drawing',
40
+ element: 'element',
41
+ },
42
+ record: {
43
+ stop: 'Stop',
44
+ timer_label: 'Recording',
45
+ max_reached: 'Maximum recording time reached',
46
+ },
47
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, it, expect } from 'vitest'
2
+ import { getMessages } from './index'
3
+
4
+ describe('getMessages', () => {
5
+ it('returns English for "en"', () => {
6
+ const m = getMessages('en')
7
+ expect(m.tab).toBe('Feedback')
8
+ })
9
+
10
+ it('returns Chinese for "zh-CN"', () => {
11
+ const m = getMessages('zh-CN')
12
+ expect(m.tab).toBe('反馈')
13
+ })
14
+
15
+ it('falls back to English for unknown locale', () => {
16
+ const m = getMessages('fr')
17
+ expect(m.tab).toBe('Feedback')
18
+ })
19
+
20
+ it('returns English for "en-US" by prefix match fallback', () => {
21
+ const m = getMessages('en-US')
22
+ expect(m.tab).toBe('Feedback')
23
+ })
24
+ })
@@ -0,0 +1,16 @@
1
+ import en from './en'
2
+ import zhCN from './zh-CN'
3
+
4
+ export type I18nMessages = typeof en
5
+
6
+ const locales: Record<string, I18nMessages> = {
7
+ en,
8
+ 'zh-CN': zhCN,
9
+ }
10
+
11
+ export function getMessages(locale: string): I18nMessages {
12
+ if (locales[locale]) return locales[locale]
13
+ const lang = locale.split('-')[0]
14
+ if (locales[lang]) return locales[lang]
15
+ return locales['en']
16
+ }
@@ -0,0 +1,47 @@
1
+ export default {
2
+ tab: '反馈',
3
+ toolbar: {
4
+ markup: '标注',
5
+ record: '录屏',
6
+ hint: '点击任意元素进行标注 · 拖拽可自由绘制',
7
+ hintRecord: '正在录制屏幕',
8
+ exit: '退出',
9
+ },
10
+ popup: {
11
+ about: '关于',
12
+ placeholder: '描述问题或建议...',
13
+ submit: '提交',
14
+ cancel: '取消',
15
+ quickOptions: [
16
+ { emoji: '🐛', label: '这里有问题' },
17
+ { emoji: '💡', label: '我希望它能更好用' },
18
+ { emoji: '🤔', label: '我看不懂这里' },
19
+ { emoji: '✨', label: '我有个想法' },
20
+ ],
21
+ },
22
+ success: {
23
+ title: '感谢!',
24
+ message: '您的反馈已收到。',
25
+ close: '关闭',
26
+ },
27
+ error: {
28
+ submit: '提交失败,请重试。',
29
+ },
30
+ pet: {
31
+ bubble: '需要帮助吗?',
32
+ },
33
+ frustration: {
34
+ prompt: '遇到问题了?告诉我们。',
35
+ action: '标注',
36
+ dismiss: '关闭',
37
+ },
38
+ annotation: {
39
+ drawing: '绘制',
40
+ element: '元素',
41
+ },
42
+ record: {
43
+ stop: '停止',
44
+ timer_label: '录制中',
45
+ max_reached: '已达到最大录制时长',
46
+ },
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { WidgetController } from './widget/controller'
2
+ import type { MakeThisBetterConfig } from './types'
3
+
4
+ let instance: WidgetController | null = null
5
+
6
+ const MakeThisBetter = {
7
+ init(config: MakeThisBetterConfig): void {
8
+ if (instance) {
9
+ instance.destroy()
10
+ instance = null
11
+ }
12
+ if (document.readyState === 'loading') {
13
+ document.addEventListener('DOMContentLoaded', () => {
14
+ instance = new WidgetController(config)
15
+ })
16
+ } else {
17
+ instance = new WidgetController(config)
18
+ }
19
+ },
20
+
21
+ destroy(): void {
22
+ instance?.destroy()
23
+ instance = null
24
+ },
25
+ }
26
+
27
+ export { MakeThisBetter }
28
+ export type { MakeThisBetterConfig }
29
+
30
+ if (typeof window !== 'undefined') {
31
+ (window as Window & { MakeThisBetter?: typeof MakeThisBetter }).MakeThisBetter = MakeThisBetter
32
+ }
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it, vi } from 'vitest'
2
+ import { CommentPopup } from './comment'
3
+ import { ShadowContainer } from '../widget/shadow'
4
+ import { getMessages } from '../i18n'
5
+
6
+ function setupPopup() {
7
+ const shadow = new ShadowContainer()
8
+ const onSubmit = vi.fn()
9
+ const onClose = vi.fn()
10
+ const popup = new CommentPopup(shadow, {
11
+ targetName: 'Export PDF',
12
+ x: 100,
13
+ y: 120,
14
+ messages: getMessages('en'),
15
+ onSubmit,
16
+ onClose,
17
+ })
18
+
19
+ return { shadow, popup, onSubmit, onClose }
20
+ }
21
+
22
+ describe('CommentPopup', () => {
23
+ it('renders visible submit and cancel actions', () => {
24
+ const { shadow, popup } = setupPopup()
25
+
26
+ expect(shadow.root.querySelector('.mtb-submit-btn')?.textContent).toBe('Submit')
27
+ expect(shadow.root.querySelector('.mtb-cancel-btn')?.textContent).toBe('Cancel')
28
+
29
+ popup.destroy()
30
+ shadow.destroy()
31
+ })
32
+
33
+ it('enables submit after entering a description', () => {
34
+ const { shadow, popup } = setupPopup()
35
+ const textarea = shadow.root.querySelector<HTMLTextAreaElement>('.mtb-textarea')!
36
+ const submit = shadow.root.querySelector<HTMLButtonElement>('.mtb-submit-btn')!
37
+
38
+ textarea.value = 'Export does not work'
39
+ textarea.dispatchEvent(new Event('input'))
40
+
41
+ expect(submit.disabled).toBe(false)
42
+ expect(submit.classList.contains('ready')).toBe(true)
43
+
44
+ popup.destroy()
45
+ shadow.destroy()
46
+ })
47
+
48
+ it('submits trimmed description', () => {
49
+ const { shadow, popup, onSubmit } = setupPopup()
50
+ const textarea = shadow.root.querySelector<HTMLTextAreaElement>('.mtb-textarea')!
51
+ const submit = shadow.root.querySelector<HTMLButtonElement>('.mtb-submit-btn')!
52
+
53
+ textarea.value = ' Export does not work '
54
+ textarea.dispatchEvent(new Event('input'))
55
+ submit.click()
56
+
57
+ expect(onSubmit).toHaveBeenCalledWith('Export does not work')
58
+
59
+ popup.destroy()
60
+ shadow.destroy()
61
+ })
62
+
63
+ it('calls close from cancel', () => {
64
+ const { shadow, popup, onClose } = setupPopup()
65
+
66
+ shadow.root.querySelector<HTMLButtonElement>('.mtb-cancel-btn')!.click()
67
+
68
+ expect(onClose).toHaveBeenCalled()
69
+
70
+ popup.destroy()
71
+ shadow.destroy()
72
+ })
73
+ })