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.
Files changed (49) hide show
  1. package/README.md +52 -11
  2. package/dist/assets/tips/phone-fingers.light.mp4 +0 -0
  3. package/dist/assets/tips/phone-fingers.light.webp +0 -0
  4. package/dist/assets/tips/tour-agent.webp +0 -0
  5. package/dist/assets/tips/tour-arrange.webp +0 -0
  6. package/dist/assets/tips/tour-artwork.webp +0 -0
  7. package/dist/assets/tips/tour-checks.webp +0 -0
  8. package/dist/assets/tips/tour-export.webp +0 -0
  9. package/dist/assets/tips/tour-keys.webp +0 -0
  10. package/dist/assets/tips/tour-library.webp +0 -0
  11. package/dist/assets/tips/tour-project.webp +0 -0
  12. package/dist/assets/tips/tour-review.webp +0 -0
  13. package/dist/assets/tips/tour-share.webp +0 -0
  14. package/dist/assets/tips/tour-source.webp +0 -0
  15. package/dist/assets/tips/tour-target.webp +0 -0
  16. package/dist/assets/tips/tour-view.webp +0 -0
  17. package/dist/assets/tips/tour.json +289 -0
  18. package/dist/assets/tips/tour.webp +0 -0
  19. package/dist/mcp/visualproof-mcp.mjs +3 -1
  20. package/dist/server/agent.js +10 -5
  21. package/dist/server/children.js +85 -0
  22. package/dist/server/doctor.js +105 -42
  23. package/dist/server/events.js +10 -0
  24. package/dist/server/export.js +11 -4
  25. package/dist/server/extract.js +3 -2
  26. package/dist/server/figures.js +3 -2
  27. package/dist/server/film.js +380 -0
  28. package/dist/server/history.js +11 -1
  29. package/dist/server/hover.js +8 -5
  30. package/dist/server/live.js +66 -9
  31. package/dist/server/main.js +217 -16
  32. package/dist/server/media.js +114 -0
  33. package/dist/server/pipeline.js +6 -1
  34. package/dist/server/poppler.js +3 -2
  35. package/dist/server/readonlygate.js +4 -4
  36. package/dist/server/sourcestore.js +3 -2
  37. package/dist/server/status.js +3 -2
  38. package/dist/server/supervise.js +219 -0
  39. package/dist/server/updates.js +951 -0
  40. package/dist/server/usage.js +3 -1
  41. package/dist/server/version.js +183 -0
  42. package/dist/shared/timeline.js +5 -0
  43. package/dist/web/bundle/app.js +2299 -255
  44. package/dist/web/index.html +13 -1
  45. package/dist/web/src/canvas.js +22 -0
  46. package/dist/web/style.css +189 -1
  47. package/docs/agent-api/export.md +2 -2
  48. package/docs/agent-api/history.md +3 -2
  49. package/package.json +3 -1
@@ -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 plain = name.normalize("NFD").replace(new RegExp("\\p{M}+", "gu"), "").normalize("NFC");
2004
+ const plain2 = name.normalize("NFD").replace(new RegExp("\\p{M}+", "gu"), "").normalize("NFC");
2005
2005
  let slug = "";
2006
- for (const c3 of plain) {
2007
- const keep = /^[\p{L}\p{N}]$/u.test(c3) && c3.normalize("NFD") === c3 && c3.normalize("NFKC") === c3;
2008
- const add = keep ? c3 : slug.endsWith("-") ? "" : "-";
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, read) => {
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 = read();
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, board) {
2488
- return board.op === "all" || sameQuery(q2, board) ? q2 : { op: "and", a: q2, b: board };
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 plain = text2.replace(/```[\s\S]*?```/g, " ").replace(/[`*_]/g, "").replace(/\s+/g, " ").trim();
3637
- return plain.length > most ? plain.slice(0, most - 1).trimEnd() + "\u2026" : plain;
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, keep = true) {
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 || !keep ? [change] : [change, this.keep(v2, by)];
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, keep = true) {
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 || !keep ? [change] : [change, this.keep(v2, by)];
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 plain = (word) => {
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]) : plain(p2)).join(".");
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 plain(word);
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, board) {
5167
- return view === void 0 || view === board ? null : 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, keep) {
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 (!keep(u2)) continue;
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 (!keep(v2)) continue;
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 keep = [path[0]];
6222
- for (let k2 = 1; k2 < MAX_POINTS - 1; k2++) keep.push(path[Math.round(k2 * (path.length - 1) / (MAX_POINTS - 1))]);
6223
- keep.push(path[path.length - 1]);
6224
- path = keep;
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 pairs = aggregate(g2, unit, sites, (i2) => i2 < g2.projectCount || ext);
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(keep) {
6619
- for (const i2 of [...this.points.keys()]) if (!keep(i2)) this.points.delete(i2);
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 board = /* @__PURE__ */ new Map();
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 ? board : visitors;
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 = [...board.values()].sort((a3, b2) => Number(b2[2]) - Number(a3[2]));
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
- return `<div class="p-sec">${sectionHead(key, shut, title, aside)}${shut ? "" : `<div class="p-sec-body">${body()}</div>`}</div>`;
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, "Trusted definitions", `${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>`) : ""}
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 ? `?v=${v2}` : ""}`).then((r2) => r2.ok ? r2.json() : Promise.reject(new Error(`meta ${r2.status}`))),
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 ? `&v=${v2}` : ""}`).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),
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 keep = /* @__PURE__ */ new Set();
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 || keep.has(n3.id)) continue;
14338
- keep.add(n3.id);
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 (!keep.has(id)) {
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(read) {
16827
- return Object.assign({}, ...FOLLOWED.map((p2) => read[p2.part]()));
16874
+ function followedOf(read2) {
16875
+ return Object.assign({}, ...FOLLOWED.map((p2) => read2[p2.part]()));
16828
16876
  }
16829
- function presenceOf(read, tabs) {
16830
- return { ...followedOf(read), tabs };
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(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/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
- if (t2.closest("[data-tour-next]")) this.go(this.k + 1);
19143
- else if (t2.closest("[data-tour-back]")) this.go(this.k - 1);
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.go(this.k + 1);
19153
- else if (e2.key === "ArrowLeft") this.go(this.k - 1);
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
- if (t2 === this.help || t2.closest("[data-help-close]")) this.closeHelp();
19164
- else if (t2.closest("[data-help-tour]")) {
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 field = e2.target.closest("[data-help-editor]");
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
- if (e2.target.closest("[data-help-editor]")) this.help.querySelector("[data-help-editor-err]").hidden = true;
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
- /** The first visit: the tour, once. */
19191
- firstVisit() {
19192
- if (this.store.data.guided) return;
19193
- this.start();
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
- get showing() {
19196
- return !this.el.hidden || !this.help.hidden;
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 live = () => !this.hooks.snapshot();
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: "Click a declaration, or a circle for all of its declarations. <b>Mode</b> is how a click selects: just it, what it <b>rests on</b> (everything it uses, its TCB, the key declarations in it, or what it uses directly: <b>2</b> again for the next), or what uses it. Rest the pointer on a mode to read it. What you selected is listed above the panel.",
19218
- before: () => {
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() ? "<b>Approve</b> or <b>Comment</b> on what you have read. <b>Mark as target</b> to track a theorem's review." : "Opinions as they were when this snapshot was taken.",
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 <b>c</b>) and says what each colour means.",
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
- text: "Back, forward and links work as in a browser. <b>/</b> searches names, statements and docstrings; <b>?</b> lists every key.",
19255
- at: () => around(".brand .nav", ".search-wrap", "#help-btn")
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
- const last2 = k2 === this.steps.length - 1;
19271
- this.card.innerHTML = `<div class="tour-step">${k2 + 1} of ${this.steps.length}</div><div class="tour-title">${s2.title}</div><div class="tour-text">${s2.text}</div><div class="tour-actions"><button class="rv-btn" data-tour-skip>${last2 ? "Close" : "Skip"}</button><span class="tour-gap"></span>` + (k2 > 0 ? `<button class="rv-btn" data-tour-back>Back</button>` : "") + `<button class="rv-btn primary" data-tour-next>${k2 === 0 ? "Show me" : last2 ? "Done" : "Next"}</button></div>`;
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.top + 24];
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.store.data.guided = true;
19313
- this.store.save();
19314
- this.hooks.ended();
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
- openHelp() {
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 title = phone ? "Gestures" : "Keys and gestures";
19329
- this.help.innerHTML = `<div class="modal-box glass help-box${phone ? " help-touch" : ""}" role="dialog" aria-label="${title}" tabindex="-1"><div class="help-head"><div class="modal-title">${title}</div><button class="icon" data-help-close title="Close (Esc)">\u2715</button></div><div class="help-grid">${(phone ? TOUCH : KEYS).map(
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>`) + `<div class="help-about"><div class="help-h">About ${escapeHtml(this.hooks.project())}</div><div class="help-note">${escapeHtml(this.hooks.about())}</div></div><div class="help-about"><div class="help-h">About VisualProof</div><div class="help-note">Open source: its documentation, its code and its issues are at <a class="help-repo" href="${REPO}" target="_blank" rel="noopener noreferrer">${REPO.replace("https://", "")}</a>.</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>`;
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
- this.help.querySelector(".help-box")?.focus();
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 KEY2 = "visualproof:theme";
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(KEY2);
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(KEY2);
19405
- else localStorage.setItem(KEY2, choice);
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><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>`;
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 keep = (e2) => e2.target.closest?.("[data-sw-quote]") && e2.preventDefault();
20882
- el.addEventListener("pointerdown", keep);
20883
- el.addEventListener("mousedown", keep);
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 keep = !fresh3 && scroll ? scroll.scrollTop : null;
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 && keep !== null) scrolled.scrollTop = keep;
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 keep = !long || k2 < FIRST || l2.kind === "namespace" || l2.kind === "section" || l2.kind === "mutual" || used.has(k2) || / in$/.test(l2.text);
21508
- if (!keep) hidden++;
21509
- const cls = `sw-ctx-l${used.has(k2) ? " used" : ""}${keep ? "" : " fold"}`;
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 win = this.scroller.closest(".pw-win");
43239
- if (win && document.activeElement !== win) win.focus({ preventScroll: true });
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 keep = pairKeeps(w2.byHand ? w2.rect() : null, this.win.byHand ? this.win.rect() : null, pairArea(viewport2(), pairTop()));
44385
- if (this.win.byHand && !keep.paper) this.win.unmove();
44386
- if (!w2.byHand || keep.source) return;
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 plain = text2.replace(/\*\*|`/g, "").replace(/(^|\s)\*(\S[^*]*)\*/g, "$1$2");
47796
+ const plain2 = text2.replace(/\*\*|`/g, "").replace(/(^|\s)\*(\S[^*]*)\*/g, "$1$2");
46098
47797
  const out = [];
46099
- for (const para of plain.split("\n")) {
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 board = 1 - ease2(this.t);
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 = board >= 0.999 ? "" : String(board);
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 pairs = /* @__PURE__ */ new Map();
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 = pairs.get(key);
48108
+ const was = pairs2.get(key);
46410
48109
  if (was) was.lit ||= lit;
46411
- else pairs.set(key, { a: ru, b: rt2, lit });
48110
+ else pairs2.set(key, { a: ru, b: rt2, lit });
46412
48111
  }
46413
48112
  }
46414
48113
  const links = this.hooks.keyLinks();
46415
- const out = [...pairs.values()];
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 pairs = this.arcPairs();
46431
- const few = pairs.length <= ARCS_MOST;
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 pairs) {
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 [plain, mark] = footFonts(100, font);
47105
- const across2 = (measure2(FOOT_PRE, plain) + measure2("VisualProof", mark) + (source ? measure2(source, plain) : 0)) / 100;
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 [plain, mark] = footFonts(px2, font);
47116
- const pw = measure2(FOOT_PRE, plain), bw = measure2("VisualProof", mark);
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: plain, align: "left", fill: colours.text },
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: plain, align: "right", fill: colours.text }] : []
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 read = () => {
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) read();
47808
- else this.rowsWait = window.setTimeout(read, 300);
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(keep) {
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 (keep && was.w > 0) {
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 keep = [];
50868
+ const keep2 = [];
49159
50869
  for (const j2 of this.waiting) {
49160
- if (this.want(j2.snapshot) > 0) keep.push(j2);
50870
+ if (this.want(j2.snapshot) > 0) keep2.push(j2);
49161
50871
  else this.drop(j2);
49162
50872
  }
49163
- this.waiting = keep;
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 kept = g2.isExt(i2) || !g2.nodes[i2].range ? null : past.kept.get(g2.nodes[i2].module);
50565
- const then = g2.isExt(i2) || !g2.nodes[i2].range ? "its statement and doc then." : kept ? "its statement, doc and source then (Space)." : "its statement and doc then; its source then is not kept.";
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 = /* @__PURE__ */ new Map();
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 read = readRecipe(decodeRecipe(link.text));
51325
- if ("error" in read) return say(`This link's artwork could not be read: ${read.error}`);
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(read.recipe, {
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 read = (e2) => {
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(read).filter((e2) => e2 !== null) : null;
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) => people.connected(JSON.parse(e2.data)));
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", () => setMode(b2.dataset.mode));
54192
+ b2.addEventListener("click", () => readerMode(b2.dataset.mode));
52375
54193
  }
52376
54194
  $4("#rests-btn").addEventListener("click", () => {
52377
- if (!restsOn(S2.mode)) setMode(enterStep(S2.restsOn));
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 board = nav.board.id;
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(board)}\u201D alone`;
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 !== board) return;
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 board = nav.board.id;
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 !== board || !noteOf(id)) return;
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 board = nav.board.id;
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 === board,
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 win = sources.at(e2.target);
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 (win) win.el.focus();
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, win !== null, !!e2.target?.closest?.("button, a[href], summary, select"));
54886
+ const route = routeKey(e2.key, win2 !== null, !!e2.target?.closest?.("button, a[href], summary, select"));
53069
54887
  if (route === "press") return;
53070
- if (win && route === "close") {
54888
+ if (win2 && route === "close") {
53071
54889
  e2.preventDefault();
53072
- sources.close(win);
54890
+ sources.close(win2);
53073
54891
  return;
53074
54892
  }
53075
- if (win && route === "own") {
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(win.decl);
53079
- else reviewBox.give(e2.key === "a" ? "approve" : "comment", win.decl, win.el);
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") setMode("plain");
53086
- else if (e2.key === "2") setMode(keyTwo(S2.mode, S2.restsOn));
53087
- else if (e2.key === "3") setMode("apps");
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.toggleHelp();
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
- example: () => {
53173
- if (selected() !== null) return selected();
53174
- for (const name of Object.keys(review.data.targets)) {
53175
- const i2 = g2.byName.get(name);
53176
- if (i2 !== void 0) return i2;
53177
- }
53178
- let tries = 0;
53179
- for (let i2 = 0; i2 < g2.projectCount && tries < 400; i2++) {
53180
- if (g2.nodes[i2].kind !== "theorem") continue;
53181
- tries++;
53182
- let n3 = 0;
53183
- const seen = g2.reach([i2], SITE_STMT | SITE_BODY, "out");
53184
- for (let j2 = 0; j2 < g2.projectCount; j2++) n3 += seen[j2];
53185
- if (n3 >= 6 && n3 <= 300) return i2;
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
- $4("#help-btn").addEventListener("click", () => guide.toggleHelp());
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(() => guide.firstVisit(), 400);
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(() => {