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:
2026-04-19 20:05:54 +01:00
parent 6605266587
commit d6bf9ed245
48 changed files with 1693 additions and 1156 deletions

View File

@@ -1,12 +1,15 @@
// src/lib/actions/bionicReading.js
function applyBionic(node) {
function applyBionic(node: HTMLElement) {
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT);
const textNodes = [];
let current;
while ((current = walker.nextNode())) textNodes.push(current);
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;
@@ -22,21 +25,21 @@ function applyBionic(node) {
fragment.appendChild(b);
fragment.appendChild(document.createTextNode(word.slice(boldLen)));
}
textNode.parentNode.replaceChild(fragment, textNode);
textNode.parentNode?.replaceChild(fragment, textNode);
}
}
function stripBionic(node) {
const bolds = node.querySelectorAll('b');
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);
const text = document.createTextNode(b.textContent ?? "");
b.parentNode?.replaceChild(text, b);
}
node.normalize(); // merge adjacent text nodes
}
export function bionicReading(node, enabled) {
let observer = null;
export function bionicReading(node: HTMLElement, enabled: boolean) {
let observer: MutationObserver | null = null;
function refresh() {
// Disconnect observer while we mutate, to avoid infinite loop
@@ -47,6 +50,7 @@ export function bionicReading(node, enabled) {
}
function observe() {
if (!observer) return;
observer.observe(node, { childList: true, characterData: true, subtree: true });
}
@@ -58,7 +62,7 @@ export function bionicReading(node, enabled) {
observe();
return {
update(newEnabled) {
update(newEnabled: boolean) {
enabled = newEnabled;
refresh();
},