pair-mode 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -4
- package/dist/cli.js +474 -111
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -125,9 +125,13 @@ Run this command in the directory or any subdirectory of where you are working w
|
|
|
125
125
|
pair-mode on --web
|
|
126
126
|
```
|
|
127
127
|
|
|
128
|
-

|
|
129
129
|
|
|
130
|
-
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.
|
|
130
|
+
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.
|
|
131
|
+
|
|
132
|
+
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.
|
|
133
|
+
|
|
134
|
+

|
|
131
135
|
|
|
132
136
|
## Configuration
|
|
133
137
|
|
|
@@ -163,8 +167,9 @@ existing config with no `editor` key now opens pair mode's own pane by default.
|
|
|
163
167
|
|
|
164
168
|
`notes` and `layout` both describe position, and they are easy to confuse. `layout`
|
|
165
169
|
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.
|
|
170
|
+
controls only where a note you write renders in the pair pane — in a docked panel, or
|
|
171
|
+
anchored next to the span it annotates. Setting one does not affect the other. The web
|
|
172
|
+
view anchors every note, so it reads `layout` and ignores `notes`.
|
|
168
173
|
|
|
169
174
|
## License
|
|
170
175
|
|
package/dist/cli.js
CHANGED
|
@@ -12626,10 +12626,11 @@ var PairClient = (() => {
|
|
|
12626
12626
|
draftRange: () => draftRange,
|
|
12627
12627
|
escapeHtml: () => escapeHtml,
|
|
12628
12628
|
hiddenRows: () => hiddenRows,
|
|
12629
|
+
inlineHtml: () => inlineHtml,
|
|
12629
12630
|
labelOf: () => labelOf,
|
|
12631
|
+
marginNotesHtml: () => marginNotesHtml,
|
|
12630
12632
|
markRange: () => markRange,
|
|
12631
12633
|
marksFor: () => marksFor,
|
|
12632
|
-
notesHtml: () => notesHtml,
|
|
12633
12634
|
numberOf: () => numberOf,
|
|
12634
12635
|
paintCell: () => paintCell,
|
|
12635
12636
|
quoteOf: () => quoteOf,
|
|
@@ -12661,6 +12662,9 @@ var PairClient = (() => {
|
|
|
12661
12662
|
function tokensOf(row, pane) {
|
|
12662
12663
|
return pane === "right" ? row.rightTokens : row.leftTokens;
|
|
12663
12664
|
}
|
|
12665
|
+
function other(pane) {
|
|
12666
|
+
return pane === "right" ? "left" : "right";
|
|
12667
|
+
}
|
|
12664
12668
|
function numberOf(row, pane) {
|
|
12665
12669
|
return pane === "right" ? row.rightNumber : row.leftNumber;
|
|
12666
12670
|
}
|
|
@@ -12719,25 +12723,36 @@ var PairClient = (() => {
|
|
|
12719
12723
|
});
|
|
12720
12724
|
return hidden;
|
|
12721
12725
|
}
|
|
12726
|
+
function signOf(kind, pane) {
|
|
12727
|
+
if (pane === "left") {
|
|
12728
|
+
return kind === "del" || kind === "replace" ? "-" : " ";
|
|
12729
|
+
}
|
|
12730
|
+
return kind === "add" || kind === "replace" ? "+" : " ";
|
|
12731
|
+
}
|
|
12722
12732
|
function cellHtml(notes, row, rowIndex, pane) {
|
|
12723
12733
|
const text = textOf(row, pane);
|
|
12724
12734
|
const body = paintCell(text, tokensOf(row, pane), marksFor(notes, rowIndex, pane, text.length));
|
|
12725
|
-
|
|
12735
|
+
const number = numberOf(row, pane);
|
|
12736
|
+
const void_ = number === null ? " void" : "";
|
|
12737
|
+
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
12738
|
}
|
|
12727
12739
|
function foldsByStart(review) {
|
|
12728
12740
|
return new Map(review.folds.map((fold, foldIndex) => [fold.start, foldIndex]));
|
|
12729
12741
|
}
|
|
12742
|
+
var SPLIT_COLUMNS = 6;
|
|
12743
|
+
var INLINE_COLUMNS = 4;
|
|
12744
|
+
function foldHtml(foldIndex, count, columns) {
|
|
12745
|
+
return '<tr class="fold" data-fold="' + foldIndex + '"><td colspan="' + columns + '">' + count + " unchanged lines</td></tr>";
|
|
12746
|
+
}
|
|
12730
12747
|
function tableHtml(review, notes, expanded) {
|
|
12731
12748
|
const hidden = hiddenRows(review, expanded);
|
|
12732
12749
|
const byStart = foldsByStart(review);
|
|
12733
|
-
const parts = [
|
|
12750
|
+
const parts = ['<table class="split">'];
|
|
12734
12751
|
review.rows.forEach((row, index) => {
|
|
12735
12752
|
const foldIndex = byStart.get(index);
|
|
12736
12753
|
const fold = foldIndex === void 0 ? void 0 : review.folds[foldIndex];
|
|
12737
12754
|
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
|
-
);
|
|
12755
|
+
parts.push(foldHtml(foldIndex, fold.count, SPLIT_COLUMNS));
|
|
12741
12756
|
}
|
|
12742
12757
|
if (hidden.has(index)) {
|
|
12743
12758
|
return;
|
|
@@ -12745,6 +12760,77 @@ var PairClient = (() => {
|
|
|
12745
12760
|
parts.push(
|
|
12746
12761
|
'<tr class="' + escapeHtml(row.kind) + '">' + cellHtml(notes, row, index, "left") + cellHtml(notes, row, index, "right") + "</tr>"
|
|
12747
12762
|
);
|
|
12763
|
+
parts.push(threadsAt(review, notes, index, SPLIT_COLUMNS));
|
|
12764
|
+
});
|
|
12765
|
+
parts.push("</table>");
|
|
12766
|
+
return parts.join("");
|
|
12767
|
+
}
|
|
12768
|
+
function inlineLines(kind) {
|
|
12769
|
+
if (kind === "del") {
|
|
12770
|
+
return [{ pane: "left", sign: "-", kind: "del" }];
|
|
12771
|
+
}
|
|
12772
|
+
if (kind === "add") {
|
|
12773
|
+
return [{ pane: "right", sign: "+", kind: "add" }];
|
|
12774
|
+
}
|
|
12775
|
+
if (kind === "replace") {
|
|
12776
|
+
return [
|
|
12777
|
+
{ pane: "left", sign: "-", kind: "del" },
|
|
12778
|
+
{ pane: "right", sign: "+", kind: "add" }
|
|
12779
|
+
];
|
|
12780
|
+
}
|
|
12781
|
+
return [{ pane: "right", sign: " ", kind: "context" }];
|
|
12782
|
+
}
|
|
12783
|
+
function inlineMarks(notes, rowIndex, kind, pane, length) {
|
|
12784
|
+
if (kind !== "context") {
|
|
12785
|
+
return marksFor(notes, rowIndex, pane, length);
|
|
12786
|
+
}
|
|
12787
|
+
return [
|
|
12788
|
+
...marksFor(notes, rowIndex, "left", length),
|
|
12789
|
+
...marksFor(notes, rowIndex, "right", length)
|
|
12790
|
+
];
|
|
12791
|
+
}
|
|
12792
|
+
function anchorsHere(line, panes, notePane) {
|
|
12793
|
+
return line.pane === notePane || !panes.includes(notePane);
|
|
12794
|
+
}
|
|
12795
|
+
function anchorsAt(notes, rowIndex, line, panes) {
|
|
12796
|
+
return notes.map((note, index) => ({ note, index })).filter(
|
|
12797
|
+
(entry) => entry.note.startRow === rowIndex && anchorsHere(line, panes, entry.note.pane)
|
|
12798
|
+
).map((entry) => entry.index);
|
|
12799
|
+
}
|
|
12800
|
+
function inlineRowHtml(review, notes, rowIndex, line, panes) {
|
|
12801
|
+
const row = review.rows[rowIndex];
|
|
12802
|
+
if (row === void 0) {
|
|
12803
|
+
return "";
|
|
12804
|
+
}
|
|
12805
|
+
const text = textOf(row, line.pane);
|
|
12806
|
+
const body = paintCell(
|
|
12807
|
+
text,
|
|
12808
|
+
tokensOf(row, line.pane),
|
|
12809
|
+
inlineMarks(notes, rowIndex, row.kind, line.pane, text.length)
|
|
12810
|
+
);
|
|
12811
|
+
const anchors = anchorsAt(notes, rowIndex, line, panes);
|
|
12812
|
+
const leftNumber = line.pane === "left" ? row.leftNumber ?? "" : "";
|
|
12813
|
+
const rightNumber = line.pane === "right" ? row.rightNumber ?? "" : "";
|
|
12814
|
+
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>";
|
|
12815
|
+
}
|
|
12816
|
+
function inlineHtml(review, notes, expanded) {
|
|
12817
|
+
const hidden = hiddenRows(review, expanded);
|
|
12818
|
+
const byStart = foldsByStart(review);
|
|
12819
|
+
const parts = ['<table class="inline">'];
|
|
12820
|
+
review.rows.forEach((row, index) => {
|
|
12821
|
+
const foldIndex = byStart.get(index);
|
|
12822
|
+
const fold = foldIndex === void 0 ? void 0 : review.folds[foldIndex];
|
|
12823
|
+
if (foldIndex !== void 0 && fold !== void 0 && !expanded.has(foldIndex)) {
|
|
12824
|
+
parts.push(foldHtml(foldIndex, fold.count, INLINE_COLUMNS));
|
|
12825
|
+
}
|
|
12826
|
+
if (hidden.has(index)) {
|
|
12827
|
+
return;
|
|
12828
|
+
}
|
|
12829
|
+
const lines = inlineLines(row.kind);
|
|
12830
|
+
const panes = lines.map((line) => line.pane);
|
|
12831
|
+
lines.forEach((line) => {
|
|
12832
|
+
parts.push(inlineRowHtml(review, notes, index, line, panes));
|
|
12833
|
+
});
|
|
12748
12834
|
});
|
|
12749
12835
|
parts.push("</table>");
|
|
12750
12836
|
return parts.join("");
|
|
@@ -12764,13 +12850,21 @@ var PairClient = (() => {
|
|
|
12764
12850
|
if (startRow === void 0 || endRow === void 0) {
|
|
12765
12851
|
return "";
|
|
12766
12852
|
}
|
|
12767
|
-
const start = numberOf(startRow, note.pane);
|
|
12853
|
+
const start = numberOf(startRow, note.pane) ?? numberOf(startRow, other(note.pane));
|
|
12768
12854
|
const end = numberOf(endRow, note.pane);
|
|
12855
|
+
if (start === null) {
|
|
12856
|
+
return "";
|
|
12857
|
+
}
|
|
12769
12858
|
return start === end || end === null ? "L" + start : "L" + start + "-" + end;
|
|
12770
12859
|
}
|
|
12771
|
-
function
|
|
12860
|
+
function threadsAt(review, notes, rowIndex, columns) {
|
|
12861
|
+
return notes.map((note, index) => ({ note, index })).filter((entry) => entry.note.endRow === rowIndex).map(
|
|
12862
|
+
(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>"
|
|
12863
|
+
).join("");
|
|
12864
|
+
}
|
|
12865
|
+
function marginNotesHtml(review, notes) {
|
|
12772
12866
|
return notes.map(
|
|
12773
|
-
(note, index) => '<div class="note"><button class="drop" data-drop="' + index + '">×</button><div class="
|
|
12867
|
+
(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
12868
|
).join("");
|
|
12775
12869
|
}
|
|
12776
12870
|
function sortedNotes(notes) {
|
|
@@ -12810,157 +12904,237 @@ var PairClient = (() => {
|
|
|
12810
12904
|
var STYLE = `
|
|
12811
12905
|
:root {
|
|
12812
12906
|
--bg: #0d1117;
|
|
12813
|
-
--
|
|
12814
|
-
--
|
|
12907
|
+
--chrome: #10141a;
|
|
12908
|
+
--rule: #1e242c;
|
|
12815
12909
|
--text: #c9d1d9;
|
|
12816
12910
|
--muted: ${theme.fold};
|
|
12817
|
-
--
|
|
12911
|
+
--dim: #39424d;
|
|
12818
12912
|
--add-bar: ${theme.addBar};
|
|
12819
12913
|
--del-bar: ${theme.delBar};
|
|
12820
|
-
--add-tint: rgba(63, 185, 80, 0.
|
|
12821
|
-
--del-tint: rgba(248, 81, 73, 0.
|
|
12914
|
+
--add-tint: rgba(63, 185, 80, 0.08);
|
|
12915
|
+
--del-tint: rgba(248, 81, 73, 0.08);
|
|
12822
12916
|
--note: ${theme.note};
|
|
12823
|
-
--note-tint: rgba(210, 168, 255, 0.
|
|
12917
|
+
--note-tint: rgba(210, 168, 255, 0.16);
|
|
12918
|
+
--amber: ${theme.chrome};
|
|
12919
|
+
--mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
12824
12920
|
}
|
|
12825
12921
|
|
|
12826
12922
|
* { box-sizing: border-box; }
|
|
12827
12923
|
|
|
12828
12924
|
body {
|
|
12829
12925
|
margin: 0;
|
|
12926
|
+
min-height: 100vh;
|
|
12927
|
+
display: flex;
|
|
12928
|
+
flex-direction: column;
|
|
12830
12929
|
background: var(--bg);
|
|
12831
12930
|
color: var(--text);
|
|
12832
|
-
font: 13px/1.6
|
|
12931
|
+
font: 13px/1.6 var(--mono);
|
|
12932
|
+
-webkit-font-smoothing: antialiased;
|
|
12833
12933
|
}
|
|
12834
12934
|
|
|
12935
|
+
/* ---- chrome ---- */
|
|
12936
|
+
|
|
12835
12937
|
header {
|
|
12836
12938
|
position: sticky;
|
|
12837
12939
|
top: 0;
|
|
12838
|
-
z-index:
|
|
12940
|
+
z-index: 6;
|
|
12839
12941
|
display: flex;
|
|
12840
|
-
align-items:
|
|
12841
|
-
gap: 12px;
|
|
12842
|
-
padding: 9px 14px;
|
|
12942
|
+
align-items: stretch;
|
|
12843
12943
|
background: var(--chrome);
|
|
12844
|
-
|
|
12845
|
-
font-
|
|
12944
|
+
border-bottom: 1px solid var(--rule);
|
|
12945
|
+
font-size: 12px;
|
|
12846
12946
|
}
|
|
12847
12947
|
|
|
12848
|
-
header .
|
|
12948
|
+
header .seg { padding: 6px 13px; white-space: nowrap; }
|
|
12949
|
+
header .mode { background: var(--amber); color: ${theme.statusText}; font-weight: 700; letter-spacing: 0.1em; }
|
|
12950
|
+
header .path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; color: var(--muted); }
|
|
12951
|
+
header .plus { color: var(--add-bar); }
|
|
12952
|
+
header .minus { color: var(--del-bar); }
|
|
12953
|
+
header .tally { color: var(--note); }
|
|
12849
12954
|
|
|
12850
|
-
|
|
12851
|
-
|
|
12852
|
-
|
|
12853
|
-
|
|
12854
|
-
|
|
12855
|
-
|
|
12856
|
-
|
|
12955
|
+
footer {
|
|
12956
|
+
position: sticky;
|
|
12957
|
+
bottom: 0;
|
|
12958
|
+
z-index: 6;
|
|
12959
|
+
display: flex;
|
|
12960
|
+
align-items: stretch;
|
|
12961
|
+
background: var(--chrome);
|
|
12962
|
+
border-top: 1px solid var(--rule);
|
|
12963
|
+
font-size: 11.5px;
|
|
12964
|
+
}
|
|
12965
|
+
|
|
12966
|
+
footer .seg { padding: 6px 12px; color: var(--muted); white-space: nowrap; }
|
|
12967
|
+
footer .seg .key { color: var(--text); font-weight: 700; }
|
|
12968
|
+
footer .fill { flex: 1; }
|
|
12969
|
+
|
|
12970
|
+
footer .swap { display: flex; }
|
|
12971
|
+
|
|
12972
|
+
footer .swap button {
|
|
12973
|
+
font: 400 11.5px/1.6 var(--mono);
|
|
12974
|
+
padding: 6px 11px;
|
|
12975
|
+
border: 0;
|
|
12976
|
+
background: none;
|
|
12977
|
+
color: var(--muted);
|
|
12978
|
+
cursor: pointer;
|
|
12979
|
+
}
|
|
12980
|
+
|
|
12981
|
+
footer .swap button:hover { color: var(--text); }
|
|
12982
|
+
footer .swap button[aria-pressed="true"] { background: var(--note-tint); color: var(--note); font-weight: 700; }
|
|
12983
|
+
|
|
12984
|
+
footer .act {
|
|
12985
|
+
font: 700 11.5px/1.6 var(--mono);
|
|
12986
|
+
padding: 6px 14px;
|
|
12987
|
+
border: 0;
|
|
12857
12988
|
cursor: pointer;
|
|
12858
12989
|
}
|
|
12859
12990
|
|
|
12860
|
-
|
|
12991
|
+
footer .act:disabled { opacity: 0.35; cursor: default; }
|
|
12992
|
+
footer .act.ok { background: rgba(63, 185, 80, 0.16); color: var(--add-bar); }
|
|
12993
|
+
footer .act.ok:enabled:hover { background: var(--add-bar); color: #06140a; }
|
|
12994
|
+
footer .act.send { background: var(--note); color: #170f1e; }
|
|
12995
|
+
footer .act.send:enabled:hover { filter: brightness(1.12); }
|
|
12996
|
+
|
|
12997
|
+
button:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--note); outline-offset: -2px; }
|
|
12998
|
+
|
|
12999
|
+
/* ---- diff, both layouts ---- */
|
|
12861
13000
|
|
|
12862
|
-
main { display: flex; align-items: flex-start; }
|
|
13001
|
+
main { position: relative; flex: 1; display: flex; align-items: flex-start; }
|
|
12863
13002
|
|
|
12864
13003
|
#diff { flex: 1; min-width: 0; overflow-x: auto; }
|
|
12865
13004
|
|
|
12866
13005
|
table { width: 100%; border-collapse: collapse; }
|
|
12867
13006
|
|
|
12868
|
-
td { padding: 0
|
|
13007
|
+
td { padding: 0; vertical-align: top; white-space: pre; }
|
|
12869
13008
|
|
|
12870
13009
|
td.num {
|
|
12871
13010
|
width: 1%;
|
|
12872
|
-
padding: 0
|
|
13011
|
+
padding: 0 8px;
|
|
12873
13012
|
text-align: right;
|
|
12874
|
-
color: var(--
|
|
13013
|
+
color: var(--dim);
|
|
13014
|
+
font-variant-numeric: tabular-nums;
|
|
12875
13015
|
user-select: none;
|
|
12876
13016
|
}
|
|
12877
13017
|
|
|
12878
|
-
td.
|
|
13018
|
+
td.sign { width: 1%; padding: 0 6px 0 0; text-align: center; color: var(--dim); user-select: none; }
|
|
13019
|
+
td.code { cursor: text; padding-right: 18px; }
|
|
12879
13020
|
|
|
12880
|
-
|
|
12881
|
-
|
|
13021
|
+
table.split td.code { width: 50%; }
|
|
13022
|
+
table.inline td.code { width: auto; }
|
|
12882
13023
|
|
|
12883
|
-
tr.add td.
|
|
12884
|
-
tr.del td.
|
|
13024
|
+
tr.add td.sign.right, tr.replace td.sign.right { color: var(--add-bar); }
|
|
13025
|
+
tr.del td.sign.left, tr.replace td.sign.left { color: var(--del-bar); }
|
|
13026
|
+
tr.add td.sign, tr.add td.code { background: var(--add-tint); }
|
|
13027
|
+
tr.del td.sign, tr.del td.code { background: var(--del-tint); }
|
|
13028
|
+
tr.replace td.sign.right, tr.replace td.code.right { background: var(--add-tint); }
|
|
13029
|
+
tr.replace td.sign.left, tr.replace td.code.left { background: var(--del-tint); }
|
|
12885
13030
|
|
|
12886
|
-
td.
|
|
13031
|
+
tr.add td.void, tr.del td.void, tr.replace td.void {
|
|
13032
|
+
background: repeating-linear-gradient(-45deg, transparent 0 5px, rgba(255, 255, 255, 0.022) 5px 10px);
|
|
13033
|
+
cursor: default;
|
|
13034
|
+
}
|
|
12887
13035
|
|
|
12888
13036
|
::selection { background: rgba(88, 166, 255, 0.35); }
|
|
12889
13037
|
|
|
12890
13038
|
mark.noted {
|
|
12891
13039
|
background: var(--note-tint);
|
|
12892
|
-
border-bottom: 2px solid var(--note);
|
|
12893
13040
|
color: inherit;
|
|
12894
|
-
|
|
13041
|
+
box-shadow: inset 0 -2px 0 var(--note);
|
|
12895
13042
|
}
|
|
12896
13043
|
|
|
12897
13044
|
tr.fold td {
|
|
12898
|
-
padding:
|
|
12899
|
-
color: var(--
|
|
12900
|
-
background:
|
|
13045
|
+
padding: 3px 0 3px 18px;
|
|
13046
|
+
color: var(--dim);
|
|
13047
|
+
background: rgba(255, 255, 255, 0.016);
|
|
13048
|
+
border-top: 1px solid var(--rule);
|
|
13049
|
+
border-bottom: 1px solid var(--rule);
|
|
12901
13050
|
cursor: pointer;
|
|
12902
|
-
text-align: center;
|
|
12903
13051
|
user-select: none;
|
|
12904
13052
|
}
|
|
12905
13053
|
|
|
12906
13054
|
tr.fold td:hover { color: var(--text); }
|
|
12907
13055
|
|
|
12908
|
-
|
|
12909
|
-
|
|
12910
|
-
|
|
12911
|
-
|
|
12912
|
-
|
|
12913
|
-
|
|
12914
|
-
|
|
12915
|
-
padding: 12px;
|
|
12916
|
-
border-left: 1px solid var(--line);
|
|
12917
|
-
background: var(--panel);
|
|
13056
|
+
/* ---- notes threaded under a split row ---- */
|
|
13057
|
+
|
|
13058
|
+
tr.thread td {
|
|
13059
|
+
padding: 9px 22px 10px 60px;
|
|
13060
|
+
border-left: 2px solid var(--note);
|
|
13061
|
+
background: rgba(210, 168, 255, 0.05);
|
|
13062
|
+
white-space: normal;
|
|
12918
13063
|
}
|
|
12919
13064
|
|
|
12920
|
-
|
|
12921
|
-
|
|
12922
|
-
font-size:
|
|
12923
|
-
letter-spacing: 0.
|
|
12924
|
-
color: var(--muted);
|
|
13065
|
+
tr.thread .at {
|
|
13066
|
+
font-family: var(--mono);
|
|
13067
|
+
font-size: 10.5px;
|
|
13068
|
+
letter-spacing: 0.11em;
|
|
12925
13069
|
text-transform: uppercase;
|
|
13070
|
+
color: var(--note);
|
|
13071
|
+
}
|
|
13072
|
+
|
|
13073
|
+
tr.thread p, .note p {
|
|
13074
|
+
margin: 3px 0 0;
|
|
13075
|
+
max-width: 62ch;
|
|
13076
|
+
font-size: 13px;
|
|
13077
|
+
line-height: 1.5;
|
|
13078
|
+
color: #e7e2ea;
|
|
12926
13079
|
}
|
|
12927
13080
|
|
|
13081
|
+
/* ---- notes in the inline margin ---- */
|
|
13082
|
+
|
|
13083
|
+
#margin {
|
|
13084
|
+
position: relative;
|
|
13085
|
+
width: 320px;
|
|
13086
|
+
flex: none;
|
|
13087
|
+
align-self: stretch;
|
|
13088
|
+
padding: 0 20px 0 4px;
|
|
13089
|
+
}
|
|
13090
|
+
|
|
13091
|
+
#margin.hidden { display: none; }
|
|
13092
|
+
|
|
12928
13093
|
.note {
|
|
12929
|
-
|
|
12930
|
-
|
|
12931
|
-
|
|
12932
|
-
|
|
12933
|
-
|
|
13094
|
+
position: absolute;
|
|
13095
|
+
left: 4px;
|
|
13096
|
+
right: 20px;
|
|
13097
|
+
padding-left: 14px;
|
|
13098
|
+
border-left: 1px solid var(--note);
|
|
13099
|
+
}
|
|
13100
|
+
|
|
13101
|
+
.note .at {
|
|
13102
|
+
font-size: 10.5px;
|
|
13103
|
+
letter-spacing: 0.11em;
|
|
13104
|
+
text-transform: uppercase;
|
|
13105
|
+
color: var(--note);
|
|
12934
13106
|
}
|
|
12935
13107
|
|
|
12936
|
-
|
|
12937
|
-
.note .quote { margin: 3px 0; color: var(--muted); font-size: 11px; }
|
|
12938
|
-
.note .body { white-space: pre-wrap; }
|
|
13108
|
+
#leaders { position: absolute; inset: 0; pointer-events: none; z-index: 1; }
|
|
12939
13109
|
|
|
12940
|
-
.
|
|
13110
|
+
.drop {
|
|
12941
13111
|
float: right;
|
|
12942
|
-
padding: 0
|
|
13112
|
+
padding: 0 4px;
|
|
12943
13113
|
border: 0;
|
|
12944
13114
|
background: none;
|
|
12945
|
-
color: var(--
|
|
13115
|
+
color: var(--dim);
|
|
13116
|
+
font: 400 14px/1 var(--mono);
|
|
13117
|
+
cursor: pointer;
|
|
12946
13118
|
}
|
|
12947
13119
|
|
|
12948
|
-
.
|
|
13120
|
+
.drop:hover { color: var(--del-bar); }
|
|
13121
|
+
|
|
13122
|
+
/* ---- popup ---- */
|
|
12949
13123
|
|
|
12950
13124
|
#popup {
|
|
12951
13125
|
position: absolute;
|
|
12952
|
-
z-index:
|
|
13126
|
+
z-index: 8;
|
|
12953
13127
|
display: none;
|
|
12954
|
-
width:
|
|
12955
|
-
padding:
|
|
13128
|
+
width: 320px;
|
|
13129
|
+
padding: 11px;
|
|
12956
13130
|
border: 1px solid var(--note);
|
|
12957
|
-
border-radius:
|
|
12958
|
-
background: var(--
|
|
12959
|
-
box-shadow: 0
|
|
13131
|
+
border-radius: 5px;
|
|
13132
|
+
background: var(--chrome);
|
|
13133
|
+
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.6);
|
|
12960
13134
|
}
|
|
12961
13135
|
|
|
12962
13136
|
#popup .quote {
|
|
12963
|
-
margin-bottom:
|
|
13137
|
+
margin-bottom: 7px;
|
|
12964
13138
|
overflow: hidden;
|
|
12965
13139
|
color: var(--muted);
|
|
12966
13140
|
font-size: 11px;
|
|
@@ -12970,27 +13144,48 @@ aside h2 {
|
|
|
12970
13144
|
|
|
12971
13145
|
#popup .row { display: flex; gap: 8px; margin-top: 8px; }
|
|
12972
13146
|
|
|
13147
|
+
#popup button {
|
|
13148
|
+
font: 500 12px/1 var(--mono);
|
|
13149
|
+
padding: 6px 12px;
|
|
13150
|
+
border: 1px solid var(--rule);
|
|
13151
|
+
border-radius: 4px;
|
|
13152
|
+
background: none;
|
|
13153
|
+
color: var(--text);
|
|
13154
|
+
cursor: pointer;
|
|
13155
|
+
}
|
|
13156
|
+
|
|
13157
|
+
#popup button.save { background: var(--note); border-color: var(--note); color: #170f1e; font-weight: 700; }
|
|
13158
|
+
|
|
12973
13159
|
textarea {
|
|
12974
13160
|
width: 100%;
|
|
12975
|
-
min-height:
|
|
12976
|
-
padding:
|
|
12977
|
-
border: 1px solid var(--
|
|
13161
|
+
min-height: 66px;
|
|
13162
|
+
padding: 7px;
|
|
13163
|
+
border: 1px solid var(--rule);
|
|
12978
13164
|
border-radius: 4px;
|
|
12979
13165
|
background: var(--bg);
|
|
12980
13166
|
color: var(--text);
|
|
12981
|
-
font:
|
|
13167
|
+
font: 13px/1.5 var(--mono);
|
|
12982
13168
|
resize: vertical;
|
|
12983
13169
|
}
|
|
12984
13170
|
|
|
12985
|
-
.hint { color: var(--
|
|
13171
|
+
.hint { color: var(--dim); font-size: 11px; }
|
|
12986
13172
|
|
|
12987
|
-
#idle { padding:
|
|
13173
|
+
#idle { padding: 64px 24px; color: var(--dim); text-align: center; }
|
|
13174
|
+
|
|
13175
|
+
@media (max-width: 900px) {
|
|
13176
|
+
main { flex-wrap: wrap; }
|
|
13177
|
+
#leaders { display: none; }
|
|
13178
|
+
#margin { width: 100%; padding: 12px 16px; border-top: 1px solid var(--rule); }
|
|
13179
|
+
.note { position: static; margin-bottom: 12px; }
|
|
13180
|
+
}
|
|
12988
13181
|
`;
|
|
12989
13182
|
var WIRING = String.raw`
|
|
12990
13183
|
const C = PairClient;
|
|
12991
13184
|
const token = location.pathname.split("/")[2];
|
|
13185
|
+
const main = document.querySelector("main");
|
|
12992
13186
|
const diff = document.getElementById("diff");
|
|
12993
|
-
const
|
|
13187
|
+
const margin = document.getElementById("margin");
|
|
13188
|
+
const leaders = document.getElementById("leaders");
|
|
12994
13189
|
const popup = document.getElementById("popup");
|
|
12995
13190
|
const popupQuote = document.getElementById("popup-quote");
|
|
12996
13191
|
const popupText = document.getElementById("popup-text");
|
|
@@ -12999,37 +13194,151 @@ const popupCancel = document.getElementById("popup-cancel");
|
|
|
12999
13194
|
const sendButton = document.getElementById("send");
|
|
13000
13195
|
const approveButton = document.getElementById("approve");
|
|
13001
13196
|
const pathLabel = document.getElementById("path");
|
|
13197
|
+
const tallyLabel = document.getElementById("tally");
|
|
13198
|
+
const plusLabel = document.getElementById("plus");
|
|
13199
|
+
const minusLabel = document.getElementById("minus");
|
|
13002
13200
|
|
|
13003
13201
|
let review = null;
|
|
13004
13202
|
let notes = [];
|
|
13005
13203
|
let expanded = new Set();
|
|
13006
13204
|
let draft = null;
|
|
13205
|
+
let layout = document.body.dataset.layout === "inline" ? "inline" : "split";
|
|
13206
|
+
|
|
13207
|
+
function isInline() {
|
|
13208
|
+
return layout === "inline";
|
|
13209
|
+
}
|
|
13210
|
+
|
|
13211
|
+
function countKinds() {
|
|
13212
|
+
let plus = 0;
|
|
13213
|
+
let minus = 0;
|
|
13214
|
+
|
|
13215
|
+
review.rows.forEach((row) => {
|
|
13216
|
+
if (row.kind === "add" || row.kind === "replace") { plus += 1; }
|
|
13217
|
+
if (row.kind === "del" || row.kind === "replace") { minus += 1; }
|
|
13218
|
+
});
|
|
13007
13219
|
|
|
13008
|
-
|
|
13009
|
-
|
|
13010
|
-
|
|
13220
|
+
return { plus, minus };
|
|
13221
|
+
}
|
|
13222
|
+
|
|
13223
|
+
// A leader line joins a marked run to its card, so it redraws whenever either one moves.
|
|
13224
|
+
function drawLeaders() {
|
|
13225
|
+
if (!isInline() || review === null || notes.length === 0) {
|
|
13226
|
+
leaders.innerHTML = "";
|
|
13011
13227
|
return;
|
|
13012
13228
|
}
|
|
13013
13229
|
|
|
13014
|
-
|
|
13230
|
+
const base = main.getBoundingClientRect();
|
|
13231
|
+
|
|
13232
|
+
leaders.setAttribute("viewBox", "0 0 " + base.width + " " + base.height);
|
|
13233
|
+
leaders.setAttribute("width", base.width);
|
|
13234
|
+
leaders.setAttribute("height", base.height);
|
|
13235
|
+
|
|
13236
|
+
leaders.innerHTML = notes.map((_note, index) => {
|
|
13237
|
+
const row = diff.querySelector('[data-anchor~="' + index + '"]');
|
|
13238
|
+
const card = margin.querySelector('[data-card="' + index + '"]');
|
|
13239
|
+
|
|
13240
|
+
if (row === null || card === null) {
|
|
13241
|
+
return "";
|
|
13242
|
+
}
|
|
13243
|
+
|
|
13244
|
+
// The line leaves from under the marked run, so it continues that underline and crosses no glyph.
|
|
13245
|
+
const marks = row.querySelectorAll("mark.noted");
|
|
13246
|
+
const ordinal = (row.dataset.anchor ?? "").split(" ").indexOf(String(index));
|
|
13247
|
+
const source = marks[ordinal] ?? marks[marks.length - 1] ?? row;
|
|
13248
|
+
const a = source.getBoundingClientRect();
|
|
13249
|
+
const b = card.getBoundingClientRect();
|
|
13250
|
+
const x1 = a.right - base.left + 5;
|
|
13251
|
+
const y1 = a.bottom - base.top - 0.5;
|
|
13252
|
+
const x2 = b.left - base.left;
|
|
13253
|
+
const y2 = b.top - base.top;
|
|
13254
|
+
const mid = x1 + (x2 - x1) * 0.55;
|
|
13255
|
+
|
|
13256
|
+
return '<path d="M ' + x1 + ' ' + y1 + ' C ' + mid + ' ' + y1 + ', ' + mid + ' ' + y2 + ', ' + x2 + ' ' + y2 +
|
|
13257
|
+
'" fill="none" stroke="var(--note)" stroke-width="1" stroke-opacity="0.5" />' +
|
|
13258
|
+
'<circle cx="' + x1 + '" cy="' + y1 + '" r="2" fill="var(--note)" fill-opacity="0.9" />';
|
|
13259
|
+
}).join("");
|
|
13260
|
+
}
|
|
13261
|
+
|
|
13262
|
+
// Two notes on nearby lines would overlap, so each card starts below the one before it.
|
|
13263
|
+
function placeCards() {
|
|
13264
|
+
if (!isInline()) {
|
|
13265
|
+
return;
|
|
13266
|
+
}
|
|
13267
|
+
|
|
13268
|
+
const base = margin.getBoundingClientRect();
|
|
13269
|
+
let floor = 0;
|
|
13270
|
+
|
|
13271
|
+
notes.forEach((_note, index) => {
|
|
13272
|
+
const row = diff.querySelector('[data-anchor~="' + index + '"]');
|
|
13273
|
+
const card = margin.querySelector('[data-card="' + index + '"]');
|
|
13274
|
+
|
|
13275
|
+
if (row === null || card === null) {
|
|
13276
|
+
return;
|
|
13277
|
+
}
|
|
13278
|
+
|
|
13279
|
+
const top = Math.max(row.getBoundingClientRect().bottom - base.top, floor);
|
|
13280
|
+
card.style.top = top + "px";
|
|
13281
|
+
floor = top + card.offsetHeight + 18;
|
|
13282
|
+
});
|
|
13283
|
+
}
|
|
13284
|
+
|
|
13285
|
+
function renderMargin() {
|
|
13286
|
+
const wanted = isInline() && review !== null && notes.length > 0;
|
|
13287
|
+
|
|
13288
|
+
margin.classList.toggle("hidden", !wanted);
|
|
13289
|
+
margin.innerHTML = wanted ? C.marginNotesHtml(review, notes) : "";
|
|
13015
13290
|
}
|
|
13016
13291
|
|
|
13017
13292
|
function render() {
|
|
13018
13293
|
if (review === null) {
|
|
13019
13294
|
diff.innerHTML = '<div id="idle">waiting for an edit</div>';
|
|
13020
13295
|
pathLabel.textContent = "no review open";
|
|
13296
|
+
plusLabel.textContent = "";
|
|
13297
|
+
minusLabel.textContent = "";
|
|
13298
|
+
tallyLabel.textContent = "";
|
|
13021
13299
|
sendButton.disabled = true;
|
|
13022
13300
|
approveButton.disabled = true;
|
|
13023
13301
|
hidePopup();
|
|
13024
|
-
|
|
13302
|
+
renderMargin();
|
|
13303
|
+
refreshLeaders();
|
|
13025
13304
|
return;
|
|
13026
13305
|
}
|
|
13027
13306
|
|
|
13307
|
+
const counts = countKinds();
|
|
13308
|
+
|
|
13028
13309
|
pathLabel.textContent = review.path;
|
|
13310
|
+
plusLabel.textContent = "+" + counts.plus;
|
|
13311
|
+
minusLabel.textContent = "−" + counts.minus;
|
|
13312
|
+
tallyLabel.textContent = notes.length === 0 ? "" : notes.length + (notes.length === 1 ? " note" : " notes");
|
|
13029
13313
|
sendButton.disabled = notes.length === 0;
|
|
13314
|
+
sendButton.textContent = "^S send" + (notes.length === 0 ? "" : " " + notes.length);
|
|
13030
13315
|
approveButton.disabled = false;
|
|
13031
|
-
|
|
13032
|
-
|
|
13316
|
+
|
|
13317
|
+
diff.innerHTML = isInline()
|
|
13318
|
+
? C.inlineHtml(review, notes, expanded)
|
|
13319
|
+
: C.tableHtml(review, notes, expanded);
|
|
13320
|
+
|
|
13321
|
+
renderMargin();
|
|
13322
|
+
refreshLeaders();
|
|
13323
|
+
}
|
|
13324
|
+
|
|
13325
|
+
// A card needs its final height before anything measures it, so the placement waits for the paint.
|
|
13326
|
+
function refreshLeaders() {
|
|
13327
|
+
requestAnimationFrame(() => {
|
|
13328
|
+
placeCards();
|
|
13329
|
+
drawLeaders();
|
|
13330
|
+
});
|
|
13331
|
+
}
|
|
13332
|
+
|
|
13333
|
+
function setLayout(next) {
|
|
13334
|
+
layout = next;
|
|
13335
|
+
|
|
13336
|
+
document.querySelectorAll(".swap button").forEach((button) => {
|
|
13337
|
+
button.setAttribute("aria-pressed", String(button.dataset.layout === next));
|
|
13338
|
+
});
|
|
13339
|
+
|
|
13340
|
+
hidePopup();
|
|
13341
|
+
render();
|
|
13033
13342
|
}
|
|
13034
13343
|
|
|
13035
13344
|
function cellOf(node) {
|
|
@@ -13063,9 +13372,11 @@ function readSelection() {
|
|
|
13063
13372
|
return span === null ? null : { span, rect: range.getBoundingClientRect() };
|
|
13064
13373
|
}
|
|
13065
13374
|
|
|
13375
|
+
// A hidden textarea still holds focus, and the key handler ignores every key while it does.
|
|
13066
13376
|
function hidePopup() {
|
|
13067
13377
|
popup.style.display = "none";
|
|
13068
13378
|
popupText.value = "";
|
|
13379
|
+
popupText.blur();
|
|
13069
13380
|
draft = null;
|
|
13070
13381
|
}
|
|
13071
13382
|
|
|
@@ -13074,7 +13385,7 @@ function showPopup(selected) {
|
|
|
13074
13385
|
popupQuote.textContent = C.quoteOf(review, selected.span) || "(whole line)";
|
|
13075
13386
|
|
|
13076
13387
|
const top = window.scrollY + selected.rect.bottom + 8;
|
|
13077
|
-
const left = Math.max(8, Math.min(window.scrollX + selected.rect.left, window.innerWidth -
|
|
13388
|
+
const left = Math.max(8, Math.min(window.scrollX + selected.rect.left, window.innerWidth - 340));
|
|
13078
13389
|
|
|
13079
13390
|
popup.style.top = top + "px";
|
|
13080
13391
|
popup.style.left = left + "px";
|
|
@@ -13093,7 +13404,7 @@ diff.addEventListener("mouseup", () => {
|
|
|
13093
13404
|
});
|
|
13094
13405
|
|
|
13095
13406
|
diff.addEventListener("click", (event) => {
|
|
13096
|
-
const fold = event.target.closest("[data-fold]");
|
|
13407
|
+
const fold = event.target instanceof Element ? event.target.closest("[data-fold]") : null;
|
|
13097
13408
|
|
|
13098
13409
|
if (fold === null) {
|
|
13099
13410
|
return;
|
|
@@ -13131,8 +13442,8 @@ popupText.addEventListener("keydown", (event) => {
|
|
|
13131
13442
|
}
|
|
13132
13443
|
});
|
|
13133
13444
|
|
|
13134
|
-
|
|
13135
|
-
const drop = event.target.closest("[data-drop]");
|
|
13445
|
+
document.addEventListener("click", (event) => {
|
|
13446
|
+
const drop = event.target instanceof Element ? event.target.closest("[data-drop]") : null;
|
|
13136
13447
|
|
|
13137
13448
|
if (drop === null) {
|
|
13138
13449
|
return;
|
|
@@ -13143,6 +13454,44 @@ notesPanel.addEventListener("click", (event) => {
|
|
|
13143
13454
|
render();
|
|
13144
13455
|
});
|
|
13145
13456
|
|
|
13457
|
+
document.querySelectorAll(".swap button").forEach((button) => {
|
|
13458
|
+
button.addEventListener("click", () => setLayout(button.dataset.layout));
|
|
13459
|
+
});
|
|
13460
|
+
|
|
13461
|
+
// The pane binds u to the same swap, so the browser answers the key a pair mode user already knows.
|
|
13462
|
+
document.addEventListener("keydown", (event) => {
|
|
13463
|
+
if (event.target === popupText || event.metaKey || event.ctrlKey || event.altKey) {
|
|
13464
|
+
return;
|
|
13465
|
+
}
|
|
13466
|
+
|
|
13467
|
+
if (event.key === "u") {
|
|
13468
|
+
setLayout(isInline() ? "split" : "inline");
|
|
13469
|
+
}
|
|
13470
|
+
});
|
|
13471
|
+
|
|
13472
|
+
// The footer names ^S and ^Q, so both keys act rather than reaching the browser.
|
|
13473
|
+
document.addEventListener("keydown", (event) => {
|
|
13474
|
+
if (!event.ctrlKey || event.metaKey || event.altKey || review === null) {
|
|
13475
|
+
return;
|
|
13476
|
+
}
|
|
13477
|
+
|
|
13478
|
+
if (event.key === "s" && notes.length > 0) {
|
|
13479
|
+
event.preventDefault();
|
|
13480
|
+
post({ id: review.id, notes });
|
|
13481
|
+
return;
|
|
13482
|
+
}
|
|
13483
|
+
|
|
13484
|
+
if (event.key === "q") {
|
|
13485
|
+
event.preventDefault();
|
|
13486
|
+
post({ id: review.id, notes: [] });
|
|
13487
|
+
}
|
|
13488
|
+
});
|
|
13489
|
+
|
|
13490
|
+
window.addEventListener("resize", () => {
|
|
13491
|
+
placeCards();
|
|
13492
|
+
drawLeaders();
|
|
13493
|
+
});
|
|
13494
|
+
|
|
13146
13495
|
function warn(message) {
|
|
13147
13496
|
pathLabel.textContent = message;
|
|
13148
13497
|
}
|
|
@@ -13211,7 +13560,10 @@ function inlineScript(source) {
|
|
|
13211
13560
|
}
|
|
13212
13561
|
var SCRIPT = inlineScript(`${CLIENT_BUNDLE}
|
|
13213
13562
|
${WIRING}`);
|
|
13214
|
-
function
|
|
13563
|
+
function pressed(layout, wanted) {
|
|
13564
|
+
return layout === wanted ? "true" : "false";
|
|
13565
|
+
}
|
|
13566
|
+
function renderPage(layout = "split") {
|
|
13215
13567
|
return `<!doctype html>
|
|
13216
13568
|
<html lang="en">
|
|
13217
13569
|
<head>
|
|
@@ -13220,25 +13572,35 @@ function renderPage() {
|
|
|
13220
13572
|
<title>pair mode</title>
|
|
13221
13573
|
<style>${STYLE}</style>
|
|
13222
13574
|
</head>
|
|
13223
|
-
<body>
|
|
13575
|
+
<body data-layout="${layout}">
|
|
13224
13576
|
<header>
|
|
13225
|
-
<span
|
|
13226
|
-
<span class="path" id="path">no review open</span>
|
|
13227
|
-
<
|
|
13228
|
-
<
|
|
13577
|
+
<span class="seg mode">PAIR</span>
|
|
13578
|
+
<span class="seg path" id="path">no review open</span>
|
|
13579
|
+
<span class="seg plus" id="plus"></span>
|
|
13580
|
+
<span class="seg minus" id="minus"></span>
|
|
13581
|
+
<span class="seg tally" id="tally"></span>
|
|
13229
13582
|
</header>
|
|
13230
13583
|
<main>
|
|
13584
|
+
<svg id="leaders" aria-hidden="true"></svg>
|
|
13231
13585
|
<div id="diff"></div>
|
|
13232
|
-
<
|
|
13233
|
-
<h2>Notes</h2>
|
|
13234
|
-
<div id="notes"></div>
|
|
13235
|
-
</aside>
|
|
13586
|
+
<div id="margin"></div>
|
|
13236
13587
|
</main>
|
|
13588
|
+
<footer>
|
|
13589
|
+
<span class="seg">drag to <span class="key">note</span></span>
|
|
13590
|
+
<span class="seg">click a fold to <span class="key">open</span></span>
|
|
13591
|
+
<span class="swap" role="group" aria-label="Layout">
|
|
13592
|
+
<button data-layout="inline" aria-pressed="${pressed(layout, "inline")}">u inline</button>
|
|
13593
|
+
<button data-layout="split" aria-pressed="${pressed(layout, "split")}">u split</button>
|
|
13594
|
+
</span>
|
|
13595
|
+
<span class="fill"></span>
|
|
13596
|
+
<button class="act ok" id="approve">^Q approve</button>
|
|
13597
|
+
<button class="act send" id="send">^S send</button>
|
|
13598
|
+
</footer>
|
|
13237
13599
|
<div id="popup">
|
|
13238
13600
|
<div class="quote" id="popup-quote"></div>
|
|
13239
13601
|
<textarea id="popup-text" placeholder="What do you want to ask?"></textarea>
|
|
13240
13602
|
<div class="row">
|
|
13241
|
-
<button id="popup-save">Add note</button>
|
|
13603
|
+
<button class="save" id="popup-save">Add note</button>
|
|
13242
13604
|
<button id="popup-cancel">Cancel</button>
|
|
13243
13605
|
</div>
|
|
13244
13606
|
<div class="hint">Enter saves. Shift-Enter makes a new line.</div>
|
|
@@ -13416,7 +13778,7 @@ data: ${data}
|
|
|
13416
13778
|
const url = request.url ?? "";
|
|
13417
13779
|
if (url === base && request.method === "GET") {
|
|
13418
13780
|
response.writeHead(OK, { "content-type": "text/html; charset=utf-8" });
|
|
13419
|
-
response.end(renderPage());
|
|
13781
|
+
response.end(renderPage(options.layout));
|
|
13420
13782
|
return;
|
|
13421
13783
|
}
|
|
13422
13784
|
if (url === `${base}/events` && request.method === "GET") {
|
|
@@ -13516,6 +13878,7 @@ async function startWebWatch(options, config) {
|
|
|
13516
13878
|
const web = await startWebServer({
|
|
13517
13879
|
port: options.port,
|
|
13518
13880
|
token: options.token,
|
|
13881
|
+
layout: config.layout,
|
|
13519
13882
|
onVerdict(id, questions) {
|
|
13520
13883
|
client.write(encode({ type: "verdict", id, questions }));
|
|
13521
13884
|
}
|