Something went wrong

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

Row Roles & Facades - Lona Docs Log in

Row Roles & Facades

Every row in a sheet has a role that governs its behavior, and a wire type that names its presentation. The role determines the row's base class and which APIs are available on it. Typed access to domain data — calendar events, tasks, weather — flows through facades attached to rows at construction time.

This doc is the reference for the role hierarchy, the facade registry, and the RowTypeSpec metadata table that drives defaults and subsumption.

The Four Roles

RoleBase classWhat it owns
sourceSourceRowData — user edits, provider pushes, formula output
layoutLayoutRowA rendering container (timeline, canvas, grid, list, column)
rendererRendererRowA single filled slot inside a layout
aliasAliasRowA Figma-style template instance with props + overrides

The role lives on the row's immutable descriptor; it's fixed at construction and never changes.

import { SourceRow, LayoutRow, RendererRow, AliasRow } from "@tento-lona/sheets";

const row = sheet.row({ label: "Weight" });

switch (row?.descriptor.role) {
  case "source":   // row is SourceRow — has cells(), setCell(), facades
  case "layout":   // row is LayoutRow — renders children into slots
  case "renderer": // row is RendererRow — fills one slot
  case "alias":    // row is AliasRow — instance of a template
}

SourceRow

Owns or computes the row's cells. Every user-editable row, every provider- backed row (calendar, weather, tasks, garmin, etc.), and every formula row is a SourceRow subclass. Two concrete subclasses:

  • DataSource — user or provider cells. Reads/writes through sheet.stores.sourceCellData.
  • FormulaDataSource — cells owned by the formula engine. Writes reject; the engine recomputes on dependency changes.
  • ComputedDataSource — cells synthesized by a registered computed source (see COMPUTED_SOURCE_BUILDERS).

Typed cell access for scalar dtypes lives on the auto-hydrated column.* renderer child (renderer-as-editor). Typed access for object dtypes goes through facades — see below.

LayoutRow

A container that provides rendering slots for its children. Timeline, calendar-month, canvas, grid, column, list, group, block, header — each is a distinct LayoutRow subclass.

Layout rows don't own cells; they arrange the output of their children.

RendererRow

A single filled slot inside a parent layout. Renderers are named <layout>.<renderer> — for example:

  • column.text, column.number, column.checkmark, column.graph
  • canvas.hrule, canvas.line, canvas.points, canvas.regression-band
  • timeline[row].event, timeline[column].number
  • grid.event, grid.text, list.event, list.number

When a SourceRow needs to render as a standalone row (not subsumed by an ancestor), the framework hydrates a default renderer child via resolveDefaultRendererId + hydrateDefaultRenderer.

AliasRow

A template instance in the Figma sense — an alias carries a reference to a master template plus a prop bag and an override bag. Editing the master propagates to every instance; per-instance overrides live on the alias row itself. Calendar-list is the canonical example — the calendar row holds a virtual alias to the :calendar-list-template master.

const aliasRow = sheet.row({ type: "calendar-list" });
if (aliasRow instanceof AliasRow) {
  const instance = aliasRow.instance();
  console.log(instance.template, instance.props, instance.overrides);
  await aliasRow.setProp("showWeekNumbers", true);
}

Facades

A facade is a typed API over a row's cells, attached at row construction by a FacadeFactory. You obtain one via row.as(FACADE_KEY). The facade returns null when the row's shape isn't compatible — so the optional-chain pattern is always safe.

Built-in facades

FacadeKeyMatchesTypical use
EventsEVENTS_FACADEsource rows with dtype="obj" + event-shape cellsCalendar events, meetings
TasksTASKS_FACADEsource rows with task-shape cellsTodo / task lists
WeatherWEATHER_FACADEsource rows with weather-shape cellsDaily observations, forecasts
DataDATA_FACADEsource rows with scalar cells + graph propsTime-series + graph config
Scheduling linkSCHEDULING_LINK_FACADEsource rows with scheduling-link attrsAvailability + booking slots
import {
  EVENTS_FACADE,
  TASKS_FACADE,
  WEATHER_FACADE,
  DATA_FACADE,
  SCHEDULING_LINK_FACADE,
} from "@tento-lona/sheets";
import { NaiveDate } from "@tento-chrono";

const calendarRow = sheet.row({ type: "calendar" });
const events = calendarRow?.as(EVENTS_FACADE);

if (events) {
  const today = NaiveDate.today();
  // Raw Events for a date
  for (const event of events.eventsOn(today)) {
    console.log(event.summary, event.start);
  }

  // Hydrated EventItem overlapping a UTC range
  const item = events.find("event-id-123");

  // Mutations route through row.setCell
  await events.addEvent({ id: "...", summary: "...", start: ..., end: ... });
}

Writing a custom facade

Facades are registered per-sheet. A factory is { key, build(row) }; the registry walks every factory at row construction and installs the facades whose build(row) returns non-null.

import { FacadeKey, FacadeRegistry, SourceRow } from "@tento-lona/sheets";

class MyFacade {
  constructor(private readonly row: SourceRow) {}
  query(): readonly MyThing[] { /* ... */ }
}

export const MY_FACADE = new FacadeKey<MyFacade>("my-facade");

export const MY_FACADE_FACTORY = {
  key: MY_FACADE,
  build(row: SourceRow): MyFacade | null {
    if (row.descriptor.dtype !== "obj") return null;
    // ... additional shape gating ...
    return new MyFacade(row);
  },
};

// Register at sheet setup
sheetStores.facades.register(MY_FACADE_FACTORY);

RowTypeSpec Registry

Per-wire-type static metadata lives in a single registry, ROW_TYPE_SPECS, keyed by wire type string ("calendar", "weather", "formula", …). The spec carries:

FieldPurpose
defaultAttributes(row)Seed attribute bag for new rows of this type
childSpecsDeclarative child-row seeds expanded at hydrate time
subsumedByLayout keys that absorb this row (don't render standalone)
activeLayout(timescale)Which layout key this row provides at a given timescale
rendersChildren(attrs)Whether children render (e.g. collapsed blocks return false)
heightBoundsMin/max height clamp enforced by setHeight
import { ROW_TYPE_SPECS, reconcileAttributes } from "@tento-lona/sheets";

const spec = ROW_TYPE_SPECS.get("calendar");
const defaults = spec?.defaultAttributes?.(rawRow) ?? {};
const attrs = reconcileAttributes(defaults, rawRow.attributes);

reconcileAttributes(defaults, instance) merges static defaults with the row's wire attributes. Instance values win at the top level; the children visibility bag is deep-merged per preset so instances can hide a specific default child without re-specifying the rest.

Subsumption

When a parent layout's activeLayout(timescale) returns a key that appears in a child's subsumedBy list, the child doesn't render as a standalone row — the parent's slot paints its cells. Example: calendar-list declares subsumedBy: ["calendar.day", "calendar.week"], so in day or week view it vanishes as a row and its events feed the calendar layout. In month view, the calendar's activeLayout("month") returns "calendar.month" (not in the list), so calendar-list renders as its own row.

Dep Direction

All of the above — roles, facades, ROW_TYPE_SPECS, RowLocalId — live in @tento-lona/sheets. The SDK (@tento-lona) re-exports many of them but the canonical home is the rows package.

yuzu-js/core, @tento-chrono       ← pure data-structures
       ↑
@tento-lona/sheets                     ← rows + facades + layout math (no DOM)
       ↑
@tento-lona                          ← SDK: clients, HTTP backend, preferences
       ↑
@tento-lona/sheets-ui                   ← sheet rendering, plugins (uses DOM)
       ↑
lona-so                           ← application

See Also