Add sliding task detail view with custom date/time picker

Replace inline task editing with a full-viewport detail panel that
slides in from the right. Includes editable title, description, custom
calendar-based date/time picker (bottom sheet), kebab menu with delete,
and mark complete/restore button. Simplify TaskItem to remove inline
editing and kebab menu, add chevron hint and onopen callback. Use list
icon for drawer toggle instead of back arrow.
This commit is contained in:
Tristan Michael 2026-03-30 07:54:35 -07:00
parent 0c8c2a0272
commit c6895c12ae
3 changed files with 416 additions and 123 deletions

View file

@ -0,0 +1,186 @@
<script lang="ts">
let { value = null, onchange, onclose }: {
value: string | null;
onchange: (iso: string | null) => void;
onclose: () => void;
} = $props();
const DAY_NAMES = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
let now = new Date();
let existing = value ? new Date(value) : null;
let viewYear = $state(existing ? existing.getFullYear() : now.getFullYear());
let viewMonth = $state(existing ? existing.getMonth() : now.getMonth());
let selectedDay = $state(existing ? existing.getDate() : now.getDate());
let includeTime = $state(existing ? (existing.getHours() !== 0 || existing.getMinutes() !== 0) : false);
let selectedHour = $state(existing ? existing.getHours() : now.getHours());
let selectedMinute = $state(existing ? existing.getMinutes() : 0);
let visible = $state(false);
let todayStr = `${now.getFullYear()}-${now.getMonth()}-${now.getDate()}`;
let daysInMonth = $derived(new Date(viewYear, viewMonth + 1, 0).getDate());
let firstDayOfWeek = $derived(new Date(viewYear, viewMonth, 1).getDay());
let monthLabel = $derived(new Date(viewYear, viewMonth).toLocaleDateString(undefined, { month: "long", year: "numeric" }));
let calendarCells = $derived.by(() => {
const cells: (number | null)[] = [];
for (let i = 0; i < firstDayOfWeek; i++) cells.push(null);
for (let d = 1; d <= daysInMonth; d++) cells.push(d);
return cells;
});
requestAnimationFrame(() => { visible = true; });
function dismiss() {
visible = false;
setTimeout(onclose, 200);
}
function prevMonth() {
if (viewMonth === 0) { viewMonth = 11; viewYear--; }
else viewMonth--;
}
function nextMonth() {
if (viewMonth === 11) { viewMonth = 0; viewYear++; }
else viewMonth++;
}
function selectDay(day: number) {
selectedDay = day;
}
function isToday(day: number): boolean {
return `${viewYear}-${viewMonth}-${day}` === todayStr;
}
function isSelected(day: number): boolean {
return selectedDay === day && (!value || (() => {
const v = new Date(value);
return v.getFullYear() === viewYear && v.getMonth() === viewMonth;
})());
}
function done() {
const h = includeTime ? selectedHour : 0;
const m = includeTime ? selectedMinute : 0;
const iso = new Date(viewYear, viewMonth, selectedDay, h, m).toISOString();
onchange(iso);
dismiss();
}
function clear() {
onchange(null);
dismiss();
}
</script>
<!-- Wrapper: backdrop click dismisses, sheet click stops propagation -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="absolute inset-0 z-40 transition-opacity duration-200 {visible ? 'opacity-100' : 'opacity-0'}"
style="background: rgba(0,0,0,0.4)"
onclick={dismiss}
onkeydown={(e) => { if (e.key === "Escape") dismiss(); }}
>
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="absolute bottom-0 left-0 right-0 rounded-t-2xl bg-surface-light shadow-xl transition-transform duration-200 ease-out dark:bg-card-dark {visible ? 'translate-y-0' : 'translate-y-full'}"
onclick={(e) => e.stopPropagation()}
>
<!-- Header -->
<div class="flex items-center justify-between px-4 pt-3 pb-2">
<span class="text-sm font-semibold">Date & Time</span>
<button onclick={done} class="text-sm font-medium text-primary hover:opacity-70">Done</button>
</div>
<!-- Month navigation -->
<div class="flex items-center justify-between px-4 py-2">
<span class="text-sm font-medium">{monthLabel}</span>
<div class="flex gap-1">
<button onclick={prevMonth} class="rounded p-1 hover:bg-black/5 dark:hover:bg-white/10">
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" />
</svg>
</button>
<button onclick={nextMonth} class="rounded p-1 hover:bg-black/5 dark:hover:bg-white/10">
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" />
</svg>
</button>
</div>
</div>
<!-- Day names -->
<div class="grid grid-cols-7 px-4">
{#each DAY_NAMES as name}
<div class="py-1 text-center text-xs font-medium opacity-40">{name}</div>
{/each}
</div>
<!-- Calendar grid -->
<div class="grid grid-cols-7 px-4 pb-2">
{#each calendarCells as day}
{#if day === null}
<div></div>
{:else}
<button
onclick={() => selectDay(day)}
class="mx-auto flex h-8 w-8 items-center justify-center rounded-full text-sm transition-colors
{selectedDay === day ? 'bg-primary text-white' : ''}
{isToday(day) && selectedDay !== day ? 'font-bold text-primary' : ''}
{selectedDay !== day && !isToday(day) ? 'hover:bg-black/5 dark:hover:bg-white/10' : ''}"
>
{day}
</button>
{/if}
{/each}
</div>
<!-- Time section -->
<div class="flex items-center gap-3 border-t border-border-light px-4 py-3 dark:border-border-dark">
{#if includeTime}
<span class="text-sm opacity-50">Time</span>
<div class="flex items-center gap-1">
<select
bind:value={selectedHour}
class="appearance-none rounded-lg border border-border-light bg-surface-light px-2 py-1 text-sm text-text-light outline-none dark:border-border-dark dark:bg-surface-dark dark:text-text-dark"
>
{#each Array(24) as _, h}
<option value={h}>{String(h).padStart(2, "0")}</option>
{/each}
</select>
<span class="text-sm opacity-40">:</span>
<select
bind:value={selectedMinute}
class="appearance-none rounded-lg border border-border-light bg-surface-light px-2 py-1 text-sm text-text-light outline-none dark:border-border-dark dark:bg-surface-dark dark:text-text-dark"
>
{#each [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55] as m}
<option value={m}>{String(m).padStart(2, "0")}</option>
{/each}
</select>
</div>
<button onclick={() => (includeTime = false)} class="ml-auto opacity-40 hover:opacity-80">
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
</button>
{:else}
<button
onclick={() => (includeTime = true)}
class="text-sm opacity-50 hover:opacity-80"
>
Set time
</button>
{/if}
</div>
<!-- Clear button -->
{#if value}
<div class="border-t border-border-light px-4 py-3 dark:border-border-dark">
<button onclick={clear} class="text-sm text-danger hover:opacity-70">Clear date</button>
</div>
{/if}
</div>
</div>

View file

@ -0,0 +1,195 @@
<script lang="ts">
import type { Task } from "../types";
import { app } from "../stores/app.svelte";
import DateTimePicker from "./DateTimePicker.svelte";
import { getCurrentWindow } from "@tauri-apps/api/window";
import { platform } from "@tauri-apps/plugin-os";
const appWindow = getCurrentWindow();
const currentPlatform = platform();
const isDesktop = currentPlatform === "linux" || currentPlatform === "windows";
let { task, onback }: { task: Task; onback: () => void } = $props();
let title = $state(task.title);
let description = $state(task.description);
let showMenu = $state(false);
let menuEl = $state<HTMLDivElement | null>(null);
let showDatePicker = $state(false);
let saveTimer: ReturnType<typeof setTimeout>;
function handleHeaderMouseDown(e: MouseEvent) {
if (e.button !== 0) return;
if ((e.target as HTMLElement).closest("button")) return;
if (isDesktop) appWindow.startDragging();
}
function debouncedSave(fields: Partial<Task>) {
clearTimeout(saveTimer);
saveTimer = setTimeout(() => {
app.updateTask({ ...task, ...fields, updated_at: new Date().toISOString() });
}, 400);
}
function handleTitleInput() {
debouncedSave({ title: title.trim() || task.title });
}
function handleDescInput() {
debouncedSave({ description });
}
function handleDateChange(iso: string | null) {
app.updateTask({ ...task, due_date: iso, updated_at: new Date().toISOString() });
}
async function handleToggle() {
await app.toggleTask(task.id);
onback();
}
async function handleDelete() {
showMenu = false;
await app.deleteTask(task.id);
onback();
}
function handleMenuClickOutside(e: MouseEvent) {
if (showMenu && menuEl && !menuEl.contains(e.target as Node)) {
showMenu = false;
}
}
$effect(() => {
if (showMenu) {
window.addEventListener("mousedown", handleMenuClickOutside);
return () => window.removeEventListener("mousedown", handleMenuClickOutside);
}
});
let isCompleted = $derived(task.status === "completed");
function formatDateChip(iso: string): string {
const d = new Date(iso);
const today = new Date();
const dayNames = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const day = dayNames[d.getDay()];
const pad = (n: number) => String(n).padStart(2, "0");
const hasTime = d.getHours() !== 0 || d.getMinutes() !== 0;
const timePart = hasTime ? `, ${pad(d.getHours())}:${pad(d.getMinutes())}` : "";
if (d.toDateString() === today.toDateString()) return `Today${timePart}`;
return `${day}, ${pad(d.getDate())}/${pad(d.getMonth() + 1)}${timePart}`;
}
</script>
<!-- Header -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<header
onmousedown={handleHeaderMouseDown}
class="flex items-center justify-between border-b border-border-light px-4 py-3 dark:border-border-dark"
>
<button
onclick={onback}
class="rounded-lg p-1.5 hover:bg-black/5 dark:hover:bg-white/10"
>
<svg class="h-5 w-5 opacity-60" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.79 5.23a.75.75 0 01-.02 1.06L8.832 10l3.938 3.71a.75.75 0 11-1.04 1.08l-4.5-4.25a.75.75 0 010-1.08l4.5-4.25a.75.75 0 011.06.02z" />
</svg>
</button>
<!-- Kebab menu -->
<div class="relative" bind:this={menuEl}>
<button
onclick={() => (showMenu = !showMenu)}
class="rounded-lg p-1.5 opacity-50 hover:bg-black/5 hover:opacity-80 dark:hover:bg-white/10"
>
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" />
</svg>
</button>
{#if showMenu}
<div class="absolute right-0 top-full z-40 mt-1 min-w-[140px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark">
<button
onclick={handleDelete}
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10"
>
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
</svg>
Delete
</button>
</div>
{/if}
</div>
</header>
<!-- Content -->
<main class="flex-1 overflow-y-auto px-4 pt-4">
<!-- Title -->
<input
type="text"
bind:value={title}
oninput={handleTitleInput}
placeholder="Task title"
class="w-full bg-transparent text-xl font-bold outline-none placeholder:opacity-30"
/>
<!-- Description -->
<div class="mt-4 flex items-start gap-3">
<svg class="mt-0.5 h-5 w-5 shrink-0 opacity-40" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4 4a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zm0 4a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zm0 4a1 1 0 011-1h7a1 1 0 110 2H5a1 1 0 01-1-1z" />
</svg>
<textarea
bind:value={description}
oninput={handleDescInput}
placeholder="Add details"
rows="3"
class="w-full flex-1 resize-none bg-transparent text-sm outline-none placeholder:opacity-40"
></textarea>
</div>
<!-- Date/time -->
<div class="mt-4 flex items-center gap-3">
<svg class="h-5 w-5 shrink-0 opacity-40" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd" />
</svg>
{#if task.due_date}
<div class="flex items-center gap-1.5 rounded-full border border-border-light bg-black/5 px-3 py-1 text-sm dark:border-border-dark dark:bg-white/10">
<button onclick={() => (showDatePicker = true)} class="hover:opacity-70">
{formatDateChip(task.due_date)}
</button>
<button onclick={() => handleDateChange(null)} class="opacity-40 hover:opacity-80">
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
</button>
</div>
{:else}
<button
onclick={() => (showDatePicker = true)}
class="text-sm opacity-40 hover:opacity-70"
>
Add date/time
</button>
{/if}
</div>
</main>
<!-- Bottom action -->
<div class="border-t border-border-light px-4 py-3 dark:border-border-dark">
<button
onclick={handleToggle}
class="w-full text-center text-sm font-medium text-primary hover:opacity-70"
>
{isCompleted ? "Restore task" : "Mark as completed"}
</button>
</div>
<!-- Date picker overlay -->
{#if showDatePicker}
<DateTimePicker
value={task.due_date}
onchange={handleDateChange}
onclose={() => (showDatePicker = false)}
/>
{/if}

View file

@ -1,5 +1,4 @@
<script lang="ts" module> <script lang="ts" module>
let editingTaskId = $state<string | null>(null);
export const animateInIds = new Set<string>(); export const animateInIds = new Set<string>();
</script> </script>
@ -7,26 +6,18 @@
import type { Task } from "../types"; import type { Task } from "../types";
import { app } from "../stores/app.svelte"; import { app } from "../stores/app.svelte";
let { task }: { task: Task } = $props(); let { task, onopen }: { task: Task; onopen?: (task: Task) => void } = $props();
let editTitle = $state(task.title);
let editDesc = $state(task.description);
let editing = $derived(editingTaskId === task.id);
let touchStartX = $state(0); let touchStartX = $state(0);
let swipeX = $state(0); let swipeX = $state(0);
let swiping = $state(false); let swiping = $state(false);
let containerEl = $state<HTMLDivElement | null>(null);
let titleInputEl = $state<HTMLInputElement | null>(null);
let showMenu = $state(false);
let menuEl = $state<HTMLDivElement | null>(null);
let transitioning = $state(false); let transitioning = $state(false);
let animatingIn = $state(false); let animatingIn = $state(false);
let isCompleted = $derived(task.status === "completed"); let isCompleted = $derived(task.status === "completed");
$effect(() => { $effect(() => {
// Check on status change whether this task should animate in const _ = task.status;
const _ = task.status; // track reactively
if (animateInIds.has(task.id)) { if (animateInIds.has(task.id)) {
animateInIds.delete(task.id); animateInIds.delete(task.id);
animatingIn = true; animatingIn = true;
@ -38,50 +29,14 @@
} }
}); });
async function handleToggle() { async function handleToggle(e: MouseEvent) {
e.stopPropagation();
transitioning = true; transitioning = true;
animateInIds.add(task.id); animateInIds.add(task.id);
await new Promise((r) => setTimeout(r, 200)); await new Promise((r) => setTimeout(r, 200));
await app.toggleTask(task.id); await app.toggleTask(task.id);
} }
function handleMenuClickOutside(e: MouseEvent) {
if (showMenu && menuEl && !menuEl.contains(e.target as Node)) {
showMenu = false;
}
}
$effect(() => {
if (showMenu) {
window.addEventListener("mousedown", handleMenuClickOutside);
return () => window.removeEventListener("mousedown", handleMenuClickOutside);
}
});
function startEditing() {
if (editing) return;
editingTaskId = task.id;
editTitle = task.title;
editDesc = task.description;
setTimeout(() => titleInputEl?.focus(), 220);
}
async function save() {
if (editingTaskId !== task.id) return;
editingTaskId = null;
const trimmed = editTitle.trim();
if (!trimmed) { editTitle = task.title; return; }
if (trimmed === task.title && editDesc === task.description) return;
await app.updateTask({ ...task, title: trimmed, description: editDesc });
}
function handleFocusOut(e: FocusEvent) {
if (containerEl?.contains(e.relatedTarget as Node)) return;
requestAnimationFrame(() => {
if (editingTaskId === task.id) save();
});
}
function handleTouchStart(e: TouchEvent) { function handleTouchStart(e: TouchEvent) {
touchStartX = e.touches[0].clientX; touchStartX = e.touches[0].clientX;
swiping = true; swiping = true;
@ -98,7 +53,9 @@
if (Math.abs(swipeX) > 100) { if (Math.abs(swipeX) > 100) {
swipeX = 0; swipeX = 0;
swiping = false; swiping = false;
handleToggle(); transitioning = true;
animateInIds.add(task.id);
setTimeout(() => app.toggleTask(task.id), 200);
return; return;
} }
swipeX = 0; swipeX = 0;
@ -120,9 +77,9 @@
class="grid transition-[grid-template-rows,opacity] duration-300 ease-out {animatingIn || transitioning ? 'grid-rows-[0fr] opacity-0' : 'grid-rows-[1fr] opacity-100'}" class="grid transition-[grid-template-rows,opacity] duration-300 ease-out {animatingIn || transitioning ? 'grid-rows-[0fr] opacity-0' : 'grid-rows-[1fr] opacity-100'}"
> >
<div class="overflow-hidden"> <div class="overflow-hidden">
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div <div
bind:this={containerEl} class="relative"
class="relative {showMenu ? 'z-40' : ''}"
ontouchstart={handleTouchStart} ontouchstart={handleTouchStart}
ontouchmove={handleTouchMove} ontouchmove={handleTouchMove}
ontouchend={handleTouchEnd} ontouchend={handleTouchEnd}
@ -139,15 +96,15 @@
{/if} {/if}
<!-- Task content --> <!-- Task content -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <button
<div class="group flex w-full items-start gap-3 bg-surface-light px-4 py-3 text-left hover:bg-black/5 dark:bg-surface-dark dark:hover:bg-white/5"
class="group relative flex items-start gap-3 bg-surface-light px-4 py-3 hover:bg-black/5 dark:bg-surface-dark dark:hover:bg-white/5"
style="transform: translateX({swipeX}px); transition: {swiping ? 'none' : 'transform 0.2s ease-out'}" style="transform: translateX({swipeX}px); transition: {swiping ? 'none' : 'transform 0.2s ease-out'}"
onmousedown={startEditing} onclick={() => onopen?.(task)}
> >
<!-- Checkbox --> <!-- Checkbox -->
<button <!-- svelte-ignore a11y_no_static_element_interactions -->
onmousedown={(e) => { e.stopPropagation(); handleToggle(); }} <div
onclick={handleToggle}
class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 transition-colors {isCompleted class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border-2 transition-colors {isCompleted
? 'border-primary bg-primary' ? 'border-primary bg-primary'
: 'border-gray-400 dark:border-gray-500'}" : 'border-gray-400 dark:border-gray-500'}"
@ -160,73 +117,28 @@
/> />
</svg> </svg>
{/if} {/if}
</button>
<!-- Content -->
<div class="min-w-0 flex-1" onfocusout={handleFocusOut}>
{#if editing}
<input
type="text"
bind:this={titleInputEl}
bind:value={editTitle}
class="w-full bg-transparent text-sm font-medium outline-none"
onkeydown={(e) => { if (e.key === "Enter") (e.target as HTMLElement).blur(); if (e.key === "Escape") { editTitle = task.title; editDesc = task.description; editingTaskId = null; } }}
/>
{:else}
<p class="text-sm {isCompleted ? 'line-through opacity-50' : 'font-medium'}">
{task.title}
</p>
{#if task.description}
<p class="mt-0.5 text-xs opacity-40 line-clamp-1">{task.description}</p>
{/if}
{#if task.due_date}
<span class="mt-1 inline-block rounded-full border border-border-light px-2 py-0.5 text-xs opacity-50 dark:border-border-dark">
{formatDate(task.due_date)}
</span>
{/if}
{/if}
<!-- Expandable edit description -->
<div class="grid transition-[grid-template-rows,opacity] duration-200 ease-out {editing ? 'grid-rows-[1fr] opacity-100' : 'grid-rows-[0fr] opacity-0'}">
<div class="overflow-hidden">
<textarea
bind:value={editDesc}
placeholder="Add description…"
rows="2"
class="mt-1 w-full resize-none bg-transparent text-xs opacity-60 outline-none"
tabindex={editing ? 0 : -1}
onkeydown={(e) => { if (e.key === "Escape") { editTitle = task.title; editDesc = task.description; editingTaskId = null; } }}
></textarea>
</div>
</div>
</div> </div>
<!-- Kebab menu --> <!-- Content -->
<div class="relative shrink-0" bind:this={menuEl}> <div class="min-w-0 flex-1">
<button <p class="text-sm {isCompleted ? 'line-through opacity-50' : 'font-medium'}">
onmousedown={(e) => { e.stopPropagation(); showMenu = !showMenu; }} {task.title}
class="rounded p-1 opacity-0 transition-opacity group-hover:opacity-40 hover:!opacity-80 {showMenu || editing ? '!opacity-40' : ''}" </p>
title="More" {#if task.description}
> <p class="mt-0.5 text-xs opacity-40 line-clamp-1">{task.description}</p>
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> {/if}
<path d="M10 6a2 2 0 110-4 2 2 0 010 4zM10 12a2 2 0 110-4 2 2 0 010 4zM10 18a2 2 0 110-4 2 2 0 010 4z" /> {#if task.due_date}
</svg> <span class="mt-1 inline-block rounded-full border border-border-light px-2 py-0.5 text-xs opacity-50 dark:border-border-dark">
</button> {formatDate(task.due_date)}
{#if showMenu} </span>
<div class="absolute right-0 top-full z-40 mt-1 min-w-[140px] rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark"> {/if}
<button </div>
onmousedown={(e) => { e.stopPropagation(); showMenu = false; app.deleteTask(task.id); }}
class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm text-danger hover:bg-black/5 dark:hover:bg-white/10" <!-- Chevron -->
> <svg class="mt-1 h-4 w-4 shrink-0 opacity-0 transition-opacity group-hover:opacity-30" viewBox="0 0 20 20" fill="currentColor">
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor"> <path fill-rule="evenodd" d="M7.21 14.77a.75.75 0 01.02-1.06L11.168 10 7.23 6.29a.75.75 0 111.04-1.08l4.5 4.25a.75.75 0 010 1.08l-4.5 4.25a.75.75 0 01-1.06-.02z" />
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" /> </svg>
</svg> </button>
Delete
</button>
</div>
{/if}
</div>
</div>
</div> </div>
</div> </div>
</div> </div>