chore: normalize line endings

This commit is contained in:
Maze Winther
2026-03-29 15:56:48 +02:00
parent 82817cb50c
commit 4d127a038b
736 changed files with 73880 additions and 73782 deletions
+182 -182
View File
@@ -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);
}
}
}
+28 -28
View File
@@ -1,28 +1,28 @@
export function generateUUID(): string {
if (
typeof crypto !== "undefined" &&
typeof crypto.randomUUID === "function"
) {
return crypto.randomUUID();
}
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
return (
hex.slice(0, 4).join("") +
"-" +
hex.slice(4, 6).join("") +
"-" +
hex.slice(6, 8).join("") +
"-" +
hex.slice(8, 10).join("") +
"-" +
hex.slice(10, 16).join("")
);
}
export function generateUUID(): string {
if (
typeof crypto !== "undefined" &&
typeof crypto.randomUUID === "function"
) {
return crypto.randomUUID();
}
const bytes = new Uint8Array(16);
crypto.getRandomValues(bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
const hex = [...bytes].map((b) => b.toString(16).padStart(2, "0"));
return (
hex.slice(0, 4).join("") +
"-" +
hex.slice(4, 6).join("") +
"-" +
hex.slice(6, 8).join("") +
"-" +
hex.slice(8, 10).join("") +
"-" +
hex.slice(10, 16).join("")
);
}
+84 -84
View File
@@ -1,84 +1,84 @@
export function clamp({
value,
min,
max,
}: {
value: number;
min: number;
max: number;
}): number {
return Math.max(min, Math.min(max, value));
}
export function clampRound({
value,
min,
max,
}: {
value: number;
min: number;
max: number;
}): number {
return Math.round(clamp({ value, min, max }));
}
function getFractionDigitsForStep({ step }: { step: number }): number {
const normalizedStep = step.toString().toLowerCase();
if (normalizedStep.includes("e-")) {
return Number(normalizedStep.split("e-")[1] ?? 0);
}
const [, fractionalPart = ""] = normalizedStep.split(".");
return fractionalPart.length;
}
export function snapToStep({
value,
step,
}: {
value: number;
step: number;
}): number {
if (step <= 0) return value;
const snappedValue = Math.round(value / step) * step;
return Number(
snappedValue.toFixed(getFractionDigitsForStep({ step })),
);
}
export function isNearlyEqual({
leftValue,
rightValue,
epsilon = 0.0001,
}: {
leftValue: number;
rightValue: number;
epsilon?: number;
}): boolean {
return Math.abs(leftValue - rightValue) <= epsilon;
}
export function formatNumberForDisplay({
value,
maxFractionDigits = 6,
}: {
value: number;
maxFractionDigits?: number;
}): string {
return Number(value.toFixed(maxFractionDigits)).toString();
}
export function evaluateMathExpression({
input,
}: {
input: string;
}): number | null {
const sanitized = input.trim();
if (!/^[\d.\s+\-*/()]+$/.test(sanitized)) return null;
try {
const result = new Function(`return (${sanitized})`)();
if (typeof result !== "number" || !Number.isFinite(result)) return null;
return result;
} catch {
return null;
}
}
export function clamp({
value,
min,
max,
}: {
value: number;
min: number;
max: number;
}): number {
return Math.max(min, Math.min(max, value));
}
export function clampRound({
value,
min,
max,
}: {
value: number;
min: number;
max: number;
}): number {
return Math.round(clamp({ value, min, max }));
}
function getFractionDigitsForStep({ step }: { step: number }): number {
const normalizedStep = step.toString().toLowerCase();
if (normalizedStep.includes("e-")) {
return Number(normalizedStep.split("e-")[1] ?? 0);
}
const [, fractionalPart = ""] = normalizedStep.split(".");
return fractionalPart.length;
}
export function snapToStep({
value,
step,
}: {
value: number;
step: number;
}): number {
if (step <= 0) return value;
const snappedValue = Math.round(value / step) * step;
return Number(
snappedValue.toFixed(getFractionDigitsForStep({ step })),
);
}
export function isNearlyEqual({
leftValue,
rightValue,
epsilon = 0.0001,
}: {
leftValue: number;
rightValue: number;
epsilon?: number;
}): boolean {
return Math.abs(leftValue - rightValue) <= epsilon;
}
export function formatNumberForDisplay({
value,
maxFractionDigits = 6,
}: {
value: number;
maxFractionDigits?: number;
}): string {
return Number(value.toFixed(maxFractionDigits)).toString();
}
export function evaluateMathExpression({
input,
}: {
input: string;
}): number | null {
const sanitized = input.trim();
if (!/^[\d.\s+\-*/()]+$/.test(sanitized)) return null;
try {
const result = new Function(`return (${sanitized})`)();
if (typeof result !== "number" || !Number.isFinite(result)) return null;
return result;
} catch {
return null;
}
}