diff --git a/src/App.tsx b/src/App.tsx index 77b045d..9342bfd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -25,6 +25,9 @@ function App() { const [stories, setStories] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) + const [cvdMode, setCvdMode] = useState( + localStorage.getItem('cvd-preference') + ) useEffect(() => { const fetchTopStories = async () => { @@ -69,6 +72,17 @@ function App() { fetchTopStories() }, []) + const handleCvdChange = (mode: string | null) => { + setCvdMode(mode) + if (mode) { + document.body.setAttribute('data-cvd', mode) + localStorage.setItem('cvd-preference', mode) + } else { + document.body.removeAttribute('data-cvd') + localStorage.removeItem('cvd-preference') + } + } + const formatTime = (timestamp: number) => { const now = Date.now() / 1000 const diff = now - timestamp @@ -94,36 +108,48 @@ function App() { if (loading) { return ( -
-

Loading...

+
+

Loading...

) } if (error) { return ( -
-

{error}

+
+

{error}

) } return ( -
-
+
+
-
-

- Calm HN -

-

- Top stories from the last three months -

+
+
+

+ Calm HN +

+

+ Top stories from the last three months +

+
+
{stories.map((story, index) => ( -
+
- + {index + 1} -

+

{story.title} {story.url && ( @@ -144,7 +170,7 @@ function App() {

-
+
{story.score} diff --git a/src/index.css b/src/index.css index b329b2a..f34bc63 100644 --- a/src/index.css +++ b/src/index.css @@ -8,6 +8,153 @@ src: url("./InterVariable.woff2") format("woff2"); } +/* Light theme (default) */ +:root { + --bg-primary: 250 250 250; /* neutral-50 */ + --bg-secondary: 241 245 249; /* slate-100 */ + --bg-badge: 226 232 240; /* slate-200 */ + --bg-badge-hover: 254 215 170; /* orange-200 */ + --gradient-from: 254 215 170; /* orange-200 */ + --text-primary: 15 23 42; /* slate-900 */ + --text-secondary: 100 116 139; /* slate-500 */ + --text-tertiary: 148 163 184; /* slate-400 */ + --text-badge: 100 116 139; /* slate-500 */ + --text-badge-hover: 71 85 105; /* slate-600 */ + --text-muted: 203 213 225; /* slate-300 */ +} + +/* Dark theme */ +@media (prefers-color-scheme: dark) { + :root { + --bg-primary: 15 23 42; /* slate-900 */ + --bg-secondary: 30 41 59; /* slate-800 */ + --bg-badge: 51 65 85; /* slate-700 */ + --bg-badge-hover: 194 65 12; /* orange-700 */ + --gradient-from: 194 65 12; /* orange-700 */ + --text-primary: 248 250 252; /* slate-50 */ + --text-secondary: 203 213 225; /* slate-300 */ + --text-tertiary: 148 163 184; /* slate-400 */ + --text-badge: 203 213 225; /* slate-300 */ + --text-badge-hover: 226 232 240; /* slate-200 */ + --text-muted: 71 85 105; /* slate-600 */ + } +} + +/* High contrast light theme */ +@media (prefers-contrast: more) and (prefers-color-scheme: light) { + :root { + --bg-primary: 255 255 255; /* white */ + --bg-secondary: 226 232 240; /* slate-200 */ + --bg-badge: 203 213 225; /* slate-300 */ + --bg-badge-hover: 251 146 60; /* orange-400 */ + --gradient-from: 251 146 60; /* orange-400 */ + --text-primary: 0 0 0; /* black */ + --text-secondary: 30 41 59; /* slate-800 */ + --text-tertiary: 71 85 105; /* slate-600 */ + --text-badge: 30 41 59; /* slate-800 */ + --text-badge-hover: 0 0 0; /* black */ + --text-muted: 148 163 184; /* slate-400 */ + } +} + +/* High contrast dark theme */ +@media (prefers-contrast: more) and (prefers-color-scheme: dark) { + :root { + --bg-primary: 0 0 0; /* black */ + --bg-secondary: 30 41 59; /* slate-800 */ + --bg-badge: 71 85 105; /* slate-600 */ + --bg-badge-hover: 255 140 0; /* bright orange */ + --gradient-from: 255 140 0; /* bright orange */ + --text-primary: 255 255 255; /* white */ + --text-secondary: 226 232 240; /* slate-200 */ + --text-tertiary: 203 213 225; /* slate-300 */ + --text-badge: 241 245 249; /* slate-100 */ + --text-badge-hover: 255 255 255; /* white */ + --text-muted: 100 116 139; /* slate-500 */ + } +} + +/* Protanopia & Deuteranopia light theme (red-green color blindness) */ +/* Uses blue-yellow contrasts instead of red-green */ +@media (prefers-color-scheme: light) { + @supports (color: color(display-p3 1 1 1)) { + :root:has(body[data-cvd="protanopia"]), + :root:has(body[data-cvd="deuteranopia"]) { + --bg-primary: 250 250 250; /* neutral-50 */ + --bg-secondary: 241 245 249; /* slate-100 */ + --bg-badge: 219 234 254; /* blue-100 */ + --bg-badge-hover: 147 197 253; /* blue-300 */ + --gradient-from: 147 197 253; /* blue-300 */ + --text-primary: 15 23 42; /* slate-900 */ + --text-secondary: 71 85 105; /* slate-600 */ + --text-tertiary: 100 116 139; /* slate-500 */ + --text-badge: 30 64 175; /* blue-800 */ + --text-badge-hover: 30 58 138; /* blue-900 */ + --text-muted: 203 213 225; /* slate-300 */ + } + } +} + +/* Protanopia & Deuteranopia dark theme */ +@media (prefers-color-scheme: dark) { + @supports (color: color(display-p3 1 1 1)) { + :root:has(body[data-cvd="protanopia"]), + :root:has(body[data-cvd="deuteranopia"]) { + --bg-primary: 15 23 42; /* slate-900 */ + --bg-secondary: 30 41 59; /* slate-800 */ + --bg-badge: 30 58 138; /* blue-900 */ + --bg-badge-hover: 59 130 246; /* blue-500 */ + --gradient-from: 59 130 246; /* blue-500 */ + --text-primary: 248 250 252; /* slate-50 */ + --text-secondary: 203 213 225; /* slate-300 */ + --text-tertiary: 148 163 184; /* slate-400 */ + --text-badge: 147 197 253; /* blue-300 */ + --text-badge-hover: 191 219 254; /* blue-200 */ + --text-muted: 71 85 105; /* slate-600 */ + } + } +} + +/* Protanopia & Deuteranopia high contrast light */ +@media (prefers-contrast: more) and (prefers-color-scheme: light) { + @supports (color: color(display-p3 1 1 1)) { + :root:has(body[data-cvd="protanopia"]), + :root:has(body[data-cvd="deuteranopia"]) { + --bg-primary: 255 255 255; /* white */ + --bg-secondary: 226 232 240; /* slate-200 */ + --bg-badge: 191 219 254; /* blue-200 */ + --bg-badge-hover: 96 165 250; /* blue-400 */ + --gradient-from: 96 165 250; /* blue-400 */ + --text-primary: 0 0 0; /* black */ + --text-secondary: 30 41 59; /* slate-800 */ + --text-tertiary: 51 65 85; /* slate-700 */ + --text-badge: 29 78 216; /* blue-700 */ + --text-badge-hover: 0 0 0; /* black */ + --text-muted: 148 163 184; /* slate-400 */ + } + } +} + +/* Protanopia & Deuteranopia high contrast dark */ +@media (prefers-contrast: more) and (prefers-color-scheme: dark) { + @supports (color: color(display-p3 1 1 1)) { + :root:has(body[data-cvd="protanopia"]), + :root:has(body[data-cvd="deuteranopia"]) { + --bg-primary: 0 0 0; /* black */ + --bg-secondary: 30 41 59; /* slate-800 */ + --bg-badge: 29 78 216; /* blue-700 */ + --bg-badge-hover: 96 165 250; /* blue-400 */ + --gradient-from: 96 165 250; /* blue-400 */ + --text-primary: 255 255 255; /* white */ + --text-secondary: 226 232 240; /* slate-200 */ + --text-tertiary: 203 213 225; /* slate-300 */ + --text-badge: 219 234 254; /* blue-100 */ + --text-badge-hover: 255 255 255; /* white */ + --text-muted: 100 116 139; /* slate-500 */ + } + } +} + body { font-family: "InterVariable", sans-serif; } \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx index bef5202..cd21b50 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,20 @@ import { createRoot } from 'react-dom/client' import './index.css' import App from './App.tsx' +// Detect color vision deficiency preference +// Users can set this via browser extension or URL parameter +const params = new URLSearchParams(window.location.search) +const cvdParam = params.get('cvd') +if (cvdParam === 'protanopia' || cvdParam === 'deuteranopia') { + document.body.setAttribute('data-cvd', cvdParam) + localStorage.setItem('cvd-preference', cvdParam) +} else { + const stored = localStorage.getItem('cvd-preference') + if (stored === 'protanopia' || stored === 'deuteranopia') { + document.body.setAttribute('data-cvd', stored) + } +} + createRoot(document.getElementById('root')!).render(