mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-18 07:23:51 +01:00
Refactored some core UI
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user