From f20776235ada16d6a520c3e27421866c198c378b Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 08:40:14 +0000 Subject: [PATCH 01/25] fix(uxc-dark): restore bias-modal questions section + UXCG dark contrast - /uxcore/[slug] now fetches UXCG questions in getStaticProps and feeds them via prop; the old read-from-GlobalContext path saw the null default (provider wires uxcgLocalizedData: null in _app.tsx) and the modal silently dropped its "This bias answers the following questions" section. - UXCG stage selector: invert + dim the unselected light-grey tile image in dark mode (selected tiles untouched, keep their colored bg). - UXCG PanelHeader: invert dark monochrome icons in dark mode so they read against the dark panel. Co-Authored-By: Claude Opus 4.7 --- src/pages/uxcore/[slug].tsx | 24 +++++++++---------- .../uxcg/PanelHeader/PanelHeader.module.scss | 6 +++++ .../TagContainer/TagContainer.module.scss | 8 +++++++ 3 files changed, 25 insertions(+), 13 deletions(-) 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/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); +} From 463d651fae6a0fc566906f56fcce25768eb1623e Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 08:53:31 +0000 Subject: [PATCH 02/25] fix(uxc-dark): polish dropdown/button/icon/login contrast in dark mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LanguageSwitcher: dark dropdown bg + light link/hover so the panel stops leaking light-theme look inside the bias modal; flag SVGs now show on a matching dark surface. - Table show-more/less buttons: explicit dark border-color so the light default 1px border stops drawing a frame against the dark panel. - Input clear icon: invert filter so the dark monochrome × is visible against the dark input. - UserDropdown "Log In" / username: lift base text from rgba(0,0,0,.85) to the dark-theme foreground; same for hover/active. Co-Authored-By: Claude Opus 4.7 --- src/uxcore/components/Input/Input.module.scss | 6 +++++ .../LanguageSwitcher.module.scss | 23 +++++++++++++++++++ src/uxcore/components/Table/Table.module.scss | 2 ++ .../UserDropdown/UserDropdown.module.scss | 23 +++++++++++++++++++ 4 files changed, 54 insertions(+) diff --git a/src/uxcore/components/Input/Input.module.scss b/src/uxcore/components/Input/Input.module.scss index 5e37525..713e845 100644 --- a/src/uxcore/components/Input/Input.module.scss +++ b/src/uxcore/components/Input/Input.module.scss @@ -129,3 +129,9 @@ font-size: 16px; } } + +// Dark theme: the input "clear" (×) icon ships as a dark monochrome SVG +// for the light theme; invert it so it reads against the dark input bg. +:global(body.darkTheme) .Input .InputWrapper .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..96e81ce 100644 --- a/src/uxcore/components/LanguageSwitcher/LanguageSwitcher.module.scss +++ b/src/uxcore/components/LanguageSwitcher/LanguageSwitcher.module.scss @@ -76,3 +76,26 @@ } } } + +// Dark theme: the dropdown sits on its own #fff panel by default, which +// looks like a light-mode leak inside the dark bias modal and washes out +// the colored flag SVGs by sheer brightness contrast. Match the modal +// palette instead. +: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; + } +} diff --git a/src/uxcore/components/Table/Table.module.scss b/src/uxcore/components/Table/Table.module.scss index dfd5555..beb8a97 100644 --- a/src/uxcore/components/Table/Table.module.scss +++ b/src/uxcore/components/Table/Table.module.scss @@ -505,10 +505,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/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; + } + } +} From 010358e4725c42779fe1d6dcf4348a4793fdc4c9 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 09:04:31 +0000 Subject: [PATCH 03/25] fix(uxc-dark): drop App-Router usePathname() from Pages-Router layout usePathname() from next/navigation is App-Router-only. In a Pages Router app it returns undefined during SSR and the real path on the client, so every conditional that branches on it (isUXCoreRoot, isUXCoreNested, shouldHideToolHeader) flipped between server and client render and produced a hydration mismatch on /uxcore (and any UX Core route). Swap to router.pathname which is identical in both phases. Co-Authored-By: Claude Opus 4.7 --- src/uxcore/layouts/Layout.tsx | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) 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) From fb2fe1490ee8f44cac0118d886830706ed15112e Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 09:11:34 +0000 Subject: [PATCH 04/25] fix(uxc-dark): more router.asPath hydration leaks on /uxcore#hr router.asPath includes the URL hash on the client but is hash-free during SSR. Four render-time comparisons against asPath === '/uxcore' (or /uxcg) were flipping between server and client whenever the URL carried a hash (e.g. /uxcore#hr), producing a second hydration mismatch even after the previous usePathname() fix. - MobileHeader: gate the podcast button on pathname instead of asPath - ToolHeader: same for the two header tooltips and the desktop podcast button; introduce a local pathname constant. Co-Authored-By: Claude Opus 4.7 --- .../components/ToolHeader/ToolHeader.tsx | 72 ++++++++++--------- .../_biases/MobileHeader/MobileHeader.tsx | 24 +++---- 2 files changed, 46 insertions(+), 50 deletions(-) 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/_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' && (
Date: Fri, 15 May 2026 09:31:38 +0000 Subject: [PATCH 05/25] fix(uxc): make View-type + Use-cases switcher buttons clickable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two sidebar switchers on /uxcore relied on a dataset.type === defaultViewLabel comparison that silently no-op'd whenever defaultViewLabel was undefined — which is exactly how UXCoreLayout calls the "View type" switcher (no defaultViewLabel prop). On a fresh load, the only ever-clickable button was the one whose state was already active, so neither button toggled. Drop the dataset trick: each button knows whether it's the first or second slot and toggles only when clicked from the opposite side. Also guard handleSnackbarOpening so the Use-cases pair stops throwing when the "View type" pair (no snackbar prop) is clicked. Co-Authored-By: Claude Opus 4.7 --- .../_biases/ViewSwitcher/ViewSwitcher.tsx | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/uxcore/components/_biases/ViewSwitcher/ViewSwitcher.tsx b/src/uxcore/components/_biases/ViewSwitcher/ViewSwitcher.tsx index 2a1aa44..52150df 100644 --- a/src/uxcore/components/_biases/ViewSwitcher/ViewSwitcher.tsx +++ b/src/uxcore/components/_biases/ViewSwitcher/ViewSwitcher.tsx @@ -1,11 +1,9 @@ +import biasesViewSwitcherIntl from '@uxcore/data/biasesViewSwitcher'; +import { TRouter } from '@uxcore/local-types/global'; import cn from 'classnames'; import { useRouter } from 'next/router'; import { memo, useCallback } from 'react'; -import { TRouter } from '@uxcore/local-types/global'; - -import biasesViewSwitcherIntl from '@uxcore/data/biasesViewSwitcher'; - import styles from './ViewSwitcher.module.scss'; type PropTypes = { @@ -47,19 +45,23 @@ const ViewSwitcher = ({ const { locale } = router as TRouter; const { label, labelViewerTeam } = biasesViewSwitcherIntl[locale]; - const handlePageViewChange = useCallback( - e => { - const { type } = e.currentTarget.dataset; - if ((type === defaultViewLabel) !== isSecondView) { - toggleIsCoreView(); - } - }, + // Activate the *opposite* side of the current view. Previously this was a + // dataset.type === defaultViewLabel comparison, which silently no-op'd + // whenever defaultViewLabel was undefined (the case for the "View type" + // pair) because both buttons read the same dataset value. + 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 +80,8 @@ const ViewSwitcher = ({ data-cy={dataCy} className={styles.ViewSwitcherButton} data-type={defaultViewLabel} - onClick={e => { - handlePageViewChange(e); + onClick={() => { + handleFirstClick(); switchATeamView(); }} > @@ -92,8 +94,8 @@ const ViewSwitcher = ({ data-cy={dataCySecondView} className={styles.ViewSwitcherButton} data-type={secondViewLabel} - onClick={e => { - handlePageViewChange(e); + onClick={() => { + handleSecondClick(); switchATeamView(); }} > From 2fa2586755678079ae88eb06520be29bdaadb386 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 09:37:37 +0000 Subject: [PATCH 06/25] fix(fonts): consolidate UX Core @font-face into globals.scss Per review on #113: the UX Core font declarations + the scoped body.uxcorePage default no longer live in a sibling stylesheet. The @font-face block and the body.uxcorePage Lato fallback move into src/styles/globals.scss, the separate src/uxcore/styles/uxcore-fonts.scss is deleted, and the extra import in _app.tsx is dropped. The body.uxcorePage rule stays placed AFTER the global `*` selector so specificity still wins over the Source-Serif default. Co-Authored-By: Claude Opus 4.7 --- src/pages/_app.tsx | 1 - src/styles/globals.scss | 89 ++++++++++++++++++++++++++++ src/uxcore/styles/uxcore-fonts.scss | 90 ----------------------------- 3 files changed, 89 insertions(+), 91 deletions(-) delete mode 100644 src/uxcore/styles/uxcore-fonts.scss diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 118af23..4d86125 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 = { diff --git a/src/styles/globals.scss b/src/styles/globals.scss index fd8d72b..4e4a4b1 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -376,6 +376,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/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; -} From 4621907f81e5aec2f4104ee4def4fc9e5586bab6 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 09:45:22 +0000 Subject: [PATCH 07/25] fix(uxc-dark): keep lang flags coloured + dark bias-popup tooltip - LanguageSwitcher: explicitly null any inherited image filter on the flag SVGs in dark mode so the colour stripes survive when the switcher sits inside a modal header whose dark rules apply saturate(0) / invert to images. - Tooltip popup: dark surface + arrow recolour for the non-Dark variant so the AnswerBiasLink hover popup inside the UXCG modal stops rendering as a white card on a dark modal. - BiasPopupContent: lift link colour and muted tip line for the dark surface. Co-Authored-By: Claude Opus 4.7 --- .../BiasPopupContent.module.scss | 14 +++++++++++++ .../LanguageSwitcher.module.scss | 12 ++++++++--- .../components/Tooltip/Tooltip.module.scss | 21 +++++++++++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) 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/LanguageSwitcher/LanguageSwitcher.module.scss b/src/uxcore/components/LanguageSwitcher/LanguageSwitcher.module.scss index 96e81ce..335075f 100644 --- a/src/uxcore/components/LanguageSwitcher/LanguageSwitcher.module.scss +++ b/src/uxcore/components/LanguageSwitcher/LanguageSwitcher.module.scss @@ -78,9 +78,10 @@ } // Dark theme: the dropdown sits on its own #fff panel by default, which -// looks like a light-mode leak inside the dark bias modal and washes out -// the colored flag SVGs by sheer brightness contrast. Match the modal -// palette instead. +// 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; @@ -98,4 +99,9 @@ .switcher { color: #dadada; } + + .flag, + .switcher img { + filter: none !important; + } } 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; + } +} From 5dd51b342ebd408c8ea384915d01efe5b587e331 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 09:47:52 +0000 Subject: [PATCH 08/25] fix(uxc-dark): keep selected navbar icons visible on hover MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dark override applied the dark-monochrome invert/brightness filter to every navbar SVG, including the selected (Active / ActivePodcast / ActiveProjects) items whose base path fill is already #fafafa. On hover, the filter combined with the lighter hover background flattened the icon to dark grey on dark grey — effectively disappearing. Null the filter on selected variants (and their hover state) so the white fill wins. Co-Authored-By: Claude Opus 4.7 --- .../ToolHeader/ToolHeader.module.scss | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) 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 { From 6bb7c21fa8489a2e35f638011d1288b11c5f7153 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 09:54:48 +0000 Subject: [PATCH 09/25] fix(uxc): unblock View-type + Use-cases switcher clicks Two compounding bugs were silencing the right-sidebar switchers: 1. .coreView (the hexagon/spider page) is sized 100vw x 100vh and is rendered AFTER the two .viewTypeSwitcher / .viewTeamSwitcher siblings in the DOM. Without a z-index, the core view layer stacks on top of the absolute-positioned switchers and absorbs every click that lands on them. Lift both switchers to z-index: 3. 2. The earlier "click any inactive button toggles" refactor had the active/inactive sides inverted. Restore the correct semantics: first slot is active when isSecondView=true (FolderView class), second slot is active when isSecondView=false (CoreView class). Toggle fires only from the inactive side. Co-Authored-By: Claude Opus 4.7 --- .../components/_biases/ViewSwitcher/ViewSwitcher.tsx | 11 +++++------ .../layouts/UXCoreLayout/UXCoreLayout.module.scss | 6 ++++++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/src/uxcore/components/_biases/ViewSwitcher/ViewSwitcher.tsx b/src/uxcore/components/_biases/ViewSwitcher/ViewSwitcher.tsx index 52150df..3db8e3e 100644 --- a/src/uxcore/components/_biases/ViewSwitcher/ViewSwitcher.tsx +++ b/src/uxcore/components/_biases/ViewSwitcher/ViewSwitcher.tsx @@ -45,16 +45,15 @@ const ViewSwitcher = ({ const { locale } = router as TRouter; const { label, labelViewerTeam } = biasesViewSwitcherIntl[locale]; - // Activate the *opposite* side of the current view. Previously this was a - // dataset.type === defaultViewLabel comparison, which silently no-op'd - // whenever defaultViewLabel was undefined (the case for the "View type" - // pair) because both buttons read the same dataset value. + // 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?.(); + if (!isSecondView) toggleIsCoreView?.(); }, [isSecondView, toggleIsCoreView]); const handleSecondClick = useCallback(() => { - if (!isSecondView) toggleIsCoreView?.(); + if (isSecondView) toggleIsCoreView?.(); }, [isSecondView, toggleIsCoreView]); const switchATeamView = () => { 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 { From f606465d1480cd51d34d7f8c629abc6844900117 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 10:09:18 +0000 Subject: [PATCH 10/25] fix(uxc-dark): make UXCG stage filter buttons visible MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inactive stage tag pills sat at 0.3 opacity, which disappears against the dark page background — including the "All Questions" button whose inline #282828 bg matched the page bg almost exactly. Lift inactive pills to 0.75 with a faint border and re-skin "All Questions" to a lighter neutral so the row reads as a real filter strip in dark theme. Also capitalize the label to "All Questions" for consistency with the other pill labels. Co-Authored-By: Claude Opus 4.7 --- src/uxcore/components/Table/Table.module.scss | 8 ++++++++ src/uxcore/components/Tag/Tag.module.scss | 19 +++++++++++++++++++ src/uxcore/data/table/en.ts | 2 +- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/uxcore/components/Table/Table.module.scss b/src/uxcore/components/Table/Table.module.scss index beb8a97..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 { 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/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', From 55f030da0e7f8f35de387ece0b7a0f61992ab196 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 10:18:36 +0000 Subject: [PATCH 11/25] fix(uxc-dark): dark theme for Our Projects + Log In modals; kill layout shift Dark theme for the shared Modal chrome (bg, header divider, title, close icon), the OurProjectsModal rows / GitHub-API outline buttons / divider, the LogInModal title / description / provider buttons, and the neutral Button variant (so the Cancel button reads on dark). Primary / Orange / BlueOutline button variants are left alone since they keep their brand colors. Also fixes the long-standing page-shift-on-modal-open: the old .hide-body-move rule added margin-right: 8px to when a modal opened, which only partially compensated for the ~15px scrollbar (net visible jump). Switch to scrollbar-gutter: stable on , which reserves the scrollbar's space at all times so toggling overflow:hidden on modal open no longer reflows the page sideways. Co-Authored-By: Claude Opus 4.7 --- .../components/Button/Button.module.scss | 15 ++++++++ src/uxcore/components/Modal/Modal.module.scss | 37 +++++++++++++++++++ .../OurProjectsModal.module.scss | 32 ++++++++++++++++ .../_uxcp/LogInModal/LogInModal.module.scss | 23 ++++++++++++ src/uxcore/styles/globals.scss | 11 +++++- 5 files changed, 116 insertions(+), 2 deletions(-) diff --git a/src/uxcore/components/Button/Button.module.scss b/src/uxcore/components/Button/Button.module.scss index 893752a..c2833a6 100644 --- a/src/uxcore/components/Button/Button.module.scss +++ b/src/uxcore/components/Button/Button.module.scss @@ -151,3 +151,18 @@ } } } + +// 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; + } +} diff --git a/src/uxcore/components/Modal/Modal.module.scss b/src/uxcore/components/Modal/Modal.module.scss index ceda65d..550814b 100644 --- a/src/uxcore/components/Modal/Modal.module.scss +++ b/src/uxcore/components/Modal/Modal.module.scss @@ -188,3 +188,40 @@ } } } + +// 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; + } + + .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/OurProjectsModal/OurProjectsModal.module.scss b/src/uxcore/components/OurProjectsModal/OurProjectsModal.module.scss index ebd9893..4da98c5 100644 --- a/src/uxcore/components/OurProjectsModal/OurProjectsModal.module.scss +++ b/src/uxcore/components/OurProjectsModal/OurProjectsModal.module.scss @@ -90,3 +90,35 @@ 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; + } + + hr { + border: none; + border-top: 1px solid #303338; + } + + .buttonStyleLink { + border-color: #303338; + color: #dadada; + background-color: transparent; + + &:hover { + background-color: #dadada; + color: #1b1e26; + } + } +} 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/styles/globals.scss b/src/uxcore/styles/globals.scss index 4c5e367..1fcd30c 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 { @@ -55,9 +59,12 @@ body.darkTheme { color: #dadada; } +// 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 { From 95b7e6ded1c643867665e24a2c2db2f45b6d3c07 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 10:24:12 +0000 Subject: [PATCH 12/25] feat(uxc-dark): Contact Us dark theme + cross-realm theme sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dark theme for the Contact Us modal (CustomModal chrome, body copy and inline links, Input + Textarea fields). Input + Textarea now flip globally on dark, which fixes other forms that reuse them too. Cross-realm dark-theme sync: keepsimple-side and UX Core-side each ship their own copy of useGlobals with separate module state. Both write to the same localStorage key and the same body class, so persistence was fine — but in-memory state diverged when the user toggled in one realm and navigated to the other, leaving the dark/light toggle button out of phase with the actual page. Wire a window 'darktheme:change' custom event that every toggle dispatches and that both realms subscribe to, so the realm you didn't toggle in mirrors the new value into its own state immediately. Also fix the init paths to apply the persisted value unconditionally (was only re-applying when true), and bootstrap the body class from localStorage at the _app root so deep-links to /uxcore etc. also pick up the persisted theme. Co-Authored-By: Claude Opus 4.7 --- src/hooks/useGlobals.ts | 44 ++++++++++++++--- src/pages/_app.tsx | 13 +++++ .../CustomModal/CustomModal.module.scss | 28 +++++++++++ .../ContactUs/ContactUs.module.scss | 9 ++++ src/uxcore/components/Input/Input.module.scss | 33 +++++++++++-- .../components/Textarea/Textarea.module.scss | 23 +++++++++ src/uxcore/hooks/useGlobals.ts | 48 +++++++++++++++---- 7 files changed, 177 insertions(+), 21 deletions(-) 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 4d86125..9787ea9 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -121,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/uxcore/components/CustomModal/CustomModal.module.scss b/src/uxcore/components/CustomModal/CustomModal.module.scss index 55a28d0..9d05a3d 100644 --- a/src/uxcore/components/CustomModal/CustomModal.module.scss +++ b/src/uxcore/components/CustomModal/CustomModal.module.scss @@ -92,3 +92,31 @@ } } } + +// 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::-webkit-scrollbar-thumb { + background: rgba(127, 179, 213, 0.5); + } + } +} 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 713e845..87e6d0c 100644 --- a/src/uxcore/components/Input/Input.module.scss +++ b/src/uxcore/components/Input/Input.module.scss @@ -130,8 +130,33 @@ } } -// Dark theme: the input "clear" (×) icon ships as a dark monochrome SVG -// for the light theme; invert it so it reads against the dark input bg. -:global(body.darkTheme) .Input .InputWrapper .Icon { - filter: invert(0.85) brightness(1.5); +// 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/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/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 }); } }; From b52387bc55421d769bd684078aaba29f66a7084f Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 10:27:38 +0000 Subject: [PATCH 13/25] =?UTF-8?q?fix(uxc-dark):=20unbreak=20DEV=20build=20?= =?UTF-8?q?=E2=80=94=20pure-selector=20rule=20for=20dark=20
?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bare hr inside OurProjectsModal.module.scss's :global(body.darkTheme) block was rejected by CSS Modules ("not pure — selectors must contain at least one local class or id"), failing the DEV build. Move the dark-mode divider rule into uxcore/styles/globals.scss under body.darkTheme where a bare element selector is allowed, so every
in dark mode gets the same divider treatment instead of one modal carrying its own copy. Co-Authored-By: Claude Opus 4.7 --- .../OurProjectsModal/OurProjectsModal.module.scss | 5 ----- src/uxcore/styles/globals.scss | 7 +++++++ 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/uxcore/components/OurProjectsModal/OurProjectsModal.module.scss b/src/uxcore/components/OurProjectsModal/OurProjectsModal.module.scss index 4da98c5..b372c3b 100644 --- a/src/uxcore/components/OurProjectsModal/OurProjectsModal.module.scss +++ b/src/uxcore/components/OurProjectsModal/OurProjectsModal.module.scss @@ -106,11 +106,6 @@ color: rgba(218, 218, 218, 0.75) !important; } - hr { - border: none; - border-top: 1px solid #303338; - } - .buttonStyleLink { border-color: #303338; color: #dadada; diff --git a/src/uxcore/styles/globals.scss b/src/uxcore/styles/globals.scss index 1fcd30c..1565250 100644 --- a/src/uxcore/styles/globals.scss +++ b/src/uxcore/styles/globals.scss @@ -57,6 +57,13 @@ 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 From a540f3702bcfec366673bb1f78c85c0d828db82e Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 10:34:15 +0000 Subject: [PATCH 14/25] fix(uxc-dark): visible X icon + dark + scrollable keepsimple Log In modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit UX Core LogInModal: the Twitter / X provider icon is a single-path SVG hard-coded to fill #000, which disappears against the dark provider button bg. Switch the uxcore copy of XIcon to fill="currentColor" so it inherits the link's text color — light grey in dark mode, black in light mode (matches the old visual). KeepSimple-side LogIn modal: this is a different component (different visual design, different Modal chrome) and was never dark-themed nor bounded to viewport height. Two problems fixed: 1. Position. A tall content tree (six provider buttons + email form) exceeded 100dvh and the centered modal clipped above and below the viewport. Bound the wrapper with max-height: calc(100dvh - 32px) and give the body flex: 1 + min-height: 0 so its overflow: auto engages. 2. Dark theme. Re-skin the wrapper (drop the paper-texture bg image and apply a dark surface), the header copy, the LogIn heading and subtitle, the Google button (gets a faint border so it doesn't blow out against the dark wrapper), the error banner, and the MagicLink email form (divider, label, input, submit, banner, confirmation). Brand-colored provider buttons (Discord / LinkedIn / X / Mail.ru / Yandex) keep their own colors. Co-Authored-By: Claude Opus 4.7 --- src/components/LogIn/LogIn.module.scss | 26 +++++++++ .../MagicLinkEmailForm.module.scss | 57 +++++++++++++++++++ src/components/Modal/Modal.module.scss | 39 +++++++++++++ src/uxcore/assets/icons/XIcon.tsx | 2 +- 4 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/components/LogIn/LogIn.module.scss b/src/components/LogIn/LogIn.module.scss index 9ad2697..c60ff63 100644 --- a/src/components/LogIn/LogIn.module.scss +++ b/src/components/LogIn/LogIn.module.scss @@ -75,3 +75,29 @@ } } } + +// 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..789107a 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,10 @@ .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; } } @@ -293,3 +302,33 @@ 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; + } + + .header { + .title, + .blackTitle, + .grayTitle { + color: #dadada; + } + + .grayTitle { + color: rgba(218, 218, 218, 0.55); + } + } + + .hasBorder { + border-bottom-color: #303338; + } +} 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 = () => { > ); From 44a4ff845ccba2feaa6febf7b6dad6870d31afbb Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 10:48:05 +0000 Subject: [PATCH 15/25] feat(uxc-dark): polish modal scroll + fade transitions + UXCP dark mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three pieces: 1. Modal scrollbar. The newly-bounded keepsimple modal body was using the default browser scrollbar (wide, white). Apply the same slim thumb treatment used elsewhere on the site so the scroll surface reads as part of the modal chrome instead of an OS widget. 2. Open / close fade transitions for the UX Core Modal and CustomModal. The keepsimple-side Modal already had wrapperIn / wrapperOut + overlayFadeIn / Out animations and a 180ms delayed-unmount close. The UX Core copies had neither, so modals popped in and out. Add the same pattern (closing state, delayed onClose, CSS animations) to both UX Core Modal and CustomModal so every modal site-wide animates in and out instead of snapping. 3. UXCP dark theme. The /uxcp page (UX Core Persona) was light-on-light with the dark page bg, leaving every card and form unreadable. Add dark-mode overrides for the layout headings (UX CORE PERSONA title, subtitle, Motto, section Heading), the shared Section card chrome, CountryBiasMap (search input, country grid cards, selected card, show-more / show-less buttons), BiasPanel (the selected-country detail card, bias chips, help popover, Use button), BiasItem (bias rows in the persona builder), PersonaButton (Copy URL button neutral variant), Switcher (All / High / Medium / Low filter pills), TabHeader (tab card with hover state), and BiasSearch (scrollbar thumb). Brand-colored buttons keep their colors. Also a flag-display fix on the country picker: flag images come from the flagcdn.com external CDN, which is commonly blocked by ad-blockers and some restrictive network policies — that's why every flag row showed a broken-image icon. Add an onError fallback that switches to a Unicode regional-indicator emoji flag (🇦🇷, 🇦🇲 etc.) so the row stays readable when the CDN is unreachable instead of showing a broken-image glyph. Co-Authored-By: Claude Opus 4.7 --- src/components/Modal/Modal.module.scss | 16 ++++ .../CustomModal/CustomModal.module.scss | 47 +++++++++++ .../components/CustomModal/CustomModal.tsx | 36 +++++--- src/uxcore/components/Modal/Modal.module.scss | 47 +++++++++++ src/uxcore/components/Modal/Modal.tsx | 21 ++++- .../components/Section/Section.module.scss | 5 ++ .../_uxcp/BiasItem/BiasItem.module.scss | 16 ++++ .../_uxcp/BiasSearch/BiasSearch.module.scss | 17 ++++ .../BiasPanel/BiasPanel.module.scss | 84 +++++++++++++++++++ .../CountryList/CountryList.module.scss | 68 +++++++++++++++ .../CountryBiasMap/FlagImage/FlagImage.tsx | 36 +++++++- .../PersonaButton/PersonaButton.module.scss | 21 +++++ .../_uxcp/Switcher/Switcher.module.scss | 20 +++++ .../_uxcp/TabHeader/TabHeader.module.scss | 17 ++++ .../layouts/UXCPLayout/UXCPLayout.module.scss | 22 +++++ 15 files changed, 458 insertions(+), 15 deletions(-) diff --git a/src/components/Modal/Modal.module.scss b/src/components/Modal/Modal.module.scss index 789107a..d39aa91 100644 --- a/src/components/Modal/Modal.module.scss +++ b/src/components/Modal/Modal.module.scss @@ -155,6 +155,22 @@ // 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; + } } } diff --git a/src/uxcore/components/CustomModal/CustomModal.module.scss b/src/uxcore/components/CustomModal/CustomModal.module.scss index 9d05a3d..7e08625 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; @@ -93,6 +103,43 @@ } } +@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. diff --git a/src/uxcore/components/CustomModal/CustomModal.tsx b/src/uxcore/components/CustomModal/CustomModal.tsx index b1efe02..1642d0e 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, 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,21 @@ 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); + }; useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === 'Escape') onClose(); + if (e.key === 'Escape') handleClose(); }; // @ts-ignore @@ -66,14 +75,17 @@ const CustomModal: FC = ({ <> {createPortal(
e.stopPropagation()} > @@ -85,7 +97,7 @@ const CustomModal: FC = ({
modal close button
@@ -93,10 +105,10 @@ const CustomModal: FC = ({
{contentType === 'addQuestion' && ( - + )} {contentType === 'contactUs' && ( - + )}
diff --git a/src/uxcore/components/Modal/Modal.module.scss b/src/uxcore/components/Modal/Modal.module.scss index 550814b..0e022ae 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 { @@ -189,6 +199,43 @@ } } +@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. diff --git a/src/uxcore/components/Modal/Modal.tsx b/src/uxcore/components/Modal/Modal.tsx index 10b63eb..17f087c 100644 --- a/src/uxcore/components/Modal/Modal.tsx +++ b/src/uxcore/components/Modal/Modal.tsx @@ -1,5 +1,11 @@ import cn from 'classnames'; -import React, { FC, KeyboardEvent, ReactNode, useEffect } from 'react'; +import React, { + FC, + KeyboardEvent, + ReactNode, + useEffect, + useState, +} from 'react'; import { createPortal } from 'react-dom'; import styles from './Modal.module.scss'; @@ -49,8 +55,17 @@ 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); }; useEffect(() => { @@ -89,6 +104,7 @@ const Modal: FC = ({
@@ -106,6 +122,7 @@ const Modal: FC = ({ [wrapperClassName]: wrapperClassName, [styles.fullSizeMobile]: fullSizeMobile, [styles.fullHeightMobile]: fullHeightMobile, + [styles.wrapperClosing]: closing, })} > {!removeHeader && ( 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/_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..e91e25b 100644 --- a/src/uxcore/components/_uxcp/CountryBiasMap/BiasPanel/BiasPanel.module.scss +++ b/src/uxcore/components/_uxcp/CountryBiasMap/BiasPanel/BiasPanel.module.scss @@ -270,3 +270,87 @@ 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: rgba(218, 218, 218, 0.7); + } + + .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: rgba(218, 218, 218, 0.7); + } + + .FooterMeta { + color: rgba(218, 218, 218, 0.5); + } + + .UseButton { + background: #28587b; + + &:hover { + background: #5396d3; + } + } +} 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/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/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/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..e98cb56 100644 --- a/src/uxcore/components/_uxcp/TabHeader/TabHeader.module.scss +++ b/src/uxcore/components/_uxcp/TabHeader/TabHeader.module.scss @@ -117,3 +117,20 @@ } } } + +// Dark theme: tab card. +:global(body.darkTheme) .TabHeader { + background-color: #1b1e26; + + &:hover { + background-color: #232a3a; + } + + .description { + color: #7fb3d5; + + &::after { + background-color: #7fb3d5; + } + } +} diff --git a/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss b/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss index be7fbdc..e758032 100644 --- a/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss +++ b/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss @@ -6,6 +6,28 @@ position: relative; overflow-y: auto; overflow-x: hidden; + + // Dark theme: page surface + headings. + :global(body.darkTheme) & { + background-color: transparent; + + .Title { + color: #7fb3d5; + } + + .ShortName, + .Motto { + color: rgba(218, 218, 218, 0.6); + } + + .Motto { + color: #7fb3d5; + } + + .Heading { + color: #dadada; + } + } } .Content { From 062cbc15c4c3a9b1de06cfd74bee087d8805d9e6 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 10:58:49 +0000 Subject: [PATCH 16/25] fix(uxc-dark): kill modal-open layout shift; polish UXCP dark theme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Modal-open scrollbar shift on every page. The previous scrollbar-gutter fix lived in uxcore/styles/globals.scss — but that file is not imported anywhere, so the rule was dead code. Move it to src/styles/globals.scss (the file _app.tsx actually loads) and make it unconditional on . The page now reserves the scrollbar's gutter at all times, so opening any modal — which locks scroll by setting overflow-y: hidden on — no longer reflows the layout sideways. 2. Modal body scrollbars. The keepsimple Modal already got a slim styled scrollbar; bring the UX Core Modal body and the CustomModal body into the same shape (6px width, soft blue thumb) with dark-theme variants (light-blue thumb on dark surface). 3. UXCP dark-theme gaps. Several surfaces were still light-on-light: - The "Choose your Rival" outer card carried a hard-coded white bg and dark text. Clear the bg and lift the eyebrow / subtitle / lead to dark-theme values. - The CountryMap label text-shadow was hard-coded white (designed to glow against the light map), which left a white halo on dark. Flip to a dark glow and dark-theme the hover tooltip. - The UXCPDescription welcome paragraph and "on Medium via this link." inline link were hard-coded dark, invisible on dark bg. - Lift the BiasPanel rationale and bias-chip description from a 0.7 alpha to solid #c8c8c8 — the dim text was hard to read on dark. - Slim styled scrollbar on the inner bias-chip description (it overflows on long bias descriptions). Co-Authored-By: Claude Opus 4.7 --- src/components/Modal/Modal.module.scss | 8 +++++ src/styles/globals.scss | 9 ++++++ .../CustomModal/CustomModal.module.scss | 12 ++++++-- src/uxcore/components/Modal/Modal.module.scss | 24 +++++++++++++++ .../BiasPanel/BiasPanel.module.scss | 24 +++++++++++++-- .../CountryBiasMap/CountryBiasMap.module.scss | 23 +++++++++++++++ .../CountryMap/CountryMap.module.scss | 29 +++++++++++++++++++ .../UXCPDescription.module.scss | 15 ++++++++++ 8 files changed, 139 insertions(+), 5 deletions(-) diff --git a/src/components/Modal/Modal.module.scss b/src/components/Modal/Modal.module.scss index d39aa91..8e8c73c 100644 --- a/src/components/Modal/Modal.module.scss +++ b/src/components/Modal/Modal.module.scss @@ -330,6 +330,14 @@ 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 { diff --git a/src/styles/globals.scss b/src/styles/globals.scss index 4e4a4b1..4b58c86 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; } diff --git a/src/uxcore/components/CustomModal/CustomModal.module.scss b/src/uxcore/components/CustomModal/CustomModal.module.scss index 7e08625..c531f52 100644 --- a/src/uxcore/components/CustomModal/CustomModal.module.scss +++ b/src/uxcore/components/CustomModal/CustomModal.module.scss @@ -60,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 { @@ -73,7 +75,7 @@ /* Handle */ &::-webkit-scrollbar-thumb { - background: rgba(40, 88, 123, 0.5); + background: rgba(40, 88, 123, 0.4); border-radius: 5px; } } @@ -162,8 +164,12 @@ } } - .ModalBody::-webkit-scrollbar-thumb { - background: rgba(127, 179, 213, 0.5); + .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/Modal/Modal.module.scss b/src/uxcore/components/Modal/Modal.module.scss index 0e022ae..4fa08fb 100644 --- a/src/uxcore/components/Modal/Modal.module.scss +++ b/src/uxcore/components/Modal/Modal.module.scss @@ -75,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; + } } } @@ -245,6 +261,14 @@ .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 { diff --git a/src/uxcore/components/_uxcp/CountryBiasMap/BiasPanel/BiasPanel.module.scss b/src/uxcore/components/_uxcp/CountryBiasMap/BiasPanel/BiasPanel.module.scss index e91e25b..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; @@ -296,7 +311,7 @@ } .Rationale { - color: rgba(218, 218, 218, 0.7); + color: #c8c8c8; } .HelpPopover { @@ -339,7 +354,12 @@ } .BiasShort { - color: rgba(218, 218, 218, 0.7); + color: #c8c8c8; + + scrollbar-color: rgba(127, 179, 213, 0.4) transparent; + &::-webkit-scrollbar-thumb { + background: rgba(127, 179, 213, 0.4); + } } .FooterMeta { diff --git a/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.module.scss b/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.module.scss index c1b4835..a045a9b 100644 --- a/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.module.scss +++ b/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.module.scss @@ -54,3 +54,26 @@ 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; + } +} 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/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; + } +} From d33b4ed272771be025ed637b65445c3aa05e08bc Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 11:06:15 +0000 Subject: [PATCH 17/25] fix(uxc-dark): unbreak UXCP dark page bg + allow flag CDN MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two real-world bugs Wolf hit on DEV that did not reproduce on prod: 1. UXCP appeared light-on-light in dark mode. The body.darkTheme rule was setting only `background-color: #1b1e26`, but the base body rule used `background: linear-gradient(...) #f9fafb` (shorthand: light gradient image PLUS color). The shorthand sets background-image as well, and the dark-theme override only changed the color — the LIGHT gradient kept rendering on top, so anywhere a child surface did not carry its own dark bg (like UXCP), the page leaked back to white. Use the full `background:` shorthand on body.darkTheme so the image is also cleared. 2. Country flags were blocked by CSP, not by an ad-blocker. The site's img-src directive in next.config.js whitelists the strapi hosts + Google + Discord but does NOT include flagcdn.com — so every flag request was refused by the browser with a CSP violation, which is why the same flagcdn URLs that load on prod failed here. Prod must have been built before the CSP tightened; DEV has the latest config. Add https://flagcdn.com to the img-src allowlist. Plus: LogIn modal provider buttons now have proper hover + active + keyboard-focus states (lift on hover, depress on click, brand-color darken on hover for each provider). And bump UXCP "(UXCP)" subtitle contrast from a 0.6-alpha gray to solid #a8a8a8. Co-Authored-By: Claude Opus 4.7 --- next.config.js | 2 +- src/components/LogIn/LogIn.module.scss | 51 +++++++++++++++++++ src/styles/globals.scss | 8 ++- .../layouts/UXCPLayout/UXCPLayout.module.scss | 5 +- 4 files changed, 61 insertions(+), 5 deletions(-) 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 c60ff63..43c1289 100644 --- a/src/components/LogIn/LogIn.module.scss +++ b/src/components/LogIn/LogIn.module.scss @@ -44,34 +44,85 @@ 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; + } } } } diff --git a/src/styles/globals.scss b/src/styles/globals.scss index 4b58c86..5473916 100644 --- a/src/styles/globals.scss +++ b/src/styles/globals.scss @@ -68,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 { diff --git a/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss b/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss index e758032..c7e0dd0 100644 --- a/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss +++ b/src/uxcore/layouts/UXCPLayout/UXCPLayout.module.scss @@ -15,9 +15,8 @@ color: #7fb3d5; } - .ShortName, - .Motto { - color: rgba(218, 218, 218, 0.6); + .ShortName { + color: #a8a8a8; } .Motto { From b7d8f1aa554789fd330993806d7b908a8e491ea2 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 11:23:47 +0000 Subject: [PATCH 18/25] fix(uxc-dark): close the rest of the UXCP dark-mode gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wolf hit four remaining light-on-dark surfaces on /uxcp: 1. Bias-row strip ("Add biases to Persona" list). BiasActionCell used a pale blue bg with a hard-white Add button — both blew out on dark. Re-skin the row (faint blue tint, light text, dark Add button with blue-accent hover) and invert the remove-icon. 2. SelectionView right panel. The "John Doe" persona-name header and the "Add bias to begin" placeholder were hard-coded mid-gray and washed out on the dark panel. Lift to readable light-on-dark values. 3. PersonaRelatedQuestions / PriorityFilter. The "Relevance level / All / High / Medium / Low" filter strip carried a hard white bg, and the pill buttons were white-on-light-border. Clear the strip bg and flip the inactive pill to a dark surface (active blue pill is kept). Also re-skin the question-list zebra striping and the placeholder. 4. Decision Table. Wrapper border, header row bg, alternating row bg, hover row bg, the gradient text-fade endpoint (was fading cells to white — now fades to the dark panel), the empty-cell color, the footer divider, the Save button neutral state, and the error-message callout were all light-theme leftovers. Plus give the TabHeader pink variant ("Decision Table example") a dark-mode counterpart so its wedge doesn't blow out against dark. Co-Authored-By: Claude Opus 4.7 --- .../BiasActionCell/BiasActionCell.module.scss | 32 ++++++++ .../DecisionTable/DecisionTable.module.scss | 81 +++++++++++++++++-- .../PersonaRelatedQuestions.module.scss | 21 +++++ .../PriorityFilter/PriorityFilter.module.scss | 26 ++++++ .../SelectionView/SelectionView.module.scss | 20 +++++ .../_uxcp/TabHeader/TabHeader.module.scss | 28 ++++++- 6 files changed, 202 insertions(+), 6 deletions(-) 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/DecisionTable/DecisionTable.module.scss b/src/uxcore/components/_uxcp/DecisionTable/DecisionTable.module.scss index 24e271b..7dfb520 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,77 @@ $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); + } + + .tableText { + // Original light-theme value fades the bottom of long cells to + // white; on dark that fade goes to the panel bg instead. + background: + linear-gradient(180deg, rgba(27, 30, 38, 0) 60.23%, #1b1e26 92.05%), + rgba(218, 218, 218, 0.95); + } + + .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/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/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/TabHeader/TabHeader.module.scss b/src/uxcore/components/_uxcp/TabHeader/TabHeader.module.scss index e98cb56..3740a3d 100644 --- a/src/uxcore/components/_uxcp/TabHeader/TabHeader.module.scss +++ b/src/uxcore/components/_uxcp/TabHeader/TabHeader.module.scss @@ -118,7 +118,9 @@ } } -// Dark theme: tab card. +// 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; @@ -133,4 +135,28 @@ 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; + } + } + } } From 74fea32cf4745a126ca0cbed95ab87eeed62a00d Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 11:32:30 +0000 Subject: [PATCH 19/25] fix(uxc-dark): unbreak Decision Table cells + pagination + stage chip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five things Wolf flagged on /uxcp dark: 1. Decision Table cells were rendering as blurred gray smudges. The light-theme rule uses background-clip:text + a gradient that fades long cells to white at the bottom; my dark counterpart kept the trick with a dark gradient, but short text ends up entirely inside the fade band and reads as a blurred rectangle. Disable the clip-text trick on dark and paint solid #dadada — text is readable, the trade-off is no bottom-fade on overflow but the cell already truncates with ellipsis-via-line-clamp so the fade was decorative. 2. Stage-indicator pill ("Released stage" etc.) rendered with a saturated brand bg and the label looked black. The DynamicButton sets color:#fff on the button but a downstream cascade was dimming the inner span. Force #fff on the button + inner Title in dark mode. 3. Pagination active/inactive states read inverted on dark — the "inactive" white pill looked more selected than the dark-blue "active" one. Flip the palette: inactive page sits on a dark surface with light text + faint border (hover lifts to blue accent), active page is the brighter #5396d3 blue pill. Also pad the click target and add a 4px gap between pages so it feels less cramped. 4. Suggested questions copy + link colors were leaving body text dim and the per-question anchor stuck at the dark-blue light-theme color on dark. Lift section copy to #dadada and links to #7fb3d5. 5. SuggestedQuestions empty state ("no data") was at #bfbfbf — too bright on dark, distracting. Drop to rgba(218,218,218,0.45). Co-Authored-By: Claude Opus 4.7 --- .../DecisionTable/DecisionTable.module.scss | 13 +++--- .../_uxcp/Pagination/Pagination.module.scss | 45 +++++++++++++++++++ .../DynamicButton/DynamicButton.module.scss | 12 +++++ .../Question/Question.module.scss | 9 ++++ .../SuggestedQuestions.module.scss | 15 +++++++ 5 files changed, 89 insertions(+), 5 deletions(-) diff --git a/src/uxcore/components/_uxcp/DecisionTable/DecisionTable.module.scss b/src/uxcore/components/_uxcp/DecisionTable/DecisionTable.module.scss index 7dfb520..5342989 100644 --- a/src/uxcore/components/_uxcp/DecisionTable/DecisionTable.module.scss +++ b/src/uxcore/components/_uxcp/DecisionTable/DecisionTable.module.scss @@ -257,12 +257,15 @@ $font-color: rgba(0, 0, 0, 0.65); 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 { - // Original light-theme value fades the bottom of long cells to - // white; on dark that fade goes to the panel bg instead. - background: - linear-gradient(180deg, rgba(27, 30, 38, 0) 60.23%, #1b1e26 92.05%), - rgba(218, 218, 218, 0.95); + background: none !important; + -webkit-text-fill-color: #dadada !important; + color: #dadada !important; } .tableTextEmpty { 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/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss index fd6a64b..b27b59f 100644 --- a/src/uxcore/components/_uxcp/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss +++ b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss @@ -108,3 +108,15 @@ } } } + +// Dark theme: the stage-indicator pill renders with a saturated brand +// color bg; in dark mode the inherited cascade can dim the white label. +// Force white text + slightly mute the bg so the label stays legible. +:global(body.darkTheme) .DynamicButton { + color: #fff !important; + + & .Titles, + & .Title { + color: #fff; + } +} 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); + } +} From 90da069929ac706dc1bda1a46493ae40dfb50529 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 11:34:14 +0000 Subject: [PATCH 20/25] fix(uxc-dark): make UXCP cycling subtitle word visible on dark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The "Choose your " subtitle on /uxcp cycles a word through several values (e.g. Rival / Customer / Audience / Persona). The component set color inline — `#337AB7` for the bold word ("Persona") and `#1A1A2E` for the rest. The near-black non-bold color was invisible on the dark page bg, so only "Persona" showed up while every other rotation looked blank. Move the colour + font-weight off the inline style and onto two CSS classes (CyclingWord / CyclingWordBold) so the dark-theme rule can override: non-bold lifts to #dadada, bold stays a light blue accent (#7fb3d5). Light theme keeps the original colors. Co-Authored-By: Claude Opus 4.7 --- .../CountryBiasMap/CountryBiasMap.module.scss | 20 +++++++++++++++++++ .../_uxcp/CountryBiasMap/CountryBiasMap.tsx | 12 ++++------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.module.scss b/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.module.scss index a045a9b..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 { @@ -76,4 +85,15 @@ .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..fe61af0 100644 --- a/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.tsx +++ b/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.tsx @@ -1,10 +1,8 @@ -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 { useRouter } from 'next/router'; +import { type FC, useEffect, useRef, useState } from 'react'; import BiasPanel from './BiasPanel'; import CountryList from './CountryList'; @@ -231,7 +229,7 @@ const CyclingSubtitleWord: FC = ({ return ( = ({ : phase === 'entering' ? 'translateY(6px)' : 'translateY(0)', - fontWeight: isBold ? 900 : 700, - color: isBold ? '#337AB7' : '#1A1A2E', }} > {word} From c9bf08733e34b30eec69bd7ca1f2d173389c3023 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 11:38:21 +0000 Subject: [PATCH 21/25] fix(uxc-dark): force stage-pill label visible + lift question row text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two follow-ups on /uxcp dark: 1. Stage-indicator pill ("Released stage" etc.). Previous attempt set color:#fff !important on .DynamicButton, but the cascade was still reaching the inner `.Title` text node through body.darkTheme's inherited #dadada via specificity. Apply the white override to every child node (.DynamicButton, .Titles, .Title) AND darken the saturated brand background ~20-25% on each Active state so the white label has real contrast (the coral and orange variants in particular were washing the label out at full saturation). 2. Persona-relevant questions rows. RelatedQuestion.Text was hard-coded #000 and the divider #e9e9e9 — both visible on light, both wrong on dark. Lift the question text to #dadada (#fff on hover), the bias number to the #7fb3d5 accent, and the row divider to #303338. Co-Authored-By: Claude Opus 4.7 --- .../DynamicButton/DynamicButton.module.scss | 33 ++++++++++++++----- .../RelatedQuestion.module.scss | 19 +++++++++++ 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/uxcore/components/_uxcp/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss index b27b59f..e306642 100644 --- a/src/uxcore/components/_uxcp/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss +++ b/src/uxcore/components/_uxcp/PersonaRelatedQuestions/DynamicButton/DynamicButton.module.scss @@ -109,14 +109,31 @@ } } -// Dark theme: the stage-indicator pill renders with a saturated brand -// color bg; in dark mode the inherited cascade can dim the white label. -// Force white text + slightly mute the bg so the label stays legible. -:global(body.darkTheme) .DynamicButton { - color: #fff !important; +// 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; +} - & .Titles, - & .Title { - color: #fff; +: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/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; + } + } + } +} From f35d16b7880fb3b9f9a523aa653c64d9f3f148aa Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 11:45:50 +0000 Subject: [PATCH 22/25] feat(uxc-dark): dark theme for the UX Core Awareness Test (/uxcat) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The /uxcat route stack had no dark-mode coverage — every visible surface across the four pages (landing, start-test, ongoing, test-result) was light-on-light against the dark page bg. Adds :global(body.darkTheme) blocks to: LAYOUTS - UXCatLayout — page bg clears, title flips to the #7fb3d5 accent, subtitle to a readable mid-gray. - OngoingLayout — question card chrome, question text, bias-info chip, answer rows + answer-prefix tag + selected-state border, skeleton placeholders, and the "btn icon" SVG fill in the footer. - TestResult — section header card, encourage / additionalInfo split panel + center divider, reading-list links, "next test available" date accent. - StartTestLayout — duration badge (desktop + mobile variant) and modal description text. - CalculatingResults — info card, loader spinner border, progress-item track. - CertificateLayout — footer button bar. COMPONENTS - UserProfile — name/title/level/awareness-points/user-statistic color shades, badge tooltip. - Result — passed/failed result cards, question pills, hover state, "failed" row tint, bobIcon container. - CompletionBar — bar surface, mainTitle, level text, base track, step number color, hover tooltip background. - AchievementContainer — grid container surface + mainTitle. - TestResultsAchievements — card surface + achievement title. - UXCatFooter — contact-us copy, mail/telegram links, motto accent. Brand-coloured accents (orange title in StartTest, the level-up gold gradients in CompletionBar, etc.) are left untouched — they read on dark already and changing them would lose intent. Co-Authored-By: Claude Opus 4.7 --- .../AchievementContainer.module.scss | 13 +++ .../CompletionBar/CompletionBar.module.scss | 51 ++++++++++-- .../components/Result/Result.module.scss | 39 +++++++++ .../TestResultsAchievements.module.scss | 12 +++ .../UXCatFooter/UXCatFooter.module.scss | 16 ++++ .../UserProfile/UserProfile.module.scss | 38 +++++++++ .../CalculatingResults.module.scss | 32 ++++++++ .../CertificateLayout.module.scss | 6 ++ .../OngoingLayout/OngoingLayout.module.scss | 81 +++++++++++++++++++ .../StartTestLayout.module.scss | 21 +++++ .../TestResult/TestResultLayout.module.scss | 48 +++++++++++ .../UXCatLayout/UXCatLayout.module.scss | 21 +++++ 12 files changed, 371 insertions(+), 7 deletions(-) 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/CompletionBar/CompletionBar.module.scss b/src/uxcore/components/CompletionBar/CompletionBar.module.scss index 122875c..1423fc6 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,43 @@ 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; + } + + .progressBarWrapper .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; + } +} 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/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/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/UserProfile/UserProfile.module.scss b/src/uxcore/components/UserProfile/UserProfile.module.scss index 1e130a5..abbbf68 100644 --- a/src/uxcore/components/UserProfile/UserProfile.module.scss +++ b/src/uxcore/components/UserProfile/UserProfile.module.scss @@ -226,3 +226,41 @@ margin: 0 12px; } } + +// Dark theme: user-profile header colors. +:global(body.darkTheme) .userProfile { + .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/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/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/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; + } + } +} From 8f1c6b9e9ac7843504dceeecee50672e917a1905 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 11:55:37 +0000 Subject: [PATCH 23/25] fix(uxc-dark): close the rest of the UXCP dark-mode gaps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wolf hit four remaining surfaces on /uxcat that were still light-on-dark: 1. Rules accordion body — the Accordion's existing dark theme lived under a `&.darkTheme` modifier that only kicks in when the component opts in by setting that class on itself. UXCat does not, so the body stayed white-on-light. Auto-apply via `:global(body.darkTheme)` so every accordion picks up dark without per-component plumbing — title bar, content surface, border, download-button accent. 2. UserProfile cover. The header card sets `background-image` inline from JS pointing at the light pastel coverImage.png (or a Strapi cover). Cover image kept rendering on top of the dark page bg with washed-out "Guest User" text on it. Override with `!important` to drop the image in dark mode and use flat #15181f (guest) / #1b1e26 (logged-in) surfaces. Plus a faint border + radius so the panel reads as a card. 3. Level-progression + achievements hover tooltips. Both used a `.tooltipContainer` class on react-tooltip, which portals the popup to . The previous dark-mode rules were descendant-scoped (`.progressBarWrapper .tooltipContainer`, `.userProfile .badgeTooltip`) so they never matched the portaled DOM. Move to flat `:global(body.darkTheme) .tooltipContainer` rules so the popovers actually flip to the dark surface. 4. "Show all achievements" outline button + every other OrangeOutline / BlueOutline button on dark — the variants kept their white bg. Switch to a transparent surface in dark theme so the brand border + text read on the dark page; keep the brand colors untouched. Co-Authored-By: Claude Opus 4.7 --- .../Accordion/Accordion.module.scss | 22 ++++++++++++++ .../Achievement/Achievement.module.scss | 23 ++++++++++++++- .../components/Button/Button.module.scss | 7 +++++ .../CompletionBar/CompletionBar.module.scss | 29 ++++++++++++------- .../UserProfile/UserProfile.module.scss | 15 +++++++++- 5 files changed, 84 insertions(+), 12 deletions(-) diff --git a/src/uxcore/components/Accordion/Accordion.module.scss b/src/uxcore/components/Accordion/Accordion.module.scss index b8df302..01dbe41 100644 --- a/src/uxcore/components/Accordion/Accordion.module.scss +++ b/src/uxcore/components/Accordion/Accordion.module.scss @@ -88,6 +88,28 @@ } } +// 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; + + & .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/Button/Button.module.scss b/src/uxcore/components/Button/Button.module.scss index c2833a6..b4560ff 100644 --- a/src/uxcore/components/Button/Button.module.scss +++ b/src/uxcore/components/Button/Button.module.scss @@ -166,3 +166,10 @@ 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 1423fc6..5d33608 100644 --- a/src/uxcore/components/CompletionBar/CompletionBar.module.scss +++ b/src/uxcore/components/CompletionBar/CompletionBar.module.scss @@ -548,16 +548,25 @@ .removeHover { background-color: #303338; } +} - .progressBarWrapper .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; +// 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/UserProfile/UserProfile.module.scss b/src/uxcore/components/UserProfile/UserProfile.module.scss index abbbf68..7a7294c 100644 --- a/src/uxcore/components/UserProfile/UserProfile.module.scss +++ b/src/uxcore/components/UserProfile/UserProfile.module.scss @@ -227,7 +227,20 @@ } } -// Dark theme: user-profile header colors. +// 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; + } +} + :global(body.darkTheme) .userProfile { .wrapper { .badgeWrapper .badgeTooltip { From 902577b5575356015e4fc7c7c8a7c1f0715cdebd Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 12:06:14 +0000 Subject: [PATCH 24/25] fix(uxc-dark): visible /uxcat accordion chevron, section titles, tooltip body MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three more dark-mode leaks on /uxcat: 1. Accordion chevron. The collapse/expand caret is rendered from /assets/icons/caret.svg by default and only swaps to caret-dark.svg when the parent passes isDarkTheme={true} — UXCatLayout does not. Result: dark caret on dark title bar, invisible. Invert the icon via CSS filter inside the body.darkTheme Accordion rule so it shows white regardless of which sprite shipped. 2. Section titles "Level Progression" / "Achievements" / etc. These render via the shared UXCatPageTitle component whose .pageTitle class was hard-coded to rgba(0,0,0,0.65) — invisible on dark. Lift to #dadada in dark mode. 3. Achievement / level hover tooltip body. The .tooltipContainer wrapper got a dark surface in the previous pass, but the text actually inside (UXCatTooltip — title, description, unlocked / statistics labels, points-to-next-level color) was still rendered in #000000a6 / #000000d9 / #5e62a7 / #a6a6a6. Lift each to the dark palette so the popover body reads. Co-Authored-By: Claude Opus 4.7 --- .../Accordion/Accordion.module.scss | 6 +++++ .../UXCatPageTitle/UXCatPageTitle.module.scss | 7 ++++++ .../UXCatTooltip/UXCatTooltip.module.scss | 24 +++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/src/uxcore/components/Accordion/Accordion.module.scss b/src/uxcore/components/Accordion/Accordion.module.scss index 01dbe41..b398e5e 100644 --- a/src/uxcore/components/Accordion/Accordion.module.scss +++ b/src/uxcore/components/Accordion/Accordion.module.scss @@ -98,6 +98,12 @@ 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; } 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; + } +} From 415dbd0b9a80acb4188c514d6d8c5cbbd5922e14 Mon Sep 17 00:00:00 2001 From: manager Date: Fri, 15 May 2026 13:05:45 +0000 Subject: [PATCH 25/25] fix(uxc-dark): address Mary's PR #114 review Per Mary's review: - Remove duplicate @font-face block from src/uxcore/styles/globals.scss (lines 90-258 in the previous state). The same declarations were consolidated into src/styles/globals.scss in 2fa2586; the uxcore-side copy was left behind. The file is not imported anywhere, but the cleanup eliminates the duplicate-declaration risk if it ever becomes loaded. Keep body/html resets, scrollbar styles, dark-theme body rules, and the mobile background-size override. - Fix stale-closure in modal Escape listeners. CustomModal.tsx and the uxcore Modal.tsx registered their keydown handlers in a useEffect whose handleClose captured the initial `closing` state, so a second Escape during the 180ms fade-out could re-enter the close cycle. Read handleClose through a ref so the `if (closing) return` guard always sees the current value. - Merge the two split :global(body.darkTheme) .userProfile blocks in UserProfile.module.scss into one. No runtime change. - Use classnames (`cn`) for the CountryBiasMap CyclingWord className instead of an inline template literal with a ternary. Co-Authored-By: Claude Opus 4.7 --- .../components/CustomModal/CustomModal.tsx | 10 +- src/uxcore/components/Modal/Modal.tsx | 9 +- .../UserProfile/UserProfile.module.scss | 2 - .../_uxcp/CountryBiasMap/CountryBiasMap.tsx | 3 +- src/uxcore/styles/globals.scss | 173 +----------------- 5 files changed, 20 insertions(+), 177 deletions(-) diff --git a/src/uxcore/components/CustomModal/CustomModal.tsx b/src/uxcore/components/CustomModal/CustomModal.tsx index 1642d0e..dbb94b9 100644 --- a/src/uxcore/components/CustomModal/CustomModal.tsx +++ b/src/uxcore/components/CustomModal/CustomModal.tsx @@ -3,7 +3,7 @@ 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, useState } from 'react'; +import { FC, KeyboardEvent, useEffect, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { AddQuestion, ContactUs } from './contentTypes'; @@ -39,9 +39,15 @@ const CustomModal: FC = ({ }, 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') handleClose(); + if (e.key === 'Escape') handleCloseRef.current(); }; // @ts-ignore diff --git a/src/uxcore/components/Modal/Modal.tsx b/src/uxcore/components/Modal/Modal.tsx index 17f087c..577cc73 100644 --- a/src/uxcore/components/Modal/Modal.tsx +++ b/src/uxcore/components/Modal/Modal.tsx @@ -4,6 +4,7 @@ import React, { KeyboardEvent, ReactNode, useEffect, + useRef, useState, } from 'react'; import { createPortal } from 'react-dom'; @@ -68,6 +69,12 @@ const Modal: FC = ({ }, 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 @@ -76,7 +83,7 @@ const Modal: FC = ({ const handleKeyDown = (e: KeyboardEvent) => { if (!close) { - if (e.key === 'Escape') handleClose(); + if (e.key === 'Escape') handleCloseRef.current(); } }; diff --git a/src/uxcore/components/UserProfile/UserProfile.module.scss b/src/uxcore/components/UserProfile/UserProfile.module.scss index 7a7294c..8779146 100644 --- a/src/uxcore/components/UserProfile/UserProfile.module.scss +++ b/src/uxcore/components/UserProfile/UserProfile.module.scss @@ -239,9 +239,7 @@ &.guestMode { background-color: #15181f !important; } -} -:global(body.darkTheme) .userProfile { .wrapper { .badgeWrapper .badgeTooltip { background: #1b1e26; diff --git a/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.tsx b/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.tsx index fe61af0..a6dc7de 100644 --- a/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.tsx +++ b/src/uxcore/components/_uxcp/CountryBiasMap/CountryBiasMap.tsx @@ -1,6 +1,7 @@ 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 cn from 'classnames'; import { useRouter } from 'next/router'; import { type FC, useEffect, useRef, useState } from 'react'; @@ -229,7 +230,7 @@ const CyclingSubtitleWord: FC = ({ return (