Skip to content

Commit

Permalink
feat(ui): creates useColorThemeResolver hook (#24)
Browse files Browse the repository at this point in the history
* feat(ui): adds ColorContextResolver
  • Loading branch information
csantiago132 authored Jan 2, 2025
1 parent c84b698 commit d9e6065
Show file tree
Hide file tree
Showing 9 changed files with 370 additions and 14 deletions.
54 changes: 54 additions & 0 deletions app/components/ColorContextChangerContainer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { motion, useInView } from 'framer-motion';
import { debounce } from 'lodash-es';
import * as React from 'react';

import { ColorContext } from '~/context/ColorContext';
import type { ColorThemeContextEnum } from '~/context/types';

export interface ColorChangeContainerProps {
children: React.ReactNode;
className?: string;
colorContext: ColorThemeContextEnum;
tag?: string;
}

export function ColorContextChangerContainer({
colorContext,
className,
tag,
children,
}: ColorChangeContainerProps): React.ReactNode {
const ref = React.useRef(null);

const { setColorContext } = React.useContext(ColorContext);

const isInView = useInView(ref, { once: false, margin: '-500px' });

const debouncedColorContextHandler = debounce(
(debounceColorContext: ColorThemeContextEnum) => {
setColorContext(debounceColorContext);
},
100,
);

React.useEffect(() => {
if (isInView) {
debouncedColorContextHandler(colorContext);
}
}, [isInView, colorContext, debouncedColorContextHandler]);

const CurrentTag = tag || 'div';
// @ts-expect-error Element mismatch between motion
const ColorContextChanger = motion[CurrentTag];

return (
<ColorContextChanger
ref={ref}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
className={className}
>
{children}
</ColorContextChanger>
);
}
39 changes: 39 additions & 0 deletions app/components/StaggerSplitText.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { motion } from 'framer-motion';
import * as React from 'react';

export function StaggerSplitText(props: { text: string }): React.ReactNode {
const { text } = props;

return (
<motion.div
initial='hidden'
animate='visible'
style={{ display: 'inline-flex', flexWrap: 'wrap' }}
>
{text.split('').map((individualLetter, individualLetterIndex) => (
<motion.span
key={String(individualLetterIndex + 1)}
style={{
display: 'inline-block',
overflow: 'hidden',
whiteSpace: 'pre',
}}
custom={individualLetterIndex}
variants={{
hidden: { opacity: 0, y: 12 },
visible: (currentIndex) => ({
opacity: 1,
y: 0,
transition: {
duration: 0.6,
delay: currentIndex * 0.04,
},
}),
}}
>
{individualLetter}
</motion.span>
))}
</motion.div>
);
}
63 changes: 63 additions & 0 deletions app/context/ColorContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { motion } from 'framer-motion';
import { get } from 'lodash-es';
import React, { createContext, useState } from 'react';

import { ColorThemeContextEnum } from '~/context/types';
import type { ColorContextState } from '~/context/types';
import { useColorThemeResolver } from '~/hooks/useColorThemeResolver';

export const ColorContext = createContext({
colorContext: ColorThemeContextEnum.DEFAULT,
setColorContext: (colorContext: ColorThemeContextEnum): void => {
/**
* as we need a default action to handle the param
*/
// eslint-disable-next-line no-console
console.debug(colorContext);
},
});

export function BodyHTMLTagColorProvider({
children,
}: {
children: React.ReactNode;
}): React.ReactNode {
const { resolveColorTheme, colorThemeMap, colorTheme } =
useColorThemeResolver();

const [colors, setColors] = useState<ColorContextState>(
get(colorThemeMap, [ColorThemeContextEnum.DEFAULT]),
);

const setColorsHandler = (
selectedColorContext: ColorThemeContextEnum,
): void => {
const { background, foreground } = resolveColorTheme(selectedColorContext);

setColors({ background, foreground });
};

const providerValue = React.useMemo(
() => ({
colorContext: colorTheme,
setColorContext: setColorsHandler,
}),
[colorTheme],
);

return (
<ColorContext.Provider value={providerValue}>
<motion.body
style={{
backgroundColor: get(colors, ['background']),
color: get(colors, ['foreground']),
transition: 'background-color 0.9s, color 1.2s',
}}
className='selection:bg-lime-200 selection:text-[#f52891cc]'
data-testid='root-body-test-id'
>
{children}
</motion.body>
</ColorContext.Provider>
);
}
53 changes: 53 additions & 0 deletions app/context/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
export type ColorContextState = {
background: string;
foreground: string;
};

export enum BaseColorsEnum {
WHITE = 'WHITE',
BLACK = 'BLACK',
}

export enum PrimaryColorsEnum {
RED = 'RED',
BLUE = 'BLUE',
YELLOW = 'YELLOW',
}

export enum SecondaryColorsEnum {
GREEN = 'GREEN',
ORANGE = 'ORANGE',
PURPLE = 'PURPLE',
}

type PrimaryColorsObject = {
[K in keyof typeof PrimaryColorsEnum]: string;
};

type SecondaryColorsObject = {
[K in keyof typeof SecondaryColorsEnum]: string;
};

type BaseColorsObject = {
[K in keyof typeof BaseColorsEnum]: string;
};

export type CombinedColorsObject = PrimaryColorsObject &
SecondaryColorsObject &
BaseColorsObject;

export enum ColorThemeContextEnum {
BLUE = PrimaryColorsEnum.BLUE,
DEFAULT = BaseColorsEnum.BLACK,
WHITE = BaseColorsEnum.WHITE,
RED = PrimaryColorsEnum.RED,
YELLOW = PrimaryColorsEnum.YELLOW,

GREEN = SecondaryColorsEnum.GREEN,
ORANGE = SecondaryColorsEnum.ORANGE,
PURPLE = SecondaryColorsEnum.PURPLE,
}

export type ColorThemeContextMap = {
[K in ColorThemeContextEnum]: ColorContextState;
};
91 changes: 91 additions & 0 deletions app/hooks/useColorThemeResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { get } from 'lodash-es';
import { useState } from 'react';

import {
BaseColorsEnum,
ColorThemeContextEnum,
PrimaryColorsEnum,
SecondaryColorsEnum,
} from '~/context/types';
import type {
ColorContextState,
ColorThemeContextMap,
CombinedColorsObject,
} from '~/context/types';

export type UseColorThemeResolver = () => {
colorTheme: ColorThemeContextEnum;
colorThemeMap: ColorThemeContextMap;
resolveColorTheme: (theme: ColorThemeContextEnum) => ColorContextState;
};

export const useColorThemeResolver: UseColorThemeResolver = () => {
const [colorTheme, setColorTheme] = useState<ColorThemeContextEnum>(
ColorThemeContextEnum.DEFAULT,
);

const colors: {
[K in keyof CombinedColorsObject]: string;
} = {
[BaseColorsEnum.BLACK]: 'Black',
[BaseColorsEnum.WHITE]: 'GhostWhite',
[PrimaryColorsEnum.BLUE]: 'DarkBlue',
[PrimaryColorsEnum.RED]: 'Crimson',
[PrimaryColorsEnum.YELLOW]: 'Khaki',
[SecondaryColorsEnum.GREEN]: 'MediumSeaGreen',
[SecondaryColorsEnum.ORANGE]: 'DarkOrange',
[SecondaryColorsEnum.PURPLE]: 'Indigo',
};
const colorThemeMap: ColorThemeContextMap = {
[ColorThemeContextEnum.DEFAULT]: {
background: get(colors, [BaseColorsEnum.BLACK]),
foreground: get(colors, [PrimaryColorsEnum.RED]),
},
[ColorThemeContextEnum.WHITE]: {
background: get(colors, [BaseColorsEnum.WHITE]),
foreground: get(colors, [BaseColorsEnum.BLACK]),
},
[ColorThemeContextEnum.RED]: {
background: get(colors, [PrimaryColorsEnum.RED]),
foreground: get(colors, [SecondaryColorsEnum.GREEN]),
},
[ColorThemeContextEnum.GREEN]: {
background: get(colors, [SecondaryColorsEnum.GREEN]),
foreground: get(colors, [PrimaryColorsEnum.RED]),
},
[ColorThemeContextEnum.BLUE]: {
background: get(colors, [PrimaryColorsEnum.BLUE]),
foreground: get(colors, [SecondaryColorsEnum.ORANGE]),
},
[ColorThemeContextEnum.ORANGE]: {
background: get(colors, [SecondaryColorsEnum.ORANGE]),
foreground: get(colors, [PrimaryColorsEnum.BLUE]),
},
[ColorThemeContextEnum.PURPLE]: {
background: get(colors, [SecondaryColorsEnum.PURPLE]),
foreground: get(colors, [PrimaryColorsEnum.YELLOW]),
},
[ColorThemeContextEnum.YELLOW]: {
background: get(colors, [PrimaryColorsEnum.YELLOW]),
foreground: get(colors, [SecondaryColorsEnum.PURPLE]),
},
};

const resolveColorTheme = (
selectedColorContext: ColorThemeContextEnum,
): ColorContextState => {
setColorTheme(selectedColorContext);

return get(
colorThemeMap,
[selectedColorContext],
get(colorThemeMap, [ColorThemeContextEnum.DEFAULT]),
);
};

return {
resolveColorTheme,
colorThemeMap,
colorTheme,
};
};
2 changes: 1 addition & 1 deletion app/lib/HorizontalScrollText.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function HorizontalScrollText({
)}
style={{
lineHeight: '1.0',
color: idx % 3 === 0 ? '#344054' : '#e0e6e6',
color: idx % 3 === 0 ? undefined : '#e0e6e6',
}}
>
{children}
Expand Down
8 changes: 3 additions & 5 deletions app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
} from '@remix-run/react';
import React from 'react';

import { BodyHTMLTagColorProvider } from '~/context/ColorContext';
import '~/tailwind.css';

export const links: LinksFunction = () => [
Expand Down Expand Up @@ -49,14 +50,11 @@ export function Layout({
<Links />
<title>Kurocado Studio</title>
</head>
<body
className='selection:bg-lime-200 selection:text-[#f52891cc]'
data-testid='root-body-test-id'
>
<BodyHTMLTagColorProvider>
{children}
<ScrollRestoration />
<Scripts />
</body>
</BodyHTMLTagColorProvider>
</html>
);
}
Expand Down
Loading

0 comments on commit d9e6065

Please sign in to comment.