Switch to BiomeJS (#306)

This commit is contained in:
Gregory Schier
2025-11-23 08:38:13 -08:00
committed by GitHub
parent 2bac610efe
commit ec3e2e16a9
332 changed files with 3007 additions and 4097 deletions

View File

@@ -39,6 +39,8 @@ export function AutoScroller<T>({ data, render, header }: Props<T>) {
useLayoutEffect(() => {
if (!autoScroll) return;
data.length; // Make linter happy. We want to refresh when length changes
const el = containerRef.current;
if (el == null) return;
@@ -69,21 +71,26 @@ export function AutoScroller<T>({ data, render, header }: Props<T>) {
position: 'relative',
}}
>
{rowVirtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{render(data[virtualItem.index]!, virtualItem.index)}
</div>
))}
{rowVirtualizer.getVirtualItems().map((virtualItem) => {
const item = data[virtualItem.index];
return (
item != null && (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
{render(item, virtualItem.index)}
</div>
)
);
})}
</div>
</div>
</div>

View File

@@ -1,5 +1,5 @@
import classNames from 'classnames';
import { type ReactNode } from 'react';
import type { ReactNode } from 'react';
import { Icon } from './Icon';
import { IconTooltip } from './IconTooltip';
import { HStack } from './Stacks';
@@ -58,7 +58,9 @@ export function Checkbox({
</div>
</div>
{!hideLabel && (
<div className={classNames('text-sm', fullWidth && 'w-full', disabled && 'opacity-disabled')}>
<div
className={classNames('text-sm', fullWidth && 'w-full', disabled && 'opacity-disabled')}
>
{title}
</div>
)}

View File

@@ -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();
@@ -61,13 +61,9 @@ export function Confirm({
}
/>
)}
<HStack
space={2}
justifyContent="start"
className="mt-2 mb-4 flex-row-reverse"
>
<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

View File

@@ -24,9 +24,7 @@ export function DetailsBanner({ className, color, summary, children, ...extraPro
/>
{summary}
</summary>
<div className="mt-1.5">
{children}
</div>
<div className="mt-1.5">{children}</div>
</details>
</Banner>
);

View File

@@ -43,7 +43,6 @@ export function Dialog({
return (
<Overlay open={open} onClose={disableBackdropClose ? undefined : onClose} portalName="dialog">
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
<div
role="dialog"
className={classNames(

View File

@@ -12,7 +12,10 @@ export function DismissibleBanner({
id,
actions,
...props
}: BannerProps & { id: string; actions?: { label: string; onClick: () => void; color?: Color }[] }) {
}: BannerProps & {
id: string;
actions?: { label: string; onClick: () => void; color?: Color }[];
}) {
const { set: setDismissed, value: dismissed } = useKeyValue<boolean>({
namespace: 'global',
key: ['dismiss-banner', id],
@@ -28,9 +31,9 @@ export function DismissibleBanner({
>
{children}
<HStack space={1.5}>
{actions?.map((a, i) => (
{actions?.map((a) => (
<Button
key={a.label + i}
key={a.label}
variant="border"
color={a.color ?? props.color}
size="xs"

View File

@@ -3,15 +3,16 @@ import { atom } from 'jotai';
import * as m from 'motion/react-m';
import type {
CSSProperties,
FocusEvent as ReactFocusEvent,
HTMLAttributes,
MouseEvent,
ReactElement,
FocusEvent as ReactFocusEvent,
KeyboardEvent as ReactKeyboardEvent,
ReactNode,
RefObject,
SetStateAction,
} from 'react';
import React, {
import {
Children,
cloneElement,
forwardRef,
@@ -30,6 +31,7 @@ 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';
@@ -37,7 +39,6 @@ import { Icon } from './Icon';
import { LoadingIcon } from './LoadingIcon';
import { Separator } from './Separator';
import { HStack, VStack } from './Stacks';
import { ErrorBoundary } from '../ErrorBoundary';
export type DropdownItemSeparator = {
type: 'separator';
@@ -96,24 +97,24 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
ref,
) {
const id = useRef(generateId());
const [isOpen, _setIsOpen] = useState<boolean>(false);
const [isOpen, setIsOpen] = useState<boolean>(false);
useEffect(() => {
return jotaiStore.sub(openAtom, () => {
const globalOpenId = jotaiStore.get(openAtom);
const newIsOpen = globalOpenId === id.current;
if (newIsOpen !== isOpen) {
_setIsOpen(newIsOpen);
setIsOpen(newIsOpen);
}
});
}, [isOpen, _setIsOpen]);
}, [isOpen]);
// 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 setIsOpen = useCallback(
const handleSetIsOpen = useCallback(
(o: SetStateAction<boolean>) => {
jotaiStore.set(openAtom, (prevId) => {
const prevIsOpen = prevId === id.current;
@@ -121,9 +122,11 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
// Persist background color of button until we close the dropdown
if (newIsOpen) {
onOpen?.();
buttonRef.current!.style.backgroundColor = window
.getComputedStyle(buttonRef.current!)
.getPropertyValue('background-color');
if (buttonRef.current) {
buttonRef.current.style.backgroundColor = window
.getComputedStyle(buttonRef.current)
.getPropertyValue('background-color');
}
}
return newIsOpen ? id.current : null; // Set global atom to current ID to signify open state
});
@@ -136,7 +139,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
useEffect(() => {
if (!isOpen) {
// Clear persisted BG
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);
@@ -157,38 +160,38 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
else this.close();
},
open(index?: number) {
setIsOpen(true);
handleSetIsOpen(true);
setDefaultSelectedIndex(index ?? -1);
},
close() {
setIsOpen(false);
handleSetIsOpen(false);
},
}),
[isOpen, setIsOpen, menuRefCurrent],
[isOpen, handleSetIsOpen, menuRefCurrent],
);
useHotKey(hotKeyAction ?? null, () => {
setDefaultSelectedIndex(0);
setIsOpen(true);
handleSetIsOpen(true);
});
const child = useMemo(() => {
const existingChild = Children.only(children);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const props: any = {
...existingChild.props,
ref: buttonRef,
'aria-haspopup': 'true',
onClick:
existingChild.props?.onClick ??
((e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
setIsOpen((o) => !o); // Toggle dropdown
}),
};
const props: HTMLAttributes<HTMLButtonElement> & { ref: RefObject<HTMLButtonElement | null> } =
{
...existingChild.props,
ref: buttonRef,
'aria-haspopup': 'true',
onClick:
existingChild.props?.onClick ??
((e: MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
e.stopPropagation();
handleSetIsOpen((o) => !o); // Toggle dropdown
}),
};
return cloneElement(existingChild, props);
}, [children, setIsOpen]);
}, [children, handleSetIsOpen]);
useEffect(() => {
buttonRef.current?.setAttribute('aria-expanded', isOpen.toString());
@@ -204,7 +207,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
return (
<>
{child}
<ErrorBoundary name={`Dropdown Menu`}>
<ErrorBoundary name={'Dropdown Menu'}>
<Menu
ref={menuRef}
showTriangle
@@ -213,7 +216,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
defaultSelectedIndex={defaultSelectedIndex}
items={items}
triggerShape={triggerRect ?? null}
onClose={() => setIsOpen(false)}
onClose={() => handleSetIsOpen(false)}
isOpen={isOpen}
/>
</ErrorBoundary>
@@ -303,7 +306,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
}, [onClose]);
// Close menu on space bar
const handleMenuKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
const handleMenuKeyDown = (e: ReactKeyboardEvent<HTMLDivElement>) => {
const isCharacter = e.key.length === 1;
const isSpecial = e.ctrlKey || e.metaKey || e.altKey;
if (isCharacter && !isSpecial) {
@@ -348,7 +351,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
);
const handleNext = useCallback(
(incrBy: number = 1) => {
(incrBy = 1) => {
setSelectedIndex((currIndex) => {
let nextIndex = (currIndex ?? -1) + incrBy;
const maxTries = items.length;
@@ -474,7 +477,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
const handleFocus = useCallback(
(i: DropdownItem) => {
const index = filteredItems.findIndex((item) => item === i) ?? null;
const index = filteredItems.indexOf(i) ?? null;
setSelectedIndex(index);
},
[filteredItems, setSelectedIndex],
@@ -556,6 +559,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
if (item.type === 'separator') {
return (
<Separator
// biome-ignore lint/suspicious/noArrayIndexKey: Nothing else available
key={i}
className={classNames('my-1.5', item.label ? 'ml-2' : null)}
>
@@ -565,12 +569,9 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
}
if (item.type === 'content') {
return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions,jsx-a11y/click-events-have-key-events
<div
key={i}
className={classNames('my-1 mx-2 max-w-xs')}
onClick={onClose}
>
// biome-ignore lint/a11y/noStaticElementInteractions: Needs to be clickable but want to support nested buttons
// biome-ignore lint/suspicious/noArrayIndexKey: index is fine
<div key={i} className={classNames('my-1 mx-2 max-w-xs')} onClick={onClose}>
{item.label}
</div>
);
@@ -580,7 +581,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
focused={i === selectedIndex}
onFocus={handleFocus}
onSelect={handleSelect}
key={`item_${i}`}
key={`item_${item.label}`}
item={item}
/>
);

View File

@@ -7,8 +7,7 @@ export class BetterMatchDecorator extends MatchDecorator {
updateDeco(update: ViewUpdate, deco: DecorationSet): DecorationSet {
if (!update.startState.selection.eq(update.state.selection)) {
return super.createDeco(update.view);
} else {
return super.updateDeco(update, deco);
}
return super.updateDeco(update, deco);
}
}

View File

@@ -26,7 +26,8 @@
}
&:not(.cm-focused) {
.cm-cursor, .cm-fat-cursor {
.cm-cursor,
.cm-fat-cursor {
@apply hidden;
}
}
@@ -77,7 +78,6 @@
.cm-gutterElement {
@apply cursor-default;
}
}
.cm-gutter-lint {
@@ -91,7 +91,7 @@
@apply cursor-default opacity-80 hover:opacity-100 transition-opacity;
@apply rounded-full w-[0.9em] h-[0.9em];
content: '';
content: "";
&.cm-lint-marker-error {
@apply bg-danger;
@@ -168,7 +168,8 @@
@apply outline-text;
@apply bg-text !important;
&, * {
&,
* {
@apply text-surface font-semibold !important;
}
}
@@ -199,8 +200,7 @@
}
.cm-editor .fold-gutter-icon::after {
@apply block w-1.5 h-1.5 p-0.5 border-transparent
border-l border-b border-l-[currentColor] border-b-[currentColor] content-[''];
@apply block w-1.5 h-1.5 p-0.5 border-transparent border-l border-b border-l-[currentColor] border-b-[currentColor] content-[''];
}
/* Rotate the fold gutter chevron when open */
@@ -270,7 +270,8 @@
/* Style the tooltip for popping up "open in browser" and other stuff */
a, button {
a,
button {
@apply text-text hover:bg-surface-highlight w-full h-sm flex items-center px-2 rounded;
}
@@ -279,7 +280,7 @@
&::after {
@apply text-text bg-text h-3 w-3 ml-1;
content: '';
content: "";
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='black' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E");
-webkit-mask-size: contain;
display: inline-block;
@@ -300,59 +301,59 @@
@apply opacity-80 italic;
&::after {
content: 'a' !important; /* Default (eg. for GraphQL) */
content: "a" !important; /* Default (eg. for GraphQL) */
}
&.cm-completionIcon-function::after {
content: 'f' !important;
content: "f" !important;
@apply text-info;
}
&.cm-completionIcon-variable::after {
content: 'x' !important;
content: "x" !important;
@apply text-primary;
}
&.cm-completionIcon-namespace::after {
content: 'n' !important;
content: "n" !important;
@apply text-warning;
}
&.cm-completionIcon-constant::after {
content: 'c' !important;
content: "c" !important;
@apply text-notice;
}
&.cm-completionIcon-class::after {
content: 'o' !important;
content: "o" !important;
}
&.cm-completionIcon-enum::after {
content: 'e' !important;
content: "e" !important;
}
&.cm-completionIcon-interface::after {
content: 'i' !important;
content: "i" !important;
}
&.cm-completionIcon-keyword::after {
content: 'k' !important;
content: "k" !important;
}
&.cm-completionIcon-method::after {
content: 'm' !important;
content: "m" !important;
}
&.cm-completionIcon-property::after {
content: 'a' !important;
content: "a" !important;
}
&.cm-completionIcon-text::after {
content: 't' !important;
content: "t" !important;
}
&.cm-completionIcon-type::after {
content: 't' !important;
content: "t" !important;
}
}
@@ -406,7 +407,7 @@
@apply appearance-none bg-none cursor-default;
}
button[name='close'] {
button[name="close"] {
@apply text-text-subtle hocus:text-text px-2 -mr-1.5 !important;
}
@@ -421,7 +422,7 @@
/* Hide the "All" button */
button[name='select'] {
button[name="select"] {
@apply hidden;
}
}

View File

@@ -28,7 +28,6 @@ import {
import { activeEnvironmentAtom } from '../../../hooks/useActiveEnvironment';
import type { WrappedEnvironmentVariable } from '../../../hooks/useEnvironmentVariables';
import { useEnvironmentVariables } from '../../../hooks/useEnvironmentVariables';
import { useRandomKey } from '../../../hooks/useRandomKey';
import { useRequestEditor } from '../../../hooks/useRequestEditor';
import { useTemplateFunctionCompletionOptions } from '../../../hooks/useTemplateFunctions';
import { editEnvironment } from '../../../lib/editEnvironment';
@@ -113,7 +112,7 @@ export function Editor({
disabled,
extraExtensions,
forcedEnvironmentId,
forceUpdateKey: forceUpdateKeyFromAbove,
forceUpdateKey,
format,
heightMode,
hideGutter,
@@ -144,10 +143,6 @@ export function Editor({
? allEnvironmentVariables.filter(autocompleteVariables)
: allEnvironmentVariables;
}, [allEnvironmentVariables, autocompleteVariables]);
// Track a local key for updates. If the default value is changed when the input is not in focus,
// regenerate this to force the field to update.
const [focusedUpdateKey, regenerateFocusedUpdateKey] = useRandomKey('initial');
const forceUpdateKey = `${forceUpdateKeyFromAbove}::${focusedUpdateKey}`;
if (settings && wrapLines === undefined) {
wrapLines = settings.editorSoftWrap;
@@ -223,7 +218,7 @@ export function Editor({
const effects = placeholderCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects });
},
[placeholder, type],
[placeholder],
);
// Update vim
@@ -233,12 +228,12 @@ export function Editor({
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 ext = keymapExtensions[settings.editorKeymap] ?? keymapExtensions.default;
const effects = keymapCompartment.current.reconfigure(ext);
cm.current.view.dispatch({ effects });
},
@@ -324,6 +319,7 @@ export function Editor({
);
// Update the language extension when the language changes
// biome-ignore lint/correctness/useExhaustiveDependencies: none
useEffect(() => {
if (cm.current === null) return;
const { view, languageCompartment } = cm.current;
@@ -355,6 +351,7 @@ export function Editor({
]);
// Initialize the editor when ref mounts
// biome-ignore lint/correctness/useExhaustiveDependencies: Only reinitialize when necessary
const initEditorRef = useCallback(
function initEditorRef(container: HTMLDivElement | null) {
if (container === null) {
@@ -384,7 +381,7 @@ export function Editor({
!disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension,
),
keymapCompartment.current.of(
keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'],
keymapExtensions[settings.editorKeymap] ?? keymapExtensions.default,
),
...getExtensions({
container,
@@ -434,7 +431,6 @@ export function Editor({
console.log('Failed to initialize Codemirror', e);
}
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[forceUpdateKey],
);
@@ -456,7 +452,7 @@ export function Editor({
updateContents(cm.current.view, defaultValue || '');
}
},
[defaultValue, readOnly, regenerateFocusedUpdateKey],
[defaultValue],
);
// Add bg classes to actions, so they appear over the text
@@ -628,7 +624,7 @@ function saveCachedEditorState(stateKey: string | null, state: EditorState | nul
// Save state in sessionStorage by removing doc and saving the hash of it instead.
// This will be checked on restore and put back in if it matches.
stateObj.docHash = md5(stateObj.doc);
delete stateObj.doc;
stateObj.doc = undefined;
try {
sessionStorage.setItem(computeFullStateKey(stateKey), JSON.stringify(stateObj));
@@ -670,7 +666,9 @@ function updateContents(view: EditorView, text: string) {
if (currentDoc === text) {
return;
} else if (text.startsWith(currentDoc)) {
}
if (text.startsWith(currentDoc)) {
// If we're just appending, append only the changes. This preserves
// things like scroll position.
view.dispatch({

View File

@@ -1,6 +1,7 @@
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 {
@@ -40,9 +41,10 @@ function wordBefore(doc: string, pos: number): { from: number; to: number; text:
function inPhrase(ctx: CompletionContext): boolean {
// Lezer node names from your grammar: Phrase is the quoted token
let n = syntaxTree(ctx.state).resolveInner(ctx.pos, -1);
for (; n; n = n.parent!) {
let n: SyntaxNode | null = syntaxTree(ctx.state).resolveInner(ctx.pos, -1);
while (n) {
if (n.name === 'Phrase') return true;
n = n.parent;
}
return false;
}

View File

@@ -1,23 +1,27 @@
// biome-ignore-all lint: Disable for generated file
// 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",
nodeNames: "⚠ Query Expr OrExpr AndExpr Unary Not Primary RParen LParen Group Field FieldName Word Colon FieldValue Phrase Term And Or",
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',
nodeNames:
'⚠ 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],
skippedNodes: [0, 20],
repeatNodeCount: 3,
tokenData: ")f~RgX^!jpq!jrs#_xy${yz%Q}!O%V!Q![%[![!]%m!c!d%r!d!p%[!p!q'V!q!r(j!r!}%[#R#S%[#T#o%[#y#z!j$f$g!j#BY#BZ!j$IS$I_!j$I|$JO!j$JT$JU!j$KV$KW!j&FU&FV!j~!oYi~X^!jpq!j#y#z!j$f$g!j#BY#BZ!j$IS$I_!j$I|$JO!j$JT$JU!j$KV$KW!j&FU&FV!j~#bVOr#_rs#ws#O#_#O#P#|#P;'S#_;'S;=`$u<%lO#_~#|O`~~$PRO;'S#_;'S;=`$Y;=`O#_~$]WOr#_rs#ws#O#_#O#P#|#P;'S#_;'S;=`$u;=`<%l#_<%lO#_~$xP;=`<%l#_~%QOX~~%VOW~~%[OU~~%aS]~!Q![%[!c!}%[#R#S%[#T#o%[~%rO^~~%wU]~!Q![%[!c!p%[!p!q&Z!q!}%[#R#S%[#T#o%[~&`U]~!Q![%[!c!f%[!f!g&r!g!}%[#R#S%[#T#o%[~&ySb~]~!Q![%[!c!}%[#R#S%[#T#o%[~'[U]~!Q![%[!c!q%[!q!r'n!r!}%[#R#S%[#T#o%[~'sU]~!Q![%[!c!v%[!v!w(V!w!}%[#R#S%[#T#o%[~(^SU~]~!Q![%[!c!}%[#R#S%[#T#o%[~(oU]~!Q![%[!c!t%[!t!u)R!u!}%[#R#S%[#T#o%[~)YSc~]~!Q![%[!c!}%[#R#S%[#T#o%[",
tokenData:
")f~RgX^!jpq!jrs#_xy${yz%Q}!O%V!Q![%[![!]%m!c!d%r!d!p%[!p!q'V!q!r(j!r!}%[#R#S%[#T#o%[#y#z!j$f$g!j#BY#BZ!j$IS$I_!j$I|$JO!j$JT$JU!j$KV$KW!j&FU&FV!j~!oYi~X^!jpq!j#y#z!j$f$g!j#BY#BZ!j$IS$I_!j$I|$JO!j$JT$JU!j$KV$KW!j&FU&FV!j~#bVOr#_rs#ws#O#_#O#P#|#P;'S#_;'S;=`$u<%lO#_~#|O`~~$PRO;'S#_;'S;=`$Y;=`O#_~$]WOr#_rs#ws#O#_#O#P#|#P;'S#_;'S;=`$u;=`<%l#_<%lO#_~$xP;=`<%l#_~%QOX~~%VOW~~%[OU~~%aS]~!Q![%[!c!}%[#R#S%[#T#o%[~%rO^~~%wU]~!Q![%[!c!p%[!p!q&Z!q!}%[#R#S%[#T#o%[~&`U]~!Q![%[!c!f%[!f!g&r!g!}%[#R#S%[#T#o%[~&ySb~]~!Q![%[!c!}%[#R#S%[#T#o%[~'[U]~!Q![%[!c!q%[!q!r'n!r!}%[#R#S%[#T#o%[~'sU]~!Q![%[!c!v%[!v!w(V!w!}%[#R#S%[#T#o%[~(^SU~]~!Q![%[!c!}%[#R#S%[#T#o%[~(oU]~!Q![%[!c!t%[!t!u)R!u!}%[#R#S%[#T#o%[~)YSc~]~!Q![%[!c!}%[#R#S%[#T#o%[",
tokenizers: [0],
topRules: {"Query":[0,1]},
tokenPrec: 145
})
topRules: { Query: [0, 1] },
tokenPrec: 145,
});

View File

@@ -255,9 +255,9 @@ type Technique = 'substring' | 'fuzzy' | 'strict';
function includes(hay: string | undefined, needle: string, technique: Technique): boolean {
if (!hay || !needle) return false;
else if (technique === 'strict') return hay === needle;
else if (technique === 'fuzzy') return !!fuzzyMatch(hay, needle);
else return hay.indexOf(needle) !== -1;
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 {

View File

@@ -11,9 +11,10 @@ const REGEX =
const tooltip = hoverTooltip(
(view, pos, side) => {
const { from, text } = view.state.doc.lineAt(pos);
let match;
let match: RegExpExecArray | null;
let found: { start: number; end: number } | null = null;
// biome-ignore lint/suspicious/noAssignInExpressions: none
while ((match = REGEX.exec(text))) {
const start = from + match.index;
const end = start + match[0].length;
@@ -28,7 +29,7 @@ const tooltip = hoverTooltip(
return null;
}
if ((found.start == pos && side < 0) || (found.end == pos && side > 0)) {
if ((found.start === pos && side < 0) || (found.end === pos && side > 0)) {
return null;
}
@@ -37,7 +38,7 @@ const tooltip = hoverTooltip(
end: found.end,
create() {
const workspaceId = jotaiStore.get(activeWorkspaceIdAtom);
const link = text.substring(found!.start - from, found!.end - from);
const link = text.substring(found?.start - from, found?.end - from);
const dom = document.createElement('div');
const $open = document.createElement('a');
@@ -77,7 +78,7 @@ const tooltip = hoverTooltip(
},
);
const decorator = function () {
const decorator = () => {
const placeholderMatcher = new MatchDecorator({
regexp: REGEX,
decoration(match, view, matchStartPos) {

View File

@@ -12,7 +12,7 @@ export function jsonParseLinter() {
// syntax with repeating `1` characters, so it's valid JSON and the position is still correct.
const escapedDoc = doc.replace(TEMPLATE_SYNTAX_REGEX, '1');
jsonLintParse(escapedDoc);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
} catch (err: any) {
if (!('location' in err)) {
return [];

View File

@@ -1,6 +1,5 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
pairs = 1,
export const pairs = 1,
Key = 2,
Sep = 3,
Value = 4
Value = 4;

View File

@@ -1,19 +1,29 @@
// 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],
repeatNodeCount: 1,
tokenData: "$]VRVOYhYZ#[Z![h![!]#o!];'Sh;'S;=`#U<%lOhToVQPSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!US!rSSSOY!mZ;'S!m;'S;=`#O<%lO!mS#RP;=`<%l!mT#XP;=`<%lhR#cSVQQPO![!U!];'S!U;'S;=`!g<%lO!UV#vVRQSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOh",
tokenData:
"$]VRVOYhYZ#[Z![h![!]#o!];'Sh;'S;=`#U<%lOhToVQPSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!US!rSSSOY!mZ;'S!m;'S;=`#O<%lO!mS#RP;=`<%l!mT#XP;=`<%lhR#cSVQQPO![!U!];'S!U;'S;=`!g<%lO!UV#vVRQSSOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOh",
tokenizers: [0, 1, 2],
topRules: {"pairs":[0,1]},
topRules: { pairs: [0, 1] },
tokenPrec: 0,
termNames: {"0":"⚠","1":"@top","2":"Key","3":"Sep","4":"Value","5":"(Key Sep Value \"\\n\")+","6":"␄","7":"\"\\n\""}
})
termNames: {
'0': '⚠',
'1': '@top',
'2': 'Key',
'3': 'Sep',
'4': 'Value',
'5': '(Key Sep Value "\\n")+',
'6': '␄',
'7': '"\\n"',
},
});

View File

@@ -1,4 +1,3 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Template = 1,
Text = 2
export const Template = 1,
Text = 2;

View File

@@ -1,16 +1,16 @@
// 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,
tokenData: "p~RRO;'S[;'S;=`j<%lO[~aRQ~O;'S[;'S;=`j<%lO[~mP;=`<%l[",
tokenizers: [0],
topRules: {"Template":[0,1]},
tokenPrec: 0
})
topRules: { Template: [0, 1] },
tokenPrec: 0,
});

View File

@@ -44,14 +44,13 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
if (toMatch === null) return null;
const matchLen = toMatch.to - toMatch.from;
if (toMatch.from >0 && matchLen < MIN_MATCH_NAME) {
if (toMatch.from > 0 && matchLen < MIN_MATCH_NAME) {
return null;
}
const completions: Completion[] = options
.flatMap((o): Completion[] => {
const matchSegments = toMatch!.text.replace(/^\$/, '').split('.');
const matchSegments = toMatch.text.replace(/^\$/, '').split('.');
const optionSegments = o.name.split('.');
// If not on the last segment, only complete the namespace
@@ -59,7 +58,7 @@ export function twigCompletion({ options }: TwigCompletionConfig) {
const prefix = optionSegments.slice(0, matchSegments.length).join('.');
return [
{
label: prefix + '.*',
label: `${prefix}.*`,
type: 'namespace',
detail: 'namespace',
apply: (view, _completion, from, to) => {

View File

@@ -1,7 +1,7 @@
import { syntaxTree } from '@codemirror/language';
import type { Range } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, ViewPlugin, WidgetType, EditorView } from '@codemirror/view';
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
class PathPlaceholderWidget extends WidgetType {
readonly #clickListenerCallback: () => void;
@@ -23,7 +23,7 @@ class PathPlaceholderWidget extends WidgetType {
toDOM() {
const elt = document.createElement('span');
elt.className = `x-theme-templateTag x-theme-templateTag--secondary template-tag`;
elt.className = 'x-theme-templateTag x-theme-templateTag--secondary template-tag';
elt.textContent = this.rawText;
elt.addEventListener('click', this.#clickListenerCallback);
return elt;

View File

@@ -1,7 +1,7 @@
import { syntaxTree } from '@codemirror/language';
import type { Range } from '@codemirror/state';
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
import { Decoration, ViewPlugin, WidgetType, EditorView } from '@codemirror/view';
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
import type { SyntaxNodeRef } from '@lezer/common';
import { parseTemplate } from '@yaakapp-internal/templates';
import type { TwigCompletionOption } from './completion';

View File

@@ -1,8 +1,7 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Template = 1,
export const Template = 1,
Tag = 2,
TagOpen = 3,
TagContent = 4,
TagClose = 5,
Text = 6
Text = 6;

View File

@@ -1,18 +1,20 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser, LocalTokenGroup} 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",
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',
maxTerm: 10,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 2,
tokenData: "#]~RTOtbtu!hu;'Sb;'S;=`!]<%lOb~gTU~Otbtuvu;'Sb;'S;=`!]<%lOb~yUO#ob#p;'Sb;'S;=`!]<%l~b~Ob~~!c~!`P;=`<%lb~!hOU~~!kVO#ob#o#p#Q#p;'Sb;'S;=`!]<%l~b~Ob~~!c~#TP!}#O#W~#]OY~",
tokenizers: [1, new LocalTokenGroup("b~RP#P#QU~XP#q#r[~aOT~~", 17, 4)],
topRules: {"Template":[0,1]},
tokenPrec: 0
})
tokenData:
"#]~RTOtbtu!hu;'Sb;'S;=`!]<%lOb~gTU~Otbtuvu;'Sb;'S;=`!]<%lOb~yUO#ob#p;'Sb;'S;=`!]<%l~b~Ob~~!c~!`P;=`<%lb~!hOU~~!kVO#ob#o#p#Q#p;'Sb;'S;=`!]<%l~b~Ob~~!c~#TP!}#O#W~#]OY~",
tokenizers: [1, new LocalTokenGroup('b~RP#P#QU~XP#q#r[~aOT~~', 17, 4)],
topRules: { Template: [0, 1] },
tokenPrec: 0,
});

View File

@@ -1,10 +1,9 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
url = 1,
export const url = 1,
Protocol = 2,
Host = 3,
Port = 4,
Path = 5,
Placeholder = 6,
PathSegment = 7,
Query = 8
Query = 8;

View File

@@ -1,19 +1,20 @@
// 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",
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',
maxTerm: 14,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 2,
tokenData: ".i~RgOs!jtv!jvw#Xw}!j}!O#r!O!P#r!P!Q%U!Q![%Z![!]'o!]!a!j!a!b+W!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jQ!oUUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jQ#UP;=`<%l!jR#`U^PUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jR#ycRPUQOs!jt}!j}!O#r!O!P#r!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!j~%ZOZ~V%de]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!]#r!]!_!j!_!`&u!`!a!j!b!c!j!c!}%Z!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o%Z#o;'S!j;'S;=`#R<%lO!jU&|Z]SUQOs!jt!P!j!Q![&u![!a!j!b!c!j!c!}&u!}#T!j#T#o&u#o;'S!j;'S;=`#R<%lO!jR'vcRPUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)RQ)YUTQUQOs)Rt!P)R!Q!a)R!b;'S)R;'S;=`)l<%lO)RQ)oP;=`<%l)RR){cRPTQUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)R~+]O[~V+fe]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!],w!]!_!j!_!`&u!`!a!j!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jR-OdRPUQOs!jt}!j}!O#r!O!P#r!P!Q.^!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!jP.aP!P!Q.dP.iOQP",
tokenData:
".i~RgOs!jtv!jvw#Xw}!j}!O#r!O!P#r!P!Q%U!Q![%Z![!]'o!]!a!j!a!b+W!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jQ!oUUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jQ#UP;=`<%l!jR#`U^PUQOs!jt!P!j!Q!a!j!b;'S!j;'S;=`#R<%lO!jR#ycRPUQOs!jt}!j}!O#r!O!P#r!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!j~%ZOZ~V%de]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!]#r!]!_!j!_!`&u!`!a!j!b!c!j!c!}%Z!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o%Z#o;'S!j;'S;=`#R<%lO!jU&|Z]SUQOs!jt!P!j!Q![&u![!a!j!b!c!j!c!}&u!}#T!j#T#o&u#o;'S!j;'S;=`#R<%lO!jR'vcRPUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)RQ)YUTQUQOs)Rt!P)R!Q!a)R!b;'S)R;'S;=`)l<%lO)RQ)oP;=`<%l)RR){cRPTQUQOs)Rt})R}!O)r!O!P)r!Q![)r![!])r!]!a)R!b!c)R!c!})r!}#O)r#O#P)R#P#Q)r#Q#R)R#R#S)r#S#T)R#T#o)r#o;'S)R;'S;=`)l<%lO)R~+]O[~V+fe]SRPUQOs!jt}!j}!O#r!O!P#r!Q![%Z![!],w!]!_!j!_!`&u!`!a!j!b!c!j!c!}+]!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o+]#o;'S!j;'S;=`#R<%lO!jR-OdRPUQOs!jt}!j}!O#r!O!P#r!P!Q.^!Q![#r![!]#r!]!a!j!b!c!j!c!}#r!}#O#r#O#P!j#P#Q#r#Q#R!j#R#S#r#S#T!j#T#o#r#o;'S!j;'S;=`#R<%lO!jP.aP!P!Q.dP.iOQP",
tokenizers: [0, 1, 2],
topRules: {"url":[0,1]},
tokenPrec: 63
})
topRules: { url: [0, 1] },
tokenPrec: 63,
});

View File

@@ -24,6 +24,7 @@ export function HotKey({ action, className, variant }: Props) {
)}
>
{labelParts.map((char, index) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<div key={index} className="min-w-[1.1em] text-center">
{char}
</div>

View File

@@ -1,5 +1,6 @@
import classNames from 'classnames';
import React, { Fragment } from 'react';
import type React from 'react';
import { Fragment } from 'react';
import type { HotkeyAction } from '../../hooks/useHotKey';
import { HotKey } from './HotKey';
import { HotKeyLabel } from './HotKeyLabel';

View File

@@ -14,10 +14,10 @@ export function HttpResponseDurationTag({ response }: Props) {
clearInterval(timeout.current);
if (response.state === 'closed') return;
timeout.current = setInterval(() => {
setFallbackElapsed(Date.now() - new Date(response.createdAt + 'Z').getTime());
setFallbackElapsed(Date.now() - new Date(`${response.createdAt}Z`).getTime());
}, 100);
return () => clearInterval(timeout.current);
}, [response.createdAt, response.elapsed, response.state]);
}, [response.createdAt, response.state]);
const title = `HEADER: ${formatMillis(response.elapsedHeaders)}\nTOTAL: ${formatMillis(response.elapsed)}`;
@@ -33,12 +33,12 @@ export function HttpResponseDurationTag({ response }: Props) {
function formatMillis(ms: number) {
if (ms < 1000) {
return `${ms} ms`;
} else if (ms < 60_000) {
}
if (ms < 60_000) {
const seconds = (ms / 1000).toFixed(ms < 10_000 ? 1 : 0);
return `${seconds} s`;
} else {
const minutes = Math.floor(ms / 60_000);
const seconds = Math.round((ms % 60_000) / 1000);
return `${minutes}m ${seconds}s`;
}
const minutes = Math.floor(ms / 60_000);
const seconds = Math.round((ms % 60_000) / 1000);
return `${minutes}m ${seconds}s`;
}

View File

@@ -11,7 +11,7 @@ interface Props {
export function HttpStatusTag({ response, className, showReason, short }: Props) {
const { status, state } = response;
let colorClass;
let colorClass: string;
let label = `${status}`;
if (state === 'initialized') {

View File

@@ -33,6 +33,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
showBadge,
iconColor,
isLoading,
type = 'button',
...props
}: IconButtonProps,
ref,
@@ -56,6 +57,7 @@ export const IconButton = forwardRef<HTMLButtonElement, IconButtonProps>(functio
innerClassName="flex items-center justify-center"
size={size}
color={color}
type={type}
className={classNames(
className,
'group/button relative flex-shrink-0',

View File

@@ -1,4 +1,3 @@
import React from 'react';
import type { IconProps } from './Icon';
import { Icon } from './Icon';
import type { TooltipProps } from './Tooltip';

View File

@@ -88,9 +88,8 @@ export function Input({ type, ...props }: InputProps) {
// use the encrypted input component.
if (type === 'password' && props.autocompleteFunctions) {
return <EncryptionInput {...props} />;
} else {
return <BaseInput type={type} {...props} />;
}
return <BaseInput type={type} {...props} />;
}
function BaseInput({
@@ -145,7 +144,7 @@ function BaseInput({
isFocused: () => editorRef.current?.hasFocus ?? false,
value: () => editorRef.current?.state.doc.toString() ?? '',
dispatch: (...args) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
editorRef.current?.dispatch(...(args as any));
},
selectAll() {
@@ -168,7 +167,9 @@ function BaseInput({
);
useEffect(() => {
const fn = () => (skipNextFocus.current = true);
const fn = () => {
skipNextFocus.current = true;
};
window.addEventListener('focus', fn);
return () => {
window.removeEventListener('focus', fn);

View File

@@ -5,7 +5,7 @@ import { Icon } from './Icon';
interface Props {
depth?: number;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
attrValue: any;
attrKey?: string | number;
attrKeyJsonPath?: string;
@@ -47,15 +47,17 @@ export const JsonAttributeTree = ({
))
: null,
isExpandable: Object.keys(attrValue).length > 0,
label: isExpanded ? `{${Object.keys(attrValue).length || ' '}}` : `{⋯}`,
label: isExpanded ? `{${Object.keys(attrValue).length || ' '}}` : '{⋯}',
labelClassName: 'text-text-subtlest',
};
} else if (jsonType === '[object Array]') {
}
if (jsonType === '[object Array]') {
return {
children: isExpanded
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
? // biome-ignore lint/suspicious/noExplicitAny: none
attrValue.flatMap((v: any, i: number) => (
<JsonAttributeTree
// biome-ignore lint/suspicious/noArrayIndexKey: none
key={i}
depth={depth + 1}
attrValue={v}
@@ -65,26 +67,27 @@ export const JsonAttributeTree = ({
))
: null,
isExpandable: attrValue.length > 0,
label: isExpanded ? `[${attrValue.length || ' '}]` : `[⋯]`,
label: isExpanded ? `[${attrValue.length || ' '}]` : '[⋯]',
labelClassName: 'text-text-subtlest',
};
} else {
return {
children: null,
isExpandable: false,
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',
),
};
}
return {
children: null,
isExpandable: false,
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',
),
};
}, [attrValue, attrKeyJsonPath, isExpanded, depth]);
const labelEl = (
<span className={classNames(labelClassName, 'cursor-text select-text group-hover:text-text-subtle')}>
<span
className={classNames(labelClassName, 'cursor-text select-text group-hover:text-text-subtle')}
>
{label}
</span>
);
@@ -98,7 +101,11 @@ export const JsonAttributeTree = ({
>
<div className="flex items-center">
{isExpandable ? (
<button className="group relative flex items-center pl-4 w-full" onClick={toggleExpanded}>
<button
type="button"
className="group relative flex items-center pl-4 w-full"
onClick={toggleExpanded}
>
<Icon
size="xs"
icon="chevron_right"
@@ -131,7 +138,7 @@ function joinObjectKey(baseKey: string | undefined, key: string): string {
const quotedKey = key.match(/^[a-z0-9_]+$/i) ? key : `\`${key}\``;
if (baseKey == null) return quotedKey;
else return `${baseKey}.${quotedKey}`;
return `${baseKey}.${quotedKey}`;
}
function joinArrayKey(baseKey: string | undefined, index: number): string {

View File

@@ -13,6 +13,7 @@ export function KeyValueRows({ children }: Props) {
<table className="text-xs font-mono min-w-0 w-full mb-auto">
<tbody className="divide-y divide-surface-highlight">
{children.map((child, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<tr key={i}>{child}</tr>
))}
</tbody>

View File

@@ -37,6 +37,7 @@ export function Label({
{required === true && <span className="text-text-subtlest">*</span>}
</span>
{tags.map((tag, i) => (
// biome-ignore lint/suspicious/noArrayIndexKey: none
<span key={i} className="text-xs text-text-subtlest">
({tag})
</span>

View File

@@ -143,6 +143,7 @@ export function PairEditor({
[handle, pairs, setRef],
);
// biome-ignore lint/correctness/useExhaustiveDependencies: Only care about forceUpdateKey
useEffect(() => {
// Remove empty headers on initial render and ensure they all have valid ids (pairs didn't use to have IDs)
const newPairs: PairWithId[] = [];
@@ -161,8 +162,6 @@ export function PairEditor({
setPairs(newPairs);
regenerateLocalForceUpdateKey();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [forceUpdateKey]);
const setPairsAndSave = useCallback(
@@ -534,9 +533,8 @@ export function PairEditorRow({
const valueAutocompleteVariablesFiltered = useMemo<EditorProps['autocompleteVariables']>(() => {
if (valueAutocompleteVariables === 'environment') {
return (v: WrappedEnvironmentVariable): boolean => v.variable.name !== pair.name;
} else {
return valueAutocompleteVariables;
}
return valueAutocompleteVariables;
}, [pair.name, valueAutocompleteVariables]);
const handleSetRef = useCallback(

View File

@@ -4,7 +4,6 @@ import type { Pair, PairWithId } from './PairEditor';
export function ensurePairId(p: Pair): PairWithId {
if (typeof p.id === 'string') {
return p as PairWithId;
} else {
return { ...p, id: p.id ?? generateId() };
}
return { ...p, id: p.id ?? generateId() };
}

View File

@@ -194,6 +194,8 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
key={forceUpdateKey}
type={type === 'password' && !obscured ? 'text' : type}
name={name}
// biome-ignore lint/a11y/noAutofocus: Who cares
autoFocus={autoFocus}
defaultValue={defaultValue ?? undefined}
autoComplete="off"
autoCapitalize="off"
@@ -204,7 +206,6 @@ export const PlainInput = forwardRef<{ focus: () => void }, PlainInputProps>(fun
onFocus={handleFocus}
onBlur={handleBlur}
required={required}
autoFocus={autoFocus}
placeholder={placeholder}
onKeyDownCapture={onKeyDownCapture}
/>

View File

@@ -30,7 +30,7 @@ export function Prompt({
[onResult, value],
);
const id = 'prompt.form.' + useRef(generateId()).current;
const id = `prompt.form.${useRef(generateId()).current}`;
return (
<form

View File

@@ -45,15 +45,14 @@ export function RadioDropdown<T = string | null>({
...items.map((item) => {
if (item.type === 'separator') {
return item;
} else {
return {
key: item.value,
label: item.label,
rightSlot: item.rightSlot,
onSelect: () => onChange(item.value),
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
} as DropdownItem;
}
return {
key: item.value,
label: item.label,
rightSlot: item.rightSlot,
onSelect: () => onChange(item.value),
leftSlot: <Icon icon={value === item.value ? 'check' : 'empty'} />,
} as DropdownItem;
}),
...((itemsAfter
? [{ type: 'separator', hidden: itemsAfter[0]?.type === 'separator' }, ...itemsAfter]

View File

@@ -37,18 +37,18 @@ export function SegmentedControl<T extends string>({
const selectedIndex = options.findIndex((o) => o.value === selectedValue);
if (e.key === 'ArrowRight') {
const newIndex = Math.abs((selectedIndex + 1) % options.length);
setSelectedValue(options[newIndex]!.value);
options[newIndex] && setSelectedValue(options[newIndex].value);
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
child.focus();
} else if (e.key === 'ArrowLeft') {
const newIndex = Math.abs((selectedIndex - 1) % options.length);
setSelectedValue(options[newIndex]!.value);
options[newIndex] && setSelectedValue(options[newIndex].value);
const child = containerRef.current?.children[newIndex] as HTMLButtonElement;
child.focus();
}
}}
>
{options.map((o, i) => {
{options.map((o) => {
const isSelected = selectedValue === o.value;
const isActive = value === o.value;
return (
@@ -63,7 +63,7 @@ export function SegmentedControl<T extends string>({
'!px-1.5 !w-auto',
'focus:ring-border-focus',
)}
key={i}
key={o.label}
title={o.label}
icon={o.icon}
onClick={() => onChange(o.value)}

View File

@@ -1,3 +1,4 @@
import { type } from '@tauri-apps/plugin-os';
import classNames from 'classnames';
import type { CSSProperties, ReactNode } from 'react';
import { useState } from 'react';
@@ -7,7 +8,6 @@ import { Label } from './Label';
import type { RadioDropdownItem } from './RadioDropdown';
import { RadioDropdown } from './RadioDropdown';
import { HStack } from './Stacks';
import { type } from '@tauri-apps/plugin-os';
export interface SelectProps<T extends string> {
name: string;

View File

@@ -10,9 +10,15 @@ interface Props {
color?: Color;
}
export function Separator({ color, className, dashed, orientation = 'horizontal', children }: Props) {
export function Separator({
color,
className,
dashed,
orientation = 'horizontal',
children,
}: Props) {
return (
<div role="separator" 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>
)}

View File

@@ -1,7 +1,7 @@
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import type { CSSProperties, ReactNode } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
import { useCallback, useMemo, useRef } from 'react';
import { useLocalStorage } from 'react-use';
import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace';
import { useContainerSize } from '../../hooks/useContainerQuery';
@@ -99,8 +99,10 @@ export function SplitLayout({
containerRef.current,
);
const $c = containerRef.current;
const containerWidth = $c.clientWidth - parseFloat(paddingLeft) - parseFloat(paddingRight);
const containerHeight = $c.clientHeight - parseFloat(paddingTop) - parseFloat(paddingBottom);
const containerWidth =
$c.clientWidth - Number.parseFloat(paddingLeft) - Number.parseFloat(paddingRight);
const containerHeight =
$c.clientHeight - Number.parseFloat(paddingTop) - Number.parseFloat(paddingBottom);
const mouseStartX = e.xStart;
const mouseStartY = e.yStart;

View File

@@ -20,7 +20,7 @@ interface HStackProps extends BaseStackProps {
export const HStack = forwardRef(function HStack(
{ className, space, children, alignItems = 'center', ...props }: HStackProps,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
ref: ForwardedRef<any>,
) {
return (
@@ -41,7 +41,7 @@ export type VStackProps = BaseStackProps & {
export const VStack = forwardRef(function VStack(
{ className, space, children, ...props }: VStackProps,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
ref: ForwardedRef<any>,
) {
return (
@@ -65,7 +65,7 @@ type BaseStackProps = HTMLAttributes<HTMLElement> & {
const BaseStack = forwardRef(function BaseStack(
{ className, alignItems, justifyContent, wrap, children, as, ...props }: BaseStackProps,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// biome-ignore lint/suspicious/noExplicitAny: none
ref: ForwardedRef<any>,
) {
const Component = as ?? 'div';

View File

@@ -1,6 +1,5 @@
import classNames from 'classnames';
import type { ReactNode } from 'react';
import React from 'react';
export function Table({ children }: { children: ReactNode }) {
return (
@@ -57,7 +56,12 @@ export function TableHeaderCell({
className?: string;
}) {
return (
<th className={classNames(className, 'py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle')}>
<th
className={classNames(
className,
'py-2 [&:not(:first-child)]:pl-4 text-left text-text-subtle',
)}
>
{children}
</th>
);

View File

@@ -50,7 +50,7 @@ export 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');
@@ -80,6 +80,7 @@ export function Tabs({
)}
>
<div
role="tablist"
aria-label={label}
className={classNames(
tabListClassName,
@@ -162,13 +163,12 @@ export function Tabs({
</Button>
</RadioDropdown>
);
} else {
return (
<Button key={t.value} rightSlot={t.rightSlot} {...btnProps}>
{t.label}
</Button>
);
}
return (
<Button key={t.value} rightSlot={t.rightSlot} {...btnProps}>
{t.label}
</Button>
);
})}
</div>
</div>

View File

@@ -2,7 +2,7 @@ import type { ShowToastRequest } from '@yaakapp-internal/plugins';
import classNames from 'classnames';
import * as m from 'motion/react-m';
import type { ReactNode } from 'react';
import React from 'react';
import { useKey } from 'react-use';
import type { IconProps } from './Icon';
import { Icon } from './Icon';

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { CSSProperties, KeyboardEvent, ReactNode } from 'react';
import React, { useRef, useState } from 'react';
import { useRef, useState } from 'react';
import { generateId } from '../../lib/generateId';
import { Portal } from '../Portal';
@@ -91,6 +91,7 @@ export function Tooltip({ children, className, content, tabIndex, size = 'md' }:
<Triangle className="text-border mb-2" />
</div>
</Portal>
{/** biome-ignore lint/a11y/useSemanticElements: Needs to be usable in other buttons */}
<span
ref={triggerRef}
role="button"
@@ -124,6 +125,7 @@ function Triangle({ className }: { className?: string }) {
'h-[0.5rem] w-[0.8rem]',
)}
>
<title>Triangle</title>
<polygon className="fill-surface-highlight" points="0,0 30,0 15,10" />
<path
d="M0 0 L15 9 L30 0"

View File

@@ -9,7 +9,7 @@ interface Props {
export function WebsocketStatusTag({ connection, className }: Props) {
const { state, error } = connection;
let label;
let label: string;
let colorClass = 'text-text-subtle';
if (error) {

View File

@@ -11,9 +11,18 @@ import {
import { type } from '@tauri-apps/plugin-os';
import classNames from 'classnames';
import type { ComponentType, MouseEvent, ReactElement, Ref, RefAttributes } from 'react';
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState, } from 'react';
import {
forwardRef,
memo,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from 'react';
import { useKey, useKeyPressEvent } from 'react-use';
import type { HotkeyAction, HotKeyOptions } from '../../../hooks/useHotKey';
import type { HotKeyOptions, HotkeyAction } from '../../../hooks/useHotKey';
import { useHotKey } from '../../../hooks/useHotKey';
import { computeSideForDragMove } from '../../../lib/dnd';
import { jotaiStore } from '../../../lib/jotai';
@@ -102,6 +111,7 @@ function TreeInner<T extends { id: string }>(
}, []);
// Select the first item on first render
// biome-ignore lint/correctness/useExhaustiveDependencies: Only used for initial render
useEffect(() => {
const ids = jotaiStore.get(selectedIdsFamily(treeId));
const fallback = selectableItems[0];
@@ -112,7 +122,6 @@ function TreeInner<T extends { id: string }>(
lastId: fallback.node.item.id,
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [treeId]);
const handleCloseContextMenu = useCallback(() => {
@@ -129,11 +138,10 @@ function TreeInner<T extends { id: string }>(
);
if ($el == null) {
return false;
} else {
$el.focus();
$el.scrollIntoView({ block: 'nearest' });
return true;
}
$el.focus();
$el.scrollIntoView({ block: 'nearest' });
return true;
}, []);
const ensureTabbableItem = useCallback(() => {
@@ -166,7 +174,7 @@ function TreeInner<T extends { id: string }>(
useEffect(() => {
const unsub = jotaiStore.sub(collapsedFamily(treeId), ensureTabbableItem);
return unsub;
}, [ensureTabbableItem, isTreeFocused, selectableItems, treeId, tryFocus]);
}, [ensureTabbableItem, treeId]);
// Ensure there's always a tabbable item after render
useEffect(() => {
@@ -224,13 +232,12 @@ function TreeInner<T extends { id: string }>(
if (isSelected) {
// If right-clicked an item that was in the multiple-selection, use the entire selection
return getContextMenu(items);
} else {
// If right-clicked an item that was NOT in the multiple-selection, just use that one
// Also update the selection with it
setSelected([item.id], false);
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id }));
return getContextMenu([item]);
}
// If right-clicked an item that was NOT in the multiple-selection, just use that one
// Also update the selection with it
setSelected([item.id], false);
jotaiStore.set(focusIdsFamily(treeId), (prev) => ({ ...prev, lastId: item.id }));
return getContextMenu([item]);
};
}, [getContextMenu, selectableItems, setSelected, treeId]);
@@ -728,6 +735,7 @@ function DropRegionAfterList({
onContextMenu?: (e: MouseEvent<HTMLDivElement>) => void;
}) {
const { setNodeRef } = useDroppable({ id });
// biome-ignore lint/a11y/noStaticElementInteractions: Meh
return <div ref={setNodeRef} onContextMenu={onContextMenu} />;
}
@@ -758,7 +766,7 @@ function TreeHotKey<T extends { id: string }>({
enable: () => {
if (enable == null) return true;
if (typeof enable === 'function') return enable();
else return enable;
return enable;
},
},
);

View File

@@ -19,7 +19,7 @@ export function TreeDragOverlay<T extends { id: string }>({
return (
<DragOverlay dropAnimation={null}>
<TreeItemList
treeId={treeId + '.dragging'}
treeId={`${treeId}.dragging`}
nodes={selectableItems.filter((i) => draggingItems.includes(i.node.item.id))}
getItemKey={getItemKey}
ItemInner={ItemInner}

View File

@@ -19,6 +19,7 @@ export const TreeIndentGuide = memo(function TreeIndentGuide({
<div className="flex">
{Array.from({ length: depth }).map((_, i) => (
<div
// biome-ignore lint/suspicious/noArrayIndexKey: none
key={i}
className={classNames(
'w-[calc(1rem+0.5px)] border-r border-r-text-subtlest',

View File

@@ -3,14 +3,15 @@ import { useDndContext, useDndMonitor, useDraggable, useDroppable } from '@dnd-k
import classNames from 'classnames';
import { useAtomValue } from 'jotai';
import { selectAtom } from 'jotai/utils';
import type React from 'react';
import type { MouseEvent, PointerEvent } from 'react';
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } 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 { collapsedFamily, isCollapsedFamily, isLastFocusedFamily, isSelectedFamily } from './atoms';
import type { TreeNode } from './common';
import { getNodeKey } from './common';
import type { TreeProps } from './Tree';
@@ -84,7 +85,7 @@ function TreeItem_<T extends { id: string }>({
},
scrollIntoView: () => {
listItemRef.current?.scrollIntoView({ block: 'nearest' });
}
},
}),
[editing, getEditOptions],
);
@@ -272,10 +273,6 @@ function TreeItem_<T extends { id: string }>({
return (
<li
ref={listItemRef}
role="treeitem"
aria-level={depth + 1}
aria-expanded={node.children == null ? undefined : !isCollapsed}
aria-selected={isSelected}
onContextMenu={handleContextMenu}
className={classNames(
className,
@@ -304,6 +301,7 @@ function TreeItem_<T extends { id: string }>({
)}
{node.children != null ? (
<button
type="button"
tabIndex={-1}
className="h-full pl-[0.5rem] outline-none"
onClick={toggleCollapsed}

View File

@@ -29,7 +29,7 @@ export function TreeItemList<T extends { id: string }>({
...props
}: TreeItemListProps<T>) {
return (
<ul role="tree" style={style} className={className}>
<ul style={style} className={className}>
<TreeDropMarker node={null} treeId={treeId} index={0} />
{nodes.map((child, i) => (
<Fragment key={getItemKey(child.node.item)}>

View File

@@ -21,7 +21,7 @@ export const focusIdsFamily = atomFamily((_treeId: string) => {
export const isLastFocusedFamily = atomFamily(
({ treeId, itemId }: { treeId: string; itemId: string }) =>
selectAtom(focusIdsFamily(treeId), (v) => v.lastId == itemId, Object.is),
selectAtom(focusIdsFamily(treeId), (v) => v.lastId === itemId, Object.is),
(a, b) => a.treeId === b.treeId && a.itemId === b.itemId,
);

View File

@@ -35,8 +35,13 @@ export function equalSubtree<T extends { id: string }>(
if (getNodeKey(a, getItemKey) !== getNodeKey(b, getItemKey)) return false;
const ak = a.children ?? [];
const bk = b.children ?? [];
if (ak.length !== bk.length) return false;
if (ak.length !== bk.length) {
return false;
}
for (let i = 0; i < ak.length; i++) {
// biome-ignore lint/style/noNonNullAssertion: none
if (!equalSubtree(ak[i]!, bk[i]!, getItemKey)) return false;
}

View File

@@ -6,7 +6,7 @@ export function useSelectableItems<T extends { id: string }>(root: TreeNode<T>)
const selectableItems: SelectableTreeNode<T>[] = [];
// Put requests and folders into a tree structure
const next = (node: TreeNode<T>, depth: number = 0) => {
const next = (node: TreeNode<T>, depth = 0) => {
if (node.children == null) {
return;
}