UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: локальный акцент и аналитика

This commit is contained in:
DCCONSTRUCTIONS 2026-04-29 19:18:55 +03:00
parent aa348d5d64
commit a7e0a63426
17 changed files with 493 additions and 14 deletions

View File

@ -78,6 +78,14 @@ const designConfigStyle = {
"--bg-accent-primary": formatCssRgb(accentRgb),
"--bg-accent-primary-hover": formatCssRgb(accentHoverRgb),
"--bg-accent-primary-active": formatCssRgb(accentActiveRgb),
"--txt-accent-primary": formatCssRgb(accentRgb),
"--txt-accent-secondary": formatCssRgb(blendRgb(accentRgb, 0, 0.25)),
"--txt-icon-accent-primary": formatCssRgb(accentRgb),
"--text-color-accent-primary": formatCssRgb(accentRgb),
"--text-color-accent-secondary": formatCssRgb(blendRgb(accentRgb, 0, 0.25)),
"--text-color-icon-accent-primary": formatCssRgb(accentRgb),
"--stroke-accent-primary": formatCssRgb(accentRgb),
"--fill-accent-primary": formatCssRgb(accentRgb),
"--txt-on-color": formatCssRgb(onAccentRgb),
"--txt-icon-on-color": formatCssRgb(onAccentRgb),
} as CSSProperties;

View File

@ -129,7 +129,7 @@ export const ExternalContoursBoardItem = observer(function ExternalContoursBoard
? projectStateIds.map((stateId) => getStateById(stateId)).filter((state): state is IState => !!state)
: sourceStateIds.map((stateId) => sourceStateMap[stateId]).filter((state): state is IState => !!state);
const foregroundClasses = isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]" : "text-white";
const subtleTextClasses = isActive ? "text-[#2F4721]" : "text-[#B3B3B8]";
const subtleTextClasses = isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/70" : "text-[#B3B3B8]";
const pillBackgroundClasses = isActive
? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]"
: "bg-[rgb(var(--nodedc-card-passive-rgb))] text-white";

View File

@ -23,6 +23,8 @@ import AnalyticsSectionWrapper from "../analytics-section-wrapper";
import { ChartLoader } from "../loaders";
const analyticsService = new AnalyticsService();
const NODEDC_ANALYTICS_ACCENT = "rgb(var(--nodedc-accent-rgb))";
const NODEDC_ANALYTICS_ACCENT_SOFT = "rgb(var(--nodedc-accent-rgb) / 0.18)";
const CreatedVsResolved = observer(function CreatedVsResolved() {
const {
selectedDuration,
@ -66,12 +68,12 @@ const CreatedVsResolved = observer(function CreatedVsResolved() {
{
key: "completed_issues",
label: "Решено",
fill: "rgba(195, 255, 102, 0.18)",
fill: NODEDC_ANALYTICS_ACCENT_SOFT,
fillOpacity: 1,
stackId: "bar-one",
showDot: false,
smoothCurves: true,
strokeColor: "#C3FF66",
strokeColor: NODEDC_ANALYTICS_ACCENT,
strokeOpacity: 1,
},
{

View File

@ -15,7 +15,7 @@ interface ParamsProps {
}
export const NODEDC_ANALYTICS_COLORS = [
"#C3FF66",
"rgb(var(--nodedc-accent-rgb))",
"#F5F7FB",
"#7C7F85",
"#050505",
@ -29,12 +29,12 @@ const STATE_GROUP_COLORS: Record<TStateGroups, string> = {
backlog: "#050505",
unstarted: "#7C7F85",
started: "#FFFFFF",
completed: "#C3FF66",
completed: "rgb(var(--nodedc-accent-rgb))",
cancelled: "#050505",
};
const PRIORITY_COLORS: Record<string, string> = {
urgent: "#C3FF66",
urgent: "rgb(var(--nodedc-accent-rgb))",
high: "#F5F7FB",
medium: "#7C7F85",
low: "#2A2B2E",

View File

@ -206,7 +206,12 @@ export const InternalContourKanbanCard = observer(function InternalContourKanban
<div className="min-w-0 pr-[162px] pl-[58px] pt-1">
<div className="truncate text-body-sm-medium leading-5">{creatorName}</div>
<div className={cn("truncate text-[10px] leading-3.5 font-medium", isActive ? "text-[#2F4721]" : "text-[#B3B3B8]")}>
<div
className={cn(
"truncate text-[10px] leading-3.5 font-medium",
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/70" : "text-[#B3B3B8]"
)}
>
{sourceContourName}
</div>
</div>

View File

@ -30,7 +30,7 @@ export const getNodedcWorkItemCardAppearance = (isActive: boolean) => ({
foregroundClasses: isActive
? "text-[rgb(var(--nodedc-on-card-active-rgb))]"
: "text-[rgb(var(--nodedc-on-card-passive-rgb))]",
subtleTextClasses: isActive ? "text-[#2F4721]" : "text-[#B3B3B8]",
subtleTextClasses: isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/70" : "text-[#B3B3B8]",
pillBackgroundClasses: isActive
? "bg-black/10 text-[rgb(var(--nodedc-on-card-active-rgb))]"
: "bg-[rgb(var(--nodedc-card-passive-rgb))] text-[rgb(var(--nodedc-on-card-passive-rgb))]",
@ -61,7 +61,7 @@ export const NodedcWorkItemProgress = (props: TNodedcWorkItemProgressProps) => {
<span
className={cn(
"absolute -top-1 right-6 translate-x-1/2 text-center text-[8px] leading-[10px] font-medium",
isActive ? "text-[#2F4721]" : "text-[#B3B3B8]"
isActive ? "text-[rgb(var(--nodedc-on-card-active-rgb))]/70" : "text-[#B3B3B8]"
)}
>
{percentage}%
@ -70,10 +70,16 @@ export const NodedcWorkItemProgress = (props: TNodedcWorkItemProgressProps) => {
{segments.map((segmentFill, segmentIndex) => (
<div
key={segmentIndex}
className={cn("h-full flex-1 overflow-hidden rounded-full", isActive ? "bg-black/20" : "bg-white/16")}
className={cn(
"h-full flex-1 overflow-hidden rounded-full",
isActive ? "bg-[rgb(var(--nodedc-on-card-active-rgb))]/20" : "bg-white/16"
)}
>
<div
className={cn("h-full rounded-full", isActive ? "bg-black" : "bg-[#C3FF66]")}
className={cn(
"h-full rounded-full",
isActive ? "bg-[rgb(var(--nodedc-on-card-active-rgb))]" : "bg-[rgb(var(--nodedc-accent-rgb))]"
)}
style={{ width: `${segmentFill}%` }}
/>
</div>

View File

@ -10,7 +10,7 @@ export const PROFILE_STATE_GROUP_COLORS: Partial<Record<TStateGroups, string>> =
backlog: "#050505",
unstarted: "#7C7F85",
started: "#F5F7FB",
completed: "#C3FF66",
completed: "rgb(var(--nodedc-accent-rgb))",
cancelled: "#050505",
};
@ -23,7 +23,7 @@ export const PROFILE_STATE_GROUP_LABELS: Partial<Record<TStateGroups, string>> =
};
export const PROFILE_PRIORITY_COLORS: Record<string, string> = {
urgent: "#C3FF66",
urgent: "rgb(var(--nodedc-accent-rgb))",
high: "#F5F7FB",
medium: "#7C7F85",
low: "#2A2B2E",

View File

@ -0,0 +1,283 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { Fragment, useEffect, useMemo, useState } from "react";
import { Popover, Transition } from "@headlessui/react";
import { observer } from "mobx-react";
import * as ColorPicker from "react-color";
import type { ColorResult } from "react-color";
// plane imports
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import {
applyNodedcAccent,
getReadableNodedcTextRgb,
nodedcAccentHexToRgb,
normalizeNodedcAccentHex,
} from "@plane/utils";
// helpers
import { NODEDC_DEFAULT_ACCENT_HEX } from "@/helpers/nodedc-design";
// components
import { SettingsControlItem } from "@/components/settings/control-item";
// hooks
import { useUserProfile } from "@/hooks/store/user";
const ACCENT_PRESET_COLORS = [
"#EF4444",
"#F97316",
"#FACC15",
"#8B5E34",
"#C3FF66",
"#5FBF2A",
"#A855F7",
"#7C3AED",
"#3B82F6",
"#2DD4BF",
"#86EFAC",
"#050505",
"#2A2B2E",
"#7C7F85",
"#F5F7FB",
];
const CHROME_PICKER_STYLES = {
default: {
picker: {
width: "100%",
background: "transparent",
borderRadius: 0,
boxShadow: "none",
fontFamily: "inherit",
},
saturation: {
borderRadius: "1.35rem",
overflow: "hidden",
},
Saturation: {
borderRadius: "1.35rem",
},
body: {
padding: "0.9rem 0 0",
},
controls: {
alignItems: "center",
gap: "0.75rem",
},
color: {
width: "2rem",
},
swatch: {
borderRadius: "999px",
boxShadow: "0 0 0 1px rgba(255,255,255,0.14)",
},
hue: {
borderRadius: "999px",
overflow: "hidden",
},
Hue: {
borderRadius: "999px",
},
alpha: {
display: "none",
},
Alpha: {
display: "none",
},
},
};
const getReadableColor = (hex: string) => {
const rgb = nodedcAccentHexToRgb(hex);
if (!rgb) return undefined;
return `rgb(${getReadableNodedcTextRgb(rgb).join(" ")})`;
};
export const ProfileSettingsAccentColor = observer(function ProfileSettingsAccentColor() {
const { data: userProfile, updateUserTheme } = useUserProfile();
const [draftAccent, setDraftAccent] = useState(NODEDC_DEFAULT_ACCENT_HEX);
const [isSaving, setIsSaving] = useState(false);
const savedAccent = useMemo(
() => normalizeNodedcAccentHex(userProfile?.theme?.nodedcAccent) || NODEDC_DEFAULT_ACCENT_HEX,
[userProfile?.theme?.nodedcAccent]
);
const normalizedDraftAccent = normalizeNodedcAccentHex(draftAccent);
const isDraftValid = !!normalizedDraftAccent;
const isDirty = normalizedDraftAccent !== savedAccent;
useEffect(() => {
setDraftAccent(savedAccent);
}, [savedAccent]);
const handleAccentChange = (value: string) => {
const nextValue = value.startsWith("#") ? value : `#${value}`;
setDraftAccent(nextValue);
const normalizedValue = normalizeNodedcAccentHex(nextValue);
if (normalizedValue) applyNodedcAccent(normalizedValue);
};
const handleColorPickerChange = (color: ColorResult) => {
handleAccentChange(color.hex);
};
const handleSave = async () => {
if (!normalizedDraftAccent) {
setToast({
type: TOAST_TYPE.ERROR,
title: "Ошибка",
message: "Введите корректный HEX-цвет.",
});
return;
}
try {
setIsSaving(true);
applyNodedcAccent(normalizedDraftAccent);
await updateUserTheme({ nodedcAccent: normalizedDraftAccent });
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Сохранено",
message: "Акцентный цвет обновлен.",
});
} catch (_error) {
applyNodedcAccent(savedAccent);
setDraftAccent(savedAccent);
setToast({
type: TOAST_TYPE.ERROR,
title: "Ошибка",
message: "Не удалось сохранить акцентный цвет.",
});
} finally {
setIsSaving(false);
}
};
const handleReset = async () => {
try {
setIsSaving(true);
setDraftAccent(NODEDC_DEFAULT_ACCENT_HEX);
applyNodedcAccent(NODEDC_DEFAULT_ACCENT_HEX);
await updateUserTheme({ nodedcAccent: undefined });
setToast({
type: TOAST_TYPE.SUCCESS,
title: "Сброшено",
message: "Акцентный цвет возвращен к дизайн-конфигу.",
});
} catch (_error) {
applyNodedcAccent(savedAccent);
setDraftAccent(savedAccent);
setToast({
type: TOAST_TYPE.ERROR,
title: "Ошибка",
message: "Не удалось сбросить акцентный цвет.",
});
} finally {
setIsSaving(false);
}
};
return (
<SettingsControlItem
title="Акцентный цвет"
description="Локальная настройка пользователя. Меняет цвет кнопок, активных элементов, шкал и выделений без перезапуска системы."
control={
<div className="flex flex-col gap-2 sm:flex-row sm:items-center">
<div className="relative w-40">
<Popover as="div" className="absolute top-1/2 left-3 z-20 -translate-y-1/2">
{() => (
<>
<Popover.Button
type="button"
className="grid size-5 place-items-center rounded-full outline-none transition-transform hover:scale-105 focus-visible:scale-105"
aria-label="Открыть палитру акцентного цвета"
>
<span
className="size-4 rounded-full shadow-[0_0_0_1px_rgba(255,255,255,0.24)]"
style={{ backgroundColor: normalizedDraftAccent || NODEDC_DEFAULT_ACCENT_HEX }}
/>
</Popover.Button>
<Transition
as={Fragment}
enter="transition ease-out duration-150"
enterFrom="opacity-0 translate-y-1 scale-95"
enterTo="opacity-100 translate-y-0 scale-100"
leave="transition ease-in duration-100"
leaveFrom="opacity-100 translate-y-0 scale-100"
leaveTo="opacity-0 translate-y-1 scale-95"
>
<Popover.Panel className="nodedc-accent-picker-panel absolute top-full left-0 z-[90] mt-4 w-[21rem] origin-top-left rounded-[1.75rem] p-4 shadow-[0_28px_80px_rgba(0,0,0,0.46)]">
<ColorPicker.ChromePicker
className="nodedc-accent-chrome-picker"
color={normalizedDraftAccent || NODEDC_DEFAULT_ACCENT_HEX}
disableAlpha
onChange={handleColorPickerChange}
styles={CHROME_PICKER_STYLES}
/>
<div className="mt-4 grid grid-cols-8 gap-2">
{ACCENT_PRESET_COLORS.map((color) => {
const isSelected = color === normalizedDraftAccent;
return (
<button
key={color}
type="button"
className="grid size-7 place-items-center rounded-full transition-transform hover:scale-105 focus-visible:scale-105"
onClick={() => handleAccentChange(color)}
aria-label={`Выбрать цвет ${color}`}
>
<span
className="size-5 rounded-full"
style={{
backgroundColor: color,
boxShadow: isSelected
? `0 0 0 2px rgba(245,247,251,0.92), 0 0 0 5px ${color}`
: "0 0 0 1px rgba(255,255,255,0.16)",
}}
/>
</button>
);
})}
</div>
</Popover.Panel>
</Transition>
</>
)}
</Popover>
<input
name="nodedcAccent"
value={draftAccent}
onChange={(event) => handleAccentChange(event.target.value)}
placeholder={NODEDC_DEFAULT_ACCENT_HEX}
className="nodedc-settings-input h-11 min-h-11 w-full pl-10 pr-4 text-13 font-medium uppercase"
style={{
color: normalizedDraftAccent ? getReadableColor(normalizedDraftAccent) : undefined,
}}
aria-invalid={!isDraftValid}
/>
</div>
<div className="flex items-center gap-2">
<button
type="button"
className="nodedc-settings-primary-button min-w-[7rem] text-13 font-medium disabled:cursor-not-allowed disabled:opacity-50"
onClick={handleSave}
disabled={!isDraftValid || !isDirty || isSaving}
>
Применить
</button>
<button
type="button"
className="nodedc-settings-secondary-button min-w-[6rem] text-13 font-medium disabled:cursor-not-allowed disabled:opacity-50"
onClick={handleReset}
disabled={savedAccent === NODEDC_DEFAULT_ACCENT_HEX || isSaving}
>
Сбросить
</button>
</div>
</div>
}
/>
);
});

View File

@ -7,6 +7,8 @@
import { observer } from "mobx-react";
// components
import { ThemeSwitcher } from "@/plane-web/components/preferences/theme-switcher";
// local imports
import { ProfileSettingsAccentColor } from "./accent-color";
export const ProfileSettingsDefaultPreferencesList = observer(function ProfileSettingsDefaultPreferencesList() {
return (
@ -18,6 +20,7 @@ export const ProfileSettingsDefaultPreferencesList = observer(function ProfileSe
description: "select_or_customize_your_interface_color_scheme",
}}
/>
<ProfileSettingsAccentColor />
</div>
);
});

View File

@ -0,0 +1,12 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { rgbToNodedcAccentHex } from "@plane/utils";
import designConfig from "../../design.config.json";
const defaultAccentRgb = designConfig.nodedc.accent_rgb as [number, number, number];
export const NODEDC_DEFAULT_ACCENT_HEX = rgbToNodedcAccentHex(defaultAccentRgb);

View File

@ -12,7 +12,8 @@ import { useTheme } from "next-themes";
import type { TLanguage } from "@plane/i18n";
import { DEFAULT_LANGUAGE, useTranslation } from "@plane/i18n";
// helpers
import { applyCustomTheme, clearCustomTheme } from "@plane/utils";
import { applyCustomTheme, applyNodedcAccent, clearCustomTheme } from "@plane/utils";
import { NODEDC_DEFAULT_ACCENT_HEX } from "@/helpers/nodedc-design";
// hooks
import { useAppTheme } from "@/hooks/store/use-app-theme";
import { useRouterParams } from "@/hooks/store/use-router-params";
@ -107,6 +108,11 @@ function StoreWrapper(props: TStoreWrapper) {
previousThemeRef.current = currentTheme;
}, [userProfile?.theme]);
useEffect(() => {
if (!userProfile?.id) return;
applyNodedcAccent(userProfile?.theme?.nodedcAccent || NODEDC_DEFAULT_ACCENT_HEX);
}, [userProfile?.id, userProfile?.theme?.nodedcAccent]);
useEffect(() => {
if (!userProfile?.id) return;
changeLanguage((userProfile?.language as TLanguage) || DEFAULT_LANGUAGE);

View File

@ -45,6 +45,7 @@ export class ProfileStore implements IUserProfileStore {
primary: undefined,
background: undefined,
darkPalette: false,
nodedcAccent: undefined,
},
onboarding_step: {
workspace_join: false,

View File

@ -1285,6 +1285,45 @@
color: var(--text-color-primary) !important;
}
.nodedc-accent-picker-panel {
border: 0 !important;
outline: none !important;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.075) 0%, rgba(255, 255, 255, 0.026) 100%),
rgba(8, 9, 12, 0.78) !important;
-webkit-backdrop-filter: blur(28px);
backdrop-filter: blur(28px);
}
.nodedc-accent-chrome-picker,
.nodedc-accent-chrome-picker * {
font-family: inherit !important;
}
.nodedc-accent-chrome-picker input {
min-height: 2rem !important;
border: 0 !important;
outline: none !important;
border-radius: 0.85rem !important;
background: rgba(255, 255, 255, 0.07) !important;
box-shadow: none !important;
color: var(--text-color-primary) !important;
font-size: 0.75rem !important;
font-weight: 500 !important;
}
.nodedc-accent-chrome-picker label {
color: var(--text-color-secondary) !important;
font-size: 0.62rem !important;
font-weight: 600 !important;
letter-spacing: 0.08em !important;
text-transform: uppercase !important;
}
.nodedc-accent-chrome-picker .flexbox-fix {
border: 0 !important;
}
.nodedc-settings-danger-button {
min-height: 2.75rem;
border: 0 !important;

View File

@ -13,6 +13,7 @@ export type TUserProfile = {
theme: {
theme: string | undefined;
nodedcAccent?: string | undefined;
};
onboarding_step: {

View File

@ -69,6 +69,7 @@ export type TUserProfile = {
primary: string | undefined;
background: string | undefined;
darkPalette: boolean | undefined;
nodedcAccent?: string | undefined;
};
onboarding_step: TOnboardingSteps;
is_onboarded: boolean;
@ -107,6 +108,7 @@ export interface IUserTheme {
primary?: string | undefined;
background?: string | undefined;
darkPalette?: boolean | undefined;
nodedcAccent?: string | undefined;
}
export interface IUserMemberLite extends IUserLite {

View File

@ -27,6 +27,15 @@ export {
type DarknessDetectionMethod,
} from "./theme-application";
// NODE.DC runtime accent
export {
applyNodedcAccent,
getReadableNodedcTextRgb,
nodedcAccentHexToRgb,
normalizeNodedcAccentHex,
rgbToNodedcAccentHex,
} from "./nodedc-accent";
// Color conversion utilities
export {
hexToHSL,

View File

@ -0,0 +1,102 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { normalizeHexColor, validateHexColor } from "./color-validation";
type TRgbTuple = [number, number, number];
const DARK_TEXT_RGB: TRgbTuple = [11, 17, 23];
const LIGHT_TEXT_RGB: TRgbTuple = [245, 247, 251];
const clampRgbChannel = (value: number) => Math.min(Math.max(Math.round(value), 0), 255);
const formatRgbTuple = (rgb: readonly number[]) => rgb.map(clampRgbChannel).join(" ");
const formatCssRgb = (rgb: readonly number[]) => `rgb(${formatRgbTuple(rgb)})`;
const blendRgb = (rgb: readonly number[], target: number, ratio: number): TRgbTuple =>
rgb.map((channel) => clampRgbChannel(channel * (1 - ratio) + target * ratio)) as TRgbTuple;
const toRelativeLuminance = (rgb: readonly number[]) => {
const [r, g, b] = rgb.map((channel) => {
const normalized = channel / 255;
return normalized <= 0.03928 ? normalized / 12.92 : ((normalized + 0.055) / 1.055) ** 2.4;
});
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
export const normalizeNodedcAccentHex = (hex: string | null | undefined): string | undefined => {
if (!hex || !validateHexColor(hex)) return undefined;
return `#${normalizeHexColor(hex)}`;
};
export const nodedcAccentHexToRgb = (hex: string | null | undefined): TRgbTuple | undefined => {
const normalizedHex = normalizeNodedcAccentHex(hex);
if (!normalizedHex) return undefined;
const cleanHex = normalizedHex.replace("#", "");
return [0, 2, 4].map((start) => parseInt(cleanHex.slice(start, start + 2), 16)) as TRgbTuple;
};
export const rgbToNodedcAccentHex = (rgb: readonly number[]): string =>
`#${rgb
.map((channel) => clampRgbChannel(channel).toString(16).padStart(2, "0"))
.join("")
.toUpperCase()}`;
export const getReadableNodedcTextRgb = (rgb: readonly number[]): TRgbTuple =>
toRelativeLuminance(rgb) > 0.52 ? DARK_TEXT_RGB : LIGHT_TEXT_RGB;
export const applyNodedcAccent = (hex: string | null | undefined): boolean => {
if (typeof document === "undefined") return false;
const accentRgb = nodedcAccentHexToRgb(hex);
if (!accentRgb) return false;
const root = document.documentElement;
const onAccentRgb = getReadableNodedcTextRgb(accentRgb);
const brandScale: Record<string, TRgbTuple> = {
"100": blendRgb(accentRgb, 255, 0.9),
"200": blendRgb(accentRgb, 255, 0.8),
"300": blendRgb(accentRgb, 255, 0.65),
"400": blendRgb(accentRgb, 255, 0.45),
"500": blendRgb(accentRgb, 255, 0.25),
"600": blendRgb(accentRgb, 255, 0.1),
"700": blendRgb(accentRgb, 0, 0.25),
"800": blendRgb(accentRgb, 0, 0.35),
"900": blendRgb(accentRgb, 0, 0.45),
"1000": blendRgb(accentRgb, 0, 0.6),
"1100": blendRgb(accentRgb, 0, 0.72),
"1200": blendRgb(accentRgb, 0, 0.82),
};
root.style.setProperty("--nodedc-accent-rgb", formatRgbTuple(accentRgb));
root.style.setProperty("--nodedc-card-active-rgb", formatRgbTuple(accentRgb));
root.style.setProperty("--nodedc-on-accent-rgb", formatRgbTuple(onAccentRgb));
root.style.setProperty("--nodedc-on-card-active-rgb", formatRgbTuple(onAccentRgb));
Object.entries(brandScale).forEach(([key, value]) => {
root.style.setProperty(`--brand-${key}`, formatCssRgb(value));
});
root.style.setProperty("--brand-default", formatCssRgb(accentRgb));
root.style.setProperty("--bg-accent-primary", formatCssRgb(accentRgb));
root.style.setProperty("--bg-accent-primary-hover", formatCssRgb(blendRgb(accentRgb, 255, 0.18)));
root.style.setProperty("--bg-accent-primary-active", formatCssRgb(blendRgb(accentRgb, 0, 0.1)));
root.style.setProperty("--txt-accent-primary", formatCssRgb(accentRgb));
root.style.setProperty("--txt-accent-secondary", formatCssRgb(blendRgb(accentRgb, 0, 0.25)));
root.style.setProperty("--txt-icon-accent-primary", formatCssRgb(accentRgb));
root.style.setProperty("--text-color-accent-primary", formatCssRgb(accentRgb));
root.style.setProperty("--text-color-accent-secondary", formatCssRgb(blendRgb(accentRgb, 0, 0.25)));
root.style.setProperty("--text-color-icon-accent-primary", formatCssRgb(accentRgb));
root.style.setProperty("--stroke-accent-primary", formatCssRgb(accentRgb));
root.style.setProperty("--fill-accent-primary", formatCssRgb(accentRgb));
root.style.setProperty("--txt-on-color", formatCssRgb(onAccentRgb));
root.style.setProperty("--txt-icon-on-color", formatCssRgb(onAccentRgb));
return true;
};