Issue 97 dark mode (#105)
This commit is contained in:
parent
9e1e153e74
commit
7e15cd3412
16 changed files with 88 additions and 25 deletions
|
|
@ -1 +1 @@
|
|||
<div class="w-full h-px bg-gray-200"></div>
|
||||
<div class="w-full h-px bg-gray-200 dark:bg-gray-600"></div>
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ const { icon, href, target, size, ...rest } = Astro.props;
|
|||
'focus:ring-2',
|
||||
'focus:ring-offset-2',
|
||||
'focus:ring-primary-500',
|
||||
'dark:bg-gray-600',
|
||||
'dark:text-gray-200',
|
||||
sizeMap[size],
|
||||
]}
|
||||
{...rest}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,6 @@ const { label, value } = Astro.props;
|
|||
---
|
||||
|
||||
<div class="text-base">
|
||||
<span class="font-medium text-gray-700">{label}:</span>
|
||||
<span class="font-normal text-gray-500">{value}</span>
|
||||
<span class="font-medium text-gray-700 dark:text-gray-300">{label}:</span>
|
||||
<span class="font-normal text-gray-500 dark:text-gray-400">{value}</span>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,4 +8,4 @@ export interface Props {
|
|||
const { section } = Astro.props;
|
||||
---
|
||||
|
||||
<div id={section} class="p-8 bg-white rounded-2xl shadow-md flex flex-col gap-6"><slot /></div>
|
||||
<div id={section} class="p-8 bg-white dark:bg-gray-800 rounded-2xl shadow-md flex flex-col gap-6"><slot /></div>
|
||||
|
|
|
|||
|
|
@ -24,7 +24,11 @@ const SidebarItem = ({ section, icon, title = '' }: SidebarItemProps) => {
|
|||
<a
|
||||
href={href}
|
||||
className={`inline-flex justify-center items-center h-10 w-10 rounded-lg transition
|
||||
${active ? 'bg-primary-600 text-white' : 'bg-white text-gray-400 hover:bg-primary-600 hover:text-white'}
|
||||
${
|
||||
active
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'bg-white text-gray-400 dark:bg-gray-800 dark:text-gray-200 hover:bg-primary-600 hover:text-white'
|
||||
}
|
||||
`}
|
||||
aria-current={active ? 'page' : undefined}
|
||||
aria-label={`${section} section`}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ const isFilled = skillLevel >= tileLevel;
|
|||
'last:rounded-r-sm',
|
||||
'first:rounded-l-sm',
|
||||
'first:rounded-r-none',
|
||||
{ 'bg-gray-500': isFilled, 'bg-gray-300': !isFilled },
|
||||
{ 'dark:bg-gray-300': isFilled, 'dark:bg-gray-500': !isFilled, 'bg-gray-200': !isFilled, 'bg-gray-500': isFilled },
|
||||
]}
|
||||
>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,9 @@ export interface Props {
|
|||
const { name, color } = Astro.props;
|
||||
---
|
||||
|
||||
<div class="flex items-center w-fit h-6 px-2.5 rounded text-sm font-medium text-gray-700 bg-gray-100 gap-x-1.5">
|
||||
<div
|
||||
class="flex items-center w-fit h-6 px-2.5 rounded text-sm font-medium tracking-wide text-gray-700 dark:text-gray-100 bg-gray-100 dark:bg-gray-700 gap-x-1.5"
|
||||
>
|
||||
<Icon client:load name={name} color={color} size={16} />
|
||||
<slot />
|
||||
</div>
|
||||
|
|
|
|||
49
src/components/atoms/theme-icon.tsx
Normal file
49
src/components/atoms/theme-icon.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
import Icon from './icon';
|
||||
|
||||
const STORAGE_THEME_KEY = 'theme';
|
||||
const DARK_THEME_KEY = 'dark';
|
||||
const LIGHT_THEME_KEY = 'light';
|
||||
|
||||
type ThemeVariant = typeof DARK_THEME_KEY | typeof LIGHT_THEME_KEY;
|
||||
|
||||
const getInitialTheme = (): ThemeVariant => {
|
||||
if (typeof localStorage !== 'undefined' && localStorage.getItem(STORAGE_THEME_KEY)) {
|
||||
return localStorage.getItem(STORAGE_THEME_KEY) === LIGHT_THEME_KEY ? LIGHT_THEME_KEY : DARK_THEME_KEY;
|
||||
}
|
||||
if (window.matchMedia(`(prefers-color-scheme: ${DARK_THEME_KEY})`).matches) {
|
||||
return DARK_THEME_KEY;
|
||||
}
|
||||
return LIGHT_THEME_KEY;
|
||||
};
|
||||
|
||||
const ThemeToggle = () => {
|
||||
const [theme, setTheme] = useState<ThemeVariant>(() => getInitialTheme());
|
||||
|
||||
const handleClick = () => {
|
||||
setTheme((prev) => (prev === LIGHT_THEME_KEY ? DARK_THEME_KEY : LIGHT_THEME_KEY));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (theme === DARK_THEME_KEY) {
|
||||
document.documentElement.classList.add(DARK_THEME_KEY);
|
||||
}
|
||||
if (theme === LIGHT_THEME_KEY) {
|
||||
document.documentElement.classList.remove(DARK_THEME_KEY);
|
||||
}
|
||||
localStorage.setItem(STORAGE_THEME_KEY, theme);
|
||||
}, [theme]);
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
type="button"
|
||||
className="text-gray-400 bg-gray-100 focus:ring-primary-500 dark:bg-gray-600 dark:text-gray-200 inline-flex justify-center items-center h-10 w-10 transition fixed bottom-3 left-3 shadow-xl rounded-lg z-10"
|
||||
>
|
||||
<Icon name={theme === DARK_THEME_KEY ? 'ri:moon-fill' : 'ri:sun-line'} size={20} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ThemeToggle;
|
||||
|
|
@ -11,7 +11,10 @@ const Tooltip = ({ children, content, placement = 'top' }: TooltipProps) => {
|
|||
return (
|
||||
<Tippy
|
||||
render={(attrs) => (
|
||||
<div {...attrs} className="bg-gray-700 rounded-lg px-2 py-1.5 text-white max-w-[95%] sm:max-w-xs">
|
||||
<div
|
||||
{...attrs}
|
||||
className="bg-gray-700 dark:bg-gray-100 dark:text-gray-800 rounded-lg px-2 py-1.5 text-white max-w-[95%] sm:max-w-xs"
|
||||
>
|
||||
{content}
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -25,16 +25,16 @@ const variantToElement = {
|
|||
} as const;
|
||||
|
||||
const variantToClassName = {
|
||||
'main-title': 'text-3xl sm:text-4xl font-extrabold text-gray-900',
|
||||
'main-subtitle': 'text-md sm:text-lg font-medium text-gray-700',
|
||||
'section-title': 'text-3xl font-extrabold text-gray-900',
|
||||
'section-subtitle': 'text-lg font-extrabold text-gray-900',
|
||||
'item-title': 'text-xl font-extrabold text-gray-900',
|
||||
'item-title-suffix': 'text-xl font-medium text-gray-700',
|
||||
'item-subtitle': 'text-md font-medium text-gray-700',
|
||||
'tile-title': 'text-sm font-medium text-gray-700',
|
||||
'tile-subtitle': 'text-sm font-normal text-gray-500',
|
||||
paragraph: 'text-sm sm:text-base leading-relaxed font-normal text-gray-500',
|
||||
'main-title': 'text-3xl sm:text-4xl font-extrabold text-gray-900 dark:text-gray-100',
|
||||
'main-subtitle': 'text-md sm:text-lg font-medium text-gray-700 dark:text-gray-100',
|
||||
'section-title': 'text-3xl font-extrabold text-gray-900 dark:text-gray-100',
|
||||
'section-subtitle': 'text-lg font-extrabold text-gray-900 dark:text-gray-100',
|
||||
'item-title': 'text-xl font-extrabold text-gray-900 dark:text-gray-100',
|
||||
'item-title-suffix': 'text-xl font-medium text-gray-700 dark:text-gray-100',
|
||||
'item-subtitle': 'text-md font-medium text-gray-700 dark:text-gray-100',
|
||||
'tile-title': 'text-sm font-medium text-gray-700 dark:text-gray-200',
|
||||
'tile-subtitle': 'text-sm font-normal text-gray-500 dark:text-gray-300',
|
||||
paragraph: 'text-sm sm:text-base leading-relaxed font-normal text-gray-500 dark:text-gray-300',
|
||||
};
|
||||
|
||||
export interface Props extends Omit<astroHTML.JSX.HTMLAttributes, 'slot'> {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@ export interface Props {
|
|||
const { className } = Astro.props;
|
||||
---
|
||||
|
||||
<nav class:list={['flex flex-col w-max h-fit p-2 rounded-lg gap-2 bg-white shadow-md', className]}>
|
||||
<nav class:list={['flex flex-col w-max h-fit p-2 rounded-lg gap-2 bg-white dark:bg-gray-800 shadow-md', className]}>
|
||||
<slot />
|
||||
</nav>
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ const IconWrapper = url ? 'a' : 'div';
|
|||
>
|
||||
<Icon client:load name={icon} color={iconColor} size={20} />
|
||||
<Typography variant="tile-subtitle">
|
||||
<span class="text-gray-700">{name}</span>
|
||||
<span class:list={['text-gray-700', 'dark:text-gray-300']}>{name}</span>
|
||||
</Typography>
|
||||
</IconWrapper>
|
||||
{
|
||||
|
|
|
|||
|
|
@ -59,8 +59,8 @@ const section: Section = 'main';
|
|||
details.map(({ label: detailLabel, value }) => (
|
||||
<div class="w-fit">
|
||||
<Typography variant="paragraph">
|
||||
<span class="text-gray-700">{detailLabel}: </span>
|
||||
<span class="break-all">{value}</span>
|
||||
<span class:list={['text-gray-700', 'dark:text-gray-300']}>{detailLabel}: </span>
|
||||
<span class:list={['break-all', 'dark:text-gray-400']}>{value}</span>
|
||||
</Typography>
|
||||
</div>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
---
|
||||
import SidebarItem from '@/atoms/sidebar-item';
|
||||
import ThemeToggle from '@/atoms/theme-icon';
|
||||
import Sidebar from '@/organisms/sidebar.astro';
|
||||
import ExperienceSection from '@/sections/experience-section.astro';
|
||||
import FavoritesSection from '@/sections/favorites-section.astro';
|
||||
|
|
@ -28,7 +29,8 @@ const seoImage = seo.image ? seo.image : '/favicon.svg';
|
|||
<meta property="og:description" content={seo.description} />
|
||||
<meta property="og:image" content={seoImage} />
|
||||
</head>
|
||||
<body class="flex justify-center bg-gray-50">
|
||||
<body class="flex justify-center bg-gray-50 dark:bg-gray-900">
|
||||
<ThemeToggle client:only="react" />
|
||||
<div class="flex relative transform-none gap-8 w-full max-w-5xl px-2 py-3 sm:px-8 sm:py-12 lg:py-20">
|
||||
<div class="absolute z-40 -right-2">
|
||||
<Sidebar className="hidden xl:flex fixed">
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
import type { CircleFlags, Fa6Brands, Fa6Solid, Logos, SimpleIcons } from 'iconify-icon-names';
|
||||
import type { CircleFlags, Fa6Brands, Fa6Solid, Ri, SimpleIcons } from 'iconify-icon-names';
|
||||
|
||||
export type IconName = Fa6Brands | Fa6Solid | SimpleIcons | CircleFlags | Logos;
|
||||
export type IconName = Fa6Brands | Fa6Solid | SimpleIcons | CircleFlags | Ri;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ const colors = require('tailwindcss/colors');
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
darkMode: ['class'],
|
||||
theme: {
|
||||
colors: {
|
||||
transparent: 'transparent',
|
||||
|
|
|
|||
Loading…
Reference in a new issue