@redocly/theme 0.1.28 → 0.1.31
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/CopyButton/CopyButtonWrapper.js +1 -1
- package/JsonViewer/JsonViewer.js +1 -1
- package/PageNavigation/NextPageLink.js +3 -3
- package/PageNavigation/PageNavigation.d.ts +1 -1
- package/PageNavigation/PageNavigation.js +7 -1
- package/PageNavigation/PreviousPageLink.js +3 -3
- package/Profile/Profile.d.ts +8 -0
- package/Profile/Profile.js +60 -0
- package/Profile/index.d.ts +2 -0
- package/Profile/index.js +5 -0
- package/TableOfContent/TableOfContent.js +9 -1
- package/Tooltip/Tooltip.d.ts +5 -4
- package/Tooltip/Tooltip.js +43 -21
- package/hooks/__tests__/mocks/MockIntersectionObserver.d.ts +15 -0
- package/hooks/__tests__/mocks/MockIntersectionObserver.js +39 -0
- package/hooks/useActiveHeading.d.ts +1 -1
- package/hooks/useActiveHeading.js +69 -20
- package/index.d.ts +1 -0
- package/index.js +1 -0
- package/mocks/hooks/index.d.ts +3 -3
- package/mocks/hooks/index.js +5 -8
- package/package.json +1 -1
- package/settings.yaml +4 -0
- package/src/CopyButton/CopyButtonWrapper.tsx +1 -1
- package/src/JsonViewer/JsonViewer.tsx +19 -17
- package/src/PageNavigation/NextPageLink.tsx +1 -1
- package/src/PageNavigation/PageNavigation.tsx +10 -2
- package/src/PageNavigation/PreviousPageLink.tsx +2 -2
- package/src/Profile/Profile.tsx +91 -0
- package/src/Profile/index.ts +2 -0
- package/src/TableOfContent/TableOfContent.tsx +11 -1
- package/src/Tooltip/Tooltip.tsx +87 -63
- package/src/hooks/useActiveHeading.ts +92 -28
- package/src/index.ts +1 -0
- package/src/mocks/hooks/index.ts +6 -9
- package/src/utils/color.ts +9 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/theme-helpers.ts +3 -1
- package/utils/color.d.ts +2 -0
- package/utils/color.js +12 -0
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/theme-helpers.js +3 -1
|
@@ -154,11 +154,6 @@ export const JsonViewer = styled(Json).attrs(() => ({
|
|
|
154
154
|
color: gray;
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
.collapsed > .collapser:after {
|
|
158
|
-
content: '+';
|
|
159
|
-
cursor: pointer;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
157
|
.ellipsis:after {
|
|
163
158
|
content: ' … ';
|
|
164
159
|
}
|
|
@@ -174,7 +169,11 @@ export const JsonViewer = styled(Json).attrs(() => ({
|
|
|
174
169
|
}
|
|
175
170
|
|
|
176
171
|
.collapsed {
|
|
177
|
-
|
|
172
|
+
white-space: nowrap;
|
|
173
|
+
|
|
174
|
+
& > .collapser:after {
|
|
175
|
+
content: '+';
|
|
176
|
+
}
|
|
178
177
|
}
|
|
179
178
|
|
|
180
179
|
.hovered {
|
|
@@ -182,13 +181,19 @@ export const JsonViewer = styled(Json).attrs(() => ({
|
|
|
182
181
|
}
|
|
183
182
|
|
|
184
183
|
.collapser {
|
|
184
|
+
--size: 15px;
|
|
185
|
+
--margin-right: 3px;
|
|
186
|
+
|
|
187
|
+
display: inline-block;
|
|
185
188
|
background-color: transparent;
|
|
186
189
|
border: 0;
|
|
187
|
-
padding:
|
|
190
|
+
padding: 1px;
|
|
188
191
|
color: #fff;
|
|
189
|
-
width:
|
|
190
|
-
height:
|
|
191
|
-
|
|
192
|
+
width: var(--size);
|
|
193
|
+
height: var(--size);
|
|
194
|
+
margin-left: calc((var(--size) + var(--margin-right)) * -1);
|
|
195
|
+
margin-right: var(--margin-right);
|
|
196
|
+
cursor: pointer;
|
|
192
197
|
user-select: none;
|
|
193
198
|
-webkit-user-select: none;
|
|
194
199
|
font-family: var(--code-font-family);
|
|
@@ -196,18 +201,15 @@ export const JsonViewer = styled(Json).attrs(() => ({
|
|
|
196
201
|
|
|
197
202
|
&:after {
|
|
198
203
|
content: '-';
|
|
199
|
-
cursor: pointer;
|
|
200
204
|
display: flex;
|
|
201
205
|
align-items: center;
|
|
202
206
|
justify-content: center;
|
|
203
|
-
width: 15px;
|
|
204
207
|
height: 100%;
|
|
205
|
-
|
|
206
|
-
|
|
208
|
+
width: 100%;
|
|
209
|
+
}
|
|
207
210
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
+
&:focus {
|
|
212
|
+
outline: #fff dotted 1px;
|
|
211
213
|
}
|
|
212
214
|
}
|
|
213
215
|
|
|
@@ -14,7 +14,7 @@ export function NextPageLink(): JSX.Element {
|
|
|
14
14
|
const { nextPage }: NextPageType = useSidebarSiblingsData() || {};
|
|
15
15
|
const { navigation } = useThemeSettings(DEFAULT_THEME_NAME);
|
|
16
16
|
|
|
17
|
-
if (!nextPage || navigation?.hide) {
|
|
17
|
+
if (!nextPage || navigation?.hide || navigation?.nextPageLink?.hide) {
|
|
18
18
|
return <div> </div>;
|
|
19
19
|
}
|
|
20
20
|
|
|
@@ -3,8 +3,16 @@ import styled from 'styled-components';
|
|
|
3
3
|
|
|
4
4
|
import { PreviousPageLink } from '@theme/PageNavigation/PreviousPageLink';
|
|
5
5
|
import { NextPageLink } from '@theme/PageNavigation/NextPageLink';
|
|
6
|
+
import { useThemeSettings } from '@portal/hooks';
|
|
7
|
+
import { DEFAULT_THEME_NAME } from '@portal/constants';
|
|
8
|
+
|
|
9
|
+
export function PageNavigation(): JSX.Element | null {
|
|
10
|
+
const { navigation } = useThemeSettings(DEFAULT_THEME_NAME);
|
|
11
|
+
|
|
12
|
+
if (navigation?.hide) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
6
15
|
|
|
7
|
-
export function PageNavigation(): JSX.Element {
|
|
8
16
|
return (
|
|
9
17
|
<PageNavigationWrapper data-component-name="PageNavigation/PageNavigation">
|
|
10
18
|
<PreviousPageLink />
|
|
@@ -16,5 +24,5 @@ export function PageNavigation(): JSX.Element {
|
|
|
16
24
|
const PageNavigationWrapper = styled.div`
|
|
17
25
|
display: flex;
|
|
18
26
|
justify-content: space-between;
|
|
19
|
-
margin: 25px
|
|
27
|
+
margin: 25px 0;
|
|
20
28
|
`;
|
|
@@ -14,11 +14,11 @@ export function PreviousPageLink(): JSX.Element {
|
|
|
14
14
|
const { prevPage }: PreviousPageType = useSidebarSiblingsData() || {};
|
|
15
15
|
const { navigation } = useThemeSettings(DEFAULT_THEME_NAME);
|
|
16
16
|
|
|
17
|
-
if (!prevPage || navigation?.hide) {
|
|
17
|
+
if (!prevPage || navigation?.hide || navigation?.previousPageLink?.hide) {
|
|
18
18
|
return <div> </div>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
const label = navigation?.
|
|
21
|
+
const label = navigation?.previousPageLink?.label || `Back to ${prevPage.label}`;
|
|
22
22
|
|
|
23
23
|
return (
|
|
24
24
|
<StyledButton
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import React, { memo } from 'react';
|
|
2
|
+
import styled, { css } from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import { getRandomColor } from '@theme/utils';
|
|
5
|
+
|
|
6
|
+
export interface ProfileProps {
|
|
7
|
+
name: string;
|
|
8
|
+
imageUrl?: string;
|
|
9
|
+
color?: string;
|
|
10
|
+
onClick?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const RANDOM_BG_COLOR: string = getRandomColor();
|
|
14
|
+
|
|
15
|
+
function ProfileComponent({ name, imageUrl, onClick, color }: ProfileProps): JSX.Element {
|
|
16
|
+
if (imageUrl) {
|
|
17
|
+
return (
|
|
18
|
+
<ProfileWrapper onClick={onClick}>
|
|
19
|
+
<StyledUserName data-cy="user-name" color={color}>
|
|
20
|
+
{name}
|
|
21
|
+
</StyledUserName>
|
|
22
|
+
{imageUrl && (
|
|
23
|
+
<AvatarWrapper>
|
|
24
|
+
<img data-cy="user-avatar" src={imageUrl} alt="profile" />
|
|
25
|
+
</AvatarWrapper>
|
|
26
|
+
)}
|
|
27
|
+
</ProfileWrapper>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const avatarLetters = `${name.charAt(0).toUpperCase()}${
|
|
32
|
+
name.split(' ')[1]?.charAt(0).toUpperCase() || ''
|
|
33
|
+
}`;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<ProfileWrapper onClick={onClick}>
|
|
37
|
+
<StyledUserName data-cy="user-name" color={color}>
|
|
38
|
+
{name}
|
|
39
|
+
</StyledUserName>
|
|
40
|
+
<AvatarWrapper background={RANDOM_BG_COLOR}>
|
|
41
|
+
<span>{avatarLetters}</span>
|
|
42
|
+
</AvatarWrapper>
|
|
43
|
+
</ProfileWrapper>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Profile = memo<ProfileProps>(ProfileComponent);
|
|
48
|
+
|
|
49
|
+
const ProfileWrapper = styled.div`
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
width: auto;
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const StyledUserName = styled.span`
|
|
57
|
+
color: ${({ color }) => color || 'var(--color-content)'};
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
const AvatarWrapper = styled.div<{ background?: string }>`
|
|
61
|
+
width: 40px;
|
|
62
|
+
height: 40px;
|
|
63
|
+
display: flex;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
position: relative;
|
|
66
|
+
font-size: 1.25rem;
|
|
67
|
+
align-items: center;
|
|
68
|
+
flex-shrink: 0;
|
|
69
|
+
line-height: 1;
|
|
70
|
+
user-select: none;
|
|
71
|
+
border-radius: 50%;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
margin-left: 16px;
|
|
74
|
+
|
|
75
|
+
${({ background }) => css`
|
|
76
|
+
background-color: ${background};
|
|
77
|
+
span {
|
|
78
|
+
color: ${background};
|
|
79
|
+
filter: invert(100%);
|
|
80
|
+
}
|
|
81
|
+
`}
|
|
82
|
+
|
|
83
|
+
& > img {
|
|
84
|
+
color: transparent;
|
|
85
|
+
width: 100%;
|
|
86
|
+
height: 100%;
|
|
87
|
+
object-fit: cover;
|
|
88
|
+
text-align: center;
|
|
89
|
+
text-indent: 10000px;
|
|
90
|
+
}
|
|
91
|
+
`;
|
|
@@ -18,9 +18,19 @@ export function TableOfContent(props: TableOfContentProps): JSX.Element | null {
|
|
|
18
18
|
|
|
19
19
|
const sidebar = useRef<HTMLDivElement | null>(null);
|
|
20
20
|
useFullHeight(sidebar);
|
|
21
|
-
const activeHeadingId = useActiveHeading(contentWrapper);
|
|
22
21
|
const { toc } = useThemeSettings(DEFAULT_THEME_NAME);
|
|
23
22
|
|
|
23
|
+
const getDisplayedHeaderIds = () => {
|
|
24
|
+
if (!headings) {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
return headings
|
|
28
|
+
.filter((header) => header && tocMaxDepth >= header.depth)
|
|
29
|
+
.map((header) => header?.id);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const activeHeadingId = useActiveHeading(contentWrapper, getDisplayedHeaderIds());
|
|
33
|
+
|
|
24
34
|
if (toc?.hide) {
|
|
25
35
|
return null;
|
|
26
36
|
}
|
package/src/Tooltip/Tooltip.tsx
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import type { PropsWithChildren } from 'react';
|
|
2
|
-
import React, {
|
|
1
|
+
import type { PropsWithChildren, ReactNode } from 'react';
|
|
2
|
+
import React, { useEffect, memo } from 'react';
|
|
3
3
|
import styled, { css } from 'styled-components';
|
|
4
4
|
|
|
5
5
|
import { useControl } from '@theme/hooks';
|
|
6
6
|
|
|
7
7
|
export interface TooltipProps {
|
|
8
|
-
tip: string;
|
|
9
|
-
|
|
8
|
+
tip: string | ReactNode;
|
|
9
|
+
isOpen?: boolean;
|
|
10
|
+
withArrow?: boolean;
|
|
10
11
|
placement?: 'top' | 'bottom' | 'left' | 'right';
|
|
11
12
|
className?: string;
|
|
12
13
|
width?: string;
|
|
@@ -15,46 +16,48 @@ export interface TooltipProps {
|
|
|
15
16
|
|
|
16
17
|
export function TooltipComponent({
|
|
17
18
|
children,
|
|
18
|
-
|
|
19
|
+
isOpen,
|
|
19
20
|
tip,
|
|
21
|
+
withArrow = true,
|
|
20
22
|
placement = 'top',
|
|
21
23
|
className = 'default',
|
|
22
24
|
width,
|
|
23
|
-
dataTestId
|
|
25
|
+
dataTestId,
|
|
24
26
|
}: PropsWithChildren<TooltipProps>): JSX.Element {
|
|
25
|
-
const { isOpened, handleOpen, handleClose } = useControl(
|
|
27
|
+
const { isOpened, handleOpen, handleClose } = useControl(isOpen);
|
|
26
28
|
|
|
27
|
-
const isControlled =
|
|
29
|
+
const isControlled = isOpen !== undefined;
|
|
28
30
|
|
|
29
31
|
useEffect(() => {
|
|
30
32
|
if (isControlled) {
|
|
31
|
-
if (
|
|
33
|
+
if (isOpen) {
|
|
32
34
|
handleOpen();
|
|
33
35
|
} else {
|
|
34
36
|
handleClose();
|
|
35
37
|
}
|
|
36
38
|
}
|
|
37
|
-
}, [
|
|
39
|
+
}, [isOpen, isControlled, handleOpen, handleClose]);
|
|
38
40
|
|
|
39
|
-
const
|
|
40
|
-
handleOpen
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
handleClose();
|
|
45
|
-
}, [handleClose]);
|
|
41
|
+
const controllers = !isControlled && {
|
|
42
|
+
onMouseEnter: handleOpen,
|
|
43
|
+
onMouseLeave: handleClose,
|
|
44
|
+
onClick: handleClose,
|
|
45
|
+
};
|
|
46
46
|
|
|
47
47
|
return (
|
|
48
48
|
<TooltipWrapper
|
|
49
|
-
|
|
50
|
-
onMouseLeave={isControlled ? undefined : handleLeave}
|
|
51
|
-
onClick={isControlled ? undefined : handleLeave}
|
|
49
|
+
{...controllers}
|
|
52
50
|
className={`tooltip-${className}`}
|
|
53
51
|
data-component-name="Tooltip/Tooltip"
|
|
54
52
|
>
|
|
55
53
|
{children}
|
|
56
54
|
{isOpened && (
|
|
57
|
-
<TooltipBody
|
|
55
|
+
<TooltipBody
|
|
56
|
+
data-cy={dataTestId || (typeof tip === 'string' ? tip : '')}
|
|
57
|
+
placement={placement}
|
|
58
|
+
width={width}
|
|
59
|
+
withArrow={withArrow}
|
|
60
|
+
>
|
|
58
61
|
{tip}
|
|
59
62
|
</TooltipBody>
|
|
60
63
|
)}
|
|
@@ -65,69 +68,85 @@ export function TooltipComponent({
|
|
|
65
68
|
export const Tooltip = memo<PropsWithChildren<TooltipProps>>(TooltipComponent);
|
|
66
69
|
|
|
67
70
|
const PLACEMENTS = {
|
|
68
|
-
top: css
|
|
71
|
+
top: css<Pick<TooltipProps, 'withArrow'>>`
|
|
69
72
|
top: 0;
|
|
70
73
|
left: 50%;
|
|
71
74
|
transform: translate(-50%, -99%);
|
|
72
75
|
margin-top: -10px;
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
77
|
+
${({ withArrow }) =>
|
|
78
|
+
withArrow &&
|
|
79
|
+
css`
|
|
80
|
+
&::after {
|
|
81
|
+
border-left: 5px solid transparent;
|
|
82
|
+
border-right: 5px solid transparent;
|
|
83
|
+
border-top-width: 6px;
|
|
84
|
+
border-top-style: solid;
|
|
85
|
+
bottom: 0;
|
|
86
|
+
left: 50%;
|
|
87
|
+
transform: translate(-50%, 99%);
|
|
88
|
+
}
|
|
89
|
+
`}
|
|
83
90
|
`,
|
|
84
|
-
bottom: css
|
|
91
|
+
bottom: css<Pick<TooltipProps, 'withArrow'>>`
|
|
85
92
|
bottom: 0;
|
|
86
93
|
left: 50%;
|
|
87
94
|
transform: translate(-50%, 99%);
|
|
88
95
|
margin-bottom: -10px;
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
${({ withArrow }) =>
|
|
98
|
+
withArrow &&
|
|
99
|
+
css`
|
|
100
|
+
&::after {
|
|
101
|
+
border-left: 5px solid transparent;
|
|
102
|
+
border-right: 5px solid transparent;
|
|
103
|
+
border-bottom-width: 6px;
|
|
104
|
+
border-bottom-style: solid;
|
|
105
|
+
top: 0;
|
|
106
|
+
left: 50%;
|
|
107
|
+
transform: translate(-50%, -99%);
|
|
108
|
+
}
|
|
109
|
+
`}
|
|
99
110
|
`,
|
|
100
|
-
left: css
|
|
111
|
+
left: css<Pick<TooltipProps, 'withArrow'>>`
|
|
101
112
|
top: 50%;
|
|
102
113
|
left: 0;
|
|
103
114
|
transform: translate(-100%, -50%);
|
|
104
115
|
margin-left: -10px;
|
|
105
116
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
117
|
+
${({ withArrow }) =>
|
|
118
|
+
withArrow &&
|
|
119
|
+
css`
|
|
120
|
+
&::after {
|
|
121
|
+
border-top: 5px solid transparent;
|
|
122
|
+
border-bottom: 5px solid transparent;
|
|
123
|
+
border-left-width: 6px;
|
|
124
|
+
border-left-style: solid;
|
|
125
|
+
top: 50%;
|
|
126
|
+
right: 0;
|
|
127
|
+
transform: translate(99%, -50%);
|
|
128
|
+
}
|
|
129
|
+
`}
|
|
115
130
|
`,
|
|
116
|
-
right: css
|
|
131
|
+
right: css<Pick<TooltipProps, 'withArrow'>>`
|
|
117
132
|
top: 50%;
|
|
118
133
|
right: 0;
|
|
119
134
|
transform: translate(100%, -50%);
|
|
120
135
|
margin-right: -10px;
|
|
121
136
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
${({ withArrow }) =>
|
|
138
|
+
withArrow &&
|
|
139
|
+
css`
|
|
140
|
+
&::after {
|
|
141
|
+
border-top: 5px solid transparent;
|
|
142
|
+
border-bottom: 5px solid transparent;
|
|
143
|
+
border-right-width: 6px;
|
|
144
|
+
border-right-style: solid;
|
|
145
|
+
top: 50%;
|
|
146
|
+
left: 0;
|
|
147
|
+
transform: translate(-99%, -50%);
|
|
148
|
+
}
|
|
149
|
+
`}
|
|
131
150
|
`,
|
|
132
151
|
};
|
|
133
152
|
|
|
@@ -136,7 +155,9 @@ const TooltipWrapper = styled.div`
|
|
|
136
155
|
display: inline-block;
|
|
137
156
|
`;
|
|
138
157
|
|
|
139
|
-
const TooltipBody = styled.span<
|
|
158
|
+
const TooltipBody = styled.span<
|
|
159
|
+
Pick<Required<TooltipProps>, 'placement' | 'withArrow'> & { width?: string }
|
|
160
|
+
>`
|
|
140
161
|
display: inline-block;
|
|
141
162
|
|
|
142
163
|
position: absolute;
|
|
@@ -168,5 +189,8 @@ const TooltipBody = styled.span<Pick<Required<TooltipProps>, 'placement'> & { wi
|
|
|
168
189
|
box-shadow: rgb(0 0 0 / 25%) 0 2px 4px;
|
|
169
190
|
|
|
170
191
|
width: ${({ width }) => width || '120px'};
|
|
171
|
-
${({ placement }) =>
|
|
192
|
+
${({ placement }) =>
|
|
193
|
+
css`
|
|
194
|
+
${PLACEMENTS[placement]};
|
|
195
|
+
`}
|
|
172
196
|
`;
|
|
@@ -1,46 +1,110 @@
|
|
|
1
|
-
import { useState,
|
|
1
|
+
import { useState, useEffect, useRef } from 'react';
|
|
2
|
+
import { useHistory } from 'react-router-dom';
|
|
2
3
|
|
|
3
4
|
export type UseActiveHeadingReturnType = string | undefined;
|
|
4
5
|
|
|
5
|
-
|
|
6
|
+
type HeadingEntry = {
|
|
7
|
+
[key: string]: IntersectionObserverEntry;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export function useActiveHeading(
|
|
11
|
+
contentElement: HTMLDivElement | null,
|
|
12
|
+
displayedHeaders: Array<string | undefined>,
|
|
13
|
+
): UseActiveHeadingReturnType {
|
|
6
14
|
const [heading, setHeading] = useState<string | undefined>(undefined);
|
|
15
|
+
const [headingElements, setHeadingElements] = useState<HTMLElement[]>([]);
|
|
16
|
+
const headingElementsRef = useRef<HeadingEntry>({});
|
|
7
17
|
|
|
8
|
-
const
|
|
9
|
-
() => contentElement && contentElement.querySelectorAll<HTMLElement>('.heading-anchor'),
|
|
10
|
-
[contentElement],
|
|
11
|
-
);
|
|
18
|
+
const history = useHistory();
|
|
12
19
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
20
|
+
const getVisibleHeadings = () => {
|
|
21
|
+
const visibleHeadings: IntersectionObserverEntry[] = [];
|
|
22
|
+
|
|
23
|
+
for (const key in headingElementsRef.current) {
|
|
24
|
+
const headingElement = headingElementsRef.current[key];
|
|
19
25
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const newHeading = i === 0 ? undefined : headings[i - 1].getAttribute('id') || undefined;
|
|
23
|
-
setHeading(newHeading);
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
+
if (headingElement.isIntersecting) {
|
|
27
|
+
visibleHeadings.push(headingElement);
|
|
26
28
|
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return visibleHeadings;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const getIndexFromId = (id: string) => {
|
|
35
|
+
return headingElements.findIndex((item) => item.id === id);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const findHeaders = (allContent: HTMLDivElement) => {
|
|
39
|
+
const allHeaders = allContent.querySelectorAll<HTMLElement>('.heading-anchor');
|
|
40
|
+
return Array.from(allHeaders);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const intersectionCallback = (headings: IntersectionObserverEntry[]) => {
|
|
44
|
+
headingElementsRef.current = headings.reduce(
|
|
45
|
+
(map: HeadingEntry, headingElement: IntersectionObserverEntry) => {
|
|
46
|
+
map[headingElement.target.id] = headingElement;
|
|
47
|
+
return map;
|
|
48
|
+
},
|
|
49
|
+
headingElementsRef.current,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const totalHeight = window.scrollY + window.innerHeight;
|
|
53
|
+
// handle bottom of the page
|
|
54
|
+
if (totalHeight >= document.body.scrollHeight) {
|
|
55
|
+
const newHeading = headingElements[headingElements?.length - 1]?.id || undefined;
|
|
56
|
+
setHeading(newHeading);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const visibleHeadings = getVisibleHeadings();
|
|
61
|
+
if (!visibleHeadings.length) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (visibleHeadings.length === 1) {
|
|
66
|
+
setHeading(visibleHeadings[0].target.id);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
visibleHeadings.sort((a, b) => {
|
|
71
|
+
return getIndexFromId(a.target.id) - getIndexFromId(b.target.id);
|
|
72
|
+
});
|
|
73
|
+
setHeading(visibleHeadings[0].target.id);
|
|
74
|
+
};
|
|
30
75
|
|
|
31
76
|
useEffect(() => {
|
|
32
|
-
if (
|
|
33
|
-
return
|
|
77
|
+
if (!contentElement) {
|
|
78
|
+
return;
|
|
34
79
|
}
|
|
80
|
+
setHeadingElements(findHeaders(contentElement));
|
|
35
81
|
|
|
36
|
-
|
|
37
|
-
|
|
82
|
+
const unlisten = history.listen(() => {
|
|
83
|
+
setHeadingElements(findHeaders(contentElement));
|
|
38
84
|
});
|
|
39
85
|
|
|
40
|
-
|
|
86
|
+
return () => unlisten();
|
|
87
|
+
}, [contentElement]);
|
|
88
|
+
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
if (!headingElements?.length) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
headingElementsRef.current = {};
|
|
94
|
+
|
|
95
|
+
// Bottom rootMargin -30% changes part of the view where IntersectionObserver starts to detect headers
|
|
96
|
+
const observer = new IntersectionObserver(intersectionCallback, {
|
|
97
|
+
rootMargin: '0px 0px -30% 0px',
|
|
98
|
+
threshold: 1,
|
|
99
|
+
});
|
|
100
|
+
headingElements?.forEach((element) => {
|
|
101
|
+
if (displayedHeaders.includes(element.id)) {
|
|
102
|
+
observer.observe(element);
|
|
103
|
+
}
|
|
104
|
+
});
|
|
41
105
|
|
|
42
|
-
return () =>
|
|
43
|
-
}, [
|
|
106
|
+
return () => observer.disconnect();
|
|
107
|
+
}, [headingElements, displayedHeaders]);
|
|
44
108
|
|
|
45
109
|
return heading;
|
|
46
110
|
}
|
package/src/index.ts
CHANGED
package/src/mocks/hooks/index.ts
CHANGED
|
@@ -9,20 +9,17 @@ interface PageLink {
|
|
|
9
9
|
type: 'link';
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export function useThemeSettings(
|
|
12
|
+
export function useThemeSettings(_: string): RawTheme['settings'] {
|
|
13
13
|
return {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
nextPageLink: { label: 'next page' },
|
|
19
|
-
previousPageLink: { label: 'prev page' },
|
|
20
|
-
},
|
|
14
|
+
toc: { header: 'header', hide: false },
|
|
15
|
+
navigation: {
|
|
16
|
+
nextPageLink: { label: 'next page theme settings label' },
|
|
17
|
+
previousPageLink: { label: 'prev page theme settings label' },
|
|
21
18
|
},
|
|
22
19
|
};
|
|
23
20
|
}
|
|
24
21
|
|
|
25
|
-
export function useSidebarSiblingsData(): { nextPage: PageLink; prevPage: PageLink } {
|
|
22
|
+
export function useSidebarSiblingsData(): { nextPage: PageLink | null; prevPage: PageLink | null } {
|
|
26
23
|
return {
|
|
27
24
|
nextPage: {
|
|
28
25
|
type: 'link',
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const getRandomNumber = (limit: number): number => Math.floor(Math.random() * limit);
|
|
2
|
+
|
|
3
|
+
export function getRandomColor(): string {
|
|
4
|
+
const h = getRandomNumber(360);
|
|
5
|
+
const s = getRandomNumber(100);
|
|
6
|
+
const l = getRandomNumber(100);
|
|
7
|
+
|
|
8
|
+
return `hsl(${h}deg, ${s}%, ${l}%)`;
|
|
9
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -7,7 +7,7 @@ enum Token {
|
|
|
7
7
|
Doctype = 'doctype',
|
|
8
8
|
Cdata = 'cdata',
|
|
9
9
|
Punctuation = 'punctuation',
|
|
10
|
-
|
|
10
|
+
Property = 'property',
|
|
11
11
|
Tag = 'tag',
|
|
12
12
|
Number = 'number',
|
|
13
13
|
Constant = 'constant',
|
|
@@ -23,6 +23,8 @@ enum Token {
|
|
|
23
23
|
Url = 'url',
|
|
24
24
|
Variable = 'variable',
|
|
25
25
|
Atrule = 'atrule',
|
|
26
|
+
AttrValue = 'attr-value',
|
|
27
|
+
AttrName = 'attr-name',
|
|
26
28
|
Keyword = 'keyword',
|
|
27
29
|
Regex = 'regex',
|
|
28
30
|
Important = 'important',
|
package/utils/color.d.ts
ADDED