visualproof 0.1.0 → 0.3.0

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 (94) hide show
  1. package/README.md +40 -26
  2. package/dist/assets/webfonts/LICENSE-Jost.txt +93 -0
  3. package/dist/assets/webfonts/LICENSE-Noto.txt +95 -0
  4. package/dist/assets/webfonts/LICENSE-SourceSans3.txt +93 -0
  5. package/dist/assets/webfonts/LICENSE-SourceSerif4.txt +93 -0
  6. package/dist/assets/webfonts/jost-latin-ext-wght-normal.woff2 +0 -0
  7. package/dist/assets/webfonts/jost-latin-wght-normal.woff2 +0 -0
  8. package/dist/assets/webfonts/metrics.json +1 -0
  9. package/dist/assets/webfonts/source-sans-3-greek-wght-italic.woff2 +0 -0
  10. package/dist/assets/webfonts/source-sans-3-greek-wght-normal.woff2 +0 -0
  11. package/dist/assets/webfonts/source-sans-3-latin-ext-wght-italic.woff2 +0 -0
  12. package/dist/assets/webfonts/source-sans-3-latin-ext-wght-normal.woff2 +0 -0
  13. package/dist/assets/webfonts/source-sans-3-latin-wght-italic.woff2 +0 -0
  14. package/dist/assets/webfonts/source-sans-3-latin-wght-normal.woff2 +0 -0
  15. package/dist/assets/webfonts/source-serif-4-greek-wght-italic.woff2 +0 -0
  16. package/dist/assets/webfonts/source-serif-4-greek-wght-normal.woff2 +0 -0
  17. package/dist/assets/webfonts/source-serif-4-latin-ext-wght-italic.woff2 +0 -0
  18. package/dist/assets/webfonts/source-serif-4-latin-ext-wght-normal.woff2 +0 -0
  19. package/dist/assets/webfonts/source-serif-4-latin-wght-italic.woff2 +0 -0
  20. package/dist/assets/webfonts/source-serif-4-latin-wght-normal.woff2 +0 -0
  21. package/dist/assets/webfonts/ttf/jost-400-normal.ttf +0 -0
  22. package/dist/assets/webfonts/ttf/jost-500-normal.ttf +0 -0
  23. package/dist/assets/webfonts/ttf/noto-sans-math.ttf +0 -0
  24. package/dist/assets/webfonts/ttf/noto-sans.ttf +0 -0
  25. package/dist/assets/webfonts/ttf/source-sans-3-400-italic.ttf +0 -0
  26. package/dist/assets/webfonts/ttf/source-sans-3-400-normal.ttf +0 -0
  27. package/dist/assets/webfonts/ttf/source-sans-3-500-normal.ttf +0 -0
  28. package/dist/assets/webfonts/ttf/source-sans-3-600-normal.ttf +0 -0
  29. package/dist/assets/webfonts/ttf/source-sans-3-700-normal.ttf +0 -0
  30. package/dist/assets/webfonts/ttf/source-serif-4-400-italic.ttf +0 -0
  31. package/dist/assets/webfonts/ttf/source-serif-4-600-italic.ttf +0 -0
  32. package/dist/server/agent.js +125 -92
  33. package/dist/server/agentdocs.js +1 -1
  34. package/dist/server/artcmd.js +335 -0
  35. package/dist/server/history.js +39 -0
  36. package/dist/server/historyapi.js +2 -2
  37. package/dist/server/hover.js +3 -3
  38. package/dist/server/layoutthread.js +1 -1
  39. package/dist/server/live.js +6 -0
  40. package/dist/server/main.js +151 -11
  41. package/dist/server/mcptools.js +6 -4
  42. package/dist/server/pastdoc.js +1 -1
  43. package/dist/server/pipeline.js +18 -9
  44. package/dist/server/readonlygate.js +2 -2
  45. package/dist/server/review.js +4 -1
  46. package/dist/server/reviewapi.js +35 -1
  47. package/dist/server/snapshot.js +13 -3
  48. package/dist/server/usage.js +2 -2
  49. package/dist/server/views.js +25 -1
  50. package/dist/shared/artfonts.js +81 -0
  51. package/dist/shared/artrecipe.js +316 -0
  52. package/dist/shared/collab.js +99 -1
  53. package/dist/shared/declfind.js +95 -0
  54. package/dist/shared/doc/order.js +17 -0
  55. package/dist/shared/doc/outline.js +45 -0
  56. package/dist/shared/keys.js +1 -1
  57. package/dist/shared/lostfound.js +1 -0
  58. package/dist/shared/moves.js +2 -1
  59. package/dist/shared/pastgraph.js +9 -1
  60. package/dist/shared/query.js +21 -13
  61. package/dist/shared/review.js +62 -5
  62. package/dist/shared/sharesnap.js +2 -1
  63. package/dist/shared/titles.js +2 -0
  64. package/dist/shared/viewgraph.js +18 -0
  65. package/dist/web/bundle/app.js +7709 -3222
  66. package/dist/web/bundle/edges.js +4 -6
  67. package/dist/web/index.html +24 -6
  68. package/dist/web/src/agentmark.js +14 -0
  69. package/dist/web/src/artwork.js +1000 -0
  70. package/dist/web/src/bundles.js +151 -0
  71. package/dist/web/src/canvas.js +3694 -0
  72. package/dist/web/src/drag.js +61 -0
  73. package/dist/web/src/edgequeue.js +204 -0
  74. package/dist/web/src/gl/gl.js +123 -0
  75. package/dist/web/src/gl/renderer.js +451 -0
  76. package/dist/web/src/halo.js +95 -0
  77. package/dist/web/src/highlight.js +108 -0
  78. package/dist/web/src/overview.js +393 -0
  79. package/dist/web/src/reviewqueue.js +250 -0
  80. package/dist/web/src/scene.js +17 -0
  81. package/dist/web/src/selection.js +272 -0
  82. package/dist/web/src/status.js +80 -0
  83. package/dist/web/src/visitors.js +318 -0
  84. package/dist/web/src/visits.js +174 -0
  85. package/dist/web/style.css +255 -12
  86. package/docs/agent-api/README.md +1 -1
  87. package/docs/agent-api/export.md +41 -2
  88. package/docs/agent-api/following.md +2 -2
  89. package/docs/agent-api/history.md +2 -1
  90. package/docs/agent-api/mcp.md +1 -1
  91. package/docs/agent-api/reading.md +2 -1
  92. package/docs/agent-api/views.md +3 -2
  93. package/package.json +3 -1
  94. package/test/toolchains.json +2 -2
@@ -0,0 +1,174 @@
1
+ // The reader's visitors laid on the board's scene (ViewSpec decision 27, the owner's decision of
2
+ // 2026-10-11): each in its own place in the cluster hierarchy, the clusters it needs added as
3
+ // temporary ones (web/src/visitors.ts, planVisitors), the outermost of them in the deepest cluster the
4
+ // board draws, at a spot chosen once.
5
+ //
6
+ // Making room: what a visitor brings goes where its cluster has free room, nearest the declarations it
7
+ // has references with; where the cluster has none, against its rim where it covers least, and what it
8
+ // covers makes way, as from a drop, the circle growing only where that cannot fit (web/src/scene.ts,
9
+ // setHeld). Nothing of it is a pin or the board's: when the visitors go, every place comes back.
10
+ //
11
+ // A spot is in its cluster's coordinates and carried to followers with the trail (PresenceView), so a
12
+ // follower draws the temporary clusters where the leader does; the rest — which clusters, nested how,
13
+ // packed how — is a function of the visitors and the board, the same on every page.
14
+ import { LEAF_R } from "./scene.js";
15
+ import { freeSpot, packPlan, planVisitors, rimSpot } from "./visitors.js";
16
+ /** How far outside what it holds a cluster's circle is drawn (web/src/scene.ts, computeExtents). */
17
+ const DRAWN_MARGIN = 6;
18
+ /** Whether a declaration's name is a namespace too, on a board whose declarations are `names` (the
19
+ * outside world's left out), the visitors' among them: a structure then sits in its own namespace's
20
+ * cluster, as the board would draw it (shared/layout.ts, pathOf). */
21
+ export function prefixTest(g, visitors) {
22
+ const memo = new Map();
23
+ return (name) => {
24
+ let hit = memo.get(name);
25
+ if (hit !== undefined)
26
+ return hit;
27
+ const head = `${name}.`;
28
+ hit = visitors.some((v) => v.startsWith(head));
29
+ for (let i = 0; !hit && i < g.n; i++)
30
+ if (!g.isExt(i) && g.nodes[i].name.startsWith(head))
31
+ hit = true;
32
+ memo.set(name, hit);
33
+ return hit;
34
+ };
35
+ }
36
+ /** Where the visitors `vs` belong on a board (web/src/visitors.ts, planVisitors): its layout `L` by
37
+ * `mode`, the levels `hidden`, its graph `g`. */
38
+ export function planOn(L, mode, hidden, g, vs) {
39
+ const drawn = new Map(L.clusters.map((c, k) => [c.key, k]));
40
+ return planVisitors(mode, drawn, new Set(hidden), vs, prefixTest(g, vs.map((v) => v.name)));
41
+ }
42
+ /** The board's clusters round the places of the visitors `chosen` (indices of the trail): the folded
43
+ * ones among them open for the reader while those are selected (ViewSpec decision 27). */
44
+ export function openFor(plan, parent, chosen) {
45
+ const out = new Set();
46
+ for (const k of chosen)
47
+ for (let c = plan.at[k]?.in ?? -1; c >= 0; c = parent[c])
48
+ out.add(c);
49
+ return out;
50
+ }
51
+ /** Place the visitors `vs` (the trail, oldest first) on a board as `plan` says (planOn): its layout
52
+ * `L`, its scene `S` as the board arranges it. An item keeps the place its oldest visitor has in
53
+ * that cluster; one with none is given a spot — in the order its oldest visitor came, so that a newer
54
+ * one never moves an older one — where `ok` (a spot of the world and a radius) finds no cluster's
55
+ * name written. */
56
+ export function placeVisits(S, L, plan, vs, ok = () => true) {
57
+ const names = vs.map((v) => v.name);
58
+ const pack = packPlan(plan, names);
59
+ const keyOf = (c) => (c < 0 ? "" : L.clusters[c].key);
60
+ // the outermost items, and which visitors each holds
61
+ const items = [];
62
+ const ofTemp = new Map();
63
+ plan.temps.forEach((t, k) => {
64
+ if (t.up >= 0)
65
+ return;
66
+ ofTemp.set(k, items.length);
67
+ items.push({ in: t.in, temp: k, visitor: -1, at: [0, 0], r: pack.r[k], slot: 0 });
68
+ });
69
+ const item = plan.at.map((a, k) => {
70
+ if (a.up < 0) {
71
+ items.push({ in: a.in, temp: -1, visitor: k, at: [0, 0], r: LEAF_R, slot: 0 });
72
+ return items.length - 1;
73
+ }
74
+ let t = a.up;
75
+ while (plan.temps[t].up >= 0)
76
+ t = plan.temps[t].up;
77
+ return ofTemp.get(t);
78
+ });
79
+ const members = items.map(() => []);
80
+ item.forEach((j, k) => members[j].push(k));
81
+ // in the order their oldest visitors came
82
+ const order = items.map((_, j) => j).sort((a, b) => members[a][0] - members[b][0]);
83
+ const placed = new Map();
84
+ for (const j of order) {
85
+ const it = items[j];
86
+ const kept = members[j].map((k) => vs[k].place).find((p) => p && p.in === keyOf(it.in));
87
+ if (kept)
88
+ it.at = [kept.at[0], kept.at[1]];
89
+ else {
90
+ const room = S.roomOf(it.in);
91
+ // a sub-cluster's circle is drawn a little outside what it holds (Scene, computeExtents)
92
+ const bodies = [...S.discsIn(it.in).map((d) => (d.r > LEAF_R ? { ...d, r: d.r + DRAWN_MARGIN } : d)), ...(placed.get(it.in) ?? []).map((o) => ({ x: o.at[0], y: o.at[1], r: o.r }))];
93
+ // towards the declarations it has references with, as the board arranges them
94
+ const [ox, oy] = S.anchorWorld(it.in);
95
+ let sx = 0, sy = 0, m = 0;
96
+ for (const k of members[j])
97
+ for (const i of vs[k].near) {
98
+ const [cx, cy] = S.anchorWorld(S.nodeCluster[i]);
99
+ sx += cx + S.ancNX[i];
100
+ sy += cy + S.ancNY[i];
101
+ m++;
102
+ }
103
+ const prefer = m ? [sx / m - ox, sy / m - oy] : [0, 0];
104
+ const okHere = (x, y) => ok(ox + x, oy + y, it.r);
105
+ it.at = freeSpot(bodies, room, it.r, prefer, okHere) ?? rimSpot(bodies, room, it.r, prefer);
106
+ }
107
+ const list = placed.get(it.in) ?? [];
108
+ it.slot = list.length;
109
+ list.push(it);
110
+ placed.set(it.in, list);
111
+ }
112
+ const held = new Map();
113
+ for (const [c, list] of placed)
114
+ held.set(c, list.map((o) => ({ x: o.at[0], y: o.at[1], r: o.r })));
115
+ const places = item.map((j) => ({ in: keyOf(items[j].in), at: [items[j].at[0], items[j].at[1]] }));
116
+ return { plan, pack, items, item, places, held };
117
+ }
118
+ export function visitWorld(V, S) {
119
+ const T = V.plan.temps.length, K = V.plan.at.length;
120
+ const W = { tx: new Float64Array(T), ty: new Float64Array(T), tr: new Float64Array(T), trep: new Int32Array(T), vx: new Float64Array(K), vy: new Float64Array(K), vrep: new Int32Array(K) };
121
+ const centre = (it) => {
122
+ const at = S.heldAt.get(it.in);
123
+ const hx = at ? at[2 * it.slot] : it.at[0], hy = at ? at[2 * it.slot + 1] : it.at[1];
124
+ if (it.in < 0)
125
+ return [hx, hy, 1];
126
+ const s = S.scale[it.in];
127
+ return [S.cwx[it.in] + hx * s, S.cwy[it.in] + hy * s, s];
128
+ };
129
+ const scaleOf = (c) => (c < 0 ? 1 : S.scale[c]);
130
+ const repOf = (c) => (c < 0 ? -1 : S.rep[c]);
131
+ for (let t = 0; t < T; t++) {
132
+ const tc = V.plan.temps[t];
133
+ const s = scaleOf(tc.in);
134
+ if (tc.up < 0) {
135
+ const it = V.items[V.items.findIndex((x) => x.temp === t)];
136
+ [W.tx[t], W.ty[t]] = centre(it);
137
+ }
138
+ else {
139
+ W.tx[t] = W.tx[tc.up] + V.pack.x[t] * s;
140
+ W.ty[t] = W.ty[tc.up] + V.pack.y[t] * s;
141
+ }
142
+ W.tr[t] = V.pack.r[t] * s;
143
+ W.trep[t] = repOf(tc.in);
144
+ }
145
+ for (let k = 0; k < K; k++) {
146
+ const a = V.plan.at[k];
147
+ if (a.up >= 0) {
148
+ const s = scaleOf(a.in);
149
+ W.vx[k] = W.tx[a.up] + V.pack.vx[k] * s;
150
+ W.vy[k] = W.ty[a.up] + V.pack.vy[k] * s;
151
+ }
152
+ else
153
+ [W.vx[k], W.vy[k]] = centre(V.items[V.item[k]]);
154
+ W.vrep[k] = repOf(a.in);
155
+ }
156
+ return W;
157
+ }
158
+ /** A visitor's chain of position ids up to the top level, as its edges are bundled through the
159
+ * hierarchy (web/src/bundles.ts, joinChains): itself, its temporary clusters, the board's clusters
160
+ * round it — or, hidden in a folded cluster's card, that card and the clusters round it. Ids past the
161
+ * scene's (`base` = clusters and declarations) are the visitors' own: a temporary cluster t at
162
+ * base + t, a visitor k at base + temps + k. */
163
+ export function visitChain(V, W, S, base, k) {
164
+ const out = [];
165
+ const a = V.plan.at[k];
166
+ if (W.vrep[k] < 0) {
167
+ out.push(base + V.plan.temps.length + k);
168
+ for (let t = a.up; t >= 0; t = V.plan.temps[t].up)
169
+ out.push(base + t);
170
+ }
171
+ for (let c = W.vrep[k] >= 0 ? W.vrep[k] : a.in; c >= 0; c = S.parent[c])
172
+ out.push(c);
173
+ return out;
174
+ }
@@ -1,10 +1,33 @@
1
1
  /* VisualProof — quiet surfaces, one gradient for what matters. */
2
2
 
3
+ /* the fonts an artwork is drawn in (ExportSpec decision 19; assets/webfonts/, SIL Open Font License):
4
+ Source Sans 3 for its names, Source Serif 4 for the starfield's; their Latin and Greek, the rest of
5
+ a name from the fonts the system has */
6
+ @font-face { font-family: "Source Sans 3"; font-style: normal; font-display: swap; font-weight: 200 900; src: url("/fonts/source-sans-3-greek-wght-normal.woff2") format("woff2"); unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF; }
7
+ @font-face { font-family: "Source Sans 3"; font-style: normal; font-display: swap; font-weight: 200 900; src: url("/fonts/source-sans-3-latin-ext-wght-normal.woff2") format("woff2"); unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C4,U+2113,U+2C60-2C7F,U+A720-A7FF; }
8
+ @font-face { font-family: "Source Sans 3"; font-style: normal; font-display: swap; font-weight: 200 900; src: url("/fonts/source-sans-3-latin-wght-normal.woff2") format("woff2"); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
9
+ @font-face { font-family: "Source Sans 3"; font-style: italic; font-display: swap; font-weight: 200 900; src: url("/fonts/source-sans-3-greek-wght-italic.woff2") format("woff2"); unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF; }
10
+ @font-face { font-family: "Source Sans 3"; font-style: italic; font-display: swap; font-weight: 200 900; src: url("/fonts/source-sans-3-latin-ext-wght-italic.woff2") format("woff2"); unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C4,U+2113,U+2C60-2C7F,U+A720-A7FF; }
11
+ @font-face { font-family: "Source Sans 3"; font-style: italic; font-display: swap; font-weight: 200 900; src: url("/fonts/source-sans-3-latin-wght-italic.woff2") format("woff2"); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
12
+ @font-face { font-family: "Source Serif 4"; font-style: normal; font-display: swap; font-weight: 200 900; src: url("/fonts/source-serif-4-greek-wght-normal.woff2") format("woff2"); unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF; }
13
+ @font-face { font-family: "Source Serif 4"; font-style: normal; font-display: swap; font-weight: 200 900; src: url("/fonts/source-serif-4-latin-ext-wght-normal.woff2") format("woff2"); unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C4,U+2113,U+2C60-2C7F,U+A720-A7FF; }
14
+ @font-face { font-family: "Source Serif 4"; font-style: normal; font-display: swap; font-weight: 200 900; src: url("/fonts/source-serif-4-latin-wght-normal.woff2") format("woff2"); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
15
+ @font-face { font-family: "Source Serif 4"; font-style: italic; font-display: swap; font-weight: 200 900; src: url("/fonts/source-serif-4-greek-wght-italic.woff2") format("woff2"); unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF; }
16
+ @font-face { font-family: "Source Serif 4"; font-style: italic; font-display: swap; font-weight: 200 900; src: url("/fonts/source-serif-4-latin-ext-wght-italic.woff2") format("woff2"); unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C4,U+2113,U+2C60-2C7F,U+A720-A7FF; }
17
+ @font-face { font-family: "Source Serif 4"; font-style: italic; font-display: swap; font-weight: 200 900; src: url("/fonts/source-serif-4-latin-wght-italic.woff2") format("woff2"); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
18
+
19
+ /* Jost, for an artwork's title and its top-level clusters' names, in spaced capitals (the owner's
20
+ decision of 2026-10-15): its Latin */
21
+ @font-face { font-family: "Jost"; font-style: normal; font-display: swap; font-weight: 100 900; src: url("/fonts/jost-latin-ext-wght-normal.woff2") format("woff2"); unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; }
22
+ @font-face { font-family: "Jost"; font-style: normal; font-display: swap; font-weight: 100 900; src: url("/fonts/jost-latin-wght-normal.woff2") format("woff2"); unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; }
23
+
3
24
  /* `hidden` hides, whatever display a class gives the element (the tooltip's `display: flex` kept
4
25
  it on screen after it was hidden) */
5
26
  [hidden] { display: none !important; }
6
27
 
7
28
  :root {
29
+ /* the camera's easing (web/src/canvas.ts, animate), for what moves with it */
30
+ --art-ease: cubic-bezier(0.65, 0, 0.35, 1);
8
31
  --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", "Segoe UI", Roboto, sans-serif;
9
32
  --mono: "SF Mono", "JetBrains Mono", Menlo, Consolas, monospace;
10
33
 
@@ -189,6 +212,88 @@ button { font: inherit; color: inherit; cursor: pointer; }
189
212
  #canvas { position: fixed; inset: 0; width: 100%; height: 100%; display: block; cursor: grab; touch-action: none; }
190
213
  #labels { position: fixed; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; }
191
214
  #cards { position: fixed; inset: 0; pointer-events: none; overflow: hidden; }
215
+ /* a finger held on the board opens its menu there, under the finger (web/src/canvas.ts): iOS would
216
+ select the text it then finds under it, or offer the canvas as an image */
217
+ #canvas, #cards, #ctxmenu { -webkit-user-select: none; user-select: none; -webkit-touch-callout: none; }
218
+ #ctxmenu input, #ctxmenu textarea { -webkit-user-select: text; user-select: text; }
219
+ /* the linear form, over the board and its notes, under the panels (web/src/linear.ts) */
220
+ #linear { position: fixed; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; z-index: 5; }
221
+ #linear[hidden] { display: none; }
222
+ /* the artwork mode (web/src/artframe.ts): the board is the picture, the frame over it a viewfinder,
223
+ everything around it shaded; the board under it takes the pointer */
224
+ #art { position: fixed; inset: 0; z-index: 30; pointer-events: none; }
225
+ #art[hidden] { display: none; }
226
+ .art-frame { position: absolute; box-shadow: 0 0 0 200vmax rgba(0, 0, 0, 0.45); outline: 2px solid var(--accent); border-radius: 2px; }
227
+ /* over the board within the frame, its title and foot as the files have them (web/src/artframe.ts,
228
+ renderInk); under it, a style's ground (renderSky), before the board's canvas in the page */
229
+ .art-ink { position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; }
230
+ #art-sky { position: fixed; display: block; pointer-events: none; }
231
+ #art-sky[hidden] { display: none; }
232
+ /* the mode coming and going (web/src/artframe.ts, start and end; ExportSpec decision 19): as the linear
233
+ form, over 480 ms — the frame closing in from the whole screen as its shade comes, or opening out as
234
+ it goes; the panel sliding in or away; the board dipping to its ground while its look changes; the
235
+ title, the foot and the strips beside the frame once it is still */
236
+ body.art-moving .art-frame { transition: left 480ms var(--art-ease), top 480ms var(--art-ease), width 480ms var(--art-ease), height 480ms var(--art-ease), box-shadow 480ms ease, outline-color 480ms ease; }
237
+ #art.art-away .art-frame { box-shadow: 0 0 0 200vmax rgba(0, 0, 0, 0); outline-color: transparent; }
238
+ .art-ink, .art-head, .art-tail { transition: opacity 200ms ease; }
239
+ body.art-moving .art-ink, body.art-moving .art-head, body.art-moving .art-tail, #art.art-away .art-head, #art.art-away .art-tail { opacity: 0; }
240
+ #art-panel { transition: opacity 480ms var(--art-ease), transform 480ms var(--art-ease); }
241
+ #art-panel.art-away { opacity: 0; transform: translateX(24px); }
242
+ body.art-moving { transition: background-color 180ms ease; }
243
+ body.art-moving #canvas, body.art-moving #labels, body.art-moving #cards, body.art-moving #stickies { transition: opacity 180ms ease; }
244
+ body.art-moving #art-sky { transition: opacity 180ms ease, left 480ms var(--art-ease), top 480ms var(--art-ease), width 480ms var(--art-ease), height 480ms var(--art-ease); }
245
+ body.art-dip #canvas, body.art-dip #labels, body.art-dip #cards, body.art-dip #stickies, body.art-dip #art-sky { opacity: 0; }
246
+ body[data-art-style]:not([data-art-bg]) { background: #06080e; }
247
+ .art-size output { margin-left: 6px; color: var(--text-2); font-variant-numeric: tabular-nums; }
248
+ .art-size input[type="range"] { width: 100%; }
249
+ /* the artwork's background on the board: none is shown as a checkerboard; its notes hidden */
250
+ body[data-art-bg="transparent"] { background: repeating-conic-gradient(rgba(128, 128, 128, 0.2) 0 25%, transparent 0 50%) 0 0 / 16px 16px, var(--canvas); }
251
+ body.art-nonotes #stickies { display: none; }
252
+ #art-panel { position: fixed; top: 76px; right: 16px; z-index: 41; width: 300px; max-height: calc(100vh - 100px); overflow: auto; padding-bottom: 0; }
253
+ /* the panel's sections, each under a small heading; its buttons kept in view at its foot */
254
+ .art-secs { display: grid; gap: 14px; margin-top: 10px; }
255
+ .art-sec { border-top: 1px solid var(--line); padding-top: 10px; }
256
+ .art-sec-h { margin: 0 0 8px; font-size: 11px; font-weight: 600; letter-spacing: 0.08em; text-transform: uppercase; color: var(--text-2); }
257
+ .art-save { position: sticky; bottom: 0; margin: 4px -18px 0; padding: 12px 18px 14px; background: var(--surface-solid); border-top: 1px solid var(--line); }
258
+ /* in the shade beside the frame: the title's strip above its top left, the source's switch below its
259
+ bottom right (web/src/artframe.ts) */
260
+ .art-head, .art-tail { position: absolute; pointer-events: auto; border-radius: 10px; font-size: 12px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12); }
261
+ .art-head { transform: translateY(-100%); display: flex; flex-wrap: wrap; align-items: flex-end; gap: 6px 10px; padding: 7px 10px; }
262
+ .art-head-field input[type="text"] { width: var(--w, 10em); max-width: 100%; }
263
+ .art-head .art-size input[type="range"], .art-tail .art-size input[type="range"] { width: 9em; padding: 0; border: 0; background: none; }
264
+ .art-head [data-art-own][hidden] { display: none; }
265
+ .art-tail { display: flex; align-items: center; gap: 14px; padding: 4px 10px; }
266
+ .art-tail .art-size { flex-direction: row; align-items: center; gap: 8px; }
267
+ .art-tail .ex-check { margin: 0; }
268
+ #art-panel[hidden] { display: none; }
269
+ .art-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px 10px; }
270
+ .art-grid .ex-wide, .art-grid .art-note, .art-grid .ex-checks { grid-column: 1 / -1; }
271
+ .art-note { font-size: 11.5px; line-height: 1.4; }
272
+ /* names and targets by selection (ExportSpec decision 19): a filter, All and None, the lines ticked
273
+ or not, scrolled within the panel */
274
+ .art-pick { grid-column: 1 / -1; display: flex; flex-direction: column; gap: 5px; }
275
+ .art-pick-head { display: flex; align-items: center; gap: 5px; font-size: 11.5px; }
276
+ .art-pick-head > span { flex: 1; }
277
+ .art-pick-head .rv-btn { padding: 3px 8px; font-size: 11.5px; white-space: nowrap; }
278
+ .art-filter {
279
+ flex: 1; min-width: 0; font: 12.5px var(--font); color: var(--text); background: var(--surface-solid);
280
+ border: 1px solid var(--line); border-radius: 7px; padding: 4px 7px;
281
+ }
282
+ .art-rows { max-height: 176px; overflow: auto; padding: 3px 0; border: 1px solid var(--line); border-radius: 7px; background: var(--surface-solid); }
283
+ .art-row { display: flex; align-items: center; gap: 6px; padding: 1px 8px; font-size: 12px; line-height: 1.5; cursor: pointer; white-space: nowrap; }
284
+ .art-row:hover { background: var(--code-bg); }
285
+ .art-row input { margin: 0; flex: none; }
286
+ .art-row span { overflow: hidden; text-overflow: ellipsis; }
287
+ .art-rows-h { padding: 4px 8px 1px; font-size: 10.5px; color: var(--text-3); }
288
+ .ex-tabs { margin: 2px 0 4px; }
289
+ /* in the artwork mode, the panels are put away: the frame has the room */
290
+ body.framing #side, body.framing #panel, body.framing #selbar, body.framing #lensbar, body.framing #tm-kept,
291
+ body.framing #viewbar, body.framing #foot, body.framing #zoom, body.framing #tabs, body.framing #legend { visibility: hidden; opacity: 0; }
292
+ /* of the bar, Share and Search alone: the rest out of reach (web/src/main.ts, drawLook) */
293
+ body.framing #bar [inert] { opacity: 0.32; }
294
+ /* both fading as the mode comes and goes */
295
+ body.art-moving :is(#side, #panel, #selbar, #lensbar, #tm-kept, #viewbar, #foot, #zoom, #tabs, #legend) { transition: opacity 240ms ease, visibility 240ms; }
296
+ body.art-moving :is(#bar .brand, #bar .library-wrap, #export-btn, #activity-btn, #bar .peers-wrap, #bar .who-wrap, #help-btn, #bar .more-wrap) { transition: opacity 240ms ease; }
192
297
 
193
298
  .glass {
194
299
  background: var(--surface);
@@ -333,6 +438,12 @@ button { font: inherit; color: inherit; cursor: pointer; }
333
438
  }
334
439
  .sw-review .rv-text { min-height: calc(5 * 1.45em + 12px); }
335
440
  .rv-actions { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
441
+ /* whom the opinion is for (ReviewSpec decision 8): a name beside the buttons; while it is another
442
+ person's, a coloured line over the box says so */
443
+ .rv-for { display: inline-flex; align-items: center; gap: 5px; margin-left: auto; font-size: 11.5px; color: var(--text-2); white-space: nowrap; }
444
+ .rv-for-in { width: 9em; font: 12px var(--font); color: var(--text); background: var(--surface-solid); border: 1px solid var(--line); border-radius: 6px; padding: 3px 6px; }
445
+ .rv-for-on { margin: 0 0 6px; padding: 5px 9px; border-radius: 7px; font-size: 12px; color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, transparent); border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent); }
446
+ .rv-for-on[hidden] { display: none; }
336
447
  .rv-btn {
337
448
  border: 1px solid var(--line); background: var(--surface-solid); color: var(--text); font: 12px var(--font);
338
449
  padding: 4px 10px; border-radius: 7px; cursor: pointer;
@@ -526,11 +637,11 @@ button.o-trust:hover { color: var(--accent); background: var(--code-bg); }
526
637
  .sw-win.moving .sw-head { cursor: grabbing; }
527
638
  .sw-head .nav { margin-right: 2px; }
528
639
  /* under the title bar, why the pairing opened no reference beside it (AttachmentSpec decision 8) */
529
- .sw-pair { padding: 5px 12px; border-bottom: 1px solid var(--line); font-size: 12px; color: var(--text-2); }
530
- /* a narrow window shortens the namespace and the place, never its buttons */
531
- .sw-name { font: 600 15px var(--font); flex: none; }
532
- .sw-ns { color: var(--text-2); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40vw; min-width: 0; }
533
- .sw-loc { color: var(--text-3); font: 11.5px var(--mono); margin-left: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
640
+ .sw-pair { padding: 5px 12px; border-bottom: 1px solid var(--line); font-size: 12px; color: var(--text-2); overflow-wrap: anywhere; }
641
+ /* a narrow window shortens the namespace and the place, then the name, never its buttons */
642
+ .sw-name { font: 600 15px var(--font); flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
643
+ .sw-ns { color: var(--text-2); font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 40vw; min-width: 0; flex-shrink: 1000; }
644
+ .sw-loc { color: var(--text-3); font: 11.5px var(--mono); margin-left: auto; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; flex-shrink: 1000; }
534
645
  .sw-acts { display: inline-flex; align-items: center; gap: 2px; align-self: center; flex: none; }
535
646
  .sw-acts .icon, .sw-pill .icon {
536
647
  display: inline-grid; place-items: center; padding: 0; border: 0; background: transparent; color: var(--text-2);
@@ -818,12 +929,10 @@ button.o-trust:hover { color: var(--accent); background: var(--code-bg); }
818
929
  .modal-title { font-weight: 600; font-size: 15px; }
819
930
  .modal-text { color: var(--text-2); font-size: 12.5px; line-height: 1.45; }
820
931
  /* Export (web/src/exportmodal.ts): which view, what its document holds, its title and author line,
821
- the common settings, then the job */
822
- #export-modal {
823
- position: fixed; inset: 0; z-index: 40; display: flex; align-items: center; justify-content: center;
824
- background: rgba(0, 0, 0, 0.22);
825
- }
826
- .ex-box { width: min(580px, calc(100vw - 32px)); max-height: 90vh; overflow: auto; outline: none; }
932
+ the common settings, then the job. At the right, over nothing but the panel: the board is drawn
933
+ in the linear form beside it while it is open (ExportSpec decision 18) */
934
+ #export-modal { position: fixed; top: 106px; right: 12px; z-index: 40; pointer-events: none; }
935
+ .ex-box { width: min(480px, calc(100vw - 32px)); max-height: calc(100vh - 130px); overflow: auto; outline: none; pointer-events: auto; }
827
936
  .ex-head { display: flex; align-items: center; justify-content: space-between; }
828
937
  .ex-sec { display: flex; flex-direction: column; gap: 6px; }
829
938
  .ex-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px 10px; }
@@ -1687,7 +1796,7 @@ details.lf-archived > summary { cursor: pointer; }
1687
1796
  word-break: break-word;
1688
1797
  }
1689
1798
  .p-ns { color: var(--text-2); font-size: 12.5px; word-break: break-all; }
1690
- .p-mod { color: var(--text-3); font-size: 12px; margin: 6px 0 12px; display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; }
1799
+ .p-mod { color: var(--text-3); font-size: 12px; margin: 6px 0 12px; display: flex; gap: 10px; align-items: baseline; flex-wrap: wrap; overflow-wrap: anywhere; }
1691
1800
  .p-gen { color: var(--text-2); font-size: 12px; margin: -6px 0 12px; line-height: 1.45; }
1692
1801
  .sw-print { margin-top: 14px; border-top: 1px solid var(--line); padding-top: 10px; }
1693
1802
  .sw-print-h { color: var(--text-3); font-size: 12px; margin-bottom: 6px; font-family: var(--mono, ui-monospace, monospace); }
@@ -1864,6 +1973,7 @@ a.link:hover { text-decoration: underline; }
1864
1973
  /* the reader's "Open in editor" template, under the keys */
1865
1974
  .help-editor, .help-about { display: flex; flex-direction: column; gap: 6px; margin-top: 16px; padding-top: 12px; border-top: 1px solid var(--line); }
1866
1975
  .help-note { color: var(--text-2); font-size: 12px; line-height: 1.45; }
1976
+ .help-note a { color: var(--accent); }
1867
1977
  .help-note code { font: 11px var(--mono); background: var(--code-bg); padding: 1px 4px; border-radius: 4px; overflow-wrap: anywhere; }
1868
1978
  .help-input {
1869
1979
  font: 12.5px var(--mono); color: var(--text); background: var(--surface-solid); border: 1px solid var(--line);
@@ -2310,3 +2420,136 @@ span.tm-row { color: var(--text-2); cursor: default; }
2310
2420
  @media (max-width: 1350px) { .tm-growth, .tm-w { display: none; } }
2311
2421
  @media (max-width: 1150px) { .tm-view { display: none; } .tm-n { padding: 0 6px; } }
2312
2422
  @media (max-width: 900px) { .tm-btn[data-tm-save] { display: none; } }
2423
+
2424
+ /* ---------------------------------------------------------------- a phone (PhoneSpec)
2425
+
2426
+ At most 640 px wide: a page for reading and reviewing (web/src/phone.ts). The bar keeps Back, the
2427
+ project's name, search and ⋯, which holds the modes and the rest; the side panel is a drawer from
2428
+ ☰; the panel is a sheet from the bottom, the board above it. */
2429
+ .more-wrap, .side-btn, .side-back { display: none; }
2430
+ @media (max-width: 640px) {
2431
+ /* over the sheet, which stops under it at its tallest, so that what opens from the bar (⋯, the
2432
+ search's results, Share) opens over the sheet too */
2433
+ #bar { top: 8px; left: 8px; right: 8px; height: 44px; gap: 6px; padding: 0 6px 0 4px; z-index: 31; }
2434
+ #bar .right { gap: 4px; }
2435
+ #nav-fwd, .brand .logo { display: none; }
2436
+ .brand { align-items: center; gap: 6px; }
2437
+ .brand .project { display: inline; font-weight: 600; font-size: 15px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2438
+ #share-btn, #library-btn, #export-btn, #activity-btn, #who, #help-btn { display: none !important; }
2439
+ .more-wrap { display: block; position: relative; }
2440
+ #more-btn { width: 38px; height: 34px; }
2441
+ #more-badge { position: absolute; top: -5px; right: -5px; }
2442
+ #more-pop { position: fixed; top: 58px; right: 8px; width: min(260px, calc(100vw - 16px)); z-index: 46; border-radius: 14px; padding: 6px; display: flex; flex-direction: column; background: var(--surface-solid); box-shadow: 0 0 0 0.5px var(--line), var(--shadow-lg); }
2443
+ #more-pop[hidden] { display: none; }
2444
+ .more-item { display: flex; align-items: center; gap: 10px; min-height: 44px; padding: 0 12px; border: 0; border-radius: 10px; background: none; color: var(--text); font: 15px var(--font); text-align: left; }
2445
+ .more-item:active { background: var(--code-bg); }
2446
+ .more-item svg { flex: none; width: 18px; height: 18px; color: var(--text-2); fill: none; stroke: currentColor; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; }
2447
+ .more-item .act-badge { position: static; margin-left: auto; }
2448
+ /* the modes, at the top of ⋯: each with what a tap selects under its name, the one in use ticked */
2449
+ #more-pop { max-height: calc(100dvh - 70px); overflow-y: auto; }
2450
+ .more-h { padding: 6px 12px 2px; font-size: 12px; font-weight: 600; color: var(--text-2); }
2451
+ .more-sub { padding: 6px 12px 0 36px; font-size: 12px; color: var(--text-2); }
2452
+ .more-mode { gap: 6px; padding: 4px 12px; }
2453
+ .more-mode.step { padding-left: 28px; }
2454
+ .more-mode.on { color: var(--accent); }
2455
+ .more-tick { flex: none; width: 16px; text-align: center; font-weight: 700; }
2456
+ .more-l { display: flex; flex-direction: column; min-width: 0; line-height: 1.25; }
2457
+ .more-l small { font-size: 12px; color: var(--text-2); }
2458
+ .more-sep { height: 1px; margin: 6px 4px; background: var(--line); }
2459
+ /* search: a narrow field that takes the whole bar while it is used */
2460
+ #search { width: 92px; height: 34px; padding: 0 10px; }
2461
+ .search-wrap:focus-within { position: fixed; top: 8px; left: 8px; right: 8px; z-index: 45; }
2462
+ .search-wrap:focus-within #search { width: 100%; height: 44px; font-size: 16px; box-shadow: var(--shadow-lg); }
2463
+ #results { position: fixed; top: 58px; left: 8px; right: 8px; width: auto; max-height: calc(100dvh - 70px); }
2464
+ /* every popover of the bar and the view bar: a sheet under the bar, the screen's width */
2465
+ #share-pop, #library-pop, #who-menu, #peer-menu, #views-menu, #focus-menu {
2466
+ position: fixed !important; top: 58px !important; left: 8px !important; right: 8px !important; bottom: auto !important;
2467
+ width: auto !important; max-width: none !important; max-height: calc(100dvh - 70px); overflow: auto;
2468
+ }
2469
+ #viewbar { top: 58px; left: 8px; right: 8px; gap: 6px; }
2470
+ .side-btn { display: inline-flex; flex: none; width: 36px; justify-content: center; padding: 0; }
2471
+ #tabs { flex: 1 1 auto; min-width: 0; overflow-x: auto; scrollbar-width: none; }
2472
+ #tabs::-webkit-scrollbar { display: none; }
2473
+ .vb-gap, .light-l { display: none; }
2474
+ .focus-btn { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2475
+ /* the modes are in ⋯ */
2476
+ #lightup { display: none; }
2477
+ #zoom { right: 8px; bottom: calc(8px + env(safe-area-inset-bottom)); }
2478
+ /* ⋯ opens upwards from its box, at the right (the box's blur makes it the popover's frame) */
2479
+ #eyes-pop { right: 0; bottom: calc(100% + 8px); width: min(280px, calc(100vw - 16px)); min-width: 0; background: var(--surface-solid); }
2480
+ #zoom #eyes-pop button.eye { min-height: 40px; }
2481
+ /* no finger moves anything: the lock means nothing here */
2482
+ #lock-btn { display: none !important; }
2483
+ /* the help: gestures, as large as the screen allows */
2484
+ .help-box { width: calc(100vw - 16px); max-height: calc(100dvh - 32px); background: var(--surface-solid); }
2485
+ /* no keyboard: no keys beside the menus' items */
2486
+ #eyes-pop kbd { display: none; }
2487
+ .help-touch .help-row { grid-template-columns: 116px minmax(0, 1fr); font-size: 14px; padding: 5px 0; }
2488
+ .help-touch .help-g { color: var(--text); font-weight: 600; }
2489
+ #zoom #zoom-in, #zoom #zoom-out { display: none; }
2490
+ #foot { left: 8px; bottom: calc(8px + env(safe-area-inset-bottom)); }
2491
+ /* under the view bar, the review strip, then the selection bar (--strip-h: main.ts) */
2492
+ #lensbar, #tm-kept { top: 100px; left: 8px; right: 8px; transform: none; max-width: none; }
2493
+ #selbar { top: calc(100px + var(--strip-h, 0px)); left: 8px; right: 8px; width: auto; }
2494
+ /* the panel: a sheet from the bottom, its height the reader's to drag */
2495
+ #panel {
2496
+ top: auto; left: 0; right: 0; bottom: 0; width: auto; height: var(--sheet-h, 55dvh); max-height: calc(100dvh - 60px);
2497
+ border-radius: 18px 18px 0 0; padding: 26px 16px calc(24px + env(safe-area-inset-bottom)); z-index: 30;
2498
+ background: var(--surface-solid); box-shadow: 0 -1px 0 var(--line), 0 -8px 30px rgba(0, 0, 0, 0.12);
2499
+ transform: translateY(105%); transition: transform 0.32s cubic-bezier(0.2, 0.9, 0.25, 1), height 0.2s;
2500
+ }
2501
+ #panel.open { transform: none; }
2502
+ #panel::before { content: ""; position: sticky; top: -18px; display: block; width: 40px; height: 5px; margin: -18px auto 13px; border-radius: 3px; background: var(--text-3); opacity: 0.6; }
2503
+ /* the side panel: a drawer from the left, over a shade */
2504
+ body.side-open #side { display: flex; position: fixed; top: 0; left: 0; bottom: 0; width: min(320px, 86vw); border-radius: 0 16px 16px 0; z-index: 47; padding-top: env(safe-area-inset-top); background: var(--surface-solid); }
2505
+ body.side-open::after { content: ""; position: fixed; inset: 0; z-index: 46; background: rgba(0, 0, 0, 0.3); }
2506
+ /* ⚙ Board and Trusted: in the drawer's place, over it, with a way back */
2507
+ body.side-open .side-pop {
2508
+ left: 0 !important; top: 0 !important; bottom: 0 !important; right: auto !important; width: min(320px, 86vw) !important;
2509
+ height: auto !important; max-height: none !important; border-radius: 0 16px 16px 0; z-index: 48;
2510
+ background: var(--surface-solid); padding-top: calc(8px + env(safe-area-inset-top));
2511
+ }
2512
+ body.side-open .side-back {
2513
+ display: flex; align-items: center; gap: 4px; min-height: 40px; margin: 0 0 6px -6px; padding: 0 6px; border: 0;
2514
+ background: none; color: var(--accent); font: 600 15px var(--font);
2515
+ }
2516
+ .side-back svg { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
2517
+ /* a field under 16 px makes iOS zoom the whole page into it */
2518
+ #panel textarea, #panel input, #share-pop input, #share-pop select, #who-menu input, #modal input, .sh-field input, .sh-field select { font-size: 16px; }
2519
+ /* nothing slides sideways: the page is the screen */
2520
+ html, body { overflow: hidden; overscroll-behavior: none; }
2521
+ #panel { overflow-x: hidden; overscroll-behavior: contain; }
2522
+ /* a source window: the whole screen, its title bar its name and a close */
2523
+ .sw-win:not(.collapsed) {
2524
+ left: 0 !important; top: 0 !important; right: 0 !important; bottom: 0 !important; width: auto !important; height: auto !important;
2525
+ max-width: none !important; max-height: none !important; min-width: 0 !important; min-height: 0 !important;
2526
+ border-radius: 0 !important; padding-top: env(safe-area-inset-top); padding-bottom: env(safe-area-inset-bottom); z-index: 48;
2527
+ }
2528
+ .sw-win .fl-rz, .sw-ns, .sw-loc, .sw-acts [data-sw-collapse], .sw-pin { display: none !important; }
2529
+ /* the menu: no keys beside its items */
2530
+ #ctxmenu .cm-key { display: none; }
2531
+ /* the tree in the drawer: a row a finger can hit, an arrow wide enough to open or fold a level;
2532
+ hiding a level arranges the board, which a phone does not */
2533
+ #outline .o-row { height: 40px; }
2534
+ #outline .o-row .tw { width: 32px; height: 40px; display: inline-flex; align-items: center; justify-content: center; font-size: 12px; margin-left: -8px; }
2535
+ #outline .o-hide { display: none; }
2536
+ /* the Time Machine: its line, the build and a way out; keeping a build or saving it as a view
2537
+ arranges the board, which a phone does not */
2538
+ body.tm .focus-wrap { display: none; }
2539
+ #tmbar { flex: 1 1 auto; gap: 6px; padding: 0 2px 0 8px; }
2540
+ #tmbar .tm-acts, #tmbar .tm-sum, #tmbar .tm-growth, #tmbar .tm-latest { display: none !important; }
2541
+ #tmbar .tm-x { flex: none; width: 36px; height: 34px; font-size: 16px; }
2542
+ .sw-head .sw-close { width: 40px; height: 40px; font-size: 20px; }
2543
+ /* a long name goes on over a second line, its end (what tells it from its neighbours) in sight;
2544
+ the buttons keep their place at the right */
2545
+ .sw-head { align-items: center; }
2546
+ .sw-name { flex: 1 1 auto; white-space: normal; overflow-wrap: anywhere; }
2547
+ .pw-win:not(.collapsed) { left: 4px !important; right: 4px !important; width: auto !important; }
2548
+ /* Activity and the lost-and-found: the whole screen, from the right */
2549
+ #activity, #lostfound {
2550
+ top: 0; left: 0; right: 0; bottom: 0; width: auto; border-radius: 0; z-index: 48;
2551
+ padding: calc(10px + env(safe-area-inset-top)) 16px calc(24px + env(safe-area-inset-bottom));
2552
+ background: var(--surface-solid); transform: translateX(100%);
2553
+ }
2554
+ .act-head .icon { width: 40px; height: 40px; font-size: 20px; }
2555
+ }
@@ -85,7 +85,7 @@ parameters and answer.
85
85
  | [following](following.md) | where people are, your own view, links to it, asking to follow, what people see |
86
86
  | [views](views.md) | the query language, views, places on the board, pins and undo |
87
87
  | [library](library.md) | papers and images, their pages, links to them |
88
- | [export](export.md) | a view's document: its model, typesetting, its files |
88
+ | [export](export.md) | a view's document: its model, typesetting, its files; a picture (artwork) |
89
89
  | [history](history.md) | the timeline, the builds kept, diffs, renames, the lost-and-found |
90
90
  | [talking](talking.md) | your inbox, replies, your status line, your state, the conversation |
91
91
  | [mcp](mcp.md) | the MCP server: installing it, its tools and their actions, `wait_for_message` |
@@ -1,7 +1,7 @@
1
1
  # Exported documents
2
2
 
3
- A view's document, read as JSON or typeset as a PDF. Any level: it only reads. MCP: `export`
4
- (`model`, `typeset`, `job`, `file`).
3
+ A view's document, read as JSON or typeset as a PDF; and a picture of the board ([Artwork](#artwork)).
4
+ Any level: it only reads. MCP: `export` (`model`, `typeset`, `job`, `file`, `artwork`).
5
5
 
6
6
  ## What it holds
7
7
 
@@ -118,3 +118,42 @@ kept). 503: no XeLaTeX.
118
118
  typeset it again (`.tex`, style file, fonts). **409** with `Retry-After: 2` while running, 409 if
119
119
  failed, 404 if unknown or gone from the cache. E.g.
120
120
  `curl -fsSK ~/.visualproof/curl.cfg "$VP/api/agent/export/file?id=<id>" -o doc.pdf`.
121
+
122
+ ## Artwork
123
+
124
+ `POST /api/agent/artwork {recipe, save?}`: a link that opens the board as a picture in the artwork mode, as a **recipe** says, in words that
125
+ hold on any project: one recipe makes the same kind of picture of every codebase. The page that
126
+ opens it draws the picture and, with `save` (`png`, `svg`, `png,svg`), saves the files there once
127
+ the board is still; nothing comes back to the server. The link holds no key (a script enters with
128
+ it: `scripts/artwork-batch.mjs` in VisualProof's repository).
129
+
130
+ ```json
131
+ { "recipe": { "size": "a2", "orientation": "portrait", "theme": "light", "border": true,
132
+ "names": { "clusters": 1 }, "targets": ["Lib.mainTheorem"],
133
+ "selection": { "name": "Lib.mainTheorem", "mode": "tcb" }, "keys": false, "notes": false,
134
+ "source": true, "title": "Lib", "subtitle": "One line about the work" }, "save": "png,svg" }
135
+ ```
136
+
137
+ Every field is optional; left out, as a new artwork has it.
138
+
139
+ - `size`: `hd`, `4k` (default), `square`, `a3`, `a2` (300 dpi), or `{w, h}`; `orientation`;
140
+ `style` (`board`, `cells`, `stars`); `theme` (`light`, `dark`), `transparent`; `colouring`
141
+ (`kind`, `status`); `border`; `title`, `subtitle` (null: the project's own; `""`: none),
142
+ `titleSize`, `footSize` (hundredths of the height); `source` (git source and commit).
143
+ - `names`: `board`, `clusters`, `none`, or a rule `{clusters, minShare, targets, decls, omit,
144
+ from}`: `clusters` a depth (0: the top level) or keys; a depth names the outside world and the
145
+ clusters holding at least `minShare` of the project (default 0.01), each written only where it
146
+ covers no name already written (`ruled`), never over another; keys, `decls` (full names) and
147
+ `targets` (default true: the marked targets' names) written wherever they fall; `omit` keys or
148
+ names; `from` `none` (default) or `board`.
149
+ - `targets`: `all` (default), `none`, `faded`, or a list of targets marked alone. `keys`, `notes`:
150
+ default true. `edges`: `all` (under a selection, the rest faint), `selection`, `none`.
151
+ - `selection`: `{name, mode}` (`mode` as `POST view`'s; default `tcb`), lit while `lit` (default
152
+ true). `view`: a view's id; left out, the Project.
153
+
154
+ Answer: `{ok, link, length, size, names: {clusters, decls, ruled}, targets, selection, left, note}`;
155
+ `left`: names or targets beyond what an artwork carries, left out. Saving both files, the page says
156
+ the browser may ask to allow several downloads. 400: a field unknown or wrong; 404: a name, target,
157
+ cluster or view the recipe names that the board has not (`problems`; a target of the review the
158
+ graph no longer has is left out unsaid).
159
+
@@ -30,7 +30,7 @@ MCP: `companion`.
30
30
  | `source`, `sourceCollapsed` | the source window following their selection: open; collapsed to its pill |
31
31
  | `pinned` | pinned source windows, first pinned first, `{declaration, collapsed}`; followers mirror them (an agent pins none) |
32
32
  | `history` | `{snapshot, built}`: the build they browse in the Time Machine |
33
- | `activity`, `lostAndFound`, `colouring`, `edges`, `proofEdges` | what else followers take, where their page says it (a page from before, and an agent, say none): `activity` `{open, kind, who, mine?}` (kind: all, review, build, change; who: all, agents, people; `mine`: the person whose targets it keeps to); `lostAndFound` `{open}`; `colouring` `kind` or `status`; edges drawn or not |
33
+ | `activity`, `lostAndFound`, `colouring`, `edges` | what else followers take, where their page says it (a page from before, and an agent, say none): `activity` `{open, kind, who, mine?}` (kind: all, review, build, change; who: all, agents, people; `mine`: the person whose targets it keeps to); `lostAndFound` `{open}`; `colouring` `kind` or `status`; edges drawn or not |
34
34
  | `inWords` | the place in words; of the row above, only what differs from a fresh page |
35
35
 
36
36
  `wait=<s>` (≤ 60) waits until someone comes, goes, goes idle or moves (view, selection,
@@ -80,7 +80,7 @@ Fields go over your own place (none: your place): every `POST view` field but `l
80
80
  | `history` | a snapshot id: the Time Machine at that build; `null`: none |
81
81
  | `region` | `[x, y, w, h]`: centre, width, height in the board's world (a top-level cluster's place is its `at` in `views/view`, a nested one's its parents' added up), fitted to the opener's screen. Left out, framed as for any link |
82
82
  | `activity` | `{"open": true, "kind": "review", "who": "people"}`, values as in `companion` |
83
- | `lostFound`, `scheme`, `edges`, `proofEdges` | the lost-and-found open; the colouring, `kind` or `status`; edges, proof edges drawn |
83
+ | `lostFound`, `scheme`, `edges` | the lost-and-found open; the colouring, `kind` or `status`; edges drawn |
84
84
  | `exact` | `false`: the view's address alone (Share's switch off) |
85
85
 
86
86
  ```json
@@ -20,7 +20,8 @@ name bare).
20
20
  Every event with a `by` has `agent`. `by` is null on a `view`, `note` or `undo` event done without a
21
21
  name (a private reader's; their opinions, targets, key marks and trust keep their name).
22
22
 
23
- - Review's logs (no `id`): `opinion`, `target`, `key` (as marked now: `target`, `name`), `file`
23
+ - Review's logs (no `id`): `opinion` (`recordedBy`: who typed it for `by`, at one screen), `target`,
24
+ `key` (as marked now: `target`, `name`), `file`
24
25
  (`what` added, edited, archived or restored; `sha`, `title` then, `wasTitle` before an edit),
25
26
  `link` (`what` made, edited or removed; `id`, `subject`, `label`, `file`; an edit's `changed`
26
27
  (of `subject`, `label`, `place`), `wasSubject`, `wasLabel`, and `auto` if VisualProof carried it
@@ -40,7 +40,7 @@ Each wraps the endpoints of a topic, which says what their answers hold. `*`: th
40
40
  | `library` | `list`, `file`, `text`, `search`, `page`, `clip`, `save`; `add_url`, `add_file`, `edit`, `archive` (advise) | `library/…`, [library](library.md) |
41
41
  | `links` | `list`; `make`, `edit`, `remove` (advise) | `GET links`, `POST link`, `link/edit`, `unlink`, [library](library.md) |
42
42
  | `history` | `snapshots`, `diff`, `moves`, `last`, `lost`, `archived`; `attach`, `unmove`, `archive` (full) | `history/…`, [history](history.md) |
43
- | `export` | `model`, `typeset`, `job`, `file` | `export`, `export/job`, `export/file`, [export](export.md) |
43
+ | `export` | `model`, `typeset`, `job`, `file`, `artwork` | `export`, `export/job`, `export/file`, `artwork`, [export](export.md) |
44
44
 
45
45
  Paths are under `/api/agent/`. `GET /api/agent/history/graph` has no tool (it is large: ask it over
46
46
  HTTP when you need a past build's whole graph).
@@ -36,7 +36,8 @@ Everything about one declaration; here `include=print` while Lean loads:
36
36
  - `axioms`: all its proof and definition reach; `nonStandardAxioms`: beyond the standard three
37
37
  (`sorryAx`). `review`: `opinions` (people's latest) and `advice` (agents', `yours: true`), one
38
38
  each; `onPresentVersion: false`: on an earlier version; `verified: true`: from someone invited by
39
- a personal link, `by` their name now; `trustedBy`: covering prefixes, or `["standard axiom"]`.
39
+ a personal link, `by` their name now; `recordedBy`: typed for `by` by that person, at one screen
40
+ (theirs all the same); `trustedBy`: covering prefixes, or `["standard axiom"]`.
40
41
  - `editedSinceCompiled`: file differs from what was compiled (null outside the project); `keyFor`,
41
42
  `inTcbOf`: targets it is key for, whose TCB holds it; `usesDirect`, `usedBy.direct`: what it
42
43
  refers to by name, what refers to it (`uses`, `users` list them); `usedBy.all`: all reaching it,