@atlaskit/renderer 132.7.0 → 133.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +48 -0
  2. package/dist/cjs/analytics/enums.js +1 -0
  3. package/dist/cjs/react/index.js +3 -0
  4. package/dist/cjs/react/nodes/codeBlock/codeBlock.js +5 -1
  5. package/dist/cjs/react/nodes/codeBlock/components/codeBlockButtonContainer.js +7 -1
  6. package/dist/cjs/react/nodes/codeBlock/components/codeBlockContainer.js +4 -0
  7. package/dist/cjs/react/nodes/codeBlock/components/codeBlockDownloadButton.js +133 -0
  8. package/dist/cjs/react/nodes/codeBlock/windowedCodeBlock.js +4 -1
  9. package/dist/cjs/react/nodes/layoutColumn-compiled.js +3 -1
  10. package/dist/cjs/react/nodes/layoutColumn-emotion.js +3 -1
  11. package/dist/cjs/react/utils/use-select-all-trap.js +4 -4
  12. package/dist/cjs/steps/index.js +4 -4
  13. package/dist/cjs/ui/Renderer/ErrorBoundary.js +4 -4
  14. package/dist/cjs/ui/Renderer/index.js +4 -1
  15. package/dist/cjs/ui/annotations/element/mark.js +5 -3
  16. package/dist/es2019/analytics/enums.js +1 -0
  17. package/dist/es2019/react/index.js +3 -0
  18. package/dist/es2019/react/nodes/codeBlock/codeBlock.js +4 -1
  19. package/dist/es2019/react/nodes/codeBlock/components/codeBlockButtonContainer.js +7 -1
  20. package/dist/es2019/react/nodes/codeBlock/components/codeBlockContainer.js +4 -0
  21. package/dist/es2019/react/nodes/codeBlock/components/codeBlockDownloadButton.js +123 -0
  22. package/dist/es2019/react/nodes/codeBlock/windowedCodeBlock.js +4 -1
  23. package/dist/es2019/react/nodes/layoutColumn-compiled.js +3 -1
  24. package/dist/es2019/react/nodes/layoutColumn-emotion.js +3 -1
  25. package/dist/es2019/react/utils/use-select-all-trap.js +5 -4
  26. package/dist/es2019/steps/index.js +5 -4
  27. package/dist/es2019/ui/Renderer/ErrorBoundary.js +5 -4
  28. package/dist/es2019/ui/Renderer/index.js +4 -1
  29. package/dist/es2019/ui/annotations/element/mark.js +5 -3
  30. package/dist/esm/analytics/enums.js +1 -0
  31. package/dist/esm/react/index.js +3 -0
  32. package/dist/esm/react/nodes/codeBlock/codeBlock.js +5 -1
  33. package/dist/esm/react/nodes/codeBlock/components/codeBlockButtonContainer.js +7 -1
  34. package/dist/esm/react/nodes/codeBlock/components/codeBlockContainer.js +4 -0
  35. package/dist/esm/react/nodes/codeBlock/components/codeBlockDownloadButton.js +125 -0
  36. package/dist/esm/react/nodes/codeBlock/windowedCodeBlock.js +4 -1
  37. package/dist/esm/react/nodes/layoutColumn-compiled.js +3 -1
  38. package/dist/esm/react/nodes/layoutColumn-emotion.js +3 -1
  39. package/dist/esm/react/utils/use-select-all-trap.js +5 -4
  40. package/dist/esm/steps/index.js +5 -4
  41. package/dist/esm/ui/Renderer/ErrorBoundary.js +5 -4
  42. package/dist/esm/ui/Renderer/index.js +4 -1
  43. package/dist/esm/ui/annotations/element/mark.js +5 -3
  44. package/dist/types/analytics/enums.d.ts +1 -0
  45. package/dist/types/react/index.d.ts +2 -0
  46. package/dist/types/react/nodes/codeBlock/codeBlock.d.ts +1 -0
  47. package/dist/types/react/nodes/codeBlock/components/codeBlockButtonContainer.d.ts +3 -1
  48. package/dist/types/react/nodes/codeBlock/components/codeBlockContainer.d.ts +1 -1
  49. package/dist/types/react/nodes/codeBlock/components/codeBlockDownloadButton.d.ts +14 -0
  50. package/dist/types/react/nodes/codeBlock/windowedCodeBlock.d.ts +1 -1
  51. package/dist/types/react/types.d.ts +1 -0
  52. package/dist/types/ui/Renderer/index.d.ts +1 -0
  53. package/dist/types/ui/renderer-props.d.ts +1 -0
  54. package/package.json +56 -56
@@ -0,0 +1,123 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ */
5
+
6
+ import { injectIntl } from 'react-intl';
7
+ import DownloadIcon from '@atlaskit/icon/core/download';
8
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
9
+ import { jsx } from '@emotion/react';
10
+ import { IconButton } from '@atlaskit/button/new';
11
+ import Tooltip from '@atlaskit/tooltip';
12
+ import { codeBlockButtonMessages } from '@atlaskit/editor-common/messages';
13
+ import AnalyticsContext from '../../../../analytics/analyticsContext';
14
+ import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '../../../../analytics/enums';
15
+ /**
16
+ * NOTE: The language-to-extension mapping logic below is intentionally duplicated in
17
+ * platform/packages/ai-mate/conversation-assistant-utils/src/downloadCodeBlock.ts.
18
+ * This is because @atlaskit/renderer cannot import from @atlassian/conversation-assistant-utils
19
+ * (cross-package dependency not allowed). If you update the logic here, update it there too.
20
+ *
21
+ * Maps ADF code block language names to file extensions.
22
+ * Inlined here to avoid cross-package dependencies on ai-mate packages.
23
+ */
24
+ const languageToExtension = {
25
+ bash: 'sh',
26
+ c: 'c',
27
+ cpp: 'cpp',
28
+ css: 'css',
29
+ csv: 'csv',
30
+ go: 'go',
31
+ htm: 'html',
32
+ html: 'html',
33
+ java: 'java',
34
+ javascript: 'js',
35
+ js: 'js',
36
+ json: 'json',
37
+ jsx: 'jsx',
38
+ less: 'less',
39
+ markdown: 'md',
40
+ md: 'md',
41
+ python: 'py',
42
+ ruby: 'rb',
43
+ rust: 'rs',
44
+ scss: 'scss',
45
+ sh: 'sh',
46
+ shell: 'sh',
47
+ sql: 'sql',
48
+ ts: 'ts',
49
+ tsx: 'tsx',
50
+ typescript: 'ts',
51
+ xml: 'xml',
52
+ yaml: 'yaml',
53
+ yml: 'yaml'
54
+ };
55
+ const getFileExtension = language => {
56
+ var _languageToExtension$;
57
+ if (!language) {
58
+ return 'txt';
59
+ }
60
+ return (_languageToExtension$ = languageToExtension[language.toLowerCase()]) !== null && _languageToExtension$ !== void 0 ? _languageToExtension$ : 'txt';
61
+ };
62
+ const triggerDownload = (content, language) => {
63
+ // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
64
+ if (typeof document === 'undefined') {
65
+ return;
66
+ }
67
+ const extension = getFileExtension(language);
68
+ const filename = `rovo-snippet.${extension}`;
69
+ // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
70
+ const doc = document;
71
+ const blob = new Blob([content], {
72
+ type: 'text/plain'
73
+ });
74
+ const url = URL.createObjectURL(blob);
75
+ const anchor = doc.createElement('a');
76
+ anchor.href = url;
77
+ anchor.download = filename;
78
+ anchor.style.display = 'none';
79
+ doc.body.appendChild(anchor);
80
+ anchor.dispatchEvent(new MouseEvent('click', {
81
+ bubbles: false,
82
+ cancelable: true,
83
+ view: window
84
+ }));
85
+ doc.body.removeChild(anchor);
86
+ // Defer revocation to avoid race condition in Safari/Firefox
87
+ setTimeout(() => URL.revokeObjectURL(url), 0);
88
+ };
89
+ const DownloadButton = ({
90
+ content,
91
+ language,
92
+ intl
93
+ }) => {
94
+ const tooltip = intl.formatMessage(codeBlockButtonMessages.downloadCodeBlock);
95
+ return jsx(AnalyticsContext.Consumer, null, ({
96
+ fireAnalyticsEvent
97
+ }) => jsx("span", null, jsx(Tooltip, {
98
+ content: tooltip,
99
+ hideTooltipOnClick: false,
100
+ position: "top"
101
+ }, jsx("div", null, jsx(IconButton, {
102
+ appearance: "subtle",
103
+ label: tooltip,
104
+ icon: DownloadIcon
105
+ // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
106
+ ,
107
+ onClick: event => {
108
+ fireAnalyticsEvent({
109
+ // @ts-expect-error - Type 'ACTION.CLICKED' is not assignable to type 'ACTION.CLICKED | ACTION.MEDIA_LINK_TRANSFORMED | ACTION.STARTED | ACTION.TOGGLE_EXPAND | ACTION.UNSUPPORTED_CONTENT_ENCOUNTERED | ACTION.VISITED | ACTION.RENDERED | ACTION.INVALID_PROSEMIRROR_DOCUMENT | ACTION.CRASHED | ... 6 more ... | AnnotationActionType'.
110
+ // This error was introduced after upgrading to TypeScript 5
111
+ action: ACTION.CLICKED,
112
+ actionSubject: ACTION_SUBJECT.BUTTON,
113
+ actionSubjectId: ACTION_SUBJECT_ID.CODEBLOCK_DOWNLOAD,
114
+ eventType: EVENT_TYPE.UI
115
+ });
116
+ triggerDownload(content, language);
117
+ event.stopPropagation();
118
+ },
119
+ spacing: "compact"
120
+ })))));
121
+ };
122
+ const _default_1 = injectIntl(DownloadButton);
123
+ export default _default_1;
@@ -22,6 +22,7 @@ const WindowedCodeBlock = ({
22
22
  text,
23
23
  language,
24
24
  allowCopyToClipboard,
25
+ allowDownloadCodeBlock,
25
26
  allowWrapCodeBlock = false,
26
27
  codeBidiWarningTooltipEnabled,
27
28
  hideLineNumbers = false,
@@ -52,7 +53,9 @@ const WindowedCodeBlock = ({
52
53
  fallback: memoizedLightWeightCodeBlock
53
54
  }, jsx(CodeBlockContainer, {
54
55
  allowCopyToClipboard: allowCopyToClipboard,
55
- allowWrapCodeBlock: allowWrapCodeBlock
56
+ allowWrapCodeBlock: allowWrapCodeBlock,
57
+ allowDownloadCodeBlock: allowDownloadCodeBlock,
58
+ language: language
56
59
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
57
60
  ,
58
61
  className: className,
@@ -22,6 +22,8 @@ const multipleWrappedImagesStyle = null;
22
22
  const clearNextSiblingBlockMarkMarginTopStyle = null;
23
23
  export const LayoutSectionCompiled = props => {
24
24
  const isLayoutColumnMenuEnabled = expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true);
25
+ // Pure rollout gate: no A/B exposure analysis is planned for this rendering switch.
26
+ const isLayoutColumnValignRenderingEnabled = expValEqualsNoExposure('platform_editor_layout_column_valign_rendering', 'isEnabled', true);
25
27
  return /*#__PURE__*/React.createElement("div", {
26
28
  "data-layout-column": true,
27
29
  "data-column-width": props.width,
@@ -31,7 +33,7 @@ export const LayoutSectionCompiled = props => {
31
33
  style: {
32
34
  flexBasis: `${props.width}%`
33
35
  },
34
- className: ax([isLayoutColumnMenuEnabled && props.valign === 'middle' && "_1e0c1txw _2lx21bp4 _1bah1h6o", isLayoutColumnMenuEnabled && props.valign === 'bottom' && "_1e0c1txw _2lx21bp4 _1bahesu3", fg('platform_editor_fix_media_in_renderer') && "_1tihidpf _12kpidpf"])
36
+ className: ax([(isLayoutColumnValignRenderingEnabled || isLayoutColumnMenuEnabled) && props.valign === 'middle' && "_1e0c1txw _2lx21bp4 _1bah1h6o", (isLayoutColumnValignRenderingEnabled || isLayoutColumnMenuEnabled) && props.valign === 'bottom' && "_1e0c1txw _2lx21bp4 _1bahesu3", fg('platform_editor_fix_media_in_renderer') && "_1tihidpf _12kpidpf"])
35
37
  }, /*#__PURE__*/React.createElement(WidthProvider, null, /*#__PURE__*/React.createElement("div", {
36
38
  className: ax(["_1skbgrf3", "_19segrf3 _1ki1grf3 _bmdegrf3 _166hgrf3 _7g1ogrf3 _sk2jgrf3 _hgeogrf3"])
37
39
  }), props.children));
@@ -60,6 +60,8 @@ const clearNextSiblingBlockMarkMarginTopStyle = css({
60
60
  });
61
61
  export const LayoutSectionEmotion = props => {
62
62
  const isLayoutColumnMenuEnabled = expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true);
63
+ // Pure rollout gate: no A/B exposure analysis is planned for this rendering switch.
64
+ const isLayoutColumnValignRenderingEnabled = expValEqualsNoExposure('platform_editor_layout_column_valign_rendering', 'isEnabled', true);
63
65
  return jsx("div", {
64
66
  "data-layout-column": true,
65
67
  "data-column-width": props.width,
@@ -71,7 +73,7 @@ export const LayoutSectionEmotion = props => {
71
73
  },
72
74
  css: [
73
75
  // Keep separate: Compiled crashes on ternary/object lookup here.
74
- isLayoutColumnMenuEnabled && props.valign === 'middle' && verticalAlignMiddleStyles, isLayoutColumnMenuEnabled && props.valign === 'bottom' && verticalAlignBottomStyles, fg('platform_editor_fix_media_in_renderer') && multipleWrappedImagesStyle]
76
+ (isLayoutColumnValignRenderingEnabled || isLayoutColumnMenuEnabled) && props.valign === 'middle' && verticalAlignMiddleStyles, (isLayoutColumnValignRenderingEnabled || isLayoutColumnMenuEnabled) && props.valign === 'bottom' && verticalAlignBottomStyles, fg('platform_editor_fix_media_in_renderer') && multipleWrappedImagesStyle]
75
77
  }, jsx(WidthProvider, null, jsx("div", {
76
78
  css: [clearNextSiblingMarginTopStyle, clearNextSiblingBlockMarkMarginTopStyle]
77
79
  }), props.children));
@@ -2,6 +2,10 @@ import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/anal
2
2
  import React from 'react';
3
3
  import AnalyticsContext from '../../analytics/analyticsContext';
4
4
  import { ElementSelection } from './element-selection';
5
+
6
+ // Ignored via go/ees005
7
+ // eslint-disable-next-line require-unicode-regexp
8
+ const MAC_PLATFORM_REGEX = /Mac/;
5
9
  export const useSelectAllTrap = () => {
6
10
  const {
7
11
  fireAnalyticsEvent
@@ -9,10 +13,7 @@ export const useSelectAllTrap = () => {
9
13
  const ref = React.useRef(null);
10
14
  const clicked = React.useRef(false);
11
15
  const caught = React.useRef();
12
-
13
- // Ignored via go/ees005
14
- // eslint-disable-next-line require-unicode-regexp
15
- const mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false;
16
+ const mac = typeof navigator !== 'undefined' ? MAC_PLATFORM_REGEX.test(navigator.platform) : false;
16
17
  const onKeyDown = React.useCallback(e => {
17
18
  var _e$target, _e$target$matches;
18
19
  const el = ref.current;
@@ -1,6 +1,10 @@
1
1
  /* eslint-disable jsdoc/require-jsdoc -- internal step helpers */
2
2
  import { fg } from '@atlaskit/platform-feature-flags';
3
3
  import { AddMarkStep } from '@atlaskit/editor-prosemirror/transform';
4
+
5
+ // Ignored via go/ees005
6
+ // eslint-disable-next-line require-unicode-regexp
7
+ const MEDIA_OR_MEDIA_SINGLE_REGEX = /media|mediaSingle/;
4
8
  export function getStartPos(element) {
5
9
  return parseInt(element.dataset.rendererStartPos || '-1', 10);
6
10
  }
@@ -224,10 +228,7 @@ export function getPosFromRange(range) {
224
228
  const possibleMediaOrMediaSingleElement = findParent(startContainer);
225
229
 
226
230
  // Video hover targets return media single, not media, thus, the extra check in condition.
227
- const isMediaOrMediaSingle = possibleMediaOrMediaSingleElement &&
228
- // Ignored via go/ees005
229
- // eslint-disable-next-line require-unicode-regexp
230
- /media|mediaSingle/.test(getNodeType(possibleMediaOrMediaSingleElement) || '');
231
+ const isMediaOrMediaSingle = possibleMediaOrMediaSingleElement && MEDIA_OR_MEDIA_SINGLE_REGEX.test(getNodeType(possibleMediaOrMediaSingleElement) || '');
231
232
  if (isMediaOrMediaSingle) {
232
233
  let pos;
233
234
  const mediaSingleElement = getNodeType(possibleMediaOrMediaSingleElement) === 'mediaSingle' ? possibleMediaOrMediaSingleElement : findMediaParent(possibleMediaOrMediaSingleElement);
@@ -7,6 +7,10 @@ import { PLATFORM } from '../../analytics/events';
7
7
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
8
8
  // eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
9
9
  import uuid from 'uuid';
10
+
11
+ // Ignored via go/ees005
12
+ // eslint-disable-next-line require-unicode-regexp
13
+ const FAILED_TO_EXECUTE_REGEX = /Failed to execute.*on 'Node'.*/;
10
14
  // Ignored via go/ees005
11
15
  // eslint-disable-next-line @repo/internal/react/no-class-components
12
16
  export class ErrorBoundary extends React.Component {
@@ -51,10 +55,7 @@ export class ErrorBoundary extends React.Component {
51
55
  logException(error, {
52
56
  location: 'renderer'
53
57
  });
54
- // Ignored via go/ees005
55
- // eslint-disable-next-line require-unicode-regexp
56
- const pattern = /Failed to execute.*on 'Node'.*/;
57
- const matchesPattern = pattern.test(error.message);
58
+ const matchesPattern = FAILED_TO_EXECUTE_REGEX.test(error.message);
58
59
  if (matchesPattern) {
59
60
  this.fireAnalyticsEvent({
60
61
  action: ACTION.CAUGHT_DOM_ERROR,
@@ -58,7 +58,7 @@ export const DEGRADED_SEVERITY_THRESHOLD = 3000;
58
58
  const TABLE_INFO_TIMEOUT = 10000;
59
59
  const RENDER_EVENT_SAMPLE_RATE = 0.2;
60
60
  const packageName = "@atlaskit/renderer";
61
- const packageVersion = "132.7.0";
61
+ const packageVersion = "133.0.0";
62
62
  const setAsQueryContainerStyles = css({
63
63
  containerName: 'ak-renderer-wrapper',
64
64
  containerType: 'inline-size'
@@ -255,6 +255,7 @@ export const RendererFunctionalComponent = props => {
255
255
  extensionViewportSizes: props.extensionViewportSizes,
256
256
  getExtensionHeight: props.getExtensionHeight,
257
257
  allowCopyToClipboard: props.allowCopyToClipboard,
258
+ allowDownloadCodeBlock: props.allowDownloadCodeBlock,
258
259
  allowWrapCodeBlock: props.allowWrapCodeBlock,
259
260
  allowCustomPanels: props.allowCustomPanels,
260
261
  allowAnnotations: props.allowAnnotations,
@@ -472,6 +473,7 @@ export const RendererFunctionalComponent = props => {
472
473
  allowNestedHeaderLinks: isNestedHeaderLinksEnabled(props.allowHeadingAnchorLinks),
473
474
  allowColumnSorting: props.allowColumnSorting,
474
475
  allowCopyToClipboard: props.allowCopyToClipboard,
476
+ allowDownloadCodeBlock: props.allowDownloadCodeBlock,
475
477
  allowWrapCodeBlock: props.allowWrapCodeBlock,
476
478
  allowCustomPanels: props.allowCustomPanels,
477
479
  allowPlaceholderText: props.allowPlaceholderText,
@@ -509,6 +511,7 @@ export const RendererFunctionalComponent = props => {
509
511
  appearance: props.appearance,
510
512
  contentMode: props.contentMode || 'standard',
511
513
  allowCopyToClipboard: props.allowCopyToClipboard,
514
+ allowDownloadCodeBlock: props.allowDownloadCodeBlock,
512
515
  allowWrapCodeBlock: props.allowWrapCodeBlock,
513
516
  allowPlaceholderText: props.allowPlaceholderText,
514
517
  allowColumnSorting: props.allowColumnSorting,
@@ -94,10 +94,12 @@ const markStylesWithCommentsPanel = css({
94
94
  }
95
95
  }
96
96
  });
97
+
98
+ // Ignored via go/ees005
99
+ // eslint-disable-next-line require-unicode-regexp
100
+ const MOBILE_USER_AGENT_REGEX = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i;
97
101
  const isMobile = () => {
98
- // Ignored via go/ees005
99
- // eslint-disable-next-line require-unicode-regexp
100
- return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
102
+ return MOBILE_USER_AGENT_REGEX.test(navigator.userAgent);
101
103
  };
102
104
  const accessibilityStylesNew = css({
103
105
  '&::before, &::after': {
@@ -51,6 +51,7 @@ export var ACTION_SUBJECT_ID = /*#__PURE__*/function (ACTION_SUBJECT_ID) {
51
51
  ACTION_SUBJECT_ID["HOVER_LABEL"] = "hoverLabel";
52
52
  ACTION_SUBJECT_ID["INLINE_COMMENT"] = "inlineComment";
53
53
  ACTION_SUBJECT_ID["CODEBLOCK_COPY"] = "codeBlockCopy";
54
+ ACTION_SUBJECT_ID["CODEBLOCK_DOWNLOAD"] = "codeBlockDownload";
54
55
  ACTION_SUBJECT_ID["CODEBLOCK_WRAP"] = "codeBlockWrap";
55
56
  return ACTION_SUBJECT_ID;
56
57
  }({});
@@ -52,6 +52,7 @@ var ReactSerializer = /*#__PURE__*/function () {
52
52
  */
53
53
  _defineProperty(this, "expandHeadingIds", []);
54
54
  _defineProperty(this, "allowCopyToClipboard", false);
55
+ _defineProperty(this, "allowDownloadCodeBlock", false);
55
56
  _defineProperty(this, "allowWrapCodeBlock", false);
56
57
  _defineProperty(this, "allowPlaceholderText", true);
57
58
  _defineProperty(this, "allowCustomPanels", false);
@@ -190,6 +191,7 @@ var ReactSerializer = /*#__PURE__*/function () {
190
191
  this.disableActions = init.disableActions;
191
192
  this.allowHeadingAnchorLinks = init.allowHeadingAnchorLinks;
192
193
  this.allowCopyToClipboard = init.allowCopyToClipboard;
194
+ this.allowDownloadCodeBlock = init.allowDownloadCodeBlock;
193
195
  this.allowWrapCodeBlock = init.allowWrapCodeBlock;
194
196
  this.allowPlaceholderText = init.allowPlaceholderText;
195
197
  this.allowCustomPanels = init.allowCustomPanels;
@@ -656,6 +658,7 @@ var ReactSerializer = /*#__PURE__*/function () {
656
658
  content: node.content ? node.content.toJSON() : undefined,
657
659
  allowHeadingAnchorLinks: this.allowHeadingAnchorLinks,
658
660
  allowCopyToClipboard: this.allowCopyToClipboard,
661
+ allowDownloadCodeBlock: this.allowDownloadCodeBlock,
659
662
  allowWrapCodeBlock: this.allowWrapCodeBlock,
660
663
  allowPlaceholderText: this.allowPlaceholderText,
661
664
  rendererAppearance: this.appearance,
@@ -19,6 +19,8 @@ function CodeBlock(props) {
19
19
  allowCopyToClipboard = _props$allowCopyToCli === void 0 ? false : _props$allowCopyToCli,
20
20
  _props$allowWrapCodeB = props.allowWrapCodeBlock,
21
21
  allowWrapCodeBlock = _props$allowWrapCodeB === void 0 ? false : _props$allowWrapCodeB,
22
+ _props$allowDownloadC = props.allowDownloadCodeBlock,
23
+ allowDownloadCodeBlock = _props$allowDownloadC === void 0 ? false : _props$allowDownloadC,
22
24
  codeBidiWarningTooltipEnabled = props.codeBidiWarningTooltipEnabled,
23
25
  _props$hideLineNumber = props.hideLineNumbers,
24
26
  hideLineNumbers = _props$hideLineNumber === void 0 ? false : _props$hideLineNumber,
@@ -34,7 +36,9 @@ function CodeBlock(props) {
34
36
  setWrapLongLines = _useState2[1];
35
37
  return jsx(CodeBlockContainer, {
36
38
  allowCopyToClipboard: allowCopyToClipboard,
37
- allowWrapCodeBlock: allowWrapCodeBlock
39
+ allowWrapCodeBlock: allowWrapCodeBlock,
40
+ allowDownloadCodeBlock: allowDownloadCodeBlock,
41
+ language: language
38
42
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
39
43
  ,
40
44
  className: className,
@@ -5,6 +5,7 @@
5
5
  /* eslint-disable @typescript-eslint/consistent-type-imports, @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766; jsx required at runtime for @jsxRuntime classic */
6
6
  import { jsx, css } from '@emotion/react';
7
7
  import CopyButton from './codeBlockCopyButton';
8
+ import DownloadButton from './codeBlockDownloadButton';
8
9
  import CodeWrapButton from './codeBlockWrapButton';
9
10
  var codeBlockButtonsWrapper = css({
10
11
  position: 'sticky',
@@ -47,7 +48,9 @@ var codeBlockButtonsStyle = css({
47
48
  });
48
49
  var CodeBlockButtonContainer = function CodeBlockButtonContainer(_ref) {
49
50
  var allowCopyToClipboard = _ref.allowCopyToClipboard,
51
+ allowDownloadCodeBlock = _ref.allowDownloadCodeBlock,
50
52
  allowWrapCodeBlock = _ref.allowWrapCodeBlock,
53
+ language = _ref.language,
51
54
  setWrapLongLines = _ref.setWrapLongLines,
52
55
  text = _ref.text,
53
56
  wrapLongLines = _ref.wrapLongLines;
@@ -55,7 +58,10 @@ var CodeBlockButtonContainer = function CodeBlockButtonContainer(_ref) {
55
58
  css: codeBlockButtonsWrapper
56
59
  }, jsx("div", {
57
60
  css: codeBlockButtonsStyle
58
- }, allowWrapCodeBlock && jsx(CodeWrapButton, {
61
+ }, allowDownloadCodeBlock && jsx(DownloadButton, {
62
+ content: text,
63
+ language: language !== null && language !== void 0 ? language : null
64
+ }), allowWrapCodeBlock && jsx(CodeWrapButton, {
59
65
  setWrapLongLines: setWrapLongLines,
60
66
  wrapLongLines: wrapLongLines
61
67
  }), allowCopyToClipboard && jsx(CopyButton, {
@@ -40,9 +40,11 @@ var denseModeOverrides = css(_defineProperty({}, "".concat(CodeBlockSharedCssCla
40
40
  }));
41
41
  var CodeBlockContainer = function CodeBlockContainer(_ref) {
42
42
  var allowCopyToClipboard = _ref.allowCopyToClipboard,
43
+ allowDownloadCodeBlock = _ref.allowDownloadCodeBlock,
43
44
  allowWrapCodeBlock = _ref.allowWrapCodeBlock,
44
45
  children = _ref.children,
45
46
  className = _ref.className,
47
+ language = _ref.language,
46
48
  localId = _ref.localId,
47
49
  setWrapLongLines = _ref.setWrapLongLines,
48
50
  text = _ref.text,
@@ -54,7 +56,9 @@ var CodeBlockContainer = function CodeBlockContainer(_ref) {
54
56
  css: [codeBlockStyleOverrides, (expValEquals('confluence_compact_text_format', 'isEnabled', true) || expValEquals('cc_editor_ai_content_mode', 'variant', 'test') && fg('platform_editor_content_mode_button_mvp')) && denseModeOverrides]
55
57
  }, jsx(CodeBlockButtonContainer, {
56
58
  allowCopyToClipboard: allowCopyToClipboard,
59
+ allowDownloadCodeBlock: allowDownloadCodeBlock,
57
60
  allowWrapCodeBlock: allowWrapCodeBlock,
61
+ language: language,
58
62
  setWrapLongLines: setWrapLongLines,
59
63
  text: text,
60
64
  wrapLongLines: wrapLongLines
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @jsxRuntime classic
3
+ * @jsx jsx
4
+ */
5
+
6
+ import { injectIntl } from 'react-intl';
7
+ import DownloadIcon from '@atlaskit/icon/core/download';
8
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
9
+ import { jsx } from '@emotion/react';
10
+ import { IconButton } from '@atlaskit/button/new';
11
+ import Tooltip from '@atlaskit/tooltip';
12
+ import { codeBlockButtonMessages } from '@atlaskit/editor-common/messages';
13
+ import AnalyticsContext from '../../../../analytics/analyticsContext';
14
+ import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '../../../../analytics/enums';
15
+ /**
16
+ * NOTE: The language-to-extension mapping logic below is intentionally duplicated in
17
+ * platform/packages/ai-mate/conversation-assistant-utils/src/downloadCodeBlock.ts.
18
+ * This is because @atlaskit/renderer cannot import from @atlassian/conversation-assistant-utils
19
+ * (cross-package dependency not allowed). If you update the logic here, update it there too.
20
+ *
21
+ * Maps ADF code block language names to file extensions.
22
+ * Inlined here to avoid cross-package dependencies on ai-mate packages.
23
+ */
24
+ var languageToExtension = {
25
+ bash: 'sh',
26
+ c: 'c',
27
+ cpp: 'cpp',
28
+ css: 'css',
29
+ csv: 'csv',
30
+ go: 'go',
31
+ htm: 'html',
32
+ html: 'html',
33
+ java: 'java',
34
+ javascript: 'js',
35
+ js: 'js',
36
+ json: 'json',
37
+ jsx: 'jsx',
38
+ less: 'less',
39
+ markdown: 'md',
40
+ md: 'md',
41
+ python: 'py',
42
+ ruby: 'rb',
43
+ rust: 'rs',
44
+ scss: 'scss',
45
+ sh: 'sh',
46
+ shell: 'sh',
47
+ sql: 'sql',
48
+ ts: 'ts',
49
+ tsx: 'tsx',
50
+ typescript: 'ts',
51
+ xml: 'xml',
52
+ yaml: 'yaml',
53
+ yml: 'yaml'
54
+ };
55
+ var getFileExtension = function getFileExtension(language) {
56
+ var _languageToExtension$;
57
+ if (!language) {
58
+ return 'txt';
59
+ }
60
+ return (_languageToExtension$ = languageToExtension[language.toLowerCase()]) !== null && _languageToExtension$ !== void 0 ? _languageToExtension$ : 'txt';
61
+ };
62
+ var triggerDownload = function triggerDownload(content, language) {
63
+ // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
64
+ if (typeof document === 'undefined') {
65
+ return;
66
+ }
67
+ var extension = getFileExtension(language);
68
+ var filename = "rovo-snippet.".concat(extension);
69
+ // eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
70
+ var doc = document;
71
+ var blob = new Blob([content], {
72
+ type: 'text/plain'
73
+ });
74
+ var url = URL.createObjectURL(blob);
75
+ var anchor = doc.createElement('a');
76
+ anchor.href = url;
77
+ anchor.download = filename;
78
+ anchor.style.display = 'none';
79
+ doc.body.appendChild(anchor);
80
+ anchor.dispatchEvent(new MouseEvent('click', {
81
+ bubbles: false,
82
+ cancelable: true,
83
+ view: window
84
+ }));
85
+ doc.body.removeChild(anchor);
86
+ // Defer revocation to avoid race condition in Safari/Firefox
87
+ setTimeout(function () {
88
+ return URL.revokeObjectURL(url);
89
+ }, 0);
90
+ };
91
+ var DownloadButton = function DownloadButton(_ref) {
92
+ var content = _ref.content,
93
+ language = _ref.language,
94
+ intl = _ref.intl;
95
+ var tooltip = intl.formatMessage(codeBlockButtonMessages.downloadCodeBlock);
96
+ return jsx(AnalyticsContext.Consumer, null, function (_ref2) {
97
+ var fireAnalyticsEvent = _ref2.fireAnalyticsEvent;
98
+ return jsx("span", null, jsx(Tooltip, {
99
+ content: tooltip,
100
+ hideTooltipOnClick: false,
101
+ position: "top"
102
+ }, jsx("div", null, jsx(IconButton, {
103
+ appearance: "subtle",
104
+ label: tooltip,
105
+ icon: DownloadIcon
106
+ // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
107
+ ,
108
+ onClick: function onClick(event) {
109
+ fireAnalyticsEvent({
110
+ // @ts-expect-error - Type 'ACTION.CLICKED' is not assignable to type 'ACTION.CLICKED | ACTION.MEDIA_LINK_TRANSFORMED | ACTION.STARTED | ACTION.TOGGLE_EXPAND | ACTION.UNSUPPORTED_CONTENT_ENCOUNTERED | ACTION.VISITED | ACTION.RENDERED | ACTION.INVALID_PROSEMIRROR_DOCUMENT | ACTION.CRASHED | ... 6 more ... | AnnotationActionType'.
111
+ // This error was introduced after upgrading to TypeScript 5
112
+ action: ACTION.CLICKED,
113
+ actionSubject: ACTION_SUBJECT.BUTTON,
114
+ actionSubjectId: ACTION_SUBJECT_ID.CODEBLOCK_DOWNLOAD,
115
+ eventType: EVENT_TYPE.UI
116
+ });
117
+ triggerDownload(content, language);
118
+ event.stopPropagation();
119
+ },
120
+ spacing: "compact"
121
+ }))));
122
+ });
123
+ };
124
+ var _default_1 = injectIntl(DownloadButton);
125
+ export default _default_1;
@@ -41,6 +41,7 @@ var WindowedCodeBlock = function WindowedCodeBlock(_ref2) {
41
41
  var text = _ref2.text,
42
42
  language = _ref2.language,
43
43
  allowCopyToClipboard = _ref2.allowCopyToClipboard,
44
+ allowDownloadCodeBlock = _ref2.allowDownloadCodeBlock,
44
45
  _ref2$allowWrapCodeBl = _ref2.allowWrapCodeBlock,
45
46
  allowWrapCodeBlock = _ref2$allowWrapCodeBl === void 0 ? false : _ref2$allowWrapCodeBl,
46
47
  codeBidiWarningTooltipEnabled = _ref2.codeBidiWarningTooltipEnabled,
@@ -75,7 +76,9 @@ var WindowedCodeBlock = function WindowedCodeBlock(_ref2) {
75
76
  fallback: memoizedLightWeightCodeBlock
76
77
  }, jsx(CodeBlockContainer, {
77
78
  allowCopyToClipboard: allowCopyToClipboard,
78
- allowWrapCodeBlock: allowWrapCodeBlock
79
+ allowWrapCodeBlock: allowWrapCodeBlock,
80
+ allowDownloadCodeBlock: allowDownloadCodeBlock,
81
+ language: language
79
82
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
80
83
  ,
81
84
  className: className,
@@ -22,6 +22,8 @@ var multipleWrappedImagesStyle = null;
22
22
  var clearNextSiblingBlockMarkMarginTopStyle = null;
23
23
  export var LayoutSectionCompiled = function LayoutSectionCompiled(props) {
24
24
  var isLayoutColumnMenuEnabled = expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true);
25
+ // Pure rollout gate: no A/B exposure analysis is planned for this rendering switch.
26
+ var isLayoutColumnValignRenderingEnabled = expValEqualsNoExposure('platform_editor_layout_column_valign_rendering', 'isEnabled', true);
25
27
  return /*#__PURE__*/React.createElement("div", {
26
28
  "data-layout-column": true,
27
29
  "data-column-width": props.width,
@@ -31,7 +33,7 @@ export var LayoutSectionCompiled = function LayoutSectionCompiled(props) {
31
33
  style: {
32
34
  flexBasis: "".concat(props.width, "%")
33
35
  },
34
- className: ax([isLayoutColumnMenuEnabled && props.valign === 'middle' && "_1e0c1txw _2lx21bp4 _1bah1h6o", isLayoutColumnMenuEnabled && props.valign === 'bottom' && "_1e0c1txw _2lx21bp4 _1bahesu3", fg('platform_editor_fix_media_in_renderer') && "_1tihidpf _12kpidpf"])
36
+ className: ax([(isLayoutColumnValignRenderingEnabled || isLayoutColumnMenuEnabled) && props.valign === 'middle' && "_1e0c1txw _2lx21bp4 _1bah1h6o", (isLayoutColumnValignRenderingEnabled || isLayoutColumnMenuEnabled) && props.valign === 'bottom' && "_1e0c1txw _2lx21bp4 _1bahesu3", fg('platform_editor_fix_media_in_renderer') && "_1tihidpf _12kpidpf"])
35
37
  }, /*#__PURE__*/React.createElement(WidthProvider, null, /*#__PURE__*/React.createElement("div", {
36
38
  className: ax(["_1skbgrf3", "_19segrf3 _1ki1grf3 _bmdegrf3 _166hgrf3 _7g1ogrf3 _sk2jgrf3 _hgeogrf3"])
37
39
  }), props.children));
@@ -51,6 +51,8 @@ var clearNextSiblingBlockMarkMarginTopStyle = css(_defineProperty({}, "+ .fabric
51
51
  }));
52
52
  export var LayoutSectionEmotion = function LayoutSectionEmotion(props) {
53
53
  var isLayoutColumnMenuEnabled = expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true);
54
+ // Pure rollout gate: no A/B exposure analysis is planned for this rendering switch.
55
+ var isLayoutColumnValignRenderingEnabled = expValEqualsNoExposure('platform_editor_layout_column_valign_rendering', 'isEnabled', true);
54
56
  return jsx("div", {
55
57
  "data-layout-column": true,
56
58
  "data-column-width": props.width,
@@ -62,7 +64,7 @@ export var LayoutSectionEmotion = function LayoutSectionEmotion(props) {
62
64
  },
63
65
  css: [
64
66
  // Keep separate: Compiled crashes on ternary/object lookup here.
65
- isLayoutColumnMenuEnabled && props.valign === 'middle' && verticalAlignMiddleStyles, isLayoutColumnMenuEnabled && props.valign === 'bottom' && verticalAlignBottomStyles, fg('platform_editor_fix_media_in_renderer') && multipleWrappedImagesStyle]
67
+ (isLayoutColumnValignRenderingEnabled || isLayoutColumnMenuEnabled) && props.valign === 'middle' && verticalAlignMiddleStyles, (isLayoutColumnValignRenderingEnabled || isLayoutColumnMenuEnabled) && props.valign === 'bottom' && verticalAlignBottomStyles, fg('platform_editor_fix_media_in_renderer') && multipleWrappedImagesStyle]
66
68
  }, jsx(WidthProvider, null, jsx("div", {
67
69
  css: [clearNextSiblingMarginTopStyle, clearNextSiblingBlockMarkMarginTopStyle]
68
70
  }), props.children));
@@ -2,16 +2,17 @@ import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/anal
2
2
  import React from 'react';
3
3
  import AnalyticsContext from '../../analytics/analyticsContext';
4
4
  import { ElementSelection } from './element-selection';
5
+
6
+ // Ignored via go/ees005
7
+ // eslint-disable-next-line require-unicode-regexp
8
+ var MAC_PLATFORM_REGEX = /Mac/;
5
9
  export var useSelectAllTrap = function useSelectAllTrap() {
6
10
  var _React$useContext = React.useContext(AnalyticsContext),
7
11
  fireAnalyticsEvent = _React$useContext.fireAnalyticsEvent;
8
12
  var ref = React.useRef(null);
9
13
  var clicked = React.useRef(false);
10
14
  var caught = React.useRef();
11
-
12
- // Ignored via go/ees005
13
- // eslint-disable-next-line require-unicode-regexp
14
- var mac = typeof navigator !== 'undefined' ? /Mac/.test(navigator.platform) : false;
15
+ var mac = typeof navigator !== 'undefined' ? MAC_PLATFORM_REGEX.test(navigator.platform) : false;
15
16
  var onKeyDown = React.useCallback(function (e) {
16
17
  var _e$target, _e$target$matches;
17
18
  var el = ref.current;
@@ -1,6 +1,10 @@
1
1
  /* eslint-disable jsdoc/require-jsdoc -- internal step helpers */
2
2
  import { fg } from '@atlaskit/platform-feature-flags';
3
3
  import { AddMarkStep } from '@atlaskit/editor-prosemirror/transform';
4
+
5
+ // Ignored via go/ees005
6
+ // eslint-disable-next-line require-unicode-regexp
7
+ var MEDIA_OR_MEDIA_SINGLE_REGEX = /media|mediaSingle/;
4
8
  export function getStartPos(element) {
5
9
  return parseInt(element.dataset.rendererStartPos || '-1', 10);
6
10
  }
@@ -219,10 +223,7 @@ export function getPosFromRange(range) {
219
223
  var possibleMediaOrMediaSingleElement = findParent(startContainer);
220
224
 
221
225
  // Video hover targets return media single, not media, thus, the extra check in condition.
222
- var isMediaOrMediaSingle = possibleMediaOrMediaSingleElement &&
223
- // Ignored via go/ees005
224
- // eslint-disable-next-line require-unicode-regexp
225
- /media|mediaSingle/.test(getNodeType(possibleMediaOrMediaSingleElement) || '');
226
+ var isMediaOrMediaSingle = possibleMediaOrMediaSingleElement && MEDIA_OR_MEDIA_SINGLE_REGEX.test(getNodeType(possibleMediaOrMediaSingleElement) || '');
226
227
  if (isMediaOrMediaSingle) {
227
228
  var pos;
228
229
  var mediaSingleElement = getNodeType(possibleMediaOrMediaSingleElement) === 'mediaSingle' ? possibleMediaOrMediaSingleElement : findMediaParent(possibleMediaOrMediaSingleElement);