Skip to content

Add PDCalendar component - #244

Merged
davidnmbond merged 6 commits into
mainfrom
feature/pdcalendar
Oct 15, 2026
Merged

davidnmbond merged 6 commits into
mainfrom
feature/pdcalendar

Conversation

@davidnmbond

Copy link
Copy Markdown
Contributor

Summary

Adds PDCalendar<TItem>, a generic calendar for anything with a start and an end in time. It reads items through selector delegates, so any type can be shown without converting it to a calendar model.

  • Views: a Month grid, a Week time grid (seven day columns against an hour axis) and an Agenda list for narrow screens.
  • Toolbar: previous, next, today, a title ("October 2026", "12 to 18 Oct 2026") and a view switcher, built from Bootstrap button groups; every part can be hidden.
  • Time zones: TimeZone (default TimeZoneInfo.Local) decides which day and hour an item falls in. The week view's hour axis is real elapsed time, so the day the clocks go forward shows 23 hours and the day they go back shows 25, with the repeated hour labelled by its UTC offsets. Items spanning midnight are split across day columns.
  • Layout: overlapping items in the week view sit side by side (greedy column packing, with a shared column count across a chain of overlaps). The month view lists up to MaxItemsPerDay items per day with a "+n more" button that opens that day's agenda, and shows multi-day items on every day.
  • Lazy loading: OnVisibleRangeChanged is raised whenever the visible range changes, including on first show; IsLoading shows the library's PDBusyOverlay.
  • Accessibility: ARIA grid roles with the focused day or hour as the active descendant. Arrow keys move, Enter or Space selects, Page Up and Page Down change period, and Home goes to today. Items are buttons. Transitions respect prefers-reduced-motion, and below 576 px the month view collapses items to dots.
  • No JavaScript unless ScrollToHour is set; that uses the existing setProperty helper in common.js.

Public API

  • PDCalendar<TItem> parameters:
    • Data: Items, StartSelector, EndSelector, TitleSelector, KeySelector, CssClassSelector, TooltipSelector, IsDisabledSelector, ItemTemplate, TimeZone, Clock (TimeProvider).
    • State: Date/DateChanged (DateOnly?, null means today), View/ViewChanged, AvailableViews, SelectedItem/SelectedItemChanged, FirstDayOfWeek (default Monday), MinDate, MaxDate, IsLoading.
    • Events: OnItemSelected, OnItemDoubleClick, OnSlotSelected (DateTimeOffset), OnVisibleRangeChanged ((DateTimeOffset From, DateTimeOffset To)).
    • Layout: FirstVisibleHour, LastVisibleHour, SlotHeight, MinItemHeight, ScrollHeight, ScrollToHour, MaxItemsPerDay, AgendaDays, NowIndicator, ShadeWeekends, WeekendDays, DayCssClassSelector, TimeFormat, ShowToolbar, ShowNavigation, ShowToday, ShowTitle, ShowViewSwitcher, EmptyText, AriaLabel, CssClass.
  • Methods: NavigateAsync, GoToTodayAsync, GoToDateAsync, SetViewAsync, ShowDayAgendaAsync, CanNavigate. Read-only properties: Id, CurrentDate, CurrentView, CurrentSelection, VisibleRange, Title.
  • New enum: CalendarView (Month, Week, Agenda).
  • New public static helpers, each usable and testable without a renderer: CalendarMath (visible dates and ranges, navigation, start and length of a day, wall clock to instant with gap and overlap handling, titles), CalendarLayoutEngine (SplitByDay, PackColumns).
  • New models: CalendarTimeAxis, CalendarAxisRow, CalendarHourSlot, CalendarSegment<TItem>.

No existing public member is changed or removed.

Tests and coverage

  • 126 new tests: 71 plain unit tests for the helpers and 55 bUnit tests for the component. They cover month and week range maths with each first day of week, the Europe/London spring and autumn changes, a custom zone where midnight is skipped and one where it is repeated, items spanning midnight and days, overlap packing with one, two, three and chained overlaps, selection, keyboard handling, view switching, OnVisibleRangeChanged, MinDate and MaxDate, ItemTemplate, "+n more", the empty state, time zone changes, the current time indicator driven by a test TimeProvider, ScrollToHour and disposal.
  • Full suite: 3,927 tests, 0 failed, 0 skipped.
  • Coverage of the new library files with the repo's coverage.config: 576 of 576 lines (100%) and 299 of 300 branches. Because that config leaves out [CompilerGenerated] code, and so async method bodies and Razor lambdas, I also measured with that exclusion removed: 913 of 916 lines (99.7%) and 457 of 474 branches (96.4%). The three lines not covered are the ObjectDisposedException guard in the minute timer callback, which only runs if a tick races the renderer's disposal.

Command used:

dotnet test PanoramicData.Blazor.Test/PanoramicData.Blazor.Test.csproj -- --coverage --coverage-output-format cobertura --coverage-settings coverage.config --results-directory <dir> --coverage-output result.cobertura.xml

Demo

A new /pdcalendar page (Demo | Source | Documentation) shows a two-station radio schedule in Europe/London, loaded lazily with a deliberate delay so the busy overlay shows. It has overlapping shows, a late show running past midnight, a festival spanning a weekend, a button to show the week the clocks go back (25 October 2026), controls for most parameters, and a second agenda-only calendar showing ItemTemplate and disabled items. Also added to the NavMenu. ComponentDocumentation.md is regenerated with GenerateDocs.ps1. The hand-written PDChat notes that the script would have dropped are kept, and one generated PDTree line is left out because its source XML summary contains em dashes.

I checked the page by running the Web host and driving it with headless Chromium (Playwright). I confirmed the week, clock-change week, month, "+n more" to agenda, phone-width (no horizontal scroll) and Documentation views, keyboard focus movement and item selection. The only console errors were the existing BlazorMonaco "monaco is undefined" messages, which also appear on other documentation tabs such as /pdbutton?tab=docs. I did not test with a screen reader or on a real phone.

dotnet build PanoramicData.Blazor.slnx gives 0 errors. The 2 warnings are the existing "packaging has been disabled" messages from the Web and WebAssembly.Server hosts.

🤖 Generated with Claude Code

davidnmbond and others added 4 commits October 15, 2026 14:35
PDCalendar<TItem> shows any item type through selector delegates, in a
month grid, a week time grid or an agenda list, with a Bootstrap
toolbar, two-way Date, View and SelectedItem, lazy loading through
OnVisibleRangeChanged, keyboard navigation and ARIA grid roles.

The date and layout logic is in testable helpers: CalendarMath (view
ranges, navigation, time zone aware day boundaries), CalendarLayoutEngine
(splitting items by day, greedy overlap column packing) and
CalendarTimeAxis (an hour axis that stays aligned across daylight saving
changes, giving 23 and 25 hour days their true lengths).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
bUnit tests cover the month, week and agenda views, selection, slot
selection, keyboard handling, view switching, the toolbar, MinDate and
MaxDate bounds, OnVisibleRangeChanged, the current time indicator and
its timer, ScrollToHour and disposal. Plain unit tests cover
CalendarMath, CalendarLayoutEngine and CalendarTimeAxis, including the
Europe/London spring and autumn clock changes, a zone where midnight is
skipped and one where it is repeated, items spanning midnight and days,
and overlap packing with one, two, three and chained overlaps.

NavigateAsync, GoToTodayAsync, GoToDateAsync, SetViewAsync and
ShowDayAgendaAsync now re-render the calendar themselves, so a host
calling them through a component reference sees the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The day number in a week view header is centred under the weekday
name, and week view items put the title before the times, since a
narrow time block has room for little else. The period title is plain
text rather than a heading, so it does not disturb the host page's
heading outline.

Found by checking the demo page in headless Chromium.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The demo shows a two-station radio schedule in Europe/London, loaded
lazily through OnVisibleRangeChanged with the busy overlay, with
overlapping shows, a late show running past midnight, a festival
spanning the weekend and a button for the week the clocks go back
(25 October 2026). A second calendar shows ItemTemplate and disabled
items in an agenda. The Documentation tab describes the API, events,
keyboard support, time zone handling and styling.

ComponentDocumentation.md is regenerated with GenerateDocs.ps1. The
hand-written PDChat notes that the script would drop are kept, and one
generated PDTree line whose source summary contains em dashes is left
out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codacy-production

codacy-production Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 374 complexity · 2 duplication

Metric Results
Complexity 374
Duplication 2

View in Codacy

🟢 Coverage 100.00% diff coverage · +0.01% coverage variation

Metric Results
Coverage variation ✅ +0.01% coverage variation
Diff coverage ✅ 100.00% diff coverage

View coverage diff in Codacy

Coverage variation details
Coverable lines Covered lines Coverage
Common ancestor commit (1b4b7bc) 16056 16033 99.86%
Head commit (da89ab9) 16679 (+623) 16656 (+623) 99.86% (+0.01%)

Coverage variation is the difference between the coverage for the head and common ancestor commits of the pull request branch: <coverage of head commit> - <coverage of common ancestor commit>

Diff coverage details
Coverable lines Covered lines Diff coverage
Pull request (#244) 623 623 100.00%

Diff coverage is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: <covered lines added or modified>/<coverable lines added or modified> * 100%

AI Reviewer: run a review on demand. To trigger the first review automatically, go to your organization or repository integration settings. AI can make mistakes. Always validate suggestions.

Run reviewer

TIP This summary will be updated as you push new changes.

davidnmbond and others added 2 commits October 15, 2026 16:23
- Split long or branchy methods (OnParametersSetAsync,
  OnAfterRenderAsync, OnKeyDownAsync, CalendarLayoutEngine.SplitByDay)
  into small helpers; key handling is now a lookup from key to action.
- Give conditional expression-bodied members block bodies, so Lizard
  counts them as methods rather than file-level code.
- Move static defaults out of the generic component into a non-generic
  CalendarDefaults class.
- CalendarMath.DaysPerWeek is a static read-only property, and
  CalendarTimeAxis has an internal constructor.
- The minute timer passes the component as its state instead of reusing
  a discard parameter.
- Reorder CSS rules so specificity never descends, add the missing empty
  line, use inset, flex-flow and :not() selector lists.
- The demo page's colours move from scoped CSS with ::deep to an inline
  style block, as PDTimelinePage does.
- Test fakes use all their parameters, and a test property no longer
  shadows a helper method.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GetDayClass, GetItemClass and GetSlotClass now pass (condition, class)
pairs to one JoinClasses helper instead of a chain of if statements,
which brings GetDayClass under Lizard's complexity limit. The test
JS runtime fake records the identifier of each call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@davidnmbond
davidnmbond merged commit a227c82 into main Oct 15, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant