Refactored some core UI

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

View File

@@ -2,4 +2,6 @@ import { hello } from './hello.js';
export function entrypoint() {
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,
},
],
[activeEnvironment, environments, routes, prompt, createEnvironment, showEnvironmentDialog],
[activeEnvironment, environments, routes, createEnvironment, showEnvironmentDialog],
);
return (

View File

@@ -12,12 +12,13 @@ import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
import { HStack, VStack } from './core/Stacks';
import { IconButton } from './core/IconButton';
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { usePrompt } from '../hooks/usePrompt';
import { InlineCode } from './core/InlineCode';
import { useWindowSize } from 'react-use';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
export const EnvironmentEditDialog = function () {
const routes = useAppRoutes();
@@ -25,36 +26,48 @@ export const EnvironmentEditDialog = function () {
const createEnvironment = useCreateEnvironment();
const activeEnvironment = useActiveEnvironment();
const windowSize = useWindowSize();
const showSidebar = windowSize.width > 500;
return (
<div className="h-full grid gap-x-8 grid-rows-[minmax(0,1fr)] grid-cols-[auto_minmax(0,1fr)]">
<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">
<div className="h-full overflow-y-scroll">
{environments.map((e) => (
<Button
size="xs"
className={classNames(
'w-full',
activeEnvironment?.id === e.id && 'bg-gray-100 text-gray-1000',
)}
justify="start"
key={e.id}
onClick={() => {
routes.setEnvironment(e);
}}
>
{e.name}
</Button>
))}
</div>
<Button
size="sm"
className="w-full"
color="gray"
onClick={() => createEnvironment.mutate()}
>
New Environment
</Button>
</div>
<div
className={classNames(
'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) => (
<Button
size="xs"
color="custom"
className={classNames(
'w-full',
'text-gray-600 hocus:text-gray-800',
activeEnvironment?.id === e.id && 'bg-highlightSecondary !text-gray-900',
)}
justify="start"
key={e.id}
onClick={() => {
routes.setEnvironment(e);
}}
>
{e.name}
</Button>
))}
</div>
<Button
size="sm"
className="w-full"
color="gray"
onClick={() => createEnvironment.mutate()}
>
New Environment
</Button>
</aside>
)}
{activeEnvironment != null && <EnvironmentEditor environment={activeEnvironment} />}
</div>
);
@@ -113,6 +126,12 @@ const EnvironmentEditor = function ({ environment }: { environment: Environment
[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 (
<VStack space={2}>
<HStack space={2} className="justify-between">
@@ -124,6 +143,9 @@ const EnvironmentEditor = function ({ environment }: { environment: Environment
<PairEditor
nameAutocomplete={nameAutocomplete}
nameAutocompleteVariables={false}
namePlaceholder="VAR_NAME"
valuePlaceholder="variable value"
nameValidate={validateName}
valueAutocompleteVariables={false}
forceUpdateKey={environment.id}
pairs={environment.variables}

View File

@@ -10,6 +10,7 @@ interface Props {
open: boolean;
onClose?: () => void;
zIndex?: keyof typeof zIndexes;
variant?: 'default' | 'transparent';
}
const zIndexes: Record<number, string> = {
@@ -20,7 +21,14 @@ const zIndexes: Record<number, string> = {
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 (
<Portal name={portalName}>
{open && (
@@ -33,14 +41,19 @@ export function Overlay({ zIndex = 30, open, onClose, portalName, children }: Pr
<div
aria-hidden
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 */}
<div data-tauri-drag-region className="absolute top-0 left-0 right-0 h-md" />
{children}
{variant !== 'transparent' && (
<div data-tauri-drag-region className="absolute top-0 left-0 right-0 h-md" />
)}
<div className="bg-red-100">{children}</div>
</motion.div>
</FocusTrap>
)}
)}
</Portal>
);
}

View File

@@ -42,9 +42,9 @@ export const RecentResponsesDropdown = function ResponsePane({
...responses.slice(0, 20).map((r) => ({
key: r.id,
label: (
<HStack space={2}>
<HStack space={2} alignItems="center">
<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>
),
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);
setIsResizing(true);
},
[setWidth, width],
[setWidth, resetWidth, width, hide, show],
);
const sideWidth = hidden ? 0 : width;
@@ -121,7 +121,7 @@ export default function Workspace() {
)}
>
{floating ? (
<Overlay open={!hidden} portalName="sidebar" onClose={hide}>
<Overlay open={!hidden} portalName="sidebar" onClose={hide} zIndex={10}>
<motion.div
initial={{ opacity: 0, x: -10 }}
animate={{ opacity: 1, x: 0 }}

View File

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

View File

@@ -59,7 +59,7 @@ export function Dialog({
'dark:border border-highlight shadow shadow-black/10',
size === 'sm' && 'w-[25rem] 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]',
)}
>

View File

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

View File

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

View File

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

View File

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