2024.5.0-beta.2 (#38)

This commit is contained in:
Gregory Schier
2024-06-03 14:03:25 -07:00
committed by GitHub
parent ee6c7b6b1a
commit 3135f9c187
29 changed files with 242 additions and 184 deletions

View File

@@ -120,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(

View File

@@ -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,
@@ -359,21 +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,
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]);
}, [fullWidth, triggerShape]);
const filteredItems = useMemo(
() => items.filter((i) => getNodeText(i.label).toLowerCase().includes(filter.toLowerCase())),
@@ -521,9 +527,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
onClick={handleClick}
justify="start"
leftSlot={
item.leftSlot && (
<div className="pr-2 flex justify-start text-fg-subtle">{item.leftSlot}</div>
)
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"

View File

@@ -266,7 +266,7 @@
}
&.cm-completionInfo-right {
@apply ml-1 -mt-0.5;
@apply ml-1 -mt-0.5 font-sans;
}
&.cm-completionInfo-right-narrow {
@@ -278,7 +278,7 @@
}
&.cm-tooltip-autocomplete {
@apply font-mono text-editor;
@apply font-mono;
& > ul {
@apply p-1 max-h-[40vh];

View File

@@ -195,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,
@@ -331,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?.();
@@ -346,22 +364,6 @@ 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,
];
}

View File

@@ -39,7 +39,7 @@ 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: 'var(--fg-subtler)',
@@ -61,7 +61,7 @@ export const myHighlightStyle = HighlightStyle.define([
{ tag: [t.atom, t.meta, t.operator, t.bool, t.null, t.keyword], color: 'var(--fg-danger)' },
]);
const myTheme = EditorView.theme({}, { dark: true });
const syntaxTheme = EditorView.theme({}, { dark: true });
const syntaxExtensions: Record<string, LanguageSupport> = {
'application/graphql': graphqlLanguageSupport(),
@@ -108,8 +108,8 @@ export const baseExtensions = [
return (a.boost ?? 0) - (b.boost ?? 0);
},
}),
syntaxHighlighting(myHighlightStyle),
myTheme,
syntaxHighlighting(syntaxHighlightStyle),
syntaxTheme,
EditorState.allowMultipleSelections.of(true),
];

View File

@@ -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(/.*/);

View File

@@ -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) }),
];
}

View File

@@ -9,7 +9,7 @@ export function FormattedError({ children }: Props) {
return (
<pre
className={classNames(
'w-full select-auto cursor-text bg-gray-100 p-3 rounded',
'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',
)}
>

View File

@@ -133,7 +133,11 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
>
<label
htmlFor={id}
className={classNames(labelClassName, 'text-fg whitespace-nowrap', hideLabel && 'sr-only')}
className={classNames(
labelClassName,
'text-fg-subtle whitespace-nowrap',
hideLabel && 'sr-only',
)}
>
{label}
</label>

View File

@@ -389,7 +389,7 @@ function PairEditorRow({
<Button
size="xs"
color="secondary"
className="font-mono text-sm"
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 */}
&#x200E;
{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] ?? '';
};

View File

@@ -85,7 +85,11 @@ export const PlainInput = forwardRef<HTMLInputElement, PlainInputProps>(function
>
<label
htmlFor={id}
className={classNames(labelClassName, 'text-fg whitespace-nowrap', hideLabel && 'sr-only')}
className={classNames(
labelClassName,
'text-fg-subtle whitespace-nowrap',
hideLabel && 'sr-only',
)}
>
{label}
</label>

View File

@@ -50,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>
);
}

View File

@@ -1,9 +1,14 @@
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';
@@ -11,22 +16,12 @@ interface Props<T extends string> {
hideLabel?: boolean;
value: T;
leftSlot?: ReactNode;
options: SelectOption<T>[] | SelectOptionGroup<T>[];
options: RadioDropdownItem<T>[];
onChange: (value: T) => void;
size?: 'xs' | 'sm' | 'md' | 'lg';
size?: ButtonProps['size'];
className?: string;
}
export interface SelectOption<T extends string> {
label: string;
value: T;
}
export interface SelectOptionGroup<T extends string> {
label: string;
options: SelectOption<T>[];
}
export function Select<T extends string>({
labelPosition = 'top',
name,
@@ -39,7 +34,8 @@ export function Select<T extends string>({
onChange,
className,
size = 'md',
}: Props<T>) {
}: SelectProps<T>) {
const osInfo = useOsInfo();
const [focused, setFocused] = useState<boolean>(false);
const id = `input-${name}`;
return (
@@ -49,55 +45,68 @@ export function Select<T extends string>({
'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',
)}
>
<label
htmlFor={id}
className={classNames(labelClassName, 'text-fg whitespace-nowrap', hideLabel && 'sr-only')}
className={classNames(
labelClassName,
'text-fg-subtle whitespace-nowrap',
hideLabel && 'sr-only',
)}
>
{label}
</label>
<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',
size === 'lg' && 'h-lg',
)}
>
{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) =>
'options' in o ? (
<optgroup key={o.label} label={o.label}>
{o.options.map(({ label, value }) => (
<option key={label} value={value}>
{label}
</option>
))}
</optgroup>
) : (
<option key={o.label} value={o.value}>
{o.label}
</option>
),
{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',
)}
</select>
</HStack>
>
{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>
);
}