@astryxdesign/lab 0.7.6-canary.fc44bc3 → 0.7.6-canary.fda92ac

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.
@@ -1 +1 @@
1
- {"version":3,"file":"DiffViewer.d.ts","sourceRoot":"","sources":["../../src/DiffViewer/DiffViewer.tsx"],"names":[],"mappings":"AAqCA,OAAO,EAQL,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,GAAG,EAET,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAGL,KAAK,YAAY,EAGlB,MAAM,8BAA8B,CAAC;AAOtC,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,iCAAiC,CAAC;AAqBzC,OAAO,KAAK,EACV,cAAc,EAGd,iBAAiB,EAElB,MAAM,aAAa,CAAC;AAGrB,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAAC;AAErD;;;;GAIG;AACH,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpE;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjE,MAAM,WAAW,eAAgB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAChE,yCAAyC;IACzC,GAAG,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC1B;;;OAGG;IACH,IAAI,EAAE,cAAc,CAAC;IACrB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IACjC;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC5C;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,KAAK,IAAI,CAAC;IACnD;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,SAAS,CAAC;IAC7D;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,CAClB,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,KACjC,IAAI,CAAC;IACV,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B;;;OAGG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC;AAq2BD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,EACzB,GAAG,EACH,IAAI,EACJ,QAAoB,EACpB,QAAQ,EAAE,YAAY,EACtB,aAAa,EACb,cAAqB,EACrB,SAAiB,EACjB,eAA2B,EAC3B,eAAwB,EACxB,mBAA0B,EAC1B,aAAoB,EACpB,WAAW,EAAE,eAAe,EAC5B,kBAA0B,EAC1B,iBAAiB,EACjB,SAAS,EACT,oBAAoB,EACpB,iBAAiB,EACjB,SAAS,EACT,WAAW,EACX,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EACT,EAAE,eAAe,+BAopBjB;yBA5qBe,UAAU"}
1
+ {"version":3,"file":"DiffViewer.d.ts","sourceRoot":"","sources":["../../src/DiffViewer/DiffViewer.tsx"],"names":[],"mappings":"AAuCA,OAAO,EAQL,KAAK,UAAU,EACf,KAAK,SAAS,EACd,KAAK,GAAG,EAET,MAAM,OAAO,CAAC;AAGf,OAAO,KAAK,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAClD,OAAO,EAGL,KAAK,YAAY,EAGlB,MAAM,8BAA8B,CAAC;AAQtC,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,iCAAiC,CAAC;AAsBzC,OAAO,KAAK,EACV,cAAc,EAGd,iBAAiB,EAElB,MAAM,aAAa,CAAC;AAGrB,uEAAuE;AACvE,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,OAAO,CAAC;AAErD;;;;GAIG;AACH,MAAM,MAAM,yBAAyB,GAAG,SAAS,GAAG,MAAM,GAAG,MAAM,CAAC;AAEpE;;;GAGG;AACH,MAAM,MAAM,yBAAyB,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAEjE,MAAM,WAAW,eAAgB,SAAQ,SAAS,CAAC,cAAc,CAAC;IAChE,yCAAyC;IACzC,GAAG,CAAC,EAAE,GAAG,CAAC,cAAc,CAAC,CAAC;IAC1B;;;OAGG;IACH,IAAI,EAAE,cAAc,CAAC;IACrB;;;;;;;;OAQG;IACH,QAAQ,CAAC,EAAE,kBAAkB,CAAC;IAC9B;;;;;;;OAOG;IACH,QAAQ,CAAC,EAAE,MAAM,GAAG,YAAY,CAAC;IACjC;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAC5C;;;OAGG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;;;;;OAQG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB;;;OAGG;IACH,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C;;;;;OAKG;IACH,eAAe,CAAC,EAAE,yBAAyB,CAAC;IAC5C;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,uEAAuE;IACvE,iBAAiB,CAAC,EAAE,CAAC,WAAW,EAAE,OAAO,KAAK,IAAI,CAAC;IACnD;;;OAGG;IACH,SAAS,CAAC,EAAE,SAAS,CAAC;IACtB;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,SAAS,CAAC;IAC7D;;;;;;OAMG;IACH,iBAAiB,CAAC,EAAE,CAClB,GAAG,EAAE,iBAAiB,EACtB,KAAK,EAAE,UAAU,CAAC,iBAAiB,CAAC,KACjC,IAAI,CAAC;IACV,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC5B;;;OAGG;IACH,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC;AA++BD;;;;;;;;;;GAUG;AACH,wBAAgB,UAAU,CAAC,EACzB,GAAG,EACH,IAAI,EACJ,QAAoB,EACpB,QAAQ,EAAE,YAAY,EACtB,aAAa,EACb,cAAqB,EACrB,SAAiB,EACjB,eAA2B,EAC3B,eAAwB,EACxB,mBAA0B,EAC1B,aAAoB,EACpB,WAAW,EAAE,eAAe,EAC5B,kBAA0B,EAC1B,iBAAiB,EACjB,SAAS,EACT,oBAAoB,EACpB,iBAAiB,EACjB,SAAS,EACT,WAAW,EACX,MAAM,EACN,SAAS,EACT,KAAK,EACL,GAAG,KAAK,EACT,EAAE,eAAe,+BA0uBjB;yBAlwBe,UAAU"}
@@ -4,7 +4,7 @@
4
4
  * @file DiffViewer.tsx
5
5
  * @input Uses React, StyleX, theme tokens, the shared CodeBlock tokenizer and
6
6
  * syntax-theme slots (incl. the diff row slots), useScrollableArea,
7
- * Token, Icon, VisuallyHidden, and the i18n translator
7
+ * Token, Icon, Button, VisuallyHidden, and the i18n translator
8
8
  * @output Exports DiffViewer component, DiffViewerProps, DiffViewerViewMode,
9
9
  * DiffViewerChangeIndicator, DiffViewerInlineHighlight
10
10
  * @position Lab implementation; read-only, syntax-highlighted file diff for
@@ -12,6 +12,8 @@
12
12
  *
13
13
  * Renders one DiffViewerFile: a file header (path, status, +/- counts, a
14
14
  * caller slot, optional disclosure) over its hunks in unified or split view.
15
+ * A hunk marked `isReplacedBlock` shows a labeled fold in place of its
16
+ * changed lines until the reader opens it.
15
17
  * Unified view is one table. Split view is two side-by-side panes (each an
16
18
  * ARIA table) whose rows share one grid's row tracks through `subgrid`, so a
17
19
  * row is as tall as its taller side while each pane scrolls sideways on its
@@ -39,12 +41,13 @@ import { Fragment, useId, useInsertionEffect, useMemo, useRef, useState } from '
39
41
  import * as stylex from '@stylexjs/stylex';
40
42
  import { ensureHighlightStyles, tokenize } from '@astryxdesign/core/CodeBlock';
41
43
  import { useScrollableArea } from '@astryxdesign/core/hooks';
44
+ import { Button } from '@astryxdesign/core/Button';
42
45
  import { useTranslator } from '@astryxdesign/core/i18n';
43
46
  import { Icon } from '@astryxdesign/core/Icon';
44
47
  import { SyntaxTheme } from '@astryxdesign/core/theme/syntax';
45
48
  import '@astryxdesign/core/theme/tokens.stylex';
46
49
  import { borderVars, colorVars, durationVars, easeVars, fontWeightVars, radiusVars, spacingVars, typeScaleVars, typographyVars } from '@astryxdesign/core/theme/tokens.stylex';
47
- import { focusOutlineProps, mergeProps, rtlStyles, themeProps } from '@astryxdesign/core/utils';
50
+ import { focusOutlineProps, isRenderable, mergeProps, rtlStyles, themeProps } from '@astryxdesign/core/utils';
48
51
  import { Token } from '@astryxdesign/core/Token';
49
52
  import { VisuallyHidden } from '@astryxdesign/core/VisuallyHidden';
50
53
  import { diffInlineRanges } from "./inlineDiff.js";
@@ -134,6 +137,41 @@ function inferLanguage(path, codeLanguages) {
134
137
  // Row model
135
138
  // ---------------------------------------------------------------------------
136
139
 
140
+ /** The control that folds a replaced block's changed lines. */
141
+
142
+ /**
143
+ * The caller's annotation for a line, or null when it renders nothing. An
144
+ * annotation row is drawn only for a note that renders, so
145
+ * `hasThread && <Thread />` adds no empty row.
146
+ */
147
+ function lineAnnotation(renderLineAnnotation, ref) {
148
+ const note = renderLineAnnotation(ref);
149
+ return isRenderable(note) ? note : null;
150
+ }
151
+
152
+ /**
153
+ * Replaced blocks with an annotation on one of their changed lines. Such a
154
+ * block starts open, so a review comment is never hidden behind the fold.
155
+ */
156
+ function findAnnotatedBlocks(file, renderLineAnnotation) {
157
+ const blocks = new Set();
158
+ if (renderLineAnnotation == null) {
159
+ return blocks;
160
+ }
161
+ file.hunks.forEach((hunk, hunkIndex) => {
162
+ if (hunk.isReplacedBlock !== true) {
163
+ return;
164
+ }
165
+ const isAnnotated = hunk.lines.some(line => {
166
+ const ref = line.kind === 'context' ? null : lineRef(line, hunkIndex);
167
+ return ref != null && lineAnnotation(renderLineAnnotation, ref) != null;
168
+ });
169
+ if (isAnnotated) {
170
+ blocks.add(hunkIndex);
171
+ }
172
+ });
173
+ return blocks;
174
+ }
137
175
  function lineRef(line, hunkIndex) {
138
176
  if (line.kind === 'removed') {
139
177
  return line.oldLineNumber == null ? null : {
@@ -181,44 +219,96 @@ function addInlineEmphasis(cells, granularity) {
181
219
  }
182
220
 
183
221
  /**
184
- * Tokenize each hunk's old and new side as continuous text, so constructs
185
- * that span lines (block comments, template strings) keep their paint within
186
- * the hunk, then hand each line the tokens of the side it renders from.
222
+ * Tokenize a run of hunk lines with its old and new side each as continuous
223
+ * text, so constructs that span lines (block comments, template strings) keep
224
+ * their paint within the run, and give each line the tokens of the side it
225
+ * renders from.
226
+ */
227
+ function tokenizeRun(lines, language) {
228
+ if (language === 'plaintext') {
229
+ return lines.map(() => []);
230
+ }
231
+ const sideText = kind => lines.filter(line => line.kind !== kind).map(line => line.content).join('\n');
232
+ const oldTokens = tokenize(sideText('added'), language);
233
+ const newTokens = tokenize(sideText('removed'), language);
234
+ let oldIndex = 0;
235
+ let newIndex = 0;
236
+ return lines.map(line => {
237
+ if (line.kind === 'removed') {
238
+ return oldTokens[oldIndex++] ?? [];
239
+ }
240
+ if (line.kind === 'added') {
241
+ return newTokens[newIndex++] ?? [];
242
+ }
243
+ oldIndex++;
244
+ return newTokens[newIndex++] ?? [];
245
+ });
246
+ }
247
+
248
+ /**
249
+ * Build each hunk's line cells. A folded replaced block keeps only the
250
+ * context around it, and its changed lines are not tokenized.
187
251
  */
188
- function buildCells(file, language, inlineHighlight) {
252
+ function buildCells(file, language, inlineHighlight, expandedBlocks) {
189
253
  return file.hunks.map((hunk, hunkIndex) => {
190
- const oldLines = hunk.lines.filter(line => line.kind !== 'added');
191
- const newLines = hunk.lines.filter(line => line.kind !== 'removed');
192
- const highlight = language !== 'plaintext';
193
- const oldTokens = highlight ? tokenize(oldLines.map(line => line.content).join('\n'), language) : [];
194
- const newTokens = highlight ? tokenize(newLines.map(line => line.content).join('\n'), language) : [];
195
- let oldIndex = 0;
196
- let newIndex = 0;
197
254
  const cells = [];
198
- for (const line of hunk.lines) {
199
- let tokens;
200
- if (line.kind === 'removed') {
201
- tokens = oldTokens[oldIndex++] ?? [];
202
- } else if (line.kind === 'added') {
203
- tokens = newTokens[newIndex++] ?? [];
204
- } else {
205
- oldIndex++;
206
- tokens = newTokens[newIndex++] ?? [];
207
- }
208
- const ref = lineRef(line, hunkIndex);
209
- if (ref != null) {
210
- cells.push({
211
- line,
212
- ref,
213
- tokens,
214
- emphasis: []
215
- });
255
+ const addRun = lines => {
256
+ const tokens = tokenizeRun(lines, language);
257
+ lines.forEach((line, i) => {
258
+ const ref = lineRef(line, hunkIndex);
259
+ if (ref != null) {
260
+ cells.push({
261
+ line,
262
+ ref,
263
+ tokens: tokens[i],
264
+ emphasis: []
265
+ });
266
+ }
267
+ });
268
+ };
269
+ const changeStart = hunk.isReplacedBlock === true ? hunk.lines.findIndex(line => line.kind !== 'context') : -1;
270
+ if (changeStart === -1) {
271
+ addRun(hunk.lines);
272
+ if (inlineHighlight !== 'none') {
273
+ addInlineEmphasis(cells, inlineHighlight);
216
274
  }
275
+ return {
276
+ cells,
277
+ fold: null
278
+ };
217
279
  }
218
- if (inlineHighlight !== 'none') {
219
- addInlineEmphasis(cells, inlineHighlight);
280
+
281
+ // A replaced block's removed and added lines are not edits of each
282
+ // other, so they get no inline highlight.
283
+ const isExpanded = expandedBlocks.has(hunkIndex);
284
+ const changedLines = hunk.lines.filter(line => line.kind !== 'context').length;
285
+ if (isExpanded) {
286
+ addRun(hunk.lines);
287
+ const at = cells.findIndex(cell => cell.line.kind !== 'context');
288
+ return {
289
+ cells,
290
+ fold: {
291
+ hunkIndex,
292
+ at,
293
+ changedLines,
294
+ isExpanded
295
+ }
296
+ };
220
297
  }
221
- return cells;
298
+ // Leading and trailing context are tokenized apart: the hidden lines
299
+ // between them can open or close a comment or a string.
300
+ addRun(hunk.lines.slice(0, changeStart));
301
+ const at = cells.length;
302
+ addRun(hunk.lines.slice(changeStart + changedLines));
303
+ return {
304
+ cells,
305
+ fold: {
306
+ hunkIndex,
307
+ at,
308
+ changedLines,
309
+ isExpanded
310
+ }
311
+ };
222
312
  });
223
313
  }
224
314
  function buildUnifiedRows(file, cells) {
@@ -229,13 +319,31 @@ function buildUnifiedRows(file, cells) {
229
319
  key: `h${h}`,
230
320
  header: hunk.header
231
321
  });
232
- cells[h].forEach((cell, i) => {
322
+ const {
323
+ cells: hunkCells,
324
+ fold
325
+ } = cells[h];
326
+ hunkCells.forEach((cell, i) => {
327
+ if (fold != null && fold.at === i) {
328
+ rows.push({
329
+ type: 'fold',
330
+ key: `h${h}f`,
331
+ fold
332
+ });
333
+ }
233
334
  rows.push({
234
335
  type: 'line',
235
336
  key: `h${h}l${i}`,
236
337
  cell
237
338
  });
238
339
  });
340
+ if (fold != null && fold.at === hunkCells.length) {
341
+ rows.push({
342
+ type: 'fold',
343
+ key: `h${h}f`,
344
+ fold
345
+ });
346
+ }
239
347
  });
240
348
  return rows;
241
349
  }
@@ -252,9 +360,20 @@ function buildSplitRows(file, cells) {
252
360
  key: `h${h}`,
253
361
  header: hunk.header
254
362
  });
255
- const hunkCells = cells[h];
363
+ const {
364
+ cells: hunkCells,
365
+ fold
366
+ } = cells[h];
256
367
  let i = 0;
257
368
  while (i < hunkCells.length) {
369
+ // The fold sits where a run of changes starts or ends, never inside one.
370
+ if (fold != null && fold.at === i) {
371
+ rows.push({
372
+ type: 'fold',
373
+ key: `h${h}f`,
374
+ fold
375
+ });
376
+ }
258
377
  const cell = hunkCells[i];
259
378
  if (cell.line.kind === 'context') {
260
379
  rows.push({
@@ -284,6 +403,13 @@ function buildSplitRows(file, cells) {
284
403
  });
285
404
  }
286
405
  }
406
+ if (fold != null && fold.at === hunkCells.length) {
407
+ rows.push({
408
+ type: 'fold',
409
+ key: `h${h}f`,
410
+ fold
411
+ });
412
+ }
287
413
  });
288
414
  return rows;
289
415
  }
@@ -764,7 +890,26 @@ export function DiffViewer({
764
890
  }
765
891
  onCollapsedChange?.(next);
766
892
  };
767
- const cells = useMemo(() => buildCells(file, language, inlineHighlight), [file, language, inlineHighlight]);
893
+
894
+ // A replaced block starts folded unless one of its lines is annotated. The
895
+ // reader's toggles are kept per file; a new file starts over.
896
+ const [blockToggles, setBlockToggles] = useState(null);
897
+ const toggles = blockToggles?.file === file ? blockToggles.isExpanded : null;
898
+ const annotatedBlocks = findAnnotatedBlocks(file, renderLineAnnotation);
899
+ const isBlockExpanded = hunkIndex => toggles?.get(hunkIndex) ?? annotatedBlocks.has(hunkIndex);
900
+ const toggleBlock = hunkIndex => {
901
+ const next = new Map(toggles);
902
+ next.set(hunkIndex, !isBlockExpanded(hunkIndex));
903
+ setBlockToggles({
904
+ file,
905
+ isExpanded: next
906
+ });
907
+ };
908
+ // A key of the open blocks keeps the set stable across renders, so the
909
+ // cells are built again only when a block opens or closes.
910
+ const expandedKey = file.hunks.flatMap((hunk, hunkIndex) => hunk.isReplacedBlock === true && isBlockExpanded(hunkIndex) ? [hunkIndex] : []).join(',');
911
+ const expandedBlocks = useMemo(() => new Set(expandedKey === '' ? [] : expandedKey.split(',').map(Number)), [expandedKey]);
912
+ const cells = useMemo(() => buildCells(file, language, inlineHighlight, expandedBlocks), [file, language, inlineHighlight, expandedBlocks]);
768
913
  const unifiedRows = useMemo(() => viewMode === 'unified' ? buildUnifiedRows(file, cells) : [], [viewMode, file, cells]);
769
914
  const splitRows = useMemo(() => viewMode === 'split' ? buildSplitRows(file, cells) : [], [viewMode, file, cells]);
770
915
  let addedCount = 0;
@@ -875,6 +1020,27 @@ export function DiffViewer({
875
1020
  children: cell != null && buildSpans(cell.line.content, cell.tokens, cell.emphasis, [styles.change, kind === 'added' ? styles.changeAdded : styles.changeRemoved])
876
1021
  }, key);
877
1022
  };
1023
+ const renderFold = fold => /*#__PURE__*/_jsxs("span", {
1024
+ ...{
1025
+ 0: {
1026
+ className: "x3nfvp2 x6s0dn4 x1a02dak xjcht0a"
1027
+ },
1028
+ 1: {
1029
+ className: "x3nfvp2 x6s0dn4 x1a02dak xjcht0a x7wzq59 x1ybfrjj"
1030
+ }
1031
+ }[!!isPinned << 0],
1032
+ children: [/*#__PURE__*/_jsx("span", {
1033
+ children: t('@astryx.diffViewer.largeChange', {
1034
+ count: fold.changedLines
1035
+ })
1036
+ }), /*#__PURE__*/_jsx(Button, {
1037
+ size: "sm",
1038
+ variant: "secondary",
1039
+ label: fold.isExpanded ? t('@astryx.diffViewer.hideFullDiff') : t('@astryx.diffViewer.showFullDiff'),
1040
+ "aria-expanded": fold.isExpanded,
1041
+ onClick: () => toggleBlock(fold.hunkIndex)
1042
+ })]
1043
+ });
878
1044
  const renderHunkText = header => /*#__PURE__*/_jsx("span", {
879
1045
  ...{
880
1046
  0: {
@@ -886,16 +1052,7 @@ export function DiffViewer({
886
1052
  }[!!isPinned << 0],
887
1053
  children: header
888
1054
  });
889
-
890
- // `null`, `undefined` and booleans render nothing in React, so they draw no
891
- // row either; that keeps `hasThread && <Thread />` from adding empty rows.
892
- const annotationFor = cell => {
893
- if (cell == null || renderLineAnnotation == null) {
894
- return null;
895
- }
896
- const note = renderLineAnnotation(cell.ref);
897
- return typeof note === 'boolean' ? null : note;
898
- };
1055
+ const annotationFor = cell => cell == null || renderLineAnnotation == null ? null : lineAnnotation(renderLineAnnotation, cell.ref);
899
1056
 
900
1057
  // Name every column so a screen reader moving across a row hears which
901
1058
  // number is which (and, in split view, which side's code it is in).
@@ -955,6 +1112,17 @@ export function DiffViewer({
955
1112
  })
956
1113
  }, row.key);
957
1114
  }
1115
+ if (row.type === 'fold') {
1116
+ return /*#__PURE__*/_jsx("tr", {
1117
+ children: /*#__PURE__*/_jsx("td", {
1118
+ colSpan: columns,
1119
+ ...{
1120
+ className: "xce4md1 xrrkdod x1yc453h x11ds9ch xv1l7n4 x9ynric x141an7d x1ltkj2j xszpfcc xykt701 xgtxpae"
1121
+ },
1122
+ children: renderFold(row.fold)
1123
+ })
1124
+ }, row.key);
1125
+ }
958
1126
  const note = annotationFor(row.cell);
959
1127
  return /*#__PURE__*/_jsxs(Fragment, {
960
1128
  children: [/*#__PURE__*/_jsxs("tr", {
@@ -1021,7 +1189,7 @@ export function DiffViewer({
1021
1189
  };
1022
1190
  };
1023
1191
  const renderSplit = () => {
1024
- const items = splitRows.map(row => row.type === 'hunk' ? {
1192
+ const items = splitRows.map(row => row.type !== 'pair' ? {
1025
1193
  row,
1026
1194
  oldNote: null,
1027
1195
  newNote: null
@@ -1033,7 +1201,7 @@ export function DiffViewer({
1033
1201
  newNote: annotationFor(row.new)
1034
1202
  });
1035
1203
  const hasSizer = !isWrapped;
1036
- const sizerText = hasSizer ? widestLine(cells.flatMap(hunkCells => hunkCells.map(cell => cell.line.content))) : '';
1204
+ const sizerText = hasSizer ? widestLine(cells.flatMap(hunk => hunk.cells.map(cell => cell.line.content))) : '';
1037
1205
  const rowCount = 1 + (hasSizer ? 1 : 0) + items.reduce((count, item) => count + 1 + (item.oldNote != null || item.newNote != null ? 1 : 0), 0);
1038
1206
  const columns = `${hasLineNumbers ? `${gutter} ` : ''}${markerWidth} ${isWrapped ? 'minmax(0, 1fr)' : '1fr'}`;
1039
1207
  const markerPin = hasLineNumbers ? gutter : '0px';
@@ -1084,6 +1252,41 @@ export function DiffViewer({
1084
1252
  }, row.key));
1085
1253
  continue;
1086
1254
  }
1255
+ if (row.type === 'fold') {
1256
+ // Only the old side holds the control; the new side keeps its row
1257
+ // as tall and gives its own table's readers the label.
1258
+ rows.push(/*#__PURE__*/_jsx("div", {
1259
+ role: "row",
1260
+ ...{
1261
+ className: "xrvj5dj x1osaytk x4w6khx"
1262
+ },
1263
+ children: /*#__PURE__*/_jsx("div", {
1264
+ role: "cell",
1265
+ ...{
1266
+ 0: {
1267
+ className: "xce4md1 xrrkdod x1yc453h x11ds9ch xv1l7n4 x9ynric x141an7d x1ltkj2j xszpfcc xykt701 xgtxpae x1osaytk"
1268
+ },
1269
+ 1: {
1270
+ className: "xce4md1 xrrkdod x1yc453h x11ds9ch xv1l7n4 x9ynric x141an7d x1ltkj2j xszpfcc xykt701 xgtxpae x1osaytk x126k92a x1lldw8n xj0a0fe x5ujdhh"
1271
+ }
1272
+ }[!!isWrapped << 0],
1273
+ children: isOld ? renderFold(row.fold) : /*#__PURE__*/_jsxs(_Fragment, {
1274
+ children: [/*#__PURE__*/_jsx("span", {
1275
+ "aria-hidden": "true",
1276
+ ...{
1277
+ className: "xlshs6z"
1278
+ },
1279
+ children: renderFold(row.fold)
1280
+ }), /*#__PURE__*/_jsx(VisuallyHidden, {
1281
+ children: t('@astryx.diffViewer.largeChange', {
1282
+ count: row.fold.changedLines
1283
+ })
1284
+ })]
1285
+ })
1286
+ })
1287
+ }, row.key));
1288
+ continue;
1289
+ }
1087
1290
  const cell = isOld ? row.old : row.new;
1088
1291
  rows.push(/*#__PURE__*/_jsxs("div", {
1089
1292
  role: "row"
@@ -8,8 +8,8 @@
8
8
  *
9
9
  * Shared leading and trailing lines are matched without a search. The minimal
10
10
  * search over the rest stops after MAX_EDIT_DISTANCE changed lines, so memory
11
- * stays bounded for any input; a middle that differs more than that is shown
12
- * as one replaced block rather than a minimal diff.
11
+ * stays bounded for any input; a middle that differs more than that becomes
12
+ * one replaced block, marked with `isReplacedBlock` on its hunk.
13
13
  *
14
14
  * SYNC: When modified, update:
15
15
  * - /packages/lab/src/DiffViewer/createDiffFile.test.ts
@@ -1 +1 @@
1
- {"version":3,"file":"createDiffFile.d.ts","sourceRoot":"","sources":["../../src/DiffViewer/createDiffFile.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,qBAAqB;IACpC,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAiJD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,aAAa,EACtB,YAAgB,GACjB,EAAE,qBAAqB,GAAG,cAAc,CA0GxC"}
1
+ {"version":3,"file":"createDiffFile.d.ts","sourceRoot":"","sources":["../../src/DiffViewer/createDiffFile.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AAEH,OAAO,KAAK,EACV,cAAc,EAIf,MAAM,aAAa,CAAC;AAErB,MAAM,WAAW,qBAAqB;IACpC,yBAAyB;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,sEAAsE;IACtE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA0JD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,EAC7B,QAAQ,EACR,QAAQ,EACR,IAAI,EACJ,OAAO,EAAE,aAAa,EACtB,OAAO,EAAE,aAAa,EACtB,YAAgB,GACjB,EAAE,qBAAqB,GAAG,cAAc,CA4GxC"}
@@ -10,8 +10,8 @@
10
10
  *
11
11
  * Shared leading and trailing lines are matched without a search. The minimal
12
12
  * search over the rest stops after MAX_EDIT_DISTANCE changed lines, so memory
13
- * stays bounded for any input; a middle that differs more than that is shown
14
- * as one replaced block rather than a minimal diff.
13
+ * stays bounded for any input; a middle that differs more than that becomes
14
+ * one replaced block, marked with `isReplacedBlock` on its hunk.
15
15
  *
16
16
  * SYNC: When modified, update:
17
17
  * - /packages/lab/src/DiffViewer/createDiffFile.test.ts
@@ -35,7 +35,6 @@ function splitLines(value) {
35
35
  * megabytes; past it the changed middle is shown as one replaced block.
36
36
  */
37
37
  const MAX_EDIT_DISTANCE = 2000;
38
-
39
38
  /**
40
39
  * Line edit script from `a` to `b`. Shared leading and trailing lines are
41
40
  * context without a search; the middle uses Myers' O(ND) shortest edit script,
@@ -61,7 +60,10 @@ function diffLines(a, b) {
61
60
  b: i
62
61
  });
63
62
  }
64
- const middle = myers(a, b, head, tailA, tailB);
63
+ // When one side of the middle is empty, adding or removing the other side
64
+ // is already the minimal script.
65
+ const isOneSided = tailA === head || tailB === head;
66
+ const middle = isOneSided ? null : myers(a, b, head, tailA, tailB);
65
67
  if (middle != null) {
66
68
  // A loop, not a spread: one call argument per op overflows the stack on
67
69
  // long inputs.
@@ -89,7 +91,10 @@ function diffLines(a, b) {
89
91
  b: tailB + i
90
92
  });
91
93
  }
92
- return ops;
94
+ return {
95
+ ops,
96
+ isReplacedBlock: middle == null && !isOneSided
97
+ };
93
98
  }
94
99
 
95
100
  /**
@@ -197,7 +202,10 @@ export function createDiffFile({
197
202
  }
198
203
  const a = splitLines(oldValue);
199
204
  const b = splitLines(newValue);
200
- const ops = diffLines(a, b);
205
+ const {
206
+ ops,
207
+ isReplacedBlock
208
+ } = diffLines(a, b);
201
209
  const changed = [];
202
210
  ops.forEach((op, i) => {
203
211
  if (op.kind !== 'context') {
@@ -269,7 +277,11 @@ export function createDiffFile({
269
277
  oldLines: oldCount,
270
278
  newStart,
271
279
  newLines: newCount,
272
- lines
280
+ lines,
281
+ // A replaced block is the only change, so it fills the only hunk.
282
+ ...(isReplacedBlock ? {
283
+ isReplacedBlock: true
284
+ } : {})
273
285
  });
274
286
  g = h;
275
287
  }
@@ -39,6 +39,13 @@ export interface DiffViewerHunk {
39
39
  newStart: number;
40
40
  newLines: number;
41
41
  lines: DiffViewerLine[];
42
+ /**
43
+ * True when the change was too large to compare line by line, so the hunk
44
+ * lists every old line as removed and every new line as added instead of a
45
+ * minimal diff. `createDiffFile` sets it; DiffViewer folds the changed lines
46
+ * behind a labeled control that shows them on request.
47
+ */
48
+ isReplacedBlock?: boolean;
42
49
  }
43
50
  /** One file's changes. */
44
51
  export interface DiffViewerFile {
@@ -1 +1 @@
1
- {"version":3,"file":"diffTypes.d.ts","sourceRoot":"","sources":["../../src/DiffViewer/diffTypes.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,oEAAoE;AACpE,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjE,uCAAuC;AACvC,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAEhF;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,KAAK,CAAC;AAE3C,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,sEAAsE;IACtE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED,0BAA0B;AAC1B,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,oBAAoB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,cAAc,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB"}
1
+ {"version":3,"file":"diffTypes.d.ts","sourceRoot":"","sources":["../../src/DiffViewer/diffTypes.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AAEH,oEAAoE;AACpE,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjE,uCAAuC;AACvC,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,CAAC;AAEhF;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,KAAK,GAAG,KAAK,CAAC;AAE3C,mEAAmE;AACnE,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,kBAAkB,CAAC;IACzB,mDAAmD;IACnD,OAAO,EAAE,MAAM,CAAC;IAChB,qEAAqE;IACrE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,sEAAsE;IACtE,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED,4EAA4E;AAC5E,MAAM,WAAW,cAAc;IAC7B,4EAA4E;IAC5E,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,cAAc,EAAE,CAAC;IACxB;;;;;OAKG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,0BAA0B;AAC1B,MAAM,WAAW,cAAc;IAC7B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,+DAA+D;IAC/D,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,MAAM,EAAE,oBAAoB,CAAC;IAC7B,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE,cAAc,EAAE,CAAC;CACzB;AAED;;;;;GAKG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,cAAc,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,cAAc,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astryxdesign/lab",
3
- "version": "0.7.6-canary.fc44bc3",
3
+ "version": "0.7.6-canary.fda92ac",
4
4
  "description": "Experimental Astryx components — published to npm only under the @canary dist-tag for early testing; never released as a stable (latest) version.",
5
5
  "author": "Meta Open Source",
6
6
  "license": "MIT",
@@ -49,7 +49,7 @@
49
49
  },
50
50
  "peerDependencies": {
51
51
  "@stylexjs/stylex": ">=0.10.0",
52
- "@astryxdesign/core": "0.7.6-canary.fc44bc3",
52
+ "@astryxdesign/core": "0.7.6-canary.fda92ac",
53
53
  "react": ">=19.0.0",
54
54
  "react-dom": ">=19.0.0"
55
55
  },
@@ -59,9 +59,9 @@
59
59
  "d3-shape": "^3.2.0"
60
60
  },
61
61
  "devDependencies": {
62
- "@astryxdesign/a11y-spec": "0.7.6-canary.fc44bc3",
63
- "@astryxdesign/cli": "0.7.6-canary.fc44bc3",
64
- "@astryxdesign/interaction-harness": "0.7.6-canary.fc44bc3",
62
+ "@astryxdesign/a11y-spec": "0.7.6-canary.fda92ac",
63
+ "@astryxdesign/cli": "0.7.6-canary.fda92ac",
64
+ "@astryxdesign/interaction-harness": "0.7.6-canary.fda92ac",
65
65
  "@babel/cli": "^7.29.7",
66
66
  "@babel/core": "^7.29.7",
67
67
  "@babel/preset-react": "^7.29.7",
@@ -30,7 +30,7 @@ export const docs = {
30
30
 
31
31
  usage: {
32
32
  description:
33
- 'Read-only, syntax-highlighted diff of one file for code review surfaces. Renders a file header (path, Added/Deleted/Renamed token, +/− line counts, a headerEnd slot, and an optional disclosure) over the file\'s hunks in unified or split (side-by-side) view. Rows use the same syntax-theme diff slots as CodeBlock with isDiff, so one syntax theme styles both. renderLineAnnotation adds a full-width row under any line (for review comments) and onLineNumberClick turns line numbers into buttons. Build the file model with parseUnifiedDiff(patch), which returns one file per file in a git or diff -u patch, or with createDiffFile({oldValue, newValue}) when you hold two versions of a text. Display options cover how a change is marked (classic +/− glyphs, bars, or none), whether changed rows are tinted, and word- or character-level highlighting of what changed inside each removed/added line pair. Each changed line carries a visually hidden "Added"/"Removed" label, so the change reads without colour in every mode. Unwrapped, line numbers and change indicators stay pinned while the code scrolls; in split view each side is its own scroller, so the split stays fixed at half and half, the sides scroll sideways in lockstep, and a row is as tall as its taller side. While code overflows sideways, sideways overscroll is off, so trackpads neither bounce past a line nor swipe the browser back. In right-to-left pages the hunk area stays left-to-right, like a code editor: annotation content reads in the page direction, and annotation rows scroll with the code. Rows are plain DOM with no virtualization: collapse or window very large files.',
33
+ 'Read-only, syntax-highlighted diff of one file for code review surfaces. Renders a file header (path, Added/Deleted/Renamed token, +/− line counts, a headerEnd slot, and an optional disclosure) over the file\'s hunks in unified or split (side-by-side) view. Rows use the same syntax-theme diff slots as CodeBlock with isDiff, so one syntax theme styles both. renderLineAnnotation adds a full-width row under any line (for review comments) and onLineNumberClick turns line numbers into buttons. Build the file model with parseUnifiedDiff(patch), which returns one file per file in a git or diff -u patch, or with createDiffFile({oldValue, newValue}) when you hold two versions of a text. Display options cover how a change is marked (classic +/− glyphs, bars, or none), whether changed rows are tinted, and word- or character-level highlighting of what changed inside each removed/added line pair. Each changed line carries a visually hidden "Added"/"Removed" label, so the change reads without colour in every mode. Unwrapped, line numbers and change indicators stay pinned while the code scrolls; in split view each side is its own scroller, so the split stays fixed at half and half, the sides scroll sideways in lockstep, and a row is as tall as its taller side. While code overflows sideways, sideways overscroll is off, so trackpads neither bounce past a line nor swipe the browser back. In right-to-left pages the hunk area stays left-to-right, like a code editor: annotation content reads in the page direction, and annotation rows scroll with the code. When a change is too large to compare line by line, createDiffFile marks its hunk with isReplacedBlock: it lists every old line as removed and every new line as added. DiffViewer folds those lines behind a "Large change: N lines" row, where N counts the block\'s removed plus added lines, with a Show full diff button. A block opens by itself when one of its lines has an annotation, so no review comment is hidden. Rows are plain DOM with no virtualization: collapse or window very large files.',
34
34
  bestPractices: [
35
35
  {
36
36
  guidance: true,