mirror of
https://github.com/actualbudget/actual.git
synced 2026-03-09 03:32:54 -05:00
Implement PayeeAutocomplete2 based on react-aria-component's ComboBox
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
import {
|
||||
type ComponentProps,
|
||||
useRef,
|
||||
useContext,
|
||||
type KeyboardEvent,
|
||||
useEffect,
|
||||
createContext,
|
||||
type ReactNode,
|
||||
} from 'react';
|
||||
import {
|
||||
ComboBox,
|
||||
ListBox,
|
||||
ListBoxItem,
|
||||
ListBoxSection,
|
||||
type ListBoxSectionProps,
|
||||
type ComboBoxProps,
|
||||
type ListBoxItemProps,
|
||||
ComboBoxStateContext,
|
||||
type Key,
|
||||
} from 'react-aria-components';
|
||||
import { type ComboBoxState } from 'react-stately';
|
||||
|
||||
import { Input } from '@actual-app/components/input';
|
||||
import { Popover } from '@actual-app/components/popover';
|
||||
import { styles } from '@actual-app/components/styles';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
import { View } from '@actual-app/components/view';
|
||||
import { css, cx } from '@emotion/css';
|
||||
|
||||
const popoverClassName = () =>
|
||||
css({
|
||||
...styles.darkScrollbar,
|
||||
...styles.popover,
|
||||
backgroundColor: theme.menuAutoCompleteBackground,
|
||||
color: theme.menuAutoCompleteText,
|
||||
padding: '5px 0',
|
||||
borderRadius: 4,
|
||||
});
|
||||
|
||||
const listBoxClassName = ({ width }: { width?: number }) =>
|
||||
css({
|
||||
width,
|
||||
minWidth: 200,
|
||||
maxHeight: 200,
|
||||
overflow: 'auto',
|
||||
'& [data-focused]': {
|
||||
backgroundColor: theme.menuAutoCompleteBackgroundHover,
|
||||
},
|
||||
});
|
||||
|
||||
type Autocomplete2Props<T extends object> = Omit<
|
||||
ComboBoxProps<T>,
|
||||
'children'
|
||||
> & {
|
||||
inputPlaceholder?: string;
|
||||
children: ComponentProps<typeof ListBox<T>>['children'];
|
||||
};
|
||||
|
||||
export function Autocomplete2<T extends object>({
|
||||
children,
|
||||
...props
|
||||
}: Autocomplete2Props<T>) {
|
||||
const viewRef = useRef<HTMLDivElement | null>(null);
|
||||
return (
|
||||
<ComboBox<T>
|
||||
allowsEmptyCollection
|
||||
allowsCustomValue
|
||||
menuTrigger="focus"
|
||||
{...props}
|
||||
>
|
||||
<View ref={viewRef}>
|
||||
<AutocompleteInput placeholder={props.inputPlaceholder} />
|
||||
</View>
|
||||
<Popover isNonModal className={popoverClassName()}>
|
||||
<ListBox<T>
|
||||
className={listBoxClassName({ width: viewRef.current?.clientWidth })}
|
||||
>
|
||||
{children}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</ComboBox>
|
||||
);
|
||||
}
|
||||
|
||||
type AutocompleteInputContextValue = {
|
||||
getFocusedKey?: (state: ComboBoxState<unknown>) => Key | null;
|
||||
};
|
||||
|
||||
const AutocompleteInputContext =
|
||||
createContext<AutocompleteInputContextValue | null>(null);
|
||||
|
||||
type AutocompleteInputProviderProps = {
|
||||
children: ReactNode;
|
||||
getFocusedKey?: (state: ComboBoxState<unknown>) => Key | null;
|
||||
};
|
||||
|
||||
export function AutocompleteInputProvider({
|
||||
children,
|
||||
getFocusedKey,
|
||||
}: AutocompleteInputProviderProps) {
|
||||
return (
|
||||
<AutocompleteInputContext.Provider value={{ getFocusedKey }}>
|
||||
{children}
|
||||
</AutocompleteInputContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
type AutocompleteInputProps = ComponentProps<typeof Input>;
|
||||
|
||||
function AutocompleteInput({ onKeyUp, ...props }: AutocompleteInputProps) {
|
||||
const state = useContext(ComboBoxStateContext);
|
||||
const _onKeyUp = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Escape') {
|
||||
state.revert();
|
||||
}
|
||||
onKeyUp?.(e);
|
||||
};
|
||||
|
||||
const autocompleteInputContext = useContext(AutocompleteInputContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (state && state.inputValue && !state.selectionManager.focusedKey) {
|
||||
const focusedKey = autocompleteInputContext?.getFocusedKey
|
||||
? autocompleteInputContext.getFocusedKey(state)
|
||||
: defaultGetFocusedKey(state);
|
||||
|
||||
state.selectionManager.setFocusedKey(focusedKey);
|
||||
}
|
||||
}, [autocompleteInputContext, state, state?.inputValue]);
|
||||
|
||||
return <Input onKeyUp={_onKeyUp} {...props} />;
|
||||
}
|
||||
|
||||
function defaultGetFocusedKey<T>(state: ComboBoxState<T>) {
|
||||
// Focus on the first suggestion item when typing.
|
||||
const keys = Array.from(state.collection.getKeys());
|
||||
return keys
|
||||
.map(key => state.collection.getItem(key))
|
||||
.find(i => i.type === 'item')?.key;
|
||||
}
|
||||
|
||||
const defaultAutocompleteSectionClassName = css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
'& header': {
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 10,
|
||||
color: theme.menuAutoCompleteTextHeader,
|
||||
},
|
||||
});
|
||||
|
||||
type AutocompleteSectionProps<T extends object> = ListBoxSectionProps<T>;
|
||||
|
||||
export function AutocompleteSection<T extends object>({
|
||||
className,
|
||||
...props
|
||||
}: AutocompleteSectionProps<T>) {
|
||||
return (
|
||||
<ListBoxSection
|
||||
className={cx(defaultAutocompleteSectionClassName, className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const defaultAutocompleteItemClassName = css({
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
paddingTop: 5,
|
||||
paddingBottom: 5,
|
||||
paddingLeft: 20,
|
||||
});
|
||||
|
||||
type AutocompleteItemProps = ListBoxItemProps;
|
||||
|
||||
export function AutocompleteItem({
|
||||
className,
|
||||
...props
|
||||
}: AutocompleteItemProps) {
|
||||
return (
|
||||
<ListBoxItem
|
||||
className={
|
||||
typeof className === 'function'
|
||||
? renderProps =>
|
||||
cx(defaultAutocompleteItemClassName, className(renderProps))
|
||||
: cx(defaultAutocompleteItemClassName, className)
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,346 @@
|
||||
import { type ComponentProps, useMemo, useState } from 'react';
|
||||
import { Header, type Key } from 'react-aria-components';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { SvgAdd } from '@actual-app/components/icons/v1';
|
||||
import { theme } from '@actual-app/components/theme';
|
||||
|
||||
import {
|
||||
normalisedEquals,
|
||||
normalisedIncludes,
|
||||
} from 'loot-core/shared/normalisation';
|
||||
import { type AccountEntity, type PayeeEntity } from 'loot-core/types/models';
|
||||
|
||||
import {
|
||||
Autocomplete2,
|
||||
AutocompleteInputProvider,
|
||||
AutocompleteItem,
|
||||
AutocompleteSection,
|
||||
} from './Autocomplete2';
|
||||
|
||||
import { useAccounts } from '@desktop-client/hooks/useAccounts';
|
||||
import { useCommonPayees, usePayees } from '@desktop-client/hooks/usePayees';
|
||||
import { usePrevious } from '@desktop-client/hooks/usePrevious';
|
||||
import {
|
||||
createPayee,
|
||||
getActivePayees,
|
||||
} from '@desktop-client/payees/payeesSlice';
|
||||
import { useDispatch } from '@desktop-client/redux';
|
||||
|
||||
type PayeeAutocompleteItemType = {
|
||||
type: 'account' | 'payee' | 'suggested';
|
||||
};
|
||||
|
||||
type PayeeAutocompleteItem = PayeeEntity & PayeeAutocompleteItemType;
|
||||
|
||||
type PayeeAutocomplete2Props = Omit<
|
||||
ComponentProps<typeof Autocomplete2>,
|
||||
'children'
|
||||
> & {
|
||||
showInactive?: boolean;
|
||||
};
|
||||
|
||||
export function PayeeAutocomplete2({
|
||||
showInactive,
|
||||
selectedKey,
|
||||
onOpenChange,
|
||||
onSelectionChange,
|
||||
...props
|
||||
}: PayeeAutocomplete2Props) {
|
||||
const { t } = useTranslation();
|
||||
const payees = usePayees();
|
||||
const commonPayees = useCommonPayees();
|
||||
const accounts = useAccounts();
|
||||
const [focusTransferPayees, setFocusedTransferPayees] = useState(false);
|
||||
|
||||
const allPayeeSuggestions: PayeeAutocompleteItem[] = useMemo(() => {
|
||||
const suggestions = getPayeeSuggestions(commonPayees, payees);
|
||||
|
||||
let filteredSuggestions: PayeeAutocompleteItem[] = [...suggestions];
|
||||
|
||||
if (!showInactive) {
|
||||
filteredSuggestions = filterActivePayees(filteredSuggestions, accounts);
|
||||
}
|
||||
|
||||
if (focusTransferPayees) {
|
||||
filteredSuggestions = filterTransferPayees(filteredSuggestions);
|
||||
}
|
||||
|
||||
return filteredSuggestions;
|
||||
}, [commonPayees, payees, showInactive, focusTransferPayees, accounts]);
|
||||
|
||||
const [inputValue, setInputValue] = useState(
|
||||
getPayeeName(allPayeeSuggestions, selectedKey),
|
||||
);
|
||||
const [_selectedKey, setSelectedKey] = useState<Key | null>(
|
||||
selectedKey || null,
|
||||
);
|
||||
|
||||
const [isAutocompleteOpen, setIsAutocompleteOpen] = useState(false);
|
||||
const previousIsAutocompeleteOpen = usePrevious(isAutocompleteOpen);
|
||||
const isInitialAutocompleteOpen =
|
||||
!previousIsAutocompeleteOpen && isAutocompleteOpen;
|
||||
|
||||
const filter = (textValue: string, inputValue: string) => {
|
||||
return (
|
||||
isInitialAutocompleteOpen || normalisedIncludes(textValue, inputValue)
|
||||
);
|
||||
};
|
||||
|
||||
const filteredPayeeSuggestions = allPayeeSuggestions.filter(p =>
|
||||
filter(p.name, inputValue),
|
||||
);
|
||||
|
||||
const suggestedPayees = filteredPayeeSuggestions.filter(
|
||||
p => p.type === 'suggested',
|
||||
);
|
||||
const regularPayees = filteredPayeeSuggestions.filter(
|
||||
p => !p.favorite && p.type === 'payee',
|
||||
);
|
||||
const accountPayees = filteredPayeeSuggestions.filter(
|
||||
p => p.type === 'account',
|
||||
);
|
||||
|
||||
const findExactMatchPayee = () =>
|
||||
filteredPayeeSuggestions.find(
|
||||
p => p.id !== 'new' && normalisedEquals(p.name, inputValue),
|
||||
);
|
||||
|
||||
const exactMatchPayee = findExactMatchPayee();
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const onCreatePayee = () => {
|
||||
return dispatch(createPayee({ name: inputValue })).unwrap();
|
||||
};
|
||||
|
||||
const _onSelectionChange = async (id: Key | null) => {
|
||||
if (id === 'new') {
|
||||
const newPayeeId = await onCreatePayee?.();
|
||||
setSelectedKey(newPayeeId);
|
||||
onSelectionChange?.(newPayeeId);
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedKey(id);
|
||||
setInputValue(getPayeeName(filteredPayeeSuggestions, id));
|
||||
onSelectionChange?.(id);
|
||||
};
|
||||
|
||||
const _onOpenChange = (isOpen: boolean) => {
|
||||
setIsAutocompleteOpen(isOpen);
|
||||
onOpenChange?.(isOpen);
|
||||
};
|
||||
|
||||
const getFocusedKey: ComponentProps<
|
||||
typeof AutocompleteInputProvider
|
||||
>['getFocusedKey'] = state => {
|
||||
const keys = Array.from(state.collection.getKeys());
|
||||
const found = keys
|
||||
.map(key => state.collection.getItem(key))
|
||||
.find(i => i.type === 'item' && i.key !== 'new');
|
||||
|
||||
// Focus on the first suggestion item when typing.
|
||||
// Otherwise, if there are no results, focus on the "new" item to allow creating a new entry.
|
||||
return found?.key || 'new';
|
||||
};
|
||||
|
||||
return (
|
||||
<AutocompleteInputProvider getFocusedKey={getFocusedKey}>
|
||||
<Autocomplete2
|
||||
aria-label={t('Payee autocomplete')}
|
||||
inputPlaceholder="nothing"
|
||||
inputValue={inputValue}
|
||||
onInputChange={setInputValue}
|
||||
selectedKey={_selectedKey || selectedKey}
|
||||
onSelectionChange={_onSelectionChange}
|
||||
onOpenChange={_onOpenChange}
|
||||
{...props}
|
||||
>
|
||||
<PayeeList
|
||||
showCreatePayee={!!inputValue && !exactMatchPayee}
|
||||
inputValue={inputValue}
|
||||
suggestedPayees={suggestedPayees}
|
||||
regularPayees={regularPayees}
|
||||
accountPayees={accountPayees}
|
||||
/>
|
||||
|
||||
{/* <AutocompleteSection className={css({ position: 'sticky', bottom: 0, })}>
|
||||
<Button variant="menu" slot={null}>
|
||||
<Trans>Make transfer</Trans>
|
||||
</Button>
|
||||
<Button variant="menu" slot={null}>
|
||||
<Trans>Manage payees</Trans>
|
||||
</Button>
|
||||
</AutocompleteSection> */}
|
||||
</Autocomplete2>
|
||||
</AutocompleteInputProvider>
|
||||
);
|
||||
}
|
||||
|
||||
type PayeeListProps = {
|
||||
showCreatePayee: boolean;
|
||||
inputValue: string;
|
||||
suggestedPayees: PayeeAutocompleteItem[];
|
||||
regularPayees: PayeeAutocompleteItem[];
|
||||
accountPayees: PayeeAutocompleteItem[];
|
||||
};
|
||||
|
||||
function PayeeList({
|
||||
showCreatePayee,
|
||||
inputValue,
|
||||
suggestedPayees,
|
||||
regularPayees,
|
||||
accountPayees,
|
||||
}: PayeeListProps) {
|
||||
// useAutocompleteFocusOnInput({
|
||||
// getFocusedKey: state => {
|
||||
// const keys = Array.from(state.collection.getKeys());
|
||||
// return keys
|
||||
// .map(key => state.collection.getItem(key))
|
||||
// .find(i => i.type === 'item' && i.key !== 'new')?.key || 'new';
|
||||
// },
|
||||
// });
|
||||
|
||||
return (
|
||||
<>
|
||||
{showCreatePayee && (
|
||||
<AutocompleteItem
|
||||
key="new"
|
||||
id="new"
|
||||
textValue={inputValue}
|
||||
style={{ paddingLeft: 10, color: theme.noticeText }}
|
||||
>
|
||||
<SvgAdd width={8} height={8} style={{ marginRight: 5 }} />
|
||||
Create payee: {inputValue}
|
||||
</AutocompleteItem>
|
||||
)}
|
||||
|
||||
{suggestedPayees.length > 0 && (
|
||||
<AutocompleteSection>
|
||||
<Header>
|
||||
<Trans>Suggested Payees</Trans>
|
||||
</Header>
|
||||
{suggestedPayees.map(payee => (
|
||||
<AutocompleteItem
|
||||
key={payee.id}
|
||||
id={payee.id}
|
||||
textValue={payee.name}
|
||||
value={payee}
|
||||
>
|
||||
{payee.name}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</AutocompleteSection>
|
||||
)}
|
||||
|
||||
{regularPayees.length > 0 && (
|
||||
<AutocompleteSection>
|
||||
<Header>
|
||||
<Trans>Payees</Trans>
|
||||
</Header>
|
||||
{regularPayees.map(payee => (
|
||||
<AutocompleteItem
|
||||
key={payee.id}
|
||||
id={payee.id}
|
||||
textValue={payee.name}
|
||||
value={payee}
|
||||
>
|
||||
{payee.name}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</AutocompleteSection>
|
||||
)}
|
||||
|
||||
{accountPayees.length > 0 && (
|
||||
<AutocompleteSection>
|
||||
<Header>
|
||||
<Trans>Transfer To/From</Trans>
|
||||
</Header>
|
||||
{accountPayees.map(payee => (
|
||||
<AutocompleteItem
|
||||
key={payee.id}
|
||||
id={payee.id}
|
||||
textValue={payee.name}
|
||||
value={payee}
|
||||
>
|
||||
{payee.name}
|
||||
</AutocompleteItem>
|
||||
))}
|
||||
</AutocompleteSection>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const MAX_AUTO_SUGGESTIONS = 5;
|
||||
|
||||
function getPayeeSuggestions(
|
||||
commonPayees: PayeeEntity[],
|
||||
payees: PayeeEntity[],
|
||||
): PayeeAutocompleteItem[] {
|
||||
const favoritePayees = payees
|
||||
.filter(p => p.favorite)
|
||||
.map(p => {
|
||||
return { ...p, type: determineType(p, true) };
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
let additionalCommonPayees: PayeeAutocompleteItem[] = [];
|
||||
if (commonPayees?.length > 0) {
|
||||
if (favoritePayees.length < MAX_AUTO_SUGGESTIONS) {
|
||||
additionalCommonPayees = commonPayees
|
||||
.filter(
|
||||
p => !(p.favorite || favoritePayees.map(fp => fp.id).includes(p.id)),
|
||||
)
|
||||
.slice(0, MAX_AUTO_SUGGESTIONS - favoritePayees.length)
|
||||
.map(p => {
|
||||
return { ...p, type: determineType(p, true) };
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
}
|
||||
|
||||
if (favoritePayees.length + additionalCommonPayees.length) {
|
||||
const filteredPayees: PayeeAutocompleteItem[] = payees
|
||||
.filter(p => !favoritePayees.find(fp => fp.id === p.id))
|
||||
.filter(p => !additionalCommonPayees.find(fp => fp.id === p.id))
|
||||
.map<PayeeAutocompleteItem>(p => {
|
||||
return { ...p, type: determineType(p, false) };
|
||||
});
|
||||
|
||||
return favoritePayees.concat(additionalCommonPayees).concat(filteredPayees);
|
||||
}
|
||||
|
||||
return payees.map(p => {
|
||||
return { ...p, type: determineType(p, false) };
|
||||
});
|
||||
}
|
||||
|
||||
function filterActivePayees<T extends PayeeEntity>(
|
||||
payees: T[],
|
||||
accounts: AccountEntity[],
|
||||
): T[] {
|
||||
return accounts ? (getActivePayees(payees, accounts) as T[]) : payees;
|
||||
}
|
||||
|
||||
function filterTransferPayees(payees: PayeeAutocompleteItem[]) {
|
||||
return payees.filter(payee => !!payee.transfer_acct);
|
||||
}
|
||||
|
||||
function determineType(
|
||||
payee: PayeeEntity,
|
||||
isCommon: boolean,
|
||||
): PayeeAutocompleteItem['type'] {
|
||||
if (payee.transfer_acct) {
|
||||
return 'account';
|
||||
}
|
||||
if (isCommon) {
|
||||
return 'suggested';
|
||||
} else {
|
||||
return 'payee';
|
||||
}
|
||||
}
|
||||
|
||||
function getPayeeName<T extends PayeeEntity>(items: T[], id: Key | null) {
|
||||
return items.find(p => p.id === id)?.name || '';
|
||||
}
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
|
||||
import { FormulaActionEditor } from './FormulaActionEditor';
|
||||
|
||||
import { PayeeAutocomplete2 } from '@desktop-client/components/autocomplete/PayeeAutocomplete2';
|
||||
import { StatusBadge } from '@desktop-client/components/schedules/StatusBadge';
|
||||
import { SimpleTransactionsTable } from '@desktop-client/components/transactions/SimpleTransactionsTable';
|
||||
import { BetweenAmountInput } from '@desktop-client/components/util/AmountInput';
|
||||
@@ -319,6 +320,13 @@ function ConditionEditor({
|
||||
onChange={v => onChange('value', v)}
|
||||
/>
|
||||
);
|
||||
} else if (field === 'payee') {
|
||||
valueEditor = (
|
||||
<PayeeAutocomplete2
|
||||
selectedKey={value}
|
||||
onSelectionChange={id => onChange('value', id)}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
valueEditor = (
|
||||
<GenericInput
|
||||
|
||||
@@ -4,3 +4,11 @@ export function getNormalisedString(value: string) {
|
||||
.normalize('NFD')
|
||||
.replace(/\p{Diacritic}/gu, '');
|
||||
}
|
||||
|
||||
export function normalisedEquals(a: string, b: string) {
|
||||
return getNormalisedString(a) === getNormalisedString(b);
|
||||
}
|
||||
|
||||
export function normalisedIncludes(a: string, b: string) {
|
||||
return getNormalisedString(a).includes(getNormalisedString(b));
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/5875.md
Normal file
6
upcoming-release-notes/5875.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Enhancements
|
||||
authors: [joel-jeremy]
|
||||
---
|
||||
|
||||
Implement PayeeAutocomplete2 based on react-aria-component's ComboBox
|
||||
113
yarn.lock
113
yarn.lock
@@ -4099,6 +4099,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@internationalized/string@npm:^3.2.7":
|
||||
version: 3.2.7
|
||||
resolution: "@internationalized/string@npm:3.2.7"
|
||||
dependencies:
|
||||
"@swc/helpers": "npm:^0.5.0"
|
||||
checksum: 10/38b54817cf125ba88d1136a6bca4fb57c46672d26d21490f838efe928049546800df6d9c8048411696455fc8caacb8ac23c2de2a1b61f2258b1302c1c97cc128
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@isaacs/balanced-match@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "@isaacs/balanced-match@npm:4.0.1"
|
||||
@@ -6376,6 +6385,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-aria/visually-hidden@npm:^3.8.27":
|
||||
version: 3.8.27
|
||||
resolution: "@react-aria/visually-hidden@npm:3.8.27"
|
||||
dependencies:
|
||||
"@react-aria/interactions": "npm:^3.25.5"
|
||||
"@react-aria/utils": "npm:^3.30.1"
|
||||
"@react-types/shared": "npm:^3.32.0"
|
||||
"@swc/helpers": "npm:^0.5.0"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
checksum: 10/6c0f27ecdf7aa7eda4e5f6ab9b88dc1d024cc489b54d96ddc692c673b09dabb8ac2fc9017b4d0160f82384bcae698774e53031e5a701d59197e37e5c852231cd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@react-dnd/asap@npm:^5.0.1":
|
||||
version: 5.0.2
|
||||
resolution: "@react-dnd/asap@npm:5.0.2"
|
||||
@@ -22933,6 +22957,59 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-aria@npm:^3.43.2":
|
||||
version: 3.43.2
|
||||
resolution: "react-aria@npm:3.43.2"
|
||||
dependencies:
|
||||
"@internationalized/string": "npm:^3.2.7"
|
||||
"@react-aria/breadcrumbs": "npm:^3.5.28"
|
||||
"@react-aria/button": "npm:^3.14.1"
|
||||
"@react-aria/calendar": "npm:^3.9.1"
|
||||
"@react-aria/checkbox": "npm:^3.16.1"
|
||||
"@react-aria/color": "npm:^3.1.1"
|
||||
"@react-aria/combobox": "npm:^3.13.2"
|
||||
"@react-aria/datepicker": "npm:^3.15.1"
|
||||
"@react-aria/dialog": "npm:^3.5.30"
|
||||
"@react-aria/disclosure": "npm:^3.0.8"
|
||||
"@react-aria/dnd": "npm:^3.11.2"
|
||||
"@react-aria/focus": "npm:^3.21.1"
|
||||
"@react-aria/gridlist": "npm:^3.14.0"
|
||||
"@react-aria/i18n": "npm:^3.12.12"
|
||||
"@react-aria/interactions": "npm:^3.25.5"
|
||||
"@react-aria/label": "npm:^3.7.21"
|
||||
"@react-aria/landmark": "npm:^3.0.6"
|
||||
"@react-aria/link": "npm:^3.8.5"
|
||||
"@react-aria/listbox": "npm:^3.14.8"
|
||||
"@react-aria/menu": "npm:^3.19.2"
|
||||
"@react-aria/meter": "npm:^3.4.26"
|
||||
"@react-aria/numberfield": "npm:^3.12.1"
|
||||
"@react-aria/overlays": "npm:^3.29.1"
|
||||
"@react-aria/progress": "npm:^3.4.26"
|
||||
"@react-aria/radio": "npm:^3.12.1"
|
||||
"@react-aria/searchfield": "npm:^3.8.8"
|
||||
"@react-aria/select": "npm:^3.16.2"
|
||||
"@react-aria/selection": "npm:^3.25.1"
|
||||
"@react-aria/separator": "npm:^3.4.12"
|
||||
"@react-aria/slider": "npm:^3.8.1"
|
||||
"@react-aria/ssr": "npm:^3.9.10"
|
||||
"@react-aria/switch": "npm:^3.7.7"
|
||||
"@react-aria/table": "npm:^3.17.7"
|
||||
"@react-aria/tabs": "npm:^3.10.7"
|
||||
"@react-aria/tag": "npm:^3.7.1"
|
||||
"@react-aria/textfield": "npm:^3.18.1"
|
||||
"@react-aria/toast": "npm:^3.0.7"
|
||||
"@react-aria/tooltip": "npm:^3.8.7"
|
||||
"@react-aria/tree": "npm:^3.1.3"
|
||||
"@react-aria/utils": "npm:^3.30.1"
|
||||
"@react-aria/visually-hidden": "npm:^3.8.27"
|
||||
"@react-types/shared": "npm:^3.32.0"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
react-dom: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
checksum: 10/f8981d63202df94faebe3b548cb2aeabf0a9043ced7dfd90820a09a4b644ac2b1b894b78f88cc5910679f8cb2c42b40a314bf5c5cf359990b695e3043e939d16
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-dnd-html5-backend@npm:^16.0.1":
|
||||
version: 16.0.1
|
||||
resolution: "react-dnd-html5-backend@npm:16.0.1"
|
||||
@@ -23378,6 +23455,42 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-stately@npm:^3.41.0":
|
||||
version: 3.41.0
|
||||
resolution: "react-stately@npm:3.41.0"
|
||||
dependencies:
|
||||
"@react-stately/calendar": "npm:^3.8.4"
|
||||
"@react-stately/checkbox": "npm:^3.7.1"
|
||||
"@react-stately/collections": "npm:^3.12.7"
|
||||
"@react-stately/color": "npm:^3.9.1"
|
||||
"@react-stately/combobox": "npm:^3.11.1"
|
||||
"@react-stately/data": "npm:^3.14.0"
|
||||
"@react-stately/datepicker": "npm:^3.15.1"
|
||||
"@react-stately/disclosure": "npm:^3.0.7"
|
||||
"@react-stately/dnd": "npm:^3.7.0"
|
||||
"@react-stately/form": "npm:^3.2.1"
|
||||
"@react-stately/list": "npm:^3.13.0"
|
||||
"@react-stately/menu": "npm:^3.9.7"
|
||||
"@react-stately/numberfield": "npm:^3.10.1"
|
||||
"@react-stately/overlays": "npm:^3.6.19"
|
||||
"@react-stately/radio": "npm:^3.11.1"
|
||||
"@react-stately/searchfield": "npm:^3.5.15"
|
||||
"@react-stately/select": "npm:^3.7.1"
|
||||
"@react-stately/selection": "npm:^3.20.5"
|
||||
"@react-stately/slider": "npm:^3.7.1"
|
||||
"@react-stately/table": "npm:^3.15.0"
|
||||
"@react-stately/tabs": "npm:^3.8.5"
|
||||
"@react-stately/toast": "npm:^3.1.2"
|
||||
"@react-stately/toggle": "npm:^3.9.1"
|
||||
"@react-stately/tooltip": "npm:^3.5.7"
|
||||
"@react-stately/tree": "npm:^3.9.2"
|
||||
"@react-types/shared": "npm:^3.32.0"
|
||||
peerDependencies:
|
||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1
|
||||
checksum: 10/0ac4703c6a7a71e737894138fdfdb06412af76855ffeba83491897e311a0a44880366a6aa23c19f33ba7e93e233fd05d250f447cc1983595d2fee3e6362e5b27
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-style-singleton@npm:^2.2.2, react-style-singleton@npm:^2.2.3":
|
||||
version: 2.2.3
|
||||
resolution: "react-style-singleton@npm:2.2.3"
|
||||
|
||||
Reference in New Issue
Block a user