Merge pull request #31329 from overleaf/mj-dark-mode-notification

[web] Introduce notification for dashboard dark mode

GitOrigin-RevId: da5045d412ddc87a4b18823a4a5fa3192fe15c89
This commit is contained in:
Mathias Jakobsen
2026-02-11 10:43:45 +00:00
committed by Copybot
parent 55c3b6b7ea
commit 498c89c6ed
20 changed files with 369 additions and 89 deletions

View File

@@ -31,6 +31,7 @@ const VALID_KEYS = [
'old-editor-warning-tooltip',
'old-editor-warning-tooltip-2',
'workbench-rail-popover',
'themed-dashboard-intro',
]
async function completeTutorial(req, res, next) {

View File

@@ -14,6 +14,7 @@
"Pricing": "",
"Solutions": "",
"a_custom_size_has_been_used_in_the_latex_code": "",
"a_dashboard_that_follows_your_lead": "",
"a_file_with_that_name_already_exists_and_will_be_overriden": "",
"a_more_comprehensive_list_of_keyboard_shortcuts": "",
"a_new_reference_was_added": "",
@@ -601,6 +602,7 @@
"failed_to_send_managed_user_invite_to_email": "",
"failed_to_send_sso_link_invite_to_email": "",
"fair_usage_policy_applies": "",
"fancy_going_dark": "",
"fast": "",
"fast_draft": "",
"feature_enabled_or_disabled": "",
@@ -761,6 +763,7 @@
"go_to_writefull": "",
"good_news_you_already_purchased_this_add_on": "",
"good_news_you_are_already_receiving_this_add_on_via_writefull": "",
"got_it": "",
"got_questions": "",
"group_admin": "",
"group_audit_logs": "",
@@ -1091,6 +1094,7 @@
"math_inline": "",
"maximum_files_uploaded_together": "",
"maybe_later": "",
"meet_the_new_dark_dashboard": "",
"members_added": "",
"members_management": "",
"mendeley": "",
@@ -2219,11 +2223,15 @@
"wed_love_you_to_stay": "",
"welcome_to_overleaf_opening_workspace": "",
"welcome_to_sl": "",
"welcome_to_the_dark_side": "",
"well_be_here_when_youre_ready": "",
"were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "",
"were_performing_maintenance": "",
"weve_given_your_dashboard_a_sleek_new_dark_theme_for_more_comfortable_late_night_research_prefer_the_light_switch_back_anytime_right_here": "",
"weve_hit_a_problem_try_starting_a_new_chat": "",
"weve_made_it_easier_to_find_and_use_the_tools_you_need_today": "",
"weve_matched_your_dashboard_theme_to_your_editor_preferences_but_you_can_change_that_here_anytime": "",
"weve_set_your_dashboard_to_dark_mode_to_help_you_stay_focused_if_youre_a_fan_of_a_lighter_look_you_can_easily_switch_themes_here": "",
"what_did_you_find_most_helpful": "",
"what_do_you_need_help_with": "",
"what_does_this_mean_for_you": "",
@@ -2308,6 +2316,7 @@
"your_current_plan_gives_you": "",
"your_current_plan_supports_up_to_x_licenses": "",
"your_current_project_will_revert_to_the_version_from_time": "",
"your_dashboard_is_set_to_match_your_system_theme_automatically_want_a_different_look_pick_your_favorite_theme_here": "",
"your_email_is_confirmed": "",
"your_feedback_matters_answer_two_quick_questions": "",
"your_git_access_info": "",

View File

@@ -7,13 +7,13 @@ import { isVersionSelected } from '../../utils/history-details'
import { useUserContext } from '../../../../shared/context/user-context'
import useDropdownActiveItem from '../../hooks/use-dropdown-active-item'
import { useHistoryContext } from '../../context/history-context'
import { useEditorContext } from '../../../../shared/context/editor-context'
import OLPopover from '@/shared/components/ol/ol-popover'
import OLOverlay from '@/shared/components/ol/ol-overlay'
import Close from '@/shared/components/close'
import { Trans, useTranslation } from 'react-i18next'
import MaterialIcon from '@/shared/components/material-icon'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import { useTutorialContext } from '@/shared/context/tutorial-context'
function AllHistoryList() {
const { id: currentUserId } = useUserContext()
@@ -91,7 +91,7 @@ function AllHistoryList() {
}
}, [updatesLoadingState])
const { inactiveTutorials } = useEditorContext()
const { inactiveTutorials } = useTutorialContext()
const {
showPopup: showHistoryTutorial,
tryShowingPopup: tryShowingHistoryTutorial,

View File

@@ -5,12 +5,12 @@ import OLFormGroup from '@/shared/components/ol/ol-form-group'
import OLIconButton from '@/shared/components/ol/ol-icon-button'
import { OLToast } from '@/shared/components/ol/ol-toast'
import { OLToastContainer } from '@/shared/components/ol/ol-toast-container'
import { useEditorContext } from '@/shared/context/editor-context'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import { memo, useCallback, useEffect, useMemo, useState } from 'react'
import { sendMB } from '@/infrastructure/event-tracking'
import { useIsNewEditorEnabled } from '@/features/ide-redesign/utils/new-editor-utils'
import { useTranslation } from 'react-i18next'
import { useTutorialContext } from '@/shared/context/tutorial-context'
type EditorSurveyPage = 'ease-of-use' | 'meets-my-needs' | 'thank-you'
@@ -28,7 +28,7 @@ const EditorSurveyContent = () => {
const [easeOfUse, setEaseOfUse] = useState<number | null>(null)
const [meetsMyNeeds, setMeetsMyNeeds] = useState<number | null>(null)
const [page, setPage] = useState<EditorSurveyPage>('ease-of-use')
const { inactiveTutorials } = useEditorContext()
const { inactiveTutorials } = useTutorialContext()
const hasCompletedSurvey = inactiveTutorials.includes(TUTORIAL_KEY)
const newEditor = useIsNewEditorEnabled()

View File

@@ -32,6 +32,7 @@ import { CommandRegistryProvider } from './command-registry-context'
import { NewEditorTourProvider } from '@/features/ide-redesign/contexts/new-editor-tour-context'
import { EditorSelectionProvider } from '@/shared/context/editor-selection-context'
import importOverleafModules from '../../../../macros/import-overleaf-module.macro'
import { TutorialProvider } from '@/shared/context/tutorial-context'
const rootContextProviders = importOverleafModules('rootContextProviders') as {
import: { default: ElementType }
@@ -76,6 +77,7 @@ export const ReactContextRoot: FC<
UserFeaturesProvider,
NewEditorTourProvider,
EditorSelectionProvider,
TutorialProvider,
...providers,
}
@@ -105,47 +107,49 @@ export const ReactContextRoot: FC<
<Providers.EditorViewProvider>
<Providers.EditorOpenDocProvider>
<Providers.EditorProvider>
<Providers.FileTreeDataProvider>
<Providers.FileTreePathProvider>
<Providers.UserFeaturesProvider>
<Providers.PermissionsProvider>
<Providers.RailProvider>
<Providers.LayoutProvider>
<Providers.NewEditorTourProvider>
<Providers.ProjectSettingsProvider>
<Providers.EditorManagerProvider>
<Providers.ReferencesProvider>
<Providers.LocalCompileProvider>
<Providers.DetachCompileProvider>
<Providers.ChatProvider>
<Providers.FileTreeOpenProvider>
<Providers.OnlineUsersProvider>
<Providers.MetadataProvider>
<Providers.OutlineProvider>
<Providers.CommandRegistryProvider>
<Providers.EditorSelectionProvider>
{
childrenWrappedWithDynamicProviders
}
</Providers.EditorSelectionProvider>
</Providers.CommandRegistryProvider>
</Providers.OutlineProvider>
</Providers.MetadataProvider>
</Providers.OnlineUsersProvider>
</Providers.FileTreeOpenProvider>
</Providers.ChatProvider>
</Providers.DetachCompileProvider>
</Providers.LocalCompileProvider>
</Providers.ReferencesProvider>
</Providers.EditorManagerProvider>
</Providers.ProjectSettingsProvider>
</Providers.NewEditorTourProvider>
</Providers.LayoutProvider>
</Providers.RailProvider>
</Providers.PermissionsProvider>
</Providers.UserFeaturesProvider>
</Providers.FileTreePathProvider>
</Providers.FileTreeDataProvider>
<Providers.TutorialProvider>
<Providers.FileTreeDataProvider>
<Providers.FileTreePathProvider>
<Providers.UserFeaturesProvider>
<Providers.PermissionsProvider>
<Providers.RailProvider>
<Providers.LayoutProvider>
<Providers.NewEditorTourProvider>
<Providers.ProjectSettingsProvider>
<Providers.EditorManagerProvider>
<Providers.ReferencesProvider>
<Providers.LocalCompileProvider>
<Providers.DetachCompileProvider>
<Providers.ChatProvider>
<Providers.FileTreeOpenProvider>
<Providers.OnlineUsersProvider>
<Providers.MetadataProvider>
<Providers.OutlineProvider>
<Providers.CommandRegistryProvider>
<Providers.EditorSelectionProvider>
{
childrenWrappedWithDynamicProviders
}
</Providers.EditorSelectionProvider>
</Providers.CommandRegistryProvider>
</Providers.OutlineProvider>
</Providers.MetadataProvider>
</Providers.OnlineUsersProvider>
</Providers.FileTreeOpenProvider>
</Providers.ChatProvider>
</Providers.DetachCompileProvider>
</Providers.LocalCompileProvider>
</Providers.ReferencesProvider>
</Providers.EditorManagerProvider>
</Providers.ProjectSettingsProvider>
</Providers.NewEditorTourProvider>
</Providers.LayoutProvider>
</Providers.RailProvider>
</Providers.PermissionsProvider>
</Providers.UserFeaturesProvider>
</Providers.FileTreePathProvider>
</Providers.FileTreeDataProvider>
</Providers.TutorialProvider>
</Providers.EditorProvider>
</Providers.EditorOpenDocProvider>
</Providers.EditorViewProvider>

View File

@@ -9,15 +9,15 @@ import { useCallback, useEffect, useState } from 'react'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import OLButton from '@/shared/components/ol/ol-button'
import { useTranslation } from 'react-i18next'
import { useEditorContext } from '@/shared/context/editor-context'
import { useIsNewToNewEditor } from '../utils/new-editor-utils'
import { useNewEditorTourContext } from '../contexts/new-editor-tour-context'
import promoVideo from './new-editor-promo-video.mp4'
import { useTutorialContext } from '@/shared/context/tutorial-context'
const TUTORIAL_KEY = 'new-editor-intro-2'
export default function NewEditorOptOutIntroModal() {
const { inactiveTutorials } = useEditorContext()
const { inactiveTutorials } = useTutorialContext()
const {
tryShowingPopup,
showPopup: showModal,

View File

@@ -5,8 +5,8 @@ import { useTranslation } from 'react-i18next'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import { useCallback, useEffect, useState } from 'react'
import { useSwitchEnableNewEditorState } from '../hooks/use-switch-enable-new-editor-state'
import { useEditorContext } from '@/shared/context/editor-context'
import { canUseNewEditor } from '../utils/new-editor-utils'
import { useTutorialContext } from '@/shared/context/tutorial-context'
const TUTORIAL_KEY = 'old-editor-warning-tooltip-2'
@@ -15,7 +15,7 @@ export default function OldEditorWarningTooltip({
}: {
target: HTMLElement | null
}) {
const { inactiveTutorials } = useEditorContext()
const { inactiveTutorials } = useTutorialContext()
const { t } = useTranslation()
const { loading, setEditorRedesignStatus } = useSwitchEnableNewEditorState()

View File

@@ -1,5 +1,5 @@
import Close from '@/shared/components/close'
import { useEditorContext } from '@/shared/context/editor-context'
import { useTutorialContext } from '@/shared/context/tutorial-context'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import classNames from 'classnames'
@@ -26,7 +26,7 @@ export default function TooltipPromotion({
placement?: OverlayProps['placement']
splitTestName?: string
}) {
const { inactiveTutorials } = useEditorContext()
const { inactiveTutorials } = useTutorialContext()
const { showPopup, tryShowingPopup, hideUntilReload, dismissTutorial } =
useTutorial(tutorialKey, eventData)

View File

@@ -1,17 +1,17 @@
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import { useEditorContext } from '@/shared/context/editor-context'
import { useProjectContext } from '@/shared/context/project-context'
import OLNotification from '@/shared/components/ol/ol-notification'
import { useTranslation, Trans } from 'react-i18next'
import { useCallback } from 'react'
import { onRollingBuild } from '@/shared/utils/rolling-build'
import { useTutorialContext } from '@/shared/context/tutorial-context'
export const TUTORIAL_KEY = 'rolling-compile-image-changed'
const RollingCompileImageChangedAlert = () => {
const { completeTutorial } = useTutorial(TUTORIAL_KEY)
const { project } = useProjectContext()
const { inactiveTutorials } = useEditorContext()
const { inactiveTutorials } = useTutorialContext()
const { t } = useTranslation()

View File

@@ -21,6 +21,7 @@ import { DsNavStyleProvider } from '@/features/project-list/components/use-is-ds
import CookieBanner from '@/shared/components/cookie-banner'
import useThemedPage from '@/shared/hooks/use-themed-page'
import { UserSettingsProvider } from '@/shared/context/user-settings-context'
import { TutorialProvider } from '@/shared/context/tutorial-context'
function ProjectListRoot() {
const { isReady } = useWaitForI18n()
@@ -37,9 +38,11 @@ export function ProjectListRootInner() {
<ProjectListProvider>
<ColorPickerProvider>
<SplitTestProvider>
<UserSettingsProvider>
<ProjectListPageContent />
</UserSettingsProvider>
<TutorialProvider>
<UserSettingsProvider>
<ProjectListPageContent />
</UserSettingsProvider>
</TutorialProvider>
</SplitTestProvider>
</ColorPickerProvider>
</ProjectListProvider>

View File

@@ -18,6 +18,8 @@ import { useScrolled } from '@/features/project-list/components/sidebar/use-scro
import { useSendProjectListMB } from '@/features/project-list/components/project-list-events'
import { SurveyWidgetDsNav } from '@/features/project-list/components/survey-widget-ds-nav'
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { ThemedProjectDashboardNotification } from './themed-project-dashboard-notification'
import { useThemedDashboardIntro } from './use-themed-dashboard-intro'
function SidebarDsNav() {
const { t } = useTranslation()
@@ -38,6 +40,12 @@ function SidebarDsNav() {
) as NavbarDropdownItemData
const { containerRef, scrolledUp, scrolledDown } = useScrolled()
const themedDsNav = useFeatureFlag('themed-project-dashboard')
const {
completeThemedDashboardIntro,
dismissThemedDashboardIntro,
targetRef,
showingThemedDashboardIntro,
} = useThemedDashboardIntro()
return (
<div
@@ -131,6 +139,12 @@ function SidebarDsNav() {
item: 'account',
location: 'sidebar',
})
if (showingThemedDashboardIntro) {
completeThemedDashboardIntro({
action: 'complete',
event: 'promo-click',
})
}
}
}}
role="menu"
@@ -144,7 +158,7 @@ function SidebarDsNav() {
}}
hidden={showAccountDropdown}
>
<div>
<div ref={targetRef}>
<User size={24} />
</div>
</OLTooltip>
@@ -166,7 +180,16 @@ function SidebarDsNav() {
/>
</Dropdown.Menu>
</Dropdown>
<UserProvider>{contactUsModal}</UserProvider>
<UserProvider>
{themedDsNav && (
<ThemedProjectDashboardNotification
target={targetRef.current}
show={showingThemedDashboardIntro}
onDismiss={dismissThemedDashboardIntro}
/>
)}
{contactUsModal}
</UserProvider>
</>
)}
</nav>

View File

@@ -0,0 +1,102 @@
import Close from '@/shared/components/close'
import OLButton from '@/shared/components/ol/ol-button'
import { useUserContext } from '@/shared/context/user-context'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { useActiveOverallTheme } from '@/shared/hooks/use-active-overall-theme'
import { Overlay, Popover } from 'react-bootstrap'
import { useTranslation } from 'react-i18next'
// TODO: Update this before release
const NEW_USER_CUTOFF_DATE = new Date('2026-02-15')
type ThemedProjectDashboardNotificationProps = {
target: HTMLElement | null
show: boolean
onDismiss: () => void
}
export const ThemedProjectDashboardNotification = ({
target,
show,
onDismiss,
}: ThemedProjectDashboardNotificationProps) => {
const theme = useActiveOverallTheme('themed-project-dashboard')
const {
userSettings: { overallTheme },
} = useUserSettingsContext()
const { signUpDate: signUpDateString } = useUserContext()
const signUpDate = signUpDateString ? new Date(signUpDateString) : new Date(0)
const isNewUser = signUpDate > NEW_USER_CUTOFF_DATE
const { t } = useTranslation()
if (!target) {
return null
}
if (!show) {
return null
}
let content
switch (true) {
case overallTheme === 'system':
content = {
header: t('a_dashboard_that_follows_your_lead'),
body: t(
'your_dashboard_is_set_to_match_your_system_theme_automatically_want_a_different_look_pick_your_favorite_theme_here'
),
}
break
case isNewUser && theme === 'dark':
content = {
header: t('meet_the_new_dark_dashboard'),
body: t(
'weve_set_your_dashboard_to_dark_mode_to_help_you_stay_focused_if_youre_a_fan_of_a_lighter_look_you_can_easily_switch_themes_here'
),
}
break
case theme === 'dark':
content = {
header: t('welcome_to_the_dark_side'),
body: t(
'weve_given_your_dashboard_a_sleek_new_dark_theme_for_more_comfortable_late_night_research_prefer_the_light_switch_back_anytime_right_here'
),
}
break
case theme === 'light':
content = {
header: t('fancy_going_dark'),
body: t(
'weve_matched_your_dashboard_theme_to_your_editor_preferences_but_you_can_change_that_here_anytime'
),
}
break
default:
content = { header: '', body: '' }
}
if (!content.header || !content.body) {
return null
}
return (
<Overlay show placement="right" target={target} onHide={onDismiss}>
<Popover className="themed-dashboard-intro-popover">
<Popover.Header>
{content.header}
<Close
variant={theme === 'light' ? 'dark' : 'light'}
onDismiss={onDismiss}
/>
</Popover.Header>
<Popover.Body>
{content.body}
<div className="d-flex justify-content-end">
<OLButton onClick={onDismiss} variant="link">
{t('got_it')}
</OLButton>
</div>
</Popover.Body>
</Popover>
</Overlay>
)
}

View File

@@ -0,0 +1,38 @@
import { useFeatureFlag } from '@/shared/context/split-test-context'
import { useTutorialContext } from '@/shared/context/tutorial-context'
import useTutorial from '@/shared/hooks/promotions/use-tutorial'
import { useCallback, useEffect, useRef } from 'react'
const THEMED_DASHBOARD_TUTORIAL_KEY = 'themed-dashboard-intro'
export const useThemedDashboardIntro = () => {
const themedDsNav = useFeatureFlag('themed-project-dashboard')
const targetRef = useRef<HTMLDivElement | null>(null)
const { inactiveTutorials } = useTutorialContext()
const {
tryShowingPopup: tryShowingPopupThemedDashboardIntro,
showPopup: showingThemedDashboardIntro,
completeTutorial: completeThemedDashboardIntro,
dismissTutorial,
} = useTutorial(THEMED_DASHBOARD_TUTORIAL_KEY, {
name: THEMED_DASHBOARD_TUTORIAL_KEY,
})
const dismissThemedDashboardIntro = useCallback(() => {
dismissTutorial()
}, [dismissTutorial])
useEffect(() => {
if (
themedDsNav &&
!inactiveTutorials.includes(THEMED_DASHBOARD_TUTORIAL_KEY)
) {
tryShowingPopupThemedDashboardIntro()
}
}, [inactiveTutorials, tryShowingPopupThemedDashboardIntro, themedDsNav])
return {
targetRef,
showingThemedDashboardIntro,
completeThemedDashboardIntro,
dismissThemedDashboardIntro,
}
}

View File

@@ -29,10 +29,6 @@ export const EditorContext = createContext<
isProjectOwner: boolean
isRestrictedTokenMember?: boolean
isPendingEditor: boolean
deactivateTutorial: (tutorial: string) => void
inactiveTutorials: string[]
currentPopup: string | null
setCurrentPopup: Dispatch<SetStateAction<string | null>>
hasPremiumSuggestion: boolean
setHasPremiumSuggestion: (value: boolean) => void
setPremiumSuggestionResetDate: (date: Date) => void
@@ -77,11 +73,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
)
}, [])
const [inactiveTutorials, setInactiveTutorials] = useState(
() => getMeta('ol-inactiveTutorials') || []
)
const [currentPopup, setCurrentPopup] = useState<string | null>(null)
const [hasPremiumSuggestion, setHasPremiumSuggestion] = useState<boolean>(
() => {
return Boolean(
@@ -111,13 +102,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
[members, userId]
)
const deactivateTutorial = useCallback(
(tutorialKey: string) => {
setInactiveTutorials([...inactiveTutorials, tutorialKey])
},
[inactiveTutorials]
)
const renameProject = useCallback(
(newName: string) => {
const oldName = projectName
@@ -180,10 +164,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
isRestrictedTokenMember: getMeta('ol-isRestrictedTokenMember'),
isPendingEditor,
insertSymbol,
inactiveTutorials,
deactivateTutorial,
currentPopup,
setCurrentPopup,
hasPremiumSuggestion,
setHasPremiumSuggestion,
premiumSuggestionResetDate,
@@ -201,10 +181,6 @@ export const EditorProvider: FC<React.PropsWithChildren> = ({ children }) => {
renameProject,
isPendingEditor,
insertSymbol,
inactiveTutorials,
deactivateTutorial,
currentPopup,
setCurrentPopup,
hasPremiumSuggestion,
setHasPremiumSuggestion,
premiumSuggestionResetDate,

View File

@@ -0,0 +1,64 @@
import getMeta from '@/utils/meta'
import {
createContext,
Dispatch,
FC,
SetStateAction,
useCallback,
useContext,
useMemo,
useState,
} from 'react'
export const TutorialContext = createContext<
| {
deactivateTutorial: (tutorial: string) => void
inactiveTutorials: string[]
currentPopup: string | null
setCurrentPopup: Dispatch<SetStateAction<string | null>>
}
| undefined
>(undefined)
export const TutorialProvider: FC<React.PropsWithChildren> = ({ children }) => {
const [inactiveTutorials, setInactiveTutorials] = useState(
() => getMeta('ol-inactiveTutorials') || []
)
const [currentPopup, setCurrentPopup] = useState<string | null>(null)
const deactivateTutorial = useCallback(
(tutorialKey: string) => {
setInactiveTutorials([...inactiveTutorials, tutorialKey])
},
[inactiveTutorials]
)
const value = useMemo(
() => ({
deactivateTutorial,
inactiveTutorials,
currentPopup,
setCurrentPopup,
}),
[deactivateTutorial, inactiveTutorials, currentPopup, setCurrentPopup]
)
return (
<TutorialContext.Provider value={value}>
{children}
</TutorialContext.Provider>
)
}
export function useTutorialContext() {
const context = useContext(TutorialContext)
if (!context) {
throw new Error(
'useTutorialContext is only available inside TutorialProvider'
)
}
return context
}

View File

@@ -2,7 +2,7 @@ import { useCallback, useState } from 'react'
import * as eventTracking from '@/infrastructure/event-tracking'
import { postJSON } from '@/infrastructure/fetch-json'
import { debugConsole } from '@/utils/debugging'
import { useEditorContext } from '@/shared/context/editor-context'
import { useTutorialContext } from '@/shared/context/tutorial-context'
type CompleteTutorialParams = {
event: string
@@ -22,7 +22,7 @@ const useTutorial = (
const [showPopup, setShowPopup] = useState(false)
const { deactivateTutorial, currentPopup, setCurrentPopup } =
useEditorContext()
useTutorialContext()
const completeTutorial = useCallback(
async (

View File

@@ -4,6 +4,11 @@
--theme-toggle-selected-background: var(--green-20);
--ds-nav-content-bg-secondary: var(--bg-light-secondary);
--table-icon-bg-hover: 27 34 44;
--themed-dashboard-popover-bg: var(--bg-dark-primary);
--themed-dashboard-popover-color: var(--content-primary-dark);
--themed-dashboard-popover-link-color: var(--link-ui-dark);
--themed-dashboard-popover-link-hover-color: var(--link-ui-hover-dark);
--themed-dashboard-popover-link-visited-color: var(--link-ui-visited-dark);
@include theme('default') {
--ds-nav-active-bg: var(--green-70);
@@ -11,6 +16,11 @@
--theme-toggle-selected-background: var(--green-70);
--ds-nav-content-bg-secondary: var(--bg-dark-secondary);
--table-icon-bg-hover: 255 255 255;
--themed-dashboard-popover-bg: var(--bg-light-primary);
--themed-dashboard-popover-color: var(--content-primary);
--themed-dashboard-popover-link-color: var(--link-ui);
--themed-dashboard-popover-link-hover-color: var(--link-ui-hover);
--themed-dashboard-popover-link-visited-color: var(--link-ui-visited);
}
}
@@ -420,3 +430,26 @@ body {
background-color: var(--theme-toggle-selected-background);
}
}
.themed-dashboard-intro-popover {
--bs-popover-bg: var(--themed-dashboard-popover-bg);
--bs-popover-header-bg: var(--themed-dashboard-popover-bg);
--bs-popover-body-color: var(--themed-dashboard-popover-color);
--bs-popover-header-color: var(--themed-dashboard-popover-color);
.btn-link {
color: var(--bs-popover-body-color);
}
a {
color: var(--themed-dashboard-popover-link-color);
&:visited {
color: var(--themed-dashboard-popover-link-hover-color);
}
&:hover {
color: var(--themed-dashboard-popover-link-visited-color);
}
}
}

View File

@@ -15,6 +15,7 @@
"Terms": "Terms",
"Universities": "Universities",
"a_custom_size_has_been_used_in_the_latex_code": "A custom size has been used in the LaTeX code.",
"a_dashboard_that_follows_your_lead": "A dashboard that follows your lead",
"a_file_with_that_name_already_exists_and_will_be_overriden": "A file with that name already exists. That file will be overwritten.",
"a_more_comprehensive_list_of_keyboard_shortcuts": "A more comprehensive list of keyboard shortcuts can be found in <0>this __appName__ project template</0>",
"a_new_reference_was_added": "A new reference was added",
@@ -771,6 +772,7 @@
"failed_to_send_managed_user_invite_to_email": "Failed to send Managed User invite to <0>__email__</0>. Please try again later.",
"failed_to_send_sso_link_invite_to_email": "Failed to send SSO invite reminder to <0>__email__</0>. Please try again later.",
"fair_usage_policy_applies": "Fair usage policy applies.",
"fancy_going_dark": "Fancy going dark?",
"faq_how_does_free_trial_works_answer": "You get full access to your chosen __appName__ plan during your __len__-day free trial. There is no obligation to continue beyond the trial. Your card will be charged at the end of your __len__ day trial unless you cancel before then. You can cancel via your subscription settings.",
"fast": "Fast",
"fast_draft": "Fast [draft]",
@@ -974,6 +976,7 @@
"go_to_writefull": "Go to Writefull",
"good_news_you_already_purchased_this_add_on": "Good news! You already have this add-on, so no need to pay again.",
"good_news_you_are_already_receiving_this_add_on_via_writefull": "Good news! You already have this add-on via your Writefull subscription. No need to pay again.",
"got_it": "Got it",
"got_questions": "Got questions?",
"great_for_getting_started": "Great for getting started",
"great_for_small_teams_and_departments": "Great for small teams and departments",
@@ -1413,6 +1416,7 @@
"maximum_files_uploaded_together": "Maximum __max__ files uploaded together",
"may": "May",
"maybe_later": "Maybe later",
"meet_the_new_dark_dashboard": "Meet the new dark dashboard",
"member_picker": "Select number of users for group plan",
"members_added": "Member(s) added.",
"members_management": "Members management",
@@ -2774,11 +2778,15 @@
"wed_love_you_to_stay": "Wed love you to stay",
"welcome_to_overleaf_opening_workspace": "Welcome to Overleaf. Opening your workspace.",
"welcome_to_sl": "Welcome to __appName__",
"welcome_to_the_dark_side": "Welcome to the dark side",
"well_be_here_when_youre_ready": "Well be here when youre ready to dive back in! 🦆",
"were_making_some_changes_to_project_sharing_this_means_you_will_be_visible": "Were making some <0>changes to project sharing</0>. This means, as someone with edit access, your name and email address will be visible to the project owner and other editors.",
"were_performing_maintenance": "Were performing maintenance on Overleaf and you need to wait a moment. Sorry for any inconvenience. The editor will refresh automatically in __seconds__ seconds.",
"weve_given_your_dashboard_a_sleek_new_dark_theme_for_more_comfortable_late_night_research_prefer_the_light_switch_back_anytime_right_here": "Weve given your dashboard a sleek new dark theme for more comfortable late-night research. Prefer the light? Switch back anytime right here.",
"weve_hit_a_problem_try_starting_a_new_chat": "Weve hit a problem. Try starting a new chat.",
"weve_made_it_easier_to_find_and_use_the_tools_you_need_today": "The new editor design makes it easier to find and use the tools you need today, while making space for the new features youll love tomorrow.",
"weve_matched_your_dashboard_theme_to_your_editor_preferences_but_you_can_change_that_here_anytime": "Weve matched your dashboard theme to your editor preferences, but you can change that here anytime.",
"weve_set_your_dashboard_to_dark_mode_to_help_you_stay_focused_if_youre_a_fan_of_a_lighter_look_you_can_easily_switch_themes_here": "Weve set your dashboard to dark mode to help you stay focused. If youre a fan of a lighter look, you can easily switch themes here.",
"what_did_you_find_most_helpful": "What did you find most helpful?",
"what_do_you_need": "What do you need?",
"what_do_you_need_help_with": "What do you need help with?",
@@ -2876,6 +2884,7 @@
"your_current_plan_gives_you": "By pausing your subscription, youll be able to access your premium features faster when you need them again.",
"your_current_plan_supports_up_to_x_licenses": "Your current plan supports up to __users__ licenses.",
"your_current_project_will_revert_to_the_version_from_time": "Your current project will revert to the version from __timestamp__",
"your_dashboard_is_set_to_match_your_system_theme_automatically_want_a_different_look_pick_your_favorite_theme_here": "Your dashboard is set to match your system theme automatically. Want a different look? Pick your favorite theme here.",
"your_email_is_confirmed": "Your email is confirmed.",
"your_feedback_matters_answer_two_quick_questions": "Your feedback matters! Answer two quick questions.",
"your_git_access_info": "Your Git authentication tokens should be entered whenever youre prompted for a password.",

View File

@@ -4,6 +4,7 @@ import { EditorProviders } from '../../helpers/editor-providers'
import { EditorView } from '@codemirror/view'
import type { FC, PropsWithChildren } from 'react'
import { EditorViewContext } from '@/features/ide-react/context/editor-view-context'
import { TutorialProvider } from '@/shared/context/tutorial-context'
describe('Workbench', { scrollBehavior: false }, function () {
beforeEach(function () {
@@ -34,7 +35,7 @@ describe('Workbench', { scrollBehavior: false }, function () {
return (
<EditorProviders
features={{ aiErrorAssistant: aiAssistEnabled }}
providers={{ EditorViewProvider }}
providers={{ EditorViewProvider, TutorialProvider }}
>
<div style={{ backgroundColor: '#1b222c' }}>{children}</div>
</EditorProviders>

View File

@@ -55,6 +55,7 @@ import { DetachCompileContext } from '@/shared/context/detach-compile-context'
import { type CompileContext } from '@/shared/context/local-compile-context'
import { EditorContext } from '@/shared/context/editor-context'
import { Cobranding } from '@ol-types/cobranding'
import { TutorialContext } from '@/shared/context/tutorial-context'
// these constants can be imported in tests instead of
// using magic strings
@@ -248,6 +249,7 @@ export function EditorProviders({
LayoutProvider: makeLayoutProvider(layoutContext),
ProjectProvider: makeProjectProvider(project),
ReferencesProvider: makeReferencesProvider(),
TutorialProvider: makeTutorialProvider(),
...providers,
}
@@ -284,10 +286,6 @@ export function makeEditorProvider({
isProjectOwner,
renameProject,
isPendingEditor: false,
deactivateTutorial: () => {},
inactiveTutorials: [],
currentPopup: null,
setCurrentPopup: () => {},
hasPremiumSuggestion: false,
setHasPremiumSuggestion: () => {},
premiumSuggestionResetDate: new Date(),
@@ -307,6 +305,25 @@ export function makeEditorProvider({
return EditorProvider
}
export const makeTutorialProvider = (opts?: {
inactiveTutorials: string[]
}) => {
const TutorialProvider: FC<PropsWithChildren> = ({ children }) => {
const value = {
deactivateTutorial: () => {},
inactiveTutorials: opts?.inactiveTutorials ?? [],
currentPopup: null,
setCurrentPopup: () => {},
}
return (
<TutorialContext.Provider value={value}>
{children}
</TutorialContext.Provider>
)
}
return TutorialProvider
}
const makeReferencesProvider = () => {
const ReferencesProvider: FC<PropsWithChildren> = ({ children }) => {
return (