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", "serde",
"tauri", "tauri",
"tauri-build", "tauri-build",
"tauri-plugin-os",
"yaak-proxy", "yaak-proxy",
"yaak-window", "yaak-window",
] ]

View File

@@ -12,7 +12,7 @@ import { CountBadge } from '../core/CountBadge';
import { Icon } from '../core/Icon'; import { Icon } from '../core/Icon';
import { HStack } from '../core/Stacks'; import { HStack } from '../core/Stacks';
import { TabContent, type TabItem, Tabs } from '../core/Tabs/Tabs'; import { TabContent, type TabItem, Tabs } from '../core/Tabs/Tabs';
import { HeaderSize } from '../HeaderSize'; import { HeaderSize } from '@yaakapp-internal/ui';
import { SettingsCertificates } from './SettingsCertificates'; import { SettingsCertificates } from './SettingsCertificates';
import { SettingsGeneral } from './SettingsGeneral'; import { SettingsGeneral } from './SettingsGeneral';
import { SettingsHotkeys } from './SettingsHotkeys'; import { SettingsHotkeys } from './SettingsHotkeys';
@@ -77,6 +77,10 @@ export default function Settings({ hide }: Props) {
onlyXWindowControl onlyXWindowControl
size="md" 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" 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 <HStack
space={2} 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 classNames from 'classnames';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import * as m from 'motion/react-m'; import * as m from 'motion/react-m';
@@ -39,7 +40,7 @@ import { HStack } from './core/Stacks';
import { ErrorBoundary } from './ErrorBoundary'; import { ErrorBoundary } from './ErrorBoundary';
import { FolderLayout } from './FolderLayout'; import { FolderLayout } from './FolderLayout';
import { GrpcConnectionLayout } from './GrpcConnectionLayout'; import { GrpcConnectionLayout } from './GrpcConnectionLayout';
import { HeaderSize } from './HeaderSize'; import { HeaderSize } from '@yaakapp-internal/ui';
import { HttpRequestLayout } from './HttpRequestLayout'; import { HttpRequestLayout } from './HttpRequestLayout';
import { Overlay } from './Overlay'; import { Overlay } from './Overlay';
import type { ResizeHandleEvent } from './ResizeHandle'; import type { ResizeHandleEvent } from './ResizeHandle';
@@ -59,6 +60,8 @@ export function Workspace() {
useGlobalWorkspaceHooks(); useGlobalWorkspaceHooks();
const workspaces = useAtomValue(workspacesAtom); const workspaces = useAtomValue(workspacesAtom);
const settings = useAtomValue(settingsAtom);
const osType = type();
const [width, setWidth, resetWidth] = useSidebarWidth(); const [width, setWidth, resetWidth] = useSidebarWidth();
const [sidebarHidden, setSidebarHidden] = useSidebarHidden(); const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden(); const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
@@ -146,7 +149,7 @@ export function Workspace() {
'grid grid-rows-[auto_1fr]', '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 /> <SidebarActions />
</HeaderSize> </HeaderSize>
<ErrorBoundary name="Sidebar (Floating)"> <ErrorBoundary name="Sidebar (Floating)">
@@ -178,6 +181,10 @@ export function Workspace() {
size="lg" size="lg"
className="relative x-theme-appHeader bg-surface" className="relative x-theme-appHeader bg-surface"
style={head} style={head}
osType={osType}
hideWindowControls={settings.hideWindowControls}
useNativeTitlebar={settings.useNativeTitlebar}
interfaceScale={settings.interfaceScale}
> >
<div className="absolute inset-0 pointer-events-none"> <div className="absolute inset-0 pointer-events-none">
<div // Add subtle background <div // Add subtle background

View File

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

View File

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

View File

@@ -9,9 +9,11 @@
"lint": "tsc --noEmit" "lint": "tsc --noEmit"
}, },
"dependencies": { "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/theme": "^1.0.0",
"@yaakapp-internal/ui": "^1.0.0", "@yaakapp-internal/ui": "^1.0.0",
"@tauri-apps/api": "^2.9.1",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0"
}, },

View File

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

View File

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

View File

@@ -5,6 +5,14 @@
"*" "*"
], ],
"permissions": [ "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() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_os::init())
.manage(ProxyState::default()) .manage(ProxyState::default())
.invoke_handler(tauri::generate_handler![proxy_metadata, proxy_start, proxy_stop]) .invoke_handler(tauri::generate_handler![proxy_metadata, proxy_start, proxy_stop])
.build(tauri::generate_context!()) .build(tauri::generate_context!())

10
package-lock.json generated
View File

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

View File

@@ -6,8 +6,13 @@
"main": "src/index.ts", "main": "src/index.ts",
"types": "src/index.ts", "types": "src/index.ts",
"dependencies": { "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": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0",
"react-use": "^17.6.0"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^19.1.0", "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 classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties, HTMLAttributes, ReactNode } from 'react'; import type { CSSProperties, HTMLAttributes, ReactNode } from 'react';
import { useMemo } from 'react'; import { useMemo } from 'react';
import { useIsFullscreen } from '../hooks/useIsFullscreen'; import { useIsFullscreen } from '../hooks/useIsFullscreen';
@@ -14,6 +11,10 @@ interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
ignoreControlsSpacing?: boolean; ignoreControlsSpacing?: boolean;
onlyXWindowControl?: boolean; onlyXWindowControl?: boolean;
hideControls?: boolean; hideControls?: boolean;
osType: string;
hideWindowControls: boolean;
useNativeTitlebar: boolean;
interfaceScale: number;
} }
export function HeaderSize({ export function HeaderSize({
@@ -24,10 +25,12 @@ export function HeaderSize({
onlyXWindowControl, onlyXWindowControl,
children, children,
hideControls, hideControls,
osType,
hideWindowControls,
useNativeTitlebar,
interfaceScale,
}: HeaderSizeProps) { }: HeaderSizeProps) {
const settings = useAtomValue(settingsAtom);
const isFullscreen = useIsFullscreen(); const isFullscreen = useIsFullscreen();
const nativeTitlebar = settings.useNativeTitlebar;
const finalStyle = useMemo<CSSProperties>(() => { const finalStyle = useMemo<CSSProperties>(() => {
const s = { ...style }; const s = { ...style };
@@ -35,14 +38,14 @@ export function HeaderSize({
if (size === 'md') s.minHeight = HEADER_SIZE_MD; if (size === 'md') s.minHeight = HEADER_SIZE_MD;
if (size === 'lg') s.minHeight = HEADER_SIZE_LG; if (size === 'lg') s.minHeight = HEADER_SIZE_LG;
if (nativeTitlebar) { if (useNativeTitlebar) {
// No style updates when using native titlebar // No style updates when using native titlebar
} else if (type() === 'macos') { } else if (osType === 'macos') {
if (!isFullscreen) { if (!isFullscreen) {
// Add large padding for window controls // 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; s.paddingRight = WINDOW_CONTROLS_WIDTH;
} }
@@ -50,11 +53,12 @@ export function HeaderSize({
}, [ }, [
ignoreControlsSpacing, ignoreControlsSpacing,
isFullscreen, isFullscreen,
settings.hideWindowControls, hideWindowControls,
settings.interfaceScale, interfaceScale,
size, size,
style, style,
nativeTitlebar, useNativeTitlebar,
osType,
]); ]);
return ( return (
@@ -70,6 +74,7 @@ export function HeaderSize({
> >
{/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */} {/* NOTE: This needs display:grid or else the element shrinks (even though scrollable) */}
<div <div
data-tauri-drag-region
className={classNames( className={classNames(
'pointer-events-none h-full w-full overflow-x-auto hide-scrollbars grid', '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 'px-1', // Give it some space on either end for focus outlines
@@ -77,7 +82,14 @@ export function HeaderSize({
> >
{children} {children}
</div> </div>
{!hideControls && !nativeTitlebar && <WindowControls onlyX={onlyXWindowControl} />} {!hideControls && !useNativeTitlebar && (
<WindowControls
onlyX={onlyXWindowControl}
osType={osType}
hideWindowControls={hideWindowControls}
useNativeTitlebar={useNativeTitlebar}
/>
)}
</div> </div>
); );
} }

View File

@@ -1,31 +1,28 @@
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; 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 classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { useState } from 'react'; import { useState } from 'react';
import { WINDOW_CONTROLS_WIDTH } from '../lib/constants'; import { WINDOW_CONTROLS_WIDTH } from '../lib/constants';
import { Button } from './core/Button'; import { Button } from './Button';
import { HStack } from './core/Stacks';
interface Props { interface Props {
className?: string; className?: string;
onlyX?: boolean; 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 [maximized, setMaximized] = useState<boolean>(false);
const settings = useAtomValue(settingsAtom);
// Never show controls on macOS or if hideWindowControls is true // 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 null;
} }
return ( return (
<HStack <div
className={classNames(className, 'ml-4 absolute right-0 top-0 bottom-0')} className={classNames(className, 'ml-4 absolute right-0 top-0 bottom-0 flex items-center justify-end')}
justifyContent="end"
style={{ width: WINDOW_CONTROLS_WIDTH }} style={{ width: WINDOW_CONTROLS_WIDTH }}
data-tauri-drag-region data-tauri-drag-region
> >
@@ -88,6 +85,6 @@ export function WindowControls({ className, onlyX }: Props) {
/> />
</svg> </svg>
</Button> </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 { Button } from "./components/Button";
export type { ButtonProps } 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", "target": "es2021",
"lib": ["DOM", "DOM.Iterable", "ESNext"], "lib": ["DOM", "DOM.Iterable", "ESNext"],
"strict": true, "strict": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Node", "moduleResolution": "Node",
"jsx": "react-jsx", "jsx": "react-jsx",