Paint each board row in its own notification type's colour
Some checks failed
CI / backend (push) Failing after 8s
CI / naming (push) Successful in 2s
CI / frontend (push) Successful in 8s
CI / migrations-mysql (push) Failing after 6s

The carousel gold and the grid blue were hardcoded in the dashboard's CSS,
which was fine while carousel meant Recognition and grid meant Recertification.
Now that any type can take either style, a type arrived wearing somebody else's
colour: a green Kudos row came out gold, and every grid row came out
Recertification blue regardless of what was picked in the type settings.

Row headings now take the type's colour, and cards and tiles take theirs
through a --accent property, so a shared category row shows a red incident tile
beside a purple change tile. Heading text colour is computed from the
background's luma rather than listed per colour, since a site picks its own
hexes - gold needs dark text, navy needs white.

The card backgrounds were gold- and blue-tinted gradients; they are neutral
dark now so the accent is what carries the colour. The old values remain as CSS
fallbacks for a card that somehow arrives with no type.
This commit is contained in:
cproudlock
2026-08-07 11:05:47 -04:00
parent 245f94d344
commit d180a693ee

View File

@@ -45,13 +45,17 @@
<!-- Carousel - one card at a time, rotating -->
<section v-else-if="group.displaystyle === 'carousel'" class="recognition-section">
<div class="section-title recognition">{{ group.typename }}</div>
<div
class="section-title recognition"
:style="{ backgroundColor: getTypeColor(group.typecolor), color: textOn(group.typecolor) }"
>{{ group.typename }}</div>
<div class="recognition-carousel">
<div
v-for="(rec, idx) in group.items"
:key="`${rec.notificationid}-${rec.employeesso}`"
class="recognition-card"
:class="{ active: idx === carouselIndex(group) }"
:style="{ '--accent': getTypeColor(rec.typecolor) }"
>
<!-- A type that names no employee is a message, not a person:
no photo frame, no empty name line. -->
@@ -82,7 +86,10 @@
<!-- Grid - a row of tiles, cycling a page at a time -->
<section v-else class="recert-section">
<div class="section-title recert-title">
<div
class="section-title recert-title"
:style="{ backgroundColor: getTypeColor(group.typecolor), color: textOn(group.typecolor) }"
>
<span>{{ group.typename }} ({{ group.items.length }})</span>
<span v-if="gridRangeLabel(group)" class="recert-range">{{ gridRangeLabel(group) }}</span>
</div>
@@ -99,6 +106,7 @@
:key="`grid-${rec.notificationid}-${rec.employeesso}`"
class="recert-tile"
:class="{ 'message-tile': !hasEmployee(rec) }"
:style="{ '--accent': getTypeColor(rec.typecolor) }"
>
<template v-if="hasEmployee(rec)">
<img
@@ -265,6 +273,10 @@ const styledGroups = computed(() => {
key,
typename: heading,
displaystyle: n.displaystyle,
// Heading colour comes from the first type in the row. A shared
// category is one heading, so it can only carry one colour; the
// individual cards below still wear their own type's colour.
typecolor: n.typecolor,
items: []
})
}
@@ -453,6 +465,19 @@ function getTypeColor(typecolor) {
return aliases[typecolor] || typecolor || '#14abef'
}
// Text colour for a heading painted an arbitrary type colour. Gold needs dark
// text, navy needs white, and a site picks its own hexes - so this is computed
// rather than a per-colour rule.
function textOn(backgroundcolor) {
const hex = getTypeColor(backgroundcolor).replace('#', '')
if (hex.length < 6) return '#fff'
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
// Rec. 709 luma, the usual quick contrast test.
return (0.2126 * r + 0.7152 * g + 0.0722 * b) > 150 ? '#231b00' : '#fff'
}
function getSectionClass(notifications) {
// Use danger color if any active incidents
const hasIncident = notifications.some(n => n.typecolor === 'danger' && !n.resolved)
@@ -602,6 +627,7 @@ function handlePhotoError(e) {
}
.section-title.recognition {
/* Fallback only - the real colour is the type's, set inline. */
background: #ffc107;
color: #3a2e00;
}
@@ -612,6 +638,7 @@ function handlePhotoError(e) {
margin-bottom: 25px;
}
.section-title.recert-title {
/* Fallback only - the real colour is the type's, set inline. */
background: #0d6efd;
color: #fff;
display: flex;
@@ -638,8 +665,8 @@ function handlePhotoError(e) {
gap: 14px;
}
.recert-tile {
background: linear-gradient(135deg, #1e3a5f 0%, #0d2137 100%);
border: 2px solid #0d6efd;
background: linear-gradient(135deg, #16233b 0%, #0d2137 100%);
border: 2px solid var(--accent, #0d6efd);
border-radius: 10px;
padding: 14px 10px;
display: flex;
@@ -653,7 +680,7 @@ function handlePhotoError(e) {
height: 90px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #0d6efd;
border: 3px solid var(--accent, #0d6efd);
background: #1a1a2e;
}
.recert-photo.ge-logo-fallback {
@@ -713,8 +740,8 @@ function handlePhotoError(e) {
top: 0;
left: 0;
right: 0;
background: linear-gradient(135deg, #4a3a0a 0%, #2a2200 100%);
border: 3px solid #ffc107;
background: linear-gradient(135deg, #2b2718 0%, #1d1a10 100%);
border: 3px solid var(--accent, #ffc107);
border-radius: 12px;
padding: 20px 25px;
display: flex;
@@ -736,7 +763,7 @@ function handlePhotoError(e) {
height: 140px;
border-radius: 50%;
object-fit: cover;
border: 4px solid #ffc107;
border: 4px solid var(--accent, #ffc107);
background: #1a1a2e;
}