fix: temporarily lock tasker dark theme

This commit is contained in:
DCCONSTRUCTIONS 2026-05-23 10:32:04 +03:00
parent 3f86b73d5c
commit 6e74964853
10 changed files with 126 additions and 66 deletions

View File

@ -10,7 +10,7 @@ import { Links, Meta, Outlet, Scripts } from "react-router";
import type { LinksFunction } from "react-router"; import type { LinksFunction } from "react-router";
import { ThemeProvider } from "next-themes"; import { ThemeProvider } from "next-themes";
// plane imports // plane imports
import { SITE_DESCRIPTION, SITE_NAME } from "@plane/constants"; import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, SITE_DESCRIPTION, SITE_NAME, THEMES } from "@plane/constants";
import { cn } from "@plane/utils"; import { cn } from "@plane/utils";
// types // types
// assets // assets
@ -24,9 +24,12 @@ import { NodeDCSessionSync } from "@/components/auth-screens/nodedc-session-sync
import { CustomErrorComponent } from "./error"; import { CustomErrorComponent } from "./error";
import { AppProvider } from "./provider"; import { AppProvider } from "./provider";
// fonts // fonts
// oxlint-disable-next-line import/no-unassigned-import
import "@fontsource-variable/inter"; import "@fontsource-variable/inter";
import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url"; import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url";
// oxlint-disable-next-line import/no-unassigned-import
import "@fontsource/material-symbols-rounded"; import "@fontsource/material-symbols-rounded";
// oxlint-disable-next-line import/no-unassigned-import
import "@fontsource/ibm-plex-mono"; import "@fontsource/ibm-plex-mono";
const APP_TITLE = "NODE.DC | Self-hosted task management workspace."; const APP_TITLE = "NODE.DC | Self-hosted task management workspace.";
@ -110,15 +113,20 @@ export function Layout({ children }: { children: ReactNode }) {
const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0"); const isSessionRecorderEnabled = parseInt(process.env.VITE_ENABLE_SESSION_RECORDER || "0");
return ( return (
<html lang="en" suppressHydrationWarning style={designConfigStyle}> <html
lang="en"
data-theme={NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : undefined}
suppressHydrationWarning
style={designConfigStyle}
>
<head> <head>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#fff" /> <meta name="theme-color" content={NODEDC_THEME_LOCK_ENABLED ? "#111113" : "#fff"} />
{/* Meta info for PWA */} {/* Meta info for PWA */}
<meta name="application-name" content="NODE.DC" /> <meta name="application-name" content="NODE.DC" />
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" /> <meta name="apple-mobile-web-app-status-bar-style" content={NODEDC_THEME_LOCK_ENABLED ? "black" : "default"} />
<meta name="apple-mobile-web-app-title" content={SITE_NAME} /> <meta name="apple-mobile-web-app-title" content={SITE_NAME} />
<meta name="format-detection" content="telephone=no" /> <meta name="format-detection" content="telephone=no" />
<meta name="mobile-web-app-capable" content="yes" /> <meta name="mobile-web-app-capable" content="yes" />
@ -128,7 +136,14 @@ export function Layout({ children }: { children: ReactNode }) {
<body suppressHydrationWarning> <body suppressHydrationWarning>
<div id="context-menu-portal" /> <div id="context-menu-portal" />
<div id="editor-portal" /> <div id="editor-portal" />
<ThemeProvider themes={["light", "dark", "light-contrast", "dark-contrast", "custom"]} defaultTheme="system"> <ThemeProvider
attribute="data-theme"
themes={THEMES}
defaultTheme={NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : "system"}
forcedTheme={NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : undefined}
enableSystem={!NODEDC_THEME_LOCK_ENABLED}
disableTransitionOnChange
>
{children} {children}
</ThemeProvider> </ThemeProvider>
<Scripts /> <Scripts />

View File

@ -9,7 +9,7 @@ import { observer } from "mobx-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// plane imports // plane imports
import type { I_THEME_OPTION } from "@plane/constants"; import type { I_THEME_OPTION } from "@plane/constants";
import { THEME_OPTIONS } from "@plane/constants"; import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { setPromiseToast } from "@plane/propel/toast"; import { setPromiseToast } from "@plane/propel/toast";
import { applyCustomTheme } from "@plane/utils"; import { applyCustomTheme } from "@plane/utils";
@ -35,18 +35,20 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
const { t } = useTranslation(); const { t } = useTranslation();
// derived values // derived values
const currentTheme = useMemo(() => { const currentTheme = useMemo(() => {
const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme); const themeValue = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : userProfile?.theme?.theme;
return userThemeOption || null; return THEME_OPTIONS.find((themeOption) => themeOption.value === themeValue) || null;
}, [userProfile?.theme?.theme]); }, [userProfile?.theme?.theme]);
const handleThemeChange = useCallback( const handleThemeChange = useCallback(
async (themeOption: I_THEME_OPTION) => { async (themeOption: I_THEME_OPTION) => {
try { try {
setTheme(themeOption.value); const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : themeOption.value;
setTheme(nextTheme);
// If switching to custom theme and user has saved custom colors, apply them immediately // If switching to custom theme and user has saved custom colors, apply them immediately
if ( if (
themeOption.value === "custom" && !NODEDC_THEME_LOCK_ENABLED &&
nextTheme === "custom" &&
userProfile?.theme?.primary && userProfile?.theme?.primary &&
userProfile?.theme?.background && userProfile?.theme?.background &&
userProfile?.theme?.darkPalette !== undefined userProfile?.theme?.darkPalette !== undefined
@ -58,7 +60,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
); );
} }
const updatePromise = updateUserTheme({ theme: themeOption.value }); const updatePromise = updateUserTheme({ theme: nextTheme });
setPromiseToast(updatePromise, { setPromiseToast(updatePromise, {
loading: "Updating theme...", loading: "Updating theme...",
success: { success: {
@ -96,7 +98,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
/> />
} }
/> />
{userProfile.theme?.theme === "custom" && <CustomThemeSelector />} {!NODEDC_THEME_LOCK_ENABLED && userProfile.theme?.theme === "custom" && <CustomThemeSelector />}
</> </>
); );
}); });

View File

@ -9,7 +9,7 @@ import { observer } from "mobx-react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
// plane imports // plane imports
import type { I_THEME_OPTION } from "@plane/constants"; import type { I_THEME_OPTION } from "@plane/constants";
import { THEME_OPTIONS } from "@plane/constants"; import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED, THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { setPromiseToast } from "@plane/propel/toast"; import { setPromiseToast } from "@plane/propel/toast";
// components // components
@ -34,15 +34,16 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
const { t } = useTranslation(); const { t } = useTranslation();
// derived values // derived values
const currentTheme = useMemo(() => { const currentTheme = useMemo(() => {
const userThemeOption = THEME_OPTIONS.find((t) => t.value === userProfile?.theme?.theme); const themeValue = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : userProfile?.theme?.theme;
return userThemeOption || null; return THEME_OPTIONS.find((themeOption) => themeOption.value === themeValue) || null;
}, [userProfile?.theme?.theme]); }, [userProfile?.theme?.theme]);
const handleThemeChange = useCallback( const handleThemeChange = useCallback(
(themeOption: I_THEME_OPTION) => { (themeOption: I_THEME_OPTION) => {
try { try {
setTheme(themeOption.value); const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : themeOption.value;
const updatePromise = updateUserTheme({ theme: themeOption.value }); setTheme(nextTheme);
const updatePromise = updateUserTheme({ theme: nextTheme });
setPromiseToast(updatePromise, { setPromiseToast(updatePromise, {
loading: "Updating theme...", loading: "Updating theme...",
success: { success: {
@ -58,7 +59,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
console.error("Error updating theme:", error); console.error("Error updating theme:", error);
} }
}, },
[updateUserTheme] [setTheme, updateUserTheme]
); );
if (!userProfile) return null; if (!userProfile) return null;
@ -70,7 +71,7 @@ export const ThemeSwitcher = observer(function ThemeSwitcher(props: {
description={t(props.option.description)} description={t(props.option.description)}
control={<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />} control={<ThemeSwitch value={currentTheme} onChange={handleThemeChange} />}
/> />
{userProfile.theme?.theme === "custom" && <CustomThemeSelector />} {!NODEDC_THEME_LOCK_ENABLED && userProfile.theme?.theme === "custom" && <CustomThemeSelector />}
</> </>
); );
}); });

View File

@ -6,7 +6,7 @@
// plane imports // plane imports
import type { I_THEME_OPTION } from "@plane/constants"; import type { I_THEME_OPTION } from "@plane/constants";
import { THEME_OPTIONS } from "@plane/constants"; import { NODEDC_ACTIVE_THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// constants // constants
import { SelectionDropdown } from "@/components/common/selection-dropdown"; import { SelectionDropdown } from "@/components/common/selection-dropdown";
@ -17,11 +17,6 @@ type Props = {
onChange: (value: I_THEME_OPTION) => void; onChange: (value: I_THEME_OPTION) => void;
}; };
export function ThemeSwitch(props: Props) {
const { value, onChange } = props;
// translation
const { t } = useTranslation();
const renderThemeSwatch = (themeOption: I_THEME_OPTION) => ( const renderThemeSwatch = (themeOption: I_THEME_OPTION) => (
<div <div
className="relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border-1" className="relative flex h-4 w-4 rotate-45 transform items-center justify-center rounded-full border-1"
@ -45,6 +40,11 @@ export function ThemeSwitch(props: Props) {
</div> </div>
); );
export function ThemeSwitch(props: Props) {
const { value, onChange } = props;
// translation
const { t } = useTranslation();
return ( return (
<SelectionDropdown <SelectionDropdown
placement="bottom-end" placement="bottom-end"
@ -59,7 +59,7 @@ export function ThemeSwitch(props: Props) {
) )
} }
menuButtonWrapperClassName="flex w-full items-center justify-between rounded-full border border-subtle-1 px-3 py-2 text-13" menuButtonWrapperClassName="flex w-full items-center justify-between rounded-full border border-subtle-1 px-3 py-2 text-13"
options={THEME_OPTIONS.map((themeOption) => ({ options={NODEDC_ACTIVE_THEME_OPTIONS.map((themeOption) => ({
key: themeOption.value, key: themeOption.value,
isChecked: value?.value === themeOption.value, isChecked: value?.value === themeOption.value,
onClick: () => onChange(themeOption), onClick: () => onChange(themeOption),

View File

@ -9,6 +9,7 @@ import React, { useState } from "react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { ArrowRightLeft } from "lucide-react"; import { ArrowRightLeft } from "lucide-react";
import { Dialog, Transition } from "@headlessui/react"; import { Dialog, Transition } from "@headlessui/react";
import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants";
// ui // ui
import { Button } from "@plane/propel/button"; import { Button } from "@plane/propel/button";
import { TOAST_TYPE, setToast } from "@plane/propel/toast"; import { TOAST_TYPE, setToast } from "@plane/propel/toast";
@ -42,9 +43,10 @@ export function SwitchAccountModal(props: Props) {
await signOut() await signOut()
.then(() => { .then(() => {
setTheme("system"); setTheme(NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : "system");
router.push("/"); router.push("/");
handleClose(); handleClose();
return undefined;
}) })
.catch(() => .catch(() =>
setToast({ setToast({

View File

@ -8,6 +8,7 @@ import { useCallback } from "react";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { Calendar, Earth, Languages, Palette } from "lucide-react"; import { Calendar, Earth, Languages, Palette } from "lucide-react";
// plane imports // plane imports
import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
import { setToast, TOAST_TYPE } from "@plane/propel/toast"; import { setToast, TOAST_TYPE } from "@plane/propel/toast";
import type { EStartOfTheWeek, TUserProfile } from "@plane/types"; import type { EStartOfTheWeek, TUserProfile } from "@plane/types";
@ -29,8 +30,9 @@ export const usePowerKPreferencesCommands = (): TPowerKCommandConfig[] => {
const handleUpdateTheme = useCallback( const handleUpdateTheme = useCallback(
async (newTheme: string) => { async (newTheme: string) => {
setTheme(newTheme); const nextTheme = NODEDC_THEME_LOCK_ENABLED ? NODEDC_FORCED_THEME : newTheme;
return updateUserTheme({ theme: newTheme }) setTheme(nextTheme);
return updateUserTheme({ theme: nextTheme })
.then(() => { .then(() => {
setToast({ setToast({
type: TOAST_TYPE.SUCCESS, type: TOAST_TYPE.SUCCESS,

View File

@ -8,7 +8,7 @@ import React, { useEffect, useState } from "react";
import { Command } from "cmdk"; import { Command } from "cmdk";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
// plane imports // plane imports
import { THEME_OPTIONS } from "@plane/constants"; import { NODEDC_ACTIVE_THEME_OPTIONS } from "@plane/constants";
import { useTranslation } from "@plane/i18n"; import { useTranslation } from "@plane/i18n";
// local imports // local imports
import { PowerKModalCommandItem } from "../../modal/command-item"; import { PowerKModalCommandItem } from "../../modal/command-item";
@ -33,7 +33,7 @@ export const PowerKPreferencesThemesMenu = observer(function PowerKPreferencesTh
return ( return (
<Command.Group> <Command.Group>
{THEME_OPTIONS.map((theme) => ( {NODEDC_ACTIVE_THEME_OPTIONS.map((theme) => (
<PowerKModalCommandItem key={theme.value} onSelect={() => onSelect(theme.value)} label={t(theme.i18n_label)} /> <PowerKModalCommandItem key={theme.value} onSelect={() => onSelect(theme.value)} label={t(theme.i18n_label)} />
))} ))}
</Command.Group> </Command.Group>

View File

@ -137,7 +137,10 @@ export const CodexAgentApiSettingsContent = observer(function CodexAgentApiSetti
}) })
) )
); );
const workspaceProjectGroups = projectAccessPayload?.workspaces ?? []; const workspaceProjectGroups = useMemo(
() => projectAccessPayload?.workspaces ?? [],
[projectAccessPayload?.workspaces]
);
const projectOptions = useMemo(() => flattenProjectAccessOptions(workspaceProjectGroups), [workspaceProjectGroups]); const projectOptions = useMemo(() => flattenProjectAccessOptions(workspaceProjectGroups), [workspaceProjectGroups]);
const effectiveSelectedProjectKey = selectedProjectKey || projectOptions[0]?.key || ""; const effectiveSelectedProjectKey = selectedProjectKey || projectOptions[0]?.key || "";
const setupCards = useMemo( const setupCards = useMemo(
@ -735,6 +738,10 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
В строке <code>Authorization</code> замените только <code>ВАШ_УНИКАЛЬНЫЙ_ТОКЕН</code> на значение из{" "} В строке <code>Authorization</code> замените только <code>ВАШ_УНИКАЛЬНЫЙ_ТОКЕН</code> на значение из{" "}
<code>Agent token</code>. <code>Bearer</code> и <code>ndcag_</code> оставьте как есть. <code>Agent token</code>. <code>Bearer</code> и <code>ndcag_</code> оставьте как есть.
</p> </p>
<p className="mt-3">
Не используйте <code>bearer_token_env_var</code>, если переменная окружения не добавлена в macOS launchd:
Codex Desktop не увидит обычный <code>export</code> из shell-профиля.
</p>
</div> </div>
<div className="nodedc-settings-input min-h-36 px-4 py-4 text-13 leading-5 text-secondary"> <div className="nodedc-settings-input min-h-36 px-4 py-4 text-13 leading-5 text-secondary">
@ -743,6 +750,10 @@ function CodexConnectionGuide(props: TCodexConnectionGuideProps) {
Сохраните <code>config.toml</code> и перезапустите локальный Codex. После старта попросите Codex проверить Сохраните <code>config.toml</code> и перезапустите локальный Codex. После старта попросите Codex проверить
доступные проекты через <code>tasker_list_projects</code>. доступные проекты через <code>tasker_list_projects</code>.
</p> </p>
<p className="mt-3">
Если токен или endpoint временно недоступны, чат Codex все равно откроется, но MCP tools появятся только
после исправления подключения.
</p>
<p className="mt-3">Правила работы и grants Codex получит сам через MCP по токену.</p> <p className="mt-3">Правила работы и grants Codex получит сам через MCP по токену.</p>
</div> </div>
</div> </div>
@ -775,13 +786,13 @@ function buildCodexConfigSnippet(endpoint: string): string {
return `[mcp_servers.${CODEX_MCP_SERVER_NAME}] return `[mcp_servers.${CODEX_MCP_SERVER_NAME}]
url = "${endpoint}" url = "${endpoint}"
enabled = true enabled = true
required = true required = false
startup_timeout_sec = 20 startup_timeout_sec = 20
tool_timeout_sec = 60 tool_timeout_sec = 60
[mcp_servers.${CODEX_MCP_SERVER_NAME}.headers] [mcp_servers.${CODEX_MCP_SERVER_NAME}.headers]
Authorization = "Bearer ndcag_ВАШ_УНИКАЛЬНЫЙ_ТОКЕН" Authorization = "Bearer ndcag_ВАШ_УНИКАЛЬНЫЙ_ТОКЕН"
Accept = "application/json, text/event-stream" Accept = "application/json"
"MCP-Protocol-Version" = "2025-06-18"`; "MCP-Protocol-Version" = "2025-06-18"`;
} }
@ -995,6 +1006,7 @@ function mergeTokens(primaryTokens: TCodexAgentToken[], secondaryTokens: TCodexA
const tokensById = new Map<string, TCodexAgentToken>(); const tokensById = new Map<string, TCodexAgentToken>();
for (const token of primaryTokens) tokensById.set(token.id, token); for (const token of primaryTokens) tokensById.set(token.id, token);
for (const token of secondaryTokens) tokensById.set(token.id, tokensById.get(token.id) ?? token); for (const token of secondaryTokens) tokensById.set(token.id, tokensById.get(token.id) ?? token);
// oxlint-disable-next-line unicorn/no-array-sort
return Array.from(tokensById.values()).sort( return Array.from(tokensById.values()).sort(
(leftToken, rightToken) => new Date(rightToken.created_at).getTime() - new Date(leftToken.created_at).getTime() (leftToken, rightToken) => new Date(rightToken.created_at).getTime() - new Date(leftToken.created_at).getTime()
); );
@ -1072,13 +1084,16 @@ function buildProjectGrantPayload(grantKeys: string[]): { workspace_slug: string
} }
function getGrantedProjectKeys(grants: TCodexAgentGrant[]): string[] { function getGrantedProjectKeys(grants: TCodexAgentGrant[]): string[] {
return [ const grantedProjectKeys = [
...new Set( ...new Set(
grants grants
.filter((grant) => grant.project_id) .filter((grant) => grant.project_id)
.map((grant) => buildProjectGrantKey(grant.workspace_slug, String(grant.project_id))) .map((grant) => buildProjectGrantKey(grant.workspace_slug, String(grant.project_id)))
), ),
].sort(); ];
// oxlint-disable-next-line unicorn/no-array-sort
return grantedProjectKeys.sort();
} }
type TMergeAgentGrantsOptions = { type TMergeAgentGrantsOptions = {

View File

@ -9,6 +9,7 @@ import { useEffect, useRef } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { useTheme } from "next-themes"; import { useTheme } from "next-themes";
import { NODEDC_FORCED_THEME, NODEDC_THEME_LOCK_ENABLED } from "@plane/constants";
import type { TLanguage } from "@plane/i18n"; import type { TLanguage } from "@plane/i18n";
import { DEFAULT_LANGUAGE, useTranslation } from "@plane/i18n"; import { DEFAULT_LANGUAGE, useTranslation } from "@plane/i18n";
// helpers // helpers
@ -57,16 +58,13 @@ function StoreWrapper(props: TStoreWrapper) {
*/ */
useEffect(() => { useEffect(() => {
const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed"); const localValue = localStorage && localStorage.getItem("app_sidebar_collapsed");
const localBoolValue = localValue ? (localValue === "true" ? true : false) : false; const localBoolValue = localValue === "true";
if (localValue && sidebarCollapsed === undefined) toggleSidebar(localBoolValue); if (localValue && sidebarCollapsed === undefined) toggleSidebar(localBoolValue);
}, [sidebarCollapsed, setTheme, toggleSidebar]); }, [sidebarCollapsed, setTheme, toggleSidebar]);
/** /**
* Effect 1: Initial theme sync from server (one-time only) * Temporary NODE.DC theme lock: keep the operational dark palette active
* * until the light theme gets a full pass.
* This effect runs ONCE per user session to load theme from server.
* After initial load, all theme changes are localStorage-driven (next-themes).
* This prevents a feedback loop where server updates trigger UI updates in a cycle.
*/ */
useEffect(() => { useEffect(() => {
const userId = userProfile?.id; const userId = userProfile?.id;
@ -79,6 +77,16 @@ function StoreWrapper(props: TStoreWrapper) {
currentUserIdRef.current = userId; currentUserIdRef.current = userId;
} }
if (NODEDC_THEME_LOCK_ENABLED) {
if (!userProfile?.id || hasInitializedThemeRef.current) {
return;
}
setTheme(NODEDC_FORCED_THEME);
hasInitializedThemeRef.current = true;
return;
}
// Only initialize theme from server on FIRST load for this user // Only initialize theme from server on FIRST load for this user
if (!userProfile?.theme?.theme || hasInitializedThemeRef.current) { if (!userProfile?.theme?.theme || hasInitializedThemeRef.current) {
return; // Skip if already initialized or no profile data return; // Skip if already initialized or no profile data
@ -89,7 +97,7 @@ function StoreWrapper(props: TStoreWrapper) {
// Mark as initialized - prevents future syncs from server // Mark as initialized - prevents future syncs from server
hasInitializedThemeRef.current = true; hasInitializedThemeRef.current = true;
}, [userProfile?.theme?.theme, setTheme]); }, [userProfile?.id, userProfile?.theme?.theme, setTheme]);
/** /**
* Effect 2: Custom theme CSS application (runs on every change) * Effect 2: Custom theme CSS application (runs on every change)
@ -98,12 +106,21 @@ function StoreWrapper(props: TStoreWrapper) {
* the theme changes. It runs independently of the initial sync effect. * the theme changes. It runs independently of the initial sync effect.
*/ */
useEffect(() => { useEffect(() => {
if (!userProfile?.theme?.theme) return; if (NODEDC_THEME_LOCK_ENABLED) {
clearCustomTheme();
return;
}
if (!userProfile?.theme?.theme) {
return;
}
const currentTheme = userProfile?.theme?.theme; const currentTheme = userProfile?.theme?.theme;
const previousTheme = previousThemeRef.current; const previousTheme = previousThemeRef.current;
const themeData = userProfile?.theme; const themeData = userProfile?.theme;
if (!currentTheme) return;
// Apply custom theme if current theme is custom // Apply custom theme if current theme is custom
if (currentTheme === "custom" && themeData.primary && themeData.background && themeData.darkPalette !== undefined) { if (currentTheme === "custom" && themeData.primary && themeData.background && themeData.darkPalette !== undefined) {
applyCustomTheme(themeData.primary, themeData.background, themeData.darkPalette ? "dark" : "light"); applyCustomTheme(themeData.primary, themeData.background, themeData.darkPalette ? "dark" : "light");
@ -116,7 +133,7 @@ function StoreWrapper(props: TStoreWrapper) {
// Update previous theme for next comparison // Update previous theme for next comparison
previousThemeRef.current = currentTheme; previousThemeRef.current = currentTheme;
}, [userProfile?.theme]); }, [userProfile?.id, userProfile?.theme]);
useEffect(() => { useEffect(() => {
if (!userProfile?.id) return; if (!userProfile?.id) return;
@ -138,7 +155,7 @@ function StoreWrapper(props: TStoreWrapper) {
useEffect(() => { useEffect(() => {
if (!userProfile?.id) return; if (!userProfile?.id) return;
changeLanguage((userProfile?.language as TLanguage) || DEFAULT_LANGUAGE); changeLanguage((userProfile?.language as TLanguage) || DEFAULT_LANGUAGE);
}, [userProfile?.language, changeLanguage]); }, [userProfile?.id, userProfile?.language, changeLanguage]);
useEffect(() => { useEffect(() => {
if (!params) return; if (!params) return;

View File

@ -4,7 +4,9 @@
* See the LICENSE file for details. * See the LICENSE file for details.
*/ */
export const THEMES = ["light", "dark", "light-contrast", "dark-contrast", "custom"]; export const NODEDC_FORCED_THEME = "dark";
export const NODEDC_THEME_LOCK_ENABLED = true;
export const THEMES = ["light", NODEDC_FORCED_THEME, "light-contrast", "dark-contrast", "custom"];
export interface I_THEME_OPTION { export interface I_THEME_OPTION {
key: string; key: string;
@ -43,7 +45,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
}, },
{ {
key: "dark", key: "dark",
value: "dark", value: NODEDC_FORCED_THEME,
i18n_label: "Dark", i18n_label: "Dark",
type: "dark", type: "dark",
icon: { icon: {
@ -86,3 +88,7 @@ export const THEME_OPTIONS: I_THEME_OPTION[] = [
}, },
}, },
]; ];
export const NODEDC_ACTIVE_THEME_OPTIONS = NODEDC_THEME_LOCK_ENABLED
? THEME_OPTIONS.filter((themeOption) => themeOption.value === NODEDC_FORCED_THEME)
: THEME_OPTIONS;