Files
nocodb/packages/nc-gui/utils/colorsUtils.ts
2026-03-28 07:09:09 +00:00

924 lines
24 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import colors from 'windicss/colors'
import { enumColors as enumColor } from 'nocodb-sdk'
import tinycolor from 'tinycolor2'
export { enumColors as enumColor } from 'nocodb-sdk'
export const theme = {
light: ['#ffdce5', '#fee2d5', '#ffeab6', '#d1f7c4', '#ede2fe', '#eee', '#cfdffe', '#d0f1fd', '#c2f5e8', '#ffdaf6'],
dark: [
'#f82b6099',
'#ff6f2c99',
'#fcb40099',
'#20c93399',
'#8b46ff99',
'#666',
'#2d7ff999',
'#18bfff99',
'#20d9d299',
'#ff08c299',
],
}
export const themeColors = {
'background': '#FFFFFF',
'surface': '#FFFFFF',
'primary': '#4351e8',
'primary-selected': 'var(--color-brand-50)',
'primary-selected-sidebar': 'var(--color-brand-50)',
'hover': '#E1E3E6',
'scrollbar': '#d7d7d7',
'scrollbar-hover': '#cbcbcb',
'border': '#F3F4F6',
'secondary': '#F2F4F7',
'secondary-darken-1': '#018786',
'error': '#B00020',
'info': '#2196F3',
'success': '#4CAF50',
'warning': '#FB8C00',
}
export const themeV2Colors = {
/** Primary shades */
'royal-blue': {
'DEFAULT': '#4351E8',
'50': '#E7E8FC',
'100': '#D4D8FA',
'200': '#B0B6F5',
'300': '#8C94F1',
'400': '#6773EC',
'500': '#4351E8',
'600': '#1A2BD8',
'700': '#1421A6',
'800': '#0E1774',
'900': '#080D42',
},
/** Accent shades */
'pink': colors.pink,
}
// @deprecated
// Use CSS variables from variables.css directly in future like:
// color: var(--nc-content-brand)
// background: var(--nc-bg-brand)
// The above values map 1:1 directly with Figma CSS variables.
export const themeV3Colors = {
base: {
white: '#FFFFFF',
black: '#000000',
},
brand: {
50: '#EBF0FF',
100: '#D6E0FF',
200: '#ADC2FF',
300: '#85A3FF',
400: '#5C85FF',
500: '#3366FF',
600: '#2952CC',
700: '#1F3D99',
800: '#142966',
900: '#0A1433',
},
gray: {
10: '#FCFCFC',
50: '#F9F9FA',
100: '#F4F4F5',
200: '#E7E7E9',
300: '#D5D5D9',
400: '#9AA2AF',
500: '#6A7184',
600: '#4A5268',
700: '#374151',
800: '#1F293A',
900: '#101015',
},
red: {
50: '#FFF2F1',
100: '#FFDBD9',
200: '#FFB7B2',
300: '#FF928C',
400: '#FF6E65',
500: '#FF4A3F',
600: '#E8463C',
700: '#CB3F36',
800: '#B23830',
900: '#7D2721',
},
pink: {
50: '#FFEEFB',
100: '#FED8F4',
200: '#FEB0E8',
300: '#FD89DD',
400: '#FD61D1',
500: '#FC3AC6',
600: '#CA2E9E',
700: '#972377',
800: '#65174F',
900: '#320C28',
},
orange: {
50: '#FFF5EF',
100: '#FEE6D6',
200: '#FDCDAD',
300: '#FCB483',
400: '#FB9B5A',
500: '#FA8231',
600: '#E1752C',
700: '#C86827',
800: '#964E1D',
900: '#4B270F',
},
purple: {
50: '#F3ECFA',
100: '#E5D4F5',
200: '#CBA8EB',
300: '#B17DE1',
400: '#9751D7',
500: '#7D26CD',
600: '#641EA4',
700: '#4B177B',
800: '#320F52',
900: '#190829',
},
blue: {
50: '#EDF9FF',
100: '#D7F2FF',
200: '#AFE5FF',
300: '#86D9FF',
400: '#5ECCFF',
500: '#36BFFF',
600: '#2B99CC',
700: '#207399',
800: '#164C66',
900: '#0B2633',
},
yellow: {
50: '#fffbf2',
100: '#fff0d1',
200: '#fee5b0',
300: '#fdd889',
400: '#fdcb61',
500: '#fcbe3a',
600: '#ca982e',
700: '#977223',
800: '#654c17',
900: '#32260c',
},
maroon: {
50: '#FFF0F7',
100: '#FFCFE6',
200: '#FFABD2',
300: '#EC7DB1',
400: '#D45892',
500: '#B33771',
600: '#9D255D',
700: '#801044',
800: '#690735',
900: '#42001F',
},
green: {
50: '#ECFFF2',
100: '#D4F7E0',
200: '#A9EFC1',
300: '#7DE6A3',
400: '#52DE84',
500: '#27D665',
600: '#1FAB51',
700: '#17803D',
800: '#105628',
900: '#082B14',
},
}
type ThemeV3ColorKeys = Exclude<keyof typeof themeV3Colors, 'base'>
type Shade = keyof (typeof themeV3Colors)[ThemeV3ColorKeys]
/**
* Get a random color from the themeV3Colors
* @param randomNumber - The random number to use to get the color
* @param shade - The shade of the color to get
* @returns The color
*/
export function getThemeV3RandomColor(randomNumber = 1, shade: Shade = 600): string {
const colorGroups = Object.keys(themeV3Colors).filter((key) => key !== 'base') as ThemeV3ColorKeys[]
const groupIndex = Math.floor(Math.random() * 1000 * randomNumber) % colorGroups.length
const colorGroup = colorGroups[groupIndex]!
const selectedGroup = themeV3Colors[colorGroup]
return selectedGroup[shade]
}
const isValidHex = (hex: string) => /^#([A-Fa-f0-9]{3,4}){1,2}$/.test(hex)
const getChunksFromString = (st: string, chunkSize: number) => st.match(new RegExp(`.{${chunkSize}}`, 'g'))
const convertHexUnitTo256 = (hexStr: string) => parseInt(hexStr.repeat(2 / hexStr.length), 16)
export const hexToRGB = (hex: string) => {
if (!isValidHex(hex)) {
throw new Error('Invalid HEX')
}
const chunkSize = Math.floor((hex.length - 1) / 3)
const hexArr = getChunksFromString(hex.slice(1), chunkSize)!
const [r, g, b] = hexArr.map(convertHexUnitTo256)
return `${r}, ${g}, ${b}`
}
export const baseThemeColors = [
themeV2Colors['royal-blue'].DEFAULT,
'#2D7FF9',
'#18BFFF',
'#0C4E65',
'#EC2CBD',
'#F82B60',
'#F57134',
'#1BAF2C',
'#8B46FF',
'#1B51A2',
'#146C8E',
'#24716E',
'#8A2170',
'#941737',
'#B94915',
'#0E4C15',
'#381475',
'#333333',
]
export const baseIconColors = ['#36BFFF', '#FA8231', '#FCBE3A', '#27D665', '#6A7184', '#FF4A3F', '#FC3AC6', '#7D26CD']
const designSystem = {
light: [
// '#EBF0FF',
// '#D6E0FF',
// '#ADC2FF',
// '#85A3FF',
// '#5C85FF',
'#3366FF',
'#2952CC',
'#1F3D99',
'#142966',
'#0A1433',
// '#FCFCFC',
// '#F9F9FA',
// '#F4F4F5',
// '#E7E7E9',
// '#D5D5D9',
// '#9AA2AF',
// '#6A7184',
'#4A5268',
'#374151',
'#1F293A',
'#101015',
// '#FFF2F1',
// '#FFDBD9',
// '#FFB7B2',
// '#FF928C',
// '#FF6E65',
// '#FF4A3F',
'#E8463C',
'#CB3F36',
'#B23830',
'#7D2721',
// '#FFEEFB',
// '#FED8F4',
// '#FEB0E8',
// '#FD89DD',
// '#FD61D1',
'#FC3AC6',
'#CA2E9E',
'#972377',
'#65174F',
'#320C28',
// '#FFF5EF',
// '#FEE6D6',
// '#FDCDAD',
// '#FCB483',
// '#FB9B5A',
'#FA8231',
'#E1752C',
'#C86827',
'#964E1D',
'#4B270F',
// '#F3ECFA',
// '#E5D4F5',
// '#CBA8EB',
// '#B17DE1',
// '#9751D7',
'#7D26CD',
'#641EA4',
'#4B177B',
'#320F52',
'#190829',
// '#EDF9FF',
// '#D7F2FF',
// '#AFE5FF',
// '#86D9FF',
// '#5ECCFF',
'#36BFFF',
'#2B99CC',
'#207399',
'#164C66',
'#0B2633',
// '#fffbf2',
// '#fff0d1',
// '#fee5b0',
// '#fdd889',
// '#fdcb61',
// '#fcbe3a',
'#ca982e',
'#977223',
'#654c17',
'#32260c',
],
dark: [],
}
// convert string into a unique color
export const stringToColor = (input: string, colorArray = designSystem.light) => {
// Calculate a numeric hash value from the input string
let hash = 0
for (let i = 0; i < input.length; i++) {
hash = input.charCodeAt(i) + ((hash << 5) - hash)
}
// Ensure the hash value is within the array length
const index = Math.abs(hash) % colorArray.length
// Return the selected color
return colorArray[index]
}
// Function to convert hex color to RGB
function hexToRGBObject(hexColor: string) {
// Remove '#' if present in the hexColor
hexColor = hexColor.replace(/^#/, '')
// Split the hexColor into red, green, and blue components
const r = parseInt(hexColor.substring(0, 2), 16)
const g = parseInt(hexColor.substring(2, 4), 16)
const b = parseInt(hexColor.substring(4, 6), 16)
return { r, g, b }
}
export function isColorDark(hexColor: string) {
const rgbColor = hexToRGBObject(hexColor)
const luminance = 0.299 * rgbColor.r + 0.587 * rgbColor.g + 0.114 * rgbColor.b
// Choose a luminance threshold (e.g., 0.5) to determine darkness/lightness
return luminance < 128
}
export function getEnumColorByIndex(i: number, mode: 'light' | 'dark' = 'light') {
return enumColor[mode][i % enumColor[mode].length]
}
export function hexToRgb(hex: string) {
const cleaned = hex.replace('#', '')
const bigint = parseInt(cleaned, 16)
const r = (bigint >> 16) & 255
const g = (bigint >> 8) & 255
const b = bigint & 255
return `${r}, ${g}, ${b}`
}
export function flattenColors(colors: Record<string, any>, prefix = ''): Record<string, string> {
const result: Record<string, string> = {}
Object.entries(colors).forEach(([key, value]) => {
const newKey = prefix ? (key === 'DEFAULT' ? prefix : `${prefix}-${key}`) : key
if (typeof value === 'string') {
result[newKey] = value
} else {
Object.assign(result, flattenColors(value, newKey))
}
})
return result
}
export function ncBuildColorsWithOpacity(colors: Record<string, any>, prefix: string = ''): Record<string, any> {
const flat = flattenColors(colors, prefix)
const result: Record<string, any> = {}
Object.entries(flat).forEach(([key, value]) => {
const rgb = value.startsWith('#') ? hexToRgb(value) : `var(${value})`
result[key] = ({ opacityVariable, opacityValue }: { opacityVariable?: string; opacityValue?: number } = {}) => {
if (opacityValue !== undefined) return `rgba(${rgb}, ${opacityValue})`
if (opacityVariable !== undefined) return `rgba(${rgb}, var(${opacityVariable}, 1))`
return `rgb(${rgb})`
}
})
return result
}
export const themeV4Colors = {
base: { white: '--rgb-color-base-white', black: '--rgb-color-base-black' },
brand: {
inverted: '--rgb-nc-bg-brand-inverted',
20: '--rgb-color-brand-20',
50: '--rgb-color-brand-50',
100: '--rgb-color-brand-100',
200: '--rgb-color-brand-200',
300: '--rgb-color-brand-300',
400: '--rgb-color-brand-400',
500: '--rgb-color-brand-500',
600: '--rgb-color-brand-600',
700: '--rgb-color-brand-700',
800: '--rgb-color-brand-800',
900: '--rgb-color-brand-900',
},
gray: {
10: '#FCFCFC',
20: '--rgb-color-gray-20',
50: '--rgb-color-gray-50',
100: '--rgb-color-gray-100',
200: '--rgb-color-gray-200',
300: '--rgb-color-gray-300',
400: '--rgb-color-gray-400',
500: '--rgb-color-gray-500',
600: '--rgb-color-gray-600',
700: '--rgb-color-gray-700',
800: '--rgb-color-gray-800',
900: '--rgb-color-gray-900',
},
red: {
20: '--rgb-color-red-20',
50: '--rgb-color-red-50',
100: '--rgb-color-red-100',
200: '--rgb-color-red-200',
300: '--rgb-color-red-300',
400: '--rgb-color-red-400',
500: '--rgb-color-red-500',
600: '--rgb-color-red-600',
700: '--rgb-color-red-700',
800: '--rgb-color-red-800',
900: '--rgb-color-red-900',
},
pink: {
20: '--rgb-color-pink-20',
50: '--rgb-color-pink-50',
100: '--rgb-color-pink-100',
200: '--rgb-color-pink-200',
300: '--rgb-color-pink-300',
400: '--rgb-color-pink-400',
500: '--rgb-color-pink-500',
600: '--rgb-color-pink-600',
700: '--rgb-color-pink-700',
800: '--rgb-color-pink-800',
900: '--rgb-color-pink-900',
},
orange: {
20: '--rgb-color-orange-20',
50: '--rgb-color-orange-50',
100: '--rgb-color-orange-100',
200: '--rgb-color-orange-200',
300: '--rgb-color-orange-300',
400: '--rgb-color-orange-400',
500: '--rgb-color-orange-500',
600: '--rgb-color-orange-600',
700: '--rgb-color-orange-700',
800: '--rgb-color-orange-800',
900: '--rgb-color-orange-900',
},
purple: {
20: '--rgb-color-purple-20',
50: '--rgb-color-purple-50',
100: '--rgb-color-purple-100',
200: '--rgb-color-purple-200',
300: '--rgb-color-purple-300',
400: '--rgb-color-purple-400',
500: '--rgb-color-purple-500',
600: '--rgb-color-purple-600',
700: '--rgb-color-purple-700',
800: '--rgb-color-purple-800',
900: '--rgb-color-purple-900',
},
blue: {
20: '--rgb-color-blue-20',
50: '--rgb-color-blue-50',
100: '--rgb-color-blue-100',
200: '--rgb-color-blue-200',
300: '--rgb-color-blue-300',
400: '--rgb-color-blue-400',
500: '--rgb-color-blue-500',
600: '--rgb-color-blue-600',
700: '--rgb-color-blue-700',
800: '--rgb-color-blue-800',
900: '--rgb-color-blue-900',
},
yellow: {
20: '--rgb-color-yellow-20',
50: '--rgb-color-yellow-50',
100: '--rgb-color-yellow-100',
200: '--rgb-color-yellow-200',
300: '--rgb-color-yellow-300',
400: '--rgb-color-yellow-400',
500: '--rgb-color-yellow-500',
600: '--rgb-color-yellow-600',
700: '--rgb-color-yellow-700',
800: '--rgb-color-yellow-800',
900: '--rgb-color-yellow-900',
},
maroon: {
20: '--rgb-color-maroon-20',
50: '--rgb-color-maroon-50',
100: '--rgb-color-maroon-100',
200: '--rgb-color-maroon-200',
300: '--rgb-color-maroon-300',
400: '--rgb-color-maroon-400',
500: '--rgb-color-maroon-500',
600: '--rgb-color-maroon-600',
700: '--rgb-color-maroon-700',
800: '--rgb-color-maroon-800',
900: '--rgb-color-maroon-900',
},
green: {
20: '--rgb-color-green-20',
50: '--rgb-color-green-50',
100: '--rgb-color-green-100',
200: '--rgb-color-green-200',
300: '--rgb-color-green-300',
400: '--rgb-color-green-400',
500: '--rgb-color-green-500',
600: '--rgb-color-green-600',
700: '--rgb-color-green-700',
800: '--rgb-color-green-800',
900: '--rgb-color-green-900',
},
}
/**
* In our WindiCSS config, we already added `themeV3Colors`.
* To add `themeV4Colors` without conflicts, we create a new object
* with all top-level keys prefixed by `nc-` (e.g., `gray` → `nc-gray`).
*
* This keeps both V3 and V4 colors available in the theme without overwriting each other.
*/
export const themeV4ColorsWithNcPrefix: {
[K in keyof typeof themeV4Colors as `nc-${K}`]: (typeof themeV4Colors)[K]
} = Object.entries(themeV4Colors).reduce((acc, [key, value]) => {
acc[`nc-${key}` as `nc-${string}`] = value
return acc
}, {} as any)
/**
* ### Light Theme Configuration
* In this project, we've integrated a custom WindiCSS configuration that aligns with our Figma design system.
* This setup introduces shorthand class names for various UI elements like text color, border color,
* background color, and more. The goal is to ensure design consistency and streamline the development
* process by directly reflecting Figma styles in our codebase.
*
* #### Why We Introduced This
*
* The light theme was introduced to:
* - **Maintain Consistency**: Ensures that the color scheme used in the design (Figma) is reflected accurately in the codebase.
* - **Ease of Use**: Simplifies the application of colors by using intuitive, design-referenced class names.
*
* #### Usage
*
* The configuration extends WindiCSS with custom screens, font sizes, font weights, colors, and other utilities.
* These can be used directly in files through class names.
*
* ###### Text Color
* To apply a text color, you can use:
* ```html
* <p class="text-nc-content-gray-subtle">This is subtle gray text.</p>
* ```
*
* ###### Border Color
* To apply a border color, you can use:
* ```html
* <div class="border-nc-border-gray-light">This div has a light gray border.</div>
* ```
*
* ###### Background Color
* To apply a background color, you can use:
* ```html
* <div class="bg-nc-bg-brand">This div has a brand color background.</div>
* <div class="bg-nc-bg-blue-dark">This div has a blue dark color background.</div>
* ```
*
* ###### Fill Color
* light theme fill colors are globally extended in WindiCSS and can be used for various purposes such as:
* - **SVG Fill**:
* ```html
* <svg class="fill-nc-fill-primary">...</svg>
* ```
* - **Text Color**:
* ```html
* <p class="text-nc-fill-red-dark">...</p>
* ```
* - **Border Color**:
* ```html
* <div class="border-nc-fill-primary">...</div>
* ```
* - **Background Color**:
* ```html
* <div class="bg-nc-fill-primary-hover">...</div>
* ```
* This setup ensures that your styles are consistent with your design specifications and easily maintainable across the project.
*/
export const themeVariables = {
content: {
'nc-content-gray': {
extreme: themeV4Colors.base.black,
emphasis: themeV4Colors.gray[900],
DEFAULT: themeV4Colors.gray[800],
subtle: themeV4Colors.gray[700],
subtle2: themeV4Colors.gray[600],
muted: themeV4Colors.gray[500],
disabled: themeV4Colors.gray[400],
},
'nc-content-brand': {
DEFAULT: themeV4Colors.brand[500],
disabled: themeV4Colors.brand[600],
hover: themeV4Colors.gray[300],
},
'nc-content-inverted-primary': {
DEFAULT: themeV4Colors.base.white,
hover: themeV4Colors.base.white,
disabled: themeV4Colors.gray[500],
},
'nc-content-inverted-secondary': {
DEFAULT: themeV4Colors.gray[700],
hover: themeV4Colors.gray[700],
disabled: themeV4Colors.gray[500],
},
'nc-content-red': {
dark: themeV4Colors.red[700],
medium: themeV4Colors.red[500],
light: themeV4Colors.red[300],
},
'nc-content-green': {
dark: themeV4Colors.green[700],
medium: themeV4Colors.green[500],
light: themeV4Colors.green[300],
},
'nc-content-yellow': {
dark: themeV4Colors.yellow[700],
medium: themeV4Colors.yellow[500],
light: themeV4Colors.yellow[300],
},
'nc-content-blue': {
dark: themeV4Colors.blue[700],
medium: themeV4Colors.blue[500],
light: themeV4Colors.blue[300],
},
'nc-content-purple': {
dark: themeV4Colors.purple[700],
medium: themeV4Colors.purple[500],
light: themeV4Colors.purple[300],
},
'nc-content-pink': {
dark: themeV4Colors.pink[700],
medium: themeV4Colors.pink[500],
light: themeV4Colors.pink[300],
},
'nc-content-orange': {
dark: themeV4Colors.orange[700],
medium: themeV4Colors.orange[500],
light: themeV4Colors.orange[300],
},
'nc-content-maroon': {
dark: themeV4Colors.maroon[700],
medium: themeV4Colors.maroon[500],
light: themeV4Colors.maroon[300],
},
},
background: {
'nc-bg-default': themeV4Colors.base.white,
'nc-bg-brand': {
DEFAULT: themeV4Colors.brand[50],
inverted: themeV4Colors.brand.inverted,
},
'nc-bg-gray': {
extralight: themeV4Colors.gray[50],
sidebar: themeV4Colors.gray[50],
minisidebar: themeV4Colors.gray[100],
light: themeV4Colors.gray[100],
medium: themeV4Colors.gray[200],
dark: themeV4Colors.gray[300],
extradark: themeV4Colors.gray[400],
},
'nc-bg-red': {
light: themeV4Colors.red[50],
dark: themeV4Colors.red[100],
},
'nc-bg-green': {
light: themeV4Colors.green[50],
dark: themeV4Colors.green[100],
},
'nc-bg-yellow': {
light: themeV4Colors.yellow[50],
dark: themeV4Colors.yellow[100],
},
'nc-bg-blue': {
light: themeV4Colors.blue[50],
dark: themeV4Colors.blue[100],
},
'nc-bg-purple': {
light: themeV4Colors.purple[50],
dark: themeV4Colors.purple[100],
},
'nc-bg-pink': {
light: themeV4Colors.pink[50],
dark: themeV4Colors.pink[100],
},
'nc-bg-orange': {
light: themeV4Colors.orange[50],
dark: themeV4Colors.orange[100],
},
'nc-bg-maroon': {
light: themeV4Colors.maroon[50],
dark: themeV4Colors.maroon[100],
},
},
border: {
'nc-border-brand': {
DEFAULT: themeV4Colors.brand[500],
medium: themeV4Colors.brand[200],
},
'nc-border-gray': {
extralight: themeV4Colors.gray[50],
light: themeV4Colors.gray[100],
medium: themeV4Colors.gray[200],
dark: themeV4Colors.gray[300],
extradark: themeV4Colors.gray[400],
underline: themeV4Colors.gray[600],
},
'nc-border-red': {
DEFAULT: themeV4Colors.red[500],
},
'nc-border-green': {
DEFAULT: themeV4Colors.green[500],
},
'nc-border-yellow': {
DEFAULT: themeV4Colors.yellow[500],
},
'nc-border-blue': {
DEFAULT: themeV4Colors.blue[500],
},
'nc-border-purple': {
DEFAULT: themeV4Colors.purple[500],
medium: themeV4Colors.purple[200],
light: themeV4Colors.purple[100],
},
'nc-border-pink': {
DEFAULT: themeV4Colors.pink[500],
},
'nc-border-orange': {
DEFAULT: themeV4Colors.orange[500],
},
'nc-border-maroon': {
DEFAULT: themeV4Colors.maroon[500],
},
},
fill: {
'nc-fill-primary': {
DEFAULT: themeV4Colors.brand[500],
hover: themeV4Colors.brand[600],
disabled: themeV4Colors.gray[300],
disabled2: themeV4Colors.brand[200],
},
'nc-fill-secondary': {
DEFAULT: themeV4Colors.base.white,
hover: themeV4Colors.gray[50],
disabled: themeV4Colors.base.white,
},
'nc-fill-warning': {
DEFAULT: themeV4Colors.red[500],
hover: themeV4Colors.red[600],
disabled: themeV4Colors.gray[50],
},
'nc-fill-success': {
DEFAULT: themeV4Colors.green[500],
hover: themeV4Colors.green[600],
disabled: themeV4Colors.gray[50],
},
'nc-fill-red': {
dark: themeV4Colors.red[700],
medium: themeV4Colors.red[500],
light: themeV4Colors.red[300],
},
'nc-fill-green': {
dark: themeV4Colors.green[700],
medium: themeV4Colors.green[500],
light: themeV4Colors.green[300],
},
'nc-fill-yellow': {
dark: themeV4Colors.yellow[700],
medium: themeV4Colors.yellow[500],
light: themeV4Colors.yellow[300],
},
'nc-fill-blue': {
dark: themeV4Colors.blue[700],
medium: themeV4Colors.blue[500],
light: themeV4Colors.blue[300],
},
'nc-fill-purple': {
dark: themeV4Colors.purple[700],
medium: themeV4Colors.purple[500],
light: themeV4Colors.purple[300],
},
'nc-fill-pink': {
dark: themeV4Colors.pink[700],
medium: themeV4Colors.pink[500],
light: themeV4Colors.pink[300],
},
'nc-fill-orange': {
dark: themeV4Colors.orange[700],
medium: themeV4Colors.orange[500],
light: themeV4Colors.orange[300],
},
'nc-fill-maroon': {
dark: themeV4Colors.maroon[700],
medium: themeV4Colors.maroon[500],
light: themeV4Colors.maroon[300],
},
},
}
export const getAdaptiveTint = (
color: string,
opts?: {
isDarkMode?: boolean
saturationMod?: number
brightnessMod?: number
shade?: number // darker
tint?: number // lighter
},
) => {
const { isDarkMode, shade = 0, tint = 0 } = opts || {}
const { saturationMod = 0, brightnessMod = 0 } = opts || {}
const base = tinycolor(color)
const hsv = base.toHsv()
const isGray = hsv.s < 0.01
let s = hsv.s
let v = hsv.v
//
// --- STEP 1: ORIGINAL BASE LOGIC (unchanged for light mode) ---
//
if (!isDarkMode) {
// LIGHT MODE — keep exactly your original behavior (no extra dark/light here)
const safeS = isGray ? 0 : Math.min(100, 5 + saturationMod)
const safeV = Math.min(100, (isGray ? 97 : 100) + brightnessMod)
s = safeS
v = safeV
} else {
//
// 🌙 DARK MODE — softer, not too dark
//
// Reduce saturation (avoid neon). Target ~4055%
s = isGray ? 0 : hsv.s * 0.45
// Deep darkness curve — compress v to ~0.080.14
v = hsv.v * 0.25 + 0.06 + brightnessMod * 0.005
// Hard clamp for reliable ultra-dark look
v = Math.max(0.08, Math.min(0.14, v))
// gentle shade/tint
if (shade) v = Math.max(0.06, v - shade * 0.004)
if (tint) v = Math.min(0.16, v + tint * 0.004)
}
return tinycolor({ h: hsv.h, s, v }).toHexString()
}
export const getOppositeColorOfBackground = (
background?: string,
preferredText?: string,
fallbackColors: string[] = ['#1f293a', '#101015', '#ffffff'],
) => {
const bg = background || '#cccccc'
const txt = preferredText || '#ffffff'
// If preferred text color is readable on this background
if (tinycolor.isReadable(bg, txt, { level: 'AA', size: 'large' })) {
return tinycolor(txt).toHex8String()
}
// Else choose best fallback between white & black
const fallback = tinycolor.mostReadable(bg, fallbackColors)
return fallback.toHex8String()
}