mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-11 21:11:36 +01:00
Refactored some core UI
This commit is contained in:
@@ -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]+/));
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
onSelect: showEnvironmentDialog,
|
||||
},
|
||||
],
|
||||
[activeEnvironment, environments, routes, prompt, createEnvironment, showEnvironmentDialog],
|
||||
[activeEnvironment, environments, routes, createEnvironment, showEnvironmentDialog],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>•</span> <span>{r.elapsed}ms</span>
|
||||
<span>•</span> <span className="font-mono text-xs">{r.elapsed}ms</span>
|
||||
</HStack>
|
||||
),
|
||||
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <Icon icon="empty" />,
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user