diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f63339c7..a9658a64f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: run: npm run build - name: Run Coverage - uses: codecov/codecov-action@75cd11691c0faa626561e295848008c8a7dddffe # v5.5.4 + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 with: token: ${{ secrets.CODECOV_TOKEN }} fail_ci_if_error: true diff --git a/package-lock.json b/package-lock.json index 2a8d38c08..f27f6651e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@openedx/frontend-plugin-framework": "^1.7.0", "@openedx/paragon": "^23.4.5", + "@tabler/icons-react": "^3.44.0", "@tanstack/react-query": "^5.90.16", "classnames": "^2.3.1", "core-js": "3.49.0", @@ -5989,6 +5990,32 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@tabler/icons": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/@tabler/icons/-/icons-3.44.0.tgz", + "integrity": "sha512-Wn0AOZG9sg0L+bjfMqq4eNhC6pQjIrk94LvvWYNYkY8KH8wC3YILRzQlrnVJc4FUeMxH/AK97QsYCX35H3LndA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + } + }, + "node_modules/@tabler/icons-react": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/@tabler/icons-react/-/icons-react-3.44.0.tgz", + "integrity": "sha512-8+rvzBbVm/1Z3sG3x7GUNAaxIKxwgz8xaMhRs23nrCnMTKRFAhEC+82zAIFeAA0seXdrAGX5HFCkaLpGK2rVHg==", + "license": "MIT", + "dependencies": { + "@tabler/icons": "3.44.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/codecalm" + }, + "peerDependencies": { + "react": ">= 16" + } + }, "node_modules/@tanstack/query-core": { "version": "5.96.2", "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.96.2.tgz", diff --git a/package.json b/package.json index 5cf80d543..a4b712646 100755 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@openedx/frontend-plugin-framework": "^1.7.0", "@openedx/paragon": "^23.4.5", + "@tabler/icons-react": "^3.44.0", "@tanstack/react-query": "^5.90.16", "classnames": "^2.3.1", "core-js": "3.49.0", diff --git a/src/App.scss b/src/App.scss index bd72d6387..bd8d125ee 100755 --- a/src/App.scss +++ b/src/App.scss @@ -39,8 +39,7 @@ $input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get up display: block; box-sizing: content-box; position: relative; - top: 0.1em; - height: 1.75rem; + height: 37px; margin-right: 1rem; img { display: block; diff --git a/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx b/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx index f13177ffa..c1696da61 100644 --- a/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx +++ b/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx @@ -1,77 +1,169 @@ +import React from 'react'; +import { + IconHome, + IconBook, + IconClockHour3, + IconHelpHexagon, + IconBell, +} from '@tabler/icons-react'; import { getConfig } from '@edx/frontend-platform'; import urls from 'data/services/lms/urls'; import messages from './messages'; +const ICON_MAP = { + Home: IconHome, + LibraryBooks: IconBook, + ClockHour3: IconClockHour3, + Search: ({ size = 18, className, ...props }) => ( + + ), + HelpHexagon: IconHelpHexagon, +}; + +const NavItem = ({ icon: IconComponent, label }) => ( + + + {label} + +); + const getLearnerHeaderMenu = ( formatMessage, courseSearchUrl, authenticatedUser, exploreCoursesClick, -) => ({ - mainMenu: [ - { - type: 'item', - href: '/', - content: formatMessage(messages.course), - isActive: true, - }, - ...(getConfig().ENABLE_PROGRAMS ? [{ - type: 'item', - href: `${urls.programsUrl()}`, - content: formatMessage(messages.program), - }] : []), - ...(!getConfig().NON_BROWSABLE_COURSES ? [{ +) => { + const BASE_URL = getConfig().LMS_BASE_URL; + const searchCatalogUrl = getConfig().SEARCH_CATALOG_URL; + const configNavLinks = getConfig().HEADER_NAV_LINKS; + const isLinkActive = (link) => { + if (link.url === '/dashboard' || link.url.endsWith('/dashboard')) { + return getConfig().APP_ID === 'learner-dashboard'; + } + const linkPath = link.url.startsWith('http') + ? new URL(link.url).pathname + : link.url; + return window.location.pathname === linkPath; + }; + + const mainMenu = configNavLinks + ? configNavLinks.map((link) => ({ type: 'item', - href: `${urls.baseAppUrl(courseSearchUrl)}`, - content: formatMessage(messages.discoverNew), - onClick: (e) => { - exploreCoursesClick(e); + href: link.url.startsWith('http') ? link.url : `${BASE_URL}${link.url}`, + isActive: isLinkActive(link), + content: ( + + ), + })) + : [ + { + type: 'item', + href: '/', + content: formatMessage(messages.course), + isActive: true, }, - }] - : []), - ], - secondaryMenu: [ - ...(getConfig().SUPPORT_URL ? [{ - type: 'item', - href: `${getConfig().SUPPORT_URL}`, - content: formatMessage(messages.help), - }] : []), - ], - userMenu: [ - { - heading: '', - items: [ - { - type: 'item', - href: `${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser?.username}`, - content: formatMessage(messages.profile), + ...(getConfig().ENABLE_PROGRAMS ? [{ + type: 'item', + href: `${urls.programsUrl()}`, + content: formatMessage(messages.program), + }] : []), + ...(!getConfig().NON_BROWSABLE_COURSES ? [{ + type: 'item', + href: `${urls.baseAppUrl(courseSearchUrl)}`, + content: formatMessage(messages.discoverNew), + onClick: (e) => { + exploreCoursesClick(e); }, - { - type: 'item', - href: `${getConfig().ACCOUNT_SETTINGS_URL}`, - content: formatMessage(messages.account), - }, - ...(getConfig().ORDER_HISTORY_URL ? [{ - type: 'item', - href: getConfig().ORDER_HISTORY_URL, - content: formatMessage(messages.orderHistory), - }] : []), - ], - }, - { - heading: '', - items: [ - { - type: 'item', - href: `${getConfig().LOGOUT_URL}`, - content: formatMessage(messages.signOut), - }, - ], - }, - ], -} -); + }] + : []), + ]; + + const searchItem = searchCatalogUrl ? [{ + type: 'item', + href: null, + className: 'lw-search-item', + content: ( +
+ + { + if (e.key === 'Enter') { + const q = e.target.value.trim(); + window.location.href = urls.baseAppUrl(searchCatalogUrl) + + (q ? `?q=${encodeURIComponent(q)}` : ''); + } + }} + /> +
+ ), + }] : []; + + return { + mainMenu: [...mainMenu, ...searchItem], + secondaryMenu: ( + + ), + userMenu: [ + { + heading: '', + items: [ + { + type: 'item', + href: `${getConfig().ACCOUNT_PROFILE_URL}/u/${authenticatedUser?.username}`, + content: formatMessage(messages.profile), + }, + { + type: 'item', + href: `${getConfig().ACCOUNT_SETTINGS_URL}`, + content: formatMessage(messages.account), + }, + ...(getConfig().ORDER_HISTORY_URL ? [{ + type: 'item', + href: getConfig().ORDER_HISTORY_URL, + content: formatMessage(messages.orderHistory), + }] : []), + ], + }, + { + heading: '', + items: [ + { + type: 'item', + href: `${getConfig().LOGOUT_URL}`, + content: formatMessage(messages.signOut), + }, + ], + }, + ], + }; +}; export default getLearnerHeaderMenu; diff --git a/src/containers/LearnerDashboardHeader/index.scss b/src/containers/LearnerDashboardHeader/index.scss index d8bd8da1b..34184f989 100644 --- a/src/containers/LearnerDashboardHeader/index.scss +++ b/src/containers/LearnerDashboardHeader/index.scss @@ -9,6 +9,7 @@ // needed to make the link not resize the header border-bottom: 2px solid transparent; } + .course-link { border-bottom: 2px solid !important; } @@ -19,7 +20,7 @@ } .nav-small-menu { - > * { + >* { justify-content: flex-start !important; border-radius: 0 !important; @@ -36,3 +37,181 @@ // copy from legacy dashboard height: 40px; } + +// ─── Header layout: logo left | nav center | account right ─────────────────── +.site-header-desktop .nav-container { + + // True-center the main nav using absolute positioning. + // .nav-container already carries position-relative from Bootstrap. + .main-nav { + position: absolute; + left: 50%; + transform: translateX(-50%); + flex: 0 0 auto; + } + + // Push secondary menu to the far right + .secondary-menu-container { + margin-left: auto; + } +} + +// Hide the logged-in username label; keep avatar + caret visible. +// The button renders: {username text} +// font-size: 0 collapses the raw text node; restore sizes explicitly. +.site-header-desktop .secondary-menu-container .btn-outline-primary { + font-size: 0; + gap: 0.25rem; + + // Avatar span — both inline-style and class-based sizing use em, so fix with px + !important + .avatar { + width: 1.5rem !important; + height: 1.5rem !important; + + // AvatarIcon SVG also carries an inline style with em-based size — must override + svg { + width: 100% !important; + height: 100% !important; + } + } + + // Caret — direct child SVG of the button (not inside .avatar) + >svg { + width: 0.75rem; + height: 0.75rem; + } +} + +// ───────────────────────────────────────────────────────────────────────────── + +// ─── Navigation link styling overrides ─────────────────────────────────────── +.site-header-desktop .main-nav { + display: flex; + justify-content: center; + align-items: center; + + .nav-link { + padding: 5px 10px !important; + margin: 0.25rem 0.125rem; + font-size: 0.9rem; + font-weight: 500 !important; + letter-spacing: 0.01em !important; + text-decoration: none !important; + border-radius: 8px; + color: #374151; + } + + .nav-link:hover, + .nav-link:focus { + background: #D1FAE5; + color: #059669; + font-weight: 600 !important; + } + + .nav-link:has(.lw-search-wrapper):hover { + background: none; + color: inherit; + } + + .nav-link.active, + .expanded .nav-link { + background: #D1FAE5; + color: #059669; + font-weight: 600 !important; + } +} + +// ───────────────────────────────────────────────────────────────────────────── + +// Nav icon + label pairing +.lw-nav-item { + gap: 0.375rem; +} + +.lw-nav-icon { + width: 1rem; + height: 1rem; + flex-shrink: 0; +} + +// Search input in the header secondary slot (before bell icon) +// Scoped to override shared frontend-component-header pill defaults. +.site-header-desktop .lw-search-wrapper { + box-sizing: border-box; + display: inline-flex; + align-items: center; + gap: 8px; + width: 233px; + height: 40px; + padding: 8px 12px; + margin-right: 0.5rem; + border-radius: 6px; + border: 1px solid #E2E8F0; + background: #F8FAFC; + opacity: 1; + transition: border-color 150ms ease, background 150ms ease, box-shadow 150ms ease; + + &:focus-within { + border-color: #059669; + background: #fff; + box-shadow: 0 0 0 3px rgba(5, 150, 105, 0.16); + + .lw-search-icon { + color: #059669; + } + } + + .lw-search-icon { + position: static; + flex-shrink: 0; + width: 18px; + height: 18px; + color: #64748B; + pointer-events: none; + } + + .lw-search-icon path { + stroke-width: 1.7px; + } + + .lw-search-input { + flex: 1; + min-width: 0; + width: auto; + height: 21px; + border: none; + border-radius: 0; + padding: 0; + margin: 0; + background: transparent; + outline: none; + font-size: 14px; + line-height: 21px; + letter-spacing: 0; + color: #64748B; + + &:focus { + border: none; + background: transparent; + } + + &::placeholder { + font-family: inherit; + font-weight: 400; + font-size: 14px; + line-height: 21px; + letter-spacing: 0; + color: #64748B; + opacity: 1; + } + + &::-webkit-search-decoration, + &::-webkit-search-cancel-button { + -webkit-appearance: none; + } + } +} + +.lw-search-item:hover { + background: none !important; +} \ No newline at end of file diff --git a/src/containers/LearnerDashboardHeader/messages.js b/src/containers/LearnerDashboardHeader/messages.js index 08c71cd78..8c51e456f 100644 --- a/src/containers/LearnerDashboardHeader/messages.js +++ b/src/containers/LearnerDashboardHeader/messages.js @@ -86,6 +86,26 @@ const messages = defineMessages({ defaultMessage: 'New', description: 'The text announcing that an item in the user menu is New', }, + library: { + id: 'learnerVariantDashboard.menu.library.label', + defaultMessage: 'Library', + description: 'Header nav link for the course library.', + }, + history: { + id: 'learnerVariantDashboard.menu.history.label', + defaultMessage: 'History', + description: 'Header nav link for learning history.', + }, + browse: { + id: 'learnerVariantDashboard.menu.browse.label', + defaultMessage: 'Browse', + description: 'Header nav link for browsing the course catalog.', + }, + searchPlaceholder: { + id: 'learnerVariantDashboard.search.placeholder', + defaultMessage: 'Search courses...', + description: 'Placeholder text for the header course search input.', + }, }); export default messages;