Git support (#143)

This commit is contained in:
Gregory Schier
2025-02-07 07:59:48 -08:00
committed by GitHub
parent cffc7714c1
commit 1a7c27663a
111 changed files with 4264 additions and 372 deletions

View File

@@ -1,3 +1,4 @@
import type { Color } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import type { HTMLAttributes, ReactNode } from 'react';
import { forwardRef, useImperativeHandle, useRef } from 'react';
@@ -9,16 +10,7 @@ import { LoadingIcon } from './LoadingIcon';
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color' | 'onChange'> & {
innerClassName?: string;
color?:
| 'custom'
| 'default'
| 'secondary'
| 'primary'
| 'info'
| 'success'
| 'notice'
| 'warning'
| 'danger';
color?: Color | 'custom' | 'default';
variant?: 'border' | 'solid';
isLoading?: boolean;
size?: '2xs' | 'xs' | 'sm' | 'md';
@@ -59,6 +51,10 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null)?.join('');
const fullTitle = hotkeyTrigger ? `${title ?? ''} ${hotkeyTrigger}`.trim() : title;
if (isLoading) {
disabled = true;
}
const classes = classNames(
className,
'x-theme-button',
@@ -110,7 +106,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
ref={buttonRef}
type={type}
className={classes}
disabled={disabled || isLoading}
disabled={disabled}
onClick={(e) => {
onClick?.(e);
if (event != null) {

View File

@@ -12,6 +12,7 @@ export interface CheckboxProps {
disabled?: boolean;
inputWrapperClassName?: string;
hideLabel?: boolean;
fullWidth?: boolean;
event?: string;
}
@@ -23,6 +24,7 @@ export function Checkbox({
disabled,
title,
hideLabel,
fullWidth,
event,
}: CheckboxProps) {
return (
@@ -52,7 +54,9 @@ export function Checkbox({
/>
</div>
</div>
<span className={classNames(disabled && 'opacity-disabled')}>{!hideLabel && title}</span>
<div className={classNames(fullWidth && 'w-full', disabled && 'opacity-disabled')}>
{!hideLabel && title}
</div>
</HStack>
);
}

View File

@@ -1,25 +1,15 @@
import type { ButtonProps } from './Button';
import type { Color } from '@yaakapp-internal/plugins';
import { Button } from './Button';
import { HStack } from './Stacks';
export interface ConfirmProps {
onHide: () => void;
onResult: (result: boolean) => void;
variant?: 'delete' | 'confirm';
confirmText?: string;
color?: Color;
}
const colors: Record<NonNullable<ConfirmProps['variant']>, ButtonProps['color']> = {
delete: 'danger',
confirm: 'primary',
};
const confirmButtonTexts: Record<NonNullable<ConfirmProps['variant']>, string> = {
delete: 'Delete',
confirm: 'Confirm',
};
export function Confirm({ onHide, onResult, confirmText, variant = 'confirm' }: ConfirmProps) {
export function Confirm({ onHide, onResult, confirmText, color = 'primary' }: ConfirmProps) {
const handleHide = () => {
onResult(false);
onHide();
@@ -32,8 +22,8 @@ export function Confirm({ onHide, onResult, confirmText, variant = 'confirm' }:
return (
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse">
<Button color={colors[variant]} onClick={handleSuccess}>
{confirmText ?? confirmButtonTexts[variant]}
<Button color={color} onClick={handleSuccess}>
{confirmText ?? 'Confirm'}
</Button>
<Button onClick={handleHide} variant="border">
Cancel

View File

@@ -36,17 +36,23 @@ import { HotKey } from './HotKey';
import { Icon } from './Icon';
import { Separator } from './Separator';
import { HStack, VStack } from './Stacks';
import { LoadingIcon } from './LoadingIcon';
export type DropdownItemSeparator = {
type: 'separator';
label?: string;
label?: ReactNode;
hidden?: boolean;
};
export type DropdownItemContent = {
type: 'content';
label?: ReactNode;
hidden?: boolean;
};
export type DropdownItemDefault = {
type?: 'default';
label: ReactNode;
keepOpen?: boolean;
hotKeyAction?: HotkeyAction;
hotKeyLabelOnly?: boolean;
color?: 'default' | 'danger' | 'info' | 'warning' | 'notice';
@@ -54,10 +60,11 @@ export type DropdownItemDefault = {
hidden?: boolean;
leftSlot?: ReactNode;
rightSlot?: ReactNode;
onSelect?: () => void;
waitForOnSelect?: boolean;
onSelect?: () => void | Promise<void>;
};
export type DropdownItem = DropdownItemDefault | DropdownItemSeparator;
export type DropdownItem = DropdownItemDefault | DropdownItemSeparator | DropdownItemContent;
export interface DropdownProps {
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
@@ -374,14 +381,20 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
);
const handleSelect = useCallback(
(i: DropdownItem) => {
if (i.type !== 'separator' && !i.keepOpen) {
handleClose();
}
async (item: DropdownItem) => {
if (!('onSelect' in item) || !item.onSelect) return;
setSelectedIndex(null);
if (i.type !== 'separator' && typeof i.onSelect === 'function') {
i.onSelect();
const promise = item.onSelect();
if (item.waitForOnSelect) {
try {
await promise;
} catch {
// Nothing
}
}
handleClose();
},
[handleClose, setSelectedIndex],
);
@@ -391,10 +404,10 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
close: handleClose,
prev: handlePrev,
next: handleNext,
select() {
async select() {
const item = items[selectedIndexRef.current ?? -1] ?? null;
if (!item) return;
handleSelect(item);
await handleSelect(item);
},
};
}, [handleClose, handleNext, handlePrev, handleSelect, items]);
@@ -466,6 +479,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
{items.map(
(item, i) =>
item.type !== 'separator' &&
item.type !== 'content' &&
!item.hotKeyLabelOnly &&
item.hotKeyAction && (
<MenuItemHotKey
@@ -519,7 +533,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
space={2}
className="pb-0.5 px-1.5 mb-2 text-sm border border-border-subtle mx-2 rounded font-mono h-xs"
>
<Icon icon="search" size="xs" className="text-text-subtle" />
<Icon icon="search" size="xs" />
<div className="text">{filter}</div>
</HStack>
)}
@@ -537,6 +551,13 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
</Separator>
);
}
if (item.type === 'content') {
return (
<div key={i} className={classNames('my-1.5 mx-2 max-w-xs')}>
{item.label}
</div>
);
}
return (
<MenuItem
focused={i === selectedIndex}
@@ -559,13 +580,19 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
interface MenuItemProps {
className?: string;
item: DropdownItemDefault;
onSelect: (item: DropdownItemDefault) => void;
onSelect: (item: DropdownItemDefault) => Promise<void>;
onFocus: (item: DropdownItemDefault) => void;
focused: boolean;
}
function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: MenuItemProps) {
const handleClick = useCallback(() => onSelect?.(item), [item, onSelect]);
const [isLoading, setIsLoading] = useState(false);
const handleClick = useCallback(async () => {
if (item.waitForOnSelect) setIsLoading(true);
await onSelect?.(item);
if (item.waitForOnSelect) setIsLoading(false);
}, [item, onSelect]);
const handleFocus = useCallback(
(e: ReactFocusEvent<HTMLButtonElement>) => {
e.stopPropagation(); // Don't trigger focus on any parents
@@ -598,7 +625,11 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
onClick={handleClick}
justify="start"
leftSlot={
item.leftSlot && <div className="pr-2 flex justify-start opacity-70">{item.leftSlot}</div>
(isLoading || item.leftSlot) && (
<div className={classNames('pr-2 flex justify-start opacity-70')}>
{isLoading ? <LoadingIcon /> : item.leftSlot}
</div>
)
}
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
innerClassName="!text-left"

View File

@@ -1,3 +1,4 @@
import type { Color } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import * as lucide from 'lucide-react';
import type { HTMLAttributes } from 'react';
@@ -14,9 +15,11 @@ const icons = {
arrow_big_up_dash: lucide.ArrowBigUpDashIcon,
arrow_down: lucide.ArrowDownIcon,
arrow_down_to_dot: lucide.ArrowDownToDotIcon,
arrow_down_to_line: lucide.ArrowDownToLineIcon,
arrow_up: lucide.ArrowUpIcon,
arrow_up_down: lucide.ArrowUpDownIcon,
arrow_up_from_dot: lucide.ArrowUpFromDotIcon,
arrow_up_from_line: lucide.ArrowUpFromLineIcon,
badge_check: lucide.BadgeCheckIcon,
box: lucide.BoxIcon,
cake: lucide.CakeIcon,
@@ -42,11 +45,14 @@ const icons = {
filter: lucide.FilterIcon,
flask: lucide.FlaskConicalIcon,
folder: lucide.FolderIcon,
folder_git: lucide.FolderGitIcon,
folder_input: lucide.FolderInputIcon,
folder_open: lucide.FolderOpenIcon,
folder_output: lucide.FolderOutputIcon,
folder_symlink: lucide.FolderSymlinkIcon,
folder_sync: lucide.FolderSyncIcon,
git_branch: lucide.GitBranchIcon,
git_branch_plus: lucide.GitBranchPlusIcon,
git_commit: lucide.GitCommitIcon,
git_commit_vertical: lucide.GitCommitVerticalIcon,
git_pull_request: lucide.GitPullRequestIcon,
@@ -63,6 +69,7 @@ const icons = {
left_panel_visible: lucide.PanelLeftCloseIcon,
lock: lucide.LockIcon,
magic_wand: lucide.Wand2Icon,
merge: lucide.MergeIcon,
minus: lucide.MinusIcon,
minus_circle: lucide.MinusCircleIcon,
moon: lucide.MoonIcon,
@@ -86,6 +93,8 @@ const icons = {
unpin: lucide.PinOffIcon,
update: lucide.RefreshCcwIcon,
upload: lucide.UploadIcon,
variable: lucide.VariableIcon,
wrench: lucide.WrenchIcon,
x: lucide.XIcon,
_unknown: lucide.ShieldAlertIcon,
@@ -98,22 +107,38 @@ export interface IconProps {
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
spin?: boolean;
title?: string;
color?: Color | 'custom' | 'default';
}
export const Icon = memo(function Icon({ icon, spin, size = 'md', className, title }: IconProps) {
export const Icon = memo(function Icon({
icon,
color = 'default',
spin,
size = 'md',
className,
title,
}: IconProps) {
const Component = icons[icon] ?? icons._unknown;
return (
<Component
title={title}
className={classNames(
className,
'text-inherit flex-shrink-0',
'flex-shrink-0',
size === 'xl' && 'h-6 w-6',
size === 'lg' && 'h-5 w-5',
size === 'md' && 'h-4 w-4',
size === 'sm' && 'h-3.5 w-3.5',
size === 'xs' && 'h-3 w-3',
size === '2xs' && 'h-2.5 w-2.5',
color === 'default' && 'inherit',
color === 'danger' && 'text-danger',
color === 'warning' && 'text-warning',
color === 'notice' && 'text-notice',
color === 'info' && 'text-info',
color === 'success' && 'text-success',
color === 'primary' && 'text-primary',
color === 'secondary' && 'text-secondary',
spin && 'animate-spin',
)}
/>

View File

@@ -12,6 +12,7 @@ export type IconButtonProps = IconProps &
showConfirm?: boolean;
iconClassName?: string;
iconSize?: IconProps['size'];
iconColor?: IconProps['color'];
title: string;
showBadge?: boolean;
};
@@ -29,6 +30,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
size = 'md',
iconSize,
showBadge,
iconColor,
...props
}: IconButtonProps,
ref,
@@ -47,7 +49,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
ref={ref}
aria-hidden={icon === 'empty'}
disabled={icon === 'empty'}
tabIndex={tabIndex ?? icon === 'empty' ? -1 : undefined}
tabIndex={(tabIndex ?? icon === 'empty') ? -1 : undefined}
onClick={handleClick}
innerClassName="flex items-center justify-center"
size={size}
@@ -56,8 +58,6 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
className,
'group/button relative flex-shrink-0',
'!px-0',
color === 'custom' && 'text-text-subtle',
color === 'default' && 'text-text-subtle',
size === 'md' && 'w-md',
size === 'sm' && 'w-sm',
size === 'xs' && 'w-xs',
@@ -74,11 +74,11 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
size={iconSize}
icon={confirmed ? 'check' : icon}
spin={spin}
color={confirmed ? 'success' : iconColor}
className={classNames(
iconClassName,
'group-hover/button:text',
'group-hover/button:text-text',
props.disabled && 'opacity-70',
confirmed && 'text-green-600',
)}
/>
</Button>

View File

@@ -46,6 +46,7 @@ export type InputProps = Pick<
required?: boolean;
wrapLines?: boolean;
multiLine?: boolean;
fullHeight?: boolean;
stateKey: EditorProps['stateKey'];
};
@@ -56,6 +57,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
inputWrapperClassName,
defaultValue,
forceUpdateKey,
fullHeight,
hideLabel,
label,
labelClassName,
@@ -148,8 +150,9 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
<div
ref={wrapperRef}
className={classNames(
'w-full',
'pointer-events-auto', // Just in case we're placing in disabled parent
'w-full',
fullHeight && 'h-full',
labelPosition === 'left' && 'flex items-center gap-2',
labelPosition === 'top' && 'flex-row gap-0.5',
)}
@@ -166,6 +169,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
alignItems="stretch"
className={classNames(
containerClassName,
fullHeight && 'h-full',
'x-theme-input',
'relative w-full rounded-md text',
'border',
@@ -182,6 +186,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
className={classNames(
inputWrapperClassName,
'w-full min-w-0 px-2',
fullHeight && 'h-full',
leftSlot && 'pl-0.5 -ml-2',
rightSlot && 'pr-0.5 -mr-2',
)}
@@ -218,8 +223,11 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
<IconButton
title={obscured ? `Show ${label}` : `Obscure ${label}`}
size="xs"
className={classNames("mr-0.5 group/obscure !h-auto my-0.5", disabled && 'opacity-disabled')}
iconClassName="text-text-subtle group-hover/obscure:text"
className={classNames(
'mr-0.5 group/obscure !h-auto my-0.5',
disabled && 'opacity-disabled',
)}
iconClassName="group-hover/obscure:text"
iconSize="sm"
icon={obscured ? 'eye' : 'eye_closed'}
onClick={() => setObscured((o) => !o)}

View File

@@ -104,7 +104,7 @@ export const JsonAttributeTree = ({
icon="chevron_right"
className={classNames(
'left-0 absolute transition-transform flex items-center',
'text-text-subtlest group-hover:text-text-subtle',
'group-hover:text-text-subtle',
isExpanded ? 'rotate-90' : '',
)}
/>

View File

@@ -30,7 +30,7 @@ export const PairOrBulkEditor = forwardRef<PairEditorRef, Props>(function PairOr
title={useBulk ? 'Enable form edit' : 'Enable bulk edit'}
className={classNames(
'transition-opacity opacity-0 group-hover:opacity-80 hover:!opacity-100 shadow',
'bg-surface text-text-subtle hover:text group-hover/wrapper:opacity-100',
'bg-surface hover:text group-hover/wrapper:opacity-100',
)}
onClick={() => setUseBulk((b) => !b)}
icon={useBulk ? 'table' : 'file_code'}

View File

@@ -148,7 +148,7 @@ export function PlainInput({
title={obscured ? `Show ${label}` : `Obscure ${label}`}
size="xs"
className="mr-0.5 group/obscure !h-auto my-0.5"
iconClassName="text-text-subtle group-hover/obscure:text"
iconClassName="group-hover/obscure:text"
iconSize="sm"
icon={obscured ? 'eye' : 'eye_closed'}
onClick={() => setObscured((o) => !o)}

View File

@@ -45,7 +45,7 @@ export function SegmentedControl<T extends string>({ value, onChange, options, n
<IconButton
size="xs"
variant="solid"
color={isActive ? "secondary" : "default"}
color={isActive ? "secondary" : undefined}
role="radio"
event={{ id: name, value: String(o.value) }}
tabIndex={isSelected ? 0 : -1}

View File

@@ -0,0 +1,64 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import React from 'react';
export function Table({ children }: { children: ReactNode }) {
return (
<table className="w-full text-sm mb-auto min-w-full max-w-full divide-y divide-surface-highlight">
{children}
</table>
);
}
export function TableBody({ children }: { children: ReactNode }) {
return <tbody className="divide-y divide-surface-highlight">{children}</tbody>;
}
export function TableHead({ children }: { children: ReactNode }) {
return <thead>{children}</thead>;
}
export function TableRow({ children }: { children: ReactNode }) {
return <tr>{children}</tr>;
}
export function TableCell({ children, className }: { children: ReactNode; className?: string }) {
return (
<td
className={classNames(
className,
'py-2 [&:not(:first-child)]:pl-4 text-left w-0 whitespace-nowrap',
)}
>
{children}
</td>
);
}
export function TruncatedWideTableCell({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<TableCell className={classNames(className, 'w-full relative')}>
<div className="absolute inset-0 py-2 truncate">{children}</div>
</TableCell>
);
}
export function TableHeaderCell({
children,
className,
}: {
children: ReactNode;
className?: string;
}) {
return (
<th className={classNames(className, 'py-2 [&:not(:first-child)]:pl-4 text-left w-0')}>
{children}
</th>
);
}

View File

@@ -121,7 +121,7 @@ export function Tabs({
<Icon
size="sm"
icon="chevron_down"
className={classNames('ml-1', isActive ? 'text-text-subtle' : 'opacity-50')}
className={classNames('ml-1', !isActive && 'opacity-50')}
/>
</button>
</RadioDropdown>

View File

@@ -55,14 +55,14 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
<div
className={classNames(
`x-theme-toast x-theme-toast--${color}`,
'pointer-events-auto overflow-hidden break-all',
'pointer-events-auto overflow-hidden',
'relative pointer-events-auto bg-surface text-text rounded-lg',
'border border-border shadow-lg w-[25rem]',
'grid grid-cols-[1fr_auto]',
)}
>
<div className="px-3 py-3 flex items-start gap-2 w-full">
{toastIcon && <Icon icon={toastIcon} className="mt-1 text-text-subtle" />}
{toastIcon && <Icon icon={toastIcon} color={color} className="mt-1" />}
<VStack space={2} className="w-full">
<div>{children}</div>
{action?.({ hide: onClose })}