Add drag-and-drop task reordering, sliding settings panel, and inline task editing
This commit is contained in:
parent
f810cfa2a3
commit
68dfbb078a
|
|
@ -25,10 +25,8 @@
|
||||||
|
|
||||||
{#if app.screen === "setup"}
|
{#if app.screen === "setup"}
|
||||||
<SetupScreen />
|
<SetupScreen />
|
||||||
{:else if app.screen === "tasks"}
|
{:else}
|
||||||
<TasksScreen />
|
<TasksScreen />
|
||||||
{:else if app.screen === "settings"}
|
|
||||||
<SettingsScreen />
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,50 @@
|
||||||
|
<script lang="ts" module>
|
||||||
|
let editingTaskId = $state<string | null>(null);
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 }: { task: Task } = $props();
|
||||||
|
|
||||||
let editing = $state(false);
|
|
||||||
let editTitle = $state(task.title);
|
let editTitle = $state(task.title);
|
||||||
let editDesc = $state(task.description);
|
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 isCompleted = $derived(task.status === "completed");
|
let isCompleted = $derived(task.status === "completed");
|
||||||
|
|
||||||
|
function startEditing() {
|
||||||
|
if (editing) return;
|
||||||
|
editingTaskId = task.id;
|
||||||
|
editTitle = task.title;
|
||||||
|
editDesc = task.description;
|
||||||
|
// Wait for expand/contract animation (200ms) to settle before focusing
|
||||||
|
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;
|
||||||
|
// Delay so that clicking a different task can set editingTaskId first
|
||||||
|
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;
|
||||||
|
|
@ -21,7 +53,6 @@
|
||||||
function handleTouchMove(e: TouchEvent) {
|
function handleTouchMove(e: TouchEvent) {
|
||||||
if (!swiping) return;
|
if (!swiping) return;
|
||||||
const dx = e.touches[0].clientX - touchStartX;
|
const dx = e.touches[0].clientX - touchStartX;
|
||||||
// Only allow left swipe for pending, right swipe for completed
|
|
||||||
if (isCompleted) swipeX = Math.max(0, dx);
|
if (isCompleted) swipeX = Math.max(0, dx);
|
||||||
else swipeX = Math.min(0, dx);
|
else swipeX = Math.min(0, dx);
|
||||||
}
|
}
|
||||||
|
|
@ -34,13 +65,6 @@
|
||||||
swiping = false;
|
swiping = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function saveEdit() {
|
|
||||||
if (!editTitle.trim()) return;
|
|
||||||
const updated = { ...task, title: editTitle.trim(), description: editDesc };
|
|
||||||
await app.updateTask(updated);
|
|
||||||
editing = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDate(iso: string): string {
|
function formatDate(iso: string): string {
|
||||||
const d = new Date(iso);
|
const d = new Date(iso);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
@ -53,6 +77,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
bind:this={containerEl}
|
||||||
class="relative overflow-hidden border-b border-border-light dark:border-border-dark"
|
class="relative overflow-hidden border-b border-border-light dark:border-border-dark"
|
||||||
ontouchstart={handleTouchStart}
|
ontouchstart={handleTouchStart}
|
||||||
ontouchmove={handleTouchMove}
|
ontouchmove={handleTouchMove}
|
||||||
|
|
@ -70,13 +95,15 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Task content -->
|
<!-- Task content -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="relative flex items-start gap-3 bg-surface-light px-4 py-3 dark:bg-surface-dark"
|
class="relative flex cursor-pointer items-start gap-3 bg-surface-light px-4 py-3 transition-colors 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}
|
||||||
>
|
>
|
||||||
<!-- Checkbox -->
|
<!-- Checkbox -->
|
||||||
<button
|
<button
|
||||||
onclick={() => app.toggleTask(task.id)}
|
onmousedown={(e) => { e.stopPropagation(); app.toggleTask(task.id) }}
|
||||||
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'}"
|
||||||
|
|
@ -92,40 +119,16 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- Content -->
|
<!-- Content -->
|
||||||
|
<div class="min-w-0 flex-1" onfocusout={handleFocusOut}>
|
||||||
{#if editing}
|
{#if editing}
|
||||||
<div class="min-w-0 flex-1">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
bind:this={titleInputEl}
|
||||||
bind:value={editTitle}
|
bind:value={editTitle}
|
||||||
class="w-full bg-transparent text-sm font-medium outline-none"
|
class="w-full bg-transparent text-sm font-medium outline-none"
|
||||||
onkeydown={(e) => { if (e.key === "Enter") saveEdit(); if (e.key === "Escape") editing = false; }}
|
onkeydown={(e) => { if (e.key === "Enter") (e.target as HTMLElement).blur(); if (e.key === "Escape") { editTitle = task.title; editDesc = task.description; editingTaskId = null; } }}
|
||||||
/>
|
/>
|
||||||
<textarea
|
|
||||||
bind:value={editDesc}
|
|
||||||
placeholder="Add description…"
|
|
||||||
rows="2"
|
|
||||||
class="mt-1 w-full resize-none bg-transparent text-xs opacity-60 outline-none"
|
|
||||||
/>
|
|
||||||
<div class="mt-1 flex gap-2">
|
|
||||||
<button
|
|
||||||
onclick={saveEdit}
|
|
||||||
class="rounded px-2 py-1 text-xs font-medium text-primary"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onclick={() => (editing = false)}
|
|
||||||
class="rounded px-2 py-1 text-xs opacity-60"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{:else}
|
{:else}
|
||||||
<button
|
|
||||||
onclick={() => { editing = true; editTitle = task.title; editDesc = task.description; }}
|
|
||||||
class="min-w-0 flex-1 text-left"
|
|
||||||
>
|
|
||||||
<p class="text-sm {isCompleted ? 'line-through opacity-50' : 'font-medium'}">
|
<p class="text-sm {isCompleted ? 'line-through opacity-50' : 'font-medium'}">
|
||||||
{task.title}
|
{task.title}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -137,13 +140,27 @@
|
||||||
{formatDate(task.due_date)}
|
{formatDate(task.due_date)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
|
||||||
{/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; } }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Delete -->
|
<!-- Delete -->
|
||||||
{#if !editing}
|
{#if !editing}
|
||||||
<button
|
<button
|
||||||
onclick={() => app.deleteTask(task.id)}
|
onmousedown={(e) => { e.stopPropagation(); app.deleteTask(task.id) }}
|
||||||
class="shrink-0 rounded p-1 opacity-0 transition-opacity hover:opacity-60 group-hover:opacity-30"
|
class="shrink-0 rounded p-1 opacity-0 transition-opacity hover:opacity-60 group-hover:opacity-30"
|
||||||
style="opacity: 0.15"
|
style="opacity: 0.15"
|
||||||
title="Delete"
|
title="Delete"
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { app } from "../stores/app.svelte";
|
import { app } from "../stores/app.svelte";
|
||||||
|
|
||||||
|
let { onclose }: { onclose?: () => void } = $props();
|
||||||
|
|
||||||
let webdavUrl = $state("");
|
let webdavUrl = $state("");
|
||||||
let webdavUser = $state("");
|
let webdavUser = $state("");
|
||||||
let webdavPass = $state("");
|
let webdavPass = $state("");
|
||||||
|
|
@ -47,7 +49,7 @@
|
||||||
class="flex items-center gap-3 border-b border-border-light px-4 py-3 dark:border-border-dark"
|
class="flex items-center gap-3 border-b border-border-light px-4 py-3 dark:border-border-dark"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onclick={() => app.setScreen("tasks")}
|
onclick={() => onclose?.()}
|
||||||
class="rounded-lg p-1.5 hover:bg-black/5 dark:hover:bg-white/10"
|
class="rounded-lg p-1.5 hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
|
@ -60,7 +62,7 @@
|
||||||
<h1 class="text-lg font-bold">Settings</h1>
|
<h1 class="text-lg font-bold">Settings</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="overflow-y-auto p-4" style="height: calc(100vh - 57px)">
|
<main class="flex-1 overflow-y-auto p-4">
|
||||||
<!-- Workspaces -->
|
<!-- Workspaces -->
|
||||||
<section class="mb-6">
|
<section class="mb-6">
|
||||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide opacity-50">
|
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide opacity-50">
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,26 @@
|
||||||
import { app } from "../stores/app.svelte";
|
import { app } from "../stores/app.svelte";
|
||||||
import TaskItem from "../components/TaskItem.svelte";
|
import TaskItem from "../components/TaskItem.svelte";
|
||||||
import NewTaskInput, { newTaskState } from "../components/NewTaskInput.svelte";
|
import NewTaskInput, { newTaskState } from "../components/NewTaskInput.svelte";
|
||||||
|
import SettingsScreen from "./SettingsScreen.svelte";
|
||||||
|
|
||||||
let showDrawer = $state(false);
|
let showDrawer = $state(false);
|
||||||
|
let showSettings = $state(false);
|
||||||
let showNewList = $state(false);
|
let showNewList = $state(false);
|
||||||
let newListName = $state("");
|
let newListName = $state("");
|
||||||
let showCompleted = $state(true);
|
let showCompleted = $state(true);
|
||||||
let confirmDeleteList = $state<string | null>(null);
|
let confirmDeleteList = $state<string | null>(null);
|
||||||
|
let dragId = $state<string | null>(null);
|
||||||
|
let dragOverId = $state<string | null>(null);
|
||||||
|
let resizing = $state(false);
|
||||||
|
let resizeTimer: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.addEventListener("resize", () => {
|
||||||
|
resizing = true;
|
||||||
|
clearTimeout(resizeTimer);
|
||||||
|
resizeTimer = setTimeout(() => (resizing = false), 150);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function handleNewList() {
|
async function handleNewList() {
|
||||||
if (!newListName.trim()) return;
|
if (!newListName.trim()) return;
|
||||||
|
|
@ -23,20 +37,81 @@
|
||||||
showDrawer = false;
|
showDrawer = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleDragStart(e: DragEvent, taskId: string) {
|
||||||
|
dragId = taskId;
|
||||||
|
if (e.dataTransfer) {
|
||||||
|
e.dataTransfer.effectAllowed = "move";
|
||||||
|
e.dataTransfer.setData("text/plain", taskId);
|
||||||
|
// Create an unclipped drag image
|
||||||
|
const el = (e.target as HTMLElement).closest("[draggable]") as HTMLElement;
|
||||||
|
if (el) {
|
||||||
|
const clone = el.cloneNode(true) as HTMLElement;
|
||||||
|
clone.style.width = `${el.offsetWidth}px`;
|
||||||
|
clone.style.position = "absolute";
|
||||||
|
clone.style.top = "-9999px";
|
||||||
|
clone.style.left = "-9999px";
|
||||||
|
if (app.darkMode) {
|
||||||
|
clone.classList.add("dark");
|
||||||
|
clone.style.backgroundColor = "var(--color-surface-dark)";
|
||||||
|
clone.style.color = "var(--color-text-dark)";
|
||||||
|
}
|
||||||
|
clone.style.opacity = "0.85";
|
||||||
|
clone.style.borderRadius = "8px";
|
||||||
|
clone.style.overflow = "hidden";
|
||||||
|
clone.style.boxShadow = "0 4px 12px rgba(0,0,0,0.3)";
|
||||||
|
document.body.appendChild(clone);
|
||||||
|
e.dataTransfer.setDragImage(clone, e.offsetX, e.offsetY);
|
||||||
|
requestAnimationFrame(() => clone.remove());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver(e: DragEvent, taskId: string) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
|
||||||
|
dragOverId = taskId;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd() {
|
||||||
|
dragId = null;
|
||||||
|
dragOverId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleDrop(e: DragEvent, targetId: string) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!dragId || dragId === targetId) { handleDragEnd(); return; }
|
||||||
|
const targetIndex = app.pendingTasks.findIndex((t) => t.id === targetId);
|
||||||
|
if (targetIndex >= 0) await app.reorderTask(dragId, targetIndex);
|
||||||
|
handleDragEnd();
|
||||||
|
}
|
||||||
|
|
||||||
function closeDrawer() {
|
function closeDrawer() {
|
||||||
showDrawer = false;
|
showDrawer = false;
|
||||||
showNewList = false;
|
showNewList = false;
|
||||||
confirmDeleteList = null;
|
confirmDeleteList = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openSettings() {
|
||||||
|
showSettings = true;
|
||||||
|
showDrawer = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeSettings() {
|
||||||
|
showSettings = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let translateX = $derived(showDrawer ? '0' : showSettings ? '-160vw' : '-80vw');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Sliding container: drawer + main content move as one piece -->
|
<!-- Viewport clip -->
|
||||||
|
<div class="h-screen w-screen overflow-hidden">
|
||||||
|
<!-- Sliding container: left drawer + main content + settings panel move as one piece -->
|
||||||
<div
|
<div
|
||||||
class="flex h-screen transition-transform duration-250 ease-out"
|
class="flex h-full ease-out {resizing ? '' : 'transition-transform duration-250'}"
|
||||||
style="width: calc(100vw + 18rem); transform: translateX({showDrawer ? '0' : '-18rem'})"
|
style="width: calc(100vw + 160vw); transform: translateX({translateX})"
|
||||||
>
|
>
|
||||||
<!-- Drawer panel (always rendered, sits to the left) -->
|
<!-- Drawer panel (always rendered, sits to the left) -->
|
||||||
<div class="flex h-full w-72 shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
<div class="flex h-full w-[80vw] shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
||||||
<!-- Drawer header -->
|
<!-- Drawer header -->
|
||||||
<div class="border-b border-border-light px-4 py-4 dark:border-border-dark">
|
<div class="border-b border-border-light px-4 py-4 dark:border-border-dark">
|
||||||
<p class="text-xs text-text-secondary-light dark:text-text-secondary-dark">
|
<p class="text-xs text-text-secondary-light dark:text-text-secondary-dark">
|
||||||
|
|
@ -119,13 +194,13 @@
|
||||||
|
|
||||||
<!-- Main content panel -->
|
<!-- Main content panel -->
|
||||||
<div class="relative flex h-full w-screen shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
<div class="relative flex h-full w-screen shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
||||||
<!-- Dim overlay + shadow when drawer is open -->
|
<!-- Dim overlay + shadow when drawer or settings is open -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
class="absolute inset-0 z-30 transition-opacity duration-250 ease-out {showDrawer ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}"
|
class="absolute inset-0 z-30 transition-opacity duration-250 ease-out {showDrawer || showSettings ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}"
|
||||||
style="box-shadow: inset 8px 0 24px rgba(0,0,0,0.4); background: rgba(0,0,0,0.4)"
|
style="box-shadow: {showDrawer ? 'inset 8px 0 24px rgba(0,0,0,0.4)' : showSettings ? 'inset -8px 0 24px rgba(0,0,0,0.4)' : 'none'}; background: rgba(0,0,0,0.4)"
|
||||||
onclick={closeDrawer}
|
onclick={() => { if (showDrawer) closeDrawer(); if (showSettings) closeSettings(); }}
|
||||||
onkeydown={(e) => { if (e.key === "Escape") closeDrawer(); }}
|
onkeydown={(e) => { if (e.key === "Escape") { closeDrawer(); closeSettings(); } }}
|
||||||
></div>
|
></div>
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<header
|
<header
|
||||||
|
|
@ -154,7 +229,7 @@
|
||||||
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
onclick={() => app.setScreen("settings")}
|
onclick={openSettings}
|
||||||
class="rounded-lg p-2 hover:bg-black/5 dark:hover:bg-white/10"
|
class="rounded-lg p-2 hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
title="Settings"
|
title="Settings"
|
||||||
>
|
>
|
||||||
|
|
@ -182,7 +257,17 @@
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each app.pendingTasks as task (task.id)}
|
{#each app.pendingTasks as task (task.id)}
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
draggable="true"
|
||||||
|
ondragstart={(e) => handleDragStart(e, task.id)}
|
||||||
|
ondragover={(e) => handleDragOver(e, task.id)}
|
||||||
|
ondragend={handleDragEnd}
|
||||||
|
ondrop={(e) => handleDrop(e, task.id)}
|
||||||
|
class="transition-all duration-150 {dragId === task.id ? 'opacity-30' : ''} {dragOverId === task.id && dragId !== task.id ? 'border-t-2 border-t-primary' : ''}"
|
||||||
|
>
|
||||||
<TaskItem {task} />
|
<TaskItem {task} />
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if app.pendingTasks.length === 0}
|
{#if app.pendingTasks.length === 0}
|
||||||
|
|
@ -217,7 +302,7 @@
|
||||||
|
|
||||||
<!-- FAB button, slides with main content -->
|
<!-- FAB button, slides with main content -->
|
||||||
<div
|
<div
|
||||||
class="pointer-events-none absolute bottom-6 left-0 right-0 z-30 flex justify-center transition-all duration-250 ease-out {newTaskState.open ? 'opacity-0 scale-75' : ''} {showDrawer ? 'translate-y-24 opacity-0' : 'translate-y-0 opacity-100'}"
|
class="pointer-events-none absolute bottom-6 left-0 right-0 z-30 flex justify-center transition-all duration-250 ease-out {newTaskState.open ? 'opacity-0 scale-75' : ''} {showDrawer || showSettings ? 'translate-y-24 opacity-0' : 'translate-y-0 opacity-100'}"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
onclick={() => { if (app.activeListId) newTaskState.open = true; }}
|
onclick={() => { if (app.activeListId) newTaskState.open = true; }}
|
||||||
|
|
@ -230,6 +315,12 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Settings panel (sits to the right of main content) -->
|
||||||
|
<div class="flex h-full w-[80vw] shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
||||||
|
<SettingsScreen onclose={closeSettings} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Toast overlay (outside sliding container so it stays centered) -->
|
<!-- Toast overlay (outside sliding container so it stays centered) -->
|
||||||
|
|
|
||||||
|
|
@ -178,6 +178,16 @@ async function updateTask(task: Task) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function reorderTask(taskId: string, newPosition: number) {
|
||||||
|
if (!activeListId) return;
|
||||||
|
try {
|
||||||
|
await invoke("reorder_task", { listId: activeListId, taskId, newPosition });
|
||||||
|
await loadTasks();
|
||||||
|
} catch (e) {
|
||||||
|
error = String(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteTask(taskId: string) {
|
async function deleteTask(taskId: string) {
|
||||||
if (!activeListId) return;
|
if (!activeListId) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -278,6 +288,7 @@ export const app = {
|
||||||
createTask,
|
createTask,
|
||||||
toggleTask,
|
toggleTask,
|
||||||
updateTask,
|
updateTask,
|
||||||
|
reorderTask,
|
||||||
deleteTask,
|
deleteTask,
|
||||||
triggerSync,
|
triggerSync,
|
||||||
toggleDarkMode,
|
toggleDarkMode,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue