fix: keyboard layout map, picker type tabs as filters, add unit tests

1. Fix RU_TO_EN keyboard map in keyboardLayout.ts:
   - ч was mapped to 'c' (wrong), now correctly 'x'
   - с was mapped to 'v' (wrong), now correctly 'c'
   - ю was mapped to '.' (wrong), now correctly ','
   - Full rewrite with standard QWERTY/ЙЦУКЕН positional mapping
   - Examples: dthcnfr→верстак, руддщ→hello, цщкдв→world all correct now

2. Root cause of search breaking after 3-4 chars:
   Old map had ч:'c', с:'v' swapped. So dthcnfr → 'верчиак' (wrong).
   Each character was mapped to wrong Cyrillic equivalent.

3. Add unit tests: keyboardLayout.test.js (39 tests, node-runner):
   - EN→RU: dthcnfr, ghbdtn, ntcn
   - RU→EN: руддщ, цщкдв, ышеш
   - Unicode safety: Latin c (U+0063) ≠ Cyrillic с (U+0441)
   - expandKeyboardVariants for mixed inputs
   - Edge cases: empty, single char, mixed case, numbers

4. InternalLinkPicker type tabs → filters (not search modes):
   - Store rawResults (all) + filtered results by activeType
   - Switching type tab no longer clears query or triggers new search
   - Just filters existing rawResults by selected type
   - Shows 'Нет результатов для этого типа' when filtered empty

5. Both GlobalSearch and InternalLinkPicker use same expandKeyboardVariants()

All tests PASS, full build OK.
This commit is contained in:
2026-06-15 11:37:35 +08:00
parent 700e4dae5b
commit 077d25a269
3 changed files with 170 additions and 23 deletions
@@ -17,7 +17,8 @@
let activeType = 'note';
let query = '';
let results = [];
let rawResults = []; // all search results (unfiltered)
let results = []; // filtered by activeType
let selectedIndex = 0;
let loading = false;
let error = '';
@@ -31,8 +32,15 @@
return 'case';
}
// Filter rawResults by activeType into displayed results
function applyTypeFilter() {
results = rawResults.filter(n => nodeTypeToFilter(n.type) === activeType);
selectedIndex = 0;
}
async function search() {
if (!query.trim() || query.trim().length < 2) {
rawResults = [];
results = [];
return;
}
@@ -41,7 +49,6 @@
try {
// Expand query with keyboard layout variants for tolerant search
const variants = expandKeyboardVariants(query.trim());
// Deduplicate: skip variants identical to the original query's lowercase
const seen = new Set();
const queries = [];
for (const v of variants) {
@@ -63,11 +70,11 @@
}
}
}
// Filter by active type
results = Array.from(merged.values()).filter(n => nodeTypeToFilter(n.type) === activeType);
selectedIndex = 0;
rawResults = Array.from(merged.values());
applyTypeFilter();
} catch (e) {
error = String(e);
rawResults = [];
results = [];
} finally {
loading = false;
@@ -104,9 +111,7 @@
function handleTypeChange(typeId) {
activeType = typeId;
query = '';
results = [];
error = '';
applyTypeFilter();
if (inputRef) inputRef.focus();
}
@@ -159,8 +164,10 @@
<div class="picker-loading">Загрузка...</div>
{:else if error}
<div class="picker-error">{error}</div>
{:else if query.trim().length >= 2 && results.length === 0}
<div class="picker-empty">Ничего не найдено</div>
{:else if query.trim().length >= 2 && results.length === 0 && !loading}
<div class="picker-empty">
{rawResults.length > 0 ? 'Нет результатов для этого типа' : 'Ничего не найдено'}
</div>
{:else}
{#each results as item, i}
<button