diff --git a/index.html b/index.html index bc193db..60b2d16 100644 --- a/index.html +++ b/index.html @@ -3,7 +3,7 @@ - + diff --git a/src/App.jsx b/src/App.jsx index 967f301..2d5ef6f 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -1,4 +1,6 @@ import { useState, useEffect, useCallback, useMemo, lazy, Suspense } from "react"; +import PolicyEngineHeader from "./components/PolicyEngineHeader"; +import PolicyEngineFooter from "./components/PolicyEngineFooter"; import USMap from "./components/USMap"; import Breadcrumb from "./components/Breadcrumb"; import StateSearchCombobox from "./components/StateSearchCombobox"; @@ -125,47 +127,38 @@ function App() { return (
- {/* Header */} + {/* PE org header */} + + + {/* Tracker title bar + state search */}
-
- - PolicyEngine - -
-

- 2026 State Legislative Tracker -

-

- PolicyEngine State Tax Research -

-
+
+

+ 2026 State Legislative Tracker +

+

+ PolicyEngine State Tax Research +

@@ -338,44 +331,8 @@ function App() { )} - {/* Footer */} -
- {/* Gradient accent at top */} -
-
-

- © {new Date().getFullYear()} PolicyEngine. Open-source tax and benefit policy simulation. -

-
- GitHub - PolicyEngine.org -
-
-
+ {/* PE org footer */} +
); } @@ -536,27 +493,4 @@ function QuickLinkCard({ href, title, description }) { ); } -// Footer Link Component -function FooterLink({ href, children }) { - return ( - e.currentTarget.style.color = colors.primary[300]} - onMouseLeave={(e) => e.currentTarget.style.color = colors.gray[400]} - > - {children} - - ); -} - export default App; diff --git a/src/components/PolicyEngineFooter.jsx b/src/components/PolicyEngineFooter.jsx new file mode 100644 index 0000000..4432060 --- /dev/null +++ b/src/components/PolicyEngineFooter.jsx @@ -0,0 +1,206 @@ +import { colors, typography } from '../designTokens'; + +/** + * PolicyEngine org footer — matches policyengine.org production footer. + * Adapted from keep-your-pay-act Footer.tsx. + * + * TODO: Replace this entire file with `import { Footer } from "@policyengine/ui-kit"` + * once ui-kit 0.4.0 is published to npm. See: + * - https://github.com/PolicyEngine/policyengine-ui-kit/issues/18 (build OOM blocking publish) + * - https://github.com/PolicyEngine/policyengine-ui-kit/issues/16 (changelog/publish pipeline) + */ + +const COLORS = { + primary500: colors.primary[500], + primary600: colors.primary[600], + primary800: colors.primary[800], +}; + +const FONT = typography.fontFamily.primary; + +const NAV_LINKS = [ + { href: 'https://policyengine.org/us/team', text: 'About us' }, + { href: 'https://policyengine.org/us/donate', text: 'Donate' }, + { href: 'https://policyengine.org/us/privacy', text: 'Privacy policy' }, + { href: 'https://policyengine.org/us/terms', text: 'Terms and conditions' }, +]; + +const SOCIAL_LINKS = [ + { + label: 'Email', + href: 'mailto:hello@policyengine.org', + icon: ( + + ), + }, + { + label: 'Twitter', + href: 'https://twitter.com/ThePolicyEngine', + icon: ( + + ), + }, + { + label: 'Facebook', + href: 'https://www.facebook.com/PolicyEngine', + icon: ( + + ), + }, + { + label: 'LinkedIn', + href: 'https://www.linkedin.com/company/thepolicyengine', + icon: ( + + ), + }, + { + label: 'YouTube', + href: 'https://www.youtube.com/@policyengine', + icon: ( + + ), + }, + { + label: 'Instagram', + href: 'https://www.instagram.com/PolicyEngine/', + icon: ( + + ), + }, + { + label: 'GitHub', + href: 'https://github.com/PolicyEngine', + icon: ( + + ), + }, +]; + +export default function PolicyEngineFooter() { + return ( +
+
+ PolicyEngine + +
+
+
+ {NAV_LINKS.map(({ href, text }) => ( + + {text} + + ))} +
+ +
+
+ {SOCIAL_LINKS.map(({ label, href, icon }) => ( + + {icon} + + ))} +
+

+ © {new Date().getFullYear()} PolicyEngine. All rights reserved. +

+
+
+ +
+

+ Subscribe to PolicyEngine +

+

+ Get the latest posts delivered right to your inbox. +

+ +
+
+
+
+ ); +} diff --git a/src/components/PolicyEngineHeader.jsx b/src/components/PolicyEngineHeader.jsx new file mode 100644 index 0000000..93c5090 --- /dev/null +++ b/src/components/PolicyEngineHeader.jsx @@ -0,0 +1,499 @@ +import { useCallback, useState, useRef, useEffect } from 'react'; +import { colors, typography } from '../designTokens'; + +/** + * PolicyEngine org header — matches policyengine.org production header. + * Adapted from keep-your-pay-act Header.tsx with added API and Citations nav items. + * + * TODO: Replace this entire file with `import { Header } from "@policyengine/ui-kit"` + * once ui-kit 0.4.0 is published to npm. See: + * - https://github.com/PolicyEngine/policyengine-ui-kit/issues/18 (build OOM blocking publish) + * - https://github.com/PolicyEngine/policyengine-ui-kit/issues/16 (changelog/publish pipeline) + */ + +const COLORS = { + primary500: colors.primary[500], + primary600: colors.primary[600], + primary700: colors.primary[700], + primary800: colors.primary[800], + textInverse: colors.text.inverse, + shadowLight: 'rgba(16, 24, 40, 0.05)', + shadowMedium: 'rgba(16, 24, 40, 0.1)', +}; + +const FONT = typography.fontFamily.primary; + +const NAV_ITEMS = [ + { label: 'Research', href: 'https://policyengine.org/us/research' }, + { label: 'Model', href: 'https://policyengine.org/us/model' }, + { label: 'API', href: 'https://policyengine.org/us/api' }, + { + label: 'About', + hasDropdown: true, + items: [ + { label: 'Team', href: 'https://policyengine.org/us/team' }, + { label: 'Supporters', href: 'https://policyengine.org/us/supporters' }, + { label: 'Citations', href: 'https://policyengine.org/us/citations' }, + ], + }, + { label: 'Donate', href: 'https://policyengine.org/us/donate' }, +]; + +const COUNTRIES = [ + { id: 'us', label: 'United States' }, + { id: 'uk', label: 'United Kingdom' }, +]; + +const navItemStyle = { + color: COLORS.textInverse, + fontWeight: 500, + fontSize: '15px', + fontFamily: FONT, + textDecoration: 'none', + padding: '6px 14px', + borderRadius: '6px', + transition: 'background-color 0.15s ease', + letterSpacing: '0.01em', +}; + +const hoverHandlers = { + onMouseEnter: (e) => { + e.currentTarget.style.backgroundColor = 'rgba(255, 255, 255, 0.12)'; + }, + onMouseLeave: (e) => { + e.currentTarget.style.backgroundColor = 'transparent'; + }, +}; + +function AppleDropdown({ items, open, onClose, align = 'center' }) { + const contentRef = useRef(null); + const [contentHeight, setContentHeight] = useState(0); + const [visible, setVisible] = useState(false); + + useEffect(() => { + if (open && contentRef.current) { + setContentHeight(contentRef.current.scrollHeight); + requestAnimationFrame(() => setVisible(true)); + } else { + setVisible(false); + const timer = setTimeout(() => setContentHeight(0), 250); + return () => clearTimeout(timer); + } + }, [open]); + + if (!open && contentHeight === 0) return null; + + const positionStyle = + align === 'right' + ? { right: 0, transform: visible ? 'translateY(0)' : 'translateY(-8px)' } + : { + left: '50%', + transform: visible + ? 'translateX(-50%) translateY(0)' + : 'translateX(-50%) translateY(-8px)', + }; + + return ( + <> +
+
+
+ {items.map((item, i) => ( + onClose()} + style={{ + display: 'flex', + alignItems: 'center', + width: '100%', + textAlign: 'left', + padding: '11px 16px', + borderRadius: '10px', + border: 'none', + background: 'transparent', + cursor: 'pointer', + fontSize: '14px', + fontFamily: FONT, + fontWeight: 600, + color: COLORS.primary800, + textDecoration: 'none', + transition: 'background-color 0.12s ease, color 0.12s ease, opacity 0.3s ease', + transitionDelay: visible ? `${i * 50}ms` : '0ms', + opacity: visible ? 1 : 0, + lineHeight: '1.3', + letterSpacing: '-0.01em', + }} + onMouseEnter={(e) => { + e.currentTarget.style.backgroundColor = COLORS.primary500; + e.currentTarget.style.color = COLORS.textInverse; + }} + onMouseLeave={(e) => { + e.currentTarget.style.backgroundColor = 'transparent'; + e.currentTarget.style.color = COLORS.primary800; + }} + > + {item.label} + + ))} +
+
+ + ); +} + +function CountrySelector() { + const [open, setOpen] = useState(false); + const containerRef = useRef(null); + const contentRef = useRef(null); + const [contentHeight, setContentHeight] = useState(0); + const [visible, setVisible] = useState(false); + + useEffect(() => { + if (open && contentRef.current) { + setContentHeight(contentRef.current.scrollHeight); + requestAnimationFrame(() => setVisible(true)); + } else { + setVisible(false); + const timer = setTimeout(() => setContentHeight(0), 250); + return () => clearTimeout(timer); + } + }, [open]); + + useEffect(() => { + if (!open) return; + function handleClick(e) { + if (containerRef.current && !containerRef.current.contains(e.target)) setOpen(false); + } + function handleKey(e) { + if (e.key === 'Escape') setOpen(false); + } + document.addEventListener('mousedown', handleClick); + document.addEventListener('keydown', handleKey); + return () => { + document.removeEventListener('mousedown', handleClick); + document.removeEventListener('keydown', handleKey); + }; + }, [open]); + + return ( +
+ + + {(open || contentHeight > 0) && ( + <> + + ); +} + +export default function PolicyEngineHeader() { + const [aboutOpen, setAboutOpen] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); + const aboutRef = useRef(null); + + useEffect(() => { + if (!aboutOpen) return; + function handleClick(e) { + if (aboutRef.current && !aboutRef.current.contains(e.target)) setAboutOpen(false); + } + function handleKey(e) { + if (e.key === 'Escape') setAboutOpen(false); + } + document.addEventListener('mousedown', handleClick); + document.addEventListener('keydown', handleKey); + return () => { + document.removeEventListener('mousedown', handleClick); + document.removeEventListener('keydown', handleKey); + }; + }, [aboutOpen]); + + return ( +
+
+
+ + PolicyEngine + + + +
+ +
+ +
+ +
+ + +
+
+ + {mobileOpen && ( + <> +
setMobileOpen(false)} + /> +
+
+ Menu + +
+
+ {NAV_ITEMS.map((item) => + item.hasDropdown ? ( +
+ + {item.label} + +
+ {item.items.map((sub) => ( + + {sub.label} + + ))} +
+
+ ) : ( + + {item.label} + + ), + )} +
+
+ + )} +
+ ); +} diff --git a/src/components/reform/ChartExportWrapper.jsx b/src/components/reform/ChartExportWrapper.jsx index c4cd8ae..38c922f 100644 --- a/src/components/reform/ChartExportWrapper.jsx +++ b/src/components/reform/ChartExportWrapper.jsx @@ -2,7 +2,7 @@ import { useRef, useCallback } from "react"; import { toPng } from "html-to-image"; import { colors, typography, spacing } from "../../designTokens"; -const PE_LOGO_URL = "/policyengine-logo.svg"; +const PE_LOGO_URL = "https://www.policyengine.org/assets/logos/policyengine/teal.svg"; const DownloadIcon = () => (