@chrisdudek/yg 5.3.0 → 5.4.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/dist/bin.js +1860 -373
- package/dist/structure.d.ts +7 -0
- package/dist/structure.js +10 -5
- package/dist/templates/portal/js/bootstrap.js +206 -0
- package/dist/templates/portal/js/consumer.js +128 -0
- package/dist/templates/portal/js/dispatch.js +181 -0
- package/dist/templates/portal/js/export.js +151 -0
- package/dist/templates/portal/js/glossary.js +72 -0
- package/dist/templates/portal/js/namespace.js +60 -0
- package/dist/templates/portal/js/palette-overlay.js +176 -0
- package/dist/templates/portal/js/palette.js +133 -0
- package/dist/templates/portal/js/router.js +169 -0
- package/dist/templates/portal/js/shell.js +264 -0
- package/dist/templates/portal/js/state-model.js +142 -0
- package/dist/templates/portal/js/tree.js +190 -0
- package/dist/templates/portal/js/views/coverage-view.js +247 -0
- package/dist/templates/portal/js/views/flows-view.js +204 -0
- package/dist/templates/portal/js/views/overview-view.js +167 -0
- package/dist/templates/portal/js/views/panel-aspect.js +141 -0
- package/dist/templates/portal/js/views/panel-view.js +314 -0
- package/dist/templates/portal/js/views/relations-matrix.js +185 -0
- package/dist/templates/portal/js/views/relations-view.js +185 -0
- package/dist/templates/portal/js/views/rulebook-view.js +174 -0
- package/dist/templates/portal/js/views/start-view.js +287 -0
- package/dist/templates/portal/js/views/suppressions-view.js +187 -0
- package/dist/templates/portal/js/views/tree-view.js +68 -0
- package/dist/templates/portal/js/views/types-view.js +154 -0
- package/dist/templates/portal/vendor/d3-hierarchy.js +350 -0
- package/package.json +5 -2
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* URL-hash router — the deep-link grammar for every view and entity.
|
|
3
|
+
*
|
|
4
|
+
* The portal is a single page, but EVERY view and EVERY entity (node / aspect / flow,
|
|
5
|
+
* down to a per-verdict node→aspect→file-unit) is deep-linkable: a hash routes straight
|
|
6
|
+
* to it, and reloading the page reopens exactly what the hash names (an entity reopens
|
|
7
|
+
* its panel). The grammar:
|
|
8
|
+
*
|
|
9
|
+
* (empty) -> { view: 'overview' } (default)
|
|
10
|
+
* #/view/<viewId> -> { view: <viewId> }
|
|
11
|
+
* #/node/<path> -> { view: 'tree', node: <path> }
|
|
12
|
+
* #/node/<path>/<aspectId>/<file-unit> -> { ..., aspect, file } (per-verdict)
|
|
13
|
+
* #/aspect/<id> -> { view: 'rulebook', aspect: <id> }
|
|
14
|
+
* #/flow/<name> -> { view: 'flows', flow: <name> }
|
|
15
|
+
*
|
|
16
|
+
* Each path segment is percent-encoded, so a node path with slashes, or a file unit with
|
|
17
|
+
* its own slashes, round-trips losslessly. parse() and serialize() are PURE string
|
|
18
|
+
* functions (no DOM, no window) so they are testable directly; start() wires them to the
|
|
19
|
+
* live `hashchange` event. Browser globals only — no network, no Node.
|
|
20
|
+
*/
|
|
21
|
+
(function () {
|
|
22
|
+
'use strict';
|
|
23
|
+
|
|
24
|
+
var Yg = (window.YgPortal = window.YgPortal || {});
|
|
25
|
+
|
|
26
|
+
// The known full-view ids. An unknown view id falls back to the overview so a stale or
|
|
27
|
+
// hand-typed hash never lands on a blank page.
|
|
28
|
+
var VIEWS = [
|
|
29
|
+
'overview',
|
|
30
|
+
'coverage',
|
|
31
|
+
'tree',
|
|
32
|
+
'relations',
|
|
33
|
+
'rulebook',
|
|
34
|
+
'types',
|
|
35
|
+
'flows',
|
|
36
|
+
'suppressions',
|
|
37
|
+
'start',
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
// Which view an entity kind opens into (the entity's natural home surface).
|
|
41
|
+
var ENTITY_VIEW = { node: 'tree', aspect: 'rulebook', flow: 'flows' };
|
|
42
|
+
|
|
43
|
+
function isKnownView(id) {
|
|
44
|
+
return VIEWS.indexOf(id) !== -1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Split a hash into clean, decoded segments (drops the leading '#', '/' and empties). */
|
|
48
|
+
function segments(hash) {
|
|
49
|
+
var raw = String(hash || '');
|
|
50
|
+
if (raw.charAt(0) === '#') raw = raw.slice(1);
|
|
51
|
+
if (raw.charAt(0) === '/') raw = raw.slice(1);
|
|
52
|
+
if (raw.length === 0) return [];
|
|
53
|
+
return raw.split('/').map(decodeSegment);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function decodeSegment(seg) {
|
|
57
|
+
try {
|
|
58
|
+
return decodeURIComponent(seg);
|
|
59
|
+
} catch (_e) {
|
|
60
|
+
return seg;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse a hash string into a normalized route object. Always returns a valid route — an
|
|
66
|
+
* empty or malformed hash resolves to the default overview, never null. The route always
|
|
67
|
+
* carries a `view`; entity fields (node / aspect / flow / file) are present only when the
|
|
68
|
+
* hash names them.
|
|
69
|
+
*/
|
|
70
|
+
function parse(hash) {
|
|
71
|
+
var segs = segments(hash);
|
|
72
|
+
if (segs.length === 0) return { view: 'overview' };
|
|
73
|
+
|
|
74
|
+
var kind = segs[0];
|
|
75
|
+
|
|
76
|
+
if (kind === 'view') {
|
|
77
|
+
var id = segs[1];
|
|
78
|
+
if (isKnownView(id)) return { view: id };
|
|
79
|
+
return { view: 'overview', notFound: id || '(none)' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (kind === 'node' && segs[1]) {
|
|
83
|
+
var route = { view: ENTITY_VIEW.node, node: segs[1] };
|
|
84
|
+
// Per-verdict file unit: node / aspectId / file-unit (file may itself contain '/').
|
|
85
|
+
if (segs.length >= 4) {
|
|
86
|
+
route.aspect = segs[2];
|
|
87
|
+
route.file = segs.slice(3).join('/');
|
|
88
|
+
}
|
|
89
|
+
return route;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (kind === 'aspect' && segs[1]) {
|
|
93
|
+
return { view: ENTITY_VIEW.aspect, aspect: segs[1] };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (kind === 'flow' && segs[1]) {
|
|
97
|
+
return { view: ENTITY_VIEW.flow, flow: segs[1] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Bare `#/view-id` shorthand (no `view/` prefix) — accept a known view id directly.
|
|
101
|
+
if (isKnownView(kind)) return { view: kind };
|
|
102
|
+
|
|
103
|
+
// An unrecognized hash — degrade to overview but carry the requested token so the page can
|
|
104
|
+
// say "nothing here" honestly, instead of silently presenting overview as if it resolved.
|
|
105
|
+
return { view: 'overview', notFound: segs.join('/') };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function enc(seg) {
|
|
109
|
+
return encodeURIComponent(String(seg));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Serialize a route object back into a canonical hash string. The inverse of parse for
|
|
114
|
+
* any route parse() produces, so a round-trip is lossless. A node route with a per-verdict
|
|
115
|
+
* file unit encodes node / aspect / file as separate segments (the file's own slashes are
|
|
116
|
+
* percent-encoded inside its single segment).
|
|
117
|
+
*/
|
|
118
|
+
function serialize(route) {
|
|
119
|
+
if (!route || !route.view) return '#/view/overview';
|
|
120
|
+
|
|
121
|
+
if (route.node) {
|
|
122
|
+
var parts = ['#', 'node', enc(route.node)];
|
|
123
|
+
if (route.aspect && route.file) {
|
|
124
|
+
parts.push(enc(route.aspect));
|
|
125
|
+
parts.push(enc(route.file));
|
|
126
|
+
}
|
|
127
|
+
return parts.join('/');
|
|
128
|
+
}
|
|
129
|
+
if (route.aspect) return '#/aspect/' + enc(route.aspect);
|
|
130
|
+
if (route.flow) return '#/flow/' + enc(route.flow);
|
|
131
|
+
return '#/view/' + enc(route.view);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Create a router bound to the live location. `onRoute(route)` fires on every hash change
|
|
136
|
+
* and once at start with the current route. `go(route)` updates the hash (which triggers
|
|
137
|
+
* onRoute via the hashchange listener — one code path, no double render). Returns a small
|
|
138
|
+
* controller; `current()` reads the live route without navigating.
|
|
139
|
+
*/
|
|
140
|
+
function create(onRoute) {
|
|
141
|
+
function emit() {
|
|
142
|
+
onRoute(parse(window.location.hash));
|
|
143
|
+
}
|
|
144
|
+
function go(route) {
|
|
145
|
+
var next = serialize(route);
|
|
146
|
+
if (window.location.hash === next) {
|
|
147
|
+
emit(); // same hash — re-emit so a repeat selection still re-renders
|
|
148
|
+
} else {
|
|
149
|
+
window.location.hash = next;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function current() {
|
|
153
|
+
return parse(window.location.hash);
|
|
154
|
+
}
|
|
155
|
+
function start() {
|
|
156
|
+
window.addEventListener('hashchange', emit);
|
|
157
|
+
emit();
|
|
158
|
+
}
|
|
159
|
+
return { go: go, current: current, start: start };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
Yg.router = {
|
|
163
|
+
VIEWS: VIEWS,
|
|
164
|
+
parse: parse,
|
|
165
|
+
serialize: serialize,
|
|
166
|
+
create: create,
|
|
167
|
+
isKnownView: isKnownView,
|
|
168
|
+
};
|
|
169
|
+
})();
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shell — the persistent three-region chrome rendered on every view.
|
|
3
|
+
*
|
|
4
|
+
* Builds and owns the frame the whole portal lives in:
|
|
5
|
+
* - LEFT nav rail: brand (-> overview), a ⌘K trigger, and the grouped view links
|
|
6
|
+
* ("Get oriented" / "Inspect & audit"), the active one highlighted.
|
|
7
|
+
* - TOP bar: a live-status pill + Refresh + Approve control cluster, a breadcrumb, the
|
|
8
|
+
* ⌘K search trigger, a write/view-only indicator, and the theme toggle.
|
|
9
|
+
* - RIGHT panel slot: an empty mount the Node Attestation panel slides into on selection.
|
|
10
|
+
* - CENTER stage: the mount the view dispatcher renders the current view into.
|
|
11
|
+
*
|
|
12
|
+
* The shell is built ONCE; navigation only swaps the center stage and updates the rail
|
|
13
|
+
* highlight + breadcrumb, so the chrome is stable. Browser globals only — Refresh/Approve
|
|
14
|
+
* are wired by the boot to the server (or disabled on a static page); no network here.
|
|
15
|
+
*/
|
|
16
|
+
(function () {
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
var Yg = (window.YgPortal = window.YgPortal || {});
|
|
20
|
+
var dom = Yg.dom;
|
|
21
|
+
|
|
22
|
+
// Nav rail groups -> view ids, mirroring §3a primary navigation.
|
|
23
|
+
var NAV_GROUPS = [
|
|
24
|
+
{
|
|
25
|
+
title: 'Get oriented',
|
|
26
|
+
items: [
|
|
27
|
+
{ view: 'overview', glyph: '◧', label: 'Overview' },
|
|
28
|
+
{ view: 'start', glyph: '◆', label: 'Start here' },
|
|
29
|
+
{ view: 'flows', glyph: '⤳', label: 'Flows' },
|
|
30
|
+
{ view: 'rulebook', glyph: '▤', label: 'Rulebook' },
|
|
31
|
+
{ view: 'types', glyph: '◇', label: 'Type model' },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
title: 'Inspect & audit',
|
|
36
|
+
items: [
|
|
37
|
+
{ view: 'coverage', glyph: '▦', label: 'Coverage & audit' },
|
|
38
|
+
{ view: 'tree', glyph: '⌗', label: 'Structure' },
|
|
39
|
+
{ view: 'relations', glyph: '↔', label: 'Relations & boundaries' },
|
|
40
|
+
{ view: 'suppressions', glyph: '⛉', label: 'Suppressions' },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the shell into `root` for a given PortalData. `handlers` carries the user actions
|
|
47
|
+
* the chrome triggers: { onNavigate(view), onSearch(), onRefresh(), onApprove(), onTheme() }.
|
|
48
|
+
* Returns a controller exposing the live region mounts and the per-navigation updaters.
|
|
49
|
+
*/
|
|
50
|
+
function build(root, data, handlers) {
|
|
51
|
+
dom.clear(root);
|
|
52
|
+
var app = dom.el('div', 'app-grid');
|
|
53
|
+
|
|
54
|
+
var rail = buildRail(data, handlers);
|
|
55
|
+
var main = dom.el('main', 'app-main');
|
|
56
|
+
var topbar = buildTopbar(data, handlers);
|
|
57
|
+
var stage = dom.el('div', 'app-stage');
|
|
58
|
+
main.appendChild(topbar.el);
|
|
59
|
+
main.appendChild(stage);
|
|
60
|
+
|
|
61
|
+
var panel = dom.el('aside', 'app-panel');
|
|
62
|
+
panel.setAttribute('aria-label', 'Node attestation panel');
|
|
63
|
+
|
|
64
|
+
app.appendChild(rail.el);
|
|
65
|
+
app.appendChild(main);
|
|
66
|
+
app.appendChild(panel);
|
|
67
|
+
|
|
68
|
+
// The honest key as ONE pinned, collapsible bottom bar — built once here, never inside a
|
|
69
|
+
// view's scrolling stage, so it can be pinned and the stage clears it via its bottom padding.
|
|
70
|
+
if (Yg.dispatch && typeof Yg.dispatch.buildLegendBar === 'function') {
|
|
71
|
+
app.appendChild(Yg.dispatch.buildLegendBar());
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
root.appendChild(app);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
stage: stage,
|
|
78
|
+
panel: panel,
|
|
79
|
+
setActive: function (view) {
|
|
80
|
+
rail.setActive(view);
|
|
81
|
+
},
|
|
82
|
+
setBreadcrumb: function (route) {
|
|
83
|
+
topbar.setBreadcrumb(route);
|
|
84
|
+
},
|
|
85
|
+
setFreshness: function (text, tone) {
|
|
86
|
+
topbar.setFreshness(text, tone);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function buildRail(data, handlers) {
|
|
92
|
+
var rail = dom.el('aside', 'app-rail');
|
|
93
|
+
rail.setAttribute('aria-label', 'Primary navigation');
|
|
94
|
+
|
|
95
|
+
var brand = dom.el('button', 'rail-brand');
|
|
96
|
+
brand.type = 'button';
|
|
97
|
+
// The brand mark: reuse the inline data: URI the serializer injected into the favicon
|
|
98
|
+
// link (single source, no network). Guarded so the non-DOM test sandbox (no querySelector)
|
|
99
|
+
// and any environment lacking the link simply omit the mark rather than break. The mark is
|
|
100
|
+
// decorative (alt=''), as the adjacent text already names the product.
|
|
101
|
+
var favicon = document.querySelector && document.querySelector('link[rel~="icon"]');
|
|
102
|
+
if (favicon && favicon.getAttribute('href')) {
|
|
103
|
+
var logo = dom.el('img', 'rail-logo');
|
|
104
|
+
logo.src = favicon.getAttribute('href');
|
|
105
|
+
logo.alt = '';
|
|
106
|
+
brand.appendChild(logo);
|
|
107
|
+
}
|
|
108
|
+
brand.appendChild(dom.el('b', null, 'Yggdrasil'));
|
|
109
|
+
// A static export is a frozen snapshot (no server), not a live view — say so honestly.
|
|
110
|
+
var brandSub = !Yg.consumer.isServed() ? 'snapshot' : data.meta.writeEnabled ? 'live view' : 'view-only';
|
|
111
|
+
brand.appendChild(dom.el('span', 'rail-brand-sub', brandSub));
|
|
112
|
+
brand.addEventListener('click', function () {
|
|
113
|
+
handlers.onNavigate('overview');
|
|
114
|
+
});
|
|
115
|
+
rail.appendChild(brand);
|
|
116
|
+
|
|
117
|
+
var cmdk = dom.el('button', 'rail-cmdk');
|
|
118
|
+
cmdk.type = 'button';
|
|
119
|
+
cmdk.appendChild(dom.el('span', null, '⌕ Search…'));
|
|
120
|
+
cmdk.appendChild(dom.el('kbd', null, '⌘K'));
|
|
121
|
+
cmdk.addEventListener('click', function () {
|
|
122
|
+
handlers.onSearch();
|
|
123
|
+
});
|
|
124
|
+
rail.appendChild(cmdk);
|
|
125
|
+
|
|
126
|
+
var links = {};
|
|
127
|
+
for (var g = 0; g < NAV_GROUPS.length; g += 1) {
|
|
128
|
+
var grp = NAV_GROUPS[g];
|
|
129
|
+
rail.appendChild(dom.el('div', 'rail-h', grp.title));
|
|
130
|
+
var nav = dom.el('nav', 'rail-nav');
|
|
131
|
+
for (var i = 0; i < grp.items.length; i += 1) {
|
|
132
|
+
nav.appendChild(buildLink(grp.items[i], handlers, links));
|
|
133
|
+
}
|
|
134
|
+
rail.appendChild(nav);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
el: rail,
|
|
139
|
+
setActive: function (view) {
|
|
140
|
+
for (var key in links) {
|
|
141
|
+
if (!Object.prototype.hasOwnProperty.call(links, key)) continue;
|
|
142
|
+
if (key === view) links[key].classList.add('on');
|
|
143
|
+
else links[key].classList.remove('on');
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildLink(item, handlers, links) {
|
|
150
|
+
var a = dom.el('button', 'rail-link');
|
|
151
|
+
a.type = 'button';
|
|
152
|
+
a.appendChild(dom.el('span', 'rail-glyph', item.glyph));
|
|
153
|
+
a.appendChild(dom.el('span', null, item.label));
|
|
154
|
+
a.addEventListener('click', function () {
|
|
155
|
+
handlers.onNavigate(item.view);
|
|
156
|
+
});
|
|
157
|
+
links[item.view] = a;
|
|
158
|
+
return a;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function buildTopbar(data, handlers) {
|
|
162
|
+
var bar = dom.el('div', 'app-topbar');
|
|
163
|
+
|
|
164
|
+
var live = dom.el('span', 'topbar-live');
|
|
165
|
+
var dot = dom.el('span', 'topbar-dot');
|
|
166
|
+
live.appendChild(dot);
|
|
167
|
+
var liveText = dom.el(
|
|
168
|
+
'span',
|
|
169
|
+
null,
|
|
170
|
+
!Yg.consumer.isServed()
|
|
171
|
+
? 'Exported snapshot — re-run yg portal for a live view'
|
|
172
|
+
: data.meta.writeEnabled
|
|
173
|
+
? 'Live · re-checks free on refresh'
|
|
174
|
+
: 'View-only',
|
|
175
|
+
);
|
|
176
|
+
live.appendChild(liveText);
|
|
177
|
+
bar.appendChild(live);
|
|
178
|
+
|
|
179
|
+
var crumb = dom.el('span', 'topbar-crumb', '');
|
|
180
|
+
bar.appendChild(crumb);
|
|
181
|
+
|
|
182
|
+
var spacer = dom.el('span', 'topbar-spacer');
|
|
183
|
+
bar.appendChild(spacer);
|
|
184
|
+
|
|
185
|
+
// Refresh and Approve are live ONLY when the page is served by `yg portal`. On a static
|
|
186
|
+
// export they cannot reach the server, so they are disabled UP FRONT with a plain reason
|
|
187
|
+
// (never click-to-discover), and the page never presents a write it cannot perform.
|
|
188
|
+
var served = Yg.consumer.isServed();
|
|
189
|
+
var staticReason = 'Available only when the portal is served by `yg portal` — this is a read-only export.';
|
|
190
|
+
|
|
191
|
+
var refresh = dom.el('button', 'btn topbar-refresh');
|
|
192
|
+
refresh.type = 'button';
|
|
193
|
+
refresh.appendChild(dom.el('span', null, '↻ Refresh'));
|
|
194
|
+
refresh.appendChild(dom.el('span', 'btn-free', 'free'));
|
|
195
|
+
refresh.addEventListener('click', function () {
|
|
196
|
+
handlers.onRefresh();
|
|
197
|
+
});
|
|
198
|
+
if (!served) {
|
|
199
|
+
refresh.disabled = true;
|
|
200
|
+
refresh.title = staticReason;
|
|
201
|
+
refresh.setAttribute('aria-label', 'Refresh — ' + staticReason);
|
|
202
|
+
}
|
|
203
|
+
bar.appendChild(refresh);
|
|
204
|
+
|
|
205
|
+
var approve = dom.el('button', 'btn primary topbar-approve', '✓ Approve');
|
|
206
|
+
approve.type = 'button';
|
|
207
|
+
approve.disabled = !data.meta.writeEnabled || !served;
|
|
208
|
+
if (approve.disabled) {
|
|
209
|
+
var approveReason = !served ? staticReason : 'View-only mode (--no-write) — approving is disabled.';
|
|
210
|
+
approve.title = approveReason;
|
|
211
|
+
approve.setAttribute('aria-label', '✓ Approve — ' + approveReason);
|
|
212
|
+
}
|
|
213
|
+
approve.addEventListener('click', function () {
|
|
214
|
+
handlers.onApprove();
|
|
215
|
+
});
|
|
216
|
+
bar.appendChild(approve);
|
|
217
|
+
|
|
218
|
+
var search = dom.el('button', 'btn topbar-search', '⌕');
|
|
219
|
+
search.type = 'button';
|
|
220
|
+
search.setAttribute('aria-label', 'Open command palette');
|
|
221
|
+
search.addEventListener('click', function () {
|
|
222
|
+
handlers.onSearch();
|
|
223
|
+
});
|
|
224
|
+
bar.appendChild(search);
|
|
225
|
+
|
|
226
|
+
var theme = dom.el('button', 'btn topbar-theme', '◐');
|
|
227
|
+
theme.type = 'button';
|
|
228
|
+
theme.setAttribute('aria-label', 'Toggle light / dark theme');
|
|
229
|
+
theme.addEventListener('click', function () {
|
|
230
|
+
handlers.onTheme();
|
|
231
|
+
});
|
|
232
|
+
bar.appendChild(theme);
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
el: bar,
|
|
236
|
+
setBreadcrumb: function (route) {
|
|
237
|
+
crumb.textContent = breadcrumbText(route);
|
|
238
|
+
},
|
|
239
|
+
setFreshness: function (text, tone) {
|
|
240
|
+
liveText.textContent = text;
|
|
241
|
+
dot.className = 'topbar-dot' + (tone ? ' ' + tone : '');
|
|
242
|
+
},
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function breadcrumbText(route) {
|
|
247
|
+
if (!route) return '';
|
|
248
|
+
if (route.node) return 'Structure / ' + route.node;
|
|
249
|
+
if (route.aspect) return 'Rulebook / ' + route.aspect;
|
|
250
|
+
if (route.flow) return 'Flows / ' + route.flow;
|
|
251
|
+
return labelForView(route.view);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function labelForView(view) {
|
|
255
|
+
for (var g = 0; g < NAV_GROUPS.length; g += 1) {
|
|
256
|
+
for (var i = 0; i < NAV_GROUPS[g].items.length; i += 1) {
|
|
257
|
+
if (NAV_GROUPS[g].items[i].view === view) return NAV_GROUPS[g].items[i].label;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return view || '';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
Yg.shell = { build: build, NAV_GROUPS: NAV_GROUPS, _labelForView: labelForView };
|
|
264
|
+
})();
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The honest-state taxonomy — the spine of the whole portal.
|
|
3
|
+
*
|
|
4
|
+
* Eight distinct render states, each carried by color + GLYPH + label + border-style
|
|
5
|
+
* (never color alone — accessibility), exactly as the visual-foundations reference
|
|
6
|
+
* defines them. "verified" is the ONLY green: a reviewer ran, approved, AND the stored
|
|
7
|
+
* hash still matches the current inputs. The others are deliberately, visibly distinct
|
|
8
|
+
* and must NEVER be collapsed into one "green". This module is the single source of
|
|
9
|
+
* truth a renderer reads — no view re-invents a glyph or a color.
|
|
10
|
+
*
|
|
11
|
+
* Five of these (verified / refused / unverified / no-rule / warning) are the
|
|
12
|
+
* PortalNode.state values the contract enumerates; the remaining three
|
|
13
|
+
* (not-applicable / suppressed / draft) are pair/aspect-level honest states, plus
|
|
14
|
+
* "boundary" for a live undeclared-dependency violation. All eight render here so the
|
|
15
|
+
* legend, the panels, and the cells share one honest vocabulary.
|
|
16
|
+
*/
|
|
17
|
+
(function () {
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
var Yg = (window.YgPortal = window.YgPortal || {});
|
|
21
|
+
|
|
22
|
+
// Glyph + border + plain-language label for every honest state. The plain text is the
|
|
23
|
+
// same wording as the glossary key card, so a tooltip and the legend never diverge.
|
|
24
|
+
var STATE_META = {
|
|
25
|
+
verified: {
|
|
26
|
+
glyph: '✓', // check
|
|
27
|
+
label: 'verified',
|
|
28
|
+
border: 'solid',
|
|
29
|
+
plain: 'A reviewer actually checked this against the current code and it passed. The only green.',
|
|
30
|
+
},
|
|
31
|
+
refused: {
|
|
32
|
+
glyph: '✕', // cross
|
|
33
|
+
label: 'refused',
|
|
34
|
+
border: 'solid',
|
|
35
|
+
plain: 'A reviewer checked it and it broke a rule — with a reason you can read.',
|
|
36
|
+
},
|
|
37
|
+
unverified: {
|
|
38
|
+
glyph: '◌', // dashed circle
|
|
39
|
+
label: 'unverified',
|
|
40
|
+
border: 'dashed',
|
|
41
|
+
plain: "The code changed, so no reviewer has confirmed it yet. Not a pass — just “we don’t know”.",
|
|
42
|
+
},
|
|
43
|
+
'no-rule': {
|
|
44
|
+
glyph: '⊖', // minus-circle
|
|
45
|
+
label: 'no rule',
|
|
46
|
+
border: 'dotted',
|
|
47
|
+
plain: 'Nothing is checking this part. Not broken — just unguarded. Absence of red is not a pass.',
|
|
48
|
+
},
|
|
49
|
+
warning: {
|
|
50
|
+
glyph: '▲', // triangle (advisory)
|
|
51
|
+
label: 'warning',
|
|
52
|
+
border: 'dashed',
|
|
53
|
+
plain: 'An advisory rule flagged this. It does not block — it is signal worth a look, not a failure.',
|
|
54
|
+
},
|
|
55
|
+
'not-applicable': {
|
|
56
|
+
glyph: '–', // en-dash
|
|
57
|
+
label: 'not applicable',
|
|
58
|
+
border: 'none',
|
|
59
|
+
plain: 'A rule was filtered out here on purpose — an empty cell, distinct from unverified.',
|
|
60
|
+
},
|
|
61
|
+
suppressed: {
|
|
62
|
+
glyph: '⛉', // shield-like waiver mark
|
|
63
|
+
label: 'waived',
|
|
64
|
+
border: 'dashed',
|
|
65
|
+
plain: 'Someone told the reviewer to skip a rule here, on purpose, with a reason. Not the same as verified.',
|
|
66
|
+
},
|
|
67
|
+
draft: {
|
|
68
|
+
glyph: '‖', // double bar (paused)
|
|
69
|
+
label: 'draft',
|
|
70
|
+
border: 'dotted',
|
|
71
|
+
plain: 'Parked — removed from the expected set. It verifies nothing while it is a draft.',
|
|
72
|
+
},
|
|
73
|
+
boundary: {
|
|
74
|
+
glyph: '⚡', // zap
|
|
75
|
+
label: 'live boundary',
|
|
76
|
+
border: 'hazard',
|
|
77
|
+
plain: 'An undeclared code dependency, recomputed live right now — never read from the stored lock.',
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// The canonical order the legend renders in (mirrors the foundations reference).
|
|
82
|
+
var STATE_ORDER = [
|
|
83
|
+
'verified',
|
|
84
|
+
'refused',
|
|
85
|
+
'unverified',
|
|
86
|
+
'no-rule',
|
|
87
|
+
'warning',
|
|
88
|
+
'not-applicable',
|
|
89
|
+
'suppressed',
|
|
90
|
+
'draft',
|
|
91
|
+
'boundary',
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
/** Metadata for a state, falling back to no-rule for an unknown value (never throws). */
|
|
95
|
+
function meta(state) {
|
|
96
|
+
return STATE_META[state] || STATE_META['no-rule'];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The glyph for a state (color is carried by the CSS class, never alone). */
|
|
100
|
+
function glyph(state) {
|
|
101
|
+
return meta(state).glyph;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** The plain-language label for a state. */
|
|
105
|
+
function label(state) {
|
|
106
|
+
return meta(state).label;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The plain-language one-line explanation (tooltip text) for a state. */
|
|
110
|
+
function plain(state) {
|
|
111
|
+
return meta(state).plain;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The CSS state class a renderer attaches so color + border come from one place. */
|
|
115
|
+
function cssClass(state) {
|
|
116
|
+
return 'state-' + (STATE_META[state] ? state : 'no-rule');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build an accessible state badge: a glyph element carrying the state color/border (from
|
|
121
|
+
* the CSS class) plus an aria-label of the plain wording, so the state is conveyed by
|
|
122
|
+
* shape AND text, never color alone. Returns a <span> ready to insert.
|
|
123
|
+
*/
|
|
124
|
+
function badge(state) {
|
|
125
|
+
var m = meta(state);
|
|
126
|
+
var node = Yg.dom.el('span', 'state-glyph ' + cssClass(state), m.glyph);
|
|
127
|
+
node.setAttribute('role', 'img');
|
|
128
|
+
node.setAttribute('aria-label', m.label);
|
|
129
|
+
node.setAttribute('title', m.plain);
|
|
130
|
+
return node;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
Yg.states = {
|
|
134
|
+
ORDER: STATE_ORDER,
|
|
135
|
+
meta: meta,
|
|
136
|
+
glyph: glyph,
|
|
137
|
+
label: label,
|
|
138
|
+
plain: plain,
|
|
139
|
+
cssClass: cssClass,
|
|
140
|
+
badge: badge,
|
|
141
|
+
};
|
|
142
|
+
})();
|