Repository: openmrs/openmrs-esm-core
Pull request #1279: (feat) O3-4425 - add ability to define collapsed left nav
Author: login: chibongho
Created: 2025-02-03T17:25:06Z | Merged: 2025-03-15T18:01:15Z

# Requirements

- [x] This PR has a title that briefly describes the work done including the ticket number. Ensure your PR title includes a [conventional commit](https://o3-docs.openmrs.org/docs/frontend-modules/contributing#contributing-guidelines) label (such as `feat`, `fix`, or `chore`, among others). See existing PR titles for inspiration.

## For changes to apps

- [x] My work conforms to the [**O3 Styleguide**](https://om.rs/styleguide) and [**design documentation**](https://om.rs/o3ui).

## If applicable

- [x] My work includes tests or is validated by existing tests.
- [x] I have updated the [esm-framework mock](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-framework/mock.tsx) to reflect any API changes I have made.

## Summary
<!-- Please describe what problems your PR addresses. -->
This PR adds the ability for apps to define a left nav that's always collapsed into the top nav hamburger menu.

This PR also includes refactoring to remove the need to render `<LeftNavMenu>` within the home or patient-chart app. Currently, the home / patient-chart app needs to render `<LeftNavMenu>` in its root component in order for the left nav to show in desktop mode. The top nav, however, also has a `<SideMenuPanel>`, which renders the menu when in tablet mode and the hamburger button is toggled on. (Incidentally, `<SideMenuPanel>` actually uses `<LeftNavMenu>` as child component)

Screenshot: `<LeftNavMenu>` rendered by home app:
![image](https://github.com/user-attachments/assets/600a46fa-f25d-47c0-96c7-1ac1aaa844df)

Screenshot: `<SideMenuPanel>` rendered by primary-navigation-app:
![image](https://github.com/user-attachments/assets/dcf6c4f6-ed12-45ef-836d-f311bbc1be69)

The `<LeftNavMenu>` is a carbon's `<SideNav>` component, which is a `position: fixed` element. This means we shouldn't really need the home or patient chart app to render it as part of its component tree. The refactoring makes it so the that the `<SideMenuPanel>` renders both:
-  the pop-up menu when the top nav hamburger is pressed, in tablet mode
- the actual left nav, in desktop mode

## Screenshots
<!-- Required if you are making UI changes. -->
Video of the home app with this change. No noticeable difference from before. (Not part of this PR, but in the video capture, `<LeftNavMenu>` is removed from the home app.)
https://github.com/user-attachments/assets/f89bae32-5b20-4270-aa44-8874fff62cf4

Video of the dispensing app with this change, The dispensing app added the following to define the left nav to be always collapsed:

    useEffect(() => {
      const basePath = window.spaBase + "/dispensing"; 
      setLeftNav({ name: 'homepage-dashboard-slot', basePath, mode: 'collapsed'});
      return () => unsetLeftNav('homepage-dashboard-slot');
    }, []);

https://github.com/user-attachments/assets/12f8b46f-2776-4676-a287-054db27ded27



## Related Issue
<!-- Paste the link to the Jira ticket here if one exists. -->
<!-- https://issues.openmrs.org/browse/O3- -->
https://openmrs.atlassian.net/browse/O3-4425

## Other
<!-- Anything not covered above -->
I attempted to refactor `<LeftNavMenu>`  hoping this will give us a cleaner way to define a collapsed left nav. While this change removes the need to render `<LeftNavMenu>` in home / patient-chart, it fails to completely abstract the left nav logic away from those apps. In particular, the home dashboard actually needs to maintain a `margin-left` in order to be rendered to the right of the left nav in desktop mode. (That margin is set to 0 in tablet mode). ~~I'm not sure if there is a way around it as the left nav is `position: fixed`.~~ This is probably doable with a more invasive change by overriding the `position: fixed` in the carbon `<SideNav>` and adding a `top-nav-container` div in `index.ejs` (so we can render the top nav there via `createPortal())`.

![image](https://github.com/user-attachments/assets/aa130f52-a0d3-42fc-b4b0-8b8a98737cf4)




Diff:
diff --git a/packages/apps/esm-primary-navigation-app/src/components/navbar-header-panels/side-menu-panel.component.tsx b/packages/apps/esm-primary-navigation-app/src/components/navbar-header-panels/side-menu-panel.component.tsx
index 84edca97f1..423d7e964f 100644
--- a/packages/apps/esm-primary-navigation-app/src/components/navbar-header-panels/side-menu-panel.component.tsx
+++ b/packages/apps/esm-primary-navigation-app/src/components/navbar-header-panels/side-menu-panel.component.tsx
@@ -1,11 +1,17 @@
 import React, { useEffect } from 'react';
-import { LeftNavMenu, useOnClickOutside } from '@openmrs/esm-framework';
+import { isDesktop, LeftNavMenu, useLayoutType, useLeftNavStore, useOnClickOutside } from '@openmrs/esm-framework';
+import { createPortal } from 'react-dom';
 
 interface SideMenuPanelProps {
   expanded: boolean;
   hidePanel: Parameters<typeof useOnClickOutside>[0];
 }
 
+/**
+ * This is the menu that pops up when clicking on the hamburger button
+ * on the top nav. It's also responsible for rendering the left nav
+ * in desktop mode (via a react portal).
+ */
 const SideMenuPanel: React.FC<SideMenuPanelProps> = ({ expanded, hidePanel }) => {
   const menuRef = useOnClickOutside(hidePanel, expanded);
 
@@ -13,8 +19,19 @@ const SideMenuPanel: React.FC<SideMenuPanelProps> = ({ expanded, hidePanel }) =>
     window.addEventListener('popstate', hidePanel);
     return () => window.removeEventListener('popstate', hidePanel);
   }, [hidePanel]);
+  const layout = useLayoutType();
+  const { mode } = useLeftNavStore();
 
-  return expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />;
+  const leftNavContainer = window.document.getElementById('omrs-left-nav-container-root');
+  return (
+    <>
+      {(!isDesktop(layout) || mode === 'collapsed') && expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />}
+      {isDesktop(layout) &&
+        mode !== 'collapsed' &&
+				leftNavContainer &&
+        createPortal(<LeftNavMenu ref={menuRef} isChildOfHeader />, leftNavContainer)}
+    </>
+  );
 };
 
 export default SideMenuPanel;
diff --git a/packages/apps/esm-primary-navigation-app/src/components/navbar/navbar.component.tsx b/packages/apps/esm-primary-navigation-app/src/components/navbar/navbar.component.tsx
index 40b1df9695..3d7f9d8111 100644
--- a/packages/apps/esm-primary-navigation-app/src/components/navbar/navbar.component.tsx
+++ b/packages/apps/esm-primary-navigation-app/src/components/navbar/navbar.component.tsx
@@ -13,6 +13,7 @@ import {
   CloseIcon,
   UserAvatarIcon,
   SwitcherIcon,
+  useLeftNavStore,
 } from '@openmrs/esm-framework';
 import { isDesktop } from '../../utils';
 import AppMenuPanel from '../navbar-header-panels/app-menu-panel.component';
@@ -28,7 +29,8 @@ const HeaderItems: React.FC = () => {
   const config = useConfig();
   const [activeHeaderPanel, setActiveHeaderPanel] = useState<string>(null);
   const layout = useLayoutType();
-  const navMenuItems = useAssignedExtensions('patient-chart-dashboard-slot').map((e) => e.id);
+  const { slotName, mode } = useLeftNavStore();
+  const navMenuItems = useAssignedExtensions(slotName);
   const appMenuItems = useAssignedExtensions('app-menu-slot');
   const userMenuItems = useAssignedExtensions('user-panel-slot');
   const isActivePanel = useCallback((panelName: string) => activeHeaderPanel === panelName, [activeHeaderPanel]);
@@ -44,7 +46,10 @@ const HeaderItems: React.FC = () => {
     [],
   );
 
-  const showHamburger = useMemo(() => !isDesktop(layout) && navMenuItems.length > 0, [navMenuItems.length, layout]);
+  const showHamburger = useMemo(
+    () => (!isDesktop(layout) || mode === 'collapsed') && navMenuItems.length > 0,
+    [navMenuItems.length, layout, mode],
+  );
   const showAppMenu = useMemo(() => appMenuItems.length > 0, [appMenuItems.length]);
   const showUserMenu = useMemo(() => userMenuItems.length > 0, [userMenuItems.length]);
   return (
@@ -115,7 +120,7 @@ const HeaderItems: React.FC = () => {
             </HeaderGlobalAction>
           )}
         </HeaderGlobalBar>
-        {!isDesktop(layout) && <SideMenuPanel hidePanel={hidePanel('sideMenu')} expanded={isActivePanel('sideMenu')} />}
+        <SideMenuPanel hidePanel={hidePanel('sideMenu')} expanded={isActivePanel('sideMenu')} />
         {showAppMenu && <AppMenuPanel expanded={isActivePanel('appMenu')} hidePanel={hidePanel('appMenu')} />}
         <NotificationsMenuPanel expanded={isActivePanel('notificationsMenu')} />
         {showUserMenu && <UserMenuPanel expanded={isActivePanel('userMenu')} hidePanel={hidePanel('userMenu')} />}
diff --git a/packages/apps/esm-primary-navigation-app/src/root.component.test.tsx b/packages/apps/esm-primary-navigation-app/src/root.component.test.tsx
index c6d40a201d..f164d3b631 100644
--- a/packages/apps/esm-primary-navigation-app/src/root.component.test.tsx
+++ b/packages/apps/esm-primary-navigation-app/src/root.component.test.tsx
@@ -8,6 +8,7 @@ import {
   useSession,
   type AssignedExtension,
   type Session,
+  useLeftNavStore,
 } from '@openmrs/esm-framework';
 import { isDesktop } from './utils';
 import { mockUser } from '../__mocks__/mock-user';
@@ -21,12 +22,14 @@ const mockIsDesktop = jest.mocked(isDesktop);
 const mockUseConfig = jest.mocked(useConfig);
 const mockUseAssignedExtensions = jest.mocked(useAssignedExtensions);
 const mockUseSession = jest.mocked(useSession);
+const mockUseLeftNavStore = jest.mocked(useLeftNavStore);
 
 mockUseConfig.mockReturnValue({
   logo: { src: null, alt: null, name: 'Mock EMR', link: 'Mock EMR' },
 });
 mockUseAssignedExtensions.mockReturnValue(['mock-extension'] as unknown as AssignedExtension[]);
 mockUseSession.mockReturnValue(mockSession as unknown as Session);
+mockUseLeftNavStore.mockReturnValue({ slotName: '', basePath: '', mode: 'normal' });
 
 jest.mock('./root.resource', () => ({
   getSynchronizedCurrentUser: jest.fn(() => mockUserObservable),
diff --git a/packages/framework/esm-framework/docs/API.md b/packages/framework/esm-framework/docs/API.md
index a7f34c2f70..da9570c066 100644
--- a/packages/framework/esm-framework/docs/API.md
+++ b/packages/framework/esm-framework/docs/API.md
@@ -230,6 +230,7 @@
 - [useBodyScrollLock](API.md#usebodyscrolllock)
 - [useFhirPagination](API.md#usefhirpagination)
 - [useLayoutType](API.md#uselayouttype)
+- [useLeftNavStore](API.md#useleftnavstore)
 - [useOnClickOutside](API.md#useonclickoutside)
 - [useOpenmrsFetchAll](API.md#useopenmrsfetchall)
 - [useOpenmrsInfinite](API.md#useopenmrsinfinite)
@@ -2363,9 +2364,15 @@ ___
 
 • `Const` **LeftNavMenu**: `ForwardRefExoticComponent`<`SideNavProps` & `RefAttributes`<`HTMLElement`\>\>
 
+This component renders the left nav in desktop mode. It's also used to render the same
+nav when the hamburger menu is clicked on in tablet mode. See side-menu-panel.component.tsx
+
+Use of this component by anything other than <SideMenuPanel> (where isChildOfHeader == false)
+is deprecated; it simply renders nothing.
+
 #### Defined in
 
-[packages/framework/esm-styleguide/src/left-nav/index.tsx:30](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/left-nav/index.tsx#L30)
+[packages/framework/esm-styleguide/src/left-nav/index.tsx:53](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/left-nav/index.tsx#L53)
 
 ___
 
@@ -7115,7 +7122,7 @@ ___
 
 | Name | Type |
 | :------ | :------ |
-| `__namedParameters` | `Object` |
+| `__namedParameters` | `SetLeftNavParams` |
 
 #### Returns
 
@@ -7123,7 +7130,7 @@ ___
 
 #### Defined in
 
-[packages/framework/esm-styleguide/src/left-nav/index.tsx:18](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/left-nav/index.tsx#L18)
+[packages/framework/esm-styleguide/src/left-nav/index.tsx:31](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/left-nav/index.tsx#L31)
 
 ___
 
@@ -7372,7 +7379,7 @@ ___
 
 #### Defined in
 
-[packages/framework/esm-styleguide/src/left-nav/index.tsx:22](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/left-nav/index.tsx#L22)
+[packages/framework/esm-styleguide/src/left-nav/index.tsx:35](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/left-nav/index.tsx#L35)
 
 ___
 
@@ -7470,6 +7477,20 @@ ___
 
 ___
 
+### useLeftNavStore
+
+▸ **useLeftNavStore**(): `LeftNavStore`
+
+#### Returns
+
+`LeftNavStore`
+
+#### Defined in
+
+[packages/framework/esm-styleguide/src/left-nav/index.tsx:41](https://github.com/openmrs/openmrs-esm-core/blob/main/packages/framework/esm-styleguide/src/left-nav/index.tsx#L41)
+
+___
+
 ### useOnClickOutside
 
 ▸ **useOnClickOutside**<`T`\>(`handler`, `active?`): `RefObject`<`T`\>
diff --git a/packages/framework/esm-styleguide/mock.tsx b/packages/framework/esm-styleguide/mock.tsx
index 0ca564efa8..fda628c20d 100644
--- a/packages/framework/esm-styleguide/mock.tsx
+++ b/packages/framework/esm-styleguide/mock.tsx
@@ -141,3 +141,4 @@ export const DiagnosisTags = jest.fn(({ diagnoses }) => (
     ))}
   </>
 ));
+export const useLeftNavStore = jest.fn();
diff --git a/packages/framework/esm-styleguide/src/index.ts b/packages/framework/esm-styleguide/src/index.ts
index b20c45dc7f..007ab610d7 100644
--- a/packages/framework/esm-styleguide/src/index.ts
+++ b/packages/framework/esm-styleguide/src/index.ts
@@ -1,8 +1,8 @@
 import { defineConfigSchema } from '@openmrs/esm-config';
+import { setupLogo } from './logo';
+import { setupIcons } from './icons/icon-registration';
 import { setupBranding } from './brand';
 import { esmStyleGuideSchema } from './config-schema';
-import { setupIcons } from './icons/icon-registration';
-import { setupLogo } from './logo';
 import { setupPictograms } from './pictograms/pictogram-registration';
 
 export * from './breakpoints';
diff --git a/packages/framework/esm-styleguide/src/left-nav/index.tsx b/packages/framework/esm-styleguide/src/left-nav/index.tsx
index 9c289fa3a0..642174e0d6 100644
--- a/packages/framework/esm-styleguide/src/left-nav/index.tsx
+++ b/packages/framework/esm-styleguide/src/left-nav/index.tsx
@@ -1,22 +1,35 @@
 /** @module @category UI */
-import React from 'react';
+import { SideNav } from '@carbon/react';
 import { ExtensionSlot, useStore } from '@openmrs/esm-react-utils';
 import { createGlobalStore } from '@openmrs/esm-state';
-import { SideNav } from '@carbon/react';
+import React from 'react';
 import styles from './left-nav.module.scss';
 
 interface LeftNavStore {
   slotName: string | null;
   basePath: string;
+  mode: 'normal' | 'collapsed';
 }
 
 const leftNavStore = createGlobalStore<LeftNavStore>('left-nav', {
   slotName: null,
   basePath: window.spaBase,
+  mode: 'normal',
 });
 
-export function setLeftNav({ name, basePath }) {
-  leftNavStore.setState({ slotName: name, basePath });
+interface SetLeftNavParams {
+  name: string;
+  basePath: string;
+
+  /**
+   * In normal mode, the left nav is shown in desktop mode, and collapse into hamburger menu button in tablet mode
+   * In collapsed mode, the left nav is always collapsed, regardless of desktop / tablet mode
+   */
+  mode?: 'normal' | 'collapsed';
+}
+
+export function setLeftNav({ name, basePath, mode }: SetLeftNavParams) {
+  leftNavStore.setState({ slotName: name, basePath, mode: mode ?? 'normal' });
 }
 
 export function unsetLeftNav(name) {
@@ -25,16 +38,30 @@ export function unsetLeftNav(name) {
   }
 }
 
+export function useLeftNavStore() {
+  return useStore(leftNavStore);
+}
 type LeftNavMenuProps = SideNavProps;
 
+/**
+ * This component renders the left nav in desktop mode. It's also used to render the same
+ * nav when the hamburger menu is clicked on in tablet mode. See side-menu-panel.component.tsx
+ *
+ * Use of this component by anything other than <SideMenuPanel> (where isChildOfHeader == false)
+ * is deprecated; it simply renders nothing.
+ */
 export const LeftNavMenu = React.forwardRef<HTMLElement, LeftNavMenuProps>((props, ref) => {
-  const { slotName, basePath } = useStore(leftNavStore);
+  const { slotName, basePath } = useLeftNavStore();
   const currentPath = window.location ?? { pathname: '' };
 
-  return (
-    <SideNav ref={ref} expanded aria-label="Left navigation" className={styles.leftNav} {...props}>
-      <ExtensionSlot name="global-nav-menu-slot" />
-      {slotName ? <ExtensionSlot name={slotName} state={{ basePath, currentPath }} /> : null}
-    </SideNav>
-  );
+  if (props.isChildOfHeader) {
+    return (
+      <SideNav ref={ref} expanded aria-label="Left navigation" className={styles.leftNav} {...props}>
+        <ExtensionSlot name="global-nav-menu-slot" />
+        {slotName ? <ExtensionSlot name={slotName} state={{ basePath, currentPath }} /> : null}
+      </SideNav>
+    );
+  } else {
+    return <></>;
+  }
 });
diff --git a/packages/shell/esm-app-shell/src/index.ejs b/packages/shell/esm-app-shell/src/index.ejs
index df1bd56545..2b2b5dacfe 100644
--- a/packages/shell/esm-app-shell/src/index.ejs
+++ b/packages/shell/esm-app-shell/src/index.ejs
@@ -48,6 +48,7 @@
     <div class="omrs-actionable-notifications-container"></div>
     <div class="omrs-snackbars-container"></div>
     <div class="omrs-modals-container"></div>
+    <div id="omrs-left-nav-container-root"></div>
     <template id="loading-spinner">
       <div class="omrs-loading-spinner">
         <div data-inline-loading="" class="cds--inline-loading" role="alert" aria-live="assertive">


Reviews:
user: login: chibongho
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-02-03T17:31:32Z
user: login: denniskigen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:19:36Z
user: login: chibongho
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-13T20:41:02Z
user: login: denniskigen
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-13T20:56:11Z
user: login: denniskigen
body: LGTM
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-15T18:01:04Z

Review comments:
diff_hunk: @@ -25,16 +25,30 @@ export function unsetLeftNav(name) {
   }
 }
 
+export function useLeftNavStore() {
+  return useStore(leftNavStore);
+}
 type LeftNavMenuProps = SideNavProps;
 
+/**
+ * This component renders the left nav in desktop mode. It's also used to render the same
+ * nav when the hamburger menu is clicked on in tablet mode. See side-menu-panel.component.tsx
+ *
+ * Use of this component by anything other than <SideMenuPanel> (where isChildOfHeader == false)
+ * is deprecated; it simply renders nothing.
path: packages/framework/esm-styleguide/src/left-nav/index.tsx
user: login: chibongho
body: This is a bit hacky. If we feel comfortable with this, we can opt for more aggressive breaking change where we stop exporting `<LeftNavMenu>` in `@openmrs/esm-framework`, and force other ESMs to remove it.
created_at: 2025-02-03T17:31:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,7 +46,10 @@ const HeaderItems: React.FC = () => {
     [],
   );
 
-  const showHamburger = useMemo(() => !isDesktop(layout) && navMenuItems.length > 0, [navMenuItems.length, layout]);
+  const showHamburger = useMemo(
+    () => (!isDesktop(layout) || mode == 'collapsed') && navMenuItems.length > 0,
path: packages/apps/esm-primary-navigation-app/src/components/navbar/navbar.component.tsx
user: login: denniskigen
body: ```suggestion
    () => (!isDesktop(layout) || mode === 'collapsed') && navMenuItems.length > 0,
```

nit
created_at: 2025-03-13T20:04:11Z
author_association: MEMBER
diff_hunk: @@ -1,20 +1,36 @@
 import React, { useEffect } from 'react';
-import { LeftNavMenu, useOnClickOutside } from '@openmrs/esm-framework';
+import { isDesktop, LeftNavMenu, useLayoutType, useLeftNavStore, useOnClickOutside } from '@openmrs/esm-framework';
+import { createPortal } from 'react-dom';
 
 interface SideMenuPanelProps {
   expanded: boolean;
   hidePanel: Parameters<typeof useOnClickOutside>[0];
 }
 
+/**
+ * This is the menu that pops up when clicking on the hamburger button
+ * on the top nav. It's also responsible for rendering the left nav
+ * in desktop mode (via a react portal).
+ */
 const SideMenuPanel: React.FC<SideMenuPanelProps> = ({ expanded, hidePanel }) => {
   const menuRef = useOnClickOutside(hidePanel, expanded);
 
   useEffect(() => {
     window.addEventListener('popstate', hidePanel);
     return () => window.removeEventListener('popstate', hidePanel);
   }, [hidePanel]);
+  const layout = useLayoutType();
+  const { mode } = useLeftNavStore();
 
-  return expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />;
+  const leftNavContainer = window.document.getElementById('omrs-left-nav-container-root');
+  return (
+    <>
+      {(!isDesktop(layout) || mode == 'collapsed') && expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />}
path: packages/apps/esm-primary-navigation-app/src/components/navbar-header-panels/side-menu-panel.component.tsx
user: login: denniskigen
body: ```suggestion
      {(!isDesktop(layout) || mode === 'collapsed') && expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />}
```
created_at: 2025-03-13T20:05:08Z
author_association: MEMBER
diff_hunk: @@ -1,20 +1,36 @@
 import React, { useEffect } from 'react';
-import { LeftNavMenu, useOnClickOutside } from '@openmrs/esm-framework';
+import { isDesktop, LeftNavMenu, useLayoutType, useLeftNavStore, useOnClickOutside } from '@openmrs/esm-framework';
+import { createPortal } from 'react-dom';
 
 interface SideMenuPanelProps {
   expanded: boolean;
   hidePanel: Parameters<typeof useOnClickOutside>[0];
 }
 
+/**
+ * This is the menu that pops up when clicking on the hamburger button
+ * on the top nav. It's also responsible for rendering the left nav
+ * in desktop mode (via a react portal).
+ */
 const SideMenuPanel: React.FC<SideMenuPanelProps> = ({ expanded, hidePanel }) => {
   const menuRef = useOnClickOutside(hidePanel, expanded);
 
   useEffect(() => {
     window.addEventListener('popstate', hidePanel);
     return () => window.removeEventListener('popstate', hidePanel);
   }, [hidePanel]);
+  const layout = useLayoutType();
+  const { mode } = useLeftNavStore();
 
-  return expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />;
+  const leftNavContainer = window.document.getElementById('omrs-left-nav-container-root');
+  return (
+    <>
+      {(!isDesktop(layout) || mode == 'collapsed') && expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />}
path: packages/apps/esm-primary-navigation-app/src/components/navbar-header-panels/side-menu-panel.component.tsx
user: login: denniskigen
body: nit
created_at: 2025-03-13T20:05:13Z
author_association: MEMBER
diff_hunk: @@ -1,20 +1,36 @@
 import React, { useEffect } from 'react';
-import { LeftNavMenu, useOnClickOutside } from '@openmrs/esm-framework';
+import { isDesktop, LeftNavMenu, useLayoutType, useLeftNavStore, useOnClickOutside } from '@openmrs/esm-framework';
+import { createPortal } from 'react-dom';
 
 interface SideMenuPanelProps {
   expanded: boolean;
   hidePanel: Parameters<typeof useOnClickOutside>[0];
 }
 
+/**
+ * This is the menu that pops up when clicking on the hamburger button
+ * on the top nav. It's also responsible for rendering the left nav
+ * in desktop mode (via a react portal).
+ */
 const SideMenuPanel: React.FC<SideMenuPanelProps> = ({ expanded, hidePanel }) => {
   const menuRef = useOnClickOutside(hidePanel, expanded);
 
   useEffect(() => {
     window.addEventListener('popstate', hidePanel);
     return () => window.removeEventListener('popstate', hidePanel);
   }, [hidePanel]);
+  const layout = useLayoutType();
+  const { mode } = useLeftNavStore();
 
-  return expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />;
+  const leftNavContainer = window.document.getElementById('omrs-left-nav-container-root');
+  return (
+    <>
+      {(!isDesktop(layout) || mode == 'collapsed') && expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />}
+      {isDesktop(layout) &&
+        mode != 'collapsed' &&
path: packages/apps/esm-primary-navigation-app/src/components/navbar-header-panels/side-menu-panel.component.tsx
user: login: denniskigen
body: ```suggestion
        mode !== 'collapsed' &&
```
created_at: 2025-03-13T20:05:25Z
author_association: MEMBER
diff_hunk: @@ -1,20 +1,36 @@
 import React, { useEffect } from 'react';
-import { LeftNavMenu, useOnClickOutside } from '@openmrs/esm-framework';
+import { isDesktop, LeftNavMenu, useLayoutType, useLeftNavStore, useOnClickOutside } from '@openmrs/esm-framework';
+import { createPortal } from 'react-dom';
 
 interface SideMenuPanelProps {
   expanded: boolean;
   hidePanel: Parameters<typeof useOnClickOutside>[0];
 }
 
+/**
+ * This is the menu that pops up when clicking on the hamburger button
+ * on the top nav. It's also responsible for rendering the left nav
+ * in desktop mode (via a react portal).
+ */
 const SideMenuPanel: React.FC<SideMenuPanelProps> = ({ expanded, hidePanel }) => {
   const menuRef = useOnClickOutside(hidePanel, expanded);
 
   useEffect(() => {
     window.addEventListener('popstate', hidePanel);
     return () => window.removeEventListener('popstate', hidePanel);
   }, [hidePanel]);
+  const layout = useLayoutType();
+  const { mode } = useLeftNavStore();
 
-  return expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />;
+  const leftNavContainer = window.document.getElementById('omrs-left-nav-container-root');
+  return (
+    <>
+      {(!isDesktop(layout) || mode == 'collapsed') && expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />}
+      {isDesktop(layout) &&
+        mode != 'collapsed' &&
+        createPortal(<LeftNavMenu ref={menuRef} isChildOfHeader />, leftNavContainer)}
path: packages/apps/esm-primary-navigation-app/src/components/navbar-header-panels/side-menu-panel.component.tsx
user: login: denniskigen
body: ```suggestion
				leftNavContainer &&
        createPortal(<LeftNavMenu ref={menuRef} isChildOfHeader />, leftNavContainer)}
```

nit: Would it be useful to assert the existence of the `omrs-left-nav-container-root` DOM element here, or is that guaranteed?
created_at: 2025-03-13T20:06:27Z
author_association: MEMBER
diff_hunk: @@ -1,20 +1,36 @@
 import React, { useEffect } from 'react';
-import { LeftNavMenu, useOnClickOutside } from '@openmrs/esm-framework';
+import { isDesktop, LeftNavMenu, useLayoutType, useLeftNavStore, useOnClickOutside } from '@openmrs/esm-framework';
+import { createPortal } from 'react-dom';
 
 interface SideMenuPanelProps {
   expanded: boolean;
   hidePanel: Parameters<typeof useOnClickOutside>[0];
 }
 
+/**
+ * This is the menu that pops up when clicking on the hamburger button
+ * on the top nav. It's also responsible for rendering the left nav
+ * in desktop mode (via a react portal).
+ */
 const SideMenuPanel: React.FC<SideMenuPanelProps> = ({ expanded, hidePanel }) => {
   const menuRef = useOnClickOutside(hidePanel, expanded);
 
   useEffect(() => {
     window.addEventListener('popstate', hidePanel);
     return () => window.removeEventListener('popstate', hidePanel);
   }, [hidePanel]);
+  const layout = useLayoutType();
+  const { mode } = useLeftNavStore();
 
-  return expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />;
+  const leftNavContainer = window.document.getElementById('omrs-left-nav-container-root');
+  return (
+    <>
+      {(!isDesktop(layout) || mode == 'collapsed') && expanded && <LeftNavMenu ref={menuRef} isChildOfHeader />}
+      {isDesktop(layout) &&
+        mode != 'collapsed' &&
+        createPortal(<LeftNavMenu ref={menuRef} isChildOfHeader />, leftNavContainer)}
path: packages/apps/esm-primary-navigation-app/src/components/navbar-header-panels/side-menu-panel.component.tsx
user: login: chibongho
body: The `omrs-left-nav-container-root` div is hard-coded into `index.ejs`, so it should be guaranteed.
created_at: 2025-03-13T20:39:39Z
author_association: CONTRIBUTOR
diff_hunk: @@ -25,16 +38,30 @@ export function unsetLeftNav(name) {
   }
 }
 
+export function useLeftNavStore() {
+  return useStore(leftNavStore);
+}
 type LeftNavMenuProps = SideNavProps;
 
+/**
+ * This component renders the left nav in desktop mode. It's also used to render the same
+ * nav when the hamburger menu is clicked on in tablet mode. See side-menu-panel.component.tsx
+ *
+ * Use of this component by anything other than <SideMenuPanel> (where isChildOfHeader == false)
+ * is deprecated; it simply renders nothing.
+ */
 export const LeftNavMenu = React.forwardRef<HTMLElement, LeftNavMenuProps>((props, ref) => {
-  const { slotName, basePath } = useStore(leftNavStore);
+  const { slotName, basePath } = useLeftNavStore();
   const currentPath = window.location ?? { pathname: '' };
 
-  return (
-    <SideNav ref={ref} expanded aria-label="Left navigation" className={styles.leftNav} {...props}>
-      <ExtensionSlot name="global-nav-menu-slot" />
-      {slotName ? <ExtensionSlot name={slotName} state={{ basePath, currentPath }} /> : null}
-    </SideNav>
-  );
+  if (props.isChildOfHeader) {
+    return (
+      <SideNav ref={ref} expanded aria-label="Left navigation" className={styles.leftNav} {...props}>
+        <ExtensionSlot name="global-nav-menu-slot" />
+        {slotName ? <ExtensionSlot name={slotName} state={{ basePath, currentPath }} /> : null}
+      </SideNav>
+    );
+  } else {
+    return <></>;
path: packages/framework/esm-styleguide/src/left-nav/index.tsx
user: login: denniskigen
body: nit: Is it preferable to return `null` in an early return if this is `!isChildOfHeader`?

```tsx
  // Early return if not a child of header
  if (!props.isChildOfHeader) {
    return null;
  }

 return (
    <SideNav ref={ref} expanded aria-label="Left navigation" className={styles.leftNav} {...props}>
      <ExtensionSlot name="global-nav-menu-slot" />
      {slotName ? <ExtensionSlot name={slotName} state={{ basePath, currentPath }} /> : null}
    </SideNav>
  );
```

I think this pattern is a lot cleaner if we don't want to render anything if the condition is not met.
created_at: 2025-03-13T20:55:59Z
author_association: MEMBER

PR comments:
user: login: github-actions[bot]
created_at: 2025-02-03T17:32:30Z
body: **Size Change:** -157 kB (-2.45%) 

**Total Size:** 6.24 MB

| Filename | Size | Change |  |
| :--- | :---: | :---: | :---: |
| `packages/shell/esm-app-shell/dist/797bf814dfa1c7ba.js` | 0 B | -68.5 kB (removed) | 🏆 |
| `packages/shell/esm-app-shell/dist/9775b46a8401bba4.js` | 0 B | -48.9 kB (removed) | 🏆 |
| `packages/shell/esm-app-shell/dist/openmrs.59c5695472a3b155.js` | 0 B | -21.8 kB (removed) | 🏆 |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size | Change |  |
| :--- | :---: | :---: | :---: |
| `packages/apps/esm-devtools-app/dist/373.js` | 12 kB | 0 B |  |
| `packages/apps/esm-devtools-app/dist/593.js` | 149 kB | 0 B |  |
| `packages/apps/esm-devtools-app/dist/642.js` | 21.1 kB | 0 B |  |
| `packages/apps/esm-devtools-app/dist/647.js` | 2.32 kB | 0 B |  |
| `packages/apps/esm-devtools-app/dist/657.js` | 7.02 kB | 0 B |  |
| `packages/apps/esm-devtools-app/dist/735.js` | 2.63 kB | 0 B |  |
| `packages/apps/esm-devtools-app/dist/788.js` | 42.9 kB | 0 B |  |
| `packages/apps/esm-devtools-app/dist/889.js` | 377 kB | -14 B (0%) |  |
| `packages/apps/esm-devtools-app/dist/988.js` | 325 B | 0 B |  |
| `packages/apps/esm-devtools-app/dist/main.js` | 3.22 kB | 0 B |  |
| `packages/apps/esm-devtools-app/dist/openmrs-esm-devtools-app.js` | 3.28 kB | 0 B |  |
| `packages/apps/esm-help-menu-app/dist/248.js` | 10.2 kB | 0 B |  |
| `packages/apps/esm-help-menu-app/dist/474.js` | 705 B | 0 B |  |
| `packages/apps/esm-help-menu-app/dist/611.js` | 2.45 kB | 0 B |  |
| `packages/apps/esm-help-menu-app/dist/642.js` | 21.1 kB | 0 B |  |
| `packages/apps/esm-help-menu-app/dist/657.js` | 7.02 kB | 0 B |  |
| `packages/apps/esm-help-menu-app/dist/662.js` | 147 kB | 0 B |  |
| `packages/apps/esm-help-menu-app/dist/735.js` | 2.63 kB | 0 B |  |
| `packages/apps/esm-help-menu-app/dist/788.js` | 42.9 kB | 0 B |  |
| `packages/apps/esm-help-menu-app/dist/889.js` | 377 kB | -13 B (0%) |  |
| `packages/apps/esm-help-menu-app/dist/958.js` | 3.73 kB | 0 B |  |
| `packages/apps/esm-help-menu-app/dist/main.js` | 8.56 kB | 0 B |  |
| `packages/apps/esm-help-menu-app/dist/openmrs-esm-help-menu-app.js` | 3.23 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/1491.js` | 134 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/1644.js` | 758 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/1873.js` | 3.66 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/1901.js` | 590 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/2016.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/236.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/2735.js` | 2.63 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/2757.js` | 714 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/2788.js` | 42.9 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/3336.js` | 137 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/3448.js` | 4.66 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/3574.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/3652.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/4272.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/4378.js` | 699 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/4426.js` | 27.8 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/4460.js` | 786 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/4705.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/4889.js` | 377 kB | -14 B (0%) |  |
| `packages/apps/esm-implementer-tools-app/dist/5240.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/5277.js` | 4.67 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/5336.js` | 591 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/539.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/5642.js` | 21.1 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/5673.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/5711.js` | 671 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/5737.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/5833.js` | 727 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/5962.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/6497.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/6566.js` | 700 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/667.js` | 121 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/6727.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/7036.js` | 2.5 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/744.js` | 722 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/7807.js` | 591 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/7859.js` | 3.09 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/7998.js` | 590 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/8271.js` | 753 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/8289.js` | 14.3 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/8319.js` | 689 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/8323.js` | 591 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/845.js` | 6.43 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/8657.js` | 7.02 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/8788.js` | 590 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/899.js` | 590 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/9261.js` | 592 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/9441.js` | 4.38 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/9579.js` | 591 B | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/main.js` | 21 kB | 0 B |  |
| `packages/apps/esm-implementer-tools-app/dist/openmrs-esm-implementer-tools-app.js` | 3.39 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/1644.js` | 949 B | 0 B |  |
| `packages/apps/esm-login-app/dist/1676.js` | 2.57 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/1901.js` | 758 B | 0 B |  |
| `packages/apps/esm-login-app/dist/2016.js` | 759 B | 0 B |  |
| `packages/apps/esm-login-app/dist/236.js` | 784 B | 0 B |  |
| `packages/apps/esm-login-app/dist/2735.js` | 2.62 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/2757.js` | 932 B | 0 B |  |
| `packages/apps/esm-login-app/dist/2788.js` | 42.9 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/3574.js` | 759 B | 0 B |  |
| `packages/apps/esm-login-app/dist/3593.js` | 149 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/3652.js` | 759 B | 0 B |  |
| `packages/apps/esm-login-app/dist/415.js` | 26.7 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/4272.js` | 772 B | 0 B |  |
| `packages/apps/esm-login-app/dist/4378.js` | 888 B | 0 B |  |
| `packages/apps/esm-login-app/dist/4460.js` | 1 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/4705.js` | 759 B | 0 B |  |
| `packages/apps/esm-login-app/dist/4889.js` | 377 kB | -14 B (0%) |  |
| `packages/apps/esm-login-app/dist/5211.js` | 35.4 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/5240.js` | 848 B | 0 B |  |
| `packages/apps/esm-login-app/dist/5336.js` | 843 B | 0 B |  |
| `packages/apps/esm-login-app/dist/539.js` | 790 B | 0 B |  |
| `packages/apps/esm-login-app/dist/5642.js` | 21.1 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/5673.js` | 798 B | 0 B |  |
| `packages/apps/esm-login-app/dist/5711.js` | 886 B | 0 B |  |
| `packages/apps/esm-login-app/dist/5737.js` | 758 B | 0 B |  |
| `packages/apps/esm-login-app/dist/5833.js` | 896 B | 0 B |  |
| `packages/apps/esm-login-app/dist/5962.js` | 759 B | 0 B |  |
| `packages/apps/esm-login-app/dist/6202.js` | 2.23 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/6497.js` | 759 B | 0 B |  |
| `packages/apps/esm-login-app/dist/6566.js` | 914 B | 0 B |  |
| `packages/apps/esm-login-app/dist/6727.js` | 759 B | 0 B |  |
| `packages/apps/esm-login-app/dist/7.js` | 3.03 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/7063.js` | 16.5 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/744.js` | 1.02 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/7807.js` | 1.06 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/7859.js` | 3.08 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/7998.js` | 757 B | 0 B |  |
| `packages/apps/esm-login-app/dist/8271.js` | 947 B | 0 B |  |
| `packages/apps/esm-login-app/dist/8319.js` | 893 B | 0 B |  |
| `packages/apps/esm-login-app/dist/8323.js` | 1.13 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/8657.js` | 7.01 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/8788.js` | 757 B | 0 B |  |
| `packages/apps/esm-login-app/dist/899.js` | 756 B | 0 B |  |
| `packages/apps/esm-login-app/dist/9261.js` | 759 B | 0 B |  |
| `packages/apps/esm-login-app/dist/93.js` | 2.16 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/9579.js` | 758 B | 0 B |  |
| `packages/apps/esm-login-app/dist/9755.js` | 3.36 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/main.js` | 63.5 kB | 0 B |  |
| `packages/apps/esm-login-app/dist/openmrs-esm-login-app.js` | 3.46 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/16.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/236.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/240.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/261.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/271.js` | 1.19 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/272.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/319.js` | 1.16 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/323.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/336.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/378.js` | 1.17 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/460.js` | 1.3 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/497.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/539.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/566.js` | 1.17 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/574.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/579.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/601.js` | 142 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/625.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/63.js` | 16.5 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/642.js` | 21.1 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/644.js` | 1.19 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/645.js` | 91.6 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/652.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/657.js` | 7.02 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/673.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/703.js` | 6.32 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/705.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/711.js` | 1.16 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/727.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/735.js` | 2.63 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/737.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/744.js` | 1.28 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/757.js` | 1.19 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/788.js` | 42.9 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/807.js` | 1.1 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/833.js` | 1.21 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/859.js` | 3.09 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/889.js` | 377 kB | -15 B (0%) |  |
| `packages/apps/esm-offline-tools-app/dist/899.js` | 1.02 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/901.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/947.js` | 8.66 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/962.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/998.js` | 1.03 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/main.js` | 107 kB | 0 B |  |
| `packages/apps/esm-offline-tools-app/dist/openmrs-esm-offline-tools-app.js` | 3.38 kB | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/16.js` | 240 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/236.js` | 239 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/238.js` | 146 kB | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/240.js` | 240 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/261.js` | 241 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/271.js` | 282 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/272.js` | 240 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/319.js` | 263 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/323.js` | 240 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/336.js` | 240 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/378.js` | 267 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/379.js` | 4.22 kB | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/460.js` | 277 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/482.js` | 15.2 kB | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/497.js` | 240 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/539.js` | 239 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/566.js` | 262 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/574.js` | 240 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/577.js` | 7.65 kB | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/579.js` | 240 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/619.js` | 6.45 kB | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/625.js` | 241 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/63.js` | 16.5 kB | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/644.js` | 290 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/652.js` | 241 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/657.js` | 7.03 kB | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/673.js` | 240 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/705.js` | 241 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/711.js` | 255 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/727.js` | 240 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/735.js` | 2.63 kB | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/737.js` | 240 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/744.js` | 288 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/757.js` | 248 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/788.js` | 42.9 kB | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/807.js` | 301 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/833.js` | 282 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/888.js` | 25.5 kB | +105 B (+0.41%) |  |
| `packages/apps/esm-primary-navigation-app/dist/889.js` | 377 kB | -13 B (0%) |  |
| `packages/apps/esm-primary-navigation-app/dist/899.js` | 237 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/901.js` | 239 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/962.js` | 241 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/998.js` | 239 B | 0 B |  |
| `packages/apps/esm-primary-navigation-app/dist/main.js` | 31.5 kB | +104 B (+0.33%) |  |
| `packages/apps/esm-primary-navigation-app/dist/openmrs-esm-primary-navigation-app.js` | 3.38 kB | +3 B (+0.09%) |  |
| `packages/framework/esm-api/dist/openmrs-esm-api.js` | 16.9 kB | 0 B |  |
| `packages/framework/esm-config/dist/openmrs-esm-module-config.js` | 8.42 kB | 0 B |  |
| `packages/framework/esm-context/dist/openmrs-esm-context.js` | 1.16 kB | 0 B |  |
| `packages/framework/esm-dynamic-loading/dist/openmrs-esm-dynamic-loading.js` | 2.89 kB | 0 B |  |
| `packages/framework/esm-error-handling/dist/openmrs-esm-error-handling.js` | 891 B | 0 B |  |
| `packages/framework/esm-expression-evaluator/dist/openmrs-esm-expression-evaluator.js` | 9.02 kB | 0 B |  |
| `packages/framework/esm-extensions/dist/openmrs-esm-extensions.js` | 25.5 kB | 0 B |  |
| `packages/framework/esm-feature-flags/dist/openmrs-esm-feature-flags.js` | 1.66 kB | 0 B |  |
| `packages/framework/esm-framework/dist/278.openmrs-esm-framework.js` | 14.5 kB | 0 B |  |
| `packages/framework/esm-framework/dist/530.openmrs-esm-framework.js` | 2.93 kB | 0 B |  |
| `packages/framework/esm-framework/dist/588.openmrs-esm-framework.js` | 2.15 kB | 0 B |  |
| `packages/framework/esm-framework/dist/619.openmrs-esm-framework.js` | 6.49 kB | 0 B |  |
| `packages/framework/esm-framework/dist/645.openmrs-esm-framework.js` | 9.3 kB | 0 B |  |
| `packages/framework/esm-framework/dist/735.openmrs-esm-framework.js` | 2.65 kB | 0 B |  |
| `packages/framework/esm-framework/dist/746.openmrs-esm-framework.js` | 6.14 kB | 0 B |  |
| `packages/framework/esm-framework/dist/788.openmrs-esm-framework.js` | 42.9 kB | 0 B |  |
| `packages/framework/esm-framework/dist/openmrs-esm-framework.js` | 465 kB | +67 B (+0.01%) |  |
| `packages/framework/esm-globals/dist/openmrs-esm-globals.js` | 791 B | 0 B |  |
| `packages/framework/esm-navigation/dist/openmrs-esm-navigation.js` | 9.34 kB | 0 B |  |
| `packages/framework/esm-offline/dist/openmrs-esm-offline.js` | 34.4 kB | 0 B |  |
| `packages/framework/esm-react-utils/dist/openmrs-esm-react-utils.js` | 22.7 kB | 0 B |  |
| `packages/framework/esm-routes/dist/openmrs-esm-utils.js` | 4.82 kB | 0 B |  |
| `packages/framework/esm-state/dist/openmrs-esm-state.js` | 1.59 kB | 0 B |  |
| `packages/framework/esm-styleguide/dist/openmrs-esm-styleguide.js` | 196 kB | +15 B (+0.01%) |  |
| `packages/framework/esm-translations/dist/openmrs-esm-core-translations.js` | 1.92 kB | 0 B |  |
| `packages/framework/esm-utils/dist/openmrs-esm-utils.js` | 45.4 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/01001bd452edf96b.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/01b94747dec36fb0.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/16a96c3f174be741.js` | 3.38 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/2058b4ec98841572.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/2280106e69fcb6e7.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/2e9429d21af6e3ac.js` | 1.26 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/38d4b8fb75f8850c.js` | 18.3 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/3b01402cd7292844.js` | 2.85 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/3c905a475a09929a.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/3d59b015a911ce78.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/3f6dd2a036ca68a6.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/3f75953146d56a11.js` | 1.37 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/401c363772da4d87.js` | 7.16 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/41ed5c5b03ca8e28.js` | 1.39 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/4c3a04d6304ef175.js` | 2.23 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/50c6bb7970c7a1b9.js` | 1.1 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/528acd8ff95c0d83.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/55908a3f26b2a9a6.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/572e81f004cc10c5.js` | 15.1 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/5a68009041911dad.js` | 1.35 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/5ad62e2d8dfc200e.js` | 0 B | -6.49 kB (removed) | 🏆 |
| `packages/shell/esm-app-shell/dist/5ef17790adbcff54.js` | 176 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/629c417e6764717f.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/697715687da17953.js` | 3.95 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/6e8f59ec2f5d2761.js` | 168 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/6f28b7b740beb889.js` | 1.35 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/6f47008d9c93c1cb.js` | 3.33 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/788fdd707198d0b7.js` | 6.99 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/79e0fe636d1ed41c.js` | 1.59 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/7b204450699b0daf.js` | 2.6 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/8e0d11f862dfbba6.js` | 626 B | 0 B |  |
| `packages/shell/esm-app-shell/dist/970468127f2fb034.js` | 3.04 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/a7089447b7f2614b.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/a7792d6252d85b20.js` | 43 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/aaf16219c38498fe.js` | 0 B | -3.94 kB (removed) | 🏆 |
| `packages/shell/esm-app-shell/dist/ada2430816347bb4.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/b0c2799f84483ab9.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/b237fc31ac7dfd73.js` | 0 B | -7.15 kB (removed) | 🏆 |
| `packages/shell/esm-app-shell/dist/b399188e97044ac6.js` | 6.49 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/b407f548b6fadfa3.js` | 3.08 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/b44fb50339c45d13.js` | 1.25 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/b5d46f6cbc64597b.js` | 48.9 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/b88532bde74b513a.js` | 9.41 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/c6a19d9750c44512.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/c7abf1814f63a7d2.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/c7dc0c60e84d6e1e.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/d6a680dea8be9922.js` | 1.24 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/d7ec4411c22fcbc7.js` | 68.6 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/d80464c66e5d20d7.js` | 3.23 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/e2ec9aeac53221df.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/e37b7c268f18e9f0.js` | 1.24 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/e419a658f4742c3e.js` | 9.36 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/ed52f4fbd2f38ad4.js` | 2.58 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/eda7b495e8f8cf3a.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/f07d8d15ffe495b4.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/f09898631f52dc55.js` | 1.1 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/f157bed92d3f6fc7.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/f5c65190072bb3f7.js` | 626 B | 0 B |  |
| `packages/shell/esm-app-shell/dist/fb95c0d3b16066f3.js` | 1.22 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/fc6da4a7d41ed938.js` | 1.05 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/fcb385df17a82210.js` | 6.77 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/openmrs.ba1906bc43b85fa0.js` | 21.8 kB | 0 B |  |
| `packages/shell/esm-app-shell/dist/service-worker.js` | 46.7 kB | +3 B (+0.01%) |  |
| `packages/tooling/openmrs/dist/cli.js` | 2.84 kB | 0 B |  |
| `packages/tooling/openmrs/dist/commands/assemble.js` | 3.31 kB | 0 B |  |
| `packages/tooling/openmrs/dist/commands/build.js` | 1.34 kB | 0 B |  |
| `packages/tooling/openmrs/dist/commands/debug.js` | 543 B | 0 B |  |
| `packages/tooling/openmrs/dist/commands/develop.js` | 2.71 kB | 0 B |  |
| `packages/tooling/openmrs/dist/commands/index.js` | 437 B | 0 B |  |
| `packages/tooling/openmrs/dist/commands/start.js` | 850 B | 0 B |  |
| `packages/tooling/openmrs/dist/index.js` | 517 B | 0 B |  |
| `packages/tooling/openmrs/dist/runner.js` | 640 B | 0 B |  |
| `packages/tooling/openmrs/dist/utils/config.js` | 726 B | 0 B |  |
| `packages/tooling/openmrs/dist/utils/debugger.js` | 575 B | 0 B |  |
| `packages/tooling/openmrs/dist/utils/dependencies.js` | 643 B | 0 B |  |
| `packages/tooling/openmrs/dist/utils/helpers.js` | 397 B | 0 B |  |
| `packages/tooling/openmrs/dist/utils/importmap.js` | 3.21 kB | 0 B |  |
| `packages/tooling/openmrs/dist/utils/index.js` | 443 B | 0 B |  |
| `packages/tooling/openmrs/dist/utils/logger.js` | 368 B | 0 B |  |
| `packages/tooling/openmrs/dist/utils/npmConfig.js` | 831 B | 0 B |  |
| `packages/tooling/openmrs/dist/utils/untar.js` | 725 B | 0 B |  |
| `packages/tooling/openmrs/dist/utils/variables.js` | 192 B | 0 B |  |
| `packages/tooling/openmrs/dist/utils/webpack.js` | 278 B | 0 B |  |
| `packages/tooling/webpack-config/dist/index.js` | 3.61 kB | 0 B |  |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR
user: login: chibongho
created_at: 2025-02-05T22:03:02Z
body: @ibacher @denniskigen @samuelmale @vasharma05 This touches core code, so I'm hoping to get feedback from outside PIH. Please review when you have a chance. Thanks!
author_association: CONTRIBUTOR
user: login: chibongho
created_at: 2025-03-12T21:07:58Z
body: Bump. Can I get a review for this? @ibacher @denniskigen @vasharma05 @samuelmale 

I just investigated the E2E failure. It's caused by the patient chart having a custom VisitHeader in the top nav. It should be fixed when we remove the VisitHeader.  (There is a [separate PR](https://github.com/openmrs/openmrs-esm-patient-chart/pull/2262) for that.)

Is there a way to either: bypass the E2E test failure in patient chart, or run the E2E test again a different branch?
author_association: CONTRIBUTOR
user: login: denniskigen
created_at: 2025-03-13T20:20:34Z
body: Is this artifacting to be expected? 

https://github.com/user-attachments/assets/eb8c590e-c16d-4857-9918-bf8537165849


author_association: MEMBER
user: login: chibongho
created_at: 2025-03-13T20:37:37Z
body: > Is this artifacting to be expected?
> artifacting.mp4

Yeah, it's caused by the patient chart having a custom VisitHeader in the top nav. It should be fixed when we remove the VisitHeader. (There is a [pending PR](https://github.com/openmrs/openmrs-esm-patient-chart/pull/2262) for that.) That PR is pretty big, so maybe I can make a smaller one in patient chart just to unblock this.
author_association: CONTRIBUTOR
user: login: denniskigen
created_at: 2025-03-13T21:15:06Z
body: > That PR is pretty big, so maybe I can make a smaller one in patient chart just to unblock this.

We probably should to prevent potential breakage if this PR gets checked in before the fix PR is ready to go.
author_association: MEMBER