NODEDC_TASKMANAGER/plane-src/packages/utils/src/theme/nodedc-accent.ts

133 lines
5.5 KiB
TypeScript

/**
* 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;
};
export const applyNodedcPassiveCardColor = (hex: string | null | undefined): boolean => {
if (typeof document === "undefined") return false;
const passiveCardRgb = nodedcAccentHexToRgb(hex);
if (!passiveCardRgb) return false;
const root = document.documentElement;
const onPassiveCardRgb = getReadableNodedcTextRgb(passiveCardRgb);
root.style.setProperty("--nodedc-card-passive-rgb", formatRgbTuple(passiveCardRgb));
root.style.setProperty("--nodedc-on-card-passive-rgb", formatRgbTuple(onPassiveCardRgb));
return true;
};
export const applyNodedcPassiveCardSurfaceColor = (hex: string | null | undefined): boolean => {
if (typeof document === "undefined") return false;
const passiveCardSurfaceRgb = nodedcAccentHexToRgb(hex);
if (!passiveCardSurfaceRgb) return false;
const root = document.documentElement;
const onPassiveCardSurfaceRgb = getReadableNodedcTextRgb(passiveCardSurfaceRgb);
root.style.setProperty("--nodedc-card-passive-surface-rgb", formatRgbTuple(passiveCardSurfaceRgb));
root.style.setProperty("--nodedc-on-card-passive-surface-rgb", formatRgbTuple(onPassiveCardSurfaceRgb));
return true;
};