@redocly/theme 0.59.0-next.0 → 0.59.0-next.1
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/lib/components/Accordion/Accordion.d.ts +12 -0
- package/lib/components/Accordion/Accordion.js +75 -0
- package/lib/components/Accordion/AccordionBody.d.ts +8 -0
- package/lib/components/Accordion/AccordionBody.js +63 -0
- package/lib/components/Accordion/AccordionHeader.d.ts +10 -0
- package/lib/components/Accordion/AccordionHeader.js +37 -0
- package/lib/components/Accordion/AccordionTitle.d.ts +6 -0
- package/lib/components/Accordion/AccordionTitle.js +20 -0
- package/lib/components/Accordion/variables.d.ts +1 -0
- package/lib/components/Accordion/variables.js +59 -0
- package/lib/components/Buttons/AIAssistantButton.d.ts +2 -0
- package/lib/components/Buttons/AIAssistantButton.js +125 -0
- package/lib/components/Buttons/variables.d.ts +1 -0
- package/lib/components/Buttons/variables.dark.d.ts +1 -0
- package/lib/components/Buttons/variables.dark.js +10 -0
- package/lib/components/Buttons/variables.js +51 -0
- package/lib/components/Catalog/Catalog.js +3 -3
- package/lib/components/Catalog/CatalogFilter/CatalogFilter.d.ts +6 -0
- package/lib/components/Catalog/CatalogFilter/CatalogFilter.js +35 -0
- package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.d.ts +6 -0
- package/lib/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.js +142 -0
- package/lib/components/Catalog/CatalogFilter/CatalogFilterContent.d.ts +13 -0
- package/lib/components/Catalog/CatalogFilter/CatalogFilterContent.js +92 -0
- package/lib/components/Catalog/CatalogFilter/CatalogFilterDateRange.d.ts +6 -0
- package/lib/components/Catalog/CatalogFilter/CatalogFilterDateRange.js +111 -0
- package/lib/components/Catalog/CatalogFilter/CatalogFilterSelect.d.ts +6 -0
- package/lib/components/Catalog/CatalogFilter/CatalogFilterSelect.js +116 -0
- package/lib/components/Catalog/CatalogSelector.js +0 -1
- package/lib/components/Catalog/variables.js +0 -1
- package/lib/components/Filter/FilterInput.d.ts +1 -0
- package/lib/components/Filter/FilterInput.js +2 -2
- package/lib/components/Filter/FilterOptions.js +2 -0
- package/lib/components/Filter/variables.js +7 -4
- package/lib/components/Search/SearchAiDialog.js +2 -3
- package/lib/components/Search/SearchAiResponse.js +2 -3
- package/lib/components/Search/SearchDialog.d.ts +2 -1
- package/lib/components/Search/SearchDialog.js +2 -2
- package/lib/components/Tag/variables.dark.js +2 -2
- package/lib/core/styles/dark.js +29 -26
- package/lib/core/styles/global.js +64 -59
- package/lib/core/types/l10n.d.ts +1 -1
- package/lib/icons/RedoclyIcon/RedoclyIcon.d.ts +9 -0
- package/lib/icons/RedoclyIcon/RedoclyIcon.js +27 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +2 -0
- package/lib/layouts/RootLayout.js +6 -1
- package/package.json +1 -1
- package/src/components/Accordion/Accordion.tsx +100 -0
- package/src/components/Accordion/AccordionBody.tsx +65 -0
- package/src/components/Accordion/AccordionHeader.tsx +68 -0
- package/src/components/Accordion/AccordionTitle.tsx +26 -0
- package/src/components/Accordion/variables.ts +56 -0
- package/src/components/Buttons/AIAssistantButton.tsx +141 -0
- package/src/components/Buttons/variables.dark.ts +7 -0
- package/src/components/Buttons/variables.ts +48 -0
- package/src/components/Catalog/Catalog.tsx +3 -2
- package/src/components/Catalog/CatalogFilter/CatalogFilter.tsx +56 -0
- package/src/components/Catalog/CatalogFilter/CatalogFilterCheckboxes.tsx +169 -0
- package/src/components/Catalog/CatalogFilter/CatalogFilterContent.tsx +121 -0
- package/src/components/Catalog/CatalogFilter/CatalogFilterDateRange.tsx +147 -0
- package/src/components/Catalog/CatalogFilter/CatalogFilterSelect.tsx +136 -0
- package/src/components/Catalog/CatalogSelector.tsx +0 -1
- package/src/components/Catalog/variables.ts +0 -1
- package/src/components/Filter/FilterInput.tsx +3 -2
- package/src/components/Filter/FilterOptions.tsx +2 -0
- package/src/components/Filter/variables.ts +7 -4
- package/src/components/Search/SearchAiDialog.tsx +2 -2
- package/src/components/Search/SearchAiResponse.tsx +2 -2
- package/src/components/Search/SearchDialog.tsx +7 -2
- package/src/components/Tag/variables.dark.ts +2 -2
- package/src/core/styles/dark.ts +11 -8
- package/src/core/styles/global.ts +7 -2
- package/src/core/types/l10n.ts +1 -0
- package/src/icons/RedoclyIcon/RedoclyIcon.tsx +44 -0
- package/src/index.ts +2 -0
- package/src/layouts/RootLayout.tsx +6 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { BaseSyntheticEvent, JSX, ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
export type AccordionHeaderProps = {
|
|
7
|
+
isExpandable?: boolean;
|
|
8
|
+
expanded?: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
onClick?: (e: BaseSyntheticEvent) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
type AccordionHeaderWrapperProps = {
|
|
16
|
+
$isExpandable?: boolean;
|
|
17
|
+
$expanded?: boolean;
|
|
18
|
+
$disabled?: boolean;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function AccordionHeader({
|
|
22
|
+
isExpandable,
|
|
23
|
+
expanded,
|
|
24
|
+
disabled,
|
|
25
|
+
children,
|
|
26
|
+
className,
|
|
27
|
+
onClick,
|
|
28
|
+
}: AccordionHeaderProps): JSX.Element {
|
|
29
|
+
return (
|
|
30
|
+
<AccordionHeaderWrapper
|
|
31
|
+
className={className}
|
|
32
|
+
$isExpandable={isExpandable}
|
|
33
|
+
$expanded={expanded}
|
|
34
|
+
$disabled={disabled}
|
|
35
|
+
onClick={onClick}
|
|
36
|
+
data-component-name="Accordion/AccordionHeader"
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</AccordionHeaderWrapper>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const AccordionHeaderWrapper = styled.div<AccordionHeaderWrapperProps>`
|
|
44
|
+
display: flex;
|
|
45
|
+
gap: var(--spacing-xs);
|
|
46
|
+
align-items: center;
|
|
47
|
+
justify-content: space-between;
|
|
48
|
+
min-height: 32px;
|
|
49
|
+
color: var(--accordion-header-text-color);
|
|
50
|
+
|
|
51
|
+
cursor: ${({ $isExpandable = true }) => ($isExpandable ? 'pointer' : 'default')};
|
|
52
|
+
|
|
53
|
+
line-height: var(--accordion-header-line-height);
|
|
54
|
+
font-size: var(--accordion-header-font-size);
|
|
55
|
+
font-family: var(--accordion-header-font-family);
|
|
56
|
+
font-weight: var(--accordion-header-font-weight);
|
|
57
|
+
padding: var(--accordion-header-padding);
|
|
58
|
+
border: var(--accordion-header-border);
|
|
59
|
+
background-color: var(--accordion-header-bg-color);
|
|
60
|
+
border-radius: var(--accordion-border-radius);
|
|
61
|
+
|
|
62
|
+
pointer-events: ${({ $disabled }) => ($disabled ? 'none' : 'auto')};
|
|
63
|
+
|
|
64
|
+
&:hover {
|
|
65
|
+
color: ${({ $isExpandable = true }) =>
|
|
66
|
+
$isExpandable ? 'var(--text-color-primary)' : 'var(--accordion-header-text-color)'};
|
|
67
|
+
}
|
|
68
|
+
`;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { JSX, ReactNode } from 'react';
|
|
5
|
+
|
|
6
|
+
export type AccordionTitleProps = {
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function AccordionTitle({ children, className }: AccordionTitleProps): JSX.Element {
|
|
12
|
+
return (
|
|
13
|
+
<AccordionTitleWrapper className={className} data-component-name="Accordion/AccordionTitle">
|
|
14
|
+
{children}
|
|
15
|
+
</AccordionTitleWrapper>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const AccordionTitleWrapper = styled.span`
|
|
20
|
+
font-weight: var(--accordion-header-font-weight);
|
|
21
|
+
line-height: var(--line-height-base);
|
|
22
|
+
overflow: hidden;
|
|
23
|
+
text-overflow: ellipsis;
|
|
24
|
+
color: var(--accordion-header-text-color);
|
|
25
|
+
flex: 1;
|
|
26
|
+
`;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { css } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const accordion = css`
|
|
4
|
+
/**
|
|
5
|
+
* @tokens Accordion spacing
|
|
6
|
+
* @presenter Spacing
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
--accordion-gap: var(--spacing-base);
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @tokens Accordion common
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
--accordion-border-radius: var(--border-radius); // @presenter BorderRadius
|
|
16
|
+
--accordion-border: none; // @presenter Border
|
|
17
|
+
|
|
18
|
+
--accordion-line-height: var(--line-height-base); // @presenter LineHeight
|
|
19
|
+
--accordion-font-size: var(--font-size-base); // @presenter FontSize
|
|
20
|
+
--accordion-font-family: var(--font-family-base); // @presenter FontFamily
|
|
21
|
+
--accordion-font-weight: var(--font-weight-regular); // @presenter FontWeight
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @tokens Accordion header common
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
--accordion-header-font-family: var(--font-family-base); // @presenter FontFamily
|
|
28
|
+
--accordion-header-font-size: var(--font-size-base); // @presenter FontSize
|
|
29
|
+
--accordion-header-font-weight: var(--font-weight-medium, 500); // @presenter FontWeight
|
|
30
|
+
--accordion-header-text-color: var(--text-color-primary);
|
|
31
|
+
--accordion-header-padding: var(--spacing-sm) var(--spacing-base);
|
|
32
|
+
--accordion-header-line-height: var(--line-height-base); // @presenter LineHeight
|
|
33
|
+
--accordion-header-bg-color: transparent; // @presenter Color
|
|
34
|
+
--accordion-header-border: none; // @presenter Border
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @tokens Accordion body common
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
--accordion-body-font-family: var(--font-family-base); // @presenter FontFamily
|
|
41
|
+
--accordion-body-font-size: var(--font-size-base); // @presenter FontSize
|
|
42
|
+
--accordion-body-font-weight: var(--font-weight-regular); // @presenter FontWeight
|
|
43
|
+
--accordion-body-text-color: var(--text-color-secondary);
|
|
44
|
+
--accordion-body-padding: 0 var(--spacing-xs) var(--spacing-sm);
|
|
45
|
+
--accordion-body-bg-color: transparent; // @presenter Color
|
|
46
|
+
--accordion-body-border: none; // @presenter Border
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @tokens Accordion chevron icon
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
--accordion-chevron-icon-color: var(--text-color-secondary); // @presenter Color
|
|
53
|
+
--accordion-chevron-icon-size: 16px;
|
|
54
|
+
|
|
55
|
+
// @tokens End
|
|
56
|
+
`;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
|
|
5
|
+
import { Button } from '@redocly/theme/components/Button/Button';
|
|
6
|
+
import { SearchDialog } from '@redocly/theme/components/Search/SearchDialog';
|
|
7
|
+
import { useThemeConfig, useThemeHooks } from '@redocly/theme/core/hooks';
|
|
8
|
+
import { ChatIcon } from '@redocly/theme/icons/ChatIcon/ChatIcon';
|
|
9
|
+
import { AiStarsGradientIcon } from '@redocly/theme/icons/AiStarsGradientIcon/AiStarsGradientIcon';
|
|
10
|
+
import { RedoclyIcon } from '@redocly/theme/icons/RedoclyIcon/RedoclyIcon';
|
|
11
|
+
|
|
12
|
+
type AIAssistantButtonIconType = 'chat' | 'sparkles' | 'redocly';
|
|
13
|
+
type AIAssistantButtonType = 'button' | 'icon';
|
|
14
|
+
|
|
15
|
+
interface AIAssistantButtonConfig {
|
|
16
|
+
hide?: boolean;
|
|
17
|
+
inputType?: AIAssistantButtonType;
|
|
18
|
+
inputIcon?: AIAssistantButtonIconType;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const defaultConfig: Required<AIAssistantButtonConfig> = {
|
|
22
|
+
hide: false,
|
|
23
|
+
inputType: 'button',
|
|
24
|
+
inputIcon: 'redocly',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const getIcon = (
|
|
28
|
+
iconType: AIAssistantButtonIconType,
|
|
29
|
+
inputType: AIAssistantButtonType = 'button',
|
|
30
|
+
) => {
|
|
31
|
+
const iconSize =
|
|
32
|
+
inputType === 'icon'
|
|
33
|
+
? 'var(--ai-assistant-button-icon-icon-size)'
|
|
34
|
+
: 'var(--ai-assistant-button-text-icon-size)';
|
|
35
|
+
|
|
36
|
+
const redoclyIcon = (
|
|
37
|
+
<RedoclyIcon size={iconSize} color="var(--ai-assistant-button-redocly-icon-color)" />
|
|
38
|
+
);
|
|
39
|
+
const sparklesIcon = (
|
|
40
|
+
<AiStarsGradientIcon size={iconSize} color="var(--search-ai-button-icon-color)" />
|
|
41
|
+
);
|
|
42
|
+
const chatIcon = <ChatIcon size={iconSize} />;
|
|
43
|
+
|
|
44
|
+
switch (iconType) {
|
|
45
|
+
case 'chat':
|
|
46
|
+
return chatIcon;
|
|
47
|
+
case 'sparkles':
|
|
48
|
+
return sparklesIcon;
|
|
49
|
+
case 'redocly':
|
|
50
|
+
return redoclyIcon;
|
|
51
|
+
default:
|
|
52
|
+
return redoclyIcon;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export function AIAssistantButton() {
|
|
57
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
58
|
+
const themeConfig = useThemeConfig();
|
|
59
|
+
const { useTranslate } = useThemeHooks();
|
|
60
|
+
const { translate } = useTranslate();
|
|
61
|
+
|
|
62
|
+
const buttonConfig = {
|
|
63
|
+
...defaultConfig,
|
|
64
|
+
...(themeConfig.aiAssistant?.trigger ?? {}),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const { hide, inputIcon, inputType } = buttonConfig;
|
|
68
|
+
|
|
69
|
+
if (hide) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const icon = getIcon(inputIcon, inputType);
|
|
74
|
+
const text = translate('aiAssistant.trigger', 'Ask AI');
|
|
75
|
+
|
|
76
|
+
const handleOpen = () => {
|
|
77
|
+
setIsOpen(true);
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const handleClose = () => {
|
|
81
|
+
setIsOpen(false);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<>
|
|
86
|
+
<StyledAIAssistantButton
|
|
87
|
+
variant="outlined"
|
|
88
|
+
size="medium"
|
|
89
|
+
$inputType={inputType}
|
|
90
|
+
onClick={handleOpen}
|
|
91
|
+
aria-label={`AI Assistant button - ${inputIcon}`}
|
|
92
|
+
data-component-name="Buttons/AIAssistantButton"
|
|
93
|
+
>
|
|
94
|
+
{icon}
|
|
95
|
+
{inputType === 'button' && text}
|
|
96
|
+
</StyledAIAssistantButton>
|
|
97
|
+
|
|
98
|
+
{isOpen && <SearchDialog onClose={handleClose} initialMode="ai-dialog" />}
|
|
99
|
+
</>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const StyledAIAssistantButton = styled(Button)<{ $inputType?: AIAssistantButtonType }>`
|
|
104
|
+
position: fixed;
|
|
105
|
+
bottom: var(--ai-assistant-button-bottom);
|
|
106
|
+
right: var(--ai-assistant-button-right);
|
|
107
|
+
${({ $inputType }) =>
|
|
108
|
+
$inputType === 'icon'
|
|
109
|
+
? `
|
|
110
|
+
border-radius: var(--ai-assistant-button-border-radius-icon);
|
|
111
|
+
width: var(--ai-assistant-button-icon-size);
|
|
112
|
+
height: var(--ai-assistant-button-icon-size);
|
|
113
|
+
`
|
|
114
|
+
: `
|
|
115
|
+
border-radius: var(--ai-assistant-button-border-radius-text);
|
|
116
|
+
padding: var(--ai-assistant-button-text-padding);
|
|
117
|
+
height: var(--ai-assistant-button-text-height);
|
|
118
|
+
`}
|
|
119
|
+
min-width: var(--ai-assistant-button-min-width);
|
|
120
|
+
font-size: var(--ai-assistant-button-font-size);
|
|
121
|
+
font-weight: var(--ai-assistant-button-font-weight);
|
|
122
|
+
box-shadow: var(--bg-raised-shadow);
|
|
123
|
+
z-index: var(--ai-assistant-button-z-index);
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
gap: var(--ai-assistant-button-gap);
|
|
128
|
+
background-color: var(--ai-assistant-button-bg-color) !important;
|
|
129
|
+
|
|
130
|
+
transition: var(--ai-assistant-button-transition);
|
|
131
|
+
|
|
132
|
+
&:hover {
|
|
133
|
+
box-shadow: var(--ai-assistant-button-shadow-hover);
|
|
134
|
+
transform: var(--ai-assistant-button-transform-hover);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
&:active {
|
|
138
|
+
transform: var(--bg-raised-shadow);
|
|
139
|
+
box-shadow: var(--ai-assistant-button-shadow-active);
|
|
140
|
+
}
|
|
141
|
+
`;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { css } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const aiAssistantButton = css`
|
|
4
|
+
/**
|
|
5
|
+
* @tokens AI Assistant Button
|
|
6
|
+
* @presenter Color
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/* Button sizing */
|
|
10
|
+
--ai-assistant-button-icon-size: 40px;
|
|
11
|
+
--ai-assistant-button-text-height: 40px;
|
|
12
|
+
--ai-assistant-button-text-padding: var(--spacing-sm) var(--spacing-md);
|
|
13
|
+
--ai-assistant-button-border-radius-icon: 50%;
|
|
14
|
+
--ai-assistant-button-border-radius-text: 1.75rem;
|
|
15
|
+
--ai-assistant-button-min-width: auto;
|
|
16
|
+
|
|
17
|
+
/* Icon sizing */
|
|
18
|
+
--ai-assistant-button-icon-icon-size: 18px;
|
|
19
|
+
--ai-assistant-button-text-icon-size: 20px;
|
|
20
|
+
|
|
21
|
+
/* Positioning */
|
|
22
|
+
--ai-assistant-button-bottom: var(--spacing-xl);
|
|
23
|
+
--ai-assistant-button-right: var(--spacing-xl);
|
|
24
|
+
--ai-assistant-button-z-index: 1000;
|
|
25
|
+
|
|
26
|
+
/* Typography */
|
|
27
|
+
--ai-assistant-button-font-size: var(--font-size-base);
|
|
28
|
+
--ai-assistant-button-font-weight: var(--font-weight-medium);
|
|
29
|
+
--ai-assistant-button-gap: var(--spacing-xs);
|
|
30
|
+
|
|
31
|
+
/* Background color */
|
|
32
|
+
--ai-assistant-button-bg-color: var(--color-static-white);
|
|
33
|
+
|
|
34
|
+
/* Icon colors */
|
|
35
|
+
--ai-assistant-button-redocly-icon-color: #297AFE;
|
|
36
|
+
|
|
37
|
+
/* Transform */
|
|
38
|
+
--ai-assistant-button-transform-hover: none;
|
|
39
|
+
--ai-assistant-button-transform-active: translateY(0);
|
|
40
|
+
|
|
41
|
+
/* Shadow */
|
|
42
|
+
--ai-assistant-button-shadow-hover:
|
|
43
|
+
2px 1px 12px 4px rgba(143, 98, 254, 0.2),
|
|
44
|
+
-3px -2px 24px 0px rgba(41, 122, 254, 0.2);
|
|
45
|
+
|
|
46
|
+
/* Transition */
|
|
47
|
+
--ai-assistant-button-transition: box-shadow 0.3s ease, transform 0.2s ease;
|
|
48
|
+
`;
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { breakpoints } from '@redocly/theme/core/utils';
|
|
11
11
|
import { useThemeHooks } from '@redocly/theme/core/hooks';
|
|
12
12
|
import { H3 } from '@redocly/theme/components/Typography/H3';
|
|
13
|
-
import {
|
|
13
|
+
import { CatalogFilterContent } from '@redocly/theme/components/Catalog/CatalogFilter/CatalogFilterContent';
|
|
14
14
|
import { Sidebar, SidebarHeader } from '@redocly/theme/components/Sidebar/Sidebar';
|
|
15
15
|
import { CatalogSelector } from '@redocly/theme/components/Catalog/CatalogSelector';
|
|
16
16
|
import { SidebarActions } from '@redocly/theme/components/SidebarActions/SidebarActions';
|
|
@@ -89,7 +89,7 @@ export function Catalog(props: CatalogProps): JSX.Element {
|
|
|
89
89
|
)
|
|
90
90
|
}
|
|
91
91
|
menu={
|
|
92
|
-
<
|
|
92
|
+
<CatalogFilterContent
|
|
93
93
|
setFilterTerm={setSearchQuery}
|
|
94
94
|
filters={filters}
|
|
95
95
|
filterTerm={searchQuery}
|
|
@@ -128,6 +128,7 @@ export function Catalog(props: CatalogProps): JSX.Element {
|
|
|
128
128
|
<FilterInput
|
|
129
129
|
value={searchQuery}
|
|
130
130
|
onChange={(updatedTerm) => setSearchQuery(updatedTerm)}
|
|
131
|
+
dataTestId="catalog-search-input"
|
|
131
132
|
/>
|
|
132
133
|
</CatalogSearchInputWrapper>
|
|
133
134
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
import type { JSX } from 'react';
|
|
5
|
+
import type { FilterProps, FilterTypes } from '@redocly/theme/core/types';
|
|
6
|
+
|
|
7
|
+
import { CatalogFilterSelect } from '@redocly/theme/components/Catalog/CatalogFilter/CatalogFilterSelect';
|
|
8
|
+
import { CatalogFilterCheckboxes } from '@redocly/theme/components/Catalog/CatalogFilter/CatalogFilterCheckboxes';
|
|
9
|
+
import { CatalogFilterDateRange } from '@redocly/theme/components/Catalog/CatalogFilter/CatalogFilterDateRange';
|
|
10
|
+
|
|
11
|
+
const filterComponents = {
|
|
12
|
+
select: CatalogFilterSelect,
|
|
13
|
+
'date-range': CatalogFilterDateRange,
|
|
14
|
+
checkboxes: CatalogFilterCheckboxes,
|
|
15
|
+
} as const;
|
|
16
|
+
|
|
17
|
+
export type CatalogFilterProps = FilterProps & {
|
|
18
|
+
className?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function CatalogFilter({
|
|
22
|
+
filter,
|
|
23
|
+
filterValuesCasing,
|
|
24
|
+
showCounter = true,
|
|
25
|
+
className,
|
|
26
|
+
}: CatalogFilterProps): JSX.Element | null {
|
|
27
|
+
if (!filter.parentUsed) return null;
|
|
28
|
+
|
|
29
|
+
const FilterComponent = filterComponents[(filter.type || 'checkboxes') as FilterTypes];
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<CatalogFilterGroup
|
|
33
|
+
className={className}
|
|
34
|
+
data-component-name="Catalog/CatalogFilter"
|
|
35
|
+
key={filter.property + filter.title}
|
|
36
|
+
>
|
|
37
|
+
<FilterComponent
|
|
38
|
+
filter={filter}
|
|
39
|
+
filterValuesCasing={filterValuesCasing}
|
|
40
|
+
showCounter={showCounter}
|
|
41
|
+
/>
|
|
42
|
+
</CatalogFilterGroup>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const CatalogFilterGroup = styled.div`
|
|
47
|
+
padding: var(--filter-group-padding);
|
|
48
|
+
display: flex;
|
|
49
|
+
flex-direction: column;
|
|
50
|
+
gap: var(--filter-group-gap);
|
|
51
|
+
border-bottom: 1px solid var(--catalog-table-border-color);
|
|
52
|
+
|
|
53
|
+
&:first-child {
|
|
54
|
+
border-top: 1px solid var(--catalog-table-border-color);
|
|
55
|
+
}
|
|
56
|
+
`;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { findAll } from 'highlight-words-core';
|
|
4
|
+
|
|
5
|
+
import type { JSX } from 'react';
|
|
6
|
+
import type { FilterProps } from '@redocly/theme/core/types';
|
|
7
|
+
|
|
8
|
+
import { FilterOptions } from '@redocly/theme/components/Filter/FilterOptions';
|
|
9
|
+
import { FilterOption } from '@redocly/theme/components/Filter/FilterOption';
|
|
10
|
+
import { FilterOptionLabel } from '@redocly/theme/components/Filter/FilterOptionLabel';
|
|
11
|
+
import { FilterInput } from '@redocly/theme/components/Filter/FilterInput';
|
|
12
|
+
import { useThemeHooks } from '@redocly/theme/core/hooks';
|
|
13
|
+
import { CheckboxIcon } from '@redocly/theme/icons/CheckboxIcon/CheckboxIcon';
|
|
14
|
+
import { CounterTag } from '@redocly/theme/components/Tags/CounterTag';
|
|
15
|
+
import { changeTextCasing } from '@redocly/theme/core/utils';
|
|
16
|
+
import { Accordion } from '@redocly/theme/components/Accordion/Accordion';
|
|
17
|
+
|
|
18
|
+
export type CatalogFilterCheckboxesProps = FilterProps & {
|
|
19
|
+
className?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function CatalogFilterCheckboxes({
|
|
23
|
+
filter,
|
|
24
|
+
filterValuesCasing,
|
|
25
|
+
showCounter = true,
|
|
26
|
+
className,
|
|
27
|
+
}: CatalogFilterCheckboxesProps): JSX.Element {
|
|
28
|
+
const { useTranslate, useTelemetry } = useThemeHooks();
|
|
29
|
+
const { translate } = useTranslate();
|
|
30
|
+
const telemetry = useTelemetry();
|
|
31
|
+
|
|
32
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
33
|
+
const [isExpanded, setIsExpanded] = useState(false);
|
|
34
|
+
|
|
35
|
+
const filteredOptions = useMemo(() => {
|
|
36
|
+
const options = filter.filteredOptions || filter.options;
|
|
37
|
+
if (!searchTerm.trim()) return options;
|
|
38
|
+
|
|
39
|
+
const lowerSearchTerm = searchTerm.toLowerCase();
|
|
40
|
+
return options.filter(({ value }) => {
|
|
41
|
+
const translatedValue = changeTextCasing(translate(value), filterValuesCasing);
|
|
42
|
+
if (!translatedValue) return false;
|
|
43
|
+
return translatedValue.toLowerCase().includes(lowerSearchTerm);
|
|
44
|
+
});
|
|
45
|
+
}, [searchTerm, filter.filteredOptions, filter.options, translate, filterValuesCasing]);
|
|
46
|
+
|
|
47
|
+
const selectedCount = useMemo(() => {
|
|
48
|
+
if (filter.selectedOptions instanceof Set) {
|
|
49
|
+
return filter.selectedOptions.size;
|
|
50
|
+
}
|
|
51
|
+
return 0;
|
|
52
|
+
}, [filter.selectedOptions]);
|
|
53
|
+
|
|
54
|
+
const highlightText = (text: string): JSX.Element => {
|
|
55
|
+
if (!searchTerm.trim()) {
|
|
56
|
+
return <>{text}</>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const chunks = findAll({
|
|
60
|
+
searchWords: [searchTerm],
|
|
61
|
+
textToHighlight: text,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<>
|
|
66
|
+
{chunks.map((chunk, idx) => {
|
|
67
|
+
const { end, highlight, start } = chunk;
|
|
68
|
+
const chunkText = text.substr(start, end - start);
|
|
69
|
+
if (highlight) {
|
|
70
|
+
return <HighlightedText key={idx}>{chunkText}</HighlightedText>;
|
|
71
|
+
} else {
|
|
72
|
+
return <span key={idx}>{chunkText}</span>;
|
|
73
|
+
}
|
|
74
|
+
})}
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const headerContent = (
|
|
80
|
+
<AccordionHeaderContent>
|
|
81
|
+
<span>{translate(filter.titleTranslationKey, filter.title)}</span>
|
|
82
|
+
{selectedCount > 0 && <CounterTag borderless>{selectedCount}</CounterTag>}
|
|
83
|
+
</AccordionHeaderContent>
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<AccordionWrapper
|
|
88
|
+
className={className}
|
|
89
|
+
$hasSelection={selectedCount > 0 && !isExpanded}
|
|
90
|
+
data-component-name="Catalog/CatalogFilterCheckboxes"
|
|
91
|
+
>
|
|
92
|
+
<Accordion expanded={isExpanded} header={headerContent} onToggle={setIsExpanded}>
|
|
93
|
+
<FilterSearchWrapper>
|
|
94
|
+
<FilterInput value={searchTerm} onChange={setSearchTerm} />
|
|
95
|
+
</FilterSearchWrapper>
|
|
96
|
+
<FilterOptions>
|
|
97
|
+
{filteredOptions.map(({ value, count }) => {
|
|
98
|
+
const id = 'filter--' + filter.property + '--' + value;
|
|
99
|
+
return (
|
|
100
|
+
<FilterCheckboxOption
|
|
101
|
+
key={id}
|
|
102
|
+
role="link"
|
|
103
|
+
onClick={() => {
|
|
104
|
+
filter.toggleOption(value);
|
|
105
|
+
telemetry.sendFilterCheckboxToggledMessage({ id });
|
|
106
|
+
}}
|
|
107
|
+
>
|
|
108
|
+
<CheckboxIcon
|
|
109
|
+
checked={
|
|
110
|
+
filter.selectedOptions instanceof Set
|
|
111
|
+
? filter.selectedOptions.has(value) ||
|
|
112
|
+
filter.selectedOptions.has(value?.toLowerCase())
|
|
113
|
+
: false
|
|
114
|
+
}
|
|
115
|
+
/>
|
|
116
|
+
<FilterOptionLabel data-translation-key={value}>
|
|
117
|
+
{highlightText(changeTextCasing(translate(value), filterValuesCasing) || '')}
|
|
118
|
+
</FilterOptionLabel>
|
|
119
|
+
{showCounter && <CounterTag borderless>{count}</CounterTag>}
|
|
120
|
+
</FilterCheckboxOption>
|
|
121
|
+
);
|
|
122
|
+
})}
|
|
123
|
+
</FilterOptions>
|
|
124
|
+
</Accordion>
|
|
125
|
+
</AccordionWrapper>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const AccordionWrapper = styled.div<{ $hasSelection: boolean }>`
|
|
130
|
+
position: relative;
|
|
131
|
+
border-right: 4px solid transparent;
|
|
132
|
+
|
|
133
|
+
${({ $hasSelection }) =>
|
|
134
|
+
$hasSelection &&
|
|
135
|
+
`
|
|
136
|
+
border-right-color: var(--color-blueberry-6);
|
|
137
|
+
|
|
138
|
+
&::after {
|
|
139
|
+
content: '';
|
|
140
|
+
position: absolute;
|
|
141
|
+
bottom: -1px;
|
|
142
|
+
right: -4px;
|
|
143
|
+
width: 4px;
|
|
144
|
+
height: 1px;
|
|
145
|
+
background-color: var(--color-blueberry-6);
|
|
146
|
+
z-index: 1;
|
|
147
|
+
}
|
|
148
|
+
`}
|
|
149
|
+
`;
|
|
150
|
+
|
|
151
|
+
const AccordionHeaderContent = styled.div`
|
|
152
|
+
display: flex;
|
|
153
|
+
align-items: center;
|
|
154
|
+
gap: var(--spacing-xs);
|
|
155
|
+
width: 100%;
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
const HighlightedText = styled.span`
|
|
159
|
+
background-color: var(--catalog-highlight-bg-color);
|
|
160
|
+
color: var(--catalog-highlight-text-color);
|
|
161
|
+
`;
|
|
162
|
+
|
|
163
|
+
const FilterCheckboxOption = styled(FilterOption)`
|
|
164
|
+
padding-left: var(--filter-option-checkbox-padding-left);
|
|
165
|
+
`;
|
|
166
|
+
|
|
167
|
+
const FilterSearchWrapper = styled.div`
|
|
168
|
+
padding: var(--filter-content-search-padding);
|
|
169
|
+
`;
|