git-reviewer 2.4.1 → 2.5.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
@@ -162,8 +162,10 @@ shown in full, read back out of `HEAD`:
162
162
  | `Escape` | Cancel input |
163
163
  | `↑` / `↓` | Move between files |
164
164
 
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.
165
+ None of that is guessable — a column of line numbers does not look clickable —
166
+ so the app shows you rather than telling you. **Reviewing, in four moves** opens
167
+ by itself the first time you review anything, and lives behind the `?` in the
168
+ header after that. Each move is a small animation of the gesture itself.
167
169
 
168
170
  ## What it writes
169
171
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-reviewer",
3
- "version": "2.4.1",
3
+ "version": "2.5.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) {
@@ -1624,8 +1625,6 @@ document.addEventListener('DOMContentLoaded', () => {
1624
1625
  }
1625
1626
  });
1626
1627
 
1627
- restoreHowTo();
1628
-
1629
1628
  for (const id of ['recentList', 'browseList']) {
1630
1629
  const list = document.getElementById(id);
1631
1630
  list.addEventListener('click', onPickerActivate);
@@ -1635,7 +1634,14 @@ document.addEventListener('DOMContentLoaded', () => {
1635
1634
  // Escape, and a click on the dimmed area around it, both mean "never mind"
1636
1635
  // -- but only when there is a review behind the picker to go back to.
1637
1636
  document.addEventListener('keydown', event => {
1638
- if (event.key === 'Escape') closePicker();
1637
+ if (event.key !== 'Escape') return;
1638
+
1639
+ // Topmost first: the help sits over the picker, which sits over a review.
1640
+ if (!document.getElementById('helpModal').classList.contains('hidden')) {
1641
+ closeHelp();
1642
+ return;
1643
+ }
1644
+ closePicker();
1639
1645
  });
1640
1646
 
1641
1647
  document.getElementById('picker').addEventListener('click', event => {
@@ -1883,53 +1889,48 @@ function onPickerActivate(event) {
1883
1889
  if (into) loadBrowse(into.dataset.into);
1884
1890
  }
1885
1891
 
1886
-
1887
- /* How to comment
1892
+ /* How to review
1888
1893
  * ---------------------------------------------------------------------------
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.
1894
+ * Shown once, on the first review this browser ever opens, and after that only
1895
+ * when asked for. The gestures here are not guessable -- a column of line
1896
+ * numbers does not look clickable -- but they are learned in one go, and a
1897
+ * panel that keeps explaining them afterwards is furniture.
1891
1898
  */
1892
1899
 
1893
- /** Where the dismissal is remembered. Per browser; it is a convenience, not state. */
1894
- const HOW_TO_KEY = 'reviewer.howTo.collapsed';
1900
+ /** Set the first time the help has been seen. Per browser; a convenience, not state. */
1901
+ const HELP_SEEN_KEY = 'reviewer.help.seen';
1895
1902
 
1896
- /**
1897
- * @returns {boolean} whether it has been collapsed before
1898
- */
1899
- function howToWasCollapsed() {
1903
+ /** @returns {boolean} */
1904
+ function helpWasSeen() {
1900
1905
  try {
1901
- return window.localStorage.getItem(HOW_TO_KEY) === 'true';
1906
+ return window.localStorage.getItem(HELP_SEEN_KEY) === 'true';
1902
1907
  } 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.
1908
+ // Private window, or site data blocked. Showing it again is the harmless
1909
+ // direction to fail in.
1905
1910
  return false;
1906
1911
  }
1907
1912
  }
1908
1913
 
1909
- /**
1910
- * @param {boolean} collapsed
1911
- */
1912
- function rememberHowTo(collapsed) {
1914
+ function openHelp() {
1915
+ document.getElementById('helpModal').classList.remove('hidden');
1913
1916
  try {
1914
- window.localStorage.setItem(HOW_TO_KEY, String(collapsed));
1917
+ window.localStorage.setItem(HELP_SEEN_KEY, 'true');
1915
1918
  } catch {
1916
- // Nothing to do. It reopens next time, which is the harmless direction.
1919
+ // Nothing to do; it opens again next time.
1917
1920
  }
1918
1921
  }
1919
1922
 
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);
1923
+ function closeHelp() {
1924
+ document.getElementById('helpModal').classList.add('hidden');
1926
1925
  }
1927
1926
 
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));
1927
+ /**
1928
+ * Offer it unprompted the first time, once a repository is actually on screen.
1929
+ *
1930
+ * Not at page load: with nothing open there is nothing to apply it to, and it
1931
+ * would be the first thing between somebody and the tool they just started.
1932
+ */
1933
+ function offerHelpOnce() {
1934
+ if (helpWasSeen()) return;
1935
+ openHelp();
1935
1936
  }
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,344 @@ 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
+ }