@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
@@ -1,9 +1,9 @@
1
- import React, { memo, useEffect, useState } from "react";
1
+ import { memo, useEffect, useState } from "react";
2
2
 
3
3
  import { IconAction, TextField, FloatingPanel, FieldsBehavior, PageFinder } from "@ax/components";
4
4
  import { useModal } from "@ax/hooks";
5
5
  import { isReqOk } from "@ax/helpers";
6
- import { IPage, IUrlField, Field, ISelectOption } from "@ax/types";
6
+ import type { IPage, IUrlField, Field, ISelectOption } from "@ax/types";
7
7
  import { pages as pagesApi } from "@ax/api";
8
8
  import { findAnchorsFromPage, findAnchorsFromTab, findTabsFromPage } from "./utils";
9
9
 
@@ -29,7 +29,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
29
29
  const [internalPageName, setInternalPageName] = useState(null);
30
30
  const [pageData, setPageData] = useState<IPage | null>(null);
31
31
 
32
- const pageID = value && value.linkTo ? value.linkTo : null;
32
+ const pageID = value?.linkTo ? value.linkTo : null;
33
33
 
34
34
  // biome-ignore lint/correctness/useExhaustiveDependencies: TODO: fix this
35
35
  useEffect(() => {
@@ -67,13 +67,13 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
67
67
 
68
68
  const handleOnClick = () => {
69
69
  toggleModal();
70
- handlePanel && handlePanel(isOpen);
70
+ handlePanel?.(isOpen);
71
71
  };
72
72
 
73
73
  const handleReset = () => {
74
74
  onChange(null);
75
75
  setInternalPageName(null);
76
- resetValidation && resetValidation();
76
+ resetValidation?.();
77
77
  };
78
78
 
79
79
  const handleSetPage = (page: IPage | IPage[]) => {
@@ -85,10 +85,10 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
85
85
  linkTo: selectedPage.id,
86
86
  linkToURL: selectedPage.fullUrl,
87
87
  title: selectedPage.title,
88
- noFollow: !selectedPage.follow ? true : false,
88
+ noFollow: !selectedPage.follow,
89
89
  });
90
- handleValidation && handleValidation(selectedPage.id.toString(), validators);
91
- handlePanel && handlePanel(isOpen);
90
+ handleValidation?.(selectedPage.id.toString(), validators);
91
+ handlePanel?.(isOpen);
92
92
  };
93
93
 
94
94
  const handleChange = (newValue: string) => onChange({ ...value, href: newValue });
@@ -113,7 +113,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
113
113
  };
114
114
 
115
115
  const validator = { format: "fullURL" };
116
- const defensiveHref = value && value.href ? value.href : "";
116
+ const defensiveHref = value?.href ? value.href : "";
117
117
 
118
118
  let field = (
119
119
  <TextField
@@ -140,7 +140,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
140
140
  );
141
141
  }
142
142
 
143
- const titleValue = value && value.title ? value.title : internalPageName ? internalPageName : "";
143
+ const titleValue = value?.title ? value.title : internalPageName ? internalPageName : "";
144
144
  const noFollowValue =
145
145
  value && value.noFollow !== undefined
146
146
  ? value.noFollow
@@ -160,7 +160,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
160
160
  type: "UniqueCheck" as Field,
161
161
  name: "newTab",
162
162
  options: [{ title: "Open in new tab" }],
163
- value: value && value.newTab ? value.newTab : null,
163
+ value: value?.newTab ? value.newTab : null,
164
164
  onChange: handleNewTabChange,
165
165
  },
166
166
  {
@@ -175,7 +175,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
175
175
  return (
176
176
  <S.UrlFieldWrapper data-testid="url-field-wrapper">
177
177
  {field}
178
- {value && value.linkTo && isTabsVisible && (
178
+ {value?.linkTo && isTabsVisible && (
179
179
  <S.AnchorWrapper>
180
180
  <FieldsBehavior
181
181
  title="Tab"
@@ -188,7 +188,7 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
188
188
  />
189
189
  </S.AnchorWrapper>
190
190
  )}
191
- {value && value.linkTo && isVisible && (
191
+ {value?.linkTo && isVisible && (
192
192
  <S.AnchorWrapper>
193
193
  <FieldsBehavior
194
194
  title="Anchor"
@@ -203,20 +203,19 @@ const UrlField = (props: IUrlFieldProps): JSX.Element => {
203
203
  )}
204
204
  {showAdvanced && (
205
205
  <S.AdvancedWrapper>
206
- {advancedFields &&
207
- advancedFields.map((adField) => {
208
- return (
209
- <FieldsBehavior
210
- key={adField.name}
211
- options={adField.options}
212
- name={adField.name}
213
- title={adField.title}
214
- fieldType={adField.type}
215
- value={adField.value}
216
- onChange={adField.onChange}
217
- />
218
- );
219
- })}
206
+ {advancedFields?.map((adField) => {
207
+ return (
208
+ <FieldsBehavior
209
+ key={adField.name}
210
+ options={adField.options}
211
+ name={adField.name}
212
+ title={adField.title}
213
+ fieldType={adField.type}
214
+ value={adField.value}
215
+ onChange={adField.onChange}
216
+ />
217
+ );
218
+ })}
220
219
  </S.AdvancedWrapper>
221
220
  )}
222
221
  <FloatingPanel
@@ -17,6 +17,7 @@ import FormCategorySelect from "./FormCategorySelect";
17
17
  import FormContainer from "./FormContainer";
18
18
  import FormFieldArray from "./FormFieldArray";
19
19
  import HeadingField from "./HeadingField";
20
+ import SEOPreview from "./SEOPreview";
20
21
  import HiddenField from "./HiddenField";
21
22
  import ImageField from "./ImageField";
22
23
  import LinkField from "./LinkField";
@@ -64,6 +65,7 @@ export {
64
65
  FormContainer,
65
66
  FormFieldArray,
66
67
  HeadingField,
68
+ SEOPreview,
67
69
  HiddenField,
68
70
  ImageField,
69
71
  LinkField,
@@ -0,0 +1,35 @@
1
+ import { Button, Icon } from "@ax/components";
2
+
3
+ import * as S from "./style";
4
+
5
+ const FloatingNote = (props: IFloatingNoteProps): JSX.Element => {
6
+ const { message, icon, className, btnText, onClick } = props;
7
+
8
+ return (
9
+ <S.Wrapper data-testid="floating-note-wrapper" className={className}>
10
+ {icon && (
11
+ <S.IconWrapper>
12
+ <Icon name={icon} size="16" />
13
+ </S.IconWrapper>
14
+ )}
15
+ <S.Text data-testid="floating-note-message">{message}</S.Text>
16
+ {btnText && (
17
+ <S.ActionWrapper>
18
+ <Button type="button" buttonStyle="minimal" onClick={onClick}>
19
+ {btnText}
20
+ </Button>
21
+ </S.ActionWrapper>
22
+ )}
23
+ </S.Wrapper>
24
+ );
25
+ };
26
+
27
+ export interface IFloatingNoteProps {
28
+ message: string;
29
+ icon?: string;
30
+ className?: string;
31
+ btnText?: string;
32
+ onClick?: () => void;
33
+ }
34
+
35
+ export default FloatingNote;
@@ -0,0 +1,26 @@
1
+ import styled from "styled-components";
2
+
3
+ const Wrapper = styled.div`
4
+ display: flex;
5
+ background-color: ${(p) => p.theme.color.uiBackground03};
6
+ padding: ${(p) => p.theme.spacing.s};
7
+ border-radius: ${(p) => p.theme.radii.s};
8
+ z-index: 98;
9
+ `;
10
+
11
+ const Text = styled.div`
12
+ ${(p) => p.theme.textStyle.uiS};
13
+ color: ${(p) => p.theme.color.textMediumEmphasis};
14
+ `;
15
+
16
+ const IconWrapper = styled.div`
17
+ width: ${(p) => p.theme.spacing.s};
18
+ height: ${(p) => p.theme.spacing.s};
19
+ margin-right: ${(p) => p.theme.spacing.xs};
20
+ `;
21
+
22
+ const ActionWrapper = styled.div`
23
+ margin-left: auto;
24
+ `;
25
+
26
+ export { Wrapper, Text, IconWrapper, ActionWrapper };
@@ -1,4 +1,4 @@
1
- import React, { memo, useRef } from "react";
1
+ import { memo, useRef } from "react";
2
2
  import { createPortal } from "react-dom";
3
3
 
4
4
  import { useHandleClickOutside } from "@ax/hooks";
@@ -16,12 +16,13 @@ const FloatingPanel = (props: IFloatingPanelProps): JSX.Element | null => {
16
16
  handlePanel,
17
17
  secondary,
18
18
  closeOnOutsideClick = true,
19
+ width,
19
20
  } = props;
20
21
 
21
22
  const node = useRef<HTMLElement>(null);
22
23
 
23
24
  const handleClickOutside = (e: any) => {
24
- if ((node.current && node.current.contains(e.target)) || isOpenedSecond || !closeOnOutsideClick) {
25
+ if (node.current?.contains(e.target) || isOpenedSecond || !closeOnOutsideClick) {
25
26
  return;
26
27
  }
27
28
  toggleModal();
@@ -46,6 +47,7 @@ const FloatingPanel = (props: IFloatingPanelProps): JSX.Element | null => {
46
47
  isOpen={isOpen}
47
48
  isOpenedSecond={isOpenedSecond}
48
49
  secondary={secondary}
50
+ width={width}
49
51
  >
50
52
  <S.Header>
51
53
  <S.Title>{title}</S.Title>
@@ -70,6 +72,7 @@ export interface IFloatingPanelProps {
70
72
  handlePanel?: (value: boolean) => void;
71
73
  secondary?: boolean;
72
74
  closeOnOutsideClick?: boolean;
75
+ width?: number;
73
76
  }
74
77
 
75
78
  export default memo(FloatingPanel);
@@ -17,12 +17,13 @@ export const Wrapper = styled.section<{
17
17
  isOpen: boolean;
18
18
  isOpenedSecond: boolean | undefined;
19
19
  secondary?: boolean;
20
+ width?: number;
20
21
  }>`
21
22
  position: fixed;
22
23
  right: 0;
23
24
  top: 0;
24
25
  z-index: 1200;
25
- width: calc(${(p) => p.theme.spacing.xl} * 6);
26
+ width: ${(p) => (p.width ? `${p.width}px` : `calc(${p.theme.spacing.xl} * 6)`)};
26
27
  height: 100vh;
27
28
  background: ${(p) => p.theme.colors.uiBackground01};
28
29
  box-shadow: ${(p) => (p.secondary || !p.isOpen ? `none` : p.theme.shadow.rightPanel)};
@@ -0,0 +1,85 @@
1
+ import { useState } from "react";
2
+
3
+ import { Icon, Tooltip } from "@ax/components";
4
+
5
+ import type { IHeadingError } from "../../utils";
6
+
7
+ import * as S from "./style";
8
+
9
+ const ErrorsItem = (props: IErrorsItemProps) => {
10
+ const { error, onSelectHeading } = props;
11
+ const { message, description, headingIds } = error;
12
+
13
+ const [isOpen, setIsOpen] = useState(false);
14
+ const [isDeleted, setIsDeleted] = useState(false);
15
+ const [currentIndex, setCurrentIndex] = useState<number | null>(null);
16
+
17
+ const handlePrevious = () => {
18
+ if (!currentIndex) return;
19
+ const newIndex = currentIndex - 1;
20
+ setCurrentIndex(newIndex);
21
+ onSelectHeading(headingIds[newIndex])();
22
+ };
23
+
24
+ const handleNext = () => {
25
+ const newIndex = currentIndex === null ? 0 : currentIndex + 1;
26
+ if (newIndex > headingIds.length - 1) return;
27
+ setCurrentIndex(newIndex);
28
+ onSelectHeading(headingIds[newIndex])();
29
+ };
30
+
31
+ const handleOpen = () => {
32
+ setIsOpen(!isOpen);
33
+ setCurrentIndex(0);
34
+ onSelectHeading(headingIds[0])();
35
+ };
36
+
37
+ const errorText =
38
+ headingIds.length > 1 && currentIndex !== null
39
+ ? `${currentIndex + 1} of ${headingIds.length} headings`
40
+ : `${headingIds.length} headings`;
41
+
42
+ if (isDeleted) {
43
+ return <></>;
44
+ }
45
+
46
+ return (
47
+ <S.ErrorItem>
48
+ <Tooltip content={description}>
49
+ <S.ErrorHeader>
50
+ <S.ErrorMessage>{message}</S.ErrorMessage>
51
+ <S.ErrorActions>
52
+ {headingIds.length > 0 && (
53
+ <S.IconWrapper onClick={handleOpen}>
54
+ <Icon name={isOpen ? "UpArrow" : "DownArrow"} size="16" />
55
+ </S.IconWrapper>
56
+ )}
57
+ <S.IconWrapper onClick={() => setIsDeleted(true)}>
58
+ <Icon name="close" size="16" />
59
+ </S.IconWrapper>
60
+ </S.ErrorActions>
61
+ </S.ErrorHeader>
62
+ </Tooltip>
63
+ <S.ErrorContentWrapper isOpen={isOpen}>
64
+ <S.ErrorContent>
65
+ <S.ErrorContentActions>
66
+ <S.IconWrapper onClick={handlePrevious} isDisabled={!currentIndex}>
67
+ <Icon name="UpArrow" size="16" />
68
+ </S.IconWrapper>
69
+ <S.IconWrapper onClick={handleNext} isDisabled={currentIndex === headingIds.length - 1}>
70
+ <Icon name="DownArrow" size="16" />
71
+ </S.IconWrapper>
72
+ </S.ErrorContentActions>
73
+ <S.ErrorContentText>{errorText}</S.ErrorContentText>
74
+ </S.ErrorContent>
75
+ </S.ErrorContentWrapper>
76
+ </S.ErrorItem>
77
+ );
78
+ };
79
+
80
+ interface IErrorsItemProps {
81
+ error: IHeadingError;
82
+ onSelectHeading: (id: number) => () => void;
83
+ }
84
+
85
+ export default ErrorsItem;
@@ -0,0 +1,80 @@
1
+ import styled from "styled-components";
2
+
3
+ const ErrorItem = styled.li`
4
+ display: flex;
5
+ background-color: ${(p) => p.theme.colors.uiBackground02};
6
+ border: ${(p) => `1px solid ${p.theme.colors.uiLine}`};
7
+ padding: ${(p) => p.theme.spacing.xs};
8
+ margin-bottom: ${(p) => p.theme.spacing.xxs};
9
+ border-radius: ${(p) => p.theme.radii.s};
10
+ flex-direction: column;
11
+
12
+ &:last-child {
13
+ margin-bottom: 0;
14
+ }
15
+ `;
16
+
17
+ const ErrorHeader = styled.div`
18
+ display: flex;
19
+ width: 100%;
20
+ `;
21
+
22
+ const ErrorMessage = styled.div`
23
+ ${(p) => p.theme.textStyle.uiS};
24
+ color: ${(p) => p.theme.colors.textHighEmphasis};
25
+ font-weight: 400;
26
+ `;
27
+
28
+ const ErrorActions = styled.div`
29
+ display: flex;
30
+ margin-left: auto;
31
+ align-items: start;
32
+ gap: ${(p) => p.theme.spacing.xxs};
33
+ padding-left: ${(p) => p.theme.spacing.xs};
34
+ `;
35
+
36
+ const IconWrapper = styled.button<{ isDisabled?: boolean }>`
37
+ width: ${(p) => p.theme.spacing.s};
38
+ height: ${(p) => p.theme.spacing.s};
39
+ svg {
40
+ path {
41
+ fill: ${(p) => (p.isDisabled ? p.theme.color.interactiveDisabled : p.theme.color.interactive01)};
42
+ }
43
+ }
44
+ `;
45
+
46
+ const ErrorContentWrapper = styled.div<{ isOpen: boolean }>`
47
+ width: 100%;
48
+ overflow: hidden;
49
+ height: ${(p) => (p.isOpen ? "auto" : "0")};
50
+ transition: height 1s ease-in-out;
51
+ `;
52
+
53
+ const ErrorContent = styled.div`
54
+ display: flex;
55
+ width: 100%;
56
+ margin-top: ${(p) => p.theme.spacing.xxs};
57
+ `;
58
+
59
+ const ErrorContentActions = styled.div`
60
+ display: flex;
61
+ gap: ${(p) => p.theme.spacing.xxs};
62
+ `;
63
+
64
+ const ErrorContentText = styled.div`
65
+ ${(p) => p.theme.textStyle.uiXS};
66
+ color: ${(p) => p.theme.colors.textMediumEmphasis};
67
+ margin-left: ${(p) => p.theme.spacing.s};
68
+ `;
69
+
70
+ export {
71
+ ErrorItem,
72
+ ErrorHeader,
73
+ ErrorMessage,
74
+ ErrorActions,
75
+ IconWrapper,
76
+ ErrorContentWrapper,
77
+ ErrorContent,
78
+ ErrorContentActions,
79
+ ErrorContentText,
80
+ };
@@ -0,0 +1,57 @@
1
+ import { useState } from "react";
2
+
3
+ import { Icon } from "@ax/components";
4
+
5
+ import type { IHeadingError } from "../utils";
6
+ import ErrorItem from "./ErrorItem";
7
+
8
+ import * as S from "./style";
9
+
10
+ const ErrorsBanner = (props: IErrorsBannerProps) => {
11
+ const { errors, onSelectHeading, isOpen, setIsOpen, resetKey } = props;
12
+
13
+ const [isDeleted, setIsDeleted] = useState(false);
14
+
15
+ if (isDeleted) {
16
+ return <></>;
17
+ }
18
+
19
+ return (
20
+ <S.ErrorsWrapper>
21
+ <S.ErrorsHeader>
22
+ <S.WarningWrapper>
23
+ <Icon name="warning" size="16" />
24
+ </S.WarningWrapper>
25
+ <S.HeaderText>SEO Alerts</S.HeaderText>
26
+ <S.HeaderActions>
27
+ <S.ToggleWrapper onClick={() => setIsOpen(!isOpen)}>
28
+ <Icon name={isOpen ? "UpArrow" : "DownArrow"} size="24" />
29
+ </S.ToggleWrapper>
30
+ <S.IconWrapper onClick={() => setIsDeleted(true)}>
31
+ <Icon name="close" size="16" />
32
+ </S.IconWrapper>
33
+ </S.HeaderActions>
34
+ </S.ErrorsHeader>
35
+ <S.ErrorsContent isOpen={isOpen}>
36
+ <S.Description>
37
+ Review <strong>suggestions and warnings</strong> to enhance your page's search engine optimization.
38
+ </S.Description>
39
+ <S.ErrorListWrapper>
40
+ {errors.map((error) => (
41
+ <ErrorItem key={`${error.message}-${resetKey}`} error={error} onSelectHeading={onSelectHeading} />
42
+ ))}
43
+ </S.ErrorListWrapper>
44
+ </S.ErrorsContent>
45
+ </S.ErrorsWrapper>
46
+ );
47
+ };
48
+
49
+ interface IErrorsBannerProps {
50
+ errors: IHeadingError[];
51
+ onSelectHeading: (id: number) => () => void;
52
+ isOpen: boolean;
53
+ setIsOpen: (value: boolean) => void;
54
+ resetKey: number;
55
+ }
56
+
57
+ export default ErrorsBanner;
@@ -0,0 +1,82 @@
1
+ import styled from "styled-components";
2
+
3
+ const ErrorsWrapper = styled.div`
4
+ position: sticky;
5
+ top: 0;
6
+ background-color: ${(p) => p.theme.colors.uiBackground03};
7
+ width: 100%;
8
+ padding: ${(p) => p.theme.spacing.s};
9
+ margin-bottom: ${(p) => p.theme.spacing.xs};
10
+ border-radius: ${(p) => p.theme.radii.s};
11
+ z-index: 2;
12
+ `;
13
+
14
+ const ErrorsHeader = styled.div`
15
+ display: flex;
16
+ align-items: center;
17
+ `;
18
+
19
+ const WarningWrapper = styled.div`
20
+ width: ${(p) => p.theme.spacing.s};
21
+ height: ${(p) => p.theme.spacing.s};
22
+ svg {
23
+ path {
24
+ fill: ${(p) => p.theme.color.interactive02};
25
+ }
26
+ }
27
+ `;
28
+
29
+ const HeaderText = styled.div`
30
+ ${(p) => p.theme.textStyle.uiM};
31
+ color: ${(p) => p.theme.colors.textHighEmphasis};
32
+ margin-left: ${(p) => p.theme.spacing.xxs};
33
+ font-weight: 600;
34
+ `;
35
+
36
+ const HeaderActions = styled.div`
37
+ display: flex;
38
+ margin-left: auto;
39
+ gap: ${(p) => p.theme.spacing.xxs};
40
+ align-items: center;
41
+ `;
42
+
43
+ const ToggleWrapper = styled.button`
44
+ width: ${(p) => p.theme.spacing.m};
45
+ height: ${(p) => p.theme.spacing.m};
46
+ `;
47
+
48
+ const IconWrapper = styled.button`
49
+ width: ${(p) => p.theme.spacing.s};
50
+ height: ${(p) => p.theme.spacing.s};
51
+ `;
52
+
53
+ const ErrorsContent = styled.div<{ isOpen: boolean }>`
54
+ display: flex;
55
+ flex-direction: column;
56
+ overflow: hidden;
57
+ height: ${(p) => (p.isOpen ? "auto" : "0")};
58
+ transition: height 1s ease-in-out;
59
+ `;
60
+
61
+ const Description = styled.div`
62
+ ${(p) => p.theme.textStyle.uiXS};
63
+ color: ${(p) => p.theme.colors.textMediumEmphasis};
64
+ margin-top: ${(p) => p.theme.spacing.xs};
65
+ `;
66
+
67
+ const ErrorListWrapper = styled.ul`
68
+ margin-top: ${(p) => p.theme.spacing.xs};
69
+ `;
70
+
71
+ export {
72
+ ErrorsWrapper,
73
+ ErrorsHeader,
74
+ IconWrapper,
75
+ WarningWrapper,
76
+ HeaderText,
77
+ HeaderActions,
78
+ ErrorsContent,
79
+ ToggleWrapper,
80
+ Description,
81
+ ErrorListWrapper,
82
+ };
@@ -0,0 +1,71 @@
1
+ import { Icon } from "@ax/components";
2
+ import type { HeadingNode } from "@ax/types";
3
+
4
+ import * as S from "./style";
5
+
6
+ const HeadingItem = ({
7
+ head,
8
+ index,
9
+ isFiltering,
10
+ selected,
11
+ onHeadingClick,
12
+ counter,
13
+ parentPath,
14
+ }: IHeadingItemProps) => {
15
+ counter.value += 1;
16
+ const headingId = counter.value;
17
+ const uniqueKey = `${parentPath}${head.tag}-${head.level}-${index}-${head.text.slice(0, 20)}`;
18
+
19
+ return (
20
+ <>
21
+ <S.HeadItem
22
+ level={isFiltering ? 1 : head.level}
23
+ tag={head.tag}
24
+ tabIndex={0}
25
+ role="button"
26
+ onClick={onHeadingClick(headingId)}
27
+ isSelected={selected === headingId}
28
+ isHidden={head.isHidden}
29
+ data-heading-id={headingId}
30
+ >
31
+ <S.HeadTag>
32
+ <div>{head.tag}</div>
33
+ </S.HeadTag>
34
+ <S.StyledTooltip content={head.isHidden ? "Hidden with CSS" : null}>
35
+ <S.HeadText>
36
+ <div>{head.text}</div>
37
+ {head.isHidden && (
38
+ <S.HiddenIcon>
39
+ <Icon name="hide" size="16" />
40
+ </S.HiddenIcon>
41
+ )}
42
+ </S.HeadText>
43
+ </S.StyledTooltip>
44
+ </S.HeadItem>
45
+ {head.children.map((child, childIndex) => (
46
+ <HeadingItem
47
+ key={`${uniqueKey}-child-${childIndex}`}
48
+ head={child}
49
+ index={childIndex}
50
+ isFiltering={isFiltering}
51
+ selected={selected}
52
+ onHeadingClick={onHeadingClick}
53
+ counter={counter}
54
+ parentPath={`${uniqueKey}-`}
55
+ />
56
+ ))}
57
+ </>
58
+ );
59
+ };
60
+
61
+ interface IHeadingItemProps {
62
+ head: HeadingNode;
63
+ index: number;
64
+ isFiltering: boolean;
65
+ selected: number | null;
66
+ onHeadingClick: (id: number) => () => void;
67
+ counter: { value: number };
68
+ parentPath: string;
69
+ }
70
+
71
+ export default HeadingItem;