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