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
+2
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]+/));
} }
@@ -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 (
+28 -6
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,16 +26,27 @@ 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)]',
showSidebar ? 'grid-cols-[auto_minmax(0,1fr)]' : 'grid-cols-[minmax(0,1fr)]',
)}
>
{showSidebar && (
<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">
<div className="min-w-0 h-full w-full overflow-y-scroll">
{environments.map((e) => ( {environments.map((e) => (
<Button <Button
size="xs" size="xs"
color="custom"
className={classNames( className={classNames(
'w-full', 'w-full',
activeEnvironment?.id === e.id && 'bg-gray-100 text-gray-1000', 'text-gray-600 hocus:text-gray-800',
activeEnvironment?.id === e.id && 'bg-highlightSecondary !text-gray-900',
)} )}
justify="start" justify="start"
key={e.id} key={e.id}
@@ -54,7 +66,8 @@ export const EnvironmentEditDialog = function () {
> >
New Environment New Environment
</Button> </Button>
</div> </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}
+16 -3
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,11 +41,16 @@ 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 */}
{variant !== 'transparent' && (
<div data-tauri-drag-region className="absolute top-0 left-0 right-0 h-md" /> <div data-tauri-drag-region className="absolute top-0 left-0 right-0 h-md" />
{children} )}
<div className="bg-red-100">{children}</div>
</motion.div> </motion.div>
</FocusTrap> </FocusTrap>
)} )}
@@ -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" />,
+2 -2
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 }}
+4 -4
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>
+1 -1
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]',
)} )}
> >
+11 -12
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,10 +268,9 @@ 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-50" onClick={onClose} /> <div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={onClose} />
<motion.div <motion.div
tabIndex={0} tabIndex={0}
onKeyDown={handleMenuKeyDown} onKeyDown={handleMenuKeyDown}
@@ -320,8 +320,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
)} )}
</motion.div> </motion.div>
</div> </div>
</FocusTrap> </Overlay>
</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>
); );
} }
+4 -2
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}
+6 -4
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={
{option && 'shortLabel' in option
? option.shortLabel
: option?.label ?? 'Unknown'}
<Icon <Icon
icon="triangleDown" icon="triangleDown"
className={classNames('-mr-1.5', isActive ? 'opacity-100' : 'opacity-20')} className={classNames('-mr-1.5', isActive ? 'opacity-100' : 'opacity-20')}
/> />
}
>
{option && 'shortLabel' in option
? option.shortLabel
: option?.label ?? 'Unknown'}
</Button> </Button>
</RadioDropdown> </RadioDropdown>
); );
+5 -4
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,8 +25,10 @@ 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"
onSubmit={handleSubmit}
>
<Input <Input
hideLabel hideLabel
require require
@@ -44,7 +46,6 @@ export function Prompt({ onHide, label, name, defaultValue, onResult }: PromptPr
Save Save
</Button> </Button>
</HStack> </HStack>
</VStack>
</form> </form>
); );
} }