@jjlmoya/utils-hardware 1.24.0 → 1.25.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/src/category/index.ts +3 -1
- package/src/entries.ts +7 -1
- package/src/index.ts +2 -0
- package/src/tests/locale_completeness.test.ts +2 -2
- package/src/tests/tool_validation.test.ts +2 -2
- package/src/tool/keyboardChatterTest/bibliography.astro +15 -0
- package/src/tool/keyboardChatterTest/bibliography.ts +20 -0
- package/src/tool/keyboardChatterTest/component.astro +353 -0
- package/src/tool/keyboardChatterTest/entry.ts +30 -0
- package/src/tool/keyboardChatterTest/i18n/de.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/en.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/es.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/fr.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/id.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/it.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/ja.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/ko.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/nl.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/pl.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/pt.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/ru.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/sv.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/tr.ts +232 -0
- package/src/tool/keyboardChatterTest/i18n/zh.ts +232 -0
- package/src/tool/keyboardChatterTest/index.ts +12 -0
- package/src/tool/keyboardChatterTest/keyboard-chatter-test.css +512 -0
- package/src/tool/keyboardChatterTest/logic.ts +23 -0
- package/src/tool/keyboardChatterTest/seo.astro +16 -0
- package/src/tool/keyboardChatterTest/ui.ts +34 -0
- package/src/tool/webBluetoothBleScanner/bibliography.astro +14 -0
- package/src/tool/webBluetoothBleScanner/bibliography.ts +16 -0
- package/src/tool/webBluetoothBleScanner/component.astro +339 -0
- package/src/tool/webBluetoothBleScanner/entry.ts +29 -0
- package/src/tool/webBluetoothBleScanner/i18n/de.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/en.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/es.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/fr.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/id.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/it.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/ja.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/ko.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/nl.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/pl.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/pt.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/ru.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/sv.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/tr.ts +233 -0
- package/src/tool/webBluetoothBleScanner/i18n/zh.ts +233 -0
- package/src/tool/webBluetoothBleScanner/index.ts +11 -0
- package/src/tool/webBluetoothBleScanner/logic.ts +79 -0
- package/src/tool/webBluetoothBleScanner/seo.astro +15 -0
- package/src/tool/webBluetoothBleScanner/ui.ts +41 -0
- package/src/tool/webBluetoothBleScanner/web-bluetooth-ble-scanner.css +406 -0
- package/src/tools.ts +3 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jjlmoya/utils-hardware",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"scripts": {
|
|
20
20
|
"dev": "astro dev",
|
|
21
|
+
"dev:host": "astro dev --host --port 3024",
|
|
21
22
|
"start": "astro dev",
|
|
22
23
|
"build": "astro build",
|
|
23
24
|
"preview": "astro preview",
|
package/src/category/index.ts
CHANGED
|
@@ -13,10 +13,12 @@ import { monitorGhostingTest } from '../tool/monitorGhostingTest/index';
|
|
|
13
13
|
import { spectrumCanvas } from '../tool/colorAccuracyTest/index';
|
|
14
14
|
import { upsRuntimeCalculator } from '../tool/upsRuntimeCalculator/index';
|
|
15
15
|
import { stereoAudioTest } from '../tool/stereoAudioTest/index';
|
|
16
|
+
import { webBluetoothBleScanner } from '../tool/webBluetoothBleScanner/index';
|
|
17
|
+
import { keyboardChatterTest } from '../tool/keyboardChatterTest/index';
|
|
16
18
|
|
|
17
19
|
export const hardwareCategory: HardwareCategoryEntry = {
|
|
18
20
|
icon: 'mdi:memory',
|
|
19
|
-
tools: [pixelesPantalla, testTeclado, testMando, probadorVibracionMando, testRaton, mouseDoubleClickTest, mouseScrollTest, estimadorSaludBateria, toneGenerator, refreshRateDetector, monitorGhostingTest, spectrumCanvas, upsRuntimeCalculator, stereoAudioTest],
|
|
21
|
+
tools: [pixelesPantalla, testTeclado, keyboardChatterTest, testMando, probadorVibracionMando, testRaton, mouseDoubleClickTest, mouseScrollTest, estimadorSaludBateria, toneGenerator, refreshRateDetector, monitorGhostingTest, spectrumCanvas, upsRuntimeCalculator, stereoAudioTest, webBluetoothBleScanner],
|
|
20
22
|
i18n: {
|
|
21
23
|
en: () => import('./i18n/en').then((m) => m.content),
|
|
22
24
|
es: () => import('./i18n/es').then((m) => m.content),
|
package/src/entries.ts
CHANGED
|
@@ -26,6 +26,10 @@ export { upsRuntimeCalculator } from './tool/upsRuntimeCalculator/entry';
|
|
|
26
26
|
export type { UpsRuntimeCalculatorLocaleContent } from './tool/upsRuntimeCalculator/entry';
|
|
27
27
|
export { stereoAudioTest } from './tool/stereoAudioTest/entry';
|
|
28
28
|
export type { StereoAudioTestLocaleContent } from './tool/stereoAudioTest/entry';
|
|
29
|
+
export { webBluetoothBleScanner } from './tool/webBluetoothBleScanner/entry';
|
|
30
|
+
export type { WebBluetoothBleScannerLocaleContent } from './tool/webBluetoothBleScanner/entry';
|
|
31
|
+
export { keyboardChatterTest } from './tool/keyboardChatterTest/entry';
|
|
32
|
+
export type { KeyboardChatterTestLocaleContent } from './tool/keyboardChatterTest/entry';
|
|
29
33
|
export { hardwareCategory } from './category';
|
|
30
34
|
import { estimadorSaludBateria } from './tool/batteryHealthEstimator/entry';
|
|
31
35
|
import { pixelesPantalla } from './tool/deadPixelTest/entry';
|
|
@@ -41,4 +45,6 @@ import { monitorGhostingTest } from './tool/monitorGhostingTest/entry';
|
|
|
41
45
|
import { spectrumCanvas } from './tool/colorAccuracyTest/entry';
|
|
42
46
|
import { upsRuntimeCalculator } from './tool/upsRuntimeCalculator/entry';
|
|
43
47
|
import { stereoAudioTest } from './tool/stereoAudioTest/entry';
|
|
44
|
-
|
|
48
|
+
import { webBluetoothBleScanner } from './tool/webBluetoothBleScanner/entry';
|
|
49
|
+
import { keyboardChatterTest } from './tool/keyboardChatterTest/entry';
|
|
50
|
+
export const ALL_ENTRIES = [estimadorSaludBateria, pixelesPantalla, testMando, probadorVibracionMando, testTeclado, keyboardChatterTest, testRaton, mouseDoubleClickTest, mouseScrollTest, toneGenerator, refreshRateDetector, monitorGhostingTest, spectrumCanvas, upsRuntimeCalculator, stereoAudioTest, webBluetoothBleScanner];
|
package/src/index.ts
CHANGED
|
@@ -31,3 +31,5 @@ export { MONITOR_GHOSTING_TEST_TOOL } from './tool/monitorGhostingTest/index';
|
|
|
31
31
|
export { SPECTRUM_CANVAS_TOOL } from './tool/colorAccuracyTest/index';
|
|
32
32
|
export { UPS_RUNTIME_CALCULATOR_TOOL } from './tool/upsRuntimeCalculator/index';
|
|
33
33
|
export { STEREO_AUDIO_TEST_TOOL } from './tool/stereoAudioTest/index';
|
|
34
|
+
export { WEB_BLUETOOTH_BLE_SCANNER_TOOL } from './tool/webBluetoothBleScanner/index';
|
|
35
|
+
export { KEYBOARD_CHATTER_TEST_TOOL } from './tool/keyboardChatterTest/index';
|
|
@@ -4,8 +4,8 @@ import { hardwareCategory } from '../data';
|
|
|
4
4
|
|
|
5
5
|
describe('Tool Validation Suite', () => {
|
|
6
6
|
describe('Library Registration', () => {
|
|
7
|
-
it('should have
|
|
8
|
-
expect(ALL_TOOLS.length).toBe(
|
|
7
|
+
it('should have all tools in ALL_TOOLS', () => {
|
|
8
|
+
expect(ALL_TOOLS.length).toBe(16);
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it('hardwareCategory should be defined', () => {
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Bibliography as SharedBibliography } from '@jjlmoya/utils-shared';
|
|
3
|
+
import type { KnownLocale } from '../../types';
|
|
4
|
+
import { keyboardChatterTest } from './index';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const { locale = 'en' } = Astro.props;
|
|
11
|
+
const content = await keyboardChatterTest.i18n[locale]?.();
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
{content && content.bibliography.length > 0 && <SharedBibliography links={content.bibliography} />}
|
|
15
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { BibliographyEntry } from '../../types';
|
|
2
|
+
|
|
3
|
+
export const bibliography: BibliographyEntry[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'QMK Firmware - Understanding keyboard debounce settings',
|
|
6
|
+
url: 'https://docs.qmk.fm/feature_debounce_type',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: 'WIRED - How to clean your keyboard safely',
|
|
10
|
+
url: 'https://www.wired.com/story/how-to-clean-your-keyboard/',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'Lifewire - How to clean a mechanical keyboard',
|
|
14
|
+
url: 'https://www.lifewire.com/clean-mechanical-keyboard-8681086',
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'Tom\'s Guide - Deep cleaning a mechanical keyboard',
|
|
18
|
+
url: 'https://www.tomsguide.com/computing/keyboards/your-mechanical-keyboard-is-disgusting-heres-how-to-clean-it-properly-in-30-minutes',
|
|
19
|
+
},
|
|
20
|
+
];
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
---
|
|
2
|
+
import { Icon } from 'astro-icon/components';
|
|
3
|
+
import type { KnownLocale } from '../../types';
|
|
4
|
+
import type { KeyboardChatterTestUI } from './ui';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
locale?: KnownLocale;
|
|
8
|
+
ui?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const { ui } = Astro.props;
|
|
12
|
+
const t = (ui ?? {}) as KeyboardChatterTestUI;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<div
|
|
16
|
+
class="kct-root"
|
|
17
|
+
data-config={JSON.stringify({
|
|
18
|
+
statusIdle: t.statusIdle,
|
|
19
|
+
statusListening: t.statusListening,
|
|
20
|
+
statusChatter: t.statusChatter,
|
|
21
|
+
normal: t.normal,
|
|
22
|
+
suspect: t.suspect,
|
|
23
|
+
chatter: t.chatter,
|
|
24
|
+
waiting: t.waiting,
|
|
25
|
+
spaceKey: t.spaceKey,
|
|
26
|
+
csvHeader: t.csvHeader,
|
|
27
|
+
})}
|
|
28
|
+
>
|
|
29
|
+
<section class="kct-panel" tabindex="0" aria-live="polite">
|
|
30
|
+
<div class="kct-console">
|
|
31
|
+
<div class="kct-actions">
|
|
32
|
+
<button type="button" data-clear title={t.clear} aria-label={t.clear}>
|
|
33
|
+
<Icon name="mdi:trash-can-outline" />
|
|
34
|
+
</button>
|
|
35
|
+
<button type="button" data-export title={t.exportLog} aria-label={t.exportLog}>
|
|
36
|
+
<Icon name="mdi:download" />
|
|
37
|
+
</button>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<div class="kct-readout">
|
|
41
|
+
<span data-status>{t.statusIdle}</span>
|
|
42
|
+
<strong data-live-key>{t.waiting}</strong>
|
|
43
|
+
<em>{t.captureNotice}</em>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<div class="kct-gauge" aria-hidden="true">
|
|
47
|
+
<span data-pulse></span>
|
|
48
|
+
<i>50</i>
|
|
49
|
+
<i>30</i>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<div class="kct-strip" aria-hidden="true">
|
|
53
|
+
<span></span>
|
|
54
|
+
<span></span>
|
|
55
|
+
<span></span>
|
|
56
|
+
<span></span>
|
|
57
|
+
<span></span>
|
|
58
|
+
<span></span>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
62
|
+
<div class="kct-keyboard" aria-label={t.keyboardAriaLabel}>
|
|
63
|
+
<kbd data-key="Escape">{t.escapeKey}</kbd>
|
|
64
|
+
<kbd data-key="Digit1">1</kbd>
|
|
65
|
+
<kbd data-key="Digit2">2</kbd>
|
|
66
|
+
<kbd data-key="Digit3">3</kbd>
|
|
67
|
+
<kbd data-key="Digit4">4</kbd>
|
|
68
|
+
<kbd data-key="Digit5">5</kbd>
|
|
69
|
+
<kbd data-key="Digit6">6</kbd>
|
|
70
|
+
<kbd data-key="Digit7">7</kbd>
|
|
71
|
+
<kbd data-key="Digit8">8</kbd>
|
|
72
|
+
<kbd data-key="Digit9">9</kbd>
|
|
73
|
+
<kbd data-key="Digit0">0</kbd>
|
|
74
|
+
<kbd data-key="Backspace" class="wide">{t.backspaceKey}</kbd>
|
|
75
|
+
<kbd data-key="Tab" class="wide">{t.tabKey}</kbd>
|
|
76
|
+
<kbd data-key="KeyQ">Q</kbd>
|
|
77
|
+
<kbd data-key="KeyW">W</kbd>
|
|
78
|
+
<kbd data-key="KeyE">E</kbd>
|
|
79
|
+
<kbd data-key="KeyR">R</kbd>
|
|
80
|
+
<kbd data-key="KeyT">T</kbd>
|
|
81
|
+
<kbd data-key="KeyY">Y</kbd>
|
|
82
|
+
<kbd data-key="KeyU">U</kbd>
|
|
83
|
+
<kbd data-key="KeyI">I</kbd>
|
|
84
|
+
<kbd data-key="KeyO">O</kbd>
|
|
85
|
+
<kbd data-key="KeyP">P</kbd>
|
|
86
|
+
<kbd data-key="Enter" class="wide">{t.enterKey}</kbd>
|
|
87
|
+
<kbd data-key="CapsLock" class="wide">{t.capsLockKey}</kbd>
|
|
88
|
+
<kbd data-key="KeyA">A</kbd>
|
|
89
|
+
<kbd data-key="KeyS">S</kbd>
|
|
90
|
+
<kbd data-key="KeyD">D</kbd>
|
|
91
|
+
<kbd data-key="KeyF">F</kbd>
|
|
92
|
+
<kbd data-key="KeyG">G</kbd>
|
|
93
|
+
<kbd data-key="KeyH">H</kbd>
|
|
94
|
+
<kbd data-key="KeyJ">J</kbd>
|
|
95
|
+
<kbd data-key="KeyK">K</kbd>
|
|
96
|
+
<kbd data-key="KeyL">L</kbd>
|
|
97
|
+
<kbd data-key="ShiftLeft" class="wide">{t.shiftKey}</kbd>
|
|
98
|
+
<kbd data-key="KeyZ">Z</kbd>
|
|
99
|
+
<kbd data-key="KeyX">X</kbd>
|
|
100
|
+
<kbd data-key="KeyC">C</kbd>
|
|
101
|
+
<kbd data-key="KeyV">V</kbd>
|
|
102
|
+
<kbd data-key="KeyB">B</kbd>
|
|
103
|
+
<kbd data-key="KeyN">N</kbd>
|
|
104
|
+
<kbd data-key="KeyM">M</kbd>
|
|
105
|
+
<kbd data-key="ShiftRight" class="wide">{t.shiftKey}</kbd>
|
|
106
|
+
<kbd data-key="ControlLeft">{t.controlKey}</kbd>
|
|
107
|
+
<kbd data-key="MetaLeft">{t.metaKey}</kbd>
|
|
108
|
+
<kbd data-key="AltLeft">{t.altKey}</kbd>
|
|
109
|
+
<kbd data-key="Space" class="space">{t.spaceKey}</kbd>
|
|
110
|
+
<kbd data-key="AltRight">{t.altKey}</kbd>
|
|
111
|
+
<kbd data-key="MetaRight">{t.metaKey}</kbd>
|
|
112
|
+
<kbd data-key="ControlRight">{t.controlKey}</kbd>
|
|
113
|
+
</div>
|
|
114
|
+
|
|
115
|
+
<div class="kct-metrics">
|
|
116
|
+
<article>
|
|
117
|
+
<span>{t.totalPresses}</span>
|
|
118
|
+
<strong data-total>0</strong>
|
|
119
|
+
</article>
|
|
120
|
+
<article>
|
|
121
|
+
<span>{t.chatterEvents}</span>
|
|
122
|
+
<strong data-chatter-count>0</strong>
|
|
123
|
+
</article>
|
|
124
|
+
<article>
|
|
125
|
+
<span>{t.worstDelta}</span>
|
|
126
|
+
<strong data-worst>--</strong>
|
|
127
|
+
</article>
|
|
128
|
+
<article>
|
|
129
|
+
<span>{t.watchWindow}</span>
|
|
130
|
+
<strong data-window>0</strong>
|
|
131
|
+
</article>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<div class="kct-log" role="table" aria-label={t.logAriaLabel}>
|
|
135
|
+
<div class="kct-row kct-head" role="row">
|
|
136
|
+
<span role="columnheader">{t.keyColumn}</span>
|
|
137
|
+
<span role="columnheader">{t.deltaColumn}</span>
|
|
138
|
+
<span role="columnheader">{t.verdictColumn}</span>
|
|
139
|
+
<span role="columnheader">{t.timeColumn}</span>
|
|
140
|
+
</div>
|
|
141
|
+
<div data-log></div>
|
|
142
|
+
</div>
|
|
143
|
+
|
|
144
|
+
<p class="kct-hint">{t.hint}</p>
|
|
145
|
+
</section>
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<link rel="stylesheet" href="./keyboard-chatter-test.css" />
|
|
149
|
+
|
|
150
|
+
<script>
|
|
151
|
+
type Severity = 'normal' | 'suspect' | 'chatter';
|
|
152
|
+
interface Config {
|
|
153
|
+
statusIdle: string;
|
|
154
|
+
statusListening: string;
|
|
155
|
+
statusChatter: string;
|
|
156
|
+
normal: string;
|
|
157
|
+
suspect: string;
|
|
158
|
+
chatter: string;
|
|
159
|
+
waiting: string;
|
|
160
|
+
spaceKey: string;
|
|
161
|
+
csvHeader: string;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
interface LogItem {
|
|
165
|
+
key: string;
|
|
166
|
+
code: string;
|
|
167
|
+
delta: number | null;
|
|
168
|
+
severity: Severity;
|
|
169
|
+
time: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const root = document.querySelector<HTMLElement>('.kct-root');
|
|
173
|
+
const panel = root?.querySelector<HTMLElement>('.kct-panel');
|
|
174
|
+
const config = JSON.parse(root?.dataset.config ?? '{}') as Config;
|
|
175
|
+
const maxRows = 80;
|
|
176
|
+
const lastDownByCode = new Map<string, number>();
|
|
177
|
+
const logItems: LogItem[] = [];
|
|
178
|
+
const state = {
|
|
179
|
+
total: 0,
|
|
180
|
+
chatter: 0,
|
|
181
|
+
worst: Number.POSITIVE_INFINITY,
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const els = {
|
|
185
|
+
status: root?.querySelector<HTMLElement>('[data-status]'),
|
|
186
|
+
liveKey: root?.querySelector<HTMLElement>('[data-live-key]'),
|
|
187
|
+
total: root?.querySelector<HTMLElement>('[data-total]'),
|
|
188
|
+
chatter: root?.querySelector<HTMLElement>('[data-chatter-count]'),
|
|
189
|
+
worst: root?.querySelector<HTMLElement>('[data-worst]'),
|
|
190
|
+
window: root?.querySelector<HTMLElement>('[data-window]'),
|
|
191
|
+
log: root?.querySelector<HTMLElement>('[data-log]'),
|
|
192
|
+
pulse: root?.querySelector<HTMLElement>('[data-pulse]'),
|
|
193
|
+
clear: root?.querySelector<HTMLButtonElement>('[data-clear]'),
|
|
194
|
+
exportButton: root?.querySelector<HTMLButtonElement>('[data-export]'),
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
function classify(delta: number | null): Severity {
|
|
198
|
+
if (delta === null) return 'normal';
|
|
199
|
+
if (delta < 30) return 'chatter';
|
|
200
|
+
if (delta <= 50) return 'suspect';
|
|
201
|
+
return 'normal';
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function formatDelta(delta: number | null) {
|
|
205
|
+
return delta === null ? '--' : `${Math.max(0, delta).toFixed(1)} ms`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getLabel(severity: Severity) {
|
|
209
|
+
if (severity === 'chatter') return config.chatter;
|
|
210
|
+
if (severity === 'suspect') return config.suspect;
|
|
211
|
+
return config.normal;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function isEditableTarget(target: EventTarget | null) {
|
|
215
|
+
return target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getKeyLabel(event: KeyboardEvent) {
|
|
219
|
+
if (event.key === ' ') return config.spaceKey;
|
|
220
|
+
return event.key;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function addLogItem(event: KeyboardEvent, delta: number | null, severity: Severity) {
|
|
224
|
+
logItems.unshift({
|
|
225
|
+
key: getKeyLabel(event),
|
|
226
|
+
code: event.code,
|
|
227
|
+
delta,
|
|
228
|
+
severity,
|
|
229
|
+
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }),
|
|
230
|
+
});
|
|
231
|
+
if (logItems.length > maxRows) logItems.length = maxRows;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function updateState(delta: number | null, severity: Severity) {
|
|
235
|
+
state.total += 1;
|
|
236
|
+
if (severity === 'chatter') state.chatter += 1;
|
|
237
|
+
if (delta !== null) state.worst = Math.min(state.worst, delta);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
241
|
+
if (event.repeat) return;
|
|
242
|
+
if (isEditableTarget(event.target)) return;
|
|
243
|
+
|
|
244
|
+
const now = event.timeStamp;
|
|
245
|
+
const previous = lastDownByCode.get(event.code);
|
|
246
|
+
const delta = previous === undefined ? null : now - previous;
|
|
247
|
+
const severity = classify(delta);
|
|
248
|
+
lastDownByCode.set(event.code, now);
|
|
249
|
+
updateState(delta, severity);
|
|
250
|
+
addLogItem(event, delta, severity);
|
|
251
|
+
lightKey(event.code, severity);
|
|
252
|
+
render(severity, event.code);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function handleKeyup(event: KeyboardEvent) {
|
|
256
|
+
root?.style.setProperty('--kct-release-x', `${Math.min(100, Math.max(0, event.key.length * 11))}%`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getStatusText(severity: Severity) {
|
|
260
|
+
if (severity === 'chatter') return config.statusChatter;
|
|
261
|
+
if (state.total > 0) return config.statusListening;
|
|
262
|
+
return config.statusIdle;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function renderMetrics() {
|
|
266
|
+
if (els.total) els.total.textContent = String(state.total);
|
|
267
|
+
if (els.chatter) els.chatter.textContent = String(state.chatter);
|
|
268
|
+
if (els.worst) els.worst.textContent = Number.isFinite(state.worst) ? formatDelta(state.worst) : '--';
|
|
269
|
+
if (els.window) els.window.textContent = String(logItems.length);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function restartPulse() {
|
|
273
|
+
if (els.pulse) {
|
|
274
|
+
els.pulse.style.animation = 'none';
|
|
275
|
+
window.requestAnimationFrame(() => {
|
|
276
|
+
if (els.pulse) els.pulse.style.animation = '';
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function render(severity: Severity = 'normal', activeKey = config.waiting) {
|
|
282
|
+
if (root) root.dataset.severity = severity;
|
|
283
|
+
if (els.status) els.status.textContent = getStatusText(severity);
|
|
284
|
+
if (els.liveKey) els.liveKey.textContent = activeKey;
|
|
285
|
+
renderMetrics();
|
|
286
|
+
restartPulse();
|
|
287
|
+
renderRows();
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function lightKey(code: string, severity: Severity) {
|
|
291
|
+
const key = root?.querySelector<HTMLElement>(`[data-key="${CSS.escape(code)}"]`);
|
|
292
|
+
if (!key) return;
|
|
293
|
+
key.classList.remove('is-normal', 'is-suspect', 'is-chatter', 'is-hit');
|
|
294
|
+
key.classList.add(`is-${severity}`);
|
|
295
|
+
window.requestAnimationFrame(() => key.classList.add('is-hit'));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function renderRows() {
|
|
299
|
+
if (!els.log) return;
|
|
300
|
+
const fragment = document.createDocumentFragment();
|
|
301
|
+
for (const item of logItems) {
|
|
302
|
+
const row = document.createElement('div');
|
|
303
|
+
row.className = `kct-row is-${item.severity}`;
|
|
304
|
+
row.setAttribute('role', 'row');
|
|
305
|
+
row.innerHTML = `
|
|
306
|
+
<span role="cell"><b>${escapeHtml(item.key)}</b><small>${escapeHtml(item.code)}</small></span>
|
|
307
|
+
<span role="cell">${formatDelta(item.delta)}</span>
|
|
308
|
+
<span role="cell"><i>${getLabel(item.severity)}</i></span>
|
|
309
|
+
<span role="cell">${item.time}</span>
|
|
310
|
+
`;
|
|
311
|
+
fragment.append(row);
|
|
312
|
+
}
|
|
313
|
+
els.log.replaceChildren(fragment);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function clearLog() {
|
|
317
|
+
lastDownByCode.clear();
|
|
318
|
+
logItems.length = 0;
|
|
319
|
+
state.total = 0;
|
|
320
|
+
state.chatter = 0;
|
|
321
|
+
state.worst = Number.POSITIVE_INFINITY;
|
|
322
|
+
render('normal', config.waiting);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function exportLog() {
|
|
326
|
+
const rows = [config.csvHeader];
|
|
327
|
+
for (const item of logItems.slice().reverse()) {
|
|
328
|
+
rows.push([item.key, item.code, item.delta?.toFixed(1) ?? '', item.severity, item.time].map(csvEscape).join(','));
|
|
329
|
+
}
|
|
330
|
+
const blob = new Blob([rows.join('\n')], { type: 'text/csv' });
|
|
331
|
+
const url = URL.createObjectURL(blob);
|
|
332
|
+
const link = document.createElement('a');
|
|
333
|
+
link.href = url;
|
|
334
|
+
link.download = 'keyboard-chatter-test.csv';
|
|
335
|
+
link.click();
|
|
336
|
+
URL.revokeObjectURL(url);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function csvEscape(value: string) {
|
|
340
|
+
return `"${value.replaceAll('"', '""')}"`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function escapeHtml(value: string) {
|
|
344
|
+
return value.replace(/[&<>"']/g, (char) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[char] ?? char));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
window.addEventListener('keydown', handleKeydown, { passive: true });
|
|
348
|
+
window.addEventListener('keyup', handleKeyup, { passive: true });
|
|
349
|
+
els.clear?.addEventListener('click', clearLog);
|
|
350
|
+
els.exportButton?.addEventListener('click', exportLog);
|
|
351
|
+
panel?.focus({ preventScroll: true });
|
|
352
|
+
render();
|
|
353
|
+
</script>
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { HardwareToolEntry, ToolLocaleContent } from '../../types';
|
|
2
|
+
import type { KeyboardChatterTestUI } from './ui';
|
|
3
|
+
|
|
4
|
+
export type KeyboardChatterTestLocaleContent = ToolLocaleContent<KeyboardChatterTestUI>;
|
|
5
|
+
|
|
6
|
+
export const keyboardChatterTest: HardwareToolEntry<KeyboardChatterTestUI> = {
|
|
7
|
+
id: 'keyboard-chatter-test',
|
|
8
|
+
icons: {
|
|
9
|
+
bg: 'mdi:keyboard-outline',
|
|
10
|
+
fg: 'mdi:alert-decagram',
|
|
11
|
+
},
|
|
12
|
+
i18n: {
|
|
13
|
+
de: () => import('./i18n/de').then((m) => m.content),
|
|
14
|
+
en: () => import('./i18n/en').then((m) => m.content),
|
|
15
|
+
es: () => import('./i18n/es').then((m) => m.content),
|
|
16
|
+
fr: () => import('./i18n/fr').then((m) => m.content),
|
|
17
|
+
id: () => import('./i18n/id').then((m) => m.content),
|
|
18
|
+
it: () => import('./i18n/it').then((m) => m.content),
|
|
19
|
+
ja: () => import('./i18n/ja').then((m) => m.content),
|
|
20
|
+
ko: () => import('./i18n/ko').then((m) => m.content),
|
|
21
|
+
nl: () => import('./i18n/nl').then((m) => m.content),
|
|
22
|
+
pl: () => import('./i18n/pl').then((m) => m.content),
|
|
23
|
+
pt: () => import('./i18n/pt').then((m) => m.content),
|
|
24
|
+
ru: () => import('./i18n/ru').then((m) => m.content),
|
|
25
|
+
sv: () => import('./i18n/sv').then((m) => m.content),
|
|
26
|
+
tr: () => import('./i18n/tr').then((m) => m.content),
|
|
27
|
+
zh: () => import('./i18n/zh').then((m) => m.content),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|