mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-26 19:31:12 +01:00
Run oxfmt across repo, add format script and docs
Add .oxfmtignore to skip generated bindings and wasm-pack output. Add npm format script, update DEVELOPMENT.md for Vite+ toolchain, and format all non-generated files with oxfmt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { Button } from './Button';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
import type { ReactNode } from "react";
|
||||
import { Button } from "./Button";
|
||||
import { HStack, VStack } from "./Stacks";
|
||||
|
||||
export interface AlertProps {
|
||||
onHide: () => void;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useVirtualizer, type Virtualizer } from '@tanstack/react-virtual';
|
||||
import type { ReactElement, ReactNode, UIEvent } from 'react';
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { IconButton } from './IconButton';
|
||||
import { useVirtualizer, type Virtualizer } from "@tanstack/react-virtual";
|
||||
import type { ReactElement, ReactNode, UIEvent } from "react";
|
||||
import { useCallback, useLayoutEffect, useRef, useState } from "react";
|
||||
import { IconButton } from "./IconButton";
|
||||
|
||||
interface Props<T> {
|
||||
data: T[];
|
||||
@@ -87,8 +87,8 @@ export function AutoScroller<T>({
|
||||
<div
|
||||
style={{
|
||||
height: `${rowVirtualizer.getTotalSize()}px`,
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
|
||||
@@ -98,10 +98,10 @@ export function AutoScroller<T>({
|
||||
<div
|
||||
key={virtualItem.key}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: '100%',
|
||||
width: "100%",
|
||||
height: `${virtualItem.size}px`,
|
||||
transform: `translateY(${virtualItem.start}px)`,
|
||||
}}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface BannerProps {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger' | 'info';
|
||||
color?: "primary" | "secondary" | "success" | "notice" | "warning" | "danger" | "info";
|
||||
}
|
||||
|
||||
export function Banner({ children, className, color }: BannerProps) {
|
||||
@@ -13,12 +13,12 @@ export function Banner({ children, className, color }: BannerProps) {
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
color && 'bg-surface',
|
||||
color && "bg-surface",
|
||||
`x-theme-banner--${color}`,
|
||||
'border border-border border-dashed',
|
||||
'px-4 py-2 rounded-lg select-auto cursor-auto',
|
||||
'overflow-auto text-text',
|
||||
'mb-auto', // Don't stretch all the way down if the parent is in grid or flexbox
|
||||
"border border-border border-dashed",
|
||||
"px-4 py-2 rounded-lg select-auto cursor-auto",
|
||||
"overflow-auto text-text",
|
||||
"mb-auto", // Don't stretch all the way down if the parent is in grid or flexbox
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import { Editor } from './Editor/LazyEditor';
|
||||
import type { Pair, PairEditorProps, PairWithId } from './PairEditor';
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { generateId } from "../../lib/generateId";
|
||||
import { Editor } from "./Editor/LazyEditor";
|
||||
import type { Pair, PairEditorProps, PairWithId } from "./PairEditor";
|
||||
|
||||
type Props = PairEditorProps;
|
||||
|
||||
@@ -16,15 +16,15 @@ export function BulkPairEditor({
|
||||
}: Props) {
|
||||
const pairsText = useMemo(() => {
|
||||
return pairs
|
||||
.filter((p) => !(p.name.trim() === '' && p.value.trim() === ''))
|
||||
.filter((p) => !(p.name.trim() === "" && p.value.trim() === ""))
|
||||
.map(pairToLine)
|
||||
.join('\n');
|
||||
.join("\n");
|
||||
}, [pairs]);
|
||||
|
||||
const handleChange = useCallback(
|
||||
(text: string) => {
|
||||
const pairs = text
|
||||
.split('\n')
|
||||
.split("\n")
|
||||
.filter((l: string) => l.trim())
|
||||
.map(lineToPair);
|
||||
onChange(pairs);
|
||||
@@ -39,7 +39,7 @@ export function BulkPairEditor({
|
||||
stateKey={`bulk_pair.${stateKey}`}
|
||||
forcedEnvironmentId={forcedEnvironmentId}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
placeholder={`${namePlaceholder ?? 'name'}: ${valuePlaceholder ?? 'value'}`}
|
||||
placeholder={`${namePlaceholder ?? "name"}: ${valuePlaceholder ?? "value"}`}
|
||||
defaultValue={pairsText}
|
||||
language="pairs"
|
||||
onChange={handleChange}
|
||||
@@ -48,7 +48,7 @@ export function BulkPairEditor({
|
||||
}
|
||||
|
||||
function pairToLine(pair: Pair) {
|
||||
const value = pair.value.replaceAll('\n', '\\n');
|
||||
const value = pair.value.replaceAll("\n", "\\n");
|
||||
return `${pair.name}: ${value}`;
|
||||
}
|
||||
|
||||
@@ -56,8 +56,8 @@ function lineToPair(line: string): PairWithId {
|
||||
const [, name, value] = line.match(/^(:?[^:]+):\s+(.*)$/) ?? [];
|
||||
return {
|
||||
enabled: true,
|
||||
name: (name ?? '').trim(),
|
||||
value: (value ?? '').replaceAll('\\n', '\n').trim(),
|
||||
name: (name ?? "").trim(),
|
||||
value: (value ?? "").replaceAll("\\n", "\n").trim(),
|
||||
id: generateId(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef, useImperativeHandle, useRef } from 'react';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey';
|
||||
import { Icon } from './Icon';
|
||||
import { LoadingIcon } from './LoadingIcon';
|
||||
import type { Color } from "@yaakapp-internal/plugins";
|
||||
import classNames from "classnames";
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
import { forwardRef, useImperativeHandle, useRef } from "react";
|
||||
import type { HotkeyAction } from "../../hooks/useHotKey";
|
||||
import { useFormattedHotkey, useHotKey } from "../../hooks/useHotKey";
|
||||
import { Icon } from "./Icon";
|
||||
import { LoadingIcon } from "./LoadingIcon";
|
||||
|
||||
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color' | 'onChange'> & {
|
||||
export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, "color" | "onChange"> & {
|
||||
innerClassName?: string;
|
||||
color?: Color | 'custom' | 'default';
|
||||
variant?: 'border' | 'solid';
|
||||
color?: Color | "custom" | "default";
|
||||
variant?: "border" | "solid";
|
||||
isLoading?: boolean;
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md' | 'auto';
|
||||
justify?: 'start' | 'center';
|
||||
type?: 'button' | 'submit';
|
||||
size?: "2xs" | "xs" | "sm" | "md" | "auto";
|
||||
justify?: "start" | "center";
|
||||
type?: "button" | "submit";
|
||||
forDropdown?: boolean;
|
||||
disabled?: boolean;
|
||||
title?: string;
|
||||
@@ -32,11 +32,11 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
innerClassName,
|
||||
children,
|
||||
forDropdown,
|
||||
color = 'default',
|
||||
type = 'button',
|
||||
justify = 'center',
|
||||
size = 'md',
|
||||
variant = 'solid',
|
||||
color = "default",
|
||||
type = "button",
|
||||
justify = "center",
|
||||
size = "md",
|
||||
variant = "solid",
|
||||
leftSlot,
|
||||
rightSlot,
|
||||
disabled,
|
||||
@@ -49,8 +49,8 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
}: ButtonProps,
|
||||
ref,
|
||||
) {
|
||||
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null)?.join('');
|
||||
const fullTitle = hotkeyTrigger ? `${title ?? ''} ${hotkeyTrigger}`.trim() : title;
|
||||
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null)?.join("");
|
||||
const fullTitle = hotkeyTrigger ? `${title ?? ""} ${hotkeyTrigger}`.trim() : title;
|
||||
|
||||
if (isLoading) {
|
||||
disabled = true;
|
||||
@@ -58,37 +58,37 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
|
||||
const classes = classNames(
|
||||
className,
|
||||
'x-theme-button',
|
||||
"x-theme-button",
|
||||
`x-theme-button--${variant}`,
|
||||
`x-theme-button--${variant}--${color}`,
|
||||
'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',
|
||||
'outline-0',
|
||||
disabled ? 'pointer-events-none opacity-disabled' : 'pointer-events-auto',
|
||||
justify === 'start' && 'justify-start',
|
||||
justify === 'center' && 'justify-center',
|
||||
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-2xs px-2 text-xs rounded',
|
||||
"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",
|
||||
"outline-0",
|
||||
disabled ? "pointer-events-none opacity-disabled" : "pointer-events-auto",
|
||||
justify === "start" && "justify-start",
|
||||
justify === "center" && "justify-center",
|
||||
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-2xs px-2 text-xs rounded",
|
||||
|
||||
// Solids
|
||||
variant === 'solid' && 'border-transparent',
|
||||
variant === 'solid' && color === 'custom' && 'focus-visible:outline-2 outline-border-focus',
|
||||
variant === 'solid' &&
|
||||
color !== 'custom' &&
|
||||
'text-text enabled:hocus:text-text enabled:hocus:bg-surface-highlight outline-border-subtle',
|
||||
variant === 'solid' && color !== 'custom' && color !== 'default' && 'bg-surface',
|
||||
variant === "solid" && "border-transparent",
|
||||
variant === "solid" && color === "custom" && "focus-visible:outline-2 outline-border-focus",
|
||||
variant === "solid" &&
|
||||
color !== "custom" &&
|
||||
"text-text enabled:hocus:text-text enabled:hocus:bg-surface-highlight outline-border-subtle",
|
||||
variant === "solid" && color !== "custom" && color !== "default" && "bg-surface",
|
||||
|
||||
// Borders
|
||||
variant === 'border' && 'border',
|
||||
variant === 'border' &&
|
||||
color !== 'custom' &&
|
||||
'border-border-subtle text-text-subtle enabled:hocus:border-border ' +
|
||||
'enabled:hocus:bg-surface-highlight enabled:hocus:text-text outline-border-subtler',
|
||||
variant === "border" && "border",
|
||||
variant === "border" &&
|
||||
color !== "custom" &&
|
||||
"border-border-subtle text-text-subtle enabled:hocus:border-border " +
|
||||
"enabled:hocus:bg-surface-highlight enabled:hocus:text-text outline-border-subtler",
|
||||
);
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
@@ -121,14 +121,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoadingIcon size={size === 'auto' ? 'md' : size} className="mr-1" />
|
||||
<LoadingIcon size={size === "auto" ? "md" : size} className="mr-1" />
|
||||
) : leftSlot ? (
|
||||
<div className="mr-2">{leftSlot}</div>
|
||||
) : null}
|
||||
<div
|
||||
className={classNames(
|
||||
'truncate w-full',
|
||||
justify === 'start' ? 'text-left' : 'text-center',
|
||||
"truncate w-full",
|
||||
justify === "start" ? "text-left" : "text-center",
|
||||
innerClassName,
|
||||
)}
|
||||
>
|
||||
@@ -138,7 +138,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
{forDropdown && (
|
||||
<Icon
|
||||
icon="chevron_down"
|
||||
size={size === 'auto' ? 'md' : size}
|
||||
size={size === "auto" ? "md" : size}
|
||||
className="ml-1 -mr-1 relative top-[0.1em]"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react';
|
||||
import type { ButtonProps } from './Button';
|
||||
import { Button } from './Button';
|
||||
import { useState } from "react";
|
||||
import type { ButtonProps } from "./Button";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export function ButtonInfiniteLoading({
|
||||
onClick,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
import { IconTooltip } from './IconTooltip';
|
||||
import { HStack } from './Stacks';
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import { Icon } from "./Icon";
|
||||
import { IconTooltip } from "./IconTooltip";
|
||||
import { HStack } from "./Stacks";
|
||||
|
||||
export interface CheckboxProps {
|
||||
checked: boolean | 'indeterminate';
|
||||
checked: boolean | "indeterminate";
|
||||
title: ReactNode;
|
||||
onChange: (checked: boolean) => void;
|
||||
className?: string;
|
||||
@@ -32,34 +32,34 @@ export function Checkbox({
|
||||
as="label"
|
||||
alignItems="center"
|
||||
space={2}
|
||||
className={classNames(className, 'text-text mr-auto')}
|
||||
className={classNames(className, "text-text mr-auto")}
|
||||
>
|
||||
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex mr-0.5')}>
|
||||
<div className={classNames(inputWrapperClassName, "x-theme-input", "relative flex mr-0.5")}>
|
||||
<input
|
||||
aria-hidden
|
||||
className={classNames(
|
||||
'appearance-none w-4 h-4 flex-shrink-0 border border-border',
|
||||
'rounded outline-none ring-0',
|
||||
!disabled && 'hocus:border-border-focus hocus:bg-focus/[5%]',
|
||||
disabled && 'border-dotted',
|
||||
"appearance-none w-4 h-4 flex-shrink-0 border border-border",
|
||||
"rounded outline-none ring-0",
|
||||
!disabled && "hocus:border-border-focus hocus:bg-focus/[5%]",
|
||||
disabled && "border-dotted",
|
||||
)}
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
onChange={() => {
|
||||
onChange(checked === 'indeterminate' ? true : !checked);
|
||||
onChange(checked === "indeterminate" ? true : !checked);
|
||||
}}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Icon
|
||||
size="sm"
|
||||
className={classNames(disabled && 'opacity-disabled')}
|
||||
icon={checked === 'indeterminate' ? 'minus' : checked ? 'check' : 'empty'}
|
||||
className={classNames(disabled && "opacity-disabled")}
|
||||
icon={checked === "indeterminate" ? "minus" : checked ? "check" : "empty"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{!hideLabel && (
|
||||
<div
|
||||
className={classNames('text-sm', fullWidth && 'w-full', disabled && 'opacity-disabled')}
|
||||
className={classNames("text-sm", fullWidth && "w-full", disabled && "opacity-disabled")}
|
||||
>
|
||||
{title}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import classNames from 'classnames';
|
||||
import { useState } from 'react';
|
||||
import { HexColorPicker } from 'react-colorful';
|
||||
import { useRandomKey } from '../../hooks/useRandomKey';
|
||||
import { Icon } from './Icon';
|
||||
import { PlainInput } from './PlainInput';
|
||||
import classNames from "classnames";
|
||||
import { useState } from "react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { useRandomKey } from "../../hooks/useRandomKey";
|
||||
import { Icon } from "./Icon";
|
||||
import { PlainInput } from "./PlainInput";
|
||||
|
||||
interface Props {
|
||||
onChange: (value: string | null) => void;
|
||||
@@ -27,7 +27,7 @@ export function ColorPicker({ onChange, color, className }: Props) {
|
||||
hideLabel
|
||||
label="Plain Color"
|
||||
forceUpdateKey={updateKey}
|
||||
defaultValue={color ?? ''}
|
||||
defaultValue={color ?? ""}
|
||||
onChange={onChange}
|
||||
validate={(color) => color.match(/#[0-9a-fA-F]{6}$/) !== null}
|
||||
/>
|
||||
@@ -37,14 +37,14 @@ export function ColorPicker({ onChange, color, className }: Props) {
|
||||
|
||||
const colors = [
|
||||
null,
|
||||
'danger',
|
||||
'warning',
|
||||
'notice',
|
||||
'success',
|
||||
'primary',
|
||||
'info',
|
||||
'secondary',
|
||||
'custom',
|
||||
"danger",
|
||||
"warning",
|
||||
"notice",
|
||||
"success",
|
||||
"primary",
|
||||
"info",
|
||||
"secondary",
|
||||
"custom",
|
||||
] as const;
|
||||
|
||||
export function ColorPickerWithThemeColors({ onChange, color, className }: Props) {
|
||||
@@ -52,10 +52,10 @@ export function ColorPickerWithThemeColors({ onChange, color, className }: Props
|
||||
const [selectedColor, setSelectedColor] = useState<string | null>(() => {
|
||||
if (color == null) return null;
|
||||
const c = color?.match(/var\(--([a-z]+)\)/)?.[1];
|
||||
return c ?? 'custom';
|
||||
return c ?? "custom";
|
||||
});
|
||||
return (
|
||||
<div className={classNames(className, 'flex flex-col gap-3')}>
|
||||
<div className={classNames(className, "flex flex-col gap-3")}>
|
||||
<div className="flex items-center gap-2.5">
|
||||
{colors.map((color) => (
|
||||
<button
|
||||
@@ -65,34 +65,34 @@ export function ColorPickerWithThemeColors({ onChange, color, className }: Props
|
||||
setSelectedColor(color);
|
||||
if (color == null) {
|
||||
onChange(null);
|
||||
} else if (color === 'custom') {
|
||||
onChange('#ffffff');
|
||||
} else if (color === "custom") {
|
||||
onChange("#ffffff");
|
||||
} else {
|
||||
onChange(`var(--${color})`);
|
||||
}
|
||||
}}
|
||||
className={classNames(
|
||||
'flex items-center justify-center',
|
||||
'w-8 h-8 rounded-full transition-all',
|
||||
selectedColor === color && 'scale-[1.15]',
|
||||
selectedColor === color ? 'opacity-100' : 'opacity-60',
|
||||
color === null && 'border border-text-subtle',
|
||||
color === 'primary' && 'bg-primary',
|
||||
color === 'secondary' && 'bg-secondary',
|
||||
color === 'success' && 'bg-success',
|
||||
color === 'notice' && 'bg-notice',
|
||||
color === 'warning' && 'bg-warning',
|
||||
color === 'danger' && 'bg-danger',
|
||||
color === 'info' && 'bg-info',
|
||||
color === 'custom' &&
|
||||
'bg-[conic-gradient(var(--danger),var(--warning),var(--notice),var(--success),var(--info),var(--primary),var(--danger))]',
|
||||
"flex items-center justify-center",
|
||||
"w-8 h-8 rounded-full transition-all",
|
||||
selectedColor === color && "scale-[1.15]",
|
||||
selectedColor === color ? "opacity-100" : "opacity-60",
|
||||
color === null && "border border-text-subtle",
|
||||
color === "primary" && "bg-primary",
|
||||
color === "secondary" && "bg-secondary",
|
||||
color === "success" && "bg-success",
|
||||
color === "notice" && "bg-notice",
|
||||
color === "warning" && "bg-warning",
|
||||
color === "danger" && "bg-danger",
|
||||
color === "info" && "bg-info",
|
||||
color === "custom" &&
|
||||
"bg-[conic-gradient(var(--danger),var(--warning),var(--notice),var(--success),var(--info),var(--primary),var(--danger))]",
|
||||
)}
|
||||
>
|
||||
{color == null && <Icon icon="minus" className="text-text-subtle" size="md" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{selectedColor === 'custom' && (
|
||||
{selectedColor === "custom" && (
|
||||
<>
|
||||
<HexColorPicker
|
||||
color={color ?? undefined}
|
||||
@@ -106,7 +106,7 @@ export function ColorPickerWithThemeColors({ onChange, color, className }: Props
|
||||
hideLabel
|
||||
label="Plain Color"
|
||||
forceUpdateKey={updateKey}
|
||||
defaultValue={color ?? ''}
|
||||
defaultValue={color ?? ""}
|
||||
onChange={onChange}
|
||||
validate={(color) => color.match(/#[0-9a-fA-F]{6}$/) !== null}
|
||||
/>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import type { FormEvent } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { CopyIconButton } from '../CopyIconButton';
|
||||
import { Button } from './Button';
|
||||
import { PlainInput } from './PlainInput';
|
||||
import { HStack } from './Stacks';
|
||||
import type { Color } from "@yaakapp-internal/plugins";
|
||||
import type { FormEvent } from "react";
|
||||
import { useState } from "react";
|
||||
import { CopyIconButton } from "../CopyIconButton";
|
||||
import { Button } from "./Button";
|
||||
import { PlainInput } from "./PlainInput";
|
||||
import { HStack } from "./Stacks";
|
||||
|
||||
export interface ConfirmProps {
|
||||
onHide: () => void;
|
||||
@@ -19,9 +19,9 @@ export function Confirm({
|
||||
onResult,
|
||||
confirmText,
|
||||
requireTyping,
|
||||
color = 'primary',
|
||||
color = "primary",
|
||||
}: ConfirmProps) {
|
||||
const [confirm, setConfirm] = useState<string>('');
|
||||
const [confirm, setConfirm] = useState<string>("");
|
||||
const handleHide = () => {
|
||||
onResult(false);
|
||||
onHide();
|
||||
@@ -63,7 +63,7 @@ export function Confirm({
|
||||
)}
|
||||
<HStack space={2} justifyContent="start" className="mt-2 mb-4 flex-row-reverse">
|
||||
<Button type="submit" color={color} disabled={!didConfirm}>
|
||||
{confirmText ?? 'Confirm'}
|
||||
{confirmText ?? "Confirm"}
|
||||
</Button>
|
||||
<Button onClick={handleHide} variant="border">
|
||||
Cancel
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import type { Color } from "@yaakapp-internal/plugins";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
count: number | true;
|
||||
@@ -17,15 +17,15 @@ export function CountBadge({ count, count2, className, color, showZero }: Props)
|
||||
aria-hidden
|
||||
className={classNames(
|
||||
className,
|
||||
'flex items-center',
|
||||
'opacity-70 border text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono',
|
||||
color == null && 'border-border-subtle',
|
||||
color === 'primary' && 'text-primary',
|
||||
color === 'secondary' && 'text-secondary',
|
||||
color === 'success' && 'text-success',
|
||||
color === 'notice' && 'text-notice',
|
||||
color === 'warning' && 'text-warning',
|
||||
color === 'danger' && 'text-danger',
|
||||
"flex items-center",
|
||||
"opacity-70 border text-4xs rounded mb-0.5 px-1 ml-1 h-4 font-mono",
|
||||
color == null && "border-border-subtle",
|
||||
color === "primary" && "text-primary",
|
||||
color === "secondary" && "text-secondary",
|
||||
color === "success" && "text-success",
|
||||
color === "notice" && "text-notice",
|
||||
color === "warning" && "text-warning",
|
||||
color === "danger" && "text-danger",
|
||||
)}
|
||||
>
|
||||
{count === true ? (
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import classNames from 'classnames';
|
||||
import { atom, useAtom } from 'jotai';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { atomWithKVStorage } from '../../lib/atoms/atomWithKVStorage';
|
||||
import type { BannerProps } from './Banner';
|
||||
import { Banner } from './Banner';
|
||||
import classNames from "classnames";
|
||||
import { atom, useAtom } from "jotai";
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { atomWithKVStorage } from "../../lib/atoms/atomWithKVStorage";
|
||||
import type { BannerProps } from "./Banner";
|
||||
import { Banner } from "./Banner";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLDetailsElement> {
|
||||
summary: ReactNode;
|
||||
color?: BannerProps['color'];
|
||||
color?: BannerProps["color"];
|
||||
defaultOpen?: boolean;
|
||||
storageKey?: string;
|
||||
}
|
||||
@@ -26,7 +26,7 @@ export function DetailsBanner({
|
||||
const openAtom = useMemo(
|
||||
() =>
|
||||
storageKey
|
||||
? atomWithKVStorage<boolean>(['details_banner', storageKey], defaultOpen ?? false)
|
||||
? atomWithKVStorage<boolean>(["details_banner", storageKey], defaultOpen ?? false)
|
||||
: atom(defaultOpen ?? false),
|
||||
[storageKey],
|
||||
);
|
||||
@@ -45,10 +45,10 @@ export function DetailsBanner({
|
||||
<summary className="!cursor-default !select-none list-none flex items-center gap-3 focus:outline-none opacity-70">
|
||||
<div
|
||||
className={classNames(
|
||||
'transition-transform',
|
||||
'group-open:rotate-90',
|
||||
'w-0 h-0 border-t-[0.3em] border-b-[0.3em] border-l-[0.5em] border-r-0',
|
||||
'border-t-transparent border-b-transparent border-l-text-subtle',
|
||||
"transition-transform",
|
||||
"group-open:rotate-90",
|
||||
"w-0 h-0 border-t-[0.3em] border-b-[0.3em] border-l-[0.5em] border-r-0",
|
||||
"border-t-transparent border-b-transparent border-l-text-subtle",
|
||||
)}
|
||||
/>
|
||||
{summary}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import classNames from 'classnames';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { Heading } from './Heading';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { DialogSize } from '@yaakapp-internal/plugins';
|
||||
import classNames from "classnames";
|
||||
import * as m from "motion/react-m";
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { Overlay } from "../Overlay";
|
||||
import { Heading } from "./Heading";
|
||||
import { IconButton } from "./IconButton";
|
||||
import type { DialogSize } from "@yaakapp-internal/plugins";
|
||||
|
||||
export interface DialogProps {
|
||||
children: ReactNode;
|
||||
@@ -19,13 +19,13 @@ export interface DialogProps {
|
||||
hideX?: boolean;
|
||||
noPadding?: boolean;
|
||||
noScroll?: boolean;
|
||||
vAlign?: 'top' | 'center';
|
||||
vAlign?: "top" | "center";
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
children,
|
||||
className,
|
||||
size = 'full',
|
||||
size = "full",
|
||||
open,
|
||||
onClose,
|
||||
disableBackdropClose,
|
||||
@@ -34,7 +34,7 @@ export function Dialog({
|
||||
hideX,
|
||||
noPadding,
|
||||
noScroll,
|
||||
vAlign = 'center',
|
||||
vAlign = "center",
|
||||
}: DialogProps) {
|
||||
const titleId = useMemo(() => Math.random().toString(36).slice(2), []);
|
||||
const descriptionId = useMemo(
|
||||
@@ -47,10 +47,10 @@ export function Dialog({
|
||||
<div
|
||||
role="dialog"
|
||||
className={classNames(
|
||||
'py-4 x-theme-dialog absolute inset-0 pointer-events-none',
|
||||
'h-full flex flex-col items-center justify-center',
|
||||
vAlign === 'top' && 'justify-start',
|
||||
vAlign === 'center' && 'justify-center',
|
||||
"py-4 x-theme-dialog absolute inset-0 pointer-events-none",
|
||||
"h-full flex flex-col items-center justify-center",
|
||||
vAlign === "top" && "justify-start",
|
||||
vAlign === "center" && "justify-center",
|
||||
)}
|
||||
aria-labelledby={titleId}
|
||||
aria-describedby={descriptionId}
|
||||
@@ -58,7 +58,7 @@ export function Dialog({
|
||||
onKeyDown={(e) => {
|
||||
// NOTE: We handle Escape on the element itself so that it doesn't close multiple
|
||||
// dialogs and can be intercepted by children if needed.
|
||||
if (e.key === 'Escape') {
|
||||
if (e.key === "Escape") {
|
||||
onClose?.();
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@@ -70,18 +70,18 @@ export function Dialog({
|
||||
animate={{ top: 0, scale: 1 }}
|
||||
className={classNames(
|
||||
className,
|
||||
'grid grid-rows-[auto_auto_minmax(0,1fr)]',
|
||||
'grid-cols-1', // must be here for inline code blocks to correctly break words
|
||||
'relative bg-surface pointer-events-auto',
|
||||
'rounded-lg',
|
||||
'border border-border-subtle shadow-lg shadow-[rgba(0,0,0,0.1)]',
|
||||
'min-h-[10rem]',
|
||||
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]',
|
||||
size === 'sm' && 'w-[30rem]',
|
||||
size === 'md' && 'w-[50rem]',
|
||||
size === 'lg' && 'w-[70rem]',
|
||||
size === 'full' && 'w-[100vw] h-[100vh]',
|
||||
size === 'dynamic' && 'min-w-[20rem] max-w-[100vw]',
|
||||
"grid grid-rows-[auto_auto_minmax(0,1fr)]",
|
||||
"grid-cols-1", // must be here for inline code blocks to correctly break words
|
||||
"relative bg-surface pointer-events-auto",
|
||||
"rounded-lg",
|
||||
"border border-border-subtle shadow-lg shadow-[rgba(0,0,0,0.1)]",
|
||||
"min-h-[10rem]",
|
||||
"max-w-[calc(100vw-5rem)] max-h-[calc(100vh-5rem)]",
|
||||
size === "sm" && "w-[30rem]",
|
||||
size === "md" && "w-[50rem]",
|
||||
size === "lg" && "w-[70rem]",
|
||||
size === "full" && "w-[100vw] h-[100vh]",
|
||||
size === "dynamic" && "min-w-[20rem] max-w-[100vw]",
|
||||
)}
|
||||
>
|
||||
{title ? (
|
||||
@@ -102,9 +102,9 @@ export function Dialog({
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'h-full w-full grid grid-cols-[minmax(0,1fr)] grid-rows-1',
|
||||
!noPadding && 'px-6 py-2',
|
||||
!noScroll && 'overflow-y-auto overflow-x-hidden',
|
||||
"h-full w-full grid grid-cols-[minmax(0,1fr)] grid-rows-1",
|
||||
!noPadding && "px-6 py-2",
|
||||
!noScroll && "overflow-y-auto overflow-x-hidden",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import { useKeyValue } from '../../hooks/useKeyValue';
|
||||
import type { BannerProps } from './Banner';
|
||||
import { Banner } from './Banner';
|
||||
import { Button } from './Button';
|
||||
import { HStack } from './Stacks';
|
||||
import type { Color } from "@yaakapp-internal/plugins";
|
||||
import classNames from "classnames";
|
||||
import { useKeyValue } from "../../hooks/useKeyValue";
|
||||
import type { BannerProps } from "./Banner";
|
||||
import { Banner } from "./Banner";
|
||||
import { Button } from "./Button";
|
||||
import { HStack } from "./Stacks";
|
||||
|
||||
export function DismissibleBanner({
|
||||
children,
|
||||
@@ -17,8 +17,8 @@ export function DismissibleBanner({
|
||||
actions?: { label: string; onClick: () => void; color?: Color }[];
|
||||
}) {
|
||||
const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
|
||||
namespace: 'global',
|
||||
key: ['dismiss-banner', id],
|
||||
namespace: "global",
|
||||
key: ["dismiss-banner", id],
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ export function DismissibleBanner({
|
||||
|
||||
return (
|
||||
<Banner
|
||||
className={classNames(className, 'relative grid grid-cols-[1fr_auto] gap-3')}
|
||||
className={classNames(className, "relative grid grid-cols-[1fr_auto] gap-3")}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import { atom } from 'jotai';
|
||||
import * as m from 'motion/react-m';
|
||||
import classNames from "classnames";
|
||||
import { atom } from "jotai";
|
||||
import * as m from "motion/react-m";
|
||||
import type {
|
||||
CSSProperties,
|
||||
HTMLAttributes,
|
||||
@@ -11,7 +11,7 @@ import type {
|
||||
ReactNode,
|
||||
RefObject,
|
||||
SetStateAction,
|
||||
} from 'react';
|
||||
} from "react";
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
@@ -22,43 +22,43 @@ import {
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useKey, useWindowSize } from 'react-use';
|
||||
import { useClickOutside } from '../../hooks/useClickOutside';
|
||||
import { fireAndForget } from '../../lib/fireAndForget';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useHotKey } from '../../hooks/useHotKey';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import { getNodeText } from '../../lib/getNodeText';
|
||||
import { jotaiStore } from '../../lib/jotai';
|
||||
import { ErrorBoundary } from '../ErrorBoundary';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { Button } from './Button';
|
||||
import { Hotkey } from './Hotkey';
|
||||
import { Icon, type IconProps } from './Icon';
|
||||
import { LoadingIcon } from './LoadingIcon';
|
||||
import { Separator } from './Separator';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
} from "react";
|
||||
import { useKey, useWindowSize } from "react-use";
|
||||
import { useClickOutside } from "../../hooks/useClickOutside";
|
||||
import { fireAndForget } from "../../lib/fireAndForget";
|
||||
import type { HotkeyAction } from "../../hooks/useHotKey";
|
||||
import { useHotKey } from "../../hooks/useHotKey";
|
||||
import { useStateWithDeps } from "../../hooks/useStateWithDeps";
|
||||
import { generateId } from "../../lib/generateId";
|
||||
import { getNodeText } from "../../lib/getNodeText";
|
||||
import { jotaiStore } from "../../lib/jotai";
|
||||
import { ErrorBoundary } from "../ErrorBoundary";
|
||||
import { Overlay } from "../Overlay";
|
||||
import { Button } from "./Button";
|
||||
import { Hotkey } from "./Hotkey";
|
||||
import { Icon, type IconProps } from "./Icon";
|
||||
import { LoadingIcon } from "./LoadingIcon";
|
||||
import { Separator } from "./Separator";
|
||||
import { HStack, VStack } from "./Stacks";
|
||||
|
||||
export type DropdownItemSeparator = {
|
||||
type: 'separator';
|
||||
type: "separator";
|
||||
label?: ReactNode;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export type DropdownItemContent = {
|
||||
type: 'content';
|
||||
type: "content";
|
||||
label?: ReactNode;
|
||||
hidden?: boolean;
|
||||
};
|
||||
|
||||
export type DropdownItemDefault = {
|
||||
type?: 'default';
|
||||
type?: "default";
|
||||
label: ReactNode;
|
||||
hotKeyAction?: HotkeyAction;
|
||||
hotKeyLabelOnly?: boolean;
|
||||
color?: 'default' | 'primary' | 'danger' | 'info' | 'warning' | 'notice' | 'success';
|
||||
color?: "default" | "primary" | "danger" | "info" | "warning" | "notice" | "success";
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
leftSlot?: ReactNode;
|
||||
@@ -69,7 +69,7 @@ export type DropdownItemDefault = {
|
||||
submenu?: DropdownItem[];
|
||||
/** If true, submenu opens on click instead of hover */
|
||||
submenuOpenOnClick?: boolean;
|
||||
icon?: IconProps['icon'];
|
||||
icon?: IconProps["icon"];
|
||||
};
|
||||
|
||||
export type DropdownItem = DropdownItemDefault | DropdownItemSeparator | DropdownItemContent;
|
||||
@@ -117,20 +117,20 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
// const [isOpen, _setIsOpen] = useState<boolean>(false);
|
||||
const [defaultSelectedIndex, setDefaultSelectedIndex] = useState<number | null>(null);
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
|
||||
const menuRef = useRef<Omit<DropdownRef, "open">>(null);
|
||||
|
||||
const handleSetIsOpen = useCallback(
|
||||
(o: SetStateAction<boolean>) => {
|
||||
jotaiStore.set(openAtom, (prevId) => {
|
||||
const prevIsOpen = prevId === id.current;
|
||||
const newIsOpen = typeof o === 'function' ? o(prevIsOpen) : o;
|
||||
const newIsOpen = typeof o === "function" ? o(prevIsOpen) : o;
|
||||
// Persist background color of button until we close the dropdown
|
||||
if (newIsOpen) {
|
||||
onOpen?.();
|
||||
if (buttonRef.current) {
|
||||
buttonRef.current.style.backgroundColor = window
|
||||
.getComputedStyle(buttonRef.current)
|
||||
.getPropertyValue('background-color');
|
||||
.getPropertyValue("background-color");
|
||||
}
|
||||
}
|
||||
return newIsOpen ? id.current : null; // Set global atom to current ID to signify open state
|
||||
@@ -144,7 +144,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
// Clear persisted BG
|
||||
if (buttonRef.current) buttonRef.current.style.backgroundColor = '';
|
||||
if (buttonRef.current) buttonRef.current.style.backgroundColor = "";
|
||||
// Set to different value when opened and closed to force it to update. This is to force
|
||||
// <Menu/> to reset its selected-index state, which it does when this prop changes
|
||||
setDefaultSelectedIndex(null);
|
||||
@@ -187,7 +187,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
{
|
||||
...existingChild.props,
|
||||
ref: buttonRef,
|
||||
'aria-haspopup': 'true',
|
||||
"aria-haspopup": "true",
|
||||
onClick: (e: MouseEvent<HTMLButtonElement>) => {
|
||||
// Call original onClick first if it exists
|
||||
originalOnClick?.(e);
|
||||
@@ -204,7 +204,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
}, [children, handleSetIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
buttonRef.current?.setAttribute('aria-expanded', isOpen.toString());
|
||||
buttonRef.current?.setAttribute("aria-expanded", isOpen.toString());
|
||||
}, [isOpen]);
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
@@ -217,7 +217,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
return (
|
||||
<>
|
||||
{child}
|
||||
<ErrorBoundary name={'Dropdown Menu'}>
|
||||
<ErrorBoundary name={"Dropdown Menu"}>
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
showTriangle
|
||||
@@ -237,7 +237,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
export interface ContextMenuProps {
|
||||
triggerPosition: { x: number; y: number } | null;
|
||||
className?: string;
|
||||
items: DropdownProps['items'];
|
||||
items: DropdownProps["items"];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ export const ContextMenu = forwardRef<DropdownRef, ContextMenuProps>(function Co
|
||||
interface MenuProps {
|
||||
className?: string;
|
||||
defaultSelectedIndex: number | null;
|
||||
triggerShape: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'> | null;
|
||||
triggerShape: Pick<DOMRect, "top" | "bottom" | "left" | "right"> | null;
|
||||
onClose: () => void;
|
||||
onCloseAll?: () => void;
|
||||
showTriangle?: boolean;
|
||||
@@ -284,7 +284,7 @@ interface MenuProps {
|
||||
isSubmenu?: boolean;
|
||||
}
|
||||
|
||||
const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'>, MenuProps>(
|
||||
const Menu = forwardRef<Omit<DropdownRef, "open" | "isOpen" | "toggle" | "items">, MenuProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
@@ -306,12 +306,12 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
[defaultSelectedIndex],
|
||||
);
|
||||
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
const [filter, setFilter] = useState<string>("");
|
||||
|
||||
// Clear filter when menu opens
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setFilter('');
|
||||
setFilter("");
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
@@ -357,18 +357,18 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
e.preventDefault();
|
||||
setFilter((f) => f + e.key);
|
||||
setSelectedIndex(0);
|
||||
} else if (e.key === 'Backspace' && !isSpecial) {
|
||||
} else if (e.key === "Backspace" && !isSpecial) {
|
||||
e.preventDefault();
|
||||
setFilter((f) => f.slice(0, -1));
|
||||
}
|
||||
};
|
||||
|
||||
useKey(
|
||||
'Escape',
|
||||
"Escape",
|
||||
() => {
|
||||
if (!isOpen) return;
|
||||
if (activeSubmenu) setActiveSubmenu(null);
|
||||
else if (filter !== '') setFilter('');
|
||||
else if (filter !== "") setFilter("");
|
||||
else handleClose();
|
||||
},
|
||||
{},
|
||||
@@ -381,7 +381,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
let nextIndex = (currIndex ?? 0) - incrBy;
|
||||
const maxTries = items.length;
|
||||
for (let i = 0; i < maxTries; i++) {
|
||||
if (items[nextIndex]?.hidden || items[nextIndex]?.type === 'separator') {
|
||||
if (items[nextIndex]?.hidden || items[nextIndex]?.type === "separator") {
|
||||
nextIndex--;
|
||||
} else if (nextIndex < 0) {
|
||||
nextIndex = items.length - 1;
|
||||
@@ -401,7 +401,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
let nextIndex = (currIndex ?? -1) + incrBy;
|
||||
const maxTries = items.length;
|
||||
for (let i = 0; i < maxTries; i++) {
|
||||
if (items[nextIndex]?.hidden || items[nextIndex]?.type === 'separator') {
|
||||
if (items[nextIndex]?.hidden || items[nextIndex]?.type === "separator") {
|
||||
nextIndex++;
|
||||
} else if (nextIndex >= items.length) {
|
||||
nextIndex = 0;
|
||||
@@ -418,13 +418,13 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
// Ensure selection is on a valid item (not hidden/separator/content)
|
||||
useEffect(() => {
|
||||
const item = items[selectedIndex ?? -1];
|
||||
if (item?.hidden || item?.type === 'separator' || item?.type === 'content') {
|
||||
if (item?.hidden || item?.type === "separator" || item?.type === "content") {
|
||||
handleNext();
|
||||
}
|
||||
}, [selectedIndex, items, handleNext]);
|
||||
|
||||
useKey(
|
||||
'ArrowUp',
|
||||
"ArrowUp",
|
||||
(e) => {
|
||||
if (!isOpen || activeSubmenu) return;
|
||||
e.preventDefault();
|
||||
@@ -435,7 +435,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
);
|
||||
|
||||
useKey(
|
||||
'ArrowDown',
|
||||
"ArrowDown",
|
||||
(e) => {
|
||||
if (!isOpen || activeSubmenu) return;
|
||||
e.preventDefault();
|
||||
@@ -446,7 +446,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
);
|
||||
|
||||
useKey(
|
||||
'ArrowLeft',
|
||||
"ArrowLeft",
|
||||
(e) => {
|
||||
if (!isOpen) return;
|
||||
// Only handle if this menu doesn't have an open submenu
|
||||
@@ -465,12 +465,12 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
const handleSelect = useCallback(
|
||||
async (item: DropdownItem, parentEl?: HTMLButtonElement) => {
|
||||
// Handle click-to-open submenu
|
||||
if ('submenu' in item && item.submenu && item.submenuOpenOnClick && parentEl) {
|
||||
if ("submenu" in item && item.submenu && item.submenuOpenOnClick && parentEl) {
|
||||
setActiveSubmenu({ item, parent: parentEl });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!('onSelect' in item) || !item.onSelect) return;
|
||||
if (!("onSelect" in item) || !item.onSelect) return;
|
||||
setSelectedIndex(null);
|
||||
|
||||
const promise = item.onSelect();
|
||||
@@ -554,17 +554,17 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
right: onRight ? docRect.width - triggerShape.right : undefined,
|
||||
left: !onRight ? triggerShape.left : undefined,
|
||||
minWidth: fullWidth ? triggerWidth : undefined,
|
||||
maxWidth: '40rem',
|
||||
maxWidth: "40rem",
|
||||
},
|
||||
triangle: {
|
||||
width: '0.4rem',
|
||||
height: '0.4rem',
|
||||
width: "0.4rem",
|
||||
height: "0.4rem",
|
||||
...(onRight
|
||||
? { right: width / 2, marginRight: '-0.2rem' }
|
||||
: { left: width / 2, marginLeft: '-0.2rem' }),
|
||||
? { right: width / 2, marginRight: "-0.2rem" }
|
||||
: { left: width / 2, marginLeft: "-0.2rem" }),
|
||||
...(upsideDown
|
||||
? { bottom: '-0.2rem', rotate: '225deg' }
|
||||
: { top: '-0.2rem', rotate: '45deg' }),
|
||||
? { bottom: "-0.2rem", rotate: "225deg" }
|
||||
: { top: "-0.2rem", rotate: "45deg" }),
|
||||
},
|
||||
menu: {
|
||||
maxHeight: `${(upsideDown ? heightAbove : heightBelow) - 15}px`,
|
||||
@@ -586,11 +586,11 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
);
|
||||
|
||||
useKey(
|
||||
'ArrowRight',
|
||||
"ArrowRight",
|
||||
(e) => {
|
||||
if (!isOpen || activeSubmenu) return;
|
||||
const item = filteredItems[selectedIndex ?? -1];
|
||||
if (item?.type !== 'separator' && item?.type !== 'content' && item?.submenu) {
|
||||
if (item?.type !== "separator" && item?.type !== "content" && item?.submenu) {
|
||||
e.preventDefault();
|
||||
const parent = document.activeElement as HTMLButtonElement;
|
||||
if (parent) {
|
||||
@@ -603,11 +603,11 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
);
|
||||
|
||||
useKey(
|
||||
'Enter',
|
||||
"Enter",
|
||||
(e) => {
|
||||
if (!isOpen || activeSubmenu) return;
|
||||
const item = filteredItems[selectedIndex ?? -1];
|
||||
if (!item || item.type === 'separator' || item.type === 'content') return;
|
||||
if (!item || item.type === "separator" || item.type === "content") return;
|
||||
e.preventDefault();
|
||||
if (item.submenu) {
|
||||
const parent = document.activeElement as HTMLButtonElement;
|
||||
@@ -714,9 +714,9 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
style={styles.container}
|
||||
className={classNames(
|
||||
className,
|
||||
'x-theme-menu',
|
||||
'outline-none my-1 pointer-events-auto z-40',
|
||||
'fixed',
|
||||
"x-theme-menu",
|
||||
"outline-none my-1 pointer-events-auto z-40",
|
||||
"fixed",
|
||||
)}
|
||||
>
|
||||
{showTriangle && !isSubmenu && (
|
||||
@@ -730,8 +730,8 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
style={styles.menu}
|
||||
className={classNames(
|
||||
className,
|
||||
'h-auto bg-surface rounded-md shadow-lg py-1.5 border',
|
||||
'border-border-subtle overflow-y-auto overflow-x-hidden mx-0.5',
|
||||
"h-auto bg-surface rounded-md shadow-lg py-1.5 border",
|
||||
"border-border-subtle overflow-y-auto overflow-x-hidden mx-0.5",
|
||||
)}
|
||||
>
|
||||
{filter && (
|
||||
@@ -750,22 +750,22 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
if (item.hidden) {
|
||||
return null;
|
||||
}
|
||||
if (item.type === 'separator') {
|
||||
if (item.type === "separator") {
|
||||
return (
|
||||
<Separator
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Nothing else available
|
||||
key={i}
|
||||
className={classNames('my-1.5', item.label ? 'ml-2' : null)}
|
||||
className={classNames("my-1.5", item.label ? "ml-2" : null)}
|
||||
>
|
||||
{item.label}
|
||||
</Separator>
|
||||
);
|
||||
}
|
||||
if (item.type === 'content') {
|
||||
if (item.type === "content") {
|
||||
return (
|
||||
// oxlint-disable-next-line jsx-a11y/no-static-element-interactions
|
||||
// oxlint-disable-next-line react/no-array-index-key
|
||||
<div key={i} className={classNames('my-1 mx-2 max-w-xs')} onClick={onClose}>
|
||||
<div key={i} className={classNames("my-1 mx-2 max-w-xs")} onClick={onClose}>
|
||||
{item.label}
|
||||
</div>
|
||||
);
|
||||
@@ -812,8 +812,8 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
|
||||
// Hotkeys must be rendered even when menu is closed (so they work globally)
|
||||
const hotKeyElements = items.map(
|
||||
(item, i) =>
|
||||
item.type !== 'separator' &&
|
||||
item.type !== 'content' &&
|
||||
item.type !== "separator" &&
|
||||
item.type !== "content" &&
|
||||
!item.hotKeyLabelOnly &&
|
||||
item.hotKeyAction && (
|
||||
<MenuItemHotKey
|
||||
@@ -915,7 +915,7 @@ function MenuItem({
|
||||
justify="start"
|
||||
leftSlot={
|
||||
(isLoading || item.leftSlot || item.icon) && (
|
||||
<div className={classNames('pr-2 flex justify-start [&_svg]:opacity-70')}>
|
||||
<div className={classNames("pr-2 flex justify-start [&_svg]:opacity-70")}>
|
||||
{isLoading ? <LoadingIcon /> : item.icon ? <Icon icon={item.icon} /> : item.leftSlot}
|
||||
</div>
|
||||
)
|
||||
@@ -925,28 +925,28 @@ function MenuItem({
|
||||
color="custom"
|
||||
className={classNames(
|
||||
className,
|
||||
'h-xs', // More compact
|
||||
'min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap',
|
||||
'focus:bg-surface-highlight focus:text rounded focus:outline-none focus-visible:outline-1',
|
||||
isParentOfActiveSubmenu && 'bg-surface-highlight text rounded',
|
||||
item.color === 'danger' && '!text-danger',
|
||||
item.color === 'primary' && '!text-primary',
|
||||
item.color === 'success' && '!text-success',
|
||||
item.color === 'warning' && '!text-warning',
|
||||
item.color === 'notice' && '!text-notice',
|
||||
item.color === 'info' && '!text-info',
|
||||
"h-xs", // More compact
|
||||
"min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap",
|
||||
"focus:bg-surface-highlight focus:text rounded focus:outline-none focus-visible:outline-1",
|
||||
isParentOfActiveSubmenu && "bg-surface-highlight text rounded",
|
||||
item.color === "danger" && "!text-danger",
|
||||
item.color === "primary" && "!text-primary",
|
||||
item.color === "success" && "!text-success",
|
||||
item.color === "warning" && "!text-warning",
|
||||
item.color === "notice" && "!text-notice",
|
||||
item.color === "info" && "!text-info",
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<div className={classNames('truncate min-w-[5rem]')}>{item.label}</div>
|
||||
<div className={classNames("truncate min-w-[5rem]")}>{item.label}</div>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
interface MenuItemHotKeyProps {
|
||||
action: HotkeyAction | undefined;
|
||||
onSelect: MenuItemProps['onSelect'];
|
||||
item: MenuItemProps['item'];
|
||||
onSelect: MenuItemProps["onSelect"];
|
||||
item: MenuItemProps["item"];
|
||||
}
|
||||
|
||||
function MenuItemHotKey({ action, onSelect, item }: MenuItemHotKeyProps) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type DecorationSet, MatchDecorator, type ViewUpdate } from '@codemirror/view';
|
||||
import { type DecorationSet, MatchDecorator, type ViewUpdate } from "@codemirror/view";
|
||||
|
||||
/**
|
||||
* This is a custom MatchDecorator that will not decorate a match if the selection is inside it
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { yaml } from '@codemirror/lang-yaml';
|
||||
import { syntaxHighlighting } from '@codemirror/language';
|
||||
import { MergeView } from '@codemirror/merge';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import classNames from 'classnames';
|
||||
import { useEffect, useRef } from 'react';
|
||||
import './DiffViewer.css';
|
||||
import { readonlyExtensions, syntaxHighlightStyle } from './extensions';
|
||||
import { yaml } from "@codemirror/lang-yaml";
|
||||
import { syntaxHighlighting } from "@codemirror/language";
|
||||
import { MergeView } from "@codemirror/merge";
|
||||
import { EditorView } from "@codemirror/view";
|
||||
import classNames from "classnames";
|
||||
import { useEffect, useRef } from "react";
|
||||
import "./DiffViewer.css";
|
||||
import { readonlyExtensions, syntaxHighlightStyle } from "./extensions";
|
||||
|
||||
interface Props {
|
||||
/** Original/previous version (left side) */
|
||||
@@ -45,7 +45,7 @@ export function DiffViewer({ original, modified, className }: Props) {
|
||||
collapseUnchanged: { margin: 2, minSize: 3 },
|
||||
highlightChanges: false,
|
||||
gutter: true,
|
||||
orientation: 'a-b',
|
||||
orientation: "a-b",
|
||||
revertControls: undefined,
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ export function DiffViewer({ original, modified, className }: Props) {
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={classNames('cm-wrapper cm-multiline h-full w-full', className)}
|
||||
className={classNames("cm-wrapper cm-multiline h-full w-full", className)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@
|
||||
}
|
||||
|
||||
input.cm-textfield {
|
||||
@apply cursor-text;
|
||||
@apply cursor-text;
|
||||
}
|
||||
|
||||
.cm-search label {
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { startCompletion } from '@codemirror/autocomplete';
|
||||
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';
|
||||
import { EditorView, keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
||||
import { emacs } from '@replit/codemirror-emacs';
|
||||
import { vim } from '@replit/codemirror-vim';
|
||||
import { startCompletion } from "@codemirror/autocomplete";
|
||||
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";
|
||||
import { EditorView, keymap, placeholder as placeholderExt, tooltips } from "@codemirror/view";
|
||||
import { emacs } from "@replit/codemirror-emacs";
|
||||
import { vim } from "@replit/codemirror-vim";
|
||||
|
||||
import { vscodeKeymap } from '@replit/codemirror-vscode-keymap';
|
||||
import type { EditorKeymap } from '@yaakapp-internal/models';
|
||||
import { settingsAtom } from '@yaakapp-internal/models';
|
||||
import type { EditorLanguage, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import type { GraphQLSchema } from 'graphql';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { md5 } from 'js-md5';
|
||||
import type { ReactNode, RefObject } from 'react';
|
||||
import { vscodeKeymap } from "@replit/codemirror-vscode-keymap";
|
||||
import type { EditorKeymap } from "@yaakapp-internal/models";
|
||||
import { settingsAtom } from "@yaakapp-internal/models";
|
||||
import type { EditorLanguage, TemplateFunction } from "@yaakapp-internal/plugins";
|
||||
import classNames from "classnames";
|
||||
import type { GraphQLSchema } from "graphql";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { md5 } from "js-md5";
|
||||
import type { ReactNode, RefObject } from "react";
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
@@ -25,32 +25,32 @@ import {
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { activeEnvironmentAtom } from '../../../hooks/useActiveEnvironment';
|
||||
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
|
||||
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
|
||||
import { eventMatchesHotkey } from '../../../hooks/useHotKey';
|
||||
import { useRequestEditor } from '../../../hooks/useRequestEditor';
|
||||
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
|
||||
import { editEnvironment } from '../../../lib/editEnvironment';
|
||||
import { tryFormatJson, tryFormatXml } from '../../../lib/formatters';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import { withEncryptionEnabled } from '../../../lib/setupOrConfigureEncryption';
|
||||
import { TemplateFunctionDialog } from '../../TemplateFunctionDialog';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { HStack } from '../Stacks';
|
||||
import './Editor.css';
|
||||
} from "react";
|
||||
import { activeEnvironmentAtom } from "../../../hooks/useActiveEnvironment";
|
||||
import type { WrappedEnvironmentVariable } from "../../../hooks/useEnvironmentVariables";
|
||||
import { useEnvironmentVariables } from "../../../hooks/useEnvironmentVariables";
|
||||
import { eventMatchesHotkey } from "../../../hooks/useHotKey";
|
||||
import { useRequestEditor } from "../../../hooks/useRequestEditor";
|
||||
import { useTemplateFunctionCompletionOptions } from "../../../hooks/useTemplateFunctions";
|
||||
import { editEnvironment } from "../../../lib/editEnvironment";
|
||||
import { tryFormatJson, tryFormatXml } from "../../../lib/formatters";
|
||||
import { jotaiStore } from "../../../lib/jotai";
|
||||
import { withEncryptionEnabled } from "../../../lib/setupOrConfigureEncryption";
|
||||
import { TemplateFunctionDialog } from "../../TemplateFunctionDialog";
|
||||
import { IconButton } from "../IconButton";
|
||||
import { HStack } from "../Stacks";
|
||||
import "./Editor.css";
|
||||
import {
|
||||
baseExtensions,
|
||||
getLanguageExtension,
|
||||
multiLineExtensions,
|
||||
readonlyExtensions,
|
||||
} from './extensions';
|
||||
import type { GenericCompletionConfig } from './genericCompletion';
|
||||
import { singleLineExtensions } from './singleLine';
|
||||
} 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 vsCodeWithoutTab = vscodeKeymap.filter((k) => k.key !== "Tab");
|
||||
|
||||
const keymapExtensions: Record<EditorKeymap, Extension> = {
|
||||
vim: vim(),
|
||||
@@ -74,10 +74,10 @@ export interface EditorProps {
|
||||
forcedEnvironmentId?: string;
|
||||
forceUpdateKey?: string | number;
|
||||
format?: (v: string) => Promise<string>;
|
||||
heightMode?: 'auto' | 'full';
|
||||
heightMode?: "auto" | "full";
|
||||
hideGutter?: boolean;
|
||||
id?: string;
|
||||
language?: EditorLanguage | 'pairs' | 'url' | 'timeline' | null;
|
||||
language?: EditorLanguage | "pairs" | "url" | "timeline" | null;
|
||||
lintExtension?: Extension;
|
||||
graphQLSchema?: GraphQLSchema | null;
|
||||
onBlur?: () => void;
|
||||
@@ -92,7 +92,7 @@ export interface EditorProps {
|
||||
containerOnly?: boolean;
|
||||
stateKey: string | null;
|
||||
tooltipContainer?: HTMLElement;
|
||||
type?: 'text' | 'password';
|
||||
type?: "text" | "password";
|
||||
wrapLines?: boolean;
|
||||
setRef?: (view: EditorView | null) => void;
|
||||
}
|
||||
@@ -147,7 +147,7 @@ function EditorInner({
|
||||
const useTemplating = !!(autocompleteFunctions || autocompleteVariables || autocomplete);
|
||||
const environmentVariables = useMemo(() => {
|
||||
if (!autocompleteVariables) return emptyVariables;
|
||||
return typeof autocompleteVariables === 'function'
|
||||
return typeof autocompleteVariables === "function"
|
||||
? allEnvironmentVariables.filter(autocompleteVariables)
|
||||
: allEnvironmentVariables;
|
||||
}, [allEnvironmentVariables, autocompleteVariables]);
|
||||
@@ -163,18 +163,18 @@ function EditorInner({
|
||||
if (
|
||||
singleLine ||
|
||||
language == null ||
|
||||
language === 'text' ||
|
||||
language === 'url' ||
|
||||
language === 'pairs'
|
||||
language === "text" ||
|
||||
language === "url" ||
|
||||
language === "pairs"
|
||||
) {
|
||||
disableTabIndent = true;
|
||||
}
|
||||
|
||||
if (format == null && !readOnly) {
|
||||
format =
|
||||
language === 'json'
|
||||
language === "json"
|
||||
? tryFormatJson
|
||||
: language === 'xml' || language === 'html'
|
||||
: language === "xml" || language === "html"
|
||||
? tryFormatXml
|
||||
: undefined;
|
||||
}
|
||||
@@ -182,37 +182,37 @@ function EditorInner({
|
||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handleChange = useRef<EditorProps['onChange']>(onChange);
|
||||
const handleChange = useRef<EditorProps["onChange"]>(onChange);
|
||||
useEffect(() => {
|
||||
handleChange.current = onChange;
|
||||
}, [onChange]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handlePaste = useRef<EditorProps['onPaste']>(onPaste);
|
||||
const handlePaste = useRef<EditorProps["onPaste"]>(onPaste);
|
||||
useEffect(() => {
|
||||
handlePaste.current = onPaste;
|
||||
}, [onPaste]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handlePasteOverwrite = useRef<EditorProps['onPasteOverwrite']>(onPasteOverwrite);
|
||||
const handlePasteOverwrite = useRef<EditorProps["onPasteOverwrite"]>(onPasteOverwrite);
|
||||
useEffect(() => {
|
||||
handlePasteOverwrite.current = onPasteOverwrite;
|
||||
}, [onPasteOverwrite]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handleFocus = useRef<EditorProps['onFocus']>(onFocus);
|
||||
const handleFocus = useRef<EditorProps["onFocus"]>(onFocus);
|
||||
useEffect(() => {
|
||||
handleFocus.current = onFocus;
|
||||
}, [onFocus]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handleBlur = useRef<EditorProps['onBlur']>(onBlur);
|
||||
const handleBlur = useRef<EditorProps["onBlur"]>(onBlur);
|
||||
useEffect(() => {
|
||||
handleBlur.current = onBlur;
|
||||
}, [onBlur]);
|
||||
|
||||
// Use ref so we can update the handler without re-initializing the editor
|
||||
const handleKeyDown = useRef<EditorProps['onKeyDown']>(onKeyDown);
|
||||
const handleKeyDown = useRef<EditorProps["onKeyDown"]>(onKeyDown);
|
||||
useEffect(() => {
|
||||
handleKeyDown.current = onKeyDown;
|
||||
}, [onKeyDown]);
|
||||
@@ -236,10 +236,10 @@ function EditorInner({
|
||||
if (cm.current === null) return;
|
||||
const current = keymapCompartment.current.get(cm.current.view.state) ?? [];
|
||||
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
|
||||
if (settings.editorKeymap === 'default' && current === keymapExtensions.default) return; // Nothing to do
|
||||
if (settings.editorKeymap === 'vim' && current === keymapExtensions.vim) return; // Nothing to do
|
||||
if (settings.editorKeymap === 'vscode' && current === keymapExtensions.vscode) return; // Nothing to do
|
||||
if (settings.editorKeymap === 'emacs' && current === keymapExtensions.emacs) return; // Nothing to do
|
||||
if (settings.editorKeymap === "default" && current === keymapExtensions.default) return; // Nothing to do
|
||||
if (settings.editorKeymap === "vim" && current === keymapExtensions.vim) return; // Nothing to do
|
||||
if (settings.editorKeymap === "vscode" && current === keymapExtensions.vscode) return; // Nothing to do
|
||||
if (settings.editorKeymap === "emacs" && current === keymapExtensions.emacs) return; // Nothing to do
|
||||
|
||||
const ext = keymapExtensions[settings.editorKeymap] ?? keymapExtensions.default;
|
||||
const effects = keymapCompartment.current.reconfigure(ext);
|
||||
@@ -289,7 +289,7 @@ function EditorInner({
|
||||
TemplateFunctionDialog.show(fn, tagValue, startPos, cm.current.view);
|
||||
};
|
||||
|
||||
if (fn.name === 'secure') {
|
||||
if (fn.name === "secure") {
|
||||
withEncryptionEnabled(show);
|
||||
} else {
|
||||
show();
|
||||
@@ -309,7 +309,7 @@ function EditorInner({
|
||||
const onClickMissingVariable = useCallback(async (name: string) => {
|
||||
const activeEnvironment = jotaiStore.get(activeEnvironmentAtom);
|
||||
await editEnvironment(activeEnvironment, {
|
||||
addOrFocusVariable: { name, value: '', enabled: true },
|
||||
addOrFocusVariable: { name, value: "", enabled: true },
|
||||
});
|
||||
}, []);
|
||||
|
||||
@@ -414,9 +414,9 @@ function EditorInner({
|
||||
: []),
|
||||
];
|
||||
|
||||
const cachedJsonState = getCachedEditorState(defaultValue ?? '', stateKey);
|
||||
const cachedJsonState = getCachedEditorState(defaultValue ?? "", stateKey);
|
||||
|
||||
const doc = `${defaultValue ?? ''}`;
|
||||
const doc = `${defaultValue ?? ""}`;
|
||||
const config: EditorStateConfig = { extensions, doc };
|
||||
|
||||
const state = cachedJsonState
|
||||
@@ -439,7 +439,7 @@ function EditorInner({
|
||||
}
|
||||
setRef?.(view);
|
||||
} catch (e) {
|
||||
console.log('Failed to initialize Codemirror', e);
|
||||
console.log("Failed to initialize Codemirror", e);
|
||||
}
|
||||
},
|
||||
[forceUpdateKey],
|
||||
@@ -449,7 +449,7 @@ function EditorInner({
|
||||
useEffect(
|
||||
function updateReadOnlyEditor() {
|
||||
if (readOnly && cm.current?.view != null) {
|
||||
updateContents(cm.current.view, defaultValue || '');
|
||||
updateContents(cm.current.view, defaultValue || "");
|
||||
}
|
||||
},
|
||||
[defaultValue, readOnly],
|
||||
@@ -460,7 +460,7 @@ function EditorInner({
|
||||
function updateNonFocusedEditor() {
|
||||
const notFocused = !cm.current?.view.hasFocus;
|
||||
if (notFocused && cm.current != null) {
|
||||
updateContents(cm.current.view, defaultValue || '');
|
||||
updateContents(cm.current.view, defaultValue || "");
|
||||
}
|
||||
},
|
||||
[defaultValue],
|
||||
@@ -470,7 +470,7 @@ function EditorInner({
|
||||
const decoratedActions = useMemo(() => {
|
||||
const results = [];
|
||||
const actionClassName = classNames(
|
||||
'bg-surface transition-opacity transform-gpu opacity-0 group-hover:opacity-100 hover:!opacity-100 shadow',
|
||||
"bg-surface transition-opacity transform-gpu opacity-0 group-hover:opacity-100 hover:!opacity-100 shadow",
|
||||
);
|
||||
|
||||
if (format) {
|
||||
@@ -517,12 +517,12 @@ function EditorInner({
|
||||
ref={initEditorRef}
|
||||
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',
|
||||
readOnly && 'cm-readonly',
|
||||
"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",
|
||||
readOnly && "cm-readonly",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -539,8 +539,8 @@ function EditorInner({
|
||||
space={1}
|
||||
justifyContent="end"
|
||||
className={classNames(
|
||||
'absolute bottom-2 left-0 right-0',
|
||||
'pointer-events-none', // No pointer events, so we don't block the editor
|
||||
"absolute bottom-2 left-0 right-0",
|
||||
"pointer-events-none", // No pointer events, so we don't block the editor
|
||||
)}
|
||||
>
|
||||
{decoratedActions}
|
||||
@@ -562,20 +562,20 @@ function getExtensions({
|
||||
onFocus,
|
||||
onBlur,
|
||||
onKeyDown,
|
||||
}: Pick<EditorProps, 'singleLine' | 'readOnly' | 'hideGutter'> & {
|
||||
stateKey: EditorProps['stateKey'];
|
||||
}: Pick<EditorProps, "singleLine" | "readOnly" | "hideGutter"> & {
|
||||
stateKey: EditorProps["stateKey"];
|
||||
container: HTMLDivElement | null;
|
||||
onChange: RefObject<EditorProps['onChange']>;
|
||||
onPaste: RefObject<EditorProps['onPaste']>;
|
||||
onPasteOverwrite: RefObject<EditorProps['onPasteOverwrite']>;
|
||||
onFocus: RefObject<EditorProps['onFocus']>;
|
||||
onBlur: RefObject<EditorProps['onBlur']>;
|
||||
onKeyDown: RefObject<EditorProps['onKeyDown']>;
|
||||
onChange: RefObject<EditorProps["onChange"]>;
|
||||
onPaste: RefObject<EditorProps["onPaste"]>;
|
||||
onPasteOverwrite: RefObject<EditorProps["onPasteOverwrite"]>;
|
||||
onFocus: RefObject<EditorProps["onFocus"]>;
|
||||
onBlur: RefObject<EditorProps["onBlur"]>;
|
||||
onKeyDown: RefObject<EditorProps["onKeyDown"]>;
|
||||
}) {
|
||||
// TODO: Ensure tooltips render inside the dialog if we are in one.
|
||||
const parent =
|
||||
container?.closest<HTMLDivElement>('[role="dialog"]') ??
|
||||
document.querySelector<HTMLDivElement>('#cm-portal') ??
|
||||
document.querySelector<HTMLDivElement>("#cm-portal") ??
|
||||
undefined;
|
||||
|
||||
return [
|
||||
@@ -589,7 +589,7 @@ function getExtensions({
|
||||
},
|
||||
keydown: (e, view) => {
|
||||
// Check if the hotkey matches the editor.autocomplete action
|
||||
if (eventMatchesHotkey(e, 'editor.autocomplete')) {
|
||||
if (eventMatchesHotkey(e, "editor.autocomplete")) {
|
||||
e.preventDefault();
|
||||
startCompletion(view);
|
||||
return true;
|
||||
@@ -597,7 +597,7 @@ function getExtensions({
|
||||
onKeyDown.current?.(e);
|
||||
},
|
||||
paste: (e, v) => {
|
||||
const textData = e.clipboardData?.getData('text/plain') ?? '';
|
||||
const textData = e.clipboardData?.getData("text/plain") ?? "";
|
||||
onPaste.current?.(textData);
|
||||
if (v.state.selection.main.from === 0 && v.state.selection.main.to === v.state.doc.length) {
|
||||
onPasteOverwrite.current?.(e, textData);
|
||||
@@ -605,7 +605,7 @@ function getExtensions({
|
||||
},
|
||||
}),
|
||||
tooltips({ parent }),
|
||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
|
||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== "Enter") : defaultKeymap),
|
||||
...(singleLine ? [singleLineExtensions()] : []),
|
||||
...(!singleLine ? multiLineExtensions({ hideGutter }) : []),
|
||||
...(readOnly ? readonlyExtensions : []),
|
||||
@@ -627,10 +627,10 @@ function getExtensions({
|
||||
}
|
||||
|
||||
const placeholderElFromText = (text: string | undefined) => {
|
||||
const el = document.createElement('div');
|
||||
const el = document.createElement("div");
|
||||
// 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/>') : ' ';
|
||||
el.innerHTML = text ? text.replaceAll("\n", "<br/>") : " ";
|
||||
return el;
|
||||
};
|
||||
|
||||
@@ -646,7 +646,7 @@ function saveCachedEditorState(stateKey: string | null, state: EditorState | nul
|
||||
try {
|
||||
sessionStorage.setItem(computeFullStateKey(stateKey), JSON.stringify(stateObj));
|
||||
} catch (err) {
|
||||
console.log('Failed to save to editor state', stateKey, err);
|
||||
console.log("Failed to save to editor state", stateKey, err);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,7 +667,7 @@ function getCachedEditorState(doc: string, stateKey: string | null) {
|
||||
state.doc = doc;
|
||||
return state;
|
||||
} catch (err) {
|
||||
console.log('Failed to restore editor storage', stateKey, err);
|
||||
console.log("Failed to restore editor storage", stateKey, err);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { lazy, Suspense } from 'react';
|
||||
import type { EditorProps } from './Editor';
|
||||
import { lazy, Suspense } from "react";
|
||||
import type { EditorProps } from "./Editor";
|
||||
|
||||
const Editor_ = lazy(() => import('./Editor').then((m) => ({ default: m.Editor })));
|
||||
const Editor_ = lazy(() => import("./Editor").then((m) => ({ default: m.Editor })));
|
||||
|
||||
export function Editor(props: EditorProps) {
|
||||
return (
|
||||
|
||||
@@ -3,15 +3,15 @@ import {
|
||||
closeBrackets,
|
||||
closeBracketsKeymap,
|
||||
completionKeymap,
|
||||
} from '@codemirror/autocomplete';
|
||||
import { history, historyKeymap } from '@codemirror/commands';
|
||||
import { go } from '@codemirror/lang-go';
|
||||
import { java } from '@codemirror/lang-java';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { php } from '@codemirror/lang-php';
|
||||
import { python } from '@codemirror/lang-python';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
} from "@codemirror/autocomplete";
|
||||
import { history, historyKeymap } from "@codemirror/commands";
|
||||
import { go } from "@codemirror/lang-go";
|
||||
import { java } from "@codemirror/lang-java";
|
||||
import { javascript } from "@codemirror/lang-javascript";
|
||||
import { markdown } from "@codemirror/lang-markdown";
|
||||
import { php } from "@codemirror/lang-php";
|
||||
import { python } from "@codemirror/lang-python";
|
||||
import { xml } from "@codemirror/lang-xml";
|
||||
import {
|
||||
bracketMatching,
|
||||
codeFolding,
|
||||
@@ -22,20 +22,20 @@ import {
|
||||
LanguageSupport,
|
||||
StreamLanguage,
|
||||
syntaxHighlighting,
|
||||
} from '@codemirror/language';
|
||||
import { c, csharp, kotlin, objectiveC } from '@codemirror/legacy-modes/mode/clike';
|
||||
import { clojure } from '@codemirror/legacy-modes/mode/clojure';
|
||||
import { http } from '@codemirror/legacy-modes/mode/http';
|
||||
import { oCaml } from '@codemirror/legacy-modes/mode/mllike';
|
||||
import { powerShell } from '@codemirror/legacy-modes/mode/powershell';
|
||||
import { r } from '@codemirror/legacy-modes/mode/r';
|
||||
import { ruby } from '@codemirror/legacy-modes/mode/ruby';
|
||||
import { shell } from '@codemirror/legacy-modes/mode/shell';
|
||||
import { swift } from '@codemirror/legacy-modes/mode/swift';
|
||||
import { linter, lintGutter, lintKeymap } from '@codemirror/lint';
|
||||
import { search, searchKeymap } from '@codemirror/search';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { EditorState } from '@codemirror/state';
|
||||
} from "@codemirror/language";
|
||||
import { c, csharp, kotlin, objectiveC } from "@codemirror/legacy-modes/mode/clike";
|
||||
import { clojure } from "@codemirror/legacy-modes/mode/clojure";
|
||||
import { http } from "@codemirror/legacy-modes/mode/http";
|
||||
import { oCaml } from "@codemirror/legacy-modes/mode/mllike";
|
||||
import { powerShell } from "@codemirror/legacy-modes/mode/powershell";
|
||||
import { r } from "@codemirror/legacy-modes/mode/r";
|
||||
import { ruby } from "@codemirror/legacy-modes/mode/ruby";
|
||||
import { shell } from "@codemirror/legacy-modes/mode/shell";
|
||||
import { swift } from "@codemirror/legacy-modes/mode/swift";
|
||||
import { linter, lintGutter, lintKeymap } from "@codemirror/lint";
|
||||
import { search, searchKeymap } from "@codemirror/search";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { EditorState } from "@codemirror/state";
|
||||
import {
|
||||
crosshairCursor,
|
||||
drawSelection,
|
||||
@@ -46,51 +46,51 @@ import {
|
||||
keymap,
|
||||
lineNumbers,
|
||||
rectangularSelection,
|
||||
} from '@codemirror/view';
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { jsonc, jsoncLanguage } from '@shopify/lang-jsonc';
|
||||
import { graphql } from 'cm6-graphql';
|
||||
import type { GraphQLSchema } from 'graphql';
|
||||
import { activeRequestIdAtom } from '../../../hooks/useActiveRequestId';
|
||||
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import { renderMarkdown } from '../../../lib/markdown';
|
||||
import { pluralizeCount } from '../../../lib/pluralize';
|
||||
import { showGraphQLDocExplorerAtom } from '../../graphql/graphqlAtoms';
|
||||
import type { EditorProps } from './Editor';
|
||||
import { jsonParseLinter } from './json-lint';
|
||||
import { pairs } from './pairs/extension';
|
||||
import { searchMatchCount } from './searchMatchCount';
|
||||
import { text } from './text/extension';
|
||||
import { timeline } from './timeline/extension';
|
||||
import type { TwigCompletionOption } from './twig/completion';
|
||||
import { twig } from './twig/extension';
|
||||
import { pathParametersPlugin } from './twig/pathParameters';
|
||||
import { url } from './url/extension';
|
||||
} from "@codemirror/view";
|
||||
import { tags as t } from "@lezer/highlight";
|
||||
import { jsonc, jsoncLanguage } from "@shopify/lang-jsonc";
|
||||
import { graphql } from "cm6-graphql";
|
||||
import type { GraphQLSchema } from "graphql";
|
||||
import { activeRequestIdAtom } from "../../../hooks/useActiveRequestId";
|
||||
import type { WrappedEnvironmentVariable } from "../../../hooks/useEnvironmentVariables";
|
||||
import { jotaiStore } from "../../../lib/jotai";
|
||||
import { renderMarkdown } from "../../../lib/markdown";
|
||||
import { pluralizeCount } from "../../../lib/pluralize";
|
||||
import { showGraphQLDocExplorerAtom } from "../../graphql/graphqlAtoms";
|
||||
import type { EditorProps } from "./Editor";
|
||||
import { jsonParseLinter } from "./json-lint";
|
||||
import { pairs } from "./pairs/extension";
|
||||
import { searchMatchCount } from "./searchMatchCount";
|
||||
import { text } from "./text/extension";
|
||||
import { timeline } from "./timeline/extension";
|
||||
import type { TwigCompletionOption } from "./twig/completion";
|
||||
import { twig } from "./twig/extension";
|
||||
import { pathParametersPlugin } from "./twig/pathParameters";
|
||||
import { url } from "./url/extension";
|
||||
|
||||
export const syntaxHighlightStyle = HighlightStyle.define([
|
||||
{
|
||||
tag: [t.documentMeta, t.blockComment, t.lineComment, t.docComment, t.comment],
|
||||
color: 'var(--textSubtlest)',
|
||||
color: "var(--textSubtlest)",
|
||||
},
|
||||
{
|
||||
tag: [t.emphasis],
|
||||
textDecoration: 'underline',
|
||||
textDecoration: "underline",
|
||||
},
|
||||
{
|
||||
tag: [t.angleBracket, t.paren, t.bracket, t.squareBracket, t.brace, t.separator, t.punctuation],
|
||||
color: 'var(--textSubtle)',
|
||||
color: "var(--textSubtle)",
|
||||
},
|
||||
{
|
||||
tag: [t.link, t.name, t.tagName, t.angleBracket, t.docString, t.number],
|
||||
color: 'var(--info)',
|
||||
color: "var(--info)",
|
||||
},
|
||||
{ tag: [t.variableName], color: 'var(--success)' },
|
||||
{ tag: [t.bool], color: 'var(--warning)' },
|
||||
{ tag: [t.attributeName, t.propertyName], color: 'var(--primary)' },
|
||||
{ tag: [t.attributeValue], color: 'var(--warning)' },
|
||||
{ tag: [t.string], color: 'var(--notice)' },
|
||||
{ tag: [t.atom, t.meta, t.operator, t.bool, t.null, t.keyword], color: 'var(--danger)' },
|
||||
{ tag: [t.variableName], color: "var(--success)" },
|
||||
{ tag: [t.bool], color: "var(--warning)" },
|
||||
{ tag: [t.attributeName, t.propertyName], color: "var(--primary)" },
|
||||
{ tag: [t.attributeValue], color: "var(--warning)" },
|
||||
{ tag: [t.string], color: "var(--notice)" },
|
||||
{ tag: [t.atom, t.meta, t.operator, t.bool, t.null, t.keyword], color: "var(--danger)" },
|
||||
]);
|
||||
|
||||
const syntaxTheme = EditorView.theme({}, { dark: true });
|
||||
@@ -102,7 +102,7 @@ const legacyLang = (mode: Parameters<typeof StreamLanguage.define>[0]) => {
|
||||
};
|
||||
|
||||
const syntaxExtensions: Record<
|
||||
NonNullable<EditorProps['language']>,
|
||||
NonNullable<EditorProps["language"]>,
|
||||
null | (() => LanguageSupport)
|
||||
> = {
|
||||
graphql: null,
|
||||
@@ -134,11 +134,11 @@ const syntaxExtensions: Record<
|
||||
swift: legacyLang(swift),
|
||||
};
|
||||
|
||||
const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ['json', 'javascript', 'graphql'];
|
||||
const closeBracketsFor: (keyof typeof syntaxExtensions)[] = ["json", "javascript", "graphql"];
|
||||
|
||||
export function getLanguageExtension({
|
||||
useTemplating,
|
||||
language = 'text',
|
||||
language = "text",
|
||||
lintExtension,
|
||||
environmentVariables,
|
||||
autocomplete,
|
||||
@@ -156,10 +156,10 @@ export function getLanguageExtension({
|
||||
onClickPathParameter: (name: string) => void;
|
||||
completionOptions: TwigCompletionOption[];
|
||||
graphQLSchema: GraphQLSchema | null;
|
||||
} & Pick<EditorProps, 'language' | 'autocomplete' | 'hideGutter' | 'lintExtension'>) {
|
||||
} & Pick<EditorProps, "language" | "autocomplete" | "hideGutter" | "lintExtension">) {
|
||||
const extraExtensions: Extension[] = [];
|
||||
|
||||
if (language === 'url') {
|
||||
if (language === "url") {
|
||||
extraExtensions.push(pathParametersPlugin(onClickPathParameter));
|
||||
}
|
||||
|
||||
@@ -169,13 +169,13 @@ export function getLanguageExtension({
|
||||
}
|
||||
|
||||
// GraphQL is a special exception
|
||||
if (language === 'graphql') {
|
||||
if (language === "graphql") {
|
||||
return [
|
||||
graphql(graphQLSchema ?? undefined, {
|
||||
async onCompletionInfoRender(gqlCompletionItem): Promise<Node | null> {
|
||||
if (!gqlCompletionItem.documentation) return null;
|
||||
const innerHTML = await renderMarkdown(gqlCompletionItem.documentation);
|
||||
const span = document.createElement('span');
|
||||
const span = document.createElement("span");
|
||||
span.innerHTML = innerHTML;
|
||||
return span;
|
||||
},
|
||||
@@ -192,11 +192,11 @@ export function getLanguageExtension({
|
||||
];
|
||||
}
|
||||
|
||||
if (language === 'json') {
|
||||
if (language === "json") {
|
||||
extraExtensions.push(lintExtension ?? linter(jsonParseLinter()));
|
||||
extraExtensions.push(
|
||||
jsoncLanguage.data.of({
|
||||
commentTokens: { line: '//', block: { open: '/*', close: '*/' } },
|
||||
commentTokens: { line: "//", block: { open: "/*", close: "*/" } },
|
||||
}),
|
||||
);
|
||||
if (!hideGutter) {
|
||||
@@ -205,7 +205,7 @@ export function getLanguageExtension({
|
||||
}
|
||||
|
||||
const maybeBase = language ? syntaxExtensions[language] : null;
|
||||
const base = typeof maybeBase === 'function' ? maybeBase() : null;
|
||||
const base = typeof maybeBase === "function" ? maybeBase() : null;
|
||||
if (base == null) {
|
||||
return [];
|
||||
}
|
||||
@@ -229,10 +229,10 @@ export function getLanguageExtension({
|
||||
// Filter out autocomplete start triggers from completionKeymap since we handle it via configurable hotkeys.
|
||||
// Keep navigation keys (ArrowUp/Down, Enter, Escape, etc.) but remove startCompletion bindings.
|
||||
const filteredCompletionKeymap = completionKeymap.filter((binding) => {
|
||||
const key = binding.key?.toLowerCase() ?? '';
|
||||
const mac = (binding as { mac?: string }).mac?.toLowerCase() ?? '';
|
||||
const key = binding.key?.toLowerCase() ?? "";
|
||||
const mac = (binding as { mac?: string }).mac?.toLowerCase() ?? "";
|
||||
// Filter out Ctrl-Space and Mac-specific autocomplete triggers (Alt-`, Alt-i)
|
||||
const isStartTrigger = key.includes('space') || mac.includes('alt-') || mac.includes('`');
|
||||
const isStartTrigger = key.includes("space") || mac.includes("alt-") || mac.includes("`");
|
||||
return !isStartTrigger;
|
||||
});
|
||||
|
||||
@@ -242,7 +242,7 @@ export const baseExtensions = [
|
||||
dropCursor(),
|
||||
drawSelection(),
|
||||
autocompletion({
|
||||
tooltipClass: () => 'x-theme-menu',
|
||||
tooltipClass: () => "x-theme-menu",
|
||||
closeOnBlur: true, // Set to `false` for debugging in devtools without closing it
|
||||
defaultKeymap: false, // We handle the trigger via configurable hotkeys
|
||||
compareCompletions: (a, b) => {
|
||||
@@ -257,7 +257,7 @@ export const baseExtensions = [
|
||||
|
||||
export const readonlyExtensions = [
|
||||
EditorState.readOnly.of(true),
|
||||
EditorView.contentAttributes.of({ tabindex: '-1' }),
|
||||
EditorView.contentAttributes.of({ tabindex: "-1" }),
|
||||
];
|
||||
|
||||
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
|
||||
@@ -269,11 +269,11 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
lineNumbers(),
|
||||
foldGutter({
|
||||
markerDOM: (open) => {
|
||||
const el = document.createElement('div');
|
||||
el.classList.add('fold-gutter-icon');
|
||||
const el = document.createElement("div");
|
||||
el.classList.add("fold-gutter-icon");
|
||||
el.tabIndex = -1;
|
||||
if (open) {
|
||||
el.setAttribute('data-open', '');
|
||||
el.setAttribute("data-open", "");
|
||||
}
|
||||
return el;
|
||||
},
|
||||
@@ -281,12 +281,12 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
],
|
||||
codeFolding({
|
||||
placeholderDOM(_view, onclick, prepared) {
|
||||
const el = document.createElement('span');
|
||||
const el = document.createElement("span");
|
||||
el.onclick = onclick;
|
||||
el.className = 'cm-foldPlaceholder';
|
||||
el.innerText = prepared || '…';
|
||||
el.title = 'unfold';
|
||||
el.ariaLabel = 'folded code';
|
||||
el.className = "cm-foldPlaceholder";
|
||||
el.innerText = prepared || "…";
|
||||
el.title = "unfold";
|
||||
el.ariaLabel = "folded code";
|
||||
return el;
|
||||
},
|
||||
/**
|
||||
@@ -295,15 +295,15 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
*/
|
||||
preparePlaceholder(state, range) {
|
||||
let count: number | undefined;
|
||||
let startToken = '{';
|
||||
let endToken = '}';
|
||||
let startToken = "{";
|
||||
let endToken = "}";
|
||||
|
||||
const prevLine = state.doc.lineAt(range.from).text;
|
||||
const isArray = prevLine.lastIndexOf('[') > prevLine.lastIndexOf('{');
|
||||
const isArray = prevLine.lastIndexOf("[") > prevLine.lastIndexOf("{");
|
||||
|
||||
if (isArray) {
|
||||
startToken = '[';
|
||||
endToken = ']';
|
||||
startToken = "[";
|
||||
endToken = "]";
|
||||
}
|
||||
|
||||
const internal = state.sliceDoc(range.from, range.to);
|
||||
@@ -317,7 +317,7 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
}
|
||||
|
||||
if (count !== undefined) {
|
||||
const label = isArray ? 'item' : 'key';
|
||||
const label = isArray ? "item" : "key";
|
||||
return pluralizeCount(label, count);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
|
||||
import { autocompletion, startCompletion } from '@codemirror/autocomplete';
|
||||
import { LanguageSupport, LRLanguage, syntaxTree } from '@codemirror/language';
|
||||
import type { SyntaxNode } from '@lezer/common';
|
||||
import { parser } from './filter';
|
||||
import type { Completion, CompletionContext, CompletionResult } from "@codemirror/autocomplete";
|
||||
import { autocompletion, startCompletion } from "@codemirror/autocomplete";
|
||||
import { LanguageSupport, LRLanguage, syntaxTree } from "@codemirror/language";
|
||||
import type { SyntaxNode } from "@lezer/common";
|
||||
import { parser } from "./filter";
|
||||
|
||||
export interface FieldDef {
|
||||
name: string;
|
||||
@@ -43,7 +43,7 @@ function inPhrase(ctx: CompletionContext): boolean {
|
||||
// Lezer node names from your grammar: Phrase is the quoted token
|
||||
let n: SyntaxNode | null = syntaxTree(ctx.state).resolveInner(ctx.pos, -1);
|
||||
while (n) {
|
||||
if (n.name === 'Phrase') return true;
|
||||
if (n.name === "Phrase") return true;
|
||||
n = n.parent;
|
||||
}
|
||||
return false;
|
||||
@@ -53,7 +53,7 @@ function inPhrase(ctx: CompletionContext): boolean {
|
||||
function inUnclosedQuote(doc: string, pos: number): boolean {
|
||||
let quotes = 0;
|
||||
for (let i = 0; i < pos; i++) {
|
||||
if (doc[i] === '"' && doc[i - 1] !== '\\') quotes++;
|
||||
if (doc[i] === '"' && doc[i - 1] !== "\\") quotes++;
|
||||
}
|
||||
return quotes % 2 === 1; // odd = inside an open quote
|
||||
}
|
||||
@@ -64,13 +64,13 @@ function inUnclosedQuote(doc: string, pos: number): boolean {
|
||||
* - Otherwise, we're in a field name or bare term position.
|
||||
*/
|
||||
function contextInfo(stateDoc: string, pos: number) {
|
||||
const lastColon = stateDoc.lastIndexOf(':', pos - 1);
|
||||
const lastColon = stateDoc.lastIndexOf(":", pos - 1);
|
||||
const lastBoundary = Math.max(
|
||||
stateDoc.lastIndexOf(' ', pos - 1),
|
||||
stateDoc.lastIndexOf('\t', pos - 1),
|
||||
stateDoc.lastIndexOf('\n', pos - 1),
|
||||
stateDoc.lastIndexOf('(', pos - 1),
|
||||
stateDoc.lastIndexOf(')', pos - 1),
|
||||
stateDoc.lastIndexOf(" ", pos - 1),
|
||||
stateDoc.lastIndexOf("\t", pos - 1),
|
||||
stateDoc.lastIndexOf("\n", pos - 1),
|
||||
stateDoc.lastIndexOf("(", pos - 1),
|
||||
stateDoc.lastIndexOf(")", pos - 1),
|
||||
);
|
||||
|
||||
const inValue = lastColon > lastBoundary;
|
||||
@@ -96,7 +96,7 @@ function contextInfo(stateDoc: string, pos: number) {
|
||||
function fieldNameCompletions(fieldNames: string[]): Completion[] {
|
||||
return fieldNames.map((name) => ({
|
||||
label: name,
|
||||
type: 'property',
|
||||
type: "property",
|
||||
apply: (view, _completion, from, to) => {
|
||||
// Insert "name:" (leave cursor right after colon)
|
||||
view.dispatch({
|
||||
@@ -117,7 +117,7 @@ function fieldValueCompletions(
|
||||
return vals.map((v) => ({
|
||||
label: v.match(IDENT_ONLY) ? v : `"${v}"`,
|
||||
displayLabel: v,
|
||||
type: 'constant',
|
||||
type: "constant",
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ function makeCompletionSource(opts: FilterOptions) {
|
||||
}
|
||||
|
||||
const language = LRLanguage.define({
|
||||
name: 'filter',
|
||||
name: "filter",
|
||||
parser,
|
||||
languageData: {
|
||||
autocompletion: {},
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
/* oxlint-disable */
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from '@lezer/lr';
|
||||
import { highlight } from './highlight';
|
||||
import { LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states:
|
||||
"%QOVQPOOPeOPOOOVQPO'#CfOjQPO'#ChO!XQPO'#CgOOQO'#Cc'#CcOVQPO'#CaOOQO'#Ca'#CaO!oQPO'#C`O!|QPO'#C_OOQO'#C^'#C^QOQPOOPOOO'#Cp'#CpP#XOPO)C>jO#`QPO,59QO#eQPO,59ROOQO,58{,58{OVQPO'#CqOOQO'#Cq'#CqO#mQPO,58zOVQPO'#CrO#zQPO,58yPOOO-E6n-E6nOOQO1G.l1G.lOOQO'#Cm'#CmOOQO'#Ck'#CkOOQO1G.m1G.mOOQO,59],59]OOQO-E6o-E6oOOQO,59^,59^OOQO-E6p-E6p",
|
||||
stateData:
|
||||
'$]~OiPQ~OUUOXQO]RO`TO~Oi[O~OUaXXaX]aX^[X`aXbaXcaXgaXWaX~O^_O~OUUOXQO]RO`TObaO~OcSXgSXWSX~P!^OcdOgRXWRX~Oi[O~Qh]WgO~O]hO`iO~OcSagSaWSa~P!^OcdOgRaWRa~OUbc]c~',
|
||||
goto: '#hgPPhnryP!YPP!c!c!lPP!uP!xPP#U#[#bQZOR^QTYOQSXOQRmdUWOQdQ`USbWcRka_VOQUWacd_TOQUWacd_SOQUWacdRj_^TOQUWacdRi_Q]PRf]QcWRlcQeXRne',
|
||||
"$]~OiPQ~OUUOXQO]RO`TO~Oi[O~OUaXXaX]aX^[X`aXbaXcaXgaXWaX~O^_O~OUUOXQO]RO`TObaO~OcSXgSXWSX~P!^OcdOgRXWRX~Oi[O~Qh]WgO~O]hO`iO~OcSagSaWSa~P!^OcdOgRaWRa~OUbc]c~",
|
||||
goto: "#hgPPhnryP!YPP!c!c!lPP!uP!xPP#U#[#bQZOR^QTYOQSXOQRmdUWOQdQ`USbWcRka_VOQUWacd_TOQUWacd_SOQUWacdRj_^TOQUWacdRi_Q]PRf]QcWRlcQeXRne",
|
||||
nodeNames:
|
||||
'⚠ Query Expr OrExpr AndExpr Unary Not Primary RParen LParen Group Field FieldName Word Colon FieldValue Phrase Term And Or',
|
||||
"⚠ Query Expr OrExpr AndExpr Unary Not Primary RParen LParen Group Field FieldName Word Colon FieldValue Phrase Term And Or",
|
||||
maxTerm: 25,
|
||||
nodeProps: [
|
||||
['openedBy', 8, 'LParen'],
|
||||
['closedBy', 9, 'RParen'],
|
||||
["openedBy", 8, "LParen"],
|
||||
["closedBy", 9, "RParen"],
|
||||
],
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0, 20],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const highlight = styleTags({
|
||||
// Boolean operators
|
||||
@@ -16,6 +16,6 @@ export const highlight = styleTags({
|
||||
Phrase: t.string, // "quoted string"
|
||||
|
||||
// Fields
|
||||
'FieldName/Word': t.attributeName,
|
||||
'FieldValue/Term/Word': t.attributeValue,
|
||||
"FieldName/Word": t.attributeName,
|
||||
"FieldValue/Term/Word": t.attributeValue,
|
||||
});
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
// query.ts
|
||||
// A tiny query language parser with NOT/AND/OR, parentheses, phrases, negation, and field:value.
|
||||
|
||||
import { fuzzyMatch } from 'fuzzbunny';
|
||||
import { fuzzyMatch } from "fuzzbunny";
|
||||
/////////////////////////
|
||||
// AST
|
||||
/////////////////////////
|
||||
|
||||
export type Ast =
|
||||
| { type: 'Term'; value: string } // foo
|
||||
| { type: 'Phrase'; value: string } // "hi there"
|
||||
| { type: 'Field'; field: string; value: string } // method:POST or title:"exact phrase"
|
||||
| { type: 'Not'; node: Ast } // -foo or NOT foo
|
||||
| { type: 'And'; left: Ast; right: Ast } // a AND b
|
||||
| { type: 'Or'; left: Ast; right: Ast }; // a OR b
|
||||
| { type: "Term"; value: string } // foo
|
||||
| { type: "Phrase"; value: string } // "hi there"
|
||||
| { type: "Field"; field: string; value: string } // method:POST or title:"exact phrase"
|
||||
| { type: "Not"; node: Ast } // -foo or NOT foo
|
||||
| { type: "And"; left: Ast; right: Ast } // a AND b
|
||||
| { type: "Or"; left: Ast; right: Ast }; // a OR b
|
||||
|
||||
/////////////////////////
|
||||
// Tokenizer
|
||||
/////////////////////////
|
||||
type Tok =
|
||||
| { kind: 'LPAREN' }
|
||||
| { kind: 'RPAREN' }
|
||||
| { kind: 'AND' }
|
||||
| { kind: 'OR' }
|
||||
| { kind: 'NOT' } // explicit NOT
|
||||
| { kind: 'MINUS' } // unary minus before term/phrase/paren group
|
||||
| { kind: 'COLON' }
|
||||
| { kind: 'WORD'; text: string } // bareword (unquoted)
|
||||
| { kind: 'PHRASE'; text: string } // "quoted phrase"
|
||||
| { kind: 'EOF' };
|
||||
| { kind: "LPAREN" }
|
||||
| { kind: "RPAREN" }
|
||||
| { kind: "AND" }
|
||||
| { kind: "OR" }
|
||||
| { kind: "NOT" } // explicit NOT
|
||||
| { kind: "MINUS" } // unary minus before term/phrase/paren group
|
||||
| { kind: "COLON" }
|
||||
| { kind: "WORD"; text: string } // bareword (unquoted)
|
||||
| { kind: "PHRASE"; text: string } // "quoted phrase"
|
||||
| { kind: "EOF" };
|
||||
|
||||
const isSpace = (c: string) => /\s/.test(c);
|
||||
const isIdent = (c: string) => /[A-Za-z0-9_\-./]/.test(c);
|
||||
@@ -37,11 +37,11 @@ export function tokenize(input: string): Tok[] {
|
||||
let i = 0;
|
||||
const n = input.length;
|
||||
|
||||
const peek = () => input[i] ?? '';
|
||||
const peek = () => input[i] ?? "";
|
||||
const advance = () => input[i++];
|
||||
|
||||
const readWord = () => {
|
||||
let s = '';
|
||||
let s = "";
|
||||
while (i < n && isIdent(peek())) s += advance();
|
||||
return s;
|
||||
};
|
||||
@@ -49,11 +49,11 @@ export function tokenize(input: string): Tok[] {
|
||||
const readPhrase = () => {
|
||||
// assumes current char is opening quote
|
||||
advance(); // consume opening "
|
||||
let s = '';
|
||||
let s = "";
|
||||
while (i < n) {
|
||||
const c = advance();
|
||||
if (c === `"`) break;
|
||||
if (c === '\\' && i < n) {
|
||||
if (c === "\\" && i < n) {
|
||||
// escape \" and \\ (simple)
|
||||
const next = advance();
|
||||
s += next;
|
||||
@@ -72,28 +72,28 @@ export function tokenize(input: string): Tok[] {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c === '(') {
|
||||
toks.push({ kind: 'LPAREN' });
|
||||
if (c === "(") {
|
||||
toks.push({ kind: "LPAREN" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === ')') {
|
||||
toks.push({ kind: 'RPAREN' });
|
||||
if (c === ")") {
|
||||
toks.push({ kind: "RPAREN" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === ':') {
|
||||
toks.push({ kind: 'COLON' });
|
||||
if (c === ":") {
|
||||
toks.push({ kind: "COLON" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
if (c === `"`) {
|
||||
const text = readPhrase();
|
||||
toks.push({ kind: 'PHRASE', text });
|
||||
toks.push({ kind: "PHRASE", text });
|
||||
continue;
|
||||
}
|
||||
if (c === '-') {
|
||||
toks.push({ kind: 'MINUS' });
|
||||
if (c === "-") {
|
||||
toks.push({ kind: "MINUS" });
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
@@ -102,10 +102,10 @@ export function tokenize(input: string): Tok[] {
|
||||
if (isIdent(c)) {
|
||||
const w = readWord();
|
||||
const upper = w.toUpperCase();
|
||||
if (upper === 'AND') toks.push({ kind: 'AND' });
|
||||
else if (upper === 'OR') toks.push({ kind: 'OR' });
|
||||
else if (upper === 'NOT') toks.push({ kind: 'NOT' });
|
||||
else toks.push({ kind: 'WORD', text: w });
|
||||
if (upper === "AND") toks.push({ kind: "AND" });
|
||||
else if (upper === "OR") toks.push({ kind: "OR" });
|
||||
else if (upper === "NOT") toks.push({ kind: "NOT" });
|
||||
else toks.push({ kind: "WORD", text: w });
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export function tokenize(input: string): Tok[] {
|
||||
i++;
|
||||
}
|
||||
|
||||
toks.push({ kind: 'EOF' });
|
||||
toks.push({ kind: "EOF" });
|
||||
return toks;
|
||||
}
|
||||
|
||||
@@ -122,20 +122,20 @@ class Parser {
|
||||
constructor(private toks: Tok[]) {}
|
||||
|
||||
private peek(): Tok {
|
||||
return this.toks[this.i] ?? { kind: 'EOF' };
|
||||
return this.toks[this.i] ?? { kind: "EOF" };
|
||||
}
|
||||
private advance(): Tok {
|
||||
return this.toks[this.i++] ?? { kind: 'EOF' };
|
||||
return this.toks[this.i++] ?? { kind: "EOF" };
|
||||
}
|
||||
private at(kind: Tok['kind']) {
|
||||
private at(kind: Tok["kind"]) {
|
||||
return this.peek().kind === kind;
|
||||
}
|
||||
|
||||
// Top-level: parse OR-precedence chain, allowing implicit AND.
|
||||
parse(): Ast | null {
|
||||
if (this.at('EOF')) return null;
|
||||
if (this.at("EOF")) return null;
|
||||
const expr = this.parseOr();
|
||||
if (!this.at('EOF')) {
|
||||
if (!this.at("EOF")) {
|
||||
// Optionally, consume remaining tokens or throw
|
||||
}
|
||||
return expr;
|
||||
@@ -144,10 +144,10 @@ class Parser {
|
||||
// Precedence: NOT (highest), AND, OR (lowest)
|
||||
private parseOr(): Ast {
|
||||
let node = this.parseAnd();
|
||||
while (this.at('OR')) {
|
||||
while (this.at("OR")) {
|
||||
this.advance();
|
||||
const rhs = this.parseAnd();
|
||||
node = { type: 'Or', left: node, right: rhs };
|
||||
node = { type: "Or", left: node, right: rhs };
|
||||
}
|
||||
return node;
|
||||
}
|
||||
@@ -155,31 +155,31 @@ class Parser {
|
||||
private parseAnd(): Ast {
|
||||
let node = this.parseUnary();
|
||||
// Implicit AND: if next token starts a primary, treat as AND.
|
||||
while (this.at('AND') || this.startsPrimary()) {
|
||||
if (this.at('AND')) this.advance();
|
||||
while (this.at("AND") || this.startsPrimary()) {
|
||||
if (this.at("AND")) this.advance();
|
||||
const rhs = this.parseUnary();
|
||||
node = { type: 'And', left: node, right: rhs };
|
||||
node = { type: "And", left: node, right: rhs };
|
||||
}
|
||||
return node;
|
||||
}
|
||||
|
||||
private parseUnary(): Ast {
|
||||
if (this.at('NOT') || this.at('MINUS')) {
|
||||
if (this.at("NOT") || this.at("MINUS")) {
|
||||
this.advance();
|
||||
const node = this.parseUnary();
|
||||
return { type: 'Not', node };
|
||||
return { type: "Not", node };
|
||||
}
|
||||
return this.parsePrimaryOrField();
|
||||
}
|
||||
|
||||
private startsPrimary(): boolean {
|
||||
const k = this.peek().kind;
|
||||
return k === 'WORD' || k === 'PHRASE' || k === 'LPAREN' || k === 'MINUS' || k === 'NOT';
|
||||
return k === "WORD" || k === "PHRASE" || k === "LPAREN" || k === "MINUS" || k === "NOT";
|
||||
}
|
||||
|
||||
private parsePrimaryOrField(): Ast {
|
||||
// Parenthesized group
|
||||
if (this.at('LPAREN')) {
|
||||
if (this.at("LPAREN")) {
|
||||
this.advance();
|
||||
const inside = this.parseOr();
|
||||
// if (!this.at('RPAREN')) throw new Error("Missing closing ')'");
|
||||
@@ -188,59 +188,59 @@ class Parser {
|
||||
}
|
||||
|
||||
// Phrase
|
||||
if (this.at('PHRASE')) {
|
||||
const t = this.advance() as Extract<Tok, { kind: 'PHRASE' }>;
|
||||
return { type: 'Phrase', value: t.text };
|
||||
if (this.at("PHRASE")) {
|
||||
const t = this.advance() as Extract<Tok, { kind: "PHRASE" }>;
|
||||
return { type: "Phrase", value: t.text };
|
||||
}
|
||||
|
||||
// Field or bare word
|
||||
if (this.at('WORD')) {
|
||||
const wordTok = this.advance() as Extract<Tok, { kind: 'WORD' }>;
|
||||
if (this.at("WORD")) {
|
||||
const wordTok = this.advance() as Extract<Tok, { kind: "WORD" }>;
|
||||
|
||||
if (this.at('COLON')) {
|
||||
if (this.at("COLON")) {
|
||||
// field:value or field:"phrase"
|
||||
this.advance(); // :
|
||||
let value: string;
|
||||
if (this.at('PHRASE')) {
|
||||
const p = this.advance() as Extract<Tok, { kind: 'PHRASE' }>;
|
||||
if (this.at("PHRASE")) {
|
||||
const p = this.advance() as Extract<Tok, { kind: "PHRASE" }>;
|
||||
value = p.text;
|
||||
} else if (this.at('WORD')) {
|
||||
const w = this.advance() as Extract<Tok, { kind: 'WORD' }>;
|
||||
} else if (this.at("WORD")) {
|
||||
const w = this.advance() as Extract<Tok, { kind: "WORD" }>;
|
||||
value = w.text;
|
||||
} else {
|
||||
// Anything else after colon is treated literally as a single Term token.
|
||||
const t = this.advance();
|
||||
value = tokText(t);
|
||||
}
|
||||
return { type: 'Field', field: wordTok.text, value };
|
||||
return { type: "Field", field: wordTok.text, value };
|
||||
}
|
||||
|
||||
// plain term
|
||||
return { type: 'Term', value: wordTok.text };
|
||||
return { type: "Term", value: wordTok.text };
|
||||
}
|
||||
|
||||
const w = this.advance() as Extract<Tok, { kind: 'WORD' }>;
|
||||
return { type: 'Phrase', value: 'text' in w ? w.text : '' };
|
||||
const w = this.advance() as Extract<Tok, { kind: "WORD" }>;
|
||||
return { type: "Phrase", value: "text" in w ? w.text : "" };
|
||||
}
|
||||
}
|
||||
|
||||
function tokText(t: Tok): string {
|
||||
if ('text' in t) return t.text;
|
||||
if ("text" in t) return t.text;
|
||||
|
||||
switch (t.kind) {
|
||||
case 'COLON':
|
||||
return ':';
|
||||
case 'LPAREN':
|
||||
return '(';
|
||||
case 'RPAREN':
|
||||
return ')';
|
||||
case "COLON":
|
||||
return ":";
|
||||
case "LPAREN":
|
||||
return "(";
|
||||
case "RPAREN":
|
||||
return ")";
|
||||
default:
|
||||
return '';
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function parseQuery(q: string): Ast | null {
|
||||
if (q.trim() === '') return null;
|
||||
if (q.trim() === "") return null;
|
||||
const toks = tokenize(q);
|
||||
const parser = new Parser(toks);
|
||||
return parser.parse();
|
||||
@@ -251,45 +251,45 @@ export type Doc = {
|
||||
fields?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
type Technique = 'substring' | 'fuzzy' | 'strict';
|
||||
type Technique = "substring" | "fuzzy" | "strict";
|
||||
|
||||
function includes(hay: string | undefined, needle: string, technique: Technique): boolean {
|
||||
if (!hay || !needle) return false;
|
||||
if (technique === 'strict') return hay === needle;
|
||||
if (technique === 'fuzzy') return !!fuzzyMatch(hay, needle);
|
||||
if (technique === "strict") return hay === needle;
|
||||
if (technique === "fuzzy") return !!fuzzyMatch(hay, needle);
|
||||
return hay.indexOf(needle) !== -1;
|
||||
}
|
||||
|
||||
export function evaluate(ast: Ast | null, doc: Doc): boolean {
|
||||
if (!ast) return true; // Match everything if no query is provided
|
||||
|
||||
const text = (doc.text ?? '').toLowerCase();
|
||||
const text = (doc.text ?? "").toLowerCase();
|
||||
const fieldsNorm: Record<string, string[]> = {};
|
||||
|
||||
for (const [k, v] of Object.entries(doc.fields ?? {})) {
|
||||
if (!(typeof v === 'string' || Array.isArray(v))) continue;
|
||||
if (!(typeof v === "string" || Array.isArray(v))) continue;
|
||||
fieldsNorm[k.toLowerCase()] = Array.isArray(v)
|
||||
? v.filter((v) => typeof v === 'string').map((s) => s.toLowerCase())
|
||||
: [String(v ?? '').toLowerCase()];
|
||||
? v.filter((v) => typeof v === "string").map((s) => s.toLowerCase())
|
||||
: [String(v ?? "").toLowerCase()];
|
||||
}
|
||||
|
||||
const evalNode = (node: Ast): boolean => {
|
||||
switch (node.type) {
|
||||
case 'Term':
|
||||
return includes(text, node.value.toLowerCase(), 'fuzzy');
|
||||
case 'Phrase':
|
||||
case "Term":
|
||||
return includes(text, node.value.toLowerCase(), "fuzzy");
|
||||
case "Phrase":
|
||||
// Quoted phrases match exactly
|
||||
return includes(text, node.value.toLowerCase(), 'substring');
|
||||
case 'Field': {
|
||||
return includes(text, node.value.toLowerCase(), "substring");
|
||||
case "Field": {
|
||||
const vals = fieldsNorm[node.field.toLowerCase()] ?? [];
|
||||
if (vals.length === 0) return false;
|
||||
return vals.some((v) => includes(v, node.value.toLowerCase(), 'substring'));
|
||||
return vals.some((v) => includes(v, node.value.toLowerCase(), "substring"));
|
||||
}
|
||||
case 'Not':
|
||||
case "Not":
|
||||
return !evalNode(node.node);
|
||||
case 'And':
|
||||
case "And":
|
||||
return evalNode(node.left) && evalNode(node.right);
|
||||
case 'Or':
|
||||
case "Or":
|
||||
return evalNode(node.left) || evalNode(node.right);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CompletionContext } from '@codemirror/autocomplete';
|
||||
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
|
||||
import { defaultBoost } from './twig/completion';
|
||||
import type { CompletionContext } from "@codemirror/autocomplete";
|
||||
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
||||
import { defaultBoost } from "./twig/completion";
|
||||
|
||||
export interface GenericCompletionConfig {
|
||||
minMatch?: number;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||
import { Decoration, EditorView, hoverTooltip, MatchDecorator, ViewPlugin } from '@codemirror/view';
|
||||
import { activeWorkspaceIdAtom } from '../../../../hooks/useActiveWorkspace';
|
||||
import { copyToClipboard } from '../../../../lib/copy';
|
||||
import { createRequestAndNavigate } from '../../../../lib/createRequestAndNavigate';
|
||||
import { jotaiStore } from '../../../../lib/jotai';
|
||||
import type { DecorationSet, ViewUpdate } from "@codemirror/view";
|
||||
import { Decoration, EditorView, hoverTooltip, MatchDecorator, ViewPlugin } from "@codemirror/view";
|
||||
import { activeWorkspaceIdAtom } from "../../../../hooks/useActiveWorkspace";
|
||||
import { copyToClipboard } from "../../../../lib/copy";
|
||||
import { createRequestAndNavigate } from "../../../../lib/createRequestAndNavigate";
|
||||
import { jotaiStore } from "../../../../lib/jotai";
|
||||
|
||||
const REGEX =
|
||||
/(https?:\/\/([-a-zA-Z0-9@:%._+*~#=]{1,256})+(\.[a-zA-Z0-9()]{1,6})?\b([-a-zA-Z0-9()@:%_+*.~#?&/={}[\]]*))/g;
|
||||
@@ -39,26 +39,26 @@ const tooltip = hoverTooltip(
|
||||
create() {
|
||||
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
|
||||
const link = text.substring(found?.start - from, found?.end - from);
|
||||
const dom = document.createElement('div');
|
||||
const dom = document.createElement("div");
|
||||
|
||||
const $open = document.createElement('a');
|
||||
$open.textContent = 'Open in browser';
|
||||
const $open = document.createElement("a");
|
||||
$open.textContent = "Open in browser";
|
||||
$open.href = link;
|
||||
$open.target = '_blank';
|
||||
$open.rel = 'noopener noreferrer';
|
||||
$open.target = "_blank";
|
||||
$open.rel = "noopener noreferrer";
|
||||
|
||||
const $copy = document.createElement('button');
|
||||
$copy.textContent = 'Copy to clipboard';
|
||||
$copy.addEventListener('click', () => {
|
||||
const $copy = document.createElement("button");
|
||||
$copy.textContent = "Copy to clipboard";
|
||||
$copy.addEventListener("click", () => {
|
||||
copyToClipboard(link);
|
||||
});
|
||||
|
||||
const $create = document.createElement('button');
|
||||
$create.textContent = 'Create new request';
|
||||
$create.addEventListener('click', async () => {
|
||||
const $create = document.createElement("button");
|
||||
$create.textContent = "Create new request";
|
||||
$create.addEventListener("click", async () => {
|
||||
await createRequestAndNavigate({
|
||||
model: 'http_request',
|
||||
workspaceId: workspaceId ?? 'n/a',
|
||||
model: "http_request",
|
||||
workspaceId: workspaceId ?? "n/a",
|
||||
url: link,
|
||||
});
|
||||
});
|
||||
@@ -94,12 +94,12 @@ const decorator = () => {
|
||||
const groupMatch = match[1];
|
||||
if (groupMatch == null) {
|
||||
// Should never happen, but make TS happy
|
||||
console.warn('Group match was empty', match);
|
||||
console.warn("Group match was empty", match);
|
||||
return Decoration.replace({});
|
||||
}
|
||||
|
||||
return Decoration.mark({
|
||||
class: 'hyperlink-widget',
|
||||
class: "hyperlink-widget",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Diagnostic } from '@codemirror/lint';
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import { parse as jsonLintParse } from '@prantlf/jsonlint';
|
||||
import type { Diagnostic } from "@codemirror/lint";
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import { parse as jsonLintParse } from "@prantlf/jsonlint";
|
||||
|
||||
const TEMPLATE_SYNTAX_REGEX = /\$\{\[[\s\S]*?]}/g;
|
||||
|
||||
@@ -15,14 +15,14 @@ export function jsonParseLinter(options?: JsonLintOptions) {
|
||||
const doc = view.state.doc.toString();
|
||||
// We need lint to not break on stuff like {"foo:" ${[ ... ]}} so we'll replace all template
|
||||
// syntax with repeating `1` characters, so it's valid JSON and the position is still correct.
|
||||
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => '1'.repeat(m.length));
|
||||
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, (m) => "1".repeat(m.length));
|
||||
jsonLintParse(escapedDoc, {
|
||||
mode: (options?.allowComments ?? true) ? 'cjson' : 'json',
|
||||
mode: (options?.allowComments ?? true) ? "cjson" : "json",
|
||||
ignoreTrailingCommas: options?.allowTrailingCommas ?? false,
|
||||
});
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
} catch (err: any) {
|
||||
if (!('location' in err)) {
|
||||
if (!("location" in err)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function jsonParseLinter(options?: JsonLintOptions) {
|
||||
{
|
||||
from: err.location.start.offset,
|
||||
to: err.location.start.offset,
|
||||
severity: 'error',
|
||||
severity: "error",
|
||||
message: err.message,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { parser } from './pairs';
|
||||
import { LanguageSupport, LRLanguage } from "@codemirror/language";
|
||||
import { parser } from "./pairs";
|
||||
|
||||
const language = LRLanguage.define({
|
||||
name: 'pairs',
|
||||
name: "pairs",
|
||||
parser,
|
||||
languageData: {},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const highlight = styleTags({
|
||||
Sep: t.bracket,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from '@lezer/lr';
|
||||
import { highlight } from './highlight';
|
||||
import { LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "zQQOPOOOVOQO'#CaQQOPOOO[OSO,58{OOOO-E6_-E6_OaOQO1G.gOOOO7+$R7+$R",
|
||||
stateData: 'f~OQPO~ORRO~OSTO~OVUO~O',
|
||||
goto: ']UPPPPPVQQORSQ',
|
||||
nodeNames: '⚠ pairs Key Sep Value',
|
||||
stateData: "f~OQPO~ORRO~OSTO~OVUO~O",
|
||||
goto: "]UPPPPPVQQORSQ",
|
||||
nodeNames: "⚠ pairs Key Sep Value",
|
||||
maxTerm: 7,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
@@ -17,13 +17,13 @@ export const parser = LRParser.deserialize({
|
||||
topRules: { pairs: [0, 1] },
|
||||
tokenPrec: 0,
|
||||
termNames: {
|
||||
'0': '⚠',
|
||||
'1': '@top',
|
||||
'2': 'Key',
|
||||
'3': 'Sep',
|
||||
'4': 'Value',
|
||||
'5': '(Key Sep Value "\\n")+',
|
||||
'6': '␄',
|
||||
'7': '"\\n"',
|
||||
"0": "⚠",
|
||||
"1": "@top",
|
||||
"2": "Key",
|
||||
"3": "Sep",
|
||||
"4": "Value",
|
||||
"5": '(Key Sep Value "\\n")+',
|
||||
"6": "␄",
|
||||
"7": '"\\n"',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { getSearchQuery, searchPanelOpen } from '@codemirror/search';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { type EditorView, ViewPlugin, type ViewUpdate } from '@codemirror/view';
|
||||
import { getSearchQuery, searchPanelOpen } from "@codemirror/search";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { type EditorView, ViewPlugin, type ViewUpdate } from "@codemirror/view";
|
||||
|
||||
/**
|
||||
* A CodeMirror extension that displays the total number of search matches
|
||||
@@ -41,7 +41,7 @@ export function searchMatchCount(): Extension {
|
||||
|
||||
if (!query.search) {
|
||||
if (this.countEl) {
|
||||
this.countEl.textContent = '0/0';
|
||||
this.countEl.textContent = "0/0";
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -64,7 +64,7 @@ export function searchMatchCount(): Extension {
|
||||
if (count > MAX_COUNT) {
|
||||
this.countEl.textContent = `${MAX_COUNT}+`;
|
||||
} else if (count === 0) {
|
||||
this.countEl.textContent = '0/0';
|
||||
this.countEl.textContent = "0/0";
|
||||
} else if (currentIndex > 0) {
|
||||
this.countEl.textContent = `${currentIndex}/${count}`;
|
||||
} else {
|
||||
@@ -75,7 +75,7 @@ export function searchMatchCount(): Extension {
|
||||
|
||||
private ensureCountEl() {
|
||||
// Find the search panel in the editor DOM
|
||||
const panel = this.view.dom.querySelector('.cm-search');
|
||||
const panel = this.view.dom.querySelector(".cm-search");
|
||||
if (!panel) {
|
||||
this.countEl = null;
|
||||
return;
|
||||
@@ -85,11 +85,11 @@ export function searchMatchCount(): Extension {
|
||||
return; // Already attached
|
||||
}
|
||||
|
||||
this.countEl = document.createElement('span');
|
||||
this.countEl.className = 'cm-search-match-count';
|
||||
this.countEl = document.createElement("span");
|
||||
this.countEl.className = "cm-search-match-count";
|
||||
|
||||
// Reorder: insert prev button, then next button, then count after the search input
|
||||
const searchInput = panel.querySelector('input');
|
||||
const searchInput = panel.querySelector("input");
|
||||
const prevBtn = panel.querySelector('button[name="prev"]');
|
||||
const nextBtn = panel.querySelector('button[name="next"]');
|
||||
if (searchInput && searchInput.parentElement === panel) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Extension, TransactionSpec } from '@codemirror/state';
|
||||
import { EditorSelection, EditorState, Transaction } from '@codemirror/state';
|
||||
import type { Extension, TransactionSpec } from "@codemirror/state";
|
||||
import { EditorSelection, EditorState, Transaction } from "@codemirror/state";
|
||||
|
||||
/**
|
||||
* A CodeMirror extension that forces single-line input by stripping
|
||||
@@ -17,14 +17,14 @@ import { EditorSelection, EditorState, Transaction } from '@codemirror/state';
|
||||
export function singleLineExtensions(): Extension {
|
||||
return EditorState.transactionFilter.of(
|
||||
(tr: Transaction): TransactionSpec | readonly TransactionSpec[] => {
|
||||
if (!tr.isUserEvent('input') || tr.isUserEvent('input.type.compose')) return tr;
|
||||
if (!tr.isUserEvent("input") || tr.isUserEvent("input.type.compose")) return tr;
|
||||
|
||||
const changes: { from: number; to: number; insert: string }[] = [];
|
||||
|
||||
tr.changes.iterChanges((_fromA, toA, fromB, _toB, inserted) => {
|
||||
let insert = '';
|
||||
let insert = "";
|
||||
for (const line of inserted.iterLines()) {
|
||||
insert += line.replace(/\n/g, '');
|
||||
insert += line.replace(/\n/g, "");
|
||||
}
|
||||
|
||||
if (insert !== inserted.toString()) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { parser } from './text';
|
||||
import { LanguageSupport, LRLanguage } from "@codemirror/language";
|
||||
import { parser } from "./text";
|
||||
|
||||
export const textLanguage = LRLanguage.define({
|
||||
name: 'text',
|
||||
name: "text",
|
||||
parser,
|
||||
languageData: {},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from '@lezer/lr';
|
||||
import { LRParser } from "@lezer/lr";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: '[OQOPOOQOOOOO',
|
||||
stateData: 'V~OQPO~O',
|
||||
goto: 'QPP',
|
||||
nodeNames: '⚠ Template Text',
|
||||
states: "[OQOPOOQOOOOO",
|
||||
stateData: "V~OQPO~O",
|
||||
goto: "QPP",
|
||||
nodeNames: "⚠ Template Text",
|
||||
maxTerm: 3,
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 0,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { parser } from './timeline';
|
||||
import { LanguageSupport, LRLanguage } from "@codemirror/language";
|
||||
import { parser } from "./timeline";
|
||||
|
||||
export const timelineLanguage = LRLanguage.define({
|
||||
name: 'timeline',
|
||||
name: "timeline",
|
||||
parser,
|
||||
languageData: {},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const highlight = styleTags({
|
||||
OutgoingText: t.propertyName, // > lines - primary color (matches timeline icons)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
export const
|
||||
Timeline = 1,
|
||||
export const Timeline = 1,
|
||||
OutgoingLine = 2,
|
||||
OutgoingText = 3,
|
||||
Newline = 4,
|
||||
@@ -9,4 +8,4 @@ export const
|
||||
InfoLine = 7,
|
||||
InfoText = 8,
|
||||
PlainLine = 9,
|
||||
PlainText = 10
|
||||
PlainText = 10;
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
import {highlight} from "./highlight"
|
||||
import { LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "!pQQOPOOO`OPO'#C^OeOPO'#CaOjOPO'#CcOoOPO'#CeOOOO'#Ci'#CiOOOO'#Cg'#CgQQOPOOOOOO,58x,58xOOOO,58{,58{OOOO,58},58}OOOO,59P,59POOOO-E6e-E6e",
|
||||
states:
|
||||
"!pQQOPOOO`OPO'#C^OeOPO'#CaOjOPO'#CcOoOPO'#CeOOOO'#Ci'#CiOOOO'#Cg'#CgQQOPOOOOOO,58x,58xOOOO,58{,58{OOOO,58},58}OOOO,59P,59POOOO-E6e-E6e",
|
||||
stateData: "z~ORPOUQOWROYSO~OSWO~OSXO~OSYO~OSZO~ORUWYW~",
|
||||
goto: "m^PP_PP_P_P_PcPiTTOVQVOR[VTUOV",
|
||||
nodeNames: "⚠ Timeline OutgoingLine OutgoingText Newline IncomingLine IncomingText InfoLine InfoText PlainLine PlainText",
|
||||
nodeNames:
|
||||
"⚠ Timeline OutgoingLine OutgoingText Newline IncomingLine IncomingText InfoLine InfoText PlainLine PlainText",
|
||||
maxTerm: 13,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 1,
|
||||
tokenData: "%h~RZOYtYZ!]Zztz{!b{!^t!^!_#d!_!`t!`!a$f!a;'St;'S;=`!V<%lOt~ySY~OYtZ;'St;'S;=`!V<%lOt~!YP;=`<%lt~!bOS~~!gUY~OYtZptpq!yq;'St;'S;=`!V<%lOt~#QSW~Y~OY!yZ;'S!y;'S;=`#^<%lO!y~#aP;=`<%l!y~#iUY~OYtZptpq#{q;'St;'S;=`!V<%lOt~$SSU~Y~OY#{Z;'S#{;'S;=`$`<%lO#{~$cP;=`<%l#{~$kUY~OYtZptpq$}q;'St;'S;=`!V<%lOt~%USR~Y~OY$}Z;'S$};'S;=`%b<%lO$}~%eP;=`<%l$}",
|
||||
tokenData:
|
||||
"%h~RZOYtYZ!]Zztz{!b{!^t!^!_#d!_!`t!`!a$f!a;'St;'S;=`!V<%lOt~ySY~OYtZ;'St;'S;=`!V<%lOt~!YP;=`<%lt~!bOS~~!gUY~OYtZptpq!yq;'St;'S;=`!V<%lOt~#QSW~Y~OY!yZ;'S!y;'S;=`#^<%lO!y~#aP;=`<%l!y~#iUY~OYtZptpq#{q;'St;'S;=`!V<%lOt~$SSU~Y~OY#{Z;'S#{;'S;=`$`<%lO#{~$cP;=`<%l#{~$kUY~OYtZptpq$}q;'St;'S;=`!V<%lOt~%USR~Y~OY$}Z;'S$};'S;=`%b<%lO$}~%eP;=`<%l$}",
|
||||
tokenizers: [0],
|
||||
topRules: {"Timeline":[0,1]},
|
||||
tokenPrec: 36
|
||||
})
|
||||
topRules: { Timeline: [0, 1] },
|
||||
tokenPrec: 36,
|
||||
});
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import type { Completion, CompletionContext } from '@codemirror/autocomplete';
|
||||
import { startCompletion } from '@codemirror/autocomplete';
|
||||
import type { TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import type { Completion, CompletionContext } from "@codemirror/autocomplete";
|
||||
import { startCompletion } from "@codemirror/autocomplete";
|
||||
import type { TemplateFunction } from "@yaakapp-internal/plugins";
|
||||
|
||||
const openTag = '${[ ';
|
||||
const closeTag = ' ]}';
|
||||
const openTag = "${[ ";
|
||||
const closeTag = " ]}";
|
||||
|
||||
export type TwigCompletionOptionVariable = {
|
||||
type: 'variable';
|
||||
type: "variable";
|
||||
};
|
||||
|
||||
export type TwigCompletionOptionNamespace = {
|
||||
type: 'namespace';
|
||||
type: "namespace";
|
||||
};
|
||||
|
||||
export type TwigCompletionOptionFunction = TemplateFunction & {
|
||||
type: 'function';
|
||||
type: "function";
|
||||
};
|
||||
|
||||
export type TwigCompletionOption = (
|
||||
@@ -50,17 +50,17 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||
|
||||
const completions: Completion[] = options
|
||||
.flatMap((o): Completion[] => {
|
||||
const matchSegments = toMatch.text.replace(/^\$/, '').split('.');
|
||||
const optionSegments = o.name.split('.');
|
||||
const matchSegments = toMatch.text.replace(/^\$/, "").split(".");
|
||||
const optionSegments = o.name.split(".");
|
||||
|
||||
// If not on the last segment, only complete the namespace
|
||||
if (matchSegments.length < optionSegments.length) {
|
||||
const prefix = optionSegments.slice(0, matchSegments.length).join('.');
|
||||
const prefix = optionSegments.slice(0, matchSegments.length).join(".");
|
||||
return [
|
||||
{
|
||||
label: `${prefix}.*`,
|
||||
type: 'namespace',
|
||||
detail: 'namespace',
|
||||
type: "namespace",
|
||||
detail: "namespace",
|
||||
apply: (view, _completion, from, to) => {
|
||||
const insert = `${prefix}.`;
|
||||
view.dispatch({
|
||||
@@ -75,13 +75,13 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||
}
|
||||
|
||||
// If on the last segment, wrap the entire tag
|
||||
const inner = o.type === 'function' ? `${o.name}()` : o.name;
|
||||
const inner = o.type === "function" ? `${o.name}()` : o.name;
|
||||
return [
|
||||
{
|
||||
label: o.name,
|
||||
info: o.description,
|
||||
detail: o.type,
|
||||
type: o.type === 'variable' ? 'variable' : 'function',
|
||||
type: o.type === "variable" ? "variable" : "function",
|
||||
apply: (view, _completion, from, to) => {
|
||||
const insert = openTag + inner + closeTag;
|
||||
view.dispatch({
|
||||
@@ -94,7 +94,7 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
|
||||
})
|
||||
.filter((v) => v != null);
|
||||
|
||||
const uniqueCompletions = uniqueBy(completions, 'label');
|
||||
const uniqueCompletions = uniqueBy(completions, "label");
|
||||
const sortedCompletions = uniqueCompletions.sort((a, b) => {
|
||||
const boostDiff = defaultBoost(b) - defaultBoost(a);
|
||||
if (boostDiff !== 0) return boostDiff;
|
||||
@@ -119,9 +119,9 @@ export function uniqueBy<T, K extends keyof T>(arr: T[], key: K): T[] {
|
||||
}
|
||||
|
||||
export function defaultBoost(o: Completion) {
|
||||
if (o.type === 'variable') return 4;
|
||||
if (o.type === 'constant') return 3;
|
||||
if (o.type === 'function') return 2;
|
||||
if (o.type === 'namespace') return 1;
|
||||
if (o.type === "variable") return 4;
|
||||
if (o.type === "constant") return 3;
|
||||
if (o.type === "function") return 2;
|
||||
if (o.type === "namespace") return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { LanguageSupport } from '@codemirror/language';
|
||||
import { LRLanguage } from '@codemirror/language';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { parseMixed } from '@lezer/common';
|
||||
import type { WrappedEnvironmentVariable } from '../../../../hooks/useEnvironmentVariables';
|
||||
import type { GenericCompletionConfig } from '../genericCompletion';
|
||||
import { genericCompletion } from '../genericCompletion';
|
||||
import { textLanguage } from '../text/extension';
|
||||
import type { TwigCompletionOption } from './completion';
|
||||
import { twigCompletion } from './completion';
|
||||
import { templateTagsPlugin } from './templateTags';
|
||||
import { parser as twigParser } from './twig';
|
||||
import type { LanguageSupport } from "@codemirror/language";
|
||||
import { LRLanguage } from "@codemirror/language";
|
||||
import type { Extension } from "@codemirror/state";
|
||||
import { parseMixed } from "@lezer/common";
|
||||
import type { WrappedEnvironmentVariable } from "../../../../hooks/useEnvironmentVariables";
|
||||
import type { GenericCompletionConfig } from "../genericCompletion";
|
||||
import { genericCompletion } from "../genericCompletion";
|
||||
import { textLanguage } from "../text/extension";
|
||||
import type { TwigCompletionOption } from "./completion";
|
||||
import { twigCompletion } from "./completion";
|
||||
import { templateTagsPlugin } from "./templateTags";
|
||||
import { parser as twigParser } from "./twig";
|
||||
|
||||
export function twig({
|
||||
base,
|
||||
@@ -35,7 +35,7 @@ export function twig({
|
||||
environmentVariables.map((v) => ({
|
||||
name: v.variable.name,
|
||||
value: v.variable.value,
|
||||
type: 'variable',
|
||||
type: "variable",
|
||||
label: v.variable.name,
|
||||
description: `Inherited from ${v.source}`,
|
||||
onClick: (rawTag: string, startPos: number) => onClickVariable(v, rawTag, startPos),
|
||||
@@ -74,12 +74,12 @@ function mixLanguage(base: LanguageSupport): LRLanguage {
|
||||
|
||||
return {
|
||||
parser: base.language.parser,
|
||||
overlay: (node) => node.type.name === 'Text',
|
||||
overlay: (node) => node.type.name === "Text",
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const language = LRLanguage.define({ name: 'twig', parser });
|
||||
const language = LRLanguage.define({ name: "twig", parser });
|
||||
mixedLanguagesCache[base.language.name] = language;
|
||||
return language;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const highlight = styleTags({
|
||||
TagOpen: t.bracket,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { Range } from '@codemirror/state';
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
import type { Range } from "@codemirror/state";
|
||||
import type { DecorationSet, ViewUpdate } from "@codemirror/view";
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view";
|
||||
|
||||
class PathPlaceholderWidget extends WidgetType {
|
||||
readonly #clickListenerCallback: () => void;
|
||||
@@ -22,15 +22,15 @@ class PathPlaceholderWidget extends WidgetType {
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const elt = document.createElement('span');
|
||||
elt.className = 'x-theme-templateTag x-theme-templateTag--secondary template-tag';
|
||||
const elt = document.createElement("span");
|
||||
elt.className = "x-theme-templateTag x-theme-templateTag--secondary template-tag";
|
||||
elt.textContent = this.rawText;
|
||||
elt.addEventListener('click', this.#clickListenerCallback);
|
||||
elt.addEventListener("click", this.#clickListenerCallback);
|
||||
return elt;
|
||||
}
|
||||
|
||||
destroy(dom: HTMLElement) {
|
||||
dom.removeEventListener('click', this.#clickListenerCallback);
|
||||
dom.removeEventListener("click", this.#clickListenerCallback);
|
||||
super.destroy(dom);
|
||||
}
|
||||
|
||||
@@ -50,14 +50,14 @@ function pathParameters(
|
||||
from,
|
||||
to,
|
||||
enter(node) {
|
||||
if (node.name === 'Text') {
|
||||
if (node.name === "Text") {
|
||||
// Find the `url` node and then jump into it to find the placeholders
|
||||
for (let i = node.from; i < node.to; i++) {
|
||||
const innerTree = syntaxTree(view.state).resolveInner(i);
|
||||
if (innerTree.node.name === 'url') {
|
||||
if (innerTree.node.name === "url") {
|
||||
innerTree.toTree().iterate({
|
||||
enter(node) {
|
||||
if (node.name !== 'Placeholder') return;
|
||||
if (node.name !== "Placeholder") return;
|
||||
const globalFrom = innerTree.node.from + node.from;
|
||||
const globalTo = innerTree.node.from + node.to;
|
||||
const rawText = view.state.doc.sliceString(globalFrom, globalTo);
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
import type { Range } from '@codemirror/state';
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||
import type { SyntaxNodeRef } from '@lezer/common';
|
||||
import { applyFormInputDefaults, validateTemplateFunctionArgs } from '@yaakapp-internal/lib';
|
||||
import type { FormInput, JsonPrimitive, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import { parseTemplate } from '@yaakapp-internal/templates';
|
||||
import type { TwigCompletionOption } from './completion';
|
||||
import { collectArgumentValues } from './util';
|
||||
import { syntaxTree } from "@codemirror/language";
|
||||
import type { Range } from "@codemirror/state";
|
||||
import type { DecorationSet, ViewUpdate } from "@codemirror/view";
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from "@codemirror/view";
|
||||
import type { SyntaxNodeRef } from "@lezer/common";
|
||||
import { applyFormInputDefaults, validateTemplateFunctionArgs } from "@yaakapp-internal/lib";
|
||||
import type { FormInput, JsonPrimitive, TemplateFunction } from "@yaakapp-internal/plugins";
|
||||
import { parseTemplate } from "@yaakapp-internal/templates";
|
||||
import type { TwigCompletionOption } from "./completion";
|
||||
import { collectArgumentValues } from "./util";
|
||||
|
||||
class TemplateTagWidget extends WidgetType {
|
||||
readonly #clickListenerCallback: () => void;
|
||||
@@ -34,24 +34,24 @@ class TemplateTagWidget extends WidgetType {
|
||||
}
|
||||
|
||||
toDOM() {
|
||||
const elt = document.createElement('span');
|
||||
const elt = document.createElement("span");
|
||||
elt.className = `x-theme-templateTag template-tag ${
|
||||
this.option.invalid
|
||||
? 'x-theme-templateTag--danger'
|
||||
: this.option.type === 'variable'
|
||||
? 'x-theme-templateTag--primary'
|
||||
: 'x-theme-templateTag--info'
|
||||
? "x-theme-templateTag--danger"
|
||||
: this.option.type === "variable"
|
||||
? "x-theme-templateTag--primary"
|
||||
: "x-theme-templateTag--info"
|
||||
}`;
|
||||
elt.title = this.option.invalid ? 'Not Found' : (this.option.value ?? '');
|
||||
elt.setAttribute('data-tag-type', this.option.type);
|
||||
if (typeof this.option.label === 'string') elt.textContent = this.option.label;
|
||||
elt.title = this.option.invalid ? "Not Found" : (this.option.value ?? "");
|
||||
elt.setAttribute("data-tag-type", this.option.type);
|
||||
if (typeof this.option.label === "string") elt.textContent = this.option.label;
|
||||
else elt.appendChild(this.option.label);
|
||||
elt.addEventListener('click', this.#clickListenerCallback);
|
||||
elt.addEventListener("click", this.#clickListenerCallback);
|
||||
return elt;
|
||||
}
|
||||
|
||||
destroy(dom: HTMLElement) {
|
||||
dom.removeEventListener('click', this.#clickListenerCallback);
|
||||
dom.removeEventListener("click", this.#clickListenerCallback);
|
||||
super.destroy(dom);
|
||||
}
|
||||
|
||||
@@ -72,34 +72,34 @@ function templateTags(
|
||||
from,
|
||||
to,
|
||||
enter(node) {
|
||||
if (node.name === 'Tag') {
|
||||
if (node.name === "Tag") {
|
||||
// Don't decorate if the cursor is inside the match
|
||||
if (isSelectionInsideNode(view, node)) return;
|
||||
|
||||
const rawTag = view.state.doc.sliceString(node.from, node.to);
|
||||
|
||||
// TODO: Search `node.tree` instead of using Regex here
|
||||
const inner = rawTag.replace(/^\$\{\[\s*/, '').replace(/\s*]}$/, '');
|
||||
const inner = rawTag.replace(/^\$\{\[\s*/, "").replace(/\s*]}$/, "");
|
||||
let name = inner.match(/([\w.]+)[(]/)?.[1] ?? inner;
|
||||
|
||||
if (inner.includes('\n')) {
|
||||
if (inner.includes("\n")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// The beta named the function `Response` but was changed in stable.
|
||||
// Keep this here for a while because there's no easy way to migrate
|
||||
if (name === 'Response') {
|
||||
name = 'response';
|
||||
if (name === "Response") {
|
||||
name = "response";
|
||||
}
|
||||
|
||||
let option = options.find(
|
||||
(o) => o.name === name || (o.type === 'function' && o.aliases?.includes(name)),
|
||||
(o) => o.name === name || (o.type === "function" && o.aliases?.includes(name)),
|
||||
);
|
||||
|
||||
if (option == null) {
|
||||
const from = node.from; // Cache here so the reference doesn't change
|
||||
option = {
|
||||
type: 'variable',
|
||||
type: "variable",
|
||||
invalid: true,
|
||||
name: inner,
|
||||
value: null,
|
||||
@@ -110,7 +110,7 @@ function templateTags(
|
||||
};
|
||||
}
|
||||
|
||||
if (option.type === 'function') {
|
||||
if (option.type === "function") {
|
||||
const tokens = parseTemplate(rawTag);
|
||||
const rawValues = collectArgumentValues(tokens, option);
|
||||
const values = applyFormInputDefaults(option.args, rawValues);
|
||||
@@ -175,49 +175,49 @@ function makeFunctionLabel(
|
||||
): HTMLElement | string {
|
||||
if (fn.args.length === 0) return fn.name;
|
||||
|
||||
const $outer = document.createElement('span');
|
||||
$outer.className = 'fn';
|
||||
const $bOpen = document.createElement('span');
|
||||
$bOpen.className = 'fn-bracket';
|
||||
$bOpen.textContent = '(';
|
||||
const $outer = document.createElement("span");
|
||||
$outer.className = "fn";
|
||||
const $bOpen = document.createElement("span");
|
||||
$bOpen.className = "fn-bracket";
|
||||
$bOpen.textContent = "(";
|
||||
$outer.appendChild(document.createTextNode(fn.name));
|
||||
$outer.appendChild($bOpen);
|
||||
|
||||
const $inner = document.createElement('span');
|
||||
$inner.className = 'fn-inner';
|
||||
$inner.title = '';
|
||||
const $inner = document.createElement("span");
|
||||
$inner.className = "fn-inner";
|
||||
$inner.title = "";
|
||||
fn.previewArgs?.forEach((name: string, i: number, all: string[]) => {
|
||||
const v = String(values[name] || '');
|
||||
const v = String(values[name] || "");
|
||||
if (!v) return;
|
||||
if (all.length > 1) {
|
||||
const $c = document.createElement('span');
|
||||
$c.className = 'fn-arg-name';
|
||||
const $c = document.createElement("span");
|
||||
$c.className = "fn-arg-name";
|
||||
$c.textContent = i > 0 ? `, ${name}=` : `${name}=`;
|
||||
$inner.appendChild($c);
|
||||
}
|
||||
|
||||
const $v = document.createElement('span');
|
||||
$v.className = 'fn-arg-value';
|
||||
$v.textContent = v.includes(' ') ? `'${v}'` : v;
|
||||
const $v = document.createElement("span");
|
||||
$v.className = "fn-arg-value";
|
||||
$v.textContent = v.includes(" ") ? `'${v}'` : v;
|
||||
$inner.appendChild($v);
|
||||
});
|
||||
fn.args.forEach((a: FormInput, i: number) => {
|
||||
if (!('name' in a)) return;
|
||||
if (!("name" in a)) return;
|
||||
const v = values[a.name];
|
||||
if (v == null) return;
|
||||
if (i > 0) $inner.title += '\n';
|
||||
if (i > 0) $inner.title += "\n";
|
||||
$inner.title += `${a.name} = ${JSON.stringify(v)}`;
|
||||
});
|
||||
|
||||
if ($inner.childNodes.length === 0) {
|
||||
$inner.appendChild(document.createTextNode('…'));
|
||||
$inner.appendChild(document.createTextNode("…"));
|
||||
}
|
||||
|
||||
$outer.appendChild($inner);
|
||||
|
||||
const $bClose = document.createElement('span');
|
||||
$bClose.className = 'fn-bracket';
|
||||
$bClose.textContent = ')';
|
||||
const $bClose = document.createElement("span");
|
||||
$bClose.className = "fn-bracket";
|
||||
$bClose.textContent = ")";
|
||||
$outer.appendChild($bClose);
|
||||
|
||||
return $outer;
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/* oxlint-disable no-template-curly-in-string */
|
||||
|
||||
import { describe, expect, test } from 'vite-plus/test';
|
||||
import { parser } from './twig';
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { parser } from "./twig";
|
||||
|
||||
function getNodeNames(input: string): string[] {
|
||||
const tree = parser.parse(input);
|
||||
const nodes: string[] = [];
|
||||
const cursor = tree.cursor();
|
||||
do {
|
||||
if (cursor.name !== 'Template') {
|
||||
if (cursor.name !== "Template") {
|
||||
nodes.push(cursor.name);
|
||||
}
|
||||
} while (cursor.next());
|
||||
@@ -16,93 +16,93 @@ function getNodeNames(input: string): string[] {
|
||||
}
|
||||
|
||||
function hasTag(input: string): boolean {
|
||||
return getNodeNames(input).includes('Tag');
|
||||
return getNodeNames(input).includes("Tag");
|
||||
}
|
||||
|
||||
function hasError(input: string): boolean {
|
||||
return getNodeNames(input).includes('⚠');
|
||||
return getNodeNames(input).includes("⚠");
|
||||
}
|
||||
|
||||
describe('twig grammar', () => {
|
||||
describe('${[var]} format (valid template tags)', () => {
|
||||
test('parses simple variable as Tag', () => {
|
||||
expect(hasTag('${[var]}')).toBe(true);
|
||||
expect(hasError('${[var]}')).toBe(false);
|
||||
describe("twig grammar", () => {
|
||||
describe("${[var]} format (valid template tags)", () => {
|
||||
test("parses simple variable as Tag", () => {
|
||||
expect(hasTag("${[var]}")).toBe(true);
|
||||
expect(hasError("${[var]}")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses variable with whitespace as Tag', () => {
|
||||
expect(hasTag('${[ var ]}')).toBe(true);
|
||||
expect(hasError('${[ var ]}')).toBe(false);
|
||||
test("parses variable with whitespace as Tag", () => {
|
||||
expect(hasTag("${[ var ]}")).toBe(true);
|
||||
expect(hasError("${[ var ]}")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses embedded variable as Tag', () => {
|
||||
expect(hasTag('hello ${[name]} world')).toBe(true);
|
||||
expect(hasError('hello ${[name]} world')).toBe(false);
|
||||
test("parses embedded variable as Tag", () => {
|
||||
expect(hasTag("hello ${[name]} world")).toBe(true);
|
||||
expect(hasError("hello ${[name]} world")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses function call as Tag', () => {
|
||||
expect(hasTag('${[fn()]}')).toBe(true);
|
||||
expect(hasError('${[fn()]}')).toBe(false);
|
||||
test("parses function call as Tag", () => {
|
||||
expect(hasTag("${[fn()]}")).toBe(true);
|
||||
expect(hasError("${[fn()]}")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('${var} format (should be plain text, not tags)', () => {
|
||||
test('parses ${var} as plain Text without errors', () => {
|
||||
expect(hasTag('${var}')).toBe(false);
|
||||
expect(hasError('${var}')).toBe(false);
|
||||
describe("${var} format (should be plain text, not tags)", () => {
|
||||
test("parses ${var} as plain Text without errors", () => {
|
||||
expect(hasTag("${var}")).toBe(false);
|
||||
expect(hasError("${var}")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses embedded ${var} as plain Text', () => {
|
||||
expect(hasTag('hello ${name} world')).toBe(false);
|
||||
expect(hasError('hello ${name} world')).toBe(false);
|
||||
test("parses embedded ${var} as plain Text", () => {
|
||||
expect(hasTag("hello ${name} world")).toBe(false);
|
||||
expect(hasError("hello ${name} world")).toBe(false);
|
||||
});
|
||||
|
||||
test('parses JSON with ${var} as plain Text', () => {
|
||||
test("parses JSON with ${var} as plain Text", () => {
|
||||
const json = '{"key": "${value}"}';
|
||||
expect(hasTag(json)).toBe(false);
|
||||
expect(hasError(json)).toBe(false);
|
||||
});
|
||||
|
||||
test('parses multiple ${var} as plain Text', () => {
|
||||
expect(hasTag('${a} and ${b}')).toBe(false);
|
||||
expect(hasError('${a} and ${b}')).toBe(false);
|
||||
test("parses multiple ${var} as plain Text", () => {
|
||||
expect(hasTag("${a} and ${b}")).toBe(false);
|
||||
expect(hasError("${a} and ${b}")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mixed content', () => {
|
||||
test('distinguishes ${var} from ${[var]} in same string', () => {
|
||||
const input = '${plain} and ${[tag]}';
|
||||
describe("mixed content", () => {
|
||||
test("distinguishes ${var} from ${[var]} in same string", () => {
|
||||
const input = "${plain} and ${[tag]}";
|
||||
expect(hasTag(input)).toBe(true);
|
||||
expect(hasError(input)).toBe(false);
|
||||
});
|
||||
|
||||
test('parses JSON with ${[var]} as having Tag', () => {
|
||||
test("parses JSON with ${[var]} as having Tag", () => {
|
||||
const json = '{"key": "${[value]}"}';
|
||||
expect(hasTag(json)).toBe(true);
|
||||
expect(hasError(json)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
test('handles $ at end of string', () => {
|
||||
expect(hasError('hello$')).toBe(false);
|
||||
expect(hasTag('hello$')).toBe(false);
|
||||
describe("edge cases", () => {
|
||||
test("handles $ at end of string", () => {
|
||||
expect(hasError("hello$")).toBe(false);
|
||||
expect(hasTag("hello$")).toBe(false);
|
||||
});
|
||||
|
||||
test('handles ${ at end of string without crash', () => {
|
||||
test("handles ${ at end of string without crash", () => {
|
||||
// Incomplete syntax may produce errors, but should not crash
|
||||
expect(() => parser.parse('hello${')).not.toThrow();
|
||||
expect(() => parser.parse("hello${")).not.toThrow();
|
||||
});
|
||||
|
||||
test('handles ${[ without closing without crash', () => {
|
||||
test("handles ${[ without closing without crash", () => {
|
||||
// Unclosed tag may produce partial match, but should not crash
|
||||
expect(() => parser.parse('${[unclosed')).not.toThrow();
|
||||
expect(() => parser.parse("${[unclosed")).not.toThrow();
|
||||
});
|
||||
|
||||
test('handles empty ${[]}', () => {
|
||||
test("handles empty ${[]}", () => {
|
||||
// Empty tags may or may not be valid depending on grammar
|
||||
// Just ensure no crash
|
||||
expect(() => parser.parse('${[]}')).not.toThrow();
|
||||
expect(() => parser.parse("${[]}")).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LocalTokenGroup, LRParser } from '@lezer/lr';
|
||||
import { highlight } from './highlight';
|
||||
import { LocalTokenGroup, LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states:
|
||||
"!^QQOPOOOOOO'#C_'#C_OYOQO'#C^OOOO'#Cc'#CcQQOPOOOOOO'#Cd'#CdO_OQO,58xOOOO-E6a-E6aOOOO-E6b-E6bOOOO1G.d1G.d",
|
||||
stateData: 'g~OUROYPO~OSTO~OSTOTXO~O',
|
||||
goto: 'nXPPY^PPPbhTROSTQOSQSORVSQUQRWU',
|
||||
nodeNames: '⚠ Template Tag TagOpen TagContent TagClose Text',
|
||||
stateData: "g~OUROYPO~OSTO~OSTOTXO~O",
|
||||
goto: "nXPPY^PPPbhTROSTQOSQSORVSQUQRWU",
|
||||
nodeNames: "⚠ Template Tag TagOpen TagContent TagClose Text",
|
||||
maxTerm: 10,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
repeatNodeCount: 2,
|
||||
tokenData:
|
||||
"#{~RTOtbtu!zu;'Sb;'S;=`!o<%lOb~gTU~Otbtuvu;'Sb;'S;=`!o<%lOb~yVO#ob#o#p!`#p;'Sb;'S;=`!o<%l~b~Ob~~!u~!cSO!}b#O;'Sb;'S;=`!o<%lOb~!rP;=`<%lb~!zOU~~!}VO#ob#o#p#d#p;'Sb;'S;=`!o<%l~b~Ob~~!u~#gTO!}b!}#O#v#O;'Sb;'S;=`!o<%lOb~#{OY~",
|
||||
tokenizers: [1, new LocalTokenGroup('b~RP#P#QU~XP#q#r[~aOT~~', 17, 4)],
|
||||
tokenizers: [1, new LocalTokenGroup("b~RP#P#QU~XP#q#r[~aOT~~", 17, 4)],
|
||||
topRules: { Template: [0, 1] },
|
||||
tokenPrec: 0,
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { FormInput, TemplateFunction } from '@yaakapp-internal/plugins';
|
||||
import type { Tokens } from '@yaakapp-internal/templates';
|
||||
import type { FormInput, TemplateFunction } from "@yaakapp-internal/plugins";
|
||||
import type { Tokens } from "@yaakapp-internal/templates";
|
||||
|
||||
/**
|
||||
* Process the initial tokens from the template and merge those with the default values pulled from
|
||||
@@ -8,21 +8,21 @@ import type { Tokens } from '@yaakapp-internal/templates';
|
||||
export function collectArgumentValues(initialTokens: Tokens, templateFunction: TemplateFunction) {
|
||||
const initial: Record<string, string | boolean> = {};
|
||||
const initialArgs =
|
||||
initialTokens.tokens[0]?.type === 'tag' && initialTokens.tokens[0]?.val.type === 'fn'
|
||||
initialTokens.tokens[0]?.type === "tag" && initialTokens.tokens[0]?.val.type === "fn"
|
||||
? initialTokens.tokens[0]?.val.args
|
||||
: [];
|
||||
|
||||
const processArg = (arg: FormInput) => {
|
||||
if ('inputs' in arg && arg.inputs) {
|
||||
if ("inputs" in arg && arg.inputs) {
|
||||
arg.inputs.forEach(processArg);
|
||||
}
|
||||
if (!('name' in arg)) return;
|
||||
if (!("name" in arg)) return;
|
||||
|
||||
const initialArg = initialArgs.find((a) => a.name === arg.name);
|
||||
const initialArgValue =
|
||||
initialArg?.value.type === 'str'
|
||||
initialArg?.value.type === "str"
|
||||
? initialArg?.value.text
|
||||
: initialArg?.value.type === 'bool'
|
||||
: initialArg?.value.type === "bool"
|
||||
? initialArg.value.value
|
||||
: undefined;
|
||||
const value = initialArgValue ?? arg.defaultValue;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { genericCompletion } from '../genericCompletion';
|
||||
import { genericCompletion } from "../genericCompletion";
|
||||
|
||||
export const completions = genericCompletion({
|
||||
options: [
|
||||
{ label: 'http://', type: 'constant' },
|
||||
{ label: 'https://', type: 'constant' },
|
||||
{ label: "http://", type: "constant" },
|
||||
{ label: "https://", type: "constant" },
|
||||
],
|
||||
minMatch: 1,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { parser } from './url';
|
||||
import { LanguageSupport, LRLanguage } from "@codemirror/language";
|
||||
import { parser } from "./url";
|
||||
|
||||
const urlLanguage = LRLanguage.define({
|
||||
name: 'url',
|
||||
name: "url",
|
||||
parser,
|
||||
languageData: {},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { styleTags, tags as t } from '@lezer/highlight';
|
||||
import { styleTags, tags as t } from "@lezer/highlight";
|
||||
|
||||
export const highlight = styleTags({
|
||||
Protocol: t.comment,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import { LRParser } from '@lezer/lr';
|
||||
import { highlight } from './highlight';
|
||||
import { LRParser } from "@lezer/lr";
|
||||
import { highlight } from "./highlight";
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states:
|
||||
"!|OQOPOOQYOPOOOTOPOOObOQO'#CdOjOPO'#C`OuOSO'#CcQOOOOOQ]OPOOOOOO,59O,59OOOOO-E6b-E6bOzOPO,58}O!SOSO'#CeO!XOPO1G.iOOOO,59P,59POOOO-E6c-E6c",
|
||||
stateData: '!g~OQQORPO~OZRO[TO~OTWOUWO~OZROYSX[SX~O]YO~O^ZOYVa~O]]O~O^ZOYVi~OQRTUT~',
|
||||
goto: 'nYPPPPZPP^bhRVPTUPVQSPRXSQ[YR^[',
|
||||
nodeNames: '⚠ url Protocol Host Path Placeholder PathSegment Query',
|
||||
stateData: "!g~OQQORPO~OZRO[TO~OTWOUWO~OZROYSX[SX~O]YO~O^ZOYVa~O]]O~O^ZOYVi~OQRTUT~",
|
||||
goto: "nYPPPPZPP^bhRVPTUPVQSPRXSQ[YR^[",
|
||||
nodeNames: "⚠ url Protocol Host Path Placeholder PathSegment Query",
|
||||
maxTerm: 14,
|
||||
propSources: [highlight],
|
||||
skippedNodes: [0],
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import type { Virtualizer } from '@tanstack/react-virtual';
|
||||
import { format } from 'date-fns';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useEventViewerKeyboard } from '../../hooks/useEventViewerKeyboard';
|
||||
import { CopyIconButton } from '../CopyIconButton';
|
||||
import { AutoScroller } from './AutoScroller';
|
||||
import { Banner } from './Banner';
|
||||
import { Button } from './Button';
|
||||
import { Separator } from './Separator';
|
||||
import { SplitLayout } from './SplitLayout';
|
||||
import { HStack } from './Stacks';
|
||||
import { IconButton } from './IconButton';
|
||||
import classNames from 'classnames';
|
||||
import type { Virtualizer } from "@tanstack/react-virtual";
|
||||
import { format } from "date-fns";
|
||||
import type { ReactNode } from "react";
|
||||
import { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { useEventViewerKeyboard } from "../../hooks/useEventViewerKeyboard";
|
||||
import { CopyIconButton } from "../CopyIconButton";
|
||||
import { AutoScroller } from "./AutoScroller";
|
||||
import { Banner } from "./Banner";
|
||||
import { Button } from "./Button";
|
||||
import { Separator } from "./Separator";
|
||||
import { SplitLayout } from "./SplitLayout";
|
||||
import { HStack } from "./Stacks";
|
||||
import { IconButton } from "./IconButton";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface EventViewerProps<T> {
|
||||
/** Array of events to display */
|
||||
@@ -70,8 +70,8 @@ export function EventViewer<T>({
|
||||
defaultRatio = 0.4,
|
||||
enableKeyboardNav = true,
|
||||
isLoading = false,
|
||||
loadingMessage = 'Loading events...',
|
||||
emptyMessage = 'No events recorded',
|
||||
loadingMessage = "Loading events...",
|
||||
emptyMessage = "No events recorded",
|
||||
onActiveIndexChange,
|
||||
}: EventViewerProps<T>) {
|
||||
const [activeIndex, setActiveIndexInternal] = useState<number | null>(null);
|
||||
@@ -82,7 +82,7 @@ export function EventViewer<T>({
|
||||
(indexOrUpdater: number | null | ((prev: number | null) => number | null)) => {
|
||||
setActiveIndexInternal((prev) => {
|
||||
const newIndex =
|
||||
typeof indexOrUpdater === 'function' ? indexOrUpdater(prev) : indexOrUpdater;
|
||||
typeof indexOrUpdater === "function" ? indexOrUpdater(prev) : indexOrUpdater;
|
||||
onActiveIndexChange?.(newIndex);
|
||||
return newIndex;
|
||||
});
|
||||
@@ -129,7 +129,7 @@ export function EventViewer<T>({
|
||||
setIsPanelOpen(true);
|
||||
// Scroll to ensure selected item is visible after panel opens
|
||||
requestAnimationFrame(() => {
|
||||
virtualizerRef.current?.scrollToIndex(index, { align: 'auto' });
|
||||
virtualizerRef.current?.scrollToIndex(index, { align: "auto" });
|
||||
});
|
||||
},
|
||||
[setActiveIndex],
|
||||
@@ -189,7 +189,11 @@ export function EventViewer<T>({
|
||||
<Separator />
|
||||
</div>
|
||||
<div className="mx-2 overflow-y-auto">
|
||||
{renderDetail({ event: activeEvent, index: activeIndex ?? 0, onClose: handleClose })}
|
||||
{renderDetail({
|
||||
event: activeEvent,
|
||||
index: activeIndex ?? 0,
|
||||
onClose: handleClose,
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
@@ -228,7 +232,7 @@ export function EventDetailHeader({
|
||||
copyText,
|
||||
onClose,
|
||||
}: EventDetailHeaderProps) {
|
||||
const formattedTime = timestamp ? format(new Date(`${timestamp}Z`), 'HH:mm:ss.SSS') : null;
|
||||
const formattedTime = timestamp ? format(new Date(`${timestamp}Z`), "HH:mm:ss.SSS") : null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between gap-2 mb-2 h-xs">
|
||||
@@ -249,8 +253,21 @@ export function EventDetailHeader({
|
||||
{formattedTime && (
|
||||
<span className="text-text-subtlest font-mono text-editor ml-2">{formattedTime}</span>
|
||||
)}
|
||||
<div className={classNames(copyText != null || formattedTime || (actions ?? []).length > 0 && "border-l border-l-surface-highlight ml-2 pl-3")}>
|
||||
<IconButton color="custom" className="text-text-subtle -mr-3" size="xs" icon="x" title="Close event panel" onClick={onClose} />
|
||||
<div
|
||||
className={classNames(
|
||||
copyText != null ||
|
||||
formattedTime ||
|
||||
((actions ?? []).length > 0 && "border-l border-l-surface-highlight ml-2 pl-3"),
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
color="custom"
|
||||
className="text-text-subtle -mr-3"
|
||||
size="xs"
|
||||
icon="x"
|
||||
title="Close event panel"
|
||||
onClick={onClose}
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import { format } from 'date-fns';
|
||||
import type { ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import { format } from "date-fns";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface EventViewerRowProps {
|
||||
isActive: boolean;
|
||||
@@ -23,15 +23,15 @@ export function EventViewerRow({
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'w-full grid grid-cols-[auto_minmax(0,1fr)_auto] gap-2 items-center text-left',
|
||||
'px-1.5 h-xs font-mono text-editor cursor-default group focus:outline-none focus:text-text rounded',
|
||||
isActive && 'bg-surface-active !text-text',
|
||||
'text-text-subtle hover:text',
|
||||
"w-full grid grid-cols-[auto_minmax(0,1fr)_auto] gap-2 items-center text-left",
|
||||
"px-1.5 h-xs font-mono text-editor cursor-default group focus:outline-none focus:text-text rounded",
|
||||
isActive && "bg-surface-active !text-text",
|
||||
"text-text-subtle hover:text",
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
<div className="w-full truncate">{content}</div>
|
||||
{timestamp && <div className="opacity-50">{format(`${timestamp}Z`, 'HH:mm:ss.SSS')}</div>}
|
||||
{timestamp && <div className="opacity-50">{format(`${timestamp}Z`, "HH:mm:ss.SSS")}</div>}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
@@ -11,10 +11,10 @@ export function FormattedError({ children, className }: Props) {
|
||||
<pre
|
||||
className={classNames(
|
||||
className,
|
||||
'cursor-text select-auto',
|
||||
'[&_*]:cursor-text [&_*]:select-auto',
|
||||
'font-mono text-sm w-full bg-surface-highlight p-3 rounded',
|
||||
'whitespace-pre-wrap border border-danger border-dashed overflow-x-auto',
|
||||
"cursor-text select-auto",
|
||||
"[&_*]:cursor-text [&_*]:select-auto",
|
||||
"font-mono text-sm w-full bg-surface-highlight p-3 rounded",
|
||||
"whitespace-pre-wrap border border-danger border-dashed overflow-x-auto",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLHeadingElement> {
|
||||
level?: 1 | 2 | 3;
|
||||
}
|
||||
|
||||
export function Heading({ className, level = 1, ...props }: Props) {
|
||||
const Component = level === 1 ? 'h1' : level === 2 ? 'h2' : 'h3';
|
||||
const Component = level === 1 ? "h1" : level === 2 ? "h2" : "h3";
|
||||
return (
|
||||
<Component
|
||||
className={classNames(
|
||||
className,
|
||||
'font-semibold text-text',
|
||||
level === 1 && 'text-2xl',
|
||||
level === 2 && 'text-xl',
|
||||
level === 3 && 'text-lg',
|
||||
"font-semibold text-text",
|
||||
level === 1 && "text-2xl",
|
||||
level === 2 && "text-xl",
|
||||
level === 3 && "text-lg",
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useFormattedHotkey } from '../../hooks/useHotKey';
|
||||
import { HStack } from './Stacks';
|
||||
import classNames from "classnames";
|
||||
import type { HotkeyAction } from "../../hooks/useHotKey";
|
||||
import { useFormattedHotkey } from "../../hooks/useHotKey";
|
||||
import { HStack } from "./Stacks";
|
||||
|
||||
interface Props {
|
||||
action: HotkeyAction | null;
|
||||
className?: string;
|
||||
variant?: 'text' | 'with-bg';
|
||||
variant?: "text" | "with-bg";
|
||||
}
|
||||
|
||||
export function Hotkey({ action, className, variant }: Props) {
|
||||
@@ -21,7 +21,7 @@ export function Hotkey({ action, className, variant }: Props) {
|
||||
interface HotkeyRawProps {
|
||||
labelParts: string[];
|
||||
className?: string;
|
||||
variant?: 'text' | 'with-bg';
|
||||
variant?: "text" | "with-bg";
|
||||
}
|
||||
|
||||
export function HotkeyRaw({ labelParts, className, variant }: HotkeyRawProps) {
|
||||
@@ -29,9 +29,9 @@ export function HotkeyRaw({ labelParts, className, variant }: HotkeyRawProps) {
|
||||
<HStack
|
||||
className={classNames(
|
||||
className,
|
||||
variant === 'with-bg' &&
|
||||
'rounded bg-surface-highlight px-1 border border-border text-text-subtle',
|
||||
variant === 'text' && 'text-text-subtlest',
|
||||
variant === "with-bg" &&
|
||||
"rounded bg-surface-highlight px-1 border border-border text-text-subtle",
|
||||
variant === "text" && "text-text-subtlest",
|
||||
)}
|
||||
>
|
||||
{labelParts.map((char, index) => (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useHotkeyLabel } from '../../hooks/useHotKey';
|
||||
import classNames from "classnames";
|
||||
import type { HotkeyAction } from "../../hooks/useHotKey";
|
||||
import { useHotkeyLabel } from "../../hooks/useHotKey";
|
||||
|
||||
interface Props {
|
||||
action: HotkeyAction;
|
||||
@@ -10,6 +10,6 @@ interface Props {
|
||||
export function HotkeyLabel({ action, className }: Props) {
|
||||
const label = useHotkeyLabel(action);
|
||||
return (
|
||||
<span className={classNames(className, 'text-text-subtle whitespace-nowrap')}>{label}</span>
|
||||
<span className={classNames(className, "text-text-subtle whitespace-nowrap")}>{label}</span>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { Hotkey } from './Hotkey';
|
||||
import { HotkeyLabel } from './HotkeyLabel';
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import { Fragment } from "react";
|
||||
import type { HotkeyAction } from "../../hooks/useHotKey";
|
||||
import { Hotkey } from "./Hotkey";
|
||||
import { HotkeyLabel } from "./HotkeyLabel";
|
||||
|
||||
interface Props {
|
||||
hotkeys: HotkeyAction[];
|
||||
@@ -13,7 +13,7 @@ interface Props {
|
||||
|
||||
export const HotkeyList = ({ hotkeys, bottomSlot, className }: Props) => {
|
||||
return (
|
||||
<div className={classNames(className, 'h-full flex items-center justify-center')}>
|
||||
<div className={classNames(className, "h-full flex items-center justify-center")}>
|
||||
<div className="grid gap-2 grid-cols-[auto_auto]">
|
||||
{hotkeys.map((hotkey) => (
|
||||
<Fragment key={hotkey}>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type { GrpcRequest, HttpRequest, WebsocketRequest } from '@yaakapp-internal/models';
|
||||
import { settingsAtom } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { memo } from 'react';
|
||||
import type { GrpcRequest, HttpRequest, WebsocketRequest } from "@yaakapp-internal/models";
|
||||
import { settingsAtom } from "@yaakapp-internal/models";
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo } from "react";
|
||||
|
||||
interface Props {
|
||||
request: HttpRequest | GrpcRequest | WebsocketRequest;
|
||||
@@ -12,27 +12,32 @@ interface Props {
|
||||
}
|
||||
|
||||
const methodNames: Record<string, string> = {
|
||||
get: 'GET',
|
||||
put: 'PUT',
|
||||
post: 'POST',
|
||||
patch: 'PTCH',
|
||||
delete: 'DELE',
|
||||
options: 'OPTN',
|
||||
head: 'HEAD',
|
||||
query: 'QURY',
|
||||
graphql: 'GQL',
|
||||
grpc: 'GRPC',
|
||||
websocket: 'WS',
|
||||
get: "GET",
|
||||
put: "PUT",
|
||||
post: "POST",
|
||||
patch: "PTCH",
|
||||
delete: "DELE",
|
||||
options: "OPTN",
|
||||
head: "HEAD",
|
||||
query: "QURY",
|
||||
graphql: "GQL",
|
||||
grpc: "GRPC",
|
||||
websocket: "WS",
|
||||
};
|
||||
|
||||
export const HttpMethodTag = memo(function HttpMethodTag({ request, className, short, noAlias }: Props) {
|
||||
export const HttpMethodTag = memo(function HttpMethodTag({
|
||||
request,
|
||||
className,
|
||||
short,
|
||||
noAlias,
|
||||
}: Props) {
|
||||
const method =
|
||||
request.model === 'http_request' && (request.bodyType === 'graphql' && !noAlias)
|
||||
? 'graphql'
|
||||
: request.model === 'grpc_request'
|
||||
? 'grpc'
|
||||
: request.model === 'websocket_request'
|
||||
? 'websocket'
|
||||
request.model === "http_request" && request.bodyType === "graphql" && !noAlias
|
||||
? "graphql"
|
||||
: request.model === "grpc_request"
|
||||
? "grpc"
|
||||
: request.model === "websocket_request"
|
||||
? "websocket"
|
||||
: request.method;
|
||||
|
||||
return <HttpMethodTagRaw method={method} className={className} short={short} />;
|
||||
@@ -52,7 +57,7 @@ export function HttpMethodTagRaw({
|
||||
let label = method.toUpperCase();
|
||||
if (short) {
|
||||
label = methodNames[method.toLowerCase()] ?? method.slice(0, 4);
|
||||
label = label.padStart(4, ' ');
|
||||
label = label.padStart(4, " ");
|
||||
}
|
||||
|
||||
const m = method.toUpperCase();
|
||||
@@ -64,20 +69,20 @@ export function HttpMethodTagRaw({
|
||||
<span
|
||||
className={classNames(
|
||||
className,
|
||||
!colored && 'text-text-subtle',
|
||||
colored && m === 'GRAPHQL' && 'text-info',
|
||||
colored && m === 'WEBSOCKET' && 'text-info',
|
||||
colored && m === 'GRPC' && 'text-info',
|
||||
colored && m === 'QUERY' && 'text-text-subtle',
|
||||
colored && m === 'OPTIONS' && 'text-info',
|
||||
colored && m === 'HEAD' && 'text-text-subtle',
|
||||
colored && m === 'GET' && 'text-primary',
|
||||
colored && m === 'PUT' && 'text-warning',
|
||||
colored && m === 'PATCH' && 'text-notice',
|
||||
colored && m === 'POST' && 'text-success',
|
||||
colored && m === 'DELETE' && 'text-danger',
|
||||
'font-mono flex-shrink-0 whitespace-pre',
|
||||
'pt-[0.15em]', // Fix for monospace font not vertically centering
|
||||
!colored && "text-text-subtle",
|
||||
colored && m === "GRAPHQL" && "text-info",
|
||||
colored && m === "WEBSOCKET" && "text-info",
|
||||
colored && m === "GRPC" && "text-info",
|
||||
colored && m === "QUERY" && "text-text-subtle",
|
||||
colored && m === "OPTIONS" && "text-info",
|
||||
colored && m === "HEAD" && "text-text-subtle",
|
||||
colored && m === "GET" && "text-primary",
|
||||
colored && m === "PUT" && "text-warning",
|
||||
colored && m === "PATCH" && "text-notice",
|
||||
colored && m === "POST" && "text-success",
|
||||
colored && m === "DELETE" && "text-danger",
|
||||
"font-mono flex-shrink-0 whitespace-pre",
|
||||
"pt-[0.15em]", // Fix for monospace font not vertically centering
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HttpResponse } from '@yaakapp-internal/models';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import type { HttpResponse } from "@yaakapp-internal/models";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
@@ -12,17 +12,17 @@ export function HttpResponseDurationTag({ response }: Props) {
|
||||
// Calculate the duration of the response for use when the response hasn't finished yet
|
||||
useEffect(() => {
|
||||
clearInterval(timeout.current);
|
||||
if (response.state === 'closed') return;
|
||||
if (response.state === "closed") return;
|
||||
timeout.current = setInterval(() => {
|
||||
setFallbackElapsed(Date.now() - new Date(`${response.createdAt}Z`).getTime());
|
||||
}, 100);
|
||||
return () => clearInterval(timeout.current);
|
||||
}, [response.createdAt, response.state]);
|
||||
|
||||
const dnsValue = response.elapsedDns > 0 ? formatMillis(response.elapsedDns) : '--';
|
||||
const dnsValue = response.elapsedDns > 0 ? formatMillis(response.elapsedDns) : "--";
|
||||
const title = `DNS: ${dnsValue}\nHEADER: ${formatMillis(response.elapsedHeaders)}\nTOTAL: ${formatMillis(response.elapsed)}`;
|
||||
|
||||
const elapsed = response.state === 'closed' ? response.elapsed : fallbackElapsed;
|
||||
const elapsed = response.state === "closed" ? response.elapsed : fallbackElapsed;
|
||||
|
||||
return (
|
||||
<span className="font-mono" title={title}>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { HttpResponse, HttpResponseState } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import type { HttpResponse, HttpResponseState } from "@yaakapp-internal/models";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
@@ -20,35 +20,35 @@ export function HttpStatusTagRaw({
|
||||
showReason,
|
||||
statusReason,
|
||||
short,
|
||||
}: Omit<Props, 'response'> & {
|
||||
}: Omit<Props, "response"> & {
|
||||
status: number | string;
|
||||
state?: HttpResponseState;
|
||||
statusReason?: string | null;
|
||||
}) {
|
||||
let colorClass: string;
|
||||
let label = `${status}`;
|
||||
const statusN = typeof status === 'number' ? status : parseInt(status, 10);
|
||||
const statusN = typeof status === "number" ? status : parseInt(status, 10);
|
||||
|
||||
if (state === 'initialized') {
|
||||
label = short ? 'CONN' : 'CONNECTING';
|
||||
colorClass = 'text-text-subtle';
|
||||
if (state === "initialized") {
|
||||
label = short ? "CONN" : "CONNECTING";
|
||||
colorClass = "text-text-subtle";
|
||||
} else if (statusN < 100) {
|
||||
label = short ? 'ERR' : 'ERROR';
|
||||
colorClass = 'text-danger';
|
||||
label = short ? "ERR" : "ERROR";
|
||||
colorClass = "text-danger";
|
||||
} else if (statusN < 200) {
|
||||
colorClass = 'text-info';
|
||||
colorClass = "text-info";
|
||||
} else if (statusN < 300) {
|
||||
colorClass = 'text-success';
|
||||
colorClass = "text-success";
|
||||
} else if (statusN < 400) {
|
||||
colorClass = 'text-primary';
|
||||
colorClass = "text-primary";
|
||||
} else if (statusN < 500) {
|
||||
colorClass = 'text-warning';
|
||||
colorClass = "text-warning";
|
||||
} else {
|
||||
colorClass = 'text-danger';
|
||||
colorClass = "text-danger";
|
||||
}
|
||||
|
||||
return (
|
||||
<span className={classNames(className, 'font-mono min-w-0', colorClass)}>
|
||||
<span className={classNames(className, "font-mono min-w-0", colorClass)}>
|
||||
{label} {showReason && statusReason}
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import type { Color } from "@yaakapp-internal/plugins";
|
||||
import classNames from "classnames";
|
||||
import {
|
||||
AlarmClockIcon,
|
||||
AlertTriangleIcon,
|
||||
@@ -134,9 +134,9 @@ import {
|
||||
WifiIcon,
|
||||
WrenchIcon,
|
||||
XIcon,
|
||||
} from 'lucide-react';
|
||||
import type { CSSProperties, HTMLAttributes } from 'react';
|
||||
import { memo } from 'react';
|
||||
} from "lucide-react";
|
||||
import type { CSSProperties, HTMLAttributes } from "react";
|
||||
import { memo } from "react";
|
||||
|
||||
const icons = {
|
||||
alarm_clock: AlarmClockIcon,
|
||||
@@ -281,17 +281,17 @@ export interface IconProps {
|
||||
icon: keyof typeof icons;
|
||||
className?: string;
|
||||
style?: CSSProperties;
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
size?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl";
|
||||
spin?: boolean;
|
||||
title?: string;
|
||||
color?: Color | 'custom' | 'default';
|
||||
color?: Color | "custom" | "default";
|
||||
}
|
||||
|
||||
export const Icon = memo(function Icon({
|
||||
icon,
|
||||
color = 'default',
|
||||
color = "default",
|
||||
spin,
|
||||
size = 'md',
|
||||
size = "md",
|
||||
style,
|
||||
className,
|
||||
title,
|
||||
@@ -303,23 +303,23 @@ export const Icon = memo(function Icon({
|
||||
title={title}
|
||||
className={classNames(
|
||||
className,
|
||||
!spin && 'transform-gpu',
|
||||
spin && 'animate-spin',
|
||||
'flex-shrink-0',
|
||||
size === 'xl' && 'h-6 w-6',
|
||||
size === 'lg' && 'h-5 w-5',
|
||||
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',
|
||||
color === 'default' && 'inherit',
|
||||
color === 'danger' && 'text-danger',
|
||||
color === 'warning' && 'text-warning',
|
||||
color === 'notice' && 'text-notice',
|
||||
color === 'info' && 'text-info',
|
||||
color === 'success' && 'text-success',
|
||||
color === 'primary' && 'text-primary',
|
||||
color === 'secondary' && 'text-secondary',
|
||||
!spin && "transform-gpu",
|
||||
spin && "animate-spin",
|
||||
"flex-shrink-0",
|
||||
size === "xl" && "h-6 w-6",
|
||||
size === "lg" && "h-5 w-5",
|
||||
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",
|
||||
color === "default" && "inherit",
|
||||
color === "danger" && "text-danger",
|
||||
color === "warning" && "text-warning",
|
||||
color === "notice" && "text-notice",
|
||||
color === "info" && "text-info",
|
||||
color === "success" && "text-success",
|
||||
color === "primary" && "text-primary",
|
||||
color === "secondary" && "text-secondary",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import classNames from 'classnames';
|
||||
import type { MouseEvent } from 'react';
|
||||
import { forwardRef, useCallback } from 'react';
|
||||
import { useTimedBoolean } from '../../hooks/useTimedBoolean';
|
||||
import type { ButtonProps } from './Button';
|
||||
import { Button } from './Button';
|
||||
import type { IconProps } from './Icon';
|
||||
import { Icon } from './Icon';
|
||||
import { LoadingIcon } from './LoadingIcon';
|
||||
import classNames from "classnames";
|
||||
import type { MouseEvent } from "react";
|
||||
import { forwardRef, useCallback } from "react";
|
||||
import { useTimedBoolean } from "../../hooks/useTimedBoolean";
|
||||
import type { ButtonProps } from "./Button";
|
||||
import { Button } from "./Button";
|
||||
import type { IconProps } from "./Icon";
|
||||
import { Icon } from "./Icon";
|
||||
import { LoadingIcon } from "./LoadingIcon";
|
||||
|
||||
export type IconButtonProps = IconProps &
|
||||
ButtonProps & {
|
||||
showConfirm?: boolean;
|
||||
iconClassName?: string;
|
||||
iconSize?: IconProps['size'];
|
||||
iconColor?: IconProps['color'];
|
||||
iconSize?: IconProps["size"];
|
||||
iconColor?: IconProps["color"];
|
||||
title: string;
|
||||
showBadge?: boolean;
|
||||
};
|
||||
@@ -22,18 +22,18 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
|
||||
{
|
||||
showConfirm,
|
||||
icon,
|
||||
color = 'default',
|
||||
color = "default",
|
||||
spin,
|
||||
onClick,
|
||||
className,
|
||||
iconClassName,
|
||||
tabIndex,
|
||||
size = 'md',
|
||||
size = "md",
|
||||
iconSize,
|
||||
showBadge,
|
||||
iconColor,
|
||||
isLoading,
|
||||
type = 'button',
|
||||
type = "button",
|
||||
...props
|
||||
}: IconButtonProps,
|
||||
ref,
|
||||
@@ -50,9 +50,9 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
|
||||
return (
|
||||
<Button
|
||||
ref={ref}
|
||||
aria-hidden={icon === 'empty'}
|
||||
disabled={icon === 'empty'}
|
||||
tabIndex={(tabIndex ?? icon === 'empty') ? -1 : undefined}
|
||||
aria-hidden={icon === "empty"}
|
||||
disabled={icon === "empty"}
|
||||
tabIndex={(tabIndex ?? icon === "empty") ? -1 : undefined}
|
||||
onClick={handleClick}
|
||||
innerClassName="flex items-center justify-center"
|
||||
size={size}
|
||||
@@ -60,12 +60,12 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
|
||||
type={type}
|
||||
className={classNames(
|
||||
className,
|
||||
'group/button relative flex-shrink-0',
|
||||
'!px-0',
|
||||
size === 'md' && 'w-md',
|
||||
size === 'sm' && 'w-sm',
|
||||
size === 'xs' && 'w-xs',
|
||||
size === '2xs' && 'w-5',
|
||||
"group/button relative flex-shrink-0",
|
||||
"!px-0",
|
||||
size === "md" && "w-md",
|
||||
size === "sm" && "w-sm",
|
||||
size === "xs" && "w-xs",
|
||||
size === "2xs" && "w-5",
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
@@ -79,14 +79,14 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
|
||||
) : (
|
||||
<Icon
|
||||
size={iconSize}
|
||||
icon={confirmed ? 'check' : icon}
|
||||
icon={confirmed ? "check" : icon}
|
||||
spin={spin}
|
||||
color={iconColor}
|
||||
className={classNames(
|
||||
iconClassName,
|
||||
'group-hover/button:text-text',
|
||||
confirmed && '!text-success', // Don't use Icon.color here because it won't override the hover color
|
||||
props.disabled && 'opacity-70',
|
||||
"group-hover/button:text-text",
|
||||
confirmed && "!text-success", // Don't use Icon.color here because it won't override the hover color
|
||||
props.disabled && "opacity-70",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import type { IconProps } from './Icon';
|
||||
import { Icon } from './Icon';
|
||||
import type { TooltipProps } from './Tooltip';
|
||||
import { Tooltip } from './Tooltip';
|
||||
import type { IconProps } from "./Icon";
|
||||
import { Icon } from "./Icon";
|
||||
import type { TooltipProps } from "./Tooltip";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
type Props = Omit<TooltipProps, 'children'> & {
|
||||
icon?: IconProps['icon'];
|
||||
iconSize?: IconProps['size'];
|
||||
iconColor?: IconProps['color'];
|
||||
type Props = Omit<TooltipProps, "children"> & {
|
||||
icon?: IconProps["icon"];
|
||||
iconSize?: IconProps["size"];
|
||||
iconColor?: IconProps["color"];
|
||||
className?: string;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export function IconTooltip({
|
||||
content,
|
||||
icon = 'info',
|
||||
icon = "info",
|
||||
iconColor,
|
||||
iconSize,
|
||||
...tooltipProps
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { HTMLAttributes } from "react";
|
||||
|
||||
export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
|
||||
return (
|
||||
<code
|
||||
className={classNames(
|
||||
className,
|
||||
'font-mono text-shrink bg-surface-highlight border border-border-subtle flex-grow-0',
|
||||
'px-1.5 py-0.5 rounded text shadow-inner break-words',
|
||||
"font-mono text-shrink bg-surface-highlight border border-border-subtle flex-grow-0",
|
||||
"px-1.5 py-0.5 rounded text shadow-inner break-words",
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -1,47 +1,47 @@
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { createFastMutation } from '../../hooks/useFastMutation';
|
||||
import { useIsEncryptionEnabled } from '../../hooks/useIsEncryptionEnabled';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { copyToClipboard } from '../../lib/copy';
|
||||
import type { EditorView } from "@codemirror/view";
|
||||
import type { Color } from "@yaakapp-internal/plugins";
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { createFastMutation } from "../../hooks/useFastMutation";
|
||||
import { useIsEncryptionEnabled } from "../../hooks/useIsEncryptionEnabled";
|
||||
import { useStateWithDeps } from "../../hooks/useStateWithDeps";
|
||||
import { copyToClipboard } from "../../lib/copy";
|
||||
import {
|
||||
analyzeTemplate,
|
||||
convertTemplateToInsecure,
|
||||
convertTemplateToSecure,
|
||||
} from '../../lib/encryption';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
} from "../../lib/encryption";
|
||||
import { generateId } from "../../lib/generateId";
|
||||
import {
|
||||
setupOrConfigureEncryption,
|
||||
withEncryptionEnabled,
|
||||
} from '../../lib/setupOrConfigureEncryption';
|
||||
import { Button } from './Button';
|
||||
import type { DropdownItem } from './Dropdown';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import type { EditorProps } from './Editor/Editor';
|
||||
import { Editor } from './Editor/LazyEditor';
|
||||
import type { IconProps } from './Icon';
|
||||
import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
import { IconTooltip } from './IconTooltip';
|
||||
import { Label } from './Label';
|
||||
import { HStack } from './Stacks';
|
||||
} from "../../lib/setupOrConfigureEncryption";
|
||||
import { Button } from "./Button";
|
||||
import type { DropdownItem } from "./Dropdown";
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import type { EditorProps } from "./Editor/Editor";
|
||||
import { Editor } from "./Editor/LazyEditor";
|
||||
import type { IconProps } from "./Icon";
|
||||
import { Icon } from "./Icon";
|
||||
import { IconButton } from "./IconButton";
|
||||
import { IconTooltip } from "./IconTooltip";
|
||||
import { Label } from "./Label";
|
||||
import { HStack } from "./Stacks";
|
||||
|
||||
export type InputProps = Pick<
|
||||
EditorProps,
|
||||
| 'language'
|
||||
| 'autocomplete'
|
||||
| 'forcedEnvironmentId'
|
||||
| 'forceUpdateKey'
|
||||
| 'disabled'
|
||||
| 'autoFocus'
|
||||
| 'autoSelect'
|
||||
| 'autocompleteVariables'
|
||||
| 'autocompleteFunctions'
|
||||
| 'onKeyDown'
|
||||
| 'readOnly'
|
||||
| "language"
|
||||
| "autocomplete"
|
||||
| "forcedEnvironmentId"
|
||||
| "forceUpdateKey"
|
||||
| "disabled"
|
||||
| "autoFocus"
|
||||
| "autoSelect"
|
||||
| "autocompleteVariables"
|
||||
| "autocompleteFunctions"
|
||||
| "onKeyDown"
|
||||
| "readOnly"
|
||||
> & {
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
@@ -53,7 +53,7 @@ export type InputProps = Pick<
|
||||
help?: ReactNode;
|
||||
label: ReactNode;
|
||||
labelClassName?: string;
|
||||
labelPosition?: 'top' | 'left';
|
||||
labelPosition?: "top" | "left";
|
||||
leftSlot?: ReactNode;
|
||||
multiLine?: boolean;
|
||||
name?: string;
|
||||
@@ -61,15 +61,15 @@ export type InputProps = Pick<
|
||||
onChange?: (value: string) => void;
|
||||
onFocus?: () => void;
|
||||
onPaste?: (value: string) => void;
|
||||
onPasteOverwrite?: EditorProps['onPasteOverwrite'];
|
||||
onPasteOverwrite?: EditorProps["onPasteOverwrite"];
|
||||
placeholder?: string;
|
||||
required?: boolean;
|
||||
rightSlot?: ReactNode;
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md' | 'auto';
|
||||
stateKey: EditorProps['stateKey'];
|
||||
extraExtensions?: EditorProps['extraExtensions'];
|
||||
size?: "2xs" | "xs" | "sm" | "md" | "auto";
|
||||
stateKey: EditorProps["stateKey"];
|
||||
extraExtensions?: EditorProps["extraExtensions"];
|
||||
tint?: Color;
|
||||
type?: 'text' | 'password';
|
||||
type?: "text" | "password";
|
||||
validate?: boolean | ((v: string) => boolean);
|
||||
wrapLines?: boolean;
|
||||
setRef?: (h: InputHandle | null) => void;
|
||||
@@ -80,13 +80,13 @@ export interface InputHandle {
|
||||
isFocused: () => boolean;
|
||||
value: () => string;
|
||||
selectAll: () => void;
|
||||
dispatch: EditorView['dispatch'];
|
||||
dispatch: EditorView["dispatch"];
|
||||
}
|
||||
|
||||
export function Input({ type, ...props }: InputProps) {
|
||||
// If it's a password and template functions are supported (ie. secure(...)) then
|
||||
// use the encrypted input component.
|
||||
if (type === 'password' && props.autocompleteFunctions) {
|
||||
if (type === "password" && props.autocompleteFunctions) {
|
||||
return <EncryptionInput {...props} />;
|
||||
}
|
||||
return <BaseInput type={type} {...props} />;
|
||||
@@ -105,7 +105,7 @@ function BaseInput({
|
||||
inputWrapperClassName,
|
||||
label,
|
||||
labelClassName,
|
||||
labelPosition = 'top',
|
||||
labelPosition = "top",
|
||||
leftSlot,
|
||||
multiLine,
|
||||
onBlur,
|
||||
@@ -117,17 +117,17 @@ function BaseInput({
|
||||
readOnly,
|
||||
required,
|
||||
rightSlot,
|
||||
size = 'md',
|
||||
size = "md",
|
||||
stateKey,
|
||||
tint,
|
||||
type = 'text',
|
||||
type = "text",
|
||||
validate,
|
||||
wrapLines,
|
||||
setRef,
|
||||
...props
|
||||
}: InputProps) {
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
|
||||
const [obscured, setObscured] = useStateWithDeps(type === "password", [type]);
|
||||
const [hasChanged, setHasChanged] = useStateWithDeps<boolean>(false, [forceUpdateKey]);
|
||||
const editorRef = useRef<EditorView | null>(null);
|
||||
const skipNextFocus = useRef<boolean>(false);
|
||||
@@ -142,7 +142,7 @@ function BaseInput({
|
||||
editorRef.current.dispatch({ selection: { anchor, head: anchor }, scrollIntoView: true });
|
||||
},
|
||||
isFocused: () => editorRef.current?.hasFocus ?? false,
|
||||
value: () => editorRef.current?.state.doc.toString() ?? '',
|
||||
value: () => editorRef.current?.state.doc.toString() ?? "",
|
||||
dispatch: (...args) => {
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
editorRef.current?.dispatch(...(args as any));
|
||||
@@ -170,9 +170,9 @@ function BaseInput({
|
||||
const fn = () => {
|
||||
skipNextFocus.current = true;
|
||||
};
|
||||
window.addEventListener('focus', fn);
|
||||
window.addEventListener("focus", fn);
|
||||
return () => {
|
||||
window.removeEventListener('focus', fn);
|
||||
window.removeEventListener("focus", fn);
|
||||
};
|
||||
}, []);
|
||||
|
||||
@@ -203,13 +203,13 @@ function BaseInput({
|
||||
const id = useRef(`input-${generateId()}`);
|
||||
const editorClassName = classNames(
|
||||
className,
|
||||
'!bg-transparent min-w-0 h-auto w-full focus:outline-none placeholder:text-placeholder',
|
||||
"!bg-transparent min-w-0 h-auto w-full focus:outline-none placeholder:text-placeholder",
|
||||
);
|
||||
|
||||
const isValid = useMemo(() => {
|
||||
if (required && !validateRequire(defaultValue ?? '')) return false;
|
||||
if (typeof validate === 'boolean') return validate;
|
||||
if (typeof validate === 'function' && !validate(defaultValue ?? '')) return false;
|
||||
if (required && !validateRequire(defaultValue ?? "")) return false;
|
||||
if (typeof validate === "boolean") return validate;
|
||||
if (typeof validate === "function" && !validate(defaultValue ?? "")) return false;
|
||||
return true;
|
||||
}, [required, defaultValue, validate]);
|
||||
|
||||
@@ -226,12 +226,12 @@ function BaseInput({
|
||||
// Submit the nearest form on Enter key press
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key !== 'Enter') return;
|
||||
if (e.key !== "Enter") return;
|
||||
|
||||
const form = wrapperRef.current?.closest('form');
|
||||
const form = wrapperRef.current?.closest("form");
|
||||
if (!isValid || form == null) return;
|
||||
|
||||
form?.dispatchEvent(new Event('submit', { cancelable: true, bubbles: true }));
|
||||
form?.dispatchEvent(new Event("submit", { cancelable: true, bubbles: true }));
|
||||
},
|
||||
[isValid],
|
||||
);
|
||||
@@ -240,11 +240,11 @@ function BaseInput({
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={classNames(
|
||||
'pointer-events-auto', // Just in case we're placing in disabled parent
|
||||
'w-full',
|
||||
fullHeight && 'h-full',
|
||||
labelPosition === 'left' && 'flex items-center gap-2',
|
||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||
"pointer-events-auto", // Just in case we're placing in disabled parent
|
||||
"w-full",
|
||||
fullHeight && "h-full",
|
||||
labelPosition === "left" && "flex items-center gap-2",
|
||||
labelPosition === "top" && "flex-row gap-0.5",
|
||||
)}
|
||||
>
|
||||
<Label
|
||||
@@ -260,31 +260,31 @@ function BaseInput({
|
||||
alignItems="stretch"
|
||||
className={classNames(
|
||||
containerClassName,
|
||||
fullHeight && 'h-full',
|
||||
'x-theme-input',
|
||||
'relative w-full rounded-md text overflow-hidden',
|
||||
'border',
|
||||
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',
|
||||
size === '2xs' && 'min-h-2xs',
|
||||
fullHeight && "h-full",
|
||||
"x-theme-input",
|
||||
"relative w-full rounded-md text overflow-hidden",
|
||||
"border",
|
||||
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",
|
||||
size === "2xs" && "min-h-2xs",
|
||||
)}
|
||||
>
|
||||
{tint != null && (
|
||||
<div
|
||||
aria-hidden
|
||||
className={classNames(
|
||||
'absolute inset-0 opacity-5 pointer-events-none',
|
||||
tint === 'primary' && 'bg-primary',
|
||||
tint === 'secondary' && 'bg-secondary',
|
||||
tint === 'info' && 'bg-info',
|
||||
tint === 'success' && 'bg-success',
|
||||
tint === 'notice' && 'bg-notice',
|
||||
tint === 'warning' && 'bg-warning',
|
||||
tint === 'danger' && 'bg-danger',
|
||||
"absolute inset-0 opacity-5 pointer-events-none",
|
||||
tint === "primary" && "bg-primary",
|
||||
tint === "secondary" && "bg-secondary",
|
||||
tint === "info" && "bg-info",
|
||||
tint === "success" && "bg-success",
|
||||
tint === "notice" && "bg-notice",
|
||||
tint === "warning" && "bg-warning",
|
||||
tint === "danger" && "bg-danger",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -292,10 +292,10 @@ function BaseInput({
|
||||
<HStack
|
||||
className={classNames(
|
||||
inputWrapperClassName,
|
||||
'w-full min-w-0 px-2',
|
||||
fullHeight && 'h-full',
|
||||
leftSlot ? 'pl-0.5 -ml-2' : null,
|
||||
rightSlot ? 'pr-0.5 -mr-2' : null,
|
||||
"w-full min-w-0 px-2",
|
||||
fullHeight && "h-full",
|
||||
leftSlot ? "pl-0.5 -ml-2" : null,
|
||||
rightSlot ? "pr-0.5 -mr-2" : null,
|
||||
)}
|
||||
>
|
||||
<Editor
|
||||
@@ -308,7 +308,7 @@ function BaseInput({
|
||||
wrapLines={wrapLines}
|
||||
heightMode="auto"
|
||||
onKeyDown={handleKeyDown}
|
||||
type={type === 'password' && !obscured ? 'text' : type}
|
||||
type={type === "password" && !obscured ? "text" : type}
|
||||
defaultValue={defaultValue}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
placeholder={placeholder}
|
||||
@@ -318,8 +318,8 @@ function BaseInput({
|
||||
disabled={disabled}
|
||||
className={classNames(
|
||||
editorClassName,
|
||||
multiLine && size === 'md' && 'py-1.5',
|
||||
multiLine && size === 'sm' && 'py-1',
|
||||
multiLine && size === "md" && "py-1.5",
|
||||
multiLine && size === "sm" && "py-1",
|
||||
)}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
@@ -327,11 +327,15 @@ function BaseInput({
|
||||
{...props}
|
||||
/>
|
||||
</HStack>
|
||||
{type === 'password' && !disableObscureToggle && (
|
||||
{type === "password" && !disableObscureToggle && (
|
||||
<IconButton
|
||||
title={obscured ? `Show ${typeof label === 'string' ? label : 'field'}` : `Obscure ${typeof label === 'string' ? label : 'field'}`}
|
||||
title={
|
||||
obscured
|
||||
? `Show ${typeof label === "string" ? label : "field"}`
|
||||
: `Obscure ${typeof label === "string" ? label : "field"}`
|
||||
}
|
||||
size="xs"
|
||||
className={classNames('mr-0.5 !h-auto my-0.5', disabled && 'opacity-disabled')}
|
||||
className={classNames("mr-0.5 !h-auto my-0.5", disabled && "opacity-disabled")}
|
||||
color={tint}
|
||||
// iconClassName={classNames(
|
||||
// tint === 'primary' && 'text-primary',
|
||||
@@ -343,7 +347,7 @@ function BaseInput({
|
||||
// tint === 'danger' && 'text-danger',
|
||||
// )}
|
||||
iconSize="sm"
|
||||
icon={obscured ? 'eye' : 'eye_closed'}
|
||||
icon={obscured ? "eye" : "eye_closed"}
|
||||
onClick={() => setObscured((o) => !o)}
|
||||
/>
|
||||
)}
|
||||
@@ -357,7 +361,7 @@ function validateRequire(v: string) {
|
||||
return v.length > 0;
|
||||
}
|
||||
|
||||
type PasswordFieldType = 'text' | 'encrypted';
|
||||
type PasswordFieldType = "text" | "encrypted";
|
||||
|
||||
function EncryptionInput({
|
||||
defaultValue,
|
||||
@@ -377,7 +381,7 @@ function EncryptionInput({
|
||||
error: string | null;
|
||||
}>(
|
||||
{
|
||||
fieldType: isEncryptionEnabled ? 'encrypted' : 'text',
|
||||
fieldType: isEncryptionEnabled ? "encrypted" : "text",
|
||||
value: null,
|
||||
security: null,
|
||||
obscured: true,
|
||||
@@ -395,19 +399,19 @@ function EncryptionInput({
|
||||
return;
|
||||
}
|
||||
|
||||
const security = analyzeTemplate(defaultValue ?? '');
|
||||
if (analyzeTemplate(defaultValue ?? '') === 'global_secured') {
|
||||
const security = analyzeTemplate(defaultValue ?? "");
|
||||
if (analyzeTemplate(defaultValue ?? "") === "global_secured") {
|
||||
// Lazily update value to decrypted representation
|
||||
templateToInsecure.mutate(defaultValue ?? '', {
|
||||
templateToInsecure.mutate(defaultValue ?? "", {
|
||||
onSuccess: (value) => {
|
||||
setState({ fieldType: 'encrypted', security, value, obscured: true, error: null });
|
||||
setState({ fieldType: "encrypted", security, value, obscured: true, error: null });
|
||||
// We're calling this here because we want the input to be fully initialized so the caller
|
||||
// can do stuff like change the selection.
|
||||
requestAnimationFrame(() => setRef?.(inputRef.current));
|
||||
},
|
||||
onError: (value) => {
|
||||
setState({
|
||||
fieldType: 'encrypted',
|
||||
fieldType: "encrypted",
|
||||
security,
|
||||
value: null,
|
||||
error: String(value),
|
||||
@@ -417,14 +421,14 @@ function EncryptionInput({
|
||||
});
|
||||
} else if (isEncryptionEnabled && !defaultValue) {
|
||||
// Default to encrypted field for new encrypted inputs
|
||||
setState({ fieldType: 'encrypted', security, value: '', obscured: true, error: null });
|
||||
setState({ fieldType: "encrypted", security, value: "", obscured: true, error: null });
|
||||
requestAnimationFrame(() => setRef?.(inputRef.current));
|
||||
} else if (isEncryptionEnabled) {
|
||||
// Don't obscure plain text when encryption is enabled
|
||||
setState({
|
||||
fieldType: 'text',
|
||||
fieldType: "text",
|
||||
security,
|
||||
value: defaultValue ?? '',
|
||||
value: defaultValue ?? "",
|
||||
obscured: false,
|
||||
error: null,
|
||||
});
|
||||
@@ -432,9 +436,9 @@ function EncryptionInput({
|
||||
} else {
|
||||
// Don't obscure plain text when encryption is disabled
|
||||
setState({
|
||||
fieldType: 'text',
|
||||
fieldType: "text",
|
||||
security,
|
||||
value: defaultValue ?? '',
|
||||
value: defaultValue ?? "",
|
||||
obscured: true,
|
||||
error: null,
|
||||
});
|
||||
@@ -444,16 +448,16 @@ function EncryptionInput({
|
||||
|
||||
const handleChange = useCallback(
|
||||
(value: string, fieldType: PasswordFieldType) => {
|
||||
if (fieldType === 'encrypted') {
|
||||
if (fieldType === "encrypted") {
|
||||
templateToSecure.mutate(value, { onSuccess: (value) => onChange?.(value) });
|
||||
} else {
|
||||
onChange?.(value);
|
||||
}
|
||||
setState((s) => {
|
||||
// We can't analyze when encrypted because we don't have the raw value, so assume it's secured
|
||||
const security = fieldType === 'encrypted' ? 'global_secured' : analyzeTemplate(value);
|
||||
const security = fieldType === "encrypted" ? "global_secured" : analyzeTemplate(value);
|
||||
// Reset obscured value when the field type is being changed
|
||||
const obscured = fieldType === s.fieldType ? s.obscured : fieldType !== 'text';
|
||||
const obscured = fieldType === s.fieldType ? s.obscured : fieldType !== "text";
|
||||
return { fieldType, value, security, obscured, error: s.error };
|
||||
});
|
||||
},
|
||||
@@ -491,22 +495,22 @@ function EncryptionInput({
|
||||
const dropdownItems = useMemo<DropdownItem[]>(
|
||||
() => [
|
||||
{
|
||||
label: state.obscured ? 'Show' : 'Hide',
|
||||
disabled: isEncryptionEnabled && state.fieldType === 'text',
|
||||
leftSlot: <Icon icon={state.obscured ? 'eye' : 'eye_closed'} />,
|
||||
label: state.obscured ? "Show" : "Hide",
|
||||
disabled: isEncryptionEnabled && state.fieldType === "text",
|
||||
leftSlot: <Icon icon={state.obscured ? "eye" : "eye_closed"} />,
|
||||
onSelect: () => setState((s) => ({ ...s, obscured: !s.obscured })),
|
||||
},
|
||||
{
|
||||
label: 'Copy',
|
||||
label: "Copy",
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
hidden: !state.value,
|
||||
onSelect: () => copyToClipboard(state.value ?? ''),
|
||||
onSelect: () => copyToClipboard(state.value ?? ""),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ type: "separator" },
|
||||
{
|
||||
label: state.fieldType === 'text' ? 'Encrypt Field' : 'Decrypt Field',
|
||||
leftSlot: <Icon icon={state.fieldType === 'text' ? 'lock' : 'lock_open'} />,
|
||||
onSelect: () => handleFieldTypeChange(state.fieldType === 'text' ? 'encrypted' : 'text'),
|
||||
label: state.fieldType === "text" ? "Encrypt Field" : "Decrypt Field",
|
||||
leftSlot: <Icon icon={state.fieldType === "text" ? "lock" : "lock_open"} />,
|
||||
onSelect: () => handleFieldTypeChange(state.fieldType === "text" ? "encrypted" : "text"),
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -519,23 +523,23 @@ function EncryptionInput({
|
||||
],
|
||||
);
|
||||
|
||||
let tint: InputProps['tint'];
|
||||
let tint: InputProps["tint"];
|
||||
if (!isEncryptionEnabled) {
|
||||
tint = undefined;
|
||||
} else if (state.fieldType === 'encrypted') {
|
||||
tint = 'info';
|
||||
} else if (state.security === 'local_secured') {
|
||||
tint = 'secondary';
|
||||
} else if (state.security === 'insecure') {
|
||||
tint = 'notice';
|
||||
} else if (state.fieldType === "encrypted") {
|
||||
tint = "info";
|
||||
} else if (state.security === "local_secured") {
|
||||
tint = "secondary";
|
||||
} else if (state.security === "insecure") {
|
||||
tint = "notice";
|
||||
}
|
||||
|
||||
const rightSlot = useMemo(() => {
|
||||
let icon: IconProps['icon'];
|
||||
let icon: IconProps["icon"];
|
||||
if (isEncryptionEnabled) {
|
||||
icon = state.security === 'insecure' ? 'shield_off' : 'shield_check';
|
||||
icon = state.security === "insecure" ? "shield_off" : "shield_check";
|
||||
} else {
|
||||
icon = state.obscured ? 'eye_closed' : 'eye';
|
||||
icon = state.obscured ? "eye_closed" : "eye";
|
||||
}
|
||||
return (
|
||||
<HStack className="h-auto m-0.5">
|
||||
@@ -546,9 +550,9 @@ function EncryptionInput({
|
||||
color={tint}
|
||||
aria-label="Configure encryption"
|
||||
className={classNames(
|
||||
'flex items-center justify-center !h-full !px-1',
|
||||
'opacity-70', // Makes it a bit subtler
|
||||
props.disabled && '!opacity-disabled',
|
||||
"flex items-center justify-center !h-full !px-1",
|
||||
"opacity-70", // Makes it a bit subtler
|
||||
props.disabled && "!opacity-disabled",
|
||||
)}
|
||||
>
|
||||
<HStack space={0.5}>
|
||||
@@ -561,7 +565,7 @@ function EncryptionInput({
|
||||
);
|
||||
}, [dropdownItems, isEncryptionEnabled, props.disabled, state.obscured, state.security, tint]);
|
||||
|
||||
const type = state.obscured ? 'password' : 'text';
|
||||
const type = state.obscured ? "password" : "text";
|
||||
|
||||
if (state.error) {
|
||||
return (
|
||||
@@ -575,7 +579,7 @@ function EncryptionInput({
|
||||
setupOrConfigureEncryption();
|
||||
}}
|
||||
>
|
||||
{state.error.replace(/^Render Error: /i, '')}
|
||||
{state.error.replace(/^Render Error: /i, "")}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
@@ -586,7 +590,7 @@ function EncryptionInput({
|
||||
disableObscureToggle
|
||||
autocompleteFunctions={autocompleteFunctions}
|
||||
autocompleteVariables={autocompleteVariables}
|
||||
defaultValue={state.value ?? ''}
|
||||
defaultValue={state.value ?? ""}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
onChange={handleInputChange}
|
||||
tint={tint}
|
||||
@@ -600,12 +604,12 @@ function EncryptionInput({
|
||||
}
|
||||
|
||||
const templateToSecure = createFastMutation({
|
||||
mutationKey: ['template-to-secure'],
|
||||
mutationKey: ["template-to-secure"],
|
||||
mutationFn: convertTemplateToSecure,
|
||||
});
|
||||
|
||||
const templateToInsecure = createFastMutation({
|
||||
mutationKey: ['template-to-insecure'],
|
||||
mutationKey: ["template-to-insecure"],
|
||||
mutationFn: convertTemplateToInsecure,
|
||||
disableToastError: true,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
interface Props {
|
||||
depth?: number;
|
||||
@@ -31,7 +31,7 @@ export const JsonAttributeTree = ({
|
||||
labelClassName?: string;
|
||||
}>(() => {
|
||||
const jsonType = Object.prototype.toString.call(attrValue);
|
||||
if (jsonType === '[object Object]') {
|
||||
if (jsonType === "[object Object]") {
|
||||
return {
|
||||
children: isExpanded
|
||||
? Object.keys(attrValue)
|
||||
@@ -47,11 +47,11 @@ export const JsonAttributeTree = ({
|
||||
))
|
||||
: null,
|
||||
isExpandable: Object.keys(attrValue).length > 0,
|
||||
label: isExpanded ? `{${Object.keys(attrValue).length || ' '}}` : '{⋯}',
|
||||
labelClassName: 'text-text-subtlest',
|
||||
label: isExpanded ? `{${Object.keys(attrValue).length || " "}}` : "{⋯}",
|
||||
labelClassName: "text-text-subtlest",
|
||||
};
|
||||
}
|
||||
if (jsonType === '[object Array]') {
|
||||
if (jsonType === "[object Array]") {
|
||||
return {
|
||||
children: isExpanded
|
||||
? // oxlint-disable-next-line no-explicit-any
|
||||
@@ -67,26 +67,26 @@ export const JsonAttributeTree = ({
|
||||
))
|
||||
: null,
|
||||
isExpandable: attrValue.length > 0,
|
||||
label: isExpanded ? `[${attrValue.length || ' '}]` : '[⋯]',
|
||||
labelClassName: 'text-text-subtlest',
|
||||
label: isExpanded ? `[${attrValue.length || " "}]` : "[⋯]",
|
||||
labelClassName: "text-text-subtlest",
|
||||
};
|
||||
}
|
||||
return {
|
||||
children: null,
|
||||
isExpandable: false,
|
||||
label: jsonType === '[object String]' ? `"${attrValue}"` : `${attrValue}`,
|
||||
label: jsonType === "[object String]" ? `"${attrValue}"` : `${attrValue}`,
|
||||
labelClassName: classNames(
|
||||
jsonType === '[object Boolean]' && 'text-primary',
|
||||
jsonType === '[object Number]' && 'text-info',
|
||||
jsonType === '[object String]' && 'text-notice',
|
||||
jsonType === '[object Null]' && 'text-danger',
|
||||
jsonType === "[object Boolean]" && "text-primary",
|
||||
jsonType === "[object Number]" && "text-info",
|
||||
jsonType === "[object String]" && "text-notice",
|
||||
jsonType === "[object Null]" && "text-danger",
|
||||
),
|
||||
};
|
||||
}, [attrValue, attrKeyJsonPath, isExpanded, depth]);
|
||||
|
||||
const labelEl = (
|
||||
<span
|
||||
className={classNames(labelClassName, 'cursor-text select-text group-hover:text-text-subtle')}
|
||||
className={classNames(labelClassName, "cursor-text select-text group-hover:text-text-subtle")}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
@@ -95,8 +95,8 @@ export const JsonAttributeTree = ({
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
/*depth === 0 && '-ml-4',*/ 'font-mono text-xs',
|
||||
depth === 0 && 'h-full overflow-y-auto pb-2',
|
||||
/*depth === 0 && '-ml-4',*/ "font-mono text-xs",
|
||||
depth === 0 && "h-full overflow-y-auto pb-2",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
@@ -110,13 +110,13 @@ export const JsonAttributeTree = ({
|
||||
size="xs"
|
||||
icon="chevron_right"
|
||||
className={classNames(
|
||||
'left-0 absolute transition-transform flex items-center',
|
||||
'group-hover:text-text-subtle',
|
||||
isExpanded ? 'rotate-90' : '',
|
||||
"left-0 absolute transition-transform flex items-center",
|
||||
"group-hover:text-text-subtle",
|
||||
isExpanded ? "rotate-90" : "",
|
||||
)}
|
||||
/>
|
||||
<span className="text-primary group-hover:text-primary mr-1.5 whitespace-nowrap">
|
||||
{attrKey === undefined ? '$' : attrKey}:
|
||||
{attrKey === undefined ? "$" : attrKey}:
|
||||
</span>
|
||||
{labelEl}
|
||||
</button>
|
||||
@@ -142,5 +142,5 @@ function joinObjectKey(baseKey: string | undefined, key: string): string {
|
||||
}
|
||||
|
||||
function joinArrayKey(baseKey: string | undefined, index: number): string {
|
||||
return `${baseKey ?? ''}[${index}]`;
|
||||
return `${baseKey ?? ""}[${index}]`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactElement, ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { HTMLAttributes, ReactElement, ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
children:
|
||||
@@ -27,7 +27,7 @@ interface KeyValueRowProps {
|
||||
rightSlot?: ReactNode;
|
||||
leftSlot?: ReactNode;
|
||||
labelClassName?: string;
|
||||
labelColor?: 'secondary' | 'primary' | 'info';
|
||||
labelColor?: "secondary" | "primary" | "info";
|
||||
}
|
||||
|
||||
export function KeyValueRow({
|
||||
@@ -35,18 +35,18 @@ export function KeyValueRow({
|
||||
children,
|
||||
rightSlot,
|
||||
leftSlot,
|
||||
labelColor = 'secondary',
|
||||
labelColor = "secondary",
|
||||
labelClassName,
|
||||
}: KeyValueRowProps) {
|
||||
return (
|
||||
<>
|
||||
<td
|
||||
className={classNames(
|
||||
'select-none py-0.5 pr-2 h-full align-top max-w-[10rem]',
|
||||
"select-none py-0.5 pr-2 h-full align-top max-w-[10rem]",
|
||||
labelClassName,
|
||||
labelColor === 'primary' && 'text-primary',
|
||||
labelColor === 'secondary' && 'text-text-subtle',
|
||||
labelColor === 'info' && 'text-info',
|
||||
labelColor === "primary" && "text-primary",
|
||||
labelColor === "secondary" && "text-text-subtle",
|
||||
labelColor === "info" && "text-info",
|
||||
)}
|
||||
>
|
||||
<span className="select-text cursor-text">{label}</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { IconTooltip } from './IconTooltip';
|
||||
import classNames from "classnames";
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
import { IconTooltip } from "./IconTooltip";
|
||||
|
||||
export function Label({
|
||||
htmlFor,
|
||||
@@ -26,9 +26,9 @@ export function Label({
|
||||
htmlFor={htmlFor ?? undefined}
|
||||
className={classNames(
|
||||
className,
|
||||
visuallyHidden && 'sr-only',
|
||||
'flex-shrink-0 text-sm',
|
||||
'text-text-subtle whitespace-nowrap flex items-center gap-1 mb-0.5',
|
||||
visuallyHidden && "sr-only",
|
||||
"flex-shrink-0 text-sm",
|
||||
"text-text-subtle whitespace-nowrap flex items-center gap-1 mb-0.5",
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Link as RouterLink } from '@tanstack/react-router';
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
import { appInfo } from '../../lib/appInfo';
|
||||
import { Icon } from './Icon';
|
||||
import { Link as RouterLink } from "@tanstack/react-router";
|
||||
import classNames from "classnames";
|
||||
import type { HTMLAttributes } from "react";
|
||||
import { appInfo } from "../../lib/appInfo";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
interface Props extends HTMLAttributes<HTMLAnchorElement> {
|
||||
href: string;
|
||||
@@ -14,17 +14,17 @@ export function Link({ href, children, noUnderline, className, ...other }: Props
|
||||
|
||||
className = classNames(
|
||||
className,
|
||||
'relative',
|
||||
'inline-flex items-center hover:underline group',
|
||||
!noUnderline && 'underline',
|
||||
"relative",
|
||||
"inline-flex items-center hover:underline group",
|
||||
!noUnderline && "underline",
|
||||
);
|
||||
|
||||
if (isExternal) {
|
||||
const isYaakLink = href.startsWith('https://yaak.app');
|
||||
const isYaakLink = href.startsWith("https://yaak.app");
|
||||
let finalHref = href;
|
||||
if (isYaakLink) {
|
||||
const url = new URL(href);
|
||||
url.searchParams.set('ref', appInfo.identifier);
|
||||
url.searchParams.set("ref", appInfo.identifier);
|
||||
finalHref = url.toString();
|
||||
}
|
||||
return (
|
||||
@@ -32,7 +32,7 @@ export function Link({ href, children, noUnderline, className, ...other }: Props
|
||||
<a
|
||||
href={finalHref}
|
||||
target="_blank"
|
||||
rel={isYaakLink ? undefined : 'noopener noreferrer'}
|
||||
rel={isYaakLink ? undefined : "noopener noreferrer"}
|
||||
onClick={(e) => e.preventDefault()}
|
||||
className={className}
|
||||
{...other}
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
import classNames from 'classnames';
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
size?: '2xs' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
size?: "2xs" | "xs" | "sm" | "md" | "lg" | "xl";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function LoadingIcon({ size = 'md', className }: Props) {
|
||||
export function LoadingIcon({ size = "md", className }: Props) {
|
||||
const classes = classNames(
|
||||
className,
|
||||
'text-inherit flex-shrink-0',
|
||||
size === 'xl' && 'h-6 w-6',
|
||||
size === 'lg' && 'h-5 w-5',
|
||||
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',
|
||||
'animate-spin',
|
||||
"text-inherit flex-shrink-0",
|
||||
size === "xl" && "h-6 w-6",
|
||||
size === "lg" && "h-5 w-5",
|
||||
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",
|
||||
"animate-spin",
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
classes,
|
||||
'border-[currentColor] border-b-transparent rounded-full',
|
||||
size === 'xl' && 'border-[0.2rem]',
|
||||
size === 'lg' && 'border-[0.16rem]',
|
||||
size === 'md' && 'border-[0.13rem]',
|
||||
size === 'sm' && 'border-[0.1rem]',
|
||||
size === 'xs' && 'border-[0.08rem]',
|
||||
size === '2xs' && 'border-[0.06rem]',
|
||||
"border-[currentColor] border-b-transparent rounded-full",
|
||||
size === "xl" && "border-[0.2rem]",
|
||||
size === "lg" && "border-[0.16rem]",
|
||||
size === "md" && "border-[0.13rem]",
|
||||
size === "sm" && "border-[0.1rem]",
|
||||
size === "xs" && "border-[0.08rem]",
|
||||
size === "2xs" && "border-[0.06rem]",
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
|
||||
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from "@dnd-kit/core";
|
||||
import {
|
||||
DndContext,
|
||||
DragOverlay,
|
||||
@@ -8,33 +8,33 @@ import {
|
||||
useDroppable,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { basename } from '@tauri-apps/api/path';
|
||||
import classNames from 'classnames';
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import type { WrappedEnvironmentVariable } from '../../hooks/useEnvironmentVariables';
|
||||
import { useRandomKey } from '../../hooks/useRandomKey';
|
||||
import { useToggle } from '../../hooks/useToggle';
|
||||
import { languageFromContentType } from '../../lib/contentType';
|
||||
import { showDialog } from '../../lib/dialog';
|
||||
import { computeSideForDragMove } from '../../lib/dnd';
|
||||
import { showPrompt } from '../../lib/prompt';
|
||||
import { DropMarker } from '../DropMarker';
|
||||
import { SelectFile } from '../SelectFile';
|
||||
import { Button } from './Button';
|
||||
import { Checkbox } from './Checkbox';
|
||||
import type { DropdownItem } from './Dropdown';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import type { EditorProps } from './Editor/Editor';
|
||||
import type { GenericCompletionConfig } from './Editor/genericCompletion';
|
||||
import { Editor } from './Editor/LazyEditor';
|
||||
import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { InputHandle, InputProps } from './Input';
|
||||
import { Input } from './Input';
|
||||
import { ensurePairId } from './PairEditor.util';
|
||||
import type { RadioDropdownItem } from './RadioDropdown';
|
||||
import { RadioDropdown } from './RadioDropdown';
|
||||
} from "@dnd-kit/core";
|
||||
import { basename } from "@tauri-apps/api/path";
|
||||
import classNames from "classnames";
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import type { WrappedEnvironmentVariable } from "../../hooks/useEnvironmentVariables";
|
||||
import { useRandomKey } from "../../hooks/useRandomKey";
|
||||
import { useToggle } from "../../hooks/useToggle";
|
||||
import { languageFromContentType } from "../../lib/contentType";
|
||||
import { showDialog } from "../../lib/dialog";
|
||||
import { computeSideForDragMove } from "../../lib/dnd";
|
||||
import { showPrompt } from "../../lib/prompt";
|
||||
import { DropMarker } from "../DropMarker";
|
||||
import { SelectFile } from "../SelectFile";
|
||||
import { Button } from "./Button";
|
||||
import { Checkbox } from "./Checkbox";
|
||||
import type { DropdownItem } from "./Dropdown";
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import type { EditorProps } from "./Editor/Editor";
|
||||
import type { GenericCompletionConfig } from "./Editor/genericCompletion";
|
||||
import { Editor } from "./Editor/LazyEditor";
|
||||
import { Icon } from "./Icon";
|
||||
import { IconButton } from "./IconButton";
|
||||
import type { InputHandle, InputProps } from "./Input";
|
||||
import { Input } from "./Input";
|
||||
import { ensurePairId } from "./PairEditor.util";
|
||||
import type { RadioDropdownItem } from "./RadioDropdown";
|
||||
import { RadioDropdown } from "./RadioDropdown";
|
||||
|
||||
export interface PairEditorHandle {
|
||||
focusName(id: string): void;
|
||||
@@ -51,18 +51,18 @@ export type PairEditorProps = {
|
||||
nameAutocompleteFunctions?: boolean;
|
||||
nameAutocompleteVariables?: boolean;
|
||||
namePlaceholder?: string;
|
||||
nameValidate?: InputProps['validate'];
|
||||
nameValidate?: InputProps["validate"];
|
||||
noScroll?: boolean;
|
||||
onChange: (pairs: PairWithId[]) => void;
|
||||
pairs: Pair[];
|
||||
stateKey: InputProps['stateKey'];
|
||||
stateKey: InputProps["stateKey"];
|
||||
setRef?: (n: PairEditorHandle) => void;
|
||||
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
|
||||
valueAutocompleteFunctions?: boolean;
|
||||
valueAutocompleteVariables?: boolean | 'environment';
|
||||
valueAutocompleteVariables?: boolean | "environment";
|
||||
valuePlaceholder?: string;
|
||||
valueType?: InputProps['type'] | ((pair: Pair) => InputProps['type']);
|
||||
valueValidate?: InputProps['validate'];
|
||||
valueType?: InputProps["type"] | ((pair: Pair) => InputProps["type"]);
|
||||
valueValidate?: InputProps["validate"];
|
||||
};
|
||||
|
||||
export type Pair = {
|
||||
@@ -188,7 +188,7 @@ export function PairEditor({
|
||||
if (focusPrevious) {
|
||||
const index = pairs.findIndex((p) => p.id === pair.id);
|
||||
const id = pairs[index - 1]?.id ?? null;
|
||||
rowsRef.current[id ?? 'n/a']?.focusName();
|
||||
rowsRef.current[id ?? "n/a"]?.focusName();
|
||||
}
|
||||
return setPairsAndSave((oldPairs) => oldPairs.filter((p) => p.id !== pair.id));
|
||||
},
|
||||
@@ -224,7 +224,7 @@ export function PairEditor({
|
||||
|
||||
const side = computeSideForDragMove(overPair.id, e);
|
||||
const overIndex = pairs.findIndex((p) => p.id === overId);
|
||||
const hoveredIndex = overIndex + (side === 'before' ? 0 : 1);
|
||||
const hoveredIndex = overIndex + (side === "before" ? 0 : 1);
|
||||
|
||||
setHoveredIndex(hoveredIndex);
|
||||
},
|
||||
@@ -270,14 +270,14 @@ export function PairEditor({
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'@container relative',
|
||||
'pb-2 mb-auto h-full',
|
||||
!noScroll && 'overflow-y-auto max-h-full',
|
||||
"@container relative",
|
||||
"pb-2 mb-auto h-full",
|
||||
!noScroll && "overflow-y-auto max-h-full",
|
||||
// Move over the width of the drag handle
|
||||
'-mr-2 pr-2',
|
||||
"-mr-2 pr-2",
|
||||
// Pad to make room for the drag divider
|
||||
'pt-0.5',
|
||||
'grid grid-rows-[auto_1fr]',
|
||||
"pt-0.5",
|
||||
"grid grid-rows-[auto_1fr]",
|
||||
)}
|
||||
>
|
||||
<div>
|
||||
@@ -379,22 +379,22 @@ type PairEditorRowProps = {
|
||||
setRef?: (id: string, n: RowHandle | null) => void;
|
||||
} & Pick<
|
||||
PairEditorProps,
|
||||
| 'allowFileValues'
|
||||
| 'allowMultilineValues'
|
||||
| 'forcedEnvironmentId'
|
||||
| 'forceUpdateKey'
|
||||
| 'nameAutocomplete'
|
||||
| 'nameAutocompleteVariables'
|
||||
| 'namePlaceholder'
|
||||
| 'nameValidate'
|
||||
| 'nameAutocompleteFunctions'
|
||||
| 'stateKey'
|
||||
| 'valueAutocomplete'
|
||||
| 'valueAutocompleteFunctions'
|
||||
| 'valueAutocompleteVariables'
|
||||
| 'valuePlaceholder'
|
||||
| 'valueType'
|
||||
| 'valueValidate'
|
||||
| "allowFileValues"
|
||||
| "allowMultilineValues"
|
||||
| "forcedEnvironmentId"
|
||||
| "forceUpdateKey"
|
||||
| "nameAutocomplete"
|
||||
| "nameAutocompleteVariables"
|
||||
| "namePlaceholder"
|
||||
| "nameValidate"
|
||||
| "nameAutocompleteFunctions"
|
||||
| "stateKey"
|
||||
| "valueAutocomplete"
|
||||
| "valueAutocompleteFunctions"
|
||||
| "valueAutocompleteVariables"
|
||||
| "valuePlaceholder"
|
||||
| "valueType"
|
||||
| "valueValidate"
|
||||
>;
|
||||
|
||||
interface RowHandle {
|
||||
@@ -485,7 +485,7 @@ export function PairEditorRow({
|
||||
const handleChangeValueFile = useMemo(
|
||||
() =>
|
||||
({ filePath }: { filePath: string | null }) =>
|
||||
onChange?.({ ...pair, value: filePath ?? '', isFile: true }),
|
||||
onChange?.({ ...pair, value: filePath ?? "", isFile: true }),
|
||||
[onChange, pair],
|
||||
);
|
||||
|
||||
@@ -502,8 +502,8 @@ export function PairEditorRow({
|
||||
const handleEditMultiLineValue = useCallback(
|
||||
() =>
|
||||
showDialog({
|
||||
id: 'pair-edit-multiline',
|
||||
size: 'dynamic',
|
||||
id: "pair-edit-multiline",
|
||||
size: "dynamic",
|
||||
title: <>Edit {pair.name}</>,
|
||||
render: ({ hide }) => (
|
||||
<MultilineEditDialog
|
||||
@@ -520,14 +520,14 @@ export function PairEditorRow({
|
||||
const defaultItems = useMemo(
|
||||
(): DropdownItem[] => [
|
||||
{
|
||||
label: 'Edit Multi-line',
|
||||
label: "Edit Multi-line",
|
||||
onSelect: handleEditMultiLineValue,
|
||||
hidden: !allowMultilineValues,
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
label: "Delete",
|
||||
onSelect: handleDelete,
|
||||
color: 'danger',
|
||||
color: "danger",
|
||||
},
|
||||
],
|
||||
[allowMultilineValues, handleDelete, handleEditMultiLineValue],
|
||||
@@ -537,8 +537,8 @@ export function PairEditorRow({
|
||||
const { setNodeRef: setDroppableRef } = useDroppable({ id: pair.id });
|
||||
|
||||
// Filter out the current pair name
|
||||
const valueAutocompleteVariablesFiltered = useMemo<EditorProps['autocompleteVariables']>(() => {
|
||||
if (valueAutocompleteVariables === 'environment') {
|
||||
const valueAutocompleteVariablesFiltered = useMemo<EditorProps["autocompleteVariables"]>(() => {
|
||||
if (valueAutocompleteVariables === "environment") {
|
||||
return (v: WrappedEnvironmentVariable): boolean => v.variable.name !== pair.name;
|
||||
}
|
||||
return valueAutocompleteVariables;
|
||||
@@ -557,17 +557,17 @@ export function PairEditorRow({
|
||||
ref={handleSetRef}
|
||||
className={classNames(
|
||||
className,
|
||||
'group/pair-row grid grid-cols-[auto_auto_minmax(0,1fr)_auto]',
|
||||
'grid-rows-1 items-center',
|
||||
!pair.enabled && 'opacity-60',
|
||||
"group/pair-row grid grid-cols-[auto_auto_minmax(0,1fr)_auto]",
|
||||
"grid-rows-1 items-center",
|
||||
!pair.enabled && "opacity-60",
|
||||
)}
|
||||
>
|
||||
<Checkbox
|
||||
hideLabel
|
||||
title={pair.enabled ? 'Disable item' : 'Enable item'}
|
||||
title={pair.enabled ? "Disable item" : "Enable item"}
|
||||
disabled={isLast || disabled}
|
||||
checked={isLast ? false : !!pair.enabled}
|
||||
className={classNames(isLast && '!opacity-disabled')}
|
||||
className={classNames(isLast && "!opacity-disabled")}
|
||||
onChange={handleChangeEnabled}
|
||||
/>
|
||||
{!isLast && !disableDrag ? (
|
||||
@@ -575,8 +575,8 @@ export function PairEditorRow({
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={classNames(
|
||||
'py-2 h-7 w-4 flex items-center',
|
||||
'justify-center opacity-0 group-hover/pair-row:opacity-70',
|
||||
"py-2 h-7 w-4 flex items-center",
|
||||
"justify-center opacity-0 group-hover/pair-row:opacity-70",
|
||||
)}
|
||||
>
|
||||
<Icon size="sm" icon="grip_vertical" className="pointer-events-none" />
|
||||
@@ -586,9 +586,9 @@ export function PairEditorRow({
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'grid items-center',
|
||||
'@xs:gap-2 @xs:!grid-rows-1 @xs:!grid-cols-[minmax(0,1fr)_minmax(0,1fr)]',
|
||||
'gap-0.5 grid-cols-1 grid-rows-2',
|
||||
"grid items-center",
|
||||
"@xs:gap-2 @xs:!grid-rows-1 @xs:!grid-cols-[minmax(0,1fr)_minmax(0,1fr)]",
|
||||
"gap-0.5 grid-cols-1 grid-rows-2",
|
||||
)}
|
||||
>
|
||||
<Input
|
||||
@@ -603,13 +603,13 @@ export function PairEditorRow({
|
||||
validate={nameValidate}
|
||||
forcedEnvironmentId={forcedEnvironmentId}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
containerClassName={classNames('bg-surface', isLast && 'border-dashed')}
|
||||
containerClassName={classNames("bg-surface", isLast && "border-dashed")}
|
||||
defaultValue={pair.name}
|
||||
label="Name"
|
||||
name={`name[${index}]`}
|
||||
onChange={handleChangeName}
|
||||
onFocus={handleFocusName}
|
||||
placeholder={namePlaceholder ?? 'name'}
|
||||
placeholder={namePlaceholder ?? "name"}
|
||||
autocomplete={nameAutocomplete}
|
||||
autocompleteVariables={nameAutocompleteVariables}
|
||||
autocompleteFunctions={nameAutocompleteFunctions}
|
||||
@@ -624,7 +624,7 @@ export function PairEditorRow({
|
||||
nameOverride={pair.filename || null}
|
||||
onChange={handleChangeValueFile}
|
||||
/>
|
||||
) : pair.value.includes('\n') ? (
|
||||
) : pair.value.includes("\n") ? (
|
||||
<Button
|
||||
color="secondary"
|
||||
size="sm"
|
||||
@@ -632,7 +632,7 @@ export function PairEditorRow({
|
||||
title={pair.value}
|
||||
className="text-xs font-mono"
|
||||
>
|
||||
{pair.value.split('\n').join(' ')}
|
||||
{pair.value.split("\n").join(" ")}
|
||||
</Button>
|
||||
) : (
|
||||
<Input
|
||||
@@ -643,7 +643,7 @@ export function PairEditorRow({
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
readOnly={isDraggingGlobal}
|
||||
containerClassName={classNames('bg-surface', isLast && 'border-dashed')}
|
||||
containerClassName={classNames("bg-surface", isLast && "border-dashed")}
|
||||
validate={valueValidate}
|
||||
forcedEnvironmentId={forcedEnvironmentId}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
@@ -652,8 +652,8 @@ export function PairEditorRow({
|
||||
name={`value[${index}]`}
|
||||
onChange={handleChangeValueText}
|
||||
onFocus={handleFocusValue}
|
||||
type={isLast ? 'text' : typeof valueType === 'function' ? valueType(pair) : valueType}
|
||||
placeholder={valuePlaceholder ?? 'value'}
|
||||
type={isLast ? "text" : typeof valueType === "function" ? valueType(pair) : valueType}
|
||||
placeholder={valuePlaceholder ?? "value"}
|
||||
autocomplete={valueAutocomplete?.(pair.name)}
|
||||
autocompleteFunctions={valueAutocompleteFunctions}
|
||||
autocompleteVariables={valueAutocompleteVariablesFiltered}
|
||||
@@ -676,7 +676,7 @@ export function PairEditorRow({
|
||||
<IconButton
|
||||
iconSize="sm"
|
||||
size="xs"
|
||||
icon={isLast || disabled ? 'empty' : 'chevron_down'}
|
||||
icon={isLast || disabled ? "empty" : "chevron_down"}
|
||||
title="Select form data type"
|
||||
className="text-text-subtlest"
|
||||
/>
|
||||
@@ -687,8 +687,8 @@ export function PairEditorRow({
|
||||
}
|
||||
|
||||
const fileItems: RadioDropdownItem<string>[] = [
|
||||
{ label: 'Text', value: 'text' },
|
||||
{ label: 'File', value: 'file' },
|
||||
{ label: "Text", value: "text" },
|
||||
{ label: "File", value: "file" },
|
||||
];
|
||||
|
||||
function FileActionsDropdown({
|
||||
@@ -710,8 +710,8 @@ function FileActionsDropdown({
|
||||
}) {
|
||||
const onChange = useCallback(
|
||||
(v: string) => {
|
||||
if (v === 'file') onChangeFile({ filePath: '' });
|
||||
else onChangeText('');
|
||||
if (v === "file") onChangeFile({ filePath: "" });
|
||||
else onChangeText("");
|
||||
},
|
||||
[onChangeFile, onChangeText],
|
||||
);
|
||||
@@ -719,51 +719,51 @@ function FileActionsDropdown({
|
||||
const itemsAfter = useMemo<DropdownItem[]>(
|
||||
() => [
|
||||
{
|
||||
label: 'Edit Multi-Line',
|
||||
label: "Edit Multi-Line",
|
||||
leftSlot: <Icon icon="file_code" />,
|
||||
hidden: pair.isFile,
|
||||
onSelect: editMultiLine,
|
||||
},
|
||||
{
|
||||
label: 'Set Content-Type',
|
||||
label: "Set Content-Type",
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: async () => {
|
||||
const contentType = await showPrompt({
|
||||
id: 'content-type',
|
||||
title: 'Override Content-Type',
|
||||
label: 'Content-Type',
|
||||
id: "content-type",
|
||||
title: "Override Content-Type",
|
||||
label: "Content-Type",
|
||||
required: false,
|
||||
placeholder: 'text/plain',
|
||||
defaultValue: pair.contentType ?? '',
|
||||
confirmText: 'Set',
|
||||
description: 'Leave blank to auto-detect',
|
||||
placeholder: "text/plain",
|
||||
defaultValue: pair.contentType ?? "",
|
||||
confirmText: "Set",
|
||||
description: "Leave blank to auto-detect",
|
||||
});
|
||||
if (contentType == null) return;
|
||||
onChangeContentType(contentType);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Set File Name',
|
||||
label: "Set File Name",
|
||||
leftSlot: <Icon icon="file_code" />,
|
||||
onSelect: async () => {
|
||||
console.log('PAIR', pair);
|
||||
const defaultFilename = await basename(pair.value ?? '');
|
||||
console.log("PAIR", pair);
|
||||
const defaultFilename = await basename(pair.value ?? "");
|
||||
const filename = await showPrompt({
|
||||
id: 'filename',
|
||||
title: 'Override Filename',
|
||||
label: 'Filename',
|
||||
id: "filename",
|
||||
title: "Override Filename",
|
||||
label: "Filename",
|
||||
required: false,
|
||||
placeholder: defaultFilename ?? 'myfile.png',
|
||||
placeholder: defaultFilename ?? "myfile.png",
|
||||
defaultValue: pair.filename,
|
||||
confirmText: 'Set',
|
||||
description: 'Leave blank to use the name of the selected file',
|
||||
confirmText: "Set",
|
||||
description: "Leave blank to use the name of the selected file",
|
||||
});
|
||||
if (filename == null) return;
|
||||
onChangeFilename(filename);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Unset File',
|
||||
label: "Unset File",
|
||||
leftSlot: <Icon icon="x" />,
|
||||
hidden: pair.isFile,
|
||||
onSelect: async () => {
|
||||
@@ -771,11 +771,11 @@ function FileActionsDropdown({
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Delete',
|
||||
label: "Delete",
|
||||
onSelect: onDelete,
|
||||
variant: 'danger',
|
||||
variant: "danger",
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
color: 'danger',
|
||||
color: "danger",
|
||||
},
|
||||
],
|
||||
[
|
||||
@@ -793,7 +793,7 @@ function FileActionsDropdown({
|
||||
|
||||
return (
|
||||
<RadioDropdown
|
||||
value={pair.isFile ? 'file' : 'text'}
|
||||
value={pair.isFile ? "file" : "text"}
|
||||
onChange={onChange}
|
||||
items={fileItems}
|
||||
itemsAfter={itemsAfter}
|
||||
@@ -810,7 +810,7 @@ function FileActionsDropdown({
|
||||
}
|
||||
|
||||
function emptyPair(): PairWithId {
|
||||
return ensurePairId({ enabled: true, name: '', value: '' });
|
||||
return ensurePairId({ enabled: true, name: "", value: "" });
|
||||
}
|
||||
|
||||
function isPairEmpty(pair: Pair): boolean {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import type { Pair, PairWithId } from './PairEditor';
|
||||
import { generateId } from "../../lib/generateId";
|
||||
import type { Pair, PairWithId } from "./PairEditor";
|
||||
|
||||
export function ensurePairId(p: Pair): PairWithId {
|
||||
if (typeof p.id === 'string') {
|
||||
if (typeof p.id === "string") {
|
||||
return p as PairWithId;
|
||||
}
|
||||
return { ...p, id: p.id ?? generateId() };
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import classNames from 'classnames';
|
||||
import { useKeyValue } from '../../hooks/useKeyValue';
|
||||
import { BulkPairEditor } from './BulkPairEditor';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { PairEditorProps } from './PairEditor';
|
||||
import { PairEditor } from './PairEditor';
|
||||
import classNames from "classnames";
|
||||
import { useKeyValue } from "../../hooks/useKeyValue";
|
||||
import { BulkPairEditor } from "./BulkPairEditor";
|
||||
import { IconButton } from "./IconButton";
|
||||
import type { PairEditorProps } from "./PairEditor";
|
||||
import { PairEditor } from "./PairEditor";
|
||||
|
||||
interface Props extends PairEditorProps {
|
||||
preferenceName: string;
|
||||
@@ -12,8 +12,8 @@ interface Props extends PairEditorProps {
|
||||
|
||||
export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
|
||||
const { value: useBulk, set: setUseBulk } = useKeyValue<boolean>({
|
||||
namespace: 'global',
|
||||
key: ['bulk_edit', preferenceName],
|
||||
namespace: "global",
|
||||
key: ["bulk_edit", preferenceName],
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
@@ -24,13 +24,13 @@ export function PairOrBulkEditor({ preferenceName, ...props }: Props) {
|
||||
<IconButton
|
||||
size="sm"
|
||||
variant="border"
|
||||
title={useBulk ? 'Enable form edit' : 'Enable bulk edit'}
|
||||
title={useBulk ? "Enable form edit" : "Enable bulk edit"}
|
||||
className={classNames(
|
||||
'transition-opacity opacity-0 group-hover:opacity-80 hover:!opacity-100 shadow',
|
||||
'bg-surface hover:text group-hover/wrapper:opacity-100',
|
||||
"transition-opacity opacity-0 group-hover:opacity-80 hover:!opacity-100 shadow",
|
||||
"bg-surface hover:text group-hover/wrapper:opacity-100",
|
||||
)}
|
||||
onClick={() => setUseBulk((b) => !b)}
|
||||
icon={useBulk ? 'table' : 'file_code'}
|
||||
icon={useBulk ? "table" : "file_code"}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ButtonProps } from './Button';
|
||||
import { Button } from './Button';
|
||||
import classNames from "classnames";
|
||||
import type { ButtonProps } from "./Button";
|
||||
import { Button } from "./Button";
|
||||
|
||||
export function PillButton({ className, ...props }: ButtonProps) {
|
||||
return (
|
||||
<Button
|
||||
size="2xs"
|
||||
variant="border"
|
||||
className={classNames(className, '!rounded-full mx-1 !px-3')}
|
||||
className={classNames(className, "!rounded-full mx-1 !px-3")}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import type { FocusEvent, HTMLAttributes, ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { FocusEvent, HTMLAttributes, ReactNode } from "react";
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
@@ -7,30 +7,30 @@ import {
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useRandomKey } from '../../hooks/useRandomKey';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { InputProps } from './Input';
|
||||
import { Label } from './Label';
|
||||
import { HStack } from './Stacks';
|
||||
} from "react";
|
||||
import { useRandomKey } from "../../hooks/useRandomKey";
|
||||
import { useStateWithDeps } from "../../hooks/useStateWithDeps";
|
||||
import { generateId } from "../../lib/generateId";
|
||||
import { IconButton } from "./IconButton";
|
||||
import type { InputProps } from "./Input";
|
||||
import { Label } from "./Label";
|
||||
import { HStack } from "./Stacks";
|
||||
|
||||
export type PlainInputProps = Omit<
|
||||
InputProps,
|
||||
| 'wrapLines'
|
||||
| 'onKeyDown'
|
||||
| 'type'
|
||||
| 'stateKey'
|
||||
| 'autocompleteVariables'
|
||||
| 'autocompleteFunctions'
|
||||
| 'autocomplete'
|
||||
| 'extraExtensions'
|
||||
| 'forcedEnvironmentId'
|
||||
| "wrapLines"
|
||||
| "onKeyDown"
|
||||
| "type"
|
||||
| "stateKey"
|
||||
| "autocompleteVariables"
|
||||
| "autocompleteFunctions"
|
||||
| "autocomplete"
|
||||
| "extraExtensions"
|
||||
| "forcedEnvironmentId"
|
||||
> &
|
||||
Pick<HTMLAttributes<HTMLInputElement>, 'onKeyDownCapture'> & {
|
||||
onFocusRaw?: HTMLAttributes<HTMLInputElement>['onFocus'];
|
||||
type?: 'text' | 'password' | 'number';
|
||||
Pick<HTMLAttributes<HTMLInputElement>, "onKeyDownCapture"> & {
|
||||
onFocusRaw?: HTMLAttributes<HTMLInputElement>["onFocus"];
|
||||
type?: "text" | "password" | "number";
|
||||
step?: number;
|
||||
hideObscureToggle?: boolean;
|
||||
labelRightSlot?: ReactNode;
|
||||
@@ -49,7 +49,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
hideObscureToggle,
|
||||
label,
|
||||
labelClassName,
|
||||
labelPosition = 'top',
|
||||
labelPosition = "top",
|
||||
labelRightSlot,
|
||||
leftSlot,
|
||||
name,
|
||||
@@ -62,9 +62,9 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
placeholder,
|
||||
required,
|
||||
rightSlot,
|
||||
size = 'md',
|
||||
size = "md",
|
||||
tint,
|
||||
type = 'text',
|
||||
type = "text",
|
||||
validate,
|
||||
},
|
||||
ref,
|
||||
@@ -74,7 +74,7 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey();
|
||||
const forceUpdateKey = `${forceUpdateKeyFromAbove}::${focusedUpdateKey}`;
|
||||
|
||||
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
|
||||
const [obscured, setObscured] = useStateWithDeps(type === "password", [type]);
|
||||
const [focused, setFocused] = useState(false);
|
||||
const [hasChanged, setHasChanged] = useStateWithDeps<boolean>(false, [forceUpdateKey]);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
@@ -114,8 +114,8 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
const id = useRef(`input-${generateId()}`);
|
||||
const commonClassName = classNames(
|
||||
className,
|
||||
'!bg-transparent min-w-0 w-full focus:outline-none placeholder:text-placeholder',
|
||||
'px-2 text-xs font-mono cursor-text',
|
||||
"!bg-transparent min-w-0 w-full focus:outline-none placeholder:text-placeholder",
|
||||
"px-2 text-xs font-mono cursor-text",
|
||||
);
|
||||
|
||||
const handleChange = useCallback(
|
||||
@@ -124,11 +124,11 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
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;
|
||||
if (typeof validate === "boolean") return validate;
|
||||
if (typeof validate === "function" && !validate(value)) return false;
|
||||
return true;
|
||||
};
|
||||
inputRef.current?.setCustomValidity(isValid(value) ? '' : 'Invalid value');
|
||||
inputRef.current?.setCustomValidity(isValid(value) ? "" : "Invalid value");
|
||||
},
|
||||
[onChange, required, setHasChanged, validate],
|
||||
);
|
||||
@@ -139,10 +139,10 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
<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',
|
||||
"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
|
||||
@@ -159,41 +159,41 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
alignItems="stretch"
|
||||
className={classNames(
|
||||
containerClassName,
|
||||
'x-theme-input',
|
||||
'relative w-full rounded-md text',
|
||||
'border',
|
||||
'overflow-hidden',
|
||||
focused ? 'border-border-focus' : 'border-border-subtle',
|
||||
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',
|
||||
size === '2xs' && 'min-h-2xs',
|
||||
"x-theme-input",
|
||||
"relative w-full rounded-md text",
|
||||
"border",
|
||||
"overflow-hidden",
|
||||
focused ? "border-border-focus" : "border-border-subtle",
|
||||
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",
|
||||
size === "2xs" && "min-h-2xs",
|
||||
)}
|
||||
>
|
||||
{tint != null && (
|
||||
<div
|
||||
aria-hidden
|
||||
className={classNames(
|
||||
'absolute inset-0 opacity-5 pointer-events-none',
|
||||
tint === 'info' && 'bg-info',
|
||||
tint === 'warning' && 'bg-warning',
|
||||
"absolute inset-0 opacity-5 pointer-events-none",
|
||||
tint === "info" && "bg-info",
|
||||
tint === "warning" && "bg-warning",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{leftSlot}
|
||||
<HStack
|
||||
className={classNames(
|
||||
'w-full min-w-0',
|
||||
leftSlot ? 'pl-0.5 -ml-2' : null,
|
||||
rightSlot ? 'pr-0.5 -mr-2' : null,
|
||||
"w-full min-w-0",
|
||||
leftSlot ? "pl-0.5 -ml-2" : null,
|
||||
rightSlot ? "pr-0.5 -mr-2" : null,
|
||||
)}
|
||||
>
|
||||
<input
|
||||
id={id.current}
|
||||
ref={inputRef}
|
||||
key={forceUpdateKey}
|
||||
type={type === 'password' && !obscured ? 'text' : type}
|
||||
type={type === "password" && !obscured ? "text" : type}
|
||||
name={name}
|
||||
// oxlint-disable-next-line jsx-a11y/no-autofocus
|
||||
autoFocus={autoFocus}
|
||||
@@ -202,8 +202,8 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
autoCapitalize="off"
|
||||
autoCorrect="off"
|
||||
onChange={(e) => handleChange(e.target.value)}
|
||||
onPaste={(e) => onPaste?.(e.clipboardData.getData('Text'))}
|
||||
className={classNames(commonClassName, 'h-full')}
|
||||
onPaste={(e) => onPaste?.(e.clipboardData.getData("Text"))}
|
||||
className={classNames(commonClassName, "h-full")}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
required={required}
|
||||
@@ -211,14 +211,18 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
|
||||
onKeyDownCapture={onKeyDownCapture}
|
||||
/>
|
||||
</HStack>
|
||||
{type === 'password' && !hideObscureToggle && (
|
||||
{type === "password" && !hideObscureToggle && (
|
||||
<IconButton
|
||||
title={obscured ? `Show ${typeof label === 'string' ? label : 'field'}` : `Obscure ${typeof label === 'string' ? label : 'field'}`}
|
||||
title={
|
||||
obscured
|
||||
? `Show ${typeof label === "string" ? label : "field"}`
|
||||
: `Obscure ${typeof label === "string" ? label : "field"}`
|
||||
}
|
||||
size="xs"
|
||||
className="mr-0.5 group/obscure !h-auto my-0.5"
|
||||
iconClassName="group-hover/obscure:text"
|
||||
iconSize="sm"
|
||||
icon={obscured ? 'eye' : 'eye_closed'}
|
||||
icon={obscured ? "eye" : "eye_closed"}
|
||||
onClick={() => setObscured((o) => !o)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { FormInput, JsonPrimitive } from '@yaakapp-internal/plugins';
|
||||
import type { FormEvent } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import { DynamicForm } from '../DynamicForm';
|
||||
import { Button } from './Button';
|
||||
import { HStack } from './Stacks';
|
||||
import type { FormInput, JsonPrimitive } from "@yaakapp-internal/plugins";
|
||||
import type { FormEvent } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { generateId } from "../../lib/generateId";
|
||||
import { DynamicForm } from "../DynamicForm";
|
||||
import { Button } from "./Button";
|
||||
import { HStack } from "./Stacks";
|
||||
|
||||
export interface PromptProps {
|
||||
inputs: FormInput[];
|
||||
@@ -20,8 +20,8 @@ export function Prompt({
|
||||
onCancel,
|
||||
inputs: initialInputs,
|
||||
onResult,
|
||||
confirmText = 'Confirm',
|
||||
cancelText = 'Cancel',
|
||||
confirmText = "Confirm",
|
||||
cancelText = "Cancel",
|
||||
onValuesChange,
|
||||
onInputsUpdated,
|
||||
}: PromptProps) {
|
||||
@@ -55,10 +55,10 @@ export function Prompt({
|
||||
<DynamicForm inputs={inputs} onChange={setValue} data={value} stateKey={id} />
|
||||
<HStack space={2} justifyContent="end">
|
||||
<Button onClick={onCancel} variant="border" color="secondary">
|
||||
{cancelText || 'Cancel'}
|
||||
{cancelText || "Cancel"}
|
||||
</Button>
|
||||
<Button type="submit" color="primary">
|
||||
{confirmText || 'Done'}
|
||||
{confirmText || "Done"}
|
||||
</Button>
|
||||
</HStack>
|
||||
</form>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface RadioCardOption<T extends string> {
|
||||
value: T;
|
||||
@@ -28,11 +28,9 @@ export function RadioCards<T extends string>({
|
||||
<label
|
||||
key={option.value}
|
||||
className={classNames(
|
||||
'flex items-start gap-3 p-3 rounded-lg border cursor-pointer',
|
||||
'transition-colors',
|
||||
selected
|
||||
? 'border-border-focus'
|
||||
: 'border-border-subtle hocus:border-text-subtlest',
|
||||
"flex items-start gap-3 p-3 rounded-lg border cursor-pointer",
|
||||
"transition-colors",
|
||||
selected ? "border-border-focus" : "border-border-subtle hocus:border-text-subtlest",
|
||||
)}
|
||||
>
|
||||
<input
|
||||
@@ -45,9 +43,9 @@ export function RadioCards<T extends string>({
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'mt-1 w-4 h-4 flex-shrink-0 rounded-full border',
|
||||
'flex items-center justify-center',
|
||||
selected ? 'border-focus' : 'border-border',
|
||||
"mt-1 w-4 h-4 flex-shrink-0 rounded-full border",
|
||||
"flex items-center justify-center",
|
||||
selected ? "border-focus" : "border-border",
|
||||
)}
|
||||
>
|
||||
{selected && <div className="w-2 h-2 rounded-full bg-text" />}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import type { DropdownItem, DropdownItemSeparator, DropdownProps } from './Dropdown';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import { Icon } from './Icon';
|
||||
import type { ReactNode } from "react";
|
||||
import { useMemo } from "react";
|
||||
import type { DropdownItem, DropdownItemSeparator, DropdownProps } from "./Dropdown";
|
||||
import { Dropdown } from "./Dropdown";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
export type RadioDropdownItem<T = string | null> =
|
||||
| {
|
||||
type?: 'default';
|
||||
type?: "default";
|
||||
label: ReactNode;
|
||||
shortLabel?: ReactNode;
|
||||
value: T;
|
||||
@@ -20,7 +20,7 @@ export interface RadioDropdownProps<T = string | null> {
|
||||
itemsBefore?: DropdownItem[];
|
||||
items: RadioDropdownItem<T>[];
|
||||
itemsAfter?: DropdownItem[];
|
||||
children: DropdownProps['children'];
|
||||
children: DropdownProps["children"];
|
||||
}
|
||||
|
||||
export function RadioDropdown<T = string | null>({
|
||||
@@ -37,13 +37,13 @@ export function RadioDropdown<T = string | null>({
|
||||
? [
|
||||
...itemsBefore,
|
||||
{
|
||||
type: 'separator',
|
||||
hidden: itemsBefore[itemsBefore.length - 1]?.type === 'separator',
|
||||
type: "separator",
|
||||
hidden: itemsBefore[itemsBefore.length - 1]?.type === "separator",
|
||||
},
|
||||
]
|
||||
: []) as DropdownItem[]),
|
||||
...items.map((item) => {
|
||||
if (item.type === 'separator') {
|
||||
if (item.type === "separator") {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
@@ -51,11 +51,11 @@ export function RadioDropdown<T = string | null>({
|
||||
label: item.label,
|
||||
rightSlot: item.rightSlot,
|
||||
onSelect: () => onChange(item.value),
|
||||
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
|
||||
leftSlot: <Icon icon={value === item.value ? "check" : "empty"} />,
|
||||
} as DropdownItem;
|
||||
}),
|
||||
...((itemsAfter
|
||||
? [{ type: 'separator', hidden: itemsAfter[0]?.type === 'separator' }, ...itemsAfter]
|
||||
? [{ type: "separator", hidden: itemsAfter[0]?.type === "separator" }, ...itemsAfter]
|
||||
: []) as DropdownItem[]),
|
||||
],
|
||||
[itemsBefore, items, itemsAfter, value, onChange],
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import classNames from 'classnames';
|
||||
import { type ReactNode, useRef } from 'react';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import { Button } from './Button';
|
||||
import type { IconProps } from './Icon';
|
||||
import { IconButton, type IconButtonProps } from './IconButton';
|
||||
import { Label } from './Label';
|
||||
import { HStack } from './Stacks';
|
||||
import classNames from "classnames";
|
||||
import { type ReactNode, useRef } from "react";
|
||||
import { useStateWithDeps } from "../../hooks/useStateWithDeps";
|
||||
import { generateId } from "../../lib/generateId";
|
||||
import { Button } from "./Button";
|
||||
import type { IconProps } from "./Icon";
|
||||
import { IconButton, type IconButtonProps } from "./IconButton";
|
||||
import { Label } from "./Label";
|
||||
import { HStack } from "./Stacks";
|
||||
|
||||
interface Props<T extends string> {
|
||||
options: { value: T; label: string; icon?: IconProps['icon'] }[];
|
||||
options: { value: T; label: string; icon?: IconProps["icon"] }[];
|
||||
onChange: (value: T) => void;
|
||||
value: T;
|
||||
name: string;
|
||||
size?: IconButtonProps['size'];
|
||||
size?: IconButtonProps["size"];
|
||||
label: string;
|
||||
className?: string;
|
||||
hideLabel?: boolean;
|
||||
@@ -25,7 +25,7 @@ export function SegmentedControl<T extends string>({
|
||||
value,
|
||||
onChange,
|
||||
options,
|
||||
size = 'xs',
|
||||
size = "xs",
|
||||
label,
|
||||
hideLabel,
|
||||
labelClassName,
|
||||
@@ -54,18 +54,18 @@ export function SegmentedControl<T extends string>({
|
||||
space={1}
|
||||
className={classNames(
|
||||
className,
|
||||
'bg-surface-highlight rounded-lg mb-auto mr-auto',
|
||||
'transition-opacity transform-gpu p-1',
|
||||
"bg-surface-highlight rounded-lg mb-auto mr-auto",
|
||||
"transition-opacity transform-gpu p-1",
|
||||
)}
|
||||
onKeyDown={(e) => {
|
||||
const selectedIndex = options.findIndex((o) => o.value === selectedValue);
|
||||
if (e.key === 'ArrowRight') {
|
||||
if (e.key === "ArrowRight") {
|
||||
e.preventDefault();
|
||||
const newIndex = Math.abs((selectedIndex + 1) % options.length);
|
||||
if (options[newIndex]) setSelectedValue(options[newIndex].value);
|
||||
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
|
||||
child.focus();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
} else if (e.key === "ArrowLeft") {
|
||||
e.preventDefault();
|
||||
const newIndex = Math.abs((selectedIndex - 1) % options.length);
|
||||
if (options[newIndex]) setSelectedValue(options[newIndex].value);
|
||||
@@ -84,12 +84,12 @@ export function SegmentedControl<T extends string>({
|
||||
aria-checked={isActive}
|
||||
size={size}
|
||||
variant="solid"
|
||||
color={isActive ? 'secondary' : undefined}
|
||||
color={isActive ? "secondary" : undefined}
|
||||
role="radio"
|
||||
tabIndex={isSelected ? 0 : -1}
|
||||
className={classNames(
|
||||
isActive && '!text-text',
|
||||
'focus:ring-1 focus:ring-border-focus',
|
||||
isActive && "!text-text",
|
||||
"focus:ring-1 focus:ring-border-focus",
|
||||
)}
|
||||
onClick={() => onChange(o.value)}
|
||||
>
|
||||
@@ -103,13 +103,13 @@ export function SegmentedControl<T extends string>({
|
||||
aria-checked={isActive}
|
||||
size={size}
|
||||
variant="solid"
|
||||
color={isActive ? 'secondary' : undefined}
|
||||
color={isActive ? "secondary" : undefined}
|
||||
role="radio"
|
||||
tabIndex={isSelected ? 0 : -1}
|
||||
className={classNames(
|
||||
isActive && '!text-text',
|
||||
'!px-1.5 !w-auto',
|
||||
'focus:ring-border-focus',
|
||||
isActive && "!text-text",
|
||||
"!px-1.5 !w-auto",
|
||||
"focus:ring-border-focus",
|
||||
)}
|
||||
title={o.label}
|
||||
icon={o.icon}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { useState } from 'react';
|
||||
import type { ButtonProps } from './Button';
|
||||
import { Button } from './Button';
|
||||
import { Label } from './Label';
|
||||
import type { RadioDropdownItem } from './RadioDropdown';
|
||||
import { RadioDropdown } from './RadioDropdown';
|
||||
import { HStack } from './Stacks';
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import classNames from "classnames";
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import { useState } from "react";
|
||||
import type { ButtonProps } from "./Button";
|
||||
import { Button } from "./Button";
|
||||
import { Label } from "./Label";
|
||||
import type { RadioDropdownItem } from "./RadioDropdown";
|
||||
import { RadioDropdown } from "./RadioDropdown";
|
||||
import { HStack } from "./Stacks";
|
||||
|
||||
export interface SelectProps<T extends string> {
|
||||
name: string;
|
||||
label: string;
|
||||
labelPosition?: 'top' | 'left';
|
||||
labelPosition?: "top" | "left";
|
||||
labelClassName?: string;
|
||||
hideLabel?: boolean;
|
||||
value: T;
|
||||
@@ -21,14 +21,14 @@ export interface SelectProps<T extends string> {
|
||||
options: RadioDropdownItem<T>[];
|
||||
onChange: (value: T) => void;
|
||||
defaultValue?: T;
|
||||
size?: ButtonProps['size'];
|
||||
size?: ButtonProps["size"];
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
filterable?: boolean;
|
||||
}
|
||||
|
||||
export function Select<T extends string>({
|
||||
labelPosition = 'top',
|
||||
labelPosition = "top",
|
||||
name,
|
||||
help,
|
||||
labelClassName,
|
||||
@@ -42,11 +42,11 @@ export function Select<T extends string>({
|
||||
className,
|
||||
defaultValue,
|
||||
filterable,
|
||||
size = 'md',
|
||||
size = "md",
|
||||
}: SelectProps<T>) {
|
||||
const [focused, setFocused] = useState<boolean>(false);
|
||||
const id = `input-${name}`;
|
||||
const isInvalidSelection = options.find((o) => 'value' in o && o.value === value) == null;
|
||||
const isInvalidSelection = options.find((o) => "value" in o && o.value === value) == null;
|
||||
|
||||
const handleChange = (value: T) => {
|
||||
onChange?.(value);
|
||||
@@ -56,29 +56,29 @@ export function Select<T extends string>({
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'x-theme-input',
|
||||
'w-full',
|
||||
'pointer-events-auto', // Just in case we're placing in disabled parent
|
||||
labelPosition === 'left' && 'grid grid-cols-[auto_1fr] items-center gap-2',
|
||||
labelPosition === 'top' && 'flex-row gap-0.5',
|
||||
"x-theme-input",
|
||||
"w-full",
|
||||
"pointer-events-auto", // Just in case we're placing in disabled parent
|
||||
labelPosition === "left" && "grid grid-cols-[auto_1fr] items-center gap-2",
|
||||
labelPosition === "top" && "flex-row gap-0.5",
|
||||
)}
|
||||
>
|
||||
<Label htmlFor={id} visuallyHidden={hideLabel} className={labelClassName} help={help}>
|
||||
{label}
|
||||
</Label>
|
||||
{type() === 'macos' && !filterable ? (
|
||||
{type() === "macos" && !filterable ? (
|
||||
<HStack
|
||||
space={2}
|
||||
className={classNames(
|
||||
'w-full rounded-md text text-sm font-mono',
|
||||
'pl-2',
|
||||
'border',
|
||||
focused && !disabled ? 'border-border-focus' : 'border-border',
|
||||
disabled && 'border-dotted',
|
||||
isInvalidSelection && 'border-danger',
|
||||
size === 'xs' && 'h-xs',
|
||||
size === 'sm' && 'h-sm',
|
||||
size === 'md' && 'h-md',
|
||||
"w-full rounded-md text text-sm font-mono",
|
||||
"pl-2",
|
||||
"border",
|
||||
focused && !disabled ? "border-border-focus" : "border-border",
|
||||
disabled && "border-dotted",
|
||||
isInvalidSelection && "border-danger",
|
||||
size === "xs" && "h-xs",
|
||||
size === "sm" && "h-sm",
|
||||
size === "md" && "h-md",
|
||||
)}
|
||||
>
|
||||
{leftSlot && <div>{leftSlot}</div>}
|
||||
@@ -90,17 +90,17 @@ export function Select<T extends string>({
|
||||
onBlur={() => setFocused(false)}
|
||||
disabled={disabled}
|
||||
className={classNames(
|
||||
'pr-7 w-full outline-none bg-transparent disabled:opacity-disabled',
|
||||
'leading-[1] rounded-none', // Center the text better vertically
|
||||
"pr-7 w-full outline-none bg-transparent disabled:opacity-disabled",
|
||||
"leading-[1] rounded-none", // Center the text better vertically
|
||||
)}
|
||||
>
|
||||
{isInvalidSelection && <option value={'__NONE__'}>-- Select an Option --</option>}
|
||||
{isInvalidSelection && <option value={"__NONE__"}>-- Select an Option --</option>}
|
||||
{options.map((o) => {
|
||||
if (o.type === 'separator') return null;
|
||||
if (o.type === "separator") return null;
|
||||
return (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
{o.value === defaultValue && ' (default)'}
|
||||
{o.value === defaultValue && " (default)"}
|
||||
</option>
|
||||
);
|
||||
})}
|
||||
@@ -119,7 +119,7 @@ export function Select<T extends string>({
|
||||
disabled={disabled}
|
||||
forDropdown
|
||||
>
|
||||
{options.find((o) => o.type !== 'separator' && o.value === value)?.label ?? '--'}
|
||||
{options.find((o) => o.type !== "separator" && o.value === value)?.label ?? "--"}
|
||||
</Button>
|
||||
</RadioDropdown>
|
||||
)}
|
||||
@@ -129,9 +129,9 @@ export function Select<T extends string>({
|
||||
|
||||
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',
|
||||
backgroundPosition: "right 0.3rem center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "1.5em 1.5em",
|
||||
appearance: "none",
|
||||
printColorAdjust: "exact",
|
||||
};
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Color } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Color } from "@yaakapp-internal/plugins";
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
interface Props {
|
||||
orientation?: 'horizontal' | 'vertical';
|
||||
orientation?: "horizontal" | "vertical";
|
||||
dashed?: boolean;
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
@@ -14,28 +14,28 @@ export function Separator({
|
||||
color,
|
||||
className,
|
||||
dashed,
|
||||
orientation = 'horizontal',
|
||||
orientation = "horizontal",
|
||||
children,
|
||||
}: Props) {
|
||||
return (
|
||||
<div role="presentation" className={classNames(className, 'flex items-center w-full')}>
|
||||
<div role="presentation" className={classNames(className, "flex items-center w-full")}>
|
||||
{children && (
|
||||
<div className="text-sm text-text-subtlest mr-2 whitespace-nowrap">{children}</div>
|
||||
)}
|
||||
<div
|
||||
className={classNames(
|
||||
'h-0 border-t opacity-60',
|
||||
color == null && 'border-border',
|
||||
color === 'primary' && 'border-primary',
|
||||
color === 'secondary' && 'border-secondary',
|
||||
color === 'success' && 'border-success',
|
||||
color === 'notice' && 'border-notice',
|
||||
color === 'warning' && 'border-warning',
|
||||
color === 'danger' && 'border-danger',
|
||||
color === 'info' && 'border-info',
|
||||
dashed && 'border-dashed',
|
||||
orientation === 'horizontal' && 'w-full h-[1px]',
|
||||
orientation === 'vertical' && 'h-full w-[1px]',
|
||||
"h-0 border-t opacity-60",
|
||||
color == null && "border-border",
|
||||
color === "primary" && "border-primary",
|
||||
color === "secondary" && "border-secondary",
|
||||
color === "success" && "border-success",
|
||||
color === "notice" && "border-notice",
|
||||
color === "warning" && "border-warning",
|
||||
color === "danger" && "border-danger",
|
||||
color === "info" && "border-info",
|
||||
dashed && "border-dashed",
|
||||
orientation === "horizontal" && "w-full h-[1px]",
|
||||
orientation === "vertical" && "h-full w-[1px]",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { formatSize } from '@yaakapp-internal/lib/formatSize';
|
||||
import { formatSize } from "@yaakapp-internal/lib/formatSize";
|
||||
|
||||
interface Props {
|
||||
contentLength: number;
|
||||
@@ -11,7 +11,7 @@ export function SizeTag({ contentLength, contentLengthCompressed }: Props) {
|
||||
className="font-mono"
|
||||
title={
|
||||
`${contentLength} bytes` +
|
||||
(contentLengthCompressed ? `\n${contentLengthCompressed} bytes compressed` : '')
|
||||
(contentLengthCompressed ? `\n${contentLengthCompressed} bytes compressed` : "")
|
||||
}
|
||||
>
|
||||
{formatSize(contentLength)}
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { CSSProperties, ReactNode } from 'react';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
|
||||
import { useContainerSize } from '../../hooks/useContainerQuery';
|
||||
import { clamp } from '../../lib/clamp';
|
||||
import type { ResizeHandleEvent } from '../ResizeHandle';
|
||||
import { ResizeHandle } from '../ResizeHandle';
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import type { CSSProperties, ReactNode } from "react";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { useLocalStorage } from "react-use";
|
||||
import { activeWorkspaceAtom } from "../../hooks/useActiveWorkspace";
|
||||
import { useContainerSize } from "../../hooks/useContainerQuery";
|
||||
import { clamp } from "../../lib/clamp";
|
||||
import type { ResizeHandleEvent } from "../ResizeHandle";
|
||||
import { ResizeHandle } from "../ResizeHandle";
|
||||
|
||||
export type SplitLayoutLayout = 'responsive' | 'horizontal' | 'vertical';
|
||||
export type SplitLayoutLayout = "responsive" | "horizontal" | "vertical";
|
||||
|
||||
export interface SlotProps {
|
||||
orientation: 'horizontal' | 'vertical';
|
||||
orientation: "horizontal" | "vertical";
|
||||
style: CSSProperties;
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ interface Props {
|
||||
}
|
||||
|
||||
const baseProperties = { minWidth: 0 };
|
||||
const areaL = { ...baseProperties, gridArea: 'left' };
|
||||
const areaR = { ...baseProperties, gridArea: 'right' };
|
||||
const areaD = { ...baseProperties, gridArea: 'drag' };
|
||||
const areaL = { ...baseProperties, gridArea: "left" };
|
||||
const areaR = { ...baseProperties, gridArea: "right" };
|
||||
const areaD = { ...baseProperties, gridArea: "drag" };
|
||||
|
||||
const STACK_VERTICAL_WIDTH = 500;
|
||||
|
||||
@@ -42,7 +42,7 @@ export function SplitLayout({
|
||||
secondSlot,
|
||||
className,
|
||||
name,
|
||||
layout = 'responsive',
|
||||
layout = "responsive",
|
||||
resizeHandleClassName,
|
||||
defaultRatio = 0.5,
|
||||
minHeightPx = 10,
|
||||
@@ -51,10 +51,10 @@ export function SplitLayout({
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const activeWorkspace = useAtomValue(activeWorkspaceAtom);
|
||||
const [widthRaw, setWidth] = useLocalStorage<number>(
|
||||
`${name}_width::${activeWorkspace?.id ?? 'n/a'}`,
|
||||
`${name}_width::${activeWorkspace?.id ?? "n/a"}`,
|
||||
);
|
||||
const [heightRaw, setHeight] = useLocalStorage<number>(
|
||||
`${name}_height::${activeWorkspace?.id ?? 'n/a'}`,
|
||||
`${name}_height::${activeWorkspace?.id ?? "n/a"}`,
|
||||
);
|
||||
const width = widthRaw ?? defaultRatio;
|
||||
let height = heightRaw ?? defaultRatio;
|
||||
@@ -66,7 +66,7 @@ export function SplitLayout({
|
||||
|
||||
const size = useContainerSize(containerRef);
|
||||
const verticalBasedOnSize = size.width !== 0 && size.width < STACK_VERTICAL_WIDTH;
|
||||
const vertical = layout !== 'horizontal' && (layout === 'vertical' || verticalBasedOnSize);
|
||||
const vertical = layout !== "horizontal" && (layout === "vertical" || verticalBasedOnSize);
|
||||
|
||||
const styles = useMemo<CSSProperties>(() => {
|
||||
return {
|
||||
@@ -126,23 +126,23 @@ export function SplitLayout({
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={styles}
|
||||
className={classNames(className, 'grid w-full h-full overflow-hidden')}
|
||||
className={classNames(className, "grid w-full h-full overflow-hidden")}
|
||||
>
|
||||
{firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
|
||||
{firstSlot({ style: areaL, orientation: vertical ? "vertical" : "horizontal" })}
|
||||
{secondSlot && (
|
||||
<>
|
||||
<ResizeHandle
|
||||
style={areaD}
|
||||
className={classNames(
|
||||
resizeHandleClassName,
|
||||
vertical ? '-translate-y-1' : '-translate-x-1',
|
||||
vertical ? "-translate-y-1" : "-translate-x-1",
|
||||
)}
|
||||
onResizeMove={handleResizeMove}
|
||||
onReset={handleReset}
|
||||
side={vertical ? 'top' : 'left'}
|
||||
side={vertical ? "top" : "left"}
|
||||
justify="center"
|
||||
/>
|
||||
{secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
|
||||
{secondSlot({ style: areaR, orientation: vertical ? "vertical" : "horizontal" })}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ComponentType, ForwardedRef, HTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { ComponentType, ForwardedRef, HTMLAttributes, ReactNode } from "react";
|
||||
import { forwardRef } from "react";
|
||||
|
||||
const gapClasses = {
|
||||
0: 'gap-0',
|
||||
0.5: 'gap-0.5',
|
||||
1: 'gap-1',
|
||||
1.5: 'gap-1.5',
|
||||
2: 'gap-2',
|
||||
3: 'gap-3',
|
||||
4: 'gap-4',
|
||||
5: 'gap-5',
|
||||
6: 'gap-6',
|
||||
0: "gap-0",
|
||||
0.5: "gap-0.5",
|
||||
1: "gap-1",
|
||||
1.5: "gap-1.5",
|
||||
2: "gap-2",
|
||||
3: "gap-3",
|
||||
4: "gap-4",
|
||||
5: "gap-5",
|
||||
6: "gap-6",
|
||||
};
|
||||
|
||||
interface HStackProps extends BaseStackProps {
|
||||
@@ -19,14 +19,14 @@ interface HStackProps extends BaseStackProps {
|
||||
}
|
||||
|
||||
export const HStack = forwardRef(function HStack(
|
||||
{ className, space, children, alignItems = 'center', ...props }: HStackProps,
|
||||
{ className, space, children, alignItems = "center", ...props }: HStackProps,
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
return (
|
||||
<BaseStack
|
||||
ref={ref}
|
||||
className={classNames(className, 'flex-row', space != null && gapClasses[space])}
|
||||
className={classNames(className, "flex-row", space != null && gapClasses[space])}
|
||||
alignItems={alignItems}
|
||||
{...props}
|
||||
>
|
||||
@@ -47,7 +47,7 @@ export const VStack = forwardRef(function VStack(
|
||||
return (
|
||||
<BaseStack
|
||||
ref={ref}
|
||||
className={classNames(className, 'flex-col', space != null && gapClasses[space])}
|
||||
className={classNames(className, "flex-col", space != null && gapClasses[space])}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
@@ -56,10 +56,10 @@ export const VStack = forwardRef(function VStack(
|
||||
});
|
||||
|
||||
type BaseStackProps = HTMLAttributes<HTMLElement> & {
|
||||
as?: ComponentType | 'ul' | 'label' | 'form' | 'p';
|
||||
as?: ComponentType | "ul" | "label" | "form" | "p";
|
||||
space?: keyof typeof gapClasses;
|
||||
alignItems?: 'start' | 'center' | 'stretch' | 'end';
|
||||
justifyContent?: 'start' | 'center' | 'end' | 'between';
|
||||
alignItems?: "start" | "center" | "stretch" | "end";
|
||||
justifyContent?: "start" | "center" | "end" | "between";
|
||||
wrap?: boolean;
|
||||
};
|
||||
|
||||
@@ -68,22 +68,22 @@ const BaseStack = forwardRef(function BaseStack(
|
||||
// oxlint-disable-next-line no-explicit-any
|
||||
ref: ForwardedRef<any>,
|
||||
) {
|
||||
const Component = as ?? 'div';
|
||||
const Component = as ?? "div";
|
||||
return (
|
||||
<Component
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
className,
|
||||
'flex',
|
||||
wrap && 'flex-wrap',
|
||||
alignItems === 'center' && 'items-center',
|
||||
alignItems === 'start' && 'items-start',
|
||||
alignItems === 'stretch' && 'items-stretch',
|
||||
alignItems === 'end' && 'items-end',
|
||||
justifyContent === 'start' && 'justify-start',
|
||||
justifyContent === 'center' && 'justify-center',
|
||||
justifyContent === 'end' && 'justify-end',
|
||||
justifyContent === 'between' && 'justify-between',
|
||||
"flex",
|
||||
wrap && "flex-wrap",
|
||||
alignItems === "center" && "items-center",
|
||||
alignItems === "start" && "items-start",
|
||||
alignItems === "stretch" && "items-stretch",
|
||||
alignItems === "end" && "items-end",
|
||||
justifyContent === "start" && "justify-start",
|
||||
justifyContent === "center" && "justify-center",
|
||||
justifyContent === "end" && "justify-end",
|
||||
justifyContent === "between" && "justify-between",
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export function Table({
|
||||
children,
|
||||
@@ -11,13 +11,13 @@ export function Table({
|
||||
scrollable?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={classNames('w-full', scrollable && 'h-full overflow-y-auto')}>
|
||||
<div className={classNames("w-full", scrollable && "h-full overflow-y-auto")}>
|
||||
<table
|
||||
className={classNames(
|
||||
className,
|
||||
'w-full text-sm mb-auto min-w-full max-w-full',
|
||||
'border-separate border-spacing-0',
|
||||
scrollable && '[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-10',
|
||||
"w-full text-sm mb-auto min-w-full max-w-full",
|
||||
"border-separate border-spacing-0",
|
||||
scrollable && "[&_thead]:sticky [&_thead]:top-0 [&_thead]:z-10",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -39,7 +39,7 @@ export function TableHead({ children, className }: { children: ReactNode; classN
|
||||
<thead
|
||||
className={classNames(
|
||||
className,
|
||||
'bg-surface [&_th]:border-b [&_th]:border-b-surface-highlight',
|
||||
"bg-surface [&_th]:border-b [&_th]:border-b-surface-highlight",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -54,18 +54,18 @@ export function TableRow({ children }: { children: ReactNode }) {
|
||||
export function TableCell({
|
||||
children,
|
||||
className,
|
||||
align = 'left',
|
||||
align = "left",
|
||||
}: {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
align?: 'left' | 'center' | 'right';
|
||||
align?: "left" | "center" | "right";
|
||||
}) {
|
||||
return (
|
||||
<td
|
||||
className={classNames(
|
||||
className,
|
||||
'py-2 [&:not(:first-child)]:pl-4 whitespace-nowrap',
|
||||
align === 'left' ? 'text-left' : align === 'center' ? 'text-center' : 'text-right',
|
||||
"py-2 [&:not(:first-child)]:pl-4 whitespace-nowrap",
|
||||
align === "left" ? "text-left" : align === "center" ? "text-center" : "text-right",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
@@ -81,7 +81,7 @@ export function TruncatedWideTableCell({
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<TableCell className={classNames(className, 'truncate max-w-0 w-full')}>{children}</TableCell>
|
||||
<TableCell className={classNames(className, "truncate max-w-0 w-full")}>{children}</TableCell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ export function TableHeaderCell({
|
||||
<th
|
||||
className={classNames(
|
||||
className,
|
||||
'py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle',
|
||||
"py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
|
||||
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from "@dnd-kit/core";
|
||||
import {
|
||||
closestCenter,
|
||||
DndContext,
|
||||
@@ -8,9 +8,9 @@ import {
|
||||
useDroppable,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode, Ref } from 'react';
|
||||
} from "@dnd-kit/core";
|
||||
import classNames from "classnames";
|
||||
import type { ReactNode, Ref } from "react";
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
@@ -20,17 +20,17 @@ import {
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useKeyValue } from '../../../hooks/useKeyValue';
|
||||
import { fireAndForget } from '../../../lib/fireAndForget';
|
||||
import { computeSideForDragMove } from '../../../lib/dnd';
|
||||
import { DropMarker } from '../../DropMarker';
|
||||
import { ErrorBoundary } from '../../ErrorBoundary';
|
||||
import type { ButtonProps } from '../Button';
|
||||
import { Button } from '../Button';
|
||||
import { Icon } from '../Icon';
|
||||
import type { RadioDropdownProps } from '../RadioDropdown';
|
||||
import { RadioDropdown } from '../RadioDropdown';
|
||||
} from "react";
|
||||
import { useKeyValue } from "../../../hooks/useKeyValue";
|
||||
import { fireAndForget } from "../../../lib/fireAndForget";
|
||||
import { computeSideForDragMove } from "../../../lib/dnd";
|
||||
import { DropMarker } from "../../DropMarker";
|
||||
import { ErrorBoundary } from "../../ErrorBoundary";
|
||||
import type { ButtonProps } from "../Button";
|
||||
import { Button } from "../Button";
|
||||
import { Icon } from "../Icon";
|
||||
import type { RadioDropdownProps } from "../RadioDropdown";
|
||||
import { RadioDropdown } from "../RadioDropdown";
|
||||
|
||||
export type TabItem =
|
||||
| {
|
||||
@@ -42,7 +42,7 @@ export type TabItem =
|
||||
}
|
||||
| {
|
||||
value: string;
|
||||
options: Omit<RadioDropdownProps, 'children'>;
|
||||
options: Omit<RadioDropdownProps, "children">;
|
||||
leftSlot?: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
};
|
||||
@@ -68,7 +68,7 @@ interface Props {
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
addBorders?: boolean;
|
||||
layout?: 'horizontal' | 'vertical';
|
||||
layout?: "horizontal" | "vertical";
|
||||
/** Storage key for persisting tab order and active tab. When provided, enables drag-to-reorder and active tab persistence. */
|
||||
storageKey?: string | string[];
|
||||
/** Key to identify which context this tab belongs to (e.g., request ID). Used for per-context active tab persistence. */
|
||||
@@ -85,7 +85,7 @@ export const Tabs = forwardRef<TabsRef, Props>(function Tabs(
|
||||
className,
|
||||
tabListClassName,
|
||||
addBorders,
|
||||
layout = 'vertical',
|
||||
layout = "vertical",
|
||||
storageKey,
|
||||
activeTabKey,
|
||||
}: Props,
|
||||
@@ -97,8 +97,8 @@ export const Tabs = forwardRef<TabsRef, Props>(function Tabs(
|
||||
// Use key-value storage for persistence if storageKey is provided
|
||||
// Handle migration from old format (string[]) to new format (TabsStorage)
|
||||
const { value: rawStorage, set: setStorage } = useKeyValue<TabsStorage | string[]>({
|
||||
namespace: 'no_sync',
|
||||
key: storageKey ?? ['tabs', 'default'],
|
||||
namespace: "no_sync",
|
||||
key: storageKey ?? ["tabs", "default"],
|
||||
fallback: { order: [], activeTabs: {} },
|
||||
});
|
||||
|
||||
@@ -203,20 +203,20 @@ export const Tabs = forwardRef<TabsRef, Props>(function Tabs(
|
||||
|
||||
// Update tabs when value changes
|
||||
useEffect(() => {
|
||||
const tabs = ref.current?.querySelectorAll<HTMLDivElement>('[data-tab]');
|
||||
const tabs = ref.current?.querySelectorAll<HTMLDivElement>("[data-tab]");
|
||||
for (const tab of tabs ?? []) {
|
||||
const v = tab.getAttribute('data-tab');
|
||||
const parent = tab.closest('.tabs-container');
|
||||
const v = tab.getAttribute("data-tab");
|
||||
const parent = tab.closest(".tabs-container");
|
||||
if (parent !== ref.current) {
|
||||
// Tab is part of a nested tab container, so ignore it
|
||||
} else if (v === value) {
|
||||
tab.setAttribute('data-state', 'active');
|
||||
tab.setAttribute('aria-hidden', 'false');
|
||||
tab.style.display = 'block';
|
||||
tab.setAttribute("data-state", "active");
|
||||
tab.setAttribute("aria-hidden", "false");
|
||||
tab.style.display = "block";
|
||||
} else {
|
||||
tab.setAttribute('data-state', 'inactive');
|
||||
tab.setAttribute('aria-hidden', 'true');
|
||||
tab.style.display = 'none';
|
||||
tab.setAttribute("data-state", "inactive");
|
||||
tab.setAttribute("aria-hidden", "true");
|
||||
tab.style.display = "none";
|
||||
}
|
||||
}
|
||||
}, [value]);
|
||||
@@ -241,14 +241,14 @@ export const Tabs = forwardRef<TabsRef, Props>(function Tabs(
|
||||
if (overTab == null) return setHoveredIndex(null);
|
||||
|
||||
// For vertical layout, tabs are arranged horizontally (side-by-side)
|
||||
const orientation = layout === 'vertical' ? 'horizontal' : 'vertical';
|
||||
const orientation = layout === "vertical" ? "horizontal" : "vertical";
|
||||
const side = computeSideForDragMove(overTab.value, e, orientation);
|
||||
|
||||
// If computeSideForDragMove returns null (shouldn't happen but be safe), default to null
|
||||
if (side === null) return setHoveredIndex(null);
|
||||
|
||||
const overIndex = tabs.findIndex((t) => t.value === overId);
|
||||
const hoveredIndex = overIndex + (side === 'before' ? 0 : 1);
|
||||
const hoveredIndex = overIndex + (side === "before" ? 0 : 1);
|
||||
|
||||
setHoveredIndex(hoveredIndex);
|
||||
},
|
||||
@@ -291,7 +291,7 @@ export const Tabs = forwardRef<TabsRef, Props>(function Tabs(
|
||||
const tabButtons = useMemo(() => {
|
||||
const items: ReactNode[] = [];
|
||||
tabs.forEach((t, i) => {
|
||||
if ('hidden' in t && t.hidden) {
|
||||
if ("hidden" in t && t.hidden) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -302,9 +302,9 @@ export const Tabs = forwardRef<TabsRef, Props>(function Tabs(
|
||||
items.push(
|
||||
<div
|
||||
key={`marker-${t.value}`}
|
||||
className={classNames('relative', layout === 'vertical' ? 'w-0' : 'h-0')}
|
||||
className={classNames("relative", layout === "vertical" ? "w-0" : "h-0")}
|
||||
>
|
||||
<DropMarker orientation={layout === 'vertical' ? 'vertical' : 'horizontal'} />
|
||||
<DropMarker orientation={layout === "vertical" ? "vertical" : "horizontal"} />
|
||||
</div>,
|
||||
);
|
||||
}
|
||||
@@ -331,25 +331,25 @@ export const Tabs = forwardRef<TabsRef, Props>(function Tabs(
|
||||
aria-label={label}
|
||||
className={classNames(
|
||||
tabListClassName,
|
||||
addBorders && layout === 'horizontal' && 'pl-3 -ml-1',
|
||||
addBorders && layout === 'vertical' && 'ml-0 mb-2',
|
||||
'flex items-center hide-scrollbars',
|
||||
layout === 'horizontal' && 'h-full overflow-auto p-2',
|
||||
layout === 'vertical' && 'overflow-x-auto overflow-y-visible ',
|
||||
addBorders && layout === "horizontal" && "pl-3 -ml-1",
|
||||
addBorders && layout === "vertical" && "ml-0 mb-2",
|
||||
"flex items-center hide-scrollbars",
|
||||
layout === "horizontal" && "h-full overflow-auto p-2",
|
||||
layout === "vertical" && "overflow-x-auto overflow-y-visible ",
|
||||
// Give space for button focus states within overflow boundary.
|
||||
!addBorders && layout === 'vertical' && 'py-1 pl-3 -ml-5 pr-1',
|
||||
!addBorders && layout === "vertical" && "py-1 pl-3 -ml-5 pr-1",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
layout === 'horizontal' && 'flex flex-col w-full pb-3 mb-auto',
|
||||
layout === 'vertical' && 'flex flex-row flex-shrink-0 w-full',
|
||||
layout === "horizontal" && "flex flex-col w-full pb-3 mb-auto",
|
||||
layout === "vertical" && "flex flex-row flex-shrink-0 w-full",
|
||||
)}
|
||||
>
|
||||
{tabButtons}
|
||||
{hoveredIndex === tabs.length && (
|
||||
<div className={classNames('relative', layout === 'vertical' ? 'w-0' : 'h-0')}>
|
||||
<DropMarker orientation={layout === 'vertical' ? 'vertical' : 'horizontal'} />
|
||||
<div className={classNames("relative", layout === "vertical" ? "w-0" : "h-0")}>
|
||||
<DropMarker orientation={layout === "vertical" ? "vertical" : "horizontal"} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -361,10 +361,10 @@ export const Tabs = forwardRef<TabsRef, Props>(function Tabs(
|
||||
ref={ref}
|
||||
className={classNames(
|
||||
className,
|
||||
'tabs-container',
|
||||
'h-full grid',
|
||||
layout === 'horizontal' && 'grid-rows-1 grid-cols-[auto_minmax(0,1fr)]',
|
||||
layout === 'vertical' && 'grid-rows-[auto_minmax(0,1fr)] grid-cols-1',
|
||||
"tabs-container",
|
||||
"h-full grid",
|
||||
layout === "horizontal" && "grid-rows-1 grid-cols-[auto_minmax(0,1fr)]",
|
||||
layout === "vertical" && "grid-rows-[auto_minmax(0,1fr)] grid-cols-1",
|
||||
)}
|
||||
>
|
||||
{reorderable ? (
|
||||
@@ -405,7 +405,7 @@ interface TabButtonProps {
|
||||
tab: TabItem;
|
||||
isActive: boolean;
|
||||
addBorders?: boolean;
|
||||
layout: 'horizontal' | 'vertical';
|
||||
layout: "horizontal" | "vertical";
|
||||
reorderable: boolean;
|
||||
isDragging: boolean;
|
||||
onChangeValue?: (value: string) => void;
|
||||
@@ -448,8 +448,8 @@ function TabButton({
|
||||
);
|
||||
|
||||
const btnProps: Partial<ButtonProps> = {
|
||||
color: 'custom',
|
||||
justify: layout === 'horizontal' ? 'start' : 'center',
|
||||
color: "custom",
|
||||
justify: layout === "horizontal" ? "start" : "center",
|
||||
onClick: isActive
|
||||
? undefined
|
||||
: (e: React.MouseEvent) => {
|
||||
@@ -457,27 +457,27 @@ function TabButton({
|
||||
onChangeValue?.(tab.value);
|
||||
},
|
||||
className: classNames(
|
||||
'flex items-center rounded whitespace-nowrap',
|
||||
'!px-2 ml-[1px]',
|
||||
'outline-none',
|
||||
'ring-none',
|
||||
'focus-visible-or-class:outline-2',
|
||||
addBorders && 'border focus-visible:bg-surface-highlight',
|
||||
isActive ? 'text-text' : 'text-text-subtle',
|
||||
"flex items-center rounded whitespace-nowrap",
|
||||
"!px-2 ml-[1px]",
|
||||
"outline-none",
|
||||
"ring-none",
|
||||
"focus-visible-or-class:outline-2",
|
||||
addBorders && "border focus-visible:bg-surface-highlight",
|
||||
isActive ? "text-text" : "text-text-subtle",
|
||||
isActive && addBorders
|
||||
? 'border-surface-active bg-surface-active'
|
||||
: layout === 'vertical'
|
||||
? 'border-border-subtle'
|
||||
: 'border-transparent',
|
||||
layout === 'horizontal' && 'min-w-[10rem]',
|
||||
isDragging && 'opacity-50',
|
||||
overlay && 'opacity-80',
|
||||
? "border-surface-active bg-surface-active"
|
||||
: layout === "vertical"
|
||||
? "border-border-subtle"
|
||||
: "border-transparent",
|
||||
layout === "horizontal" && "min-w-[10rem]",
|
||||
isDragging && "opacity-50",
|
||||
overlay && "opacity-80",
|
||||
),
|
||||
};
|
||||
|
||||
const buttonContent = (() => {
|
||||
if ('options' in tab) {
|
||||
const option = tab.options.items.find((i) => 'value' in i && i.value === tab.options.value);
|
||||
if ("options" in tab) {
|
||||
const option = tab.options.items.find((i) => "value" in i && i.value === tab.options.value);
|
||||
return (
|
||||
<RadioDropdown
|
||||
key={tab.value}
|
||||
@@ -496,24 +496,24 @@ function TabButton({
|
||||
size="sm"
|
||||
icon="chevron_down"
|
||||
className={classNames(
|
||||
'ml-1',
|
||||
isActive ? 'text-text-subtle' : 'text-text-subtlest',
|
||||
"ml-1",
|
||||
isActive ? "text-text-subtle" : "text-text-subtlest",
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{...btnProps}
|
||||
>
|
||||
{option && 'shortLabel' in option && option.shortLabel
|
||||
{option && "shortLabel" in option && option.shortLabel
|
||||
? option.shortLabel
|
||||
: (option?.label ?? 'Unknown')}
|
||||
: (option?.label ?? "Unknown")}
|
||||
</Button>
|
||||
</RadioDropdown>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button leftSlot={tab.leftSlot} rightSlot={tab.rightSlot} {...btnProps}>
|
||||
{'label' in tab && tab.label ? tab.label : tab.value}
|
||||
{"label" in tab && tab.label ? tab.label : tab.value}
|
||||
</Button>
|
||||
);
|
||||
})();
|
||||
@@ -524,7 +524,7 @@ function TabButton({
|
||||
return (
|
||||
<div
|
||||
ref={handleSetWrapperRef}
|
||||
className={classNames('relative', layout === 'vertical' && 'mr-2')}
|
||||
className={classNames("relative", layout === "vertical" && "mr-2")}
|
||||
{...wrapperProps}
|
||||
>
|
||||
{buttonContent}
|
||||
@@ -548,7 +548,7 @@ export const TabContent = memo(function TabContent({
|
||||
<div
|
||||
tabIndex={-1}
|
||||
data-tab={value}
|
||||
className={classNames(className, 'tab-content', 'hidden w-full h-full pt-2')}
|
||||
className={classNames(className, "tab-content", "hidden w-full h-full pt-2")}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
@@ -569,14 +569,14 @@ export async function setActiveTab({
|
||||
activeTabKey: string;
|
||||
value: string;
|
||||
}): Promise<void> {
|
||||
const { getKeyValue, setKeyValue } = await import('../../../lib/keyValueStore');
|
||||
const { getKeyValue, setKeyValue } = await import("../../../lib/keyValueStore");
|
||||
const current = getKeyValue<TabsStorage>({
|
||||
namespace: 'no_sync',
|
||||
namespace: "no_sync",
|
||||
key: storageKey,
|
||||
fallback: { order: [], activeTabs: {} },
|
||||
});
|
||||
await setKeyValue({
|
||||
namespace: 'no_sync',
|
||||
namespace: "no_sync",
|
||||
key: storageKey,
|
||||
value: {
|
||||
...current,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import type { ShowToastRequest } from '@yaakapp-internal/plugins';
|
||||
import classNames from 'classnames';
|
||||
import * as m from 'motion/react-m';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { ShowToastRequest } from "@yaakapp-internal/plugins";
|
||||
import classNames from "classnames";
|
||||
import * as m from "motion/react-m";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { useKey } from 'react-use';
|
||||
import type { IconProps } from './Icon';
|
||||
import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
import { VStack } from './Stacks';
|
||||
import { useKey } from "react-use";
|
||||
import type { IconProps } from "./Icon";
|
||||
import { Icon } from "./Icon";
|
||||
import { IconButton } from "./IconButton";
|
||||
import { VStack } from "./Stacks";
|
||||
|
||||
export interface ToastProps {
|
||||
children: ReactNode;
|
||||
@@ -16,24 +16,24 @@ export interface ToastProps {
|
||||
className?: string;
|
||||
timeout: number | null;
|
||||
action?: (args: { hide: () => void }) => ReactNode;
|
||||
icon?: ShowToastRequest['icon'] | null;
|
||||
color?: ShowToastRequest['color'];
|
||||
icon?: ShowToastRequest["icon"] | null;
|
||||
color?: ShowToastRequest["color"];
|
||||
}
|
||||
|
||||
const ICONS: Record<NonNullable<ToastProps['color'] | 'custom'>, IconProps['icon'] | null> = {
|
||||
const ICONS: Record<NonNullable<ToastProps["color"] | "custom">, IconProps["icon"] | null> = {
|
||||
custom: null,
|
||||
danger: 'alert_triangle',
|
||||
info: 'info',
|
||||
notice: 'alert_triangle',
|
||||
primary: 'info',
|
||||
secondary: 'info',
|
||||
success: 'check_circle',
|
||||
warning: 'alert_triangle',
|
||||
danger: "alert_triangle",
|
||||
info: "info",
|
||||
notice: "alert_triangle",
|
||||
primary: "info",
|
||||
secondary: "info",
|
||||
success: "check_circle",
|
||||
warning: "alert_triangle",
|
||||
};
|
||||
|
||||
export function Toast({ children, open, onClose, timeout, action, icon, color }: ToastProps) {
|
||||
useKey(
|
||||
'Escape',
|
||||
"Escape",
|
||||
() => {
|
||||
if (!open) return;
|
||||
onClose();
|
||||
@@ -46,18 +46,18 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
|
||||
|
||||
return (
|
||||
<m.div
|
||||
initial={{ opacity: 0, right: '-10%' }}
|
||||
initial={{ opacity: 0, right: "-10%" }}
|
||||
animate={{ opacity: 100, right: 0 }}
|
||||
exit={{ opacity: 0, right: '-100%' }}
|
||||
exit={{ opacity: 0, right: "-100%" }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className={classNames('bg-surface m-2 rounded-lg')}
|
||||
className={classNames("bg-surface m-2 rounded-lg")}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
`x-theme-toast x-theme-toast--${color}`,
|
||||
'pointer-events-auto overflow-hidden',
|
||||
'relative pointer-events-auto bg-surface text-text rounded-lg',
|
||||
'border border-border shadow-lg w-[25rem]',
|
||||
"pointer-events-auto overflow-hidden",
|
||||
"relative pointer-events-auto bg-surface text-text rounded-lg",
|
||||
"border border-border shadow-lg w-[25rem]",
|
||||
)}
|
||||
>
|
||||
<div className="pl-3 py-3 pr-10 flex items-start gap-2 w-full max-h-[11rem] overflow-auto">
|
||||
@@ -81,9 +81,9 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
|
||||
<div className="w-full absolute bottom-0 left-0 right-0">
|
||||
<m.div
|
||||
className="bg-surface-highlight h-[3px]"
|
||||
initial={{ width: '100%' }}
|
||||
animate={{ width: '0%', opacity: 0.2 }}
|
||||
transition={{ duration: timeout / 1000, ease: 'linear' }}
|
||||
initial={{ width: "100%" }}
|
||||
animate={{ width: "0%", opacity: 0.2 }}
|
||||
transition={{ duration: timeout / 1000, ease: "linear" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties, KeyboardEvent, ReactNode } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { generateId } from '../../lib/generateId';
|
||||
import { Portal } from '../Portal';
|
||||
import classNames from "classnames";
|
||||
import type { CSSProperties, KeyboardEvent, ReactNode } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { generateId } from "../../lib/generateId";
|
||||
import { Portal } from "../Portal";
|
||||
|
||||
export interface TooltipProps {
|
||||
children: ReactNode;
|
||||
content: ReactNode;
|
||||
tabIndex?: number;
|
||||
size?: 'md' | 'lg';
|
||||
size?: "md" | "lg";
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const hiddenStyles: CSSProperties = {
|
||||
left: -99999,
|
||||
top: -99999,
|
||||
visibility: 'hidden',
|
||||
pointerEvents: 'none',
|
||||
visibility: "hidden",
|
||||
pointerEvents: "none",
|
||||
opacity: 0,
|
||||
};
|
||||
|
||||
type TooltipPosition = 'top' | 'bottom';
|
||||
type TooltipPosition = "top" | "bottom";
|
||||
|
||||
interface TooltipOpenState {
|
||||
styles: CSSProperties;
|
||||
position: TooltipPosition;
|
||||
}
|
||||
|
||||
export function Tooltip({ children, className, content, tabIndex, size = 'md' }: TooltipProps) {
|
||||
export function Tooltip({ children, className, content, tabIndex, size = "md" }: TooltipProps) {
|
||||
const [openState, setOpenState] = useState<TooltipOpenState | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const tooltipRef = useRef<HTMLDivElement>(null);
|
||||
@@ -44,12 +44,12 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
const spaceAbove = Math.max(0, triggerRect.top - margin);
|
||||
const spaceBelow = Math.max(0, viewportHeight - triggerRect.bottom - margin);
|
||||
const preferBottom = spaceAbove < tooltipRect.height + margin && spaceBelow > spaceAbove;
|
||||
const position: TooltipPosition = preferBottom ? 'bottom' : 'top';
|
||||
const position: TooltipPosition = preferBottom ? "bottom" : "top";
|
||||
|
||||
const styles: CSSProperties = {
|
||||
left: Math.max(0, triggerRect.left + triggerRect.width / 2 - tooltipRect.width / 2),
|
||||
maxHeight: position === 'top' ? spaceAbove : spaceBelow,
|
||||
...(position === 'top'
|
||||
maxHeight: position === "top" ? spaceAbove : spaceBelow,
|
||||
...(position === "top"
|
||||
? { bottom: viewportHeight - triggerRect.top }
|
||||
: { top: triggerRect.bottom }),
|
||||
};
|
||||
@@ -73,7 +73,7 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLButtonElement>) => {
|
||||
if (openState && e.key === 'Escape') {
|
||||
if (openState && e.key === "Escape") {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleClose();
|
||||
@@ -97,16 +97,16 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'bg-surface-highlight rounded-md px-3 py-2 z-50 border border-border overflow-auto',
|
||||
size === 'md' && 'max-w-sm',
|
||||
size === 'lg' && 'max-w-md',
|
||||
"bg-surface-highlight rounded-md px-3 py-2 z-50 border border-border overflow-auto",
|
||||
size === "md" && "max-w-sm",
|
||||
size === "lg" && "max-w-md",
|
||||
)}
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
<Triangle
|
||||
className="text-border"
|
||||
position={openState?.position === 'bottom' ? 'top' : 'bottom'}
|
||||
position={openState?.position === "bottom" ? "top" : "bottom"}
|
||||
/>
|
||||
</div>
|
||||
</Portal>
|
||||
@@ -116,7 +116,7 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
role="button"
|
||||
aria-describedby={openState ? id.current : undefined}
|
||||
tabIndex={tabIndex ?? -1}
|
||||
className={classNames(className, 'flex-grow-0 flex items-center')}
|
||||
className={classNames(className, "flex-grow-0 flex items-center")}
|
||||
onClick={handleToggleImmediate}
|
||||
onMouseEnter={handleOpen}
|
||||
onMouseLeave={handleClose}
|
||||
@@ -130,8 +130,8 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
|
||||
);
|
||||
}
|
||||
|
||||
function Triangle({ className, position }: { className?: string; position: 'top' | 'bottom' }) {
|
||||
const isBottom = position === 'bottom';
|
||||
function Triangle({ className, position }: { className?: string; position: "top" | "bottom" }) {
|
||||
const isBottom = position === "bottom";
|
||||
|
||||
return (
|
||||
<svg
|
||||
@@ -141,19 +141,19 @@ function Triangle({ className, position }: { className?: string; position: 'top'
|
||||
shapeRendering="crispEdges"
|
||||
className={classNames(
|
||||
className,
|
||||
'absolute z-50 left-[calc(50%-0.4rem)] h-[0.5rem] w-[0.8rem]',
|
||||
"absolute z-50 left-[calc(50%-0.4rem)] h-[0.5rem] w-[0.8rem]",
|
||||
isBottom
|
||||
? 'border-t-[2px] border-surface-highlight -bottom-[calc(0.5rem-3px)] mb-2'
|
||||
: 'border-b-[2px] border-surface-highlight -top-[calc(0.5rem-3px)] mt-2',
|
||||
? "border-t-[2px] border-surface-highlight -bottom-[calc(0.5rem-3px)] mb-2"
|
||||
: "border-b-[2px] border-surface-highlight -top-[calc(0.5rem-3px)] mt-2",
|
||||
)}
|
||||
>
|
||||
<title>Triangle</title>
|
||||
<polygon
|
||||
className="fill-surface-highlight"
|
||||
points={isBottom ? '0,0 30,0 15,10' : '0,10 30,10 15,0'}
|
||||
points={isBottom ? "0,0 30,0 15,10" : "0,10 30,10 15,0"}
|
||||
/>
|
||||
<path
|
||||
d={isBottom ? 'M0 0 L15 9 L30 0' : 'M0 10 L15 1 L30 10'}
|
||||
d={isBottom ? "M0 0 L15 9 L30 0" : "M0 10 L15 1 L30 10"}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="1"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { WebsocketConnection } from '@yaakapp-internal/models';
|
||||
import classNames from 'classnames';
|
||||
import type { WebsocketConnection } from "@yaakapp-internal/models";
|
||||
import classNames from "classnames";
|
||||
|
||||
interface Props {
|
||||
connection: WebsocketConnection;
|
||||
@@ -10,22 +10,22 @@ export function WebsocketStatusTag({ connection, className }: Props) {
|
||||
const { state, error } = connection;
|
||||
|
||||
let label: string;
|
||||
let colorClass = 'text-text-subtle';
|
||||
let colorClass = "text-text-subtle";
|
||||
|
||||
if (error) {
|
||||
label = 'ERROR';
|
||||
colorClass = 'text-danger';
|
||||
} else if (state === 'connected') {
|
||||
label = 'CONNECTED';
|
||||
colorClass = 'text-success';
|
||||
} else if (state === 'closing') {
|
||||
label = 'CLOSING';
|
||||
} else if (state === 'closed') {
|
||||
label = 'CLOSED';
|
||||
colorClass = 'text-warning';
|
||||
label = "ERROR";
|
||||
colorClass = "text-danger";
|
||||
} else if (state === "connected") {
|
||||
label = "CONNECTED";
|
||||
colorClass = "text-success";
|
||||
} else if (state === "closing") {
|
||||
label = "CLOSING";
|
||||
} else if (state === "closed") {
|
||||
label = "CLOSED";
|
||||
colorClass = "text-warning";
|
||||
} else {
|
||||
label = 'CONNECTING';
|
||||
label = "CONNECTING";
|
||||
}
|
||||
|
||||
return <span className={classNames(className, 'font-mono', colorClass)}>{label}</span>;
|
||||
return <span className={classNames(className, "font-mono", colorClass)}>{label}</span>;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
|
||||
import type { DragEndEvent, DragMoveEvent, DragStartEvent } from "@dnd-kit/core";
|
||||
import {
|
||||
DndContext,
|
||||
MeasuringStrategy,
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
useDroppable,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from '@dnd-kit/core';
|
||||
import { type } from '@tauri-apps/plugin-os';
|
||||
import classNames from 'classnames';
|
||||
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from 'react';
|
||||
} from "@dnd-kit/core";
|
||||
import { type } from "@tauri-apps/plugin-os";
|
||||
import classNames from "classnames";
|
||||
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from "react";
|
||||
import {
|
||||
forwardRef,
|
||||
memo,
|
||||
@@ -20,14 +20,14 @@ import {
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import type { HotKeyOptions, HotkeyAction } from '../../../hooks/useHotKey';
|
||||
import { useHotKey } from '../../../hooks/useHotKey';
|
||||
import { computeSideForDragMove } from '../../../lib/dnd';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import type { ContextMenuProps, DropdownItem } from '../Dropdown';
|
||||
import { ContextMenu } from '../Dropdown';
|
||||
} from "react";
|
||||
import { useKey, useKeyPressEvent } from "react-use";
|
||||
import type { HotKeyOptions, HotkeyAction } from "../../../hooks/useHotKey";
|
||||
import { useHotKey } from "../../../hooks/useHotKey";
|
||||
import { computeSideForDragMove } from "../../../lib/dnd";
|
||||
import { jotaiStore } from "../../../lib/jotai";
|
||||
import type { ContextMenuProps, DropdownItem } from "../Dropdown";
|
||||
import { ContextMenu } from "../Dropdown";
|
||||
import {
|
||||
collapsedFamily,
|
||||
draggingIdsFamily,
|
||||
@@ -35,14 +35,14 @@ import {
|
||||
hoveredParentFamily,
|
||||
isCollapsedFamily,
|
||||
selectedIdsFamily,
|
||||
} from './atoms';
|
||||
import type { SelectableTreeNode, TreeNode } from './common';
|
||||
import { closestVisibleNode, equalSubtree, getSelectedItems, hasAncestor } from './common';
|
||||
import { TreeDragOverlay } from './TreeDragOverlay';
|
||||
import type { TreeItemClickEvent, TreeItemHandle, TreeItemProps } from './TreeItem';
|
||||
import type { TreeItemListProps } from './TreeItemList';
|
||||
import { TreeItemList } from './TreeItemList';
|
||||
import { useSelectableItems } from './useSelectableItems';
|
||||
} from "./atoms";
|
||||
import type { SelectableTreeNode, TreeNode } from "./common";
|
||||
import { closestVisibleNode, equalSubtree, getSelectedItems, hasAncestor } from "./common";
|
||||
import { TreeDragOverlay } from "./TreeDragOverlay";
|
||||
import type { TreeItemClickEvent, TreeItemHandle, TreeItemProps } from "./TreeItem";
|
||||
import type { TreeItemListProps } from "./TreeItemList";
|
||||
import { TreeItemList } from "./TreeItemList";
|
||||
import { useSelectableItems } from "./useSelectableItems";
|
||||
|
||||
/** So we re-calculate after expanding a folder during drag */
|
||||
const measuring = { droppable: { strategy: MeasuringStrategy.Always } };
|
||||
@@ -51,7 +51,7 @@ export interface TreeProps<T extends { id: string }> {
|
||||
root: TreeNode<T>;
|
||||
treeId: string;
|
||||
getItemKey: (item: T) => string;
|
||||
getContextMenu?: (items: T[]) => ContextMenuProps['items'] | Promise<ContextMenuProps['items']>;
|
||||
getContextMenu?: (items: T[]) => ContextMenuProps["items"] | Promise<ContextMenuProps["items"]>;
|
||||
ItemInner: ComponentType<{ treeId: string; item: T }>;
|
||||
ItemLeftSlotInner?: ComponentType<{ treeId: string; item: T }>;
|
||||
ItemRightSlot?: ComponentType<{ treeId: string; item: T }>;
|
||||
@@ -140,7 +140,7 @@ function TreeInner<T extends { id: string }>(
|
||||
return false;
|
||||
}
|
||||
$el.focus();
|
||||
$el.scrollIntoView({ block: 'nearest' });
|
||||
$el.scrollIntoView({ block: "nearest" });
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
@@ -241,7 +241,7 @@ function TreeInner<T extends { id: string }>(
|
||||
};
|
||||
}, [getContextMenu, selectableItems, setSelected, treeId]);
|
||||
|
||||
const handleSelect = useCallback<NonNullable<TreeItemProps<T>['onClick']>>(
|
||||
const handleSelect = useCallback<NonNullable<TreeItemProps<T>["onClick"]>>(
|
||||
(item, { shiftKey, metaKey, ctrlKey }) => {
|
||||
const anchorSelectedId = jotaiStore.get(focusIdsFamily(treeId)).anchorId;
|
||||
const selectedIdsAtom = selectedIdsFamily(treeId);
|
||||
@@ -281,7 +281,7 @@ function TreeInner<T extends { id: string }>(
|
||||
} else {
|
||||
setSelected([item.id], true);
|
||||
}
|
||||
} else if (type() === 'macos' ? metaKey : ctrlKey) {
|
||||
} else if (type() === "macos" ? metaKey : ctrlKey) {
|
||||
const withoutCurr = selectedIds.filter((id) => id !== item.id);
|
||||
if (withoutCurr.length === selectedIds.length) {
|
||||
// It wasn't in there, so add it
|
||||
@@ -299,7 +299,7 @@ function TreeInner<T extends { id: string }>(
|
||||
[selectableItems, setSelected, treeId],
|
||||
);
|
||||
|
||||
const handleClick = useCallback<NonNullable<TreeItemProps<T>['onClick']>>(
|
||||
const handleClick = useCallback<NonNullable<TreeItemProps<T>["onClick"]>>(
|
||||
(item, e) => {
|
||||
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
||||
handleSelect(item, e);
|
||||
@@ -350,7 +350,7 @@ function TreeInner<T extends { id: string }>(
|
||||
);
|
||||
|
||||
useKey(
|
||||
(e) => e.key === 'ArrowUp' || e.key.toLowerCase() === 'k',
|
||||
(e) => e.key === "ArrowUp" || e.key.toLowerCase() === "k",
|
||||
(e) => {
|
||||
if (!isTreeFocused()) return;
|
||||
e.preventDefault();
|
||||
@@ -361,7 +361,7 @@ function TreeInner<T extends { id: string }>(
|
||||
);
|
||||
|
||||
useKey(
|
||||
(e) => e.key === 'ArrowDown' || e.key.toLowerCase() === 'j',
|
||||
(e) => e.key === "ArrowDown" || e.key.toLowerCase() === "j",
|
||||
(e) => {
|
||||
if (!isTreeFocused()) return;
|
||||
e.preventDefault();
|
||||
@@ -373,7 +373,7 @@ function TreeInner<T extends { id: string }>(
|
||||
|
||||
// If the selected item is a collapsed folder, expand it. Otherwise, select next item
|
||||
useKey(
|
||||
(e) => e.key === 'ArrowRight' || e.key === 'l',
|
||||
(e) => e.key === "ArrowRight" || e.key === "l",
|
||||
(e) => {
|
||||
if (!isTreeFocused()) return;
|
||||
e.preventDefault();
|
||||
@@ -399,7 +399,7 @@ function TreeInner<T extends { id: string }>(
|
||||
// If the selected item is in a folder, select its parent.
|
||||
// If the selected item is an expanded folder, collapse it.
|
||||
useKey(
|
||||
(e) => e.key === 'ArrowLeft' || e.key === 'h',
|
||||
(e) => e.key === "ArrowLeft" || e.key === "h",
|
||||
(e) => {
|
||||
if (!isTreeFocused()) return;
|
||||
e.preventDefault();
|
||||
@@ -422,7 +422,7 @@ function TreeInner<T extends { id: string }>(
|
||||
[selectableItems, handleSelect],
|
||||
);
|
||||
|
||||
useKeyPressEvent('Escape', async () => {
|
||||
useKeyPressEvent("Escape", async () => {
|
||||
if (!treeRef.current?.contains(document.activeElement)) return;
|
||||
clearDragState();
|
||||
const lastSelectedId = jotaiStore.get(focusIdsFamily(treeId)).lastId;
|
||||
@@ -486,11 +486,11 @@ function TreeInner<T extends { id: string }>(
|
||||
let hoveredParent = node.parent;
|
||||
const dragIndex = selectableItems.findIndex((n) => n.node.item.id === item.id) ?? -1;
|
||||
const hovered = selectableItems[dragIndex]?.node ?? null;
|
||||
const hoveredIndex = dragIndex + (side === 'before' ? 0 : 1);
|
||||
let hoveredChildIndex = overSelectableItem.index + (side === 'before' ? 0 : 1);
|
||||
const hoveredIndex = dragIndex + (side === "before" ? 0 : 1);
|
||||
let hoveredChildIndex = overSelectableItem.index + (side === "before" ? 0 : 1);
|
||||
|
||||
// Move into the folder if it's open and we're moving after it
|
||||
if (hovered?.children != null && side === 'after') {
|
||||
if (hovered?.children != null && side === "after") {
|
||||
hoveredParent = hovered;
|
||||
hoveredChildIndex = 0;
|
||||
}
|
||||
@@ -620,7 +620,7 @@ function TreeInner<T extends { id: string }>(
|
||||
|
||||
const treeItemListProps: Omit<
|
||||
TreeItemListProps<T>,
|
||||
'nodes' | 'treeId' | 'activeIdAtom' | 'hoveredParent' | 'hoveredIndex'
|
||||
"nodes" | "treeId" | "activeIdAtom" | "hoveredParent" | "hoveredIndex"
|
||||
> = {
|
||||
getItemKey,
|
||||
getContextMenu: handleGetContextMenu,
|
||||
@@ -670,24 +670,24 @@ function TreeInner<T extends { id: string }>(
|
||||
ref={treeRef}
|
||||
className={classNames(
|
||||
className,
|
||||
'outline-none h-full',
|
||||
'overflow-y-auto overflow-x-hidden',
|
||||
'grid grid-rows-[auto_1fr]',
|
||||
"outline-none h-full",
|
||||
"overflow-y-auto overflow-x-hidden",
|
||||
"grid grid-rows-[auto_1fr]",
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
'[&_.tree-item.selected_.tree-item-inner]:text-text',
|
||||
'[&:focus-within]:[&_.tree-item.selected]:bg-surface-active',
|
||||
'[&:not(:focus-within)]:[&_.tree-item.selected:not([data-context-menu-open])]:bg-surface-highlight',
|
||||
'[&_.tree-item.selected[data-context-menu-open]]:bg-surface-active',
|
||||
"[&_.tree-item.selected_.tree-item-inner]:text-text",
|
||||
"[&:focus-within]:[&_.tree-item.selected]:bg-surface-active",
|
||||
"[&:not(:focus-within)]:[&_.tree-item.selected:not([data-context-menu-open])]:bg-surface-highlight",
|
||||
"[&_.tree-item.selected[data-context-menu-open]]:bg-surface-active",
|
||||
// Round the items, but only if the ends of the selection.
|
||||
// Also account for the drop marker being in between items
|
||||
'[&_.tree-item]:rounded-md',
|
||||
'[&_.tree-item.selected+.tree-item.selected]:rounded-t-none',
|
||||
'[&_.tree-item.selected+.drop-marker+.tree-item.selected]:rounded-t-none',
|
||||
'[&_.tree-item.selected:has(+.tree-item.selected)]:rounded-b-none',
|
||||
'[&_.tree-item.selected:has(+.drop-marker+.tree-item.selected)]:rounded-b-none',
|
||||
"[&_.tree-item]:rounded-md",
|
||||
"[&_.tree-item.selected+.tree-item.selected]:rounded-t-none",
|
||||
"[&_.tree-item.selected+.drop-marker+.tree-item.selected]:rounded-t-none",
|
||||
"[&_.tree-item.selected:has(+.tree-item.selected)]:rounded-b-none",
|
||||
"[&_.tree-item.selected:has(+.drop-marker+.tree-item.selected)]:rounded-b-none",
|
||||
)}
|
||||
>
|
||||
<TreeItemList
|
||||
@@ -766,7 +766,7 @@ function TreeHotKey<T extends { id: string }>({
|
||||
...options,
|
||||
enable: () => {
|
||||
if (enable == null) return true;
|
||||
if (typeof enable === 'function') return enable();
|
||||
if (typeof enable === "function") return enable();
|
||||
return enable;
|
||||
},
|
||||
},
|
||||
@@ -780,7 +780,7 @@ function TreeHotKeys<T extends { id: string }>({
|
||||
selectableItems,
|
||||
}: {
|
||||
treeId: string;
|
||||
hotkeys: TreeProps<T>['hotkeys'];
|
||||
hotkeys: TreeProps<T>["hotkeys"];
|
||||
selectableItems: SelectableTreeNode<T>[];
|
||||
}) {
|
||||
if (hotkeys == null) return null;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { DragOverlay } from '@dnd-kit/core';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { draggingIdsFamily } from './atoms';
|
||||
import type { SelectableTreeNode } from './common';
|
||||
import type { TreeProps } from './Tree';
|
||||
import { TreeItemList } from './TreeItemList';
|
||||
import { DragOverlay } from "@dnd-kit/core";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { draggingIdsFamily } from "./atoms";
|
||||
import type { SelectableTreeNode } from "./common";
|
||||
import type { TreeProps } from "./Tree";
|
||||
import { TreeItemList } from "./TreeItemList";
|
||||
|
||||
export function TreeDragOverlay<T extends { id: string }>({
|
||||
treeId,
|
||||
@@ -14,7 +14,7 @@ export function TreeDragOverlay<T extends { id: string }>({
|
||||
}: {
|
||||
treeId: string;
|
||||
selectableItems: SelectableTreeNode<T>[];
|
||||
} & Pick<TreeProps<T>, 'getItemKey' | 'ItemInner' | 'ItemLeftSlotInner'>) {
|
||||
} & Pick<TreeProps<T>, "getItemKey" | "ItemInner" | "ItemLeftSlotInner">) {
|
||||
const draggingItems = useAtomValue(draggingIdsFamily(treeId));
|
||||
return (
|
||||
<DragOverlay dropAnimation={null}>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { memo } from 'react';
|
||||
import { DropMarker } from '../../DropMarker';
|
||||
import { hoveredParentDepthFamily, isCollapsedFamily, isIndexHoveredFamily } from './atoms';
|
||||
import type { TreeNode } from './common';
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo } from "react";
|
||||
import { DropMarker } from "../../DropMarker";
|
||||
import { hoveredParentDepthFamily, isCollapsedFamily, isIndexHoveredFamily } from "./atoms";
|
||||
import type { TreeNode } from "./common";
|
||||
|
||||
export const TreeDropMarker = memo(function TreeDropMarker<T extends { id: string }>({
|
||||
className,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { memo } from 'react';
|
||||
import { hoveredParentDepthFamily, isAncestorHoveredFamily } from './atoms';
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { memo } from "react";
|
||||
import { hoveredParentDepthFamily, isAncestorHoveredFamily } from "./atoms";
|
||||
|
||||
export const TreeIndentGuide = memo(function TreeIndentGuide({
|
||||
treeId,
|
||||
@@ -22,8 +22,8 @@ export const TreeIndentGuide = memo(function TreeIndentGuide({
|
||||
// oxlint-disable-next-line react/no-array-index-key
|
||||
key={i}
|
||||
className={classNames(
|
||||
'w-[calc(1rem+0.5px)] border-r border-r-text-subtlest',
|
||||
!(parentDepth === i + 1 && isHovered) && 'opacity-30',
|
||||
"w-[calc(1rem+0.5px)] border-r border-r-text-subtlest",
|
||||
!(parentDepth === i + 1 && isHovered) && "opacity-30",
|
||||
)}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -1,25 +1,25 @@
|
||||
import type { DragMoveEvent } from '@dnd-kit/core';
|
||||
import { useDndContext, useDndMonitor, useDraggable, useDroppable } from '@dnd-kit/core';
|
||||
import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { selectAtom } from 'jotai/utils';
|
||||
import type { DragMoveEvent } from "@dnd-kit/core";
|
||||
import { useDndContext, useDndMonitor, useDraggable, useDroppable } from "@dnd-kit/core";
|
||||
import classNames from "classnames";
|
||||
import { useAtomValue } from "jotai";
|
||||
import { selectAtom } from "jotai/utils";
|
||||
import type {
|
||||
MouseEvent,
|
||||
PointerEvent,
|
||||
FocusEvent as ReactFocusEvent,
|
||||
KeyboardEvent as ReactKeyboardEvent,
|
||||
} from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { computeSideForDragMove } from '../../../lib/dnd';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import type { ContextMenuProps, DropdownItem } from '../Dropdown';
|
||||
import { ContextMenu } from '../Dropdown';
|
||||
import { Icon } from '../Icon';
|
||||
import { collapsedFamily, isCollapsedFamily, isLastFocusedFamily, isSelectedFamily } from './atoms';
|
||||
import type { TreeNode } from './common';
|
||||
import { getNodeKey } from './common';
|
||||
import type { TreeProps } from './Tree';
|
||||
import { TreeIndentGuide } from './TreeIndentGuide';
|
||||
} from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { computeSideForDragMove } from "../../../lib/dnd";
|
||||
import { jotaiStore } from "../../../lib/jotai";
|
||||
import type { ContextMenuProps, DropdownItem } from "../Dropdown";
|
||||
import { ContextMenu } from "../Dropdown";
|
||||
import { Icon } from "../Icon";
|
||||
import { collapsedFamily, isCollapsedFamily, isLastFocusedFamily, isSelectedFamily } from "./atoms";
|
||||
import type { TreeNode } from "./common";
|
||||
import { getNodeKey } from "./common";
|
||||
import type { TreeProps } from "./Tree";
|
||||
import { TreeIndentGuide } from "./TreeIndentGuide";
|
||||
|
||||
export interface TreeItemClickEvent {
|
||||
shiftKey: boolean;
|
||||
@@ -29,12 +29,12 @@ export interface TreeItemClickEvent {
|
||||
|
||||
export type TreeItemProps<T extends { id: string }> = Pick<
|
||||
TreeProps<T>,
|
||||
'ItemInner' | 'ItemLeftSlotInner' | 'ItemRightSlot' | 'treeId' | 'getEditOptions' | 'getItemKey'
|
||||
"ItemInner" | "ItemLeftSlotInner" | "ItemRightSlot" | "treeId" | "getEditOptions" | "getItemKey"
|
||||
> & {
|
||||
node: TreeNode<T>;
|
||||
className?: string;
|
||||
onClick?: (item: T, e: TreeItemClickEvent) => void;
|
||||
getContextMenu?: (item: T) => ContextMenuProps['items'] | Promise<ContextMenuProps['items']>;
|
||||
getContextMenu?: (item: T) => ContextMenuProps["items"] | Promise<ContextMenuProps["items"]>;
|
||||
depth: number;
|
||||
setRef?: (item: T, n: TreeItemHandle | null) => void;
|
||||
};
|
||||
@@ -68,7 +68,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
const isCollapsed = useAtomValue(isCollapsedFamily({ treeId, itemId: node.item.id }));
|
||||
const isLastSelected = useAtomValue(isLastFocusedFamily({ treeId, itemId: node.item.id }));
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const [dropHover, setDropHover] = useState<null | 'drop' | 'animate'>(null);
|
||||
const [dropHover, setDropHover] = useState<null | "drop" | "animate">(null);
|
||||
const startedHoverTimeout = useRef<NodeJS.Timeout>(undefined);
|
||||
const handle = useMemo<TreeItemHandle>(
|
||||
() => ({
|
||||
@@ -88,7 +88,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
return listItemRef.current.getBoundingClientRect();
|
||||
},
|
||||
scrollIntoView: () => {
|
||||
listItemRef.current?.scrollIntoView({ block: 'nearest' });
|
||||
listItemRef.current?.scrollIntoView({ block: "nearest" });
|
||||
},
|
||||
}),
|
||||
[editing, getEditOptions],
|
||||
@@ -162,13 +162,13 @@ function TreeItem_<T extends { id: string }>({
|
||||
async (e: ReactKeyboardEvent<HTMLInputElement>) => {
|
||||
e.stopPropagation(); // Don't trigger other tree keys (like arrows)
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
case "Enter":
|
||||
if (editing) {
|
||||
e.preventDefault();
|
||||
await handleSubmitNameEdit(e.currentTarget);
|
||||
}
|
||||
break;
|
||||
case 'Escape':
|
||||
case "Escape":
|
||||
if (editing) {
|
||||
e.preventDefault();
|
||||
setEditing(false);
|
||||
@@ -208,8 +208,8 @@ function TreeItem_<T extends { id: string }>({
|
||||
const isFolder = node.children != null;
|
||||
const hasChildren = (node.children?.length ?? 0) > 0;
|
||||
const isCollapsed = jotaiStore.get(isCollapsedFamily({ treeId, itemId: node.item.id }));
|
||||
if (isCollapsed && isFolder && hasChildren && side === 'after') {
|
||||
setDropHover('animate');
|
||||
if (isCollapsed && isFolder && hasChildren && side === "after") {
|
||||
setDropHover("animate");
|
||||
clearTimeout(startedHoverTimeout.current);
|
||||
startedHoverTimeout.current = setTimeout(() => {
|
||||
jotaiStore.set(isCollapsedFamily({ treeId, itemId: node.item.id }), false);
|
||||
@@ -221,8 +221,8 @@ function TreeItem_<T extends { id: string }>({
|
||||
);
|
||||
});
|
||||
}, HOVER_CLOSED_FOLDER_DELAY);
|
||||
} else if (isFolder && !hasChildren && side === 'after') {
|
||||
setDropHover('drop');
|
||||
} else if (isFolder && !hasChildren && side === "after") {
|
||||
setDropHover("drop");
|
||||
} else {
|
||||
clearDropHover();
|
||||
}
|
||||
@@ -238,7 +238,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
|
||||
// Set data attribute on the list item to preserve active state
|
||||
if (listItemRef.current) {
|
||||
listItemRef.current.setAttribute('data-context-menu-open', 'true');
|
||||
listItemRef.current.setAttribute("data-context-menu-open", "true");
|
||||
}
|
||||
|
||||
const items = await getContextMenu(node.item);
|
||||
@@ -250,7 +250,7 @@ function TreeItem_<T extends { id: string }>({
|
||||
const handleCloseContextMenu = useCallback(() => {
|
||||
// Remove data attribute when context menu closes
|
||||
if (listItemRef.current) {
|
||||
listItemRef.current.removeAttribute('data-context-menu-open');
|
||||
listItemRef.current.removeAttribute("data-context-menu-open");
|
||||
}
|
||||
setShowContextMenu(null);
|
||||
}, []);
|
||||
@@ -290,20 +290,20 @@ function TreeItem_<T extends { id: string }>({
|
||||
onContextMenu={handleContextMenu}
|
||||
className={classNames(
|
||||
className,
|
||||
'tree-item',
|
||||
'h-sm',
|
||||
'grid grid-cols-[auto_minmax(0,1fr)]',
|
||||
editing && 'ring-1 focus-within:ring-focus',
|
||||
dropHover != null && 'relative z-10 ring-2 ring-primary',
|
||||
dropHover === 'animate' && 'animate-blinkRing',
|
||||
isSelected && 'selected',
|
||||
"tree-item",
|
||||
"h-sm",
|
||||
"grid grid-cols-[auto_minmax(0,1fr)]",
|
||||
editing && "ring-1 focus-within:ring-focus",
|
||||
dropHover != null && "relative z-10 ring-2 ring-primary",
|
||||
dropHover === "animate" && "animate-blinkRing",
|
||||
isSelected && "selected",
|
||||
)}
|
||||
>
|
||||
<TreeIndentGuide treeId={treeId} depth={depth} ancestorIds={ancestorIds} />
|
||||
<div
|
||||
className={classNames(
|
||||
'text-text-subtle',
|
||||
'grid grid-cols-[auto_minmax(0,1fr)_auto] gap-x-2 items-center rounded-md',
|
||||
"text-text-subtle",
|
||||
"grid grid-cols-[auto_minmax(0,1fr)_auto] gap-x-2 items-center rounded-md",
|
||||
)}
|
||||
>
|
||||
{showContextMenu && (
|
||||
@@ -321,12 +321,12 @@ function TreeItem_<T extends { id: string }>({
|
||||
onClick={toggleCollapsed}
|
||||
>
|
||||
<Icon
|
||||
icon={node.children.length === 0 ? 'dot' : 'chevron_right'}
|
||||
icon={node.children.length === 0 ? "dot" : "chevron_right"}
|
||||
className={classNames(
|
||||
'transition-transform text-text-subtlest',
|
||||
'ml-auto',
|
||||
'w-[1rem] h-[1rem]',
|
||||
!isCollapsed && node.children.length > 0 && 'rotate-90',
|
||||
"transition-transform text-text-subtlest",
|
||||
"ml-auto",
|
||||
"w-[1rem] h-[1rem]",
|
||||
!isCollapsed && node.children.length > 0 && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
</button>
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import type { CSSProperties } from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import type { SelectableTreeNode } from './common';
|
||||
import type { TreeProps } from './Tree';
|
||||
import { TreeDropMarker } from './TreeDropMarker';
|
||||
import type { TreeItemHandle, TreeItemProps } from './TreeItem';
|
||||
import { TreeItem } from './TreeItem';
|
||||
import type { CSSProperties } from "react";
|
||||
import { Fragment } from "react";
|
||||
import type { SelectableTreeNode } from "./common";
|
||||
import type { TreeProps } from "./Tree";
|
||||
import { TreeDropMarker } from "./TreeDropMarker";
|
||||
import type { TreeItemHandle, TreeItemProps } from "./TreeItem";
|
||||
import { TreeItem } from "./TreeItem";
|
||||
|
||||
export type TreeItemListProps<T extends { id: string }> = Pick<
|
||||
TreeProps<T>,
|
||||
'ItemInner' | 'ItemLeftSlotInner' | 'ItemRightSlot' | 'treeId' | 'getItemKey' | 'getEditOptions'
|
||||
"ItemInner" | "ItemLeftSlotInner" | "ItemRightSlot" | "treeId" | "getItemKey" | "getEditOptions"
|
||||
> &
|
||||
Pick<TreeItemProps<T>, 'onClick' | 'getContextMenu'> & {
|
||||
Pick<TreeItemProps<T>, "onClick" | "getContextMenu"> & {
|
||||
nodes: SelectableTreeNode<T>[];
|
||||
style?: CSSProperties;
|
||||
className?: string;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { atom } from 'jotai';
|
||||
import { atomFamily, selectAtom } from 'jotai/utils';
|
||||
import { atomWithKVStorage } from '../../../lib/atoms/atomWithKVStorage';
|
||||
import { atom } from "jotai";
|
||||
import { atomFamily, selectAtom } from "jotai/utils";
|
||||
import { atomWithKVStorage } from "../../../lib/atoms/atomWithKVStorage";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
export const selectedIdsFamily = atomFamily((_treeId: string) => {
|
||||
@@ -58,7 +58,7 @@ export const isAncestorHoveredFamily = atomFamily(
|
||||
(v) => v.parentId && ancestorIds.includes(v.parentId),
|
||||
Object.is,
|
||||
),
|
||||
(a, b) => a.treeId === b.treeId && a.ancestorIds.join(',') === b.ancestorIds.join(','),
|
||||
(a, b) => a.treeId === b.treeId && a.ancestorIds.join(",") === b.ancestorIds.join(","),
|
||||
);
|
||||
|
||||
export const isIndexHoveredFamily = atomFamily(
|
||||
@@ -76,12 +76,12 @@ export const hoveredParentDepthFamily = atomFamily((treeId: string) =>
|
||||
);
|
||||
|
||||
export const collapsedFamily = atomFamily((workspaceId: string) => {
|
||||
const key = ['sidebar_collapsed', workspaceId ?? 'n/a'];
|
||||
const key = ["sidebar_collapsed", workspaceId ?? "n/a"];
|
||||
return atomWithKVStorage<Record<string, boolean>>(key, {});
|
||||
});
|
||||
|
||||
export const isCollapsedFamily = atomFamily(
|
||||
({ treeId, itemId = 'n/a' }: { treeId: string; itemId: string | undefined }) =>
|
||||
({ treeId, itemId = "n/a" }: { treeId: string; itemId: string | undefined }) =>
|
||||
atom(
|
||||
// --- getter ---
|
||||
(get) => !!get(collapsedFamily(treeId))[itemId],
|
||||
@@ -91,7 +91,7 @@ export const isCollapsedFamily = atomFamily(
|
||||
const a = collapsedFamily(treeId);
|
||||
const prevMap = get(a);
|
||||
const prevValue = !!prevMap[itemId];
|
||||
const value = typeof next === 'function' ? next(prevValue) : next;
|
||||
const value = typeof next === "function" ? next(prevValue) : next;
|
||||
|
||||
if (value === prevValue) return; // no-op
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import { collapsedFamily, selectedIdsFamily } from './atoms';
|
||||
import { jotaiStore } from "../../../lib/jotai";
|
||||
import { collapsedFamily, selectedIdsFamily } from "./atoms";
|
||||
|
||||
export interface TreeNode<T extends { id: string }> {
|
||||
children?: TreeNode<T>[];
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { SelectableTreeNode, TreeNode } from './common';
|
||||
import { useMemo } from "react";
|
||||
import type { SelectableTreeNode, TreeNode } from "./common";
|
||||
|
||||
export function useSelectableItems<T extends { id: string }>(root: TreeNode<T>) {
|
||||
return useMemo(() => {
|
||||
|
||||
Reference in New Issue
Block a user