@griddo/ax 11.11.7 → 11.11.8-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/config/jest/componentsMock.js +7 -5
  2. package/package.json +2 -2
  3. package/src/__tests__/components/Browser/Browser.test.tsx +438 -87
  4. package/src/__tests__/components/Browser/Browser.utils.test.ts +55 -0
  5. package/src/__tests__/components/ConfigPanel/ConfigPanel.test.tsx +1 -3
  6. package/src/__tests__/components/Fields/Button/Button.test.tsx +29 -27
  7. package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/ErrorItem.test.tsx +158 -0
  8. package/src/__tests__/components/HeadingsPreviewModal/ErrorsBanner/ErrorsBanner.test.tsx +90 -0
  9. package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.test.tsx +178 -0
  10. package/src/__tests__/components/HeadingsPreviewModal/HeadingsPreviewModal.utils.test.tsx +150 -0
  11. package/src/__tests__/components/KeywordsPreviewModal/KeywordItem/KeywordItem.test.tsx +91 -0
  12. package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.test.tsx +122 -0
  13. package/src/__tests__/components/KeywordsPreviewModal/KeywordsPreviewModal.utils.test.ts +15 -0
  14. package/src/__tests__/components/KeywordsPreviewModal/atoms.test.tsx +101 -0
  15. package/src/__tests__/components/ResizePanel/ResizePanel.test.tsx +1 -1
  16. package/src/__tests__/modules/FramePreview/FramePreview.test.tsx +318 -0
  17. package/src/__tests__/modules/FramePreview/FramePreview.utils.test.ts +242 -0
  18. package/src/__tests__/modules/FramePreview/HeadingsOverlay/HeadingsOverlay.test.tsx +185 -0
  19. package/src/components/Browser/index.tsx +301 -134
  20. package/src/components/Browser/style.tsx +75 -6
  21. package/src/components/Browser/utils.tsx +13 -0
  22. package/src/components/Button/index.tsx +2 -1
  23. package/src/components/ConfigPanel/Form/ConnectedField/PageConnectedField/Field/index.tsx +2 -4
  24. package/src/components/Fields/AsyncSelect/style.tsx +13 -0
  25. package/src/components/Fields/FieldGroup/index.tsx +5 -2
  26. package/src/components/Fields/FieldGroup/style.tsx +32 -7
  27. package/src/components/Fields/HeadingField/index.tsx +2 -2
  28. package/src/components/Fields/HiddenField/style.tsx +1 -1
  29. package/src/components/Fields/NumberField/index.tsx +15 -16
  30. package/src/components/Fields/NumberField/style.tsx +2 -0
  31. package/src/components/Fields/ReferenceField/index.tsx +1 -1
  32. package/src/components/Fields/SEOPreview/index.tsx +36 -0
  33. package/src/components/Fields/SEOPreview/style.tsx +24 -0
  34. package/src/components/Fields/Select/index.tsx +5 -1
  35. package/src/components/Fields/Select/style.tsx +56 -0
  36. package/src/components/Fields/SummaryButton/index.tsx +18 -9
  37. package/src/components/Fields/SummaryButton/style.tsx +1 -2
  38. package/src/components/Fields/TagsField/index.tsx +8 -9
  39. package/src/components/Fields/UrlField/index.tsx +26 -27
  40. package/src/components/Fields/index.tsx +2 -0
  41. package/src/components/FloatingNote/index.tsx +35 -0
  42. package/src/components/FloatingNote/style.tsx +26 -0
  43. package/src/components/FloatingPanel/index.tsx +5 -2
  44. package/src/components/FloatingPanel/style.tsx +2 -1
  45. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/index.tsx +85 -0
  46. package/src/components/HeadingsPreviewModal/ErrorsBanner/ErrorItem/style.tsx +80 -0
  47. package/src/components/HeadingsPreviewModal/ErrorsBanner/index.tsx +57 -0
  48. package/src/components/HeadingsPreviewModal/ErrorsBanner/style.tsx +82 -0
  49. package/src/components/HeadingsPreviewModal/HeadingItem/index.tsx +71 -0
  50. package/src/components/HeadingsPreviewModal/HeadingItem/style.tsx +77 -0
  51. package/src/components/HeadingsPreviewModal/index.tsx +146 -0
  52. package/src/components/HeadingsPreviewModal/style.tsx +82 -0
  53. package/src/components/HeadingsPreviewModal/utils.tsx +257 -0
  54. package/src/components/IconAction/index.tsx +1 -1
  55. package/src/components/KeywordsPreviewModal/KeywordItem/index.tsx +46 -0
  56. package/src/components/KeywordsPreviewModal/KeywordItem/style.tsx +64 -0
  57. package/src/components/KeywordsPreviewModal/atoms.tsx +96 -0
  58. package/src/components/KeywordsPreviewModal/index.tsx +99 -0
  59. package/src/components/KeywordsPreviewModal/style.tsx +87 -0
  60. package/src/components/KeywordsPreviewModal/utils.tsx +22 -0
  61. package/src/components/MainWrapper/AppBar/index.tsx +8 -1
  62. package/src/components/MainWrapper/index.tsx +7 -1
  63. package/src/components/Notification/index.tsx +2 -2
  64. package/src/components/PageFinder/index.tsx +1 -1
  65. package/src/components/ResizePanel/index.tsx +4 -3
  66. package/src/components/ResizePanel/style.tsx +1 -1
  67. package/src/components/SearchField/style.tsx +2 -2
  68. package/src/components/SideModal/index.tsx +2 -1
  69. package/src/components/Tabs/index.tsx +13 -4
  70. package/src/components/Tabs/style.tsx +7 -8
  71. package/src/components/Toast/index.tsx +4 -2
  72. package/src/components/Tooltip/index.tsx +4 -3
  73. package/src/components/index.tsx +8 -0
  74. package/src/forms/fields.tsx +70 -68
  75. package/src/hooks/forms.tsx +22 -1
  76. package/src/hooks/index.tsx +13 -3
  77. package/src/hooks/modals.tsx +103 -15
  78. package/src/hooks/users.tsx +25 -8
  79. package/src/modules/Forms/atoms.tsx +2 -2
  80. package/src/modules/FramePreview/HeadingsOverlay/index.tsx +113 -0
  81. package/src/modules/FramePreview/HeadingsOverlay/style.tsx +24 -0
  82. package/src/modules/FramePreview/index.tsx +55 -16
  83. package/src/modules/FramePreview/style.tsx +34 -2
  84. package/src/modules/FramePreview/utils.tsx +140 -0
  85. package/src/modules/GlobalEditor/Editor/index.tsx +37 -3
  86. package/src/modules/GlobalEditor/PageBrowser/index.tsx +19 -2
  87. package/src/modules/GlobalEditor/Preview/index.tsx +0 -2
  88. package/src/modules/GlobalEditor/Preview/style.tsx +1 -1
  89. package/src/modules/GlobalEditor/index.tsx +119 -57
  90. package/src/modules/PageEditor/Editor/index.tsx +33 -2
  91. package/src/modules/PageEditor/PageBrowser/index.tsx +20 -2
  92. package/src/modules/PageEditor/Preview/index.tsx +0 -2
  93. package/src/modules/PageEditor/Preview/style.tsx +1 -1
  94. package/src/modules/PageEditor/atoms.tsx +1 -1
  95. package/src/modules/PageEditor/index.tsx +130 -66
  96. package/src/modules/PublicPreview/index.tsx +5 -2
  97. package/src/schemas/pages/GlobalPage.ts +87 -70
  98. package/src/schemas/pages/Page.ts +87 -70
  99. package/src/types/index.tsx +12 -0
@@ -0,0 +1,185 @@
1
+ import { ThemeProvider } from "styled-components";
2
+ import { render, cleanup, screen, act } from "@testing-library/react";
3
+ import { parseTheme } from "@ax/helpers";
4
+ import "@testing-library/jest-dom";
5
+
6
+ import HeadingsOverlay from "@ax/modules/FramePreview/HeadingsOverlay";
7
+ import globalTheme from "@ax/themes/theme.json";
8
+
9
+ const renderWithTheme = (ui: React.ReactElement) =>
10
+ render(<ThemeProvider theme={parseTheme(globalTheme)}>{ui}</ThemeProvider>);
11
+
12
+ beforeEach(() => {
13
+ jest.spyOn(window, "requestAnimationFrame").mockImplementation((cb) => {
14
+ cb(0);
15
+ return 0;
16
+ });
17
+ });
18
+
19
+ afterEach(() => {
20
+ cleanup();
21
+ for (const el of document.querySelectorAll("h1, h2, h3, h4, h5, h6")) el.remove();
22
+ jest.restoreAllMocks();
23
+ });
24
+
25
+ describe("HeadingsOverlay rendering", () => {
26
+ it("should render without boxes when no headings exist in the document", () => {
27
+ renderWithTheme(<HeadingsOverlay headingFilter={null} />);
28
+
29
+ expect(screen.queryByText("H1")).toBeNull();
30
+ expect(screen.queryByText("H2")).toBeNull();
31
+ });
32
+
33
+ it("should render a labeled box for an h1 element with a non-zero bounding rect", async () => {
34
+ const h1 = document.createElement("h1");
35
+ h1.dataset.griddoid = "heading-1";
36
+ document.body.appendChild(h1);
37
+ jest.spyOn(h1, "getBoundingClientRect").mockReturnValue({
38
+ top: 10,
39
+ left: 10,
40
+ width: 200,
41
+ height: 40,
42
+ bottom: 50,
43
+ right: 210,
44
+ x: 10,
45
+ y: 10,
46
+ toJSON: () => {},
47
+ } as DOMRect);
48
+
49
+ await act(async () => {
50
+ renderWithTheme(<HeadingsOverlay headingFilter={null} />);
51
+ });
52
+
53
+ expect(screen.getByText("H1")).toBeTruthy();
54
+ });
55
+
56
+ it("should render an uppercase label for the heading tag", async () => {
57
+ const h2 = document.createElement("h2");
58
+ h2.dataset.griddoid = "heading-1";
59
+ document.body.appendChild(h2);
60
+ jest.spyOn(h2, "getBoundingClientRect").mockReturnValue({
61
+ top: 10,
62
+ left: 10,
63
+ width: 200,
64
+ height: 40,
65
+ bottom: 50,
66
+ right: 210,
67
+ x: 10,
68
+ y: 10,
69
+ toJSON: () => {},
70
+ } as DOMRect);
71
+
72
+ await act(async () => {
73
+ renderWithTheme(<HeadingsOverlay headingFilter={null} />);
74
+ });
75
+
76
+ expect(screen.getByText("H2")).toBeTruthy();
77
+ });
78
+
79
+ it("should not render boxes for headings with zero bounding rect", () => {
80
+ const h1 = document.createElement("h1");
81
+ document.body.appendChild(h1);
82
+ // JSDOM returns zero rect by default — no mock needed
83
+
84
+ renderWithTheme(<HeadingsOverlay headingFilter={null} />);
85
+
86
+ expect(screen.queryByText("H1")).toBeNull();
87
+ });
88
+
89
+ it("should not render a box for a heading with display:none", async () => {
90
+ const h1 = document.createElement("h1");
91
+ h1.dataset.griddoid = "heading-1";
92
+ document.body.appendChild(h1);
93
+ jest.spyOn(h1, "getBoundingClientRect").mockReturnValue({
94
+ top: 10,
95
+ left: 10,
96
+ width: 200,
97
+ height: 40,
98
+ bottom: 50,
99
+ right: 210,
100
+ x: 10,
101
+ y: 10,
102
+ toJSON: () => {},
103
+ } as DOMRect);
104
+
105
+ const originalGetComputedStyle = window.getComputedStyle.bind(window);
106
+ jest.spyOn(window, "getComputedStyle").mockImplementation((el) => {
107
+ if (el === h1) return { display: "none", visibility: "", opacity: "1" } as CSSStyleDeclaration;
108
+ return originalGetComputedStyle(el);
109
+ });
110
+
111
+ await act(async () => {
112
+ renderWithTheme(<HeadingsOverlay headingFilter={null} />);
113
+ });
114
+
115
+ expect(screen.queryByText("H1")).toBeNull();
116
+ });
117
+
118
+ it("should not render a box for a heading with opacity 0", async () => {
119
+ const h1 = document.createElement("h1");
120
+ h1.dataset.griddoid = "heading-1";
121
+ document.body.appendChild(h1);
122
+ jest.spyOn(h1, "getBoundingClientRect").mockReturnValue({
123
+ top: 10,
124
+ left: 10,
125
+ width: 200,
126
+ height: 40,
127
+ bottom: 50,
128
+ right: 210,
129
+ x: 10,
130
+ y: 10,
131
+ toJSON: () => {},
132
+ } as DOMRect);
133
+
134
+ const originalGetComputedStyle = window.getComputedStyle.bind(window);
135
+ jest.spyOn(window, "getComputedStyle").mockImplementation((el) => {
136
+ if (el === h1) return { display: "block", visibility: "visible", opacity: "0" } as CSSStyleDeclaration;
137
+ return originalGetComputedStyle(el);
138
+ });
139
+
140
+ await act(async () => {
141
+ renderWithTheme(<HeadingsOverlay headingFilter={null} />);
142
+ });
143
+
144
+ expect(screen.queryByText("H1")).toBeNull();
145
+ });
146
+
147
+ it("should only render boxes for headings matching the headingFilter", async () => {
148
+ const h1 = document.createElement("h1");
149
+ h1.dataset.griddoid = "heading-1";
150
+ document.body.appendChild(h1);
151
+ jest.spyOn(h1, "getBoundingClientRect").mockReturnValue({
152
+ top: 10,
153
+ left: 10,
154
+ width: 200,
155
+ height: 40,
156
+ bottom: 50,
157
+ right: 210,
158
+ x: 10,
159
+ y: 10,
160
+ toJSON: () => {},
161
+ } as DOMRect);
162
+
163
+ const h2 = document.createElement("h2");
164
+ h2.dataset.griddoid = "heading-2";
165
+ document.body.appendChild(h2);
166
+ jest.spyOn(h2, "getBoundingClientRect").mockReturnValue({
167
+ top: 60,
168
+ left: 10,
169
+ width: 200,
170
+ height: 40,
171
+ bottom: 100,
172
+ right: 210,
173
+ x: 10,
174
+ y: 60,
175
+ toJSON: () => {},
176
+ } as DOMRect);
177
+
178
+ await act(async () => {
179
+ renderWithTheme(<HeadingsOverlay headingFilter="h1" />);
180
+ });
181
+
182
+ expect(screen.getByText("H1")).toBeTruthy();
183
+ expect(screen.queryByText("H2")).toBeNull();
184
+ });
185
+ });
@@ -1,20 +1,36 @@
1
- import { useEffect, useState } from "react";
1
+ import { useEffect, useMemo, useRef, useState } from "react";
2
2
 
3
3
  import type { IShareData } from "@ax/api";
4
4
  import { shareToken as shareTokenApi } from "@ax/api";
5
- import { PageInfoBanner } from "@ax/components";
5
+ import { BrowserContent, Icon, PageInfoBanner, Select, SharePageModal, Toast, Tooltip } from "@ax/components";
6
6
  import { findByEditorID } from "@ax/forms";
7
- import { DEV_NOW, getShareTokenInfo } from "@ax/helpers";
7
+ import { copyTextToClipboard, DEV_NOW, getShareTokenInfo } from "@ax/helpers";
8
8
  import { useModal, useOnMessageReceivedFromIframe, useToast } from "@ax/hooks";
9
+ import type { HeadingFilter } from "@ax/types";
9
10
 
10
- import BrowserContent from "../BrowserContent";
11
- import Icon from "../Icon";
12
- import SharePageModal from "../SharePageModal";
13
- import Toast from "../Toast";
14
- import Tooltip from "../Tooltip";
11
+ import { calcAutoZoom, calcDefaultResolution } from "./utils";
15
12
 
16
13
  import * as S from "./style";
17
14
 
15
+ const DEFAULT_RESOLUTION = "1280px";
16
+
17
+ const resolutionOptions = [
18
+ { value: "1920px", label: "1920px" },
19
+ { value: "1440px", label: "1440px" },
20
+ { value: "1280px", label: "1280px" },
21
+ { value: "1024px", label: "1024px" },
22
+ { value: "768px", label: "768px" },
23
+ { value: "360px", label: "360px" },
24
+ ];
25
+
26
+ const zoomOptions = [
27
+ { value: "reset", label: "Reset" },
28
+ { value: "100", label: "100%" },
29
+ { value: "75", label: "75%" },
30
+ { value: "50", label: "50%" },
31
+ { value: "25", label: "25%" },
32
+ ];
33
+
18
34
  const Browser = (props: IBrowserProps): JSX.Element => {
19
35
  const {
20
36
  url,
@@ -33,15 +49,27 @@ const Browser = (props: IBrowserProps): JSX.Element => {
33
49
  browserRef,
34
50
  actions,
35
51
  editorType = "page",
52
+ headingFilter,
53
+ keywordsFilter,
54
+ toggleHeadingsPreview,
55
+ toggleKeywordsPreview,
36
56
  } = props;
37
57
 
38
58
  const { id, entity, haveDraftPage } = content;
39
59
  const domain = window.location.origin;
40
- const urlPreview = `${domain}/editor/page-preview?preview=${!!isPreview}&disabled=${!!disabled}&type=${editorType}`;
60
+ const headingFilterParam = !headingFilter || headingFilter === "all" ? "" : `&headingFilter=${headingFilter}`;
61
+ const keywordsFilterParam = !keywordsFilter || !keywordsFilter.length ? "" : `&keywordFilter=${keywordsFilter}`;
41
62
  const isPageEditor = editorType === "page";
42
63
  const isFormEditor = editorType === "form";
64
+ const isHeadingsEditor = editorType === "headings";
65
+ const isKeywordsEditor = editorType === "keywords";
43
66
 
44
- const [resolution, setResolution] = useState("desktop");
67
+ const frameWrapperRef = useRef<HTMLDivElement>(null);
68
+
69
+ const [previewResolution, setPreviewResolution] = useState("desktop");
70
+ const [dimensions, setDimensions] = useState({ resolution: DEFAULT_RESOLUTION, zoom: "100" });
71
+
72
+ const urlPreview = `${domain}/editor/page-preview?preview=${!!isPreview}&disabled=${!!disabled}&type=${editorType}${headingFilterParam}${keywordsFilterParam}`;
45
73
  const { isVisible, toggleToast, setIsVisible, state: toastState } = useToast();
46
74
  const { isOpen: isShareOpen, toggleModal: toggleSharePageModal } = useModal(false, true);
47
75
  const [shareData, setShareData] = useState<IShareData | null>(null);
@@ -53,6 +81,24 @@ const Browser = (props: IBrowserProps): JSX.Element => {
53
81
  useEffect(() => {
54
82
  localStorage.setItem("selectedID", "0");
55
83
  (window as any).browserRef = null;
84
+
85
+ const el = frameWrapperRef.current;
86
+ if (!el) return;
87
+
88
+ let lastWidth = 0;
89
+ const observer = new ResizeObserver(([entry]) => {
90
+ const containerWidth = entry.contentRect.width;
91
+ if (containerWidth > 0 && Math.abs(containerWidth - lastWidth) > 20) {
92
+ lastWidth = containerWidth;
93
+ const resolution = calcDefaultResolution(containerWidth, resolutionOptions);
94
+ const newZoom = calcAutoZoom(containerWidth, resolution);
95
+ setDimensions({ resolution, zoom: newZoom });
96
+ }
97
+ });
98
+
99
+ observer.observe(el);
100
+
101
+ return () => observer.disconnect();
56
102
  }, []);
57
103
 
58
104
  // Fetch share data when in preview mode
@@ -85,141 +131,258 @@ const Browser = (props: IBrowserProps): JSX.Element => {
85
131
  }
86
132
  };
87
133
 
88
- const deleteModuleSelected = (editorID: number) => {
89
- actions?.setSelectedContentAction(0);
90
- actions?.deleteModuleAction?.([editorID]);
134
+ const getWidth = (res: string) => {
135
+ if (!isPreview) return dimensions.resolution;
136
+
137
+ switch (res) {
138
+ case "tablet":
139
+ return "768px";
140
+ case "phone":
141
+ return "425px";
142
+ default:
143
+ return "100%";
144
+ }
91
145
  };
92
146
 
93
- const duplicateModuleSelected = (editorID: number) => {
94
- const duplicatedEditorID = actions?.duplicateModuleAction?.([editorID]);
95
- duplicatedEditorID && actions?.setSelectedContentAction(duplicatedEditorID);
147
+ const copyUrl = () => {
148
+ const sharedUrl = `${domain}/page-preview/${id}/${entity}`;
149
+ copyTextToClipboard(sharedUrl).then(
150
+ () => {
151
+ toggleToast("URL Copied");
152
+ },
153
+ (err) => {
154
+ console.error("Could not copy text: ", err);
155
+ },
156
+ );
96
157
  };
97
158
 
98
- const copyModuleSelected = (editorID: number) => {
99
- const isCopied = actions?.copyModuleAction?.([editorID]);
100
- isCopied && toggleToast("1 module copied to clipboard");
159
+ const moduleActions = useMemo(
160
+ () => ({
161
+ deleteModuleAction: (editorID: number) => {
162
+ actions?.setSelectedContentAction(0);
163
+ actions?.deleteModuleAction?.([editorID]);
164
+ },
165
+ duplicateModuleAction: (editorID: number) => {
166
+ const duplicatedEditorID = actions?.duplicateModuleAction?.([editorID]);
167
+ duplicatedEditorID && actions?.setSelectedContentAction(duplicatedEditorID);
168
+ },
169
+ copyModuleAction: (editorID: number) => {
170
+ const isCopied = actions?.copyModuleAction?.([editorID]);
171
+ isCopied && toggleToast("1 module copied to clipboard");
172
+ },
173
+ }),
174
+ [actions, toggleToast],
175
+ );
176
+
177
+ const maxFittingZoom = parseInt(calcAutoZoom(frameWrapperRef.current?.clientWidth ?? 0, dimensions.resolution));
178
+ const zoomOptionsWithDisabled = useMemo(() => {
179
+ const isCurrentZoomInOptions = zoomOptions.some((opt) => opt.value === dimensions.zoom);
180
+ return [
181
+ ...(!isCurrentZoomInOptions ? [{ value: dimensions.zoom, label: `${dimensions.zoom}%` }] : []),
182
+ ...zoomOptions.map((option) => ({
183
+ ...option,
184
+ isDisabled: option.value === "reset" ? !isCurrentZoomInOptions : parseInt(option.value) > maxFittingZoom,
185
+ })),
186
+ ];
187
+ }, [dimensions.zoom, maxFittingZoom]);
188
+
189
+ const handleResolutionChange = (resolution: string) => {
190
+ const containerWidth = frameWrapperRef.current?.clientWidth ?? 0;
191
+ setDimensions({ resolution, zoom: calcAutoZoom(containerWidth, resolution) });
101
192
  };
102
193
 
103
- const moduleActions = {
104
- deleteModuleAction: deleteModuleSelected,
105
- duplicateModuleAction: duplicateModuleSelected,
106
- copyModuleAction: copyModuleSelected,
194
+ const handleZoomChange = (zoom: string) => {
195
+ if (zoom === "reset") {
196
+ const containerWidth = frameWrapperRef.current?.clientWidth ?? 0;
197
+ setDimensions((prev) => ({ ...prev, zoom: calcAutoZoom(containerWidth, prev.resolution) }));
198
+ } else {
199
+ setDimensions((prev) => ({ ...prev, zoom }));
200
+ }
107
201
  };
108
202
 
203
+ const scaledWidth = !isPreview
204
+ ? Math.floor(parseInt(dimensions.resolution) * (parseInt(dimensions.zoom) / 100))
205
+ : undefined;
206
+
207
+ const isCompact = scaledWidth !== undefined && scaledWidth <= 400;
208
+ const floatingNoteConfig = isHeadingsEditor
209
+ ? { label: "Headings", onClick: toggleHeadingsPreview }
210
+ : isKeywordsEditor
211
+ ? { label: "Keywords", onClick: toggleKeywordsPreview }
212
+ : null;
213
+
109
214
  return (
110
- <S.BrowserWrapper data-testid="browser-wrapper" ref={browserRef}>
111
- {isPageEditor && (
112
- <S.NavBar>
113
- <S.NavUrl>{url}</S.NavUrl>
114
- {isPreview && (
115
- <S.NavActions data-testid="nav-actions-wrapper">
116
- <S.IconWrapper
117
- data-testid="icon-wrapper-browser"
118
- onClick={haveDraftPage ? undefined : toggleSharePageModal}
119
- active={!haveDraftPage}
120
- >
121
- <Tooltip
122
- hideOnClick={!haveDraftPage}
123
- content={
124
- haveDraftPage ? "Only available for drafts. This page already has a public URL." : "Share draft"
125
- }
126
- bottom
215
+ <S.OuterContainer ref={frameWrapperRef}>
216
+ <S.BrowserWrapper data-testid="browser-wrapper" ref={browserRef} scaledWidth={scaledWidth} isPreview={isPreview}>
217
+ {(isPageEditor || isHeadingsEditor || isKeywordsEditor) && (
218
+ <S.NavBar>
219
+ <S.NavUrl>{url}</S.NavUrl>
220
+ {isPreview ? (
221
+ <S.NavActions data-testid="nav-actions-wrapper">
222
+ <S.IconWrapper
223
+ data-testid="icon-wrapper-browser"
224
+ onClick={haveDraftPage ? undefined : toggleSharePageModal}
225
+ active={!haveDraftPage}
226
+ >
227
+ <Tooltip
228
+ hideOnClick={!haveDraftPage}
229
+ content={
230
+ haveDraftPage ? "Only available for drafts. This page already has a public URL." : "Share draft"
231
+ }
232
+ bottom
233
+ >
234
+ <Icon name="share" size="24" />
235
+ </Tooltip>
236
+ </S.IconWrapper>
237
+ <S.IconWrapper active={previewResolution === "desktop"} onClick={() => setPreviewResolution("desktop")}>
238
+ <Tooltip content="Desktop" bottom>
239
+ <Icon name="desktop" size="24" />
240
+ </Tooltip>
241
+ </S.IconWrapper>
242
+ <S.IconWrapper
243
+ data-testid="icon-res-tablet"
244
+ active={previewResolution === "tablet"}
245
+ onClick={() => setPreviewResolution("tablet")}
246
+ >
247
+ <Tooltip content="Tablet" bottom>
248
+ <Icon name="tablet" size="24" />
249
+ </Tooltip>
250
+ </S.IconWrapper>
251
+ <S.IconWrapper
252
+ data-testid="icon-res-phone"
253
+ active={previewResolution === "phone"}
254
+ onClick={() => setPreviewResolution("phone")}
127
255
  >
128
- <Icon name="share" size="24" />
129
- </Tooltip>
130
- </S.IconWrapper>
131
- <S.IconWrapper active={resolution === "desktop"} onClick={() => setResolution("desktop")}>
132
- <Tooltip content="Desktop" bottom>
133
- <Icon name="desktop" size="24" />
134
- </Tooltip>
135
- </S.IconWrapper>
136
- <S.IconWrapper
137
- data-testid="icon-res-tablet"
138
- active={resolution === "tablet"}
139
- onClick={() => setResolution("tablet")}
140
- >
141
- <Tooltip content="Tablet" bottom>
142
- <Icon name="tablet" size="24" />
143
- </Tooltip>
144
- </S.IconWrapper>
145
- <S.IconWrapper
146
- data-testid="icon-res-phone"
147
- active={resolution === "phone"}
148
- onClick={() => setResolution("phone")}
149
- >
150
- <Tooltip content="Mobile" bottom>
151
- <Icon name="phone" size="24" />
152
- </Tooltip>
153
- </S.IconWrapper>
154
- </S.NavActions>
155
- )}
156
- </S.NavBar>
157
- )}
158
-
159
- {showIframe ? (
160
- <S.FrameWrapper hasBorder={isPageEditor} isFormEditor={isFormEditor} data-testid="navbar-iframe-wrapper">
161
- <iframe
162
- title="Preview"
163
- width={getWidth(resolution)}
164
- height="100%"
165
- src={urlPreview}
166
- loading="lazy"
167
- className="frame-content"
256
+ <Tooltip content="Mobile" bottom>
257
+ <Icon name="phone" size="24" />
258
+ </Tooltip>
259
+ </S.IconWrapper>
260
+ </S.NavActions>
261
+ ) : (
262
+ <S.NavActions data-testid="nav-actions-wrapper">
263
+ <S.ResolutionWrapper>
264
+ <S.SelectLabel>Screen</S.SelectLabel>
265
+ <Select
266
+ name="resolution"
267
+ options={resolutionOptions}
268
+ value={dimensions.resolution}
269
+ onChange={handleResolutionChange}
270
+ type="round"
271
+ offSet="30%"
272
+ mandatory
273
+ />
274
+ </S.ResolutionWrapper>
275
+ <S.ZoomWrapper>
276
+ <Select
277
+ name="zoom"
278
+ options={zoomOptionsWithDisabled}
279
+ value={dimensions.zoom}
280
+ onChange={handleZoomChange}
281
+ type="round"
282
+ mandatory
283
+ />
284
+ </S.ZoomWrapper>
285
+ </S.NavActions>
286
+ )}
287
+ </S.NavBar>
288
+ )}
289
+ {showIframe ? (
290
+ <S.ContentWrapper>
291
+ {floatingNoteConfig && (
292
+ <S.StyledFloatingNote
293
+ message={`You are viewing the ${floatingNoteConfig.label} editor${isCompact ? "" : " of this page"}`}
294
+ btnText={isCompact ? "Back" : "Back to Page Editor"}
295
+ onClick={floatingNoteConfig.onClick}
296
+ $compact={isCompact}
297
+ />
298
+ )}
299
+ <S.FrameWrapper
300
+ hasBorder={isPageEditor || isHeadingsEditor || isKeywordsEditor}
301
+ isFormEditor={isFormEditor}
302
+ data-testid="navbar-iframe-wrapper"
303
+ >
304
+ {isPreview ? (
305
+ <iframe
306
+ title="Preview"
307
+ width={getWidth(previewResolution)}
308
+ height="100%"
309
+ src={urlPreview}
310
+ loading="lazy"
311
+ className="frame-content"
312
+ />
313
+ ) : (
314
+ <div
315
+ style={{
316
+ width: `${scaledWidth}px`,
317
+ height: "100%",
318
+ overflow: "hidden",
319
+ flexShrink: 0,
320
+ }}
321
+ >
322
+ <iframe
323
+ title="Preview"
324
+ width={dimensions.resolution}
325
+ src={urlPreview}
326
+ loading="lazy"
327
+ className="frame-content"
328
+ style={{
329
+ display: "block",
330
+ transform: `scale(${parseInt(dimensions.zoom) / 100})`,
331
+ transformOrigin: "0 0",
332
+ height: `${Math.round(100 / (parseInt(dimensions.zoom) / 100))}%`,
333
+ }}
334
+ />
335
+ </div>
336
+ )}
337
+ </S.FrameWrapper>
338
+ </S.ContentWrapper>
339
+ ) : (
340
+ <S.Wrapper data-testid="browser-content-wrapper" className="browser-content">
341
+ <BrowserContent
342
+ cloudinaryName={cloudinaryName}
343
+ theme={theme}
344
+ socials={socials}
345
+ siteLangs={siteLangs}
346
+ selectEditorID={selectEditorID}
347
+ siteID={siteID}
348
+ isPage={isPage}
349
+ content={content}
350
+ header={header}
351
+ footer={footer}
352
+ languageID={content.language}
353
+ pageLanguages={content.pageLanguages}
354
+ moduleActions={moduleActions}
355
+ renderer="editor"
356
+ />
357
+ </S.Wrapper>
358
+ )}
359
+ {isVisible && <Toast message={toastState} setIsVisible={setIsVisible} />}
360
+ {isPreview && shareData && tokenInfo && (
361
+ <PageInfoBanner
362
+ message={
363
+ tokenInfo.tokenHasExpired
364
+ ? `Access to this draft link expired on ${tokenInfo.tokenExpirationDate}. You need to generate a`
365
+ : `This draft link expires on ${tokenInfo.tokenExpirationDate}. ${tokenInfo.tokenCanBeRenewed ? "Renew it" : "Open details"}`
366
+ }
367
+ actionLabel={tokenInfo.tokenHasExpired ? "new one" : "here"}
368
+ icon={tokenInfo.tokenHasExpired ? "hide" : "view"}
369
+ onAction={toggleSharePageModal}
168
370
  />
169
- </S.FrameWrapper>
170
- ) : (
171
- <S.Wrapper
172
- data-testid="browser-content-wrapper"
173
- // biome-ignore lint/suspicious/noAssignInExpressions: TODO: fix this
174
- ref={(ref: any) => ((window as any).browserRef = ref)}
175
- className="browser-content"
176
- >
177
- <BrowserContent
178
- cloudinaryName={cloudinaryName}
179
- theme={theme}
180
- socials={socials}
181
- siteLangs={siteLangs}
182
- selectEditorID={selectEditorID}
183
- siteID={siteID}
184
- isPage={isPage}
185
- content={content}
186
- header={header}
187
- footer={footer}
188
- languageID={content.language}
189
- pageLanguages={content.pageLanguages}
190
- moduleActions={moduleActions}
191
- renderer="editor"
371
+ )}
372
+
373
+ {isPreview && (
374
+ <SharePageModal
375
+ pageTitle={content.title}
376
+ isOpen={isShareOpen}
377
+ hide={toggleSharePageModal}
378
+ pageID={id}
379
+ entity={entity}
380
+ shareData={shareData}
381
+ onShareChange={setShareData}
192
382
  />
193
- </S.Wrapper>
194
- )}
195
-
196
- {isVisible && <Toast message={toastState} setIsVisible={setIsVisible} />}
197
-
198
- {isPreview && shareData && tokenInfo && (
199
- <PageInfoBanner
200
- message={
201
- tokenInfo.tokenHasExpired
202
- ? `Access to this draft link expired on ${tokenInfo.tokenExpirationDate}. You need to generate a`
203
- : `This draft link expires on ${tokenInfo.tokenExpirationDate}. ${tokenInfo.tokenCanBeRenewed ? "Renew it" : "Open details"}`
204
- }
205
- actionLabel={tokenInfo.tokenHasExpired ? "new one" : "here"}
206
- icon={tokenInfo.tokenHasExpired ? "hide" : "view"}
207
- onAction={toggleSharePageModal}
208
- />
209
- )}
210
-
211
- {isPreview && (
212
- <SharePageModal
213
- pageTitle={content.title}
214
- isOpen={isShareOpen}
215
- hide={toggleSharePageModal}
216
- pageID={id}
217
- entity={entity}
218
- shareData={shareData}
219
- onShareChange={setShareData}
220
- />
221
- )}
222
- </S.BrowserWrapper>
383
+ )}
384
+ </S.BrowserWrapper>
385
+ </S.OuterContainer>
223
386
  );
224
387
  };
225
388
 
@@ -248,7 +411,11 @@ export interface IBrowserProps {
248
411
  isPreview?: boolean;
249
412
  showIframe?: boolean;
250
413
  browserRef?: React.RefObject<HTMLDivElement>;
251
- editorType?: "form" | "page";
414
+ editorType?: "form" | "page" | "headings" | "keywords";
415
+ headingFilter?: HeadingFilter;
416
+ keywordsFilter?: string[];
417
+ toggleHeadingsPreview?(): void;
418
+ toggleKeywordsPreview?(): void;
252
419
  actions?: {
253
420
  setSelectedContentAction(editorID: number): void;
254
421
  deleteModuleAction?(editorID: number[]): void;