Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,17 @@ jobs:
example_path: examples/tanstack-router/editor-composition
runtime_path: packages/tanstack-router-runtime
core_path: packages/tanstack-router-core
# Overlay-host examples — exercise the state-keyed overlay
# (`<OverlayOutlet>` / `useOverlay` / `useModalBehavior`) in each
# binding. The managed modal behaviour (focus trap, shared Escape
# stack, scroll lock) is engine-first in packages/frontend-core, so
# the React and Vue hosts must observe the identical contract — both
# are covered to catch drift in either binding's glue.
- shell: "@example-rr-overlay-windows/shell"
shell_artifact: "example-rr-overlay-windows-shell"
example_path: examples/react-router/overlay-result-windows
runtime_path: packages/react-router-runtime
core_path: packages/react-router-core
# Vue examples — exercise the @modular-vue/* family mounted in
# a real vue-router shell: sibling modules, a journey, and a
# composition. Same duplicate-module / provider-plumbing regressions
Expand All @@ -234,6 +245,11 @@ jobs:
example_path: examples/vue/editor-composition
runtime_path: packages/vue-runtime
core_path: packages/vue-core
- shell: "@example-vue-overlay-windows/shell"
shell_artifact: "example-vue-overlay-windows-shell"
example_path: examples/vue/overlay-result-windows
runtime_path: packages/vue-runtime
core_path: packages/vue-core
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,16 @@ Per-package detail lives in the GitHub release tagged `<npm-name>@<version>`.

## Unreleased

### Added — state-keyed overlay host (pick-one, modal)

Implements the counter-proposal from the cat-factory slice-5 triage (`docs/overlay-host-triage.md`): the **overlay host**, a pick-**one**, app-state-keyed, open-contribution modal surface — the modal dual of the render-all panels. Modules (first-party and consumer alike) contribute _windows_ to a host's slot; application state names the one active window by id; the framework mounts it inside a managed modal shell owning the behaviour hand-rolled modals get wrong somewhere: teleport/portal, backdrop press-and-release close (a press that starts inside the dialog and slips onto the backdrop is not a close request), a shared overlay **stack** (nested overlays layer; Escape closes the top first), focus trap + focus return (re-applied when the active window swaps without closing), body scroll lock, and a11y wiring (`role="dialog"` / `aria-modal` / `aria-label` from the entry's subject-aware `title`). Headless — the host renders only the backdrop and dialog elements (class props + stable `data-modular-overlay-*` e2e hooks); the app's chrome lives in a `wrap` render-prop/slot, and the host never closes itself (it _requests_ close; state stays app-owned). Contributions ride the existing module → slot path; no new registration seam, package, or descriptor change.

- **`@modular-frontend/core`** — `OverlayEntry<TSubject, TMeta>` (a superset of `ComponentEntry`, so a slot serving the pick-one pairing surface can serve an overlay host during an incremental migration; `title` is first-class because the shell wires it to `aria-label`, icons/variants stay in opaque `meta`), `OverlayHostHandle<TSubject>`, `defineOverlayHost(slotKey)` (phantom-typed handle, the `definePanelGroup` convention), `resolveOverlay(entries, activeId, opts?)` (duplicate-id validation first via the same shared collapse as `resolveComponentRegistry` / `resolvePanels`, then null-id → null, then pick-one lookup; a dangling id is data — `null`, host dev-warns — mirroring `pairById`'s `missing` stance), `resolveOverlayTitle`, and `createOverlayStack()` — a pure LIFO stack with a subscribe seam, the shared "top overlay closes first" semantics both bindings' behaviour implementations consume. The behaviour's framework-neutral DOM semantics also live here (`overlay-dom.ts`, client-only, SSR-guarded): the single app-wide `sharedOverlayStack` instance, the counted body scroll lock (`lockBodyScroll` / `unlockBodyScroll`), and the focusable-scan/Tab-trap rules (`firstFocusableIn` / `trapTabFocus`) — one implementation, so the behaviour cannot drift between bindings; each binding contributes only event/reactivity glue. Re-exported by `@modular-react/core` and `@modular-vue/core`.
- **`@modular-vue/vue`** (re-exported from `@modular-vue/core`) — `useOverlay` (a `computed` over both runtime slot sources + the active id, all inputs `MaybeRefOrGetter`), `<OverlayOutlet>` (Teleport with `to` / `teleportDisabled`, backdrop press-and-release → `close` emit, managed behaviour, `#wrap` / `#empty` slots, subject injected as a prop **and** via `provide`, per-`(entry.id, subjectKey)` keying, per-window `ModuleErrorBoundary` labeled `"Overlay"`), `useOverlaySubject` / `overlaySubjectKey`, and `useModalBehavior({ active, onClose, initialFocus?, contentKey? }) → { dialogRef, isTop }` — the managed behaviour standalone, for bespoke roots (full-bleed surfaces) that must stack/focus/scroll-lock correctly against hosted windows (`contentKey` re-applies initial focus when the hosted content swaps under an open overlay).
- **`@modular-react/react`** — the same surface with hooks/context/portal: `useOverlay`, `<OverlayOutlet>` (`empty` / `wrap` / `onClose` props, `to` / `portalDisabled`), `useOverlaySubject` / `OverlaySubjectContext`, `useModalBehavior`.
- **Docs** — new [`docs/overlay-host.md`](docs/overlay-host.md) guide (entry shape, resolver semantics, both hosts, the behaviour contract, the app-shell recipe, overlays-vs-neighbours), including a **"Conscious constraints"** section that freezes the behaviour bundle's scope: the guarantee list is the whole contract, and no-pixels-ever, no chrome anatomy, no background `inert` management, structural-only focusable detection, no platform scroll-lock workarounds, and no router awareness are out of scope by decision — the overlay host is the family's first DOM-owning surface, and the line it holds is documented rather than renegotiated per request. The compositions README's sibling-primitive comparison extended four → five; cross-links from `subject-panels.md`, `remote-capability-manifests.md`, and the Nuxt consumer-seam guide; the triage doc records what was accepted, redirected (no `defineModule({ overlays })` seam, no `/testing` subpath, no Nuxt manifest threading, no route-sync helper, Vue-only → both bindings), and why.
- **Version alignment** — the request's carry-over peer-range item ("Gap D") was already closed by the shared-peer-dependency change below; the overlay surface is born onto the unified `>=0.1.0 <2.0.0` range with no per-package edits.

### Added — subject-keyed panels (render-all, predicate-gated)

Implements the counter-proposal from the cat-factory slice-4 triage (`docs/subject-keyed-zones-triage.md`): **panels**, a named region whose module-contributed entries are selected at render time by a runtime **subject**, gated by per-entry `when(subject)` predicates, ordered, and rendered **all-matching** — the render-all, open-contribution sibling of the pick-one component-pairing helpers. Contributions ride the existing module → slot path; no new registration seam, package, or descriptor change. Deliberately _not_ named "zones" (that word already names two pick-one primitives).
Expand Down
9 changes: 9 additions & 0 deletions docs/framework-mode-nuxt.md
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,15 @@ code-shipped components](remote-capability-manifests.md#pairing-wire-safe-manife
for that join — the components ship as code through the seam above; only the
selecting id crosses the wire.

The seam also carries the two selection-hosted UI surfaces with no extra
wiring, because their hosts read the same slots context the layer already
provides: a consumer module can contribute inspector panels (render-all,
subject-keyed — [subject-panels.md](subject-panels.md)) or modal windows with
framework-managed chrome behaviour (pick-one, state-keyed —
[overlay-host.md](overlay-host.md)) to slots the layer's shell hosts with
`<PanelsOutlet>` / `<OverlayOutlet>`. No `installModularApp` change, no
manifest threading — the slots manifest _is_ the manifest.

## Rules of thumb

- **Registry as a factory, called in the plugin.** This is the one rule that
Expand Down
Loading
Loading