Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 20 additions & 27 deletions app/components/ToastStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,42 +5,35 @@
*
* Copyright Oxide Computer Company
*/
import { animated, useTransition } from '@react-spring/web'
import { AnimatePresence, m } from 'framer-motion'

import { removeToast, useToastStore } from '~/stores/toast'
import { Toast } from '~/ui/lib/Toast'

export function ToastStack() {
const toasts = useToastStore((state) => state.toasts)

const transition = useTransition(toasts, {
keys: (toast) => toast.id,
from: { opacity: 0, y: 10, scale: 95 },
enter: { opacity: 1, y: 0, scale: 100 },
leave: { opacity: 0, y: 10, scale: 95 },
config: { duration: 100 },
})

return (
<div className="pointer-events-auto fixed bottom-4 left-4 z-toast flex flex-col items-end space-y-2">
{transition((style, item) => (
<animated.div
style={{
opacity: style.opacity,
y: style.y,
transform: style.scale.to((val) => `scale(${val}%, ${val}%)`),
}}
>
<Toast
key={item.id}
{...item.options}
onClose={() => {
removeToast(item.id)
item.options.onClose?.()
}}
/>
</animated.div>
))}
<AnimatePresence>
{toasts.map((toast) => (
<m.div
key={toast.id}
initial={{ opacity: 0, y: 20, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 20, scale: 0.95 }}
transition={{ type: 'spring', duration: 0.2, bounce: 0 }}
>
<Toast
{...toast.options}
onClose={() => {
removeToast(toast.id)
toast.options.onClose?.()
}}
/>
</m.div>
))}
</AnimatePresence>
</div>
)
}
1 change: 0 additions & 1 deletion app/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,3 @@ export * from './use-is-overflow'
export * from './use-key'
export * from './use-params'
export * from './use-quick-actions'
export * from './use-reduce-motion'
38 changes: 0 additions & 38 deletions app/hooks/use-reduce-motion.tsx

This file was deleted.

7 changes: 5 additions & 2 deletions app/layouts/RootLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*
* Copyright Oxide Computer Company
*/
import { domAnimation, LazyMotion } from 'framer-motion'
import { useEffect, useRef } from 'react'
import { Outlet, useNavigation } from 'react-router-dom'

Expand All @@ -26,8 +27,10 @@ export function RootLayout() {
<>
<LoadingBar />
{process.env.MSW_BANNER ? <MswBanner /> : null}
<Outlet />
<ToastStack />
<LazyMotion strict features={domAnimation}>
<Outlet />
<ToastStack />
</LazyMotion>
</>
)
}
Expand Down
2 changes: 0 additions & 2 deletions app/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { queryClient } from '@oxide/api'

import { ConfirmActionModal } from './components/ConfirmActionModal'
import { ErrorBoundary } from './components/ErrorBoundary'
import { ReduceMotion } from './hooks'
// stripped out by rollup in production
import { startMockAPI } from './msw-mock-api'
import { routes } from './routes'
Expand Down Expand Up @@ -51,7 +50,6 @@ function render() {
<ErrorBoundary>
<ConfirmActionModal />
<SkipLink id="skip-nav" />
<ReduceMotion />
<RouterProvider router={router} />
</ErrorBoundary>
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
Expand Down
34 changes: 28 additions & 6 deletions app/ui/lib/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Copyright Oxide Computer Company
*/
import cn from 'classnames'
import { m } from 'framer-motion'
import { forwardRef, type MouseEventHandler, type ReactNode } from 'react'

import { Spinner } from '~/ui/lib/Spinner'
Expand Down Expand Up @@ -90,20 +91,41 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
with={<Tooltip content={disabledReason!} ref={ref} />}
>
<button
className={cn(buttonStyle({ size, variant }), className, {
'visually-disabled': isDisabled,
})}
className={cn(
buttonStyle({ size, variant }),
className,
{
'visually-disabled': isDisabled,
},
'overflow-hidden'
)}
ref={ref}
type={type}
onMouseDown={isDisabled ? noop : undefined}
onClick={isDisabled ? noop : onClick}
aria-disabled={isDisabled}
{...rest}
>
{loading && <Spinner className="absolute" variant={variant} />}
<span className={cn('flex items-center', innerClassName, { invisible: loading })}>
{loading && (
<m.span
animate={{ opacity: 1, y: '-50%', x: '-50%' }}
initial={{ opacity: 0, y: 'calc(-50% - 25px)', x: '-50%' }}
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
className="absolute left-1/2 top-1/2"
>
<Spinner variant={variant} />
</m.span>
)}
<m.span
className={cn('flex items-center', innerClassName)}
animate={{
opacity: loading ? 0 : 1,
y: loading ? 25 : 0,
}}
transition={{ type: 'spring', duration: 0.3, bounce: 0 }}
>
{children}
</span>
</m.span>
</button>
</Wrap>
)
Expand Down
42 changes: 25 additions & 17 deletions app/ui/lib/CopyToClipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
* Copyright Oxide Computer Company
*/

import { animated, config, useTransition } from '@react-spring/web'
import cn from 'classnames'
import { AnimatePresence, m } from 'framer-motion'
import { useState } from 'react'

import { Copy12Icon, Success12Icon } from '@oxide/design-system/icons/react'
Expand All @@ -20,6 +20,11 @@ type Props = {
className?: string
}

const variants = {
hidden: { opacity: 0, scale: 0.75 },
visible: { opacity: 1, scale: 1 },
}

export const CopyToClipboard = ({
ariaLabel = 'Click to copy',
text,
Expand All @@ -35,14 +40,14 @@ export const CopyToClipboard = ({
})
}

const transitions = useTransition(hasCopied, {
from: { opacity: 0, transform: 'scale(0.8)' },
enter: { opacity: 1, transform: 'scale(1)' },
leave: { opacity: 0, transform: 'scale(0.8)' },
config: config.stiff,
trail: 100,
initial: null,
})
const animateProps = {
className: 'absolute inset-0 flex items-center justify-center',
variants,
initial: 'hidden',
animate: 'visible',
exit: 'hidden',
transition: { type: 'spring', duration: 0.2, bounce: 0 },
}

return (
<button
Expand All @@ -58,14 +63,17 @@ export const CopyToClipboard = ({
type="button"
aria-label={hasCopied ? 'Copied' : ariaLabel}
>
{transitions((styles, item) => (
<animated.div
style={styles}
className="absolute inset-0 flex items-center justify-center"
>
{item ? <Success12Icon /> : <Copy12Icon />}
</animated.div>
))}
<AnimatePresence mode="wait" initial={false}>
{hasCopied ? (
<m.span key="checkmark" {...animateProps}>
<Success12Icon />
</m.span>
) : (
<m.span key="copy" {...animateProps}>
<Copy12Icon />
</m.span>
)}
</AnimatePresence>
</button>
)
}
11 changes: 10 additions & 1 deletion app/ui/lib/DialogOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,17 @@
* Copyright Oxide Computer Company
*/

import { m } from 'framer-motion'
import { forwardRef } from 'react'

export const DialogOverlay = forwardRef<HTMLDivElement>((_, ref) => (
<div ref={ref} aria-hidden className="fixed inset-0 z-10 overflow-auto bg-scrim" />
<m.div
ref={ref}
aria-hidden
className="fixed inset-0 z-10 overflow-auto bg-scrim"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.15, ease: 'easeOut' }}
/>
))
Loading