@capillarytech/creatives-library 8.0.262-alpha.0 → 8.0.262

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 (24) hide show
  1. package/package.json +1 -1
  2. package/v2Components/ErrorInfoNote/index.js +57 -112
  3. package/v2Components/ErrorInfoNote/messages.js +8 -12
  4. package/v2Components/ErrorInfoNote/style.scss +0 -4
  5. package/v2Components/HtmlEditor/HTMLEditor.js +46 -182
  6. package/v2Components/HtmlEditor/__tests__/HTMLEditor.apiErrors.test.js +11 -15
  7. package/v2Components/HtmlEditor/__tests__/HTMLEditor.test.js +7 -8
  8. package/v2Components/HtmlEditor/_htmlEditor.scss +6 -6
  9. package/v2Components/HtmlEditor/components/CodeEditorPane/_codeEditorPane.scss +1 -1
  10. package/v2Components/HtmlEditor/components/PreviewPane/_previewPane.scss +4 -4
  11. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/_validationErrorDisplay.scss +17 -0
  12. package/v2Components/HtmlEditor/components/ValidationErrorDisplay/index.js +15 -28
  13. package/v2Components/HtmlEditor/components/ValidationPanel/_validationPanel.scss +0 -4
  14. package/v2Components/HtmlEditor/components/ValidationPanel/index.js +7 -31
  15. package/v2Components/HtmlEditor/components/ValidationTabs/_validationTabs.scss +53 -26
  16. package/v2Components/HtmlEditor/components/ValidationTabs/index.js +74 -144
  17. package/v2Components/HtmlEditor/components/ValidationTabs/messages.js +14 -14
  18. package/v2Components/HtmlEditor/hooks/useValidation.js +23 -3
  19. package/v2Components/HtmlEditor/utils/validationConstants.js +3 -4
  20. package/v2Containers/CreativesContainer/index.js +1 -3
  21. package/v2Containers/CreativesContainer/tests/__snapshots__/index.test.js.snap +6 -9
  22. package/v2Containers/EmailWrapper/components/EmailHTMLEditor.js +5 -49
  23. package/v2Containers/EmailWrapper/components/__tests__/EmailHTMLEditor.test.js +23 -38
  24. 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
 
@@ -19,48 +18,27 @@ import CapTooltip from '@capillarytech/cap-ui-library/CapTooltip';
19
18
  // Messages
20
19
  import messages from './messages';
21
20
  import { BLOCKING_ERROR_RULE_IDS } from '../../constants';
22
- import { ISSUE_SOURCES, LABEL_ISSUE_PATTERNS } from '../../utils/validationConstants';
23
21
 
24
22
  // Styles
25
23
  import './_validationTabs.scss';
26
- import {StyledCapTab} from '../../../../v2Containers/MobilePushNew/style';
24
+ import { StyledCapTab } from '../../../../v2Containers/MobilePushNew/style';
27
25
 
28
26
  /**
29
- * Categorize issues into HTML, Label, and Liquid categories
27
+ * Group issues into Errors (blocking) and Warnings (non-blocking)
30
28
  */
31
- const categorizeIssues = (allIssues) => {
32
- const htmlIssues = [];
33
- const labelIssues = [];
34
- const liquidIssues = [];
29
+ const groupByErrorsAndWarnings = (allIssues) => {
30
+ const errors = [];
31
+ const warnings = [];
35
32
 
36
33
  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;
34
+ if (isBlockingError(issue)) {
35
+ errors.push(issue);
36
+ } else {
37
+ warnings.push(issue);
57
38
  }
58
-
59
- // Default to HTML issues
60
- htmlIssues.push(issue);
61
39
  });
62
40
 
63
- return { htmlIssues, labelIssues, liquidIssues };
41
+ return { errors, warnings };
64
42
  };
65
43
 
66
44
  /**
@@ -83,24 +61,13 @@ const isBlockingError = (issue) => {
83
61
  return false;
84
62
  };
85
63
 
86
- /**
87
- * Get icon based on whether issue is blocking error or warning
88
- * Blocking errors use error-icon, warnings use alert-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="alert-warning" className="validation-tabs__icon validation-tabs__icon--warning" />;
96
- };
97
-
98
64
  /**
99
65
  * ValidationTabContent - Renders the content for each tab
100
66
  */
101
67
  const ValidationTabContent = ({
102
68
  issues,
103
69
  onErrorClick,
70
+ onExpand,
104
71
  }) => {
105
72
  if (!issues || issues.length === 0) {
106
73
  return null;
@@ -108,6 +75,10 @@ const ValidationTabContent = ({
108
75
 
109
76
  const handleNavigateClick = (issue, e) => {
110
77
  e.stopPropagation();
78
+ // Expand panel when redirection is clicked so it does not stay stuck to footer
79
+ if (onExpand) {
80
+ onExpand();
81
+ }
111
82
  if (onErrorClick) {
112
83
  // Always call onErrorClick to acknowledge the error (enables buttons)
113
84
  // If line number exists, navigate to it; otherwise just acknowledge and focus editor
@@ -135,9 +106,6 @@ const ValidationTabContent = ({
135
106
  key={key}
136
107
  className={`validation-tabs__item validation-tabs__item--${displaySeverity}`}
137
108
  >
138
- <div className="validation-tabs__item-icon">
139
- {getSeverityIcon(issue)}
140
- </div>
141
109
  <div className="validation-tabs__item-content">
142
110
  <span className="validation-tabs__item-message">
143
111
  {message}
@@ -173,11 +141,13 @@ const ValidationTabContent = ({
173
141
  ValidationTabContent.propTypes = {
174
142
  issues: PropTypes.array,
175
143
  onErrorClick: PropTypes.func,
144
+ onExpand: PropTypes.func,
176
145
  };
177
146
 
178
147
  ValidationTabContent.defaultProps = {
179
148
  issues: [],
180
149
  onErrorClick: null,
150
+ onExpand: null,
181
151
  };
182
152
 
183
153
  /**
@@ -187,137 +157,95 @@ const ValidationTabs = ({
187
157
  intl,
188
158
  validation,
189
159
  onErrorClick,
190
- onClose,
160
+ isCollapsed = false,
161
+ onToggleCollapse,
162
+ onExpand,
191
163
  className,
192
164
  }) => {
193
- const [activeKey, setActiveKey] = useState(null);
165
+ const [activeKey, setActiveKey] = useState('errors');
194
166
 
195
- // Categorize issues
196
- const { htmlIssues, labelIssues, liquidIssues } = useMemo(() => {
167
+ // Group issues into Errors (blocking) and Warnings (non-blocking)
168
+ const { errors, warnings } = useMemo(() => {
197
169
  if (!validation) {
198
- return { htmlIssues: [], labelIssues: [], liquidIssues: [] };
170
+ return { errors: [], warnings: [] };
199
171
  }
200
-
201
- // Get all issues from validation
202
172
  const allIssues = validation.getAllIssues ? validation.getAllIssues() : [];
203
- const categorized = categorizeIssues(allIssues);
204
- return categorized;
173
+ return groupByErrorsAndWarnings(allIssues);
205
174
  }, [validation]);
206
175
 
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;
176
+ const errorsCount = errors.length;
177
+ const warningsCount = warnings.length;
178
+ const totalCount = errorsCount + warningsCount;
214
179
 
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');
180
+ // Default active tab: errors if any, else warnings
181
+ useEffect(() => {
182
+ if (errorsCount > 0) {
183
+ setActiveKey('errors');
184
+ } else if (warningsCount > 0) {
185
+ setActiveKey('warnings');
224
186
  }
225
- }, [htmlCount, labelCount, liquidCount, activeKey]);
187
+ }, [errorsCount, warningsCount]);
226
188
 
227
- // Don't render if no issues
228
189
  if (totalCount === 0) {
229
190
  return null;
230
191
  }
231
192
 
232
- // Build tab panes (CapTab uses 'panes' with 'tab' and 'content' properties)
233
193
  const tabPanes = [];
234
194
 
235
- if (htmlCount > 0) {
195
+ if (errorsCount > 0) {
236
196
  tabPanes.push({
237
- key: 'html',
197
+ key: 'errors',
238
198
  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>
199
+ <CapTooltip title={`${intl.formatMessage(messages.errors)} (${errorsCount})`}>
200
+ <span className="validation-tabs__tab-label validation-tabs__tab-label--errors">
201
+ <FormattedMessage {...messages.errors} />
202
+ <span className="validation-tabs__tab-count">({errorsCount})</span>
247
203
  </span>
248
204
  </CapTooltip>
249
205
  ),
250
206
  content: (
251
207
  <ValidationTabContent
252
- issues={htmlIssues}
208
+ issues={errors}
253
209
  onErrorClick={onErrorClick}
254
- intl={intl}
210
+ onExpand={onExpand}
255
211
  />
256
212
  ),
257
213
  });
258
214
  }
259
215
 
260
- if (labelCount > 0) {
216
+ if (warningsCount > 0) {
261
217
  tabPanes.push({
262
- key: 'label',
218
+ key: 'warnings',
263
219
  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>
220
+ <CapTooltip title={`${intl.formatMessage(messages.warnings)} (${warningsCount})`}>
221
+ <span className="validation-tabs__tab-label validation-tabs__tab-label--warnings">
222
+ <FormattedMessage {...messages.warnings} />
223
+ <span className="validation-tabs__tab-count">({warningsCount})</span>
272
224
  </span>
273
225
  </CapTooltip>
274
226
  ),
275
227
  content: (
276
228
  <ValidationTabContent
277
- issues={labelIssues}
229
+ issues={warnings}
278
230
  onErrorClick={onErrorClick}
279
- intl={intl}
231
+ onExpand={onExpand}
280
232
  />
281
233
  ),
282
234
  });
283
235
  }
284
236
 
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();
237
+ const handleToggleCollapse = () => {
238
+ if (onToggleCollapse) {
239
+ onToggleCollapse();
316
240
  }
317
241
  };
318
242
 
243
+ const collapseLabel = isCollapsed
244
+ ? intl.formatMessage(messages.expandPanel)
245
+ : intl.formatMessage(messages.collapsePanel);
246
+
319
247
  return (
320
- <div className={`validation-tabs ${className || ''}`}>
248
+ <div className={`validation-tabs ${isCollapsed ? 'validation-tabs--collapsed' : ''} ${className || ''}`}>
321
249
  <CapRow className="validation-tabs__header">
322
250
  <StyledCapTab
323
251
  className="validation-tabs__tabs"
@@ -326,14 +254,14 @@ const ValidationTabs = ({
326
254
  panes={tabPanes}
327
255
  />
328
256
  <CapRow className="validation-tabs__actions">
329
- <CapTooltip title={intl.formatMessage(messages.closePanel)}>
257
+ <CapTooltip title={collapseLabel}>
330
258
  <button
331
259
  type="button"
332
- className="validation-tabs__close"
333
- onClick={handleClose}
334
- aria-label={intl.formatMessage(messages.closePanel)}
260
+ className="validation-tabs__collapse-toggle"
261
+ onClick={handleToggleCollapse}
262
+ aria-label={collapseLabel}
335
263
  >
336
- <CapIcon type="close" />
264
+ <CapIcon type={isCollapsed ? 'chevron-up' : 'chevron-down'} />
337
265
  </button>
338
266
  </CapTooltip>
339
267
  </CapRow>
@@ -348,16 +276,18 @@ ValidationTabs.propTypes = {
348
276
  getAllIssues: PropTypes.func,
349
277
  }),
350
278
  onErrorClick: PropTypes.func,
351
- onClose: PropTypes.func,
352
- isLiquidEnabled: PropTypes.bool,
279
+ isCollapsed: PropTypes.bool,
280
+ onToggleCollapse: PropTypes.func,
281
+ onExpand: PropTypes.func,
353
282
  className: PropTypes.string,
354
283
  };
355
284
 
356
285
  ValidationTabs.defaultProps = {
357
286
  validation: null,
358
287
  onErrorClick: null,
359
- onClose: null,
360
- isLiquidEnabled: false,
288
+ isCollapsed: false,
289
+ onToggleCollapse: null,
290
+ onExpand: null,
361
291
  className: '',
362
292
  };
363
293
 
@@ -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
@@ -29,6 +29,21 @@ const getLineNumberFromPosition = (text, position) => {
29
29
  return text.substring(0, position).split('\n').length;
30
30
  };
31
31
 
32
+ /**
33
+ * Get 1-based line and column from a character position in text
34
+ */
35
+ const getLineAndColumnFromPosition = (text, position) => {
36
+ if (!text || position === undefined || position < 0) {
37
+ return { line: 1, column: 1 };
38
+ }
39
+ const before = text.substring(0, position);
40
+ const lines = before.split('\n');
41
+ const line = lines.length;
42
+ const lastLine = lines[lines.length - 1] || '';
43
+ const column = lastLine.length + 1;
44
+ return { line, column };
45
+ };
46
+
32
47
  /**
33
48
  * Find line number for a tag or pattern in content
34
49
  * Used for API errors to locate where the error occurs
@@ -350,14 +365,19 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
350
365
  });
351
366
 
352
367
  // Security: protocol types are Rule Group #1 (blocking); others are warnings
368
+ // Use issue.position (from findUnsafeContent) to show real line/column when available
353
369
  const PROTOCOL_TYPES = ['JavaScript Protocol', 'Data URL', 'VBScript Protocol'];
370
+ const contentStr = typeof content === 'string' ? content : '';
354
371
  const securityAsIssues = (validationState.securityIssues || []).map((issue) => {
355
372
  const isBlocking = PROTOCOL_TYPES.includes(issue?.type);
373
+ const { line, column } = (issue?.position !== undefined && contentStr)
374
+ ? getLineAndColumnFromPosition(contentStr, issue.position)
375
+ : { line: 1, column: 1 };
356
376
  return {
357
377
  type: isBlocking ? 'error' : 'warning',
358
378
  message: `Security issue: ${issue.type}`,
359
- line: 1,
360
- column: 1,
379
+ line,
380
+ column,
361
381
  rule: isBlocking ? 'sanitizer.dangerousProtocolDetected' : 'security-violation',
362
382
  severity: isBlocking ? 'error' : 'warning',
363
383
  source: 'security',
@@ -399,7 +419,7 @@ export const useValidation = (content, variant = 'email', options = {}, formatSa
399
419
 
400
420
  // Ensure we always return an array
401
421
  return Array.isArray(allIssues) ? allIssues : [];
402
- }, [validationState, apiValidationErrors, extractLineNumberFromMessage]);
422
+ }, [validationState, apiValidationErrors, extractLineNumberFromMessage, content]);
403
423
 
404
424
  /**
405
425
  * Check if validation is clean (no errors or warnings)
@@ -31,9 +31,8 @@ export const LABEL_ISSUE_PATTERNS = [
31
31
  'alt-require',
32
32
  ];
33
33
 
34
- // Tab keys for error categorization
34
+ // Tab keys: Errors = blocking, Warnings = non-blocking
35
35
  export const ERROR_TAB_KEYS = {
36
- HTML: 'html',
37
- LABEL: 'label',
38
- LIQUID: 'liquid',
36
+ ERRORS: 'errors',
37
+ WARNINGS: 'warnings',
39
38
  };
@@ -126,9 +126,7 @@ export class Creatives extends React.Component {
126
126
  // HTML Editor validation state (for email channel)
127
127
  htmlEditorValidationState: {
128
128
  isContentEmpty: true,
129
- issueCounts: {
130
- html: 0, label: 0, liquid: 0, total: 0,
131
- },
129
+ issueCounts: { errors: 0, warnings: 0, total: 0 },
132
130
  validationComplete: false, // Flag to track if validation has completed
133
131
  errorsAcknowledged: false, // Flag to track if user has acknowledged errors by clicking redirection icon
134
132
  },
@@ -262,10 +262,9 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit all data
262
262
  "errorsAcknowledged": false,
263
263
  "isContentEmpty": true,
264
264
  "issueCounts": Object {
265
- "html": 0,
266
- "label": 0,
267
- "liquid": 0,
265
+ "errors": 0,
268
266
  "total": 0,
267
+ "warnings": 0,
269
268
  },
270
269
  "validationComplete": false,
271
270
  }
@@ -392,10 +391,9 @@ exports[`Test SlideBoxContent container campaign message, whatsapp edit min data
392
391
  "errorsAcknowledged": false,
393
392
  "isContentEmpty": true,
394
393
  "issueCounts": Object {
395
- "html": 0,
396
- "label": 0,
397
- "liquid": 0,
394
+ "errors": 0,
398
395
  "total": 0,
396
+ "warnings": 0,
399
397
  },
400
398
  "validationComplete": false,
401
399
  }
@@ -522,10 +520,9 @@ exports[`Test SlideBoxContent container it should clear the url, on channel chan
522
520
  "errorsAcknowledged": false,
523
521
  "isContentEmpty": true,
524
522
  "issueCounts": Object {
525
- "html": 0,
526
- "label": 0,
527
- "liquid": 0,
523
+ "errors": 0,
528
524
  "total": 0,
525
+ "warnings": 0,
529
526
  },
530
527
  "validationComplete": false,
531
528
  }
@@ -651,55 +651,11 @@ const EmailHTMLEditor = (props) => {
651
651
  setSubjectError('');
652
652
  }
653
653
 
654
- // 1.5. Check for HTML/Label/Liquid validation errors (excluding API errors which we just cleared)
655
- // If errors exist, block save and reset acknowledgment
656
- // Try to get issue counts from ref first, fallback to stored state
657
- let issueCounts = {
658
- html: 0, label: 0, liquid: 0, total: 0,
659
- };
660
- let allIssues = [];
661
- if (htmlEditorRef.current && typeof htmlEditorRef.current.getAllIssues === 'function') {
662
- allIssues = htmlEditorRef.current.getAllIssues();
663
- // Filter out API validation errors - they're validated separately via API call
664
- const clientSideIssues = allIssues.filter((issue) => issue.rule !== 'liquid-api-validation' && issue.rule !== 'standard-api-validation');
665
-
666
- // Count only client-side errors
667
- issueCounts = {
668
- html: 0,
669
- label: 0,
670
- liquid: 0,
671
- total: clientSideIssues.length,
672
- };
673
-
674
- clientSideIssues.forEach((issue) => {
675
- const { source, rule, message } = issue;
676
- const messageLower = (message || '').toLowerCase();
677
- const ruleLower = (rule || '').toLowerCase();
678
-
679
- // Check if it's a liquid error (client-side only, not API)
680
- if (source === 'liquid-validator' && rule !== 'liquid-api-validation') {
681
- issueCounts.liquid++;
682
- } else if (
683
- messageLower.includes('tag must be paired')
684
- || messageLower.includes('open tag match failed')
685
- || messageLower.includes('closed tag match failed')
686
- || messageLower.includes('unclosed')
687
- || messageLower.includes('missing required')
688
- || ruleLower.includes('tag-pair')
689
- || ruleLower.includes('attr-value-not-empty')
690
- || ruleLower.includes('attr-no-duplication')
691
- || ruleLower.includes('tag-self-close')
692
- || ruleLower.includes('spec-char-escape')
693
- || ruleLower.includes('tagname-lowercase')
694
- || ruleLower.includes('attr-lowercase')
695
- || ruleLower.includes('src-not-empty')
696
- || ruleLower.includes('alt-require')
697
- ) {
698
- issueCounts.label++;
699
- } else {
700
- issueCounts.html++;
701
- }
702
- });
654
+ // 1.5. Check for validation errors (Errors = blocking, Warnings = non-blocking)
655
+ // Get issue counts from ref or stored state
656
+ let issueCounts = { errors: 0, warnings: 0, total: 0 };
657
+ if (htmlEditorRef.current && typeof htmlEditorRef.current.getIssueCounts === 'function') {
658
+ issueCounts = htmlEditorRef.current.getIssueCounts();
703
659
  } else if (lastValidationStateRef.current?.issueCounts) {
704
660
  issueCounts = lastValidationStateRef.current.issueCounts;
705
661
  }