Skip to content

Make @modular-frontend/core a shared peer dependency so panels adopt cleanly - #95

Merged
kibertoad merged 1 commit into
mainfrom
claude/panels-dependency-version-jylka6
Jul 19, 2026
Merged

Make @modular-frontend/core a shared peer dependency so panels adopt cleanly#95
kibertoad merged 1 commit into
mainfrom
claude/panels-dependency-version-jylka6

Conversation

@kibertoad

@kibertoad kibertoad commented Jul 19, 2026

Copy link
Copy Markdown
Owner

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 pulled 0.4.0 for 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 hard dependencies. This PR expresses that singleton relationship with peer dependencies instead.

  • Unified, forward-looking peer range. Every binding that peer-depends on @modular-frontend/core now declares a single >=0.1.0 <2.0.0 range instead of ^0.1.0 || ^0.2.0 || ^0.3.0. Under semver a 0.x caret is patch-only, so that union was really >=0.1.0 <0.4.0 — it excluded 0.4.0 and every future minor, and each release required a manual append (the step that was missed for 0.4.0). The open range admits the whole pre-2.0 line 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, excluding 0.2/0.3 too).
  • Hard dependencies promoted to peers. @modular-react/core, @modular-frontend/compositions-engine, and @modular-frontend/journeys-engine carried @modular-frontend/core as a plain dependencies: "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/core as peerDependencies (>=0.1.0 <2.0.0) backed by a devDependencies: "workspace:*" — matching the stance @modular-frontend/testing already took and the philosophy in scripts/check-publish.mjs ("shared singletons stay peers so a consumer's types bind to their single copy").
  • Vue scaffolder. @modular-vue/cli baked @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.0 range (snapshot updated).

Note on the chosen range: <2.0.0 reflects the maintainer policy that @modular-frontend/core stays compatible through the 1.x line and only 2.x+ is treated as breaking.

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-output dir from a prior run, green after cleanup).
  • pnpm check:publishpublint + 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 . and oxlint . — clean (only pre-existing warnings in untouched files).

Checklist

  • pnpm lint passes (typecheck + oxfmt + oxlint).
  • Tests cover the change and pass (pnpm test) — Vue scaffolder snapshot updated; packaging validated by check:publish.
  • Docs / READMEs / tracker updated in the same PR where relevant — CHANGELOG.md documents the change and the peer-dependency policy.
  • Vue impact stated. This is a framework-neutral packaging change and the Vue family is handled in this same PR: all @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

  • Changed
    • Broadened compatibility with @modular-frontend/core across Angular, Vue, testing, runtime, and composition packages to support versions >=0.1.0 <2.0.0.
    • Updated package integrations to share a compatible core instance.
    • Updated the Vue CLI scaffolder to generate projects using the broadened core version range.
    • Documented the updated compatibility and package dependency arrangements in the changelog.

…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
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 51460d08-3ce1-4bbe-ab7f-f74302d623a2

📥 Commits

Reviewing files that changed from the base of the PR and between fb3c9dd and e36dff3.

⛔ Files ignored due to path filters (2)
  • packages/vue-cli/test/__snapshots__/cli.test.ts.snap is excluded by !**/*.snap
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (15)
  • CHANGELOG.md
  • packages/angular-router-core/package.json
  • packages/angular/package.json
  • packages/compositions-engine/package.json
  • packages/core/package.json
  • packages/frontend-testing/package.json
  • packages/journeys-engine/package.json
  • packages/vue-cli/src/templates/scaffold.ts
  • packages/vue-compositions/package.json
  • packages/vue-core/package.json
  • packages/vue-journeys/package.json
  • packages/vue-nuxt/package.json
  • packages/vue-runtime/package.json
  • packages/vue-testing/package.json
  • packages/vue/package.json

📝 Walkthrough

Walkthrough

@modular-frontend/core peer requirements are standardized to >=0.1.0 <2.0.0. Selected packages move the dependency from runtime dependencies to peer and development dependencies, while Vue scaffolding and the changelog document the updated contract.

Changes

Shared core peer dependency alignment

Layer / File(s) Summary
Unified peer version ranges
packages/angular*/package.json, packages/frontend-testing/package.json, packages/vue*/package.json
Peer dependency ranges for @modular-frontend/core now use >=0.1.0 <2.0.0.
Runtime dependency promotion
packages/core/package.json, packages/compositions-engine/package.json, packages/journeys-engine/package.json
Selected packages remove the runtime dependency, retain workspace development access, and declare @modular-frontend/core as a peer dependency.
Scaffold and changelog updates
packages/vue-cli/src/templates/scaffold.ts, CHANGELOG.md
Vue CLI-generated projects and changelog documentation use and describe the unified dependency range.

Estimated code review effort: 2 (Simple) | ~10 minutes

Possibly related PRs

Suggested labels: minor

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: sharing @modular-frontend/core as a peer dependency to avoid panel adoption conflicts.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/panels-dependency-version-jylka6

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@kibertoad
kibertoad merged commit 18536a9 into main Jul 19, 2026
40 of 42 checks passed
@kibertoad
kibertoad deleted the claude/panels-dependency-version-jylka6 branch July 19, 2026 12:34
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants