MarkupRefineLib

Menu


Layer architecture

Phase 1 defined the Layer model used to unify local overlay mechanics without introducing a generic overlay engine prematurely. Phase 2 implemented that model as a small native-first runtime, Phase 3 added opt-in modal/drawer presentation, Phase 4 migrated Search Tool and Application Shell onto the shared lifecycle, Phase 5 added native popovers/tooltips, Phase 6 added typed local interaction outcomes, Phase 7 added an optional HTML-first remote navigation module, and Phase 8 locks the resulting public/degradation contract with permanent checks. The full normative contract lives inLAYER-API.md at the repository root.

Boundary

A Layer owns local open/close lifecycle, trigger ownership, focus restoration, parent/child relationships, normalized lifecycle observation, and opaque local interaction outcomes. It does not know about HTTP, URLs, CRUD entities, remote forms, redirects, fragment refresh, or server frameworks.

The guiding rule is: attributes identify behavior; JavaScript composes workflows. Promises sequence interaction flow; DOM events observe the lifecycle.

Existing behavior inventory

ConsumerCurrent mechanicsLayer modelComponent-specific behavior
Search ToolGenerated native <dialog> opened through layers.open(); native/Layer close requests and Layer focus restorationmodalFetching, Fuse.js, debounce, query/result rendering, initial input focus, generated DOM cleanup
Application ShellPersistent sidebar content is moved into a transient native <dialog> opened through the same Layer core; native backdrop/Escape and Layer focus restorationdrawerNavigation layout, responsive/no-JS behavior, content transfer, dismiss focus, disclosure ARIA
Legacy Side-Top-Top navigationIndependent overlay/classes/Escape listener; no focus restorationConfirms duplicated mechanics, but does not expand the APIHandled by legacy cleanup/migration work

Other keyboard-focus code such as tabs or resettable file inputs is ordinary component behavior, not Layer behavior. The existing floatingElementutility is a CSS float, not a top-layer primitive.

Phase 4 consumer architecture

[data-mr-search] ── creates modal ──┐
                                    ├──> Layer core ──> native <dialog> lifecycle
[data-mr-shell]  ── creates drawer ─┘          │
                                               └──> focus + ownership + lifecycle events

Search-specific: query / loading / results
Shell-specific: navigation / responsive transfer / disclosure ARIA

Dependency direction is one-way: both components depend on src/library/layers/; Layer core does not import either component.

Shared lifecycle versus consumer behavior

ConcernShared Layer coreSearch ToolApplication Shell
Open/close stateYesNo private state machineNo private state machine
Trigger ownership/focus restorationYesPasses search triggerPasses menu toggle
Escape/request-close/backdrop lifecycleNative dialog + Layer normalization; backdrop click closes the document's active Layer stackNo private Escape/backdrop engineNo private Escape/custom overlay
Initial focusConsumer choiceSearch inputDismiss/navigation control
Domain behaviorNoneQuery, loading, results, statusNavigation, responsive layout, disclosure ARIA

This split is the Phase 4 proof: deleting either consumer leaves the Layer registry, lifecycle, modal/drawer behavior, and other consumer intact. The Layer core imports neither component.

Lifecycle and modes

closed → opening → open → closing → closed

opening and closing are Markup Refine coordination states. Settled visibility should remain native wherever the platform already exposes it.

modal
Blocking interaction backed by modal <dialog>.
drawer
Primarily a presentation mode. A transient modal drawer may use the same<dialog> primitive as a modal rather than owning a second state engine.
popover
Non-modal floating interaction backed by the native Popover API where appropriate.

Ownership

Parent/child ownership is logical, not a custom visual stack. A child never implicitly refreshes, replaces, reloads, or closes its parent.

Programmatic API

type LayerMode = "modal" | "drawer" | "popover";
type LayerState = "closed" | "opening" | "open" | "closing";

interface Layer {
  readonly element: HTMLElement;
  readonly mode: LayerMode;
  readonly state: LayerState;
  readonly trigger: HTMLElement | null;
  readonly parent: Layer | null;
  readonly children: readonly Layer[];

  open(options?: LayerOpenOptions): Promise<void>;
  close(options?: LayerCloseOptions): Promise<void>;
  accept<T>(value: T): Promise<void>;
  dismiss<R = unknown>(reason?: R): Promise<void>;
  ask<T, R = unknown>(options?: LayerOpenOptions): Promise<LayerOutcome<T, R>>;
}

The implemented manager exposes get(target), closest(node),open(target, options), ask(target, options), current, and stack. Individual Layers add accept(value), dismiss(reason), and ask(). Accepted values remain opaque; the API deliberately contains no URL, fetch, form, template, or result-to-field mapping.

import { initLayers, layers } from "markup-refine-lib/layers";

initLayers(document);
const settings = await layers.open("#settings-dialog", { trigger: settingsButton });
console.log(settings.state, layers.current, layers.stack);

Local results and nested workflows

LayerOutcome<T, R>
  ├─ { status: "accepted", value: T }
  └─ { status: "dismissed", reason?: R }

parent Layer
  └─ await child.ask(...)
       ├─ child.accept(value)  → caller receives accepted value
       └─ child close/dismiss  → caller receives dismissed

No automatic parent DOM mutation or parent completion.

close() remains a lifecycle operation; accept() anddismiss() are semantic completion. If a pending ask() is closed normally (including native Escape/request-close), it resolves as a dismissal rather than rejecting. Result Promises are the workflow API;mr:layer:result is an observation hook carrying the same outcome. See Layer results and local subinteractions.

Modal and drawer presentation

.mr-layer provides the shared dialog surface presentation;.mr-layer--modal and .mr-layer--drawer identify the visual variant. Both behavior modes still use native modal <dialog> andshowModal(). See modal and drawer Layersfor live examples, tokens, request-close semantics, and unstyled HTML fallback.

Declarative discovery

<button commandfor="settings-dialog" command="show-modal">Settings</button>
<dialog id="settings-dialog"
        class="mr-layer mr-layer--modal"
        data-mr-layer="modal">
  <button commandfor="settings-dialog" command="request-close">Close</button>
</dialog>

data-mr-layer identifies the surface; mr-layer* classes are optional presentation hooks. Native invoker commands and popover controls remain responsible for the browser action. Repeated initialization is safe. The standard markup-refine-lib/behaviors entry initializes Layers automatically, while the focused markup-refine-lib/layers export provides explicit initLayers().

Lifecycle events

EventMeaningCancellation
mr:layer:beforeopenBefore an MR-controlled open commitsCancelable only while the request can still be prevented
mr:layer:openSettled openNo
mr:layer:beforecloseBefore a preventable close/request-close commitsOnly when the underlying close is actually preventable
mr:layer:closeSettled closedNo
mr:layer:resultSemantic outcome / implicit dismissal for a pending ask()No

Events originate on the Layer surface and bubble. Lifecycle details stay minimal;mr:layer:result additionally carries the same opaque outcome returned byask(). Events remain observational rather than the primary workflow API.

State ownership

ConcernOwner
Dialog open/closed, modal inertness, top layerNative <dialog>/browser
Popover shown/hidden, top layer, light dismiss, implicit anchorNative Popover API
Tooltip delays, preview-to-pin promotion, whole-tree outside dismissal, and topmost Escape dismissalFocused Tooltip module
Backdrop appearanceCSS ::backdrop
Disclosure/accessibility relationshipARIA only where needed, e.g. shell aria-expanded/aria-controls
Opening/closing transition and parent/child ownershipMarkup Refine Layer runtime
Visual variantmr-layer* classes/tokens

Popover and Tooltip layering

Tooltip trigger
  ├─ transient preview: native popover=hint + role=tooltip (non-focusable, hover-persistent)
  │                    (not registered; never in layers.stack)
  └─ pinned surface:    native popover=manual + data-mr-layer=popover
                         └─ nested pinned tooltip Layer
                              └─ …

Phase 5 keeps tooltip timing/content discovery in a focused Tooltip module above Layer core. Promotion is the boundary: only the interactive pinned surface becomes a Layer. Parent/child ownership is then inferred from the nested trigger exactly as for any other Layer, and parent close cascades through descendants child-first.

Generic popover="auto" Layers keep native light dismiss. Pinned Tooltips deliberately use popover="manual" so the focused Tooltip module can dismiss the whole active Tooltip stack on outside pointer-down while keeping Escapetopmost-only. Both paths still use the native top layer, and showPopover({ source: trigger })supplies the invoker/anchor relationship for programmatic opens. Seepopovers and pinned tooltips for the authoring API and decision matrix.

Remote interactions remain separate

Application / semantic href + form action
                  │
                  ▼
markup-refine-lib/layer-navigation
  fetch + fragment extraction + form enhancement
  validation replacement + server-result protocol
                  │
                  ▼
              Layer core
  local lifecycle + ownership + outcomes
                  │
                  ▼
     native dialog / Popover primitives

Phase 7 implements the optional layer-navigation entry above Layer core. It can fetch standalone HTML URLs, extract a declared fragment, progressively enhance opted-in forms/links, and translate a small server result marker into the existing accept()/dismiss() contract. Layer core still has no fetch, URL, redirect, form, or backend-protocol dependency.

Remote navigation never refreshes a parent implicitly. The direct JavaScript caller receives the child outcome and decides whether to update a field, reload another fragment, or do nothing, preserving unsaved parent state. The module is also absent from the default behaviors bundle so HTTP interception remains an explicit opt-in. See remote Layer navigation.

Stable contract

The finalized public surface and permanent graceful-degradation matrix are documented inStable Layer API. The Layer core remains independent from Search Tool, Application Shell, and optional remote navigation.

Platform references