---
number: 12224
title: 'refactor(portal): promote canvas glue to shared Portal.view.content (#12206)'
author: neo-opus-ada
state: MERGED
createdAt: '2026-05-30T20:46:16Z'
updatedAt: '2026-05-30T21:03:50Z'
closedAt: '2026-05-30T21:03:48Z'
mergedAt: '2026-05-30T21:03:48Z'
head: refactor/portal-shared-canvas-glue-12206
base: dev
url: 'https://github.com/neomjs/neo/pull/12224'
---
Authored by Opus 4.8 (Claude Code). Session 94a91ebc.

Resolves #12206

Foundation step 2 of epic #12204. Promotes the App-worker canvas glue out of the tickets view into a shared portal-content home so the upcoming PR + Discussion views reuse it via config.

- Relocates the bridge `Portal.view.news.tickets.TimelineCanvas` → `Portal.view.content.TimelineCanvas` and the wrapper `...tickets.CanvasWrapper` → `Portal.view.content.CanvasWrapper` (into `apps/portal/view/content/`).
- Removes CanvasWrapper's only per-type coupling — the hardcoded `import Component from './Component.mjs'` default. `contentComponent` now defaults to `null`; consumers supply it through the existing config seam.
- The tickets `MainContainer` now imports the shared `CanvasWrapper` and passes its own `Component` as `contentComponent` via `pageContainerConfig.contentConfig`.
- Generalizes the wrapper cls `neo-ticket-timeline-wrapper` → `neo-timeline-wrapper` (per-type prefixes stay).

Per the operator's call, the shared home is portal-scoped (`Portal.view.content.*`), not framework — #12208 still adds the `Neo.app.content.Component` base these extend, keeping the framework-API commitment to one deliberate step.

FAIR-band: over-target — sole active implementer on an operator-directed lane (epic #12204).

Evidence: L1 — `grep` for the old `view.news.tickets.{TimelineCanvas,CanvasWrapper}` ns + `tickets/CanvasWrapper` import == 0; `node --check` passes on the bridge, wrapper, and MainContainer; git records both files as renames (content preserved). The `sections` store key and `.neo-timeline-item[data-record-id]` selector are untouched (canvas-bridge contract, per AC). Residual: L2 — Tickets tab renders + animates unchanged (post-merge/deploy).

## Deltas from ticket (if any)
- Shared home is `apps/portal/view/content/` (`Portal.view.content.*`), per the operator's portal-shared decision over the framework (`src/app/content/`) option.

## Test Evidence
- `grep -rn "view.news.tickets.TimelineCanvas|...CanvasWrapper|tickets/CanvasWrapper" apps/portal` → 0 matches.
- `node --check` on `view/content/TimelineCanvas.mjs`, `view/content/CanvasWrapper.mjs`, `view/news/tickets/MainContainer.mjs` → pass.
- `sections` store key + `.neo-timeline-item[data-record-id]` selector present and unchanged.

## Post-Merge Validation
- [ ] Tickets tab timeline still renders + animates (behavior-neutral relocation).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
