Match card chrome to title color

This commit is contained in:
Lauri Ojansivu committed 2026-09-06 11:23:31 +03:00
1 parent a8369e5cd0
commit ebb35af50d
4 files changed
+135 -14

No files matched your search

+23 -1
View File
@@ -263,6 +263,28 @@ body.desktop-mode .card-details.card-details-collapsed {
display: flow-root;
min-height: 44px;
}
/* Every piece of window chrome beside the title uses the title's computed
contrast colour. This outranks the global `a i.fa` grey icon rule without
duplicating the card palette, and therefore also follows custom hex colours.
Hover/focus/active deliberately keep the same colour: the control may not
become grey merely because it is being used. */
.card-details .card-details-header .card-header-control,
.card-details .card-details-header .card-header-control .fa,
.card-details .card-details-header .card-header-control:hover,
.card-details .card-details-header .card-header-control:hover .fa,
.card-details .card-details-header .card-header-control:focus,
.card-details .card-details-header .card-header-control:focus .fa,
.card-details .card-details-header .card-header-control:active,
.card-details .card-details-header .card-header-control:active .fa {
color: inherit;
}
.card-details .card-details-header .card-details-title,
.card-details .card-details-header .card-details-title .viewer,
.card-details .card-details-header .card-details-title .viewer p,
.card-details .card-details-header .card-details-title .viewer a,
.card-details .card-details-header .card-details-title .viewer a:hover {
color: inherit;
}
.card-details .card-details-header .card-number {
color: #b3b3b3;
display: inline-block;
@@ -278,7 +300,7 @@ body.desktop-mode .card-details.card-details-collapsed {
margin-inline-end: 5px;
cursor: pointer;
user-select: none;
color: #000;
color: inherit;
vertical-align: middle;
line-height: 1.2;
}
+13 -13
View File
@@ -28,18 +28,18 @@ template(name="cardDetails")
+editCardTitleForm
else
unless isMiniScreen
a.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
a.card-header-control.card-collapse-toggle.js-card-collapse-toggle(title="{{_ 'collapse-card'}}")
if cardCollapsed
i.fa.fa-caret-right
else
i.fa.fa-caret-down
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
a.card-header-control.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
i.fa.fa-times-thin
if cardMaximized
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
a.card-header-control.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
else
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
a.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
a.card-header-control.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
a.card-header-control.card-details-menu.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
i.fa.fa-bars
//- The handle appears only when drag handles are ON, like the
minicard's. With them off the window is dragged by its own title bar
@@ -49,25 +49,25 @@ template(name="cardDetails")
client/components/cards/minicard.jade
if canModifyCard
if isTouchScreenOrShowDesktopDragHandles
span.card-drag-handle.js-card-drag-handle(title="Drag card")
span.card-header-control.card-drag-handle.js-card-drag-handle(title="Drag card")
i.fa.fa-arrows
else
a.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
a.card-header-control.close-card-details.js-close-card-details(title="{{_ 'close-card'}}")
i.fa.fa-times-thin
a.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
a.card-header-control.card-zoom-out.js-card-zoom-out(title="{{_ 'zoom-out'}}")
i.fa.fa-search-minus
a.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
a.card-header-control.card-zoom-in.js-card-zoom-in(title="{{_ 'zoom-in'}}")
i.fa.fa-search-plus
a.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
a.card-header-control.card-mobile-desktop-toggle.js-card-mobile-desktop-toggle(title="{{_ 'mobile-desktop-toggle'}}")
if mobileMode
i.fa.fa-desktop
else
i.fa.fa-mobile
if cardMaximized
a.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
a.card-header-control.fa.fa-window-minimize.minimize-card-details.js-minimize-card-details(title="{{_ 'minimize-card'}}")
else
a.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
a.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
a.card-header-control.fa.fa-window-maximize.maximize-card-details.js-maximize-card-details(title="{{_ 'maximize-card'}}")
a.card-header-control.card-details-menu-mobile-web.js-open-card-details-menu(title="{{_ 'cardDetailsActionsPopup-title'}}")
i.fa.fa-bars
if canModifyCard
if currentBoard.allowsDueComplete
+37
View File
@@ -72,6 +72,29 @@
font-size: 0.875rem;
padding-inline-start: 10px;
}
/* The minicard title and its chrome use the card's one computed text colour.
The global Font Awesome rules otherwise turn the menu and handle grey even
when the card palette says black or white. */
.minicard .minicard-title,
.minicard .minicard-details-menu,
.minicard .minicard-details-menu-with-handle,
.minicard .minicard-details-menu .fa,
.minicard .minicard-details-menu-with-handle .fa,
.minicard .handle,
.minicard .handle .fa,
.minicard .handle .drag-handle,
.minicard .minicard-details-menu:hover .fa,
.minicard .minicard-details-menu-with-handle:hover .fa,
.minicard .handle:hover .fa,
.minicard .handle:hover .drag-handle {
color: inherit;
}
.minicard .minicard-title .viewer,
.minicard .minicard-title .viewer p,
.minicard .minicard-title .viewer a,
.minicard .minicard-title .viewer a:hover {
color: inherit;
}
@media print {
.minicard-details-menu,
.minicard-details-menu-with-handle {
@@ -603,6 +626,10 @@
}
}
/* https://github.com/wekan/wekan/issues/4254#issuecomment-1037192960 */
.minicard-white {
background-color: #fff !important;
color: #000 !important;
}
.minicard-green {
background-color: #3cb500 !important;
color: #fff !important;
@@ -617,6 +644,7 @@
}
.minicard-yellow {
background-color: #fad900 !important;
color: #000 !important;
}
.minicard-yellow:hover:not(.minicard-composer),
.is-selected .minicard-yellow,
@@ -628,6 +656,7 @@
}
.minicard-orange {
background-color: #ff9f19 !important;
color: #000 !important;
}
.minicard-orange:hover:not(.minicard-composer),
.is-selected .minicard-orange,
@@ -675,6 +704,7 @@
}
.minicard-pink {
background-color: #ff78cb !important;
color: #000 !important;
}
.minicard-pink:hover:not(.minicard-composer),
.is-selected .minicard-pink,
@@ -710,6 +740,7 @@
}
.minicard-lime {
background-color: #51e898 !important;
color: #000 !important;
}
.minicard-lime:hover:not(.minicard-composer),
.is-selected .minicard-lime,
@@ -721,6 +752,7 @@
}
.minicard-silver {
background-color: #c0c0c0 !important;
color: #000 !important;
}
.minicard-silver:hover:not(.minicard-composer),
.is-selected .minicard-silver,
@@ -732,6 +764,7 @@
}
.minicard-peachpuff {
background-color: #ffdab9 !important;
color: #000 !important;
}
.minicard-peachpuff:hover:not(.minicard-composer),
.is-selected .minicard-peachpuff,
@@ -755,6 +788,7 @@
}
.minicard-plum {
background-color: #dda0dd !important;
color: #000 !important;
}
.minicard-plum:hover:not(.minicard-composer),
.is-selected .minicard-plum,
@@ -802,6 +836,7 @@
}
.minicard-gold {
background-color: #ffd700 !important;
color: #000 !important;
}
.minicard-gold:hover:not(.minicard-composer),
.is-selected .minicard-gold,
@@ -849,6 +884,7 @@
}
.minicard-paleturquoise {
background-color: #afeeee !important;
color: #000 !important;
}
.minicard-paleturquoise:hover:not(.minicard-composer),
.is-selected .minicard-paleturquoise,
@@ -860,6 +896,7 @@
}
.minicard-mistyrose {
background-color: #ffe4e1 !important;
color: #000 !important;
}
.minicard-mistyrose:hover:not(.minicard-composer),
.is-selected .minicard-mistyrose,
+62
View File
@@ -0,0 +1,62 @@
'use strict';
const assert = require('assert');
const fs = require('fs');
const path = require('path');
const root = path.resolve(__dirname, '..');
const read = relative => fs.readFileSync(path.join(root, relative), 'utf8');
const detailsCss = read('client/components/cards/cardDetails.css');
const detailsJade = read('client/components/cards/cardDetails.jade');
const minicardCss = read('client/components/cards/minicard.css');
const chrome = [
'card-collapse-toggle',
'close-card-details',
'minimize-card-details',
'maximize-card-details',
'card-details-menu',
'card-drag-handle',
'card-zoom-out',
'card-zoom-in',
'card-mobile-desktop-toggle',
'card-details-menu-mobile-web',
];
for (const name of chrome) {
assert.match(detailsJade, new RegExp(`card-header-control[^\\n]*${name}|${name}[^\\n]*card-header-control`),
`${name} is part of the title-coloured header chrome`);
}
const controlRule = detailsCss.slice(
detailsCss.indexOf('.card-details .card-details-header .card-header-control,'),
detailsCss.indexOf('}', detailsCss.indexOf('.card-details .card-details-header .card-header-control,')) + 1,
);
assert.match(controlRule, /card-header-control:hover \.fa/);
assert.match(controlRule, /card-header-control:focus \.fa/);
assert.match(controlRule, /card-header-control:active \.fa/);
assert.match(controlRule, /color:\s*inherit;/);
assert.doesNotMatch(controlRule, /color:\s*#[0-9a-f]{3,8}/i,
'card header controls never hard-code black, white or grey');
assert.match(detailsCss, /\.card-details-title \.viewer a:hover \{\s*color:\s*inherit;/,
'formatted title content cannot override the header contrast colour');
assert.match(minicardCss, /\.minicard \.minicard-title,[\s\S]{0,700}\.minicard \.handle:hover \.drag-handle \{\s*color:\s*inherit;/,
'minicard title, menu and handle share the card text colour in every state');
assert.match(minicardCss, /\.minicard-title \.viewer a:hover \{\s*color:\s*inherit;/,
'formatted minicard title content keeps the card contrast colour');
const lightColors = [
'white', 'yellow', 'orange', 'pink', 'lime', 'silver', 'peachpuff',
'plum', 'gold', 'paleturquoise', 'mistyrose',
];
for (const color of lightColors) {
assert.match(minicardCss, new RegExp(`\\.minicard-${color} \\{[^}]*color: #000 !important;`, 's'),
`${color} minicard uses the same black title contrast as opened cards`);
}
for (const color of ['green', 'red', 'purple', 'blue', 'black', 'navy', 'indigo']) {
assert.match(minicardCss, new RegExp(`\\.minicard-${color} \\{[^}]*color: #fff !important;`, 's'),
`${color} minicard retains white title contrast`);
}
console.log('cardHeaderColorInheritance: opened and mini card colors passed');