From e3c82362a8b3f307b3f10d8d649f83a6a538d536 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Thu, 16 Apr 2026 07:21:43 -0400 Subject: [PATCH 1/8] feat(playground): add light mode toggle and announcement toast Add a dark/light playground theme with default dark and persisted theme selection. Wire runtime CodeMirror theme reconfiguration, theme-aware JS output highlighting, and settings controls. Add a new light-mode toast with dismiss and try-it-now actions, auto-hide timeout, and session-based seen tracking. --- plans/playground-light-mode-plan.md | 93 ++++++++++ src/Playground.res | 269 ++++++++++++++++++++++++---- src/components/CodeMirror.res | 187 ++++++++++++++++--- src/components/CodeMirror.resi | 8 + 4 files changed, 493 insertions(+), 64 deletions(-) create mode 100644 plans/playground-light-mode-plan.md diff --git a/plans/playground-light-mode-plan.md b/plans/playground-light-mode-plan.md new file mode 100644 index 000000000..32059c208 --- /dev/null +++ b/plans/playground-light-mode-plan.md @@ -0,0 +1,93 @@ +# Playground Light Mode Implementation Plan + +## Goal + +Add light mode support to the Playground and let users switch between dark/light from the Settings tab, with persistent preference. + +## Status + +- Overall: `In progress` +- Owner: `Codex + Josh` +- Last updated: `2026-04-16` + +## Scope + +- In scope: + - Theme model for Playground (`Dark | Light`) + - Settings UI toggle to switch theme + - Persist theme in `localStorage` + - Apply theme to CodeMirror editor + - Apply theme to JavaScript tab syntax highlighting + - Apply theme to Playground shell/container styles as needed + - Basic validation (build + targeted tests) +- Out of scope: + - Global site-wide light/dark mode system + - URL/share-link theme parameter + +## Decisions + +- Default theme: `Dark` (for backward compatibility) +- Persistence key: `playgroundTheme` (to confirm before implementation) +- Theme updates should apply immediately without page reload + +## Implementation Phases + +### Phase 1: Theme State + Wiring + +- [x] Add Playground-local theme type and conversion helpers +- [x] Read initial theme from `localStorage` +- [x] Persist theme changes to `localStorage` +- [x] Thread theme through Playground modules that need it + +### Phase 2: CodeMirror Theme Support + +- [x] Add `Theme` type to `src/components/CodeMirror.res` + `.resi` +- [x] Add `theme` to `editorConfig` +- [x] Add theme compartment to editor instance +- [x] Implement `themeToExtension` for dark and light variants +- [x] Add `editorSetTheme` API for runtime switching + +### Phase 3: Settings Toggle + +- [x] Add “Playground Theme” section in Settings tab +- [x] Reuse existing toggle/select UI patterns +- [x] Wire setting change to state + `editorSetTheme` + +### Phase 4: Visual Integration + +- [x] Switch JS tab highlighting from hardcoded dark to selected theme +- [x] Adjust playground shell classes for both themes +- [x] Ensure contrast/readability in Output, JS, Problems, Settings tabs +- [ ] Add any minimal scoped CSS needed for scrollbar/theme polish + +### Phase 4.5: Feature Toast + +- [x] Show a “new light mode” toast on Playground +- [x] Add dismiss action +- [x] Auto-hide after 10 seconds +- [x] Persist toast “seen” state to avoid repeated display + +### Phase 5: Verification + +- [ ] `yarn build:res` +- [ ] `yarn test` (or focused checks if full suite is slow) +- [ ] Update/add Playground e2e test for: + - [ ] Toggle to light mode + - [ ] Reload persistence + - [ ] Editor and JS panel theme effect + +## Risks / Watchouts + +- CodeMirror theme must remain readable for diagnostics (warnings/errors) in both modes. +- Playground has many hardcoded dark utility classes; missing one can cause mixed-theme UI. +- Avoid touching generated `.jsx` files; only edit `.res`/`.resi`/CSS sources. + +## Progress Log + +- `2026-04-16`: Plan document created. +- `2026-04-16`: Implemented light/dark theme state, CodeMirror runtime theming, settings toggle, JS tab theme support, and new light mode toast behavior. + +## Open Questions + +- Should theme preference remain local to Playground only (recommended), or align with any future site-wide preference? +- Confirm persistence key name: `playgroundTheme`? diff --git a/src/Playground.res b/src/Playground.res index df8aa010c..8e1d0e3d6 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -36,13 +36,32 @@ module ExperimentalFeatures = { } let breakingPoint = 1024 +let playgroundThemeStorageKey = "playgroundTheme" +let newLightModeToastSeenStorageKey = "playgroundLightModeToastSeen" + +let isDarkTheme = (theme: CodeMirror.Theme.t): bool => + switch theme { + | CodeMirror.Theme.Dark => true + | CodeMirror.Theme.Light => false + } + +let themeLabel = (theme: CodeMirror.Theme.t): string => + switch theme { + | CodeMirror.Theme.Dark => "Dark" + | CodeMirror.Theme.Light => "Light" + } 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" : "" >))} - className="inline-block p-1 max-w-20 outline-hidden bg-gray-90 placeholder-gray-20/50" + className={"inline-block p-1 max-w-20 outline-hidden " ++ ( + isDarkTheme(theme) + ? "bg-gray-90 text-gray-20 placeholder-gray-20/50" + : "bg-gray-10 text-gray-80 placeholder-gray-60" + )} placeholder="Flags" type_="text" tabIndex=0 @@ -868,9 +909,11 @@ module Settings = { ~editorCode: React.ref, ~config: Api.Config.t, ~keyMapState: (CodeMirror.KeyMap.t, (CodeMirror.KeyMap.t => CodeMirror.KeyMap.t) => unit), + ~themeState: (CodeMirror.Theme.t, (CodeMirror.Theme.t => CodeMirror.Theme.t) => unit), ) => { let {Api.Config.warnFlags: warnFlags} = config let (keyMap, setKeyMap) = keyMapState + let (theme, setTheme) = themeState let availableTargetLangs = Api.Version.availableLanguages(readyState.selected.apiVersion) @@ -924,13 +967,14 @@ module Settings = { let onCompilerSelect = id => dispatch(SwitchToCompiler(id)) - let titleClass = "hl-5 text-gray-20 mb-2" -
+ let titleClass = "hl-5 mb-2 " ++ (isDarkTheme(theme) ? "text-gray-20" : "text-gray-80") +
{React.string("ReScript Version")}
{ ReactEvent.Form.preventDefault(evt) let id: string = (evt->ReactEvent.Form.target)["value"] @@ -1018,6 +1062,7 @@ module Settings = { lang->Api.Lang.toExt->String.toUpperCase} + theme selected=readyState.targetLang onChange=onTargetLangSelect /> @@ -1029,6 +1074,7 @@ module Settings = {
{React.string("Use Vim Keymap")}
switch enabled { | CodeMirror.KeyMap.Vim => "On" @@ -1042,17 +1088,29 @@ module Settings = {
{React.string("Module-System")}
value} selected=config.moduleSystem onChange=onModuleSystemUpdate />
+
+
{React.string("Playground Theme")}
+ setTheme(_ => value)} + /> +
{readyState.selected.apiVersion->RescriptCompilerApi.Version.isMinimumVersion(V6) ? <>
{React.string("JSX")}
Option.getOr(false)->JsxCompilation.fromBool} onChange=onJsxPreserveModeUpdate @@ -1067,6 +1125,7 @@ module Settings = { ExperimentalFeatures.getLabel} isActive={config.experimentalFeatures ->Option.getOr([]) @@ -1098,7 +1157,7 @@ module Settings = {
- +
@@ -1195,6 +1254,7 @@ module ControlPanel = { let make = ( ~actionIndicatorKey: string, ~state: CompilerManagerHook.state, + ~theme: CodeMirror.Theme.t, ~dispatch: CompilerManagerHook.action => unit, ~editorRef: React.ref>, ~setCurrentTab: (tab => tab) => unit, @@ -1277,7 +1337,56 @@ module ControlPanel = { | _ => React.null } -
children
+
+ children +
+ } +} + +module NewLightModeToast = { + @react.component + let make = (~theme: CodeMirror.Theme.t, ~onClose, ~onTryNow) => { + let containerClass = isDarkTheme(theme) + ? "bg-gray-90 text-gray-20 border-gray-70" + : "bg-white text-gray-80 border-gray-30" +
+
+
+
{React.string("New: Light Mode")}
+
+ {React.string("You can now switch Playground theme in Settings.")} +
+
+ + +
+
} } @@ -1300,20 +1409,22 @@ module OutputPanel = { ~compilerState: CompilerManagerHook.state, ~editorCode: React.ref, ~keyMapState: (CodeMirror.KeyMap.t, (CodeMirror.KeyMap.t => CodeMirror.KeyMap.t) => unit), + ~themeState: (CodeMirror.Theme.t, (CodeMirror.Theme.t => CodeMirror.Theme.t) => unit), ~currentTab: tab, ) => { + let (theme, _setTheme) = themeState let output = -
+
{switch compilerState { | Compiling({previousJsCode: Some(jsCode)}) | Executing({jsCode}) | Ready({result: Comp(Success({jsCode}))}) =>
-            {HighlightJs.renderHLJS(~code=jsCode, ~darkmode=true, ~lang="js", ())}
+            {HighlightJs.renderHLJS(~code=jsCode, ~darkmode=isDarkTheme(theme), ~lang="js", ())}
           
| Ready({result: Conv(Success(_))}) => React.null | Ready({result, targetLang, selected}) => - + | _ => React.null }}
@@ -1326,6 +1437,7 @@ module OutputPanel = { | SetupFailed(msg) =>
{React.string("Setup failed: " ++ msg)}
@@ -1341,7 +1453,13 @@ module OutputPanel = { let setConfig = config => compilerDispatch(UpdateConfig(config)) | SetupFailed(msg) =>
{React.string("Setup failed: " ++ msg)}
| Init =>
{React.string("Initalizing Playground...")}
@@ -1631,19 +1749,51 @@ let make = (~bundleBaseUrl: string, ~versions: array) => { ) let (keyMap, setKeyMap) = React.useState(() => CodeMirror.KeyMap.Default) + let (theme, setTheme) = React.useState(() => + WebAPI.Storage.getItem(window.localStorage, playgroundThemeStorageKey) + ->Null.toOption + ->Option.map(CodeMirror.Theme.fromString) + ->Option.getOr(CodeMirror.Theme.Dark) + ) + let (showNewLightModeToast, setShowNewLightModeToast) = React.useState(_ => false) let typingTimer = React.useRef(None) let timeoutCompile = React.useRef(_ => ()) React.useEffect(() => { setKeyMap(_ => - Dom.Storage2.localStorage - ->Dom.Storage2.getItem("vimMode") + WebAPI.Storage.getItem(window.localStorage, "vimMode") + ->Null.toOption ->Option.map(CodeMirror.KeyMap.fromString) ->Option.getOr(CodeMirror.KeyMap.Default) ) None }, []) + React.useEffect(() => { + let hasSeenToast = + WebAPI.Storage.getItem(window.sessionStorage, newLightModeToastSeenStorageKey) + ->Null.toOption + ->Option.isSome + + if hasSeenToast { + None + } else { + setShowNewLightModeToast(_ => true) + + let hideToast = () => { + setShowNewLightModeToast(_ => false) + WebAPI.Storage.setItem( + window.sessionStorage, + ~key=newLightModeToastSeenStorageKey, + ~value="true", + ) + } + + let timer = setTimeout(~handler=hideToast, ~timeout=10000) + Some(() => clearTimeout(timer)) + } + }, []) + React.useEffect(() => { switch containerRef.current { | Value(parent) => @@ -1659,6 +1809,7 @@ let make = (~bundleBaseUrl: string, ~versions: array) => { readOnly: false, lineNumbers: true, lineWrapping: false, + theme, keyMap: CodeMirror.KeyMap.Default, errors: [], hoverHints: [], @@ -1684,11 +1835,25 @@ let make = (~bundleBaseUrl: string, ~versions: array) => { }, []) React.useEffect(() => { - Dom.Storage2.localStorage->Dom.Storage2.setItem("vimMode", CodeMirror.KeyMap.toString(keyMap)) + WebAPI.Storage.setItem( + window.localStorage, + ~key="vimMode", + ~value=CodeMirror.KeyMap.toString(keyMap), + ) editorRef.current->Option.forEach(CodeMirror.editorSetKeyMap(_, keyMap)) None }, [keyMap]) + React.useEffect(() => { + WebAPI.Storage.setItem( + window.localStorage, + ~key=playgroundThemeStorageKey, + ~value=theme->CodeMirror.Theme.toString, + ) + editorRef.current->Option.forEach(CodeMirror.editorSetTheme(_, theme)) + None + }, [theme]) + let editorCode = React.useRef(initialContent) /* @@ -1929,10 +2094,30 @@ let make = (~bundleBaseUrl: string, ~versions: array) => { let (currentTab, setCurrentTab) = React.useState(_ => JavaScript) + let hideNewLightModeToast = () => { + setShowNewLightModeToast(_ => false) + WebAPI.Storage.setItem( + window.sessionStorage, + ~key=newLightModeToastSeenStorageKey, + ~value="true", + ) + } + + let tryLightModeFromToast = () => { + setTheme(_ => CodeMirror.Theme.Light) + hideNewLightModeToast() + } + let disabled = false let makeTabClass = active => { - let activeClass = active ? "text-white border-sky-70! font-medium hover:cursor-default" : "" + let activeClass = if active { + isDarkTheme(theme) + ? "text-white border-sky-70! font-medium hover:cursor-default" + : "text-gray-80 border-sky-70! font-medium hover:cursor-default" + } else { + "" + } "flex-1 items-center p-4 border-t-4 border-transparent " ++ activeClass } @@ -2011,10 +2196,15 @@ let make = (~bundleBaseUrl: string, ~versions: array) => { }) -
+
) => { : "h-full!"} ${layout == Column ? "w-full" : "w-[50%]"}`} >
>))} />
@@ -2039,10 +2229,9 @@ let make = (~bundleBaseUrl: string, ~versions: array) => {
>))} // TODO: touch-none not applied - className={`flex items-center justify-center touch-none select-none bg-gray-70 opacity-30 hover:opacity-50 rounded-lg ${layout == - Column - ? "cursor-row-resize" - : "cursor-col-resize"}`} + className={"flex items-center justify-center touch-none select-none opacity-30 hover:opacity-50 rounded-lg " ++ + (isDarkTheme(theme) ? "bg-gray-70" : "bg-gray-20") ++ + " " ++ (layout == Column ? "cursor-row-resize" : "cursor-col-resize")} onMouseDown={onMouseDown} onTouchStart={onTouchStart} onTouchEnd={onMouseUp} @@ -2066,10 +2255,20 @@ let make = (~bundleBaseUrl: string, ~versions: array) => { className="overflow-auto playground-scrollbar" >
+ {if showNewLightModeToast { + + } else { + React.null + }} } diff --git a/src/components/CodeMirror.res b/src/components/CodeMirror.res index b4e0802c8..6fb68f1fc 100644 --- a/src/components/CodeMirror.res +++ b/src/components/CodeMirror.res @@ -25,6 +25,22 @@ module KeyMap = { } } +module Theme = { + type t = Dark | Light + + let toString = (theme: t) => + switch theme { + | Dark => "dark" + | Light => "light" + } + + let fromString = (str: string) => + switch str { + | "light" => Light + | _ => Dark + } +} + module Side = { @@warning("-37") type t = @@ -429,7 +445,7 @@ module CM6 = { @scope("HighlightStyle") @module("@codemirror/language") external define: array => t = "define" - let default = define([ + let dark = define([ { tag: [Tags.keyword, Tags.moduleKeyword, Tags.operator], class: "text-berry-dark-50", @@ -478,6 +494,56 @@ module CM6 = { color: "#bcc9ab", }, ]) + + let light = define([ + { + tag: [Tags.keyword, Tags.moduleKeyword, Tags.operator], + class: "text-berry", + }, + { + tag: [Tags.variableName, Tags.labelName, Tags.special(Tags.angleBracket)], + class: "text-gray-80", + }, + { + tag: [ + Tags.bool, + Tags.atom, + Tags.typeName, + Tags.special(Tags.tagName), + Tags.definition(Tags.typeName), + ], + class: "text-orange", + }, + { + tag: [Tags.string, Tags.special(Tags.string), Tags.number], + class: "text-turtle", + }, + { + tag: [Tags.comment], + class: "text-gray-60", + }, + { + tag: [Tags.definition(Tags.namespace)], + class: "text-orange", + }, + { + tag: [Tags.namespace], + class: "text-water", + }, + { + tag: [ + Tags.annotation, + Tags.tagName, + Tags.propertyName, + Tags.definition(Tags.propertyName), + ], + class: "text-sky", + }, + { + tag: [Tags.attributeName, Tags.labelName, Tags.definition(Tags.variableName)], + color: "#4f5f78", + }, + ]) } type t @@ -564,6 +630,7 @@ module CM6 = { type editorInstance = { view: CM6.editorView, languageConf: CM6.compartment, + themeConf: CM6.compartment, readOnlyConf: CM6.compartment, keymapConf: CM6.compartment, lintConf: CM6.compartment, @@ -577,6 +644,7 @@ type editorConfig = { readOnly: bool, lineNumbers: bool, lineWrapping: bool, + theme: Theme.t, keyMap: KeyMap.t, onChange?: string => unit, errors: array, @@ -699,37 +767,18 @@ let keyMapToExtension = (keyMap: KeyMap.t) => [defaultKeymapExt, historyKeymapExt, searchKeymapExt]->CM6.Extension.fromArray } -let createEditor = (config: editorConfig): editorInstance => { - // Setup language based on mode - let language = switch config.mode { - | "rescript" => ReScript.extension - | "reason" => CM6.CustomLanguages.reasonLanguage - | _ => CM6.JavaScript.javascript() - } - - // Setup compartments for dynamic config - let languageConf = CM6.Compartment.create() - let readOnlyConf = CM6.Compartment.create() - let keymapConf = CM6.Compartment.create() - let lintConf = CM6.Compartment.create() - let hintConf = CM6.Compartment.create() - - let lineHeight = "1.5" - let cursorColor = "#dd8c1b" - - // Basic extensions - let extensions = [ - CM6.Compartment.make(languageConf, (language: CM6.extension)), - CM6.Commands.history(), - CM6.EditorView.theme( +let themeToExtension = (theme: Theme.t): CM6.extension => + switch theme { + | Theme.Dark => + let lineHeight = "1.5" + let cursorColor = "#dd8c1b" + let editorTheme = CM6.EditorView.theme( dict{ ".cm-content": dict{ "lineHeight": lineHeight, "caretColor": cursorColor, }, - ".cm-line": dict{ - "lineHeight": lineHeight, - }, + ".cm-line": dict{"lineHeight": lineHeight}, ".cm-cursor, .cm-dropCursor": dict{"borderLeftColor": cursorColor}, ".cm-activeLine": dict{ "backgroundColor": "rgba(255, 255, 255, 0.02)", @@ -750,12 +799,79 @@ let createEditor = (config: editorConfig): editorInstance => { ".cm-selectionMatch": dict{"backgroundColor": "#aafe661a"}, }, ~options={dark: true}, - ), + ) + let syntaxHighlight = CM6.Language.syntaxHighlighting( + CM6.Language.HighlightStyle.dark, + {fallback: true}, + ) + [editorTheme, syntaxHighlight]->CM6.Extension.fromArray + | Theme.Light => + let lineHeight = "1.5" + let cursorColor = "#2258c3" + let editorTheme = CM6.EditorView.theme( + dict{ + "&": dict{ + "backgroundColor": "#ffffff", + "color": "#232538", + }, + ".cm-content": dict{ + "lineHeight": lineHeight, + "caretColor": cursorColor, + }, + ".cm-line": dict{"lineHeight": lineHeight}, + ".cm-cursor, .cm-dropCursor": dict{"borderLeftColor": cursorColor}, + ".cm-activeLine": dict{ + "backgroundColor": "rgba(34, 88, 195, 0.07)", + }, + ".cm-gutters": dict{ + "color": "#696b7d", + "backgroundColor": "#fafbfc", + "borderRight": "1px solid #edf0f2", + }, + ".cm-gutters.cm-gutters-before": dict{"border": "none"}, + ".cm-gutterElement.cm-activeLineGutter": dict{ + "color": "#232538", + "backgroundColor": "#edf0f2", + }, + "&.cm-focused > .cm-scroller > .cm-selectionLayer .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": dict{ + "backgroundColor": "rgba(34, 88, 195, 0.22)", + }, + ".cm-selectionMatch": dict{"backgroundColor": "rgba(94, 94, 222, 0.18)"}, + }, + ~options={dark: false}, + ) + let syntaxHighlight = CM6.Language.syntaxHighlighting( + CM6.Language.HighlightStyle.light, + {fallback: true}, + ) + [editorTheme, syntaxHighlight]->CM6.Extension.fromArray + } + +let createEditor = (config: editorConfig): editorInstance => { + // Setup language based on mode + let language = switch config.mode { + | "rescript" => ReScript.extension + | "reason" => CM6.CustomLanguages.reasonLanguage + | _ => CM6.JavaScript.javascript() + } + + // Setup compartments for dynamic config + let languageConf = CM6.Compartment.create() + let themeConf = CM6.Compartment.create() + let readOnlyConf = CM6.Compartment.create() + let keymapConf = CM6.Compartment.create() + let lintConf = CM6.Compartment.create() + let hintConf = CM6.Compartment.create() + + // Basic extensions + let extensions = [ + CM6.Compartment.make(languageConf, (language: CM6.extension)), + CM6.Commands.history(), + CM6.Compartment.make(themeConf, themeToExtension(config.theme)), CM6.EditorView.drawSelection(), CM6.EditorView.dropCursor(), CM6.Language.bracketMatching(), CM6.Search.highlightSelectionMatches(), - CM6.Language.syntaxHighlighting(CM6.Language.HighlightStyle.default, {fallback: true}), ] // Add optional extensions @@ -825,6 +941,7 @@ let createEditor = (config: editorConfig): editorInstance => { { view, languageConf, + themeConf, readOnlyConf, keymapConf, lintConf, @@ -929,6 +1046,18 @@ let editorSetKeyMap = (instance: editorInstance, keyMap: KeyMap.t): unit => { ) } +let editorSetTheme = (instance: editorInstance, theme: Theme.t): unit => { + CM6.EditorView.dispatchEffects( + instance.view, + { + effects: CM6.Compartment.reconfigure( + instance.themeConf, + (theme->themeToExtension: CM6.extension), + ), + }, + ) +} + let editorSetMode = (instance: editorInstance, mode: string): unit => { let language = switch mode { | "rescript" => ReScript.extension diff --git a/src/components/CodeMirror.resi b/src/components/CodeMirror.resi index 96d0ea221..6d7a39d24 100644 --- a/src/components/CodeMirror.resi +++ b/src/components/CodeMirror.resi @@ -4,6 +4,12 @@ module KeyMap: { let fromString: string => t } +module Theme: { + type t = Dark | Light + let toString: t => string + let fromString: string => t +} + module Error: { type kind = [#Error | #Warning] @@ -41,6 +47,7 @@ type editorConfig = { readOnly: bool, lineNumbers: bool, lineWrapping: bool, + theme: Theme.t, keyMap: KeyMap.t, onChange?: string => unit, errors: array, @@ -54,6 +61,7 @@ let createEditor: editorConfig => editorInstance let editorSetErrors: (editorInstance, array) => unit let editorSetMode: (editorInstance, string) => unit let editorSetKeyMap: (editorInstance, KeyMap.t) => unit +let editorSetTheme: (editorInstance, Theme.t) => unit let editorDestroy: editorInstance => unit let editorSetValue: (editorInstance, string) => unit let editorGetValue: editorInstance => string From 2678cb287b67c5a218257700b76ca3233068b485 Mon Sep 17 00:00:00 2001 From: Josh Vlk Date: Thu, 16 Apr 2026 07:44:28 -0400 Subject: [PATCH 2/8] fix(playground): improve light mode contrast and cover theme toggle flow Improve light-mode readability for the Auto-run control and center divider styling. Add Cypress coverage for toast-driven light mode switch and switching back to dark in settings. Co-authored-by: Codex --- e2e/Playground.cy.res | 30 ++++++++++++++++++++++++++++++ src/Playground.res | 13 ++++++++++--- src/components/ToggleButton.res | 20 ++++++++++++++------ 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/e2e/Playground.cy.res b/e2e/Playground.cy.res index 1c81f4e76..67e435bac 100644 --- a/e2e/Playground.cy.res +++ b/e2e/Playground.cy.res @@ -64,4 +64,34 @@ describe("Playground", () => { ->shouldContainText("Hello ReScript!") ->ignore }) + + it("should switch to light mode from toast and back to dark mode in settings", () => { + // Navigate to playground and wait for initial render + clickNavLink(~testId="navbar-primary-left-content", ~text="Playground") + url()->shouldInclude("/try")->ignore + waitForPlayground() + + // Switch to light mode through the onboarding toast + getByTestId("playground-lightmode-toast") + ->should("be.visible") + ->find("button") + ->containsChainable("Try it now") + ->click + ->ignore + + // Verify playground shell is in light mode + get("main")->shouldWithValue("have.class", "bg-gray-5")->ignore + + // Switch back to dark mode from Settings + contains("Settings")->click->ignore + contains("Playground Theme") + ->closest("div") + ->find("button") + ->containsChainable("Dark") + ->click + ->ignore + + // Verify playground shell is back to dark mode + get("main")->shouldWithValue("have.class", "bg-gray-100")->ignore + }) }) diff --git a/src/Playground.res b/src/Playground.res index 8e1d0e3d6..4bcafe595 100644 --- a/src/Playground.res +++ b/src/Playground.res @@ -1311,6 +1311,7 @@ module ControlPanel = {
{ switch state { | Ready({autoRun: false}) => setCurrentTab(_ => Output) @@ -2229,14 +2230,20 @@ let make = (~bundleBaseUrl: string, ~versions: array) => {
>))} // TODO: touch-none not applied - className={"flex items-center justify-center touch-none select-none opacity-30 hover:opacity-50 rounded-lg " ++ - (isDarkTheme(theme) ? "bg-gray-70" : "bg-gray-20") ++ + className={"flex items-center justify-center touch-none select-none rounded-lg " ++ + (isDarkTheme(theme) + ? "bg-gray-70 opacity-30 hover:opacity-50" + : "bg-gray-20 border border-gray-30 opacity-100 hover:bg-gray-30") ++ " " ++ (layout == Column ? "cursor-row-resize" : "cursor-col-resize")} onMouseDown={onMouseDown} onTouchStart={onTouchStart} onTouchEnd={onMouseUp} > - + {React.string("⣿")}
diff --git a/src/components/ToggleButton.res b/src/components/ToggleButton.res index 1112da7f4..c3078ee74 100644 --- a/src/components/ToggleButton.res +++ b/src/components/ToggleButton.res @@ -1,16 +1,24 @@ @react.component -let make = (~checked, ~onChange, ~children) => { +let make = (~checked, ~onChange, ~children, ~isLightTheme=false) => { + let switchThemeClass = if isLightTheme { + "bg-gray-30 after:bg-white after:border-gray-40 border-gray-40 peer-checked:bg-sky" + } else { + "bg-gray-700 after:bg-white after:border-gray-300 border-gray-600 peer-checked:bg-sky" + } + + let labelThemeClass = isLightTheme ? "text-gray-80" : "text-gray-300" +