Redesign drawer, settings popup, workspace switcher, kebab menus, and task UX polish
This commit is contained in:
parent
68dfbb078a
commit
3f6f327c92
|
|
@ -3,7 +3,7 @@
|
||||||
import { app } from "./lib/stores/app.svelte";
|
import { app } from "./lib/stores/app.svelte";
|
||||||
import SetupScreen from "./lib/screens/SetupScreen.svelte";
|
import SetupScreen from "./lib/screens/SetupScreen.svelte";
|
||||||
import TasksScreen from "./lib/screens/TasksScreen.svelte";
|
import TasksScreen from "./lib/screens/TasksScreen.svelte";
|
||||||
import SettingsScreen from "./lib/screens/SettingsScreen.svelte";
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
app.loadConfig();
|
app.loadConfig();
|
||||||
|
|
|
||||||
|
|
@ -16,9 +16,24 @@
|
||||||
let swiping = $state(false);
|
let swiping = $state(false);
|
||||||
let containerEl = $state<HTMLDivElement | null>(null);
|
let containerEl = $state<HTMLDivElement | null>(null);
|
||||||
let titleInputEl = $state<HTMLInputElement | null>(null);
|
let titleInputEl = $state<HTMLInputElement | null>(null);
|
||||||
|
let showMenu = $state(false);
|
||||||
|
let menuEl = $state<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
let isCompleted = $derived(task.status === "completed");
|
let isCompleted = $derived(task.status === "completed");
|
||||||
|
|
||||||
|
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() {
|
function startEditing() {
|
||||||
if (editing) return;
|
if (editing) return;
|
||||||
editingTaskId = task.id;
|
editingTaskId = task.id;
|
||||||
|
|
@ -78,7 +93,7 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={containerEl}
|
bind:this={containerEl}
|
||||||
class="relative overflow-hidden border-b border-border-light dark:border-border-dark"
|
class="relative {showMenu ? 'z-40' : ''}"
|
||||||
ontouchstart={handleTouchStart}
|
ontouchstart={handleTouchStart}
|
||||||
ontouchmove={handleTouchMove}
|
ontouchmove={handleTouchMove}
|
||||||
ontouchend={handleTouchEnd}
|
ontouchend={handleTouchEnd}
|
||||||
|
|
@ -97,7 +112,7 @@
|
||||||
<!-- Task content -->
|
<!-- Task content -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
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"
|
class="group relative flex 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}
|
onmousedown={startEditing}
|
||||||
>
|
>
|
||||||
|
|
@ -157,22 +172,30 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete -->
|
<!-- Kebab menu -->
|
||||||
{#if !editing}
|
<div class="relative shrink-0" bind:this={menuEl}>
|
||||||
<button
|
<button
|
||||||
onmousedown={(e) => { e.stopPropagation(); app.deleteTask(task.id) }}
|
onmousedown={(e) => { e.stopPropagation(); showMenu = !showMenu; }}
|
||||||
class="shrink-0 rounded p-1 opacity-0 transition-opacity hover:opacity-60 group-hover:opacity-30"
|
class="rounded p-1 opacity-0 transition-opacity group-hover:opacity-40 hover:!opacity-80 {showMenu || editing ? '!opacity-40' : ''}"
|
||||||
style="opacity: 0.15"
|
title="More"
|
||||||
title="Delete"
|
|
||||||
>
|
>
|
||||||
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path
|
<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" />
|
||||||
fill-rule="evenodd"
|
|
||||||
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
|
||||||
clip-rule="evenodd"
|
|
||||||
/>
|
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@
|
||||||
let webdavUser = $state("");
|
let webdavUser = $state("");
|
||||||
let webdavPass = $state("");
|
let webdavPass = $state("");
|
||||||
let testStatus = $state<"idle" | "testing" | "ok" | "fail">("idle");
|
let testStatus = $state<"idle" | "testing" | "ok" | "fail">("idle");
|
||||||
let confirmRemove = $state<string | null>(null);
|
|
||||||
|
|
||||||
async function testConnection() {
|
async function testConnection() {
|
||||||
testStatus = "testing";
|
testStatus = "testing";
|
||||||
|
|
@ -39,10 +38,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleRemoveWorkspace(name: string) {
|
|
||||||
await app.removeWorkspace(name);
|
|
||||||
confirmRemove = null;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header
|
<header
|
||||||
|
|
@ -63,75 +58,6 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main class="flex-1 overflow-y-auto p-4">
|
<main class="flex-1 overflow-y-auto p-4">
|
||||||
<!-- Workspaces -->
|
|
||||||
<section class="mb-6">
|
|
||||||
<h2 class="mb-3 text-sm font-semibold uppercase tracking-wide opacity-50">
|
|
||||||
Workspaces
|
|
||||||
</h2>
|
|
||||||
{#if app.config}
|
|
||||||
{#each Object.entries(app.config.workspaces) as [name, ws]}
|
|
||||||
<div
|
|
||||||
class="mb-2 rounded-xl border border-border-light p-3 dark:border-border-dark"
|
|
||||||
>
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="font-medium {name === app.config.current_workspace ? 'text-primary' : ''}">
|
|
||||||
{name}
|
|
||||||
</p>
|
|
||||||
<p class="text-xs opacity-50">{ws.path}</p>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-1">
|
|
||||||
{#if name !== app.config.current_workspace}
|
|
||||||
<button
|
|
||||||
onclick={() => app.switchWorkspace(name)}
|
|
||||||
class="rounded-lg px-3 py-1.5 text-xs font-medium text-primary hover:bg-primary/5"
|
|
||||||
>
|
|
||||||
Switch
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
{#if confirmRemove === name}
|
|
||||||
<button
|
|
||||||
onclick={() => handleRemoveWorkspace(name)}
|
|
||||||
class="rounded-lg px-3 py-1.5 text-xs font-medium text-danger"
|
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onclick={() => (confirmRemove = null)}
|
|
||||||
class="rounded-lg px-3 py-1.5 text-xs opacity-60"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
onclick={() => (confirmRemove = name)}
|
|
||||||
class="rounded-lg px-3 py-1.5 text-xs opacity-40 hover:text-danger hover:opacity-100"
|
|
||||||
>
|
|
||||||
Remove
|
|
||||||
</button>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{#if ws.webdav_url}
|
|
||||||
<p class="mt-1 text-xs opacity-40">Sync: {ws.webdav_url}</p>
|
|
||||||
{/if}
|
|
||||||
{#if ws.last_sync}
|
|
||||||
<p class="text-xs opacity-40">
|
|
||||||
Last synced: {new Date(ws.last_sync).toLocaleString()}
|
|
||||||
</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onclick={() => app.setScreen("setup")}
|
|
||||||
class="mt-2 rounded-lg px-3 py-2 text-sm text-primary hover:bg-primary/5"
|
|
||||||
>
|
|
||||||
+ Add workspace
|
|
||||||
</button>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<!-- WebDAV Sync -->
|
<!-- WebDAV Sync -->
|
||||||
<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">
|
||||||
|
|
|
||||||
|
|
@ -7,9 +7,25 @@
|
||||||
let showDrawer = $state(false);
|
let showDrawer = $state(false);
|
||||||
let showSettings = $state(false);
|
let showSettings = $state(false);
|
||||||
let showNewList = $state(false);
|
let showNewList = $state(false);
|
||||||
|
let showWorkspacePicker = $state(false);
|
||||||
|
let workspacePickerEl = $state<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
function handleWindowClick(e: MouseEvent) {
|
||||||
|
if (showWorkspacePicker && workspacePickerEl && !workspacePickerEl.contains(e.target as Node)) {
|
||||||
|
showWorkspacePicker = false;
|
||||||
|
}
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (listMenuId && !target.closest("[data-list-menu]")) listMenuId = null;
|
||||||
|
if (wsMenuName && !target.closest("[data-ws-menu]")) wsMenuName = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
window.addEventListener("mousedown", handleWindowClick);
|
||||||
|
}
|
||||||
let newListName = $state("");
|
let newListName = $state("");
|
||||||
let showCompleted = $state(true);
|
let showCompleted = $state(true);
|
||||||
let confirmDeleteList = $state<string | null>(null);
|
let listMenuId = $state<string | null>(null);
|
||||||
|
let wsMenuName = $state<string | null>(null);
|
||||||
let dragId = $state<string | null>(null);
|
let dragId = $state<string | null>(null);
|
||||||
let dragOverId = $state<string | null>(null);
|
let dragOverId = $state<string | null>(null);
|
||||||
let resizing = $state(false);
|
let resizing = $state(false);
|
||||||
|
|
@ -28,13 +44,11 @@
|
||||||
await app.createList(newListName.trim());
|
await app.createList(newListName.trim());
|
||||||
newListName = "";
|
newListName = "";
|
||||||
showNewList = false;
|
showNewList = false;
|
||||||
showDrawer = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleDeleteList(id: string) {
|
async function handleDeleteList(id: string) {
|
||||||
|
listMenuId = null;
|
||||||
await app.deleteList(id);
|
await app.deleteList(id);
|
||||||
confirmDeleteList = null;
|
|
||||||
showDrawer = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragStart(e: DragEvent, taskId: string) {
|
function handleDragStart(e: DragEvent, taskId: string) {
|
||||||
|
|
@ -42,7 +56,6 @@
|
||||||
if (e.dataTransfer) {
|
if (e.dataTransfer) {
|
||||||
e.dataTransfer.effectAllowed = "move";
|
e.dataTransfer.effectAllowed = "move";
|
||||||
e.dataTransfer.setData("text/plain", taskId);
|
e.dataTransfer.setData("text/plain", taskId);
|
||||||
// Create an unclipped drag image
|
|
||||||
const el = (e.target as HTMLElement).closest("[draggable]") as HTMLElement;
|
const el = (e.target as HTMLElement).closest("[draggable]") as HTMLElement;
|
||||||
if (el) {
|
if (el) {
|
||||||
const clone = el.cloneNode(true) as HTMLElement;
|
const clone = el.cloneNode(true) as HTMLElement;
|
||||||
|
|
@ -88,90 +101,81 @@
|
||||||
function closeDrawer() {
|
function closeDrawer() {
|
||||||
showDrawer = false;
|
showDrawer = false;
|
||||||
showNewList = false;
|
showNewList = false;
|
||||||
confirmDeleteList = null;
|
listMenuId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function openSettings() {
|
function openSettings() {
|
||||||
showSettings = true;
|
showSettings = true;
|
||||||
showDrawer = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeSettings() {
|
function closeSettings() {
|
||||||
showSettings = false;
|
showSettings = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let translateX = $derived(showDrawer ? '0' : showSettings ? '-160vw' : '-80vw');
|
let workspaceNames = $derived(app.config ? Object.keys(app.config.workspaces) : []);
|
||||||
|
let translateX = $derived(showDrawer ? '0' : '-80vw');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Viewport clip -->
|
<!-- Viewport clip -->
|
||||||
<div class="h-screen w-screen overflow-hidden">
|
<div class="h-screen w-screen overflow-hidden">
|
||||||
<!-- Sliding container: left drawer + main content + settings panel move as one piece -->
|
<!-- Sliding container: left drawer + main content -->
|
||||||
<div
|
<div
|
||||||
class="flex h-full ease-out {resizing ? '' : 'transition-transform duration-250'}"
|
class="flex h-full ease-out {resizing ? '' : 'transition-transform duration-250'}"
|
||||||
style="width: calc(100vw + 160vw); transform: translateX({translateX})"
|
style="width: calc(100vw + 80vw); transform: translateX({translateX})"
|
||||||
>
|
>
|
||||||
<!-- Drawer panel (always rendered, sits to the left) -->
|
<!-- 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 w-[80vw] shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
||||||
<!-- Drawer header -->
|
<!-- List items + new list button -->
|
||||||
<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">
|
|
||||||
{app.config?.current_workspace ?? ""}
|
|
||||||
</p>
|
|
||||||
<h2 class="text-lg font-bold">Lists</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- List items -->
|
|
||||||
<div class="flex-1 overflow-y-auto py-2">
|
<div class="flex-1 overflow-y-auto py-2">
|
||||||
{#each app.lists as list (list.id)}
|
{#each app.lists as list (list.id)}
|
||||||
<div class="flex items-center px-2">
|
<div class="group relative flex items-center px-2 hover:bg-black/5 dark:hover:bg-white/10">
|
||||||
<button
|
<button
|
||||||
onclick={() => { app.selectList(list.id); closeDrawer(); }}
|
onclick={() => { app.selectList(list.id); closeDrawer(); }}
|
||||||
class="flex-1 rounded-lg px-3 py-2.5 text-left text-sm hover:bg-black/5 dark:hover:bg-white/10 {list.id === app.activeListId ? 'font-bold text-primary bg-primary/5' : ''}"
|
class="flex flex-1 items-center gap-2 px-3 py-2.5 text-left text-sm {list.id === app.activeListId ? 'font-bold' : ''}"
|
||||||
>
|
>
|
||||||
{list.title}
|
{#if list.id === app.activeListId}
|
||||||
|
<svg class="h-4 w-4 shrink-0 opacity-50" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<span>{list.title}</span>
|
||||||
</button>
|
</button>
|
||||||
{#if confirmDeleteList === list.id}
|
<div class="relative shrink-0" data-list-menu>
|
||||||
<button
|
<button
|
||||||
onclick={() => handleDeleteList(list.id)}
|
onclick={() => (listMenuId = listMenuId === list.id ? null : list.id)}
|
||||||
class="rounded px-2 py-1 text-xs font-medium text-danger hover:bg-danger/10"
|
class="rounded p-1 opacity-0 transition-opacity group-hover:opacity-40 hover:!opacity-80 {listMenuId === list.id ? '!opacity-80' : ''}"
|
||||||
>
|
|
||||||
Confirm
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onclick={() => (confirmDeleteList = null)}
|
|
||||||
class="rounded px-2 py-1 text-xs opacity-60 hover:opacity-100"
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
{:else}
|
|
||||||
<button
|
|
||||||
onclick={() => (confirmDeleteList = list.id)}
|
|
||||||
class="rounded p-1.5 opacity-30 hover:opacity-60"
|
|
||||||
title="Delete list"
|
|
||||||
>
|
>
|
||||||
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path
|
<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" />
|
||||||
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>
|
</button>
|
||||||
|
{#if listMenuId === list.id}
|
||||||
|
<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={() => handleDeleteList(list.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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
<!-- New list button at bottom -->
|
<!-- New list inline -->
|
||||||
<div class="border-t border-border-light px-2 py-2 dark:border-border-dark">
|
<div class="px-2 mt-1">
|
||||||
{#if showNewList}
|
{#if showNewList}
|
||||||
<div class="flex gap-2 px-2">
|
<div class="flex gap-2 px-1">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newListName}
|
bind:value={newListName}
|
||||||
placeholder="List name"
|
placeholder="List name"
|
||||||
class="min-w-0 flex-1 rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm outline-none focus:border-primary dark:border-border-dark"
|
class="min-w-0 flex-1 rounded-lg border border-border-light bg-transparent px-3 py-2 text-sm outline-none focus:border-primary dark:border-border-dark"
|
||||||
onkeydown={(e) => { if (e.key === "Enter") handleNewList(); }}
|
onkeydown={(e) => { if (e.key === "Enter") handleNewList(); if (e.key === "Escape") { showNewList = false; newListName = ""; } }}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onclick={handleNewList}
|
onclick={handleNewList}
|
||||||
|
|
@ -192,48 +196,85 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Main content panel -->
|
<!-- Footer: workspace switcher (left) + settings gear (right) -->
|
||||||
<div class="relative flex h-full w-screen shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
<div class="flex items-center justify-between border-t border-border-light px-3 py-2 dark:border-border-dark">
|
||||||
<!-- Dim overlay + shadow when drawer or settings is open -->
|
<!-- Workspace switcher (custom drop-up) -->
|
||||||
|
<div class="relative min-w-0 flex-1" bind:this={workspacePickerEl}>
|
||||||
|
<button
|
||||||
|
onclick={() => (showWorkspacePicker = !showWorkspacePicker)}
|
||||||
|
class="flex w-full items-center gap-1.5 rounded-lg px-2 py-1.5 text-sm opacity-60 hover:bg-black/5 hover:opacity-100 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<svg class="h-3.5 w-3.5 shrink-0 transition-transform {showWorkspacePicker ? 'rotate-180' : ''}" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M14.77 12.79a.75.75 0 01-1.06-.02L10 8.832 6.29 12.77a.75.75 0 11-1.08-1.04l4.25-4.5a.75.75 0 011.08 0l4.25 4.5a.75.75 0 01-.02 1.06z" />
|
||||||
|
</svg>
|
||||||
|
<span class="truncate">{app.config?.current_workspace ?? "Workspace"}</span>
|
||||||
|
</button>
|
||||||
|
{#if showWorkspacePicker}
|
||||||
<!-- 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 || showSettings ? 'opacity-100 pointer-events-auto' : 'opacity-0 pointer-events-none'}"
|
class="absolute bottom-full left-0 mb-1 w-full rounded-lg border border-border-light bg-surface-light py-1 shadow-lg dark:border-border-dark dark:bg-surface-dark"
|
||||||
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={() => { if (showDrawer) closeDrawer(); if (showSettings) closeSettings(); }}
|
|
||||||
onkeydown={(e) => { if (e.key === "Escape") { closeDrawer(); closeSettings(); } }}
|
|
||||||
></div>
|
|
||||||
<!-- Header -->
|
|
||||||
<header
|
|
||||||
class="flex items-center justify-between border-b border-border-light px-4 py-3 dark:border-border-dark"
|
|
||||||
>
|
>
|
||||||
<div class="min-w-0 flex-1">
|
{#each workspaceNames as name}
|
||||||
<p class="text-xs text-text-secondary-light dark:text-text-secondary-dark">
|
{@const ws = app.config?.workspaces[name]}
|
||||||
{app.config?.current_workspace ?? ""}
|
<div class="group flex items-center px-1 hover:bg-black/5 dark:hover:bg-white/10">
|
||||||
</p>
|
|
||||||
<button
|
<button
|
||||||
onclick={() => (showDrawer = !showDrawer)}
|
onclick={() => { app.switchWorkspace(name); showWorkspacePicker = false; }}
|
||||||
class="flex items-center gap-1 text-lg font-bold"
|
class="flex min-w-0 flex-1 items-center gap-2 px-2 py-1.5 text-left {name === app.config?.current_workspace ? 'font-bold' : ''}"
|
||||||
>
|
>
|
||||||
{app.activeList?.title ?? "Tasks"}
|
{#if name === app.config?.current_workspace}
|
||||||
<svg class="h-4 w-4 opacity-50 transition-transform {showDrawer ? 'rotate-180' : ''}" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-4 w-4 shrink-0 opacity-50" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path
|
<path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" />
|
||||||
fill-rule="evenodd"
|
</svg>
|
||||||
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
|
{/if}
|
||||||
/>
|
<div class="min-w-0 flex-1">
|
||||||
|
<p class="truncate text-sm">{name}</p>
|
||||||
|
<p class="truncate text-xs opacity-40">{ws?.path ?? ""}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
<div class="relative shrink-0" data-ws-menu>
|
||||||
|
<button
|
||||||
|
onclick={(e) => { e.stopPropagation(); wsMenuName = wsMenuName === name ? null : name; }}
|
||||||
|
class="rounded p-1 opacity-0 transition-opacity group-hover:opacity-40 hover:!opacity-80 {wsMenuName === name ? '!opacity-80' : ''}"
|
||||||
|
>
|
||||||
|
<svg class="h-3.5 w-3.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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
{#if wsMenuName === name}
|
||||||
|
<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={() => { wsMenuName = null; app.removeWorkspace(name); }}
|
||||||
|
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>
|
||||||
|
{/each}
|
||||||
|
<div class="mt-1 border-t border-border-light px-1 pt-1 dark:border-border-dark">
|
||||||
|
<button
|
||||||
|
onclick={() => { showWorkspacePicker = false; app.setScreen("setup"); }}
|
||||||
|
class="w-full rounded-md px-2 py-1.5 text-left text-sm text-primary hover:bg-primary/5"
|
||||||
|
>
|
||||||
|
+ Add workspace
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-2">
|
<!-- Settings gear -->
|
||||||
{#if app.syncing}
|
|
||||||
<div class="h-5 w-5 animate-spin rounded-full border-2 border-primary border-t-transparent"></div>
|
|
||||||
{/if}
|
|
||||||
<button
|
<button
|
||||||
onclick={openSettings}
|
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"
|
||||||
>
|
>
|
||||||
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
<svg class="h-5 w-5 opacity-50 hover:opacity-80" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<path
|
<path
|
||||||
fill-rule="evenodd"
|
fill-rule="evenodd"
|
||||||
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
|
d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z"
|
||||||
|
|
@ -242,6 +283,44 @@
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main content panel -->
|
||||||
|
<div class="relative flex h-full w-screen shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
||||||
|
<!-- Dim overlay when drawer is open -->
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 z-30 transition-opacity duration-250 ease-out {showDrawer ? '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)"
|
||||||
|
onclick={closeDrawer}
|
||||||
|
onkeydown={(e) => { if (e.key === "Escape") closeDrawer(); }}
|
||||||
|
></div>
|
||||||
|
<!-- Header -->
|
||||||
|
<header
|
||||||
|
class="relative flex items-center border-b border-border-light px-4 py-3 dark:border-border-dark"
|
||||||
|
>
|
||||||
|
<!-- Back arrow (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" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Centered title -->
|
||||||
|
<div class="flex-1 text-center">
|
||||||
|
<p class="text-xs text-text-secondary-light dark:text-text-secondary-dark">
|
||||||
|
{app.config?.current_workspace ?? ""}
|
||||||
|
</p>
|
||||||
|
<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>
|
||||||
|
{/if}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Task list -->
|
<!-- Task list -->
|
||||||
|
|
@ -275,10 +354,12 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if app.completedTasks.length > 0}
|
{#if app.completedTasks.length > 0}
|
||||||
|
<div class="h-4"></div>
|
||||||
<button
|
<button
|
||||||
onclick={() => (showCompleted = !showCompleted)}
|
onclick={() => (showCompleted = !showCompleted)}
|
||||||
class="flex w-full items-center gap-2 border-t border-border-light px-4 py-3 text-sm font-medium text-text-secondary-light dark:border-border-dark dark:text-text-secondary-dark"
|
class="flex w-full items-center justify-between border-t border-border-light px-4 py-3 text-sm font-medium text-text-secondary-light transition-colors hover:bg-black/5 dark:border-border-dark dark:text-text-secondary-dark dark:hover:bg-white/5"
|
||||||
>
|
>
|
||||||
|
Completed ({app.completedTasks.length})
|
||||||
<svg
|
<svg
|
||||||
class="h-4 w-4 transition-transform {showCompleted ? 'rotate-90' : ''}"
|
class="h-4 w-4 transition-transform {showCompleted ? 'rotate-90' : ''}"
|
||||||
viewBox="0 0 20 20"
|
viewBox="0 0 20 20"
|
||||||
|
|
@ -289,7 +370,6 @@
|
||||||
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"
|
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>
|
</svg>
|
||||||
Completed ({app.completedTasks.length})
|
|
||||||
</button>
|
</button>
|
||||||
{#if showCompleted}
|
{#if showCompleted}
|
||||||
{#each app.completedTasks as task (task.id)}
|
{#each app.completedTasks as task (task.id)}
|
||||||
|
|
@ -300,9 +380,9 @@
|
||||||
{/if}
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<!-- FAB button, slides with main content -->
|
<!-- FAB button -->
|
||||||
<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 || showSettings ? '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 ? '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; }}
|
||||||
|
|
@ -315,13 +395,29 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Settings panel (sits to the right of main content) -->
|
<!-- Settings popup overlay -->
|
||||||
<div class="flex h-full w-[80vw] shrink-0 flex-col bg-surface-light dark:bg-surface-dark">
|
<!-- 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'}"
|
||||||
|
style="padding: 4%"
|
||||||
|
>
|
||||||
|
<!-- Backdrop -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-black/50"
|
||||||
|
onclick={closeSettings}
|
||||||
|
onkeydown={(e) => { if (e.key === "Escape") closeSettings(); }}
|
||||||
|
></div>
|
||||||
|
<!-- Settings card -->
|
||||||
|
<div
|
||||||
|
class="relative flex h-full w-full flex-col overflow-hidden rounded-2xl bg-surface-light transition-transform duration-200 dark:bg-surface-dark {showSettings ? 'scale-100' : 'scale-95'}"
|
||||||
|
style="border: 1px solid rgba(255,255,255,0.1); box-shadow: 0 25px 60px rgba(0,0,0,0.7), 0 10px 20px rgba(0,0,0,0.5)"
|
||||||
|
>
|
||||||
<SettingsScreen onclose={closeSettings} />
|
<SettingsScreen onclose={closeSettings} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Toast overlay (outside sliding container so it stays centered) -->
|
<!-- Toast overlay (outside sliding container so it stays centered) -->
|
||||||
<div class="pointer-events-none fixed inset-0 z-50">
|
<div class="pointer-events-none fixed inset-0 z-50">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue