HeaderSize as shared component

This commit is contained in:
Gregory Schier
2026-03-07 07:32:58 -08:00
parent 6f9e4ada15
commit ff6686f982
20 changed files with 165 additions and 72 deletions

View File

@@ -1,83 +0,0 @@
import { type } from '@tauri-apps/plugin-os';
import { settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
import { useMemo } from 'react';
import { useIsFullscreen } from '../hooks/useIsFullscreen';
import { HEADER_SIZE_LG, HEADER_SIZE_MD, WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { WindowControls } from './WindowControls';
interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
size: 'md' | 'lg';
ignoreControlsSpacing?: boolean;
onlyXWindowControl?: boolean;
hideControls?: boolean;
}
export function HeaderSize({
className,
style,
size,
ignoreControlsSpacing,
onlyXWindowControl,
children,
hideControls,
}: HeaderSizeProps) {
const settings = useAtomValue(settingsAtom);
const isFullscreen = useIsFullscreen();
const nativeTitlebar = settings.useNativeTitlebar;
const finalStyle = useMemo<CSSProperties>(() => {
const s = { ...style };
// Set the height (use min-height because scaling font size may make it larger
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
if (nativeTitlebar) {
// No style updates when using native titlebar
} else if (type() === 'macos') {
if (!isFullscreen) {
// Add large padding for window controls
s.paddingLeft = 72 / settings.interfaceScale;
}
} else if (!ignoreControlsSpacing && !settings.hideWindowControls) {
s.paddingRight = WINDOW_CONTROLS_WIDTH;
}
return s;
}, [
ignoreControlsSpacing,
isFullscreen,
settings.hideWindowControls,
settings.interfaceScale,
size,
style,
nativeTitlebar,
]);
return (
<div
data-tauri-drag-region
style={finalStyle}
className={classNames(
className,
'pt-[1px]', // Make up for bottom border
'select-none relative',
'w-full border-b border-border-subtle min-w-0',
)}
>
{/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */}
<div
className={classNames(
'pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid',
'px-1', // Give it some space on either end for focus outlines
)}
>
{children}
</div>
{!hideControls && !nativeTitlebar && <WindowControls onlyX={onlyXWindowControl} />}
</div>
);
}

View File

@@ -12,7 +12,7 @@ import { CountBadge } from '../core/CountBadge';
import { Icon } from '../core/Icon';
import { HStack } from '../core/Stacks';
import { TabContent, type TabItem, Tabs } from '../core/Tabs/Tabs';
import { HeaderSize } from '../HeaderSize';
import { HeaderSize } from '@yaakapp-internal/ui';
import { SettingsCertificates } from './SettingsCertificates';
import { SettingsGeneral } from './SettingsGeneral';
import { SettingsHotkeys } from './SettingsHotkeys';
@@ -77,6 +77,10 @@ export default function Settings({ hide }: Props) {
onlyXWindowControl
size="md"
className="x-theme-appHeader bg-surface text-text-subtle flex items-center justify-center border-b border-border-subtle text-sm font-semibold"
osType={type()}
hideWindowControls={settings.hideWindowControls}
useNativeTitlebar={settings.useNativeTitlebar}
interfaceScale={settings.interfaceScale}
>
<HStack
space={2}

View File

@@ -1,93 +0,0 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { type } from '@tauri-apps/plugin-os';
import { settingsAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { useState } from 'react';
import { WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { Button } from './core/Button';
import { HStack } from './core/Stacks';
interface Props {
className?: string;
onlyX?: boolean;
macos?: boolean;
}
export function WindowControls({ className, onlyX }: Props) {
const [maximized, setMaximized] = useState<boolean>(false);
const settings = useAtomValue(settingsAtom);
// Never show controls on macOS or if hideWindowControls is true
if (type() === 'macos' || settings.hideWindowControls || settings.useNativeTitlebar) {
return null;
}
return (
<HStack
className={classNames(className, 'ml-4 absolute right-0 top-0 bottom-0')}
justifyContent="end"
style={{ width: WINDOW_CONTROLS_WIDTH }}
data-tauri-drag-region
>
{!onlyX && (
<>
<Button
className="!h-full px-4 text-text-subtle hocus:text hocus:bg-surface-highlight rounded-none"
color="custom"
onClick={() => getCurrentWebviewWindow().minimize()}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Minimize</title>
<path fill="currentColor" d="M14 8v1H3V8z" />
</svg>
</Button>
<Button
className="!h-full px-4 text-text-subtle hocus:text hocus:bg-surface-highlight rounded-none"
color="custom"
onClick={async () => {
const w = getCurrentWebviewWindow();
const isMaximized = await w.isMaximized();
if (isMaximized) {
await w.unmaximize();
setMaximized(false);
} else {
await w.maximize();
setMaximized(true);
}
}}
>
{maximized ? (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Unmaximize</title>
<g fill="currentColor">
<path d="M3 5v9h9V5zm8 8H4V6h7z" />
<path fillRule="evenodd" d="M5 5h1V4h7v7h-1v1h2V3H5z" clipRule="evenodd" />
</g>
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Maximize</title>
<path fill="currentColor" d="M3 3v10h10V3zm9 9H4V4h8z" />
</svg>
)}
</Button>
</>
)}
<Button
color="custom"
className="!h-full px-4 text-text-subtle rounded-none hocus:bg-danger hocus:text-text"
onClick={() => getCurrentWebviewWindow().close()}
>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<title>Close</title>
<path
fill="currentColor"
fillRule="evenodd"
d="m7.116 8l-4.558 4.558l.884.884L8 8.884l4.558 4.558l.884-.884L8.884 8l4.558-4.558l-.884-.884L8 7.116L3.442 2.558l-.884.884z"
clipRule="evenodd"
/>
</svg>
</Button>
</HStack>
);
}

View File

@@ -1,4 +1,5 @@
import { workspacesAtom } from '@yaakapp-internal/models';
import { type } from '@tauri-apps/plugin-os';
import { settingsAtom, workspacesAtom } from '@yaakapp-internal/models';
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import * as m from 'motion/react-m';
@@ -39,7 +40,7 @@ import { HStack } from './core/Stacks';
import { ErrorBoundary } from './ErrorBoundary';
import { FolderLayout } from './FolderLayout';
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
import { HeaderSize } from './HeaderSize';
import { HeaderSize } from '@yaakapp-internal/ui';
import { HttpRequestLayout } from './HttpRequestLayout';
import { Overlay } from './Overlay';
import type { ResizeHandleEvent } from './ResizeHandle';
@@ -59,6 +60,8 @@ export function Workspace() {
useGlobalWorkspaceHooks();
const workspaces = useAtomValue(workspacesAtom);
const settings = useAtomValue(settingsAtom);
const osType = type();
const [width, setWidth, resetWidth] = useSidebarWidth();
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
@@ -146,7 +149,7 @@ export function Workspace() {
'grid grid-rows-[auto_1fr]',
)}
>
<HeaderSize hideControls size="lg" className="border-transparent flex items-center">
<HeaderSize hideControls size="lg" className="border-transparent flex items-center" osType={osType} hideWindowControls={settings.hideWindowControls} useNativeTitlebar={settings.useNativeTitlebar} interfaceScale={settings.interfaceScale}>
<SidebarActions />
</HeaderSize>
<ErrorBoundary name="Sidebar (Floating)">
@@ -178,6 +181,10 @@ export function Workspace() {
size="lg"
className="relative x-theme-appHeader bg-surface"
style={head}
osType={osType}
hideWindowControls={settings.hideWindowControls}
useNativeTitlebar={settings.useNativeTitlebar}
interfaceScale={settings.interfaceScale}
>
<div className="absolute inset-0 pointer-events-none">
<div // Add subtle background

View File

@@ -1,22 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
import { useWindowSize } from 'react-use';
import { useDebouncedValue } from './useDebouncedValue';
export function useIsFullscreen() {
const windowSize = useWindowSize();
const debouncedWindowWidth = useDebouncedValue(windowSize.width);
// NOTE: Fullscreen state isn't updated right after resize event on Mac (needs to wait for animation) so
// we'll wait for a bit using the debounced window size. Hopefully Tauri eventually adds a way to listen
// for fullscreen change events.
return (
useQuery({
queryKey: ['is_fullscreen', debouncedWindowWidth],
queryFn: async () => {
return getCurrentWebviewWindow().isFullscreen();
},
}).data ?? false
);
}

View File

@@ -1,5 +1,5 @@
import { type } from '@tauri-apps/plugin-os';
import { useIsFullscreen } from './useIsFullscreen';
import { useIsFullscreen } from '@yaakapp-internal/ui';
export function useStoplightsVisible() {
const fullscreen = useIsFullscreen();

View File

@@ -1,4 +0,0 @@
export const HEADER_SIZE_MD = '27px';
export const HEADER_SIZE_LG = '40px';
export const WINDOW_CONTROLS_WIDTH = '10.5rem';

View File

@@ -1,10 +1,14 @@
import "./main.css";
import { Button } from "@yaakapp-internal/ui";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { invoke } from "@tauri-apps/api/core";
import { type } from "@tauri-apps/plugin-os";
import { Button, HeaderSize } from "@yaakapp-internal/ui";
import { StrictMode } from "react";
import { useState } from "react";
import { createRoot } from "react-dom/client";
const queryClient = new QueryClient();
type ProxyStartResult = {
port: number;
alreadyRunning: boolean;
@@ -14,6 +18,7 @@ function App() {
const [status, setStatus] = useState("Idle");
const [port, setPort] = useState<number | null>(null);
const [busy, setBusy] = useState(false);
const osType = type();
async function startProxy() {
setBusy(true);
@@ -46,46 +51,61 @@ function App() {
}
return (
<main className="h-full w-full overflow-auto p-6">
<section className="flex items-start">
<div className="flex w-full max-w-xl flex-col gap-4">
<div>
<h1 className="text-2xl font-semibold text-text">Yaak Proxy</h1>
<p className="mt-2 text-sm text-text-subtle">Status: {status}</p>
<p className="mt-1 text-sm text-text-subtle">
Port: {port ?? "Not running"}
</p>
</div>
<div className="flex flex-wrap gap-3">
<Button
disabled={busy}
onClick={startProxy}
size="sm"
tone="primary"
>
Start Proxy
</Button>
<Button
disabled={busy}
onClick={stopProxy}
size="sm"
variant="border"
>
Stop Proxy
</Button>
<Button size="sm" type="button">
Shared Button
</Button>
</div>
<div className="h-full w-full grid grid-rows-[auto_1fr]">
<HeaderSize
size="lg"
osType={osType}
hideWindowControls={false}
useNativeTitlebar={false}
interfaceScale={1}
className="x-theme-appHeader bg-surface"
>
<div
data-tauri-drag-region
className="flex items-center h-full px-2 text-sm font-semibold text-text-subtle"
>
Yaak Proxy
</div>
</section>
</main>
</HeaderSize>
<main className="overflow-auto p-6">
<section className="flex items-start">
<div className="flex w-full max-w-xl flex-col gap-4">
<div>
<p className="text-sm text-text-subtle">Status: {status}</p>
<p className="mt-1 text-sm text-text-subtle">
Port: {port ?? "Not running"}
</p>
</div>
<div className="flex flex-wrap gap-3">
<Button
disabled={busy}
onClick={startProxy}
size="sm"
tone="primary"
>
Start Proxy
</Button>
<Button
disabled={busy}
onClick={stopProxy}
size="sm"
variant="border"
>
Stop Proxy
</Button>
</div>
</div>
</section>
</main>
</div>
);
}
createRoot(document.getElementById("root") as HTMLElement).render(
<StrictMode>
<App />
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>
</StrictMode>,
);

View File

@@ -9,9 +9,11 @@
"lint": "tsc --noEmit"
},
"dependencies": {
"@tanstack/react-query": "^5.90.5",
"@tauri-apps/api": "^2.9.1",
"@tauri-apps/plugin-os": "^2.3.2",
"@yaakapp-internal/theme": "^1.0.0",
"@yaakapp-internal/ui": "^1.0.0",
"@tauri-apps/api": "^2.9.1",
"react": "^19.1.0",
"react-dom": "^19.1.0"
},

View File

@@ -15,7 +15,6 @@
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"@yaakapp-internal/theme": ["../../packages/theme/src/index.ts"],
"@yaakapp-internal/theme/*": ["../../packages/theme/src/*"],