mirror of https://github.com/Mai-with-u/MaiBot.git
204 lines
5.1 KiB
TypeScript
204 lines
5.1 KiB
TypeScript
import type { ColorTokens } from './tokens'
|
|
|
|
type HSL = {
|
|
h: number
|
|
s: number
|
|
l: number
|
|
}
|
|
|
|
const clamp = (value: number, min: number, max: number): number => {
|
|
if (value < min) return min
|
|
if (value > max) return max
|
|
return value
|
|
}
|
|
|
|
const roundToTenth = (value: number): number => Math.round(value * 10) / 10
|
|
|
|
const wrapHue = (value: number): number => ((value % 360) + 360) % 360
|
|
|
|
export const parseHSL = (hslStr: string): HSL => {
|
|
const cleaned = hslStr
|
|
.trim()
|
|
.replace(/^hsl\(/i, '')
|
|
.replace(/\)$/i, '')
|
|
.replace(/,/g, ' ')
|
|
const parts = cleaned.split(/\s+/).filter(Boolean)
|
|
const rawH = parts[0] ?? '0'
|
|
const rawS = parts[1] ?? '0%'
|
|
const rawL = parts[2] ?? '0%'
|
|
|
|
const h = Number.parseFloat(rawH)
|
|
const s = Number.parseFloat(rawS.replace('%', ''))
|
|
const l = Number.parseFloat(rawL.replace('%', ''))
|
|
|
|
return {
|
|
h: Number.isNaN(h) ? 0 : h,
|
|
s: Number.isNaN(s) ? 0 : s,
|
|
l: Number.isNaN(l) ? 0 : l,
|
|
}
|
|
}
|
|
|
|
export const formatHSL = (h: number, s: number, l: number): string => {
|
|
const safeH = roundToTenth(wrapHue(h))
|
|
const safeS = roundToTenth(clamp(s, 0, 100))
|
|
const safeL = roundToTenth(clamp(l, 0, 100))
|
|
return `${safeH} ${safeS}% ${safeL}%`
|
|
}
|
|
|
|
export const hexToHSL = (hex: string): string => {
|
|
let cleaned = hex.trim().replace('#', '')
|
|
if (cleaned.length === 3) {
|
|
cleaned = cleaned
|
|
.split('')
|
|
.map((char) => `${char}${char}`)
|
|
.join('')
|
|
}
|
|
|
|
if (cleaned.length !== 6) {
|
|
return formatHSL(0, 0, 0)
|
|
}
|
|
|
|
const r = Number.parseInt(cleaned.slice(0, 2), 16) / 255
|
|
const g = Number.parseInt(cleaned.slice(2, 4), 16) / 255
|
|
const b = Number.parseInt(cleaned.slice(4, 6), 16) / 255
|
|
|
|
const max = Math.max(r, g, b)
|
|
const min = Math.min(r, g, b)
|
|
const delta = max - min
|
|
const l = (max + min) / 2
|
|
|
|
let h = 0
|
|
let s = 0
|
|
|
|
if (delta !== 0) {
|
|
s = l > 0.5 ? delta / (2 - max - min) : delta / (max + min)
|
|
switch (max) {
|
|
case r:
|
|
h = (g - b) / delta + (g < b ? 6 : 0)
|
|
break
|
|
case g:
|
|
h = (b - r) / delta + 2
|
|
break
|
|
case b:
|
|
h = (r - g) / delta + 4
|
|
break
|
|
default:
|
|
break
|
|
}
|
|
h *= 60
|
|
}
|
|
|
|
return formatHSL(h, s * 100, l * 100)
|
|
}
|
|
|
|
export const adjustLightness = (hsl: string, amount: number): string => {
|
|
const { h, s, l } = parseHSL(hsl)
|
|
return formatHSL(h, s, l + amount)
|
|
}
|
|
|
|
export const adjustSaturation = (hsl: string, amount: number): string => {
|
|
const { h, s, l } = parseHSL(hsl)
|
|
return formatHSL(h, s + amount, l)
|
|
}
|
|
|
|
export const rotateHue = (hsl: string, degrees: number): string => {
|
|
const { h, s, l } = parseHSL(hsl)
|
|
return formatHSL(h + degrees, s, l)
|
|
}
|
|
|
|
const setLightness = (hsl: string, lightness: number): string => {
|
|
const { h, s } = parseHSL(hsl)
|
|
return formatHSL(h, s, lightness)
|
|
}
|
|
|
|
const setSaturation = (hsl: string, saturation: number): string => {
|
|
const { h, l } = parseHSL(hsl)
|
|
return formatHSL(h, saturation, l)
|
|
}
|
|
|
|
const getReadableForeground = (hsl: string): string => {
|
|
const { h, s, l } = parseHSL(hsl)
|
|
const neutralSaturation = clamp(s * 0.15, 6, 20)
|
|
return l > 60
|
|
? formatHSL(h, neutralSaturation, 10)
|
|
: formatHSL(h, neutralSaturation, 96)
|
|
}
|
|
|
|
export const generatePalette = (accentHSL: string, isDark: boolean): ColorTokens => {
|
|
const accent = parseHSL(accentHSL)
|
|
const primary = formatHSL(accent.h, accent.s, accent.l)
|
|
|
|
const background = isDark ? '222.2 84% 4.9%' : '0 0% 100%'
|
|
const foreground = isDark ? '210 40% 98%' : '222.2 84% 4.9%'
|
|
|
|
const secondary = formatHSL(
|
|
accent.h,
|
|
clamp(accent.s * 0.35, 8, 40),
|
|
isDark ? 17.5 : 96,
|
|
)
|
|
|
|
const muted = formatHSL(
|
|
accent.h,
|
|
clamp(accent.s * 0.12, 2, 18),
|
|
isDark ? 17.5 : 96,
|
|
)
|
|
|
|
const accentVariant = formatHSL(
|
|
accent.h + 35,
|
|
clamp(accent.s * 0.6, 20, 85),
|
|
isDark ? clamp(accent.l * 0.6 + 8, 25, 60) : clamp(accent.l * 0.8 + 14, 40, 75),
|
|
)
|
|
|
|
const destructive = formatHSL(
|
|
0,
|
|
clamp(accent.s, 60, 90),
|
|
isDark ? 30.6 : 60.2,
|
|
)
|
|
|
|
const border = formatHSL(
|
|
accent.h,
|
|
clamp(accent.s * 0.2, 5, 25),
|
|
isDark ? 17.5 : 91.4,
|
|
)
|
|
|
|
const mutedForeground = setSaturation(
|
|
setLightness(muted, isDark ? 65.1 : 46.9),
|
|
clamp(accent.s * 0.2, 10, 30),
|
|
)
|
|
|
|
const chartBase = formatHSL(accent.h, accent.s, accent.l)
|
|
const chartSteps = [0, 72, 144, 216, 288]
|
|
const charts = chartSteps.map((step) => rotateHue(chartBase, step))
|
|
|
|
const card = adjustLightness(background, isDark ? 2 : -1)
|
|
const popover = adjustLightness(background, isDark ? 3 : -0.5)
|
|
|
|
return {
|
|
primary,
|
|
'primary-foreground': getReadableForeground(primary),
|
|
'primary-gradient': 'none',
|
|
secondary,
|
|
'secondary-foreground': getReadableForeground(secondary),
|
|
muted,
|
|
'muted-foreground': mutedForeground,
|
|
accent: accentVariant,
|
|
'accent-foreground': getReadableForeground(accentVariant),
|
|
destructive,
|
|
'destructive-foreground': getReadableForeground(destructive),
|
|
background,
|
|
foreground,
|
|
card,
|
|
'card-foreground': foreground,
|
|
popover,
|
|
'popover-foreground': foreground,
|
|
border,
|
|
input: border,
|
|
ring: primary,
|
|
'chart-1': charts[0],
|
|
'chart-2': charts[1],
|
|
'chart-3': charts[2],
|
|
'chart-4': charts[3],
|
|
'chart-5': charts[4],
|
|
}
|
|
}
|