mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
feat(desktop): integrate window controls instead of a title-bar strip
Rework the custom Windows chrome from a dedicated full-width title bar (which wasted the top row and stopped the rail/panels reaching the window edge) into an overlay: minimize/maximize/close pinned to the top-right corner, with the app's own chrome reserving that corner. - WindowTitleBar is a fixed top-right overlay; it publishes --wincontrols-w and --wincontrols-h for the app to reserve against. - AppFrame/AppLayout revert to full height, so the rail, sidebars, workbench and search run all the way to the top again. - Reserve the controls' corner wherever app chrome reaches the window's right edge: the tool-panel header (PDF Tools + active tools like Automate), the collapsed panel's expand toggle, and the files-page header. - Drag the window (and double-click-maximize) from any non-interactive spot in the top strip via a document-level hit test, since data-tauri-drag-region only fires on bare container backgrounds. All reservations key off --wincontrols-w (0 unless the Windows custom chrome is active), so macOS/Linux/web are unaffected.
This commit is contained in:
@@ -22,15 +22,7 @@ export function AppLayout({ children }: AppLayoutProps) {
|
||||
}
|
||||
`}</style>
|
||||
<div
|
||||
style={{
|
||||
// Viewport height minus the desktop custom title bar. --titlebar-h is
|
||||
// unset (0) on web/macOS/Linux, so this stays 100dvh there. Kept
|
||||
// viewport-relative on purpose: an ancestor (ThemeProvider) is only
|
||||
// min-height:100vh, so a percentage height here would collapse.
|
||||
height: "calc(100dvh - var(--titlebar-h, 0px))",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
style={{ height: "100dvh", display: "flex", flexDirection: "column" }}
|
||||
>
|
||||
{banner}
|
||||
<div style={{ flex: 1, minHeight: 0, height: 0 }}>{children}</div>
|
||||
|
||||
@@ -21,7 +21,9 @@
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
min-height: 48px;
|
||||
padding: 0 0.75rem;
|
||||
/* Right padding also clears the desktop window-controls overlay so the whole
|
||||
grid (search + actions) reflows inside it (0 unless Windows custom chrome). */
|
||||
padding: 0 calc(0.75rem + var(--wincontrols-w, 0px)) 0 0.75rem;
|
||||
border-bottom: 1px solid var(--c-border-subtle);
|
||||
background: var(--c-bg-raised);
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
/* ========== APP FRAME ========== */
|
||||
/* A column: the optional custom title bar (desktop), then the rail + app row. */
|
||||
/* The rail's column, then whichever app is mounted, so a switch changes only the app. */
|
||||
.app-frame {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
height: 100dvh; /* track mobile browser chrome */
|
||||
overflow: hidden;
|
||||
background-color: var(--c-bg);
|
||||
}
|
||||
|
||||
/* The rail's column, then whichever app is mounted, so a switch changes only the app. */
|
||||
.app-frame__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* min-width: 0 so the app shrinks instead of forcing the frame past the window. */
|
||||
.app-frame__content {
|
||||
flex: 1;
|
||||
|
||||
@@ -11,17 +11,15 @@ export function AppFrame() {
|
||||
return (
|
||||
<QuickNavHostProvider>
|
||||
<div className="app-frame">
|
||||
{/* Desktop (Windows) custom window chrome; null on web + macOS/Linux. It
|
||||
spans the full width above the rail so the panel dividers meet a clean
|
||||
edge instead of the native caption. */}
|
||||
{/* Desktop (Windows) window controls, drawn as a fixed overlay in the
|
||||
top-right corner so the rail and panels run to the window edge; null
|
||||
on web + macOS/Linux. */}
|
||||
<WindowTitleBar />
|
||||
<div className="app-frame__body">
|
||||
<QuickNavRailHost />
|
||||
<div className="app-frame__content">
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
<QuickNavRailHost />
|
||||
<div className="app-frame__content">
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</QuickNavHostProvider>
|
||||
|
||||
@@ -31,7 +31,11 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding-top: 10px;
|
||||
/* Extra top space keeps the expand toggle clear of the desktop window
|
||||
controls overlay. Derive the ~control-height from --wincontrols-w (which is
|
||||
138px when the custom Windows chrome is active, 0 otherwise) via min(), so
|
||||
it needs only that one variable. */
|
||||
padding-top: calc(10px + min(var(--wincontrols-w, 0px), 2rem));
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
@@ -157,9 +161,19 @@
|
||||
gap: 0.5rem;
|
||||
min-height: 52px;
|
||||
padding: 0.5rem 0.75rem;
|
||||
/* Keep the collapse toggle clear of the desktop window-controls overlay
|
||||
(--wincontrols-w is 0 unless the custom Windows title bar is active). */
|
||||
padding-right: calc(0.75rem + var(--wincontrols-w, 0px));
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Active-tool header (e.g. Automate) sits at the panel's top-right too; keep its
|
||||
close button clear of the window-controls overlay. Scoped to the tool panel so
|
||||
the shared header elsewhere (AI chat, etc.) is untouched. */
|
||||
.tool-panel .sui-panelhdr__close {
|
||||
margin-right: var(--wincontrols-w, 0px);
|
||||
}
|
||||
|
||||
.tool-panel__compact-title {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
/* Custom Windows title bar (see WindowTitleBar.tsx). Full-width strip above the
|
||||
rail + panels; the drag region is the empty area, controls sit top-right. */
|
||||
/* Custom Windows window controls (see WindowTitleBar.tsx): a fixed cluster in
|
||||
the top-right corner, overlaying the app chrome — which reserves the corner
|
||||
via --wincontrols-w. Kept above app overlays so the controls stay usable. */
|
||||
.titleBar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
z-index: 1500;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: flex-end;
|
||||
flex: 0 0 auto;
|
||||
height: var(--titlebar-h, 2rem);
|
||||
background: var(--c-bg);
|
||||
height: 2rem;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
/* Keep the window controls above app overlays so they stay usable. */
|
||||
position: relative;
|
||||
z-index: 1500;
|
||||
}
|
||||
|
||||
.controls {
|
||||
|
||||
@@ -16,10 +16,11 @@ const seedIsWindows =
|
||||
typeof navigator !== "undefined" && /Windows/i.test(navigator.userAgent);
|
||||
|
||||
/**
|
||||
* Custom in-app window title bar for the Windows desktop build. The native
|
||||
* caption is removed in Rust (decorations:false), so this draws the themed drag
|
||||
* region + minimize/maximize/close controls in its place, letting the app chrome
|
||||
* run edge to edge. Renders nothing on macOS/Linux (native decorations kept) and
|
||||
* Custom window controls for the Windows desktop build. The native caption is
|
||||
* removed in Rust (decorations:false), so this draws minimize/maximize/close as
|
||||
* a fixed overlay pinned to the top-right corner — the rail and panels run all
|
||||
* the way to the window edge, and the app chrome reserves the corner via
|
||||
* --wincontrols-w. Renders nothing on macOS/Linux (native decorations kept) and
|
||||
* in the browser build (core stub). tao provides edge/corner resize for the
|
||||
* undecorated window, so no manual resize handles are needed here.
|
||||
*/
|
||||
@@ -43,18 +44,21 @@ export function WindowTitleBar() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Reserve the bar's height for the rest of the app. AppLayout sizes itself as
|
||||
// calc(100dvh - var(--titlebar-h)); everywhere else the var is unset (0).
|
||||
// Layout effect so it lands before paint and nothing overflows for a frame.
|
||||
// The controls are a fixed overlay in the top-right corner. Publish their
|
||||
// width so app chrome at the top-right (the tool panel header) can pad clear
|
||||
// of them; unset (0) everywhere else. Layout effect so it lands before paint.
|
||||
useIsomorphicEffect(() => {
|
||||
const root = document.documentElement;
|
||||
if (active) {
|
||||
root.style.setProperty("--titlebar-h", "2rem");
|
||||
root.style.setProperty("--wincontrols-w", "8.625rem"); // 3 x 46px
|
||||
root.style.setProperty("--wincontrols-h", "2rem");
|
||||
} else {
|
||||
root.style.removeProperty("--titlebar-h");
|
||||
root.style.removeProperty("--wincontrols-w");
|
||||
root.style.removeProperty("--wincontrols-h");
|
||||
}
|
||||
return () => {
|
||||
root.style.removeProperty("--titlebar-h");
|
||||
root.style.removeProperty("--wincontrols-w");
|
||||
root.style.removeProperty("--wincontrols-h");
|
||||
};
|
||||
}, [active]);
|
||||
|
||||
@@ -75,6 +79,36 @@ export function WindowTitleBar() {
|
||||
return () => unlisten?.();
|
||||
}, [active]);
|
||||
|
||||
// Let the window be dragged (and double-click-maximized) from any
|
||||
// non-interactive spot in the top strip. data-tauri-drag-region only fires
|
||||
// when the bare container is the click target, which leaves most of a busy
|
||||
// toolbar undraggable; a document-level hit test covers the whole top.
|
||||
useEffect(() => {
|
||||
if (!active) return;
|
||||
const TOP_STRIP_PX = 48;
|
||||
const INTERACTIVE =
|
||||
"button, a[href], input, textarea, select, label, summary," +
|
||||
'[role="button"], [role="tab"], [role="menuitem"], [role="switch"],' +
|
||||
'[role="slider"], [contenteditable="true"], [data-no-window-drag]';
|
||||
const draggableAt = (e: MouseEvent) => {
|
||||
if (e.button !== 0 || e.clientY > TOP_STRIP_PX) return false;
|
||||
const el = e.target as Element | null;
|
||||
return !!el && !el.closest(INTERACTIVE);
|
||||
};
|
||||
const onMouseDown = (e: MouseEvent) => {
|
||||
if (draggableAt(e)) void getCurrentWindow().startDragging();
|
||||
};
|
||||
const onDoubleClick = (e: MouseEvent) => {
|
||||
if (draggableAt(e)) void getCurrentWindow().toggleMaximize();
|
||||
};
|
||||
document.addEventListener("mousedown", onMouseDown);
|
||||
document.addEventListener("dblclick", onDoubleClick);
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", onMouseDown);
|
||||
document.removeEventListener("dblclick", onDoubleClick);
|
||||
};
|
||||
}, [active]);
|
||||
|
||||
if (!active) return null;
|
||||
|
||||
const appWindow = getCurrentWindow();
|
||||
|
||||
Reference in New Issue
Block a user