Frameless transparent window with custom title bar
Remove native window decorations and add transparent background with rounded corners, border, and drop shadow. Add custom close button (Linux) and minimize/maximize/close (Windows) in the header. Add programmatic window dragging via mousedown and double-click to maximize. Install tauri-plugin-os for platform detection. Move sync spinner to bottom-right corner. Convert drawer layout from vw to cqi units to support the padding-based shadow approach.
This commit is contained in:
parent
e5c78ddfde
commit
ea76f65579
12
apps/tauri/package-lock.json
generated
12
apps/tauri/package-lock.json
generated
|
|
@ -9,7 +9,8 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@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": {
|
"devDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||||
|
|
@ -1421,6 +1422,15 @@
|
||||||
"@tauri-apps/api": "^2.8.0"
|
"@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": {
|
"node_modules/@types/estree": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tauri-apps/api": "^2.0.0",
|
"@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]
|
[dependencies]
|
||||||
tauri = { version = "2", features = [] }
|
tauri = { version = "2", features = [] }
|
||||||
tauri-plugin-dialog = "2"
|
tauri-plugin-dialog = "2"
|
||||||
|
tauri-plugin-os = "2"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
bevy-tasks-core = { path = "../../../crates/bevy-tasks-core" }
|
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()
|
tauri::Builder::default()
|
||||||
.plugin(tauri_plugin_dialog::init())
|
.plugin(tauri_plugin_dialog::init())
|
||||||
|
.plugin(tauri_plugin_os::init())
|
||||||
.manage(Mutex::new(AppState { config, repo: None }))
|
.manage(Mutex::new(AppState { config, repo: None }))
|
||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_config,
|
get_config,
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@
|
||||||
"height": 700,
|
"height": 700,
|
||||||
"minWidth": 320,
|
"minWidth": 320,
|
||||||
"minHeight": 500,
|
"minHeight": 500,
|
||||||
"resizable": true
|
"resizable": true,
|
||||||
|
"decorations": false,
|
||||||
|
"transparent": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"security": {
|
"security": {
|
||||||
|
|
|
||||||
|
|
@ -11,22 +11,25 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={app.darkMode ? "dark" : ""}>
|
<div class={app.darkMode ? "dark" : ""}>
|
||||||
<div
|
<div class="h-screen w-screen p-2">
|
||||||
class="h-screen w-screen overflow-hidden bg-surface-light text-text-light dark:bg-surface-dark dark:text-text-dark"
|
<div
|
||||||
>
|
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"
|
||||||
{#if app.error}
|
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)"
|
||||||
<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"
|
{#if app.error}
|
||||||
>
|
<div
|
||||||
<span>{app.error}</span>
|
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"
|
||||||
<button onclick={() => app.clearError()} class="ml-2 font-bold">✕</button>
|
>
|
||||||
</div>
|
<span>{app.error}</span>
|
||||||
{/if}
|
<button onclick={() => app.clearError()} class="ml-2 font-bold">✕</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if app.screen === "setup"}
|
{#if app.screen === "setup"}
|
||||||
<SetupScreen />
|
<SetupScreen />
|
||||||
{:else}
|
{:else}
|
||||||
<TasksScreen />
|
<TasksScreen />
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,11 +24,16 @@
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Scrollbar styling */
|
/* Scrollbar styling */
|
||||||
|
|
@ -42,3 +47,9 @@ body {
|
||||||
.dark ::-webkit-scrollbar-thumb {
|
.dark ::-webkit-scrollbar-thumb {
|
||||||
background: #4b5563;
|
background: #4b5563;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Select dropdown theming */
|
||||||
|
.dark select option {
|
||||||
|
background-color: #242424;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,28 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
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 TaskDetailView from "../components/TaskDetailView.svelte";
|
||||||
import NewTaskInput, { newTaskState } from "../components/NewTaskInput.svelte";
|
import NewTaskInput, { newTaskState } from "../components/NewTaskInput.svelte";
|
||||||
import SettingsScreen from "./SettingsScreen.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 showDrawer = $state(false);
|
||||||
let showSettings = $state(false);
|
let showSettings = $state(false);
|
||||||
|
|
@ -23,8 +43,8 @@
|
||||||
window.addEventListener("mousedown", handleWindowClick);
|
window.addEventListener("mousedown", handleWindowClick);
|
||||||
}
|
}
|
||||||
let newListName = $state("");
|
let newListName = $state("");
|
||||||
let showCompleted = $state(true);
|
let showCompleted = $state(false);
|
||||||
let completedVisible = $state(true);
|
let completedVisible = $state(false);
|
||||||
let listMenuId = $state<string | null>(null);
|
let listMenuId = $state<string | null>(null);
|
||||||
let wsMenuName = $state<string | null>(null);
|
let wsMenuName = $state<string | null>(null);
|
||||||
let dragId = $state<string | null>(null);
|
let dragId = $state<string | null>(null);
|
||||||
|
|
@ -113,19 +133,25 @@
|
||||||
showSettings = false;
|
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 workspaceNames = $derived(app.config ? Object.keys(app.config.workspaces) : []);
|
||||||
let translateX = $derived(showDrawer ? '0' : '-80vw');
|
let translateX = $derived(showDrawer ? '0' : '-80cqi');
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- Viewport clip -->
|
<!-- Viewport clip -->
|
||||||
<div class="h-screen w-screen overflow-hidden">
|
<div class="h-full w-full overflow-hidden">
|
||||||
<!-- Sliding container: left drawer + main content -->
|
<!-- 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 + 80vw); transform: translateX({translateX})"
|
style="width: calc(100cqi + 80cqi); transform: translateX({translateX})"
|
||||||
>
|
>
|
||||||
<!-- Drawer panel -->
|
<!-- 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 -->
|
<!-- List items + new list button -->
|
||||||
<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)}
|
||||||
|
|
@ -287,7 +313,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 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 h-full shrink-0 overflow-hidden bg-surface-light dark:bg-surface-dark" style="width: 100cqi">
|
||||||
<!-- Dim overlay when drawer is open -->
|
<!-- Dim overlay when drawer is open -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<div
|
||||||
|
|
@ -296,115 +322,167 @@
|
||||||
onclick={closeDrawer}
|
onclick={closeDrawer}
|
||||||
onkeydown={(e) => { if (e.key === "Escape") closeDrawer(); }}
|
onkeydown={(e) => { if (e.key === "Escape") closeDrawer(); }}
|
||||||
></div>
|
></div>
|
||||||
<!-- Header -->
|
|
||||||
<header
|
<!-- Sliding inner: task list + detail view -->
|
||||||
class="relative flex items-center border-b border-border-light px-4 py-3 dark:border-border-dark"
|
<div
|
||||||
|
class="flex h-full {resizing ? '' : 'transition-transform duration-250'} ease-out"
|
||||||
|
style="width: 200%; transform: translateX({selectedTask ? '-50%' : '0'})"
|
||||||
>
|
>
|
||||||
<!-- Back arrow (left) -->
|
<!-- Sub-panel: Task list -->
|
||||||
<button
|
<div class="relative flex h-full w-1/2 flex-col">
|
||||||
onclick={() => (showDrawer = !showDrawer)}
|
<!-- Header / drag region -->
|
||||||
class="absolute left-2 rounded-lg p-1.5 hover:bg-black/5 dark:hover:bg-white/10"
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
>
|
<header
|
||||||
<svg class="h-5 w-5 opacity-60" viewBox="0 0 20 20" fill="currentColor">
|
onmousedown={handleHeaderMouseDown}
|
||||||
<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" />
|
ondblclick={() => { if (isDesktop) appWindow.toggleMaximize(); }}
|
||||||
</svg>
|
class="relative flex items-center border-b border-border-light px-4 py-3 dark:border-border-dark"
|
||||||
</button>
|
>
|
||||||
|
<!-- Drawer toggle (left) -->
|
||||||
<!-- 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>
|
|
||||||
|
|
||||||
<!-- Task list -->
|
|
||||||
<main class="flex-1 overflow-y-auto">
|
|
||||||
{#if app.lists.length === 0}
|
|
||||||
<div class="flex h-full flex-col items-center justify-center p-8 text-center">
|
|
||||||
<p class="text-lg font-medium opacity-60">No lists yet</p>
|
|
||||||
<p class="mt-1 text-sm opacity-40">Tap the list name above to create one</p>
|
|
||||||
</div>
|
|
||||||
{:else if !app.activeListId}
|
|
||||||
<div class="flex h-full items-center justify-center opacity-40">
|
|
||||||
Select a list
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
{#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="{dragId === task.id ? 'opacity-30' : ''} {dragOverId === task.id && dragId !== task.id ? 'border-t-2 border-t-primary' : ''}"
|
|
||||||
>
|
|
||||||
<TaskItem {task} />
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
{#if app.pendingTasks.length === 0}
|
|
||||||
<div class="p-8 text-center text-sm opacity-40">No tasks. Add one below.</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if app.completedTasks.length > 0}
|
|
||||||
<div class="h-4"></div>
|
|
||||||
<button
|
<button
|
||||||
onclick={() => {
|
onclick={() => (showDrawer = !showDrawer)}
|
||||||
if (showCompleted) {
|
class="absolute left-2 rounded-lg p-1.5 hover:bg-black/5 dark:hover:bg-white/10"
|
||||||
showCompleted = false;
|
|
||||||
setTimeout(() => (completedVisible = false), 300);
|
|
||||||
} else {
|
|
||||||
completedVisible = true;
|
|
||||||
requestAnimationFrame(() => (showCompleted = true));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
class="relative z-10 flex w-full items-center justify-between border-t border-border-light bg-surface-light px-4 py-3 text-sm font-medium text-text-secondary-light transition-colors hover:bg-black/5 dark:border-border-dark dark:bg-surface-dark dark:text-text-secondary-dark dark:hover:bg-white/5"
|
|
||||||
>
|
>
|
||||||
Completed ({app.completedTasks.length})
|
<svg class="h-5 w-5 opacity-60" viewBox="0 0 20 20" fill="currentColor">
|
||||||
<svg
|
<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" />
|
||||||
class="h-4 w-4 transition-transform {showCompleted ? 'rotate-90' : ''}"
|
|
||||||
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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
{#if completedVisible}
|
|
||||||
<div class="transition-all duration-300 ease-out {showCompleted ? 'opacity-100 translate-y-0' : 'opacity-0 -translate-y-4'}">
|
<!-- Centered title -->
|
||||||
{#each app.completedTasks as task (task.id)}
|
<div class="flex-1 text-center">
|
||||||
<TaskItem {task} />
|
<p class="text-xs text-text-secondary-light dark:text-text-secondary-dark">
|
||||||
{/each}
|
{app.config?.current_workspace ?? ""}
|
||||||
|
</p>
|
||||||
|
<p class="text-lg font-bold">{app.activeList?.title ?? "Tasks"}</p>
|
||||||
|
</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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
</header>
|
||||||
{/if}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
<!-- FAB button -->
|
<!-- Task list -->
|
||||||
<div
|
<main class="flex-1 overflow-y-auto">
|
||||||
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'}"
|
{#if app.lists.length === 0}
|
||||||
>
|
<div class="flex h-full flex-col items-center justify-center p-8 text-center">
|
||||||
<button
|
<p class="text-lg font-medium opacity-60">No lists yet</p>
|
||||||
onclick={() => { if (app.activeListId) newTaskState.open = true; }}
|
<p class="mt-1 text-sm opacity-40">Tap the list name above to create one</p>
|
||||||
disabled={!app.activeListId}
|
</div>
|
||||||
class="pointer-events-auto flex h-14 w-14 items-center justify-center rounded-full bg-primary text-white shadow-lg transition-transform hover:scale-105 active:scale-95 disabled:opacity-40 disabled:shadow-none"
|
{:else if !app.activeListId}
|
||||||
>
|
<div class="flex h-full items-center justify-center opacity-40">
|
||||||
<svg class="h-7 w-7" viewBox="0 0 20 20" fill="currentColor">
|
Select a list
|
||||||
<path d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" />
|
</div>
|
||||||
</svg>
|
{:else}
|
||||||
</button>
|
{#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="{dragId === task.id ? 'opacity-30' : ''} {dragOverId === task.id && dragId !== task.id ? 'border-t-2 border-t-primary' : ''}"
|
||||||
|
>
|
||||||
|
<TaskItem {task} onopen={openTask} />
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
{#if app.pendingTasks.length === 0}
|
||||||
|
<div class="p-8 text-center text-sm opacity-40">No tasks. Add one below.</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if app.completedTasks.length > 0}
|
||||||
|
<div class="h-4"></div>
|
||||||
|
<button
|
||||||
|
onclick={() => {
|
||||||
|
if (showCompleted) {
|
||||||
|
showCompleted = false;
|
||||||
|
setTimeout(() => (completedVisible = false), 300);
|
||||||
|
} else {
|
||||||
|
completedVisible = true;
|
||||||
|
requestAnimationFrame(() => (showCompleted = true));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
class="relative z-10 flex w-full items-center justify-between border-t border-border-light bg-surface-light px-4 py-3 text-sm font-medium text-text-secondary-light transition-colors hover:bg-black/5 dark:border-border-dark dark:bg-surface-dark dark:text-text-secondary-dark dark:hover:bg-white/5"
|
||||||
|
>
|
||||||
|
Completed ({app.completedTasks.length})
|
||||||
|
<svg
|
||||||
|
class="h-4 w-4 transition-transform {showCompleted ? 'rotate-90' : ''}"
|
||||||
|
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 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} onopen={openTask} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
{/if}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<!-- FAB button -->
|
||||||
|
<div
|
||||||
|
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; }}
|
||||||
|
disabled={!app.activeListId}
|
||||||
|
class="pointer-events-auto flex h-14 w-14 items-center justify-center rounded-full bg-primary text-white shadow-lg transition-transform hover:scale-105 active:scale-95 disabled:opacity-40 disabled:shadow-none"
|
||||||
|
>
|
||||||
|
<svg class="h-7 w-7" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" />
|
||||||
|
</svg>
|
||||||
|
</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>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -412,7 +490,7 @@
|
||||||
<!-- Settings popup overlay -->
|
<!-- Settings popup overlay -->
|
||||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
<div
|
<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%"
|
style="padding: 4%"
|
||||||
>
|
>
|
||||||
<!-- Backdrop -->
|
<!-- Backdrop -->
|
||||||
|
|
@ -431,6 +509,6 @@
|
||||||
</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 absolute inset-0 z-50">
|
||||||
<NewTaskInput />
|
<NewTaskInput />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue