@jjlmoya/utils-hardware 1.18.0 → 1.19.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 (38) hide show
  1. package/package.json +1 -1
  2. package/src/category/index.ts +2 -1
  3. package/src/entries.ts +4 -1
  4. package/src/index.ts +1 -0
  5. package/src/tests/locale_completeness.test.ts +2 -2
  6. package/src/tests/pagespeed_best_practices.test.ts +198 -0
  7. package/src/tests/tool_validation.test.ts +2 -2
  8. package/src/tool/batteryHealthEstimator/component.astro +3 -3
  9. package/src/tool/gamepadTest/component.astro +4 -3
  10. package/src/tool/gamepadVibrationTester/component.astro +3 -3
  11. package/src/tool/keyboardTest/component.astro +6 -1
  12. package/src/tool/mouseDoubleClickTest/bibliography.astro +14 -0
  13. package/src/tool/mouseDoubleClickTest/bibliography.ts +16 -0
  14. package/src/tool/mouseDoubleClickTest/component.astro +135 -0
  15. package/src/tool/mouseDoubleClickTest/entry.ts +29 -0
  16. package/src/tool/mouseDoubleClickTest/i18n/de.ts +274 -0
  17. package/src/tool/mouseDoubleClickTest/i18n/en.ts +274 -0
  18. package/src/tool/mouseDoubleClickTest/i18n/es.ts +274 -0
  19. package/src/tool/mouseDoubleClickTest/i18n/fr.ts +274 -0
  20. package/src/tool/mouseDoubleClickTest/i18n/id.ts +285 -0
  21. package/src/tool/mouseDoubleClickTest/i18n/it.ts +274 -0
  22. package/src/tool/mouseDoubleClickTest/i18n/ja.ts +274 -0
  23. package/src/tool/mouseDoubleClickTest/i18n/ko.ts +274 -0
  24. package/src/tool/mouseDoubleClickTest/i18n/nl.ts +274 -0
  25. package/src/tool/mouseDoubleClickTest/i18n/pl.ts +274 -0
  26. package/src/tool/mouseDoubleClickTest/i18n/pt.ts +274 -0
  27. package/src/tool/mouseDoubleClickTest/i18n/ru.ts +274 -0
  28. package/src/tool/mouseDoubleClickTest/i18n/sv.ts +274 -0
  29. package/src/tool/mouseDoubleClickTest/i18n/tr.ts +274 -0
  30. package/src/tool/mouseDoubleClickTest/i18n/zh.ts +274 -0
  31. package/src/tool/mouseDoubleClickTest/index.ts +9 -0
  32. package/src/tool/mouseDoubleClickTest/logic.ts +258 -0
  33. package/src/tool/mouseDoubleClickTest/mouse-double-click-test.css +488 -0
  34. package/src/tool/mouseDoubleClickTest/seo.astro +15 -0
  35. package/src/tool/mouseDoubleClickTest/ui.ts +26 -0
  36. package/src/tool/mousePollingTest/logic/RatonManager.ts +6 -6
  37. package/src/tool/toneGenerator/component.astro +7 -7
  38. package/src/tools.ts +2 -2
@@ -0,0 +1,274 @@
1
+ import type { WithContext, FAQPage, HowTo, SoftwareApplication } from 'schema-dts';
2
+ import type { ToolLocaleContent } from '../../../types';
3
+ import type { MouseDoubleClickTestUI } from '../ui';
4
+ import { bibliography } from '../bibliography';
5
+
6
+ const slug = 'mouse-double-click-test';
7
+ const title = '鼠标双击测试';
8
+ const description =
9
+ '测试每个鼠标按键,在浏览器中按按钮计时检测不必要的双击、磨损的开关和触点弹跳。';
10
+
11
+ const faqData = [
12
+ {
13
+ question: '什么是鼠标双击问题?',
14
+ answer:
15
+ '双击问题发生在一次物理按压被报告为两次点击时。它可能影响左键、右键、滚轮点击或侧键,通常由开关磨损、触点弹跳、固件去抖设置或无线信号不稳定引起。',
16
+ },
17
+ {
18
+ question: '什么样的间隔算可疑?',
19
+ answer:
20
+ '点击之间非常短的时间间隔是可疑的,因为人类的双击通常需要更多时间。此工具从80毫秒阈值开始,但您可以根据鼠标和测试风格进行调整。',
21
+ },
22
+ {
23
+ question: '浏览器能证明开关坏了吗?',
24
+ answer:
25
+ '浏览器不能直接检查电气开关,但可以记录操作系统接收到的点击事件。在单击测试期间重复出现可疑间隔是弹跳或不必要双击的有力证据。',
26
+ },
27
+ {
28
+ question: '我应该如何正确测试?',
29
+ answer:
30
+ '缓慢而刻意地点击,目标是单次按压。即使在您没有故意双击的情况下可疑计数器仍在上升,请在其他USB端口、其他浏览器以及可能的情况下在其他计算机上重复测试。',
31
+ },
32
+ ];
33
+
34
+ const howToData = [
35
+ {
36
+ name: '设置检测阈值',
37
+ text: '从80毫秒开始。对于严格的开关弹跳检测,降低阈值;如果您的设备产生稍宽的意外间隔,则提高阈值。',
38
+ },
39
+ {
40
+ name: '像正常的单击一样点击',
41
+ text: '在鼠标图形上逐个按下每个鼠标按钮。在第一轮中不要故意双击。',
42
+ },
43
+ {
44
+ name: '观察可疑计数器',
45
+ text: '如果您在单击时出现可疑事件,请检查哪个视觉按钮被高亮显示,并与紧凑的事件芯片进行比较。',
46
+ },
47
+ {
48
+ name: '与另一台设备比较',
49
+ text: '健康的鼠标在相同阈值下应保持高分。比较设备有助于区分硬件故障和软件设置。',
50
+ },
51
+ ];
52
+
53
+ const faqSchema: WithContext<FAQPage> = {
54
+ '@context': 'https://schema.org',
55
+ '@type': 'FAQPage',
56
+ mainEntity: faqData.map((item) => ({
57
+ '@type': 'Question',
58
+ name: item.question,
59
+ acceptedAnswer: { '@type': 'Answer', text: item.answer },
60
+ })),
61
+ };
62
+
63
+ const howToSchema: WithContext<HowTo> = {
64
+ '@context': 'https://schema.org',
65
+ '@type': 'HowTo',
66
+ name: title,
67
+ description,
68
+ step: howToData.map((step, i) => ({
69
+ '@type': 'HowToStep',
70
+ position: i + 1,
71
+ name: step.name,
72
+ text: step.text,
73
+ })),
74
+ };
75
+
76
+ const appSchema: WithContext<SoftwareApplication> = {
77
+ '@context': 'https://schema.org',
78
+ '@type': 'SoftwareApplication',
79
+ name: title,
80
+ description,
81
+ applicationCategory: 'UtilityApplication',
82
+ operatingSystem: 'All',
83
+ offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
84
+ inLanguage: 'zh',
85
+ };
86
+
87
+ export const content: ToolLocaleContent<MouseDoubleClickTestUI> = {
88
+ slug,
89
+ title,
90
+ description,
91
+ faq: faqData,
92
+ howTo: howToData,
93
+ schemas: [faqSchema, howToSchema, appSchema],
94
+ bibliography,
95
+ seo: [
96
+ {
97
+ type: 'title',
98
+ text: '鼠标双击测试:在线诊断按键弹跳',
99
+ level: 2,
100
+ },
101
+ {
102
+ type: 'paragraph',
103
+ html: '按下一次却双击的鼠标不仅仅是令人烦恼。它可能会意外打开文件夹、取消拖放操作、在游戏中发射两枪、关闭浏览器标签页,或者在您使用之前让右键菜单出现又消失。此在线鼠标双击测试专注于有用的诊断信号:<strong>操作系统报告的按键事件之间的时间间隔</strong>。',
104
+ },
105
+ {
106
+ type: 'paragraph',
107
+ html: '与简单的点击计数器不同,此工具独立跟踪每个按钮:左键点击、右键点击、滚轮点击、浏览器后退和浏览器前进。这很重要,因为鼠标可能右键故障而左键仍然健康,或者一个磨损的侧键在数月的游戏或使用生产力快捷键后才出现误触发。',
108
+ },
109
+ {
110
+ type: 'title',
111
+ text: '此鼠标按键测试测量什么',
112
+ level: 3,
113
+ },
114
+ {
115
+ type: 'paragraph',
116
+ html: '当您按下鼠标按钮时,浏览器会收到包含按钮代码的指针事件。该工具存储同一物理按钮的最后时间戳,并将其与同一按钮的下一次按压进行比较。如果间隔短于您选择的阈值,则该事件被标记为可疑,因为正常的故意第二次点击通常需要更长时间。',
117
+ },
118
+ {
119
+ type: 'list',
120
+ items: [
121
+ '左键:用于检测打开文件、选择文本或拖动窗口时的意外双击',
122
+ '右键:当上下文菜单闪烁、打开两次或立即关闭时有用',
123
+ '滚轮键:对于中键点击打开多个标签页或在大量浏览后失败的鼠标有用',
124
+ '后退和前进键:对于带有侧键开关的游戏鼠标和生产力鼠标有用',
125
+ '按按钮计时:避免将左键点击与右键点击混合并称之为错误双击',
126
+ ],
127
+ },
128
+ {
129
+ type: 'title',
130
+ text: '为什么鼠标开始双击',
131
+ level: 3,
132
+ },
133
+ {
134
+ type: 'paragraph',
135
+ html: '大多数鼠标在每个按钮下面使用小型机械式开关。当开关触点闭合时,金属在稳定之前可能会在非常短的时间内产生电气弹跳。鼠标固件通常通过去抖逻辑来过滤这种噪音。随着开关磨损,弹跳可能变得更长、更嘈杂或不一致,因此即使您的手指只做了一次物理按压,计算机也会接收到两次按钮按压。',
136
+ },
137
+ {
138
+ type: 'paragraph',
139
+ html: '双击并不总是由同一原因引起的。可能是机械开关磨损、固件去抖设置过于激进、开关内部的灰尘或氧化、无线数据包不稳定、宏软件、损坏的电缆或使意外双击更容易注意到的操作系统设置。',
140
+ },
141
+ {
142
+ type: 'table',
143
+ headers: ['症状', '可能的原因', '测试内容'],
144
+ rows: [
145
+ ['单击打开文件如同双击', '左键开关弹跳或操作系统双击速度混淆', '以80毫秒慢速单次按压测试左键'],
146
+ ['右键菜单闪烁或关闭', '右键开关弹跳或拦截上下文菜单的软件', '测试右键并暂时禁用鼠标实用程序'],
147
+ ['中键点击打开两个标签页', '滚轮开关磨损', '用有意识的单次按压测试滚轮'],
148
+ ['后退按钮跳过两页', '侧键开关弹跳或浏览器快捷键重复', '分别测试后退和前进'],
149
+ ['仅在无线模式下发生', '无线干扰、电池电量不足或接收器位置', '有线重新测试或将接收器移近'],
150
+ ],
151
+ },
152
+ {
153
+ type: 'title',
154
+ text: '选择合适的去抖阈值',
155
+ level: 3,
156
+ },
157
+ {
158
+ type: 'paragraph',
159
+ html: '阈值是该工具仍然认为可疑的最大间隔。默认值<strong>80毫秒</strong>是一个实用的中间值:足够严格以捕获许多不需要的重复事件,但不会过于激进以至于每次正常的故意双击都变成硬件故障。',
160
+ },
161
+ {
162
+ type: 'table',
163
+ headers: ['阈值', '最适合', '如何解读'],
164
+ rows: [
165
+ ['30-50毫秒', '严格的电气弹跳检查', '适合确认来自磨损开关的非常快速的重复事件'],
166
+ ['60-90毫秒', '通用鼠标双击诊断', '大多数游戏和办公鼠标的最佳默认范围'],
167
+ ['100-140毫秒', '间歇性磨损开关', '当鼠标有时产生较宽的意外间隔时有用'],
168
+ ['150-180毫秒', '压力测试和弱开关', '谨慎使用,因为快速的故意双击可能落入此范围'],
169
+ ],
170
+ },
171
+ {
172
+ type: 'title',
173
+ text: '如何运行可靠的测试',
174
+ level: 3,
175
+ },
176
+ {
177
+ type: 'paragraph',
178
+ html: '在第一轮中,不要故意双击。将光标放在鼠标图形上,缓慢而有意识地逐个按下每个鼠标按钮。健康的开关应产生稳定的单个事件。如果在缓慢的单次按压期间可疑计数器增加,请重复相同的按钮测试几次以确认模式。',
179
+ },
180
+ {
181
+ type: 'list',
182
+ items: [
183
+ '用20到30次缓慢按压测试左键,然后右键,然后滚轮,然后侧键',
184
+ '在测试按钮弹跳时不要拖动鼠标,因为移动可能会干扰计时结果',
185
+ '如果按钮显示可疑事件,请在更换USB端口或浏览器后重复相同的测试',
186
+ '对于无线鼠标,请使用新电池并将接收器放在鼠标附近进行测试',
187
+ '如果只有一个按钮反复失败,则故障可能在该特定开关而非整个设备',
188
+ ],
189
+ },
190
+ {
191
+ type: 'title',
192
+ text: '解读结果',
193
+ level: 3,
194
+ },
195
+ {
196
+ type: 'table',
197
+ headers: ['结果', '含义', '建议的下一步'],
198
+ rows: [
199
+ ['多次按压后0个可疑事件', '测试的按钮在所选阈值下看起来健康', '保持默认阈值或稍后如果症状复发再测试'],
200
+ ['1个孤立可疑事件', '可能是真实的弹跳或意外的快速按压', '在得出结论之前缓慢重复同一按钮'],
201
+ ['一个按钮上反复出现可疑事件', '开关弹跳或触点磨损的强烈迹象', '在另一台计算机上测试并记录结果'],
202
+ ['每个按钮上都有可疑事件', '可能是软件、驱动程序、宏实用程序或浏览器环境', '禁用鼠标软件并重新测试'],
203
+ ['仅无线模式失败', '可能是电池、接收器位置或干扰', '尝试有线模式或将接收器移近'],
204
+ ],
205
+ },
206
+ {
207
+ type: 'paragraph',
208
+ html: '健康评分是一个快速摘要,不是最终裁决。最重要的证据是<strong>哪个按钮</strong>产生了可疑事件,以及当您缓慢按下同一按钮时模式是否重复。在匆忙测试中的一次不良事件不如在有意识的单次按压中的五次可疑右键事件有意义。',
209
+ },
210
+ {
211
+ type: 'title',
212
+ text: '更换鼠标之前',
213
+ level: 3,
214
+ },
215
+ {
216
+ type: 'list',
217
+ items: [
218
+ '检查您的鼠标软件是否有去抖时间设置,并在更改后重新测试',
219
+ '在诊断期间禁用宏、连发配置文件或按钮重新映射',
220
+ '尝试不同的USB端口,特别是如果您使用集线器或前面板连接器',
221
+ '对于无线鼠标,使用靠近鼠标垫的延长线上的加密狗进行测试',
222
+ '与同一台计算机上的另一个鼠标进行比较,以区分硬件故障和软件行为',
223
+ ],
224
+ },
225
+ {
226
+ type: 'title',
227
+ text: '维修、保修和证据',
228
+ level: 3,
229
+ },
230
+ {
231
+ type: 'paragraph',
232
+ html: '如果故障是机械性的,清洁外壳很少能永久解决问题,因为问题在开关触点内部。一些用户更换微动开关,但这需要焊接,并非对每只鼠标都值得。如果鼠标在保修期内,同一按钮上重复出现的可疑间隔是向支持人员解释问题时的有用证据。',
233
+ },
234
+ {
235
+ type: 'paragraph',
236
+ html: '对于保修索赔,在缓慢按下故障按钮时录制简短的屏幕捕获。确保事件芯片显示按钮标签和可疑时间。这比说"我的鼠标有时双击"更清晰,因为它展示了实际按钮和不需要的重复事件的大致时间。',
237
+ },
238
+ {
239
+ type: 'title',
240
+ text: '基于浏览器的鼠标测试的局限性',
241
+ level: 3,
242
+ },
243
+ {
244
+ type: 'paragraph',
245
+ html: '此测试测量到达浏览器的事件。它不能直接检查开关内部的电波形,也不能绕过每个驱动程序、操作系统或供应商实用程序。这仍然有用:如果浏览器接收到重复事件,您的普通应用程序也可能接收到它们。对于工程级的验证,示波器或开关测试仪等硬件工具提供更深入的证据,但对于大多数用户诊断来说并不是必需的。',
246
+ },
247
+ ],
248
+ ui: {
249
+ badge: '开关弹跳实验室',
250
+ clickTarget: '按钮矩阵',
251
+ clickTargetHint: '按左键、右键、滚轮、后退和前进',
252
+ totalClicks: '总计',
253
+ suspiciousClicks: '可疑',
254
+ fastestGap: '最快间隔',
255
+ healthScore: '健康评分',
256
+ thresholdLabel: '检测阈值',
257
+ thresholdUnit: '毫秒',
258
+ cleanEvent: '正常',
259
+ suspiciousEvent: '可疑',
260
+ reset: '重置',
261
+ statusIdle: '按下每个鼠标按钮以独立测试',
262
+ statusClean: '未检测到可疑的按钮弹跳',
263
+ statusWarning: '在鼠标按钮上检测到可疑弹跳',
264
+ lastGap: '最后事件',
265
+ logTitle: '最近的按钮事件',
266
+ emptyLog: '在鼠标主体上按下任意鼠标按钮。',
267
+ leftButton: '左键',
268
+ middleButton: '滚轮',
269
+ rightButton: '右键',
270
+ backButton: '后退',
271
+ forwardButton: '前进',
272
+ otherButton: '其他',
273
+ },
274
+ };
@@ -0,0 +1,9 @@
1
+ import type { ToolDefinition } from '../../types';
2
+ import { mouseDoubleClickTest } from './entry';
3
+ export * from './entry';
4
+ export const MOUSE_DOUBLE_CLICK_TEST_TOOL: ToolDefinition = {
5
+ entry: mouseDoubleClickTest,
6
+ Component: () => import('./component.astro'),
7
+ SEOComponent: () => import('./seo.astro'),
8
+ BibliographyComponent: () => import('./bibliography.astro'),
9
+ };
@@ -0,0 +1,258 @@
1
+ type MouseButtonId = 'left' | 'middle' | 'right' | 'back' | 'forward' | 'other';
2
+
3
+ interface ButtonState {
4
+ count: number;
5
+ lastTime: number;
6
+ suspicious: number;
7
+ fastestGap: number;
8
+ }
9
+
10
+ export interface ClickSample {
11
+ button: MouseButtonId;
12
+ label: string;
13
+ gap: number;
14
+ suspicious: boolean;
15
+ }
16
+
17
+ interface MouseDoubleClickElements {
18
+ target: HTMLElement | null;
19
+ total: HTMLElement | null;
20
+ suspicious: HTMLElement | null;
21
+ fastest: HTMLElement | null;
22
+ score: HTMLElement | null;
23
+ status: HTMLElement | null;
24
+ threshold: HTMLInputElement | null;
25
+ thresholdValue: HTMLElement | null;
26
+ lastGap: HTMLElement | null;
27
+ log: HTMLElement | null;
28
+ reset: HTMLElement | null;
29
+ buttons: NodeListOf<HTMLElement>;
30
+ }
31
+
32
+ interface MouseDoubleClickLabels {
33
+ statusIdle: string;
34
+ statusClean: string;
35
+ statusWarning: string;
36
+ emptyLog: string;
37
+ leftButton: string;
38
+ middleButton: string;
39
+ rightButton: string;
40
+ backButton: string;
41
+ forwardButton: string;
42
+ otherButton: string;
43
+ thresholdUnit: string;
44
+ cleanEvent: string;
45
+ suspiciousEvent: string;
46
+ }
47
+
48
+ const BUTTON_FROM_CODE: Record<number, MouseButtonId> = {
49
+ 0: 'left',
50
+ 1: 'middle',
51
+ 2: 'right',
52
+ 3: 'back',
53
+ 4: 'forward',
54
+ };
55
+
56
+ export class MouseDoubleClickTester {
57
+ private threshold = 80;
58
+ private samples: ClickSample[] = [];
59
+ private readonly states = new Map<MouseButtonId, ButtonState>();
60
+
61
+ constructor(
62
+ private readonly elements: MouseDoubleClickElements,
63
+ private readonly labels: MouseDoubleClickLabels,
64
+ ) {
65
+ this.threshold = Number(elements.threshold?.value ?? 80);
66
+ this.bindEvents();
67
+ this.render();
68
+ }
69
+
70
+ private bindEvents() {
71
+ this.elements.target?.addEventListener('pointerdown', (event) => this.recordPointer(event));
72
+ this.elements.target?.addEventListener('contextmenu', (event) => event.preventDefault());
73
+ this.elements.target?.addEventListener('auxclick', (event) => event.preventDefault());
74
+ this.elements.threshold?.addEventListener('input', () => {
75
+ this.threshold = Number(this.elements.threshold?.value ?? 80);
76
+ this.recalculateSuspicion();
77
+ this.render();
78
+ });
79
+ this.elements.reset?.addEventListener('click', () => this.reset());
80
+ }
81
+
82
+ private recordPointer(event: PointerEvent) {
83
+ event.preventDefault();
84
+
85
+ const button = BUTTON_FROM_CODE[event.button] ?? 'other';
86
+ const now = performance.now();
87
+ const state = this.getButtonState(button);
88
+ const gap = state.lastTime > 0 ? now - state.lastTime : 0;
89
+ const suspicious = gap > 0 && gap <= this.threshold;
90
+
91
+ state.count++;
92
+ state.lastTime = now;
93
+
94
+ if (gap > 0) {
95
+ state.fastestGap = Math.min(state.fastestGap, gap);
96
+ if (suspicious) state.suspicious++;
97
+ this.samples.unshift({
98
+ button,
99
+ label: this.getButtonLabel(button),
100
+ gap,
101
+ suspicious,
102
+ });
103
+ }
104
+
105
+ this.flashButton(button, suspicious);
106
+ this.render();
107
+ }
108
+
109
+ private getButtonState(button: MouseButtonId) {
110
+ const existing = this.states.get(button);
111
+ if (existing) return existing;
112
+
113
+ const state: ButtonState = {
114
+ count: 0,
115
+ lastTime: 0,
116
+ suspicious: 0,
117
+ fastestGap: Infinity,
118
+ };
119
+ this.states.set(button, state);
120
+ return state;
121
+ }
122
+
123
+ private recalculateSuspicion() {
124
+ for (const state of this.states.values()) {
125
+ state.suspicious = 0;
126
+ }
127
+
128
+ this.samples = this.samples.map((sample) => {
129
+ const suspicious = sample.gap <= this.threshold;
130
+ if (suspicious) {
131
+ this.getButtonState(sample.button).suspicious++;
132
+ }
133
+ return { ...sample, suspicious };
134
+ });
135
+ }
136
+
137
+ private reset() {
138
+ this.states.clear();
139
+ this.samples = [];
140
+ this.elements.buttons.forEach((button) => {
141
+ button.dataset.count = '0';
142
+ button.dataset.state = '';
143
+ });
144
+ this.render();
145
+ }
146
+
147
+ private getTotals() {
148
+ let total = 0;
149
+ let suspicious = 0;
150
+ let fastestGap = Infinity;
151
+
152
+ for (const state of this.states.values()) {
153
+ total += state.count;
154
+ suspicious += state.suspicious;
155
+ fastestGap = Math.min(fastestGap, state.fastestGap);
156
+ }
157
+
158
+ return { total, suspicious, fastestGap };
159
+ }
160
+
161
+ private getScore() {
162
+ const { total, suspicious } = this.getTotals();
163
+ if (total < 2) return 100;
164
+ const cleanRatio = 1 - suspicious / Math.max(total - 1, 1);
165
+ return Math.max(0, Math.round(cleanRatio * 100));
166
+ }
167
+
168
+ private render() {
169
+ const score = this.getScore();
170
+ const totals = this.getTotals();
171
+ const lastSample = this.samples[0];
172
+
173
+ this.renderMetrics(score, totals, lastSample);
174
+ this.renderButtonCounts();
175
+ this.renderStatus(totals.total, totals.suspicious);
176
+ this.renderLog();
177
+ }
178
+
179
+ private renderMetrics(score: number, totals: ReturnType<MouseDoubleClickTester['getTotals']>, lastSample: ClickSample | undefined) {
180
+ this.setText(this.elements.total, String(totals.total));
181
+ this.setText(this.elements.suspicious, String(totals.suspicious));
182
+ this.setText(this.elements.fastest, this.formatGap(totals.fastestGap));
183
+ this.setText(this.elements.score, `${score}%`);
184
+ this.setText(this.elements.thresholdValue, String(this.threshold));
185
+ this.setText(this.elements.lastGap, lastSample ? `${lastSample.label} ${this.formatGap(lastSample.gap)}` : '--');
186
+ }
187
+
188
+ private setText(element: HTMLElement | null, value: string) {
189
+ if (element) element.textContent = value;
190
+ }
191
+
192
+ private formatGap(gap: number) {
193
+ return Number.isFinite(gap) ? `${Math.round(gap)} ${this.labels.thresholdUnit}` : '--';
194
+ }
195
+
196
+ private renderButtonCounts() {
197
+ this.elements.buttons.forEach((element) => {
198
+ const button = (element.dataset.button ?? 'other') as MouseButtonId;
199
+ const state = this.states.get(button);
200
+ element.dataset.count = String(state?.count ?? 0);
201
+ element.dataset.state = state && state.suspicious > 0 ? 'warning' : '';
202
+ });
203
+ }
204
+
205
+ private renderStatus(total: number, suspicious: number) {
206
+ if (!this.elements.status) return;
207
+
208
+ if (total < 2) {
209
+ this.elements.status.textContent = this.labels.statusIdle;
210
+ this.elements.status.dataset.state = 'clean';
211
+ return;
212
+ }
213
+
214
+ this.elements.status.textContent = suspicious > 0 ? this.labels.statusWarning : this.labels.statusClean;
215
+ this.elements.status.dataset.state = suspicious > 0 ? 'warning' : 'clean';
216
+ }
217
+
218
+ private renderLog() {
219
+ if (!this.elements.log) return;
220
+
221
+ if (this.samples.length === 0) {
222
+ this.elements.log.innerHTML = `<li class="mdct-empty">${this.labels.emptyLog}</li>`;
223
+ return;
224
+ }
225
+
226
+ this.elements.log.innerHTML = this.samples
227
+ .slice(0, 14)
228
+ .map((sample) => {
229
+ const state = sample.suspicious ? 'suspect' : 'clean';
230
+ const label = sample.suspicious ? this.labels.suspiciousEvent : this.labels.cleanEvent;
231
+ return `<li class="mdct-log-item ${state}"><b>${sample.label}</b><span>${Math.round(sample.gap)} ${this.labels.thresholdUnit}</span><em>${label}</em></li>`;
232
+ })
233
+ .join('');
234
+ }
235
+
236
+ private flashButton(button: MouseButtonId, suspicious: boolean) {
237
+ this.elements.target?.setAttribute('data-flash', suspicious ? 'warning' : 'clean');
238
+ const visualButton = Array.from(this.elements.buttons).find((element) => element.dataset.button === button);
239
+ if (visualButton) visualButton.dataset.flash = suspicious ? 'warning' : 'clean';
240
+
241
+ window.setTimeout(() => {
242
+ this.elements.target?.setAttribute('data-flash', '');
243
+ if (visualButton) visualButton.dataset.flash = '';
244
+ }, 180);
245
+ }
246
+
247
+ private getButtonLabel(button: MouseButtonId) {
248
+ const labels: Record<MouseButtonId, string> = {
249
+ left: this.labels.leftButton,
250
+ middle: this.labels.middleButton,
251
+ right: this.labels.rightButton,
252
+ back: this.labels.backButton,
253
+ forward: this.labels.forwardButton,
254
+ other: this.labels.otherButton,
255
+ };
256
+ return labels[button];
257
+ }
258
+ }