@griddo/ax 11.12.0 → 11.12.1-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 (100) 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 +294 -149
  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 +22 -22
  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 +148 -0
  52. package/src/components/HeadingsPreviewModal/style.tsx +82 -0
  53. package/src/components/HeadingsPreviewModal/utils.tsx +329 -0
  54. package/src/components/Icon/index.tsx +1 -2
  55. package/src/components/IconAction/index.tsx +1 -1
  56. package/src/components/KeywordsPreviewModal/KeywordItem/index.tsx +46 -0
  57. package/src/components/KeywordsPreviewModal/KeywordItem/style.tsx +64 -0
  58. package/src/components/KeywordsPreviewModal/atoms.tsx +96 -0
  59. package/src/components/KeywordsPreviewModal/index.tsx +99 -0
  60. package/src/components/KeywordsPreviewModal/style.tsx +87 -0
  61. package/src/components/KeywordsPreviewModal/utils.tsx +22 -0
  62. package/src/components/MainWrapper/AppBar/index.tsx +8 -1
  63. package/src/components/MainWrapper/index.tsx +7 -1
  64. package/src/components/Notification/index.tsx +2 -2
  65. package/src/components/PageFinder/index.tsx +1 -1
  66. package/src/components/ResizePanel/index.tsx +4 -3
  67. package/src/components/ResizePanel/style.tsx +1 -1
  68. package/src/components/SearchField/style.tsx +2 -2
  69. package/src/components/SideModal/index.tsx +2 -1
  70. package/src/components/Tabs/index.tsx +13 -4
  71. package/src/components/Tabs/style.tsx +7 -8
  72. package/src/components/Toast/index.tsx +4 -2
  73. package/src/components/Tooltip/index.tsx +4 -3
  74. package/src/components/index.tsx +8 -0
  75. package/src/forms/fields.tsx +70 -68
  76. package/src/hooks/forms.tsx +22 -1
  77. package/src/hooks/index.tsx +13 -3
  78. package/src/hooks/modals.tsx +103 -15
  79. package/src/hooks/users.tsx +25 -8
  80. package/src/modules/Forms/atoms.tsx +2 -2
  81. package/src/modules/FramePreview/HeadingsOverlay/index.tsx +116 -0
  82. package/src/modules/FramePreview/HeadingsOverlay/style.tsx +34 -0
  83. package/src/modules/FramePreview/index.tsx +55 -16
  84. package/src/modules/FramePreview/style.tsx +34 -2
  85. package/src/modules/FramePreview/utils.tsx +140 -0
  86. package/src/modules/GlobalEditor/Editor/index.tsx +37 -3
  87. package/src/modules/GlobalEditor/PageBrowser/index.tsx +19 -2
  88. package/src/modules/GlobalEditor/Preview/index.tsx +0 -2
  89. package/src/modules/GlobalEditor/Preview/style.tsx +1 -1
  90. package/src/modules/GlobalEditor/index.tsx +119 -57
  91. package/src/modules/PageEditor/Editor/index.tsx +33 -2
  92. package/src/modules/PageEditor/PageBrowser/index.tsx +20 -2
  93. package/src/modules/PageEditor/Preview/index.tsx +0 -2
  94. package/src/modules/PageEditor/Preview/style.tsx +1 -1
  95. package/src/modules/PageEditor/atoms.tsx +1 -1
  96. package/src/modules/PageEditor/index.tsx +130 -66
  97. package/src/modules/PublicPreview/index.tsx +5 -2
  98. package/src/schemas/pages/GlobalPage.ts +87 -70
  99. package/src/schemas/pages/Page.ts +87 -70
  100. 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 { OcassionalToast } from "@ax/components";
5
+ import { BrowserContent, Icon, OcassionalToast, Select, SharePageModal, Toast, Tooltip } from "@ax/components";
6
6
  import { findByEditorID } from "@ax/forms";
7
7
  import { 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,159 +131,254 @@ 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]);
91
- };
134
+ const getWidth = (res: string) => {
135
+ if (!isPreview) return dimensions.resolution;
92
136
 
93
- const duplicateModuleSelected = (editorID: number) => {
94
- const duplicatedEditorID = actions?.duplicateModuleAction?.([editorID]);
95
- duplicatedEditorID && actions?.setSelectedContentAction(duplicatedEditorID);
137
+ switch (res) {
138
+ case "tablet":
139
+ return "768px";
140
+ case "phone":
141
+ return "425px";
142
+ default:
143
+ return "100%";
144
+ }
96
145
  };
97
146
 
98
- const copyModuleSelected = (editorID: number) => {
99
- const isCopied = actions?.copyModuleAction?.([editorID]);
100
- isCopied && toggleToast("1 module copied to clipboard");
147
+ const moduleActions = useMemo(
148
+ () => ({
149
+ deleteModuleAction: (editorID: number) => {
150
+ actions?.setSelectedContentAction(0);
151
+ actions?.deleteModuleAction?.([editorID]);
152
+ },
153
+ duplicateModuleAction: (editorID: number) => {
154
+ const duplicatedEditorID = actions?.duplicateModuleAction?.([editorID]);
155
+ duplicatedEditorID && actions?.setSelectedContentAction(duplicatedEditorID);
156
+ },
157
+ copyModuleAction: (editorID: number) => {
158
+ const isCopied = actions?.copyModuleAction?.([editorID]);
159
+ isCopied && toggleToast("1 module copied to clipboard");
160
+ },
161
+ }),
162
+ [actions, toggleToast],
163
+ );
164
+
165
+ const maxFittingZoom = parseInt(calcAutoZoom(frameWrapperRef.current?.clientWidth ?? 0, dimensions.resolution));
166
+ const zoomOptionsWithDisabled = useMemo(() => {
167
+ const isCurrentZoomInOptions = zoomOptions.some((opt) => opt.value === dimensions.zoom);
168
+ return [
169
+ ...(!isCurrentZoomInOptions ? [{ value: dimensions.zoom, label: `${dimensions.zoom}%` }] : []),
170
+ ...zoomOptions.map((option) => ({
171
+ ...option,
172
+ isDisabled: option.value === "reset" ? !isCurrentZoomInOptions : parseInt(option.value) > maxFittingZoom,
173
+ })),
174
+ ];
175
+ }, [dimensions.zoom, maxFittingZoom]);
176
+
177
+ const handleResolutionChange = (resolution: string) => {
178
+ const containerWidth = frameWrapperRef.current?.clientWidth ?? 0;
179
+ setDimensions({ resolution, zoom: calcAutoZoom(containerWidth, resolution) });
101
180
  };
102
181
 
103
- const moduleActions = {
104
- deleteModuleAction: deleteModuleSelected,
105
- duplicateModuleAction: duplicateModuleSelected,
106
- copyModuleAction: copyModuleSelected,
182
+ const handleZoomChange = (zoom: string) => {
183
+ if (zoom === "reset") {
184
+ const containerWidth = frameWrapperRef.current?.clientWidth ?? 0;
185
+ setDimensions((prev) => ({ ...prev, zoom: calcAutoZoom(containerWidth, prev.resolution) }));
186
+ } else {
187
+ setDimensions((prev) => ({ ...prev, zoom }));
188
+ }
107
189
  };
108
190
 
191
+ const scaledWidth = !isPreview
192
+ ? Math.floor(parseInt(dimensions.resolution) * (parseInt(dimensions.zoom) / 100))
193
+ : undefined;
194
+
195
+ const isCompact = scaledWidth !== undefined && scaledWidth <= 400;
196
+ const floatingNoteConfig = isHeadingsEditor
197
+ ? { label: "Headings", onClick: toggleHeadingsPreview }
198
+ : isKeywordsEditor
199
+ ? { label: "Keywords", onClick: toggleKeywordsPreview }
200
+ : null;
201
+
109
202
  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
203
+ <S.OuterContainer ref={frameWrapperRef}>
204
+ <S.BrowserWrapper data-testid="browser-wrapper" ref={browserRef} scaledWidth={scaledWidth} isPreview={isPreview}>
205
+ {(isPageEditor || isHeadingsEditor || isKeywordsEditor) && (
206
+ <S.NavBar>
207
+ <S.NavUrl>{url}</S.NavUrl>
208
+ {isPreview ? (
209
+ <S.NavActions data-testid="nav-actions-wrapper">
210
+ <S.IconWrapper
211
+ data-testid="icon-wrapper-browser"
212
+ onClick={haveDraftPage ? undefined : toggleSharePageModal}
213
+ active={!haveDraftPage}
127
214
  >
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"
215
+ <Tooltip
216
+ hideOnClick={!haveDraftPage}
217
+ content={
218
+ haveDraftPage ? "Only available for drafts. This page already has a public URL." : "Share draft"
219
+ }
220
+ bottom
221
+ >
222
+ <Icon name="share" size="24" />
223
+ </Tooltip>
224
+ </S.IconWrapper>
225
+ <S.IconWrapper active={previewResolution === "desktop"} onClick={() => setPreviewResolution("desktop")}>
226
+ <Tooltip content="Desktop" bottom>
227
+ <Icon name="desktop" size="24" />
228
+ </Tooltip>
229
+ </S.IconWrapper>
230
+ <S.IconWrapper
231
+ data-testid="icon-res-tablet"
232
+ active={previewResolution === "tablet"}
233
+ onClick={() => setPreviewResolution("tablet")}
234
+ >
235
+ <Tooltip content="Tablet" bottom>
236
+ <Icon name="tablet" size="24" />
237
+ </Tooltip>
238
+ </S.IconWrapper>
239
+ <S.IconWrapper
240
+ data-testid="icon-res-phone"
241
+ active={previewResolution === "phone"}
242
+ onClick={() => setPreviewResolution("phone")}
243
+ >
244
+ <Tooltip content="Mobile" bottom>
245
+ <Icon name="phone" size="24" />
246
+ </Tooltip>
247
+ </S.IconWrapper>
248
+ </S.NavActions>
249
+ ) : (
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>
274
+ )}
275
+ </S.NavBar>
276
+ )}
277
+ {showIframe ? (
278
+ <S.ContentWrapper>
279
+ {floatingNoteConfig && (
280
+ <S.StyledFloatingNote
281
+ message={`You are viewing the ${floatingNoteConfig.label} editor${isCompact ? "" : " of this page"}`}
282
+ btnText={isCompact ? "Back" : "Back to Page Editor"}
283
+ onClick={floatingNoteConfig.onClick}
284
+ $compact={isCompact}
285
+ />
286
+ )}
287
+ <S.FrameWrapper
288
+ hasBorder={isPageEditor || isHeadingsEditor || isKeywordsEditor}
289
+ isFormEditor={isFormEditor}
290
+ data-testid="navbar-iframe-wrapper"
291
+ >
292
+ {isPreview ? (
293
+ <iframe
294
+ title="Preview"
295
+ width={getWidth(previewResolution)}
296
+ height="100%"
297
+ src={urlPreview}
298
+ loading="lazy"
299
+ className="frame-content"
300
+ />
301
+ ) : (
302
+ <div
303
+ style={{
304
+ width: `${scaledWidth}px`,
305
+ height: "100%",
306
+ overflow: "hidden",
307
+ flexShrink: 0,
308
+ }}
309
+ >
310
+ <iframe
311
+ title="Preview"
312
+ width={dimensions.resolution}
313
+ src={urlPreview}
314
+ loading="lazy"
315
+ className="frame-content"
316
+ style={{
317
+ display: "block",
318
+ transform: `scale(${parseInt(dimensions.zoom) / 100})`,
319
+ transformOrigin: "0 0",
320
+ height: `${Math.round(100 / (parseInt(dimensions.zoom) / 100))}%`,
321
+ }}
322
+ />
323
+ </div>
324
+ )}
325
+ </S.FrameWrapper>
326
+ </S.ContentWrapper>
327
+ ) : (
328
+ <S.Wrapper data-testid="browser-content-wrapper" className="browser-content">
329
+ <BrowserContent
330
+ cloudinaryName={cloudinaryName}
331
+ theme={theme}
332
+ socials={socials}
333
+ siteLangs={siteLangs}
334
+ selectEditorID={selectEditorID}
335
+ siteID={siteID}
336
+ isPage={isPage}
337
+ content={content}
338
+ header={header}
339
+ footer={footer}
340
+ languageID={content.language}
341
+ pageLanguages={content.pageLanguages}
342
+ moduleActions={moduleActions}
343
+ renderer="editor"
344
+ />
345
+ </S.Wrapper>
346
+ )}
347
+ {isVisible && <Toast message={toastState} setIsVisible={setIsVisible} />}
348
+ {isPreview && shareData && tokenInfo && (
349
+ <OcassionalToast
350
+ message={
351
+ <>
352
+ {tokenInfo.tokenHasExpired
353
+ ? `Access to this draft link expired on ${tokenInfo.tokenExpirationDate}. You need to generate a `
354
+ : `This draft link expires on ${tokenInfo.tokenExpirationDate}. ${tokenInfo.tokenCanBeRenewed ? "Renew it" : "Open details"}${" "}`}
355
+ <button type="button" onClick={toggleSharePageModal}>
356
+ <strong style={{ textDecoration: "underline" }}>
357
+ {tokenInfo.tokenHasExpired ? "new one" : "here"}
358
+ </strong>
359
+ </button>
360
+ </>
361
+ }
362
+ icon={tokenInfo.tokenHasExpired ? "hide" : "view"}
168
363
  />
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"
364
+ )}
365
+
366
+ {isPreview && (
367
+ <SharePageModal
368
+ pageTitle={content.title}
369
+ isOpen={isShareOpen}
370
+ hide={toggleSharePageModal}
371
+ pageID={id}
372
+ entity={entity}
373
+ shareData={shareData}
374
+ onShareChange={setShareData}
192
375
  />
193
- </S.Wrapper>
194
- )}
195
-
196
- {isVisible && <Toast message={toastState} setIsVisible={setIsVisible} />}
197
-
198
- {isPreview && shareData && tokenInfo && (
199
- <OcassionalToast
200
- message={
201
- <>
202
- {tokenInfo.tokenHasExpired
203
- ? `Access to this draft link expired on ${tokenInfo.tokenExpirationDate}. You need to generate a `
204
- : `This draft link expires on ${tokenInfo.tokenExpirationDate}. ${tokenInfo.tokenCanBeRenewed ? "Renew it" : "Open details"}${" "}`}
205
- <button type="button" onClick={toggleSharePageModal}>
206
- <strong style={{ textDecoration: "underline" }}>
207
- {tokenInfo.tokenHasExpired ? "new one" : "here"}
208
- </strong>
209
- </button>
210
- </>
211
- }
212
- icon={tokenInfo.tokenHasExpired ? "hide" : "view"}
213
- />
214
- )}
215
-
216
- {isPreview && (
217
- <SharePageModal
218
- pageTitle={content.title}
219
- isOpen={isShareOpen}
220
- hide={toggleSharePageModal}
221
- pageID={id}
222
- entity={entity}
223
- shareData={shareData}
224
- onShareChange={setShareData}
225
- />
226
- )}
227
- </S.BrowserWrapper>
376
+ )}
377
+ </S.BrowserWrapper>
378
+ </S.OuterContainer>
228
379
  );
229
380
  };
230
381
 
231
- function getWidth(res: string) {
232
- switch (res) {
233
- case "tablet":
234
- return "768px";
235
- case "phone":
236
- return "425px";
237
- default:
238
- return "100%";
239
- }
240
- }
241
382
  export interface IBrowserProps {
242
383
  content: any;
243
384
  header?: any;
@@ -253,7 +394,11 @@ export interface IBrowserProps {
253
394
  isPreview?: boolean;
254
395
  showIframe?: boolean;
255
396
  browserRef?: React.RefObject<HTMLDivElement>;
256
- editorType?: "form" | "page";
397
+ editorType?: "form" | "page" | "headings" | "keywords";
398
+ headingFilter?: HeadingFilter;
399
+ keywordsFilter?: string[];
400
+ toggleHeadingsPreview?(): void;
401
+ toggleKeywordsPreview?(): void;
257
402
  actions?: {
258
403
  setSelectedContentAction(editorID: number): void;
259
404
  deleteModuleAction?(editorID: number[]): void;