From 4d127a038bf768eded68aae3cfa5c1b12241bdac Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Fri, 27 Mar 2026 00:55:15 +0100 Subject: chore: normalize line endings --- apps/web/src/utils/color.ts | 364 ++++++++++++++++++++++---------------------- 1 file changed, 182 insertions(+), 182 deletions(-) (limited to 'apps/web/src/utils/color.ts') diff --git a/apps/web/src/utils/color.ts b/apps/web/src/utils/color.ts index 7b01a92e..1176ffd3 100644 --- a/apps/web/src/utils/color.ts +++ b/apps/web/src/utils/color.ts @@ -1,182 +1,182 @@ -import { converter, formatHex, formatHex8, parse, type Rgb } from "culori"; - -export type ColorFormat = "hex" | "rgb" | "hsl" | "hsv"; - -const toRgb = converter("rgb"); -const toHsv = converter("hsv"); -const toHsl = converter("hsl"); - -export function hexToHsv({ hex }: { hex: string }): [number, number, number] { - const color = toHsv(`#${hex}`); - if (!color) return [0, 0, 0]; - return [color.h ?? 0, color.s ?? 0, color.v ?? 0]; -} - -export function hsvToHex({ - h, - s, - v, -}: { - h: number; - s: number; - v: number; -}): string { - const hex = formatHex({ mode: "hsv", h, s, v }); - return hex.slice(1); -} - -export function parseHexAlpha({ hex }: { hex: string }): { - rgb: string; - alpha: number; -} { - const color = parse(`#${hex}`); - const rgbHex = color - ? formatHex(color).slice(1) - : hex.slice(0, 6).toLowerCase(); - return { - rgb: rgbHex, - alpha: color?.alpha ?? 1, - }; -} - -export function appendAlpha({ - rgbHex, - alpha, -}: { - rgbHex: string; - alpha: number; -}): string { - if (alpha >= 1) return rgbHex; - const hex8 = formatHex8({ mode: "rgb", r: 0, g: 0, b: 0, alpha }); - const alphaHex = hex8.slice(7, 9); - return rgbHex + alphaHex; -} - -function stripCssNoise({ text }: { text: string }): string { - let cleaned = text.trim(); - cleaned = cleaned - .replace(/\s*!important\s*/gi, "") - .replace(/;+\s*$/, "") - .trim(); - - const colonIndex = cleaned.indexOf(":"); - const parenIndex = cleaned.indexOf("("); - if (colonIndex !== -1 && (parenIndex === -1 || colonIndex < parenIndex)) { - cleaned = cleaned.slice(colonIndex + 1).trim(); - } - - return cleaned; -} - -function colorToHexWithAlpha({ color }: { color: Rgb }): string { - const hex = formatHex(color).slice(1); - if (color.alpha !== undefined && color.alpha < 1) { - const hex8 = formatHex8(color); - return hex8.slice(1); - } - return hex; -} - -export function extractColorFromText({ - text, -}: { - text: string; -}): string | null { - const cleaned = stripCssNoise({ text }); - - const color = toRgb(cleaned); - if (color) return colorToHexWithAlpha({ color }); - - // bare hex without # (culori needs the prefix) - const bareHexMatch = cleaned.match(/^([0-9a-fA-F]{3,8})$/); - if (bareHexMatch) { - const withHash = toRgb(`#${bareHexMatch[1]}`); - if (withHash) return colorToHexWithAlpha({ color: withHash }); - } - - // fallback: find #hex anywhere in the original text - const embeddedHexMatch = text.match(/#([0-9a-fA-F]{3,8})\b/); - if (embeddedHexMatch) { - const embedded = toRgb(`#${embeddedHexMatch[1]}`); - if (embedded) return colorToHexWithAlpha({ color: embedded }); - } - - return null; -} - -export function formatColorValue({ - hex, - format, -}: { - hex: string; - format: ColorFormat; -}): string { - switch (format) { - case "hex": - return hex; - case "rgb": { - const color = toRgb(`#${hex}`); - if (!color) return hex; - return `${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}`; - } - case "hsl": { - const color = toHsl(`#${hex}`); - if (!color) return hex; - return `${Math.round(color.h ?? 0)}, ${Math.round((color.s ?? 0) * 100)}%, ${Math.round((color.l ?? 0) * 100)}%`; - } - case "hsv": { - const color = toHsv(`#${hex}`); - if (!color) return hex; - return `${Math.round(color.h ?? 0)}, ${Math.round((color.s ?? 0) * 100)}%, ${Math.round((color.v ?? 0) * 100)}%`; - } - } -} - -export function parseColorInput({ - input, - format, -}: { - input: string; - format: ColorFormat; -}): string | null { - switch (format) { - case "hex": { - const cleaned = input.replace("#", ""); - const isValidHex = /^[0-9a-fA-F]{3,8}$/.test(cleaned); - return isValidHex ? cleaned : null; - } - case "rgb": { - const parts = input.split(",").map((part) => parseInt(part.trim(), 10)); - if (parts.length < 3 || parts.some(Number.isNaN)) return null; - const color = { - mode: "rgb" as const, - r: parts[0] / 255, - g: parts[1] / 255, - b: parts[2] / 255, - }; - return formatHex(color).slice(1); - } - case "hsl": { - const parts = input.split(",").map((part) => parseFloat(part.trim())); - if (parts.length < 3 || parts.some(Number.isNaN)) return null; - const color = { - mode: "hsl" as const, - h: parts[0], - s: parts[1] / 100, - l: parts[2] / 100, - }; - return formatHex(color).slice(1); - } - case "hsv": { - const parts = input.split(",").map((part) => parseFloat(part.trim())); - if (parts.length < 3 || parts.some(Number.isNaN)) return null; - const color = { - mode: "hsv" as const, - h: parts[0], - s: parts[1] / 100, - v: parts[2] / 100, - }; - return formatHex(color).slice(1); - } - } -} +import { converter, formatHex, formatHex8, parse, type Rgb } from "culori"; + +export type ColorFormat = "hex" | "rgb" | "hsl" | "hsv"; + +const toRgb = converter("rgb"); +const toHsv = converter("hsv"); +const toHsl = converter("hsl"); + +export function hexToHsv({ hex }: { hex: string }): [number, number, number] { + const color = toHsv(`#${hex}`); + if (!color) return [0, 0, 0]; + return [color.h ?? 0, color.s ?? 0, color.v ?? 0]; +} + +export function hsvToHex({ + h, + s, + v, +}: { + h: number; + s: number; + v: number; +}): string { + const hex = formatHex({ mode: "hsv", h, s, v }); + return hex.slice(1); +} + +export function parseHexAlpha({ hex }: { hex: string }): { + rgb: string; + alpha: number; +} { + const color = parse(`#${hex}`); + const rgbHex = color + ? formatHex(color).slice(1) + : hex.slice(0, 6).toLowerCase(); + return { + rgb: rgbHex, + alpha: color?.alpha ?? 1, + }; +} + +export function appendAlpha({ + rgbHex, + alpha, +}: { + rgbHex: string; + alpha: number; +}): string { + if (alpha >= 1) return rgbHex; + const hex8 = formatHex8({ mode: "rgb", r: 0, g: 0, b: 0, alpha }); + const alphaHex = hex8.slice(7, 9); + return rgbHex + alphaHex; +} + +function stripCssNoise({ text }: { text: string }): string { + let cleaned = text.trim(); + cleaned = cleaned + .replace(/\s*!important\s*/gi, "") + .replace(/;+\s*$/, "") + .trim(); + + const colonIndex = cleaned.indexOf(":"); + const parenIndex = cleaned.indexOf("("); + if (colonIndex !== -1 && (parenIndex === -1 || colonIndex < parenIndex)) { + cleaned = cleaned.slice(colonIndex + 1).trim(); + } + + return cleaned; +} + +function colorToHexWithAlpha({ color }: { color: Rgb }): string { + const hex = formatHex(color).slice(1); + if (color.alpha !== undefined && color.alpha < 1) { + const hex8 = formatHex8(color); + return hex8.slice(1); + } + return hex; +} + +export function extractColorFromText({ + text, +}: { + text: string; +}): string | null { + const cleaned = stripCssNoise({ text }); + + const color = toRgb(cleaned); + if (color) return colorToHexWithAlpha({ color }); + + // bare hex without # (culori needs the prefix) + const bareHexMatch = cleaned.match(/^([0-9a-fA-F]{3,8})$/); + if (bareHexMatch) { + const withHash = toRgb(`#${bareHexMatch[1]}`); + if (withHash) return colorToHexWithAlpha({ color: withHash }); + } + + // fallback: find #hex anywhere in the original text + const embeddedHexMatch = text.match(/#([0-9a-fA-F]{3,8})\b/); + if (embeddedHexMatch) { + const embedded = toRgb(`#${embeddedHexMatch[1]}`); + if (embedded) return colorToHexWithAlpha({ color: embedded }); + } + + return null; +} + +export function formatColorValue({ + hex, + format, +}: { + hex: string; + format: ColorFormat; +}): string { + switch (format) { + case "hex": + return hex; + case "rgb": { + const color = toRgb(`#${hex}`); + if (!color) return hex; + return `${Math.round(color.r * 255)}, ${Math.round(color.g * 255)}, ${Math.round(color.b * 255)}`; + } + case "hsl": { + const color = toHsl(`#${hex}`); + if (!color) return hex; + return `${Math.round(color.h ?? 0)}, ${Math.round((color.s ?? 0) * 100)}%, ${Math.round((color.l ?? 0) * 100)}%`; + } + case "hsv": { + const color = toHsv(`#${hex}`); + if (!color) return hex; + return `${Math.round(color.h ?? 0)}, ${Math.round((color.s ?? 0) * 100)}%, ${Math.round((color.v ?? 0) * 100)}%`; + } + } +} + +export function parseColorInput({ + input, + format, +}: { + input: string; + format: ColorFormat; +}): string | null { + switch (format) { + case "hex": { + const cleaned = input.replace("#", ""); + const isValidHex = /^[0-9a-fA-F]{3,8}$/.test(cleaned); + return isValidHex ? cleaned : null; + } + case "rgb": { + const parts = input.split(",").map((part) => parseInt(part.trim(), 10)); + if (parts.length < 3 || parts.some(Number.isNaN)) return null; + const color = { + mode: "rgb" as const, + r: parts[0] / 255, + g: parts[1] / 255, + b: parts[2] / 255, + }; + return formatHex(color).slice(1); + } + case "hsl": { + const parts = input.split(",").map((part) => parseFloat(part.trim())); + if (parts.length < 3 || parts.some(Number.isNaN)) return null; + const color = { + mode: "hsl" as const, + h: parts[0], + s: parts[1] / 100, + l: parts[2] / 100, + }; + return formatHex(color).slice(1); + } + case "hsv": { + const parts = input.split(",").map((part) => parseFloat(part.trim())); + if (parts.length < 3 || parts.some(Number.isNaN)) return null; + const color = { + mode: "hsv" as const, + h: parts[0], + s: parts[1] / 100, + v: parts[2] / 100, + }; + return formatHex(color).slice(1); + } + } +} -- cgit v1.3.1