Zoom, better sizes, color picker, sidebar footer

This commit is contained in:
Gregory Schier
2023-03-08 19:22:04 -08:00
parent c37cfaf0e4
commit 6c8f4c943a
26 changed files with 424 additions and 239 deletions

View File

@@ -1,65 +1,57 @@
import classnames from 'classnames';
import type {
ButtonHTMLAttributes,
ComponentPropsWithoutRef,
ElementType,
ForwardedRef,
} from 'react';
import type { ButtonHTMLAttributes, ForwardedRef } from 'react';
import { forwardRef } from 'react';
import { Icon } from './Icon';
const colorStyles = {
default: 'hover:bg-gray-700/10 text-gray-700 hover:text-gray-1000',
gray: 'text-gray-800 bg-gray-100 hover:bg-gray-500/20 hover:text-gray-1000',
primary: 'bg-blue-400 text-white',
secondary: 'bg-violet-400 text-white',
warning: 'bg-orange-400 text-white',
danger: 'bg-red-400 text-white',
custom: '',
default: 'text-gray-700 enabled:hover:bg-gray-700/10 enabled:hover:text-gray-1000',
gray: 'text-gray-800 bg-gray-100 enabled:hover:bg-gray-500/20 enabled:hover:text-gray-1000',
primary: 'bg-blue-400 text-white hover:bg-blue-500',
secondary: 'bg-violet-400 text-white hover:bg-violet-500',
warning: 'bg-orange-400 text-white hover:bg-orange-500',
danger: 'bg-red-400 text-white hover:bg-red-500',
};
export type ButtonProps<T extends ElementType> = ButtonHTMLAttributes<HTMLButtonElement> & {
export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
color?: keyof typeof colorStyles;
size?: 'xs' | 'sm' | 'md';
size?: 'sm' | 'md';
justify?: 'start' | 'center';
forDropdown?: boolean;
as?: T;
};
export const Button = forwardRef(function Button<T extends ElementType>(
export const Button = forwardRef(function Button(
{
className,
as,
justify = 'center',
children,
size = 'md',
forDropdown,
color,
justify = 'center',
size = 'md',
type = 'button',
...props
}: ButtonProps<T> & Omit<ComponentPropsWithoutRef<T>, keyof ButtonProps<T>>,
}: ButtonProps,
ref: ForwardedRef<HTMLButtonElement>,
) {
const Component = as || 'button';
return (
<Component
<button
ref={ref}
type="button"
type={type}
className={classnames(
className,
'outline-none',
'border border-transparent focus-visible:border-blue-300',
'transition-all rounded-md flex items-center',
'bg-opacity-90 hover:bg-opacity-100',
colorStyles[color || 'default'],
justify === 'start' && 'justify-start',
justify === 'center' && 'justify-center',
size === 'md' && 'h-10 px-4',
size === 'sm' && 'h-8 px-3 text-sm',
size === 'xs' && 'h-7 px-2.5 text-sm',
size === 'md' && 'h-9 px-3',
size === 'sm' && 'h-7 px-2.5 text-sm',
)}
{...props}
>
{children}
{forDropdown && <Icon icon="triangleDown" className="ml-1 -mr-1" />}
</Component>
</button>
);
});

View File

@@ -1,10 +1,25 @@
import classnames from 'classnames';
import type { LinkProps } from 'react-router-dom';
import { Link } from 'react-router-dom';
import type { ButtonProps } from './Button';
import { Button } from './Button';
type Props = ButtonProps<typeof Link> & LinkProps;
type Props = ButtonProps & LinkProps;
export function ButtonLink({ ...props }: Props) {
return <Button as={Link} {...props} />;
export function ButtonLink({
reloadDocument,
replace,
state,
preventScrollReset,
relative,
to,
className,
...buttonProps
}: Props) {
const linkProps = { reloadDocument, replace, state, preventScrollReset, relative, to };
return (
<Link {...linkProps}>
<Button className={classnames(className, 'w-full')} {...buttonProps} />
</Link>
);
}

View File

@@ -43,7 +43,7 @@ export function Dialog({
<IconButton aria-label="Close" icon="x" size="sm" />
</D.Close>
<VStack space={3}>
<HStack items="center" className="pb-3">
<HStack alignItems="center" className="pb-3">
<D.Title className="text-xl font-semibold">{title}</D.Title>
</HStack>
{description && <D.Description>{description}</D.Description>}

View File

@@ -25,19 +25,15 @@
}
.cm-gutters {
@apply border-0 text-gray-500 text-opacity-30;
@apply border-0 text-gray-500/60;
.cm-gutterElement {
@apply cursor-default;
}
}
&.cm-focused .cm-gutters {
@apply text-opacity-60;
}
.placeholder-widget {
@apply text-[0.9em] text-gray-800 dark:text-gray-1000 px-1 rounded cursor-default dark:shadow;
@apply text-[0.9em] text-gray-800 dark:text-gray-900 px-1 rounded cursor-default dark:shadow;
/* NOTE: Background and border are translucent so we can see text selection through it */
@apply bg-gray-300/40 border border-gray-300 border-opacity-40 hover:border-opacity-80;
@@ -105,11 +101,12 @@
}
.cm-editor .fold-gutter-icon:hover {
@apply text-gray-400 bg-gray-100/20;
@apply text-gray-900 bg-gray-300/50;
}
.cm-editor .cm-foldPlaceholder {
@apply px-2 border border-gray-200 bg-gray-100;
@apply px-2 border border-gray-400/50 bg-gray-300/50 cursor-default;
@apply hover:text-gray-800 hover:border-gray-400;
}
.cm-editor .cm-activeLineGutter,

View File

@@ -34,7 +34,6 @@ import {
} from '@codemirror/view';
import { tags as t } from '@lezer/highlight';
import { debouncedAutocompletionDisplay } from './autocomplete';
import { readOnlyTransactionFilter } from './readOnlyTransactionFilter';
import { twig } from './twig/extension';
import { url } from './url/extension';

View File

@@ -106,7 +106,7 @@ function FormRow({
defaultValue={value}
onChange={onChangeValue}
/>
{onDelete && <IconButton size="sm" icon="trash" onClick={onDelete} />}
{onDelete && <IconButton icon="trash" onClick={onDelete} />}
</HStack>
{addSubmit && <input type="submit" value="Add" className="sr-only" />}
</div>

View File

@@ -4,11 +4,13 @@ import {
CheckIcon,
ClockIcon,
CodeIcon,
ColorWheelIcon,
Cross2Icon,
EyeOpenIcon,
GearIcon,
HomeIcon,
MoonIcon,
ListBulletIcon,
PaperPlaneIcon,
PlusCircledIcon,
PlusIcon,
@@ -19,59 +21,40 @@ import {
TriangleLeftIcon,
TriangleRightIcon,
UpdateIcon,
RowsIcon,
} from '@radix-ui/react-icons';
import classnames from 'classnames';
import type { NamedExoticComponent } from 'react';
type IconName =
| 'archive'
| 'home'
| 'camera'
| 'gear'
| 'eye'
| 'triangleDown'
| 'triangleLeft'
| 'triangleRight'
| 'paperPlane'
| 'update'
| 'question'
| 'check'
| 'plus'
| 'plusCircle'
| 'clock'
| 'sun'
| 'code'
| 'x'
| 'trash'
| 'moon';
const icons: Record<IconName, NamedExoticComponent<{ className: string }>> = {
paperPlane: PaperPlaneIcon,
triangleDown: TriangleDownIcon,
plus: PlusIcon,
plusCircle: PlusCircledIcon,
clock: ClockIcon,
const icons = {
archive: ArchiveIcon,
camera: CameraIcon,
check: CheckIcon,
triangleLeft: TriangleLeftIcon,
triangleRight: TriangleRightIcon,
clock: ClockIcon,
code: CodeIcon,
colorWheel: ColorWheelIcon,
eye: EyeOpenIcon,
gear: GearIcon,
home: HomeIcon,
update: UpdateIcon,
sun: SunIcon,
listBullet: ListBulletIcon,
moon: MoonIcon,
x: Cross2Icon,
paperPlane: PaperPlaneIcon,
plus: PlusIcon,
plusCircle: PlusCircledIcon,
question: QuestionMarkIcon,
eye: EyeOpenIcon,
code: CodeIcon,
rows: RowsIcon,
sun: SunIcon,
trash: TrashIcon,
triangleDown: TriangleDownIcon,
triangleLeft: TriangleLeftIcon,
triangleRight: TriangleRightIcon,
update: UpdateIcon,
x: Cross2Icon,
};
export interface IconProps {
icon: IconName;
icon: keyof typeof icons;
className?: string;
size?: 'md';
size?: 'xs' | 'sm' | 'md';
spin?: boolean;
}
@@ -83,6 +66,8 @@ export function Icon({ icon, spin, size = 'md', className }: IconProps) {
className,
'text-gray-800',
size === 'md' && 'h-4 w-4',
size === 'sm' && 'h-3 w-3',
size === 'xs' && 'h-2 w-2',
spin && 'animate-spin',
)}
/>

View File

@@ -1,22 +1,20 @@
import classnames from 'classnames';
import { forwardRef } from 'react';
import type { IconProps } from './Icon';
import { Icon } from './Icon';
import type { ButtonProps } from './Button';
import { Button } from './Button';
import classnames from 'classnames';
import type { IconProps } from './Icon';
import { Icon } from './Icon';
type Props = Omit<IconProps, 'size'> &
ButtonProps<typeof Button> & {
iconClassName?: string;
};
type Props = IconProps & ButtonProps & { iconClassName?: string; iconSize?: IconProps['size'] };
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
{ icon, spin, className, iconClassName, ...props }: Props,
{ icon, spin, className, iconClassName, size, iconSize, ...props }: Props,
ref,
) {
return (
<Button ref={ref} className={classnames(className, 'group')} {...props}>
<Button ref={ref} className={classnames(className, 'group')} size={size} {...props}>
<Icon
size={iconSize}
icon={icon}
spin={spin}
className={classnames(

View File

@@ -59,13 +59,13 @@ export function Input({
{label}
</label>
<HStack
items="center"
alignItems="center"
className={classnames(
containerClassName,
'relative w-full rounded-md text-gray-900',
'border border-gray-200 focus-within:border-blue-400/40',
size === 'md' && 'h-10',
size === 'sm' && 'h-8',
size === 'md' && 'h-9',
size === 'sm' && 'h-7',
)}
>
{leftSlot}

View File

@@ -38,9 +38,9 @@ export function RequestPane({ fullHeight, request, className }: Props) {
{['JSON', 'Params', 'Headers', 'Auth'].map((label, i) => (
<Button
key={label}
size="xs"
color={i === 0 && 'gray'}
className={i !== 0 && 'opacity-80 hover:opacity-100'}
size="sm"
color={i === 0 ? 'gray' : undefined}
className={i !== 0 ? 'opacity-80 hover:opacity-100' : undefined}
>
{label}
</Button>

View File

@@ -61,7 +61,7 @@ export function ResponsePane({ requestId, className }: Props) {
{response && (
<>
<HStack
items="center"
alignItems="center"
className="italic text-gray-600 text-sm w-full mb-1 flex-shrink-0 pl-2"
>
{response.status > 0 && (
@@ -76,7 +76,7 @@ export function ResponsePane({ requestId, className }: Props) {
</div>
)}
<HStack items="center" className="ml-auto h-8">
<HStack alignItems="center" className="ml-auto h-8">
{contentType.includes('html') && (
<IconButton
icon={viewMode === 'pretty' ? 'eye' : 'code'}

View File

@@ -1,11 +1,12 @@
import classnames from 'classnames';
import type { HTMLAttributes } from 'react';
import React, { useState } from 'react';
import { Link } from 'react-router-dom';
import { SketchPicker } from 'react-color';
import { useRequestCreate } from '../hooks/useRequest';
import useTheme from '../hooks/useTheme';
import type { HttpRequest } from '../lib/models';
import { Button } from './Button';
import { ButtonLink } from './ButtonLink';
import { Dialog } from './Dialog';
import { HeaderEditor } from './HeaderEditor';
import { IconButton } from './IconButton';
@@ -20,28 +21,27 @@ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
export function Sidebar({ className, activeRequestId, workspaceId, requests, ...props }: Props) {
const createRequest = useRequestCreate({ workspaceId, navigateAfter: true });
const { appearance, toggleAppearance } = useTheme();
const { appearance, toggleAppearance, forceSetTheme } = useTheme();
const [open, setOpen] = useState<boolean>(false);
const [color, setColor] = useState<string>('blue');
const [showPicker, setShowPicker] = useState<boolean>(false);
return (
<div
className={classnames(className, 'w-52 bg-gray-100 h-full border-r border-gray-200')}
className={classnames(
className,
'min-w-[10rem] bg-gray-100 h-full border-r border-gray-200 relative',
)}
{...props}
>
<HStack as={WindowDragRegion} items="center" justify="end">
<HStack as={WindowDragRegion} alignItems="center" justifyContent="end">
<Dialog wide open={open} onOpenChange={setOpen} title="Edit Headers">
<HeaderEditor />
<Button className="ml-auto mt-5" color="primary" onClick={() => setOpen(false)}>
Save
</Button>
</Dialog>
<IconButton size="sm" icon="camera" onClick={() => setOpen(true)} />
<IconButton
size="sm"
icon={appearance === 'dark' ? 'moon' : 'sun'}
onClick={toggleAppearance}
/>
<IconButton
size="sm"
className="mx-1"
icon="plusCircle"
onClick={async () => {
await createRequest.mutate({ name: 'Test Request' });
@@ -53,6 +53,27 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ...
<SidebarItem key={r.id} request={r} active={r.id === activeRequestId} />
))}
{/*<Colors />*/}
<HStack
className="absolute bottom-1 left-1 right-0 mx-1"
alignItems="center"
justifyContent="end"
>
<IconButton icon="colorWheel" onClick={() => setShowPicker((p) => !p)} />
<IconButton icon={appearance === 'dark' ? 'moon' : 'sun'} onClick={toggleAppearance} />
<IconButton icon="rows" onClick={() => setOpen(true)} />
</HStack>
{showPicker && (
<SketchPicker
className="fixed z-10 bottom-2 right-2"
color={color}
onChange={(c) => {
setColor(c.hex);
forceSetTheme(c.hex);
}}
/>
)}
</VStack>
</div>
);
@@ -61,15 +82,16 @@ export function Sidebar({ className, activeRequestId, workspaceId, requests, ...
function SidebarItem({ request, active }: { request: HttpRequest; active: boolean }) {
return (
<li key={request.id}>
<Button
as={Link}
<ButtonLink
color="custom"
to={`/workspaces/${request.workspaceId}/requests/${request.id}`}
className={classnames('w-full', active ? 'bg-gray-200/70 text-gray-900' : 'text-gray-500')}
size="xs"
disabled={active}
className={classnames('w-full', active ? 'bg-gray-200/70 text-gray-900' : 'text-gray-600')}
size="sm"
justify="start"
>
{request.name || request.url}
</Button>
</ButtonLink>
</li>
);
}

View File

@@ -1,6 +1,6 @@
import type { HTMLAttributes, ReactNode } from 'react';
import React, { Children, Fragment } from 'react';
import classnames from 'classnames';
import type { ReactNode } from 'react';
import React, { Children, Fragment } from 'react';
const spaceClassesX = {
0: 'pr-0',
@@ -78,26 +78,35 @@ export function VStack({ className, space, children, ...props }: VStackProps) {
);
}
interface BaseStackProps extends HTMLAttributes<HTMLElement> {
items?: 'start' | 'center';
justify?: 'start' | 'center' | 'end';
interface BaseStackProps {
as?: React.ElementType;
alignItems?: 'start' | 'center';
justifyContent?: 'start' | 'center' | 'end';
className?: string;
children?: ReactNode;
}
function BaseStack({ className, items, justify, as = 'div', ...props }: BaseStackProps) {
function BaseStack({
className,
alignItems,
justifyContent,
children,
as = 'div',
}: BaseStackProps) {
const Component = as;
return (
<Component
className={classnames(
className,
'flex flex-grow-0',
items === 'center' && 'items-center',
items === 'start' && 'items-start',
justify === 'start' && 'justify-start',
justify === 'center' && 'justify-center',
justify === 'end' && 'justify-end',
alignItems === 'center' && 'items-center',
alignItems === 'start' && 'items-start',
justifyContent === 'start' && 'justify-start',
justifyContent === 'center' && 'justify-center',
justifyContent === 'end' && 'justify-end',
)}
{...props}
/>
>
{children}
</Component>
);
}

View File

@@ -1,8 +1,8 @@
import { DropdownMenuRadio } from './Dropdown';
import { Button } from './Button';
import { Input } from './Input';
import type { FormEvent } from 'react';
import { Button } from './Button';
import { DropdownMenuRadio } from './Dropdown';
import { IconButton } from './IconButton';
import { Input } from './Input';
interface Props {
sendRequest: () => void;
@@ -24,7 +24,6 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
<Input
hideLabel
useEditor={{ useTemplating: true, contentType: 'url' }}
size="sm"
className="font-mono px-0"
name="url"
label="Enter URL"
@@ -46,7 +45,7 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
{ label: 'HEAD', value: 'HEAD' },
]}
>
<Button type="button" disabled={loading} size="xs" className="mx-0.5" justify="start">
<Button type="button" disabled={loading} size="sm" className="mx-0.5" justify="start">
{method.toUpperCase()}
</Button>
</DropdownMenuRadio>
@@ -55,7 +54,7 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
<IconButton
type="submit"
className="mr-0.5"
size="xs"
size="sm"
icon={loading ? 'update' : 'paperPlane'}
spin={loading}
disabled={loading}

View File

@@ -7,7 +7,7 @@ export function WindowDragRegion({ className, ...props }: Props) {
return (
<div
data-tauri-drag-region
className={classnames(className, 'w-full h-10 flex-shrink-0')}
className={classnames(className, 'w-full h-12 flex-shrink-0')}
{...props}
/>
);