@capillarytech/creatives-library 8.0.262-alpha.1 → 8.0.263

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 (29) hide show
  1. package/package.json +1 -1
  2. package/v2Components/ErrorInfoNote/index.js +58 -113
  3. package/v2Components/ErrorInfoNote/messages.js +8 -12
  4. package/v2Components/HtmlEditor/HTMLEditor.js +48 -182
  5. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +11 -15
  6. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +7 -8
  7. package/v2Components/HtmlEditor/_htmlEditor.scss +6 -6
  8. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +1 -1
  9. package/v2Components/HtmlEditor/components/EditorToolbar/_editorToolbar.scss +0 -1
  10. package/v2Components/HtmlEditor/components/EditorToolbar/index.js +27 -2
  11. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +4 -4
  12. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +17 -0
  13. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +15 -28
  14. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +10 -33
  15. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +48 -13
  16. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +77 -146
  17. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +14 -14
  18. package/v2Components/HtmlEditor/constants.js +3 -0
  19. package/v2Components/HtmlEditor/hooks/useValidation.js +34 -13
  20. package/v2Components/HtmlEditor/messages.js +10 -0
  21. package/v2Components/HtmlEditor/utils/liquidTemplateSupport.js +38 -36
  22. package/v2Components/HtmlEditor/utils/validationConstants.js +3 -4
  23. package/v2Components/MobilePushPreviewV2/constants.js +6 -0
  24. package/v2Components/MobilePushPreviewV2/index.js +4 -3
  25. package/v2Containers/CreativesContainer/index.js +1 -3
  26. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +6 -9
  27. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +5 -49
  28. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +23 -38
  29. package/v2Containers/TemplatesV2/TemplatesV2.style.js +4 -2
@@ -1,13 +1,12 @@
1
1
  /**
2
2
  * ValidationTabs Component
3
3
  *
4
- * Displays validation errors in a tabbed interface with 3 categories:
5
- * - HTML issues: General HTML/CSS validation errors and warnings
6
- * - Label issues: Tag syntax errors (open/close tags, attributes, brackets)
7
- * - Liquid issues: Liquid expression errors (shown when liquid content is detected, even if liquid feature is disabled)
4
+ * Displays validation issues in a tabbed interface with 2 categories:
5
+ * - Errors: Blocking issues (API errors, Rule Group #1, client-side Liquid errors)
6
+ * - Warnings: Non-blocking issues
8
7
  */
9
8
 
10
- import React, { useState, useMemo } from 'react';
9
+ import React, { useState, useMemo, useEffect } from 'react';
11
10
  import PropTypes from 'prop-types';
12
11
  import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
13
12
 
@@ -18,49 +17,29 @@ import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
18
17
 
19
18
  // Messages
20
19
  import messages from './messages';
21
- import { BLOCKING_ERROR_RULE_IDS } from '../../constants';
22
- import { ISSUE_SOURCES, LABEL_ISSUE_PATTERNS } from '../../utils/validationConstants';
20
+ import { BLOCKING_ERROR_RULE_IDS, VALIDATION_SEVERITY } from '../../constants';
23
21
 
24
22
  // Styles
25
23
  import './_validationTabs.scss';
26
- import {StyledCapTab} from '../../../../v2Containers/MobilePushNew/style';
24
+ import { StyledCapTab } from '../../../../v2Containers/MobilePushNew/style';
25
+ import { ISSUE_SOURCES } from '../../utils/validationConstants';
27
26
 
28
27
  /**
29
- * Categorize issues into HTML, Label, and Liquid categories
28
+ * Group issues into Errors (blocking) and Warnings (non-blocking)
30
29
  */
31
- const categorizeIssues = (allIssues) => {
32
- const htmlIssues = [];
33
- const labelIssues = [];
34
- const liquidIssues = [];
30
+ const groupByErrorsAndWarnings = (allIssues) => {
31
+ const errors = [];
32
+ const warnings = [];
35
33
 
36
34
  allIssues.forEach((issue) => {
37
- const { source, rule, message } = issue;
38
- const messageLower = (message || '').toLowerCase();
39
- const ruleLower = (rule || '').toLowerCase();
40
-
41
- // Check if it's a Liquid issue - ONLY by source, not by message content
42
- // This prevents false positives where HTML errors mention liquid syntax
43
- if (source === ISSUE_SOURCES.LIQUID) {
44
- liquidIssues.push(issue);
45
- return;
46
- }
47
-
48
- // Check if it's a Label (tag syntax) issue
49
- const isLabelIssue = LABEL_ISSUE_PATTERNS.some(
50
- (pattern) => messageLower.includes(pattern.toLowerCase())
51
- || ruleLower.includes(pattern.toLowerCase()),
52
- );
53
-
54
- if (isLabelIssue) {
55
- labelIssues.push(issue);
56
- return;
35
+ if (isBlockingError(issue)) {
36
+ errors.push(issue);
37
+ } else {
38
+ warnings.push(issue);
57
39
  }
58
-
59
- // Default to HTML issues
60
- htmlIssues.push(issue);
61
40
  });
62
41
 
63
- return { htmlIssues, labelIssues, liquidIssues };
42
+ return { errors, warnings };
64
43
  };
65
44
 
66
45
  /**
@@ -73,7 +52,7 @@ const isBlockingError = (issue) => {
73
52
  return true;
74
53
  }
75
54
  // Client-side Liquid validation errors are blocking (genuine syntax errors)
76
- if (source === 'liquid-validator' && severity === 'error') {
55
+ if (source === ISSUE_SOURCES.LIQUID && severity === VALIDATION_SEVERITY.ERROR) {
77
56
  return true;
78
57
  }
79
58
  // Rule Group #1 errors are blocking
@@ -83,24 +62,13 @@ const isBlockingError = (issue) => {
83
62
  return false;
84
63
  };
85
64
 
86
- /**
87
- * Get icon based on whether issue is blocking error or warning
88
- * Blocking errors use error-icon, warnings use warning
89
- */
90
- const getSeverityIcon = (issue) => {
91
- if (isBlockingError(issue)) {
92
- return <CapIcon type="error-icon" className="validation-tabs__icon validation-tabs__icon--error" />;
93
- }
94
- // All other issues (warnings, non-blocking) use warning icon
95
- return <CapIcon type="warning" className="validation-tabs__icon validation-tabs__icon--warning" />;
96
- };
97
-
98
65
  /**
99
66
  * ValidationTabContent - Renders the content for each tab
100
67
  */
101
68
  const ValidationTabContent = ({
102
69
  issues,
103
70
  onErrorClick,
71
+ onExpand,
104
72
  }) => {
105
73
  if (!issues || issues.length === 0) {
106
74
  return null;
@@ -108,6 +76,10 @@ const ValidationTabContent = ({
108
76
 
109
77
  const handleNavigateClick = (issue, e) => {
110
78
  e.stopPropagation();
79
+ // Expand panel when redirection is clicked so it does not stay stuck to footer
80
+ if (onExpand) {
81
+ onExpand();
82
+ }
111
83
  if (onErrorClick) {
112
84
  // Always call onErrorClick to acknowledge the error (enables buttons)
113
85
  // If line number exists, navigate to it; otherwise just acknowledge and focus editor
@@ -135,9 +107,6 @@ const ValidationTabContent = ({
135
107
  key={key}
136
108
  className={`validation-tabs__item validation-tabs__item--${displaySeverity}`}
137
109
  >
138
- <div className="validation-tabs__item-icon">
139
- {getSeverityIcon(issue)}
140
- </div>
141
110
  <div className="validation-tabs__item-content">
142
111
  <span className="validation-tabs__item-message">
143
112
  {message}
@@ -173,11 +142,13 @@ const ValidationTabContent = ({
173
142
  ValidationTabContent.propTypes = {
174
143
  issues: PropTypes.array,
175
144
  onErrorClick: PropTypes.func,
145
+ onExpand: PropTypes.func,
176
146
  };
177
147
 
178
148
  ValidationTabContent.defaultProps = {
179
149
  issues: [],
180
150
  onErrorClick: null,
151
+ onExpand: null,
181
152
  };
182
153
 
183
154
  /**
@@ -187,137 +158,95 @@ const ValidationTabs = ({
187
158
  intl,
188
159
  validation,
189
160
  onErrorClick,
190
- onClose,
161
+ isCollapsed = false,
162
+ onToggleCollapse,
163
+ onExpand,
191
164
  className,
192
165
  }) => {
193
- const [activeKey, setActiveKey] = useState(null);
166
+ const [activeKey, setActiveKey] = useState('errors');
194
167
 
195
- // Categorize issues
196
- const { htmlIssues, labelIssues, liquidIssues } = useMemo(() => {
168
+ // Group issues into Errors (blocking) and Warnings (non-blocking)
169
+ const { errors, warnings } = useMemo(() => {
197
170
  if (!validation) {
198
- return { htmlIssues: [], labelIssues: [], liquidIssues: [] };
171
+ return { errors: [], warnings: [] };
199
172
  }
200
-
201
- // Get all issues from validation
202
173
  const allIssues = validation.getAllIssues ? validation.getAllIssues() : [];
203
- const categorized = categorizeIssues(allIssues);
204
- return categorized;
174
+ return groupByErrorsAndWarnings(allIssues);
205
175
  }, [validation]);
206
176
 
207
- // Calculate counts
208
- const htmlCount = htmlIssues.length;
209
- const labelCount = labelIssues.length;
210
- const liquidCount = liquidIssues.length;
211
- // Include liquid issues in total count even when liquid is disabled
212
- // This ensures liquid errors are shown when liquid content is detected but feature is disabled
213
- const totalCount = htmlCount + labelCount + liquidCount;
177
+ const errorsCount = errors.length;
178
+ const warningsCount = warnings.length;
179
+ const totalCount = errorsCount + warningsCount;
214
180
 
215
- // Set default active key when issues change
216
- useMemo(() => {
217
- if (htmlCount > 0 && !activeKey) {
218
- setActiveKey('html');
219
- } else if (labelCount > 0 && !activeKey) {
220
- setActiveKey('label');
221
- } else if (liquidCount > 0 && !activeKey) {
222
- // Show liquid tab even when liquid is disabled if liquid content is detected
223
- setActiveKey('liquid');
181
+ // Default active tab: errors if any, else warnings
182
+ useEffect(() => {
183
+ if (errorsCount > 0) {
184
+ setActiveKey('errors');
185
+ } else if (warningsCount > 0) {
186
+ setActiveKey('warnings');
224
187
  }
225
- }, [htmlCount, labelCount, liquidCount, activeKey]);
188
+ }, [errorsCount, warningsCount]);
226
189
 
227
- // Don't render if no issues
228
190
  if (totalCount === 0) {
229
191
  return null;
230
192
  }
231
193
 
232
- // Build tab panes (CapTab uses 'panes' with 'tab' and 'content' properties)
233
194
  const tabPanes = [];
234
195
 
235
- if (htmlCount > 0) {
196
+ if (errorsCount > 0) {
236
197
  tabPanes.push({
237
- key: 'html',
198
+ key: 'errors',
238
199
  tab: (
239
- <CapTooltip title={`${intl.formatMessage(messages.htmlIssues)} (${htmlCount})`}>
240
- <span className="validation-tabs__tab-label">
241
- <FormattedMessage {...messages.htmlIssues} />
242
- <span className="validation-tabs__tab-count">
243
- (
244
- {htmlCount}
245
- )
246
- </span>
200
+ <CapTooltip title={`${intl.formatMessage(messages.errors)} (${errorsCount})`}>
201
+ <span className="validation-tabs__tab-label validation-tabs__tab-label--errors">
202
+ <FormattedMessage {...messages.errors} />
203
+ <span className="validation-tabs__tab-count">({errorsCount})</span>
247
204
  </span>
248
205
  </CapTooltip>
249
206
  ),
250
207
  content: (
251
208
  <ValidationTabContent
252
- issues={htmlIssues}
209
+ issues={errors}
253
210
  onErrorClick={onErrorClick}
254
- intl={intl}
211
+ onExpand={onExpand}
255
212
  />
256
213
  ),
257
214
  });
258
215
  }
259
216
 
260
- if (labelCount > 0) {
217
+ if (warningsCount > 0) {
261
218
  tabPanes.push({
262
- key: 'label',
219
+ key: 'warnings',
263
220
  tab: (
264
- <CapTooltip title={`${intl.formatMessage(messages.labelIssues)} (${labelCount})`}>
265
- <span className="validation-tabs__tab-label">
266
- <FormattedMessage {...messages.labelIssues} />
267
- <span className="validation-tabs__tab-count">
268
- (
269
- {labelCount}
270
- )
271
- </span>
221
+ <CapTooltip title={`${intl.formatMessage(messages.warnings)} (${warningsCount})`}>
222
+ <span className="validation-tabs__tab-label validation-tabs__tab-label--warnings">
223
+ <FormattedMessage {...messages.warnings} />
224
+ <span className="validation-tabs__tab-count">({warningsCount})</span>
272
225
  </span>
273
226
  </CapTooltip>
274
227
  ),
275
228
  content: (
276
229
  <ValidationTabContent
277
- issues={labelIssues}
230
+ issues={warnings}
278
231
  onErrorClick={onErrorClick}
279
- intl={intl}
232
+ onExpand={onExpand}
280
233
  />
281
234
  ),
282
235
  });
283
236
  }
284
237
 
285
- // Show liquid issues tab even when liquid is disabled if liquid content is detected
286
- // This allows users to see errors when they add liquid content but liquid feature is not enabled
287
- if (liquidCount > 0) {
288
- tabPanes.push({
289
- key: 'liquid',
290
- tab: (
291
- <CapTooltip title={`${intl.formatMessage(messages.liquidIssues)} (${liquidCount})`}>
292
- <span className="validation-tabs__tab-label">
293
- <FormattedMessage {...messages.liquidIssues} />
294
- <span className="validation-tabs__tab-count">
295
- (
296
- {liquidCount}
297
- )
298
- </span>
299
- </span>
300
- </CapTooltip>
301
- ),
302
- content: (
303
- <ValidationTabContent
304
- issues={liquidIssues}
305
- onErrorClick={onErrorClick}
306
- intl={intl}
307
- />
308
- ),
309
- });
310
- }
311
-
312
- // Handle close
313
- const handleClose = () => {
314
- if (onClose) {
315
- onClose();
238
+ const handleToggleCollapse = () => {
239
+ if (onToggleCollapse) {
240
+ onToggleCollapse();
316
241
  }
317
242
  };
318
243
 
244
+ const collapseLabel = isCollapsed
245
+ ? intl.formatMessage(messages.expandPanel)
246
+ : intl.formatMessage(messages.collapsePanel);
247
+
319
248
  return (
320
- <div className={`validation-tabs ${className || ''}`}>
249
+ <div className={`validation-tabs ${isCollapsed ? 'validation-tabs--collapsed' : ''} ${className || ''}`}>
321
250
  <CapRow className="validation-tabs__header">
322
251
  <StyledCapTab
323
252
  className="validation-tabs__tabs"
@@ -326,14 +255,14 @@ const ValidationTabs = ({
326
255
  panes={tabPanes}
327
256
  />
328
257
  <CapRow className="validation-tabs__actions">
329
- <CapTooltip title={intl.formatMessage(messages.closePanel)}>
258
+ <CapTooltip title={collapseLabel}>
330
259
  <button
331
260
  type="button"
332
- className="validation-tabs__close"
333
- onClick={handleClose}
334
- aria-label={intl.formatMessage(messages.closePanel)}
261
+ className="validation-tabs__collapse-toggle"
262
+ onClick={handleToggleCollapse}
263
+ aria-label={collapseLabel}
335
264
  >
336
- <CapIcon type="close" />
265
+ <CapIcon type={isCollapsed ? 'chevron-up' : 'chevron-down'} />
337
266
  </button>
338
267
  </CapTooltip>
339
268
  </CapRow>
@@ -348,16 +277,18 @@ ValidationTabs.propTypes = {
348
277
  getAllIssues: PropTypes.func,
349
278
  }),
350
279
  onErrorClick: PropTypes.func,
351
- onClose: PropTypes.func,
352
- isLiquidEnabled: PropTypes.bool,
280
+ isCollapsed: PropTypes.bool,
281
+ onToggleCollapse: PropTypes.func,
282
+ onExpand: PropTypes.func,
353
283
  className: PropTypes.string,
354
284
  };
355
285
 
356
286
  ValidationTabs.defaultProps = {
357
287
  validation: null,
358
288
  onErrorClick: null,
359
- onClose: null,
360
- isLiquidEnabled: false,
289
+ isCollapsed: false,
290
+ onToggleCollapse: null,
291
+ onExpand: null,
361
292
  className: '',
362
293
  };
363
294
 
@@ -9,18 +9,14 @@ import { defineMessages } from 'react-intl';
9
9
  const scope = 'app.components.HtmlEditor.ValidationTabs';
10
10
 
11
11
  export default defineMessages({
12
- // Tab labels
13
- htmlIssues: {
14
- id: `${scope}.htmlIssues`,
15
- defaultMessage: 'HTML issues',
12
+ // Tab labels (Errors = blocking, Warnings = non-blocking)
13
+ errors: {
14
+ id: `${scope}.errors`,
15
+ defaultMessage: 'Errors',
16
16
  },
17
- labelIssues: {
18
- id: `${scope}.labelIssues`,
19
- defaultMessage: 'Label issues',
20
- },
21
- liquidIssues: {
22
- id: `${scope}.liquidIssues`,
23
- defaultMessage: 'Liquid issues',
17
+ warnings: {
18
+ id: `${scope}.warnings`,
19
+ defaultMessage: 'Warnings',
24
20
  },
25
21
 
26
22
  // Error item labels
@@ -38,9 +34,13 @@ export default defineMessages({
38
34
  id: `${scope}.navigateToError`,
39
35
  defaultMessage: 'Go to error location',
40
36
  },
41
- closePanel: {
42
- id: `${scope}.closePanel`,
43
- defaultMessage: 'Close validation panel',
37
+ collapsePanel: {
38
+ id: `${scope}.collapsePanel`,
39
+ defaultMessage: 'Collapse validation panel',
40
+ },
41
+ expandPanel: {
42
+ id: `${scope}.expandPanel`,
43
+ defaultMessage: 'Expand validation panel',
44
44
  },
45
45
 
46
46
  // Liquid documentation
@@ -4,6 +4,9 @@
4
4
  * Centralized constants for the HTML Editor component
5
5
  */
6
6
 
7
+ // Documentation URL for Liquid / HTML editor
8
+ export const LIQUID_DOC_URL = 'https://docs.capillarytech.com/docs/liquid-language-in-messages';
9
+
7
10
  // HTML Editor Variants
8
11
  export const HTML_EDITOR_VARIANTS = {
9
12
  EMAIL: 'email',
@@ -10,7 +10,8 @@ import {
10
10
  } from 'react';
11
11
  import { validateHTML, extractAndValidateCSS } from '../utils/htmlValidator';
12
12
  import { sanitizeHTML, isContentSafe, findUnsafeContent } from '../utils/contentSanitizer';
13
- import { BLOCKING_ERROR_RULE_IDS } from '../constants';
13
+ import { BLOCKING_ERROR_RULE_IDS, VALIDATION_SEVERITY } from '../constants';
14
+ import { ISSUE_SOURCES } from '../utils/validationConstants';
14
15
 
15
16
  /**
16
17
  * Custom hook for managing HTML/CSS validation
@@ -29,6 +30,21 @@ const getLineNumberFromPosition = (text, position) => {
29
30
  return text.substring(0, position).split('\n').length;
30
31
  };
31
32
 
33
+ /**
34
+ * Get 1-based line and column from a character position in text
35
+ */
36
+ const getLineAndColumnFromPosition = (text, position) => {
37
+ if (!text || position === undefined || position < 0) {
38
+ return { line: 1, column: 1 };
39
+ }
40
+ const before = text.substring(0, position);
41
+ const lines = before.split('\n');
42
+ const line = lines.length;
43
+ const lastLine = lines[lines.length - 1] || '';
44
+ const column = lastLine.length + 1;
45
+ return { line, column };
46
+ };
47
+
32
48
  /**
33
49
  * Find line number for a tag or pattern in content
34
50
  * Used for API errors to locate where the error occurs
@@ -326,47 +342,52 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
326
342
  const apiLiquidErrors = (apiValidationErrors?.liquidErrors || []).map((errorMessage) => {
327
343
  const extractedLine = extractLineNumberFromMessage(errorMessage);
328
344
  return {
329
- type: 'error',
345
+ type: VALIDATION_SEVERITY.ERROR,
330
346
  message: errorMessage,
331
347
  line: extractedLine,
332
348
  column: null,
333
349
  rule: 'liquid-api-validation',
334
- severity: 'error',
335
- source: 'liquid-validator',
350
+ severity: VALIDATION_SEVERITY.ERROR,
351
+ source: ISSUE_SOURCES.LIQUID,
336
352
  };
337
353
  });
338
354
 
339
355
  const apiStandardErrors = (apiValidationErrors?.standardErrors || []).map((errorMessage) => {
340
356
  const extractedLine = extractLineNumberFromMessage(errorMessage);
341
357
  return {
342
- type: 'error',
358
+ type: VALIDATION_SEVERITY.ERROR,
343
359
  message: errorMessage,
344
360
  line: extractedLine,
345
361
  column: null,
346
362
  rule: 'standard-api-validation',
347
- severity: 'error',
363
+ severity: VALIDATION_SEVERITY.ERROR,
348
364
  source: 'api-validator',
349
365
  };
350
366
  });
351
367
 
352
368
  // Security: protocol types are Rule Group #1 (blocking); others are warnings
369
+ // Use issue.position (from findUnsafeContent) to show real line/column when available
353
370
  const PROTOCOL_TYPES = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
371
+ const contentStr = typeof content === 'string' ? content : '';
354
372
  const securityAsIssues = (validationState.securityIssues || []).map((issue) => {
355
373
  const isBlocking = PROTOCOL_TYPES.includes(issue?.type);
374
+ const { line, column } = (issue?.position !== undefined && contentStr)
375
+ ? getLineAndColumnFromPosition(contentStr, issue.position)
376
+ : { line: 1, column: 1 };
356
377
  return {
357
- type: isBlocking ? 'error' : 'warning',
378
+ type: isBlocking ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING,
358
379
  message: `Security issue: ${issue.type}`,
359
- line: 1,
360
- column: 1,
380
+ line,
381
+ column,
361
382
  rule: isBlocking ? 'sanitizer.dangerousProtocolDetected' : 'security-violation',
362
- severity: isBlocking ? 'error' : 'warning',
383
+ severity: isBlocking ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING,
363
384
  source: 'security',
364
385
  };
365
386
  });
366
387
 
367
388
  // Sanitization warnings (Rule Group #1 entries have rule set by contentSanitizer)
368
389
  const sanitizationAsIssues = (validationState.sanitizationWarnings || []).map((w) => {
369
- const sev = BLOCKING_ERROR_RULE_IDS.includes(w.rule) ? 'error' : 'warning';
390
+ const sev = BLOCKING_ERROR_RULE_IDS.includes(w.rule) ? VALIDATION_SEVERITY.ERROR : VALIDATION_SEVERITY.WARNING;
370
391
  return {
371
392
  ...w,
372
393
  severity: sev,
@@ -399,7 +420,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
399
420
 
400
421
  // Ensure we always return an array
401
422
  return Array.isArray(allIssues) ? allIssues : [];
402
- }, [validationState, apiValidationErrors, extractLineNumberFromMessage]);
423
+ }, [validationState, apiValidationErrors, extractLineNumberFromMessage, content]);
403
424
 
404
425
  /**
405
426
  * Check if validation is clean (no errors or warnings)
@@ -431,7 +452,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
431
452
 
432
453
  const protocolTypes = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
433
454
  // Client-side Liquid validation errors are blocking (genuine syntax errors)
434
- const hasClientSideLiquidErrors = (validationState.htmlErrors || []).some((e) => e.source === 'liquid-validator' && e.severity === 'error');
455
+ const hasClientSideLiquidErrors = (validationState.htmlErrors || []).some((e) => e.source === ISSUE_SOURCES.LIQUID && e.severity === VALIDATION_SEVERITY.ERROR);
435
456
  const hasBlockingErrors = (validationState.sanitizationWarnings || []).some((w) => BLOCKING_ERROR_RULE_IDS.includes(w.rule)) || (validationState.securityIssues || []).some((s) => protocolTypes.includes(s?.type)) || hasApiErrors || hasClientSideLiquidErrors;
436
457
 
437
458
  return {
@@ -328,6 +328,16 @@ export default defineMessages({
328
328
  },
329
329
  },
330
330
 
331
+ htmlEditorTooltip: {
332
+ id: `${scope}.htmlEditorTooltip`,
333
+ defaultMessage: 'This editor supports standard HTML for emails. Avoid CSS frameworks, external stylesheets, and scripts. {docLink}',
334
+ },
335
+
336
+ viewDocumentation: {
337
+ id: `${scope}.viewDocumentation`,
338
+ defaultMessage: 'View documentation',
339
+ },
340
+
331
341
  // HTML Validator messages
332
342
  validator: {
333
343
  // General validation messages