diff --git a/static/js/calendar.js b/static/js/calendar.js
index bea1ca0..31a4423 100644
--- a/static/js/calendar.js
+++ b/static/js/calendar.js
@@ -13,6 +13,7 @@ import {
CAL_PALETTE, CAL_COLORS, _CAL_CUSTOM_GRADIENT, _TYPE_PALETTE,
_trashIcon, _moreIcon, _bellIcon,
_isCalBgImage, _calBgImageUrl, _calBgCss,
+ _calReadableTextColor,
_ds, _addDays, _shiftDT, _tzOffset, _localDateOf,
} from './calendar/utils.js';
@@ -371,6 +372,10 @@ function _calColor(ev) {
return c?.color || 'var(--accent)';
}
+function _calEventFg(ev) {
+ return _calReadableTextColor(_calColor(ev));
+}
+
// Extra inline style for an event row when the event has a custom BG image.
// Returns '' for normal solid-color events.
function _calItemBgStyle(ev) {
@@ -975,7 +980,7 @@ async function _renderMonth() {
const startColInt = Math.round(startCol);
const endColInt = Math.round(endCol);
const span = endColInt - startColInt + 1;
- h += `
${_e(md.summary)}
`;
+ h += `${_e(md.summary)}
`;
barSlot++;
}
h += '';
@@ -1141,7 +1146,7 @@ async function _renderWeek() {
// All-day strip
colsHtml += ``;
for (const ev of allDayEvents) {
- colsHtml += `
${_e(ev.summary)}
`;
+ colsHtml += `
${_e(ev.summary)}
`;
}
colsHtml += `
`;
// Hour-grid body
diff --git a/static/js/calendar/utils.js b/static/js/calendar/utils.js
index a688852..66d30f7 100644
--- a/static/js/calendar/utils.js
+++ b/static/js/calendar/utils.js
@@ -74,6 +74,42 @@ export function _calBgCss(c, fallback) {
return c || fallback || 'var(--accent)';
}
+function _hexToRgb(c) {
+ if (typeof c !== 'string') return null;
+ const m = c.trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i);
+ if (!m) return null;
+ const hex = m[1].length === 3
+ ? m[1].split('').map(ch => ch + ch).join('')
+ : m[1];
+ return {
+ r: parseInt(hex.slice(0, 2), 16),
+ g: parseInt(hex.slice(2, 4), 16),
+ b: parseInt(hex.slice(4, 6), 16),
+ };
+}
+
+function _relativeLuminance({ r, g, b }) {
+ return [r, g, b].map(v => {
+ const c = v / 255;
+ return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
+ }).reduce((sum, c, i) => sum + c * [0.2126, 0.7152, 0.0722][i], 0);
+}
+
+function _contrastRatio(a, b) {
+ const light = Math.max(a, b);
+ const dark = Math.min(a, b);
+ return (light + 0.05) / (dark + 0.05);
+}
+
+export function _calReadableTextColor(bg) {
+ const rgb = _hexToRgb(bg);
+ if (!rgb) return 'var(--fg)';
+ const lum = _relativeLuminance(rgb);
+ const white = _contrastRatio(lum, 1);
+ const ink = _contrastRatio(lum, 0.006);
+ return ink >= white ? '#111820' : '#ffffff';
+}
+
// ── date helpers ──
// `YYYY-MM-DD` string from a Date.
diff --git a/static/style.css b/static/style.css
index a10b6c9..e77c84b 100644
--- a/static/style.css
+++ b/static/style.css
@@ -33129,7 +33129,7 @@ button.cal-view-btn {
line-height: 11px;
padding: 0 4px;
border-radius: 3px;
- color: #fff;
+ color: var(--cal-event-fg, #fff);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -33355,7 +33355,7 @@ button.cal-view-btn {
font-weight: 500;
padding: 2px 5px;
border-radius: 3px;
- color: var(--fg);
+ color: var(--cal-event-fg, var(--fg));
cursor: pointer;
white-space: nowrap;
overflow: hidden;
diff --git a/tests/test_calendar_event_contrast.py b/tests/test_calendar_event_contrast.py
new file mode 100644
index 0000000..1558551
--- /dev/null
+++ b/tests/test_calendar_event_contrast.py
@@ -0,0 +1,76 @@
+import json
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+
+ROOT = Path(__file__).resolve().parents[1]
+CALENDAR_JS = ROOT / "static" / "js" / "calendar.js"
+STYLE_CSS = ROOT / "static" / "style.css"
+UTILS_JS = ROOT / "static" / "js" / "calendar" / "utils.js"
+
+pytestmark = pytest.mark.skipif(not shutil.which("node"), reason="node binary not on PATH")
+
+
+def _node_eval(source: str):
+ result = subprocess.run(
+ ["node", "--input-type=module", "-e", source],
+ cwd=ROOT,
+ check=True,
+ capture_output=True,
+ text=True,
+ )
+ return json.loads(result.stdout)
+
+
+def test_calendar_readable_text_color_prefers_dark_ink_for_pastels():
+ values = _node_eval(
+ """
+ import { _calReadableTextColor } from './static/js/calendar/utils.js';
+ console.log(JSON.stringify({
+ blue: _calReadableTextColor('#b0d7f7'),
+ yellow: _calReadableTextColor('#f2dfbd'),
+ shortHex: _calReadableTextColor('#abc')
+ }));
+ """
+ )
+
+ assert values == {
+ "blue": "#111820",
+ "yellow": "#111820",
+ "shortHex": "#111820",
+ }
+
+
+def test_calendar_readable_text_color_keeps_light_text_for_dark_colors():
+ values = _node_eval(
+ """
+ import { _calReadableTextColor } from './static/js/calendar/utils.js';
+ console.log(JSON.stringify({
+ navy: _calReadableTextColor('#1f3552'),
+ red: _calReadableTextColor('#78252d'),
+ variable: _calReadableTextColor('var(--accent)')
+ }));
+ """
+ )
+
+ assert values == {
+ "navy": "#ffffff",
+ "red": "#ffffff",
+ "variable": "var(--fg)",
+ }
+
+
+def test_calendar_event_surfaces_use_computed_foreground_variable():
+ calendar_js = CALENDAR_JS.read_text(encoding="utf-8")
+ style_css = STYLE_CSS.read_text(encoding="utf-8")
+ utils_js = UTILS_JS.read_text(encoding="utf-8")
+
+ assert "_calReadableTextColor" in utils_js
+ assert "function _calEventFg(ev)" in calendar_js
+ assert "--cal-event-fg:${_calEventFg(md)}" in calendar_js
+ assert "--cal-event-fg:${_calEventFg(ev)}" in calendar_js
+ assert "color: var(--cal-event-fg, #fff);" in style_css
+ assert "color: var(--cal-event-fg, var(--fg));" in style_css