Colorised spreadsheet (#1643)

* feat: colorise spreadSheet with rundown color

* feat: add colorisation in spreadsheet for block

* feat: border color

* fix: cleanup accessible color

* fix: review before merge (https://github.com/cpvalente/ontime/pull/1608)

* fix: usgae of map instead foreach (https://github.com/cpvalente/ontime/pull/1608)

* Update apps/server/src/services/sheet-service/SheetService.ts

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

* fix: comment condition

* fix: Remove Color plugin (but don't can get color like 'red' or 'black')

* fix: isLight for more clean code

* fix: remove map on isLight function

* fix: remove debug logger

* refactor colour

* lint

---------

Co-authored-by: Gwenitora <gwenitora.gardu74@gmail.com>
Co-authored-by: Gwenitora <61121870+Gwenitora@users.noreply.github.com>
Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
This commit is contained in:
Alex Christoffer Rasmussen
2025-06-24 22:38:03 +02:00
committed by GitHub
parent 984dcb0181
commit 4872cd0a48
14 changed files with 350 additions and 252 deletions
-2
View File
@@ -21,7 +21,6 @@
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"autosize": "^6.0.1", "autosize": "^6.0.1",
"axios": "^1.9.0", "axios": "^1.9.0",
"color": "^4.2.3",
"csv-stringify": "^6.4.5", "csv-stringify": "^6.4.5",
"framer-motion": "^10.10.0", "framer-motion": "^10.10.0",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
@@ -67,7 +66,6 @@
"devDependencies": { "devDependencies": {
"@sentry/vite-plugin": "^2.16.1", "@sentry/vite-plugin": "^2.16.1",
"@tanstack/eslint-plugin-query": "^5.8.4", "@tanstack/eslint-plugin-query": "^5.8.4",
"@types/color": "^3.0.3",
"@types/prismjs": "^1.26.5", "@types/prismjs": "^1.26.5",
"@types/react": "^18.0.26", "@types/react": "^18.0.26",
"@types/react-dom": "^18.0.10", "@types/react-dom": "^18.0.10",
@@ -18,19 +18,19 @@ describe('getAccessibleColour()', () => {
it('handles named colours', () => { it('handles named colours', () => {
const colour = 'red'; const colour = 'red';
const { backgroundColor, color } = getAccessibleColour(colour); const { backgroundColor, color } = getAccessibleColour(colour);
expect(backgroundColor).toBe('#FF0000FF'); expect(backgroundColor).toBe('#ff0000ff');
expect(color).toBe('#fffffa'); expect(color).toBe('#fffffa');
}); });
it('handles hex colours', () => { it('handles hex colours', () => {
const colour = '#0F0'; const colour = '#0F0';
const { backgroundColor, color } = getAccessibleColour(colour); const { backgroundColor, color } = getAccessibleColour(colour);
expect(backgroundColor).toBe('#00FF00FF'); expect(backgroundColor).toBe('#00ff00ff');
expect(color).toBe('black'); expect(color).toBe('black');
}); });
it('handles transparens', () => { it('handles transparens', () => {
const colour = '#0F08'; const colour = '#0F08';
const { backgroundColor, color } = getAccessibleColour(colour); const { backgroundColor, color } = getAccessibleColour(colour);
expect(backgroundColor).toBe('#0C940CFF'); expect(backgroundColor).toBe('#0c940cff');
expect(color).toBe('#fffffa'); expect(color).toBe('#fffffa');
}); });
}); });
+16 -19
View File
@@ -1,27 +1,27 @@
import Color from 'color'; import { RGBColour } from 'ontime-types';
import { colourToHex, cssOrHexToColour, isLightColour, mixColours } from 'ontime-utils';
type ColourCombination = { type ColourCombination = {
backgroundColor: string; backgroundColor: string;
color: string; color: string;
}; };
const defaultUiBackground: RGBColour = { red: 26, green: 26, blue: 26, alpha: 1 };
/** /**
* @description Selects text colour to maintain accessible contrast * @description Selects text colour to maintain accessible contrast
* @param bgColour * @param bgColour
* @return {{backgroundColor, color: string}} * @return {{backgroundColor, color: string}}
*/ */
export const getAccessibleColour = (bgColour?: string): ColourCombination => { export const getAccessibleColour = (bgColour?: string): ColourCombination => {
if (bgColour) { if (!bgColour) return { backgroundColor: '#1a1a1a', color: '#fffffa' };
try {
const originalColour = Color(bgColour); const originalColour = cssOrHexToColour(bgColour);
const backgroundColorMix = originalColour.alpha(1).mix(Color('#1a1a1a'), 1 - originalColour.alpha()); if (!originalColour) return { backgroundColor: '#1a1a1a', color: '#fffffa' };
const textColor = backgroundColorMix.isLight() ? 'black' : '#fffffa';
return { backgroundColor: backgroundColorMix.hexa(), color: textColor }; const backgroundColorMix = mixColours(defaultUiBackground, originalColour, 1 - originalColour.alpha);
} catch (_error) { const textColor = isLightColour(backgroundColorMix) ? 'black' : '#fffffa';
/* we do not handle errors here */
} return { backgroundColor: colourToHex(backgroundColorMix), color: textColor };
}
return { backgroundColor: '#1a1a1a', color: '#fffffa' };
}; };
/** /**
@@ -39,11 +39,8 @@ export const timerPlaceholderMin = '––:––';
* Adds opacity to a given colour if possible * Adds opacity to a given colour if possible
*/ */
export function alpha(colour: string, amount: number): string { export function alpha(colour: string, amount: number): string {
try { const originalColour = cssOrHexToColour(colour);
const withAlpha = Color(colour).alpha(amount).hexa(); if (!originalColour) return colour;
return withAlpha; originalColour.alpha = amount;
} catch (_error) { return colourToHex(originalColour);
/* we do not handle errors here */
}
return colour;
} }
@@ -1,7 +1,7 @@
import { MutableRefObject } from 'react'; import { MutableRefObject } from 'react';
import { RowModel, Table } from '@tanstack/react-table'; import { RowModel, Table } from '@tanstack/react-table';
import Color from 'color';
import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeEntry } from 'ontime-types'; import { isOntimeBlock, isOntimeDelay, isOntimeEvent, OntimeEntry } from 'ontime-types';
import { colourToHex, cssOrHexToColour } from 'ontime-utils';
import { useSelectedEventId } from '../../../../common/hooks/useSocket'; import { useSelectedEventId } from '../../../../common/hooks/useSocket';
import { lazyEvaluate } from '../../../../common/utils/lazyEvaluate'; import { lazyEvaluate } from '../../../../common/utils/lazyEvaluate';
@@ -76,12 +76,13 @@ export default function CuesheetBody(props: CuesheetBodyProps) {
if (isSelected) { if (isSelected) {
rowBgColour = '#D20300'; // $red-700 rowBgColour = '#D20300'; // $red-700
} else if (entry.colour) { } else if (entry.colour) {
try { // the colour is user defined and might be invalid
// the colour is user defined and might be invalid const accessibleBackgroundColor = cssOrHexToColour(getAccessibleColour(entry.colour).backgroundColor);
const accessibleBackgroundColor = Color(getAccessibleColour(entry.colour).backgroundColor); if (accessibleBackgroundColor !== null) {
rowBgColour = accessibleBackgroundColor.fade(0.75).hexa(); rowBgColour = colourToHex({
} catch (_error) { ...accessibleBackgroundColor,
/* we do not handle errors here */ alpha: accessibleBackgroundColor.alpha * 0.25,
});
} }
} }
@@ -1,8 +1,8 @@
import { memo, MutableRefObject, useLayoutEffect, useRef, useState } from 'react'; import { memo, MutableRefObject, useLayoutEffect, useRef, useState } from 'react';
import { IoEllipsisHorizontal } from 'react-icons/io5'; import { IoEllipsisHorizontal } from 'react-icons/io5';
import { flexRender, Table } from '@tanstack/react-table'; import { flexRender, Table } from '@tanstack/react-table';
import Color from 'color'; import { OntimeEntry, OntimeEvent, RGBColour } from 'ontime-types';
import { OntimeEntry, OntimeEvent } from 'ontime-types'; import { colourToHex, cssOrHexToColour } from 'ontime-utils';
import IconButton from '../../../../common/components/buttons/IconButton'; import IconButton from '../../../../common/components/buttons/IconButton';
import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils'; import { cx, getAccessibleColour } from '../../../../common/utils/styleUtils';
@@ -75,7 +75,8 @@ function EventRow(props: EventRowProps) {
}, [ownRef, selectedRef]); }, [ownRef, selectedRef]);
const { color, backgroundColor } = getAccessibleColour(event.colour); const { color, backgroundColor } = getAccessibleColour(event.colour);
const mutedText = Color(color).fade(0.4).hexa(); const tmpColour = cssOrHexToColour(color) as RGBColour; // we know this to be a correct colour
const mutedText = colourToHex({ ...tmpColour, alpha: tmpColour.alpha * 0.6 });
return ( return (
<tr <tr
@@ -211,16 +211,18 @@ async function verifySheet(
spreadsheetId: sheetId, spreadsheetId: sheetId,
includeGridData: false, includeGridData: false,
}); });
const worksheets = spreadsheets.data.sheets?.forEach((sheet) => {
const worksheets: string[] = [];
spreadsheets.data.sheets?.forEach((sheet) => {
if (sheet.properties?.title) { if (sheet.properties?.title) {
return sheet.properties.title; worksheets.push(sheet.properties.title);
} }
}); });
if (!worksheets) { if (worksheets.length === 0) {
throw new Error('No worksheets found'); throw new Error('No worksheets found');
} }
return worksheets; return { worksheetOptions: worksheets };
} catch (error) { } catch (error) {
// attempt to catch errors caused by importing xlsx // attempt to catch errors caused by importing xlsx
catchCommonImportXlsxError(error); catchCommonImportXlsxError(error);
@@ -293,7 +295,17 @@ async function verifyWorksheet(sheetId: string, worksheet: string): Promise<{ wo
if (!selectedWorksheet) { if (!selectedWorksheet) {
throw new Error('Could not find worksheet'); throw new Error('Could not find worksheet');
} }
if (!selectedWorksheet.properties || !selectedWorksheet.properties.sheetId) { /*
The first spreadsheet provided by google sheet has an id = 0,
so !0 returns true, the only other number that returns true in this setup is NaN,
so if x !== 0 && x !== NaN, then !x returns false, we indeed want !NaN to return true,
but we would like !0 to return false, reason why is also checked that the id is not 0,
because if it is 0, then I should not enter the condition.
*/
if (
!selectedWorksheet.properties ||
(!selectedWorksheet.properties.sheetId && selectedWorksheet.properties.sheetId !== 0)
) {
throw new Error('Got invalid data from worksheet'); throw new Error('Got invalid data from worksheet');
} }
@@ -274,6 +274,6 @@ describe('cellRequestFromEvent()', () => {
const result2 = cellRequestFromEvent(event, 10, 1234, metadata); const result2 = cellRequestFromEvent(event, 10, 1234, metadata);
expect(result2.updateCells?.start?.rowIndex).toStrictEqual(21); expect(result2.updateCells?.start?.rowIndex).toStrictEqual(21);
expect(result2.updateCells?.start?.columnIndex).toStrictEqual(5); expect(result2.updateCells?.start?.columnIndex).toStrictEqual(5);
expect(result2.updateCells?.fields).toStrictEqual('userEnteredValue'); expect(result2.updateCells?.fields).toStrictEqual('userEnteredValue,userEnteredFormat');
}); });
}); });
@@ -1,5 +1,5 @@
import { isOntimeBlock, isOntimeEvent, OntimeEvent, OntimeEntry } from 'ontime-types'; import { isOntimeBlock, isOntimeEvent, OntimeEvent, OntimeEntry, RGBColour } from 'ontime-types';
import { millisToString } from 'ontime-utils'; import { cssOrHexToColour, isLightColour, millisToString, mixColours } from 'ontime-utils';
import type { sheets_v4 } from '@googleapis/sheets'; import type { sheets_v4 } from '@googleapis/sheets';
import { is } from '../../utils/is.js'; import { is } from '../../utils/is.js';
@@ -100,8 +100,26 @@ export function cellRequestFromEvent(
} }
} }
const colors = isOntimeEvent(event) || isOntimeBlock(event) ? getAccessibleColour(event.colour) : undefined;
const cellColor: sheets_v4.Schema$CellData = !colors
? {}
: {
userEnteredFormat: {
backgroundColor: toSheetColourLevel(colors.background),
textFormat: {
foregroundColor: toSheetColourLevel(colors.text),
},
borders: {
bottom: {
style: 'SOLID',
color: toSheetColourLevel(colors.border),
},
},
},
};
const returnRows: sheets_v4.Schema$CellData[] = rowData.map(([key, _]) => { const returnRows: sheets_v4.Schema$CellData[] = rowData.map(([key, _]) => {
return getCellData(key, event); return { ...getCellData(key, event), ...cellColor };
}); });
return { return {
@@ -111,7 +129,7 @@ export function cellRequestFromEvent(
rowIndex: index + rowData[0][1]['row'] + 1, rowIndex: index + rowData[0][1]['row'] + 1,
columnIndex: titleCol, columnIndex: titleCol,
}, },
fields: 'userEnteredValue', fields: 'userEnteredValue,userEnteredFormat',
rows: [ rows: [
{ {
values: returnRows, values: returnRows,
@@ -156,3 +174,29 @@ function getCellData(key: keyof OntimeEvent | 'blank', event: OntimeEntry) {
return {}; return {};
} }
type googleSheetCellColour = {
background: RGBColour;
text: RGBColour;
border: RGBColour;
};
function getAccessibleColour(bgColour?: string): googleSheetCellColour | undefined {
if (!bgColour) return undefined;
const background = cssOrHexToColour(bgColour);
if (!background) return undefined;
const text = isLightColour(background) ? BLACK : WHITE;
const border = mixColours(background, text, 0.2);
return { background, text, border };
}
const BLACK: RGBColour = { red: 0, green: 0, blue: 0, alpha: 1 };
const WHITE: RGBColour = { red: 255, green: 255, blue: 255, alpha: 0.98 };
// sheets use color values from 0 to 1
function toSheetColourLevel(colour: RGBColour): RGBColour {
return { red: colour.red / 255, green: colour.green / 255, blue: colour.blue / 255, alpha: colour.alpha };
}
+2 -154
View File
@@ -1,4 +1,4 @@
import { isColourHex } from 'ontime-utils'; import { CssColours, isColourHex } from 'ontime-utils';
/** /**
* @description Converts a value to an item in the provided enume. * @description Converts a value to an item in the provided enume.
@@ -95,160 +95,8 @@ export function coerceColour(value: unknown): string {
if (lowerCaseValue === '') { if (lowerCaseValue === '') {
return lowerCaseValue; // None colour the same as the UI 'Ø' button return lowerCaseValue; // None colour the same as the UI 'Ø' button
} }
if (!(lowerCaseValue in cssColours)) { if (!(lowerCaseValue in CssColours)) {
throw new Error('Invalid colour name received'); throw new Error('Invalid colour name received');
} }
return lowerCaseValue; return lowerCaseValue;
} }
//https://developer.mozilla.org/en-US/docs/Web/CSS/named-color
const cssColours = {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgreen: '#006400',
darkgrey: '#a9a9a9',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
goldenrod: '#daa520',
gold: '#ffd700',
gray: '#808080',
green: '#008000',
greenyellow: '#adff2f',
grey: '#808080',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavenderblush: '#fff0f5',
lavender: '#e6e6fa',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgreen: '#90ee90',
lightgrey: '#d3d3d3',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd3',
} as const;
@@ -0,0 +1 @@
export type RGBColour = { [key in 'red' | 'green' | 'blue' | 'alpha']: number };
+3
View File
@@ -122,3 +122,6 @@ export {
isTimerLifeCycle, isTimerLifeCycle,
} from './utils/guards.js'; } from './utils/guards.js';
export type { MaybeNumber, MaybeString } from './utils/utils.type.js'; export type { MaybeNumber, MaybeString } from './utils/utils.type.js';
// Colour
export type { RGBColour } from './definitions/Colour.type.js';
+10
View File
@@ -90,3 +90,13 @@ export {
} from './src/feature/spreadsheet-import/spreadsheetImport.js'; } from './src/feature/spreadsheet-import/spreadsheetImport.js';
export { isPlaybackActive } from './src/playback-utils/playbackstate.js'; export { isPlaybackActive } from './src/playback-utils/playbackstate.js';
//Colour
export {
colourToHex,
cssOrHexToColour,
hexToColour,
isLightColour,
mixColours,
CssColours,
} from './src/colour/colour.utils.js';
+237
View File
@@ -0,0 +1,237 @@
import type { RGBColour } from 'ontime-types';
import { isColourHex } from '../regex-utils/isColourHex.js';
// naive colour mix
export function mixColours(colour1: RGBColour, colour2: RGBColour, p: number = 0.5) {
const w1 = p;
const w2 = 1 - w1;
return {
red: Math.round(colour1.red * w1 + colour2.red * w2),
green: Math.round(colour1.green * w1 + colour2.green * w2),
blue: Math.round(colour1.blue * w1 + colour2.blue * w2),
alpha: 1,
};
}
export function colourToHex(colour: RGBColour): string {
const alpha = Math.round(colour.alpha * 255)
.toString(16)
.padStart(2, '0');
const red = colour.red.toString(16).padStart(2, '0');
const green = colour.green.toString(16).padStart(2, '0');
const blue = colour.blue.toString(16).padStart(2, '0');
return '#' + red + green + blue + alpha;
}
export function cssOrHexToColour(colour: string): RGBColour | null {
if (colour.startsWith('#')) return hexToColour(colour);
const maybeCssColour = colour.toLocaleLowerCase();
if (maybeCssColour in CssColours) {
return hexToColour(CssColours[maybeCssColour]);
}
return null;
}
export function hexToColour(hexColour: string): RGBColour | null {
if (!isColourHex(hexColour)) return null;
let hex = hexColour.toLocaleLowerCase();
hex = hex.replace(/^#/, '');
let alpha = 1;
// full length hex #FFFFFFFF
if (hex.length === 8) {
const alphaPart = hex.slice(6, 8);
alpha = parseInt(alphaPart, 16) / 255;
hex = hex.slice(0, 6);
}
// compressed hex with alpha #FFFF
if (hex.length === 4) {
const alphaPart = hex.slice(3, 4).repeat(2);
alpha = parseInt(alphaPart, 16) / 255;
hex = hex.slice(0, 3);
}
// compressed hex without alpha or after the alpha channel has been removed
// here all the values duplicated to create a 6 length hex value
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
const number = parseInt(hex, 16);
const red = number >> 16;
const green = (number >> 8) & 255;
const blue = number & 255;
if (isNaN(red) || isNaN(green) || isNaN(blue) || isNaN(alpha)) return null;
return { red, green, blue, alpha };
}
/**
* Calculates if black or white is the best contras
*
* @returns true if the colour is light -> the best contrast is black
*
* is done with YIQ calculation
* @link https://24ways.org/2010/calculating-color-contrast
*/
export function isLightColour(colour: RGBColour): boolean {
const yiq = (colour.red * 299 + colour.green * 587 + colour.blue * 114) / 1000;
return yiq >= 128;
}
//https://developer.mozilla.org/en-US/docs/Web/CSS/named-color
export const CssColours: Record<string, string> = {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgreen: '#006400',
darkgrey: '#a9a9a9',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
goldenrod: '#daa520',
gold: '#ffd700',
gray: '#808080',
green: '#008000',
greenyellow: '#adff2f',
grey: '#808080',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavenderblush: '#fff0f5',
lavender: '#e6e6fa',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgreen: '#90ee90',
lightgrey: '#d3d3d3',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd3',
} as const;
-54
View File
@@ -137,9 +137,6 @@ importers:
axios: axios:
specifier: ^1.9.0 specifier: ^1.9.0
version: 1.9.0 version: 1.9.0
color:
specifier: ^4.2.3
version: 4.2.3
csv-stringify: csv-stringify:
specifier: ^6.4.5 specifier: ^6.4.5
version: 6.4.5 version: 6.4.5
@@ -189,9 +186,6 @@ importers:
'@tanstack/eslint-plugin-query': '@tanstack/eslint-plugin-query':
specifier: ^5.8.4 specifier: ^5.8.4
version: 5.8.4(eslint@8.56.0)(typescript@5.5.3) version: 5.8.4(eslint@8.56.0)(typescript@5.5.3)
'@types/color':
specifier: ^3.0.3
version: 3.0.3
'@types/prismjs': '@types/prismjs':
specifier: ^1.26.5 specifier: ^1.26.5
version: 1.26.5 version: 1.26.5
@@ -2234,15 +2228,6 @@ packages:
'@types/chai@5.2.2': '@types/chai@5.2.2':
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
'@types/color-convert@2.0.0':
resolution: {integrity: sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==}
'@types/color-name@1.1.1':
resolution: {integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==}
'@types/color@3.0.3':
resolution: {integrity: sha512-X//qzJ3d3Zj82J9sC/C18ZY5f43utPbAJ6PhYt/M7uG6etcF6MRpKdN880KBy43B0BMzSfeT96MzrsNjFI3GbA==}
'@types/connect@3.4.35': '@types/connect@3.4.35':
resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==} resolution: {integrity: sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==}
@@ -2887,16 +2872,9 @@ packages:
color-name@1.1.4: color-name@1.1.4:
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
color-string@1.9.1:
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
color2k@2.0.2: color2k@2.0.2:
resolution: {integrity: sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w==} resolution: {integrity: sha512-kJhwH5nAwb34tmyuqq/lgjEKzlFXn1U99NlnB6Ws4qVaERcRUYeYP1cBw6BJ4vxaWStAUEef4WMr7WjOCnBt8w==}
color@4.2.3:
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
engines: {node: '>=12.5.0'}
combined-stream@1.0.8: combined-stream@1.0.8:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
@@ -3898,9 +3876,6 @@ packages:
is-arrayish@0.2.1: is-arrayish@0.2.1:
resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==}
is-arrayish@0.3.2:
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
is-bigint@1.0.4: is-bigint@1.0.4:
resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==} resolution: {integrity: sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==}
@@ -5003,9 +4978,6 @@ packages:
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
engines: {node: '>=14'} engines: {node: '>=14'}
simple-swizzle@0.2.2:
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
simple-update-notifier@2.0.0: simple-update-notifier@2.0.0:
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
engines: {node: '>=10'} engines: {node: '>=10'}
@@ -7646,16 +7618,6 @@ snapshots:
dependencies: dependencies:
'@types/deep-eql': 4.0.2 '@types/deep-eql': 4.0.2
'@types/color-convert@2.0.0':
dependencies:
'@types/color-name': 1.1.1
'@types/color-name@1.1.1': {}
'@types/color@3.0.3':
dependencies:
'@types/color-convert': 2.0.0
'@types/connect@3.4.35': '@types/connect@3.4.35':
dependencies: dependencies:
'@types/node': 22.15.26 '@types/node': 22.15.26
@@ -8526,18 +8488,8 @@ snapshots:
color-name@1.1.4: {} color-name@1.1.4: {}
color-string@1.9.1:
dependencies:
color-name: 1.1.4
simple-swizzle: 0.2.2
color2k@2.0.2: {} color2k@2.0.2: {}
color@4.2.3:
dependencies:
color-convert: 2.0.1
color-string: 1.9.1
combined-stream@1.0.8: combined-stream@1.0.8:
dependencies: dependencies:
delayed-stream: 1.0.0 delayed-stream: 1.0.0
@@ -9819,8 +9771,6 @@ snapshots:
is-arrayish@0.2.1: {} is-arrayish@0.2.1: {}
is-arrayish@0.3.2: {}
is-bigint@1.0.4: is-bigint@1.0.4:
dependencies: dependencies:
has-bigints: 1.0.2 has-bigints: 1.0.2
@@ -10935,10 +10885,6 @@ snapshots:
signal-exit@4.1.0: {} signal-exit@4.1.0: {}
simple-swizzle@0.2.2:
dependencies:
is-arrayish: 0.3.2
simple-update-notifier@2.0.0: simple-update-notifier@2.0.0:
dependencies: dependencies:
semver: 7.6.2 semver: 7.6.2