titaned-frontend-library 1.0.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/CHANGELOG.md +43 -0
- package/MEILISEARCH_INTEGRATION.md +261 -0
- package/README.md +236 -0
- package/SEARCH_INTEGRATION_README.md +208 -0
- package/dist/App.d.ts +3 -0
- package/dist/components/ButtonComponent.scss +3 -0
- package/dist/components/Footer.scss +87 -0
- package/dist/components/MainHeader.scss +915 -0
- package/dist/components/Sidebar.scss +170 -0
- package/dist/components/UserMenu.scss +0 -0
- package/dist/components/common/Button/ButtonComponent.d.ts +4 -0
- package/dist/components/common/Footer/Footer.d.ts +4 -0
- package/dist/components/common/Header/MainHeader.d.ts +4 -0
- package/dist/components/common/Header/SimpleSearchContext.d.ts +36 -0
- package/dist/components/common/NavDropdownMenu/NavDropdownMenu.d.ts +11 -0
- package/dist/components/common/SearchHighlight.d.ts +11 -0
- package/dist/components/common/SearchResultItem.d.ts +12 -0
- package/dist/components/common/Sidebar/Sidebar.d.ts +12 -0
- package/dist/components/common/UserMenu/UserMenu.d.ts +15 -0
- package/dist/components/common/menu/MenuComponent.d.ts +4 -0
- package/dist/contexts/SidebarContext.d.ts +6 -0
- package/dist/examples/MeiliSearchUsageExample.d.ts +3 -0
- package/dist/hooks/useIsMobileSize.d.ts +2 -0
- package/dist/hooks/useSidebar.d.ts +1 -0
- package/dist/index.cjs +27 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1867 -0
- package/dist/interfaces/components.d.ts +181 -0
- package/dist/main.d.ts +0 -0
- package/dist/providers/SidebarProvider.d.ts +4 -0
- package/dist/services/meiliSearchService.d.ts +47 -0
- package/dist/utils/searchNavigation.d.ts +43 -0
- package/dist/vite.svg +1 -0
- package/eslint.config.js +28 -0
- package/index.html +13 -0
- package/next.config.ts +35 -0
- package/package.json +53 -0
- package/public/vite.svg +1 -0
- package/src/@edx-frontend-platform-i18n.d.ts +1 -0
- package/src/App.css +66 -0
- package/src/App.tsx +54 -0
- package/src/assets/react.svg +1 -0
- package/src/components/common/Button/ButtonComponent.scss +3 -0
- package/src/components/common/Button/ButtonComponent.tsx +34 -0
- package/src/components/common/Footer/Footer.scss +87 -0
- package/src/components/common/Footer/Footer.tsx +53 -0
- package/src/components/common/Header/MainHeader.scss +915 -0
- package/src/components/common/Header/MainHeader.tsx +701 -0
- package/src/components/common/Header/SimpleSearchContext.tsx +194 -0
- package/src/components/common/NavDropdownMenu/NavDropdownMenu.tsx +58 -0
- package/src/components/common/SearchHighlight.tsx +64 -0
- package/src/components/common/SearchResultItem.tsx +162 -0
- package/src/components/common/Sidebar/Sidebar.scss +170 -0
- package/src/components/common/Sidebar/Sidebar.tsx +137 -0
- package/src/components/common/UserMenu/UserMenu.scss +0 -0
- package/src/components/common/UserMenu/UserMenu.tsx +73 -0
- package/src/components/common/menu/MenuComponent.module.css +12 -0
- package/src/components/common/menu/MenuComponent.tsx +37 -0
- package/src/contexts/SidebarContext.tsx +9 -0
- package/src/declarations.d.ts +10 -0
- package/src/examples/MeiliSearchUsageExample.tsx +102 -0
- package/src/hooks/useIsMobileSize.ts +19 -0
- package/src/hooks/useSidebar.ts +10 -0
- package/src/index.css +68 -0
- package/src/index.ts +21 -0
- package/src/interfaces/components.ts +172 -0
- package/src/main.tsx +11 -0
- package/src/providers/SidebarProvider.tsx +20 -0
- package/src/services/meiliSearchService.ts +233 -0
- package/src/styles/global-overrides.scss +1131 -0
- package/src/utils/searchNavigation.ts +140 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.app.json +26 -0
- package/tsconfig.json +17 -0
- package/tsconfig.node.json +24 -0
- package/vite.config.ts +34 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import React, { createContext, useContext, useState } from 'react';
|
|
2
|
+
import { MeiliSearchResult, MeiliSearchConfig } from '../../../interfaces/components';
|
|
3
|
+
import { createMeiliSearchService } from '../../../services/meiliSearchService';
|
|
4
|
+
|
|
5
|
+
export interface SearchResult {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
type: string;
|
|
9
|
+
url?: string;
|
|
10
|
+
description?: string;
|
|
11
|
+
block_type?: string;
|
|
12
|
+
published?: string;
|
|
13
|
+
_formatted?: {
|
|
14
|
+
display_name?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
published?: string;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SimpleSearchContextData {
|
|
21
|
+
// Search state
|
|
22
|
+
searchKeywords: string;
|
|
23
|
+
setSearchKeywords: React.Dispatch<React.SetStateAction<string>>;
|
|
24
|
+
|
|
25
|
+
// Results state
|
|
26
|
+
searchResults: SearchResult[];
|
|
27
|
+
totalHits: number;
|
|
28
|
+
isLoading: boolean;
|
|
29
|
+
hasError: boolean;
|
|
30
|
+
errorMessage: string | null;
|
|
31
|
+
facets: Record<string, Record<string, number>> | null;
|
|
32
|
+
|
|
33
|
+
// Actions
|
|
34
|
+
performSearch: (query: string) => Promise<void>;
|
|
35
|
+
clearSearch: () => void;
|
|
36
|
+
|
|
37
|
+
// Optional: pagination
|
|
38
|
+
hasNextPage: boolean;
|
|
39
|
+
fetchNextPage: () => void;
|
|
40
|
+
isFetchingNextPage: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SimpleSearchContext = createContext<SimpleSearchContextData | undefined>(undefined);
|
|
44
|
+
|
|
45
|
+
export const SimpleSearchProvider: React.FC<{
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
meiliSearchConfig: MeiliSearchConfig;
|
|
48
|
+
}> = ({ children, meiliSearchConfig }) => {
|
|
49
|
+
const [searchKeywords, setSearchKeywords] = useState('');
|
|
50
|
+
const [searchResults, setSearchResults] = useState<SearchResult[]>([]);
|
|
51
|
+
const [totalHits, setTotalHits] = useState(0);
|
|
52
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
53
|
+
const [hasError, setHasError] = useState(false);
|
|
54
|
+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
|
55
|
+
const [currentOffset, setCurrentOffset] = useState(0);
|
|
56
|
+
const [hasNextPage, setHasNextPage] = useState(false);
|
|
57
|
+
const [isFetchingNextPage, setIsFetchingNextPage] = useState(false);
|
|
58
|
+
const [facets, setFacets] = useState<Record<string, Record<string, number>> | null>(null);
|
|
59
|
+
|
|
60
|
+
const searchService = createMeiliSearchService(meiliSearchConfig);
|
|
61
|
+
|
|
62
|
+
const performSearch = async (query: string) => {
|
|
63
|
+
if (!query.trim()) {
|
|
64
|
+
clearSearch();
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
setIsLoading(true);
|
|
69
|
+
setHasError(false);
|
|
70
|
+
setErrorMessage(null);
|
|
71
|
+
setCurrentOffset(0);
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
const result = await searchService.search(query, {
|
|
75
|
+
limit: 20,
|
|
76
|
+
offset: 0
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Convert MeiliSearch results to SearchResult format
|
|
80
|
+
const results: SearchResult[] = result.hits.map((hit: MeiliSearchResult) => ({
|
|
81
|
+
id: hit.id,
|
|
82
|
+
title: hit._formatted?.display_name || hit.display_name,
|
|
83
|
+
type: hit.type,
|
|
84
|
+
url: hit.url,
|
|
85
|
+
description: hit._formatted?.description || hit.description,
|
|
86
|
+
block_type: hit.block_type,
|
|
87
|
+
published: hit._formatted?.published || hit.published,
|
|
88
|
+
_formatted: hit._formatted
|
|
89
|
+
}));
|
|
90
|
+
|
|
91
|
+
const total = result.totalHits;
|
|
92
|
+
const hasMore = results.length === 20 && total > results.length;
|
|
93
|
+
|
|
94
|
+
setSearchResults(results);
|
|
95
|
+
setTotalHits(total);
|
|
96
|
+
setHasNextPage(hasMore);
|
|
97
|
+
setFacets(result.facets || null);
|
|
98
|
+
|
|
99
|
+
} catch (error) {
|
|
100
|
+
console.error('Search error:', error);
|
|
101
|
+
setHasError(true);
|
|
102
|
+
setErrorMessage(error instanceof Error ? error.message : 'Search failed');
|
|
103
|
+
setSearchResults([]);
|
|
104
|
+
setTotalHits(0);
|
|
105
|
+
setHasNextPage(false);
|
|
106
|
+
setFacets(null);
|
|
107
|
+
} finally {
|
|
108
|
+
setIsLoading(false);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const fetchNextPage = async () => {
|
|
113
|
+
if (!hasNextPage || isFetchingNextPage) return;
|
|
114
|
+
|
|
115
|
+
setIsFetchingNextPage(true);
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const nextOffset = currentOffset + 20;
|
|
119
|
+
const result = await searchService.simpleSearch(searchKeywords, {
|
|
120
|
+
limit: 20,
|
|
121
|
+
offset: nextOffset
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Convert MeiliSearch results to SearchResult format
|
|
125
|
+
const newResults: SearchResult[] = result.hits.map((hit: MeiliSearchResult) => ({
|
|
126
|
+
id: hit.id,
|
|
127
|
+
title: hit._formatted?.display_name || hit.display_name,
|
|
128
|
+
type: hit.type,
|
|
129
|
+
url: hit.url,
|
|
130
|
+
description: hit._formatted?.description || hit.description,
|
|
131
|
+
block_type: hit.block_type,
|
|
132
|
+
published: hit._formatted?.published || hit.published,
|
|
133
|
+
_formatted: hit._formatted
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
const total = result.totalHits;
|
|
137
|
+
const hasMore = newResults.length === 20 && total > (searchResults.length + newResults.length);
|
|
138
|
+
|
|
139
|
+
setSearchResults(prev => [...prev, ...newResults]);
|
|
140
|
+
setTotalHits(total);
|
|
141
|
+
setHasNextPage(hasMore);
|
|
142
|
+
setCurrentOffset(nextOffset);
|
|
143
|
+
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('Error fetching next page:', error);
|
|
146
|
+
setHasError(true);
|
|
147
|
+
setErrorMessage(error instanceof Error ? error.message : 'Failed to load more results');
|
|
148
|
+
} finally {
|
|
149
|
+
setIsFetchingNextPage(false);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const clearSearch = () => {
|
|
154
|
+
setSearchKeywords('');
|
|
155
|
+
setSearchResults([]);
|
|
156
|
+
setTotalHits(0);
|
|
157
|
+
setHasError(false);
|
|
158
|
+
setErrorMessage(null);
|
|
159
|
+
setCurrentOffset(0);
|
|
160
|
+
setHasNextPage(false);
|
|
161
|
+
setFacets(null);
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const contextValue: SimpleSearchContextData = {
|
|
165
|
+
searchKeywords,
|
|
166
|
+
setSearchKeywords,
|
|
167
|
+
searchResults,
|
|
168
|
+
totalHits,
|
|
169
|
+
isLoading,
|
|
170
|
+
hasError,
|
|
171
|
+
errorMessage,
|
|
172
|
+
facets,
|
|
173
|
+
performSearch,
|
|
174
|
+
clearSearch,
|
|
175
|
+
hasNextPage,
|
|
176
|
+
fetchNextPage,
|
|
177
|
+
isFetchingNextPage,
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
return (
|
|
181
|
+
<SimpleSearchContext.Provider value={contextValue}>
|
|
182
|
+
{children}
|
|
183
|
+
</SimpleSearchContext.Provider>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const useSimpleSearch = () => {
|
|
188
|
+
const context = useContext(SimpleSearchContext);
|
|
189
|
+
if (context === undefined) {
|
|
190
|
+
throw new Error('useSimpleSearch must be used within a SimpleSearchProvider');
|
|
191
|
+
}
|
|
192
|
+
return context;
|
|
193
|
+
};
|
|
194
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
// import PropTypes from 'prop-types';
|
|
3
|
+
import {
|
|
4
|
+
Dropdown,
|
|
5
|
+
DropdownButton,
|
|
6
|
+
} from '@openedx/paragon';
|
|
7
|
+
|
|
8
|
+
interface NavDropdownMenuProps {
|
|
9
|
+
id: string;
|
|
10
|
+
buttonTitle: React.ReactNode;
|
|
11
|
+
items: {
|
|
12
|
+
href: string;
|
|
13
|
+
title: string;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const NavDropdownMenu: React.FC<NavDropdownMenuProps> = ({
|
|
18
|
+
id,
|
|
19
|
+
buttonTitle,
|
|
20
|
+
items,
|
|
21
|
+
}) => {
|
|
22
|
+
console.log('NavDropdownMenu Props:', items)
|
|
23
|
+
return (
|
|
24
|
+
<DropdownButton
|
|
25
|
+
id={id}
|
|
26
|
+
title={buttonTitle}
|
|
27
|
+
variant="tertiary"
|
|
28
|
+
className="mr-2"
|
|
29
|
+
>
|
|
30
|
+
{items.map(item => (
|
|
31
|
+
<Dropdown.Item
|
|
32
|
+
key={`${item.title}-dropdown-item`}
|
|
33
|
+
href={item.href}
|
|
34
|
+
as="a"
|
|
35
|
+
className="small"
|
|
36
|
+
>
|
|
37
|
+
{/* <a href={item.href} className="dropdown-item"> */}
|
|
38
|
+
{item.title}
|
|
39
|
+
{/* </a> */}
|
|
40
|
+
</Dropdown.Item>
|
|
41
|
+
|
|
42
|
+
))}
|
|
43
|
+
</DropdownButton>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// NavDropdownMenu.propTypes = {
|
|
48
|
+
// id: PropTypes.string.isRequired,
|
|
49
|
+
// buttonTitle: PropTypes.node.isRequired,
|
|
50
|
+
// items: PropTypes.arrayOf(
|
|
51
|
+
// PropTypes.shape({
|
|
52
|
+
// href: PropTypes.string.isRequired,
|
|
53
|
+
// title: PropTypes.node.isRequired,
|
|
54
|
+
// }).isRequired
|
|
55
|
+
// ).isRequired,
|
|
56
|
+
// };
|
|
57
|
+
|
|
58
|
+
export default NavDropdownMenu;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
interface HighlightProps {
|
|
4
|
+
text: string;
|
|
5
|
+
highlightTag?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Component to highlight search terms in text
|
|
10
|
+
* Handles MeiliSearch highlight tags like __meili-highlight__term__/meili-highlight__
|
|
11
|
+
*/
|
|
12
|
+
const Highlight: React.FC<HighlightProps> = ({
|
|
13
|
+
text,
|
|
14
|
+
highlightTag = '__meili-highlight__'
|
|
15
|
+
}) => {
|
|
16
|
+
if (!text) return null;
|
|
17
|
+
|
|
18
|
+
const endTag = `__/meili-highlight__`;
|
|
19
|
+
|
|
20
|
+
// Split text by highlight tags and create highlighted segments
|
|
21
|
+
const parts = text.split(highlightTag);
|
|
22
|
+
const processedParts: React.ReactNode[] = [];
|
|
23
|
+
|
|
24
|
+
parts.forEach((part, index) => {
|
|
25
|
+
if (index === 0) {
|
|
26
|
+
// First part (before any highlights)
|
|
27
|
+
if (part) {
|
|
28
|
+
processedParts.push(part);
|
|
29
|
+
}
|
|
30
|
+
} else {
|
|
31
|
+
// Check if this part contains the end tag
|
|
32
|
+
const endIndex = part.indexOf(endTag);
|
|
33
|
+
|
|
34
|
+
if (endIndex !== -1) {
|
|
35
|
+
// This part contains a highlighted segment
|
|
36
|
+
const highlightedText = part.substring(0, endIndex);
|
|
37
|
+
const remainingText = part.substring(endIndex + endTag.length);
|
|
38
|
+
|
|
39
|
+
if (highlightedText) {
|
|
40
|
+
processedParts.push(
|
|
41
|
+
<mark key={`highlight-${index}`} className="search-highlight">
|
|
42
|
+
{highlightedText}
|
|
43
|
+
</mark>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (remainingText) {
|
|
48
|
+
processedParts.push(remainingText);
|
|
49
|
+
}
|
|
50
|
+
} else {
|
|
51
|
+
// This part doesn't contain end tag, treat as highlighted
|
|
52
|
+
processedParts.push(
|
|
53
|
+
<mark key={`highlight-${index}`} className="search-highlight">
|
|
54
|
+
{part}
|
|
55
|
+
</mark>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return <>{processedParts}</>;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export default Highlight;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Icon, IconButton } from '@openedx/paragon';
|
|
3
|
+
import { OpenInNew } from '@openedx/paragon/icons';
|
|
4
|
+
import { MeiliSearchResult } from '../../interfaces/components';
|
|
5
|
+
import { navigateToSearchHit, getSearchHitUrl } from '../../utils/searchNavigation';
|
|
6
|
+
import Highlight from './SearchHighlight';
|
|
7
|
+
|
|
8
|
+
interface SearchResultItemProps {
|
|
9
|
+
result: MeiliSearchResult;
|
|
10
|
+
onNavigate?: (url: string) => void;
|
|
11
|
+
baseUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get icon for block type
|
|
16
|
+
*/
|
|
17
|
+
function getBlockTypeIcon(blockType: string): string {
|
|
18
|
+
const iconMap: Record<string, string> = {
|
|
19
|
+
'html': 'text',
|
|
20
|
+
'video': 'video',
|
|
21
|
+
'problem': 'quiz',
|
|
22
|
+
'vertical': 'folder',
|
|
23
|
+
'sequential': 'folder',
|
|
24
|
+
'chapter': 'folder',
|
|
25
|
+
'about': 'info',
|
|
26
|
+
'static_tab': 'text',
|
|
27
|
+
'openassessment': 'quiz',
|
|
28
|
+
'lti_consumer': 'link',
|
|
29
|
+
'pdf': 'file',
|
|
30
|
+
'scorm': 'file',
|
|
31
|
+
'itembank': 'library',
|
|
32
|
+
'library_content': 'library',
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return iconMap[blockType] || 'text';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Single search result item component
|
|
40
|
+
*/
|
|
41
|
+
const SearchResultItem: React.FC<SearchResultItemProps> = ({
|
|
42
|
+
result,
|
|
43
|
+
onNavigate,
|
|
44
|
+
baseUrl
|
|
45
|
+
}) => {
|
|
46
|
+
const handleClick = (e: React.MouseEvent | React.KeyboardEvent) => {
|
|
47
|
+
e.preventDefault();
|
|
48
|
+
e.stopPropagation();
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
// Map MeiliSearch result to SearchHit interface
|
|
52
|
+
const searchHit = {
|
|
53
|
+
id: result.id,
|
|
54
|
+
blockType: result.block_type || result.type,
|
|
55
|
+
contextKey: result.context_key, // Use the actual context_key from MeiliSearch
|
|
56
|
+
usageKey: result.usage_key, // Use the actual usage_key from MeiliSearch
|
|
57
|
+
breadcrumbs: result.breadcrumbs?.map(bc => ({
|
|
58
|
+
displayName: bc.display_name,
|
|
59
|
+
usageKey: bc.usage_key
|
|
60
|
+
})) || [],
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
// If we have a direct URL, use it
|
|
65
|
+
if (result.url) {
|
|
66
|
+
if (onNavigate) {
|
|
67
|
+
onNavigate(result.url);
|
|
68
|
+
} else {
|
|
69
|
+
window.location.href = result.url;
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Otherwise try to generate URL
|
|
75
|
+
const url = navigateToSearchHit(searchHit, baseUrl, onNavigate);
|
|
76
|
+
|
|
77
|
+
// If URL generation failed, try a simple fallback
|
|
78
|
+
if (url === '#') {
|
|
79
|
+
// You can customize this fallback based on your needs
|
|
80
|
+
if (onNavigate) {
|
|
81
|
+
onNavigate(`/search-result/${result.id}`);
|
|
82
|
+
} else {
|
|
83
|
+
window.location.href = `/search-result/${result.id}`;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const handleOpenInNewWindow = (e: React.MouseEvent) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
e.stopPropagation();
|
|
91
|
+
|
|
92
|
+
const searchHit = {
|
|
93
|
+
id: result.id,
|
|
94
|
+
blockType: result.block_type || result.type,
|
|
95
|
+
contextKey: result.context_key,
|
|
96
|
+
usageKey: result.usage_key,
|
|
97
|
+
breadcrumbs: result.breadcrumbs?.map(bc => ({
|
|
98
|
+
displayName: bc.display_name,
|
|
99
|
+
usageKey: bc.usage_key
|
|
100
|
+
})) || [],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const url = getSearchHitUrl(searchHit, baseUrl);
|
|
104
|
+
if (url && url !== '#') {
|
|
105
|
+
window.open(url, '_blank');
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const displayName = result._formatted?.display_name || result.display_name;
|
|
110
|
+
const description = result._formatted?.description || result.description;
|
|
111
|
+
const blockType = result.block_type || result.type;
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
className="search-result-item"
|
|
116
|
+
onClick={handleClick}
|
|
117
|
+
role="button"
|
|
118
|
+
tabIndex={0}
|
|
119
|
+
onKeyDown={(e) => {
|
|
120
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
121
|
+
handleClick(e);
|
|
122
|
+
}
|
|
123
|
+
}}
|
|
124
|
+
>
|
|
125
|
+
<div className="search-result-content">
|
|
126
|
+
<div className="search-result-icon">
|
|
127
|
+
<Icon src={getBlockTypeIcon(blockType) as any} />
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
<div className="search-result-text">
|
|
131
|
+
<div className="search-result-title">
|
|
132
|
+
<Highlight text={displayName} />
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{description && (
|
|
136
|
+
<div className="search-result-description">
|
|
137
|
+
<Highlight text={description} />
|
|
138
|
+
</div>
|
|
139
|
+
)}
|
|
140
|
+
|
|
141
|
+
{result.breadcrumbs && result.breadcrumbs.length > 0 && (
|
|
142
|
+
<div className="search-result-breadcrumbs">
|
|
143
|
+
{result.breadcrumbs.map(bc => bc.display_name).join(' / ')}
|
|
144
|
+
</div>
|
|
145
|
+
)}
|
|
146
|
+
</div>
|
|
147
|
+
|
|
148
|
+
<div className="search-result-actions">
|
|
149
|
+
<IconButton
|
|
150
|
+
src={OpenInNew}
|
|
151
|
+
iconAs={Icon}
|
|
152
|
+
onClick={handleOpenInNewWindow}
|
|
153
|
+
alt="Open in new window"
|
|
154
|
+
size="sm"
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export default SearchResultItem;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
.sidebar {
|
|
2
|
+
width: 200px;
|
|
3
|
+
min-height: 100vh;
|
|
4
|
+
background-color: var(--bg-surface);
|
|
5
|
+
border-right: 1px solid var(--border-color);
|
|
6
|
+
position: relative;
|
|
7
|
+
// padding: 8px 4px;
|
|
8
|
+
transition: all 0.3s ease;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
padding: 0;
|
|
12
|
+
z-index: 101;
|
|
13
|
+
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
14
|
+
overflow: visible;
|
|
15
|
+
|
|
16
|
+
// Collapsed state styles
|
|
17
|
+
&.collapsed {
|
|
18
|
+
width: 80px;
|
|
19
|
+
min-width: 50px;
|
|
20
|
+
padding: 0;
|
|
21
|
+
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
22
|
+
.sidebar-btn {
|
|
23
|
+
padding: 14px 10px;
|
|
24
|
+
padding-right: 0px;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
position: relative;
|
|
27
|
+
z-index: 25;
|
|
28
|
+
.btn-content {
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 0;
|
|
32
|
+
.btn-label {
|
|
33
|
+
display: none;
|
|
34
|
+
opacity: 0;
|
|
35
|
+
max-width: 0;
|
|
36
|
+
margin-left: 0;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// &:hover {
|
|
41
|
+
// .btn-label {
|
|
42
|
+
// /* Removed old CSS tooltip styles */
|
|
43
|
+
// }
|
|
44
|
+
// }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Styles for non-standard routes
|
|
49
|
+
&.non-standard-route {
|
|
50
|
+
// When NOT collapsed (i.e., expanded state) on a non-standard route
|
|
51
|
+
&:not(.collapsed) {
|
|
52
|
+
width: 200px; // Expand to 200px
|
|
53
|
+
position: fixed; // Overlay content
|
|
54
|
+
// height: 100vh;
|
|
55
|
+
top: 6rem;
|
|
56
|
+
left: 0;
|
|
57
|
+
z-index: 1000; // Ensure it's above other content
|
|
58
|
+
transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.3s ease; // Animate transform for slide effect
|
|
59
|
+
transform: translateX(0); // Ensure it's visible
|
|
60
|
+
margin-right: 80px;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.sidebar-buttons {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
gap: 4px;
|
|
69
|
+
margin-top: 1.5rem;
|
|
70
|
+
margin-right: 1rem;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.sidebar-btn {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
justify-content: flex-start;
|
|
77
|
+
width: 100%;
|
|
78
|
+
padding: 14px 10px;
|
|
79
|
+
border-radius: 0 32px 32px 0;
|
|
80
|
+
background: transparent;
|
|
81
|
+
color: var(--text-secondary);
|
|
82
|
+
font-weight: 500;
|
|
83
|
+
font-size: 16px;
|
|
84
|
+
border: none;
|
|
85
|
+
transition: background 0.2s, color 0.2s;
|
|
86
|
+
margin: 2px 0;
|
|
87
|
+
text-align: left;
|
|
88
|
+
|
|
89
|
+
.btn-content {
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: row;
|
|
92
|
+
align-items: center;
|
|
93
|
+
width: 100%;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.icon-container {
|
|
97
|
+
font-size: 22px;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
min-width: 28px;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.btn-label {
|
|
105
|
+
font-size: 14px;
|
|
106
|
+
font-weight: 500;
|
|
107
|
+
color: inherit;
|
|
108
|
+
white-space: nowrap;
|
|
109
|
+
transition: opacity 0.2s, max-width 0.2s, margin 0.2s;
|
|
110
|
+
opacity: 1;
|
|
111
|
+
max-width: 200px;
|
|
112
|
+
margin-left: 0.5rem;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.pgn-btn-active {
|
|
118
|
+
background: var(--primary) !important;
|
|
119
|
+
color: var(--text-white) !important;
|
|
120
|
+
|
|
121
|
+
&:hover {
|
|
122
|
+
background-color: var(--primary-light) !important;
|
|
123
|
+
color: var(--primary) !important;
|
|
124
|
+
|
|
125
|
+
.icon-container,
|
|
126
|
+
.btn-label {
|
|
127
|
+
color: var(--primary) !important;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.icon-container,
|
|
132
|
+
.btn-label {
|
|
133
|
+
color: var(--text-white) !important;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.sidebar-btn {
|
|
138
|
+
&.pgn-btn-inactive {
|
|
139
|
+
color: var(--text-secondary) !important;
|
|
140
|
+
|
|
141
|
+
&:hover {
|
|
142
|
+
color: var(--primary) !important;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.sidebar-extra-space {
|
|
148
|
+
width: 80px;
|
|
149
|
+
// margin-right: 80px ; // Adjust margin for full width panel
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// RTL-specific tooltip adjustments
|
|
153
|
+
[dir="rtl"] .sidebar {
|
|
154
|
+
.tooltip {
|
|
155
|
+
// Ensure tooltip appears on the left side for RTL
|
|
156
|
+
&.bs-tooltip-left {
|
|
157
|
+
margin-right: 8px;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Additional RTL adjustments for collapsed state
|
|
162
|
+
&.collapsed {
|
|
163
|
+
.tooltip {
|
|
164
|
+
// Fine-tune positioning for RTL collapsed sidebar
|
|
165
|
+
&.bs-tooltip-left {
|
|
166
|
+
transform: translateX(-4px);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|