@griddo/ax 11.12.1-rc.0 → 11.12.1-rc.10

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@griddo/ax",
3
3
  "description": "Griddo Author Experience",
4
- "version": "11.12.1-rc.0",
4
+ "version": "11.12.1-rc.10",
5
5
  "authors": [
6
6
  "Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
7
7
  "Diego M. Béjar <diego.bejar@secuoyas.com>",
@@ -217,5 +217,5 @@
217
217
  "publishConfig": {
218
218
  "access": "public"
219
219
  },
220
- "gitHead": "658c8d4628746910799f072fb0ab2603398804a0"
220
+ "gitHead": "eb32fff3cb2388110e448459eca5a251580ffc77"
221
221
  }
@@ -70,7 +70,7 @@ describe("Browser component rendering", () => {
70
70
 
71
71
  renderBrowser(defaultProps);
72
72
 
73
- expect(screen.getByTestId("nav-actions-wrapper")).toBeInTheDocument();
73
+ expect(screen.queryByTestId("nav-actions-wrapper")).not.toBeInTheDocument();
74
74
  expect(screen.queryByTestId("navbar-iframe-wrapper")).not.toBeInTheDocument();
75
75
  expect(screen.getByTestId("browser-content-wrapper")).toBeInTheDocument();
76
76
  });
@@ -312,7 +312,7 @@ describe("Events", () => {
312
312
  fireEvent.click(actionMenu[1]);
313
313
 
314
314
  const buttonsDefault = screen.getAllByTestId("button-default");
315
- fireEvent.click(buttonsDefault[4]);
315
+ fireEvent.click(buttonsDefault[buttonsDefault.length - 1]);
316
316
 
317
317
  const integrationsWithDeletion = initialStore.integrations.integrations.slice(0, 3);
318
318
 
@@ -290,6 +290,12 @@ describe("UrlField component rendering", () => {
290
290
  render(Component, { store });
291
291
  });
292
292
 
293
+ // Open FloatingPanel first
294
+ const pageField = screen.getByTestId("page-field");
295
+ await act(async () => {
296
+ fireEvent.click(pageField);
297
+ });
298
+
293
299
  const selectComponents = screen.getAllByTestId("select-component");
294
300
 
295
301
  await act(async () => {
@@ -384,6 +390,12 @@ describe("onClick events", () => {
384
390
  render(Component, { store });
385
391
  });
386
392
 
393
+ // Open FloatingPanel first
394
+ const pageField = screen.getByTestId("page-field");
395
+ await act(async () => {
396
+ fireEvent.click(pageField);
397
+ });
398
+
387
399
  await act(async () => {
388
400
  const selectionListItem = screen.getAllByTestId("selection-list-item");
389
401
  selectionListItem[0].click();
@@ -430,10 +442,16 @@ describe("onClick events", () => {
430
442
  render(Component, { store });
431
443
  });
432
444
 
433
- const selectionListItem = screen.getAllByTestId("selection-list-item");
434
445
  const checkFieldInputs = screen.getAllByTestId("check-field-input");
435
446
  const pageField = screen.getByTestId("page-field");
436
447
 
448
+ // Open FloatingPanel first
449
+ await act(async () => {
450
+ fireEvent.click(pageField);
451
+ });
452
+
453
+ const selectionListItem = screen.getAllByTestId("selection-list-item");
454
+
437
455
  await act(async () => {
438
456
  selectionListItem[0].click();
439
457
  fireEvent.click(checkFieldInputs[0]);
@@ -20,6 +20,7 @@ const mockError: IHeadingError = {
20
20
  const defaultProps = {
21
21
  error: mockError,
22
22
  onSelectHeading: jest.fn(() => jest.fn()),
23
+ onDelete: jest.fn(),
23
24
  };
24
25
 
25
26
  const renderComponent = (props = defaultProps) =>
@@ -0,0 +1,303 @@
1
+ import { act, renderHook } from "@testing-library/react";
2
+ import "@testing-library/jest-dom";
3
+
4
+ import { useModal, useModals } from "@ax/hooks";
5
+
6
+ describe("useModal hook", () => {
7
+ beforeEach(() => {
8
+ document.body.classList.remove("modal-open");
9
+ });
10
+
11
+ afterEach(() => {
12
+ document.body.classList.remove("modal-open");
13
+ });
14
+
15
+ it("should initialize with false when no initialState provided", () => {
16
+ const { result } = renderHook(() => useModal());
17
+ expect(result.current.isOpen).toBe(false);
18
+ });
19
+
20
+ it("should initialize with provided initialState", () => {
21
+ const { result } = renderHook(() => useModal(true));
22
+ expect(result.current.isOpen).toBe(true);
23
+ });
24
+
25
+ it("should toggle modal state when toggleModal is called", () => {
26
+ const { result } = renderHook(() => useModal(false));
27
+ expect(result.current.isOpen).toBe(false);
28
+
29
+ act(() => {
30
+ result.current.toggleModal();
31
+ });
32
+
33
+ expect(result.current.isOpen).toBe(true);
34
+
35
+ act(() => {
36
+ result.current.toggleModal();
37
+ });
38
+
39
+ expect(result.current.isOpen).toBe(false);
40
+ });
41
+
42
+ it("should add modal-open class to body when modal opens and bodyBlock is true", () => {
43
+ const { result } = renderHook(() => useModal(false, true));
44
+ expect(document.body.classList.contains("modal-open")).toBe(false);
45
+
46
+ act(() => {
47
+ result.current.toggleModal();
48
+ });
49
+
50
+ expect(document.body.classList.contains("modal-open")).toBe(true);
51
+ });
52
+
53
+ it("should not add modal-open class to body when bodyBlock is false", () => {
54
+ const { result } = renderHook(() => useModal(false, false));
55
+
56
+ act(() => {
57
+ result.current.toggleModal();
58
+ });
59
+
60
+ expect(document.body.classList.contains("modal-open")).toBe(false);
61
+ });
62
+
63
+ it("should check if modal-open class already exists before adding", () => {
64
+ document.body.classList.add("modal-open");
65
+
66
+ const { result } = renderHook(() => useModal(false, true));
67
+
68
+ act(() => {
69
+ result.current.toggleModal();
70
+ });
71
+
72
+ // Class should still be present
73
+ expect(document.body.classList.contains("modal-open")).toBe(true);
74
+
75
+ // Count of modal-open should be 1
76
+ const classList = [...document.body.classList];
77
+ const count = classList.filter((c) => c === "modal-open").length;
78
+ expect(count).toBe(1);
79
+ });
80
+ });
81
+
82
+ describe("useModals hook", () => {
83
+ beforeEach(() => {
84
+ document.body.classList.remove("modal-open");
85
+ });
86
+
87
+ afterEach(() => {
88
+ document.body.classList.remove("modal-open");
89
+ });
90
+
91
+ it("should initialize all modals as closed", () => {
92
+ const { result } = renderHook(() => useModals(["modal1", "modal2", "modal3"]));
93
+
94
+ expect(result.current.isOpen("modal1")).toBe(false);
95
+ expect(result.current.isOpen("modal2")).toBe(false);
96
+ expect(result.current.isOpen("modal3")).toBe(false);
97
+ });
98
+
99
+ it("should open a modal with openModal", () => {
100
+ const { result } = renderHook(() => useModals(["modal1", "modal2"]));
101
+
102
+ act(() => {
103
+ result.current.openModal("modal1");
104
+ });
105
+
106
+ expect(result.current.isOpen("modal1")).toBe(true);
107
+ expect(result.current.isOpen("modal2")).toBe(false);
108
+ });
109
+
110
+ it("should close a modal with closeModal", () => {
111
+ const { result } = renderHook(() => useModals(["modal1"]));
112
+
113
+ act(() => {
114
+ result.current.openModal("modal1");
115
+ });
116
+
117
+ expect(result.current.isOpen("modal1")).toBe(true);
118
+
119
+ act(() => {
120
+ result.current.closeModal("modal1");
121
+ });
122
+
123
+ expect(result.current.isOpen("modal1")).toBe(false);
124
+ });
125
+
126
+ it("should toggle a modal with toggleModal", () => {
127
+ const { result } = renderHook(() => useModals(["modal1"]));
128
+
129
+ expect(result.current.isOpen("modal1")).toBe(false);
130
+
131
+ act(() => {
132
+ result.current.toggleModal("modal1");
133
+ });
134
+
135
+ expect(result.current.isOpen("modal1")).toBe(true);
136
+
137
+ act(() => {
138
+ result.current.toggleModal("modal1");
139
+ });
140
+
141
+ expect(result.current.isOpen("modal1")).toBe(false);
142
+ });
143
+
144
+ it("should handle multiple modals independently", () => {
145
+ const { result } = renderHook(() => useModals(["modal1", "modal2", "modal3"]));
146
+
147
+ act(() => {
148
+ result.current.openModal("modal1");
149
+ });
150
+
151
+ expect(result.current.isOpen("modal1")).toBe(true);
152
+ expect(result.current.isOpen("modal2")).toBe(false);
153
+ expect(result.current.isOpen("modal3")).toBe(false);
154
+
155
+ act(() => {
156
+ result.current.openModal("modal2");
157
+ });
158
+
159
+ expect(result.current.isOpen("modal1")).toBe(true);
160
+ expect(result.current.isOpen("modal2")).toBe(true);
161
+ expect(result.current.isOpen("modal3")).toBe(false);
162
+ });
163
+
164
+ it("should add modal-open class when a modal is opened", () => {
165
+ const { result } = renderHook(() => useModals(["modal1"], true));
166
+
167
+ expect(document.body.classList.contains("modal-open")).toBe(false);
168
+
169
+ act(() => {
170
+ result.current.openModal("modal1");
171
+ });
172
+
173
+ expect(document.body.classList.contains("modal-open")).toBe(true);
174
+ });
175
+
176
+ it("should remove modal-open class when all modals are closed", () => {
177
+ const { result } = renderHook(() => useModals(["modal1", "modal2"], true));
178
+
179
+ act(() => {
180
+ result.current.openModal("modal1");
181
+ result.current.openModal("modal2");
182
+ });
183
+
184
+ expect(document.body.classList.contains("modal-open")).toBe(true);
185
+
186
+ act(() => {
187
+ result.current.closeModal("modal1");
188
+ });
189
+
190
+ expect(document.body.classList.contains("modal-open")).toBe(true);
191
+
192
+ act(() => {
193
+ result.current.closeModal("modal2");
194
+ });
195
+
196
+ expect(document.body.classList.contains("modal-open")).toBe(false);
197
+ });
198
+
199
+ it("should close multiple modals individually", () => {
200
+ const { result } = renderHook(() => useModals(["modal1", "modal2", "modal3"]));
201
+
202
+ act(() => {
203
+ result.current.openModal("modal1");
204
+ result.current.openModal("modal2");
205
+ result.current.openModal("modal3");
206
+ });
207
+
208
+ expect(result.current.isOpen("modal1")).toBe(true);
209
+ expect(result.current.isOpen("modal2")).toBe(true);
210
+ expect(result.current.isOpen("modal3")).toBe(true);
211
+
212
+ act(() => {
213
+ result.current.closeModal("modal1");
214
+ result.current.closeModal("modal2");
215
+ result.current.closeModal("modal3");
216
+ });
217
+
218
+ expect(result.current.isOpen("modal1")).toBe(false);
219
+ expect(result.current.isOpen("modal2")).toBe(false);
220
+ expect(result.current.isOpen("modal3")).toBe(false);
221
+ });
222
+
223
+ it("should not add modal-open class if bodyBlock is false", () => {
224
+ const { result } = renderHook(() => useModals(["modal1"], false));
225
+
226
+ act(() => {
227
+ result.current.openModal("modal1");
228
+ });
229
+
230
+ expect(document.body.classList.contains("modal-open")).toBe(false);
231
+ });
232
+
233
+ it("should not duplicate modal-open class when multiple modals are open", () => {
234
+ const { result } = renderHook(() => useModals(["modal1", "modal2"], true));
235
+
236
+ act(() => {
237
+ result.current.openModal("modal1");
238
+ });
239
+
240
+ const classList1 = [...document.body.classList];
241
+ const count1 = classList1.filter((c) => c === "modal-open").length;
242
+
243
+ act(() => {
244
+ result.current.openModal("modal2");
245
+ });
246
+
247
+ const classList2 = [...document.body.classList];
248
+ const count2 = classList2.filter((c) => c === "modal-open").length;
249
+
250
+ expect(count1).toBe(1);
251
+ expect(count2).toBe(1);
252
+ });
253
+
254
+ it("should handle isOpen callback correctly", () => {
255
+ const { result } = renderHook(() => useModals(["modal1"]));
256
+
257
+ act(() => {
258
+ result.current.openModal("modal1");
259
+ });
260
+
261
+ const isOpenResult = result.current.isOpen("modal1");
262
+ expect(isOpenResult).toBe(true);
263
+ });
264
+ });
265
+
266
+ describe("useModal and useModals working together", () => {
267
+ beforeEach(() => {
268
+ document.body.classList.remove("modal-open");
269
+ });
270
+
271
+ afterEach(() => {
272
+ document.body.classList.remove("modal-open");
273
+ });
274
+
275
+ it("should verify both hooks add modal-open class independently", () => {
276
+ const { result: modalResult } = renderHook(() => useModal(false, true));
277
+ const { result: modalsResult } = renderHook(() => useModals(["modal1"], true));
278
+
279
+ // useModal should add class
280
+ act(() => {
281
+ modalResult.current.toggleModal();
282
+ });
283
+ expect(document.body.classList.contains("modal-open")).toBe(true);
284
+
285
+ // useModals should keep class
286
+ act(() => {
287
+ modalsResult.current.openModal("modal1");
288
+ });
289
+ expect(document.body.classList.contains("modal-open")).toBe(true);
290
+
291
+ // Closing useModal should keep class (useModals still open)
292
+ act(() => {
293
+ modalResult.current.toggleModal();
294
+ });
295
+ // Note: This depends on DOM check in useModal
296
+
297
+ // Closing useModals should remove class
298
+ act(() => {
299
+ modalsResult.current.closeModal("modal1");
300
+ });
301
+ expect(document.body.classList.contains("modal-open")).toBe(false);
302
+ });
303
+ });
@@ -63,6 +63,7 @@ const Browser = (props: IBrowserProps): JSX.Element => {
63
63
  const isFormEditor = editorType === "form";
64
64
  const isHeadingsEditor = editorType === "headings";
65
65
  const isKeywordsEditor = editorType === "keywords";
66
+ const isZoomEditor = isPageEditor || isHeadingsEditor || isKeywordsEditor;
66
67
 
67
68
  const frameWrapperRef = useRef<HTMLDivElement>(null);
68
69
 
@@ -82,6 +83,8 @@ const Browser = (props: IBrowserProps): JSX.Element => {
82
83
  localStorage.setItem("selectedID", "0");
83
84
  (window as any).browserRef = null;
84
85
 
86
+ if (!isZoomEditor) return;
87
+
85
88
  const el = frameWrapperRef.current;
86
89
  if (!el) return;
87
90
 
@@ -99,7 +102,7 @@ const Browser = (props: IBrowserProps): JSX.Element => {
99
102
  observer.observe(el);
100
103
 
101
104
  return () => observer.disconnect();
102
- }, []);
105
+ }, [isZoomEditor]);
103
106
 
104
107
  // Fetch share data when in preview mode
105
108
  useEffect(() => {
@@ -188,7 +191,7 @@ const Browser = (props: IBrowserProps): JSX.Element => {
188
191
  }
189
192
  };
190
193
 
191
- const scaledWidth = !isPreview
194
+ const scaledWidth = isZoomEditor
192
195
  ? Math.floor(parseInt(dimensions.resolution) * (parseInt(dimensions.zoom) / 100))
193
196
  : undefined;
194
197
 
@@ -201,8 +204,13 @@ const Browser = (props: IBrowserProps): JSX.Element => {
201
204
 
202
205
  return (
203
206
  <S.OuterContainer ref={frameWrapperRef}>
204
- <S.BrowserWrapper data-testid="browser-wrapper" ref={browserRef} scaledWidth={scaledWidth} isPreview={isPreview}>
205
- {(isPageEditor || isHeadingsEditor || isKeywordsEditor) && (
207
+ <S.BrowserWrapper
208
+ data-testid="browser-wrapper"
209
+ ref={browserRef}
210
+ scaledWidth={scaledWidth}
211
+ isPreview={!isZoomEditor}
212
+ >
213
+ {isZoomEditor && (
206
214
  <S.NavBar>
207
215
  <S.NavUrl>{url}</S.NavUrl>
208
216
  {isPreview ? (
@@ -247,30 +255,34 @@ const Browser = (props: IBrowserProps): JSX.Element => {
247
255
  </S.IconWrapper>
248
256
  </S.NavActions>
249
257
  ) : (
250
- <S.NavActions data-testid="nav-actions-wrapper">
251
- <S.ResolutionWrapper>
252
- <S.SelectLabel>Screen</S.SelectLabel>
253
- <Select
254
- name="resolution"
255
- options={resolutionOptions}
256
- value={dimensions.resolution}
257
- onChange={handleResolutionChange}
258
- type="round"
259
- offSet="30%"
260
- mandatory
261
- />
262
- </S.ResolutionWrapper>
263
- <S.ZoomWrapper>
264
- <Select
265
- name="zoom"
266
- options={zoomOptionsWithDisabled}
267
- value={dimensions.zoom}
268
- onChange={handleZoomChange}
269
- type="round"
270
- mandatory
271
- />
272
- </S.ZoomWrapper>
273
- </S.NavActions>
258
+ <>
259
+ {showIframe && (
260
+ <S.NavActions data-testid="nav-actions-wrapper">
261
+ <S.ResolutionWrapper>
262
+ <S.SelectLabel>Screen</S.SelectLabel>
263
+ <Select
264
+ name="resolution"
265
+ options={resolutionOptions}
266
+ value={dimensions.resolution}
267
+ onChange={handleResolutionChange}
268
+ type="round"
269
+ offSet="30%"
270
+ mandatory
271
+ />
272
+ </S.ResolutionWrapper>
273
+ <S.ZoomWrapper>
274
+ <Select
275
+ name="zoom"
276
+ options={zoomOptionsWithDisabled}
277
+ value={dimensions.zoom}
278
+ onChange={handleZoomChange}
279
+ type="round"
280
+ mandatory
281
+ />
282
+ </S.ZoomWrapper>
283
+ </S.NavActions>
284
+ )}
285
+ </>
274
286
  )}
275
287
  </S.NavBar>
276
288
  )}
@@ -284,11 +296,7 @@ const Browser = (props: IBrowserProps): JSX.Element => {
284
296
  $compact={isCompact}
285
297
  />
286
298
  )}
287
- <S.FrameWrapper
288
- hasBorder={isPageEditor || isHeadingsEditor || isKeywordsEditor}
289
- isFormEditor={isFormEditor}
290
- data-testid="navbar-iframe-wrapper"
291
- >
299
+ <S.FrameWrapper hasBorder={isZoomEditor} isFormEditor={isFormEditor} data-testid="navbar-iframe-wrapper">
292
300
  {isPreview ? (
293
301
  <iframe
294
302
  title="Preview"
@@ -301,23 +309,23 @@ const Browser = (props: IBrowserProps): JSX.Element => {
301
309
  ) : (
302
310
  <div
303
311
  style={{
304
- width: `${scaledWidth}px`,
312
+ width: scaledWidth !== undefined ? `${scaledWidth}px` : "100%",
305
313
  height: "100%",
306
314
  overflow: "hidden",
307
- flexShrink: 0,
315
+ flexShrink: isZoomEditor ? 0 : "unset",
308
316
  }}
309
317
  >
310
318
  <iframe
311
319
  title="Preview"
312
- width={dimensions.resolution}
320
+ width={isZoomEditor ? dimensions.resolution : "100%"}
313
321
  src={urlPreview}
314
322
  loading="lazy"
315
323
  className="frame-content"
316
324
  style={{
317
325
  display: "block",
318
- transform: `scale(${parseInt(dimensions.zoom) / 100})`,
326
+ transform: isZoomEditor ? `scale(${parseInt(dimensions.zoom) / 100})` : "scale(1)",
319
327
  transformOrigin: "0 0",
320
- height: `${Math.round(100 / (parseInt(dimensions.zoom) / 100))}%`,
328
+ height: isZoomEditor ? `${Math.round(100 / (parseInt(dimensions.zoom) / 100))}%` : "100%",
321
329
  }}
322
330
  />
323
331
  </div>
@@ -70,7 +70,7 @@ const FrameWrapper = styled.div<{ hasBorder: boolean; isFormEditor: boolean }>`
70
70
  justify-content: center;
71
71
  flex: 1;
72
72
  min-height: 0;
73
- padding: ${(p) => (p.isFormEditor ? p.theme.spacing.m : "0")};
73
+ padding: ${(p) => (p.isFormEditor ? `${p.theme.spacing.m} 0 0 ${p.theme.spacing.m}` : "0")};
74
74
  `;
75
75
 
76
76
  const OuterContainer = styled.div`
@@ -1,8 +1,8 @@
1
1
  import { memo, useRef } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
 
4
- import { useHandleClickOutside } from "@ax/hooks";
5
4
  import { IconAction } from "@ax/components";
5
+ import { useHandleClickOutside } from "@ax/hooks";
6
6
 
7
7
  import * as S from "./style";
8
8
 
@@ -43,6 +43,7 @@ const FloatingPanel = (props: IFloatingPanelProps): JSX.Element | null => {
43
43
  return createPortal(
44
44
  <S.Wrapper
45
45
  data-testid="floating-panel"
46
+ data-is-open={isOpen}
46
47
  ref={node}
47
48
  isOpen={isOpen}
48
49
  isOpenedSecond={isOpenedSecond}
@@ -7,7 +7,7 @@ import type { IHeadingError } from "../../utils";
7
7
  import * as S from "./style";
8
8
 
9
9
  const ErrorsItem = (props: IErrorsItemProps) => {
10
- const { error, onSelectHeading } = props;
10
+ const { error, onSelectHeading, onDelete } = props;
11
11
  const { message, description, headingIds } = error;
12
12
 
13
13
  const [isOpen, setIsOpen] = useState(false);
@@ -54,7 +54,12 @@ const ErrorsItem = (props: IErrorsItemProps) => {
54
54
  <Icon name={isOpen ? "UpArrow" : "DownArrow"} size="16" />
55
55
  </S.IconWrapper>
56
56
  )}
57
- <S.IconWrapper onClick={() => setIsDeleted(true)}>
57
+ <S.IconWrapper
58
+ onClick={() => {
59
+ setIsDeleted(true);
60
+ onDelete();
61
+ }}
62
+ >
58
63
  <Icon name="close" size="16" />
59
64
  </S.IconWrapper>
60
65
  </S.ErrorActions>
@@ -80,6 +85,7 @@ const ErrorsItem = (props: IErrorsItemProps) => {
80
85
  interface IErrorsItemProps {
81
86
  error: IHeadingError;
82
87
  onSelectHeading: (id: number) => () => void;
88
+ onDelete: () => void;
83
89
  }
84
90
 
85
91
  export default ErrorsItem;
@@ -1,4 +1,4 @@
1
- import { useState } from "react";
1
+ import { useMemo, useState } from "react";
2
2
 
3
3
  import { Icon } from "@ax/components";
4
4
 
@@ -11,8 +11,18 @@ const ErrorsBanner = (props: IErrorsBannerProps) => {
11
11
  const { errors, onSelectHeading, isOpen, setIsOpen, resetKey } = props;
12
12
 
13
13
  const [isDeleted, setIsDeleted] = useState(false);
14
+ const [deletedErrorIndices, setDeletedErrorIndices] = useState<Set<number>>(new Set());
14
15
 
15
- if (isDeleted) {
16
+ const allErrorsDeleted = useMemo(
17
+ () => deletedErrorIndices.size === errors.length && errors.length > 0,
18
+ [deletedErrorIndices.size, errors.length],
19
+ );
20
+
21
+ const handleErrorDelete = (index: number) => {
22
+ setDeletedErrorIndices((prev) => new Set([...prev, index]));
23
+ };
24
+
25
+ if (isDeleted || allErrorsDeleted) {
16
26
  return <></>;
17
27
  }
18
28
 
@@ -37,8 +47,13 @@ const ErrorsBanner = (props: IErrorsBannerProps) => {
37
47
  Review <strong>suggestions and warnings</strong> to enhance your page's search engine optimization.
38
48
  </S.Description>
39
49
  <S.ErrorListWrapper>
40
- {errors.map((error) => (
41
- <ErrorItem key={`${error.message}-${resetKey}`} error={error} onSelectHeading={onSelectHeading} />
50
+ {errors.map((error, index) => (
51
+ <ErrorItem
52
+ key={`${error.message}-${resetKey}`}
53
+ error={error}
54
+ onSelectHeading={onSelectHeading}
55
+ onDelete={() => handleErrorDelete(index)}
56
+ />
42
57
  ))}
43
58
  </S.ErrorListWrapper>
44
59
  </S.ErrorsContent>
@@ -35,6 +35,7 @@ const HeadingsPreviewModal = (props: IHeadingsPreviewProps) => {
35
35
  setHeadings(headings);
36
36
  } else {
37
37
  setHeadingsFilter("all");
38
+ window.scrollTo(0, 0);
38
39
  }
39
40
  }, [isOpen, getSEOHeadings, setHeadingsFilter]);
40
41
 
@@ -67,12 +67,16 @@ const analyzeHeadings = (headings: HeadingNode[], isFiltering = false): IHeading
67
67
 
68
68
  // 2. Check for incorrect nesting (skipped levels)
69
69
  const nestingErrorIds: number[] = [];
70
+ let lastValidLevel = flatHeadings[0].level;
70
71
  for (let i = 1; i < flatHeadings.length; i++) {
71
- const prevLevel = flatHeadings[i - 1].level;
72
72
  const currLevel = flatHeadings[i].level;
73
73
 
74
- if (currLevel > prevLevel && currLevel - prevLevel > 1) {
74
+ if (currLevel > lastValidLevel + 1) {
75
+ // Heading skips levels - mark as error
75
76
  nestingErrorIds.push(flatHeadings[i].id);
77
+ } else {
78
+ // Correct nesting - update last valid level
79
+ lastValidLevel = currLevel;
76
80
  }
77
81
  }
78
82
  if (nestingErrorIds.length > 0) {
@@ -163,8 +167,8 @@ const parseHeadingsTree = (html: HTMLDivElement): HeadingNode[] => {
163
167
 
164
168
  const style = iframeWindow.getComputedStyle(el);
165
169
 
166
- // 1. Check CSS properties directly on element
167
- if (style.display === "none" || style.visibility === "hidden" || parseFloat(style.opacity) === 0) {
170
+ // 1. Check CSS properties directly on element (only definite cases)
171
+ if (style.display === "none" || style.visibility === "hidden") {
168
172
  return true;
169
173
  }
170
174
 
@@ -175,33 +179,10 @@ const parseHeadingsTree = (html: HTMLDivElement): HeadingNode[] => {
175
179
 
176
180
  // 3. Check dimensions (width or height is 0)
177
181
  const rect = el.getBoundingClientRect();
178
- if (rect.width === 0 || rect.height === 0) {
182
+ if (rect.width === 0 && rect.height === 0) {
179
183
  return true;
180
184
  }
181
185
 
182
- // 4. Check if completely out of viewport
183
- const frameRect = frameObject?.getBoundingClientRect();
184
- if (frameRect) {
185
- if (
186
- rect.bottom < frameRect.top ||
187
- rect.top > frameRect.bottom ||
188
- rect.right < frameRect.left ||
189
- rect.left > frameRect.right
190
- ) {
191
- return true;
192
- }
193
- }
194
-
195
- // 5. Check if element is covered by another element (elementFromPoint)
196
- const centerX = rect.left + rect.width / 2;
197
- const centerY = rect.top + rect.height / 2;
198
- if (frameObject && centerX >= 0 && centerY >= 0 && centerX <= window.innerWidth && centerY <= window.innerHeight) {
199
- const topElement = document.elementFromPoint(centerX, centerY);
200
- if (topElement && topElement !== el && !el.contains(topElement)) {
201
- return true;
202
- }
203
- }
204
-
205
186
  return false;
206
187
  };
207
188
 
@@ -230,17 +211,14 @@ const parseHeadingsTree = (html: HTMLDivElement): HeadingNode[] => {
230
211
  }
231
212
  }
232
213
 
233
- // Check if ancestor is in collapsed navigation
214
+ // Check if ancestor is in collapsed navigation (only when explicitly collapsed)
234
215
  const navSelector = "nav, header, [role='navigation'], .nav, .menu, .navbar, .sidebar";
235
216
  if (node.matches(navSelector)) {
236
- const ancestorRect = node.getBoundingClientRect();
237
217
  const nodeHeight = parseFloat(style.height);
238
218
  const nodeMaxHeight = parseFloat(style.maxHeight);
239
219
 
240
- if (
241
- (nodeHeight === 0 || nodeMaxHeight === 0 || style.overflow === "hidden") &&
242
- (ancestorRect.height === 0 || ancestorRect.height < 10)
243
- ) {
220
+ // Only mark as hidden if height is explicitly 0
221
+ if ((nodeHeight === 0 || nodeMaxHeight === 0) && style.overflow === "hidden") {
244
222
  return true;
245
223
  }
246
224
  }
@@ -39,11 +39,22 @@ const KeywordsPreviewModal = (props: IKeywordsPreviewProps) => {
39
39
  const handleDeleteKeyword = (value: string) => {
40
40
  setDeletedKeyword(value);
41
41
  deleteKeyword(value);
42
+ setKeywordsFilter([]);
43
+ };
44
+
45
+ const handleCloseModal = () => {
46
+ setKeywordsFilter([]);
47
+ toggleModal();
48
+ };
49
+
50
+ const handleAddKeywords = (value: string[]) => {
51
+ setKeywordsFilter([]);
52
+ addKeywords(value);
42
53
  };
43
54
 
44
55
  return (
45
56
  <S.Wrapper>
46
- <FloatingPanel title="Keywords" toggleModal={toggleModal} closeOnOutsideClick={false} isOpen={isOpen} width={358}>
57
+ <FloatingPanel title="Keywords" toggleModal={handleCloseModal} closeOnOutsideClick={false} isOpen={isOpen} width={358}>
47
58
  {isOpen && (
48
59
  <S.KeywordsWrapper>
49
60
  {keywordsFilter.length > 0 && (
@@ -79,7 +90,7 @@ const KeywordsPreviewModal = (props: IKeywordsPreviewProps) => {
79
90
  </S.KeywordsWrapper>
80
91
  )}
81
92
  </FloatingPanel>
82
- <AddKeywordsModal isOpen={isAddOpen} toggleModal={toggleAddModal} addNewKeyword={addKeywords} />
93
+ <AddKeywordsModal isOpen={isAddOpen} toggleModal={toggleAddModal} addNewKeyword={handleAddKeywords} />
83
94
  {isVisible && <Toast message={toastState} setIsVisible={setIsVisible} />}
84
95
  </S.Wrapper>
85
96
  );
@@ -81,6 +81,7 @@ const Modal = (props: IModalProps): JSX.Element | null => {
81
81
  <S.ModalOverlay isChild={isChild} />
82
82
  <S.ModalWrapper
83
83
  data-testid="modal-wrapper"
84
+ data-is-open={isOpen}
84
85
  isChild={isChild}
85
86
  role="dialog"
86
87
  aria-modal="true"
@@ -11,18 +11,27 @@ const ResizePanel = (props: IResizePanelProps): JSX.Element => {
11
11
 
12
12
  const [rwidth, setRwidth] = useState(MIN_WIDTH);
13
13
  const rightPanelRef = useRef<HTMLDivElement>(null);
14
+ const containerRef = useRef<HTMLDivElement>(null);
14
15
 
15
16
  const calculateFixedPanelMinWidth = (currentWidth: number) => {
16
- const minWidth = 1280 - currentWidth - 32;
17
- return minWidth < 500 ? "500px" : `${minWidth}px`;
17
+ const containerWidth = containerRef.current?.offsetWidth ?? 1280;
18
+ const minWidth = Math.max(500, containerWidth - currentWidth - 32);
19
+ return `${minWidth}px`;
18
20
  };
19
21
 
20
22
  useEffect(() => {
21
- requestAnimationFrame(() => {
22
- if (rightPanelRef.current) {
23
- setRwidth(rightPanelRef.current.offsetWidth);
24
- }
25
- });
23
+ const updateWidth = () => {
24
+ requestAnimationFrame(() => {
25
+ if (rightPanelRef.current) {
26
+ setRwidth(rightPanelRef.current.offsetWidth);
27
+ }
28
+ });
29
+ };
30
+
31
+ updateWidth();
32
+
33
+ window.addEventListener("resize", updateWidth);
34
+ return () => window.removeEventListener("resize", updateWidth);
26
35
  }, []);
27
36
 
28
37
  const resize = (value: number) => {
@@ -30,7 +39,7 @@ const ResizePanel = (props: IResizePanelProps): JSX.Element => {
30
39
  };
31
40
 
32
41
  return (
33
- <>
42
+ <div ref={containerRef} style={{ display: "flex", width: "100%", height: "100%" }}>
34
43
  <S.LeftPanel data-testid="left-panel">
35
44
  {fixed ? (
36
45
  <S.FixedPanel
@@ -48,7 +57,7 @@ const ResizePanel = (props: IResizePanelProps): JSX.Element => {
48
57
  <S.RightPanel ref={rightPanelRef} data-testid="right-panel" style={{ width: rwidth ? `${rwidth}px` : "auto" }}>
49
58
  {rightPanel}
50
59
  </S.RightPanel>
51
- </>
60
+ </div>
52
61
  );
53
62
  };
54
63
 
@@ -3,7 +3,7 @@ import styled from "styled-components";
3
3
  const LeftPanel = styled.section`
4
4
  position: relative;
5
5
  flex-grow: 1;
6
- min-width: 500px;
6
+ min-width: 620px;
7
7
  `;
8
8
 
9
9
  const RightPanel = styled.section`
@@ -12,7 +12,7 @@ const RightPanel = styled.section`
12
12
  padding: ${(p) => `0 ${p.theme.spacing.m} ${p.theme.spacing.m} ${p.theme.spacing.m}`};
13
13
  flex-shrink: 0;
14
14
  min-width: 368px;
15
- max-width: ${(p) => `calc(100% - 500px - ${p.theme.spacing.m})`};
15
+ max-width: ${(p) => `calc(100% - 620px - ${p.theme.spacing.m})`};
16
16
  flex-direction: column;
17
17
  `;
18
18
 
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useRef, useCallback } from "react";
1
+ import { useCallback, useEffect, useRef, useState } from "react";
2
2
 
3
3
  const useModal = (initialState?: boolean, bodyBlock = true) => {
4
4
  const [isOpen, setIsOpen] = useState(initialState || false);
@@ -11,17 +11,10 @@ const useModal = (initialState?: boolean, bodyBlock = true) => {
11
11
  if (!document.body.classList.contains("modal-open")) {
12
12
  document.body.classList.add("modal-open");
13
13
  }
14
- return () => {
15
- // Solo eliminar si no hay otros modales abiertos
16
- const modals = document.querySelectorAll('[data-testid="modal-wrapper"]');
17
- if (modals.length <= 1) {
18
- document.body.classList.remove("modal-open");
19
- }
20
- };
21
- } else if (!bodyBlock || !isOpen) {
22
- // Solo eliminar si no hay modales abiertos
23
- const modals = document.querySelectorAll('[data-testid="modal-wrapper"]');
24
- if (modals.length === 0) {
14
+ } else if (!isOpen && bodyBlock) {
15
+ // Solo remover si no hay otros modales o FloatingPanels abiertos
16
+ const hasOpenModals = document.querySelectorAll('[data-is-open="true"]').length > 0;
17
+ if (!hasOpenModals) {
25
18
  document.body.classList.remove("modal-open");
26
19
  }
27
20
  }
@@ -70,18 +63,12 @@ const useModals = <T extends string>(modalKeys: readonly T[], bodyBlock = true)
70
63
  if (!document.body.classList.contains("modal-open")) {
71
64
  document.body.classList.add("modal-open");
72
65
  }
73
- return () => {
74
- // Solo eliminar si no hay otros modales abiertos
75
- const modals = document.querySelectorAll('[data-testid="modal-wrapper"]');
76
- if (modals.length <= 1) {
77
- document.body.classList.remove("modal-open");
78
- }
79
- };
80
- }
81
- // Solo eliminar si no hay modales abiertos
82
- const modals = document.querySelectorAll('[data-testid="modal-wrapper"]');
83
- if (modals.length === 0) {
84
- document.body.classList.remove("modal-open");
66
+ } else if (!hasOpenModals) {
67
+ // Solo remover si no hay otros modales o FloatingPanels abiertos
68
+ const hasAnyOpenModals = document.querySelectorAll('[data-is-open="true"]').length > 0;
69
+ if (!hasAnyOpenModals) {
70
+ document.body.classList.remove("modal-open");
71
+ }
85
72
  }
86
73
  }, [openModals, bodyBlock]);
87
74
 
@@ -23,6 +23,18 @@ const isEffectivelyVisible = (el: HTMLElement): boolean => {
23
23
  return true;
24
24
  };
25
25
 
26
+ const isInStickyOrFixed = (el: HTMLElement): boolean => {
27
+ let parent: HTMLElement | null = el.parentElement;
28
+ while (parent && parent !== document.body) {
29
+ const style = window.getComputedStyle(parent);
30
+ if (style.position === "sticky" || style.position === "fixed") {
31
+ return true;
32
+ }
33
+ parent = parent.parentElement;
34
+ }
35
+ return false;
36
+ };
37
+
26
38
  const HeadingsOverlay = ({ headingFilter }: IHeadingsOverlayProps) => {
27
39
  const [boxes, setBoxes] = useState<HeadingBox[]>([]);
28
40
  const rafRef = useRef<number>(0);
@@ -33,14 +45,18 @@ const HeadingsOverlay = ({ headingFilter }: IHeadingsOverlayProps) => {
33
45
  rafRef.current = requestAnimationFrame(() => {
34
46
  const selector = headingFilter || "h1, h2, h3, h4, h5, h6";
35
47
  const headings = Array.from(document.querySelectorAll<HTMLElement>(selector));
36
- const scrollX = window.scrollX;
37
- const scrollY = window.scrollY;
38
48
  const boxes: HeadingBox[] = [];
39
49
  for (let i = 0; i < headings.length; i++) {
40
50
  const el = headings[i];
41
51
  const rect = el.getBoundingClientRect();
42
52
  if (rect.width === 0 && rect.height === 0) continue;
43
53
  if (!isEffectivelyVisible(el)) continue;
54
+
55
+ // Para headings dentro de sticky/fixed, no sumar scroll de página
56
+ const inStickyOrFixed = isInStickyOrFixed(el);
57
+ const scrollX = inStickyOrFixed ? 0 : window.scrollX;
58
+ const scrollY = inStickyOrFixed ? 0 : window.scrollY;
59
+
44
60
  boxes.push({
45
61
  id: el.dataset.griddoid || `heading-${i}`,
46
62
  tag: el.tagName.toLowerCase(),
@@ -55,18 +71,18 @@ const HeadingsOverlay = ({ headingFilter }: IHeadingsOverlayProps) => {
55
71
 
56
72
  update();
57
73
 
74
+ document.fonts.ready.then(update);
75
+
58
76
  window.addEventListener("resize", update);
59
- window.addEventListener("scroll", update, true);
60
77
  document.addEventListener("animationend", update, true);
61
78
  document.addEventListener("transitionend", update, true);
62
79
 
63
80
  const observer = new MutationObserver(update);
64
- observer.observe(document.body, { childList: true, subtree: true, attributes: true });
81
+ observer.observe(document.body, { childList: true, subtree: true });
65
82
 
66
83
  return () => {
67
84
  cancelAnimationFrame(rafRef.current);
68
85
  window.removeEventListener("resize", update);
69
- window.removeEventListener("scroll", update, true);
70
86
  document.removeEventListener("animationend", update, true);
71
87
  document.removeEventListener("transitionend", update, true);
72
88
  observer.disconnect();
@@ -21,8 +21,8 @@ const Label = styled.span<{ $color: string; $above?: boolean }>`
21
21
  p.$above
22
22
  ? `
23
23
  transform: none;
24
- top: -25px;
25
- left: 1px;
24
+ top: -20px;
25
+ left: -2px;
26
26
  `
27
27
  : `
28
28
  transform: rotate(-90deg);
@@ -6,7 +6,10 @@ const addIdsToHeadings = (container: HTMLElement, headingFilter: string | null):
6
6
  const text = heading.textContent?.trim();
7
7
  if (!text) return;
8
8
 
9
- heading.dataset.griddoid = `heading-${index + 1}`;
9
+ const id = `heading-${index + 1}`;
10
+ if (heading.dataset.griddoid !== id) {
11
+ heading.dataset.griddoid = id;
12
+ }
10
13
  });
11
14
  };
12
15