From 91dc737a35e8a421da075cc5d670c66de06cc64e Mon Sep 17 00:00:00 2001 From: silverwind Date: Fri, 20 Feb 2026 16:43:01 +0100 Subject: [PATCH] Replace `tinycolor2` with `colord` (#36673) [`colord`](https://github.com/omgovich/colord) is significantly smaller than [`tinycolor2`](https://github.com/bgrins/TinyColor) (~4KB vs ~29KB minified) and ships its own TypeScript types, removing the need for `@types/tinycolor2`. Behaviour is exactly the same for our use cases. By using `.alpha(1)` we force the function to always output 6-digit hex format (it would output 8-digit for non-opaque colors). --------- Signed-off-by: silverwind Co-authored-by: Claude Opus 4.6 --- package.json | 3 +-- pnpm-lock.yaml | 19 +++---------------- web_src/js/features/codeeditor.ts | 4 ++-- web_src/js/utils/color.ts | 15 +++++++-------- 4 files changed, 13 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index 9c5116d6b5f..a7792c5fee4 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "swagger-ui-dist": "5.31.1", "tailwindcss": "3.4.17", "throttle-debounce": "5.0.2", - "tinycolor2": "1.6.0", + "colord": "2.9.3", "tippy.js": "6.3.7", "toastify-js": "1.12.0", "tributejs": "5.1.3", @@ -82,7 +82,6 @@ "@types/sortablejs": "1.15.9", "@types/swagger-ui-dist": "3.30.6", "@types/throttle-debounce": "5.0.2", - "@types/tinycolor2": "1.4.6", "@types/toastify-js": "1.12.4", "@typescript-eslint/parser": "8.56.0", "@vitejs/plugin-vue": "6.0.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 383e0c8c2ed..e67da15ad33 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -89,6 +89,9 @@ importers: clippie: specifier: 4.1.10 version: 4.1.10 + colord: + specifier: 2.9.3 + version: 2.9.3 compare-versions: specifier: 6.1.1 version: 6.1.1 @@ -164,9 +167,6 @@ importers: throttle-debounce: specifier: 5.0.2 version: 5.0.2 - tinycolor2: - specifier: 1.6.0 - version: 1.6.0 tippy.js: specifier: 6.3.7 version: 6.3.7 @@ -249,9 +249,6 @@ importers: '@types/throttle-debounce': specifier: 5.0.2 version: 5.0.2 - '@types/tinycolor2': - specifier: 1.4.6 - version: 1.4.6 '@types/toastify-js': specifier: 1.12.4 version: 1.12.4 @@ -1297,9 +1294,6 @@ packages: '@types/throttle-debounce@5.0.2': resolution: {integrity: sha512-pDzSNulqooSKvSNcksnV72nk8p7gRqN8As71Sp28nov1IgmPKWbOEIwAWvBME5pPTtaXJAvG3O4oc76HlQ4kqQ==} - '@types/tinycolor2@1.4.6': - resolution: {integrity: sha512-iEN8J0BoMnsWBqjVbWH/c0G0Hh7O21lpR2/+PrvAVgWdzL7eexIFm4JN/Wn10PTcmNdtS6U67r499mlWMXOxNw==} - '@types/toastify-js@1.12.4': resolution: {integrity: sha512-zfZHU4tKffPCnZRe7pjv/eFKzTVHozKewFCKaCjZ4gFinKgJRz/t0bkZiMCXJxPhv/ZoeDGNOeRD09R0kQZ/nw==} @@ -4056,9 +4050,6 @@ packages: tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinycolor2@1.6.0: - resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} - tinyexec@1.0.2: resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} engines: {node: '>=18'} @@ -5241,8 +5232,6 @@ snapshots: '@types/throttle-debounce@5.0.2': {} - '@types/tinycolor2@1.4.6': {} - '@types/toastify-js@1.12.4': {} '@types/trusted-types@2.0.7': @@ -8215,8 +8204,6 @@ snapshots: tinybench@2.9.0: {} - tinycolor2@1.6.0: {} - tinyexec@1.0.2: {} tinyglobby@0.2.15: diff --git a/web_src/js/features/codeeditor.ts b/web_src/js/features/codeeditor.ts index 47f378c47ac..b2aa9ea1c5e 100644 --- a/web_src/js/features/codeeditor.ts +++ b/web_src/js/features/codeeditor.ts @@ -1,4 +1,4 @@ -import tinycolor from 'tinycolor2'; +import {colord} from 'colord'; import {basename, extname, isObject, isDarkTheme} from '../utils.ts'; import {onInputDebounce} from '../utils/dom.ts'; import type MonacoNamespace from 'monaco-editor'; @@ -94,7 +94,7 @@ function updateTheme(monaco: Monaco): void { // https://github.com/microsoft/monaco-editor/issues/2427 // also, monaco can only parse 6-digit hex colors, so we convert the colors to that format const styles = window.getComputedStyle(document.documentElement); - const getColor = (name: string) => tinycolor(styles.getPropertyValue(name).trim()).toString('hex6'); + const getColor = (name: string) => colord(styles.getPropertyValue(name).trim()).alpha(1).toHex(); monaco.editor.defineTheme('gitea', { base: isDarkTheme() ? 'vs-dark' : 'vs', diff --git a/web_src/js/utils/color.ts b/web_src/js/utils/color.ts index 57c909b8a0b..096356983a8 100644 --- a/web_src/js/utils/color.ts +++ b/web_src/js/utils/color.ts @@ -1,22 +1,21 @@ -import tinycolor from 'tinycolor2'; -import type {ColorInput} from 'tinycolor2'; +import {colord} from 'colord'; +import type {AnyColor} from 'colord'; /** Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance */ // Keep this in sync with modules/util/color.go -function getRelativeLuminance(color: ColorInput): number { - const {r, g, b} = tinycolor(color).toRgb(); +function getRelativeLuminance(color: AnyColor): number { + const {r, g, b} = colord(color).toRgb(); return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255; } -function useLightText(backgroundColor: ColorInput): boolean { +function useLightText(backgroundColor: AnyColor): boolean { return getRelativeLuminance(backgroundColor) < 0.453; } -/** Given a background color, returns a black or white foreground color that the highest - * contrast ratio. */ +/** Given a background color, returns a black or white foreground color with the highest contrast ratio. */ // In the future, the APCA contrast function, or CSS `contrast-color` will be better. // https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42 -export function contrastColor(backgroundColor: ColorInput): string { +export function contrastColor(backgroundColor: AnyColor): string { return useLightText(backgroundColor) ? '#fff' : '#000'; }