Skip to content

Commit 0b8f108

Browse files
committedJan 18, 2025
feat: themming with provider and utils for theme actions
1 parent 9deca9c commit 0b8f108

File tree

11 files changed

+260
-15
lines changed

11 files changed

+260
-15
lines changed
 

‎.storybook/main.ts

+16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { StorybookConfig } from "@storybook/react-webpack5";
22

3+
const path = require("path");
4+
const toPath = (filePath) => path.join(process.cwd(), filePath);
5+
36
const config: StorybookConfig = {
47
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
58
addons: [
@@ -13,5 +16,18 @@ const config: StorybookConfig = {
1316
name: "@storybook/react-webpack5",
1417
options: {},
1518
},
19+
webpackFinal: async (config) => {
20+
return {
21+
...config!,
22+
resolve: {
23+
...config.resolve,
24+
alias: {
25+
...config!.resolve!.alias,
26+
"@emotion/core": toPath("node_modules/@emotion/react"),
27+
"emotion-theming": toPath("node_modules/@emotion/react"),
28+
},
29+
},
30+
};
31+
},
1632
};
1733
export default config;

‎.storybook/preview.ts

+17-7
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
import type { Preview } from "@storybook/react";
2+
import { withThemeProvider } from "../src/utils/withThemeProvider";
23

3-
const preview: Preview = {
4-
parameters: {
5-
controls: {
6-
matchers: {
7-
color: /(background|color)$/i,
8-
date: /Date$/i,
9-
},
4+
export const decorators = [withThemeProvider];
5+
6+
export const parameters = {
7+
actions: { argTypesRegex: "^on[A-Z].*" },
8+
controls: {
9+
expanded: true,
10+
matchers: {
11+
color: /(background|color)$/i,
12+
date: /Date$/i,
1013
},
1114
},
15+
docs: {
16+
inlineStories: true,
17+
},
18+
};
19+
20+
const preview: Preview = {
21+
parameters,
1222
};
1323

1424
export default preview;
+40-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,47 @@
11
import React from "react";
2-
import { Button } from "./Button";
2+
import { Button, ButtonWithLoading } from ".";
3+
import { WithLoadingProps } from "../../hoc/withLoading";
4+
import { ButtonProps } from "./Button.styles";
35

46
export default {
57
title: "Components/Button",
68
component: Button,
9+
argTypes: {
10+
variant: {
11+
control: { type: "select" },
12+
options: ["primary", "secondary"],
13+
},
14+
isLoading: {
15+
control: { type: "boolean" },
16+
},
17+
loadingText: {
18+
control: { type: "text" },
19+
},
20+
children: {
21+
control: { type: "text" },
22+
},
23+
},
724
};
825

9-
export const Primary = () => <Button>Primary Button</Button>;
10-
export const Secondary = () => <Button variant="secondary">Secondary Button</Button>;
26+
const Template = (args: React.JSX.IntrinsicAttributes & ButtonProps) => <Button {...args} />;
27+
const LoadingTemplate = (args: React.JSX.IntrinsicAttributes & WithLoadingProps & ButtonProps) => <ButtonWithLoading {...args} />;
28+
29+
export const Primary = Template.bind({});
30+
Primary.args = {
31+
children: "Primary Button",
32+
variant: "primary",
33+
};
34+
35+
export const Secondary = Template.bind({});
36+
Secondary.args = {
37+
children: "Secondary Button",
38+
variant: "secondary",
39+
};
40+
41+
export const WithLoading = LoadingTemplate.bind({});
42+
WithLoading.args = {
43+
children: "Secondary Button",
44+
variant: "secondary",
45+
isLoading: true,
46+
loadingText: "Loading...",
47+
};

‎src/components/Button/Button.styles.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,33 @@ import styled from "@emotion/styled";
22

33
export interface ButtonProps
44
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
5+
children: React.ReactNode;
56
variant?: "primary" | "secondary";
67
}
78

89
export const StyledButton = styled.button<ButtonProps>`
9-
padding: 10px 20px;
10+
padding: 10px 30px;
1011
font-size: 1rem;
1112
border-radius: 5px;
1213
border: none;
14+
display: flex;
15+
justify-content: center;
16+
align-items: center;
17+
gap: 10px;
18+
1319
cursor: pointer;
1420
background-color: ${({ variant }) =>
1521
variant === "secondary" ? "gray" : "blue"};
1622
color: white;
23+
transition: all 0.3s ease;
1724
1825
&:hover {
1926
opacity: 0.9;
27+
transform: scale(1.04);
28+
}
29+
30+
&:disabled {
31+
background-color: #ccc;
32+
cursor: not-allowed;
2033
}
2134
`;

‎src/components/Button/Button.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import React from "react";
22
import { ButtonProps, StyledButton } from "./Button.styles";
33

4-
export const Button: React.FC<ButtonProps> = ({ variant = "primary", ...props }) => {
5-
return <StyledButton variant={variant} {...props} />;
4+
export const Button: React.FC<ButtonProps> = ({ variant = "primary", children, ...props }) => {
5+
return (
6+
<StyledButton variant={variant} {...props}>
7+
{children}
8+
</StyledButton>
9+
);
610
};

‎src/components/Button/index.tsx

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Button as ButtonBase } from "./Button";
2+
import { withLoading } from "../../hoc/withLoading";
3+
4+
export const Button = ButtonBase;
5+
6+
export const ButtonWithLoading = withLoading(ButtonBase);

‎src/contexts/ThemeProvider.tsx

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import React, { createContext, useContext, useState } from "react";
2+
import { ThemeProvider as EmotionThemeProvider } from "@emotion/react";
3+
import { lightTheme, darkTheme, Theme } from "../theme/defaultTheme";
4+
5+
interface ThemeContextProps {
6+
theme: Theme;
7+
toggleTheme: () => void;
8+
}
9+
10+
const ThemeContext = createContext<ThemeContextProps | undefined>(undefined);
11+
12+
interface LnbThemeProviderProps {
13+
children: React.ReactNode;
14+
theme?: Theme;
15+
}
16+
17+
export const LnbThemeProvider: React.FC<LnbThemeProviderProps> = ({
18+
children,
19+
theme = lightTheme,
20+
}) => {
21+
const [currentTheme, setCurrentTheme] = useState<Theme>(theme);
22+
23+
const toggleTheme = () => {
24+
setCurrentTheme((prev: any) => (prev === lightTheme ? darkTheme : lightTheme));
25+
};
26+
27+
return (
28+
<ThemeContext.Provider value={{ theme: currentTheme, toggleTheme }}>
29+
<EmotionThemeProvider theme={currentTheme}>{children}</EmotionThemeProvider>
30+
</ThemeContext.Provider>
31+
);
32+
};
33+
34+
export const useTheme = (): ThemeContextProps => {
35+
const context = useContext(ThemeContext);
36+
if (!context) {
37+
throw new Error("useTheme must be used within a CustomThemeProvider");
38+
}
39+
return context;
40+
};

‎src/hoc/withLoading/index.tsx

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import { StyledSpinner } from './spinner';
3+
import { useTheme } from '../../contexts/ThemeProvider';
4+
5+
export interface WithLoadingProps {
6+
isLoading?: boolean;
7+
children: React.ReactNode;
8+
variant?: "primary" | "secondary";
9+
loadingText?: string;
10+
}
11+
12+
export interface SpinnerProps {
13+
variant?: "primary" | "secondary";
14+
}
15+
16+
17+
function Spinner({ variant = "primary" }: SpinnerProps) {
18+
const { theme } = useTheme();
19+
20+
console.log(theme.colors[variant])
21+
return (
22+
<StyledSpinner viewBox="0 0 50 50">
23+
<circle
24+
className="path"
25+
cx="25"
26+
cy="25"
27+
r="20"
28+
fill="none"
29+
strokeWidth="4"
30+
stroke={theme.colors[variant]}
31+
/>
32+
</StyledSpinner>
33+
)
34+
}
35+
36+
export function withLoading<T extends object>(
37+
WrappedComponent: React.ComponentType<T>
38+
) {
39+
return ({
40+
isLoading = false,
41+
loadingText = "Carregando...",
42+
variant = "primary",
43+
children,
44+
...props
45+
}: WithLoadingProps & T) => {
46+
return (
47+
<WrappedComponent
48+
{...(props as T)}
49+
disabled={isLoading || (props as any).disabled}
50+
>
51+
{isLoading ? <>{loadingText} <Spinner variant={variant} /></> : children}
52+
</WrappedComponent>
53+
);
54+
};
55+
}
56+

‎src/hoc/withLoading/spinner.ts

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import styled from "@emotion/styled";
2+
3+
export const StyledSpinner = styled.svg`
4+
animation: rotate 2s linear infinite;
5+
width: 20px;
6+
height: 20px;
7+
8+
& .path {
9+
stroke-linecap: round;
10+
animation: dash 1.5s ease-in-out infinite;
11+
}
12+
13+
@keyframes rotate {
14+
100% {
15+
transform: rotate(360deg);
16+
}
17+
}
18+
@keyframes dash {
19+
0% {
20+
stroke-dasharray: 1, 150;
21+
stroke-dashoffset: 0;
22+
}
23+
50% {
24+
stroke-dasharray: 90, 150;
25+
stroke-dashoffset: -35;
26+
}
27+
100% {
28+
stroke-dasharray: 90, 150;
29+
stroke-dashoffset: -124;
30+
}
31+
}
32+
`;

‎src/theme/defaultTheme.ts

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,31 @@
1-
export const defaultTheme = {
1+
export const lightTheme = {
22
colors: {
3-
primary: "#007BFF",
3+
background: "#ffffff",
4+
text: "#000000",
5+
primary: "#dc3545",
46
secondary: "#6C757D",
7+
border: "#DDDDDD",
58
},
69
spacing: (factor: number) => `${factor * 8}px`,
10+
typography: {
11+
fontFamily: "'Roboto', sans-serif",
12+
fontSize: "16px",
13+
},
14+
};
15+
16+
export const darkTheme = {
17+
colors: {
18+
background: "#121212",
19+
text: "#ffffff",
20+
primary: "#1E88E5",
21+
secondary: "#8A8A8A",
22+
border: "#333333",
23+
},
24+
spacing: (factor: number) => `${factor * 8}px`,
25+
typography: {
26+
fontFamily: "'Roboto', sans-serif",
27+
fontSize: "16px",
28+
},
729
};
30+
31+
export type Theme = typeof lightTheme;

‎src/utils/withThemeProvider.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from "react";
2+
import { LnbThemeProvider } from "../contexts/ThemeProvider";
3+
import { lightTheme } from "../theme/defaultTheme";
4+
5+
export const withThemeProvider = (Story: any, context: any) => {
6+
return (<LnbThemeProvider theme={lightTheme}><Story /></LnbThemeProvider>)
7+
};

0 commit comments

Comments
 (0)
Failed to load comments.