Skip to content

Commit c1ee999

Browse files
committed
feat: add dark mode
1 parent 105b8ed commit c1ee999

10 files changed

Lines changed: 245 additions & 16 deletions

File tree

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/Playground.res

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,10 +59,15 @@ let playgroundThemeClass = (theme: CodeMirror.Theme.t): string =>
5959

6060
module DropdownSelect = {
6161
@react.component
62-
let make = (~onChange, ~name, ~value, ~disabled=false, ~children) => {
62+
let make = (~onChange, ~name, ~value, ~theme, ~disabled=false, ~children) => {
63+
let themeClass = switch theme {
64+
| CodeMirror.Theme.Dark => "bg-gray-100 border-gray-80 text-gray-20"
65+
| CodeMirror.Theme.Light => "bg-white border-gray-30 text-gray-80"
66+
}
6367
let opacity = disabled ? " opacity-50" : ""
6468
<select
65-
className={"playground-select text-14 border inline-block rounded px-4 py-1 font-semibold" ++
69+
className={"playground-select text-14 border inline-block rounded px-4 py-1 font-semibold " ++
70+
themeClass ++
6671
opacity}
6772
name
6873
value
@@ -76,12 +81,16 @@ module DropdownSelect = {
7681

7782
module SelectionOption = {
7883
@react.component
79-
let make = (~label, ~isActive, ~disabled, ~onClick) => {
84+
let make = (~label, ~isActive, ~disabled, ~onClick, ~theme) => {
85+
let inactiveClass = switch theme {
86+
| CodeMirror.Theme.Dark => "bg-gray-80 opacity-50 hover:opacity-80 text-gray-20"
87+
| CodeMirror.Theme.Light => "bg-gray-10 border border-gray-30 text-gray-80 hover:bg-gray-20"
88+
}
8089
<button
8190
className={"playground-selection-option mr-1 px-2 py-1 rounded inline-block " ++ if isActive {
8291
"playground-selection-option-active font-bold"
8392
} else {
84-
"opacity-50 hover:opacity-80"
93+
inactiveClass
8594
}}
8695
onClick
8796
disabled
@@ -97,6 +106,7 @@ module ToggleSelection = {
97106
~onChange: 'a => unit,
98107
~values: array<'a>,
99108
~toLabel: 'a => string,
109+
~theme: CodeMirror.Theme.t,
100110
~selected: 'a,
101111
~disabled=false,
102112
) => {
@@ -119,7 +129,7 @@ module ToggleSelection = {
119129
}
120130
}
121131

122-
<SelectionOption key={label} label isActive onClick disabled />
132+
<SelectionOption key={label} label isActive onClick disabled theme />
123133
})
124134
->React.array}
125135
</div>
@@ -954,6 +964,7 @@ module Settings = {
954964
<DropdownSelect
955965
name="compilerVersions"
956966
value={Semver.toString(readyState.selected.id)}
967+
theme
957968
onChange={evt => {
958969
ReactEvent.Form.preventDefault(evt)
959970
let id: string = (evt->ReactEvent.Form.target)["value"]
@@ -1041,6 +1052,7 @@ module Settings = {
10411052
<ToggleSelection
10421053
values=availableTargetLangs
10431054
toLabel={lang => lang->Api.Lang.toExt->String.toUpperCase}
1055+
theme
10441056
selected=readyState.targetLang
10451057
onChange=onTargetLangSelect
10461058
/>
@@ -1052,6 +1064,7 @@ module Settings = {
10521064
<div className=titleClass> {React.string("Use Vim Keymap")} </div>
10531065
<ToggleSelection
10541066
values=[CodeMirror.KeyMap.Default, CodeMirror.KeyMap.Vim]
1067+
theme
10551068
toLabel={enabled =>
10561069
switch enabled {
10571070
| CodeMirror.KeyMap.Vim => "On"
@@ -1065,6 +1078,7 @@ module Settings = {
10651078
<div className=titleClass> {React.string("Module-System")} </div>
10661079
<ToggleSelection
10671080
values=["commonjs", "esmodule"]
1081+
theme
10681082
toLabel={value => value}
10691083
selected=config.moduleSystem
10701084
onChange=onModuleSystemUpdate
@@ -1074,6 +1088,7 @@ module Settings = {
10741088
<div className=titleClass> {React.string("Playground Theme")} </div>
10751089
<ToggleSelection
10761090
values=[CodeMirror.Theme.Dark, CodeMirror.Theme.Light]
1091+
theme
10771092
toLabel=themeLabel
10781093
selected=theme
10791094
onChange={value => setTheme(_ => value)}
@@ -1085,6 +1100,7 @@ module Settings = {
10851100
<div className=titleClass> {React.string("JSX")} </div>
10861101
<ToggleSelection
10871102
values=[JsxCompilation.Plain, PreserveJsx]
1103+
theme
10881104
toLabel=JsxCompilation.getLabel
10891105
selected={config.jsxPreserveMode->Option.getOr(false)->JsxCompilation.fromBool}
10901106
onChange=onJsxPreserveModeUpdate
@@ -1099,6 +1115,7 @@ module Settings = {
10991115
<SelectionOption
11001116
key
11011117
disabled=false
1118+
theme
11021119
label={feature->ExperimentalFeatures.getLabel}
11031120
isActive={config.experimentalFeatures
11041121
->Option.getOr([])
@@ -1227,6 +1244,7 @@ module ControlPanel = {
12271244
let make = (
12281245
~actionIndicatorKey: string,
12291246
~state: CompilerManagerHook.state,
1247+
~theme: CodeMirror.Theme.t,
12301248
~dispatch: CompilerManagerHook.action => unit,
12311249
~editorRef: React.ref<option<CodeMirror.editorInstance>>,
12321250
~setCurrentTab: (tab => tab) => unit,
@@ -1283,6 +1301,7 @@ module ControlPanel = {
12831301
<div className="flex flex-row gap-x-2" dataTestId="control-panel">
12841302
<ToggleButton
12851303
checked=autoRun
1304+
isLightTheme={!isDarkTheme(theme)}
12861305
onChange={_ => {
12871306
switch state {
12881307
| Ready({autoRun: false}) => setCurrentTab(_ => Output)
@@ -2158,6 +2177,7 @@ let make = (~bundleBaseUrl: string, ~versions: array<string>) => {
21582177
<ControlPanel
21592178
actionIndicatorKey={Int.toString(actionCount)}
21602179
state=compilerState
2180+
theme
21612181
dispatch=compilerDispatch
21622182
setCurrentTab
21632183
editorRef

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/CodeMirror.res

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -765,7 +765,6 @@ let themeToExtension = (theme: Theme.t): CM6.extension => {
765765
)
766766
[editorTheme, syntaxHighlight]->CM6.Extension.fromArray
767767
}
768-
769768
let createEditor = (config: editorConfig): editorInstance => {
770769
// Setup language based on mode
771770
let language = switch config.mode {

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
</>

src/components/ToggleButton.res

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
@react.component
2-
let make = (~checked, ~onChange, ~children) => {
2+
let make = (~checked, ~onChange, ~children, ~isLightTheme=false) => {
3+
let _ = isLightTheme
4+
35
<label className="inline-flex items-center cursor-pointer">
46
<input type_="checkbox" value="" checked onChange className="sr-only peer" />
57
<div

0 commit comments

Comments
 (0)