From 22cc85580cffa139a99a6175418c0791fb9298b6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 11 Jun 2026 12:12:11 -0400 Subject: [PATCH 1/3] chore(deps): bump codecov/codecov-action from 5.5.4 to 7.0.0 (#847) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From a98e35ad738587402367c15300d0224ca77b8a53 Mon Sep 17 00:00:00 2001 From: Jeffrey Ramirez Date: Thu, 18 Jun 2026 11:21:58 +0800 Subject: [PATCH 2/3] Added custom header --- package-lock.json | 27 +++ package.json | 1 + src/App.scss | 3 +- .../LearnerDashboardMenu.jsx | 195 ++++++++++++------ .../LearnerDashboardHeader/index.scss | 146 ++++++++++++- .../LearnerDashboardHeader/messages.js | 20 ++ 6 files changed, 328 insertions(+), 64 deletions(-) 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..393f5823f 100644 --- a/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx +++ b/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx @@ -1,77 +1,150 @@ +import React from 'react'; +import { + IconHome, + IconBook, + IconClockHour3, + IconSearch, + 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: IconSearch, + 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 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 = courseSearchUrl ? [{ + type: 'item', + href: null, + className: 'lw-search-item', + content: ( +
+ + { + if (e.key === 'Enter') { + const q = e.target.value.trim(); + window.location.href = urls.baseAppUrl(courseSearchUrl) + + (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..1ac256a98 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,146 @@ // 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; +} + +// Pill search input in the header secondary slot (before bell icon) +.lw-search-wrapper { + position: relative; + display: inline-flex; + align-items: center; + margin-right: 0.5rem; + + .lw-search-icon { + position: absolute; + left: 0.625rem; + width: 1rem; + height: 1rem; + color: #6b7280; + pointer-events: none; + } + + .lw-search-input { + border-radius: 9999px; + border: 1.5px solid #e5e7eb; + padding: 0.375rem 1rem 0.375rem 2.125rem; + font-size: 0.875rem; + color: #374151; + background: #f9fafb; + width: 13rem; + outline: none; + transition: border-color 150ms ease, background 150ms ease; + + &:focus { + border-color: #9ca3af; + background: #fff; + } + + &::placeholder { + color: #9ca3af; + } + + &::-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; From 6fee8e5c04797cd60c0a5afa942da4146fa22a64 Mon Sep 17 00:00:00 2001 From: pns-opswerks Date: Tue, 14 Jul 2026 12:33:23 +0800 Subject: [PATCH 3/3] Wire nav search to SEARCH_CATALOG_URL and update search input styles. (#2) Redirect header search to /search and restyle the search field. Co-authored-by: Paul Nathaniel Sango --- .../LearnerDashboardMenu.jsx | 29 ++++++-- .../LearnerDashboardHeader/index.scss | 73 ++++++++++++++----- 2 files changed, 78 insertions(+), 24 deletions(-) diff --git a/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx b/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx index 393f5823f..c1696da61 100644 --- a/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx +++ b/src/containers/LearnerDashboardHeader/LearnerDashboardMenu.jsx @@ -3,7 +3,6 @@ import { IconHome, IconBook, IconClockHour3, - IconSearch, IconHelpHexagon, IconBell, } from '@tabler/icons-react'; @@ -17,7 +16,26 @@ const ICON_MAP = { Home: IconHome, LibraryBooks: IconBook, ClockHour3: IconClockHour3, - Search: IconSearch, + Search: ({ size = 18, className, ...props }) => ( + + ), HelpHexagon: IconHelpHexagon, }; @@ -35,6 +53,7 @@ const getLearnerHeaderMenu = ( exploreCoursesClick, ) => { 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')) { @@ -81,13 +100,13 @@ const getLearnerHeaderMenu = ( : []), ]; - const searchItem = courseSearchUrl ? [{ + 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(courseSearchUrl) + window.location.href = urls.baseAppUrl(searchCatalogUrl) + (q ? `?q=${encodeURIComponent(q)}` : ''); } }} diff --git a/src/containers/LearnerDashboardHeader/index.scss b/src/containers/LearnerDashboardHeader/index.scss index 1ac256a98..34184f989 100644 --- a/src/containers/LearnerDashboardHeader/index.scss +++ b/src/containers/LearnerDashboardHeader/index.scss @@ -134,40 +134,75 @@ flex-shrink: 0; } -// Pill search input in the header secondary slot (before bell icon) -.lw-search-wrapper { - position: relative; +// 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: absolute; - left: 0.625rem; - width: 1rem; - height: 1rem; - color: #6b7280; + 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 { - border-radius: 9999px; - border: 1.5px solid #e5e7eb; - padding: 0.375rem 1rem 0.375rem 2.125rem; - font-size: 0.875rem; - color: #374151; - background: #f9fafb; - width: 13rem; + flex: 1; + min-width: 0; + width: auto; + height: 21px; + border: none; + border-radius: 0; + padding: 0; + margin: 0; + background: transparent; outline: none; - transition: border-color 150ms ease, background 150ms ease; + font-size: 14px; + line-height: 21px; + letter-spacing: 0; + color: #64748B; &:focus { - border-color: #9ca3af; - background: #fff; + border: none; + background: transparent; } &::placeholder { - color: #9ca3af; + font-family: inherit; + font-weight: 400; + font-size: 14px; + line-height: 21px; + letter-spacing: 0; + color: #64748B; + opacity: 1; } &::-webkit-search-decoration,