@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 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). Nothing
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((a) => ({ path: a.path }))
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") continue;
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 ?? []) if (!m.attributes.includes(a.path)) m.attributes.push(a.path);
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.6",
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; }