feat(web): rewrite home page as single-screen CLI-passthrough orchestrator

Replace multi-page task management with single-screen layout: Header
with report picker at top, scrollable TaskList in the middle, and
InputBar with property pills fixed at the bottom. Owns state for
active report, task loading, input parsing, and task completion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-14 17:35:07 +01:00
parent a6cd0ea41d
commit 5e829320cf
+61 -146
View File
@@ -2,175 +2,90 @@
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { authStore } from '$lib/stores/auth.js';
import { tasksStore, pendingTasks, completedTasks } from '$lib/stores/tasks.js';
import { tasksStore } from '$lib/stores/tasks.js';
import Header from '$lib/components/Header.svelte';
import TaskList from '$lib/components/TaskList.svelte';
import Button from '$lib/components/ui/Button.svelte';
import InputBar from '$lib/components/InputBar.svelte';
let activeReport = 'list';
/** @type {import('$lib/api/types.js').Task[]} */
let tasks = [];
let loading = true;
let showCompleted = false;
let inputError = '';
$: displayTasks = showCompleted ? $completedTasks : $pendingTasks;
// Subscribe to store
const unsubscribe = tasksStore.subscribe(value => {
tasks = value;
});
onMount(async () => {
// Redirect to login if not authenticated
onMount(() => {
if (!$authStore.isAuthenticated) {
goto('/auth/login');
return;
}
// Load tasks
try {
await tasksStore.load({ status: showCompleted ? 'C' : 'P' });
} catch (error) {
console.error('Failed to load tasks:', error);
} finally {
loading = false;
}
loadReport(activeReport);
return unsubscribe;
});
/**
* Toggle task completion
* @param {string} uuid
* @param {string} reportName
*/
async function handleToggle(uuid) {
try {
await tasksStore.complete(uuid);
// Reload tasks
await tasksStore.load({ status: showCompleted ? 'C' : 'P' });
} catch (error) {
console.error('Failed to toggle task:', error);
}
}
/**
* Navigate to task detail
* @param {string} uuid
*/
function handleTaskClick(uuid) {
goto(`/tasks/${uuid}`);
}
/**
* Toggle between pending and completed view
*/
async function toggleView() {
showCompleted = !showCompleted;
async function loadReport(reportName) {
loading = true;
inputError = '';
try {
await tasksStore.load({ status: showCompleted ? 'C' : 'P' });
await tasksStore.loadReport(reportName);
} catch (error) {
console.error('Failed to load tasks:', error);
console.error('Failed to load report:', error);
} finally {
loading = false;
}
}
/**
* @param {string} reportName
*/
function handleReportChange(reportName) {
activeReport = reportName;
loadReport(reportName);
}
/**
* @param {string} input
*/
async function handleSubmit(input) {
inputError = '';
try {
await tasksStore.parseAndCreate(input);
} catch (error) {
inputError = error instanceof Error ? error.message : 'Failed to create task';
}
}
/**
* @param {string} uuid
*/
async function handleComplete(uuid) {
try {
await tasksStore.complete(uuid);
} catch (error) {
console.error('Failed to complete task:', error);
}
}
</script>
<div class="page">
<div class="container">
<div class="header">
<h1>Tasks</h1>
<a href="/tasks/new" class="new-btn">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
</svg>
New
</a>
</div>
<Header {activeReport} onReportChange={handleReportChange} />
<div class="filter-bar">
<button
class="filter-btn"
class:active={!showCompleted}
on:click={() => !showCompleted || toggleView()}
>
Pending ({$pendingTasks.length})
</button>
<button
class="filter-btn"
class:active={showCompleted}
on:click={() => showCompleted || toggleView()}
>
Completed ({$completedTasks.length})
</button>
</div>
<TaskList
tasks={displayTasks}
<TaskList
{tasks}
{loading}
onToggle={handleToggle}
onTaskClick={handleTaskClick}
emptyMessage={showCompleted ? 'No completed tasks' : 'No pending tasks. Add one to get started!'}
/>
</div>
</div>
{activeReport}
onComplete={handleComplete}
/>
<style>
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: var(--spacing-lg);
}
.header h1 {
margin-bottom: 0;
}
.icon {
width: 1rem;
height: 1rem;
}
.new-btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.75rem;
background-color: var(--color-primary);
color: white;
border-radius: var(--border-radius);
font-size: var(--font-size-sm);
font-weight: 500;
text-decoration: none;
transition: background-color 0.2s;
}
.new-btn:hover {
background-color: var(--color-primary-dark);
text-decoration: none;
}
.filter-bar {
display: flex;
gap: var(--spacing-sm);
margin-bottom: var(--spacing-md);
background-color: var(--bg-primary);
padding: var(--spacing-sm);
border-radius: var(--border-radius);
box-shadow: var(--shadow-sm);
}
.filter-btn {
flex: 1;
padding: 0.5rem 1rem;
background-color: transparent;
border: none;
border-radius: var(--border-radius);
font-size: var(--font-size-sm);
font-weight: 500;
color: var(--text-secondary);
cursor: pointer;
transition: all 0.2s;
font-family: inherit;
}
.filter-btn.active {
background-color: var(--color-primary);
color: white;
}
.filter-btn:hover:not(.active) {
background-color: var(--bg-tertiary);
}
</style>
<InputBar
onSubmit={handleSubmit}
error={inputError}
/>