@redocly/theme 0.2.1 → 0.3.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 (45) hide show
  1. package/ColorModeSwitcher/ColorModeSwitcher.d.ts +2 -0
  2. package/ColorModeSwitcher/ColorModeSwitcher.js +80 -0
  3. package/ColorModeSwitcher/index.d.ts +1 -0
  4. package/ColorModeSwitcher/index.js +17 -0
  5. package/CopyButton/CopyButtonWrapper.d.ts +2 -1
  6. package/CopyButton/CopyButtonWrapper.js +3 -2
  7. package/Markdown/Details.d.ts +6 -0
  8. package/Markdown/Details.js +22 -0
  9. package/Markdown/MarkdownLayout.d.ts +3 -1
  10. package/Markdown/MarkdownLayout.js +2 -2
  11. package/Navbar/Navbar.js +3 -1
  12. package/Navbar/NavbarMenu.js +1 -1
  13. package/PageNavigation/PageNavigation.d.ts +6 -1
  14. package/PageNavigation/PageNavigation.js +4 -3
  15. package/SourceCode/SourceCode.js +5 -5
  16. package/globalStyle.d.ts +1 -0
  17. package/globalStyle.js +26 -24
  18. package/hooks/useActiveHeading.js +6 -5
  19. package/icons/ColorModeIcon/ColorModeIcon.d.ts +10 -0
  20. package/icons/ColorModeIcon/ColorModeIcon.js +30 -0
  21. package/icons/ColorModeIcon/index.d.ts +2 -0
  22. package/icons/ColorModeIcon/index.js +5 -0
  23. package/icons/index.d.ts +1 -0
  24. package/icons/index.js +1 -0
  25. package/index.d.ts +1 -0
  26. package/index.js +1 -0
  27. package/mocks/hooks/index.js +4 -0
  28. package/package.json +1 -1
  29. package/src/ColorModeSwitcher/ColorModeSwitcher.tsx +48 -0
  30. package/src/ColorModeSwitcher/index.ts +1 -0
  31. package/src/CopyButton/CopyButtonWrapper.tsx +6 -3
  32. package/src/Markdown/Details.tsx +19 -0
  33. package/src/Markdown/MarkdownLayout.tsx +5 -1
  34. package/src/Navbar/Navbar.tsx +2 -0
  35. package/src/Navbar/NavbarMenu.tsx +2 -2
  36. package/src/PageNavigation/PageNavigation.tsx +11 -3
  37. package/src/SourceCode/SourceCode.tsx +4 -4
  38. package/src/globalStyle.ts +41 -1
  39. package/src/hooks/useActiveHeading.ts +41 -34
  40. package/src/icons/ColorModeIcon/ColorModeIcon.tsx +53 -0
  41. package/src/icons/ColorModeIcon/index.ts +2 -0
  42. package/src/icons/index.ts +1 -0
  43. package/src/index.ts +1 -0
  44. package/src/mocks/hooks/index.ts +4 -0
  45. package/{settings.yaml → src/settings.yaml} +6 -0
@@ -0,0 +1,48 @@
1
+ import React, { useState } from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ import { useThemeSettings } from '@portal/hooks';
5
+ import { DEFAULT_THEME_NAME } from '@portal/constants';
6
+ import { ColorModeIcon } from '@theme/icons/ColorModeIcon';
7
+ import { useMount } from '@theme/hooks';
8
+
9
+ export function ColorModeSwitcher(): JSX.Element | null {
10
+ const themeSettings = useThemeSettings(DEFAULT_THEME_NAME);
11
+ const colorMode = themeSettings.colorMode;
12
+ const [activeColorMode, setActiveColorMode] = useState('');
13
+
14
+ useMount(() => {
15
+ setActiveColorMode(document.documentElement.className || colorMode?.default);
16
+ });
17
+
18
+ if (!colorMode?.modes || colorMode?.hide) {
19
+ return null;
20
+ }
21
+
22
+ const handelChangeColorMode = () => {
23
+ const activeIndex = colorMode.modes.indexOf(activeColorMode);
24
+ const mode =
25
+ activeIndex < colorMode.modes.length - 1
26
+ ? colorMode.modes[activeIndex + 1]
27
+ : colorMode.modes[0];
28
+ setActiveColorMode(mode);
29
+ localStorage.setItem('colorSchema', mode);
30
+ document.documentElement.className = mode;
31
+ };
32
+
33
+ return (
34
+ <Wrapper
35
+ data-component-name="ColorModeSwitcher/ColorModeSwitcher"
36
+ onClick={handelChangeColorMode}
37
+ >
38
+ <ColorModeIcon mode={activeColorMode} />
39
+ </Wrapper>
40
+ );
41
+ }
42
+
43
+ const Wrapper = styled.div`
44
+ margin-left: calc(var(--sidebar-spacing-horizontal) * 2);
45
+ display: flex;
46
+ align-items: center;
47
+ cursor: pointer;
48
+ `;
@@ -0,0 +1 @@
1
+ export * from '@theme/ColorModeSwitcher/ColorModeSwitcher';
@@ -1,13 +1,15 @@
1
1
  import React, { memo } from 'react';
2
2
 
3
3
  import { SamplesControlButton } from '@theme/SamplesPanelControls';
4
- import { Tooltip } from '@theme/Tooltip';
4
+ import { Tooltip, TooltipProps } from '@theme/Tooltip';
5
5
  import { useControl } from '@theme/hooks';
6
6
  import { ClipboardService } from '@theme/utils';
7
7
 
8
8
  export interface CopyButtonWrapperProps {
9
9
  data: unknown;
10
- children: (props: { renderCopyButton: () => JSX.Element }) => JSX.Element;
10
+ children: (props: {
11
+ renderCopyButton: (placement?: TooltipProps['placement']) => JSX.Element;
12
+ }) => JSX.Element;
11
13
  dataTestId?: string;
12
14
  }
13
15
 
@@ -32,12 +34,13 @@ function CopyButtonWrapperComponent({
32
34
  showTooltip();
33
35
  };
34
36
 
35
- const renderCopyButton = (): JSX.Element => {
37
+ const renderCopyButton = (placement: TooltipProps['placement'] = 'top'): JSX.Element => {
36
38
  return (
37
39
  <Tooltip
38
40
  className="copy-button"
39
41
  tip={ClipboardService.isSupported() ? 'Copied' : 'Not supported in your browser'}
40
42
  isOpen={tooltip.isOpened}
43
+ placement={placement}
41
44
  >
42
45
  <SamplesControlButton onClick={copy} data-cy={dataTestId}>
43
46
  Copy
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ type DetailsProps = {
5
+ summary: string;
6
+ };
7
+
8
+ export function Details({ summary, children }: React.PropsWithChildren<DetailsProps>): JSX.Element {
9
+ return (
10
+ <StyledDetails>
11
+ <summary>{summary}</summary>
12
+ <StyledDetailsContent>{children}</StyledDetailsContent>
13
+ </StyledDetails>
14
+ );
15
+ }
16
+
17
+ const StyledDetails = styled.details``;
18
+
19
+ const StyledDetailsContent = styled.div``;
@@ -7,17 +7,21 @@ import { PageNavigation } from '@theme/PageNavigation/PageNavigation';
7
7
  type MarkdownLayoutProps = {
8
8
  tableOfContent: React.ReactNode;
9
9
  markdownWrapper: React.ReactNode;
10
+ showPrevButton?: boolean;
11
+ showNextButton?: boolean;
10
12
  };
11
13
 
12
14
  export function MarkdownLayout({
13
15
  tableOfContent,
14
16
  markdownWrapper,
17
+ showPrevButton,
18
+ showNextButton,
15
19
  }: MarkdownLayoutProps): JSX.Element {
16
20
  return (
17
21
  <PageWrapper data-component-name="Markdown/MarkdownLayout">
18
22
  <ContentWrapper withToc={true}>
19
23
  {markdownWrapper}
20
- <PageNavigation />
24
+ <PageNavigation showPrevButton={showPrevButton} showNextButton={showNextButton} />
21
25
  </ContentWrapper>
22
26
  {tableOfContent}
23
27
  </PageWrapper>
@@ -8,6 +8,7 @@ import { NavbarMenu } from '@theme/Navbar';
8
8
  import { useMobileMenu } from '@theme/hooks/useMobileMenu';
9
9
  import { MobileNavbarMenuButton } from '@theme/Navbar/MobileNavbarMenuButton';
10
10
  import { MobileNavbarMenu } from '@theme/Navbar/MobileNavbarMenu';
11
+ import { ColorModeSwitcher } from '@theme/ColorModeSwitcher/ColorModeSwitcher';
11
12
 
12
13
  interface NavbarProps {
13
14
  menu: ResolvedConfigLinks;
@@ -37,6 +38,7 @@ export function Navbar({ menu, logo, search, profile }: NavbarProps): JSX.Elemen
37
38
  <NavbarMenu menuItems={menu} />
38
39
  {hideSearch ? null : search}
39
40
  {profile}
41
+ <ColorModeSwitcher />
40
42
  </NavbarContainer>
41
43
  );
42
44
  }
@@ -4,10 +4,10 @@ import styled from 'styled-components';
4
4
  import type { ResolvedConfigLinks, ResolvedNavItem } from '@theme/types/portal';
5
5
 
6
6
  import { NavbarItem } from '@theme/Navbar/NavbarItem';
7
- import { isPrimitive, isEmptyArray } from '@theme/utils';
7
+ import { isPrimitive } from '@theme/utils';
8
8
 
9
9
  export function NavbarMenu({ menuItems }: { menuItems: ResolvedConfigLinks }): JSX.Element | null {
10
- if (isPrimitive(menuItems) || isEmptyArray(menuItems)) {
10
+ if (isPrimitive(menuItems)) {
11
11
  return null;
12
12
  }
13
13
 
@@ -6,7 +6,15 @@ import { NextPageLink } from '@theme/PageNavigation/NextPageLink';
6
6
  import { useThemeSettings } from '@portal/hooks';
7
7
  import { DEFAULT_THEME_NAME } from '@portal/constants';
8
8
 
9
- export function PageNavigation(): JSX.Element | null {
9
+ type PageNavigationProps = {
10
+ showPrevButton?: boolean;
11
+ showNextButton?: boolean;
12
+ };
13
+
14
+ export function PageNavigation({
15
+ showPrevButton = true,
16
+ showNextButton = true,
17
+ }: PageNavigationProps): JSX.Element | null {
10
18
  const { navigation } = useThemeSettings(DEFAULT_THEME_NAME);
11
19
 
12
20
  if (navigation?.hide) {
@@ -15,8 +23,8 @@ export function PageNavigation(): JSX.Element | null {
15
23
 
16
24
  return (
17
25
  <PageNavigationWrapper data-component-name="PageNavigation/PageNavigation">
18
- <PreviousPageLink />
19
- <NextPageLink />
26
+ {showPrevButton && <PreviousPageLink />}
27
+ {showNextButton && <NextPageLink />}
20
28
  </PageNavigationWrapper>
21
29
  );
22
30
  }
@@ -78,11 +78,11 @@ export function SourceCode({
78
78
  // Because we don't have session storage in ssr and can't get the security details there
79
79
  // Issue for more details https://github.com/Redocly/reference-docs/issues/888
80
80
  useEffect(() => {
81
- const _externalSource = externalSource?.sample?.get?.(externalSource) ?? '';
82
- if (_externalSource) {
83
- setSourceCode(_externalSource);
81
+ const _source = source || externalSource?.sample?.get?.(externalSource);
82
+ if (_source) {
83
+ setSourceCode(_source);
84
84
  }
85
- }, [externalSource]);
85
+ }, [source, externalSource]);
86
86
 
87
87
  if (withCopyButton) {
88
88
  return (
@@ -79,6 +79,23 @@ const baseColors = css`
79
79
 
80
80
  // @tokens End
81
81
  `;
82
+ const baseDarkColors = css`
83
+ /**
84
+ * @tokens Base Dark Colors
85
+ * @presenter Color
86
+ */
87
+ --color-primary-100: #969ca6;
88
+ --color-primary-200: #7f8693;
89
+ --color-primary-300: #7d7d80;
90
+ --color-primary-400: #4b4f56;
91
+ --color-primary-500: #404042;
92
+ --color-primary-600: #36383d;
93
+ --color-primary-700: #28282a;
94
+ --color-primary-800: #202021;
95
+ --color-primary-900: #000000;
96
+
97
+ // @tokens End
98
+ `;
82
99
 
83
100
  const httpColors = css`
84
101
  /**
@@ -477,6 +494,21 @@ const panels = css`
477
494
  --samples-panel-width: 50%;
478
495
 
479
496
  --panels-background-color: #fff;
497
+
498
+ --panels-title-color: var(--color-content);
499
+ --panels-title-font-size: var(--h3-font-size);
500
+ --panels-title-line-height: var(--h3-line-height);
501
+ --panels-title-font-weight: var(--h-font-weight);
502
+ --panels-title-font-family: var(--font-family-base);
503
+
504
+ --panels-sub-title-color: var(--color-content);
505
+ --panels-sub-title-font-size: 0.9em;
506
+ --panels-sub-title-font-weight: normal;
507
+ --panels-sub-title-font-family: var(--font-family-base);
508
+ --panels-sub-title-line-height: var(--h3-line-height);
509
+
510
+ --panels-shelf-icon-color: var(--color-content);
511
+
480
512
  --samples-panel-block-background-color: #fff;
481
513
  --samples-panel-background-color: #52606d;
482
514
  --samples-panel-callback-background-color: var(--color-secondary-300);
@@ -812,6 +844,10 @@ const portalSearch = css`
812
844
  // @tokens End
813
845
  `;
814
846
 
847
+ export const darkMode = css`
848
+ ${baseDarkColors}
849
+ `;
850
+
815
851
  export const styles = css`
816
852
  :root {
817
853
  ${baseColors}
@@ -837,8 +873,12 @@ export const styles = css`
837
873
 
838
874
  ${openapiAndGraphqlDocs}
839
875
  }
876
+
877
+ :root.dark {
878
+ ${darkMode};
879
+ }
840
880
  `;
841
881
 
842
882
  export const GlobalStyle = createGlobalStyle`
843
- ${styles}
883
+ ${styles};
844
884
  `;
@@ -1,4 +1,4 @@
1
- import { useState, useEffect, useRef } from 'react';
1
+ import { useState, useEffect, useRef, useCallback } from 'react';
2
2
  import { useHistory } from 'react-router-dom';
3
3
 
4
4
  export type UseActiveHeadingReturnType = string | undefined;
@@ -31,47 +31,53 @@ export function useActiveHeading(
31
31
  return visibleHeadings;
32
32
  };
33
33
 
34
- const getIndexFromId = (id: string) => {
35
- return headingElements.findIndex((item) => item.id === id);
36
- };
34
+ const getIndexFromId = useCallback(
35
+ (id: string) => {
36
+ return headingElements.findIndex((item) => item.id === id);
37
+ },
38
+ [headingElements],
39
+ );
37
40
 
38
41
  const findHeaders = (allContent: HTMLDivElement) => {
39
42
  const allHeaders = allContent.querySelectorAll<HTMLElement>('.heading-anchor');
40
43
  return Array.from(allHeaders);
41
44
  };
42
45
 
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
- }
46
+ const intersectionCallback = useCallback(
47
+ (headings: IntersectionObserverEntry[]) => {
48
+ headingElementsRef.current = headings.reduce(
49
+ (map: HeadingEntry, headingElement: IntersectionObserverEntry) => {
50
+ map[headingElement.target.id] = headingElement;
51
+ return map;
52
+ },
53
+ headingElementsRef.current,
54
+ );
55
+
56
+ const totalHeight = window.scrollY + window.innerHeight;
57
+ // handle bottom of the page
58
+ if (totalHeight >= document.body.scrollHeight) {
59
+ const newHeading = headingElements[headingElements?.length - 1]?.id || undefined;
60
+ setHeading(newHeading);
61
+ return;
62
+ }
59
63
 
60
- const visibleHeadings = getVisibleHeadings();
61
- if (!visibleHeadings.length) {
62
- return;
63
- }
64
+ const visibleHeadings = getVisibleHeadings();
65
+ if (!visibleHeadings.length) {
66
+ return;
67
+ }
64
68
 
65
- if (visibleHeadings.length === 1) {
66
- setHeading(visibleHeadings[0].target.id);
67
- return;
68
- }
69
+ if (visibleHeadings.length === 1) {
70
+ setHeading(visibleHeadings[0].target.id);
71
+ return;
72
+ }
69
73
 
70
- visibleHeadings.sort((a, b) => {
71
- return getIndexFromId(a.target.id) - getIndexFromId(b.target.id);
72
- });
73
- setHeading(visibleHeadings[0].target.id);
74
- };
74
+ visibleHeadings.sort((a, b) => {
75
+ return getIndexFromId(a.target.id) - getIndexFromId(b.target.id);
76
+ });
77
+ setHeading(visibleHeadings[0].target.id);
78
+ },
79
+ [getIndexFromId, headingElements],
80
+ );
75
81
 
76
82
  useEffect(() => {
77
83
  if (!contentElement) {
@@ -84,6 +90,7 @@ export function useActiveHeading(
84
90
  });
85
91
 
86
92
  return () => unlisten();
93
+ // eslint-disable-next-line react-hooks/exhaustive-deps
87
94
  }, [contentElement]);
88
95
 
89
96
  useEffect(() => {
@@ -104,7 +111,7 @@ export function useActiveHeading(
104
111
  });
105
112
 
106
113
  return () => observer.disconnect();
107
- }, [headingElements, displayedHeaders]);
114
+ }, [headingElements, displayedHeaders, intersectionCallback]);
108
115
 
109
116
  return heading;
110
117
  }
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ export interface ColorModeIconProps {
5
+ mode?: 'dark' | 'light' | string;
6
+ className?: string;
7
+ }
8
+
9
+ function Icon({ mode, className }: ColorModeIconProps) {
10
+ switch (mode) {
11
+ case 'dark':
12
+ return (
13
+ <svg
14
+ className={className}
15
+ data-testid="dark"
16
+ viewBox="0 0 16 16"
17
+ xmlns="http://www.w3.org/2000/svg"
18
+ >
19
+ <path d="M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z" />
20
+ </svg>
21
+ );
22
+ case 'light':
23
+ return (
24
+ <svg
25
+ data-testid="light"
26
+ className={className}
27
+ viewBox="0 0 16 16"
28
+ xmlns="http://www.w3.org/2000/svg"
29
+ >
30
+ <path d="M12 8a4 4 0 1 1-8 0 4 4 0 0 1 8 0zM8 0a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 0zm0 13a.5.5 0 0 1 .5.5v2a.5.5 0 0 1-1 0v-2A.5.5 0 0 1 8 13zm8-5a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2a.5.5 0 0 1 .5.5zM3 8a.5.5 0 0 1-.5.5h-2a.5.5 0 0 1 0-1h2A.5.5 0 0 1 3 8zm10.657-5.657a.5.5 0 0 1 0 .707l-1.414 1.415a.5.5 0 1 1-.707-.708l1.414-1.414a.5.5 0 0 1 .707 0zm-9.193 9.193a.5.5 0 0 1 0 .707L3.05 13.657a.5.5 0 0 1-.707-.707l1.414-1.414a.5.5 0 0 1 .707 0zm9.193 2.121a.5.5 0 0 1-.707 0l-1.414-1.414a.5.5 0 0 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .707zM4.464 4.465a.5.5 0 0 1-.707 0L2.343 3.05a.5.5 0 1 1 .707-.707l1.414 1.414a.5.5 0 0 1 0 .708z" />
31
+ </svg>
32
+ );
33
+ default:
34
+ return (
35
+ <svg
36
+ data-testid="custom"
37
+ className={className}
38
+ viewBox="0 0 16 16"
39
+ xmlns="http://www.w3.org/2000/svg"
40
+ >
41
+ <path d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm0 13V2a6 6 0 1 1 0 12z" />
42
+ </svg>
43
+ );
44
+ }
45
+ }
46
+
47
+ export const ColorModeIcon = styled(Icon).attrs(() => ({
48
+ 'data-component-name': 'icons/ColorModeIcon/ColorModeIcon',
49
+ }))`
50
+ width: var(--navbar-item-font-size);
51
+ box-sizing: border-box;
52
+ fill: var(--navbar-text-color);
53
+ `;
@@ -0,0 +1,2 @@
1
+ export { ColorModeIcon } from '@theme/icons/ColorModeIcon/ColorModeIcon';
2
+ export type { ColorModeIconProps } from '@theme/icons/ColorModeIcon/ColorModeIcon';
@@ -1,3 +1,4 @@
1
1
  export * from '@theme/icons/ShelfIcon';
2
2
  export * from '@theme/icons/AlertIcon';
3
3
  export * from '@theme/icons/ArrowIcon';
4
+ export * from '@theme/icons/ColorModeIcon';
package/src/index.ts CHANGED
@@ -15,3 +15,4 @@ export * from './globalStyle';
15
15
  export * from './OperationBadge';
16
16
  export * from './TableOfContent';
17
17
  export * from './Profile';
18
+ export * from './ColorModeSwitcher';
@@ -16,6 +16,10 @@ export function useThemeSettings(_: string): RawTheme['settings'] {
16
16
  nextPageLink: { label: 'next page theme settings label' },
17
17
  previousPageLink: { label: 'prev page theme settings label' },
18
18
  },
19
+ colorMode: {
20
+ modes: ['light', 'dark'],
21
+ default: 'light',
22
+ },
19
23
  };
20
24
  }
21
25
 
@@ -17,3 +17,9 @@ sidebar:
17
17
  hide: false
18
18
  footer:
19
19
  hide: false
20
+ colorMode:
21
+ hide: false
22
+ detect: true
23
+ modes:
24
+ - 'light'
25
+ - 'dark'