Paint each board row in its own notification type's colour

This commit is contained in:
2026-08-07 11:06:04 -04:00
parent 7c5b1bc74e
commit 344d252264

View File

@@ -45,13 +45,17 @@
<!-- Carousel - one card at a time, rotating --> <!-- Carousel - one card at a time, rotating -->
<section v-else-if="group.displaystyle === 'carousel'" class="recognition-section"> <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 class="recognition-carousel">
<div <div
v-for="(rec, idx) in group.items" v-for="(rec, idx) in group.items"
:key="`${rec.notificationid}-${rec.employeesso}`" :key="`${rec.notificationid}-${rec.employeesso}`"
class="recognition-card" class="recognition-card"
:class="{ active: idx === carouselIndex(group) }" :class="{ active: idx === carouselIndex(group) }"
:style="{ '--accent': getTypeColor(rec.typecolor) }"
> >
<!-- A type that names no employee is a message, not a person: <!-- A type that names no employee is a message, not a person:
no photo frame, no empty name line. --> no photo frame, no empty name line. -->
@@ -82,7 +86,10 @@
<!-- Grid - a row of tiles, cycling a page at a time --> <!-- Grid - a row of tiles, cycling a page at a time -->
<section v-else class="recert-section"> <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>{{ group.typename }} ({{ group.items.length }})</span>
<span v-if="gridRangeLabel(group)" class="recert-range">{{ gridRangeLabel(group) }}</span> <span v-if="gridRangeLabel(group)" class="recert-range">{{ gridRangeLabel(group) }}</span>
</div> </div>
@@ -99,6 +106,7 @@
:key="`grid-${rec.notificationid}-${rec.employeesso}`" :key="`grid-${rec.notificationid}-${rec.employeesso}`"
class="recert-tile" class="recert-tile"
:class="{ 'message-tile': !hasEmployee(rec) }" :class="{ 'message-tile': !hasEmployee(rec) }"
:style="{ '--accent': getTypeColor(rec.typecolor) }"
> >
<template v-if="hasEmployee(rec)"> <template v-if="hasEmployee(rec)">
<img <img
@@ -265,6 +273,10 @@ const styledGroups = computed(() => {
key, key,
typename: heading, typename: heading,
displaystyle: n.displaystyle, 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: [] items: []
}) })
} }
@@ -453,6 +465,19 @@ function getTypeColor(typecolor) {
return aliases[typecolor] || typecolor || '#14abef' 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) { function getSectionClass(notifications) {
// Use danger color if any active incidents // Use danger color if any active incidents
const hasIncident = notifications.some(n => n.typecolor === 'danger' && !n.resolved) const hasIncident = notifications.some(n => n.typecolor === 'danger' && !n.resolved)
@@ -602,6 +627,7 @@ function handlePhotoError(e) {
} }
.section-title.recognition { .section-title.recognition {
/* Fallback only - the real colour is the type's, set inline. */
background: #ffc107; background: #ffc107;
color: #3a2e00; color: #3a2e00;
} }
@@ -612,6 +638,7 @@ function handlePhotoError(e) {
margin-bottom: 25px; margin-bottom: 25px;
} }
.section-title.recert-title { .section-title.recert-title {
/* Fallback only - the real colour is the type's, set inline. */
background: #0d6efd; background: #0d6efd;
color: #fff; color: #fff;
display: flex; display: flex;
@@ -638,8 +665,8 @@ function handlePhotoError(e) {
gap: 14px; gap: 14px;
} }
.recert-tile { .recert-tile {
background: linear-gradient(135deg, #1e3a5f 0%, #0d2137 100%); background: linear-gradient(135deg, #16233b 0%, #0d2137 100%);
border: 2px solid #0d6efd; border: 2px solid var(--accent, #0d6efd);
border-radius: 10px; border-radius: 10px;
padding: 14px 10px; padding: 14px 10px;
display: flex; display: flex;
@@ -653,7 +680,7 @@ function handlePhotoError(e) {
height: 90px; height: 90px;
border-radius: 50%; border-radius: 50%;
object-fit: cover; object-fit: cover;
border: 3px solid #0d6efd; border: 3px solid var(--accent, #0d6efd);
background: #1a1a2e; background: #1a1a2e;
} }
.recert-photo.ge-logo-fallback { .recert-photo.ge-logo-fallback {
@@ -713,8 +740,8 @@ function handlePhotoError(e) {
top: 0; top: 0;
left: 0; left: 0;
right: 0; right: 0;
background: linear-gradient(135deg, #4a3a0a 0%, #2a2200 100%); background: linear-gradient(135deg, #2b2718 0%, #1d1a10 100%);
border: 3px solid #ffc107; border: 3px solid var(--accent, #ffc107);
border-radius: 12px; border-radius: 12px;
padding: 20px 25px; padding: 20px 25px;
display: flex; display: flex;
@@ -736,7 +763,7 @@ function handlePhotoError(e) {
height: 140px; height: 140px;
border-radius: 50%; border-radius: 50%;
object-fit: cover; object-fit: cover;
border: 4px solid #ffc107; border: 4px solid var(--accent, #ffc107);
background: #1a1a2e; background: #1a1a2e;
} }