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",
|
||||
"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>
|
||||
|
|
@ -30,3 +32,4 @@
|
|||
{/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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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