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
14 changes: 7 additions & 7 deletions src/components/Badge.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import styled from "styled-components";
import { BadgeShape, getThemeColorsByVariant, ThemeVariant } from "./utils";
import { BadgeShape, ThemeVariant, getVariantColors } from "./utils";

interface BadgeProps extends React.HTMLAttributes<HTMLSpanElement> {
$variant?: ThemeVariant;
Expand Down Expand Up @@ -31,16 +31,16 @@ export const Badge = styled.span<BadgeProps>`
border-radius: ${({ $shape = BadgeShape.Pill, theme }) =>
$shape === BadgeShape.Rounded ? theme.radius.md : theme.radius.full};
border: 1px solid
${({ $variant = ThemeVariant.Info }) =>
getThemeColorsByVariant($variant).border};
${({ $variant = ThemeVariant.Info, theme }) =>
getVariantColors(theme, $variant).border};

font: ${({ theme }) => theme.typography.small.font};
line-height: ${({ theme }) => theme.fonts.lineHeight.mini};

background: ${({ $variant = ThemeVariant.Info }) =>
getThemeColorsByVariant($variant).bg};
color: ${({ $variant = ThemeVariant.Info }) =>
getThemeColorsByVariant($variant).text};
background: ${({ $variant = ThemeVariant.Info, theme }) =>
getVariantColors(theme, $variant).bg};
color: ${({ $variant = ThemeVariant.Info, theme }) =>
getVariantColors(theme, $variant).text};

${({ $isInteractive, theme }) =>
$isInteractive
Expand Down
4 changes: 2 additions & 2 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import styled from "styled-components";
import {
ButtonWidth,
getThemeColorsByVariant,
getThemeSizeValuesByThemeSize,
getVariantColors,
ThemeSize,
ThemeVariant,
} from "./utils";
Expand Down Expand Up @@ -67,7 +67,7 @@ export const Button = styled.button<ButtonProps>`
transition: all ${({ theme }) => theme.transitions.base};

${({ $variant = ThemeVariant.Info, $ghost, theme }) => {
const colors = getThemeColorsByVariant($variant);
const colors = getVariantColors(theme, $variant);
const baseBg = colors.solidBg;
const hoverBg = colors.text;
const textColor = colors.solidText;
Expand Down
4 changes: 4 additions & 0 deletions src/components/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export const Input = styled.input`
padding: ${({ theme }) => theme.spacing.md} ${({ theme }) => theme.spacing.md};

font: ${({ theme }) => theme.typography.caption.font};
color: ${({ theme }) => theme.colors.text.primary};
background: ${({ theme }) => theme.colors.background.primary};

transition: border-color ${({ theme }) => theme.transitions.base};

Expand Down Expand Up @@ -58,6 +60,8 @@ const StyledSelect = styled.select`
padding: ${({ theme }) => `${theme.spacing.sm} ${theme.spacing.md}`};

font: ${({ theme }) => theme.typography.body.font};
color: ${({ theme }) => theme.colors.text.primary};
background: ${({ theme }) => theme.colors.background.primary};
line-height: 1.5;

transition: border-color ${({ theme }) => theme.transitions.base};
Expand Down
6 changes: 3 additions & 3 deletions src/components/Icon.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { LucideIcon } from "lucide-react";
import styled from "styled-components";
import {
getThemeColorsByVariant,
getThemePixelSizeValuesByThemeSize,
getVariantColors,
ThemeSize,
ThemeVariant,
} from "./utils";
Expand Down Expand Up @@ -36,10 +36,10 @@ const IconWrapper = styled.span<{

transition: all ${({ theme }) => theme.transitions.base};

${({ $variant, $ghost, $round }) => {
${({ $variant, $ghost, $round, theme }) => {
if (!$round || !$variant) return "";

const colors = getThemeColorsByVariant($variant);
const colors = getVariantColors(theme, $variant);
const baseBg = colors.solidBg;
const textColor = colors.solidText;
const borderColor = colors.border;
Expand Down
14 changes: 7 additions & 7 deletions src/components/NotificationCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import styled from "styled-components";
import { ThemeVariant, getThemeColorsByVariant } from "./utils";
import { ThemeVariant, getVariantColors } from "./utils";

export interface NotificationCardProps extends React.HTMLAttributes<HTMLDivElement> {
$variant?: ThemeVariant;
Expand Down Expand Up @@ -38,13 +38,13 @@ export const NotificationCard = styled.div<NotificationCardProps>`

border-radius: ${({ theme }) => theme.radius.md};
border: 1px solid
${({ $variant = ThemeVariant.Info }) =>
getThemeColorsByVariant($variant).border};
${({ $variant = ThemeVariant.Info, theme }) =>
getVariantColors(theme, $variant).border};

background: ${({ $variant = ThemeVariant.Info }) =>
getThemeColorsByVariant($variant).bg};
color: ${({ $variant = ThemeVariant.Info }) =>
getThemeColorsByVariant($variant).text};
background: ${({ $variant = ThemeVariant.Info, theme }) =>
getVariantColors(theme, $variant).bg};
color: ${({ $variant = ThemeVariant.Info, theme }) =>
getVariantColors(theme, $variant).text};

font: ${({ theme }) => theme.typography.body.font};
`;
16 changes: 10 additions & 6 deletions src/components/Spinner.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { spin } from "@/theme/animations";
import { theme } from "@/theme/theme";
import React from "react";
import styled from "styled-components";
import {
getThemeColorsByVariant,
getThemePixelSizeValuesByThemeSize,
getThemeSizeValuesByThemeSize,
getVariantColors,
ThemeSize,
ThemeVariant,
} from "./utils";
Expand Down Expand Up @@ -35,10 +36,10 @@ const SpinnerContainer = styled.div<{

border-radius: 50%;

background-color: ${({ $variant }) =>
getThemeColorsByVariant($variant).solidBg};
background-color: ${({ $variant, theme }) =>
getVariantColors(theme, $variant).solidBg};
border: 1px solid
${({ $variant }) => getThemeColorsByVariant($variant).border};
${({ $variant, theme }) => getVariantColors(theme, $variant).border};
`;

export interface SpinnerProps extends React.HTMLAttributes<HTMLDivElement> {
Expand Down Expand Up @@ -77,9 +78,12 @@ export const Spinner: React.FC<SpinnerProps> = ({
className,
...rest
}) => {
const resolvedColor = $color || getThemeColorsByVariant($variant).solidBg;
// Note: We can't access theme context here in the component function,
// so we continue using the static theme for optional color overrides.
// This is acceptable since $color and $trackColor are explicit overrides.
const resolvedColor = $color || getVariantColors(theme, $variant).solidBg;
const resolvedTrackColor =
$trackColor || getThemeColorsByVariant($variant).solidText;
$trackColor || getVariantColors(theme, $variant).solidText;

const spinner = (
<Ring
Expand Down
108 changes: 82 additions & 26 deletions src/components/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,95 @@
import { theme } from "@/theme/theme";
import { darkTheme, theme } from "@/theme/theme";
import { describe, expect, test } from "vitest";
import {
getThemeColorsByVariant,
getThemePixelSizeValuesByThemeSize,
getThemeSizeValuesByThemeSize,
getVariantColors,
ThemeSize,
ThemeVariant,
} from "./utils";

describe("Component utility functions", () => {
test("getThemeColorsByVariant returns correct colors for all variants", () => {
expect(getThemeColorsByVariant(ThemeVariant.Success)).toBe(
theme.colors.success,
);
expect(getThemeColorsByVariant(ThemeVariant.Warning)).toBe(
theme.colors.warning,
);
expect(getThemeColorsByVariant(ThemeVariant.Error)).toBe(
theme.colors.error,
);
expect(getThemeColorsByVariant(ThemeVariant.Info)).toBe(theme.colors.info);
expect(getThemeColorsByVariant(ThemeVariant.Primary)).toBe(
theme.colors.info,
);
expect(getThemeColorsByVariant(ThemeVariant.Accent)).toBe(
theme.colors.accent,
);
expect(getThemeColorsByVariant(ThemeVariant.Secondary)).toBe(
theme.colors.accent,
);
});
describe("getVariantColors", () => {
test("returns correct colors for all variants with light theme", () => {
expect(getVariantColors(theme, ThemeVariant.Success)).toEqual(
theme.colors.success,
);
expect(getVariantColors(theme, ThemeVariant.Warning)).toEqual(
theme.colors.warning,
);
expect(getVariantColors(theme, ThemeVariant.Error)).toEqual(
theme.colors.error,
);
expect(getVariantColors(theme, ThemeVariant.Info)).toEqual(
theme.colors.info,
);
expect(getVariantColors(theme, ThemeVariant.Primary)).toEqual(
theme.colors.info,
);
expect(getVariantColors(theme, ThemeVariant.Accent)).toEqual(
theme.colors.accent,
);
expect(getVariantColors(theme, ThemeVariant.Secondary)).toEqual(
theme.colors.accent,
);
});

test("returns correct colors for all variants with dark theme", () => {
expect(getVariantColors(darkTheme, ThemeVariant.Success)).toEqual(
darkTheme.colors.success,
);
expect(getVariantColors(darkTheme, ThemeVariant.Warning)).toEqual(
darkTheme.colors.warning,
);
expect(getVariantColors(darkTheme, ThemeVariant.Error)).toEqual(
darkTheme.colors.error,
);
expect(getVariantColors(darkTheme, ThemeVariant.Info)).toEqual(
darkTheme.colors.info,
);
expect(getVariantColors(darkTheme, ThemeVariant.Primary)).toEqual(
darkTheme.colors.info,
);
expect(getVariantColors(darkTheme, ThemeVariant.Accent)).toEqual(
darkTheme.colors.accent,
);
expect(getVariantColors(darkTheme, ThemeVariant.Secondary)).toEqual(
darkTheme.colors.accent,
);
});

test("returns different colors for light and dark themes", () => {
const lightSuccess = getVariantColors(theme, ThemeVariant.Success);
const darkSuccess = getVariantColors(darkTheme, ThemeVariant.Success);

expect(lightSuccess.bg).not.toBe(darkSuccess.bg);
expect(lightSuccess.text).not.toBe(darkSuccess.text);
expect(lightSuccess.border).not.toBe(darkSuccess.border);
});

test("has consistent structure across all variants", () => {
const variants = [
ThemeVariant.Success,
ThemeVariant.Warning,
ThemeVariant.Error,
ThemeVariant.Info,
ThemeVariant.Accent,
];

test("getThemeColorsByVariant returns info color as fallback for invalid variant", () => {
const invalidVariant = "invalid" as ThemeVariant;
expect(getThemeColorsByVariant(invalidVariant)).toBe(theme.colors.info);
variants.forEach((variant) => {
const colors = getVariantColors(theme, variant);
expect(colors).toHaveProperty("bg");
expect(colors).toHaveProperty("border");
expect(colors).toHaveProperty("text");
expect(colors).toHaveProperty("solidBg");
expect(colors).toHaveProperty("solidText");
expect(typeof colors.bg).toBe("string");
expect(typeof colors.border).toBe("string");
expect(typeof colors.text).toBe("string");
expect(typeof colors.solidBg).toBe("string");
expect(typeof colors.solidText).toBe("string");
});
});
});

test("getThemeSizeValuesByThemeSize returns correct size values for all sizes", () => {
Expand Down
51 changes: 33 additions & 18 deletions src/components/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Whenever you export both components and other things TypeScript complaints that fast-reload
// cannot handle mixed exports. For this reason, we export them here.

import { theme } from "@/theme/theme";
import { theme, Theme } from "@/theme/theme";
import { createContext, useContext } from "react";

// ENUMS & CONSTANTS
Expand Down Expand Up @@ -35,17 +35,6 @@ export const enum ThemeSize {
FourXL = "4xl",
}

const THEME_COLORS_BY_VARIANT: Record<ThemeVariant, typeof theme.colors.info> =
{
[ThemeVariant.Success]: theme.colors.success,
[ThemeVariant.Warning]: theme.colors.warning,
[ThemeVariant.Error]: theme.colors.error,
[ThemeVariant.Info]: theme.colors.info,
[ThemeVariant.Primary]: theme.colors.info,
[ThemeVariant.Accent]: theme.colors.accent,
[ThemeVariant.Secondary]: theme.colors.accent,
};

const THEME_SIZE_VALUES_BY_THEME_SIZE: Record<ThemeSize, string> = {
[ThemeSize.ExtraSmall]: theme.size.xs,
[ThemeSize.Small]: theme.size.sm,
Expand Down Expand Up @@ -108,17 +97,43 @@ export const useTabs = () => {
return context;
};

// TYPES

export type VariantColors = {
bg: string;
border: string;
text: string;
solidBg: string;
solidText: string;
};

// FUNCTIONS

/**
* Maps the theme colors based on the provided theme variant.
* @param variant - The ThemeVariant to map.
* @returns The corresponding theme colors for the given variant.
* Maps a theme variant to the corresponding color set from a theme object.
* This function reads colors from the provided theme parameter, allowing it to work
* with both light and dark themes dynamically.
* @param themeObj - The theme object containing color definitions (light or dark theme)
* @param variant - The ThemeVariant to map
* @returns The corresponding theme colors for the given variant
* @example
* const colors = getThemeColorsByVariant(ThemeVariant.Success);
* // In a styled-component (theme comes from context)
* const colors = getVariantColors(theme, ThemeVariant.Success);
*/
export const getThemeColorsByVariant = (variant: ThemeVariant) => {
return THEME_COLORS_BY_VARIANT[variant] ?? theme.colors.info;
export const getVariantColors = (
themeObj: Theme,
variant: ThemeVariant,
): VariantColors => {
const variantMap: Record<ThemeVariant, keyof typeof themeObj.colors> = {
[ThemeVariant.Success]: "success",
[ThemeVariant.Warning]: "warning",
[ThemeVariant.Error]: "error",
[ThemeVariant.Info]: "info",
[ThemeVariant.Primary]: "info",
[ThemeVariant.Accent]: "accent",
[ThemeVariant.Secondary]: "accent",
};
return themeObj.colors[variantMap[variant]] as VariantColors;
};

/**
Expand Down
20 changes: 11 additions & 9 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter } from "react-router";
import { ThemeProvider } from "styled-components";

import App from "./App";
import { theme } from "./theme";
import { ThemeModeProvider } from "./theme";
import { GlobalStyles } from "./theme/GlobalStyles";
import { ThemedApp } from "./theme/ThemedApp";

// Create a client for React Query
const queryClient = new QueryClient();

createRoot(document.getElementById("root")!).render(
<StrictMode>
<QueryClientProvider client={queryClient}>
<ThemeProvider theme={theme}>
<BrowserRouter>
<GlobalStyles />
<App />
</BrowserRouter>
</ThemeProvider>
<ThemeModeProvider>
<ThemedApp>
<BrowserRouter>
<GlobalStyles />
<App />
</BrowserRouter>
</ThemedApp>
</ThemeModeProvider>
</QueryClientProvider>
</StrictMode>
</StrictMode>,
);
Loading