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.
Files changed (77) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/MEILISEARCH_INTEGRATION.md +261 -0
  3. package/README.md +236 -0
  4. package/SEARCH_INTEGRATION_README.md +208 -0
  5. package/dist/App.d.ts +3 -0
  6. package/dist/components/ButtonComponent.scss +3 -0
  7. package/dist/components/Footer.scss +87 -0
  8. package/dist/components/MainHeader.scss +915 -0
  9. package/dist/components/Sidebar.scss +170 -0
  10. package/dist/components/UserMenu.scss +0 -0
  11. package/dist/components/common/Button/ButtonComponent.d.ts +4 -0
  12. package/dist/components/common/Footer/Footer.d.ts +4 -0
  13. package/dist/components/common/Header/MainHeader.d.ts +4 -0
  14. package/dist/components/common/Header/SimpleSearchContext.d.ts +36 -0
  15. package/dist/components/common/NavDropdownMenu/NavDropdownMenu.d.ts +11 -0
  16. package/dist/components/common/SearchHighlight.d.ts +11 -0
  17. package/dist/components/common/SearchResultItem.d.ts +12 -0
  18. package/dist/components/common/Sidebar/Sidebar.d.ts +12 -0
  19. package/dist/components/common/UserMenu/UserMenu.d.ts +15 -0
  20. package/dist/components/common/menu/MenuComponent.d.ts +4 -0
  21. package/dist/contexts/SidebarContext.d.ts +6 -0
  22. package/dist/examples/MeiliSearchUsageExample.d.ts +3 -0
  23. package/dist/hooks/useIsMobileSize.d.ts +2 -0
  24. package/dist/hooks/useSidebar.d.ts +1 -0
  25. package/dist/index.cjs +27 -0
  26. package/dist/index.css +1 -0
  27. package/dist/index.d.ts +8 -0
  28. package/dist/index.js +1867 -0
  29. package/dist/interfaces/components.d.ts +181 -0
  30. package/dist/main.d.ts +0 -0
  31. package/dist/providers/SidebarProvider.d.ts +4 -0
  32. package/dist/services/meiliSearchService.d.ts +47 -0
  33. package/dist/utils/searchNavigation.d.ts +43 -0
  34. package/dist/vite.svg +1 -0
  35. package/eslint.config.js +28 -0
  36. package/index.html +13 -0
  37. package/next.config.ts +35 -0
  38. package/package.json +53 -0
  39. package/public/vite.svg +1 -0
  40. package/src/@edx-frontend-platform-i18n.d.ts +1 -0
  41. package/src/App.css +66 -0
  42. package/src/App.tsx +54 -0
  43. package/src/assets/react.svg +1 -0
  44. package/src/components/common/Button/ButtonComponent.scss +3 -0
  45. package/src/components/common/Button/ButtonComponent.tsx +34 -0
  46. package/src/components/common/Footer/Footer.scss +87 -0
  47. package/src/components/common/Footer/Footer.tsx +53 -0
  48. package/src/components/common/Header/MainHeader.scss +915 -0
  49. package/src/components/common/Header/MainHeader.tsx +701 -0
  50. package/src/components/common/Header/SimpleSearchContext.tsx +194 -0
  51. package/src/components/common/NavDropdownMenu/NavDropdownMenu.tsx +58 -0
  52. package/src/components/common/SearchHighlight.tsx +64 -0
  53. package/src/components/common/SearchResultItem.tsx +162 -0
  54. package/src/components/common/Sidebar/Sidebar.scss +170 -0
  55. package/src/components/common/Sidebar/Sidebar.tsx +137 -0
  56. package/src/components/common/UserMenu/UserMenu.scss +0 -0
  57. package/src/components/common/UserMenu/UserMenu.tsx +73 -0
  58. package/src/components/common/menu/MenuComponent.module.css +12 -0
  59. package/src/components/common/menu/MenuComponent.tsx +37 -0
  60. package/src/contexts/SidebarContext.tsx +9 -0
  61. package/src/declarations.d.ts +10 -0
  62. package/src/examples/MeiliSearchUsageExample.tsx +102 -0
  63. package/src/hooks/useIsMobileSize.ts +19 -0
  64. package/src/hooks/useSidebar.ts +10 -0
  65. package/src/index.css +68 -0
  66. package/src/index.ts +21 -0
  67. package/src/interfaces/components.ts +172 -0
  68. package/src/main.tsx +11 -0
  69. package/src/providers/SidebarProvider.tsx +20 -0
  70. package/src/services/meiliSearchService.ts +233 -0
  71. package/src/styles/global-overrides.scss +1131 -0
  72. package/src/utils/searchNavigation.ts +140 -0
  73. package/src/vite-env.d.ts +1 -0
  74. package/tsconfig.app.json +26 -0
  75. package/tsconfig.json +17 -0
  76. package/tsconfig.node.json +24 -0
  77. 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
+ }