Authored by Opus 4.8 (Claude Code). Session 94a91ebc.
Resolves #12208
Foundation step 4 of epic #12204. Extracts the parser-agnostic timeline plumbing into a shared Portal.view.content.Component base so the upcoming PR + Discussion views subclass it and supply only their own parser.
Neo.app.content.Component already exists but runs in headline/sidenav mode (observes .neo-h2/.neo-h3, onHeadline, updateSectionsStore: true). Rather than overload it with timeline concerns, this adds an intermediate timeline-mode base in the portal-shared Portal.view.content.* home (per the #12206 decision and the ticket's "TimelineContent base" option):
- New
apps/portal/view/content/Component.mjs (Portal.view.content.Component extends Neo.app.content.Component) holds the content-agnostic plumbing lifted verbatim from the tickets Component: formatTimestamp, the timeline-mode doFetchContent (observes .neo-timeline-item[data-record-id]), getContentPath, onResize → toggleSummary, the contentComponentId construct hook, and the updateSectionsStore: false + domListeners defaults.
Portal.view.news.tickets.Component now extends it and keeps the Issue specialization: the modifyMarkdown pipeline, the renderTimeline line-walker, the event-type/label/commit machinery, and the frontmatter/badge helpers.
This satisfies the AC: a new content type subclasses the base and supplies only its modifyMarkdown/renderTimeline parser. Pure hierarchy refactor — no logic change, no HTML-template change.
Theming (honored): #12206 moved CanvasWrapper.mjs into Portal.view.content.* but left its SCSS stranded in the news/tickets namespace. The theme build (buildScripts/build/themes.mjs) compiles each .scss to a CSS file mirroring its tree path and keys it into theme-map.json by namespace, so this relocates resources/scss/src/apps/portal/news/tickets/CanvasWrapper.scss → …/content/CanvasWrapper.scss to match. The .portal-canvas-wrapper selector is unchanged → identical compiled CSS.
FAIR-band: over-target — sole active implementer on an operator-directed lane (epic #12204).
Evidence: L1 — node --check passes on the new base, the refactored tickets Component, and MainContainer; the lifted members (construct, formatTimestamp, doFetchContent, getContentPath, onResize, #dateTimeFormat*) are absent from tickets (grep == 0) and present once in the base; tickets retains its parser (renderTimeline, modifyMarkdown, 3× me.formatTimestamp inherited calls); git records the SCSS as a rename (content preserved). Residual: L2 — the Tickets tab renders + animates unchanged (behavior rests on Neo config/method inheritance; visual confirmation post-merge/deploy).
Deltas from ticket (if any)
- Timeline base placed at
Portal.view.content.Component (portal-shared), not in Neo.app.content directly — the existing framework Component is headline-mode; overloading it would mix two content modes. Consistent with the #12206 portal-shared decision.
- Template extraction deferred to #12213: the ticket lists lifting the body/comment/event HTML templates into the base. With the Issue view as the only consumer today that is abstraction-ahead-of-the-shape, and the HTML output has no unit test (can't be self-verified). The true shared shape emerges when the PR view (#12213) lands — extracted then under visual verification (rule-of-three).
- #12209 (per-type cross-link URLs) batched with the views (#12211/#12213), where the cross-link behavior is actually exercised.
- Bundles the #12206 SCSS-namespace fix (cohesive — same
content/ relocation).
- Converted four pre-existing
{key: key} properties in renderTimeline (color, icon, id ×2) to ES2015 shorthand — the check-shorthand pre-commit hook scans the whole staged file, and this is the first change to stage tickets/Component.mjs since they were introduced.
Test Evidence
node --check → base, tickets Component, MainContainer all pass.
- Lifted members: 0 occurrences in
tickets/Component.mjs, 1 each in content/Component.mjs.
- Issue parser retained in tickets:
renderTimeline ×3, modifyMarkdown ×2, me.formatTimestamp ×3 (inherited).
- SCSS move recorded as a git rename; selector unchanged.
Post-Merge Validation
Authored by Opus 4.8 (Claude Code). Session 94a91ebc.
Resolves #12208
Foundation step 4 of epic #12204. Extracts the parser-agnostic timeline plumbing into a shared
Portal.view.content.Componentbase so the upcoming PR + Discussion views subclass it and supply only their own parser.Neo.app.content.Componentalready exists but runs in headline/sidenav mode (observes.neo-h2/.neo-h3,onHeadline,updateSectionsStore: true). Rather than overload it with timeline concerns, this adds an intermediate timeline-mode base in the portal-sharedPortal.view.content.*home (per the #12206 decision and the ticket's "TimelineContent base" option):apps/portal/view/content/Component.mjs(Portal.view.content.Component extends Neo.app.content.Component) holds the content-agnostic plumbing lifted verbatim from the tickets Component:formatTimestamp, the timeline-modedoFetchContent(observes.neo-timeline-item[data-record-id]),getContentPath,onResize → toggleSummary, thecontentComponentIdconstruct hook, and theupdateSectionsStore: false+domListenersdefaults.Portal.view.news.tickets.Componentnowextendsit and keeps the Issue specialization: themodifyMarkdownpipeline, therenderTimelineline-walker, the event-type/label/commit machinery, and the frontmatter/badge helpers.This satisfies the AC: a new content type subclasses the base and supplies only its
modifyMarkdown/renderTimelineparser. Pure hierarchy refactor — no logic change, no HTML-template change.Theming (honored): #12206 moved
CanvasWrapper.mjsintoPortal.view.content.*but left its SCSS stranded in thenews/ticketsnamespace. The theme build (buildScripts/build/themes.mjs) compiles each.scssto a CSS file mirroring its tree path and keys it intotheme-map.jsonby namespace, so this relocatesresources/scss/src/apps/portal/news/tickets/CanvasWrapper.scss→…/content/CanvasWrapper.scssto match. The.portal-canvas-wrapperselector is unchanged → identical compiled CSS.FAIR-band: over-target — sole active implementer on an operator-directed lane (epic #12204).
Evidence: L1 —
node --checkpasses on the new base, the refactored tickets Component, and MainContainer; the lifted members (construct,formatTimestamp,doFetchContent,getContentPath,onResize,#dateTimeFormat*) are absent from tickets (grep == 0) and present once in the base; tickets retains its parser (renderTimeline,modifyMarkdown, 3×me.formatTimestampinherited calls); git records the SCSS as a rename (content preserved). Residual: L2 — the Tickets tab renders + animates unchanged (behavior rests on Neo config/method inheritance; visual confirmation post-merge/deploy).Deltas from ticket (if any)
Portal.view.content.Component(portal-shared), not inNeo.app.contentdirectly — the existing framework Component is headline-mode; overloading it would mix two content modes. Consistent with the #12206 portal-shared decision.content/relocation).{key: key}properties inrenderTimeline(color,icon,id×2) to ES2015 shorthand — thecheck-shorthandpre-commit hook scans the whole staged file, and this is the first change to stagetickets/Component.mjssince they were introduced.Test Evidence
node --check→ base, tickets Component, MainContainer all pass.tickets/Component.mjs, 1 each incontent/Component.mjs.renderTimeline×3,modifyMarkdown×2,me.formatTimestamp×3 (inherited).Post-Merge Validation
npm run build-themes -- -n -e dev -t all) compiles the relocated CanvasWrapper.scss.