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

1
Cargo.lock generated
View File

@@ -10309,6 +10309,7 @@ dependencies = [
"serde",
"tauri",
"tauri-build",
"tauri-plugin-os",
"yaak-proxy",
"yaak-window",
]

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,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,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,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/*"],

View File

@@ -16,5 +16,6 @@ tauri-build = { version = "2.5.3", features = [] }
log = { workspace = true }
serde = { workspace = true, features = ["derive"] }
tauri = { workspace = true }
tauri-plugin-os = "2.3.2"
yaak-proxy = { workspace = true }
yaak-window = { workspace = true }

View File

@@ -5,6 +5,14 @@
"*"
],
"permissions": [
"core:default"
"core:default",
"os:allow-os-type",
"core:window:allow-close",
"core:window:allow-is-fullscreen",
"core:window:allow-is-maximized",
"core:window:allow-maximize",
"core:window:allow-minimize",
"core:window:allow-start-dragging",
"core:window:allow-unmaximize"
]
}

View File

@@ -58,6 +58,7 @@ fn proxy_stop(state: State<'_, ProxyState>) -> Result<bool, String> {
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_os::init())
.manage(ProxyState::default())
.invoke_handler(tauri::generate_handler![proxy_metadata, proxy_start, proxy_stop])
.build(tauri::generate_context!())

10
package-lock.json generated
View File

@@ -224,7 +224,9 @@
"name": "@yaakapp/yaak-proxy",
"version": "1.0.0",
"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",
"react": "^19.1.0",
@@ -16143,6 +16145,7 @@
}
},
"packages/theme": {
"name": "@yaakapp-internal/theme",
"version": "1.0.0",
"dependencies": {
"@tauri-apps/api": "^2.9.1",
@@ -16154,8 +16157,13 @@
"name": "@yaakapp-internal/ui",
"version": "1.0.0",
"dependencies": {
"@tanstack/react-query": "^5.90.5",
"@tauri-apps/api": "^2.9.1",
"@yaakapp-internal/lib": "^1.0.0",
"classnames": "^2.5.1",
"react": "^19.1.0",
"react-dom": "^19.1.0"
"react-dom": "^19.1.0",
"react-use": "^17.6.0"
},
"peerDependencies": {
"react": "^19.1.0",

View File

@@ -6,8 +6,13 @@
"main": "src/index.ts",
"types": "src/index.ts",
"dependencies": {
"@tanstack/react-query": "^5.90.5",
"@tauri-apps/api": "^2.9.1",
"@yaakapp-internal/lib": "^1.0.0",
"classnames": "^2.5.1",
"react": "^19.1.0",
"react-dom": "^19.1.0"
"react-dom": "^19.1.0",
"react-use": "^17.6.0"
},
"peerDependencies": {
"react": "^19.1.0",

View File

@@ -1,7 +1,4 @@
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';
@@ -14,6 +11,10 @@ interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
ignoreControlsSpacing?: boolean;
onlyXWindowControl?: boolean;
hideControls?: boolean;
osType: string;
hideWindowControls: boolean;
useNativeTitlebar: boolean;
interfaceScale: number;
}
export function HeaderSize({
@@ -24,10 +25,12 @@ export function HeaderSize({
onlyXWindowControl,
children,
hideControls,
osType,
hideWindowControls,
useNativeTitlebar,
interfaceScale,
}: HeaderSizeProps) {
const settings = useAtomValue(settingsAtom);
const isFullscreen = useIsFullscreen();
const nativeTitlebar = settings.useNativeTitlebar;
const finalStyle = useMemo<CSSProperties>(() => {
const s = { ...style };
@@ -35,14 +38,14 @@ export function HeaderSize({
if (size === 'md') s.minHeight = HEADER_SIZE_MD;
if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
if (nativeTitlebar) {
if (useNativeTitlebar) {
// No style updates when using native titlebar
} else if (type() === 'macos') {
} else if (osType === 'macos') {
if (!isFullscreen) {
// Add large padding for window controls
s.paddingLeft = 72 / settings.interfaceScale;
s.paddingLeft = 72 / interfaceScale;
}
} else if (!ignoreControlsSpacing && !settings.hideWindowControls) {
} else if (!ignoreControlsSpacing && !hideWindowControls) {
s.paddingRight = WINDOW_CONTROLS_WIDTH;
}
@@ -50,11 +53,12 @@ export function HeaderSize({
}, [
ignoreControlsSpacing,
isFullscreen,
settings.hideWindowControls,
settings.interfaceScale,
hideWindowControls,
interfaceScale,
size,
style,
nativeTitlebar,
useNativeTitlebar,
osType,
]);
return (
@@ -70,6 +74,7 @@ export function HeaderSize({
>
{/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */}
<div
data-tauri-drag-region
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
@@ -77,7 +82,14 @@ export function HeaderSize({
>
{children}
</div>
{!hideControls && !nativeTitlebar && <WindowControls onlyX={onlyXWindowControl} />}
{!hideControls && !useNativeTitlebar && (
<WindowControls
onlyX={onlyXWindowControl}
osType={osType}
hideWindowControls={hideWindowControls}
useNativeTitlebar={useNativeTitlebar}
/>
)}
</div>
);
}

View File

@@ -1,31 +1,28 @@
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';
import { Button } from './Button';
interface Props {
className?: string;
onlyX?: boolean;
macos?: boolean;
osType: string;
hideWindowControls: boolean;
useNativeTitlebar: boolean;
}
export function WindowControls({ className, onlyX }: Props) {
export function WindowControls({ className, onlyX, osType, hideWindowControls, useNativeTitlebar }: 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) {
if (osType === 'macos' || hideWindowControls || useNativeTitlebar) {
return null;
}
return (
<HStack
className={classNames(className, 'ml-4 absolute right-0 top-0 bottom-0')}
justifyContent="end"
<div
className={classNames(className, 'ml-4 absolute right-0 top-0 bottom-0 flex items-center justify-end')}
style={{ width: WINDOW_CONTROLS_WIDTH }}
data-tauri-drag-region
>
@@ -88,6 +85,6 @@ export function WindowControls({ className, onlyX }: Props) {
/>
</svg>
</Button>
</HStack>
</div>
);
}

View File

@@ -0,0 +1,12 @@
import { debounce } from '@yaakapp-internal/lib';
import type { Dispatch, SetStateAction } from 'react';
import { useMemo, useState } from 'react';
export function useDebouncedState<T>(
defaultValue: T,
delay = 500,
): [T, Dispatch<SetStateAction<T>>, Dispatch<SetStateAction<T>>] {
const [state, setState] = useState<T>(defaultValue);
const debouncedSetState = useMemo(() => debounce(setState, delay), [delay]);
return [state, debouncedSetState, setState];
}

View File

@@ -0,0 +1,8 @@
import { useEffect } from 'react';
import { useDebouncedState } from './useDebouncedState';
export function useDebouncedValue<T>(value: T, delay = 500) {
const [state, setState] = useDebouncedState<T>(value, delay);
useEffect(() => setState(value), [setState, value]);
return state;
}

View File

@@ -1,2 +1,8 @@
export { Button } from "./components/Button";
export type { ButtonProps } from "./components/Button";
export { HeaderSize } from "./components/HeaderSize";
export { WindowControls } from "./components/WindowControls";
export { useIsFullscreen } from "./hooks/useIsFullscreen";
export { useDebouncedValue } from "./hooks/useDebouncedValue";
export { useDebouncedState } from "./hooks/useDebouncedState";
export { HEADER_SIZE_MD, HEADER_SIZE_LG, WINDOW_CONTROLS_WIDTH } from "./lib/constants";

View File

@@ -3,6 +3,8 @@
"target": "es2021",
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"strict": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"module": "ESNext",
"moduleResolution": "Node",
"jsx": "react-jsx",