Thank you for being patient! We're working hard on resolving the issue
Parent plugins host subcolumn children in a shared grid layout. The calendar plugin is the primary example — it renders a time grid and lets child rows (calendar-list, task-list) place items within it.
A parent plugin:
subcolumnProviderParent plugin (calendar)
├── Owns the grid layout (time columns, hour markers)
├── Owns the gesture manager (pointer tracking across cells)
├── Discovers children via row.children()
└── Positions menus for children via SheetDelegate handles
Child plugin (calendar-list)
├── Provides items via subcolumnProvider.events()
├── Provides cell rendering via subcolumnProvider.bindCell()
├── Creates menu content (details, forms, context menus)
└── Calls positioning APIs provided by parent
In bindCell(), iterate the row's children and collect subcolumns:
bindCell($cell: CalendarColumn, context: BindCellContext): void {
const { row, handle } = context;
const subcolumns = this.getSubcolumns(row, handle.pluginManager);
$cell.bind({ subcolumns, /* ... */ });
}
private getSubcolumns(
row: SheetRow.Joined.Client,
pluginManager: SheetDelegate.PluginManager,
): CalendarSubcolumn<any>[] {
const children = row.children?.() ?? [];
const subcolumns: CalendarSubcolumn<any>[] = [];
for (const child of children) {
const plugin = pluginManager.getPlugin(child.type);
const sub = plugin.subcolumnProvider?.subcolumnFromRow(sheet, child, row);
if (sub) subcolumns.push(sub);
}
return subcolumns;
}
This is fully generic — any plugin with a subcolumnProvider becomes a
child. The parent doesn't know or care what content the child renders.
For cell-based subcolumns (renderer: "cells"), the parent creates
calendar cells and lets each subcolumn bind them:
for (const subcolumn of subcolumns) {
if (subcolumn.renderer !== "cells") continue;
const items = subcolumn.events(timeRange);
for (const item of items) {
const $cell = CalendarColumnCell.make();
// Child controls appearance
subcolumn.bindCell($cell, item, { windowed, currentTime });
// Parent controls position (time → pixel offset)
const top = this.timeToPixels(item.startTime);
const height = this.durationToPixels(item.duration);
$cell.assignStyles({ top: `${top}px`, height: `${height}px` });
$column.appendChild($cell);
}
}
For span-based subcolumns (renderer: "spans"), the parent requests
span data and renders horizontal bars across columns.
The parent creates gesture info for each cell. The gesture info carries the parent's coordinate data (column position, row ID) that children need for positioning menus:
// Parent creates gesture info with its coordinate system
const gestureItem = subcolumn.gestureItem(item);
const info = new GestureInfo(
rowId,
gestureItem, // from child
column.withSubindex(subindex), // from parent
windowed, // from parent
);
// Bind cell to gesture manager
$cell.bindCallbacks(
this.gestureManager.asCellDelegate(info),
);
The child provides gestureItem() — a wrapper around its item that the
gesture system can inspect. The parent provides column and windowed —
the coordinate data needed to position menus.
The parent owns a gesture manager that tracks pointer state across all cells. Gesture handlers are registered on this manager:
constructor() {
this.gestureManager = new CalendarGesture.Manager<GestureInfo>();
}
The app layer registers gesture handlers that use both the child's content and the parent's positioning:
// App layer wiring
manager
.addGesture(new ClickToOpenGesture(presenter, coordinator))
.addGesture(new DragToCreateGesture(presenter, coordinator))
.addGesture(new ContextMenuGesture(presenter, coordinator));
If children provide fullDayEventsFromRow, the parent aggregates them
for the all-day row:
subcolumnProvider: CalendarSubcolumn.Provider<EventItem, FullDayItem> = {
subcolumnFromRow(sheet, row, parentRow) { /* ... */ },
fullDayEventsFromRow(row, range): FullDayItem[] {
const children = row.children?.() ?? [];
const items: FullDayItem[] = [];
for (const child of children) {
const plugin = pluginManager.getPlugin(child.type);
const childItems = plugin.subcolumnProvider?.fullDayEventsFromRow?.(child, range);
if (childItems) items.push(...childItems);
}
return items;
},
};
Parent plugins can implement different grid layouts while keeping the child interface identical:
// Column-based positioning — scroll-aware
handle.popover.reveal(id, $content, { x: column, y: rowId }, { offsetY });
The daily grid uses ColumnIndex.Key.Resolved.WithSubindex for X
coordinates. Each column is a date or time slot. Popovers track scroll
position automatically.
// Pixel-based positioning — no column tracking
const $cell = this.$calendar.getCellForDate(date);
const rect = $cell.getBoundingClientRect();
const offset = { x: rect.left - calRect.left, y: rect.top - calRect.top };
handle.staticChild.reveal(id, $content, rowId, { offset });
The monthly grid uses raw pixel offsets computed from DOM rects. No column index, no scroll awareness — the entire month fits in one row.
To create a new parent layout (e.g., a Gantt chart), implement a plugin that:
row.children() + subcolumnProviderThe child subcolumns don't change — they still provide events(),
bindCell(), and gestureItem() the same way.