diff --git a/app/src/WebsiteApp.tsx b/app/src/WebsiteApp.tsx index 090624a77..5b6af9d1a 100644 --- a/app/src/WebsiteApp.tsx +++ b/app/src/WebsiteApp.tsx @@ -9,6 +9,7 @@ import { Provider } from 'react-redux'; import DevTools from '@/components/DevTools'; import { TooltipProvider } from '@/components/ui/tooltip'; import { AppProvider } from './contexts/AppContext'; +import { ThemeProvider } from './contexts/ThemeContext'; import { store } from './store'; import { cacheMonitor } from './utils/cacheMonitor'; import { WebsiteRouter } from './WebsiteRouter'; @@ -26,14 +27,16 @@ cacheMonitor.init(queryClient); export default function WebsiteApp() { return ( - - - - - - - - + + + + + + + + + + ); } diff --git a/app/src/app.css b/app/src/app.css index a658806fe..05819e209 100644 --- a/app/src/app.css +++ b/app/src/app.css @@ -161,6 +161,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); +} + /* * ========================================================= * Base styles diff --git a/app/src/components/IframeContent.tsx b/app/src/components/IframeContent.tsx index d776eecd3..459cbac0a 100644 --- a/app/src/components/IframeContent.tsx +++ b/app/src/components/IframeContent.tsx @@ -88,7 +88,7 @@ export default function IframeContent({ display: 'flex', alignItems: 'center', justifyContent: 'center', - backgroundColor: colors.gray[50], + backgroundColor: colors.background.secondary, zIndex: 1, }} > @@ -97,14 +97,14 @@ export default function IframeContent({ style={{ width: '48px', height: '48px', - border: `4px solid ${colors.gray[200]}`, + border: `4px solid ${colors.border.light}`, borderTop: `4px solid ${colors.primary[600]}`, borderRadius: '50%', margin: `0 auto ${spacing.lg}`, animation: 'spin 1s linear infinite', }} /> -

+

Loading calculator...

@@ -121,7 +121,7 @@ export default function IframeContent({ display: 'flex', alignItems: 'center', justifyContent: 'center', - backgroundColor: colors.gray[50], + backgroundColor: colors.background.secondary, zIndex: 1, }} > @@ -132,10 +132,10 @@ export default function IframeContent({ maxWidth: '500px', }} > -

+

Unable to load calculator

-

+

The embedded calculator could not be loaded. You can try opening it directly:

{ + event.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.18)'; + event.currentTarget.style.borderColor = colors.primary[300]; + }} + onMouseLeave={(event) => { + event.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.1)'; + event.currentTarget.style.borderColor = colors.primary.alpha[40]; + }} + > +
-
+
- - - - - {copied ? "Copied!" : "Click to copy"} - + + + + + {copied ? "Copied!" : "Click to copy"} + ); } @@ -153,7 +154,7 @@ export default function BrandDesignClient() {
. Import from{" "} - - @tabler/icons-react - + @tabler/icons-react .

diff --git a/website/src/app/[countryId]/brand/writing/page.tsx b/website/src/app/[countryId]/brand/writing/page.tsx index 4a080ef27..85415b85a 100644 --- a/website/src/app/[countryId]/brand/writing/page.tsx +++ b/website/src/app/[countryId]/brand/writing/page.tsx @@ -2,11 +2,7 @@ import type { Metadata } from "next"; import Link from "next/link"; import { IconCheck, IconX } from "@tabler/icons-react"; import { Text, Title } from "@/components/ui"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; export const metadata: Metadata = { title: "Writing guide", @@ -74,9 +70,7 @@ function ExampleBox({
- How PolicyEngine communicates. Voice, tone, and content guidelines - for research-oriented writing. + How PolicyEngine communicates. Voice, tone, and content guidelines for + research-oriented writing.
@@ -255,16 +243,16 @@ export default function BrandWritingPage() { lineHeight: typography.lineHeight.relaxed, }} > - Always use sentence case for headings, not title case. This - follows the modern standard used by Apple, Google, Slack, Notion, - and GOV.UK. + Always use sentence case for headings, not title case. This follows + the modern standard used by Apple, Google, Slack, Notion, and + GOV.UK.
The rule - Capitalize only the first word of a heading and any proper - nouns. Everything else stays lowercase. + Capitalize only the first word of a heading and any proper nouns. + Everything else stays lowercase.
@@ -295,8 +283,7 @@ export default function BrandWritingPage() {
Proper nouns stay capitalized - Brand names, acronyms, and proper nouns keep their - capitalization. + Brand names, acronyms, and proper nouns keep their capitalization.
@@ -342,9 +329,7 @@ export default function BrandWritingPage() {
  • Navigation labels
  • Button text
  • Form labels
  • -
  • - Documentation titles -
  • +
  • Documentation titles
  • Blog post titles
  • Error messages
  • @@ -371,8 +356,8 @@ export default function BrandWritingPage() {
    Use active voice - Write in active voice. The subject should perform the action, - not receive it. + Write in active voice. The subject should perform the action, not + receive it.
    @@ -438,9 +423,9 @@ export default function BrandWritingPage() {
    Present numbers dispassionately - When presenting results from PolicyEngine, let the data speak - for itself. Avoid adjectives or adverbs that aren't backed - by the numbers themselves. + When presenting results from PolicyEngine, let the data speak for + itself. Avoid adjectives or adverbs that aren't backed by the + numbers themselves.
    @@ -454,13 +439,11 @@ export default function BrandWritingPage() {
    - The policy dramatically slashes poverty by an impressive - 15%. + The policy dramatically slashes poverty by an impressive 15%.
    Benefits skyrocket by a remarkable $2,400/year.
    - The reform has a surprisingly modest cost of just $80 - billion. + The reform has a surprisingly modest cost of just $80 billion.
    diff --git a/website/src/app/[countryId]/citations/CitationsClient.tsx b/website/src/app/[countryId]/citations/CitationsClient.tsx index 65ee5e3c7..4c8e6a58a 100644 --- a/website/src/app/[countryId]/citations/CitationsClient.tsx +++ b/website/src/app/[countryId]/citations/CitationsClient.tsx @@ -6,11 +6,7 @@ import OptimisedImage from "@/components/ui/OptimisedImage"; import { Container } from "@/components/ui/Container"; import { Text } from "@/components/ui/Text"; import citationsData from "@/data/citations.json"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; interface Citation { /** Headline or title of the citing article */ @@ -72,7 +68,7 @@ function CitationCard({ border: `1px solid ${colors.border.light}`, transition: "box-shadow 0.2s ease, transform 0.2s ease", cursor: "pointer", - backgroundColor: colors.white, + backgroundColor: colors.background.elevated, height: "100%", display: "flex", flexDirection: "column", @@ -85,7 +81,7 @@ function CitationCard({ flex: large ? "1 1 0" : undefined, aspectRatio: large ? undefined : "16 / 9", minHeight: large ? "200px" : undefined, - backgroundColor: colors.gray[100], + backgroundColor: colors.background.secondary, overflow: "hidden", }} > @@ -106,7 +102,7 @@ function CitationCard({ parent.style.alignItems = "center"; parent.style.justifyContent = "center"; parent.style.padding = "24px"; - parent.innerHTML = `${citation.outlet}`; + parent.innerHTML = `${citation.outlet}`; } }} /> @@ -127,7 +123,7 @@ function CitationCard({ {citation.title} - + {formatDate(citation.date)}
    @@ -176,8 +172,10 @@ function LocationFilter({ padding: `${spacing.xs} ${spacing.md}`, borderRadius: "999px", border: `1px solid ${isActive ? colors.primary[500] : colors.border.light}`, - backgroundColor: isActive ? colors.primary[500] : colors.white, - color: isActive ? colors.white : colors.gray[600], + backgroundColor: isActive + ? colors.primary[500] + : colors.background.elevated, + color: isActive ? colors.text.inverse : colors.text.secondary, fontSize: "14px", fontFamily: typography.fontFamily.primary, fontWeight: isActive ? 600 : 400, @@ -302,11 +300,13 @@ export default function CitationsClient({ countryId }: { countryId: string }) { className="tw:text-center" style={{ padding: spacing["3xl"], - backgroundColor: colors.gray[50], + backgroundColor: colors.background.secondary, borderRadius: spacing.radius.container, }} > - No citations yet. + + No citations yet. +
    ) : ( <> @@ -317,7 +317,7 @@ export default function CitationsClient({ countryId }: { countryId: string }) { size="lg" style={{ fontWeight: 600, - color: colors.gray[800], + color: colors.text.primary, marginBottom: spacing.xl, }} > @@ -334,7 +334,7 @@ export default function CitationsClient({ countryId }: { countryId: string }) { size="lg" style={{ fontWeight: 600, - color: colors.gray[800], + color: colors.text.primary, marginBottom: spacing.xl, }} > diff --git a/website/src/app/[countryId]/claude-plugin/ClaudePluginClient.tsx b/website/src/app/[countryId]/claude-plugin/ClaudePluginClient.tsx index 9d7ea4be1..d84395413 100644 --- a/website/src/app/[countryId]/claude-plugin/ClaudePluginClient.tsx +++ b/website/src/app/[countryId]/claude-plugin/ClaudePluginClient.tsx @@ -1,11 +1,7 @@ "use client"; import { useEffect, useRef, useState } from "react"; -import { - colors, - spacing, - typography, -} from "@/designTokens"; +import { colors, spacing, typography } from "@/designTokens"; /* ─── animation hook ─── */ @@ -438,7 +434,7 @@ export default function ClaudePluginClient({ style={{ paddingTop: 80, paddingBottom: 80, - backgroundColor: colors.white, + backgroundColor: colors.background.primary, ...SECTION_PX, borderBottom: `1px solid ${colors.border.light}`, overflow: "hidden", @@ -478,7 +474,7 @@ export default function ClaudePluginClient({ fontSize: "clamp(32px, 4.5vw, 48px)", fontWeight: typography.fontWeight.bold, fontFamily: typography.fontFamily.primary, - color: colors.gray[900], + color: colors.text.primary, lineHeight: 1.08, letterSpacing: "-0.03em", marginBottom: spacing.lg, @@ -512,7 +508,7 @@ export default function ClaudePluginClient({ style={{ display: "inline-block", backgroundColor: colors.primary[500], - color: colors.white, + color: colors.text.inverse, fontFamily: typography.fontFamily.primary, fontWeight: typography.fontWeight.semibold, fontSize: typography.fontSize.sm, @@ -586,7 +582,7 @@ export default function ClaudePluginClient({ style={{ paddingTop: spacing["4xl"], paddingBottom: spacing["4xl"], - backgroundColor: colors.gray[50], + backgroundColor: colors.background.secondary, ...SECTION_PX, borderBottom: `1px solid ${colors.border.light}`, }} @@ -619,7 +615,7 @@ export default function ClaudePluginClient({ border: `1px solid ${colors.border.light}`, borderRadius: spacing.md, padding: spacing.xl, - backgroundColor: colors.white, + backgroundColor: colors.background.elevated, display: "flex", flexDirection: "column", height: "100%", @@ -663,7 +659,7 @@ export default function ClaudePluginClient({ style={{ paddingTop: spacing["4xl"], paddingBottom: spacing["4xl"], - backgroundColor: colors.white, + backgroundColor: colors.background.primary, ...SECTION_PX, borderBottom: `1px solid ${colors.border.light}`, }} @@ -704,7 +700,7 @@ export default function ClaudePluginClient({ width: "100%", borderRadius: "12px", overflow: "hidden", - boxShadow: "0 8px 32px -4px rgba(0,0,0,0.12)", + boxShadow: `0 8px 32px -4px ${colors.shadow.medium}`, }} >
    to force a full server request instead of client-side routing. const cardClassName = "tw:no-underline tw:text-inherit tw:group"; const cardContent = ( -
    - {/* Image */} -
    - {item.image && ( - { - e.currentTarget.style.display = "none"; - }} - /> - )} - {/* Gradient overlay at bottom of image for depth */} -
    + {/* Image */} +
    + {item.image && ( + { + e.currentTarget.style.display = "none"; }} /> -
    + )} + {/* Gradient overlay at bottom of image for depth */} +
    +
    - {/* Content */} + {/* Content */} +
    + {/* Tags + Date row */}
    - {/* Tags + Date row */} -
    -
    - {displayTags.map((tag, i) => ( - - {tag} - {i < displayTags.length - 1 && ( - - ● - - )} - - ))} -
    - {formattedDate} +
    + {displayTags.map((tag, i) => ( + + {tag} + {i < displayTags.length - 1 && ( + + ● + + )} + + ))}
    + {formattedDate} +
    - {/* Title */} -

    - {item.title} -

    + {/* Title */} +

    + {item.title} +

    - {/* Description */} - - {item.description} - + {/* Description */} + + {item.description} + - {/* CTA */} -
    - {item.isApp ? "Open" : "Read more"} - -
    + {/* CTA */} +
    + {item.isApp ? "Open" : "Read more"} +
    +
    ); if (item.isApp) { - return {cardContent}; + return ( + + {cardContent} + + ); } - return {cardContent}; + return ( + + {cardContent} + + ); } /* ─── BlogPostGrid ─── */ @@ -416,7 +427,9 @@ function FilterSection({ style={{ borderRadius: "12px", border: `1px solid ${isExpanded ? colors.primary[200] : colors.border.light}`, - backgroundColor: isExpanded ? "rgba(230, 255, 250, 0.3)" : colors.white, + backgroundColor: isExpanded + ? colors.background.accent + : colors.background.elevated, transition: "border-color 0.2s ease, background-color 0.2s ease", overflow: "hidden", // Collapsed sections must keep their full header height when a sibling @@ -434,7 +447,7 @@ function FilterSection({ onClick={onToggle} onMouseEnter={(e) => { if (!isExpanded) { - e.currentTarget.style.backgroundColor = colors.gray[50]; + e.currentTarget.style.backgroundColor = colors.background.hover; } }} onMouseLeave={(e) => { @@ -448,9 +461,7 @@ function FilterSection({ fw={typography.fontWeight.semibold} size="sm" style={{ - color: isExpanded - ? colors.primary[700] - : colors.secondary[800], + color: isExpanded ? colors.text.link : colors.text.primary, }} > {label} @@ -465,7 +476,7 @@ function FilterSection({ height: "20px", borderRadius: "10px", backgroundColor: colors.primary[500], - color: colors.white, + color: colors.text.inverse, fontSize: "11px", fontWeight: typography.fontWeight.bold, fontFamily: typography.fontFamily.primary, @@ -478,7 +489,7 @@ function FilterSection({
    { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onChange(); } }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onChange(); + } + }} style={{ display: "flex", alignItems: "center", @@ -541,7 +555,7 @@ function CheckboxRow({ textAlign: "left", }} onMouseEnter={(e) => { - e.currentTarget.style.backgroundColor = colors.gray[50]; + e.currentTarget.style.backgroundColor = colors.background.hover; }} onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = "transparent"; @@ -558,7 +572,7 @@ function CheckboxRow({ style={{ fontSize: "13.5px", fontFamily: typography.fontFamily.primary, - color: checked ? colors.primary[700] : colors.secondary[700], + color: checked ? colors.text.link : colors.text.secondary, fontWeight: checked ? typography.fontWeight.semibold : typography.fontWeight.normal, @@ -605,8 +619,7 @@ function ResearchFilters({ availableAuthors, countryId = "us", }: ResearchFiltersProps) { - const [expandedSection, setExpandedSection] = - useState(null); + const [expandedSection, setExpandedSection] = useState(null); const [usStatesExpanded, setUsStatesExpanded] = useState(false); const [availableHeight, setAvailableHeight] = useState(400); const filterContainerRef = useRef(null); @@ -800,11 +813,10 @@ function ResearchFilters({ }} onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = - colors.primary[50]; + colors.background.accent; }} onMouseLeave={(e) => { - e.currentTarget.style.backgroundColor = - "transparent"; + e.currentTarget.style.backgroundColor = "transparent"; }} > {usStatesExpanded ? "Hide states" : "Show states"} @@ -855,11 +867,7 @@ function ResearchFilters({ /* ─── Main Client Component ─── */ -export default function ResearchClient({ - countryId, -}: { - countryId: string; -}) { +export default function ResearchClient({ countryId }: { countryId: string }) { const searchParams = useSearchParams(); const router = useRouter(); const pathname = usePathname(); @@ -869,10 +877,7 @@ export default function ResearchClient({ const allItems = useMemo(() => getResearchItems(), []); // Default locations based on country - const defaultLocations = useMemo( - () => [countryId, "global"], - [countryId], - ); + const defaultLocations = useMemo(() => [countryId, "global"], [countryId]); // Filter state - initialize from URL params const [searchQuery, setSearchQuery] = useState( @@ -1010,25 +1015,19 @@ export default function ResearchClient({
    {/* Sidebar Filters */}
    {filteredItems.length}{" "} {filteredItems.length === 1 ? "result" : "results"} @@ -1061,10 +1060,7 @@ export default function ResearchClient({ {filteredItems.length > 0 ? ( <> - + {/* Sentinel element for infinite scroll */} {hasMore && ( @@ -1082,11 +1078,11 @@ export default function ResearchClient({ className="tw:text-center" style={{ padding: spacing["3xl"], - backgroundColor: colors.gray[50], + backgroundColor: colors.background.secondary, borderRadius: spacing.radius.container, }} > - + No results found. Try adjusting your filters.
    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 ( - + + +