visualproof 0.3.0 → 0.4.1
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/README.md +52 -11
- package/dist/assets/tips/phone-fingers.light.mp4 +0 -0
- package/dist/assets/tips/phone-fingers.light.webp +0 -0
- package/dist/assets/tips/tour-agent.webp +0 -0
- package/dist/assets/tips/tour-arrange.webp +0 -0
- package/dist/assets/tips/tour-artwork.webp +0 -0
- package/dist/assets/tips/tour-checks.webp +0 -0
- package/dist/assets/tips/tour-export.webp +0 -0
- package/dist/assets/tips/tour-keys.webp +0 -0
- package/dist/assets/tips/tour-library.webp +0 -0
- package/dist/assets/tips/tour-project.webp +0 -0
- package/dist/assets/tips/tour-review.webp +0 -0
- package/dist/assets/tips/tour-share.webp +0 -0
- package/dist/assets/tips/tour-source.webp +0 -0
- package/dist/assets/tips/tour-target.webp +0 -0
- package/dist/assets/tips/tour-view.webp +0 -0
- package/dist/assets/tips/tour.json +289 -0
- package/dist/assets/tips/tour.webp +0 -0
- package/dist/mcp/visualproof-mcp.mjs +3 -1
- package/dist/server/agent.js +10 -5
- package/dist/server/children.js +85 -0
- package/dist/server/doctor.js +105 -42
- package/dist/server/events.js +10 -0
- package/dist/server/export.js +11 -4
- package/dist/server/extract.js +3 -2
- package/dist/server/figures.js +3 -2
- package/dist/server/film.js +380 -0
- package/dist/server/history.js +11 -1
- package/dist/server/hover.js +8 -5
- package/dist/server/live.js +66 -9
- package/dist/server/main.js +217 -16
- package/dist/server/media.js +114 -0
- package/dist/server/pipeline.js +6 -1
- package/dist/server/poppler.js +3 -2
- package/dist/server/readonlygate.js +4 -4
- package/dist/server/sourcestore.js +3 -2
- package/dist/server/status.js +3 -2
- package/dist/server/supervise.js +219 -0
- package/dist/server/updates.js +951 -0
- package/dist/server/usage.js +3 -1
- package/dist/server/version.js +183 -0
- package/dist/shared/timeline.js +5 -0
- package/dist/web/bundle/app.js +2299 -255
- package/dist/web/index.html +13 -1
- package/dist/web/src/canvas.js +22 -0
- package/dist/web/style.css +189 -1
- package/docs/agent-api/export.md +2 -2
- package/docs/agent-api/history.md +3 -2
- package/package.json +3 -1
package/dist/web/bundle/app.js
CHANGED
|
@@ -2001,11 +2001,11 @@ function shortHash(s2) {
|
|
|
2001
2001
|
var SLUG = 32;
|
|
2002
2002
|
function logFileName(name, agent, person = null) {
|
|
2003
2003
|
if (person) return `${person}.person.jsonl`;
|
|
2004
|
-
const
|
|
2004
|
+
const plain2 = name.normalize("NFD").replace(new RegExp("\\p{M}+", "gu"), "").normalize("NFC");
|
|
2005
2005
|
let slug = "";
|
|
2006
|
-
for (const c3 of
|
|
2007
|
-
const
|
|
2008
|
-
const add =
|
|
2006
|
+
for (const c3 of plain2) {
|
|
2007
|
+
const keep2 = /^[\p{L}\p{N}]$/u.test(c3) && c3.normalize("NFD") === c3 && c3.normalize("NFKC") === c3;
|
|
2008
|
+
const add = keep2 ? c3 : slug.endsWith("-") ? "" : "-";
|
|
2009
2009
|
if (slug.length + add.length > SLUG) break;
|
|
2010
2010
|
slug += add;
|
|
2011
2011
|
}
|
|
@@ -2244,9 +2244,9 @@ function parse(text2) {
|
|
|
2244
2244
|
const ts = tokens(text2);
|
|
2245
2245
|
let k2 = 0;
|
|
2246
2246
|
let parens = 0, terms = 0, names2 = 0;
|
|
2247
|
-
const nest = (at,
|
|
2247
|
+
const nest = (at, read2) => {
|
|
2248
2248
|
if (++parens > MAX_PARENS) throw new QueryError(`the query nests more than ${QUERY_LIMITS.depth} levels deep`, at);
|
|
2249
|
-
const r2 =
|
|
2249
|
+
const r2 = read2();
|
|
2250
2250
|
parens--;
|
|
2251
2251
|
return r2;
|
|
2252
2252
|
};
|
|
@@ -2484,8 +2484,8 @@ function chainOf(q2) {
|
|
|
2484
2484
|
function fromChain(base, conditions) {
|
|
2485
2485
|
return conditions.reduce((a3, c3) => ({ op: c3.op, a: a3, b: c3.q }), base);
|
|
2486
2486
|
}
|
|
2487
|
-
function within(q2,
|
|
2488
|
-
return
|
|
2487
|
+
function within(q2, board2) {
|
|
2488
|
+
return board2.op === "all" || sameQuery(q2, board2) ? q2 : { op: "and", a: q2, b: board2 };
|
|
2489
2489
|
}
|
|
2490
2490
|
function readsReview(q2) {
|
|
2491
2491
|
if (q2.op === "trusted" || q2.op === "status" || q2.op === "advice" || q2.op === "key") return true;
|
|
@@ -3633,8 +3633,8 @@ function noteMarkers(clustersOf, shownAs) {
|
|
|
3633
3633
|
return out;
|
|
3634
3634
|
}
|
|
3635
3635
|
function noteExcerpt(text2, most = 18) {
|
|
3636
|
-
const
|
|
3637
|
-
return
|
|
3636
|
+
const plain2 = text2.replace(/```[\s\S]*?```/g, " ").replace(/[`*_]/g, "").replace(/\s+/g, " ").trim();
|
|
3637
|
+
return plain2.length > most ? plain2.slice(0, most - 1).trimEnd() + "\u2026" : plain2;
|
|
3638
3638
|
}
|
|
3639
3639
|
function noteNames(text2) {
|
|
3640
3640
|
const out = /* @__PURE__ */ new Set();
|
|
@@ -4755,24 +4755,24 @@ var ViewBook = class {
|
|
|
4755
4755
|
* a view not kept yet is kept by it (ViewSpec decision 6), so that what was added to it or taken
|
|
4756
4756
|
* out of it from the canvas's menu outlives the pages that had it open. A rename carried into the
|
|
4757
4757
|
* scope (`keep` false: server/views.ts, moveDeclaration) is no one's work and keeps nothing. */
|
|
4758
|
-
setScope(id, scope2, by,
|
|
4758
|
+
setScope(id, scope2, by, keep2 = true) {
|
|
4759
4759
|
const v2 = this.find(id);
|
|
4760
4760
|
if (typeof v2 === "string") return v2;
|
|
4761
4761
|
if (sameQuery(v2.scope, scope2)) return null;
|
|
4762
4762
|
v2.scope = scope2;
|
|
4763
4763
|
const change = { id, what: "scope", by };
|
|
4764
|
-
return v2.kept || !
|
|
4764
|
+
return v2.kept || !keep2 ? [change] : [change, this.keep(v2, by)];
|
|
4765
4765
|
}
|
|
4766
4766
|
/** A view's own focus set by `by` (ViewSpec decision 28): a target's name, or null for none, for
|
|
4767
4767
|
* everyone on it; each reader's own focus is left as it is. Work on the board, as a scope changed
|
|
4768
4768
|
* is: a view not kept yet is kept by it. A rename carried into it (`keep` false) keeps nothing. */
|
|
4769
|
-
setFocus(id, focus, by,
|
|
4769
|
+
setFocus(id, focus, by, keep2 = true) {
|
|
4770
4770
|
const v2 = this.find(id);
|
|
4771
4771
|
if (typeof v2 === "string") return v2;
|
|
4772
4772
|
if (v2.focus === focus) return null;
|
|
4773
4773
|
v2.focus = focus;
|
|
4774
4774
|
const change = { id, what: "focus", by };
|
|
4775
|
-
return v2.kept || !
|
|
4775
|
+
return v2.kept || !keep2 ? [change] : [change, this.keep(v2, by)];
|
|
4776
4776
|
}
|
|
4777
4777
|
/** The view target `target` is linked to (ViewSpec decision 29), or undefined: it has none. */
|
|
4778
4778
|
viewOf(target) {
|
|
@@ -5087,7 +5087,7 @@ function highlightLean(src, isRef) {
|
|
|
5087
5087
|
function tokenHtml(t2, isRef) {
|
|
5088
5088
|
const span = (cls, text2) => `<span class="tk-${cls}">${escapeHtml(text2)}</span>`;
|
|
5089
5089
|
const link = (word, to) => `<span class="tk-ref${/^[A-Z]/.test(word) ? " tk-type" : ""}" data-ref="${escapeHtml(to)}">${escapeHtml(word)}</span>`;
|
|
5090
|
-
const
|
|
5090
|
+
const plain2 = (word) => {
|
|
5091
5091
|
const k2 = wordKind(word);
|
|
5092
5092
|
return k2 === "ident" ? escapeHtml(word) : span(k2, word);
|
|
5093
5093
|
};
|
|
@@ -5101,9 +5101,9 @@ function tokenHtml(t2, isRef) {
|
|
|
5101
5101
|
const word = t2.text;
|
|
5102
5102
|
const ref = isRef?.(word, t2.at);
|
|
5103
5103
|
const parts = word.split(".");
|
|
5104
|
-
if (Array.isArray(ref) && ref.length === parts.length) return parts.map((p2, k2) => ref[k2] ? link(p2, ref[k2]) :
|
|
5104
|
+
if (Array.isArray(ref) && ref.length === parts.length) return parts.map((p2, k2) => ref[k2] ? link(p2, ref[k2]) : plain2(p2)).join(".");
|
|
5105
5105
|
if (ref && !Array.isArray(ref)) return link(word, typeof ref === "string" ? ref : word);
|
|
5106
|
-
return
|
|
5106
|
+
return plain2(word);
|
|
5107
5107
|
}
|
|
5108
5108
|
default:
|
|
5109
5109
|
return span(t2.kind, t2.text);
|
|
@@ -5163,8 +5163,8 @@ function targetRows(targets, views) {
|
|
|
5163
5163
|
const linked = new Map(views.flatMap((v2) => (v2.targets ?? []).map((t2) => [t2, v2])));
|
|
5164
5164
|
return [.../* @__PURE__ */ new Set([...targets, ...linked.keys()])].sort((a3, b2) => a3 < b2 ? -1 : a3 > b2 ? 1 : 0).map((target) => ({ target, view: linked.get(target) ?? null }));
|
|
5165
5165
|
}
|
|
5166
|
-
function targetStep(view,
|
|
5167
|
-
return view === void 0 || view ===
|
|
5166
|
+
function targetStep(view, board2) {
|
|
5167
|
+
return view === void 0 || view === board2 ? null : view;
|
|
5168
5168
|
}
|
|
5169
5169
|
function sectionHead(id, shut, title, aside = "") {
|
|
5170
5170
|
return `<button class="sec-h" data-sec="${id}" aria-expanded="${!shut}" title="${shut ? "Show" : "Fold"} ${escapeHtml(title.toLowerCase())}"><span class="sec-chev" aria-hidden="true">${shut ? "\u25B8" : "\u25BE"}</span><span class="sec-t">${escapeHtml(title)}</span>` + (aside ? `<span class="sec-aside muted">${aside}</span>` : "") + `</button>`;
|
|
@@ -6133,16 +6133,16 @@ function units(S2, k2, minPx) {
|
|
|
6133
6133
|
}
|
|
6134
6134
|
return out;
|
|
6135
6135
|
}
|
|
6136
|
-
function aggregate(g2, unit, sites,
|
|
6136
|
+
function aggregate(g2, unit, sites, keep2) {
|
|
6137
6137
|
const counts2 = /* @__PURE__ */ new Map();
|
|
6138
6138
|
const W2 = 1 << 22;
|
|
6139
6139
|
for (let u2 = 0; u2 < g2.n; u2++) {
|
|
6140
|
-
if (!
|
|
6140
|
+
if (!keep2(u2)) continue;
|
|
6141
6141
|
const a3 = unit[u2];
|
|
6142
6142
|
for (let e2 = g2.outStart[u2]; e2 < g2.outStart[u2 + 1]; e2++) {
|
|
6143
6143
|
if (!(g2.outSite[e2] & sites)) continue;
|
|
6144
6144
|
const v2 = g2.outDst[e2];
|
|
6145
|
-
if (!
|
|
6145
|
+
if (!keep2(v2)) continue;
|
|
6146
6146
|
const b2 = unit[v2];
|
|
6147
6147
|
if (a3 === b2) continue;
|
|
6148
6148
|
const key = a3 * W2 + b2;
|
|
@@ -6218,10 +6218,10 @@ function joinChains(ca, cb) {
|
|
|
6218
6218
|
path = [...ca, ...[...cb].reverse()];
|
|
6219
6219
|
}
|
|
6220
6220
|
if (path.length > MAX_POINTS) {
|
|
6221
|
-
const
|
|
6222
|
-
for (let k2 = 1; k2 < MAX_POINTS - 1; k2++)
|
|
6223
|
-
|
|
6224
|
-
path =
|
|
6221
|
+
const keep2 = [path[0]];
|
|
6222
|
+
for (let k2 = 1; k2 < MAX_POINTS - 1; k2++) keep2.push(path[Math.round(k2 * (path.length - 1) / (MAX_POINTS - 1))]);
|
|
6223
|
+
keep2.push(path[path.length - 1]);
|
|
6224
|
+
path = keep2;
|
|
6225
6225
|
}
|
|
6226
6226
|
return path;
|
|
6227
6227
|
}
|
|
@@ -6330,8 +6330,8 @@ var OverviewModel = class {
|
|
|
6330
6330
|
const unit = units({ ...this.tree, curR: places.curR }, Math.pow(2, level / 2), 45);
|
|
6331
6331
|
const sites = SITE_STMT | SITE_BODY;
|
|
6332
6332
|
const ext = this.toggles.externals;
|
|
6333
|
-
const
|
|
6334
|
-
return { pairs, units: Int32Array.from(new Set(unit)) };
|
|
6333
|
+
const pairs2 = aggregate(g2, unit, sites, (i2) => i2 < g2.projectCount || ext);
|
|
6334
|
+
return { pairs: pairs2, units: Int32Array.from(new Set(unit)) };
|
|
6335
6335
|
}
|
|
6336
6336
|
/** Choose what step `v` draws for a camera `cam`. About 0.6 per unit on screen (between 250 and
|
|
6337
6337
|
* 1,500), so that zoomed in each stays an edge one can follow: those with both ends in view
|
|
@@ -6615,8 +6615,8 @@ var EdgeQueue = class {
|
|
|
6615
6615
|
return true;
|
|
6616
6616
|
}
|
|
6617
6617
|
/** Let go of the points `keep` does not want. */
|
|
6618
|
-
evict(
|
|
6619
|
-
for (const i2 of [...this.points.keys()]) if (!
|
|
6618
|
+
evict(keep2) {
|
|
6619
|
+
for (const i2 of [...this.points.keys()]) if (!keep2(i2)) this.points.delete(i2);
|
|
6620
6620
|
}
|
|
6621
6621
|
/** A point's looks by edge key. */
|
|
6622
6622
|
indexOf(p2) {
|
|
@@ -8343,12 +8343,12 @@ function shareVisitors(trail, world = () => null) {
|
|
|
8343
8343
|
var MAX_EDGES = 400;
|
|
8344
8344
|
function visitorEdges(g2, v2, toView, visitorOf) {
|
|
8345
8345
|
const strong = (site) => (site & (SITE_STMT | SITE_BODY)) !== 0;
|
|
8346
|
-
const
|
|
8346
|
+
const board2 = /* @__PURE__ */ new Map();
|
|
8347
8347
|
const visitors = /* @__PURE__ */ new Map();
|
|
8348
8348
|
const take = (u2, incoming, site) => {
|
|
8349
8349
|
if (u2 === v2) return;
|
|
8350
8350
|
const j2 = toView(u2);
|
|
8351
|
-
const m3 = j2 >= 0 ?
|
|
8351
|
+
const m3 = j2 >= 0 ? board2 : visitors;
|
|
8352
8352
|
const end = j2 >= 0 ? j2 : visitorOf(u2);
|
|
8353
8353
|
if (end < 0) return;
|
|
8354
8354
|
const key = `${end}${incoming ? "<" : ">"}`;
|
|
@@ -8357,7 +8357,7 @@ function visitorEdges(g2, v2, toView, visitorOf) {
|
|
|
8357
8357
|
};
|
|
8358
8358
|
for (let e2 = g2.outStart[v2]; e2 < g2.outStart[v2 + 1]; e2++) take(g2.outDst[e2], false, g2.outSite[e2]);
|
|
8359
8359
|
for (let e2 = g2.inStart[v2]; e2 < g2.inStart[v2 + 1]; e2++) take(g2.inSrc[e2], true, g2.inSite[e2]);
|
|
8360
|
-
const all = [...
|
|
8360
|
+
const all = [...board2.values()].sort((a3, b2) => Number(b2[2]) - Number(a3[2]));
|
|
8361
8361
|
return { board: all.slice(0, MAX_EDGES), visitors: [...visitors.values()] };
|
|
8362
8362
|
}
|
|
8363
8363
|
|
|
@@ -9099,6 +9099,10 @@ var GraphCanvas = class {
|
|
|
9099
9099
|
* fitBoxes) */
|
|
9100
9100
|
fitExtra = null;
|
|
9101
9101
|
/** The canvas element: what is laid over it sends it the wheel. */
|
|
9102
|
+
/** Whether the pointer is down on the board (a drag, a pan): no moment to reload in (web/src/reload.ts). */
|
|
9103
|
+
get pressed() {
|
|
9104
|
+
return this.pressing;
|
|
9105
|
+
}
|
|
9102
9106
|
get element() {
|
|
9103
9107
|
return this.canvas;
|
|
9104
9108
|
}
|
|
@@ -10045,6 +10049,22 @@ var GraphCanvas = class {
|
|
|
10045
10049
|
const k2 = Math.min(4, Math.min(this.w, this.h) * 0.42 / Math.max(S2.drawnR[c3], 20));
|
|
10046
10050
|
this.moveTo(S2.cwx[c3], S2.cwy[c3], Math.min(ZOOM_MOST, Math.max(k2, minPx / Math.max(S2.drawnR[c3], 1e-6))), true);
|
|
10047
10051
|
}
|
|
10052
|
+
/** Frame the declarations `set` marks (the board's numbering) in the room the bars and panels
|
|
10053
|
+
* leave, never nearer than a declaration is pointed at well: the tour's views of what a theorem
|
|
10054
|
+
* rests on (GuideSpec decision 2). */
|
|
10055
|
+
fitTo(set, animate = true) {
|
|
10056
|
+
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
10057
|
+
for (let i2 = 0; i2 < set.length; i2++) {
|
|
10058
|
+
if (!set[i2]) continue;
|
|
10059
|
+
const [x2, y2] = this.posOf(i2);
|
|
10060
|
+
[x0, y0, x1, y1] = [Math.min(x0, x2), Math.min(y0, y2), Math.max(x1, x2), Math.max(y1, y2)];
|
|
10061
|
+
}
|
|
10062
|
+
if (!isFinite(x0)) return;
|
|
10063
|
+
const vw = this.w - this.insets.left - this.insets.right;
|
|
10064
|
+
const vh = this.h - this.insets.top - this.insets.bottom;
|
|
10065
|
+
const k2 = Math.min(vw / (x1 - x0 + 80), vh / (y1 - y0 + 80)) * 0.9;
|
|
10066
|
+
this.moveTo((x0 + x1) / 2, (y0 + y1) / 2, Math.min(1.6, Math.max(ZOOM_LEAST, k2)), animate);
|
|
10067
|
+
}
|
|
10048
10068
|
/** Bring world point (x, y) to the middle of the unobstructed area at zoom k. */
|
|
10049
10069
|
moveTo(px2, py, k2, animate, dur = 520) {
|
|
10050
10070
|
if (this.anim) cancelAnimationFrame(this.anim);
|
|
@@ -12358,9 +12378,11 @@ var Panel = class {
|
|
|
12358
12378
|
const queue = this.hooks.queue(i2, keyOnly);
|
|
12359
12379
|
const inOrder = queue !== null && listOrder !== "kind";
|
|
12360
12380
|
const orderSwitch = queue === null ? `<span class="muted">\u2014 reached from the statement, proofs skipped</span>` : `<span class="seg mini"><button data-tcb-order="review" class="${inOrder && !keyOnly ? "on" : ""}" title="Bottom-up: what a declaration uses before it">Review order</button><button data-tcb-order="key" class="${keyOnly ? "on" : ""}" title="The key declarations alone, in review order">\u25C6 Key</button><button data-tcb-order="kind" class="${inOrder ? "" : "on"}" title="Grouped by kind, by name">By kind</button></span>`;
|
|
12361
|
-
const part = (key, title, aside, body) => {
|
|
12381
|
+
const part = (key, title, aside, body, tip) => {
|
|
12362
12382
|
const shut = this.hooks.shut(key);
|
|
12363
|
-
|
|
12383
|
+
const head = sectionHead(key, shut, title, aside);
|
|
12384
|
+
const info2 = tip ? `<button class="p-info" data-tip-open="${tip.id}" title="${escapeHtml(tip.title)}" aria-label="${escapeHtml(tip.title)}">\u24D8</button>` : "";
|
|
12385
|
+
return `<div class="p-sec ${key}">${info2 ? `<div class="sec-row">${head}${info2}</div>` : head}${shut ? "" : `<div class="p-sec-body">${body()}</div>`}</div>`;
|
|
12364
12386
|
};
|
|
12365
12387
|
const flags = (v2.flags ?? []).map((f2) => `<span class="chip ${f2 === "opaque" || f2 === "unsafe" || f2 === "partial" || f2 === "implemented_by" || f2 === "extern" ? "warn" : ""}">${f2}</span>`).join("");
|
|
12366
12388
|
const line = v2.range ? `:${v2.range[0]}` : "";
|
|
@@ -12405,7 +12427,7 @@ var Panel = class {
|
|
|
12405
12427
|
() => `<div class="axioms">${axioms.length ? axioms.map((a3) => `<span class="chip ${nonStd.includes(a3) ? "bad" : ""}">${escapeHtml(a3)}</span>`).join("") : `<span class="muted">none</span>`}</div>`
|
|
12406
12428
|
)}
|
|
12407
12429
|
${notationHtml(g2, i2)}
|
|
12408
|
-
${tcbP ? part(PANEL_SECTIONS.tcb, "
|
|
12430
|
+
${tcbP ? part(PANEL_SECTIONS.tcb, "Its TCB", `${tcbP}${queue === null ? "" : keyOnly ? " \xB7 key declarations" : inOrder ? " \xB7 in review order" : " \xB7 by kind"}`, () => `<div class="sec-tcb p-order">${orderSwitch}</div><div class="tcb-list">${inOrder ? queue : tcbList}</div>`, { id: "checks", title: "What a TCB is: the film's chapter on the ways to check" }) : ""}
|
|
12409
12431
|
`;
|
|
12410
12432
|
this.el.classList.add("open");
|
|
12411
12433
|
if (!same2) this.el.scrollTop = 0;
|
|
@@ -12999,6 +13021,7 @@ function reviewState(raw) {
|
|
|
12999
13021
|
error: typeof r2.error === "string" ? r2.error : null
|
|
13000
13022
|
};
|
|
13001
13023
|
}
|
|
13024
|
+
var graphQuery = (v2, i2, first) => v2 ? `${first}v=${v2}${i2 ? `&i=${encodeURIComponent(i2)}` : ""}` : "";
|
|
13002
13025
|
function heartbeatRequest(token, beat, headers) {
|
|
13003
13026
|
if ("private" in beat) return { headers: { "content-type": "application/json" }, body: JSON.stringify({ token, private: true }) };
|
|
13004
13027
|
return { headers, body: JSON.stringify({ token, view: beat.view, idle: beat.idle }) };
|
|
@@ -13118,6 +13141,9 @@ function snapshotSource(info2) {
|
|
|
13118
13141
|
exportDoc: async () => "a snapshot cannot typeset documents: export from the server",
|
|
13119
13142
|
exportJob: async () => "a snapshot cannot typeset documents: export from the server",
|
|
13120
13143
|
shareInfo: async () => ({ path: null }),
|
|
13144
|
+
updates: async () => null,
|
|
13145
|
+
tipClips: async () => /* @__PURE__ */ new Set(),
|
|
13146
|
+
tourJson: async () => null,
|
|
13121
13147
|
agentShares: async () => null,
|
|
13122
13148
|
shareAgent: async () => ({ error: "a snapshot has no server to share with an agent" }),
|
|
13123
13149
|
agentPrompt: async () => ({ error: "a snapshot has no server" }),
|
|
@@ -13173,10 +13199,10 @@ function serverSource() {
|
|
|
13173
13199
|
},
|
|
13174
13200
|
status: () => fetch("/api/status").then((r2) => r2.json()),
|
|
13175
13201
|
forget: () => cache.clear(),
|
|
13176
|
-
meta: (v2) => fetch(`/api/meta${v2
|
|
13202
|
+
meta: (v2, i2) => fetch(`/api/meta${graphQuery(v2, i2, "?")}`).then((r2) => r2.ok ? r2.json() : Promise.reject(new Error(`meta ${r2.status}`))),
|
|
13177
13203
|
git: () => fetch("/api/git").then((r2) => r2.ok ? r2.json() : null).then((b2) => b2?.git ?? null).catch(() => null),
|
|
13178
13204
|
// packed for the way (shared/edgecodec.ts): a quarter of the bytes
|
|
13179
|
-
edges: (v2) => fetch(`/api/edges?pack=2${v2
|
|
13205
|
+
edges: (v2, i2) => fetch(`/api/edges?pack=2${graphQuery(v2, i2, "&")}`).then((r2) => r2.ok ? r2.arrayBuffer() : Promise.reject(new Error(`edges ${r2.status}`))).then((b2) => isPackedEdges(new Uint8Array(b2)) ? unpackEdges(new Uint8Array(b2)).buffer : b2),
|
|
13180
13206
|
review: () => fetch("/api/review").then((r2) => r2.ok ? r2.json() : Promise.reject(new Error(`review ${r2.status}`))).then(reviewState),
|
|
13181
13207
|
hover: (module, line, character, hash) => fetch(`/api/hover?module=${encodeURIComponent(module)}&line=${line}&character=${character}&hash=${hash}`).then((r2) => r2.json()).catch(() => ({ error: "the server could not be reached" })),
|
|
13182
13208
|
hoverOpen: (module) => void fetch(`/api/hover/open?module=${encodeURIComponent(module)}`, { method: "POST" }).catch(() => {
|
|
@@ -13200,6 +13226,9 @@ function serverSource() {
|
|
|
13200
13226
|
revokeReadOnly: (id) => fetch("/api/share/readonly/revoke", { method: "POST", headers: headers(), body: JSON.stringify({ id }) }).then((r2) => r2.json()).catch(() => ({ error: "the server could not be reached" })),
|
|
13201
13227
|
activity: () => fetch("/api/activity").then((r2) => r2.ok ? r2.json() : { events: [] }).then((b2) => b2.events).catch(() => []),
|
|
13202
13228
|
shareInfo: () => fetch("/api/share").then((r2) => r2.json()).then((b2) => ({ ...b2, path: typeof b2.path === "string" ? b2.path : null })),
|
|
13229
|
+
updates: () => fetch("/api/updates").then((r2) => r2.ok ? r2.json() : null).then((b2) => b2 && typeof b2.running === "string" ? b2 : null).catch(() => null),
|
|
13230
|
+
tipClips: () => fetch("/tips/index.json").then((r2) => r2.ok ? r2.json() : null).then((b2) => new Set(Array.isArray(b2?.clips) ? b2.clips.filter((c3) => typeof c3 === "string") : [])).catch(() => /* @__PURE__ */ new Set()),
|
|
13231
|
+
tourJson: () => fetch("/tips/tour.json").then((r2) => r2.ok ? r2.json() : null).catch(() => null),
|
|
13203
13232
|
agentShares: () => fetch("/api/share/agents").then((r2) => r2.ok ? r2.json() : null).catch(() => null),
|
|
13204
13233
|
shareAgent: (body) => fetch("/api/share/agents", { method: "POST", headers: headers(), body: JSON.stringify(body) }).then((r2) => r2.json()).catch(() => ({ error: "the server could not be reached" })),
|
|
13205
13234
|
agentPrompt: (id) => fetch("/api/share/agents/prompt", { method: "POST", headers: headers(), body: JSON.stringify({ id }) }).then((r2) => r2.json()).catch(() => ({ error: "the server could not be reached" })),
|
|
@@ -14319,7 +14348,7 @@ var Stickies = class {
|
|
|
14319
14348
|
this.layoutSeen = L2;
|
|
14320
14349
|
const nb = this.board();
|
|
14321
14350
|
const b2 = this.hooks.board();
|
|
14322
|
-
const
|
|
14351
|
+
const keep2 = /* @__PURE__ */ new Set();
|
|
14323
14352
|
if (nb && b2) {
|
|
14324
14353
|
const exists = existsIn(b2.full);
|
|
14325
14354
|
const index = new Map(L2.clusters.map((c3, k2) => [c3.key, k2]));
|
|
@@ -14334,8 +14363,8 @@ var Stickies = class {
|
|
|
14334
14363
|
...view.map((n3) => ({ kind: "view", note: n3, spot: viewSpot(n3, nb, (key) => index.get(key) ?? -1), why: null }))
|
|
14335
14364
|
];
|
|
14336
14365
|
for (const { kind, note: n3, spot, why } of all) {
|
|
14337
|
-
if (!spot ||
|
|
14338
|
-
|
|
14366
|
+
if (!spot || keep2.has(n3.id)) continue;
|
|
14367
|
+
keep2.add(n3.id);
|
|
14339
14368
|
let d2 = this.drawn.get(n3.id);
|
|
14340
14369
|
const fresh3 = !d2;
|
|
14341
14370
|
const opened2 = n3.open && (d2 ? !d2.note.open : this.rendered);
|
|
@@ -14392,7 +14421,7 @@ var Stickies = class {
|
|
|
14392
14421
|
}
|
|
14393
14422
|
}
|
|
14394
14423
|
for (const [id, d2] of this.drawn)
|
|
14395
|
-
if (!
|
|
14424
|
+
if (!keep2.has(id)) {
|
|
14396
14425
|
d2.el.remove();
|
|
14397
14426
|
this.drawn.delete(id);
|
|
14398
14427
|
}
|
|
@@ -15596,6 +15625,14 @@ var ReviewBox = class {
|
|
|
15596
15625
|
const f2 = this.speakingFor?.trim() || null;
|
|
15597
15626
|
return f2 && f2 !== this.hooks.review().reviewer?.trim() ? f2 : null;
|
|
15598
15627
|
}
|
|
15628
|
+
/** The drafts not empty, kept across a reload into a new version (web/src/reload.ts). */
|
|
15629
|
+
get keptDrafts() {
|
|
15630
|
+
return [...this.drafts].filter(([, v2]) => v2.trim() !== "");
|
|
15631
|
+
}
|
|
15632
|
+
/** The drafts kept by the reload that made this page (UpdateSpec decision 8). */
|
|
15633
|
+
restoreDrafts(kept) {
|
|
15634
|
+
for (const [k2, v2] of kept) if (!this.drafts.has(k2)) this.drafts.set(k2, v2);
|
|
15635
|
+
}
|
|
15599
15636
|
/** Keep what is typed into a comment box: the other boxes of the declaration show it too. Whom
|
|
15600
15637
|
* one speaks for, typed into any box, is every box's. */
|
|
15601
15638
|
input(t2, i2) {
|
|
@@ -16114,6 +16151,14 @@ var Talk = class {
|
|
|
16114
16151
|
/** `#following`: the title bar's state dot and the pill are there */
|
|
16115
16152
|
frame;
|
|
16116
16153
|
sized = typeof ResizeObserver === "undefined" ? null : new ResizeObserver(() => this.atEnd && this.scrollEnd());
|
|
16154
|
+
/** The drafts not empty, kept across a reload into a new version (web/src/reload.ts). */
|
|
16155
|
+
get keptDrafts() {
|
|
16156
|
+
return [...this.drafts].filter(([, v2]) => v2.trim() !== "");
|
|
16157
|
+
}
|
|
16158
|
+
/** The drafts kept by the reload that made this page (UpdateSpec decision 8). */
|
|
16159
|
+
restoreDrafts(kept) {
|
|
16160
|
+
for (const [k2, v2] of kept) if (!this.drafts.has(k2)) this.drafts.set(k2, v2);
|
|
16161
|
+
}
|
|
16117
16162
|
constructor(box, hooks) {
|
|
16118
16163
|
this.box = box;
|
|
16119
16164
|
this.hooks = hooks;
|
|
@@ -16689,7 +16734,7 @@ var FOLLOWED = [
|
|
|
16689
16734
|
part: "selection",
|
|
16690
16735
|
what: "the selection, and going somewhere: a row, a name or a result that goes to a declaration or a cluster",
|
|
16691
16736
|
fields: ["selection"],
|
|
16692
|
-
changes: "#selbar, #ctxmenu, .cm-sub, #results .hit, [data-go], [data-ref], [data-walk], [data-c], [data-module], [data-tm-go], #help [data-help-tour], #linear",
|
|
16737
|
+
changes: "#selbar, #ctxmenu, .cm-sub, #results .hit, [data-go], [data-ref], [data-walk], [data-c], [data-module], [data-tm-go], #help [data-help-tour], #help-menu [data-hm=tour], #welcome [data-wel=tour], #help [data-tip-show], #help [data-tour-show], #intip [data-intip-show], #linear",
|
|
16693
16738
|
keys: ["ContextMenu", "F10"],
|
|
16694
16739
|
fieldKeys: [{ in: "#search", key: "Enter" }],
|
|
16695
16740
|
place: true
|
|
@@ -16798,7 +16843,10 @@ var YOURS = [
|
|
|
16798
16843
|
{ id: "paperTools", what: "zoom, find and the box tool in the reference window", where: "[data-pw-zoom], [data-pw-find], [data-pw-box], .pw-find" },
|
|
16799
16844
|
{ id: "share", what: "Share, and its switch Exactly what I see", where: "#share-btn, #share-pop" },
|
|
16800
16845
|
{ id: "export", what: "Export", where: "#export-btn, #export-modal" },
|
|
16801
|
-
{ id: "help", what: "Help: the ? sheet and the tour", where: "#help-btn, #help, #tour", keys: ["?"] },
|
|
16846
|
+
{ id: "help", what: "Help: the ? menu and its sheet, its tips and its keys, the tour, a tip in place, What's new, the welcome of a first visit (GuideSpec)", where: "#help-btn, #help-menu, #help, #tour, #intip, #whatsnew, #welcome, [data-tip-open]", keys: ["?"] },
|
|
16847
|
+
// the tour film in the sheet (GuideSpec decision 6, web/src/tourfilm.ts): its keys play it, go
|
|
16848
|
+
// from chapter to chapter, turn its captions on or off, there alone
|
|
16849
|
+
{ id: "film", what: "the tour film in the help sheet: playing, its chapters, its captions, its larger sizes", where: "[data-film]", ownKeys: [" ", "ArrowLeft", "ArrowRight", "c"] },
|
|
16802
16850
|
{ id: "library", what: "the Library popover", where: "#library-btn, #library-pop" },
|
|
16803
16851
|
{ id: "board", what: "\u2699 Board: the board's scope, clustering, depth and Re-layout, shared with everyone on it", where: "#board-btn, #board-pop", keys: ["e", "[", "]"] },
|
|
16804
16852
|
{ id: "trusted", what: "Trusted, and trusting without review", where: "#trusted-btn, #trust-list, #trust-pop" },
|
|
@@ -16823,11 +16871,11 @@ var YOURS = [
|
|
|
16823
16871
|
{ id: "meter", what: "the frame meter", keys: ["p"] }
|
|
16824
16872
|
];
|
|
16825
16873
|
var BOOKKEEPING = ["tabs"];
|
|
16826
|
-
function followedOf(
|
|
16827
|
-
return Object.assign({}, ...FOLLOWED.map((p2) =>
|
|
16874
|
+
function followedOf(read2) {
|
|
16875
|
+
return Object.assign({}, ...FOLLOWED.map((p2) => read2[p2.part]()));
|
|
16828
16876
|
}
|
|
16829
|
-
function presenceOf(
|
|
16830
|
-
return { ...followedOf(
|
|
16877
|
+
function presenceOf(read2, tabs) {
|
|
16878
|
+
return { ...followedOf(read2), tabs };
|
|
16831
16879
|
}
|
|
16832
16880
|
function applyOwn(v2, apply) {
|
|
16833
16881
|
for (const p2 of FOLLOWED) {
|
|
@@ -16846,6 +16894,7 @@ var REGIONS = [
|
|
|
16846
16894
|
...FOLLOWED.flatMap((p2) => "reads" in p2 ? [{ sel: p2.reads, verdict: "reads" }] : [])
|
|
16847
16895
|
];
|
|
16848
16896
|
var FOLLOWED_KEYS = new Set(FOLLOWED.flatMap((p2) => "keys" in p2 ? [...p2.keys] : []));
|
|
16897
|
+
var OWN_KEYS = YOURS.flatMap((y2) => "ownKeys" in y2 ? [{ in: y2.where, keys: y2.ownKeys }] : []);
|
|
16849
16898
|
var YOURS_KEYS = new Set(YOURS.flatMap((y2) => "keys" in y2 ? [...y2.keys] : []));
|
|
16850
16899
|
var FIELD_KEYS = FOLLOWED.flatMap((p2) => "fieldKeys" in p2 ? [...p2.fieldKeys] : []);
|
|
16851
16900
|
var WHOLE = YOURS.flatMap((y2) => "whole" in y2 ? [y2.where] : []).join(", ");
|
|
@@ -16877,6 +16926,7 @@ function keyActs(e2, t2, field) {
|
|
|
16877
16926
|
const yours = inYours(t2);
|
|
16878
16927
|
if (field || e2.isComposing) return !yours || !e2.isComposing && FIELD_KEYS.some((k2) => k2.key === e2.key && closest(t2, k2.in) !== null);
|
|
16879
16928
|
if ((e2.key === "Enter" || e2.key === " ") && closest(t2, PRESSABLE)) return pressActs(t2);
|
|
16929
|
+
if (OWN_KEYS.some((o2) => o2.keys.includes(e2.key) && closest(t2, o2.in) !== null)) return false;
|
|
16880
16930
|
if ((e2.ctrlKey || e2.metaKey) && e2.key.length === 1) return false;
|
|
16881
16931
|
if (FOLLOWED_KEYS.has(e2.key)) return true;
|
|
16882
16932
|
if (yours || YOURS_KEYS.has(e2.key)) return false;
|
|
@@ -18346,6 +18396,11 @@ ${tip}">${title(name)}</button>` : `<span class="act-gone" title="${tip}">${titl
|
|
|
18346
18396
|
html: `${who(e2.by, e2.agent, e2.person)} undid ${escapeHtml(whose)} layout: ${e2.restored} put back` + (e2.kept.length ? `<div class="act-warn">${e2.kept.length} left where ${e2.kept.length === 1 ? "it is" : "they are"}, moved by someone since: ${e2.kept.slice(0, 8).map((k2) => this.hooks.has(k2) ? decl(k2) : `<span class="act-key">${escapeHtml(k2)}</span>`).join(", ")}${e2.kept.length > 8 ? " \u2026" : ""}</div>` : ""),
|
|
18347
18397
|
names: e2.kept
|
|
18348
18398
|
});
|
|
18399
|
+
} else if (e2.kind === "tool") {
|
|
18400
|
+
const v2 = (x2) => `<b>VisualProof ${escapeHtml(x2)}</b>`;
|
|
18401
|
+
const aside = e2.setAside ? `${v2(e2.setAside.version)} was set aside${e2.setAside.why ? `: it ${escapeHtml(e2.setAside.why)}` : ""}. ` : "";
|
|
18402
|
+
const what = e2.from === null || e2.from === e2.to ? `${v2(e2.to)} serves the site` : `Updated from ${escapeHtml(e2.from)} to ${v2(e2.to)}`;
|
|
18403
|
+
out.push({ ...base, by: null, cat: "build", html: `${aside}${what}` });
|
|
18349
18404
|
}
|
|
18350
18405
|
}
|
|
18351
18406
|
return out.sort((a3, b2) => a3.at < b2.at ? 1 : a3.at > b2.at ? -1 : 0);
|
|
@@ -18992,9 +19047,1093 @@ var Phone = class {
|
|
|
18992
19047
|
}
|
|
18993
19048
|
};
|
|
18994
19049
|
|
|
19050
|
+
// src/web/src/tips.ts
|
|
19051
|
+
var TOUR = [
|
|
19052
|
+
{
|
|
19053
|
+
id: "target",
|
|
19054
|
+
title: "Mark a target",
|
|
19055
|
+
text: "Search for a theorem, select it, and mark it as a target: the review is about what its statement says.",
|
|
19056
|
+
about: ["#search", "#panel [data-rv-target]", "#targets"],
|
|
19057
|
+
showMe: "ring-target",
|
|
19058
|
+
manual: "review"
|
|
19059
|
+
},
|
|
19060
|
+
{
|
|
19061
|
+
id: "view",
|
|
19062
|
+
title: "A view of what it uses",
|
|
19063
|
+
text: "Rests on \xB7 Uses lights everything it uses. <b>Zoom in</b> opens just that as a view: a board of its own, laid out on its own, shared with everyone.",
|
|
19064
|
+
about: ["#rests-steps [data-mode=deps]", "#selbar [data-zoom]", "#tabs"],
|
|
19065
|
+
showMe: "ring-zoom",
|
|
19066
|
+
manual: "views"
|
|
19067
|
+
},
|
|
19068
|
+
{
|
|
19069
|
+
id: "arrange",
|
|
19070
|
+
title: "Arrange the board",
|
|
19071
|
+
text: "Drag a declaration with <kbd>Ctrl</kbd> held, or let the lock go, to place it for everyone; double-click a circle to fold it, again to open it.",
|
|
19072
|
+
about: ["#canvas", "#lock-btn", "key:l"],
|
|
19073
|
+
showMe: "ring-lock",
|
|
19074
|
+
manual: "what-you-see"
|
|
19075
|
+
},
|
|
19076
|
+
{
|
|
19077
|
+
id: "source",
|
|
19078
|
+
title: "Read the source",
|
|
19079
|
+
text: "<kbd>Space</kbd> opens the source, following the selection. Rest on a name for what Lean says it is; click it to go there.",
|
|
19080
|
+
about: [".sw-win", "key:Space"],
|
|
19081
|
+
showMe: "open-source",
|
|
19082
|
+
manual: "review"
|
|
19083
|
+
},
|
|
19084
|
+
{
|
|
19085
|
+
id: "keys",
|
|
19086
|
+
title: "Key declarations",
|
|
19087
|
+
text: "On the target's view, select the declarations that carry its meaning and mark them key with a click. <b>Keys</b> selects them.",
|
|
19088
|
+
about: ["#panel [data-rv-key]", "#selbar [data-mark-keys]", "#rests-steps [data-mode=keys]"],
|
|
19089
|
+
showMe: "ring-key",
|
|
19090
|
+
// key-in-target: on a view focused on a target T (its review strip shows), the reader approves a
|
|
19091
|
+
// declaration of T's TCB — the second they approved in T's TCB in this page's session — while T
|
|
19092
|
+
// has no key declaration but itself. At #panel [data-rv-key]. Ends: a key marked (k, ◇ Mark as
|
|
19093
|
+
// key, Mark selected as key); Got it.
|
|
19094
|
+
when: "key-in-target",
|
|
19095
|
+
manual: "review"
|
|
19096
|
+
},
|
|
19097
|
+
{
|
|
19098
|
+
id: "checks",
|
|
19099
|
+
title: "Three ways to check",
|
|
19100
|
+
text: "<b>Selection</b>: the statement alone, least effort. <b>Keys</b>: the few that carry the meaning. <b>TCB</b>: all it rests on, most effort, most confidence. The counts say what each costs.",
|
|
19101
|
+
about: ["#lightup [data-mode=plain]", "#rests-steps [data-mode=keys]", "#rests-steps [data-mode=tcb]", "key:2"],
|
|
19102
|
+
showMe: "tcb-three",
|
|
19103
|
+
// tcb-first: the reader's own click selects, under Rests on · TCB, a theorem whose TCB lights
|
|
19104
|
+
// more than itself (not the tour's selection, not one taken by following or by a link opened),
|
|
19105
|
+
// for the first time in this browser. At #rests-steps [data-mode=tcb] on a larger screen, at the
|
|
19106
|
+
// panel's TCB part (#panel .p-tcb) on a phone. Ends: Got it; Show me pressed in it (it runs the
|
|
19107
|
+
// three views); Watch; the reader stepping to another step of Rests on.
|
|
19108
|
+
when: "tcb-first",
|
|
19109
|
+
manual: "how-the-tcb-is-computed"
|
|
19110
|
+
},
|
|
19111
|
+
{
|
|
19112
|
+
id: "review",
|
|
19113
|
+
title: "Comment and approve",
|
|
19114
|
+
text: "Comment or approve each declaration you read: an opinion holds for the version it judged, and the target's progress counts it.",
|
|
19115
|
+
about: ["#panel [data-rv=approve]", "#panel [data-rv=comment]", "#targets"],
|
|
19116
|
+
showMe: "ring-review",
|
|
19117
|
+
manual: "review"
|
|
19118
|
+
},
|
|
19119
|
+
{
|
|
19120
|
+
id: "project",
|
|
19121
|
+
title: "Back to the Project",
|
|
19122
|
+
text: "The Project is the whole codebase; views are slices of it. Every board shows how far the review has come.",
|
|
19123
|
+
about: ["#tabs", "#targets"],
|
|
19124
|
+
manual: "views"
|
|
19125
|
+
},
|
|
19126
|
+
{
|
|
19127
|
+
id: "share",
|
|
19128
|
+
title: "Share",
|
|
19129
|
+
text: "A link for people, a read-only link for readers, and for an AI agent a token with a prompt to paste.",
|
|
19130
|
+
about: ["#share-btn", "#share-pop"],
|
|
19131
|
+
showMe: "ring-share",
|
|
19132
|
+
manual: "together"
|
|
19133
|
+
},
|
|
19134
|
+
{
|
|
19135
|
+
id: "agent",
|
|
19136
|
+
title: "Work with an agent",
|
|
19137
|
+
text: "Ask the agent: it reads the codebase through the API, takes you to the declaration and explains it. It gives advice; approving stays with people.",
|
|
19138
|
+
about: ["#share-pop [data-tab=agent]", "#peers", "#following"],
|
|
19139
|
+
showMe: "share-agent",
|
|
19140
|
+
manual: "together"
|
|
19141
|
+
},
|
|
19142
|
+
{
|
|
19143
|
+
id: "library",
|
|
19144
|
+
title: "Papers and notes",
|
|
19145
|
+
text: "Add a paper to the <b>Library</b> and link it to the declarations it explains; pin notes on the board for everyone.",
|
|
19146
|
+
about: ["#library-btn", ".pw-win", "#stickies"],
|
|
19147
|
+
showMe: "library-pair",
|
|
19148
|
+
manual: "references"
|
|
19149
|
+
},
|
|
19150
|
+
{
|
|
19151
|
+
id: "export",
|
|
19152
|
+
title: "A typeset document",
|
|
19153
|
+
text: "<b>Export</b> draws the view as its outline on one line: drag a section to reorder it, then statements, sources and the review are typeset as a PDF.",
|
|
19154
|
+
about: ["#export-btn", "#export-modal", "#linear"],
|
|
19155
|
+
showMe: "ring-export",
|
|
19156
|
+
// first-export: the Export window opened for the first time in this browser, the linear form
|
|
19157
|
+
// drawn beside it. At #linear. Ends: Got it; Watch; it goes, unanswered, when the window closes.
|
|
19158
|
+
when: "first-export",
|
|
19159
|
+
manual: "exported-documents"
|
|
19160
|
+
},
|
|
19161
|
+
{
|
|
19162
|
+
id: "artwork",
|
|
19163
|
+
title: "Artwork",
|
|
19164
|
+
text: "The board as a poster: frame it, choose what is named, and export a PNG and an SVG.",
|
|
19165
|
+
about: ["#export-modal [data-ex-art]", "#art", "#art-panel"],
|
|
19166
|
+
showMe: "ring-export",
|
|
19167
|
+
manual: "exported-documents"
|
|
19168
|
+
}
|
|
19169
|
+
];
|
|
19170
|
+
var GROUPS = [
|
|
19171
|
+
{ id: "start", title: "Start" },
|
|
19172
|
+
{ id: "seeing", title: "Seeing" },
|
|
19173
|
+
{ id: "reviewing", title: "Reviewing" },
|
|
19174
|
+
{ id: "arranging", title: "Arranging" },
|
|
19175
|
+
{ id: "together", title: "Together" },
|
|
19176
|
+
{ id: "export", title: "Export" },
|
|
19177
|
+
{ id: "history", title: "History" }
|
|
19178
|
+
];
|
|
19179
|
+
var TIPS = [
|
|
19180
|
+
// ─────────────────────────────────────────────────────────────── start
|
|
19181
|
+
{
|
|
19182
|
+
id: "select-and-modes",
|
|
19183
|
+
group: "start",
|
|
19184
|
+
title: "How a click selects",
|
|
19185
|
+
text: "<b>Mode</b> is how a click selects: just it (<kbd>1</kbd>), what it rests on (<kbd>2</kbd>), or what uses it (<kbd>3</kbd>). Under Selection, Shift-click adds a declaration or takes it off.",
|
|
19186
|
+
who: "all",
|
|
19187
|
+
where: "desktop",
|
|
19188
|
+
since: "0.1.0",
|
|
19189
|
+
about: ["#lightup", "key:1", "key:2", "key:3"],
|
|
19190
|
+
showMe: "ring-modes",
|
|
19191
|
+
// selection-replace: under Selection, the reader clicks a declaration while another one is
|
|
19192
|
+
// selected, three times within 20 s, each click replacing the selection (no Shift). At
|
|
19193
|
+
// #lightup [data-mode=plain]. Ends: a Shift-click that adds to the selection; Got it.
|
|
19194
|
+
when: "selection-replace",
|
|
19195
|
+
manual: "what-you-see"
|
|
19196
|
+
},
|
|
19197
|
+
{
|
|
19198
|
+
id: "phone-fingers",
|
|
19199
|
+
group: "start",
|
|
19200
|
+
title: "Fingers on the board",
|
|
19201
|
+
text: "Drag to move around, pinch to zoom, tap to select. Touch and hold for what can be done with what is under your finger; <b>\u22EF</b> at the top holds the modes.",
|
|
19202
|
+
who: "all",
|
|
19203
|
+
where: "phone",
|
|
19204
|
+
since: "0.2.0",
|
|
19205
|
+
about: ["#canvas", "#more-btn"],
|
|
19206
|
+
clip: "phone-fingers",
|
|
19207
|
+
demo: "A phone's screen (390\xD7844), the Project. One finger drags the board up a little; two fingers pinch it open about the FltRegular circle; a tap on IsRegularPrime: the sheet rises from the bottom to half the screen, on its statement. Finger dots, no pointer.",
|
|
19208
|
+
manual: "on-a-phone"
|
|
19209
|
+
},
|
|
19210
|
+
{
|
|
19211
|
+
id: "updates",
|
|
19212
|
+
group: "start",
|
|
19213
|
+
title: "Kept up to date",
|
|
19214
|
+
text: "A server installed from npm takes VisualProof's fixes by itself, unless told not to, and this page reloads where you were. The help's About says which VisualProof runs.",
|
|
19215
|
+
who: "all",
|
|
19216
|
+
where: "both",
|
|
19217
|
+
since: "0.4.0",
|
|
19218
|
+
about: ["#help"],
|
|
19219
|
+
showMe: "help-about",
|
|
19220
|
+
manual: "updates"
|
|
19221
|
+
},
|
|
19222
|
+
// ─────────────────────────────────────────────────────────────── seeing
|
|
19223
|
+
{
|
|
19224
|
+
id: "rests-on-steps",
|
|
19225
|
+
group: "seeing",
|
|
19226
|
+
title: "Rests on, step by step",
|
|
19227
|
+
text: "Press <kbd>2</kbd> again for the next step: its TCB, the key declarations in it, everything it uses (proofs included), what it uses directly.",
|
|
19228
|
+
who: "all",
|
|
19229
|
+
where: "desktop",
|
|
19230
|
+
since: "0.1.0",
|
|
19231
|
+
about: ["#rests-steps", "key:2"],
|
|
19232
|
+
showMe: "rests-steps",
|
|
19233
|
+
// rests-only-tcb: the reader's fifth pick under Rests on · TCB (counted in this browser), never
|
|
19234
|
+
// having chosen Keys, Uses or Direct. At #rests-steps. Ends: any other step chosen (2 again, or a
|
|
19235
|
+
// click on Keys, Uses or Direct); Got it.
|
|
19236
|
+
when: "rests-only-tcb",
|
|
19237
|
+
manual: "what-you-see"
|
|
19238
|
+
},
|
|
19239
|
+
{
|
|
19240
|
+
id: "used-by",
|
|
19241
|
+
group: "seeing",
|
|
19242
|
+
title: "What uses it",
|
|
19243
|
+
text: "<b>Used by</b> (<kbd>3</kbd>) lights everything that uses a declaration: what a change to it reaches.",
|
|
19244
|
+
who: "all",
|
|
19245
|
+
where: "both",
|
|
19246
|
+
since: "0.1.0",
|
|
19247
|
+
about: ["#lightup [data-mode=apps]", "key:3"],
|
|
19248
|
+
showMe: "used-by",
|
|
19249
|
+
manual: "what-you-see"
|
|
19250
|
+
},
|
|
19251
|
+
{
|
|
19252
|
+
id: "colours-status",
|
|
19253
|
+
group: "seeing",
|
|
19254
|
+
title: "Colour by status",
|
|
19255
|
+
text: "The legend at the bottom left, or <kbd>c</kbd>, colours by what reviewers said: approved, commented, stale, not yet reviewed, trusted. Open it for how many of each.",
|
|
19256
|
+
who: "all",
|
|
19257
|
+
where: "both",
|
|
19258
|
+
since: "0.1.0",
|
|
19259
|
+
about: ["#legend", "key:c"],
|
|
19260
|
+
showMe: "colour-status",
|
|
19261
|
+
manual: "what-you-see"
|
|
19262
|
+
},
|
|
19263
|
+
// ─────────────────────────────────────────────────────────────── reviewing
|
|
19264
|
+
{
|
|
19265
|
+
id: "what-changed",
|
|
19266
|
+
group: "reviewing",
|
|
19267
|
+
title: "What changed",
|
|
19268
|
+
text: "An opinion given on an earlier version says so: <b>What changed</b> shows the lines removed and added since, so a second reading takes only the change.",
|
|
19269
|
+
who: "all",
|
|
19270
|
+
where: "both",
|
|
19271
|
+
since: "0.1.0",
|
|
19272
|
+
about: ["#panel [data-rv-changes]"],
|
|
19273
|
+
showMe: "what-changed",
|
|
19274
|
+
// stale-opinion: the panel shows a declaration with an opinion given on an earlier version (by
|
|
19275
|
+
// anyone), its What changed closed, for the first time in this browser. At the panel's
|
|
19276
|
+
// [data-rv-changes] summary. Ends: What changed opened; Got it.
|
|
19277
|
+
when: "stale-opinion",
|
|
19278
|
+
manual: "review"
|
|
19279
|
+
},
|
|
19280
|
+
{
|
|
19281
|
+
id: "trusted",
|
|
19282
|
+
group: "reviewing",
|
|
19283
|
+
title: "Trusted without review",
|
|
19284
|
+
text: "Mathlib, Lean core and the standard axioms are trusted: not counted, in need of no opinion. <b>Trusted</b>, over the clusters, lists them; a row's shield trusts a part of the project.",
|
|
19285
|
+
who: "full",
|
|
19286
|
+
where: "both",
|
|
19287
|
+
since: "0.1.0",
|
|
19288
|
+
about: ["#trusted-btn", "#trust-list"],
|
|
19289
|
+
showMe: "ring-trusted",
|
|
19290
|
+
manual: "review"
|
|
19291
|
+
},
|
|
19292
|
+
// ─────────────────────────────────────────────────────────────── arranging
|
|
19293
|
+
{
|
|
19294
|
+
id: "drag-to-pin",
|
|
19295
|
+
group: "arranging",
|
|
19296
|
+
title: "Move things",
|
|
19297
|
+
text: "Hold <kbd>Ctrl</kbd> (<kbd>\u2318</kbd> on a Mac) and drag, or let the lock go (<kbd>l</kbd>): what you drop stays there, for everyone on this board. <b>Re-layout</b> drops the pins.",
|
|
19298
|
+
who: "full",
|
|
19299
|
+
where: "desktop",
|
|
19300
|
+
since: "0.1.0",
|
|
19301
|
+
about: ["#lock-btn", "#relayout", "key:l"],
|
|
19302
|
+
showMe: "ring-lock",
|
|
19303
|
+
manual: "what-you-see"
|
|
19304
|
+
},
|
|
19305
|
+
{
|
|
19306
|
+
id: "board-scope",
|
|
19307
|
+
group: "arranging",
|
|
19308
|
+
title: "What a board holds",
|
|
19309
|
+
text: "<b>\u2699 Board</b> sets what is on this board \u2014 chips such as <i>TCB of X</i> or <i>except trusted</i> \u2014 its clustering and its depth, for everyone on it.",
|
|
19310
|
+
who: "full",
|
|
19311
|
+
where: "desktop",
|
|
19312
|
+
since: "0.1.0",
|
|
19313
|
+
about: ["#board-btn", "#board-pop"],
|
|
19314
|
+
showMe: "ring-board",
|
|
19315
|
+
manual: "views"
|
|
19316
|
+
},
|
|
19317
|
+
// ─────────────────────────────────────────────────────────────── together
|
|
19318
|
+
{
|
|
19319
|
+
id: "follow",
|
|
19320
|
+
group: "together",
|
|
19321
|
+
title: "Follow someone",
|
|
19322
|
+
text: "Click someone's initials to see what they see: their view, camera, selection and windows. Anything you change ends it, and so does Esc.",
|
|
19323
|
+
who: "all",
|
|
19324
|
+
where: "both",
|
|
19325
|
+
since: "0.1.0",
|
|
19326
|
+
about: ["#peers", "#following"],
|
|
19327
|
+
showMe: "ring-peers",
|
|
19328
|
+
manual: "together"
|
|
19329
|
+
},
|
|
19330
|
+
{
|
|
19331
|
+
id: "read-only-link",
|
|
19332
|
+
group: "together",
|
|
19333
|
+
title: "Read-only links",
|
|
19334
|
+
text: "Share \u2192 <b>Read-only link</b>: whoever has it reads the project, the review and the history, changes nothing shared, and nobody sees them.",
|
|
19335
|
+
who: "full",
|
|
19336
|
+
where: "both",
|
|
19337
|
+
since: "0.1.0",
|
|
19338
|
+
about: ["#share-btn", "#share-pop [data-tab=readonly]"],
|
|
19339
|
+
showMe: "share-readonly",
|
|
19340
|
+
// share-twice: Share opened for the second time in this page's session, on a server whose Share
|
|
19341
|
+
// has the Read-only link tab (started with --access-key-file), the reader never having opened
|
|
19342
|
+
// that tab in this browser. At #share-pop [data-tab=readonly]. Ends: that tab opened; Got it.
|
|
19343
|
+
// Without that tab on this server, the tip itself is not shown.
|
|
19344
|
+
when: "share-twice",
|
|
19345
|
+
manual: "together"
|
|
19346
|
+
},
|
|
19347
|
+
// ─────────────────────────────────────────────────────────────── export
|
|
19348
|
+
{
|
|
19349
|
+
id: "linear-form",
|
|
19350
|
+
group: "export",
|
|
19351
|
+
title: "The board as an outline",
|
|
19352
|
+
text: "<kbd>o</kbd> draws the board as its document's outline, on one line, the first section at the top: drag along it to reorder what the PDF prints.",
|
|
19353
|
+
who: "full",
|
|
19354
|
+
where: "desktop",
|
|
19355
|
+
since: "0.2.0",
|
|
19356
|
+
about: ["#linear", "key:o"],
|
|
19357
|
+
showMe: "linear",
|
|
19358
|
+
manual: "exported-documents"
|
|
19359
|
+
},
|
|
19360
|
+
// ─────────────────────────────────────────────────────────────── history
|
|
19361
|
+
{
|
|
19362
|
+
id: "time-machine",
|
|
19363
|
+
group: "history",
|
|
19364
|
+
title: "The Time Machine",
|
|
19365
|
+
text: "<kbd>t</kbd> shows this view at a past build: what is gone is a red ghost, what changed since has an amber ring. <kbd>\u2190</kbd> <kbd>\u2192</kbd> step through the builds; Esc leaves.",
|
|
19366
|
+
who: "all",
|
|
19367
|
+
where: "desktop",
|
|
19368
|
+
since: "0.1.0",
|
|
19369
|
+
about: ["#tmbar", "key:t"],
|
|
19370
|
+
showMe: "time-machine",
|
|
19371
|
+
// first-time-machine: the Time Machine opened by the reader (t, the build popover, a link) for
|
|
19372
|
+
// the first time in this browser. At #tmbar, its ruler. Ends: another build reached (← or →);
|
|
19373
|
+
// Got it; Esc puts it away, unanswered.
|
|
19374
|
+
when: "first-time-machine",
|
|
19375
|
+
manual: "review"
|
|
19376
|
+
},
|
|
19377
|
+
{
|
|
19378
|
+
id: "lost-and-found",
|
|
19379
|
+
group: "history",
|
|
19380
|
+
title: "Lost and found",
|
|
19381
|
+
text: "What was said of a declaration the graph no longer has waits in the build status's <b>Lost-and-found</b>: attach it to the new name, or archive it.",
|
|
19382
|
+
who: "full",
|
|
19383
|
+
where: "both",
|
|
19384
|
+
since: "0.1.0",
|
|
19385
|
+
about: ["#build", "#build-pop [data-bp=lost]", "#lostfound"],
|
|
19386
|
+
showMe: "ring-lost",
|
|
19387
|
+
manual: "review"
|
|
19388
|
+
}
|
|
19389
|
+
];
|
|
19390
|
+
|
|
19391
|
+
// src/web/src/tipstate.ts
|
|
19392
|
+
var KEY2 = "visualproof:tips";
|
|
19393
|
+
function newer(a3, b2) {
|
|
19394
|
+
const parse2 = (v2) => {
|
|
19395
|
+
const m3 = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(v2.trim());
|
|
19396
|
+
return m3 ? { n: [+m3[1], +m3[2], +m3[3]], pre: m3[4] ?? null } : null;
|
|
19397
|
+
};
|
|
19398
|
+
const x2 = parse2(a3), y2 = parse2(b2);
|
|
19399
|
+
if (!x2 || !y2) return false;
|
|
19400
|
+
for (let i2 = 0; i2 < 3; i2++) if (x2.n[i2] !== y2.n[i2]) return x2.n[i2] > y2.n[i2];
|
|
19401
|
+
if (x2.pre === y2.pre) return false;
|
|
19402
|
+
if (x2.pre === null) return true;
|
|
19403
|
+
if (y2.pre === null) return false;
|
|
19404
|
+
return x2.pre > y2.pre;
|
|
19405
|
+
}
|
|
19406
|
+
function read() {
|
|
19407
|
+
let raw;
|
|
19408
|
+
try {
|
|
19409
|
+
raw = localStorage.getItem(KEY2);
|
|
19410
|
+
} catch {
|
|
19411
|
+
return null;
|
|
19412
|
+
}
|
|
19413
|
+
try {
|
|
19414
|
+
const o2 = raw ? JSON.parse(raw) : null;
|
|
19415
|
+
if (!o2 || typeof o2 !== "object") return { shown: {}, counts: {}, done: [] };
|
|
19416
|
+
const ids = (x2) => Array.isArray(x2) ? x2.filter((s2) => typeof s2 === "string") : [];
|
|
19417
|
+
const numbers = (x2) => x2 && typeof x2 === "object" ? Object.fromEntries(Object.entries(x2).filter((e2) => Number.isInteger(e2[1]) && e2[1] > 0)) : {};
|
|
19418
|
+
return { shown: numbers(o2.shown), counts: numbers(o2.counts), done: ids(o2.done), off: o2.off === true || void 0, version: typeof o2.version === "string" ? o2.version : void 0, newSince: typeof o2.newSince === "string" ? o2.newSince : void 0, dot: o2.dot === true || void 0 };
|
|
19419
|
+
} catch {
|
|
19420
|
+
return { shown: {}, counts: {}, done: [] };
|
|
19421
|
+
}
|
|
19422
|
+
}
|
|
19423
|
+
var TipState = class {
|
|
19424
|
+
d;
|
|
19425
|
+
constructor() {
|
|
19426
|
+
this.d = read() ?? { shown: {}, counts: {}, done: [] };
|
|
19427
|
+
if (typeof addEventListener === "function")
|
|
19428
|
+
addEventListener("storage", (e2) => {
|
|
19429
|
+
if (e2.key === KEY2 || e2.key === null) this.d = read() ?? this.d;
|
|
19430
|
+
});
|
|
19431
|
+
}
|
|
19432
|
+
/** A change made to what is kept as it is now, not to this page's copy of it: pages open at once,
|
|
19433
|
+
* on one project or on two, never write back each other's older state. */
|
|
19434
|
+
change(f2) {
|
|
19435
|
+
const d2 = read() ?? this.d;
|
|
19436
|
+
const out = f2(d2);
|
|
19437
|
+
this.d = d2;
|
|
19438
|
+
try {
|
|
19439
|
+
localStorage.setItem(KEY2, JSON.stringify(d2));
|
|
19440
|
+
} catch {
|
|
19441
|
+
}
|
|
19442
|
+
return out;
|
|
19443
|
+
}
|
|
19444
|
+
isDone(id) {
|
|
19445
|
+
return this.d.done.includes(id);
|
|
19446
|
+
}
|
|
19447
|
+
done(id) {
|
|
19448
|
+
if (this.isDone(id)) return;
|
|
19449
|
+
this.change((d2) => {
|
|
19450
|
+
if (!d2.done.includes(id)) d2.done.push(id);
|
|
19451
|
+
});
|
|
19452
|
+
}
|
|
19453
|
+
/** A tip shown in place: the second time, it is done, answered or not. */
|
|
19454
|
+
shown(id) {
|
|
19455
|
+
this.change((d2) => {
|
|
19456
|
+
const n3 = (d2.shown[id] ?? 0) + 1;
|
|
19457
|
+
d2.shown[id] = n3;
|
|
19458
|
+
if (n3 >= 2 && !d2.done.includes(id)) d2.done.push(id);
|
|
19459
|
+
});
|
|
19460
|
+
}
|
|
19461
|
+
/** One more of what a rule counts; how many now. */
|
|
19462
|
+
bump(name) {
|
|
19463
|
+
return this.change((d2) => d2.counts[name] = (d2.counts[name] ?? 0) + 1);
|
|
19464
|
+
}
|
|
19465
|
+
get off() {
|
|
19466
|
+
return this.d.off === true;
|
|
19467
|
+
}
|
|
19468
|
+
set off(v2) {
|
|
19469
|
+
this.change((d2) => void (d2.off = v2 || void 0));
|
|
19470
|
+
}
|
|
19471
|
+
/** The dot on the help: shown until the help is opened, the tour taken, or Tips looked at. */
|
|
19472
|
+
get dot() {
|
|
19473
|
+
return this.d.dot === true;
|
|
19474
|
+
}
|
|
19475
|
+
set dot(v2) {
|
|
19476
|
+
if (this.dot === v2 && read()?.dot === (v2 || void 0)) return;
|
|
19477
|
+
this.change((d2) => void (d2.dot = v2 || void 0));
|
|
19478
|
+
}
|
|
19479
|
+
/** The releases New is told against: tips newer than `since` are New; null when none is. */
|
|
19480
|
+
get newSince() {
|
|
19481
|
+
return this.d.newSince ?? null;
|
|
19482
|
+
}
|
|
19483
|
+
/** Tips has been looked at: nothing is New any more. */
|
|
19484
|
+
newLooked() {
|
|
19485
|
+
if (this.d.newSince === void 0 && read()?.newSince === void 0) return;
|
|
19486
|
+
this.change((d2) => void (d2.newSince = void 0));
|
|
19487
|
+
}
|
|
19488
|
+
/** The server runs release `v`: the release this browser saw before it when it is newer (What's
|
|
19489
|
+
* new is told), else null. Read and written at once, so that it is told once in the browser, not
|
|
19490
|
+
* once in each page open. A first visit records it and tells nothing. */
|
|
19491
|
+
meet(v2) {
|
|
19492
|
+
return this.change((d2) => {
|
|
19493
|
+
const was = d2.version;
|
|
19494
|
+
if (was === v2) return null;
|
|
19495
|
+
if (was !== void 0 && !newer(v2, was)) return null;
|
|
19496
|
+
d2.version = v2;
|
|
19497
|
+
if (was !== void 0) d2.newSince = d2.newSince !== void 0 && newer(was, d2.newSince) ? d2.newSince : was;
|
|
19498
|
+
return was ?? null;
|
|
19499
|
+
});
|
|
19500
|
+
}
|
|
19501
|
+
};
|
|
19502
|
+
|
|
19503
|
+
// src/web/src/tipcards.ts
|
|
19504
|
+
function tipsFor(ctx, tips = TIPS) {
|
|
19505
|
+
return tips.filter((t2) => !(ctx.readOnly && t2.who === "full") && (t2.where === "both" || t2.where === (ctx.phone ? "phone" : "desktop")) && ctx.applies(t2));
|
|
19506
|
+
}
|
|
19507
|
+
function plain(html) {
|
|
19508
|
+
return html.replace(/<[^>]*>/g, "").replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
19509
|
+
}
|
|
19510
|
+
function matches(t2, filter) {
|
|
19511
|
+
const words = filter.toLowerCase().split(/\s+/).filter(Boolean);
|
|
19512
|
+
const text2 = `${t2.title} ${plain(t2.text)}`.toLowerCase();
|
|
19513
|
+
return words.every((w2) => text2.includes(w2));
|
|
19514
|
+
}
|
|
19515
|
+
var reducedMotion = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
19516
|
+
function actsHtml(showMe, manual, ctx, attr) {
|
|
19517
|
+
const acts2 = [
|
|
19518
|
+
showMe && ctx.canShow(showMe) ? `<button class="link" ${attr}>Show me</button>` : "",
|
|
19519
|
+
manual ? `<a class="link" href="${ctx.manual}#${manual}" target="_blank" rel="noopener noreferrer">More</a>` : ""
|
|
19520
|
+
].filter(Boolean);
|
|
19521
|
+
return acts2.length ? `<div class="tip-acts">${acts2.join("")}</div>` : "";
|
|
19522
|
+
}
|
|
19523
|
+
function rowHtml(t2, ctx, open) {
|
|
19524
|
+
const isNew = ctx.newSince !== null && newer(t2.since, ctx.newSince);
|
|
19525
|
+
const clip = open && ctx.phone && t2.clip && ctx.clips.has(t2.clip) ? `<div class="ref-clip"><video muted loop playsinline preload="auto" poster="/tips/${t2.clip}.light.webp" src="/tips/${t2.clip}.light.mp4"${reducedMotion() ? " controls" : " autoplay"} aria-label="${escapeHtml(t2.title)}: a short clip"></video></div>` : "";
|
|
19526
|
+
return `<div class="ref${open ? " open" : ""}" data-tip="${t2.id}"><button class="ref-head" data-ref-toggle="${t2.id}" aria-expanded="${open}"><span class="ref-title">${escapeHtml(t2.title)}${isNew ? ` <span class="tip-new">New</span>` : ""}</span><span class="ref-line">${escapeHtml(plain(t2.text))}</span></button>` + (open ? `<div class="ref-body"><div class="tip-text">${t2.text}</div>${clip}${actsHtml(t2.showMe, t2.manual, ctx, `data-tip-show="${t2.id}"`)}</div>` : "") + `</div>`;
|
|
19527
|
+
}
|
|
19528
|
+
function refsHtml(ctx, filter, open) {
|
|
19529
|
+
const mine = tipsFor(ctx).filter((t2) => matches(t2, filter));
|
|
19530
|
+
if (!mine.length) return `<div class="tips-none help-note">No tip says that. The <b>Keys</b> tab lists every key.</div>`;
|
|
19531
|
+
return GROUPS.map((g2) => {
|
|
19532
|
+
const of = mine.filter((t2) => t2.group === g2.id);
|
|
19533
|
+
return of.length ? `<section class="tips-group"><div class="help-h">${g2.title}</div><div class="refs">${of.map((t2) => rowHtml(t2, ctx, t2.id === open)).join("")}</div></section>` : "";
|
|
19534
|
+
}).join("");
|
|
19535
|
+
}
|
|
19536
|
+
|
|
19537
|
+
// src/web/src/tourfilm.ts
|
|
19538
|
+
function tourData(raw, tour) {
|
|
19539
|
+
const o2 = raw;
|
|
19540
|
+
const f2 = o2?.film;
|
|
19541
|
+
if (!f2 || typeof f2.duration !== "number" || !(f2.duration > 0) || !Array.isArray(o2.chapters)) return null;
|
|
19542
|
+
const duration = f2.duration;
|
|
19543
|
+
const byId = /* @__PURE__ */ new Map();
|
|
19544
|
+
for (const c3 of o2.chapters)
|
|
19545
|
+
if (c3 && typeof c3.id === "string" && typeof c3.start === "number" && typeof c3.end === "number" && c3.end > c3.start) byId.set(c3.id, { start: c3.start, end: c3.end });
|
|
19546
|
+
const chapters = [];
|
|
19547
|
+
let at = 0;
|
|
19548
|
+
for (const c3 of tour) {
|
|
19549
|
+
const t2 = byId.get(c3.id);
|
|
19550
|
+
if (!t2 || t2.start < at - 0.05 || t2.end > duration + 0.05) return null;
|
|
19551
|
+
chapters.push({ id: c3.id, ...t2 });
|
|
19552
|
+
at = t2.end;
|
|
19553
|
+
}
|
|
19554
|
+
const captions = (Array.isArray(o2.captions) ? o2.captions : []).filter((c3) => !!c3 && typeof c3.start === "number" && typeof c3.end === "number" && c3.end > c3.start && c3.start >= 0 && c3.end <= duration + 0.05 && typeof c3.text === "string" && !!c3.text.trim() && c3.text.length <= 200).map((c3) => ({ start: c3.start, end: c3.end, text: c3.text.trim() })).sort((a3, b2) => a3.start - b2.start);
|
|
19555
|
+
const w2 = typeof f2.width === "number" && f2.width > 0 ? f2.width : 16;
|
|
19556
|
+
const h2 = typeof f2.height === "number" && f2.height > 0 ? f2.height : 10;
|
|
19557
|
+
return { duration, width: w2, height: h2, chapters, captions };
|
|
19558
|
+
}
|
|
19559
|
+
function cueAt(captions, t2) {
|
|
19560
|
+
let lo = 0, hi = captions.length - 1, found = null;
|
|
19561
|
+
while (lo <= hi) {
|
|
19562
|
+
const mid = lo + hi >> 1;
|
|
19563
|
+
if (captions[mid].start <= t2) {
|
|
19564
|
+
found = captions[mid];
|
|
19565
|
+
lo = mid + 1;
|
|
19566
|
+
} else hi = mid - 1;
|
|
19567
|
+
}
|
|
19568
|
+
return found && t2 < found.end ? found : null;
|
|
19569
|
+
}
|
|
19570
|
+
function cuesOf(captions, ch) {
|
|
19571
|
+
return captions.filter((c3) => c3.start >= ch.start - 0.01 && c3.start < ch.end);
|
|
19572
|
+
}
|
|
19573
|
+
var POLL_FIRST_MS = 2e3;
|
|
19574
|
+
var POLL_QUICK_FOR_MS = 3e4;
|
|
19575
|
+
var POLL_LATER_S = 10;
|
|
19576
|
+
var PAGE_CLOCK = { now: () => Date.now(), after: (f2, ms) => setTimeout(f2, ms), cancel: (t2) => clearTimeout(t2) };
|
|
19577
|
+
var FilmWaiter = class {
|
|
19578
|
+
ask;
|
|
19579
|
+
told;
|
|
19580
|
+
clock;
|
|
19581
|
+
timer = null;
|
|
19582
|
+
/** when the asking began, for the first quick ones */
|
|
19583
|
+
since = null;
|
|
19584
|
+
running = false;
|
|
19585
|
+
done = false;
|
|
19586
|
+
constructor(ask, told, clock = PAGE_CLOCK) {
|
|
19587
|
+
this.ask = ask;
|
|
19588
|
+
this.told = told;
|
|
19589
|
+
this.clock = clock;
|
|
19590
|
+
}
|
|
19591
|
+
start() {
|
|
19592
|
+
if (this.running || this.done) return;
|
|
19593
|
+
this.running = true;
|
|
19594
|
+
this.since ??= this.clock.now();
|
|
19595
|
+
void this.tick();
|
|
19596
|
+
}
|
|
19597
|
+
stop() {
|
|
19598
|
+
this.running = false;
|
|
19599
|
+
if (this.timer !== null) this.clock.cancel(this.timer);
|
|
19600
|
+
this.timer = null;
|
|
19601
|
+
}
|
|
19602
|
+
async tick() {
|
|
19603
|
+
this.timer = null;
|
|
19604
|
+
let a3;
|
|
19605
|
+
try {
|
|
19606
|
+
a3 = await this.ask();
|
|
19607
|
+
} catch {
|
|
19608
|
+
a3 = { status: 0, retryAfter: null };
|
|
19609
|
+
}
|
|
19610
|
+
if (!this.running) return;
|
|
19611
|
+
if (a3.status === 200 || a3.status === 206 || a3.status === 404) {
|
|
19612
|
+
this.done = true;
|
|
19613
|
+
this.running = false;
|
|
19614
|
+
return this.told(a3.status === 404 ? "absent" : "ready");
|
|
19615
|
+
}
|
|
19616
|
+
this.told("loading");
|
|
19617
|
+
const quick = this.clock.now() - this.since < POLL_QUICK_FOR_MS;
|
|
19618
|
+
this.timer = this.clock.after(() => void this.tick(), quick ? POLL_FIRST_MS : Math.max(POLL_FIRST_MS, (a3.retryAfter ?? POLL_LATER_S) * 1e3));
|
|
19619
|
+
}
|
|
19620
|
+
};
|
|
19621
|
+
async function askFilm() {
|
|
19622
|
+
const r2 = await fetch(FILM, { headers: { range: "bytes=0-0" }, cache: "no-store" });
|
|
19623
|
+
void r2.body?.cancel().catch(() => {
|
|
19624
|
+
});
|
|
19625
|
+
const after = Number(r2.headers.get("retry-after"));
|
|
19626
|
+
return { status: r2.status, retryAfter: Number.isFinite(after) && after > 0 ? after : null };
|
|
19627
|
+
}
|
|
19628
|
+
var FILM_KEYS = [" ", "ArrowLeft", "ArrowRight", "c"];
|
|
19629
|
+
var FILM = "/tips/tour.mp4";
|
|
19630
|
+
var POSTER = "/tips/tour.webp";
|
|
19631
|
+
var VTT = "/tips/tour.en.vtt";
|
|
19632
|
+
var stillOf = (id) => `/tips/tour-${id}.webp`;
|
|
19633
|
+
var CC_KEY = "visualproof:cc";
|
|
19634
|
+
function ccOn() {
|
|
19635
|
+
try {
|
|
19636
|
+
return localStorage.getItem(CC_KEY) !== "off";
|
|
19637
|
+
} catch {
|
|
19638
|
+
return true;
|
|
19639
|
+
}
|
|
19640
|
+
}
|
|
19641
|
+
var TourFilm = class {
|
|
19642
|
+
root;
|
|
19643
|
+
tour;
|
|
19644
|
+
data;
|
|
19645
|
+
opts;
|
|
19646
|
+
/** the chapter open: playing, or shown */
|
|
19647
|
+
at = 0;
|
|
19648
|
+
video = null;
|
|
19649
|
+
/** whether the film is here: not yet known, being fetched by the server, there, or not (404) */
|
|
19650
|
+
film = "unknown";
|
|
19651
|
+
/** whether the reader wants it playing: not after they paused it, nor with reduced motion */
|
|
19652
|
+
wantPlay;
|
|
19653
|
+
waiter = null;
|
|
19654
|
+
cc = ccOn();
|
|
19655
|
+
/** normal, as large as the sheet, the whole screen */
|
|
19656
|
+
size = 0;
|
|
19657
|
+
frame = 0;
|
|
19658
|
+
onFullscreen = () => {
|
|
19659
|
+
if (this.size === 2 && !document.fullscreenElement) this.resize(1);
|
|
19660
|
+
};
|
|
19661
|
+
constructor(root, tour, data, opts) {
|
|
19662
|
+
this.root = root;
|
|
19663
|
+
this.tour = tour;
|
|
19664
|
+
this.data = data;
|
|
19665
|
+
this.opts = opts;
|
|
19666
|
+
this.wantPlay = !opts.still;
|
|
19667
|
+
root.addEventListener("click", (e2) => {
|
|
19668
|
+
const t2 = e2.target;
|
|
19669
|
+
const go = t2.closest("[data-chapter]");
|
|
19670
|
+
if (t2.closest("[data-film-play]")) this.toggle();
|
|
19671
|
+
else if (t2.closest("[data-film-cc]")) this.setCc(!this.cc);
|
|
19672
|
+
else if (t2.closest("[data-film-large]")) this.resize(this.size === 0 ? 1 : this.size === 1 ? 2 : 1);
|
|
19673
|
+
else if (t2.closest("[data-film-list]")) root.querySelector(".film")?.classList.toggle("list-hidden");
|
|
19674
|
+
else if (go && !t2.closest("a, [data-tour-show]")) this.go(this.index(go.dataset.chapter), true);
|
|
19675
|
+
});
|
|
19676
|
+
root.addEventListener("keydown", (e2) => {
|
|
19677
|
+
const t2 = e2.target;
|
|
19678
|
+
const card = t2.closest(".film-card[data-chapter]");
|
|
19679
|
+
if (card && (e2.key === "Enter" || e2.key === " ")) {
|
|
19680
|
+
e2.preventDefault();
|
|
19681
|
+
e2.stopPropagation();
|
|
19682
|
+
return this.go(this.index(card.dataset.chapter), true);
|
|
19683
|
+
}
|
|
19684
|
+
if (!this.data || t2.closest("button, a, input") || !FILM_KEYS.includes(e2.key) || e2.ctrlKey || e2.metaKey || e2.altKey) return;
|
|
19685
|
+
e2.preventDefault();
|
|
19686
|
+
e2.stopPropagation();
|
|
19687
|
+
if (e2.key === " ") this.toggle();
|
|
19688
|
+
else if (e2.key === "c") this.setCc(!this.cc);
|
|
19689
|
+
else this.go(Math.max(0, Math.min(this.tour.length - 1, this.at + (e2.key === "ArrowLeft" ? -1 : 1))), !!this.video && !this.video.paused);
|
|
19690
|
+
});
|
|
19691
|
+
document.addEventListener("fullscreenchange", this.onFullscreen);
|
|
19692
|
+
this.render();
|
|
19693
|
+
}
|
|
19694
|
+
index(id) {
|
|
19695
|
+
return Math.max(0, this.tour.findIndex((c3) => c3.id === id));
|
|
19696
|
+
}
|
|
19697
|
+
/** Whether it is larger than the sheet's own place: Esc steps it back (`back`). */
|
|
19698
|
+
get large() {
|
|
19699
|
+
return this.size > 0;
|
|
19700
|
+
}
|
|
19701
|
+
/** Esc: from the whole screen to the sheet, from the sheet to its place. True where it stepped. */
|
|
19702
|
+
back() {
|
|
19703
|
+
if (this.size === 0) return false;
|
|
19704
|
+
this.resize(this.size - 1);
|
|
19705
|
+
return true;
|
|
19706
|
+
}
|
|
19707
|
+
/** The words of an open card: its text, its captions where the film is not playing them (no film,
|
|
19708
|
+
* reduced motion), its actions. */
|
|
19709
|
+
body(c3, k2) {
|
|
19710
|
+
const ch = this.data?.chapters[k2];
|
|
19711
|
+
const cues = ch && (this.film === "absent" || this.film === "loading" || this.opts.still) ? cuesOf(this.data.captions, ch) : [];
|
|
19712
|
+
const list2 = cues.length ? `<ol class="film-cues" aria-label="Captions">${cues.map((q2) => `<li>${escapeHtml(q2.text)}</li>`).join("")}</ol>` : "";
|
|
19713
|
+
return `<div class="tip-text">${c3.text}</div>${list2}${this.opts.acts(c3)}`;
|
|
19714
|
+
}
|
|
19715
|
+
render() {
|
|
19716
|
+
const cards = this.tour.map((c3, k2) => {
|
|
19717
|
+
const on = k2 === this.at;
|
|
19718
|
+
return `<li class="film-item${on ? " on" : ""}"><div class="film-card" data-chapter="${c3.id}" role="button" tabindex="0" aria-expanded="${on}"${on ? ` aria-current="step"` : ""}><span class="film-n">${k2 + 1}</span><span class="film-title">${escapeHtml(c3.title)}</span></div><div class="film-body"${on ? "" : " hidden"}>${on ? this.body(c3, k2) : ""}</div></li>`;
|
|
19719
|
+
}).join("");
|
|
19720
|
+
const name = `A review in ${this.tour.length === 13 ? "thirteen" : this.tour.length} scenes`;
|
|
19721
|
+
if (!this.data) {
|
|
19722
|
+
this.root.innerHTML = `<ol class="film-list words" aria-label="${name}">${cards}</ol>`;
|
|
19723
|
+
this.video = null;
|
|
19724
|
+
return;
|
|
19725
|
+
}
|
|
19726
|
+
const d2 = this.data;
|
|
19727
|
+
const segs = d2.chapters.map((t2, k2) => `<button class="film-seg" data-chapter="${t2.id}" style="flex-grow: ${(t2.end - t2.start).toFixed(2)}" aria-label="${k2 + 1}. ${escapeHtml(this.tour[k2].title)}" title="${escapeHtml(this.tour[k2].title)}"><span class="film-track"><span class="film-fill"></span></span></button>`).join("");
|
|
19728
|
+
const fs = typeof document.documentElement.requestFullscreen === "function";
|
|
19729
|
+
this.root.innerHTML = `<div class="film"><div class="film-stage" tabindex="0" aria-label="${name}: Space plays or pauses, the arrows go to the chapter before or after, c turns the captions on or off"><div class="film-frame" style="aspect-ratio: ${d2.width} / ${d2.height}"><video class="film-video" muted playsinline preload="metadata" poster="${POSTER}" aria-label="${name}: the film"><track kind="captions" src="${VTT}" srclang="en" label="English" default></video><img class="film-still" alt="" hidden><div class="film-wait" role="status" hidden>Loading the film\u2026</div><div class="film-cc" aria-hidden="true" hidden></div><div class="film-controls"><button data-film-play aria-label="Play" title="Play">\u25B6</button><span class="film-gap"></span><button data-film-cc aria-pressed="${this.cc}" title="Captions (c)">CC</button><button data-film-large aria-label="Larger" title="Larger">\u2922</button></div></div><div class="film-bar" role="group" aria-label="Chapters">${segs}</div></div><div class="film-side"><button class="film-list-btn" data-film-list title="Show or hide the chapters">Chapters</button><ol class="film-list" aria-label="Chapters">${cards}</ol></div></div>`;
|
|
19730
|
+
if (!fs) this.root.querySelector("[data-film-large]").dataset.noFull = "";
|
|
19731
|
+
const v2 = this.root.querySelector("video");
|
|
19732
|
+
this.video = v2;
|
|
19733
|
+
const hide = () => {
|
|
19734
|
+
for (const tt2 of Array.from(v2.textTracks)) tt2.mode = "hidden";
|
|
19735
|
+
};
|
|
19736
|
+
v2.textTracks.addEventListener?.("addtrack", hide);
|
|
19737
|
+
v2.addEventListener("loadedmetadata", hide);
|
|
19738
|
+
hide();
|
|
19739
|
+
v2.addEventListener("loadedmetadata", () => {
|
|
19740
|
+
if (this.film !== "unknown") return;
|
|
19741
|
+
this.film = "ok";
|
|
19742
|
+
v2.currentTime = this.data.chapters[this.at].start;
|
|
19743
|
+
if (this.wantPlay && !this.opts.still) {
|
|
19744
|
+
this.hideStill();
|
|
19745
|
+
void v2.play().catch(() => {
|
|
19746
|
+
});
|
|
19747
|
+
} else if (this.opts.still) this.showStill(this.at);
|
|
19748
|
+
else this.hideStill();
|
|
19749
|
+
this.show();
|
|
19750
|
+
});
|
|
19751
|
+
v2.addEventListener("error", () => {
|
|
19752
|
+
if (!v2.getAttribute("src")) return;
|
|
19753
|
+
this.film = "absent";
|
|
19754
|
+
this.show();
|
|
19755
|
+
});
|
|
19756
|
+
this.waiter = new FilmWaiter(askFilm, (state) => {
|
|
19757
|
+
if (state === "ready") {
|
|
19758
|
+
this.film = "unknown";
|
|
19759
|
+
v2.src = FILM;
|
|
19760
|
+
v2.load();
|
|
19761
|
+
} else this.film = state;
|
|
19762
|
+
this.show();
|
|
19763
|
+
});
|
|
19764
|
+
v2.addEventListener("play", () => this.sync());
|
|
19765
|
+
v2.addEventListener("pause", () => this.sync());
|
|
19766
|
+
v2.addEventListener("ended", () => this.sync());
|
|
19767
|
+
v2.addEventListener("timeupdate", () => this.follow());
|
|
19768
|
+
v2.addEventListener("seeked", () => this.follow());
|
|
19769
|
+
}
|
|
19770
|
+
/** Begin at a chapter (the first by default): it plays, unless motion is reduced — once the server
|
|
19771
|
+
* has the film, which is asked for while the sheet shows. */
|
|
19772
|
+
start(id) {
|
|
19773
|
+
this.go(id ? this.index(id) : 0, !this.opts.still);
|
|
19774
|
+
this.waiter?.start();
|
|
19775
|
+
}
|
|
19776
|
+
/** Chapter `k` open, the film at its start: playing if `play`; else (reduced motion, no film)
|
|
19777
|
+
* the chapter's still, and the play button plays from there. */
|
|
19778
|
+
go(k2, play) {
|
|
19779
|
+
this.open(k2);
|
|
19780
|
+
const v2 = this.video;
|
|
19781
|
+
if (!v2 || !this.data) return;
|
|
19782
|
+
if (play && !this.opts.still) this.wantPlay = true;
|
|
19783
|
+
if (this.film === "ok") v2.currentTime = this.data.chapters[k2].start;
|
|
19784
|
+
if (this.opts.still || this.film !== "ok" || !play) {
|
|
19785
|
+
v2.pause();
|
|
19786
|
+
if (this.opts.still || this.film !== "ok") this.showStill(k2);
|
|
19787
|
+
} else {
|
|
19788
|
+
this.hideStill();
|
|
19789
|
+
void v2.play().catch(() => {
|
|
19790
|
+
});
|
|
19791
|
+
}
|
|
19792
|
+
this.sync();
|
|
19793
|
+
}
|
|
19794
|
+
showStill(k2) {
|
|
19795
|
+
const still = this.root.querySelector(".film-still");
|
|
19796
|
+
if (!still) return;
|
|
19797
|
+
still.src = stillOf(this.tour[k2].id);
|
|
19798
|
+
still.hidden = false;
|
|
19799
|
+
}
|
|
19800
|
+
hideStill() {
|
|
19801
|
+
const still = this.root.querySelector(".film-still");
|
|
19802
|
+
if (still) still.hidden = true;
|
|
19803
|
+
}
|
|
19804
|
+
/** The film found, coming, or not: the open card again (its captions listed or not), its still,
|
|
19805
|
+
* the quiet word that it is loading. */
|
|
19806
|
+
show() {
|
|
19807
|
+
this.open(this.at, true);
|
|
19808
|
+
if (this.film === "absent" || this.film === "loading" || this.opts.still) this.showStill(this.at);
|
|
19809
|
+
const wait = this.root.querySelector(".film-wait");
|
|
19810
|
+
if (wait) wait.hidden = this.film !== "loading";
|
|
19811
|
+
this.sync();
|
|
19812
|
+
}
|
|
19813
|
+
/** The play button: play or pause; at the end, again from the start; while the film comes, to
|
|
19814
|
+
* play it when it does; without one here, nothing. */
|
|
19815
|
+
toggle() {
|
|
19816
|
+
const v2 = this.video;
|
|
19817
|
+
if (!v2 || !this.data) return;
|
|
19818
|
+
if (this.film !== "ok") {
|
|
19819
|
+
if (this.film !== "absent" && !this.opts.still) this.wantPlay = !this.wantPlay;
|
|
19820
|
+
return this.sync();
|
|
19821
|
+
}
|
|
19822
|
+
if (v2.ended || v2.currentTime >= this.data.duration - 0.05) {
|
|
19823
|
+
v2.currentTime = 0;
|
|
19824
|
+
this.open(0);
|
|
19825
|
+
}
|
|
19826
|
+
if (v2.paused) {
|
|
19827
|
+
this.wantPlay = true;
|
|
19828
|
+
this.hideStill();
|
|
19829
|
+
void v2.play().catch(() => {
|
|
19830
|
+
});
|
|
19831
|
+
} else {
|
|
19832
|
+
this.wantPlay = false;
|
|
19833
|
+
v2.pause();
|
|
19834
|
+
}
|
|
19835
|
+
}
|
|
19836
|
+
setCc(on) {
|
|
19837
|
+
this.cc = on;
|
|
19838
|
+
try {
|
|
19839
|
+
if (on) localStorage.removeItem(CC_KEY);
|
|
19840
|
+
else localStorage.setItem(CC_KEY, "off");
|
|
19841
|
+
} catch {
|
|
19842
|
+
}
|
|
19843
|
+
this.root.querySelector("[data-film-cc]")?.setAttribute("aria-pressed", String(on));
|
|
19844
|
+
this.follow();
|
|
19845
|
+
}
|
|
19846
|
+
/** Normal, as large as the sheet, or the whole screen (the browser's fullscreen). */
|
|
19847
|
+
resize(size) {
|
|
19848
|
+
const stage = this.root.querySelector(".film-stage");
|
|
19849
|
+
if (size === 2 && (!stage || typeof stage.requestFullscreen !== "function")) size = 1;
|
|
19850
|
+
if (size === 2) void stage.requestFullscreen().catch(() => {
|
|
19851
|
+
});
|
|
19852
|
+
else if (this.size === 2 && document.fullscreenElement) void document.exitFullscreen().catch(() => {
|
|
19853
|
+
});
|
|
19854
|
+
this.size = size;
|
|
19855
|
+
this.opts.sheet.classList.toggle("film-large", size > 0);
|
|
19856
|
+
const b2 = this.root.querySelector("[data-film-large]");
|
|
19857
|
+
if (b2) {
|
|
19858
|
+
const label = size === 0 ? "Larger" : size === 1 ? "The whole screen" : "Back to the sheet";
|
|
19859
|
+
b2.textContent = size === 2 ? "\u2921" : "\u2922";
|
|
19860
|
+
b2.setAttribute("aria-label", label);
|
|
19861
|
+
b2.title = label;
|
|
19862
|
+
}
|
|
19863
|
+
this.root.querySelector(".film-stage")?.focus();
|
|
19864
|
+
}
|
|
19865
|
+
/** The card of chapter `k` open, the others one row each. */
|
|
19866
|
+
open(k2, again = false) {
|
|
19867
|
+
if (k2 === this.at && !again && this.root.querySelector(".film-item.on")) return;
|
|
19868
|
+
this.at = k2;
|
|
19869
|
+
const items = this.root.querySelectorAll(".film-item");
|
|
19870
|
+
items.forEach((li2, j2) => {
|
|
19871
|
+
const on = j2 === k2;
|
|
19872
|
+
li2.classList.toggle("on", on);
|
|
19873
|
+
const card = li2.querySelector(".film-card");
|
|
19874
|
+
card.setAttribute("aria-expanded", String(on));
|
|
19875
|
+
if (on) card.setAttribute("aria-current", "step");
|
|
19876
|
+
else card.removeAttribute("aria-current");
|
|
19877
|
+
const body = li2.querySelector(".film-body");
|
|
19878
|
+
body.hidden = !on;
|
|
19879
|
+
body.innerHTML = on ? this.body(this.tour[j2], j2) : "";
|
|
19880
|
+
});
|
|
19881
|
+
const list2 = this.root.querySelector(".film-list");
|
|
19882
|
+
const li = items[k2];
|
|
19883
|
+
if (list2 && li && list2.scrollHeight > list2.clientHeight) {
|
|
19884
|
+
const top = li.offsetTop - list2.offsetTop, bottom = top + li.offsetHeight;
|
|
19885
|
+
if (top < list2.scrollTop) list2.scrollTop = top;
|
|
19886
|
+
else if (bottom > list2.scrollTop + list2.clientHeight) list2.scrollTop = bottom - list2.clientHeight;
|
|
19887
|
+
}
|
|
19888
|
+
}
|
|
19889
|
+
/** The card that follows the film, the bar filled as far as it played, the caption of now. */
|
|
19890
|
+
follow() {
|
|
19891
|
+
const v2 = this.video;
|
|
19892
|
+
if (!v2 || !this.data) return;
|
|
19893
|
+
const t2 = v2.currentTime;
|
|
19894
|
+
const cs = this.data.chapters;
|
|
19895
|
+
if (this.film === "ok") {
|
|
19896
|
+
let k2 = cs.findIndex((c3) => t2 < c3.end);
|
|
19897
|
+
if (k2 < 0) k2 = cs.length - 1;
|
|
19898
|
+
if (k2 !== this.at) this.open(k2);
|
|
19899
|
+
}
|
|
19900
|
+
this.root.querySelectorAll(".film-fill").forEach((f2, j2) => {
|
|
19901
|
+
const c3 = cs[j2];
|
|
19902
|
+
const at = this.film !== "ok" ? j2 < this.at ? 1 : 0 : t2 >= c3.end ? 1 : t2 <= c3.start ? 0 : (t2 - c3.start) / (c3.end - c3.start);
|
|
19903
|
+
f2.style.transform = `scaleX(${at.toFixed(4)})`;
|
|
19904
|
+
});
|
|
19905
|
+
const box = this.root.querySelector(".film-cc");
|
|
19906
|
+
if (box) {
|
|
19907
|
+
const still = this.root.querySelector(".film-still");
|
|
19908
|
+
const cue = this.cc && this.film === "ok" && (!still || still.hidden) ? cueAt(this.data.captions, t2) : null;
|
|
19909
|
+
if (cue) {
|
|
19910
|
+
if (box.textContent !== cue.text) box.textContent = cue.text;
|
|
19911
|
+
box.hidden = false;
|
|
19912
|
+
} else box.hidden = true;
|
|
19913
|
+
}
|
|
19914
|
+
}
|
|
19915
|
+
/** The play button as the film is, and the bar and captions following it while it plays. */
|
|
19916
|
+
sync() {
|
|
19917
|
+
const v2 = this.video;
|
|
19918
|
+
if (!v2) return;
|
|
19919
|
+
const b2 = this.root.querySelector("[data-film-play]");
|
|
19920
|
+
const end = v2.ended;
|
|
19921
|
+
const playing = this.film === "ok" ? !v2.paused : this.wantPlay && !this.opts.still;
|
|
19922
|
+
b2.textContent = end ? "\u21BB" : playing ? "\u275A\u275A" : "\u25B6";
|
|
19923
|
+
b2.toggleAttribute("disabled", this.film === "absent");
|
|
19924
|
+
const label = this.film === "absent" ? "The film is not on this server" : end ? "Play again from the start" : playing ? "Pause" : "Play";
|
|
19925
|
+
b2.setAttribute("aria-label", label);
|
|
19926
|
+
b2.title = label;
|
|
19927
|
+
cancelAnimationFrame(this.frame);
|
|
19928
|
+
const tick = () => {
|
|
19929
|
+
this.follow();
|
|
19930
|
+
if (this.video && !this.video.paused) this.frame = requestAnimationFrame(tick);
|
|
19931
|
+
};
|
|
19932
|
+
tick();
|
|
19933
|
+
}
|
|
19934
|
+
stop() {
|
|
19935
|
+
cancelAnimationFrame(this.frame);
|
|
19936
|
+
this.waiter?.stop();
|
|
19937
|
+
this.video?.pause();
|
|
19938
|
+
if (this.size) this.resize(0);
|
|
19939
|
+
document.removeEventListener("fullscreenchange", this.onFullscreen);
|
|
19940
|
+
}
|
|
19941
|
+
};
|
|
19942
|
+
|
|
19943
|
+
// src/web/src/intip.ts
|
|
19944
|
+
var RULES = {
|
|
19945
|
+
"selection-replace": { at: "#lightup [data-mode=plain]" },
|
|
19946
|
+
"tcb-first": { at: "#rests-steps [data-mode=tcb]", phone: "#panel .p-tcb" },
|
|
19947
|
+
"rests-only-tcb": { at: "#rests-steps" },
|
|
19948
|
+
"key-in-target": { at: "#panel [data-rv-key]" },
|
|
19949
|
+
"stale-opinion": { at: "#panel [data-rv-changes]" },
|
|
19950
|
+
"share-twice": { at: "#share-pop [data-tab=readonly]" },
|
|
19951
|
+
"first-export": { at: "#linear" },
|
|
19952
|
+
"first-time-machine": { at: "#tmbar" }
|
|
19953
|
+
};
|
|
19954
|
+
function inPlaceOf(rule) {
|
|
19955
|
+
const t2 = TIPS.find((x2) => x2.when === rule);
|
|
19956
|
+
if (t2) return { key: t2.id, title: t2.title, text: t2.text, showMe: t2.showMe, tip: t2 };
|
|
19957
|
+
const c3 = TOUR.find((x2) => x2.when === rule);
|
|
19958
|
+
return c3 ? { key: `tour-${c3.id}`, title: c3.title, text: c3.text, showMe: c3.showMe, chapter: c3.id } : null;
|
|
19959
|
+
}
|
|
19960
|
+
var STAYS_MS = 15e3;
|
|
19961
|
+
var SETTLE_MS = 600;
|
|
19962
|
+
var InTips = class {
|
|
19963
|
+
el;
|
|
19964
|
+
/** the rule shown in this page's session, if one was */
|
|
19965
|
+
used = null;
|
|
19966
|
+
/** no tip in place in this session: the tour ran */
|
|
19967
|
+
quiet = false;
|
|
19968
|
+
anchor = null;
|
|
19969
|
+
follow = null;
|
|
19970
|
+
stay = null;
|
|
19971
|
+
state;
|
|
19972
|
+
hooks;
|
|
19973
|
+
constructor(state, hooks) {
|
|
19974
|
+
this.state = state;
|
|
19975
|
+
this.hooks = hooks;
|
|
19976
|
+
this.el = document.getElementById("intip");
|
|
19977
|
+
this.el.addEventListener("click", (e2) => {
|
|
19978
|
+
const t2 = e2.target;
|
|
19979
|
+
const shown = this.entry();
|
|
19980
|
+
if (!shown) return;
|
|
19981
|
+
if (t2.closest("[data-intip-ok]")) this.end(true);
|
|
19982
|
+
else if (t2.closest("[data-intip-show]")) {
|
|
19983
|
+
this.end(true);
|
|
19984
|
+
this.hooks.showMe(shown);
|
|
19985
|
+
} else if (t2.closest("[data-intip-watch]") && shown.chapter) {
|
|
19986
|
+
this.end(true);
|
|
19987
|
+
this.hooks.watch(shown.chapter);
|
|
19988
|
+
}
|
|
19989
|
+
});
|
|
19990
|
+
window.addEventListener("keydown", (e2) => {
|
|
19991
|
+
if (e2.key === "Escape" && !this.el.hidden) this.end(false);
|
|
19992
|
+
});
|
|
19993
|
+
document.addEventListener("pointerdown", (e2) => {
|
|
19994
|
+
if (!this.el.hidden && !this.el.contains(e2.target)) this.end(false);
|
|
19995
|
+
});
|
|
19996
|
+
window.addEventListener("resize", () => this.place());
|
|
19997
|
+
}
|
|
19998
|
+
/** The tour ran in this session: nothing more is shown in place in it. */
|
|
19999
|
+
hush() {
|
|
20000
|
+
this.quiet = true;
|
|
20001
|
+
this.end(false);
|
|
20002
|
+
}
|
|
20003
|
+
entry() {
|
|
20004
|
+
return this.used ? inPlaceOf(this.used) : null;
|
|
20005
|
+
}
|
|
20006
|
+
/** Whether a rule's tip would show now, were its moment to come: what the page asks before it
|
|
20007
|
+
* counts anything for it. */
|
|
20008
|
+
open(rule) {
|
|
20009
|
+
if (this.used !== null || this.quiet || this.state.off || this.hooks.blocked()) return false;
|
|
20010
|
+
const e2 = inPlaceOf(rule);
|
|
20011
|
+
return !!e2 && !!RULES[rule] && !this.state.isDone(e2.key) && (!e2.tip || this.hooks.fits(e2.tip));
|
|
20012
|
+
}
|
|
20013
|
+
/** One more of what a rule counts, kept across visits (the picks under one step): how many now;
|
|
20014
|
+
* nothing is counted once the rule's tip is done. */
|
|
20015
|
+
count(name, rule) {
|
|
20016
|
+
const e2 = inPlaceOf(rule);
|
|
20017
|
+
return !e2 || this.state.isDone(e2.key) ? 0 : this.state.bump(name);
|
|
20018
|
+
}
|
|
20019
|
+
/** The page says a rule's moment came: its tip shows once what triggered it has settled, unless
|
|
20020
|
+
* one did in this session, the reader had it already, turned tips off, or the page is busy. */
|
|
20021
|
+
offer(rule) {
|
|
20022
|
+
if (!this.open(rule)) return;
|
|
20023
|
+
this.used = rule;
|
|
20024
|
+
setTimeout(() => {
|
|
20025
|
+
const tip = this.entry();
|
|
20026
|
+
const r2 = RULES[rule];
|
|
20027
|
+
const sel = isPhone() && r2.phone || r2.at;
|
|
20028
|
+
const at = document.querySelector(sel);
|
|
20029
|
+
if (!tip || this.hooks.blocked() || !at || at.hidden || !at.getBoundingClientRect().width) {
|
|
20030
|
+
this.used = null;
|
|
20031
|
+
return;
|
|
20032
|
+
}
|
|
20033
|
+
this.anchor = sel;
|
|
20034
|
+
this.state.shown(tip.key);
|
|
20035
|
+
const watch = tip.chapter && this.hooks.canWatch() ? `<button class="rv-btn" data-intip-watch>Watch</button>` : "";
|
|
20036
|
+
this.el.innerHTML = `<div class="intip-title">${escapeHtml(tip.title)}</div><div class="intip-text">${tip.text}</div><div class="intip-acts">${watch}${tip.showMe ? `<button class="rv-btn" data-intip-show>Show me</button>` : ""}<button class="rv-btn primary" data-intip-ok>Got it</button></div>`;
|
|
20037
|
+
this.el.hidden = false;
|
|
20038
|
+
this.place();
|
|
20039
|
+
this.follow = setInterval(() => this.place(), 400);
|
|
20040
|
+
this.stay = setTimeout(() => this.end(false), STAYS_MS);
|
|
20041
|
+
}, SETTLE_MS);
|
|
20042
|
+
}
|
|
20043
|
+
/** The reader did what a rule's tip teaches: it is done, shown or not. */
|
|
20044
|
+
did(rule) {
|
|
20045
|
+
const tip = inPlaceOf(rule);
|
|
20046
|
+
if (!tip || this.state.isDone(tip.key)) return;
|
|
20047
|
+
this.state.done(tip.key);
|
|
20048
|
+
if (this.used === rule && !this.el.hidden) this.end(false);
|
|
20049
|
+
}
|
|
20050
|
+
end(done) {
|
|
20051
|
+
if (this.follow) clearInterval(this.follow);
|
|
20052
|
+
if (this.stay) clearTimeout(this.stay);
|
|
20053
|
+
this.follow = this.stay = null;
|
|
20054
|
+
const tip = this.entry();
|
|
20055
|
+
if (done && tip) this.state.done(tip.key);
|
|
20056
|
+
this.el.hidden = true;
|
|
20057
|
+
}
|
|
20058
|
+
/** Beside its control: below it where there is room, else above, within the screen. */
|
|
20059
|
+
place() {
|
|
20060
|
+
if (this.el.hidden || !this.anchor) return;
|
|
20061
|
+
const at = document.querySelector(this.anchor);
|
|
20062
|
+
if (!at || !at.getBoundingClientRect().width) return this.end(false);
|
|
20063
|
+
const r2 = at.getBoundingClientRect();
|
|
20064
|
+
const w2 = this.el.offsetWidth, h2 = this.el.offsetHeight, gap = 10;
|
|
20065
|
+
const below = innerHeight - r2.bottom >= h2 + gap + 8;
|
|
20066
|
+
const x2 = Math.max(8, Math.min(r2.left + r2.width / 2 - w2 / 2, innerWidth - w2 - 8));
|
|
20067
|
+
const y2 = below ? r2.bottom + gap : Math.max(8, r2.top - h2 - gap);
|
|
20068
|
+
this.el.style.transform = `translate(${Math.round(x2)}px, ${Math.round(y2)}px)`;
|
|
20069
|
+
this.el.classList.toggle("above", !below);
|
|
20070
|
+
this.el.style.setProperty("--arrow-x", `${Math.round(Math.max(14, Math.min(r2.left + r2.width / 2 - x2, w2 - 14)))}px`);
|
|
20071
|
+
}
|
|
20072
|
+
};
|
|
20073
|
+
|
|
20074
|
+
// src/web/src/popover.ts
|
|
20075
|
+
var Popover = class {
|
|
20076
|
+
btn;
|
|
20077
|
+
el;
|
|
20078
|
+
/** puts the panel where it goes, once it is shown (it has a size then) */
|
|
20079
|
+
place;
|
|
20080
|
+
/** what a press may land on without closing it: another window it opened (a question, the
|
|
20081
|
+
* name), as selectors */
|
|
20082
|
+
keep;
|
|
20083
|
+
/** told when it opens (to draw what it holds) or closes */
|
|
20084
|
+
changed;
|
|
20085
|
+
constructor(btn, el, opts = {}) {
|
|
20086
|
+
this.btn = btn;
|
|
20087
|
+
this.el = el;
|
|
20088
|
+
this.place = opts.place ?? null;
|
|
20089
|
+
this.keep = opts.keep ?? "";
|
|
20090
|
+
this.changed = opts.changed ?? (() => {
|
|
20091
|
+
});
|
|
20092
|
+
btn.addEventListener("click", () => this.toggle());
|
|
20093
|
+
document.addEventListener("pointerdown", (e2) => {
|
|
20094
|
+
const t2 = e2.target;
|
|
20095
|
+
if (this.el.hidden || !t2 || this.el.contains(t2) || this.btn.contains(t2) || this.keep && t2.closest?.(this.keep)) return;
|
|
20096
|
+
this.toggle(false);
|
|
20097
|
+
});
|
|
20098
|
+
window.addEventListener("resize", () => {
|
|
20099
|
+
if (!this.el.hidden) this.place?.();
|
|
20100
|
+
});
|
|
20101
|
+
}
|
|
20102
|
+
get open() {
|
|
20103
|
+
return !this.el.hidden;
|
|
20104
|
+
}
|
|
20105
|
+
toggle(open = this.el.hidden) {
|
|
20106
|
+
if (open === !this.el.hidden) return;
|
|
20107
|
+
this.el.hidden = !open;
|
|
20108
|
+
this.btn.classList.toggle("on", open);
|
|
20109
|
+
this.btn.setAttribute("aria-expanded", String(open));
|
|
20110
|
+
this.changed(open);
|
|
20111
|
+
if (open) this.place?.();
|
|
20112
|
+
}
|
|
20113
|
+
};
|
|
20114
|
+
function besideSide(btn, el) {
|
|
20115
|
+
const side2 = document.querySelector("#side")?.getBoundingClientRect();
|
|
20116
|
+
const at = btn.getBoundingClientRect();
|
|
20117
|
+
const left = side2 && side2.width ? side2.right + 8 : at.left;
|
|
20118
|
+
const top = Math.max(66, Math.min(at.top - 6, innerHeight - el.offsetHeight - 12, innerHeight - 240));
|
|
20119
|
+
el.style.left = `${Math.round(Math.max(12, Math.min(left, innerWidth - el.offsetWidth - 12)))}px`;
|
|
20120
|
+
el.style.top = `${Math.round(top)}px`;
|
|
20121
|
+
el.style.maxHeight = `${Math.round(innerHeight - top - 12)}px`;
|
|
20122
|
+
}
|
|
20123
|
+
|
|
18995
20124
|
// src/web/src/guide.ts
|
|
18996
20125
|
var $2 = (sel) => document.querySelector(sel);
|
|
20126
|
+
function welcomeOf(o2) {
|
|
20127
|
+
if (o2.guided || o2.welcomed) return "none";
|
|
20128
|
+
return o2.deepLink || o2.following || o2.snapshot ? "dot" : "card";
|
|
20129
|
+
}
|
|
20130
|
+
function filmChapterOf(id) {
|
|
20131
|
+
return id && TOUR.some((c3) => c3.id === id) ? id : null;
|
|
20132
|
+
}
|
|
20133
|
+
var WELCOME_IDLE_MS = 2e3;
|
|
20134
|
+
var GUIDE_SHOWS = ["tcb-three", "help-about"];
|
|
18997
20135
|
var REPO = "https://github.com/LucianoXu/VisualProof";
|
|
20136
|
+
var MANUAL = `${REPO}/blob/main/docs/MANUAL.md`;
|
|
18998
20137
|
function around(...sels) {
|
|
18999
20138
|
let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;
|
|
19000
20139
|
for (const s2 of sels) {
|
|
@@ -19009,6 +20148,15 @@ function around(...sels) {
|
|
|
19009
20148
|
}
|
|
19010
20149
|
return isFinite(x0) ? new DOMRect(x0, y0, x1 - x0, y1 - y0) : null;
|
|
19011
20150
|
}
|
|
20151
|
+
function board() {
|
|
20152
|
+
const side2 = $2("#side")?.getBoundingClientRect();
|
|
20153
|
+
const panel = $2("#panel.open")?.getBoundingClientRect();
|
|
20154
|
+
const left = side2 && side2.width && !isPhone() ? side2.right + 8 : 12;
|
|
20155
|
+
const right = panel && panel.width && !isPhone() ? panel.left - 8 : innerWidth - 12;
|
|
20156
|
+
const top = isPhone() ? 104 : 108;
|
|
20157
|
+
const bottom = isPhone() && panel && panel.height ? panel.top - 8 : innerHeight - 48;
|
|
20158
|
+
return new DOMRect(left, top, Math.max(40, right - left), Math.max(40, bottom - top));
|
|
20159
|
+
}
|
|
19012
20160
|
var TOUCH = [
|
|
19013
20161
|
[
|
|
19014
20162
|
"The board",
|
|
@@ -19096,7 +20244,7 @@ var KEYS = [
|
|
|
19096
20244
|
{ k: ["c"], what: "colour by kind or by status, for you" },
|
|
19097
20245
|
{ k: ["b"], what: "build status" },
|
|
19098
20246
|
{ k: ["p"], what: "frame meter" },
|
|
19099
|
-
{ k: ["?"], what: "this sheet" }
|
|
20247
|
+
{ k: ["?"], what: "this sheet: the tips, and the keys" }
|
|
19100
20248
|
]
|
|
19101
20249
|
],
|
|
19102
20250
|
[
|
|
@@ -19124,11 +20272,43 @@ var Guide = class {
|
|
|
19124
20272
|
hooks;
|
|
19125
20273
|
steps = [];
|
|
19126
20274
|
k = 0;
|
|
20275
|
+
/** the view of a step with views */
|
|
20276
|
+
v = 0;
|
|
20277
|
+
/** the tour as a whole, or one step for a tip's Show me (GuideSpec decision 7) */
|
|
20278
|
+
kind = "tour";
|
|
20279
|
+
/** where the reader was when the tour began: theirs again when it ends (GuideSpec decision 3) */
|
|
20280
|
+
placeBefore = null;
|
|
19127
20281
|
el;
|
|
19128
20282
|
spot;
|
|
19129
20283
|
shades;
|
|
19130
20284
|
card;
|
|
19131
20285
|
help;
|
|
20286
|
+
news;
|
|
20287
|
+
/** the help menu, under ? (GuideSpec decision 8) */
|
|
20288
|
+
menu;
|
|
20289
|
+
menuPop;
|
|
20290
|
+
/** the welcome card of a first visit (decision 1) */
|
|
20291
|
+
welcomeEl;
|
|
20292
|
+
welcomeOff = null;
|
|
20293
|
+
tips = new TipState();
|
|
20294
|
+
inPlace;
|
|
20295
|
+
/** the tour film in the sheet, while it shows */
|
|
20296
|
+
film = null;
|
|
20297
|
+
/** what the recording says of the film; null: none here (a snapshot), the cards their words;
|
|
20298
|
+
* undefined: not known yet */
|
|
20299
|
+
tourData = void 0;
|
|
20300
|
+
/** the reference tip open, one at a time */
|
|
20301
|
+
openRef = null;
|
|
20302
|
+
/** the chapter the film starts at in this showing of the sheet (an ⓘ, Watch); null: the first */
|
|
20303
|
+
startAt = null;
|
|
20304
|
+
tab = "tips";
|
|
20305
|
+
filter = "";
|
|
20306
|
+
clipsHad = null;
|
|
20307
|
+
/** the release tips newer than which are New while the sheet shows (decision 11); undefined until
|
|
20308
|
+
* Tips is looked at in this showing of the sheet */
|
|
20309
|
+
newShown = void 0;
|
|
20310
|
+
/** the release the server runs, as its event stream said (UpdateSpec); null until it says */
|
|
20311
|
+
running = null;
|
|
19132
20312
|
constructor(store, hooks) {
|
|
19133
20313
|
this.store = store;
|
|
19134
20314
|
this.hooks = hooks;
|
|
@@ -19137,11 +20317,76 @@ var Guide = class {
|
|
|
19137
20317
|
this.shades = [...this.el.querySelectorAll(".tour-shade")];
|
|
19138
20318
|
this.card = this.el.querySelector(".tour-card");
|
|
19139
20319
|
this.help = $2("#help");
|
|
20320
|
+
this.news = $2("#whatsnew");
|
|
20321
|
+
this.welcomeEl = $2("#welcome");
|
|
20322
|
+
this.menu = $2("#help-menu");
|
|
20323
|
+
this.menuPop = new Popover($2("#help-btn"), this.menu, {
|
|
20324
|
+
changed: (open) => {
|
|
20325
|
+
if (!open) return;
|
|
20326
|
+
this.renderMenu();
|
|
20327
|
+
this.setDot(false);
|
|
20328
|
+
this.menu.querySelector("[role=menuitem]")?.focus();
|
|
20329
|
+
}
|
|
20330
|
+
});
|
|
20331
|
+
this.menu.addEventListener("click", (e2) => {
|
|
20332
|
+
const t2 = e2.target.closest("[data-hm]");
|
|
20333
|
+
if (!t2) return;
|
|
20334
|
+
const what = t2.dataset.hm;
|
|
20335
|
+
if (what !== "repo" && what !== "notes") e2.preventDefault();
|
|
20336
|
+
this.closeMenu(false);
|
|
20337
|
+
if (what === "tips") this.openHelp("tips");
|
|
20338
|
+
else if (what === "keys") this.openHelp("keys");
|
|
20339
|
+
else if (what === "tour") this.start();
|
|
20340
|
+
});
|
|
20341
|
+
this.menu.addEventListener("keydown", (e2) => {
|
|
20342
|
+
const items = [...this.menu.querySelectorAll("[role=menuitem]")];
|
|
20343
|
+
const k2 = items.indexOf(document.activeElement);
|
|
20344
|
+
const to = (j2) => items[(j2 + items.length) % items.length]?.focus();
|
|
20345
|
+
if (e2.key === "ArrowDown") to(k2 + 1);
|
|
20346
|
+
else if (e2.key === "ArrowUp") to(k2 < 0 ? -1 : k2 - 1);
|
|
20347
|
+
else if (e2.key === "Home") to(0);
|
|
20348
|
+
else if (e2.key === "End") to(-1);
|
|
20349
|
+
else if (e2.key === "Escape") this.closeMenu(true);
|
|
20350
|
+
else if (e2.key === "Tab") return this.closeMenu(false);
|
|
20351
|
+
else return;
|
|
20352
|
+
e2.preventDefault();
|
|
20353
|
+
e2.stopPropagation();
|
|
20354
|
+
});
|
|
20355
|
+
this.welcomeEl.addEventListener("click", (e2) => {
|
|
20356
|
+
const t2 = e2.target.closest("[data-wel]");
|
|
20357
|
+
if (!t2) return;
|
|
20358
|
+
const what = t2.dataset.wel;
|
|
20359
|
+
if (what === "later") return this.foldWelcome();
|
|
20360
|
+
this.hideWelcome();
|
|
20361
|
+
if (what === "tour") this.start();
|
|
20362
|
+
else this.openHelp("tips");
|
|
20363
|
+
});
|
|
20364
|
+
this.renderDot();
|
|
20365
|
+
this.inPlace = new InTips(this.tips, {
|
|
20366
|
+
blocked: () => !this.el.hidden || !this.help.hidden || this.hooks.snapshot() || this.hooks.following() || this.hooks.busy(),
|
|
20367
|
+
fits: (t2) => tipsFor(this.context(/* @__PURE__ */ new Set())).includes(t2),
|
|
20368
|
+
showMe: (e2) => this.showTip(e2),
|
|
20369
|
+
canWatch: () => !!this.tourData,
|
|
20370
|
+
watch: (chapter) => this.openHelp("tips", { chapter })
|
|
20371
|
+
});
|
|
20372
|
+
if (!this.hooks.snapshot())
|
|
20373
|
+
void Promise.all([this.hooks.clips(), this.hooks.tour()]).then(([clips, raw]) => {
|
|
20374
|
+
this.clipsHad = clips;
|
|
20375
|
+
this.tourData = tourData(raw, TOUR);
|
|
20376
|
+
if (!this.help.hidden && this.tab === "tips") this.renderFilm();
|
|
20377
|
+
});
|
|
20378
|
+
else this.tourData = null;
|
|
19140
20379
|
this.card.addEventListener("click", (e2) => {
|
|
19141
20380
|
const t2 = e2.target;
|
|
19142
|
-
|
|
19143
|
-
|
|
20381
|
+
const seg = t2.closest("[data-tour-view]");
|
|
20382
|
+
if (seg) this.view(Number(seg.dataset.tourView));
|
|
20383
|
+
else if (t2.closest("[data-tour-next]")) this.next();
|
|
20384
|
+
else if (t2.closest("[data-tour-back]")) this.back();
|
|
19144
20385
|
else if (t2.closest("[data-tour-skip]")) this.stop();
|
|
20386
|
+
else if (t2.closest("[data-tour-tips]")) {
|
|
20387
|
+
this.stop();
|
|
20388
|
+
this.openHelp("tips");
|
|
20389
|
+
}
|
|
19145
20390
|
});
|
|
19146
20391
|
window.addEventListener(
|
|
19147
20392
|
"keydown",
|
|
@@ -19149,8 +20394,8 @@ var Guide = class {
|
|
|
19149
20394
|
if (this.el.hidden) return;
|
|
19150
20395
|
e2.stopImmediatePropagation();
|
|
19151
20396
|
if (e2.key === "Escape") this.stop();
|
|
19152
|
-
else if (e2.key === "ArrowRight" || e2.key === "Enter") this.
|
|
19153
|
-
else if (e2.key === "ArrowLeft") this.
|
|
20397
|
+
else if (e2.key === "ArrowRight" || e2.key === "Enter") this.next();
|
|
20398
|
+
else if (e2.key === "ArrowLeft") this.back();
|
|
19154
20399
|
e2.preventDefault();
|
|
19155
20400
|
},
|
|
19156
20401
|
true
|
|
@@ -19160,13 +20405,32 @@ var Guide = class {
|
|
|
19160
20405
|
});
|
|
19161
20406
|
this.help.addEventListener("click", (e2) => {
|
|
19162
20407
|
const t2 = e2.target;
|
|
19163
|
-
|
|
19164
|
-
|
|
20408
|
+
const tab = t2.closest("[data-help-tab]");
|
|
20409
|
+
const show = t2.closest("[data-tip-show]");
|
|
20410
|
+
const tourShow = t2.closest("[data-tour-show]");
|
|
20411
|
+
const row = t2.closest("[data-ref-toggle]");
|
|
20412
|
+
if (row) this.toggleRef(row.dataset.refToggle);
|
|
20413
|
+
else if (tourShow) {
|
|
20414
|
+
const c3 = TOUR.find((x2) => x2.id === tourShow.dataset.tourShow);
|
|
20415
|
+
this.closeHelp();
|
|
20416
|
+
if (c3) this.showTip({ key: `tour-${c3.id}`, title: c3.title, text: c3.text, showMe: c3.showMe });
|
|
20417
|
+
} else if (t2 === this.help || t2.closest("[data-help-close]")) this.closeHelp();
|
|
20418
|
+
else if (tab) this.showTab(tab.dataset.helpTab === "keys" ? "keys" : "tips");
|
|
20419
|
+
else if (show) {
|
|
20420
|
+
const tip = TIPS.find((x2) => x2.id === show.dataset.tipShow);
|
|
20421
|
+
this.closeHelp();
|
|
20422
|
+
if (tip) this.showTip({ key: tip.id, title: tip.title, text: tip.text, showMe: tip.showMe });
|
|
20423
|
+
} else if (t2.closest("[data-help-tour]")) {
|
|
19165
20424
|
this.closeHelp();
|
|
19166
20425
|
this.start();
|
|
19167
20426
|
}
|
|
19168
20427
|
});
|
|
19169
20428
|
this.help.addEventListener("keydown", (e2) => {
|
|
20429
|
+
if (e2.key === "Escape" && this.film?.back()) {
|
|
20430
|
+
e2.preventDefault();
|
|
20431
|
+
e2.stopPropagation();
|
|
20432
|
+
return;
|
|
20433
|
+
}
|
|
19170
20434
|
if (e2.key === "Escape" || e2.key === "?" && !typing(e2.target)) {
|
|
19171
20435
|
e2.stopPropagation();
|
|
19172
20436
|
const field = this.help.querySelector("[data-help-editor]");
|
|
@@ -19175,7 +20439,12 @@ var Guide = class {
|
|
|
19175
20439
|
}
|
|
19176
20440
|
});
|
|
19177
20441
|
this.help.addEventListener("change", (e2) => {
|
|
19178
|
-
const
|
|
20442
|
+
const el = e2.target;
|
|
20443
|
+
if (el.closest("[data-tips-off]")) {
|
|
20444
|
+
this.tips.off = !el.checked;
|
|
20445
|
+
return;
|
|
20446
|
+
}
|
|
20447
|
+
const field = el.closest("[data-help-editor]");
|
|
19179
20448
|
if (!field) return;
|
|
19180
20449
|
const why = this.hooks.setEditor(field.value);
|
|
19181
20450
|
const err = this.help.querySelector("[data-help-editor-err]");
|
|
@@ -19184,82 +20453,325 @@ var Guide = class {
|
|
|
19184
20453
|
if (!why) this.help.querySelector("[data-help-editor-now]").innerHTML = this.editorNow();
|
|
19185
20454
|
});
|
|
19186
20455
|
this.help.addEventListener("input", (e2) => {
|
|
19187
|
-
|
|
20456
|
+
const el = e2.target;
|
|
20457
|
+
if (el.closest("[data-help-editor]")) this.help.querySelector("[data-help-editor-err]").hidden = true;
|
|
20458
|
+
else if (el.closest("[data-tips-filter]")) {
|
|
20459
|
+
this.filter = el.value;
|
|
20460
|
+
this.renderRefs();
|
|
20461
|
+
}
|
|
19188
20462
|
});
|
|
20463
|
+
document.addEventListener(
|
|
20464
|
+
"click",
|
|
20465
|
+
(e2) => {
|
|
20466
|
+
const i2 = e2.target.closest("[data-tip-open]");
|
|
20467
|
+
if (!i2) return;
|
|
20468
|
+
e2.preventDefault();
|
|
20469
|
+
e2.stopPropagation();
|
|
20470
|
+
const id = i2.dataset.tipOpen;
|
|
20471
|
+
this.openHelp("tips", TOUR.some((c3) => c3.id === id) ? { chapter: id } : { tip: id });
|
|
20472
|
+
},
|
|
20473
|
+
true
|
|
20474
|
+
);
|
|
20475
|
+
this.news.addEventListener("click", (e2) => {
|
|
20476
|
+
const t2 = e2.target;
|
|
20477
|
+
this.news.hidden = true;
|
|
20478
|
+
if (!t2.closest("[data-news-close]")) this.openHelp("tips");
|
|
20479
|
+
});
|
|
20480
|
+
}
|
|
20481
|
+
/** A first visit (GuideSpec decision 1): the welcome card once the page has stayed still for a
|
|
20482
|
+
* moment, or only the dot on the help where the reader came to work (`deepLink`: an address
|
|
20483
|
+
* naming a place). Nothing here starts the tour. */
|
|
20484
|
+
welcome(deepLink) {
|
|
20485
|
+
const w2 = welcomeOf({ guided: !!this.store.data.guided, welcomed: !!this.store.data.welcomed, deepLink, following: this.hooks.following(), snapshot: this.hooks.snapshot() });
|
|
20486
|
+
if (w2 === "dot") return this.setDot(true);
|
|
20487
|
+
if (w2 === "none") return;
|
|
20488
|
+
let timer = 0;
|
|
20489
|
+
const busy = (e2) => {
|
|
20490
|
+
if (e2 instanceof KeyboardEvent && ["Shift", "Control", "Alt", "Meta"].includes(e2.key)) return;
|
|
20491
|
+
stop();
|
|
20492
|
+
this.setDot(true);
|
|
20493
|
+
};
|
|
20494
|
+
const stop = () => {
|
|
20495
|
+
clearTimeout(timer);
|
|
20496
|
+
window.removeEventListener("pointerdown", busy, true);
|
|
20497
|
+
window.removeEventListener("keydown", busy, true);
|
|
20498
|
+
};
|
|
20499
|
+
window.addEventListener("pointerdown", busy, true);
|
|
20500
|
+
window.addEventListener("keydown", busy, true);
|
|
20501
|
+
timer = window.setTimeout(() => {
|
|
20502
|
+
stop();
|
|
20503
|
+
if (!this.el.hidden || !this.help.hidden || this.hooks.following()) return this.setDot(true);
|
|
20504
|
+
this.showWelcome();
|
|
20505
|
+
}, WELCOME_IDLE_MS);
|
|
20506
|
+
}
|
|
20507
|
+
/** The welcome card: no backdrop, never the keyboard's; it folds into the dot when the reader
|
|
20508
|
+
* starts working, or is put off (Not now, Esc). Shown once per browser and project. */
|
|
20509
|
+
showWelcome() {
|
|
20510
|
+
this.store.data.welcomed = true;
|
|
20511
|
+
this.store.save();
|
|
20512
|
+
const project = escapeHtml(this.hooks.project());
|
|
20513
|
+
this.welcomeEl.innerHTML = `<div class="wel-title">New here?</div><div class="wel-text">A map of ${project}'s declarations, and what each theorem rests on.</div><div class="wel-acts"><button class="rv-btn primary" data-wel="tour" title="About a minute">Take the tour</button><button class="rv-btn" data-wel="film" title="Tips: the film of a whole review">Watch a review</button><span class="tour-gap"></span><button class="rv-btn" data-wel="later">Not now</button></div>`;
|
|
20514
|
+
const side2 = $2("#side")?.getBoundingClientRect();
|
|
20515
|
+
this.welcomeEl.style.left = isPhone() || !side2?.width ? "" : `${Math.round(side2.right + 16)}px`;
|
|
20516
|
+
this.welcomeEl.hidden = false;
|
|
20517
|
+
const away = (e2) => {
|
|
20518
|
+
if (this.welcomeEl.contains(e2.target)) return;
|
|
20519
|
+
if (e2 instanceof KeyboardEvent && ["Shift", "Control", "Alt", "Meta"].includes(e2.key)) return;
|
|
20520
|
+
this.foldWelcome();
|
|
20521
|
+
};
|
|
20522
|
+
window.addEventListener("pointerdown", away, true);
|
|
20523
|
+
window.addEventListener("keydown", away, true);
|
|
20524
|
+
this.welcomeOff = () => {
|
|
20525
|
+
window.removeEventListener("pointerdown", away, true);
|
|
20526
|
+
window.removeEventListener("keydown", away, true);
|
|
20527
|
+
this.welcomeOff = null;
|
|
20528
|
+
};
|
|
19189
20529
|
}
|
|
19190
|
-
|
|
19191
|
-
|
|
19192
|
-
|
|
19193
|
-
this.
|
|
20530
|
+
hideWelcome() {
|
|
20531
|
+
this.welcomeOff?.();
|
|
20532
|
+
this.welcomeEl.hidden = true;
|
|
20533
|
+
this.welcomeEl.classList.remove("folding");
|
|
20534
|
+
this.welcomeEl.style.transform = "";
|
|
20535
|
+
}
|
|
20536
|
+
/** The card folds into the dot on the help: not now, and not "never". */
|
|
20537
|
+
foldWelcome() {
|
|
20538
|
+
if (this.welcomeEl.hidden) return;
|
|
20539
|
+
this.welcomeOff?.();
|
|
20540
|
+
this.setDot(true);
|
|
20541
|
+
const to = (isPhone() ? $2("#more-btn") : $2("#help-btn"))?.getBoundingClientRect();
|
|
20542
|
+
if (reducedMotion() || !to?.width) return this.hideWelcome();
|
|
20543
|
+
const r2 = this.welcomeEl.getBoundingClientRect();
|
|
20544
|
+
this.welcomeEl.classList.add("folding");
|
|
20545
|
+
this.welcomeEl.style.transform = `translate(${Math.round(to.right - r2.right)}px, ${Math.round(to.top - r2.top)}px) scale(0.06)`;
|
|
20546
|
+
setTimeout(() => this.hideWelcome(), 380);
|
|
20547
|
+
}
|
|
20548
|
+
/** The dot on the help (and on ⋯, which holds it on a phone). */
|
|
20549
|
+
setDot(on) {
|
|
20550
|
+
this.tips.dot = on;
|
|
20551
|
+
this.renderDot();
|
|
20552
|
+
}
|
|
20553
|
+
renderDot() {
|
|
20554
|
+
const on = this.tips.dot;
|
|
20555
|
+
$2("#help-btn")?.classList.toggle("has-dot", on);
|
|
20556
|
+
$2("#more-btn")?.classList.toggle("has-dot", on);
|
|
20557
|
+
}
|
|
20558
|
+
// ---------------------------------------------------------------- the menu
|
|
20559
|
+
/** The help menu (GuideSpec decision 8): opened, or closed, by ? — the focus on its first item. */
|
|
20560
|
+
toggleMenu() {
|
|
20561
|
+
if (this.menuPop.open) this.closeMenu(true);
|
|
20562
|
+
else this.menuPop.toggle(true);
|
|
20563
|
+
}
|
|
20564
|
+
closeMenu(focusButton) {
|
|
20565
|
+
if (!this.menuPop.open) return;
|
|
20566
|
+
this.menuPop.toggle(false);
|
|
20567
|
+
if (focusButton && !isPhone()) $2("#help-btn")?.focus();
|
|
20568
|
+
}
|
|
20569
|
+
/** Tips, Keys, the tour; then what the project is and which VisualProof the server runs, said in
|
|
20570
|
+
* the menu itself. */
|
|
20571
|
+
renderMenu() {
|
|
20572
|
+
const keys = isPhone() ? "Gestures" : "Keys";
|
|
20573
|
+
this.menu.innerHTML = `<button class="hm-item" role="menuitem" data-hm="tips"><span class="hm-t">Tips</span><span class="hm-d">The film of a review, and a tip for the rest</span></button><button class="hm-item" role="menuitem" data-hm="keys"><span class="hm-t">${keys}</span></button><button class="hm-item" role="menuitem" data-hm="tour"><span class="hm-t">Take the tour</span></button><div class="hm-sep" role="separator"></div><div class="hm-item" role="menuitem" aria-disabled="true" tabindex="-1"><span class="hm-h">About ${escapeHtml(this.hooks.project())}</span><span class="hm-d">${escapeHtml(this.hooks.about())}</span></div><div data-hm-tool>${this.menuTool(null)}</div>`;
|
|
20574
|
+
void this.hooks.updates().then((u2) => {
|
|
20575
|
+
const el = this.menu.querySelector("[data-hm-tool]");
|
|
20576
|
+
if (u2 && el && this.menuPop.open) el.innerHTML = this.menuTool(u2);
|
|
20577
|
+
});
|
|
20578
|
+
}
|
|
20579
|
+
/** About VisualProof in the menu: which one runs — a press opens its repository —, and to those
|
|
20580
|
+
* let in fully a newer release, the server's word as text, its notes a link only at an https
|
|
20581
|
+
* address (GuideSpec decision 13). */
|
|
20582
|
+
menuTool(u2) {
|
|
20583
|
+
const running = u2?.running ?? this.running;
|
|
20584
|
+
let html = `<a class="hm-item" role="menuitem" data-hm="repo" href="${REPO}" target="_blank" rel="noopener noreferrer"><span class="hm-h">About VisualProof</span><span class="hm-d">${running ? `VisualProof ${escapeHtml(running)}${u2?.installed && u2.installed !== running ? ` (installed: ${escapeHtml(u2.installed)})` : ""}. ` : ""}Open source: ${REPO.replace("https://", "")}</span></a>`;
|
|
20585
|
+
const n3 = u2?.newer;
|
|
20586
|
+
if (n3 && u2?.mode !== "off") {
|
|
20587
|
+
html += `<div class="hm-item" role="menuitem" aria-disabled="true" tabindex="-1"><span class="hm-d">${escapeHtml(n3.version)} is out: ${n3.taken ? "this server takes it by itself, at a quiet moment." : n3.command ? `<code>${escapeHtml(n3.command)}</code> on the server takes it.` : "the server's owner takes it."}</span></div>`;
|
|
20588
|
+
if (/^https:\/\/[^\s"'<>]+$/.test(n3.notes)) html += `<a class="hm-item hm-link" role="menuitem" data-hm="notes" href="${escapeHtml(n3.notes)}" target="_blank" rel="noopener noreferrer">What is new in ${escapeHtml(n3.version)}</a>`;
|
|
20589
|
+
}
|
|
20590
|
+
return html;
|
|
19194
20591
|
}
|
|
19195
|
-
|
|
19196
|
-
|
|
20592
|
+
/** What the tips are shown against now. */
|
|
20593
|
+
context(clips) {
|
|
20594
|
+
const snapshot = this.hooks.snapshot();
|
|
20595
|
+
return {
|
|
20596
|
+
readOnly: this.hooks.readOnly(),
|
|
20597
|
+
phone: isPhone(),
|
|
20598
|
+
clips,
|
|
20599
|
+
newSince: this.newShown ?? null,
|
|
20600
|
+
manual: MANUAL,
|
|
20601
|
+
applies: (t2) => this.hooks.applies(t2),
|
|
20602
|
+
canShow: (a3) => !snapshot && !this.hooks.following() && this.hooks.canShow(a3)
|
|
20603
|
+
};
|
|
20604
|
+
}
|
|
20605
|
+
// ---------------------------------------------------------------- the tour
|
|
20606
|
+
/** The TCB step (GuideSpec decision 2): its theorem alone, its TCB, its key declarations, each
|
|
20607
|
+
* with the counts of what it lights, the camera framing what is lit. */
|
|
20608
|
+
restsOnStep(phone) {
|
|
20609
|
+
const i2 = this.hooks.example();
|
|
20610
|
+
if (i2 === null) return null;
|
|
20611
|
+
const ro = this.hooks.readOnly();
|
|
20612
|
+
let r2 = this.hooks.restsOn(i2);
|
|
20613
|
+
const ring = (step2) => () => (phone ? around("#panel") : around(step2)) ?? board();
|
|
20614
|
+
const lit = (step2) => () => {
|
|
20615
|
+
const b2 = board();
|
|
20616
|
+
const m3 = phone ? null : around(step2);
|
|
20617
|
+
if (!m3) return b2;
|
|
20618
|
+
const l2 = Math.min(b2.left, m3.left), t2 = Math.min(b2.top, m3.top);
|
|
20619
|
+
return new DOMRect(l2, t2, Math.max(b2.right, m3.right) - l2, Math.max(b2.bottom, m3.bottom) - t2);
|
|
20620
|
+
};
|
|
20621
|
+
const views = [
|
|
20622
|
+
{
|
|
20623
|
+
label: "Statement",
|
|
20624
|
+
before: () => {
|
|
20625
|
+
this.hooks.setTool("plain");
|
|
20626
|
+
this.hooks.select(i2);
|
|
20627
|
+
r2 = this.hooks.restsOn(i2);
|
|
20628
|
+
},
|
|
20629
|
+
text: () => `This is <b>${escapeHtml(r2.name)}</b>. Lean checked its proof; whether it says what you mean is in the words of its statement, and they are defined elsewhere.`,
|
|
20630
|
+
at: () => (phone ? around("#panel") : around("#panel .p-head", "#panel .p-name", "#panel .sig")) ?? board()
|
|
20631
|
+
},
|
|
20632
|
+
{
|
|
20633
|
+
label: "TCB",
|
|
20634
|
+
before: () => {
|
|
20635
|
+
this.hooks.setTool("tcb");
|
|
20636
|
+
this.hooks.frameLit();
|
|
20637
|
+
},
|
|
20638
|
+
// the numbers add up to the selection bar's: the theorem, what is to be read, what is assumed
|
|
20639
|
+
text: () => {
|
|
20640
|
+
const lit2 = r2.read + r2.assumed;
|
|
20641
|
+
const assumed = r2.assumed ? `${r2.assumed} trusted, as Mathlib is, and assumed` : "";
|
|
20642
|
+
const proofs = r2.proofs ? ` What its proofs alone use, ${r2.proofs} more, is not lit: the proofs are Lean's to check.` : "";
|
|
20643
|
+
if (!lit2) return `Its <b>TCB</b> is itself alone: its statement rests on nothing but Lean.${proofs}`;
|
|
20644
|
+
if (!r2.read) return `Its <b>TCB</b> lights ${lit2} besides it, all ${r2.assumed === 1 ? "of it " : ""}trusted, as Mathlib is, and assumed: nothing of the project's to read.${proofs}`;
|
|
20645
|
+
return `Its <b>TCB</b> lights ${lit2} besides it: ${r2.read} to read \u2014 definitions, axioms, the project's notation \u2014${assumed ? ` and ${assumed}` : ""}. To trust it, read ${r2.read === 1 ? "that one" : `those ${r2.read}`}.${proofs}`;
|
|
20646
|
+
},
|
|
20647
|
+
at: lit("#rests-steps [data-mode=tcb]")
|
|
20648
|
+
},
|
|
20649
|
+
{
|
|
20650
|
+
label: "Keys",
|
|
20651
|
+
// no key declaration: the TCB stays lit, and the card says how keys are marked
|
|
20652
|
+
before: () => {
|
|
20653
|
+
this.hooks.setTool(r2.keys ? "keys" : "tcb");
|
|
20654
|
+
this.hooks.frameLit();
|
|
20655
|
+
},
|
|
20656
|
+
text: () => r2.keys ? r2.keys === 1 ? `Of those ${r2.read + r2.assumed}, the one the review marked <b>key</b>: what carries its meaning. Read it first.` : `Of those ${r2.read + r2.assumed}, the ${r2.keys} the review marked <b>key</b>: the few that carry its meaning. Read them first.` : ro || this.hooks.snapshot() ? "None is marked <b>key</b> yet: the review marks the few of a target's TCB that carry its meaning." : phone ? "None is marked <b>key</b> yet. On a target's view, its panel's \u25C7 Mark as key marks the few that carry its meaning." : "None is marked <b>key</b> yet. On a view focused on a target, <kbd>k</kbd> marks the few of its TCB that carry its meaning.",
|
|
20657
|
+
at: () => r2.keys ? lit("#rests-steps [data-mode=keys]")() : ring("#rests-steps [data-mode=keys]")()
|
|
20658
|
+
}
|
|
20659
|
+
];
|
|
20660
|
+
return { title: "What it rests on", text: "", views };
|
|
19197
20661
|
}
|
|
19198
20662
|
start() {
|
|
20663
|
+
this.hideWelcome();
|
|
20664
|
+
this.setDot(false);
|
|
20665
|
+
this.kind = "tour";
|
|
20666
|
+
this.placeBefore = this.hooks.place();
|
|
20667
|
+
this.inPlace.hush();
|
|
19199
20668
|
const project = escapeHtml(this.hooks.project());
|
|
19200
|
-
const
|
|
20669
|
+
const snapshot = this.hooks.snapshot();
|
|
20670
|
+
const live = () => !snapshot;
|
|
20671
|
+
const ro = this.hooks.readOnly();
|
|
20672
|
+
const reader = ro && !snapshot;
|
|
20673
|
+
const phone = isPhone();
|
|
20674
|
+
const rests = this.restsOnStep(phone);
|
|
19201
20675
|
const all = [
|
|
19202
|
-
{
|
|
19203
|
-
title: "Welcome",
|
|
19204
|
-
text: `${project}'s declarations as a map, and what each theorem rests on. A quick tour.`
|
|
19205
|
-
},
|
|
20676
|
+
{ title: "Welcome", text: `${project}'s declarations as a map, and what each theorem rests on. A quick tour.` },
|
|
19206
20677
|
{
|
|
19207
20678
|
title: "The map",
|
|
19208
|
-
text: `Dots are declarations, circles namespaces. Scroll to zoom, drag to pan, double-click a circle to fold it. To move things, drag with <b>${dragKeyName()}</b> held, or let the lock go (<b>l</b>).`,
|
|
19209
|
-
at:
|
|
19210
|
-
const side2 = $2("#side")?.getBoundingClientRect();
|
|
19211
|
-
const left = side2 && side2.width ? side2.right + 8 : 12;
|
|
19212
|
-
return new DOMRect(left, 108, innerWidth - left - 12, innerHeight - 108 - 48);
|
|
19213
|
-
}
|
|
20679
|
+
text: phone ? "Dots are declarations, circles namespaces. Drag to move around, pinch to zoom, tap to select; touch and hold for what can be done with what is under your finger." : ro ? `Dots are declarations, circles namespaces. Scroll to zoom, drag to pan, double-click a circle to fold it${reader ? ", for you alone" : ""}.` : `Dots are declarations, circles namespaces. Scroll to zoom, drag to pan, double-click a circle to fold it. To move things, drag with <b>${dragKeyName()}</b> held, or let the lock go (<b>l</b>).`,
|
|
20680
|
+
at: board
|
|
19214
20681
|
},
|
|
19215
20682
|
{
|
|
19216
20683
|
title: "Select",
|
|
19217
|
-
text: "
|
|
19218
|
-
|
|
19219
|
-
const i2 = this.hooks.example();
|
|
19220
|
-
if (i2 !== null) this.hooks.select(i2);
|
|
19221
|
-
},
|
|
19222
|
-
at: () => around("#lightup")
|
|
20684
|
+
text: phone ? "Tap a declaration, or a circle for all of its declarations. How a tap selects is the mode, in <b>\u22EF</b> at the top: just it, what it rests on, or what uses it." : "Click a declaration, or a circle for all of its declarations. <b>Mode</b> is how a click selects: just it (<kbd>1</kbd>), what it <b>rests on</b> (<kbd>2</kbd>), or what uses it (<kbd>3</kbd>). Rest the pointer on a mode to read it.",
|
|
20685
|
+
at: () => around(phone ? "#more-btn" : "#lightup")
|
|
19223
20686
|
},
|
|
20687
|
+
...rests ? [rests] : [],
|
|
19224
20688
|
{
|
|
19225
20689
|
title: "The board",
|
|
19226
|
-
text: "The left panel: the targets under review, and this board's clusters. A board is shared with everyone on it: <b>\u2699 Board</b> holds what is on it (its scope, as chips), the clustering, the depth. Tabs are the views you have open; <b>Views</b> makes and lists them. Your camera and what you select are yours alone.",
|
|
19227
|
-
at: () => around("#side", "#tabs", ".views-wrap")
|
|
20690
|
+
text: phone ? "<b>\u2630</b> opens the targets under review and this board's namespaces or folders." : ro ? "The left panel: the targets under review, and this board's clusters; <b>\u2699 Board</b> shows what is on it. Tabs are the views you have open; <b>Views</b> lists them. Whatever you select, fold or open is yours alone." : "The left panel: the targets under review, and this board's clusters. A board is shared with everyone on it: <b>\u2699 Board</b> holds what is on it (its scope, as chips), the clustering, the depth. Tabs are the views you have open; <b>Views</b> makes and lists them. Your camera and what you select are yours alone.",
|
|
20691
|
+
at: () => phone ? around("#side-btn") : around("#side", "#tabs", ".views-wrap")
|
|
19228
20692
|
},
|
|
19229
20693
|
{
|
|
19230
20694
|
title: "The panel",
|
|
19231
|
-
text: "All about what you clicked last; <b>Source</b> (Space) opens the code, and the parts with a \u25B8 open on a click. Above it, what is selected: <b>Zoom in</b> opens a view of it.",
|
|
19232
|
-
at: () => around("#selbar", "#panel"),
|
|
20695
|
+
text: phone ? "The sheet is what you tapped last: a tap on its handle gives it the whole screen or half of it, \u2715 closes it. <b>Source</b> shows its code." : ro ? "All about what you clicked last; <b>Source</b> (<kbd>Space</kbd>) opens the code, and the parts with a \u25B8 open on a click." : "All about what you clicked last; <b>Source</b> (<kbd>Space</kbd>) opens the code, and the parts with a \u25B8 open on a click. Above it, what is selected: <b>Zoom in</b> opens a view of it.",
|
|
20696
|
+
at: () => phone ? around("#panel") : around("#selbar", "#panel"),
|
|
19233
20697
|
when: () => !!$2("#panel.open")
|
|
19234
20698
|
},
|
|
19235
20699
|
{
|
|
19236
20700
|
title: "Review",
|
|
19237
|
-
text: live
|
|
20701
|
+
text: snapshot ? "Opinions as they were when this snapshot was taken." : ro ? "The review as it stands, live: who approved, who commented, on which version. A read-only page records nothing." : phone ? "<b>Approve</b> or <b>Comment</b> on what you have read, under your name (\u22EF: your name)." : "<b>Approve</b> or <b>Comment</b> on what you have read. <b>Mark as target</b> a theorem to follow its review.",
|
|
19238
20702
|
at: () => around("#panel .rv"),
|
|
19239
20703
|
when: () => !!$2("#panel.open .rv")
|
|
19240
20704
|
},
|
|
19241
20705
|
{
|
|
19242
20706
|
title: "Together",
|
|
19243
|
-
text: "Who else is here: click one to follow. <b>Activity</b> lists what happened.",
|
|
19244
|
-
at: () => around("#activity-btn", "#peers", "#who"),
|
|
20707
|
+
text: phone ? `Who else is here: tap one's initials to follow${reader ? "; nobody sees you" : ""}. <b>\u22EF</b> holds Activity.` : reader ? "Who else is here: click one to follow; nobody sees you. <b>Activity</b> lists what happened." : "Who else is here: click one to follow. <b>Activity</b> lists what happened.",
|
|
20708
|
+
at: () => phone ? around("#peers", "#more-btn") : reader ? around("#activity-btn", "#peers") : around("#activity-btn", "#peers", "#who"),
|
|
19245
20709
|
only: live
|
|
19246
20710
|
},
|
|
19247
20711
|
{
|
|
19248
20712
|
title: "Colours",
|
|
19249
|
-
text: "By kind, or by <b>status</b>: what reviewers said, what is trusted, what failed to build. The legend at the bottom left switches them (or <
|
|
20713
|
+
text: phone ? "By kind, or by <b>status</b>: what reviewers said, what is trusted, what failed to build. <b>Kind \u25BE</b> at the bottom left switches them and says what each colour means." : "By kind, or by <b>status</b>: what reviewers said, what is trusted, what failed to build. The legend at the bottom left switches them (or <kbd>c</kbd>) and says what each colour means.",
|
|
19250
20714
|
at: () => around("#legend")
|
|
19251
20715
|
},
|
|
19252
20716
|
{
|
|
19253
20717
|
title: "Getting around",
|
|
19254
|
-
|
|
19255
|
-
|
|
20718
|
+
// the last step points at the tips (GuideSpec decision 1)
|
|
20719
|
+
text: phone ? "\u2039 goes back; <b>Search</b> finds names, statements and docstrings. There is more under <b>\u22EF</b> \u2192 Help \u2192 <b>Tips</b>: the film of a whole review, and a tip for the rest." : "Back, forward and links work as in a browser; <kbd>/</kbd> searches names, statements and docstrings. There is more under <b>?</b> \u2192 <b>Tips</b>: the film of a whole review, and a tip for the rest.",
|
|
20720
|
+
at: () => phone ? around(".brand .nav", ".search-wrap", "#more-btn") : around(".brand .nav", ".search-wrap", "#help-btn")
|
|
19256
20721
|
}
|
|
19257
20722
|
];
|
|
19258
20723
|
this.steps = all.filter((s2) => !s2.only || s2.only());
|
|
19259
20724
|
this.k = 0;
|
|
20725
|
+
this.v = 0;
|
|
19260
20726
|
this.el.hidden = false;
|
|
19261
20727
|
this.go(0);
|
|
19262
20728
|
}
|
|
20729
|
+
/** A tip's Show me (GuideSpec decision 7): its control ringed, its words beside it, after what
|
|
20730
|
+
* its action does on the reader's own page. */
|
|
20731
|
+
showTip(tip) {
|
|
20732
|
+
if (!tip.showMe || !this.hooks.canShow(tip.showMe)) return;
|
|
20733
|
+
if (tip.showMe === GUIDE_SHOWS[1]) {
|
|
20734
|
+
this.closeHelp();
|
|
20735
|
+
this.menuPop.toggle(true);
|
|
20736
|
+
this.menu.querySelector("[data-hm=repo]")?.focus();
|
|
20737
|
+
return;
|
|
20738
|
+
}
|
|
20739
|
+
let step2;
|
|
20740
|
+
if (tip.showMe === GUIDE_SHOWS[0]) {
|
|
20741
|
+
step2 = this.restsOnStep(isPhone());
|
|
20742
|
+
if (step2) step2.title = tip.title;
|
|
20743
|
+
} else {
|
|
20744
|
+
const ring = this.hooks.show(tip.showMe);
|
|
20745
|
+
step2 = ring === null ? null : { title: tip.title, text: tip.text, at: () => around(...ring.split(/\s*,\s*/)) };
|
|
20746
|
+
}
|
|
20747
|
+
if (!step2) return;
|
|
20748
|
+
this.tips.done(tip.key);
|
|
20749
|
+
this.kind = "show";
|
|
20750
|
+
this.placeBefore = null;
|
|
20751
|
+
this.steps = [step2];
|
|
20752
|
+
this.k = 0;
|
|
20753
|
+
this.v = 0;
|
|
20754
|
+
this.el.hidden = false;
|
|
20755
|
+
this.go(0);
|
|
20756
|
+
}
|
|
20757
|
+
next() {
|
|
20758
|
+
const s2 = this.steps[this.k];
|
|
20759
|
+
if (s2?.views && this.v < s2.views.length - 1) this.view(this.v + 1);
|
|
20760
|
+
else this.go(this.k + 1);
|
|
20761
|
+
}
|
|
20762
|
+
back() {
|
|
20763
|
+
const s2 = this.steps[this.k];
|
|
20764
|
+
if (s2?.views && this.v > 0) this.view(this.v - 1);
|
|
20765
|
+
else this.go(this.k - 1, -1);
|
|
20766
|
+
}
|
|
20767
|
+
/** A view of the step shown: what it does done, its words, its place. */
|
|
20768
|
+
view(v2) {
|
|
20769
|
+
const s2 = this.steps[this.k];
|
|
20770
|
+
if (!s2?.views?.[v2]) return;
|
|
20771
|
+
this.v = v2;
|
|
20772
|
+
s2.views[v2].before();
|
|
20773
|
+
this.render();
|
|
20774
|
+
}
|
|
19263
20775
|
go(k2, dir = k2 >= this.k ? 1 : -1) {
|
|
19264
20776
|
if (k2 < 0) return;
|
|
19265
20777
|
if (k2 >= this.steps.length) return this.stop();
|
|
@@ -19267,8 +20779,21 @@ var Guide = class {
|
|
|
19267
20779
|
s2.before?.();
|
|
19268
20780
|
if (s2.when && !s2.when()) return this.go(k2 + dir, dir);
|
|
19269
20781
|
this.k = k2;
|
|
19270
|
-
|
|
19271
|
-
|
|
20782
|
+
this.v = s2.views ? dir > 0 ? 0 : s2.views.length - 1 : 0;
|
|
20783
|
+
s2.views?.[this.v].before();
|
|
20784
|
+
this.render();
|
|
20785
|
+
}
|
|
20786
|
+
render() {
|
|
20787
|
+
const s2 = this.steps[this.k];
|
|
20788
|
+
const k2 = this.k;
|
|
20789
|
+
const show = this.kind === "show";
|
|
20790
|
+
const lastView = !s2.views || this.v === s2.views.length - 1;
|
|
20791
|
+
const last2 = k2 === this.steps.length - 1 && lastView;
|
|
20792
|
+
const text2 = s2.views ? s2.views[this.v].text() : s2.text;
|
|
20793
|
+
const seg = s2.views ? `<div class="seg mini tour-seg" role="group" aria-label="${escapeHtml(s2.title)}">${s2.views.map((v2, j2) => `<button data-tour-view="${j2}" class="${j2 === this.v ? "on" : ""}" aria-pressed="${j2 === this.v}">${v2.label}</button>`).join("")}</div>` : "";
|
|
20794
|
+
const next = k2 === 0 && this.v === 0 && !show ? "Show me" : last2 ? "Done" : "Next";
|
|
20795
|
+
this.card.innerHTML = (show ? "" : `<div class="tour-step">${k2 + 1} of ${this.steps.length}</div>`) + `<div class="tour-title">${s2.title}</div>${seg}<div class="tour-text">${text2}</div><div class="tour-actions">${show || last2 ? "" : `<button class="rv-btn" data-tour-skip>Skip</button>`}<span class="tour-gap"></span>` + (k2 > 0 || this.v > 0 ? `<button class="rv-btn" data-tour-back>Back</button>` : "") + // the last step points on to the tips: the film from its start (GuideSpec decision 1)
|
|
20796
|
+
(last2 && !show ? `<button class="rv-btn" data-tour-tips>Open Tips</button>` : "") + `<button class="rv-btn primary" data-tour-next>${next}</button></div>`;
|
|
19272
20797
|
this.place();
|
|
19273
20798
|
setTimeout(() => this.place(), 360);
|
|
19274
20799
|
this.card.querySelector("[data-tour-next]")?.focus();
|
|
@@ -19277,7 +20802,7 @@ var Guide = class {
|
|
|
19277
20802
|
place() {
|
|
19278
20803
|
const s2 = this.steps[this.k];
|
|
19279
20804
|
if (!s2) return;
|
|
19280
|
-
const r2 = s2.at?.() ?? null;
|
|
20805
|
+
const r2 = (s2.views ? s2.views[this.v].at : s2.at)?.() ?? null;
|
|
19281
20806
|
const pad = 6;
|
|
19282
20807
|
const card = this.card;
|
|
19283
20808
|
const w2 = card.offsetWidth, h2 = card.offsetHeight;
|
|
@@ -19301,7 +20826,7 @@ var Guide = class {
|
|
|
19301
20826
|
else if (room2.above >= h2 + gap + pad) [x2, y2] = [r2.left, r2.top - h2 - gap - pad];
|
|
19302
20827
|
else if (room2.left >= w2 + gap + pad) [x2, y2] = [r2.left - w2 - gap - pad, r2.top];
|
|
19303
20828
|
else if (room2.right >= w2 + gap + pad) [x2, y2] = [r2.right + gap + pad, r2.top];
|
|
19304
|
-
else [x2, y2] = [r2.left + 24, r2.
|
|
20829
|
+
else [x2, y2] = [r2.left + 24, r2.bottom - h2 - 24];
|
|
19305
20830
|
x2 = Math.max(12, Math.min(x2, innerWidth - w2 - 12));
|
|
19306
20831
|
y2 = Math.max(12, Math.min(y2, innerHeight - h2 - 12));
|
|
19307
20832
|
card.style.transform = `translate(${Math.round(x2)}px, ${Math.round(y2)}px)`;
|
|
@@ -19309,9 +20834,26 @@ var Guide = class {
|
|
|
19309
20834
|
stop() {
|
|
19310
20835
|
if (this.el.hidden) return;
|
|
19311
20836
|
this.el.hidden = true;
|
|
19312
|
-
this.
|
|
19313
|
-
|
|
19314
|
-
|
|
20837
|
+
if (this.kind === "tour") {
|
|
20838
|
+
this.store.data.guided = true;
|
|
20839
|
+
this.store.save();
|
|
20840
|
+
this.hooks.back(this.placeBefore);
|
|
20841
|
+
this.hooks.ended();
|
|
20842
|
+
this.setDot(true);
|
|
20843
|
+
} else this.hooks.record();
|
|
20844
|
+
this.placeBefore = null;
|
|
20845
|
+
}
|
|
20846
|
+
// ---------------------------------------------------------------- What's new
|
|
20847
|
+
/** The server's event stream said which release it runs (UpdateSpec): What's new is said once when
|
|
20848
|
+
* it is newer than the one this browser saw last (GuideSpec decision 11). */
|
|
20849
|
+
serverIs(version) {
|
|
20850
|
+
this.running = version;
|
|
20851
|
+
if (this.hooks.snapshot()) return;
|
|
20852
|
+
const was = this.tips.meet(version);
|
|
20853
|
+
if (was === null) return;
|
|
20854
|
+
this.news.innerHTML = `<button class="news-open">What's new in ${escapeHtml(version)}</button><button class="icon" data-news-close title="Close" aria-label="Close">\u2715</button>`;
|
|
20855
|
+
this.news.hidden = false;
|
|
20856
|
+
setTimeout(() => this.news.hidden = true, 15e3);
|
|
19315
20857
|
}
|
|
19316
20858
|
// ---------------------------------------------------------------- the sheet
|
|
19317
20859
|
/** What the editor link is now, in a sentence. */
|
|
@@ -19322,20 +20864,101 @@ var Guide = class {
|
|
|
19322
20864
|
if (server) return `The server's. Yours, if you set one here, is kept in this browser and goes first.`;
|
|
19323
20865
|
return `None is set, so there is no link. VS Code on the server's machine: ${code("vscode://file/{path}:{line}:{col}")}; over SSH: ${code("vscode://vscode-remote/ssh-remote+HOST{path}:{line}")}.`;
|
|
19324
20866
|
}
|
|
19325
|
-
|
|
20867
|
+
/** Which VisualProof the server runs, and what it says of a newer one (GuideSpec decision 13). */
|
|
20868
|
+
aboutTool(u2) {
|
|
20869
|
+
const running = u2?.running ?? this.running;
|
|
20870
|
+
const repo = `<a class="help-repo" href="${REPO}" target="_blank" rel="noopener noreferrer">${REPO.replace("https://", "")}</a>`;
|
|
20871
|
+
let html = running ? `VisualProof ${escapeHtml(running)}${u2?.installed && u2.installed !== running ? ` (installed: ${escapeHtml(u2.installed)})` : ""}. ` : "";
|
|
20872
|
+
html += `Open source: its documentation, its code and its issues are at ${repo}.`;
|
|
20873
|
+
const n3 = u2?.newer;
|
|
20874
|
+
if (n3 && u2?.mode !== "off") {
|
|
20875
|
+
const notes = /^https:\/\/[^\s"'<>]+$/.test(n3.notes) ? `<a href="${escapeHtml(n3.notes)}" target="_blank" rel="noopener noreferrer">what is new in it</a>` : "its notes on GitHub";
|
|
20876
|
+
html += `<div class="help-update">${escapeHtml(n3.version)} is out (${notes}): ${n3.taken ? "this server takes it by itself, at a quiet moment." : n3.command ? `<code>${escapeHtml(n3.command)}</code> on the server takes it.` : "the server's owner takes it."}</div>`;
|
|
20877
|
+
}
|
|
20878
|
+
return html;
|
|
20879
|
+
}
|
|
20880
|
+
/** The sheet, on a tab; on Tips, the film from its first chapter, or from `at.chapter`, or a
|
|
20881
|
+
* reference tip open (`at.tip`). */
|
|
20882
|
+
openHelp(tab = this.tab, at = {}) {
|
|
20883
|
+
const tipId = at.tip;
|
|
19326
20884
|
const { server, mine } = this.hooks.editor();
|
|
19327
20885
|
const phone = isPhone();
|
|
19328
|
-
const
|
|
19329
|
-
this.
|
|
20886
|
+
const keysTab = phone ? "Gestures" : "Keys";
|
|
20887
|
+
this.tab = tab;
|
|
20888
|
+
if (tipId) this.filter = "";
|
|
20889
|
+
this.help.innerHTML = `<div class="modal-box glass help-box${phone ? " help-touch" : ""}" role="dialog" aria-label="Help" tabindex="-1"><div class="help-head"><div class="seg help-tabs" role="tablist" aria-label="Help"><button role="tab" data-help-tab="tips">Tips</button><button role="tab" data-help-tab="keys">${keysTab}</button></div><button class="icon" data-help-close title="Close (Esc)">\u2715</button></div><div class="help-pane" data-help-pane="tips" role="tabpanel"><div class="film-h">A review in thirteen scenes</div><div data-film></div><div class="refs-head"><div class="help-h">More tips</div><input class="help-input tips-filter" data-tips-filter type="search" placeholder="Find a tip" value="${escapeHtml(this.filter)}" spellcheck="false" autocomplete="off" aria-label="Find a tip"></div><div class="tips-list" data-tips-list></div>` + (this.hooks.snapshot() ? "" : `<label class="help-note tips-off"><input type="checkbox" data-tips-off${this.tips.off ? "" : " checked"}> Show a tip beside a control when it helps, now and then</label>`) + `</div><div class="help-pane" data-help-pane="keys" role="tabpanel" hidden><div class="help-grid">${(phone ? TOUCH : KEYS).map(
|
|
19330
20890
|
([group, rows]) => `<div class="help-group"><div class="help-h">${group}</div>${rows.map(
|
|
19331
20891
|
(r2) => `<div class="help-row"><span class="help-k">${(r2.k ?? []).map((x2) => `<kbd>${escapeHtml(x2)}</kbd>`).join(" ")}${r2.k && r2.g ? " " : ""}${r2.g ? `<span class="help-g">${escapeHtml(r2.g)}</span>` : ""}</span><span>${escapeHtml(r2.what)}</span></div>`
|
|
19332
20892
|
).join("")}</div>`
|
|
19333
|
-
).join("")}</div>` + (phone ? "" : `<div class="help-editor"><div class="help-h">Open in editor</div><div class="help-note">A link from the panel and the source window to the declaration in your editor. <code>{path}</code> is the file on the server, <code>{rel}</code> the file in the project, <code>{line}</code> and <code>{col}</code> where it starts.</div><input class="help-input" data-help-editor value="${escapeHtml(mine ?? "")}" placeholder="${escapeHtml(server ?? "vscode://file/{path}:{line}:{col}")}" spellcheck="false" autocomplete="off" aria-label="Open in editor: URL template"><div class="modal-err" data-help-editor-err hidden></div><div class="help-note" data-help-editor-now>${this.editorNow()}</div></div>`) +
|
|
20893
|
+
).join("")}</div>` + (phone ? "" : `<div class="help-editor"><div class="help-h">Open in editor</div><div class="help-note">A link from the panel and the source window to the declaration in your editor. <code>{path}</code> is the file on the server, <code>{rel}</code> the file in the project, <code>{line}</code> and <code>{col}</code> where it starts.</div><input class="help-input" data-help-editor value="${escapeHtml(mine ?? "")}" placeholder="${escapeHtml(server ?? "vscode://file/{path}:{line}:{col}")}" spellcheck="false" autocomplete="off" aria-label="Open in editor: URL template"><div class="modal-err" data-help-editor-err hidden></div><div class="help-note" data-help-editor-now>${this.editorNow()}</div></div>`) + `</div><div class="modal-actions"><button class="rv-btn" data-help-tour>Take the tour again</button><button class="rv-btn primary" data-help-close>Close</button></div></div>`;
|
|
19334
20894
|
this.help.hidden = false;
|
|
19335
|
-
|
|
20895
|
+
if (tab === "tips") this.setDot(false);
|
|
20896
|
+
this.closeMenu(false);
|
|
20897
|
+
this.hideWelcome();
|
|
20898
|
+
const box = this.help.querySelector(".help-box");
|
|
20899
|
+
if (tipId) this.openRef = tipId;
|
|
20900
|
+
this.startAt = filmChapterOf(at.chapter);
|
|
20901
|
+
this.showTab(tab);
|
|
20902
|
+
box.focus();
|
|
20903
|
+
if (tipId) {
|
|
20904
|
+
const row = this.help.querySelector(`[data-tip="${CSS.escape(tipId)}"]`);
|
|
20905
|
+
if (row) {
|
|
20906
|
+
row.scrollIntoView({ block: "center" });
|
|
20907
|
+
row.classList.add("flash");
|
|
20908
|
+
}
|
|
20909
|
+
}
|
|
20910
|
+
}
|
|
20911
|
+
showTab(tab) {
|
|
20912
|
+
this.tab = tab;
|
|
20913
|
+
for (const b2 of this.help.querySelectorAll("[data-help-tab]")) {
|
|
20914
|
+
const on = b2.dataset.helpTab === tab;
|
|
20915
|
+
b2.classList.toggle("on", on);
|
|
20916
|
+
b2.setAttribute("aria-selected", String(on));
|
|
20917
|
+
}
|
|
20918
|
+
for (const p2 of this.help.querySelectorAll("[data-help-pane]")) p2.hidden = p2.dataset.helpPane !== tab;
|
|
20919
|
+
if (tab === "tips" && this.newShown === void 0) {
|
|
20920
|
+
this.newShown = this.tips.newSince;
|
|
20921
|
+
this.tips.newLooked();
|
|
20922
|
+
}
|
|
20923
|
+
if (tab === "tips") {
|
|
20924
|
+
this.renderFilm();
|
|
20925
|
+
this.renderRefs();
|
|
20926
|
+
} else this.film?.stop();
|
|
20927
|
+
this.hooks.shown(tab === "tips", tab === "tips" ? this.startAt : null);
|
|
20928
|
+
}
|
|
20929
|
+
/** The film and its cards (GuideSpec decision 6): from the chapter asked for, else the first;
|
|
20930
|
+
* without the film, the cards are their words. Shown again when the film is found. */
|
|
20931
|
+
renderFilm() {
|
|
20932
|
+
const el = this.help.querySelector("[data-film]");
|
|
20933
|
+
if (!el) return;
|
|
20934
|
+
this.film?.stop();
|
|
20935
|
+
const ctx = this.context(this.clipsHad ?? /* @__PURE__ */ new Set());
|
|
20936
|
+
this.film = new TourFilm(el, TOUR, this.tourData ?? null, {
|
|
20937
|
+
still: reducedMotion(),
|
|
20938
|
+
acts: (c3) => actsHtml(c3.showMe, c3.manual, ctx, `data-tour-show="${c3.id}"`),
|
|
20939
|
+
sheet: this.help.querySelector(".help-box")
|
|
20940
|
+
});
|
|
20941
|
+
this.film.start(this.startAt ?? void 0);
|
|
20942
|
+
}
|
|
20943
|
+
/** The reference tips as the filter has them, one open; New marked until Tips has been looked at. */
|
|
20944
|
+
renderRefs() {
|
|
20945
|
+
const list2 = this.help.querySelector("[data-tips-list]");
|
|
20946
|
+
if (!list2) return;
|
|
20947
|
+
list2.innerHTML = refsHtml(this.context(this.clipsHad ?? /* @__PURE__ */ new Set()), this.filter, this.openRef);
|
|
20948
|
+
}
|
|
20949
|
+
/** A reference tip opened, the one open before closed; or closed. */
|
|
20950
|
+
toggleRef(id) {
|
|
20951
|
+
this.openRef = this.openRef === id ? null : id;
|
|
20952
|
+
this.renderRefs();
|
|
20953
|
+
this.help.querySelector(`[data-ref-toggle="${CSS.escape(id)}"]`)?.focus();
|
|
19336
20954
|
}
|
|
19337
20955
|
closeHelp() {
|
|
19338
20956
|
if (this.help.contains(document.activeElement)) document.activeElement.blur();
|
|
20957
|
+
if (!this.help.hidden) this.hooks.shown(false, null);
|
|
20958
|
+
this.film?.stop();
|
|
20959
|
+
this.film = null;
|
|
20960
|
+
this.startAt = null;
|
|
20961
|
+
this.newShown = void 0;
|
|
19339
20962
|
this.help.hidden = true;
|
|
19340
20963
|
}
|
|
19341
20964
|
toggleHelp() {
|
|
@@ -19385,11 +21008,11 @@ var Legend = class {
|
|
|
19385
21008
|
};
|
|
19386
21009
|
|
|
19387
21010
|
// src/web/src/theme.ts
|
|
19388
|
-
var
|
|
21011
|
+
var KEY3 = "visualproof:theme";
|
|
19389
21012
|
var dark = matchMedia("(prefers-color-scheme: dark)");
|
|
19390
21013
|
function themeChoice() {
|
|
19391
21014
|
try {
|
|
19392
|
-
const v2 = localStorage.getItem(
|
|
21015
|
+
const v2 = localStorage.getItem(KEY3);
|
|
19393
21016
|
if (v2 === "dark" || v2 === "system") return v2;
|
|
19394
21017
|
} catch {
|
|
19395
21018
|
}
|
|
@@ -19401,8 +21024,8 @@ function applyTheme(choice = themeChoice()) {
|
|
|
19401
21024
|
}
|
|
19402
21025
|
function setTheme(choice) {
|
|
19403
21026
|
try {
|
|
19404
|
-
if (choice === "light") localStorage.removeItem(
|
|
19405
|
-
else localStorage.setItem(
|
|
21027
|
+
if (choice === "light") localStorage.removeItem(KEY3);
|
|
21028
|
+
else localStorage.setItem(KEY3, choice);
|
|
19406
21029
|
} catch {
|
|
19407
21030
|
}
|
|
19408
21031
|
applyTheme(choice);
|
|
@@ -19430,56 +21053,6 @@ function themeChoices(el) {
|
|
|
19430
21053
|
});
|
|
19431
21054
|
}
|
|
19432
21055
|
|
|
19433
|
-
// src/web/src/popover.ts
|
|
19434
|
-
var Popover = class {
|
|
19435
|
-
btn;
|
|
19436
|
-
el;
|
|
19437
|
-
/** puts the panel where it goes, once it is shown (it has a size then) */
|
|
19438
|
-
place;
|
|
19439
|
-
/** what a press may land on without closing it: another window it opened (a question, the
|
|
19440
|
-
* name), as selectors */
|
|
19441
|
-
keep;
|
|
19442
|
-
/** told when it opens (to draw what it holds) or closes */
|
|
19443
|
-
changed;
|
|
19444
|
-
constructor(btn, el, opts = {}) {
|
|
19445
|
-
this.btn = btn;
|
|
19446
|
-
this.el = el;
|
|
19447
|
-
this.place = opts.place ?? null;
|
|
19448
|
-
this.keep = opts.keep ?? "";
|
|
19449
|
-
this.changed = opts.changed ?? (() => {
|
|
19450
|
-
});
|
|
19451
|
-
btn.addEventListener("click", () => this.toggle());
|
|
19452
|
-
document.addEventListener("pointerdown", (e2) => {
|
|
19453
|
-
const t2 = e2.target;
|
|
19454
|
-
if (this.el.hidden || !t2 || this.el.contains(t2) || this.btn.contains(t2) || this.keep && t2.closest?.(this.keep)) return;
|
|
19455
|
-
this.toggle(false);
|
|
19456
|
-
});
|
|
19457
|
-
window.addEventListener("resize", () => {
|
|
19458
|
-
if (!this.el.hidden) this.place?.();
|
|
19459
|
-
});
|
|
19460
|
-
}
|
|
19461
|
-
get open() {
|
|
19462
|
-
return !this.el.hidden;
|
|
19463
|
-
}
|
|
19464
|
-
toggle(open = this.el.hidden) {
|
|
19465
|
-
if (open === !this.el.hidden) return;
|
|
19466
|
-
this.el.hidden = !open;
|
|
19467
|
-
this.btn.classList.toggle("on", open);
|
|
19468
|
-
this.btn.setAttribute("aria-expanded", String(open));
|
|
19469
|
-
this.changed(open);
|
|
19470
|
-
if (open) this.place?.();
|
|
19471
|
-
}
|
|
19472
|
-
};
|
|
19473
|
-
function besideSide(btn, el) {
|
|
19474
|
-
const side2 = document.querySelector("#side")?.getBoundingClientRect();
|
|
19475
|
-
const at = btn.getBoundingClientRect();
|
|
19476
|
-
const left = side2 && side2.width ? side2.right + 8 : at.left;
|
|
19477
|
-
const top = Math.max(66, Math.min(at.top - 6, innerHeight - el.offsetHeight - 12, innerHeight - 240));
|
|
19478
|
-
el.style.left = `${Math.round(Math.max(12, Math.min(left, innerWidth - el.offsetWidth - 12)))}px`;
|
|
19479
|
-
el.style.top = `${Math.round(top)}px`;
|
|
19480
|
-
el.style.maxHeight = `${Math.round(innerHeight - top - 12)}px`;
|
|
19481
|
-
}
|
|
19482
|
-
|
|
19483
21056
|
// src/web/src/reviewqueue.ts
|
|
19484
21057
|
function chainName(g2, i2) {
|
|
19485
21058
|
return g2.nodes[i2].name.split(".").slice(-2).join(".");
|
|
@@ -20021,7 +21594,7 @@ var SelectionBar = class {
|
|
|
20021
21594
|
const words = pickWords(p2);
|
|
20022
21595
|
return `<span class="sb-chip t-${p2.tool}${k2 === panel ? " on" : ""}" title="${esc2(printQuery(p2.q))}"><button class="sb-go" data-show="${k2}"${p2.root ? ` title="Show ${esc2(words)}"` : " disabled"}>${esc2(words)}</button><button class="sb-x" data-rm="${k2}" title="Take it out" aria-label="Take ${esc2(words)} out">\xD7</button></span>`;
|
|
20023
21596
|
}).join("");
|
|
20024
|
-
this.el.innerHTML = `<span class="sb-chips">${chips}</span><span class="sb-meta"><span class="sb-n" title="Declarations lit on this board">${lit.toLocaleString()} lit</span
|
|
21597
|
+
this.el.innerHTML = `<span class="sb-chips">${chips}</span><span class="sb-meta"><span class="sb-n" title="Declarations lit on this board">${lit.toLocaleString()} lit</span>` + (this.hooks.canOpenViews() ? `<button class="sb-btn" data-zoom${zoom ? ` title="Open a view of what is selected, laid out on its own, in a tab"` : ` disabled title="${why}"`}>Zoom in</button>` : "") + (key ? `<button class="sb-btn sb-key" data-mark-keys${key.fresh ? ` title="${esc2(`Mark the ${key.fresh} declaration${key.fresh === 1 ? "" : "s"} you clicked as key for ${key.target}`)}"` : ` disabled title="${esc2(key.already ? `What you clicked is key for ${key.target} already` : `Nothing you clicked is in ${key.target}'s review besides the target`)}"`}>\u25C6 Mark selected as key${key.fresh > 1 ? ` (${key.fresh})` : ""}</button>` : "") + `<button class="sb-btn" data-clear title="Select nothing (Esc)">Clear</button></span>`;
|
|
20025
21598
|
}
|
|
20026
21599
|
};
|
|
20027
21600
|
|
|
@@ -20878,9 +22451,9 @@ var SourceWindow = class {
|
|
|
20878
22451
|
// a pinned window the page placed keeps under the page's bars as the page is resized
|
|
20879
22452
|
top: () => this.hooks.top()
|
|
20880
22453
|
});
|
|
20881
|
-
const
|
|
20882
|
-
el.addEventListener("pointerdown",
|
|
20883
|
-
el.addEventListener("mousedown",
|
|
22454
|
+
const keep2 = (e2) => e2.target.closest?.("[data-sw-quote]") && e2.preventDefault();
|
|
22455
|
+
el.addEventListener("pointerdown", keep2);
|
|
22456
|
+
el.addEventListener("mousedown", keep2);
|
|
20884
22457
|
document.addEventListener("selectionchange", this.onSelection);
|
|
20885
22458
|
el.addEventListener("click", (e2) => {
|
|
20886
22459
|
const t2 = e2.target;
|
|
@@ -21375,7 +22948,7 @@ var SourceWindow = class {
|
|
|
21375
22948
|
this.still = !!past;
|
|
21376
22949
|
this.era = then ? "then" : "now";
|
|
21377
22950
|
const scroll = this.el.querySelector(".sw-scroll");
|
|
21378
|
-
const
|
|
22951
|
+
const keep2 = !fresh3 && scroll ? scroll.scrollTop : null;
|
|
21379
22952
|
const typed = document.activeElement;
|
|
21380
22953
|
const hadFocus = (typed instanceof HTMLTextAreaElement || typed instanceof HTMLInputElement) && this.el.contains(typed) && typed.matches("[data-rv-comment], [data-rv-for]") ? { sel: typed.matches("[data-rv-for]") ? "[data-rv-for]" : "[data-rv-comment]", from: typed.selectionStart, to: typed.selectionEnd } : null;
|
|
21381
22954
|
const short6 = escapeHtml(v2.display ?? parts[parts.length - 1]);
|
|
@@ -21398,7 +22971,7 @@ var SourceWindow = class {
|
|
|
21398
22971
|
if (fresh3) this.awaitCode(turn, () => frame2(escapeHtml(v2.module), "", `<div class="sw-none">Reading the source\u2026</div>`, void 0, false));
|
|
21399
22972
|
const settle2 = () => {
|
|
21400
22973
|
const scrolled = this.el.querySelector(".sw-scroll");
|
|
21401
|
-
if (scrolled &&
|
|
22974
|
+
if (scrolled && keep2 !== null) scrolled.scrollTop = keep2;
|
|
21402
22975
|
if (hadFocus) {
|
|
21403
22976
|
const box = this.el.querySelector(hadFocus.sel);
|
|
21404
22977
|
box?.focus();
|
|
@@ -21504,9 +23077,9 @@ var SourceWindow = class {
|
|
|
21504
23077
|
const long = ctx.length > LONG;
|
|
21505
23078
|
let hidden = 0;
|
|
21506
23079
|
const rows = ctx.map((l2, k2) => {
|
|
21507
|
-
const
|
|
21508
|
-
if (!
|
|
21509
|
-
const cls = `sw-ctx-l${used.has(k2) ? " used" : ""}${
|
|
23080
|
+
const keep2 = !long || k2 < FIRST || l2.kind === "namespace" || l2.kind === "section" || l2.kind === "mutual" || used.has(k2) || / in$/.test(l2.text);
|
|
23081
|
+
if (!keep2) hidden++;
|
|
23082
|
+
const cls = `sw-ctx-l${used.has(k2) ? " used" : ""}${keep2 ? "" : " fold"}`;
|
|
21510
23083
|
const tip = !used.has(k2) ? "" : exact ? ` title="Lean included this variable in the declaration"` : ` title="A name it binds occurs in this declaration: Lean may include it"`;
|
|
21511
23084
|
return `<div class="${cls}"${tip}>${highlightLines(l2.text, (w2) => resolve(w2))}</div>`;
|
|
21512
23085
|
});
|
|
@@ -21624,6 +23197,132 @@ function offsetIn(root, node, off) {
|
|
|
21624
23197
|
return r2.toString().length;
|
|
21625
23198
|
}
|
|
21626
23199
|
|
|
23200
|
+
// src/web/src/reload.ts
|
|
23201
|
+
var PAGE_VERSION = true ? "0.4.1" : null;
|
|
23202
|
+
var KEY4 = "visualproof:reload";
|
|
23203
|
+
var FOR_KEY2 = "visualproof:reloaded-for";
|
|
23204
|
+
var FRESH_MS = 10 * 6e4;
|
|
23205
|
+
var session = () => {
|
|
23206
|
+
try {
|
|
23207
|
+
return globalThis.sessionStorage ?? null;
|
|
23208
|
+
} catch {
|
|
23209
|
+
return null;
|
|
23210
|
+
}
|
|
23211
|
+
};
|
|
23212
|
+
function keep(k2, storage = session()) {
|
|
23213
|
+
try {
|
|
23214
|
+
storage?.setItem(KEY4, JSON.stringify(k2));
|
|
23215
|
+
} catch {
|
|
23216
|
+
}
|
|
23217
|
+
}
|
|
23218
|
+
var pairs = (x2, ok) => Array.isArray(x2) ? x2.filter((p2) => Array.isArray(p2) && p2.length === 2 && typeof p2[0] === "string" && ok(p2[1])) : [];
|
|
23219
|
+
var isString = (v2) => typeof v2 === "string";
|
|
23220
|
+
var isBool = (v2) => typeof v2 === "boolean";
|
|
23221
|
+
var win = (v2) => v2 && typeof v2 === "object" && typeof v2.name === "string" ? { name: v2.name, collapsed: v2.collapsed === true } : null;
|
|
23222
|
+
function takeKept(root, now = Date.now(), storage = session()) {
|
|
23223
|
+
let raw = null;
|
|
23224
|
+
try {
|
|
23225
|
+
raw = storage?.getItem(KEY4) ?? null;
|
|
23226
|
+
storage?.removeItem(KEY4);
|
|
23227
|
+
} catch {
|
|
23228
|
+
return null;
|
|
23229
|
+
}
|
|
23230
|
+
if (!raw) return null;
|
|
23231
|
+
try {
|
|
23232
|
+
const k2 = JSON.parse(raw);
|
|
23233
|
+
if (k2.root !== root || typeof k2.at !== "number" || now - k2.at > FRESH_MS || now < k2.at) return null;
|
|
23234
|
+
const pinned = Array.isArray(k2.sources?.pinned) ? k2.sources.pinned.map(win).filter((w2) => w2 !== null) : [];
|
|
23235
|
+
return { root, at: k2.at, sources: { following: win(k2.sources?.following), pinned }, review: pairs(k2.review, isString), talk: pairs(k2.talk, isString), notes: pairs(k2.notes, isBool) };
|
|
23236
|
+
} catch {
|
|
23237
|
+
return null;
|
|
23238
|
+
}
|
|
23239
|
+
}
|
|
23240
|
+
var RETRY_MS = 1e3;
|
|
23241
|
+
var SPREAD_MS = 5e3;
|
|
23242
|
+
var NAME_PREFIX = "visualproof-reloaded-for:";
|
|
23243
|
+
var Reloader = class {
|
|
23244
|
+
/** the version the server runs, where it is another than this page's; null: none waits */
|
|
23245
|
+
wanted = null;
|
|
23246
|
+
hooks;
|
|
23247
|
+
timer = null;
|
|
23248
|
+
page;
|
|
23249
|
+
spread;
|
|
23250
|
+
constructor(hooks, page = PAGE_VERSION, spread = SPREAD_MS) {
|
|
23251
|
+
this.hooks = hooks;
|
|
23252
|
+
this.page = page;
|
|
23253
|
+
this.spread = spread;
|
|
23254
|
+
}
|
|
23255
|
+
/** The server said which it is, as a stream opened: of another version, the page reloads into
|
|
23256
|
+
* it as soon as nothing is cut short; of this page's (a step back meanwhile), nothing waits. */
|
|
23257
|
+
hello(server) {
|
|
23258
|
+
if (!server || !this.page) return;
|
|
23259
|
+
if (server.version === this.page) {
|
|
23260
|
+
if (this.wanted !== null) {
|
|
23261
|
+
this.wanted = null;
|
|
23262
|
+
this.stopWaiting();
|
|
23263
|
+
this.hooks.offer(null);
|
|
23264
|
+
}
|
|
23265
|
+
return;
|
|
23266
|
+
}
|
|
23267
|
+
const fresh3 = this.wanted !== server.version;
|
|
23268
|
+
this.wanted = server.version;
|
|
23269
|
+
this.stopWaiting();
|
|
23270
|
+
if (fresh3 && this.spread > 0) this.timer = setTimeout(() => this.attempt(), Math.random() * this.spread);
|
|
23271
|
+
else this.attempt();
|
|
23272
|
+
}
|
|
23273
|
+
/** Reload now, keeping what this page holds: the reader asked (the pill), or nothing waits. */
|
|
23274
|
+
go() {
|
|
23275
|
+
this.stopWaiting();
|
|
23276
|
+
const storage = this.storage();
|
|
23277
|
+
keep(this.hooks.kept(), storage);
|
|
23278
|
+
if (this.wanted !== null) this.reloadedFor(this.wanted);
|
|
23279
|
+
this.hooks.reload();
|
|
23280
|
+
}
|
|
23281
|
+
storage() {
|
|
23282
|
+
return this.hooks.storage === void 0 ? session() : this.hooks.storage;
|
|
23283
|
+
}
|
|
23284
|
+
/** Keep the version this tab reloads for: in its sessionStorage, else, where that is refused, in
|
|
23285
|
+
* its name (window.name), which a reload keeps too — so that a page that cannot keep it never
|
|
23286
|
+
* reloads, and reloads, for ever. */
|
|
23287
|
+
reloadedFor(v2) {
|
|
23288
|
+
try {
|
|
23289
|
+
const st2 = this.storage();
|
|
23290
|
+
if (!st2) throw new Error("no storage");
|
|
23291
|
+
st2.setItem(FOR_KEY2, v2);
|
|
23292
|
+
return;
|
|
23293
|
+
} catch {
|
|
23294
|
+
}
|
|
23295
|
+
try {
|
|
23296
|
+
globalThis.name = `${NAME_PREFIX}${v2}`;
|
|
23297
|
+
} catch {
|
|
23298
|
+
}
|
|
23299
|
+
}
|
|
23300
|
+
lastReloadedFor() {
|
|
23301
|
+
try {
|
|
23302
|
+
const st2 = this.storage();
|
|
23303
|
+
if (!st2) throw new Error("no storage");
|
|
23304
|
+
const v2 = st2.getItem(FOR_KEY2);
|
|
23305
|
+
if (v2 !== null) return v2;
|
|
23306
|
+
} catch {
|
|
23307
|
+
}
|
|
23308
|
+
const name = globalThis.name;
|
|
23309
|
+
return typeof name === "string" && name.startsWith(NAME_PREFIX) ? name.slice(NAME_PREFIX.length) : null;
|
|
23310
|
+
}
|
|
23311
|
+
attempt() {
|
|
23312
|
+
if (this.wanted === null) return;
|
|
23313
|
+
const done = this.lastReloadedFor();
|
|
23314
|
+
if (done === this.wanted) return;
|
|
23315
|
+
if (this.hooks.busy() === null) return this.go();
|
|
23316
|
+
this.hooks.offer(this.wanted);
|
|
23317
|
+
this.stopWaiting();
|
|
23318
|
+
this.timer = setTimeout(() => this.attempt(), RETRY_MS);
|
|
23319
|
+
}
|
|
23320
|
+
stopWaiting() {
|
|
23321
|
+
if (this.timer !== null) clearTimeout(this.timer);
|
|
23322
|
+
this.timer = null;
|
|
23323
|
+
}
|
|
23324
|
+
};
|
|
23325
|
+
|
|
21627
23326
|
// src/web/src/paperdock.ts
|
|
21628
23327
|
function pairingStep(o2) {
|
|
21629
23328
|
if (!o2.found?.links.length) return { none: "unlinked", said: o2.open };
|
|
@@ -43235,8 +44934,8 @@ var PdfView = class extends View {
|
|
|
43235
44934
|
dragText(e2, pageEl) {
|
|
43236
44935
|
this.clearDrag();
|
|
43237
44936
|
this.hooks.picked(null, null);
|
|
43238
|
-
const
|
|
43239
|
-
if (
|
|
44937
|
+
const win2 = this.scroller.closest(".pw-win");
|
|
44938
|
+
if (win2 && document.activeElement !== win2) win2.focus({ preventScroll: true });
|
|
43240
44939
|
const n3 = Number(pageEl.dataset.n);
|
|
43241
44940
|
const r0 = pageEl.getBoundingClientRect();
|
|
43242
44941
|
const fx = (e2.clientX - r0.left) / r0.width, fy = (e2.clientY - r0.top) / r0.height;
|
|
@@ -44381,9 +46080,9 @@ var Papers = class {
|
|
|
44381
46080
|
* pairing puts it (web/src/paperdock.ts, `pairKeeps`): so that the pairing never shows the two
|
|
44382
46081
|
* overlapping (the owner's decision of 2026-10-11), but where the page is too narrow for both. */
|
|
44383
46082
|
keepApart(w2) {
|
|
44384
|
-
const
|
|
44385
|
-
if (this.win.byHand && !
|
|
44386
|
-
if (!w2.byHand ||
|
|
46083
|
+
const keep2 = pairKeeps(w2.byHand ? w2.rect() : null, this.win.byHand ? this.win.rect() : null, pairArea(viewport2(), pairTop()));
|
|
46084
|
+
if (this.win.byHand && !keep2.paper) this.win.unmove();
|
|
46085
|
+
if (!w2.byHand || keep2.source) return;
|
|
44387
46086
|
if (w2.pinned) this.placePinned(w2);
|
|
44388
46087
|
else w2.pairAt(null);
|
|
44389
46088
|
}
|
|
@@ -46094,9 +47793,9 @@ var LinearForm = class {
|
|
|
46094
47793
|
setText(text2) {
|
|
46095
47794
|
const c3 = this.ctx;
|
|
46096
47795
|
c3.font = `12px ${FONT2}`;
|
|
46097
|
-
const
|
|
47796
|
+
const plain2 = text2.replace(/\*\*|`/g, "").replace(/(^|\s)\*(\S[^*]*)\*/g, "$1$2");
|
|
46098
47797
|
const out = [];
|
|
46099
|
-
for (const para of
|
|
47798
|
+
for (const para of plain2.split("\n")) {
|
|
46100
47799
|
let line = "";
|
|
46101
47800
|
for (const word of para.split(/\s+/).filter(Boolean)) {
|
|
46102
47801
|
const next = line ? `${line} ${word}` : word;
|
|
@@ -46199,10 +47898,10 @@ var LinearForm = class {
|
|
|
46199
47898
|
this.t = this.target > this.t ? Math.min(this.target, this.t + step2) : Math.max(this.target, this.t - step2);
|
|
46200
47899
|
this.request();
|
|
46201
47900
|
}
|
|
46202
|
-
const
|
|
47901
|
+
const board2 = 1 - ease2(this.t);
|
|
46203
47902
|
for (const id of ["canvas", "labels", "cards", "stickies"]) {
|
|
46204
47903
|
const e2 = document.getElementById(id);
|
|
46205
|
-
if (e2) e2.style.opacity =
|
|
47904
|
+
if (e2) e2.style.opacity = board2 >= 0.999 ? "" : String(board2);
|
|
46206
47905
|
}
|
|
46207
47906
|
if (this.t === 0 && this.target === 0) {
|
|
46208
47907
|
this.el.hidden = true;
|
|
@@ -46397,7 +48096,7 @@ var LinearForm = class {
|
|
|
46397
48096
|
for (const r2 of this.rows) if (r2.kind === "decl" && r2.k >= 0) rowOf.set(r2.k, r2);
|
|
46398
48097
|
for (const [i2, r2] of this.under) rowOf.set(i2, r2);
|
|
46399
48098
|
const sites = SITE_STMT | SITE_BODY;
|
|
46400
|
-
const
|
|
48099
|
+
const pairs2 = /* @__PURE__ */ new Map();
|
|
46401
48100
|
for (const [u2, ru] of rowOf) {
|
|
46402
48101
|
for (let e2 = g2.outStart[u2]; e2 < g2.outStart[u2 + 1]; e2++) {
|
|
46403
48102
|
const t2 = g2.outDst[e2];
|
|
@@ -46406,13 +48105,13 @@ var LinearForm = class {
|
|
|
46406
48105
|
if (!rt2 || rt2 === ru) continue;
|
|
46407
48106
|
const lit = this.hooks.edgeLit(u2, t2, g2.outSite[e2]);
|
|
46408
48107
|
const key = `${ru.y0}:${rt2.y0}`;
|
|
46409
|
-
const was =
|
|
48108
|
+
const was = pairs2.get(key);
|
|
46410
48109
|
if (was) was.lit ||= lit;
|
|
46411
|
-
else
|
|
48110
|
+
else pairs2.set(key, { a: ru, b: rt2, lit });
|
|
46412
48111
|
}
|
|
46413
48112
|
}
|
|
46414
48113
|
const links = this.hooks.keyLinks();
|
|
46415
|
-
const out = [...
|
|
48114
|
+
const out = [...pairs2.values()];
|
|
46416
48115
|
if (links) {
|
|
46417
48116
|
const seen = /* @__PURE__ */ new Set();
|
|
46418
48117
|
for (let j2 = 0; j2 + 1 < links.length; j2 += 2) {
|
|
@@ -46427,12 +48126,12 @@ var LinearForm = class {
|
|
|
46427
48126
|
/** The arcs on the left of the line, orange at the end that uses and blue at the end used, as on
|
|
46428
48127
|
* the board; the lit drawn bolder, a key link dashed. Past ARCS_MOST, only the lit. */
|
|
46429
48128
|
arcs(c3, pos, alpha, css) {
|
|
46430
|
-
const
|
|
46431
|
-
const few =
|
|
48129
|
+
const pairs2 = this.arcPairs();
|
|
48130
|
+
const few = pairs2.length <= ARCS_MOST;
|
|
46432
48131
|
const { h: h2 } = this.canvas.screen;
|
|
46433
48132
|
const from = css("--edge-from") || "rgba(240,110,20,0.85)";
|
|
46434
48133
|
const to = css("--edge-to") || "rgba(59,91,219,0.85)";
|
|
46435
|
-
for (const { a: a3, b: b2, lit, key } of
|
|
48134
|
+
for (const { a: a3, b: b2, lit, key } of pairs2) {
|
|
46436
48135
|
if (!few && !lit) continue;
|
|
46437
48136
|
const [xa, ya] = pos(a3), [xb, yb] = pos(b2);
|
|
46438
48137
|
if (ya < -20 && yb < -20 || ya > h2 + 20 && yb > h2 + 20) continue;
|
|
@@ -47101,8 +48800,8 @@ var FOOT_PRE = "Generated by ";
|
|
|
47101
48800
|
var footFonts = (px2, font) => [`500 ${px2}px ${font}`, `700 ${px2 * 1.1}px ${font}`];
|
|
47102
48801
|
var FOOT_GAP = 1.6;
|
|
47103
48802
|
function footMost(measure2, w2, h2, source, font) {
|
|
47104
|
-
const [
|
|
47105
|
-
const across2 = (measure2(FOOT_PRE,
|
|
48803
|
+
const [plain2, mark] = footFonts(100, font);
|
|
48804
|
+
const across2 = (measure2(FOOT_PRE, plain2) + measure2("VisualProof", mark) + (source ? measure2(source, plain2) : 0)) / 100;
|
|
47106
48805
|
return w2 / (across2 + (source ? FOOT_GAP : 0) + 2) / h2 * 100;
|
|
47107
48806
|
}
|
|
47108
48807
|
function drawFoot(ctx, rect, source, colours, font, size) {
|
|
@@ -47112,17 +48811,17 @@ function drawFoot(ctx, rect, source, colours, font, size) {
|
|
|
47112
48811
|
};
|
|
47113
48812
|
ctx.save();
|
|
47114
48813
|
const px2 = Math.min(size, footMost(measure2, rect.w, rect.h, source, font)) / 100 * rect.h;
|
|
47115
|
-
const [
|
|
47116
|
-
const pw = measure2(FOOT_PRE,
|
|
48814
|
+
const [plain2, mark] = footFonts(px2, font);
|
|
48815
|
+
const pw = measure2(FOOT_PRE, plain2), bw = measure2("VisualProof", mark);
|
|
47117
48816
|
const y2 = rect.y + rect.h - px2, x2 = rect.x + px2, right = rect.x + rect.w - px2;
|
|
47118
48817
|
const grad = ctx.createLinearGradient(x2 + pw, 0, x2 + pw + bw, 0);
|
|
47119
48818
|
BRAND.forEach((c3, k2) => grad.addColorStop(k2 / (BRAND.length - 1), c3));
|
|
47120
48819
|
writeNames(
|
|
47121
48820
|
ctx,
|
|
47122
48821
|
[
|
|
47123
|
-
{ text: FOOT_PRE, x: x2, y: y2, font:
|
|
48822
|
+
{ text: FOOT_PRE, x: x2, y: y2, font: plain2, align: "left", fill: colours.text },
|
|
47124
48823
|
{ text: "VisualProof", x: x2 + pw, y: y2, font: mark, align: "left", fill: grad },
|
|
47125
|
-
...source ? [{ text: source, x: right, y: y2, font:
|
|
48824
|
+
...source ? [{ text: source, x: right, y: y2, font: plain2, align: "right", fill: colours.text }] : []
|
|
47126
48825
|
],
|
|
47127
48826
|
colours.halo
|
|
47128
48827
|
);
|
|
@@ -47798,14 +49497,14 @@ var ArtFrame = class {
|
|
|
47798
49497
|
frame() {
|
|
47799
49498
|
if (!this.isOpen || this.s?.labels !== "chosen") return;
|
|
47800
49499
|
clearTimeout(this.rowsWait);
|
|
47801
|
-
const
|
|
49500
|
+
const read2 = () => {
|
|
47802
49501
|
const was = this.rows, wrote = this.wrote;
|
|
47803
49502
|
this.readRows();
|
|
47804
49503
|
const now = this.canvas.writtenNames();
|
|
47805
49504
|
if (was.length !== this.rows.length || was.some((r2, j2) => r2.id !== this.rows[j2].id) || wrote.size !== now.size || [...now].some((id) => !wrote.has(id))) this.renderRows();
|
|
47806
49505
|
};
|
|
47807
|
-
if (performance.now() - this.rowsAt > 1e3)
|
|
47808
|
-
else this.rowsWait = window.setTimeout(
|
|
49506
|
+
if (performance.now() - this.rowsAt > 1e3) read2();
|
|
49507
|
+
else this.rowsWait = window.setTimeout(read2, 300);
|
|
47809
49508
|
}
|
|
47810
49509
|
/** What the panel lists changed elsewhere (the review's targets): it is drawn again. */
|
|
47811
49510
|
refresh() {
|
|
@@ -47956,11 +49655,11 @@ var ArtFrame = class {
|
|
|
47956
49655
|
}
|
|
47957
49656
|
/** The frame in the middle of the room the panel leaves, of the size's shape. `keep`: the frame
|
|
47958
49657
|
* there keeps what it showed — the same middle, as much of the board. */
|
|
47959
|
-
place(
|
|
49658
|
+
place(keep2) {
|
|
47960
49659
|
const s2 = this.s;
|
|
47961
49660
|
const { x: x2, y: y2, w: w2, h: h2 } = frameRect({ w: innerWidth, h: innerHeight }, s2);
|
|
47962
49661
|
const was = this.rect;
|
|
47963
|
-
if (
|
|
49662
|
+
if (keep2 && was.w > 0) {
|
|
47964
49663
|
const C2 = this.canvas;
|
|
47965
49664
|
const [cx, cy] = C2.toWorld(was.x + was.w / 2, was.y + was.h / 2);
|
|
47966
49665
|
const k2 = C2.cam.k * Math.sqrt(w2 * h2 / (was.w * was.h));
|
|
@@ -48440,6 +50139,17 @@ function encodeState(s2) {
|
|
|
48440
50139
|
}
|
|
48441
50140
|
return p2.toString();
|
|
48442
50141
|
}
|
|
50142
|
+
function tipsIn(hash) {
|
|
50143
|
+
const text2 = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
50144
|
+
if (!text2) return void 0;
|
|
50145
|
+
const p2 = new URLSearchParams(text2);
|
|
50146
|
+
if (!p2.has("tips")) return void 0;
|
|
50147
|
+
const c3 = p2.get("tips").trim();
|
|
50148
|
+
return /^[a-z0-9]+(-[a-z0-9]+)*$/.test(c3) ? c3 : null;
|
|
50149
|
+
}
|
|
50150
|
+
function tipsPart(open, chapter) {
|
|
50151
|
+
return open ? `&tips${chapter ? `=${encodeURIComponent(chapter)}` : ""}` : "";
|
|
50152
|
+
}
|
|
48443
50153
|
function decodeState(hash) {
|
|
48444
50154
|
const text2 = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
48445
50155
|
if (!text2) return null;
|
|
@@ -49155,12 +50865,12 @@ var LayoutQueue = class {
|
|
|
49155
50865
|
/** What is wanted changed: what nobody wants is dropped, and a layout running is stopped where
|
|
49156
50866
|
* nobody wants it and a wanted one waits with no worker free; then the next ones start. */
|
|
49157
50867
|
prune() {
|
|
49158
|
-
const
|
|
50868
|
+
const keep2 = [];
|
|
49159
50869
|
for (const j2 of this.waiting) {
|
|
49160
|
-
if (this.want(j2.snapshot) > 0)
|
|
50870
|
+
if (this.want(j2.snapshot) > 0) keep2.push(j2);
|
|
49161
50871
|
else this.drop(j2);
|
|
49162
50872
|
}
|
|
49163
|
-
this.waiting =
|
|
50873
|
+
this.waiting = keep2;
|
|
49164
50874
|
for (const s2 of this.slots) {
|
|
49165
50875
|
if (this.waiting.length <= this.slots.filter((x2) => !x2.job).length) break;
|
|
49166
50876
|
const j2 = s2.job;
|
|
@@ -50184,7 +51894,7 @@ async function waitReady(data) {
|
|
|
50184
51894
|
const log = $4("#loader-log");
|
|
50185
51895
|
for (; ; ) {
|
|
50186
51896
|
const st2 = await data.status();
|
|
50187
|
-
log.textContent = st2.log.slice(-8).join("\n");
|
|
51897
|
+
log.textContent = (st2.log ?? []).slice(-8).join("\n");
|
|
50188
51898
|
if (st2.state === "ready") return { graph: st2.graph ?? null, editor: st2.editor ?? null };
|
|
50189
51899
|
if (st2.state === "error") {
|
|
50190
51900
|
$4("#loader-title").textContent = "Could not read the project";
|
|
@@ -50208,14 +51918,23 @@ async function boot() {
|
|
|
50208
51918
|
const ready = await waitReady(data);
|
|
50209
51919
|
let graphInfo = ready.graph;
|
|
50210
51920
|
const reviewing = data.review().catch(() => ({ epoch: "", rev: 0, data: emptyReview(), problems: [], error: "The review could not be loaded." }));
|
|
50211
|
-
const [meta0, edges0] = await Promise.all([data.meta(graphInfo?.version), data.edges(graphInfo?.version)]);
|
|
51921
|
+
const [meta0, edges0] = await Promise.all([data.meta(graphInfo?.version, graphInfo?.instance), data.edges(graphInfo?.version, graphInfo?.instance)]);
|
|
50212
51922
|
let meta = meta0;
|
|
50213
51923
|
let present = new Graph(meta, new Int32Array(edges0));
|
|
50214
51924
|
let g2 = present;
|
|
50215
51925
|
const store = new Store(meta.project.root);
|
|
50216
51926
|
const S2 = store.data;
|
|
51927
|
+
const kept = data.snapshot ? null : takeKept(meta.project.root);
|
|
50217
51928
|
const reader = data.snapshot ? null : (await data.identity()).readOnly?.by ?? null;
|
|
50218
51929
|
const editor = new Editor(store, ready.editor);
|
|
51930
|
+
let offerTip = (_rule) => {
|
|
51931
|
+
};
|
|
51932
|
+
let didTip = (_rule) => {
|
|
51933
|
+
};
|
|
51934
|
+
let countTip = (_name, _rule) => 0;
|
|
51935
|
+
let heardRelease = null;
|
|
51936
|
+
let hearRelease = (v2) => void (heardRelease = v2);
|
|
51937
|
+
let readOnlyLinks = false;
|
|
50219
51938
|
document.title = `${meta.project.name} \u2014 VisualProof`;
|
|
50220
51939
|
$4("#project").textContent = meta.project.name;
|
|
50221
51940
|
if (data.snapshot) {
|
|
@@ -50240,6 +51959,8 @@ async function boot() {
|
|
|
50240
51959
|
let updating = false;
|
|
50241
51960
|
let extractError = null;
|
|
50242
51961
|
let offline = false;
|
|
51962
|
+
let updatingTo = null;
|
|
51963
|
+
let reloadOffered = null;
|
|
50243
51964
|
let showLevel = () => {
|
|
50244
51965
|
};
|
|
50245
51966
|
let setEdges = (_edges) => {
|
|
@@ -50361,6 +52082,7 @@ async function boot() {
|
|
|
50361
52082
|
pastShown() ? Promise.resolve(PAST_REVIEW2) : data.postReview(op).then(
|
|
50362
52083
|
(c3) => {
|
|
50363
52084
|
if (c3) takeChange(c3);
|
|
52085
|
+
tipsOnReview(op);
|
|
50364
52086
|
return null;
|
|
50365
52087
|
},
|
|
50366
52088
|
(e2) => {
|
|
@@ -50450,6 +52172,7 @@ async function boot() {
|
|
|
50450
52172
|
go: (i2) => go(i2),
|
|
50451
52173
|
openSource: (name, from, to, quoted, root) => openSourceAt(name, from, to, quoted, root)
|
|
50452
52174
|
});
|
|
52175
|
+
if (kept) reviewBox.restoreDrafts(kept.review);
|
|
50453
52176
|
const declNow = (i2) => nav.board?.past?.past.nowName.get(g2.nodes[i2].name) ?? g2.nodes[i2].name;
|
|
50454
52177
|
const clickedDecls = () => picks.flatMap((p2) => {
|
|
50455
52178
|
const j2 = p2.root && "decl" in p2.root ? g2.byName.get(p2.root.decl) : void 0;
|
|
@@ -50561,8 +52284,8 @@ async function boot() {
|
|
|
50561
52284
|
if (!past || g2 !== past.graph) return null;
|
|
50562
52285
|
const s2 = past.snapshot;
|
|
50563
52286
|
const m3 = past.marks[i2] ?? 0;
|
|
50564
|
-
const
|
|
50565
|
-
const then = g2.isExt(i2) || !g2.nodes[i2].range ? "its statement and doc then." :
|
|
52287
|
+
const kept2 = g2.isExt(i2) || !g2.nodes[i2].range ? null : past.kept.get(g2.nodes[i2].module);
|
|
52288
|
+
const then = g2.isExt(i2) || !g2.nodes[i2].range ? "its statement and doc then." : kept2 ? "its statement, doc and source then (Space)." : "its statement and doc then; its source then is not kept.";
|
|
50566
52289
|
const now = past.nowName.get(g2.nodes[i2].name);
|
|
50567
52290
|
const what = m3 & PAST_GHOST ? "Gone now: no rename since carried it." : [m3 & PAST_RENAMED ? `Renamed since: now <code>${escapeHtml(now ?? "")}</code>.` : "", m3 & PAST_CHANGED ? "Changed since: another version now." : ""].filter(Boolean).join(" ");
|
|
50568
52291
|
return `<div class="banner tm-then"><b>As built ${escapeHtml(buildWhen(s2.at))}</b>${s2.commit ? ` \xB7 <code>${escapeHtml(s2.commit.hash.slice(0, 7))}</code>` : ""}: ${then}${what ? `<div class="tm-then-m">${what}</div>` : ""}</div>`;
|
|
@@ -50577,7 +52300,7 @@ async function boot() {
|
|
|
50577
52300
|
preview: (i2) => canvas.preview(i2 === null || toView(i2) < 0 ? null : toView(i2)),
|
|
50578
52301
|
previewCluster: (c3) => canvas.previewCluster(c3),
|
|
50579
52302
|
zoom: (c3) => void zoomCluster(c3),
|
|
50580
|
-
canZoom: (c3) => clusterScope(c3) !== null,
|
|
52303
|
+
canZoom: (c3) => !reader && clusterScope(c3) !== null,
|
|
50581
52304
|
hide: (c3) => hideLevel(canvas.layout.clusters[c3].key),
|
|
50582
52305
|
trust: (c3) => trust.state(canvas.layout.clusters[c3], cm()),
|
|
50583
52306
|
trustClicked: (c3, anchor) => trust.clicked(canvas.layout.clusters[c3], cm(), anchor),
|
|
@@ -50593,6 +52316,7 @@ async function boot() {
|
|
|
50593
52316
|
links: (key, mode) => papers.clusterHtml(key, mode)
|
|
50594
52317
|
});
|
|
50595
52318
|
const selBar = new SelectionBar($4("#selbar"), {
|
|
52319
|
+
canOpenViews: () => !reader,
|
|
50596
52320
|
show: (k2) => {
|
|
50597
52321
|
const to = shownOf(picks[k2]);
|
|
50598
52322
|
if (!to) return;
|
|
@@ -50694,6 +52418,7 @@ async function boot() {
|
|
|
50694
52418
|
sources.redrawReviews();
|
|
50695
52419
|
const k2 = i2 === null ? -1 : visitorOf(i2);
|
|
50696
52420
|
canvas.setLead(i2 !== null && toView(i2) >= 0 ? { kind: "node", i: toView(i2) } : k2 >= 0 ? { kind: "visitor", k: k2 } : null, leadEnd);
|
|
52421
|
+
if ($4("#panel details[data-rv-changes]:not([open])")) offerTip("stale-opinion");
|
|
50697
52422
|
if (i2 !== null && editedNow(i2) === null && !pastShown())
|
|
50698
52423
|
void edited(i2).then((e2) => {
|
|
50699
52424
|
if ((e2 || reviewBox.needsSource(i2)) && selected() === i2 && !pastShown()) {
|
|
@@ -50703,6 +52428,8 @@ async function boot() {
|
|
|
50703
52428
|
});
|
|
50704
52429
|
};
|
|
50705
52430
|
const linked = decodeState(location.hash);
|
|
52431
|
+
const filmLink = tipsIn(location.hash);
|
|
52432
|
+
let filmPart = "";
|
|
50706
52433
|
let first = linked ? linked.view ?? PROJECT : S2.active ?? PROJECT;
|
|
50707
52434
|
if (linked?.scope) {
|
|
50708
52435
|
const v2 = await views.open(linked.scope, { version: linked.version ?? null });
|
|
@@ -50774,7 +52501,7 @@ async function boot() {
|
|
|
50774
52501
|
menu: (t2, x2, y2, world) => openMenu(t2, x2, y2, world),
|
|
50775
52502
|
frozenDrag: () => say(frozenWhy())
|
|
50776
52503
|
});
|
|
50777
|
-
const ownOpen =
|
|
52504
|
+
const ownOpen = new Map(reader && kept ? kept.notes : []);
|
|
50778
52505
|
const asOpenHere = (ns) => ownOpen.size ? ns.map((n3) => ownOpen.has(n3.id) && ownOpen.get(n3.id) !== n3.open ? { ...n3, open: ownOpen.get(n3.id) } : n3) : ns;
|
|
50779
52506
|
const stickies = new Stickies($4("#stickies"), canvas, {
|
|
50780
52507
|
notes: () => ({ project: asOpenHere(projectNotes.list), view: asOpenHere(nav.board?.rec.notes ?? []) }),
|
|
@@ -51181,6 +52908,7 @@ async function boot() {
|
|
|
51181
52908
|
},
|
|
51182
52909
|
replied: () => chatWin.replied()
|
|
51183
52910
|
});
|
|
52911
|
+
if (kept) talk.restoreDrafts(kept.talk);
|
|
51184
52912
|
const describePlace = (v2) => {
|
|
51185
52913
|
const name = (m3) => TOOLS.includes(m3) ? toolName(m3) : m3;
|
|
51186
52914
|
const parts2 = [];
|
|
@@ -51321,10 +53049,10 @@ async function boot() {
|
|
|
51321
53049
|
if (line) say(line);
|
|
51322
53050
|
};
|
|
51323
53051
|
const openRecipe = async (link) => {
|
|
51324
|
-
const
|
|
51325
|
-
if ("error" in
|
|
53052
|
+
const read2 = readRecipe(decodeRecipe(link.text));
|
|
53053
|
+
if ("error" in read2) return say(`This link's artwork could not be read: ${read2.error}`);
|
|
51326
53054
|
const L2 = canvas.layout;
|
|
51327
|
-
const { look, problems, left } = resolveRecipe(
|
|
53055
|
+
const { look, problems, left } = resolveRecipe(read2.recipe, {
|
|
51328
53056
|
clusters: L2.clusters,
|
|
51329
53057
|
clusterOf: (name) => {
|
|
51330
53058
|
const i2 = g2.byName.get(name);
|
|
@@ -51686,11 +53414,11 @@ async function boot() {
|
|
|
51686
53414
|
};
|
|
51687
53415
|
const takeChange = (c3) => {
|
|
51688
53416
|
if (c3.epoch === review.epoch && c3.rev <= review.rev) return;
|
|
51689
|
-
const
|
|
53417
|
+
const read2 = (e2) => {
|
|
51690
53418
|
const o2 = e2;
|
|
51691
53419
|
return readEvent(e2, o2.agent === true, validPersonId(o2.person) ? o2.person : null);
|
|
51692
53420
|
};
|
|
51693
|
-
const events = c3.epoch === review.epoch && c3.base === review.rev && c3.events ? c3.events.map(
|
|
53421
|
+
const events = c3.epoch === review.epoch && c3.base === review.rev && c3.events ? c3.events.map(read2).filter((e2) => e2 !== null) : null;
|
|
51694
53422
|
if (!events || events.some((e2) => typeof e2 === "string")) {
|
|
51695
53423
|
void data.review().then(takeWhole, () => {
|
|
51696
53424
|
});
|
|
@@ -51799,12 +53527,39 @@ async function boot() {
|
|
|
51799
53527
|
say(`${pickWords(p2)} is inside ${pickWords(r2.covered)}, which is selected: nothing changed.`);
|
|
51800
53528
|
return false;
|
|
51801
53529
|
}
|
|
53530
|
+
const before = picks.length;
|
|
51802
53531
|
picks = r2.picks;
|
|
51803
53532
|
shown = r2.shown;
|
|
51804
53533
|
apply();
|
|
51805
53534
|
commit();
|
|
53535
|
+
tipsOnClick(p2, shift, before);
|
|
51806
53536
|
return true;
|
|
51807
53537
|
};
|
|
53538
|
+
let replaced = [];
|
|
53539
|
+
const tipsOnClick = (p2, shift, before) => {
|
|
53540
|
+
const decl = p2.root && "decl" in p2.root ? p2.root.decl : null;
|
|
53541
|
+
if (S2.mode === "plain") {
|
|
53542
|
+
if (shift) return didTip("selection-replace");
|
|
53543
|
+
if (!decl || !before) return;
|
|
53544
|
+
const now = Date.now();
|
|
53545
|
+
replaced = [...replaced.filter((t2) => now - t2 < 2e4), now];
|
|
53546
|
+
if (replaced.length >= 3) offerTip("selection-replace");
|
|
53547
|
+
} else if (S2.mode === "tcb" && decl) {
|
|
53548
|
+
const i2 = g2.byName.get(decl);
|
|
53549
|
+
const q2 = selectionQuery(picks);
|
|
53550
|
+
if (i2 !== void 0 && g2.nodes[i2].kind === "theorem" && q2 && litOn(q2).count > 1) offerTip("tcb-first");
|
|
53551
|
+
if (countTip("tcb-picks", "rests-only-tcb") >= 5) offerTip("rests-only-tcb");
|
|
53552
|
+
}
|
|
53553
|
+
};
|
|
53554
|
+
let approvedInFocus = 0;
|
|
53555
|
+
const tipsOnReview = (op) => {
|
|
53556
|
+
if (op.op === "key" && op.on) return didTip("key-in-target");
|
|
53557
|
+
if (op.op !== "opinion" || op.verdict !== "approve") return;
|
|
53558
|
+
const r2 = reviewRoot();
|
|
53559
|
+
const i2 = g2.byName.get(op.name);
|
|
53560
|
+
if (r2 === null || i2 === void 0 || !flow.inReview(r2, i2) || keyMarks(review.data, g2.nodes[r2].name).length) return;
|
|
53561
|
+
if (++approvedInFocus >= 2) offerTip("key-in-target");
|
|
53562
|
+
};
|
|
51808
53563
|
const useTool = (m3) => {
|
|
51809
53564
|
S2.mode = m3;
|
|
51810
53565
|
if (restsOn(m3)) S2.restsOn = m3;
|
|
@@ -51821,6 +53576,13 @@ async function boot() {
|
|
|
51821
53576
|
apply();
|
|
51822
53577
|
commit();
|
|
51823
53578
|
};
|
|
53579
|
+
const readerMode = (m3) => {
|
|
53580
|
+
if (m3 === "keys" || m3 === "deps" || m3 === "refs") {
|
|
53581
|
+
didTip("tcb-first");
|
|
53582
|
+
didTip("rests-only-tcb");
|
|
53583
|
+
}
|
|
53584
|
+
setMode(m3);
|
|
53585
|
+
};
|
|
51824
53586
|
const clear = () => {
|
|
51825
53587
|
picks = [];
|
|
51826
53588
|
shown = null;
|
|
@@ -51951,6 +53713,7 @@ async function boot() {
|
|
|
51951
53713
|
return false;
|
|
51952
53714
|
}
|
|
51953
53715
|
if (!await tm.open({ snapshot: at.snapshot })) return false;
|
|
53716
|
+
offerTip("first-time-machine");
|
|
51954
53717
|
if (!at.name || goName(at.name)) return true;
|
|
51955
53718
|
say(`${at.name} is not in the graph of that build.`);
|
|
51956
53719
|
return true;
|
|
@@ -52115,7 +53878,7 @@ async function boot() {
|
|
|
52115
53878
|
};
|
|
52116
53879
|
const writeAddress = (push) => {
|
|
52117
53880
|
if (!push && nav.busy) return;
|
|
52118
|
-
const hash = "#" + encodeState(current2());
|
|
53881
|
+
const hash = "#" + encodeState(current2()) + filmPart;
|
|
52119
53882
|
try {
|
|
52120
53883
|
if (push && hash !== location.hash) {
|
|
52121
53884
|
history.replaceState({ ...entry(), cam: { ...canvas.cam } }, "");
|
|
@@ -52200,9 +53963,14 @@ async function boot() {
|
|
|
52200
53963
|
let cls = "ok";
|
|
52201
53964
|
let text2 = "Built";
|
|
52202
53965
|
const c3 = build?.counts;
|
|
52203
|
-
if (offline) {
|
|
53966
|
+
if (offline && updatingTo) {
|
|
53967
|
+
cls = "building";
|
|
53968
|
+
text2 = `Updating to ${updatingTo}\u2026`;
|
|
53969
|
+
} else if (offline) {
|
|
52204
53970
|
cls = "off";
|
|
52205
53971
|
text2 = "Offline";
|
|
53972
|
+
} else if (reloadOffered) {
|
|
53973
|
+
text2 = "Updated \u2014 reload";
|
|
52206
53974
|
} else if (build?.active) {
|
|
52207
53975
|
cls = "building";
|
|
52208
53976
|
const p2 = build.progress;
|
|
@@ -52281,10 +54049,60 @@ async function boot() {
|
|
|
52281
54049
|
else timeMachine(snap2 !== void 0 ? { snapshot: Number(snap2) } : void 0);
|
|
52282
54050
|
});
|
|
52283
54051
|
let reconnected = false;
|
|
54052
|
+
const reloader = new Reloader({
|
|
54053
|
+
busy: () => {
|
|
54054
|
+
const a3 = document.activeElement;
|
|
54055
|
+
if ((a3 instanceof HTMLTextAreaElement || a3 instanceof HTMLInputElement && (a3.type === "text" || a3.type === "search")) && a3.value.trim()) return "text being typed";
|
|
54056
|
+
if (a3 instanceof HTMLElement && a3.isContentEditable) return "text being typed";
|
|
54057
|
+
if (stickies.editing) return "a note being written";
|
|
54058
|
+
if (canvas.pressed) return "a drag";
|
|
54059
|
+
if (exportModal.isOpen) return "the export window";
|
|
54060
|
+
if (artLook) return "the artwork mode";
|
|
54061
|
+
if (linear.shown) return "the linear form";
|
|
54062
|
+
return null;
|
|
54063
|
+
},
|
|
54064
|
+
kept: () => ({
|
|
54065
|
+
root: meta.project.root,
|
|
54066
|
+
at: Date.now(),
|
|
54067
|
+
sources: {
|
|
54068
|
+
following: sources.following ? { name: sources.following.name, collapsed: sources.following.collapsed } : null,
|
|
54069
|
+
pinned: sources.pinned.map((p2) => ({ name: p2.name, collapsed: p2.collapsed }))
|
|
54070
|
+
},
|
|
54071
|
+
review: reviewBox.keptDrafts,
|
|
54072
|
+
talk: talk.keptDrafts,
|
|
54073
|
+
notes: reader ? [...ownOpen] : []
|
|
54074
|
+
}),
|
|
54075
|
+
offer: (v2) => {
|
|
54076
|
+
reloadOffered = v2;
|
|
54077
|
+
renderBuild();
|
|
54078
|
+
},
|
|
54079
|
+
reload: () => location.reload()
|
|
54080
|
+
});
|
|
54081
|
+
pill.addEventListener(
|
|
54082
|
+
"click",
|
|
54083
|
+
(e2) => {
|
|
54084
|
+
if (!reloadOffered) return;
|
|
54085
|
+
e2.stopImmediatePropagation();
|
|
54086
|
+
reloader.go();
|
|
54087
|
+
},
|
|
54088
|
+
true
|
|
54089
|
+
);
|
|
52284
54090
|
const listen = () => {
|
|
52285
54091
|
if (data.snapshot) return;
|
|
52286
54092
|
const es = new EventSource(`/api/events?session=${people.session}`);
|
|
52287
|
-
es.addEventListener("hello", (e2) =>
|
|
54093
|
+
es.addEventListener("hello", (e2) => {
|
|
54094
|
+
const hello = JSON.parse(e2.data);
|
|
54095
|
+
people.connected(hello);
|
|
54096
|
+
updatingTo = null;
|
|
54097
|
+
reloader.hello(hello.server);
|
|
54098
|
+
const release = hello.server?.version;
|
|
54099
|
+
if (release && (!PAGE_VERSION || release === PAGE_VERSION)) hearRelease(release);
|
|
54100
|
+
renderBuild();
|
|
54101
|
+
});
|
|
54102
|
+
es.addEventListener("update", (e2) => {
|
|
54103
|
+
updatingTo = JSON.parse(e2.data).to ?? null;
|
|
54104
|
+
renderBuild();
|
|
54105
|
+
});
|
|
52288
54106
|
es.addEventListener("presence", (e2) => people.setPeers(JSON.parse(e2.data)));
|
|
52289
54107
|
es.addEventListener("private", (e2) => people.setPrivateCount(JSON.parse(e2.data).count));
|
|
52290
54108
|
es.addEventListener("people", (e2) => {
|
|
@@ -52360,9 +54178,9 @@ async function boot() {
|
|
|
52360
54178
|
es.addEventListener("graph", async (e2) => {
|
|
52361
54179
|
const info2 = JSON.parse(e2.data);
|
|
52362
54180
|
extractError = null;
|
|
52363
|
-
if (graphInfo && info2.version === graphInfo.version) return;
|
|
54181
|
+
if (graphInfo && info2.version === graphInfo.version && info2.instance === graphInfo.instance) return;
|
|
52364
54182
|
try {
|
|
52365
|
-
const [next, edges] = await Promise.all([data.meta(info2.version), data.edges(info2.version)]);
|
|
54183
|
+
const [next, edges] = await Promise.all([data.meta(info2.version, info2.instance), data.edges(info2.version, info2.instance)]);
|
|
52366
54184
|
graphInfo = info2;
|
|
52367
54185
|
await install(next, edges);
|
|
52368
54186
|
} catch {
|
|
@@ -52371,10 +54189,10 @@ async function boot() {
|
|
|
52371
54189
|
});
|
|
52372
54190
|
};
|
|
52373
54191
|
for (const b2 of document.querySelectorAll("#lightup [data-mode]")) {
|
|
52374
|
-
b2.addEventListener("click", () =>
|
|
54192
|
+
b2.addEventListener("click", () => readerMode(b2.dataset.mode));
|
|
52375
54193
|
}
|
|
52376
54194
|
$4("#rests-btn").addEventListener("click", () => {
|
|
52377
|
-
if (!restsOn(S2.mode))
|
|
54195
|
+
if (!restsOn(S2.mode)) readerMode(enterStep(S2.restsOn));
|
|
52378
54196
|
});
|
|
52379
54197
|
for (const b2 of document.querySelectorAll("#board-pop [data-cluster]")) {
|
|
52380
54198
|
b2.addEventListener("click", () => void sync.setClustering(b2.dataset.cluster));
|
|
@@ -52695,10 +54513,10 @@ async function boot() {
|
|
|
52695
54513
|
if (frozen()) return say(frozenWhy());
|
|
52696
54514
|
const why = await named2();
|
|
52697
54515
|
if (why) return say(why.replace("A view is made", "A note is written"));
|
|
52698
|
-
const
|
|
52699
|
-
const label = kind === "project" ? fixedScope(nav.board.rec) ? "A note of the project, on the Project and every view whose scope gives it" : "A note of the project, on this view, the Project and every view whose scope gives it" : `A note of \u201C${views.title(
|
|
54516
|
+
const board2 = nav.board.id;
|
|
54517
|
+
const label = kind === "project" ? fixedScope(nav.board.rec) ? "A note of the project, on the Project and every view whose scope gives it" : "A note of the project, on this view, the Project and every view whose scope gives it" : `A note of \u201C${views.title(board2)}\u201D alone`;
|
|
52700
54518
|
stickies.edit(at, "", label, (text2) => {
|
|
52701
|
-
if (text2 === null || nav.board.id !==
|
|
54519
|
+
if (text2 === null || nav.board.id !== board2) return;
|
|
52702
54520
|
const to = stickies.placeFor(kind, world);
|
|
52703
54521
|
if (!to) return;
|
|
52704
54522
|
const who = { open: true, by: people.name, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
@@ -52712,9 +54530,9 @@ async function boot() {
|
|
|
52712
54530
|
if (frozen()) return say(frozenWhy());
|
|
52713
54531
|
const why = await named2();
|
|
52714
54532
|
if (why) return say(why.replace("A view is made", "A note is edited"));
|
|
52715
|
-
const
|
|
54533
|
+
const board2 = nav.board.id;
|
|
52716
54534
|
stickies.edit(stickies.screenOf(id) ?? at ?? { x: innerWidth / 2 - 150, y: innerHeight / 3 }, n3.note.text, "Edit the note", (text2) => {
|
|
52717
|
-
if (text2 === null || text2 === n3.note.text || nav.board.id !==
|
|
54535
|
+
if (text2 === null || text2 === n3.note.text || nav.board.id !== board2 || !noteOf(id)) return;
|
|
52718
54536
|
const edit = { id, text: text2, by: people.name, at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
52719
54537
|
if (n3.kind === "project") void projectOps([{ op: "pnote-edit", ...edit }]);
|
|
52720
54538
|
else noteOps([{ op: "note-edit", ...edit }]);
|
|
@@ -52875,14 +54693,14 @@ async function boot() {
|
|
|
52875
54693
|
const no = noteRefused();
|
|
52876
54694
|
if (no) return say(no);
|
|
52877
54695
|
if (!await people.ensure()) return say("A file is added to the library under your name: give one first.");
|
|
52878
|
-
const
|
|
54696
|
+
const board2 = nav.board.id;
|
|
52879
54697
|
const spots = stickies.placesFor(canvas.toWorld(at.x, at.y), files2.length);
|
|
52880
54698
|
if (!spots) return say("This board is not drawn yet.");
|
|
52881
54699
|
const made3 = await notesOfFiles(files2, spots, {
|
|
52882
54700
|
upload: (f2) => uploadFile(f2, papers.headers()),
|
|
52883
54701
|
take: (c3) => takeChange(c3),
|
|
52884
54702
|
write: (note) => projectOps([{ op: "pnote", note }], true),
|
|
52885
|
-
still: () => nav.board.id ===
|
|
54703
|
+
still: () => nav.board.id === board2,
|
|
52886
54704
|
say: (text2) => say(text2),
|
|
52887
54705
|
by: people.name,
|
|
52888
54706
|
lost: () => people.lost()
|
|
@@ -53044,10 +54862,10 @@ ${n3.note.text.slice(0, 200)}`)) return;
|
|
|
53044
54862
|
const input = $4("#search");
|
|
53045
54863
|
const searchBox = new SearchBox(input, $4("#results"), g2, { go: (i2) => go(i2) });
|
|
53046
54864
|
window.addEventListener("keydown", (e2) => {
|
|
53047
|
-
const
|
|
54865
|
+
const win2 = sources.at(e2.target);
|
|
53048
54866
|
if (typing(e2.target) || typing(document.activeElement)) {
|
|
53049
54867
|
if (e2.key === "Escape" && e2.target !== input && !$4("#modal").contains(e2.target)) {
|
|
53050
|
-
if (
|
|
54868
|
+
if (win2) win2.el.focus();
|
|
53051
54869
|
else e2.target.blur();
|
|
53052
54870
|
}
|
|
53053
54871
|
return;
|
|
@@ -53065,26 +54883,26 @@ ${n3.note.text.slice(0, 200)}`)) return;
|
|
|
53065
54883
|
}
|
|
53066
54884
|
if (artLook && !ART_KEYS.has(e2.key)) return;
|
|
53067
54885
|
if ((e2.ctrlKey || e2.metaKey) && e2.key.length === 1) return;
|
|
53068
|
-
const route = routeKey(e2.key,
|
|
54886
|
+
const route = routeKey(e2.key, win2 !== null, !!e2.target?.closest?.("button, a[href], summary, select"));
|
|
53069
54887
|
if (route === "press") return;
|
|
53070
|
-
if (
|
|
54888
|
+
if (win2 && route === "close") {
|
|
53071
54889
|
e2.preventDefault();
|
|
53072
|
-
sources.close(
|
|
54890
|
+
sources.close(win2);
|
|
53073
54891
|
return;
|
|
53074
54892
|
}
|
|
53075
|
-
if (
|
|
54893
|
+
if (win2 && route === "own") {
|
|
53076
54894
|
if (e2.ctrlKey || e2.metaKey || e2.altKey || e2.repeat) return;
|
|
53077
54895
|
e2.preventDefault();
|
|
53078
|
-
if (e2.key === "k") toggleKey(
|
|
53079
|
-
else reviewBox.give(e2.key === "a" ? "approve" : "comment",
|
|
54896
|
+
if (e2.key === "k") toggleKey(win2.decl);
|
|
54897
|
+
else reviewBox.give(e2.key === "a" ? "approve" : "comment", win2.decl, win2.el);
|
|
53080
54898
|
return;
|
|
53081
54899
|
}
|
|
53082
54900
|
if (e2.key === "/") {
|
|
53083
54901
|
e2.preventDefault();
|
|
53084
54902
|
searchBox.focus();
|
|
53085
|
-
} else if (e2.key === "1")
|
|
53086
|
-
else if (e2.key === "2")
|
|
53087
|
-
else if (e2.key === "3")
|
|
54903
|
+
} else if (e2.key === "1") readerMode("plain");
|
|
54904
|
+
else if (e2.key === "2") readerMode(keyTwo(S2.mode, S2.restsOn));
|
|
54905
|
+
else if (e2.key === "3") readerMode("apps");
|
|
53088
54906
|
else if (e2.key === " ") {
|
|
53089
54907
|
e2.preventDefault();
|
|
53090
54908
|
const p2 = sources.following;
|
|
@@ -53134,6 +54952,7 @@ ${n3.note.text.slice(0, 200)}`)) return;
|
|
|
53134
54952
|
} else if (tm.active && !e2.altKey && (e2.key === "ArrowLeft" || e2.key === "ArrowRight")) {
|
|
53135
54953
|
e2.preventDefault();
|
|
53136
54954
|
tm.step(e2.key === "ArrowLeft" ? -1 : 1);
|
|
54955
|
+
didTip("first-time-machine");
|
|
53137
54956
|
} else if (tm.active && (e2.key === "Home" || e2.key === "End")) {
|
|
53138
54957
|
e2.preventDefault();
|
|
53139
54958
|
tm.end(e2.key === "End");
|
|
@@ -53144,15 +54963,16 @@ ${n3.note.text.slice(0, 200)}`)) return;
|
|
|
53144
54963
|
if (i2 !== null && pastShown()) say(PAST_REVIEW2);
|
|
53145
54964
|
else if (i2 !== null) reviewBox.give(e2.key === "a" ? "approve" : "comment", i2, f2 && !f2.collapsed ? f2.el : $4("#panel"));
|
|
53146
54965
|
} else if (e2.key === "k" && !e2.ctrlKey && !e2.metaKey && !e2.altKey && !e2.repeat) toggleKey();
|
|
53147
|
-
else if (e2.key === "?") guide.
|
|
54966
|
+
else if (e2.key === "?") guide.toggleMenu();
|
|
53148
54967
|
});
|
|
53149
|
-
window.visualproof = { canvas, graph: () => g2, present: () => present, board: () => nav.board, views, tm, timeMachine: (at) => openTimeMachine(at) };
|
|
54968
|
+
window.visualproof = { canvas, linear, graph: () => g2, present: () => present, board: () => nav.board, views, tm, timeMachine: (at) => openTimeMachine(at) };
|
|
53150
54969
|
window.addEventListener("resize", syncInsets);
|
|
53151
54970
|
const firstPlace = linked ? { mode: linked.mode, picks: linked.picks, shown: linked.shown, cam: null } : null;
|
|
53152
54971
|
if (!await nav.select(first, firstPlace)) await nav.show(PROJECT, firstPlace);
|
|
53153
54972
|
listen();
|
|
53154
54973
|
const snap = snapshotIn(location.hash);
|
|
53155
54974
|
if (snap) await openSnapshot(snap);
|
|
54975
|
+
const deepLink = filmLink !== void 0 || !!snap || !!recipeIn(location.hash) || !!linked && (!!linked.picks || !!linked.shown || linked.view !== null && linked.view !== PROJECT || !!linked.scope);
|
|
53156
54976
|
const recipe = recipeIn(location.hash);
|
|
53157
54977
|
if (recipe) void openRecipe(recipe);
|
|
53158
54978
|
if (!data.snapshot) void tm.load().then(() => renderTabs());
|
|
@@ -53164,31 +54984,219 @@ ${n3.note.text.slice(0, 200)}`)) return;
|
|
|
53164
54984
|
people.renderMe();
|
|
53165
54985
|
reviewChanged();
|
|
53166
54986
|
renderBuild();
|
|
54987
|
+
if (kept && (kept.sources.following || kept.sources.pinned.length)) {
|
|
54988
|
+
const on = (w2) => {
|
|
54989
|
+
const decl = present.byName.get(w2.name);
|
|
54990
|
+
return decl === void 0 ? null : { decl, collapsed: w2.collapsed };
|
|
54991
|
+
};
|
|
54992
|
+
const f2 = kept.sources.following && on(kept.sources.following);
|
|
54993
|
+
sources.mirror({ following: f2, pinned: kept.sources.pinned.map(on).filter((w2) => w2 !== null) });
|
|
54994
|
+
sources.release();
|
|
54995
|
+
}
|
|
53167
54996
|
$4("#loader").classList.add("gone");
|
|
54997
|
+
const tourSelect = (i2) => {
|
|
54998
|
+
if (selected() === i2) return reveal(i2);
|
|
54999
|
+
if (toView(i2) < 0) visitOn(i2, selected());
|
|
55000
|
+
const p2 = pickDecl(g2.nodes[i2].name);
|
|
55001
|
+
picks = [p2];
|
|
55002
|
+
shown = shownOf(p2);
|
|
55003
|
+
apply();
|
|
55004
|
+
reveal(i2);
|
|
55005
|
+
};
|
|
55006
|
+
const tourTool = (m3) => {
|
|
55007
|
+
if (m3 === S2.mode) return;
|
|
55008
|
+
S2.mode = m3;
|
|
55009
|
+
if (restsOn(m3)) S2.restsOn = m3;
|
|
55010
|
+
syncTool();
|
|
55011
|
+
const r2 = retool(picks, shown, m3, (root) => keysFor(root));
|
|
55012
|
+
if (r2.picks === picks) return;
|
|
55013
|
+
picks = r2.picks;
|
|
55014
|
+
shown = r2.shown;
|
|
55015
|
+
apply();
|
|
55016
|
+
};
|
|
55017
|
+
const tourPlace = () => ({ mode: S2.mode, restsOn: S2.restsOn, picks, shown, visitors, cam: { ...canvas.cam } });
|
|
55018
|
+
const tourBack = (p2) => {
|
|
55019
|
+
S2.mode = p2.mode;
|
|
55020
|
+
S2.restsOn = p2.restsOn;
|
|
55021
|
+
store.save();
|
|
55022
|
+
syncTool();
|
|
55023
|
+
picks = p2.picks;
|
|
55024
|
+
shown = p2.shown;
|
|
55025
|
+
visitors = p2.visitors;
|
|
55026
|
+
apply();
|
|
55027
|
+
if (reducedMotion()) canvas.jumpTo(p2.cam);
|
|
55028
|
+
else canvas.glideTo(p2.cam, 450);
|
|
55029
|
+
writeAddress(false);
|
|
55030
|
+
};
|
|
55031
|
+
const keysLit = (name) => {
|
|
55032
|
+
const q2 = selectionQuery([declPick("keys", name, keysFor({ decl: name }))]);
|
|
55033
|
+
return q2 ? Math.max(0, litOn(q2).count - 1) : 0;
|
|
55034
|
+
};
|
|
55035
|
+
const findExample = () => {
|
|
55036
|
+
if (selected() !== null) return selected();
|
|
55037
|
+
const targets = Object.keys(review.data.targets).map((n3) => g2.byName.get(n3)).filter((i2) => i2 !== void 0);
|
|
55038
|
+
const keyed = targets.find((i2) => keysLit(g2.nodes[i2].name) > 0);
|
|
55039
|
+
if (keyed !== void 0) return keyed;
|
|
55040
|
+
if (targets.length) return targets[0];
|
|
55041
|
+
let fallback = null;
|
|
55042
|
+
let tries = 0;
|
|
55043
|
+
for (let i2 = 0; i2 < g2.projectCount && tries < 400; i2++) {
|
|
55044
|
+
if (g2.nodes[i2].kind !== "theorem") continue;
|
|
55045
|
+
tries++;
|
|
55046
|
+
let n3 = 0, u2 = 0;
|
|
55047
|
+
const tcb = g2.reach([i2], SITE_STMT | SITE_BODY, "out");
|
|
55048
|
+
for (let j2 = 0; j2 < g2.projectCount; j2++) n3 += tcb[j2];
|
|
55049
|
+
if (n3 < 6 || n3 > 300) continue;
|
|
55050
|
+
const uses = g2.reach([i2], SITE_STMT | SITE_BODY | SITE_PROOF | SITE_GRAMMAR, "out");
|
|
55051
|
+
for (let j2 = 0; j2 < g2.projectCount; j2++) u2 += uses[j2];
|
|
55052
|
+
if (u2 > n3) return i2;
|
|
55053
|
+
fallback ??= i2;
|
|
55054
|
+
}
|
|
55055
|
+
return fallback;
|
|
55056
|
+
};
|
|
55057
|
+
let exampleOf = null;
|
|
55058
|
+
const tourExample = () => {
|
|
55059
|
+
const key = [g2, nav.board, review.epoch, review.rev, selected()];
|
|
55060
|
+
if (exampleOf && exampleOf.key.every((x2, k2) => x2 === key[k2])) return exampleOf.i;
|
|
55061
|
+
const i2 = findExample();
|
|
55062
|
+
exampleOf = { key, i: i2 };
|
|
55063
|
+
return i2;
|
|
55064
|
+
};
|
|
55065
|
+
const restsOnOf = (i2) => {
|
|
55066
|
+
const name = g2.nodes[i2].name;
|
|
55067
|
+
const G2 = vg();
|
|
55068
|
+
const d2 = boardReview();
|
|
55069
|
+
const lit = (tool) => {
|
|
55070
|
+
const q2 = selectionQuery([declPick(tool, name, keysFor({ decl: name }))]);
|
|
55071
|
+
return q2 ? litOn(q2).set : null;
|
|
55072
|
+
};
|
|
55073
|
+
const tcb = lit("tcb"), uses = lit("deps"), keys = lit("keys");
|
|
55074
|
+
const self2 = toView(i2);
|
|
55075
|
+
let read2 = 0, assumed = 0, proofs = 0, k2 = 0;
|
|
55076
|
+
for (let j2 = 0; j2 < G2.n; j2++) {
|
|
55077
|
+
if (j2 === self2) continue;
|
|
55078
|
+
if (keys?.[j2]) k2++;
|
|
55079
|
+
if (!tcb?.[j2]) {
|
|
55080
|
+
if (uses?.[j2]) proofs++;
|
|
55081
|
+
continue;
|
|
55082
|
+
}
|
|
55083
|
+
if (trusted(d2, G2.nodes[j2].name, G2.nodes[j2].module, G2.isExt(j2))) assumed++;
|
|
55084
|
+
else read2++;
|
|
55085
|
+
}
|
|
55086
|
+
return { name: g2.nodes[i2].display ?? name.split(".").pop(), read: read2, assumed, proofs, keys: k2, target: isTarget(name) };
|
|
55087
|
+
};
|
|
55088
|
+
const showMe = (action) => {
|
|
55089
|
+
const i2 = tourExample();
|
|
55090
|
+
const phone = isPhone();
|
|
55091
|
+
const pick = (m3) => {
|
|
55092
|
+
if (i2 === null) return false;
|
|
55093
|
+
if (m3) tourTool(m3);
|
|
55094
|
+
tourSelect(i2);
|
|
55095
|
+
return true;
|
|
55096
|
+
};
|
|
55097
|
+
switch (action) {
|
|
55098
|
+
case "ring-modes":
|
|
55099
|
+
return phone ? "#more-btn" : "#lightup";
|
|
55100
|
+
case "rests-steps":
|
|
55101
|
+
return pick("tcb") ? phone ? "#more-btn" : "#rests-steps" : null;
|
|
55102
|
+
case "used-by": {
|
|
55103
|
+
if (i2 === null) return null;
|
|
55104
|
+
const tcb = g2.reach([i2], SITE_STMT | SITE_BODY, "out");
|
|
55105
|
+
let d2 = i2;
|
|
55106
|
+
for (let j2 = 0; j2 < g2.projectCount; j2++) if (tcb[j2] && j2 !== i2 && g2.nodes[j2].kind === "def") {
|
|
55107
|
+
d2 = j2;
|
|
55108
|
+
break;
|
|
55109
|
+
}
|
|
55110
|
+
tourTool("apps");
|
|
55111
|
+
tourSelect(d2);
|
|
55112
|
+
return phone ? "#more-btn" : "#lightup [data-mode=apps]";
|
|
55113
|
+
}
|
|
55114
|
+
case "open-source":
|
|
55115
|
+
if (!pick(null)) return null;
|
|
55116
|
+
if (!sources.following) sources.toggle(selected());
|
|
55117
|
+
return ".sw-win";
|
|
55118
|
+
case "colour-status":
|
|
55119
|
+
if (ownScheme() !== "status") chooseScheme("status");
|
|
55120
|
+
$4("#legend [data-lg-open]")?.click();
|
|
55121
|
+
return "#legend";
|
|
55122
|
+
case "ring-review":
|
|
55123
|
+
return pick(null) ? "#panel .rv" : null;
|
|
55124
|
+
case "ring-target":
|
|
55125
|
+
return pick(null) ? phone ? "#panel [data-rv-target]" : "#panel [data-rv-target], #targets" : null;
|
|
55126
|
+
case "ring-key":
|
|
55127
|
+
return reviewRoot() !== null && pick(null) ? "#panel [data-rv-key]" : phone ? "#side-btn" : "#targets";
|
|
55128
|
+
case "what-changed": {
|
|
55129
|
+
const d2 = $4("#panel details[data-rv-changes]");
|
|
55130
|
+
if (!d2) return null;
|
|
55131
|
+
d2.open = true;
|
|
55132
|
+
return "#panel [data-rv-changes]";
|
|
55133
|
+
}
|
|
55134
|
+
case "ring-trusted":
|
|
55135
|
+
return phone ? "#side-btn" : "#trusted-btn";
|
|
55136
|
+
case "library-pair":
|
|
55137
|
+
return phone ? "#more-btn" : "#library-btn";
|
|
55138
|
+
case "ring-lock":
|
|
55139
|
+
if (!eyesPop.open) eyesPop.toggle(true);
|
|
55140
|
+
return "#lock-btn";
|
|
55141
|
+
case "ring-zoom":
|
|
55142
|
+
return pick("tcb") ? "#selbar [data-zoom]" : null;
|
|
55143
|
+
case "ring-board":
|
|
55144
|
+
return phone ? "#side-btn" : "#board-btn";
|
|
55145
|
+
case "ring-peers":
|
|
55146
|
+
return "#peers";
|
|
55147
|
+
// Share, the library and Export are in ⋯ on a phone
|
|
55148
|
+
case "ring-share":
|
|
55149
|
+
case "share-readonly":
|
|
55150
|
+
case "share-agent":
|
|
55151
|
+
return phone ? "#more-btn" : "#share-btn";
|
|
55152
|
+
case "linear":
|
|
55153
|
+
if (!linear.asked) linear.toggle();
|
|
55154
|
+
return "#linear";
|
|
55155
|
+
case "ring-export":
|
|
55156
|
+
return phone ? "#more-btn" : "#export-btn";
|
|
55157
|
+
case "time-machine":
|
|
55158
|
+
if (!tm.active) void openTimeMachine();
|
|
55159
|
+
return "#tmbar";
|
|
55160
|
+
case "ring-lost":
|
|
55161
|
+
if ($4("#build-pop").hidden) $4("#build").click();
|
|
55162
|
+
return "#build-pop [data-bp=lost]";
|
|
55163
|
+
}
|
|
55164
|
+
return null;
|
|
55165
|
+
};
|
|
55166
|
+
const canShowMe = (action) => {
|
|
55167
|
+
if (["rests-steps", "used-by", "open-source", "ring-review", "ring-target", "ring-zoom", "tcb-three"].includes(action)) return tourExample() !== null;
|
|
55168
|
+
if (isPhone() && (action === "ring-lock" || action === "linear")) return false;
|
|
55169
|
+
if (action === "ring-peers") return !$4("#peers").hidden;
|
|
55170
|
+
if (action === "what-changed") return !!$4("#panel details[data-rv-changes]");
|
|
55171
|
+
if (action === "share-readonly") return readOnlyLinks;
|
|
55172
|
+
if (action === "share-agent" || action === "ring-lock") return !reader;
|
|
55173
|
+
if (action === "time-machine" || action === "ring-lost") return !data.snapshot;
|
|
55174
|
+
return true;
|
|
55175
|
+
};
|
|
53168
55176
|
const guide = new Guide(store, {
|
|
53169
55177
|
project: () => meta.project.name,
|
|
53170
55178
|
about,
|
|
53171
55179
|
snapshot: () => !!data.snapshot,
|
|
53172
|
-
|
|
53173
|
-
|
|
53174
|
-
|
|
53175
|
-
|
|
53176
|
-
|
|
53177
|
-
|
|
53178
|
-
|
|
53179
|
-
|
|
53180
|
-
|
|
53181
|
-
|
|
53182
|
-
|
|
53183
|
-
|
|
53184
|
-
|
|
53185
|
-
|
|
53186
|
-
|
|
53187
|
-
return null;
|
|
53188
|
-
},
|
|
53189
|
-
select: (i2) => {
|
|
53190
|
-
if (selected() !== i2) go(i2);
|
|
55180
|
+
readOnly: () => !!data.snapshot || !!reader,
|
|
55181
|
+
following: () => people.followedName !== null,
|
|
55182
|
+
busy: () => !!artLook,
|
|
55183
|
+
example: tourExample,
|
|
55184
|
+
select: tourSelect,
|
|
55185
|
+
setTool: tourTool,
|
|
55186
|
+
place: tourPlace,
|
|
55187
|
+
back: (p2) => tourBack(p2),
|
|
55188
|
+
// Show me's moves, one step in the history
|
|
55189
|
+
record: commit,
|
|
55190
|
+
restsOn: restsOnOf,
|
|
55191
|
+
frameLit: () => {
|
|
55192
|
+
const q2 = selectionQuery(picks);
|
|
55193
|
+
const lit = q2 && litOn(q2);
|
|
55194
|
+
if (lit?.count) canvas.fitTo(lit.set, !reducedMotion());
|
|
53191
55195
|
},
|
|
55196
|
+
show: showMe,
|
|
55197
|
+
canShow: canShowMe,
|
|
55198
|
+
// the read-only link's tip on a server that makes them (web/src/share.ts)
|
|
55199
|
+
applies: (t2) => t2.id !== "read-only-link" || readOnlyLinks,
|
|
53192
55200
|
ended: () => {
|
|
53193
55201
|
},
|
|
53194
55202
|
editor: () => ({ server: editor.server, mine: editor.mine }),
|
|
@@ -53199,13 +55207,49 @@ ${n3.note.text.slice(0, 200)}`)) return;
|
|
|
53199
55207
|
showSelected();
|
|
53200
55208
|
}
|
|
53201
55209
|
return why;
|
|
53202
|
-
}
|
|
55210
|
+
},
|
|
55211
|
+
updates: () => data.snapshot ? Promise.resolve(null) : data.updates(),
|
|
55212
|
+
clips: () => data.snapshot ? Promise.resolve(/* @__PURE__ */ new Set()) : data.tipClips(),
|
|
55213
|
+
// the film in the address while it shows (`tips`), and not once the sheet closes
|
|
55214
|
+
shown: (open, chapter) => {
|
|
55215
|
+
const part = tipsPart(open, chapter);
|
|
55216
|
+
if (part === filmPart) return;
|
|
55217
|
+
filmPart = part;
|
|
55218
|
+
writeAddress(false);
|
|
55219
|
+
},
|
|
55220
|
+
tour: () => data.snapshot ? Promise.resolve(null) : data.tourJson()
|
|
55221
|
+
});
|
|
55222
|
+
offerTip = (rule) => guide.inPlace.offer(rule);
|
|
55223
|
+
didTip = (rule) => guide.inPlace.did(rule);
|
|
55224
|
+
countTip = (name, rule) => guide.inPlace.count(name, rule);
|
|
55225
|
+
hearRelease = (v2) => guide.serverIs(v2);
|
|
55226
|
+
if (heardRelease) guide.serverIs(heardRelease);
|
|
55227
|
+
if (!data.snapshot && !reader) void data.shareInfo().then((i2) => readOnlyLinks = !!i2.readOnlyLinks, () => {
|
|
53203
55228
|
});
|
|
53204
|
-
|
|
55229
|
+
let shareOpened = 0;
|
|
55230
|
+
$4("#share-btn").addEventListener(
|
|
55231
|
+
"click",
|
|
55232
|
+
() => setTimeout(() => {
|
|
55233
|
+
if (!$4("#share-pop").hidden && ++shareOpened >= 2 && readOnlyLinks) offerTip("share-twice");
|
|
55234
|
+
}, 300)
|
|
55235
|
+
);
|
|
55236
|
+
$4("#export-btn").addEventListener("click", () => setTimeout(() => exportModal.isOpen && offerTip("first-export"), 300));
|
|
55237
|
+
document.addEventListener(
|
|
55238
|
+
"click",
|
|
55239
|
+
(e2) => {
|
|
55240
|
+
const t2 = e2.target;
|
|
55241
|
+
if (t2.closest("#share-pop [data-tab=readonly]")) didTip("share-twice");
|
|
55242
|
+
else if (t2.closest("#panel [data-rv-changes] > summary")) didTip("stale-opinion");
|
|
55243
|
+
},
|
|
55244
|
+
true
|
|
55245
|
+
);
|
|
53205
55246
|
void data.activity().then((events) => activity.load(events));
|
|
53206
55247
|
void people.init().then(() => {
|
|
53207
55248
|
people.start();
|
|
53208
|
-
setTimeout(() =>
|
|
55249
|
+
setTimeout(() => {
|
|
55250
|
+
guide.welcome(deepLink);
|
|
55251
|
+
if (filmLink !== void 0) guide.openHelp("tips", filmLink ? { chapter: filmLink } : {});
|
|
55252
|
+
}, 400);
|
|
53209
55253
|
activity.changed();
|
|
53210
55254
|
let last2 = "";
|
|
53211
55255
|
setInterval(() => {
|