diff --git a/pages/dashboard/PLAN.md b/pages/dashboard/PLAN.md index dee2f328e..f2d2789b1 100644 --- a/pages/dashboard/PLAN.md +++ b/pages/dashboard/PLAN.md @@ -114,7 +114,7 @@ - `src/components/data-state.js` — presentation-only data-state metrics card grid for availability, completeness, and freshness. - `src/components/table-region.js` — presentation-only reusable table wrapper for repeated table-region markup, header rows, empty-state rows, and keyed body-row descriptors across built-in and custom tables. - `src/components/summary-copy.js` — presentation-only shared singular/plural summary-count copy for repeated item-count text surfaces. -- `src/components/view-chrome.js` — presentation-only reusable page-section, titled-region, summary-list/summary-region, summary-style definition-list and definition-list rows, generic custom-view chrome paragraphs, custom-view source/metadata/context chrome, shared custom-view section chrome, custom-view state-message/details helpers, and built-in provenance-section helpers. +- `src/components/view-chrome.js` — presentation-only reusable page-section, titled-region, metadata-section, summary-list/summary-region, summary-style definition-list and definition-list rows, generic custom-view chrome paragraphs, custom-view source/metadata/context chrome, shared custom-view section chrome, custom-view state-message/details helpers, and built-in provenance-section helpers. - `src/components/workflow-topology.js` — topology overview section for package and standalone workflow structure, now reusing shared definition-list rows for its summary metrics. - `src/components/ui-primitives.js` — presentation-only shared section-heading, vital-stat, and UTC date-time helpers for repeated dashboard component chrome, including optional summary-free heading shells. - `src/components/count-formatters.js` — presentation-only shared count and singular/plural text helpers for repeated dashboard status, workflow, and signal copy. @@ -132,6 +132,15 @@ ## Run log +### 2026-08-31 (metadata-section helper refactor) +- Re-inventoried repeated UI construction under `pages/dashboard/src/` and selected the next bounded presentation-only DOM slice from the queue: repeated titled metadata `

` assembly in `src/components/outcome-detail.js`, with the same section-chrome family already housed in `src/components/view-chrome.js`. +- Extracted `renderMetadataSection(...)` into `src/components/view-chrome.js` with a minimal composable API for section title, content node, and optional heading tag, then replaced every identified duplicate call site in `src/components/outcome-detail.js`. +- Collapsed the local outcome-detail metadata-section helper so the outcome sidebar now reuses shared section chrome without changing heading text, link labels, DOM text, or class names. +- Added unit coverage in `test/unit/view-chrome.test.js` for default and custom heading levels while preserving the existing `test/unit/outcome-detail.test.js` assertions that lock the affected metadata headings, badges, links, and sanitized Markdown behavior. +- Proved unchanged behavior by rendering the affected outcome-detail route fixture through `pages/dashboard/scripts/check-outcome-detail-render.mjs` after the refactor and confirming the refactor was structural-only, while the existing outcome-detail unit assertions remained unchanged and green. +- Verified `pages/dashboard/` quality gates in this run: `npm install`, `npm run build`, `npm run typecheck`, `npm run lint`, and `npm test` all pass. Playwright was not rerun for this bounded helper extraction because no browser-only behavior changed; the current browser-gate status remains tracked in `PLAN.md` infrastructure blockers. +- Next candidates in the queue: extract the repeated definitions/observations dual-region composition in `src/presenter.js`; extract the repeated summary-plus-trend section composition in `src/presenter.js`; extract the next shared pure helpers around subject/provenance/count formatting in `src/presenter.js`. + ### 2026-08-31 (section-heading shell refactor) - Re-inventoried repeated UI construction under `pages/dashboard/src/` and selected the next bounded presentation-only DOM slice from the queue: repeated `.section-heading` shells in `src/components/package-detail.js`, `src/components/repositories-view.js`, plus the existing shared call sites in `src/components/execution-elements.js` and `src/components/ui-elements.js`. - Generalized `src/components/ui-primitives.js` `renderSectionHeading(...)` to a minimal options object with optional summary support, then replaced every identified call site so the shared helper now renders both summary-bearing and summary-free section headers without changing classes, headings, or visible copy. diff --git a/pages/dashboard/src/components/outcome-detail.js b/pages/dashboard/src/components/outcome-detail.js index d1bd40035..b897958fa 100644 --- a/pages/dashboard/src/components/outcome-detail.js +++ b/pages/dashboard/src/components/outcome-detail.js @@ -7,6 +7,7 @@ import { octicon } from '../octicons.js'; import { renderModeBadge, renderStatusBadge } from './badge.js'; import { findLink, renderExternalLink } from './link-content.js'; import { formatUtcDateTime } from './ui-primitives.js'; +import { renderMetadataSection } from './view-chrome.js'; const ALLOWED_MARKDOWN_TAGS = new Set([ 'A', 'BLOCKQUOTE', 'BR', 'CODE', 'DEL', 'DETAILS', 'DIV', 'EM', @@ -144,15 +145,6 @@ function renderOutcome(outcome) { ); } -/** - * @param {string} title - * @param {Node} content - * @returns {HTMLElement} - */ -function renderMetadataSection(title, content) { - return h('section', null, h('h2', null, title), content); -} - /** * Rebuilds GitHub-rendered Markdown through a strict element and attribute allowlist. * @param {string} html diff --git a/pages/dashboard/src/components/view-chrome.js b/pages/dashboard/src/components/view-chrome.js index fef509e5f..68e28dbfb 100644 --- a/pages/dashboard/src/components/view-chrome.js +++ b/pages/dashboard/src/components/view-chrome.js @@ -180,6 +180,16 @@ export function renderProvenanceSection(pageId, items) { return renderTitledRegion(pageId, 'Provenance', renderProvenanceList(items)); } +/** + * @param {string} title + * @param {Node} content + * @param {'h2'|'h3'|'h4'} [headingTag] + * @returns {HTMLElement} + */ +export function renderMetadataSection(title, content, headingTag = 'h2') { + return h('section', null, h(headingTag, null, title), content); +} + /** * @param {string} value * @returns {string} diff --git a/pages/dashboard/test/e2e/smoke.spec.js b/pages/dashboard/test/e2e/smoke.spec.js index bc8514133..5712e324c 100644 --- a/pages/dashboard/test/e2e/smoke.spec.js +++ b/pages/dashboard/test/e2e/smoke.spec.js @@ -174,7 +174,8 @@ function buildPresenterModuleUrl() { .replace("'../octicons.js'", JSON.stringify(octiconsModuleUrl)) .replace("'./badge.js'", JSON.stringify(badgeModuleUrl)) .replace("'./link-content.js'", JSON.stringify(linkContentModuleUrl)) - .replace("'./ui-primitives.js'", JSON.stringify(uiPrimitivesModuleUrl)); + .replace("'./ui-primitives.js'", JSON.stringify(uiPrimitivesModuleUrl)) + .replace("'./view-chrome.js'", JSON.stringify(viewChromeModuleUrl)); const outcomeDetailModuleUrl = `data:text/javascript;charset=utf-8,${encodeURIComponent(outcomeDetailSource)}`; const executionElementsSource = readFileSync(new URL('../../src/components/execution-elements.js', import.meta.url), 'utf8') diff --git a/pages/dashboard/test/unit/view-chrome.test.js b/pages/dashboard/test/unit/view-chrome.test.js index 2dd139cec..459a8d212 100644 --- a/pages/dashboard/test/unit/view-chrome.test.js +++ b/pages/dashboard/test/unit/view-chrome.test.js @@ -1,6 +1,6 @@ // @vitest-environment jsdom import { describe, expect, it } from 'vitest'; -import { customViewAvailabilityMessage, renderContextChrome, renderContextList, renderCustomViewStateDetails, renderDefinitionList, renderDefinitionListRows, renderPageSection, renderProvenanceList, renderProvenanceSection, renderSummaryList, renderSummaryRegion, renderTitledRegion, renderViewChrome, renderViewHeader, renderViewSectionChrome } from '../../src/components/view-chrome.js'; +import { customViewAvailabilityMessage, renderContextChrome, renderContextList, renderCustomViewStateDetails, renderDefinitionList, renderDefinitionListRows, renderMetadataSection, renderPageSection, renderProvenanceList, renderProvenanceSection, renderSummaryList, renderSummaryRegion, renderTitledRegion, renderViewChrome, renderViewHeader, renderViewSectionChrome } from '../../src/components/view-chrome.js'; describe('view chrome component helpers', () => { it('DLS-SAFE-007 renders focusable labeled page sections with deterministic heading ids', () => { @@ -199,4 +199,16 @@ describe('view chrome component helpers', () => { expect(section.getAttribute('aria-labelledby')).toBe('evals-provenance-heading'); expect(section.querySelector('.provenance-list')?.textContent).toContain('evals: evals-fixture (fixture) — as of 2026-08-29T20:00:00Z'); }); + + it('renders metadata sections with configurable heading levels', () => { + const defaultHeading = renderMetadataSection('Status', document.createElement('p')); + defaultHeading.querySelector('p')?.append('Closed'); + const customHeading = renderMetadataSection('Workflow', document.createElement('p'), 'h3'); + customHeading.querySelector('p')?.append('Daily review'); + + expect(defaultHeading.querySelector('h2')?.textContent).toBe('Status'); + expect(defaultHeading.textContent).toContain('Closed'); + expect(customHeading.querySelector('h3')?.textContent).toBe('Workflow'); + expect(customHeading.textContent).toContain('Daily review'); + }); });