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,5 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import type { FontFamily, ReduceMotion } from "$lib/types/app";
|
||||
import { getPreferences, updateAccessibility } from '$lib/stores/preferences.svelte.js';
|
||||
import Toggle from '$lib/components/Toggle.svelte';
|
||||
import SegmentedButton from '$lib/components/SegmentedButton.svelte';
|
||||
@@ -9,7 +10,7 @@
|
||||
{ id: 'lexend', label: 'Lexend' },
|
||||
{ id: 'atkinson', label: 'Atkinson' },
|
||||
{ id: 'opendyslexic', label: 'OpenDyslexic' },
|
||||
];
|
||||
] satisfies { id: FontFamily; label: string }[];
|
||||
|
||||
let bionicChecked = $state(prefs.accessibility.bionicReading || false);
|
||||
let motionValue = $state(
|
||||
@@ -25,11 +26,15 @@
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
const mapped = motionValue === 'On' ? 'on' : motionValue === 'Off' ? 'off' : 'system';
|
||||
const mapped: ReduceMotion = motionValue === 'On' ? 'on' : motionValue === 'Off' ? 'off' : 'system';
|
||||
if (mapped !== prefs.accessibility.reduceMotion) {
|
||||
updateAccessibility({ reduceMotion: mapped });
|
||||
}
|
||||
});
|
||||
|
||||
function rangeValue(event: Event): number {
|
||||
return Number((event.currentTarget as HTMLInputElement).value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-5">
|
||||
@@ -59,7 +64,7 @@
|
||||
<span class="text-[11px] text-text-tertiary">{prefs.accessibility.fontSize}px</span>
|
||||
</div>
|
||||
<input type="range" min="16" max="24" step="1" value={prefs.accessibility.fontSize}
|
||||
oninput={(e) => updateAccessibility({ fontSize: Number(e.target.value) })}
|
||||
oninput={(e) => updateAccessibility({ fontSize: rangeValue(e) })}
|
||||
class="w-full accent-accent" />
|
||||
</div>
|
||||
|
||||
@@ -70,7 +75,7 @@
|
||||
<span class="text-[11px] text-text-tertiary">{prefs.accessibility.letterSpacing.toFixed(2)}em</span>
|
||||
</div>
|
||||
<input type="range" min="0" max="0.15" step="0.01" value={prefs.accessibility.letterSpacing}
|
||||
oninput={(e) => updateAccessibility({ letterSpacing: Number(e.target.value) })}
|
||||
oninput={(e) => updateAccessibility({ letterSpacing: rangeValue(e) })}
|
||||
class="w-full accent-accent" />
|
||||
</div>
|
||||
|
||||
@@ -81,7 +86,7 @@
|
||||
<span class="text-[11px] text-text-tertiary">{prefs.accessibility.lineHeight.toFixed(1)}</span>
|
||||
</div>
|
||||
<input type="range" min="1.3" max="2.2" step="0.1" value={prefs.accessibility.lineHeight}
|
||||
oninput={(e) => updateAccessibility({ lineHeight: Number(e.target.value) })}
|
||||
oninput={(e) => updateAccessibility({ lineHeight: rangeValue(e) })}
|
||||
class="w-full accent-accent" />
|
||||
</div>
|
||||
|
||||
@@ -92,7 +97,7 @@
|
||||
<span class="text-[11px] text-text-tertiary">{prefs.accessibility.transcriptSize}px</span>
|
||||
</div>
|
||||
<input type="range" min="16" max="24" step="1" value={prefs.accessibility.transcriptSize}
|
||||
oninput={(e) => updateAccessibility({ transcriptSize: Number(e.target.value) })}
|
||||
oninput={(e) => updateAccessibility({ transcriptSize: rangeValue(e) })}
|
||||
class="w-full accent-accent" />
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { settings, saveSettings } from "$lib/stores/page.svelte.js";
|
||||
|
||||
let recording = $state(false);
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
const modifierKeys = new Set(["Control", "Shift", "Alt", "Meta"]);
|
||||
|
||||
function handleKeyDown(e) {
|
||||
function handleKeyDown(e: KeyboardEvent) {
|
||||
// Capture-phase listener; guard still belts-and-braces.
|
||||
if (!recording) return;
|
||||
e.preventDefault();
|
||||
@@ -20,7 +20,7 @@
|
||||
// Wait for a non-modifier key
|
||||
if (modifierKeys.has(e.key)) return;
|
||||
|
||||
const parts = [];
|
||||
const parts: string[] = [];
|
||||
if (e.ctrlKey) parts.push("Ctrl");
|
||||
if (e.shiftKey) parts.push("Shift");
|
||||
if (e.altKey) parts.push("Alt");
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { ListTree, Check, Timer, Loader2 } from 'lucide-svelte';
|
||||
|
||||
let { parentTaskId, reduceMotion = false } = $props();
|
||||
|
||||
let subtasks = $state([]);
|
||||
interface Subtask {
|
||||
id: string;
|
||||
text: string;
|
||||
done: boolean;
|
||||
}
|
||||
|
||||
let subtasks = $state<Subtask[]>([]);
|
||||
let loading = $state(false);
|
||||
let error = $state('');
|
||||
let decomposing = $state(false);
|
||||
@@ -13,7 +19,7 @@
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
subtasks = await invoke('list_subtasks_cmd', { parentTaskId });
|
||||
subtasks = await invoke<Subtask[]>('list_subtasks_cmd', { parentTaskId });
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
} finally {
|
||||
@@ -25,7 +31,7 @@
|
||||
decomposing = true;
|
||||
error = '';
|
||||
try {
|
||||
subtasks = await invoke('decompose_and_store', { parentTaskId });
|
||||
subtasks = await invoke<Subtask[]>('decompose_and_store', { parentTaskId });
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
} finally {
|
||||
@@ -33,7 +39,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function checkStep(subtaskId) {
|
||||
async function checkStep(subtaskId: string) {
|
||||
try {
|
||||
await invoke('complete_subtask_cmd', { subtaskId });
|
||||
const idx = subtasks.findIndex(s => s.id === subtaskId);
|
||||
@@ -41,7 +47,7 @@
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function startTimer(subtaskId) {
|
||||
function startTimer(subtaskId: string) {
|
||||
window.dispatchEvent(new CustomEvent('kon:start-timer', {
|
||||
detail: { taskId: subtaskId, seconds: 120 }
|
||||
}));
|
||||
|
||||
@@ -1,31 +1,38 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { errorMessage } from "$lib/utils/errors.js";
|
||||
import Card from "$lib/components/Card.svelte";
|
||||
|
||||
let { modelSize = "base", onComplete = () => {} } = $props();
|
||||
let { modelSize = "base", onComplete = () => {} } = $props<{
|
||||
modelSize?: "tiny" | "base" | "small" | "medium";
|
||||
onComplete?: () => void;
|
||||
}>();
|
||||
|
||||
let progress = $state(0);
|
||||
let downloading = $state(false);
|
||||
let downloaded = $state(0);
|
||||
let total = $state(0);
|
||||
let error = $state("");
|
||||
let unlisten = null;
|
||||
let unlisten: null | (() => void) = null;
|
||||
|
||||
const modelInfo = {
|
||||
tiny: { size: "~75 MB", accuracy: "Basic" },
|
||||
base: { size: "~142 MB", accuracy: "Good" },
|
||||
small: { size: "~466 MB", accuracy: "Better" },
|
||||
medium: { size: "~1.5 GB", accuracy: "Best" },
|
||||
};
|
||||
} satisfies Record<string, { size: string; accuracy: string }>;
|
||||
let currentModelInfo = $derived(
|
||||
modelInfo[String(modelSize) as keyof typeof modelInfo]
|
||||
);
|
||||
|
||||
onMount(async () => {
|
||||
unlisten = await listen("model-download-progress", (event) => {
|
||||
const data = event.payload;
|
||||
progress = data.progress;
|
||||
downloaded = data.downloaded;
|
||||
total = data.total;
|
||||
const data = event.payload as { progress?: number; downloaded?: number; total?: number };
|
||||
progress = data.progress ?? 0;
|
||||
downloaded = data.downloaded ?? 0;
|
||||
total = data.total ?? 0;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,12 +48,12 @@
|
||||
await invoke("download_model", { size: modelSize });
|
||||
onComplete();
|
||||
} catch (err) {
|
||||
error = typeof err === "string" ? err : err.message || "Download failed";
|
||||
error = errorMessage(err) || "Download failed";
|
||||
downloading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
function formatBytes(bytes: number) {
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1048576) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
if (bytes < 1073741824) return `${(bytes / 1048576).toFixed(1)} MB`;
|
||||
@@ -68,7 +75,7 @@
|
||||
Kon needs the <span class="font-medium text-text">{modelSize}</span> model to transcribe speech.
|
||||
</p>
|
||||
<p class="text-[11px] text-text-tertiary mb-6">
|
||||
{modelInfo[modelSize]?.size ?? "?"} · {modelInfo[modelSize]?.accuracy ?? "?"} accuracy · 100% offline
|
||||
{currentModelInfo?.size ?? "?"} · {currentModelInfo?.accuracy ?? "?"} accuracy · 100% offline
|
||||
</p>
|
||||
|
||||
{#if downloading}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
// Invisible resize-handle overlays around each Kon window edge.
|
||||
// Needed because Kon runs with decorations off, so the WM
|
||||
// provides no resize affordance on KDE/GNOME Wayland.
|
||||
@@ -24,7 +24,10 @@
|
||||
|
||||
const enabled = hasTauriRuntime();
|
||||
|
||||
async function startResize(e, direction) {
|
||||
async function startResize(
|
||||
e: PointerEvent,
|
||||
direction: "North" | "South" | "West" | "East" | "NorthWest" | "NorthEast" | "SouthWest" | "SouthEast",
|
||||
) {
|
||||
if (!enabled) return;
|
||||
if (e.button !== 0) return;
|
||||
e.preventDefault();
|
||||
@@ -35,7 +38,7 @@
|
||||
// diagonal component and collapsing a corner drag to a single axis.
|
||||
// pointerdown + setPointerCapture keeps the direction pinned.
|
||||
try {
|
||||
e.currentTarget?.setPointerCapture?.(e.pointerId);
|
||||
(e.currentTarget as HTMLElement | null)?.setPointerCapture?.(e.pointerId);
|
||||
} catch {}
|
||||
try {
|
||||
await getCurrentWindow().startResizeDragging(direction);
|
||||
@@ -44,13 +47,21 @@
|
||||
</script>
|
||||
|
||||
{#if enabled}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="kon-rh kon-rh-n" onpointerdown={(e) => startResize(e,"North")}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="kon-rh kon-rh-s" onpointerdown={(e) => startResize(e,"South")}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="kon-rh kon-rh-w" onpointerdown={(e) => startResize(e,"West")}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="kon-rh kon-rh-e" onpointerdown={(e) => startResize(e,"East")}></div>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="kon-rh kon-rh-nw" onpointerdown={(e) => startResize(e,"NorthWest")}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="kon-rh kon-rh-ne" onpointerdown={(e) => startResize(e,"NorthEast")}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="kon-rh kon-rh-sw" onpointerdown={(e) => startResize(e,"SouthWest")}></div>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="kon-rh kon-rh-se" onpointerdown={(e) => startResize(e,"SouthEast")}></div>
|
||||
{/if}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { page, tasks, addTask, completeTask, uncompleteTask } from "$lib/stores/page.svelte.js";
|
||||
import { BUCKET_DOT_COLORS, SIDEBAR_MAX_TASKS } from "$lib/utils/constants.js";
|
||||
|
||||
let quickInput = $state("");
|
||||
let recentlyAdded = $state(new Set());
|
||||
let recentlyAdded = $state(new Set<string>());
|
||||
|
||||
let activeTasks = $derived(tasks.filter((t) => !t.done).slice(0, SIDEBAR_MAX_TASKS));
|
||||
let taskCount = $derived(tasks.filter((t) => !t.done).length);
|
||||
|
||||
function handleQuickAdd(e) {
|
||||
function handleQuickAdd(e: KeyboardEvent) {
|
||||
if (e.key === "Enter" && quickInput.trim()) {
|
||||
addTask({ text: quickInput.trim(), bucket: "inbox" });
|
||||
quickInput = "";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { settings } from "$lib/stores/page.svelte.js";
|
||||
|
||||
@@ -19,16 +19,16 @@
|
||||
await getCurrentWindow().hide();
|
||||
}
|
||||
|
||||
function handleDragStart(e) {
|
||||
function handleDragStart(e: PointerEvent) {
|
||||
if (e.button !== 0) return;
|
||||
if (e.target.closest("button")) return;
|
||||
try { e.currentTarget?.setPointerCapture?.(e.pointerId); } catch {}
|
||||
if ((e.target as HTMLElement | null)?.closest("button")) return;
|
||||
try { (e.currentTarget as HTMLElement | null)?.setPointerCapture?.(e.pointerId); } catch {}
|
||||
getCurrentWindow().startDragging();
|
||||
}
|
||||
|
||||
// Track maximise state
|
||||
$effect(() => {
|
||||
let unlisten;
|
||||
let unlisten: (() => void) | undefined;
|
||||
getCurrentWindow().onResized(async () => {
|
||||
maximised = await getCurrentWindow().isMaximized();
|
||||
}).then((fn) => unlisten = fn);
|
||||
@@ -36,6 +36,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="flex items-center select-none bg-sidebar border-b border-border-subtle
|
||||
{compact ? 'h-[28px]' : 'h-[32px]'}"
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import type { ToastSeverity } from "$lib/types/app";
|
||||
// Renders the toast stack in the bottom-right of the viewport. Mount once
|
||||
// at the app root (in +layout.svelte). Reads from the global toasts store.
|
||||
//
|
||||
@@ -10,7 +11,7 @@
|
||||
import { toasts } from '$lib/stores/toasts.svelte.js';
|
||||
import { X } from 'lucide-svelte';
|
||||
|
||||
function severityClass(severity) {
|
||||
function severityClass(severity: ToastSeverity) {
|
||||
switch (severity) {
|
||||
case 'success': return 'toast-success';
|
||||
case 'info': return 'toast-info';
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<script>
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
<script lang="ts">
|
||||
type SpinnerType = "dots" | "circle" | "pulse" | "stars" | "orbit";
|
||||
|
||||
let { type = "dots", speed = 100, classes = "" } = $props();
|
||||
let { type = "dots", speed = 100, classes = "" } = $props<{
|
||||
type?: SpinnerType;
|
||||
speed?: number;
|
||||
classes?: string;
|
||||
}>();
|
||||
|
||||
const sequences = {
|
||||
dots: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
||||
@@ -12,17 +16,14 @@
|
||||
};
|
||||
|
||||
let frame = $state(0);
|
||||
let interval = null;
|
||||
let chars = sequences[type] || sequences.dots;
|
||||
let chars = $derived(sequences[type as SpinnerType] ?? sequences.dots);
|
||||
|
||||
onMount(() => {
|
||||
interval = setInterval(() => {
|
||||
$effect(() => {
|
||||
frame = 0;
|
||||
const interval = setInterval(() => {
|
||||
frame = (frame + 1) % chars.length;
|
||||
}, speed);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (interval) clearInterval(interval);
|
||||
return () => clearInterval(interval);
|
||||
});
|
||||
</script>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import type { ViewerSegment } from "$lib/types/app";
|
||||
import { measureTextHeight } from "$lib/utils/textMeasure.js";
|
||||
import { buildCumulativeOffsets, findVisibleRange } from "$lib/utils/virtualList.js";
|
||||
import { getPreferences } from "$lib/stores/preferences.svelte.js";
|
||||
@@ -8,23 +9,39 @@
|
||||
let {
|
||||
segments = [],
|
||||
activeSegmentIdx = -1,
|
||||
matchingSegments = new Set(),
|
||||
matchingSegments = new Set<number>(),
|
||||
showTimestamps = true,
|
||||
editingIdx = -1,
|
||||
editingText = $bindable(""),
|
||||
searchQuery = "",
|
||||
formatTime = (t) => String(t),
|
||||
onSeekToTime = () => {},
|
||||
onStartEditing = () => {},
|
||||
formatTime = (t: number) => String(t),
|
||||
onSeekToTime = (_time: number) => {},
|
||||
onStartEditing = (_idx: number) => {},
|
||||
onFinishEditing = () => {},
|
||||
onToggleStar = () => {},
|
||||
onCopySegment = () => {},
|
||||
onDeleteSegment = () => {},
|
||||
highlightText = (t) => t,
|
||||
} = $props();
|
||||
onToggleStar = (_idx: number) => {},
|
||||
onCopySegment = (_idx: number) => {},
|
||||
onDeleteSegment = (_idx: number) => {},
|
||||
highlightText = (t: string) => t,
|
||||
} = $props<{
|
||||
segments?: ViewerSegment[];
|
||||
activeSegmentIdx?: number;
|
||||
matchingSegments?: Set<number>;
|
||||
showTimestamps?: boolean;
|
||||
editingIdx?: number;
|
||||
editingText?: string;
|
||||
searchQuery?: string;
|
||||
formatTime?: (time: number) => string;
|
||||
onSeekToTime?: (time: number) => void;
|
||||
onStartEditing?: (idx: number) => void;
|
||||
onFinishEditing?: () => void;
|
||||
onToggleStar?: (idx: number) => void;
|
||||
onCopySegment?: (idx: number) => void;
|
||||
onDeleteSegment?: (idx: number) => void;
|
||||
highlightText?: (text: string) => string;
|
||||
}>();
|
||||
|
||||
// Internal state
|
||||
let containerEl = $state(null);
|
||||
let containerEl = $state<HTMLDivElement | null>(null);
|
||||
let containerHeight = $state(0);
|
||||
let containerWidth = $state(0);
|
||||
let scrollTop = $state(0);
|
||||
@@ -46,7 +63,7 @@
|
||||
const textWidth = containerWidth - 26 - timestampWidth - 24;
|
||||
if (textWidth <= 0) return segments.map(() => 40);
|
||||
|
||||
return segments.map(seg => {
|
||||
return segments.map((seg: ViewerSegment) => {
|
||||
const text = seg.text?.trim() || "";
|
||||
if (!text) return SEGMENT_PADDING + textLineHeight;
|
||||
const { height } = measureTextHeight(text, textFont, textWidth, textLineHeight);
|
||||
@@ -75,7 +92,7 @@
|
||||
|
||||
// Visible segment slice with positioning
|
||||
let visibleItems = $derived.by(() => {
|
||||
const items = [];
|
||||
const items: Array<{ seg: ViewerSegment; idx: number; top: number; height: number }> = [];
|
||||
for (let i = visibleRange.start; i < visibleRange.end; i++) {
|
||||
items.push({
|
||||
seg: segments[i],
|
||||
@@ -88,7 +105,7 @@
|
||||
});
|
||||
|
||||
// Scroll to a specific segment index (used for active segment tracking + search)
|
||||
export function scrollToSegment(idx) {
|
||||
export function scrollToSegment(idx: number) {
|
||||
if (!containerEl || idx < 0 || idx >= segments.length) return;
|
||||
const segTop = cumulativeOffsets[idx];
|
||||
const segHeight = segmentHeights[idx];
|
||||
@@ -96,8 +113,8 @@
|
||||
containerEl.scrollTop = segTop - viewCenter + segHeight / 2;
|
||||
}
|
||||
|
||||
function onScroll(e) {
|
||||
scrollTop = e.target.scrollTop;
|
||||
function onScroll(e: Event) {
|
||||
scrollTop = (e.currentTarget as HTMLDivElement).scrollTop;
|
||||
// Track selection state for expanded buffer
|
||||
hasSelection = !document.getSelection()?.isCollapsed;
|
||||
}
|
||||
@@ -105,7 +122,7 @@
|
||||
// ResizeObserver for container dimensions
|
||||
$effect(() => {
|
||||
if (!containerEl) return;
|
||||
const ro = new ResizeObserver(entries => {
|
||||
const ro = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
containerHeight = entry.contentRect.height;
|
||||
containerWidth = entry.contentRect.width;
|
||||
@@ -119,7 +136,7 @@
|
||||
$effect(() => {
|
||||
if (activeSegmentIdx >= 0) {
|
||||
// Find the index in our segments array that matches
|
||||
const arrayIdx = segments.findIndex(s => s._idx === activeSegmentIdx);
|
||||
const arrayIdx = segments.findIndex((s: ViewerSegment) => s._idx === activeSegmentIdx);
|
||||
if (arrayIdx >= 0) {
|
||||
const segTop = cumulativeOffsets[arrayIdx];
|
||||
const segBottom = segTop + segmentHeights[arrayIdx];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { tasks, addTask, completeTask, uncompleteTask, deleteTask } from '$lib/stores/page.svelte.js';
|
||||
import MicroSteps from '$lib/components/MicroSteps.svelte';
|
||||
import { ChevronDown, ChevronRight } from 'lucide-svelte';
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
let newTaskText = $state('');
|
||||
let showOverflow = $state(false);
|
||||
let expandedTaskIds = $state(new Set());
|
||||
let expandedTaskIds = $state(new Set<string>());
|
||||
|
||||
// Exclude subtasks from WIP count — only top-level tasks count
|
||||
let activeTasks = $derived(tasks.filter(t => !t.done && !t.parentTaskId));
|
||||
@@ -22,11 +22,11 @@
|
||||
newTaskText = '';
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
function handleKeydown(e: KeyboardEvent) {
|
||||
if (e.key === 'Enter') handleAdd();
|
||||
}
|
||||
|
||||
function toggleExpand(taskId) {
|
||||
function toggleExpand(taskId: string) {
|
||||
const next = new Set(expandedTaskIds);
|
||||
if (next.has(taskId)) {
|
||||
next.delete(taskId);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<script>
|
||||
<script lang="ts">
|
||||
import { getPreferences, updatePreferences } from '$lib/stores/preferences.svelte.js';
|
||||
|
||||
const prefs = getPreferences();
|
||||
@@ -8,9 +8,9 @@
|
||||
{ id: 'cave', label: 'Cave', description: 'Cool, focused', swatch: 'bg-[#141a1e]' },
|
||||
{ id: 'energy', label: 'Energy', description: 'Warm, active', swatch: 'bg-[#1c1815]' },
|
||||
{ id: 'reset', label: 'Reset', description: 'Natural, calm', swatch: 'bg-[#161a16]' },
|
||||
];
|
||||
] as const;
|
||||
|
||||
function selectZone(id) {
|
||||
function selectZone(id: string) {
|
||||
updatePreferences({ zone: id });
|
||||
}
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user