@intentius/behold 0.29.6 → 0.29.7
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/AGENTS.md +6 -1
- package/dist/cli.js +42 -5
- package/package.json +1 -1
- package/web/app.js +264 -0
- package/web/index.html +22 -0
- package/web/kiosk-popover.js +234 -0
- package/web/kiosk-popover.test.js +180 -0
- package/web/kiosk-roots.js +191 -0
- package/web/kiosk-roots.test.js +95 -0
- package/web/kiosk-runs.js +29 -3
- package/web/kiosk.html +3 -0
- package/web/kiosk.js +91 -16
- package/web/kiosk.test.js +3 -3
package/AGENTS.md
CHANGED
|
@@ -71,7 +71,12 @@ and none is the checkout's git remote) adds two reads, in
|
|
|
71
71
|
Terraform root of the checkout with the newest `plan`, `drift` and `apply`
|
|
72
72
|
run that held it; a missing stage means no report holds that root, which is
|
|
73
73
|
not "no changes". `cards` maps a card id to its marks (`verdict: drift |
|
|
74
|
-
plan | wave`, terragucci's words, the instance addresses, the run).
|
|
74
|
+
plan | wave`, terragucci's words, the instance addresses, the run). Each
|
|
75
|
+
mark's `values` gives each changed attribute's `before` and `after`, or
|
|
76
|
+
`unknown: true` for a value known only after apply. A sensitive attribute
|
|
77
|
+
is never there, and a value longer than 300 characters of JSON is left out
|
|
78
|
+
with `long: true`. `applied` maps a card id to what its root's newest
|
|
79
|
+
applied wave wrote. The kiosk's popover reads both (behold#571). Nothing
|
|
75
80
|
sets `_status`: a report is never painted as live. `waiting[]` carries each
|
|
76
81
|
wave's roots, destroys, and the `command` a person runs. `estate` is
|
|
77
82
|
terragucci's own counts when the source has `estate.json`. A key such as a
|
package/dist/cli.js
CHANGED
|
@@ -22722,6 +22722,20 @@ var TerragucciReadError = class extends Error {
|
|
|
22722
22722
|
var refuse10 = (error, remedy) => {
|
|
22723
22723
|
throw new TerragucciReadError({ error, code: "terragucci-report", remedy });
|
|
22724
22724
|
};
|
|
22725
|
+
var VALUE_CAP = 300;
|
|
22726
|
+
function attributeValues(a) {
|
|
22727
|
+
const out = { path: a.path };
|
|
22728
|
+
let long = false;
|
|
22729
|
+
for (const k of ["before", "after"]) {
|
|
22730
|
+
if (!(k in a) || a[k] === void 0) continue;
|
|
22731
|
+
const json2 = JSON.stringify(a[k]);
|
|
22732
|
+
if (json2 === void 0 || json2.length > VALUE_CAP) long = true;
|
|
22733
|
+
else out[k] = a[k];
|
|
22734
|
+
}
|
|
22735
|
+
if (a.unknown === true) out.unknown = true;
|
|
22736
|
+
if (long) out.long = true;
|
|
22737
|
+
return out;
|
|
22738
|
+
}
|
|
22725
22739
|
var SCHEMA_FILES = { report: "report.schema.json", index: "report-index.schema.json", estate: "estate.schema.json" };
|
|
22726
22740
|
var SCHEMA_IDS = { report: REPORT_SCHEMA, index: INDEX_SCHEMA, estate: ESTATE_SCHEMA };
|
|
22727
22741
|
var isRecord14 = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
@@ -22897,7 +22911,7 @@ async function readTerragucci(source2, opts = {}) {
|
|
|
22897
22911
|
action: c.action,
|
|
22898
22912
|
...c.type ? { type: c.type } : {},
|
|
22899
22913
|
...c.module ? { module: c.module } : {},
|
|
22900
|
-
attributes: (c.attributes ?? []).filter((a) => !a.sensitive).map(
|
|
22914
|
+
attributes: (c.attributes ?? []).filter((a) => !a.sensitive).map(attributeValues)
|
|
22901
22915
|
})),
|
|
22902
22916
|
...stage === "tf-apply" && row.approval ? { approval: row.approval } : {},
|
|
22903
22917
|
...stage === "tf-apply" && row.applied ? { applied: row.applied } : {}
|
|
@@ -23079,6 +23093,7 @@ function joinTerragucci(read, nodes, roots) {
|
|
|
23079
23093
|
byRoot.get(key).set(n.id.slice(at2 + root.length + 2), n.id);
|
|
23080
23094
|
}
|
|
23081
23095
|
const cards = {};
|
|
23096
|
+
const applied = {};
|
|
23082
23097
|
const unmatchedChanges = [];
|
|
23083
23098
|
const out = [];
|
|
23084
23099
|
const known = new Set(roots.map((r) => r.path));
|
|
@@ -23089,7 +23104,17 @@ function joinTerragucci(read, nodes, roots) {
|
|
|
23089
23104
|
for (const [slot, run3] of [["drift", runs2.drift], ["plan", runs2.plan], ["apply", runs2.apply]]) {
|
|
23090
23105
|
if (!run3) continue;
|
|
23091
23106
|
marks[slot] = verdict(run3);
|
|
23092
|
-
if (slot === "apply" && run3.approval !== "waiting")
|
|
23107
|
+
if (slot === "apply" && run3.approval !== "waiting") {
|
|
23108
|
+
if (run3.applied) {
|
|
23109
|
+
for (const c of run3.changes) {
|
|
23110
|
+
const id = blocks?.get(blockPath(c.address));
|
|
23111
|
+
if (!id || !c.attributes?.length) continue;
|
|
23112
|
+
const a = applied[id] ??= { values: [], run: ref(run3) };
|
|
23113
|
+
for (const v of c.attributes) a.values.push({ ...v, address: c.address });
|
|
23114
|
+
}
|
|
23115
|
+
}
|
|
23116
|
+
continue;
|
|
23117
|
+
}
|
|
23093
23118
|
const kind = slot === "apply" ? "wave" : slot;
|
|
23094
23119
|
const words = slot === "drift" ? DRIFT_WORDS : PLAN_WORDS;
|
|
23095
23120
|
const grouped = /* @__PURE__ */ new Map();
|
|
@@ -23102,9 +23127,12 @@ function joinTerragucci(read, nodes, roots) {
|
|
|
23102
23127
|
continue;
|
|
23103
23128
|
}
|
|
23104
23129
|
const k = `${id}\0${c.action}`;
|
|
23105
|
-
const m = grouped.get(k) ?? { verdict: kind, action: c.action, words: said4, addresses: [], attributes: [], run: ref(run3) };
|
|
23130
|
+
const m = grouped.get(k) ?? { verdict: kind, action: c.action, words: said4, addresses: [], attributes: [], values: [], run: ref(run3) };
|
|
23106
23131
|
m.addresses.push(c.address);
|
|
23107
|
-
for (const a of c.attributes ?? [])
|
|
23132
|
+
for (const a of c.attributes ?? []) {
|
|
23133
|
+
if (!m.attributes.includes(a.path)) m.attributes.push(a.path);
|
|
23134
|
+
m.values.push({ ...a, address: c.address });
|
|
23135
|
+
}
|
|
23108
23136
|
grouped.set(k, m);
|
|
23109
23137
|
}
|
|
23110
23138
|
for (const [k, m] of grouped) (cards[k.slice(0, k.indexOf("\0"))] ??= []).push(m);
|
|
@@ -23115,7 +23143,8 @@ function joinTerragucci(read, nodes, roots) {
|
|
|
23115
23143
|
cards,
|
|
23116
23144
|
roots: out,
|
|
23117
23145
|
unmatched: { roots: Object.keys(read.roots).filter((p) => !known.has(p)).sort(), changes: unmatchedChanges },
|
|
23118
|
-
waiting: read.waiting
|
|
23146
|
+
waiting: read.waiting,
|
|
23147
|
+
applied
|
|
23119
23148
|
};
|
|
23120
23149
|
}
|
|
23121
23150
|
|
|
@@ -27337,6 +27366,14 @@ function createApp(cfg, broadcaster = new Broadcaster(), frames = new FrameBuffe
|
|
|
27337
27366
|
// #571: the repo declares its roots as components, so the page opens on
|
|
27338
27367
|
// the components zoom (web/app.js `bootZoom`).
|
|
27339
27368
|
...servedTerragucciComponents() ? { terragucciComponents: true } : {},
|
|
27369
|
+
// behold#571: the declared roots and the wave jobs that apply them, so
|
|
27370
|
+
// the kiosk gives each root its colour and ties a job row to its roots.
|
|
27371
|
+
...(() => {
|
|
27372
|
+
const decl = servedTerragucciComponents();
|
|
27373
|
+
if (!decl) return {};
|
|
27374
|
+
const waves = decl.pipeline.jobs.filter((j) => typeof j.wave === "number").map((j) => ({ job: j.name, wave: j.wave, roots: [...j.applies] }));
|
|
27375
|
+
return { terragucciRoots: { roots: Object.keys(decl.roots).sort(), waves } };
|
|
27376
|
+
})(),
|
|
27340
27377
|
// #517: a SQL Yodeler repo is migrated by its pipeline; same contract.
|
|
27341
27378
|
...(() => {
|
|
27342
27379
|
const r = guardedRepo();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/behold",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.7",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "behold — a live control plane on chant. See your whole estate (every substrate in one graph), coloured by drift; act through delegated, gated Ops.",
|
package/web/app.js
CHANGED
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
import { createRefreshQueue } from "./refresh-queue.js";
|
|
12
12
|
import { readViewQuery, writeViewQuery, settleView, settlePlace, refusalLine } from "./view-url.js";
|
|
13
13
|
import { quietCardLines, readEmbed, selectMessage, viewFromMessage } from "./embed.js";
|
|
14
|
+
import { WORDS as POP_WORDS, cardRoot as popCardRoot, placePopover, popReduce, popState, resourcePopover, rootPopover } from "./kiosk-popover.js";
|
|
15
|
+
import { cssString, hoverCss, hoverFromMessage, hoverLinks, hoverMessage, isLightBg, rootColour, rootText } from "./kiosk-roots.js";
|
|
14
16
|
import { gateCards } from "./workspace-gates.js";
|
|
15
17
|
import { cornerOf, estateLine, hrefOf as tgHref, legend as tgLegend, markLine, rootRows as tgRootRows, waveCards } from "./terragucci.js";
|
|
16
18
|
import { laneElement as tgLane, rootOfCard as tgRootOfCard, historyElement as tgHistoryLane } from "./terragucci-timeline.js";
|
|
@@ -3378,6 +3380,7 @@ let tgTimeline = null; // #506: /api/terragucci/timeline, the audit record's lan
|
|
|
3378
3380
|
let tgHistory = null; // #522: /api/lifecycle/history, the gate ledgers' history
|
|
3379
3381
|
async function initTerragucci() {
|
|
3380
3382
|
const project = await apiFetch("/api/project").then((r) => r.json()).catch(() => ({}));
|
|
3383
|
+
kioskRoots = project.terragucciRoots || null; // behold#571
|
|
3381
3384
|
if (!project.terragucciReports) return;
|
|
3382
3385
|
await loadTerragucci();
|
|
3383
3386
|
// #505: the chant/lifecycle lane, and what moved while this page is open.
|
|
@@ -3485,6 +3488,7 @@ function paintTgProgress() {
|
|
|
3485
3488
|
// merged. Not live; redrawn on each render and each read.
|
|
3486
3489
|
const TG_TONE = { plan: "var(--pending)", wait: "var(--foreign)", busy: "var(--runtime)", ok: "var(--managed)", bad: "var(--degraded)", idle: "var(--neutral)" };
|
|
3487
3490
|
function drawTgReads() {
|
|
3491
|
+
if (embedOpts.bare) queueMicrotask(kioskDecorate); // behold#571: once this draw is done
|
|
3488
3492
|
watchTgRuns(); // #571: the wave boxes' jobs, while the components zoom is on screen
|
|
3489
3493
|
const svg = document.querySelector("#graph svg");
|
|
3490
3494
|
if (!svg) return;
|
|
@@ -4083,6 +4087,266 @@ function markTerragucciCards() {
|
|
|
4083
4087
|
}
|
|
4084
4088
|
drawTgReads(); // #568
|
|
4085
4089
|
}
|
|
4090
|
+
// --- behold#571: the kiosk frame's popover, root colours and hover links -------
|
|
4091
|
+
// The kiosk frames this page bare, with no Inspect pane. Pointing at a card
|
|
4092
|
+
// shows a popover beside it (web/kiosk-popover.js says what it holds); a
|
|
4093
|
+
// click keeps it open. Each root's name carries a dot in its own colour, and
|
|
4094
|
+
// hovering a card or box here lights its root and wave in the kiosk's other
|
|
4095
|
+
// panes, and theirs light it here (`behold:hover`, web/kiosk-roots.js).
|
|
4096
|
+
// Nothing happens until someone points: a TV with no pointer sees the page
|
|
4097
|
+
// as before.
|
|
4098
|
+
let kioskRoots = null; // /api/project's terragucciRoots: { roots, waves }
|
|
4099
|
+
let kioskPop = popState();
|
|
4100
|
+
let kioskHover = null; // this frame's pointer: { root } | { wave } | null
|
|
4101
|
+
let kioskHostHover = null; // the kiosk's pointer, from its message
|
|
4102
|
+
function kioskRootList() {
|
|
4103
|
+
if (kioskRoots && Array.isArray(kioskRoots.roots) && kioskRoots.roots.length) return kioskRoots.roots;
|
|
4104
|
+
return tgAnswer && Array.isArray(tgAnswer.roots) ? tgAnswer.roots.map((r) => r.path) : [];
|
|
4105
|
+
}
|
|
4106
|
+
function kioskWaves() {
|
|
4107
|
+
return (kioskRoots && kioskRoots.waves) || [];
|
|
4108
|
+
}
|
|
4109
|
+
function kioskLight() {
|
|
4110
|
+
return isLightBg(getComputedStyle(document.documentElement).getPropertyValue("--bg"));
|
|
4111
|
+
}
|
|
4112
|
+
function kioskCompMode() {
|
|
4113
|
+
return !!(view.components && terragucciComponentsRepo && tgComponentFacets);
|
|
4114
|
+
}
|
|
4115
|
+
function kioskRootOfComponent(c) {
|
|
4116
|
+
const m = (tgComponentFacets && tgComponentFacets.roots) || {};
|
|
4117
|
+
return m[c] || c;
|
|
4118
|
+
}
|
|
4119
|
+
/** The terragucci root (its path) of a resources-zoom box name. */
|
|
4120
|
+
function kioskRootOfBox(name) {
|
|
4121
|
+
const r = tgAnswer && Array.isArray(tgAnswer.roots) ? tgAnswer.roots.find((x) => x.name === name) : null;
|
|
4122
|
+
return r ? r.path : name;
|
|
4123
|
+
}
|
|
4124
|
+
/** What the pointer is on: a card's or a box's root, or a wave's box. */
|
|
4125
|
+
function kioskHoverOf(target) {
|
|
4126
|
+
if (!target || !target.closest) return null;
|
|
4127
|
+
const card = target.closest("[data-node-id]");
|
|
4128
|
+
if (card) {
|
|
4129
|
+
const id = card.getAttribute("data-node-id");
|
|
4130
|
+
if (kioskCompMode()) return { root: kioskRootOfComponent(id) };
|
|
4131
|
+
const at = popCardRoot(tgAnswer, id);
|
|
4132
|
+
return at ? { root: at.path } : null;
|
|
4133
|
+
}
|
|
4134
|
+
const rect = target.closest("rect[data-group-id]");
|
|
4135
|
+
if (!rect) return null;
|
|
4136
|
+
const g = rect.getAttribute("data-group-id") || "";
|
|
4137
|
+
const w = /^wave-(\d+)$/.exec(g);
|
|
4138
|
+
if (w) return { wave: Number(w[1]) };
|
|
4139
|
+
const root = kioskRootOfBox(g);
|
|
4140
|
+
return kioskRootList().includes(root) ? { root } : null;
|
|
4141
|
+
}
|
|
4142
|
+
const sameHover = (a, b) => (a && a.root) === (b && b.root) && (a && a.wave) === (b && b.wave);
|
|
4143
|
+
function kioskSetHover(h) {
|
|
4144
|
+
if (sameHover(h, kioskHover)) return;
|
|
4145
|
+
kioskHover = h;
|
|
4146
|
+
kioskGlow();
|
|
4147
|
+
if (!embedOpts.host || window.parent === window) return;
|
|
4148
|
+
try {
|
|
4149
|
+
window.parent.postMessage(hoverMessage(h), embedOpts.host);
|
|
4150
|
+
} catch {
|
|
4151
|
+
/* the parent went away */
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
/** Light what the two pointers link: a root's box (or card) and a wave's box, with one style element that survives a redraw. */
|
|
4155
|
+
function kioskGlow() {
|
|
4156
|
+
let style = document.getElementById("k-hover-style");
|
|
4157
|
+
if (!style) {
|
|
4158
|
+
style = document.createElement("style");
|
|
4159
|
+
style.id = "k-hover-style";
|
|
4160
|
+
document.head.appendChild(style);
|
|
4161
|
+
}
|
|
4162
|
+
const a = hoverLinks(kioskHover, kioskWaves());
|
|
4163
|
+
const b = hoverLinks(kioskHostHover, kioskWaves());
|
|
4164
|
+
const links = { roots: [...new Set([...a.roots, ...b.roots])], waves: [...new Set([...a.waves, ...b.waves])] };
|
|
4165
|
+
const boxes = (root) => {
|
|
4166
|
+
const names = new Set([root]);
|
|
4167
|
+
const r = tgAnswer && Array.isArray(tgAnswer.roots) ? tgAnswer.roots.find((x) => x.path === root || x.name === root) : null;
|
|
4168
|
+
if (r) names.add(r.name);
|
|
4169
|
+
const comps = tgComponentFacets && tgComponentFacets.roots ? Object.keys(tgComponentFacets.roots).filter((c) => tgComponentFacets.roots[c] === root) : [];
|
|
4170
|
+
for (const c of comps) names.add(c);
|
|
4171
|
+
return [...names].map((n) => `#graph rect[data-group-id=${cssString(n)}], #graph g[data-node-id=${cssString(n)}]`).join(", ");
|
|
4172
|
+
};
|
|
4173
|
+
style.textContent = hoverCss(links, { root: boxes, wave: (w) => `#graph rect[data-group-id="wave-${w}"]` }, "filter: drop-shadow(0 0 2px var(--focus)) drop-shadow(0 0 7px var(--focus));");
|
|
4174
|
+
}
|
|
4175
|
+
/** The dots by each root's name: on its box's title (Resources) or its card (Roots by wave). */
|
|
4176
|
+
function kioskDots(svg) {
|
|
4177
|
+
for (const d of svg.querySelectorAll("[data-k-dot]")) d.remove();
|
|
4178
|
+
const roots = kioskRootList();
|
|
4179
|
+
if (!roots.length) return;
|
|
4180
|
+
const light = kioskLight();
|
|
4181
|
+
const dotBy = (text, root) => {
|
|
4182
|
+
const colour = rootColour(root, roots, { light });
|
|
4183
|
+
if (!colour) return;
|
|
4184
|
+
let bb;
|
|
4185
|
+
try {
|
|
4186
|
+
bb = text.getBBox();
|
|
4187
|
+
} catch {
|
|
4188
|
+
return;
|
|
4189
|
+
}
|
|
4190
|
+
if (!bb.width) return;
|
|
4191
|
+
const c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
4192
|
+
c.setAttribute("data-k-dot", "1");
|
|
4193
|
+
// After the name: before it sits the card's icon.
|
|
4194
|
+
c.setAttribute("cx", String(bb.x + bb.width + 9));
|
|
4195
|
+
c.setAttribute("cy", String(bb.y + bb.height / 2));
|
|
4196
|
+
c.setAttribute("r", "5.5");
|
|
4197
|
+
c.setAttribute("fill", colour);
|
|
4198
|
+
c.setAttribute("pointer-events", "none");
|
|
4199
|
+
text.parentNode.insertBefore(c, text.nextSibling);
|
|
4200
|
+
};
|
|
4201
|
+
if (kioskCompMode()) {
|
|
4202
|
+
for (const card of svg.querySelectorAll("[data-node-id]")) {
|
|
4203
|
+
const id = card.getAttribute("data-node-id");
|
|
4204
|
+
const t = [...card.querySelectorAll("text")].find((x) => (x.textContent || "").trim() === id);
|
|
4205
|
+
if (t) dotBy(t, kioskRootOfComponent(id));
|
|
4206
|
+
}
|
|
4207
|
+
return;
|
|
4208
|
+
}
|
|
4209
|
+
for (const rect of svg.querySelectorAll("rect[data-group-id]")) {
|
|
4210
|
+
const name = rect.getAttribute("data-group-id");
|
|
4211
|
+
const root = kioskRootOfBox(name);
|
|
4212
|
+
if (!roots.includes(root)) continue;
|
|
4213
|
+
const t = [...(rect.parentNode ? rect.parentNode.querySelectorAll(":scope > text") : [])].find((x) => (x.textContent || "").trim() === name && x.getAttribute("visibility") !== "hidden");
|
|
4214
|
+
if (t) dotBy(t, root);
|
|
4215
|
+
}
|
|
4216
|
+
}
|
|
4217
|
+
function kioskDecorate() {
|
|
4218
|
+
const svg = document.querySelector("#graph svg");
|
|
4219
|
+
if (!svg) return;
|
|
4220
|
+
kioskDots(svg);
|
|
4221
|
+
kioskGlow();
|
|
4222
|
+
kioskPopRender();
|
|
4223
|
+
}
|
|
4224
|
+
function kioskPopEvent(ev) {
|
|
4225
|
+
const next = popReduce(kioskPop, ev, Date.now());
|
|
4226
|
+
if (next === kioskPop) return;
|
|
4227
|
+
kioskPop = next;
|
|
4228
|
+
kioskPopRender();
|
|
4229
|
+
}
|
|
4230
|
+
function kioskPopRender() {
|
|
4231
|
+
let pop = document.getElementById("k-pop");
|
|
4232
|
+
const svg = document.querySelector("#graph svg");
|
|
4233
|
+
const id = kioskPop.shown;
|
|
4234
|
+
const card = id && svg ? svg.querySelector(`[data-node-id="${CSS.escape(id)}"]`) : null;
|
|
4235
|
+
const answer = tgAnswerDrawn();
|
|
4236
|
+
const content = !card || !answer ? null : kioskCompMode() ? rootPopover(answer, kioskRootOfComponent(id), resourcesByComponent[id]) : resourcePopover(answer, id);
|
|
4237
|
+
if (!content) {
|
|
4238
|
+
if (pop) pop.hidden = true;
|
|
4239
|
+
return;
|
|
4240
|
+
}
|
|
4241
|
+
if (!pop) {
|
|
4242
|
+
pop = document.createElement("div");
|
|
4243
|
+
pop.id = "k-pop";
|
|
4244
|
+
pop.setAttribute("role", "tooltip");
|
|
4245
|
+
document.body.appendChild(pop);
|
|
4246
|
+
}
|
|
4247
|
+
pop.replaceChildren();
|
|
4248
|
+
pop.classList.toggle("k-pinned", kioskPop.pinned);
|
|
4249
|
+
const roots = kioskRootList();
|
|
4250
|
+
const light = kioskLight();
|
|
4251
|
+
const add = (tag, cls, text) => {
|
|
4252
|
+
const n = document.createElement(tag);
|
|
4253
|
+
n.className = cls;
|
|
4254
|
+
if (text !== undefined) n.textContent = text;
|
|
4255
|
+
pop.appendChild(n);
|
|
4256
|
+
return n;
|
|
4257
|
+
};
|
|
4258
|
+
const list = (rows) => {
|
|
4259
|
+
const ul = add("ul", "k-pop-lines");
|
|
4260
|
+
for (const r of rows) {
|
|
4261
|
+
const li = document.createElement("li");
|
|
4262
|
+
li.className = r.tone === "unknown" ? "k-pop-unknown" : "";
|
|
4263
|
+
li.textContent = r.text;
|
|
4264
|
+
ul.appendChild(li);
|
|
4265
|
+
}
|
|
4266
|
+
return ul;
|
|
4267
|
+
};
|
|
4268
|
+
if (content.address !== undefined) {
|
|
4269
|
+
add("div", "k-pop-addr", content.address);
|
|
4270
|
+
add("div", "k-pop-root").appendChild(rootText(document, `It sits in root ${content.root}.`, roots, { light }));
|
|
4271
|
+
add("p", "k-pop-head", content.head);
|
|
4272
|
+
if (content.lines.length) {
|
|
4273
|
+
const ul = list(content.lines);
|
|
4274
|
+
if (content.more) ul.appendChild(Object.assign(document.createElement("li"), { className: "k-pop-more", textContent: `and ${content.more} more` }));
|
|
4275
|
+
}
|
|
4276
|
+
if (content.applied) {
|
|
4277
|
+
add("p", "k-pop-note", content.applied.when);
|
|
4278
|
+
list(content.applied.lines);
|
|
4279
|
+
} else if (content.note) add("p", "k-pop-note", content.note);
|
|
4280
|
+
} else {
|
|
4281
|
+
add("div", "k-pop-addr").appendChild(rootText(document, content.root, roots, { light }));
|
|
4282
|
+
add("p", "k-pop-head", content.head);
|
|
4283
|
+
const ul = add("ul", "k-pop-lines");
|
|
4284
|
+
for (const r of content.rows) {
|
|
4285
|
+
const li = document.createElement("li");
|
|
4286
|
+
li.className = "k-pop-res";
|
|
4287
|
+
const st = document.createElement("span");
|
|
4288
|
+
st.className = `k-pop-state k-pop-${r.state}`;
|
|
4289
|
+
st.textContent = r.words;
|
|
4290
|
+
li.append(document.createTextNode(`${r.address} `), st);
|
|
4291
|
+
ul.appendChild(li);
|
|
4292
|
+
}
|
|
4293
|
+
if (content.more > 0) ul.appendChild(Object.assign(document.createElement("li"), { className: "k-pop-more", textContent: `and ${content.more} more` }));
|
|
4294
|
+
}
|
|
4295
|
+
add("p", "k-pop-hint", kioskPop.pinned ? POP_WORDS.pinnedHint : POP_WORDS.pinHint);
|
|
4296
|
+
pop.hidden = false;
|
|
4297
|
+
const cb = card.getBoundingClientRect();
|
|
4298
|
+
const pb = pop.getBoundingClientRect();
|
|
4299
|
+
const at = placePopover({ left: cb.left, top: cb.top, width: cb.width, height: cb.height }, { width: pb.width, height: pb.height }, { width: window.innerWidth, height: window.innerHeight });
|
|
4300
|
+
pop.style.left = `${Math.round(at.left)}px`;
|
|
4301
|
+
pop.style.top = `${Math.round(at.top)}px`;
|
|
4302
|
+
}
|
|
4303
|
+
/** Wire the frame's pointer once: the popover, and the hover links to the kiosk. Bare frames only. */
|
|
4304
|
+
function wireKioskFrame() {
|
|
4305
|
+
if (!embedOpts.bare) return;
|
|
4306
|
+
const graph = document.getElementById("graph");
|
|
4307
|
+
if (!graph) return;
|
|
4308
|
+
graph.addEventListener("pointerover", (e) => {
|
|
4309
|
+
const card = e.target.closest && e.target.closest("[data-node-id]");
|
|
4310
|
+
if (card) {
|
|
4311
|
+
// The card's own tooltips would sit over the popover.
|
|
4312
|
+
for (const t of card.querySelectorAll("title")) t.remove();
|
|
4313
|
+
kioskPopEvent({ type: "enter", id: card.getAttribute("data-node-id") });
|
|
4314
|
+
}
|
|
4315
|
+
kioskSetHover(kioskHoverOf(e.target));
|
|
4316
|
+
});
|
|
4317
|
+
graph.addEventListener("pointerout", (e) => {
|
|
4318
|
+
const card = e.target.closest && e.target.closest("[data-node-id]");
|
|
4319
|
+
const into = e.relatedTarget;
|
|
4320
|
+
if (card && !(into && card.contains(into))) kioskPopEvent({ type: "leave", id: card.getAttribute("data-node-id") });
|
|
4321
|
+
if (!into || !graph.contains(into)) kioskSetHover(null);
|
|
4322
|
+
});
|
|
4323
|
+
graph.addEventListener(
|
|
4324
|
+
"click",
|
|
4325
|
+
(e) => {
|
|
4326
|
+
const card = e.target.closest && e.target.closest("[data-node-id]");
|
|
4327
|
+
if (card) kioskPopEvent({ type: "click", id: card.getAttribute("data-node-id") });
|
|
4328
|
+
else kioskPopEvent({ type: "outside" });
|
|
4329
|
+
},
|
|
4330
|
+
true,
|
|
4331
|
+
);
|
|
4332
|
+
document.addEventListener("click", (e) => {
|
|
4333
|
+
if (!graph.contains(e.target) && !(e.target.closest && e.target.closest("#k-pop"))) kioskPopEvent({ type: "outside" });
|
|
4334
|
+
});
|
|
4335
|
+
document.addEventListener("keydown", (e) => {
|
|
4336
|
+
if (e.key === "Escape") kioskPopEvent({ type: "escape" });
|
|
4337
|
+
});
|
|
4338
|
+
setInterval(() => kioskPopEvent({ type: "tick" }), 1000);
|
|
4339
|
+
window.addEventListener("message", (e) => {
|
|
4340
|
+
if (!embedOpts.host || e.origin !== embedOpts.host) return;
|
|
4341
|
+
if (e.data && e.data.type === "behold:unpin") return kioskPopEvent({ type: "outside" });
|
|
4342
|
+
const h = hoverFromMessage(e.data);
|
|
4343
|
+
if (h === undefined) return;
|
|
4344
|
+
kioskHostHover = h;
|
|
4345
|
+
kioskGlow();
|
|
4346
|
+
});
|
|
4347
|
+
}
|
|
4348
|
+
wireKioskFrame();
|
|
4349
|
+
|
|
4086
4350
|
function markAge(marks) {
|
|
4087
4351
|
const newest = marks.map((m) => m.run.finished).sort().pop();
|
|
4088
4352
|
const s = Math.max(0, (Date.now() - Date.parse(newest)) / 1000);
|
package/web/index.html
CHANGED
|
@@ -419,6 +419,28 @@
|
|
|
419
419
|
.bare #app { grid-template-columns: 1fr 0 !important; }
|
|
420
420
|
.bare #graph svg > rect[fill^="url(#pin-"] { display: none; }
|
|
421
421
|
.bare #graph svg { cursor: default; }
|
|
422
|
+
/* behold#571: the marks drawn over a card let the pointer through to it, so pointing anywhere on the card opens its popover. */
|
|
423
|
+
.bare #graph text[data-tg-root], .bare #graph g[data-tg-root] { pointer-events: none; }
|
|
424
|
+
/* behold#571: the kiosk frame's popover, beside the card it describes. */
|
|
425
|
+
#k-pop { position: fixed; z-index: 50; max-width: min(30em, 46vw); padding: 0.7em 0.9em 0.6em; background: var(--panel); color: var(--fg);
|
|
426
|
+
border: 1px solid var(--rule); border-radius: 8px; box-shadow: 0 6px 22px color-mix(in srgb, var(--shadow) 70%, transparent);
|
|
427
|
+
font: 14px/1.4 var(--font-ui); pointer-events: auto; }
|
|
428
|
+
#k-pop[hidden] { display: none; }
|
|
429
|
+
#k-pop.k-pinned { border-color: var(--focus); }
|
|
430
|
+
#k-pop p { margin: 0.25em 0; }
|
|
431
|
+
.k-pop-addr { font: 700 1.05em var(--font-mono); overflow-wrap: anywhere; }
|
|
432
|
+
.k-pop-root { color: var(--muted); font-size: 0.92em; }
|
|
433
|
+
.k-pop-head { font-weight: 600; }
|
|
434
|
+
.k-pop-lines { list-style: none; margin: 0.2em 0 0.3em; padding: 0.35em 0.6em; background: var(--well); border-radius: 6px; font: 0.95em/1.45 var(--font-mono); }
|
|
435
|
+
.k-pop-lines li { overflow-wrap: anywhere; }
|
|
436
|
+
.k-pop-unknown, .k-pop-unknown.k-pop-state { color: var(--foreign); }
|
|
437
|
+
.k-pop-more { color: var(--muted); }
|
|
438
|
+
.k-pop-note { color: var(--muted); font-size: 0.92em; }
|
|
439
|
+
.k-pop-hint { color: var(--muted); font-size: 0.85em; border-top: 1px solid var(--rule); padding-top: 0.35em; margin-top: 0.4em !important; }
|
|
440
|
+
.k-pop-state { margin-left: 0.4em; font-family: var(--font-ui); font-weight: 600; white-space: nowrap; }
|
|
441
|
+
.k-pop-change { color: var(--pending); }
|
|
442
|
+
.k-pop-same { color: var(--muted); }
|
|
443
|
+
.k-root::before { content: ""; display: inline-block; width: 0.6em; height: 0.6em; border-radius: 50%; background: var(--k-root); margin: 0 0.3em 0.1em 0.05em; vertical-align: middle; }
|
|
422
444
|
#panel-tabs #hintk { flex: none; align-self: center; padding: 2px 6px; margin-left: 2px; }
|
|
423
445
|
#panel-collapse { flex: none; align-self: center; background: none; border: none; color: var(--muted);
|
|
424
446
|
font-size: var(--t-body); cursor: pointer; line-height: 1; padding: 2px 4px; }
|