MarkupRefineLib

Menu


Layer results and local subinteractions

Phase 6 adds semantic completion without adding navigation or HTTP. A Layer can close normally, accept a caller-defined value, or dismiss with an optional reason. Direct workflow composition uses a Promise returned by ask().

Outcome contract

type LayerOutcome<T, R = unknown> =
  | { status: "accepted"; value: T }
  | { status: "dismissed"; reason?: R };

Ordinary user cancellation is data, not an exception. Runtime/programming failures may still reject, but Escape, request-close, light dismiss, or an ordinaryclose() while an ask() is pending resolve as{ status: "dismissed" }.

Three completion operations

OperationMeaningResult behavior
close()Close the surface without declaring success.A pending ask() resolves as dismissal with no reason.
accept(value)Complete successfully with an opaque caller-defined value.Closes, emits mr:layer:result, resolves ask() as accepted.
dismiss(reason)Complete without acceptance, optionally explaining why.Closes, emits mr:layer:result, resolves ask() as dismissed.

Book → Author local proof of concept

This example has no fetch, router, remote fragment, entity protocol, or parent refresh primitive. The Author Layer returns an opaque object. The caller alone decides to copy its name into the Book form.

Book

New Author

type Author = { name: string };

authorButton.addEventListener("click", async () => {
  const outcome = await layers.ask<Author, "cancel-button">(
    authorDialog,
    { trigger: authorButton },
  );

  if (outcome.status === "accepted") {
    bookAuthorInput.value = outcome.value.name;
  }
});

The Author opener sits inside the active Book Layer, so normal ownership inference makes Book the parent. Accepting or dismissing Author closes only Author. The Book Layer stays open, focus returns to the quick-create button, and only then does the awaiting caller apply an accepted value to its own form.

Arbitrary nesting uses the same API

Parent modal → child modal

const outcome = await layers.ask<ChildValue>(childDialog, {
  trigger: openChildButton, // inside the parent modal
});

if (outcome.status === "accepted") {
  renderChildValue(outcome.value); // caller owns parent mutation
}

Drawer → confirmation modal

const confirmation = await layers.ask<boolean, "cancel">(confirmDialog, {
  trigger: deleteButton, // inside the open drawer
});

if (confirmation.status === "accepted" && confirmation.value) {
  removeLocalItem();
}

Modal → popover

const choice = await layers.ask<string>(statusPopover, {
  trigger: statusButton, // inside the open modal
});

if (choice.status === "accepted") {
  statusOutput.textContent = choice.value;
}

There is no nestedModal, nestedNestedModal, or special workflow subtype. Parentage comes from the opener (or an explicitparent option), and every child independently produces its own result.

Result events are observational

surface.addEventListener("mr:layer:result", (event) => {
  console.log(event.detail.layer, event.detail.outcome);
});

The event carries the same discriminated outcome and bubbles for diagnostics or integration. Direct parent/child workflow composition should still use the returned Promise. No automatic parent DOM mutation or completion occurs: a child result never implicitly changes parent DOM or completes the parent.

Propagation rules

Lower-capability fallback

Nested ask() is an enhancement, not a requirement on the underlying task. A quick-create control for an essential workflow should retain a normal destination that works when JavaScript or native Layer capability is unavailable.

<a href="/authors/new">Create author on the standalone page</a>