Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions DARK_MODE.md
Original file line number Diff line number Diff line change
@@ -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 `<html>`.
- 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 `<html>`; 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 |
130 changes: 130 additions & 0 deletions DARK_MODE_HANDOFF.md
Original file line number Diff line number Diff line change
@@ -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).
17 changes: 17 additions & 0 deletions app/root.res
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
<html lang="en">
<head>
<script> {React.string(initializeThemeScript)} </script>
<style> {React.string("html {opacity:0;}")} </style>
<link rel="preload" href={mainCss} as_="style" />
<link rel="stylesheet" href={mainCss} />
Expand Down
3 changes: 2 additions & 1 deletion app/routes/ApiRoute.res
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,8 @@ let default = () => {
<VersionSelect />
</div>
<button
className="flex items-center" onClick={_ => NavbarUtils.closeMobileTertiaryDrawer()}
className="flex items-center text-gray-60 dark:text-gray-30 hover:text-gray-80 dark:hover:text-gray-20"
onClick={_ => NavbarUtils.closeMobileTertiaryDrawer()}
>
<Icon.Close />
</button>
Expand Down
41 changes: 29 additions & 12 deletions src/ApiDocs.res
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,18 @@ module RightSidebar = {
switch item {
| Value({name, deprecated}) as kind | Type({name, deprecated}) as kind =>
let (icon, textColor, bgColor, href) = switch kind {
| Type(_) => ("t", "text-fire-30", "bg-fire-5", `#type-${name}`)
| Value(_) => ("v", "text-sky-30", "bg-sky-5", `#value-${name}`)
| Type(_) => (
"t",
"text-fire-30 dark:text-fire-dark",
"bg-fire-5 dark:bg-fire-100",
`#type-${name}`,
)
| Value(_) => (
"v",
"text-sky-30 dark:text-water-dark",
"bg-sky-5 dark:bg-sky-90",
`#value-${name}`,
)
}
let deprecatedIcon = switch deprecated->Null.toOption {
| Some(_) =>
Expand All @@ -69,7 +79,7 @@ module RightSidebar = {
<li className="my-3" key={href}>
<ReactRouter.Link.String
title
className="flex items-center w-full font-normal text-14 text-gray-40 leading-tight hover:text-gray-80"
className="flex items-center w-full font-normal text-14 text-gray-40 dark:text-gray-30 leading-tight hover:text-gray-80 dark:hover:text-gray-20"
to=href
onClick={_ => onLinkClick->Option.forEach(fn => fn())}
>
Expand Down Expand Up @@ -109,8 +119,8 @@ module SidebarTree = {

let isCurrentlyAtRoot = currentPathname == moduleRoute

let summaryClassName = "truncate py-1 md:h-auto tracking-tight text-gray-60 font-medium text-14 rounded-sm hover:bg-gray-20 hover:-ml-2 hover:py-1 hover:pl-2 "
let classNameActive = " bg-fire-5 text-red-500 -ml-2 pl-2 font-medium hover:bg-fire-70"
let summaryClassName = "truncate py-1 md:h-auto tracking-tight text-gray-60 dark:text-gray-30 font-medium text-14 rounded-sm hover:bg-gray-20 dark:hover:bg-gray-90 hover:-ml-2 hover:py-1 hover:pl-2 "
let classNameActive = " bg-fire-5 dark:bg-fire-100 text-red-500 dark:text-fire-dark -ml-2 pl-2 font-medium hover:bg-fire-70 dark:hover:bg-fire-90"

let subMenu = switch items->Array.length > 0 {
| true =>
Expand Down Expand Up @@ -179,7 +189,10 @@ module SidebarTree = {

<aside className="px-4 w-full block">
<div className="my-10">
<div className="hl-overline block text-gray-80 mt-5 mb-2" dataTestId="overview">
<div
className="hl-overline block text-gray-80 dark:text-gray-20 mt-5 mb-2"
dataTestId="overview"
>
{"Overview"->React.string}
</div>
<Link.String
Expand All @@ -190,7 +203,9 @@ module SidebarTree = {
</Link.String>
{isCurrentlyAtRoot ? subMenu : React.null}
</div>
<div className="hl-overline text-gray-80 mt-5 mb-2"> {"submodules"->React.string} </div>
<div className="hl-overline text-gray-80 dark:text-gray-20 mt-5 mb-2">
{"submodules"->React.string}
</div>
{node.children
->Array.toSorted((v1, v2) => String.compare(v1.name, v2.name))
->Array.filter(child => child.name !== node.name)
Expand Down Expand Up @@ -299,11 +314,13 @@ let make = (props: props) => {
// This is the sidebar on the right side of the page for desktops showing types and values
let rightSidebar = switch props {
| Ok({module_: {items}}) if Array.length(items) > 0 =>
<div className="hidden xl:block lg:w-1/5 md:h-auto md:relative overflow-y-visible bg-white">
<div
className="hidden xl:block lg:w-1/5 md:h-auto md:relative overflow-y-visible bg-white dark:bg-gray-95"
>
<aside
className="relative pl-4 w-full block md:top-28 md:pt-4 md:sticky border-l border-gray-20 overflow-y-auto pb-24 h-[calc(100vh-7rem)]"
className="relative pl-4 w-full block md:top-28 md:pt-4 md:sticky border-l border-gray-20 dark:border-gray-80 overflow-y-auto pb-24 h-[calc(100vh-7rem)]"
>
<div className="hl-overline block text-gray-80 mb-2">
<div className="hl-overline block text-gray-80 dark:text-gray-20 mb-2">
{"Types and values"->React.string}
</div>
<ul>
Expand All @@ -319,10 +336,10 @@ let make = (props: props) => {
| Ok({toctree, module_: {items}}) =>
<div
id="sidebar"
className="hidden md:block md:w-48 md:-ml-4 lg:w-1/5 h-auto md:relative overflow-y-visible bg-white"
className="hidden md:block md:w-48 md:-ml-4 lg:w-1/5 h-auto md:relative overflow-y-visible bg-white dark:bg-gray-95"
>
<div
className="w-80 h-full relative top-12 w-full md:top-28 md:sticky border-r border-gray-20 overflow-y-auto pb-24 max-h-[calc(100vh-7rem)]"
className="w-80 h-full relative top-12 w-full md:top-28 md:sticky border-r border-gray-20 dark:border-gray-80 overflow-y-auto pb-24 max-h-[calc(100vh-7rem)]"
>
<SidebarTree node={toctree} items />
</div>
Expand Down
Loading
Loading