feat: integrate Tailwind CSS and update theme context for improved styling

This commit is contained in:
Marco Beretta 2025-07-18 18:54:03 +02:00
parent 1c3f5b972d
commit 6e7fdeb3a3
No known key found for this signature in database
GPG key ID: D918033D8E74CC11
8 changed files with 476 additions and 143 deletions

View file

@ -4,6 +4,7 @@ export * from './AlertDialog';
export * from './Breadcrumb';
export * from './Button';
export * from './Checkbox';
export * from './Collapsible';
export * from './DataTableColumnHeader';
export * from './Dialog';
export * from './DropdownMenu';

View file

@ -1,7 +1,5 @@
//ThemeContext.js
// source: https://plainenglish.io/blog/light-and-dark-mode-in-react-web-application-with-tailwind-css-89674496b942
import { useSetAtom } from 'jotai';
import React, { createContext, useState, useEffect } from 'react';
import { useSetAtom } from 'jotai';
import { getInitialTheme, applyFontSize } from '~/utils';
import { fontSizeAtom } from '~/store';
@ -10,21 +8,10 @@ type ProviderValue = {
setTheme: React.Dispatch<React.SetStateAction<string>>;
};
const defaultContextValue: ProviderValue = {
theme: getInitialTheme(),
setTheme: () => {
return;
},
};
export const isDark = (theme: string): boolean => {
if (theme === 'system') {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
return theme === 'dark';
};
export const ThemeContext = createContext<ProviderValue>(defaultContextValue);
export const ThemeContext = createContext<ProviderValue>({
theme: 'light',
setTheme: () => {},
});
export const ThemeProvider = ({
initialTheme,
@ -33,56 +20,35 @@ export const ThemeProvider = ({
initialTheme?: string;
children: React.ReactNode;
}) => {
const [theme, setTheme] = useState(getInitialTheme);
const [theme, setTheme] = useState<string>(() => initialTheme ?? getInitialTheme());
const setFontSize = useSetAtom(fontSizeAtom);
const rawSetTheme = (rawTheme: string) => {
const root = window.document.documentElement;
const darkMode = isDark(rawTheme);
root.classList.remove(darkMode ? 'light' : 'dark');
root.classList.add(darkMode ? 'dark' : 'light');
localStorage.setItem('color-theme', rawTheme);
};
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const changeThemeOnSystemChange = () => {
rawSetTheme(mediaQuery.matches ? 'dark' : 'light');
};
const root = document.documentElement;
const darkMode =
theme === 'system'
? window.matchMedia('(prefers-color-scheme: dark)').matches
: theme === 'dark';
mediaQuery.addEventListener('change', changeThemeOnSystemChange);
return () => {
mediaQuery.removeEventListener('change', changeThemeOnSystemChange);
};
}, []);
useEffect(() => {
const fontSize = localStorage.getItem('fontSize');
if (fontSize == null) {
setFontSize('text-base');
applyFontSize('text-base');
localStorage.setItem('fontSize', JSON.stringify('text-base'));
return;
}
try {
applyFontSize(JSON.parse(fontSize));
} catch (error) {
console.log(error);
}
// Reason: This effect should only run once, and `setFontSize` is a stable function
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (initialTheme) {
rawSetTheme(initialTheme);
}
useEffect(() => {
rawSetTheme(theme);
root.classList.toggle('dark', darkMode);
root.classList.toggle('light', !darkMode);
localStorage.setItem('color-theme', theme);
}, [theme]);
useEffect(() => {
if (theme !== 'system') return;
const mq = window.matchMedia('(prefers-color-scheme: dark)');
const handler = (e: MediaQueryListEvent) => setTheme(e.matches ? 'dark' : 'light');
mq.addEventListener('change', handler);
return () => mq.removeEventListener('change', handler);
}, [theme]);
useEffect(() => {
const saved = localStorage.getItem('fontSize') || 'text-base';
applyFontSize(saved);
setFontSize(saved);
}, [setFontSize]);
return <ThemeContext.Provider value={{ theme, setTheme }}>{children}</ThemeContext.Provider>;
};

View file

@ -0,0 +1,172 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom Variables */
:root {
--white: #fff;
--black: #000;
--gray-20: #ececf1;
--gray-50: #f7f7f8;
--gray-100: #ececec;
--gray-200: #e3e3e3;
--gray-300: #cdcdcd;
--gray-400: #999696;
--gray-500: #595959;
--gray-600: #424242;
--gray-700: #2f2f2f;
--gray-800: #212121;
--gray-850: #171717;
--gray-900: #0d0d0d;
--green-50: #ecfdf5;
--green-100: #d1fae5;
--green-200: #a7f3d0;
--green-300: #6ee7b7;
--green-400: #34d399;
--green-500: #10b981;
--green-600: #059669;
--green-700: #047857;
--green-800: #065f46;
--green-900: #064e3b;
--green-950: #022c22;
--red-50: #fef2f2;
--red-100: #fee2e2;
--red-200: #fecaca;
--red-300: #fca5a5;
--red-400: #f87171;
--red-500: #ef4444;
--red-600: #dc2626;
--red-700: #b91c1c;
--red-800: #991b1b;
--red-900: #7f1d1d;
--red-950: #450a0a;
--amber-50: #fffbeb;
--amber-100: #fef3c7;
--amber-200: #fde68a;
--amber-300: #fcd34d;
--amber-400: #fbbf24;
--amber-500: #f59e0b;
--amber-600: #d97706;
--amber-700: #b45309;
--amber-800: #92400e;
--amber-900: #78350f;
--amber-950: #451a03;
}
html {
--presentation: var(--white);
--text-primary: var(--gray-800);
--text-secondary: var(--gray-600);
--text-secondary-alt: var(--gray-500);
--text-tertiary: var(--gray-500);
--text-warning: var(--amber-500);
--ring-primary: var(--gray-500);
--header-primary: var(--white);
--header-hover: var(--gray-50);
--header-button-hover: var(--gray-50);
--surface-active: var(--gray-100);
--surface-active-alt: var(--gray-200);
--surface-hover: var(--gray-200);
--surface-hover-alt: var(--gray-300);
--surface-primary: var(--white);
--surface-primary-alt: var(--gray-50);
--surface-primary-contrast: var(--gray-100);
--surface-secondary: var(--gray-50);
--surface-secondary-alt: var(--gray-200);
--surface-tertiary: var(--gray-100);
--surface-tertiary-alt: var(--white);
--surface-dialog: var(--white);
--surface-submit: var(--green-700);
--surface-submit-hover: var(--green-800);
--surface-destructive: var(--red-700);
--surface-destructive-hover: var(--red-800);
--surface-chat: var(--white);
--border-light: var(--gray-200);
--border-medium-alt: var(--gray-300);
--border-medium: var(--gray-300);
--border-heavy: var(--gray-400);
--border-xheavy: var(--gray-500);
--background: 0 0% 100%;
--foreground: 0 0% 3.9%;
--card: 0 0% 100%;
--card-foreground: 0 0% 3.9%;
--primary: 0 0% 9%;
--primary-foreground: 0 0% 98%;
--secondary: 0 0% 96.1%;
--secondary-foreground: 0 0% 9%;
--muted: 0 0% 96.1%;
--muted-foreground: 0 0% 45.1%;
--accent: 0 0% 96.1%;
--accent-foreground: 0 0% 9%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%;
--input: 0 0% 89.8%;
--ring: 0 0% 3.9%;
--radius: 0.5rem;
--chart-1: 12 76% 61%;
--chart-2: 173 58% 39%;
--chart-3: 197 37% 24%;
--chart-4: 43 74% 66%;
--chart-5: 27 87% 67%;
--switch-unchecked: 0 0% 58%;
}
.dark {
--presentation: var(--gray-800);
--text-primary: var(--gray-100);
--text-secondary: var(--gray-300);
--text-secondary-alt: var(--gray-400);
--text-tertiary: var(--gray-500);
--text-warning: var(--amber-500);
--header-primary: var(--gray-700);
--header-hover: var(--gray-600);
--header-button-hover: var(--gray-700);
--surface-active: var(--gray-500);
--surface-active-alt: var(--gray-700);
--surface-hover: var(--gray-600);
--surface-hover-alt: var(--gray-600);
--surface-primary: var(--gray-900);
--surface-primary-alt: var(--gray-850);
--surface-primary-contrast: var(--gray-850);
--surface-secondary: var(--gray-800);
--surface-secondary-alt: var(--gray-800);
--surface-tertiary: var(--gray-700);
--surface-tertiary-alt: var(--gray-700);
--surface-dialog: var(--gray-850);
--surface-submit: var(--green-700);
--surface-submit-hover: var(--green-800);
--surface-destructive: var(--red-800);
--surface-destructive-hover: var(--red-900);
--surface-chat: var(--gray-700);
--border-light: var(--gray-700);
--border-medium-alt: var(--gray-600);
--border-medium: var(--gray-600);
--border-heavy: var(--gray-500);
--border-xheavy: var(--gray-400);
--background: 0 0% 7%;
--foreground: 0 0% 98%;
--card: 0 0% 3.9%;
--card-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 9%;
--secondary: 0 0% 14.9%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 14.9%;
--muted-foreground: 0 0% 63.9%;
--accent: 0 0% 14.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 40.6%;
--destructive-foreground: 0 0% 98%;
--border: 0 0% 14.9%;
--input: 0 0% 14.9%;
--ring: 0 0% 83.1%;
--chart-1: 220 70% 50%;
--chart-2: 160 60% 45%;
--chart-3: 30 80% 55%;
--chart-4: 280 65% 60%;
--chart-5: 340 75% 55%;
--switch-unchecked: 0 0% 40%;
}

View file

@ -1,3 +1,6 @@
// Styles
import './index.css';
// Components
export * from './components';