mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-26 11:21:16 +01:00
OAuth 2 (#158)
This commit is contained in:
@@ -7,7 +7,7 @@ interface Props {
|
||||
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger' | 'info';
|
||||
}
|
||||
|
||||
export function Banner({ children, className, color = 'secondary' }: Props) {
|
||||
export function Banner({ children, className, color }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
@@ -16,7 +16,7 @@ export function Banner({ children, className, color = 'secondary' }: Props) {
|
||||
`x-theme-banner--${color}`,
|
||||
'whitespace-pre-wrap',
|
||||
'border border-dashed border-border bg-surface',
|
||||
'px-3 py-2 rounded select-auto cursor-text',
|
||||
'px-3 py-2 rounded select-auto',
|
||||
'overflow-x-auto text-text',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { type ReactNode } from 'react';
|
||||
import { trackEvent } from '../../lib/analytics';
|
||||
import { Icon } from './Icon';
|
||||
import { HStack } from './Stacks';
|
||||
@@ -26,17 +26,15 @@ export function Checkbox({
|
||||
event,
|
||||
}: CheckboxProps) {
|
||||
return (
|
||||
<HStack
|
||||
as="label"
|
||||
space={2}
|
||||
className={classNames(className, 'text-text mr-auto', disabled && 'opacity-disabled')}
|
||||
>
|
||||
<HStack as="label" space={2} className={classNames(className, 'text-text mr-auto')}>
|
||||
<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-border',
|
||||
'rounded hocus:border-border-focus hocus:bg-focus/[5%] outline-none ring-0',
|
||||
'rounded outline-none ring-0',
|
||||
!disabled && 'hocus:border-border-focus hocus:bg-focus/[5%] ',
|
||||
disabled && 'border-dotted',
|
||||
)}
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
@@ -54,7 +52,7 @@ export function Checkbox({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!hideLabel && title}
|
||||
<span className={classNames(disabled && 'opacity-disabled')}>{!hideLabel && title}</span>
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ export type DropdownItemSeparator = {
|
||||
};
|
||||
|
||||
export type DropdownItemDefault = {
|
||||
key: string;
|
||||
type?: 'default';
|
||||
label: ReactNode;
|
||||
keepOpen?: boolean;
|
||||
@@ -465,11 +464,12 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
return (
|
||||
<>
|
||||
{items.map(
|
||||
(item) =>
|
||||
(item, i) =>
|
||||
item.type !== 'separator' &&
|
||||
!item.hotKeyLabelOnly && (
|
||||
!item.hotKeyLabelOnly &&
|
||||
item.hotKeyAction && (
|
||||
<MenuItemHotKey
|
||||
key={item.key}
|
||||
key={`${item.hotKeyAction}::${i}`}
|
||||
onSelect={handleSelect}
|
||||
item={item}
|
||||
action={item.hotKeyAction}
|
||||
@@ -542,7 +542,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
focused={i === selectedIndex}
|
||||
onFocus={handleFocus}
|
||||
onSelect={handleSelect}
|
||||
key={item.key}
|
||||
key={`item_${i}`}
|
||||
item={item}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
@apply w-full block text-base;
|
||||
|
||||
/* Regular cursor */
|
||||
|
||||
.cm-cursor {
|
||||
@apply border-text !important;
|
||||
/* Widen the cursor a bit */
|
||||
@@ -12,6 +13,7 @@
|
||||
}
|
||||
|
||||
/* Vim-mode cursor */
|
||||
|
||||
.cm-fat-cursor {
|
||||
@apply outline-0 bg-text !important;
|
||||
@apply text-surface !important;
|
||||
@@ -181,7 +183,7 @@
|
||||
@apply hidden !important;
|
||||
}
|
||||
|
||||
&.cm-singleline .cm-line {
|
||||
&.cm-singleline * {
|
||||
@apply cursor-default;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defaultKeymap, historyField } from '@codemirror/commands';
|
||||
import { defaultKeymap, historyField, indentWithTab } from '@codemirror/commands';
|
||||
import { foldState, forceParsing } from '@codemirror/language';
|
||||
import type { EditorStateConfig, Extension } from '@codemirror/state';
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
@@ -34,14 +34,22 @@ import { TemplateVariableDialog } from '../../TemplateVariableDialog';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { HStack } from '../Stacks';
|
||||
import './Editor.css';
|
||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
||||
import {
|
||||
baseExtensions,
|
||||
getLanguageExtension,
|
||||
multiLineExtensions,
|
||||
readonlyExtensions,
|
||||
} from './extensions';
|
||||
import type { GenericCompletionConfig } from './genericCompletion';
|
||||
import { singleLineExtensions } from './singleLine';
|
||||
|
||||
// VSCode's Tab actions mess with the single-line editor tab actions, so remove it.
|
||||
const vsCodeWithoutTab = vscodeKeymap.filter((k) => k.key !== 'Tab');
|
||||
|
||||
const keymapExtensions: Record<EditorKeymap, Extension> = {
|
||||
vim: vim(),
|
||||
emacs: emacs(),
|
||||
vscode: keymap.of(vscodeKeymap),
|
||||
vscode: keymap.of(vsCodeWithoutTab),
|
||||
default: [],
|
||||
};
|
||||
|
||||
@@ -68,6 +76,7 @@ export interface EditorProps {
|
||||
onKeyDown?: (e: KeyboardEvent) => void;
|
||||
singleLine?: boolean;
|
||||
wrapLines?: boolean;
|
||||
disableTabIndent?: boolean;
|
||||
format?: (v: string) => Promise<string>;
|
||||
autocomplete?: GenericCompletionConfig;
|
||||
autocompleteVariables?: boolean;
|
||||
@@ -85,9 +94,9 @@ const emptyExtension: Extension = [];
|
||||
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
{
|
||||
readOnly,
|
||||
type = 'text',
|
||||
type,
|
||||
heightMode,
|
||||
language = 'text',
|
||||
language,
|
||||
autoFocus,
|
||||
autoSelect,
|
||||
placeholder,
|
||||
@@ -101,6 +110,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
className,
|
||||
disabled,
|
||||
singleLine,
|
||||
format,
|
||||
autocomplete,
|
||||
@@ -108,6 +118,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
autocompleteVariables,
|
||||
actions,
|
||||
wrapLines,
|
||||
disableTabIndent,
|
||||
hideGutter,
|
||||
stateKey,
|
||||
}: EditorProps,
|
||||
@@ -122,6 +133,20 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
wrapLines = settings.editorSoftWrap;
|
||||
}
|
||||
|
||||
if (disabled) {
|
||||
readOnly = true;
|
||||
}
|
||||
|
||||
if (
|
||||
singleLine ||
|
||||
language == null ||
|
||||
language === 'text' ||
|
||||
language === 'url' ||
|
||||
language === 'pairs'
|
||||
) {
|
||||
disableTabIndent = true;
|
||||
}
|
||||
|
||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||
useImperativeHandle(ref, () => cm.current?.view, []);
|
||||
|
||||
@@ -166,7 +191,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
useEffect(
|
||||
function configurePlaceholder() {
|
||||
if (cm.current === null) return;
|
||||
const ext = placeholderExt(placeholderElFromText(placeholder ?? '', type));
|
||||
const ext = placeholderExt(placeholderElFromText(placeholder, type));
|
||||
const effects = placeholderCompartment.current.reconfigure(ext);
|
||||
cm.current?.view.dispatch({ effects });
|
||||
},
|
||||
@@ -209,6 +234,23 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
[wrapLines],
|
||||
);
|
||||
|
||||
// Update tab indent
|
||||
const tabIndentCompartment = useRef(new Compartment());
|
||||
useEffect(
|
||||
function configureTabIndent() {
|
||||
if (cm.current === null) return;
|
||||
const current = tabIndentCompartment.current.get(cm.current.view.state) ?? emptyExtension;
|
||||
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
|
||||
if (disableTabIndent && current !== emptyExtension) return; // Nothing to do
|
||||
if (!disableTabIndent && current === emptyExtension) return; // Nothing to do
|
||||
|
||||
const ext = !disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension;
|
||||
const effects = tabIndentCompartment.current.reconfigure(ext);
|
||||
cm.current?.view.dispatch({ effects });
|
||||
},
|
||||
[disableTabIndent],
|
||||
);
|
||||
|
||||
const onClickFunction = useCallback(
|
||||
async (fn: TemplateFunction, tagValue: string, startPos: number) => {
|
||||
const initialTokens = await parseTemplate(tagValue);
|
||||
@@ -342,9 +384,12 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
const extensions = [
|
||||
languageCompartment.of(langExt),
|
||||
placeholderCompartment.current.of(
|
||||
placeholderExt(placeholderElFromText(placeholder ?? '', type)),
|
||||
placeholderExt(placeholderElFromText(placeholder, type)),
|
||||
),
|
||||
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : emptyExtension),
|
||||
tabIndentCompartment.current.of(
|
||||
!disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension,
|
||||
),
|
||||
keymapCompartment.current.of(
|
||||
keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'],
|
||||
),
|
||||
@@ -475,6 +520,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
|
||||
className={classNames(
|
||||
className,
|
||||
'cm-wrapper text-base',
|
||||
disabled && 'opacity-disabled',
|
||||
type === 'password' && 'cm-obscure-text',
|
||||
heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height',
|
||||
singleLine ? 'cm-singleline' : 'cm-multiline',
|
||||
@@ -557,10 +603,8 @@ function getExtensions({
|
||||
tooltips({ parent }),
|
||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
|
||||
...(singleLine ? [singleLineExtensions()] : []),
|
||||
...(!singleLine ? [multiLineExtensions({ hideGutter })] : []),
|
||||
...(readOnly
|
||||
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
|
||||
: []),
|
||||
...(!singleLine ? multiLineExtensions({ hideGutter }) : []),
|
||||
...(readOnly ? readonlyExtensions : []),
|
||||
|
||||
// ------------------------ //
|
||||
// Things that must be last //
|
||||
@@ -580,13 +624,15 @@ function getExtensions({
|
||||
];
|
||||
}
|
||||
|
||||
const placeholderElFromText = (text: string, type: EditorProps['type']) => {
|
||||
const placeholderElFromText = (text: string | undefined, type: EditorProps['type']) => {
|
||||
const el = document.createElement('div');
|
||||
if (type === 'password') {
|
||||
// Will be obscured (dots) so just needs to be something to take up space
|
||||
el.innerHTML = 'something-cool';
|
||||
el.setAttribute('aria-hidden', 'true');
|
||||
} else {
|
||||
// Default to <SPACE> because codemirror needs it for sizing. I'm not sure why, but probably something
|
||||
// to do with how Yaak "hacks" it with CSS for single line input.
|
||||
el.innerHTML = text ? text.replaceAll('\n', '<br/>') : ' ';
|
||||
}
|
||||
return el;
|
||||
@@ -596,8 +642,8 @@ function saveCachedEditorState(stateKey: string | null, state: EditorState | nul
|
||||
if (!stateKey || state == null) return;
|
||||
const stateObj = state.toJSON(stateFields);
|
||||
|
||||
// Save state in sessionStorage by removing doc and saving the hash of it instead
|
||||
// This will be checked on restore and put back in if it matches
|
||||
// Save state in sessionStorage by removing doc and saving the hash of it instead.
|
||||
// This will be checked on restore and put back in if it matches.
|
||||
stateObj.docHash = md5(stateObj.doc);
|
||||
delete stateObj.doc;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
closeBracketsKeymap,
|
||||
completionKeymap,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { history, historyKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import { history, historyKeymap } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
@@ -142,6 +142,11 @@ export const baseExtensions = [
|
||||
keymap.of([...historyKeymap, ...completionKeymap]),
|
||||
];
|
||||
|
||||
export const readonlyExtensions = [
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.contentAttributes.of({ tabindex: '-1' }),
|
||||
];
|
||||
|
||||
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
|
||||
hideGutter
|
||||
? []
|
||||
@@ -208,5 +213,5 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
highlightActiveLineGutter(),
|
||||
keymap.of([indentWithTab, ...closeBracketsKeymap, ...searchKeymap, ...foldKeymap, ...lintKeymap]),
|
||||
keymap.of([...closeBracketsKeymap, ...searchKeymap, ...foldKeymap, ...lintKeymap]),
|
||||
];
|
||||
|
||||
@@ -1,16 +1,5 @@
|
||||
import type { CompletionContext } from '@codemirror/autocomplete';
|
||||
|
||||
export interface GenericCompletionOption {
|
||||
label: string;
|
||||
type: 'constant' | 'variable';
|
||||
detail?: string;
|
||||
info?: string;
|
||||
/** When given, should be a number from -99 to 99 that adjusts
|
||||
* how this completion is ranked compared to other completions
|
||||
* that match the input as well as this one. A negative number
|
||||
* moves it down the list, a positive number moves it up. */
|
||||
boost?: number;
|
||||
}
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
|
||||
export interface GenericCompletionConfig {
|
||||
minMatch?: number;
|
||||
|
||||
@@ -16,6 +16,7 @@ export type InputProps = Pick<
|
||||
| 'useTemplating'
|
||||
| 'autocomplete'
|
||||
| 'forceUpdateKey'
|
||||
| 'disabled'
|
||||
| 'autoFocus'
|
||||
| 'autoSelect'
|
||||
| 'autocompleteVariables'
|
||||
@@ -75,6 +76,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
readOnly,
|
||||
stateKey,
|
||||
multiLine,
|
||||
disabled,
|
||||
...props
|
||||
}: InputProps,
|
||||
ref,
|
||||
@@ -82,18 +84,26 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
|
||||
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [hasChanged, setHasChanged] = useStateWithDeps<boolean>(false, [stateKey, forceUpdateKey]);
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
useImperativeHandle<EditorView | null, EditorView | null>(ref, () => editorRef.current);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
if (readOnly) return;
|
||||
setFocused(true);
|
||||
// Select all text on focus
|
||||
editorRef.current?.dispatch({
|
||||
selection: { anchor: 0, head: editorRef.current.state.doc.length },
|
||||
});
|
||||
onFocus?.();
|
||||
}, [onFocus, readOnly]);
|
||||
|
||||
const handleBlur = useCallback(() => {
|
||||
setFocused(false);
|
||||
editorRef.current?.dispatch({ selection: { anchor: 0 } });
|
||||
// Move selection to the end on blur
|
||||
editorRef.current?.dispatch({
|
||||
selection: { anchor: editorRef.current.state.doc.length },
|
||||
});
|
||||
onBlur?.();
|
||||
}, [onBlur]);
|
||||
|
||||
@@ -114,13 +124,14 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
(value: string) => {
|
||||
setCurrentValue(value);
|
||||
onChange?.(value);
|
||||
setHasChanged(true);
|
||||
},
|
||||
[onChange],
|
||||
[onChange, setHasChanged],
|
||||
);
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Submit nearest form on Enter key press
|
||||
// Submit the nearest form on Enter key press
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key !== 'Enter') return;
|
||||
@@ -145,7 +156,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
>
|
||||
<Label
|
||||
htmlFor={id.current}
|
||||
optional={!required}
|
||||
required={required}
|
||||
visuallyHidden={hideLabel}
|
||||
className={classNames(labelClassName)}
|
||||
>
|
||||
@@ -158,8 +169,9 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
'x-theme-input',
|
||||
'relative w-full rounded-md text',
|
||||
'border',
|
||||
focused ? 'border-border-focus' : 'border-border',
|
||||
!isValid && '!border-danger',
|
||||
focused && !disabled ? 'border-border-focus' : 'border-border',
|
||||
disabled && 'border-dotted',
|
||||
!isValid && hasChanged && '!border-danger',
|
||||
size === 'md' && 'min-h-md',
|
||||
size === 'sm' && 'min-h-sm',
|
||||
size === 'xs' && 'min-h-xs',
|
||||
@@ -190,7 +202,12 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
onChange={handleChange}
|
||||
onPaste={onPaste}
|
||||
onPasteOverwrite={onPasteOverwrite}
|
||||
className={classNames(editorClassName, multiLine && 'py-1.5')}
|
||||
disabled={disabled}
|
||||
className={classNames(
|
||||
editorClassName,
|
||||
multiLine && size === 'md' && 'py-1.5',
|
||||
multiLine && size === 'sm' && 'py-1',
|
||||
)}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
readOnly={readOnly}
|
||||
@@ -201,7 +218,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
|
||||
<IconButton
|
||||
title={obscured ? `Show ${label}` : `Obscure ${label}`}
|
||||
size="xs"
|
||||
className="mr-0.5 group/obscure !h-auto my-0.5"
|
||||
className={classNames("mr-0.5 group/obscure !h-auto my-0.5", disabled && 'opacity-disabled')}
|
||||
iconClassName="text-text-subtle group-hover/obscure:text"
|
||||
iconSize="sm"
|
||||
icon={obscured ? 'eye' : 'eye_closed'}
|
||||
|
||||
@@ -4,30 +4,32 @@ import type { HTMLAttributes } from 'react';
|
||||
export function Label({
|
||||
htmlFor,
|
||||
className,
|
||||
optional,
|
||||
children,
|
||||
visuallyHidden,
|
||||
otherTags = [],
|
||||
tags = [],
|
||||
required,
|
||||
...props
|
||||
}: HTMLAttributes<HTMLLabelElement> & {
|
||||
htmlFor: string;
|
||||
optional?: boolean;
|
||||
otherTags?: string[];
|
||||
required?: boolean;
|
||||
tags?: string[];
|
||||
visuallyHidden?: boolean;
|
||||
}) {
|
||||
const tags = optional ? ['optional', ...otherTags] : otherTags;
|
||||
return (
|
||||
<label
|
||||
htmlFor={htmlFor}
|
||||
className={classNames(
|
||||
className,
|
||||
visuallyHidden && 'sr-only',
|
||||
'flex-shrink-0',
|
||||
'flex-shrink-0 text-sm',
|
||||
'text-text-subtle whitespace-nowrap flex items-center gap-1',
|
||||
)}
|
||||
htmlFor={htmlFor}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<span>
|
||||
{children}
|
||||
{required === true && <span className="text-text-subtlest">*</span>}
|
||||
</span>
|
||||
{tags.map((tag, i) => (
|
||||
<span key={i} className="text-xs text-text-subtlest">
|
||||
({tag})
|
||||
|
||||
@@ -168,7 +168,8 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(pair: PairWithId) => setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))),
|
||||
(pair: PairWithId) =>
|
||||
setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))),
|
||||
[setPairsAndSave],
|
||||
);
|
||||
|
||||
@@ -344,7 +345,6 @@ function PairEditorRow({
|
||||
const deleteItems = useMemo(
|
||||
(): DropdownItem[] => [
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
onSelect: handleDelete,
|
||||
color: 'danger',
|
||||
@@ -570,7 +570,6 @@ function FileActionsDropdown({
|
||||
const extraItems = useMemo<DropdownItem[]>(
|
||||
() => [
|
||||
{
|
||||
key: 'mime',
|
||||
label: 'Set Content-Type',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
hidden: !pair.isFile,
|
||||
@@ -589,7 +588,6 @@ function FileActionsDropdown({
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'clear-file',
|
||||
label: 'Unset File',
|
||||
leftSlot: <Icon icon="x" />,
|
||||
hidden: pair.isFile,
|
||||
@@ -598,7 +596,6 @@ function FileActionsDropdown({
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
onSelect: onDelete,
|
||||
variant: 'danger',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, FocusEvent } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { FocusEvent, HTMLAttributes } from 'react';
|
||||
import { useCallback, useRef, useState } from 'react';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { InputProps } from './Input';
|
||||
@@ -41,8 +41,8 @@ export function PlainInput({
|
||||
onFocusRaw,
|
||||
}: PlainInputProps) {
|
||||
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
|
||||
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [hasChanged, setHasChanged] = useState<boolean>(false);
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
@@ -71,19 +71,19 @@ export function PlainInput({
|
||||
'px-2 text-xs font-mono cursor-text',
|
||||
);
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
if (required && !validateRequire(currentValue)) return false;
|
||||
if (typeof validate === 'boolean') return validate;
|
||||
if (typeof validate === 'function' && !validate(currentValue)) return false;
|
||||
return true;
|
||||
}, [required, currentValue, validate]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string) => {
|
||||
setCurrentValue(value);
|
||||
onChange?.(value);
|
||||
setHasChanged(true);
|
||||
const isValid = (value: string) => {
|
||||
if (required && !validateRequire(value)) return false;
|
||||
if (typeof validate === 'boolean') return validate;
|
||||
if (typeof validate === 'function' && !validate(value)) return false;
|
||||
return true;
|
||||
};
|
||||
inputRef.current?.setCustomValidity(isValid(value) ? '' : 'Invalid value');
|
||||
},
|
||||
[onChange],
|
||||
[onChange, required, validate],
|
||||
);
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||
@@ -98,12 +98,7 @@ export function PlainInput({
|
||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||
)}
|
||||
>
|
||||
<Label
|
||||
htmlFor={id}
|
||||
className={labelClassName}
|
||||
visuallyHidden={hideLabel}
|
||||
optional={!required}
|
||||
>
|
||||
<Label htmlFor={id} className={labelClassName} visuallyHidden={hideLabel} required={required}>
|
||||
{label}
|
||||
</Label>
|
||||
<HStack
|
||||
@@ -114,7 +109,7 @@ export function PlainInput({
|
||||
'relative w-full rounded-md text',
|
||||
'border',
|
||||
focused ? 'border-border-focus' : 'border-border-subtle',
|
||||
!isValid && '!border-danger',
|
||||
hasChanged && 'has-[:invalid]:border-danger', // For built-in HTML validation
|
||||
size === 'md' && 'min-h-md',
|
||||
size === 'sm' && 'min-h-sm',
|
||||
size === 'xs' && 'min-h-xs',
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { PromptTextRequest } from '@yaakapp-internal/plugins';
|
||||
import type { FormEvent, ReactNode } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import { Button } from './Button';
|
||||
import { Input } from './Input';
|
||||
import { PlainInput } from './PlainInput';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
export type PromptProps = Omit<PromptTextRequest, 'id' | 'title' | 'description'> & {
|
||||
@@ -35,7 +35,7 @@ export function Prompt({
|
||||
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-4 mb-4"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<Input
|
||||
<PlainInput
|
||||
hideLabel
|
||||
autoSelect
|
||||
required={required}
|
||||
@@ -43,7 +43,6 @@ export function Prompt({
|
||||
label={label}
|
||||
defaultValue={defaultValue}
|
||||
onChange={setValue}
|
||||
stateKey={null}
|
||||
/>
|
||||
<HStack space={2} justifyContent="end">
|
||||
<Button onClick={onCancel} variant="border" color="secondary">
|
||||
|
||||
@@ -23,12 +23,14 @@ export interface SelectProps<T extends string> {
|
||||
size?: ButtonProps['size'];
|
||||
className?: string;
|
||||
event?: string;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function Select<T extends string>({
|
||||
labelPosition = 'top',
|
||||
name,
|
||||
labelClassName,
|
||||
disabled,
|
||||
hideLabel,
|
||||
label,
|
||||
value,
|
||||
@@ -72,7 +74,8 @@ export function Select<T extends string>({
|
||||
'w-full rounded-md text text-sm font-mono',
|
||||
'pl-2',
|
||||
'border',
|
||||
focused ? 'border-border-focus' : 'border-border',
|
||||
focused && !disabled ? 'border-border-focus' : 'border-border',
|
||||
disabled && 'border-dotted',
|
||||
isInvalidSelection && 'border-danger',
|
||||
size === 'xs' && 'h-xs',
|
||||
size === 'sm' && 'h-sm',
|
||||
@@ -86,7 +89,8 @@ export function Select<T extends string>({
|
||||
onChange={(e) => handleChange(e.target.value as T)}
|
||||
onFocus={() => setFocused(true)}
|
||||
onBlur={() => setFocused(false)}
|
||||
className={classNames('pr-7 w-full outline-none bg-transparent')}
|
||||
className={classNames('pr-7 w-full outline-none bg-transparent disabled:opacity-disabled')}
|
||||
disabled={disabled}
|
||||
>
|
||||
{isInvalidSelection && <option value={'__NONE__'}>-- Select an Option --</option>}
|
||||
{options.map((o) => {
|
||||
@@ -109,6 +113,7 @@ export function Select<T extends string>({
|
||||
variant="border"
|
||||
size={size}
|
||||
leftSlot={leftSlot}
|
||||
disabled={disabled}
|
||||
forDropdown
|
||||
>
|
||||
{options.find((o) => o.type !== 'separator' && o.value === value)?.label ?? '--'}
|
||||
|
||||
@@ -20,9 +20,8 @@ export interface ToastProps {
|
||||
color?: ShowToastRequest['color'];
|
||||
}
|
||||
|
||||
const ICONS: Record<NonNullable<ToastProps['color']>, IconProps['icon'] | null> = {
|
||||
const ICONS: Record<NonNullable<ToastProps['color'] | 'custom'>, IconProps['icon'] | null> = {
|
||||
custom: null,
|
||||
default: 'info',
|
||||
danger: 'alert_triangle',
|
||||
info: 'info',
|
||||
notice: 'alert_triangle',
|
||||
@@ -42,9 +41,8 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
|
||||
{},
|
||||
[open],
|
||||
);
|
||||
color = color ?? 'default';
|
||||
|
||||
const toastIcon = icon ?? (color in ICONS && ICONS[color]);
|
||||
const toastIcon = icon ?? (color && color in ICONS && ICONS[color]);
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
|
||||
Reference in New Issue
Block a user