mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-11 17:46:41 -05:00
Floating sidebar
This commit is contained in:
@@ -7,7 +7,8 @@ license = "MIT"
|
||||
repository = "https://github.com/gschier/yaak-app"
|
||||
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]
|
||||
tauri-build = { version = "1.2", features = [] }
|
||||
|
||||
@@ -491,8 +491,9 @@ fn main() {
|
||||
|
||||
create_dir_all(dir.clone()).expect("Problem creating App directory!");
|
||||
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);
|
||||
println!("Connecting to database at {}", url);
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect(url.as_str())
|
||||
|
||||
BIN
src-web/assets/icons/Icons.afdesign
Normal file
BIN
src-web/assets/icons/Icons.afdesign
Normal file
Binary file not shown.
41
src-web/components/Overlay.tsx
Normal file
41
src-web/components/Overlay.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,10 +1,20 @@
|
||||
import classnames from 'classnames';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { motion } from 'framer-motion';
|
||||
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 { WINDOW_FLOATING_SIDEBAR_WIDTH } from '../lib/constants';
|
||||
import { Overlay } from './Overlay';
|
||||
import { RequestResponse } from './RequestResponse';
|
||||
import { ResizeHandle } from './ResizeHandle';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { SidebarDisplayToggle } from './SidebarDisplayToggle';
|
||||
import { WorkspaceHeader } from './WorkspaceHeader';
|
||||
|
||||
const side = { gridArea: 'side' };
|
||||
@@ -14,12 +24,23 @@ const drag = { gridArea: 'drag' };
|
||||
|
||||
export default function Workspace() {
|
||||
const sidebar = useSidebarDisplay();
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const [floating, setFloating] = useState<boolean>(false);
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH) {
|
||||
setFloating(true);
|
||||
sidebar.hide();
|
||||
} else {
|
||||
setFloating(false);
|
||||
sidebar.show();
|
||||
}
|
||||
}, [windowSize.width]);
|
||||
|
||||
const unsub = () => {
|
||||
if (moveState.current !== null) {
|
||||
document.documentElement.removeEventListener('mousemove', moveState.current.move);
|
||||
@@ -55,46 +76,86 @@ export default function Workspace() {
|
||||
const sideWidth = sidebar.hidden ? 0 : sidebar.width;
|
||||
const styles = useMemo<CSSProperties>(
|
||||
() => ({
|
||||
gridTemplate: `
|
||||
gridTemplate: floating
|
||||
? `
|
||||
' ${head.gridArea}' auto
|
||||
' ${body.gridArea}' minmax(0,1fr)
|
||||
/ 1fr`
|
||||
: `
|
||||
' ${head.gridArea} ${head.gridArea} ${head.gridArea}' auto
|
||||
' ${side.gridArea} ${drag.gridArea} ${body.gridArea}' minmax(0,1fr)
|
||||
/ ${sideWidth}px 0 1fr`,
|
||||
}),
|
||||
[sideWidth],
|
||||
[sideWidth, floating],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={styles}
|
||||
className={classnames(
|
||||
'grid w-full h-full',
|
||||
// Animate sidebar width changes but only when not resizing
|
||||
// because it's too slow to animate on mouse move
|
||||
!isResizing && 'transition-all',
|
||||
)}
|
||||
style={styles}
|
||||
>
|
||||
<div
|
||||
<HeaderSize
|
||||
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}
|
||||
>
|
||||
<WorkspaceHeader className="pointer-events-none" />
|
||||
</div>
|
||||
<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}
|
||||
/>
|
||||
</HeaderSize>
|
||||
{floating ? (
|
||||
<Overlay open={!sidebar.hidden} portalName="sidebar" onClick={sidebar.hide}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className={classnames(
|
||||
'absolute top-0 left-0 bottom-0 bg-gray-100 border-r border-highlight w-[14rem]',
|
||||
)}
|
||||
>
|
||||
<HeaderSize className="border-transparent">
|
||||
<SidebarDisplayToggle />
|
||||
</HeaderSize>
|
||||
<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} />
|
||||
</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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import classnames from 'classnames';
|
||||
import { memo } from 'react';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useSidebarDisplay } from '../hooks/useSidebarDisplay';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { RequestSettingsDropdown } from './RequestSettingsDropdown';
|
||||
@@ -15,8 +14,12 @@ interface Props {
|
||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
||||
const activeRequest = useActiveRequest();
|
||||
return (
|
||||
<HStack justifyContent="center" alignItems="center" className={classnames(className, 'h-full')}>
|
||||
<HStack className="flex-1 -ml-2 pointer-events-none" alignItems="center">
|
||||
<HStack
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
className={classnames(className, 'w-full h-full')}
|
||||
>
|
||||
<HStack className="flex-1 pointer-events-none" alignItems="center">
|
||||
<SidebarDisplayToggle />
|
||||
<WorkspaceDropdown className="pointer-events-auto" />
|
||||
</HStack>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classnames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Portal } from '../Portal';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { IconButton } from './IconButton';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
|
||||
@@ -25,43 +25,38 @@ export function Dialog({
|
||||
description,
|
||||
}: Props) {
|
||||
return (
|
||||
<Portal name="dialog">
|
||||
{open && (
|
||||
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
|
||||
<div
|
||||
aria-hidden
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="fixed inset-0 bg-gray-600/60 dark:bg-black/50"
|
||||
/>
|
||||
<div>
|
||||
<div
|
||||
className={classnames(
|
||||
className,
|
||||
'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',
|
||||
wide && 'w-[80vw] max-w-[50rem]',
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => onOpenChange(false)}
|
||||
title="Close dialog"
|
||||
aria-label="Close"
|
||||
icon="x"
|
||||
size="sm"
|
||||
className="ml-auto absolute right-1 top-1"
|
||||
/>
|
||||
<VStack space={3}>
|
||||
<HStack alignItems="center" className="pb-3">
|
||||
<div className="text-xl font-semibold">{title}</div>
|
||||
</HStack>
|
||||
{description && <div>{description}</div>}
|
||||
<div>{children}</div>
|
||||
</VStack>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</Portal>
|
||||
<Overlay open={open} onClick={() => onOpenChange(false)} portalName="dialog">
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="pointer-events-auto">
|
||||
<motion.div
|
||||
initial={{ top: 5, scale: 0.97 }}
|
||||
animate={{ top: 0, scale: 1 }}
|
||||
className={classnames(
|
||||
className,
|
||||
'relative bg-gray-100 pointer-events-auto',
|
||||
'w-[20rem] max-h-[80vh] p-5 rounded-lg overflow-auto',
|
||||
'dark:border border-gray-200 shadow-md shadow-black/10',
|
||||
wide && 'w-[80vw] max-w-[50rem]',
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
onClick={() => onOpenChange(false)}
|
||||
title="Close dialog"
|
||||
aria-label="Close"
|
||||
icon="x"
|
||||
size="sm"
|
||||
className="ml-auto absolute right-1 top-1"
|
||||
/>
|
||||
<VStack space={3}>
|
||||
<HStack alignItems="center" className="pb-3">
|
||||
<div className="text-xl font-semibold">{title}</div>
|
||||
</HStack>
|
||||
{description && <div>{description}</div>}
|
||||
<div>{children}</div>
|
||||
</VStack>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
</Overlay>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,8 +161,8 @@ function Menu({ className, items, onClose, triggerRect }: MenuProps) {
|
||||
<Portal name="dropdown">
|
||||
<button aria-hidden title="close" className="fixed inset-0" onClick={onClose} />
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
dir="ltr"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export const DEFAULT_FONT_SIZE = 16;
|
||||
export const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;
|
||||
|
||||
Reference in New Issue
Block a user