Skip to content
Merged
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
3 changes: 1 addition & 2 deletions app/components/RoundedSector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,9 @@
*
* Copyright Oxide Computer Company
*/
import { useReducedMotion } from 'motion/react'
import { useEffect, useMemo, useState } from 'react'

import { useReducedMotion } from '~/hooks/use-reduce-motion'

export function RoundedSector({
angle,
size,
Expand Down
48 changes: 21 additions & 27 deletions app/components/ToastStack.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,39 @@
*
* Copyright Oxide Computer Company
*/
import { animated, useTransition } from '@react-spring/web'
import { AnimatePresence } from 'motion/react'
import * as m from 'motion/react-m'

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"
data-testid="Toasts"
>
{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>
)
}
38 changes: 0 additions & 38 deletions app/hooks/use-reduce-motion.tsx

This file was deleted.

19 changes: 12 additions & 7 deletions app/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
* Copyright Oxide Computer Company
*/
import { QueryClientProvider } from '@tanstack/react-query'
import { LazyMotion, MotionConfig } from 'motion/react'
// import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
Expand All @@ -16,7 +17,6 @@ import { queryClient } from '@oxide/api'

import { ConfirmActionModal } from './components/ConfirmActionModal'
import { ErrorBoundary } from './components/ErrorBoundary'
import { ReduceMotion } from './hooks/use-reduce-motion'
// stripped out by rollup in production
import { startMockAPI } from './msw-mock-api'
import { routes } from './routes'
Expand All @@ -33,6 +33,8 @@ if (process.env.SHA) {
)
}

const loadFeatures = () => import('./util/motion-features').then((res) => res.domAnimation)

const root = createRoot(document.getElementById('root')!)

function render() {
Expand All @@ -46,12 +48,15 @@ function render() {
root.render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ErrorBoundary>
<ConfirmActionModal />
<SkipLink id="skip-nav" />
<ReduceMotion />
<RouterProvider router={router} />
</ErrorBoundary>
<LazyMotion strict features={loadFeatures}>
<MotionConfig reducedMotion="user">
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this rules

<ErrorBoundary>
<ConfirmActionModal />
<SkipLink id="skip-nav" />
<RouterProvider router={router} />
</ErrorBoundary>
</MotionConfig>
</LazyMotion>
{/* <ReactQueryDevtools initialIsOpen={false} /> */}
</QueryClientProvider>
</StrictMode>
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 * as m from 'motion/react-m'
import { forwardRef, type MouseEventHandler, type ReactNode } from 'react'

import { Spinner } from '~/ui/lib/Spinner'
Expand Down Expand Up @@ -90,9 +91,14 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
with={<Tooltip content={disabledReason} ref={ref} placement="bottom" />}
>
<button
className={cn(buttonStyle({ size, variant }), className, {
'visually-disabled': isDisabled,
})}
className={cn(
buttonStyle({ size, variant }),
className,
{
'visually-disabled': isDisabled,
},
'overflow-hidden'
)}
ref={ref}
/* eslint-disable-next-line react/button-has-type */
type={type}
Expand All @@ -101,10 +107,26 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
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
43 changes: 26 additions & 17 deletions app/ui/lib/CopyToClipboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* Copyright Oxide Computer Company
*/

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

import { Copy12Icon, Success12Icon } from '@oxide/design-system/icons/react'
Expand All @@ -20,6 +21,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 +41,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 +64,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>
)
}
7 changes: 6 additions & 1 deletion app/ui/lib/DialogOverlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,17 @@
* Copyright Oxide Computer Company
*/

import * as m from 'motion/react-m'
import { forwardRef } from 'react'

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