Refactored some core UI

This commit is contained in:
Gregory Schier
2023-10-30 06:35:52 -07:00
parent c8e674d015
commit b392f0c00f
12 changed files with 175 additions and 134 deletions

View File

@@ -2,4 +2,6 @@ import { hello } from './hello.js';
export function entrypoint() { export function entrypoint() {
hello(); hello();
console.log('Try JSON parse', JSON.parse(`{ "hello": 123 }`).hello);
console.log('Try RegExp', '123'.match(/[\d]+/));
} }

View File

@@ -67,7 +67,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
onSelect: showEnvironmentDialog, onSelect: showEnvironmentDialog,
}, },
], ],
[activeEnvironment, environments, routes, prompt, createEnvironment, showEnvironmentDialog], [activeEnvironment, environments, routes, createEnvironment, showEnvironmentDialog],
); );
return ( return (

View File

@@ -12,12 +12,13 @@ import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
import { HStack, VStack } from './core/Stacks'; import { HStack, VStack } from './core/Stacks';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment'; import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import type { DropdownItem } from './core/Dropdown'; import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown'; import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { usePrompt } from '../hooks/usePrompt'; import { usePrompt } from '../hooks/usePrompt';
import { InlineCode } from './core/InlineCode'; import { InlineCode } from './core/InlineCode';
import { useWindowSize } from 'react-use';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
export const EnvironmentEditDialog = function () { export const EnvironmentEditDialog = function () {
const routes = useAppRoutes(); const routes = useAppRoutes();
@@ -25,36 +26,48 @@ export const EnvironmentEditDialog = function () {
const createEnvironment = useCreateEnvironment(); const createEnvironment = useCreateEnvironment();
const activeEnvironment = useActiveEnvironment(); const activeEnvironment = useActiveEnvironment();
const windowSize = useWindowSize();
const showSidebar = windowSize.width > 500;
return ( return (
<div className="h-full grid gap-x-8 grid-rows-[minmax(0,1fr)] grid-cols-[auto_minmax(0,1fr)]"> <div
<div className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full min-w-[200px] pr-4 border-r border-gray-100"> className={classNames(
<div className="h-full overflow-y-scroll"> 'h-full grid gap-x-8 grid-rows-[minmax(0,1fr)]',
{environments.map((e) => ( showSidebar ? 'grid-cols-[auto_minmax(0,1fr)]' : 'grid-cols-[minmax(0,1fr)]',
<Button )}
size="xs" >
className={classNames( {showSidebar && (
'w-full', <aside className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full max-w-[200px] pr-4 border-r border-gray-100">
activeEnvironment?.id === e.id && 'bg-gray-100 text-gray-1000', <div className="min-w-0 h-full w-full overflow-y-scroll">
)} {environments.map((e) => (
justify="start" <Button
key={e.id} size="xs"
onClick={() => { color="custom"
routes.setEnvironment(e); className={classNames(
}} 'w-full',
> 'text-gray-600 hocus:text-gray-800',
{e.name} activeEnvironment?.id === e.id && 'bg-highlightSecondary !text-gray-900',
</Button> )}
))} justify="start"
</div> key={e.id}
<Button onClick={() => {
size="sm" routes.setEnvironment(e);
className="w-full" }}
color="gray" >
onClick={() => createEnvironment.mutate()} {e.name}
> </Button>
New Environment ))}
</Button> </div>
</div> <Button
size="sm"
className="w-full"
color="gray"
onClick={() => createEnvironment.mutate()}
>
New Environment
</Button>
</aside>
)}
{activeEnvironment != null && <EnvironmentEditor environment={activeEnvironment} />} {activeEnvironment != null && <EnvironmentEditor environment={activeEnvironment} />}
</div> </div>
); );
@@ -113,6 +126,12 @@ const EnvironmentEditor = function ({ environment }: { environment: Environment
[deleteEnvironment, updateEnvironment, environment.name, prompt], [deleteEnvironment, updateEnvironment, environment.name, prompt],
); );
const validateName = useCallback((name: string) => {
// Empty just means the variable doesn't have a name yet, and is unusable
if (name === '') return true;
return name.match(/^[a-z_][a-z0-9_]*$/i) != null;
}, []);
return ( return (
<VStack space={2}> <VStack space={2}>
<HStack space={2} className="justify-between"> <HStack space={2} className="justify-between">
@@ -124,6 +143,9 @@ const EnvironmentEditor = function ({ environment }: { environment: Environment
<PairEditor <PairEditor
nameAutocomplete={nameAutocomplete} nameAutocomplete={nameAutocomplete}
nameAutocompleteVariables={false} nameAutocompleteVariables={false}
namePlaceholder="VAR_NAME"
valuePlaceholder="variable value"
nameValidate={validateName}
valueAutocompleteVariables={false} valueAutocompleteVariables={false}
forceUpdateKey={environment.id} forceUpdateKey={environment.id}
pairs={environment.variables} pairs={environment.variables}

View File

@@ -10,6 +10,7 @@ interface Props {
open: boolean; open: boolean;
onClose?: () => void; onClose?: () => void;
zIndex?: keyof typeof zIndexes; zIndex?: keyof typeof zIndexes;
variant?: 'default' | 'transparent';
} }
const zIndexes: Record<number, string> = { const zIndexes: Record<number, string> = {
@@ -20,7 +21,14 @@ const zIndexes: Record<number, string> = {
50: 'z-50', 50: 'z-50',
}; };
export function Overlay({ zIndex = 30, open, onClose, portalName, children }: Props) { export function Overlay({
variant = 'default',
zIndex = 30,
open,
onClose,
portalName,
children,
}: Props) {
return ( return (
<Portal name={portalName}> <Portal name={portalName}>
{open && ( {open && (
@@ -33,14 +41,19 @@ export function Overlay({ zIndex = 30, open, onClose, portalName, children }: Pr
<div <div
aria-hidden aria-hidden
onClick={onClose} onClick={onClose}
className="absolute inset-0 bg-gray-600/30 dark:bg-black/30 backdrop-blur-sm" className={classNames(
'absolute inset-0',
variant === 'default' && 'bg-gray-600/30 dark:bg-black/30 backdrop-blur-sm',
)}
/> />
{/* Add region to still be able to drag the window */} {/* Add region to still be able to drag the window */}
<div data-tauri-drag-region className="absolute top-0 left-0 right-0 h-md" /> {variant !== 'transparent' && (
{children} <div data-tauri-drag-region className="absolute top-0 left-0 right-0 h-md" />
)}
<div className="bg-red-100">{children}</div>
</motion.div> </motion.div>
</FocusTrap> </FocusTrap>
)} )}
</Portal> </Portal>
); );
} }

View File

@@ -42,9 +42,9 @@ export const RecentResponsesDropdown = function ResponsePane({
...responses.slice(0, 20).map((r) => ({ ...responses.slice(0, 20).map((r) => ({
key: r.id, key: r.id,
label: ( label: (
<HStack space={2}> <HStack space={2} alignItems="center">
<StatusTag className="text-xs" response={r} /> <StatusTag className="text-xs" response={r} />
<span>&bull;</span> <span>{r.elapsed}ms</span> <span>&bull;</span> <span className="font-mono text-xs">{r.elapsed}ms</span>
</HStack> </HStack>
), ),
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />, leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />,

View File

@@ -83,7 +83,7 @@ export default function Workspace() {
document.documentElement.addEventListener('mouseup', moveState.current.up); document.documentElement.addEventListener('mouseup', moveState.current.up);
setIsResizing(true); setIsResizing(true);
}, },
[setWidth, width], [setWidth, resetWidth, width, hide, show],
); );
const sideWidth = hidden ? 0 : width; const sideWidth = hidden ? 0 : width;
@@ -121,7 +121,7 @@ export default function Workspace() {
)} )}
> >
{floating ? ( {floating ? (
<Overlay open={!hidden} portalName="sidebar" onClose={hide}> <Overlay open={!hidden} portalName="sidebar" onClose={hide} zIndex={10}>
<motion.div <motion.div
initial={{ opacity: 0, x: -10 }} initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }} animate={{ opacity: 1, x: 0 }}

View File

@@ -49,9 +49,9 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
() => () =>
classNames( classNames(
className, className,
'flex-shrink-0 outline-none whitespace-nowrap', 'whitespace-nowrap outline-none',
'focus-visible-or-class:ring', 'flex-shrink-0 flex items-center',
'rounded-md flex items-center', 'focus-visible-or-class:ring rounded-md',
disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto', disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto',
colorStyles[color || 'default'], colorStyles[color || 'default'],
justify === 'start' && 'justify-start', justify === 'start' && 'justify-start',
@@ -70,7 +70,7 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
) : leftSlot ? ( ) : leftSlot ? (
<div className="mr-1">{leftSlot}</div> <div className="mr-1">{leftSlot}</div>
) : null} ) : null}
{children} <div className="max-w-[15em] truncate">{children}</div>
{rightSlot && <div className="ml-1">{rightSlot}</div>} {rightSlot && <div className="ml-1">{rightSlot}</div>}
{forDropdown && <Icon icon="chevronDown" size={size} className="ml-1 -mr-1" />} {forDropdown && <Icon icon="chevronDown" size={size} className="ml-1 -mr-1" />}
</button> </button>

View File

@@ -59,7 +59,7 @@ export function Dialog({
'dark:border border-highlight shadow shadow-black/10', 'dark:border border-highlight shadow shadow-black/10',
size === 'sm' && 'w-[25rem] max-h-[80vh]', size === 'sm' && 'w-[25rem] max-h-[80vh]',
size === 'md' && 'w-[45rem] max-h-[80vh]', size === 'md' && 'w-[45rem] max-h-[80vh]',
size === 'full' && 'w-[calc(100vw-8em)] h-[calc(100vh-8em)]', size === 'full' && 'w-[95vw] h-[calc(100vh-6em)]',
size === 'dynamic' && 'min-w-[30vw] max-w-[80vw]', size === 'dynamic' && 'min-w-[30vw] max-w-[80vw]',
)} )}
> >

View File

@@ -1,5 +1,4 @@
import classNames from 'classnames'; import classNames from 'classnames';
import FocusTrap from 'focus-trap-react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react'; import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';
import React, { import React, {
@@ -13,11 +12,11 @@ import React, {
useRef, useRef,
useState, useState,
} from 'react'; } from 'react';
import { useKey, useKeyPressEvent } from 'react-use'; import { useKey, useKeyPressEvent, useWindowSize } from 'react-use';
import { Portal } from '../Portal';
import { Button } from './Button'; import { Button } from './Button';
import { Separator } from './Separator'; import { Separator } from './Separator';
import { VStack } from './Stacks'; import { VStack } from './Stacks';
import { Overlay } from '../Overlay';
export type DropdownItemSeparator = { export type DropdownItemSeparator = {
type: 'separator'; type: 'separator';
@@ -65,7 +64,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
...menuRef.current, ...menuRef.current,
isOpen: open, isOpen: open,
toggle (activeIndex?: number) { toggle(activeIndex?: number) {
if (!open) this.open(activeIndex); if (!open) this.open(activeIndex);
else setOpen(false); else setOpen(false);
}, },
@@ -107,10 +106,12 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
buttonRef.current?.setAttribute('aria-expanded', open.toString()); buttonRef.current?.setAttribute('aria-expanded', open.toString());
}, [open]); }, [open]);
const windowSize = useWindowSize();
const triggerRect = useMemo(() => { const triggerRect = useMemo(() => {
windowSize; // Make TS happy with this dep
if (!open) return null; if (!open) return null;
return buttonRef.current?.getBoundingClientRect(); return buttonRef.current?.getBoundingClientRect();
}, [open]); }, [open, windowSize]);
return ( return (
<> <>
@@ -267,61 +268,59 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
if (items.length === 0) return null; if (items.length === 0) return null;
return ( return (
<Portal name="dropdown"> <Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
<FocusTrap> <div>
<div> <div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={onClose} />
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-50" onClick={onClose} /> <motion.div
<motion.div tabIndex={0}
tabIndex={0} onKeyDown={handleMenuKeyDown}
onKeyDown={handleMenuKeyDown} initial={{ opacity: 0, y: -5, scale: 0.98 }}
initial={{ opacity: 0, y: -5, scale: 0.98 }} animate={{ opacity: 1, y: 0, scale: 1 }}
animate={{ opacity: 1, y: 0, scale: 1 }} role="menu"
role="menu" aria-orientation="vertical"
aria-orientation="vertical" dir="ltr"
dir="ltr" ref={containerRef}
ref={containerRef} style={containerStyles}
style={containerStyles} className={classNames(className, 'outline-none mt-1 pointer-events-auto fixed z-50')}
className={classNames(className, 'outline-none mt-1 pointer-events-auto fixed z-50')} >
> <span
<span aria-hidden
aria-hidden style={triangleStyles}
style={triangleStyles} className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l" />
/> {containerStyles && (
{containerStyles && ( <VStack
<VStack space={0.5}
space={0.5} ref={initMenu}
ref={initMenu} style={menuStyles}
style={menuStyles} className={classNames(
className={classNames( className,
className, 'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border',
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border', 'border-gray-200 overflow-auto mb-1 mx-0.5',
'border-gray-200 overflow-auto mb-1 mx-0.5', )}
)} >
> {items.map((item, i) => {
{items.map((item, i) => { if (item.type === 'separator') {
if (item.type === 'separator') { return <Separator key={i} className="my-1.5" label={item.label} />;
return <Separator key={i} className="my-1.5" label={item.label} />; }
} if (item.hidden) {
if (item.hidden) { return null;
return null; }
} return (
return ( <MenuItem
<MenuItem focused={i === selectedIndex}
focused={i === selectedIndex} onFocus={handleFocus}
onFocus={handleFocus} onSelect={handleSelect}
onSelect={handleSelect} key={item.key}
key={item.key} item={item}
item={item} />
/> );
); })}
})} </VStack>
</VStack> )}
)} </motion.div>
</motion.div> </div>
</div> </Overlay>
</FocusTrap>
</Portal>
); );
}); });
@@ -359,6 +358,8 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
onFocus={handleFocus} onFocus={handleFocus}
onClick={handleClick} onClick={handleClick}
justify="start" justify="start"
leftSlot={item.leftSlot && <div className="pr-2 flex justify-start">{item.leftSlot}</div>}
rightSlot={item.rightSlot && <div className="ml-auto pl-3">{item.rightSlot}</div>}
className={classNames( className={classNames(
className, className,
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap', 'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap',
@@ -367,7 +368,6 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
)} )}
{...props} {...props}
> >
{item.leftSlot && <div className="pr-2 flex justify-start">{item.leftSlot}</div>}
<div <div
className={classNames( className={classNames(
// Add padding on right when no right slot, for some visual balance // Add padding on right when no right slot, for some visual balance
@@ -376,7 +376,6 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
> >
{item.label} {item.label}
</div> </div>
{item.rightSlot && <div className="ml-auto pl-3">{item.rightSlot}</div>}
</Button> </Button>
); );
} }

View File

@@ -154,6 +154,8 @@ export const PairEditor = memo(function PairEditor({
'pb-2 grid overflow-auto max-h-full', 'pb-2 grid overflow-auto max-h-full',
// Move over the width of the drag handle // Move over the width of the drag handle
'-ml-3', '-ml-3',
// Pad to make room for the drag divider
'pt-0.5',
)} )}
> >
{pairs.map((p, i) => { {pairs.map((p, i) => {
@@ -171,8 +173,8 @@ export const PairEditor = memo(function PairEditor({
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
nameAutocomplete={nameAutocomplete} nameAutocomplete={nameAutocomplete}
valueAutocomplete={valueAutocomplete} valueAutocomplete={valueAutocomplete}
namePlaceholder={namePlaceholder} namePlaceholder={isLast ? namePlaceholder : ''}
valuePlaceholder={valuePlaceholder} valuePlaceholder={isLast ? valuePlaceholder : ''}
nameValidate={nameValidate} nameValidate={nameValidate}
valueValidate={valueValidate} valueValidate={valueValidate}
showDelete={!isLast} showDelete={!isLast}

View File

@@ -102,14 +102,16 @@ export function Tabs({
size="sm" size="sm"
onClick={isActive ? undefined : () => handleTabChange(t.value)} onClick={isActive ? undefined : () => handleTabChange(t.value)}
className={btnClassName} className={btnClassName}
rightSlot={
<Icon
icon="triangleDown"
className={classNames('-mr-1.5', isActive ? 'opacity-100' : 'opacity-20')}
/>
}
> >
{option && 'shortLabel' in option {option && 'shortLabel' in option
? option.shortLabel ? option.shortLabel
: option?.label ?? 'Unknown'} : option?.label ?? 'Unknown'}
<Icon
icon="triangleDown"
className={classNames('-mr-1.5', isActive ? 'opacity-100' : 'opacity-20')}
/>
</Button> </Button>
</RadioDropdown> </RadioDropdown>
); );

View File

@@ -3,7 +3,7 @@ import { useCallback, useState } from 'react';
import { Button } from '../components/core/Button'; import { Button } from '../components/core/Button';
import type { InputProps } from '../components/core/Input'; import type { InputProps } from '../components/core/Input';
import { Input } from '../components/core/Input'; import { Input } from '../components/core/Input';
import { HStack, VStack } from '../components/core/Stacks'; import { HStack } from '../components/core/Stacks';
export interface PromptProps { export interface PromptProps {
onHide: () => void; onHide: () => void;
@@ -25,26 +25,27 @@ export function Prompt({ onHide, label, name, defaultValue, onResult }: PromptPr
); );
return ( return (
<form onSubmit={handleSubmit}> <form
<VStack space={6}> className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-6"
<Input onSubmit={handleSubmit}
hideLabel >
require <Input
autoSelect hideLabel
label={label} require
name={name} autoSelect
defaultValue={defaultValue} label={label}
onChange={setValue} name={name}
/> defaultValue={defaultValue}
<HStack space={2} justifyContent="end"> onChange={setValue}
<Button className="focus" color="gray" onClick={onHide}> />
Cancel <HStack space={2} justifyContent="end">
</Button> <Button className="focus" color="gray" onClick={onHide}>
<Button type="submit" className="focus" color="primary"> Cancel
Save </Button>
</Button> <Button type="submit" className="focus" color="primary">
</HStack> Save
</VStack> </Button>
</HStack>
</form> </form>
); );
} }