style: format all files with prettier

This commit is contained in:
Seth Hobson
2026-01-19 17:07:03 -05:00
parent 8d37048deb
commit 56848874a2
355 changed files with 15215 additions and 10241 deletions

View File

@@ -10,20 +10,22 @@ Compound components share implicit state through React context, allowing flexibl
```tsx
// Compound component pattern
import * as React from 'react';
import * as React from "react";
interface AccordionContextValue {
openItems: Set<string>;
toggle: (id: string) => void;
type: 'single' | 'multiple';
type: "single" | "multiple";
}
const AccordionContext = React.createContext<AccordionContextValue | null>(null);
const AccordionContext = React.createContext<AccordionContextValue | null>(
null,
);
function useAccordionContext() {
const context = React.useContext(AccordionContext);
if (!context) {
throw new Error('Accordion components must be used within an Accordion');
throw new Error("Accordion components must be used within an Accordion");
}
return context;
}
@@ -31,13 +33,17 @@ function useAccordionContext() {
// Root component
interface AccordionProps {
children: React.ReactNode;
type?: 'single' | 'multiple';
type?: "single" | "multiple";
defaultOpen?: string[];
}
function Accordion({ children, type = 'single', defaultOpen = [] }: AccordionProps) {
function Accordion({
children,
type = "single",
defaultOpen = [],
}: AccordionProps) {
const [openItems, setOpenItems] = React.useState<Set<string>>(
new Set(defaultOpen)
new Set(defaultOpen),
);
const toggle = React.useCallback(
@@ -47,7 +53,7 @@ function Accordion({ children, type = 'single', defaultOpen = [] }: AccordionPro
if (next.has(id)) {
next.delete(id);
} else {
if (type === 'single') {
if (type === "single") {
next.clear();
}
next.add(id);
@@ -55,7 +61,7 @@ function Accordion({ children, type = 'single', defaultOpen = [] }: AccordionPro
return next;
});
},
[type]
[type],
);
return (
@@ -93,7 +99,7 @@ function AccordionTrigger({ children }: { children: React.ReactNode }) {
>
{children}
<ChevronDown
className={`h-4 w-4 transition-transform ${isOpen ? 'rotate-180' : ''}`}
className={`h-4 w-4 transition-transform ${isOpen ? "rotate-180" : ""}`}
/>
</button>
);
@@ -120,7 +126,7 @@ export const AccordionCompound = Object.assign(Accordion, {
// Usage
function Example() {
return (
<AccordionCompound type="single" defaultOpen={['item-1']}>
<AccordionCompound type="single" defaultOpen={["item-1"]}>
<AccordionCompound.Item id="item-1">
<AccordionCompound.Trigger>Is it accessible?</AccordionCompound.Trigger>
<AccordionCompound.Content>
@@ -144,7 +150,7 @@ Polymorphic components can render as different HTML elements or other components
```tsx
// Polymorphic component with proper TypeScript support
import * as React from 'react';
import * as React from "react";
type AsProp<C extends React.ElementType> = {
as?: C;
@@ -154,64 +160,71 @@ type PropsToOmit<C extends React.ElementType, P> = keyof (AsProp<C> & P);
type PolymorphicComponentProp<
C extends React.ElementType,
Props = {}
Props = {},
> = React.PropsWithChildren<Props & AsProp<C>> &
Omit<React.ComponentPropsWithoutRef<C>, PropsToOmit<C, Props>>;
type PolymorphicRef<C extends React.ElementType> =
React.ComponentPropsWithRef<C>['ref'];
React.ComponentPropsWithRef<C>["ref"];
type PolymorphicComponentPropWithRef<
C extends React.ElementType,
Props = {}
Props = {},
> = PolymorphicComponentProp<C, Props> & { ref?: PolymorphicRef<C> };
// Button component
interface ButtonOwnProps {
variant?: 'default' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
variant?: "default" | "outline" | "ghost";
size?: "sm" | "md" | "lg";
}
type ButtonProps<C extends React.ElementType = 'button'> =
type ButtonProps<C extends React.ElementType = "button"> =
PolymorphicComponentPropWithRef<C, ButtonOwnProps>;
const Button = React.forwardRef(
<C extends React.ElementType = 'button'>(
{ as, variant = 'default', size = 'md', className, children, ...props }: ButtonProps<C>,
ref?: PolymorphicRef<C>
<C extends React.ElementType = "button">(
{
as,
variant = "default",
size = "md",
className,
children,
...props
}: ButtonProps<C>,
ref?: PolymorphicRef<C>,
) => {
const Component = as || 'button';
const Component = as || "button";
const variantClasses = {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
outline: 'border border-input bg-background hover:bg-accent',
ghost: 'hover:bg-accent hover:text-accent-foreground',
default: "bg-primary text-primary-foreground hover:bg-primary/90",
outline: "border border-input bg-background hover:bg-accent",
ghost: "hover:bg-accent hover:text-accent-foreground",
};
const sizeClasses = {
sm: 'h-8 px-3 text-sm',
md: 'h-10 px-4 text-sm',
lg: 'h-12 px-6 text-base',
sm: "h-8 px-3 text-sm",
md: "h-10 px-4 text-sm",
lg: "h-12 px-6 text-base",
};
return (
<Component
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
variantClasses[variant],
sizeClasses[size],
className
className,
)}
{...props}
>
{children}
</Component>
);
}
},
);
Button.displayName = 'Button';
Button.displayName = "Button";
// Usage
function Example() {
@@ -242,31 +255,31 @@ Slots allow users to replace default elements with custom implementations.
```tsx
// Slot pattern for customizable components
import * as React from 'react';
import { Slot } from '@radix-ui/react-slot';
import * as React from "react";
import { Slot } from "@radix-ui/react-slot";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
asChild?: boolean;
variant?: 'default' | 'outline';
variant?: "default" | "outline";
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ asChild = false, variant = 'default', className, ...props }, ref) => {
const Comp = asChild ? Slot : 'button';
({ asChild = false, variant = "default", className, ...props }, ref) => {
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
className={cn(
'inline-flex items-center justify-center rounded-md font-medium',
variant === 'default' && 'bg-primary text-primary-foreground',
variant === 'outline' && 'border border-input bg-background',
className
"inline-flex items-center justify-center rounded-md font-medium",
variant === "default" && "bg-primary text-primary-foreground",
variant === "outline" && "border border-input bg-background",
className,
)}
{...props}
/>
);
}
},
);
// Usage - Button styles applied to child element
@@ -285,7 +298,7 @@ Headless components provide behavior without styling, enabling complete visual c
```tsx
// Headless toggle hook
import * as React from 'react';
import * as React from "react";
interface UseToggleProps {
defaultPressed?: boolean;
@@ -315,8 +328,8 @@ function useToggle({
pressed,
toggle,
buttonProps: {
role: 'switch' as const,
'aria-checked': pressed,
role: "switch" as const,
"aria-checked": pressed,
onClick: toggle,
},
};
@@ -334,7 +347,8 @@ function useListbox<T>({
defaultSelectedIndex = -1,
onSelect,
}: UseListboxProps<T>) {
const [selectedIndex, setSelectedIndex] = React.useState(defaultSelectedIndex);
const [selectedIndex, setSelectedIndex] =
React.useState(defaultSelectedIndex);
const [highlightedIndex, setHighlightedIndex] = React.useState(-1);
const select = React.useCallback(
@@ -342,40 +356,40 @@ function useListbox<T>({
setSelectedIndex(index);
onSelect?.(items[index], index);
},
[items, onSelect]
[items, onSelect],
);
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent) => {
switch (event.key) {
case 'ArrowDown':
case "ArrowDown":
event.preventDefault();
setHighlightedIndex((prev) =>
prev < items.length - 1 ? prev + 1 : prev
prev < items.length - 1 ? prev + 1 : prev,
);
break;
case 'ArrowUp':
case "ArrowUp":
event.preventDefault();
setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : prev));
break;
case 'Enter':
case ' ':
case "Enter":
case " ":
event.preventDefault();
if (highlightedIndex >= 0) {
select(highlightedIndex);
}
break;
case 'Home':
case "Home":
event.preventDefault();
setHighlightedIndex(0);
break;
case 'End':
case "End":
event.preventDefault();
setHighlightedIndex(items.length - 1);
break;
}
},
[items.length, highlightedIndex, select]
[items.length, highlightedIndex, select],
);
return {
@@ -384,13 +398,13 @@ function useListbox<T>({
select,
setHighlightedIndex,
listboxProps: {
role: 'listbox' as const,
role: "listbox" as const,
tabIndex: 0,
onKeyDown: handleKeyDown,
},
getOptionProps: (index: number) => ({
role: 'option' as const,
'aria-selected': index === selectedIndex,
role: "option" as const,
"aria-selected": index === selectedIndex,
onClick: () => select(index),
onMouseEnter: () => setHighlightedIndex(index),
}),
@@ -461,28 +475,28 @@ function Badge({ className, variant, size, ...props }: BadgeProps) {
## Responsive Variants
```tsx
import { cva } from 'class-variance-authority';
import { cva } from "class-variance-authority";
// Responsive variant configuration
const containerVariants = cva('mx-auto w-full px-4', {
const containerVariants = cva("mx-auto w-full px-4", {
variants: {
size: {
sm: 'max-w-screen-sm',
md: 'max-w-screen-md',
lg: 'max-w-screen-lg',
xl: 'max-w-screen-xl',
full: 'max-w-full',
sm: "max-w-screen-sm",
md: "max-w-screen-md",
lg: "max-w-screen-lg",
xl: "max-w-screen-xl",
full: "max-w-full",
},
padding: {
none: 'px-0',
sm: 'px-4 md:px-6',
md: 'px-4 md:px-8 lg:px-12',
lg: 'px-6 md:px-12 lg:px-20',
none: "px-0",
sm: "px-4 md:px-6",
md: "px-4 md:px-8 lg:px-12",
lg: "px-6 md:px-12 lg:px-20",
},
},
defaultVariants: {
size: 'lg',
padding: 'md',
size: "lg",
padding: "md",
},
});
@@ -498,23 +512,23 @@ interface ResponsiveValue<T> {
function getResponsiveClasses<T extends string>(
prop: T | ResponsiveValue<T> | undefined,
classMap: Record<T, string>,
responsiveClassMap: Record<string, Record<T, string>>
responsiveClassMap: Record<string, Record<T, string>>,
): string {
if (!prop) return '';
if (!prop) return "";
if (typeof prop === 'string') {
if (typeof prop === "string") {
return classMap[prop];
}
return Object.entries(prop)
.map(([breakpoint, value]) => {
if (breakpoint === 'base') {
if (breakpoint === "base") {
return classMap[value as T];
}
return responsiveClassMap[breakpoint]?.[value as T];
})
.filter(Boolean)
.join(' ');
.join(" ");
}
```
@@ -555,7 +569,7 @@ function DataList<T>({
keyExtractor={(user) => user.id}
renderItem={(user) => <UserCard user={user} />}
renderEmpty={() => <EmptyState message="No users found" />}
/>
/>;
```
### Children as Function
@@ -577,11 +591,11 @@ function Disclosure({ children, defaultOpen = false }: DisclosureProps) {
<Disclosure>
{({ isOpen, toggle }) => (
<>
<button onClick={toggle}>{isOpen ? 'Close' : 'Open'}</button>
<button onClick={toggle}>{isOpen ? "Close" : "Open"}</button>
{isOpen && <div>Content</div>}
</>
)}
</Disclosure>
</Disclosure>;
```
## Best Practices

View File

@@ -129,9 +129,15 @@ Design tokens are the atomic values of a design system - the smallest pieces tha
{
"shadow": {
"sm": { "value": "0 1px 2px 0 rgb(0 0 0 / 0.05)" },
"md": { "value": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)" },
"lg": { "value": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)" },
"xl": { "value": "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)" }
"md": {
"value": "0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)"
},
"lg": {
"value": "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)"
},
"xl": {
"value": "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)"
}
},
"radius": {
"none": { "value": "0" },
@@ -302,13 +308,13 @@ Examples:
### Style Dictionary Transforms
```javascript
const StyleDictionary = require('style-dictionary');
const StyleDictionary = require("style-dictionary");
// Custom transform for px to rem
StyleDictionary.registerTransform({
name: 'size/pxToRem',
type: 'value',
matcher: (token) => token.attributes.category === 'size',
name: "size/pxToRem",
type: "value",
matcher: (token) => token.attributes.category === "size",
transformer: (token) => {
const value = parseFloat(token.value);
return `${value / 16}rem`;
@@ -317,14 +323,14 @@ StyleDictionary.registerTransform({
// Custom format for CSS custom properties
StyleDictionary.registerFormat({
name: 'css/customProperties',
formatter: function({ dictionary, options }) {
const tokens = dictionary.allTokens.map(token => {
const name = token.name.replace(/\./g, '-');
name: "css/customProperties",
formatter: function ({ dictionary, options }) {
const tokens = dictionary.allTokens.map((token) => {
const name = token.name.replace(/\./g, "-");
return ` --${name}: ${token.value};`;
});
return `:root {\n${tokens.join('\n')}\n}`;
return `:root {\n${tokens.join("\n")}\n}`;
},
});
```
@@ -399,10 +405,10 @@ interface TokenValidation {
function validateContrast(
foreground: string,
background: string,
level: 'AA' | 'AAA' = 'AA'
level: "AA" | "AAA" = "AA",
): boolean {
const ratio = getContrastRatio(foreground, background);
return level === 'AA' ? ratio >= 4.5 : ratio >= 7;
return level === "AA" ? ratio >= 4.5 : ratio >= 7;
}
```

View File

@@ -16,7 +16,7 @@ A robust theming system enables applications to support multiple visual appearan
/* Base tokens that don't change */
--font-sans: Inter, system-ui, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
--font-mono: "JetBrains Mono", monospace;
/* Animation tokens */
--duration-fast: 150ms;
@@ -34,7 +34,7 @@ A robust theming system enables applications to support multiple visual appearan
/* 2. Light theme (default) */
:root,
[data-theme='light'] {
[data-theme="light"] {
--color-bg: #ffffff;
--color-bg-subtle: #f8fafc;
--color-bg-muted: #f1f5f9;
@@ -57,7 +57,7 @@ A robust theming system enables applications to support multiple visual appearan
}
/* 3. Dark theme */
[data-theme='dark'] {
[data-theme="dark"] {
--color-bg: #0f172a;
--color-bg-subtle: #1e293b;
--color-bg-muted: #334155;
@@ -81,7 +81,7 @@ A robust theming system enables applications to support multiple visual appearan
/* 4. System preference detection */
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
:root:not([data-theme="light"]) {
/* Inherit dark theme values */
--color-bg: #0f172a;
/* ... other dark values */
@@ -129,16 +129,16 @@ A robust theming system enables applications to support multiple visual appearan
```tsx
// theme-provider.tsx
import * as React from 'react';
import * as React from "react";
type Theme = 'light' | 'dark' | 'system';
type ResolvedTheme = 'light' | 'dark';
type Theme = "light" | "dark" | "system";
type ResolvedTheme = "light" | "dark";
interface ThemeProviderProps {
children: React.ReactNode;
defaultTheme?: Theme;
storageKey?: string;
attribute?: 'class' | 'data-theme';
attribute?: "class" | "data-theme";
enableSystem?: boolean;
disableTransitionOnChange?: boolean;
}
@@ -150,31 +150,32 @@ interface ThemeProviderState {
toggleTheme: () => void;
}
const ThemeProviderContext = React.createContext<ThemeProviderState | undefined>(
undefined
);
const ThemeProviderContext = React.createContext<
ThemeProviderState | undefined
>(undefined);
export function ThemeProvider({
children,
defaultTheme = 'system',
storageKey = 'theme',
attribute = 'data-theme',
defaultTheme = "system",
storageKey = "theme",
attribute = "data-theme",
enableSystem = true,
disableTransitionOnChange = false,
}: ThemeProviderProps) {
const [theme, setThemeState] = React.useState<Theme>(() => {
if (typeof window === 'undefined') return defaultTheme;
if (typeof window === "undefined") return defaultTheme;
return (localStorage.getItem(storageKey) as Theme) || defaultTheme;
});
const [resolvedTheme, setResolvedTheme] = React.useState<ResolvedTheme>('light');
const [resolvedTheme, setResolvedTheme] =
React.useState<ResolvedTheme>("light");
// Get system preference
const getSystemTheme = React.useCallback((): ResolvedTheme => {
if (typeof window === 'undefined') return 'light';
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light';
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light";
}, []);
// Apply theme to DOM
@@ -184,11 +185,11 @@ export function ThemeProvider({
// Disable transitions temporarily
if (disableTransitionOnChange) {
const css = document.createElement('style');
const css = document.createElement("style");
css.appendChild(
document.createTextNode(
`*,*::before,*::after{transition:none!important}`
)
`*,*::before,*::after{transition:none!important}`,
),
);
document.head.appendChild(css);
@@ -202,8 +203,8 @@ export function ThemeProvider({
}
// Apply attribute
if (attribute === 'class') {
root.classList.remove('light', 'dark');
if (attribute === "class") {
root.classList.remove("light", "dark");
root.classList.add(newTheme);
} else {
root.setAttribute(attribute, newTheme);
@@ -214,27 +215,27 @@ export function ThemeProvider({
setResolvedTheme(newTheme);
},
[attribute, disableTransitionOnChange]
[attribute, disableTransitionOnChange],
);
// Handle theme changes
React.useEffect(() => {
const resolved = theme === 'system' ? getSystemTheme() : theme;
const resolved = theme === "system" ? getSystemTheme() : theme;
applyTheme(resolved);
}, [theme, applyTheme, getSystemTheme]);
// Listen for system theme changes
React.useEffect(() => {
if (!enableSystem || theme !== 'system') return;
if (!enableSystem || theme !== "system") return;
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const handleChange = () => {
applyTheme(getSystemTheme());
};
mediaQuery.addEventListener('change', handleChange);
return () => mediaQuery.removeEventListener('change', handleChange);
mediaQuery.addEventListener("change", handleChange);
return () => mediaQuery.removeEventListener("change", handleChange);
}, [theme, enableSystem, applyTheme, getSystemTheme]);
// Persist to localStorage
@@ -243,11 +244,11 @@ export function ThemeProvider({
localStorage.setItem(storageKey, newTheme);
setThemeState(newTheme);
},
[storageKey]
[storageKey],
);
const toggleTheme = React.useCallback(() => {
setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
setTheme(resolvedTheme === "light" ? "dark" : "light");
}, [resolvedTheme, setTheme]);
const value = React.useMemo(
@@ -257,7 +258,7 @@ export function ThemeProvider({
setTheme,
toggleTheme,
}),
[theme, resolvedTheme, setTheme, toggleTheme]
[theme, resolvedTheme, setTheme, toggleTheme],
);
return (
@@ -270,7 +271,7 @@ export function ThemeProvider({
export function useTheme() {
const context = React.useContext(ThemeProviderContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
throw new Error("useTheme must be used within a ThemeProvider");
}
return context;
}
@@ -280,8 +281,8 @@ export function useTheme() {
```tsx
// theme-toggle.tsx
import { Moon, Sun, Monitor } from 'lucide-react';
import { useTheme } from './theme-provider';
import { Moon, Sun, Monitor } from "lucide-react";
import { useTheme } from "./theme-provider";
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
@@ -289,27 +290,27 @@ export function ThemeToggle() {
return (
<div className="flex items-center gap-1 rounded-lg bg-muted p-1">
<button
onClick={() => setTheme('light')}
onClick={() => setTheme("light")}
className={`rounded-md p-2 ${
theme === 'light' ? 'bg-background shadow-sm' : ''
theme === "light" ? "bg-background shadow-sm" : ""
}`}
aria-label="Light theme"
>
<Sun className="h-4 w-4" />
</button>
<button
onClick={() => setTheme('dark')}
onClick={() => setTheme("dark")}
className={`rounded-md p-2 ${
theme === 'dark' ? 'bg-background shadow-sm' : ''
theme === "dark" ? "bg-background shadow-sm" : ""
}`}
aria-label="Dark theme"
>
<Moon className="h-4 w-4" />
</button>
<button
onClick={() => setTheme('system')}
onClick={() => setTheme("system")}
className={`rounded-md p-2 ${
theme === 'system' ? 'bg-background shadow-sm' : ''
theme === "system" ? "bg-background shadow-sm" : ""
}`}
aria-label="System theme"
>
@@ -326,42 +327,42 @@ export function ThemeToggle() {
```css
/* Brand A - Corporate Blue */
[data-brand='corporate'] {
[data-brand="corporate"] {
--brand-primary: #0066cc;
--brand-primary-hover: #0052a3;
--brand-secondary: #f0f7ff;
--brand-accent: #00a3e0;
--brand-font-heading: 'Helvetica Neue', sans-serif;
--brand-font-body: 'Open Sans', sans-serif;
--brand-font-heading: "Helvetica Neue", sans-serif;
--brand-font-body: "Open Sans", sans-serif;
--brand-radius: 0.25rem;
--brand-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Brand B - Modern Startup */
[data-brand='startup'] {
[data-brand="startup"] {
--brand-primary: #7c3aed;
--brand-primary-hover: #6d28d9;
--brand-secondary: #faf5ff;
--brand-accent: #f472b6;
--brand-font-heading: 'Poppins', sans-serif;
--brand-font-body: 'Inter', sans-serif;
--brand-font-heading: "Poppins", sans-serif;
--brand-font-body: "Inter", sans-serif;
--brand-radius: 1rem;
--brand-shadow: 0 4px 12px rgba(124, 58, 237, 0.15);
}
/* Brand C - Minimal */
[data-brand='minimal'] {
[data-brand="minimal"] {
--brand-primary: #171717;
--brand-primary-hover: #404040;
--brand-secondary: #fafafa;
--brand-accent: #171717;
--brand-font-heading: 'Space Grotesk', sans-serif;
--brand-font-body: 'IBM Plex Sans', sans-serif;
--brand-font-heading: "Space Grotesk", sans-serif;
--brand-font-body: "IBM Plex Sans", sans-serif;
--brand-radius: 0;
--brand-shadow: none;
@@ -402,7 +403,7 @@ export function ThemeToggle() {
--color-accent: #0000ee;
}
[data-theme='dark'] {
[data-theme="dark"] {
--color-text: #ffffff;
--color-text-muted: #ffffff;
--color-bg: #000000;
@@ -470,9 +471,9 @@ export default function RootLayout({ children }) {
```tsx
// theme.test.tsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider, useTheme } from './theme-provider';
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { ThemeProvider, useTheme } from "./theme-provider";
function TestComponent() {
const { theme, setTheme, resolvedTheme } = useTheme();
@@ -480,34 +481,34 @@ function TestComponent() {
<div>
<span data-testid="theme">{theme}</span>
<span data-testid="resolved">{resolvedTheme}</span>
<button onClick={() => setTheme('dark')}>Set Dark</button>
<button onClick={() => setTheme("dark")}>Set Dark</button>
</div>
);
}
describe('ThemeProvider', () => {
it('should default to system theme', () => {
describe("ThemeProvider", () => {
it("should default to system theme", () => {
render(
<ThemeProvider>
<TestComponent />
</ThemeProvider>
</ThemeProvider>,
);
expect(screen.getByTestId('theme')).toHaveTextContent('system');
expect(screen.getByTestId("theme")).toHaveTextContent("system");
});
it('should switch to dark theme', async () => {
it("should switch to dark theme", async () => {
const user = userEvent.setup();
render(
<ThemeProvider>
<TestComponent />
</ThemeProvider>
</ThemeProvider>,
);
await user.click(screen.getByText('Set Dark'));
expect(screen.getByTestId('theme')).toHaveTextContent('dark');
expect(document.documentElement).toHaveAttribute('data-theme', 'dark');
await user.click(screen.getByText("Set Dark"));
expect(screen.getByTestId("theme")).toHaveTextContent("dark");
expect(document.documentElement).toHaveAttribute("data-theme", "dark");
});
});
```