Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/nicolasgrajaleshoyos/portafolio/llms.txt

Use this file to discover all available pages before exploring further.

Portfolio Moderno implements dark mode using Tailwind CSS’s darkMode: 'class' strategy. Under this approach, dark-variant utility classes (e.g. dark:bg-dark, dark:text-light) only activate when the string "dark" is present as a class on the root <html> element. Adding or removing that class is the sole mechanism that switches the entire visual theme — no CSS variables, no separate stylesheets. The useTheme custom hook in App.tsx is the single owner of this responsibility: it reads the user’s preference on first load, keeps React state and the DOM in sync, and persists the choice to localStorage so it survives page reloads.

Color Tokens

These custom color tokens are defined in tailwind.config.js under theme.extend.colors and are available throughout the project as Tailwind utility classes (e.g. bg-primary, text-medium, dark:bg-dark-secondary).
TokenValueUsage
primary#06b6d4Cyan 500 — links, highlights, skill badges, CTA button
primary-hover#0891b2Cyan 600 — hover state on primary elements
dark#0f172aSlate 900 — dark mode page background and Footer background
dark-secondary#1e293bSlate 800 — card and section background in dark mode
medium#64748bSlate 500 — muted body text in light mode
light#f1f5f9Slate 100 — heading and body text color on dark backgrounds

Typography

Two Google Fonts families are loaded via the <head> of index.html and mapped to Tailwind’s fontFamily extension in tailwind.config.js:
Tailwind classFont familyRole
font-sansInterBody copy, navigation labels, paragraph text
font-headingPoppinsSection headings (h1h3), logo wordmark, card titles
// tailwind.config.js (excerpt)
fontFamily: {
  sans: ['Inter', 'sans-serif'],
  heading: ['Poppins', 'sans-serif'],
},
Both families are declared in Tailwind’s extend block so they augment — rather than replace — the default font stack, keeping all built-in utilities intact.

Custom Animations

All custom keyframes and their animation shorthand utilities are defined in tailwind.config.js under theme.extend. They are used with standard Tailwind animate-* classes in component JSX.
NameDurationDescription
fade-in-up0.8sOpacity 0 → 1, translateY(20px) → 0 — general entrance animation
gradient-shift15sBackground position 0% → 100% → 0% — animated Hero background gradient
blink1sOpacity steps between 1 and 0 — blinking cursor in the typing effect
tilt10sRotation 0° → 1° → 0° → −1° → 0° — subtle tilt loop on the glow ring
float6stranslateY(0) → translateY(−10px) → translateY(0) — gentle vertical drift on profile images
// tailwind.config.js (excerpt)
animation: {
  'fade-in-up':     'fadeInUp 0.8s ease-out forwards',
  'gradient-shift': 'gradientShift 15s ease infinite',
  'blink':          'blink 1s steps(1) infinite',
  'tilt':           'tilt 10s infinite linear',
  'float':          'float 6s ease-in-out infinite',
},
keyframes: {
  fadeInUp: {
    '0%':   { opacity: '0', transform: 'translateY(20px)' },
    '100%': { opacity: '1', transform: 'translateY(0)' },
  },
  gradientShift: {
    '0%, 100%': { backgroundPosition: '0% 50%' },
    '50%':      { backgroundPosition: '100% 50%' },
  },
  blink: {
    '50%': { opacity: '0' },
  },
  tilt: {
    '0%, 100%': { transform: 'rotate(0deg)' },
    '25%':      { transform: 'rotate(1deg)' },
    '75%':      { transform: 'rotate(-1deg)' },
  },
  float: {
    '0%, 100%': { transform: 'translateY(0px)' },
    '50%':      { transform: 'translateY(-10px)' },
  },
},

useTheme Hook

The useTheme hook in App.tsx is the single source of truth for the active theme. It uses two useEffect calls with deliberately separate concerns: initialization runs once on mount, and the sync effect re-runs every time theme changes.
type Theme = 'light' | 'dark';

const useTheme = (): [Theme, () => void] => {
  const [theme, setTheme] = useState<Theme>('light');

  // Effect 1 — runs once on mount.
  // Priority: localStorage value → OS prefers-color-scheme → default 'light'
  useEffect(() => {
    const storedTheme = localStorage.getItem('theme') as Theme | null;
    const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
    const initialTheme = storedTheme || (prefersDark ? 'dark' : 'light');
    setTheme(initialTheme);
  }, []);

  // Effect 2 — runs on every theme change.
  // Syncs the <html> class and persists to localStorage.
  useEffect(() => {
    if (theme === 'dark') {
      document.documentElement.classList.add('dark');
      localStorage.setItem('theme', 'dark');
    } else {
      document.documentElement.classList.remove('dark');
      localStorage.setItem('theme', 'light');
    }
  }, [theme]);

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return [theme, toggleTheme];
};
Initialization order:
  1. Stored value — if localStorage contains a "theme" key set by a previous visit, that value wins.
  2. OS preference — if no stored value exists, window.matchMedia('(prefers-color-scheme: dark)').matches is checked.
  3. Default — if neither condition is met, 'light' is used.
The animate-float and animate-tilt classes on the Hero and About profile images are paired with motion-reduce:animate-none. This Tailwind variant disables those animations automatically for users who have enabled Reduce Motion in their operating system’s accessibility settings, ensuring Portfolio Moderno respects the prefers-reduced-motion media query without any additional JavaScript.

Build docs developers (and LLMs) love