133 lines
5.5 KiB
TypeScript
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;
|
|
};
|