Skip to content

Commit c8f7fd5

Browse files
committed
darkmode start
1 parent 7000908 commit c8f7fd5

8 files changed

Lines changed: 328 additions & 9 deletions

File tree

DARK_MODE_HANDOFF.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
# Dark Mode Handoff (Current Status)
2+
3+
## What We Completed
4+
5+
### 1) Playground dark/light support
6+
7+
- Added a `Playground Theme` toggle in Settings.
8+
- Added runtime CodeMirror theme switching (dark/light).
9+
- Added theme persistence for playground via `localStorage` key:
10+
- `playgroundTheme`
11+
- Updated JS output panel highlighting to follow playground theme.
12+
13+
### 2) Playground onboarding toast
14+
15+
- Added “New: Light Mode” toast in Playground.
16+
- Supports:
17+
- `Dismiss`
18+
- `Try it now` (switches to light mode + closes toast)
19+
- auto-close after 10s
20+
- Toast “seen” state moved to `sessionStorage` key:
21+
- `playgroundLightModeToastSeen`
22+
23+
### 3) Playground visual fixes
24+
25+
- Improved light-mode contrast for:
26+
- Auto-run toggle text
27+
- Middle panel divider appearance
28+
- Added Cypress flow to:
29+
- click toast `Try it now`
30+
- switch back to dark mode from Settings
31+
32+
### 4) Site-wide dark mode foundation (first pass)
33+
34+
- Added global site theme model:
35+
- `src/common/SiteTheme.res`
36+
- `src/common/SiteTheme.resi`
37+
- Added early root theme initialization script in `app/root.res`.
38+
- Added navbar theme toggle in `src/components/NavbarPrimary.res`.
39+
- Added dark-mode styling hooks for shared chrome in `styles/main.css`:
40+
- body
41+
- primary navbar
42+
- docs subnav
43+
- mobile overlay
44+
- footer
45+
- Added footer dark-logo swap and footer dark classes in `src/components/Footer.res`.
46+
47+
## Files Changed (Dark Mode Work)
48+
49+
- `app/root.res`
50+
- `src/common/SiteTheme.res`
51+
- `src/common/SiteTheme.resi`
52+
- `src/components/NavbarPrimary.res`
53+
- `src/components/Footer.res`
54+
- `src/components/ToggleButton.res`
55+
- `src/components/CodeMirror.res`
56+
- `src/components/CodeMirror.resi`
57+
- `src/Playground.res`
58+
- `src/components/LandingPage.res`
59+
- `styles/main.css`
60+
- `e2e/Playground.cy.res`
61+
62+
## Current Known Issues
63+
64+
### Homepage (dark mode)
65+
66+
- Contrast is still too low in multiple sections.
67+
- Some typography is still too dim after first pass.
68+
- Landing surfaces are not fully harmonized (dark values are inconsistent section-to-section).
69+
70+
### General
71+
72+
- This is a foundational implementation, not a complete “theme polish” pass.
73+
- Full compile/test verification was not fully completed in this environment due hanging command output for `yarn build:res`.
74+
75+
## Recommended Next Steps
76+
77+
### Phase 1: Homepage contrast pass (high priority)
78+
79+
1. Make primary marketing text brighter:
80+
- Hero title/subtitle
81+
- USP headings and paragraph text
82+
- Trusted-by and Curated Resources headings
83+
2. Normalize dark backgrounds section-by-section:
84+
- ensure no remaining light patches
85+
- keep visual hierarchy with 2–3 dark surface levels
86+
3. Tune card and border contrast:
87+
- cards in Curated Resources
88+
- hr/dividers and muted labels
89+
90+
### Phase 2: Global component pass
91+
92+
1. Audit and fix dark-mode contrast in shared UI:
93+
- navbar links/icons/search
94+
- mobile overlays
95+
- doc-sidebars
96+
2. Replace broad selector overrides with cleaner semantic dark classes where needed.
97+
98+
### Phase 3: Accessibility + regression checks
99+
100+
1. Check color contrast (target WCAG AA for body text and controls).
101+
2. Run:
102+
- `yarn build:res`
103+
- `yarn test`
104+
- `yarn cy:run`
105+
3. Add/adjust e2e checks for global theme toggle and homepage dark-mode snapshots (if desired).
106+
107+
## Suggested Cleanup (after visual stabilization)
108+
109+
- Consolidate theme constants for shared text/surface levels.
110+
- Avoid overly broad selectors in `styles/main.css` and scope to specific component wrappers.
111+
- Decide final UX for theme toggle location (navbar-only vs additional settings entry).

app/root.res

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,27 @@ external utilsCss: string = "default"
3636

3737
open ReactRouter
3838

39+
let initializeThemeScript = `
40+
(() => {
41+
try {
42+
const key = "siteTheme";
43+
const darkClass = "site-dark";
44+
const lightClass = "site-light";
45+
const stored = localStorage.getItem(key);
46+
const preferred = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
47+
const theme = stored === "dark" || stored === "light" ? stored : preferred;
48+
const root = document.documentElement;
49+
root.classList.remove(darkClass, lightClass);
50+
root.classList.add(theme === "dark" ? darkClass : lightClass);
51+
} catch (_err) {}
52+
})();
53+
`
54+
3955
@react.component
4056
let default = () => {
4157
<html lang="en">
4258
<head>
59+
<script> {React.string(initializeThemeScript)} </script>
4360
<style> {React.string("html {opacity:0;}")} </style>
4461
<link rel="preload" href={mainCss} as_="style" />
4562
<link rel="stylesheet" href={mainCss} />

src/common/SiteTheme.res

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
let storageKey = "siteTheme"
2+
let darkClassName = "site-dark"
3+
let lightClassName = "site-light"
4+
5+
type t = Light | Dark
6+
7+
let toString = (theme: t): string =>
8+
switch theme {
9+
| Light => "light"
10+
| Dark => "dark"
11+
}
12+
13+
let fromString = (value: string): t =>
14+
switch value {
15+
| "dark" => Dark
16+
| _ => Light
17+
}
18+
19+
let toggle = (theme: t): t =>
20+
switch theme {
21+
| Light => Dark
22+
| Dark => Light
23+
}
24+
25+
let applyToDom = (theme: t): unit => {
26+
let classList = document.documentElement.classList
27+
switch theme {
28+
| Dark =>
29+
WebAPI.DOMTokenList.add(classList, darkClassName)
30+
WebAPI.DOMTokenList.remove(classList, lightClassName)
31+
| Light =>
32+
WebAPI.DOMTokenList.add(classList, lightClassName)
33+
WebAPI.DOMTokenList.remove(classList, darkClassName)
34+
}
35+
}
36+
37+
let getPreferred = (): t => {
38+
let mediaQuery = window->WebAPI.Window.matchMedia("(prefers-color-scheme: dark)")
39+
mediaQuery.matches ? Dark : Light
40+
}
41+
42+
let getInitial = (): t => {
43+
let stored = WebAPI.Storage.getItem(window.localStorage, storageKey)->Null.toOption
44+
switch stored {
45+
| Some(value) => fromString(value)
46+
| None => getPreferred()
47+
}
48+
}
49+
50+
let persist = (theme: t): unit =>
51+
WebAPI.Storage.setItem(window.localStorage, ~key=storageKey, ~value=theme->toString)
52+
53+
let set = (theme: t): unit => {
54+
applyToDom(theme)
55+
persist(theme)
56+
}

src/common/SiteTheme.resi

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
let storageKey: string
2+
3+
type t = Light | Dark
4+
5+
let toString: t => string
6+
let fromString: string => t
7+
let toggle: t => t
8+
let getInitial: unit => t
9+
let applyToDom: t => unit
10+
let persist: t => unit
11+
let set: t => unit

src/components/Footer.res

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,13 @@ let make = () => {
1818
let iconLink = "hover:pointer hover:text-gray-60-tr"
1919
let copyrightYear = Date.make()->Date.getFullYear->Int.toString
2020

21-
<footer className="flex justify-center border-t border-gray-10">
21+
<footer id="site-footer" className="flex justify-center border-t border-gray-10">
2222
<div
23-
className="flex flex-col md:flex-row justify-between max-w-1280 w-full px-8 py-16 text-gray-80 "
23+
className="site-footer-content flex flex-col md:flex-row justify-between max-w-1280 w-full px-8 py-16 text-gray-80 "
2424
>
2525
<div>
26-
<img className="w-40 mb-5" src="/rescript_logo_black.svg" />
26+
<img className="site-logo-light w-40 mb-5" src="/rescript_logo_black.svg" />
27+
<img className="site-logo-dark hidden w-40 mb-5" src="/brand/rescript-logo-white.svg" />
2728
<div className="text-16">
2829
<p> {React.string(`© ${copyrightYear} The ReScript Project`)} </p>
2930
</div>
@@ -32,7 +33,7 @@ let make = () => {
3233
className="flex flex-col space-y-16 md:flex-row mt-16 md:mt-0 md:ml-16 md:space-y-0 md:space-x-16"
3334
>
3435
<Section title="About">
35-
<ul className="text-16 text-gray-80-tr space-y-2">
36+
<ul className="site-footer-muted text-16 text-gray-80-tr space-y-2">
3637
<li>
3738
<Link to=#"/community/overview" className={linkClass}>
3839
{React.string("Community")}
@@ -47,7 +48,7 @@ let make = () => {
4748
</ul>
4849
</Section>
4950
<Section title="Find us on">
50-
<div className="flex space-x-3 text-gray-100">
51+
<div className="site-footer-icons flex space-x-3 text-gray-100">
5152
<a className=iconLink rel="noopener noreferrer" href=Constants.githubHref>
5253
<Icon.GitHub className="w-6 h-6" />
5354
</a>

src/components/LandingPage.res

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ export {
7070
let (example, _setExample) = React.useState(_ => examples->Array.getUnsafe(0))
7171

7272
//Playground Section & Background
73-
<section className="relative mt-20 bg-gray-10">
73+
<section className="lp-playground-hero relative mt-20 bg-gray-10">
7474
<div className="relative flex justify-center w-full">
7575
<div className="relative w-full pt-6 pb-8 sm:px-8 md:px-16 max-w-[1400px]">
7676
// Playground widget
@@ -686,7 +686,7 @@ let make = (~components=MarkdownComponents.default) => {
686686
description="Fast, Simple, Fully Typed JavaScript from the Future"
687687
keywords=["ReScript", "rescriptlang", "JavaScript", "JS", "TypeScript"]
688688
/>
689-
<div className="mt-4 xs:mt-16">
689+
<div id="landing-page" className="mt-4 xs:mt-16">
690690
<div className="text-gray-80 text-18 z">
691691
<div className="absolute w-full top-16">
692692
<Banner>

src/components/NavbarPrimary.res

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,29 @@ let isActive = (~url, ~pathname: Path.t) => {
77
: "hover:text-fire-30"
88
}
99

10+
module ThemeToggle = {
11+
@react.component
12+
let make = (~theme: SiteTheme.t, ~onToggle: unit => unit) => {
13+
let (label, title) = switch theme {
14+
| SiteTheme.Light => ("Light", "Switch to dark mode")
15+
| SiteTheme.Dark => ("Dark", "Switch to light mode")
16+
}
17+
18+
<button
19+
className="rounded border border-gray-60 px-2 py-1 text-12 hover:cursor-pointer hover:text-white"
20+
onClick={evt => {
21+
ReactEvent.Mouse.preventDefault(evt)
22+
onToggle()
23+
}}
24+
title
25+
ariaLabel={title}
26+
dataTestId="theme-toggle"
27+
>
28+
{React.string(label)}
29+
</button>
30+
}
31+
}
32+
1033
module LeftContent = {
1134
@react.component
1235
let make = () => {
@@ -44,14 +67,15 @@ module LeftContent = {
4467

4568
module RightContent = {
4669
@react.component
47-
let make = () => {
70+
let make = (~theme, ~onToggleTheme) => {
4871
let iconClasses = "w-6 h-6 opacity-50 hover:opacity-100"
4972
let linkClasses = "hidden md:block"
5073
<div
5174
dataTestId="navbar-primary-right-content"
5275
className="row-start-1 justify-self-end col-[content] grid grid-flow-col items-center space-x-5 text-gray-40"
5376
>
5477
<Search />
78+
<ThemeToggle theme onToggle=onToggleTheme />
5579
<button
5680
className={"h-1 w-auto block md:hidden opacity-50 hover:opacity-100 m-0"}
5781
onClick={toggleMobileOverlay}
@@ -95,8 +119,23 @@ module RightContent = {
95119

96120
@react.component
97121
let make = () => {
122+
let (theme, setTheme) = React.useState(_ => SiteTheme.Light)
98123
let scrollDirection = Hooks.useScrollDirection(~topMargin=64, ~threshold=32)
99124

125+
React.useEffect(() => {
126+
let initialTheme = SiteTheme.getInitial()
127+
setTheme(_ => initialTheme)
128+
SiteTheme.applyToDom(initialTheme)
129+
None
130+
}, [])
131+
132+
let onToggleTheme = () =>
133+
setTheme(prev => {
134+
let next = SiteTheme.toggle(prev)
135+
SiteTheme.set(next)
136+
next
137+
})
138+
100139
let navbarClasses = switch scrollDirection {
101140
| Up(_) => "translate-y-0"
102141
| Down(_) => "-translate-y-full md:translate-y-0"
@@ -112,7 +151,7 @@ let make = () => {
112151
`}
113152
>
114153
<LeftContent />
115-
<RightContent />
154+
<RightContent theme onToggleTheme />
116155
</nav>
117156
<NavbarMobileOverlay />
118157
</>

0 commit comments

Comments
 (0)