diff --git a/next.config.js b/next.config.js index 1e5f9d5..dffe462 100644 --- a/next.config.js +++ b/next.config.js @@ -69,7 +69,7 @@ module.exports = withBundleAnalyzer({ "default-src 'self'", `script-src ${scriptSrc}`, "style-src 'self' 'unsafe-inline'", - "img-src 'self' data: blob: https://lh3.googleusercontent.com https://cdn.discordapp.com https://strapi.keepsimple.io https://staging-strapi.keepsimple.io https://www.google-analytics.com", + "img-src 'self' data: blob: https://lh3.googleusercontent.com https://cdn.discordapp.com https://strapi.keepsimple.io https://staging-strapi.keepsimple.io https://www.google-analytics.com https://flagcdn.com", "font-src 'self' data:", `connect-src ${connectSrc}`, "frame-ancestors 'none'", diff --git a/src/components/LogIn/LogIn.module.scss b/src/components/LogIn/LogIn.module.scss index 9ad2697..43c1289 100644 --- a/src/components/LogIn/LogIn.module.scss +++ b/src/components/LogIn/LogIn.module.scss @@ -44,34 +44,111 @@ justify-content: center; align-items: center; gap: 16px; + transition: + transform 0.15s ease, + box-shadow 0.15s ease, + filter 0.15s ease; + svg { width: 24px; height: 24px; } + + &:hover { + transform: translateY(-2px); + box-shadow: + 0px 6px 10px rgba(0, 0, 0, 0.18), + 0px 2px 4px rgba(0, 0, 0, 0.1); + filter: brightness(1.05); + } + + &:active { + transform: translateY(0); + box-shadow: + 0px 1px 2px rgba(0, 0, 0, 0.2) inset, + 0px 1px 1px rgba(0, 0, 0, 0.08); + filter: brightness(0.95); + transition-duration: 0.05s; + } + + &:focus-visible { + outline: 2px solid #b83232; + outline-offset: 2px; + } } .googleBtn { background-color: #fff; color: #0000008a; + + &:hover { + background-color: #f7f7f7; + } } .discordBtn { background-color: #5865f2; color: white; + + &:hover { + background-color: #4854e0; + } } .linkedinBtn { background-color: #0177b5; color: white; + + &:hover { + background-color: #015d8d; + } } .twitterBtn { background-color: #000; color: white; + + &:hover { + background-color: #1a1a1a; + } } .mailruBtn { background-color: #168de2; color: white; + + &:hover { + background-color: #1078c0; + } } .yandexBtn { background-color: #fc3f1d; color: white; + + &:hover { + background-color: #e0331a; + } + } + } +} + +// Dark theme: flip the heading / subtitle to read on the dark modal bg. +// The brand-colored provider buttons stay as-is; only the Google button +// (white bg + dark text) gets a slight border so it doesn't blow out +// against the dark surface. +:global(body.darkTheme) .container { + .heading h3 { + color: #dadada !important; + } + + .text { + color: rgba(218, 218, 218, 0.75); + } + + .errorBanner { + background: rgba(180, 35, 24, 0.15); + border-color: rgba(245, 194, 199, 0.4); + color: #f5c2c7; + } + + .btnWrapper { + .googleBtn { + border: 1px solid #303338; } } } diff --git a/src/components/LogIn/MagicLinkEmailForm/MagicLinkEmailForm.module.scss b/src/components/LogIn/MagicLinkEmailForm/MagicLinkEmailForm.module.scss index 028c8a5..f5fe564 100644 --- a/src/components/LogIn/MagicLinkEmailForm/MagicLinkEmailForm.module.scss +++ b/src/components/LogIn/MagicLinkEmailForm/MagicLinkEmailForm.module.scss @@ -134,3 +134,60 @@ margin: 0; text-align: center; } + +// Dark theme: flip the "OR SIGN IN WITH EMAIL" divider, label, email +// input, submit button, banner and confirmation block. +:global(body.darkTheme) { + .divider { + color: rgba(218, 218, 218, 0.55); + + &::before, + &::after { + background: #303338; + } + } + + .label { + color: #dadada; + } + + .input { + background: #151a26; + border-color: #303338; + color: #dadada; + + &::placeholder { + color: rgba(218, 218, 218, 0.45); + } + + &:focus { + border-color: #b83232; + } + } + + .submit { + color: #dadada; + border-color: #303338; + background: transparent; + + &:hover:not(:disabled) { + border-color: #b83232; + color: #ff8a8a; + } + } + + .confirmation, + .banner { + background: rgba(127, 179, 213, 0.08); + border-color: #303338; + color: rgba(218, 218, 218, 0.85); + } + + .confirmationHeading { + color: #dadada; + } + + .confirmationBody { + color: rgba(218, 218, 218, 0.75); + } +} diff --git a/src/components/Modal/Modal.module.scss b/src/components/Modal/Modal.module.scss index 3bc7c2f..8e8c73c 100644 --- a/src/components/Modal/Modal.module.scss +++ b/src/components/Modal/Modal.module.scss @@ -36,6 +36,11 @@ z-index: 80; border-radius: 4px; background-image: url('/keepsimple_/assets/landingPage/landing-bg.webp'); + // Constrain wrapper height so a tall children tree (e.g. Log In with + // six providers + email form) does not push the modal above and below + // the viewport. Body has `overflow: auto`, so content scrolls inside. + max-height: calc(100dvh - 32px); + overflow: hidden; .header { .hadBorder { @@ -146,6 +151,26 @@ .body { padding: 16px 28px; overflow: auto; + // Take remaining flex space and allow shrinking below content so + // overflow: auto can engage when wrapper is bounded by max-height. + flex: 1 1 auto; + min-height: 0; + + // Slim scrollbar to match the rest of the site (default white/wide + // browser scrollbar is jarring inside the modal chrome). + scrollbar-width: thin; + scrollbar-color: rgba(40, 88, 123, 0.4) transparent; + + &::-webkit-scrollbar { + width: 6px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: rgba(40, 88, 123, 0.4); + border-radius: 5px; + } } } @@ -293,3 +318,41 @@ transform: scale(0.97) translateY(8px); } } + +// Dark theme: re-skin the keepsimple modal chrome (wrapper bg, title, +// header dividers, close-icon SVGs). The wrapper carries a paper-texture +// background image on light theme — clear it out so the dark bg reads +// cleanly. +:global(body.darkTheme) .overlay { + background-color: rgba(0, 0, 0, 0.55); + + .wrapper { + background-color: #1b1e26; + background-image: none; + border: 1px solid #303338; + + .body { + scrollbar-color: rgba(127, 179, 213, 0.4) transparent; + + &::-webkit-scrollbar-thumb { + background: rgba(127, 179, 213, 0.4); + } + } + } + + .header { + .title, + .blackTitle, + .grayTitle { + color: #dadada; + } + + .grayTitle { + color: rgba(218, 218, 218, 0.55); + } + } + + .hasBorder { + border-bottom-color: #303338; + } +} diff --git a/src/hooks/useGlobals.ts b/src/hooks/useGlobals.ts index 2b08736..c8c0ebe 100644 --- a/src/hooks/useGlobals.ts +++ b/src/hooks/useGlobals.ts @@ -36,6 +36,30 @@ const reducer = (newState: any) => { /* ACTIONS */ +// Cross-realm theme sync. The keepsimple side and the UX Core side each +// ship their own copy of useGlobals (separate module state, separate +// listener arrays). They share the same localStorage key + body class, so +// persistence is fine — but in-memory state diverges when the user toggles +// in one realm and navigates to the other. To keep them in lockstep +// within a single tab we dispatch a window event on every toggle; both +// realms subscribe and mirror the new value into their own state. +const DARK_THEME_EVENT = 'darktheme:change'; + +if (typeof window !== 'undefined') { + window.addEventListener(DARK_THEME_EVENT, (e: Event) => { + const next = !!(e as CustomEvent).detail?.isDarkTheme; + if (state.isDarkTheme !== next) { + // Side effects (body class, articleRef class, localStorage) are + // already applied by the dispatcher; we only mirror state so this + // realm's listeners re-render with the new value. + if (state.articleRef) { + state.articleRef.classList.toggle('darkTheme', next); + } + reducer({ isDarkTheme: next }); + } + }); +} + // dark theme action const toggleIsDarkTheme = () => { const newThemeState = !state.isDarkTheme; @@ -45,6 +69,11 @@ const toggleIsDarkTheme = () => { state.articleRef.classList.toggle('darkTheme', newThemeState); } reducer({ isDarkTheme: newThemeState }); + window.dispatchEvent( + new CustomEvent(DARK_THEME_EVENT, { + detail: { isDarkTheme: newThemeState }, + }), + ); }; // sidebar action @@ -130,14 +159,15 @@ const initUseGlobals = (articleRef: HTMLElement) => { // Init articleRef setArticleRef(articleRef); - // Dark theme + // Dark theme — apply localStorage value unconditionally (true OR false) + // so navigation between realms always re-syncs in-memory state. const isDarkTheme = localStorage.getItem('darkTheme') === 'true'; - if (isDarkTheme) { - document.body.classList.add('darkTheme'); - if (articleRef) { - articleRef.classList.add('darkTheme'); - } - reducer({ isDarkTheme: true }); + document.body.classList.toggle('darkTheme', isDarkTheme); + if (articleRef) { + articleRef.classList.toggle('darkTheme', isDarkTheme); + } + if (state.isDarkTheme !== isDarkTheme) { + reducer({ isDarkTheme }); } window.addEventListener('resize', handleSidebarChanges); diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 118af23..9787ea9 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -22,7 +22,6 @@ import { LongevityProvider, useLongevity } from '../context/LongevityContext'; import '../styles/globals.scss'; import '../styles/vibesuite.scss'; import '../styles/ai-atlas.css'; -import '../uxcore/styles/uxcore-fonts.scss'; // import '../styles/tom.scss'; type TApp = { @@ -122,6 +121,19 @@ function AppContent({ Component, pageProps: { session, ...pageProps } }: TApp) { }); }, []); + // Cold-load dark-theme bootstrap: not every route calls initUseGlobals + // on mount (e.g. /uxcore, /uxcg). Read the persisted flag once at the + // app root so dark theme applies on any deep-link, and dispatch the + // cross-realm sync event so both useGlobals stores see the value. + useEffect(() => { + if (typeof window === 'undefined') return; + const isDarkTheme = localStorage.getItem('darkTheme') === 'true'; + document.body.classList.toggle('darkTheme', isDarkTheme); + window.dispatchEvent( + new CustomEvent('darktheme:change', { detail: { isDarkTheme } }), + ); + }, []); + useEffect(() => { initUseMobile(); diff --git a/src/pages/uxcore/[slug].tsx b/src/pages/uxcore/[slug].tsx index 930730d..6b1fc19 100644 --- a/src/pages/uxcore/[slug].tsx +++ b/src/pages/uxcore/[slug].tsx @@ -1,6 +1,6 @@ import { getStrapiBiases } from '@uxcore/api/biases'; +import { getStrapiQuestions } from '@uxcore/api/questions'; import { getTags } from '@uxcore/api/tags'; -import { GlobalContext } from '@uxcore/components/Context/GlobalContext'; import SeoGenerator from '@uxcore/components/SeoGenerator'; import UXCoreModal from '@uxcore/components/UXCoreModal'; import UXCoreModalMobile from '@uxcore/components/UXCoreModalMobile'; @@ -21,7 +21,7 @@ import type { import { TRouter } from '@uxcore/local-types/global'; import { GetStaticPaths, GetStaticProps } from 'next'; import { useRouter } from 'next/router'; -import { FC, useContext, useEffect, useMemo, useState } from 'react'; +import { FC, useEffect, useMemo, useState } from 'react'; import styles from './uxcoreId.module.scss'; @@ -31,6 +31,7 @@ interface UXCoreProps { currentActiveBias?: any; languageSwitchSlugs: Record; biases: Record; + uxcgLocalizedData: Record; } const UXCoreIds: FC = ({ @@ -39,9 +40,8 @@ const UXCoreIds: FC = ({ currentActiveBias, languageSwitchSlugs, biases, + uxcgLocalizedData, }) => { - const { uxcgLocalizedData } = useContext(GlobalContext); - const [strapiQuestions, setStrapiQuestions] = useState([]); const [activeBiasNumber, setActiveBiasNumber] = useState(null); const [isModalClosed, setIsModalClosed] = useState(true); const [{ toggleIsProductView }, { isProductView }] = useUXCoreGlobals(); @@ -54,6 +54,8 @@ const UXCoreIds: FC = ({ slugRu: `/uxcore/${currentModalData?.slugRu}`, }; + const strapiQuestions = uxcgLocalizedData?.[locale] ?? []; + const mentionedQuestions = strapiQuestions.filter(({ attributes }) => JSON.parse(currentModalData.mentionedQuestionsIds).includes( attributes?.number, @@ -136,14 +138,6 @@ const UXCoreIds: FC = ({ router.prefetch('/uxcore'); }, []); - useEffect(() => { - if (uxcgLocalizedData) { - setStrapiQuestions(uxcgLocalizedData[locale]); - } else { - setStrapiQuestions([]); - } - }, [locale, uxcgLocalizedData]); - return ( <> { const [number] = slug.split('-'); - const strapiBiases = await getStrapiBiases(); + const [strapiBiases, strapiQuestions] = await Promise.all([ + getStrapiBiases(), + getStrapiQuestions(), + ]); const biases = mergeBiasesLocalization( strapiBiases.en, strapiBiases.ru, @@ -272,6 +269,7 @@ export const getStaticProps: GetStaticProps = async ({ params, locale }) => { languageSwitchSlugs, currentActiveBias: currentActiveBiasWithLocale.attributes, biases: strapiBiases, + uxcgLocalizedData: strapiQuestions, }, revalidate: 5, }; diff --git a/src/styles/globals.scss b/src/styles/globals.scss index fd8d72b..5473916 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -10,6 +10,12 @@ html { html { overflow-y: overlay; overflow-x: hidden; + // Reserve the scrollbar gutter at all times. Modals lock body scroll by + // setting overflow-y: hidden on ; without a stable gutter, the + // scrollbar disappears and content jumps ~15px sideways on every modal + // open. Stable applies to auto / scroll / hidden per CSS spec, so this + // covers the locked state too. + scrollbar-gutter: stable; /* width */ &::-webkit-scrollbar { @@ -49,6 +55,9 @@ html.scroll-style-articles::-webkit-scrollbar-thumb { html.scroll-style-longevity::-webkit-scrollbar-thumb { background: #e2d0b1; } +// (Stable gutter is now unconditional on above; this rule kept as +// a no-op hook in case future modal logic needs to target the locked +// state from CSS.) html.hide-body-move { scrollbar-gutter: stable; } @@ -59,7 +68,13 @@ body { transition: 0.3s; &.darkTheme { - background-color: #1b1e26; + // The light-theme rule above sets `background: linear-gradient(...) + // #f9fafb` (shorthand: image + color). Setting only background-color + // here leaves the LIGHT gradient image rendering on top, so dark mode + // ended up showing a light page through everywhere there was no + // explicit dark surface. Use the full shorthand to clear the image + // and recolor. + background: #1b1e26; color: #dadada; &::-webkit-scrollbar { @@ -376,6 +391,95 @@ body { font-family: 'Source-Serif-Regular', sans-serif; } +// UX Core @font-face declarations (consolidated here from +// src/uxcore/styles/uxcore-fonts.scss per review). The scoped +// `body.uxcorePage *` rule below restores prod behaviour where every +// element on a UX Core route falls through to Lato when no component- +// level font-family is set — it must stay AFTER the global `* { font-family }` +// rule above so specificity wins. +@font-face { + font-family: 'Lato'; + src: url('/fonts/Lato/Lato-Regular.woff2') format('woff2'); + font-weight: normal; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Lato'; + src: url('/fonts/Lato/Lato-Semibold.woff2') format('woff2'); + font-weight: bold; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'IBM Plex Mono'; + src: url('/fonts/biases/IBMPlexMono-Regular.ttf') format('truetype'); + font-weight: normal; + font-display: swap; +} + +@font-face { + font-family: 'Oswald'; + src: url('/fonts/biases/Oswald-Bold') format('truetype'); + font-weight: normal; + font-display: swap; +} + +@font-face { + font-family: 'RedHatDisplay'; + src: url('/fonts/biases/RedHatDisplay.ttf'); + font-display: swap; +} + +@font-face { + font-family: 'DelaGothicOne-Regular'; + src: url('/fonts/DelaGothicOne-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Manrope-ExtraLight.ttf'; + src: url('/fonts/Manrope-ExtraLight.ttf') format('truetype'); + font-weight: 200; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'IBMPlexSans-Regular.ttf'; + src: url('/fonts/IBMPlexSans-Regular.ttf') format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'NotoSansArmenian-Regular'; + src: url('/fonts/NotoSansArmenian/NotoSansArmenian-Regular.ttf') + format('truetype'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'Cormorant-Garamond-Medium'; + src: url('/fonts/Cormorant_Garamond/static/CormorantGaramond-Medium.ttf') + format('truetype'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +body.uxcorePage, +body.uxcorePage * { + font-family: 'Lato', 'NotoSansArmenian-Regular', Arial, serif; +} + @media (max-width: 961px) { body { background-size: contain; diff --git a/src/uxcore/assets/icons/XIcon.tsx b/src/uxcore/assets/icons/XIcon.tsx index b9c61b8..8f96987 100644 --- a/src/uxcore/assets/icons/XIcon.tsx +++ b/src/uxcore/assets/icons/XIcon.tsx @@ -9,7 +9,7 @@ const XIcon = () => { > ); diff --git a/src/uxcore/components/Accordion/Accordion.module.scss b/src/uxcore/components/Accordion/Accordion.module.scss index b8df302..b398e5e 100644 --- a/src/uxcore/components/Accordion/Accordion.module.scss +++ b/src/uxcore/components/Accordion/Accordion.module.scss @@ -88,6 +88,34 @@ } } +// Dark theme: the legacy `&.darkTheme` modifier only kicks in when the +// component sets a `darkTheme` class on itself; on /uxcat it doesn't, +// so the Rules accordion stayed white-on-light. Auto-apply via body +// class so every accordion picks up dark without per-component plumbing. +:global(body.darkTheme) .Accordion { + & .Title { + color: #dadada; + background-color: #1b1e26; + border-color: #303338; + + // The collapse/expand chevron is /assets/icons/caret.svg — a dark + // SVG that's invisible against the dark title bar. Invert to white. + & > img { + filter: invert(0.9); + } + + & .DownloadButton { + color: #7fb3d5; + } + } + + & .Content { + background-color: #1b1e26; + border-color: #303338; + color: #dadada; + } +} + @media (max-width: 800px) { .Accordion { & .Title { diff --git a/src/uxcore/components/Achievement/Achievement.module.scss b/src/uxcore/components/Achievement/Achievement.module.scss index 43db40b..04bc9a8 100644 --- a/src/uxcore/components/Achievement/Achievement.module.scss +++ b/src/uxcore/components/Achievement/Achievement.module.scss @@ -62,7 +62,8 @@ .tooltipContainer { z-index: 99999; - background: linear-gradient( + background: + linear-gradient( 94deg, rgba(231, 233, 255, 0.61) 23.62%, rgba(210, 215, 255, 0.61) 99.75% @@ -161,3 +162,23 @@ opacity: 0.2; } } + +// Dark theme. react-tooltip portals the popup to , so it does NOT +// sit inside .achievement — target the class directly without parent +// scope or the descendant rule never matches. +:global(body.darkTheme) .tooltipContainer { + background: + linear-gradient( + 94deg, + rgba(35, 42, 58, 0.85) 23.62%, + rgba(45, 50, 75, 0.85) 99.75% + ), + #1b1e26; + border-color: #303338; + color: #dadada; + + > div:nth-child(2) { + border-bottom-color: #303338; + border-right-color: #303338; + } +} diff --git a/src/uxcore/components/AchievementContainer/AchievementContainer.module.scss b/src/uxcore/components/AchievementContainer/AchievementContainer.module.scss index 95592da..625cfe3 100644 --- a/src/uxcore/components/AchievementContainer/AchievementContainer.module.scss +++ b/src/uxcore/components/AchievementContainer/AchievementContainer.module.scss @@ -78,3 +78,16 @@ } } } + +// Dark theme: achievements grid container. +:global(body.darkTheme) { + .mainTitle { + color: #dadada; + } + + .container { + background-color: #1b1e26; + border-color: #303338; + color: #dadada; + } +} diff --git a/src/uxcore/components/AnswerBiasLink/BiasPopupContent/BiasPopupContent.module.scss b/src/uxcore/components/AnswerBiasLink/BiasPopupContent/BiasPopupContent.module.scss index 970ce6d..85965fb 100644 --- a/src/uxcore/components/AnswerBiasLink/BiasPopupContent/BiasPopupContent.module.scss +++ b/src/uxcore/components/AnswerBiasLink/BiasPopupContent/BiasPopupContent.module.scss @@ -38,6 +38,20 @@ $mobile-border-radius: 4px; } } + // Dark theme: lift the link colour to keep contrast against the dark + // popup surface and brighten the muted tip line. + :global(body.darkTheme) & { + & .LinkSection { + & a { + color: #7fb3d5; + } + + & .Tip { + color: rgba(218, 218, 218, 0.65); + } + } + } + &.Mobile { padding: 0 10px; width: 100vw; diff --git a/src/uxcore/components/Button/Button.module.scss b/src/uxcore/components/Button/Button.module.scss index 893752a..b4560ff 100644 --- a/src/uxcore/components/Button/Button.module.scss +++ b/src/uxcore/components/Button/Button.module.scss @@ -151,3 +151,25 @@ } } } + +// Dark theme: re-skin only the default neutral button (white bg / black +// text). Primary / Orange / OrangeOutline / BlueOutline variants keep +// their own brand colors. +:global(body.darkTheme) + .Button:not(.Primary):not(.Orange):not(.OrangeOutline):not(.BlueOutline) { + background-color: #151a26; + border-color: #303338; + color: #dadada; + + &:not(.Disabled):hover { + border-color: #7fb3d5; + color: #7fb3d5; + } +} + +// Dark theme: outline variants — keep the brand border + text but drop +// the white surface fill so the button reads on dark. +:global(body.darkTheme) .OrangeOutline, +:global(body.darkTheme) .BlueOutline { + background: transparent; +} diff --git a/src/uxcore/components/CompletionBar/CompletionBar.module.scss b/src/uxcore/components/CompletionBar/CompletionBar.module.scss index 122875c..5d33608 100644 --- a/src/uxcore/components/CompletionBar/CompletionBar.module.scss +++ b/src/uxcore/components/CompletionBar/CompletionBar.module.scss @@ -161,7 +161,8 @@ .tooltipContainer { z-index: 99999; - background: linear-gradient( + background: + linear-gradient( 94deg, rgba(231, 233, 255, 0.61) 23.62%, rgba(210, 215, 255, 0.61) 99.75% @@ -216,12 +217,8 @@ .completedTxt, .hoveredTxtOnGuestMode { color: #ff9900; - background: linear-gradient( - 180deg, - #ff9900 0%, - #ffbf00 83.33%, - #ffc700 100% - ), + background: + linear-gradient(180deg, #ff9900 0%, #ffbf00 83.33%, #ffc700 100%), linear-gradient(0deg, #ffa001, #ffa001); -webkit-text-fill-color: transparent; -webkit-background-clip: text; @@ -524,3 +521,52 @@ opacity: 0; } } + +// Dark theme: completion bar surface + dim labels + base track. +:global(body.darkTheme) { + .mainTitle { + color: #dadada; + } + + .completionBar { + background-color: #1b1e26; + } + + .withBorders { + border-color: #303338; + } + + .progressBar .levelTxt { + color: rgba(218, 218, 218, 0.55); + } + + .step { + color: rgba(218, 218, 218, 0.5); + } + + .bar, + .removeHover { + background-color: #303338; + } +} + +// react-tooltip portals the popup to , so it sits OUTSIDE +// .progressBarWrapper — the previous descendant-scoped rule never +// matched. Target the class globally so the level-hover tooltip on the +// CompletionBar gets a dark surface like the rest of the page. +:global(body.darkTheme) .tooltipContainer { + background: + linear-gradient( + 94deg, + rgba(35, 42, 58, 0.85) 23.62%, + rgba(45, 50, 75, 0.85) 99.75% + ), + #1b1e26; + border-color: #303338; + color: #dadada; + + > div:nth-child(2) { + border-bottom-color: #303338; + border-right-color: #303338; + } +} diff --git a/src/uxcore/components/CustomModal/CustomModal.module.scss b/src/uxcore/components/CustomModal/CustomModal.module.scss index 55a28d0..c531f52 100644 --- a/src/uxcore/components/CustomModal/CustomModal.module.scss +++ b/src/uxcore/components/CustomModal/CustomModal.module.scss @@ -11,6 +11,11 @@ align-items: center; justify-content: center; z-index: 106; + animation: cmOverlayFadeIn 0.2s ease-out; + + &.ModalOverlayClosing { + animation: cmOverlayFadeOut 0.18s ease-in forwards; + } & .Modal { position: relative; @@ -19,6 +24,11 @@ border: 1px solid #cbcbcb; box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.15); border-radius: 4px; + animation: cmWrapperIn 0.2s ease-out; + + &.ModalClosing { + animation: cmWrapperOut 0.18s ease-in forwards; + } & .ModalHeader { display: flex; @@ -50,6 +60,8 @@ max-height: calc(100vh - 315px); overflow: auto; box-sizing: border-box; + scrollbar-width: thin; + scrollbar-color: rgba(40, 88, 123, 0.4) transparent; /* width */ &::-webkit-scrollbar { @@ -63,7 +75,7 @@ /* Handle */ &::-webkit-scrollbar-thumb { - background: rgba(40, 88, 123, 0.5); + background: rgba(40, 88, 123, 0.4); border-radius: 5px; } } @@ -92,3 +104,72 @@ } } } + +@keyframes cmOverlayFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes cmOverlayFadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +@keyframes cmWrapperIn { + from { + opacity: 0; + transform: scale(0.97) translateY(8px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} +@keyframes cmWrapperOut { + from { + opacity: 1; + transform: scale(1) translateY(0); + } + to { + opacity: 0; + transform: scale(0.97) translateY(8px); + } +} + +// Dark theme: re-skin the CustomModal chrome (used by Contact Us / +// Add Question). Modal is portaled under , so the descendant +// selector from body.darkTheme still matches. +:global(body.darkTheme) .ModalOverlay { + background-color: rgba(0, 0, 0, 0.55); + + .Modal { + background: #1b1e26; + border-color: #303338; + + .ModalHeader { + border-bottom-color: #303338; + + .ModalHeaderBody { + color: #7fb3d5; + } + + .ModalHeaderCloseButton img { + filter: invert(0.85); + } + } + + .ModalBody { + scrollbar-color: rgba(127, 179, 213, 0.4) transparent; + + &::-webkit-scrollbar-thumb { + background: rgba(127, 179, 213, 0.4); + } + } + } +} diff --git a/src/uxcore/components/CustomModal/CustomModal.tsx b/src/uxcore/components/CustomModal/CustomModal.tsx index b1efe02..dbb94b9 100644 --- a/src/uxcore/components/CustomModal/CustomModal.tsx +++ b/src/uxcore/components/CustomModal/CustomModal.tsx @@ -1,13 +1,11 @@ +import customModalData from '@uxcore/data/customModal'; +import type { TagType } from '@uxcore/local-types/data'; +import type { TRouter } from '@uxcore/local-types/global'; import cn from 'classnames'; import { useRouter } from 'next/router'; -import { FC, KeyboardEvent, useEffect } from 'react'; +import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import type { TagType } from '@uxcore/local-types/data'; -import type { TRouter } from '@uxcore/local-types/global'; - -import customModalData from '@uxcore/data/customModal'; - import { AddQuestion, ContactUs } from './contentTypes'; import styles from './CustomModal.module.scss'; @@ -29,10 +27,27 @@ const CustomModal: FC = ({ const router = useRouter(); const { locale } = router as TRouter; const { titles } = customModalData[locale]; + const [closing, setClosing] = useState(false); + + // Trigger fade-out CSS animation, then unmount on next tick. + const handleClose = () => { + if (closing) return; + setClosing(true); + setTimeout(() => { + setClosing(false); + onClose(); + }, 180); + }; + + // Mount-time Escape listener reads handleClose through a ref so the + // `closing` guard inside handleClose sees the latest value on a second + // press during the 180ms fade-out. + const handleCloseRef = useRef(handleClose); + handleCloseRef.current = handleClose; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose(); + if (e.key === 'Escape') handleCloseRef.current(); }; // @ts-ignore @@ -66,14 +81,17 @@ const CustomModal: FC = ({ <> {createPortal(
e.stopPropagation()} > @@ -85,7 +103,7 @@ const CustomModal: FC = ({
modal close button
@@ -93,10 +111,10 @@ const CustomModal: FC = ({
{contentType === 'addQuestion' && ( - + )} {contentType === 'contactUs' && ( - + )}
diff --git a/src/uxcore/components/CustomModal/contentTypes/ContactUs/ContactUs.module.scss b/src/uxcore/components/CustomModal/contentTypes/ContactUs/ContactUs.module.scss index 1e17445..5bd01e8 100644 --- a/src/uxcore/components/CustomModal/contentTypes/ContactUs/ContactUs.module.scss +++ b/src/uxcore/components/CustomModal/contentTypes/ContactUs/ContactUs.module.scss @@ -51,3 +51,12 @@ } } } + +// Dark theme: flip the "You can also write Wolf..." body copy. +:global(body.darkTheme) .ContactUs .Footer .Content { + color: #dadada; + + & a { + color: #7fb3d5; + } +} diff --git a/src/uxcore/components/Input/Input.module.scss b/src/uxcore/components/Input/Input.module.scss index 5e37525..87e6d0c 100644 --- a/src/uxcore/components/Input/Input.module.scss +++ b/src/uxcore/components/Input/Input.module.scss @@ -129,3 +129,34 @@ font-size: 16px; } } + +// Dark theme: re-skin the input chrome (bg, border, text, placeholder) +// and invert the dark "clear" (×) icon so it reads against the dark +// input bg. +:global(body.darkTheme) .Input { + color: #dadada; + + .Label { + color: #dadada; + } + + .InputWrapper { + & input { + background-color: #151a26; + border-color: #303338; + color: #dadada; + + &::placeholder { + color: rgba(218, 218, 218, 0.45); + } + + &:focus { + border-color: #7fb3d5; + } + } + + .Icon { + filter: invert(0.85) brightness(1.5); + } + } +} diff --git a/src/uxcore/components/LanguageSwitcher/LanguageSwitcher.module.scss b/src/uxcore/components/LanguageSwitcher/LanguageSwitcher.module.scss index a7d657b..335075f 100644 --- a/src/uxcore/components/LanguageSwitcher/LanguageSwitcher.module.scss +++ b/src/uxcore/components/LanguageSwitcher/LanguageSwitcher.module.scss @@ -76,3 +76,32 @@ } } } + +// Dark theme: the dropdown sits on its own #fff panel by default, which +// looks like a light-mode leak inside the dark bias modal. Match the modal +// palette and keep the flag SVGs in full colour (some parent modal-header +// rules apply a saturate(0)/invert filter to images — explicitly null it +// out on the flag icons so the colour stripes survive). +:global(body.darkTheme) .languageSwitcher { + &:hover .dropdown { + background: #1b1e26; + border-color: #303338 !important; + + .link { + color: #dadada; + + &:hover { + background: rgba(255, 255, 255, 0.06); + } + } + } + + .switcher { + color: #dadada; + } + + .flag, + .switcher img { + filter: none !important; + } +} diff --git a/src/uxcore/components/Modal/Modal.module.scss b/src/uxcore/components/Modal/Modal.module.scss index ceda65d..4fa08fb 100644 --- a/src/uxcore/components/Modal/Modal.module.scss +++ b/src/uxcore/components/Modal/Modal.module.scss @@ -11,6 +11,11 @@ align-items: center; justify-content: center; z-index: 45; + animation: overlayFadeIn 0.2s ease-out; + + &.overlayClosing { + animation: overlayFadeOut 0.18s ease-in forwards; + } .wrapper { display: flex; @@ -20,6 +25,11 @@ position: relative; z-index: 80; border-radius: 4px; + animation: wrapperIn 0.2s ease-out; + + &.wrapperClosing { + animation: wrapperOut 0.18s ease-in forwards; + } .header { .hadBorder { @@ -65,6 +75,22 @@ .body { padding: 16px 28px; overflow: auto; + + // Slim styled scrollbar so the inside of the modal does not look + // like a Windows-95 widget when the content overflows. + scrollbar-width: thin; + scrollbar-color: rgba(40, 88, 123, 0.4) transparent; + + &::-webkit-scrollbar { + width: 6px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: rgba(40, 88, 123, 0.4); + border-radius: 5px; + } } } @@ -188,3 +214,85 @@ } } } + +@keyframes overlayFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} +@keyframes overlayFadeOut { + from { + opacity: 1; + } + to { + opacity: 0; + } +} +@keyframes wrapperIn { + from { + opacity: 0; + transform: scale(0.97) translateY(8px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} +@keyframes wrapperOut { + from { + opacity: 1; + transform: scale(1) translateY(0); + } + to { + opacity: 0; + transform: scale(0.97) translateY(8px); + } +} + +// Dark theme: re-skin the base modal chrome (wrapper bg, header divider, +// title, close button). Modal renders through a portal under , so a +// descendant selector from body.darkTheme still matches. +:global(body.darkTheme) .overlay { + background-color: rgba(0, 0, 0, 0.55); + + .wrapper { + background-color: #1b1e26 !important; + border: 1px solid #303338; + + .body { + scrollbar-color: rgba(127, 179, 213, 0.4) transparent; + + &::-webkit-scrollbar-thumb { + background: rgba(127, 179, 213, 0.4); + } + } + } + + .header { + .title { + color: #7fb3d5 !important; + } + + .blackTitle { + color: #dadada !important; + } + + .grayTitle { + color: rgba(218, 218, 218, 0.55) !important; + } + + .closeBtn, + .closeBtnWithoutHeader { + // The close icon is a dark SVG — invert it so it reads against the + // dark modal chrome. + filter: invert(0.85); + } + } + + .hasBorder { + border-bottom-color: #303338 !important; + } +} diff --git a/src/uxcore/components/Modal/Modal.tsx b/src/uxcore/components/Modal/Modal.tsx index 10b63eb..577cc73 100644 --- a/src/uxcore/components/Modal/Modal.tsx +++ b/src/uxcore/components/Modal/Modal.tsx @@ -1,5 +1,12 @@ import cn from 'classnames'; -import React, { FC, KeyboardEvent, ReactNode, useEffect } from 'react'; +import React, { + FC, + KeyboardEvent, + ReactNode, + useEffect, + useRef, + useState, +} from 'react'; import { createPortal } from 'react-dom'; import styles from './Modal.module.scss'; @@ -49,10 +56,25 @@ const Modal: FC = ({ dataCy, fullHeightMobile, }) => { + const [closing, setClosing] = useState(false); + + // Trigger the fade-out CSS animation, then unmount on the next tick so + // the user sees the modal leave instead of pop out. const handleClose = () => { - onClick(); + if (closing) return; + setClosing(true); + setTimeout(() => { + setClosing(false); + onClick(); + }, 180); }; + // Escape listener reads handleClose through a ref so the `closing` + // guard sees the latest value on a second press during the 180ms + // fade-out window. + const handleCloseRef = useRef(handleClose); + handleCloseRef.current = handleClose; + useEffect(() => { if (!isConfirmationModal) { // @ts-ignore @@ -61,7 +83,7 @@ const Modal: FC = ({ const handleKeyDown = (e: KeyboardEvent) => { if (!close) { - if (e.key === 'Escape') handleClose(); + if (e.key === 'Escape') handleCloseRef.current(); } }; @@ -89,6 +111,7 @@ const Modal: FC = ({
@@ -106,6 +129,7 @@ const Modal: FC = ({ [wrapperClassName]: wrapperClassName, [styles.fullSizeMobile]: fullSizeMobile, [styles.fullHeightMobile]: fullHeightMobile, + [styles.wrapperClosing]: closing, })} > {!removeHeader && ( diff --git a/src/uxcore/components/OurProjectsModal/OurProjectsModal.module.scss b/src/uxcore/components/OurProjectsModal/OurProjectsModal.module.scss index ebd9893..b372c3b 100644 --- a/src/uxcore/components/OurProjectsModal/OurProjectsModal.module.scss +++ b/src/uxcore/components/OurProjectsModal/OurProjectsModal.module.scss @@ -90,3 +90,30 @@ background: #fff !important; color: #0b0921 !important; } + +// Dark theme: project list rows, description text, divider, and the +// GitHub / API outline buttons all need to flip from a light-on-white +// palette to a light-on-dark one. +:global(body.darkTheme) { + .projectInfo { + .name { + color: #dadada !important; + } + } + + .description, + .description :global(p) { + color: rgba(218, 218, 218, 0.75) !important; + } + + .buttonStyleLink { + border-color: #303338; + color: #dadada; + background-color: transparent; + + &:hover { + background-color: #dadada; + color: #1b1e26; + } + } +} diff --git a/src/uxcore/components/Result/Result.module.scss b/src/uxcore/components/Result/Result.module.scss index 6d050e3..65d8277 100644 --- a/src/uxcore/components/Result/Result.module.scss +++ b/src/uxcore/components/Result/Result.module.scss @@ -247,3 +247,42 @@ opacity: 0.2; } } + +// Dark theme: result cards + question pills. +:global(body.darkTheme) .result { + .failedResult, + .passedResult { + background: #1b1e26; + border-color: #303338; + color: #dadada; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); + + .resultList { + .list { + .question { + background-color: rgba(127, 179, 213, 0.06); + border-color: #303338; + color: #dadada; + + &:hover { + background: rgba(127, 179, 213, 0.18); + border-color: #7fb3d5; + } + } + + .imgWrapper { + background-color: #303338; + } + } + + .failed > div { + background-color: rgba(255, 57, 57, 0.08); + border-color: rgba(255, 57, 57, 0.35); + } + } + } + + .passedResult .resultList .list .imgWrapper .bobIcon { + background-color: rgba(127, 179, 213, 0.08); + } +} diff --git a/src/uxcore/components/Section/Section.module.scss b/src/uxcore/components/Section/Section.module.scss index 9946762..eafe6cf 100644 --- a/src/uxcore/components/Section/Section.module.scss +++ b/src/uxcore/components/Section/Section.module.scss @@ -3,3 +3,8 @@ border: 1px solid #d9d9d9; border-radius: 4px; } + +:global(body.darkTheme) .Section { + background-color: #1b1e26; + border-color: #303338; +} diff --git a/src/uxcore/components/Table/Table.module.scss b/src/uxcore/components/Table/Table.module.scss index dfd5555..5fb57a9 100644 --- a/src/uxcore/components/Table/Table.module.scss +++ b/src/uxcore/components/Table/Table.module.scss @@ -439,6 +439,14 @@ .SelectionTxt { color: rgba(218, 218, 218, 0.7) !important; } + + // "All Questions" button uses inline backgroundColor: #282828, which is + // basically the dark page bg. Override with a lighter neutral so it reads + // as a real button. + .AllQuestionsButton { + background-color: #2f3441 !important; + color: #dadada !important; + } } .TableRow { @@ -505,10 +513,12 @@ .showLessBtn { background-color: #151a26 !important; color: #dadada !important; + border-color: #303338 !important; &:hover { background-color: #1b1e26 !important; color: #ffffff !important; + border-color: #303338 !important; } } } diff --git a/src/uxcore/components/Tag/Tag.module.scss b/src/uxcore/components/Tag/Tag.module.scss index 40842f9..20f780c 100644 --- a/src/uxcore/components/Tag/Tag.module.scss +++ b/src/uxcore/components/Tag/Tag.module.scss @@ -23,3 +23,22 @@ } } } + +// Dark theme: inactive stage filter pills are nearly invisible at 0.3 opacity +// against the dark page bg. Raise to 0.75 and give them a faint border so they +// register as real buttons. Active pill stays fully opaque. +:global(body.darkTheme) .Tag { + &Button { + opacity: 0.75; + border: 1px solid rgba(218, 218, 218, 0.2); + + &:hover { + opacity: 1; + } + + &.TagButtonActive { + opacity: 1; + border-color: transparent; + } + } +} diff --git a/src/uxcore/components/TestResultsAchievements/TestResultsAchievements.module.scss b/src/uxcore/components/TestResultsAchievements/TestResultsAchievements.module.scss index 78cd1e4..30e4a2c 100644 --- a/src/uxcore/components/TestResultsAchievements/TestResultsAchievements.module.scss +++ b/src/uxcore/components/TestResultsAchievements/TestResultsAchievements.module.scss @@ -42,3 +42,15 @@ } } } + +// Dark theme. +:global(body.darkTheme) .achievements { + background: #1b1e26; + border-color: #303338; + color: #dadada; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); + + .achievementTitle { + color: #dadada; + } +} diff --git a/src/uxcore/components/Textarea/Textarea.module.scss b/src/uxcore/components/Textarea/Textarea.module.scss index 1e43285..8fd7aff 100644 --- a/src/uxcore/components/Textarea/Textarea.module.scss +++ b/src/uxcore/components/Textarea/Textarea.module.scss @@ -82,3 +82,26 @@ } } } + +// Dark theme: re-skin the textarea chrome (bg, border, text, placeholder). +:global(body.darkTheme) .Textarea { + color: #dadada; + + .Label { + color: #dadada; + } + + .TextareaWrapper textarea { + background-color: #151a26; + border-color: #303338; + color: #dadada; + + &::placeholder { + color: rgba(218, 218, 218, 0.45); + } + + &:focus { + border-color: #7fb3d5; + } + } +} diff --git a/src/uxcore/components/ToolHeader/ToolHeader.module.scss b/src/uxcore/components/ToolHeader/ToolHeader.module.scss index f096778..18c1f8a 100644 --- a/src/uxcore/components/ToolHeader/ToolHeader.module.scss +++ b/src/uxcore/components/ToolHeader/ToolHeader.module.scss @@ -527,6 +527,24 @@ $headerHeight: 46px; filter: invert(0.95) brightness(1.6); } } + + // Selected nav items already have their SVG path fill set to a light + // colour (#fafafa) by the base rule. Pushing them through the + // invert/brightness filter intended for the dark monochrome icons + // turns them dark grey on the dark navbar — invisible on hover when + // the hover background washes them further. Null the filter on + // selected variants so the white fill wins. + &.Active, + &.ActivePodcast, + &.ActiveProjects { + & svg { + filter: none; + } + + &:hover svg { + filter: none; + } + } } .openLink { diff --git a/src/uxcore/components/ToolHeader/ToolHeader.tsx b/src/uxcore/components/ToolHeader/ToolHeader.tsx index 90bdc1b..21de655 100644 --- a/src/uxcore/components/ToolHeader/ToolHeader.tsx +++ b/src/uxcore/components/ToolHeader/ToolHeader.tsx @@ -1,36 +1,9 @@ -import cn from 'classnames'; -import dynamic from 'next/dynamic'; -import Image from 'next/image'; -import { useRouter } from 'next/router'; -import React, { - FC, - useCallback, - useContext, - useEffect, - useMemo, - useRef, - useState, -} from 'react'; - -import type { TRouter } from '@uxcore/local-types/global'; -import { UserTypes } from '@uxcore/local-types/uxcat-types/types'; - -import useGlobals from '@uxcore/hooks/useGlobals'; -import useMobile from '@uxcore/hooks/useMobile'; -import useUXCoreGlobals from '@uxcore/hooks/useUXCoreGlobals'; - -import { isLevelMilestone } from '@uxcore/lib/uxcat-helpers'; - import { getMyInfo } from '@uxcore/api/strapi'; import { userInfoUpdate } from '@uxcore/api/uxcat/settings'; import { getUserInfo } from '@uxcore/api/uxcat/users-me'; - -import toolHeaderData from '@uxcore/data/toolHeader'; - import CloseIcon from '@uxcore/assets/icons/CloseIcon'; import DiamondIcon from '@uxcore/assets/icons/DiamondIcon'; import PodcastIcon from '@uxcore/assets/icons/PodcastIcon'; - import MobileHeader from '@uxcore/components/_biases/MobileHeader'; import { GlobalContext } from '@uxcore/components/Context/GlobalContext'; import LanguageSwitcher from '@uxcore/components/LanguageSwitcher'; @@ -38,14 +11,37 @@ import Link from '@uxcore/components/NextLink'; import OurProjectsModal from '@uxcore/components/OurProjectsModal'; import PageSwitcher from '@uxcore/components/PageSwitcher'; import UserDropdown from '@uxcore/components/UserDropdown'; +import toolHeaderData from '@uxcore/data/toolHeader'; +import useGlobals from '@uxcore/hooks/useGlobals'; +import useMobile from '@uxcore/hooks/useMobile'; +import useUXCoreGlobals from '@uxcore/hooks/useUXCoreGlobals'; +import { isLevelMilestone } from '@uxcore/lib/uxcat-helpers'; +import type { TRouter } from '@uxcore/local-types/global'; +import { UserTypes } from '@uxcore/local-types/uxcat-types/types'; +import cn from 'classnames'; +import dynamic from 'next/dynamic'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import React, { + FC, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; import { navItems } from './navItems'; import styles from './ToolHeader.module.scss'; -const SettingsModal = dynamic(() => import('@uxcore/components/SettingsModal'), { - ssr: false, -}); +const SettingsModal = dynamic( + () => import('@uxcore/components/SettingsModal'), + { + ssr: false, + }, +); type TToolHeader = { homepageLinkTarget?: '_blank' | '_self'; @@ -91,7 +87,11 @@ const ToolHeader: FC = ({ hidden, }) => { const router = useRouter(); - const { locale, asPath } = router as TRouter; + const { locale } = router as TRouter; + // Use router.pathname (route template, hash-free) not router.asPath: + // asPath contains the URL hash on the client but not on SSR, which + // breaks hydration on routes like /uxcore#hr. + const pathname = router.pathname; const { isMobile } = useMobile()[1]; const [{ toggleIsDarkTheme }, { isDarkTheme }] = useGlobals(); @@ -383,7 +383,7 @@ const ToolHeader: FC = ({ })}
- {showUxcgTooltip && asPath === '/uxcore' && ( + {showUxcgTooltip && pathname === '/uxcore' && (
= ({
)} - {showUxcoreTooltip && asPath === '/uxcg' && ( + {showUxcoreTooltip && pathname === '/uxcg' && (
= ({ [styles.authorized]: !!accountData, })} > - {isCoreView && asPath === '/uxcore' && locale !== 'hy' && ( + {isCoreView && pathname === '/uxcore' && locale !== 'hy' && (
= ({ type="button" className={styles.themeToggle} onClick={toggleIsDarkTheme} - aria-label={isDarkTheme ? 'Switch to light theme' : 'Switch to dark theme'} + aria-label={ + isDarkTheme ? 'Switch to light theme' : 'Switch to dark theme' + } aria-pressed={isDarkTheme} data-cy="theme-toggle" > diff --git a/src/uxcore/components/Tooltip/Tooltip.module.scss b/src/uxcore/components/Tooltip/Tooltip.module.scss index 5c301ed..516ec00 100644 --- a/src/uxcore/components/Tooltip/Tooltip.module.scss +++ b/src/uxcore/components/Tooltip/Tooltip.module.scss @@ -73,3 +73,24 @@ max-width: 380px; } } + +// Dark theme: the white tooltip popup (used by bias-link hover in the UXCG +// modal, among others) leaks the light-mode panel onto the dark modal. +// Match the dark surface palette and recolour the arrow. +:global(body.darkTheme) .Popup:not(.Dark) { + background: #1b1e26; + border-color: #303338; + color: #dadada; + box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.5); + + &::after { + background-color: #1b1e26; + border-right-color: #303338; + border-bottom-color: #303338; + } + + &.Bottom::after { + border-left-color: #303338; + border-top-color: #303338; + } +} diff --git a/src/uxcore/components/UXCatFooter/UXCatFooter.module.scss b/src/uxcore/components/UXCatFooter/UXCatFooter.module.scss index 40e1871..8411503 100644 --- a/src/uxcore/components/UXCatFooter/UXCatFooter.module.scss +++ b/src/uxcore/components/UXCatFooter/UXCatFooter.module.scss @@ -63,3 +63,19 @@ } } } + +// Dark theme: contact-us footer copy. +:global(body.darkTheme) .footerContainer { + .contactUsTxt { + color: #dadada; + } + + .contactInfo .mail, + .contactInfo .telegram { + color: #dadada; + } + + .motto { + color: #7fb3d5; + } +} diff --git a/src/uxcore/components/UXCatPageTitle/UXCatPageTitle.module.scss b/src/uxcore/components/UXCatPageTitle/UXCatPageTitle.module.scss index 0dc20a2..9e12c76 100644 --- a/src/uxcore/components/UXCatPageTitle/UXCatPageTitle.module.scss +++ b/src/uxcore/components/UXCatPageTitle/UXCatPageTitle.module.scss @@ -16,3 +16,10 @@ margin-top: 40px; } } + +// Dark theme: this is the section heading shared by "Level Progression", +// "Achievements", etc. on /uxcat — was rgba(0,0,0,0.65), invisible on +// dark page bg. +:global(body.darkTheme) .pageTitle { + color: #dadada; +} diff --git a/src/uxcore/components/UXCatTooltip/UXCatTooltip.module.scss b/src/uxcore/components/UXCatTooltip/UXCatTooltip.module.scss index e882bf4..ae0d1d2 100644 --- a/src/uxcore/components/UXCatTooltip/UXCatTooltip.module.scss +++ b/src/uxcore/components/UXCatTooltip/UXCatTooltip.module.scss @@ -186,3 +186,27 @@ opacity: 0; } } + +// Dark theme: tooltip body copy. The wrapper .tooltipContainer got a +// dark surface in Achievement.module.scss / CompletionBar.module.scss, +// but the text rendered inside this component (title, description, +// unlocked/statistics labels, points-to-next-level) was still +// hard-coded dark on dark. +:global(body.darkTheme) .achievementInfo .infoContainer { + .txtWrapper .title { + color: #dadada; + } + + .pointsToNextLevel { + color: #9da3e1; + } + + .unlockedTxt, + .statisticsTxt { + color: rgba(218, 218, 218, 0.55); + } + + .description { + color: #dadada; + } +} diff --git a/src/uxcore/components/UserDropdown/UserDropdown.module.scss b/src/uxcore/components/UserDropdown/UserDropdown.module.scss index 5d5c119..27d2a8c 100644 --- a/src/uxcore/components/UserDropdown/UserDropdown.module.scss +++ b/src/uxcore/components/UserDropdown/UserDropdown.module.scss @@ -151,3 +151,26 @@ } } } + +// Dark theme: "Log In" / username text was near-invisible because the base +// color is rgba(0,0,0,0.85). Lift it to the dark-theme foreground. +:global(body.darkTheme) .userContainer { + .user, + .active { + &:hover { + background: rgba(255, 255, 255, 0.06); + } + + .userName { + color: #dadada; + } + } + + .active { + background: rgba(255, 255, 255, 0.06); + + .userName { + color: #ffffff; + } + } +} diff --git a/src/uxcore/components/UserProfile/UserProfile.module.scss b/src/uxcore/components/UserProfile/UserProfile.module.scss index 1e130a5..8779146 100644 --- a/src/uxcore/components/UserProfile/UserProfile.module.scss +++ b/src/uxcore/components/UserProfile/UserProfile.module.scss @@ -226,3 +226,52 @@ margin: 0 12px; } } + +// Dark theme: user-profile header colors. Cover image is set inline via +// JS so override with !important + dark fill + darken any image that's +// rendered behind to keep the panel readable. +:global(body.darkTheme) .userProfile { + background-image: none !important; + background-color: #1b1e26 !important; + border: 1px solid #303338; + border-radius: 4px; + + &.guestMode { + background-color: #15181f !important; + } + + .wrapper { + .badgeWrapper .badgeTooltip { + background: #1b1e26; + color: #dadada; + border-color: #303338; + + > div:nth-child(2) { + border-bottom-color: #303338; + border-right-color: #303338; + } + } + + .nameAndTitle { + .title { + color: #c79bc8; + } + + .userName { + color: #dadada; + } + } + + .level { + color: #dadada; + } + + .awarenessPoints { + color: #d49066; + } + } + + .btnWrapper .userStatistic { + color: #9da3e1; + } +} diff --git a/src/uxcore/components/_biases/MobileHeader/MobileHeader.tsx b/src/uxcore/components/_biases/MobileHeader/MobileHeader.tsx index 5019724..0f22363 100644 --- a/src/uxcore/components/_biases/MobileHeader/MobileHeader.tsx +++ b/src/uxcore/components/_biases/MobileHeader/MobileHeader.tsx @@ -1,25 +1,19 @@ -import cn from 'classnames'; -import Image from 'next/image'; -import { useRouter } from 'next/router'; -import { FC, useContext, useEffect, useMemo, useState } from 'react'; - -import type { TRouter } from '@uxcore/local-types/global'; -import { UserTypes } from '@uxcore/local-types/uxcat-types/types'; - -import { isLevelMilestone } from '@uxcore/lib/uxcat-helpers'; - import { getMyInfo } from '@uxcore/api/strapi'; import { userInfoUpdate } from '@uxcore/api/uxcat/settings'; import { getUserInfo } from '@uxcore/api/uxcat/users-me'; - -import toolHeaderData from '@uxcore/data/toolHeader'; - import PodcastIcon from '@uxcore/assets/icons/PodcastIcon'; - import { GlobalContext } from '@uxcore/components/Context/GlobalContext'; import LanguageSwitcher from '@uxcore/components/LanguageSwitcher'; import SettingsModal from '@uxcore/components/SettingsModal'; import UserDropdown from '@uxcore/components/UserDropdown'; +import toolHeaderData from '@uxcore/data/toolHeader'; +import { isLevelMilestone } from '@uxcore/lib/uxcat-helpers'; +import type { TRouter } from '@uxcore/local-types/global'; +import { UserTypes } from '@uxcore/local-types/uxcat-types/types'; +import cn from 'classnames'; +import Image from 'next/image'; +import { useRouter } from 'next/router'; +import { FC, useContext, useEffect, useMemo, useState } from 'react'; import styles from './MobileHeader.module.scss'; @@ -166,7 +160,7 @@ const MobileHeader: FC = ({
- {router.asPath === '/uxcore' && locale !== 'hy' && ( + {router.pathname === '/uxcore' && locale !== 'hy' && (
{ - const { type } = e.currentTarget.dataset; - if ((type === defaultViewLabel) !== isSecondView) { - toggleIsCoreView(); - } - }, + // First button is the "active" side when isSecondView=true (the + // .FolderView class is applied then), second button is active when + // isSecondView=false. Toggle only when the inactive side is clicked. + const handleFirstClick = useCallback(() => { + if (!isSecondView) toggleIsCoreView?.(); + }, [isSecondView, toggleIsCoreView]); - [isSecondView, toggleIsCoreView, defaultViewLabel], - ); + const handleSecondClick = useCallback(() => { + if (isSecondView) toggleIsCoreView?.(); + }, [isSecondView, toggleIsCoreView]); const switchATeamView = () => { - if (defaultViewLabel === 'Product' || secondViewLabel === 'hr') { + if ( + (defaultViewLabel === 'Product' || secondViewLabel === 'hr') && + handleSnackbarOpening + ) { handleSnackbarOpening(); } }; @@ -78,8 +79,8 @@ const ViewSwitcher = ({ data-cy={dataCy} className={styles.ViewSwitcherButton} data-type={defaultViewLabel} - onClick={e => { - handlePageViewChange(e); + onClick={() => { + handleFirstClick(); switchATeamView(); }} > @@ -92,8 +93,8 @@ const ViewSwitcher = ({ data-cy={dataCySecondView} className={styles.ViewSwitcherButton} data-type={secondViewLabel} - onClick={e => { - handlePageViewChange(e); + onClick={() => { + handleSecondClick(); switchATeamView(); }} > diff --git a/src/uxcore/components/_uxcp/BiasActionCell/BiasActionCell.module.scss b/src/uxcore/components/_uxcp/BiasActionCell/BiasActionCell.module.scss index c612cbf..87e61fd 100644 --- a/src/uxcore/components/_uxcp/BiasActionCell/BiasActionCell.module.scss +++ b/src/uxcore/components/_uxcp/BiasActionCell/BiasActionCell.module.scss @@ -90,3 +90,35 @@ font-size: 14px; } } + +// Dark theme: the bias rows in "Add biases to Persona" used a pale blue +// surface with a white "Add" button — both blew out against dark. Use a +// faint blue tint on the row, light text, and a dark "Add" button. +:global(body.darkTheme) .BiasActionCell { + background-color: rgba(127, 179, 213, 0.08); + border-bottom-color: #303338; + color: #dadada; + + .Title .Number { + color: #7fb3d5; + } + + .Actions .AddButton { + background: #151a26; + border-color: #303338; + color: #dadada; + + &:hover { + border-color: #7fb3d5; + color: #7fb3d5; + } + } + + .Actions .RemoveButton > img { + filter: invert(0.85); + } + + &.Selected { + background-color: rgba(127, 179, 213, 0.18); + } +} diff --git a/src/uxcore/components/_uxcp/BiasItem/BiasItem.module.scss b/src/uxcore/components/_uxcp/BiasItem/BiasItem.module.scss index 6250f21..6c9e38b 100644 --- a/src/uxcore/components/_uxcp/BiasItem/BiasItem.module.scss +++ b/src/uxcore/components/_uxcp/BiasItem/BiasItem.module.scss @@ -98,3 +98,19 @@ } } } + +// Dark theme: bias rows in the left/persona list. +:global(body.darkTheme) .BiasItem { + .BiasItemWrapper { + background-color: rgba(127, 179, 213, 0.08); + color: #dadada; + } + + .Title .Number { + color: #7fb3d5; + } + + .Action .RemoveButton > img { + filter: invert(0.85); + } +} diff --git a/src/uxcore/components/_uxcp/BiasSearch/BiasSearch.module.scss b/src/uxcore/components/_uxcp/BiasSearch/BiasSearch.module.scss index 4b8bf19..37436c7 100644 --- a/src/uxcore/components/_uxcp/BiasSearch/BiasSearch.module.scss +++ b/src/uxcore/components/_uxcp/BiasSearch/BiasSearch.module.scss @@ -79,3 +79,20 @@ font-size: 14px; } } + +// Dark theme: bias search container + show-all button. +:global(body.darkTheme) .BiasSearch { + .SearchContainer { + background: transparent; + } + + .Table { + &::-webkit-scrollbar-thumb { + background: rgba(127, 179, 213, 0.5); + } + } + + .ShowAllButtonContainer .ShowAllButton { + color: #7fb3d5; + } +} diff --git a/src/uxcore/components/_uxcp/CountryBiasMap/BiasPanel/BiasPanel.module.scss b/src/uxcore/components/_uxcp/CountryBiasMap/BiasPanel/BiasPanel.module.scss index 25e3d8a..6cad1ff 100644 --- a/src/uxcore/components/_uxcp/CountryBiasMap/BiasPanel/BiasPanel.module.scss +++ b/src/uxcore/components/_uxcp/CountryBiasMap/BiasPanel/BiasPanel.module.scss @@ -222,6 +222,21 @@ max-height: calc(1.6em * 8); overflow-y: auto; + // Slim styled scrollbar inside each bias chip (default OS scrollbar + // looks heavy inside the small chip surface). + scrollbar-width: thin; + scrollbar-color: rgba(40, 88, 123, 0.35) transparent; + &::-webkit-scrollbar { + width: 5px; + } + &::-webkit-scrollbar-track { + background: transparent; + } + &::-webkit-scrollbar-thumb { + background: rgba(40, 88, 123, 0.35); + border-radius: 4px; + } + p { margin: 0; @@ -270,3 +285,92 @@ box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); } } + +// Dark theme: country-detail card + bias chips + help popover. +:global(body.darkTheme) { + .EmptyText { + color: rgba(218, 218, 218, 0.55); + } + + .EmptyHint { + color: #7fb3d5; + } + + .Card { + background: #1b1e26; + border-color: #303338; + } + + .Header, + .Footer { + border-color: #303338; + } + + .Title { + color: #dadada; + } + + .Rationale { + color: #c8c8c8; + } + + .HelpPopover { + background: #1b1e26; + border-color: #303338; + } + + .HelpLabel { + color: #7fb3d5; + } + + .HelpText { + color: rgba(218, 218, 218, 0.75); + } + + .HelpFootnote { + border-color: #303338; + + p { + color: rgba(218, 218, 218, 0.45); + } + } + + .BiasChip { + background: #151a26; + border-color: #303338; + + &:hover { + background: rgba(127, 179, 213, 0.08); + border-color: #7fb3d5; + } + } + + .BiasNumber { + color: #7fb3d5; + } + + .BiasName { + color: #dadada; + } + + .BiasShort { + color: #c8c8c8; + + scrollbar-color: rgba(127, 179, 213, 0.4) transparent; + &::-webkit-scrollbar-thumb { + background: rgba(127, 179, 213, 0.4); + } + } + + .FooterMeta { + color: rgba(218, 218, 218, 0.5); + } + + .UseButton { + background: #28587b; + + &:hover { + background: #5396d3; + } + } +} diff --git a/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.module.scss b/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.module.scss index c1b4835..0daed11 100644 --- a/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.module.scss +++ b/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.module.scss @@ -30,6 +30,15 @@ .CyclingWord { display: inline-block; transition: all 0.3s; + // Default (non-bold) cycling word. Light theme: near-black. Inline JS + // colour was the same; moved here so the dark-theme rule can override. + color: #1a1a2e; + font-weight: 700; +} + +.CyclingWordBold { + color: #337ab7; + font-weight: 900; } .SubTagline { @@ -54,3 +63,37 @@ border-top: 1px solid #f1f5f9; } } + +// Dark theme: the outer "Choose your Rival" card surface + headings. +:global(body.darkTheme) { + .Root { + background: transparent; + } + + .Eyebrow { + color: #dadada; + } + + .SubTagline { + color: rgba(218, 218, 218, 0.6); + } + + .SubTaglineLead { + color: #dadada; + } + + .Divider hr { + border-top-color: #303338; + } + + // Cycling subtitle word: non-bold (Rival / Customer / Audience) was a + // near-black inline color, invisible on dark. Bold word (Persona) was + // light blue, already visible — keep a lighter accent in dark mode. + .CyclingWord { + color: #dadada; + } + + .CyclingWordBold { + color: #7fb3d5; + } +} diff --git a/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.tsx b/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.tsx index b4098eb..a6dc7de 100644 --- a/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.tsx +++ b/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.tsx @@ -1,10 +1,9 @@ -import { useRouter } from 'next/router'; -import { type FC, useEffect, useRef, useState } from 'react'; - +import { type Country, countryBiasByLocale } from '@uxcore/data/countryBias'; import type { StrapiBiasType } from '@uxcore/local-types/data'; import type { TRouter } from '@uxcore/local-types/global'; - -import { type Country, countryBiasByLocale } from '@uxcore/data/countryBias'; +import cn from 'classnames'; +import { useRouter } from 'next/router'; +import { type FC, useEffect, useRef, useState } from 'react'; import BiasPanel from './BiasPanel'; import CountryList from './CountryList'; @@ -231,7 +230,7 @@ const CyclingSubtitleWord: FC = ({ return ( = ({ : phase === 'entering' ? 'translateY(6px)' : 'translateY(0)', - fontWeight: isBold ? 900 : 700, - color: isBold ? '#337AB7' : '#1A1A2E', }} > {word} diff --git a/src/uxcore/components/_uxcp/CountryBiasMap/CountryList/CountryList.module.scss b/src/uxcore/components/_uxcp/CountryBiasMap/CountryList/CountryList.module.scss index 7077d7a..ac28412 100644 --- a/src/uxcore/components/_uxcp/CountryBiasMap/CountryList/CountryList.module.scss +++ b/src/uxcore/components/_uxcp/CountryBiasMap/CountryList/CountryList.module.scss @@ -175,3 +175,71 @@ .Spacer { padding-bottom: 24px; } + +// Dark theme: country picker search, cards, more/less buttons. +:global(body.darkTheme) { + .SearchInput { + background: #151a26; + border-color: #303338; + color: #dadada; + + &::placeholder { + color: rgba(218, 218, 218, 0.45); + } + + &:focus { + border-color: #7fb3d5; + box-shadow: 0 0 0 2px rgba(127, 179, 213, 0.2); + } + } + + .Card { + background: #151a26; + border-color: #303338; + + &:hover { + background: #232a3a; + border-color: #7fb3d5; + } + } + + .CardSelected { + background: rgba(127, 179, 213, 0.08); + border-color: #7fb3d5; + box-shadow: + 0 0 0 1px #7fb3d5, + 0 4px 20px rgba(127, 179, 213, 0.1); + + &:hover { + background: rgba(127, 179, 213, 0.1); + border-color: #7fb3d5; + box-shadow: + 0 0 0 1px #7fb3d5, + 0 4px 20px rgba(127, 179, 213, 0.1); + } + } + + .CardName { + color: #dadada; + } + + .CardConfidence { + color: #7fb3d5; + } + + .MoreButton, + .LessButton { + background: #151a26; + border-color: #303338; + color: #7fb3d5; + + &:hover { + background: rgba(127, 179, 213, 0.08); + border-color: #7fb3d5; + } + } + + .LessButton { + color: rgba(218, 218, 218, 0.65); + } +} diff --git a/src/uxcore/components/_uxcp/CountryBiasMap/CountryMap/CountryMap.module.scss b/src/uxcore/components/_uxcp/CountryBiasMap/CountryMap/CountryMap.module.scss index c636a78..d165f79 100644 --- a/src/uxcore/components/_uxcp/CountryBiasMap/CountryMap/CountryMap.module.scss +++ b/src/uxcore/components/_uxcp/CountryBiasMap/CountryMap/CountryMap.module.scss @@ -118,3 +118,32 @@ letter-spacing: 0.025em; color: #337ab7; } + +// Dark theme: country-map label text-shadow needs to flip (was hard-coded +// white to glow against the light map; needs dark glow on dark) and the +// hover tooltip needs a dark surface. +:global(body.darkTheme) { + .LabelText { + text-shadow: + 0 0 3px #1b1e26, + 0 0 6px #1b1e26, + 0 0 9px #1b1e26; + } + + .Tooltip { + background: rgba(27, 30, 38, 0.95); + border-color: #303338; + } + + .TooltipName { + color: #dadada; + } + + .TooltipMeta { + color: rgba(218, 218, 218, 0.7); + } + + .TooltipHint { + color: #7fb3d5; + } +} diff --git a/src/uxcore/components/_uxcp/CountryBiasMap/FlagImage/FlagImage.tsx b/src/uxcore/components/_uxcp/CountryBiasMap/FlagImage/FlagImage.tsx index e6fa6b7..19d2d9f 100644 --- a/src/uxcore/components/_uxcp/CountryBiasMap/FlagImage/FlagImage.tsx +++ b/src/uxcore/components/_uxcp/CountryBiasMap/FlagImage/FlagImage.tsx @@ -1,5 +1,5 @@ import cn from 'classnames'; -import type { FC } from 'react'; +import { type FC, useState } from 'react'; import styles from './FlagImage.module.scss'; @@ -9,6 +9,15 @@ interface FlagImageProps { className?: string; } +// Convert "AR" → "🇦🇷" using Unicode regional indicator symbols. +const codeToEmoji = (code: string): string => { + if (code.length !== 2) return ''; + const A = 0x1f1e6; // 🇦 + return Array.from(code.toUpperCase()) + .map(c => String.fromCodePoint(A + (c.charCodeAt(0) - 65))) + .join(''); +}; + const FlagImage: FC = ({ countryCode, size = 20, @@ -18,6 +27,30 @@ const FlagImage: FC = ({ const w = Math.round(size * 1.5); const cdnW = w >= 40 ? 80 : 40; const cdnW2x = w >= 40 ? 160 : 80; + const [failed, setFailed] = useState(false); + + if (failed) { + // External flag CDN (flagcdn.com) is sometimes blocked by ad-blockers + // or restrictive network policies. Fall back to an emoji flag so the + // row still reads instead of showing a broken-image icon. + return ( + + {codeToEmoji(countryCode) || countryCode.toUpperCase()} + + ); + } return ( = ({ className={cn(styles.Flag, className)} style={{ width: w, height: size }} loading="lazy" + onError={() => setFailed(true)} /> ); }; diff --git a/src/uxcore/components/_uxcp/DecisionTable/DecisionTable.module.scss b/src/uxcore/components/_uxcp/DecisionTable/DecisionTable.module.scss index 24e271b..5342989 100644 --- a/src/uxcore/components/_uxcp/DecisionTable/DecisionTable.module.scss +++ b/src/uxcore/components/_uxcp/DecisionTable/DecisionTable.module.scss @@ -91,11 +91,8 @@ $font-color: rgba(0, 0, 0, 0.65); } .tableText { - background: linear-gradient( - 180deg, - rgba(255, 255, 255, 0) 60.23%, - #ffffff 92.05% - ), + background: + linear-gradient(180deg, rgba(255, 255, 255, 0) 60.23%, #ffffff 92.05%), rgba(0, 0, 0, 0.85); -webkit-background-clip: text; -webkit-text-fill-color: transparent; @@ -220,3 +217,80 @@ $font-color: rgba(0, 0, 0, 0.65); } } } + +// Dark theme: re-skin the whole Decision Table — wrapper border, header +// row, body row zebra striping, row hover, gradient text fade endpoint, +// empty-cell color, divider above the footer buttons, error message +// callout. +:global(body.darkTheme) { + .mainTitle { + color: #dadada; + } + + .mobileView { + border-color: #303338; + } + + .wrapper { + border-color: #303338; + + .disabledTable { + background: rgba(27, 30, 38, 0.55); + } + + .table { + th, + td { + border-bottom-color: #303338; + } + + th { + background-color: rgba(127, 179, 213, 0.06); + color: #dadada; + } + + tr:nth-child(even) { + background-color: rgba(127, 179, 213, 0.04); + } + + tr:hover { + background-color: rgba(127, 179, 213, 0.12); + } + + // The light-theme rule uses background-clip:text + a gradient that + // fades the bottom of long cells to white. That trick is fragile on + // dark — short text ends up entirely inside the fade band and the + // cell reads as a blurred gray smudge. Disable the clip and paint + // solid text instead. + .tableText { + background: none !important; + -webkit-text-fill-color: #dadada !important; + color: #dadada !important; + } + + .tableTextEmpty { + color: rgba(218, 218, 218, 0.35); + } + } + + .btnWrapper { + border-top-color: #303338; + + .saveBtn { + background: #151a26; + border-color: #303338; + color: rgba(218, 218, 218, 0.6); + } + + .saveBtnIcon { + fill: rgba(218, 218, 218, 0.6); + } + } + + .errorMessage { + background-color: #1b1e26; + border-color: #fb1717; + color: #f5c2c7; + } + } +} diff --git a/src/uxcore/components/_uxcp/LogInModal/LogInModal.module.scss b/src/uxcore/components/_uxcp/LogInModal/LogInModal.module.scss index 6b4487e..d87f759 100644 --- a/src/uxcore/components/_uxcp/LogInModal/LogInModal.module.scss +++ b/src/uxcore/components/_uxcp/LogInModal/LogInModal.module.scss @@ -53,3 +53,26 @@ line-height: 10px !important; } } + +// Dark theme: re-skin the provider buttons (which are white-on-light by +// default) and the supporting copy so the whole login modal reads on a +// dark surface. +:global(body.darkTheme) .contentWrapper { + .title { + color: #7fb3d5; + } + + .description { + color: rgba(218, 218, 218, 0.7); + } + + .link { + background-color: #151a26; + border-color: #303338; + color: #dadada; + + &:hover { + background-color: #232a3a; + } + } +} diff --git a/src/uxcore/components/_uxcp/Pagination/Pagination.module.scss b/src/uxcore/components/_uxcp/Pagination/Pagination.module.scss index 5239865..f487d79 100644 --- a/src/uxcore/components/_uxcp/Pagination/Pagination.module.scss +++ b/src/uxcore/components/_uxcp/Pagination/Pagination.module.scss @@ -42,3 +42,48 @@ } } } + +// Dark theme: in light mode the active page is dark-blue on white; on a +// dark page bg an inactive WHITE pill reads as "selected" while the +// actual selected one disappears. Invert: inactive pages get a dark +// surface, active gets a brighter blue pill that pops. Also pad more +// generously so the clickable area is comfortable. +:global(body.darkTheme) .Pagination { + & .Title { + color: rgba(218, 218, 218, 0.7); + margin-right: 8px; + } + + & .Pages { + gap: 4px; + + & .Page { + color: #dadada; + background-color: #151a26; + border: 1px solid #303338; + padding: 2px 10px; + min-width: 24px; + text-align: center; + transition: + background-color 0.15s ease, + border-color 0.15s ease; + + &:hover { + border-color: #7fb3d5; + color: #7fb3d5; + } + + &.Active { + background-color: #5396d3; + border-color: #5396d3; + color: #fff; + + &:hover { + background-color: #5396d3; + border-color: #5396d3; + color: #fff; + } + } + } + } +} diff --git a/src/uxcore/components/_uxcp/PersonaButton/PersonaButton.module.scss b/src/uxcore/components/_uxcp/PersonaButton/PersonaButton.module.scss index b2ba642..3d9f922 100644 --- a/src/uxcore/components/_uxcp/PersonaButton/PersonaButton.module.scss +++ b/src/uxcore/components/_uxcp/PersonaButton/PersonaButton.module.scss @@ -117,3 +117,24 @@ $hover-color: #1890ff; opacity: 0; } } + +// Dark theme: the neutral "Copy URL" button. Inverted (primary) variant +// keeps its blue brand color, just leans a hair lighter on hover. +:global(body.darkTheme) .PersonaButton { + background-color: #151a26; + border-color: #303338; + color: #dadada; + + & svg path { + fill: #dadada; + } + + &:not(.Disabled):hover { + border-color: #7fb3d5; + color: #7fb3d5; + + & path { + fill: #7fb3d5; + } + } +} diff --git a/src/uxcore/components/_uxcp/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss index fd6a64b..e306642 100644 --- a/src/uxcore/components/_uxcp/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss +++ b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss @@ -108,3 +108,32 @@ } } } + +// Dark theme: the stage-indicator pill renders on a saturated brand +// color (green / purple / coral / orange). In dark mode the body cascade +// (#dadada) was overriding the button's intended white label, AND on the +// brightest two variants (Active3 coral and Active4 orange) even white +// text was washing out. Lock label to a high-contrast white on every +// child node AND darken the brand bg ~20-25% so the label has real +// contrast. Wider specificity (#dadada won via `body.darkTheme` chain +// before). +:global(body.darkTheme) .DynamicButton, +:global(body.darkTheme) .DynamicButton .Titles, +:global(body.darkTheme) .DynamicButton .Title { + color: #ffffff !important; +} + +:global(body.darkTheme) .DynamicButton { + &.Active1 { + background-color: #5e8a37; // greener / darker than #77a34b + } + &.Active2 { + background-color: #804a82; // darker purple + } + &.Active3 { + background-color: #ad4951; // darker coral + } + &.Active4 { + background-color: #c07f1f; // darker orange + } +} diff --git a/src/uxcore/components/_uxcp/PersonaRelatedQuestions/PersonaRelatedQuestions.module.scss b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/PersonaRelatedQuestions.module.scss index 22f918b..92850d1 100644 --- a/src/uxcore/components/_uxcp/PersonaRelatedQuestions/PersonaRelatedQuestions.module.scss +++ b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/PersonaRelatedQuestions.module.scss @@ -77,3 +77,24 @@ } } } + +// Dark theme: zebra-stripe rows + "select stage to begin" placeholder. +:global(body.darkTheme) { + .Table { + & > *:nth-child(even) { + background-color: rgba(127, 179, 213, 0.05); + } + + & .NoData { + color: rgba(218, 218, 218, 0.45); + } + } + + .Placeholder { + color: rgba(218, 218, 218, 0.55); + + & > img { + filter: invert(0.85); + } + } +} diff --git a/src/uxcore/components/_uxcp/PersonaRelatedQuestions/PriorityFilter/PriorityFilter.module.scss b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/PriorityFilter/PriorityFilter.module.scss index 4567445..a8a993c 100644 --- a/src/uxcore/components/_uxcp/PersonaRelatedQuestions/PriorityFilter/PriorityFilter.module.scss +++ b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/PriorityFilter/PriorityFilter.module.scss @@ -127,3 +127,29 @@ } } } + +// Dark theme: the "Relevance level / All / High / Medium / Low" filter +// strip was a hard white horizontal bar on top of the dark column. +:global(body.darkTheme) .PriorityFilter { + background-color: transparent; + color: #dadada; + + .TitleContainer { + color: #dadada; + } + + .Buttons .Button { + background-color: #151a26; + border-color: #303338; + color: #dadada; + } + + // The active state already paints #28587b bg + white text — keep it, + // it reads on dark too. Just lift the un-selected sibling border so it + // matches the active button's left border in dark. + .Buttons.Active-all .Button:nth-child(2), + .Buttons.Active-high .Button:nth-child(3), + .Buttons.Active-medium .Button:nth-child(4) { + border-left-color: #28587b; + } +} diff --git a/src/uxcore/components/_uxcp/PersonaRelatedQuestions/RelatedQuestion/RelatedQuestion.module.scss b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/RelatedQuestion/RelatedQuestion.module.scss index 287e869..f67d8f0 100644 --- a/src/uxcore/components/_uxcp/PersonaRelatedQuestions/RelatedQuestion/RelatedQuestion.module.scss +++ b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/RelatedQuestion/RelatedQuestion.module.scss @@ -44,3 +44,22 @@ } } } + +// Dark theme: question row text and divider were hard-coded for light. +:global(body.darkTheme) .RelatedQuestionContainer { + border-bottom-color: #303338; + + .RelatedQuestion { + .Number { + color: #7fb3d5; + } + + .Text { + color: #dadada; + + &:hover { + color: #ffffff; + } + } + } +} diff --git a/src/uxcore/components/_uxcp/SelectionView/SelectionView.module.scss b/src/uxcore/components/_uxcp/SelectionView/SelectionView.module.scss index 0426365..f992605 100644 --- a/src/uxcore/components/_uxcp/SelectionView/SelectionView.module.scss +++ b/src/uxcore/components/_uxcp/SelectionView/SelectionView.module.scss @@ -71,3 +71,23 @@ } } } + +// Dark theme: persona-name header, list metadata, "Add bias to begin" +// placeholder. +:global(body.darkTheme) .SelectionView { + .PersonaName { + color: #dadada; + } + + .BiasList .selectedBiasesNum { + color: rgba(218, 218, 218, 0.55); + } + + .Placeholder { + color: rgba(218, 218, 218, 0.55); + + & > img { + filter: invert(0.85); + } + } +} diff --git a/src/uxcore/components/_uxcp/SuggestedQuestions/Question/Question.module.scss b/src/uxcore/components/_uxcp/SuggestedQuestions/Question/Question.module.scss index c95bbd8..5815df3 100644 --- a/src/uxcore/components/_uxcp/SuggestedQuestions/Question/Question.module.scss +++ b/src/uxcore/components/_uxcp/SuggestedQuestions/Question/Question.module.scss @@ -29,3 +29,12 @@ font-size: 16px; } } + +// Dark theme: lift link color (hard-coded dark blue reads dim on dark). +:global(body.darkTheme) .Question { + color: #dadada; + + & a { + color: #7fb3d5; + } +} diff --git a/src/uxcore/components/_uxcp/SuggestedQuestions/SuggestedQuestions.module.scss b/src/uxcore/components/_uxcp/SuggestedQuestions/SuggestedQuestions.module.scss index daa0f01..c431105 100644 --- a/src/uxcore/components/_uxcp/SuggestedQuestions/SuggestedQuestions.module.scss +++ b/src/uxcore/components/_uxcp/SuggestedQuestions/SuggestedQuestions.module.scss @@ -27,3 +27,18 @@ font-size: 16px; } } + +// Dark theme: lift section copy + empty-state contrast. +:global(body.darkTheme) { + .SectionTitle { + color: #dadada; + } + + .SectionDescription { + color: #dadada; + } + + .QuestionList .NoData { + color: rgba(218, 218, 218, 0.45); + } +} diff --git a/src/uxcore/components/_uxcp/Switcher/Switcher.module.scss b/src/uxcore/components/_uxcp/Switcher/Switcher.module.scss index f5f11cc..8070576 100644 --- a/src/uxcore/components/_uxcp/Switcher/Switcher.module.scss +++ b/src/uxcore/components/_uxcp/Switcher/Switcher.module.scss @@ -1,5 +1,6 @@ $primary-color: #c4c4c4; $active-color: #5b88bd; +$dark-active-color: #7fb3d5; .Switcher { display: flex; @@ -39,3 +40,22 @@ $active-color: #5b88bd; } } } + +:global(body.darkTheme) .Switcher .Button { + color: #dadada; + border-color: #303338; + + &:hover { + background-color: rgba(127, 179, 213, 0.1); + } + + &.Active { + color: $dark-active-color; + border-color: $dark-active-color; + background-color: #1b1e26; + } + + &:nth-child(2) { + border-left-color: $dark-active-color; + } +} diff --git a/src/uxcore/components/_uxcp/TabHeader/TabHeader.module.scss b/src/uxcore/components/_uxcp/TabHeader/TabHeader.module.scss index 5ae1fe3..3740a3d 100644 --- a/src/uxcore/components/_uxcp/TabHeader/TabHeader.module.scss +++ b/src/uxcore/components/_uxcp/TabHeader/TabHeader.module.scss @@ -117,3 +117,46 @@ } } } + +// Dark theme: tab card. The default variant uses a blue accent wedge, +// the `.pink` variant uses a purple one; both ship pastel light colors +// that wash out on dark, so darken the wedge color and the hover bg. +:global(body.darkTheme) .TabHeader { + background-color: #1b1e26; + + &:hover { + background-color: #232a3a; + } + + .description { + color: #7fb3d5; + + &::after { + background-color: #7fb3d5; + } + } + + .icons { + border-top-color: rgba(127, 179, 213, 0.18); + } + + &.pink { + background-color: #20182a; + + &:hover { + background-color: #2a1f38; + } + + .icons { + border-top-color: rgba(163, 106, 164, 0.22); + } + + .description { + color: #c79bc8; + + &::after { + background-color: #c79bc8; + } + } + } +} diff --git a/src/uxcore/components/_uxcp/UXCPDescription/UXCPDescription.module.scss b/src/uxcore/components/_uxcp/UXCPDescription/UXCPDescription.module.scss index f1dcf71..643c461 100644 --- a/src/uxcore/components/_uxcp/UXCPDescription/UXCPDescription.module.scss +++ b/src/uxcore/components/_uxcp/UXCPDescription/UXCPDescription.module.scss @@ -61,3 +61,18 @@ } } } + +// Dark theme: page description copy and inline link. Default link color +// is hard-coded black, which is invisible on dark bg. +:global(body.darkTheme) .ContentTitle { + color: #dadada; + + .ContentTitleDescription, + .ContentTitleDescription > b { + color: #dadada; + } + + .Link { + color: #7fb3d5; + } +} diff --git a/src/uxcore/components/uxcg/PanelHeader/PanelHeader.module.scss b/src/uxcore/components/uxcg/PanelHeader/PanelHeader.module.scss index b602b4c..c90f944 100644 --- a/src/uxcore/components/uxcg/PanelHeader/PanelHeader.module.scss +++ b/src/uxcore/components/uxcg/PanelHeader/PanelHeader.module.scss @@ -26,4 +26,10 @@ .text { color: #dadada !important; } + + // Panel-header icons are dark monochrome SVGs designed for the light panel. + // Invert them so they read against the dark panel background. + .img { + filter: invert(0.85); + } } diff --git a/src/uxcore/components/uxcg/TagContainer/TagContainer.module.scss b/src/uxcore/components/uxcg/TagContainer/TagContainer.module.scss index e22e353..6c0de66 100644 --- a/src/uxcore/components/uxcg/TagContainer/TagContainer.module.scss +++ b/src/uxcore/components/uxcg/TagContainer/TagContainer.module.scss @@ -160,3 +160,11 @@ :global(body.darkTheme) .iconAndTitle .title { color: rgba(218, 218, 218, 0.85) !important; } + +// Dark theme: the unselected tile uses a light grey PNG as its background, +// which leaks light-theme look in dark mode. Invert + darken just the +// unselected image so the texture survives and contrast holds. Selected tile +// keeps its colored background untouched. +:global(body.darkTheme) .tagContainer:not(.selected) .backgroundImage { + filter: invert(1) brightness(0.55) contrast(1.05); +} diff --git a/src/uxcore/data/table/en.ts b/src/uxcore/data/table/en.ts index df5e36b..11d9a49 100644 --- a/src/uxcore/data/table/en.ts +++ b/src/uxcore/data/table/en.ts @@ -1,6 +1,6 @@ const en = { searchPlaceholder: 'Search by your keywords', - allQuestionsButtonLabel: 'All questions', + allQuestionsButtonLabel: 'All Questions', showMoreText: 'Show more', showLessText: 'Show less', noResultsText: ' No results found', diff --git a/src/uxcore/hooks/useGlobals.ts b/src/uxcore/hooks/useGlobals.ts index 2c27ee3..5bc0ef4 100644 --- a/src/uxcore/hooks/useGlobals.ts +++ b/src/uxcore/hooks/useGlobals.ts @@ -1,6 +1,5 @@ -import { useCallback, useEffect, useState } from 'react'; - import { CustomHookType, DispatchFuntion } from '@uxcore/local-types/global'; +import { useCallback, useEffect, useState } from 'react'; type TFullscreenFunction = (options?: FullscreenOptions) => Promise; @@ -36,6 +35,27 @@ const reducer = (newState: any) => { /* ACTIONS */ +// Cross-realm theme sync. The keepsimple side and the UX Core side each +// ship their own copy of useGlobals (separate module state, separate +// listener arrays). They share the same localStorage key + body class, so +// persistence is fine — but in-memory state diverges when the user toggles +// in one realm and navigates to the other. To keep them in lockstep +// within a single tab we dispatch a window event on every toggle; both +// realms subscribe and mirror the new value into their own state. +const DARK_THEME_EVENT = 'darktheme:change'; + +if (typeof window !== 'undefined') { + window.addEventListener(DARK_THEME_EVENT, (e: Event) => { + const next = !!(e as CustomEvent).detail?.isDarkTheme; + if (state.isDarkTheme !== next) { + if (state.articleRef) { + state.articleRef.classList.toggle('darkTheme', next); + } + reducer({ isDarkTheme: next }); + } + }); +} + // dark theme action const toggleIsDarkTheme = () => { const newThemeState = !state.isDarkTheme; @@ -45,6 +65,11 @@ const toggleIsDarkTheme = () => { state.articleRef.classList.toggle('darkTheme', newThemeState); } reducer({ isDarkTheme: newThemeState }); + window.dispatchEvent( + new CustomEvent(DARK_THEME_EVENT, { + detail: { isDarkTheme: newThemeState }, + }), + ); }; // sidebar action @@ -130,12 +155,15 @@ const initUseGlobals = (articleRef: HTMLElement) => { // Init articleRef setArticleRef(articleRef); - // Dark theme + // Dark theme — apply localStorage value unconditionally (true OR false) + // so navigation between realms always re-syncs in-memory state. const isDarkTheme = localStorage.getItem('darkTheme') === 'true'; - if (isDarkTheme) { - document.body.classList.add('darkTheme'); - articleRef.classList.add('darkTheme'); - reducer({ isDarkTheme: true }); + document.body.classList.toggle('darkTheme', isDarkTheme); + if (articleRef) { + articleRef.classList.toggle('darkTheme', isDarkTheme); + } + if (state.isDarkTheme !== isDarkTheme) { + reducer({ isDarkTheme }); } window.addEventListener('resize', handleSidebarChanges); @@ -146,9 +174,9 @@ const initUseGlobals = (articleRef: HTMLElement) => { const initDarkTheme = () => { if (typeof window === 'undefined') return; const isDarkTheme = localStorage.getItem('darkTheme') === 'true'; - if (isDarkTheme) { - document.body.classList.add('darkTheme'); - reducer({ isDarkTheme: true }); + document.body.classList.toggle('darkTheme', isDarkTheme); + if (state.isDarkTheme !== isDarkTheme) { + reducer({ isDarkTheme }); } }; diff --git a/src/uxcore/layouts/CalculatingResults/CalculatingResults.module.scss b/src/uxcore/layouts/CalculatingResults/CalculatingResults.module.scss index efd2ea3..dbaf017 100644 --- a/src/uxcore/layouts/CalculatingResults/CalculatingResults.module.scss +++ b/src/uxcore/layouts/CalculatingResults/CalculatingResults.module.scss @@ -124,6 +124,38 @@ } } +// Dark theme: info card and progress bars. +:global(body.darkTheme) .calculatingResults { + .loaderWrapper { + .loader div { + border-color: #7fb3d5 transparent transparent transparent; + } + + .title { + color: #dadada; + } + } + + .info { + background-color: #1b1e26; + border: 1px solid #303338; + box-shadow: + 0px 8px 17px rgba(0, 0, 0, 0.35), + 0px 30px 30px rgba(0, 0, 0, 0.25), + 0px 68px 41px rgba(0, 0, 0, 0.15); + + .progress { + .txtAndCheckmark .progressTxt { + color: #dadada; + } + + .progressItem { + background: #303338; + } + } + } +} + @media (max-width: 560px) { .calculatingResults { .loaderWrapper { diff --git a/src/uxcore/layouts/CertificateLayout/CertificateLayout.module.scss b/src/uxcore/layouts/CertificateLayout/CertificateLayout.module.scss index bb8e6d6..8084241 100644 --- a/src/uxcore/layouts/CertificateLayout/CertificateLayout.module.scss +++ b/src/uxcore/layouts/CertificateLayout/CertificateLayout.module.scss @@ -33,3 +33,9 @@ font-family: 'Lato', sans-serif !important; } } + +// Dark theme: footer button bar. +:global(body.darkTheme) .wrapper .btnWrapper { + background: #15181f; + box-shadow: 0px -4px 16px 0px rgba(0, 0, 0, 0.45); +} diff --git a/src/uxcore/layouts/Layout.tsx b/src/uxcore/layouts/Layout.tsx index 4a62795..82f4959 100644 --- a/src/uxcore/layouts/Layout.tsx +++ b/src/uxcore/layouts/Layout.tsx @@ -1,19 +1,13 @@ -import { usePathname } from 'next/navigation'; -import { useRouter } from 'next/router'; -import React, { useContext, useEffect, useState } from 'react'; - -import { TRouter } from '@uxcore/local-types/global'; - -import useUCoreMobile from '@uxcore/hooks/uxcoreMobile'; - import { getPersonaList } from '@uxcore/api/personas'; - -import decisionTable from '@uxcore/data/decisionTable'; - import SavedPersonas from '@uxcore/components/_uxcp/SavedPersonas'; import { GlobalContext } from '@uxcore/components/Context/GlobalContext'; import ToolHeader from '@uxcore/components/ToolHeader'; import UXCorePopup from '@uxcore/components/UXCorePopup'; +import decisionTable from '@uxcore/data/decisionTable'; +import useUCoreMobile from '@uxcore/hooks/uxcoreMobile'; +import { TRouter } from '@uxcore/local-types/global'; +import { useRouter } from 'next/router'; +import React, { useContext, useEffect, useState } from 'react'; export default function Layout({ children }: { children: React.ReactNode }) { const router = useRouter(); @@ -32,7 +26,11 @@ export default function Layout({ children }: { children: React.ReactNode }) { const { savedPersonasTitles } = decisionTable[locale]; const { isUxcoreMobile } = useUCoreMobile()[1]; - const pathName = usePathname() ?? ''; + // Pages Router: usePathname() from next/navigation is App-Router-only and + // returns undefined on SSR but the real path on the client → hydration + // mismatch on every UX Core page. router.pathname is the Pages-Router + // equivalent and is stable across SSR/client. + const pathName = router.pathname ?? ''; const normalizedPath = pathName.replace(/\/+$/, ''); const pathnameWithBypass = /^\/uxcp$/i.test(normalizedPath) diff --git a/src/uxcore/layouts/OngoingLayout/OngoingLayout.module.scss b/src/uxcore/layouts/OngoingLayout/OngoingLayout.module.scss index 920b640..8d26b6c 100644 --- a/src/uxcore/layouts/OngoingLayout/OngoingLayout.module.scss +++ b/src/uxcore/layouts/OngoingLayout/OngoingLayout.module.scss @@ -503,3 +503,84 @@ $white: #fafafa; opacity: 1; } } + +// Dark theme: re-skin the ongoing-test surface — page bg, question +// card, question text, the bias info chip, answer rows, skeleton +// placeholders, the answer prefix tag, and the selected-state border. +:global(body.darkTheme) .ongoing { + background-color: transparent; + + .questionWrapper { + background-color: transparent; + box-shadow: none; + + .sectionHeader { + .questionTxt { + color: #dadada; + } + } + } + + .questionAnswers { + .iconAndBias { + background: rgba(127, 179, 213, 0.06); + color: #dadada; + } + + .description p span { + color: inherit !important; + } + + .answerWrapper, + .skeleton { + background-color: #1b1e26; + border-color: #303338; + color: #dadada; + box-shadow: + 0px 8px 17px 0px rgba(0, 0, 0, 0.25), + 0px 30px 30px 0px rgba(0, 0, 0, 0.18), + 0px 68px 41px 0px rgba(0, 0, 0, 0.12); + + .prefix { + color: #ffffff; + background: rgba(127, 179, 213, 0.25); + } + + .question { + color: #dadada; + } + } + + .skeleton { + background-color: #15181f; + } + + .selected { + border-color: #7fb3d5; + background-color: rgba(127, 179, 213, 0.18); + } + } + + .btnWrapper { + .btnIcon > svg { + fill: #dadada; + } + } +} + +@media (max-width: 800px) { + :global(body.darkTheme) .ongoing { + .questionAnswers { + .answerWrapper, + .quizQuestion { + .skeleton { + border-color: #303338; + } + + .selected { + border-color: #7fb3d5; + } + } + } + } +} diff --git a/src/uxcore/layouts/StartTestLayout/StartTestLayout.module.scss b/src/uxcore/layouts/StartTestLayout/StartTestLayout.module.scss index 30ac3e2..28a852f 100644 --- a/src/uxcore/layouts/StartTestLayout/StartTestLayout.module.scss +++ b/src/uxcore/layouts/StartTestLayout/StartTestLayout.module.scss @@ -81,7 +81,28 @@ } } +// Dark theme: duration badge color + mobile background. +:global(body.darkTheme) .startTestWrapper { + .duration { + color: #c8c8c8; + } +} + +:global(body.darkTheme) .modal { + .description { + color: #dadada; + } +} + @media (max-width: 901px) { + :global(body.darkTheme) .startTestWrapper { + .duration { + background-color: rgba(21, 24, 31, 0.85); + border-color: #303338; + color: #dadada; + } + } + .startTestWrapper { height: calc(100vh - 45px); diff --git a/src/uxcore/layouts/TestResult/TestResultLayout.module.scss b/src/uxcore/layouts/TestResult/TestResultLayout.module.scss index db7865b..68ebeee 100644 --- a/src/uxcore/layouts/TestResult/TestResultLayout.module.scss +++ b/src/uxcore/layouts/TestResult/TestResultLayout.module.scss @@ -213,6 +213,54 @@ } } +// Dark theme: result page — section header, encouragement card, the +// additionalInfo split panel + divider, reading-list links, and the +// "next test available" date accent. +:global(body.darkTheme) { + .testResult, + .testResultLayoutWithCode { + .sectionHeader { + background-color: #15181f; + box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.4); + } + + .sectionBody { + .encourage, + .additionalInf, + .additionalInfo { + background: #1b1e26; + border-color: #303338; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4); + color: #dadada; + } + + .additionalInfo { + &::after { + background: #303338; + } + + .links { + .readingList { + .link { + color: #7fb3d5; + } + } + + .title { + color: #dadada; + } + } + + .reminder { + .date { + color: #c79bc8; + } + } + } + } + } +} + @media (max-width: 901px) { .mobileHeader { display: block; diff --git a/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss b/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss index be7fbdc..c7e0dd0 100644 --- a/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss +++ b/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss @@ -6,6 +6,27 @@ position: relative; overflow-y: auto; overflow-x: hidden; + + // Dark theme: page surface + headings. + :global(body.darkTheme) & { + background-color: transparent; + + .Title { + color: #7fb3d5; + } + + .ShortName { + color: #a8a8a8; + } + + .Motto { + color: #7fb3d5; + } + + .Heading { + color: #dadada; + } + } } .Content { diff --git a/src/uxcore/layouts/UXCatLayout/UXCatLayout.module.scss b/src/uxcore/layouts/UXCatLayout/UXCatLayout.module.scss index 06e7581..6d83f95 100644 --- a/src/uxcore/layouts/UXCatLayout/UXCatLayout.module.scss +++ b/src/uxcore/layouts/UXCatLayout/UXCatLayout.module.scss @@ -110,3 +110,24 @@ } } } + +// Dark theme: page surface + headings. +:global(body.darkTheme) .body { + background-color: transparent; + + .completionSkeleton { + border-color: #303338; + } + + .content { + background-color: transparent; + + .title { + color: #7fb3d5; + } + + .shortTitle { + color: #a8a8a8; + } + } +} diff --git a/src/uxcore/layouts/UXCoreLayout/UXCoreLayout.module.scss b/src/uxcore/layouts/UXCoreLayout/UXCoreLayout.module.scss index 2efbad2..dc5f0d3 100644 --- a/src/uxcore/layouts/UXCoreLayout/UXCoreLayout.module.scss +++ b/src/uxcore/layouts/UXCoreLayout/UXCoreLayout.module.scss @@ -22,12 +22,18 @@ position: absolute; top: 65px; right: 20px; + // The sibling .coreView fills the entire viewport (100vw / 100vh) and is + // rendered AFTER the switchers in the DOM, so without a z-index it + // stacks on top of them and silently eats every click on the switcher + // buttons. Lift the switchers above the core view layer. + z-index: 3; } .viewTeamSwitcher { position: absolute; top: 150px; right: 20px; + z-index: 3; } .Logos { diff --git a/src/uxcore/styles/globals.scss b/src/uxcore/styles/globals.scss index 4c5e367..c6fd90a 100644 --- a/src/uxcore/styles/globals.scss +++ b/src/uxcore/styles/globals.scss @@ -7,6 +7,10 @@ html { html { overflow-y: overlay; overflow-x: hidden; + // Reserve the scrollbar gutter at all times so modals (which set + // overflow-y: hidden on ) do not cause the page to jump sideways + // when the scrollbar disappears. + scrollbar-gutter: stable; /* width */ &::-webkit-scrollbar { @@ -53,11 +57,21 @@ body { body.darkTheme { background: linear-gradient(180deg, #1b1e26 0%, #151a26 100%), #1b1e26; color: #dadada; + + // Default divider on the dark site — used by modals (Our Projects etc.) + // and any other surface that renders a bare
. + hr { + border: none; + border-top: 1px solid #303338; + } } +// Modal scroll-lock marker. Width compensation is now handled by +// `scrollbar-gutter: stable` on , so this rule no longer needs to +// push the body sideways — kept as a hook in case future modal logic +// needs to target the locked state from CSS. .hide-body-move { - margin-right: 8px; - background-color: rgba(0, 0, 0, 0.35); + // intentionally empty } .Toastify__toast-container { @@ -73,177 +87,8 @@ body.darkTheme { cursor: default !important; } -@font-face { - font-family: 'Lato'; - src: url('/fonts/Lato/Lato-Regular.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'Lato'; - src: url('/fonts/Lato/Lato-Semibold.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'IBM Plex Mono'; - src: url('/fonts/biases/IBMPlexMono-Regular.ttf') format('truetype'); - font-weight: normal; - font-display: swap; -} - -// Oswald is being used in BadgeBox aka Achievement Box -@font-face { - font-family: 'Oswald'; - src: url('/fonts/biases/Oswald-Bold') format('truetype'); - font-weight: normal; - font-display: swap; -} - -@font-face { - font-family: 'RedHatDisplay'; - src: url('/fonts/biases/RedHatDisplay.ttf'); - font-display: swap; -} - -@font-face { - font-family: 'Tomorrow-Regular'; - src: url('/fonts/Tomorrow/Tomorrow-Light.ttf') format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -// This is only for mobile -@font-face { - font-family: 'Tomorrow'; - src: url('/fonts/Tomorrow/Tomorrow-Regular.ttf') format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -// Tomorrow mediumru -@font-face { - font-family: 'Tomorrow-Medium'; - src: url('/fonts/Tomorrow/Tomorrow-Medium.ttf') format('truetype'); - font-weight: 500; - font-style: normal; - font-display: swap; -} - -// For user information of uxcat. Ranking and Top -@font-face { - font-family: 'DelaGothicOne-Regular'; - src: url('/fonts/DelaGothicOne-Regular.ttf') format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -// For UXCat certificate -@font-face { - font-family: 'Manrope-ExtraLight.ttf'; - src: url('/fonts/Manrope-ExtraLight.ttf') format('truetype'); - font-weight: 200; - font-style: normal; - font-display: swap; -} - -// For UXCat certificate -@font-face { - font-family: 'IBMPlexSans-Regular.ttf'; - src: url('/fonts/IBMPlexSans-Regular.ttf') format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -// For UXCat certificate -@font-face { - font-family: 'IBMPlexSans-SemiBold.ttf'; - src: url('/fonts/IBMPlexSans-SemiBold.ttf') format('truetype'); - font-weight: 600; - font-style: normal; - font-display: swap; -} - -// Armenian -@font-face { - font-family: 'NotoSansArmenian-Regular'; - src: url('/fonts/NotoSansArmenian/NotoSansArmenian-Regular.ttf') - format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -// Aboreto-Regular for Article titles -@font-face { - font-family: 'Aboreto-Regular'; - src: url('/fonts/Aboreto-Regular.ttf') format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -// Cormorant_Garamond-Bold for ArticlesLayout [for russian version] -@font-face { - font-family: 'Cormorant_Garamond-Regular'; - src: url('/fonts/Cormorant_Garamond/static/CormorantGaramond-Regular.ttf') - format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -// Cormorant_Garamond-Medium for ArticlesLayout [for russian version] -@font-face { - font-family: 'Cormorant-Garamond-Medium'; - src: url('/fonts/Cormorant_Garamond/static/CormorantGaramond-Medium.ttf') - format('truetype'); - font-weight: 500; - font-style: normal; - font-display: swap; -} - -// Source Serif 4 for ArticlesLayout -@font-face { - font-family: 'Source-Serif-Regular'; - src: url('/fonts/Source-Serif-4/static/SourceSerif4-Regular.ttf') - format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -// Source Serif 4 for ArticlesLayout -@font-face { - font-family: 'Source-Serif-Bold'; - src: url('/fonts/Source-Serif-4/static/SourceSerif4-Bold.ttf') - format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -// Source Serif 4 for ArticlesLayout -@font-face { - font-family: 'Source-Serif-SemiBold'; - src: url('/fonts/Source-Serif-4/static/SourceSerif4-SemiBold.ttf') - format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -* { - font-family: 'Lato', 'NotoSansArmenian-Regular', Arial, serif; -} +// @font-face declarations live in src/styles/globals.scss. +// The body.uxcorePage default-to-Lato rule also lives there. @media (max-width: 961px) { body { diff --git a/src/uxcore/styles/uxcore-fonts.scss b/src/uxcore/styles/uxcore-fonts.scss deleted file mode 100644 index c38b45a..0000000 --- a/src/uxcore/styles/uxcore-fonts.scss +++ /dev/null @@ -1,90 +0,0 @@ -// UX Core @font-face declarations + scoped default. -// Extracted from src/uxcore/styles/globals.scss so the UX Core fonts load -// in the consolidated app (which uses keepsimple's globals.scss as the -// canonical global stylesheet) without colliding on the global `* { font-family }` rule. -// The scoped `body.uxcorePage *` selector matches the prod behaviour where -// every element on a UX Core route falls through to Lato when no component-level -// font-family is set. - -@font-face { - font-family: 'Lato'; - src: url('/fonts/Lato/Lato-Regular.woff2') format('woff2'); - font-weight: normal; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'Lato'; - src: url('/fonts/Lato/Lato-Semibold.woff2') format('woff2'); - font-weight: bold; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'IBM Plex Mono'; - src: url('/fonts/biases/IBMPlexMono-Regular.ttf') format('truetype'); - font-weight: normal; - font-display: swap; -} - -@font-face { - font-family: 'Oswald'; - src: url('/fonts/biases/Oswald-Bold') format('truetype'); - font-weight: normal; - font-display: swap; -} - -@font-face { - font-family: 'RedHatDisplay'; - src: url('/fonts/biases/RedHatDisplay.ttf'); - font-display: swap; -} - -@font-face { - font-family: 'DelaGothicOne-Regular'; - src: url('/fonts/DelaGothicOne-Regular.ttf') format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'Manrope-ExtraLight.ttf'; - src: url('/fonts/Manrope-ExtraLight.ttf') format('truetype'); - font-weight: 200; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'IBMPlexSans-Regular.ttf'; - src: url('/fonts/IBMPlexSans-Regular.ttf') format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'NotoSansArmenian-Regular'; - src: url('/fonts/NotoSansArmenian/NotoSansArmenian-Regular.ttf') - format('truetype'); - font-weight: 400; - font-style: normal; - font-display: swap; -} - -@font-face { - font-family: 'Cormorant-Garamond-Medium'; - src: url('/fonts/Cormorant_Garamond/static/CormorantGaramond-Medium.ttf') - format('truetype'); - font-weight: 500; - font-style: normal; - font-display: swap; -} - -body.uxcorePage, -body.uxcorePage * { - font-family: 'Lato', 'NotoSansArmenian-Regular', Arial, serif; -}