@atlaskit/editor-plugin-show-diff 20.0.11 → 21.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/cjs/pm-plugins/NodeViewSerializer.js +71 -7
  3. package/dist/cjs/pm-plugins/createEditorProxy.js +84 -0
  4. package/dist/cjs/pm-plugins/decorations/createContributorTagWidget.js +25 -6
  5. package/dist/cjs/pm-plugins/decorations/createNodeChangedDecorationWidget.js +35 -11
  6. package/dist/cjs/pm-plugins/decorations/utils/wrapBlockNodeView.js +38 -2
  7. package/dist/cjs/pm-plugins/markViews.js +65 -0
  8. package/dist/cjs/pm-plugins/resolveDiffContributors.js +3 -3
  9. package/dist/cjs/ui/ContributorTag/contributorAvatarRenderer.js +8 -9
  10. package/dist/cjs/ui/ContributorTag/contributorTagIcons.js +1 -8
  11. package/dist/es2019/pm-plugins/NodeViewSerializer.js +64 -8
  12. package/dist/es2019/pm-plugins/createEditorProxy.js +76 -0
  13. package/dist/es2019/pm-plugins/decorations/createContributorTagWidget.js +26 -8
  14. package/dist/es2019/pm-plugins/decorations/createNodeChangedDecorationWidget.js +35 -12
  15. package/dist/es2019/pm-plugins/decorations/utils/wrapBlockNodeView.js +36 -2
  16. package/dist/es2019/pm-plugins/markViews.js +61 -0
  17. package/dist/es2019/pm-plugins/resolveDiffContributors.js +3 -3
  18. package/dist/es2019/ui/ContributorTag/contributorAvatarRenderer.js +8 -9
  19. package/dist/es2019/ui/ContributorTag/contributorTagIcons.js +1 -8
  20. package/dist/esm/pm-plugins/NodeViewSerializer.js +71 -7
  21. package/dist/esm/pm-plugins/createEditorProxy.js +78 -0
  22. package/dist/esm/pm-plugins/decorations/createContributorTagWidget.js +27 -8
  23. package/dist/esm/pm-plugins/decorations/createNodeChangedDecorationWidget.js +37 -12
  24. package/dist/esm/pm-plugins/decorations/utils/wrapBlockNodeView.js +38 -2
  25. package/dist/esm/pm-plugins/markViews.js +60 -0
  26. package/dist/esm/pm-plugins/resolveDiffContributors.js +3 -3
  27. package/dist/esm/ui/ContributorTag/contributorAvatarRenderer.js +8 -9
  28. package/dist/esm/ui/ContributorTag/contributorTagIcons.js +1 -8
  29. package/dist/types/pm-plugins/NodeViewSerializer.d.ts +7 -2
  30. package/dist/types/pm-plugins/createEditorProxy.d.ts +18 -0
  31. package/dist/types/pm-plugins/decorations/createContributorTagWidget.d.ts +2 -1
  32. package/dist/types/pm-plugins/markViews.d.ts +14 -0
  33. package/dist/types/ui/ContributorTag/contributorTagIcons.d.ts +1 -4
  34. package/package.json +11 -11
@@ -1,5 +1,10 @@
1
+ import { expandedState } from '@atlaskit/editor-common/expand';
1
2
  import { DOMSerializer } from '@atlaskit/editor-prosemirror/model';
3
+ import { contains } from '@atlaskit/editor-prosemirror/utils';
2
4
  import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
5
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
6
+ import { createEditorProxy } from './createEditorProxy';
7
+ import { wrapInMarkViews } from './markViews';
3
8
 
4
9
  /**
5
10
  * Utilities for working with ProseMirror node views and DOM serialization within the
@@ -28,6 +33,18 @@ export function isEditorViewWithNodeViews(view) {
28
33
  return view.nodeViews !== undefined;
29
34
  }
30
35
 
36
+ /** Expand-family nodes whose expanded state lives in a node-keyed WeakMap. */
37
+ const EXPAND_TYPES = new Set(['expand', 'nestedExpand']);
38
+
39
+ /** Tables are the only node views that read and write positions, so only they need a preview. */
40
+ const containsTable = node => {
41
+ const {
42
+ table
43
+ } = node.type.schema.nodes;
44
+ // `contains` only checks descendants, so the node itself is checked separately.
45
+ return !!table && (node.type === table || contains(node, table));
46
+ };
47
+
31
48
  /**
32
49
  * Encapsulates DOM serialization and node view access/creation.
33
50
  *
@@ -66,12 +83,17 @@ export class NodeViewSerializer {
66
83
  /**
67
84
  * Appends serialized child nodes to the given contentDOM element.
68
85
  */
69
- appendChildNodes(children, contentDOM) {
86
+ appendChildNodes(children, contentDOM, basePos = 0, editorProxy) {
87
+ // A node's first child sits one position inside it, and each subsequent
88
+ // child is offset by the previous child's `nodeSize`. Tracking this lets nested node
89
+ // views resolve to their real depth instead of every one of them seeing depth 0.
90
+ let childPos = basePos + 1;
70
91
  children.forEach(child => {
71
- const childNode = this.tryCreateNodeView(child) || this.serializeNode(child);
92
+ const childNode = this.tryCreateNodeViewInner(child, childPos, editorProxy) || this.serializeNode(child);
72
93
  if (childNode) {
73
94
  contentDOM === null || contentDOM === void 0 ? void 0 : contentDOM.append(childNode);
74
95
  }
96
+ childPos += child.nodeSize;
75
97
  });
76
98
  }
77
99
 
@@ -80,8 +102,20 @@ export class NodeViewSerializer {
80
102
  *
81
103
  * Returns `null` when there is no `EditorView`, no constructor for the node type,
82
104
  * or the node type is blocklisted. Otherwise returns the constructed node view instance.
105
+ *
106
+ * `$sourcePos` is where the node sits in the doc it came from, so nested tables keep their depth.
83
107
  */
84
- tryCreateNodeView(targetNode) {
108
+ tryCreateNodeView(targetNode, basePos = 0, $sourcePos) {
109
+ var _preview$rootPos;
110
+ if (!this.editorView) {
111
+ return null;
112
+ }
113
+ // The preview becomes the document every nested node view resolves its position against,
114
+ // so positions stay internally consistent.
115
+ const preview = fg('platform_editor_ai_show_diff_patch_2') && containsTable(targetNode) ? createEditorProxy(this.editorView, targetNode, $sourcePos) : null;
116
+ return this.tryCreateNodeViewInner(targetNode, (_preview$rootPos = preview === null || preview === void 0 ? void 0 : preview.rootPos) !== null && _preview$rootPos !== void 0 ? _preview$rootPos : basePos, preview === null || preview === void 0 ? void 0 : preview.editorProxy);
117
+ }
118
+ tryCreateNodeViewInner(targetNode, basePos = 0, editorProxy) {
85
119
  var _this$nodeViews;
86
120
  if (!this.editorView) {
87
121
  return null;
@@ -129,22 +163,44 @@ export class NodeViewSerializer {
129
163
  if (!isExperimentEnabled('platform_editor_show_diff_deleted_nodeview_content') && targetNode.type.name === 'paragraph' && targetNode.children.length === 1) {
130
164
  return this.serializeFragment(targetNode.content);
131
165
  }
132
- this.appendChildNodes(targetNode.children, contentDOM);
166
+ this.appendChildNodes(targetNode.children, contentDOM, basePos, editorProxy);
133
167
  }
134
- return dom;
168
+ return this.withMarkViews(targetNode, dom, editorProxy);
135
169
  }
170
+
171
+ // The expand node view reads `expandedState.get(node) ?? false`, never `attrs.__expanded`.
172
+ // Open the preview so its tables get real layout; an existing entry is the live expand's
173
+ // own state, so leave it alone.
174
+ if (EXPAND_TYPES.has(targetNode.type.name) && !expandedState.has(targetNode) && fg('platform_editor_ai_show_diff_patch_2')) {
175
+ expandedState.set(targetNode, true);
176
+ }
177
+
178
+ // `isTableNested` is `doc.resolve(pos).depth > 0`, so the old hardcoded `0` made nested
179
+ // tables size as top-level. Positions are resolved against the preview document, so
180
+ // they are only meaningful with an editor proxy.
181
+ const view = editorProxy !== null && editorProxy !== void 0 ? editorProxy : this.editorView;
182
+ const docSize = view.state.doc.content.size;
183
+ const resolvedPos = editorProxy ? Math.max(0, Math.min(basePos, docSize)) : 0;
136
184
  const {
137
185
  dom,
138
186
  contentDOM
139
- } = constructor(targetNode, this.editorView, () => 0, [], {});
187
+ } = constructor(targetNode, view, () => resolvedPos, [], {});
140
188
  // Iteratively populate children
141
- this.appendChildNodes(targetNode.children, contentDOM);
142
- return dom;
189
+ this.appendChildNodes(targetNode.children, contentDOM, basePos, editorProxy);
190
+ return this.withMarkViews(targetNode, dom, editorProxy);
143
191
  } catch {
144
192
  return null;
145
193
  }
146
194
  }
147
195
 
196
+ /** Wraps rendered DOM in its marks, rendered against the editor proxy when there is one. */
197
+ withMarkViews(targetNode, nodeDom, editorProxy) {
198
+ return wrapInMarkViews(targetNode, nodeDom, {
199
+ nodeViews: this.nodeViews,
200
+ view: editorProxy !== null && editorProxy !== void 0 ? editorProxy : this.editorView
201
+ });
202
+ }
203
+
148
204
  /**
149
205
  * Serializes a node to a DOM `Node` using the schema's `DOMSerializer`.
150
206
  */
@@ -0,0 +1,76 @@
1
+ import { GapCursorSelection, Side } from '@atlaskit/editor-common/selection/gap-cursor/selection';
2
+ /**
3
+ * A preview is rendered by the live view's own node views, which read `getPos()` against
4
+ * `view.state.doc` for both layout (`isTableNested`) and writes (`scaleTable` builds attribute
5
+ * steps at `getPos() + 1`). Handing them live positions makes the layout correct but points the
6
+ * writes at unrelated live nodes, so give them a view whose document *is* the preview instead.
7
+ *
8
+ * With `$sourcePos` (the node's position in the doc it came from) the node is swapped into a copy
9
+ * of its real top-level ancestor, so a table inside an expand still resolves as nested.
10
+ *
11
+ * Returns `null` when the node cannot be a top-level child (e.g. a bare `tableCell`), in which
12
+ * case the caller falls back to the live view and the previous stubbed positions.
13
+ */
14
+ export function createEditorProxy(liveView, rootNode, $sourcePos) {
15
+ const liveState = liveView.state;
16
+ const createDoc = topLevel => liveState.schema.topNodeType.createChecked(null, topLevel);
17
+ let previewDoc;
18
+ let rootPos = 0;
19
+ try {
20
+ var _$sourcePos$nodeAfter;
21
+ if ($sourcePos && $sourcePos.depth > 0 && ((_$sourcePos$nodeAfter = $sourcePos.nodeAfter) === null || _$sourcePos$nodeAfter === void 0 ? void 0 : _$sourcePos$nodeAfter.type) === rootNode.type) {
22
+ let topLevel = rootNode;
23
+ for (let depth = $sourcePos.depth; depth > 0; depth--) {
24
+ const ancestor = $sourcePos.node(depth);
25
+ topLevel = ancestor.copy(ancestor.content.replaceChild($sourcePos.index(depth), topLevel));
26
+ }
27
+ previewDoc = createDoc(topLevel);
28
+ rootPos = $sourcePos.pos - $sourcePos.before(1);
29
+ } else {
30
+ previewDoc = createDoc(rootNode);
31
+ }
32
+ } catch {
33
+ return null;
34
+ }
35
+
36
+ // A gap cursor just before the node selects nothing, so the table's node view doesn't render
37
+ // as selected or show its controls.
38
+ const previewSelection = new GapCursorSelection(previewDoc.resolve(rootPos), Side.LEFT);
39
+ // Proxy rather than `EditorState.create` so plugin state is shared as-is: re-initialising
40
+ // every plugin per preview would be costly and re-runs their side effects.
41
+ let stateProxy = new Proxy(liveState, {
42
+ get(target, prop, receiver) {
43
+ if (prop === 'doc') {
44
+ return previewDoc;
45
+ }
46
+ if (prop === 'selection') {
47
+ return previewSelection;
48
+ }
49
+ return Reflect.get(target, prop, receiver);
50
+ }
51
+ });
52
+ const editorProxy = new Proxy(liveView, {
53
+ get(target, prop, receiver) {
54
+ if (prop === 'state') {
55
+ return stateProxy;
56
+ }
57
+ if (prop === 'dispatch') {
58
+ // Keep the transaction inside the preview. It was built against `previewDoc`, so
59
+ // applying it to the live state would corrupt the real document.
60
+ return tr => {
61
+ try {
62
+ stateProxy = stateProxy.apply(tr);
63
+ } catch {
64
+ // A preview is throwaway; a failed self-update is not worth surfacing.
65
+ }
66
+ };
67
+ }
68
+ const value = Reflect.get(target, prop, receiver);
69
+ return typeof value === 'function' ? value.bind(target) : value;
70
+ }
71
+ });
72
+ return {
73
+ editorProxy,
74
+ rootPos
75
+ };
76
+ }
@@ -2,8 +2,8 @@ import { Decoration } from '@atlaskit/editor-prosemirror/view';
2
2
  import { fg } from '@atlaskit/platform-feature-flags/fg';
3
3
  import { ContributorTagController } from '../../ui/ContributorTag/contributorTagController';
4
4
  import { buildCharsByOffset, isWhitespaceChar } from '../utils/charsByOffset';
5
- import { clampAnchorPosIntoCell } from './createAnchorDecorationWidgets';
6
- import { AnchorTypeKey, buildAnchorDecorationKey, buildContributorTagDecorationSpec } from './decorationKeys';
5
+ import { clampAnchorPosIntoCell, createAnchorNameSpan } from './createAnchorDecorationWidgets';
6
+ import { AnchorTypeKey, buildAnchorDecorationKey, buildAnchorDecorationSpec, buildContributorTagDecorationSpec } from './decorationKeys';
7
7
 
8
8
  /** Marks the host element of one tag, so a tag can be found in the document by its own diff. */
9
9
  export const CONTRIBUTOR_TAG_HOST_ATTRIBUTE = 'data-contributor-tag-host';
@@ -212,7 +212,8 @@ export const resolveHoistedCodeBlockAnchor = (doc, pos, diffId) => {
212
212
  * highlights — or, with `anchorAtRangeStart`, at `from` itself. A host inside a code block is
213
213
  * hoisted in front of it instead, anchored via `resolveHoistedCodeBlockAnchor` (EDITOR-9045).
214
214
  *
215
- * Returns one decoration normally, two (marker + tag) when hoisted out of a code block.
215
+ * Returns one decoration when a caller-supplied `anchorName` is used, two (marker + tag)
216
+ * otherwise — hoisted out of a code block, or anchored to its own point marker.
216
217
  *
217
218
  * The tag is mounted in `toDOM` and taken down in `destroy`, so it lives exactly as long as the host
218
219
  * element ProseMirror drew it into — see `mountContributorTag`.
@@ -226,7 +227,7 @@ export const createContributorTagWidget = ({
226
227
  diffId,
227
228
  mountContext
228
229
  }) => {
229
- var _hoisted$anchorName, _hoisted$codeBlockSta;
230
+ var _hoisted$codeBlockSta, _ref2, _hoisted$anchorName;
230
231
  if (!isContributorTagWidgetEnabled()) {
231
232
  return undefined;
232
233
  }
@@ -238,15 +239,32 @@ export const createContributorTagWidget = ({
238
239
  // A change inside a code block is hoisted out of it, anchored to its own marker; anything else
239
240
  // keeps its own anchor.
240
241
  const hoisted = resolveHoistedCodeBlockAnchor(doc, anchorPos, diffId);
241
- const effectiveAnchorName = (_hoisted$anchorName = hoisted === null || hoisted === void 0 ? void 0 : hoisted.anchorName) !== null && _hoisted$anchorName !== void 0 ? _hoisted$anchorName : anchorName;
242
+ // Keep the host out of the table row's grid (EDITOR-8442).
243
+ const hostPos = clampAnchorPosIntoCell(doc, (_hoisted$codeBlockSta = hoisted === null || hoisted === void 0 ? void 0 : hoisted.codeBlockStart) !== null && _hoisted$codeBlockSta !== void 0 ? _hoisted$codeBlockSta : anchorPos, 1);
244
+
245
+ // Inline diffs pass no anchorName. Give them their own point anchor instead of the
246
+ // `position: relative` fallback, which drifts left once the range wraps a line (EDITOR-9286).
247
+ const ownAnchorName = buildAnchorDecorationKey({
248
+ diffId,
249
+ anchorType: AnchorTypeKey.tag
250
+ });
251
+ const effectiveAnchorName = (_ref2 = (_hoisted$anchorName = hoisted === null || hoisted === void 0 ? void 0 : hoisted.anchorName) !== null && _hoisted$anchorName !== void 0 ? _hoisted$anchorName : anchorName) !== null && _ref2 !== void 0 ? _ref2 : ownAnchorName;
242
252
  const tagAnchorName = effectiveAnchorName && supportsAnchorPositioning() ? effectiveAnchorName : undefined;
243
253
  const decorations = [];
244
254
  if (hoisted) {
245
255
  decorations.push(hoisted.marker);
256
+ } else if (!anchorName && tagAnchorName) {
257
+ decorations.push(Decoration.widget(hostPos, () => createAnchorNameSpan(ownAnchorName), {
258
+ ...buildAnchorDecorationSpec({
259
+ diffId,
260
+ anchorType: AnchorTypeKey.tag,
261
+ side: -1
262
+ }),
263
+ marks: [],
264
+ ignoreSelection: true
265
+ }));
246
266
  }
247
- decorations.push(Decoration.widget(
248
- // Keep the host out of the table row's grid (EDITOR-8442).
249
- clampAnchorPosIntoCell(doc, (_hoisted$codeBlockSta = hoisted === null || hoisted === void 0 ? void 0 : hoisted.codeBlockStart) !== null && _hoisted$codeBlockSta !== void 0 ? _hoisted$codeBlockSta : anchorPos, 1), () => {
267
+ decorations.push(Decoration.widget(hostPos, () => {
250
268
  const host = buildContributorTagHost(diffId, tagAnchorName);
251
269
  mount = mountContributorTag({
252
270
  anchorName: tagAnchorName,
@@ -2,7 +2,7 @@ import { Decoration } from '@atlaskit/editor-prosemirror/view';
2
2
  import { fg } from '@atlaskit/platform-feature-flags/fg';
3
3
  import { countEmptyTextBlockOnlySlice } from '../utils/emptyTextBlocks';
4
4
  import { isEmptyParagraphSlice } from '../utils/isEmptyParagraphSlice';
5
- import { clampAnchorPosIntoCell, createLeftAnchorWidget } from './createAnchorDecorationWidgets';
5
+ import { clampAnchorPosIntoCell, createAnchorNameSpan, createLeftAnchorWidget } from './createAnchorDecorationWidgets';
6
6
  import { createChangedRowDecorationWidgets } from './createChangedRowDecorationWidgets';
7
7
  import { createContributorTagHost, resolveHoistedCodeBlockAnchor, unmountContributorTag } from './createContributorTagWidget';
8
8
  import { AnchorTypeKey, buildContributorTagDecorationSpec, buildDiffDecorationSpec, buildAnchorDecorationKey, scrollMarginTopValue } from './decorationKeys';
@@ -14,6 +14,22 @@ import { findSafeInsertPos } from './utils/findSafeInsertPos';
14
14
  import { safeResolve } from './utils/safeResolve';
15
15
  import { getTableDiffMode } from './utils/tableDiffMode';
16
16
  import { wrapBlockNodeView, createRemovedLozenge, injectInnerWrapper, createContentWrapper } from './utils/wrapBlockNodeView';
17
+
18
+ /**
19
+ * Where a slice's `index`th child sits in `doc`: the children belong to the ancestor `openStart`
20
+ * levels above the change. `null` if out of range — `posAtIndex` throws past the last child.
21
+ */
22
+ const getSourcePos = (doc, $changeFrom, openStart, index) => {
23
+ const depth = $changeFrom ? $changeFrom.depth - openStart : -1;
24
+ if (!$changeFrom || depth < 0) {
25
+ return null;
26
+ }
27
+ const childIndex = $changeFrom.index(depth) + index;
28
+ if (childIndex >= $changeFrom.node(depth).childCount) {
29
+ return null;
30
+ }
31
+ return safeResolve(doc, $changeFrom.posAtIndex(childIndex, depth));
32
+ };
17
33
  const isHeadingLevel = level => typeof level === 'number' && level >= 1 && level <= 6;
18
34
 
19
35
  /**
@@ -282,7 +298,7 @@ export const createNodeChangedDecorationWidget = ({
282
298
  * and if it's the first or last content, we go in however many the sliced Open
283
299
  * or sliced End depth is and match only the entire node.
284
300
  */
285
- slice.content.forEach(node => {
301
+ slice.content.forEach((node, _offset, index) => {
286
302
  const isFirst = firstReplacedNode === node;
287
303
  const isLast = lastReplacedNode === node;
288
304
  const isOpenAtSliceBoundary = isFirst && slice.openStart > 0 || isLast && slice.openEnd > 0;
@@ -347,7 +363,8 @@ export const createNodeChangedDecorationWidget = ({
347
363
  }
348
364
 
349
365
  // Try to create node view, fallback to serialization
350
- const nodeView = serializer.tryCreateNodeView(node);
366
+ const $sourcePos = fg('platform_editor_ai_show_diff_patch_2') ? getSourcePos(doc, $changeFromA, slice.openStart, index) : null;
367
+ const nodeView = serializer.tryCreateNodeView(node, 0, $sourcePos !== null && $sourcePos !== void 0 ? $sourcePos : undefined);
351
368
  if (nodeView) {
352
369
  if (node.isInline) {
353
370
  const wrapper = createContentWrapper(colorScheme, isActive, isInserted, reveal);
@@ -450,18 +467,25 @@ export const createNodeChangedDecorationWidget = ({
450
467
  });
451
468
  }
452
469
 
453
- // Needed even when the indicator bar is off, because a contributor tag also anchors against the
454
- // widget.
470
+ // Needed even with the indicator bar off, since it anchors its top/bottom to the widget.
455
471
  if (showIndicators || showContributorTags) {
456
472
  dom.style.setProperty('anchor-name', `--${buildAnchorDecorationKey({
457
473
  diffId
458
474
  })}`);
459
- // The tag built below only anchors via `anchor()` if `contributorTagAnchorName` is set; without
460
- // it the tag falls back to a non-anchored position (EDITOR-9045).
461
- if (fg('confluence_ncs_step_diffing_version_history')) {
462
- contributorTagAnchorName ??= buildAnchorDecorationKey({
463
- diffId
475
+ // `dom` is inline, so its box fragments across a line wrap and `anchor()` resolves to the
476
+ // wrong edge. The tag anchors to its own point marker instead (EDITOR-9286).
477
+ if (fg('confluence_ncs_step_diffing_version_history') && !contributorTagAnchorName) {
478
+ const tagPointAnchorName = buildAnchorDecorationKey({
479
+ diffId,
480
+ anchorType: AnchorTypeKey.tag
464
481
  });
482
+ contributorTagAnchorName = tagPointAnchorName;
483
+ decorations.push(Decoration.widget(safeInsertPos, () => createAnchorNameSpan(tagPointAnchorName), {
484
+ key: `contributor-tag-anchor-${diffId}`,
485
+ side: -1,
486
+ marks: [],
487
+ ignoreSelection: true
488
+ }));
465
489
  }
466
490
  }
467
491
 
@@ -495,8 +519,7 @@ export const createNodeChangedDecorationWidget = ({
495
519
  }
496
520
  }));
497
521
  } else {
498
- // Hosted at the start of the deleted run rather than on `dom` itself: `dom` is inline, so once
499
- // the deleted content wraps its box is the union of its line fragments.
522
+ // Anchored to the point marker pushed above, not to `dom` (EDITOR-9286).
500
523
  const tagHost = createContributorTagHost({
501
524
  anchorName: contributorTagAnchorName,
502
525
  diffId,
@@ -311,6 +311,19 @@ const applyTextLikeBlockNodeStyles = ({
311
311
  /**
312
312
  * Creates a content wrapper with deleted styles for a block node
313
313
  */
314
+ /**
315
+ * The element that spans breakout width, for nodes carrying a `breakout` mark. The wrapper divs
316
+ * stay at editor-column width, so anything that should align to the node's visible edges —
317
+ * outlines, absolutely positioned lozenges — has to attach here instead.
318
+ */
319
+ const findBreakoutElement = (targetNode, nodeView) => {
320
+ var _targetNode$marks;
321
+ const hasBreakoutMark = (_targetNode$marks = targetNode.marks) === null || _targetNode$marks === void 0 ? void 0 : _targetNode$marks.some(mark => mark.type.name === 'breakout');
322
+ if (!hasBreakoutMark || !(nodeView instanceof HTMLElement)) {
323
+ return null;
324
+ }
325
+ return nodeView.matches('[data-node-type]') ? nodeView : nodeView.querySelector('[data-node-type]');
326
+ };
314
327
  const createBlockNodeContentWrapper = ({
315
328
  nodeView,
316
329
  targetNode,
@@ -328,7 +341,16 @@ const createBlockNodeContentWrapper = ({
328
341
  // should not also get the inline content style (borderBottom underline) on their container.
329
342
  const shouldSkipContentStyle = isInserted && nodeStyle !== undefined;
330
343
  const contentStyle = shouldSkipContentStyle ? '' : getChangedContentStyle(colorScheme, isActive, isInserted, hideAddedDiffsUnderline);
331
- contentWrapper.setAttribute('style', `${contentStyle}${nodeStyle || ''}`);
344
+
345
+ // A `breakout` node renders wider than the editor column while the wrapper divs
346
+ // stay at column width, so paint the outline on the node element that spans that width.
347
+ const breakoutTarget = nodeStyle ? findBreakoutElement(targetNode, nodeView) : null;
348
+ if (fg('platform_editor_ai_show_diff_patch_2') && breakoutTarget) {
349
+ appendStyleToElement(breakoutTarget, nodeStyle !== null && nodeStyle !== void 0 ? nodeStyle : '');
350
+ contentWrapper.setAttribute('style', contentStyle || '');
351
+ } else {
352
+ contentWrapper.setAttribute('style', `${contentStyle}${nodeStyle || ''}`);
353
+ }
332
354
  contentWrapper.append(nodeView);
333
355
  return contentWrapper;
334
356
  };
@@ -456,7 +478,19 @@ const wrapBlockNode = ({
456
478
  })) {
457
479
  return;
458
480
  }
459
- blockWrapper.append(lozenge);
481
+
482
+ // `lozenge` is absolutely positioned, so it lands on the nearest positioned ancestor. For a
483
+ // breakout node that is the column-width wrapper, leaving the pill short of the node's
484
+ // right edge — host it on the breakout element, which must establish a containing block.
485
+ const breakoutHost = fg('platform_editor_ai_show_diff_patch_2') ? findBreakoutElement(targetNode, nodeView) : null;
486
+ if (breakoutHost) {
487
+ appendStyleToElement(breakoutHost, convertToInlineCss({
488
+ position: 'relative'
489
+ }));
490
+ breakoutHost.append(lozenge);
491
+ } else {
492
+ blockWrapper.append(lozenge);
493
+ }
460
494
  }
461
495
  const contentWrapper = createBlockNodeContentWrapper({
462
496
  nodeView,
@@ -0,0 +1,61 @@
1
+ import { DOMSerializer } from '@atlaskit/editor-prosemirror/model';
2
+ import { fg } from '@atlaskit/platform-feature-flags/fg';
3
+
4
+ /**
5
+ * `@atlaskit/editor-prosemirror/view` doesn't re-export prosemirror-view's `MarkViewConstructor`,
6
+ * so it's typed locally, as `EditorSSRRenderer` does.
7
+ */
8
+
9
+ /** Where to look up a mark's renderer, and the view to render it against. */
10
+
11
+ /**
12
+ * Renders a mark as ProseMirror does: `view.nodeViews` also holds mark views, keyed by mark name,
13
+ * so `breakout` resolves whether it's registered under `markViews` or `nodeViews`. Falls back to
14
+ * `toDOM` — they can differ: `breakout` renders `.fabric-editor-breakout-mark-dom`, which the width
15
+ * CSS targets.
16
+ */
17
+ const renderMarkView = (mark, inline, {
18
+ nodeViews,
19
+ view
20
+ }) => {
21
+ try {
22
+ const markView = nodeViews === null || nodeViews === void 0 ? void 0 : nodeViews[mark.type.name];
23
+ if (markView && view) {
24
+ const rendered = markView(mark, view, inline);
25
+ return {
26
+ dom: rendered.dom,
27
+ contentDOM: rendered.contentDOM
28
+ };
29
+ }
30
+ const toDOM = mark.type.spec.toDOM;
31
+ if (!toDOM) {
32
+ return null;
33
+ }
34
+ return DOMSerializer.renderSpec(document, toDOM(mark, inline));
35
+ } catch {
36
+ return null;
37
+ }
38
+ };
39
+
40
+ /**
41
+ * Wraps rendered DOM in its marks, as ProseMirror does for the live document. Without this a
42
+ * block node loses `breakout`, whose wrapper supplies the width. Marks nest
43
+ * outside-in, so `marks[0]` is outermost.
44
+ */
45
+ export const wrapInMarkViews = (targetNode, nodeDom, sources) => {
46
+ if (targetNode.marks.length === 0 || !fg('platform_editor_ai_show_diff_patch_2')) {
47
+ return nodeDom;
48
+ }
49
+ let wrapped = nodeDom;
50
+ for (let index = targetNode.marks.length - 1; index >= 0; index--) {
51
+ const mark = targetNode.marks[index];
52
+ const rendered = renderMarkView(mark, targetNode.isInline, sources);
53
+ // No content hole means nowhere to nest the node, so skip the mark, not the node.
54
+ if (!(rendered !== null && rendered !== void 0 && rendered.contentDOM)) {
55
+ continue;
56
+ }
57
+ rendered.contentDOM.appendChild(wrapped);
58
+ wrapped = rendered.dom;
59
+ }
60
+ return wrapped;
61
+ };
@@ -18,10 +18,10 @@ const SHOW_DIFF_AGENT_BRANDS = {
18
18
 
19
19
  /**
20
20
  * Brands that prefer the account's real avatar over their fixed glyph when the invoking profile
21
- * has one. Claude/ChatGPT/Rovo intentionally keep their fixed glyph always (see `AGENT_KIND_ICONS`
22
- * in `contributorAvatarRenderer.ts`).
21
+ * has one. Claude/ChatGPT intentionally keep their fixed glyph always (see `AGENT_KIND_ICONS` in
22
+ * `contributorAvatarRenderer.ts`).
23
23
  */
24
- const AVATAR_ELIGIBLE_BRANDS = new Set(['figma', 'lovable', 'replit']);
24
+ const AVATAR_ELIGIBLE_BRANDS = new Set(['figma', 'lovable', 'replit', 'rovo']);
25
25
 
26
26
  /** Known external agent types emitted by Confluence's agent identification service that carry
27
27
  * no `@atlaskit/agent-color` brand — a generic labelled fallback. */
@@ -5,25 +5,24 @@ import { convertToInlineCss } from '@atlaskit/editor-common/lazy-node-view';
5
5
  import { getContributorTagIcon } from './contributorTagIcons';
6
6
 
7
7
  /**
8
- * Agent kinds with a custom contributor-tag icon. Figma/Lovable/Replit have no entry here — they
9
- * always render the real avatar `resolveDiffContributors` supplies, falling back to the generic
10
- * `aiAgent` glyph (see `showContributorIcon`) on load failure. The declared type stays the wider
8
+ * Agent kinds with a custom contributor-tag icon, used as a fallback glyph when no `avatarUrl` is
9
+ * available or its image fails to load (see `showContributorIcon`). Rovo/Figma/Lovable/Replit have
10
+ * no entry here — they fall back to the generic `aiAgent` glyph. The declared type stays the wider
11
11
  * `TagContributor['agentKind']` so indexing below (with `'identified' | 'external'` in play too)
12
12
  * stays safely partial.
13
13
  */
14
14
  const AGENT_KIND_ICONS = {
15
15
  claude: 'claude',
16
- chatgpt: 'chatgpt',
17
- rovo: 'rovoHex'
16
+ chatgpt: 'chatgpt'
18
17
  };
19
18
 
20
19
  /**
21
20
  * Brands that always render their fixed glyph, even when the contributor also carries an
22
- * `avatarUrl`. Figma/Lovable/Replit are excluded here and use the real avatar when
21
+ * `avatarUrl`. Rovo/Figma/Lovable/Replit are excluded here and use the real avatar when
23
22
  * `resolveDiffContributors` supplies one, falling back to the generic `aiAgent` glyph on load
24
23
  * failure.
25
24
  */
26
- const FIXED_GLYPH_AGENT_KINDS = new Set(['claude', 'chatgpt', 'rovo']);
25
+ const FIXED_GLYPH_AGENT_KINDS = new Set(['claude', 'chatgpt']);
27
26
 
28
27
  /** The size `@atlaskit/avatar`'s `xxsmall` rendered at. */
29
28
  const AVATAR_SIZE = AVATAR_SIZES.xxsmall;
@@ -121,8 +120,8 @@ export const contributorAvatarRenderer = ({
121
120
  };
122
121
  if (contributor.avatarUrl) {
123
122
  var _contributor$agentKin2;
124
- // Claude/ChatGPT/Rovo always use their fixed glyph, even when the profile also has an
125
- // avatar URL. Other agents (e.g. Figma/Lovable/Replit) and users prefer the real avatar.
123
+ // Claude/ChatGPT always use their fixed glyph, even when the profile also has an avatar
124
+ // URL. Other agents (e.g. Rovo/Figma/Lovable/Replit) and users prefer the real avatar.
126
125
  const usesFixedGlyph = isAgent && FIXED_GLYPH_AGENT_KINDS.has((_contributor$agentKin2 = contributor.agentKind) !== null && _contributor$agentKin2 !== void 0 ? _contributor$agentKin2 : 'external');
127
126
  if (usesFixedGlyph) {
128
127
  showContributorIcon();
@@ -4,9 +4,6 @@
4
4
  * Neither `@atlaskit/icon`, `@atlaskit/icon-lab` nor `@atlaskit/logo` exposes a raw-SVG entry
5
5
  * point, so the markup is copied here rather than kept as a React island for these glyphs. Keep in
6
6
  * sync with the sources named on each constant.
7
- *
8
- * The Rovo hex is the `brand` appearance, whose `--rovo-*-color` variables resolve to `initial` in
9
- * both themes — the fallbacks below are the rendered colours, so no theme observer is needed.
10
7
  */
11
8
 
12
9
  /** `@atlaskit/icon/core/person`, at the 16px the avatar renders it. */
@@ -22,9 +19,6 @@ const AI_BOT_ICON_SVG = `<svg width="12" height="12" fill="none" viewBox="0 0 16
22
19
  */
23
20
  const AI_AGENT_ICON_SVG = `<svg width="12" height="12" fill="none" viewBox="0 0 16 16" role="presentation" style="display:block"><path fill="currentcolor" fill-rule="evenodd" d="M6.971.493a2 2 0 0 1 2.058 0l5 3A2 2 0 0 1 15 5.208v5.584a2 2 0 0 1-.971 1.715l-5 3a2 2 0 0 1-2.058 0l-5-3A2 2 0 0 1 1 10.792V5.208a2 2 0 0 1 .971-1.715zm1.286 1.286a.5.5 0 0 0-.514 0l-5 3a.5.5 0 0 0-.243.429v5.584a.5.5 0 0 0 .243.429l1.098.66a4.25 4.25 0 1 1 8.318 0l1.098-.66a.5.5 0 0 0 .243-.429V5.208a.5.5 0 0 0-.243-.429zM8 8.25a2.75 2.75 0 1 0 0 5.5 2.75 2.75 0 0 0 0-5.5" clip-rule="evenodd"/></svg>`;
24
21
 
25
- /** `RovoHexIcon` from `@atlaskit/logo/rovo-hex/icon`, at the 16px `xxsmall` renders. */
26
- const ROVO_HEX_ICON_SVG = `<svg width="16" height="16" viewBox="0 0 24 24" role="presentation" style="display:block"><path fill="#1868db" fill-rule="evenodd" d="m13.227 16.789-7.154 4.169-3.403-1.96a2.72 2.72 0 0 1-1.362-2.355V7.366c0-.973.518-1.87 1.361-2.354l6.044-3.487-.038.114a3.6 3.6 0 0 0-.172 1.088v9.278c0 1.274.68 2.45 1.786 3.085z" clip-rule="evenodd"/><path fill="#6a9a23" fill-rule="evenodd" d="m11.296 15.671-7.193 4.153 6.607 3.812a2.73 2.73 0 0 0 2.676.026l.048-.033a2.73 2.73 0 0 0 1.232-1.525 2.7 2.7 0 0 0 .127-.822v-3.594z" clip-rule="evenodd"/><path fill="#af59e1" fill-rule="evenodd" d="m21.477 5.003-4.404-2.539-5.904 4.89 2.69 1.556a3.56 3.56 0 0 1 1.785 3.086v9.277a3.6 3.6 0 0 1-.21 1.202l6.044-3.486a2.71 2.71 0 0 0 1.362-2.355V7.357c0-.97-.521-1.87-1.363-2.354" clip-rule="evenodd"/><path fill="#fca700" fill-rule="evenodd" d="M12.74 8.266 9.353 6.312V2.718c0-.283.044-.56.127-.821A2.73 2.73 0 0 1 10.71.372V.37a.4.4 0 0 0 .048-.033 2.73 2.73 0 0 1 2.676.026l6.499 3.75z" clip-rule="evenodd"/></svg>`;
27
-
28
22
  /** Claude hexagon lockup from the brand asset: the hexagon and knot glyph both baked in. */
29
23
  const CLAUDE_ICON_SVG = `<svg width="16" height="18" viewBox="0 0 16 18" fill="none" role="presentation" style="display:block"><path fill="#D97757" d="M14.5007 3.28903L9.50384 0.402344C8.5736 -0.13506 7.42701 -0.134016 6.49774 0.405082L1.49458 3.30759C0.569434 3.8443 0 4.83297 0 5.90253V11.1804C0 12.2519 0.571497 13.2421 1.49931 13.7781L6.49931 16.6666C7.42787 17.203 8.57213 17.203 9.50069 16.6666L14.5007 13.7781C15.4285 13.2421 16 12.2519 16 11.1804V5.88671C16 4.8152 15.4285 3.82504 14.5007 3.28903Z"/><path fill="#FAF9F6" d="M5.19791 10.055L7.0123 9.03681L7.04267 8.94806L7.0123 8.89907H6.92357L6.62 8.88034L5.58321 8.85232L4.68418 8.815L3.81317 8.76826L3.59367 8.72158L3.38818 8.4507L3.4092 8.31526L3.59367 8.1915L3.85754 8.21485L4.44133 8.25455L5.317 8.31526L5.95216 8.35262L6.89321 8.4507H7.04267L7.06368 8.38999L7.0123 8.35262L6.97261 8.31526L6.06658 7.70112L5.08582 7.05196L4.5721 6.67834L4.29422 6.48919L4.15411 6.31172L4.09339 5.92409L4.34559 5.64621L4.68418 5.66956L4.77058 5.69291L5.11384 5.95678L5.84708 6.52422L6.80448 7.22943L6.94459 7.34618L7.00063 7.30649L7.00763 7.27846L6.94459 7.17338L6.42385 6.23233L5.86809 5.27492L5.62057 4.87795L5.55519 4.63977C5.53183 4.54169 5.51549 4.45996 5.51549 4.35956L5.80271 3.96959L5.9615 3.91821L6.34446 3.96959L6.50558 4.10969L6.74377 4.65378L7.12907 5.51077L7.72686 6.676L7.90199 7.0216L7.99539 7.34152L8.0304 7.43959H8.09113V7.38354L8.14018 6.72737L8.23124 5.92175L8.31999 4.88496L8.35032 4.59307L8.49511 4.24279L8.78232 4.05365L9.00649 4.16107L9.19098 4.42494L9.16533 4.5954L9.05555 5.30762L8.84073 6.42381L8.70062 7.17105H8.78232L8.87574 7.07764L9.25401 6.57559L9.88922 5.78165L10.1694 5.4664L10.4963 5.11847L10.7065 4.95267H11.1035L11.3953 5.38701L11.2646 5.83535L10.8559 6.35375L10.5173 6.79276L10.0316 7.4466L9.72807 7.96966L9.7561 8.0117L9.82849 8.00469L10.926 7.77118L11.5191 7.66376L12.2266 7.54233L12.5466 7.69178L12.5816 7.84356L12.4555 8.15414L11.6989 8.34095L10.8116 8.51842L9.48985 8.83136L9.4735 8.84303L9.49223 8.86637L10.0877 8.92241L10.3422 8.93639H10.9657L12.1262 9.02283L12.4298 9.2236L12.612 9.46881L12.5816 9.6556L12.1146 9.89382L11.4841 9.74434L10.013 9.3941L9.50858 9.26797H9.43849V9.31004L9.85882 9.721L10.6294 10.4169L11.5938 11.3136L11.6429 11.5354L11.5191 11.7106L11.3884 11.6918L10.5407 11.0544L10.2137 10.7672L9.4735 10.1436H9.42451V10.209L9.59495 10.4589L10.4963 11.8133L10.543 12.2289L10.4776 12.3644L10.2441 12.4461L9.98726 12.3994L9.45952 11.6592L8.91544 10.8255L8.47645 10.0783L8.42271 10.1086L8.16352 12.8991L8.04207 13.0416L7.76188 13.149L7.52837 12.9715L7.40461 12.6843L7.52837 12.1168L7.67781 11.3766L7.79925 10.7881L7.90899 10.0573L7.97438 9.81443L7.96971 9.79808L7.916 9.80507L7.36491 10.5617L6.5266 11.6942L5.86342 12.4041L5.70463 12.4671L5.42909 12.3247L5.45477 12.0702L5.60889 11.8436L6.5266 10.6761L7.08002 9.95217L7.4373 9.53421L7.43497 9.47349H7.41395L4.97607 11.0567L4.54174 11.1127L4.35493 10.9376L4.37828 10.6504L4.46702 10.557L5.20024 10.0526L5.19791 10.055Z"/></svg>`;
30
24
 
@@ -35,8 +29,7 @@ const markup = {
35
29
  aiBot: AI_BOT_ICON_SVG,
36
30
  chatgpt: CHATGPT_ICON_SVG,
37
31
  claude: CLAUDE_ICON_SVG,
38
- person: PERSON_ICON_SVG,
39
- rovoHex: ROVO_HEX_ICON_SVG
32
+ person: PERSON_ICON_SVG
40
33
  };
41
34
 
42
35
  /**