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