UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: локальный акцент и аналитика
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Reference in New Issue
Block a user