Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion site/src/components/Loader/Loader.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Interpolation, Theme } from "@emotion/react";
import { Spinner } from "components/Spinner/Spinner";
import { Spinner } from "components/deprecated/Spinner/Spinner";
import type { FC, HTMLAttributes } from "react";

interface LoaderProps extends HTMLAttributes<HTMLDivElement> {
Expand Down
20 changes: 20 additions & 0 deletions site/src/components/Spinner/Spinner.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type { Meta, StoryObj } from "@storybook/react";
import { PlusIcon } from "lucide-react";
import { Spinner } from "./Spinner";

const meta: Meta<typeof Spinner> = {
title: "components/Spinner",
component: Spinner,
args: {
children: <PlusIcon className="size-icon-lg" />,
},
};

export default meta;
type Story = StoryObj<typeof Spinner>;

export const Idle: Story = {};

export const Loading: Story = {
args: { loading: true },
};
93 changes: 74 additions & 19 deletions site/src/components/Spinner/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,77 @@
import CircularProgress, {
type CircularProgressProps,
} from "@mui/material/CircularProgress";
import isChromatic from "chromatic/isChromatic";
import type { FC } from "react";

/**
* Spinner component used to indicate loading states. This component abstracts
* the MUI CircularProgress to provide better control over its rendering,
* especially in snapshot tests with Chromatic.
* This component was inspired by
* https://www.radix-ui.com/themes/docs/components/spinner and developed using
* https://v0.dev/ help.
*/
export const Spinner: FC<CircularProgressProps> = (props) => {
/**
* During Chromatic snapshots, we render the spinner as determinate to make it
* static without animations, using a deterministic value (75%).
*/
if (isChromatic()) {
props.variant = "determinate";
props.value = 75;

import { type VariantProps, cva } from "class-variance-authority";
import type { ReactNode } from "react";
import { cn } from "utils/cn";
import isChromatic from "chromatic/isChromatic";

const leaves = 8;

const spinnerVariants = cva("", {
variants: {
size: {
lg: "size-icon-lg",
sm: "size-icon-sm",
},
},
defaultVariants: {
size: "lg",
},
});

type SpinnerProps = React.SVGProps<SVGSVGElement> &
VariantProps<typeof spinnerVariants> & {
children?: ReactNode;
loading?: boolean;
};

export function Spinner({
className,
size,
loading,
children,
...props
}: SpinnerProps) {
if (!loading) {
return children;
}
return <CircularProgress {...props} />;
};

return (
<svg
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
className={cn(spinnerVariants({ size, className }))}
{...props}
>
<title>Loading spinner</title>
{[...Array(leaves)].map((_, i) => {
const rotation = i * (360 / leaves);

return (
<rect
key={i}
x="10.9"
y="2"
width="2"
height="5.5"
rx="1"
// 0.8 = leaves * 0.1
className={
isChromatic() ? "" : "animate-[loading_0.8s_ease-in-out_infinite]"
}
style={{
transform: `rotate(${rotation}deg)`,
transformOrigin: "center",
animationDelay: `${-i * 0.1}s`,
}}
/>
);
})}
</svg>
);
}
24 changes: 24 additions & 0 deletions site/src/components/deprecated/Spinner/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import CircularProgress, {
type CircularProgressProps,
} from "@mui/material/CircularProgress";
import isChromatic from "chromatic/isChromatic";
import type { FC } from "react";

/**
* Spinner component used to indicate loading states. This component abstracts
* the MUI CircularProgress to provide better control over its rendering,
* especially in snapshot tests with Chromatic.
*
* @deprecated prefer `components.Spinner`
*/
export const Spinner: FC<CircularProgressProps> = (props) => {
/**
* During Chromatic snapshots, we render the spinner as determinate to make it
* static without animations, using a deterministic value (75%).
*/
if (isChromatic()) {
props.variant = "determinate";
props.value = 75;
}
return <CircularProgress {...props} />;
};
7 changes: 7 additions & 0 deletions site/tailwind.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,13 @@ module.exports = {
5: "hsl(var(--chart-5))",
},
},
keyframes: {
loading: {
"0%": { opacity: 0.85 },
"50%": { opacity: 0.25 },
"100%": { opacity: 0.25 },
},
},
},
},
plugins: [require("tailwindcss-animate")],
Expand Down
Loading