git-reviewer 2.4.1 → 2.6.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.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![CI](https://github.com/dheerajjha/reviewer/actions/workflows/ci.yml/badge.svg)](https://github.com/dheerajjha/reviewer/actions/workflows/ci.yml)
4
4
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
  [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)](package.json)
6
- [![Tests](https://img.shields.io/badge/tests-271-brightgreen.svg)](test/)
6
+ [![Tests](https://img.shields.io/badge/tests-272-brightgreen.svg)](test/)
7
7
  [![Dependencies](https://img.shields.io/badge/dependencies-3-brightgreen.svg)](package.json)
8
8
 
9
9
  When an agent writes the code, reading it becomes the bottleneck — and the tool
@@ -32,16 +32,18 @@ review was saved and keeps serving, because you may have more to read.
32
32
  Everything it says to you goes to stderr either way, so only the review is
33
33
  ever on stdout.
34
34
 
35
- ![A terminal installing git-reviewer and running it inside a project; the browser opens on the diff with the first changed file already showing; two review comments are written inline, one with a threaded follow-up; then reviewer export prints those same comments back for a coding agent to act on](docs/demo.gif)
35
+ ![A terminal installing git-reviewer and running it inside a project; the browser opens on the diff; two review comments are written inline, one with a threaded follow-up; submitting hands the review back to the terminal, where Claude reads it, explains both fixes and names a tradeoff in its own approach, and git diff shows the three files it changed](docs/demo.gif)
36
36
 
37
- *The whole loop, unedited and in real time — [full resolution](docs/demo.mp4).*
37
+ *One command, start to finish, unedited — [full resolution](docs/demo.mp4). Every
38
+ frame is the real tool: the terminal output is captured from the run you are
39
+ watching, and the code at the end is what the agent actually wrote.*
38
40
 
39
41
  Nothing is installed into the project, there is nothing to sign in to, and
40
42
  nothing leaves your machine. If you would rather not install it,
41
43
  `npx git-reviewer` works the same way.
42
44
 
43
- The recording above shows the two-step form, which still works and is the one
44
- to reach for when the review and the hand-off happen at different times:
45
+ When the review and the hand-off happen at different times, the two-step form
46
+ still works and is the one to reach for:
45
47
 
46
48
  ```bash
47
49
  reviewer # read the diff, comment on it
@@ -162,8 +164,10 @@ shown in full, read back out of `HEAD`:
162
164
  | `Escape` | Cancel input |
163
165
  | `↑` / `↓` | Move between files |
164
166
 
165
- None of that is guessable, so the review pane says it too, in a **How to
166
- comment** strip above the diff. Collapse it once and it stays collapsed.
167
+ None of that is guessable — a column of line numbers does not look clickable —
168
+ so the app shows you rather than telling you. **Reviewing, in four moves** opens
169
+ by itself the first time you review anything, and lives behind the `?` in the
170
+ header after that. Each move is a small animation of the gesture itself.
167
171
 
168
172
  ## What it writes
169
173
 
@@ -290,7 +294,7 @@ rather than a public issue.
290
294
 
291
295
  ```bash
292
296
  npm install # 3 dependencies, no build step, ~6MB
293
- npm test # 271 tests
297
+ npm test # 272 tests
294
298
  npm run test:watch
295
299
  npm run test:coverage
296
300
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-reviewer",
3
- "version": "2.4.1",
3
+ "version": "2.6.0",
4
4
  "description": "Review your coding agent's changes like a pull request — locally, with inline comments and threaded follow-ups, then hand the marked-up review back to the agent",
5
5
  "author": "Dheeraj Jha",
6
6
  "license": "MIT",
package/public/app.js CHANGED
@@ -210,6 +210,7 @@ async function loadRepo() {
210
210
  const opening = previous === -1 ? 0 : previous;
211
211
 
212
212
  loadFile(currentFiles[opening].path, opening);
213
+ offerHelpOnce();
213
214
  }
214
215
 
215
216
  } catch (error) {
@@ -253,8 +254,12 @@ function displayFiles(files) {
253
254
  // before the handler is parsed, so an escaped quote is a quote again by
254
255
  // the time it matters. The row carries its position instead, and the name
255
256
  // is read back out of `currentFiles`, where it stays data.
257
+ // The count is filled in by `updateFileCommentCounts`, not here. Rendering
258
+ // it now would mean re-rendering this list on every comment to keep it
259
+ // true, and re-rendering the list is what loses the selected row.
256
260
  return `<div class="file-item" onclick="openFileAt(${Number(index)})" title="${escapeHtml(file)}">
257
261
  ${statusBadge}<span class="file-path-text">${displayText}</span>
262
+ <span class="file-comment-count" data-file-index="${Number(index)}" hidden></span>
258
263
  </div>`;
259
264
  }).join('');
260
265
 
@@ -1469,12 +1474,46 @@ function initResizeHandle() {
1469
1474
  let currentFileIndex = -1;
1470
1475
 
1471
1476
  // Update comments sidebar
1477
+ /**
1478
+ * How many comments sit on each file, on the file rows themselves.
1479
+ *
1480
+ * In a review of any size the question you keep asking is which files you have
1481
+ * already covered, and the only answer on screen was a total in the sidebar
1482
+ * header — so you found out by clicking through every file (#48).
1483
+ *
1484
+ * Updated in place rather than by re-rendering the list. Re-rendering would be
1485
+ * one line, and it would drop the selected row and the scroll position of the
1486
+ * file list every time anyone typed a comment.
1487
+ */
1488
+ function updateFileCommentCounts() {
1489
+ const badges = document.querySelectorAll('.file-comment-count');
1490
+ if (badges.length === 0) return;
1491
+
1492
+ const perFile = new Map();
1493
+ for (const comment of comments) {
1494
+ perFile.set(comment.file, (perFile.get(comment.file) ?? 0) + 1);
1495
+ }
1496
+
1497
+ for (const badge of badges) {
1498
+ const file = currentFiles[Number(badge.dataset.fileIndex)];
1499
+ const count = file ? perFile.get(file.path) ?? 0 : 0;
1500
+
1501
+ badge.textContent = count;
1502
+ badge.title = `${count} comment${count === 1 ? '' : 's'} on this file`;
1503
+ badge.hidden = count === 0;
1504
+ }
1505
+ }
1506
+
1472
1507
  function updateCommentsSidebar() {
1473
1508
  const sidebar = document.getElementById('commentsSidebar');
1474
1509
  const commentsList = document.getElementById('commentsList');
1475
1510
  const commentsCount = document.getElementById('commentsCount');
1476
1511
  const commentsResizeHandle = document.getElementById('commentsResizeHandle');
1477
1512
 
1513
+ // Every path that changes a comment already calls this, so the per-file
1514
+ // counts ride along rather than needing ten more call sites of their own.
1515
+ updateFileCommentCounts();
1516
+
1478
1517
  commentsCount.textContent = comments.length;
1479
1518
 
1480
1519
  if (comments.length === 0) {
@@ -1624,8 +1663,6 @@ document.addEventListener('DOMContentLoaded', () => {
1624
1663
  }
1625
1664
  });
1626
1665
 
1627
- restoreHowTo();
1628
-
1629
1666
  for (const id of ['recentList', 'browseList']) {
1630
1667
  const list = document.getElementById(id);
1631
1668
  list.addEventListener('click', onPickerActivate);
@@ -1635,7 +1672,14 @@ document.addEventListener('DOMContentLoaded', () => {
1635
1672
  // Escape, and a click on the dimmed area around it, both mean "never mind"
1636
1673
  // -- but only when there is a review behind the picker to go back to.
1637
1674
  document.addEventListener('keydown', event => {
1638
- if (event.key === 'Escape') closePicker();
1675
+ if (event.key !== 'Escape') return;
1676
+
1677
+ // Topmost first: the help sits over the picker, which sits over a review.
1678
+ if (!document.getElementById('helpModal').classList.contains('hidden')) {
1679
+ closeHelp();
1680
+ return;
1681
+ }
1682
+ closePicker();
1639
1683
  });
1640
1684
 
1641
1685
  document.getElementById('picker').addEventListener('click', event => {
@@ -1883,53 +1927,48 @@ function onPickerActivate(event) {
1883
1927
  if (into) loadBrowse(into.dataset.into);
1884
1928
  }
1885
1929
 
1886
-
1887
- /* How to comment
1930
+ /* How to review
1888
1931
  * ---------------------------------------------------------------------------
1889
- * Open until it is dismissed, then remembered as dismissed. A hint that cannot
1890
- * be turned off becomes furniture, and a hint that has to be found is not one.
1932
+ * Shown once, on the first review this browser ever opens, and after that only
1933
+ * when asked for. The gestures here are not guessable -- a column of line
1934
+ * numbers does not look clickable -- but they are learned in one go, and a
1935
+ * panel that keeps explaining them afterwards is furniture.
1891
1936
  */
1892
1937
 
1893
- /** Where the dismissal is remembered. Per browser; it is a convenience, not state. */
1894
- const HOW_TO_KEY = 'reviewer.howTo.collapsed';
1938
+ /** Set the first time the help has been seen. Per browser; a convenience, not state. */
1939
+ const HELP_SEEN_KEY = 'reviewer.help.seen';
1895
1940
 
1896
- /**
1897
- * @returns {boolean} whether it has been collapsed before
1898
- */
1899
- function howToWasCollapsed() {
1941
+ /** @returns {boolean} */
1942
+ function helpWasSeen() {
1900
1943
  try {
1901
- return window.localStorage.getItem(HOW_TO_KEY) === 'true';
1944
+ return window.localStorage.getItem(HELP_SEEN_KEY) === 'true';
1902
1945
  } catch {
1903
- // A private window, or site data blocked. Showing the hint again is a far
1904
- // better failure than not rendering the panel at all.
1946
+ // Private window, or site data blocked. Showing it again is the harmless
1947
+ // direction to fail in.
1905
1948
  return false;
1906
1949
  }
1907
1950
  }
1908
1951
 
1909
- /**
1910
- * @param {boolean} collapsed
1911
- */
1912
- function rememberHowTo(collapsed) {
1952
+ function openHelp() {
1953
+ document.getElementById('helpModal').classList.remove('hidden');
1913
1954
  try {
1914
- window.localStorage.setItem(HOW_TO_KEY, String(collapsed));
1955
+ window.localStorage.setItem(HELP_SEEN_KEY, 'true');
1915
1956
  } catch {
1916
- // Nothing to do. It reopens next time, which is the harmless direction.
1957
+ // Nothing to do; it opens again next time.
1917
1958
  }
1918
1959
  }
1919
1960
 
1920
- function toggleHowTo() {
1921
- const panel = document.getElementById('howTo');
1922
- const collapsed = panel.classList.toggle('collapsed');
1923
-
1924
- document.getElementById('howToToggle').setAttribute('aria-expanded', String(!collapsed));
1925
- rememberHowTo(collapsed);
1961
+ function closeHelp() {
1962
+ document.getElementById('helpModal').classList.add('hidden');
1926
1963
  }
1927
1964
 
1928
- function restoreHowTo() {
1929
- const panel = document.getElementById('howTo');
1930
- if (!panel) return;
1931
-
1932
- const collapsed = howToWasCollapsed();
1933
- panel.classList.toggle('collapsed', collapsed);
1934
- document.getElementById('howToToggle').setAttribute('aria-expanded', String(!collapsed));
1965
+ /**
1966
+ * Offer it unprompted the first time, once a repository is actually on screen.
1967
+ *
1968
+ * Not at page load: with nothing open there is nothing to apply it to, and it
1969
+ * would be the first thing between somebody and the tool they just started.
1970
+ */
1971
+ function offerHelpOnce() {
1972
+ if (helpWasSeen()) return;
1973
+ openHelp();
1935
1974
  }
package/public/index.html CHANGED
@@ -19,6 +19,13 @@
19
19
  <span id="repoButtonLabel">Open a repository</span>
20
20
  </button>
21
21
 
22
+ <button id="helpButton" class="icon-button" onclick="openHelp()"
23
+ title="How to review" aria-label="How to review">
24
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
25
+ <path d="M8 0a8 8 0 100 16A8 8 0 008 0zM6.05 6.04a.75.75 0 01-1.45-.38 3.5 3.5 0 016.86.86c0 1.23-.67 1.93-1.28 2.4l-.13.1c-.61.47-.8.68-.8 1.13a.75.75 0 01-1.5 0c0-1.23.67-1.93 1.28-2.4l.13-.1c.61-.47.8-.68.8-1.13a2 2 0 00-3.91-.48zM9 12.5a1 1 0 11-2 0 1 1 0 012 0z"></path>
26
+ </svg>
27
+ </button>
28
+
22
29
  <button
23
30
  id="pathToggle"
24
31
  class="icon-button"
@@ -81,6 +88,74 @@
81
88
  </div>
82
89
  </section>
83
90
 
91
+ <div id="helpModal" class="help-modal hidden" onclick="if (event.target.id === 'helpModal') closeHelp()">
92
+ <div class="help-card" role="dialog" aria-modal="true" aria-labelledby="helpTitle">
93
+ <div class="help-head">
94
+ <h2 id="helpTitle">Reviewing, in four moves</h2>
95
+ <button class="icon-button" onclick="closeHelp()" aria-label="Close">
96
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
97
+ <path d="M3.72 3.72a.75.75 0 011.06 0L8 6.94l3.22-3.22a.75.75 0 111.06 1.06L9.06 8l3.22 3.22a.75.75 0 11-1.06 1.06L8 9.06l-3.22 3.22a.75.75 0 01-1.06-1.06L6.94 8 3.72 4.78a.75.75 0 010-1.06z"></path>
98
+ </svg>
99
+ </button>
100
+ </div>
101
+
102
+ <div class="help-steps">
103
+ <section class="help-step">
104
+ <div class="help-demo demo-click" aria-hidden="true">
105
+ <div class="m-row"><span class="m-num">7</span><span class="m-code"></span></div>
106
+ <div class="m-row m-target"><span class="m-num">8</span><span class="m-code"></span></div>
107
+ <div class="m-row"><span class="m-num">9</span><span class="m-code"></span></div>
108
+ <div class="m-box"></div>
109
+ <div class="m-cursor"></div>
110
+ </div>
111
+ <h3>Click a line number</h3>
112
+ <p>That is the whole gesture. Deleted lines take comments too — often the
113
+ most useful note is about code that was removed.</p>
114
+ </section>
115
+
116
+ <section class="help-step">
117
+ <div class="help-demo demo-select" aria-hidden="true">
118
+ <div class="m-row"><span class="m-num">8</span><span class="m-code m-long"><span class="m-sel"></span></span></div>
119
+ <div class="m-quote"></div>
120
+ <div class="m-cursor"></div>
121
+ </div>
122
+ <h3>Hold <kbd>⌘</kbd> and drag to quote</h3>
123
+ <p>Select part of a line first, then click its number. The exact fragment
124
+ is quoted in the comment, so there is no ambiguity about what you meant.</p>
125
+ </section>
126
+
127
+ <section class="help-step">
128
+ <div class="help-demo demo-thread" aria-hidden="true">
129
+ <div class="m-comment"></div>
130
+ <div class="m-reply"></div>
131
+ </div>
132
+ <h3>Replies thread</h3>
133
+ <p>Follow up on your own comment as the code changes underneath it.
134
+ Everything saves as you type — there is no save button.</p>
135
+ </section>
136
+
137
+ <section class="help-step">
138
+ <div class="help-demo demo-handoff" aria-hidden="true">
139
+ <div class="m-term"><span class="m-prompt">$</span><span class="m-cmd"></span></div>
140
+ <div class="m-flow"></div>
141
+ <div class="m-done">✓</div>
142
+ </div>
143
+ <h3>Submit, and it goes back</h3>
144
+ <p>Run it as <code>reviewer | claude -p "Apply this review."</code> and
145
+ submitting hands the review straight to the agent, which makes the changes.</p>
146
+ </section>
147
+ </div>
148
+
149
+ <div class="help-foot">
150
+ <span class="help-keys">
151
+ <kbd>⌘</kbd><kbd>Enter</kbd> save · <kbd>Esc</kbd> cancel ·
152
+ <kbd>↑</kbd><kbd>↓</kbd> move between files
153
+ </span>
154
+ <button class="help-done" onclick="closeHelp()">Got it</button>
155
+ </div>
156
+ </div>
157
+ </div>
158
+
84
159
  <aside id="sidebar" class="sidebar hidden">
85
160
  <div class="sidebar-header">
86
161
  <h2>Changed Files</h2>
@@ -101,28 +176,6 @@
101
176
  Full Context
102
177
  </button>
103
178
  </div>
104
- <section id="howTo" class="how-to">
105
- <button
106
- id="howToToggle"
107
- class="how-to-toggle"
108
- onclick="toggleHowTo()"
109
- aria-expanded="true"
110
- aria-controls="howToBody">
111
- <span class="how-to-chevron" aria-hidden="true">▾</span>
112
- How to comment
113
- </button>
114
- <div id="howToBody" class="how-to-body">
115
- <p><strong>Click a line number</strong> in the left column to comment on that line.
116
- Removed lines work too — the most useful note is often about deleted code.</p>
117
- <p><strong>Hold <kbd>⌘</kbd>/<kbd>Ctrl</kbd> and drag</strong> to select part of a line,
118
- then click the line number. The selection is quoted in the comment.</p>
119
- <p><kbd>⌘</kbd>/<kbd>Ctrl</kbd> + <kbd>Enter</kbd> saves ·
120
- <kbd>Esc</kbd> cancels ·
121
- <kbd>↑</kbd> <kbd>↓</kbd> move between files</p>
122
- <p>Everything saves as you type. <strong>Submit Review</strong> when you are done.</p>
123
- </div>
124
- </section>
125
-
126
179
  <div id="codeViewer" class="code-viewer"></div>
127
180
  </main>
128
181
 
package/public/style.css CHANGED
@@ -1367,6 +1367,53 @@ button:disabled {
1367
1367
  align-items: flex-start;
1368
1368
  }
1369
1369
 
1370
+ /* Where the review went
1371
+ *
1372
+ * The modal used to offer a download and nothing else, which is an odd thing
1373
+ * to put in front of somebody who started this from a terminal. The review is
1374
+ * already a file; knowing where is more useful than a second copy of it.
1375
+ */
1376
+ .review-destination {
1377
+ display: flex;
1378
+ align-items: baseline;
1379
+ gap: 10px;
1380
+ margin-bottom: 14px;
1381
+ padding: 10px 12px;
1382
+ border: 1px solid #e1e4e8;
1383
+ border-radius: 6px;
1384
+ background: #f6f8fa;
1385
+ font-size: 13px;
1386
+ color: #24292e;
1387
+ }
1388
+
1389
+ .review-destination.handed-off {
1390
+ border-color: #b7e0c2;
1391
+ background: #eefcf1;
1392
+ }
1393
+
1394
+ .review-destination strong {
1395
+ flex: none;
1396
+ font-weight: 600;
1397
+ }
1398
+
1399
+ .review-destination span {
1400
+ min-width: 0;
1401
+ color: #57606a;
1402
+ }
1403
+
1404
+ .review-destination .review-path {
1405
+ /* Wrapped rather than truncated. There is room in a modal, and every way of
1406
+ * shortening a path hides the half somebody wanted: cut the front and the
1407
+ * directory goes, cut the back and the filename goes. */
1408
+ word-break: break-all;
1409
+ font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace;
1410
+ font-size: 12px;
1411
+ }
1412
+
1413
+ .review-destination {
1414
+ align-items: flex-start;
1415
+ }
1416
+
1370
1417
  /* How to comment
1371
1418
  *
1372
1419
  * Nothing on screen said that the column of line numbers was clickable, or
@@ -1448,3 +1495,365 @@ button:disabled {
1448
1495
  color: #24292e;
1449
1496
  text-align: center;
1450
1497
  }
1498
+
1499
+ /* How to review
1500
+ *
1501
+ * This was four paragraphs pinned above the diff. It said the right things and
1502
+ * nobody read them, because a wall of prose is what a person skips when they
1503
+ * are trying to get to the code. Showing the gesture takes less attention than
1504
+ * describing it.
1505
+ *
1506
+ * The demos are CSS, not video. Four short clips would be a megabyte in a
1507
+ * package that is currently 55KB, and would be soft on a retina screen; these
1508
+ * are a couple of kilobytes, sharp at any density, and inherit the palette so
1509
+ * they keep matching the app when it changes.
1510
+ */
1511
+ .help-modal {
1512
+ position: fixed;
1513
+ top: 0;
1514
+ left: 0;
1515
+ width: 100%;
1516
+ height: 100%;
1517
+ z-index: 1100;
1518
+ background: rgba(0, 0, 0, 0.5);
1519
+ display: flex;
1520
+ align-items: center;
1521
+ justify-content: center;
1522
+ padding: 32px 24px;
1523
+ }
1524
+
1525
+ .help-card {
1526
+ width: 100%;
1527
+ max-width: 720px;
1528
+ max-height: calc(100vh - 64px);
1529
+ display: flex;
1530
+ flex-direction: column;
1531
+ background: white;
1532
+ border-radius: 10px;
1533
+ box-shadow: 0 14px 48px rgba(0, 0, 0, 0.28);
1534
+ overflow: hidden;
1535
+ }
1536
+
1537
+ .help-head {
1538
+ display: flex;
1539
+ align-items: center;
1540
+ justify-content: space-between;
1541
+ padding: 18px 22px;
1542
+ border-bottom: 1px solid #e1e4e8;
1543
+ }
1544
+
1545
+ .help-head h2 {
1546
+ font-size: 17px;
1547
+ font-weight: 600;
1548
+ color: #24292e;
1549
+ }
1550
+
1551
+ .help-steps {
1552
+ display: grid;
1553
+ grid-template-columns: 1fr 1fr;
1554
+ gap: 20px;
1555
+ padding: 22px;
1556
+ overflow-y: auto;
1557
+ }
1558
+
1559
+ .help-step h3 {
1560
+ margin: 12px 0 4px;
1561
+ font-size: 13.5px;
1562
+ font-weight: 600;
1563
+ color: #24292e;
1564
+ }
1565
+
1566
+ .help-step h3 kbd,
1567
+ .help-keys kbd {
1568
+ display: inline-block;
1569
+ min-width: 17px;
1570
+ padding: 0 5px;
1571
+ border: 1px solid #d1d5da;
1572
+ border-bottom-width: 2px;
1573
+ border-radius: 4px;
1574
+ background: #fafbfc;
1575
+ font-family: inherit;
1576
+ font-size: 11px;
1577
+ color: #24292e;
1578
+ text-align: center;
1579
+ }
1580
+
1581
+ .help-step p {
1582
+ font-size: 12.5px;
1583
+ line-height: 1.55;
1584
+ color: #57606a;
1585
+ }
1586
+
1587
+ .help-step code {
1588
+ padding: 1px 5px;
1589
+ border-radius: 4px;
1590
+ background: #f0f2f4;
1591
+ font-family: 'SF Mono', Monaco, monospace;
1592
+ font-size: 11.5px;
1593
+ color: #24292e;
1594
+ white-space: nowrap;
1595
+ }
1596
+
1597
+ /* The demo stage. Every mock below is built from these few pieces. */
1598
+ .help-demo {
1599
+ position: relative;
1600
+ height: 104px;
1601
+ padding: 10px;
1602
+ border: 1px solid #e1e4e8;
1603
+ border-radius: 8px;
1604
+ background: #fbfcfd;
1605
+ overflow: hidden;
1606
+ }
1607
+
1608
+ .m-row {
1609
+ display: flex;
1610
+ align-items: center;
1611
+ gap: 8px;
1612
+ height: 16px;
1613
+ }
1614
+
1615
+ .m-num {
1616
+ width: 16px;
1617
+ border-radius: 3px;
1618
+ font-family: 'SF Mono', Monaco, monospace;
1619
+ font-size: 9px;
1620
+ color: #a0a6ad;
1621
+ text-align: right;
1622
+ }
1623
+
1624
+ .m-code {
1625
+ height: 5px;
1626
+ width: 62%;
1627
+ border-radius: 3px;
1628
+ background: #dfe3e8;
1629
+ }
1630
+
1631
+ .m-row:nth-child(2) .m-code { width: 78%; }
1632
+ .m-row:nth-child(3) .m-code { width: 48%; }
1633
+
1634
+ /* A pointer. One element, moved by each demo's own keyframes. */
1635
+ .m-cursor {
1636
+ position: absolute;
1637
+ width: 9px;
1638
+ height: 13px;
1639
+ background: #24292e;
1640
+ clip-path: polygon(0 0, 0 100%, 27% 74%, 46% 96%, 62% 84%, 44% 62%, 100% 58%);
1641
+ }
1642
+
1643
+ /* 1. Click a line number */
1644
+ .demo-click .m-target .m-num { background: transparent; color: #0366d6; }
1645
+ .demo-click .m-box {
1646
+ position: absolute;
1647
+ left: 34px;
1648
+ top: 46px;
1649
+ width: 60%;
1650
+ height: 26px;
1651
+ border: 1px solid #0366d6;
1652
+ border-radius: 4px;
1653
+ background: white;
1654
+ opacity: 0;
1655
+ transform: translateY(-3px);
1656
+ animation: m-box-in 4s ease-in-out infinite;
1657
+ }
1658
+ .demo-click .m-target .m-num { animation: m-num-hit 4s ease-in-out infinite; }
1659
+ .demo-click .m-cursor { animation: m-click-path 4s ease-in-out infinite; }
1660
+
1661
+ @keyframes m-click-path {
1662
+ 0%, 8% { left: 64px; top: 74px; opacity: 0; }
1663
+ 18% { left: 64px; top: 74px; opacity: 1; }
1664
+ 38%, 100% { left: 14px; top: 26px; opacity: 1; }
1665
+ }
1666
+ @keyframes m-num-hit {
1667
+ 0%, 38% { background: transparent; color: #0366d6; }
1668
+ 44%, 100% { background: #0366d6; color: white; }
1669
+ }
1670
+ @keyframes m-box-in {
1671
+ 0%, 46% { opacity: 0; transform: translateY(-3px); }
1672
+ 56%, 100% { opacity: 1; transform: translateY(0); }
1673
+ }
1674
+
1675
+ /* 2. Hold Cmd and drag to quote */
1676
+ .demo-select .m-long { position: relative; width: 78%; background: #dfe3e8; }
1677
+ .demo-select .m-sel {
1678
+ position: absolute;
1679
+ left: 18%;
1680
+ top: -3px;
1681
+ height: 11px;
1682
+ width: 0;
1683
+ border-radius: 2px;
1684
+ background: rgba(3, 102, 214, 0.28);
1685
+ animation: m-sweep 4s ease-in-out infinite;
1686
+ }
1687
+ .demo-select .m-quote {
1688
+ position: absolute;
1689
+ left: 34px;
1690
+ top: 44px;
1691
+ width: 52%;
1692
+ height: 22px;
1693
+ border-left: 3px solid #0366d6;
1694
+ border-radius: 3px;
1695
+ background: #eef4fb;
1696
+ opacity: 0;
1697
+ animation: m-quote-in 4s ease-in-out infinite;
1698
+ }
1699
+ .demo-select .m-cursor { animation: m-drag-path 4s ease-in-out infinite; }
1700
+
1701
+ @keyframes m-sweep {
1702
+ 0%, 14% { width: 0; }
1703
+ 46%, 100% { width: 46%; }
1704
+ }
1705
+ @keyframes m-drag-path {
1706
+ 0%, 14% { left: 42px; top: 12px; opacity: 1; }
1707
+ 46% { left: 106px; top: 12px; opacity: 1; }
1708
+ 62% { left: 22px; top: 22px; opacity: 1; }
1709
+ 100% { left: 22px; top: 22px; opacity: 1; }
1710
+ }
1711
+ @keyframes m-quote-in {
1712
+ 0%, 64% { opacity: 0; }
1713
+ 74%, 100% { opacity: 1; }
1714
+ }
1715
+
1716
+ /* 3. Replies thread */
1717
+ .demo-thread .m-comment,
1718
+ .demo-thread .m-reply {
1719
+ position: absolute;
1720
+ border-radius: 5px;
1721
+ background: #fff8dc;
1722
+ border: 1px solid #f0e0a0;
1723
+ }
1724
+ .demo-thread .m-comment { left: 12px; top: 16px; width: 72%; height: 28px; }
1725
+ .demo-thread .m-reply {
1726
+ left: 34px;
1727
+ top: 52px;
1728
+ width: 58%;
1729
+ height: 24px;
1730
+ background: white;
1731
+ border-color: #e1e4e8;
1732
+ opacity: 0;
1733
+ transform: translateX(-6px);
1734
+ animation: m-reply-in 3.6s ease-in-out infinite;
1735
+ }
1736
+
1737
+ @keyframes m-reply-in {
1738
+ 0%, 30% { opacity: 0; transform: translateX(-6px); }
1739
+ 45%, 100% { opacity: 1; transform: translateX(0); }
1740
+ }
1741
+
1742
+ /* 4. Submit, and it goes back */
1743
+ .demo-handoff { background: #24292e; border-color: #24292e; }
1744
+ .demo-handoff .m-term {
1745
+ display: flex;
1746
+ align-items: center;
1747
+ gap: 6px;
1748
+ font-family: 'SF Mono', Monaco, monospace;
1749
+ font-size: 9px;
1750
+ }
1751
+ .demo-handoff .m-prompt { color: #56d364; }
1752
+ .demo-handoff .m-cmd {
1753
+ height: 5px;
1754
+ width: 0;
1755
+ border-radius: 3px;
1756
+ background: #6e7781;
1757
+ animation: m-type 4s steps(14) infinite;
1758
+ }
1759
+ .demo-handoff .m-flow {
1760
+ position: absolute;
1761
+ left: 14px;
1762
+ top: 44px;
1763
+ width: 6px;
1764
+ height: 6px;
1765
+ border-radius: 50%;
1766
+ background: #0366d6;
1767
+ animation: m-flow 4s ease-in-out infinite;
1768
+ }
1769
+ .demo-handoff .m-done {
1770
+ position: absolute;
1771
+ right: 14px;
1772
+ top: 38px;
1773
+ color: #56d364;
1774
+ font-size: 15px;
1775
+ opacity: 0;
1776
+ animation: m-done-in 4s ease-in-out infinite;
1777
+ }
1778
+
1779
+ @keyframes m-type {
1780
+ 0% { width: 0; }
1781
+ 30%, 100% { width: 74%; }
1782
+ }
1783
+ @keyframes m-flow {
1784
+ 0%, 36% { left: 14px; opacity: 0; }
1785
+ 44% { left: 14px; opacity: 1; }
1786
+ 76% { left: 78%; opacity: 1; }
1787
+ 84%, 100%{ left: 78%; opacity: 0; }
1788
+ }
1789
+ @keyframes m-done-in {
1790
+ 0%, 78% { opacity: 0; }
1791
+ 88%, 100% { opacity: 1; }
1792
+ }
1793
+
1794
+ /* Someone who has asked not to see motion should not be shown four looping
1795
+ * animations. The mocks still read as diagrams standing still. */
1796
+ @media (prefers-reduced-motion: reduce) {
1797
+ .help-demo * { animation: none !important; }
1798
+ .demo-click .m-box,
1799
+ .demo-select .m-quote,
1800
+ .demo-thread .m-reply,
1801
+ .demo-handoff .m-done { opacity: 1; transform: none; }
1802
+ .demo-select .m-sel { width: 46%; }
1803
+ .demo-handoff .m-cmd { width: 74%; }
1804
+ }
1805
+
1806
+ .help-foot {
1807
+ display: flex;
1808
+ align-items: center;
1809
+ justify-content: space-between;
1810
+ gap: 16px;
1811
+ padding: 14px 22px;
1812
+ border-top: 1px solid #e1e4e8;
1813
+ background: #f6f8fa;
1814
+ }
1815
+
1816
+ .help-keys {
1817
+ font-size: 12px;
1818
+ color: #57606a;
1819
+ }
1820
+
1821
+ .help-done {
1822
+ flex: none;
1823
+ padding: 7px 18px;
1824
+ border: none;
1825
+ border-radius: 6px;
1826
+ background: #0366d6;
1827
+ color: white;
1828
+ font: inherit;
1829
+ font-size: 13px;
1830
+ font-weight: 500;
1831
+ cursor: pointer;
1832
+ }
1833
+
1834
+ .help-done:hover { background: #0256b9; }
1835
+
1836
+ @media (max-width: 640px) {
1837
+ .help-steps { grid-template-columns: 1fr; }
1838
+ }
1839
+
1840
+ /* Comments per file
1841
+ *
1842
+ * In a review of any size the question you keep asking is which files you have
1843
+ * already covered. The only answer on screen was a total in the sidebar
1844
+ * header, so you found out by clicking through every file.
1845
+ */
1846
+ .file-comment-count {
1847
+ flex: none;
1848
+ min-width: 18px;
1849
+ margin-left: auto;
1850
+ padding: 1px 6px;
1851
+ border-radius: 20px;
1852
+ background: #fff3cd;
1853
+ border: 1px solid #f0e0a0;
1854
+ font-size: 11px;
1855
+ font-weight: 600;
1856
+ line-height: 1.5;
1857
+ color: #7a5d00;
1858
+ text-align: center;
1859
+ }