mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-24 18:31:16 +01:00
Launch analytics events, changelog, better filter styles
This commit is contained in:
@@ -7,6 +7,7 @@ import type {
|
||||
MouseEvent,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
} from 'react';
|
||||
import React, {
|
||||
Children,
|
||||
@@ -39,7 +40,7 @@ export type DropdownItemDefault = {
|
||||
label: ReactNode;
|
||||
hotKeyAction?: HotkeyAction;
|
||||
hotKeyLabelOnly?: boolean;
|
||||
variant?: 'danger';
|
||||
variant?: 'default' | 'danger' | 'notify';
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
leftSlot?: ReactNode;
|
||||
@@ -53,6 +54,8 @@ export interface DropdownProps {
|
||||
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
||||
items: DropdownItem[];
|
||||
openOnHotKeyAction?: HotkeyAction;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export interface DropdownRef {
|
||||
@@ -66,14 +69,23 @@ export interface DropdownRef {
|
||||
}
|
||||
|
||||
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
|
||||
{ children, items, openOnHotKeyAction }: DropdownProps,
|
||||
{ children, items, openOnHotKeyAction, onOpen, onClose }: DropdownProps,
|
||||
ref,
|
||||
) {
|
||||
const [isOpen, setIsOpen] = useState<boolean>(false);
|
||||
const [isOpen, _setIsOpen] = useState<boolean>(false);
|
||||
const [defaultSelectedIndex, setDefaultSelectedIndex] = useState<number>();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
|
||||
|
||||
const setIsOpen = useCallback(
|
||||
(o: SetStateAction<boolean>) => {
|
||||
_setIsOpen(o);
|
||||
if (o) onOpen?.();
|
||||
else onClose?.();
|
||||
},
|
||||
[onClose, onOpen],
|
||||
);
|
||||
|
||||
useHotKey(openOnHotKeyAction ?? null, () => {
|
||||
setIsOpen(true);
|
||||
});
|
||||
@@ -112,12 +124,12 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
}),
|
||||
};
|
||||
return cloneElement(existingChild, props);
|
||||
}, [children]);
|
||||
}, [children, setIsOpen]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
}, []);
|
||||
}, [setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
buttonRef.current?.setAttribute('aria-expanded', isOpen.toString());
|
||||
@@ -307,11 +319,12 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
|
||||
const docRect = document.documentElement.getBoundingClientRect();
|
||||
const width = triggerShape.right - triggerShape.left;
|
||||
const heightAbove = triggerShape.top;
|
||||
const heightBelow = docRect.height - triggerShape.bottom;
|
||||
const hSpaceRemaining = docRect.width - triggerShape.left;
|
||||
const vSpaceRemaining = docRect.height - triggerShape.bottom;
|
||||
const top = triggerShape?.bottom + 5;
|
||||
const onRight = hSpaceRemaining < 200;
|
||||
const upsideDown = vSpaceRemaining < 200;
|
||||
const upsideDown = heightAbove > heightBelow && heightBelow < 200;
|
||||
const containerStyles = {
|
||||
top: !upsideDown ? top : undefined,
|
||||
bottom: upsideDown ? docRect.height - top : undefined,
|
||||
@@ -462,6 +475,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap',
|
||||
'focus:bg-highlight focus:text-gray-900 rounded',
|
||||
item.variant === 'danger' && 'text-red-600',
|
||||
item.variant === 'notify' && 'text-pink-600',
|
||||
)}
|
||||
innerClassName="!text-left"
|
||||
{...props}
|
||||
|
||||
@@ -5,7 +5,18 @@ import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/vie
|
||||
import classNames from 'classnames';
|
||||
import { EditorView } from 'codemirror';
|
||||
import type { MutableRefObject, ReactNode } from 'react';
|
||||
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useActiveEnvironment } from '../../../hooks/useActiveEnvironment';
|
||||
import { useActiveWorkspace } from '../../../hooks/useActiveWorkspace';
|
||||
import { IconButton } from '../IconButton';
|
||||
@@ -145,6 +156,12 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [forceUpdateKey]);
|
||||
|
||||
const classList = className?.split(/\s+/) ?? [];
|
||||
const bgClassList = classList
|
||||
.filter((c) => c.match(/(^|:)?bg-.+/)) // Find bg-* classes
|
||||
.map((c) => c.replace(/^bg-/, '!bg-')) // !important
|
||||
.map((c) => c.replace(/^dark:bg-/, 'dark:!bg-')); // !important
|
||||
|
||||
// Initialize the editor when ref mounts
|
||||
const initEditorRef = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container === null) {
|
||||
@@ -184,7 +201,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
|
||||
view = new EditorView({ state, parent: container });
|
||||
cm.current = { view, languageCompartment };
|
||||
syncGutterBg({ parent: container, className });
|
||||
syncGutterBg({ parent: container, bgClassList });
|
||||
if (autoFocus) {
|
||||
view.focus();
|
||||
}
|
||||
@@ -198,6 +215,50 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Add bg classes to actions, so they appear over the text
|
||||
const decoratedActions = useMemo(() => {
|
||||
const results = [];
|
||||
const actionClassName = classNames(
|
||||
'transition-opacity opacity-0 group-hover:opacity-50 hover:!opacity-100 shadow',
|
||||
bgClassList,
|
||||
);
|
||||
|
||||
if (format) {
|
||||
results.push(
|
||||
<IconButton
|
||||
showConfirm
|
||||
key="format"
|
||||
size="sm"
|
||||
title="Reformat contents"
|
||||
icon="magicWand"
|
||||
className={classNames(actionClassName)}
|
||||
onClick={() => {
|
||||
if (cm.current === null) return;
|
||||
const { doc } = cm.current.view.state;
|
||||
const formatted = format(doc.toString());
|
||||
// Update editor and blur because the cursor will reset anyway
|
||||
cm.current.view.dispatch({
|
||||
changes: { from: 0, to: doc.length, insert: formatted },
|
||||
});
|
||||
cm.current.view.contentDOM.blur();
|
||||
// Fire change event
|
||||
onChange?.(formatted);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
results.push(
|
||||
Children.map(actions, (existingChild) => {
|
||||
if (!isValidElement(existingChild)) return null;
|
||||
return cloneElement(existingChild, {
|
||||
...existingChild.props,
|
||||
className: classNames(existingChild.props.className, actionClassName),
|
||||
});
|
||||
}),
|
||||
);
|
||||
return results;
|
||||
}, [actions, bgClassList, format, onChange]);
|
||||
|
||||
const cmContainer = (
|
||||
<div
|
||||
ref={initEditorRef}
|
||||
@@ -219,7 +280,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
return (
|
||||
<div className="group relative h-full w-full">
|
||||
{cmContainer}
|
||||
{(format || actions) && (
|
||||
{decoratedActions && (
|
||||
<HStack
|
||||
space={1}
|
||||
alignItems="center"
|
||||
@@ -229,28 +290,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
'pointer-events-none', // No pointer events so we don't block the editor
|
||||
)}
|
||||
>
|
||||
{format && (
|
||||
<IconButton
|
||||
showConfirm
|
||||
size="sm"
|
||||
title="Reformat contents"
|
||||
icon="magicWand"
|
||||
className="transition-all opacity-0 group-hover:opacity-100"
|
||||
onClick={() => {
|
||||
if (cm.current === null) return;
|
||||
const { doc } = cm.current.view.state;
|
||||
const formatted = format(doc.toString());
|
||||
// Update editor and blur because the cursor will reset anyway
|
||||
cm.current.view.dispatch({
|
||||
changes: { from: 0, to: doc.length, insert: formatted },
|
||||
});
|
||||
cm.current.view.contentDOM.blur();
|
||||
// Fire change event
|
||||
onChange?.(formatted);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{actions}
|
||||
{decoratedActions}
|
||||
</HStack>
|
||||
)}
|
||||
</div>
|
||||
@@ -325,19 +365,14 @@ function isViewUpdateFromUserInput(viewUpdate: ViewUpdate) {
|
||||
|
||||
const syncGutterBg = ({
|
||||
parent,
|
||||
className = '',
|
||||
bgClassList,
|
||||
}: {
|
||||
parent: HTMLDivElement;
|
||||
className?: string;
|
||||
bgClassList: string[];
|
||||
}) => {
|
||||
const gutterEl = parent.querySelector<HTMLDivElement>('.cm-gutters');
|
||||
const classList = className?.split(/\s+/) ?? [];
|
||||
const bgClasses = classList
|
||||
.filter((c) => c.match(/(^|:)?bg-.+/)) // Find bg-* classes
|
||||
.map((c) => c.replace(/^bg-/, '!bg-')) // !important
|
||||
.map((c) => c.replace(/^dark:bg-/, 'dark:!bg-')); // !important
|
||||
if (gutterEl) {
|
||||
gutterEl?.classList.add(...bgClasses);
|
||||
gutterEl?.classList.add(...bgClassList);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { memo } from 'react';
|
||||
const icons = {
|
||||
archive: lucide.ArchiveIcon,
|
||||
box: lucide.BoxIcon,
|
||||
cake: lucide.CakeIcon,
|
||||
chat: lucide.MessageSquare,
|
||||
check: lucide.CheckIcon,
|
||||
chevronDown: lucide.ChevronDownIcon,
|
||||
@@ -13,6 +14,8 @@ const icons = {
|
||||
code: lucide.CodeIcon,
|
||||
copy: lucide.CopyIcon,
|
||||
download: lucide.DownloadIcon,
|
||||
folderInput: lucide.FolderInputIcon,
|
||||
folderOutput: lucide.FolderOutputIcon,
|
||||
externalLink: lucide.ExternalLinkIcon,
|
||||
eye: lucide.EyeIcon,
|
||||
eyeClosed: lucide.EyeOffIcon,
|
||||
|
||||
@@ -13,6 +13,7 @@ type Props = IconProps &
|
||||
iconClassName?: string;
|
||||
iconSize?: IconProps['size'];
|
||||
title: string;
|
||||
showBadge?: boolean;
|
||||
};
|
||||
|
||||
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
|
||||
@@ -26,6 +27,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
||||
tabIndex,
|
||||
size = 'md',
|
||||
iconSize,
|
||||
showBadge,
|
||||
...props
|
||||
}: Props,
|
||||
ref,
|
||||
@@ -49,7 +51,7 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
||||
innerClassName="flex items-center justify-center"
|
||||
className={classNames(
|
||||
className,
|
||||
'flex-shrink-0 text-gray-700 hover:text-gray-1000',
|
||||
'relative flex-shrink-0 text-gray-700 hover:text-gray-1000',
|
||||
'!px-0',
|
||||
size === 'md' && 'w-9',
|
||||
size === 'sm' && 'w-8',
|
||||
@@ -58,6 +60,11 @@ export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButt
|
||||
size={size}
|
||||
{...props}
|
||||
>
|
||||
{showBadge && (
|
||||
<div className="absolute top-0 right-0 w-1/2 h-1/2 flex items-center justify-center">
|
||||
<div className="w-2.5 h-2.5 bg-pink-500 rounded-full" />
|
||||
</div>
|
||||
)}
|
||||
<Icon
|
||||
size={iconSize}
|
||||
icon={confirmed ? 'check' : icon}
|
||||
|
||||
74
src-web/components/core/Select.tsx
Normal file
74
src-web/components/core/Select.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
import classNames from 'classnames';
|
||||
|
||||
interface Props<T extends string> {
|
||||
name: string;
|
||||
label: string;
|
||||
labelPosition?: 'top' | 'left';
|
||||
labelClassName?: string;
|
||||
hideLabel?: boolean;
|
||||
value: string;
|
||||
options: Record<T, string>;
|
||||
onChange: (value: T) => void;
|
||||
size?: 'xs' | 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export function Select<T extends string>({
|
||||
labelPosition = 'top',
|
||||
name,
|
||||
labelClassName,
|
||||
hideLabel,
|
||||
label,
|
||||
value,
|
||||
options,
|
||||
onChange,
|
||||
size = 'md',
|
||||
}: Props<T>) {
|
||||
const id = `input-${name}`;
|
||||
return (
|
||||
<div
|
||||
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-sm text-gray-900 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 px-2 outline-none bg-transparent',
|
||||
'border-highlight focus:border-focus',
|
||||
size === 'xs' && 'h-xs',
|
||||
size === 'sm' && 'h-sm',
|
||||
size === 'md' && 'h-md',
|
||||
size === 'lg' && 'h-lg',
|
||||
)}
|
||||
>
|
||||
{Object.entries<string>(options).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const selectBackgroundStyles = {
|
||||
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.5rem center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: '1.5em 1.5em',
|
||||
};
|
||||
Reference in New Issue
Block a user