feat: add theme manager

awesome!
This commit is contained in:
Sonny
2024-05-12 00:46:48 +02:00
committed by Sonny
parent b5cda75790
commit 3531038321
19 changed files with 271 additions and 203 deletions

View File

@@ -1,14 +1,14 @@
import { resolvePageComponent } from '@adonisjs/inertia/helpers'; import { resolvePageComponent } from '@adonisjs/inertia/helpers';
import { createInertiaApp } from '@inertiajs/react'; import { createInertiaApp } from '@inertiajs/react';
import { hydrateRoot } from 'react-dom/client'; import { hydrateRoot } from 'react-dom/client';
import { theme } from '~/styles/theme'; import { primaryColor } from '~/styles/theme';
import '../i18n/index'; import '../i18n/index';
const appName = import.meta.env.VITE_APP_NAME || 'MyLinks'; const appName = import.meta.env.VITE_APP_NAME || 'MyLinks';
createInertiaApp({ createInertiaApp({
progress: { color: theme.colors.primary }, progress: { color: primaryColor },
title: (title) => `${appName}${title && ` - ${title}`}`, title: (title) => `${appName}${title && ` - ${title}`}`,

View File

@@ -17,7 +17,7 @@ const FormField = styled('div', {
position: 'absolute', position: 'absolute',
top: 0, top: 0,
right: '-0.75em', right: '-0.75em',
color: theme.colors.red, color: theme.colors.lightRed,
content: (required ? '"*"' : '""') as any, content: (required ? '"*"' : '""') as any,
}, },
})); }));

View File

@@ -3,10 +3,10 @@ import styled from '@emotion/styled';
const Input = styled.input(({ theme }) => ({ const Input = styled.input(({ theme }) => ({
width: '100%', width: '100%',
color: theme.colors.font, color: theme.colors.font,
backgroundColor: theme.colors.white, backgroundColor: theme.colors.secondary,
padding: '0.75em', padding: '0.75em',
border: `1px solid ${theme.colors.lightestGrey}`, border: `1px solid ${theme.colors.lightGrey}`,
borderBottom: `2px solid ${theme.colors.lightestGrey}`, borderBottom: `2px solid ${theme.colors.lightGrey}`,
borderRadius: theme.border.radius, borderRadius: theme.border.radius,
transition: theme.transition.delay, transition: theme.transition.delay,
@@ -16,7 +16,7 @@ const Input = styled.input(({ theme }) => ({
'&::placeholder': { '&::placeholder': {
fontStyle: 'italic', fontStyle: 'italic',
color: theme.colors.lightestGrey, color: theme.colors.grey,
}, },
})); }));

View File

@@ -9,7 +9,6 @@ import QuickResourceAction from '~/components/dashboard/quick_action/quick_actio
import QuickLinkFavorite from '~/components/dashboard/quick_action/quick_favorite_link'; import QuickLinkFavorite from '~/components/dashboard/quick_action/quick_favorite_link';
import useCollections from '~/hooks/use_collections'; import useCollections from '~/hooks/use_collections';
import { makeRequest } from '~/lib/request'; import { makeRequest } from '~/lib/request';
import { theme as globalTheme } from '~/styles/theme';
const LinkWrapper = styled.li(({ theme }) => ({ const LinkWrapper = styled.li(({ theme }) => ({
userSelect: 'none', userSelect: 'none',
@@ -17,9 +16,9 @@ const LinkWrapper = styled.li(({ theme }) => ({
height: 'fit-content', height: 'fit-content',
width: '100%', width: '100%',
color: theme.colors.primary, color: theme.colors.primary,
backgroundColor: theme.colors.white, backgroundColor: theme.colors.secondary,
padding: '0.75em 1em', padding: '0.75em 1em',
border: `1px solid ${theme.colors.lightestGrey}`, border: `1px solid ${theme.colors.lightGrey}`,
borderRadius: theme.border.radius, borderRadius: theme.border.radius,
outline: '3px solid transparent', outline: '3px solid transparent',
})); }));
@@ -82,6 +81,10 @@ const LinkUrl = styled.span(({ theme }) => ({
fontSize: '0.8em', fontSize: '0.8em',
})); }));
const StarIcon = styled(AiFillStar)(({ theme }) => ({
color: theme.colors.yellow,
}));
const LinkUrlPathname = styled.span({ const LinkUrlPathname = styled.span({
opacity: 0, opacity: 0,
}); });
@@ -140,10 +143,7 @@ export default function LinkItem({
<LinkFavicon url={url} /> <LinkFavicon url={url} />
<ExternalLink href={url} className="reset"> <ExternalLink href={url} className="reset">
<LinkName> <LinkName>
{name}{' '} {name} {showUserControls && favorite && <StarIcon />}
{showUserControls && favorite && (
<AiFillStar color={globalTheme.colors.yellow} />
)}
</LinkName> </LinkName>
<LinkItemURL url={url} /> <LinkItemURL url={url} />
</ExternalLink> </ExternalLink>

View File

@@ -19,7 +19,7 @@ const LinksWrapper = styled.div({
}); });
const CollectionHeaderWrapper = styled.h2(({ theme }) => ({ const CollectionHeaderWrapper = styled.h2(({ theme }) => ({
color: theme.colors.blue, color: theme.colors.primary,
fontWeight: 500, fontWeight: 500,
display: 'flex', display: 'flex',
gap: '0.4em', gap: '0.4em',

View File

@@ -1,7 +1,12 @@
import styled from '@emotion/styled';
import { MouseEventHandler } from 'react'; import { MouseEventHandler } from 'react';
import { AiFillStar, AiOutlineStar } from 'react-icons/ai'; import { AiFillStar, AiOutlineStar } from 'react-icons/ai';
import ActionStyle from '~/components/dashboard/quick_action/_quick_action_style'; import ActionStyle from '~/components/dashboard/quick_action/_quick_action_style';
import { theme } from '~/styles/theme';
const StartIcon = styled(AiFillStar)(({ theme }) => ({
color: theme.colors.yellow,
}));
const UnstaredIcon = StartIcon.withComponent(AiOutlineStar);
const QuickLinkFavoriteStyle = ActionStyle.withComponent('div'); const QuickLinkFavoriteStyle = ActionStyle.withComponent('div');
const QuickLinkFavorite = ({ const QuickLinkFavorite = ({
@@ -14,11 +19,7 @@ const QuickLinkFavorite = ({
<QuickLinkFavoriteStyle <QuickLinkFavoriteStyle
onClick={onClick ? (event) => onClick(event) : undefined} onClick={onClick ? (event) => onClick(event) : undefined}
> >
{isFavorite ? ( {isFavorite ? <StartIcon /> : <UnstaredIcon />}
<AiFillStar color={theme.colors.yellow} />
) : (
<AiOutlineStar color={theme.colors.yellow} />
)}
</QuickLinkFavoriteStyle> </QuickLinkFavoriteStyle>
); );

View File

@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { ItemLink } from '~/components/dashboard/side_nav/nav_item'; import { ItemLink } from '~/components/dashboard/side_nav/nav_item';
const FavoriteItem = styled(ItemLink)(({ theme }) => ({ const FavoriteItem = styled(ItemLink)(({ theme }) => ({
backgroundColor: theme.colors.white, backgroundColor: theme.colors.secondary,
})); }));
export default FavoriteItem; export default FavoriteItem;

View File

@@ -5,7 +5,8 @@ export const Item = styled.div(({ theme }) => ({
userSelect: 'none', userSelect: 'none',
height: '40px', height: '40px',
width: '280px', width: '280px',
backgroundColor: theme.colors.lightGrey, color: theme.colors.font,
backgroundColor: theme.colors.background,
padding: '8px 12px', padding: '8px 12px',
borderRadius: theme.border.radius, borderRadius: theme.border.radius,
display: 'flex', display: 'flex',
@@ -19,7 +20,7 @@ export const Item = styled.div(({ theme }) => ({
// Disable hover effect for UserCard // Disable hover effect for UserCard
'&:hover:not(.disable-hover)': { '&:hover:not(.disable-hover)': {
backgroundColor: theme.colors.white, backgroundColor: theme.colors.secondary,
outlineWidth: '1px', outlineWidth: '1px',
outlineStyle: 'solid', outlineStyle: 'solid',
}, },

View File

@@ -1,14 +1,11 @@
import { Global, ThemeProvider } from '@emotion/react';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import documentStyle from '~/styles/document'; import ContextThemeProvider from '~/components/layouts/_theme_layout';
import { cssReset } from '~/styles/reset'; import DarkThemeContextProvider from '~/contexts/dark_theme_context';
import { theme } from '~/styles/theme';
export default function BaseLayout({ children }: { children: ReactNode }) { const BaseLayout = ({ children }: { children: ReactNode }) => (
return ( <DarkThemeContextProvider>
<> <ContextThemeProvider>{children}</ContextThemeProvider>
<ThemeProvider theme={theme}>{children}</ThemeProvider> </DarkThemeContextProvider>
<Global styles={[cssReset, documentStyle]} /> );
</>
); export default BaseLayout;
}

View File

@@ -0,0 +1,108 @@
import { Global, ThemeProvider, css, useTheme } from '@emotion/react';
import { ReactNode } from 'react';
import useDarkTheme from '~/hooks/use_dark_theme';
import { darkTheme, lightTheme } from '~/styles/theme';
export default function ContextThemeProvider({
children,
}: {
children: ReactNode;
}) {
const { isDarkTheme } = useDarkTheme();
return (
<ThemeProvider theme={isDarkTheme ? darkTheme : lightTheme}>
<GlobalStyles />
{children}
</ThemeProvider>
);
}
function GlobalStyles() {
const localTheme = useTheme();
const cssReset = css({
'*': {
boxSizing: 'border-box',
outline: 0,
margin: 0,
padding: 0,
scrollBehavior: 'smooth',
},
'.reset': {
backgroundColor: 'inherit',
color: 'inherit',
padding: 0,
margin: 0,
border: 0,
},
a: {
width: 'fit-content',
color: localTheme.colors.primary,
textDecoration: 'none',
borderBottom: '1px solid transparent',
},
b: {
fontWeight: 600,
letterSpacing: '0.5px',
},
'h1, h2, h3, h4, h5, h6': {
fontWeight: '500',
color: localTheme.colors.primary,
},
kbd: {
textShadow: '0 1px 0 #fff',
fontSize: '12px',
color: 'rgb(51, 51, 51)',
backgroundColor: 'rgb(247, 247, 247)',
padding: '0.25em 0.5em',
borderRadius: '3px',
border: '1px solid rgb(204, 204, 204)',
boxShadow: '0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset',
display: 'inline-block',
},
});
const documentStyle = css({
'html, body, #app': {
height: '100svh',
width: '100svw',
fontFamily: "'Poppins', sans-serif",
fontSize: '14px',
color: localTheme.colors.font,
backgroundColor: localTheme.colors.background,
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
overflow: 'hidden',
},
});
const scrollbarStyle = css({
/* width */
'::-webkit-scrollbar': {
height: '0.45em',
width: '0.45em',
},
/* Track */
'::-webkit-scrollbar-track': {
borderRadius: localTheme.border.radius,
},
/* Handle */
'::-webkit-scrollbar-thumb': {
background: localTheme.colors.primary,
borderRadius: localTheme.border.radius,
'&:hover': {
background: localTheme.colors.darkBlue,
},
},
});
return <Global styles={[cssReset, documentStyle, scrollbarStyle]} />;
}

View File

@@ -3,6 +3,7 @@ import { Link } from '@inertiajs/react';
import ExternalLink from '~/components/common/external_link'; import ExternalLink from '~/components/common/external_link';
import RoundedImage from '~/components/common/rounded_image'; import RoundedImage from '~/components/common/rounded_image';
import UnstyledList from '~/components/common/unstyled/unstyled_list'; import UnstyledList from '~/components/common/unstyled/unstyled_list';
import useDarkTheme from '~/hooks/use_dark_theme';
import useUser from '~/hooks/use_user'; import useUser from '~/hooks/use_user';
import PATHS from '../../../app/constants/paths'; import PATHS from '../../../app/constants/paths';
@@ -51,6 +52,9 @@ export default function Navbar() {
<option>EN</option> <option>EN</option>
</select> </select>
</li> </li>
<li>
<ThemeSwitch />
</li>
<li> <li>
<ExternalLink href={PATHS.REPO_GITHUB}>GitHub</ExternalLink> <ExternalLink href={PATHS.REPO_GITHUB}>GitHub</ExternalLink>
</li> </li>
@@ -81,3 +85,14 @@ export default function Navbar() {
</Nav> </Nav>
); );
} }
function ThemeSwitch() {
const { isDarkTheme, toggleDarkTheme } = useDarkTheme();
return (
<input
type="checkbox"
onChange={({ target }) => toggleDarkTheme(target.checked)}
checked={isDarkTheme}
/>
);
}

View File

@@ -0,0 +1,42 @@
import { ReactNode, createContext, useEffect, useState } from 'react';
const LS_KEY = 'dark_theme';
export const DarkThemeContext = createContext({
isDarkTheme: true,
toggleDarkTheme: (_value: boolean) => {},
});
export default function DarkThemeContextProvider({
children,
}: {
children: ReactNode;
}) {
const [isDarkTheme, setDarkTheme] = useState<boolean>(() => {
if (typeof window === 'undefined' || typeof localStorage === 'undefined')
return true;
const doUserPreferDarkTheme = window?.matchMedia(
'(prefers-color-scheme: dark)'
).matches;
return (
localStorage.getItem(LS_KEY) === 'true' ?? doUserPreferDarkTheme ?? true
);
});
const toggleDarkTheme = (value: boolean) => setDarkTheme(value);
useEffect(() => {
localStorage.setItem(LS_KEY, String(isDarkTheme));
}, [isDarkTheme]);
return (
<DarkThemeContext.Provider
value={{
isDarkTheme,
toggleDarkTheme,
}}
>
{children}
</DarkThemeContext.Provider>
);
}

View File

@@ -0,0 +1,5 @@
import { useContext } from 'react';
import { DarkThemeContext } from '~/contexts/dark_theme_context';
const useDarkTheme = () => useContext(DarkThemeContext);
export default useDarkTheme;

View File

@@ -6,7 +6,11 @@ import BackToDashboard from '~/components/common/navigation/bask_to_dashboard';
import FormLayout from '~/components/layouts/form_layout'; import FormLayout from '~/components/layouts/form_layout';
import { Visibility } from '../../../app/enums/visibility'; import { Visibility } from '../../../app/enums/visibility';
export default function CreateCollectionPage() { export default function CreateCollectionPage({
disableHomeLink,
}: {
disableHomeLink: boolean;
}) {
const { data, setData, post, processing } = useForm({ const { data, setData, post, processing } = useForm({
name: '', name: '',
description: '', description: '',
@@ -34,6 +38,7 @@ export default function CreateCollectionPage() {
title="Create a collection" title="Create a collection"
handleSubmit={handleSubmit} handleSubmit={handleSubmit}
canSubmit={!isFormDisabled} canSubmit={!isFormDisabled}
disableHomeLink={disableHomeLink}
> >
<BackToDashboard> <BackToDashboard>
<TextBox <TextBox

View File

@@ -25,7 +25,7 @@ interface HomePageProps {
const SideBar = styled.div(({ theme }) => ({ const SideBar = styled.div(({ theme }) => ({
paddingRight: '0.75em', paddingRight: '0.75em',
borderRight: `1px solid ${theme.colors.lightestGrey}`, borderRight: `1px solid ${theme.colors.lightGrey}`,
marginRight: '5px', marginRight: '5px',
})); }));
@@ -55,13 +55,6 @@ export default function HomePage(props: Readonly<HomePageProps>) {
<SideNavigation /> <SideNavigation />
</SideBar> </SideBar>
)} )}
{/* <AnimatePresence>
{isShowing && (
<SideMenu close={close}>
<SideNavigation />
</SideMenu>
)}
</AnimatePresence> */}
<Links isMobile={isMobile} openSideMenu={open} /> <Links isMobile={isMobile} openSideMenu={open} />
</SwiperHandler> </SwiperHandler>
</HomeProviders> </HomeProviders>

View File

@@ -1,19 +0,0 @@
import { css } from '@emotion/react';
import { theme } from './theme';
const documentStyle = css({
'html, body, #app': {
height: '100svh',
width: '100svw',
fontFamily: "'Poppins', sans-serif",
fontSize: '14px',
color: theme.colors.font,
backgroundColor: theme.colors.background,
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
overflow: 'hidden',
},
});
export default documentStyle;

View File

@@ -1,70 +0,0 @@
import { css } from '@emotion/react';
import { theme } from '~/styles/theme';
export const cssReset = css({
'*': {
boxSizing: 'border-box',
outline: 0,
margin: 0,
padding: 0,
scrollBehavior: 'smooth',
},
'.reset': {
backgroundColor: 'inherit',
color: 'inherit',
padding: 0,
margin: 0,
border: 0,
},
a: {
width: 'fit-content',
color: '#3f88c5',
textDecoration: 'none',
borderBottom: '1px solid transparent',
},
b: {
fontWeight: 600,
letterSpacing: '0.5px',
},
'h1, h2, h3, h4, h5, h6': {
fontWeight: '500',
color: theme.colors.primary,
},
kbd: {
textShadow: '0 1px 0 #fff',
fontSize: '12px',
color: 'rgb(51, 51, 51)',
backgroundColor: 'rgb(247, 247, 247)',
padding: '0.25em 0.5em',
borderRadius: '3px',
border: '1px solid rgb(204, 204, 204)',
boxShadow: '0 1px 0 rgba(0, 0, 0, 0.2), 0 0 0 2px #ffffff inset',
display: 'inline-block',
},
/* width */
'::-webkit-scrollbar': {
height: '0.45em',
width: '0.45em',
},
/* Track */
'::-webkit-scrollbar-track': {
borderRadius: theme.border.radius,
},
/* Handle */
'::-webkit-scrollbar-thumb': {
background: theme.colors.blue,
borderRadius: theme.border.radius,
'&:hover': {
background: theme.colors.darkBlue,
},
},
});

View File

@@ -1,78 +1,80 @@
import { Theme } from '@emotion/react'; import { Theme } from '@emotion/react';
const lightBlack = '#555'; export const primaryColor = '#3f88c5';
const black = '#333'; export const primaryDarkColor = '#005aa5';
const darkBlack = '#111';
const white = '#fff';
const lightestGrey = '#dadce0';
const lightGrey = '#f0eef6';
const grey = '#888888';
const darkGrey = '#4b5563';
const lightestBlue = '#d3e8fa';
const lightBlue = '#82c5fede'; const lightBlue = '#82c5fede';
const blue = '#3f88c5'; const darkBlue = primaryDarkColor;
const darkBlue = '#005aa5';
const darkestBlue = '#1f2937';
const lightestRed = '#ffbabab9';
const lightRed = '#FF5A5A'; const lightRed = '#FF5A5A';
const red = '#d8000c';
const lightGreen = '#c1ffbab9';
const green = 'green';
const yellow = '#ffc107'; const yellow = '#ffc107';
export const theme: Theme = { const border: Theme['border'] = {
colors: {
font: black,
background: lightGrey,
primary: blue,
lightBlack,
black,
darkBlack,
white,
lightestGrey,
lightGrey,
grey,
darkGrey,
lightestBlue,
lightBlue,
blue,
darkBlue,
darkestBlue,
lightestRed,
lightRed,
red,
lightGreen,
green,
yellow,
},
border: {
radius: '3px', radius: '3px',
}, };
media: { const media: Theme['media'] = {
mobile: '768px', mobile: '768px',
tablet: '1024px', tablet: '1024px',
small_desktop: '1280px', small_desktop: '1280px',
medium_desktop: '1440px', medium_desktop: '1440px',
large_desktop: '1920px', large_desktop: '1920px',
xlarge_desktop: '2560px', xlarge_desktop: '2560px',
};
const transition: Theme['transition'] = {
delay: '0.15s',
};
export const lightTheme: Theme = {
colors: {
font: '#333',
background: '#f0eef6',
primary: primaryColor,
secondary: '#fff',
white: '#fff',
lightGrey: '#dadce0',
grey: '#888888',
lightBlue,
blue: primaryColor,
darkBlue,
lightRed,
yellow,
}, },
transition: { border,
delay: '0.15s', media,
}, transition,
};
export const darkTheme: Theme = {
colors: {
font: '#f0eef6',
background: '#222831',
primary: '#4fadfc',
secondary: '#323a47',
white: '#fff',
lightGrey: '#323a47',
grey: '#888888',
lightBlue,
blue: '#4fadfc',
darkBlue,
lightRed,
yellow,
},
border,
media,
transition,
}; };

View File

@@ -6,30 +6,18 @@ declare module '@emotion/react' {
font: string; font: string;
background: string; background: string;
primary: string; primary: string;
secondary: string;
lightBlack: string;
black: string;
darkBlack: string;
white: string; white: string;
lightestGrey: string;
lightGrey: string; lightGrey: string;
grey: string; grey: string;
darkGrey: string;
lightestBlue: string;
lightBlue: string; lightBlue: string;
blue: string; blue: string;
darkBlue: string; darkBlue: string;
darkestBlue: string;
lightestRed: string;
lightRed: string; lightRed: string;
red: string;
lightGreen: string;
green: string;
yellow: string; yellow: string;
}; };