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;