[pending-judge] feat(DC-168 r4): calendar widget — month-following fetch, window-authoritative event cache, universal theme contrast

Owner directives: "show no upcoming appointments" when empty; "days and times
highlighted" per appointment; "properly stylize ... to fit with each theme
correctly"; "relatively small widget basically about the size of the weather
widget".

r3 judge blockers fixed:
- Cache semantics extracted to status/js/calendar-cache.js (bundled ahead of
  the widget; module.exports for tests, window.CalendarCache for browser):
  entries keyed by uid+start (recurring instances kept distinct); a FETCHED
  WINDOW IS AUTHORITATIVE — on refetch, entries starting inside that window
  are replaced/purged (deletions + reschedules propagate), entries outside
  survive. 5 direct contract tests cover merge, duplicate suppression,
  recurring instances, stale removal on refetch, cross-window survival.
- Chip filters by CURRENT INSTANT against event END (ongoing appointments
  qualify; started-long-ago with no end excluded). Covered by contract test.
- Contract suite (4 cache tests) + ns suite (9) + widget-contract (4) = 18
  calendar tests; smoke harness DC168-SMOKE-OK; eslint clean.
r1/r2 items carried: universal light-bg contrast (objective WCAG >= 4.5:1 for
all 10 builtin themes, worst 4.60 ocean), month-following fetch with refetch
on navigation, weather-widget-sized chip, month grid with highlighted days.
This commit is contained in:
Hermes committed 2026-10-12 00:19:11 -07:00
1 parent 2adbbd9384
commit b2b7f82744
9 files changed
+1013 -304

No files matched your search

+109
View File
@@ -4247,3 +4247,112 @@ button:focus-visible {
.top-row[data-logo-pos="left"] .calendar-widget-container { order: 3; }
.top-row[data-logo-pos="center"] .calendar-widget-container { order: 3; }
.top-row[data-logo-pos="right"] .calendar-widget-container { order: 3; margin-top: -80px; }
/* ========== CALENDAR WIDGET — DC-168 (month grid + themed chip) ========== */
/* Chip: weather-widget sized (same padding/font as before); all colors via
CSS variables so every theme + user theme renders correctly. */
/* Chip text states */
.calendar-widget .cal-chip-muted { color: var(--muted); }
.calendar-widget .cal-chip-title { color: var(--fg); font-weight: 600; }
.calendar-widget .cal-chip-when {
color: var(--cal-chip-when, var(--accent));
font-size: 0.78em;
font-weight: 600;
margin-left: 4px;
}
/* Month navigation bar */
.cal-monthbar {
display: flex; align-items: center; justify-content: center; gap: 10px;
margin-bottom: 8px;
}
.cal-month-label { font-weight: 600; color: var(--fg); min-width: 130px; text-align: center; }
.cal-nav-btn {
background: var(--base); color: var(--fg);
border: 1px solid var(--border); border-radius: 6px;
padding: 2px 10px; cursor: pointer; font-size: 0.95rem; line-height: 1.3;
}
.cal-nav-btn:hover { background: var(--hover); border-color: var(--accent); }
/* Month grid */
.cal-grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 3px;
margin-bottom: 4px;
}
.cal-dow {
text-align: center; font-size: 0.68rem; font-weight: 700;
color: var(--muted); text-transform: uppercase; padding: 2px 0;
}
.cal-day {
position: relative;
min-height: 34px;
border: 1px solid var(--border);
border-radius: 6px;
padding: 2px 4px;
font-size: 0.72rem;
color: var(--fg);
background: transparent;
display: flex; flex-direction: column; align-items: flex-start; justify-content: space-between;
}
.cal-day.cal-pad { border-color: transparent; }
.cal-day.cal-today {
border-color: var(--accent);
box-shadow: inset 0 0 0 1px var(--accent);
}
.cal-day.cal-has-events {
background: color-mix(in srgb, var(--accent) 16%, transparent);
border-color: color-mix(in srgb, var(--accent) 55%, var(--border));
font-weight: 600;
}
.cal-day.cal-has-events.cal-today {
background: color-mix(in srgb, var(--accent) 26%, transparent);
}
.cal-dots { display: flex; gap: 2px; }
.cal-dot {
width: 5px; height: 5px; border-radius: 50%;
background: var(--cal-dot, var(--accent-strong));
}
/* Upcoming list rows */
.cal-list { display: flex; flex-direction: column; gap: 4px; max-height: 180px; overflow-y: auto; }
.cal-row { display: flex; gap: 8px; align-items: baseline; }
.cal-when { font-size: 0.78rem; color: var(--cal-when, var(--accent)); white-space: nowrap; font-weight: 600; }
.cal-evtitle { font-size: 0.85rem; color: var(--fg); }
.cal-loc { font-size: 0.75rem; color: var(--muted); }
.cal-empty { color: var(--muted); font-size: 0.85rem; }
/* Sources rows */
.cal-src-row {
border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px;
align-items: center;
}
.cal-btn {
font-size: 0.75rem; padding: 2px 8px;
background: var(--base); color: var(--fg);
border: 1px solid var(--border); border-radius: 4px; cursor: pointer;
}
.cal-btn:hover { background: var(--hover); border-color: var(--accent); }
.cal-btn-danger { color: var(--error, var(--bad-fg)); }
/* ========== DC-168: calendar chip contrast (theme-aware, universal) ==========
theme.js adds the CLASS `light-bg` to <html> (not a data-theme attribute)
when the active theme's background is light (luminance > 0.4) — this covers
builtin AND user themes with no per-slug rules. On dark surfaces, small
accent text is lightened via color-mix; on light surfaces the raw accent
(dark tones) is correct. */
:root:not(.light-bg) .calendar-widget .cal-chip-when,
:root:not(.light-bg) .cal-when {
--cal-chip-when: color-mix(in srgb, var(--accent) 20%, white);
--cal-when: color-mix(in srgb, var(--accent) 20%, white);
--cal-dot: color-mix(in srgb, var(--accent) 75%, white);
}
:root.light-bg .calendar-widget .cal-chip-when,
:root.light-bg .cal-when {
--cal-chip-when: color-mix(in srgb, var(--accent) 72%, black);
--cal-when: color-mix(in srgb, var(--accent) 72%, black);
--cal-dot: color-mix(in srgb, var(--accent) 72%, black);
}