pair-mode 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -4
- package/assets/duck.png +0 -0
- package/assets/favicon.png +0 -0
- package/dist/cli.js +510 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/oFrusch/pair-mode/main/docs/images/duck.png" alt="The pair-mode duck" width="120" align="right">
|
|
2
|
+
|
|
1
3
|
# pair mode
|
|
2
4
|
|
|
3
5
|
#### For when your agent needs a rubber ducky.
|
|
@@ -125,9 +127,13 @@ Run this command in the directory or any subdirectory of where you are working w
|
|
|
125
127
|
pair-mode on --web
|
|
126
128
|
```
|
|
127
129
|
|
|
128
|
-

|
|
131
|
+
|
|
132
|
+
That spawns a detached watcher, binds an HTTP server on `127.0.0.1`, and prints a link carrying a random token. Drag across any text in the diff. A popup opens under the selection. Type the note and press Enter.
|
|
133
|
+
|
|
134
|
+
The page opens in the layout your `layout` setting names, and `u` swaps it while you read. The inline layout puts every note in the right margin and draws a leader from the marked run to the card. The split layout threads each note under the last line it covers, and it hatches the side of a row that has no line.
|
|
129
135
|
|
|
130
|
-
|
|
136
|
+

|
|
131
137
|
|
|
132
138
|
## Configuration
|
|
133
139
|
|
|
@@ -163,8 +169,9 @@ existing config with no `editor` key now opens pair mode's own pane by default.
|
|
|
163
169
|
|
|
164
170
|
`notes` and `layout` both describe position, and they are easy to confuse. `layout`
|
|
165
171
|
controls the diff itself — split into two columns, or one inline column. `notes`
|
|
166
|
-
controls only where a note you write renders — in a docked panel, or
|
|
167
|
-
the span it annotates. Setting one does not affect the other.
|
|
172
|
+
controls only where a note you write renders in the pair pane — in a docked panel, or
|
|
173
|
+
anchored next to the span it annotates. Setting one does not affect the other. The web
|
|
174
|
+
view anchors every note, so it reads `layout` and ignores `notes`.
|
|
168
175
|
|
|
169
176
|
## License
|
|
170
177
|
|
package/assets/duck.png
ADDED
|
Binary file
|
|
Binary file
|
package/dist/cli.js
CHANGED
|
@@ -7361,7 +7361,7 @@ var require_dist = __commonJS({
|
|
|
7361
7361
|
});
|
|
7362
7362
|
|
|
7363
7363
|
// src/cli/index.ts
|
|
7364
|
-
import { readFileSync as
|
|
7364
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
7365
7365
|
import { join as join13, resolve as resolve2 } from "node:path";
|
|
7366
7366
|
|
|
7367
7367
|
// src/cli/setup/setup.ts
|
|
@@ -12156,6 +12156,7 @@ function runTui(options, io, abort) {
|
|
|
12156
12156
|
var TITLE = "pair-mode watch";
|
|
12157
12157
|
var HINT = "waiting for an edit \xB7 q quits";
|
|
12158
12158
|
var LABEL_WIDTH = 10;
|
|
12159
|
+
var DUCK = [" __", " <(o )___", " ( ._> /", " `---'"];
|
|
12159
12160
|
function line(label, value, truecolor) {
|
|
12160
12161
|
return fg(theme.fold, truecolor) + label.padEnd(LABEL_WIDTH) + RESET + value;
|
|
12161
12162
|
}
|
|
@@ -12167,9 +12168,12 @@ function queueText(waiting) {
|
|
|
12167
12168
|
}
|
|
12168
12169
|
function renderIdle(status, width, truecolor) {
|
|
12169
12170
|
const heading = bg(theme.chrome, truecolor) + fg(theme.statusText, truecolor) + TITLE.padEnd(width).slice(0, width) + RESET;
|
|
12171
|
+
const duck = DUCK.map((row) => fg(theme.chrome, truecolor) + row + RESET);
|
|
12170
12172
|
return [
|
|
12171
12173
|
heading,
|
|
12172
12174
|
"",
|
|
12175
|
+
...duck,
|
|
12176
|
+
"",
|
|
12173
12177
|
line("session", status.directory, truecolor),
|
|
12174
12178
|
line("socket", status.socketPath, truecolor),
|
|
12175
12179
|
line("clients", plural(status.clients, "client"), truecolor),
|
|
@@ -12597,6 +12601,8 @@ function runConfig(args, path) {
|
|
|
12597
12601
|
// src/web/server.ts
|
|
12598
12602
|
import { createServer as createServer2 } from "node:http";
|
|
12599
12603
|
import { randomBytes as randomBytes4 } from "node:crypto";
|
|
12604
|
+
import { existsSync as existsSync10, readFileSync as readFileSync7 } from "node:fs";
|
|
12605
|
+
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
12600
12606
|
|
|
12601
12607
|
// src/web/client/bundle.ts
|
|
12602
12608
|
var CLIENT_BUNDLE = `"use strict";
|
|
@@ -12626,10 +12632,11 @@ var PairClient = (() => {
|
|
|
12626
12632
|
draftRange: () => draftRange,
|
|
12627
12633
|
escapeHtml: () => escapeHtml,
|
|
12628
12634
|
hiddenRows: () => hiddenRows,
|
|
12635
|
+
inlineHtml: () => inlineHtml,
|
|
12629
12636
|
labelOf: () => labelOf,
|
|
12637
|
+
marginNotesHtml: () => marginNotesHtml,
|
|
12630
12638
|
markRange: () => markRange,
|
|
12631
12639
|
marksFor: () => marksFor,
|
|
12632
|
-
notesHtml: () => notesHtml,
|
|
12633
12640
|
numberOf: () => numberOf,
|
|
12634
12641
|
paintCell: () => paintCell,
|
|
12635
12642
|
quoteOf: () => quoteOf,
|
|
@@ -12661,6 +12668,9 @@ var PairClient = (() => {
|
|
|
12661
12668
|
function tokensOf(row, pane) {
|
|
12662
12669
|
return pane === "right" ? row.rightTokens : row.leftTokens;
|
|
12663
12670
|
}
|
|
12671
|
+
function other(pane) {
|
|
12672
|
+
return pane === "right" ? "left" : "right";
|
|
12673
|
+
}
|
|
12664
12674
|
function numberOf(row, pane) {
|
|
12665
12675
|
return pane === "right" ? row.rightNumber : row.leftNumber;
|
|
12666
12676
|
}
|
|
@@ -12719,25 +12729,36 @@ var PairClient = (() => {
|
|
|
12719
12729
|
});
|
|
12720
12730
|
return hidden;
|
|
12721
12731
|
}
|
|
12732
|
+
function signOf(kind, pane) {
|
|
12733
|
+
if (pane === "left") {
|
|
12734
|
+
return kind === "del" || kind === "replace" ? "-" : " ";
|
|
12735
|
+
}
|
|
12736
|
+
return kind === "add" || kind === "replace" ? "+" : " ";
|
|
12737
|
+
}
|
|
12722
12738
|
function cellHtml(notes, row, rowIndex, pane) {
|
|
12723
12739
|
const text = textOf(row, pane);
|
|
12724
12740
|
const body = paintCell(text, tokensOf(row, pane), marksFor(notes, rowIndex, pane, text.length));
|
|
12725
|
-
|
|
12741
|
+
const number = numberOf(row, pane);
|
|
12742
|
+
const void_ = number === null ? " void" : "";
|
|
12743
|
+
return '<td class="num ' + pane + void_ + '">' + (number ?? "") + '</td><td class="sign ' + pane + void_ + '">' + signOf(row.kind, pane) + '</td><td class="code ' + pane + void_ + '" data-row="' + rowIndex + '" data-pane="' + pane + '">' + body + "</td>";
|
|
12726
12744
|
}
|
|
12727
12745
|
function foldsByStart(review) {
|
|
12728
12746
|
return new Map(review.folds.map((fold, foldIndex) => [fold.start, foldIndex]));
|
|
12729
12747
|
}
|
|
12748
|
+
var SPLIT_COLUMNS = 6;
|
|
12749
|
+
var INLINE_COLUMNS = 4;
|
|
12750
|
+
function foldHtml(foldIndex, count, columns) {
|
|
12751
|
+
return '<tr class="fold" data-fold="' + foldIndex + '"><td colspan="' + columns + '">' + count + " unchanged lines</td></tr>";
|
|
12752
|
+
}
|
|
12730
12753
|
function tableHtml(review, notes, expanded) {
|
|
12731
12754
|
const hidden = hiddenRows(review, expanded);
|
|
12732
12755
|
const byStart = foldsByStart(review);
|
|
12733
|
-
const parts = [
|
|
12756
|
+
const parts = ['<table class="split">'];
|
|
12734
12757
|
review.rows.forEach((row, index) => {
|
|
12735
12758
|
const foldIndex = byStart.get(index);
|
|
12736
12759
|
const fold = foldIndex === void 0 ? void 0 : review.folds[foldIndex];
|
|
12737
12760
|
if (foldIndex !== void 0 && fold !== void 0 && !expanded.has(foldIndex)) {
|
|
12738
|
-
parts.push(
|
|
12739
|
-
'<tr class="fold" data-fold="' + foldIndex + '"><td colspan="6">' + fold.count + " unchanged lines</td></tr>"
|
|
12740
|
-
);
|
|
12761
|
+
parts.push(foldHtml(foldIndex, fold.count, SPLIT_COLUMNS));
|
|
12741
12762
|
}
|
|
12742
12763
|
if (hidden.has(index)) {
|
|
12743
12764
|
return;
|
|
@@ -12745,6 +12766,77 @@ var PairClient = (() => {
|
|
|
12745
12766
|
parts.push(
|
|
12746
12767
|
'<tr class="' + escapeHtml(row.kind) + '">' + cellHtml(notes, row, index, "left") + cellHtml(notes, row, index, "right") + "</tr>"
|
|
12747
12768
|
);
|
|
12769
|
+
parts.push(threadsAt(review, notes, index, SPLIT_COLUMNS));
|
|
12770
|
+
});
|
|
12771
|
+
parts.push("</table>");
|
|
12772
|
+
return parts.join("");
|
|
12773
|
+
}
|
|
12774
|
+
function inlineLines(kind) {
|
|
12775
|
+
if (kind === "del") {
|
|
12776
|
+
return [{ pane: "left", sign: "-", kind: "del" }];
|
|
12777
|
+
}
|
|
12778
|
+
if (kind === "add") {
|
|
12779
|
+
return [{ pane: "right", sign: "+", kind: "add" }];
|
|
12780
|
+
}
|
|
12781
|
+
if (kind === "replace") {
|
|
12782
|
+
return [
|
|
12783
|
+
{ pane: "left", sign: "-", kind: "del" },
|
|
12784
|
+
{ pane: "right", sign: "+", kind: "add" }
|
|
12785
|
+
];
|
|
12786
|
+
}
|
|
12787
|
+
return [{ pane: "right", sign: " ", kind: "context" }];
|
|
12788
|
+
}
|
|
12789
|
+
function inlineMarks(notes, rowIndex, kind, pane, length) {
|
|
12790
|
+
if (kind !== "context") {
|
|
12791
|
+
return marksFor(notes, rowIndex, pane, length);
|
|
12792
|
+
}
|
|
12793
|
+
return [
|
|
12794
|
+
...marksFor(notes, rowIndex, "left", length),
|
|
12795
|
+
...marksFor(notes, rowIndex, "right", length)
|
|
12796
|
+
];
|
|
12797
|
+
}
|
|
12798
|
+
function anchorsHere(line, panes, notePane) {
|
|
12799
|
+
return line.pane === notePane || !panes.includes(notePane);
|
|
12800
|
+
}
|
|
12801
|
+
function anchorsAt(notes, rowIndex, line, panes) {
|
|
12802
|
+
return notes.map((note, index) => ({ note, index })).filter(
|
|
12803
|
+
(entry) => entry.note.startRow === rowIndex && anchorsHere(line, panes, entry.note.pane)
|
|
12804
|
+
).map((entry) => entry.index);
|
|
12805
|
+
}
|
|
12806
|
+
function inlineRowHtml(review, notes, rowIndex, line, panes) {
|
|
12807
|
+
const row = review.rows[rowIndex];
|
|
12808
|
+
if (row === void 0) {
|
|
12809
|
+
return "";
|
|
12810
|
+
}
|
|
12811
|
+
const text = textOf(row, line.pane);
|
|
12812
|
+
const body = paintCell(
|
|
12813
|
+
text,
|
|
12814
|
+
tokensOf(row, line.pane),
|
|
12815
|
+
inlineMarks(notes, rowIndex, row.kind, line.pane, text.length)
|
|
12816
|
+
);
|
|
12817
|
+
const anchors = anchorsAt(notes, rowIndex, line, panes);
|
|
12818
|
+
const leftNumber = line.pane === "left" ? row.leftNumber ?? "" : "";
|
|
12819
|
+
const rightNumber = line.pane === "right" ? row.rightNumber ?? "" : "";
|
|
12820
|
+
return '<tr class="' + line.kind + '"' + (anchors.length === 0 ? "" : ' data-anchor="' + anchors.join(" ") + '"') + '><td class="num old">' + leftNumber + '</td><td class="num new">' + rightNumber + '</td><td class="sign ' + line.pane + '">' + line.sign + '</td><td class="code ' + line.pane + '" data-row="' + rowIndex + '" data-pane="' + line.pane + '">' + body + "</td></tr>";
|
|
12821
|
+
}
|
|
12822
|
+
function inlineHtml(review, notes, expanded) {
|
|
12823
|
+
const hidden = hiddenRows(review, expanded);
|
|
12824
|
+
const byStart = foldsByStart(review);
|
|
12825
|
+
const parts = ['<table class="inline">'];
|
|
12826
|
+
review.rows.forEach((row, index) => {
|
|
12827
|
+
const foldIndex = byStart.get(index);
|
|
12828
|
+
const fold = foldIndex === void 0 ? void 0 : review.folds[foldIndex];
|
|
12829
|
+
if (foldIndex !== void 0 && fold !== void 0 && !expanded.has(foldIndex)) {
|
|
12830
|
+
parts.push(foldHtml(foldIndex, fold.count, INLINE_COLUMNS));
|
|
12831
|
+
}
|
|
12832
|
+
if (hidden.has(index)) {
|
|
12833
|
+
return;
|
|
12834
|
+
}
|
|
12835
|
+
const lines = inlineLines(row.kind);
|
|
12836
|
+
const panes = lines.map((line) => line.pane);
|
|
12837
|
+
lines.forEach((line) => {
|
|
12838
|
+
parts.push(inlineRowHtml(review, notes, index, line, panes));
|
|
12839
|
+
});
|
|
12748
12840
|
});
|
|
12749
12841
|
parts.push("</table>");
|
|
12750
12842
|
return parts.join("");
|
|
@@ -12764,13 +12856,21 @@ var PairClient = (() => {
|
|
|
12764
12856
|
if (startRow === void 0 || endRow === void 0) {
|
|
12765
12857
|
return "";
|
|
12766
12858
|
}
|
|
12767
|
-
const start = numberOf(startRow, note.pane);
|
|
12859
|
+
const start = numberOf(startRow, note.pane) ?? numberOf(startRow, other(note.pane));
|
|
12768
12860
|
const end = numberOf(endRow, note.pane);
|
|
12861
|
+
if (start === null) {
|
|
12862
|
+
return "";
|
|
12863
|
+
}
|
|
12769
12864
|
return start === end || end === null ? "L" + start : "L" + start + "-" + end;
|
|
12770
12865
|
}
|
|
12771
|
-
function
|
|
12866
|
+
function threadsAt(review, notes, rowIndex, columns) {
|
|
12867
|
+
return notes.map((note, index) => ({ note, index })).filter((entry) => entry.note.endRow === rowIndex).map(
|
|
12868
|
+
(entry) => '<tr class="thread"><td colspan="' + columns + '"><button class="drop" data-drop="' + entry.index + '" aria-label="Remove note">×</button><div class="at">' + escapeHtml(labelOf(review, entry.note)) + "</div><p>" + escapeHtml(entry.note.text) + "</p></td></tr>"
|
|
12869
|
+
).join("");
|
|
12870
|
+
}
|
|
12871
|
+
function marginNotesHtml(review, notes) {
|
|
12772
12872
|
return notes.map(
|
|
12773
|
-
(note, index) => '<div class="note"><button class="drop" data-drop="' + index + '">×</button><div class="
|
|
12873
|
+
(note, index) => '<div class="note" data-card="' + index + '"><button class="drop" data-drop="' + index + '" aria-label="Remove note">×</button><div class="at">' + escapeHtml(labelOf(review, note)) + "</div><p>" + escapeHtml(note.text) + "</p></div>"
|
|
12774
12874
|
).join("");
|
|
12775
12875
|
}
|
|
12776
12876
|
function sortedNotes(notes) {
|
|
@@ -12810,157 +12910,238 @@ var PairClient = (() => {
|
|
|
12810
12910
|
var STYLE = `
|
|
12811
12911
|
:root {
|
|
12812
12912
|
--bg: #0d1117;
|
|
12813
|
-
--
|
|
12814
|
-
--
|
|
12913
|
+
--chrome: #10141a;
|
|
12914
|
+
--rule: #1e242c;
|
|
12815
12915
|
--text: #c9d1d9;
|
|
12816
12916
|
--muted: ${theme.fold};
|
|
12817
|
-
--
|
|
12917
|
+
--dim: #39424d;
|
|
12818
12918
|
--add-bar: ${theme.addBar};
|
|
12819
12919
|
--del-bar: ${theme.delBar};
|
|
12820
|
-
--add-tint: rgba(63, 185, 80, 0.
|
|
12821
|
-
--del-tint: rgba(248, 81, 73, 0.
|
|
12920
|
+
--add-tint: rgba(63, 185, 80, 0.08);
|
|
12921
|
+
--del-tint: rgba(248, 81, 73, 0.08);
|
|
12822
12922
|
--note: ${theme.note};
|
|
12823
|
-
--note-tint: rgba(210, 168, 255, 0.
|
|
12923
|
+
--note-tint: rgba(210, 168, 255, 0.16);
|
|
12924
|
+
--amber: ${theme.chrome};
|
|
12925
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
12824
12926
|
}
|
|
12825
12927
|
|
|
12826
12928
|
* { box-sizing: border-box; }
|
|
12827
12929
|
|
|
12828
12930
|
body {
|
|
12829
12931
|
margin: 0;
|
|
12932
|
+
min-height: 100vh;
|
|
12933
|
+
display: flex;
|
|
12934
|
+
flex-direction: column;
|
|
12830
12935
|
background: var(--bg);
|
|
12831
12936
|
color: var(--text);
|
|
12832
|
-
font: 13px/1.6
|
|
12937
|
+
font: 13px/1.6 var(--mono);
|
|
12938
|
+
-webkit-font-smoothing: antialiased;
|
|
12833
12939
|
}
|
|
12834
12940
|
|
|
12941
|
+
/* ---- chrome ---- */
|
|
12942
|
+
|
|
12835
12943
|
header {
|
|
12836
12944
|
position: sticky;
|
|
12837
12945
|
top: 0;
|
|
12838
|
-
z-index:
|
|
12946
|
+
z-index: 6;
|
|
12839
12947
|
display: flex;
|
|
12840
|
-
align-items:
|
|
12841
|
-
gap: 12px;
|
|
12842
|
-
padding: 9px 14px;
|
|
12948
|
+
align-items: stretch;
|
|
12843
12949
|
background: var(--chrome);
|
|
12844
|
-
|
|
12845
|
-
font-
|
|
12950
|
+
border-bottom: 1px solid var(--rule);
|
|
12951
|
+
font-size: 12px;
|
|
12846
12952
|
}
|
|
12847
12953
|
|
|
12848
|
-
header .
|
|
12954
|
+
header .seg { padding: 6px 13px; white-space: nowrap; }
|
|
12955
|
+
header .mode { display: flex; align-items: center; gap: 7px; color: var(--amber); font-weight: 700; letter-spacing: 0.1em; }
|
|
12956
|
+
header .mark { border-radius: 4px; }
|
|
12957
|
+
header .path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--muted); }
|
|
12958
|
+
header .plus { color: var(--add-bar); }
|
|
12959
|
+
header .minus { color: var(--del-bar); }
|
|
12960
|
+
header .tally { color: var(--note); }
|
|
12849
12961
|
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12962
|
+
footer {
|
|
12963
|
+
position: sticky;
|
|
12964
|
+
bottom: 0;
|
|
12965
|
+
z-index: 6;
|
|
12966
|
+
display: flex;
|
|
12967
|
+
align-items: stretch;
|
|
12968
|
+
background: var(--chrome);
|
|
12969
|
+
border-top: 1px solid var(--rule);
|
|
12970
|
+
font-size: 11.5px;
|
|
12971
|
+
}
|
|
12972
|
+
|
|
12973
|
+
footer .seg { padding: 6px 12px; color: var(--muted); white-space: nowrap; }
|
|
12974
|
+
footer .seg .key { color: var(--text); font-weight: 700; }
|
|
12975
|
+
footer .fill { flex: 1; }
|
|
12976
|
+
|
|
12977
|
+
footer .swap { display: flex; }
|
|
12978
|
+
|
|
12979
|
+
footer .swap button {
|
|
12980
|
+
font: 400 11.5px/1.6 var(--mono);
|
|
12981
|
+
padding: 6px 11px;
|
|
12982
|
+
border: 0;
|
|
12983
|
+
background: none;
|
|
12984
|
+
color: var(--muted);
|
|
12985
|
+
cursor: pointer;
|
|
12986
|
+
}
|
|
12987
|
+
|
|
12988
|
+
footer .swap button:hover { color: var(--text); }
|
|
12989
|
+
footer .swap button[aria-pressed="true"] { background: var(--note-tint); color: var(--note); font-weight: 700; }
|
|
12990
|
+
|
|
12991
|
+
footer .act {
|
|
12992
|
+
font: 700 11.5px/1.6 var(--mono);
|
|
12993
|
+
padding: 6px 14px;
|
|
12994
|
+
border: 0;
|
|
12857
12995
|
cursor: pointer;
|
|
12858
12996
|
}
|
|
12859
12997
|
|
|
12860
|
-
|
|
12998
|
+
footer .act:disabled { opacity: 0.35; cursor: default; }
|
|
12999
|
+
footer .act.ok { background: rgba(63, 185, 80, 0.16); color: var(--add-bar); }
|
|
13000
|
+
footer .act.ok:enabled:hover { background: var(--add-bar); color: #06140a; }
|
|
13001
|
+
footer .act.send { background: var(--note); color: #170f1e; }
|
|
13002
|
+
footer .act.send:enabled:hover { filter: brightness(1.12); }
|
|
13003
|
+
|
|
13004
|
+
button:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--note); outline-offset: -2px; }
|
|
13005
|
+
|
|
13006
|
+
/* ---- diff, both layouts ---- */
|
|
12861
13007
|
|
|
12862
|
-
main { display: flex; align-items: flex-start; }
|
|
13008
|
+
main { position: relative; flex: 1; display: flex; align-items: flex-start; }
|
|
12863
13009
|
|
|
12864
13010
|
#diff { flex: 1; min-width: 0; overflow-x: auto; }
|
|
12865
13011
|
|
|
12866
13012
|
table { width: 100%; border-collapse: collapse; }
|
|
12867
13013
|
|
|
12868
|
-
td { padding: 0
|
|
13014
|
+
td { padding: 0; vertical-align: top; white-space: pre; }
|
|
12869
13015
|
|
|
12870
13016
|
td.num {
|
|
12871
13017
|
width: 1%;
|
|
12872
|
-
padding: 0
|
|
13018
|
+
padding: 0 8px;
|
|
12873
13019
|
text-align: right;
|
|
12874
|
-
color: var(--
|
|
13020
|
+
color: var(--dim);
|
|
13021
|
+
font-variant-numeric: tabular-nums;
|
|
12875
13022
|
user-select: none;
|
|
12876
13023
|
}
|
|
12877
13024
|
|
|
12878
|
-
td.
|
|
13025
|
+
td.sign { width: 1%; padding: 0 6px 0 0; text-align: center; color: var(--dim); user-select: none; }
|
|
13026
|
+
td.code { cursor: text; padding-right: 18px; }
|
|
12879
13027
|
|
|
12880
|
-
|
|
12881
|
-
|
|
13028
|
+
table.split td.code { width: 50%; }
|
|
13029
|
+
table.inline td.code { width: auto; }
|
|
12882
13030
|
|
|
12883
|
-
tr.add td.
|
|
12884
|
-
tr.del td.
|
|
13031
|
+
tr.add td.sign.right, tr.replace td.sign.right { color: var(--add-bar); }
|
|
13032
|
+
tr.del td.sign.left, tr.replace td.sign.left { color: var(--del-bar); }
|
|
13033
|
+
tr.add td.sign, tr.add td.code { background: var(--add-tint); }
|
|
13034
|
+
tr.del td.sign, tr.del td.code { background: var(--del-tint); }
|
|
13035
|
+
tr.replace td.sign.right, tr.replace td.code.right { background: var(--add-tint); }
|
|
13036
|
+
tr.replace td.sign.left, tr.replace td.code.left { background: var(--del-tint); }
|
|
12885
13037
|
|
|
12886
|
-
td.
|
|
13038
|
+
tr.add td.void, tr.del td.void, tr.replace td.void {
|
|
13039
|
+
background: repeating-linear-gradient(-45deg, transparent 0 5px, rgba(255, 255, 255, 0.022) 5px 10px);
|
|
13040
|
+
cursor: default;
|
|
13041
|
+
}
|
|
12887
13042
|
|
|
12888
13043
|
::selection { background: rgba(88, 166, 255, 0.35); }
|
|
12889
13044
|
|
|
12890
13045
|
mark.noted {
|
|
12891
13046
|
background: var(--note-tint);
|
|
12892
|
-
border-bottom: 2px solid var(--note);
|
|
12893
13047
|
color: inherit;
|
|
12894
|
-
|
|
13048
|
+
box-shadow: inset 0 -2px 0 var(--note);
|
|
12895
13049
|
}
|
|
12896
13050
|
|
|
12897
13051
|
tr.fold td {
|
|
12898
|
-
padding:
|
|
12899
|
-
color: var(--
|
|
12900
|
-
background:
|
|
13052
|
+
padding: 3px 0 3px 18px;
|
|
13053
|
+
color: var(--dim);
|
|
13054
|
+
background: rgba(255, 255, 255, 0.016);
|
|
13055
|
+
border-top: 1px solid var(--rule);
|
|
13056
|
+
border-bottom: 1px solid var(--rule);
|
|
12901
13057
|
cursor: pointer;
|
|
12902
|
-
text-align: center;
|
|
12903
13058
|
user-select: none;
|
|
12904
13059
|
}
|
|
12905
13060
|
|
|
12906
13061
|
tr.fold td:hover { color: var(--text); }
|
|
12907
13062
|
|
|
12908
|
-
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
|
|
12912
|
-
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
padding: 12px;
|
|
12916
|
-
border-left: 1px solid var(--line);
|
|
12917
|
-
background: var(--panel);
|
|
13063
|
+
/* ---- notes threaded under a split row ---- */
|
|
13064
|
+
|
|
13065
|
+
tr.thread td {
|
|
13066
|
+
padding: 9px 22px 10px 60px;
|
|
13067
|
+
border-left: 2px solid var(--note);
|
|
13068
|
+
background: rgba(210, 168, 255, 0.05);
|
|
13069
|
+
white-space: normal;
|
|
12918
13070
|
}
|
|
12919
13071
|
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
font-size:
|
|
12923
|
-
letter-spacing: 0.
|
|
12924
|
-
color: var(--muted);
|
|
13072
|
+
tr.thread .at {
|
|
13073
|
+
font-family: var(--mono);
|
|
13074
|
+
font-size: 10.5px;
|
|
13075
|
+
letter-spacing: 0.11em;
|
|
12925
13076
|
text-transform: uppercase;
|
|
13077
|
+
color: var(--note);
|
|
13078
|
+
}
|
|
13079
|
+
|
|
13080
|
+
tr.thread p, .note p {
|
|
13081
|
+
margin: 3px 0 0;
|
|
13082
|
+
max-width: 62ch;
|
|
13083
|
+
font-size: 13px;
|
|
13084
|
+
line-height: 1.5;
|
|
13085
|
+
color: #e7e2ea;
|
|
13086
|
+
}
|
|
13087
|
+
|
|
13088
|
+
/* ---- notes in the inline margin ---- */
|
|
13089
|
+
|
|
13090
|
+
#margin {
|
|
13091
|
+
position: relative;
|
|
13092
|
+
width: 320px;
|
|
13093
|
+
flex: none;
|
|
13094
|
+
align-self: stretch;
|
|
13095
|
+
padding: 0 20px 0 4px;
|
|
12926
13096
|
}
|
|
12927
13097
|
|
|
13098
|
+
#margin.hidden { display: none; }
|
|
13099
|
+
|
|
12928
13100
|
.note {
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
13101
|
+
position: absolute;
|
|
13102
|
+
left: 4px;
|
|
13103
|
+
right: 20px;
|
|
13104
|
+
padding-left: 14px;
|
|
13105
|
+
border-left: 1px solid var(--note);
|
|
12934
13106
|
}
|
|
12935
13107
|
|
|
12936
|
-
.note .
|
|
12937
|
-
|
|
12938
|
-
|
|
13108
|
+
.note .at {
|
|
13109
|
+
font-size: 10.5px;
|
|
13110
|
+
letter-spacing: 0.11em;
|
|
13111
|
+
text-transform: uppercase;
|
|
13112
|
+
color: var(--note);
|
|
13113
|
+
}
|
|
13114
|
+
|
|
13115
|
+
#leaders { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
|
|
12939
13116
|
|
|
12940
|
-
.
|
|
13117
|
+
.drop {
|
|
12941
13118
|
float: right;
|
|
12942
|
-
padding: 0
|
|
13119
|
+
padding: 0 4px;
|
|
12943
13120
|
border: 0;
|
|
12944
13121
|
background: none;
|
|
12945
|
-
color: var(--
|
|
13122
|
+
color: var(--dim);
|
|
13123
|
+
font: 400 14px/1 var(--mono);
|
|
13124
|
+
cursor: pointer;
|
|
12946
13125
|
}
|
|
12947
13126
|
|
|
12948
|
-
.
|
|
13127
|
+
.drop:hover { color: var(--del-bar); }
|
|
13128
|
+
|
|
13129
|
+
/* ---- popup ---- */
|
|
12949
13130
|
|
|
12950
13131
|
#popup {
|
|
12951
13132
|
position: absolute;
|
|
12952
|
-
z-index:
|
|
13133
|
+
z-index: 8;
|
|
12953
13134
|
display: none;
|
|
12954
|
-
width:
|
|
12955
|
-
padding:
|
|
13135
|
+
width: 320px;
|
|
13136
|
+
padding: 11px;
|
|
12956
13137
|
border: 1px solid var(--note);
|
|
12957
|
-
border-radius:
|
|
12958
|
-
background: var(--
|
|
12959
|
-
box-shadow: 0
|
|
13138
|
+
border-radius: 5px;
|
|
13139
|
+
background: var(--chrome);
|
|
13140
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
|
|
12960
13141
|
}
|
|
12961
13142
|
|
|
12962
13143
|
#popup .quote {
|
|
12963
|
-
margin-bottom:
|
|
13144
|
+
margin-bottom: 7px;
|
|
12964
13145
|
overflow: hidden;
|
|
12965
13146
|
color: var(--muted);
|
|
12966
13147
|
font-size: 11px;
|
|
@@ -12970,27 +13151,57 @@ aside h2 {
|
|
|
12970
13151
|
|
|
12971
13152
|
#popup .row { display: flex; gap: 8px; margin-top: 8px; }
|
|
12972
13153
|
|
|
13154
|
+
#popup button {
|
|
13155
|
+
font: 500 12px/1 var(--mono);
|
|
13156
|
+
padding: 6px 12px;
|
|
13157
|
+
border: 1px solid var(--rule);
|
|
13158
|
+
border-radius: 4px;
|
|
13159
|
+
background: none;
|
|
13160
|
+
color: var(--text);
|
|
13161
|
+
cursor: pointer;
|
|
13162
|
+
}
|
|
13163
|
+
|
|
13164
|
+
#popup button.save { background: var(--note); border-color: var(--note); color: #170f1e; font-weight: 700; }
|
|
13165
|
+
|
|
12973
13166
|
textarea {
|
|
12974
13167
|
width: 100%;
|
|
12975
|
-
min-height:
|
|
12976
|
-
padding:
|
|
12977
|
-
border: 1px solid var(--
|
|
13168
|
+
min-height: 66px;
|
|
13169
|
+
padding: 7px;
|
|
13170
|
+
border: 1px solid var(--rule);
|
|
12978
13171
|
border-radius: 4px;
|
|
12979
13172
|
background: var(--bg);
|
|
12980
13173
|
color: var(--text);
|
|
12981
|
-
font:
|
|
13174
|
+
font: 13px/1.5 var(--mono);
|
|
12982
13175
|
resize: vertical;
|
|
12983
13176
|
}
|
|
12984
13177
|
|
|
12985
|
-
.hint { color: var(--
|
|
13178
|
+
.hint { color: var(--dim); font-size: 11px; }
|
|
13179
|
+
|
|
13180
|
+
#idle { padding: 72px 24px; color: var(--dim); text-align: center; }
|
|
13181
|
+
#idle img { border-radius: 14px; opacity: 0.75; animation: bob 3.2s ease-in-out infinite; }
|
|
13182
|
+
#idle p { margin: 16px 0 0; }
|
|
12986
13183
|
|
|
12987
|
-
|
|
13184
|
+
@keyframes bob { 50% { transform: translateY(-6px); } }
|
|
13185
|
+
|
|
13186
|
+
@media (prefers-reduced-motion: reduce) {
|
|
13187
|
+
#idle img { animation: none; }
|
|
13188
|
+
}
|
|
13189
|
+
|
|
13190
|
+
@media (max-width: 900px) {
|
|
13191
|
+
main { flex-wrap: wrap; }
|
|
13192
|
+
#leaders { display: none; }
|
|
13193
|
+
#margin { width: 100%; padding: 12px 16px; border-top: 1px solid var(--rule); }
|
|
13194
|
+
.note { position: static; margin-bottom: 12px; }
|
|
13195
|
+
}
|
|
12988
13196
|
`;
|
|
12989
13197
|
var WIRING = String.raw`
|
|
12990
13198
|
const C = PairClient;
|
|
13199
|
+
const assets = document.body.dataset.assets ?? "";
|
|
12991
13200
|
const token = location.pathname.split("/")[2];
|
|
13201
|
+
const main = document.querySelector("main");
|
|
12992
13202
|
const diff = document.getElementById("diff");
|
|
12993
|
-
const
|
|
13203
|
+
const margin = document.getElementById("margin");
|
|
13204
|
+
const leaders = document.getElementById("leaders");
|
|
12994
13205
|
const popup = document.getElementById("popup");
|
|
12995
13206
|
const popupQuote = document.getElementById("popup-quote");
|
|
12996
13207
|
const popupText = document.getElementById("popup-text");
|
|
@@ -12999,37 +13210,152 @@ const popupCancel = document.getElementById("popup-cancel");
|
|
|
12999
13210
|
const sendButton = document.getElementById("send");
|
|
13000
13211
|
const approveButton = document.getElementById("approve");
|
|
13001
13212
|
const pathLabel = document.getElementById("path");
|
|
13213
|
+
const tallyLabel = document.getElementById("tally");
|
|
13214
|
+
const plusLabel = document.getElementById("plus");
|
|
13215
|
+
const minusLabel = document.getElementById("minus");
|
|
13002
13216
|
|
|
13003
13217
|
let review = null;
|
|
13004
13218
|
let notes = [];
|
|
13005
13219
|
let expanded = new Set();
|
|
13006
13220
|
let draft = null;
|
|
13221
|
+
let layout = document.body.dataset.layout === "inline" ? "inline" : "split";
|
|
13222
|
+
|
|
13223
|
+
function isInline() {
|
|
13224
|
+
return layout === "inline";
|
|
13225
|
+
}
|
|
13226
|
+
|
|
13227
|
+
function countKinds() {
|
|
13228
|
+
let plus = 0;
|
|
13229
|
+
let minus = 0;
|
|
13007
13230
|
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
|
|
13231
|
+
review.rows.forEach((row) => {
|
|
13232
|
+
if (row.kind === "add" || row.kind === "replace") { plus += 1; }
|
|
13233
|
+
if (row.kind === "del" || row.kind === "replace") { minus += 1; }
|
|
13234
|
+
});
|
|
13235
|
+
|
|
13236
|
+
return { plus, minus };
|
|
13237
|
+
}
|
|
13238
|
+
|
|
13239
|
+
// A leader line joins a marked run to its card, so it redraws whenever either one moves.
|
|
13240
|
+
function drawLeaders() {
|
|
13241
|
+
if (!isInline() || review === null || notes.length === 0) {
|
|
13242
|
+
leaders.innerHTML = "";
|
|
13243
|
+
return;
|
|
13244
|
+
}
|
|
13245
|
+
|
|
13246
|
+
const base = main.getBoundingClientRect();
|
|
13247
|
+
|
|
13248
|
+
leaders.setAttribute("viewBox", "0 0 " + base.width + " " + base.height);
|
|
13249
|
+
leaders.setAttribute("width", base.width);
|
|
13250
|
+
leaders.setAttribute("height", base.height);
|
|
13251
|
+
|
|
13252
|
+
leaders.innerHTML = notes.map((_note, index) => {
|
|
13253
|
+
const row = diff.querySelector('[data-anchor~="' + index + '"]');
|
|
13254
|
+
const card = margin.querySelector('[data-card="' + index + '"]');
|
|
13255
|
+
|
|
13256
|
+
if (row === null || card === null) {
|
|
13257
|
+
return "";
|
|
13258
|
+
}
|
|
13259
|
+
|
|
13260
|
+
// The line leaves from under the marked run, so it continues that underline and crosses no glyph.
|
|
13261
|
+
const marks = row.querySelectorAll("mark.noted");
|
|
13262
|
+
const ordinal = (row.dataset.anchor ?? "").split(" ").indexOf(String(index));
|
|
13263
|
+
const source = marks[ordinal] ?? marks[marks.length - 1] ?? row;
|
|
13264
|
+
const a = source.getBoundingClientRect();
|
|
13265
|
+
const b = card.getBoundingClientRect();
|
|
13266
|
+
const x1 = a.right - base.left + 5;
|
|
13267
|
+
const y1 = a.bottom - base.top - 0.5;
|
|
13268
|
+
const x2 = b.left - base.left;
|
|
13269
|
+
const y2 = b.top - base.top;
|
|
13270
|
+
const mid = x1 + (x2 - x1) * 0.55;
|
|
13271
|
+
|
|
13272
|
+
return '<path d="M ' + x1 + ' ' + y1 + ' C ' + mid + ' ' + y1 + ', ' + mid + ' ' + y2 + ', ' + x2 + ' ' + y2 +
|
|
13273
|
+
'" fill="none" stroke="var(--note)" stroke-width="1" stroke-opacity="0.5" />' +
|
|
13274
|
+
'<circle cx="' + x1 + '" cy="' + y1 + '" r="2" fill="var(--note)" fill-opacity="0.9" />';
|
|
13275
|
+
}).join("");
|
|
13276
|
+
}
|
|
13277
|
+
|
|
13278
|
+
// Two notes on nearby lines would overlap, so each card starts below the one before it.
|
|
13279
|
+
function placeCards() {
|
|
13280
|
+
if (!isInline()) {
|
|
13011
13281
|
return;
|
|
13012
13282
|
}
|
|
13013
13283
|
|
|
13014
|
-
|
|
13284
|
+
const base = margin.getBoundingClientRect();
|
|
13285
|
+
let floor = 0;
|
|
13286
|
+
|
|
13287
|
+
notes.forEach((_note, index) => {
|
|
13288
|
+
const row = diff.querySelector('[data-anchor~="' + index + '"]');
|
|
13289
|
+
const card = margin.querySelector('[data-card="' + index + '"]');
|
|
13290
|
+
|
|
13291
|
+
if (row === null || card === null) {
|
|
13292
|
+
return;
|
|
13293
|
+
}
|
|
13294
|
+
|
|
13295
|
+
const top = Math.max(row.getBoundingClientRect().bottom - base.top, floor);
|
|
13296
|
+
card.style.top = top + "px";
|
|
13297
|
+
floor = top + card.offsetHeight + 18;
|
|
13298
|
+
});
|
|
13299
|
+
}
|
|
13300
|
+
|
|
13301
|
+
function renderMargin() {
|
|
13302
|
+
const wanted = isInline() && review !== null && notes.length > 0;
|
|
13303
|
+
|
|
13304
|
+
margin.classList.toggle("hidden", !wanted);
|
|
13305
|
+
margin.innerHTML = wanted ? C.marginNotesHtml(review, notes) : "";
|
|
13015
13306
|
}
|
|
13016
13307
|
|
|
13017
13308
|
function render() {
|
|
13018
13309
|
if (review === null) {
|
|
13019
|
-
diff.innerHTML =
|
|
13310
|
+
diff.innerHTML =
|
|
13311
|
+
'<div id="idle"><img src="' + assets + '/duck.png" alt="" width="88" height="88"><p>waiting for an edit</p></div>';
|
|
13020
13312
|
pathLabel.textContent = "no review open";
|
|
13313
|
+
plusLabel.textContent = "";
|
|
13314
|
+
minusLabel.textContent = "";
|
|
13315
|
+
tallyLabel.textContent = "";
|
|
13021
13316
|
sendButton.disabled = true;
|
|
13022
13317
|
approveButton.disabled = true;
|
|
13023
13318
|
hidePopup();
|
|
13024
|
-
|
|
13319
|
+
renderMargin();
|
|
13320
|
+
refreshLeaders();
|
|
13025
13321
|
return;
|
|
13026
13322
|
}
|
|
13027
13323
|
|
|
13324
|
+
const counts = countKinds();
|
|
13325
|
+
|
|
13028
13326
|
pathLabel.textContent = review.path;
|
|
13327
|
+
plusLabel.textContent = "+" + counts.plus;
|
|
13328
|
+
minusLabel.textContent = "−" + counts.minus;
|
|
13329
|
+
tallyLabel.textContent = notes.length === 0 ? "" : notes.length + (notes.length === 1 ? " note" : " notes");
|
|
13029
13330
|
sendButton.disabled = notes.length === 0;
|
|
13331
|
+
sendButton.textContent = "^S send" + (notes.length === 0 ? "" : " " + notes.length);
|
|
13030
13332
|
approveButton.disabled = false;
|
|
13031
|
-
|
|
13032
|
-
|
|
13333
|
+
|
|
13334
|
+
diff.innerHTML = isInline()
|
|
13335
|
+
? C.inlineHtml(review, notes, expanded)
|
|
13336
|
+
: C.tableHtml(review, notes, expanded);
|
|
13337
|
+
|
|
13338
|
+
renderMargin();
|
|
13339
|
+
refreshLeaders();
|
|
13340
|
+
}
|
|
13341
|
+
|
|
13342
|
+
// A card needs its final height before anything measures it, so the placement waits for the paint.
|
|
13343
|
+
function refreshLeaders() {
|
|
13344
|
+
requestAnimationFrame(() => {
|
|
13345
|
+
placeCards();
|
|
13346
|
+
drawLeaders();
|
|
13347
|
+
});
|
|
13348
|
+
}
|
|
13349
|
+
|
|
13350
|
+
function setLayout(next) {
|
|
13351
|
+
layout = next;
|
|
13352
|
+
|
|
13353
|
+
document.querySelectorAll(".swap button").forEach((button) => {
|
|
13354
|
+
button.setAttribute("aria-pressed", String(button.dataset.layout === next));
|
|
13355
|
+
});
|
|
13356
|
+
|
|
13357
|
+
hidePopup();
|
|
13358
|
+
render();
|
|
13033
13359
|
}
|
|
13034
13360
|
|
|
13035
13361
|
function cellOf(node) {
|
|
@@ -13063,9 +13389,11 @@ function readSelection() {
|
|
|
13063
13389
|
return span === null ? null : { span, rect: range.getBoundingClientRect() };
|
|
13064
13390
|
}
|
|
13065
13391
|
|
|
13392
|
+
// A hidden textarea still holds focus, and the key handler ignores every key while it does.
|
|
13066
13393
|
function hidePopup() {
|
|
13067
13394
|
popup.style.display = "none";
|
|
13068
13395
|
popupText.value = "";
|
|
13396
|
+
popupText.blur();
|
|
13069
13397
|
draft = null;
|
|
13070
13398
|
}
|
|
13071
13399
|
|
|
@@ -13074,7 +13402,7 @@ function showPopup(selected) {
|
|
|
13074
13402
|
popupQuote.textContent = C.quoteOf(review, selected.span) || "(whole line)";
|
|
13075
13403
|
|
|
13076
13404
|
const top = window.scrollY + selected.rect.bottom + 8;
|
|
13077
|
-
const left = Math.max(8, Math.min(window.scrollX + selected.rect.left, window.innerWidth -
|
|
13405
|
+
const left = Math.max(8, Math.min(window.scrollX + selected.rect.left, window.innerWidth - 340));
|
|
13078
13406
|
|
|
13079
13407
|
popup.style.top = top + "px";
|
|
13080
13408
|
popup.style.left = left + "px";
|
|
@@ -13093,7 +13421,7 @@ diff.addEventListener("mouseup", () => {
|
|
|
13093
13421
|
});
|
|
13094
13422
|
|
|
13095
13423
|
diff.addEventListener("click", (event) => {
|
|
13096
|
-
const fold = event.target.closest("[data-fold]");
|
|
13424
|
+
const fold = event.target instanceof Element ? event.target.closest("[data-fold]") : null;
|
|
13097
13425
|
|
|
13098
13426
|
if (fold === null) {
|
|
13099
13427
|
return;
|
|
@@ -13131,8 +13459,8 @@ popupText.addEventListener("keydown", (event) => {
|
|
|
13131
13459
|
}
|
|
13132
13460
|
});
|
|
13133
13461
|
|
|
13134
|
-
|
|
13135
|
-
const drop = event.target.closest("[data-drop]");
|
|
13462
|
+
document.addEventListener("click", (event) => {
|
|
13463
|
+
const drop = event.target instanceof Element ? event.target.closest("[data-drop]") : null;
|
|
13136
13464
|
|
|
13137
13465
|
if (drop === null) {
|
|
13138
13466
|
return;
|
|
@@ -13143,6 +13471,44 @@ notesPanel.addEventListener("click", (event) => {
|
|
|
13143
13471
|
render();
|
|
13144
13472
|
});
|
|
13145
13473
|
|
|
13474
|
+
document.querySelectorAll(".swap button").forEach((button) => {
|
|
13475
|
+
button.addEventListener("click", () => setLayout(button.dataset.layout));
|
|
13476
|
+
});
|
|
13477
|
+
|
|
13478
|
+
// The pane binds u to the same swap, so the browser answers the key a pair mode user already knows.
|
|
13479
|
+
document.addEventListener("keydown", (event) => {
|
|
13480
|
+
if (event.target === popupText || event.metaKey || event.ctrlKey || event.altKey) {
|
|
13481
|
+
return;
|
|
13482
|
+
}
|
|
13483
|
+
|
|
13484
|
+
if (event.key === "u") {
|
|
13485
|
+
setLayout(isInline() ? "split" : "inline");
|
|
13486
|
+
}
|
|
13487
|
+
});
|
|
13488
|
+
|
|
13489
|
+
// The footer names ^S and ^Q, so both keys act rather than reaching the browser.
|
|
13490
|
+
document.addEventListener("keydown", (event) => {
|
|
13491
|
+
if (!event.ctrlKey || event.metaKey || event.altKey || review === null) {
|
|
13492
|
+
return;
|
|
13493
|
+
}
|
|
13494
|
+
|
|
13495
|
+
if (event.key === "s" && notes.length > 0) {
|
|
13496
|
+
event.preventDefault();
|
|
13497
|
+
post({ id: review.id, notes });
|
|
13498
|
+
return;
|
|
13499
|
+
}
|
|
13500
|
+
|
|
13501
|
+
if (event.key === "q") {
|
|
13502
|
+
event.preventDefault();
|
|
13503
|
+
post({ id: review.id, notes: [] });
|
|
13504
|
+
}
|
|
13505
|
+
});
|
|
13506
|
+
|
|
13507
|
+
window.addEventListener("resize", () => {
|
|
13508
|
+
placeCards();
|
|
13509
|
+
drawLeaders();
|
|
13510
|
+
});
|
|
13511
|
+
|
|
13146
13512
|
function warn(message) {
|
|
13147
13513
|
pathLabel.textContent = message;
|
|
13148
13514
|
}
|
|
@@ -13211,34 +13577,48 @@ function inlineScript(source) {
|
|
|
13211
13577
|
}
|
|
13212
13578
|
var SCRIPT = inlineScript(`${CLIENT_BUNDLE}
|
|
13213
13579
|
${WIRING}`);
|
|
13214
|
-
function
|
|
13580
|
+
function pressed(layout, wanted) {
|
|
13581
|
+
return layout === wanted ? "true" : "false";
|
|
13582
|
+
}
|
|
13583
|
+
function renderPage(layout = "split", assetBase = "") {
|
|
13215
13584
|
return `<!doctype html>
|
|
13216
13585
|
<html lang="en">
|
|
13217
13586
|
<head>
|
|
13218
13587
|
<meta charset="utf-8">
|
|
13219
13588
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
13589
|
+
<link rel="icon" type="image/png" href="${assetBase}/favicon.png">
|
|
13220
13590
|
<title>pair mode</title>
|
|
13221
13591
|
<style>${STYLE}</style>
|
|
13222
13592
|
</head>
|
|
13223
|
-
<body>
|
|
13593
|
+
<body data-layout="${layout}" data-assets="${assetBase}">
|
|
13224
13594
|
<header>
|
|
13225
|
-
<span>pair
|
|
13226
|
-
<span class="path" id="path">no review open</span>
|
|
13227
|
-
<
|
|
13228
|
-
<
|
|
13595
|
+
<span class="seg mode"><img class="mark" src="${assetBase}/duck.png" alt="" width="18" height="18">pair</span>
|
|
13596
|
+
<span class="seg path" id="path">no review open</span>
|
|
13597
|
+
<span class="seg plus" id="plus"></span>
|
|
13598
|
+
<span class="seg minus" id="minus"></span>
|
|
13599
|
+
<span class="seg tally" id="tally"></span>
|
|
13229
13600
|
</header>
|
|
13230
13601
|
<main>
|
|
13602
|
+
<svg id="leaders" aria-hidden="true"></svg>
|
|
13231
13603
|
<div id="diff"></div>
|
|
13232
|
-
<
|
|
13233
|
-
<h2>Notes</h2>
|
|
13234
|
-
<div id="notes"></div>
|
|
13235
|
-
</aside>
|
|
13604
|
+
<div id="margin"></div>
|
|
13236
13605
|
</main>
|
|
13606
|
+
<footer>
|
|
13607
|
+
<span class="seg">drag to <span class="key">note</span></span>
|
|
13608
|
+
<span class="seg">click a fold to <span class="key">open</span></span>
|
|
13609
|
+
<span class="swap" role="group" aria-label="Layout">
|
|
13610
|
+
<button data-layout="inline" aria-pressed="${pressed(layout, "inline")}">u inline</button>
|
|
13611
|
+
<button data-layout="split" aria-pressed="${pressed(layout, "split")}">u split</button>
|
|
13612
|
+
</span>
|
|
13613
|
+
<span class="fill"></span>
|
|
13614
|
+
<button class="act ok" id="approve">^Q approve</button>
|
|
13615
|
+
<button class="act send" id="send">^S send</button>
|
|
13616
|
+
</footer>
|
|
13237
13617
|
<div id="popup">
|
|
13238
13618
|
<div class="quote" id="popup-quote"></div>
|
|
13239
13619
|
<textarea id="popup-text" placeholder="What do you want to ask?"></textarea>
|
|
13240
13620
|
<div class="row">
|
|
13241
|
-
<button id="popup-save">Add note</button>
|
|
13621
|
+
<button class="save" id="popup-save">Add note</button>
|
|
13242
13622
|
<button id="popup-cancel">Cancel</button>
|
|
13243
13623
|
</div>
|
|
13244
13624
|
<div class="hint">Enter saves. Shift-Enter makes a new line.</div>
|
|
@@ -13285,6 +13665,15 @@ function webNotesToQuestions(review, notes) {
|
|
|
13285
13665
|
var HOST = "127.0.0.1";
|
|
13286
13666
|
var TOKEN_BYTES = 16;
|
|
13287
13667
|
var MAX_BODY_BYTES = 1e6;
|
|
13668
|
+
function readAsset(name) {
|
|
13669
|
+
const bundled = fileURLToPath4(new URL(`../assets/${name}`, import.meta.url));
|
|
13670
|
+
const source = fileURLToPath4(new URL(`../../assets/${name}`, import.meta.url));
|
|
13671
|
+
return readFileSync7(existsSync10(bundled) ? bundled : source);
|
|
13672
|
+
}
|
|
13673
|
+
var IMAGES = {
|
|
13674
|
+
"favicon.png": readAsset("favicon.png"),
|
|
13675
|
+
"duck.png": readAsset("duck.png")
|
|
13676
|
+
};
|
|
13288
13677
|
var NOT_FOUND = 404;
|
|
13289
13678
|
var OK = 200;
|
|
13290
13679
|
var BAD_REQUEST = 400;
|
|
@@ -13416,7 +13805,13 @@ data: ${data}
|
|
|
13416
13805
|
const url = request.url ?? "";
|
|
13417
13806
|
if (url === base && request.method === "GET") {
|
|
13418
13807
|
response.writeHead(OK, { "content-type": "text/html; charset=utf-8" });
|
|
13419
|
-
response.end(renderPage());
|
|
13808
|
+
response.end(renderPage(options.layout, base));
|
|
13809
|
+
return;
|
|
13810
|
+
}
|
|
13811
|
+
const image = url.startsWith(`${base}/`) ? IMAGES[url.slice(base.length + 1)] : void 0;
|
|
13812
|
+
if (image !== void 0 && request.method === "GET") {
|
|
13813
|
+
response.writeHead(OK, { "content-type": "image/png" });
|
|
13814
|
+
response.end(image);
|
|
13420
13815
|
return;
|
|
13421
13816
|
}
|
|
13422
13817
|
if (url === `${base}/events` && request.method === "GET") {
|
|
@@ -13516,6 +13911,7 @@ async function startWebWatch(options, config) {
|
|
|
13516
13911
|
const web = await startWebServer({
|
|
13517
13912
|
port: options.port,
|
|
13518
13913
|
token: options.token,
|
|
13914
|
+
layout: config.layout,
|
|
13519
13915
|
onVerdict(id, questions) {
|
|
13520
13916
|
client.write(encode({ type: "verdict", id, questions }));
|
|
13521
13917
|
}
|
|
@@ -13584,7 +13980,7 @@ Commands:
|
|
|
13584
13980
|
function readVersion() {
|
|
13585
13981
|
const pkgPath = join13(installRoot(), "package.json");
|
|
13586
13982
|
try {
|
|
13587
|
-
const raw = JSON.parse(
|
|
13983
|
+
const raw = JSON.parse(readFileSync8(pkgPath, "utf-8"));
|
|
13588
13984
|
if (isRecord(raw) && typeof raw["version"] === "string") {
|
|
13589
13985
|
return raw["version"];
|
|
13590
13986
|
}
|