rcf-lite 0.28.2 → 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 +317 -0
- package/bin/rcf.js +8 -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-4120-canonical-json-hashing.json +30 -0
- package/rcf/adrs/adr-4121-define-sidecars.json +30 -0
- package/rcf/adrs/adr-4122-define-gates-blocking-and-optout.json +30 -0
- package/rcf/adrs/adr-4123-readiness-single-object.json +30 -0
- 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-700.json +23 -0
- package/rcf/code-nodes/cn-701.json +14 -0
- package/rcf/code-nodes/cn-702.json +14 -0
- package/rcf/code-nodes/cn-703.json +16 -0
- package/rcf/code-nodes/cn-720.json +15 -0
- package/rcf/code-nodes/cn-721.json +15 -0
- package/rcf/code-nodes/cn-722.json +15 -0
- package/rcf/code-nodes/cn-723.json +18 -0
- package/rcf/code-nodes/cn-724.json +14 -0
- package/rcf/code-nodes/cn-725.json +37 -0
- package/rcf/code-nodes/cn-726.json +25 -0
- package/rcf/code-nodes/cn-727.json +23 -0
- package/rcf/code-nodes/cn-728.json +37 -0
- package/rcf/code-nodes/cn-729.json +26 -0
- package/rcf/code-nodes/cn-730.json +24 -0
- 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-190.json +37 -0
- package/rcf/fbs/fbs-191.json +34 -0
- package/rcf/fbs/fbs-192.json +17 -0
- package/rcf/fbs/fbs-193.json +37 -0
- package/rcf/fbs/fbs-194.json +35 -0
- package/rcf/fbs/fbs-195.json +37 -0
- package/rcf/fbs/fbs-196.json +45 -0
- package/rcf/fbs/fbs-197.json +41 -0
- 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-001.json +2 -1
- package/rcf/requirements/req-002.json +3 -2
- package/rcf/requirements/req-003.json +2 -1
- package/rcf/requirements/req-004.json +2 -1
- package/rcf/requirements/req-005.json +2 -1
- package/rcf/requirements/req-006.json +2 -1
- package/rcf/requirements/req-007.json +2 -1
- package/rcf/requirements/req-008.json +2 -1
- package/rcf/requirements/req-009.json +2 -1
- package/rcf/requirements/req-010.json +5 -2
- package/rcf/requirements/req-011.json +2 -1
- package/rcf/requirements/req-012.json +2 -1
- package/rcf/requirements/req-013.json +2 -1
- package/rcf/requirements/req-014.json +2 -1
- package/rcf/requirements/req-015.json +2 -1
- package/rcf/requirements/req-016.json +5 -2
- package/rcf/requirements/req-017.json +6 -2
- package/rcf/requirements/req-018.json +5 -2
- package/rcf/requirements/req-019.json +5 -2
- package/rcf/requirements/req-020.json +5 -2
- package/rcf/requirements/req-021.json +5 -2
- package/rcf/requirements/req-022.json +5 -2
- package/rcf/requirements/req-023.json +6 -4
- package/rcf/requirements/req-024.json +5 -2
- package/rcf/requirements/req-025.json +5 -2
- package/rcf/requirements/req-026.json +5 -2
- package/rcf/requirements/req-027.json +5 -2
- package/rcf/requirements/req-028.json +5 -2
- package/rcf/requirements/req-029.json +5 -2
- package/rcf/requirements/req-030.json +5 -2
- package/rcf/requirements/req-040.json +5 -2
- package/rcf/requirements/req-041.json +5 -2
- package/rcf/requirements/req-042.json +5 -2
- package/rcf/requirements/req-043.json +5 -2
- package/rcf/requirements/req-050.json +5 -2
- package/rcf/requirements/req-051.json +5 -2
- package/rcf/requirements/req-052.json +5 -2
- package/rcf/requirements/req-053.json +5 -2
- package/rcf/requirements/req-054.json +5 -2
- package/rcf/requirements/req-060.json +5 -2
- package/rcf/requirements/req-061.json +5 -2
- package/rcf/requirements/req-062.json +5 -2
- package/rcf/requirements/req-063.json +5 -2
- package/rcf/requirements/req-070.json +5 -2
- package/rcf/requirements/req-071.json +5 -2
- package/rcf/requirements/req-072.json +5 -2
- package/rcf/requirements/req-073.json +5 -2
- package/rcf/requirements/req-074.json +5 -2
- package/rcf/requirements/req-075.json +5 -2
- package/rcf/requirements/req-076.json +5 -2
- package/rcf/requirements/req-077.json +5 -2
- package/rcf/requirements/req-080.json +5 -2
- package/rcf/requirements/req-081.json +5 -2
- package/rcf/requirements/req-082.json +5 -2
- package/rcf/requirements/req-083.json +5 -2
- package/rcf/requirements/req-084.json +5 -2
- package/rcf/requirements/req-085.json +5 -2
- package/rcf/requirements/req-086.json +5 -2
- package/rcf/requirements/req-090.json +5 -2
- package/rcf/requirements/req-091.json +5 -2
- package/rcf/requirements/req-092.json +5 -2
- package/rcf/requirements/req-093.json +5 -2
- package/rcf/requirements/req-094.json +5 -2
- package/rcf/requirements/req-100.json +5 -2
- package/rcf/requirements/req-101.json +5 -2
- package/rcf/requirements/req-102.json +5 -2
- package/rcf/requirements/req-103.json +5 -2
- package/rcf/requirements/req-104.json +5 -2
- package/rcf/requirements/req-110.json +5 -2
- package/rcf/requirements/req-111.json +5 -2
- package/rcf/requirements/req-112.json +5 -2
- package/rcf/requirements/req-113.json +5 -2
- package/rcf/requirements/req-114.json +5 -2
- package/rcf/requirements/req-115.json +5 -2
- package/rcf/requirements/req-120.json +5 -2
- package/rcf/requirements/req-121.json +5 -2
- package/rcf/requirements/req-122.json +5 -2
- package/rcf/requirements/req-123.json +5 -2
- package/rcf/requirements/req-124.json +5 -2
- package/rcf/requirements/req-125.json +5 -2
- package/rcf/requirements/req-130.json +6 -2
- package/rcf/requirements/req-131.json +6 -2
- package/rcf/requirements/req-132.json +6 -2
- package/rcf/requirements/req-133.json +6 -2
- package/rcf/requirements/req-134.json +6 -2
- package/rcf/requirements/req-140.json +5 -2
- package/rcf/requirements/req-145.json +5 -2
- package/rcf/requirements/req-150.json +6 -2
- package/rcf/requirements/req-151.json +6 -2
- package/rcf/requirements/req-152.json +6 -2
- package/rcf/requirements/req-153.json +6 -2
- package/rcf/requirements/req-154.json +5 -2
- package/rcf/requirements/req-155.json +4 -1
- package/rcf/requirements/req-156.json +4 -1
- package/rcf/requirements/req-157.json +4 -1
- package/rcf/requirements/req-158.json +4 -1
- package/rcf/requirements/req-159.json +4 -1
- package/rcf/requirements/req-160.json +4 -1
- package/rcf/requirements/req-161.json +4 -1
- package/rcf/requirements/req-162.json +4 -1
- package/rcf/requirements/req-163.json +5 -2
- package/rcf/requirements/req-164.json +5 -2
- package/rcf/requirements/req-165.json +5 -2
- package/rcf/requirements/req-170.json +33 -0
- package/rcf/requirements/req-171.json +44 -0
- package/rcf/requirements/req-172.json +25 -0
- package/rcf/requirements/req-173.json +25 -0
- package/rcf/requirements/req-174.json +31 -0
- package/rcf/requirements/req-175.json +31 -0
- 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-4120-define-delta-and-freeze.json +21 -0
- package/rcf/tacs/tac-4121-define-ledgers.json +35 -0
- package/rcf/tacs/tac-4122-define-gates.json +61 -0
- package/rcf/tacs/tac-4123-define-readiness.json +61 -0
- 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-204.json +30 -0
- package/rcf/test-suites/ts-205.json +22 -0
- package/rcf/test-suites/ts-206.json +22 -0
- package/rcf/test-suites/ts-207.json +22 -0
- package/rcf/test-suites/ts-208.json +22 -0
- package/rcf/test-suites/ts-209.json +22 -0
- package/rcf/test-suites/ts-210.json +22 -0
- package/rcf/test-suites/ts-211.json +22 -0
- package/rcf/test-suites/ts-212.json +22 -0
- package/rcf/test-suites/ts-213.json +50 -0
- package/rcf/test-suites/ts-214.json +74 -0
- package/rcf/test-suites/ts-215.json +211 -0
- package/rcf/test-suites/ts-216.json +94 -0
- package/rcf/test-suites/ts-217.json +62 -0
- 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-15601.json +4 -4
- package/rcf/user-stories/us-17001.json +27 -0
- package/rcf/user-stories/us-17002.json +22 -0
- package/rcf/user-stories/us-17003.json +22 -0
- package/rcf/user-stories/us-17004.json +22 -0
- package/rcf/user-stories/us-17005.json +22 -0
- package/rcf/user-stories/us-17006.json +21 -0
- package/rcf/user-stories/us-17007.json +21 -0
- package/rcf/user-stories/us-17008.json +22 -0
- package/rcf/user-stories/us-17101.json +22 -0
- package/rcf/user-stories/us-17102.json +22 -0
- package/rcf/user-stories/us-17201.json +38 -0
- package/rcf/user-stories/us-17301.json +38 -0
- package/rcf/user-stories/us-17302.json +50 -0
- package/rcf/user-stories/us-17401.json +70 -0
- package/rcf/user-stories/us-17402.json +55 -0
- package/rcf/user-stories/us-17403.json +60 -0
- package/rcf/user-stories/us-17501.json +48 -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 +31 -1
- package/src/cli/feedback.js +126 -10
- package/src/cli/freeze.js +730 -0
- package/src/cli/help.js +23 -0
- package/src/cli/ledger.js +615 -0
- package/src/cli/questions.js +288 -0
- package/src/cli/readiness.js +498 -0
- package/src/cli/view.js +29 -0
- package/src/core/store/writer.js +86 -6
- package/src/define/freeze-record.js +247 -0
- package/src/define/ledgers.js +712 -0
- package/src/feedback/redact.js +105 -19
- 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 +282 -0
- package/src/query/gates.js +1107 -0
- package/src/query/index.js +50 -0
- package/src/query/questions.js +438 -0
- package/src/query/readiness.js +574 -0
- package/src/server/index.js +60 -1
- package/src/server/routes.js +133 -4
- 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/helpers.js +2 -2
- package/src/view/doc-renderers/index.js +2 -0
- package/src/view/doc-renderers/prd.js +35 -6
- package/src/view/doc-renderers/req.js +10 -2
- package/src/view/doc-renderers/user-story.js +10 -5
- package/src/view/html-page.js +717 -300
- package/src/view/index.js +109 -3
- package/src/view/live-client.js +61 -8
- package/src/view/needs-work.js +79 -0
- package/src/view/page-init.js +2193 -0
- package/src/view/product-map.js +751 -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 +2279 -266
- 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,2193 @@
|
|
|
1
|
+
// Page init script. Classic (non-module) browser script extracted from
|
|
2
|
+
// the former inline <script> block in html-page.js: wespa (Dex,
|
|
3
|
+
// 2026-10-02) pins inline scripts by CSP hash, so the viewer now
|
|
4
|
+
// serves this logic as a cacheable asset at ./page-init.js and the
|
|
5
|
+
// shell page carries no inline script at all. PR 1 (viewer UI
|
|
6
|
+
// refresh) also folds in the shell contract:
|
|
7
|
+
// - Theme boot: read ?theme=light|dark|auto, stamp data-theme on
|
|
8
|
+
// <html>. Live changes arrive via postMessage from the embedding
|
|
9
|
+
// host as { type: "rcf-view-theme", theme: "light"|"dark" }; we
|
|
10
|
+
// check event.origin against our own and ignore anything else.
|
|
11
|
+
// The viewport media query handles theme=auto natively; explicit
|
|
12
|
+
// light/dark wins over the media query.
|
|
13
|
+
// - Embed boot: ?embed=1 stamps data-embed on <html>; the stylesheet
|
|
14
|
+
// hides the brand block and footer in that state and keeps the
|
|
15
|
+
// tab nav sticky.
|
|
16
|
+
// - Router: hash scheme #tab=&sub=&entity=&... with bare #REQ-002
|
|
17
|
+
// still working; every hash write preserves the host query string
|
|
18
|
+
// (?embed=1, ?theme=..., ?blueprint=..., ?ticket=...) so embed and
|
|
19
|
+
// theme survive every in-viewer navigation.
|
|
20
|
+
//
|
|
21
|
+
// Idempotent: live-client re-invokes rcfPage.init() after every SSE
|
|
22
|
+
// innerHTML swap of #rcf-live-content. All wiring guards with
|
|
23
|
+
// __rcfTabWired-style sentinels.
|
|
24
|
+
|
|
25
|
+
(function () {
|
|
26
|
+
var TABS = ['readiness', 'overview', 'requirements', 'architecture', 'build', 'product-map'];
|
|
27
|
+
var PM_GROUPS = ['shape', 'component', 'trace', 'capability', 'blueprint'];
|
|
28
|
+
var PM_STATUSES = ['all', 'draft', 'review', 'needsRevision', 'approved', 'superseded'];
|
|
29
|
+
// Partial-render cache: grouping -> HTML string. First fetch fills it,
|
|
30
|
+
// subsequent switches use the cache.
|
|
31
|
+
var pmPartialCache = {};
|
|
32
|
+
function pmCacheKey(group) { return group; }
|
|
33
|
+
|
|
34
|
+
// ---- shell: theme + embed ---------------------------------------------
|
|
35
|
+
|
|
36
|
+
function parseQuery(search) {
|
|
37
|
+
var out = {};
|
|
38
|
+
if (typeof search !== 'string' || search.length === 0) return out;
|
|
39
|
+
var raw = search.charAt(0) === '?' ? search.slice(1) : search;
|
|
40
|
+
if (raw.length === 0) return out;
|
|
41
|
+
var pairs = raw.split('&');
|
|
42
|
+
for (var i = 0; i < pairs.length; i += 1) {
|
|
43
|
+
var eq = pairs[i].indexOf('=');
|
|
44
|
+
var k = eq === -1 ? pairs[i] : pairs[i].slice(0, eq);
|
|
45
|
+
var v = eq === -1 ? '' : pairs[i].slice(eq + 1);
|
|
46
|
+
if (!k) continue;
|
|
47
|
+
try { out[decodeURIComponent(k)] = decodeURIComponent(v.replace(/\+/g, ' ')); }
|
|
48
|
+
catch (e) { out[k] = v; }
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function applyTheme(t) {
|
|
54
|
+
var root = document.documentElement;
|
|
55
|
+
if (t === 'light' || t === 'dark') root.setAttribute('data-theme', t);
|
|
56
|
+
else root.removeAttribute('data-theme');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function applyEmbed(flag) {
|
|
60
|
+
var root = document.documentElement;
|
|
61
|
+
if (flag) root.setAttribute('data-embed', '1');
|
|
62
|
+
else root.removeAttribute('data-embed');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function initShellFromQuery() {
|
|
66
|
+
var q = parseQuery(window.location.search);
|
|
67
|
+
// theme: explicit light|dark wins, auto falls through to the
|
|
68
|
+
// :root:not([data-theme]) + @media prefers-color-scheme block in
|
|
69
|
+
// style.css. Absent stays light (the base :root tokens).
|
|
70
|
+
var theme = q.theme;
|
|
71
|
+
if (theme === 'light' || theme === 'dark' || theme === 'auto') applyTheme(theme === 'auto' ? null : theme);
|
|
72
|
+
// embed: any value other than exactly "1" is ignored.
|
|
73
|
+
applyEmbed(q.embed === '1');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
var messageListenerWired = false;
|
|
77
|
+
function wireThemeMessages() {
|
|
78
|
+
if (messageListenerWired) return;
|
|
79
|
+
messageListenerWired = true;
|
|
80
|
+
window.addEventListener('message', function (ev) {
|
|
81
|
+
// Same-origin only: wespa reverse-proxies the viewer under its
|
|
82
|
+
// own origin, so a legitimate theme ping comes from the viewer's
|
|
83
|
+
// own origin. Anything else is a cross-origin posting we ignore.
|
|
84
|
+
if (!ev || ev.origin !== window.location.origin) return;
|
|
85
|
+
var data = ev.data || {};
|
|
86
|
+
if (data.type !== 'rcf-view-theme') return;
|
|
87
|
+
if (data.theme === 'light' || data.theme === 'dark') applyTheme(data.theme);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---- hash parsing + preservation --------------------------------------
|
|
92
|
+
|
|
93
|
+
function flashLookupTarget(el) {
|
|
94
|
+
if (!el || !el.classList) return;
|
|
95
|
+
try {
|
|
96
|
+
el.classList.remove('is-rcf-lookup-flash');
|
|
97
|
+
// Force reflow so the animation restarts on repeat picks.
|
|
98
|
+
void el.offsetWidth;
|
|
99
|
+
el.classList.add('is-rcf-lookup-flash');
|
|
100
|
+
setTimeout(function () { try { el.classList.remove('is-rcf-lookup-flash'); } catch (e) {} }, 1400);
|
|
101
|
+
} catch (e) {}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseHashParams(raw) {
|
|
105
|
+
var out = {};
|
|
106
|
+
if (!raw) return out;
|
|
107
|
+
var parts = raw.split('&');
|
|
108
|
+
for (var i = 0; i < parts.length; i += 1) {
|
|
109
|
+
var eq = parts[i].indexOf('=');
|
|
110
|
+
if (eq === -1) continue;
|
|
111
|
+
var k = parts[i].slice(0, eq);
|
|
112
|
+
var v = parts[i].slice(eq + 1);
|
|
113
|
+
if (k) out[k] = v;
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Build a URL that keeps the host query untouched (?embed=, ?theme=,
|
|
119
|
+
// ?blueprint=, ?ticket=, ...) and only swaps the hash. Required by
|
|
120
|
+
// Dex's embed contract: embed/theme survive every in-viewer navigation.
|
|
121
|
+
function urlWithHash(hash) {
|
|
122
|
+
var base = window.location.pathname + window.location.search;
|
|
123
|
+
if (!hash) return base;
|
|
124
|
+
return base + (hash.charAt(0) === '#' ? hash : '#' + hash);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function writeHash(hash, replace) {
|
|
128
|
+
if (!window.history) return;
|
|
129
|
+
var fn = replace === false ? 'pushState' : 'replaceState';
|
|
130
|
+
if (typeof window.history[fn] !== 'function') return;
|
|
131
|
+
var next = urlWithHash(hash);
|
|
132
|
+
if (next === window.location.pathname + window.location.search + window.location.hash) return;
|
|
133
|
+
window.history[fn](null, '', next);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---- mermaid lifecycle ------------------------------------------------
|
|
137
|
+
|
|
138
|
+
function initMermaid() {
|
|
139
|
+
if (typeof window.mermaid !== 'undefined') {
|
|
140
|
+
window.mermaid.initialize({ startOnLoad: false, securityLevel: 'loose' });
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function runMermaidIn(container) {
|
|
145
|
+
if (!container || typeof window.mermaid === 'undefined') return;
|
|
146
|
+
var pending = container.querySelectorAll('.mermaid:not([data-processed="true"])');
|
|
147
|
+
if (pending.length === 0) return;
|
|
148
|
+
try {
|
|
149
|
+
window.mermaid.run({ nodes: Array.prototype.slice.call(pending) });
|
|
150
|
+
} catch (e) { /* swallow */ }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---- tabs -------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
function tabButtons() {
|
|
156
|
+
return Array.prototype.slice.call(document.querySelectorAll('nav.tabs [role="tab"]'));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function panelFor(name) {
|
|
160
|
+
return document.getElementById('tab-' + name);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function activateTab(name) {
|
|
164
|
+
if (TABS.indexOf(name) === -1) return false;
|
|
165
|
+
tabButtons().forEach(function (btn) {
|
|
166
|
+
var isTarget = btn.getAttribute('data-tab') === name;
|
|
167
|
+
btn.setAttribute('aria-selected', isTarget ? 'true' : 'false');
|
|
168
|
+
});
|
|
169
|
+
TABS.forEach(function (t) {
|
|
170
|
+
var p = panelFor(t);
|
|
171
|
+
if (!p) return;
|
|
172
|
+
if (t === name) {
|
|
173
|
+
p.removeAttribute('hidden');
|
|
174
|
+
runMermaidIn(p);
|
|
175
|
+
} else {
|
|
176
|
+
p.setAttribute('hidden', '');
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function tabForNode(node) {
|
|
183
|
+
var cur = node;
|
|
184
|
+
while (cur && cur !== document.body) {
|
|
185
|
+
if (cur.getAttribute && cur.getAttribute('role') === 'tabpanel') {
|
|
186
|
+
var id = cur.id || '';
|
|
187
|
+
if (id.indexOf('tab-') === 0) return id.slice(4);
|
|
188
|
+
}
|
|
189
|
+
cur = cur.parentNode;
|
|
190
|
+
}
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function openAncestorDetails(node) {
|
|
195
|
+
var cur = node.parentNode;
|
|
196
|
+
while (cur && cur !== document.body) {
|
|
197
|
+
if (cur.tagName && cur.tagName.toLowerCase() === 'details') {
|
|
198
|
+
cur.open = true;
|
|
199
|
+
}
|
|
200
|
+
cur = cur.parentNode;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function findByDocId(id) {
|
|
205
|
+
if (!id) return null;
|
|
206
|
+
var byId = document.getElementById(id);
|
|
207
|
+
if (byId) return byId;
|
|
208
|
+
return document.querySelector('[data-doc-id="' + id.replace(/"/g, '\\\\"') + '"]');
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ---- product map ------------------------------------------------------
|
|
212
|
+
|
|
213
|
+
function pmGroupPanel(name) {
|
|
214
|
+
return document.getElementById('pm-group-' + name);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function hydrateLazyGroup(name, cb) {
|
|
218
|
+
var panel = pmGroupPanel(name);
|
|
219
|
+
if (!panel) { if (cb) cb(false); return; }
|
|
220
|
+
if (panel.getAttribute('data-pm-lazy') !== 'true' && !pmPartialCache[pmCacheKey(name)]) {
|
|
221
|
+
if (cb) cb(true);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (window.__rcfPmDirty) {
|
|
225
|
+
pmPartialCache = {};
|
|
226
|
+
window.__rcfPmDirty = false;
|
|
227
|
+
}
|
|
228
|
+
var cached = pmPartialCache[pmCacheKey(name)];
|
|
229
|
+
var heading = panel.querySelector('.pm-group-heading');
|
|
230
|
+
var headingHtml = heading ? heading.outerHTML : '';
|
|
231
|
+
if (cached) {
|
|
232
|
+
panel.innerHTML = headingHtml + cached;
|
|
233
|
+
panel.removeAttribute('data-pm-lazy');
|
|
234
|
+
wireProductMapWithin(panel);
|
|
235
|
+
if (cb) cb(true);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
panel.setAttribute('data-pm-loading', 'true');
|
|
239
|
+
var xhr = new XMLHttpRequest();
|
|
240
|
+
// Dex / wespa 2026-10-02: asset references are relative so the
|
|
241
|
+
// reverse-proxy mount point can live anywhere under its origin.
|
|
242
|
+
xhr.open('GET', './product-map/' + name, true);
|
|
243
|
+
xhr.onreadystatechange = function () {
|
|
244
|
+
if (xhr.readyState !== 4) return;
|
|
245
|
+
panel.removeAttribute('data-pm-loading');
|
|
246
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
247
|
+
pmPartialCache[pmCacheKey(name)] = xhr.responseText;
|
|
248
|
+
panel.innerHTML = headingHtml + xhr.responseText;
|
|
249
|
+
panel.removeAttribute('data-pm-lazy');
|
|
250
|
+
wireProductMapWithin(panel);
|
|
251
|
+
if (cb) cb(true);
|
|
252
|
+
} else {
|
|
253
|
+
panel.setAttribute('data-pm-lazy', 'true');
|
|
254
|
+
if (cb) cb(false);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
xhr.send();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function activatePmGroup(name, opts) {
|
|
261
|
+
if (PM_GROUPS.indexOf(name) === -1) return false;
|
|
262
|
+
var buttons = document.querySelectorAll('.pm-group-btn');
|
|
263
|
+
for (var i = 0; i < buttons.length; i += 1) {
|
|
264
|
+
var btn = buttons[i];
|
|
265
|
+
var isTarget = btn.getAttribute('data-pm-group') === name;
|
|
266
|
+
btn.setAttribute('aria-selected', isTarget ? 'true' : 'false');
|
|
267
|
+
}
|
|
268
|
+
var panels = document.querySelectorAll('.pm-group');
|
|
269
|
+
for (var j = 0; j < panels.length; j += 1) {
|
|
270
|
+
var p = panels[j];
|
|
271
|
+
if (p.getAttribute('data-pm-group') === name) {
|
|
272
|
+
p.removeAttribute('hidden');
|
|
273
|
+
} else {
|
|
274
|
+
p.setAttribute('hidden', '');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
var target = pmGroupPanel(name);
|
|
278
|
+
if (target && target.getAttribute('data-pm-lazy') === 'true') {
|
|
279
|
+
hydrateLazyGroup(name, function () {
|
|
280
|
+
if (opts && opts.after) opts.after();
|
|
281
|
+
applyPmStatusFilter(currentStatus());
|
|
282
|
+
});
|
|
283
|
+
} else {
|
|
284
|
+
if (opts && opts.after) opts.after();
|
|
285
|
+
applyPmStatusFilter(currentStatus());
|
|
286
|
+
}
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function currentGroup() {
|
|
291
|
+
var sel = document.querySelector('.pm-group-btn[aria-selected="true"]');
|
|
292
|
+
return sel ? sel.getAttribute('data-pm-group') : 'shape';
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function currentStatus() {
|
|
296
|
+
var s = document.querySelector('.pm-status-select');
|
|
297
|
+
return s ? s.value : 'all';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function currentOpenBuckets() {
|
|
301
|
+
var group = currentGroup();
|
|
302
|
+
var panel = pmGroupPanel(group);
|
|
303
|
+
if (!panel) return [];
|
|
304
|
+
var nodes = panel.querySelectorAll('details.pm-bucket[open]');
|
|
305
|
+
var out = [];
|
|
306
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
307
|
+
var id = nodes[i].getAttribute('data-pm-bucket-id');
|
|
308
|
+
if (id) out.push(id);
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function applyPmStatusFilter(status) {
|
|
314
|
+
if (PM_STATUSES.indexOf(status) === -1) return false;
|
|
315
|
+
var select = document.querySelector('.pm-status-select');
|
|
316
|
+
if (select) select.value = status;
|
|
317
|
+
var reqCards = document.querySelectorAll('#tab-product-map [data-req-status]');
|
|
318
|
+
for (var i = 0; i < reqCards.length; i += 1) {
|
|
319
|
+
var card = reqCards[i];
|
|
320
|
+
var s = card.getAttribute('data-req-status') || '';
|
|
321
|
+
if (status === 'all' || s === status) {
|
|
322
|
+
card.removeAttribute('hidden');
|
|
323
|
+
} else {
|
|
324
|
+
card.setAttribute('hidden', '');
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
var buckets = document.querySelectorAll('#tab-product-map .pm-group:not([hidden]) details.pm-bucket');
|
|
328
|
+
var emptyCount = 0;
|
|
329
|
+
for (var k = 0; k < buckets.length; k += 1) {
|
|
330
|
+
var bk = buckets[k];
|
|
331
|
+
var visible = bk.querySelectorAll('[data-req-status]:not([hidden])');
|
|
332
|
+
var bodyEmpty = bk.querySelector('.pm-bucket-body') && bk.querySelector('.pm-bucket-body').children.length === 0;
|
|
333
|
+
var isEmpty = visible.length === 0 && !bodyEmpty;
|
|
334
|
+
var body = bk.querySelector('.pm-bucket-body');
|
|
335
|
+
var emptyNote = body ? body.querySelector('.pm-bucket-empty-filter') : null;
|
|
336
|
+
if (isEmpty && status !== 'all') {
|
|
337
|
+
if (body && !emptyNote) {
|
|
338
|
+
emptyNote = document.createElement('p');
|
|
339
|
+
emptyNote.className = 'pm-bucket-empty pm-bucket-empty-filter';
|
|
340
|
+
emptyNote.innerHTML = '<em>0 requirements match this filter.</em>';
|
|
341
|
+
body.appendChild(emptyNote);
|
|
342
|
+
}
|
|
343
|
+
bk.setAttribute('data-pm-empty-under-filter', 'true');
|
|
344
|
+
emptyCount += 1;
|
|
345
|
+
} else {
|
|
346
|
+
if (emptyNote) emptyNote.parentNode.removeChild(emptyNote);
|
|
347
|
+
bk.removeAttribute('data-pm-empty-under-filter');
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
var activePanel = document.querySelector('#tab-product-map .pm-group:not([hidden])');
|
|
351
|
+
if (activePanel) {
|
|
352
|
+
var roll = activePanel.querySelector('.pm-empty-roll');
|
|
353
|
+
if (roll) {
|
|
354
|
+
var showBtn = roll.querySelector('.pm-empty-count');
|
|
355
|
+
if (emptyCount > 0 && status !== 'all') {
|
|
356
|
+
if (showBtn) showBtn.textContent = String(emptyCount);
|
|
357
|
+
roll.removeAttribute('hidden');
|
|
358
|
+
activePanel.classList.add('pm-hide-empty');
|
|
359
|
+
} else {
|
|
360
|
+
roll.setAttribute('hidden', '');
|
|
361
|
+
activePanel.classList.remove('pm-hide-empty');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function updatePmHash() {
|
|
369
|
+
if (!window.history || typeof window.history.pushState !== 'function') return;
|
|
370
|
+
var group = currentGroup();
|
|
371
|
+
var status = currentStatus();
|
|
372
|
+
var open = currentOpenBuckets();
|
|
373
|
+
var next = '#tab=product-map&group=' + group + '&status=' + status;
|
|
374
|
+
if (open.length > 0) next += '&open=' + encodeURIComponent(open.join(','));
|
|
375
|
+
if (window.location.hash === next && window.location.search === window.location.search) return;
|
|
376
|
+
// Preserve host query (?embed=, ?theme=, ?blueprint=, ?ticket=).
|
|
377
|
+
window.history.pushState(null, '', urlWithHash(next));
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function openPmBuckets(ids) {
|
|
381
|
+
if (!ids || ids.length === 0) return;
|
|
382
|
+
var group = currentGroup();
|
|
383
|
+
var panel = pmGroupPanel(group);
|
|
384
|
+
if (!panel) return;
|
|
385
|
+
for (var i = 0; i < ids.length; i += 1) {
|
|
386
|
+
var raw = ids[i];
|
|
387
|
+
if (!raw) continue;
|
|
388
|
+
var node = panel.querySelector('details.pm-bucket[data-pm-bucket-id="' + raw.replace(/"/g, '\\"') + '"]');
|
|
389
|
+
if (node) node.open = true;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function wireProductMapWithin(root) {
|
|
394
|
+
root = root || document;
|
|
395
|
+
var buckets = root.querySelectorAll('details.pm-bucket');
|
|
396
|
+
for (var i = 0; i < buckets.length; i += 1) {
|
|
397
|
+
var bk = buckets[i];
|
|
398
|
+
if (bk.__rcfPmBucketWired) continue;
|
|
399
|
+
bk.__rcfPmBucketWired = true;
|
|
400
|
+
bk.addEventListener('toggle', function () {
|
|
401
|
+
if (window.__rcfPmSuppress) return;
|
|
402
|
+
updatePmHash();
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
var chips = root.querySelectorAll('.pm-jump-chip');
|
|
406
|
+
for (var j = 0; j < chips.length; j += 1) {
|
|
407
|
+
var chip = chips[j];
|
|
408
|
+
if (chip.__rcfPmChipWired) continue;
|
|
409
|
+
chip.__rcfPmChipWired = true;
|
|
410
|
+
chip.addEventListener('click', function (ev) {
|
|
411
|
+
var target = ev.currentTarget.getAttribute('data-pm-jump');
|
|
412
|
+
var groupAttr = ev.currentTarget.closest('.pm-jump-nav').getAttribute('data-pm-jump-group');
|
|
413
|
+
var panel = pmGroupPanel(groupAttr);
|
|
414
|
+
if (!panel) return;
|
|
415
|
+
var node = panel.querySelector('details.pm-bucket[data-pm-bucket-id="' + target.replace(/"/g, '\\"') + '"]');
|
|
416
|
+
if (!node) return;
|
|
417
|
+
node.open = true;
|
|
418
|
+
try { node.scrollIntoView({ block: 'start' }); } catch (e) { node.scrollIntoView(); }
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
var bulk = root.querySelectorAll('.pm-bulk-btn');
|
|
422
|
+
for (var b = 0; b < bulk.length; b += 1) {
|
|
423
|
+
var btn = bulk[b];
|
|
424
|
+
if (btn.__rcfPmBulkWired) continue;
|
|
425
|
+
btn.__rcfPmBulkWired = true;
|
|
426
|
+
btn.addEventListener('click', function (ev) {
|
|
427
|
+
var action = ev.currentTarget.getAttribute('data-pm-bulk');
|
|
428
|
+
var panel = document.querySelector('#tab-product-map .pm-group:not([hidden])');
|
|
429
|
+
if (!panel) return;
|
|
430
|
+
var all = panel.querySelectorAll('details.pm-bucket');
|
|
431
|
+
window.__rcfPmSuppress = true;
|
|
432
|
+
for (var i = 0; i < all.length; i += 1) all[i].open = (action === 'expand');
|
|
433
|
+
window.__rcfPmSuppress = false;
|
|
434
|
+
updatePmHash();
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
var rolls = root.querySelectorAll('.pm-empty-toggle');
|
|
438
|
+
for (var r = 0; r < rolls.length; r += 1) {
|
|
439
|
+
var rt = rolls[r];
|
|
440
|
+
if (rt.__rcfPmEmptyWired) continue;
|
|
441
|
+
rt.__rcfPmEmptyWired = true;
|
|
442
|
+
rt.addEventListener('click', function (ev) {
|
|
443
|
+
var panel = ev.currentTarget.closest('.pm-group');
|
|
444
|
+
if (!panel) return;
|
|
445
|
+
if (panel.classList.contains('pm-hide-empty')) {
|
|
446
|
+
panel.classList.remove('pm-hide-empty');
|
|
447
|
+
ev.currentTarget.textContent = 'Hide empty buckets';
|
|
448
|
+
} else {
|
|
449
|
+
panel.classList.add('pm-hide-empty');
|
|
450
|
+
var span = document.createElement('span');
|
|
451
|
+
span.className = 'pm-empty-count';
|
|
452
|
+
span.textContent = String(panel.querySelectorAll('details.pm-bucket[data-pm-empty-under-filter="true"]').length);
|
|
453
|
+
ev.currentTarget.textContent = '';
|
|
454
|
+
ev.currentTarget.appendChild(document.createTextNode('Show '));
|
|
455
|
+
ev.currentTarget.appendChild(span);
|
|
456
|
+
ev.currentTarget.appendChild(document.createTextNode(' empty buckets'));
|
|
457
|
+
}
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function wireProductMap() {
|
|
463
|
+
var buttons = document.querySelectorAll('.pm-group-btn');
|
|
464
|
+
for (var i = 0; i < buttons.length; i += 1) {
|
|
465
|
+
if (buttons[i].__rcfPmWired) continue;
|
|
466
|
+
buttons[i].__rcfPmWired = true;
|
|
467
|
+
buttons[i].addEventListener('click', function (ev) {
|
|
468
|
+
var name = ev.currentTarget.getAttribute('data-pm-group');
|
|
469
|
+
if (activatePmGroup(name)) updatePmHash();
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
var select = document.querySelector('.pm-status-select');
|
|
473
|
+
if (select && !select.__rcfPmWired) {
|
|
474
|
+
select.__rcfPmWired = true;
|
|
475
|
+
select.addEventListener('change', function () {
|
|
476
|
+
applyPmStatusFilter(select.value);
|
|
477
|
+
updatePmHash();
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
wireProductMapWithin(document);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// ---- hash router ------------------------------------------------------
|
|
484
|
+
//
|
|
485
|
+
// PR 1 contract: #tab=<name>&sub=<name>&entity=<id>&... with bare
|
|
486
|
+
// #REQ-002 still resolving. Unknown params survive. Writes go through
|
|
487
|
+
// urlWithHash so the ?embed=/?theme= query is preserved. 'sub=' is
|
|
488
|
+
// owned by the active tab (Build tab in PR 5 for specs|dag); for PR 1
|
|
489
|
+
// we only read the value and leave acting on it to a later tab-side
|
|
490
|
+
// handler.
|
|
491
|
+
|
|
492
|
+
function resolveHash(hash) {
|
|
493
|
+
if (!hash) { activateTab('readiness'); return; }
|
|
494
|
+
var raw = hash.charAt(0) === '#' ? hash.slice(1) : hash;
|
|
495
|
+
// Decision 11 (PR 3): `#entity=<id>` without a tab key still
|
|
496
|
+
// resolves - we find the target, activate its tab and open ancestors.
|
|
497
|
+
if (raw.indexOf('=') !== -1 && raw.indexOf('tab=') === -1) {
|
|
498
|
+
var kv = parseHashParams(raw);
|
|
499
|
+
if (kv.entity) {
|
|
500
|
+
var entTarget = findByDocId(kv.entity);
|
|
501
|
+
if (entTarget) {
|
|
502
|
+
var entTab = tabForNode(entTarget);
|
|
503
|
+
if (entTab) activateTab(entTab);
|
|
504
|
+
openAncestorDetails(entTarget);
|
|
505
|
+
if (entTarget.tagName && entTarget.tagName.toLowerCase() === 'details') entTarget.open = true;
|
|
506
|
+
try { entTarget.scrollIntoView({ block: 'start' }); } catch (e) { entTarget.scrollIntoView(); }
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (raw.indexOf('tab=') === 0 || raw.indexOf('tab=') > 0) {
|
|
512
|
+
// Fast path: tab is the first key.
|
|
513
|
+
var params = parseHashParams(raw);
|
|
514
|
+
var tab = params.tab;
|
|
515
|
+
if (tab && TABS.indexOf(tab) !== -1) {
|
|
516
|
+
activateTab(tab);
|
|
517
|
+
if (tab === 'product-map') {
|
|
518
|
+
var openIds = params.open ? decodeURIComponent(params.open).split(',').filter(Boolean) : [];
|
|
519
|
+
activatePmGroup(params.group || 'shape', {
|
|
520
|
+
after: function () {
|
|
521
|
+
if (params.status) applyPmStatusFilter(params.status);
|
|
522
|
+
openPmBuckets(openIds);
|
|
523
|
+
},
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
// Requirements tab: push the hash filter slots into the
|
|
527
|
+
// FilterBar inputs and re-run the row filter so the first paint
|
|
528
|
+
// reflects the URL exactly. #entity=... below opens the entity.
|
|
529
|
+
if (tab === 'requirements' && requirementsFilterBar()) {
|
|
530
|
+
applyRequirementsHash(params);
|
|
531
|
+
}
|
|
532
|
+
// Architecture tab: push the two FilterBar states + any
|
|
533
|
+
// `open=<tad-section>,<tad-section>...` deep-link so the first
|
|
534
|
+
// paint reflects the URL (PR 4, decisions 3 and 5).
|
|
535
|
+
if (tab === 'architecture' && (archBar('architecture-components') || archBar('architecture-decisions'))) {
|
|
536
|
+
applyArchitectureHash(params);
|
|
537
|
+
}
|
|
538
|
+
// Build tab (PR 5): activate the right sub-tab and push the
|
|
539
|
+
// Specs FilterBar state so the first paint reflects the URL.
|
|
540
|
+
if (tab === 'build') {
|
|
541
|
+
applyBuildHash(params);
|
|
542
|
+
}
|
|
543
|
+
// #tab=requirements&entity=REQ-002 opens the entity in place.
|
|
544
|
+
if (params.entity) {
|
|
545
|
+
var ent = findByDocId(params.entity);
|
|
546
|
+
if (ent) {
|
|
547
|
+
openAncestorDetails(ent);
|
|
548
|
+
if (ent.tagName && ent.tagName.toLowerCase() === 'details') ent.open = true;
|
|
549
|
+
try { ent.scrollIntoView({ block: 'start' }); } catch (e) { ent.scrollIntoView(); }
|
|
550
|
+
flashLookupTarget(ent);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
// Bare hash (#REQ-002 etc) stays supported.
|
|
557
|
+
var target = findByDocId(raw);
|
|
558
|
+
if (!target) { activateTab('readiness'); return; }
|
|
559
|
+
var tabName = tabForNode(target);
|
|
560
|
+
if (tabName) activateTab(tabName);
|
|
561
|
+
openAncestorDetails(target);
|
|
562
|
+
if (target.tagName && target.tagName.toLowerCase() === 'details') target.open = true;
|
|
563
|
+
try { target.scrollIntoView({ block: 'start' }); } catch (e) { target.scrollIntoView(); }
|
|
564
|
+
flashLookupTarget(target);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function onTabClick(ev) {
|
|
568
|
+
var btn = ev.currentTarget;
|
|
569
|
+
var name = btn.getAttribute('data-tab');
|
|
570
|
+
if (!name) return;
|
|
571
|
+
ev.preventDefault && ev.preventDefault();
|
|
572
|
+
activateTab(name);
|
|
573
|
+
if (name === 'product-map') {
|
|
574
|
+
activatePmGroup(currentGroup());
|
|
575
|
+
}
|
|
576
|
+
// The Build tab carries a SubTabStrip + FilterBar state the URL
|
|
577
|
+
// mirrors; re-emit the current fragment so #tab=build / UI stay in
|
|
578
|
+
// step (fixes the DAG-sticky case: previously `#tab=build` was
|
|
579
|
+
// written while DAG remained visible).
|
|
580
|
+
if (name === 'build' && buildSubTabStrip()) {
|
|
581
|
+
writeHash(buildHashFragment(), true);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
// Hash write preserves ?embed=/?theme=/... (Dex contract, PR 1).
|
|
585
|
+
writeHash('#tab=' + name, true);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function wireTabs() {
|
|
589
|
+
tabButtons().forEach(function (btn) {
|
|
590
|
+
if (btn.__rcfTabWired) return;
|
|
591
|
+
btn.__rcfTabWired = true;
|
|
592
|
+
btn.addEventListener('click', onTabClick);
|
|
593
|
+
});
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// ---- shared toast helper (viewer UI refresh PR 2) --------------------
|
|
597
|
+
//
|
|
598
|
+
// The shell renders exactly one <div class="rcf-toast" data-toast> and
|
|
599
|
+
// keeps it outside #rcf-live-content so the SSE innerHTML swap does
|
|
600
|
+
// not blow it away. showToast() is idempotent across repeat calls:
|
|
601
|
+
// the latest message replaces whatever is showing, the fade-out
|
|
602
|
+
// timer resets, and the element clears when the fade completes.
|
|
603
|
+
var toastHideTimer = null;
|
|
604
|
+
function showToast(message, opts) {
|
|
605
|
+
var el = document.querySelector('[data-toast]');
|
|
606
|
+
if (!el) return;
|
|
607
|
+
var text = message == null ? '' : String(message);
|
|
608
|
+
el.textContent = text;
|
|
609
|
+
el.classList.add('show');
|
|
610
|
+
if (toastHideTimer) { clearTimeout(toastHideTimer); toastHideTimer = null; }
|
|
611
|
+
var durationMs = opts && typeof opts.durationMs === 'number' ? opts.durationMs : 2200;
|
|
612
|
+
toastHideTimer = setTimeout(function () {
|
|
613
|
+
el.classList.remove('show');
|
|
614
|
+
// Clear the text one fade-out later so a reader does not re-announce it.
|
|
615
|
+
toastHideTimer = setTimeout(function () {
|
|
616
|
+
if (!el.classList.contains('show')) el.textContent = '';
|
|
617
|
+
}, 200);
|
|
618
|
+
}, durationMs);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// ---- component fixture page wiring (viewer UI refresh PR 2) ----------
|
|
622
|
+
//
|
|
623
|
+
// The /_fixtures/components page carries a theme toggle and a demo
|
|
624
|
+
// button that fires showToast() so a reviewer can confirm the Toast
|
|
625
|
+
// round-trips end-to-end. Both guards against null elements so the
|
|
626
|
+
// helpers no-op on every other page.
|
|
627
|
+
function wireFixturePage() {
|
|
628
|
+
var themeBtns = document.querySelectorAll('[data-rcf-fixture-theme]');
|
|
629
|
+
for (var i = 0; i < themeBtns.length; i += 1) {
|
|
630
|
+
var btn = themeBtns[i];
|
|
631
|
+
if (btn.__rcfFixtureThemeWired) continue;
|
|
632
|
+
btn.__rcfFixtureThemeWired = true;
|
|
633
|
+
btn.addEventListener('click', function (ev) {
|
|
634
|
+
var mode = ev.currentTarget.getAttribute('data-rcf-fixture-theme');
|
|
635
|
+
applyTheme(mode === 'auto' ? null : mode);
|
|
636
|
+
var buttons = document.querySelectorAll('[data-rcf-fixture-theme]');
|
|
637
|
+
for (var j = 0; j < buttons.length; j += 1) {
|
|
638
|
+
var b = buttons[j];
|
|
639
|
+
var isTarget = b.getAttribute('data-rcf-fixture-theme') === mode;
|
|
640
|
+
b.setAttribute('aria-pressed', isTarget ? 'true' : 'false');
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
var toastBtns = document.querySelectorAll('[data-rcf-fixture-toast]');
|
|
645
|
+
for (var k = 0; k < toastBtns.length; k += 1) {
|
|
646
|
+
var tb = toastBtns[k];
|
|
647
|
+
if (tb.__rcfFixtureToastWired) continue;
|
|
648
|
+
tb.__rcfFixtureToastWired = true;
|
|
649
|
+
tb.addEventListener('click', function () {
|
|
650
|
+
showToast('Saved. (demo of the shared Toast helper)');
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// ---- Requirements FilterBar + EntitySelector (viewer UI refresh PR 3) --
|
|
656
|
+
//
|
|
657
|
+
// The Requirements tab mounts a FilterBar (text, area, priority, status,
|
|
658
|
+
// Needs-work) and the PRD tab mounts an EntitySelector (type-ahead jump
|
|
659
|
+
// + area chips). Filter state lives in the hash so a filtered view is a
|
|
660
|
+
// link (decision 3). The hash slots this reads/writes:
|
|
661
|
+
// #tab=requirements[&q=...][&domain=...][&priority=...][&status=...][&needswork=1]
|
|
662
|
+
// Area chips in the PRD selector write #tab=requirements&domain=...;
|
|
663
|
+
// the type-ahead jump writes #tab=requirements&entity=<id>.
|
|
664
|
+
|
|
665
|
+
function requirementsListNode() {
|
|
666
|
+
return document.querySelector('[data-rcf-list="requirements"]');
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
function requirementsFilterBar() {
|
|
670
|
+
return document.querySelector('[data-rcf-filterbar="requirements"]');
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function readRequirementsFilterState() {
|
|
674
|
+
var bar = requirementsFilterBar();
|
|
675
|
+
if (!bar) return null;
|
|
676
|
+
var text = bar.querySelector('.rcf-filter-text');
|
|
677
|
+
var state = { q: text ? text.value.trim() : '', needswork: false };
|
|
678
|
+
var selects = bar.querySelectorAll('.rcf-filter-select');
|
|
679
|
+
for (var i = 0; i < selects.length; i += 1) {
|
|
680
|
+
state[selects[i].getAttribute('data-filter-key') || 'select'] = selects[i].value;
|
|
681
|
+
}
|
|
682
|
+
var toggles = bar.querySelectorAll('.rcf-filter-toggle input[type="checkbox"]');
|
|
683
|
+
for (var j = 0; j < toggles.length; j += 1) {
|
|
684
|
+
state[toggles[j].getAttribute('data-filter-key') || 'toggle'] = toggles[j].checked;
|
|
685
|
+
}
|
|
686
|
+
return state;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function writeRequirementsFilterState(state) {
|
|
690
|
+
var bar = requirementsFilterBar();
|
|
691
|
+
if (!bar) return;
|
|
692
|
+
var text = bar.querySelector('.rcf-filter-text');
|
|
693
|
+
if (text) text.value = state.q || '';
|
|
694
|
+
var selects = bar.querySelectorAll('.rcf-filter-select');
|
|
695
|
+
for (var i = 0; i < selects.length; i += 1) {
|
|
696
|
+
var key = selects[i].getAttribute('data-filter-key') || '';
|
|
697
|
+
selects[i].value = state[key] != null ? String(state[key]) : '';
|
|
698
|
+
}
|
|
699
|
+
var toggles = bar.querySelectorAll('.rcf-filter-toggle input[type="checkbox"]');
|
|
700
|
+
for (var j = 0; j < toggles.length; j += 1) {
|
|
701
|
+
var tkey = toggles[j].getAttribute('data-filter-key') || '';
|
|
702
|
+
toggles[j].checked = state[tkey] === true || state[tkey] === 'true' || state[tkey] === '1';
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function applyRequirementsFilter() {
|
|
707
|
+
var list = requirementsListNode();
|
|
708
|
+
if (!list) return;
|
|
709
|
+
var state = readRequirementsFilterState() || { q: '', domain: '', priority: '', status: '', needswork: false };
|
|
710
|
+
var qLower = (state.q || '').toLowerCase();
|
|
711
|
+
var rows = list.querySelectorAll(':scope > details.rcf-row[data-doc-id^="REQ-"]');
|
|
712
|
+
var visible = 0;
|
|
713
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
714
|
+
var row = rows[i];
|
|
715
|
+
var hide = false;
|
|
716
|
+
if (state.domain && row.getAttribute('data-domain') !== state.domain) hide = true;
|
|
717
|
+
if (!hide && state.priority && row.getAttribute('data-priority') !== state.priority) hide = true;
|
|
718
|
+
if (!hide && state.status && row.getAttribute('data-status') !== state.status) hide = true;
|
|
719
|
+
if (!hide && state.needswork && row.getAttribute('data-needswork') !== '1') hide = true;
|
|
720
|
+
if (!hide && qLower) {
|
|
721
|
+
var hay = row.getAttribute('data-text') || '';
|
|
722
|
+
if (hay.indexOf(qLower) === -1) hide = true;
|
|
723
|
+
}
|
|
724
|
+
if (hide) row.setAttribute('hidden', '');
|
|
725
|
+
else { row.removeAttribute('hidden'); visible += 1; }
|
|
726
|
+
}
|
|
727
|
+
var countEl = document.querySelector('[data-rcf-filterbar="requirements"] .rcf-filter-count');
|
|
728
|
+
if (countEl) countEl.textContent = String(visible) + ' of ' + String(rows.length) + ' visible';
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
function requirementsHashFragment(state) {
|
|
732
|
+
var parts = ['tab=requirements'];
|
|
733
|
+
if (state.q) parts.push('q=' + encodeURIComponent(state.q));
|
|
734
|
+
if (state.domain) parts.push('domain=' + encodeURIComponent(state.domain));
|
|
735
|
+
if (state.priority) parts.push('priority=' + encodeURIComponent(state.priority));
|
|
736
|
+
if (state.status) parts.push('status=' + encodeURIComponent(state.status));
|
|
737
|
+
if (state.needswork) parts.push('needswork=1');
|
|
738
|
+
if (state.entity) parts.push('entity=' + encodeURIComponent(state.entity));
|
|
739
|
+
// Decision 11: preserve unknown hash params through filter writes.
|
|
740
|
+
if (state.extraParts) parts = parts.concat(state.extraParts);
|
|
741
|
+
return '#' + parts.join('&');
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
function currentRequirementsHashExtras() {
|
|
745
|
+
// Read the live hash, keep every param not owned by the Requirements
|
|
746
|
+
// FilterBar so filter writes preserve the selected entity and any
|
|
747
|
+
// other extension (decision 11 preservation contract).
|
|
748
|
+
var raw = window.location.hash || '';
|
|
749
|
+
if (raw[0] === '#') raw = raw.slice(1);
|
|
750
|
+
if (!raw) return { entity: '', extraParts: [] };
|
|
751
|
+
var pairs = raw.split('&');
|
|
752
|
+
var entity = '';
|
|
753
|
+
var extras = [];
|
|
754
|
+
var owned = { tab: true, q: true, domain: true, priority: true, status: true, needswork: true, entity: true };
|
|
755
|
+
for (var i = 0; i < pairs.length; i += 1) {
|
|
756
|
+
var p = pairs[i];
|
|
757
|
+
if (!p) continue;
|
|
758
|
+
var eq = p.indexOf('=');
|
|
759
|
+
var k = eq === -1 ? p : p.slice(0, eq);
|
|
760
|
+
if (k === 'entity' && eq !== -1) { entity = decodeURIComponent(p.slice(eq + 1)); continue; }
|
|
761
|
+
if (owned[k]) continue;
|
|
762
|
+
extras.push(p);
|
|
763
|
+
}
|
|
764
|
+
return { entity: entity, extraParts: extras };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function writeRequirementsHash() {
|
|
768
|
+
var state = readRequirementsFilterState();
|
|
769
|
+
if (!state) return;
|
|
770
|
+
var extras = currentRequirementsHashExtras();
|
|
771
|
+
state.entity = extras.entity;
|
|
772
|
+
state.extraParts = extras.extraParts;
|
|
773
|
+
writeHash(requirementsHashFragment(state), true);
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function applyRequirementsHash(params) {
|
|
777
|
+
var state = {
|
|
778
|
+
q: params.q ? decodeURIComponent(params.q) : '',
|
|
779
|
+
domain: params.domain ? decodeURIComponent(params.domain) : '',
|
|
780
|
+
priority: params.priority ? decodeURIComponent(params.priority) : '',
|
|
781
|
+
status: params.status ? decodeURIComponent(params.status) : '',
|
|
782
|
+
needswork: params.needswork === '1' || params.needswork === 'true',
|
|
783
|
+
};
|
|
784
|
+
writeRequirementsFilterState(state);
|
|
785
|
+
applyRequirementsFilter();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
function expandAllRequirements(open) {
|
|
789
|
+
var list = requirementsListNode();
|
|
790
|
+
if (!list) return;
|
|
791
|
+
var rows = list.querySelectorAll(':scope > details.rcf-row');
|
|
792
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
793
|
+
if (rows[i].hasAttribute('hidden')) continue;
|
|
794
|
+
rows[i].open = !!open;
|
|
795
|
+
}
|
|
796
|
+
var btn = document.querySelector('[data-rcf-filterbar="requirements"] .rcf-filter-expand');
|
|
797
|
+
if (btn) {
|
|
798
|
+
btn.textContent = open ? 'Collapse all' : 'Expand all';
|
|
799
|
+
btn.setAttribute('data-expand-state', open ? 'expanded' : 'collapsed');
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function wireRequirementsFilterBar() {
|
|
804
|
+
var bar = requirementsFilterBar();
|
|
805
|
+
if (!bar || bar.__rcfReqFilterWired) return;
|
|
806
|
+
bar.__rcfReqFilterWired = true;
|
|
807
|
+
var text = bar.querySelector('.rcf-filter-text');
|
|
808
|
+
if (text) {
|
|
809
|
+
var debounceTimer = null;
|
|
810
|
+
text.addEventListener('input', function () {
|
|
811
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
812
|
+
debounceTimer = setTimeout(function () {
|
|
813
|
+
applyRequirementsFilter();
|
|
814
|
+
writeRequirementsHash();
|
|
815
|
+
}, 150);
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
var selects = bar.querySelectorAll('.rcf-filter-select');
|
|
819
|
+
for (var i = 0; i < selects.length; i += 1) {
|
|
820
|
+
selects[i].addEventListener('change', function () {
|
|
821
|
+
applyRequirementsFilter();
|
|
822
|
+
writeRequirementsHash();
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
var toggles = bar.querySelectorAll('.rcf-filter-toggle input[type="checkbox"]');
|
|
826
|
+
for (var j = 0; j < toggles.length; j += 1) {
|
|
827
|
+
toggles[j].addEventListener('change', function () {
|
|
828
|
+
applyRequirementsFilter();
|
|
829
|
+
writeRequirementsHash();
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
var expandBtn = bar.querySelector('.rcf-filter-expand');
|
|
833
|
+
if (expandBtn) {
|
|
834
|
+
expandBtn.addEventListener('click', function (ev) {
|
|
835
|
+
ev.preventDefault && ev.preventDefault();
|
|
836
|
+
var isCollapsed = expandBtn.getAttribute('data-expand-state') !== 'expanded';
|
|
837
|
+
expandAllRequirements(isCollapsed);
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ---- Architecture FilterBars (viewer UI refresh PR 4) ----------------
|
|
843
|
+
//
|
|
844
|
+
// The Architecture tab mounts two FilterBars: Components
|
|
845
|
+
// (architecture-components) and Decisions (architecture-decisions).
|
|
846
|
+
// Each is text + Status facet; state lives in the hash under the
|
|
847
|
+
// owning tab so the view is a link. Hash slots this reads/writes:
|
|
848
|
+
// #tab=architecture[&cq=...][&cstatus=...][&dq=...][&dstatus=...]
|
|
849
|
+
// [&open=components|decisions|<tad-section-key>]
|
|
850
|
+
// The `open=` key is a comma-separated set so a review link can
|
|
851
|
+
// deep-open one TAD section (e.g. open=integrationArchitecture).
|
|
852
|
+
|
|
853
|
+
var ARCH_HASH_KEYS = {
|
|
854
|
+
'architecture-components': { q: 'cq', status: 'cstatus' },
|
|
855
|
+
'architecture-decisions': { q: 'dq', status: 'dstatus' },
|
|
856
|
+
};
|
|
857
|
+
|
|
858
|
+
function archBar(hashKey) {
|
|
859
|
+
return document.querySelector('[data-rcf-filterbar="' + hashKey + '"]');
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
function archList(hashKey) {
|
|
863
|
+
return document.querySelector('[data-rcf-list="' + hashKey + '"]');
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
function readArchitectureFilterState(hashKey) {
|
|
867
|
+
var bar = archBar(hashKey);
|
|
868
|
+
if (!bar) return null;
|
|
869
|
+
var text = bar.querySelector('.rcf-filter-text');
|
|
870
|
+
var state = { q: text ? text.value.trim() : '' };
|
|
871
|
+
var selects = bar.querySelectorAll('.rcf-filter-select');
|
|
872
|
+
for (var i = 0; i < selects.length; i += 1) {
|
|
873
|
+
state[selects[i].getAttribute('data-filter-key') || 'select'] = selects[i].value;
|
|
874
|
+
}
|
|
875
|
+
return state;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function writeArchitectureFilterState(hashKey, state) {
|
|
879
|
+
var bar = archBar(hashKey);
|
|
880
|
+
if (!bar) return;
|
|
881
|
+
var text = bar.querySelector('.rcf-filter-text');
|
|
882
|
+
if (text) text.value = state.q || '';
|
|
883
|
+
var selects = bar.querySelectorAll('.rcf-filter-select');
|
|
884
|
+
for (var i = 0; i < selects.length; i += 1) {
|
|
885
|
+
var key = selects[i].getAttribute('data-filter-key') || '';
|
|
886
|
+
selects[i].value = state[key] != null ? String(state[key]) : '';
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
function applyArchitectureFilter(hashKey) {
|
|
891
|
+
var list = archList(hashKey);
|
|
892
|
+
if (!list) return;
|
|
893
|
+
var state = readArchitectureFilterState(hashKey) || { q: '', status: '' };
|
|
894
|
+
var qLower = (state.q || '').toLowerCase();
|
|
895
|
+
var rows = list.querySelectorAll(':scope > details.rcf-row');
|
|
896
|
+
var visible = 0;
|
|
897
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
898
|
+
var row = rows[i];
|
|
899
|
+
var hide = false;
|
|
900
|
+
if (state.status && row.getAttribute('data-status') !== state.status) hide = true;
|
|
901
|
+
if (!hide && qLower) {
|
|
902
|
+
var hay = row.getAttribute('data-text') || '';
|
|
903
|
+
if (hay.indexOf(qLower) === -1) hide = true;
|
|
904
|
+
}
|
|
905
|
+
if (hide) row.setAttribute('hidden', '');
|
|
906
|
+
else { row.removeAttribute('hidden'); visible += 1; }
|
|
907
|
+
}
|
|
908
|
+
var countEl = document.querySelector('[data-rcf-filterbar="' + hashKey + '"] .rcf-filter-count');
|
|
909
|
+
if (countEl) countEl.textContent = String(visible) + ' of ' + String(rows.length) + ' visible';
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
function architectureHashFragment() {
|
|
913
|
+
var parts = ['tab=architecture'];
|
|
914
|
+
for (var hk in ARCH_HASH_KEYS) {
|
|
915
|
+
if (!Object.prototype.hasOwnProperty.call(ARCH_HASH_KEYS, hk)) continue;
|
|
916
|
+
var state = readArchitectureFilterState(hk);
|
|
917
|
+
var map = ARCH_HASH_KEYS[hk];
|
|
918
|
+
if (!state) continue;
|
|
919
|
+
if (state.q) parts.push(map.q + '=' + encodeURIComponent(state.q));
|
|
920
|
+
if (state.status) parts.push(map.status + '=' + encodeURIComponent(state.status));
|
|
921
|
+
}
|
|
922
|
+
return '#' + parts.join('&');
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function writeArchitectureHash() {
|
|
926
|
+
writeHash(architectureHashFragment(), true);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
function applyArchitectureHash(params) {
|
|
930
|
+
var statesByBar = {
|
|
931
|
+
'architecture-components': {
|
|
932
|
+
q: params.cq ? decodeURIComponent(params.cq) : '',
|
|
933
|
+
status: params.cstatus ? decodeURIComponent(params.cstatus) : '',
|
|
934
|
+
},
|
|
935
|
+
'architecture-decisions': {
|
|
936
|
+
q: params.dq ? decodeURIComponent(params.dq) : '',
|
|
937
|
+
status: params.dstatus ? decodeURIComponent(params.dstatus) : '',
|
|
938
|
+
},
|
|
939
|
+
};
|
|
940
|
+
for (var hk in statesByBar) {
|
|
941
|
+
if (!Object.prototype.hasOwnProperty.call(statesByBar, hk)) continue;
|
|
942
|
+
writeArchitectureFilterState(hk, statesByBar[hk]);
|
|
943
|
+
applyArchitectureFilter(hk);
|
|
944
|
+
}
|
|
945
|
+
if (params.open) {
|
|
946
|
+
var openSet = decodeURIComponent(params.open).split(',').filter(Boolean);
|
|
947
|
+
openArchitectureSections(openSet);
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function openArchitectureSections(keys) {
|
|
952
|
+
for (var i = 0; i < keys.length; i += 1) {
|
|
953
|
+
var el = document.querySelector('[data-rcf-tad-section="' + keys[i] + '"]');
|
|
954
|
+
if (el && el.tagName && el.tagName.toLowerCase() === 'details') el.open = true;
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function expandAllArchitecture(hashKey, open) {
|
|
959
|
+
var list = archList(hashKey);
|
|
960
|
+
if (!list) return;
|
|
961
|
+
var rows = list.querySelectorAll(':scope > details.rcf-row');
|
|
962
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
963
|
+
if (rows[i].hasAttribute('hidden')) continue;
|
|
964
|
+
rows[i].open = !!open;
|
|
965
|
+
}
|
|
966
|
+
var btn = document.querySelector('[data-rcf-filterbar="' + hashKey + '"] .rcf-filter-expand');
|
|
967
|
+
if (btn) {
|
|
968
|
+
btn.textContent = open ? 'Collapse all' : 'Expand all';
|
|
969
|
+
btn.setAttribute('data-expand-state', open ? 'expanded' : 'collapsed');
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function wireArchitectureFilterBar(hashKey) {
|
|
974
|
+
var bar = archBar(hashKey);
|
|
975
|
+
if (!bar || bar.__rcfArchFilterWired) return;
|
|
976
|
+
bar.__rcfArchFilterWired = true;
|
|
977
|
+
var text = bar.querySelector('.rcf-filter-text');
|
|
978
|
+
if (text) {
|
|
979
|
+
var debounceTimer = null;
|
|
980
|
+
text.addEventListener('input', function () {
|
|
981
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
982
|
+
debounceTimer = setTimeout(function () {
|
|
983
|
+
applyArchitectureFilter(hashKey);
|
|
984
|
+
writeArchitectureHash();
|
|
985
|
+
}, 150);
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
var selects = bar.querySelectorAll('.rcf-filter-select');
|
|
989
|
+
for (var i = 0; i < selects.length; i += 1) {
|
|
990
|
+
selects[i].addEventListener('change', function () {
|
|
991
|
+
applyArchitectureFilter(hashKey);
|
|
992
|
+
writeArchitectureHash();
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
var expandBtn = bar.querySelector('.rcf-filter-expand');
|
|
996
|
+
if (expandBtn) {
|
|
997
|
+
expandBtn.addEventListener('click', function (ev) {
|
|
998
|
+
ev.preventDefault && ev.preventDefault();
|
|
999
|
+
var isCollapsed = expandBtn.getAttribute('data-expand-state') !== 'expanded';
|
|
1000
|
+
expandAllArchitecture(hashKey, isCollapsed);
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
function wireArchitectureFilterBars() {
|
|
1006
|
+
wireArchitectureFilterBar('architecture-components');
|
|
1007
|
+
wireArchitectureFilterBar('architecture-decisions');
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// ---- Build tab (viewer UI refresh PR 5) ------------------------------
|
|
1011
|
+
//
|
|
1012
|
+
// The Build tab carries a SubTabStrip (Specs | DAG; DAG is a PR 6
|
|
1013
|
+
// placeholder) and the Specs sub-tab mounts a FilterBar
|
|
1014
|
+
// (text, status, area, size, Buildable-now). Filter + sub-tab state
|
|
1015
|
+
// live in the hash so a filtered view is a link. Hash slots read /
|
|
1016
|
+
// written here:
|
|
1017
|
+
// #tab=build[&sub=specs|dag][&q=...][&status=...][&domain=...][&size=...][&buildable=1]
|
|
1018
|
+
|
|
1019
|
+
var BUILD_SUB = ['specs', 'dag'];
|
|
1020
|
+
|
|
1021
|
+
function buildBar() {
|
|
1022
|
+
return document.querySelector('[data-rcf-filterbar="build-specs"]');
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function buildListNode() {
|
|
1026
|
+
return document.querySelector('[data-rcf-list="build-specs"]');
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function buildSubTabStrip() {
|
|
1030
|
+
return document.querySelector('[data-rcf-subtabstrip="build"]');
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
function buildSubPanel(sub) {
|
|
1034
|
+
return document.querySelector('[data-rcf-subpanel="' + sub + '"]');
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function activateBuildSub(sub) {
|
|
1038
|
+
if (BUILD_SUB.indexOf(sub) === -1) sub = 'specs';
|
|
1039
|
+
var strip = buildSubTabStrip();
|
|
1040
|
+
if (strip) {
|
|
1041
|
+
var btns = strip.querySelectorAll('[role="tab"]');
|
|
1042
|
+
for (var i = 0; i < btns.length; i += 1) {
|
|
1043
|
+
var isTarget = btns[i].getAttribute('data-sub') === sub;
|
|
1044
|
+
btns[i].setAttribute('aria-selected', isTarget ? 'true' : 'false');
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
for (var j = 0; j < BUILD_SUB.length; j += 1) {
|
|
1048
|
+
var p = buildSubPanel(BUILD_SUB[j]);
|
|
1049
|
+
if (!p) continue;
|
|
1050
|
+
if (BUILD_SUB[j] === sub) p.removeAttribute('hidden');
|
|
1051
|
+
else p.setAttribute('hidden', '');
|
|
1052
|
+
}
|
|
1053
|
+
return sub;
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
function currentBuildSub() {
|
|
1057
|
+
var strip = buildSubTabStrip();
|
|
1058
|
+
if (!strip) return 'specs';
|
|
1059
|
+
var active = strip.querySelector('[role="tab"][aria-selected="true"]');
|
|
1060
|
+
return (active && active.getAttribute('data-sub')) || 'specs';
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
function readBuildFilterState() {
|
|
1064
|
+
var bar = buildBar();
|
|
1065
|
+
if (!bar) return null;
|
|
1066
|
+
var text = bar.querySelector('.rcf-filter-text');
|
|
1067
|
+
var state = { q: text ? text.value.trim() : '', buildable: false };
|
|
1068
|
+
var selects = bar.querySelectorAll('.rcf-filter-select');
|
|
1069
|
+
for (var i = 0; i < selects.length; i += 1) {
|
|
1070
|
+
state[selects[i].getAttribute('data-filter-key') || 'select'] = selects[i].value;
|
|
1071
|
+
}
|
|
1072
|
+
var toggles = bar.querySelectorAll('.rcf-filter-toggle input[type="checkbox"]');
|
|
1073
|
+
for (var j = 0; j < toggles.length; j += 1) {
|
|
1074
|
+
state[toggles[j].getAttribute('data-filter-key') || 'toggle'] = toggles[j].checked;
|
|
1075
|
+
}
|
|
1076
|
+
return state;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function writeBuildFilterState(state) {
|
|
1080
|
+
var bar = buildBar();
|
|
1081
|
+
if (!bar) return;
|
|
1082
|
+
var text = bar.querySelector('.rcf-filter-text');
|
|
1083
|
+
if (text) text.value = state.q || '';
|
|
1084
|
+
var selects = bar.querySelectorAll('.rcf-filter-select');
|
|
1085
|
+
for (var i = 0; i < selects.length; i += 1) {
|
|
1086
|
+
var key = selects[i].getAttribute('data-filter-key') || '';
|
|
1087
|
+
selects[i].value = state[key] != null ? String(state[key]) : '';
|
|
1088
|
+
}
|
|
1089
|
+
var toggles = bar.querySelectorAll('.rcf-filter-toggle input[type="checkbox"]');
|
|
1090
|
+
for (var j = 0; j < toggles.length; j += 1) {
|
|
1091
|
+
var tkey = toggles[j].getAttribute('data-filter-key') || '';
|
|
1092
|
+
toggles[j].checked = state[tkey] === true || state[tkey] === 'true' || state[tkey] === '1';
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
function applyBuildFilter() {
|
|
1097
|
+
var list = buildListNode();
|
|
1098
|
+
if (!list) return;
|
|
1099
|
+
var state = readBuildFilterState() || { q: '', status: '', domain: '', size: '', buildable: false };
|
|
1100
|
+
var qLower = (state.q || '').toLowerCase();
|
|
1101
|
+
var rows = list.querySelectorAll(':scope > details.rcf-row');
|
|
1102
|
+
var visible = 0;
|
|
1103
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
1104
|
+
var row = rows[i];
|
|
1105
|
+
var hide = false;
|
|
1106
|
+
if (state.status && row.getAttribute('data-status') !== state.status) hide = true;
|
|
1107
|
+
if (!hide && state.domain && row.getAttribute('data-domain') !== state.domain) hide = true;
|
|
1108
|
+
if (!hide && state.size && row.getAttribute('data-size') !== state.size) hide = true;
|
|
1109
|
+
if (!hide && state.buildable && row.getAttribute('data-buildable') !== '1') hide = true;
|
|
1110
|
+
if (!hide && qLower) {
|
|
1111
|
+
var hay = row.getAttribute('data-text') || '';
|
|
1112
|
+
if (hay.indexOf(qLower) === -1) hide = true;
|
|
1113
|
+
}
|
|
1114
|
+
if (hide) row.setAttribute('hidden', '');
|
|
1115
|
+
else { row.removeAttribute('hidden'); visible += 1; }
|
|
1116
|
+
}
|
|
1117
|
+
var countEl = document.querySelector('[data-rcf-filterbar="build-specs"] .rcf-filter-count');
|
|
1118
|
+
if (countEl) countEl.textContent = String(visible) + ' of ' + String(rows.length) + ' visible';
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
function currentBuildHashExtras() {
|
|
1122
|
+
// Read the live hash, keep every param not owned by the Build
|
|
1123
|
+
// FilterBar + SubTabStrip so sub-tab / filter writes preserve the
|
|
1124
|
+
// selected entity and any other extension (decision 11 preservation
|
|
1125
|
+
// contract; same shape as currentRequirementsHashExtras).
|
|
1126
|
+
var raw = window.location.hash || '';
|
|
1127
|
+
if (raw[0] === '#') raw = raw.slice(1);
|
|
1128
|
+
if (!raw) return { entity: '', extraParts: [] };
|
|
1129
|
+
var pairs = raw.split('&');
|
|
1130
|
+
var entity = '';
|
|
1131
|
+
var extras = [];
|
|
1132
|
+
var owned = { tab: true, sub: true, q: true, status: true, domain: true, size: true, buildable: true, entity: true };
|
|
1133
|
+
for (var i = 0; i < pairs.length; i += 1) {
|
|
1134
|
+
var p = pairs[i];
|
|
1135
|
+
if (!p) continue;
|
|
1136
|
+
var eq = p.indexOf('=');
|
|
1137
|
+
var k = eq === -1 ? p : p.slice(0, eq);
|
|
1138
|
+
if (k === 'entity' && eq !== -1) { entity = decodeURIComponent(p.slice(eq + 1)); continue; }
|
|
1139
|
+
if (owned[k]) continue;
|
|
1140
|
+
extras.push(p);
|
|
1141
|
+
}
|
|
1142
|
+
return { entity: entity, extraParts: extras };
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
function currentDagHashExtras() {
|
|
1146
|
+
// On the DAG sub-tab, the Specs filter params (q/status/domain/size/
|
|
1147
|
+
// buildable) are UNKNOWN - the DAG does not own them. Preserve them
|
|
1148
|
+
// through DAG writes per the decision 11 preservation contract so
|
|
1149
|
+
// deep links like `#tab=build&sub=dag&entity=FBS-012&status=notStarted`
|
|
1150
|
+
// survive filter and selection changes.
|
|
1151
|
+
var raw = window.location.hash || '';
|
|
1152
|
+
if (raw[0] === '#') raw = raw.slice(1);
|
|
1153
|
+
if (!raw) return [];
|
|
1154
|
+
var pairs = raw.split('&');
|
|
1155
|
+
var extras = [];
|
|
1156
|
+
var owned = { tab: true, sub: true, entity: true };
|
|
1157
|
+
for (var i = 0; i < pairs.length; i += 1) {
|
|
1158
|
+
var p = pairs[i];
|
|
1159
|
+
if (!p) continue;
|
|
1160
|
+
var eq = p.indexOf('=');
|
|
1161
|
+
var k = eq === -1 ? p : p.slice(0, eq);
|
|
1162
|
+
if (owned[k]) continue;
|
|
1163
|
+
extras.push(p);
|
|
1164
|
+
}
|
|
1165
|
+
return extras;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function buildHashFragment() {
|
|
1169
|
+
var parts = ['tab=build'];
|
|
1170
|
+
var sub = currentBuildSub();
|
|
1171
|
+
if (sub && sub !== 'specs') parts.push('sub=' + encodeURIComponent(sub));
|
|
1172
|
+
if (sub === 'dag') {
|
|
1173
|
+
var canvas = dagCanvas();
|
|
1174
|
+
var sel = canvas && canvas.getAttribute('data-rcf-dag-sel');
|
|
1175
|
+
if (sel) parts.push('entity=' + encodeURIComponent(sel));
|
|
1176
|
+
// Preserve any non-DAG-owned params (Specs filter keys etc.)
|
|
1177
|
+
// through DAG writes. Entity is already written above from the
|
|
1178
|
+
// live selection; do NOT re-append extras.entity.
|
|
1179
|
+
var dagExtras = currentDagHashExtras();
|
|
1180
|
+
if (dagExtras.length > 0) parts = parts.concat(dagExtras);
|
|
1181
|
+
return '#' + parts.join('&');
|
|
1182
|
+
}
|
|
1183
|
+
var state = readBuildFilterState();
|
|
1184
|
+
if (state) {
|
|
1185
|
+
if (state.q) parts.push('q=' + encodeURIComponent(state.q));
|
|
1186
|
+
if (state.status) parts.push('status=' + encodeURIComponent(state.status));
|
|
1187
|
+
if (state.domain) parts.push('domain=' + encodeURIComponent(state.domain));
|
|
1188
|
+
if (state.size) parts.push('size=' + encodeURIComponent(state.size));
|
|
1189
|
+
if (state.buildable) parts.push('buildable=1');
|
|
1190
|
+
}
|
|
1191
|
+
var extras = currentBuildHashExtras();
|
|
1192
|
+
if (extras.entity) parts.push('entity=' + encodeURIComponent(extras.entity));
|
|
1193
|
+
// Decision 11: preserve unknown hash params through filter writes.
|
|
1194
|
+
if (extras.extraParts && extras.extraParts.length > 0) parts = parts.concat(extras.extraParts);
|
|
1195
|
+
return '#' + parts.join('&');
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function writeBuildHash() {
|
|
1199
|
+
writeHash(buildHashFragment(), true);
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function applyBuildHash(params) {
|
|
1203
|
+
var sub = params.sub ? decodeURIComponent(params.sub) : 'specs';
|
|
1204
|
+
activateBuildSub(sub);
|
|
1205
|
+
if (sub === 'dag') {
|
|
1206
|
+
var canvas = dagCanvas();
|
|
1207
|
+
var entity = params.entity ? decodeURIComponent(params.entity) : '';
|
|
1208
|
+
if (canvas) {
|
|
1209
|
+
if (entity) {
|
|
1210
|
+
canvas.setAttribute('data-rcf-dag-sel', entity);
|
|
1211
|
+
// AC-204-4: a deep link to an unconnected FBS must land visibly.
|
|
1212
|
+
// Auto-press Show-unconnected so the lane renders and the node
|
|
1213
|
+
// is not display:none when applyDagFilters runs below.
|
|
1214
|
+
var payload = parseDagInspectorPayload();
|
|
1215
|
+
if (payload[entity] && payload[entity].unconnected) {
|
|
1216
|
+
var tunc = document.querySelector('[data-rcf-dag-toggle="unconnected"]');
|
|
1217
|
+
if (tunc && tunc.getAttribute('aria-pressed') !== 'true') tunc.setAttribute('aria-pressed', 'true');
|
|
1218
|
+
}
|
|
1219
|
+
} else {
|
|
1220
|
+
canvas.removeAttribute('data-rcf-dag-sel');
|
|
1221
|
+
}
|
|
1222
|
+
applyDagFilters();
|
|
1223
|
+
renderDagInspector();
|
|
1224
|
+
if (entity) {
|
|
1225
|
+
var el = canvas.querySelector('.rcf-dag-node[data-fbs-id="' + entity + '"]');
|
|
1226
|
+
if (el) { try { el.scrollIntoView({ block: 'center', inline: 'center' }); } catch (e) { el.scrollIntoView(); } }
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
var state = {
|
|
1232
|
+
q: params.q ? decodeURIComponent(params.q) : '',
|
|
1233
|
+
status: params.status ? decodeURIComponent(params.status) : '',
|
|
1234
|
+
domain: params.domain ? decodeURIComponent(params.domain) : '',
|
|
1235
|
+
size: params.size ? decodeURIComponent(params.size) : '',
|
|
1236
|
+
buildable: params.buildable === '1' || params.buildable === 'true',
|
|
1237
|
+
};
|
|
1238
|
+
if (buildBar()) {
|
|
1239
|
+
writeBuildFilterState(state);
|
|
1240
|
+
applyBuildFilter();
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function expandAllBuildSpecs(open) {
|
|
1245
|
+
var list = buildListNode();
|
|
1246
|
+
if (!list) return;
|
|
1247
|
+
var rows = list.querySelectorAll(':scope > details.rcf-row');
|
|
1248
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
1249
|
+
if (rows[i].hasAttribute('hidden')) continue;
|
|
1250
|
+
rows[i].open = !!open;
|
|
1251
|
+
}
|
|
1252
|
+
var btn = document.querySelector('[data-rcf-filterbar="build-specs"] .rcf-filter-expand');
|
|
1253
|
+
if (btn) {
|
|
1254
|
+
btn.textContent = open ? 'Collapse all' : 'Expand all';
|
|
1255
|
+
btn.setAttribute('data-expand-state', open ? 'expanded' : 'collapsed');
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function wireBuildFilterBar() {
|
|
1260
|
+
var bar = buildBar();
|
|
1261
|
+
if (!bar || bar.__rcfBuildFilterWired) return;
|
|
1262
|
+
bar.__rcfBuildFilterWired = true;
|
|
1263
|
+
var text = bar.querySelector('.rcf-filter-text');
|
|
1264
|
+
if (text) {
|
|
1265
|
+
var debounceTimer = null;
|
|
1266
|
+
text.addEventListener('input', function () {
|
|
1267
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
1268
|
+
debounceTimer = setTimeout(function () {
|
|
1269
|
+
applyBuildFilter();
|
|
1270
|
+
writeBuildHash();
|
|
1271
|
+
}, 150);
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
var selects = bar.querySelectorAll('.rcf-filter-select');
|
|
1275
|
+
for (var i = 0; i < selects.length; i += 1) {
|
|
1276
|
+
selects[i].addEventListener('change', function () {
|
|
1277
|
+
applyBuildFilter();
|
|
1278
|
+
writeBuildHash();
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
var toggles = bar.querySelectorAll('.rcf-filter-toggle input[type="checkbox"]');
|
|
1282
|
+
for (var j = 0; j < toggles.length; j += 1) {
|
|
1283
|
+
toggles[j].addEventListener('change', function () {
|
|
1284
|
+
applyBuildFilter();
|
|
1285
|
+
writeBuildHash();
|
|
1286
|
+
});
|
|
1287
|
+
}
|
|
1288
|
+
var expandBtn = bar.querySelector('.rcf-filter-expand');
|
|
1289
|
+
if (expandBtn) {
|
|
1290
|
+
expandBtn.addEventListener('click', function (ev) {
|
|
1291
|
+
ev.preventDefault && ev.preventDefault();
|
|
1292
|
+
var isCollapsed = expandBtn.getAttribute('data-expand-state') !== 'expanded';
|
|
1293
|
+
expandAllBuildSpecs(isCollapsed);
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function wireBuildSubTabStrip() {
|
|
1299
|
+
var strip = buildSubTabStrip();
|
|
1300
|
+
if (!strip || strip.__rcfBuildSubWired) return;
|
|
1301
|
+
strip.__rcfBuildSubWired = true;
|
|
1302
|
+
var btns = strip.querySelectorAll('[role="tab"]');
|
|
1303
|
+
for (var i = 0; i < btns.length; i += 1) {
|
|
1304
|
+
btns[i].addEventListener('click', function (ev) {
|
|
1305
|
+
ev.preventDefault && ev.preventDefault();
|
|
1306
|
+
var sub = ev.currentTarget.getAttribute('data-sub');
|
|
1307
|
+
if (!sub) return;
|
|
1308
|
+
activateBuildSub(sub);
|
|
1309
|
+
if (sub === 'dag' && dagCanvas()) {
|
|
1310
|
+
applyDagFilters();
|
|
1311
|
+
renderDagInspector();
|
|
1312
|
+
}
|
|
1313
|
+
writeBuildHash();
|
|
1314
|
+
});
|
|
1315
|
+
}
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
function wireBuildTab() {
|
|
1319
|
+
wireBuildSubTabStrip();
|
|
1320
|
+
wireBuildFilterBar();
|
|
1321
|
+
wireBuildDag();
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
// ---- Build / DAG (viewer UI refresh PR 6, TAC-4131, ADR-4133) --------
|
|
1325
|
+
//
|
|
1326
|
+
// Client-side wiring for the Build/DAG sub-tab. The server emits the
|
|
1327
|
+
// layout (toolbar, canvas with HTML nodes + SVG edges, inspector shell,
|
|
1328
|
+
// unconnected lane) plus an inline <script type="application/json"
|
|
1329
|
+
// data-rcf-dag-inspector-data> payload keyed by fbsId. The client:
|
|
1330
|
+
// - toggles filters (status chips, area, Buildable-now, Critical-path,
|
|
1331
|
+
// Show-unconnected) by setting data attributes on nodes and edges;
|
|
1332
|
+
// - handles click-to-highlight: selects a node, writes
|
|
1333
|
+
// `#tab=build&sub=dag&entity=<id>` through writeHash, outlines its
|
|
1334
|
+
// upstream closure in link colour and downstream in warning;
|
|
1335
|
+
// - fills the inspector from the inline payload (no outbound fetch,
|
|
1336
|
+
// no postMessage), with Open-in-Specs linking to
|
|
1337
|
+
// `#tab=build&sub=specs&entity=<id>` (REQ-002 / US-204 AC-204-7).
|
|
1338
|
+
|
|
1339
|
+
function dagRoot() { return document.querySelector('[data-rcf-dag="build"]'); }
|
|
1340
|
+
function dagCanvas() { return document.querySelector('[data-rcf-dag-canvas]'); }
|
|
1341
|
+
function dagToolbar() { return document.querySelector('[data-rcf-dag-toolbar]'); }
|
|
1342
|
+
function dagInspectorBody() { return document.querySelector('[data-rcf-dag-inspector-body]'); }
|
|
1343
|
+
|
|
1344
|
+
function parseDagInspectorPayload() {
|
|
1345
|
+
// PR 1 contract (standing criterion): payload rides on the inspector
|
|
1346
|
+
// shell as a `data-rcf-dag-inspector-data` attribute, not an inline
|
|
1347
|
+
// `<script>` body.
|
|
1348
|
+
var shell = document.querySelector('[data-rcf-dag-inspector-body]');
|
|
1349
|
+
if (!shell) return {};
|
|
1350
|
+
var raw = shell.getAttribute('data-rcf-dag-inspector-data');
|
|
1351
|
+
if (!raw) return {};
|
|
1352
|
+
try { return JSON.parse(raw); }
|
|
1353
|
+
catch (e) { return {}; }
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
function dagEscape(s) {
|
|
1357
|
+
return String(s == null ? '' : s)
|
|
1358
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
1359
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
function readDagState() {
|
|
1363
|
+
var state = {
|
|
1364
|
+
statuses: { notStarted: true, inProgress: true, complete: true, verified: true },
|
|
1365
|
+
domain: '',
|
|
1366
|
+
buildable: false,
|
|
1367
|
+
critical: false,
|
|
1368
|
+
unconnected: false,
|
|
1369
|
+
sel: null,
|
|
1370
|
+
};
|
|
1371
|
+
var bar = dagToolbar();
|
|
1372
|
+
if (bar) {
|
|
1373
|
+
var statusChips = bar.querySelectorAll('[data-rcf-dag-status]');
|
|
1374
|
+
for (var i = 0; i < statusChips.length; i += 1) {
|
|
1375
|
+
var k = statusChips[i].getAttribute('data-rcf-dag-status');
|
|
1376
|
+
state.statuses[k] = statusChips[i].getAttribute('aria-pressed') === 'true';
|
|
1377
|
+
}
|
|
1378
|
+
var dom = bar.querySelector('[data-rcf-dag-domain]');
|
|
1379
|
+
if (dom) state.domain = dom.value || '';
|
|
1380
|
+
var tbuild = bar.querySelector('[data-rcf-dag-toggle="buildable"]');
|
|
1381
|
+
if (tbuild) state.buildable = tbuild.getAttribute('aria-pressed') === 'true';
|
|
1382
|
+
var tcrit = bar.querySelector('[data-rcf-dag-toggle="critical"]');
|
|
1383
|
+
if (tcrit) state.critical = tcrit.getAttribute('aria-pressed') === 'true';
|
|
1384
|
+
var tunc = bar.querySelector('[data-rcf-dag-toggle="unconnected"]');
|
|
1385
|
+
if (tunc) state.unconnected = tunc.getAttribute('aria-pressed') === 'true';
|
|
1386
|
+
}
|
|
1387
|
+
var canvas = dagCanvas();
|
|
1388
|
+
if (canvas) state.sel = canvas.getAttribute('data-rcf-dag-sel') || null;
|
|
1389
|
+
return state;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
function dagClosure(payload, startId, dir) {
|
|
1393
|
+
var out = {};
|
|
1394
|
+
if (!payload[startId]) return out;
|
|
1395
|
+
var stack = (dir === 'up' ? payload[startId].needs : payload[startId].waits).slice();
|
|
1396
|
+
while (stack.length > 0) {
|
|
1397
|
+
var cur = stack.pop();
|
|
1398
|
+
if (out[cur]) continue;
|
|
1399
|
+
out[cur] = true;
|
|
1400
|
+
var p = payload[cur];
|
|
1401
|
+
if (!p) continue;
|
|
1402
|
+
var next = dir === 'up' ? p.needs : p.waits;
|
|
1403
|
+
for (var i = 0; i < next.length; i += 1) stack.push(next[i]);
|
|
1404
|
+
}
|
|
1405
|
+
return out;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function applyDagFilters() {
|
|
1409
|
+
var canvas = dagCanvas();
|
|
1410
|
+
if (!canvas) return;
|
|
1411
|
+
var payload = parseDagInspectorPayload();
|
|
1412
|
+
var state = readDagState();
|
|
1413
|
+
if (state.unconnected) canvas.removeAttribute('data-rcf-dag-unconnected-hidden');
|
|
1414
|
+
else canvas.setAttribute('data-rcf-dag-unconnected-hidden', '1');
|
|
1415
|
+
|
|
1416
|
+
var ups = {};
|
|
1417
|
+
var downs = {};
|
|
1418
|
+
if (state.sel && payload[state.sel]) {
|
|
1419
|
+
ups = dagClosure(payload, state.sel, 'up');
|
|
1420
|
+
downs = dagClosure(payload, state.sel, 'down');
|
|
1421
|
+
}
|
|
1422
|
+
if (state.sel) canvas.setAttribute('data-rcf-dag-sel', state.sel);
|
|
1423
|
+
else canvas.removeAttribute('data-rcf-dag-sel');
|
|
1424
|
+
|
|
1425
|
+
var nodes = canvas.querySelectorAll('.rcf-dag-node');
|
|
1426
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
1427
|
+
var el = nodes[i];
|
|
1428
|
+
var id = el.getAttribute('data-fbs-id');
|
|
1429
|
+
var status = el.getAttribute('data-status') || '';
|
|
1430
|
+
var domain = el.getAttribute('data-domain') || '';
|
|
1431
|
+
var buildable = el.getAttribute('data-buildable') === '1';
|
|
1432
|
+
var critical = el.getAttribute('data-critical') === '1';
|
|
1433
|
+
var unconnected = el.getAttribute('data-unconnected') === '1';
|
|
1434
|
+
var hide = false;
|
|
1435
|
+
if (state.statuses[status] === false) hide = true;
|
|
1436
|
+
if (!hide && state.domain && domain !== state.domain) hide = true;
|
|
1437
|
+
if (!hide && state.buildable && !buildable) hide = true;
|
|
1438
|
+
if (!hide && state.critical && !critical) hide = true;
|
|
1439
|
+
if (unconnected && !state.unconnected) hide = true;
|
|
1440
|
+
if (hide) el.setAttribute('data-rcf-dag-hidden', '1');
|
|
1441
|
+
else el.removeAttribute('data-rcf-dag-hidden');
|
|
1442
|
+
var role = '';
|
|
1443
|
+
if (state.sel) {
|
|
1444
|
+
if (id === state.sel) role = 'sel';
|
|
1445
|
+
else if (ups[id]) role = 'up';
|
|
1446
|
+
else if (downs[id]) role = 'down';
|
|
1447
|
+
}
|
|
1448
|
+
if (role) el.setAttribute('data-rcf-dag-role', role);
|
|
1449
|
+
else el.removeAttribute('data-rcf-dag-role');
|
|
1450
|
+
if (state.critical && critical) el.setAttribute('data-rcf-dag-critical', 'on');
|
|
1451
|
+
else el.removeAttribute('data-rcf-dag-critical');
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
var edges = canvas.querySelectorAll('[data-rcf-dag-edge]');
|
|
1455
|
+
for (var j = 0; j < edges.length; j += 1) {
|
|
1456
|
+
var ed = edges[j];
|
|
1457
|
+
var from = ed.getAttribute('data-from');
|
|
1458
|
+
var to = ed.getAttribute('data-to');
|
|
1459
|
+
var fromNode = canvas.querySelector('.rcf-dag-node[data-fbs-id="' + from + '"]');
|
|
1460
|
+
var toNode = canvas.querySelector('.rcf-dag-node[data-fbs-id="' + to + '"]');
|
|
1461
|
+
var filtered = (fromNode && fromNode.getAttribute('data-rcf-dag-hidden') === '1')
|
|
1462
|
+
|| (toNode && toNode.getAttribute('data-rcf-dag-hidden') === '1');
|
|
1463
|
+
if (filtered) ed.setAttribute('data-rcf-dag-hidden', '1');
|
|
1464
|
+
else ed.removeAttribute('data-rcf-dag-hidden');
|
|
1465
|
+
var erole = '';
|
|
1466
|
+
if (state.sel) {
|
|
1467
|
+
var fromUp = from === state.sel || ups[from];
|
|
1468
|
+
var toUp = to === state.sel || ups[to];
|
|
1469
|
+
var fromDown = from === state.sel || downs[from];
|
|
1470
|
+
var toDown = to === state.sel || downs[to];
|
|
1471
|
+
if (fromUp && toUp) erole = 'up';
|
|
1472
|
+
else if (fromDown && toDown) erole = 'down';
|
|
1473
|
+
}
|
|
1474
|
+
if (erole) ed.setAttribute('data-rcf-dag-edge-role', erole);
|
|
1475
|
+
else ed.removeAttribute('data-rcf-dag-edge-role');
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
function renderDagInspector() {
|
|
1480
|
+
var body = dagInspectorBody();
|
|
1481
|
+
if (!body) return;
|
|
1482
|
+
var payload = parseDagInspectorPayload();
|
|
1483
|
+
var state = readDagState();
|
|
1484
|
+
if (!state.sel || !payload[state.sel]) {
|
|
1485
|
+
body.innerHTML = '<div class="muted small">Select a build spec to see what it needs and what waits on it.</div>';
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
var f = payload[state.sel];
|
|
1489
|
+
var meta = '<div class="rcf-dag-inspector-meta">'
|
|
1490
|
+
+ '<span class="rcf-pill rcf-pill--doc-status rcf-pill--' + dagEscape((f.status || '').toLowerCase()) + '">' + dagEscape(f.status || 'unknown') + '</span>'
|
|
1491
|
+
+ (f.domain ? '<span class="rcf-badge rcf-badge--facet">' + dagEscape(f.domain) + '</span>' : '')
|
|
1492
|
+
+ '<span class="rcf-badge rcf-badge--count">AC ' + (f.acCount || 0) + '</span>'
|
|
1493
|
+
+ (f.size ? '<span class="rcf-badge rcf-badge--facet">' + dagEscape(f.size) + '</span>' : '')
|
|
1494
|
+
+ '<span class="rcf-badge rcf-badge--count">order ' + (f.buildOrder != null ? f.buildOrder : '') + '</span>'
|
|
1495
|
+
+ (f.buildable ? '<span class="rcf-pill rcf-pill--build-queue">buildable now</span>' : '')
|
|
1496
|
+
+ (f.critical ? '<span class="rcf-pill rcf-pill--doc-status">critical path</span>' : '')
|
|
1497
|
+
+ '</div>';
|
|
1498
|
+
var needsItems = (f.needsMeta || []).map(function (m) {
|
|
1499
|
+
return '<li><a class="mono" href="#" data-rcf-dag-go="' + dagEscape(m.id) + '">' + dagEscape(m.id) + '</a> <span>' + dagEscape(m.title) + '</span> <span class="rcf-pill rcf-pill--doc-status rcf-pill--' + dagEscape((m.status || '').toLowerCase()) + '">' + dagEscape(m.status || '?') + '</span></li>';
|
|
1500
|
+
}).join('');
|
|
1501
|
+
var needsBlock = '<h4>Needs first (' + (f.needs ? f.needs.length : 0) + ' direct, ' + (f.needsTotal || 0) + ' in total)</h4>'
|
|
1502
|
+
+ (needsItems ? '<ul class="rcf-dag-inspector-list">' + needsItems + '</ul>' : '<div class="muted small">Nothing.</div>');
|
|
1503
|
+
var waitsItems = (f.waitsMeta || []).map(function (m) {
|
|
1504
|
+
return '<li><a class="mono" href="#" data-rcf-dag-go="' + dagEscape(m.id) + '">' + dagEscape(m.id) + '</a> <span>' + dagEscape(m.title) + '</span></li>';
|
|
1505
|
+
}).join('');
|
|
1506
|
+
var waitsBlock = '<h4>Waits on this (' + (f.waits ? f.waits.length : 0) + ' direct, ' + (f.waitsTotal || 0) + ' in total)</h4>'
|
|
1507
|
+
+ (waitsItems ? '<ul class="rcf-dag-inspector-list">' + waitsItems + '</ul>' : '<div class="muted small">Nothing.</div>');
|
|
1508
|
+
var footer = '<p class="small" style="margin-top:0.8rem"><a href="#tab=build&sub=specs&entity=' + dagEscape(f.id) + '" data-rcf-dag-open-specs>Open in Specs</a> · <a href="#" data-rcf-dag-go="" class="muted">Clear selection</a></p>';
|
|
1509
|
+
body.innerHTML = '<div class="mono small" style="color:var(--sv-link)">' + dagEscape(f.id) + '</div>'
|
|
1510
|
+
+ '<h3>' + dagEscape(f.title) + '</h3>'
|
|
1511
|
+
+ meta
|
|
1512
|
+
+ needsBlock
|
|
1513
|
+
+ waitsBlock
|
|
1514
|
+
+ footer;
|
|
1515
|
+
|
|
1516
|
+
var goLinks = body.querySelectorAll('[data-rcf-dag-go]');
|
|
1517
|
+
for (var i = 0; i < goLinks.length; i += 1) {
|
|
1518
|
+
goLinks[i].addEventListener('click', function (ev) {
|
|
1519
|
+
ev.preventDefault && ev.preventDefault();
|
|
1520
|
+
var next = ev.currentTarget.getAttribute('data-rcf-dag-go');
|
|
1521
|
+
if (!next) selectDagNode(null);
|
|
1522
|
+
else selectDagNode(next);
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
var specsLink = body.querySelector('[data-rcf-dag-open-specs]');
|
|
1526
|
+
if (specsLink) {
|
|
1527
|
+
specsLink.addEventListener('click', function (ev) {
|
|
1528
|
+
ev.preventDefault && ev.preventDefault();
|
|
1529
|
+
var href = ev.currentTarget.getAttribute('href') || '';
|
|
1530
|
+
writeHash(href, false);
|
|
1531
|
+
resolveHash(window.location.hash);
|
|
1532
|
+
});
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
function selectDagNode(id) {
|
|
1537
|
+
var canvas = dagCanvas();
|
|
1538
|
+
if (!canvas) return;
|
|
1539
|
+
var cur = canvas.getAttribute('data-rcf-dag-sel') || null;
|
|
1540
|
+
var payload = parseDagInspectorPayload();
|
|
1541
|
+
var next = id === cur ? null : id;
|
|
1542
|
+
if (next && !payload[next]) next = null;
|
|
1543
|
+
if (next) canvas.setAttribute('data-rcf-dag-sel', next);
|
|
1544
|
+
else canvas.removeAttribute('data-rcf-dag-sel');
|
|
1545
|
+
if (next && payload[next] && payload[next].unconnected) {
|
|
1546
|
+
var tunc = document.querySelector('[data-rcf-dag-toggle="unconnected"]');
|
|
1547
|
+
if (tunc && tunc.getAttribute('aria-pressed') !== 'true') tunc.setAttribute('aria-pressed', 'true');
|
|
1548
|
+
}
|
|
1549
|
+
applyDagFilters();
|
|
1550
|
+
renderDagInspector();
|
|
1551
|
+
writeBuildHash();
|
|
1552
|
+
if (next) {
|
|
1553
|
+
var el = canvas.querySelector('.rcf-dag-node[data-fbs-id="' + next + '"]');
|
|
1554
|
+
if (el) { try { el.scrollIntoView({ block: 'center', inline: 'center' }); } catch (e) { el.scrollIntoView(); } }
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function wireBuildDag() {
|
|
1559
|
+
var root = dagRoot();
|
|
1560
|
+
if (!root || root.__rcfBuildDagWired) return;
|
|
1561
|
+
root.__rcfBuildDagWired = true;
|
|
1562
|
+
var canvas = dagCanvas();
|
|
1563
|
+
if (canvas) {
|
|
1564
|
+
var nodes = canvas.querySelectorAll('.rcf-dag-node');
|
|
1565
|
+
for (var i = 0; i < nodes.length; i += 1) {
|
|
1566
|
+
nodes[i].addEventListener('click', function (ev) {
|
|
1567
|
+
ev.preventDefault && ev.preventDefault();
|
|
1568
|
+
var id = ev.currentTarget.getAttribute('data-fbs-id');
|
|
1569
|
+
if (id) selectDagNode(id);
|
|
1570
|
+
});
|
|
1571
|
+
}
|
|
1572
|
+
}
|
|
1573
|
+
var bar = dagToolbar();
|
|
1574
|
+
if (bar) {
|
|
1575
|
+
var statusChips = bar.querySelectorAll('[data-rcf-dag-status]');
|
|
1576
|
+
for (var j = 0; j < statusChips.length; j += 1) {
|
|
1577
|
+
statusChips[j].addEventListener('click', function (ev) {
|
|
1578
|
+
var btn = ev.currentTarget;
|
|
1579
|
+
btn.setAttribute('aria-pressed', btn.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
|
|
1580
|
+
applyDagFilters();
|
|
1581
|
+
});
|
|
1582
|
+
}
|
|
1583
|
+
var dom = bar.querySelector('[data-rcf-dag-domain]');
|
|
1584
|
+
if (dom) dom.addEventListener('change', function () { applyDagFilters(); });
|
|
1585
|
+
var toggles = bar.querySelectorAll('[data-rcf-dag-toggle]');
|
|
1586
|
+
for (var k = 0; k < toggles.length; k += 1) {
|
|
1587
|
+
toggles[k].addEventListener('click', function (ev) {
|
|
1588
|
+
var btn = ev.currentTarget;
|
|
1589
|
+
btn.setAttribute('aria-pressed', btn.getAttribute('aria-pressed') === 'true' ? 'false' : 'true');
|
|
1590
|
+
applyDagFilters();
|
|
1591
|
+
});
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
applyDagFilters();
|
|
1595
|
+
renderDagInspector();
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// ---- EntitySelector (viewer UI refresh PR 3, decision 4) -----
|
|
1599
|
+
|
|
1600
|
+
function parseEntitySelectorPayload(root) {
|
|
1601
|
+
// Payload rides on the container as a `data-items` attribute so
|
|
1602
|
+
// the shell carries no inline <script> body (PR 1 contract).
|
|
1603
|
+
var raw = root.getAttribute('data-items');
|
|
1604
|
+
if (!raw) return [];
|
|
1605
|
+
try { return JSON.parse(raw); }
|
|
1606
|
+
catch (e) { return []; }
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
function wireEntitySelector(root) {
|
|
1610
|
+
if (!root || root.__rcfEntitySelectorWired) return;
|
|
1611
|
+
root.__rcfEntitySelectorWired = true;
|
|
1612
|
+
var input = root.querySelector('.rcf-entity-selector-input');
|
|
1613
|
+
var results = root.querySelector('.rcf-entity-selector-results');
|
|
1614
|
+
var targetTab = root.getAttribute('data-target-tab') || 'requirements';
|
|
1615
|
+
var items = parseEntitySelectorPayload(root);
|
|
1616
|
+
if (!input || !results || items.length === 0) return;
|
|
1617
|
+
|
|
1618
|
+
function escapeHtml(s) {
|
|
1619
|
+
return String(s == null ? '' : s)
|
|
1620
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
1621
|
+
.replace(/"/g, '"').replace(/'/g, ''');
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
function render(matches) {
|
|
1625
|
+
if (matches.length === 0) {
|
|
1626
|
+
results.innerHTML = '<div class="rcf-entity-selector-empty muted small">No match.</div>';
|
|
1627
|
+
results.removeAttribute('hidden');
|
|
1628
|
+
return;
|
|
1629
|
+
}
|
|
1630
|
+
var html = '';
|
|
1631
|
+
for (var i = 0; i < matches.length; i += 1) {
|
|
1632
|
+
var m = matches[i];
|
|
1633
|
+
var hash = '#tab=' + encodeURIComponent(targetTab) + '&entity=' + encodeURIComponent(m.id);
|
|
1634
|
+
html += '<a class="rcf-entity-selector-result" href="' + escapeHtml(hash) + '" role="option" data-entity-id="' + escapeHtml(m.id) + '">'
|
|
1635
|
+
+ '<span class="rcf-entity-selector-result-id">' + escapeHtml(m.id) + '</span>'
|
|
1636
|
+
+ '<span class="rcf-entity-selector-result-title">' + escapeHtml(m.title) + '</span>'
|
|
1637
|
+
+ (m.facet ? '<span class="rcf-entity-selector-result-facet">' + escapeHtml(m.facet) + '</span>' : '')
|
|
1638
|
+
+ '</a>';
|
|
1639
|
+
}
|
|
1640
|
+
results.innerHTML = html;
|
|
1641
|
+
results.removeAttribute('hidden');
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
function run() {
|
|
1645
|
+
var q = (input.value || '').trim().toLowerCase();
|
|
1646
|
+
if (!q) { results.innerHTML = ''; results.setAttribute('hidden', ''); return; }
|
|
1647
|
+
var out = [];
|
|
1648
|
+
for (var i = 0; i < items.length && out.length < 12; i += 1) {
|
|
1649
|
+
var it = items[i];
|
|
1650
|
+
var idL = (it.id || '').toLowerCase();
|
|
1651
|
+
var tL = (it.title || '').toLowerCase();
|
|
1652
|
+
if (idL.indexOf(q) === 0 || tL.indexOf(q) !== -1) out.push(it);
|
|
1653
|
+
}
|
|
1654
|
+
render(out);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
input.addEventListener('input', run);
|
|
1658
|
+
input.addEventListener('focus', function () { if (input.value.trim()) run(); });
|
|
1659
|
+
input.addEventListener('blur', function () {
|
|
1660
|
+
setTimeout(function () { results.setAttribute('hidden', ''); }, 150);
|
|
1661
|
+
});
|
|
1662
|
+
input.addEventListener('keydown', function (ev) {
|
|
1663
|
+
if (ev.key !== 'Enter') return;
|
|
1664
|
+
var first = results.querySelector('.rcf-entity-selector-result');
|
|
1665
|
+
if (!first) return;
|
|
1666
|
+
ev.preventDefault && ev.preventDefault();
|
|
1667
|
+
// Navigate via the Router: writes to hash so #entity=... resolves.
|
|
1668
|
+
var href = first.getAttribute('href');
|
|
1669
|
+
if (href) {
|
|
1670
|
+
writeHash(href, false);
|
|
1671
|
+
resolveHash(window.location.hash);
|
|
1672
|
+
}
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
|
|
1676
|
+
function wireEntitySelectors() {
|
|
1677
|
+
var selectors = document.querySelectorAll('[data-rcf-entity-selector]');
|
|
1678
|
+
for (var i = 0; i < selectors.length; i += 1) wireEntitySelector(selectors[i]);
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// ---- ID lookup (viewer UI refresh PR 7, TAC-4132) --------------------
|
|
1682
|
+
//
|
|
1683
|
+
// SearchButton + LookupModal + Cmd/Ctrl+F. The index at `./index.json`
|
|
1684
|
+
// is fetched lazily on first open; the SSE tree-update broadcast sets
|
|
1685
|
+
// __rcfLookupDirty so the next open refetches. Rank order (design
|
|
1686
|
+
// doc section 6): a single character searches ids only; otherwise ids
|
|
1687
|
+
// rank first (exact > prefix > contains), then every query word must
|
|
1688
|
+
// match title or snippet, title above snippet; 20 results; matches
|
|
1689
|
+
// marked. A pick writes `#tab=<tab>&entity=<id>` through writeHash so
|
|
1690
|
+
// `?embed=` and `?theme=` survive (Dex contract, PR 1).
|
|
1691
|
+
|
|
1692
|
+
var lookupRows = null;
|
|
1693
|
+
var lookupFetching = false;
|
|
1694
|
+
var lookupLastQuery = null;
|
|
1695
|
+
var lookupResults = [];
|
|
1696
|
+
var lookupActiveIdx = -1;
|
|
1697
|
+
var lookupPrevFocus = null;
|
|
1698
|
+
var lookupLoadError = false;
|
|
1699
|
+
var lookupKeybindWired = false;
|
|
1700
|
+
|
|
1701
|
+
function lookupModal() { return document.querySelector('[data-rcf-lookup]'); }
|
|
1702
|
+
function lookupInput() { return document.querySelector('[data-rcf-lookup-input]'); }
|
|
1703
|
+
function lookupResultsList() { return document.querySelector('[data-rcf-lookup-results]'); }
|
|
1704
|
+
function lookupEmpty() { return document.querySelector('[data-rcf-lookup-empty]'); }
|
|
1705
|
+
function lookupEmptyMsg() { return document.querySelector('[data-rcf-lookup-empty-msg]'); }
|
|
1706
|
+
function lookupReload() { return document.querySelector('[data-rcf-lookup-reload]'); }
|
|
1707
|
+
function lookupStatus() { return document.querySelector('[data-rcf-lookup-status]'); }
|
|
1708
|
+
|
|
1709
|
+
function lookupEscapeHtml(s) {
|
|
1710
|
+
if (typeof s !== 'string') return '';
|
|
1711
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
function lookupMarkMatches(text, needles) {
|
|
1715
|
+
if (!text) return '';
|
|
1716
|
+
var lower = text.toLowerCase();
|
|
1717
|
+
var marks = [];
|
|
1718
|
+
for (var i = 0; i < needles.length; i += 1) {
|
|
1719
|
+
var n = needles[i];
|
|
1720
|
+
if (!n) continue;
|
|
1721
|
+
var from = 0;
|
|
1722
|
+
while (from <= lower.length) {
|
|
1723
|
+
var at = lower.indexOf(n, from);
|
|
1724
|
+
if (at === -1) break;
|
|
1725
|
+
marks.push([at, at + n.length]);
|
|
1726
|
+
from = at + n.length;
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
if (marks.length === 0) return lookupEscapeHtml(text);
|
|
1730
|
+
marks.sort(function (a, b) { return a[0] - b[0]; });
|
|
1731
|
+
var merged = [];
|
|
1732
|
+
for (var j = 0; j < marks.length; j += 1) {
|
|
1733
|
+
var last = merged[merged.length - 1];
|
|
1734
|
+
if (last && marks[j][0] <= last[1]) last[1] = Math.max(last[1], marks[j][1]);
|
|
1735
|
+
else merged.push(marks[j].slice());
|
|
1736
|
+
}
|
|
1737
|
+
var out = '';
|
|
1738
|
+
var cursor = 0;
|
|
1739
|
+
for (var k = 0; k < merged.length; k += 1) {
|
|
1740
|
+
var start = merged[k][0];
|
|
1741
|
+
var end = merged[k][1];
|
|
1742
|
+
if (cursor < start) out += lookupEscapeHtml(text.slice(cursor, start));
|
|
1743
|
+
out += '<mark>' + lookupEscapeHtml(text.slice(start, end)) + '</mark>';
|
|
1744
|
+
cursor = end;
|
|
1745
|
+
}
|
|
1746
|
+
if (cursor < text.length) out += lookupEscapeHtml(text.slice(cursor));
|
|
1747
|
+
return out;
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
function lookupRank(query, rows) {
|
|
1751
|
+
if (!query || !rows) return [];
|
|
1752
|
+
var q = String(query).trim().toLowerCase();
|
|
1753
|
+
if (!q) return [];
|
|
1754
|
+
var scored = [];
|
|
1755
|
+
// Single-character query: id-only match (design doc section 6).
|
|
1756
|
+
if (q.length === 1) {
|
|
1757
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
1758
|
+
var r = rows[i];
|
|
1759
|
+
var idLow = (r.id || '').toLowerCase();
|
|
1760
|
+
if (idLow.indexOf(q) !== -1) {
|
|
1761
|
+
var score = 0;
|
|
1762
|
+
if (idLow === q) score = 1000;
|
|
1763
|
+
else if (idLow.indexOf(q) === 0) score = 500;
|
|
1764
|
+
else score = 100;
|
|
1765
|
+
scored.push({ row: r, score: score, needles: [q] });
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
} else {
|
|
1769
|
+
var words = q.split(/\s+/).filter(Boolean);
|
|
1770
|
+
for (var i2 = 0; i2 < rows.length; i2 += 1) {
|
|
1771
|
+
var r2 = rows[i2];
|
|
1772
|
+
var idLow2 = (r2.id || '').toLowerCase();
|
|
1773
|
+
var titleLow = (r2.title || '').toLowerCase();
|
|
1774
|
+
var snipLow = (r2.snippet || '').toLowerCase();
|
|
1775
|
+
var score2 = 0;
|
|
1776
|
+
// Id first: exact / prefix / contains against the full query.
|
|
1777
|
+
if (idLow2 === q) score2 = 10000;
|
|
1778
|
+
else if (idLow2.indexOf(q) === 0) score2 = 5000;
|
|
1779
|
+
else if (idLow2.indexOf(q) !== -1) score2 = 2500;
|
|
1780
|
+
else {
|
|
1781
|
+
// Word-match pass: every word must hit title or snippet.
|
|
1782
|
+
var titleHits = 0;
|
|
1783
|
+
var snipHits = 0;
|
|
1784
|
+
var allHit = true;
|
|
1785
|
+
for (var w = 0; w < words.length; w += 1) {
|
|
1786
|
+
var word = words[w];
|
|
1787
|
+
var inTitle = titleLow.indexOf(word) !== -1;
|
|
1788
|
+
var inSnip = snipLow.indexOf(word) !== -1;
|
|
1789
|
+
if (inTitle) titleHits += 1;
|
|
1790
|
+
else if (inSnip) snipHits += 1;
|
|
1791
|
+
else { allHit = false; break; }
|
|
1792
|
+
}
|
|
1793
|
+
if (!allHit) continue;
|
|
1794
|
+
// Title above snippet: title hits weighted higher than snippet.
|
|
1795
|
+
score2 = 1000 + titleHits * 50 + snipHits * 10;
|
|
1796
|
+
}
|
|
1797
|
+
if (score2 > 0) scored.push({ row: r2, score: score2, needles: words });
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
scored.sort(function (a, b) {
|
|
1801
|
+
if (b.score !== a.score) return b.score - a.score;
|
|
1802
|
+
return (a.row.id || '').localeCompare(b.row.id || '');
|
|
1803
|
+
});
|
|
1804
|
+
return scored.slice(0, 20);
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function lookupKindLabel(kind) {
|
|
1808
|
+
switch (kind) {
|
|
1809
|
+
case 'prd': return 'PRD';
|
|
1810
|
+
case 'req': return 'REQ';
|
|
1811
|
+
case 'us': return 'US';
|
|
1812
|
+
case 'ac': return 'AC';
|
|
1813
|
+
case 'tad': return 'TAD';
|
|
1814
|
+
case 'tac': return 'TAC';
|
|
1815
|
+
case 'adr': return 'ADR';
|
|
1816
|
+
case 'bs': return 'BS';
|
|
1817
|
+
case 'fbs': return 'FBS';
|
|
1818
|
+
case 'ts': return 'TS';
|
|
1819
|
+
default: return kind || '';
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
function lookupRenderResults(results) {
|
|
1824
|
+
var list = lookupResultsList();
|
|
1825
|
+
var empty = lookupEmpty();
|
|
1826
|
+
var emptyMsg = lookupEmptyMsg();
|
|
1827
|
+
var reload = lookupReload();
|
|
1828
|
+
var status = lookupStatus();
|
|
1829
|
+
if (!list) return;
|
|
1830
|
+
if (results.length === 0) {
|
|
1831
|
+
list.innerHTML = '';
|
|
1832
|
+
list.hidden = true;
|
|
1833
|
+
var hasQuery = (lookupLastQuery || '').trim().length > 0;
|
|
1834
|
+
if (empty) empty.hidden = !lookupLoadError && !hasQuery;
|
|
1835
|
+
if (emptyMsg) emptyMsg.textContent = lookupLoadError ? 'The index could not be loaded.' : 'No matches.';
|
|
1836
|
+
if (reload) reload.hidden = !lookupLoadError;
|
|
1837
|
+
if (status) {
|
|
1838
|
+
if (lookupLoadError) status.textContent = 'Index failed to load.';
|
|
1839
|
+
else if (hasQuery) status.textContent = 'No matches.';
|
|
1840
|
+
else status.textContent = (lookupRows ? 'Type an id or a few words.' : 'Loading index...');
|
|
1841
|
+
}
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
if (empty) empty.hidden = true;
|
|
1845
|
+
if (reload) reload.hidden = true;
|
|
1846
|
+
list.hidden = false;
|
|
1847
|
+
var html = '';
|
|
1848
|
+
for (var i = 0; i < results.length; i += 1) {
|
|
1849
|
+
var scored = results[i];
|
|
1850
|
+
var r = scored.row;
|
|
1851
|
+
var needles = scored.needles || [];
|
|
1852
|
+
var active = i === lookupActiveIdx ? ' is-active' : '';
|
|
1853
|
+
var markedId = lookupMarkMatches(r.id || '', needles);
|
|
1854
|
+
var markedTitle = lookupMarkMatches(r.title || '', needles);
|
|
1855
|
+
var markedSnip = lookupMarkMatches(r.snippet || '', needles);
|
|
1856
|
+
var parentHtml = r.parent ? ' <span class="rcf-lookup-parent">in ' + lookupEscapeHtml(r.parent) + '</span>' : '';
|
|
1857
|
+
html += '<li class="rcf-lookup-row' + active + '" role="option"'
|
|
1858
|
+
+ (i === lookupActiveIdx ? ' aria-selected="true"' : '')
|
|
1859
|
+
+ ' data-rcf-lookup-row data-rcf-lookup-id="' + lookupEscapeHtml(r.id || '')
|
|
1860
|
+
+ '" data-rcf-lookup-tab="' + lookupEscapeHtml(r.tab || '')
|
|
1861
|
+
+ '" data-rcf-lookup-kind="' + lookupEscapeHtml(r.kind || '')
|
|
1862
|
+
+ '" data-rcf-lookup-parent="' + lookupEscapeHtml(r.parent || '') + '" data-rcf-lookup-idx="' + i + '">'
|
|
1863
|
+
+ '<span class="rcf-lookup-kind rcf-lookup-kind-' + lookupEscapeHtml(r.kind || '') + '">' + lookupKindLabel(r.kind) + '</span>'
|
|
1864
|
+
+ '<span class="rcf-lookup-id">' + markedId + '</span>'
|
|
1865
|
+
+ (r.title ? '<span class="rcf-lookup-title">' + markedTitle + '</span>' : '')
|
|
1866
|
+
+ parentHtml
|
|
1867
|
+
+ (r.snippet ? '<p class="rcf-lookup-snippet">' + markedSnip + '</p>' : '')
|
|
1868
|
+
+ '</li>';
|
|
1869
|
+
}
|
|
1870
|
+
list.innerHTML = html;
|
|
1871
|
+
if (status) status.textContent = results.length + ' result' + (results.length === 1 ? '' : 's') + '.';
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
function lookupRun() {
|
|
1875
|
+
var input = lookupInput();
|
|
1876
|
+
if (!input) return;
|
|
1877
|
+
var query = input.value || '';
|
|
1878
|
+
lookupLastQuery = query;
|
|
1879
|
+
if (!lookupRows) {
|
|
1880
|
+
lookupResults = [];
|
|
1881
|
+
lookupActiveIdx = -1;
|
|
1882
|
+
var list = lookupResultsList();
|
|
1883
|
+
if (list) { list.innerHTML = ''; list.hidden = true; }
|
|
1884
|
+
var empty = lookupEmpty();
|
|
1885
|
+
var emptyMsg = lookupEmptyMsg();
|
|
1886
|
+
var reload = lookupReload();
|
|
1887
|
+
if (empty) empty.hidden = false;
|
|
1888
|
+
if (emptyMsg) emptyMsg.textContent = lookupLoadError ? 'The index could not be loaded.' : (lookupFetching ? 'Loading index...' : 'Index not loaded.');
|
|
1889
|
+
if (reload) reload.hidden = !lookupLoadError;
|
|
1890
|
+
return;
|
|
1891
|
+
}
|
|
1892
|
+
lookupResults = lookupRank(query, lookupRows);
|
|
1893
|
+
lookupActiveIdx = lookupResults.length > 0 ? 0 : -1;
|
|
1894
|
+
lookupRenderResults(lookupResults);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
function lookupLoad(force) {
|
|
1898
|
+
if (lookupFetching) return;
|
|
1899
|
+
if (lookupRows && !force && !window.__rcfLookupDirty) { lookupRun(); return; }
|
|
1900
|
+
lookupFetching = true;
|
|
1901
|
+
lookupLoadError = false;
|
|
1902
|
+
var status = lookupStatus();
|
|
1903
|
+
if (status) status.textContent = 'Loading index...';
|
|
1904
|
+
var xhr = new XMLHttpRequest();
|
|
1905
|
+
xhr.open('GET', './index.json', true);
|
|
1906
|
+
xhr.onreadystatechange = function () {
|
|
1907
|
+
if (xhr.readyState !== 4) return;
|
|
1908
|
+
lookupFetching = false;
|
|
1909
|
+
var ok = xhr.status >= 200 && xhr.status < 300;
|
|
1910
|
+
var parsed = null;
|
|
1911
|
+
if (ok) {
|
|
1912
|
+
try { parsed = JSON.parse(xhr.responseText); } catch (e) { parsed = null; }
|
|
1913
|
+
}
|
|
1914
|
+
if (!ok || !parsed || !Array.isArray(parsed.rows)) {
|
|
1915
|
+
lookupLoadError = true;
|
|
1916
|
+
lookupRows = null;
|
|
1917
|
+
lookupRun();
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
lookupLoadError = false;
|
|
1921
|
+
lookupRows = parsed.rows;
|
|
1922
|
+
window.__rcfLookupDirty = false;
|
|
1923
|
+
lookupRun();
|
|
1924
|
+
};
|
|
1925
|
+
try { xhr.send(); } catch (e) { lookupFetching = false; lookupLoadError = true; lookupRun(); }
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
function lookupOpen() {
|
|
1929
|
+
var modal = lookupModal();
|
|
1930
|
+
if (!modal) return;
|
|
1931
|
+
if (modal.hidden === false) return;
|
|
1932
|
+
lookupPrevFocus = document.activeElement;
|
|
1933
|
+
modal.hidden = false;
|
|
1934
|
+
document.documentElement.setAttribute('data-rcf-lookup-open', '1');
|
|
1935
|
+
var input = lookupInput();
|
|
1936
|
+
if (input) {
|
|
1937
|
+
input.value = '';
|
|
1938
|
+
try { input.focus(); } catch (e) { /* soft */ }
|
|
1939
|
+
}
|
|
1940
|
+
lookupLastQuery = '';
|
|
1941
|
+
lookupResults = [];
|
|
1942
|
+
lookupActiveIdx = -1;
|
|
1943
|
+
var list = lookupResultsList();
|
|
1944
|
+
if (list) { list.innerHTML = ''; list.hidden = true; }
|
|
1945
|
+
var empty = lookupEmpty();
|
|
1946
|
+
if (empty) empty.hidden = true;
|
|
1947
|
+
lookupLoad(false);
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1950
|
+
function lookupClose() {
|
|
1951
|
+
var modal = lookupModal();
|
|
1952
|
+
if (!modal || modal.hidden) return;
|
|
1953
|
+
modal.hidden = true;
|
|
1954
|
+
document.documentElement.removeAttribute('data-rcf-lookup-open');
|
|
1955
|
+
if (lookupPrevFocus && typeof lookupPrevFocus.focus === 'function') {
|
|
1956
|
+
try { lookupPrevFocus.focus(); } catch (e) { /* soft */ }
|
|
1957
|
+
}
|
|
1958
|
+
lookupPrevFocus = null;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
function lookupMove(delta) {
|
|
1962
|
+
if (lookupResults.length === 0) return;
|
|
1963
|
+
var next = lookupActiveIdx + delta;
|
|
1964
|
+
if (next < 0) next = lookupResults.length - 1;
|
|
1965
|
+
else if (next >= lookupResults.length) next = 0;
|
|
1966
|
+
lookupActiveIdx = next;
|
|
1967
|
+
lookupRenderResults(lookupResults);
|
|
1968
|
+
var row = document.querySelector('[data-rcf-lookup-row][data-rcf-lookup-idx="' + next + '"]');
|
|
1969
|
+
if (row && typeof row.scrollIntoView === 'function') {
|
|
1970
|
+
try { row.scrollIntoView({ block: 'nearest' }); } catch (e) { /* soft */ }
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
function lookupPick(row) {
|
|
1975
|
+
if (!row) return;
|
|
1976
|
+
var id = row.getAttribute('data-rcf-lookup-id');
|
|
1977
|
+
var tab = row.getAttribute('data-rcf-lookup-tab');
|
|
1978
|
+
if (!id || !tab) return;
|
|
1979
|
+
// Design decision 6: test suites render inside their owning US (not
|
|
1980
|
+
// a Build sub-tab). A TS pick re-routes to the US id so the Router
|
|
1981
|
+
// opens the US and the TS card inside it (viewer stream carry).
|
|
1982
|
+
var kind = row.getAttribute('data-rcf-lookup-kind');
|
|
1983
|
+
var parentId = row.getAttribute('data-rcf-lookup-parent');
|
|
1984
|
+
if (kind === 'ts' && parentId) { id = parentId; tab = 'requirements'; }
|
|
1985
|
+
lookupClose();
|
|
1986
|
+
// writeHash preserves ?embed=/?theme=/... (Dex contract, PR 1); the
|
|
1987
|
+
// Router's hashchange listener then activates the tab and opens
|
|
1988
|
+
// every ancestor DocRow (resolveHash).
|
|
1989
|
+
writeHash('#tab=' + encodeURIComponent(tab) + '&entity=' + encodeURIComponent(id), false);
|
|
1990
|
+
// If the hash was already the same (user picked the current entity
|
|
1991
|
+
// again), writeHash is a no-op and no hashchange fires. Re-resolve
|
|
1992
|
+
// to re-flash the target in that case.
|
|
1993
|
+
resolveHash(window.location.hash);
|
|
1994
|
+
}
|
|
1995
|
+
|
|
1996
|
+
function lookupOnKeydown(ev) {
|
|
1997
|
+
if (ev.key === 'Escape') {
|
|
1998
|
+
ev.preventDefault && ev.preventDefault();
|
|
1999
|
+
lookupClose();
|
|
2000
|
+
return;
|
|
2001
|
+
}
|
|
2002
|
+
if (ev.key === 'ArrowDown') {
|
|
2003
|
+
ev.preventDefault && ev.preventDefault();
|
|
2004
|
+
lookupMove(1);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
if (ev.key === 'ArrowUp') {
|
|
2008
|
+
ev.preventDefault && ev.preventDefault();
|
|
2009
|
+
lookupMove(-1);
|
|
2010
|
+
return;
|
|
2011
|
+
}
|
|
2012
|
+
if (ev.key === 'Enter') {
|
|
2013
|
+
ev.preventDefault && ev.preventDefault();
|
|
2014
|
+
if (lookupActiveIdx < 0 || lookupActiveIdx >= lookupResults.length) return;
|
|
2015
|
+
var row = document.querySelector('[data-rcf-lookup-row][data-rcf-lookup-idx="' + lookupActiveIdx + '"]');
|
|
2016
|
+
lookupPick(row);
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
|
|
2020
|
+
function lookupOnInput() {
|
|
2021
|
+
lookupRun();
|
|
2022
|
+
}
|
|
2023
|
+
|
|
2024
|
+
// Readiness StageLegend (viewer UI refresh PR 8). Any element carrying
|
|
2025
|
+
// `data-rcf-stage-ref="Dn"` opens the stage legend dialog focused on
|
|
2026
|
+
// that row. The dialog is server-rendered (readiness/stage-legend.js);
|
|
2027
|
+
// this wiring is purely a click + <dialog>.showModal() delegation.
|
|
2028
|
+
var stageLegendWired = false;
|
|
2029
|
+
function wireStageLegend() {
|
|
2030
|
+
if (stageLegendWired) return;
|
|
2031
|
+
stageLegendWired = true;
|
|
2032
|
+
document.addEventListener('click', function (ev) {
|
|
2033
|
+
var el = ev.target;
|
|
2034
|
+
while (el && el !== document.body) {
|
|
2035
|
+
if (el.getAttribute && el.getAttribute('data-rcf-stage-ref') !== null) {
|
|
2036
|
+
var stage = el.getAttribute('data-rcf-stage-ref');
|
|
2037
|
+
var dialog = document.getElementById('rcf-stage-legend');
|
|
2038
|
+
if (!dialog) return;
|
|
2039
|
+
var rows = dialog.querySelectorAll('tr[data-stage]');
|
|
2040
|
+
for (var i = 0; i < rows.length; i += 1) {
|
|
2041
|
+
if (rows[i].getAttribute('data-stage') === stage) {
|
|
2042
|
+
rows[i].classList.add('rcf-stage-legend__highlighted');
|
|
2043
|
+
} else {
|
|
2044
|
+
rows[i].classList.remove('rcf-stage-legend__highlighted');
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
if (typeof dialog.showModal === 'function' && !dialog.open) {
|
|
2048
|
+
ev.preventDefault && ev.preventDefault();
|
|
2049
|
+
dialog.showModal();
|
|
2050
|
+
} else if (!dialog.open) {
|
|
2051
|
+
ev.preventDefault && ev.preventDefault();
|
|
2052
|
+
dialog.setAttribute('open', '');
|
|
2053
|
+
}
|
|
2054
|
+
return;
|
|
2055
|
+
}
|
|
2056
|
+
el = el.parentNode;
|
|
2057
|
+
}
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
|
|
2061
|
+
function wireLookup() {
|
|
2062
|
+
var modal = lookupModal();
|
|
2063
|
+
if (modal && !modal.__rcfLookupWired) {
|
|
2064
|
+
modal.__rcfLookupWired = true;
|
|
2065
|
+
var input = lookupInput();
|
|
2066
|
+
if (input) {
|
|
2067
|
+
input.addEventListener('input', lookupOnInput);
|
|
2068
|
+
input.addEventListener('keydown', lookupOnKeydown);
|
|
2069
|
+
}
|
|
2070
|
+
var closeBtn = document.querySelector('[data-rcf-lookup-close]');
|
|
2071
|
+
if (closeBtn) closeBtn.addEventListener('click', function (ev) { ev.preventDefault && ev.preventDefault(); lookupClose(); });
|
|
2072
|
+
var backdrop = document.querySelector('[data-rcf-lookup-backdrop]');
|
|
2073
|
+
if (backdrop) backdrop.addEventListener('click', function () { lookupClose(); });
|
|
2074
|
+
var reload = lookupReload();
|
|
2075
|
+
if (reload) reload.addEventListener('click', function (ev) { ev.preventDefault && ev.preventDefault(); lookupLoad(true); });
|
|
2076
|
+
var list = lookupResultsList();
|
|
2077
|
+
if (list) {
|
|
2078
|
+
list.addEventListener('click', function (ev) {
|
|
2079
|
+
var el = ev.target;
|
|
2080
|
+
while (el && el !== list) {
|
|
2081
|
+
if (el.getAttribute && el.getAttribute('data-rcf-lookup-row') !== null) {
|
|
2082
|
+
ev.preventDefault && ev.preventDefault();
|
|
2083
|
+
lookupPick(el);
|
|
2084
|
+
return;
|
|
2085
|
+
}
|
|
2086
|
+
el = el.parentNode;
|
|
2087
|
+
}
|
|
2088
|
+
});
|
|
2089
|
+
list.addEventListener('mousemove', function (ev) {
|
|
2090
|
+
var el = ev.target;
|
|
2091
|
+
while (el && el !== list) {
|
|
2092
|
+
if (el.getAttribute && el.getAttribute('data-rcf-lookup-idx') !== null) {
|
|
2093
|
+
var idx = Number(el.getAttribute('data-rcf-lookup-idx'));
|
|
2094
|
+
if (!Number.isNaN(idx) && idx !== lookupActiveIdx) {
|
|
2095
|
+
lookupActiveIdx = idx;
|
|
2096
|
+
lookupRenderResults(lookupResults);
|
|
2097
|
+
}
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
el = el.parentNode;
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
var btn = document.querySelector('[data-rcf-search]');
|
|
2106
|
+
if (btn && !btn.__rcfLookupWired) {
|
|
2107
|
+
btn.__rcfLookupWired = true;
|
|
2108
|
+
btn.addEventListener('click', function (ev) { ev.preventDefault && ev.preventDefault(); lookupOpen(); });
|
|
2109
|
+
}
|
|
2110
|
+
if (!lookupKeybindWired) {
|
|
2111
|
+
lookupKeybindWired = true;
|
|
2112
|
+
// Cmd/Ctrl+F opens the modal; Cmd/Ctrl+Shift+F stays the browser's
|
|
2113
|
+
// in-page find (design doc section 11 recommendation 1).
|
|
2114
|
+
document.addEventListener('keydown', function (ev) {
|
|
2115
|
+
var mod = ev.ctrlKey || ev.metaKey;
|
|
2116
|
+
if (!mod) return;
|
|
2117
|
+
if (ev.shiftKey) return;
|
|
2118
|
+
if ((ev.key === 'f' || ev.key === 'F')) {
|
|
2119
|
+
ev.preventDefault && ev.preventDefault();
|
|
2120
|
+
var modalEl = lookupModal();
|
|
2121
|
+
if (modalEl && modalEl.hidden === false) {
|
|
2122
|
+
var inp = lookupInput();
|
|
2123
|
+
if (inp) try { inp.focus(); inp.select && inp.select(); } catch (e) { /* soft */ }
|
|
2124
|
+
} else {
|
|
2125
|
+
lookupOpen();
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
});
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
var hashchangeWired = false;
|
|
2133
|
+
|
|
2134
|
+
// AC-205-5: a mount URL of `?tab=requirements&entity=US-304` lands on the
|
|
2135
|
+
// same position at first paint. The hash-router owns tab + entity, so we
|
|
2136
|
+
// promote the tab/sub/entity query params into the hash fragment at boot
|
|
2137
|
+
// when no explicit hash is already set. embed/theme stay in the query so
|
|
2138
|
+
// urlWithHash keeps the Dex contract.
|
|
2139
|
+
function bootHashFromQuery() {
|
|
2140
|
+
if (window.location.hash) return;
|
|
2141
|
+
var q = parseQuery(window.location.search);
|
|
2142
|
+
var parts = [];
|
|
2143
|
+
if (q.tab) parts.push('tab=' + encodeURIComponent(q.tab));
|
|
2144
|
+
if (q.sub) parts.push('sub=' + encodeURIComponent(q.sub));
|
|
2145
|
+
if (q.entity) parts.push('entity=' + encodeURIComponent(q.entity));
|
|
2146
|
+
if (parts.length === 0) return;
|
|
2147
|
+
try {
|
|
2148
|
+
if (window.history && typeof window.history.replaceState === 'function') {
|
|
2149
|
+
window.history.replaceState(null, '', window.location.pathname + window.location.search + '#' + parts.join('&'));
|
|
2150
|
+
} else {
|
|
2151
|
+
window.location.hash = parts.join('&');
|
|
2152
|
+
}
|
|
2153
|
+
} catch (err) { /* best effort; resolveHash will no-op on empty hash */ }
|
|
2154
|
+
}
|
|
2155
|
+
|
|
2156
|
+
function onReady() {
|
|
2157
|
+
initShellFromQuery();
|
|
2158
|
+
bootHashFromQuery();
|
|
2159
|
+
wireThemeMessages();
|
|
2160
|
+
initMermaid();
|
|
2161
|
+
wireTabs();
|
|
2162
|
+
wireProductMap();
|
|
2163
|
+
wireRequirementsFilterBar();
|
|
2164
|
+
wireArchitectureFilterBars();
|
|
2165
|
+
wireBuildTab();
|
|
2166
|
+
wireEntitySelectors();
|
|
2167
|
+
wireFixturePage();
|
|
2168
|
+
wireLookup();
|
|
2169
|
+
wireStageLegend();
|
|
2170
|
+
resolveHash(window.location.hash);
|
|
2171
|
+
if (!hashchangeWired) {
|
|
2172
|
+
hashchangeWired = true;
|
|
2173
|
+
window.addEventListener('hashchange', function () {
|
|
2174
|
+
resolveHash(window.location.hash);
|
|
2175
|
+
});
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
window.rcfPage = window.rcfPage || {};
|
|
2180
|
+
window.rcfPage.init = onReady;
|
|
2181
|
+
// Shared helpers available for later tabs to call. The toast helper
|
|
2182
|
+
// is the one the fixture page demos; later PRs reuse it for the
|
|
2183
|
+
// ID-lookup copy-id confirmation and the DAG inspector "linked"
|
|
2184
|
+
// acknowledgement.
|
|
2185
|
+
window.rcfView = window.rcfView || {};
|
|
2186
|
+
window.rcfView.showToast = showToast;
|
|
2187
|
+
|
|
2188
|
+
if (document.readyState === 'loading') {
|
|
2189
|
+
document.addEventListener('DOMContentLoaded', onReady);
|
|
2190
|
+
} else {
|
|
2191
|
+
onReady();
|
|
2192
|
+
}
|
|
2193
|
+
})();
|