Move to motion to animate toast
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
committed by
Patrik Oldsberg
parent
2aec9470cc
commit
6c18cb30b2
@@ -51,7 +51,9 @@
|
||||
"@remixicon/react": "^4.6.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"clsx": "^2.1.1",
|
||||
"react-aria-components": "^1.14.0"
|
||||
"motion": "^12.0.0",
|
||||
"react-aria-components": "^1.14.0",
|
||||
"react-stately": "^3.35.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
|
||||
@@ -66,19 +66,10 @@
|
||||
/* Stacking */
|
||||
z-index: calc(1000 - var(--toast-index));
|
||||
transform-origin: bottom center;
|
||||
transform: translateY(calc(var(--toast-index) * var(--toast-peek) * -1))
|
||||
scale(var(--toast-scale));
|
||||
will-change: transform;
|
||||
|
||||
/* Shadow + thin line separator */
|
||||
/* Shadow */
|
||||
box-shadow: 0 4px 12px -2px rgba(0 0 0 / 0.4);
|
||||
|
||||
/* Animation */
|
||||
transition: transform 3s cubic-bezier(0.22, 1, 0.36, 1), box-shadow 3s ease;
|
||||
|
||||
/* View Transitions - name is set dynamically via inline style, class groups them */
|
||||
view-transition-class: toast;
|
||||
|
||||
/* Focus ring */
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--bui-border-focus);
|
||||
@@ -90,13 +81,6 @@
|
||||
.bui-ToastRegion:hover .bui-Toast,
|
||||
.bui-ToastRegion:focus-within .bui-Toast,
|
||||
.bui-ToastRegion[data-hover-locked] .bui-Toast {
|
||||
transform: translateY(
|
||||
calc(
|
||||
(var(--toast-index) * -100%) -
|
||||
(var(--toast-index) * var(--bui-space-2))
|
||||
)
|
||||
)
|
||||
scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
@@ -213,53 +197,4 @@
|
||||
background-color: var(--bui-bg-neutral-on-surface-1-pressed);
|
||||
}
|
||||
}
|
||||
|
||||
/* Starting state - toast entering (fallback for non-view-transition browsers) */
|
||||
.bui-Toast[data-starting-style] {
|
||||
transform: translateY(calc(100% + 2rem)) scale(1) !important;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* View Transition animations - must be outside @layer for browser support */
|
||||
@keyframes bui-toast-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(100px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes bui-toast-exit {
|
||||
from {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
to {
|
||||
opacity: 0;
|
||||
transform: translateY(100px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Apply exit animation only to the closing toast */
|
||||
::view-transition-old(toast-exit) {
|
||||
animation: bui-toast-exit 3s ease-out forwards;
|
||||
}
|
||||
|
||||
::view-transition-new(toast-exit) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
/* Reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.bui-Toast {
|
||||
transition: none;
|
||||
}
|
||||
|
||||
::view-transition-old(toast-exit) {
|
||||
animation: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,20 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
forwardRef,
|
||||
Ref,
|
||||
isValidElement,
|
||||
ReactElement,
|
||||
useContext,
|
||||
useState,
|
||||
useEffect,
|
||||
} from 'react';
|
||||
import {
|
||||
UNSTABLE_Toast as RAToast,
|
||||
UNSTABLE_ToastStateContext,
|
||||
Button as RAButton,
|
||||
} from 'react-aria-components';
|
||||
import { forwardRef, Ref, isValidElement, ReactElement, useRef } from 'react';
|
||||
import { useToast } from '@react-aria/toast';
|
||||
import { useButton } from 'react-aria';
|
||||
import { motion } from 'motion/react';
|
||||
import {
|
||||
RiInformationLine,
|
||||
RiCheckLine,
|
||||
@@ -39,6 +29,10 @@ import type { ToastProps } from './types';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { ToastDefinition } from './definition';
|
||||
|
||||
// Track which toasts are being manually closed (vs auto-timeout)
|
||||
// This allows different exit animations for each case
|
||||
const manuallyClosingToasts = new Set<string>();
|
||||
|
||||
/**
|
||||
* A Toast displays a brief, temporary notification of actions, errors, or other events in an application.
|
||||
*
|
||||
@@ -47,8 +41,6 @@ import { ToastDefinition } from './definition';
|
||||
* It supports multiple status variants (info, success, warning, danger) and can display
|
||||
* a title, description, and optional icon. Toasts can be dismissed manually or automatically.
|
||||
*
|
||||
* This component uses React Aria's unstable Toast API which is currently in alpha.
|
||||
*
|
||||
* @example
|
||||
* Basic usage with queue:
|
||||
* ```tsx
|
||||
@@ -78,24 +70,53 @@ export const Toast = forwardRef(
|
||||
ToastDefinition,
|
||||
props,
|
||||
);
|
||||
const { classes, toast, onClose, status, icon } = ownProps;
|
||||
const {
|
||||
classes,
|
||||
toast,
|
||||
state,
|
||||
index = 0,
|
||||
onClose,
|
||||
status,
|
||||
icon,
|
||||
} = ownProps;
|
||||
|
||||
// Get state from context
|
||||
const state = useContext(UNSTABLE_ToastStateContext);
|
||||
// Use internal ref if none provided
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
const toastRef = (ref as React.RefObject<HTMLDivElement>) || internalRef;
|
||||
|
||||
// Calculate index from state
|
||||
const visibleToasts = state?.visibleToasts || [];
|
||||
const arrayIndex = visibleToasts.findIndex(t => t.key === toast.key);
|
||||
const index = arrayIndex >= 0 ? arrayIndex : 0;
|
||||
// Get ARIA props from useToast hook
|
||||
const { toastProps, titleProps, closeButtonProps } = useToast(
|
||||
{ toast },
|
||||
state,
|
||||
toastRef,
|
||||
);
|
||||
|
||||
// Track starting state for enter animation
|
||||
const [isStarting, setIsStarting] = useState(true);
|
||||
// Extract only ARIA and accessibility props from toastProps to avoid
|
||||
// conflicts with motion.div's event handler types (motion has its own drag API)
|
||||
const ariaProps = {
|
||||
role: toastProps.role,
|
||||
tabIndex: toastProps.tabIndex,
|
||||
'aria-label': toastProps['aria-label'],
|
||||
'aria-labelledby': toastProps['aria-labelledby'],
|
||||
'aria-describedby': toastProps['aria-describedby'],
|
||||
'aria-posinset': toastProps['aria-posinset'],
|
||||
'aria-setsize': toastProps['aria-setsize'],
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// Remove starting state after brief delay to trigger animation
|
||||
const timer = setTimeout(() => setIsStarting(false), 50);
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
// Close button ref and props
|
||||
const closeButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const { buttonProps } = useButton(
|
||||
{
|
||||
...closeButtonProps,
|
||||
onPress: () => {
|
||||
// Mark this toast as manually closed for exit animation
|
||||
manuallyClosingToasts.add(toast.key);
|
||||
onClose?.();
|
||||
state.close(toast.key);
|
||||
},
|
||||
},
|
||||
closeButtonRef,
|
||||
);
|
||||
|
||||
// Get content from toast
|
||||
const content = toast.content;
|
||||
@@ -135,42 +156,71 @@ export const Toast = forwardRef(
|
||||
|
||||
const statusIcon = getStatusIcon();
|
||||
|
||||
// Calculate stacking values based on index
|
||||
// Each toast behind scales down 5% and moves up 12px
|
||||
const stackScale = Math.max(0, 1 - index * 0.05);
|
||||
const stackY = -index * 12;
|
||||
const stackZIndex = 1000 - index;
|
||||
|
||||
// Check if this toast is being manually closed
|
||||
const isManualClose = manuallyClosingToasts.has(toast.key);
|
||||
|
||||
// Different exit animations for manual close vs auto-timeout
|
||||
// Manual close: slide down from front, stay on top
|
||||
// Auto-timeout: fade out in place, stay in stack position
|
||||
const exitAnimation = isManualClose
|
||||
? { opacity: 0, y: 100, scale: 1, zIndex: 2000 }
|
||||
: { opacity: 0, y: stackY + 50, scale: stackScale, zIndex: stackZIndex };
|
||||
|
||||
return (
|
||||
<RAToast
|
||||
toast={toast}
|
||||
ref={ref}
|
||||
<motion.div
|
||||
{...ariaProps}
|
||||
ref={toastRef}
|
||||
className={classes.root}
|
||||
style={
|
||||
{
|
||||
'--toast-index': index,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
layout
|
||||
initial={{ opacity: 0, y: 100, scale: 1 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: stackY,
|
||||
scale: stackScale,
|
||||
zIndex: stackZIndex,
|
||||
}}
|
||||
exit={exitAnimation}
|
||||
onAnimationComplete={definition => {
|
||||
// Clean up the manual close tracking after exit animation
|
||||
if (definition === 'exit') {
|
||||
manuallyClosingToasts.delete(toast.key);
|
||||
}
|
||||
}}
|
||||
transition={{ type: 'tween', duration: 2, ease: 'easeOut' }}
|
||||
{...dataAttributes}
|
||||
data-toast-key={toast.key}
|
||||
data-status={finalStatus}
|
||||
data-starting-style={isStarting ? '' : undefined}
|
||||
{...restProps}
|
||||
>
|
||||
<div className={classes.content}>
|
||||
{statusIcon && <div className={classes.icon}>{statusIcon}</div>}
|
||||
<div>
|
||||
<div className={classes.title}>{content.title}</div>
|
||||
<div {...titleProps} className={classes.title}>
|
||||
{content.title}
|
||||
</div>
|
||||
{content.description && (
|
||||
<div className={classes.description}>{content.description}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<RAButton
|
||||
slot="close"
|
||||
<button
|
||||
{...buttonProps}
|
||||
ref={closeButtonRef}
|
||||
className={classes.closeButton}
|
||||
onPress={() => {
|
||||
onClose?.();
|
||||
state?.close(toast.key);
|
||||
}}
|
||||
>
|
||||
<RiCloseLine aria-hidden="true" />
|
||||
</RAButton>
|
||||
</RAToast>
|
||||
</button>
|
||||
</motion.div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -14,18 +14,15 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { UNSTABLE_ToastQueue as RAToastQueue } from 'react-aria-components';
|
||||
import { flushSync } from 'react-dom';
|
||||
import { ToastQueue } from 'react-stately';
|
||||
import type { ToastContent } from './types';
|
||||
|
||||
/**
|
||||
* Global toast queue for displaying toast notifications throughout the application.
|
||||
*
|
||||
* @remarks
|
||||
* This uses React Aria's unstable Toast API which is currently in alpha.
|
||||
* The API may change in future versions.
|
||||
*
|
||||
* Uses the View Transitions API for smooth enter/exit animations when supported.
|
||||
* This uses React Stately's ToastQueue for state management with motion/react
|
||||
* for smooth enter/exit animations.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
@@ -45,41 +42,6 @@ import type { ToastContent } from './types';
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
// Track which toast element is being closed for view transition
|
||||
let closingToastElement: HTMLElement | null = null;
|
||||
|
||||
export const toastQueue = new RAToastQueue<ToastContent>({
|
||||
export const toastQueue = new ToastQueue<ToastContent>({
|
||||
maxVisibleToasts: 5,
|
||||
// Wrap state updates in a CSS view transition for smooth animations
|
||||
wrapUpdate(fn) {
|
||||
if (closingToastElement && 'startViewTransition' in document) {
|
||||
// Set view-transition-name on the element BEFORE capturing old state
|
||||
closingToastElement.style.viewTransitionName = 'toast-exit';
|
||||
const element = closingToastElement;
|
||||
closingToastElement = null;
|
||||
|
||||
(
|
||||
document as Document & {
|
||||
startViewTransition: (cb: () => void) => void;
|
||||
}
|
||||
).startViewTransition(() => {
|
||||
flushSync(fn);
|
||||
// Clean up (element may be removed, but just in case)
|
||||
element.style.viewTransitionName = '';
|
||||
});
|
||||
} else {
|
||||
fn();
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Override close to capture the toast element before transition
|
||||
const originalClose = toastQueue.close.bind(toastQueue);
|
||||
toastQueue.close = (key: string) => {
|
||||
// Find the toast element by our custom data attribute
|
||||
const toastElement = document.querySelector(
|
||||
`[data-toast-key="${key}"]`,
|
||||
) as HTMLElement | null;
|
||||
closingToastElement = toastElement;
|
||||
originalClose(key);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
*/
|
||||
|
||||
import { forwardRef, Ref, useState, useRef } from 'react';
|
||||
import { UNSTABLE_ToastRegion as RAToastRegion } from 'react-aria-components';
|
||||
import { useToastRegion } from '@react-aria/toast';
|
||||
import { useToastQueue } from 'react-stately';
|
||||
import { AnimatePresence } from 'motion/react';
|
||||
import type { ToastRegionProps } from './types';
|
||||
import { useDefinition } from '../../hooks/useDefinition';
|
||||
import { useInvertedThemeMode } from '../../hooks/useInvertedThemeMode';
|
||||
@@ -32,8 +34,6 @@ import { Toast } from './Toast';
|
||||
* Toast regions are ARIA landmark regions that can be navigated using F6 (forward) and
|
||||
* Shift+F6 (backward) for keyboard accessibility.
|
||||
*
|
||||
* This component uses React Aria's unstable Toast API which is currently in alpha.
|
||||
*
|
||||
* @example
|
||||
* Basic setup in app root:
|
||||
* ```tsx
|
||||
@@ -59,6 +59,16 @@ export const ToastRegion = forwardRef(
|
||||
);
|
||||
const { classes, queue, className } = ownProps;
|
||||
|
||||
// Subscribe to the toast queue state
|
||||
const state = useToastQueue(queue);
|
||||
|
||||
// Use internal ref if none provided
|
||||
const internalRef = useRef<HTMLDivElement>(null);
|
||||
const regionRef = (ref as React.RefObject<HTMLDivElement>) || internalRef;
|
||||
|
||||
// Get ARIA props for the toast region
|
||||
const { regionProps } = useToastRegion({}, state, regionRef);
|
||||
|
||||
// Lock hover state after close to prevent stack collapse during DOM updates
|
||||
const [isHoverLocked, setIsHoverLocked] = useState(false);
|
||||
const unlockTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
@@ -82,18 +92,27 @@ export const ToastRegion = forwardRef(
|
||||
};
|
||||
|
||||
return (
|
||||
<RAToastRegion
|
||||
ref={ref}
|
||||
queue={queue}
|
||||
<div
|
||||
{...regionProps}
|
||||
ref={regionRef}
|
||||
className={className || classes.region}
|
||||
aria-label="Notifications"
|
||||
data-theme-mode={invertedThemeMode}
|
||||
data-hover-locked={isHoverLocked ? '' : undefined}
|
||||
{...dataAttributes}
|
||||
{...restProps}
|
||||
>
|
||||
{({ toast }) => <Toast toast={toast} onClose={handleClose} />}
|
||||
</RAToastRegion>
|
||||
<AnimatePresence>
|
||||
{state.visibleToasts.map((toast, index) => (
|
||||
<Toast
|
||||
key={toast.key}
|
||||
toast={toast}
|
||||
state={state}
|
||||
index={index}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -35,6 +35,8 @@ export const ToastDefinition = defineComponent<ToastOwnProps>()({
|
||||
surface: 'container',
|
||||
propDefs: {
|
||||
toast: {},
|
||||
state: {},
|
||||
index: {},
|
||||
onClose: {},
|
||||
status: { dataAttribute: true },
|
||||
icon: {},
|
||||
|
||||
@@ -15,8 +15,7 @@
|
||||
*/
|
||||
|
||||
import type { ReactElement, ReactNode } from 'react';
|
||||
import type { UNSTABLE_ToastQueue as RAToastQueue } from 'react-aria-components';
|
||||
import type { QueuedToast } from 'react-stately';
|
||||
import type { ToastQueue, ToastState, QueuedToast } from 'react-stately';
|
||||
import type { Responsive, ContainerSurfaceProps } from '../../types';
|
||||
|
||||
/**
|
||||
@@ -41,6 +40,10 @@ export interface ToastContent {
|
||||
export type ToastOwnProps = ContainerSurfaceProps & {
|
||||
/** Toast object from the queue */
|
||||
toast: QueuedToast<ToastContent>;
|
||||
/** Toast state from useToastQueue */
|
||||
state: ToastState<ToastContent>;
|
||||
/** Index of the toast in the visible toasts array */
|
||||
index?: number;
|
||||
/** Callback when toast is closed */
|
||||
onClose?: () => void;
|
||||
/** Override status from content */
|
||||
@@ -61,7 +64,7 @@ export interface ToastProps extends ToastOwnProps {}
|
||||
*/
|
||||
export type ToastRegionOwnProps = {
|
||||
/** Toast queue instance */
|
||||
queue: RAToastQueue<ToastContent>;
|
||||
queue: ToastQueue<ToastContent>;
|
||||
/** Custom class name */
|
||||
className?: string;
|
||||
};
|
||||
|
||||
@@ -8123,10 +8123,12 @@ __metadata:
|
||||
eslint-plugin-storybook: "npm:^10.3.0-alpha.1"
|
||||
glob: "npm:^11.0.1"
|
||||
globals: "npm:^15.11.0"
|
||||
motion: "npm:^12.0.0"
|
||||
react: "npm:^18.0.2"
|
||||
react-aria-components: "npm:^1.14.0"
|
||||
react-dom: "npm:^18.0.2"
|
||||
react-router-dom: "npm:^6.30.2"
|
||||
react-stately: "npm:^3.35.0"
|
||||
storybook: "npm:^10.3.0-alpha.1"
|
||||
peerDependencies:
|
||||
"@types/react": ^17.0.0 || ^18.0.0
|
||||
@@ -25778,7 +25780,18 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"axios@npm:^1.0.0, axios@npm:^1.11.0, axios@npm:^1.12.0, axios@npm:^1.12.2, axios@npm:^1.13.0, axios@npm:^1.7.4":
|
||||
"axios@npm:^1.0.0, axios@npm:^1.11.0, axios@npm:^1.12.2, axios@npm:^1.13.0, axios@npm:^1.7.4":
|
||||
version: 1.13.4
|
||||
resolution: "axios@npm:1.13.4"
|
||||
dependencies:
|
||||
follow-redirects: "npm:^1.15.6"
|
||||
form-data: "npm:^4.0.4"
|
||||
proxy-from-env: "npm:^1.1.0"
|
||||
checksum: 10/54b7ef71c64837f9d52475832337f520cf6fa85c94612e03a3a2aad7082804a2544741267122696662147e90e6d2746601346984cf531ae715ecdb56d586a04c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"axios@npm:^1.12.0":
|
||||
version: 1.13.5
|
||||
resolution: "axios@npm:1.13.5"
|
||||
dependencies:
|
||||
@@ -32468,6 +32481,28 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"framer-motion@npm:^12.31.0":
|
||||
version: 12.31.0
|
||||
resolution: "framer-motion@npm:12.31.0"
|
||||
dependencies:
|
||||
motion-dom: "npm:^12.30.1"
|
||||
motion-utils: "npm:^12.29.2"
|
||||
tslib: "npm:^2.4.0"
|
||||
peerDependencies:
|
||||
"@emotion/is-prop-valid": "*"
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@emotion/is-prop-valid":
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: 10/8cd76953b5e4e81e69b7bbec699cd5c913df87897148cd0f9fe85fa2f1c4e2768fbaeb6e40be9cf6d3f4b17a5a8024351d7e0ba93a9cdf9d8358c5031c5cdecd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"framer-motion@npm:^6.5.1":
|
||||
version: 6.5.1
|
||||
resolution: "framer-motion@npm:6.5.1"
|
||||
@@ -39881,6 +39916,43 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"motion-dom@npm:^12.30.1":
|
||||
version: 12.30.1
|
||||
resolution: "motion-dom@npm:12.30.1"
|
||||
dependencies:
|
||||
motion-utils: "npm:^12.29.2"
|
||||
checksum: 10/22af7e074b388485b8f383bc9a8a3d03dec51f2e7112a3b50b9fde7076531ec0fccbeb61c669a439eb0a535992b121a48e612901b79f139538697755c291400e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"motion-utils@npm:^12.29.2":
|
||||
version: 12.29.2
|
||||
resolution: "motion-utils@npm:12.29.2"
|
||||
checksum: 10/ae5f9be58c07939af72334894ed1a18653d724946182a718dc3d11268ef26e63804c3f16dee5a6110596d4406b539c4513822b74f86adebef9488601c34b18b7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"motion@npm:^12.0.0":
|
||||
version: 12.31.0
|
||||
resolution: "motion@npm:12.31.0"
|
||||
dependencies:
|
||||
framer-motion: "npm:^12.31.0"
|
||||
tslib: "npm:^2.4.0"
|
||||
peerDependencies:
|
||||
"@emotion/is-prop-valid": "*"
|
||||
react: ^18.0.0 || ^19.0.0
|
||||
react-dom: ^18.0.0 || ^19.0.0
|
||||
peerDependenciesMeta:
|
||||
"@emotion/is-prop-valid":
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
react-dom:
|
||||
optional: true
|
||||
checksum: 10/abb2253b2e679f3d153e472f63e29fa0f3d9d4dd6bb2f2b0c9026897dd069a2371ddc2374bf70debdd480351ad5311e830ebf98e5ea7a44588e44099381e950b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mri@npm:1.1.4":
|
||||
version: 1.1.4
|
||||
resolution: "mri@npm:1.1.4"
|
||||
@@ -44699,7 +44771,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-stately@npm:^3.43.0":
|
||||
"react-stately@npm:^3.35.0, react-stately@npm:^3.43.0":
|
||||
version: 3.43.0
|
||||
resolution: "react-stately@npm:3.43.0"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user