feat(tasks): PR 1.2 — default to Today + WIP Now lane reads filtered set
Flips the Tasks landing bucket from All to Today so the cold-open answer to "what should I do now?" is scoped to today's commitments rather than the full backlog. Refactors WipTaskList to accept a tasks prop instead of reading the global store, and wires it into TasksPage so the Now lane and the bucket list below share one filtered set as their source of truth — the lane consumes the first three top-level rows and the bucket list excludes those IDs, so no task is ever rendered in both panels. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { tasks, addTask, completeTask, uncompleteTask, deleteTask, setTaskEnergy } from '$lib/stores/page.svelte.js';
|
||||
import { addTask, completeTask, uncompleteTask, deleteTask, setTaskEnergy } from '$lib/stores/page.svelte.js';
|
||||
import type { TaskEntry } from '$lib/types/app';
|
||||
import MicroSteps from '$lib/components/MicroSteps.svelte';
|
||||
import EnergyChip from '$lib/components/EnergyChip.svelte';
|
||||
import { ChevronDown, ChevronRight, Timer } from 'lucide-svelte';
|
||||
@@ -10,14 +11,26 @@
|
||||
}));
|
||||
}
|
||||
|
||||
let { wipLimit = 3 } = $props();
|
||||
// PR 1.2: the Now lane no longer reads the global tasks store. The
|
||||
// parent passes an already-filtered `tasks` array (respecting the
|
||||
// active bucket, list, search, and energy sort) so the Now lane and
|
||||
// the bucket list below share one filtered set as source of truth.
|
||||
// Subtasks are still excluded here — that is a Now-lane invariant,
|
||||
// not a filter the parent owns.
|
||||
let {
|
||||
tasks = [],
|
||||
wipLimit = 3,
|
||||
}: {
|
||||
tasks?: TaskEntry[];
|
||||
wipLimit?: number;
|
||||
} = $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 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);
|
||||
|
||||
@@ -19,7 +19,10 @@
|
||||
import { formatTimestamp } from "$lib/utils/time.js";
|
||||
import { BUCKET_COLORS, EFFORT_LABELS, EFFORT_ORDER } from "$lib/utils/constants.js";
|
||||
|
||||
let activeBucket = $state("all");
|
||||
// PR 1.2: default landing bucket is Today rather than All. The cold-open
|
||||
// surface area for "what should I do now?" is now scoped to today's
|
||||
// commitments, with All still reachable as a one-click filter.
|
||||
let activeBucket = $state("today");
|
||||
let activeListId = $state("all");
|
||||
let showCompleted = $state(false);
|
||||
let quickInput = $state("");
|
||||
@@ -94,6 +97,20 @@
|
||||
return list;
|
||||
});
|
||||
|
||||
// PR 1.2: the Now lane consumes the same filtered set as the bucket
|
||||
// list so its rows respect the active bucket / list / search / sort.
|
||||
// We compute the rendered slice here (top-level, capped at wipLimit)
|
||||
// and exclude those IDs from the bucket list — no row appears twice.
|
||||
// Computing in the parent (rather than via callback) avoids a
|
||||
// first-paint flash where both panels briefly render the same row
|
||||
// before an effect reconciles them.
|
||||
const NOW_LANE_LIMIT = 3;
|
||||
let nowLaneTasks = $derived(
|
||||
filteredTasks.filter((t) => !t.parentTaskId).slice(0, NOW_LANE_LIMIT),
|
||||
);
|
||||
let nowLaneIds = $derived(new Set(nowLaneTasks.map((t) => t.id)));
|
||||
let bucketListTasks = $derived(filteredTasks.filter((t) => !nowLaneIds.has(t.id)));
|
||||
|
||||
function cycleCurrentEnergy(next: EnergyLevel | null) {
|
||||
settings.currentEnergy = next;
|
||||
saveSettings();
|
||||
@@ -580,6 +597,16 @@
|
||||
|
||||
<!-- Task list -->
|
||||
<div class="flex-1 overflow-y-auto min-h-0">
|
||||
<!-- PR 1.2: Now lane sits above the bucket list and shares its
|
||||
filtered set. The bucket list excludes whatever IDs the lane
|
||||
rendered, so a task is never shown in both. The lane is only
|
||||
mounted when it has something to show — when the filter is
|
||||
empty, the EmptyState below carries onboarding copy alone. -->
|
||||
{#if nowLaneTasks.length > 0}
|
||||
<div class="mb-3">
|
||||
<WipTaskList tasks={nowLaneTasks} wipLimit={NOW_LANE_LIMIT} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if filteredTasks.length === 0}
|
||||
<EmptyState
|
||||
icon={SquareCheck}
|
||||
@@ -587,9 +614,9 @@
|
||||
? "No matching tasks"
|
||||
: "Add tasks manually. Automatic extraction from your transcripts is coming."}
|
||||
/>
|
||||
{:else}
|
||||
{:else if bucketListTasks.length > 0}
|
||||
<div class="flex flex-col gap-1">
|
||||
{#each filteredTasks as task (task.id)}
|
||||
{#each bucketListTasks as task (task.id)}
|
||||
<div class="group flex items-start gap-3 p-3 rounded-xl bg-bg-card border border-border-subtle hover:border-border"
|
||||
style="transition-duration: var(--duration-ui)">
|
||||
<!-- Checkbox -->
|
||||
|
||||
Reference in New Issue
Block a user