mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-26 19:31:25 +01:00
HeaderSize as shared component
This commit is contained in:
95
packages/ui/src/components/HeaderSize.tsx
Normal file
95
packages/ui/src/components/HeaderSize.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
import classNames from 'classnames';
|
||||
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;
|
||||
osType: string;
|
||||
hideWindowControls: boolean;
|
||||
useNativeTitlebar: boolean;
|
||||
interfaceScale: number;
|
||||
}
|
||||
|
||||
export function HeaderSize({
|
||||
className,
|
||||
style,
|
||||
size,
|
||||
ignoreControlsSpacing,
|
||||
onlyXWindowControl,
|
||||
children,
|
||||
hideControls,
|
||||
osType,
|
||||
hideWindowControls,
|
||||
useNativeTitlebar,
|
||||
interfaceScale,
|
||||
}: HeaderSizeProps) {
|
||||
const isFullscreen = useIsFullscreen();
|
||||
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 (useNativeTitlebar) {
|
||||
// No style updates when using native titlebar
|
||||
} else if (osType === 'macos') {
|
||||
if (!isFullscreen) {
|
||||
// Add large padding for window controls
|
||||
s.paddingLeft = 72 / interfaceScale;
|
||||
}
|
||||
} else if (!ignoreControlsSpacing && !hideWindowControls) {
|
||||
s.paddingRight = WINDOW_CONTROLS_WIDTH;
|
||||
}
|
||||
|
||||
return s;
|
||||
}, [
|
||||
ignoreControlsSpacing,
|
||||
isFullscreen,
|
||||
hideWindowControls,
|
||||
interfaceScale,
|
||||
size,
|
||||
style,
|
||||
useNativeTitlebar,
|
||||
osType,
|
||||
]);
|
||||
|
||||
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
|
||||
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
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{!hideControls && !useNativeTitlebar && (
|
||||
<WindowControls
|
||||
onlyX={onlyXWindowControl}
|
||||
osType={osType}
|
||||
hideWindowControls={hideWindowControls}
|
||||
useNativeTitlebar={useNativeTitlebar}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
90
packages/ui/src/components/WindowControls.tsx
Normal file
90
packages/ui/src/components/WindowControls.tsx
Normal file
@@ -0,0 +1,90 @@
|
||||
import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow';
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { WINDOW_CONTROLS_WIDTH } from '../lib/constants';
|
||||
import { Button } from './Button';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
onlyX?: boolean;
|
||||
osType: string;
|
||||
hideWindowControls: boolean;
|
||||
useNativeTitlebar: boolean;
|
||||
}
|
||||
|
||||
export function WindowControls({ className, onlyX, osType, hideWindowControls, useNativeTitlebar }: Props) {
|
||||
const [maximized, setMaximized] = useState<boolean>(false);
|
||||
|
||||
// Never show controls on macOS or if hideWindowControls is true
|
||||
if (osType === 'macos' || hideWindowControls || useNativeTitlebar) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<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
|
||||
>
|
||||
{!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>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
12
packages/ui/src/hooks/useDebouncedState.ts
Normal file
12
packages/ui/src/hooks/useDebouncedState.ts
Normal 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];
|
||||
}
|
||||
8
packages/ui/src/hooks/useDebouncedValue.ts
Normal file
8
packages/ui/src/hooks/useDebouncedValue.ts
Normal 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;
|
||||
}
|
||||
22
packages/ui/src/hooks/useIsFullscreen.ts
Normal file
22
packages/ui/src/hooks/useIsFullscreen.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
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
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
4
packages/ui/src/lib/constants.ts
Normal file
4
packages/ui/src/lib/constants.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export const HEADER_SIZE_MD = '27px';
|
||||
export const HEADER_SIZE_LG = '40px';
|
||||
|
||||
export const WINDOW_CONTROLS_WIDTH = '10.5rem';
|
||||
Reference in New Issue
Block a user