@intentius/behold 0.29.6 → 0.29.8
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/README.md +7 -0
- package/dist/cli.js +1263 -770
- package/package.json +1 -1
- package/web/app.js +320 -0
- package/web/index.html +24 -0
- package/web/kiosk-annotations.js +528 -0
- package/web/kiosk-annotations.test.js +83 -0
- package/web/kiosk-approval.js +241 -0
- package/web/kiosk-approval.test.js +122 -0
- package/web/kiosk-popover.js +235 -0
- package/web/kiosk-popover.test.js +180 -0
- package/web/kiosk-repo.js +25 -0
- package/web/kiosk-repo.test.js +43 -0
- package/web/kiosk-roots.js +199 -0
- package/web/kiosk-roots.test.js +95 -0
- package/web/kiosk-runs.js +365 -23
- package/web/kiosk-runs.test.js +77 -1
- package/web/kiosk-vendor.js +380 -0
- package/web/kiosk-vendor.test.js +265 -0
- package/web/kiosk.html +160 -17
- package/web/kiosk.js +536 -59
- package/web/kiosk.test.js +76 -11
- package/web/theme.js +112 -0
- package/web/theme.test.js +51 -1
- package/web/vendors/choudoufu-128.png +0 -0
- package/web/vendors/choudoufu-64.png +0 -0
- package/web/view-repo.js +44 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/behold",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.8",
|
|
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
|
@@ -8,9 +8,12 @@
|
|
|
8
8
|
// floating control panel's chrome (panel.js — drag/snap/collapse/tabs, persisted
|
|
9
9
|
// position), and the theme picker into the panel's View-tab slot (a stable element
|
|
10
10
|
// renderPanelView never rewrites, so the select mounts once and survives re-renders).
|
|
11
|
+
import "./view-repo.js"; // behold#571: first, so every read carries the page's ?repo=
|
|
11
12
|
import { createRefreshQueue } from "./refresh-queue.js";
|
|
12
13
|
import { readViewQuery, writeViewQuery, settleView, settlePlace, refusalLine } from "./view-url.js";
|
|
13
14
|
import { quietCardLines, readEmbed, selectMessage, viewFromMessage } from "./embed.js";
|
|
15
|
+
import { WORDS as POP_WORDS, cardRoot as popCardRoot, placePopover, popReduce, popState, resourcePopover, rootPopover } from "./kiosk-popover.js";
|
|
16
|
+
import { cssString, hoverCss, hoverFromMessage, hoverLinks, hoverMessage, isLightBg, rootPaint, rootText } from "./kiosk-roots.js";
|
|
14
17
|
import { gateCards } from "./workspace-gates.js";
|
|
15
18
|
import { cornerOf, estateLine, hrefOf as tgHref, legend as tgLegend, markLine, rootRows as tgRootRows, waveCards } from "./terragucci.js";
|
|
16
19
|
import { laneElement as tgLane, rootOfCard as tgRootOfCard, historyElement as tgHistoryLane } from "./terragucci-timeline.js";
|
|
@@ -3378,6 +3381,7 @@ let tgTimeline = null; // #506: /api/terragucci/timeline, the audit record's lan
|
|
|
3378
3381
|
let tgHistory = null; // #522: /api/lifecycle/history, the gate ledgers' history
|
|
3379
3382
|
async function initTerragucci() {
|
|
3380
3383
|
const project = await apiFetch("/api/project").then((r) => r.json()).catch(() => ({}));
|
|
3384
|
+
kioskRoots = project.terragucciRoots || null; // behold#571
|
|
3381
3385
|
if (!project.terragucciReports) return;
|
|
3382
3386
|
await loadTerragucci();
|
|
3383
3387
|
// #505: the chant/lifecycle lane, and what moved while this page is open.
|
|
@@ -3485,6 +3489,7 @@ function paintTgProgress() {
|
|
|
3485
3489
|
// merged. Not live; redrawn on each render and each read.
|
|
3486
3490
|
const TG_TONE = { plan: "var(--pending)", wait: "var(--foreign)", busy: "var(--runtime)", ok: "var(--managed)", bad: "var(--degraded)", idle: "var(--neutral)" };
|
|
3487
3491
|
function drawTgReads() {
|
|
3492
|
+
if (embedOpts.bare) queueMicrotask(kioskDecorate); // behold#571: once this draw is done
|
|
3488
3493
|
watchTgRuns(); // #571: the wave boxes' jobs, while the components zoom is on screen
|
|
3489
3494
|
const svg = document.querySelector("#graph svg");
|
|
3490
3495
|
if (!svg) return;
|
|
@@ -4083,6 +4088,271 @@ function markTerragucciCards() {
|
|
|
4083
4088
|
}
|
|
4084
4089
|
drawTgReads(); // #568
|
|
4085
4090
|
}
|
|
4091
|
+
// --- behold#571: the kiosk frame's popover, root colours and hover links -------
|
|
4092
|
+
// The kiosk frames this page bare, with no Inspect pane. Pointing at a card
|
|
4093
|
+
// shows a popover beside it (web/kiosk-popover.js says what it holds); a
|
|
4094
|
+
// click keeps it open. Each root's name carries a dot in its own colour, and
|
|
4095
|
+
// hovering a card or box here lights its root and wave in the kiosk's other
|
|
4096
|
+
// panes, and theirs light it here (`behold:hover`, web/kiosk-roots.js).
|
|
4097
|
+
// Nothing happens until someone points: a TV with no pointer sees the page
|
|
4098
|
+
// as before.
|
|
4099
|
+
let kioskRoots = null; // /api/project's terragucciRoots: { roots, waves }
|
|
4100
|
+
let kioskPop = popState();
|
|
4101
|
+
let kioskHover = null; // this frame's pointer: { root } | { wave } | null
|
|
4102
|
+
let kioskHostHover = null; // the kiosk's pointer, from its message
|
|
4103
|
+
function kioskRootList() {
|
|
4104
|
+
if (kioskRoots && Array.isArray(kioskRoots.roots) && kioskRoots.roots.length) return kioskRoots.roots;
|
|
4105
|
+
return tgAnswer && Array.isArray(tgAnswer.roots) ? tgAnswer.roots.map((r) => r.path) : [];
|
|
4106
|
+
}
|
|
4107
|
+
function kioskWaves() {
|
|
4108
|
+
return (kioskRoots && kioskRoots.waves) || [];
|
|
4109
|
+
}
|
|
4110
|
+
function kioskLight() {
|
|
4111
|
+
return isLightBg(getComputedStyle(document.documentElement).getPropertyValue("--bg"));
|
|
4112
|
+
}
|
|
4113
|
+
function kioskCompMode() {
|
|
4114
|
+
return !!(view.components && terragucciComponentsRepo && tgComponentFacets);
|
|
4115
|
+
}
|
|
4116
|
+
function kioskRootOfComponent(c) {
|
|
4117
|
+
const m = (tgComponentFacets && tgComponentFacets.roots) || {};
|
|
4118
|
+
return m[c] || c;
|
|
4119
|
+
}
|
|
4120
|
+
/** The terragucci root (its path) of a resources-zoom box name. */
|
|
4121
|
+
function kioskRootOfBox(name) {
|
|
4122
|
+
const r = tgAnswer && Array.isArray(tgAnswer.roots) ? tgAnswer.roots.find((x) => x.name === name) : null;
|
|
4123
|
+
return r ? r.path : name;
|
|
4124
|
+
}
|
|
4125
|
+
/** What the pointer is on: a card's or a box's root, or a wave's box. */
|
|
4126
|
+
function kioskHoverOf(target) {
|
|
4127
|
+
if (!target || !target.closest) return null;
|
|
4128
|
+
const card = target.closest("[data-node-id]");
|
|
4129
|
+
if (card) {
|
|
4130
|
+
const id = card.getAttribute("data-node-id");
|
|
4131
|
+
if (kioskCompMode()) return { root: kioskRootOfComponent(id) };
|
|
4132
|
+
const at = popCardRoot(tgAnswer, id);
|
|
4133
|
+
return at ? { root: at.path } : null;
|
|
4134
|
+
}
|
|
4135
|
+
const rect = target.closest("rect[data-group-id]");
|
|
4136
|
+
if (!rect) return null;
|
|
4137
|
+
const g = rect.getAttribute("data-group-id") || "";
|
|
4138
|
+
const w = /^wave-(\d+)$/.exec(g);
|
|
4139
|
+
if (w) return { wave: Number(w[1]) };
|
|
4140
|
+
const root = kioskRootOfBox(g);
|
|
4141
|
+
return kioskRootList().includes(root) ? { root } : null;
|
|
4142
|
+
}
|
|
4143
|
+
const sameHover = (a, b) => (a && a.root) === (b && b.root) && (a && a.wave) === (b && b.wave);
|
|
4144
|
+
function kioskSetHover(h) {
|
|
4145
|
+
if (sameHover(h, kioskHover)) return;
|
|
4146
|
+
kioskHover = h;
|
|
4147
|
+
kioskGlow();
|
|
4148
|
+
if (!embedOpts.host || window.parent === window) return;
|
|
4149
|
+
try {
|
|
4150
|
+
window.parent.postMessage(hoverMessage(h), embedOpts.host);
|
|
4151
|
+
} catch {
|
|
4152
|
+
/* the parent went away */
|
|
4153
|
+
}
|
|
4154
|
+
}
|
|
4155
|
+
/** 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. */
|
|
4156
|
+
function kioskGlow() {
|
|
4157
|
+
let style = document.getElementById("k-hover-style");
|
|
4158
|
+
if (!style) {
|
|
4159
|
+
style = document.createElement("style");
|
|
4160
|
+
style.id = "k-hover-style";
|
|
4161
|
+
document.head.appendChild(style);
|
|
4162
|
+
}
|
|
4163
|
+
const a = hoverLinks(kioskHover, kioskWaves());
|
|
4164
|
+
const b = hoverLinks(kioskHostHover, kioskWaves());
|
|
4165
|
+
const links = { roots: [...new Set([...a.roots, ...b.roots])], waves: [...new Set([...a.waves, ...b.waves])] };
|
|
4166
|
+
const boxes = (root) => {
|
|
4167
|
+
const names = new Set([root]);
|
|
4168
|
+
const r = tgAnswer && Array.isArray(tgAnswer.roots) ? tgAnswer.roots.find((x) => x.path === root || x.name === root) : null;
|
|
4169
|
+
if (r) names.add(r.name);
|
|
4170
|
+
const comps = tgComponentFacets && tgComponentFacets.roots ? Object.keys(tgComponentFacets.roots).filter((c) => tgComponentFacets.roots[c] === root) : [];
|
|
4171
|
+
for (const c of comps) names.add(c);
|
|
4172
|
+
return [...names].map((n) => `#graph rect[data-group-id=${cssString(n)}], #graph g[data-node-id=${cssString(n)}]`).join(", ");
|
|
4173
|
+
};
|
|
4174
|
+
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));");
|
|
4175
|
+
}
|
|
4176
|
+
/** The dots by each root's name: on its box's title (Resources) or its card (Roots by wave). */
|
|
4177
|
+
function kioskDots(svg) {
|
|
4178
|
+
for (const d of svg.querySelectorAll("[data-k-dot]")) d.remove();
|
|
4179
|
+
const roots = kioskRootList();
|
|
4180
|
+
if (!roots.length) return;
|
|
4181
|
+
const light = kioskLight();
|
|
4182
|
+
const dotBy = (text, root) => {
|
|
4183
|
+
const colour = rootPaint(root, roots, { light });
|
|
4184
|
+
if (!colour) return;
|
|
4185
|
+
let bb;
|
|
4186
|
+
try {
|
|
4187
|
+
bb = text.getBBox();
|
|
4188
|
+
} catch {
|
|
4189
|
+
return;
|
|
4190
|
+
}
|
|
4191
|
+
if (!bb.width) return;
|
|
4192
|
+
const c = document.createElementNS("http://www.w3.org/2000/svg", "circle");
|
|
4193
|
+
c.setAttribute("data-k-dot", "1");
|
|
4194
|
+
// After the name: before it sits the card's icon.
|
|
4195
|
+
c.setAttribute("cx", String(bb.x + bb.width + 9));
|
|
4196
|
+
c.setAttribute("cy", String(bb.y + bb.height / 2));
|
|
4197
|
+
c.setAttribute("r", "5.5");
|
|
4198
|
+
// The theme's own root colour (--root-<i>): a style, since a fill attribute takes no var().
|
|
4199
|
+
c.style.fill = colour;
|
|
4200
|
+
c.setAttribute("pointer-events", "none");
|
|
4201
|
+
text.parentNode.insertBefore(c, text.nextSibling);
|
|
4202
|
+
};
|
|
4203
|
+
if (kioskCompMode()) {
|
|
4204
|
+
for (const card of svg.querySelectorAll("[data-node-id]")) {
|
|
4205
|
+
const id = card.getAttribute("data-node-id");
|
|
4206
|
+
const t = [...card.querySelectorAll("text")].find((x) => (x.textContent || "").trim() === id);
|
|
4207
|
+
if (t) dotBy(t, kioskRootOfComponent(id));
|
|
4208
|
+
}
|
|
4209
|
+
return;
|
|
4210
|
+
}
|
|
4211
|
+
for (const rect of svg.querySelectorAll("rect[data-group-id]")) {
|
|
4212
|
+
const name = rect.getAttribute("data-group-id");
|
|
4213
|
+
const root = kioskRootOfBox(name);
|
|
4214
|
+
if (!roots.includes(root)) continue;
|
|
4215
|
+
const t = [...(rect.parentNode ? rect.parentNode.querySelectorAll(":scope > text") : [])].find((x) => (x.textContent || "").trim() === name && x.getAttribute("visibility") !== "hidden");
|
|
4216
|
+
if (t) dotBy(t, root);
|
|
4217
|
+
}
|
|
4218
|
+
}
|
|
4219
|
+
function kioskDecorate() {
|
|
4220
|
+
const svg = document.querySelector("#graph svg");
|
|
4221
|
+
if (!svg) return;
|
|
4222
|
+
kioskDots(svg);
|
|
4223
|
+
kioskGlow();
|
|
4224
|
+
kioskPopRender();
|
|
4225
|
+
}
|
|
4226
|
+
function kioskPopEvent(ev) {
|
|
4227
|
+
const next = popReduce(kioskPop, ev, Date.now());
|
|
4228
|
+
if (next === kioskPop) return;
|
|
4229
|
+
kioskPop = next;
|
|
4230
|
+
kioskPopRender();
|
|
4231
|
+
}
|
|
4232
|
+
function kioskPopRender() {
|
|
4233
|
+
let pop = document.getElementById("k-pop");
|
|
4234
|
+
const svg = document.querySelector("#graph svg");
|
|
4235
|
+
const id = kioskPop.shown;
|
|
4236
|
+
const card = id && svg ? svg.querySelector(`[data-node-id="${CSS.escape(id)}"]`) : null;
|
|
4237
|
+
const answer = tgAnswerDrawn();
|
|
4238
|
+
const content = !card || !answer ? null : kioskCompMode() ? rootPopover(answer, kioskRootOfComponent(id), resourcesByComponent[id]) : resourcePopover(answer, id);
|
|
4239
|
+
if (!content) {
|
|
4240
|
+
if (pop) pop.hidden = true;
|
|
4241
|
+
return;
|
|
4242
|
+
}
|
|
4243
|
+
if (!pop) {
|
|
4244
|
+
pop = document.createElement("div");
|
|
4245
|
+
pop.id = "k-pop";
|
|
4246
|
+
pop.setAttribute("role", "tooltip");
|
|
4247
|
+
document.body.appendChild(pop);
|
|
4248
|
+
}
|
|
4249
|
+
pop.replaceChildren();
|
|
4250
|
+
pop.classList.toggle("k-pinned", kioskPop.pinned);
|
|
4251
|
+
const roots = kioskRootList();
|
|
4252
|
+
const light = kioskLight();
|
|
4253
|
+
const add = (tag, cls, text) => {
|
|
4254
|
+
const n = document.createElement(tag);
|
|
4255
|
+
n.className = cls;
|
|
4256
|
+
if (text !== undefined) n.textContent = text;
|
|
4257
|
+
pop.appendChild(n);
|
|
4258
|
+
return n;
|
|
4259
|
+
};
|
|
4260
|
+
const list = (rows) => {
|
|
4261
|
+
const ul = add("ul", "k-pop-lines");
|
|
4262
|
+
for (const r of rows) {
|
|
4263
|
+
const li = document.createElement("li");
|
|
4264
|
+
li.className = r.tone === "unknown" ? "k-pop-unknown" : "";
|
|
4265
|
+
li.textContent = r.text;
|
|
4266
|
+
ul.appendChild(li);
|
|
4267
|
+
}
|
|
4268
|
+
return ul;
|
|
4269
|
+
};
|
|
4270
|
+
if (content.address !== undefined) {
|
|
4271
|
+
add("div", "k-pop-addr", content.address);
|
|
4272
|
+
add("div", "k-pop-root").appendChild(rootText(document, `It sits in root ${content.root}.`, roots, { light }));
|
|
4273
|
+
add("p", "k-pop-head", content.head);
|
|
4274
|
+
if (content.lines.length) {
|
|
4275
|
+
const ul = list(content.lines);
|
|
4276
|
+
if (content.more) ul.appendChild(Object.assign(document.createElement("li"), { className: "k-pop-more", textContent: `and ${content.more} more` }));
|
|
4277
|
+
}
|
|
4278
|
+
if (content.applied) {
|
|
4279
|
+
add("p", "k-pop-note", content.applied.when);
|
|
4280
|
+
list(content.applied.lines);
|
|
4281
|
+
} else if (content.note) add("p", "k-pop-note", content.note);
|
|
4282
|
+
} else {
|
|
4283
|
+
add("div", "k-pop-addr").appendChild(rootText(document, content.root, roots, { light }));
|
|
4284
|
+
add("p", "k-pop-head", content.head);
|
|
4285
|
+
const ul = add("ul", "k-pop-lines");
|
|
4286
|
+
for (const r of content.rows) {
|
|
4287
|
+
const li = document.createElement("li");
|
|
4288
|
+
li.className = "k-pop-res";
|
|
4289
|
+
const st = document.createElement("span");
|
|
4290
|
+
st.className = `k-pop-state k-pop-${r.state}`;
|
|
4291
|
+
st.textContent = r.words;
|
|
4292
|
+
li.append(document.createTextNode(`${r.address} `), st);
|
|
4293
|
+
ul.appendChild(li);
|
|
4294
|
+
}
|
|
4295
|
+
if (content.more > 0) ul.appendChild(Object.assign(document.createElement("li"), { className: "k-pop-more", textContent: `and ${content.more} more` }));
|
|
4296
|
+
}
|
|
4297
|
+
// behold#571: a finger opens the popover with a tap, so a touch screen reads the tap's words.
|
|
4298
|
+
const touchOnly = typeof matchMedia === "function" && matchMedia("(hover: none)").matches;
|
|
4299
|
+
add("p", "k-pop-hint", kioskPop.pinned ? (touchOnly ? POP_WORDS.tapPinnedHint : POP_WORDS.pinnedHint) : POP_WORDS.pinHint);
|
|
4300
|
+
pop.hidden = false;
|
|
4301
|
+
const cb = card.getBoundingClientRect();
|
|
4302
|
+
const pb = pop.getBoundingClientRect();
|
|
4303
|
+
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 });
|
|
4304
|
+
pop.style.left = `${Math.round(at.left)}px`;
|
|
4305
|
+
pop.style.top = `${Math.round(at.top)}px`;
|
|
4306
|
+
}
|
|
4307
|
+
/** Wire the frame's pointer once: the popover, and the hover links to the kiosk. Bare frames only. */
|
|
4308
|
+
function wireKioskFrame() {
|
|
4309
|
+
if (!embedOpts.bare) return;
|
|
4310
|
+
const graph = document.getElementById("graph");
|
|
4311
|
+
if (!graph) return;
|
|
4312
|
+
graph.addEventListener("pointerover", (e) => {
|
|
4313
|
+
const card = e.target.closest && e.target.closest("[data-node-id]");
|
|
4314
|
+
if (card) {
|
|
4315
|
+
// The card's own tooltips would sit over the popover.
|
|
4316
|
+
for (const t of card.querySelectorAll("title")) t.remove();
|
|
4317
|
+
kioskPopEvent({ type: "enter", id: card.getAttribute("data-node-id") });
|
|
4318
|
+
}
|
|
4319
|
+
kioskSetHover(kioskHoverOf(e.target));
|
|
4320
|
+
});
|
|
4321
|
+
graph.addEventListener("pointerout", (e) => {
|
|
4322
|
+
const card = e.target.closest && e.target.closest("[data-node-id]");
|
|
4323
|
+
const into = e.relatedTarget;
|
|
4324
|
+
if (card && !(into && card.contains(into))) kioskPopEvent({ type: "leave", id: card.getAttribute("data-node-id") });
|
|
4325
|
+
if (!into || !graph.contains(into)) kioskSetHover(null);
|
|
4326
|
+
});
|
|
4327
|
+
graph.addEventListener(
|
|
4328
|
+
"click",
|
|
4329
|
+
(e) => {
|
|
4330
|
+
const card = e.target.closest && e.target.closest("[data-node-id]");
|
|
4331
|
+
if (card) kioskPopEvent({ type: "click", id: card.getAttribute("data-node-id") });
|
|
4332
|
+
else kioskPopEvent({ type: "outside" });
|
|
4333
|
+
},
|
|
4334
|
+
true,
|
|
4335
|
+
);
|
|
4336
|
+
document.addEventListener("click", (e) => {
|
|
4337
|
+
if (!graph.contains(e.target) && !(e.target.closest && e.target.closest("#k-pop"))) kioskPopEvent({ type: "outside" });
|
|
4338
|
+
});
|
|
4339
|
+
document.addEventListener("keydown", (e) => {
|
|
4340
|
+
if (e.key === "Escape") kioskPopEvent({ type: "escape" });
|
|
4341
|
+
});
|
|
4342
|
+
setInterval(() => kioskPopEvent({ type: "tick" }), 1000);
|
|
4343
|
+
window.addEventListener("message", (e) => {
|
|
4344
|
+
if (!embedOpts.host || e.origin !== embedOpts.host) return;
|
|
4345
|
+
if (e.data && e.data.type === "behold:unpin") return kioskPopEvent({ type: "outside" });
|
|
4346
|
+
// behold#571: the kiosk's theme picker moved; follow it here without a reload.
|
|
4347
|
+
if (e.data && e.data.type === "behold:theme" && typeof e.data.name === "string") return void setTheme(e.data.name, { persist: false });
|
|
4348
|
+
const h = hoverFromMessage(e.data);
|
|
4349
|
+
if (h === undefined) return;
|
|
4350
|
+
kioskHostHover = h;
|
|
4351
|
+
kioskGlow();
|
|
4352
|
+
});
|
|
4353
|
+
}
|
|
4354
|
+
wireKioskFrame();
|
|
4355
|
+
|
|
4086
4356
|
function markAge(marks) {
|
|
4087
4357
|
const newest = marks.map((m) => m.run.finished).sort().pop();
|
|
4088
4358
|
const s = Math.max(0, (Date.now() - Date.parse(newest)) / 1000);
|
|
@@ -5718,6 +5988,54 @@ function ensureZoomControls(host) {
|
|
|
5718
5988
|
},
|
|
5719
5989
|
{ passive: false },
|
|
5720
5990
|
);
|
|
5991
|
+
// behold#571: on a touch screen (the kiosk on a phone), one finger pans and two pinch to zoom.
|
|
5992
|
+
let touch = null;
|
|
5993
|
+
const touchPoints = (e) => [...e.touches].map((t) => ({ x: t.clientX, y: t.clientY }));
|
|
5994
|
+
host.addEventListener(
|
|
5995
|
+
"touchstart",
|
|
5996
|
+
(e) => {
|
|
5997
|
+
if (!vb) return;
|
|
5998
|
+
touch = { pts: touchPoints(e), moved: false };
|
|
5999
|
+
},
|
|
6000
|
+
{ passive: true },
|
|
6001
|
+
);
|
|
6002
|
+
host.addEventListener(
|
|
6003
|
+
"touchmove",
|
|
6004
|
+
(e) => {
|
|
6005
|
+
const s = currentSvg();
|
|
6006
|
+
if (!touch || !vb || !s) return;
|
|
6007
|
+
const pts = touchPoints(e);
|
|
6008
|
+
const r = s.getBoundingClientRect();
|
|
6009
|
+
e.preventDefault();
|
|
6010
|
+
if (pts.length >= 2 && touch.pts.length >= 2) {
|
|
6011
|
+
const dist = (p) => Math.hypot(p[0].x - p[1].x, p[0].y - p[1].y) || 1;
|
|
6012
|
+
const mid = (p) => ({ x: (p[0].x + p[1].x) / 2, y: (p[0].y + p[1].y) / 2 });
|
|
6013
|
+
const f = dist(touch.pts) / dist(pts);
|
|
6014
|
+
const m = mid(pts);
|
|
6015
|
+
const m0 = mid(touch.pts);
|
|
6016
|
+
const cx = vb[0] + ((m.x - r.left) / r.width) * vb[2];
|
|
6017
|
+
const cy = vb[1] + ((m.y - r.top) / r.height) * vb[3];
|
|
6018
|
+
const nw = Math.min(vbInit[2] * 3, Math.max(vbInit[2] / 60, vb[2] * f));
|
|
6019
|
+
const nh = nw * (vb[3] / vb[2]);
|
|
6020
|
+
vb[0] = cx - ((cx - vb[0]) * nw) / vb[2] - ((m.x - m0.x) / r.width) * nw;
|
|
6021
|
+
vb[1] = cy - ((cy - vb[1]) * nh) / vb[3] - ((m.y - m0.y) / r.height) * nh;
|
|
6022
|
+
vb[2] = nw;
|
|
6023
|
+
vb[3] = nh;
|
|
6024
|
+
} else if (pts.length === 1 && touch.pts.length === 1) {
|
|
6025
|
+
vb[0] -= ((pts[0].x - touch.pts[0].x) / r.width) * vb[2];
|
|
6026
|
+
vb[1] -= ((pts[0].y - touch.pts[0].y) / r.height) * vb[3];
|
|
6027
|
+
}
|
|
6028
|
+
if (!touch.moved && touch.pts.length && Math.abs(pts[0].x - touch.pts[0].x) + Math.abs(pts[0].y - touch.pts[0].y) > 3) touch.moved = true;
|
|
6029
|
+
if (touch.moved) panMoved = true;
|
|
6030
|
+
touch.pts = pts;
|
|
6031
|
+
vbAtFit = false;
|
|
6032
|
+
applyVB();
|
|
6033
|
+
},
|
|
6034
|
+
{ passive: false },
|
|
6035
|
+
);
|
|
6036
|
+
host.addEventListener("touchend", (e) => {
|
|
6037
|
+
touch = e.touches.length ? { pts: touchPoints(e), moved: !!(touch && touch.moved) } : null;
|
|
6038
|
+
});
|
|
5721
6039
|
let drag = false;
|
|
5722
6040
|
let px = 0;
|
|
5723
6041
|
let py = 0;
|
|
@@ -6214,6 +6532,8 @@ function ensureLayoutDrag(host) {
|
|
|
6214
6532
|
layoutWired = true;
|
|
6215
6533
|
host.addEventListener("pointerdown", (e) => {
|
|
6216
6534
|
if (e.button !== 0 || !layoutIndex) return;
|
|
6535
|
+
// behold#571: in the kiosk's bare frame a finger pans the graph; it never moves a card.
|
|
6536
|
+
if (e.pointerType === "touch" && embedOpts.bare) return;
|
|
6217
6537
|
const t = layoutTargetOf(e);
|
|
6218
6538
|
if (!t || !currentSvg()) return;
|
|
6219
6539
|
layoutDrag = { ...t, x0: e.clientX, y0: e.clientY, base: layoutDeltas[t.id] || {}, moved: false };
|
package/web/index.html
CHANGED
|
@@ -419,6 +419,30 @@
|
|
|
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
|
+
/* behold#571: on a phone the popover takes the frame's width, less a margin. */
|
|
430
|
+
@media (max-width: 600px) { #k-pop { max-width: calc(100vw - 16px); } }
|
|
431
|
+
#k-pop.k-pinned { border-color: var(--focus); }
|
|
432
|
+
#k-pop p { margin: 0.25em 0; }
|
|
433
|
+
.k-pop-addr { font: 700 1.05em var(--font-mono); overflow-wrap: anywhere; }
|
|
434
|
+
.k-pop-root { color: var(--muted); font-size: 0.92em; }
|
|
435
|
+
.k-pop-head { font-weight: 600; }
|
|
436
|
+
.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); }
|
|
437
|
+
.k-pop-lines li { overflow-wrap: anywhere; }
|
|
438
|
+
.k-pop-unknown, .k-pop-unknown.k-pop-state { color: var(--foreign); }
|
|
439
|
+
.k-pop-more { color: var(--muted); }
|
|
440
|
+
.k-pop-note { color: var(--muted); font-size: 0.92em; }
|
|
441
|
+
.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; }
|
|
442
|
+
.k-pop-state { margin-left: 0.4em; font-family: var(--font-ui); font-weight: 600; white-space: nowrap; }
|
|
443
|
+
.k-pop-change { color: var(--pending); }
|
|
444
|
+
.k-pop-same { color: var(--muted); }
|
|
445
|
+
.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
446
|
#panel-tabs #hintk { flex: none; align-self: center; padding: 2px 6px; margin-left: 2px; }
|
|
423
447
|
#panel-collapse { flex: none; align-self: center; background: none; border: none; color: var(--muted);
|
|
424
448
|
font-size: var(--t-body); cursor: pointer; line-height: 1; padding: 2px 4px; }
|