diff --git a/tests/spec/features/assistance_spec.rb b/tests/spec/features/assistance_spec.rb index b224136df..fe8a051cb 100644 --- a/tests/spec/features/assistance_spec.rb +++ b/tests/spec/features/assistance_spec.rb @@ -1,11 +1,15 @@ require 'spec_helper' require 'support/editor' +require 'support/notifications' require 'support/playground_actions' RSpec.feature "Editor assistance for common code modifications", type: :feature, js: true do include PlaygroundActions - before { visit '/' } + before do + visit '/' + Notifications.new(page).close_all + end scenario "building code without a main method offers adding one" do editor.set <<~EOF diff --git a/tests/spec/support/notifications.rb b/tests/spec/support/notifications.rb new file mode 100644 index 000000000..6b2ce183f --- /dev/null +++ b/tests/spec/support/notifications.rb @@ -0,0 +1,14 @@ +class Notifications + attr_reader :page + def initialize(page) + @page = page + end + + def close_all + page.all(:notification).each do |notification| + page.within(notification) do + page.click_on('dismiss notification') + end + end + end +end diff --git a/ui/frontend/ConfigMenu.tsx b/ui/frontend/ConfigMenu.tsx index beca9a779..8f9727ce2 100644 --- a/ui/frontend/ConfigMenu.tsx +++ b/ui/frontend/ConfigMenu.tsx @@ -16,7 +16,6 @@ import { ProcessAssembly, Theme, } from './types'; -import { showThemeSelector } from './selectors'; const MONACO_THEMES = [ 'vs', 'vs-dark', 'vscode-dark-plus', @@ -34,8 +33,6 @@ const ConfigMenu: React.FC = () => { const demangleAssembly = useAppSelector((state) => state.configuration.demangleAssembly); const processAssembly = useAppSelector((state) => state.configuration.processAssembly); - const showTheme = useAppSelector(showThemeSelector); - const dispatch = useAppDispatch(); const changeAceTheme = useCallback((t: string) => dispatch(config.changeAceTheme(t)), [dispatch]); const changeMonacoTheme = useCallback((t: string) => dispatch(config.changeMonacoTheme(t)), [dispatch]); @@ -104,13 +101,12 @@ const ConfigMenu: React.FC = () => { - {showTheme && ( - - { /* */ } - - - - )} + + + + + + { return (
+
@@ -22,6 +25,55 @@ const Notifications: React.FC = () => { ); }; +const DarkModeNotification: React.FC = () => { + const showIt = useAppSelector(selectors.showDarkModeSelector); + + const dispatch = useAppDispatch(); + const seenIt = useCallback(() => dispatch(seenDarkMode()), [dispatch]); + const swapToLight = useCallback(() => dispatch(swapTheme(Theme.Light)), [dispatch]); + const swapToDark = useCallback(() => dispatch(swapTheme(Theme.Dark)), [dispatch]); + const swapToSystem = useCallback(() => dispatch(swapTheme(Theme.System)), [dispatch]); + + return showIt ? ( + +

The playground now has a dark mode! Sample the themes here:

+ + + + + + + + + + + + + + + + +
+ + Use your system's preference
+ + The classic playground style
+ + Reduce the number of photons hitting your eyeballs
+ +

+ You can change the current UI theme (and the editor's theme) in the configuration menu. +

+
+ ) : null; +}; + const RustSurvey2023Notification: React.FC = () => { const showIt = useAppSelector(selectors.showRustSurvey2023Selector); @@ -69,7 +121,7 @@ interface NotificationProps { const Notification: React.FC = ({ onClose, children }) => (
{children}
-
diff --git a/ui/frontend/configureStore.ts b/ui/frontend/configureStore.ts index 5d128cf6b..381d81b8d 100644 --- a/ui/frontend/configureStore.ts +++ b/ui/frontend/configureStore.ts @@ -23,7 +23,7 @@ export default function configureStore(window: Window) { const baseUrl = new URL('/', window.location.href).href; const websocket = websocketMiddleware(window); - const prefersDarkTheme = false && window.matchMedia('(prefers-color-scheme: dark)').matches; + const prefersDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches; const initialThemes = prefersDarkTheme ? editorDarkThemes : {}; const initialGlobalState = { diff --git a/ui/frontend/index.module.css b/ui/frontend/index.module.css index 3f91ac4f7..e5fa8e5d5 100644 --- a/ui/frontend/index.module.css +++ b/ui/frontend/index.module.css @@ -166,13 +166,11 @@ @mixin light-theme-vars; } -/* @media (prefers-color-scheme: dark) { :root { @mixin dark-theme-vars; } } -*/ [data-theme='dark']:root { @mixin dark-theme-vars; diff --git a/ui/frontend/reducers/configuration.ts b/ui/frontend/reducers/configuration.ts index 5395602c7..a0524bd82 100644 --- a/ui/frontend/reducers/configuration.ts +++ b/ui/frontend/reducers/configuration.ts @@ -124,6 +124,22 @@ const slice = createSlice({ changeProcessAssembly: (state, action: PayloadAction) => { state.processAssembly = action.payload; }, + + swapTheme: (state, action: PayloadAction) => { + state.theme = action.payload; + switch (action.payload) { + case Theme.Light: { + state.ace.theme = 'github'; + state.monaco.theme = 'vs'; + break; + } + case Theme.Dark: { + state.ace.theme = 'github_dark'; + state.monaco.theme = 'vscode-dark-plus'; + break; + } + } + }, }, }); @@ -143,6 +159,7 @@ export const { changePairCharacters, changePrimaryAction, changeProcessAssembly, + swapTheme, } = slice.actions; export const changeEdition = diff --git a/ui/frontend/reducers/featureFlags.ts b/ui/frontend/reducers/featureFlags.ts index 6522afbb3..43bf308be 100644 --- a/ui/frontend/reducers/featureFlags.ts +++ b/ui/frontend/reducers/featureFlags.ts @@ -6,7 +6,6 @@ import { createWebsocketResponse } from '../websocketActions'; interface State { forced: boolean; showGemThreshold: number; - showThemeThreshold: number; } const ENABLED = 1.0; @@ -15,14 +14,12 @@ const DISABLED = -1.0; const initialState: State = { forced: false, showGemThreshold: DISABLED, - showThemeThreshold: DISABLED, }; const { action: wsFeatureFlags, schema: wsFeatureFlagsSchema } = createWebsocketResponse( 'featureFlags', z.object({ showGemThreshold: z.number().nullish(), - showThemeThreshold: z.number().nullish(), }), ); @@ -33,12 +30,10 @@ const slice = createSlice({ forceEnableAll: (state) => { state.forced = true; state.showGemThreshold = ENABLED; - state.showThemeThreshold = ENABLED; }, forceDisableAll: (state) => { state.forced = true; state.showGemThreshold = DISABLED; - state.showThemeThreshold = DISABLED; }, }, extraReducers: (builder) => { @@ -47,15 +42,11 @@ const slice = createSlice({ return; } - const { showGemThreshold, showThemeThreshold } = action.payload; + const { showGemThreshold } = action.payload; if (showGemThreshold) { state.showGemThreshold = showGemThreshold; } - - if (showThemeThreshold) { - state.showThemeThreshold = showThemeThreshold; - } }); }, }); diff --git a/ui/frontend/reducers/notifications.ts b/ui/frontend/reducers/notifications.ts index 1116e7b4d..f56135307 100644 --- a/ui/frontend/reducers/notifications.ts +++ b/ui/frontend/reducers/notifications.ts @@ -11,6 +11,7 @@ interface State { seenMonacoEditorAvailable: boolean; // expired seenRustSurvey2022: boolean; // expired seenRustSurvey2023: boolean; + seenDarkMode: boolean; } const initialState: State = { @@ -22,6 +23,7 @@ const initialState: State = { seenMonacoEditorAvailable: true, seenRustSurvey2022: true, seenRustSurvey2023: false, + seenDarkMode: false, }; const slice = createSlice({ @@ -30,6 +32,9 @@ const slice = createSlice({ reducers: { notificationSeen: (state, action: PayloadAction) => { switch (action.payload) { + case Notification.DarkMode: { + state.seenDarkMode = true; + } case Notification.RustSurvey2023: { state.seenRustSurvey2023 = true; } @@ -40,6 +45,7 @@ const slice = createSlice({ const { notificationSeen } = slice.actions; +export const seenDarkMode = () => notificationSeen(Notification.DarkMode); export const seenRustSurvey2023 = () => notificationSeen(Notification.RustSurvey2023); export default slice.reducer; diff --git a/ui/frontend/selectors/index.ts b/ui/frontend/selectors/index.ts index ac9ae8a01..e3afbca9c 100644 --- a/ui/frontend/selectors/index.ts +++ b/ui/frontend/selectors/index.ts @@ -357,6 +357,13 @@ const notificationsSelector = (state: State) => state.notifications; const NOW = new Date(); +const DARK_MODE_END = new Date('2024-10-15T00:00:00Z'); +const DARK_MODE_OPEN = NOW <= DARK_MODE_END; +export const showDarkModeSelector = createSelector( + notificationsSelector, + notifications => DARK_MODE_OPEN && !notifications.seenDarkMode, +); + const RUST_SURVEY_2023_END = new Date('2024-01-15T00:00:00Z'); const RUST_SURVEY_2023_OPEN = NOW <= RUST_SURVEY_2023_END; export const showRustSurvey2023Selector = createSelector( @@ -365,6 +372,7 @@ export const showRustSurvey2023Selector = createSelector( ); export const anyNotificationsToShowSelector = createSelector( + showDarkModeSelector, showRustSurvey2023Selector, excessiveExecutionSelector, (...allNotifications) => allNotifications.some(n => n), @@ -444,13 +452,11 @@ const websocket = (state: State) => state.websocket; const clientFeatureFlagThreshold = createSelector(client, (c) => c.featureFlagThreshold); const showGemThreshold = createSelector(featureFlags, ff => ff.showGemThreshold); -const showThemeThreshold = createSelector(featureFlags, ff => ff.showThemeThreshold); const createFeatureFlagSelector = (ff: (state: State) => number) => createSelector(clientFeatureFlagThreshold, ff, (c, ff) => c <= ff); export const showGemSelector = createFeatureFlagSelector(showGemThreshold); -export const showThemeSelector = createFeatureFlagSelector(showThemeThreshold); export const executeViaWebsocketSelector = createSelector(websocket, (ws) => ws.connected); diff --git a/ui/frontend/types.ts b/ui/frontend/types.ts index a028ab920..11d5fa7c2 100644 --- a/ui/frontend/types.ts +++ b/ui/frontend/types.ts @@ -165,5 +165,6 @@ export enum Focus { } export enum Notification { + DarkMode = 'dark-mode', RustSurvey2023 = 'rust-survey-2023', }