@cardstack/boxel-cli 0.11.0-unstable.2 → 0.11.0-unstable.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/bundled-realms/pretui/components/agent-mark.gts +98 -0
  2. package/bundled-realms/pretui/components/agent-question.gts +340 -37
  3. package/bundled-realms/pretui/components/answer-footer.gts +249 -0
  4. package/bundled-realms/pretui/components/card-embed.gts +523 -0
  5. package/bundled-realms/pretui/components/card-pills.gts +725 -0
  6. package/bundled-realms/pretui/components/code-block.gts +87 -45
  7. package/bundled-realms/pretui/components/collapsed-turn.gts +133 -12
  8. package/bundled-realms/pretui/components/compaction-chip.gts +81 -67
  9. package/bundled-realms/pretui/components/composer-cap.gts +516 -0
  10. package/bundled-realms/pretui/components/context-ring.gts +108 -0
  11. package/bundled-realms/pretui/components/diff-block.gts +1002 -31
  12. package/bundled-realms/pretui/components/diff-stat.gts +102 -0
  13. package/bundled-realms/pretui/components/jump-chip.gts +86 -53
  14. package/bundled-realms/pretui/components/markdown-stream.gts +799 -0
  15. package/bundled-realms/pretui/components/markdown-surface.gts +71 -0
  16. package/bundled-realms/pretui/components/plan-mark.gts +147 -0
  17. package/bundled-realms/pretui/components/prose.gts +185 -8
  18. package/bundled-realms/pretui/components/prose.md +6 -8
  19. package/bundled-realms/pretui/components/quick-look.gts +377 -0
  20. package/bundled-realms/pretui/components/reactions.gts +136 -0
  21. package/bundled-realms/pretui/components/recommendation.gts +67 -36
  22. package/bundled-realms/pretui/components/result-card.gts +58 -22
  23. package/bundled-realms/pretui/components/segmented-control.gts +13 -8
  24. package/bundled-realms/pretui/components/session-prep.gts +12 -11
  25. package/bundled-realms/pretui/components/sliding-highlight.gts +8 -3
  26. package/bundled-realms/pretui/components/speaker-label.gts +132 -0
  27. package/bundled-realms/pretui/components/state-glyph.gts +121 -0
  28. package/bundled-realms/pretui/components/step-list.gts +62 -60
  29. package/bundled-realms/pretui/components/stick-to-bottom.gts +1150 -0
  30. package/bundled-realms/pretui/components/system-line.gts +143 -0
  31. package/bundled-realms/pretui/components/task-checklist.gts +184 -0
  32. package/bundled-realms/pretui/components/thinking.gts +63 -94
  33. package/bundled-realms/pretui/components/tool-change-table.gts +285 -0
  34. package/bundled-realms/pretui/components/tool-gate.gts +462 -0
  35. package/bundled-realms/pretui/components/tool-group.gts +920 -0
  36. package/bundled-realms/pretui/components/tool-icon.ts +64 -0
  37. package/bundled-realms/pretui/components/tool-meta.gts +82 -0
  38. package/bundled-realms/pretui/components/tool-plan.gts +197 -0
  39. package/bundled-realms/pretui/components/tool-row.gts +2087 -0
  40. package/bundled-realms/pretui/components/turn-changes.gts +227 -0
  41. package/bundled-realms/pretui/components/user-bubble.gts +575 -0
  42. package/bundled-realms/pretui/components/work-item.gts +185 -48
  43. package/bundled-realms/pretui/docs/scroll-anchoring-protocol.md +136 -0
  44. package/bundled-realms/pretui/internal/markdown-projection.ts +438 -0
  45. package/bundled-realms/pretui/internal/mount-queue.ts +79 -0
  46. package/bundled-realms/pretui/internal/tool-phrases.ts +168 -0
  47. package/bundled-realms/pretui/package.json +2 -1
  48. package/bundled-test-harness/assets/{ai-module-creation-test-beRNwH-e.js → ai-module-creation-test-B6FQKqIl.js} +1 -1
  49. package/bundled-test-harness/assets/{app-CtezUCip.js → app-KpPqdyX3.js} +4 -4
  50. package/bundled-test-harness/assets/{binary-upload-dedupe-test-bI8T0QLl.js → binary-upload-dedupe-test-DN00y1tM.js} +1 -1
  51. package/bundled-test-harness/assets/{can-invoke-test-DVaU_yRT.js → can-invoke-test-Bl-EUFM3.js} +1 -1
  52. package/bundled-test-harness/assets/{card-chooser-paging-test-DnxMnXKR.js → card-chooser-paging-test-BNuZRHX3.js} +1 -1
  53. package/bundled-test-harness/assets/{card-chooser-test-DUSnLr-u.js → card-chooser-test-wKfPOOT4.js} +1 -1
  54. package/bundled-test-harness/assets/{card-def-field-def-relationships-test-GBUr3tFK.js → card-def-field-def-relationships-test-CduQ6zan.js} +1 -1
  55. package/bundled-test-harness/assets/{card-delete-test-CQveO1VC.js → card-delete-test-DKxolj5o.js} +1 -1
  56. package/bundled-test-harness/assets/{check-correctness-test-D2M2p66N.js → check-correctness-test-B-kgHztp.js} +1 -1
  57. package/bundled-test-harness/assets/{codeblocks-test-AdolhdCA.js → codeblocks-test-BDD7qK_V.js} +1 -1
  58. package/bundled-test-harness/assets/{compaction-test-DvyAQq56.js → compaction-test-BweolkhG.js} +1 -1
  59. package/bundled-test-harness/assets/{container-DIF_9lZ9.css → container-B5B6u-xm.css} +29 -22
  60. package/bundled-test-harness/assets/{container-DvvqmNpg.js → container-BnJjIM6J.js} +9 -6
  61. package/bundled-test-harness/assets/{create-listing-modal-test-D95JfQab.js → create-listing-modal-test-FvcAKpIa.js} +1 -1
  62. package/bundled-test-harness/assets/{debug-message-test-BcNtj7Wr.js → debug-message-test-CJ18Evt7.js} +1 -1
  63. package/bundled-test-harness/assets/{externals-DCnB4XSS.js → externals-BWjhmCD4.js} +1 -1
  64. package/bundled-test-harness/assets/{fallback-banner-live-delete-test-5lnN1pv2.js → fallback-banner-live-delete-test-C2H3BrT-.js} +1 -1
  65. package/bundled-test-harness/assets/{fallback-banner-test-DFb_m_Kq.js → fallback-banner-test-C10aYjkj.js} +1 -1
  66. package/bundled-test-harness/assets/{fallback-models-test-p3YIwj1L.js → fallback-models-test-BxoPiaGF.js} +1 -1
  67. package/bundled-test-harness/assets/{file-attachment-test-CmyjNWRR.js → file-attachment-test-DeKyzYaz.js} +1 -1
  68. package/bundled-test-harness/assets/{general-test-CX-TFDuX.js → general-test-BewzrpoB.js} +1 -1
  69. package/bundled-test-harness/assets/{index-query-engine-test-CY2tAkot.js → index-query-engine-test-BtI8Y2LR.js} +1 -1
  70. package/bundled-test-harness/assets/{instance-filter-matcher-test-CMBTh8xn.js → instance-filter-matcher-test-BHXHGHjE.js} +1 -1
  71. package/bundled-test-harness/assets/{main-BOZbZVem.js → main-CqAHZ2kK.js} +1 -1
  72. package/bundled-test-harness/assets/{message-tool-test-CfjRSJvt.js → message-tool-test-NMaPJFsd.js} +1 -1
  73. package/bundled-test-harness/assets/{model-cost-tier-test-mZG84Uso.js → model-cost-tier-test-BgbfN2LH.js} +1 -1
  74. package/bundled-test-harness/assets/{operation-lowering-test-CHnjKSJb.js → operation-lowering-test-C4IaOjsA.js} +1 -1
  75. package/bundled-test-harness/assets/{operator-mode-card-chooser-test-KWS8lq16.js → operator-mode-card-chooser-test-Bwe47Rn7.js} +1 -1
  76. package/bundled-test-harness/assets/{operator-mode-links-test-CViFt0Ws.js → operator-mode-links-test-BU4jkC-B.js} +1 -1
  77. package/bundled-test-harness/assets/{operator-mode-test-D0gZM2yw.js → operator-mode-test-Bh0rGM9z.js} +1 -1
  78. package/bundled-test-harness/assets/{operator-mode-ui-test-CEvHJ9DS.js → operator-mode-ui-test-whgUw0ju.js} +1 -1
  79. package/bundled-test-harness/assets/{overlay-menu-items-test-Pw80RXsr.js → overlay-menu-items-test-Cwn1vr8q.js} +1 -1
  80. package/bundled-test-harness/assets/{past-sessions-test-CW0-Bjav.js → past-sessions-test-Bh24N-Kz.js} +1 -1
  81. package/bundled-test-harness/assets/{permission-checking-realm-test-D5uXQypS.js → permission-checking-realm-test-DKnnAy0f.js} +1 -1
  82. package/bundled-test-harness/assets/{realm-config-policy-standing-test-Btatheni.js → realm-config-policy-standing-test-DngdYypq.js} +1 -1
  83. package/bundled-test-harness/assets/{realm-config-routing-rule-edit-test-Dh5zW13o.js → realm-config-routing-rule-edit-test-_rkFU8SN.js} +1 -1
  84. package/bundled-test-harness/assets/{realm-config-settings-test-BJa8gv_C.js → realm-config-settings-test-CR9NH0Zn.js} +1 -1
  85. package/bundled-test-harness/assets/{reasoning-test-2Zp37cQB.js → reasoning-test-0lH10kRl.js} +1 -1
  86. package/bundled-test-harness/assets/{room-message-test-BsrON13e.js → room-message-test-C-PddE4Y.js} +1 -1
  87. package/bundled-test-harness/assets/{run-realm-code-BxktQf6R.js → run-realm-code-C8N7DtN-.js} +1 -1
  88. package/bundled-test-harness/assets/{run-realm-code-test-7grpTos-.js → run-realm-code-test-DBdp147q.js} +1 -1
  89. package/bundled-test-harness/assets/{schema-generation-test-CM9ggQji.js → schema-generation-test-CB1Hb7GQ.js} +1 -1
  90. package/bundled-test-harness/assets/{scrolling-test-BER00htB.js → scrolling-test-3PhVin8h.js} +1 -1
  91. package/bundled-test-harness/assets/{searchable-option-test-Ctf8r3HZ.js → searchable-option-test-BAdOiL4U.js} +1 -1
  92. package/bundled-test-harness/assets/{sending-test-BJGobs2E.js → sending-test-KSmu1tSJ.js} +1 -1
  93. package/bundled-test-harness/assets/{skills-test-GPOsHRN6.js → skills-test-D-dlcFpd.js} +1 -1
  94. package/bundled-test-harness/assets/{store-test-QvvHimzH.js → store-test-CEW7xQjZ.js} +1 -1
  95. package/bundled-test-harness/assets/{tests-DoTEpvcF.js → tests-QYqqa6nv.js} +47 -47
  96. package/bundled-test-harness/assets/{text-input-validator-test-CYYH2N0L.js → text-input-validator-test-D8iNOWep.js} +1 -1
  97. package/bundled-test-harness/assets/{to-device-streaming-test-Ctqa0bVp.js → to-device-streaming-test-trCX-505.js} +1 -1
  98. package/bundled-test-harness/assets/{token-usage-test-BQVbK3X0.js → token-usage-test-C6sULooH.js} +1 -1
  99. package/bundled-test-harness/assets/{tools-DU8Rc15s.js → tools-UJfD-_al.js} +1 -1
  100. package/bundled-test-harness/assets/{tools-test-B13yvEKE.js → tools-test-Brav31AV.js} +1 -1
  101. package/bundled-test-harness/assets/{worker-DHBEBihX.js → worker-9ph9_Lpv.js} +31 -26
  102. package/bundled-test-harness/index.html +8 -8
  103. package/bundled-test-harness/tests/index.html +8 -8
  104. package/bundled-types/host-app/lib/realm-runner/worker.ts +8 -51
  105. package/bundled-types/runtime-common/realm-runner-script.d.ts +3 -0
  106. package/package.json +1 -1
@@ -1,86 +1,1057 @@
1
- // Pretui — DiffBlock: a before/after change rendered as a line diff.
1
+ // Pretui — DiffBlock: a before/after change rendered as a line diff, syntax-highlighted when it knows the language.
2
2
  import Component from '@glimmer/component';
3
- import { tracked } from '@glimmer/tracking';
3
+ import CheckIcon from '@cardstack/boxel-icons/check';
4
+ import { cached, tracked } from '@glimmer/tracking';
4
5
  import { on } from '@ember/modifier';
6
+ import { fn } from '@ember/helper';
7
+ import { modifier } from 'ember-modifier';
8
+ import { DiffStat } from './diff-stat';
9
+ import { languageForFilename, tokenizeCode } from './code-block';
10
+ import type { CodeToken } from './code-block';
5
11
 
6
12
  export interface DiffLine {
7
13
  op?: 'add' | 'del';
8
14
  text: string;
15
+ /** this line stands for that many unchanged lines left out between two
16
+ * hunks; `text` is ignored */
17
+ skip?: number;
18
+ /** the line's number: in the new file, or the old one for a removal */
19
+ n?: number;
9
20
  }
10
21
 
22
+ // Above this many cells the middle of a diff is not aligned line by line: it
23
+ // reads as everything removed, then everything added. A file the runner
24
+ // rewrites wholesale says so honestly, and the tab never stalls on it.
25
+ const MAX_DIFF_CELLS = 4_000_000;
26
+
27
+ function splitLines(text: string | undefined): string[] {
28
+ if (!text) {
29
+ return [];
30
+ }
31
+ let lines = text.split('\n');
32
+ // a trailing newline ends the last line; it does not start another
33
+ if (lines[lines.length - 1] === '') {
34
+ lines.pop();
35
+ }
36
+ return lines;
37
+ }
38
+
39
+ /**
40
+ * The lines that changed from `before` to `after`, with `context` unchanged
41
+ * lines around each change and a `skip` line for each run left out. An
42
+ * absent `before` is a new file: every line is added.
43
+ *
44
+ * Common lines at either end are trimmed before the middle is aligned by
45
+ * longest common subsequence, so an edit to a long file costs its changed
46
+ * region, not the whole file.
47
+ */
48
+ export function lineDiff(
49
+ before: string | undefined,
50
+ after: string | undefined,
51
+ options: { context?: number } = {},
52
+ ): DiffLine[] {
53
+ let context = Math.max(0, options.context ?? 3);
54
+ let a = splitLines(before);
55
+ let b = splitLines(after);
56
+ let start = 0;
57
+ while (start < a.length && start < b.length && a[start] === b[start]) {
58
+ start++;
59
+ }
60
+ let endA = a.length;
61
+ let endB = b.length;
62
+ while (endA > start && endB > start && a[endA - 1] === b[endB - 1]) {
63
+ endA--;
64
+ endB--;
65
+ }
66
+ let middle: DiffLine[] = [];
67
+ let n = endA - start;
68
+ let m = endB - start;
69
+ if (n * m > MAX_DIFF_CELLS) {
70
+ for (let i = start; i < endA; i++) {
71
+ middle.push({ op: 'del', text: a[i]! });
72
+ }
73
+ for (let j = start; j < endB; j++) {
74
+ middle.push({ op: 'add', text: b[j]! });
75
+ }
76
+ } else {
77
+ // lcs[i][j]: the longest common run of a[start+i..] and b[start+j..]
78
+ let width = m + 1;
79
+ let lcs = new Uint32Array((n + 1) * width);
80
+ for (let i = n - 1; i >= 0; i--) {
81
+ for (let j = m - 1; j >= 0; j--) {
82
+ lcs[i * width + j] =
83
+ a[start + i] === b[start + j]
84
+ ? lcs[(i + 1) * width + j + 1]! + 1
85
+ : Math.max(lcs[(i + 1) * width + j]!, lcs[i * width + j + 1]!);
86
+ }
87
+ }
88
+ let i = 0;
89
+ let j = 0;
90
+ while (i < n || j < m) {
91
+ if (i < n && j < m && a[start + i] === b[start + j]) {
92
+ middle.push({ text: a[start + i]! });
93
+ i++;
94
+ j++;
95
+ } else if (
96
+ i < n &&
97
+ (j === m || lcs[(i + 1) * width + j]! >= lcs[i * width + j + 1]!)
98
+ ) {
99
+ // a removal reads before the line that replaces it
100
+ middle.push({ op: 'del', text: a[start + i]! });
101
+ i++;
102
+ } else {
103
+ middle.push({ op: 'add', text: b[start + j]! });
104
+ j++;
105
+ }
106
+ }
107
+ }
108
+ if (!middle.some((line) => line.op)) {
109
+ return [];
110
+ }
111
+ let all: DiffLine[] = [
112
+ ...a.slice(0, start).map((text) => ({ text })),
113
+ ...middle,
114
+ ...a.slice(endA).map((text) => ({ text })),
115
+ ];
116
+ let oldN = 0;
117
+ let newN = 0;
118
+ for (let line of all) {
119
+ if (line.op === 'del') {
120
+ line.n = ++oldN;
121
+ } else if (line.op === 'add') {
122
+ line.n = ++newN;
123
+ } else {
124
+ oldN++;
125
+ line.n = ++newN;
126
+ }
127
+ }
128
+ // keep each unchanged line within `context` of a change; fold the rest
129
+ let keep = all.map(() => false);
130
+ all.forEach((line, index) => {
131
+ if (line.op) {
132
+ for (
133
+ let k = Math.max(0, index - context);
134
+ k <= Math.min(all.length - 1, index + context);
135
+ k++
136
+ ) {
137
+ keep[k] = true;
138
+ }
139
+ }
140
+ });
141
+ let out: DiffLine[] = [];
142
+ let skipped = 0;
143
+ all.forEach((line, index) => {
144
+ if (keep[index]) {
145
+ if (skipped) {
146
+ out.push({ text: '', skip: skipped });
147
+ skipped = 0;
148
+ }
149
+ out.push(line);
150
+ } else {
151
+ skipped++;
152
+ }
153
+ });
154
+ if (skipped) {
155
+ out.push({ text: '', skip: skipped });
156
+ }
157
+ return out;
158
+ }
159
+
160
+
161
+ /** which file a line number counts in: the old file for a removed line, the
162
+ * new file for an added or unchanged one */
163
+ export type DiffSide = 'old' | 'new';
164
+
11
165
  export interface DiffBlockSignature {
12
- Args: { lines: DiffLine[]; receipt?: string; maxLines?: number };
166
+ Args: {
167
+ lines: DiffLine[];
168
+ receipt?: string;
169
+ maxLines?: number;
170
+ /** highlight each line with CodeBlock's grammar for this language */
171
+ language?: string;
172
+ /** shown as a header with the change counts; also guesses the
173
+ * language when @language is absent */
174
+ filename?: string;
175
+ /** the collapsed toggle's verb, before its count: 'Show all' reads
176
+ * "Show all · 14 more lines". Default: "14 more lines" */
177
+ expandLabel?: string;
178
+ /** a gutter with each line's number, where the lines carry one */
179
+ lineNumbers?: boolean;
180
+ /** review a hunk at a time: each hunk gets Keep and Revert on its bar */
181
+ onKeepHunk?: (hunk: DiffHunk) => unknown;
182
+ onRevertHunk?: (hunk: DiffHunk) => unknown;
183
+ /** what was decided per hunk, by index: a kept hunk says so, a
184
+ * reverted one is struck */
185
+ hunkStates?: Record<number, DiffHunkState>;
186
+ /** comments on lines, shown under their line */
187
+ comments?: DiffComment[];
188
+ /** with it, a line's number is a button that opens a comment there.
189
+ * `side` says which file `line` counts in, so a removed line and the
190
+ * added line that replaces it, which can share a number, stay apart */
191
+ onComment?: (line: number, text: string, side: DiffSide) => unknown;
192
+ /** 'unified' (default), 'split' (old left, new right), or 'auto':
193
+ * split once the block is 640px wide. Both views carry the same review
194
+ * controls and comments */
195
+ view?: 'unified' | 'split' | 'auto';
196
+ /** soft-wrap long lines instead of scrolling them */
197
+ wrap?: boolean;
198
+ /** above this many characters the diff is not drawn until asked:
199
+ * "Large diff · 12,400 lines · Render anyway" */
200
+ maxBytes?: number;
201
+ };
13
202
  Element: HTMLDivElement;
14
203
  }
15
204
 
205
+ interface ShownLine {
206
+ /** the line's index in @lines */
207
+ index: number;
208
+ op?: 'add' | 'del';
209
+ n?: number;
210
+ side: DiffSide;
211
+ sign: string;
212
+ tokens: CodeToken[];
213
+ skip?: string;
214
+ }
215
+
216
+ function skipText(count: number): string {
217
+ return `${count} unchanged ${count === 1 ? 'line' : 'lines'}`;
218
+ }
219
+
220
+ /** a run of changed lines between two skips: what Keep and Revert act on */
221
+ export interface DiffHunk {
222
+ index: number;
223
+ /** the first and last line of the hunk, as indexes into the lines */
224
+ start: number;
225
+ end: number;
226
+ added: number;
227
+ removed: number;
228
+ }
229
+
230
+ /** the hunks of a diff: the runs of lines between skip lines that change
231
+ * something */
232
+ export function diffHunks(lines: DiffLine[]): DiffHunk[] {
233
+ let hunks: DiffHunk[] = [];
234
+ let start = -1;
235
+ let flush = (end: number) => {
236
+ if (start < 0) {
237
+ return;
238
+ }
239
+ let run = lines.slice(start, end + 1);
240
+ let added = run.filter((l) => l.op === 'add').length;
241
+ let removed = run.filter((l) => l.op === 'del').length;
242
+ if (added || removed) {
243
+ hunks.push({ index: hunks.length, start, end, added, removed });
244
+ }
245
+ start = -1;
246
+ };
247
+ lines.forEach((line, i) => {
248
+ if (line.skip) {
249
+ flush(i - 1);
250
+ } else if (start < 0) {
251
+ start = i;
252
+ }
253
+ });
254
+ flush(lines.length - 1);
255
+ return hunks;
256
+ }
257
+
258
+ export interface DiffComment {
259
+ /** the line it is on, numbered in the file `side` names */
260
+ line: number;
261
+ /** 'old' for a comment on a removed line; 'new' (the default) for an
262
+ * added or unchanged one */
263
+ side?: DiffSide;
264
+ author: string;
265
+ text: string;
266
+ }
267
+
268
+ export type DiffHunkState = 'kept' | 'reverted';
269
+
270
+ /** one line's place in a row: the whole row in the unified view, one half
271
+ * of it in the split view */
272
+ interface DiffCell {
273
+ line?: ShownLine;
274
+ /** whether the line's number opens a comment and its comments show here.
275
+ * In the split view an unchanged line shows on both halves, and only the
276
+ * new half carries its comments */
277
+ commentable: boolean;
278
+ }
279
+
280
+ interface DiffRow {
281
+ key: string;
282
+ /** the hunk whose bar sits above this row, when hunks are reviewed */
283
+ hunk?: DiffHunk;
284
+ skip?: string;
285
+ cells: DiffCell[];
286
+ }
287
+
16
288
  // Staged change with an immutable receipt line; long diffs collapse.
17
289
  export class DiffBlock extends Component<DiffBlockSignature> {
18
290
  @tracked collapsed = true;
291
+ @tracked renderAnyway = false;
292
+ @tracked wide = false;
293
+ @tracked commentingOn: { side: DiffSide; n: number } | undefined;
294
+ @tracked commentDraft = '';
295
+ /** the hunk whose Keep or Revert was just pressed: its decision takes the
296
+ * focus the removed button held */
297
+ private focusHunk: number | undefined;
298
+ /** the line number that opened the comment editor, to take focus back */
299
+ private commentOpener: HTMLElement | undefined;
300
+
301
+ get tooLarge(): boolean {
302
+ let max = this.args.maxBytes;
303
+ if (!max || this.renderAnyway) {
304
+ return false;
305
+ }
306
+ let size = 0;
307
+ for (let line of this.args.lines) {
308
+ size += line.text.length + 1;
309
+ if (size > max) {
310
+ return true;
311
+ }
312
+ }
313
+ return false;
314
+ }
315
+ get largeLabel(): string {
316
+ let n = this.args.lines.length;
317
+ return `Large diff · ${n.toLocaleString()} ${n === 1 ? 'line' : 'lines'}`;
318
+ }
319
+ showLarge = () => (this.renderAnyway = true);
320
+
321
+ get split(): boolean {
322
+ let view = this.args.view ?? 'unified';
323
+ return view === 'split' || (view === 'auto' && this.wide);
324
+ }
325
+ // 'auto' measures its own width: split from 640px
326
+ watchWidth = modifier(
327
+ (element: HTMLElement, [view]: [string | undefined]) => {
328
+ if (view !== 'auto' || typeof ResizeObserver !== 'function') {
329
+ return;
330
+ }
331
+ let observer = new ResizeObserver(([entry]) => {
332
+ let wide = (entry?.contentRect.width ?? 0) >= 640;
333
+ if (wide !== this.wide) {
334
+ this.wide = wide;
335
+ }
336
+ });
337
+ observer.observe(element);
338
+ return () => observer.disconnect();
339
+ },
340
+ );
341
+
342
+ @cached
343
+ get hunks(): DiffHunk[] {
344
+ return diffHunks(this.args.lines);
345
+ }
346
+ get reviews(): boolean {
347
+ return Boolean(this.args.onKeepHunk || this.args.onRevertHunk);
348
+ }
349
+ /** the hunk that starts at a line index, for its bar */
350
+ private hunkAt(index: number): DiffHunk | undefined {
351
+ return this.reviews
352
+ ? this.hunks.find((h) => h.start === index)
353
+ : undefined;
354
+ }
355
+ private hunkOf(index: number): DiffHunk | undefined {
356
+ return this.hunks.find((h) => index >= h.start && index <= h.end);
357
+ }
358
+ stateOf = (hunk: DiffHunk) => this.args.hunkStates?.[hunk.index];
359
+ isKept = (hunk: DiffHunk) => this.stateOf(hunk) === 'kept';
360
+ lineState = (line: ShownLine) => {
361
+ let hunk = this.hunkOf(line.index);
362
+ return hunk ? this.stateOf(hunk) : undefined;
363
+ };
364
+ hunkLabel = (hunk: DiffHunk) =>
365
+ `Change ${hunk.index + 1} of ${this.hunks.length} · +${hunk.added} −${hunk.removed}`;
366
+ keepHunk = (hunk: DiffHunk) => {
367
+ this.focusHunk = hunk.index;
368
+ return this.args.onKeepHunk?.(hunk);
369
+ };
370
+ revertHunk = (hunk: DiffHunk) => {
371
+ this.focusHunk = hunk.index;
372
+ return this.args.onRevertHunk?.(hunk);
373
+ };
374
+ /** a decision replaces the Keep and Revert buttons; when one of them had
375
+ * focus, the decision takes it, so the keyboard keeps its place */
376
+ claimHunkFocus = modifier((element: HTMLElement, [index]: [number]) => {
377
+ if (this.focusHunk !== index) {
378
+ return;
379
+ }
380
+ this.focusHunk = undefined;
381
+ let active = document.activeElement;
382
+ if (!active || active === document.body || !active.isConnected) {
383
+ element.focus({ preventScroll: true });
384
+ }
385
+ });
386
+
387
+ commentsOn = (line: ShownLine) =>
388
+ (this.args.comments ?? []).filter(
389
+ (c) => c.line === line.n && (c.side ?? 'new') === line.side,
390
+ );
391
+ isCommenting = (line: ShownLine) =>
392
+ this.commentingOn !== undefined &&
393
+ this.commentingOn.n === line.n &&
394
+ this.commentingOn.side === line.side;
395
+ commentLabel = (line: ShownLine) =>
396
+ line.side === 'old'
397
+ ? `Comment on removed line ${line.n}`
398
+ : `Comment on line ${line.n}`;
399
+ openComment = (line: ShownLine, event: Event) => {
400
+ if (line.n === undefined) {
401
+ return;
402
+ }
403
+ this.commentOpener = event.currentTarget as HTMLElement;
404
+ this.commentingOn = { side: line.side, n: line.n };
405
+ this.commentDraft = '';
406
+ };
407
+ typeComment = (event: Event) =>
408
+ (this.commentDraft = (event.target as HTMLTextAreaElement).value);
409
+ private closeComment() {
410
+ this.commentingOn = undefined;
411
+ let opener = this.commentOpener;
412
+ this.commentOpener = undefined;
413
+ if (opener?.isConnected) {
414
+ opener.focus({ preventScroll: true });
415
+ }
416
+ }
417
+ cancelComment = () => this.closeComment();
418
+ saveComment = async () => {
419
+ let target = this.commentingOn;
420
+ let text = this.commentDraft.trim();
421
+ if (target && text) {
422
+ await this.args.onComment?.(target.n, text, target.side);
423
+ }
424
+ this.closeComment();
425
+ };
426
+ commentKey = (e: Event) => {
427
+ let event = e as KeyboardEvent;
428
+ if (event.key === 'Escape') {
429
+ // the editor handles Escape; a host listening for it must not also act
430
+ event.stopPropagation();
431
+ this.cancelComment();
432
+ } else if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
433
+ event.preventDefault();
434
+ this.saveComment();
435
+ }
436
+ };
437
+ focusComment = modifier((element: HTMLTextAreaElement) => element.focus());
438
+
439
+ /** The rows either view draws. Unified: one line per row. Split: removals
440
+ * on the left and additions on the right, paired within each change, and
441
+ * unchanged lines on both sides. A hunk's bar sits above the row its first
442
+ * line is in. */
443
+ @cached
444
+ get rows(): DiffRow[] {
445
+ let rows: DiffRow[] = [];
446
+ let hunkFor = (...lines: (ShownLine | undefined)[]) => {
447
+ for (let line of lines) {
448
+ let hunk = line ? this.hunkAt(line.index) : undefined;
449
+ if (hunk) {
450
+ return hunk;
451
+ }
452
+ }
453
+ return undefined;
454
+ };
455
+ if (!this.split) {
456
+ for (let line of this.shown) {
457
+ rows.push(
458
+ line.skip
459
+ ? { key: `s${line.index}`, skip: line.skip, cells: [] }
460
+ : {
461
+ key: `l${line.index}`,
462
+ hunk: hunkFor(line),
463
+ cells: [{ line, commentable: true }],
464
+ },
465
+ );
466
+ }
467
+ return rows;
468
+ }
469
+ let dels: ShownLine[] = [];
470
+ let adds: ShownLine[] = [];
471
+ let flush = () => {
472
+ for (let k = 0; k < Math.max(dels.length, adds.length); k++) {
473
+ let left = dels[k];
474
+ let right = adds[k];
475
+ rows.push({
476
+ key: `p${(left ?? right)!.index}`,
477
+ // a pending hunk opens with a removal or an addition: whichever
478
+ // comes first in the file
479
+ hunk: hunkFor(left, right),
480
+ cells: [
481
+ { line: left, commentable: true },
482
+ { line: right, commentable: true },
483
+ ],
484
+ });
485
+ }
486
+ dels = [];
487
+ adds = [];
488
+ };
489
+ for (let line of this.shown) {
490
+ if (line.skip) {
491
+ flush();
492
+ rows.push({ key: `s${line.index}`, skip: line.skip, cells: [] });
493
+ } else if (line.op === 'del') {
494
+ dels.push(line);
495
+ } else if (line.op === 'add') {
496
+ adds.push(line);
497
+ } else {
498
+ flush();
499
+ rows.push({
500
+ key: `c${line.index}`,
501
+ hunk: hunkFor(line),
502
+ cells: [
503
+ { line, commentable: false },
504
+ { line, commentable: true },
505
+ ],
506
+ });
507
+ }
508
+ }
509
+ flush();
510
+ return rows;
511
+ }
19
512
  get maxLines() {
20
513
  return this.args.maxLines ?? 8;
21
514
  }
22
515
  get overflows() {
23
516
  return this.args.lines.length > this.maxLines;
24
517
  }
25
- get shown() {
26
- return this.collapsed && this.overflows
27
- ? this.args.lines.slice(0, this.maxLines)
28
- : this.args.lines;
518
+ get language(): string | undefined {
519
+ return this.args.language ?? languageForFilename(this.args.filename);
520
+ }
521
+ @cached
522
+ get shown(): ShownLine[] {
523
+ let lines =
524
+ this.collapsed && this.overflows
525
+ ? this.args.lines.slice(0, this.maxLines)
526
+ : this.args.lines;
527
+ let language = this.language;
528
+ // Each line is tokenized on its own: a diff shows fragments, and a
529
+ // fragment that opens a string or comment must not colour the lines
530
+ // after it.
531
+ return lines.map((line, index): ShownLine => {
532
+ let side: DiffSide = line.op === 'del' ? 'old' : 'new';
533
+ return line.skip
534
+ ? { index, side, sign: '', tokens: [], skip: skipText(line.skip) }
535
+ : {
536
+ index,
537
+ op: line.op,
538
+ n: line.n,
539
+ side,
540
+ sign: line.op === 'add' ? '+ ' : line.op === 'del' ? '- ' : ' ',
541
+ tokens: tokenizeCode(line.text, language),
542
+ };
543
+ });
544
+ }
545
+ get added(): number {
546
+ return this.args.lines.filter((line) => line.op === 'add' && !line.skip)
547
+ .length;
548
+ }
549
+ get removed(): number {
550
+ return this.args.lines.filter((line) => line.op === 'del').length;
29
551
  }
30
552
  get toggleLabel() {
31
- return this.collapsed ? `… ${this.args.lines.length - this.maxLines} more lines` : 'collapse';
553
+ if (!this.collapsed) {
554
+ return 'collapse';
555
+ }
556
+ let hidden = this.args.lines.length - this.maxLines;
557
+ let more = `${hidden} more ${hidden === 1 ? 'line' : 'lines'}`;
558
+ return this.args.expandLabel ? `${this.args.expandLabel} · ${more}` : more;
559
+ }
560
+ get toggleExpanded() {
561
+ return this.collapsed ? 'false' : 'true';
32
562
  }
33
- prefix = (line: DiffLine) => (line.op === 'add' ? '+ ' : line.op === 'del' ? '- ' : ' ');
34
563
  toggle = () => {
35
564
  this.collapsed = !this.collapsed;
36
565
  };
37
566
  <template>
38
- <div class='pretui-diff' data-test-pretui-diff-block ...attributes>
39
- {{#each this.shown as |line|}}
40
- <div class='pretui-diff-line' data-op={{line.op}}>{{this.prefix line}}{{line.text}}</div>
41
- {{/each}}
42
- {{#if this.overflows}}
43
- <button type='button' class='pretui-diff-line pretui-diff-toggle' {{on 'click' this.toggle}}>{{this.toggleLabel}}</button>
567
+ <div
568
+ class='pretui-diff'
569
+ data-language={{this.language}}
570
+ data-numbered={{if @lineNumbers 'true'}}
571
+ data-wrap={{if @wrap 'true'}}
572
+ data-view={{if this.split 'split' 'unified'}}
573
+ {{this.watchWidth @view}}
574
+ data-test-pretui-diff-block
575
+ ...attributes
576
+ >
577
+ {{#if @filename}}
578
+ <div class='pretui-diff-head' data-test-pretui-diff-head>
579
+ <span class='pretui-diff-file'>{{@filename}}</span>
580
+ {{! the change's size at a glance, as DiffStat draws it }}
581
+ <DiffStat
582
+ class='pretui-diff-counts'
583
+ @added={{this.added}}
584
+ @removed={{this.removed}}
585
+ />
586
+ </div>
587
+ {{/if}}
588
+ {{#if this.tooLarge}}
589
+ <div class='pretui-diff-large' data-test-pretui-diff-large>
590
+ <span>{{this.largeLabel}}</span>
591
+ <button
592
+ type='button'
593
+ class='pretui-diff-action'
594
+ {{on 'click' this.showLarge}}
595
+ data-test-pretui-diff-render-anyway
596
+ >Render anyway</button>
597
+ </div>
598
+ {{else}}
599
+ {{#each this.rows key='key' as |row|}}
600
+ {{#if row.hunk}}
601
+ {{#let row.hunk as |hunk|}}
602
+ <div
603
+ class='pretui-diff-hunk'
604
+ data-state={{this.stateOf hunk}}
605
+ data-test-pretui-diff-hunk={{hunk.index}}
606
+ >
607
+ <span class='pretui-diff-hunk-label'>{{this.hunkLabel
608
+ hunk
609
+ }}</span>
610
+ {{#if (this.stateOf hunk)}}
611
+ <span
612
+ class='pretui-diff-hunk-state'
613
+ tabindex='-1'
614
+ {{this.claimHunkFocus hunk.index}}
615
+ data-test-pretui-diff-hunk-state={{hunk.index}}
616
+ >{{#if (this.isKept hunk)}}<CheckIcon
617
+ width='11'
618
+ height='11'
619
+ aria-hidden='true'
620
+ />{{/if}}{{this.stateOf hunk}}</span>
621
+ {{else}}
622
+ {{#if @onKeepHunk}}<button
623
+ type='button'
624
+ class='pretui-diff-action'
625
+ {{on 'click' (fn this.keepHunk hunk)}}
626
+ data-test-pretui-diff-keep={{hunk.index}}
627
+ >Keep</button>{{/if}}
628
+ {{#if @onRevertHunk}}<button
629
+ type='button'
630
+ class='pretui-diff-action'
631
+ data-kind='quiet'
632
+ {{on 'click' (fn this.revertHunk hunk)}}
633
+ data-test-pretui-diff-revert={{hunk.index}}
634
+ >Revert</button>{{/if}}
635
+ {{/if}}
636
+ </div>
637
+ {{/let}}
638
+ {{/if}}
639
+ {{#if row.skip}}
640
+ <div
641
+ class='pretui-diff-line pretui-diff-skip'
642
+ data-skip
643
+ data-test-pretui-diff-skip
644
+ >{{row.skip}}</div>
645
+ {{else}}
646
+ <div class='pretui-diff-row' data-test-pretui-diff-row>
647
+ {{#each row.cells as |cell|}}
648
+ <div
649
+ class='pretui-diff-cell'
650
+ data-side={{if cell.line cell.line.side}}
651
+ data-test-pretui-diff-cell
652
+ >
653
+ {{#if cell.line}}
654
+ {{#let cell.line as |line|}}
655
+ <div
656
+ class='pretui-diff-line'
657
+ data-op={{line.op}}
658
+ data-state={{this.lineState line}}
659
+ data-test-pretui-diff-line
660
+ >{{#if @lineNumbers}}{{#if
661
+ (if @onComment cell.commentable false)
662
+ }}<button
663
+ type='button'
664
+ class='pretui-diff-n'
665
+ aria-label={{this.commentLabel line}}
666
+ {{on 'click' (fn this.openComment line)}}
667
+ data-test-pretui-diff-comment-on='{{line.side}}:{{line.n}}'
668
+ >{{line.n}}</button>{{else}}<span
669
+ class='pretui-diff-n'
670
+ aria-hidden='true'
671
+ >{{line.n}}</span>{{/if}}{{/if}}<span
672
+ class='pretui-diff-sign'
673
+ >{{line.sign}}</span>{{#each
674
+ line.tokens key='@index'
675
+ as |part|
676
+ }}<span
677
+ class={{part.cls}}
678
+ >{{part.text}}</span>{{/each}}</div>
679
+ {{#if cell.commentable}}
680
+ {{#each (this.commentsOn line) as |comment|}}
681
+ <div
682
+ class='pretui-diff-comment'
683
+ data-test-pretui-diff-comment='{{line.side}}:{{comment.line}}'
684
+ >
685
+ <span
686
+ class='pretui-diff-comment-author'
687
+ >{{comment.author}}</span>
688
+ <span
689
+ class='pretui-diff-comment-text'
690
+ >{{comment.text}}</span>
691
+ </div>
692
+ {{/each}}
693
+ {{#if (this.isCommenting line)}}
694
+ <div
695
+ class='pretui-diff-comment pretui-diff-comment-new'
696
+ data-test-pretui-diff-comment-editor='{{line.side}}:{{line.n}}'
697
+ >
698
+ <textarea
699
+ class='pretui-diff-comment-field'
700
+ aria-label={{this.commentLabel line}}
701
+ rows='2'
702
+ value={{this.commentDraft}}
703
+ {{on 'input' this.typeComment}}
704
+ {{on 'keydown' this.commentKey}}
705
+ {{this.focusComment}}
706
+ data-test-pretui-diff-comment-field
707
+ ></textarea>
708
+ <span class='pretui-diff-comment-actions'>
709
+ <button
710
+ type='button'
711
+ class='pretui-diff-action'
712
+ data-kind='quiet'
713
+ {{on 'click' this.cancelComment}}
714
+ data-test-pretui-diff-comment-cancel
715
+ >Cancel</button>
716
+ <button
717
+ type='button'
718
+ class='pretui-diff-action'
719
+ {{on 'click' this.saveComment}}
720
+ data-test-pretui-diff-comment-save
721
+ >Comment</button>
722
+ </span>
723
+ </div>
724
+ {{/if}}
725
+ {{/if}}
726
+ {{/let}}
727
+ {{/if}}
728
+ </div>
729
+ {{/each}}
730
+ </div>
731
+ {{/if}}
732
+ {{/each}}
44
733
  {{/if}}
45
- {{#if @receipt}}<div class='pretui-diff-receipt'>{{@receipt}}</div>{{/if}}
734
+ {{#if (if this.overflows (if this.tooLarge false true))}}
735
+ <button
736
+ type='button'
737
+ class='pretui-diff-line pretui-diff-toggle'
738
+ aria-expanded={{this.toggleExpanded}}
739
+ {{on 'click' this.toggle}}
740
+ data-test-pretui-diff-toggle
741
+ >{{this.toggleLabel}}</button>
742
+ {{/if}}
743
+ {{#if @receipt}}<div
744
+ class='pretui-diff-receipt'
745
+ data-test-pretui-diff-receipt
746
+ >{{@receipt}}</div>{{/if}}
46
747
  </div>
47
748
  <style scoped>
48
749
  @layer PretComponent {
750
+ /* a diff is pasteable: mono at the data size, on the inset well */
49
751
  .pretui-diff {
50
752
  font-family: var(--font-mono);
51
- font-size: var(--text-ui-sm, 11.5px);
52
- line-height: 1.7;
53
- background: var(--inset, var(--boxel-100));
54
- border-radius: 8px;
753
+ font-size: var(--boxel-font-size-xs);
754
+ line-height: var(--boxel-line-height-xs);
755
+ background-color: var(--inset);
756
+ color: var(--foreground);
757
+ border-radius: var(--boxel-border-radius-sm);
55
758
  box-shadow: var(--pretui-shadow-hairline, 0 0 0 1px var(--border));
56
759
  overflow: hidden;
57
760
  }
761
+ .pretui-diff[data-wrap] .pretui-diff-line {
762
+ white-space: pre-wrap;
763
+ overflow-wrap: anywhere;
764
+ }
765
+ .pretui-diff[data-view='split'] .pretui-diff-row {
766
+ display: grid;
767
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
768
+ }
769
+ .pretui-diff-cell {
770
+ min-width: 0;
771
+ }
772
+ .pretui-diff[data-view='split'] .pretui-diff-cell:first-child {
773
+ box-shadow: inset -1px 0 0 var(--border);
774
+ }
775
+ .pretui-diff-hunk {
776
+ display: flex;
777
+ align-items: center;
778
+ gap: var(--boxel-sp-xs);
779
+ min-height: 1.625rem;
780
+ padding: 0 var(--boxel-sp-sm);
781
+ font-family: var(--font-sans);
782
+ font-size: var(--boxel-caption-font-size);
783
+ line-height: var(--boxel-caption-line-height);
784
+ color: var(--muted-foreground);
785
+ box-shadow: inset 0 1px 0 var(--border);
786
+ }
787
+ /* "Change 1 of 3 · +2 −1" is a count, not a string to paste: sans */
788
+ .pretui-diff-hunk-label {
789
+ margin-inline-end: auto;
790
+ font-variant-numeric: tabular-nums;
791
+ }
792
+ .pretui-diff-hunk-state {
793
+ font-weight: 600;
794
+ }
795
+ .pretui-diff-hunk-state:focus-visible {
796
+ outline: 2px solid var(--ring);
797
+ outline-offset: 2px;
798
+ }
799
+ /* kept is done: ink with a check, not green */
800
+ .pretui-diff-hunk[data-state='kept'] .pretui-diff-hunk-state {
801
+ display: inline-flex;
802
+ align-items: center;
803
+ gap: var(--boxel-sp-4xs);
804
+ color: var(--foreground);
805
+ }
806
+ .pretui-diff-line[data-state='reverted'] {
807
+ text-decoration: line-through;
808
+ opacity: 0.55;
809
+ }
810
+ .pretui-diff-action {
811
+ height: 1.375rem;
812
+ padding: 0 var(--boxel-sp-xs);
813
+ border: 0;
814
+ border-radius: var(--boxel-border-radius-sm);
815
+ background-color: transparent;
816
+ box-shadow: inset 0 0 0 1px var(--border-strong);
817
+ font-family: var(--font-sans);
818
+ font-size: var(--boxel-caption-font-size);
819
+ line-height: var(--boxel-caption-line-height);
820
+ font-weight: 400;
821
+ color: var(--foreground);
822
+ cursor: pointer;
823
+ }
824
+ .pretui-diff-action[data-kind='quiet'] {
825
+ box-shadow: none;
826
+ color: var(--muted-foreground);
827
+ }
828
+ .pretui-diff-action:hover {
829
+ color: var(--primary-ink);
830
+ }
831
+ .pretui-diff-action:focus-visible,
832
+ button.pretui-diff-n:focus-visible {
833
+ outline: 2px solid var(--ring);
834
+ outline-offset: -2px;
835
+ }
836
+ button.pretui-diff-n {
837
+ padding: 0;
838
+ border: 0;
839
+ background-color: transparent;
840
+ font-family: inherit;
841
+ font-size: inherit;
842
+ line-height: inherit;
843
+ color: inherit;
844
+ cursor: pointer;
845
+ }
846
+ button.pretui-diff-n:hover {
847
+ color: var(--primary-ink);
848
+ }
849
+ .pretui-diff-comment {
850
+ display: grid;
851
+ gap: var(--boxel-sp-6xs);
852
+ margin: var(--boxel-sp-6xs) var(--boxel-sp-xs) var(--boxel-sp-4xs)
853
+ 2.5rem;
854
+ padding: var(--boxel-sp-4xs) var(--boxel-sp-xs);
855
+ border-radius: var(--boxel-border-radius-sm);
856
+ background-color: var(--card);
857
+ box-shadow: 0 0 0 1px var(--border);
858
+ font-family: var(--font-sans);
859
+ font-size: var(--boxel-ui-label-font-size);
860
+ line-height: var(--boxel-ui-label-line-height);
861
+ color: var(--card-foreground);
862
+ white-space: normal;
863
+ }
864
+ .pretui-diff-comment-author {
865
+ font-weight: 600;
866
+ }
867
+ .pretui-diff-comment-field {
868
+ width: 100%;
869
+ box-sizing: border-box;
870
+ padding: var(--boxel-sp-4xs) var(--boxel-sp-xs);
871
+ border: 0;
872
+ border-radius: var(--boxel-border-radius-xs);
873
+ background-color: var(--field);
874
+ box-shadow: inset 0 0 0 1px var(--input);
875
+ font-family: inherit;
876
+ font-size: inherit;
877
+ line-height: inherit;
878
+ color: var(--foreground);
879
+ resize: vertical;
880
+ }
881
+ .pretui-diff-comment-actions {
882
+ display: flex;
883
+ justify-content: flex-end;
884
+ gap: var(--boxel-sp-4xs);
885
+ }
886
+ .pretui-diff-large {
887
+ display: flex;
888
+ align-items: center;
889
+ justify-content: space-between;
890
+ gap: var(--boxel-sp-xs);
891
+ padding: var(--boxel-sp-sm);
892
+ font-family: var(--font-sans);
893
+ font-size: var(--boxel-ui-label-font-size);
894
+ line-height: var(--boxel-ui-label-line-height);
895
+ color: var(--muted-foreground);
896
+ }
897
+ .pretui-diff-head {
898
+ display: flex;
899
+ align-items: center;
900
+ gap: var(--boxel-sp-xs);
901
+ padding: var(--boxel-sp-4xs) var(--boxel-sp-sm);
902
+ font-family: var(--font-sans);
903
+ font-size: var(--boxel-ui-label-font-size);
904
+ line-height: var(--boxel-ui-label-line-height);
905
+ font-weight: 400;
906
+ box-shadow: 0 1px 0 var(--border);
907
+ }
908
+ /* the file is a path, a pasteable string: mono at the data size */
909
+ .pretui-diff-file {
910
+ min-width: 0;
911
+ flex: 1;
912
+ overflow: hidden;
913
+ text-overflow: ellipsis;
914
+ white-space: nowrap;
915
+ font-family: var(--font-mono);
916
+ font-size: var(--boxel-font-size-xs);
917
+ line-height: var(--boxel-line-height-xs);
918
+ }
919
+ /* the counts are figures: sans, tabular */
920
+ .pretui-diff-counts {
921
+ display: flex;
922
+ gap: var(--boxel-sp-4xs);
923
+ font-size: var(--boxel-caption-font-size);
924
+ line-height: var(--boxel-caption-line-height);
925
+ font-variant-numeric: tabular-nums;
926
+ }
58
927
  .pretui-diff-line {
59
- padding: 0 10px;
928
+ padding: 0 var(--boxel-sp-sm);
60
929
  white-space: pre;
61
930
  }
931
+ /* the tint is the fill, the line's text is the hue's ink */
62
932
  .pretui-diff-line[data-op='add'] {
63
- background: color-mix(in oklch, var(--success, var(--boxel-success)) 12%, transparent);
64
- color: color-mix(in oklch, var(--foreground) 30%, var(--success, var(--boxel-success)));
933
+ background-color: color-mix(
934
+ in oklch,
935
+ var(--success) 12%,
936
+ transparent
937
+ );
938
+ color: var(--success-ink);
65
939
  }
66
940
  .pretui-diff-line[data-op='del'] {
67
- background: color-mix(in oklch, var(--destructive) 10%, transparent);
68
- color: color-mix(in oklch, var(--foreground) 30%, var(--destructive));
941
+ background-color: color-mix(
942
+ in oklch,
943
+ var(--destructive) 10%,
944
+ transparent
945
+ );
946
+ color: var(--destructive-ink);
947
+ }
948
+ .pretui-diff-sign {
949
+ user-select: none;
950
+ }
951
+ /* the number gutter: the line's place in the file, out of the way */
952
+ .pretui-diff-n {
953
+ display: inline-block;
954
+ width: 3ch;
955
+ margin-inline-end: 1ch;
956
+ text-align: end;
957
+ /* muted, not subtle: the number sits on the add and remove tints,
958
+ where the subtle ink falls under 4.5:1 */
959
+ color: var(--muted-foreground);
960
+ user-select: none;
961
+ }
962
+ /* With a language, the tint says added or removed and the tokens
963
+ keep their own colours, as in CodeBlock; the sign keeps the op's
964
+ colour so the change still reads in greyscale. */
965
+ .pretui-diff[data-language] .pretui-diff-line[data-op] {
966
+ color: var(--foreground);
967
+ }
968
+ .pretui-diff[data-language]
969
+ .pretui-diff-line[data-op='add']
970
+ .pretui-diff-sign {
971
+ color: var(--success-ink);
972
+ }
973
+ .pretui-diff[data-language]
974
+ .pretui-diff-line[data-op='del']
975
+ .pretui-diff-sign {
976
+ color: var(--destructive-ink);
977
+ }
978
+ .pretui-code-t {
979
+ color: inherit;
980
+ }
981
+ .pretui-code-comment {
982
+ color: color-mix(
983
+ in oklch,
984
+ var(--foreground) 8%,
985
+ var(--pretui-code-comment, var(--muted-foreground))
986
+ );
987
+ font-style: italic;
988
+ }
989
+ .pretui-code-string {
990
+ color: color-mix(
991
+ in oklch,
992
+ var(--foreground) 18%,
993
+ var(--pretui-code-string, var(--chart-1))
994
+ );
995
+ }
996
+ .pretui-code-number {
997
+ color: color-mix(
998
+ in oklch,
999
+ var(--foreground) 18%,
1000
+ var(--pretui-code-number, var(--chart-3))
1001
+ );
1002
+ }
1003
+ .pretui-code-keyword {
1004
+ color: color-mix(
1005
+ in oklch,
1006
+ var(--foreground) 18%,
1007
+ var(--pretui-code-keyword, var(--chart-4))
1008
+ );
1009
+ font-weight: 400;
1010
+ }
1011
+ .pretui-code-name {
1012
+ color: color-mix(
1013
+ in oklch,
1014
+ var(--foreground) 18%,
1015
+ var(--pretui-code-name, var(--chart-2))
1016
+ );
1017
+ }
1018
+ .pretui-code-punct {
1019
+ color: color-mix(
1020
+ in oklch,
1021
+ var(--muted-foreground) 70%,
1022
+ var(--foreground)
1023
+ );
69
1024
  }
70
1025
  .pretui-diff-toggle {
71
1026
  display: block;
72
1027
  width: 100%;
73
1028
  text-align: left;
74
1029
  border: 0;
75
- background: none;
76
- font: inherit;
77
- color: var(--pretui-primary-ink, var(--primary));
1030
+ background-color: transparent;
1031
+ font-family: var(--font-sans);
1032
+ font-size: var(--boxel-caption-font-size);
1033
+ line-height: var(--boxel-caption-line-height);
1034
+ font-variant-numeric: tabular-nums;
1035
+ color: var(--primary-ink);
78
1036
  cursor: pointer;
79
1037
  }
1038
+ /* the unchanged run between two hunks: a quiet gap, never a line
1039
+ of the file */
1040
+ .pretui-diff-skip {
1041
+ font-family: var(--font-sans);
1042
+ font-size: var(--boxel-caption-font-size);
1043
+ line-height: var(--boxel-caption-line-height);
1044
+ color: var(--muted-foreground);
1045
+ background-color: color-mix(in oklch, var(--border) 35%, transparent);
1046
+ user-select: none;
1047
+ }
80
1048
  .pretui-diff-receipt {
81
- padding: 5px 10px;
82
- font-size: 10.5px;
83
- color: var(--ink-3, var(--boxel-400));
1049
+ padding: var(--boxel-sp-4xs) var(--boxel-sp-sm);
1050
+ font-family: var(--font-sans);
1051
+ font-size: var(--boxel-caption-font-size);
1052
+ line-height: var(--boxel-caption-line-height);
1053
+ font-variant-numeric: tabular-nums;
1054
+ color: var(--subtle-foreground);
84
1055
  box-shadow: 0 -1px 0 var(--border);
85
1056
  }
86
1057
  }