feat: custom TitleBar with window controls (no native decorations) #7

Merged
LyAhn merged 6 commits from feat/custom-titlebar into main 2026-04-06 20:02:45 +00:00
4 changed files with 149 additions and 18 deletions
+5 -1
View File
@@ -12,6 +12,10 @@
"fs:allow-read-file",
copilot-pull-request-reviewer[bot] commented 2026-04-06 13:50:42 +00:00 (Migrated from github.com)
Review

core:window:allow-start-dragging is added to the capability set, but the UI code in this PR doesn’t call startDragging() anywhere (and also doesn’t currently use data-tauri-drag-region). If dragging will be handled via the HTML attribute, consider removing this permission; if dragging will be handled via the API, add the corresponding call in the title bar so the extra permission is justified.


`core:window:allow-start-dragging` is added to the capability set, but the UI code in this PR doesn’t call `startDragging()` anywhere (and also doesn’t currently use `data-tauri-drag-region`). If dragging will be handled via the HTML attribute, consider removing this permission; if dragging will be handled via the API, add the corresponding call in the title bar so the extra permission is justified. ```suggestion ```
"fs:allow-read-dir",
"fs:read-files",
"fs:read-dirs"
"fs:read-dirs",
"core:window:allow-minimize",
"core:window:allow-close",
"core:window:allow-toggle-maximize",
"core:window:allow-is-maximized"
]
copilot-pull-request-reviewer[bot] commented 2026-04-06 13:50:42 +00:00 (Migrated from github.com)
Review

The capabilities add core:window:allow-maximize and core:window:allow-unmaximize, but the UI code introduced here only calls toggleMaximize() (plus minimize(), close(), isMaximized()). To keep least-privilege, consider removing permissions that aren’t required for the current implementation (unless Tauri’s permission model for toggleMaximize implicitly depends on them).

The capabilities add `core:window:allow-maximize` and `core:window:allow-unmaximize`, but the UI code introduced here only calls `toggleMaximize()` (plus `minimize()`, `close()`, `isMaximized()`). To keep least-privilege, consider removing permissions that aren’t required for the current implementation (unless Tauri’s permission model for `toggleMaximize` implicitly depends on them).
}
+3 -1
View File
@@ -16,7 +16,9 @@
"width": 1280,
"height": 800,
"minWidth": 800,
"minHeight": 600
"minHeight": 600,
"decorations": false,
"shadow": true
}
],
"security": {
+9 -1
View File
@@ -6,6 +6,7 @@ import { Toolbar } from "./components/Toolbar";
import { Gallery } from "./components/Gallery";
import { Lightbox } from "./components/Lightbox";
import { TagCloud } from "./components/TagCloud";
import { TitleBar } from "./components/TitleBar";
export default function App() {
const loadFolders = useGalleryStore((state) => state.loadFolders);
@@ -29,7 +30,12 @@ export default function App() {
}, []);
return (
<div className="flex h-screen bg-gray-950 text-white overflow-hidden select-none">
<div className="flex h-screen flex-col bg-gray-950 text-white overflow-hidden select-none">
{/* Custom title bar — sits at the very top */}
<TitleBar />
{/* Main app content below the title bar */}
<div className="flex flex-1 min-h-0">
<Sidebar />
<main className="flex-1 flex flex-col min-w-0">
{activeView === "explore" ? (
@@ -45,6 +51,8 @@ export default function App() {
</>
)}
</main>
</div>
<Lightbox />
</div>
);
+117
View File
@@ -0,0 +1,117 @@
import { useState, useEffect } from "react";
import { getCurrentWindow } from "@tauri-apps/api/window";
// SVG icons for window controls
function MinimizeIcon() {
return (
<svg width="10" height="2" viewBox="0 0 10 2" fill="none">
<rect y="0.5" width="10" height="1" rx="0.5" fill="currentColor" />
</svg>
);
}
function MaximizeIcon() {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
<rect x="0.5" y="0.5" width="9" height="9" rx="1.5" stroke="currentColor" strokeWidth="1" />
</svg>
);
}
function RestoreIcon() {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
<rect x="2.5" y="0.5" width="7" height="7" rx="1.5" stroke="currentColor" strokeWidth="1" />
<rect x="0.5" y="2.5" width="7" height="7" rx="1.5" stroke="currentColor" strokeWidth="1" fill="#030712" />
</svg>
);
}
function CloseIcon() {
return (
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
<path d="M1 1L9 9M9 1L1 9" stroke="currentColor" strokeWidth="1.25" strokeLinecap="round" />
</svg>
);
}
export function TitleBar() {
const [isMaximized, setIsMaximized] = useState(false);
const appWindow = getCurrentWindow();
useEffect(() => {
// Get initial maximized state
appWindow.isMaximized().then(setIsMaximized);
// Listen for resize events to update maximized state
const unlisten = appWindow.onResized(() => {
appWindow.isMaximized().then(setIsMaximized);
});
return () => {
unlisten.then((fn) => fn());
};
}, []);
const handleMinimize = () => appWindow.minimize();
const handleMaximize = () => appWindow.toggleMaximize();
const handleClose = () => appWindow.close();
return (
// data-tauri-drag-region is the recommended Tauri approach for drag regions.
// WebkitAppRegion is kept as a CSS fallback for compatibility.
<div
data-tauri-drag-region
className="titlebar relative z-50 flex h-9 shrink-0 items-center bg-gray-950 select-none"
style={{ WebkitAppRegion: "drag" } as React.CSSProperties}
>
copilot-pull-request-reviewer[bot] commented 2026-04-06 13:50:42 +00:00 (Migrated from github.com)
Review

The title bar drag region is implemented only via WebkitAppRegion: "drag". In Tauri (especially on Windows/Linux with WebView2), this CSS property is not reliably honored; the recommended approach is to mark draggable areas with data-tauri-drag-region (and/or explicitly call appWindow.startDragging() on pointer-down). As-is, the window may not be draggable and the PR description’s data-tauri-drag-region behavior isn’t present.

The title bar drag region is implemented only via `WebkitAppRegion: "drag"`. In Tauri (especially on Windows/Linux with WebView2), this CSS property is not reliably honored; the recommended approach is to mark draggable areas with `data-tauri-drag-region` (and/or explicitly call `appWindow.startDragging()` on pointer-down). As-is, the window may not be draggable and the PR description’s `data-tauri-drag-region` behavior isn’t present.
{/* App icon + name — left side */}
<div className="flex items-center gap-2 pl-3 pr-4">
<div className="flex h-5 w-5 items-center justify-center rounded-md bg-white/8 overflow-hidden">
{/* Phokus logo placeholder — replace with <img src={logo} /> if you have an SVG */}
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
<circle cx="6" cy="6" r="4" stroke="#a78bfa" strokeWidth="1.5" />
<circle cx="6" cy="6" r="1.5" fill="#a78bfa" />
</svg>
</div>
<span className="text-[11px] font-semibold text-gray-400 tracking-wide">Phokus</span>
</div>
{/* Spacer — draggable region fills here */}
<div data-tauri-drag-region className="flex-1" />
{/* Window control buttons — right side, non-draggable */}
<div
className="flex items-stretch h-full"
style={{ WebkitAppRegion: "no-drag" } as React.CSSProperties}
>
{/* Minimize */}
<button
onClick={handleMinimize}
title="Minimize"
className="group flex h-full w-10 items-center justify-center text-gray-600 transition-colors duration-100 hover:bg-white/6 hover:text-gray-300"
>
<MinimizeIcon />
</button>
{/* Maximize / Restore */}
<button
onClick={handleMaximize}
title={isMaximized ? "Restore" : "Maximize"}
className="group flex h-full w-10 items-center justify-center text-gray-600 transition-colors duration-100 hover:bg-white/6 hover:text-gray-300"
>
{isMaximized ? <RestoreIcon /> : <MaximizeIcon />}
</button>
{/* Close */}
<button
onClick={handleClose}
title="Close"
className="group flex h-full w-10 items-center justify-center text-gray-600 transition-colors duration-100 hover:bg-red-500/80 hover:text-white"
>
<CloseIcon />
</button>
</div>
</div>
);
}