CALENDAR: events tinted by tag colour, no side bars #238

Closed
opened 2026-09-27 12:59:59 +00:00 by kayg · 6 comments
Owner

Owner decision (2026-09-27): calendar events use a tinted fill in their tag colour instead of a coloured side bar (owner rule: no side highlights / border highlights; tint instead).

Decided

  • Colour source: the shared tagColor(tag) helper in packages/ui/src/tags (the same colours the Agenda already uses for tag chips), so a tag has one colour everywhere. Reuse it; no second palette.
  • Which tag: the event's first tag in its own order. No tags: a neutral surface tint (not the accent).
  • Fill: a soft tint of the tag colour (color-mix with the surface), text in a darker/lighter shade of the same hue. Text contrast ≥ 4.5:1 in light and dark and on every theme (check all built-in themes).
  • Remove every coloured side bar/left border on events. Neutral structural grid lines (day/hour separators) stay.
  • States without borders: hover = slightly stronger tint; selected/open = stronger tint (+ the shared focus ring only for keyboard focus); past events = reduced opacity of the tint and text; all-day and multi-day bars use the same tint; overlapping events stay distinguishable (gap/shadow, not a border).
  • Applies to every calendar surface: Day/Week TimeGrid, Month grid chips, Agenda, Year/MiniMonth dots where events show, event popover header, Home/Today event items, search results for events.
  • Performance: colours computed once per tag (memoised), no per-frame work.

Evidence

Screenshots from a production build, light + dark, desktop 1440 + phone 390: Day, Week, Month, Agenda, Home with events of 4+ different tags, untagged events, past events, a selected event, overlapping events. The orchestrator reviews and sends them to the owner.

Owner decision (2026-09-27): calendar events use a **tinted fill in their tag colour** instead of a coloured side bar (owner rule: no side highlights / border highlights; tint instead). ## Decided - Colour source: the shared `tagColor(tag)` helper in `packages/ui/src/tags` (the same colours the Agenda already uses for tag chips), so a tag has one colour everywhere. Reuse it; no second palette. - Which tag: the event's first tag in its own order. No tags: a neutral surface tint (not the accent). - Fill: a soft tint of the tag colour (color-mix with the surface), text in a darker/lighter shade of the same hue. Text contrast ≥ 4.5:1 in light and dark and on every theme (check all built-in themes). - Remove every coloured side bar/left border on events. Neutral structural grid lines (day/hour separators) stay. - States without borders: hover = slightly stronger tint; selected/open = stronger tint (+ the shared focus ring only for keyboard focus); past events = reduced opacity of the tint and text; all-day and multi-day bars use the same tint; overlapping events stay distinguishable (gap/shadow, not a border). - Applies to every calendar surface: Day/Week TimeGrid, Month grid chips, Agenda, Year/MiniMonth dots where events show, event popover header, Home/Today event items, search results for events. - Performance: colours computed once per tag (memoised), no per-frame work. ## Evidence Screenshots from a production build, light + dark, desktop 1440 + phone 390: Day, Week, Month, Agenda, Home with events of 4+ different tags, untagged events, past events, a selected event, overlapping events. The orchestrator reviews and sends them to the owner.
Author
Owner

Starting Forgejo #238 on branch job/event-tint.

Base SHA: 6f31788bdc31641dc8e2b58a5b6485a89a89b202.

I have read CLAUDE.md, docs/DESIGN.md, and CONTEXT.md. I am tracing the existing event color and calendar surface components before implementing the decided tint behavior.

Starting Forgejo #238 on branch `job/event-tint`. Base SHA: `6f31788bdc31641dc8e2b58a5b6485a89a89b202`. I have read `CLAUDE.md`, `docs/DESIGN.md`, and `CONTEXT.md`. I am tracing the existing event color and calendar surface components before implementing the decided tint behavior.
Author
Owner

Finding while tracing #238: the Calendar range projection has no category field. ExternalCalendarEvent in crates/plugins/calendar/src/view.rs only carries date, id, title, time, provider, and all-day state, and apps/web/src/lib/calendar/data.ts maps only those fields into CalendarEvent. In addition, AgendaList.svelte renders only tasks and logs, and MonthGrid.svelte's row union is only log/task. Year and mini-month are aggregate activity views with no event-color data. I am extending the existing projections and event rows so the requested tint has real event categories at each named surface.

Finding while tracing #238: the Calendar range projection has no category field. `ExternalCalendarEvent` in `crates/plugins/calendar/src/view.rs` only carries date, id, title, time, provider, and all-day state, and `apps/web/src/lib/calendar/data.ts` maps only those fields into `CalendarEvent`. In addition, `AgendaList.svelte` renders only tasks and logs, and `MonthGrid.svelte`'s row union is only log/task. Year and mini-month are aggregate activity views with no event-color data. I am extending the existing projections and event rows so the requested tint has real event categories at each named surface.
Author
Owner

Merged E2E finding: the fixed-height all-day lane renders only the first Event and collapses the rest into +N. Two all-day fixtures shared one date, so the neutral Event was sometimes hidden from the contrast selector. Moved the multiday fixture to tomorrow; the E2E now passes while retaining multiday coverage. This was a nondeterministic fixture issue, not a rendered Event defect.

Merged E2E finding: the fixed-height all-day lane renders only the first Event and collapses the rest into +N. Two all-day fixtures shared one date, so the neutral Event was sometimes hidden from the contrast selector. Moved the multiday fixture to tomorrow; the E2E now passes while retaining multiday coverage. This was a nondeterministic fixture issue, not a rendered Event defect.
Author
Owner

Merged full-workspace Clippy found the notifications reminder test fixture missing EventMetadata.categories after the calendar metadata extension. Added an empty categories vector. Notifications tests passed (12/12), targeted Clippy passed, and the repeated full-workspace Clippy gate passed.

Merged full-workspace Clippy found the notifications reminder test fixture missing EventMetadata.categories after the calendar metadata extension. Added an empty categories vector. Notifications tests passed (12/12), targeted Clippy passed, and the repeated full-workspace Clippy gate passed.
Author
Owner

Forgejo #238 — Event tint

Implemented on job/event-tint; head SHA: 5e73e205d9706dc0197f2254b3252f7fb8f4364e. Merged current dev before the final gates.

Built

CalDAV Events keep their ordered CATEGORIES in the cache. Calendar range/detail responses expose the tags, and Event search hits expose the first tag. The shared UI eventTint uses tagColor to fill Events across Day, Week, Month, Agenda, Home, popovers, search, MiniMonth and Year. Untagged Events use a neutral fill. Event states have no coloured side bars or state borders.

The contrast sweep sampled 957 foreground/background pairs across all 29 themes and six Event surfaces. The minimum observed ratio was 4.614794:1. The real-server probe passed Unicode and bidi tags, a 65,536-byte category, seven malformed inputs and 24 parallel reads.

Files

  • apps/web/e2e/event-tint.mjs
  • apps/web/src/lib/calendar/agenda.svelte.test.ts
  • apps/web/src/lib/calendar/data.ts
  • apps/web/src/lib/calendar/event-tint.test.ts
  • apps/web/src/lib/calendar/model.test.ts
  • apps/web/src/lib/calendar/sidebarState.svelte.ts
  • apps/web/src/lib/components/app-sidebar.svelte
  • apps/web/src/lib/search/SearchPreview.svelte
  • apps/web/src/lib/search/SearchResultRow.svelte
  • apps/web/src/lib/search/registry.ts
  • apps/web/src/lib/search/search-units.test.ts
  • apps/web/src/lib/search/server.ts
  • apps/web/src/routes/calendar/[view]/[date]/+page.svelte
  • contracts/openapi.json
  • crates/calternal-api/src/lib.rs
  • crates/calternal-search/src/query.rs
  • crates/calternal-search/src/semantic.rs
  • crates/plugins/calendar/migrations/0003_event_tags.sql
  • crates/plugins/calendar/src/cache/store.rs
  • crates/plugins/calendar/src/client/mod.rs
  • crates/plugins/calendar/src/recurrence.rs
  • crates/plugins/calendar/src/routes.rs
  • crates/plugins/calendar/src/search.rs
  • crates/plugins/calendar/src/view.rs
  • crates/plugins/notifications/src/reminders.rs
  • crates/plugins/photos/src/search.rs
  • packages/api-client/src/generated.ts
  • packages/ui/src/components/calendar/AgendaList.svelte
  • packages/ui/src/components/calendar/GridColumn.svelte
  • packages/ui/src/components/calendar/ItemPreview.svelte
  • packages/ui/src/components/calendar/MiniMonth.svelte
  • packages/ui/src/components/calendar/MonthGrid.svelte
  • packages/ui/src/components/calendar/TimeGrid.svelte
  • packages/ui/src/components/calendar/YearHeatmap.svelte
  • packages/ui/src/components/calendar/model.ts
  • packages/ui/src/index.ts
  • packages/ui/src/tags.ts
  • packages/ui/src/themes.css
  • tests/adversarial/calendar_event_tags.mjs
  • tests/adversarial/run.sh

Gate output

cargo fmt --check exited 0 with empty output.

cargo clippy --all-targets -- -D warnings:

   Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/event-tint/crates/calternal-server)
    Checking calternal-plugin-notifications v0.0.1 (/home/kayg/Developer/calternal-wt/event-tint/crates/plugins/notifications)
    Checking calternal-plugin-photos v0.0.1 (/home/kayg/Developer/calternal-wt/event-tint/crates/plugins/photos)
    Checking calternal-collab v0.0.1 (/home/kayg/Developer/calternal-wt/event-tint/crates/calternal-collab)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 13.22s

cargo test completed successfully. Verbatim success lines:

    Finished `test` profile [unoptimized + debuginfo] target(s) in 7m 24s
test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.77s
test result: ok. 24 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 1.20s
test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.33s

bun run check:

$ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json
Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/event-tint/apps/web
Getting Svelte diagnostics...
svelte-check found 0 errors and 0 warnings

bun run test:

 Test Files  81 passed (81)
      Tests  589 passed (589)
   Duration  82.44s (transform 72%, import 12%, environment 9%, tests 6%, setup 2%)

Production E2E: event tint e2e: 29 themes, state contrast passed on 6 Event surfaces; 20 production screenshots saved to /home/kayg/Developer/calternal-wt/event-tint/target/event-tint-shots

Adversarial probe: calendar Event tag probe: Unicode/bidi, 65536-byte category, 7 malformed inputs, and 24 parallel reads passed

Decisions and gaps

  • The design does not set a text-colour formula. Tagged text blends 56% tag foreground with the theme contrast colour; past text moves to 82% of that mix toward the contrast colour. This passed the 4.5:1 check on all themes and surfaces.
  • Reserved _calternal/* categories do not set tint. A tagless or reserved-only Event uses the neutral tint, in line with the shared tag helper.
  • The focused Event tag probe is wired into tests/adversarial/run.sh and passed. The wider existing adversarial suite was not run.
  • No other gaps were found for the requested surfaces.

Production screenshots

Day

Theme 1440 × 900 390 × 844
Paper event-tint-day-paper-1440.png event-tint-day-paper-390.png
Midnight event-tint-day-midnight-1440.png event-tint-day-midnight-390.png

Week

Theme 1440 × 900 390 × 844
Paper event-tint-week-paper-1440.png event-tint-week-paper-390.png
Midnight event-tint-week-midnight-1440.png event-tint-week-midnight-390.png

Month

Theme 1440 × 900 390 × 844
Paper event-tint-month-paper-1440.png event-tint-month-paper-390.png
Midnight event-tint-month-midnight-1440.png event-tint-month-midnight-390.png

Agenda

Theme 1440 × 900 390 × 844
Paper event-tint-agenda-paper-1440.png event-tint-agenda-paper-390.png
Midnight event-tint-agenda-midnight-1440.png event-tint-agenda-midnight-390.png

Home

Theme 1440 × 900 390 × 844
Paper event-tint-home-paper-1440.png event-tint-home-paper-390.png
Midnight event-tint-home-midnight-1440.png event-tint-home-midnight-390.png
# Forgejo #238 — Event tint Implemented on `job/event-tint`; head SHA: `5e73e205d9706dc0197f2254b3252f7fb8f4364e`. Merged current `dev` before the final gates. ## Built CalDAV Events keep their ordered CATEGORIES in the cache. Calendar range/detail responses expose the tags, and Event search hits expose the first tag. The shared UI `eventTint` uses `tagColor` to fill Events across Day, Week, Month, Agenda, Home, popovers, search, MiniMonth and Year. Untagged Events use a neutral fill. Event states have no coloured side bars or state borders. The contrast sweep sampled 957 foreground/background pairs across all 29 themes and six Event surfaces. The minimum observed ratio was 4.614794:1. The real-server probe passed Unicode and bidi tags, a 65,536-byte category, seven malformed inputs and 24 parallel reads. ## Files - `apps/web/e2e/event-tint.mjs` - `apps/web/src/lib/calendar/agenda.svelte.test.ts` - `apps/web/src/lib/calendar/data.ts` - `apps/web/src/lib/calendar/event-tint.test.ts` - `apps/web/src/lib/calendar/model.test.ts` - `apps/web/src/lib/calendar/sidebarState.svelte.ts` - `apps/web/src/lib/components/app-sidebar.svelte` - `apps/web/src/lib/search/SearchPreview.svelte` - `apps/web/src/lib/search/SearchResultRow.svelte` - `apps/web/src/lib/search/registry.ts` - `apps/web/src/lib/search/search-units.test.ts` - `apps/web/src/lib/search/server.ts` - `apps/web/src/routes/calendar/[view]/[date]/+page.svelte` - `contracts/openapi.json` - `crates/calternal-api/src/lib.rs` - `crates/calternal-search/src/query.rs` - `crates/calternal-search/src/semantic.rs` - `crates/plugins/calendar/migrations/0003_event_tags.sql` - `crates/plugins/calendar/src/cache/store.rs` - `crates/plugins/calendar/src/client/mod.rs` - `crates/plugins/calendar/src/recurrence.rs` - `crates/plugins/calendar/src/routes.rs` - `crates/plugins/calendar/src/search.rs` - `crates/plugins/calendar/src/view.rs` - `crates/plugins/notifications/src/reminders.rs` - `crates/plugins/photos/src/search.rs` - `packages/api-client/src/generated.ts` - `packages/ui/src/components/calendar/AgendaList.svelte` - `packages/ui/src/components/calendar/GridColumn.svelte` - `packages/ui/src/components/calendar/ItemPreview.svelte` - `packages/ui/src/components/calendar/MiniMonth.svelte` - `packages/ui/src/components/calendar/MonthGrid.svelte` - `packages/ui/src/components/calendar/TimeGrid.svelte` - `packages/ui/src/components/calendar/YearHeatmap.svelte` - `packages/ui/src/components/calendar/model.ts` - `packages/ui/src/index.ts` - `packages/ui/src/tags.ts` - `packages/ui/src/themes.css` - `tests/adversarial/calendar_event_tags.mjs` - `tests/adversarial/run.sh` ## Gate output `cargo fmt --check` exited 0 with empty output. `cargo clippy --all-targets -- -D warnings`: ```text Compiling calternal-server v0.0.1 (/home/kayg/Developer/calternal-wt/event-tint/crates/calternal-server) Checking calternal-plugin-notifications v0.0.1 (/home/kayg/Developer/calternal-wt/event-tint/crates/plugins/notifications) Checking calternal-plugin-photos v0.0.1 (/home/kayg/Developer/calternal-wt/event-tint/crates/plugins/photos) Checking calternal-collab v0.0.1 (/home/kayg/Developer/calternal-wt/event-tint/crates/calternal-collab) Finished `dev` profile [unoptimized + debuginfo] target(s) in 13.22s ``` `cargo test` completed successfully. Verbatim success lines: ```text Finished `test` profile [unoptimized + debuginfo] target(s) in 7m 24s test result: ok. 46 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 3.77s test result: ok. 24 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 1.20s test result: ok. 12 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.33s ``` `bun run check`: ```text $ svelte-kit sync && svelte-check --tsconfig ./tsconfig.json Loading svelte-check in workspace: /home/kayg/Developer/calternal-wt/event-tint/apps/web Getting Svelte diagnostics... svelte-check found 0 errors and 0 warnings ``` `bun run test`: ```text Test Files 81 passed (81) Tests 589 passed (589) Duration 82.44s (transform 72%, import 12%, environment 9%, tests 6%, setup 2%) ``` Production E2E: `event tint e2e: 29 themes, state contrast passed on 6 Event surfaces; 20 production screenshots saved to /home/kayg/Developer/calternal-wt/event-tint/target/event-tint-shots` Adversarial probe: `calendar Event tag probe: Unicode/bidi, 65536-byte category, 7 malformed inputs, and 24 parallel reads passed` ## Decisions and gaps - The design does not set a text-colour formula. Tagged text blends 56% tag foreground with the theme contrast colour; past text moves to 82% of that mix toward the contrast colour. This passed the 4.5:1 check on all themes and surfaces. - Reserved `_calternal/*` categories do not set tint. A tagless or reserved-only Event uses the neutral tint, in line with the shared tag helper. - The focused Event tag probe is wired into `tests/adversarial/run.sh` and passed. The wider existing adversarial suite was not run. - No other gaps were found for the requested surfaces. ## Production screenshots ### Day | Theme | 1440 × 900 | 390 × 844 | | --- | --- | --- | | Paper | ![event-tint-day-paper-1440.png](https://git.kayg.org/attachments/8b801468-8c0e-4fab-8594-a16f97b68d7e) | ![event-tint-day-paper-390.png](https://git.kayg.org/attachments/73ae3d6c-756b-4e01-9556-2e40eb4e116e) | | Midnight | ![event-tint-day-midnight-1440.png](https://git.kayg.org/attachments/8a742f52-5ab4-4409-8af3-938c88786907) | ![event-tint-day-midnight-390.png](https://git.kayg.org/attachments/0c14abe0-f086-4147-8204-0924b85de80b) | ### Week | Theme | 1440 × 900 | 390 × 844 | | --- | --- | --- | | Paper | ![event-tint-week-paper-1440.png](https://git.kayg.org/attachments/ea3a9463-b7b5-4f19-9247-dea6d7fea232) | ![event-tint-week-paper-390.png](https://git.kayg.org/attachments/5d02a469-aea7-4ed1-b050-8f272954c08b) | | Midnight | ![event-tint-week-midnight-1440.png](https://git.kayg.org/attachments/4bbf308c-e0be-4692-97e7-cf97d5613e7f) | ![event-tint-week-midnight-390.png](https://git.kayg.org/attachments/2000f245-f493-4f7d-88fb-b41ff6c0923b) | ### Month | Theme | 1440 × 900 | 390 × 844 | | --- | --- | --- | | Paper | ![event-tint-month-paper-1440.png](https://git.kayg.org/attachments/4292068b-3f0b-436b-aff3-9f1b3111bd93) | ![event-tint-month-paper-390.png](https://git.kayg.org/attachments/c5b2cd58-dba0-4953-b4b4-2ce5d60ca3fe) | | Midnight | ![event-tint-month-midnight-1440.png](https://git.kayg.org/attachments/9d14f779-2135-4e50-aefa-f4ac8e0cd10f) | ![event-tint-month-midnight-390.png](https://git.kayg.org/attachments/d74aa163-c2c6-40ac-b5a6-accb79408e8d) | ### Agenda | Theme | 1440 × 900 | 390 × 844 | | --- | --- | --- | | Paper | ![event-tint-agenda-paper-1440.png](https://git.kayg.org/attachments/e2f5741c-5959-4c28-a774-658e9874b429) | ![event-tint-agenda-paper-390.png](https://git.kayg.org/attachments/20fbb333-5ca2-4a5c-922b-811a6f4d0f38) | | Midnight | ![event-tint-agenda-midnight-1440.png](https://git.kayg.org/attachments/eb641c3f-6326-411c-9c83-e56596b92f34) | ![event-tint-agenda-midnight-390.png](https://git.kayg.org/attachments/622319e8-fea9-40f4-a8a2-1b9d95daf4c6) | ### Home | Theme | 1440 × 900 | 390 × 844 | | --- | --- | --- | | Paper | ![event-tint-home-paper-1440.png](https://git.kayg.org/attachments/2158c94a-f6dc-4779-b1bd-4f4c1f6a2a86) | ![event-tint-home-paper-390.png](https://git.kayg.org/attachments/c14de09e-3ae4-4b12-8af1-99f5b1b399d8) | | Midnight | ![event-tint-home-midnight-1440.png](https://git.kayg.org/attachments/2fc36578-7910-4a1a-878f-90dbbb47448b) | ![event-tint-home-midnight-390.png](https://git.kayg.org/attachments/c016413c-386b-4473-9d8e-90289ecb7518) |
kayg referenced this issue from a commit 2026-09-27 16:49:37 +00:00
Author
Owner

Merged in 200d171e after orchestrator review of Agenda/Month/Day/phone captures.

Merged in 200d171e after orchestrator review of Agenda/Month/Day/phone captures.
kayg closed this issue 2026-09-27 16:49:39 +00:00
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
kayg/calternal#238
No description provided.