diff --git a/website/src/app/[countryId]/research/[slug]/ArticleClient.tsx b/website/src/app/[countryId]/research/[slug]/ArticleClient.tsx
index b4e636cb2..870d2c85e 100644
--- a/website/src/app/[countryId]/research/[slug]/ArticleClient.tsx
+++ b/website/src/app/[countryId]/research/[slug]/ArticleClient.tsx
@@ -25,11 +25,7 @@ import {
} from "@/data/posts/postTransformers";
import { extractMarkdownFromNotebook } from "@/lib/notebookUtils";
import authorsData from "@/data/posts/authors.json";
-import {
- colors,
- spacing,
- typography,
-} from "@/designTokens";
+import { colors, spacing, typography } from "@/designTokens";
const authors = authorsData as AuthorsCollection;
@@ -90,7 +86,7 @@ export default function ArticleClient({
return (
<>
{/* Header section */}
-
+
{readingTime}
@@ -193,7 +189,7 @@ function PostHeading({
style={{
marginTop: spacing["3xl"],
fontSize: typography.fontSize.xl,
- color: colors.gray[500],
+ color: colors.text.secondary,
}}
>
{post.description}
@@ -239,7 +235,7 @@ function PostHeading({
{post.description}
@@ -265,7 +261,7 @@ function PostHeading({
{readingTime}
@@ -327,7 +323,7 @@ function PostBody({
size="sm"
fw={typography.fontWeight.semibold}
className="tw:uppercase tw:mb-xs"
- style={{ letterSpacing: "0.1em", color: colors.primary[600] }}
+ style={{ letterSpacing: "0.1em", color: colors.text.link }}
>
Contents
@@ -362,7 +358,7 @@ function PostBody({
size="sm"
fw={typography.fontWeight.semibold}
className="tw:uppercase tw:mb-xs"
- style={{ letterSpacing: "0.1em", color: colors.primary[600] }}
+ style={{ letterSpacing: "0.1em", color: colors.text.link }}
>
Contents
@@ -387,7 +383,7 @@ function PostBody({
size="sm"
fw={typography.fontWeight.semibold}
className="tw:uppercase tw:mb-xs"
- style={{ letterSpacing: "0.1em", color: colors.primary[600] }}
+ style={{ letterSpacing: "0.1em", color: colors.text.link }}
>
Contents
@@ -418,7 +414,7 @@ function Authorship({
@@ -511,14 +507,14 @@ function AuthorSection({
{formatAuthorName(authorId)}
-
+
{author.title}
@@ -547,7 +543,7 @@ function MoreOn({ post, countryId }: { post: BlogPost; countryId: string }) {
{
- e.currentTarget.style.color = colors.primary[600];
+ e.currentTarget.style.color = colors.text.link;
}}
onMouseLeave={(e) => {
- e.currentTarget.style.color = colors.gray[700];
+ e.currentTarget.style.color = colors.text.secondary;
}}
>
{label}
@@ -575,7 +571,7 @@ function MoreOn({ post, countryId }: { post: BlogPost; countryId: string }) {
size="sm"
fw={typography.fontWeight.semibold}
className="tw:uppercase tw:mb-xs"
- style={{ letterSpacing: "0.1em", color: colors.primary[600] }}
+ style={{ letterSpacing: "0.1em", color: colors.text.link }}
>
More on
@@ -591,8 +587,7 @@ function ShareLinks({
post: BlogPost;
displayCategory: string;
}) {
- const currentUrl =
- typeof window !== "undefined" ? window.location.href : "";
+ const currentUrl = typeof window !== "undefined" ? window.location.href : "";
const desktop = displayCategory === "desktop";
const links = [
@@ -646,7 +641,7 @@ function ShareLinks({
display: "flex",
alignItems: "center",
gap: spacing.sm,
- color: colors.gray[600],
+ color: colors.text.secondary,
textDecoration: "none",
fontSize: typography.fontSize.xs,
}}
@@ -659,11 +654,9 @@ function ShareLinks({
display: "flex",
alignItems: "center",
justifyContent: "center",
- backgroundColor: desktop ? colors.gray[500] : "transparent",
- color: desktop ? colors.white : colors.gray[600],
- border: desktop
- ? "none"
- : `1px solid ${colors.gray[400]}`,
+ backgroundColor: desktop ? colors.primary[500] : "transparent",
+ color: desktop ? colors.text.inverse : colors.text.secondary,
+ border: desktop ? "none" : `1px solid ${colors.border.medium}`,
fontSize: typography.fontSize.xs,
fontWeight: typography.fontWeight.semibold,
}}
@@ -708,7 +701,7 @@ function LeftContents({ markdown }: { markdown: string }) {
cursor: "pointer",
paddingLeft: 8 * (level - 2),
padding: "2px 0",
- color: colors.gray[700],
+ color: colors.text.secondary,
transition: "color 0.2s ease",
}}
onClick={() => {
@@ -723,10 +716,10 @@ function LeftContents({ markdown }: { markdown: string }) {
}
}}
onMouseEnter={(e) => {
- e.currentTarget.style.color = colors.primary[600];
+ e.currentTarget.style.color = colors.text.link;
}}
onMouseLeave={(e) => {
- e.currentTarget.style.color = colors.gray[700];
+ e.currentTarget.style.color = colors.text.secondary;
}}
>
{text}
diff --git a/website/src/app/globals.css b/website/src/app/globals.css
index acf657ad5..2a6a4d55a 100644
--- a/website/src/app/globals.css
+++ b/website/src/app/globals.css
@@ -1,4 +1,4 @@
-@import 'tailwindcss' prefix(tw);
+@import "tailwindcss" prefix(tw);
@import "@policyengine/ui-kit/styles.css";
@theme {
@@ -83,8 +83,9 @@
--color-ring: #319795;
/* Typography */
- --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- --font-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
+ --font-sans:
+ "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
+ --font-mono: "JetBrains Mono", "Fira Code", Consolas, monospace;
--font-size-xs: 12px;
--font-size-sm: 14px;
@@ -134,6 +135,112 @@
--spacing-container: 976px;
}
+:root,
+:root[data-pe-theme="light"] {
+ color-scheme: light;
+ --pe-color-surface-accent: #e6fffa;
+ --pe-color-surface-overlay: rgb(255 255 255 / 0.9);
+ --pe-color-surface-hover: #f9fafb;
+ --pe-shadow-light: rgb(16 24 40 / 0.05);
+ --pe-shadow-medium: rgb(16 24 40 / 0.1);
+ --pe-shadow-dark: rgb(16 24 40 / 0.2);
+
+ /* Compatibility variables for existing arbitrary Tailwind values. */
+ --color-primary-400: var(--tw-color-primary-400);
+ --color-primary-500: var(--tw-color-primary-500);
+ --color-primary-600: var(--tw-color-primary-600);
+}
+
+:root[data-pe-theme="dark"] {
+ color-scheme: dark;
+
+ --tw-color-primary-50: #0f302f;
+ --tw-color-primary-100: #15504e;
+ --tw-color-primary-200: #1f6f6c;
+ --tw-color-primary-300: #2b918d;
+ --tw-color-primary-400: #38b2ac;
+ --tw-color-primary-500: #4fd1c5;
+ --tw-color-primary-600: #63d9cf;
+ --tw-color-primary-700: #81e6d9;
+ --tw-color-primary-800: #b2f5ea;
+ --tw-color-primary-900: #e6fffa;
+
+ --tw-color-secondary-50: #101a1c;
+ --tw-color-secondary-100: #142225;
+ --tw-color-secondary-200: #1e3337;
+ --tw-color-secondary-300: #2b484d;
+ --tw-color-secondary-400: #49666a;
+ --tw-color-secondary-500: #73898b;
+ --tw-color-secondary-600: #9fb0b0;
+ --tw-color-secondary-700: #c5d0cf;
+ --tw-color-secondary-800: #e1e8e7;
+ --tw-color-secondary-900: #f5f8f8;
+
+ --tw-color-gray-50: #0d1719;
+ --tw-color-gray-100: #132124;
+ --tw-color-gray-200: #1d3034;
+ --tw-color-gray-300: #2c454a;
+ --tw-color-gray-400: #587276;
+ --tw-color-gray-500: #8ca0a2;
+ --tw-color-gray-600: #b5c4c5;
+ --tw-color-gray-700: #d3dddc;
+ --tw-color-gray-800: #ebf0ef;
+ --tw-color-gray-900: #f7fafa;
+
+ --tw-color-bg-primary: #071112;
+ --tw-color-bg-secondary: #0d1c1e;
+ --tw-color-bg-tertiary: #13272a;
+ --tw-color-text-primary: #f7fafa;
+ --tw-color-text-secondary: #bdcaca;
+ --tw-color-text-tertiary: #899fa1;
+ --tw-color-text-inverse: #061314;
+ --tw-color-text-link: #63d9cf;
+ --tw-color-text-link-hover: #81e6d9;
+ --tw-color-border-light: #20383c;
+ --tw-color-border-medium: #315257;
+ --tw-color-border-dark: #537276;
+
+ --tw-color-background: #071112;
+ --tw-color-foreground: #f7fafa;
+ --tw-color-primary: #4fd1c5;
+ --tw-color-primary-foreground: #061314;
+ --tw-color-secondary: #132124;
+ --tw-color-secondary-foreground: #f7fafa;
+ --tw-color-muted: #132124;
+ --tw-color-muted-foreground: #bdcaca;
+ --tw-color-accent: #173235;
+ --tw-color-accent-foreground: #f7fafa;
+ --tw-color-popover: #0d1c1e;
+ --tw-color-popover-foreground: #f7fafa;
+ --tw-color-card: #0d1c1e;
+ --tw-color-card-foreground: #f7fafa;
+ --tw-color-border: #20383c;
+ --tw-color-input: #20383c;
+ --tw-color-ring: #4fd1c5;
+
+ --pe-color-surface-accent: #102d2c;
+ --pe-color-surface-overlay: rgb(13 28 30 / 0.9);
+ --pe-color-surface-hover: #173235;
+ --pe-shadow-light: rgb(0 0 0 / 0.28);
+ --pe-shadow-medium: rgb(0 0 0 / 0.36);
+ --pe-shadow-dark: rgb(0 0 0 / 0.48);
+}
+
+:root[data-pe-theme="dark"] .tw\:bg-white {
+ background-color: var(--tw-color-card);
+}
+
+:root[data-pe-theme="dark"] .tw\:text-gray-900,
+:root[data-pe-theme="dark"] .tw\:text-gray-800,
+:root[data-pe-theme="dark"] .tw\:text-gray-700 {
+ color: var(--tw-color-foreground);
+}
+
+:root[data-pe-theme="dark"] .tw\:bg-gray-50,
+:root[data-pe-theme="dark"] .tw\:bg-gray-100 {
+ background-color: var(--tw-color-bg-secondary);
+}
+
@layer base {
*,
::before,
@@ -166,7 +273,7 @@
width: 100%;
}
- [data-slot='table-container'],
+ [data-slot="table-container"],
tr {
background-color: var(--tw-color-bg-primary);
}
@@ -185,13 +292,13 @@
background-color: var(--tw-color-bg-primary);
}
- input[type='number']::-webkit-inner-spin-button,
- input[type='number']::-webkit-outer-spin-button {
+ input[type="number"]::-webkit-inner-spin-button,
+ input[type="number"]::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
- input[type='number'] {
+ input[type="number"] {
-moz-appearance: textfield;
}
}
diff --git a/website/src/app/layout.tsx b/website/src/app/layout.tsx
index 41aaa2447..01bc947bc 100644
--- a/website/src/app/layout.tsx
+++ b/website/src/app/layout.tsx
@@ -2,9 +2,26 @@ import type { Metadata } from "next";
import Script from "next/script";
import { Analytics } from "@vercel/analytics/react";
import { SpeedInsights } from "@vercel/speed-insights/next";
+import { ThemeProvider } from "@/contexts/ThemeContext";
import "./globals.css";
const GA_MEASUREMENT_ID = "G-2YHG89FY0N";
+const THEME_INIT_SCRIPT = `
+(function() {
+ try {
+ var storedPreference = window.localStorage.getItem('policyengine-theme');
+ var preference = storedPreference === 'light' || storedPreference === 'dark'
+ ? storedPreference
+ : 'system';
+ var theme = preference === 'system'
+ ? (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light')
+ : preference;
+
+ document.documentElement.dataset.peTheme = theme;
+ document.documentElement.style.colorScheme = theme;
+ } catch (error) {}
+})();
+`;
export const metadata: Metadata = {
metadataBase: new URL("https://www.policyengine.org"),
@@ -25,8 +42,14 @@ export default function RootLayout({
children: React.ReactNode;
}) {
return (
-
+
+
+