Floating sidebar

This commit is contained in:
Gregory Schier
2023-03-26 10:09:28 -07:00
parent cf7ef55b7d
commit d88ae99425
9 changed files with 172 additions and 69 deletions

View File

@@ -7,7 +7,8 @@ license = "MIT"
repository = "https://github.com/gschier/yaak-app" repository = "https://github.com/gschier/yaak-app"
edition = "2021" edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [profile.release]
strip = true # Automatically strip symbols from the binary.
[build-dependencies] [build-dependencies]
tauri-build = { version = "1.2", features = [] } tauri-build = { version = "1.2", features = [] }

View File

@@ -491,8 +491,9 @@ fn main() {
create_dir_all(dir.clone()).expect("Problem creating App directory!"); create_dir_all(dir.clone()).expect("Problem creating App directory!");
let p = dir.join("db.sqlite"); let p = dir.join("db.sqlite");
let p_string = p.to_string_lossy().replace(' ', " % 20"); let p_string = p.to_string_lossy().replace(' ', "%20");
let url = format!("sqlite://{}?mode=rwc", p_string); let url = format!("sqlite://{}?mode=rwc", p_string);
println!("Connecting to database at {}", url);
tauri::async_runtime::block_on(async move { tauri::async_runtime::block_on(async move {
let pool = SqlitePoolOptions::new() let pool = SqlitePoolOptions::new()
.connect(url.as_str()) .connect(url.as_str())

Binary file not shown.

View File

@@ -0,0 +1,41 @@
import classnames from 'classnames';
import type { ReactNode } from 'react';
import { motion } from 'framer-motion';
import { Portal } from './Portal';
interface Props {
children: ReactNode;
onClick?: () => void;
portalName: string;
open: boolean;
zIndex?: keyof typeof zIndexes;
}
const zIndexes: Record<number, string> = {
10: 'z-10',
20: 'z-20',
30: 'z-30',
40: 'z-40',
50: 'z-50',
};
export function Overlay({ zIndex = 30, open, children, onClick, portalName }: Props) {
return (
<Portal name={portalName}>
{open && (
<motion.div
className={classnames('fixed inset-0', zIndexes[zIndex])}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
>
<div
aria-hidden
onClick={onClick}
className="absolute inset-0 bg-gray-600/60 dark:bg-black/50"
/>
{children}
</motion.div>
)}
</Portal>
);
}

View File

@@ -1,10 +1,20 @@
import classnames from 'classnames'; import classnames from 'classnames';
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react'; import { motion } from 'framer-motion';
import React, { useCallback, useMemo, useRef, useState } from 'react'; import type {
CSSProperties,
HTMLAttributes,
MouseEvent as ReactMouseEvent,
ReactNode,
} from 'react';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useWindowSize } from 'react-use';
import { useSidebarDisplay } from '../hooks/useSidebarDisplay'; import { useSidebarDisplay } from '../hooks/useSidebarDisplay';
import { WINDOW_FLOATING_SIDEBAR_WIDTH } from '../lib/constants';
import { Overlay } from './Overlay';
import { RequestResponse } from './RequestResponse'; import { RequestResponse } from './RequestResponse';
import { ResizeHandle } from './ResizeHandle'; import { ResizeHandle } from './ResizeHandle';
import { Sidebar } from './Sidebar'; import { Sidebar } from './Sidebar';
import { SidebarDisplayToggle } from './SidebarDisplayToggle';
import { WorkspaceHeader } from './WorkspaceHeader'; import { WorkspaceHeader } from './WorkspaceHeader';
const side = { gridArea: 'side' }; const side = { gridArea: 'side' };
@@ -14,12 +24,23 @@ const drag = { gridArea: 'drag' };
export default function Workspace() { export default function Workspace() {
const sidebar = useSidebarDisplay(); const sidebar = useSidebarDisplay();
const windowSize = useWindowSize();
const [floating, setFloating] = useState<boolean>(false);
const [isResizing, setIsResizing] = useState<boolean>(false); const [isResizing, setIsResizing] = useState<boolean>(false);
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>( const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
null, null,
); );
useEffect(() => {
if (windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH) {
setFloating(true);
sidebar.hide();
} else {
setFloating(false);
sidebar.show();
}
}, [windowSize.width]);
const unsub = () => { const unsub = () => {
if (moveState.current !== null) { if (moveState.current !== null) {
document.documentElement.removeEventListener('mousemove', moveState.current.move); document.documentElement.removeEventListener('mousemove', moveState.current.move);
@@ -55,46 +76,86 @@ export default function Workspace() {
const sideWidth = sidebar.hidden ? 0 : sidebar.width; const sideWidth = sidebar.hidden ? 0 : sidebar.width;
const styles = useMemo<CSSProperties>( const styles = useMemo<CSSProperties>(
() => ({ () => ({
gridTemplate: ` gridTemplate: floating
? `
' ${head.gridArea}' auto
' ${body.gridArea}' minmax(0,1fr)
/ 1fr`
: `
' ${head.gridArea} ${head.gridArea} ${head.gridArea}' auto ' ${head.gridArea} ${head.gridArea} ${head.gridArea}' auto
' ${side.gridArea} ${drag.gridArea} ${body.gridArea}' minmax(0,1fr) ' ${side.gridArea} ${drag.gridArea} ${body.gridArea}' minmax(0,1fr)
/ ${sideWidth}px 0 1fr`, / ${sideWidth}px 0 1fr`,
}), }),
[sideWidth], [sideWidth, floating],
); );
return ( return (
<div <div
style={styles}
className={classnames( className={classnames(
'grid w-full h-full', 'grid w-full h-full',
// Animate sidebar width changes but only when not resizing // Animate sidebar width changes but only when not resizing
// because it's too slow to animate on mouse move // because it's too slow to animate on mouse move
!isResizing && 'transition-all', !isResizing && 'transition-all',
)} )}
style={styles}
> >
<div <HeaderSize
data-tauri-drag-region data-tauri-drag-region
className="h-md px-3 w-full pl-20 bg-gray-50 border-b border-b-highlight text-gray-900 pt-[1px]" className="w-full bg-gray-50 border-b border-b-highlight text-gray-900"
style={head} style={head}
> >
<WorkspaceHeader className="pointer-events-none" /> <WorkspaceHeader className="pointer-events-none" />
</div> </HeaderSize>
<div {floating ? (
style={side} <Overlay open={!sidebar.hidden} portalName="sidebar" onClick={sidebar.hide}>
className={classnames('overflow-hidden bg-gray-100 border-r border-highlight')} <motion.div
> initial={{ opacity: 0, x: -10 }}
<Sidebar /> animate={{ opacity: 1, x: 0 }}
</div> className={classnames(
<ResizeHandle 'absolute top-0 left-0 bottom-0 bg-gray-100 border-r border-highlight w-[14rem]',
className="-translate-x-3" )}
justify="end" >
side="right" <HeaderSize className="border-transparent">
isResizing={isResizing} <SidebarDisplayToggle />
onResizeStart={handleResizeStart} </HeaderSize>
onReset={sidebar.reset} <Sidebar />
/> </motion.div>
</Overlay>
) : (
<>
<div
style={side}
className={classnames('overflow-hidden bg-gray-100 border-r border-highlight')}
>
<Sidebar />
</div>
<ResizeHandle
className="-translate-x-3"
justify="end"
side="right"
isResizing={isResizing}
onResizeStart={handleResizeStart}
onReset={sidebar.reset}
/>
</>
)}
<RequestResponse style={body} /> <RequestResponse style={body} />
</div> </div>
); );
} }
interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
children: ReactNode;
}
function HeaderSize({ className, ...props }: HeaderSizeProps) {
return (
<div
className={classnames(
className,
'h-md pt-[1px] flex items-center w-full pr-3 pl-20 border-b',
)}
{...props}
/>
);
}

View File

@@ -1,7 +1,6 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { memo } from 'react'; import { memo } from 'react';
import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveRequest } from '../hooks/useActiveRequest';
import { useSidebarDisplay } from '../hooks/useSidebarDisplay';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks'; import { HStack } from './core/Stacks';
import { RequestSettingsDropdown } from './RequestSettingsDropdown'; import { RequestSettingsDropdown } from './RequestSettingsDropdown';
@@ -15,8 +14,12 @@ interface Props {
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) { export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
return ( return (
<HStack justifyContent="center" alignItems="center" className={classnames(className, 'h-full')}> <HStack
<HStack className="flex-1 -ml-2 pointer-events-none" alignItems="center"> justifyContent="center"
alignItems="center"
className={classnames(className, 'w-full h-full')}
>
<HStack className="flex-1 pointer-events-none" alignItems="center">
<SidebarDisplayToggle /> <SidebarDisplayToggle />
<WorkspaceDropdown className="pointer-events-auto" /> <WorkspaceDropdown className="pointer-events-auto" />
</HStack> </HStack>

View File

@@ -1,7 +1,7 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { Portal } from '../Portal'; import { Overlay } from '../Overlay';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import { HStack, VStack } from './Stacks'; import { HStack, VStack } from './Stacks';
@@ -25,43 +25,38 @@ export function Dialog({
description, description,
}: Props) { }: Props) {
return ( return (
<Portal name="dialog"> <Overlay open={open} onClick={() => onOpenChange(false)} portalName="dialog">
{open && ( <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}> <div className="pointer-events-auto">
<div <motion.div
aria-hidden initial={{ top: 5, scale: 0.97 }}
onClick={() => onOpenChange(false)} animate={{ top: 0, scale: 1 }}
className="fixed inset-0 bg-gray-600/60 dark:bg-black/50" className={classnames(
/> className,
<div> 'relative bg-gray-100 pointer-events-auto',
<div 'w-[20rem] max-h-[80vh] p-5 rounded-lg overflow-auto',
className={classnames( 'dark:border border-gray-200 shadow-md shadow-black/10',
className, wide && 'w-[80vw] max-w-[50rem]',
'absolute top-[50%] left-[50%] translate-x-[-50%] translate-y-[-50%] bg-gray-100', )}
'w-[20rem] max-h-[80vh] p-5 rounded-lg overflow-auto', >
'dark:border border-gray-200 shadow-md shadow-black/10', <IconButton
wide && 'w-[80vw] max-w-[50rem]', onClick={() => onOpenChange(false)}
)} title="Close dialog"
> aria-label="Close"
<IconButton icon="x"
onClick={() => onOpenChange(false)} size="sm"
title="Close dialog" className="ml-auto absolute right-1 top-1"
aria-label="Close" />
icon="x" <VStack space={3}>
size="sm" <HStack alignItems="center" className="pb-3">
className="ml-auto absolute right-1 top-1" <div className="text-xl font-semibold">{title}</div>
/> </HStack>
<VStack space={3}> {description && <div>{description}</div>}
<HStack alignItems="center" className="pb-3"> <div>{children}</div>
<div className="text-xl font-semibold">{title}</div> </VStack>
</HStack> </motion.div>
{description && <div>{description}</div>} </div>
<div>{children}</div> </div>
</VStack> </Overlay>
</div>
</div>
</motion.div>
)}
</Portal>
); );
} }

View File

@@ -161,8 +161,8 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
<Portal name="dropdown"> <Portal name="dropdown">
<button aria-hidden title="close" className="fixed inset-0" onClick={onClose} /> <button aria-hidden title="close" className="fixed inset-0" onClick={onClose} />
<motion.div <motion.div
initial={{ opacity: 0, y: -10 }} initial={{ opacity: 0, y: -5, scale: 0.98 }}
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0, scale: 1 }}
role="menu" role="menu"
aria-orientation="vertical" aria-orientation="vertical"
dir="ltr" dir="ltr"

View File

@@ -1 +1,2 @@
export const DEFAULT_FONT_SIZE = 16; export const DEFAULT_FONT_SIZE = 16;
export const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;