feat(ui): add MicroSteps component with decompose button, step checklist, and Just Start timer
This commit is contained in:
111
src/lib/components/MicroSteps.svelte
Normal file
111
src/lib/components/MicroSteps.svelte
Normal file
@@ -0,0 +1,111 @@
|
||||
<script>
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { ListTree, Check, Timer, Loader2 } from 'lucide-svelte';
|
||||
|
||||
let { parentTaskId, reduceMotion = false } = $props();
|
||||
|
||||
let subtasks = $state([]);
|
||||
let loading = $state(false);
|
||||
let error = $state('');
|
||||
let decomposing = $state(false);
|
||||
|
||||
async function loadSubtasks() {
|
||||
loading = true;
|
||||
error = '';
|
||||
try {
|
||||
subtasks = await invoke('list_subtasks_cmd', { parentTaskId });
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function decompose() {
|
||||
decomposing = true;
|
||||
error = '';
|
||||
try {
|
||||
subtasks = await invoke('decompose_and_store', { parentTaskId });
|
||||
} catch (e) {
|
||||
error = String(e);
|
||||
} finally {
|
||||
decomposing = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkStep(subtaskId) {
|
||||
try {
|
||||
await invoke('complete_subtask_cmd', { subtaskId });
|
||||
const idx = subtasks.findIndex(s => s.id === subtaskId);
|
||||
if (idx >= 0) subtasks[idx] = { ...subtasks[idx], done: true };
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function startTimer(subtaskId) {
|
||||
window.dispatchEvent(new CustomEvent('kon:start-timer', {
|
||||
detail: { taskId: subtaskId, seconds: 120 }
|
||||
}));
|
||||
}
|
||||
|
||||
$effect(() => {
|
||||
if (parentTaskId) loadSubtasks();
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="pl-6 mt-1 flex flex-col gap-1">
|
||||
{#if loading}
|
||||
<div class="flex items-center gap-2 text-[12px] text-text-tertiary py-1">
|
||||
<Loader2 size={12} class="animate-spin" aria-hidden="true" />
|
||||
Loading steps…
|
||||
</div>
|
||||
{:else if subtasks.length === 0 && !error}
|
||||
<button
|
||||
class="flex items-center gap-1.5 text-[12px] text-text-tertiary hover:text-accent py-1 disabled:opacity-50"
|
||||
onclick={decompose}
|
||||
disabled={decomposing}
|
||||
aria-label="Break down this task into micro-steps"
|
||||
>
|
||||
{#if decomposing}
|
||||
<Loader2 size={12} class="animate-spin" aria-hidden="true" />
|
||||
Breaking down…
|
||||
{:else}
|
||||
<ListTree size={12} aria-hidden="true" />
|
||||
Break down
|
||||
{/if}
|
||||
</button>
|
||||
{:else if error}
|
||||
<p class="text-[11px] text-text-tertiary py-1">{error}</p>
|
||||
{:else}
|
||||
{#each subtasks as step (step.id)}
|
||||
<div class="flex items-center gap-2 group py-0.5">
|
||||
<button
|
||||
class="w-3.5 h-3.5 rounded border flex items-center justify-center flex-shrink-0
|
||||
{step.done
|
||||
? 'bg-success/20 border-success text-success'
|
||||
: 'border-border-subtle hover:border-accent'}"
|
||||
onclick={() => checkStep(step.id)}
|
||||
disabled={step.done}
|
||||
aria-label={step.done ? 'Step complete' : 'Mark step complete'}
|
||||
>
|
||||
{#if step.done}
|
||||
<Check size={9} aria-hidden="true" />
|
||||
{/if}
|
||||
</button>
|
||||
<span class="text-[12px] flex-1 min-w-0 {step.done ? 'line-through text-text-tertiary' : 'text-text-secondary'} truncate">
|
||||
{step.text}
|
||||
</span>
|
||||
{#if !step.done}
|
||||
<button
|
||||
class="opacity-0 group-hover:opacity-100 flex items-center gap-1 text-[10px] text-text-tertiary hover:text-accent"
|
||||
onclick={() => startTimer(step.id)}
|
||||
aria-label="Start 2-minute timer for this step"
|
||||
style={reduceMotion ? '' : 'transition: opacity var(--duration-ui)'}
|
||||
>
|
||||
<Timer size={10} aria-hidden="true" />
|
||||
Just Start
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
Reference in New Issue
Block a user