diff --git a/.gitignore b/.gitignore
index 2648984f08249b8e80bb5761b63f2af84d050ac5..1d781452cd7f043ab28e9944024cce06bb611799 100644
--- a/.gitignore
+++ b/.gitignore
@@ -15,6 +15,8 @@ dist-ssr
 .env.dev
 *.mjs
 
+**/api.config.json
+
 # Editor directories and files
 .idea
 .DS_Store
diff --git a/apps/dataset_viewer/index.html b/apps/dataset_viewer/index.html
index e4b78eae12304a075fa19675c4047061d6ab920d..c98b5a0e17a1125f4050417bf1c05d3ec26f897b 100644
--- a/apps/dataset_viewer/index.html
+++ b/apps/dataset_viewer/index.html
@@ -4,7 +4,7 @@
     <meta charset="UTF-8" />
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
-    <title>Vite + React + TS</title>
+    <title>Dataset Viewer</title>
   </head>
   <body>
     <div id="root"></div>
diff --git a/apps/dataset_viewer/package.json b/apps/dataset_viewer/package.json
index 46127ab4fb0ccd28fad967d204ca32f1b1fe779d..540b08192ff0c57560b74cfbf8ff3edf76078841 100644
--- a/apps/dataset_viewer/package.json
+++ b/apps/dataset_viewer/package.json
@@ -13,7 +13,8 @@
     "lint:tsc": "tsc",
     "lint:prettier": "prettier . --check",
     "fix:eslint": "eslint \"**/*.{js,cjs,ts,tsx}\" --fix",
-    "fix:prettier": "prettier . --write"
+    "fix:prettier": "prettier . --write",
+    "configure:standalone": "cp public/config/api.config.json.example public/config/api.config.json"
   },
   "dependencies": {
     "@babel/eslint-parser": "^7.25.1",
@@ -23,6 +24,9 @@
     "@fortawesome/free-solid-svg-icons": "^6.6.0",
     "@fortawesome/react-fontawesome": "^0.2.2",
     "@tanstack/react-query": "^5.52.1",
+    "ajv": "^8.17.1",
+    "bootstrap": "^5.3.3",
+    "bootswatch": "^5.3.3",
     "lodash": "^4.17.21",
     "react": "^18.3.1",
     "react-bootstrap": "^2.10.4",
diff --git a/apps/dataset_viewer/public/config/api.config.json.example b/apps/dataset_viewer/public/config/api.config.json.example
new file mode 100644
index 0000000000000000000000000000000000000000..fa5cfdd9063ff7a993a82fd05574f4fdf8f65ff4
--- /dev/null
+++ b/apps/dataset_viewer/public/config/api.config.json.example
@@ -0,0 +1,35 @@
+{
+  "icat_url": "https://icatplus.esrf.fr/",
+  "authentication": {
+    "anonymous": {
+      "plugin": "db",
+      "username": "reader",
+      "password": "reader"
+    },
+    "autoRefresh": true,
+    "autoRefreshThresholdMinutes": "60",
+    "authenticators": [
+      {
+        "name": "OpenID",
+        "enabled": true,
+        "title": "ESRF Single Sign On",
+        "plugin": "esrf",
+        "message": "Login with ESRF SSO",
+        "openidLogoutTransferToApp": true,
+        "refreshToken": true,
+        "minValidity": 600,
+        "configuration": {
+          "authority": "https://websso.esrf.fr/auth/realms/ESRF",
+          "clientId": "icat"
+        }
+      },
+      {
+        "name": "Database",
+        "title": "Database",
+        "enabled": true,
+        "message": "",
+        "plugin": "db"
+      }
+    ]
+  }
+}
diff --git a/apps/dataset_viewer/public/config/dataset.viewer.config.json b/apps/dataset_viewer/public/config/dataset.viewer.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..64e0a6c3b841cbdf8274c7c7235527caae1c2cca
--- /dev/null
+++ b/apps/dataset_viewer/public/config/dataset.viewer.config.json
@@ -0,0 +1,149 @@
+[
+  {
+    "beamline": "CM01",
+    "render": {
+      "details": {
+        "type": "local",
+        "remote": {
+          "name": "cryoet",
+          "component": "./CryoETDatasetViewer"
+        }
+      }
+    }
+  },
+  {
+    "beamline": "BM29",
+    "datasetParameter": {
+      "SAXS_experiment_type": ["hplc", "HPLC"]
+    },
+    "render": {
+      "details": {
+        "type": "local",
+        "remote": {
+          "name": "remoteDatasetViewerApp",
+          "component": "SAXSHPLCDatasetDetails"
+        }
+      },
+      "snapshot": {
+        "type": "local",
+        "remote": {
+          "name": "remoteDatasetViewerApp",
+          "component": "SAXSHPLCDatasetSnapshot"
+        }
+      }
+    }
+  },
+  {
+    "beamline": "BM29",
+    "datasetParameter": {
+      "SAXS_experiment_type": ["sampleChanger", "sample-changer"]
+    },
+    "render": {
+      "details": {
+        "type": "local",
+        "remote": {
+          "name": "remoteDatasetViewerApp",
+          "component": "SAXSSampleChangerDatasetDetails"
+        }
+      },
+      "snapshot": {
+        "type": "local",
+        "remote": {
+          "name": "remoteDatasetViewerApp",
+          "component": "SAXSSampleChangerDatasetSnapshot"
+        }
+      }
+    }
+  },
+  {
+    "beamline": "BM29",
+    "render": {
+      "details": {
+        "type": "local",
+        "remote": {
+          "name": "remoteDatasetViewerApp",
+          "component": "SAXSSampleChangerDatasetDetails"
+        }
+      },
+      "snapshot": {
+        "type": "local",
+        "remote": {
+          "name": "remoteDatasetViewerApp",
+          "component": "SAXSSampleChangerDatasetSnapshot"
+        }
+      }
+    }
+  },
+  {
+    "beamline": "ID31",
+    "render": {
+      "details": {
+        "type": "local",
+        "remote": {
+          "name": "remoteDatasetViewerApp",
+          "component": "HTXRPDDataset"
+        }
+      }
+    }
+  },
+  {
+    "projectName": "The Human Organ Atlas",
+    "render": {
+      "details": {
+        "type": "local",
+        "remote": {
+          "name": "remoteDatasetViewerApp",
+          "component": "./HumanOrganAtlasDataset"
+        }
+      }
+    }
+  },
+  {
+    "projectName": "paleo",
+    "render": {
+      "details": {
+        "type": "remote",
+        "remote": {
+          "name": "remoteDatasetViewerApp",
+          "component": "./PaleoDataset"
+        }
+      }
+    }
+  },
+  {
+    "beamline": ["ID24", "ID24-DCM", "ID24-ED"],
+    "datasetParameter": {
+      "InstrumentLaser01_energy": null
+    },
+    "render": {
+      "details": {
+        "type": "local",
+        "remote": {
+          "name": "XASDatasetDetail",
+          "component": "XASDatasetDetail"
+        }
+      },
+      "tableCell": {
+        "type": "local",
+        "remote": {
+          "name": "XASDatasetTableCell",
+          "component": "XASDatasetTableCell"
+        }
+      }
+    }
+  },
+  {
+    "technique": "TOMO",
+    "date": "01/01/2022",
+    "beamline": "BM05",
+    "render": {
+      "details": {
+        "type": "local",
+        "remote": {
+          "name": "TOMODataset",
+          "component": "TOMODataset"
+        }
+      }
+    }
+  }
+]
diff --git a/apps/dataset_viewer/public/config/sample.viewer.config.json b/apps/dataset_viewer/public/config/sample.viewer.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..1cdc5aba70cd06fa19098f092467abbabc970649
--- /dev/null
+++ b/apps/dataset_viewer/public/config/sample.viewer.config.json
@@ -0,0 +1,12 @@
+[
+  {
+    "beamline": "BM29",
+    "render": {
+      "type": "generic",
+      "format": "graph",
+      "props": {
+        "paginationSize": 10
+      }
+    }
+  }
+]
diff --git a/apps/dataset_viewer/public/config/ui.config.json b/apps/dataset_viewer/public/config/ui.config.json
new file mode 100644
index 0000000000000000000000000000000000000000..99dc23f62c7078e9a91824dd27a03ef9907a0995
--- /dev/null
+++ b/apps/dataset_viewer/public/config/ui.config.json
@@ -0,0 +1,169 @@
+{
+  "applicationTitle": "Data Portal",
+  "facilityName": "ESRF",
+  "homePage": {
+    "policyMessage": "Public data is accessible to anyone. You need to be logged-in to visualize your data when it is under embargo. See <a href=\"https://www.esrf.fr/fr/home/UsersAndScience/UserGuide/esrf-data-policy.html\" target=\"_blank\" rel=\"noreferrer\">ESRF data policy</a> for more details."
+  },
+  "linkToPreviousVersion": {
+    "name": "Back to Data Portal V1",
+    "url": "https://data1.esrf.fr"
+  },
+  "knowledgeBasePage": "https://confluence.esrf.fr/display/DATAPOLWK/Knowledge+Base",
+  "loginForm": {
+    "accountCreationLink": "https://smis.esrf.fr/misapps/SMISWebClient/accountManager/searchExistingAccount.do?action=search",
+    "note": {
+      "enabled": true,
+      "title": "Important note",
+      "notes": [
+        {
+          "text": "In order to login to the Data Portal of the ESRF to send samples or browse embargoed data you need to be declared a member of a proposal on the ESRF User Portal. Once this is done your account will be activated <b>45 days</b> before the first experiment starts. If you need access earlier please contact the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"http://www.esrf.eu/UsersAndScience/UserGuide/Contacts\" >ESRF User Office</a>.<br/>Anonymous login to browse open data is always possible."
+        },
+        {
+          "text": "During 2019 and according to the General Data Protection Regulation, all portal users who did not consent to the <a href=\"http://www.esrf.fr/GDPR\" rel=\"noopener noreferrer\" target=\"_blank\" > User Portal Privacy Statement</a> have had their account deactivated. Please contact the <a rel=\"noopener noreferrer\" target=\"_blank\" href=\"http://www.esrf.eu/UsersAndScience/UserGuide/Contacts\" >User Office</a> if you wish to reactivate it."
+        }
+      ]
+    }
+  },
+  "userPortal": {
+    "investigationParameterPkName": "Id",
+    "link": "https://smis.esrf.fr/misapps/SMISWebClient/protected/aform/manageAForm.do?action=view&expSessionVO.pk="
+  },
+  "doi": {
+    "link": "https://doi.esrf.fr/",
+    "minimalAbstractLength": 1000,
+    "minimalTitleLength": 40,
+    "facilityPrefix": "10.15151",
+    "facilitySuffix": "ESRF-DC",
+    "referenceDoi": "https://doi.esrf.fr/10.15151/ESRF-DC-2011729981"
+  },
+  "fileBrowser": {
+    "maxFileNb": 1000
+  },
+  "imageViewer": {
+    "fileExtensions": [".png", ".jpg", ".jpeg", ".tiff", ".gif"]
+  },
+  "h5Viewer": {
+    "url": "https://hibou.esrf.fr",
+    "fileExtensions": [".hdf5", ".h5", ".nexus", ".nx", ".nxs", ".cxi"]
+  },
+  "galleryViewer": {
+    "fileExtensions": ["jpeg", "jpg", "gif", "png", "svg"]
+  },
+  "textViewer": {
+    "maxFileSize": 5000000,
+    "fileExtensions": [
+      ".txt",
+      ".log",
+      ".json",
+      ".xml",
+      ".csv",
+      ".dat",
+      ".inp",
+      ".xds",
+      ".descr",
+      ".lp",
+      ".hkl",
+      ".site",
+      ".asc",
+      ".ini"
+    ]
+  },
+  "feedback": {
+    "email": "dataportalrequests@esrf.fr",
+    "subject": "Feedback",
+    "body": "Hi,\n\n<< Please provide your feedback here. >>\n<< To report an issue, please include screenshots, reproduction steps, proposal number, beamline, etc. >>\n<< To suggest a new feature, please describe the needs this feature would fulfill. >>\n\nThanks"
+  },
+  "footer": {
+    "images": [
+      {
+        "src": "/images/esrf.jpg",
+        "alt": "ESRF",
+        "href": "https://www.esrf.fr/"
+      },
+      {
+        "src": "/images/CoreTrustSeal.png",
+        "alt": "CoreTrustSeal",
+        "href": "https://www.coretrustseal.org/"
+      }
+    ]
+  },
+  "handsonTableLicenseKey": "non-commercial-and-evaluation",
+  "globus": {
+    "enabled": true,
+    "url": "https://app.globus.org/file-manager?",
+    "collections": [
+      {
+        "root": "/data/visitor/",
+        "origin": "/data",
+        "originId": "bfc3eff4-f5ca-4b4d-8532-e9a155f3613f"
+      },
+      {
+        "root": "/data/projects/hop",
+        "origin": "/data/projects/hop",
+        "originId": "340dc883-4b0d-476c-abb0-969e1ddd9dc0"
+      },
+      {
+        "root": "/data/projects/open-datasets",
+        "origin": "/data/projects/open-datasets/",
+        "originId": "8cbe8cdc-048a-48cf-b8a0-046a6af3ba44"
+      },
+      {
+        "root": "/data/projects/paleo",
+        "origin": "/data/projects/paleo/public",
+        "originId": "e8fb6c4b-9ab0-4a1c-a79d-d51cef0b8c3d"
+      }
+    ],
+    "messageAlert": {
+      "enabled": true,
+      "message": "For users who want to download large volume of experimental data <strong>(&gt;2GB)</strong>, ESRF users can access the Globus service, please read the <a href=\"https://confluence.esrf.fr/display/SCKB/Globus\" target=\"_blank\">documentation</a> for proceeding."
+    }
+  },
+  "sample": {
+    "pageTemplateURL": "https://smis.esrf.fr/misapps/SMISWebClient/protected/samplesheet/view.do?pk=",
+    "editable": true,
+    "descriptionParameterName": "Sample_description",
+    "nonEditableParameterName": "Id",
+    "notesParameterName": "Sample_notes"
+  },
+  "logbook": {
+    "help": "https://confluence.esrf.fr/display/DATAPOLWK/Electronic+Logbook",
+    "defaultMonthPeriodForStaff": 1
+  },
+  "projects": [
+    {
+      "title": "The Human Organ Atlas",
+      "key": "The Human Organ Atlas",
+      "url": "https://human-organ-atlas.esrf.eu/datasets/{datasetId}",
+      "homepage": "https://human-organ-atlas.esrf.eu"
+    },
+    {
+      "title": "Paleontology database",
+      "key": "paleo",
+      "url": "http://paleo.esrf.fr/datasets/{datasetId}",
+      "homepage": "http://paleo.esrf.fr"
+    }
+  ],
+  "features": {
+    "reprocessing": true,
+    "logbook": true,
+    "dmp": true,
+    "logistics": true
+  },
+  "tracking": {
+    "enabled": true,
+    "url": "https://matomo-srv-1.esrf.fr/",
+    "siteId": "14",
+    "tracker": "piwik.php",
+    "script": "piwik.js"
+  },
+  "mx": {
+    "pdb_map_mtz_viewer_url": "https://moorhen.esrf.fr"
+  },
+  "logistics": {
+    "transportOrganizationEnabled": false,
+    "facilityReimbursmentEnabled": true,
+    "facilityForwarderName": "FedEX",
+    "facilityForwarderAccount": "388310561",
+    "facilityForwarderNamePickup": ["FEDEX", "DHL express", "UPS"]
+  }
+}
diff --git a/apps/dataset_viewer/src/App.tsx b/apps/dataset_viewer/src/App.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..51edf11d36181f818f8213415e41436fb9a17269
--- /dev/null
+++ b/apps/dataset_viewer/src/App.tsx
@@ -0,0 +1,49 @@
+import {
+  AuthenticatedAPIProvider,
+  AuthenticatorProvider,
+  OpenIDProvider,
+  SideNavProvider,
+  UnauthenticatedAPIProvider,
+} from '@edata-portal/core';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+import { AppRouter } from 'standalone/routing/AppRouter';
+import { ViewersProvider } from 'standalone/providers/ViewersProvider';
+import { ConfigProvider } from 'standalone/providers/ConfigProvider';
+import { Navigation } from 'standalone/routing/Navigation';
+import 'standalone/scss/main.scss';
+
+const queryClient = new QueryClient();
+
+function App() {
+  return (
+    <>
+      <QueryClientProvider client={queryClient}>
+        <ConfigProvider>
+          <AppContextProviders>
+            <ViewersProvider>
+              <SideNavProvider>
+                <AppRouter>
+                  <Navigation />
+                </AppRouter>
+              </SideNavProvider>
+            </ViewersProvider>
+          </AppContextProviders>
+        </ConfigProvider>
+      </QueryClientProvider>
+    </>
+  );
+}
+
+function AppContextProviders({ children }: { children: React.ReactNode }) {
+  return (
+    <UnauthenticatedAPIProvider>
+      <AuthenticatorProvider>
+        <OpenIDProvider>
+          <AuthenticatedAPIProvider>{children}</AuthenticatedAPIProvider>
+        </OpenIDProvider>
+      </AuthenticatorProvider>
+    </UnauthenticatedAPIProvider>
+  );
+}
+
+export default App;
diff --git a/apps/dataset_viewer/src/main.tsx b/apps/dataset_viewer/src/main.tsx
index cf991fc9c95e55439d28f008799856e676bc87eb..8042f52c1a25adcb4f357ecf9d1529f6807cedad 100644
--- a/apps/dataset_viewer/src/main.tsx
+++ b/apps/dataset_viewer/src/main.tsx
@@ -1,6 +1,9 @@
+import App from 'App';
 import React from 'react';
 import ReactDOM from 'react-dom/client';
 
 ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
-  <React.StrictMode></React.StrictMode>,
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>,
 );
diff --git a/apps/dataset_viewer/src/standalone/InvestigationDatasets.tsx b/apps/dataset_viewer/src/standalone/InvestigationDatasets.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8d276531a3ce737854610fb0b4707b6885cc9cbf
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/InvestigationDatasets.tsx
@@ -0,0 +1,33 @@
+import { usePath, useViewers } from '@edata-portal/core';
+import {
+  useGetEndpoint,
+  INVESTIGATION_LIST_ENDPOINT,
+} from '@edata-portal/icat-plus-api';
+import { Alert } from 'react-bootstrap';
+
+/**
+ * Fetches and displays the datasets related to a specific investigation.
+ *
+ * This component:
+ * - Retrieves the `investigationId` from the URL path.
+ * - Fetches the corresponding investigation using `useGetEndpoint`.
+ * - Renders an alert if no investigation is found.
+ * - Uses `viewInvestigation` from `useViewers()` to render the investigation details.
+ *
+ * @returns {JSX.Element} Investigation details or an error alert.
+ */
+export default function InvestigationDatasets(): JSX.Element {
+  const investigationId = usePath('investigationId');
+
+  const investigations = useGetEndpoint({
+    endpoint: INVESTIGATION_LIST_ENDPOINT,
+    params: { ids: investigationId },
+  });
+  const { viewInvestigation } = useViewers();
+
+  if (!investigations || investigations.length === 0) {
+    return <Alert variant="danger">Could not find investigation</Alert>;
+  }
+
+  return viewInvestigation(investigations[0]);
+}
diff --git a/apps/dataset_viewer/src/standalone/InvestigationForm.tsx b/apps/dataset_viewer/src/standalone/InvestigationForm.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8f3273c4d761f42847eb4bd42197546e5b8aad96
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/InvestigationForm.tsx
@@ -0,0 +1,87 @@
+import { useState } from 'react';
+import { Container, Row, Col, Form, Button, ListGroup } from 'react-bootstrap';
+
+interface Investigation {
+  investigationId: string;
+  groupedBySample: boolean;
+  name: string;
+  description: string;
+}
+
+interface InvestigationFormProps {
+  investigations?: Investigation[];
+}
+
+export default function InvestigationForm({
+  investigations = [],
+}: InvestigationFormProps) {
+  const [investigationId, setInvestigationId] = useState('');
+  const [groupBySample, setGroupBySample] = useState(false);
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+    if (!investigationId) return;
+    const url = `/investigation/${investigationId}?filters=&groupBySample=${groupBySample}`;
+    window.open(url, '_blank');
+  };
+
+  const handleQuickJump = (id: string, grouped: boolean) => {
+    const url = `/investigation/${id}?filters=&groupBySample=${grouped}`;
+    window.open(url, '_blank');
+  };
+
+  return (
+    <Container className="vh-100 d-flex">
+      <Row className="w-100">
+        <Col md={3} className="d-flex flex-column align-items-start">
+          <Form
+            onSubmit={handleSubmit}
+            className="p-4 bg-light border rounded shadow-sm w-100"
+          >
+            <h2 className="mb-3">Jump to</h2>
+            <Form.Group className="mb-3">
+              <Form.Control
+                type="text"
+                placeholder="Enter Investigation ID"
+                value={investigationId}
+                onChange={(e) => setInvestigationId(e.target.value)}
+              />
+            </Form.Group>
+            <Form.Group className="mb-3" controlId="groupBySample">
+              <Form.Check
+                type="checkbox"
+                label="Group by Sample"
+                checked={groupBySample}
+                onChange={() => setGroupBySample(!groupBySample)}
+              />
+            </Form.Group>
+            <Button type="submit" variant="primary" className="w-100">
+              Go
+            </Button>
+          </Form>
+        </Col>
+
+        <Col md={6} className="d-flex flex-column align-items-center">
+          <h3 className="mb-3">Quick Access</h3>
+          <ListGroup className="w-100">
+            {investigations.map(
+              ({ investigationId, groupedBySample, name, description }) => (
+                <ListGroup.Item
+                  key={investigationId}
+                  action
+                  onClick={() =>
+                    handleQuickJump(investigationId, groupedBySample)
+                  }
+                >
+                  <h5 className="mb-1">{name}</h5>
+                  <p className="mb-1">{description}</p>
+                  <small>{`Investigation ${investigationId} - Group by Sample: ${groupedBySample}`}</small>
+                </ListGroup.Item>
+              ),
+            )}
+          </ListGroup>
+        </Col>
+      </Row>
+    </Container>
+  );
+}
diff --git a/apps/dataset_viewer/src/standalone/providers/ConfigProvider.tsx b/apps/dataset_viewer/src/standalone/providers/ConfigProvider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f432065ce47a5d23561909ff0647f135248c0b45
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/providers/ConfigProvider.tsx
@@ -0,0 +1,48 @@
+import { ConfigContext } from '@edata-portal/core';
+import { useSuspenseQueries } from '@tanstack/react-query';
+import { useMemo } from 'react';
+
+export function ConfigProvider({ children }: { children: React.ReactNode }) {
+  const [api, ui, techniques] = useSuspenseQueries({
+    queries: [
+      {
+        queryKey: ['api.config'],
+        queryFn: async () => {
+          const response = await fetch('../config/api.config.json');
+          return await response.json();
+        },
+      },
+      {
+        queryKey: ['ui.config'],
+        queryFn: async () => {
+          const response = await fetch('../config/ui.config.json');
+          return await response.json();
+        },
+      },
+      {
+        queryKey: ['techniques.config'],
+        queryFn: async () => {
+          return [
+            {
+              name: 'Crystallography',
+              shortname: 'MX',
+            },
+          ];
+        },
+      },
+    ],
+  });
+
+  const value = useMemo(
+    () => ({
+      api: api.data,
+      ui: ui.data,
+      techniques: techniques.data,
+    }),
+    [api.data, techniques.data, ui.data],
+  );
+
+  return (
+    <ConfigContext.Provider value={value}>{children}</ConfigContext.Provider>
+  );
+}
diff --git a/apps/dataset_viewer/src/standalone/providers/ViewersProvider.tsx b/apps/dataset_viewer/src/standalone/providers/ViewersProvider.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c742b52e1f5057583d027a54c6ee578658298e7e
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/providers/ViewersProvider.tsx
@@ -0,0 +1,33 @@
+import { ViewersContext } from '@edata-portal/core';
+import { DatasetViewer } from 'standalone/viewers/DatasetViewer';
+import GenericInvestigationViewer from 'standalone/viewers/GenericInvestigationViewer';
+import { SampleViewer } from 'standalone/viewers/SampleViewer';
+
+export function ViewersProvider({ children }: { children: React.ReactNode }) {
+  return (
+    <ViewersContext.Provider
+      value={{
+        viewSample: (sample, props) => (
+          <SampleViewer key={sample.id} sample={sample} props={props} />
+        ),
+        viewDataset: (dataset, type, props) => (
+          <DatasetViewer
+            key={dataset.id}
+            dataset={dataset}
+            type={type}
+            props={props}
+          />
+        ),
+        viewInvestigation: (investigation, props) => (
+          <GenericInvestigationViewer
+            key={investigation.id}
+            investigation={investigation}
+            {...props}
+          />
+        ),
+      }}
+    >
+      {children}
+    </ViewersContext.Provider>
+  );
+}
diff --git a/apps/dataset_viewer/src/standalone/routing/AppRouter.tsx b/apps/dataset_viewer/src/standalone/routing/AppRouter.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..0a661bfcc59f4aa46678b6feda5febaf6c311701
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/routing/AppRouter.tsx
@@ -0,0 +1,18 @@
+import { useMemo } from 'react';
+import { createBrowserRouter, RouterProvider } from 'react-router-dom';
+import { routes } from 'standalone/routing/Routes';
+
+export function AppRouter({ children }: { children: JSX.Element }) {
+  const router = useMemo(() => {
+    return createBrowserRouter([
+      {
+        element: <>{children}</>,
+        children: routes,
+        id: 'home',
+        handle: { breadcrumb: 'Home' },
+      },
+    ]);
+  }, [children]);
+
+  return <RouterProvider router={router} />;
+}
diff --git a/apps/dataset_viewer/src/standalone/routing/Navigation.tsx b/apps/dataset_viewer/src/standalone/routing/Navigation.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..df81786783e3d283c6607b8e6879b690fe7e92ab
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/routing/Navigation.tsx
@@ -0,0 +1,38 @@
+import { Container } from 'react-bootstrap';
+import { Outlet } from 'react-router-dom';
+import { SideNavRenderer } from '@edata-portal/core';
+
+export function Navigation() {
+  return (
+    <div
+      style={{
+        position: 'fixed',
+        top: 0,
+        left: 0,
+        right: 0,
+        bottom: 0,
+        display: 'flex',
+        flexDirection: 'column',
+        overflow: 'hidden',
+      }}
+    >
+      <Container
+        style={{
+          paddingTop: 20,
+          overflow: 'auto',
+          height: '100%',
+          position: 'relative',
+          display: 'flex',
+          flexDirection: 'column',
+          justifyContent: 'space-between',
+        }}
+        fluid
+        className="main"
+      >
+        <SideNavRenderer>
+          <Outlet />
+        </SideNavRenderer>
+      </Container>
+    </div>
+  );
+}
diff --git a/apps/dataset_viewer/src/standalone/routing/Routes.tsx b/apps/dataset_viewer/src/standalone/routing/Routes.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..2a0e079fd0a1c190ddb9d35e8904633f4d44386d
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/routing/Routes.tsx
@@ -0,0 +1,50 @@
+import InvestigationDatasets from 'standalone/InvestigationDatasets';
+import type { RouteObject } from 'react-router-dom';
+
+import InvestigationForm from 'standalone/InvestigationForm';
+
+export const routes: RouteObject[] = [
+  {
+    path: '/investigation/:investigationId',
+    element: <InvestigationDatasets></InvestigationDatasets>,
+  },
+  {
+    path: '*',
+    element: (
+      <InvestigationForm
+        investigations={[
+          {
+            name: 'BioSAXS Sample changer',
+            investigationId: '2010314049',
+            groupedBySample: true,
+            description: 'Sample Changer experiment',
+          },
+          {
+            name: 'BioSAXS Sample HPLC',
+            investigationId: '2010314049',
+            groupedBySample: true,
+            description: 'HPLC experiment',
+          },
+          {
+            name: 'TOMO',
+            investigationId: '2103286457',
+            groupedBySample: true,
+            description: 'Tomography dataset',
+          },
+          {
+            name: 'XAS',
+            investigationId: '1921857835',
+            groupedBySample: false,
+            description: 'XAS dataset',
+          },
+          {
+            name: 'HTXRPD',
+            investigationId: '2052360584',
+            groupedBySample: false,
+            description: 'HTXRPD dataset',
+          },
+        ]}
+      />
+    ),
+  },
+];
diff --git a/apps/dataset_viewer/src/standalone/scss/colors.scss b/apps/dataset_viewer/src/standalone/scss/colors.scss
new file mode 100644
index 0000000000000000000000000000000000000000..210d6faf4d6d5865d5c55e09b542a71294ab1b0a
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/scss/colors.scss
@@ -0,0 +1,29 @@
+// custom theme colors
+
+$primary: #2c3e50;
+$secondary: #456c74;
+$success: #18bc9c;
+$info: #108cdf;
+$warning: #f39c12;
+$danger: #e74c3c;
+$light: #ecf0f1;
+$dark: #222222;
+
+$dataset-raw: #b4ccd1;
+$dataset-processed: #f9e69e;
+$sample: #4d3d5f;
+
+:root {
+  --primary: #{$primary};
+  --secondary: #{$secondary};
+  --success: #{$success};
+  --info: #{$info};
+  --warning: #{$warning};
+  --danger: #{$danger};
+  --light: #{$light};
+  --dark: #{$dark};
+
+  --dataset-raw: #{$dataset-raw};
+  --dataset-processed: #{$dataset-processed};
+  --sample: #{$sample};
+}
diff --git a/apps/dataset_viewer/src/standalone/scss/images/esrf-white.png b/apps/dataset_viewer/src/standalone/scss/images/esrf-white.png
new file mode 100644
index 0000000000000000000000000000000000000000..a3c925198bc7246316308878f9ffda7079adc867
Binary files /dev/null and b/apps/dataset_viewer/src/standalone/scss/images/esrf-white.png differ
diff --git a/apps/dataset_viewer/src/standalone/scss/main.scss b/apps/dataset_viewer/src/standalone/scss/main.scss
new file mode 100644
index 0000000000000000000000000000000000000000..7c94a23909253e29305d0ecf0f920abde601c1bf
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/scss/main.scss
@@ -0,0 +1,60 @@
+@import 'bootswatch/dist/flatly/variables';
+@import './colors.scss';
+@import 'bootstrap/scss/bootstrap';
+
+$web-font-path: false; //disable loading of web fonts
+@import 'bootswatch/dist/flatly/bootswatch';
+
+body {
+  font-size: 0.9em;
+}
+
+.navbar-brand {
+  background: url('images/esrf-white.png') no-repeat;
+  background-size: contain;
+  padding-left: 70px;
+}
+
+.breadcrumb {
+  margin: 0px;
+  padding: 0px;
+}
+
+.table {
+  --bs-table-bg: 'none';
+}
+
+.monospace {
+  font-family: monospace;
+}
+
+.bg-dataset-raw {
+  background-color: $dataset-raw;
+  color: color-contrast($dataset-raw);
+  a {
+    color: color-contrast($dataset-raw);
+  }
+}
+
+.bg-dataset-processed {
+  background-color: $dataset-processed;
+  color: color-contrast($dataset-processed);
+  a {
+    color: color-contrast($dataset-processed);
+  }
+}
+
+.bg-sample {
+  background-color: $sample;
+  color: color-contrast($sample);
+  a {
+    color: color-contrast($sample);
+  }
+}
+
+.bg-secondary {
+  color: color-contrast($secondary);
+  a {
+    color: color-contrast($secondary);
+  }
+}
diff --git a/apps/dataset_viewer/src/standalone/viewers/ComponentMap.tsx b/apps/dataset_viewer/src/standalone/viewers/ComponentMap.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6af09f80319c67f8d7ec692444fae113ce1fa111
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/viewers/ComponentMap.tsx
@@ -0,0 +1,34 @@
+import React from 'react';
+
+export const COMPONENT_MAP: Record<
+  string,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  React.LazyExoticComponent<React.ComponentType<any>>
+> = {
+  SAXSSampleChangerDatasetDetails: React.lazy(
+    () => import('components/technique/saxs/SAXSSampleChangerDatasetDetails'),
+  ),
+  SAXSSampleChangerDatasetSnapshot: React.lazy(
+    () => import('components/technique/saxs/SAXSSampleChangerDatasetSnapshot'),
+  ),
+  SAXSHPLCDatasetSnapshot: React.lazy(
+    () => import('components/technique/saxs/SAXSHPLCDatasetSnapshot'),
+  ),
+  SAXSHPLCDatasetDetails: React.lazy(
+    () => import('components/technique/saxs/SAXSHPLCDatasetDetails'),
+  ),
+  HTXRPDDataset: React.lazy(
+    () => import('components/technique/htxrpd/HTXRPDDataset'),
+  ),
+  TOMODataset: React.lazy(
+    () => import('components/technique/tomo/TOMODataset'),
+  ),
+  XASDatasetDetail: React.lazy(
+    () => import('components/technique/xas/XASDatasetDetail'),
+  ),
+  XASDatasetTableCell: React.lazy(
+    () => import('components/technique/xas/XASDatasetTableCell'),
+  ),
+
+  // Add more components as needed
+};
diff --git a/apps/dataset_viewer/src/standalone/viewers/DatasetViewer.tsx b/apps/dataset_viewer/src/standalone/viewers/DatasetViewer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..61c5e8d0e4d7a5ba236438eb1d5e341ae1504016
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/viewers/DatasetViewer.tsx
@@ -0,0 +1,158 @@
+import {
+  getInstrumentNameByInvestigation,
+  DatasetViewerType,
+  first,
+  GenericDatasetDetailsViewer,
+  getDatasetParamValue,
+  PROJECT_NAME_PARAM,
+  DEFINITION_PARAM,
+} from '@edata-portal/core';
+
+import type { Dataset } from '@edata-portal/icat-plus-api';
+import { useSuspenseQuery } from '@tanstack/react-query';
+import React, { FC } from 'react';
+import { useEffect, useMemo } from 'react';
+import { Alert } from 'react-bootstrap';
+import { COMPONENT_MAP } from 'standalone/viewers/ComponentMap';
+import {
+  DatasetViewerDefinition,
+  RENDER_SCHEMA,
+} from 'standalone/viewers/RenderSchema';
+import {
+  useFilterViewers,
+  validateViewers,
+} from 'standalone/viewers/viewerDefinition';
+
+function useViewer(
+  dataset?: Dataset,
+  viewers?: DatasetViewerDefinition[],
+  type?: DatasetViewerType,
+) {
+  const investigation = dataset?.investigation;
+
+  const viewersWithType = useMemo(() => {
+    if (!type) return viewers;
+    return viewers?.filter((viewer) => type in viewer.render);
+  }, [viewers, type]);
+
+  const applicableViewers = useFilterViewers(
+    {
+      investigationId: investigation?.id?.toString(),
+      datasetId: dataset?.id?.toString(),
+      beamline: investigation
+        ? getInstrumentNameByInvestigation(investigation)
+        : undefined,
+      date: dataset?.startDate,
+      datasetParameters: dataset?.parameters,
+      technique: dataset
+        ? getDatasetParamValue(dataset, DEFINITION_PARAM)
+        : undefined,
+      project: dataset
+        ? getDatasetParamValue(dataset, PROJECT_NAME_PARAM)
+        : undefined,
+    },
+    viewersWithType,
+  );
+
+  return first(applicableViewers);
+}
+
+interface DynamicComponentProps {
+  componentName: string;
+  dataset: unknown;
+  [key: string]: unknown;
+}
+/**
+ * DynamicComponent dynamically loads and renders a React component based on the given `componentName`.
+ *
+ * The component is lazily imported using `React.lazy()`, allowing for code-splitting.
+ * If the specified component is not found in `componentMap`, an error message is displayed.
+ *
+ * @param {string} componentName - The name of the component to be rendered. Must match a key in `componentMap`.
+ * @param {any} dataset - Data to be passed as a prop to the dynamically loaded component.
+ * @param {Object} props - Additional props to pass to the dynamically loaded component.
+ * @returns {JSX.Element} - The dynamically loaded component wrapped in `React.Suspense` or an error message if not found.
+ */
+const DynamicComponent: FC<DynamicComponentProps> = (
+  props: DynamicComponentProps,
+) => {
+  const { componentName, dataset, ...restProps } = props;
+  const Component = COMPONENT_MAP[componentName];
+
+  if (!Component) {
+    return <div>Error: Component '{componentName}' not found.</div>;
+  }
+
+  return (
+    <React.Suspense fallback={<div>Loading...</div>}>
+      <Component dataset={dataset} {...restProps} />
+    </React.Suspense>
+  );
+};
+
+export function DatasetViewer({
+  dataset,
+  props,
+  type,
+}: {
+  dataset: Dataset;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  props?: any;
+  type: DatasetViewerType;
+}) {
+  const { data: config } = useSuspenseQuery({
+    queryKey: ['dataset.viewer.config'],
+    queryFn: async () => {
+      const response = await fetch('/config/dataset.viewer.config.json');
+      return (await response.json()) as DatasetViewerDefinition[];
+    },
+  });
+
+  useEffect(() => {
+    if (config) {
+      validateViewers({
+        renderSchema: RENDER_SCHEMA,
+        viewers: config,
+      });
+    }
+  }, [config]);
+
+  const viewer = useViewer(dataset, config, type);
+  // This forces the display of the viewer despite its remote configuration
+  if (type === 'generic') {
+    return <GenericDatasetDetailsViewer dataset={dataset} {...props} />;
+  }
+
+  //default viewer
+  if (type === 'details') {
+    return (
+      <DynamicComponent
+        componentName={viewer?.render.details?.remote.component || ''}
+        dataset={dataset}
+        someProp="value"
+      />
+    );
+    //return <GenericDatasetDetailsViewer dataset={dataset} {...props} />;
+  }
+
+  if (type === 'snapshot') {
+    return (
+      <DynamicComponent
+        componentName={viewer?.render.snapshot?.remote.component || ''}
+        dataset={dataset}
+        someProp="value"
+      />
+    );
+  }
+
+  if (type === 'tableCell') {
+    return (
+      <DynamicComponent
+        componentName={viewer?.render.tableCell?.remote.component || ''}
+        dataset={dataset}
+        someProp="value"
+      />
+    );
+  }
+  return <Alert variant="danger">No viewer found</Alert>;
+}
diff --git a/apps/dataset_viewer/src/standalone/viewers/GenericInvestigationViewer.tsx b/apps/dataset_viewer/src/standalone/viewers/GenericInvestigationViewer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..3f5218dcfbc03ac408af56be72697d1c0707dbe9
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/viewers/GenericInvestigationViewer.tsx
@@ -0,0 +1,64 @@
+import {
+  DatasetList,
+  useParam,
+  WithSideNav,
+  GenericDatasetsFilter,
+  useUserPreferences,
+} from '@edata-portal/core';
+import type { Investigation } from '@edata-portal/icat-plus-api';
+import { useMemo } from 'react';
+
+export default function GenericInvestigationViewer({
+  investigation,
+}: {
+  investigation: Investigation;
+}) {
+  const [filters, setFilters] = useParam<string>('filters', '');
+  const [isGroupedBySample, setIsGroupedBySample] = useUserPreferences(
+    'isGroupedBySample',
+    true,
+  );
+
+  const [sampleChecked, setSampleChecked] = useParam<string>(
+    'groupBySample',
+    isGroupedBySample.toString(),
+  );
+
+  const updateSampleChecked = (checked: string) => {
+    setSampleChecked(checked);
+    setIsGroupedBySample(checked === 'true');
+  };
+
+  const [sampleId, setSampleId] = useParam<string>('sampleId', '');
+  const [search, setSearch] = useParam<string>('search', '');
+  const groupBy = useMemo(() => {
+    return sampleChecked === 'true' ? 'sample' : 'dataset';
+  }, [sampleChecked]);
+
+  return (
+    <WithSideNav
+      sideNav={
+        <GenericDatasetsFilter
+          filters={filters}
+          setFilters={setFilters}
+          investigation={investigation}
+          setSampleId={setSampleId}
+          selectedSampleId={Number(sampleId)}
+          sampleChecked={sampleChecked}
+          setSampleChecked={updateSampleChecked}
+          datasetSearch={search}
+          setDatasetSearch={setSearch}
+        />
+      }
+    >
+      <DatasetList
+        groupBy={groupBy}
+        parameterFilter={filters?.length ? filters : undefined}
+        investigationId={investigation.id.toString()}
+        sampleIds={sampleId?.length ? [sampleId] : undefined}
+        sampleId={sampleId}
+        search={groupBy === 'sample' ? '' : search}
+      />
+    </WithSideNav>
+  );
+}
diff --git a/apps/dataset_viewer/src/standalone/viewers/GenericSampleDatasetGraph.tsx b/apps/dataset_viewer/src/standalone/viewers/GenericSampleDatasetGraph.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..bdaab9e835e463d9c570b657a19ae3e356a6d12a
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/viewers/GenericSampleDatasetGraph.tsx
@@ -0,0 +1,92 @@
+import {
+  GridGraphDefinition,
+  GridGraphItem,
+  GridGraphRender,
+  HorizontalScroll,
+  SampleWithDatasetGroups,
+  first,
+  sortDatasetsByDate,
+} from '@edata-portal/core';
+import type { Dataset, Sample } from '@edata-portal/icat-plus-api';
+import { useMemo } from 'react';
+
+export default function GenericSampleDatasetGraph({
+  sample,
+  paginationSize,
+}: {
+  sample: Sample;
+  paginationSize: number;
+}) {
+  return (
+    <SampleWithDatasetGroups
+      sample={sample}
+      computeGroups={computeGroupsDataset}
+      renderGroup={(group) => <RenderDatasetGroup group={group} />}
+      paginationSize={paginationSize}
+    />
+  );
+}
+
+function computeGroupsDataset(datasets: Dataset[]) {
+  return datasets.map((dataset) => [dataset]);
+}
+
+function RenderDatasetGroup({ group }: { group: Dataset[] }) {
+  const grid = useMemo(() => computeGridGraph(group), [group]);
+  return (
+    <HorizontalScroll>
+      <GridGraphRender grid={grid} />
+    </HorizontalScroll>
+  );
+}
+
+function computeGridGraph(datasets: Dataset[]): GridGraphDefinition {
+  return {
+    root: getDatasetsGrid(datasets),
+  };
+}
+
+function getDatasetsGrid(datasets: Dataset[]): GridGraphItem {
+  const firstDataset = first(datasets);
+
+  return {
+    id: (firstDataset?.id || 'empty') + '-list-column',
+    type: 'column',
+    items: datasets.map(getDatasetGrid),
+    collapsible: false,
+  };
+}
+
+function getDatasetGrid(dataset: Dataset): GridGraphItem {
+  const processed = sortDatasetsByDate(dataset.outputDatasets || []);
+
+  const datasetCell: GridGraphItem = {
+    type: 'cell',
+    id: dataset.id.toString(),
+    content: { type: 'dataset', dataset },
+  };
+
+  if (!processed?.length) return datasetCell;
+
+  if (processed.length >= 2)
+    return {
+      type: 'row',
+      id: dataset.id.toString() + '-processed-row',
+      items: [datasetCell, ...processed.map(getDatasetGrid)],
+    };
+
+  return {
+    type: 'row',
+    id: dataset.id.toString() + '-processed-row',
+    items: [
+      datasetCell,
+      {
+        id: dataset.id.toString() + '-processed-column',
+        type: 'column',
+        items: processed.map(getDatasetGrid),
+        collapsible: true,
+        collapsedLabel: 'Processed',
+      },
+    ],
+  };
+}
diff --git a/apps/dataset_viewer/src/standalone/viewers/GenericSampleViewer.tsx b/apps/dataset_viewer/src/standalone/viewers/GenericSampleViewer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1667edab83ebca7cd3cfcc45fafc6e5080288c63
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/viewers/GenericSampleViewer.tsx
@@ -0,0 +1,6 @@
+import { SampleHeaderWithOutputs } from '@edata-portal/core';
+import type { Sample } from '@edata-portal/icat-plus-api';
+
+export default function GenericSampleViewer({ sample }: { sample: Sample }) {
+  return <SampleHeaderWithOutputs sample={sample} />;
+}
diff --git a/apps/dataset_viewer/src/standalone/viewers/RenderSchema.tsx b/apps/dataset_viewer/src/standalone/viewers/RenderSchema.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ebb3bf040e057dd415d23211816e287aa2e0fb6d
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/viewers/RenderSchema.tsx
@@ -0,0 +1,64 @@
+import { DATASET_VIEWER_TYPES, DatasetViewerType } from '@edata-portal/core';
+import { JSONSchemaType } from 'ajv';
+
+export type GenericViewerDefinition<T> = {
+  date?: string;
+  beamline?: string | string[];
+  projectName?: string;
+  technique?: string;
+  datasetParameter?: { [key: string]: string | string[] | undefined | null };
+  render: T;
+};
+
+export type DatasetViewerDefinition =
+  GenericViewerDefinition<DatasetViewerRender>;
+
+export type DatasetViewerRender = {
+  [K in DatasetViewerType]?: {
+    type: 'remote';
+    remote: RemoteDefinition;
+  };
+};
+
+export type RemoteDefinition = {
+  name: string;
+  component: string;
+};
+
+export const REMOTE_DEFINITION_SCHEMA: JSONSchemaType<RemoteDefinition> = {
+  type: 'object',
+  properties: {
+    name: { type: 'string', nullable: false },
+    component: { type: 'string', nullable: false },
+  },
+  required: ['name', 'component'],
+  additionalProperties: false,
+  nullable: true,
+};
+export const LOCAL_DEFINITION_SCHEMA: JSONSchemaType<RemoteDefinition> = {
+  type: 'object',
+  properties: {
+    name: { type: 'string', nullable: false },
+    component: { type: 'string', nullable: false },
+  },
+  required: ['name', 'component'],
+  additionalProperties: false,
+  nullable: true,
+};
+
+export const RENDER_SCHEMA: JSONSchemaType<DatasetViewerRender> = {
+  type: 'object',
+  properties: DATASET_VIEWER_TYPES.reduce((acc, type) => {
+    acc[type] = {
+      type: 'object',
+      properties: {
+        type: { type: 'string', enum: ['remote', 'local'], nullable: false },
+        remote: REMOTE_DEFINITION_SCHEMA,
+      },
+      required: ['type', 'remote'],
+      additionalProperties: false,
+    };
+    return acc;
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  }, {} as any),
+};
diff --git a/apps/dataset_viewer/src/standalone/viewers/SampleViewer.tsx b/apps/dataset_viewer/src/standalone/viewers/SampleViewer.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..a5036d46825a2488ed329a78b9e0e4eebc18c1db
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/viewers/SampleViewer.tsx
@@ -0,0 +1,106 @@
+import type { Sample } from '@edata-portal/icat-plus-api';
+import { first, getInstrumentNameByInvestigation } from '@edata-portal/core';
+import { useSuspenseQuery } from '@tanstack/react-query';
+import { useEffect } from 'react';
+import type { JSONSchemaType } from 'ajv';
+import {
+  useFilterViewers,
+  validateViewers,
+} from 'standalone/viewers/viewerDefinition';
+import GenericSampleViewer from 'standalone/viewers/GenericSampleViewer';
+import GenericSampleDatasetGraph from 'standalone/viewers/GenericSampleDatasetGraph';
+import { GenericViewerDefinition } from 'standalone/viewers/RenderSchema';
+
+export type SampleViewerDefinition =
+  GenericViewerDefinition<SampleViewerRender>;
+
+export type SampleViewerRender = {
+  type: 'generic';
+  format: 'list' | 'graph';
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  props?: any;
+};
+
+const RENDER_SCHEMA: JSONSchemaType<SampleViewerRender> = {
+  anyOf: [
+    {
+      type: 'object',
+      properties: {
+        type: { type: 'string', enum: ['generic'], nullable: false },
+        format: { type: 'string', enum: ['list', 'graph'], nullable: false },
+        props: { type: 'object', nullable: true },
+      },
+      required: ['type', 'format'],
+    },
+  ],
+};
+
+function useViewer(sample?: Sample, viewers?: SampleViewerDefinition[]) {
+  const investigation = sample?.investigation;
+
+  const applicableViewers = useFilterViewers(
+    {
+      investigationId: sample?.investigation?.id?.toString(),
+      beamline: investigation
+        ? getInstrumentNameByInvestigation(investigation)
+        : undefined,
+      date: sample?.investigation?.startDate,
+      sampleId: sample?.id?.toString(),
+    },
+    viewers,
+  );
+
+  return first(applicableViewers);
+}
+
+export function SampleViewer({
+  sample,
+  props,
+}: {
+  sample: Sample;
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  props?: any;
+}) {
+  const { data: config } = useSuspenseQuery({
+    queryKey: ['sample.viewer.config'],
+    queryFn: async () => {
+      const response = await fetch('/config/sample.viewer.config.json');
+      return (await response.json()) as SampleViewerDefinition[];
+    },
+  });
+
+  useEffect(() => {
+    if (config) {
+      validateViewers({
+        renderSchema: RENDER_SCHEMA,
+        viewers: config,
+      });
+    }
+  }, [config]);
+
+  const viewer = useViewer(sample, config);
+
+  if (viewer?.render.type === 'generic') {
+    if (viewer.render.format === 'graph') {
+      return (
+        <GenericSampleDatasetGraph
+          sample={sample}
+          {...viewer.render.props}
+          {...props}
+        />
+      );
+    }
+    if (viewer.render.format === 'list') {
+      return (
+        <GenericSampleViewer
+          sample={sample}
+          {...viewer.render.props}
+          {...props}
+        />
+      );
+    }
+  }
+
+  //default viewer
+  return <GenericSampleViewer sample={sample} {...props} />;
+}
diff --git a/apps/dataset_viewer/src/standalone/viewers/viewerDefinition.ts b/apps/dataset_viewer/src/standalone/viewers/viewerDefinition.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ec2e3c2a96ae5c69c242a96915f21a83bd66d68b
--- /dev/null
+++ b/apps/dataset_viewer/src/standalone/viewers/viewerDefinition.ts
@@ -0,0 +1,245 @@
+import {
+  DEFINITION_PARAM,
+  parseDate,
+  PROJECT_NAME_PARAM,
+} from '@edata-portal/core';
+import {
+  Dataset,
+  DATASET_PARAMETER_VALUE_ENDPOINT,
+  DatasetParameterValuesResult,
+  Parameter,
+  useGetEndpoint,
+} from '@edata-portal/icat-plus-api';
+import Ajv, { JSONSchemaType } from 'ajv';
+import { useMemo } from 'react';
+import { GenericViewerDefinition } from 'standalone/viewers/RenderSchema';
+
+function getViewerDefinitionSchema<T>(
+  renderSchema: JSONSchemaType<T>,
+): JSONSchemaType<GenericViewerDefinition<T>> {
+  return {
+    type: 'object',
+    properties: {
+      date: { type: 'string', nullable: true },
+      beamline: {
+        type: ['string', 'array'],
+        items: { type: 'string' },
+        nullable: true,
+      },
+      projectName: { type: 'string', nullable: true },
+      technique: { type: 'string', nullable: true },
+      datasetParameter: {
+        type: 'object',
+        additionalProperties: {
+          type: ['string', 'array'],
+          items: { type: 'string' },
+          nullable: true,
+        },
+        nullable: true,
+      },
+      render: { ...renderSchema },
+    },
+    required: ['render'],
+    additionalProperties: false,
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  } as any;
+}
+
+export function validateViewers<T>({
+  renderSchema,
+  viewers,
+}: {
+  renderSchema: JSONSchemaType<T>;
+  viewers: GenericViewerDefinition<T>[];
+}) {
+  const ajv = new Ajv({
+    allowUnionTypes: true,
+  });
+  const schema = getViewerDefinitionSchema(renderSchema);
+  const validate = ajv.compile(schema);
+  viewers.forEach((viewer) => {
+    const valid = validate(viewer);
+    if (!valid) {
+      console.error(validate.errors);
+      throw new Error(
+        `Invalid sample viewer config:\n\n${ajv.errorsText(validate.errors)}\n\n${JSON.stringify(viewer)}\n\n`,
+      );
+    }
+  });
+}
+
+export function useFilterViewers<T>(
+  data: {
+    investigationId?: string;
+    sampleId?: string;
+    datasetId?: string;
+    beamline?: string;
+    date?: string;
+    datasetParameters?: Dataset['parameters'];
+    technique?: string;
+    project?: string;
+  },
+  viewers?: GenericViewerDefinition<T>[],
+) {
+  const { investigationId, sampleId, beamline, date, datasetId } = data;
+
+  const applicableViewers = useMemo(() => {
+    if (!investigationId || !beamline || !date || !viewers?.length) return [];
+    return viewers.filter((viewer) => {
+      //check beamline
+      if (viewer.beamline !== undefined) {
+        const beamlineUpper = beamline.toUpperCase();
+        if (Array.isArray(viewer.beamline)) {
+          if (
+            !viewer.beamline.map((v) => v.toUpperCase()).includes(beamlineUpper)
+          ) {
+            return false;
+          }
+        } else {
+          if (viewer.beamline.toUpperCase() !== beamlineUpper) {
+            return false;
+          }
+        }
+      }
+
+      //check date
+      if (viewer.date !== undefined) {
+        const parsedDate = parseDate(data.date);
+        const viewerDate = parseDate(viewer.date);
+        if (!viewerDate || !parsedDate) return false;
+        if (parsedDate.getTime() < viewerDate.getTime()) {
+          return false;
+        }
+      }
+
+      return true;
+    });
+  }, [investigationId, beamline, date, viewers, data.date]);
+
+  const needsTechniques = useMemo(() => {
+    if (data.technique || !investigationId || !applicableViewers?.length)
+      return false;
+    return applicableViewers.some((v) => !!v.technique);
+  }, [applicableViewers, data.technique, investigationId]);
+
+  const experimentTechniques = useGetEndpoint({
+    endpoint: DATASET_PARAMETER_VALUE_ENDPOINT,
+    params: {
+      investigationId: investigationId,
+      name: DEFINITION_PARAM,
+      datasetType: 'acquisition',
+      ...(sampleId ? { sampleId } : {}),
+    },
+    default: [] as DatasetParameterValuesResult[],
+    skipFetch: !needsTechniques,
+  });
+
+  const needsProjects = useMemo(() => {
+    if (data.project || !investigationId || !applicableViewers?.length)
+      return false;
+    return applicableViewers.some((v) => !!v.projectName);
+  }, [applicableViewers, data.project, investigationId]);
+
+  const experimentProjects = useGetEndpoint({
+    endpoint: DATASET_PARAMETER_VALUE_ENDPOINT,
+    params: {
+      investigationId: investigationId,
+      name: PROJECT_NAME_PARAM,
+      datasetType: 'acquisition',
+      ...(sampleId ? { sampleId } : {}),
+    },
+    default: [] as DatasetParameterValuesResult[],
+    skipFetch: !needsProjects,
+  });
+
+  const datasetParameterFilters = useMemo(() => {
+    if (
+      data.datasetParameters ||
+      !investigationId ||
+      !applicableViewers?.length
+    )
+      return [];
+    return applicableViewers
+      .map((v) => v.datasetParameter)
+      .flatMap((v) => Object.keys(v || {}))
+      .filter((v): v is string => !!v);
+  }, [applicableViewers, data.datasetParameters, investigationId]);
+
+  const experimentDatasetParameters = useGetEndpoint({
+    endpoint: DATASET_PARAMETER_VALUE_ENDPOINT,
+    params: {
+      investigationId: investigationId,
+      name: datasetParameterFilters.join(','),
+      datasetType: 'acquisition',
+      ...(sampleId ? { sampleId } : {}),
+    },
+    default: [] as DatasetParameterValuesResult[],
+    skipFetch: !datasetParameterFilters.length,
+  });
+
+  return useMemo(() => {
+    const projectData = data.project
+      ? [data.project]
+      : datasetId
+        ? []
+        : experimentProjects.flatMap((p) => p.values);
+    const techniqueData = data.technique
+      ? [data.technique]
+      : datasetId
+        ? []
+        : experimentTechniques.flatMap((p) => p.values);
+    const datasetParameterData =
+      data.datasetParameters || experimentDatasetParameters;
+
+    return applicableViewers.filter(
+      ({ projectName, technique, datasetParameter }) => {
+        if (
+          (projectName && !projectData.length) ||
+          (technique && !techniqueData.length) ||
+          (datasetParameter && !datasetParameterData.length)
+        ) {
+          return false;
+        }
+        return (
+          (!projectName || projectData.includes(projectName)) &&
+          (!technique || techniqueData.includes(technique)) &&
+          (!datasetParameter ||
+            checkDatasetParameters(datasetParameterData, datasetParameter))
+        );
+      },
+    );
+  }, [
+    applicableViewers,
+    data.datasetParameters,
+    data.project,
+    data.technique,
+    datasetId,
+    experimentDatasetParameters,
+    experimentProjects,
+    experimentTechniques,
+  ]);
+}
+
+function checkDatasetParameters(
+  values: DatasetParameterValuesResult[] | Parameter[],
+  datasetParameters: { [key: string]: string | string[] | undefined | null },
+) {
+  function hasValue(key: string, targetValue: string | undefined | null) {
+    return values.some((value) => {
+      if (value.name !== key) return false;
+      if (targetValue === undefined || targetValue === null) return true;
+      if ('value' in value) return value.value === targetValue;
+      if ('values' in value) return value.values.includes(targetValue);
+      return false;
+    });
+  }
+
+  for (const key in datasetParameters) {
+    const target = datasetParameters[key];
+    if (Array.isArray(target)) {
+      if (!target.some((v) => hasValue(key, v))) return false;
+    } else if (!hasValue(key, target)) return false;
+  }
+
+  return true;
+}
diff --git a/apps/dataset_viewer/tsconfig.json b/apps/dataset_viewer/tsconfig.json
index 54f4b84b0d1a9f04e7e7dc24a0f78ad19c34a379..bc6526fe14ebb2d7a79443e1c541210de3dd9f5b 100644
--- a/apps/dataset_viewer/tsconfig.json
+++ b/apps/dataset_viewer/tsconfig.json
@@ -1,6 +1,6 @@
 {
   "extends": "../../tsconfig.json",
-  "include": ["src"],
+  "include": ["src", "public/config"],
   "compilerOptions": {
     "baseUrl": "src",
     "outDir": "dist",
diff --git a/documentation/mkdocs.yml b/documentation/mkdocs.yml
index c37b7d5a8d195031c367eed9f6f9ffcb49a2e66b..7bfa99b3b638499ec0cd6030a46850996937841b 100644
--- a/documentation/mkdocs.yml
+++ b/documentation/mkdocs.yml
@@ -9,6 +9,7 @@ nav:
   - Home: index.md
   - Installation: installation.md
   - Configuration: configuration.md
+  - Development: development.md
   - Run: run.md
   - Tests: tests.md
   - Architecture: architecture.md
diff --git a/documentation/src/development.md b/documentation/src/development.md
new file mode 100644
index 0000000000000000000000000000000000000000..3f596adc37d22884227355d5d11247b67fc5f87b
--- /dev/null
+++ b/documentation/src/development.md
@@ -0,0 +1,44 @@
+# Development
+
+## Microfront ends
+
+### MX
+
+MX microntend contains all the visual components related to the display of crystalography experiments. It can be found under the folder '/apps/mx'
+
+#### Standalone mode
+
+You can run the microfrontend in standalone mode by typing:
+
+```
+pnpm prod
+```
+
+Example:
+
+```
+http://localhost:3003/investigation/1405067863
+```
+
+![Example MX](./images/mx_microfrontend_example.jpeg 'MX Microfront end')
+
+### Dataset Viewer
+
+This microfrontend contains the visual display for datasets that require dedicated visualization but are simple enough not to need a specific microfrontend
+The MX microfrontend contains all the visual components related to the display of crystallography experiments. It can be found in the '/apps/mx' folder.
+
+#### Standalone mode
+
+In order to make development easier, the data_viewer can be run in standalone mode, meaning it does not need to be compiled and run within the portal:
+
+```
+pnpm configure:standalone
+```
+
+ad then run:
+
+```
+pnpm prod
+```
+
+Once it is running, a welcome page is available, and you can enter the investigation you would like it to display. Example: http://localhost:3005
diff --git a/documentation/src/images/mx_microfrontend_example.jpeg b/documentation/src/images/mx_microfrontend_example.jpeg
new file mode 100644
index 0000000000000000000000000000000000000000..34dd6231456837694008ffa9093722dabf7ae1ed
Binary files /dev/null and b/documentation/src/images/mx_microfrontend_example.jpeg differ
diff --git a/package-lock.json b/package-lock.json
index 9b30f7b89b8b5831a16e506326853f15370b671f..27c354fd223cae96cc8f1a3815a4ab0f0649e357 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -6,12 +6,11 @@
     "": {
       "dependencies": {
         "eslint": "^8.57.0",
-        "mathjs": "^14.2.0",
         "prettier": "^3.3.3",
         "typescript": "5.5.3"
       },
       "devDependencies": {
-        "@edata-portal/icat-plus-api": "^1.8.12",
+        "@edata-portal/icat-plus-api": "^1.8.13",
         "@tanstack/react-query": "^5.52.1",
         "cypress": "^13.13.3",
         "dotenv": "^16.4.5",
@@ -52,7 +51,7 @@
     },
     "../../.local/share/pnpm/global/5/.pnpm/@edata-portal+icat-plus-api@1.8.12_@tanstack+react-query@5.52.1_react@18.3.1__react-dom@18.3._duqejkry636hus5cftbbjagihq/node_modules/@edata-portal/icat-plus-api": {
       "version": "1.8.12",
-      "dev": true,
+      "extraneous": true,
       "dependencies": {
         "date-fns": "^3.6.0"
       },
@@ -1304,148 +1303,42 @@
         "node": ">=12.0.0"
       }
     },
-    "node_modules/@babel/runtime": {
-      "version": "7.26.7",
-      "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
-      "integrity": "sha512-AOPI3D+a8dXnja+iwsUqGRjr1BbZIe771sXdapOtYI531gSqpi92vXivKcq2asu/DFpdl1ceFAKZyRzK2PCVcQ==",
-      "license": "MIT",
-      "dependencies": {
-        "regenerator-runtime": "^0.14.0"
-      },
-      "engines": {
-        "node": ">=6.9.0"
-      }
-    },
     "node_modules/@edata-portal/icat-plus-api": {
-      "resolved": "../../.local/share/pnpm/global/5/.pnpm/@edata-portal+icat-plus-api@1.8.12_@tanstack+react-query@5.52.1_react@18.3.1__react-dom@18.3._duqejkry636hus5cftbbjagihq/node_modules/@edata-portal/icat-plus-api",
-      "link": true
-    },
-    "node_modules/@lambdatest/node-tunnel": {
-      "version": "4.0.8",
-      "resolved": "https://registry.npmjs.org/@lambdatest/node-tunnel/-/node-tunnel-4.0.8.tgz",
-      "integrity": "sha512-IY42aDD4Ryqjug9V4wpCjckKpHjC2zrU/XhhorR5ztX088XITRFKUo8U6+gOjy/V8kAB+EgDuIXfK0izXbt9Ow==",
-      "license": "ISC",
+      "version": "1.8.15",
+      "resolved": "https://registry.npmjs.org/@edata-portal/icat-plus-api/-/icat-plus-api-1.8.15.tgz",
+      "integrity": "sha512-0gO/NpNUTCDlhtAmxKBvWJ255UrKnpNaOq31hIKpj2QaU3FF8YlRc/BpKt8daPBRnYCzvK5nq4rxXBWjkMbf4A==",
+      "dev": true,
       "dependencies": {
-        "adm-zip": "^0.5.10",
-        "axios": "^1.6.2",
-        "get-port": "^1.0.0",
-        "https-proxy-agent": "^5.0.0",
-        "split": "^1.0.1"
+        "date-fns": "^3.6.0"
+      },
+      "peerDependencies": {
+        "@tanstack/react-query": "^5.28.9",
+        "react": "^18.2.0",
+        "react-dom": "^18.2.0"
       }
     },
     "node_modules/@tanstack/react-query": {
       "resolved": "../../.local/share/pnpm/global/5/.pnpm/@tanstack+react-query@5.52.1_react@18.3.1/node_modules/@tanstack/react-query",
       "link": true
     },
-    "node_modules/adm-zip": {
-      "version": "0.5.16",
-      "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
-      "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=12.0"
-      }
-    },
-    "node_modules/agent-base": {
-      "version": "6.0.2",
-      "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
-      "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==",
-      "license": "MIT",
-      "dependencies": {
-        "debug": "4"
-      },
-      "engines": {
-        "node": ">= 6.0.0"
-      }
-    },
-    "node_modules/asynckit": {
-      "version": "0.4.0",
-      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
-      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
-      "license": "MIT"
-    },
-    "node_modules/axios": {
-      "version": "1.7.9",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
-      "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
-      "license": "MIT",
-      "dependencies": {
-        "follow-redirects": "^1.15.6",
-        "form-data": "^4.0.0",
-        "proxy-from-env": "^1.1.0"
-      }
-    },
-    "node_modules/combined-stream": {
-      "version": "1.0.8",
-      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
-      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
-      "license": "MIT",
-      "dependencies": {
-        "delayed-stream": "~1.0.0"
-      },
-      "engines": {
-        "node": ">= 0.8"
-      }
-    },
-    "node_modules/complex.js": {
-      "version": "2.4.2",
-      "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz",
-      "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==",
-      "license": "MIT",
-      "engines": {
-        "node": "*"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/rawify"
-      }
-    },
     "node_modules/cypress": {
       "resolved": "../../.local/share/pnpm/global/5/.pnpm/cypress@13.13.3/node_modules/cypress",
       "link": true
     },
-    "node_modules/debug": {
-      "version": "4.4.0",
-      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
-      "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
-      "license": "MIT",
-      "dependencies": {
-        "ms": "^2.1.3"
-      },
-      "engines": {
-        "node": ">=6.0"
-      },
-      "peerDependenciesMeta": {
-        "supports-color": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/decimal.js": {
-      "version": "10.5.0",
-      "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz",
-      "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==",
-      "license": "MIT"
-    },
-    "node_modules/delayed-stream": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
-      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
-      "license": "MIT",
-      "engines": {
-        "node": ">=0.4.0"
+    "node_modules/date-fns": {
+      "version": "3.6.0",
+      "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
+      "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
+      "dev": true,
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/kossnocorp"
       }
     },
     "node_modules/dotenv": {
       "resolved": "../../.local/share/pnpm/global/5/.pnpm/dotenv@16.4.5/node_modules/dotenv",
       "link": true
     },
-    "node_modules/escape-latex": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
-      "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==",
-      "license": "MIT"
-    },
     "node_modules/eslint": {
       "resolved": "../../.local/share/pnpm/global/5/.pnpm/eslint@8.57.0/node_modules/eslint",
       "link": true
@@ -1454,145 +1347,10 @@
       "resolved": "../../.local/share/pnpm/global/5/.pnpm/eslint-config-react-app@7.0.1_@babel+plugin-syntax-flow@7.24.7_@babel+core@7.25.2__@babel+plu_jygqjhomjmizk32rm4ampwzu24/node_modules/eslint-config-react-app",
       "link": true
     },
-    "node_modules/follow-redirects": {
-      "version": "1.15.9",
-      "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
-      "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
-      "funding": [
-        {
-          "type": "individual",
-          "url": "https://github.com/sponsors/RubenVerborgh"
-        }
-      ],
-      "license": "MIT",
-      "engines": {
-        "node": ">=4.0"
-      },
-      "peerDependenciesMeta": {
-        "debug": {
-          "optional": true
-        }
-      }
-    },
-    "node_modules/form-data": {
-      "version": "4.0.1",
-      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
-      "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
-      "license": "MIT",
-      "dependencies": {
-        "asynckit": "^0.4.0",
-        "combined-stream": "^1.0.8",
-        "mime-types": "^2.1.12"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/fraction.js": {
-      "version": "5.2.1",
-      "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.1.tgz",
-      "integrity": "sha512-Ah6t/7YCYjrPUFUFsOsRLMXAdnYM+aQwmojD2Ayb/Ezr82SwES0vuyQ8qZ3QO8n9j7W14VJuVZZet8U3bhSdQQ==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 12"
-      },
-      "funding": {
-        "type": "github",
-        "url": "https://github.com/sponsors/rawify"
-      }
-    },
-    "node_modules/get-port": {
-      "version": "1.0.0",
-      "resolved": "https://registry.npmjs.org/get-port/-/get-port-1.0.0.tgz",
-      "integrity": "sha512-vg59F3kcXBOtcIijwtdAyCxFocyv/fVkGQvw1kVGrxFO1U4SSGkGjrbASg5DN3TVekVle/jltwOjYRnZWc1YdA==",
-      "license": "MIT",
-      "bin": {
-        "get-port": "cli.js"
-      },
-      "engines": {
-        "node": ">=0.10.0"
-      }
-    },
-    "node_modules/https-proxy-agent": {
-      "version": "5.0.1",
-      "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
-      "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==",
-      "license": "MIT",
-      "dependencies": {
-        "agent-base": "6",
-        "debug": "4"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/javascript-natural-sort": {
-      "version": "0.7.1",
-      "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
-      "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
-      "license": "MIT"
-    },
-    "node_modules/mathjs": {
-      "version": "14.2.0",
-      "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.2.0.tgz",
-      "integrity": "sha512-CcJV1cQwRSrQIAAX3sWejFPUvUsQnTZYisEEuoMBw3gMDJDQzvKQlrul/vjKAbdtW7zaDzPCl04h1sf0wh41TA==",
-      "license": "Apache-2.0",
-      "dependencies": {
-        "@babel/runtime": "^7.25.7",
-        "@lambdatest/node-tunnel": "^4.0.8",
-        "complex.js": "^2.2.5",
-        "decimal.js": "^10.4.3",
-        "escape-latex": "^1.2.0",
-        "fraction.js": "^5.2.1",
-        "javascript-natural-sort": "^0.7.1",
-        "seedrandom": "^3.0.5",
-        "tiny-emitter": "^2.1.0",
-        "typed-function": "^4.2.1"
-      },
-      "bin": {
-        "mathjs": "bin/cli.js"
-      },
-      "engines": {
-        "node": ">= 18"
-      }
-    },
-    "node_modules/mime-db": {
-      "version": "1.52.0",
-      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
-      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/mime-types": {
-      "version": "2.1.35",
-      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
-      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
-      "license": "MIT",
-      "dependencies": {
-        "mime-db": "1.52.0"
-      },
-      "engines": {
-        "node": ">= 0.6"
-      }
-    },
-    "node_modules/ms": {
-      "version": "2.1.3",
-      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
-      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
-      "license": "MIT"
-    },
     "node_modules/prettier": {
       "resolved": "../../.local/share/pnpm/global/5/.pnpm/prettier@3.3.3/node_modules/prettier",
       "link": true
     },
-    "node_modules/proxy-from-env": {
-      "version": "1.1.0",
-      "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
-      "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
-      "license": "MIT"
-    },
     "node_modules/react": {
       "resolved": "../../.local/share/pnpm/global/5/.pnpm/react@18.3.1/node_modules/react",
       "link": true
@@ -1601,51 +1359,6 @@
       "resolved": "../../.local/share/pnpm/global/5/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom",
       "link": true
     },
-    "node_modules/regenerator-runtime": {
-      "version": "0.14.1",
-      "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
-      "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
-      "license": "MIT"
-    },
-    "node_modules/seedrandom": {
-      "version": "3.0.5",
-      "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
-      "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
-      "license": "MIT"
-    },
-    "node_modules/split": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz",
-      "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==",
-      "license": "MIT",
-      "dependencies": {
-        "through": "2"
-      },
-      "engines": {
-        "node": "*"
-      }
-    },
-    "node_modules/through": {
-      "version": "2.3.8",
-      "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz",
-      "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==",
-      "license": "MIT"
-    },
-    "node_modules/tiny-emitter": {
-      "version": "2.1.0",
-      "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
-      "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
-      "license": "MIT"
-    },
-    "node_modules/typed-function": {
-      "version": "4.2.1",
-      "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz",
-      "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==",
-      "license": "MIT",
-      "engines": {
-        "node": ">= 18"
-      }
-    },
     "node_modules/typescript": {
       "resolved": "../../.local/share/pnpm/global/5/.pnpm/typescript@5.5.3/node_modules/typescript",
       "link": true
diff --git a/packages/core/src/components/dataset/generic/GenericDatasetCardViewer.tsx b/packages/core/src/components/dataset/generic/GenericDatasetCardViewer.tsx
index 5add1867fe52e4883832c519ce912a80165cef65..65de3a083f6735f6f016aa952bb1752f6e357346 100644
--- a/packages/core/src/components/dataset/generic/GenericDatasetCardViewer.tsx
+++ b/packages/core/src/components/dataset/generic/GenericDatasetCardViewer.tsx
@@ -19,7 +19,6 @@ export function GenericDatasetCardViewer({
   isTableCell: boolean;
 }) {
   const viewers = useViewers();
-
   const tabs = useMemo(() => {
     if (!viewerConfig)
       return {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c9852425027b80e14969bc784309dda705972683..c30603c354ac6fa796cc6daaef0a36558103ce9a 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -153,6 +153,15 @@ importers:
       '@tanstack/react-query':
         specifier: ^5.52.1
         version: 5.52.1(react@18.3.1)
+      ajv:
+        specifier: ^8.17.1
+        version: 8.17.1
+      bootstrap:
+        specifier: ^5.3.3
+        version: 5.3.3(@popperjs/core@2.11.8)
+      bootswatch:
+        specifier: ^5.3.3
+        version: 5.3.3
       lodash:
         specifier: ^4.17.21
         version: 4.17.21