Something went wrong

Thank you for being patient! We're working hard on resolving the issue

Calendar - Lona Docs Log in

Calendar Template

The calendar template renders a calendar surface — timed events on a day/week grid, the connected calendar list as a sidebar, and a secondary timezone strip when the user has one configured.

It's the largest builtin template and demonstrates three patterns together: nested aliases, layout switching, and app-layer dependencies.

Wire shape

{
  "type": "alias",
  "label": "Calendar",
  "attributes": { "template": "calendar" }
}

The calendar template takes no props — one alias row produces one calendar surface for the active user.

Expanded shape

The template's activeLayout rule synthesizes a layout row at hydrate time — timeline[column] for day/week, grid for month. That row's DOM-side plugin (TimelineColumnLayout / CalendarMonthPlugin) owns the time-grid (or month-grid) surface. The calendar-list legend and secondary-timezone bar nest under the synthesized layout row via the template's layoutChildren field — those aliases get subsumed by the layout's activeLayout and absorbed into the surface's slot machinery instead of rendering as standalone rows.

alias                             template=calendar
├─ data-source                    preset=calendar              ← TRANSITIONAL ⚠
│  │                              attrs={dtype:"obj", startHr:0, endHr:24}
│  └─ data-renderer               type=calendar                ← column.calendar painter
└─ timeline[column]               ← synthesized by activeLayout(timeScale)
                                    "timeline[column]" for day/week,
                                    "grid" for month
   ├─ alias                       template=calendar-list
   └─ alias                       template=preferences-secondary-timezones

The data-source and the synthesized layout row are siblings under the alias. The alias's static children rule emits only the data-source; layoutChildren declares the calendar-list + secondary-timezones aliases that the hydrator places under the synthesized layout row. When the user switches timescale, Sheet.setTimeScale(s) re-runs hydration so the layout row swaps type (timeline[column] ↔ grid).

Transitional shape. The data-renderer (type=calendar) is the painter today — it resolves to the column.calendar plugin (CalendarSheetPlugin) which paints the time-grid surface (hourly divisions, timed-event positioning along a vertical hour axis) and walks the descendants for legend / timezone data.

TimelineColumnLayout (the new layout-role plugin for timeline[column]) takes the same painting logic but binds at the synthesized layout row, walking up to the alias and then sideways to the sibling data-source for the CalendarRenderer typed accessor. Once the live calendar is verified to paint correctly through TimelineColumnLayout, the transitional data-renderer (type=calendar) child drops out, the data-source itself can retire, and CalendarSheetPlugin retires alongside it.

Calendar nomenclature

IdentifierRoleNotes
calendaralias template, row type, column.calendar rendererOne per connected calendar; the transitional renderer paints the time grid today
calendar-listalias template, row type, aggregatorSubsumes multiple per-account calendars; provides the unified events facade
calendar-alldayalias templateWraps calendar-list under timeline-row for the all-day strip
timeline[row] / timeline[column] / gridlayout row typesSynthesized by activeLayout at hydrate time; timeline[row] (all-day strip), timeline[column] (timed grid for day/week), grid (month)
TimelineRowLayout / TimelineColumnLayoutDOM layout pluginsOne class per layout id; timeline-plugin.ts + calendar-plugin.ts are transitional and retire once the new classes paint end-to-end
timeline[*].eventrenderer suffixSingular event by convention; event-list is not a thing in this codebase
EventsIndexclient query APIrow.index(EventsIndex) returns event queries
EVENTS_FACADErows-side facade keyEstablished; do not rename

The two nested aliases recursively expand. calendar-list is itself dynamic-tail driven — one source row per connected provider account:

alias                             template=calendar-list      (app-layer)
├─ data-source                    lookupKey=:~google:auth_a:calendar:cal_x
│  ├─ data-renderer               type=calendar
│  ├─ data-renderer               type=event, slot=timeline[row]
│  ├─ data-renderer               type=event, slot=timeline[column]
│  └─ data-renderer               type=event, slot=grid
├─ data-source                    lookupKey=:~google:auth_a:calendar:cal_y
│  └─ ...
└─ data-source                    lookupKey=:~apple:auth_b:calendar:cal_z
   └─ ...

preferences-secondary-timezones produces one nested timezone alias per configured tz:

alias                             template=preferences-secondary-timezones
├─ alias                          template=timezone, props.tz=Pacific/Auckland
│  └─ data-source ...             (see timezone template page)
└─ alias                          template=timezone, props.tz=Europe/London
   └─ data-source ...

Layout switching

The calendar template declares an activeLayout rule:

activeLayout: (timeScale) =>
  timeScale === "month" ? "grid" : "timeline[column]"

This means the same alias row picks a different layout plugin at the month time scale — the FE binds the grid layout (month grid) instead of the timeline[column] layout (timed grid) without rewriting the tree. No mutation; the layout is selected per-render from the sheet's active time scale. Children whose subsumedBy list contains the active layout id (e.g. calendar-list's subsumedBy: ["timeline[column]","grid","timeline"]) get hidden and absorbed into the parent's slot machinery instead of rendering as standalone rows.

All-day strip

The all-day strip is not nested inside the calendar template. It's a separate top-level alias (calendar-allday) that the system auto-pairs with each calendar row via the header binding mechanism — users see them paired visually without having to add the all-day row themselves.

The two are peers at the row tree level:

[Sheet root]
├─ alias                          template=calendar
│  └─ data-source (preset=calendar)
│     └─ ...
└─ alias                          template=calendar-allday        ← peer, auto-linked in header
   └─ timeline-row                label="All-day"
      └─ alias                    template=calendar-list
         └─ ... (one data-source per connected calendar)

The all-day strip's events come from the same calendar-list aggregate the timed grid uses (just filtered to all-day cells via calendar-allday-events's fullDayEventsInRange filter). The visual pairing reflects the shared data flow.

App-layer dependencies

calendar-list and preferences-secondary-timezones aren't pure-static factories — they each need a closure:

TemplateDependencySource
calendar-listCalendarConfigProvidertento/tento-lona-js/src/preferences/calendar-aliases.ts
preferences-secondary-timezonesprefs LiveDatatento/tento-lona-js/src/preferences/preferences-aliases.ts

Apps register them at SheetsAccessor construction by passing calendarListAliasFactory(provider) and secondaryTimezonesAliasFactory(prefs) alongside defaultBuiltinAliasFactories(). Tests that don't exercise the calendar-list dynamic tail can register stub factories that return empty children.

Events

Events themselves are not a template — they're cells on the calendar data-source. See Events for the events surface, time-range shape, and provider routing.

See also