Merge pull request #6 from SteelDynamite/gui-enhancements
gui-enhancements
This commit is contained in:
commit
8db4258fe9
2
PLAN.md
2
PLAN.md
|
|
@ -706,6 +706,8 @@ WorkspaceConfig {
|
|||
- [x] Settings popup overlay (WebDAV config, dark mode toggle)
|
||||
- [x] Dark mode (GNOME-style neutral theme, cyan-blue accent)
|
||||
- [x] Animated completed section show/hide
|
||||
- [ ] Move task between lists (needs `move_task(from_list, to_list, task_id)` added to bevy-tasks-core + Tauri command, then wire into task detail kebab menu)
|
||||
- [ ] Optional time on due dates (backend `due_date` is `DateTime<Utc>` — needs a separate `due_time` field or a nullable time component so date-only tasks don't default to midnight; currently the GUI uses `hours == 0 && minutes == 0` as a heuristic for "no time set" which breaks for actual midnight times)
|
||||
- [ ] Due date picker/editor (backend supports it, needs date input in new task toast + inline editing)
|
||||
- [ ] WebDAV setup flow with credentials (settings panel has fields, triggerSync needs to pull creds from config)
|
||||
- [ ] List/workspace rename (needs `rename_list` added to bevy-tasks-core first)
|
||||
|
|
|
|||
12
apps/tauri/package-lock.json
generated
12
apps/tauri/package-lock.json
generated
|
|
@ -9,7 +9,8 @@
|
|||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0"
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
|
|
@ -1421,6 +1422,15 @@
|
|||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tauri-apps/plugin-os": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/@tauri-apps/plugin-os/-/plugin-os-2.3.2.tgz",
|
||||
"integrity": "sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A==",
|
||||
"license": "MIT OR Apache-2.0",
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0"
|
||||
"@tauri-apps/plugin-dialog": "^2.0.0",
|
||||
"@tauri-apps/plugin-os": "^2.3.2"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ tauri-build = { version = "2", features = [] }
|
|||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-dialog = "2"
|
||||
tauri-plugin-os = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
bevy-tasks-core = { path = "../../../crates/bevy-tasks-core" }
|
||||
|
|
|
|||
15
apps/tauri/src-tauri/capabilities/default.json
Normal file
15
apps/tauri/src-tauri/capabilities/default.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"identifier": "default",
|
||||
"description": "Default capabilities for Bevy Tasks",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"dialog:default",
|
||||
"os:default",
|
||||
"core:window:allow-close",
|
||||
"core:window:allow-minimize",
|
||||
"core:window:allow-toggle-maximize",
|
||||
"core:window:allow-start-dragging",
|
||||
"core:window:allow-is-maximized"
|
||||
]
|
||||
}
|
||||
|
|
@ -353,6 +353,7 @@ pub fn run() {
|
|||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_os::init())
|
||||
.manage(Mutex::new(AppState { config, repo: None }))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_config,
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@
|
|||
"height": 700,
|
||||
"minWidth": 320,
|
||||
"minHeight": 500,
|
||||
"resizable": true
|
||||
"resizable": true,
|
||||
"decorations": false,
|
||||
"transparent": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
|
|
|
|||
|
|
@ -11,12 +11,14 @@
|
|||
</script>
|
||||
|
||||
<div class={app.darkMode ? "dark" : ""}>
|
||||
<div class="h-screen w-screen p-2">
|
||||
<div
|
||||
class="h-screen w-screen overflow-hidden bg-surface-light text-text-light dark:bg-surface-dark dark:text-text-dark"
|
||||
class="relative h-full w-full overflow-hidden rounded-xl border border-black/15 bg-surface-light text-text-light dark:border-white/15 dark:bg-surface-dark dark:text-text-dark"
|
||||
style="container-type: inline-size; box-shadow: 0 2px 8px rgba(0,0,0,0.25), 0 0 2px rgba(0,0,0,0.1)"
|
||||
>
|
||||
{#if app.error}
|
||||
<div
|
||||
class="fixed top-0 left-0 right-0 z-50 flex items-center justify-between bg-danger px-4 py-2 text-sm text-white"
|
||||
class="absolute top-0 left-0 right-0 z-50 flex items-center justify-between bg-danger px-4 py-2 text-sm text-white"
|
||||
>
|
||||
<span>{app.error}</span>
|
||||
<button onclick={() => app.clearError()} class="ml-2 font-bold">✕</button>
|
||||
|
|
@ -29,4 +31,5 @@
|
|||
<TasksScreen />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -24,11 +24,16 @@
|
|||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
html {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* Scrollbar styling */
|
||||
|
|
@ -42,3 +47,9 @@ body {
|
|||
.dark ::-webkit-scrollbar-thumb {
|
||||
background: #4b5563;
|
||||
}
|
||||
|
||||
/* Select dropdown theming */
|
||||
.dark select option {
|
||||
background-color: #242424;
|
||||
color: #e5e7eb;
|
||||
}
|
||||
|
|
|
|||
186
apps/tauri/src/lib/components/DateTimePicker.svelte
Normal file
186
apps/tauri/src/lib/components/DateTimePicker.svelte
Normal 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>
|
||||
195
apps/tauri/src/lib/components/TaskDetailView.svelte
Normal file
195
apps/tauri/src/lib/components/TaskDetailView.svelte
Normal 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}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts" module>
|
||||
let editingTaskId = $state<string | null>(null);
|
||||
export const animateInIds = new Set<string>();
|
||||
</script>
|
||||
|
||||
|
|
@ -7,26 +6,18 @@
|
|||
import type { Task } from "../types";
|
||||
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 swipeX = $state(0);
|
||||
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 animatingIn = $state(false);
|
||||
|
||||
let isCompleted = $derived(task.status === "completed");
|
||||
|
||||
$effect(() => {
|
||||
// Check on status change whether this task should animate in
|
||||
const _ = task.status; // track reactively
|
||||
const _ = task.status;
|
||||
if (animateInIds.has(task.id)) {
|
||||
animateInIds.delete(task.id);
|
||||
animatingIn = true;
|
||||
|
|
@ -38,50 +29,14 @@
|
|||
}
|
||||
});
|
||||
|
||||
async function handleToggle() {
|
||||
async function handleToggle(e: MouseEvent) {
|
||||
e.stopPropagation();
|
||||
transitioning = true;
|
||||
animateInIds.add(task.id);
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
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) {
|
||||
touchStartX = e.touches[0].clientX;
|
||||
swiping = true;
|
||||
|
|
@ -98,7 +53,9 @@
|
|||
if (Math.abs(swipeX) > 100) {
|
||||
swipeX = 0;
|
||||
swiping = false;
|
||||
handleToggle();
|
||||
transitioning = true;
|
||||
animateInIds.add(task.id);
|
||||
setTimeout(() => app.toggleTask(task.id), 200);
|
||||
return;
|
||||
}
|
||||
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'}"
|
||||
>
|
||||
<div class="overflow-hidden">
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
bind:this={containerEl}
|
||||
class="relative {showMenu ? 'z-40' : ''}"
|
||||
class="relative"
|
||||
ontouchstart={handleTouchStart}
|
||||
ontouchmove={handleTouchMove}
|
||||
ontouchend={handleTouchEnd}
|
||||
|
|
@ -139,15 +96,15 @@
|
|||
{/if}
|
||||
|
||||
<!-- Task content -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
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"
|
||||
<button
|
||||
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"
|
||||
style="transform: translateX({swipeX}px); transition: {swiping ? 'none' : 'transform 0.2s ease-out'}"
|
||||
onmousedown={startEditing}
|
||||
onclick={() => onopen?.(task)}
|
||||
>
|
||||
<!-- Checkbox -->
|
||||
<button
|
||||
onmousedown={(e) => { e.stopPropagation(); handleToggle(); }}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<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
|
||||
? 'border-primary bg-primary'
|
||||
: 'border-gray-400 dark:border-gray-500'}"
|
||||
|
|
@ -160,19 +117,10 @@
|
|||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 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}
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm {isCompleted ? 'line-through opacity-50' : 'font-medium'}">
|
||||
{task.title}
|
||||
</p>
|
||||
|
|
@ -184,49 +132,13 @@
|
|||
{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>
|
||||
|
||||
<!-- Kebab menu -->
|
||||
<div class="relative shrink-0" bind:this={menuEl}>
|
||||
<button
|
||||
onmousedown={(e) => { e.stopPropagation(); showMenu = !showMenu; }}
|
||||
class="rounded p-1 opacity-0 transition-opacity group-hover:opacity-40 hover:!opacity-80 {showMenu || editing ? '!opacity-40' : ''}"
|
||||
title="More"
|
||||
>
|
||||
<svg class="h-4 w-4" 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" />
|
||||
<!-- 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">
|
||||
<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>
|
||||
{#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
|
||||
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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -41,20 +41,19 @@
|
|||
</script>
|
||||
|
||||
<header
|
||||
class="flex items-center gap-3 border-b border-border-light px-4 py-3 dark:border-border-dark"
|
||||
class="flex items-center justify-between border-b border-border-light px-4 py-3 dark:border-border-dark"
|
||||
>
|
||||
<h1 class="text-lg font-bold">Settings</h1>
|
||||
<button
|
||||
onclick={() => onclose?.()}
|
||||
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">
|
||||
<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"
|
||||
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>
|
||||
<h1 class="text-lg font-bold">Settings</h1>
|
||||
</header>
|
||||
|
||||
<main class="flex-1 overflow-y-auto p-4">
|
||||
|
|
|
|||
|
|
@ -1,8 +1,28 @@
|
|||
<script lang="ts">
|
||||
import { app } from "../stores/app.svelte";
|
||||
import TaskItem from "../components/TaskItem.svelte";
|
||||
import TaskDetailView from "../components/TaskDetailView.svelte";
|
||||
import NewTaskInput, { newTaskState } from "../components/NewTaskInput.svelte";
|
||||
import SettingsScreen from "./SettingsScreen.svelte";
|
||||
import { getCurrentWindow } from "@tauri-apps/api/window";
|
||||
import { platform } from "@tauri-apps/plugin-os";
|
||||
import type { Task } from "../types";
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
const currentPlatform = platform();
|
||||
const isDesktop = currentPlatform === "linux" || currentPlatform === "windows";
|
||||
const isWindows = currentPlatform === "windows";
|
||||
|
||||
let selectedTaskId = $state<string | null>(null);
|
||||
let selectedTask = $derived(selectedTaskId ? app.tasks.find(t => t.id === selectedTaskId) ?? null : null);
|
||||
|
||||
function openTask(task: Task) {
|
||||
selectedTaskId = task.id;
|
||||
}
|
||||
|
||||
function closeDetail() {
|
||||
selectedTaskId = null;
|
||||
}
|
||||
|
||||
let showDrawer = $state(false);
|
||||
let showSettings = $state(false);
|
||||
|
|
@ -23,8 +43,8 @@
|
|||
window.addEventListener("mousedown", handleWindowClick);
|
||||
}
|
||||
let newListName = $state("");
|
||||
let showCompleted = $state(true);
|
||||
let completedVisible = $state(true);
|
||||
let showCompleted = $state(false);
|
||||
let completedVisible = $state(false);
|
||||
let listMenuId = $state<string | null>(null);
|
||||
let wsMenuName = $state<string | null>(null);
|
||||
let dragId = $state<string | null>(null);
|
||||
|
|
@ -113,19 +133,25 @@
|
|||
showSettings = false;
|
||||
}
|
||||
|
||||
function handleHeaderMouseDown(e: MouseEvent) {
|
||||
if (e.button !== 0) return;
|
||||
if ((e.target as HTMLElement).closest("button")) return;
|
||||
if (isDesktop) appWindow.startDragging();
|
||||
}
|
||||
|
||||
let workspaceNames = $derived(app.config ? Object.keys(app.config.workspaces) : []);
|
||||
let translateX = $derived(showDrawer ? '0' : '-80vw');
|
||||
let translateX = $derived(showDrawer ? '0' : '-80cqi');
|
||||
</script>
|
||||
|
||||
<!-- Viewport clip -->
|
||||
<div class="h-screen w-screen overflow-hidden">
|
||||
<div class="h-full w-full overflow-hidden">
|
||||
<!-- Sliding container: left drawer + main content -->
|
||||
<div
|
||||
class="flex h-full ease-out {resizing ? '' : 'transition-transform duration-250'}"
|
||||
style="width: calc(100vw + 80vw); transform: translateX({translateX})"
|
||||
style="width: calc(100cqi + 80cqi); transform: translateX({translateX})"
|
||||
>
|
||||
<!-- Drawer panel -->
|
||||
<div class="flex h-full w-[80vw] shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
||||
<div class="flex h-full shrink-0 flex-col bg-surface-light dark:bg-surface-dark" style="width: 80cqi">
|
||||
<!-- List items + new list button -->
|
||||
<div class="flex-1 overflow-y-auto py-2">
|
||||
{#each app.lists as list (list.id)}
|
||||
|
|
@ -287,7 +313,7 @@
|
|||
</div>
|
||||
|
||||
<!-- 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 h-full shrink-0 overflow-hidden bg-surface-light dark:bg-surface-dark" style="width: 100cqi">
|
||||
<!-- Dim overlay when drawer is open -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
|
|
@ -296,17 +322,28 @@
|
|||
onclick={closeDrawer}
|
||||
onkeydown={(e) => { if (e.key === "Escape") closeDrawer(); }}
|
||||
></div>
|
||||
<!-- Header -->
|
||||
|
||||
<!-- Sliding inner: task list + detail view -->
|
||||
<div
|
||||
class="flex h-full {resizing ? '' : 'transition-transform duration-250'} ease-out"
|
||||
style="width: 200%; transform: translateX({selectedTask ? '-50%' : '0'})"
|
||||
>
|
||||
<!-- Sub-panel: Task list -->
|
||||
<div class="relative flex h-full w-1/2 flex-col">
|
||||
<!-- Header / drag region -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<header
|
||||
onmousedown={handleHeaderMouseDown}
|
||||
ondblclick={() => { if (isDesktop) appWindow.toggleMaximize(); }}
|
||||
class="relative flex items-center border-b border-border-light px-4 py-3 dark:border-border-dark"
|
||||
>
|
||||
<!-- Back arrow (left) -->
|
||||
<!-- Drawer toggle (left) -->
|
||||
<button
|
||||
onclick={() => (showDrawer = !showDrawer)}
|
||||
class="absolute left-2 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" />
|
||||
<path d="M3 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h8a1 1 0 110 2H4a1 1 0 01-1-1zm0 4a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
|
|
@ -318,9 +355,36 @@
|
|||
<p class="text-lg font-bold">{app.activeList?.title ?? "Tasks"}</p>
|
||||
</div>
|
||||
|
||||
<!-- Sync spinner (right) -->
|
||||
{#if app.syncing}
|
||||
<div class="absolute right-4 h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||
<!-- Window controls (right) -->
|
||||
{#if isDesktop}
|
||||
<div class="absolute right-1.5 flex items-center gap-0.5">
|
||||
{#if isWindows}
|
||||
<button
|
||||
onclick={() => appWindow.minimize()}
|
||||
class="rounded p-1.5 opacity-50 hover:bg-black/10 hover:opacity-80 dark:hover:bg-white/10"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path d="M4 10a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
onclick={() => appWindow.toggleMaximize()}
|
||||
class="rounded p-1.5 opacity-50 hover:bg-black/10 hover:opacity-80 dark:hover:bg-white/10"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="14" height="14" rx="1" />
|
||||
</svg>
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
onclick={() => appWindow.close()}
|
||||
class="rounded p-1.5 opacity-50 hover:bg-danger/20 hover:opacity-100 hover:text-danger dark:hover:bg-danger/20"
|
||||
>
|
||||
<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>
|
||||
{/if}
|
||||
</header>
|
||||
|
||||
|
|
@ -346,7 +410,7 @@
|
|||
ondrop={(e) => handleDrop(e, task.id)}
|
||||
class="{dragId === task.id ? 'opacity-30' : ''} {dragOverId === task.id && dragId !== task.id ? 'border-t-2 border-t-primary' : ''}"
|
||||
>
|
||||
<TaskItem {task} />
|
||||
<TaskItem {task} onopen={openTask} />
|
||||
</div>
|
||||
{/each}
|
||||
|
||||
|
|
@ -383,7 +447,7 @@
|
|||
{#if completedVisible}
|
||||
<div class="transition-all duration-300 ease-out {showCompleted ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-4'}">
|
||||
{#each app.completedTasks as task (task.id)}
|
||||
<TaskItem {task} />
|
||||
<TaskItem {task} onopen={openTask} />
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -393,7 +457,7 @@
|
|||
|
||||
<!-- FAB button -->
|
||||
<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-20 flex justify-center transition-all duration-250 ease-out {newTaskState.open ? 'opacity-0 scale-75' : ''} {showDrawer || selectedTask ? 'translate-y-24 opacity-0' : 'translate-y-0 opacity-100'}"
|
||||
>
|
||||
<button
|
||||
onclick={() => { if (app.activeListId) newTaskState.open = true; }}
|
||||
|
|
@ -406,13 +470,27 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sub-panel: Task detail -->
|
||||
<div class="relative flex h-full w-1/2 flex-col bg-surface-light dark:bg-surface-dark">
|
||||
{#if selectedTask}
|
||||
<TaskDetailView task={selectedTask} onback={closeDetail} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sync spinner -->
|
||||
{#if app.syncing}
|
||||
<div class="absolute bottom-4 right-4 z-20 h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings popup overlay -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="fixed inset-0 z-50 flex transition-opacity duration-200 {showSettings ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}"
|
||||
class="absolute inset-0 z-50 flex transition-opacity duration-200 {showSettings ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}"
|
||||
style="padding: 4%"
|
||||
>
|
||||
<!-- Backdrop -->
|
||||
|
|
@ -431,6 +509,6 @@
|
|||
</div>
|
||||
|
||||
<!-- Toast overlay (outside sliding container so it stays centered) -->
|
||||
<div class="pointer-events-none fixed inset-0 z-50">
|
||||
<div class="pointer-events-none absolute inset-0 z-50">
|
||||
<NewTaskInput />
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue