Skip to content
Open
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
59 changes: 59 additions & 0 deletions src/components/atoms/spinner/Spinner.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type {Meta, StoryObj} from "@storybook/react-vite";
import {Spinner} from "./Spinner";

const meta = {
title: "components/atoms/Spinner",
component: Spinner,
} satisfies Meta<typeof Spinner>;

export default meta;

type Story = StoryObj<typeof meta>;

/**
* Default spinner
*/
export const Default: Story = {
args: {
size: "md",
color: "#2563eb",
},
};

/**
* Spinner sizes
*/
export const Sizes: Story = {
render: () => (
<div style={{display: "flex", gap: 16, alignItems: "center"}}>
<Spinner size="sm" color="#2563eb"/>
<Spinner size="md" color="#2563eb"/>
<Spinner size="lg" color="#2563eb"/>
</div>
),
};

/**
* Spinner with different colors
*/
export const Colors: Story = {
render: () => (
<div style={{display: "flex", gap: 16, alignItems: "center"}}>
<Spinner color="#ef4444"/> {/* red */}
<Spinner color="#22c55e"/> {/* green */}
<Spinner color="#f59e0b"/> {/* amber */}
<Spinner color="#0ea5e9"/> {/* sky */}
</div>
),
};

/**
* Spinner inheriting parent color (currentColor)
*/
export const InheritColor: Story = {
render: () => (
<div style={{color: "#7c3aed"}}>
<Spinner/>
</div>
),
};
76 changes: 76 additions & 0 deletions src/components/atoms/spinner/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import type {SpinnerProps, SpinnerSize} from "./SpinnerProps";

const sizeMap: Record<SpinnerSize, number> = {
sm: 16,
md: 24,
lg: 32,
};

/**
* Renders a visually appealing and accessible SVG spinner component.
* This component is typically used to indicate a loading state or ongoing process.
*
* The spinner's size and color can be customized via props.
* It includes accessibility attributes (`role="status"`, `aria-label="Loading"`)
* to ensure screen readers can convey its purpose.
*
* @component
* @param {Readonly<SpinnerProps>} props - The properties for the Spinner component.
* @param {("sm" | "md" | "lg")} [props.size="md"] - The predefined size of the spinner.
* - "sm": Small (16px)
* - "md": Medium (24px)
* - "lg": Large (32px)
* @param {string} [props.color="currentColor"] - The color of the spinner's stroke.
* Can be any valid CSS color string (e.g., "red", "#FF0000", "var(--primary-color)").
* Defaults to "currentColor", which inherits the text color of its parent.
* @returns {JSX.Element} A React element representing the SVG spinner.
*
* @example
* // Basic usage with default size and color
* <Spinner />
* @example
* // Large spinner with a specific color
* <Spinner size="lg" color="#007bff" />
*/
export function Spinner(props: Readonly<SpinnerProps>) {
const {
size = "md",
color = "currentColor",
className,
} = props;

const pixelSize = sizeMap[size];
const strokeWidth = Math.max(2, Math.floor(pixelSize / 6));

return (
<svg
width={pixelSize}
height={pixelSize}
viewBox="0 0 50 50"
className={className}
focusable="false"
>
Comment on lines +46 to +52
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add the advertised accessibility attributes to the SVG.
The docs state role="status" and aria-label="Loading", but the SVG omits them, weakening accessibility and contradicting the comment.

🔧 Proposed fix
         <svg
             width={pixelSize}
             height={pixelSize}
             viewBox="0 0 50 50"
             className={className}
+            role="status"
+            aria-label="Loading"
             focusable="false"
         >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<svg
width={pixelSize}
height={pixelSize}
viewBox="0 0 50 50"
className={className}
focusable="false"
>
<svg
width={pixelSize}
height={pixelSize}
viewBox="0 0 50 50"
className={className}
role="status"
aria-label="Loading"
focusable="false"
>
🤖 Prompt for AI Agents
In `@src/components/atoms/spinner/Spinner.tsx` around lines 46 - 52, The SVG in
the Spinner component is missing the accessibility attributes promised in the
docs; update the <svg> element in Spinner.tsx (inside the Spinner component
where width/height are set using pixelSize and className) to include
role="status" and aria-label="Loading" (keep focusable="false") so the spinner
is announced correctly by assistive technologies.

<title>Loading</title>

<circle
cx="25"
cy="25"
r="20"
fill="none"
stroke={color}
strokeWidth={strokeWidth}
strokeLinecap="round"
strokeDasharray="31.4 31.4"
>
<animateTransform
attributeName="transform"
type="rotate"
from="0 25 25"
to="360 25 25"
dur="0.8s"
repeatCount="indefinite"
/>
</circle>
</svg>
);
}
7 changes: 7 additions & 0 deletions src/components/atoms/spinner/SpinnerProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export type SpinnerSize = "sm" | "md" | "lg";

export interface SpinnerProps {
size?: SpinnerSize;
color?: string;
className?: string;
}