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:
@@ -1,4 +1,6 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { tick } from "svelte";
|
||||
import type { TaskBucket, TaskList } from "$lib/types/app";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import {
|
||||
tasks, addTask, completeTask, uncompleteTask, deleteTask, updateTask,
|
||||
@@ -21,17 +23,29 @@
|
||||
let newListName = $state("");
|
||||
let sortMode = $state("date");
|
||||
let showSortMenu = $state(false);
|
||||
let contextMenuListId = $state(null);
|
||||
let editingListId = $state(null);
|
||||
let contextMenuListId = $state<string | null>(null);
|
||||
let editingListId = $state<string | null>(null);
|
||||
let editingName = $state("");
|
||||
let editingInputEl = $state<HTMLInputElement | null>(null);
|
||||
let newListInputEl = $state<HTMLInputElement | null>(null);
|
||||
|
||||
const buckets = [
|
||||
const buckets: Array<{ id: "all" | TaskBucket; label: string }> = [
|
||||
{ id: "all", label: "All" },
|
||||
{ id: "inbox", label: "Inbox" },
|
||||
{ id: "today", label: "Today" },
|
||||
{ id: "soon", label: "Soon" },
|
||||
{ id: "later", label: "Later" },
|
||||
];
|
||||
const taskBucketOptions: TaskBucket[] = ["today", "soon", "later"];
|
||||
const effortOptions = ["quick", "medium", "deep"];
|
||||
|
||||
function effortRank(effort: string): number {
|
||||
return EFFORT_ORDER[effort as keyof typeof EFFORT_ORDER] || 4;
|
||||
}
|
||||
|
||||
function effortLabel(effort: string): string {
|
||||
return EFFORT_LABELS[effort as keyof typeof EFFORT_LABELS] || effort;
|
||||
}
|
||||
|
||||
let filteredTasks = $derived.by(() => {
|
||||
let list = tasks.filter((t) => !t.done);
|
||||
@@ -50,9 +64,9 @@
|
||||
list = list.filter((t) => t.text.toLowerCase().includes(q));
|
||||
}
|
||||
if (sortMode === "quick-first") {
|
||||
list.sort((a, b) => (EFFORT_ORDER[a.effort] || 4) - (EFFORT_ORDER[b.effort] || 4));
|
||||
list.sort((a, b) => effortRank(a.effort) - effortRank(b.effort));
|
||||
} else if (sortMode === "deep-first") {
|
||||
list.sort((a, b) => (EFFORT_ORDER[b.effort] || 4) - (EFFORT_ORDER[a.effort] || 4));
|
||||
list.sort((a, b) => effortRank(b.effort) - effortRank(a.effort));
|
||||
}
|
||||
return list;
|
||||
});
|
||||
@@ -77,7 +91,7 @@
|
||||
});
|
||||
|
||||
let bucketCounts = $derived.by(() => {
|
||||
const counts = { all: 0, inbox: 0, today: 0, soon: 0, later: 0 };
|
||||
const counts: Record<"all" | TaskBucket, number> = { all: 0, inbox: 0, today: 0, soon: 0, later: 0 };
|
||||
for (const t of tasks) {
|
||||
if (t.done) continue;
|
||||
counts.all++;
|
||||
@@ -86,7 +100,7 @@
|
||||
return counts;
|
||||
});
|
||||
|
||||
function countForList(listId) {
|
||||
function countForList(listId: string) {
|
||||
if (listId === "all") return tasks.filter((t) => !t.done).length;
|
||||
if (listId === "inbox") return tasks.filter((t) => !t.done && !t.listId).length;
|
||||
return tasks.filter((t) => !t.done && t.listId === listId).length;
|
||||
@@ -96,27 +110,27 @@
|
||||
activeListId === "all" ? "All Lists" : (taskLists.find((l) => l.id === activeListId)?.name || "Tasks")
|
||||
);
|
||||
|
||||
function handleQuickAdd(e) {
|
||||
function handleQuickAdd(e: KeyboardEvent) {
|
||||
if (e.key === "Enter" && quickInput.trim()) {
|
||||
const listId = (activeListId === "all" || activeListId === "inbox") ? null : activeListId;
|
||||
addTask({
|
||||
text: quickInput.trim(),
|
||||
bucket: activeBucket === "all" ? "inbox" : activeBucket,
|
||||
bucket: (activeBucket === "all" ? "inbox" : activeBucket) as TaskBucket,
|
||||
listId,
|
||||
});
|
||||
quickInput = "";
|
||||
}
|
||||
}
|
||||
|
||||
function setBucket(taskId, bucket) {
|
||||
function setBucket(taskId: string, bucket: TaskBucket) {
|
||||
updateTask(taskId, { bucket });
|
||||
}
|
||||
|
||||
function setEffort(taskId, effort) {
|
||||
function setEffort(taskId: string, effort: string) {
|
||||
updateTask(taskId, { effort: effort === tasks.find((t) => t.id === taskId)?.effort ? "" : effort });
|
||||
}
|
||||
|
||||
function getListName(listId) {
|
||||
function getListName(listId: string | null) {
|
||||
if (!listId) return null;
|
||||
return taskLists.find((l) => l.id === listId)?.name || null;
|
||||
}
|
||||
@@ -129,7 +143,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleCreateList(e) {
|
||||
function handleCreateList(e: KeyboardEvent) {
|
||||
if (e.key === "Enter" && newListName.trim()) {
|
||||
addTaskList(newListName.trim());
|
||||
newListName = "";
|
||||
@@ -138,10 +152,13 @@
|
||||
if (e.key === "Escape") { showNewList = false; newListName = ""; }
|
||||
}
|
||||
|
||||
function startRenaming(list) {
|
||||
async function startRenaming(list: TaskList) {
|
||||
editingListId = list.id;
|
||||
editingName = list.name;
|
||||
contextMenuListId = null;
|
||||
await tick();
|
||||
editingInputEl?.focus();
|
||||
editingInputEl?.select();
|
||||
}
|
||||
|
||||
function finishRenaming() {
|
||||
@@ -152,17 +169,26 @@
|
||||
editingName = "";
|
||||
}
|
||||
|
||||
function handleDeleteList(id) {
|
||||
function handleDeleteList(id: string) {
|
||||
if (activeListId === id) activeListId = "all";
|
||||
deleteTaskList(id);
|
||||
contextMenuListId = null;
|
||||
}
|
||||
|
||||
function toggleContextMenu(e, listId) {
|
||||
function toggleContextMenu(e: MouseEvent, listId: string) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
contextMenuListId = contextMenuListId === listId ? null : listId;
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (showNewList) {
|
||||
tick().then(() => {
|
||||
newListInputEl?.focus();
|
||||
newListInputEl?.select();
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full animate-fade-in">
|
||||
@@ -297,13 +323,13 @@
|
||||
{#if editingListId === list.id && !sidebarCollapsed}
|
||||
<div class="px-1.5 py-0.5">
|
||||
<input
|
||||
bind:this={editingInputEl}
|
||||
type="text"
|
||||
class="w-full bg-bg-input border border-accent rounded px-2 py-1 text-[10px] text-text focus:outline-none"
|
||||
bind:value={editingName}
|
||||
onkeydown={(e) => { if (e.key === "Enter") finishRenaming(); if (e.key === "Escape") { editingListId = null; } }}
|
||||
onblur={finishRenaming}
|
||||
data-no-transition
|
||||
autofocus
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
@@ -356,6 +382,7 @@
|
||||
<div class="px-2 py-2 border-t border-border-subtle">
|
||||
{#if showNewList}
|
||||
<input
|
||||
bind:this={newListInputEl}
|
||||
type="text"
|
||||
class="w-full bg-bg-input border border-border rounded px-2 py-1 text-[10px] text-text
|
||||
placeholder:text-text-tertiary focus:outline-none focus:border-accent"
|
||||
@@ -364,7 +391,6 @@
|
||||
onkeydown={handleCreateList}
|
||||
onblur={() => { showNewList = false; newListName = ""; }}
|
||||
data-no-transition
|
||||
autofocus
|
||||
/>
|
||||
{:else}
|
||||
<button
|
||||
@@ -403,7 +429,7 @@
|
||||
<p class="text-[13px] text-text leading-relaxed">{task.text}</p>
|
||||
<div class="flex items-center gap-2 mt-1.5">
|
||||
<!-- Bucket pills -->
|
||||
{#each ["today", "soon", "later"] as b}
|
||||
{#each taskBucketOptions as b}
|
||||
<button
|
||||
class="text-[10px] px-2 py-0.5 rounded-full border
|
||||
{task.bucket === b
|
||||
@@ -415,7 +441,7 @@
|
||||
{/each}
|
||||
<span class="text-border-subtle">·</span>
|
||||
<!-- Effort pills -->
|
||||
{#each ["quick", "medium", "deep"] as e}
|
||||
{#each effortOptions as e}
|
||||
<button
|
||||
class="text-[10px] px-2 py-0.5 rounded-full border
|
||||
{task.effort === e
|
||||
@@ -423,7 +449,7 @@
|
||||
: 'text-text-tertiary border-transparent hover:border-border-subtle hover:text-text-secondary'}"
|
||||
style="transition-duration: var(--duration-ui)"
|
||||
onclick={() => setEffort(task.id, e)}
|
||||
>{EFFORT_LABELS[e]}</button>
|
||||
>{effortLabel(e)}</button>
|
||||
{/each}
|
||||
<!-- Timestamp -->
|
||||
{#if task.createdAt}
|
||||
@@ -457,7 +483,7 @@
|
||||
class="flex items-center gap-2 text-[12px] text-text-tertiary hover:text-text-secondary mb-2"
|
||||
onclick={() => showCompleted = !showCompleted}
|
||||
>
|
||||
<ChevronRight size={12} class="{showCompleted ? 'rotate-90' : ''}" aria-hidden="true"
|
||||
<ChevronRight size={12} class={showCompleted ? "rotate-90" : ""} aria-hidden="true"
|
||||
style="transition: transform var(--duration-ui)" />
|
||||
Completed ({completedTasks.length})
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user