Phase 7 of the rebrand cascade. Persisted UI state + inter-window event
channels migrated from magnotia to lumotia naming, with one-shot
localStorage key migration so dogfooded UI state survives the rename.
src/lib/utils/localStorageMigration.ts (new):
- migrateLocalStorageKey(old, new): idempotent + crash-safe shim.
- If new key exists, removes old (lumotia value is authoritative).
- If only old exists, copies value to new key, removes old.
- If neither, no-op.
- migrateLocalStorageKeys(pairs): batch wrapper.
src/lib/stores/page.svelte.ts:
- 4 key constants renamed to lumotia_settings / lumotia_profiles /
lumotia_task_lists / lumotia_templates.
- BroadcastChannel name renamed to lumotia_task_lists.
- migrateLocalStorageKeys() called at module load before any read.
src/lib/stores/focusTimer.svelte.ts:
- STORAGE_KEY renamed to lumotia.focusTimer.v1.
- migrateLocalStorageKey() called at module load.
Event channels (magnotia: -> lumotia:) renamed across frontend + Rust:
- magnotia:toggle-recording (src/routes/+layout.svelte)
- magnotia:hotkey-pressed / -released (src-tauri/src/commands/hotkey.rs +
consumers)
- magnotia:open-wind-down (src-tauri/src/tray.rs + consumer)
- magnotia:llm-download-progress (src-tauri/src/commands/llm.rs)
- magnotia:preferences-changed (src/lib/stores/preferences.svelte.ts +
consumers)
- magnotia:start-timer (nudgeBus + dispatch sites)
- magnotia:focus-timer-{complete,cancelled} (focusTimer + nudgeBus)
- magnotia:microstep-generated (nudgeBus + dispatch sites)
- magnotia:step-completed (nudgeBus + dispatch sites)
- magnotia:task-{completed,uncompleted,deleted} (page.svelte.ts +
nudgeBus + consumers)
Storage-event filters in src/routes/{float,viewer,preview}/+layout@.svelte
updated to filter on lumotia_settings.
User-facing toast strings still say "Magnotia" — deferred to Phase 8
(frontend strings).
npm run check: 0 errors / 0 warnings (3958 files).
cargo test --workspace: 339 pass / 0 fail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
154 lines
5.6 KiB
Svelte
154 lines
5.6 KiB
Svelte
<script lang="ts">
|
|
import { tasks, addTask, completeTask, uncompleteTask, deleteTask, setTaskEnergy } from '$lib/stores/page.svelte.js';
|
|
import MicroSteps from '$lib/components/MicroSteps.svelte';
|
|
import EnergyChip from '$lib/components/EnergyChip.svelte';
|
|
import { ChevronDown, ChevronRight, Timer } from 'lucide-svelte';
|
|
|
|
function startFocusTimer(task: { id: string; text: string }) {
|
|
window.dispatchEvent(new CustomEvent('lumotia:start-timer', {
|
|
detail: { taskId: task.id, seconds: 300, label: task.text }
|
|
}));
|
|
}
|
|
|
|
let { wipLimit = 3 } = $props();
|
|
|
|
let newTaskText = $state('');
|
|
let showOverflow = $state(false);
|
|
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));
|
|
let visibleTasks = $derived(activeTasks.slice(0, wipLimit));
|
|
let overflowTasks = $derived(activeTasks.slice(wipLimit));
|
|
let hasOverflow = $derived(overflowTasks.length > 0);
|
|
|
|
function handleAdd() {
|
|
const text = newTaskText.trim();
|
|
if (!text) return;
|
|
addTask({ text, bucket: 'inbox' });
|
|
newTaskText = '';
|
|
}
|
|
|
|
function handleKeydown(e: KeyboardEvent) {
|
|
if (e.key === 'Enter') handleAdd();
|
|
}
|
|
|
|
function toggleExpand(taskId: string) {
|
|
const next = new Set(expandedTaskIds);
|
|
if (next.has(taskId)) {
|
|
next.delete(taskId);
|
|
} else {
|
|
next.add(taskId);
|
|
}
|
|
expandedTaskIds = next;
|
|
}
|
|
</script>
|
|
|
|
<div class="flex flex-col gap-3">
|
|
<!-- Add task input -->
|
|
<div class="flex gap-2">
|
|
<input
|
|
type="text"
|
|
bind:value={newTaskText}
|
|
onkeydown={handleKeydown}
|
|
placeholder="Add a task…"
|
|
class="flex-1 px-3 py-2 rounded-lg bg-bg-input border border-border-subtle text-text text-[13px]
|
|
placeholder:text-text-tertiary focus:border-accent"
|
|
data-no-transition
|
|
/>
|
|
</div>
|
|
|
|
<!-- Active tasks (WIP-limited) -->
|
|
{#if visibleTasks.length === 0}
|
|
<p class="text-[12px] text-text-secondary text-center py-4">No active tasks</p>
|
|
{:else}
|
|
<div class="flex flex-col gap-1">
|
|
{#each visibleTasks as task (task.id)}
|
|
<div>
|
|
<!-- Task row -->
|
|
<div class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-hover group"
|
|
style="transition-duration: var(--duration-ui)">
|
|
<button
|
|
class="w-4 h-4 rounded border border-border-subtle flex items-center justify-center flex-shrink-0
|
|
hover:border-accent"
|
|
onclick={() => completeTask(task.id)}
|
|
aria-label="Complete task"
|
|
></button>
|
|
<span class="text-[13px] text-text flex-1 min-w-0 truncate">{task.text}</span>
|
|
<!-- Energy chip (Phase 3) — compact, reveals on hover when unset -->
|
|
<EnergyChip
|
|
energy={task.energy}
|
|
onSelect={(next) => setTaskEnergy(task.id, next)}
|
|
size="sm"
|
|
/>
|
|
<!-- 5-min focus timer — the "just-start" button from the brief -->
|
|
<button
|
|
class="opacity-0 group-hover:opacity-100 text-text-tertiary hover:text-accent"
|
|
onclick={() => startFocusTimer(task)}
|
|
aria-label="Start 5-minute focus timer for this task"
|
|
title="Start 5-minute focus timer"
|
|
style="transition: opacity var(--duration-ui)"
|
|
>
|
|
<Timer size={12} aria-hidden="true" />
|
|
</button>
|
|
<!-- Expand/collapse micro-steps toggle -->
|
|
<button
|
|
class="opacity-0 group-hover:opacity-100 text-text-tertiary hover:text-accent"
|
|
onclick={() => toggleExpand(task.id)}
|
|
aria-label={expandedTaskIds.has(task.id) ? 'Collapse steps' : 'Expand steps'}
|
|
style="transition: opacity var(--duration-ui)"
|
|
>
|
|
{#if expandedTaskIds.has(task.id)}
|
|
<ChevronDown size={12} aria-hidden="true" />
|
|
{:else}
|
|
<ChevronRight size={12} aria-hidden="true" />
|
|
{/if}
|
|
</button>
|
|
<button
|
|
class="text-text-secondary hover:text-danger opacity-0 group-hover:opacity-100 text-[12px]"
|
|
onclick={() => deleteTask(task.id)}
|
|
aria-label="Delete task"
|
|
>
|
|
×
|
|
</button>
|
|
</div>
|
|
<!-- Micro-steps panel (expanded) -->
|
|
{#if expandedTaskIds.has(task.id)}
|
|
<MicroSteps parentTaskId={task.id} parentTaskText={task.text} />
|
|
{/if}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Overflow -->
|
|
{#if hasOverflow}
|
|
<button
|
|
class="flex items-center gap-1.5 text-[12px] text-text-secondary hover:text-text-secondary px-3"
|
|
onclick={() => showOverflow = !showOverflow}
|
|
>
|
|
<ChevronDown size={12} class="transform {showOverflow ? 'rotate-180' : ''}" aria-hidden="true"
|
|
style="transition: transform var(--duration-ui)" />
|
|
{overflowTasks.length} more in your list
|
|
</button>
|
|
|
|
{#if showOverflow}
|
|
<div class="flex flex-col gap-1 opacity-60">
|
|
{#each overflowTasks as task (task.id)}
|
|
<div class="flex items-center gap-2 px-3 py-1.5 rounded-lg">
|
|
<div class="w-3 h-3 rounded border border-border-subtle flex-shrink-0"></div>
|
|
<span class="text-[12px] text-text-secondary truncate">{task.text}</span>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
<!-- WIP indicator -->
|
|
{#if activeTasks.length > 0}
|
|
<p class="text-[12px] text-text-secondary text-right">
|
|
{Math.min(activeTasks.length, wipLimit)}/{wipLimit} active
|
|
</p>
|
|
{/if}
|
|
</div>
|