@capillarytech/creatives-library 8.0.208 → 8.0.210

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 (76) hide show
  1. package/assets/Android.png +0 -0
  2. package/assets/iOS.png +0 -0
  3. package/package.json +16 -2
  4. package/v2Components/HtmlEditor/HTMLEditor.js +508 -0
  5. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +1809 -0
  6. package/v2Components/HtmlEditor/__tests__/index.lazy.test.js +532 -0
  7. package/v2Components/HtmlEditor/_htmlEditor.scss +304 -0
  8. package/v2Components/HtmlEditor/_index.lazy.scss +26 -0
  9. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +376 -0
  10. package/v2Components/HtmlEditor/components/CodeEditorPane/index.js +331 -0
  11. package/v2Components/HtmlEditor/components/DeviceToggle/__tests__/index.test.js +314 -0
  12. package/v2Components/HtmlEditor/components/DeviceToggle/_deviceToggle.scss +244 -0
  13. package/v2Components/HtmlEditor/components/DeviceToggle/index.js +111 -0
  14. package/v2Components/HtmlEditor/components/EditorToolbar/PreviewModeGroup.js +72 -0
  15. package/v2Components/HtmlEditor/components/EditorToolbar/__tests__/PreviewModeGroup.test.js +1594 -0
  16. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +113 -0
  17. package/v2Components/HtmlEditor/components/EditorToolbar/_previewModeGroup.scss +82 -0
  18. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +115 -0
  19. package/v2Components/HtmlEditor/components/FullscreenModal/_fullscreenModal.scss +57 -0
  20. package/v2Components/HtmlEditor/components/InAppPreviewPane/ContentOverlay.js +90 -0
  21. package/v2Components/HtmlEditor/components/InAppPreviewPane/DeviceFrame.js +60 -0
  22. package/v2Components/HtmlEditor/components/InAppPreviewPane/LayoutSelector.js +58 -0
  23. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/ContentOverlay.test.js +403 -0
  24. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/DeviceFrame.test.js +424 -0
  25. package/v2Components/HtmlEditor/components/InAppPreviewPane/__tests__/LayoutSelector.test.js +248 -0
  26. package/v2Components/HtmlEditor/components/InAppPreviewPane/_inAppPreviewPane.scss +253 -0
  27. package/v2Components/HtmlEditor/components/InAppPreviewPane/constants.js +104 -0
  28. package/v2Components/HtmlEditor/components/InAppPreviewPane/index.js +179 -0
  29. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +220 -0
  30. package/v2Components/HtmlEditor/components/PreviewPane/index.js +229 -0
  31. package/v2Components/HtmlEditor/components/SplitContainer/SplitContainer.js +276 -0
  32. package/v2Components/HtmlEditor/components/SplitContainer/__tests__/SplitContainer.test.js +295 -0
  33. package/v2Components/HtmlEditor/components/SplitContainer/_splitContainer.scss +257 -0
  34. package/v2Components/HtmlEditor/components/SplitContainer/index.js +7 -0
  35. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/__tests__/index.test.js +152 -0
  36. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +31 -0
  37. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +70 -0
  38. package/v2Components/HtmlEditor/components/ValidationPanel/__tests__/index.test.js +98 -0
  39. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +311 -0
  40. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +297 -0
  41. package/v2Components/HtmlEditor/components/ValidationPanel/messages.js +57 -0
  42. package/v2Components/HtmlEditor/components/common/EditorContext.js +84 -0
  43. package/v2Components/HtmlEditor/components/common/__tests__/EditorContext.test.js +660 -0
  44. package/v2Components/HtmlEditor/constants.js +241 -0
  45. package/v2Components/HtmlEditor/hooks/__tests__/useEditorContent.test.js +450 -0
  46. package/v2Components/HtmlEditor/hooks/__tests__/useInAppContent.test.js +785 -0
  47. package/v2Components/HtmlEditor/hooks/__tests__/useLayoutState.test.js +580 -0
  48. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.enhanced.test.js +768 -0
  49. package/v2Components/HtmlEditor/hooks/__tests__/useValidation.test.js +590 -0
  50. package/v2Components/HtmlEditor/hooks/useEditorContent.js +274 -0
  51. package/v2Components/HtmlEditor/hooks/useInAppContent.js +407 -0
  52. package/v2Components/HtmlEditor/hooks/useLayoutState.js +247 -0
  53. package/v2Components/HtmlEditor/hooks/useValidation.js +325 -0
  54. package/v2Components/HtmlEditor/index.js +29 -0
  55. package/v2Components/HtmlEditor/index.lazy.js +114 -0
  56. package/v2Components/HtmlEditor/messages.js +389 -0
  57. package/v2Components/HtmlEditor/utils/__tests__/contentSanitizer.test.js +741 -0
  58. package/v2Components/HtmlEditor/utils/__tests__/htmlValidator.enhanced.test.js +1042 -0
  59. package/v2Components/HtmlEditor/utils/__tests__/liquidTemplateSupport.test.js +515 -0
  60. package/v2Components/HtmlEditor/utils/__tests__/properSyntaxHighlighting.test.js +473 -0
  61. package/v2Components/HtmlEditor/utils/__tests__/simplePerformance.test.js +1109 -0
  62. package/v2Components/HtmlEditor/utils/__tests__/validationAdapter.test.js +240 -0
  63. package/v2Components/HtmlEditor/utils/contentSanitizer.js +433 -0
  64. package/v2Components/HtmlEditor/utils/htmlValidator.js +508 -0
  65. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +524 -0
  66. package/v2Components/HtmlEditor/utils/properSyntaxHighlighting.js +163 -0
  67. package/v2Components/HtmlEditor/utils/simplePerformance.js +145 -0
  68. package/v2Components/HtmlEditor/utils/validationAdapter.js +130 -0
  69. package/v2Containers/EmailWrapper/components/HTMLEditorTesting.js +200 -0
  70. package/v2Containers/EmailWrapper/components/__tests__/HTMLEditorTesting.test.js +545 -0
  71. package/v2Containers/EmailWrapper/index.js +8 -1
  72. package/v2Containers/Rcs/index.js +2 -0
  73. package/v2Containers/Templates/constants.js +8 -0
  74. package/v2Containers/Templates/index.js +56 -28
  75. package/v2Containers/Templates/tests/__snapshots__/index.test.js.snap +5 -14
  76. package/v2Containers/Whatsapp/index.js +1 -0
Binary file
package/assets/iOS.png ADDED
Binary file
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@capillarytech/creatives-library",
3
3
  "author": "meharaj",
4
- "version": "8.0.208",
4
+ "version": "8.0.210",
5
5
  "description": "Capillary creatives ui",
6
6
  "main": "./index.js",
7
7
  "module": "./index.es.js",
@@ -18,16 +18,30 @@
18
18
  "@bugsnag/plugin-react": "7.2.1",
19
19
  "@capillarytech/cap-ui-utils": "3.0.4",
20
20
  "@capillarytech/vulcan-react-sdk": "^2.3.5",
21
- "@newrelic/browser-agent": "^1.293.0",
21
+ "@codemirror/autocomplete": "^6.19.0",
22
+ "@codemirror/lang-css": "^6.3.1",
23
+ "@codemirror/lang-html": "^6.4.11",
24
+ "@codemirror/lang-javascript": "^6.2.4",
25
+ "@codemirror/language": "^6.11.3",
26
+ "@codemirror/language-data": "^6.5.1",
27
+ "@codemirror/lint": "^6.9.0",
28
+ "@codemirror/search": "^6.5.11",
29
+ "@codemirror/state": "^6.5.2",
30
+ "@codemirror/theme-one-dark": "^6.1.3",
31
+ "@codemirror/view": "^6.38.5",
32
+ "@lezer/highlight": "^1.2.1",
22
33
  "@mailupinc/bee-plugin": "^1.2.0",
34
+ "@newrelic/browser-agent": "^1.293.0",
23
35
  "babel-cli": "^6.26.0",
24
36
  "chalk": "^2.4.2",
25
37
  "cheerio": "^1.0.0-rc.3",
26
38
  "connected-react-router": "4.5.0",
39
+ "dompurify": "^3.2.7",
27
40
  "flow": "^0.2.3",
28
41
  "git": "^0.1.5",
29
42
  "history": "4.9.0",
30
43
  "html-to-text": "^8.2.1",
44
+ "htmlhint": "^1.7.1",
31
45
  "jest-date-mock": "^1.0.8",
32
46
  "jest-environment-jsdom": "27.5.1",
33
47
  "jquery": "^3.3.1",
@@ -0,0 +1,508 @@
1
+ /**
2
+ * HTMLEditor - Comprehensive HTML Editor with Split-Screen Layout
3
+ *
4
+ * Features:
5
+ * - Split-screen layout with code editor and live preview
6
+ * - CodeMirror 6 integration for HTML/CSS/JavaScript editing
7
+ * - Real-time preview with sandboxed iframe
8
+ * - Mobile/Desktop responsive preview modes
9
+ * - Comprehensive validation and error handling
10
+ * - Security-first approach with XSS protection
11
+ *
12
+ * Note: Uses injectIntl with forwardRef to provide direct access to CodeEditorPane via ref
13
+ */
14
+
15
+ import React, { useRef, useCallback, useMemo, useState } from 'react';
16
+ import PropTypes from 'prop-types';
17
+ import { Layout } from 'antd'; // Fallback - no Cap UI equivalent
18
+ import { injectIntl, intlShape } from 'react-intl';
19
+
20
+ // Cap UI Components (First Preference)
21
+ import CapRow from '@capillarytech/cap-ui-library/CapRow';
22
+ import CapColumn from '@capillarytech/cap-ui-library/CapColumn';
23
+ import CapSpin from '@capillarytech/cap-ui-library/CapSpin';
24
+ import CapNotification from '@capillarytech/cap-ui-library/CapNotification';
25
+ import CapModal from '@capillarytech/cap-ui-library/CapModal';
26
+
27
+ // Component imports
28
+ import EditorToolbar from './components/EditorToolbar';
29
+ import DeviceToggle from './components/DeviceToggle';
30
+ import SplitContainer from './components/SplitContainer';
31
+ import CodeEditorPane from './components/CodeEditorPane';
32
+ import PreviewPane from './components/PreviewPane';
33
+ import ValidationErrorDisplay from './components/ValidationErrorDisplay';
34
+ import { EditorProvider } from './components/common/EditorContext';
35
+
36
+ // Hooks and utilities
37
+ import { useEditorContent } from './hooks/useEditorContent';
38
+ import { useInAppContent } from './hooks/useInAppContent';
39
+ import { useLayoutState } from './hooks/useLayoutState';
40
+ import { useValidation } from './hooks/useValidation';
41
+
42
+ // Constants
43
+ import { HTML_EDITOR_VARIANTS, DEVICE_TYPES, DEFAULT_HTML_CONTENT } from './constants';
44
+
45
+ // Styles
46
+ import './_htmlEditor.scss';
47
+ import './components/FullscreenModal/_fullscreenModal.scss';
48
+
49
+ // Messages
50
+ import messages from './messages';
51
+
52
+ const HTMLEditor = ({
53
+ intl,
54
+ variant = HTML_EDITOR_VARIANTS.EMAIL, // New prop: 'email' or 'inapp'
55
+ layoutType, // Layout type for InApp variant
56
+ initialContent = DEFAULT_HTML_CONTENT,
57
+ onSave,
58
+ onContentChange,
59
+ className = '',
60
+ readOnly = false,
61
+ showFullscreenButton = true,
62
+ autoSave = true,
63
+ autoSaveInterval = 30000, // 30 seconds
64
+ ...props
65
+ }) => {
66
+ // Separate refs for main and modal editors to avoid conflicts
67
+ const mainEditorRef = useRef(null);
68
+ const modalEditorRef = useRef(null);
69
+ const [isFullscreenModalOpen, setIsFullscreenModalOpen] = useState(false);
70
+
71
+ // Get the currently active editor ref based on fullscreen state
72
+ const getActiveEditorRef = useCallback(() => {
73
+ return isFullscreenModalOpen ? modalEditorRef : mainEditorRef;
74
+ }, [isFullscreenModalOpen]);
75
+
76
+ // Initialize custom hooks for state management - always call both hooks to follow Rules of Hooks
77
+ const isEmailVariant = variant === HTML_EDITOR_VARIANTS.EMAIL;
78
+
79
+ const emailOptions = {
80
+ autoSave: isEmailVariant ? autoSave : false,
81
+ autoSaveInterval,
82
+ onSave: isEmailVariant ? onSave : null,
83
+ onChange: isEmailVariant ? onContentChange : null
84
+ };
85
+
86
+ const emailContent = useEditorContent(
87
+ isEmailVariant ? initialContent : '',
88
+ emailOptions
89
+ );
90
+
91
+ // Prepare content for InApp variant
92
+ const isInAppVariant = variant === HTML_EDITOR_VARIANTS.INAPP;
93
+
94
+ let inAppInitialContent = {};
95
+ if (isInAppVariant) {
96
+ if (typeof initialContent === 'string') {
97
+ // Convert string content to device-specific format
98
+ inAppInitialContent = {
99
+ [DEVICE_TYPES.ANDROID]: initialContent,
100
+ [DEVICE_TYPES.IOS]: initialContent
101
+ };
102
+ } else {
103
+ // Use provided device-specific content
104
+ inAppInitialContent = initialContent;
105
+ }
106
+ }
107
+
108
+ const inAppOptions = {
109
+ autoSave: isInAppVariant ? autoSave : false,
110
+ autoSaveInterval,
111
+ onSave: isInAppVariant ? onSave : null,
112
+ onChange: isInAppVariant ? onContentChange : null
113
+ };
114
+
115
+ const inAppContent = useInAppContent(inAppInitialContent, inAppOptions);
116
+
117
+ // Use appropriate content hook based on variant
118
+ const content = variant === HTML_EDITOR_VARIANTS.EMAIL ? emailContent : inAppContent;
119
+
120
+ // Destructure content properties for cleaner access throughout component
121
+ const {
122
+ activeDevice,
123
+ keepContentSame,
124
+ switchDevice,
125
+ toggleContentSync,
126
+ getDeviceContent,
127
+ markAsSaved
128
+ } = content || {};
129
+
130
+ const layout = useLayoutState({
131
+ splitSizes: [50, 50],
132
+ viewMode: 'desktop',
133
+ mobileWidth: 375,
134
+ isFullscreen: false
135
+ });
136
+
137
+ // Get current content for validation based on variant
138
+ const currentContent = variant === HTML_EDITOR_VARIANTS.EMAIL
139
+ ? content?.content
140
+ : getDeviceContent?.(activeDevice);
141
+
142
+ // Create message formatter for sanitizer
143
+ const formatSanitizerMessage = useCallback((messageKey, values = {}) => {
144
+ // Map sanitizer message keys to our message structure
145
+ const messageMap = {
146
+ 'sanitizer.invalidInput': messages.sanitizer.invalidInput,
147
+ 'sanitizer.invalidInputNonEmpty': messages.sanitizer.invalidInputNonEmpty,
148
+ 'sanitizer.sanitizationFailed': messages.sanitizer.sanitizationFailed,
149
+ 'sanitizer.dangerousProtocolDetected': messages.sanitizer.dangerousProtocolDetected,
150
+ 'sanitizer.emailMultimediaNotSupported': messages.sanitizer.emailMultimediaNotSupported,
151
+ 'sanitizer.emailPositioningNotSupported': messages.sanitizer.emailPositioningNotSupported,
152
+ 'sanitizer.emailModernCssLimited': messages.sanitizer.emailModernCssLimited,
153
+ 'sanitizer.emailViewportUnitsNotSupported': messages.sanitizer.emailViewportUnitsNotSupported,
154
+ 'sanitizer.mobileTablesNotFriendly': messages.sanitizer.mobileTablesNotFriendly,
155
+ 'sanitizer.mobileRelativeFontSizes': messages.sanitizer.mobileRelativeFontSizes,
156
+ 'sanitizer.mobileFixedWidthsProblematic': messages.sanitizer.mobileFixedWidthsProblematic,
157
+ 'sanitizer.mobileRelativeUnits': messages.sanitizer.mobileRelativeUnits,
158
+ 'sanitizer.productionValidHtml': messages.sanitizer.productionValidHtml,
159
+ 'sanitizer.productionSanitized': messages.sanitizer.productionSanitized,
160
+ 'sanitizer.productionInlineCss': messages.sanitizer.productionInlineCss,
161
+ 'sanitizer.productionLargeContent': messages.sanitizer.productionLargeContent
162
+ };
163
+
164
+ const messageObj = messageMap[messageKey];
165
+ return messageObj ? intl.formatMessage(messageObj, values) : messageKey;
166
+ }, [intl]);
167
+
168
+ // Create message formatter for validator
169
+ const formatValidatorMessage = useCallback((messageKey, values = {}) => {
170
+ // Map validator message keys to our message structure
171
+ const messageMap = {
172
+ 'validator.validationFailed': messages.validator.validationFailed,
173
+ 'validator.liquidValidationFailed': messages.validator.liquidValidationFailed,
174
+ 'validator.unsafeProtocolDetected': messages.validator.unsafeProtocolDetected,
175
+ 'validator.scriptTagsDetected': messages.validator.scriptTagsDetected,
176
+ 'validator.outlookIncompatible': messages.validator.outlookIncompatible,
177
+ 'validator.emailCssUnsupported': messages.validator.emailCssUnsupported,
178
+ 'validator.mobileIncompatible': messages.validator.mobileIncompatible,
179
+ 'validator.largeImageDetected': messages.validator.largeImageDetected,
180
+ 'validator.unclosedCssRule': messages.validator.unclosedCssRule,
181
+ 'validator.emptyCssRule': messages.validator.emptyCssRule,
182
+ 'validator.cssValidationFailed': messages.validator.cssValidationFailed
183
+ };
184
+
185
+ const messageObj = messageMap[messageKey];
186
+ return messageObj ? intl.formatMessage(messageObj, values) : messageKey;
187
+ }, [intl]);
188
+
189
+ const validation = useValidation(currentContent, variant, {
190
+ enableRealTime: true,
191
+ debounceMs: 500,
192
+ enableSanitization: true,
193
+ securityLevel: 'standard'
194
+ }, formatSanitizerMessage, formatValidatorMessage);
195
+
196
+ // Handle label insertion at cursor position
197
+ const handleLabelInsert = useCallback((label, position) => {
198
+ // With injectIntl({ forwardRef: true }), ref points directly to CodeEditorPane
199
+ const activeEditorRef = getActiveEditorRef();
200
+ const editor = activeEditorRef.current;
201
+
202
+ if (!editor) {
203
+ CapNotification.warning({
204
+ message: intl.formatMessage(messages.labelInsertError),
205
+ description: intl.formatMessage(messages.editorNotReady),
206
+ duration: 3
207
+ });
208
+ return;
209
+ }
210
+
211
+ // Check if the required methods exist
212
+ if (typeof editor?.insertText !== 'function') {
213
+ CapNotification.error({
214
+ message: intl.formatMessage(messages.labelInsertError),
215
+ description: intl.formatMessage(messages.editorMethodNotAvailable),
216
+ duration: 4
217
+ });
218
+ return;
219
+ }
220
+
221
+ try {
222
+ // Get current cursor position or use provided position
223
+ const cursor = position !== undefined
224
+ ? position
225
+ : (typeof editor?.getCursor === 'function' ? editor.getCursor() : 0);
226
+
227
+ // Insert label at cursor position
228
+ editor.insertText(label, cursor);
229
+
230
+ // Focus the editor if focus method is available
231
+ editor?.focus?.();
232
+
233
+ // Show success notification
234
+ CapNotification.success({
235
+ message: intl.formatMessage(messages.labelInserted),
236
+ description: intl.formatMessage(messages.labelInsertedDescription, { label }),
237
+ duration: 2
238
+ });
239
+ } catch (error) {
240
+ CapNotification.error({
241
+ message: intl.formatMessage(messages.labelInsertError),
242
+ description: error.message,
243
+ duration: 4
244
+ });
245
+ }
246
+ }, [intl, getActiveEditorRef]);
247
+
248
+ // Handle save action
249
+ const handleSave = useCallback(() => {
250
+ try {
251
+ const { html, css, javascript } = content?.content || {};
252
+ const currentContent = { html, css, javascript };
253
+
254
+ if (onSave) {
255
+ onSave(currentContent);
256
+ }
257
+
258
+ markAsSaved?.();
259
+
260
+ CapNotification.success({
261
+ message: intl.formatMessage(messages.contentSaved),
262
+ duration: 2
263
+ });
264
+ } catch (error) {
265
+ CapNotification.error({
266
+ message: intl.formatMessage(messages.saveError),
267
+ description: error.message,
268
+ duration: 4
269
+ });
270
+ }
271
+ }, [content, onSave, intl, markAsSaved]);
272
+
273
+ // Handle validation error click - navigate to error line in editor
274
+ const handleValidationErrorClick = useCallback((error) => {
275
+ const activeEditorRef = getActiveEditorRef();
276
+ const editorInstance = activeEditorRef.current;
277
+ const { line, column = 1 } = error || {};
278
+
279
+ if (editorInstance && line) {
280
+ try {
281
+ // Access the CodeMirror view through the exposed ref methods
282
+ if (editorInstance?.navigateToLine) {
283
+ editorInstance.navigateToLine(line, column);
284
+ } else {
285
+ editorInstance?.focus?.();
286
+ // Fallback: just focus the editor if navigation isn't available
287
+ }
288
+ } catch (err) {
289
+ // Fallback: just focus the editor
290
+ editorInstance?.focus?.();
291
+ }
292
+ }
293
+ }, [getActiveEditorRef]);
294
+
295
+ // Handle fullscreen modal
296
+ const handleOpenFullscreen = useCallback(() => {
297
+ setIsFullscreenModalOpen(true);
298
+ }, []);
299
+
300
+ const handleCloseFullscreen = useCallback(() => {
301
+ setIsFullscreenModalOpen(false);
302
+ }, []);
303
+
304
+ // Context value for providers
305
+ const contextValue = useMemo(() => ({
306
+ variant,
307
+ content,
308
+ layout,
309
+ validation,
310
+ editorRef: getActiveEditorRef(),
311
+ handleLabelInsert,
312
+ handleSave,
313
+ readOnly,
314
+ isFullscreenMode: isFullscreenModalOpen,
315
+ // InApp specific context
316
+ ...(variant === HTML_EDITOR_VARIANTS.INAPP && {
317
+ activeDevice,
318
+ keepContentSame,
319
+ switchDevice,
320
+ toggleContentSync,
321
+ getDeviceContent,
322
+ layoutType
323
+ })
324
+ }), [
325
+ variant,
326
+ content,
327
+ layout,
328
+ validation,
329
+ getActiveEditorRef,
330
+ handleLabelInsert,
331
+ handleSave,
332
+ readOnly,
333
+ isFullscreenModalOpen,
334
+ activeDevice,
335
+ keepContentSame,
336
+ switchDevice,
337
+ toggleContentSync,
338
+ getDeviceContent,
339
+ layoutType
340
+ ]);
341
+
342
+ // Loading state
343
+ if (!content || !layout) {
344
+ return (
345
+ <CapRow className="html-editor-loading">
346
+ <CapSpin size="large" tip={intl.formatMessage(messages.initializing)} />
347
+ </CapRow>
348
+ );
349
+ }
350
+
351
+ return (
352
+ <EditorProvider value={contextValue}>
353
+ <div className={`html-editor html-editor--${variant} ${className}`} {...props}>
354
+ {/* Editor Toolbar - Conditional based on variant */}
355
+ {variant === HTML_EDITOR_VARIANTS.EMAIL ? (
356
+ <EditorToolbar
357
+ showFullscreenButton={showFullscreenButton}
358
+ onLabelInsert={handleLabelInsert}
359
+ onSave={handleSave}
360
+ onToggleFullscreen={handleOpenFullscreen}
361
+ />
362
+ ) : (
363
+ /* InApp variant: Device Toggle + Toolbar combined */
364
+ <CapRow className="html-editor__header">
365
+ <DeviceToggle
366
+ activeDevice={activeDevice}
367
+ onDeviceChange={switchDevice}
368
+ keepContentSame={keepContentSame}
369
+ onKeepContentSameChange={toggleContentSync}
370
+ />
371
+ <EditorToolbar
372
+ showFullscreenButton={showFullscreenButton}
373
+ onLabelInsert={handleLabelInsert}
374
+ onSave={handleSave}
375
+ onToggleFullscreen={handleOpenFullscreen}
376
+ variant={variant}
377
+ showTitle={false} // Hide title in InApp variant
378
+ />
379
+ </CapRow>
380
+ )}
381
+
382
+ {/* Main Content Area - Split Container */}
383
+ <CapRow className="html-editor-content">
384
+ <SplitContainer className="html-editor-split-container">
385
+ {/* Code Editor Pane */}
386
+ <CodeEditorPane
387
+ ref={mainEditorRef}
388
+ readOnly={readOnly}
389
+ onLabelInsert={handleLabelInsert}
390
+ />
391
+
392
+ {/* Preview Pane */}
393
+ <PreviewPane />
394
+ </SplitContainer>
395
+
396
+ {/* Validation Display - Full Width Below Split Container */}
397
+ <ValidationErrorDisplay
398
+ validation={validation}
399
+ onErrorClick={handleValidationErrorClick}
400
+ variant={variant}
401
+ className="html-editor-validation"
402
+ />
403
+ </CapRow>
404
+
405
+ {/* Fullscreen Modal */}
406
+ <CapModal
407
+ title={null} // Remove modal header to avoid duplication
408
+ visible={isFullscreenModalOpen}
409
+ onCancel={handleCloseFullscreen}
410
+ footer={null}
411
+ maskClosable={false}
412
+ centered
413
+ closable={false}
414
+ width={"90vw"}
415
+ className="html-editor-fullscreen-modal"
416
+ >
417
+ <CapRow className="html-editor-fullscreen">
418
+ {/* Editor Toolbar - Conditional based on variant */}
419
+ {variant === HTML_EDITOR_VARIANTS.EMAIL ? (
420
+ <EditorToolbar
421
+ showFullscreenButton={true} // Show fullscreen button in modal to allow closing
422
+ onLabelInsert={handleLabelInsert}
423
+ onSave={handleSave}
424
+ isFullscreenMode={true}
425
+ onToggleFullscreen={handleCloseFullscreen} // Close modal when clicked in fullscreen mode
426
+ />
427
+ ) : (
428
+ /* InApp variant: Device Toggle + Toolbar combined in fullscreen */
429
+ <CapRow className="html-editor__header html-editor__header--fullscreen">
430
+ <DeviceToggle
431
+ activeDevice={activeDevice}
432
+ onDeviceChange={switchDevice}
433
+ keepContentSame={keepContentSame}
434
+ onKeepContentSameChange={toggleContentSync}
435
+ />
436
+ <EditorToolbar
437
+ showFullscreenButton={true} // Show fullscreen button in modal to allow closing
438
+ onLabelInsert={handleLabelInsert}
439
+ onSave={handleSave}
440
+ isFullscreenMode={true}
441
+ onToggleFullscreen={handleCloseFullscreen} // Close modal when clicked in fullscreen mode
442
+ variant={variant}
443
+ showTitle={false} // Hide title in InApp variant
444
+ />
445
+ </CapRow>
446
+ )}
447
+
448
+ {/* Main Content Area - Split Container */}
449
+ <CapRow className="html-editor-content-fullscreen">
450
+ <SplitContainer className="html-editor-split-container-fullscreen">
451
+ {/* Code Editor Pane */}
452
+ <CodeEditorPane
453
+ ref={modalEditorRef}
454
+ readOnly={readOnly}
455
+ isFullscreenMode={true}
456
+ onLabelInsert={handleLabelInsert}
457
+ />
458
+
459
+ {/* Preview Pane */}
460
+ <PreviewPane isFullscreenMode={true} isModalContext={true} />
461
+ </SplitContainer>
462
+
463
+ {/* Validation Display in Modal */}
464
+ <ValidationErrorDisplay
465
+ validation={validation}
466
+ onErrorClick={handleValidationErrorClick}
467
+ variant={variant}
468
+ className="html-editor-validation"
469
+ />
470
+ </CapRow>
471
+ </CapRow>
472
+ </CapModal>
473
+ </div>
474
+ </EditorProvider>
475
+ );
476
+ };
477
+
478
+ HTMLEditor.propTypes = {
479
+ intl: intlShape.isRequired,
480
+ variant: PropTypes.oneOf(Object.values(HTML_EDITOR_VARIANTS)), // Added variant prop
481
+ layoutType: PropTypes.string, // Layout type for InApp variant
482
+ initialContent: PropTypes.oneOfType([
483
+ PropTypes.string,
484
+ PropTypes.objectOf(PropTypes.string) // Per-device content for INAPP variant
485
+ ]),
486
+ onSave: PropTypes.func,
487
+ onContentChange: PropTypes.func,
488
+ className: PropTypes.string,
489
+ readOnly: PropTypes.bool,
490
+ showFullscreenButton: PropTypes.bool,
491
+ autoSave: PropTypes.bool,
492
+ autoSaveInterval: PropTypes.number
493
+ };
494
+
495
+ HTMLEditor.defaultProps = {
496
+ variant: HTML_EDITOR_VARIANTS.EMAIL, // Default to email variant
497
+ initialContent: null, // Will use default from useEditorContent hook
498
+ onSave: null,
499
+ onContentChange: null,
500
+ className: '',
501
+ readOnly: false,
502
+ showFullscreenButton: true,
503
+ autoSave: true,
504
+ autoSaveInterval: 30000
505
+ };
506
+
507
+ // Export with forwardRef to allow direct access to CodeEditorPane via ref
508
+ export default injectIntl(HTMLEditor, { forwardRef: true });