Service state colours, activity badge colours, readable named colours

This commit is contained in:
2026-08-29 17:00:34 -04:00
parent b26bbd0456
commit 6beb2afc09
5 changed files with 141 additions and 3 deletions

View File

@@ -292,4 +292,38 @@ describe('facet chips on a card that shows a health dot', () => {
it('offers no chips for a panel with no status rollup', () => {
expect(facets({ id: 'x', render: 'list', _data: [{ a: 1 }] })).toEqual([])
})
it('prefers an explicit facets declaration over the status rollup', () => {
// Services carry BOTH: a statecolor status dot and a statelabel facet.
// Chips must follow the declaration, because a service is Running or
// Stopped - "OK" and "Critical" is not how anybody describes one.
const services = {
id: 'endpoints-services',
render: 'list',
status: { key: 'statecolor', levels: { green: 'ok', red: 'critical' } },
facets: { key: 'statelabel', order: ['Running', 'Stopped', 'Paused'] },
_data: [
{ statelabel: 'Running', statecolor: 'green' },
{ statelabel: 'Stopped', statecolor: 'red' },
{ statelabel: 'Paused', statecolor: 'red' },
],
}
const chips = facets(services)
expect(chips.map((c) => c.label)).toEqual(['Running', 'Stopped', 'Paused'])
expect(chips.map((c) => c.label)).not.toContain('Critical')
})
it('gives an unlisted state its own chip rather than dropping the row', () => {
// Windows has transitional states, and an agent can report something the
// order list never anticipated. Those rows must stay reachable.
const services = {
id: 'svc2',
render: 'list',
facets: { key: 'statelabel', order: ['Running', 'Stopped'] },
_data: [{ statelabel: 'Running' }, { statelabel: 'Start Pending' }],
}
const chips = facets(services)
expect(chips.map((c) => c.value)).toEqual(['Running', 'Start Pending'])
expect(facetedRows(services, ['Start Pending']).length).toBe(1)
})
})

View File

@@ -19,10 +19,33 @@ export const PALETTE = [
'#6c757d', // gray
]
// Plugin panels emit colour NAMES, not hex - a services row says 'yellow', an
// activity row says 'gray'. Without this the brightness test cannot parse them
// and falls through to white, which is why an amber badge rendered white text
// on yellow and could not be read. Green, red and grey are dark enough that
// white worked by luck, so the fault only showed when amber was introduced.
const NAMED_COLORS = {
red: '#dc3545',
green: '#28a745',
yellow: '#ffc107',
amber: '#ffc107',
orange: '#fd7e14',
blue: '#4181ff',
grey: '#6c757d',
gray: '#6c757d',
black: '#000000',
white: '#ffffff',
}
export function resolveColor(color) {
if (!color || typeof color !== 'string') return color
return NAMED_COLORS[color.trim().toLowerCase()] || color
}
// Black or white text for a given background, by perceived brightness.
export function readableText(bg) {
if (!bg || typeof bg !== 'string') return '#ffffff'
let hex = bg.trim().replace('#', '')
let hex = resolveColor(bg).trim().replace('#', '')
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('')
if (hex.length !== 6) return '#ffffff'
const r = parseInt(hex.slice(0, 2), 16)
@@ -34,6 +57,8 @@ export function readableText(bg) {
// Style object for a badge/pill from a stored color (with a neutral fallback).
export function colorStyle(color, fallback = '#6c757d') {
const backgroundColor = color || fallback
// Resolve the name for the BACKGROUND too, so the swatch and the text
// contrast are computed from the same value.
const backgroundColor = resolveColor(color) || fallback
return { backgroundColor, color: readableText(backgroundColor) }
}

View File

@@ -0,0 +1,27 @@
import { describe, it, expect } from 'vitest'
import { readableText, colorStyle } from './colorStyle'
describe('named colours', () => {
// Panels emit names, not hex. Before these resolved, the brightness test
// could not parse them and returned white for everything - which is how an
// amber badge shipped as white-on-yellow.
it('gives yellow dark text, not white', () => {
expect(readableText('yellow')).toBe('#1a1a1a')
expect(colorStyle('yellow').color).toBe('#1a1a1a')
})
it('still gives dark colours white text', () => {
expect(readableText('red')).toBe('#ffffff')
expect(readableText('green')).toBe('#ffffff')
expect(readableText('gray')).toBe('#ffffff')
})
it('resolves the name for the background as well as the text', () => {
expect(colorStyle('yellow').backgroundColor).toBe('#ffc107')
})
it('leaves hex and unknown values alone', () => {
expect(colorStyle('#ffffff').color).toBe('#1a1a1a')
expect(colorStyle('rebeccapurple').backgroundColor).toBe('rebeccapurple')
})
})