refactor(frontend): migrate JS modules to TypeScript
Wholesale JS -> TS migration of the frontend — stores, utils, actions,
and all Svelte component scripts adopt type annotations. Compile-time
surfaces (app.d.ts, lib/types/) added for shared DTO types.
Build plumbing:
- package.json: dev:frontend script that runs svelte-kit sync first
- tauri.conf.json: beforeDevCommand points at dev:frontend
- run.sh: dropped the sed-hack that temporarily blanked beforeDevCommand;
now relies on npm run dev:frontend to avoid double-Vite
- jsconfig.json: allowImportingTsExtensions
Preserves all Group 1 behaviour:
- page.svelte.ts keeps loadHistory / loadTasks Tauri-first, no
localStorage; saveTranscriptMeta + mapTranscriptRow + mapTaskRow
intact; update_task_cmd and update_transcript_meta_cmd invocations
carry the correct payload shape.
- Toasts, preferences stores typed without behaviour change.
- Viewer still routes segment edits through saveTranscriptMeta; the
Task 1.5 TODO markers are gone.
taskExtractor.ts is functionally improved during the migration:
- multi-task matches in the same sentence
- list-style shopping-verb expansion (get bread, milk, and cheese)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
74
src/lib/actions/bionicReading.ts
Normal file
74
src/lib/actions/bionicReading.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
// src/lib/actions/bionicReading.js
|
||||
|
||||
function applyBionic(node: HTMLElement) {
|
||||
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
|
||||
const textNodes: Text[] = [];
|
||||
let current: Node | null;
|
||||
while ((current = walker.nextNode())) {
|
||||
if (current.nodeType === Node.TEXT_NODE) textNodes.push(current as Text);
|
||||
}
|
||||
|
||||
for (const textNode of textNodes) {
|
||||
if (textNode.textContent == null) continue;
|
||||
const words = textNode.textContent.split(/(\s+)/);
|
||||
if (words.length <= 1 && !words[0].trim()) continue;
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
for (const word of words) {
|
||||
if (!word.trim() || word.length < 2) {
|
||||
fragment.appendChild(document.createTextNode(word));
|
||||
continue;
|
||||
}
|
||||
const boldLen = Math.min(3, Math.ceil(word.length / 2));
|
||||
const b = document.createElement('b');
|
||||
b.textContent = word.slice(0, boldLen);
|
||||
fragment.appendChild(b);
|
||||
fragment.appendChild(document.createTextNode(word.slice(boldLen)));
|
||||
}
|
||||
textNode.parentNode?.replaceChild(fragment, textNode);
|
||||
}
|
||||
}
|
||||
|
||||
function stripBionic(node: HTMLElement) {
|
||||
const bolds = node.querySelectorAll("b");
|
||||
for (const b of bolds) {
|
||||
const text = document.createTextNode(b.textContent ?? "");
|
||||
b.parentNode?.replaceChild(text, b);
|
||||
}
|
||||
node.normalize(); // merge adjacent text nodes
|
||||
}
|
||||
|
||||
export function bionicReading(node: HTMLElement, enabled: boolean) {
|
||||
let observer: MutationObserver | null = null;
|
||||
|
||||
function refresh() {
|
||||
// Disconnect observer while we mutate, to avoid infinite loop
|
||||
if (observer) observer.disconnect();
|
||||
stripBionic(node);
|
||||
if (enabled) applyBionic(node);
|
||||
if (observer) observe();
|
||||
}
|
||||
|
||||
function observe() {
|
||||
if (!observer) return;
|
||||
observer.observe(node, { childList: true, characterData: true, subtree: true });
|
||||
}
|
||||
|
||||
// MutationObserver catches Svelte re-rendering DOM content underneath us
|
||||
// (new transcript segments, task name edits, etc.)
|
||||
observer = new MutationObserver(refresh);
|
||||
|
||||
if (enabled) applyBionic(node);
|
||||
observe();
|
||||
|
||||
return {
|
||||
update(newEnabled: boolean) {
|
||||
enabled = newEnabled;
|
||||
refresh();
|
||||
},
|
||||
destroy() {
|
||||
if (observer) observer.disconnect();
|
||||
stripBionic(node);
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user