Make @modular-frontend/core a shared peer dependency so panels adopt cleanly - #95
Merged
Merged
Conversation
…cleanly Panels ship in @modular-frontend/core@0.4.0, but much of the ecosystem still constrained the engine to older lines, so installing 0.4.0 for panels tripped peer-dependency conflicts. Treat @modular-frontend/core as the framework-neutral singleton it is and express that with peers instead of hand-maintained per-minor ranges. - Unify every @modular-frontend/core peer range to a single, forward looking ">=0.1.0 <2.0.0" (was "^0.1.0 || ^0.2.0 || ^0.3.0", i.e. >=0.1.0 <0.4.0, which excluded 0.4.0 and every future minor; angular and frontend-testing were even narrower at "^0.1.0"). No more per release append. - Promote @modular-frontend/core from a hard "dependencies: workspace:*" to peerDependencies + a devDependencies workspace:* in @modular-react/core, compositions-engine, and journeys-engine, so a consumer resolves one shared, identity-correct engine copy instead of a bundled exact pin. Matches @modular-frontend/testing and the check-publish.mjs philosophy. - Bump the Vue scaffolder's baked range so generated apps are born able to install panels. Verified: typecheck, build, full test suite, check:lockfile-dedup, and check:publish (publint + attw) all green across 29 published packages. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015p1jncNVgVekk2hFCWuaX4
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (2)
📒 Files selected for processing (15)
📝 WalkthroughWalkthrough
ChangesShared core peer dependency alignment
Estimated code review effort: 2 (Simple) | ~10 minutes Possibly related PRs
Suggested labels: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
kibertoad
added a commit
that referenced
this pull request
Jul 19, 2026
* docs: triage the cat-factory slice-5 overlay-host request
Accept the substance (a pick-one, app-state-keyed modal host with
framework-managed behaviour — focus trap + return, scroll lock, stacked
Escape, a11y — is a real, framework-shaped gap; even our own
nuxt-modal-journey example hand-rolls this chrome without it), redirect
the shape: no defineModule({ overlays }) descriptor seam (slots are the
ingress, per the slice-4 precedent), no /testing subpath, no Nuxt
manifest threading, no route-sync helper, and not Vue-only — the
behaviour is engine-first with React and Vue hosts in the same train.
Corrects the stale Gap D picture: the peer-range/hard-dep residuals the
request re-files were already closed by the shared-peer-dependency
change (#95). Counter-proposal: OverlayEntry (a ComponentEntry superset)
+ defineOverlayHost + resolveOverlay + a pure createOverlayStack in the
engine, managed OverlayOutlet hosts + useModalBehavior in both bindings.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ezPjgGYoWHHg1vVNLZUQT
* feat(overlays): state-keyed overlay host (pick-one, modal, managed behaviour)
Implements the counter-proposal from the cat-factory slice-5 triage: an
overlay host — the pick-one, modal dual of the render-all panels — landed
engine-first with both bindings in the same train. Contributions ride the
existing module -> slot path; no new package, descriptor change, registry
plugin, /testing subpath, or Nuxt manifest threading.
Engine (@modular-frontend/core):
- OverlayEntry<TSubject, TMeta> — a ComponentEntry superset (title is
first-class because the shell wires it to aria-label; icons/variants
stay in opaque meta), OverlayHostHandle, defineOverlayHost
- resolveOverlay: pure pick-one resolver (duplicate-id validation first
via the shared collapseEntriesById so the stances can't drift; null id
-> null; dangling id -> null, hosts dev-warn — pairById's "missing"
stance), resolveOverlayTitle
- createOverlayStack: pure LIFO stack with a subscribe seam — the shared
"top overlay closes first" semantics both bindings consume
- Re-exported by @modular-react/core (export *) and @modular-vue/core
Vue (@modular-vue/vue, re-exported from @modular-vue/core):
- useOverlay (computed over both runtime slot sources + the active id),
<OverlayOutlet> (Teleport, backdrop click-self -> close emit, managed
behaviour, #wrap / #empty slots, subject as prop + provide, per-
(entry.id, subjectKey) keying, ModuleErrorBoundary label "Overlay",
stable data-modular-overlay-* e2e hooks), useOverlaySubject,
useModalBehavior — the behaviour standalone for bespoke roots
- injectSlotsSource shared with panels (caller-labeled error)
React (@modular-react/react):
- The same surface with hooks/context/portal: useOverlay, <OverlayOutlet>
(empty / wrap / onClose props), useOverlaySubject / OverlaySubjectContext,
useModalBehavior (version-bump re-render over the engine stack — a
sync-external-store snapshot would read the ticket ref before the
activation effect assigns it)
Docs:
- docs/overlay-host.md guide (entry shape, resolver, both hosts, the
behaviour contract, the app-shell recipe, overlays-vs-neighbours)
- compositions README "pick by problem shape" table extended 4 -> 5;
cross-links from subject-panels.md, remote-capability-manifests.md,
and the Nuxt consumer-seam guide; CHANGELOG entry
- Gap D verified already closed by the shared-peer change; the new
surface is born onto the unified >=0.1.0 <2.0.0 range
Verified: turbo typecheck/build/test green across the workspace (214
tasks); engine + Vue + React overlay suites cover pick-one selection,
dangling/duplicate stances, stacking, Escape top-first, focus trap +
return, scroll lock, subject injection + rekeying, error containment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012ezPjgGYoWHHg1vVNLZUQT
* fix(overlays): review hardening — dedupe behaviour into the engine, guard backdrop drag-out, refocus on window swap, freeze the behaviour scope
Address the PR-96 review findings, prioritizing the architectural ones:
- Move the behaviour's framework-neutral DOM semantics into the engine
(frontend-core overlay-dom.ts, client-only and SSR-guarded): the single
app-wide sharedOverlayStack instance both bindings register on, the
counted body scroll lock, and the focusable-scan/Tab-trap rules. One
implementation so the behaviour cannot drift between bindings (the
collapseEntriesById argument applied to focus and scroll); each binding
now contributes only event/reactivity glue. This also makes the "one
overlay ordering per app" claim true by construction, across bindings.
- Backdrop close is now press-and-release: a press that starts inside the
dialog and slips onto the backdrop (text selection, missed drag) no
longer closes the overlay. Tested in both bindings.
- Initial focus is re-applied when the active window swaps without
closing (new optional contentKey on useModalBehavior; the outlets pass
the mounted window's key), so focus follows the new content instead of
falling to body. Tested in both bindings.
- Close the review's test-coverage gaps: Tab/Shift+Tab wrap and escaped-
focus pull-back, no-focusables fallback, initialFocus, and the React
custom portal target + portalDisabled, in both bindings where relevant.
- Formalize the behaviour bundle's scope: new "Conscious constraints"
section in docs/overlay-host.md freezes the guarantee list as the whole
contract (no pixels ever, no chrome anatomy, no background inert, no
focusable-detection long tail, no platform scroll workarounds, no
router awareness) so the first DOM-owning surface in the family holds
its line by documented decision rather than per-request renegotiation.
- Fix doc drift: useSyncExternalStore references (the React glue is a
deliberate subscription-driven re-render), the per-binding stack
wording, and the dedup of the React dangling-id dev warning.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G9FSij7zRf1Qt1AzHLvHP6
* docs(overlays): runnable React + Vue overlay-host examples with e2e
The overlay host was the slice-5 deliverable but had no runnable example —
the panels sibling has inspector-panels; overlays had nothing. Add the
pick-one, modal mirror of inspector-panels on BOTH bindings (the behaviour
is engine-first, so both must observe the same contract), each with a
Playwright suite asserting the full managed-modal contract.
New examples (examples/{react-router,vue}/overlay-result-windows):
- app-shared exports the shared defineOverlayHost<StepRef> handle + WindowMeta.
- run-core contributes test-report (dynamic title → aria-label; a nested
bespoke useModalBehavior confirm on the shared stack) and run-logs
(useOverlaySubject). acme-extras is a consumer window added with no host
edit (namespaced id). These are the first Vue example modules to contribute
`slots`.
- shell mounts one <OverlayOutlet>, styles the headless host, and drives
activeView from local/reactive state. An in-dialog switcher demonstrates
swap-without-close (the backdrop covers the openers behind it).
- e2e covers open/close, Escape + focus return, backdrop press-and-release
guard, focus trap, window swap, subject injection, consumer contribution,
the shared Escape stack, and the dangling-id "data not crash" stance.
Register both shells in the CI examples-e2e matrix; link them from
docs/overlay-host.md and examples/README.md.
fix(vue): make OverlayOutlet/PanelsOutlet subject + class props usable in
typed templates. `type: null` with an un-cast `default: null`/`default:
undefined` collapsed the inferred $props type to `null`/`undefined`, so the
documented `<OverlayOutlet :subject :panel-class>` / `<PanelsOutlet :subject>`
template usage did not typecheck under vue-tsc. Cast the defaults to the
prop's declared type so ExtractPropTypes keeps `unknown`. Runtime unchanged.
docs(overlays): fix the Vue #wrap snippet to render `children` through a
stable functional component instead of `<component :is="() => children" />`,
which gets a fresh identity every parent re-render (e.g. when a nested
overlay bumps the shared stack) and remounts the window, dropping its state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRyxejS9jdwipTZGZKGnZ3
* fix(overlays): expose ariaLabelledby on both overlay hosts
Addresses the review comment: when a window ships no `title`, the dialog
set only `aria-label` (from the resolved title), so a title-less window
ended up with an unnamed dialog. Add an optional `ariaLabelledby` prop to
`<OverlayOutlet>` on both bindings, forwarded to the dialog's
`aria-labelledby`, so a heading the window renders in `wrap`/`#wrap` can
name the modal. `aria-label` is preserved (ARIA resolves `aria-labelledby`
first when both are present).
Tests on both bindings assert the attribute forwards for a title-less
window (aria-label absent). Docs updated (props lists + a11y guarantee).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRyxejS9jdwipTZGZKGnZ3
* docs(overlays): drop stale collapseEntriesById reference in overlay-dom docstring
Addresses the review comment: the module docstring referenced a
`collapseEntriesById` argument that no function in this file takes — a stale
leftover from an earlier draft. Remove the parenthetical; the surrounding
description of the shared focusable/Tab/scroll-lock/sharedOverlayStack
semantics is unchanged. Comment-only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRyxejS9jdwipTZGZKGnZ3
---------
Co-authored-by: Claude <noreply@anthropic.com>
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What & why
Adopting panels is harder than it should be: panels ship in
@modular-frontend/core@0.4.0, but much of the ecosystem still constrained the engine to older lines, so an install that pulled0.4.0for panels tripped peer-dependency conflicts against packages that only admitted^0.3.0(or, worse,^0.1.0).Root cause is how
@modular-frontend/core— the one framework-neutral singleton the whole tree shares — was declared: a mix of hand-maintained enumerated peer ranges and harddependencies. This PR expresses that singleton relationship with peer dependencies instead.@modular-frontend/corenow declares a single>=0.1.0 <2.0.0range instead of^0.1.0 || ^0.2.0 || ^0.3.0. Under semver a0.xcaret is patch-only, so that union was really>=0.1.0 <0.4.0— it excluded0.4.0and every future minor, and each release required a manual append (the step that was missed for0.4.0). The open range admits the whole pre-2.0line in one expression, so future additive engine releases need no per-package edit. Touches@modular-vue/{vue,core,runtime,nuxt,journeys,compositions,testing},@modular-angular/{angular,core}, and@modular-frontend/testing(the last three were still on^0.1.0, excluding0.2/0.3too).@modular-react/core,@modular-frontend/compositions-engine, and@modular-frontend/journeys-enginecarried@modular-frontend/coreas a plaindependencies: "workspace:*", which publishes as an exact version pin and can install a second engine copy alongside a consumer's own. Because the engine holds identity-sensitive runtime (slots context, module/registry identity, the shared resolver reductions behind panels and compositions), a duplicate copy silently breaks identity checks and yields structurally-but-not-nominally compatible types. These now declare@modular-frontend/coreaspeerDependencies(>=0.1.0 <2.0.0) backed by adevDependencies: "workspace:*"— matching the stance@modular-frontend/testingalready took and the philosophy inscripts/check-publish.mjs("shared singletons stay peers so a consumer's types bind to their single copy").@modular-vue/clibaked@modular-frontend/core: "^0.1.0"into every generated app, so a freshly scaffolded project was born unable to install panels. It now bakes the same>=0.1.0 <2.0.0range (snapshot updated).How it was verified
pnpm typecheck— 156/156 tasks pass.turbo run build --filter=./packages/*— 29/29 packages build.pnpm test— full suite passes (one flaky failure was a stale.test-outputdir from a prior run, green after cleanup).pnpm check:publish— publint + attw on the packed tarballs pass for all 29 published packages, confirming the peer conversion resolves correctly for a real consumer.pnpm check:lockfile-dedup— OK.oxfmt --write .andoxlint .— clean (only pre-existing warnings in untouched files).Checklist
pnpm lintpasses (typecheck + oxfmt + oxlint).pnpm test) — Vue scaffolder snapshot updated; packaging validated bycheck:publish.CHANGELOG.mddocuments the change and the peer-dependency policy.@modular-vue/*peer ranges are widened alongside the React/Angular ones, and the Vue scaffolder is bumped. No Vue follow-up needed.🤖 Generated with Claude Code
Generated by Claude Code
Summary by CodeRabbit
@modular-frontend/coreacross Angular, Vue, testing, runtime, and composition packages to support versions>=0.1.0 <2.0.0.