mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-30 06:02:00 +02:00
2024.5.0 (#39)
This commit is contained in:
@@ -4,19 +4,18 @@ import type { ReactNode } from 'react';
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
color?: 'danger' | 'warning' | 'success' | 'gray';
|
||||
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger';
|
||||
}
|
||||
export function Banner({ children, className, color = 'gray' }: Props) {
|
||||
|
||||
export function Banner({ children, className, color = 'secondary' }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
`x-theme-banner--${color}`,
|
||||
'border border-dashed italic px-3 py-2 rounded select-auto cursor-text',
|
||||
color === 'gray' && 'border-gray-500/60 bg-gray-300/10 text-gray-800',
|
||||
color === 'warning' && 'border-orange-500/60 bg-orange-300/10 text-orange-800',
|
||||
color === 'danger' && 'border-red-500/60 bg-red-300/10 text-red-800',
|
||||
color === 'success' && 'border-violet-500/60 bg-violet-300/10 text-violet-800',
|
||||
'border-background-highlight bg-background-highlight-secondary text-fg',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -7,10 +7,19 @@ import { Icon } from './Icon';
|
||||
|
||||
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
|
||||
innerClassName?: string;
|
||||
color?: 'custom' | 'default' | 'gray' | 'primary' | 'secondary' | 'warning' | 'danger';
|
||||
color?:
|
||||
| 'custom'
|
||||
| 'default'
|
||||
| 'secondary'
|
||||
| 'primary'
|
||||
| 'info'
|
||||
| 'success'
|
||||
| 'notice'
|
||||
| 'warning'
|
||||
| 'danger';
|
||||
variant?: 'border' | 'solid';
|
||||
isLoading?: boolean;
|
||||
size?: 'xs' | 'sm' | 'md';
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md';
|
||||
justify?: 'start' | 'center';
|
||||
type?: 'button' | 'submit';
|
||||
forDropdown?: boolean;
|
||||
@@ -48,57 +57,44 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
|
||||
const classes = classNames(
|
||||
className,
|
||||
'x-theme-button',
|
||||
`x-theme-button--${variant}`,
|
||||
`x-theme-button--${variant}--${color}`,
|
||||
'text-fg',
|
||||
'border', // They all have borders to ensure the same width
|
||||
'max-w-full min-w-0', // Help with truncation
|
||||
'hocus:opacity-100', // Force opacity for certain hover effects
|
||||
'whitespace-nowrap outline-none',
|
||||
'flex-shrink-0 flex items-center',
|
||||
'focus-visible-or-class:ring rounded-md',
|
||||
'focus-visible-or-class:ring',
|
||||
disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto',
|
||||
justify === 'start' && 'justify-start',
|
||||
justify === 'center' && 'justify-center',
|
||||
size === 'md' && 'h-md px-3',
|
||||
size === 'sm' && 'h-sm px-2.5 text-sm',
|
||||
size === 'xs' && 'h-xs px-2 text-sm',
|
||||
size === 'md' && 'h-md px-3 rounded-md',
|
||||
size === 'sm' && 'h-sm px-2.5 rounded-md',
|
||||
size === 'xs' && 'h-xs px-2 text-sm rounded-md',
|
||||
size === '2xs' && 'h-5 px-1 text-xs rounded',
|
||||
|
||||
// Solids
|
||||
variant === 'solid' && 'border-transparent',
|
||||
variant === 'solid' && color === 'custom' && 'ring-blue-400',
|
||||
variant === 'solid' &&
|
||||
color !== 'custom' &&
|
||||
color !== 'default' &&
|
||||
'bg-background enabled:hocus:bg-background-highlight ring-background-highlight-secondary',
|
||||
variant === 'solid' &&
|
||||
color === 'default' &&
|
||||
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-400',
|
||||
variant === 'solid' &&
|
||||
color === 'gray' &&
|
||||
'text-gray-800 bg-gray-200/70 enabled:hocus:bg-gray-200 ring-blue-400',
|
||||
variant === 'solid' &&
|
||||
color === 'primary' &&
|
||||
'bg-blue-400 text-white ring-blue-700 enabled:hocus:bg-blue-500',
|
||||
variant === 'solid' &&
|
||||
color === 'secondary' &&
|
||||
'bg-violet-400 text-white ring-violet-700 enabled:hocus:bg-violet-500',
|
||||
variant === 'solid' &&
|
||||
color === 'warning' &&
|
||||
'bg-orange-400 text-white ring-orange-700 enabled:hocus:bg-orange-500',
|
||||
variant === 'solid' &&
|
||||
color === 'danger' &&
|
||||
'bg-red-400 text-white ring-red-700 enabled:hocus:bg-red-500',
|
||||
'enabled:hocus:bg-background-highlight ring-fg-info',
|
||||
|
||||
// Borders
|
||||
variant === 'border' && 'border',
|
||||
variant === 'border' &&
|
||||
color !== 'custom' &&
|
||||
color !== 'default' &&
|
||||
'border-fg-subtler text-fg-subtle enabled:hocus:border-fg-subtle enabled:hocus:bg-background-highlight enabled:hocus:text-fg ring-fg-subtler',
|
||||
variant === 'border' &&
|
||||
color === 'default' &&
|
||||
'border-highlight text-gray-700 enabled:hocus:border-focus enabled:hocus:text-gray-800 ring-blue-500/50',
|
||||
variant === 'border' &&
|
||||
color === 'gray' &&
|
||||
'border-gray-500/70 text-gray-700 enabled:hocus:bg-gray-500/20 enabled:hocus:text-gray-800 ring-blue-500/50',
|
||||
variant === 'border' &&
|
||||
color === 'primary' &&
|
||||
'border-blue-500/70 text-blue-700 enabled:hocus:border-blue-500 ring-blue-500/50',
|
||||
variant === 'border' &&
|
||||
color === 'secondary' &&
|
||||
'border-violet-500/70 text-violet-700 enabled:hocus:border-violet-500 ring-violet-500/50',
|
||||
variant === 'border' &&
|
||||
color === 'warning' &&
|
||||
'border-orange-500/70 text-orange-700 enabled:hocus:border-orange-500 ring-orange-500/50',
|
||||
variant === 'border' &&
|
||||
color === 'danger' &&
|
||||
'border-red-500/70 text-red-700 enabled:hocus:border-red-500 ring-red-500/50',
|
||||
'border-background-highlight enabled:hocus:border-fg-subtler enabled:hocus:bg-background-highlight-secondary',
|
||||
);
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -124,7 +120,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
{isLoading ? (
|
||||
<Icon icon="refresh" size={size} className="animate-spin mr-1" />
|
||||
) : leftSlot ? (
|
||||
<div className="mr-1">{leftSlot}</div>
|
||||
<div className="mr-2">{leftSlot}</div>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames(
|
||||
|
||||
@@ -27,15 +27,14 @@ export function Checkbox({
|
||||
<HStack
|
||||
as="label"
|
||||
space={2}
|
||||
alignItems="center"
|
||||
className={classNames(className, 'text-gray-900 text-sm', disabled && 'opacity-disabled')}
|
||||
className={classNames(className, 'text-fg', disabled && 'opacity-disabled')}
|
||||
>
|
||||
<div className={classNames(inputWrapperClassName, 'relative flex')}>
|
||||
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}>
|
||||
<input
|
||||
aria-hidden
|
||||
className={classNames(
|
||||
'appearance-none w-4 h-4 flex-shrink-0 border border-highlight',
|
||||
'rounded hocus:border-focus hocus:bg-focus/[5%] outline-none ring-0',
|
||||
'appearance-none w-4 h-4 flex-shrink-0 border border-background-highlight',
|
||||
'rounded hocus:border-border-focus hocus:bg-focus/[5%] outline-none ring-0',
|
||||
)}
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -12,7 +12,7 @@ export function CountBadge({ count, className }: Props) {
|
||||
aria-hidden
|
||||
className={classNames(
|
||||
className,
|
||||
'opacity-70 border border-highlight text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
|
||||
'opacity-70 border border-background-highlight-secondary text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
|
||||
)}
|
||||
>
|
||||
{count}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function Dialog({
|
||||
|
||||
return (
|
||||
<Overlay open={open} onClose={onClose} portalName="dialog">
|
||||
<div className="absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div className="x-theme-dialog absolute inset-0 flex items-center justify-center pointer-events-none">
|
||||
<div
|
||||
role="dialog"
|
||||
aria-labelledby={titleId}
|
||||
@@ -63,9 +63,9 @@ export function Dialog({
|
||||
className={classNames(
|
||||
className,
|
||||
'grid grid-rows-[auto_auto_minmax(0,1fr)]',
|
||||
'relative bg-gray-50 pointer-events-auto',
|
||||
'relative bg-background pointer-events-auto',
|
||||
'rounded-lg',
|
||||
'dark:border border-highlight shadow shadow-black/10',
|
||||
'border border-background-highlight-secondary shadow-lg shadow-[rgba(0,0,0,0.1)]',
|
||||
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
|
||||
size === 'sm' && 'w-[25rem] max-h-[80vh]',
|
||||
size === 'md' && 'w-[45rem] max-h-[80vh]',
|
||||
@@ -83,7 +83,7 @@ export function Dialog({
|
||||
)}
|
||||
|
||||
{description ? (
|
||||
<p className="px-6 text-gray-700" id={descriptionId}>
|
||||
<p className="px-6 text-fg-subtle" id={descriptionId}>
|
||||
{description}
|
||||
</p>
|
||||
) : (
|
||||
@@ -94,7 +94,7 @@ export function Dialog({
|
||||
className={classNames(
|
||||
'h-full w-full grid grid-cols-[minmax(0,1fr)]',
|
||||
!noPadding && 'px-6 py-2',
|
||||
!noScroll && 'overflow-y-auto',
|
||||
!noScroll && 'overflow-y-auto overflow-x-hidden',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -23,14 +23,14 @@ import React, {
|
||||
import { useKey, useWindowSize } from 'react-use';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useHotKey } from '../../hooks/useHotKey';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { getNodeText } from '../../lib/getNodeText';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { Button } from './Button';
|
||||
import { HotKey } from './HotKey';
|
||||
import { Icon } from './Icon';
|
||||
import { Separator } from './Separator';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
import { Icon } from './Icon';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
|
||||
export type DropdownItemSeparator = {
|
||||
type: 'separator';
|
||||
@@ -59,6 +59,7 @@ export interface DropdownProps {
|
||||
items: DropdownItem[];
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
fullWidth?: boolean;
|
||||
hotKeyAction?: HotkeyAction;
|
||||
}
|
||||
|
||||
@@ -73,7 +74,7 @@ export interface DropdownRef {
|
||||
}
|
||||
|
||||
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
|
||||
{ children, items, onOpen, onClose, hotKeyAction }: DropdownProps,
|
||||
{ children, items, onOpen, onClose, hotKeyAction, fullWidth }: DropdownProps,
|
||||
ref,
|
||||
) {
|
||||
const [isOpen, _setIsOpen] = useState<boolean>(false);
|
||||
@@ -153,6 +154,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
showTriangle
|
||||
fullWidth={fullWidth}
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
items={items}
|
||||
triggerShape={triggerRect ?? null}
|
||||
@@ -203,6 +205,7 @@ interface MenuProps {
|
||||
triggerShape: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'> | null;
|
||||
onClose: () => void;
|
||||
showTriangle?: boolean;
|
||||
fullWidth?: boolean;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
@@ -211,6 +214,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
className,
|
||||
isOpen,
|
||||
items,
|
||||
fullWidth,
|
||||
onClose,
|
||||
triggerShape,
|
||||
defaultSelectedIndex,
|
||||
@@ -224,7 +228,6 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
);
|
||||
const [menuStyles, setMenuStyles] = useState<CSSProperties>({});
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
const [containerWidth, setContainerWidth] = useState<number | null>(null);
|
||||
|
||||
// Calculate the max height so we can scroll
|
||||
const initMenu = useCallback((el: HTMLDivElement | null) => {
|
||||
@@ -349,11 +352,6 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
[handleClose, handleNext, handlePrev, handleSelect, items, selectedIndex],
|
||||
);
|
||||
|
||||
const initContainerRef = (n: HTMLDivElement | null) => {
|
||||
if (n == null) return null;
|
||||
setContainerWidth(n.offsetWidth);
|
||||
};
|
||||
|
||||
const { containerStyles, triangleStyles } = useMemo<{
|
||||
containerStyles: CSSProperties;
|
||||
triangleStyles: CSSProperties | null;
|
||||
@@ -365,22 +363,23 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
const heightAbove = triggerShape.top;
|
||||
const heightBelow = docRect.height - triggerShape.bottom;
|
||||
const hSpaceRemaining = docRect.width - triggerShape.left;
|
||||
const top = triggerShape?.bottom + 5;
|
||||
const top = triggerShape.bottom + 5;
|
||||
const onRight = hSpaceRemaining < 200;
|
||||
const upsideDown = heightAbove > heightBelow && heightBelow < 200;
|
||||
const triggerWidth = triggerShape.right - triggerShape.left;
|
||||
const containerStyles = {
|
||||
top: !upsideDown ? top : undefined,
|
||||
bottom: upsideDown ? docRect.height - top : undefined,
|
||||
right: onRight ? docRect.width - triggerShape?.right : undefined,
|
||||
left: !onRight ? triggerShape?.left : undefined,
|
||||
width: containerWidth ?? 'auto',
|
||||
right: onRight ? docRect.width - triggerShape.right : undefined,
|
||||
left: !onRight ? triggerShape.left : undefined,
|
||||
minWidth: fullWidth ? triggerWidth : undefined,
|
||||
};
|
||||
const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
|
||||
const triangleStyles = onRight
|
||||
? { right: width / 2, marginRight: '-0.2rem', ...size }
|
||||
: { left: width / 2, marginLeft: '-0.2rem', ...size };
|
||||
return { containerStyles, triangleStyles };
|
||||
}, [triggerShape, containerWidth]);
|
||||
}, [fullWidth, triggerShape]);
|
||||
|
||||
const filteredItems = useMemo(
|
||||
() => items.filter((i) => getNodeText(i.label).toLowerCase().includes(filter.toLowerCase())),
|
||||
@@ -413,7 +412,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
)}
|
||||
{isOpen && (
|
||||
<Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
|
||||
<div>
|
||||
<div className="x-theme-menu">
|
||||
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={handleClose} />
|
||||
<motion.div
|
||||
tabIndex={0}
|
||||
@@ -423,7 +422,6 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
dir="ltr"
|
||||
ref={initContainerRef}
|
||||
style={containerStyles}
|
||||
className={classNames(className, 'outline-none my-1 pointer-events-auto fixed z-50')}
|
||||
>
|
||||
@@ -431,7 +429,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
<span
|
||||
aria-hidden
|
||||
style={triangleStyles}
|
||||
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
|
||||
className="bg-background absolute rotate-45 border-background-highlight-secondary border-t border-l"
|
||||
/>
|
||||
)}
|
||||
{containerStyles && (
|
||||
@@ -440,22 +438,21 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
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',
|
||||
'h-auto bg-background rounded-md shadow-lg py-1.5 border',
|
||||
'border-background-highlight-secondary overflow-auto mb-1 mx-0.5',
|
||||
)}
|
||||
>
|
||||
{filter && (
|
||||
<HStack
|
||||
space={2}
|
||||
alignItems="center"
|
||||
className="pb-0.5 px-1.5 mb-2 text-xs border border-highlight mx-2 rounded font-mono h-2xs"
|
||||
className="pb-0.5 px-1.5 mb-2 text-sm border border-background-highlight-secondary mx-2 rounded font-mono h-xs"
|
||||
>
|
||||
<Icon icon="search" size="xs" className="text-gray-700" />
|
||||
<div className="text-gray-800">{filter}</div>
|
||||
<Icon icon="search" size="xs" className="text-fg-subtle" />
|
||||
<div className="text-fg">{filter}</div>
|
||||
</HStack>
|
||||
)}
|
||||
{filteredItems.length === 0 && (
|
||||
<span className="text-gray-500 text-sm text-center px-2 py-1">No matches</span>
|
||||
<span className="text-fg-subtler text-center px-2 py-1">No matches</span>
|
||||
)}
|
||||
{filteredItems.map((item, i) => {
|
||||
if (item.type === 'separator') {
|
||||
@@ -529,17 +526,21 @@ 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>}
|
||||
leftSlot={
|
||||
item.leftSlot && <div className="pr-2 flex justify-start opacity-70">{item.leftSlot}</div>
|
||||
}
|
||||
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
|
||||
innerClassName="!text-left"
|
||||
color="custom"
|
||||
className={classNames(
|
||||
className,
|
||||
'h-xs', // More compact
|
||||
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap',
|
||||
'focus:bg-highlight focus:text-gray-800 rounded',
|
||||
item.variant === 'danger' && 'text-red-600',
|
||||
item.variant === 'notify' && 'text-pink-600',
|
||||
'min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap',
|
||||
'focus:bg-background-highlight focus:text-fg rounded',
|
||||
item.variant === 'default' && 'text-fg-subtle',
|
||||
item.variant === 'danger' && 'text-fg-danger',
|
||||
item.variant === 'notify' && 'text-fg-primary',
|
||||
)}
|
||||
innerClassName="!text-left"
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -5,7 +5,10 @@ interface Props {
|
||||
|
||||
export function DurationTag({ total, headers }: Props) {
|
||||
return (
|
||||
<span title={`HEADER: ${formatMillis(headers)}\nTOTAL: ${formatMillis(total)}`}>
|
||||
<span
|
||||
className="font-mono"
|
||||
title={`HEADER: ${formatMillis(headers)}\nTOTAL: ${formatMillis(total)}`}
|
||||
>
|
||||
{formatMillis(total)}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
@apply w-full block text-base;
|
||||
|
||||
.cm-cursor {
|
||||
@apply border-gray-800 !important;
|
||||
@apply border-fg !important;
|
||||
/* Widen the cursor */
|
||||
@apply border-l-[2px];
|
||||
}
|
||||
|
||||
&.cm-focused {
|
||||
@@ -17,7 +19,7 @@
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
@apply text-gray-800 pl-1 pr-1.5;
|
||||
@apply text-fg pl-1 pr-1.5;
|
||||
}
|
||||
|
||||
.cm-placeholder {
|
||||
@@ -47,27 +49,21 @@
|
||||
/* Style gutters */
|
||||
|
||||
.cm-gutters {
|
||||
@apply border-0 text-gray-500/50;
|
||||
@apply border-0 text-fg-subtler bg-transparent;
|
||||
|
||||
.cm-gutterElement {
|
||||
@apply cursor-default;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder-widget {
|
||||
@apply text-xs text-violet-700 dark:text-violet-700 px-1 mx-[0.5px] rounded cursor-default dark:shadow;
|
||||
.placeholder {
|
||||
/* Colors */
|
||||
@apply bg-background text-fg-subtle border-background-highlight-secondary;
|
||||
@apply hover:border-background-highlight hover:text-fg hover:bg-background-highlight-secondary;
|
||||
|
||||
/* NOTE: Background and border are translucent so we can see text selection through it */
|
||||
@apply bg-violet-500/20 border border-violet-500/20 border-opacity-40;
|
||||
|
||||
/* Bring above on hover */
|
||||
@apply hover:z-10 relative;
|
||||
@apply border px-1 mx-[0.5px] rounded cursor-default dark:shadow;
|
||||
|
||||
-webkit-text-security: none;
|
||||
|
||||
&.placeholder-widget-error {
|
||||
@apply text-red-700 dark:text-red-800 bg-red-300/30 border-red-300/80 border-opacity-40 hover:border-red-300 hover:bg-red-300/40;
|
||||
}
|
||||
}
|
||||
|
||||
.hyperlink-widget {
|
||||
@@ -108,8 +104,7 @@
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
@apply font-mono text-[0.75rem];
|
||||
|
||||
@apply font-mono text-editor;
|
||||
/*
|
||||
* Round corners or they'll stick out of the editor bounds of editor is rounded.
|
||||
* Could potentially be pushed up from the editor like we do with bg color but this
|
||||
@@ -137,7 +132,7 @@
|
||||
|
||||
.cm-editor .fold-gutter-icon::after {
|
||||
@apply block w-1.5 h-1.5 border-transparent -rotate-45
|
||||
border-l border-b border-l-[currentColor] border-b-[currentColor] content-[''];
|
||||
border-l border-b border-l-[currentColor] border-b-[currentColor] content-[''];
|
||||
}
|
||||
|
||||
.cm-editor .fold-gutter-icon[data-open] {
|
||||
@@ -149,12 +144,12 @@
|
||||
}
|
||||
|
||||
.cm-editor .fold-gutter-icon:hover {
|
||||
@apply text-gray-900 bg-gray-300/50;
|
||||
@apply text-fg bg-background-highlight;
|
||||
}
|
||||
|
||||
.cm-editor .cm-foldPlaceholder {
|
||||
@apply px-2 border border-gray-400/50 bg-gray-300/50;
|
||||
@apply hover:text-gray-800 hover:border-gray-400;
|
||||
@apply px-2 border border-fg-subtler bg-background-highlight;
|
||||
@apply hover:text-fg hover:border-fg-subtle text-fg;
|
||||
@apply cursor-default !important;
|
||||
}
|
||||
|
||||
@@ -164,11 +159,13 @@
|
||||
|
||||
.cm-wrapper:not(.cm-readonly) .cm-editor {
|
||||
&.cm-focused .cm-activeLineGutter {
|
||||
@apply text-gray-600;
|
||||
@apply text-fg-subtle;
|
||||
}
|
||||
}
|
||||
|
||||
.cm-wrapper.cm-readonly .cm-editor {
|
||||
.cm-cursor {
|
||||
@apply border-l-2 border-gray-800;
|
||||
@apply border-fg-danger !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,18 +184,18 @@
|
||||
}
|
||||
|
||||
.cm-tooltip.cm-tooltip-hover {
|
||||
@apply shadow-lg bg-gray-100 rounded text-gray-700 border border-gray-500 z-50 pointer-events-auto text-xs;
|
||||
@apply shadow-lg bg-background rounded text-fg-subtle border border-fg-subtler z-50 pointer-events-auto text-sm;
|
||||
@apply px-2 py-1;
|
||||
|
||||
a {
|
||||
@apply text-gray-800;
|
||||
@apply text-fg;
|
||||
|
||||
&:hover {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@apply text-gray-800 bg-gray-800 h-3 w-3 ml-1;
|
||||
@apply text-fg bg-fg-secondary h-3 w-3 ml-1;
|
||||
content: '';
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='black' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-size: contain;
|
||||
@@ -210,7 +207,7 @@
|
||||
/* NOTE: Extra selector required to override default styles */
|
||||
.cm-tooltip.cm-tooltip-autocomplete,
|
||||
.cm-tooltip.cm-completionInfo {
|
||||
@apply shadow-lg bg-gray-100 rounded text-gray-700 border border-gray-300 z-50 pointer-events-auto text-xs;
|
||||
@apply shadow-lg bg-background rounded text-fg-subtle border border-background-highlight z-50 pointer-events-auto text-sm;
|
||||
|
||||
.cm-completionIcon {
|
||||
@apply italic font-mono;
|
||||
@@ -269,7 +266,7 @@
|
||||
}
|
||||
|
||||
&.cm-completionInfo-right {
|
||||
@apply ml-1 -mt-0.5 text-sm;
|
||||
@apply ml-1 -mt-0.5 font-sans;
|
||||
}
|
||||
|
||||
&.cm-completionInfo-right-narrow {
|
||||
@@ -281,24 +278,26 @@
|
||||
}
|
||||
|
||||
&.cm-tooltip-autocomplete {
|
||||
@apply font-mono;
|
||||
|
||||
& > ul {
|
||||
@apply p-1 max-h-[40vh];
|
||||
}
|
||||
|
||||
& > ul > li {
|
||||
@apply cursor-default px-2 rounded-sm text-gray-600 h-7 flex items-center;
|
||||
@apply cursor-default px-2 py-1.5 rounded-sm text-fg-subtle flex items-center;
|
||||
}
|
||||
|
||||
& > ul > li[aria-selected] {
|
||||
@apply bg-highlight text-gray-900;
|
||||
@apply bg-background-highlight-secondary text-fg;
|
||||
}
|
||||
|
||||
.cm-completionIcon {
|
||||
@apply text-xs flex items-center pb-0.5 flex-shrink-0;
|
||||
@apply text-sm flex items-center pb-0.5 flex-shrink-0;
|
||||
}
|
||||
|
||||
.cm-completionLabel {
|
||||
@apply text-gray-700;
|
||||
@apply text-fg-subtle;
|
||||
}
|
||||
|
||||
.cm-completionDetail {
|
||||
@@ -308,7 +307,7 @@
|
||||
}
|
||||
|
||||
.cm-editor .cm-panels {
|
||||
@apply bg-gray-100 backdrop-blur-sm p-1 mb-1 text-gray-800 z-20 rounded-md;
|
||||
@apply bg-background-highlight-secondary backdrop-blur-sm p-1 mb-1 text-fg z-20 rounded-md;
|
||||
|
||||
input,
|
||||
button {
|
||||
@@ -316,19 +315,21 @@
|
||||
}
|
||||
|
||||
button {
|
||||
@apply appearance-none bg-none bg-gray-200 hocus:bg-gray-300 hocus:text-gray-950 border-0 text-gray-800 cursor-default;
|
||||
@apply border-fg-subtler bg-background-highlight text-fg hover:border-fg-info;
|
||||
@apply appearance-none bg-none cursor-default;
|
||||
}
|
||||
|
||||
button[name='close'] {
|
||||
@apply text-gray-600 hocus:text-gray-900 px-2 -mr-1.5 !important;
|
||||
@apply text-fg-subtle hocus:text-fg px-2 -mr-1.5 !important;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply bg-gray-50 border border-gray-500/50 focus:border-focus outline-none;
|
||||
@apply bg-background border-background-highlight focus:border-border-focus;
|
||||
@apply border outline-none cursor-text;
|
||||
}
|
||||
|
||||
label {
|
||||
@apply focus-within:text-gray-950;
|
||||
@apply focus-within:text-fg;
|
||||
}
|
||||
|
||||
/* Hide the "All" button */
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
} from 'react';
|
||||
import { useActiveEnvironment } from '../../../hooks/useActiveEnvironment';
|
||||
import { useActiveWorkspace } from '../../../hooks/useActiveWorkspace';
|
||||
import { useSettings } from '../../../hooks/useSettings';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { HStack } from '../Stacks';
|
||||
import './Editor.css';
|
||||
@@ -85,11 +86,16 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
}: EditorProps,
|
||||
ref,
|
||||
) {
|
||||
const s = useSettings();
|
||||
const e = useActiveEnvironment();
|
||||
const w = useActiveWorkspace();
|
||||
const environment = autocompleteVariables ? e : null;
|
||||
const workspace = autocompleteVariables ? w : null;
|
||||
|
||||
if (s && wrapLines === undefined) {
|
||||
wrapLines = s.editorSoftWrap;
|
||||
}
|
||||
|
||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||
useImperativeHandle(ref, () => cm.current?.view);
|
||||
|
||||
@@ -189,7 +195,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
placeholderCompartment.current.of(
|
||||
placeholderExt(placeholderElFromText(placeholder ?? '')),
|
||||
),
|
||||
wrapLinesCompartment.current.of([]),
|
||||
wrapLinesCompartment.current.of(wrapLines ? [EditorView.lineWrapping] : []),
|
||||
...getExtensions({
|
||||
container,
|
||||
readOnly,
|
||||
@@ -206,7 +212,6 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
|
||||
view = new EditorView({ state, parent: container });
|
||||
cm.current = { view, languageCompartment };
|
||||
syncGutterBg({ parent: container, bgClassList });
|
||||
if (autoFocus) {
|
||||
view.focus();
|
||||
}
|
||||
@@ -270,7 +275,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
ref={initEditorRef}
|
||||
className={classNames(
|
||||
className,
|
||||
'cm-wrapper text-base bg-gray-50',
|
||||
'cm-wrapper text-base',
|
||||
type === 'password' && 'cm-obscure-text',
|
||||
heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height',
|
||||
singleLine ? 'cm-singleline' : 'cm-multiline',
|
||||
@@ -284,12 +289,11 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group relative h-full w-full">
|
||||
<div className="group relative h-full w-full x-theme-editor bg-background">
|
||||
{cmContainer}
|
||||
{decoratedActions && (
|
||||
<HStack
|
||||
space={1}
|
||||
alignItems="center"
|
||||
justifyContent="end"
|
||||
className={classNames(
|
||||
'absolute bottom-2 left-0 right-0',
|
||||
@@ -327,7 +331,25 @@ function getExtensions({
|
||||
undefined;
|
||||
|
||||
return [
|
||||
// NOTE: These *must* be anonymous functions so the references update properly
|
||||
...baseExtensions, // Must be first
|
||||
tooltips({ parent }),
|
||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
|
||||
...(singleLine ? [singleLineExt()] : []),
|
||||
...(!singleLine ? [multiLineExtensions] : []),
|
||||
...(readOnly
|
||||
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
|
||||
: []),
|
||||
|
||||
// ------------------------ //
|
||||
// Things that must be last //
|
||||
// ------------------------ //
|
||||
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (onChange && update.docChanged) {
|
||||
onChange.current?.(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
|
||||
EditorView.domEventHandlers({
|
||||
focus: () => {
|
||||
onFocus.current?.();
|
||||
@@ -342,38 +364,9 @@ function getExtensions({
|
||||
onPaste.current?.(e.clipboardData?.getData('text/plain') ?? '');
|
||||
},
|
||||
}),
|
||||
tooltips({ parent }),
|
||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
|
||||
...(singleLine ? [singleLineExt()] : []),
|
||||
...(!singleLine ? [multiLineExtensions] : []),
|
||||
...(readOnly
|
||||
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
|
||||
: []),
|
||||
|
||||
// Handle onChange
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (onChange && update.docChanged) {
|
||||
onChange.current?.(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
|
||||
...baseExtensions,
|
||||
];
|
||||
}
|
||||
|
||||
const syncGutterBg = ({
|
||||
parent,
|
||||
bgClassList,
|
||||
}: {
|
||||
parent: HTMLDivElement;
|
||||
bgClassList: string[];
|
||||
}) => {
|
||||
const gutterEl = parent.querySelector<HTMLDivElement>('.cm-gutters');
|
||||
if (gutterEl) {
|
||||
gutterEl?.classList.add(...bgClassList);
|
||||
}
|
||||
};
|
||||
|
||||
const placeholderElFromText = (text: string) => {
|
||||
const el = document.createElement('div');
|
||||
el.innerHTML = text.replace('\n', '<br/>');
|
||||
|
||||
@@ -39,51 +39,29 @@ import { text } from './text/extension';
|
||||
import { twig } from './twig/extension';
|
||||
import { url } from './url/extension';
|
||||
|
||||
export const myHighlightStyle = HighlightStyle.define([
|
||||
export const syntaxHighlightStyle = HighlightStyle.define([
|
||||
{
|
||||
tag: [t.documentMeta, t.blockComment, t.lineComment, t.docComment, t.comment],
|
||||
color: 'hsl(var(--color-gray-600))',
|
||||
color: 'var(--fg-subtler)',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
{
|
||||
tag: [t.paren],
|
||||
color: 'hsl(var(--color-gray-900))',
|
||||
tag: [t.paren, t.bracket, t.brace],
|
||||
color: 'var(--fg)',
|
||||
},
|
||||
{
|
||||
tag: [t.name, t.tagName, t.angleBracket, t.docString, t.number],
|
||||
color: 'hsl(var(--color-blue-600))',
|
||||
tag: [t.link, t.name, t.tagName, t.angleBracket, t.docString, t.number],
|
||||
color: 'var(--fg-info)',
|
||||
},
|
||||
{ tag: [t.variableName], color: 'hsl(var(--color-green-600))' },
|
||||
{ tag: [t.bool], color: 'hsl(var(--color-pink-600))' },
|
||||
{ tag: [t.attributeName, t.propertyName], color: 'hsl(var(--color-violet-600))' },
|
||||
{ tag: [t.attributeValue], color: 'hsl(var(--color-orange-600))' },
|
||||
{ tag: [t.string], color: 'hsl(var(--color-yellow-600))' },
|
||||
{ tag: [t.keyword, t.meta, t.operator], color: 'hsl(var(--color-red-600))' },
|
||||
{ tag: [t.variableName], color: 'var(--fg-success)' },
|
||||
{ tag: [t.bool], color: 'var(--fg-warning)' },
|
||||
{ tag: [t.attributeName, t.propertyName], color: 'var(--fg-primary)' },
|
||||
{ tag: [t.attributeValue], color: 'var(--fg-warning)' },
|
||||
{ tag: [t.string], color: 'var(--fg-notice)' },
|
||||
{ tag: [t.atom, t.meta, t.operator, t.bool, t.null, t.keyword], color: 'var(--fg-danger)' },
|
||||
]);
|
||||
|
||||
const myTheme = EditorView.theme({}, { dark: true });
|
||||
|
||||
// export const defaultHighlightStyle = HighlightStyle.define([
|
||||
// { tag: t.meta, color: '#404740' },
|
||||
// { tag: t.link, textDecoration: 'underline' },
|
||||
// { tag: t.heading, textDecoration: 'underline', fontWeight: 'bold' },
|
||||
// { tag: t.emphasis, fontStyle: 'italic' },
|
||||
// { tag: t.strong, fontWeight: 'bold' },
|
||||
// { tag: t.strikethrough, textDecoration: 'line-through' },
|
||||
// { tag: t.keyword, color: '#708' },
|
||||
// { tag: [t.atom, t.bool, t.url, t.contentSeparator, t.labelName], color: '#219' },
|
||||
// { tag: [t.literal, t.inserted], color: '#164' },
|
||||
// { tag: [t.string, t.deleted], color: '#a11' },
|
||||
// { tag: [t.regexp, t.escape, t.special(t.string)], color: '#e40' },
|
||||
// { tag: t.definition(t.variableName), color: '#00f' },
|
||||
// { tag: t.local(t.variableName), color: '#30a' },
|
||||
// { tag: [t.typeName, t.namespace], color: '#085' },
|
||||
// { tag: t.className, color: '#167' },
|
||||
// { tag: [t.special(t.variableName), t.macroName], color: '#256' },
|
||||
// { tag: t.definition(t.propertyName), color: '#00c' },
|
||||
// { tag: t.comment, color: '#940' },
|
||||
// { tag: t.invalid, color: '#f00' },
|
||||
// ]);
|
||||
const syntaxTheme = EditorView.theme({}, { dark: true });
|
||||
|
||||
const syntaxExtensions: Record<string, LanguageSupport> = {
|
||||
'application/graphql': graphqlLanguageSupport(),
|
||||
@@ -123,14 +101,15 @@ export const baseExtensions = [
|
||||
dropCursor(),
|
||||
drawSelection(),
|
||||
autocompletion({
|
||||
tooltipClass: () => 'x-theme-menu',
|
||||
closeOnBlur: true, // Set to `false` for debugging in devtools without closing it
|
||||
compareCompletions: (a, b) => {
|
||||
// Don't sort completions at all, only on boost
|
||||
return (a.boost ?? 0) - (b.boost ?? 0);
|
||||
},
|
||||
}),
|
||||
syntaxHighlighting(myHighlightStyle),
|
||||
myTheme,
|
||||
syntaxHighlighting(syntaxHighlightStyle),
|
||||
syntaxTheme,
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
];
|
||||
|
||||
|
||||
@@ -20,7 +20,11 @@ export interface GenericCompletionConfig {
|
||||
/**
|
||||
* Complete options, always matching until the start of the line
|
||||
*/
|
||||
export function genericCompletion({ options, minMatch = 1 }: GenericCompletionConfig) {
|
||||
export function genericCompletion(config?: GenericCompletionConfig) {
|
||||
if (config == null) return [];
|
||||
|
||||
const { minMatch = 1, options } = config;
|
||||
|
||||
return function completions(context: CompletionContext) {
|
||||
const toMatch = context.matchBefore(/.*/);
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { LanguageSupport } from '@codemirror/language';
|
||||
import { LRLanguage } from '@codemirror/language';
|
||||
import { parseMixed } from '@lezer/common';
|
||||
import type { Environment, Workspace } from '../../../../lib/models';
|
||||
import type { GenericCompletionConfig } from '../genericCompletion';
|
||||
import { genericCompletion } from '../genericCompletion';
|
||||
import { placeholders } from './placeholder';
|
||||
import { textLanguageName } from '../text/extension';
|
||||
import { twigCompletion } from './completion';
|
||||
import { placeholders } from './placeholder';
|
||||
import { parser as twigParser } from './twig';
|
||||
import type { Environment, Workspace } from '../../../../lib/models';
|
||||
|
||||
export function twig(
|
||||
base: LanguageSupport,
|
||||
@@ -15,25 +15,19 @@ export function twig(
|
||||
workspace: Workspace | null,
|
||||
autocomplete?: GenericCompletionConfig,
|
||||
) {
|
||||
const variables =
|
||||
[...(workspace?.variables ?? []), ...(environment?.variables ?? [])].filter((v) => v.enabled) ??
|
||||
[];
|
||||
const completions = twigCompletion({ options: variables });
|
||||
|
||||
const language = mixLanguage(base);
|
||||
const completion = language.data.of({ autocomplete: completions });
|
||||
const completionBase = base.language.data.of({ autocomplete: completions });
|
||||
const additionalCompletion = autocomplete
|
||||
? [base.language.data.of({ autocomplete: genericCompletion(autocomplete) })]
|
||||
: [];
|
||||
const allVariables = [...(workspace?.variables ?? []), ...(environment?.variables ?? [])];
|
||||
const variables = allVariables.filter((v) => v.enabled) ?? [];
|
||||
const completions = twigCompletion({ options: variables });
|
||||
|
||||
return [
|
||||
language,
|
||||
completion,
|
||||
completionBase,
|
||||
base.support,
|
||||
placeholders(variables),
|
||||
...additionalCompletion,
|
||||
language.data.of({ autocomplete: completions }),
|
||||
base.language.data.of({ autocomplete: completions }),
|
||||
language.data.of({ autocomplete: genericCompletion(autocomplete) }),
|
||||
base.language.data.of({ autocomplete: genericCompletion(autocomplete) }),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ class PlaceholderWidget extends WidgetType {
|
||||
}
|
||||
toDOM() {
|
||||
const elt = document.createElement('span');
|
||||
elt.className = `placeholder-widget ${
|
||||
!this.isExistingVariable ? 'placeholder-widget-error' : ''
|
||||
elt.className = `x-theme-placeholder placeholder ${
|
||||
this.isExistingVariable ? 'x-theme-placeholder--primary' : 'x-theme-placeholder--danger'
|
||||
}`;
|
||||
elt.title = !this.isExistingVariable ? 'Variable not found in active environment' : '';
|
||||
elt.textContent = this.name;
|
||||
|
||||
@@ -9,8 +9,8 @@ export function FormattedError({ children }: Props) {
|
||||
return (
|
||||
<pre
|
||||
className={classNames(
|
||||
'w-full text-sm select-auto cursor-text bg-gray-100 p-3 rounded',
|
||||
'whitespace-pre-wrap border border-red-500 border-dashed overflow-x-auto',
|
||||
'w-full select-auto cursor-text bg-background-highlight-secondary p-3 rounded',
|
||||
'whitespace-pre-wrap border border-fg-danger border-dashed overflow-x-auto',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function Heading({ className, size = 1, ...props }: Props) {
|
||||
<Component
|
||||
className={classNames(
|
||||
className,
|
||||
'font-semibold text-gray-900',
|
||||
'font-semibold text-fg',
|
||||
size === 1 && 'text-2xl',
|
||||
size === 2 && 'text-xl',
|
||||
size === 3 && 'text-lg',
|
||||
|
||||
@@ -22,7 +22,7 @@ export function HotKey({ action, className, variant }: Props) {
|
||||
className={classNames(
|
||||
className,
|
||||
variant === 'with-bg' && 'rounded border',
|
||||
'text-gray-1000 text-opacity-disabled',
|
||||
'text-fg-subtler',
|
||||
)}
|
||||
>
|
||||
{labelParts.map((char, index) => (
|
||||
|
||||
@@ -7,5 +7,5 @@ interface Props {
|
||||
|
||||
export function HotKeyLabel({ action }: Props) {
|
||||
const label = useHotKeyLabel(action);
|
||||
return <span>{label}</span>;
|
||||
return <span className="text-fg-subtle whitespace-nowrap">{label}</span>;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ interface Props {
|
||||
|
||||
export const HotKeyList = ({ hotkeys, bottomSlot }: Props) => {
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center text-gray-700 text-sm">
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<VStack space={2}>
|
||||
{hotkeys.map((hotkey) => (
|
||||
<HStack key={hotkey} className="grid grid-cols-2">
|
||||
|
||||
@@ -27,7 +27,7 @@ export function HttpMethodTag({ request, className }: Props) {
|
||||
|
||||
const m = method.toLowerCase();
|
||||
return (
|
||||
<span className={classNames(className, 'text-2xs font-mono opacity-50')}>
|
||||
<span className={classNames(className, 'text-xs font-mono text-fg-subtle')}>
|
||||
{methodMap[m] ?? m.slice(0, 3).toUpperCase()}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as lucide from 'lucide-react';
|
||||
import classNames from 'classnames';
|
||||
import * as lucide from 'lucide-react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
@@ -42,8 +42,10 @@ const icons = {
|
||||
magicWand: lucide.Wand2Icon,
|
||||
minus: lucide.MinusIcon,
|
||||
moreVertical: lucide.MoreVerticalIcon,
|
||||
moon: lucide.MoonIcon,
|
||||
paste: lucide.ClipboardPasteIcon,
|
||||
pencil: lucide.PencilIcon,
|
||||
pin: lucide.PinIcon,
|
||||
plug: lucide.Plug,
|
||||
plus: lucide.PlusIcon,
|
||||
plusCircle: lucide.PlusCircleIcon,
|
||||
@@ -54,6 +56,7 @@ const icons = {
|
||||
settings2: lucide.Settings2Icon,
|
||||
settings: lucide.SettingsIcon,
|
||||
sparkles: lucide.SparklesIcon,
|
||||
sun: lucide.SunIcon,
|
||||
trash: lucide.TrashIcon,
|
||||
update: lucide.RefreshCcwIcon,
|
||||
upload: lucide.UploadIcon,
|
||||
@@ -65,7 +68,7 @@ const icons = {
|
||||
export interface IconProps {
|
||||
icon: keyof typeof icons;
|
||||
className?: string;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg';
|
||||
spin?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
@@ -82,6 +85,7 @@ export const Icon = memo(function Icon({ icon, spin, size = 'md', className, tit
|
||||
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',
|
||||
spin && 'animate-spin',
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -52,11 +52,12 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
||||
size={size}
|
||||
className={classNames(
|
||||
className,
|
||||
'relative flex-shrink-0 text-gray-700 hover:text-gray-1000',
|
||||
'group/button relative flex-shrink-0 text-fg-subtle',
|
||||
'!px-0',
|
||||
size === 'md' && 'w-9',
|
||||
size === 'sm' && 'w-8',
|
||||
size === 'xs' && 'w-6',
|
||||
size === '2xs' && 'w-5',
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -71,6 +72,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
||||
spin={spin}
|
||||
className={classNames(
|
||||
iconClassName,
|
||||
'group-hover/button:text-fg',
|
||||
props.disabled && 'opacity-70',
|
||||
confirmed && 'text-green-600',
|
||||
)}
|
||||
|
||||
@@ -6,8 +6,8 @@ export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanEleme
|
||||
<code
|
||||
className={classNames(
|
||||
className,
|
||||
'font-mono text-xs bg-highlight border-0 border-gray-200/30',
|
||||
'px-1.5 py-0.5 rounded text-gray-800 shadow-inner',
|
||||
'font-mono text-shrink bg-background-highlight-secondary border border-background-highlight',
|
||||
'px-1.5 py-0.5 rounded text-fg shadow-inner',
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -135,7 +135,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
htmlFor={id}
|
||||
className={classNames(
|
||||
labelClassName,
|
||||
'text-sm text-gray-900 whitespace-nowrap',
|
||||
'text-fg-subtle whitespace-nowrap',
|
||||
hideLabel && 'sr-only',
|
||||
)}
|
||||
>
|
||||
@@ -145,10 +145,11 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
alignItems="stretch"
|
||||
className={classNames(
|
||||
containerClassName,
|
||||
'relative w-full rounded-md text-gray-900',
|
||||
'x-theme-input',
|
||||
'relative w-full rounded-md text-fg',
|
||||
'border',
|
||||
focused ? 'border-focus' : 'border-highlight',
|
||||
!isValid && '!border-invalid',
|
||||
focused ? 'border-border-focus' : 'border-background-highlight',
|
||||
!isValid && '!border-fg-danger',
|
||||
size === 'md' && 'min-h-md',
|
||||
size === 'sm' && 'min-h-sm',
|
||||
size === 'xs' && 'min-h-xs',
|
||||
@@ -156,7 +157,6 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
>
|
||||
{leftSlot}
|
||||
<HStack
|
||||
alignItems="center"
|
||||
className={classNames(
|
||||
'w-full min-w-0',
|
||||
leftSlot && 'pl-0.5 -ml-2',
|
||||
@@ -186,7 +186,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
title={obscured ? `Show ${label}` : `Obscure ${label}`}
|
||||
size="xs"
|
||||
className="mr-0.5 group/obscure !h-auto my-0.5"
|
||||
iconClassName="text-gray-500 group-hover/obscure:text-gray-800"
|
||||
iconClassName="text-fg-subtle group-hover/obscure:text-fg"
|
||||
iconSize="sm"
|
||||
icon={obscured ? 'eye' : 'eyeClosed'}
|
||||
onClick={() => setObscured((o) => !o)}
|
||||
|
||||
@@ -41,7 +41,7 @@ export const JsonAttributeTree = ({ depth = 0, attrKey, attrValue, attrKeyJsonPa
|
||||
: null,
|
||||
isExpandable: Object.keys(attrValue).length > 0,
|
||||
label: isExpanded ? `{${Object.keys(attrValue).length || ' '}}` : `{⋯}`,
|
||||
labelClassName: 'text-gray-600',
|
||||
labelClassName: 'text-fg-subtler',
|
||||
};
|
||||
} else if (jsonType === '[object Array]') {
|
||||
return {
|
||||
@@ -59,7 +59,7 @@ export const JsonAttributeTree = ({ depth = 0, attrKey, attrValue, attrKeyJsonPa
|
||||
: null,
|
||||
isExpandable: attrValue.length > 0,
|
||||
label: isExpanded ? `[${attrValue.length || ' '}]` : `[⋯]`,
|
||||
labelClassName: 'text-gray-600',
|
||||
labelClassName: 'text-subtler',
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
@@ -67,22 +67,20 @@ export const JsonAttributeTree = ({ depth = 0, attrKey, attrValue, attrKeyJsonPa
|
||||
isExpandable: false,
|
||||
label: jsonType === '[object String]' ? `"${attrValue}"` : `${attrValue}`,
|
||||
labelClassName: classNames(
|
||||
jsonType === '[object Boolean]' && 'text-pink-600',
|
||||
jsonType === '[object Number]' && 'text-blue-600',
|
||||
jsonType === '[object String]' && 'text-yellow-600',
|
||||
jsonType === '[object Null]' && 'text-red-600',
|
||||
jsonType === '[object Boolean]' && 'text-fg-primary',
|
||||
jsonType === '[object Number]' && 'text-fg-info',
|
||||
jsonType === '[object String]' && 'text-fg-notice',
|
||||
jsonType === '[object Null]' && 'text-fg-danger',
|
||||
),
|
||||
};
|
||||
}
|
||||
}, [attrValue, attrKeyJsonPath, isExpanded, depth]);
|
||||
|
||||
const labelEl = (
|
||||
<span className={classNames(labelClassName, 'select-text group-hover:text-gray-800')}>
|
||||
{label}
|
||||
</span>
|
||||
<span className={classNames(labelClassName, 'select-text group-hover:text-fg')}>{label}</span>
|
||||
);
|
||||
return (
|
||||
<div className={classNames(/*depth === 0 && '-ml-4',*/ 'font-mono text-2xs')}>
|
||||
<div className={classNames(/*depth === 0 && '-ml-4',*/ 'font-mono text-xs')}>
|
||||
<div className="flex items-center">
|
||||
{isExpandable ? (
|
||||
<button className="group relative flex items-center pl-4 w-full" onClick={toggleExpanded}>
|
||||
@@ -91,18 +89,18 @@ export const JsonAttributeTree = ({ depth = 0, attrKey, attrValue, attrKeyJsonPa
|
||||
icon="chevronRight"
|
||||
className={classNames(
|
||||
'left-0 absolute transition-transform flex items-center',
|
||||
'text-gray-600 group-hover:text-gray-900',
|
||||
'text-fg-subtler group-hover:text-fg-subtle',
|
||||
isExpanded ? 'rotate-90' : '',
|
||||
)}
|
||||
/>
|
||||
<span className="text-violet-600 group-hover:text-violet-700 mr-1.5 whitespace-nowrap">
|
||||
<span className="text-fg-primary group-hover:text-fg-primary mr-1.5 whitespace-nowrap">
|
||||
{attrKey === undefined ? '$' : attrKey}:
|
||||
</span>
|
||||
{labelEl}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-violet-600 mr-1.5 pl-4 whitespace-nowrap select-text">
|
||||
<span className="text-fg-primary mr-1.5 pl-4 whitespace-nowrap select-text">
|
||||
{attrKey}:
|
||||
</span>
|
||||
{labelEl}
|
||||
|
||||
@@ -30,7 +30,7 @@ export function KeyValueRow({ label, value, labelClassName }: Props) {
|
||||
return (
|
||||
<>
|
||||
<td
|
||||
className={classNames('py-0.5 pr-2 text-gray-700 select-text cursor-text', labelClassName)}
|
||||
className={classNames('py-0.5 pr-2 text-fg-subtle select-text cursor-text', labelClassName)}
|
||||
>
|
||||
{label}
|
||||
</td>
|
||||
|
||||
@@ -388,8 +388,8 @@ function PairEditorRow({
|
||||
{pairContainer.pair.isFile ? (
|
||||
<Button
|
||||
size="xs"
|
||||
color="gray"
|
||||
className="font-mono text-xs"
|
||||
color="secondary"
|
||||
className="font-mono text-2xs rtl"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const selected = await open({
|
||||
@@ -403,7 +403,9 @@ function PairEditorRow({
|
||||
handleChangeValueFile(selected.path);
|
||||
}}
|
||||
>
|
||||
{getFileName(pairContainer.pair.value) || 'Select File'}
|
||||
{/* Special character to insert ltr text in rtl element without making things wonky */}
|
||||
‎
|
||||
{pairContainer.pair.value || 'Select File'}
|
||||
</Button>
|
||||
) : (
|
||||
<Input
|
||||
@@ -494,9 +496,3 @@ const newPairContainer = (initialPair?: Pair): PairContainer => {
|
||||
const pair = initialPair ?? { name: '', value: '', enabled: true, isFile: false };
|
||||
return { id, pair };
|
||||
};
|
||||
|
||||
const getFileName = (path: string | null | undefined): string => {
|
||||
if (typeof path !== 'string') return '';
|
||||
const parts = path.split(/[\\/]/);
|
||||
return parts[parts.length - 1] ?? '';
|
||||
};
|
||||
|
||||
152
src-web/components/core/PlainInput.tsx
Normal file
152
src-web/components/core/PlainInput.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import classNames from 'classnames';
|
||||
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { InputProps } from './Input';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
export type PlainInputProps = Omit<InputProps, 'wrapLines' | 'onKeyDown' | 'type'> & {
|
||||
type: 'text' | 'password' | 'number';
|
||||
step?: number;
|
||||
};
|
||||
|
||||
export const PlainInput = forwardRef<HTMLInputElement, PlainInputProps>(function Input(
|
||||
{
|
||||
className,
|
||||
containerClassName,
|
||||
defaultValue,
|
||||
forceUpdateKey,
|
||||
hideLabel,
|
||||
label,
|
||||
labelClassName,
|
||||
labelPosition = 'top',
|
||||
leftSlot,
|
||||
name,
|
||||
onBlur,
|
||||
onChange,
|
||||
onFocus,
|
||||
onPaste,
|
||||
placeholder,
|
||||
require,
|
||||
rightSlot,
|
||||
size = 'md',
|
||||
type = 'text',
|
||||
validate,
|
||||
...props
|
||||
}: PlainInputProps,
|
||||
ref,
|
||||
) {
|
||||
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
|
||||
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
setFocused(true);
|
||||
onFocus?.();
|
||||
}, [onFocus]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setFocused(false);
|
||||
onBlur?.();
|
||||
}, [onBlur]);
|
||||
|
||||
const id = `input-${name}`;
|
||||
const inputClassName = classNames(
|
||||
className,
|
||||
'!bg-transparent min-w-0 h-auto w-full focus:outline-none placeholder:text-placeholder',
|
||||
'px-1.5 text-xs font-mono',
|
||||
);
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
if (require && !validateRequire(currentValue)) return false;
|
||||
if (validate && !validate(currentValue)) return false;
|
||||
return true;
|
||||
}, [currentValue, validate, require]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
setCurrentValue(value);
|
||||
onChange?.(value);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'pointer-events-auto', // Just in case we're placing in disabled parent
|
||||
labelPosition === 'left' && 'flex items-center gap-2',
|
||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||
)}
|
||||
>
|
||||
<label
|
||||
htmlFor={id}
|
||||
className={classNames(
|
||||
labelClassName,
|
||||
'text-fg-subtle whitespace-nowrap',
|
||||
hideLabel && 'sr-only',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<HStack
|
||||
alignItems="stretch"
|
||||
className={classNames(
|
||||
containerClassName,
|
||||
'x-theme-input',
|
||||
'relative w-full rounded-md text-fg',
|
||||
'border',
|
||||
focused ? 'border-border-focus' : 'border-background-highlight',
|
||||
!isValid && '!border-fg-danger',
|
||||
size === 'md' && 'min-h-md',
|
||||
size === 'sm' && 'min-h-sm',
|
||||
size === 'xs' && 'min-h-xs',
|
||||
)}
|
||||
>
|
||||
{leftSlot}
|
||||
<HStack
|
||||
className={classNames(
|
||||
'w-full min-w-0',
|
||||
leftSlot && 'pl-0.5 -ml-2',
|
||||
rightSlot && 'pr-0.5 -mr-2',
|
||||
)}
|
||||
>
|
||||
<input
|
||||
ref={ref}
|
||||
key={forceUpdateKey}
|
||||
id={id}
|
||||
type={type === 'password' && !obscured ? 'text' : type}
|
||||
defaultValue={defaultValue}
|
||||
placeholder={placeholder}
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
onPaste={(e) => onPaste?.(e.clipboardData.getData('Text'))}
|
||||
className={inputClassName}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
{...props}
|
||||
/>
|
||||
</HStack>
|
||||
{type === 'password' && (
|
||||
<IconButton
|
||||
title={obscured ? `Show ${label}` : `Obscure ${label}`}
|
||||
size="xs"
|
||||
className="mr-0.5 group/obscure !h-auto my-0.5"
|
||||
iconClassName="text-fg-subtle group-hover/obscure:text-fg"
|
||||
iconSize="sm"
|
||||
icon={obscured ? 'eye' : 'eyeClosed'}
|
||||
onClick={() => setObscured((o) => !o)}
|
||||
/>
|
||||
)}
|
||||
{rightSlot}
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
function validateRequire(v: string) {
|
||||
return v.length > 0;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { DropdownItem, DropdownItemSeparator, DropdownProps } from './Dropdown';
|
||||
import { Dropdown } from './Dropdown';
|
||||
@@ -9,6 +10,7 @@ export type RadioDropdownItem<T = string | null> =
|
||||
label: string;
|
||||
shortLabel?: string;
|
||||
value: T;
|
||||
rightSlot?: ReactNode;
|
||||
}
|
||||
| DropdownItemSeparator;
|
||||
|
||||
@@ -37,9 +39,10 @@ export function RadioDropdown<T = string | null>({
|
||||
key: item.label,
|
||||
label: item.label,
|
||||
shortLabel: item.shortLabel,
|
||||
rightSlot: item.rightSlot,
|
||||
onSelect: () => onChange(item.value),
|
||||
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
|
||||
};
|
||||
} as DropdownProps['items'][0];
|
||||
}
|
||||
}),
|
||||
...((extraItems ? [{ type: 'separator' }, ...extraItems] : []) as DropdownItem[]),
|
||||
@@ -47,5 +50,9 @@ export function RadioDropdown<T = string | null>({
|
||||
[items, extraItems, value, onChange],
|
||||
);
|
||||
|
||||
return <Dropdown items={dropdownItems}>{children}</Dropdown>;
|
||||
return (
|
||||
<Dropdown fullWidth items={dropdownItems}>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useOsInfo } from '../../hooks/useOsInfo';
|
||||
import type { ButtonProps } from './Button';
|
||||
import { Button } from './Button';
|
||||
import type { RadioDropdownItem } from './RadioDropdown';
|
||||
import { RadioDropdown } from './RadioDropdown';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props<T extends string> {
|
||||
export interface SelectProps<T extends string> {
|
||||
name: string;
|
||||
label: string;
|
||||
labelPosition?: 'top' | 'left';
|
||||
labelClassName?: string;
|
||||
hideLabel?: boolean;
|
||||
value: T;
|
||||
options: { label: string; value: T }[];
|
||||
leftSlot?: ReactNode;
|
||||
options: RadioDropdownItem<T>[];
|
||||
onChange: (value: T) => void;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
size?: ButtonProps['size'];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -21,18 +30,22 @@ export function Select<T extends string>({
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
leftSlot,
|
||||
onChange,
|
||||
className,
|
||||
size = 'md',
|
||||
}: Props<T>) {
|
||||
}: SelectProps<T>) {
|
||||
const osInfo = useOsInfo();
|
||||
const [focused, setFocused] = useState<boolean>(false);
|
||||
const id = `input-${name}`;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'x-theme-input',
|
||||
'w-full',
|
||||
'pointer-events-auto', // Just in case we're placing in disabled parent
|
||||
labelPosition === 'left' && 'flex items-center gap-2',
|
||||
labelPosition === 'left' && 'grid grid-cols-[auto_1fr] items-center gap-2',
|
||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||
)}
|
||||
>
|
||||
@@ -40,38 +53,69 @@ export function Select<T extends string>({
|
||||
htmlFor={id}
|
||||
className={classNames(
|
||||
labelClassName,
|
||||
'text-sm text-gray-900 whitespace-nowrap',
|
||||
'text-fg-subtle whitespace-nowrap',
|
||||
hideLabel && 'sr-only',
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</label>
|
||||
<select
|
||||
value={value}
|
||||
style={selectBackgroundStyles}
|
||||
onChange={(e) => onChange(e.target.value as T)}
|
||||
className={classNames(
|
||||
'font-mono text-xs border w-full outline-none bg-transparent pl-2 pr-7',
|
||||
'border-highlight focus:border-focus',
|
||||
size === 'xs' && 'h-xs',
|
||||
size === 'sm' && 'h-sm',
|
||||
size === 'md' && 'h-md',
|
||||
size === 'lg' && 'h-lg',
|
||||
)}
|
||||
>
|
||||
{options.map(({ label, value }) => (
|
||||
<option key={label} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{osInfo?.osType === 'macos' ? (
|
||||
<HStack
|
||||
space={2}
|
||||
className={classNames(
|
||||
'w-full rounded-md text-fg text-sm font-mono',
|
||||
'pl-2',
|
||||
'border',
|
||||
focused ? 'border-border-focus' : 'border-background-highlight',
|
||||
size === 'xs' && 'h-xs',
|
||||
size === 'sm' && 'h-sm',
|
||||
size === 'md' && 'h-md',
|
||||
)}
|
||||
>
|
||||
{leftSlot && <div>{leftSlot}</div>}
|
||||
<select
|
||||
value={value}
|
||||
style={selectBackgroundStyles}
|
||||
onChange={(e) => onChange(e.target.value as T)}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
className={classNames('pr-7 w-full outline-none bg-transparent')}
|
||||
>
|
||||
{options.map((o) => {
|
||||
if (o.type === 'separator') return null;
|
||||
return (
|
||||
<option key={o.label} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
</select>
|
||||
</HStack>
|
||||
) : (
|
||||
// Use custom "select" component until Tauri can be configured to have select menus not always appear in
|
||||
// light mode
|
||||
<RadioDropdown value={value} onChange={onChange} items={options}>
|
||||
<Button
|
||||
className="w-full text-sm font-mono"
|
||||
justify="start"
|
||||
variant="border"
|
||||
size={size}
|
||||
leftSlot={leftSlot}
|
||||
forDropdown
|
||||
>
|
||||
{options.find((o) => o.type !== 'separator' && o.value === value)?.label ?? '--'}
|
||||
</Button>
|
||||
</RadioDropdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectBackgroundStyles = {
|
||||
const selectBackgroundStyles: CSSProperties = {
|
||||
backgroundImage: `url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3e%3cpath stroke='%236b7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='M6 8l4 4 4-4'/%3e%3c/svg%3e")`,
|
||||
backgroundPosition: 'right 0.3rem center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '1.5em 1.5em',
|
||||
appearance: 'none',
|
||||
printColorAdjust: 'exact',
|
||||
};
|
||||
|
||||
@@ -8,19 +8,13 @@ interface Props {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function Separator({
|
||||
className,
|
||||
variant = 'primary',
|
||||
orientation = 'horizontal',
|
||||
children,
|
||||
}: Props) {
|
||||
export function Separator({ className, orientation = 'horizontal', children }: Props) {
|
||||
return (
|
||||
<div role="separator" className={classNames(className, 'flex items-center')}>
|
||||
{children && <div className="text-xs text-gray-500 mr-2 whitespace-nowrap">{children}</div>}
|
||||
{children && <div className="text-sm text-fg-subtler mr-2 whitespace-nowrap">{children}</div>}
|
||||
<div
|
||||
className={classNames(
|
||||
variant === 'primary' && 'bg-highlight',
|
||||
variant === 'secondary' && 'bg-highlightSecondary',
|
||||
'bg-background-highlight',
|
||||
orientation === 'horizontal' && 'w-full h-[1px]',
|
||||
orientation === 'vertical' && 'h-full w-[1px]',
|
||||
)}
|
||||
|
||||
@@ -21,7 +21,7 @@ export function SizeTag({ contentLength }: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<span title={`${contentLength} bytes`}>
|
||||
<span className="font-mono" title={`${contentLength} bytes`}>
|
||||
{Math.round(num * 10) / 10} {unit}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -19,7 +19,7 @@ interface HStackProps extends BaseStackProps {
|
||||
}
|
||||
|
||||
export const HStack = forwardRef(function HStack(
|
||||
{ className, space, children, ...props }: HStackProps,
|
||||
{ className, space, children, alignItems = 'center', ...props }: HStackProps,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
@@ -27,6 +27,7 @@ export const HStack = forwardRef(function HStack(
|
||||
<BaseStack
|
||||
ref={ref}
|
||||
className={classNames(className, 'flex-row', space != null && gapClasses[space])}
|
||||
alignItems={alignItems}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -10,17 +10,18 @@ interface Props {
|
||||
export function StatusTag({ response, className, showReason }: Props) {
|
||||
const { status } = response;
|
||||
const label = status < 100 ? 'ERR' : status;
|
||||
const category = `${status}`[0];
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
'font-mono',
|
||||
status >= 0 && status < 100 && 'text-red-600',
|
||||
status >= 100 && status < 200 && 'text-green-600',
|
||||
status >= 200 && status < 300 && 'text-green-600',
|
||||
status >= 300 && status < 400 && 'text-pink-600',
|
||||
status >= 400 && status < 500 && 'text-orange-600',
|
||||
status >= 500 && 'text-red-600',
|
||||
category === '0' && 'text-fg-danger',
|
||||
category === '1' && 'text-fg-info',
|
||||
category === '2' && 'text-fg-success',
|
||||
category === '3' && 'text-fg-primary',
|
||||
category === '4' && 'text-fg-warning',
|
||||
category === '5' && 'text-fg-danger',
|
||||
)}
|
||||
>
|
||||
{label} {showReason && response.statusReason && response.statusReason}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { memo, useCallback, useEffect, useRef } from 'react';
|
||||
import { Button } from '../Button';
|
||||
import { Icon } from '../Icon';
|
||||
import type { RadioDropdownProps } from '../RadioDropdown';
|
||||
import { RadioDropdown } from '../RadioDropdown';
|
||||
@@ -25,6 +24,7 @@ interface Props {
|
||||
tabListClassName?: string;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
addBorders?: boolean;
|
||||
}
|
||||
|
||||
export function Tabs({
|
||||
@@ -35,6 +35,7 @@ export function Tabs({
|
||||
tabs,
|
||||
className,
|
||||
tabListClassName,
|
||||
addBorders,
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -73,17 +74,21 @@ export function Tabs({
|
||||
aria-label={label}
|
||||
className={classNames(
|
||||
tabListClassName,
|
||||
addBorders && '!-ml-1 h-md mt-2',
|
||||
'flex items-center overflow-x-auto overflow-y-visible hide-scrollbars mt-1 mb-2',
|
||||
// Give space for button focus states within overflow boundary.
|
||||
'-ml-5 pl-3 pr-1 py-1',
|
||||
)}
|
||||
>
|
||||
<HStack space={2} className="flex-shrink-0">
|
||||
<HStack space={2} className="h-full flex-shrink-0">
|
||||
{tabs.map((t) => {
|
||||
const isActive = t.value === value;
|
||||
const btnClassName = classNames(
|
||||
isActive ? 'text-gray-800' : 'text-gray-600 hover:text-gray-700',
|
||||
'h-full flex items-center rounded',
|
||||
'!px-2 ml-[1px]',
|
||||
addBorders && 'border',
|
||||
isActive ? 'text-fg' : 'text-fg-subtle hover:text-fg',
|
||||
isActive && addBorders ? 'border-background-highlight' : 'border-transparent',
|
||||
);
|
||||
|
||||
if ('options' in t) {
|
||||
@@ -97,39 +102,32 @@ export function Tabs({
|
||||
value={t.options.value}
|
||||
onChange={t.options.onChange}
|
||||
>
|
||||
<Button
|
||||
<button
|
||||
color="custom"
|
||||
size="sm"
|
||||
onClick={isActive ? undefined : () => handleTabChange(t.value)}
|
||||
className={btnClassName}
|
||||
rightSlot={
|
||||
<Icon
|
||||
size="sm"
|
||||
icon="chevronDown"
|
||||
className={classNames(
|
||||
'-mr-1.5 mt-0.5',
|
||||
isActive ? 'opacity-100' : 'opacity-20',
|
||||
)}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{option && 'shortLabel' in option
|
||||
? option.shortLabel
|
||||
: option?.label ?? 'Unknown'}
|
||||
</Button>
|
||||
<Icon
|
||||
size="sm"
|
||||
icon="chevronDown"
|
||||
className={classNames('ml-1', isActive ? 'text-fg-subtle' : 'opacity-50')}
|
||||
/>
|
||||
</button>
|
||||
</RadioDropdown>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Button
|
||||
<button
|
||||
key={t.value}
|
||||
color="custom"
|
||||
size="sm"
|
||||
onClick={() => handleTabChange(t.value)}
|
||||
className={btnClassName}
|
||||
>
|
||||
{t.label}
|
||||
</Button>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
@@ -3,9 +3,9 @@ import { motion } from 'framer-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
import { useKey } from 'react-use';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { IconProps } from './Icon';
|
||||
import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
|
||||
export interface ToastProps {
|
||||
children: ReactNode;
|
||||
@@ -52,14 +52,15 @@ export function Toast({
|
||||
transition={{ duration: 0.2 }}
|
||||
className={classNames(
|
||||
className,
|
||||
'x-theme-toast',
|
||||
'pointer-events-auto',
|
||||
'relative bg-gray-50 dark:bg-gray-100 pointer-events-auto',
|
||||
'relative bg-background pointer-events-auto',
|
||||
'rounded-lg',
|
||||
'border border-highlightSecondary dark:border-highlight shadow-xl',
|
||||
'border border-background-highlight shadow-lg',
|
||||
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
|
||||
'w-[22rem] max-h-[80vh]',
|
||||
'm-2 grid grid-cols-[1fr_auto]',
|
||||
'text-gray-700',
|
||||
'text-fg',
|
||||
)}
|
||||
>
|
||||
<div className="px-3 py-2 flex items-center gap-2">
|
||||
@@ -67,10 +68,10 @@ export function Toast({
|
||||
<Icon
|
||||
icon={ICONS[variant]}
|
||||
className={classNames(
|
||||
variant === 'success' && 'text-green-500',
|
||||
variant === 'warning' && 'text-orange-500',
|
||||
variant === 'error' && 'text-red-500',
|
||||
variant === 'copied' && 'text-violet-500',
|
||||
variant === 'success' && 'text-fg-success',
|
||||
variant === 'warning' && 'text-fg-warning',
|
||||
variant === 'error' && 'text-fg-danger',
|
||||
variant === 'copied' && 'text-fg-primary',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -82,7 +83,7 @@ export function Toast({
|
||||
|
||||
<IconButton
|
||||
color="custom"
|
||||
className="opacity-50"
|
||||
className="opacity-60"
|
||||
title="Dismiss"
|
||||
icon="x"
|
||||
onClick={onClose}
|
||||
@@ -91,7 +92,7 @@ export function Toast({
|
||||
{timeout != null && (
|
||||
<div className="w-full absolute bottom-0 left-0 right-0">
|
||||
<motion.div
|
||||
className="bg-highlight h-0.5"
|
||||
className="bg-background-highlight h-0.5"
|
||||
initial={{ width: '100%' }}
|
||||
animate={{ width: '0%', opacity: 0.2 }}
|
||||
transition={{ duration: timeout / 1000, ease: 'linear' }}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function WindowDragRegion({ className, ...props }: Props) {
|
||||
return (
|
||||
<div
|
||||
data-tauri-drag-region
|
||||
className={classNames(className, 'w-full flex-shrink-0')}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user