rcf-lite 0.28.4 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +107 -1
- package/bin/rcf.js +4 -0
- package/fixtures/canary-manifest.json +9 -9
- package/guidance/define-draft-shapes.md +89 -0
- package/guidance/define-intake.md +97 -0
- package/guidance/define-intent.md +107 -0
- package/guidance/discover-prototype.md +67 -0
- package/guidance/discovery-prototypes.md +69 -0
- package/guidance/harness-template.md +34 -0
- package/guidance/managed/agent-instructions-block.hash +1 -1
- package/guidance/managed/agent-instructions-block.md +34 -0
- package/guidance/manifest.json +9 -2
- package/package.json +1 -1
- package/rcf/adrs/adr-4124-define-freeze-verb.json +30 -0
- package/rcf/adrs/adr-4126-persona-tagged-blockers-two-levels.json +40 -0
- package/rcf/adrs/adr-4127-readiness-tab-order-and-register.json +40 -0
- package/rcf/adrs/adr-4129-questions-harness-driven-loop.json +25 -0
- package/rcf/adrs/adr-4130-intake-harness-split-tool-scanned.json +20 -0
- package/rcf/adrs/adr-4132-prototypes-in-discovery-as-evidence.json +20 -0
- package/rcf/adrs/adr-4133-dag-dependency-depth-layout.json +40 -0
- package/rcf/adrs/adr-4134-index-json-route.json +35 -0
- package/rcf/adrs/adr-4135-readiness-po-register-and-adapter.json +35 -0
- package/rcf/adrs/adr-4136-embedding-host-fixture.json +35 -0
- package/rcf/code-nodes/cn-725.json +18 -2
- package/rcf/code-nodes/cn-726.json +10 -2
- package/rcf/code-nodes/cn-727.json +13 -4
- package/rcf/code-nodes/cn-728.json +18 -2
- package/rcf/code-nodes/cn-729.json +10 -2
- package/rcf/code-nodes/cn-730.json +13 -3
- package/rcf/code-nodes/cn-731.json +20 -0
- package/rcf/code-nodes/cn-732.json +14 -0
- package/rcf/code-nodes/cn-733.json +14 -0
- package/rcf/code-nodes/cn-734.json +14 -0
- package/rcf/code-nodes/cn-735.json +20 -0
- package/rcf/code-nodes/cn-736.json +14 -0
- package/rcf/code-nodes/cn-737.json +14 -0
- package/rcf/code-nodes/cn-738.json +14 -0
- package/rcf/code-nodes/cn-739.json +18 -0
- package/rcf/code-nodes/cn-740.json +19 -0
- package/rcf/code-nodes/cn-741.json +16 -0
- package/rcf/code-nodes/cn-742.json +16 -0
- package/rcf/code-nodes/cn-743.json +15 -0
- package/rcf/code-nodes/cn-744.json +22 -0
- package/rcf/code-nodes/cn-745.json +16 -0
- package/rcf/code-nodes/cn-746.json +14 -0
- package/rcf/code-nodes/cn-747.json +18 -0
- package/rcf/code-nodes/cn-748.json +16 -0
- package/rcf/code-nodes/cn-749.json +16 -0
- package/rcf/code-nodes/cn-750.json +15 -0
- package/rcf/code-nodes/cn-751.json +18 -0
- package/rcf/code-nodes/cn-752.json +14 -0
- package/rcf/fbs/fbs-196.json +13 -5
- package/rcf/fbs/fbs-198.json +42 -0
- package/rcf/fbs/fbs-199.json +59 -0
- package/rcf/fbs/fbs-200.json +46 -0
- package/rcf/fbs/fbs-201.json +49 -0
- package/rcf/fbs/fbs-202.json +110 -0
- package/rcf/fbs/fbs-203.json +100 -0
- package/rcf/requirements/req-002.json +2 -2
- package/rcf/requirements/req-173.json +2 -2
- package/rcf/requirements/req-174.json +10 -4
- package/rcf/requirements/req-175.json +10 -4
- package/rcf/requirements/req-176.json +25 -0
- package/rcf/requirements/req-180.json +31 -0
- package/rcf/requirements/req-181.json +31 -0
- package/rcf/requirements/req-182.json +25 -0
- package/rcf/requirements/req-186.json +25 -0
- package/rcf/requirements/req-187.json +31 -0
- package/rcf/requirements/req-188.json +25 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +15 -3
- package/rcf/tacs/tac-4122-define-gates.json +40 -4
- package/rcf/tacs/tac-4123-define-readiness.json +39 -3
- package/rcf/tacs/tac-4124-define-freeze.json +28 -0
- package/rcf/tacs/tac-4127-readiness-view.json +49 -0
- package/rcf/tacs/tac-4128-define-guidance.json +53 -0
- package/rcf/tacs/tac-4129-define-questions.json +45 -0
- package/rcf/tacs/tac-4130-define-intake-brief.json +35 -0
- package/rcf/tacs/tac-4131-build-dag-view.json +39 -0
- package/rcf/tacs/tac-4132-id-lookup.json +42 -0
- package/rcf/tacs/tac-4133-readiness-po-layer.json +37 -0
- package/rcf/tacs/tac-4134-wespa-host-fixture.json +54 -0
- package/rcf/test-suites/ts-016.json +3 -3
- package/rcf/test-suites/ts-215.json +148 -4
- package/rcf/test-suites/ts-216.json +19 -3
- package/rcf/test-suites/ts-218.json +78 -0
- package/rcf/test-suites/ts-219.json +70 -0
- package/rcf/test-suites/ts-220.json +78 -0
- package/rcf/test-suites/ts-221.json +78 -0
- package/rcf/test-suites/ts-222.json +100 -0
- package/rcf/test-suites/ts-223.json +94 -0
- package/rcf/test-suites/ts-224.json +70 -0
- package/rcf/test-suites/ts-225.json +78 -0
- package/rcf/test-suites/ts-226.json +158 -0
- package/rcf/test-suites/ts-227.json +38 -0
- package/rcf/test-suites/ts-228.json +86 -0
- package/rcf/test-suites/ts-229.json +83 -0
- package/rcf/user-stories/us-17302.json +50 -0
- package/rcf/user-stories/us-17401.json +14 -2
- package/rcf/user-stories/us-17402.json +55 -0
- package/rcf/user-stories/us-17403.json +60 -0
- package/rcf/user-stories/us-17502.json +60 -0
- package/rcf/user-stories/us-17503.json +65 -0
- package/rcf/user-stories/us-17601.json +58 -0
- package/rcf/user-stories/us-18001.json +55 -0
- package/rcf/user-stories/us-18002.json +35 -0
- package/rcf/user-stories/us-18101.json +35 -0
- package/rcf/user-stories/us-18202.json +40 -0
- package/rcf/user-stories/us-18601.json +55 -0
- package/rcf/user-stories/us-18602.json +50 -0
- package/rcf/user-stories/us-18603.json +45 -0
- package/rcf/user-stories/us-18701.json +50 -0
- package/rcf/user-stories/us-18702.json +50 -0
- package/rcf/user-stories/us-18801.json +45 -0
- package/rcf/user-stories/us-204.json +59 -0
- package/rcf/user-stories/us-205.json +64 -0
- package/rcf/user-stories/us-206.json +54 -0
- package/releases/releases.yaml +11 -1
- package/src/cli/freeze.js +730 -0
- package/src/cli/help.js +12 -0
- package/src/cli/ledger.js +228 -12
- package/src/cli/questions.js +288 -0
- package/src/cli/readiness.js +267 -49
- package/src/cli/view.js +29 -0
- package/src/define/freeze-record.js +30 -15
- package/src/define/ledgers.js +210 -11
- package/src/intake/index.js +1 -1
- package/src/intake/orchestrator.js +117 -0
- package/src/mcp/tools.js +354 -0
- package/src/query/delta.js +6 -0
- package/src/query/gates.js +314 -39
- package/src/query/index.js +16 -2
- package/src/query/questions.js +438 -0
- package/src/query/readiness.js +248 -7
- package/src/server/index.js +24 -2
- package/src/server/routes.js +68 -1
- package/src/setup/identity-seed.js +17 -0
- package/src/view/build-dag.js +563 -0
- package/src/view/components/badge.js +41 -0
- package/src/view/components/diff.js +72 -0
- package/src/view/components/doc-row.js +62 -0
- package/src/view/components/empty-state.js +35 -0
- package/src/view/components/entity-selector.js +137 -0
- package/src/view/components/filter-bar.js +97 -0
- package/src/view/components/findings-list.js +49 -0
- package/src/view/components/index.js +12 -0
- package/src/view/components/pill.js +46 -0
- package/src/view/components/sub-tab-strip.js +43 -0
- package/src/view/components/toast.js +25 -0
- package/src/view/components-fixture.js +265 -0
- package/src/view/doc-renderers/architecture.js +304 -0
- package/src/view/doc-renderers/build.js +160 -0
- package/src/view/doc-renderers/index.js +2 -0
- package/src/view/doc-renderers/prd.js +35 -6
- package/src/view/html-page.js +700 -602
- package/src/view/index.js +96 -3
- package/src/view/live-client.js +39 -6
- package/src/view/needs-work.js +79 -0
- package/src/view/page-init.js +2193 -0
- package/src/view/readiness/phrasebook.js +115 -0
- package/src/view/readiness/po-layer.js +288 -0
- package/src/view/readiness/question-adapter.js +329 -0
- package/src/view/readiness/stage-legend.js +71 -0
- package/src/view/readiness.js +505 -0
- package/src/view/style.css +2017 -382
- package/src/view/test-host/embed-client.js +121 -0
- package/src/view/test-host/fixture.js +183 -0
- package/src/view/test-host/lints.js +220 -0
- package/src/view/view-index.js +323 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Shared DocRow collapsible (viewer UI refresh PR 2, decision 2 / w-
|
|
2
|
+
// 2026-10-02-dave-010): one markup shape for every per-document row on
|
|
3
|
+
// every list tab (REQ, US, TAC, ADR, FBS). Closed by default, hidden-
|
|
4
|
+
// by-filter via `[hidden]`, deep-link-navigable via `data-doc-id` and
|
|
5
|
+
// the raw document id as the anchor.
|
|
6
|
+
//
|
|
7
|
+
// Shape:
|
|
8
|
+
// <details class="rcf-row doc-row {className}" data-doc-id="{id}" [open]>
|
|
9
|
+
// <summary>
|
|
10
|
+
// <span class="id">{id}</span>
|
|
11
|
+
// <span class="title">{title}</span>
|
|
12
|
+
// <span class="meta">{meta badges / pill}</span>
|
|
13
|
+
// </summary>
|
|
14
|
+
// <div class="rcf-row-body body">
|
|
15
|
+
// {body}
|
|
16
|
+
// </div>
|
|
17
|
+
// </details>
|
|
18
|
+
//
|
|
19
|
+
// The .doc-row prefix lets later tab-side migrations (PR 3-6) swap
|
|
20
|
+
// `detailsWrap` for `renderDocRow` while leaving the existing CSS
|
|
21
|
+
// selectors (`details.doc-details`) alone until each tab lands. PR 2
|
|
22
|
+
// ships the markup and the matching .rcf-row stylesheet rules; no tab
|
|
23
|
+
// is cut over here. The fixture page (/_fixtures/components) is where
|
|
24
|
+
// the component is exercised in isolation.
|
|
25
|
+
|
|
26
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {object} args
|
|
30
|
+
* @param {string} args.id display doc id (e.g. "REQ-002")
|
|
31
|
+
* @param {string} args.title one-line label (right of the id)
|
|
32
|
+
* @param {string} [args.body] rendered body HTML (empty for a header-only row)
|
|
33
|
+
* @param {string} [args.className] extra class applied to the <details>
|
|
34
|
+
* @param {boolean} [args.open] initial open state (default: closed)
|
|
35
|
+
* @param {string} [args.dataDocId] override the id used for `data-doc-id` (defaults to args.id)
|
|
36
|
+
* @param {string} [args.meta] pre-rendered meta HTML (badges + pill) shown right of the title
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function renderDocRow({
|
|
40
|
+
id,
|
|
41
|
+
title,
|
|
42
|
+
body = '',
|
|
43
|
+
className = '',
|
|
44
|
+
open = false,
|
|
45
|
+
dataDocId,
|
|
46
|
+
meta = '',
|
|
47
|
+
}) {
|
|
48
|
+
const classes = ['rcf-row', 'doc-row'];
|
|
49
|
+
if (className) classes.push(String(className));
|
|
50
|
+
const openAttr = open ? ' open' : '';
|
|
51
|
+
const anchorId = dataDocId ?? id;
|
|
52
|
+
return `<details class="${classes.join(' ')}" data-doc-id="${escapeHtml(anchorId)}"${openAttr}>
|
|
53
|
+
<summary>
|
|
54
|
+
<span class="id">${escapeHtml(id)}</span>
|
|
55
|
+
<span class="title">${escapeHtml(title ?? '')}</span>
|
|
56
|
+
${meta ? `<span class="meta">${meta}</span>` : ''}
|
|
57
|
+
</summary>
|
|
58
|
+
<div class="rcf-row-body body">
|
|
59
|
+
${body}
|
|
60
|
+
</div>
|
|
61
|
+
</details>`;
|
|
62
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Shared EmptyState (viewer UI refresh PR 2, design doc section 4):
|
|
2
|
+
// one dashed-outline, muted-text block for "nothing here yet" cases
|
|
3
|
+
// across every tab (Architecture section without content, a Needs-work
|
|
4
|
+
// table with no rows, a filter that excludes every item, etc).
|
|
5
|
+
//
|
|
6
|
+
// Shape:
|
|
7
|
+
// <div class="rcf-empty-state empty" role="status">
|
|
8
|
+
// <p class="rcf-empty-state-title">{title}</p>
|
|
9
|
+
// [<p class="rcf-empty-state-hint">{hint}</p>]
|
|
10
|
+
// [<p class="rcf-empty-state-action">{actionHtml}</p>]
|
|
11
|
+
// </div>
|
|
12
|
+
//
|
|
13
|
+
// `hint` is plain text; `actionHtml` is an escape hatch for the one
|
|
14
|
+
// case the design calls out (an authoring CLI call under a missing TAD
|
|
15
|
+
// section) and must already be safe HTML - callers that pass raw user
|
|
16
|
+
// text should use `hint` instead.
|
|
17
|
+
|
|
18
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} args
|
|
22
|
+
* @param {string} args.title
|
|
23
|
+
* @param {string} [args.hint]
|
|
24
|
+
* @param {string} [args.actionHtml]
|
|
25
|
+
* @returns {string}
|
|
26
|
+
*/
|
|
27
|
+
export function renderEmptyState({ title, hint, actionHtml }) {
|
|
28
|
+
const hintBlock = hint ? `<p class="rcf-empty-state-hint">${escapeHtml(hint)}</p>` : '';
|
|
29
|
+
const actionBlock = actionHtml ? `<p class="rcf-empty-state-action">${actionHtml}</p>` : '';
|
|
30
|
+
return `<div class="rcf-empty-state empty" role="status">
|
|
31
|
+
<p class="rcf-empty-state-title">${escapeHtml(title)}</p>
|
|
32
|
+
${hintBlock}
|
|
33
|
+
${actionBlock}
|
|
34
|
+
</div>`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// Shared EntitySelector (viewer UI refresh PR 3, decision 4): replaces
|
|
2
|
+
// the PRD's 109 inline requirement links with a compact header showing
|
|
3
|
+
// the total, a type-ahead jump input, and a row of area chips with
|
|
4
|
+
// counts that deep-link into the Requirements tab filtered by domain.
|
|
5
|
+
// Pure HTML string helper; the type-ahead behaviour (filter the list,
|
|
6
|
+
// Enter to jump) is wired by page-init.js.
|
|
7
|
+
//
|
|
8
|
+
// Markup shape:
|
|
9
|
+
//
|
|
10
|
+
// <div class="rcf-entity-selector" data-rcf-entity-selector="requirements"
|
|
11
|
+
// data-tab="requirements">
|
|
12
|
+
// <div class="rcf-entity-selector-head">
|
|
13
|
+
// <span class="rcf-entity-selector-total">109 requirements in 21 areas</span>
|
|
14
|
+
// </div>
|
|
15
|
+
// <div class="rcf-entity-selector-jump">
|
|
16
|
+
// <input class="rcf-entity-selector-input" type="search"
|
|
17
|
+
// placeholder="..." autocomplete="off"
|
|
18
|
+
// aria-label="Jump to a requirement by id or title">
|
|
19
|
+
// <div class="rcf-entity-selector-results" hidden></div>
|
|
20
|
+
// </div>
|
|
21
|
+
// <div class="rcf-entity-selector-facets" aria-label="Areas">
|
|
22
|
+
// <a class="rcf-entity-selector-chip" href="#tab=requirements&domain=platform"
|
|
23
|
+
// data-facet-key="domain" data-facet-value="platform">
|
|
24
|
+
// platform <span class="rcf-badge rcf-badge--count"><span class="rcf-badge-value">17</span></span>
|
|
25
|
+
// </a>
|
|
26
|
+
// ...
|
|
27
|
+
// </div>
|
|
28
|
+
// </div>
|
|
29
|
+
//
|
|
30
|
+
// The payload (every entity's id, title, facet value) is serialised
|
|
31
|
+
// onto the root `<div>` as a `data-items` attribute (JSON, HTML-escaped)
|
|
32
|
+
// so the jump input can filter without a network call. The container
|
|
33
|
+
// carries all state so the shell page has no inline `<script>` body
|
|
34
|
+
// (PR 1 contract, decision 10).
|
|
35
|
+
// Decision 11 names `#entity=<id>` as the deep link target so a jump
|
|
36
|
+
// writes `#tab=<targetTab>&entity=<id>` (preserving the host query via
|
|
37
|
+
// the existing urlWithHash helper).
|
|
38
|
+
|
|
39
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
40
|
+
import { renderBadge } from './badge.js';
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {object} EntitySelectorItem
|
|
44
|
+
* @property {string} id document id (e.g. "REQ-002")
|
|
45
|
+
* @property {string} title display title
|
|
46
|
+
* @property {string} [facet] facet value (e.g. "platform")
|
|
47
|
+
*/
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @typedef {object} EntitySelectorFacet
|
|
51
|
+
* @property {string} value facet value (e.g. "platform")
|
|
52
|
+
* @property {number} count count of items in this facet
|
|
53
|
+
* @property {string} [label] optional display label (defaults to value)
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @param {object} args
|
|
58
|
+
* @param {string} args.hashKey selector slot name (e.g. "requirements")
|
|
59
|
+
* @param {string} args.targetTab tab name used in hash writes (e.g. "requirements")
|
|
60
|
+
* @param {string} args.facetKey hash key for area chips (e.g. "domain")
|
|
61
|
+
* @param {EntitySelectorItem[]} args.items every entity (for the jump input)
|
|
62
|
+
* @param {EntitySelectorFacet[]} [args.facets] ordered facet chips (if omitted, derived from items)
|
|
63
|
+
* @param {string} [args.totalLabel] pre-rendered total line (e.g. "109 requirements in 21 areas")
|
|
64
|
+
* @param {string} [args.placeholder] input placeholder
|
|
65
|
+
* @param {string} [args.chipsLabel] muted lead-in above the chip row
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
export function renderEntitySelector({
|
|
69
|
+
hashKey,
|
|
70
|
+
targetTab,
|
|
71
|
+
facetKey,
|
|
72
|
+
items,
|
|
73
|
+
facets,
|
|
74
|
+
totalLabel,
|
|
75
|
+
placeholder = 'Jump to an entity by id or title',
|
|
76
|
+
chipsLabel = 'By area (click to open the tab filtered):',
|
|
77
|
+
}) {
|
|
78
|
+
const safeItems = Array.isArray(items) ? items : [];
|
|
79
|
+
const resolvedFacets = Array.isArray(facets) && facets.length > 0
|
|
80
|
+
? facets
|
|
81
|
+
: deriveFacets(safeItems);
|
|
82
|
+
const total = totalLabel
|
|
83
|
+
?? `${safeItems.length} ${safeItems.length === 1 ? 'item' : 'items'} in ${resolvedFacets.length} ${resolvedFacets.length === 1 ? 'area' : 'areas'}`;
|
|
84
|
+
|
|
85
|
+
const chipBlocks = resolvedFacets.map((f) => {
|
|
86
|
+
const label = f.label ?? f.value;
|
|
87
|
+
const hash = `#tab=${encodeURIComponent(targetTab)}&${encodeURIComponent(facetKey)}=${encodeURIComponent(f.value)}`;
|
|
88
|
+
const count = renderBadge({ value: f.count, variant: 'count' });
|
|
89
|
+
return `<a class="rcf-entity-selector-chip" href="${escapeHtml(hash)}" data-facet-key="${escapeHtml(facetKey)}" data-facet-value="${escapeHtml(f.value)}"><span class="rcf-entity-selector-chip-label">${escapeHtml(label)}</span> ${count}</a>`;
|
|
90
|
+
}).join('\n ');
|
|
91
|
+
|
|
92
|
+
const payload = JSON.stringify(safeItems.map((it) => ({
|
|
93
|
+
id: it.id,
|
|
94
|
+
title: it.title ?? '',
|
|
95
|
+
facet: it.facet ?? '',
|
|
96
|
+
})));
|
|
97
|
+
// The payload rides on the container as a `data-items` attribute.
|
|
98
|
+
// escapeHtml encodes `&`, `<`, `>`, `"` and `'` so the attribute
|
|
99
|
+
// boundary cannot be broken by any id, title or facet value, and
|
|
100
|
+
// no `<script>` element is emitted (PR 1 "no inline script body").
|
|
101
|
+
const itemsAttr = escapeHtml(payload);
|
|
102
|
+
|
|
103
|
+
return `<div class="rcf-entity-selector" data-rcf-entity-selector="${escapeHtml(hashKey)}" data-target-tab="${escapeHtml(targetTab)}" data-facet-key="${escapeHtml(facetKey)}" data-items="${itemsAttr}">
|
|
104
|
+
<div class="rcf-entity-selector-head">
|
|
105
|
+
<span class="rcf-entity-selector-total">${escapeHtml(total)}</span>
|
|
106
|
+
</div>
|
|
107
|
+
<div class="rcf-entity-selector-jump">
|
|
108
|
+
<input class="rcf-entity-selector-input" type="search" placeholder="${escapeHtml(placeholder)}" autocomplete="off" aria-label="${escapeHtml(placeholder)}">
|
|
109
|
+
<div class="rcf-entity-selector-results" hidden role="listbox"></div>
|
|
110
|
+
</div>
|
|
111
|
+
<div class="rcf-entity-selector-facets-lead muted small">${escapeHtml(chipsLabel)}</div>
|
|
112
|
+
<div class="rcf-entity-selector-facets" aria-label="Areas">
|
|
113
|
+
${chipBlocks}
|
|
114
|
+
</div>
|
|
115
|
+
</div>`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Derive facet chips (value + count) from the item list when the caller
|
|
120
|
+
* does not pass an explicit facet order. Facets sort by count desc,
|
|
121
|
+
* then by value asc. Items with an empty facet are dropped.
|
|
122
|
+
*
|
|
123
|
+
* @param {EntitySelectorItem[]} items
|
|
124
|
+
* @returns {EntitySelectorFacet[]}
|
|
125
|
+
*/
|
|
126
|
+
function deriveFacets(items) {
|
|
127
|
+
const counts = new Map();
|
|
128
|
+
for (const it of items) {
|
|
129
|
+
const v = it.facet;
|
|
130
|
+
if (typeof v !== 'string' || v.length === 0) continue;
|
|
131
|
+
counts.set(v, (counts.get(v) ?? 0) + 1);
|
|
132
|
+
}
|
|
133
|
+
const out = [];
|
|
134
|
+
for (const [value, count] of counts.entries()) out.push({ value, count });
|
|
135
|
+
out.sort((a, b) => (b.count - a.count) || a.value.localeCompare(b.value));
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Shared FilterBar (viewer UI refresh PR 2, decision 3): sticky bar
|
|
2
|
+
// under the tabstrip with a text query, facet selects, toggles, a live
|
|
3
|
+
// row count and an Expand / Collapse all control. Filter state lives
|
|
4
|
+
// in the hash so a filtered view is a link; the hashKey argument lets
|
|
5
|
+
// each tab own its own slice of the hash namespace (e.g. "requirements"
|
|
6
|
+
// writes #tab=requirements&q=...&area=...).
|
|
7
|
+
//
|
|
8
|
+
// Markup shape:
|
|
9
|
+
//
|
|
10
|
+
// <div class="rcf-filterbar filterbar" data-rcf-filterbar="{hashKey}">
|
|
11
|
+
// <input type="search" class="rcf-filter-text" placeholder="...">
|
|
12
|
+
// <select class="rcf-filter-select" data-filter-key="area">...</select>
|
|
13
|
+
// <label class="rcf-filter-toggle">
|
|
14
|
+
// <input type="checkbox" data-filter-key="needsWork"> Needs work
|
|
15
|
+
// </label>
|
|
16
|
+
// <span class="rcf-filter-count count">N of M visible</span>
|
|
17
|
+
// <button type="button" class="rcf-filter-expand linkish">Expand all</button>
|
|
18
|
+
// </div>
|
|
19
|
+
//
|
|
20
|
+
// PR 2 ships the markup helper only; the hash-sync + list-filter wiring
|
|
21
|
+
// lives in page-init.js and ships with the first list tab that adopts
|
|
22
|
+
// FilterBar (PR 3, Requirements). The helper is pure HTML and has no
|
|
23
|
+
// behaviour of its own, so a later tab can mount it without a Router
|
|
24
|
+
// handshake.
|
|
25
|
+
|
|
26
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @typedef {object} FilterSelectSpec
|
|
30
|
+
* @property {string} key facet key (e.g. "area", "status")
|
|
31
|
+
* @property {string} label visible label
|
|
32
|
+
* @property {Array<{ value: string, label: string }>} options
|
|
33
|
+
* @property {string} [value] initial selected value
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @typedef {object} FilterToggleSpec
|
|
38
|
+
* @property {string} key toggle key (e.g. "needsWork")
|
|
39
|
+
* @property {string} label visible label
|
|
40
|
+
* @property {boolean} [checked] initial state
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @param {object} args
|
|
45
|
+
* @param {string} args.hashKey owning tab, used as the hash namespace
|
|
46
|
+
* @param {string} [args.placeholder] text input placeholder
|
|
47
|
+
* @param {string} [args.text] initial text value
|
|
48
|
+
* @param {FilterSelectSpec[]} [args.selects]
|
|
49
|
+
* @param {FilterToggleSpec[]} [args.toggles]
|
|
50
|
+
* @param {{ visible: number, total: number }} [args.count] initial count readout
|
|
51
|
+
* @param {boolean} [args.showExpandAll] render the Expand / Collapse control
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function renderFilterBar({
|
|
55
|
+
hashKey,
|
|
56
|
+
placeholder = 'Search this list',
|
|
57
|
+
text = '',
|
|
58
|
+
selects = [],
|
|
59
|
+
toggles = [],
|
|
60
|
+
count,
|
|
61
|
+
showExpandAll = true,
|
|
62
|
+
}) {
|
|
63
|
+
const selectBlocks = selects.map((s) => {
|
|
64
|
+
const options = (s.options ?? []).map((o) => {
|
|
65
|
+
const selected = o.value === s.value ? ' selected' : '';
|
|
66
|
+
return `<option value="${escapeHtml(o.value)}"${selected}>${escapeHtml(o.label)}</option>`;
|
|
67
|
+
}).join('');
|
|
68
|
+
return `<label class="rcf-filter-label">
|
|
69
|
+
<span>${escapeHtml(s.label)}</span>
|
|
70
|
+
<select class="rcf-filter-select" data-filter-key="${escapeHtml(s.key)}">${options}</select>
|
|
71
|
+
</label>`;
|
|
72
|
+
}).join('\n ');
|
|
73
|
+
|
|
74
|
+
const toggleBlocks = toggles.map((t) => {
|
|
75
|
+
const checked = t.checked ? ' checked' : '';
|
|
76
|
+
return `<label class="rcf-filter-toggle">
|
|
77
|
+
<input type="checkbox" data-filter-key="${escapeHtml(t.key)}"${checked}>
|
|
78
|
+
<span>${escapeHtml(t.label)}</span>
|
|
79
|
+
</label>`;
|
|
80
|
+
}).join('\n ');
|
|
81
|
+
|
|
82
|
+
const countBlock = count
|
|
83
|
+
? `<span class="rcf-filter-count count">${escapeHtml(String(count.visible))} of ${escapeHtml(String(count.total))} visible</span>`
|
|
84
|
+
: '';
|
|
85
|
+
|
|
86
|
+
const expandBlock = showExpandAll
|
|
87
|
+
? `<button type="button" class="rcf-filter-expand linkish" data-expand-state="collapsed">Expand all</button>`
|
|
88
|
+
: '';
|
|
89
|
+
|
|
90
|
+
return `<div class="rcf-filterbar filterbar" data-rcf-filterbar="${escapeHtml(hashKey)}">
|
|
91
|
+
<input type="search" class="rcf-filter-text" value="${escapeHtml(text)}" placeholder="${escapeHtml(placeholder)}">
|
|
92
|
+
${selectBlocks}
|
|
93
|
+
${toggleBlocks}
|
|
94
|
+
${countBlock}
|
|
95
|
+
${expandBlock}
|
|
96
|
+
</div>`;
|
|
97
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Shared findings-list component (REQ-181, TAC-4127-readiness-view).
|
|
2
|
+
// Renders a heading with a count badge plus an unordered list of
|
|
3
|
+
// `{ id, why, href? }` items. Ids are capped at 20, and the tail
|
|
4
|
+
// becomes a `... N more suppressed` line so a huge failing list stays
|
|
5
|
+
// legible. Pure; no I/O. (AC-18101-2.)
|
|
6
|
+
|
|
7
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @typedef {object} FindingsListItem
|
|
11
|
+
* @property {string} id - document id (also the anchor label)
|
|
12
|
+
* @property {string} [why] - one-line reason
|
|
13
|
+
* @property {string} [href] - override anchor target; defaults to `#<id>`
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} args
|
|
18
|
+
* @param {string} args.heading - section heading text (plain)
|
|
19
|
+
* @param {number} args.count - the total count to show in the badge
|
|
20
|
+
* (may exceed `items.length`; the component renders the suppressed
|
|
21
|
+
* tail from the delta)
|
|
22
|
+
* @param {FindingsListItem[]} args.items - items to render (will be
|
|
23
|
+
* capped at the display limit)
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function findingsList({ heading, count, items }) {
|
|
27
|
+
const cap = 20;
|
|
28
|
+
const list = Array.isArray(items) ? items : [];
|
|
29
|
+
const total = typeof count === 'number' && Number.isFinite(count) ? count : list.length;
|
|
30
|
+
const shown = list.slice(0, cap);
|
|
31
|
+
const liHtml = shown.map((item) => {
|
|
32
|
+
const id = String(item?.id ?? '');
|
|
33
|
+
const anchorHref = typeof item?.href === 'string' && item.href.length > 0
|
|
34
|
+
? item.href
|
|
35
|
+
: `#${id}`;
|
|
36
|
+
const why = typeof item?.why === 'string' && item.why.length > 0
|
|
37
|
+
? `: ${escapeHtml(item.why)}`
|
|
38
|
+
: '';
|
|
39
|
+
return `<li><a href="${escapeHtml(anchorHref)}">${escapeHtml(id)}</a>${why}</li>`;
|
|
40
|
+
}).join('');
|
|
41
|
+
const suppressedCount = Math.max(0, total - shown.length);
|
|
42
|
+
const suppressedLi = suppressedCount > 0
|
|
43
|
+
? `<li class="rcf-findings-list__suppressed"><em>... ${suppressedCount} more suppressed</em></li>`
|
|
44
|
+
: '';
|
|
45
|
+
return `<div class="rcf-findings-list">`
|
|
46
|
+
+ `<h4>${escapeHtml(heading)} <span class="rcf-findings-list__count">${total}</span></h4>`
|
|
47
|
+
+ `<ul>${liHtml}${suppressedLi}</ul>`
|
|
48
|
+
+ `</div>`;
|
|
49
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Shared component barrel (viewer UI refresh PR 2). Re-exports every
|
|
2
|
+
// per-component string helper so downstream tabs can import a single
|
|
3
|
+
// well-known path. New components added in later PRs land here too.
|
|
4
|
+
|
|
5
|
+
export { pill } from './pill.js';
|
|
6
|
+
export { renderDocRow } from './doc-row.js';
|
|
7
|
+
export { renderBadge } from './badge.js';
|
|
8
|
+
export { renderFilterBar } from './filter-bar.js';
|
|
9
|
+
export { renderEmptyState } from './empty-state.js';
|
|
10
|
+
export { renderToastContainer } from './toast.js';
|
|
11
|
+
export { renderSubTabStrip } from './sub-tab-strip.js';
|
|
12
|
+
export { renderEntitySelector } from './entity-selector.js';
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Shared pill component (REQ-181, TAC-4127-readiness-view). One module,
|
|
2
|
+
// one markup shape: `<span class="rcf-pill rcf-pill--<variant>
|
|
3
|
+
// rcf-pill--<normalisedValue>" [title?]>{value}</span>`, or an `<a>`
|
|
4
|
+
// with the same classes when `href` is passed. The vocabulary families
|
|
5
|
+
// (gate state / level verdict / persona / AC class) all ride the same
|
|
6
|
+
// shape so a status reads the same on every tab (AC-18101-1).
|
|
7
|
+
|
|
8
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Normalise a value into a kebab-cased class suffix: lowercased with
|
|
12
|
+
* `/`, whitespace and `_` collapsed to `-`. Everything outside
|
|
13
|
+
* `[a-z0-9-]` is stripped. The caller controls what the display label
|
|
14
|
+
* reads; this only touches the class suffix.
|
|
15
|
+
*
|
|
16
|
+
* @param {string} raw
|
|
17
|
+
* @returns {string}
|
|
18
|
+
*/
|
|
19
|
+
function normaliseValue(raw) {
|
|
20
|
+
const s = String(raw ?? '').toLowerCase().replace(/[\s/_]+/g, '-');
|
|
21
|
+
return s.replace(/[^a-z0-9-]/g, '').replace(/-+/g, '-').replace(/^-+|-+$/g, '');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} args
|
|
26
|
+
* @param {string} args.value - display label (also drives the class suffix)
|
|
27
|
+
* @param {string} args.variant - vocabulary family (e.g. `gate-state`,
|
|
28
|
+
* `level-verdict`, `persona`, `ac-class`). Also normalised into the
|
|
29
|
+
* class suffix for the family.
|
|
30
|
+
* @param {string} [args.title] - optional tooltip text
|
|
31
|
+
* @param {string} [args.href] - optional href; when set, renders an `<a>`
|
|
32
|
+
* @returns {string}
|
|
33
|
+
*/
|
|
34
|
+
export function pill({ value, variant, title, href }) {
|
|
35
|
+
const valueText = String(value ?? '');
|
|
36
|
+
const variantClass = `rcf-pill--${normaliseValue(variant)}`;
|
|
37
|
+
const valueClass = `rcf-pill--${normaliseValue(valueText)}`;
|
|
38
|
+
const classes = `rcf-pill ${variantClass} ${valueClass}`.trim();
|
|
39
|
+
const titleAttr = typeof title === 'string' && title.length > 0
|
|
40
|
+
? ` title="${escapeHtml(title)}"`
|
|
41
|
+
: '';
|
|
42
|
+
if (typeof href === 'string' && href.length > 0) {
|
|
43
|
+
return `<a class="${classes}" href="${escapeHtml(href)}"${titleAttr}>${escapeHtml(valueText)}</a>`;
|
|
44
|
+
}
|
|
45
|
+
return `<span class="${classes}"${titleAttr}>${escapeHtml(valueText)}</span>`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Shared SubTabStrip (viewer UI refresh PR 2, design doc section 4):
|
|
2
|
+
// the one-row chip strip that sits inside a tab panel and switches
|
|
3
|
+
// sub-views (Build tab's Specs | DAG in PR 5-6). Writes `sub=` into
|
|
4
|
+
// the owning tab's hash slot via the `hashKey` argument; the Router
|
|
5
|
+
// in page-init.js owns the read.
|
|
6
|
+
//
|
|
7
|
+
// Markup shape:
|
|
8
|
+
// <nav class="rcf-subtabs subtabs" aria-label="Sub-sections"
|
|
9
|
+
// data-rcf-subtabstrip="{hashKey}">
|
|
10
|
+
// <button type="button" role="tab" data-sub="{item.key}"
|
|
11
|
+
// aria-selected="true|false">{item.label}</button>
|
|
12
|
+
// ...
|
|
13
|
+
// </nav>
|
|
14
|
+
|
|
15
|
+
import { escapeHtml } from '../doc-renderers/helpers.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {object} SubTabItem
|
|
19
|
+
* @property {string} key value written into `sub=`
|
|
20
|
+
* @property {string} label
|
|
21
|
+
* @property {string} [controls] optional id of the panel this item controls
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @param {object} args
|
|
26
|
+
* @param {string} args.hashKey owning tab (e.g. "build")
|
|
27
|
+
* @param {SubTabItem[]} args.items
|
|
28
|
+
* @param {string} [args.active] key of the item to mark as active
|
|
29
|
+
* @returns {string}
|
|
30
|
+
*/
|
|
31
|
+
export function renderSubTabStrip({ hashKey, items, active }) {
|
|
32
|
+
const effectiveActive = active ?? (items[0]?.key ?? '');
|
|
33
|
+
const buttons = items.map((item) => {
|
|
34
|
+
const isActive = item.key === effectiveActive;
|
|
35
|
+
const controlsAttr = item.controls
|
|
36
|
+
? ` aria-controls="${escapeHtml(item.controls)}"`
|
|
37
|
+
: '';
|
|
38
|
+
return `<button type="button" role="tab" data-sub="${escapeHtml(item.key)}"${controlsAttr} aria-selected="${isActive ? 'true' : 'false'}">${escapeHtml(item.label)}</button>`;
|
|
39
|
+
}).join('\n ');
|
|
40
|
+
return `<nav class="rcf-subtabs subtabs" role="tablist" aria-label="Sub-sections" data-rcf-subtabstrip="${escapeHtml(hashKey)}">
|
|
41
|
+
${buttons}
|
|
42
|
+
</nav>`;
|
|
43
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Shared Toast container (viewer UI refresh PR 2, design doc section 4):
|
|
2
|
+
// a single bottom-centred transient message block that lives in the
|
|
3
|
+
// AppShell. PR 2 ships the container markup and a tiny vanilla showToast
|
|
4
|
+
// helper on `window.rcfView`. The container is empty on first paint;
|
|
5
|
+
// the helper writes the text, flips the `.show` class, auto-hides after
|
|
6
|
+
// a brief delay and clears the content.
|
|
7
|
+
//
|
|
8
|
+
// Shape (uses <output> rather than <div> so the Phase 3.8 D13a
|
|
9
|
+
// `lastIndexOf("</div>")` live-content wrapper check in
|
|
10
|
+
// test/view/html-page.test.js stays unambiguous; <output> carries the
|
|
11
|
+
// implicit role="status" we need anyway):
|
|
12
|
+
//
|
|
13
|
+
// <output class="rcf-toast toast" aria-live="polite" aria-atomic="true"
|
|
14
|
+
// data-toast></output>
|
|
15
|
+
//
|
|
16
|
+
// Idempotent client wiring is in page-init.js; the container is
|
|
17
|
+
// rendered once by the AppShell so the SSE innerHTML swap (which
|
|
18
|
+
// replaces #rcf-live-content only) does not blow it away.
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
export function renderToastContainer() {
|
|
24
|
+
return '<output class="rcf-toast toast" aria-live="polite" aria-atomic="true" data-toast></output>';
|
|
25
|
+
}
|