Thank you for being patient! We're working hard on resolving the issue
When multiple plugins show menus (popovers, context menus, forms), the gesture coordination system ensures only one is visible at a time and dismisses others cleanly.
The coordination system has three parts:
User clicks event in daily view
→ Daily system opens event details
→ coordinator.globalState.dismissOtherSystems(self)
→ All-day system: dismisses its open form
→ Monthly system: dismisses its open popover
→ Daily system registers its state
Any plugin that shows menus should implement GestureSystem:
import type { GestureSystem } from "@tento-lona/sheets-ui";
class TaskGestureHandler implements GestureSystem {
closeAllMenus(): void {
this.dismissTaskDetails();
this.dismissContextMenu();
this.dismissCreateForm();
}
}
Register with the coordinator when your plugin binds to a row:
onBindRow(handle: SheetDelegate, row: SheetRow.Joined.Client): void {
this.coordinator.register(this.gestureHandler);
}
When your gesture handler shows a menu, follow this protocol:
showTaskDetails(task: TaskItem, position: Position): void {
// 1. Create your content (you decide what to show)
const $details = TaskDetails.make(task);
// 2. Dismiss other systems
this.coordinator.globalState.dismissOtherSystems(this);
// 3. Show via the sheet's positioning API
handle.popover.reveal("task-details", $details, position, {
offsetY: this.computeOffset(task),
context: "task-details",
});
// 4. Register your state so others can dismiss you
this.coordinator.globalState.registerState({
source: this,
id: "task-details",
dismiss: () => {
handle.popover.dismiss("task-details");
this.state = null;
},
});
}
Steps 2 and 4 are critical. Without step 2, your menu opens alongside another system's menu. Without step 4, no one can dismiss yours.
When hiding your own menu, unregister the state:
hideTaskDetails(): void {
this.coordinator.globalState.unregisterState("task-details");
handle.popover.dismiss("task-details");
this.state = null;
}
When creating new items (drag-to-create), the first click after another
system's menu is open should dismiss that menu — not start a new creation.
Use CrossSystemDismissalGuard for this:
import { CrossSystemDismissalGuard } from "@tento-lona/sheets-ui";
class TaskCreateGesture {
private guard = new CrossSystemDismissalGuard(this, this.coordinator.globalState);
canHandle(event: GestureEvent): boolean {
// If another system was dismissed this interaction, block
if (this.guard.shouldBlock(event.isPointerDown)) return false;
return event.isEmptyCell;
}
}
The guard tracks pointer interactions. On the first pointer-down after a dismissal, it blocks. On the next pointer-down, it allows.
The sheet provides four positioning handles. Choose based on your needs:
For details panels, create forms. Tracks scroll position.
handle.popover.reveal(
"task-details",
$content,
{ x: column, y: rowId }, // Point<WithSubindex, RowLocalId>
{ offsetY: 48, context: "task-details" },
);
x: Column coordinate — ColumnIndex.Key.Resolved.WithSubindexy: Row coordinate — RowLocalIdoffsetY: Pixel offset from row top (e.g., based on time position)For context menus, inline controls. Positioned relative to a column anchor with a pixel inset.
handle.fixedChild.reveal(
"task-context-menu",
$menu,
{ x: column, y: rowId },
{ inset: { left: offset.x, top: offset.y }, widthBehavior: 240 },
);
For content within a single row that doesn't need column tracking. Used by the monthly calendar view.
handle.staticChild.reveal(
"month-popover",
$content,
rowId,
{ offset: { x: pixelX, y: pixelY } },
);
handle.tooltip.showTooltip(
"task-tooltip",
$tooltip,
{ x: column, y: rowId },
"bottom",
);
Use descriptive, namespaced IDs for your states:
// Good
"task-pending-new"
"task-existing-details"
"task-context-menu"
// Bad — collides with other systems
"pending"
"details"
"menu"
If your content supports switching modes (e.g., timed ↔ all-day), register a transfer target:
coordinator.registerTransferTarget("task-allday", {
receive(payload: TransferPayload): void {
// Another system handed off a creation to us
this.showCreateForm({
title: payload.preservedTitle,
dateRange: payload.dateRange,
tz: payload.tz,
});
},
});
// To initiate a transfer from your system:
coordinator.transfer("task-allday", {
preservedTitle: currentForm.title,
dateRange,
tz,
});