From c1ee99912bcfbd2b828257f94db03424721901c1 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Fri, 17 Apr 2026 03:34:29 -0400 Subject: [PATCH 1/3] feat: add dark mode --- app/root.res | 17 +++++++ src/Playground.res | 30 ++++++++++-- src/common/SiteTheme.res | 56 +++++++++++++++++++++ src/common/SiteTheme.resi | 11 +++++ src/components/CodeMirror.res | 1 - src/components/Footer.res | 11 +++-- src/components/LandingPage.res | 4 +- src/components/NavbarPrimary.res | 43 +++++++++++++++- src/components/ToggleButton.res | 4 +- styles/main.css | 84 ++++++++++++++++++++++++++++++++ 10 files changed, 245 insertions(+), 16 deletions(-) create mode 100644 src/common/SiteTheme.res create mode 100644 src/common/SiteTheme.resi diff --git a/app/root.res b/app/root.res index cbddcf72d..6276d3d9e 100644 --- a/app/root.res +++ b/app/root.res @@ -36,10 +36,27 @@ external utilsCss: string = "default" open ReactRouter +let initializeThemeScript = ` +(() => { + try { + const key = "siteTheme"; + const darkClass = "site-dark"; + const lightClass = "site-light"; + const stored = localStorage.getItem(key); + const preferred = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; + const theme = stored === "dark" || stored === "light" ? stored : preferred; + const root = document.documentElement; + root.classList.remove(darkClass, lightClass); + root.classList.add(theme === "dark" ? darkClass : lightClass); + } catch (_err) {} +})(); +` + @react.component let default = () => { + diff --git a/src/Playground.res b/src/Playground.res index 2ff7010bc..44312dc6a 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -59,10 +59,15 @@ let playgroundThemeClass = (theme: CodeMirror.Theme.t): string => module DropdownSelect = { @react.component - let make = (~onChange, ~name, ~value, ~disabled=false, ~children) => { + let make = (~onChange, ~name, ~value, ~theme, ~disabled=false, ~children) => { + let themeClass = switch theme { + | CodeMirror.Theme.Dark => "bg-gray-100 border-gray-80 text-gray-20" + | CodeMirror.Theme.Light => "bg-white border-gray-30 text-gray-80" + } let opacity = disabled ? " opacity-50" : ""
code { @apply text-fire; } + + html.site-dark #navbar-primary { + @apply bg-gray-100 text-gray-20; + } + + html.site-dark [data-testid="navbar-primary-left-content"], + html.site-dark [data-testid="navbar-primary-right-content"] { + @apply text-gray-20; + } + + html.site-dark [data-testid="navbar-primary-right-content"] button { + @apply text-gray-30 border-gray-60; + } + + html.site-dark #mobile-overlay { + @apply bg-gray-95; + } + + html.site-dark #doc-navbar { + @apply bg-gray-95 text-gray-30 border-b border-gray-80; + } + + html.site-dark #site-footer { + @apply border-gray-80 bg-gray-95; + } + + html.site-dark #site-footer .site-footer-content { + @apply text-gray-20; + } + + html.site-dark #site-footer .site-footer-muted { + @apply text-gray-40; + } + + html.site-dark #site-footer .site-footer-icons { + @apply text-gray-20; + } + + html.site-dark #site-footer .site-logo-light { + display: none; + } + + html.site-dark #site-footer .site-logo-dark { + display: block; + } + + html.site-dark #landing-page { + @apply text-gray-20; + } + + html.site-dark #landing-page .hl-title, + html.site-dark #landing-page .hl-1, + html.site-dark #landing-page .hl-2, + html.site-dark #landing-page .hl-3, + html.site-dark #landing-page .hl-4, + html.site-dark #landing-page .hl-5, + html.site-dark #landing-page .hl-overline { + @apply text-gray-20; + } + + html.site-dark #landing-page .text-black, + html.site-dark #landing-page .text-gray-80 { + @apply text-gray-20; + } + + html.site-dark #landing-page .text-gray-60 { + @apply text-gray-30; + } + + html.site-dark #landing-page .text-gray-40 { + @apply text-gray-30; + } + + html.site-dark #landing-page .lp-playground-hero { + @apply bg-gray-100; + } + + html.site-dark #landing-page .lp-playground-hero .captions { + @apply text-gray-30 border-gray-60; + } } .wrapper { From dead44725a1cd1eae6b33d4cd7f40c87572965bd Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Fri, 17 Apr 2026 05:31:13 -0400 Subject: [PATCH 2/3] feat: continue tailwind dark mode migration Migrate shared typography, landing page, navbar, mobile overlay, and footer to Tailwind dark classes. Remove the old html.site-dark override block and update the dark mode plan documents to reflect the current state and next steps. --- DARK_MODE.md | 68 +++++++++++++ DARK_MODE_HANDOFF.md | 130 +++++++++++++++++++++++++ src/components/Footer.res | 14 +-- src/components/LandingPage.res | 42 ++++---- src/components/NavbarMobileOverlay.res | 2 +- src/components/NavbarPrimary.res | 8 +- styles/main.css | 101 ++----------------- 7 files changed, 244 insertions(+), 121 deletions(-) create mode 100644 DARK_MODE.md create mode 100644 DARK_MODE_HANDOFF.md diff --git a/DARK_MODE.md b/DARK_MODE.md new file mode 100644 index 000000000..ac038962f --- /dev/null +++ b/DARK_MODE.md @@ -0,0 +1,68 @@ +Dark Mode Migration Plan + +## Current State + +### How dark mode works today + +- Theme is toggled via `SiteTheme.res`, which adds/removes a `site-dark` class on ``. +- A script in `app/root.res` initializes the theme from `localStorage` or `prefers-color-scheme` on page load to prevent FOUC. +- Tailwind `dark:` is now enabled through `@custom-variant dark (&:where(.site-dark, .site-dark *));`. +- The landing page, navbar, mobile overlay, footer, and shared heading utilities now use `dark:` classes directly. +- The broad `html.site-dark` override block has been removed from `styles/main.css`. + +### The problem + +The remaining dark-mode work is no longer about wiring up Tailwind. It is now about finishing the migration and tightening visual consistency across the site: + +1. Some component surfaces and text levels still need contrast tuning. +2. Not all shared chrome has been migrated yet, especially older docs navigation paths. +3. The plan docs were written before the current Tailwind migration work landed, so some steps below are now completed. + +### Files involved + +| File | Role | +| ---------------------------------------- | ---------------------------------------------------------------------- | +| `styles/main.css` | Tailwind custom dark variant, shared typography utilities, body styles | +| `src/components/LandingPage.res` | Landing page with hardcoded light-mode text colors | +| `src/components/NavbarPrimary.res` | Navbar theme toggle and dark classes | +| `src/components/NavbarMobileOverlay.res` | Mobile overlay dark classes | +| `src/components/Footer.res` | Footer dark classes and dark logo swap | +| `src/common/SiteTheme.res` | Theme toggle logic (adds/removes `site-dark` class) | +| `app/root.res` | Theme initialization script | + +--- + +## Status + +### Completed + +1. Added `@custom-variant dark` to `styles/main.css`. +2. Moved `hl-title`, `hl-1` through `hl-5`, and `hl-overline` to use `dark:` colors directly. +3. Replaced the old `body` light/dark split with a single Tailwind rule using `dark:bg-*` and `dark:text-*`. +4. Removed the old `html.site-dark` override block from `styles/main.css`. +5. Migrated the landing page, navbar, mobile overlay, and footer to explicit `dark:` classes. +6. Verified the current changes with `yarn build:res`. + +### Remaining Work + +1. Audit remaining shared chrome still using older styling paths, especially docs-specific navigation. +2. Improve dark-mode contrast on sections that still feel too dim or flat. +3. Add test coverage for site-wide theme toggling and dark-mode homepage regressions. + +## Implementation Notes + +- Prefer Tailwind `dark:` classes in components over CSS selectors targeting rendered utility class names. +- Keep `.site-dark` only as the trigger on ``; styling should live in utilities and component class lists. +- Avoid reintroducing `html.site-dark #...` overrides unless there is no component-level alternative. + +## Color mapping reference + +| Light mode | Dark mode | Usage | +| ---------------- | ---------------- | -------------------------------------------------- | +| `text-black` | `text-gray-20` | Primary headings (`hl-1`, `hl-2`, `hl-3`) | +| `text-gray-80` | `text-gray-20` | Secondary headings, body text | +| `text-gray-60` | `text-gray-30` | Subtitles, secondary text | +| `text-gray-40` | `text-gray-30` | Captions, muted text | +| `bg-white` | `bg-gray-100` | Page background | +| `bg-gray-10` | `bg-gray-100` | Elevated surfaces in light (blend into bg in dark) | +| `border-gray-20` | `border-gray-80` | Borders | diff --git a/DARK_MODE_HANDOFF.md b/DARK_MODE_HANDOFF.md new file mode 100644 index 000000000..17caf1631 --- /dev/null +++ b/DARK_MODE_HANDOFF.md @@ -0,0 +1,130 @@ +# Dark Mode Handoff (Current Status) + +## What We Completed + +### 1) Playground dark/light support + +- Added a `Playground Theme` toggle in Settings. +- Added runtime CodeMirror theme switching (dark/light). +- Added theme persistence for playground via `localStorage` key: + - `playgroundTheme` +- Updated JS output panel highlighting to follow playground theme. + +### 2) Playground onboarding toast + +- Added “New: Light Mode” toast in Playground. +- Supports: + - `Dismiss` + - `Try it now` (switches to light mode + closes toast) + - auto-close after 10s +- Toast “seen” state moved to `sessionStorage` key: + - `playgroundLightModeToastSeen` + +### 3) Playground visual fixes + +- Improved light-mode contrast for: + - Auto-run toggle text + - Middle panel divider appearance +- Added Cypress flow to: + - click toast `Try it now` + - switch back to dark mode from Settings + +### 4) Site-wide dark mode foundation (first pass) + +- Added global site theme model: + - `src/common/SiteTheme.res` + - `src/common/SiteTheme.resi` +- Added early root theme initialization script in `app/root.res`. +- Added navbar theme toggle in `src/components/NavbarPrimary.res`. +- Added dark-mode styling hooks for shared chrome in `styles/main.css`: + - body + - primary navbar + - docs subnav + - mobile overlay + - footer +- Added footer dark-logo swap and footer dark classes in `src/components/Footer.res`. + +### 5) Tailwind dark-mode migration pass + +- Enabled Tailwind dark mode via: + - `@custom-variant dark (&:where(.site-dark, .site-dark *));` +- Migrated shared typography utilities to include dark colors directly: + - `hl-title` + - `hl-1` through `hl-5` + - `hl-overline` +- Replaced the old `html.site-dark body` override with a single Tailwind body rule. +- Removed the broad `html.site-dark` selector block from `styles/main.css`. +- Migrated these areas to explicit `dark:` classes: + - landing page + - navbar + - mobile overlay + - footer + +## Files Changed (Dark Mode Work) + +- `app/root.res` +- `src/common/SiteTheme.res` +- `src/common/SiteTheme.resi` +- `src/components/NavbarPrimary.res` +- `src/components/NavbarMobileOverlay.res` +- `src/components/Footer.res` +- `src/components/ToggleButton.res` +- `src/components/CodeMirror.res` +- `src/components/CodeMirror.resi` +- `src/Playground.res` +- `src/components/LandingPage.res` +- `styles/main.css` +- `e2e/Playground.cy.res` + +## Current Known Issues + +### Homepage (dark mode) + +- Some sections still need contrast tuning and more deliberate dark surface hierarchy. +- A few landing page cards and labels still use light-mode-biased values that could be tightened further. + +### General + +- This is still a foundational implementation, not a complete theme polish pass. +- `yarn build:res` now succeeds in this environment after clearing a stale `lib/rescript.lock`. +- Broader verification is still pending. + +## Recommended Next Steps + +### Phase 1: Homepage contrast pass (high priority) + +1. Make primary marketing text brighter: + - Hero title/subtitle + - USP headings and paragraph text + - Trusted-by and Curated Resources headings +2. Normalize dark backgrounds section-by-section: + - ensure no remaining light patches + - keep visual hierarchy with 2–3 dark surface levels +3. Tune card and border contrast: + - cards in Curated Resources + - hr/dividers and muted labels + +### Phase 2: Global component pass + +1. Audit and fix dark-mode contrast in shared UI: + - doc-sidebars + - docs subnav + - search and any remaining shared controls +2. Replace broad selector overrides with cleaner semantic dark classes where needed. + - This is mostly done for landing/footer/navbar/mobile overlay. + - Remaining work should continue the same Tailwind-first approach. + +### Phase 3: Accessibility + regression checks + +1. Check color contrast (target WCAG AA for body text and controls). +2. Run: + - `yarn build:res` + - `yarn test` + - `yarn vitest --browser.headless --run` +3. Add/adjust regression checks for global theme toggle and homepage dark-mode snapshots. + +## Suggested Cleanup (after visual stabilization) + +- Consolidate theme constants for shared text/surface levels. +- Avoid overly broad selectors in `styles/main.css` and scope to specific component wrappers. +- Decide final UX for theme toggle location (navbar-only vs additional settings entry). diff --git a/src/components/Footer.res b/src/components/Footer.res index 200e2813f..743552061 100644 --- a/src/components/Footer.res +++ b/src/components/Footer.res @@ -18,13 +18,15 @@ let make = () => { let iconLink = "hover:pointer hover:text-gray-60-tr" let copyrightYear = Date.make()->Date.getFullYear->Int.toString -