proctolog 0.1.1 → 0.2.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -3
  3. data/README.md +15 -4
  4. data/app/controllers/proctolog/application_controller.rb +1 -1
  5. data/app/controllers/proctolog/bursts_controller.rb +1 -1
  6. data/app/controllers/proctolog/choices_controller.rb +3 -1
  7. data/app/controllers/proctolog/decisions_controller.rb +1 -1
  8. data/app/controllers/proctolog/notes_controller.rb +2 -2
  9. data/app/controllers/proctolog/prompts_controller.rb +1 -1
  10. data/app/controllers/proctolog/pull_requests_controller.rb +2 -4
  11. data/app/controllers/proctolog/review_data_controller.rb +3 -1
  12. data/app/controllers/proctolog/shares_controller.rb +27 -0
  13. data/app/helpers/proctolog/decisions_helper.rb +6 -2
  14. data/app/helpers/proctolog/desk_helper.rb +48 -8
  15. data/app/helpers/proctolog/rounds_helper.rb +4 -2
  16. data/app/helpers/proctolog/shares_helper.rb +11 -0
  17. data/app/helpers/proctolog/standings_helper.rb +5 -4
  18. data/app/models/proctolog/burst.rb +17 -8
  19. data/app/models/proctolog/choices.rb +8 -0
  20. data/app/models/proctolog/doctor.rb +9 -1
  21. data/app/models/proctolog/github.rb +1 -1
  22. data/app/models/proctolog/marks.rb +1 -1
  23. data/app/models/proctolog/note.rb +4 -0
  24. data/app/models/proctolog/owner.rb +3 -0
  25. data/app/models/proctolog/pick.rb +12 -0
  26. data/app/models/proctolog/rounds.rb +37 -0
  27. data/app/models/proctolog/share/ngrok.rb +120 -0
  28. data/app/models/proctolog/share/settings.rb +23 -0
  29. data/app/models/proctolog/share/tailscale.rb +103 -0
  30. data/app/models/proctolog/share.rb +101 -0
  31. data/app/models/proctolog/standing.rb +24 -7
  32. data/app/views/proctolog/bursts/_line.html.erb +2 -2
  33. data/app/views/proctolog/bursts/_side.html.erb +6 -5
  34. data/app/views/proctolog/bursts/_side_stop.html.erb +4 -4
  35. data/app/views/proctolog/notes/_card.html.erb +7 -3
  36. data/app/views/proctolog/open_pull_requests/_side.html.erb +4 -3
  37. data/app/views/proctolog/open_pull_requests/_start.html.erb +8 -0
  38. data/app/views/proctolog/open_pull_requests/index.html.erb +4 -5
  39. data/app/views/proctolog/open_pull_requests/start/_first_time.html.erb +3 -0
  40. data/app/views/proctolog/open_pull_requests/start/_nothing_open.html.erb +3 -0
  41. data/app/views/proctolog/open_pull_requests/start/_nothing_written.html.erb +3 -0
  42. data/app/views/proctolog/shares/_line.html.erb +5 -0
  43. data/app/views/proctolog/shares/show.html.erb +48 -0
  44. data/app/views/proctolog/shares/ways/_ngrok.html.erb +6 -0
  45. data/app/views/proctolog/shares/ways/_tailscale.html.erb +2 -0
  46. data/bin/proctolog +92 -6
  47. data/completions/_proctolog +28 -4
  48. data/config/application.rb +23 -4
  49. data/config/initializers/grouse.rb +1 -1
  50. data/config/routes.rb +3 -0
  51. data/docs/manual/bursts.md +7 -2
  52. data/docs/manual/deciding.md +4 -1
  53. data/docs/manual/handover.md +6 -0
  54. data/docs/manual/reading.md +54 -27
  55. data/docs/manual/sharing.md +77 -0
  56. data/lib/proctolog/changelog.rb +50 -0
  57. data/lib/proctolog/version.rb +1 -1
  58. data/public/asking.js +31 -4
  59. data/public/decisions.js +4 -4
  60. data/public/desk.js +10 -6
  61. data/public/keys.js +6 -0
  62. data/public/notes.js +164 -58
  63. data/public/pick.css +21 -0
  64. data/public/respond.css +6 -0
  65. data/public/respond.js +51 -7
  66. data/public/review-drafts.js +47 -9
  67. data/public/review-rounds.js +27 -3
  68. data/public/share.css +58 -0
  69. data/public/side.js +11 -20
  70. data/public/terminal.css +295 -34
  71. data/public/viewer.js +700 -173
  72. metadata +18 -1
@@ -1,10 +1,11 @@
1
- import { helping, keyOf } from "./keys.js";
1
+ import { helping, keyOf, overThePage } from "./keys.js";
2
2
  import { prNumber } from "./pr-number.js";
3
3
  import {
4
4
  announce,
5
5
  handOver,
6
6
  draftsState,
7
7
  registerRounds,
8
+ settled,
8
9
  stale,
9
10
  updateDrafts,
10
11
  } from "./review-drafts.js";
@@ -28,6 +29,25 @@ const reviewData = async () => {
28
29
  if (!response.ok) throw new Error(`Desk returned ${response.status}`);
29
30
  return response.json();
30
31
  };
32
+ // The pane's ↩, on each pull request something came back on, in the words
33
+ // the desk gives (DeskHelper#side_mark).
34
+ const markThePane = (back = {}) => {
35
+ for (const row of document.querySelectorAll("[data-side-pr]")) {
36
+ const said = back[row.dataset.sidePr];
37
+ Object.assign(row.querySelector("[data-side-mark]"), {
38
+ textContent: said ? "↩" : "",
39
+ role: said ? "img" : null,
40
+ ariaLabel: said ?? null,
41
+ });
42
+ }
43
+ };
44
+ // Once the desk has the owner's word on an answer, it has moved the notes the
45
+ // answer was to (Rounds#move_notes), and the pane is marked again from it. A
46
+ // desk that does not answer leaves the pane as the page drew it.
47
+ const followThePane = async () => {
48
+ await settled();
49
+ markThePane((await reviewData()).back);
50
+ };
31
51
  // What GitHub draws of each piece of markdown, by its words, asked once while
32
52
  // the page is open: returned work is laid out again at every change of the
33
53
  // drafts, and must not fall back to markdown each time. A piece that cannot
@@ -589,6 +609,7 @@ const mark = (item, resolution, latest, kind, text) => {
589
609
  },
590
610
  };
591
611
  });
612
+ followThePane().catch(() => {});
592
613
  selectResponse(
593
614
  visibleResponses().find((node) => node.dataset.request === next) ||
594
615
  visibleResponses()[0],
@@ -1275,6 +1296,7 @@ registerRounds({
1275
1296
  context = await reviewData();
1276
1297
  importServerBatches();
1277
1298
  importServerResponses();
1299
+ markThePane(context.back);
1278
1300
  },
1279
1301
  });
1280
1302
 
@@ -1290,6 +1312,7 @@ tools
1290
1312
  ? "Agent response imported. Confirm or reply to its existing requests."
1291
1313
  : "The desk has this answer already.",
1292
1314
  );
1315
+ followThePane().catch(() => {});
1293
1316
  } catch (error) {
1294
1317
  announce(`Response could not be imported: ${error.message}`);
1295
1318
  } finally {
@@ -1361,8 +1384,9 @@ document
1361
1384
  .querySelector("[data-enter-responses]")
1362
1385
  ?.addEventListener("click", enterResponses);
1363
1386
  document.addEventListener("focusin", (event) => {
1364
- // A picture looked at closely leaves the response marked: Esc comes back to it.
1365
- if (event.target.closest("dialog")) return;
1387
+ // A picture looked at closely, or a note written in Grouse's overlay, leaves
1388
+ // the response marked: Esc comes back to it.
1389
+ if (overThePage(event.target)) return;
1366
1390
  const node = event.target.closest("[data-request]");
1367
1391
  if (node) markResponse(node);
1368
1392
  else activeResponse?.removeAttribute("aria-current");
data/public/share.css ADDED
@@ -0,0 +1,58 @@
1
+ /* Sharing the desk: where it is now, then each way to share it, in the
2
+ route's own frame, with what it needs first or its button under its words. */
3
+
4
+ .share .now {
5
+ margin-block: 0 0.5lh;
6
+ }
7
+
8
+ .share .ways {
9
+ border: 1px solid var(--rule);
10
+ border-top: 0;
11
+ background: var(--surface);
12
+ }
13
+
14
+ .share .way {
15
+ display: grid;
16
+ row-gap: 8px;
17
+ padding: var(--row-pad) 12px;
18
+ border-bottom: 1px solid var(--rule-soft);
19
+ }
20
+
21
+ .share .way:last-child {
22
+ border-bottom: 0;
23
+ }
24
+
25
+ .share .way h3 {
26
+ margin: 0;
27
+ font-size: 1rem;
28
+ color: var(--ink);
29
+ }
30
+
31
+ .share .way p {
32
+ margin: 0;
33
+ }
34
+
35
+ .share .way .facts {
36
+ color: var(--quiet);
37
+ }
38
+
39
+ .share .way form {
40
+ display: grid;
41
+ row-gap: 8px;
42
+ justify-items: start;
43
+ }
44
+
45
+ .share .allow {
46
+ display: flex;
47
+ flex-wrap: wrap;
48
+ gap: 4px 2ch;
49
+ align-items: baseline;
50
+ }
51
+
52
+ .share .allow input {
53
+ width: min(40ch, 100%);
54
+ }
55
+
56
+ .share code {
57
+ white-space: nowrap;
58
+ }
data/public/side.js CHANGED
@@ -14,32 +14,23 @@ const made = (name, properties = {}, ...children) => {
14
14
  return element;
15
15
  };
16
16
 
17
- // "3 of 19 decisions reviewed", from the row's "~" and "3/19".
18
- const progressInWords = (mark, fraction) => {
19
- if (!fraction) return "no decisions to review";
20
- const [reviewed, total] = fraction.split("/").map(Number);
21
- const decisions = total === 1 ? "decision" : "decisions";
22
- if (mark === "x")
23
- return total === 1
24
- ? "its one decision reviewed"
25
- : `all ${total} decisions reviewed`;
26
- if (!reviewed) return `none of ${total} ${decisions} reviewed yet`;
27
- return `${reviewed} of ${total} ${decisions} reviewed`;
28
- };
29
-
30
17
  const MOVED = {
31
18
  changed: "changed on GitHub since it was read",
32
19
  closed: "no longer open on GitHub",
33
20
  };
34
21
 
22
+ // The row's marks said in words: what came back from the agent, then each
23
+ // grade's beads with what they count, in the words the server labelled them with.
35
24
  const marksOf = (row) => {
36
- const mark = row.querySelector("[data-side-mark]").textContent.trim();
37
- const fraction = row.querySelector("[data-side-fraction]").textContent.trim();
25
+ const back = row.querySelector("[data-side-mark]");
26
+ const grades = row.querySelectorAll("[data-side-decisions] [role=img]");
38
27
  return [
39
- [
40
- [mark, fraction].filter(Boolean).join(" "),
41
- progressInWords(mark, fraction),
42
- ],
28
+ back.textContent && [back.textContent, back.ariaLabel],
29
+ ...[...grades].map((grade) => [
30
+ made("span", { className: "beads" }, grade.cloneNode(true)),
31
+ grade.ariaLabel,
32
+ ]),
33
+ !grades.length && ["", "no decisions to review"],
43
34
  row.dataset.moved && ["*", MOVED[row.dataset.moved]],
44
35
  ].filter(Boolean);
45
36
  };
@@ -62,7 +53,7 @@ const cardOf = (row) => {
62
53
  made(
63
54
  "div",
64
55
  {},
65
- made("dt", { textContent: shown }),
56
+ made("dt", {}, shown),
66
57
  made("dd", { textContent: said }),
67
58
  ),
68
59
  ),
data/public/terminal.css CHANGED
@@ -501,7 +501,10 @@ body.shell::before {
501
501
  white-space: nowrap;
502
502
  }
503
503
 
504
+ /* The ↩ of a pull request the agent's answers came back on asks for the owner. */
504
505
  .side .stops .mark {
506
+ color: var(--ink);
507
+ font-weight: 700;
505
508
  white-space: pre;
506
509
  }
507
510
 
@@ -528,8 +531,7 @@ body.shell::before {
528
531
  background: var(--rest);
529
532
  }
530
533
 
531
- .side .stops [aria-current] .number,
532
- .side .stops [aria-current] .mark {
534
+ .side .stops [aria-current] .number {
533
535
  color: var(--ink);
534
536
  }
535
537
 
@@ -539,11 +541,6 @@ body.shell::before {
539
541
  white-space: normal;
540
542
  }
541
543
 
542
- .side .stops [aria-current] .fraction {
543
- border-color: var(--rule-strong);
544
- color: var(--ink);
545
- }
546
-
547
544
  /* The bars: how far, drawn square */
548
545
 
549
546
  meter,
@@ -960,18 +957,33 @@ body.shell .foot {
960
957
  color: var(--ink);
961
958
  }
962
959
 
960
+ /* The stage, and beside it, for a pair, the panel of its views and regions;
961
+ under them the divider's slider, the caption and the keys, the width of
962
+ the window. */
963
963
  .viewer[open] {
964
964
  display: grid;
965
+ grid-template-columns: minmax(0, 1fr) auto;
965
966
  grid-template-rows: minmax(0, 1fr) auto auto auto;
966
967
  }
967
968
 
968
969
  .viewer .stage {
969
970
  position: relative;
971
+ grid-column: 1;
972
+ grid-row: 1;
970
973
  overflow: hidden;
971
974
  cursor: crosshair;
972
975
  touch-action: none;
973
976
  }
974
977
 
978
+ .viewer .swipe {
979
+ grid-column: 1;
980
+ }
981
+
982
+ .viewer .caption,
983
+ .viewer .keys {
984
+ grid-column: 1 / -1;
985
+ }
986
+
975
987
  .viewer .stage[data-zoomed="true"] {
976
988
  cursor: grab;
977
989
  }
@@ -1011,7 +1023,9 @@ body.shell .foot {
1011
1023
  }
1012
1024
 
1013
1025
  /* The swipe's divider and the changed regions: as thin on the screen at any
1014
- zoom, white inside black, so that they show on a light picture and a dark. */
1026
+ zoom, white with black, so that they show on a light picture and a dark.
1027
+ A changed region is a black line dashed white, lighter than a note's solid
1028
+ frame, so the two are told apart at a glance. */
1015
1029
  .viewer .divider,
1016
1030
  .viewer .change {
1017
1031
  position: absolute;
@@ -1038,24 +1052,211 @@ body.shell .foot {
1038
1052
  }
1039
1053
 
1040
1054
  .viewer .change {
1041
- margin: calc(var(--line) * -2);
1042
- border: calc(var(--line) * 2) solid #fff;
1043
- outline: var(--line) solid #000;
1055
+ --dash: calc(var(--line) * 2);
1056
+ margin: calc(var(--dash) * -1);
1057
+ border: var(--dash) solid #000;
1058
+ outline: var(--dash) dashed #fff;
1059
+ outline-offset: calc(var(--dash) * -1);
1044
1060
  }
1045
1061
 
1046
1062
  .viewer .change[data-current] {
1047
- outline-width: calc(var(--line) * 3);
1063
+ --dash: calc(var(--line) * 3);
1064
+ }
1065
+
1066
+ /* The region focused on says its number and its half, above it, as large on
1067
+ the screen at any zoom. */
1068
+ .viewer .change .tag {
1069
+ position: absolute;
1070
+ bottom: 100%;
1071
+ left: calc(var(--line) * -3);
1072
+ margin-bottom: calc(var(--line) * 3);
1073
+ padding: 0 calc(var(--line) * 5);
1074
+ background: #fff;
1075
+ color: #000;
1076
+ font-size: calc(var(--line) * 12);
1077
+ font-weight: 700;
1078
+ line-height: 1.5;
1079
+ white-space: nowrap;
1048
1080
  }
1049
1081
 
1050
1082
  .viewer .swipe {
1051
1083
  margin: 0.3rem 1.1rem;
1052
1084
  }
1053
1085
 
1054
- .viewer .keys .current {
1086
+ /* The map in the stage's corner, once the stage shows only part of the
1087
+ picture: the whole, small, its regions, and the part shown, framed. */
1088
+ .viewer .minimap {
1089
+ position: absolute;
1090
+ right: 12px;
1091
+ bottom: 12px;
1092
+ z-index: 1;
1093
+ padding: 4px;
1094
+ border: 1px solid var(--rule-strong);
1095
+ background: var(--paper);
1096
+ cursor: pointer;
1097
+ --line: 1px;
1098
+ }
1099
+
1100
+ .viewer .minimap[hidden] {
1101
+ display: none;
1102
+ }
1103
+
1104
+ .viewer .map-canvas {
1105
+ position: relative;
1106
+ overflow: hidden;
1107
+ }
1108
+
1109
+ .viewer .map-regions,
1110
+ .viewer .map-shown {
1111
+ pointer-events: none;
1112
+ }
1113
+
1114
+ .viewer .map-region {
1115
+ position: absolute;
1116
+ outline: 1px solid #000;
1117
+ }
1118
+
1119
+ .viewer .map-region[data-current] {
1120
+ outline: 2px solid #fff;
1121
+ }
1122
+
1123
+ .viewer .map-shown {
1124
+ position: absolute;
1125
+ border: 1px solid #fff;
1126
+ outline: 1px solid #000;
1127
+ box-shadow: 0 0 0 100vmax rgb(0 0 0 / 0.45);
1128
+ }
1129
+
1130
+ /* The panel beside a pair, quiet: rows without boxes, the view looked at in
1131
+ reverse with a line on what it shows, the regions by number, and the one
1132
+ focused on as each half has it. It scrolls under the ✕'s corner. */
1133
+ .viewer .compare {
1134
+ grid-column: 2;
1135
+ grid-row: 1;
1136
+ box-sizing: border-box;
1137
+ width: 16rem;
1138
+ min-height: 0;
1139
+ margin-top: 3rem;
1140
+ padding: 0.6rem 0.8rem 1rem;
1141
+ overflow-y: auto;
1142
+ border-left: 1px solid var(--rule);
1143
+ background: var(--panel);
1144
+ font-size: 0.86rem;
1145
+ --line: 1px;
1146
+ }
1147
+
1148
+ .viewer .compare[hidden] {
1149
+ display: none;
1150
+ }
1151
+
1152
+ .viewer .compare h2 {
1153
+ margin: 0 0 0.4rem;
1154
+ color: var(--quiet);
1155
+ font-size: 0.8rem;
1156
+ font-weight: 600;
1157
+ letter-spacing: 0.025em;
1158
+ text-transform: uppercase;
1159
+ }
1160
+
1161
+ .viewer .compare h2 .count {
1055
1162
  color: var(--ink);
1163
+ }
1164
+
1165
+ .viewer .compare section + section {
1166
+ margin-top: 1.4rem;
1167
+ }
1168
+
1169
+ .viewer .compare p {
1170
+ margin: 0.4rem 0;
1171
+ color: var(--quiet);
1172
+ }
1173
+
1174
+ .viewer .compare button {
1175
+ border-color: transparent;
1176
+ cursor: pointer;
1177
+ }
1178
+
1179
+ .viewer .compare .way {
1180
+ display: flex;
1181
+ flex-wrap: wrap;
1182
+ align-items: baseline;
1183
+ gap: 0 1ch;
1184
+ width: 100%;
1185
+ padding: 0.15rem 0.4rem;
1186
+ text-align: left;
1187
+ }
1188
+
1189
+ .viewer .compare .way .shows {
1190
+ display: flex;
1191
+ flex-wrap: wrap;
1192
+ gap: 0 1.2ch;
1193
+ width: 100%;
1194
+ padding-left: calc(1.7em + 1ch);
1195
+ font-size: 0.8rem;
1196
+ }
1197
+
1198
+ .viewer .compare [aria-pressed="true"] kbd {
1199
+ border-color: currentcolor;
1200
+ color: inherit;
1201
+ }
1202
+
1203
+ .viewer .compare .places {
1204
+ display: flex;
1205
+ flex-wrap: wrap;
1206
+ gap: 0.15rem;
1207
+ margin: 0;
1208
+ padding: 0;
1209
+ list-style: none;
1210
+ }
1211
+
1212
+ .viewer .compare .place {
1213
+ min-width: 2em;
1214
+ padding: 0 0.3rem;
1215
+ color: var(--quiet);
1216
+ font-size: 0.8rem;
1217
+ }
1218
+
1219
+ .viewer .compare .place[aria-current] {
1220
+ background: var(--ink);
1221
+ color: var(--paper);
1056
1222
  font-weight: 700;
1057
1223
  }
1058
1224
 
1225
+ .viewer .compare .going {
1226
+ display: flex;
1227
+ flex-wrap: wrap;
1228
+ gap: 0 1.2ch;
1229
+ font-size: 0.8rem;
1230
+ }
1231
+
1232
+ /* The region focused on, as the before has it and as the after does; a
1233
+ press on either writes a note on it there. */
1234
+ .viewer .compare .crop {
1235
+ display: block;
1236
+ width: 100%;
1237
+ margin-top: 0.4rem;
1238
+ padding: 0.3rem;
1239
+ text-align: left;
1240
+ }
1241
+
1242
+ .viewer .compare .crop .frame {
1243
+ position: relative;
1244
+ display: block;
1245
+ overflow: hidden;
1246
+ background: #000;
1247
+ }
1248
+
1249
+ .viewer .compare .crop .label {
1250
+ display: block;
1251
+ margin-top: 0.15rem;
1252
+ color: var(--quiet);
1253
+ font-size: 0.8rem;
1254
+ }
1255
+
1256
+ .viewer .compare .crop:hover:not(:disabled) .label {
1257
+ color: var(--ink);
1258
+ }
1259
+
1059
1260
  /* A region being drawn, and the place a note is being written on: white
1060
1261
  inside black, so that it shows on a light picture and a dark, with all
1061
1262
  outside it dimmed. */
@@ -1071,8 +1272,10 @@ body.shell .foot {
1071
1272
  display: none;
1072
1273
  }
1073
1274
 
1074
- /* The notes on a picture, as thin on the screen at any zoom as the changed
1075
- regions; a pin is a ring. */
1275
+ /* The notes on a picture: a solid frame, black outside and white inside, so
1276
+ that one band shows on a light picture and the other on a dark; heavier
1277
+ than a changed region's dashes, and as heavy on the screen at any zoom. A
1278
+ pin is a small square of the same frame. */
1076
1279
  .viewer .notes {
1077
1280
  position: absolute;
1078
1281
  inset: 0;
@@ -1080,17 +1283,18 @@ body.shell .foot {
1080
1283
  }
1081
1284
 
1082
1285
  .viewer .note {
1286
+ --frame: calc(var(--line) * 2);
1083
1287
  position: absolute;
1084
- margin: calc(var(--line) * -2);
1085
- border: calc(var(--line) * 2) solid #fff;
1086
- outline: var(--line) solid #000;
1288
+ margin: calc(var(--frame) * -2);
1289
+ padding: var(--frame);
1290
+ border: var(--frame) solid #000;
1291
+ box-shadow: inset 0 0 0 var(--frame) #fff;
1087
1292
  }
1088
1293
 
1089
1294
  .viewer .note.pin {
1090
- width: calc(var(--line) * 14);
1091
- height: calc(var(--line) * 14);
1092
- margin: calc(var(--line) * -7);
1093
- border-radius: 50%;
1295
+ width: calc(var(--line) * 12);
1296
+ height: calc(var(--line) * 12);
1297
+ margin: calc(var(--line) * -6 - var(--frame) * 2);
1094
1298
  }
1095
1299
 
1096
1300
  /* A note saved opens at a click, and says so under the pointer. */
@@ -1099,19 +1303,21 @@ body.shell .foot {
1099
1303
  cursor: pointer;
1100
1304
  }
1101
1305
 
1102
- .viewer[data-takes-notes] .note[data-note]:hover {
1103
- border-width: calc(var(--line) * 3);
1306
+ .viewer[data-takes-notes] .note[data-note]:hover,
1307
+ .viewer .note.drafted {
1308
+ --frame: calc(var(--line) * 3);
1104
1309
  }
1105
1310
 
1106
1311
  .viewer .note.drafted {
1107
- border-width: calc(var(--line) * 3);
1108
- box-shadow: 0 0 0 calc(var(--line) * 100000) rgb(0 0 0 / 0.5);
1312
+ box-shadow:
1313
+ inset 0 0 0 var(--frame) #fff,
1314
+ 0 0 0 calc(var(--line) * 100000) rgb(0 0 0 / 0.5);
1109
1315
  }
1110
1316
 
1111
- /* The note's form, beside the place it is about. */
1317
+ /* The note's form, beside the place it is about, over the map. */
1112
1318
  .viewer .beside-note {
1113
1319
  position: absolute;
1114
- z-index: 1;
1320
+ z-index: 2;
1115
1321
  width: min(24rem, calc(100% - 24px));
1116
1322
  cursor: auto;
1117
1323
  }
@@ -1150,7 +1356,7 @@ body.shell .foot {
1150
1356
  position: absolute;
1151
1357
  top: 0.6rem;
1152
1358
  right: 0.8rem;
1153
- z-index: 2;
1359
+ z-index: 3;
1154
1360
  min-width: 2.2rem;
1155
1361
  padding: 0.1rem 0.6ch;
1156
1362
  border: 1px solid var(--rule-strong);
@@ -1300,15 +1506,60 @@ body[data-page="pull_requests"] .flash {
1300
1506
  font-weight: 400;
1301
1507
  }
1302
1508
 
1303
- .side .stops .fraction {
1304
- min-width: 4ch;
1509
+ .side .stops .decisions {
1305
1510
  padding-left: 0.6ch;
1306
- border-left: 1px solid var(--rule-soft);
1307
1511
  color: var(--quiet);
1308
1512
  font-size: 0.8rem;
1309
1513
  text-align: right;
1310
1514
  }
1311
1515
 
1516
+ /* Decisions counted as beads, the stepper's own (below): a bead to each
1517
+ blocking decision, a smaller one to each non-blocking, hollow while it
1518
+ waits for the owner and solid once reviewed. In the pane they are smaller
1519
+ again. */
1520
+
1521
+ .beads {
1522
+ display: inline-flex;
1523
+ gap: 1ch;
1524
+ align-items: center;
1525
+ vertical-align: middle;
1526
+ }
1527
+
1528
+ .beads > span {
1529
+ display: inline-flex;
1530
+ gap: 3px;
1531
+ align-items: center;
1532
+ }
1533
+
1534
+ .beads .non-blocking .bead {
1535
+ width: 6px;
1536
+ height: 6px;
1537
+ border-color: var(--quiet);
1538
+ }
1539
+
1540
+ .side .beads {
1541
+ gap: 0.8ch;
1542
+ }
1543
+
1544
+ .side .beads > span {
1545
+ gap: 2px;
1546
+ }
1547
+
1548
+ .side .beads .bead {
1549
+ width: 7px;
1550
+ height: 7px;
1551
+ }
1552
+
1553
+ .side .beads .non-blocking .bead {
1554
+ width: 5px;
1555
+ height: 5px;
1556
+ }
1557
+
1558
+ /* A burst's count: a hollow bead of the grade, then how many of it wait. */
1559
+ .side .beads .count {
1560
+ gap: 0.4ch;
1561
+ }
1562
+
1312
1563
  .pull-request .status {
1313
1564
  display: block;
1314
1565
  margin: 8px 0 var(--group-gap);
@@ -2496,17 +2747,23 @@ button.choice[hidden] {
2496
2747
  }
2497
2748
 
2498
2749
  .stepper .cell,
2499
- .finish .bead {
2750
+ .finish .bead,
2751
+ .beads .bead {
2500
2752
  box-sizing: border-box;
2501
2753
  width: 9px;
2502
2754
  height: 9px;
2503
2755
  border: 1px solid var(--ink);
2504
2756
  border-radius: 50% !important;
2757
+ }
2758
+
2759
+ .stepper .cell,
2760
+ .finish .bead {
2505
2761
  cursor: pointer;
2506
2762
  }
2507
2763
 
2508
2764
  .stepper .cell:not([data-state=""]),
2509
- .finish [data-line-of]:not([data-state=""]) .bead {
2765
+ .finish [data-line-of]:not([data-state=""]) .bead,
2766
+ .beads .bead.reviewed {
2510
2767
  border-color: var(--quiet);
2511
2768
  background: var(--quiet);
2512
2769
  }
@@ -4002,6 +4259,10 @@ body[data-page="bursts"] > .main {
4002
4259
  text-align: right;
4003
4260
  }
4004
4261
 
4262
+ .lines .stands strong {
4263
+ color: var(--ink);
4264
+ }
4265
+
4005
4266
  .lines .number {
4006
4267
  margin: 0;
4007
4268
  color: var(--quiet);